diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md index d8b9ae8f78c8..f15895e05998 100644 --- a/.github/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -14,23 +14,23 @@ appearance, race, religion, or sexual identity and orientation. Examples of behavior that contributes to creating a positive environment include: -* Being available to help other contributors. -* Being respectful of differing viewpoints and experiences. -* Focusing on teaching-oriented communication so that each contributor gets a chance to learn and grow. -* Focusing on what is best for the Oppia community. -* Gracefully accepting constructive criticism. -* Showing empathy towards other community members. -* Using welcoming and inclusive language. +- Being available to help other contributors. +- Being respectful of differing viewpoints and experiences. +- Focusing on teaching-oriented communication so that each contributor gets a chance to learn and grow. +- Focusing on what is best for the Oppia community. +- Gracefully accepting constructive criticism. +- Showing empathy towards other community members. +- Using welcoming and inclusive language. Examples of unacceptable behavior by participants include: -* The use of sexualized language or imagery and unwelcome sexual attention or +- The use of sexualized language or imagery and unwelcome sexual attention or advances. -* Trolling, insulting/derogatory comments, and personal or political attacks. -* Public or private harassment. -* Publishing others' private information, such as a physical or electronic +- Trolling, insulting/derogatory comments, and personal or political attacks. +- Public or private harassment. +- Publishing others' private information, such as a physical or electronic address, without explicit permission. -* Other conduct which could reasonably be considered inappropriate in a +- Other conduct which could reasonably be considered inappropriate in a professional setting. ## Our Responsibilities diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index dc255245dcbb..58770bf74c7d 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -4,11 +4,11 @@ There are lots of ways to help out and become an Oppia contributor, from joining To make it easier to get started, we've catalogued some of the different ways to help out. Please feel free to take a look through them, and see if any interest you: - * [Coders](https://github.com/oppia/oppia/wiki/Contributing-code-to-Oppia) - * [Instructional designers and storytellers](https://github.com/oppia/oppia/wiki/Teaching-with-Oppia) - * [UX researchers](https://github.com/oppia/oppia/wiki/Conducting-research-with-students) - * [Voice artists](https://github.com/oppia/oppia/wiki/Instructions-for-voice-artists) - * [Designers and artists](https://github.com/oppia/oppia/wiki/Contributing-to-Oppia%27s-design) +- [Coders](https://github.com/oppia/oppia/wiki/Contributing-code-to-Oppia) +- [Instructional designers and storytellers](https://github.com/oppia/oppia/wiki/Teaching-with-Oppia) +- [UX researchers](https://github.com/oppia/oppia/wiki/Conducting-research-with-students) +- [Voice artists](https://github.com/oppia/oppia/wiki/Instructions-for-voice-artists) +- [Designers and artists](https://github.com/oppia/oppia/wiki/Contributing-to-Oppia%27s-design) If you are interested in working on Oppia's Android app, you should also take a look at the [oppia/oppia-android repository](https://github.com/oppia/oppia-android). diff --git a/.github/ISSUE_TEMPLATE/4_server_error_template.md b/.github/ISSUE_TEMPLATE/4_server_error_template.md index 85d52fae6290..1e4a4d085b5a 100644 --- a/.github/ISSUE_TEMPLATE/4_server_error_template.md +++ b/.github/ISSUE_TEMPLATE/4_server_error_template.md @@ -3,6 +3,7 @@ name: Server error about: Report a production bug from the server logs labels: triage needed, server errors, bug --- + + - [ ] A testing doc has been written: ... (ADD LINK) ... - [ ] _(To be confirmed by the server admin)_ All jobs in this PR have been verified on a live server, and the PR is safe to merge. - ## Proof that changes are correct /g, '').trim(); if (!containedWidgetTagName) { @@ -162,11 +164,12 @@ export class CkEditorCopyContentService { const value = match[3].replace(/&/g, '&'); startupData[key] = JSON.parse( - this.htmlEscaperService.escapedStrToUnescapedStr(value)); + this.htmlEscaperService.escapedStrToUnescapedStr(value) + ); } startupData.isCopied = true; - editor.execCommand(widgetName, { startupData }); + editor.execCommand(widgetName, {startupData}); } } @@ -183,9 +186,7 @@ export class CkEditorCopyContentService { return; } - this.copyEventEmitter.emit( - this._handleCopy(target) - ); + this.copyEventEmitter.emit(this._handleCopy(target)); } /** @@ -203,14 +204,15 @@ export class CkEditorCopyContentService { delete this.ckEditorIdToSubscription[editor.id]; return; } - this._handlePaste( - editor, rootElement, containedWidgetTagName); + this._handlePaste(editor, rootElement, containedWidgetTagName); } ); } } -angular.module('oppia').factory( - 'CkEditorCopyContentService', - downgradeInjectable(CkEditorCopyContentService) -); +angular + .module('oppia') + .factory( + 'CkEditorCopyContentService', + downgradeInjectable(CkEditorCopyContentService) + ); diff --git a/core/templates/components/ck-editor-helpers/ck-editor-copy-toolbar/ck-editor-copy-toolbar.component.spec.ts b/core/templates/components/ck-editor-helpers/ck-editor-copy-toolbar/ck-editor-copy-toolbar.component.spec.ts index 7889158e5c4f..a9e0698c89a4 100644 --- a/core/templates/components/ck-editor-helpers/ck-editor-copy-toolbar/ck-editor-copy-toolbar.component.spec.ts +++ b/core/templates/components/ck-editor-helpers/ck-editor-copy-toolbar/ck-editor-copy-toolbar.component.spec.ts @@ -16,14 +16,13 @@ * @fileoverview Unit tests for the CkEditor copy toolbar component. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; -import { CkEditorCopyToolbarComponent } from +import { + CkEditorCopyToolbarComponent, // eslint-disable-next-line max-len - 'components/ck-editor-helpers/ck-editor-copy-toolbar/ck-editor-copy-toolbar.component'; -import { CkEditorCopyContentService } from - 'components/ck-editor-helpers/ck-editor-copy-content.service'; - +} from 'components/ck-editor-helpers/ck-editor-copy-toolbar/ck-editor-copy-toolbar.component'; +import {CkEditorCopyContentService} from 'components/ck-editor-helpers/ck-editor-copy-content.service'; describe('CkEditor copy toolbar', () => { let component: CkEditorCopyToolbarComponent; @@ -34,10 +33,12 @@ describe('CkEditor copy toolbar', () => { beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [CkEditorCopyToolbarComponent], - providers: [{ - provide: CkEditorCopyContentService, - useClass: MockCkEditorCopyContentService - }] + providers: [ + { + provide: CkEditorCopyContentService, + useClass: MockCkEditorCopyContentService, + }, + ], }).compileComponents(); })); @@ -45,8 +46,9 @@ describe('CkEditor copy toolbar', () => { fixture = TestBed.createComponent(CkEditorCopyToolbarComponent); component = fixture.componentInstance; fixture.detectChanges(); - ckCopyService = ( - fixture.debugElement.injector.get(CkEditorCopyContentService)); + ckCopyService = fixture.debugElement.injector.get( + CkEditorCopyContentService + ); }); beforeEach(() => { @@ -56,8 +58,10 @@ describe('CkEditor copy toolbar', () => { }); it('should toggle copy mode correctly', () => { - const toggleCopyModeSpy = spyOn(ckCopyService, 'toggleCopyMode') - .and.callThrough(); + const toggleCopyModeSpy = spyOn( + ckCopyService, + 'toggleCopyMode' + ).and.callThrough(); expect(ckCopyService.copyModeActive).toBe(false); diff --git a/core/templates/components/ck-editor-helpers/ck-editor-copy-toolbar/ck-editor-copy-toolbar.component.ts b/core/templates/components/ck-editor-helpers/ck-editor-copy-toolbar/ck-editor-copy-toolbar.component.ts index f75071e777eb..92cf726df1ad 100644 --- a/core/templates/components/ck-editor-helpers/ck-editor-copy-toolbar/ck-editor-copy-toolbar.component.ts +++ b/core/templates/components/ck-editor-helpers/ck-editor-copy-toolbar/ck-editor-copy-toolbar.component.ts @@ -16,22 +16,20 @@ * @fileoverview Ck editor copy toolbar component. */ -import { Component, Inject } from '@angular/core'; -import { DOCUMENT } from '@angular/common'; -import { downgradeComponent } from '@angular/upgrade/static'; - -import { CkEditorCopyContentService } from - 'components/ck-editor-helpers/ck-editor-copy-content.service'; +import {Component, Inject} from '@angular/core'; +import {DOCUMENT} from '@angular/common'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {CkEditorCopyContentService} from 'components/ck-editor-helpers/ck-editor-copy-content.service'; @Component({ selector: 'ck-editor-copy-toolbar', - templateUrl: './ck-editor-copy-toolbar.component.html' + templateUrl: './ck-editor-copy-toolbar.component.html', }) export class CkEditorCopyToolbarComponent { constructor( - public ckEditorCopyContentService: CkEditorCopyContentService, - @Inject(DOCUMENT) private document: Document + public ckEditorCopyContentService: CkEditorCopyContentService, + @Inject(DOCUMENT) private document: Document ) { ckEditorCopyContentService.copyModeActive = false; } @@ -44,12 +42,12 @@ export class CkEditorCopyToolbarComponent { if (this.ckEditorCopyContentService.copyModeActive) { this.document.body.style.cursor = 'copy'; - element.forEach((editor) => { + element.forEach(editor => { editor.focus(); }); } else { this.document.body.style.cursor = ''; - element.forEach((editor) => { + element.forEach(editor => { editor.blur(); }); } @@ -60,6 +58,9 @@ export class CkEditorCopyToolbarComponent { } } -angular.module('oppia').directive( - 'ckEditorCopyToolbar', - downgradeComponent({component: CkEditorCopyToolbarComponent})); +angular + .module('oppia') + .directive( + 'ckEditorCopyToolbar', + downgradeComponent({component: CkEditorCopyToolbarComponent}) + ); diff --git a/core/templates/components/ck-editor-helpers/ck-editor-copy-toolbar/ck-editor-copy-toolbar.module.ts b/core/templates/components/ck-editor-helpers/ck-editor-copy-toolbar/ck-editor-copy-toolbar.module.ts index 0cac61265997..d57ce3aa817e 100644 --- a/core/templates/components/ck-editor-helpers/ck-editor-copy-toolbar/ck-editor-copy-toolbar.module.ts +++ b/core/templates/components/ck-editor-helpers/ck-editor-copy-toolbar/ck-editor-copy-toolbar.module.ts @@ -19,23 +19,14 @@ import 'core-js/es7/reflect'; import 'zone.js'; -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { CkEditorCopyToolbarComponent } from './ck-editor-copy-toolbar.component'; +import {CommonModule} from '@angular/common'; +import {NgModule} from '@angular/core'; +import {CkEditorCopyToolbarComponent} from './ck-editor-copy-toolbar.component'; @NgModule({ - imports: [ - CommonModule, - ], - declarations: [ - CkEditorCopyToolbarComponent - ], - entryComponents: [ - CkEditorCopyToolbarComponent - ], - exports: [ - CkEditorCopyToolbarComponent - ], + imports: [CommonModule], + declarations: [CkEditorCopyToolbarComponent], + entryComponents: [CkEditorCopyToolbarComponent], + exports: [CkEditorCopyToolbarComponent], }) - -export class OppiaCkEditorCopyToolBarModule { } +export class OppiaCkEditorCopyToolBarModule {} diff --git a/core/templates/components/ck-editor-helpers/ckeditor4.module.ts b/core/templates/components/ck-editor-helpers/ckeditor4.module.ts index 9a1a868698a1..0b7b6b34915e 100644 --- a/core/templates/components/ck-editor-helpers/ckeditor4.module.ts +++ b/core/templates/components/ck-editor-helpers/ckeditor4.module.ts @@ -19,23 +19,14 @@ import 'core-js/es7/reflect'; import 'zone.js'; -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { CkEditor4RteComponent } from './ck-editor-4-rte.component'; +import {CommonModule} from '@angular/common'; +import {NgModule} from '@angular/core'; +import {CkEditor4RteComponent} from './ck-editor-4-rte.component'; @NgModule({ - imports: [ - CommonModule, - ], - declarations: [ - CkEditor4RteComponent - ], - entryComponents: [ - CkEditor4RteComponent - ], - exports: [ - CkEditor4RteComponent - ], + imports: [CommonModule], + declarations: [CkEditor4RteComponent], + entryComponents: [CkEditor4RteComponent], + exports: [CkEditor4RteComponent], }) - -export class OppiaCkEditor4Module { } +export class OppiaCkEditor4Module {} diff --git a/core/templates/components/code-mirror/codemirror-mergeview.component.spec.ts b/core/templates/components/code-mirror/codemirror-mergeview.component.spec.ts index 3186f693ae22..c1cfa9a2ddce 100644 --- a/core/templates/components/code-mirror/codemirror-mergeview.component.spec.ts +++ b/core/templates/components/code-mirror/codemirror-mergeview.component.spec.ts @@ -16,11 +16,11 @@ * @fileoverview Unit tests for angular code mirror wrapper. */ -import { NgZone, SimpleChanges } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import {NgZone, SimpleChanges} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import CodeMirror from 'node_modules/@types/codemirror'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { CodemirrorMergeviewComponent } from './codemirror-mergeview.component'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {CodemirrorMergeviewComponent} from './codemirror-mergeview.component'; describe('Oppia CodeMirror Component', () => { let component: CodemirrorMergeviewComponent; @@ -29,7 +29,7 @@ describe('Oppia CodeMirror Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [CodemirrorMergeviewComponent] + declarations: [CodemirrorMergeviewComponent], }).compileComponents(); })); @@ -57,7 +57,7 @@ describe('Oppia CodeMirror Component', () => { mergeViewCalled = true; }; const mockCodeMirror: typeof CodeMirror = { - MergeView: mockMergeView + MergeView: mockMergeView, } as typeof CodeMirror; window.CodeMirror = mockCodeMirror; component.ngAfterViewInit(); @@ -73,14 +73,14 @@ describe('Oppia CodeMirror Component', () => { MergeView: () => { return { editor: () => { - return { setValue: editSetValueSpy }; + return {setValue: editSetValueSpy}; }, rightOriginal: () => { - return { setValue: rightOrgSetValueSpy }; - } + return {setValue: rightOrgSetValueSpy}; + }, }; - } - } + }, + }, } as unknown as Window); component.ngAfterViewInit(); let changes: SimpleChanges = { @@ -88,8 +88,8 @@ describe('Oppia CodeMirror Component', () => { currentValue: undefined, previousValue: 'B', firstChange: false, - isFirstChange: () => false - } + isFirstChange: () => false, + }, }; component.leftValue = undefined; expect(() => component.ngOnChanges(changes)).toThrowError( @@ -100,8 +100,8 @@ describe('Oppia CodeMirror Component', () => { currentValue: undefined, previousValue: 'B', firstChange: false, - isFirstChange: () => false - } + isFirstChange: () => false, + }, }; component.rightValue = undefined; expect(() => component.ngOnChanges(changes)).toThrowError( @@ -117,14 +117,14 @@ describe('Oppia CodeMirror Component', () => { MergeView: () => { return { editor: () => { - return { setValue: editSetValueSpy }; + return {setValue: editSetValueSpy}; }, rightOriginal: () => { - return { setValue: rightOrgSetValueSpy }; - } + return {setValue: rightOrgSetValueSpy}; + }, }; - } - } + }, + }, } as unknown as Window); component.ngAfterViewInit(); const changes: SimpleChanges = { @@ -132,14 +132,14 @@ describe('Oppia CodeMirror Component', () => { currentValue: 'A', previousValue: 'B', firstChange: false, - isFirstChange: () => false + isFirstChange: () => false, }, rightValue: { currentValue: 'D', previousValue: 'C', firstChange: false, - isFirstChange: () => false - } + isFirstChange: () => false, + }, }; component.leftValue = 'A'; component.rightValue = 'D'; diff --git a/core/templates/components/code-mirror/codemirror-mergeview.component.ts b/core/templates/components/code-mirror/codemirror-mergeview.component.ts index d5b348247cf0..38ebe910ef4c 100644 --- a/core/templates/components/code-mirror/codemirror-mergeview.component.ts +++ b/core/templates/components/code-mirror/codemirror-mergeview.component.ts @@ -16,17 +16,28 @@ * @fileoverview Wrapper angular component for code mirror. */ -import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Input, NgZone, OnChanges, OnInit, SimpleChanges } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + ElementRef, + Input, + NgZone, + OnChanges, + OnInit, + SimpleChanges, +} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {WindowRef} from 'services/contextual/window-ref.service'; @Component({ selector: 'oppia-codemirror-mergeview', template: '', - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) -export class CodemirrorMergeviewComponent implements - AfterViewInit, OnInit, OnChanges { +export class CodemirrorMergeviewComponent + implements AfterViewInit, OnInit, OnChanges +{ @Input() options = {}; // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see @@ -38,7 +49,8 @@ export class CodemirrorMergeviewComponent implements constructor( private elementRef: ElementRef, private ngZone: NgZone, - private windowRef: WindowRef) { } + private windowRef: WindowRef + ) {} ngOnInit(): void { // Require CodeMirror. @@ -53,49 +65,53 @@ export class CodemirrorMergeviewComponent implements // 'value', 'orig' are initial values of left and right // pane respectively. this.ngZone.runOutsideAngular(() => { - this.codeMirrorInstance = - (this.windowRef.nativeWindow as typeof window).CodeMirror.MergeView( - this.elementRef.nativeElement, - { - value: this.leftValue !== undefined ? this.leftValue : ' ', - orig: this.rightValue !== undefined ? this.rightValue : ' ', - ...this.options - } - ); + this.codeMirrorInstance = ( + this.windowRef.nativeWindow as typeof window + ).CodeMirror.MergeView(this.elementRef.nativeElement, { + value: this.leftValue !== undefined ? this.leftValue : ' ', + orig: this.rightValue !== undefined ? this.rightValue : ' ', + ...this.options, + }); }); } ngOnChanges(changes: SimpleChanges): void { // Watch for changes and set value in left pane. - if (changes.leftValue && - changes.leftValue.currentValue !== - changes.leftValue.previousValue && - this.codeMirrorInstance) { + if ( + changes.leftValue && + changes.leftValue.currentValue !== changes.leftValue.previousValue && + this.codeMirrorInstance + ) { if (this.leftValue === undefined) { throw new Error('Left pane value is not defined.'); } this.ngZone.runOutsideAngular(() => { - this.codeMirrorInstance.editor().setValue( - changes.leftValue.currentValue); + this.codeMirrorInstance + .editor() + .setValue(changes.leftValue.currentValue); }); } // Watch for changes and set value in right pane. - if (changes.rightValue && - changes.rightValue.currentValue !== - changes.rightValue.previousValue && - this.codeMirrorInstance) { + if ( + changes.rightValue && + changes.rightValue.currentValue !== changes.rightValue.previousValue && + this.codeMirrorInstance + ) { if (this.rightValue === undefined) { throw new Error('Right pane value is not defined.'); } this.ngZone.runOutsideAngular(() => { - this.codeMirrorInstance.rightOriginal().setValue( - changes.rightValue.currentValue); + this.codeMirrorInstance + .rightOriginal() + .setValue(changes.rightValue.currentValue); }); } } } angular.module('oppia').directive( - 'oppiaCodemirrorMergeview', downgradeComponent({ - component: CodemirrorMergeviewComponent - }) as angular.IDirectiveFactory); + 'oppiaCodemirrorMergeview', + downgradeComponent({ + component: CodemirrorMergeviewComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/components/code-mirror/codemirror.component.spec.ts b/core/templates/components/code-mirror/codemirror.component.spec.ts index 8424f1846222..45c03ab6c3a3 100644 --- a/core/templates/components/code-mirror/codemirror.component.spec.ts +++ b/core/templates/components/code-mirror/codemirror.component.spec.ts @@ -16,11 +16,17 @@ * @fileoverview Unit tests for angular code mirror wrapper. */ -import { SimpleChanges } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { CodemirrorComponent } from '@ctrl/ngx-codemirror'; -import { CodeMirrorComponent } from './codemirror.component'; -import { CodeMirrorModule } from './codemirror.module'; +import {SimpleChanges} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {CodemirrorComponent} from '@ctrl/ngx-codemirror'; +import {CodeMirrorComponent} from './codemirror.component'; +import {CodeMirrorModule} from './codemirror.module'; describe('Oppia CodeMirror Component', () => { let component: CodeMirrorComponent; @@ -28,7 +34,7 @@ describe('Oppia CodeMirror Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [CodeMirrorModule] + imports: [CodeMirrorModule], }).compileComponents(); })); @@ -37,20 +43,19 @@ describe('Oppia CodeMirror Component', () => { component = fixture.componentInstance; })); - it('should throw error if CodeMirrorComponent is undefined', fakeAsync( - () => { - component.codemirrorComponent = undefined; - expect(() => { - component.ngAfterViewInit(); - tick(1); - }).toThrowError('CodeMirrorComponent not Found'); - })); + it('should throw error if CodeMirrorComponent is undefined', fakeAsync(() => { + component.codemirrorComponent = undefined; + expect(() => { + component.ngAfterViewInit(); + tick(1); + }).toThrowError('CodeMirrorComponent not Found'); + })); it('should notify that it has loaded', fakeAsync(() => { const onLoadSpy = jasmine.createSpy('onLoadSpy'); let subscription = component.onLoad.subscribe(onLoadSpy); component.codemirrorComponent = { - codemirror: {} + codemirror: {}, } as unknown as CodemirrorComponent; component.ngAfterViewInit(); tick(1); @@ -66,11 +71,11 @@ describe('Oppia CodeMirror Component', () => { subscription.unsubscribe(); })); - it ('should refresh codemirror', waitForAsync(() => { + it('should refresh codemirror', waitForAsync(() => { component.codemirror = { refresh: () => { return; - } + }, } as CodeMirror.Editor; const refreshSpy = spyOn(component.codemirror, 'refresh'); const changes: SimpleChanges = { @@ -78,8 +83,8 @@ describe('Oppia CodeMirror Component', () => { previousValue: false, currentValue: true, firstChange: false, - isFirstChange: () => false - } + isFirstChange: () => false, + }, }; component.ngOnChanges(changes); expect(refreshSpy).toHaveBeenCalled(); diff --git a/core/templates/components/code-mirror/codemirror.component.ts b/core/templates/components/code-mirror/codemirror.component.ts index c97db9efc407..7ac66213b83d 100644 --- a/core/templates/components/code-mirror/codemirror.component.ts +++ b/core/templates/components/code-mirror/codemirror.component.ts @@ -16,9 +16,18 @@ * @fileoverview Wrapper angular component for code mirror. */ -import { AfterViewInit, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, ViewChild } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { CodemirrorComponent } from '@ctrl/ngx-codemirror'; +import { + AfterViewInit, + Component, + EventEmitter, + Input, + OnChanges, + Output, + SimpleChanges, + ViewChild, +} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {CodemirrorComponent} from '@ctrl/ngx-codemirror'; interface CodeMirrorMergeViewOptions { lineNumbers: boolean; @@ -29,7 +38,7 @@ interface CodeMirrorMergeViewOptions { @Component({ selector: 'oppia-codemirror', - templateUrl: './codemirror.component.html' + templateUrl: './codemirror.component.html', }) export class CodeMirrorComponent implements AfterViewInit, OnChanges { @Input() options!: CodeMirrorMergeViewOptions; @@ -44,12 +53,13 @@ export class CodeMirrorComponent implements AfterViewInit, OnChanges { // fails to initialise the component, this can make the below // properties undefined. @ViewChild(CodemirrorComponent) codemirrorComponent: - CodemirrorComponent | undefined; + | CodemirrorComponent + | undefined; codemirror: CodeMirror.Editor | undefined; autoFocus = false; - constructor() { } + constructor() {} updateValue(val: string): void { this.value = val; @@ -72,12 +82,16 @@ export class CodeMirrorComponent implements AfterViewInit, OnChanges { if ( changes.refresh !== undefined && changes.refresh.previousValue !== changes.refresh.currentValue && - this.codemirror) { + this.codemirror + ) { this.codemirror.refresh(); } } } -angular.module('oppia').directive('oppiaCodemirror', downgradeComponent({ - component: CodeMirrorComponent -}) as angular.IDirectiveFactory); +angular.module('oppia').directive( + 'oppiaCodemirror', + downgradeComponent({ + component: CodeMirrorComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/components/code-mirror/codemirror.module.ts b/core/templates/components/code-mirror/codemirror.module.ts index 13fa574c6fae..7ce987729e70 100644 --- a/core/templates/components/code-mirror/codemirror.module.ts +++ b/core/templates/components/code-mirror/codemirror.module.ts @@ -16,32 +16,19 @@ * @fileoverview Module for the CodeRepl interaction components. */ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {NgModule} from '@angular/core'; import 'core-js/es7/reflect'; import 'zone.js'; -import { CodemirrorModule } from '@ctrl/ngx-codemirror'; -import { CodemirrorMergeviewComponent } from './codemirror-mergeview.component'; -import { CodeMirrorComponent } from './codemirror.component'; -import { FormsModule } from '@angular/forms'; +import {CodemirrorModule} from '@ctrl/ngx-codemirror'; +import {CodemirrorMergeviewComponent} from './codemirror-mergeview.component'; +import {CodeMirrorComponent} from './codemirror.component'; +import {FormsModule} from '@angular/forms'; @NgModule({ - imports: [ - CommonModule, - CodemirrorModule, - FormsModule - ], - declarations: [ - CodemirrorMergeviewComponent, - CodeMirrorComponent - ], - entryComponents: [ - CodemirrorMergeviewComponent, - CodeMirrorComponent - ], - exports: [ - CodemirrorMergeviewComponent, - CodeMirrorComponent - ] + imports: [CommonModule, CodemirrorModule, FormsModule], + declarations: [CodemirrorMergeviewComponent, CodeMirrorComponent], + entryComponents: [CodemirrorMergeviewComponent, CodeMirrorComponent], + exports: [CodemirrorMergeviewComponent, CodeMirrorComponent], }) export class CodeMirrorModule {} diff --git a/core/templates/components/common-layout-directives/common-elements/alert-message.component.spec.ts b/core/templates/components/common-layout-directives/common-elements/alert-message.component.spec.ts index b04d1ce875d4..834b25ff7b29 100644 --- a/core/templates/components/common-layout-directives/common-elements/alert-message.component.spec.ts +++ b/core/templates/components/common-layout-directives/common-elements/alert-message.component.spec.ts @@ -16,10 +16,10 @@ * @fileoverview Unit tests for WarningsAndAlertsComponent. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { ToastrService } from 'ngx-toastr'; -import { AlertsService } from 'services/alerts.service'; -import { AlertMessageComponent } from './alert-message.component'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {ToastrService} from 'ngx-toastr'; +import {AlertsService} from 'services/alerts.service'; +import {AlertMessageComponent} from './alert-message.component'; describe('Alert Message Component', () => { let fixture: ComponentFixture; @@ -34,10 +34,10 @@ describe('Alert Message Component', () => { return { then: (callb: () => void) => { callb(); - } + }, }; - } - } + }, + }, }; } @@ -48,10 +48,10 @@ describe('Alert Message Component', () => { return { then: (callb: () => void) => { callb(); - } + }, }; - } - } + }, + }, }; } } @@ -64,19 +64,17 @@ describe('Alert Message Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - AlertMessageComponent, - ], + declarations: [AlertMessageComponent], providers: [ { provide: ToastrService, - useClass: MockToastrService + useClass: MockToastrService, }, { provide: AlertsService, - useClass: MockAlertsService - } - ] + useClass: MockAlertsService, + }, + ], }).compileComponents(); })); @@ -90,12 +88,18 @@ describe('Alert Message Component', () => { }); it('should initialize', () => { - componentInstance.messageObject = { type: 'info', content: 'Test', - timeout: 0 }; + componentInstance.messageObject = { + type: 'info', + content: 'Test', + timeout: 0, + }; componentInstance.ngOnInit(); expect(numOfCalls).toEqual(1); - componentInstance.messageObject = { type: 'success', content: 'Test', - timeout: 0 }; + componentInstance.messageObject = { + type: 'success', + content: 'Test', + timeout: 0, + }; componentInstance.ngOnInit(); expect(numOfCalls).toEqual(2); }); diff --git a/core/templates/components/common-layout-directives/common-elements/alert-message.component.ts b/core/templates/components/common-layout-directives/common-elements/alert-message.component.ts index 8eb9da984ce2..5136cf084048 100644 --- a/core/templates/components/common-layout-directives/common-elements/alert-message.component.ts +++ b/core/templates/components/common-layout-directives/common-elements/alert-message.component.ts @@ -16,10 +16,10 @@ * @fileoverview Component for Alert Messages */ -import { Component, Input } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { ToastrService } from 'ngx-toastr'; -import { AlertsService } from 'services/alerts.service'; +import {Component, Input} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {ToastrService} from 'ngx-toastr'; +import {AlertsService} from 'services/alerts.service'; require('ngx-toastr/toastr.css'); export interface MessageObject { @@ -30,7 +30,7 @@ export interface MessageObject { @Component({ selector: 'oppia-alert-message', - template: '
' + template: '
', }) export class AlertMessageComponent { // These properties are initialized using Angular lifecycle hooks @@ -46,22 +46,30 @@ export class AlertMessageComponent { ngOnInit(): void { if (this.messageObject.type === 'info') { - this.toastrService.info(this.messageObject.content, '', { - timeOut: this.messageObject.timeout, - }).onHidden.toPromise().then(() => { - this.alertsService.deleteMessage(this.messageObject); - }); + this.toastrService + .info(this.messageObject.content, '', { + timeOut: this.messageObject.timeout, + }) + .onHidden.toPromise() + .then(() => { + this.alertsService.deleteMessage(this.messageObject); + }); } else if (this.messageObject.type === 'success') { - this.toastrService.success(this.messageObject.content, '', { - timeOut: this.messageObject.timeout - }).onHidden.toPromise().then(() => { - this.alertsService.deleteMessage(this.messageObject); - }); + this.toastrService + .success(this.messageObject.content, '', { + timeOut: this.messageObject.timeout, + }) + .onHidden.toPromise() + .then(() => { + this.alertsService.deleteMessage(this.messageObject); + }); } } } -angular.module('oppia').directive('oppiaAlertMessage', +angular.module('oppia').directive( + 'oppiaAlertMessage', downgradeComponent({ - component: AlertMessageComponent - }) as angular.IDirectiveFactory); + component: AlertMessageComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/components/common-layout-directives/common-elements/answer-content-modal.component.spec.ts b/core/templates/components/common-layout-directives/common-elements/answer-content-modal.component.spec.ts index 4d64fbd864e7..23f28b976c6c 100644 --- a/core/templates/components/common-layout-directives/common-elements/answer-content-modal.component.spec.ts +++ b/core/templates/components/common-layout-directives/common-elements/answer-content-modal.component.spec.ts @@ -16,11 +16,17 @@ * @fileoverview Unit tests for AnswerContentModalComponent. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed, fakeAsync, tick } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { AnswerContentModalComponent } from './answer-content-modal.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + waitForAsync, + TestBed, + fakeAsync, + tick, +} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {AnswerContentModalComponent} from './answer-content-modal.component'; class MockActiveModal { close(): void { @@ -40,16 +46,14 @@ describe('Improvement Confirmation Modal', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - AnswerContentModalComponent - ], + declarations: [AnswerContentModalComponent], providers: [ { provide: NgbActiveModal, - useClass: MockActiveModal + useClass: MockActiveModal, }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -63,13 +67,12 @@ describe('Improvement Confirmation Modal', () => { fixture.detectChanges(); }); - it('should check whether component is initialized correctly', - fakeAsync(() => { - expect(component.answerHtml).toEqual(''); + it('should check whether component is initialized correctly', fakeAsync(() => { + expect(component.answerHtml).toEqual(''); - component.close(); - tick(); + component.close(); + tick(); - expect(ngbActiveModal.close).toHaveBeenCalled(); - })); + expect(ngbActiveModal.close).toHaveBeenCalled(); + })); }); diff --git a/core/templates/components/common-layout-directives/common-elements/answer-content-modal.component.ts b/core/templates/components/common-layout-directives/common-elements/answer-content-modal.component.ts index eb6a4449f32b..446aee098bc9 100644 --- a/core/templates/components/common-layout-directives/common-elements/answer-content-modal.component.ts +++ b/core/templates/components/common-layout-directives/common-elements/answer-content-modal.component.ts @@ -16,21 +16,19 @@ * @fileoverview Component for answer content modal. */ -import { Component, Input } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from './confirm-or-cancel-modal.component'; +import {Component, Input} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from './confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-answer-content-modal', - templateUrl: './answer-content-modal.component.html' + templateUrl: './answer-content-modal.component.html', }) export class AnswerContentModalComponent extends ConfirmOrCancelModal { @Input() answerHtml: string = ''; - constructor( - private ngbActiveModal: NgbActiveModal, - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } @@ -39,7 +37,9 @@ export class AnswerContentModalComponent extends ConfirmOrCancelModal { } } -angular.module('oppia').directive('oppiaAnswerContentModal', +angular.module('oppia').directive( + 'oppiaAnswerContentModal', downgradeComponent({ - component: AnswerContentModalComponent - }) as angular.IDirectiveFactory); + component: AnswerContentModalComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/components/common-layout-directives/common-elements/attribution-guide.component.spec.ts b/core/templates/components/common-layout-directives/common-elements/attribution-guide.component.spec.ts index e51c3e9120cc..a447259a96aa 100644 --- a/core/templates/components/common-layout-directives/common-elements/attribution-guide.component.spec.ts +++ b/core/templates/components/common-layout-directives/common-elements/attribution-guide.component.spec.ts @@ -16,18 +16,17 @@ * @fileoverview Unit tests for attribution guide component. */ -import { TestBed, async, ComponentFixture } from - '@angular/core/testing'; - -import { AttributionGuideComponent } from './attribution-guide.component'; -import { ContextService } from 'services/context.service'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { UrlService } from 'services/contextual/url.service'; -import { AttributionService } from 'services/attribution.service'; -import { BrowserCheckerService } from 'domain/utilities/browser-checker.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; +import {TestBed, async, ComponentFixture} from '@angular/core/testing'; + +import {AttributionGuideComponent} from './attribution-guide.component'; +import {ContextService} from 'services/context.service'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {UrlService} from 'services/contextual/url.service'; +import {AttributionService} from 'services/attribution.service'; +import {BrowserCheckerService} from 'domain/utilities/browser-checker.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; class MockAttributionService { init() { @@ -71,7 +70,7 @@ class MockUrlService { } getCurrentLocation() { - return { href: 'localhost:8181/explore/0' }; + return {href: 'localhost:8181/explore/0'}; } } @@ -81,7 +80,7 @@ class MockContextService { } } -describe('Attribution Guide Component', function() { +describe('Attribution Guide Component', function () { let component: AttributionGuideComponent; let fixture: ComponentFixture; let i18nLanguageCodeService: I18nLanguageCodeService; @@ -89,18 +88,15 @@ describe('Attribution Guide Component', function() { beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ - AttributionGuideComponent, - MockTranslatePipe - ], + declarations: [AttributionGuideComponent, MockTranslatePipe], providers: [ - { provide: AttributionService, useClass: MockAttributionService }, - { provide: BrowserCheckerService, useClass: MockBrowserCheckerService }, - { provide: UrlService, useClass: MockUrlService }, - { provide: ContextService, useClass: MockContextService }, - WindowDimensionsService + {provide: AttributionService, useClass: MockAttributionService}, + {provide: BrowserCheckerService, useClass: MockBrowserCheckerService}, + {provide: UrlService, useClass: MockUrlService}, + {provide: ContextService, useClass: MockContextService}, + WindowDimensionsService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -111,26 +107,31 @@ describe('Attribution Guide Component', function() { component = fixture.componentInstance; spyOn(i18nLanguageCodeService, 'isCurrentLanguageRTL').and.returnValue( - true); + true + ); }); - it('should switch to mobile view if the window size is less than or equal' + - 'to 1024px', - function() { - let widthSpy = spyOn(windowDimensionsService, 'getWidth'); - widthSpy.and.returnValue(400); + it( + 'should switch to mobile view if the window size is less than or equal' + + 'to 1024px', + function () { + let widthSpy = spyOn(windowDimensionsService, 'getWidth'); + widthSpy.and.returnValue(400); - expect(component.checkMobileView()).toBe(true); - }); + expect(component.checkMobileView()).toBe(true); + } + ); - it('should not switch to mobile view if the window size is less than or' + - ' equal to 1024px', - function() { - let widthSpy = spyOn(windowDimensionsService, 'getWidth'); - widthSpy.and.returnValue(1025); + it( + 'should not switch to mobile view if the window size is less than or' + + ' equal to 1024px', + function () { + let widthSpy = spyOn(windowDimensionsService, 'getWidth'); + widthSpy.and.returnValue(1025); - expect(component.checkMobileView()).toBe(false); - }); + expect(component.checkMobileView()).toBe(false); + } + ); it('should initialize component properties correctly', () => { expect(component.deviceUsedIsMobile).toBeFalse(); @@ -182,9 +183,9 @@ describe('Attribution Guide Component', function() { dummyDivElement.appendChild(dummyTextNode); let dummyDocumentFragment = document.createDocumentFragment(); dummyDocumentFragment.appendChild(dummyDivElement); - spyOn( - document, 'getElementsByClassName' - ).withArgs('class-name').and.returnValue(dummyDocumentFragment.children); + spyOn(document, 'getElementsByClassName') + .withArgs('class-name') + .and.returnValue(dummyDocumentFragment.children); spyOn(document, 'execCommand').withArgs('copy'); spyOn($.fn, 'tooltip'); component.copyAttribution('class-name'); diff --git a/core/templates/components/common-layout-directives/common-elements/attribution-guide.component.ts b/core/templates/components/common-layout-directives/common-elements/attribution-guide.component.ts index 34dff15cf189..b46c1cd9c8bf 100644 --- a/core/templates/components/common-layout-directives/common-elements/attribution-guide.component.ts +++ b/core/templates/components/common-layout-directives/common-elements/attribution-guide.component.ts @@ -16,24 +16,22 @@ * @fileoverview Component for the attribution guide. */ -import { Component, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; -import { BrowserCheckerService } from - 'domain/utilities/browser-checker.service'; -import { AttributionService } from 'services/attribution.service'; -import { ContextService } from 'services/context.service'; -import { UrlService } from 'services/contextual/url.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; +import {BrowserCheckerService} from 'domain/utilities/browser-checker.service'; +import {AttributionService} from 'services/attribution.service'; +import {ContextService} from 'services/context.service'; +import {UrlService} from 'services/contextual/url.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; import './attribution-guide.component.css'; - @Component({ selector: 'attribution-guide', templateUrl: './attribution-guide.component.html', - styleUrls: ['./attribution-guide.component.css'] + styleUrls: ['./attribution-guide.component.css'], }) export class AttributionGuideComponent implements OnInit { deviceUsedIsMobile: boolean = false; @@ -53,21 +51,21 @@ export class AttributionGuideComponent implements OnInit { ngOnInit(): void { this.deviceUsedIsMobile = this.browserCheckerService.isMobileDevice(); this.iframed = this.urlService.isIframed(); - this.printAttributionLink = ( - 'CC BY SA 4.0 license' + - '' + - ''); - this.generateAttibutionIsAllowed = ( - this.attributionService.isGenerateAttributionAllowed()); + this.printAttributionLink = + 'CC BY SA 4.0 license' + + '' + + ''; + this.generateAttibutionIsAllowed = + this.attributionService.isGenerateAttributionAllowed(); if (this.generateAttibutionIsAllowed) { this.attributionService.init(); } } checkMobileView(): boolean { - return (this.windowDimensionsService.getWidth() <= 1024); + return this.windowDimensionsService.getWidth() <= 1024; } getAttributionModalStatus(): boolean { @@ -117,6 +115,9 @@ export class AttributionGuideComponent implements OnInit { } } -angular.module('oppia').directive( - 'attributionGuide', downgradeComponent( - {component: AttributionGuideComponent})); +angular + .module('oppia') + .directive( + 'attributionGuide', + downgradeComponent({component: AttributionGuideComponent}) + ); diff --git a/core/templates/components/common-layout-directives/common-elements/background-banner.component.spec.ts b/core/templates/components/common-layout-directives/common-elements/background-banner.component.spec.ts index ba611b3abecf..b1830c71634e 100644 --- a/core/templates/components/common-layout-directives/common-elements/background-banner.component.spec.ts +++ b/core/templates/components/common-layout-directives/common-elements/background-banner.component.spec.ts @@ -12,9 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { BackgroundBannerComponent } from './background-banner.component'; - +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {BackgroundBannerComponent} from './background-banner.component'; /** * @fileoverview Unit tests for BackgroundBannerComponent. @@ -26,7 +25,7 @@ describe('BackgroundBannerComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [BackgroundBannerComponent] + declarations: [BackgroundBannerComponent], }).compileComponents(); })); @@ -41,11 +40,13 @@ describe('BackgroundBannerComponent', () => { spyOn(Math, 'random').and.returnValues(0.5, 0.3); component.ngOnInit(); - expect(component.bannerImageFileUrl) - .toBe('/assets/images/background/bannerC.svg'); + expect(component.bannerImageFileUrl).toBe( + '/assets/images/background/bannerC.svg' + ); component.ngOnInit(); - expect(component.bannerImageFileUrl) - .toBe('/assets/images/background/bannerB.svg'); + expect(component.bannerImageFileUrl).toBe( + '/assets/images/background/bannerB.svg' + ); }); }); diff --git a/core/templates/components/common-layout-directives/common-elements/background-banner.component.ts b/core/templates/components/common-layout-directives/common-elements/background-banner.component.ts index 5ddb7a2b060b..9207586eafb5 100644 --- a/core/templates/components/common-layout-directives/common-elements/background-banner.component.ts +++ b/core/templates/components/common-layout-directives/common-elements/background-banner.component.ts @@ -16,28 +16,37 @@ * @fileoverview Component for the background banner. */ -import { Component, OnInit } from '@angular/core'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component, OnInit} from '@angular/core'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {downgradeComponent} from '@angular/upgrade/static'; @Component({ selector: 'background-banner', - templateUrl: './background-banner.component.html' + templateUrl: './background-banner.component.html', }) export class BackgroundBannerComponent implements OnInit { constructor(private urlInterpolationService: UrlInterpolationService) {} bannerImageFileUrl: string = ''; ngOnInit(): void { const possibleBannerFilenames: string[] = [ - 'bannerA.svg', 'bannerB.svg', 'bannerC.svg', 'bannerD.svg']; - const bannerImageFilename: string = possibleBannerFilenames[ - Math.floor(Math.random() * possibleBannerFilenames.length)]; + 'bannerA.svg', + 'bannerB.svg', + 'bannerC.svg', + 'bannerD.svg', + ]; + const bannerImageFilename: string = + possibleBannerFilenames[ + Math.floor(Math.random() * possibleBannerFilenames.length) + ]; this.bannerImageFileUrl = this.urlInterpolationService.getStaticImageUrl( - '/background/' + bannerImageFilename); + '/background/' + bannerImageFilename + ); } } -angular.module('oppia').directive( - 'backgroundBanner', downgradeComponent( - {component: BackgroundBannerComponent})); +angular + .module('oppia') + .directive( + 'backgroundBanner', + downgradeComponent({component: BackgroundBannerComponent}) + ); diff --git a/core/templates/components/common-layout-directives/common-elements/background-banner.module.ts b/core/templates/components/common-layout-directives/common-elements/background-banner.module.ts index 19dc9e3e5f0e..e710a537e1ac 100644 --- a/core/templates/components/common-layout-directives/common-elements/background-banner.module.ts +++ b/core/templates/components/common-layout-directives/common-elements/background-banner.module.ts @@ -16,19 +16,13 @@ * @fileoverview Module for the background banner component. */ -import { NgModule } from '@angular/core'; +import {NgModule} from '@angular/core'; -import { BackgroundBannerComponent } from 'components/common-layout-directives/common-elements/background-banner.component'; +import {BackgroundBannerComponent} from 'components/common-layout-directives/common-elements/background-banner.component'; @NgModule({ - declarations: [ - BackgroundBannerComponent - ], - entryComponents: [ - BackgroundBannerComponent - ], - exports: [ - BackgroundBannerComponent - ] + declarations: [BackgroundBannerComponent], + entryComponents: [BackgroundBannerComponent], + exports: [BackgroundBannerComponent], }) export class BackgroundBannerModule {} diff --git a/core/templates/components/common-layout-directives/common-elements/common-elements.module.ts b/core/templates/components/common-layout-directives/common-elements/common-elements.module.ts index 372099f97d39..1a5a7633c6a0 100644 --- a/core/templates/components/common-layout-directives/common-elements/common-elements.module.ts +++ b/core/templates/components/common-layout-directives/common-elements/common-elements.module.ts @@ -18,23 +18,14 @@ import 'core-js/es7/reflect'; import 'zone.js'; -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { LoadingDotsComponent } from './loading-dots.component'; +import {CommonModule} from '@angular/common'; +import {NgModule} from '@angular/core'; +import {LoadingDotsComponent} from './loading-dots.component'; @NgModule({ - imports: [ - CommonModule, - ], - declarations: [ - LoadingDotsComponent - ], - entryComponents: [ - LoadingDotsComponent - ], - exports: [ - LoadingDotsComponent - ], + imports: [CommonModule], + declarations: [LoadingDotsComponent], + entryComponents: [LoadingDotsComponent], + exports: [LoadingDotsComponent], }) - -export class CommonElementsModule { } +export class CommonElementsModule {} diff --git a/core/templates/components/common-layout-directives/common-elements/confirm-or-cancel-modal.component.spec.ts b/core/templates/components/common-layout-directives/common-elements/confirm-or-cancel-modal.component.spec.ts index 80743fb88d9e..c516903cae19 100644 --- a/core/templates/components/common-layout-directives/common-elements/confirm-or-cancel-modal.component.spec.ts +++ b/core/templates/components/common-layout-directives/common-elements/confirm-or-cancel-modal.component.spec.ts @@ -16,9 +16,8 @@ * @fileoverview Unit tests for ConfirmOrCancelModalController. */ -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from './confirm-or-cancel-modal.component'; - +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from './confirm-or-cancel-modal.component'; describe('Confirm Or Cancel Modal Component', () => { let confirmOrCancelModal: ConfirmOrCancelModal; @@ -34,18 +33,18 @@ describe('Confirm Or Cancel Modal Component', () => { closeSpy = spyOn(modalInstance, 'close').and.callThrough(); }); - it('should close modal with the correct value', function() { + it('should close modal with the correct value', function () { const message = 'closing'; confirmOrCancelModal.confirm(message); expect(closeSpy).toHaveBeenCalledWith(message); }); - it('should dismiss modal', function() { + it('should dismiss modal', function () { confirmOrCancelModal.cancel(); expect(dismissSpy).toHaveBeenCalledWith('cancel'); }); - it('should dismiss modal with the correct value', function() { + it('should dismiss modal with the correct value', function () { const message = 'canceling'; confirmOrCancelModal.cancel(message); expect(dismissSpy).toHaveBeenCalledWith(message); diff --git a/core/templates/components/common-layout-directives/common-elements/confirm-or-cancel-modal.component.ts b/core/templates/components/common-layout-directives/common-elements/confirm-or-cancel-modal.component.ts index 4d1a47a54d36..efde078f828e 100644 --- a/core/templates/components/common-layout-directives/common-elements/confirm-or-cancel-modal.component.ts +++ b/core/templates/components/common-layout-directives/common-elements/confirm-or-cancel-modal.component.ts @@ -17,7 +17,7 @@ * dismiss. */ -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; export class ConfirmOrCancelModal { constructor(protected modalInstance: NgbActiveModal) {} diff --git a/core/templates/components/common-layout-directives/common-elements/confirm-or-cancel-modal.controller.spec.ts b/core/templates/components/common-layout-directives/common-elements/confirm-or-cancel-modal.controller.spec.ts index 45884dccbcb6..7c753c394d68 100644 --- a/core/templates/components/common-layout-directives/common-elements/confirm-or-cancel-modal.controller.spec.ts +++ b/core/templates/components/common-layout-directives/common-elements/confirm-or-cancel-modal.controller.spec.ts @@ -16,36 +16,40 @@ * @fileoverview Unit tests for ConfirmOrCancelModalController. */ -describe('Confirm Or Cancel Modal Controller', function() { +describe('Confirm Or Cancel Modal Controller', function () { var $scope = null; var $uibModalInstance = null; beforeEach(angular.mock.module('oppia')); - beforeEach(angular.mock.inject(function($injector, $controller) { - var $rootScope = $injector.get('$rootScope'); - - $uibModalInstance = jasmine.createSpyObj( - '$uibModalInstance', ['close', 'dismiss']); - - $scope = $rootScope.$new(); - $controller('ConfirmOrCancelModalController', { - $scope: $scope, - $uibModalInstance: $uibModalInstance, - }); - })); - - it('should close modal with the correct value', function() { + beforeEach( + angular.mock.inject(function ($injector, $controller) { + var $rootScope = $injector.get('$rootScope'); + + $uibModalInstance = jasmine.createSpyObj('$uibModalInstance', [ + 'close', + 'dismiss', + ]); + + $scope = $rootScope.$new(); + $controller('ConfirmOrCancelModalController', { + $scope: $scope, + $uibModalInstance: $uibModalInstance, + }); + }) + ); + + it('should close modal with the correct value', function () { var message = 'closing'; $scope.confirm(message); expect($uibModalInstance.close).toHaveBeenCalledWith(message); }); - it('should dismiss modal', function() { + it('should dismiss modal', function () { $scope.cancel(); expect($uibModalInstance.dismiss).toHaveBeenCalledWith('cancel'); }); - it('should dismiss modal with the correct value', function() { + it('should dismiss modal with the correct value', function () { var message = 'canceling'; $scope.cancel(message); expect($uibModalInstance.dismiss).toHaveBeenCalledWith(message); diff --git a/core/templates/components/common-layout-directives/common-elements/confirm-or-cancel-modal.controller.ts b/core/templates/components/common-layout-directives/common-elements/confirm-or-cancel-modal.controller.ts index 26de851fbe82..d28a18bdc68c 100644 --- a/core/templates/components/common-layout-directives/common-elements/confirm-or-cancel-modal.controller.ts +++ b/core/templates/components/common-layout-directives/common-elements/confirm-or-cancel-modal.controller.ts @@ -18,15 +18,16 @@ */ angular.module('oppia').controller('ConfirmOrCancelModalController', [ - '$scope', '$uibModalInstance', - function($scope, $uibModalInstance) { - $scope.confirm = function(value: string) { + '$scope', + '$uibModalInstance', + function ($scope, $uibModalInstance) { + $scope.confirm = function (value: string) { $uibModalInstance.close(value); }; - $scope.cancel = function(value: string) { + $scope.cancel = function (value: string) { var dismissValue = value || 'cancel'; $uibModalInstance.dismiss(dismissValue); }; - } + }, ]); diff --git a/core/templates/components/common-layout-directives/common-elements/lazy-loading.component.ts b/core/templates/components/common-layout-directives/common-elements/lazy-loading.component.ts index 04fae57fabd2..5b60372fca8b 100644 --- a/core/templates/components/common-layout-directives/common-elements/lazy-loading.component.ts +++ b/core/templates/components/common-layout-directives/common-elements/lazy-loading.component.ts @@ -16,17 +16,20 @@ * @fileoverview Component for displaying animated lazy loading container. */ -import { Component } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; @Component({ selector: 'lazy-loading', templateUrl: './lazy-loading.component.html', - styleUrls: [] + styleUrls: [], }) export class LazyLoadingComponent { constructor() {} } -angular.module('oppia').directive( - 'lazyLoading', downgradeComponent( - {component: LazyLoadingComponent})); +angular + .module('oppia') + .directive( + 'lazyLoading', + downgradeComponent({component: LazyLoadingComponent}) + ); diff --git a/core/templates/components/common-layout-directives/common-elements/loading-dots.component.ts b/core/templates/components/common-layout-directives/common-elements/loading-dots.component.ts index d00f5b64f4c2..da1dae155593 100644 --- a/core/templates/components/common-layout-directives/common-elements/loading-dots.component.ts +++ b/core/templates/components/common-layout-directives/common-elements/loading-dots.component.ts @@ -16,17 +16,20 @@ * @fileoverview Component for displaying animated loading dots. */ -import { Component } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; @Component({ selector: 'loading-dots', templateUrl: './loading-dots.component.html', - styleUrls: [] + styleUrls: [], }) export class LoadingDotsComponent { constructor() {} } -angular.module('oppia').directive( - 'loadingDots', downgradeComponent( - {component: LoadingDotsComponent})); +angular + .module('oppia') + .directive( + 'loadingDots', + downgradeComponent({component: LoadingDotsComponent}) + ); diff --git a/core/templates/components/common-layout-directives/common-elements/promo-bar.component.spec.ts b/core/templates/components/common-layout-directives/common-elements/promo-bar.component.spec.ts index 02b0e4d4c863..f711b9a1a8b7 100644 --- a/core/templates/components/common-layout-directives/common-elements/promo-bar.component.spec.ts +++ b/core/templates/components/common-layout-directives/common-elements/promo-bar.component.spec.ts @@ -16,11 +16,16 @@ * @fileoverview Unit tests for for PromoBarComponent. */ -import { async, ComponentFixture, fakeAsync, TestBed, tick } from - '@angular/core/testing'; -import { PromoBarComponent } from 'components/common-layout-directives/common-elements/promo-bar.component'; -import { PromoBarBackendApiService } from 'services/promo-bar-backend-api.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import { + async, + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {PromoBarComponent} from 'components/common-layout-directives/common-elements/promo-bar.component'; +import {PromoBarBackendApiService} from 'services/promo-bar-backend-api.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; describe('Promo Bar Component', () => { let component: PromoBarComponent; @@ -29,10 +34,10 @@ describe('Promo Bar Component', () => { class MockPromoBarBackendApiService { async getPromoBarDataAsync() { - return new Promise((resolve) => { + return new Promise(resolve => { resolve({ promoBarEnabled: true, - promoBarMessage: 'Promo bar message.' + promoBarMessage: 'Promo bar message.', }); }); } @@ -45,8 +50,9 @@ describe('Promo Bar Component', () => { WindowRef, { provide: PromoBarBackendApiService, - useClass: MockPromoBarBackendApiService - }] + useClass: MockPromoBarBackendApiService, + }, + ], }).compileComponents(); })); @@ -59,7 +65,9 @@ describe('Promo Bar Component', () => { it('should intialize the component and set values', fakeAsync(() => { const promoBarSpy = spyOn( - promoBarBackendApiService, 'getPromoBarDataAsync').and.callThrough(); + promoBarBackendApiService, + 'getPromoBarDataAsync' + ).and.callThrough(); expect(component.promoBarIsEnabled).toBeUndefined; expect(component.promoBarMessage).toBeUndefined; @@ -72,123 +80,168 @@ describe('Promo Bar Component', () => { expect(component.promoBarMessage).toBe('Promo bar message.'); })); - it('should return true if session storage is available when calling ' + - 'isSessionStorageAvailable', fakeAsync(() => { - const setItemSpy = spyOn( - window.sessionStorage, 'setItem').and.callThrough(); - const removeItemSpy = spyOn( - window.sessionStorage, 'removeItem').and.callThrough(); - - let isSessionStorageAvailable = component.isSessionStorageAvailable(); - - expect(setItemSpy).toHaveBeenCalled(); - expect(removeItemSpy).toHaveBeenCalled(); - expect(isSessionStorageAvailable).toBe(true); - })); - - it('should return false if session storage is not available when calling ' + - 'isSessionStorageAvailable', () => { - const setItemSpy = spyOn( - window.sessionStorage, 'setItem').and.callFake(() => { - throw new Error('Session storage not available.'); - }); - const removeItemSpy = spyOn( - window.sessionStorage, 'removeItem').and.callThrough(); - - let isSessionStorageAvailable = component.isSessionStorageAvailable(); - - expect(setItemSpy).toHaveBeenCalled(); - expect(removeItemSpy).not.toHaveBeenCalled(); - expect(isSessionStorageAvailable).toBe(false); - }); - - it('should return false if session storage is not available when calling ' + - 'isPromoDismissed', () => { - const sessionStorageSpy = spyOn( - component, 'isSessionStorageAvailable').and.callFake(() => { - return false; - }); - let isPromoDismissed = component.isPromoDismissed(); - - expect(sessionStorageSpy).toHaveBeenCalled(); - expect(isPromoDismissed).toBe(false); - }); - - it('should return true if promo bar is dismissed when calling' + - 'isPromoDismissed', () => { - const sessionStorageSpy = spyOn( - component, 'isSessionStorageAvailable').and.callFake(() => { - return true; - }); - window.sessionStorage.promoIsDismissed = true; - - let isPromoDismissed = component.isPromoDismissed(); - - expect(sessionStorageSpy).toHaveBeenCalled(); - expect(isPromoDismissed).toBe(true); - }); - - it('should return false if promo bar is not dismissed when calling ' + - 'isPromoDismissed', () => { - const sessionStorageSpy = spyOn( - component, 'isSessionStorageAvailable').and.callFake(() => { - return true; - }); - window.sessionStorage.promoIsDismissed = false; + it( + 'should return true if session storage is available when calling ' + + 'isSessionStorageAvailable', + fakeAsync(() => { + const setItemSpy = spyOn( + window.sessionStorage, + 'setItem' + ).and.callThrough(); + const removeItemSpy = spyOn( + window.sessionStorage, + 'removeItem' + ).and.callThrough(); + + let isSessionStorageAvailable = component.isSessionStorageAvailable(); + + expect(setItemSpy).toHaveBeenCalled(); + expect(removeItemSpy).toHaveBeenCalled(); + expect(isSessionStorageAvailable).toBe(true); + }) + ); + + it( + 'should return false if session storage is not available when calling ' + + 'isSessionStorageAvailable', + () => { + const setItemSpy = spyOn(window.sessionStorage, 'setItem').and.callFake( + () => { + throw new Error('Session storage not available.'); + } + ); + const removeItemSpy = spyOn( + window.sessionStorage, + 'removeItem' + ).and.callThrough(); + + let isSessionStorageAvailable = component.isSessionStorageAvailable(); + + expect(setItemSpy).toHaveBeenCalled(); + expect(removeItemSpy).not.toHaveBeenCalled(); + expect(isSessionStorageAvailable).toBe(false); + } + ); + + it( + 'should return false if session storage is not available when calling ' + + 'isPromoDismissed', + () => { + const sessionStorageSpy = spyOn( + component, + 'isSessionStorageAvailable' + ).and.callFake(() => { + return false; + }); + let isPromoDismissed = component.isPromoDismissed(); - let isPromoDismissed = component.isPromoDismissed(); + expect(sessionStorageSpy).toHaveBeenCalled(); + expect(isPromoDismissed).toBe(false); + } + ); + + it( + 'should return true if promo bar is dismissed when calling' + + 'isPromoDismissed', + () => { + const sessionStorageSpy = spyOn( + component, + 'isSessionStorageAvailable' + ).and.callFake(() => { + return true; + }); + window.sessionStorage.promoIsDismissed = true; - expect(sessionStorageSpy).toHaveBeenCalled(); - expect(isPromoDismissed).toBe(false); - }); + let isPromoDismissed = component.isPromoDismissed(); - it('should return false if session storage is not ' + - 'available when calling setPromoDismissed', () => { - let isPromoDismissed = false; - const sessionStorageSpy = spyOn( - component, 'isSessionStorageAvailable').and.callFake(() => { - return false; - }); - let setPromoDismissed = component.setPromoDismissed(isPromoDismissed); + expect(sessionStorageSpy).toHaveBeenCalled(); + expect(isPromoDismissed).toBe(true); + } + ); + + it( + 'should return false if promo bar is not dismissed when calling ' + + 'isPromoDismissed', + () => { + const sessionStorageSpy = spyOn( + component, + 'isSessionStorageAvailable' + ).and.callFake(() => { + return true; + }); + window.sessionStorage.promoIsDismissed = false; - expect(sessionStorageSpy).toHaveBeenCalled(); - expect(setPromoDismissed).toBe(false); - }); + let isPromoDismissed = component.isPromoDismissed(); - it('should set the value of promoIsDismissed as true in session storage ' + - 'when calling setPromoDismissed', () => { - let isPromoDismissed = true; - const sessionStorageSpy = spyOn( - component, 'isSessionStorageAvailable').and.callFake(() => { - return true; - }); + expect(sessionStorageSpy).toHaveBeenCalled(); + expect(isPromoDismissed).toBe(false); + } + ); + + it( + 'should return false if session storage is not ' + + 'available when calling setPromoDismissed', + () => { + let isPromoDismissed = false; + const sessionStorageSpy = spyOn( + component, + 'isSessionStorageAvailable' + ).and.callFake(() => { + return false; + }); + let setPromoDismissed = component.setPromoDismissed(isPromoDismissed); - expect(window.sessionStorage.promoIsDismissed).toBeUndefined; - component.setPromoDismissed(isPromoDismissed); + expect(sessionStorageSpy).toHaveBeenCalled(); + expect(setPromoDismissed).toBe(false); + } + ); + + it( + 'should set the value of promoIsDismissed as true in session storage ' + + 'when calling setPromoDismissed', + () => { + let isPromoDismissed = true; + const sessionStorageSpy = spyOn( + component, + 'isSessionStorageAvailable' + ).and.callFake(() => { + return true; + }); - expect(sessionStorageSpy).toHaveBeenCalled(); - expect(window.sessionStorage.promoIsDismissed).toBe('true'); - }); + expect(window.sessionStorage.promoIsDismissed).toBeUndefined; + component.setPromoDismissed(isPromoDismissed); - it('should set the value of promoIsDismissed as false in session storage ' + - 'when calling setPromoDismissed', () => { - let isPromoDismissed = false; - const sessionStorageSpy = spyOn( - component, 'isSessionStorageAvailable').and.callFake(() => { - return true; - }); + expect(sessionStorageSpy).toHaveBeenCalled(); + expect(window.sessionStorage.promoIsDismissed).toBe('true'); + } + ); + + it( + 'should set the value of promoIsDismissed as false in session storage ' + + 'when calling setPromoDismissed', + () => { + let isPromoDismissed = false; + const sessionStorageSpy = spyOn( + component, + 'isSessionStorageAvailable' + ).and.callFake(() => { + return true; + }); - expect(window.sessionStorage.promoIsDismissed).toBeUndefined; - component.setPromoDismissed(isPromoDismissed); + expect(window.sessionStorage.promoIsDismissed).toBeUndefined; + component.setPromoDismissed(isPromoDismissed); - expect(sessionStorageSpy).toHaveBeenCalled(); - expect(window.sessionStorage.promoIsDismissed).toBe('false'); - }); + expect(sessionStorageSpy).toHaveBeenCalled(); + expect(window.sessionStorage.promoIsDismissed).toBe('false'); + } + ); it('should dismiss the promo bar when calling dismissPromo', () => { expect(component.promoIsVisible).toBeUndefined; const sessionStorageSpy = spyOn( - component, 'setPromoDismissed').and.callThrough(); + component, + 'setPromoDismissed' + ).and.callThrough(); expect(window.sessionStorage.promoIsDismissed).toBeUndefined; component.dismissPromo(); diff --git a/core/templates/components/common-layout-directives/common-elements/promo-bar.component.ts b/core/templates/components/common-layout-directives/common-elements/promo-bar.component.ts index 975be46845a8..6b81ea081561 100644 --- a/core/templates/components/common-layout-directives/common-elements/promo-bar.component.ts +++ b/core/templates/components/common-layout-directives/common-elements/promo-bar.component.ts @@ -18,15 +18,15 @@ * dismissible. */ -import { Component, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { PromoBarBackendApiService } from 'services/promo-bar-backend-api.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {PromoBarBackendApiService} from 'services/promo-bar-backend-api.service'; @Component({ selector: 'oppia-promo-bar', - templateUrl: './promo-bar.component.html' + templateUrl: './promo-bar.component.html', }) export class PromoBarComponent implements OnInit { // These properties are initialized using Angular lifecycle hooks @@ -42,11 +42,10 @@ export class PromoBarComponent implements OnInit { ) {} ngOnInit(): void { - this.promoBarBackendApiService.getPromoBarDataAsync() - .then((promoBar) => { - this.promoBarIsEnabled = promoBar.promoBarEnabled; - this.promoBarMessage = promoBar.promoBarMessage; - }); + this.promoBarBackendApiService.getPromoBarDataAsync().then(promoBar => { + this.promoBarIsEnabled = promoBar.promoBarEnabled; + this.promoBarMessage = promoBar.promoBarMessage; + }); this.promoIsVisible = !this.isPromoDismissed(); } @@ -56,7 +55,7 @@ export class PromoBarComponent implements OnInit { } let promoIsDismissed = this.windowRef.nativeWindow.sessionStorage.promoIsDismissed; - if (typeof (promoIsDismissed) !== 'undefined') { + if (typeof promoIsDismissed !== 'undefined') { return JSON.parse(promoIsDismissed); } return false; @@ -89,6 +88,9 @@ export class PromoBarComponent implements OnInit { } } -angular.module('oppia').directive( - 'oppiaPromoBar', downgradeComponent( - {component: PromoBarComponent})); +angular + .module('oppia') + .directive( + 'oppiaPromoBar', + downgradeComponent({component: PromoBarComponent}) + ); diff --git a/core/templates/components/common-layout-directives/common-elements/sharing-links.component.spec.ts b/core/templates/components/common-layout-directives/common-elements/sharing-links.component.spec.ts index 275307206112..a1426cd0a5d5 100644 --- a/core/templates/components/common-layout-directives/common-elements/sharing-links.component.spec.ts +++ b/core/templates/components/common-layout-directives/common-elements/sharing-links.component.spec.ts @@ -12,13 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { ExplorationEmbedButtonModalComponent } from 'components/button-directives/exploration-embed-button-modal.component'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { SharingLinksComponent } from './sharing-links.component'; - +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {ExplorationEmbedButtonModalComponent} from 'components/button-directives/exploration-embed-button-modal.component'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {SharingLinksComponent} from './sharing-links.component'; /** * @fileoverview Unit tests for SharingLinksComponent. @@ -28,10 +27,10 @@ class MockWindowRef { _window = { location: { protocol: 'https:', - host: 'www.oppia.org' + host: 'www.oppia.org', }, open: (url: string) => {}, - gtag: () => {} + gtag: () => {}, }; get nativeWindow() { @@ -42,7 +41,7 @@ class MockWindowRef { export class MockNgbModalRef { componentInstance = { serverName: null, - explorationId: null + explorationId: null, }; } @@ -58,9 +57,7 @@ describe('SharingLinksComponent', () => { windowRef = new MockWindowRef(); TestBed.configureTestingModule({ declarations: [SharingLinksComponent], - providers: [ - {provide: WindowRef, useValue: windowRef} - ] + providers: [{provide: WindowRef, useValue: windowRef}], }).compileComponents(); })); @@ -71,65 +68,84 @@ describe('SharingLinksComponent', () => { siteAnalyticsService = TestBed.inject(SiteAnalyticsService); }); - it('should set query fields for social platform APIs when an' + - ' exploration is shared', () => { - component.shareType = 'exploration'; - component.explorationId = 'exp1'; - - component.ngOnInit(); - - expect(component.activityId).toBe('exp1'); - expect(component.activityUrlFragment).toBe('explore'); - expect(component.serverName).toBe('https://www.oppia.org'); - expect(component.escapedTwitterText).toBe( - 'Check out this interactive lesson on Oppia - a free platform' + - ' for teaching and learning!'); - expect(component.classroomUrl).toBe('/assets/images/general/classroom.png'); - }); - - it('should set query fields for social platform APIs when a' + - ' collection is shared', () => { - component.shareType = 'collection'; - component.collectionId = 'col1'; - - component.ngOnInit(); - - expect(component.activityId).toBe('col1'); - expect(component.activityUrlFragment).toBe('collection'); - expect(component.serverName).toBe('https://www.oppia.org'); - expect(component.escapedTwitterText).toBe( - 'Check out this interactive lesson on Oppia - a free platform' + - ' for teaching and learning!'); - expect(component.classroomUrl).toBe('/assets/images/general/classroom.png'); - }); - - it('should set query fields for social platform APIs when a' + - ' blog post is shared', () => { - component.shareType = 'blog'; - component.blogPostUrl = 'sample-blog-post-url'; - - component.ngOnInit(); - - expect(component.serverName).toBe('https://www.oppia.org'); - expect(component.escapedTwitterText).toBe( - 'Check out this new blog post on Oppia!'); - }); - - - it('should throw an error when SharingLink component is used' + - ' at any other place than exploration player, collection player or' + - ' blog post page', () => { - // This throws "Type '"not-exp-or-col"' is not assignable to type - // 'ShareType'". We need to suppress this error because 'shareType' can - // only be equal to 'exploration', 'collection' or 'blog', but we set an - // invalid value in order to test validations. - // @ts-expect-error - component.shareType = 'not-exp-or-col'; - - expect(() => component.ngOnInit()).toThrowError( - 'SharingLinks component can only be used in the collection player' + - ', exploration player or the blog post page.'); - }); + it( + 'should set query fields for social platform APIs when an' + + ' exploration is shared', + () => { + component.shareType = 'exploration'; + component.explorationId = 'exp1'; + + component.ngOnInit(); + + expect(component.activityId).toBe('exp1'); + expect(component.activityUrlFragment).toBe('explore'); + expect(component.serverName).toBe('https://www.oppia.org'); + expect(component.escapedTwitterText).toBe( + 'Check out this interactive lesson on Oppia - a free platform' + + ' for teaching and learning!' + ); + expect(component.classroomUrl).toBe( + '/assets/images/general/classroom.png' + ); + } + ); + + it( + 'should set query fields for social platform APIs when a' + + ' collection is shared', + () => { + component.shareType = 'collection'; + component.collectionId = 'col1'; + + component.ngOnInit(); + + expect(component.activityId).toBe('col1'); + expect(component.activityUrlFragment).toBe('collection'); + expect(component.serverName).toBe('https://www.oppia.org'); + expect(component.escapedTwitterText).toBe( + 'Check out this interactive lesson on Oppia - a free platform' + + ' for teaching and learning!' + ); + expect(component.classroomUrl).toBe( + '/assets/images/general/classroom.png' + ); + } + ); + + it( + 'should set query fields for social platform APIs when a' + + ' blog post is shared', + () => { + component.shareType = 'blog'; + component.blogPostUrl = 'sample-blog-post-url'; + + component.ngOnInit(); + + expect(component.serverName).toBe('https://www.oppia.org'); + expect(component.escapedTwitterText).toBe( + 'Check out this new blog post on Oppia!' + ); + } + ); + + it( + 'should throw an error when SharingLink component is used' + + ' at any other place than exploration player, collection player or' + + ' blog post page', + () => { + // This throws "Type '"not-exp-or-col"' is not assignable to type + // 'ShareType'". We need to suppress this error because 'shareType' can + // only be equal to 'exploration', 'collection' or 'blog', but we set an + // invalid value in order to test validations. + // @ts-expect-error + component.shareType = 'not-exp-or-col'; + + expect(() => component.ngOnInit()).toThrowError( + 'SharingLinks component can only be used in the collection player' + + ', exploration player or the blog post page.' + ); + } + ); it('should get font and flex class according to font size', () => { component.smallFont = true; @@ -137,109 +153,143 @@ describe('SharingLinksComponent', () => { component.layoutAlignType = 'center end'; expect(component.getFontAndFlexClass()).toBe( - 'font-small fx-row fx-main-center fx-cross-end'); + 'font-small fx-row fx-main-center fx-cross-end' + ); component.smallFont = false; component.layoutType = 'row-center'; component.layoutAlignType = 'center'; expect(component.getFontAndFlexClass()).toBe( - 'font-big fx-row-center fx-main-center'); + 'font-big fx-row-center fx-main-center' + ); component.smallFont = true; component.layoutType = 'row-center'; component.layoutAlignType = ''; - expect(component.getFontAndFlexClass()).toBe( - 'font-small fx-row-center'); + expect(component.getFontAndFlexClass()).toBe('font-small fx-row-center'); }); it('should get social media URLs', () => { expect(component.getUrl('facebook')).toBe( - 'https://www.facebook.com/sharer/sharer.php?sdk=joey&u=undefined/undefined/undefined&display=popup&ref=plugin&src=share_button'); + 'https://www.facebook.com/sharer/sharer.php?sdk=joey&u=undefined/undefined/undefined&display=popup&ref=plugin&src=share_button' + ); expect(component.getUrl('twitter')).toBe( - 'https://twitter.com/share?text=undefined&url=undefined/undefined/undefined'); + 'https://twitter.com/share?text=undefined&url=undefined/undefined/undefined' + ); expect(component.getUrl('classroom')).toBe( - 'https://classroom.google.com/share?url=undefined/undefined/undefined'); + 'https://classroom.google.com/share?url=undefined/undefined/undefined' + ); component.serverName = 'https://www.oppia.org'; component.activityUrlFragment = 'explore'; component.activityId = 'exp1'; - component.escapedTwitterText = 'Check out this interactive' + - ' lesson on Oppia - a free platform for teaching and learning!'; + component.escapedTwitterText = + 'Check out this interactive' + + ' lesson on Oppia - a free platform for teaching and learning!'; expect(component.getUrl('facebook')).toBe( - 'https://www.facebook.com/sharer/sharer.php?sdk=joey&u=https://www.oppia.org/explore/exp1&display=popup&ref=plugin&src=share_button'); + 'https://www.facebook.com/sharer/sharer.php?sdk=joey&u=https://www.oppia.org/explore/exp1&display=popup&ref=plugin&src=share_button' + ); expect(component.getUrl('twitter')).toBe( - 'https://twitter.com/share?text=Check out this interactive lesson on Oppia - a free platform for teaching and learning!&url=https://www.oppia.org/explore/exp1'); + 'https://twitter.com/share?text=Check out this interactive lesson on Oppia - a free platform for teaching and learning!&url=https://www.oppia.org/explore/exp1' + ); expect(component.getUrl('classroom')).toBe( - 'https://classroom.google.com/share?url=https://www.oppia.org/explore/exp1'); + 'https://classroom.google.com/share?url=https://www.oppia.org/explore/exp1' + ); component.serverName = 'https://www.oppia.org'; component.shareType = 'blog'; component.blogPostUrl = 'sample-blog-post-url'; - component.escapedTwitterText = 'Check out this new blog post on Oppia - a' + - ' free platform for teaching and learning!'; + component.escapedTwitterText = + 'Check out this new blog post on Oppia - a' + + ' free platform for teaching and learning!'; expect(component.getUrl('facebook')).toBe( - 'https://www.facebook.com/sharer/sharer.php?sdk=joey&u=https://www.oppia.org/blog/sample-blog-post-url&display=popup&ref=plugin&src=share_button'); + 'https://www.facebook.com/sharer/sharer.php?sdk=joey&u=https://www.oppia.org/blog/sample-blog-post-url&display=popup&ref=plugin&src=share_button' + ); expect(component.getUrl('twitter')).toBe( - 'https://twitter.com/share?text=Check out this new blog post on Oppia - a free platform for teaching and learning!&url=https://www.oppia.org/blog/sample-blog-post-url'); + 'https://twitter.com/share?text=Check out this new blog post on Oppia - a free platform for teaching and learning!&url=https://www.oppia.org/blog/sample-blog-post-url' + ); expect(component.getUrl('linkedin')).toBe( - 'https://www.linkedin.com/sharing/share-offsite/?url=https://www.oppia.org/blog/sample-blog-post-url'); - }); - - it('should show embed exploration modal when' + - ' user clicks on \'Embed this Exploration\'', () => { - component.serverName = 'https://www.oppia.org'; - component.explorationId = 'exp1'; - const modalSpy = spyOn(ngbModal, 'open').and.returnValue( - ngbModalRef as NgbModalRef); - - component.showEmbedExplorationModal(); - - expect(modalSpy).toHaveBeenCalledWith( - ExplorationEmbedButtonModalComponent, {backdrop: true}); - expect(ngbModalRef.componentInstance.serverName).toBe( - 'https://www.oppia.org'); - expect(ngbModalRef.componentInstance.explorationId).toBe('exp1'); - }); - - it('should register exploration share event when user clicks' + - ' on a social media icon', () => { - const shareExplorationEventSpy = - spyOn(siteAnalyticsService, 'registerShareExplorationEvent'); - const windowRefSpy = spyOn(windowRef.nativeWindow, 'open'); - component.shareType = 'exploration'; - - component.registerShareEvent('facebook'); - - expect(shareExplorationEventSpy).toHaveBeenCalledWith('facebook'); - expect(windowRefSpy).toHaveBeenCalled(); - }); - - it('should register collection share event when user clicks' + - ' on a social media icon', () => { - const shareCollectionEventSpy = - spyOn(siteAnalyticsService, 'registerShareCollectionEvent'); - const windowRefSpy = spyOn(windowRef.nativeWindow, 'open'); - component.shareType = 'collection'; - - component.registerShareEvent('twitter'); - - expect(shareCollectionEventSpy).toHaveBeenCalledWith('twitter'); - expect(windowRefSpy).toHaveBeenCalled(); + 'https://www.linkedin.com/sharing/share-offsite/?url=https://www.oppia.org/blog/sample-blog-post-url' + ); }); - it('should register blog post share event when user clicks' + - ' on a social media icon', () => { - const shareBlogPostEventSpy = - spyOn(siteAnalyticsService, 'registerShareBlogPostEvent'); - const windowRefSpy = spyOn(windowRef.nativeWindow, 'open'); - component.shareType = 'blog'; - - component.registerShareEvent('twitter'); - - expect(shareBlogPostEventSpy).toHaveBeenCalledWith('twitter'); - expect(windowRefSpy).toHaveBeenCalled(); - }); + it( + 'should show embed exploration modal when' + + " user clicks on 'Embed this Exploration'", + () => { + component.serverName = 'https://www.oppia.org'; + component.explorationId = 'exp1'; + const modalSpy = spyOn(ngbModal, 'open').and.returnValue( + ngbModalRef as NgbModalRef + ); + + component.showEmbedExplorationModal(); + + expect(modalSpy).toHaveBeenCalledWith( + ExplorationEmbedButtonModalComponent, + {backdrop: true} + ); + expect(ngbModalRef.componentInstance.serverName).toBe( + 'https://www.oppia.org' + ); + expect(ngbModalRef.componentInstance.explorationId).toBe('exp1'); + } + ); + + it( + 'should register exploration share event when user clicks' + + ' on a social media icon', + () => { + const shareExplorationEventSpy = spyOn( + siteAnalyticsService, + 'registerShareExplorationEvent' + ); + const windowRefSpy = spyOn(windowRef.nativeWindow, 'open'); + component.shareType = 'exploration'; + + component.registerShareEvent('facebook'); + + expect(shareExplorationEventSpy).toHaveBeenCalledWith('facebook'); + expect(windowRefSpy).toHaveBeenCalled(); + } + ); + + it( + 'should register collection share event when user clicks' + + ' on a social media icon', + () => { + const shareCollectionEventSpy = spyOn( + siteAnalyticsService, + 'registerShareCollectionEvent' + ); + const windowRefSpy = spyOn(windowRef.nativeWindow, 'open'); + component.shareType = 'collection'; + + component.registerShareEvent('twitter'); + + expect(shareCollectionEventSpy).toHaveBeenCalledWith('twitter'); + expect(windowRefSpy).toHaveBeenCalled(); + } + ); + + it( + 'should register blog post share event when user clicks' + + ' on a social media icon', + () => { + const shareBlogPostEventSpy = spyOn( + siteAnalyticsService, + 'registerShareBlogPostEvent' + ); + const windowRefSpy = spyOn(windowRef.nativeWindow, 'open'); + component.shareType = 'blog'; + + component.registerShareEvent('twitter'); + + expect(shareBlogPostEventSpy).toHaveBeenCalledWith('twitter'); + expect(windowRefSpy).toHaveBeenCalled(); + } + ); }); diff --git a/core/templates/components/common-layout-directives/common-elements/sharing-links.component.ts b/core/templates/components/common-layout-directives/common-elements/sharing-links.component.ts index 757220d82566..e4a83ae35ac4 100644 --- a/core/templates/components/common-layout-directives/common-elements/sharing-links.component.ts +++ b/core/templates/components/common-layout-directives/common-elements/sharing-links.component.ts @@ -16,26 +16,23 @@ * @fileoverview Component for the Social Sharing Links. */ -import { AppConstants } from 'app.constants'; +import {AppConstants} from 'app.constants'; -import { Component, Input, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import {Component, Input, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { HtmlEscaperService } from 'services/html-escaper.service'; -import { ExplorationEmbedButtonModalComponent } from - 'components/button-directives/exploration-embed-button-modal.component'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {HtmlEscaperService} from 'services/html-escaper.service'; +import {ExplorationEmbedButtonModalComponent} from 'components/button-directives/exploration-embed-button-modal.component'; +import {WindowRef} from 'services/contextual/window-ref.service'; @Component({ selector: 'sharing-links', templateUrl: './sharing-links.component.html', - styleUrls: [] + styleUrls: [], }) - export class SharingLinksComponent implements OnInit { // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see @@ -58,7 +55,8 @@ export class SharingLinksComponent implements OnInit { private urlInterpolationService: UrlInterpolationService, private siteAnalyticsService: SiteAnalyticsService, private htmlEscaperService: HtmlEscaperService, - private windowRef: WindowRef) {} + private windowRef: WindowRef + ) {} ngOnInit(): void { if (this.shareType === 'exploration') { @@ -73,27 +71,30 @@ export class SharingLinksComponent implements OnInit { // collection or exploration page is active and render accordingly. throw new Error( 'SharingLinks component can only be used in the ' + - 'collection player, exploration player or the blog post page.'); + 'collection player, exploration player or the blog post page.' + ); } - this.serverName = ( - this.windowRef.nativeWindow.location.protocol + '//' + - this.windowRef.nativeWindow.location.host); + this.serverName = + this.windowRef.nativeWindow.location.protocol + + '//' + + this.windowRef.nativeWindow.location.host; if (this.shareType === 'blog') { - this.escapedTwitterText = ( + this.escapedTwitterText = this.htmlEscaperService.unescapedStrToEscapedStr( AppConstants.DEFUALT_BLOG_POST_SHARE_TWITTER_TEXT - ) - ); + ); } else { - this.escapedTwitterText = ( + this.escapedTwitterText = this.htmlEscaperService.unescapedStrToEscapedStr( - AppConstants.DEFAULT_TWITTER_SHARE_MESSAGE_EDITOR)); + AppConstants.DEFAULT_TWITTER_SHARE_MESSAGE_EDITOR + ); } this.classroomUrl = this.urlInterpolationService.getStaticImageUrl( - '/general/classroom.png'); + '/general/classroom.png' + ); } getFontAndFlexClass(): string { @@ -113,48 +114,38 @@ export class SharingLinksComponent implements OnInit { let queryString: string; let url: string; if (this.shareType === 'blog') { - url = ( - `${this.serverName}/blog/${this.blogPostUrl}` - ); + url = `${this.serverName}/blog/${this.blogPostUrl}`; } else { - url = ( - `${this.serverName}/${this.activityUrlFragment}/${this.activityId}`); + url = `${this.serverName}/${this.activityUrlFragment}/${this.activityId}`; } switch (network) { case 'facebook': - queryString = ( + queryString = 'sdk=joey&' + `u=${url}&` + 'display=popup&' + 'ref=plugin&' + - 'src=share_button' - ); + 'src=share_button'; return `https://www.facebook.com/sharer/sharer.php?${queryString}`; case 'twitter': - queryString = ( - `text=${this.escapedTwitterText}&` + - `url=${url}` - ); + queryString = `text=${this.escapedTwitterText}&` + `url=${url}`; return `https://twitter.com/share?${queryString}`; case 'classroom': - queryString = ( - `url=${url}` - ); + queryString = `url=${url}`; return `https://classroom.google.com/share?${queryString}`; case 'linkedin': - queryString = ( - `url=${url}` - ); + queryString = `url=${url}`; return `https://www.linkedin.com/sharing/share-offsite/?${queryString.replace('http:', 'https:')}`; } } showEmbedExplorationModal(): void { - const modelRef = this.nbgModal.open( - ExplorationEmbedButtonModalComponent, {backdrop: true}); + const modelRef = this.nbgModal.open(ExplorationEmbedButtonModalComponent, { + backdrop: true, + }); modelRef.componentInstance.serverName = this.serverName; modelRef.componentInstance.explorationId = this.explorationId; } @@ -167,13 +158,20 @@ export class SharingLinksComponent implements OnInit { } else if (this.shareType === 'blog') { this.siteAnalyticsService.registerShareBlogPostEvent(network); } - this.windowRef.nativeWindow - .open(this.getUrl(network), '', 'height=460, width=640'); + this.windowRef.nativeWindow.open( + this.getUrl(network), + '', + 'height=460, width=640' + ); } } type ShareType = 'exploration' | 'collection' | 'blog'; type SharingPlatform = 'facebook' | 'twitter' | 'classroom' | 'linkedin'; -angular.module('oppia').directive('sharingLinks', downgradeComponent( - {component: SharingLinksComponent})); +angular + .module('oppia') + .directive( + 'sharingLinks', + downgradeComponent({component: SharingLinksComponent}) + ); diff --git a/core/templates/components/common-layout-directives/navigation-bars/side-navigation-bar.component.spec.ts b/core/templates/components/common-layout-directives/navigation-bars/side-navigation-bar.component.spec.ts index 5a89eb5d4b83..cfd3a85efdb2 100644 --- a/core/templates/components/common-layout-directives/navigation-bars/side-navigation-bar.component.spec.ts +++ b/core/templates/components/common-layout-directives/navigation-bars/side-navigation-bar.component.spec.ts @@ -16,34 +16,39 @@ * @fileoverview Unit tests for SideNavigationBarComponent. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { HttpClientModule } from '@angular/common/http'; -import { ComponentFixture, TestBed, fakeAsync, tick, waitForAsync } from '@angular/core/testing'; -import { APP_BASE_HREF } from '@angular/common'; -import { RouterModule } from '@angular/router'; - -import { SmartRouterModule } from 'hybrid-router-module-provider'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { SideNavigationBarComponent } from './side-navigation-bar.component'; -import { UserService } from 'services/user.service'; -import { UserInfo } from 'domain/user/user-info.model'; -import { SidebarStatusService } from 'services/sidebar-status.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {HttpClientModule} from '@angular/common/http'; +import { + ComponentFixture, + TestBed, + fakeAsync, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {APP_BASE_HREF} from '@angular/common'; +import {RouterModule} from '@angular/router'; + +import {SmartRouterModule} from 'hybrid-router-module-provider'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {SideNavigationBarComponent} from './side-navigation-bar.component'; +import {UserService} from 'services/user.service'; +import {UserInfo} from 'domain/user/user-info.model'; +import {SidebarStatusService} from 'services/sidebar-status.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; class MockWindowRef { nativeWindow = { location: { pathname: '/test', - href: '' + href: '', }, - gtag: () => {} + gtag: () => {}, }; } - describe('Side Navigation Bar Component', () => { let fixture: ComponentFixture; let componentInstance: SideNavigationBarComponent; @@ -70,26 +75,23 @@ describe('Side Navigation Bar Component', () => { // TODO(#13443): Remove hybrid router module provider once all pages are // migrated to angular router. SmartRouterModule, - RouterModule.forRoot([]) - ], - declarations: [ - SideNavigationBarComponent, - MockTranslatePipe + RouterModule.forRoot([]), ], + declarations: [SideNavigationBarComponent, MockTranslatePipe], providers: [ { provide: WindowRef, - useValue: mockWindowRef + useValue: mockWindowRef, }, { provide: UrlInterpolationService, - useClass: MockUrlInterpolationService + useClass: MockUrlInterpolationService, }, { provide: APP_BASE_HREF, - useValue: '/' - } - ] + useValue: '/', + }, + ], }); })); @@ -102,7 +104,8 @@ describe('Side Navigation Bar Component', () => { i18nLanguageCodeService = TestBed.inject(I18nLanguageCodeService); spyOn(i18nLanguageCodeService, 'isCurrentLanguageRTL').and.returnValue( - true); + true + ); }); it('should create', () => { @@ -147,74 +150,100 @@ describe('Side Navigation Bar Component', () => { expect(sidebarStatusService.closeSidebar).toHaveBeenCalled(); }); - it('should navigate to default dashboard when user clicks on ' + - 'HOME, when not on the default dashboard', fakeAsync(() => { - expect(mockWindowRef.nativeWindow.location.href).toBe(''); - - spyOn(userService, 'getUserPreferredDashboardAsync').and.returnValue( - Promise.resolve('contributor')); - spyOn(sidebarStatusService, 'closeSidebar'); + it( + 'should navigate to default dashboard when user clicks on ' + + 'HOME, when not on the default dashboard', + fakeAsync(() => { + expect(mockWindowRef.nativeWindow.location.href).toBe(''); - componentInstance.currentUrl = '/learner-dashboard'; - componentInstance.navigateToDefaultDashboard(); - tick(); + spyOn(userService, 'getUserPreferredDashboardAsync').and.returnValue( + Promise.resolve('contributor') + ); + spyOn(sidebarStatusService, 'closeSidebar'); - expect(sidebarStatusService.closeSidebar).not.toHaveBeenCalled(); - expect(mockWindowRef.nativeWindow.location.href).toBe('/'); - })); + componentInstance.currentUrl = '/learner-dashboard'; + componentInstance.navigateToDefaultDashboard(); + tick(); - it('should not navigate to default dashboard when user clicks on ' + - 'HOME, when on the default dashboard', fakeAsync(() => { - expect(mockWindowRef.nativeWindow.location.href).toBe(''); - spyOn(userService, 'getUserPreferredDashboardAsync').and.returnValue( - Promise.resolve('creator')); - spyOn(sidebarStatusService, 'closeSidebar'); + expect(sidebarStatusService.closeSidebar).not.toHaveBeenCalled(); + expect(mockWindowRef.nativeWindow.location.href).toBe('/'); + }) + ); - componentInstance.currentUrl = '/creator-dashboard'; - componentInstance.navigateToDefaultDashboard(); - tick(); + it( + 'should not navigate to default dashboard when user clicks on ' + + 'HOME, when on the default dashboard', + fakeAsync(() => { + expect(mockWindowRef.nativeWindow.location.href).toBe(''); + spyOn(userService, 'getUserPreferredDashboardAsync').and.returnValue( + Promise.resolve('creator') + ); + spyOn(sidebarStatusService, 'closeSidebar'); - expect(sidebarStatusService.closeSidebar).toHaveBeenCalled(); - expect(mockWindowRef.nativeWindow.location.href).toBe(''); - })); + componentInstance.currentUrl = '/creator-dashboard'; + componentInstance.navigateToDefaultDashboard(); + tick(); - it('should navigate to classroom page when user clicks on' + - '\'Basic Mathematics\'', fakeAsync(() => { - expect(mockWindowRef.nativeWindow.location.href).toBe(''); + expect(sidebarStatusService.closeSidebar).toHaveBeenCalled(); + expect(mockWindowRef.nativeWindow.location.href).toBe(''); + }) + ); - componentInstance.navigateToClassroomPage('/classroom/url'); - tick(151); + it( + 'should navigate to classroom page when user clicks on' + + "'Basic Mathematics'", + fakeAsync(() => { + expect(mockWindowRef.nativeWindow.location.href).toBe(''); - expect(mockWindowRef.nativeWindow.location.href).toBe('/classroom/url'); - })); + componentInstance.navigateToClassroomPage('/classroom/url'); + tick(151); - it('should registers classroom header click event when user clicks' + - ' on \'Basic Mathematics\'', () => { - spyOn(siteAnalyticsService, 'registerClassroomHeaderClickEvent'); + expect(mockWindowRef.nativeWindow.location.href).toBe('/classroom/url'); + }) + ); - componentInstance.navigateToClassroomPage('/classroom/url'); + it( + 'should registers classroom header click event when user clicks' + + " on 'Basic Mathematics'", + () => { + spyOn(siteAnalyticsService, 'registerClassroomHeaderClickEvent'); - expect(siteAnalyticsService.registerClassroomHeaderClickEvent) - .toHaveBeenCalled(); - }); + componentInstance.navigateToClassroomPage('/classroom/url'); - it('should populate properties properly on component initialization', - fakeAsync(() => { - let userInfo = new UserInfo( - ['USER_ROLE'], true, false, false, false, true, - 'en', 'username1', 'tester@example.com', true - ); - spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo); - componentInstance.ngOnInit(); - tick(); - expect(componentInstance.userIsLoggedIn).toBeTrue(); - })); + expect( + siteAnalyticsService.registerClassroomHeaderClickEvent + ).toHaveBeenCalled(); + } + ); + + it('should populate properties properly on component initialization', fakeAsync(() => { + let userInfo = new UserInfo( + ['USER_ROLE'], + true, + false, + false, + false, + true, + 'en', + 'username1', + 'tester@example.com', + true + ); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo); + componentInstance.ngOnInit(); + tick(); + expect(componentInstance.userIsLoggedIn).toBeTrue(); + })); it('should check whether hacky translations are displayed or not', () => { - spyOn(i18nLanguageCodeService, 'isHackyTranslationAvailable') - .and.returnValues(false, true); - spyOn(i18nLanguageCodeService, 'isCurrentLanguageEnglish') - .and.returnValues(false, false); + spyOn( + i18nLanguageCodeService, + 'isHackyTranslationAvailable' + ).and.returnValues(false, true); + spyOn(i18nLanguageCodeService, 'isCurrentLanguageEnglish').and.returnValues( + false, + false + ); let hackyStoryTitleTranslationIsDisplayed = componentInstance.isHackyTopicTitleTranslationDisplayed(0); diff --git a/core/templates/components/common-layout-directives/navigation-bars/side-navigation-bar.component.ts b/core/templates/components/common-layout-directives/navigation-bars/side-navigation-bar.component.ts index 7be0c4e4e4ae..cbbd2935e600 100644 --- a/core/templates/components/common-layout-directives/navigation-bars/side-navigation-bar.component.ts +++ b/core/templates/components/common-layout-directives/navigation-bars/side-navigation-bar.component.ts @@ -16,21 +16,21 @@ * @fileoverview Component for the side navigation bar. */ -import { Component, Input } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { AppConstants } from 'app.constants'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { UserService } from 'services/user.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { CreatorTopicSummary } from 'domain/topic/creator-topic-summary.model'; -import { SidebarStatusService } from 'services/sidebar-status.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; - - @Component({ - selector: 'oppia-side-navigation-bar', - templateUrl: './side-navigation-bar.component.html' - }) +import {Component, Input} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {AppConstants} from 'app.constants'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {UserService} from 'services/user.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {CreatorTopicSummary} from 'domain/topic/creator-topic-summary.model'; +import {SidebarStatusService} from 'services/sidebar-status.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; + +@Component({ + selector: 'oppia-side-navigation-bar', + templateUrl: './side-navigation-bar.component.html', +}) export class SideNavigationBarComponent { // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see @@ -45,8 +45,7 @@ export class SideNavigationBarComponent { learnSubmenuIsShown: boolean = true; userIsLoggedIn!: boolean; - PAGES_REGISTERED_WITH_FRONTEND = ( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND); + PAGES_REGISTERED_WITH_FRONTEND = AppConstants.PAGES_REGISTERED_WITH_FRONTEND; constructor( private i18nLanguageCodeService: I18nLanguageCodeService, @@ -64,14 +63,15 @@ export class SideNavigationBarComponent { ngOnInit(): void { this.currentUrl = this.windowRef.nativeWindow.location.pathname; - this.userService.getUserInfoAsync().then((userInfo) => { + this.userService.getUserInfoAsync().then(userInfo => { this.userIsLoggedIn = userInfo.isLoggedIn(); }); } navigateToDefaultDashboard(): void { - this.userService.getUserPreferredDashboardAsync().then( - (preferredDashboard) => { + this.userService + .getUserPreferredDashboardAsync() + .then(preferredDashboard => { if (this.currentUrl === '/' + preferredDashboard + '-dashboard') { this.sidebarStatusService.closeSidebar(); return; @@ -93,8 +93,7 @@ export class SideNavigationBarComponent { } togglegetinvolvedSubmenu(): void { - this.getinvolvedSubmenuIsShown = - !this.getinvolvedSubmenuIsShown; + this.getinvolvedSubmenuIsShown = !this.getinvolvedSubmenuIsShown; } navigateToClassroomPage(classroomUrl: string): void { @@ -113,7 +112,9 @@ export class SideNavigationBarComponent { } } -angular.module('oppia').directive('oppiaSideNavigationBar', +angular.module('oppia').directive( + 'oppiaSideNavigationBar', downgradeComponent({ - component: SideNavigationBarComponent - }) as angular.IDirectiveFactory); + component: SideNavigationBarComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/components/common-layout-directives/navigation-bars/top-navigation-bar.component.spec.ts b/core/templates/components/common-layout-directives/navigation-bars/top-navigation-bar.component.spec.ts index 8d68c6bc6f99..ac7e1c921e97 100644 --- a/core/templates/components/common-layout-directives/navigation-bars/top-navigation-bar.component.spec.ts +++ b/core/templates/components/common-layout-directives/navigation-bars/top-navigation-bar.component.spec.ts @@ -16,38 +16,44 @@ * @fileoverview Unit tests for TopNavigationBarComponent. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, flush, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { DeviceInfoService } from 'services/contextual/device-info.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { EventToCodes, NavigationService } from 'services/navigation.service'; -import { SearchService } from 'services/search.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { UserService } from 'services/user.service'; -import { AlertsService } from 'services/alerts.service'; -import { MockI18nService, MockTranslatePipe } from 'tests/unit-test-utils'; -import { TopNavigationBarComponent } from './top-navigation-bar.component'; -import { SidebarStatusService } from 'services/sidebar-status.service'; -import { UserInfo } from 'domain/user/user-info.model'; -import { FeedbackUpdatesBackendApiService } from 'domain/feedback_updates/feedback-updates-backend-api.service'; -import { FeedbackThreadSummary } from - 'domain/feedback_thread/feedback-thread-summary.model'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { I18nService } from 'i18n/i18n.service'; -import { CookieService } from 'ngx-cookie'; -import { PlatformFeatureService } from 'services/platform-feature.service'; -import { LearnerGroupBackendApiService } from 'domain/learner_group/learner-group-backend-api.service'; -import { AppConstants } from 'app.constants'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + flush, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; +import {DeviceInfoService} from 'services/contextual/device-info.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {EventToCodes, NavigationService} from 'services/navigation.service'; +import {SearchService} from 'services/search.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {UserService} from 'services/user.service'; +import {AlertsService} from 'services/alerts.service'; +import {MockI18nService, MockTranslatePipe} from 'tests/unit-test-utils'; +import {TopNavigationBarComponent} from './top-navigation-bar.component'; +import {SidebarStatusService} from 'services/sidebar-status.service'; +import {UserInfo} from 'domain/user/user-info.model'; +import {FeedbackUpdatesBackendApiService} from 'domain/feedback_updates/feedback-updates-backend-api.service'; +import {FeedbackThreadSummary} from 'domain/feedback_thread/feedback-thread-summary.model'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {I18nService} from 'i18n/i18n.service'; +import {CookieService} from 'ngx-cookie'; +import {PlatformFeatureService} from 'services/platform-feature.service'; +import {LearnerGroupBackendApiService} from 'domain/learner_group/learner-group-backend-api.service'; +import {AppConstants} from 'app.constants'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; class MockPlatformFeatureService { status = { ShowFeedbackUpdatesInProfilePicDropdownMenu: { - isEnabled: false - } + isEnabled: false, + }, }; } @@ -59,27 +65,27 @@ class MockWindowRef { reload: () => {}, toString: () => { return 'http://localhost:8181/?lang=es'; - } + }, }, localStorage: { last_uploaded_audio_lang: 'en', - removeItem: (name: string) => {} + removeItem: (name: string) => {}, }, sessionStorage: { last_uploaded_audio_lang: 'en', - removeItem: (name: string) => {} + removeItem: (name: string) => {}, }, gtag: () => {}, history: { - pushState(data: object, title: string, url?: string | null) {} + pushState(data: object, title: string, url?: string | null) {}, }, document: { body: { style: { overflowY: 'auto', - } - } - } + }, + }, + }, }; } @@ -95,46 +101,47 @@ describe('TopNavigationBarComponent', () => { let navigationService: NavigationService; let deviceInfoService: DeviceInfoService; let sidebarStatusService: SidebarStatusService; - let feedbackUpdatesBackendApiService: - FeedbackUpdatesBackendApiService; + let feedbackUpdatesBackendApiService: FeedbackUpdatesBackendApiService; let learnerGroupBackendApiService: LearnerGroupBackendApiService; let i18nLanguageCodeService: I18nLanguageCodeService; let i18nService: I18nService; let mockPlatformFeatureService = new MockPlatformFeatureService(); let urlInterpolationService: UrlInterpolationService; - let threadSummaryList = [{ - status: 'open', - original_author_id: '1', - last_updated_msecs: 1000, - last_message_text: 'Last Message', - total_message_count: 5, - last_message_is_read: false, - second_last_message_is_read: true, - author_last_message: '2', - author_second_last_message: 'Last Message', - exploration_title: 'Biology', - exploration_id: 'exp1', - thread_id: 'thread_1' - }, - { - status: 'open', - original_author_id: '2', - last_updated_msecs: 1001, - last_message_text: 'Last Message', - total_message_count: 5, - last_message_is_read: false, - second_last_message_is_read: true, - author_last_message: '2', - author_second_last_message: 'Last Message', - exploration_title: 'Algebra', - exploration_id: 'exp1', - thread_id: 'thread_1' - }]; + let threadSummaryList = [ + { + status: 'open', + original_author_id: '1', + last_updated_msecs: 1000, + last_message_text: 'Last Message', + total_message_count: 5, + last_message_is_read: false, + second_last_message_is_read: true, + author_last_message: '2', + author_second_last_message: 'Last Message', + exploration_title: 'Biology', + exploration_id: 'exp1', + thread_id: 'thread_1', + }, + { + status: 'open', + original_author_id: '2', + last_updated_msecs: 1001, + last_message_text: 'Last Message', + total_message_count: 5, + last_message_is_read: false, + second_last_message_is_read: true, + author_last_message: '2', + author_second_last_message: 'Last Message', + exploration_title: 'Algebra', + exploration_id: 'exp1', + thread_id: 'thread_1', + }, + ]; let FeedbackUpdatesData = { thread_summaries: threadSummaryList, - number_of_unread_threads: 10 + number_of_unread_threads: 10, }; let mockResizeEmitter: EventEmitter; @@ -143,14 +150,8 @@ describe('TopNavigationBarComponent', () => { mockResizeEmitter = new EventEmitter(); mockWindowRef = new MockWindowRef(); TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - NgbModule, - ], - declarations: [ - TopNavigationBarComponent, - MockTranslatePipe - ], + imports: [HttpClientTestingModule, NgbModule], + declarations: [TopNavigationBarComponent, MockTranslatePipe], providers: [ NavigationService, CookieService, @@ -159,26 +160,26 @@ describe('TopNavigationBarComponent', () => { UserService, { provide: I18nService, - useClass: MockI18nService + useClass: MockI18nService, }, { provide: WindowRef, - useValue: mockWindowRef + useValue: mockWindowRef, }, { provide: WindowDimensionsService, useValue: { getWidth: () => 700, getResizeEvent: () => mockResizeEmitter, - isWindowNarrow: () => false - } + isWindowNarrow: () => false, + }, }, { provide: PlatformFeatureService, - useValue: mockPlatformFeatureService - } + useValue: mockPlatformFeatureService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -193,18 +194,23 @@ describe('TopNavigationBarComponent', () => { deviceInfoService = TestBed.inject(DeviceInfoService); sidebarStatusService = TestBed.inject(SidebarStatusService); i18nService = TestBed.inject(I18nService); - feedbackUpdatesBackendApiService = - TestBed.inject(FeedbackUpdatesBackendApiService); + feedbackUpdatesBackendApiService = TestBed.inject( + FeedbackUpdatesBackendApiService + ); alertsService = TestBed.inject(AlertsService); learnerGroupBackendApiService = TestBed.inject( - LearnerGroupBackendApiService); + LearnerGroupBackendApiService + ); i18nLanguageCodeService = TestBed.inject(I18nLanguageCodeService); urlInterpolationService = TestBed.inject(UrlInterpolationService); - spyOn(searchService, 'onSearchBarLoaded') - .and.returnValue(new EventEmitter()); - spyOn(userService, 'getProfileImageDataUrl').and.returnValue( - ['default-image-url-png', 'default-image-url-webp']); + spyOn(searchService, 'onSearchBarLoaded').and.returnValue( + new EventEmitter() + ); + spyOn(userService, 'getProfileImageDataUrl').and.returnValue([ + 'default-image-url-png', + 'default-image-url-webp', + ]); }); afterEach(() => { @@ -233,46 +239,54 @@ describe('TopNavigationBarComponent', () => { expect(component.directiveSubscriptions.unsubscribe).toHaveBeenCalled(); }); - it('should try displaying the hidden navbar elements if resized' + - ' window is larger', fakeAsync(() => { - let donateElement = 'I18N_TOPNAV_DONATE'; - spyOn(component, 'truncateNavbar').and.stub(); + it( + 'should try displaying the hidden navbar elements if resized' + + ' window is larger', + fakeAsync(() => { + let donateElement = 'I18N_TOPNAV_DONATE'; + spyOn(component, 'truncateNavbar').and.stub(); - component.ngOnInit(); - tick(10); + component.ngOnInit(); + tick(10); - component.currentWindowWidth = 600; - component.navElementsVisibilityStatus[donateElement] = false; + component.currentWindowWidth = 600; + component.navElementsVisibilityStatus[donateElement] = false; - mockResizeEmitter.emit(); - tick(501); + mockResizeEmitter.emit(); + tick(501); - fixture.whenStable().then(() => { - expect(component.navElementsVisibilityStatus[donateElement]).toBe(true); - }); - })); + fixture.whenStable().then(() => { + expect(component.navElementsVisibilityStatus[donateElement]).toBe(true); + }); + }) + ); - it('should show Oppia\'s logos', () => { - expect(component.getStaticImageUrl('/logo/288x128_logo_white.webp')) - .toBe('/assets/images/logo/288x128_logo_white.webp'); + it("should show Oppia's logos", () => { + expect(component.getStaticImageUrl('/logo/288x128_logo_white.webp')).toBe( + '/assets/images/logo/288x128_logo_white.webp' + ); - expect(component.getStaticImageUrl('/logo/288x128_logo_white.png')) - .toBe('/assets/images/logo/288x128_logo_white.png'); + expect(component.getStaticImageUrl('/logo/288x128_logo_white.png')).toBe( + '/assets/images/logo/288x128_logo_white.png' + ); }); - it('should fetch login URL and redirect user to login page when user' + - ' clicks on \'Sign In\'', fakeAsync(() => { - spyOn(userService, 'getLoginUrlAsync').and.resolveTo('/login/url'); + it( + 'should fetch login URL and redirect user to login page when user' + + " clicks on 'Sign In'", + fakeAsync(() => { + spyOn(userService, 'getLoginUrlAsync').and.resolveTo('/login/url'); - expect(mockWindowRef.nativeWindow.location.href).toBe(''); + expect(mockWindowRef.nativeWindow.location.href).toBe(''); - component.onLoginButtonClicked(); - tick(151); + component.onLoginButtonClicked(); + tick(151); - fixture.whenStable().then(() => { - expect(mockWindowRef.nativeWindow.location.href).toBe('/login/url'); - }); - })); + fixture.whenStable().then(() => { + expect(mockWindowRef.nativeWindow.location.href).toBe('/login/url'); + }); + }) + ); it('should reload window if fetched login URL is null', fakeAsync(() => { spyOn(userService, 'getLoginUrlAsync').and.resolveTo(''); @@ -288,30 +302,36 @@ describe('TopNavigationBarComponent', () => { }); })); - it('should register start login event when user is being redirected to' + - ' the login page', fakeAsync(() => { - spyOn(userService, 'getLoginUrlAsync').and.resolveTo('/login/url'); - spyOn(siteAnalyticsService, 'registerStartLoginEvent'); + it( + 'should register start login event when user is being redirected to' + + ' the login page', + fakeAsync(() => { + spyOn(userService, 'getLoginUrlAsync').and.resolveTo('/login/url'); + spyOn(siteAnalyticsService, 'registerStartLoginEvent'); - component.onLoginButtonClicked(); - tick(151); + component.onLoginButtonClicked(); + tick(151); - fixture.whenStable().then(() => { - expect(siteAnalyticsService.registerStartLoginEvent) - .toHaveBeenCalledWith('loginButton'); - }); - })); + fixture.whenStable().then(() => { + expect( + siteAnalyticsService.registerStartLoginEvent + ).toHaveBeenCalledWith('loginButton'); + }); + }) + ); it('should clear last uploaded audio language on logout', () => { spyOn(mockWindowRef.nativeWindow.localStorage, 'removeItem'); - expect(mockWindowRef.nativeWindow.localStorage.last_uploaded_audio_lang) - .toBe('en'); + expect( + mockWindowRef.nativeWindow.localStorage.last_uploaded_audio_lang + ).toBe('en'); component.onLogoutButtonClicked(); - expect(mockWindowRef.nativeWindow.localStorage.removeItem) - .toHaveBeenCalledWith('last_uploaded_audio_lang'); + expect( + mockWindowRef.nativeWindow.localStorage.removeItem + ).toHaveBeenCalledWith('last_uploaded_audio_lang'); }); it('should open submenu when user hovers over the menu button', () => { @@ -322,22 +342,28 @@ describe('TopNavigationBarComponent', () => { component.openSubmenu(mouseoverEvent, 'learnMenu'); expect(navigationService.openSubmenu).toHaveBeenCalledWith( - mouseoverEvent, 'learnMenu'); + mouseoverEvent, + 'learnMenu' + ); }); - it('should close submenu when user moves the mouse away' + - ' from the menu button', () => { - let mouseleaveEvent = new KeyboardEvent('mouseleave'); - spyOn(navigationService, 'closeSubmenu'); - spyOn(deviceInfoService, 'isMobileDevice').and.returnValue(false); + it( + 'should close submenu when user moves the mouse away' + + ' from the menu button', + () => { + let mouseleaveEvent = new KeyboardEvent('mouseleave'); + spyOn(navigationService, 'closeSubmenu'); + spyOn(deviceInfoService, 'isMobileDevice').and.returnValue(false); - component.closeSubmenuIfNotMobile(mouseleaveEvent); + component.closeSubmenuIfNotMobile(mouseleaveEvent); - expect(navigationService.closeSubmenu).toHaveBeenCalledWith( - mouseleaveEvent); - }); + expect(navigationService.closeSubmenu).toHaveBeenCalledWith( + mouseleaveEvent + ); + } + ); - it('should not close the submenu is the user is on a mobile device', () =>{ + it('should not close the submenu is the user is on a mobile device', () => { spyOn(deviceInfoService, 'isMobileDevice').and.returnValue(true); spyOn(navigationService, 'closeSubmenu'); @@ -349,7 +375,7 @@ describe('TopNavigationBarComponent', () => { it('should handle keydown events on menus', () => { let keydownEvent = new KeyboardEvent('click', { shiftKey: true, - keyCode: 9 + keyCode: 9, }); expect(component.activeMenuName).toBe(undefined); @@ -364,7 +390,12 @@ describe('TopNavigationBarComponent', () => { it('should toggle side bar', () => { const clickEvent = new CustomEvent('click'); spyOn(sidebarStatusService, 'isSidebarShown').and.returnValues( - false, true, true, false, false); + false, + true, + true, + false, + false + ); spyOn(wds, 'isWindowNarrow').and.returnValue(true); spyOn(sidebarStatusService, 'toggleHamburgerIconStatus'); spyOn(clickEvent, 'stopPropagation'); @@ -377,29 +408,37 @@ describe('TopNavigationBarComponent', () => { expect(component.isSidebarShown()).toBe(false); }); - it('should navigate to classroom page when user clicks' + - ' on \'Basic Mathematics\'', fakeAsync(() => { - expect(mockWindowRef.nativeWindow.location.href).toBe(''); + it( + 'should navigate to classroom page when user clicks' + + " on 'Basic Mathematics'", + fakeAsync(() => { + expect(mockWindowRef.nativeWindow.location.href).toBe(''); - component.navigateToClassroomPage('/classroom/url'); - tick(151); + component.navigateToClassroomPage('/classroom/url'); + tick(151); - expect(mockWindowRef.nativeWindow.location.href).toBe('/classroom/url'); - })); + expect(mockWindowRef.nativeWindow.location.href).toBe('/classroom/url'); + }) + ); - it('should registers classroom header click event when user clicks' + - ' on \'Basic Mathematics\'', () => { - spyOn(siteAnalyticsService, 'registerClassroomHeaderClickEvent'); + it( + 'should registers classroom header click event when user clicks' + + " on 'Basic Mathematics'", + () => { + spyOn(siteAnalyticsService, 'registerClassroomHeaderClickEvent'); - component.navigateToClassroomPage('/classroom/url'); + component.navigateToClassroomPage('/classroom/url'); - expect(siteAnalyticsService.registerClassroomHeaderClickEvent) - .toHaveBeenCalled(); - }); + expect( + siteAnalyticsService.registerClassroomHeaderClickEvent + ).toHaveBeenCalled(); + } + ); it('should check if i18n has been run', () => { spyOn(document, 'querySelectorAll') - .withArgs('.oppia-navbar-tab-content').and.returnValues( + .withArgs('.oppia-navbar-tab-content') + .and.returnValues( [ { // This throws "Type '{ innerText: string; }' is not assignable to @@ -407,13 +446,13 @@ describe('TopNavigationBarComponent', () => { // has not run, then the tabs will not have text content and so // their innerText.length value will be 0. // @ts-expect-error - innerText: '' - } + innerText: '', + }, ], [ { - innerText: 'About' - } + innerText: 'About', + }, ] ); @@ -436,32 +475,35 @@ describe('TopNavigationBarComponent', () => { expect(document.querySelector).not.toHaveBeenCalled(); }); - it('should retry calling truncate navbar if i18n is not' + - ' complete', fakeAsync(() => { - spyOn(wds, 'isWindowNarrow').and.returnValues(false, true); - spyOn(document, 'querySelector').and.stub(); - spyOn(component, 'checkIfI18NCompleted').and.returnValue(false); + it( + 'should retry calling truncate navbar if i18n is not' + ' complete', + fakeAsync(() => { + spyOn(wds, 'isWindowNarrow').and.returnValues(false, true); + spyOn(document, 'querySelector').and.stub(); + spyOn(component, 'checkIfI18NCompleted').and.returnValue(false); - component.truncateNavbar(); - tick(101); + component.truncateNavbar(); + tick(101); - fixture.whenStable().then(() => { - expect(document.querySelector).not.toHaveBeenCalled(); - }); - })); + fixture.whenStable().then(() => { + expect(document.querySelector).not.toHaveBeenCalled(); + }); + }) + ); - it('should hide navbar if it\'s height more than 60px', fakeAsync(() => { + it("should hide navbar if it's height more than 60px", fakeAsync(() => { spyOn(wds, 'isWindowNarrow').and.returnValues(false, true); spyOn(document, 'querySelector') - // This throws "Type '{ clientWidth: number; }' is missing the following - // properties from type 'Element': assignedSlot, attributes, classList, - // className, and 122 more.". We need to suppress this error because - // typescript expects around 120 more properties than just one - // (clientWidth). We need only one 'clientWidth' for - // testing purposes. - // @ts-expect-error - .withArgs('div.collapse.navbar-collapse').and.returnValue({ - clientHeight: 61 + .withArgs('div.collapse.navbar-collapse') + // This throws "Type '{ clientWidth: number; }' is missing the following + // properties from type 'Element': assignedSlot, attributes, classList, + // className, and 122 more.". We need to suppress this error because + // typescript expects around 120 more properties than just one + // (clientWidth). We need only one 'clientWidth' for + // testing purposes. + // @ts-expect-error + .and.returnValue({ + clientHeight: 61, }); component.navElementsVisibilityStatus = { @@ -469,7 +511,7 @@ describe('TopNavigationBarComponent', () => { I18N_TOPNAV_LEARN: true, I18N_TOPNAV_ABOUT: true, I18N_TOPNAV_LIBRARY: true, - I18N_TOPNAV_HOME: true + I18N_TOPNAV_HOME: true, }; component.truncateNavbar(); @@ -481,25 +523,30 @@ describe('TopNavigationBarComponent', () => { I18N_TOPNAV_LEARN: true, I18N_TOPNAV_ABOUT: true, I18N_TOPNAV_LIBRARY: true, - I18N_TOPNAV_HOME: true + I18N_TOPNAV_HOME: true, }); }); })); - it('should change the language when user clicks on new language' + - ' from dropdown', () => { - let langCode = 'hi'; - spyOn(i18nService, 'updateUserPreferredLanguage'); - component.changeLanguage(langCode); - expect(i18nService.updateUserPreferredLanguage).toHaveBeenCalledWith( - langCode); - }); + it( + 'should change the language when user clicks on new language' + + ' from dropdown', + () => { + let langCode = 'hi'; + spyOn(i18nService, 'updateUserPreferredLanguage'); + component.changeLanguage(langCode); + expect(i18nService.updateUserPreferredLanguage).toHaveBeenCalledWith( + langCode + ); + } + ); it('should check if learner groups feature is enabled', fakeAsync(() => { spyOn(component, 'truncateNavbar').and.stub(); spyOn( - learnerGroupBackendApiService, 'isLearnerGroupFeatureEnabledAsync') - .and.resolveTo(true); + learnerGroupBackendApiService, + 'isLearnerGroupFeatureEnabledAsync' + ).and.resolveTo(true); component.ngOnInit(); tick(); @@ -507,32 +554,46 @@ describe('TopNavigationBarComponent', () => { expect(component.LEARNER_GROUPS_FEATURE_IS_ENABLED).toBe(true); })); - it('should change current language code on' + - ' I18nLanguageCode change', fakeAsync(() => { - let onI18nLanguageCodeChangeEmitter = new EventEmitter(); - spyOnProperty(i18nLanguageCodeService, 'onI18nLanguageCodeChange') - .and.returnValue(onI18nLanguageCodeChangeEmitter); - spyOn(component, 'truncateNavbar').and.stub(); + it( + 'should change current language code on' + ' I18nLanguageCode change', + fakeAsync(() => { + let onI18nLanguageCodeChangeEmitter = new EventEmitter(); + spyOnProperty( + i18nLanguageCodeService, + 'onI18nLanguageCodeChange' + ).and.returnValue(onI18nLanguageCodeChangeEmitter); + spyOn(component, 'truncateNavbar').and.stub(); - component.ngOnInit(); + component.ngOnInit(); - component.currentLanguageCode = 'hi'; + component.currentLanguageCode = 'hi'; - onI18nLanguageCodeChangeEmitter.emit('en'); - tick(); + onI18nLanguageCodeChangeEmitter.emit('en'); + tick(); - expect(component.currentLanguageCode).toBe('en'); - })); + expect(component.currentLanguageCode).toBe('en'); + }) + ); it('should get user information on initialization', fakeAsync(() => { let userInfo = new UserInfo( - ['USER_ROLE'], true, false, false, false, true, - 'en', 'username1', 'tester@example.com', true + ['USER_ROLE'], + true, + false, + false, + false, + true, + 'en', + 'username1', + 'tester@example.com', + true ); spyOn(component, 'truncateNavbar').and.stub(); spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo); - spyOn(i18nLanguageCodeService, 'getCurrentI18nLanguageCode') - .and.returnValue('en'); + spyOn( + i18nLanguageCodeService, + 'getCurrentI18nLanguageCode' + ).and.returnValue('en'); expect(component.isModerator).toBe(false); expect(component.isCurriculumAdmin).toBe(false); @@ -552,121 +613,150 @@ describe('TopNavigationBarComponent', () => { expect(component.userIsLoggedIn).toBe(true); expect(component.username).toBe('username1'); expect(component.profilePageUrl).toBe('/profile/username1'); - expect(component.profilePicturePngDataUrl).toEqual( - 'default-image-url-png'); + expect(component.profilePicturePngDataUrl).toEqual('default-image-url-png'); expect(component.profilePictureWebpDataUrl).toEqual( - 'default-image-url-webp'); + 'default-image-url-webp' + ); })); - it('should set default profile pictures when username is null', - fakeAsync(() => { - spyOn(component, 'truncateNavbar').and.stub(); - let userInfo = { - isModerator: () => false, - isCurriculumAdmin: () => false, - isTopicManager: () => false, - isSuperAdmin: () => false, - isBlogAdmin: () => false, - isBlogPostEditor: () => false, - isLoggedIn: () => true, - getUsername: () => null - }; - - spyOn(userService, 'getUserInfoAsync') - .and.resolveTo(userInfo as UserInfo); + it('should set default profile pictures when username is null', fakeAsync(() => { + spyOn(component, 'truncateNavbar').and.stub(); + let userInfo = { + isModerator: () => false, + isCurriculumAdmin: () => false, + isTopicManager: () => false, + isSuperAdmin: () => false, + isBlogAdmin: () => false, + isBlogPostEditor: () => false, + isLoggedIn: () => true, + getUsername: () => null, + }; - component.ngOnInit(); - tick(); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo as UserInfo); - expect(component.profilePicturePngDataUrl).toBe( - urlInterpolationService.getStaticImageUrl( - AppConstants.DEFAULT_PROFILE_IMAGE_PNG_PATH)); - expect(component.profilePictureWebpDataUrl).toBe( - urlInterpolationService.getStaticImageUrl( - AppConstants.DEFAULT_PROFILE_IMAGE_WEBP_PATH)); - })); + component.ngOnInit(); + tick(); - it('should fetch the number of unread feedback' + - 'when user is logged In', fakeAsync(() => { - let userInfo = new UserInfo( - ['USER_ROLE'], true, false, false, false, true, - 'en', 'username1', 'tester@example.com', true + expect(component.profilePicturePngDataUrl).toBe( + urlInterpolationService.getStaticImageUrl( + AppConstants.DEFAULT_PROFILE_IMAGE_PNG_PATH + ) ); + expect(component.profilePictureWebpDataUrl).toBe( + urlInterpolationService.getStaticImageUrl( + AppConstants.DEFAULT_PROFILE_IMAGE_WEBP_PATH + ) + ); + })); - spyOn(component, 'truncateNavbar').and.stub(); - spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo); - const fetchDataSpy = spyOn( - feedbackUpdatesBackendApiService, - 'fetchFeedbackUpdatesDataAsync').and.returnValue(Promise.resolve({ - numberOfUnreadThreads: FeedbackUpdatesData. - number_of_unread_threads, - threadSummaries: ( - FeedbackUpdatesData.thread_summaries.map( - threadSummary => FeedbackThreadSummary - .createFromBackendDict(threadSummary))), - paginatedThreadsList: [] - })); - component.userIsLoggedIn = true; + it( + 'should fetch the number of unread feedback' + 'when user is logged In', + fakeAsync(() => { + let userInfo = new UserInfo( + ['USER_ROLE'], + true, + false, + false, + false, + true, + 'en', + 'username1', + 'tester@example.com', + true + ); - component.ngOnInit(); - tick(); + spyOn(component, 'truncateNavbar').and.stub(); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo); + const fetchDataSpy = spyOn( + feedbackUpdatesBackendApiService, + 'fetchFeedbackUpdatesDataAsync' + ).and.returnValue( + Promise.resolve({ + numberOfUnreadThreads: FeedbackUpdatesData.number_of_unread_threads, + threadSummaries: FeedbackUpdatesData.thread_summaries.map( + threadSummary => + FeedbackThreadSummary.createFromBackendDict(threadSummary) + ), + paginatedThreadsList: [], + }) + ); + component.userIsLoggedIn = true; - expect(component.unreadThreadsCount).toBe(10); - expect(fetchDataSpy).toHaveBeenCalled(); - })); + component.ngOnInit(); + tick(); - it('should show an alert when fails to' + - 'get the feedback updates data', fakeAsync(() => { - let userInfo = new UserInfo( - ['USER_ROLE'], true, false, false, false, true, - 'en', 'username1', 'tester@example.com', true - ); + expect(component.unreadThreadsCount).toBe(10); + expect(fetchDataSpy).toHaveBeenCalled(); + }) + ); - spyOn(component, 'truncateNavbar').and.stub(); - spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo); - const fetchDataSpy = spyOn( - feedbackUpdatesBackendApiService, - 'fetchFeedbackUpdatesDataAsync') - .and.rejectWith(404); - const alertsSpy = spyOn(alertsService, 'addWarning').and.callThrough(); + it( + 'should show an alert when fails to' + 'get the feedback updates data', + fakeAsync(() => { + let userInfo = new UserInfo( + ['USER_ROLE'], + true, + false, + false, + false, + true, + 'en', + 'username1', + 'tester@example.com', + true + ); - component.userIsLoggedIn = true; - component.ngOnInit(); - tick(); - fixture.detectChanges(); + spyOn(component, 'truncateNavbar').and.stub(); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo); + const fetchDataSpy = spyOn( + feedbackUpdatesBackendApiService, + 'fetchFeedbackUpdatesDataAsync' + ).and.rejectWith(404); + const alertsSpy = spyOn(alertsService, 'addWarning').and.callThrough(); + + component.userIsLoggedIn = true; + component.ngOnInit(); + tick(); + fixture.detectChanges(); - expect(alertsSpy).toHaveBeenCalledWith( - 'Failed to get number of unread thread of feedback updates'); - expect(fetchDataSpy).toHaveBeenCalled(); - flush(); - })); + expect(alertsSpy).toHaveBeenCalledWith( + 'Failed to get number of unread thread of feedback updates' + ); + expect(fetchDataSpy).toHaveBeenCalled(); + flush(); + }) + ); - it('should return proper offset for dropdown', ()=>{ + it('should return proper offset for dropdown', () => { var dummyElement = document.createElement('div'); spyOn(document, 'querySelector').and.returnValue(dummyElement); spyOn(Element.prototype, 'getBoundingClientRect').and.callFake( - jasmine.createSpy('getBoundingClientRect').and - .returnValue({ top: 1, height: 100, left: 0, width: 200, right: 202 }) + jasmine + .createSpy('getBoundingClientRect') + .and.returnValue({top: 1, height: 100, left: 0, width: 200, right: 202}) ); expect(component.getDropdownOffset('.dummy', 0)).toBe(0); }); - it('should return proper offset for learn dropdown when element is undefined', - ()=>{ - spyOn(document, 'querySelector').and.returnValue(null); + it('should return proper offset for learn dropdown when element is undefined', () => { + spyOn(document, 'querySelector').and.returnValue(null); - expect(component.getDropdownOffset('.dummy', 0)).toBe(0); - }); + expect(component.getDropdownOffset('.dummy', 0)).toBe(0); + }); - it('should check if dropdown offsets are updated', fakeAsync (()=>{ + it('should check if dropdown offsets are updated', fakeAsync(() => { spyOn(component, 'truncateNavbar').and.stub(); spyOn(component, 'getDropdownOffset') - .withArgs('.learn-tab', 688).and.returnValue(-10) - .withArgs('.learn-tab', 300).and.returnValue(-10) - .withArgs('.donate-tab', 286).and.returnValue(-10) - .withArgs('.get-involved', 574).and.returnValue(-10); + .withArgs('.learn-tab', 688) + .and.returnValue(-10) + .withArgs('.learn-tab', 300) + .and.returnValue(-10) + .withArgs('.donate-tab', 286) + .and.returnValue(-10) + .withArgs('.get-involved', 574) + .and.returnValue(-10); expect(component.learnDropdownOffset).toBe(0); expect(component.getInvolvedMenuOffset).toBe(0); @@ -681,10 +771,14 @@ describe('TopNavigationBarComponent', () => { })); it('should check whether hacky translations are displayed or not', () => { - spyOn(i18nLanguageCodeService, 'isHackyTranslationAvailable') - .and.returnValues(false, true); - spyOn(i18nLanguageCodeService, 'isCurrentLanguageEnglish') - .and.returnValues(false, false); + spyOn( + i18nLanguageCodeService, + 'isHackyTranslationAvailable' + ).and.returnValues(false, true); + spyOn(i18nLanguageCodeService, 'isCurrentLanguageEnglish').and.returnValues( + false, + false + ); let hackyStoryTitleTranslationIsDisplayed = component.isHackyTopicTitleTranslationDisplayed(0); @@ -694,19 +788,20 @@ describe('TopNavigationBarComponent', () => { expect(hackyStoryTitleTranslationIsDisplayed).toBe(true); }); - it('should return correct value for show feedback updates' + - 'in profile pic drop down menu feature flag', () => { - expect( - component. - isShowFeedbackUpdatesInProfilepicDropdownFeatureFlagEnable()) - .toBeFalse(); + it( + 'should return correct value for show feedback updates' + + 'in profile pic drop down menu feature flag', + () => { + expect( + component.isShowFeedbackUpdatesInProfilepicDropdownFeatureFlagEnable() + ).toBeFalse(); - mockPlatformFeatureService.status. - ShowFeedbackUpdatesInProfilePicDropdownMenu.isEnabled = true; + mockPlatformFeatureService.status.ShowFeedbackUpdatesInProfilePicDropdownMenu.isEnabled = + true; - expect( - component. - isShowFeedbackUpdatesInProfilepicDropdownFeatureFlagEnable()) - .toBeTrue(); - }); + expect( + component.isShowFeedbackUpdatesInProfilepicDropdownFeatureFlagEnable() + ).toBeTrue(); + } + ); }); diff --git a/core/templates/components/common-layout-directives/navigation-bars/top-navigation-bar.component.ts b/core/templates/components/common-layout-directives/navigation-bars/top-navigation-bar.component.ts index ddd6b1d8a40d..df2544231f35 100644 --- a/core/templates/components/common-layout-directives/navigation-bars/top-navigation-bar.component.ts +++ b/core/templates/components/common-layout-directives/navigation-bars/top-navigation-bar.component.ts @@ -18,30 +18,36 @@ * the editor pages). */ -import { Subscription } from 'rxjs'; -import { ContextService } from 'services/context.service'; -import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; -import { SidebarStatusService } from 'services/sidebar-status.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { UserService } from 'services/user.service'; -import { DeviceInfoService } from 'services/contextual/device-info.service'; +import {Subscription} from 'rxjs'; +import {ContextService} from 'services/context.service'; +import { + ChangeDetectorRef, + Component, + Input, + OnDestroy, + OnInit, +} from '@angular/core'; +import {SidebarStatusService} from 'services/sidebar-status.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {UserService} from 'services/user.service'; +import {DeviceInfoService} from 'services/contextual/device-info.service'; import debounce from 'lodash/debounce'; -import { AlertsService } from 'services/alerts.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { SearchService } from 'services/search.service'; -import { EventToCodes, NavigationService } from 'services/navigation.service'; -import { AppConstants } from 'app.constants'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { I18nService } from 'i18n/i18n.service'; -import { CreatorTopicSummary } from 'domain/topic/creator-topic-summary.model'; -import { PlatformFeatureService } from 'services/platform-feature.service'; -import { LearnerGroupBackendApiService } from 'domain/learner_group/learner-group-backend-api.service'; -import { FeedbackUpdatesBackendApiService } from 'domain/feedback_updates/feedback-updates-backend-api.service'; -import { FeedbackThreadSummaryBackendDict } from 'domain/feedback_thread/feedback-thread-summary.model'; +import {AlertsService} from 'services/alerts.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {SearchService} from 'services/search.service'; +import {EventToCodes, NavigationService} from 'services/navigation.service'; +import {AppConstants} from 'app.constants'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {I18nService} from 'i18n/i18n.service'; +import {CreatorTopicSummary} from 'domain/topic/creator-topic-summary.model'; +import {PlatformFeatureService} from 'services/platform-feature.service'; +import {LearnerGroupBackendApiService} from 'domain/learner_group/learner-group-backend-api.service'; +import {FeedbackUpdatesBackendApiService} from 'domain/feedback_updates/feedback-updates-backend-api.service'; +import {FeedbackThreadSummaryBackendDict} from 'domain/feedback_thread/feedback-thread-summary.model'; import './top-navigation-bar.component.css'; @@ -53,7 +59,7 @@ interface LanguageInfo { @Component({ selector: 'oppia-top-navigation-bar', templateUrl: './top-navigation-bar.component.html', - styleUrls: ['./top-navigation-bar.component.css'] + styleUrls: ['./top-navigation-bar.component.css'], }) export class TopNavigationBarComponent implements OnInit, OnDestroy { // These properties are initialized using Angular lifecycle hooks @@ -89,18 +95,18 @@ export class TopNavigationBarComponent implements OnInit, OnDestroy { ACTION_CLOSE!: string; KEYBOARD_EVENT_TO_KEY_CODES!: { enter: { - shiftKeyIsPressed: boolean; - keyCode: number; - }; + shiftKeyIsPressed: boolean; + keyCode: number; + }; tab: { - shiftKeyIsPressed: boolean; - keyCode: number; - }; + shiftKeyIsPressed: boolean; + keyCode: number; + }; shiftTab: { shiftKeyIsPressed: boolean; keyCode: number; - }; }; + }; labelForClearingFocus!: string; sidebarIsShown: boolean = false; @@ -110,7 +116,6 @@ export class TopNavigationBarComponent implements OnInit, OnDestroy { unreadThreadsCount: number = 0; paginatedThreadsList: FeedbackThreadSummaryBackendDict[][] = []; - // The 'username', 'profilePageUrl' properties // are set using the asynchronous method getUserInfoAsync() // which sends a HTTP request to the backend. @@ -128,26 +133,37 @@ export class TopNavigationBarComponent implements OnInit, OnDestroy { directiveSubscriptions = new Subscription(); NAV_MODE_SIGNUP = 'signup'; NAV_MODES_WITH_CUSTOM_LOCAL_NAV = [ - 'create', 'explore', 'lesson', 'collection', 'collection_editor', - 'topics_and_skills_dashboard', 'topic_editor', 'skill_editor', - 'story_editor', 'blog-dashboard']; + 'create', + 'explore', + 'lesson', + 'collection', + 'collection_editor', + 'topics_and_skills_dashboard', + 'topic_editor', + 'skill_editor', + 'story_editor', + 'blog-dashboard', + ]; currentWindowWidth = this.windowDimensionsService.getWidth(); // The order of the elements in this array specifies the order in // which they will be hidden. Earlier elements will be hidden first. NAV_ELEMENTS_ORDER = [ - 'I18N_TOPNAV_DONATE', 'I18N_TOPNAV_LEARN', - 'I18N_TOPNAV_ABOUT', 'I18N_TOPNAV_LIBRARY', - 'I18N_TOPNAV_HOME']; + 'I18N_TOPNAV_DONATE', + 'I18N_TOPNAV_LEARN', + 'I18N_TOPNAV_ABOUT', + 'I18N_TOPNAV_LIBRARY', + 'I18N_TOPNAV_HOME', + ]; LEARNER_GROUPS_FEATURE_IS_ENABLED = false; FEEDBACK_UPDATES_IN_PROFILE_PIC_DROP_DOWN_IS_ENABLED = false; googleSignInIconUrl = this.urlInterpolationService.getStaticImageUrl( - '/google_signin_buttons/google_signin.svg'); + '/google_signin_buttons/google_signin.svg' + ); navElementsVisibilityStatus: Record = {}; - PAGES_REGISTERED_WITH_FRONTEND = ( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND); + PAGES_REGISTERED_WITH_FRONTEND = AppConstants.PAGES_REGISTERED_WITH_FRONTEND; constructor( private changeDetectorRef: ChangeDetectorRef, @@ -155,8 +171,7 @@ export class TopNavigationBarComponent implements OnInit, OnDestroy { private i18nLanguageCodeService: I18nLanguageCodeService, private i18nService: I18nService, private alertsService: AlertsService, - private feedbackUpdatesBackendApiService: - FeedbackUpdatesBackendApiService, + private feedbackUpdatesBackendApiService: FeedbackUpdatesBackendApiService, private sidebarStatusService: SidebarStatusService, private urlInterpolationService: UrlInterpolationService, private navigationService: NavigationService, @@ -177,18 +192,19 @@ export class TopNavigationBarComponent implements OnInit, OnDestroy { this.url = new URL(this.windowRef.nativeWindow.location.toString()); this.labelForClearingFocus = AppConstants.LABEL_FOR_CLEARING_FOCUS; this.focusManagerService.setFocus(this.labelForClearingFocus); - this.userMenuIsShown = (this.currentUrl !== this.NAV_MODE_SIGNUP); + this.userMenuIsShown = this.currentUrl !== this.NAV_MODE_SIGNUP; this.inClassroomPage = false; this.supportedSiteLanguages = AppConstants.SUPPORTED_SITE_LANGUAGES.map( (languageInfo: LanguageInfo) => { return languageInfo; } ); - this.showLanguageSelector = ( - !this.contextService.getPageContext().endsWith('editor')); + this.showLanguageSelector = !this.contextService + .getPageContext() + .endsWith('editor'); - this.standardNavIsShown = ( - this.NAV_MODES_WITH_CUSTOM_LOCAL_NAV.indexOf(this.currentUrl) === -1); + this.standardNavIsShown = + this.NAV_MODES_WITH_CUSTOM_LOCAL_NAV.indexOf(this.currentUrl) === -1; if (this.currentUrl === 'learn') { this.inClassroomPage = true; } @@ -198,13 +214,14 @@ export class TopNavigationBarComponent implements OnInit, OnDestroy { this.navigationService.KEYBOARD_EVENT_TO_KEY_CODES; this.windowIsNarrow = this.windowDimensionsService.isWindowNarrow(); - this.learnerGroupBackendApiService.isLearnerGroupFeatureEnabledAsync() - .then((featureIsEnabled) => { + this.learnerGroupBackendApiService + .isLearnerGroupFeatureEnabledAsync() + .then(featureIsEnabled => { this.LEARNER_GROUPS_FEATURE_IS_ENABLED = featureIsEnabled; }); this.FEEDBACK_UPDATES_IN_PROFILE_PIC_DROP_DOWN_IS_ENABLED = - this.isShowFeedbackUpdatesInProfilepicDropdownFeatureFlagEnable(); + this.isShowFeedbackUpdatesInProfilepicDropdownFeatureFlagEnable(); // Inside a setTimeout function call, 'this' points to the global object. // To access the context in which the setTimeout call is made, we need to @@ -213,18 +230,16 @@ export class TopNavigationBarComponent implements OnInit, OnDestroy { let that = this; this.directiveSubscriptions.add( - this.searchService.onSearchBarLoaded.subscribe( - () => { - setTimeout(function() { - that.truncateNavbar(); - }, 100); - } - ) + this.searchService.onSearchBarLoaded.subscribe(() => { + setTimeout(function () { + that.truncateNavbar(); + }, 100); + }) ); this.i18nService.updateViewToUserPreferredSiteLanguage(); - this.userService.getUserInfoAsync().then((userInfo) => { + this.userService.getUserInfoAsync().then(userInfo => { this.isModerator = userInfo.isModerator(); this.isCurriculumAdmin = userInfo.isCurriculumAdmin(); this.isTopicManager = userInfo.isTopicManager(); @@ -234,20 +249,21 @@ export class TopNavigationBarComponent implements OnInit, OnDestroy { this.userIsLoggedIn = userInfo.isLoggedIn(); let usernameFromUserInfo = userInfo.getUsername(); if (this.userIsLoggedIn) { - let feedbackUpdatesDataPromise = ( - this.feedbackUpdatesBackendApiService - .fetchFeedbackUpdatesDataAsync( - this.paginatedThreadsList)); + let feedbackUpdatesDataPromise = + this.feedbackUpdatesBackendApiService.fetchFeedbackUpdatesDataAsync( + this.paginatedThreadsList + ); feedbackUpdatesDataPromise.then( responseData => { - this.unreadThreadsCount = - responseData.numberOfUnreadThreads; - }, errorResponseStatus => { + this.unreadThreadsCount = responseData.numberOfUnreadThreads; + }, + errorResponseStatus => { if ( - AppConstants.FATAL_ERROR_CODES. - indexOf(errorResponseStatus) !== -1) { + AppConstants.FATAL_ERROR_CODES.indexOf(errorResponseStatus) !== -1 + ) { this.alertsService.addWarning( - 'Failed to get number of unread thread of feedback updates'); + 'Failed to get number of unread thread of feedback updates' + ); } } ); @@ -255,18 +271,22 @@ export class TopNavigationBarComponent implements OnInit, OnDestroy { if (usernameFromUserInfo) { this.username = usernameFromUserInfo; this.profilePageUrl = this.urlInterpolationService.interpolateUrl( - '/profile/', { - username: this.username - }); - [this.profilePicturePngDataUrl, this.profilePictureWebpDataUrl] = ( - this.userService.getProfileImageDataUrl(this.username)); + '/profile/', + { + username: this.username, + } + ); + [this.profilePicturePngDataUrl, this.profilePictureWebpDataUrl] = + this.userService.getProfileImageDataUrl(this.username); } else { - this.profilePicturePngDataUrl = ( + this.profilePicturePngDataUrl = this.urlInterpolationService.getStaticImageUrl( - AppConstants.DEFAULT_PROFILE_IMAGE_PNG_PATH)); - this.profilePictureWebpDataUrl = ( + AppConstants.DEFAULT_PROFILE_IMAGE_PNG_PATH + ); + this.profilePictureWebpDataUrl = this.urlInterpolationService.getStaticImageUrl( - AppConstants.DEFAULT_PROFILE_IMAGE_WEBP_PATH)); + AppConstants.DEFAULT_PROFILE_IMAGE_WEBP_PATH + ); } }); @@ -279,12 +299,9 @@ export class TopNavigationBarComponent implements OnInit, OnDestroy { this.windowIsNarrow = this.windowDimensionsService.isWindowNarrow(); // If window is resized larger, try displaying the hidden // elements. - if ( - this.currentWindowWidth < this.windowDimensionsService.getWidth()) { + if (this.currentWindowWidth < this.windowDimensionsService.getWidth()) { for (var i = 0; i < this.NAV_ELEMENTS_ORDER.length; i++) { - if ( - !this.navElementsVisibilityStatus[ - this.NAV_ELEMENTS_ORDER[i]]) { + if (!this.navElementsVisibilityStatus[this.NAV_ELEMENTS_ORDER[i]]) { this.navElementsVisibilityStatus[this.NAV_ELEMENTS_ORDER[i]] = true; } @@ -301,18 +318,17 @@ export class TopNavigationBarComponent implements OnInit, OnDestroy { ); this.directiveSubscriptions.add( - this.i18nLanguageCodeService.onI18nLanguageCodeChange.subscribe( - (code) => { - if (this.currentLanguageCode !== code) { - this.currentLanguageCode = code; - this.supportedSiteLanguages.forEach(element => { - if (element.id === this.currentLanguageCode) { - this.currentLanguageText = element.text; - } - }); - this.changeDetectorRef.detectChanges(); - } - }) + this.i18nLanguageCodeService.onI18nLanguageCodeChange.subscribe(code => { + if (this.currentLanguageCode !== code) { + this.currentLanguageCode = code; + this.supportedSiteLanguages.forEach(element => { + if (element.id === this.currentLanguageCode) { + this.currentLanguageText = element.text; + } + }); + this.changeDetectorRef.detectChanges(); + } + }) ); let langCode = this.i18nLanguageCodeService.getCurrentI18nLanguageCode(); @@ -332,18 +348,15 @@ export class TopNavigationBarComponent implements OnInit, OnDestroy { // will check if i18n is complete and set a new timeout if it is // not. Since a timeout of 0 works for at least one browser, // it is used here. - setTimeout(function() { + setTimeout(function () { that.truncateNavbar(); }, 0); } ngAfterViewChecked(): void { - this.getInvolvedMenuOffset = this - .getDropdownOffset('.get-involved', 574); - this.donateMenuOffset = this - .getDropdownOffset('.donate-tab', 286); - this.learnDropdownOffset = this.getDropdownOffset( - '.learn-tab', 688); + this.getInvolvedMenuOffset = this.getDropdownOffset('.get-involved', 574); + this.donateMenuOffset = this.getDropdownOffset('.donate-tab', 286); + this.learnDropdownOffset = this.getDropdownOffset('.learn-tab', 688); // https://stackoverflow.com/questions/34364880/expression-has-changed-after-it-was-checked this.changeDetectorRef.detectChanges(); } @@ -358,7 +371,7 @@ export class TopNavigationBarComponent implements OnInit, OnDestroy { if (learnTab) { let leftOffset = learnTab.getBoundingClientRect().left; let space = window.innerWidth - leftOffset; - return (space < width) ? (Math.round(space - width)) : 0; + return space < width ? Math.round(space - width) : 0; } return 0; } @@ -380,23 +393,22 @@ export class TopNavigationBarComponent implements OnInit, OnDestroy { } onLoginButtonClicked(): void { - this.userService.getLoginUrlAsync().then( - (loginUrl) => { - if (loginUrl) { - this.siteAnalyticsService.registerStartLoginEvent('loginButton'); - setTimeout(() => { - this.windowRef.nativeWindow.location.href = loginUrl; - }, 150); - } else { - this.windowRef.nativeWindow.location.reload(); - } + this.userService.getLoginUrlAsync().then(loginUrl => { + if (loginUrl) { + this.siteAnalyticsService.registerStartLoginEvent('loginButton'); + setTimeout(() => { + this.windowRef.nativeWindow.location.href = loginUrl; + }, 150); + } else { + this.windowRef.nativeWindow.location.reload(); } - ); + }); } onLogoutButtonClicked(): void { this.windowRef.nativeWindow.localStorage.removeItem( - 'last_uploaded_audio_lang'); + 'last_uploaded_audio_lang' + ); } /** @@ -433,10 +445,11 @@ export class TopNavigationBarComponent implements OnInit, OnDestroy { * onMenuKeypress($event, 'aboutMenu', {enter: 'open'}) */ onMenuKeypress( - evt: KeyboardEvent, menuName: string, - eventsTobeHandled: EventToCodes): void { - this.navigationService.onMenuKeypress( - evt, menuName, eventsTobeHandled); + evt: KeyboardEvent, + menuName: string, + eventsTobeHandled: EventToCodes + ): void { + this.navigationService.onMenuKeypress(evt, menuName, eventsTobeHandled); this.activeMenuName = this.navigationService.activeMenuName; } @@ -493,7 +506,7 @@ export class TopNavigationBarComponent implements OnInit, OnDestroy { let that = this; // If i18n hasn't completed, retry after 100ms. if (!this.checkIfI18NCompleted()) { - setTimeout(function() { + setTimeout(function () { that.truncateNavbar(); }, 100); return; @@ -506,17 +519,15 @@ export class TopNavigationBarComponent implements OnInit, OnDestroy { let navbar = document.querySelector('div.collapse.navbar-collapse'); if (navbar && navbar.clientHeight > 60) { for (var i = 0; i < this.NAV_ELEMENTS_ORDER.length; i++) { - if ( - this.navElementsVisibilityStatus[this.NAV_ELEMENTS_ORDER[i]]) { + if (this.navElementsVisibilityStatus[this.NAV_ELEMENTS_ORDER[i]]) { // Hide one element, then check again after 50ms. // This gives the browser time to render the visibility // change. - this.navElementsVisibilityStatus[this.NAV_ELEMENTS_ORDER[i]] = - false; + this.navElementsVisibilityStatus[this.NAV_ELEMENTS_ORDER[i]] = false; // Force a digest cycle to hide element immediately. // Otherwise it would be hidden after the next call. // This is due to setTimeout use in debounce. - setTimeout(function() { + setTimeout(function () { that.truncateNavbar(); }, 50); return; @@ -538,13 +549,14 @@ export class TopNavigationBarComponent implements OnInit, OnDestroy { } isShowFeedbackUpdatesInProfilepicDropdownFeatureFlagEnable(): boolean { - return ( - this.platformFeatureService.status. - ShowFeedbackUpdatesInProfilePicDropdownMenu.isEnabled); + return this.platformFeatureService.status + .ShowFeedbackUpdatesInProfilePicDropdownMenu.isEnabled; } } angular.module('oppia').directive( - 'oppiaTopNavigationBar', downgradeComponent({ - component: TopNavigationBarComponent - }) as angular.IDirectiveFactory); + 'oppiaTopNavigationBar', + downgradeComponent({ + component: TopNavigationBarComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/components/concept-card/concept-card.component.spec.ts b/core/templates/components/concept-card/concept-card.component.spec.ts index 2c0928e5ad95..b85da1142344 100644 --- a/core/templates/components/concept-card/concept-card.component.spec.ts +++ b/core/templates/components/concept-card/concept-card.component.spec.ts @@ -16,15 +16,21 @@ * @fileoverview Unit test for Concept Card Component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { RecordedVoiceovers } from 'domain/exploration/recorded-voiceovers.model'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { ConceptCardBackendApiService } from 'domain/skill/concept-card-backend-api.service'; -import { ConceptCard } from 'domain/skill/concept-card.model'; -import { WorkedExample } from 'domain/skill/worked-example.model'; -import { ConceptCardComponent } from './concept-card.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {RecordedVoiceovers} from 'domain/exploration/recorded-voiceovers.model'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {ConceptCardBackendApiService} from 'domain/skill/concept-card-backend-api.service'; +import {ConceptCard} from 'domain/skill/concept-card.model'; +import {WorkedExample} from 'domain/skill/worked-example.model'; +import {ConceptCardComponent} from './concept-card.component'; describe('Concept card component', () => { let fixture: ComponentFixture; @@ -33,16 +39,15 @@ describe('Concept card component', () => { let conceptCard = new ConceptCard( new SubtitledHtml('', '1'), [new WorkedExample({} as SubtitledHtml, {} as SubtitledHtml)], - RecordedVoiceovers.createEmpty()); + RecordedVoiceovers.createEmpty() + ); let conceptCardObjects = [conceptCard]; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], + imports: [HttpClientTestingModule], declarations: [ConceptCardComponent], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -53,8 +58,10 @@ describe('Concept card component', () => { }); it('should initialize and load concept cards successfully', fakeAsync(() => { - spyOn(conceptCardBackendApiService, 'loadConceptCardsAsync') - .and.returnValue(Promise.resolve(conceptCardObjects)); + spyOn( + conceptCardBackendApiService, + 'loadConceptCardsAsync' + ).and.returnValue(Promise.resolve(conceptCardObjects)); componentInstance.index = 0; componentInstance.ngOnInit(); @@ -64,18 +71,20 @@ describe('Concept card component', () => { expect(componentInstance.currentConceptCard).toEqual(conceptCard); })); - it('should initialize and handle error if fails to load concept cards', - fakeAsync(() => { - spyOn(conceptCardBackendApiService, 'loadConceptCardsAsync') - .and.returnValue(Promise.reject({})); + it('should initialize and handle error if fails to load concept cards', fakeAsync(() => { + spyOn( + conceptCardBackendApiService, + 'loadConceptCardsAsync' + ).and.returnValue(Promise.reject({})); - componentInstance.ngOnInit(); - tick(); + componentInstance.ngOnInit(); + tick(); - expect(componentInstance.loadingMessage).toEqual(''); - expect(componentInstance.skillDeletedMessage).toEqual( - 'Oops, it looks like this skill has been deleted.'); - })); + expect(componentInstance.loadingMessage).toEqual(''); + expect(componentInstance.skillDeletedMessage).toEqual( + 'Oops, it looks like this skill has been deleted.' + ); + })); it('should tell if work example is last', () => { componentInstance.numberOfWorkedExamplesShown = 1; @@ -92,6 +101,7 @@ describe('Concept card component', () => { expect(componentInstance.explanationIsShown).toBeFalse(); expect(componentInstance.numberOfWorkedExamplesShown).toEqual( - numberOfWorkedExamplesShown + 1); + numberOfWorkedExamplesShown + 1 + ); }); }); diff --git a/core/templates/components/concept-card/concept-card.component.ts b/core/templates/components/concept-card/concept-card.component.ts index 223f0943c14d..99ea94133231 100644 --- a/core/templates/components/concept-card/concept-card.component.ts +++ b/core/templates/components/concept-card/concept-card.component.ts @@ -16,14 +16,14 @@ * @fileoverview Component for the concept cards viewer. */ -import { Component, Input, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { ConceptCardBackendApiService } from 'domain/skill/concept-card-backend-api.service'; -import { ConceptCard } from 'domain/skill/concept-card.model'; +import {Component, Input, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {ConceptCardBackendApiService} from 'domain/skill/concept-card-backend-api.service'; +import {ConceptCard} from 'domain/skill/concept-card.model'; @Component({ selector: 'oppia-concept-card', - templateUrl: './concept-card.component.html' + templateUrl: './concept-card.component.html', }) export class ConceptCardComponent implements OnInit { // These properties are initialized using Angular lifecycle hooks @@ -38,16 +38,15 @@ export class ConceptCardComponent implements OnInit { numberOfWorkedExamplesShown: number = 0; explanationIsShown: boolean = false; - constructor( private conceptCardBackendApiService: ConceptCardBackendApiService ) {} ngOnInit(): void { this.loadingMessage = 'Loading'; - this.conceptCardBackendApiService.loadConceptCardsAsync(this.skillIds) - .then((conceptCardObjects) => { - conceptCardObjects.forEach((conceptCardObject) => { + this.conceptCardBackendApiService.loadConceptCardsAsync(this.skillIds).then( + conceptCardObjects => { + conceptCardObjects.forEach(conceptCardObject => { this.conceptsCards.push(conceptCardObject); }); this.loadingMessage = ''; @@ -56,16 +55,20 @@ export class ConceptCardComponent implements OnInit { if (this.currentConceptCard.getWorkedExamples().length > 0) { this.numberOfWorkedExamplesShown = 1; } - }, (errorResponse) => { + }, + errorResponse => { this.loadingMessage = ''; - this.skillDeletedMessage = 'Oops, it looks like this skill has' + - ' been deleted.'; - }); + this.skillDeletedMessage = + 'Oops, it looks like this skill has' + ' been deleted.'; + } + ); } isLastWorkedExample(): boolean { - return this.numberOfWorkedExamplesShown === - this.currentConceptCard.getWorkedExamples().length; + return ( + this.numberOfWorkedExamplesShown === + this.currentConceptCard.getWorkedExamples().length + ); } showMoreWorkedExamples(): void { @@ -75,6 +78,8 @@ export class ConceptCardComponent implements OnInit { } angular.module('oppia').directive( - 'oppiaConceptCard', downgradeComponent({ - component: ConceptCardComponent - }) as angular.IDirectiveFactory); + 'oppiaConceptCard', + downgradeComponent({ + component: ConceptCardComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/components/copy-url/copy-url.component.spec.ts b/core/templates/components/copy-url/copy-url.component.spec.ts index bedefaa4e442..c58dfe7750f6 100644 --- a/core/templates/components/copy-url/copy-url.component.spec.ts +++ b/core/templates/components/copy-url/copy-url.component.spec.ts @@ -12,18 +12,23 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Unit tests for copy exploration URL component. */ -import { Clipboard } from '@angular/cdk/clipboard'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { ComponentOverviewComponent } from './copy-url.component'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; +import {Clipboard} from '@angular/cdk/clipboard'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {ComponentOverviewComponent} from './copy-url.component'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; class MockI18nLanguageCodeService { isCurrentLanguageRTL(): boolean { @@ -38,20 +43,15 @@ describe('Copy Exploration URL component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - ], - declarations: [ - ComponentOverviewComponent, - MockTranslatePipe - ], + imports: [HttpClientTestingModule], + declarations: [ComponentOverviewComponent, MockTranslatePipe], providers: [ { provide: I18nLanguageCodeService, - useClass: MockI18nLanguageCodeService - } + useClass: MockI18nLanguageCodeService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -70,7 +70,6 @@ describe('Copy Exploration URL component', () => { component.copyUrlButton(); tick(1000); - expect(clipboard.copy).toHaveBeenCalledWith( - explorationURL); + expect(clipboard.copy).toHaveBeenCalledWith(explorationURL); })); }); diff --git a/core/templates/components/copy-url/copy-url.component.ts b/core/templates/components/copy-url/copy-url.component.ts index 00f57ace2a80..5f2826119938 100644 --- a/core/templates/components/copy-url/copy-url.component.ts +++ b/core/templates/components/copy-url/copy-url.component.ts @@ -16,22 +16,22 @@ * @fileoverview Component for the copy message tooltip. */ -import { Component, Input } from '@angular/core'; -import { Clipboard } from '@angular/cdk/clipboard'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; +import {Component, Input} from '@angular/core'; +import {Clipboard} from '@angular/cdk/clipboard'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; @Component({ selector: 'oppia-copy-url', - templateUrl: './copy-url.component.html' + templateUrl: './copy-url.component.html', }) - export class ComponentOverviewComponent { @Input() urlToCopy!: string; showTooltip: boolean = false; constructor( private clipboard: Clipboard, - private i18nLanguageCodeService: I18nLanguageCodeService) {} + private i18nLanguageCodeService: I18nLanguageCodeService + ) {} copyUrlButton(): void { this.clipboard.copy(this.urlToCopy); diff --git a/core/templates/components/entity-creation-services/collection-creation-backend-api.service.spec.ts b/core/templates/components/entity-creation-services/collection-creation-backend-api.service.spec.ts index 96cf47f51678..55f362faaab8 100644 --- a/core/templates/components/entity-creation-services/collection-creation-backend-api.service.spec.ts +++ b/core/templates/components/entity-creation-services/collection-creation-backend-api.service.spec.ts @@ -15,12 +15,13 @@ * @fileoverview Unit test for CollectionCreationBackendApiService. */ -import { HttpClientTestingModule, HttpTestingController } from - '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; -import { CollectionCreationBackendService } from - 'components/entity-creation-services/collection-creation-backend-api.service'; +import {CollectionCreationBackendService} from 'components/entity-creation-services/collection-creation-backend-api.service'; describe('Collection Creation backend service', () => { let collectionCreationBackendService: CollectionCreationBackendService; @@ -30,11 +31,12 @@ describe('Collection Creation backend service', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] + imports: [HttpClientTestingModule], }); collectionCreationBackendService = TestBed.get( - CollectionCreationBackendService); + CollectionCreationBackendService + ); httpTestingController = TestBed.get(HttpTestingController); }); @@ -42,49 +44,53 @@ describe('Collection Creation backend service', () => { httpTestingController.verify(); }); - it('should successfully create a new collection and obtain the collection ID', - fakeAsync(() => { - let successHandler = jasmine.createSpy('success'); - let failHandler = jasmine.createSpy('fail'); - - collectionCreationBackendService.createCollectionAsync().then( - successHandler, failHandler); - - let req = httpTestingController.expectOne( - '/collection_editor_handler/create_new'); - expect(req.request.method).toEqual('POST'); - req.flush({collection_id: SAMPLE_COLLECTION_ID}); - - flushMicrotasks(); - - expect(successHandler).toHaveBeenCalled(); - expect(failHandler).not.toHaveBeenCalled(); - }) - ); - - it('should fail to create a new collection and call the fail handler', - fakeAsync(() => { - let successHandler = jasmine.createSpy('success'); - let failHandler = jasmine.createSpy('fail'); - - collectionCreationBackendService.createCollectionAsync().then( - successHandler, failHandler); - - let req = httpTestingController.expectOne( - '/collection_editor_handler/create_new'); - expect(req.request.method).toEqual('POST'); - req.flush({ - error: 'Error creating a new collection.' - }, { + it('should successfully create a new collection and obtain the collection ID', fakeAsync(() => { + let successHandler = jasmine.createSpy('success'); + let failHandler = jasmine.createSpy('fail'); + + collectionCreationBackendService + .createCollectionAsync() + .then(successHandler, failHandler); + + let req = httpTestingController.expectOne( + '/collection_editor_handler/create_new' + ); + expect(req.request.method).toEqual('POST'); + req.flush({collection_id: SAMPLE_COLLECTION_ID}); + + flushMicrotasks(); + + expect(successHandler).toHaveBeenCalled(); + expect(failHandler).not.toHaveBeenCalled(); + })); + + it('should fail to create a new collection and call the fail handler', fakeAsync(() => { + let successHandler = jasmine.createSpy('success'); + let failHandler = jasmine.createSpy('fail'); + + collectionCreationBackendService + .createCollectionAsync() + .then(successHandler, failHandler); + + let req = httpTestingController.expectOne( + '/collection_editor_handler/create_new' + ); + expect(req.request.method).toEqual('POST'); + req.flush( + { + error: 'Error creating a new collection.', + }, + { status: ERROR_STATUS_CODE, - statusText: 'Error creating a new collection.' - }); + statusText: 'Error creating a new collection.', + } + ); - flushMicrotasks(); + flushMicrotasks(); - expect(successHandler).not.toHaveBeenCalled(); - expect(failHandler).toHaveBeenCalledWith( - 'Error creating a new collection.'); - }) - ); + expect(successHandler).not.toHaveBeenCalled(); + expect(failHandler).toHaveBeenCalledWith( + 'Error creating a new collection.' + ); + })); }); diff --git a/core/templates/components/entity-creation-services/collection-creation-backend-api.service.ts b/core/templates/components/entity-creation-services/collection-creation-backend-api.service.ts index 7dfcef3b91c6..d522dd97a7c2 100644 --- a/core/templates/components/entity-creation-services/collection-creation-backend-api.service.ts +++ b/core/templates/components/entity-creation-services/collection-creation-backend-api.service.ts @@ -16,12 +16,12 @@ * collection_id. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; interface CollectionCreationBackendDict { - 'collection_id': string; + collection_id: string; } interface CollectionCreationResponse { @@ -29,30 +29,37 @@ interface CollectionCreationResponse { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class CollectionCreationBackendService { constructor(private http: HttpClient) {} private _createCollection( - successCallback: (value: CollectionCreationResponse) => void, - errorCallback: (reason: string) => void): void { - this.http.post( - '/collection_editor_handler/create_new', {}).toPromise() - .then(response => { - if (successCallback) { - successCallback({ - collectionId: response.collection_id - }); + successCallback: (value: CollectionCreationResponse) => void, + errorCallback: (reason: string) => void + ): void { + this.http + .post( + '/collection_editor_handler/create_new', + {} + ) + .toPromise() + .then( + response => { + if (successCallback) { + successCallback({ + collectionId: response.collection_id, + }); + } + }, + errorResponse => { + if (errorCallback) { + errorCallback(errorResponse.error.error); + } } - }, errorResponse => { - if (errorCallback) { - errorCallback(errorResponse.error.error); - } - }); + ); } - async createCollectionAsync(): Promise { return new Promise((resolve, reject) => { this._createCollection(resolve, reject); @@ -60,6 +67,9 @@ export class CollectionCreationBackendService { } } -angular.module('oppia').factory( - 'CollectionCreationBackendService', - downgradeInjectable(CollectionCreationBackendService)); +angular + .module('oppia') + .factory( + 'CollectionCreationBackendService', + downgradeInjectable(CollectionCreationBackendService) + ); diff --git a/core/templates/components/entity-creation-services/collection-creation.service.spec.ts b/core/templates/components/entity-creation-services/collection-creation.service.spec.ts index 3587679d9cdc..66095cba5359 100644 --- a/core/templates/components/entity-creation-services/collection-creation.service.spec.ts +++ b/core/templates/components/entity-creation-services/collection-creation.service.spec.ts @@ -16,16 +16,16 @@ * @fileoverview Unit test for CollectionCreationService. */ -import { HttpClientTestingModule, HttpTestingController } - from '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks, tick } - from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks, tick} from '@angular/core/testing'; -import { CollectionCreationService } from - 'components/entity-creation-services/collection-creation.service'; -import { LoaderService } from 'services/loader.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import {CollectionCreationService} from 'components/entity-creation-services/collection-creation.service'; +import {LoaderService} from 'services/loader.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; describe('Collection Creation service', () => { let collectionCreationService: CollectionCreationService; @@ -38,9 +38,7 @@ describe('Collection Creation service', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - ], + imports: [HttpClientTestingModule], }); collectionCreationService = TestBed.inject(CollectionCreationService); @@ -54,92 +52,100 @@ describe('Collection Creation service', () => { httpTestingController.verify(); }); - it('should successfully create a collection and navigate to collection', - fakeAsync(() => { - spyOn(loaderService, 'showLoadingScreen').and.callThrough(); - spyOn(analyticsService, 'registerCreateNewCollectionEvent') - .and.callThrough(); - - spyOnProperty(windowRef, 'nativeWindow').and.returnValue({ - location: { - href: '' - }, - gtag: () => {} - } as unknown as Window); - - collectionCreationService.createNewCollection(); - - let req = httpTestingController.expectOne( - '/collection_editor_handler/create_new'); - expect(req.request.method).toEqual('POST'); - req.flush({collection_id: SAMPLE_COLLECTION_ID}); - - flushMicrotasks(); - tick(150); - - expect(loaderService.showLoadingScreen) - .toHaveBeenCalledWith('Creating collection'); - expect(analyticsService.registerCreateNewCollectionEvent) - .toHaveBeenCalledWith(SAMPLE_COLLECTION_ID); - - expect(windowRef.nativeWindow.location.href).toEqual( - '/collection_editor/create/' + SAMPLE_COLLECTION_ID); - }) - ); - - it('should fail to create a collection and hide the loading screen', - fakeAsync(() => { - spyOn(loaderService, 'hideLoadingScreen').and.callThrough(); - - collectionCreationService.createNewCollection(); - - let req = httpTestingController.expectOne( - '/collection_editor_handler/create_new'); - expect(req.request.method).toEqual('POST'); - req.flush({ - error: 'Error creating a new collection.' - }, { + it('should successfully create a collection and navigate to collection', fakeAsync(() => { + spyOn(loaderService, 'showLoadingScreen').and.callThrough(); + spyOn( + analyticsService, + 'registerCreateNewCollectionEvent' + ).and.callThrough(); + + spyOnProperty(windowRef, 'nativeWindow').and.returnValue({ + location: { + href: '', + }, + gtag: () => {}, + } as unknown as Window); + + collectionCreationService.createNewCollection(); + + let req = httpTestingController.expectOne( + '/collection_editor_handler/create_new' + ); + expect(req.request.method).toEqual('POST'); + req.flush({collection_id: SAMPLE_COLLECTION_ID}); + + flushMicrotasks(); + tick(150); + + expect(loaderService.showLoadingScreen).toHaveBeenCalledWith( + 'Creating collection' + ); + expect( + analyticsService.registerCreateNewCollectionEvent + ).toHaveBeenCalledWith(SAMPLE_COLLECTION_ID); + + expect(windowRef.nativeWindow.location.href).toEqual( + '/collection_editor/create/' + SAMPLE_COLLECTION_ID + ); + })); + + it('should fail to create a collection and hide the loading screen', fakeAsync(() => { + spyOn(loaderService, 'hideLoadingScreen').and.callThrough(); + + collectionCreationService.createNewCollection(); + + let req = httpTestingController.expectOne( + '/collection_editor_handler/create_new' + ); + expect(req.request.method).toEqual('POST'); + req.flush( + { + error: 'Error creating a new collection.', + }, + { status: ERROR_STATUS_CODE, - statusText: 'Error creating a new collection.' - }); - - flushMicrotasks(); - - expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); - }) - ); - - it('should not be able to be used while in progress', - fakeAsync(() => { - spyOn(loaderService, 'showLoadingScreen').and.callThrough(); - spyOn(analyticsService, 'registerCreateNewCollectionEvent') - .and.callThrough(); - - spyOnProperty(windowRef, 'nativeWindow').and.returnValue({ - location: { - href: '' - }, - gtag: () => {} - } as unknown as Window); - - collectionCreationService.createNewCollection(); - collectionCreationService.createNewCollection(); - - let req = httpTestingController.expectOne( - '/collection_editor_handler/create_new'); - expect(req.request.method).toEqual('POST'); - req.flush({collection_id: SAMPLE_COLLECTION_ID}); - - flushMicrotasks(); - tick(150); - - expect(loaderService.showLoadingScreen) - .toHaveBeenCalledTimes(1); - expect(analyticsService.registerCreateNewCollectionEvent) - .toHaveBeenCalledTimes(1); - - expect(windowRef.nativeWindow.location.href).toEqual( - '/collection_editor/create/' + SAMPLE_COLLECTION_ID); - }) - ); + statusText: 'Error creating a new collection.', + } + ); + + flushMicrotasks(); + + expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); + })); + + it('should not be able to be used while in progress', fakeAsync(() => { + spyOn(loaderService, 'showLoadingScreen').and.callThrough(); + spyOn( + analyticsService, + 'registerCreateNewCollectionEvent' + ).and.callThrough(); + + spyOnProperty(windowRef, 'nativeWindow').and.returnValue({ + location: { + href: '', + }, + gtag: () => {}, + } as unknown as Window); + + collectionCreationService.createNewCollection(); + collectionCreationService.createNewCollection(); + + let req = httpTestingController.expectOne( + '/collection_editor_handler/create_new' + ); + expect(req.request.method).toEqual('POST'); + req.flush({collection_id: SAMPLE_COLLECTION_ID}); + + flushMicrotasks(); + tick(150); + + expect(loaderService.showLoadingScreen).toHaveBeenCalledTimes(1); + expect( + analyticsService.registerCreateNewCollectionEvent + ).toHaveBeenCalledTimes(1); + + expect(windowRef.nativeWindow.location.href).toEqual( + '/collection_editor/create/' + SAMPLE_COLLECTION_ID + ); + })); }); diff --git a/core/templates/components/entity-creation-services/collection-creation.service.ts b/core/templates/components/entity-creation-services/collection-creation.service.ts index 0749d8701203..2e741f24560b 100644 --- a/core/templates/components/entity-creation-services/collection-creation.service.ts +++ b/core/templates/components/entity-creation-services/collection-creation.service.ts @@ -16,20 +16,18 @@ * @fileoverview Modal and functionality for the create collection button. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { AlertsService } from 'services/alerts.service'; -import { CollectionCreationBackendService } from - 'components/entity-creation-services/collection-creation-backend-api.service'; -import { LoaderService } from 'services/loader.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import {AlertsService} from 'services/alerts.service'; +import {CollectionCreationBackendService} from 'components/entity-creation-services/collection-creation-backend-api.service'; +import {LoaderService} from 'services/loader.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class CollectionCreationService { // TODO(#9154): Remove static when migration is complete. @@ -41,11 +39,11 @@ export class CollectionCreationService { private siteAnalyticsService: SiteAnalyticsService, private urlInterpolationService: UrlInterpolationService, private loaderService: LoaderService, - private windowRef: WindowRef) { - } + private windowRef: WindowRef + ) {} - CREATE_NEW_COLLECTION_URL_TEMPLATE = ( - '/collection_editor/create/'); + CREATE_NEW_COLLECTION_URL_TEMPLATE = + '/collection_editor/create/'; createNewCollection(): void { if (CollectionCreationService.collectionCreationInProgress) { @@ -57,27 +55,34 @@ export class CollectionCreationService { this.loaderService.showLoadingScreen('Creating collection'); - this.collectionCreationBackendService.createCollectionAsync() - .then(response => { + this.collectionCreationBackendService.createCollectionAsync().then( + response => { this.siteAnalyticsService.registerCreateNewCollectionEvent( - response.collectionId); + response.collectionId + ); setTimeout(() => { this.windowRef.nativeWindow.location.href = this.urlInterpolationService.interpolateUrl( - this.CREATE_NEW_COLLECTION_URL_TEMPLATE, { - collection_id: response.collectionId + this.CREATE_NEW_COLLECTION_URL_TEMPLATE, + { + collection_id: response.collectionId, } ); CollectionCreationService.collectionCreationInProgress = false; }, 150); - }, () => { + }, + () => { this.loaderService.hideLoadingScreen(); CollectionCreationService.collectionCreationInProgress = false; - }); + } + ); } } -angular.module('oppia').factory( - 'CollectionCreationService', - downgradeInjectable(CollectionCreationService)); +angular + .module('oppia') + .factory( + 'CollectionCreationService', + downgradeInjectable(CollectionCreationService) + ); diff --git a/core/templates/components/entity-creation-services/exploration-creation-backend-api.service.spec.ts b/core/templates/components/entity-creation-services/exploration-creation-backend-api.service.spec.ts index 81ae2397ea79..642b01e95533 100644 --- a/core/templates/components/entity-creation-services/exploration-creation-backend-api.service.spec.ts +++ b/core/templates/components/entity-creation-services/exploration-creation-backend-api.service.spec.ts @@ -15,26 +15,27 @@ * @fileoverview Unit test for ExplorationCreationBackendApiService. */ -import { HttpClientTestingModule, HttpTestingController } from - '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; -import { ExplorationCreationBackendApiService } from 'components/entity-creation-services/exploration-creation-backend-api.service'; - +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; +import {ExplorationCreationBackendApiService} from 'components/entity-creation-services/exploration-creation-backend-api.service'; describe('ExplorationCreationBackendApiService', () => { - let explorationCreationBackendApiService: - ExplorationCreationBackendApiService; + let explorationCreationBackendApiService: ExplorationCreationBackendApiService; let httpTestingController: HttpTestingController; let SAMPLE_EXPLORATION_ID = 'hyuy4GUlvTqJ'; let ERROR_STATUS_CODE = 500; beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] + imports: [HttpClientTestingModule], }); explorationCreationBackendApiService = TestBed.inject( - ExplorationCreationBackendApiService); + ExplorationCreationBackendApiService + ); httpTestingController = TestBed.inject(HttpTestingController); }); @@ -42,80 +43,73 @@ describe('ExplorationCreationBackendApiService', () => { httpTestingController.verify(); }); - it( - 'should successfully create new exploration and obtain the exploration ID', - fakeAsync(() => { - let successHandler = jasmine.createSpy('success'); - let failHandler = jasmine.createSpy('fail'); - - explorationCreationBackendApiService.registerNewExplorationAsync({}).then( - successHandler, failHandler); - - let req = httpTestingController.expectOne( - '/contributehandler/create_new'); - expect(req.request.method).toEqual('POST'); - req.flush({exploration_id: SAMPLE_EXPLORATION_ID}); - - flushMicrotasks(); - - expect(successHandler).toHaveBeenCalled(); - expect(failHandler).not.toHaveBeenCalled(); - }) - ); - - it('should fail to create a new exploration and call the fail handler', - fakeAsync(() => { - let successHandler = jasmine.createSpy('success'); - let failHandler = jasmine.createSpy('fail'); - - explorationCreationBackendApiService.registerNewExplorationAsync({}).then( - successHandler, failHandler); - - let req = httpTestingController.expectOne( - '/contributehandler/create_new'); - expect(req.request.method).toEqual('POST'); - req.flush({ - error: 'Error creating a new exploration.' - }, { + it('should successfully create new exploration and obtain the exploration ID', fakeAsync(() => { + let successHandler = jasmine.createSpy('success'); + let failHandler = jasmine.createSpy('fail'); + + explorationCreationBackendApiService + .registerNewExplorationAsync({}) + .then(successHandler, failHandler); + + let req = httpTestingController.expectOne('/contributehandler/create_new'); + expect(req.request.method).toEqual('POST'); + req.flush({exploration_id: SAMPLE_EXPLORATION_ID}); + + flushMicrotasks(); + + expect(successHandler).toHaveBeenCalled(); + expect(failHandler).not.toHaveBeenCalled(); + })); + + it('should fail to create a new exploration and call the fail handler', fakeAsync(() => { + let successHandler = jasmine.createSpy('success'); + let failHandler = jasmine.createSpy('fail'); + + explorationCreationBackendApiService + .registerNewExplorationAsync({}) + .then(successHandler, failHandler); + + let req = httpTestingController.expectOne('/contributehandler/create_new'); + expect(req.request.method).toEqual('POST'); + req.flush( + { + error: 'Error creating a new exploration.', + }, + { status: ERROR_STATUS_CODE, - statusText: 'Error creating a new exploration.' - }); - - flushMicrotasks(); - - expect(successHandler).not.toHaveBeenCalled(); - expect(failHandler).toHaveBeenCalledWith( - 'Error creating a new exploration.'); - }) - ); - - it( - 'should successfully upload new exploration and obtain the exploration ID', - fakeAsync(() => { - expectAsync( - explorationCreationBackendApiService.uploadExploration('yaml') - ).toBeResolvedTo({explorationId: SAMPLE_EXPLORATION_ID}); - - let req = httpTestingController.expectOne('contributehandler/upload'); - expect(req.request.method).toEqual('POST'); - req.flush({exploration_id: SAMPLE_EXPLORATION_ID}); - - flushMicrotasks(); - }) - ); - - it( - 'should fail to upload new exploration and reject the promise', - fakeAsync(() => { - expectAsync( - explorationCreationBackendApiService.uploadExploration('yaml') - ).toBeRejected(); - - let req = httpTestingController.expectOne('contributehandler/upload'); - expect(req.request.method).toEqual('POST'); - req.error(new ErrorEvent('Error creating a new exploration.')); - - flushMicrotasks(); - }) - ); + statusText: 'Error creating a new exploration.', + } + ); + + flushMicrotasks(); + + expect(successHandler).not.toHaveBeenCalled(); + expect(failHandler).toHaveBeenCalledWith( + 'Error creating a new exploration.' + ); + })); + + it('should successfully upload new exploration and obtain the exploration ID', fakeAsync(() => { + expectAsync( + explorationCreationBackendApiService.uploadExploration('yaml') + ).toBeResolvedTo({explorationId: SAMPLE_EXPLORATION_ID}); + + let req = httpTestingController.expectOne('contributehandler/upload'); + expect(req.request.method).toEqual('POST'); + req.flush({exploration_id: SAMPLE_EXPLORATION_ID}); + + flushMicrotasks(); + })); + + it('should fail to upload new exploration and reject the promise', fakeAsync(() => { + expectAsync( + explorationCreationBackendApiService.uploadExploration('yaml') + ).toBeRejected(); + + let req = httpTestingController.expectOne('contributehandler/upload'); + expect(req.request.method).toEqual('POST'); + req.error(new ErrorEvent('Error creating a new exploration.')); + + flushMicrotasks(); + })); }); diff --git a/core/templates/components/entity-creation-services/exploration-creation-backend-api.service.ts b/core/templates/components/entity-creation-services/exploration-creation-backend-api.service.ts index dbae26822aa2..86e3330c6945 100644 --- a/core/templates/components/entity-creation-services/exploration-creation-backend-api.service.ts +++ b/core/templates/components/entity-creation-services/exploration-creation-backend-api.service.ts @@ -17,12 +17,12 @@ * modal. */ -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; interface ExplorationCreationBackendDict { - 'exploration_id': string; + exploration_id: string; } export interface ExplorationCreationResponse { @@ -34,47 +34,56 @@ export interface NewExplorationData { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ExplorationCreationBackendApiService { constructor(private http: HttpClient) {} private _createExploration( - newExplorationData: NewExplorationData | {}, - successCallback: (value: ExplorationCreationResponse) => void, - errorCallback: (reason: string) => void + newExplorationData: NewExplorationData | {}, + successCallback: (value: ExplorationCreationResponse) => void, + errorCallback: (reason: string) => void ): void { - this.http.post( - '/contributehandler/create_new', newExplorationData).toPromise() - .then(response => { - if (successCallback) { - successCallback({ - explorationId: response.exploration_id - }); + this.http + .post( + '/contributehandler/create_new', + newExplorationData + ) + .toPromise() + .then( + response => { + if (successCallback) { + successCallback({ + explorationId: response.exploration_id, + }); + } + }, + errorResponse => { + if (errorCallback) { + errorCallback(errorResponse.error.error); + } } - }, errorResponse => { - if (errorCallback) { - errorCallback(errorResponse.error.error); - } - }); + ); } uploadExploration(yamlFile: string): Promise { let form = new FormData(); form.append('yaml_file', yamlFile); form.append('payload', JSON.stringify({})); - return this.http.post( - 'contributehandler/upload', form - ).toPromise().then( - (data) => Promise.resolve({ - explorationId: data.exploration_id - }), - (response) => Promise.reject(response) - ); + return this.http + .post('contributehandler/upload', form) + .toPromise() + .then( + data => + Promise.resolve({ + explorationId: data.exploration_id, + }), + response => Promise.reject(response) + ); } async registerNewExplorationAsync( - newExplorationData: NewExplorationData | {} + newExplorationData: NewExplorationData | {} ): Promise { return new Promise((resolve, reject) => { this._createExploration(newExplorationData, resolve, reject); @@ -82,7 +91,9 @@ export class ExplorationCreationBackendApiService { } } -angular.module('oppia').factory( - 'ExplorationCreationBackendApiService', - downgradeInjectable(ExplorationCreationBackendApiService) -); +angular + .module('oppia') + .factory( + 'ExplorationCreationBackendApiService', + downgradeInjectable(ExplorationCreationBackendApiService) + ); diff --git a/core/templates/components/entity-creation-services/exploration-creation.service.spec.ts b/core/templates/components/entity-creation-services/exploration-creation.service.spec.ts index 38e56d6e05fa..f8379c315b2c 100644 --- a/core/templates/components/entity-creation-services/exploration-creation.service.spec.ts +++ b/core/templates/components/entity-creation-services/exploration-creation.service.spec.ts @@ -16,17 +16,20 @@ * @fileoverview Unit test for Exploration creation service. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; - -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { AlertsService } from 'services/alerts.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { LoaderService } from 'services/loader.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { ExplorationCreationBackendApiService, ExplorationCreationResponse } from './exploration-creation-backend-api.service'; -import { ExplorationCreationService } from './exploration-creation.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {fakeAsync, TestBed, tick} from '@angular/core/testing'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; + +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {AlertsService} from 'services/alerts.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {LoaderService} from 'services/loader.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import { + ExplorationCreationBackendApiService, + ExplorationCreationResponse, +} from './exploration-creation-backend-api.service'; +import {ExplorationCreationService} from './exploration-creation.service'; class MockWindowRef { _window = { @@ -37,9 +40,9 @@ class MockWindowRef { }, set href(val) { this._href = val; - } + }, }, - gtag: () => {} + gtag: () => {}, }; get nativeWindow() { @@ -64,9 +67,9 @@ describe('ExplorationCreationService', () => { providers: [ { provide: WindowRef, - useValue: windowRef - } - ] + useValue: windowRef, + }, + ], }); ecs = TestBed.inject(ExplorationCreationService); @@ -79,14 +82,17 @@ describe('ExplorationCreationService', () => { }); describe('on calling createNewExploration', () => { - it('should not create a new exploration if another exploration' + - ' creation is in progress', () => { - spyOn(ecbas, 'registerNewExplorationAsync'); - ecs.explorationCreationInProgress = true; - - ecs.createNewExploration(); - expect(ecbas.registerNewExplorationAsync).not.toHaveBeenCalled(); - }); + it( + 'should not create a new exploration if another exploration' + + ' creation is in progress', + () => { + spyOn(ecbas, 'registerNewExplorationAsync'); + ecs.explorationCreationInProgress = true; + + ecs.createNewExploration(); + expect(ecbas.registerNewExplorationAsync).not.toHaveBeenCalled(); + } + ); it('should change loadingMessage to Creating exploration', () => { spyOn(loaderService, 'showLoadingScreen'); @@ -94,8 +100,9 @@ describe('ExplorationCreationService', () => { ecs.createNewExploration(); - expect(loaderService.showLoadingScreen) - .toHaveBeenCalledWith('Creating exploration'); + expect(loaderService.showLoadingScreen).toHaveBeenCalledWith( + 'Creating exploration' + ); }); it('should create new exploration', fakeAsync(() => { @@ -104,13 +111,16 @@ describe('ExplorationCreationService', () => { '/url/to/exp1' ); spyOn(ecbas, 'registerNewExplorationAsync').and.callFake(() => { - return new Promise(( + return new Promise( + ( successCallback: (response: {explorationId: string}) => void, - errorCallback: (errorMessage: string) => void) => { - successCallback({ - explorationId: 'exp1' - }); - }); + errorCallback: (errorMessage: string) => void + ) => { + successCallback({ + explorationId: 'exp1', + }); + } + ); }); expect(ecs.explorationCreationInProgress).toBeFalse(); @@ -128,11 +138,14 @@ describe('ExplorationCreationService', () => { spyOn(urlInterpolationService, 'interpolateUrl'); spyOn(loaderService, 'hideLoadingScreen'); spyOn(ecbas, 'registerNewExplorationAsync').and.callFake(() => { - return new Promise(( + return new Promise( + ( successCallback: (response: {explorationId: string}) => void, - errorCallback: (errorMessage: string) => void) => { - errorCallback('Error'); - }); + errorCallback: (errorMessage: string) => void + ) => { + errorCallback('Error'); + } + ); }); expect(ecs.explorationCreationInProgress).toBeFalse(); @@ -143,8 +156,9 @@ describe('ExplorationCreationService', () => { expect(ecs.explorationCreationInProgress).toBeFalse(); expect(windowRef.nativeWindow.location.href).toBe(''); - expect(siteAnalyticsService.registerCreateNewExplorationEvent) - .not.toHaveBeenCalled(); + expect( + siteAnalyticsService.registerCreateNewExplorationEvent + ).not.toHaveBeenCalled(); expect(urlInterpolationService.interpolateUrl).not.toHaveBeenCalled(); expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); })); @@ -152,16 +166,14 @@ describe('ExplorationCreationService', () => { describe('on calling showUploadExplorationModal', () => { it('should show upload exploration modal', fakeAsync(() => { - spyOn(ngbModal, 'open').and.returnValue( - { - result: Promise.resolve({ - yamlFile: '' - }) - } as NgbModalRef - ); + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.resolve({ + yamlFile: '', + }), + } as NgbModalRef); spyOn(ecbas, 'uploadExploration').and.callFake((_: string) => { return Promise.resolve({ - explorationId: 'expId' + explorationId: 'expId', }); }); @@ -172,28 +184,30 @@ describe('ExplorationCreationService', () => { expect(windowRef.nativeWindow.location.href).toBe('/create/expId'); })); - it('should show upload exploration modal and display alert if post' + - ' request fails', fakeAsync(() => { - spyOn(ngbModal, 'open').and.returnValue( - { + it( + 'should show upload exploration modal and display alert if post' + + ' request fails', + fakeAsync(() => { + spyOn(ngbModal, 'open').and.returnValue({ result: Promise.resolve({ - yamlFile: '' - }) - } as NgbModalRef - ); - spyOn(alertsService, 'addWarning'); - spyOn(loaderService, 'hideLoadingScreen'); - spyOn(ecbas, 'uploadExploration').and.callFake((_: string) => { - return Promise.reject({ error: 'Failed to upload exploration' }); - }); + yamlFile: '', + }), + } as NgbModalRef); + spyOn(alertsService, 'addWarning'); + spyOn(loaderService, 'hideLoadingScreen'); + spyOn(ecbas, 'uploadExploration').and.callFake((_: string) => { + return Promise.reject({error: 'Failed to upload exploration'}); + }); - ecs.showUploadExplorationModal(); + ecs.showUploadExplorationModal(); - tick(200); + tick(200); - expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Failed to upload exploration'); - expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); - })); + expect(alertsService.addWarning).toHaveBeenCalledWith( + 'Failed to upload exploration' + ); + expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); + }) + ); }); }); diff --git a/core/templates/components/entity-creation-services/exploration-creation.service.ts b/core/templates/components/entity-creation-services/exploration-creation.service.ts index 3f00395bd038..df1f13606f57 100644 --- a/core/templates/components/entity-creation-services/exploration-creation.service.ts +++ b/core/templates/components/entity-creation-services/exploration-creation.service.ts @@ -17,21 +17,21 @@ * modal. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { AlertsService } from 'services/alerts.service'; -import { LoaderService } from 'services/loader.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { UploadActivityModalComponent } from 'pages/creator-dashboard-page/modal-templates/upload-activity-modal.component'; -import { ExplorationCreationBackendApiService } from './exploration-creation-backend-api.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {AlertsService} from 'services/alerts.service'; +import {LoaderService} from 'services/loader.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {UploadActivityModalComponent} from 'pages/creator-dashboard-page/modal-templates/upload-activity-modal.component'; +import {ExplorationCreationBackendApiService} from './exploration-creation-backend-api.service'; - @Injectable({ - providedIn: 'root' - }) +@Injectable({ + providedIn: 'root', +}) export class ExplorationCreationService { CREATE_NEW_EXPLORATION_URL_TEMPLATE = '/create/'; explorationCreationInProgress: boolean = false; @@ -42,8 +42,7 @@ export class ExplorationCreationService { private loaderService: LoaderService, private ngbModal: NgbModal, private windowRef: WindowRef, - private explorationCreationBackendApiService: - ExplorationCreationBackendApiService + private explorationCreationBackendApiService: ExplorationCreationBackendApiService ) {} createNewExploration(): void { @@ -54,55 +53,64 @@ export class ExplorationCreationService { this.alertsService.clearWarnings(); this.loaderService.showLoadingScreen('Creating exploration'); - this.explorationCreationBackendApiService.registerNewExplorationAsync({}) - .then((response) => { - this.siteAnalyticsService.registerCreateNewExplorationEvent( - response.explorationId); - setTimeout(() => { - this.windowRef.nativeWindow.location.href = ( - this.urlInterpolationService.interpolateUrl( - this.CREATE_NEW_EXPLORATION_URL_TEMPLATE, { - exploration_id: response.explorationId - } - ) + this.explorationCreationBackendApiService + .registerNewExplorationAsync({}) + .then( + response => { + this.siteAnalyticsService.registerCreateNewExplorationEvent( + response.explorationId ); - }, 150); - return false; - }, () => { - this.loaderService.hideLoadingScreen(); - this.explorationCreationInProgress = false; - }); + setTimeout(() => { + this.windowRef.nativeWindow.location.href = + this.urlInterpolationService.interpolateUrl( + this.CREATE_NEW_EXPLORATION_URL_TEMPLATE, + { + exploration_id: response.explorationId, + } + ); + }, 150); + return false; + }, + () => { + this.loaderService.hideLoadingScreen(); + this.explorationCreationInProgress = false; + } + ); } showUploadExplorationModal(): void { this.alertsService.clearWarnings(); - this.ngbModal.open( - UploadActivityModalComponent, {backdrop: 'static'} - ).result.then((result) => { - const yamlFile = result.yamlFile; + this.ngbModal + .open(UploadActivityModalComponent, {backdrop: 'static'}) + .result.then(result => { + const yamlFile = result.yamlFile; - this.loaderService.showLoadingScreen('Creating exploration'); - this.explorationCreationBackendApiService.uploadExploration( - yamlFile - ).then( - (data) => { - this.windowRef.nativeWindow.location.href = ( - this.urlInterpolationService.interpolateUrl( - this.CREATE_NEW_EXPLORATION_URL_TEMPLATE, { - exploration_id: data.explorationId - } - ) + this.loaderService.showLoadingScreen('Creating exploration'); + this.explorationCreationBackendApiService + .uploadExploration(yamlFile) + .then( + data => { + this.windowRef.nativeWindow.location.href = + this.urlInterpolationService.interpolateUrl( + this.CREATE_NEW_EXPLORATION_URL_TEMPLATE, + { + exploration_id: data.explorationId, + } + ); + }, + response => { + this.alertsService.addWarning( + response.error || 'Error communicating with server.' + ); + this.loaderService.hideLoadingScreen(); + } ); - }, - (response) => { - this.alertsService.addWarning( - response.error || 'Error communicating with server.'); - this.loaderService.hideLoadingScreen(); - } - ); - }); + }); } } -angular.module('oppia').factory( - 'ExplorationCreationService', - downgradeInjectable(ExplorationCreationService)); +angular + .module('oppia') + .factory( + 'ExplorationCreationService', + downgradeInjectable(ExplorationCreationService) + ); diff --git a/core/templates/components/entity-creation-services/skill-creation.service.spec.ts b/core/templates/components/entity-creation-services/skill-creation.service.spec.ts index a0f9185f5ea2..5476a51f4a82 100644 --- a/core/templates/components/entity-creation-services/skill-creation.service.spec.ts +++ b/core/templates/components/entity-creation-services/skill-creation.service.spec.ts @@ -16,9 +16,9 @@ * @fileoverview Unit test for SkillCreationService. */ -import { TestBed } from '@angular/core/testing'; -import { TopicsAndSkillsDashboardPageConstants } from 'pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.constants'; -import { SkillCreationService } from './skill-creation.service'; +import {TestBed} from '@angular/core/testing'; +import {TopicsAndSkillsDashboardPageConstants} from 'pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.constants'; +import {SkillCreationService} from './skill-creation.service'; describe('SkillCreationService', () => { let skillCreationService: SkillCreationService; @@ -30,48 +30,56 @@ describe('SkillCreationService', () => { it('should get skill description status', () => { expect(skillCreationService.getSkillDescriptionStatus()).toBe( TopicsAndSkillsDashboardPageConstants.SKILL_DESCRIPTION_STATUS_VALUES - .STATUS_UNCHANGED); + .STATUS_UNCHANGED + ); }); it('should mark change in skill description status', () => { expect(skillCreationService.skillDescriptionStatusMarker).toBe( TopicsAndSkillsDashboardPageConstants.SKILL_DESCRIPTION_STATUS_VALUES - .STATUS_UNCHANGED); + .STATUS_UNCHANGED + ); skillCreationService.markChangeInSkillDescription(); expect(skillCreationService.skillDescriptionStatusMarker).toBe( TopicsAndSkillsDashboardPageConstants.SKILL_DESCRIPTION_STATUS_VALUES - .STATUS_CHANGED); + .STATUS_CHANGED + ); }); it('should disable skill description status marker', () => { expect(skillCreationService.skillDescriptionStatusMarker).toBe( TopicsAndSkillsDashboardPageConstants.SKILL_DESCRIPTION_STATUS_VALUES - .STATUS_UNCHANGED); + .STATUS_UNCHANGED + ); skillCreationService.disableSkillDescriptionStatusMarker(); expect(skillCreationService.skillDescriptionStatusMarker).toBe( TopicsAndSkillsDashboardPageConstants.SKILL_DESCRIPTION_STATUS_VALUES - .STATUS_DISABLED); + .STATUS_DISABLED + ); }); it('should reset skill description status marker', () => { expect(skillCreationService.skillDescriptionStatusMarker).toBe( TopicsAndSkillsDashboardPageConstants.SKILL_DESCRIPTION_STATUS_VALUES - .STATUS_UNCHANGED); + .STATUS_UNCHANGED + ); skillCreationService.disableSkillDescriptionStatusMarker(); expect(skillCreationService.skillDescriptionStatusMarker).toBe( TopicsAndSkillsDashboardPageConstants.SKILL_DESCRIPTION_STATUS_VALUES - .STATUS_DISABLED); + .STATUS_DISABLED + ); skillCreationService.resetSkillDescriptionStatusMarker(); expect(skillCreationService.skillDescriptionStatusMarker).toBe( TopicsAndSkillsDashboardPageConstants.SKILL_DESCRIPTION_STATUS_VALUES - .STATUS_UNCHANGED); + .STATUS_UNCHANGED + ); }); }); diff --git a/core/templates/components/entity-creation-services/skill-creation.service.ts b/core/templates/components/entity-creation-services/skill-creation.service.ts index 81a1e885b5fb..a6408cc23d3b 100644 --- a/core/templates/components/entity-creation-services/skill-creation.service.ts +++ b/core/templates/components/entity-creation-services/skill-creation.service.ts @@ -16,24 +16,23 @@ * @fileoverview Functionality for creating a new skill. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { TopicsAndSkillsDashboardPageConstants } from 'pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.constants'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {TopicsAndSkillsDashboardPageConstants} from 'pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.constants'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class SkillCreationService { CREATE_NEW_SKILL_URL_TEMPLATE: string = '/skill_editor/'; skillCreationInProgress: boolean = false; - skillDescriptionStatusMarker: string = ( - TopicsAndSkillsDashboardPageConstants - .SKILL_DESCRIPTION_STATUS_VALUES.STATUS_UNCHANGED); + skillDescriptionStatusMarker: string = + TopicsAndSkillsDashboardPageConstants.SKILL_DESCRIPTION_STATUS_VALUES + .STATUS_UNCHANGED; markChangeInSkillDescription(): void { - this.skillDescriptionStatusMarker = ( - TopicsAndSkillsDashboardPageConstants.SKILL_DESCRIPTION_STATUS_VALUES - .STATUS_CHANGED); + this.skillDescriptionStatusMarker = + TopicsAndSkillsDashboardPageConstants.SKILL_DESCRIPTION_STATUS_VALUES.STATUS_CHANGED; } getSkillDescriptionStatus(): string { @@ -41,17 +40,16 @@ export class SkillCreationService { } disableSkillDescriptionStatusMarker(): void { - this.skillDescriptionStatusMarker = ( - TopicsAndSkillsDashboardPageConstants.SKILL_DESCRIPTION_STATUS_VALUES - .STATUS_DISABLED); + this.skillDescriptionStatusMarker = + TopicsAndSkillsDashboardPageConstants.SKILL_DESCRIPTION_STATUS_VALUES.STATUS_DISABLED; } resetSkillDescriptionStatusMarker(): void { - this.skillDescriptionStatusMarker = ( - TopicsAndSkillsDashboardPageConstants.SKILL_DESCRIPTION_STATUS_VALUES - .STATUS_UNCHANGED); + this.skillDescriptionStatusMarker = + TopicsAndSkillsDashboardPageConstants.SKILL_DESCRIPTION_STATUS_VALUES.STATUS_UNCHANGED; } } -angular.module('oppia').service('SkillCreationService', - downgradeInjectable(SkillCreationService)); +angular + .module('oppia') + .service('SkillCreationService', downgradeInjectable(SkillCreationService)); diff --git a/core/templates/components/entity-creation-services/story-creation-backend-api.service.spec.ts b/core/templates/components/entity-creation-services/story-creation-backend-api.service.spec.ts index cf2cccda103b..4e36cd864e77 100644 --- a/core/templates/components/entity-creation-services/story-creation-backend-api.service.spec.ts +++ b/core/templates/components/entity-creation-services/story-creation-backend-api.service.spec.ts @@ -16,15 +16,25 @@ * @fileoverview Unit test for Story Creation Service. */ -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { async, fakeAsync, flush, flushMicrotasks, TestBed, tick } from '@angular/core/testing'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { Topic, TopicBackendDict } from 'domain/topic/topic-object.model'; -import { TopicEditorStateService } from 'pages/topic-editor-page/services/topic-editor-state.service'; -import { CsrfTokenService } from 'services/csrf-token.service'; -import { ImageLocalStorageService } from 'services/image-local-storage.service'; -import { StoryCreationBackendApiService } from './story-creation-backend-api.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import { + async, + fakeAsync, + flush, + flushMicrotasks, + TestBed, + tick, +} from '@angular/core/testing'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {Topic, TopicBackendDict} from 'domain/topic/topic-object.model'; +import {TopicEditorStateService} from 'pages/topic-editor-page/services/topic-editor-state.service'; +import {CsrfTokenService} from 'services/csrf-token.service'; +import {ImageLocalStorageService} from 'services/image-local-storage.service'; +import {StoryCreationBackendApiService} from './story-creation-backend-api.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; describe('Story Creation Backend Api Service', () => { let scbas: StoryCreationBackendApiService; @@ -38,18 +48,14 @@ describe('Story Creation Backend Api Service', () => { let windowRef: WindowRef; let mockWindow = { location: { - href: '' - } + href: '', + }, }; beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - ], - providers: [ - StoryCreationBackendApiService - ] + imports: [HttpClientTestingModule], + providers: [StoryCreationBackendApiService], }); })); @@ -61,33 +67,41 @@ describe('Story Creation Backend Api Service', () => { description: 'Topic description', version: 1, uncategorized_skill_ids: ['skill_1'], - canonical_story_references: [{ - story_id: 'story_1', - story_is_published: true - }, { - story_id: 'story_2', - story_is_published: true - }, { - story_id: 'story_3', - story_is_published: true - }], - additional_story_references: [{ - story_id: 'story_2', - story_is_published: true - }], - subtopics: [{ - id: 1, - title: 'Title', - skill_ids: ['skill_2'] - }], + canonical_story_references: [ + { + story_id: 'story_1', + story_is_published: true, + }, + { + story_id: 'story_2', + story_is_published: true, + }, + { + story_id: 'story_3', + story_is_published: true, + }, + ], + additional_story_references: [ + { + story_id: 'story_2', + story_is_published: true, + }, + ], + subtopics: [ + { + id: 1, + title: 'Title', + skill_ids: ['skill_2'], + }, + ], next_subtopic_id: 2, language_code: 'en', - skill_ids_for_diagnostic_test: [] + skill_ids_for_diagnostic_test: [], }, skillIdToDescriptionDict: { skill_1: 'Description 1', - skill_2: 'Description 2' - } + skill_2: 'Description 2', + }, }; windowRef = TestBed.inject(WindowRef); @@ -101,23 +115,27 @@ describe('Story Creation Backend Api Service', () => { imageBlob = new Blob(['image data'], {type: 'imagetype'}); topic = Topic.create( sampleTopicBackendObject.topicDict as TopicBackendDict, - sampleTopicBackendObject.skillIdToDescriptionDict); + sampleTopicBackendObject.skillIdToDescriptionDict + ); topic.getId = () => { return 'id'; }; spyOnProperty(windowRef, 'nativeWindow', 'get').and.returnValue(mockWindow); - spyOn(imageLocalStorageService, 'getStoredImagesData').and.returnValue( - [{ + spyOn(imageLocalStorageService, 'getStoredImagesData').and.returnValue([ + { filename: 'Image1', - imageBlob: imageBlob - }]); + imageBlob: imageBlob, + }, + ]); spyOn(imageLocalStorageService, 'flushStoredImagesData').and.stub(); spyOn(imageLocalStorageService, 'getThumbnailBgColor').and.returnValue( - '#f00'); + '#f00' + ); spyOn(topicEditorStateService, 'getTopic').and.returnValue(topic); - spyOn(csrfTokenService, 'getTokenAsync') - .and.returnValue(Promise.resolve('sample-csrf-token')); + spyOn(csrfTokenService, 'getTokenAsync').and.returnValue( + Promise.resolve('sample-csrf-token') + ); })); afterEach(() => { @@ -130,8 +148,8 @@ describe('Story Creation Backend Api Service', () => { isValid: () => true, title: 'Title', description: 'Description', - urlFragment: 'url' - }) + urlFragment: 'url', + }), } as NgbModalRef); scbas.createNewCanonicalStory(); @@ -148,8 +166,8 @@ describe('Story Creation Backend Api Service', () => { isValid: () => true, title: 'Title', description: 'Description', - urlFragment: 'url' - }) + urlFragment: 'url', + }), } as NgbModalRef); scbas.createNewCanonicalStory(); @@ -158,60 +176,69 @@ describe('Story Creation Backend Api Service', () => { expect(scbas.createNewCanonicalStory()).toBeUndefined(); }); + it( + 'should post story data to server and change window location' + + ' on success', + fakeAsync(() => { + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.resolve({ + isValid: () => true, + title: 'Title', + description: 'Description', + urlFragment: 'url', + }), + } as NgbModalRef); - it('should post story data to server and change window location' + - ' on success', fakeAsync(() => { - spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.resolve({ - isValid: () => true, - title: 'Title', - description: 'Description', - urlFragment: 'url' - }) - } as NgbModalRef); + expect(mockWindow.location.href).toBe(''); + scbas.createNewCanonicalStory(); + tick(); - expect(mockWindow.location.href).toBe(''); - scbas.createNewCanonicalStory(); - tick(); + let req = httpTestingController.expectOne( + '/topic_editor_story_handler/' + 'id' + ); + expect(req.request.method).toEqual('POST'); + req.flush({storyId: 'id'}); - let req = httpTestingController.expectOne( - '/topic_editor_story_handler/' + 'id'); - expect(req.request.method).toEqual('POST'); - req.flush({storyId: 'id'}); + flush(); + flushMicrotasks(); - flush(); - flushMicrotasks(); + expect(mockWindow.location.href).toBe('/story_editor/id'); + }) + ); - expect(mockWindow.location.href).toBe('/story_editor/id'); - })); + it( + 'should post story data to server and change window location' + ' on Error', + fakeAsync(() => { + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.resolve({ + isValid: () => true, + title: 'Title', + description: 'Description', + urlFragment: 'url', + }), + } as NgbModalRef); - it('should post story data to server and change window location' + - ' on Error', fakeAsync(() => { - spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.resolve({ - isValid: () => true, - title: 'Title', - description: 'Description', - urlFragment: 'url' - }) - } as NgbModalRef); + scbas.createNewCanonicalStory(); + tick(); - scbas.createNewCanonicalStory(); - tick(); - - let req = httpTestingController.expectOne( - '/topic_editor_story_handler/' + 'id'); - expect(req.request.method).toEqual('POST'); - req.flush({ - error: 'Error creating a new exploration.' - }, { - status: 500, - statusText: 'Error creating a new exploration.' - }); + let req = httpTestingController.expectOne( + '/topic_editor_story_handler/' + 'id' + ); + expect(req.request.method).toEqual('POST'); + req.flush( + { + error: 'Error creating a new exploration.', + }, + { + status: 500, + statusText: 'Error creating a new exploration.', + } + ); - flush(); - flushMicrotasks(); - })); + flush(); + flushMicrotasks(); + }) + ); it('should throw error if the newly created story is not valid', () => { spyOn(ngbModal, 'open').and.returnValue({ @@ -219,8 +246,8 @@ describe('Story Creation Backend Api Service', () => { isValid: () => false, title: 'Title', description: 'Description', - urlFragment: 'url' - }) + urlFragment: 'url', + }), } as NgbModalRef); try { scbas.createNewCanonicalStory(); diff --git a/core/templates/components/entity-creation-services/story-creation-backend-api.service.ts b/core/templates/components/entity-creation-services/story-creation-backend-api.service.ts index 814a8f4e2ec9..48712c372f32 100644 --- a/core/templates/components/entity-creation-services/story-creation-backend-api.service.ts +++ b/core/templates/components/entity-creation-services/story-creation-backend-api.service.ts @@ -16,24 +16,24 @@ * @fileoverview Modal and functionality for the create story button. */ -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { CreateNewStoryModalComponent } from 'pages/topic-editor-page/modal-templates/create-new-story-modal.component'; -import { TopicEditorStateService } from 'pages/topic-editor-page/services/topic-editor-state.service'; -import { AlertsService } from 'services/alerts.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { ImageLocalStorageService } from 'services/image-local-storage.service'; -import { LoaderService } from 'services/loader.service'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {CreateNewStoryModalComponent} from 'pages/topic-editor-page/modal-templates/create-new-story-modal.component'; +import {TopicEditorStateService} from 'pages/topic-editor-page/services/topic-editor-state.service'; +import {AlertsService} from 'services/alerts.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {ImageLocalStorageService} from 'services/image-local-storage.service'; +import {LoaderService} from 'services/loader.service'; interface StoryCreationResponse { storyId: string; } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class StoryCreationBackendApiService { constructor( @@ -52,25 +52,34 @@ export class StoryCreationBackendApiService { storyCreationInProgress = false; private _createStory( - createStoryUrl: string, body: FormData, - successCallback: (value: StoryCreationResponse) => void, - errorCallback: (reason: string) => void): void { - this.http.post( - createStoryUrl, body).toPromise().then((response) => { - if (successCallback) { - successCallback({ - storyId: response.storyId - }); - } - }, errorResponse => { - if (errorCallback) { - errorCallback(errorResponse.error.error); - } - }); + createStoryUrl: string, + body: FormData, + successCallback: (value: StoryCreationResponse) => void, + errorCallback: (reason: string) => void + ): void { + this.http + .post(createStoryUrl, body) + .toPromise() + .then( + response => { + if (successCallback) { + successCallback({ + storyId: response.storyId, + }); + } + }, + errorResponse => { + if (errorCallback) { + errorCallback(errorResponse.error.error); + } + } + ); } async createStoryAsync( - createStoryUrl: string, body: FormData): Promise { + createStoryUrl: string, + body: FormData + ): Promise { return new Promise((resolve, reject) => { this._createStory(createStoryUrl, body, resolve, reject); }); @@ -81,56 +90,66 @@ export class StoryCreationBackendApiService { return; } - const modalRef = this.ngbModal.open( - CreateNewStoryModalComponent, { - backdrop: 'static', - }); - modalRef.result.then((newlyCreatedStory) => { - if (!newlyCreatedStory.isValid()) { - throw new Error('Story fields cannot be empty'); - } - this.storyCreationInProgress = true; - this.alertsService.clearWarnings(); - let topic = this.topicEditorStateService.getTopic(); - this.loaderService.showLoadingScreen('Creating story'); - let createStoryUrl = this.urlInterpolationService.interpolateUrl( - this.STORY_CREATOR_URL_TEMPLATE, { - topic_id: topic.getId() + const modalRef = this.ngbModal.open(CreateNewStoryModalComponent, { + backdrop: 'static', + }); + modalRef.result.then( + newlyCreatedStory => { + if (!newlyCreatedStory.isValid()) { + throw new Error('Story fields cannot be empty'); } - ); - let imagesData = this.imageLocalStorageService.getStoredImagesData(); - let bgColor = this.imageLocalStorageService.getThumbnailBgColor(); - let postData = { - title: newlyCreatedStory.title, - description: newlyCreatedStory.description, - story_url_fragment: newlyCreatedStory.urlFragment, - thumbnailBgColor: bgColor, - filename: imagesData[0].filename - }; + this.storyCreationInProgress = true; + this.alertsService.clearWarnings(); + let topic = this.topicEditorStateService.getTopic(); + this.loaderService.showLoadingScreen('Creating story'); + let createStoryUrl = this.urlInterpolationService.interpolateUrl( + this.STORY_CREATOR_URL_TEMPLATE, + { + topic_id: topic.getId(), + } + ); + let imagesData = this.imageLocalStorageService.getStoredImagesData(); + let bgColor = this.imageLocalStorageService.getThumbnailBgColor(); + let postData = { + title: newlyCreatedStory.title, + description: newlyCreatedStory.description, + story_url_fragment: newlyCreatedStory.urlFragment, + thumbnailBgColor: bgColor, + filename: imagesData[0].filename, + }; - let body = new FormData(); - body.append('payload', JSON.stringify(postData)); - body.append('image', imagesData[0].imageBlob); + let body = new FormData(); + body.append('payload', JSON.stringify(postData)); + body.append('image', imagesData[0].imageBlob); - this.createStoryAsync(createStoryUrl, body).then((response) => { - this.windowRef.nativeWindow.location.href = ( - this.urlInterpolationService.interpolateUrl( - this.STORY_EDITOR_URL_TEMPLATE, { - story_id: response.storyId - } - )); - }, () => { - this.loaderService.hideLoadingScreen(); - this.imageLocalStorageService.flushStoredImagesData(); - }); - }, () =>{ - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + this.createStoryAsync(createStoryUrl, body).then( + response => { + this.windowRef.nativeWindow.location.href = + this.urlInterpolationService.interpolateUrl( + this.STORY_EDITOR_URL_TEMPLATE, + { + story_id: response.storyId, + } + ); + }, + () => { + this.loaderService.hideLoadingScreen(); + this.imageLocalStorageService.flushStoredImagesData(); + } + ); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } } -angular.module('oppia').factory( - 'StoryCreationBackendApiService', - downgradeInjectable(StoryCreationBackendApiService)); +angular + .module('oppia') + .factory( + 'StoryCreationBackendApiService', + downgradeInjectable(StoryCreationBackendApiService) + ); diff --git a/core/templates/components/entity-creation-services/topic-creation.service.spec.ts b/core/templates/components/entity-creation-services/topic-creation.service.spec.ts index d0b20535fc74..c2bc89459469 100644 --- a/core/templates/components/entity-creation-services/topic-creation.service.spec.ts +++ b/core/templates/components/entity-creation-services/topic-creation.service.spec.ts @@ -16,18 +16,22 @@ * @fileoverview Unit test for Topic Creation Service. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { NgbModal, NgbModalModule, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { TopicCreationBackendApiService } from 'domain/topic/topic-creation-backend-api.service'; -import { NewlyCreatedTopic } from 'domain/topics_and_skills_dashboard/newly-created-topic.model'; -import { TopicsAndSkillsDashboardBackendApiService } from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { AlertsService } from 'services/alerts.service'; -import { ContextService } from 'services/context.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { ImageLocalStorageService } from 'services/image-local-storage.service'; -import { TopicCreationService } from './topic-creation.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {fakeAsync, TestBed, tick, waitForAsync} from '@angular/core/testing'; +import { + NgbModal, + NgbModalModule, + NgbModalRef, +} from '@ng-bootstrap/ng-bootstrap'; +import {TopicCreationBackendApiService} from 'domain/topic/topic-creation-backend-api.service'; +import {NewlyCreatedTopic} from 'domain/topics_and_skills_dashboard/newly-created-topic.model'; +import {TopicsAndSkillsDashboardBackendApiService} from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {AlertsService} from 'services/alerts.service'; +import {ContextService} from 'services/context.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {ImageLocalStorageService} from 'services/image-local-storage.service'; +import {TopicCreationService} from './topic-creation.service'; describe('Topic creation service', () => { let topicCreationService: TopicCreationService; @@ -36,8 +40,7 @@ describe('Topic creation service', () => { let alertsService: AlertsService; let imageLocalStorageService: ImageLocalStorageService; let topicCreationBackendApiService: TopicCreationBackendApiService; - let topicsAndSkillsDashboardBackendApiService: - TopicsAndSkillsDashboardBackendApiService; + let topicsAndSkillsDashboardBackendApiService: TopicsAndSkillsDashboardBackendApiService; let urlInterpolationService: UrlInterpolationService; class MockWindowRef { @@ -46,31 +49,28 @@ describe('Topic creation service', () => { return { close: () => {}, location: { - href: '' - } + href: '', + }, }; - } + }, }; } beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - NgbModalModule - ], + imports: [HttpClientTestingModule, NgbModalModule], providers: [ { provide: WindowRef, - useClass: MockWindowRef + useClass: MockWindowRef, }, AlertsService, ContextService, ImageLocalStorageService, TopicCreationBackendApiService, TopicsAndSkillsDashboardBackendApiService, - UrlInterpolationService - ] + UrlInterpolationService, + ], }).compileComponents(); })); @@ -81,9 +81,11 @@ describe('Topic creation service', () => { alertsService = TestBed.inject(AlertsService); imageLocalStorageService = TestBed.inject(ImageLocalStorageService); topicCreationBackendApiService = TestBed.inject( - TopicCreationBackendApiService); + TopicCreationBackendApiService + ); topicsAndSkillsDashboardBackendApiService = TestBed.inject( - TopicsAndSkillsDashboardBackendApiService); + TopicsAndSkillsDashboardBackendApiService + ); urlInterpolationService = TestBed.inject(UrlInterpolationService); }); @@ -91,26 +93,31 @@ describe('Topic creation service', () => { topicCreationService.topicCreationInProgress = false; spyOn(contextService, 'setImageSaveDestinationToLocalStorage'); spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.resolve(new NewlyCreatedTopic( - 'valid', 'valid', 'valid', 'valid')) + result: Promise.resolve( + new NewlyCreatedTopic('valid', 'valid', 'valid', 'valid') + ), } as NgbModalRef); spyOn(alertsService, 'clearWarnings'); spyOn(imageLocalStorageService, 'getStoredImagesData').and.returnValue([]); spyOn(imageLocalStorageService, 'getThumbnailBgColor').and.returnValue( - 'bgColor'); + 'bgColor' + ); spyOn(imageLocalStorageService, 'flushStoredImagesData'); spyOn(topicCreationBackendApiService, 'createTopicAsync').and.returnValue( - Promise.resolve({ topicId: 'topicId' })); + Promise.resolve({topicId: 'topicId'}) + ); spyOn( - topicsAndSkillsDashboardBackendApiService - .onTopicsAndSkillsDashboardReinitialized, 'emit'); + topicsAndSkillsDashboardBackendApiService.onTopicsAndSkillsDashboardReinitialized, + 'emit' + ); spyOn(contextService, 'resetImageSaveDestination'); spyOn(urlInterpolationService, 'interpolateUrl').and.returnValue(''); topicCreationService.createNewTopic(); tick(); tick(); - expect(contextService.setImageSaveDestinationToLocalStorage) - .toHaveBeenCalled(); + expect( + contextService.setImageSaveDestinationToLocalStorage + ).toHaveBeenCalled(); expect(ngbModal.open).toHaveBeenCalled(); expect(alertsService.clearWarnings).toHaveBeenCalled(); expect(imageLocalStorageService.getStoredImagesData).toHaveBeenCalled(); @@ -125,8 +132,9 @@ describe('Topic creation service', () => { topicCreationService.topicCreationInProgress = true; spyOn(contextService, 'setImageSaveDestinationToLocalStorage'); topicCreationService.createNewTopic(); - expect(contextService.setImageSaveDestinationToLocalStorage) - .not.toHaveBeenCalled(); + expect( + contextService.setImageSaveDestinationToLocalStorage + ).not.toHaveBeenCalled(); }); it('should throw error if topic fields are empty', fakeAsync(() => { @@ -138,17 +146,18 @@ describe('Topic creation service', () => { successCallback({ isValid: () => { return false; - } + }, }); - } - } + }, + }, } as NgbModalRef); expect(() => { topicCreationService.createNewTopic(); tick(); }).toThrowError('Topic fields cannot be empty'); - expect(contextService.setImageSaveDestinationToLocalStorage) - .toHaveBeenCalled(); + expect( + contextService.setImageSaveDestinationToLocalStorage + ).toHaveBeenCalled(); expect(ngbModal.open).toHaveBeenCalled(); })); @@ -161,13 +170,14 @@ describe('Topic creation service', () => { successCallback({ isValid: () => { return true; - } + }, }); - } - } + }, + }, } as NgbModalRef); spyOn(imageLocalStorageService, 'getThumbnailBgColor').and.returnValue( - null); + null + ); expect(() => { topicCreationService.createNewTopic(); tick(); @@ -179,17 +189,20 @@ describe('Topic creation service', () => { topicCreationService.topicCreationInProgress = false; spyOn(contextService, 'setImageSaveDestinationToLocalStorage'); spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.resolve(new NewlyCreatedTopic( - 'valid', 'valid', 'valid', 'valid')) + result: Promise.resolve( + new NewlyCreatedTopic('valid', 'valid', 'valid', 'valid') + ), } as NgbModalRef); spyOn(alertsService, 'clearWarnings'); spyOn(alertsService, 'addWarning'); spyOn(imageLocalStorageService, 'getStoredImagesData').and.returnValue([]); spyOn(imageLocalStorageService, 'getThumbnailBgColor').and.returnValue( - 'bgColor'); + 'bgColor' + ); spyOn(imageLocalStorageService, 'flushStoredImagesData'); spyOn(topicCreationBackendApiService, 'createTopicAsync').and.returnValue( - Promise.reject({ error })); + Promise.reject({error}) + ); topicCreationService.createNewTopic(); tick(); tick(); @@ -197,18 +210,18 @@ describe('Topic creation service', () => { expect(alertsService.addWarning).toHaveBeenCalledWith(error); })); - it('should do nothing when user cancels the topic creation modal', - fakeAsync(() => { - topicCreationService.topicCreationInProgress = false; - spyOn(contextService, 'setImageSaveDestinationToLocalStorage'); - spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.reject() - } as NgbModalRef); - spyOn(alertsService, 'clearWarnings'); - topicCreationService.createNewTopic(); - tick(); - expect(contextService.setImageSaveDestinationToLocalStorage) - .toHaveBeenCalled(); - expect(ngbModal.open).toHaveBeenCalled(); - })); + it('should do nothing when user cancels the topic creation modal', fakeAsync(() => { + topicCreationService.topicCreationInProgress = false; + spyOn(contextService, 'setImageSaveDestinationToLocalStorage'); + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.reject(), + } as NgbModalRef); + spyOn(alertsService, 'clearWarnings'); + topicCreationService.createNewTopic(); + tick(); + expect( + contextService.setImageSaveDestinationToLocalStorage + ).toHaveBeenCalled(); + expect(ngbModal.open).toHaveBeenCalled(); + })); }); diff --git a/core/templates/components/entity-creation-services/topic-creation.service.ts b/core/templates/components/entity-creation-services/topic-creation.service.ts index 080305c75fb1..fcafa92bb00a 100644 --- a/core/templates/components/entity-creation-services/topic-creation.service.ts +++ b/core/templates/components/entity-creation-services/topic-creation.service.ts @@ -16,20 +16,20 @@ * @fileoverview Modal and functionality for the create topic button. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { TopicCreationBackendApiService } from 'domain/topic/topic-creation-backend-api.service'; -import { TopicsAndSkillsDashboardBackendApiService } from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { CreateNewTopicModalComponent } from 'pages/topics-and-skills-dashboard-page/modals/create-new-topic-modal.component'; -import { AlertsService } from 'services/alerts.service'; -import { ContextService } from 'services/context.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { ImageLocalStorageService } from 'services/image-local-storage.service'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {TopicCreationBackendApiService} from 'domain/topic/topic-creation-backend-api.service'; +import {TopicsAndSkillsDashboardBackendApiService} from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {CreateNewTopicModalComponent} from 'pages/topics-and-skills-dashboard-page/modals/create-new-topic-modal.component'; +import {AlertsService} from 'services/alerts.service'; +import {ContextService} from 'services/context.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {ImageLocalStorageService} from 'services/image-local-storage.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class TopicCreationService { TOPIC_EDITOR_URL_TEMPLATE: string = '/topic_editor/'; @@ -42,8 +42,7 @@ export class TopicCreationService { private contextService: ContextService, private imageLocalStorageService: ImageLocalStorageService, private topicCreationBackendApiService: TopicCreationBackendApiService, - private topicsAndSkillsDashboardBackendApiService: - TopicsAndSkillsDashboardBackendApiService, + private topicsAndSkillsDashboardBackendApiService: TopicsAndSkillsDashboardBackendApiService, private urlInterpolationService: UrlInterpolationService ) {} @@ -57,47 +56,57 @@ export class TopicCreationService { windowClass: 'create-new-topic', }); - modalRef.result.then((newlyCreatedTopic) => { - if (!newlyCreatedTopic.isValid()) { - throw new Error('Topic fields cannot be empty'); + modalRef.result.then( + newlyCreatedTopic => { + if (!newlyCreatedTopic.isValid()) { + throw new Error('Topic fields cannot be empty'); + } + this.topicCreationInProgress = true; + this.alertsService.clearWarnings(); + // The window.open has to be initialized separately since if the 'open + // new tab' action does not directly result from a user input (which + // is not the case, if we wait for result from the backend before + // opening a new tab), some browsers block it as a popup. Here, the + // new tab is created as soon as the user clicks the 'Create' button + // and filled with URL once the details are fetched from the backend. + let newTab = this.windowRef.nativeWindow.open() as Window; + let imagesData = this.imageLocalStorageService.getStoredImagesData(); + let bgColor = this.imageLocalStorageService.getThumbnailBgColor(); + if (bgColor === null) { + throw new Error('Background color not found.'); + } + this.topicCreationBackendApiService + .createTopicAsync(newlyCreatedTopic, imagesData, bgColor) + .then( + response => { + this.topicsAndSkillsDashboardBackendApiService.onTopicsAndSkillsDashboardReinitialized.emit(); + this.topicCreationInProgress = false; + this.imageLocalStorageService.flushStoredImagesData(); + this.contextService.resetImageSaveDestination(); + newTab.location.href = + this.urlInterpolationService.interpolateUrl( + this.TOPIC_EDITOR_URL_TEMPLATE, + { + topic_id: response.topicId, + } + ); + }, + errorResponse => { + newTab.close(); + this.topicCreationInProgress = false; + this.alertsService.addWarning(errorResponse.error); + } + ); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is + // clicked. No further action is needed. } - this.topicCreationInProgress = true; - this.alertsService.clearWarnings(); - // The window.open has to be initialized separately since if the 'open - // new tab' action does not directly result from a user input (which - // is not the case, if we wait for result from the backend before - // opening a new tab), some browsers block it as a popup. Here, the - // new tab is created as soon as the user clicks the 'Create' button - // and filled with URL once the details are fetched from the backend. - let newTab = this.windowRef.nativeWindow.open() as Window; - let imagesData = this.imageLocalStorageService.getStoredImagesData(); - let bgColor = this.imageLocalStorageService.getThumbnailBgColor(); - if (bgColor === null) { - throw new Error('Background color not found.'); - } - this.topicCreationBackendApiService.createTopicAsync( - newlyCreatedTopic, imagesData, bgColor).then((response) => { - this.topicsAndSkillsDashboardBackendApiService - .onTopicsAndSkillsDashboardReinitialized.emit(); - this.topicCreationInProgress = false; - this.imageLocalStorageService.flushStoredImagesData(); - this.contextService.resetImageSaveDestination(); - newTab.location.href = this.urlInterpolationService.interpolateUrl( - this.TOPIC_EDITOR_URL_TEMPLATE, { - topic_id: response.topicId - }); - }, (errorResponse) => { - newTab.close(); - this.topicCreationInProgress = false; - this.alertsService.addWarning(errorResponse.error); - }); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is - // clicked. No further action is needed. - }); + ); } } -angular.module('oppia').factory('TopicCreationService', - downgradeInjectable(TopicCreationService)); +angular + .module('oppia') + .factory('TopicCreationService', downgradeInjectable(TopicCreationService)); diff --git a/core/templates/components/filter-fields/filtered-choices-field/filtered-choices-field.component.spec.ts b/core/templates/components/filter-fields/filtered-choices-field/filtered-choices-field.component.spec.ts index 73d617e4bf0a..4fa698138ab5 100644 --- a/core/templates/components/filter-fields/filtered-choices-field/filtered-choices-field.component.spec.ts +++ b/core/templates/components/filter-fields/filtered-choices-field/filtered-choices-field.component.spec.ts @@ -16,11 +16,11 @@ * @fileoverview Unit tests for the filtered choices field component. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { FormsModule } from '@angular/forms'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { MaterialModule } from 'modules/material.module'; -import { FilteredChoicesFieldComponent } from './filtered-choices-field.component'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {FormsModule} from '@angular/forms'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {MaterialModule} from 'modules/material.module'; +import {FilteredChoicesFieldComponent} from './filtered-choices-field.component'; describe('Filtered Choices Field Component', () => { let componentInstance: FilteredChoicesFieldComponent; @@ -28,14 +28,8 @@ describe('Filtered Choices Field Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - BrowserAnimationsModule, - MaterialModule, - FormsModule, - ], - declarations: [ - FilteredChoicesFieldComponent - ] + imports: [BrowserAnimationsModule, MaterialModule, FormsModule], + declarations: [FilteredChoicesFieldComponent], }).compileComponents(); })); @@ -52,7 +46,8 @@ describe('Filtered Choices Field Component', () => { componentInstance.choices = ['choice1']; componentInstance.ngOnInit(); expect(componentInstance.filteredChoices).toEqual( - componentInstance.choices); + componentInstance.choices + ); }); it('should filter choices', () => { diff --git a/core/templates/components/filter-fields/filtered-choices-field/filtered-choices-field.component.ts b/core/templates/components/filter-fields/filtered-choices-field/filtered-choices-field.component.ts index 8f34bab39127..ae5fa7ccef7c 100644 --- a/core/templates/components/filter-fields/filtered-choices-field/filtered-choices-field.component.ts +++ b/core/templates/components/filter-fields/filtered-choices-field/filtered-choices-field.component.ts @@ -16,11 +16,17 @@ * @fileoverview Component for the filtering choices. */ -import { ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core'; +import { + ChangeDetectorRef, + Component, + EventEmitter, + Input, + Output, +} from '@angular/core'; @Component({ selector: 'oppia-filtered-choices-field', - templateUrl: './filtered-choices-field.component.html' + templateUrl: './filtered-choices-field.component.html', }) export class FilteredChoicesFieldComponent { // These properties are initialized using Angular lifecycle hooks @@ -32,8 +38,7 @@ export class FilteredChoicesFieldComponent { @Input() searchLabel: string = 'search'; @Input() isSearchable?: boolean = true; @Input() noEntriesFoundLabel: string = 'No matches found'; - @Output() selectionChange: EventEmitter = ( - new EventEmitter()); + @Output() selectionChange: EventEmitter = new EventEmitter(); filteredChoices!: string[]; @@ -45,7 +50,8 @@ export class FilteredChoicesFieldComponent { filterChoices(searchTerm: string): void { this.filteredChoices = this.choices.filter( - choice => choice.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1); + choice => choice.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1 + ); this.changeDetectorRef.detectChanges(); } diff --git a/core/templates/components/filter-fields/multi-selection-field/multi-selection-field.component.spec.ts b/core/templates/components/filter-fields/multi-selection-field/multi-selection-field.component.spec.ts index aadf94c43c51..79bb85b45a33 100644 --- a/core/templates/components/filter-fields/multi-selection-field/multi-selection-field.component.spec.ts +++ b/core/templates/components/filter-fields/multi-selection-field/multi-selection-field.component.spec.ts @@ -16,12 +16,12 @@ * @fileoverview Unit tests for the multi selection field component. */ -import { ElementRef } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { MaterialModule } from 'modules/material.module'; -import { MultiSelectionFieldComponent } from './multi-selection-field.component'; +import {ElementRef} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {MaterialModule} from 'modules/material.module'; +import {MultiSelectionFieldComponent} from './multi-selection-field.component'; describe('Multi Selection Field Component', () => { let componentInstance: MultiSelectionFieldComponent; @@ -33,11 +33,9 @@ describe('Multi Selection Field Component', () => { BrowserAnimationsModule, MaterialModule, FormsModule, - ReactiveFormsModule + ReactiveFormsModule, ], - declarations: [ - MultiSelectionFieldComponent - ] + declarations: [MultiSelectionFieldComponent], }).compileComponents(); })); @@ -58,8 +56,8 @@ describe('Multi Selection Field Component', () => { valueChanges: { subscribe: (callb: (value: string) => void) => { callb(input); - } - } + }, + }, } as FormControl; componentInstance.ngOnInit(); input = 'selection 1'; @@ -67,12 +65,13 @@ describe('Multi Selection Field Component', () => { valueChanges: { subscribe(callb: (val: string) => void) { callb(input); - } - } + }, + }, } as FormControl; componentInstance.ngOnInit(); expect(componentInstance.readOnlySelections).toEqual( - componentInstance.selections); + componentInstance.selections + ); }); it('should validate input', () => { @@ -91,8 +90,8 @@ describe('Multi Selection Field Component', () => { componentInstance.readOnlySelections = []; componentInstance.newSelectionInput = { nativeElement: { - value: '' - } + value: '', + }, } as ElementRef; componentInstance.add({value: 'math'}); componentInstance.add({value: ''}); @@ -110,13 +109,11 @@ describe('Multi Selection Field Component', () => { spyOn(componentInstance, 'add'); spyOn(componentInstance, 'remove'); componentInstance.selections = ['selection 1']; - componentInstance.selected( - { option: { value: 'selection 1' }}); + componentInstance.selected({option: {value: 'selection 1'}}); expect(componentInstance.remove).toHaveBeenCalled(); expect(componentInstance.add).not.toHaveBeenCalled(); componentInstance.selections = []; - componentInstance.selected( - { option: { value: 'selection 1' }}); + componentInstance.selected({option: {value: 'selection 1'}}); expect(componentInstance.add).toHaveBeenCalled(); }); diff --git a/core/templates/components/filter-fields/multi-selection-field/multi-selection-field.component.ts b/core/templates/components/filter-fields/multi-selection-field/multi-selection-field.component.ts index 1102df65925c..6c3341dab03e 100644 --- a/core/templates/components/filter-fields/multi-selection-field/multi-selection-field.component.ts +++ b/core/templates/components/filter-fields/multi-selection-field/multi-selection-field.component.ts @@ -16,22 +16,28 @@ * @fileoverview Component for subject interests form field. */ -import { ENTER } from '@angular/cdk/keycodes'; -import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; -import { FormControl } from '@angular/forms'; -import { MatChipList } from '@angular/material/chips'; +import {ENTER} from '@angular/cdk/keycodes'; +import { + Component, + ElementRef, + EventEmitter, + Input, + Output, + ViewChild, +} from '@angular/core'; +import {FormControl} from '@angular/forms'; +import {MatChipList} from '@angular/material/chips'; import cloneDeep from 'lodash/cloneDeep'; -import { Observable } from 'rxjs'; -import { map, startWith } from 'rxjs/operators'; +import {Observable} from 'rxjs'; +import {map, startWith} from 'rxjs/operators'; @Component({ selector: 'oppia-multi-selection-field', - templateUrl: './multi-selection-field.component.html' + templateUrl: './multi-selection-field.component.html', }) export class MultiSelectionFieldComponent { @Input() selections: string[] = []; - @Output() selectionsChange: EventEmitter = ( - new EventEmitter()); + @Output() selectionsChange: EventEmitter = new EventEmitter(); @Input() label!: string; @Input() placeholder!: string; @@ -49,14 +55,16 @@ export class MultiSelectionFieldComponent { // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @ViewChild('chipList') chipList!: MatChipList; - @ViewChild('newSelectionInput') newSelectionInput!: - ElementRef; + @ViewChild('newSelectionInput') + newSelectionInput!: ElementRef; constructor() { this.filteredSelections = this.formCtrl.valueChanges.pipe( startWith(null), - map((interest: string | null) => interest ? this.filter( - interest) : this.readOnlySelections.slice())); + map((interest: string | null) => + interest ? this.filter(interest) : this.readOnlySelections.slice() + ) + ); } ngOnInit(): void { @@ -77,11 +85,14 @@ export class MultiSelectionFieldComponent { } } - return this.selections.map(s => s.toLowerCase()).indexOf( - value.toLowerCase()) < 0 ? true : false; + return this.selections + .map(s => s.toLowerCase()) + .indexOf(value.toLowerCase()) < 0 + ? true + : false; } - add(event: { value: string }): void { + add(event: {value: string}): void { const value = (event.value || '').trim(); if (!value) { return; @@ -106,7 +117,7 @@ export class MultiSelectionFieldComponent { } } - selected(event: { option: {value: string }}): void { + selected(event: {option: {value: string}}): void { if (this.selections.indexOf(event.option.value) > -1) { this.remove(event.option.value); } else { @@ -117,7 +128,7 @@ export class MultiSelectionFieldComponent { filter(value: string): string[] { const filterValue = value.toLocaleLowerCase(); - return this.readOnlySelections.filter((selection) => { + return this.readOnlySelections.filter(selection => { return selection.toLowerCase().includes(filterValue); }); } diff --git a/core/templates/components/forms/custom-forms-directives/apply-validation.directive.spec.ts b/core/templates/components/forms/custom-forms-directives/apply-validation.directive.spec.ts index 4721821501cb..1eb82dfdbfaf 100644 --- a/core/templates/components/forms/custom-forms-directives/apply-validation.directive.spec.ts +++ b/core/templates/components/forms/custom-forms-directives/apply-validation.directive.spec.ts @@ -16,16 +16,16 @@ * @fileoverview Tests for Directive for applying validation. */ -import { Component } from '@angular/core'; -import { By } from '@angular/platform-browser'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { FormControl } from '@angular/forms'; -import { ApplyValidationDirective } from './apply-validation.directive'; -import { Validator } from 'interactions/TextInput/directives/text-input-validation.service'; +import {Component} from '@angular/core'; +import {By} from '@angular/platform-browser'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {FormControl} from '@angular/forms'; +import {ApplyValidationDirective} from './apply-validation.directive'; +import {Validator} from 'interactions/TextInput/directives/text-input-validation.service'; @Component({ selector: 'mock-comp-a', - template: '
' + template: '
', }) class MockCompA {} @@ -35,14 +35,15 @@ describe('Apply validation directive', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [MockCompA, ApplyValidationDirective] + declarations: [MockCompA, ApplyValidationDirective], }).compileComponents(); fixture = TestBed.createComponent(MockCompA); fixture.detectChanges(); const directiveEl = fixture.debugElement.query( - By.directive(ApplyValidationDirective)); + By.directive(ApplyValidationDirective) + ); expect(directiveEl).not.toBeNull(); directiveInstance = directiveEl.injector.get(ApplyValidationDirective); })); @@ -54,18 +55,20 @@ describe('Apply validation directive', () => { }); it('should validate value', () => { - directiveInstance.validators = [{ - id: 'isAtLeast', - minValue: -2.5 - }] as unknown as Validator[]; + directiveInstance.validators = [ + { + id: 'isAtLeast', + minValue: -2.5, + }, + ] as unknown as Validator[]; expect(directiveInstance.validate(new FormControl(2))).toBeNull(); expect(directiveInstance.validate(new FormControl(null))).toEqual({ isAtLeast: { minValue: -2.5, - actual: null - } + actual: null, + }, }); }); }); diff --git a/core/templates/components/forms/custom-forms-directives/apply-validation.directive.ts b/core/templates/components/forms/custom-forms-directives/apply-validation.directive.ts index 4acdf27c4bf4..57695b157c74 100644 --- a/core/templates/components/forms/custom-forms-directives/apply-validation.directive.ts +++ b/core/templates/components/forms/custom-forms-directives/apply-validation.directive.ts @@ -16,20 +16,27 @@ * @fileoverview Directive for applying validation. */ -import { Directive, Input } from '@angular/core'; -import { NG_VALIDATORS, Validator, AbstractControl, ValidationErrors } from '@angular/forms'; -import { UnderscoresToCamelCasePipe } from 'filters/string-utility-filters/underscores-to-camel-case.pipe'; -import { Validator as OppiaValidator } from 'interactions/TextInput/directives/text-input-validation.service'; +import {Directive, Input} from '@angular/core'; +import { + NG_VALIDATORS, + Validator, + AbstractControl, + ValidationErrors, +} from '@angular/forms'; +import {UnderscoresToCamelCasePipe} from 'filters/string-utility-filters/underscores-to-camel-case.pipe'; +import {Validator as OppiaValidator} from 'interactions/TextInput/directives/text-input-validation.service'; import cloneDeep from 'lodash/cloneDeep'; -import { SchemaValidators } from '../validators/schema-validators'; +import {SchemaValidators} from '../validators/schema-validators'; @Directive({ selector: '[applyValidation]', - providers: [{ - provide: NG_VALIDATORS, - useExisting: ApplyValidationDirective, - multi: true - }] + providers: [ + { + provide: NG_VALIDATORS, + useExisting: ApplyValidationDirective, + multi: true, + }, + ], }) export class ApplyValidationDirective implements Validator { @Input() validators: OppiaValidator[]; @@ -47,8 +54,8 @@ export class ApplyValidationDirective implements Validator { const filterArgs = {}; for (let key in validatorSpec) { if (key !== 'id') { - filterArgs[this.underscoresToCamelCasePipe.transform(key)] = ( - cloneDeep(validatorSpec[key])); + filterArgs[this.underscoresToCamelCasePipe.transform(key)] = + cloneDeep(validatorSpec[key]); } } if (SchemaValidators[validatorName]) { diff --git a/core/templates/components/forms/custom-forms-directives/audio-file-uploader.component.spec.ts b/core/templates/components/forms/custom-forms-directives/audio-file-uploader.component.spec.ts index a30138beebdb..28d9104f24f3 100644 --- a/core/templates/components/forms/custom-forms-directives/audio-file-uploader.component.spec.ts +++ b/core/templates/components/forms/custom-forms-directives/audio-file-uploader.component.spec.ts @@ -16,17 +16,16 @@ * @fileoverview Tests for audio-file-uploader component. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { APP_BASE_HREF } from '@angular/common'; -import { RouterModule } from '@angular/router'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {APP_BASE_HREF} from '@angular/common'; +import {RouterModule} from '@angular/router'; -import { SmartRouterModule } from 'hybrid-router-module-provider'; -import { AudioFileUploaderComponent } from './audio-file-uploader.component'; +import {SmartRouterModule} from 'hybrid-router-module-provider'; +import {AudioFileUploaderComponent} from './audio-file-uploader.component'; describe('Audio File Uploader Component', () => { let component: AudioFileUploaderComponent; - let fixture: - ComponentFixture; + let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -34,27 +33,27 @@ describe('Audio File Uploader Component', () => { // TODO(#13443): Remove hybrid router module provider once all pages are // migrated to angular router. SmartRouterModule, - RouterModule.forRoot([]) + RouterModule.forRoot([]), ], declarations: [AudioFileUploaderComponent], - providers: [{ - provide: APP_BASE_HREF, - useValue: '/' - }] + providers: [ + { + provide: APP_BASE_HREF, + useValue: '/', + }, + ], }).compileComponents(); })); beforeEach(() => { - fixture = - TestBed.createComponent(AudioFileUploaderComponent); + fixture = TestBed.createComponent(AudioFileUploaderComponent); component = fixture.componentInstance; fixture.detectChanges(); }); - it('should successfully instantiate the component from beforeEach block', - () => { - expect(component).toBeDefined(); - }); + it('should successfully instantiate the component from beforeEach block', () => { + expect(component).toBeDefined(); + }); it('should upload an audio file', () => { let files = [ @@ -63,14 +62,14 @@ describe('Audio File Uploader Component', () => { }), new File(['bar'], 'audio.mp3', { type: 'audio/mp3', - }) + }), ]; spyOn(component.fileChange, 'emit'); spyOn(component.fileClear, 'emit'); files.forEach(file => { component.fileInputRef.nativeElement = { - files: [file] + files: [file], }; component.addAudio(new Event('add')); @@ -80,56 +79,63 @@ describe('Audio File Uploader Component', () => { }); }); - it('should not upload an audio file when the file fails' + - ' validation criteria', () => { - const testCases = [ - { - title: 'Uploading a file that is not an mp3 audio.', - file: new File(['foo'], 'audio.mp3', {type: 'audio/aac'}), - expected: 'Only the MP3 audio format is currently supported.' - }, - { - title: 'Uploading a file with no name.', - file: new File(['foo'], '', {type: 'audio/mpeg'}), - expected: 'Filename must not be empty.' - }, - { - title: 'Uploading an audio whose audio format does not match the' + - ' filename extension.', - file: new File(['foo'], 'video.mp4', {type: 'audio/mpeg'}), - expected: 'This audio format does not match the filename extension.' - }, - { - title: 'Uploading a non-audio file.', - file: new File(['foo'], 'video.mp4', {type: 'png'}), - expected: 'This file is not recognized as an audio file.' - } - ]; - - spyOn(component.fileClear, 'emit'); - spyOn(component.fileChange, 'emit'); - spyOn(component.inputFormRef.nativeElement, 'reset'); - - testCases.forEach(testCase => { + it( + 'should not upload an audio file when the file fails' + + ' validation criteria', + () => { + const testCases = [ + { + title: 'Uploading a file that is not an mp3 audio.', + file: new File(['foo'], 'audio.mp3', {type: 'audio/aac'}), + expected: 'Only the MP3 audio format is currently supported.', + }, + { + title: 'Uploading a file with no name.', + file: new File(['foo'], '', {type: 'audio/mpeg'}), + expected: 'Filename must not be empty.', + }, + { + title: + 'Uploading an audio whose audio format does not match the' + + ' filename extension.', + file: new File(['foo'], 'video.mp4', {type: 'audio/mpeg'}), + expected: 'This audio format does not match the filename extension.', + }, + { + title: 'Uploading a non-audio file.', + file: new File(['foo'], 'video.mp4', {type: 'png'}), + expected: 'This file is not recognized as an audio file.', + }, + ]; + + spyOn(component.fileClear, 'emit'); + spyOn(component.fileChange, 'emit'); + spyOn(component.inputFormRef.nativeElement, 'reset'); + + testCases.forEach(testCase => { + component.fileInputRef.nativeElement = { + files: [testCase.file], + }; + + component.addAudio(new Event('add')); + + expect(component.errorMessage).toEqual( + testCase.expected, + testCase.title + ); + expect(component.fileClear.emit).toHaveBeenCalled(); + expect(component.fileChange.emit).not.toHaveBeenCalled(); + expect(component.inputFormRef.nativeElement.reset).toHaveBeenCalled(); + }); + + // Testing an empty file separately, as inputForm.nativeElement.reset() is + // not called in that case. component.fileInputRef.nativeElement = { - files: [testCase.file] + files: [null], }; - component.addAudio(new Event('add')); - - expect(component.errorMessage).toEqual(testCase.expected, testCase.title); expect(component.fileClear.emit).toHaveBeenCalled(); expect(component.fileChange.emit).not.toHaveBeenCalled(); - expect(component.inputFormRef.nativeElement.reset).toHaveBeenCalled(); - }); - - // Testing an empty file separately, as inputForm.nativeElement.reset() is - // not called in that case. - component.fileInputRef.nativeElement = { - files: [null] - }; - component.addAudio(new Event('add')); - expect(component.fileClear.emit).toHaveBeenCalled(); - expect(component.fileChange.emit).not.toHaveBeenCalled(); - }); + } + ); }); diff --git a/core/templates/components/forms/custom-forms-directives/audio-file-uploader.component.ts b/core/templates/components/forms/custom-forms-directives/audio-file-uploader.component.ts index f0b28086a17f..dcf53baf06c8 100644 --- a/core/templates/components/forms/custom-forms-directives/audio-file-uploader.component.ts +++ b/core/templates/components/forms/custom-forms-directives/audio-file-uploader.component.ts @@ -16,13 +16,20 @@ * @fileoverview Component that enables the user to upload audio files. */ -import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { AppConstants } from 'app.constants'; +import { + Component, + ElementRef, + EventEmitter, + Input, + Output, + ViewChild, +} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {AppConstants} from 'app.constants'; @Component({ selector: 'oppia-audio-file-uploader', - templateUrl: './audio-file-uploader.component.html' + templateUrl: './audio-file-uploader.component.html', }) export class AudioFileUploaderComponent { // These properties are initialized using Angular lifecycle hooks @@ -78,7 +85,9 @@ export class AudioFileUploaderComponent { } } -angular.module('oppia').directive( - 'oppiaAudioFileUploader', - downgradeComponent({ component: AudioFileUploaderComponent }) -); +angular + .module('oppia') + .directive( + 'oppiaAudioFileUploader', + downgradeComponent({component: AudioFileUploaderComponent}) + ); diff --git a/core/templates/components/forms/custom-forms-directives/custom-form-components.module.ts b/core/templates/components/forms/custom-forms-directives/custom-form-components.module.ts index 1034fddbd1e4..31f63cc0f8a5 100644 --- a/core/templates/components/forms/custom-forms-directives/custom-form-components.module.ts +++ b/core/templates/components/forms/custom-forms-directives/custom-form-components.module.ts @@ -18,16 +18,16 @@ import 'core-js/es7/reflect'; import 'zone.js'; -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { NgbTooltipModule, NgbModalModule } from '@ng-bootstrap/ng-bootstrap'; -import { DynamicContentModule } from 'components/interaction-display/dynamic-content.module'; -import { MaterialModule } from 'modules/material.module'; -import { SharedPipesModule } from 'filters/shared-pipes.module'; -import { ImageUploaderComponent } from './image-uploader.component'; -import { TranslateModule } from '@ngx-translate/core'; -import { SmartRouterModule } from 'hybrid-router-module-provider'; +import {CommonModule} from '@angular/common'; +import {NgModule} from '@angular/core'; +import {FormsModule} from '@angular/forms'; +import {NgbTooltipModule, NgbModalModule} from '@ng-bootstrap/ng-bootstrap'; +import {DynamicContentModule} from 'components/interaction-display/dynamic-content.module'; +import {MaterialModule} from 'modules/material.module'; +import {SharedPipesModule} from 'filters/shared-pipes.module'; +import {ImageUploaderComponent} from './image-uploader.component'; +import {TranslateModule} from '@ngx-translate/core'; +import {SmartRouterModule} from 'hybrid-router-module-provider'; @NgModule({ imports: [ @@ -43,15 +43,8 @@ import { SmartRouterModule } from 'hybrid-router-module-provider'; SharedPipesModule, TranslateModule, ], - declarations: [ - ImageUploaderComponent - ], - entryComponents: [ - ImageUploaderComponent - ], - exports: [ - ImageUploaderComponent - ], + declarations: [ImageUploaderComponent], + entryComponents: [ImageUploaderComponent], + exports: [ImageUploaderComponent], }) - -export class CustomFormsComponentsModule { } +export class CustomFormsComponentsModule {} diff --git a/core/templates/components/forms/custom-forms-directives/edit-thumbnail-modal.component.spec.ts b/core/templates/components/forms/custom-forms-directives/edit-thumbnail-modal.component.spec.ts index e4d354a21f23..66b6a9e2e189 100644 --- a/core/templates/components/forms/custom-forms-directives/edit-thumbnail-modal.component.spec.ts +++ b/core/templates/components/forms/custom-forms-directives/edit-thumbnail-modal.component.spec.ts @@ -16,12 +16,12 @@ * @fileoverview Unit tests for Edit Thumbnail Modal Component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { EditThumbnailModalComponent } from './edit-thumbnail-modal.component'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { NO_ERRORS_SCHEMA, Pipe } from '@angular/core'; -import { SvgSanitizerService } from 'services/svg-sanitizer.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {EditThumbnailModalComponent} from './edit-thumbnail-modal.component'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {NO_ERRORS_SCHEMA, Pipe} from '@angular/core'; +import {SvgSanitizerService} from 'services/svg-sanitizer.service'; @Pipe({name: 'translate'}) class MockTranslatePipe { @@ -77,27 +77,24 @@ describe('Edit Thumbnail Modal Component', () => { } } - const fileContent = ( + const fileContent = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjA' + 'wMC9zdmciICB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCI+PGNpcmNsZSBjeD0iNTAiIGN5' + 'PSI1MCIgcj0iNDAiIHN0cm9rZT0iZ3JlZW4iIHN0cm9rZS13aWR0aD0iNCIgZmlsbD0ie' + - 'WVsbG93IiAvPjwvc3ZnPg=='); + 'WVsbG93IiAvPjwvc3ZnPg=='; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - EditThumbnailModalComponent, - MockTranslatePipe - ], + declarations: [EditThumbnailModalComponent, MockTranslatePipe], providers: [ SvgSanitizerService, { provide: NgbActiveModal, - useClass: MockActiveModal - } + useClass: MockActiveModal, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(EditThumbnailModalComponent); component = fixture.componentInstance; @@ -121,24 +118,31 @@ describe('Edit Thumbnail Modal Component', () => { spyOn(component, 'setImageDimensions').and.callThrough(); })); - it('should load a image file in onchange event and save it if it\'s a' + - ' svg file', () => { - spyOn(component, 'isUploadedImageSvg').and.returnValue(true); - spyOn(component, 'isValidFilename').and.returnValue(true); - spyOn(svgSanitizerService, 'getInvalidSvgTagsAndAttrsFromDataUri') - .and.returnValue({ tags: [], attrs: [] }); - spyOn(svgSanitizerService, 'removeAllInvalidTagsAndAttributes') - .and.returnValue(fileContent); - let file = new File([fileContent], 'circle.svg', {type: 'image/svg'}); - component.invalidImageWarningIsShown = false; - component.invalidFilenameWarningIsShown = false; - component.uploadedImageMimeType = 'image/svg+xml'; - component.imgSrc = 'source'; + it( + "should load a image file in onchange event and save it if it's a" + + ' svg file', + () => { + spyOn(component, 'isUploadedImageSvg').and.returnValue(true); + spyOn(component, 'isValidFilename').and.returnValue(true); + spyOn( + svgSanitizerService, + 'getInvalidSvgTagsAndAttrsFromDataUri' + ).and.returnValue({tags: [], attrs: []}); + spyOn( + svgSanitizerService, + 'removeAllInvalidTagsAndAttributes' + ).and.returnValue(fileContent); + let file = new File([fileContent], 'circle.svg', {type: 'image/svg'}); + component.invalidImageWarningIsShown = false; + component.invalidFilenameWarningIsShown = false; + component.uploadedImageMimeType = 'image/svg+xml'; + component.imgSrc = 'source'; - component.onFileChanged(file); - expect(component.invalidImageWarningIsShown).toBe(false); - expect(component.invalidFilenameWarningIsShown).toBe(false); - }); + component.onFileChanged(file); + expect(component.invalidImageWarningIsShown).toBe(false); + expect(component.invalidFilenameWarningIsShown).toBe(false); + } + ); it('should not load file if it is not a svg type', () => { spyOn(component, 'isUploadedImageSvg').and.returnValue(false); @@ -148,8 +152,9 @@ describe('Edit Thumbnail Modal Component', () => { // This is just a mocked base 64 in order to test the FileReader event // and its result property. const dataBase64Mock = 'PHN2ZyB4bWxucz0iaHR0cDo'; - const arrayBuffer = Uint8Array.from( - window.atob(dataBase64Mock), c => c.charCodeAt(0)); + const arrayBuffer = Uint8Array.from(window.atob(dataBase64Mock), c => + c.charCodeAt(0) + ); const file = new File([arrayBuffer], 'thumbnail.png'); component.onFileChanged(file); @@ -167,8 +172,9 @@ describe('Edit Thumbnail Modal Component', () => { // This is just a mocked base 64 in order to test the FileReader event // and its result property. const dataBase64Mock = 'PHN2ZyB4bWxucz0iaHR0cDo'; - const arrayBuffer = Uint8Array.from( - window.atob(dataBase64Mock), c => c.charCodeAt(0)); + const arrayBuffer = Uint8Array.from(window.atob(dataBase64Mock), c => + c.charCodeAt(0) + ); var file = new File([arrayBuffer], 'thumb..nail.svg'); component.onFileChanged(file); expect(component.uploadedImage).toBeNull(); @@ -206,8 +212,9 @@ describe('Edit Thumbnail Modal Component', () => { it('should check for uploaded image to have correct filename', () => { const dataBase64Mock = 'PHN2ZyB4bWxucz0iaHR0cDo'; - const arrayBuffer = Uint8Array.from( - window.atob(dataBase64Mock), c => c.charCodeAt(0)); + const arrayBuffer = Uint8Array.from(window.atob(dataBase64Mock), c => + c.charCodeAt(0) + ); const file = new File([arrayBuffer], 'thumbnail.svg'); let result = component.isValidFilename(file); expect(result).toBeTrue(); @@ -216,10 +223,10 @@ describe('Edit Thumbnail Modal Component', () => { it('should set image dimensions', () => { component.dimensions = { height: 0, - width: 0 + width: 0, }; component.setImageDimensions(180, 180); - expect(component.dimensions).toEqual({ height: 180, width: 180 }); + expect(component.dimensions).toEqual({height: 180, width: 180}); }); it('should reset the uploaded image on clicking reset button', () => { @@ -241,7 +248,7 @@ describe('Edit Thumbnail Modal Component', () => { component.openInUploadMode = false; component.dimensions = { height: 180, - width: 180 + width: 180, }; component.confirm(); expect(component.thumbnailHasChanged).toBeFalse(); @@ -251,8 +258,8 @@ describe('Edit Thumbnail Modal Component', () => { openInUploadMode: false, dimensions: { height: 180, - width: 180 - } + width: 180, + }, }); }); @@ -261,18 +268,24 @@ describe('Edit Thumbnail Modal Component', () => { expect(dismissSpy).toHaveBeenCalled(); }); - it('should disable \'Add Thumbnail\' button unless a new image is' + - ' uploaded', () => { - spyOn(component, 'isUploadedImageSvg').and.returnValue(true); - spyOn(component, 'isValidFilename').and.returnValue(true); - spyOn(svgSanitizerService, 'getInvalidSvgTagsAndAttrsFromDataUri') - .and.returnValue({ tags: [], attrs: [] }); - spyOn(svgSanitizerService, 'removeAllInvalidTagsAndAttributes') - .and.returnValue(fileContent); - let file = new File([fileContent], 'triangle.svg', {type: 'image/svg'}); - component.uploadedImageMimeType = 'image/svg+xml'; - expect(component.thumbnailHasChanged).toBeFalse(); - component.onFileChanged(file); - expect(component.thumbnailHasChanged).toBeTrue(); - }); + it( + "should disable 'Add Thumbnail' button unless a new image is" + ' uploaded', + () => { + spyOn(component, 'isUploadedImageSvg').and.returnValue(true); + spyOn(component, 'isValidFilename').and.returnValue(true); + spyOn( + svgSanitizerService, + 'getInvalidSvgTagsAndAttrsFromDataUri' + ).and.returnValue({tags: [], attrs: []}); + spyOn( + svgSanitizerService, + 'removeAllInvalidTagsAndAttributes' + ).and.returnValue(fileContent); + let file = new File([fileContent], 'triangle.svg', {type: 'image/svg'}); + component.uploadedImageMimeType = 'image/svg+xml'; + expect(component.thumbnailHasChanged).toBeFalse(); + component.onFileChanged(file); + expect(component.thumbnailHasChanged).toBeTrue(); + } + ); }); diff --git a/core/templates/components/forms/custom-forms-directives/edit-thumbnail-modal.component.ts b/core/templates/components/forms/custom-forms-directives/edit-thumbnail-modal.component.ts index b9783f76f066..461dc1b61f64 100644 --- a/core/templates/components/forms/custom-forms-directives/edit-thumbnail-modal.component.ts +++ b/core/templates/components/forms/custom-forms-directives/edit-thumbnail-modal.component.ts @@ -16,11 +16,11 @@ * @fileoverview Component for edit thumbnail modal. */ -import { trigger, transition, style, animate } from '@angular/animations'; -import { Component, Input } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { AppConstants } from 'app.constants'; -import { SvgSanitizerService } from 'services/svg-sanitizer.service'; +import {trigger, transition, style, animate} from '@angular/animations'; +import {Component, Input} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {AppConstants} from 'app.constants'; +import {SvgSanitizerService} from 'services/svg-sanitizer.service'; interface InvalidTagsAndAttributes { tags: string[]; @@ -39,10 +39,10 @@ interface Dimensions { trigger('fade', [ transition('void => *', [ style({opacity: 0}), - animate(500, style({opacity: 1})) - ]) - ]) - ] + animate(500, style({opacity: 1})), + ]), + ]), + ], }) export class EditThumbnailModalComponent { // These properties are initialized using Angular lifecycle hooks @@ -64,7 +64,8 @@ export class EditThumbnailModalComponent { @Input() openInUploadMode: boolean = false; imgSrc!: string; invalidTagsAndAttributes: InvalidTagsAndAttributes = { - tags: [], attrs: [] + tags: [], + attrs: [], }; invalidImageWarningIsShown = false; @@ -74,13 +75,13 @@ export class EditThumbnailModalComponent { constructor( private svgSanitizerService: SvgSanitizerService, - private ngbActiveModal: NgbActiveModal, + private ngbActiveModal: NgbActiveModal ) {} setImageDimensions(height: number, width: number): void { this.dimensions = { height: Math.round(height), - width: Math.round(width) + width: Math.round(width), }; } @@ -90,7 +91,8 @@ export class EditThumbnailModalComponent { isValidFilename(file: File): boolean { const VALID_THUMBNAIL_FILENAME_REGEX = new RegExp( - AppConstants.VALID_THUMBNAIL_FILENAME_REGEX); + AppConstants.VALID_THUMBNAIL_FILENAME_REGEX + ); return VALID_THUMBNAIL_FILENAME_REGEX.test(file.name); } @@ -110,17 +112,21 @@ export class EditThumbnailModalComponent { // height and width defined. this.setImageDimensions( img.naturalHeight || 150, - img.naturalWidth || 300); + img.naturalWidth || 300 + ); }; this.imgSrc = reader.result as string; this.updateBackgroundColor(this.tempBgColor); img.src = this.imgSrc; this.uploadedImage = this.imgSrc; - this.invalidTagsAndAttributes = ( + this.invalidTagsAndAttributes = this.svgSanitizerService.getInvalidSvgTagsAndAttrsFromDataUri( - this.imgSrc)); - this.uploadedImage = this.svgSanitizerService - .removeAllInvalidTagsAndAttributes(this.uploadedImage); + this.imgSrc + ); + this.uploadedImage = + this.svgSanitizerService.removeAllInvalidTagsAndAttributes( + this.uploadedImage + ); this.thumbnailHasChanged = true; }; reader.readAsDataURL(file); @@ -132,7 +138,7 @@ export class EditThumbnailModalComponent { this.invalidFilenameWarningIsShown = false; this.invalidTagsAndAttributes = { tags: [], - attrs: [] + attrs: [], }; if (this.isUploadedImageSvg() && this.isValidFilename(file)) { this.setUploadedFile(file); @@ -162,7 +168,7 @@ export class EditThumbnailModalComponent { newThumbnailDataUrl: this.uploadedImage, newBgColor: this.bgColor, openInUploadMode: this.openInUploadMode, - dimensions: this.dimensions + dimensions: this.dimensions, }); } diff --git a/core/templates/components/forms/custom-forms-directives/html-select.component.spec.ts b/core/templates/components/forms/custom-forms-directives/html-select.component.spec.ts index a40dc907c396..e899697ce85b 100644 --- a/core/templates/components/forms/custom-forms-directives/html-select.component.spec.ts +++ b/core/templates/components/forms/custom-forms-directives/html-select.component.spec.ts @@ -16,9 +16,9 @@ * @fileoverview Unit tests for HTML Select Component. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { HtmlSelectComponent } from './html-select.component'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {HtmlSelectComponent} from './html-select.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; describe('HTML Select Component', () => { let fixture: ComponentFixture; @@ -32,31 +32,34 @@ describe('HTML Select Component', () => { })); beforeEach(() => { - fixture = TestBed.createComponent( - HtmlSelectComponent); + fixture = TestBed.createComponent(HtmlSelectComponent); component = fixture.componentInstance; component.options = [ - { id: '12', val: 'string' }, - { id: '21', val: 'string' }]; + {id: '12', val: 'string'}, + {id: '21', val: 'string'}, + ]; }); it('should initialize Selection with selectionId', () => { component.selectionId = '21'; component.ngOnInit(); - expect(component.selection).toEqual({ id: '21', val: 'string' }); + expect(component.selection).toEqual({id: '21', val: 'string'}); }); - it('should initialize Selection with the first option when selectionId' + - ' not in options', () => { - component.selectionId = '13'; - component.ngOnInit(); - expect(component.selection).toEqual({ id: '12', val: 'string' }); - }); + it( + 'should initialize Selection with the first option when selectionId' + + ' not in options', + () => { + component.selectionId = '13'; + component.ngOnInit(); + expect(component.selection).toEqual({id: '12', val: 'string'}); + } + ); it('should update Selection', () => { component.ngOnInit(); - component.selection = { id: '1', val: 'string' }; + component.selection = {id: '1', val: 'string'}; spyOn(component.onSelectionChange, 'emit'); component.updatedSelection(); diff --git a/core/templates/components/forms/custom-forms-directives/html-select.component.ts b/core/templates/components/forms/custom-forms-directives/html-select.component.ts index 3d9374448868..780034778a7d 100644 --- a/core/templates/components/forms/custom-forms-directives/html-select.component.ts +++ b/core/templates/components/forms/custom-forms-directives/html-select.component.ts @@ -16,28 +16,29 @@ * @fileoverview Component for the selection dropdown with HTML content. */ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; @Component({ selector: 'oppia-html-select', - templateUrl: './html-select.component.html' + templateUrl: './html-select.component.html', }) export class HtmlSelectComponent implements OnInit { // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 - @Input() options!: { id: string; val: string }[]; + @Input() options!: {id: string; val: string}[]; @Input() selectionId!: string; @Output() onSelectionChange = new EventEmitter(); - selection!: { id: string; val: string }; + selection!: {id: string; val: string}; ngOnInit(): void { if (!this.selectionId) { this.selection = this.options[0]; } else { const selectionIndex = this.options.findIndex( - option => option.id === this.selectionId); + option => option.id === this.selectionId + ); if (selectionIndex === -1) { this.selection = this.options[0]; } else { diff --git a/core/templates/components/forms/custom-forms-directives/image-uploader.component.spec.ts b/core/templates/components/forms/custom-forms-directives/image-uploader.component.spec.ts index cb5cc7857287..0bb02e5a4cf6 100644 --- a/core/templates/components/forms/custom-forms-directives/image-uploader.component.spec.ts +++ b/core/templates/components/forms/custom-forms-directives/image-uploader.component.spec.ts @@ -16,19 +16,19 @@ * @fileoverview Tests for audio-file-uploader component. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { MatIconModule } from '@angular/material/icon'; -import { APP_BASE_HREF } from '@angular/common'; -import { RouterModule } from '@angular/router'; - -import { SmartRouterModule } from 'hybrid-router-module-provider'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { IdGenerationService } from 'services/id-generation.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { ImageUploaderComponent } from './image-uploader.component'; -import { BlogDashboardPageService } from 'pages/blog-dashboard-page/services/blog-dashboard-page.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ContextService } from 'services/context.service'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {MatIconModule} from '@angular/material/icon'; +import {APP_BASE_HREF} from '@angular/common'; +import {RouterModule} from '@angular/router'; + +import {SmartRouterModule} from 'hybrid-router-module-provider'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {IdGenerationService} from 'services/id-generation.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {ImageUploaderComponent} from './image-uploader.component'; +import {BlogDashboardPageService} from 'pages/blog-dashboard-page/services/blog-dashboard-page.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ContextService} from 'services/context.service'; describe('ImageUploaderComponent', () => { let component: ImageUploaderComponent; @@ -59,17 +59,14 @@ describe('ImageUploaderComponent', () => { // migrated to angular router. SmartRouterModule, HttpClientTestingModule, - RouterModule.forRoot([]) - ], - declarations: [ - ImageUploaderComponent, - MockTranslatePipe + RouterModule.forRoot([]), ], + declarations: [ImageUploaderComponent, MockTranslatePipe], providers: [ BlogDashboardPageService, - { provide: WindowRef, useValue: windowRef }, - { provide: APP_BASE_HREF, useValue: '/' } - ] + {provide: WindowRef, useValue: windowRef}, + {provide: APP_BASE_HREF, useValue: '/'}, + ], }).compileComponents(); })); @@ -81,7 +78,9 @@ describe('ImageUploaderComponent', () => { fixture.detectChanges(); dropAreaRefSpy = spyOn( - component.dropAreaRef.nativeElement, 'addEventListener'); + component.dropAreaRef.nativeElement, + 'addEventListener' + ); windowRefSpy = spyOn(windowRef.nativeWindow, 'addEventListener'); }); @@ -93,7 +92,8 @@ describe('ImageUploaderComponent', () => { component.ngOnInit(); expect(component.fileInputClassName).toBe( - 'image-uploader-file-input-new-id'); + 'image-uploader-file-input-new-id' + ); }); it('should register drag and drop event listener', () => { @@ -103,12 +103,12 @@ describe('ImageUploaderComponent', () => { expect(dropAreaRefSpy.calls.allArgs()).toEqual([ ['drop', jasmine.any(Function)], ['dragover', jasmine.any(Function)], - ['dragleave', jasmine.any(Function)] + ['dragleave', jasmine.any(Function)], ]); expect(windowRefSpy.calls.allArgs()).toEqual([ ['dragover', jasmine.any(Function)], - ['drop', jasmine.any(Function)] + ['drop', jasmine.any(Function)], ]); }); @@ -123,9 +123,11 @@ describe('ImageUploaderComponent', () => { spyOn(component.fileChanged, 'emit'); - component.dropAreaRef.nativeElement.dispatchEvent(new DragEvent('drop', { - dataTransfer: dataTransfer - })); + component.dropAreaRef.nativeElement.dispatchEvent( + new DragEvent('drop', { + dataTransfer: dataTransfer, + }) + ); expect(component.fileChanged.emit).toHaveBeenCalledWith(validFile); }); @@ -139,76 +141,98 @@ describe('ImageUploaderComponent', () => { let dataTransfer = null; component.dropAreaRef.nativeElement.dispatchEvent( - new DragEvent('drop', {dataTransfer: dataTransfer})); + new DragEvent('drop', {dataTransfer: dataTransfer}) + ); expect(component.fileChanged.emit).not.toHaveBeenCalled(); }); - it('should not upload image on drop if the image' + - ' format is not allowed', () => { - spyOn(contextService, 'getEntityType').and.returnValue('exploration'); - component.ngAfterViewInit(); - component.allowedImageFormats = ['jpeg', 'jpg', 'gif', 'png']; + it( + 'should not upload image on drop if the image' + ' format is not allowed', + () => { + spyOn(contextService, 'getEntityType').and.returnValue('exploration'); + component.ngAfterViewInit(); + component.allowedImageFormats = ['jpeg', 'jpg', 'gif', 'png']; - let dataTransfer = new DataTransfer(); - const file = new File( - ['image'], 'image.svg', {type: 'image/svg+xml'}); - dataTransfer.items.add(file); + let dataTransfer = new DataTransfer(); + const file = new File(['image'], 'image.svg', {type: 'image/svg+xml'}); + dataTransfer.items.add(file); - spyOn(component.fileChanged, 'emit'); + spyOn(component.fileChanged, 'emit'); - component.dropAreaRef.nativeElement.dispatchEvent(new DragEvent('drop', { - dataTransfer: dataTransfer - })); + component.dropAreaRef.nativeElement.dispatchEvent( + new DragEvent('drop', { + dataTransfer: dataTransfer, + }) + ); - expect(component.errorMessage).toBe( - 'This image format is not supported'); - expect(component.fileChanged.emit).not.toHaveBeenCalled(); - }); + expect(component.errorMessage).toBe('This image format is not supported'); + expect(component.fileChanged.emit).not.toHaveBeenCalled(); + } + ); - it('should not upload image on drop if the image filename extension' + - ' does not match the image format', () => { - spyOn(contextService, 'getEntityType').and.returnValue('exploration'); - component.ngAfterViewInit(); - component.allowedImageFormats = ['jpeg', 'jpg', 'gif', 'png', 'svg']; + it( + 'should not upload image on drop if the image filename extension' + + ' does not match the image format', + () => { + spyOn(contextService, 'getEntityType').and.returnValue('exploration'); + component.ngAfterViewInit(); + component.allowedImageFormats = ['jpeg', 'jpg', 'gif', 'png', 'svg']; - let dataTransfer = new DataTransfer(); - const fileWithDiffNameAndExtension = new File( - ['image'], 'image.png', {type: 'image/svg+xml'}); - dataTransfer.items.add(fileWithDiffNameAndExtension); + let dataTransfer = new DataTransfer(); + const fileWithDiffNameAndExtension = new File(['image'], 'image.png', { + type: 'image/svg+xml', + }); + dataTransfer.items.add(fileWithDiffNameAndExtension); - spyOn(component.fileChanged, 'emit'); + spyOn(component.fileChanged, 'emit'); - component.dropAreaRef.nativeElement.dispatchEvent(new DragEvent('drop', { - dataTransfer: dataTransfer - })); + component.dropAreaRef.nativeElement.dispatchEvent( + new DragEvent('drop', { + dataTransfer: dataTransfer, + }) + ); - expect(component.errorMessage).toBe( - 'This image format does not match the filename extension.'); - expect(component.fileChanged.emit).not.toHaveBeenCalled(); - }); + expect(component.errorMessage).toBe( + 'This image format does not match the filename extension.' + ); + expect(component.fileChanged.emit).not.toHaveBeenCalled(); + } + ); - it('should not upload image on drop if the allowed image formats list' + - ' contains non allowed file formats', () => { - spyOn(contextService, 'getEntityType').and.returnValue('exploration'); - component.ngAfterViewInit(); - component.allowedImageFormats = ['jpeg', 'jpg', 'gif', 'png', 'svg', 'mp3']; + it( + 'should not upload image on drop if the allowed image formats list' + + ' contains non allowed file formats', + () => { + spyOn(contextService, 'getEntityType').and.returnValue('exploration'); + component.ngAfterViewInit(); + component.allowedImageFormats = [ + 'jpeg', + 'jpg', + 'gif', + 'png', + 'svg', + 'mp3', + ]; - let dataTransfer = new DataTransfer(); - const file = new File( - ['image'], 'image.jpeg', {type: 'image/jpeg'}); - dataTransfer.items.add(file); + let dataTransfer = new DataTransfer(); + const file = new File(['image'], 'image.jpeg', {type: 'image/jpeg'}); + dataTransfer.items.add(file); - spyOn(component.fileChanged, 'emit'); + spyOn(component.fileChanged, 'emit'); - component.dropAreaRef.nativeElement.dispatchEvent(new DragEvent('drop', { - dataTransfer: dataTransfer - })); + component.dropAreaRef.nativeElement.dispatchEvent( + new DragEvent('drop', { + dataTransfer: dataTransfer, + }) + ); - expect(component.errorMessage).toBe( - 'mp3 is not in the list of allowed image formats.'); - expect(component.fileChanged.emit).not.toHaveBeenCalled(); - }); + expect(component.errorMessage).toBe( + 'mp3 is not in the list of allowed image formats.' + ); + expect(component.fileChanged.emit).not.toHaveBeenCalled(); + } + ); it('should not upload file on drop if the file is not an image', () => { spyOn(contextService, 'getEntityType').and.returnValue('exploration'); @@ -216,18 +240,22 @@ describe('ImageUploaderComponent', () => { component.allowedImageFormats = ['jpeg', 'jpg', 'gif', 'png', 'svg']; let dataTransfer = new DataTransfer(); - const fileWithInvalidFormat = new File( - ['image'], 'image.mp3', {type: 'mp3'}); + const fileWithInvalidFormat = new File(['image'], 'image.mp3', { + type: 'mp3', + }); dataTransfer.items.add(fileWithInvalidFormat); spyOn(component.fileChanged, 'emit'); - component.dropAreaRef.nativeElement.dispatchEvent(new DragEvent('drop', { - dataTransfer: dataTransfer - })); + component.dropAreaRef.nativeElement.dispatchEvent( + new DragEvent('drop', { + dataTransfer: dataTransfer, + }) + ); expect(component.errorMessage).toBe( - 'This file is not recognized as an image'); + 'This file is not recognized as an image' + ); expect(component.fileChanged.emit).not.toHaveBeenCalled(); }); @@ -237,102 +265,113 @@ describe('ImageUploaderComponent', () => { component.allowedImageFormats = ['jpeg', 'jpg', 'gif', 'png', 'svg']; let dataTransfer = new DataTransfer(); - let fileWithLargeSize = new File( - [''], 'image.jpg', {type: 'image/jpg'}); + let fileWithLargeSize = new File([''], 'image.jpg', {type: 'image/jpg'}); let sizeOfLargeFileInBytes = 100 * 1024 + 100; - Object.defineProperty( - fileWithLargeSize, 'size', {value: sizeOfLargeFileInBytes}); + Object.defineProperty(fileWithLargeSize, 'size', { + value: sizeOfLargeFileInBytes, + }); dataTransfer.items.add(fileWithLargeSize); spyOn(component.fileChanged, 'emit'); - component.dropAreaRef.nativeElement.dispatchEvent(new DragEvent('drop', { - dataTransfer: dataTransfer - })); + component.dropAreaRef.nativeElement.dispatchEvent( + new DragEvent('drop', { + dataTransfer: dataTransfer, + }) + ); expect(component.errorMessage).toBe( - 'The maximum allowed file size is 100 KB (100.1 KB given).'); + 'The maximum allowed file size is 100 KB (100.1 KB given).' + ); expect(component.fileChanged.emit).not.toHaveBeenCalled(); }); - it('should not upload image if the size is more than 1MB for blog post', - () => { - spyOn(contextService, 'getEntityType').and.returnValue('blog_post'); - component.ngAfterViewInit(); - component.allowedImageFormats = ['jpeg', 'jpg', 'gif', 'png', 'svg']; - - let dataTransfer = new DataTransfer(); - let fileWithLargeSize = new File( - [''], 'image.jpg', {type: 'image/jpg'}); - let sizeOfLargeFileInBytes = 1024 * 1024 + 100; - - Object.defineProperty( - fileWithLargeSize, 'size', {value: sizeOfLargeFileInBytes}); + it('should not upload image if the size is more than 1MB for blog post', () => { + spyOn(contextService, 'getEntityType').and.returnValue('blog_post'); + component.ngAfterViewInit(); + component.allowedImageFormats = ['jpeg', 'jpg', 'gif', 'png', 'svg']; - dataTransfer.items.add(fileWithLargeSize); + let dataTransfer = new DataTransfer(); + let fileWithLargeSize = new File([''], 'image.jpg', {type: 'image/jpg'}); + let sizeOfLargeFileInBytes = 1024 * 1024 + 100; - spyOn(component.fileChanged, 'emit'); + Object.defineProperty(fileWithLargeSize, 'size', { + value: sizeOfLargeFileInBytes, + }); - component.dropAreaRef.nativeElement.dispatchEvent(new DragEvent('drop', { - dataTransfer: dataTransfer - })); + dataTransfer.items.add(fileWithLargeSize); - expect(component.errorMessage).toBe( - 'The maximum allowed file size is 1024 KB (100.0 MB given).'); - expect(component.fileChanged.emit).not.toHaveBeenCalled(); - }); + spyOn(component.fileChanged, 'emit'); - it('should change background color when user drags and leaves an' + - ' image into the window', () =>{ - let dragoverEvent = new DragEvent('dragover'); - let dragLeaveEvent = new DragEvent('dragleave'); - spyOn(dragLeaveEvent, 'preventDefault'); - spyOn(dragoverEvent, 'preventDefault'); + component.dropAreaRef.nativeElement.dispatchEvent( + new DragEvent('drop', { + dataTransfer: dataTransfer, + }) + ); - expect(component.backgroundWhileUploading).toBe(false); + expect(component.errorMessage).toBe( + 'The maximum allowed file size is 1024 KB (100.0 MB given).' + ); + expect(component.fileChanged.emit).not.toHaveBeenCalled(); + }); - spyOn(contextService, 'getEntityType').and.returnValue('exploration'); - component.ngAfterViewInit(); - component.dropAreaRef.nativeElement.dispatchEvent(dragoverEvent); + it( + 'should change background color when user drags and leaves an' + + ' image into the window', + () => { + let dragoverEvent = new DragEvent('dragover'); + let dragLeaveEvent = new DragEvent('dragleave'); + spyOn(dragLeaveEvent, 'preventDefault'); + spyOn(dragoverEvent, 'preventDefault'); - expect(dragoverEvent.preventDefault).toHaveBeenCalled(); - expect(component.backgroundWhileUploading).toBe(true); + expect(component.backgroundWhileUploading).toBe(false); - component.dropAreaRef.nativeElement.dispatchEvent(dragLeaveEvent); + spyOn(contextService, 'getEntityType').and.returnValue('exploration'); + component.ngAfterViewInit(); + component.dropAreaRef.nativeElement.dispatchEvent(dragoverEvent); - expect(dragLeaveEvent.preventDefault).toHaveBeenCalled(); - expect(component.backgroundWhileUploading).toBe(false); - }); + expect(dragoverEvent.preventDefault).toHaveBeenCalled(); + expect(component.backgroundWhileUploading).toBe(true); - it('should prevent default browser behavior if user drops an image outside' + - ' of image-uploader', () => { - let mockWindow = { - addEventListener: function(eventname: string, callback: () => {}) { - document.addEventListener('mock' + eventname, callback); - } - }; - spyOnProperty(windowRef, 'nativeWindow', 'get').and.returnValue( - mockWindow as Window - ); + component.dropAreaRef.nativeElement.dispatchEvent(dragLeaveEvent); - spyOn(dropEvent, 'preventDefault'); - spyOn(dragoverEvent, 'preventDefault'); + expect(dragLeaveEvent.preventDefault).toHaveBeenCalled(); + expect(component.backgroundWhileUploading).toBe(false); + } + ); - spyOn(contextService, 'getEntityType').and.returnValue('exploration'); - component.ngAfterViewInit(); + it( + 'should prevent default browser behavior if user drops an image outside' + + ' of image-uploader', + () => { + let mockWindow = { + addEventListener: function (eventname: string, callback: () => {}) { + document.addEventListener('mock' + eventname, callback); + }, + }; + spyOnProperty(windowRef, 'nativeWindow', 'get').and.returnValue( + mockWindow as Window + ); + + spyOn(dropEvent, 'preventDefault'); + spyOn(dragoverEvent, 'preventDefault'); + + spyOn(contextService, 'getEntityType').and.returnValue('exploration'); + component.ngAfterViewInit(); - document.dispatchEvent(dropEvent); - expect(dropEvent.preventDefault).toHaveBeenCalled(); + document.dispatchEvent(dropEvent); + expect(dropEvent.preventDefault).toHaveBeenCalled(); - document.dispatchEvent(dragoverEvent); - expect(dragoverEvent.preventDefault).toHaveBeenCalled(); - }); + document.dispatchEvent(dragoverEvent); + expect(dragoverEvent.preventDefault).toHaveBeenCalled(); + } + ); it('should upload a valid image', () => { component.imageInputRef.nativeElement = { - files: [new File(['image'], 'image.jpg', {type: 'image/jpg'})] + files: [new File(['image'], 'image.jpg', {type: 'image/jpg'})], }; component.imageInputRef.nativeElement.value = 'image.jpg'; component.allowedImageFormats = ['jpeg', 'jpg', 'gif', 'png', 'svg']; diff --git a/core/templates/components/forms/custom-forms-directives/image-uploader.component.ts b/core/templates/components/forms/custom-forms-directives/image-uploader.component.ts index 00e45c93d27c..ef3408c04b23 100644 --- a/core/templates/components/forms/custom-forms-directives/image-uploader.component.ts +++ b/core/templates/components/forms/custom-forms-directives/image-uploader.component.ts @@ -16,13 +16,20 @@ * @fileoverview Component for uploading images. */ -import { Component, ElementRef, Input, Output, EventEmitter, ViewChild } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { AppConstants } from 'app.constants'; -import { BlogDashboardPageService } from 'pages/blog-dashboard-page/services/blog-dashboard-page.service'; -import { ContextService } from 'services/context.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { IdGenerationService } from 'services/id-generation.service'; +import { + Component, + ElementRef, + Input, + Output, + EventEmitter, + ViewChild, +} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {AppConstants} from 'app.constants'; +import {BlogDashboardPageService} from 'pages/blog-dashboard-page/services/blog-dashboard-page.service'; +import {ContextService} from 'services/context.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {IdGenerationService} from 'services/id-generation.service'; interface ImageTypeMapping { [key: string]: { @@ -33,7 +40,7 @@ interface ImageTypeMapping { @Component({ selector: 'oppia-image-uploader', - templateUrl: './image-uploader.component.html' + templateUrl: './image-uploader.component.html', }) export class ImageUploaderComponent { @Output() fileChanged: EventEmitter = new EventEmitter(); @@ -54,19 +61,20 @@ export class ImageUploaderComponent { public blogDashboardPageService: BlogDashboardPageService, private idGenerationService: IdGenerationService, private windowRef: WindowRef, - private contextService: ContextService, - ) { } + private contextService: ContextService + ) {} ngOnInit(): void { // We generate a random class name to distinguish this input from // others in the DOM. - this.fileInputClassName = ( - 'image-uploader-file-input' + this.idGenerationService.generateNewId()); + this.fileInputClassName = + 'image-uploader-file-input' + this.idGenerationService.generateNewId(); } ngAfterViewInit(): void { this.dropAreaRef.nativeElement.addEventListener( - 'drop', (event: DragEvent) => { + 'drop', + (event: DragEvent) => { this.onDragEnd(event); if (event.dataTransfer !== null) { let file = event.dataTransfer.files[0]; @@ -76,16 +84,21 @@ export class ImageUploaderComponent { this.fileChanged.emit(file); } } - }); + } + ); - this.dropAreaRef.nativeElement - .addEventListener('dragover', (event: Event) => { + this.dropAreaRef.nativeElement.addEventListener( + 'dragover', + (event: Event) => { event.preventDefault(); this.backgroundWhileUploading = true; - }); + } + ); - this.dropAreaRef.nativeElement - .addEventListener('dragleave', this.onDragEnd.bind(this)); + this.dropAreaRef.nativeElement.addEventListener( + 'dragleave', + this.onDragEnd.bind(this) + ); // If the user accidentally drops an image outside of the image-uploader // we want to prevent the browser from applying normal drag-and-drop @@ -106,8 +119,9 @@ export class ImageUploaderComponent { handleFile(): void { let file: File = this.imageInputRef.nativeElement.files[0]; - let filename: string = this.imageInputRef.nativeElement.value.split( - /(\\|\/)/g).pop(); + let filename: string = this.imageInputRef.nativeElement.value + .split(/(\\|\/)/g) + .pop(); this.errorMessage = this.validateUploadedFile(file, filename); if (!this.errorMessage) { // Only fire this event if validation pass. @@ -144,7 +158,7 @@ export class ImageUploaderComponent { svg: { format: 'image/svg\\+xml', fileExtension: /\.svg$/, - } + }, }; let imageHasInvalidFormat: boolean = true; @@ -152,17 +166,12 @@ export class ImageUploaderComponent { for (let i = 0; i < this.allowedImageFormats.length; i++) { let imageType: string = this.allowedImageFormats[i]; if (!imageTypeMapping.hasOwnProperty(imageType)) { - return ( - imageType + ' is not in the list of allowed image formats.' - ); + return imageType + ' is not in the list of allowed image formats.'; } if (file.type.match(imageTypeMapping[imageType].format)) { imageHasInvalidFormat = false; - if ( - !file.name.match(imageTypeMapping[imageType].fileExtension)) { - return ( - 'This image format does not match the filename extension.' - ); + if (!file.name.match(imageTypeMapping[imageType].fileExtension)) { + return 'This image format does not match the filename extension.'; } } } @@ -186,14 +195,21 @@ export class ImageUploaderComponent { } if (file.size > maxAllowedFileSize) { let currentSize: string = ( - (file.size * 100 / maxAllowedFileSize).toFixed(1) + (file.size * 100) / + maxAllowedFileSize + ).toFixed(1); + return ( + `The maximum allowed file size is ${maxAllowedFileSize / 1024}` + + ` KB (${currentSize} ${fileSizeUnit} given).` ); - return `The maximum allowed file size is ${maxAllowedFileSize / 1024}` + - ` KB (${currentSize} ${fileSizeUnit} given).`; } return null; } } -angular.module('oppia').directive('oppiaImageUploader', - downgradeComponent({ component: ImageUploaderComponent })); +angular + .module('oppia') + .directive( + 'oppiaImageUploader', + downgradeComponent({component: ImageUploaderComponent}) + ); diff --git a/core/templates/components/forms/custom-forms-directives/object-editor.directive.ts b/core/templates/components/forms/custom-forms-directives/object-editor.directive.ts index adaf3b3a0f95..71760fcc52dd 100644 --- a/core/templates/components/forms/custom-forms-directives/object-editor.directive.ts +++ b/core/templates/components/forms/custom-forms-directives/object-editor.directive.ts @@ -26,55 +26,75 @@ * function. */ -import { AfterViewInit, Component, ComponentFactoryResolver, EventEmitter, forwardRef, Input, OnChanges, OnDestroy, Output, SimpleChange, SimpleChanges, ViewContainerRef } from '@angular/core'; -import { AbstractControl, ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator } from '@angular/forms'; -import { AlgebraicExpressionEditorComponent } from 'objects/templates/algebraic-expression-editor.component'; -import { BooleanEditorComponent } from 'objects/templates/boolean-editor.component'; -import { CodeStringEditorComponent } from 'objects/templates/code-string-editor.component'; -import { CoordTwoDimEditorComponent } from 'objects/templates/coord-two-dim-editor.component'; -import { AllowedVariablesEditorComponent } from 'objects/templates/allowed-variables-editor.component'; -import { DragAndDropPositiveIntEditorComponent } from 'objects/templates/drag-and-drop-positive-int-editor.component'; -import { FilepathEditorComponent } from 'objects/templates/filepath-editor.component'; -import { FractionEditorComponent } from 'objects/templates/fraction-editor.component'; -import { GraphEditorComponent } from 'objects/templates/graph-editor.component'; -import { HtmlEditorComponent } from 'objects/templates/html-editor.component'; -import { ImageWithRegionsEditorComponent } from 'objects/templates/image-with-regions-editor.component'; -import { ListOfSetsOfTranslatableHtmlContentIdsEditorComponent } from 'objects/templates/list-of-sets-of-translatable-html-content-ids-editor.component'; -import { ListOfTabsEditorComponent } from 'objects/templates/list-of-tabs-editor.component'; -import { ListOfUnicodeStringEditorComponent } from 'objects/templates/list-of-unicode-string-editor.component'; -import { MathEquationEditorComponent } from 'objects/templates/math-equation-editor.component'; -import { MathExpressionContentEditorComponent } from 'objects/templates/math-expression-content-editor.component'; -import { MusicPhraseEditorComponent } from 'objects/templates/music-phrase-editor.component'; -import { NonnegativeIntEditorComponent } from 'objects/templates/nonnegative-int-editor.component'; -import { NormalizedStringEditorComponent } from 'objects/templates/normalized-string-editor.component'; -import { NumberWithUnitsEditorComponent } from 'objects/templates/number-with-units-editor.component'; -import { NumericExpressionEditorComponent } from 'objects/templates/numeric-expression-editor.component'; -import { ParameterNameEditorComponent } from 'objects/templates/parameter-name-editor.component'; -import { PositionOfTermsEditorComponent } from 'objects/templates/position-of-terms-editor.component'; -import { PositiveIntEditorComponent } from 'objects/templates/positive-int-editor.component'; -import { RatioExpressionEditorComponent } from 'objects/templates/ratio-expression-editor.component'; -import { RealEditorComponent } from 'objects/templates/real-editor.component'; -import { SanitizedUrlEditorComponent } from 'objects/templates/sanitized-url-editor.component'; -import { SetOfAlgebraicIdentifierEditorComponent } from 'objects/templates/set-of-algebraic-identifier-editor.component'; -import { SetOfTranslatableHtmlContentIdsEditorComponent } from 'objects/templates/set-of-translatable-html-content-ids-editor.component'; -import { SetOfUnicodeStringEditorComponent } from 'objects/templates/set-of-unicode-string-editor.component'; -import { SkillSelectorEditorComponent } from 'objects/templates/skill-selector-editor.component'; -import { SubtitledHtmlEditorComponent } from 'objects/templates/subtitled-html-editor.component'; -import { SubtitledUnicodeEditorComponent } from 'objects/templates/subtitled-unicode-editor.component'; -import { SvgEditorComponent } from 'objects/templates/svg-editor.component'; -import { TranslatableHtmlContentIdEditorComponent } from 'objects/templates/translatable-html-content-id.component'; -import { TranslatableSetOfNormalizedStringEditorComponent } from 'objects/templates/translatable-set-of-normalized-string-editor.component'; -import { TranslatableSetOfUnicodeStringEditorComponent } from 'objects/templates/translatable-set-of-unicode-string-editor.component'; -import { UnicodeStringEditorComponent } from 'objects/templates/unicode-string-editor.component'; -import { IntEditorComponent } from 'objects/templates/int-editor.component'; -import { LoggerService } from 'services/contextual/logger.service'; -import { ComponentRef } from '@angular/core'; -import { Subscription } from 'rxjs'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { SchemaDefaultValue } from 'services/schema-default-value.service'; +import { + AfterViewInit, + Component, + ComponentFactoryResolver, + EventEmitter, + forwardRef, + Input, + OnChanges, + OnDestroy, + Output, + SimpleChange, + SimpleChanges, + ViewContainerRef, +} from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, +} from '@angular/forms'; +import {AlgebraicExpressionEditorComponent} from 'objects/templates/algebraic-expression-editor.component'; +import {BooleanEditorComponent} from 'objects/templates/boolean-editor.component'; +import {CodeStringEditorComponent} from 'objects/templates/code-string-editor.component'; +import {CoordTwoDimEditorComponent} from 'objects/templates/coord-two-dim-editor.component'; +import {AllowedVariablesEditorComponent} from 'objects/templates/allowed-variables-editor.component'; +import {DragAndDropPositiveIntEditorComponent} from 'objects/templates/drag-and-drop-positive-int-editor.component'; +import {FilepathEditorComponent} from 'objects/templates/filepath-editor.component'; +import {FractionEditorComponent} from 'objects/templates/fraction-editor.component'; +import {GraphEditorComponent} from 'objects/templates/graph-editor.component'; +import {HtmlEditorComponent} from 'objects/templates/html-editor.component'; +import {ImageWithRegionsEditorComponent} from 'objects/templates/image-with-regions-editor.component'; +import {ListOfSetsOfTranslatableHtmlContentIdsEditorComponent} from 'objects/templates/list-of-sets-of-translatable-html-content-ids-editor.component'; +import {ListOfTabsEditorComponent} from 'objects/templates/list-of-tabs-editor.component'; +import {ListOfUnicodeStringEditorComponent} from 'objects/templates/list-of-unicode-string-editor.component'; +import {MathEquationEditorComponent} from 'objects/templates/math-equation-editor.component'; +import {MathExpressionContentEditorComponent} from 'objects/templates/math-expression-content-editor.component'; +import {MusicPhraseEditorComponent} from 'objects/templates/music-phrase-editor.component'; +import {NonnegativeIntEditorComponent} from 'objects/templates/nonnegative-int-editor.component'; +import {NormalizedStringEditorComponent} from 'objects/templates/normalized-string-editor.component'; +import {NumberWithUnitsEditorComponent} from 'objects/templates/number-with-units-editor.component'; +import {NumericExpressionEditorComponent} from 'objects/templates/numeric-expression-editor.component'; +import {ParameterNameEditorComponent} from 'objects/templates/parameter-name-editor.component'; +import {PositionOfTermsEditorComponent} from 'objects/templates/position-of-terms-editor.component'; +import {PositiveIntEditorComponent} from 'objects/templates/positive-int-editor.component'; +import {RatioExpressionEditorComponent} from 'objects/templates/ratio-expression-editor.component'; +import {RealEditorComponent} from 'objects/templates/real-editor.component'; +import {SanitizedUrlEditorComponent} from 'objects/templates/sanitized-url-editor.component'; +import {SetOfAlgebraicIdentifierEditorComponent} from 'objects/templates/set-of-algebraic-identifier-editor.component'; +import {SetOfTranslatableHtmlContentIdsEditorComponent} from 'objects/templates/set-of-translatable-html-content-ids-editor.component'; +import {SetOfUnicodeStringEditorComponent} from 'objects/templates/set-of-unicode-string-editor.component'; +import {SkillSelectorEditorComponent} from 'objects/templates/skill-selector-editor.component'; +import {SubtitledHtmlEditorComponent} from 'objects/templates/subtitled-html-editor.component'; +import {SubtitledUnicodeEditorComponent} from 'objects/templates/subtitled-unicode-editor.component'; +import {SvgEditorComponent} from 'objects/templates/svg-editor.component'; +import {TranslatableHtmlContentIdEditorComponent} from 'objects/templates/translatable-html-content-id.component'; +import {TranslatableSetOfNormalizedStringEditorComponent} from 'objects/templates/translatable-set-of-normalized-string-editor.component'; +import {TranslatableSetOfUnicodeStringEditorComponent} from 'objects/templates/translatable-set-of-unicode-string-editor.component'; +import {UnicodeStringEditorComponent} from 'objects/templates/unicode-string-editor.component'; +import {IntEditorComponent} from 'objects/templates/int-editor.component'; +import {LoggerService} from 'services/contextual/logger.service'; +import {ComponentRef} from '@angular/core'; +import {Subscription} from 'rxjs'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {SchemaDefaultValue} from 'services/schema-default-value.service'; const EDITORS = { 'algebraic-expression': AlgebraicExpressionEditorComponent, - 'boolean': BooleanEditorComponent, + boolean: BooleanEditorComponent, 'code-string': CodeStringEditorComponent, 'coord-two-dim': CoordTwoDimEditorComponent, 'allowed-variables': AllowedVariablesEditorComponent, @@ -84,7 +104,7 @@ const EDITORS = { graph: GraphEditorComponent, html: HtmlEditorComponent, 'image-with-regions': ImageWithRegionsEditorComponent, - 'int': IntEditorComponent, + int: IntEditorComponent, 'list-of-sets-of-translatable-html-content-ids': ListOfSetsOfTranslatableHtmlContentIdsEditorComponent, 'list-of-tabs': ListOfTabsEditorComponent, @@ -138,18 +158,23 @@ interface ObjectEditor { { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ObjectEditorComponent), - multi: true + multi: true, }, { provide: NG_VALIDATORS, useExisting: forwardRef(() => ObjectEditorComponent), - multi: true - } - ] + multi: true, + }, + ], }) export class ObjectEditorComponent -implements AfterViewInit, OnChanges, OnDestroy, -ControlValueAccessor, Validator { + implements + AfterViewInit, + OnChanges, + OnDestroy, + ControlValueAccessor, + Validator +{ private _value: SchemaDefaultValue; @Input() alwaysEditable: string; @Input() initArgs: SchemaDefaultValue; @@ -174,13 +199,9 @@ ControlValueAccessor, Validator { this.valueChange.emit(this._value); if (this.componentRef.instance.ngOnChanges) { const componentInputPropsChangeObject: SimpleChanges = { - value: new SimpleChange( - previousValue, - val, - false), + value: new SimpleChange(previousValue, val, false), }; - this.componentRef.instance.ngOnChanges( - componentInputPropsChangeObject); + this.componentRef.instance.ngOnChanges(componentInputPropsChangeObject); } } } @@ -192,7 +213,6 @@ ControlValueAccessor, Validator { onTouch: () => void; onValidatorChange: () => void = () => {}; - // A hashmap is used instead of an array for faster lookup. componentErrors: Record = {}; @@ -200,7 +220,6 @@ ControlValueAccessor, Validator { return this.componentErrors; } - registerOnTouched(fn: () => void): void { this.onTouch = fn; } @@ -224,33 +243,36 @@ ControlValueAccessor, Validator { private loggerService: LoggerService, private componentFactoryResolver: ComponentFactoryResolver, private viewContainerRef: ViewContainerRef - ) { } + ) {} ngAfterViewInit(): void { - const editorName = this.objType.replace( - /([a-z])([A-Z])/g, '$1-$2').toLowerCase(); - if (editorName === ( - 'list-of-sets-of-translatable-html-content-ids' - ) && !this.initArgs + const editorName = this.objType + .replace(/([a-z])([A-Z])/g, '$1-$2') + .toLowerCase(); + if ( + editorName === 'list-of-sets-of-translatable-html-content-ids' && + !this.initArgs ) { throw new Error('\nProvided initArgs: ' + this.initArgs); } if (EDITORS.hasOwnProperty(editorName)) { - if (editorName === ( - 'list-of-sets-of-translatable-html-content-ids' - ) && !this.initArgs) { + if ( + editorName === 'list-of-sets-of-translatable-html-content-ids' && + !this.initArgs + ) { throw new Error('\nProvided initArgs: ' + this.initArgs); } - const componentFactory = ( + const componentFactory = this.componentFactoryResolver.resolveComponentFactory( - EDITORS[editorName]) - ); + EDITORS[editorName] + ); this.viewContainerRef.clear(); // Unknown is type is used because it is default property of // createComponent. This is used to access the instance of the // component created. The type of the instance is not known. const componentRef = this.viewContainerRef.createComponent( - componentFactory) as ComponentRef; + componentFactory + ) as ComponentRef; componentRef.instance.alwaysEditable = this.alwaysEditable; componentRef.instance.initArgs = this.initArgs; @@ -269,7 +291,7 @@ ControlValueAccessor, Validator { // Listening to @Output events (valueChanged and validityChange). if (componentRef.instance.valueChanged) { this.componentSubscriptions.add( - componentRef.instance.valueChanged.subscribe((newValue) => { + componentRef.instance.valueChanged.subscribe(newValue => { // Changes to array are not caught if the array reference doesn't // change. This is a hack for change detection. if (Array.isArray(newValue)) { @@ -282,7 +304,7 @@ ControlValueAccessor, Validator { } if (componentRef.instance.validityChange) { this.componentSubscriptions.add( - componentRef.instance.validityChange.subscribe((errorsMap) => { + componentRef.instance.validityChange.subscribe(errorsMap => { for (const errorKey of Object.keys(errorsMap)) { // Errors map contains true for a key if valid state and false // for an error state. We remove the key from componentErrors @@ -314,9 +336,9 @@ ControlValueAccessor, Validator { } validate(control: AbstractControl): ValidationErrors | null { - return Object.keys( - this.componentErrors - ).length > 0 ? this.componentErrors : null; + return Object.keys(this.componentErrors).length > 0 + ? this.componentErrors + : null; } ngOnChanges(changes: SimpleChanges): void { @@ -330,6 +352,9 @@ ControlValueAccessor, Validator { } } -angular.module('oppia').directive('objectEditor', downgradeComponent({ - component: ObjectEditorComponent -})); +angular.module('oppia').directive( + 'objectEditor', + downgradeComponent({ + component: ObjectEditorComponent, + }) +); diff --git a/core/templates/components/forms/custom-forms-directives/thumbnail-display.component.spec.ts b/core/templates/components/forms/custom-forms-directives/thumbnail-display.component.spec.ts index 0fe8ab7985a5..6c00180aa980 100644 --- a/core/templates/components/forms/custom-forms-directives/thumbnail-display.component.spec.ts +++ b/core/templates/components/forms/custom-forms-directives/thumbnail-display.component.spec.ts @@ -15,11 +15,16 @@ * @fileoverview Unit tests for Thumbnail Display component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { async, ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing'; -import { SvgSanitizerService } from 'services/svg-sanitizer.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import { + async, + ComponentFixture, + fakeAsync, + TestBed, +} from '@angular/core/testing'; +import {SvgSanitizerService} from 'services/svg-sanitizer.service'; -import { ThumbnailDisplayComponent } from './thumbnail-display.component'; +import {ThumbnailDisplayComponent} from './thumbnail-display.component'; describe('Thumbnail Component', () => { let component: ThumbnailDisplayComponent; @@ -38,12 +43,11 @@ describe('Thumbnail Component', () => { * */ - const safeSvg = ( + const safeSvg = 'data:image/svg+xml;base64,PHN2ZyBpZD0ic291cmNlIiB2ZXJzaW9uPSIxLjEiIGJhc2' + 'VQcm9maWxlPSJmdWxsIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogID' + 'xwb2x5Z29uIGlkPSJ0cmlhbmdsZSIgcG9pbnRzPSIwLDAgMCw1MCA1MCwwIiBmaWxsPSIjMD' + - 'A5OTAwIiBzdHJva2U9IiMwMDQ0MDAiPjwvcG9seWdvbj4KPC9zdmc+' - ); + 'A5OTAwIiBzdHJva2U9IiMwMDQ0MDAiPjwvcG9seWdvbj4KPC9zdmc+'; /** * Malicious SVG Decoded @@ -55,29 +59,25 @@ describe('Thumbnail Component', () => { * * */ - const maliciousSvg = ( + const maliciousSvg = 'data:image/svg+xml;base64,PHN2ZyBpZD0ic291cmNlIiB2ZXJzaW9uPSIxLjEiIGJhc2' + 'VQcm9maWxlPSJmdWxsIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogID' + 'xwb2x5Z29uIGlkPSJ0cmlhbmdsZSIgcG9pbnRzPSIwLDAgMCw1MCA1MCwwIiBmaWxsPSIjMD' + 'A5OTAwIiBzdHJva2U9IiMwMDQ0MDAiPjwvcG9seWdvbj4KICA8c2NyaXB0IHR5cGU9InRleH' + 'QvamF2YXNjcmlwdCI+CiAgICBhbGVydCgnVGhpcyBhcHAgaXMgcHJvYmFibHkgdnVsbmVyYW' + - 'JsZSB0byBYU1MgYXR0YWNrcyEnKTsKICA8L3NjcmlwdD4KPC9zdmc+' - ); - + 'JsZSB0byBYU1MgYXR0YWNrcyEnKTsKICA8L3NjcmlwdD4KPC9zdmc+'; const invalidBase64data = 'data:image/svg+xml;base64,This is invalid %3D'; beforeEach(async(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - ThumbnailDisplayComponent - ], + declarations: [ThumbnailDisplayComponent], providers: [ { provide: SvgSanitizerService, - useClass: MockSvgSanitizerService - } - ] + useClass: MockSvgSanitizerService, + }, + ], }).compileComponents(); fixture = TestBed.createComponent(ThumbnailDisplayComponent); @@ -85,9 +85,8 @@ describe('Thumbnail Component', () => { svgSanitizerService = TestBed.get(SvgSanitizerService); })); - it('should not render malicious SVG\'s on Init', fakeAsync(() => { - const sanitizerSpy = spyOn( - svgSanitizerService, 'getTrustedSvgResourceUrl'); + it("should not render malicious SVG's on Init", fakeAsync(() => { + const sanitizerSpy = spyOn(svgSanitizerService, 'getTrustedSvgResourceUrl'); sanitizerSpy.and.returnValue(null); component.imgSrc = maliciousSvg; component.ngOnInit(); @@ -98,9 +97,8 @@ describe('Thumbnail Component', () => { expect(component.imageSourceInView).toBe(safeSvg); })); - it('should not render malicious SVG\'s on value change', fakeAsync(() => { - const sanitizerSpy = spyOn( - svgSanitizerService, 'getTrustedSvgResourceUrl'); + it("should not render malicious SVG's on value change", fakeAsync(() => { + const sanitizerSpy = spyOn(svgSanitizerService, 'getTrustedSvgResourceUrl'); sanitizerSpy.and.returnValue(null); component.imgSrc = maliciousSvg; component.ngOnChanges(); @@ -112,8 +110,7 @@ describe('Thumbnail Component', () => { })); it('should not try to render invalid base64 images', fakeAsync(() => { - const sanitizerSpy = spyOn( - svgSanitizerService, 'getTrustedSvgResourceUrl'); + const sanitizerSpy = spyOn(svgSanitizerService, 'getTrustedSvgResourceUrl'); sanitizerSpy.and.returnValue(null); component.imgSrc = invalidBase64data; component.ngOnChanges(); diff --git a/core/templates/components/forms/custom-forms-directives/thumbnail-display.component.ts b/core/templates/components/forms/custom-forms-directives/thumbnail-display.component.ts index da4b3330face..a82cd0f01012 100644 --- a/core/templates/components/forms/custom-forms-directives/thumbnail-display.component.ts +++ b/core/templates/components/forms/custom-forms-directives/thumbnail-display.component.ts @@ -16,16 +16,16 @@ * @fileoverview Component for thumbnail display. */ -import { Component, Input, OnChanges, OnInit } from '@angular/core'; -import { SafeResourceUrl } from '@angular/platform-browser'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component, Input, OnChanges, OnInit} from '@angular/core'; +import {SafeResourceUrl} from '@angular/platform-browser'; +import {downgradeComponent} from '@angular/upgrade/static'; -import { SvgSanitizerService } from 'services/svg-sanitizer.service'; +import {SvgSanitizerService} from 'services/svg-sanitizer.service'; @Component({ selector: 'oppia-thumbnail-display', templateUrl: './thumbnail-display.component.html', - styleUrls: [] + styleUrls: [], }) export class ThumbnailDisplayComponent implements OnInit, OnChanges { // These properties are initialized using Angular lifecycle hooks @@ -55,7 +55,8 @@ export class ThumbnailDisplayComponent implements OnInit, OnChanges { // If the SVG image is passed as base64 data. if (this.imgSrc.indexOf('data:image/svg+xml;base64') === 0) { const safeResourceUrl = this.svgSanitizerService.getTrustedSvgResourceUrl( - this.imgSrc); + this.imgSrc + ); if (safeResourceUrl !== null) { this.imageSourceInView = safeResourceUrl; } @@ -75,6 +76,9 @@ export class ThumbnailDisplayComponent implements OnInit, OnChanges { } } -angular.module('oppia').directive( - 'oppiaThumbnailDisplay', downgradeComponent( - {component: ThumbnailDisplayComponent})); +angular + .module('oppia') + .directive( + 'oppiaThumbnailDisplay', + downgradeComponent({component: ThumbnailDisplayComponent}) + ); diff --git a/core/templates/components/forms/custom-forms-directives/thumbnail-uploader.component.spec.ts b/core/templates/components/forms/custom-forms-directives/thumbnail-uploader.component.spec.ts index d1f13a287076..825bb5f68d3c 100644 --- a/core/templates/components/forms/custom-forms-directives/thumbnail-uploader.component.spec.ts +++ b/core/templates/components/forms/custom-forms-directives/thumbnail-uploader.component.spec.ts @@ -15,19 +15,25 @@ * @fileoverview Unit tests for ThumbnailUploaderComponent. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA, SimpleChanges } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { of } from 'rxjs'; -import { AlertsService } from 'services/alerts.service'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; -import { ContextService } from 'services/context.service'; -import { ImageLocalStorageService } from 'services/image-local-storage.service'; -import { ImageUploadHelperService } from 'services/image-upload-helper.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { EditThumbnailModalComponent } from './edit-thumbnail-modal.component'; -import { ThumbnailUploaderComponent } from './thumbnail-uploader.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA, SimpleChanges} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {of} from 'rxjs'; +import {AlertsService} from 'services/alerts.service'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; +import {ContextService} from 'services/context.service'; +import {ImageLocalStorageService} from 'services/image-local-storage.service'; +import {ImageUploadHelperService} from 'services/image-upload-helper.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {EditThumbnailModalComponent} from './edit-thumbnail-modal.component'; +import {ThumbnailUploaderComponent} from './thumbnail-uploader.component'; describe('ThumbnailUploaderComponent', () => { let fixture: ComponentFixture; @@ -45,10 +51,10 @@ describe('ThumbnailUploaderComponent', () => { declarations: [ ThumbnailUploaderComponent, EditThumbnailModalComponent, - MockTranslatePipe + MockTranslatePipe, ], providers: [ImageUploadHelperService], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -72,22 +78,27 @@ describe('ThumbnailUploaderComponent', () => { component.ngOnInit(); - expect(component.editableThumbnailDataUrl) - .toBe('/assetsdevhandler/exploration/expId/assets/thumbnail/thumbnail-1'); - expect(component.uploadedImage) - .toBe('/assetsdevhandler/exploration/expId/assets/thumbnail/thumbnail-1'); + expect(component.editableThumbnailDataUrl).toBe( + '/assetsdevhandler/exploration/expId/assets/thumbnail/thumbnail-1' + ); + expect(component.uploadedImage).toBe( + '/assetsdevhandler/exploration/expId/assets/thumbnail/thumbnail-1' + ); expect(component.thumbnailIsLoading).toBeFalse(); }); - it('should throw error if no image is present for a preview during file' + - ' changed', () => { - spyOn(contextService, 'getEntityType').and.returnValue(undefined); - component.filename = 'thumbnail-1'; + it( + 'should throw error if no image is present for a preview during file' + + ' changed', + () => { + spyOn(contextService, 'getEntityType').and.returnValue(undefined); + component.filename = 'thumbnail-1'; - expect(() => { - component.ngOnInit(); - }).toThrowError('No image present for preview'); - }); + expect(() => { + component.ngOnInit(); + }).toThrowError('No image present for preview'); + } + ); it('should display placeholder image when filename is null', () => { component.filename = ''; @@ -101,326 +112,362 @@ describe('ThumbnailUploaderComponent', () => { expect(component.hidePlaceholder).toBeTrue(); }); - it('should update the thumbnail image when thumbnail filename' + - ' changes', () => { - spyOn(contextService, 'getEntityType').and.returnValue('exploration'); - spyOn(contextService, 'getEntityId').and.returnValue('expId'); - component.filename = 'thumbnail-1'; - let changes: SimpleChanges = { - filename: { - currentValue: 'thumbnail-2', - previousValue: 'thumbnail-1', - firstChange: false, - isFirstChange: () => false - } - }; - component.ngOnInit(); - - expect(component.editableThumbnailDataUrl) - .toBe('/assetsdevhandler/exploration/expId/assets/thumbnail/thumbnail-1'); + it( + 'should update the thumbnail image when thumbnail filename' + ' changes', + () => { + spyOn(contextService, 'getEntityType').and.returnValue('exploration'); + spyOn(contextService, 'getEntityId').and.returnValue('expId'); + component.filename = 'thumbnail-1'; + let changes: SimpleChanges = { + filename: { + currentValue: 'thumbnail-2', + previousValue: 'thumbnail-1', + firstChange: false, + isFirstChange: () => false, + }, + }; + component.ngOnInit(); - component.ngOnChanges(changes); + expect(component.editableThumbnailDataUrl).toBe( + '/assetsdevhandler/exploration/expId/assets/thumbnail/thumbnail-1' + ); - expect(component.editableThumbnailDataUrl) - .toBe('/assetsdevhandler/exploration/expId/assets/thumbnail/thumbnail-2'); - expect(component.thumbnailIsLoading).toBeFalse(); - }); + component.ngOnChanges(changes); - it('should not update the thumbnail image when new thumbnail is same as' + - ' the old one', () => { - spyOn(contextService, 'getEntityType').and.returnValue('exploration'); - spyOn(contextService, 'getEntityId').and.returnValue('expId'); - spyOn( - imageUploadHelperService, 'getTrustedResourceUrlForThumbnailFilename'); - component.filename = 'thumbnail-1'; - let changes: SimpleChanges = { - filename: { - currentValue: 'thumbnail-1', - previousValue: 'thumbnail-1', - firstChange: true, - isFirstChange: () => true - } - }; - component.thumbnailIsLoading = false; - - component.ngOnChanges(changes); + expect(component.editableThumbnailDataUrl).toBe( + '/assetsdevhandler/exploration/expId/assets/thumbnail/thumbnail-2' + ); + expect(component.thumbnailIsLoading).toBeFalse(); + } + ); + + it( + 'should not update the thumbnail image when new thumbnail is same as' + + ' the old one', + () => { + spyOn(contextService, 'getEntityType').and.returnValue('exploration'); + spyOn(contextService, 'getEntityId').and.returnValue('expId'); + spyOn( + imageUploadHelperService, + 'getTrustedResourceUrlForThumbnailFilename' + ); + component.filename = 'thumbnail-1'; + let changes: SimpleChanges = { + filename: { + currentValue: 'thumbnail-1', + previousValue: 'thumbnail-1', + firstChange: true, + isFirstChange: () => true, + }, + }; + component.thumbnailIsLoading = false; - expect(imageUploadHelperService.getTrustedResourceUrlForThumbnailFilename) - .not.toHaveBeenCalled(); - expect(component.thumbnailIsLoading).toBeFalse(); - }); + component.ngOnChanges(changes); - it('should throw error if no image is present for a preview during file' + - ' changed', () => { - spyOn(contextService, 'getEntityType').and.returnValue(undefined); + expect( + imageUploadHelperService.getTrustedResourceUrlForThumbnailFilename + ).not.toHaveBeenCalled(); + expect(component.thumbnailIsLoading).toBeFalse(); + } + ); - expect(() => { - component.filenameChanges('newFile', 'oldFile'); - }).toThrowError('No image present for preview'); - }); + it( + 'should throw error if no image is present for a preview during file' + + ' changed', + () => { + spyOn(contextService, 'getEntityType').and.returnValue(undefined); - it('should not show edit thumbnail modal if editing thumbnail is' + - ' disabled', () => { - spyOn(contextService, 'getEntityType').and.returnValue('exploration'); - spyOn(contextService, 'getEntityId').and.returnValue('expId'); - component.disabled = true; - spyOn(ngbModal, 'open'); + expect(() => { + component.filenameChanges('newFile', 'oldFile'); + }).toThrowError('No image present for preview'); + } + ); - expect(component.openInUploadMode).toBe(false); + it( + 'should not show edit thumbnail modal if editing thumbnail is' + + ' disabled', + () => { + spyOn(contextService, 'getEntityType').and.returnValue('exploration'); + spyOn(contextService, 'getEntityId').and.returnValue('expId'); + component.disabled = true; + spyOn(ngbModal, 'open'); - component.showEditThumbnailModal(); + expect(component.openInUploadMode).toBe(false); - // Here, openInUpload mode is not set as which means, showEditThumbnailModal - // returned as soon as the first check was executed. - expect(component.openInUploadMode).toBe(false); - expect(ngbModal.open).not.toHaveBeenCalled(); - }); + component.showEditThumbnailModal(); - it('should show edit thumbnail modal when user clicks on edit button and' + - ' post thumbnail to server if local storage is not used and modal is' + - ' opened in upload mode', fakeAsync(() => { - spyOn(contextService, 'getEntityType').and.returnValue('exploration'); - spyOn(contextService, 'getEntityId').and.returnValue('expId'); - class MockNgbModalRef { - result = Promise.resolve({ - dimensions: { - height: 50, - width: 50 - }, - openInUploadMode: true, - newThumbnailDataUrl: 'data:image/png;base64,xyz', - newBgColor: '#newcol' - }); - - componentInstance = { - bgColor: null, - allowedBgColors: null, - aspectRatio: null, - dimensions: null, - previewDescription: null, - previewDescriptionBgColor: null, - previewFooter: null, - previewTitle: null, - uploadedImage: null, - uploadedImageMimeType: null, - tempBgColor: null, - }; + // Here, openInUpload mode is not set as which means, showEditThumbnailModal + // returned as soon as the first check was executed. + expect(component.openInUploadMode).toBe(false); + expect(ngbModal.open).not.toHaveBeenCalled(); } - let ngbModalRef = new MockNgbModalRef(); - - // Set useLocalStorage as false to trigger fetching. - component.useLocalStorage = false; - component.disabled = false; - component.bgColor = '#ff9933'; - let promise = of({ - filename: 'filename' - }); - - spyOn(ngbModal, 'open').and.returnValue( - ngbModalRef as NgbModalRef); - spyOn(imageUploadHelperService, 'generateImageFilename').and.returnValue( - 'image_file_name.svg'); - spyOn(imageUploadHelperService, 'convertImageDataToImageFile') - .and.returnValue(new File([''], 'filename', {type: 'image/jpeg'})); - spyOn(assetsBackendApiService, 'postThumbnailFile') - .and.returnValue(promise); - spyOn(promise.toPromise(), 'then').and.resolveTo({filename: 'filename'}); - - const updateFilenameSpy = spyOn(component.updateFilename, 'emit'); - const updateBgColorSpy = spyOn(component.updateBgColor, 'emit'); - - expect(component.thumbnailIsLoading).toBe(true); - - component.showEditThumbnailModal(); - tick(); - - expect(ngbModal.open).toHaveBeenCalledWith( - EditThumbnailModalComponent, - {backdrop: 'static'}); - expect(component.tempImageName).toBe('image_file_name.svg'); - expect(component.uploadedImage).toBe('data:image/png;base64,xyz'); - expect(component.thumbnailIsLoading).toBe(false); - expect(updateFilenameSpy).toHaveBeenCalledWith('image_file_name.svg'); - expect(updateBgColorSpy).toHaveBeenCalledWith('#newcol'); - })); - - it('should show edit thumbnail modal when user clicks on edit button and' + - ' save background color if not opened in upload mode', fakeAsync(() => { - spyOn(contextService, 'getEntityType').and.returnValue('exploration'); - spyOn(contextService, 'getEntityId').and.returnValue('expId'); - // Modal is not opened in upload mode. - class MockNgbModalRef { - result = Promise.resolve({ - dimensions: { - height: 50, - width: 50 - }, - openInUploadMode: false, - newThumbnailDataUrl: 'data:image/png;base64,xyz', - newBgColor: '#newcol' + ); + + it( + 'should show edit thumbnail modal when user clicks on edit button and' + + ' post thumbnail to server if local storage is not used and modal is' + + ' opened in upload mode', + fakeAsync(() => { + spyOn(contextService, 'getEntityType').and.returnValue('exploration'); + spyOn(contextService, 'getEntityId').and.returnValue('expId'); + class MockNgbModalRef { + result = Promise.resolve({ + dimensions: { + height: 50, + width: 50, + }, + openInUploadMode: true, + newThumbnailDataUrl: 'data:image/png;base64,xyz', + newBgColor: '#newcol', + }); + + componentInstance = { + bgColor: null, + allowedBgColors: null, + aspectRatio: null, + dimensions: null, + previewDescription: null, + previewDescriptionBgColor: null, + previewFooter: null, + previewTitle: null, + uploadedImage: null, + uploadedImageMimeType: null, + tempBgColor: null, + }; + } + let ngbModalRef = new MockNgbModalRef(); + + // Set useLocalStorage as false to trigger fetching. + component.useLocalStorage = false; + component.disabled = false; + component.bgColor = '#ff9933'; + let promise = of({ + filename: 'filename', }); - componentInstance = { - bgColor: null, - allowedBgColors: null, - aspectRatio: null, - dimensions: null, - previewDescription: null, - previewDescriptionBgColor: null, - previewFooter: null, - previewTitle: null, - uploadedImage: null, - uploadedImageMimeType: null, - tempBgColor: null, - }; - } - let ngbModalRef = new MockNgbModalRef(); - - component.useLocalStorage = false; - component.disabled = false; - component.bgColor = '#ff9933'; + spyOn(ngbModal, 'open').and.returnValue(ngbModalRef as NgbModalRef); + spyOn(imageUploadHelperService, 'generateImageFilename').and.returnValue( + 'image_file_name.svg' + ); + spyOn( + imageUploadHelperService, + 'convertImageDataToImageFile' + ).and.returnValue(new File([''], 'filename', {type: 'image/jpeg'})); + spyOn(assetsBackendApiService, 'postThumbnailFile').and.returnValue( + promise + ); + spyOn(promise.toPromise(), 'then').and.resolveTo({filename: 'filename'}); + + const updateFilenameSpy = spyOn(component.updateFilename, 'emit'); + const updateBgColorSpy = spyOn(component.updateBgColor, 'emit'); + + expect(component.thumbnailIsLoading).toBe(true); + + component.showEditThumbnailModal(); + tick(); + + expect(ngbModal.open).toHaveBeenCalledWith(EditThumbnailModalComponent, { + backdrop: 'static', + }); + expect(component.tempImageName).toBe('image_file_name.svg'); + expect(component.uploadedImage).toBe('data:image/png;base64,xyz'); + expect(component.thumbnailIsLoading).toBe(false); + expect(updateFilenameSpy).toHaveBeenCalledWith('image_file_name.svg'); + expect(updateBgColorSpy).toHaveBeenCalledWith('#newcol'); + }) + ); + + it( + 'should show edit thumbnail modal when user clicks on edit button and' + + ' save background color if not opened in upload mode', + fakeAsync(() => { + spyOn(contextService, 'getEntityType').and.returnValue('exploration'); + spyOn(contextService, 'getEntityId').and.returnValue('expId'); + // Modal is not opened in upload mode. + class MockNgbModalRef { + result = Promise.resolve({ + dimensions: { + height: 50, + width: 50, + }, + openInUploadMode: false, + newThumbnailDataUrl: 'data:image/png;base64,xyz', + newBgColor: '#newcol', + }); + + componentInstance = { + bgColor: null, + allowedBgColors: null, + aspectRatio: null, + dimensions: null, + previewDescription: null, + previewDescriptionBgColor: null, + previewFooter: null, + previewTitle: null, + uploadedImage: null, + uploadedImageMimeType: null, + tempBgColor: null, + }; + } + let ngbModalRef = new MockNgbModalRef(); - spyOn(ngbModal, 'open').and.returnValue( - ngbModalRef as NgbModalRef); - spyOn(imageUploadHelperService, 'convertImageDataToImageFile') - .and.returnValue(new File([''], 'filename', {type: 'image/jpeg'})); + component.useLocalStorage = false; + component.disabled = false; + component.bgColor = '#ff9933'; - const updateFilenameSpy = spyOn(component.updateFilename, 'emit'); - const updateBgColorSpy = spyOn(component.updateBgColor, 'emit'); + spyOn(ngbModal, 'open').and.returnValue(ngbModalRef as NgbModalRef); + spyOn( + imageUploadHelperService, + 'convertImageDataToImageFile' + ).and.returnValue(new File([''], 'filename', {type: 'image/jpeg'})); - expect(component.thumbnailIsLoading).toBe(true); + const updateFilenameSpy = spyOn(component.updateFilename, 'emit'); + const updateBgColorSpy = spyOn(component.updateBgColor, 'emit'); - component.showEditThumbnailModal(); - tick(); + expect(component.thumbnailIsLoading).toBe(true); - expect(ngbModal.open).toHaveBeenCalledWith( - EditThumbnailModalComponent, - {backdrop: 'static'}); - expect(component.thumbnailIsLoading).toBe(false); - expect(updateFilenameSpy).not.toHaveBeenCalled(); - expect(updateBgColorSpy).toHaveBeenCalledWith('#newcol'); - })); + component.showEditThumbnailModal(); + tick(); - it('should show edit thumbnail modal when user clicks on edit button and' + - ' save uploaded thumbnail to local storage if local storage' + - ' is used', fakeAsync(() => { - spyOn(contextService, 'getEntityType').and.returnValue('exploration'); - spyOn(contextService, 'getEntityId').and.returnValue('expId'); - class MockNgbModalRef { - result = Promise.resolve({ - dimensions: { - height: 50, - width: 50 - }, - openInUploadMode: false, - newThumbnailDataUrl: 'data:image/png;base64,xyz', - newBgColor: '#newcol' + expect(ngbModal.open).toHaveBeenCalledWith(EditThumbnailModalComponent, { + backdrop: 'static', }); + expect(component.thumbnailIsLoading).toBe(false); + expect(updateFilenameSpy).not.toHaveBeenCalled(); + expect(updateBgColorSpy).toHaveBeenCalledWith('#newcol'); + }) + ); + + it( + 'should show edit thumbnail modal when user clicks on edit button and' + + ' save uploaded thumbnail to local storage if local storage' + + ' is used', + fakeAsync(() => { + spyOn(contextService, 'getEntityType').and.returnValue('exploration'); + spyOn(contextService, 'getEntityId').and.returnValue('expId'); + class MockNgbModalRef { + result = Promise.resolve({ + dimensions: { + height: 50, + width: 50, + }, + openInUploadMode: false, + newThumbnailDataUrl: 'data:image/png;base64,xyz', + newBgColor: '#newcol', + }); + + componentInstance = { + bgColor: null, + allowedBgColors: null, + aspectRatio: null, + dimensions: null, + previewDescription: null, + previewDescriptionBgColor: null, + previewFooter: null, + previewTitle: null, + uploadedImage: null, + uploadedImageMimeType: null, + tempBgColor: null, + }; + } + let ngbModalRef = new MockNgbModalRef(); - componentInstance = { - bgColor: null, - allowedBgColors: null, - aspectRatio: null, - dimensions: null, - previewDescription: null, - previewDescriptionBgColor: null, - previewFooter: null, - previewTitle: null, - uploadedImage: null, - uploadedImageMimeType: null, - tempBgColor: null, - }; - } - let ngbModalRef = new MockNgbModalRef(); - - component.useLocalStorage = true; - component.disabled = false; - component.allowedBgColors = ['#ff9933']; - - const imageSaveSpy = spyOn(component.imageSave, 'emit'); + component.useLocalStorage = true; + component.disabled = false; + component.allowedBgColors = ['#ff9933']; - spyOn(ngbModal, 'open').and.returnValue( - ngbModalRef as NgbModalRef); - spyOn(imageUploadHelperService, 'generateImageFilename').and.returnValue( - 'image_file_name.svg'); - spyOn(imageLocalStorageService, 'saveImage'); - spyOn(imageLocalStorageService, 'setThumbnailBgColor'); + const imageSaveSpy = spyOn(component.imageSave, 'emit'); - expect(component.thumbnailIsLoading).toBe(true); + spyOn(ngbModal, 'open').and.returnValue(ngbModalRef as NgbModalRef); + spyOn(imageUploadHelperService, 'generateImageFilename').and.returnValue( + 'image_file_name.svg' + ); + spyOn(imageLocalStorageService, 'saveImage'); + spyOn(imageLocalStorageService, 'setThumbnailBgColor'); - component.showEditThumbnailModal(); - tick(); + expect(component.thumbnailIsLoading).toBe(true); - expect(ngbModal.open).toHaveBeenCalledWith( - EditThumbnailModalComponent, - {backdrop: 'static'} - ); - expect(component.thumbnailIsLoading).toBe(false); - expect(imageLocalStorageService.saveImage).toHaveBeenCalledWith( - 'image_file_name.svg', 'data:image/png;base64,xyz'); - expect(imageLocalStorageService.setThumbnailBgColor).toHaveBeenCalledWith( - '#newcol'); - expect(imageSaveSpy).toHaveBeenCalled(); - })); + component.showEditThumbnailModal(); + tick(); - it('should close edit thumbnail modal when cancel button' + - ' is clicked', fakeAsync(() => { - spyOn(contextService, 'getEntityType').and.returnValue('exploration'); - spyOn(contextService, 'getEntityId').and.returnValue('expId'); - class MockNgbModalRef { - componentInstance = { - bgColor: null, - allowedBgColors: null, - aspectRatio: null, - dimensions: null, - previewDescription: null, - previewDescriptionBgColor: null, - previewFooter: null, - previewTitle: null, - uploadedImage: null, - uploadedImageMimeType: null, - tempBgColor: null, - }; - } - let ngbModalRef = new MockNgbModalRef(); + expect(ngbModal.open).toHaveBeenCalledWith(EditThumbnailModalComponent, { + backdrop: 'static', + }); + expect(component.thumbnailIsLoading).toBe(false); + expect(imageLocalStorageService.saveImage).toHaveBeenCalledWith( + 'image_file_name.svg', + 'data:image/png;base64,xyz' + ); + expect(imageLocalStorageService.setThumbnailBgColor).toHaveBeenCalledWith( + '#newcol' + ); + expect(imageSaveSpy).toHaveBeenCalled(); + }) + ); + + it( + 'should close edit thumbnail modal when cancel button' + ' is clicked', + fakeAsync(() => { + spyOn(contextService, 'getEntityType').and.returnValue('exploration'); + spyOn(contextService, 'getEntityId').and.returnValue('expId'); + class MockNgbModalRef { + componentInstance = { + bgColor: null, + allowedBgColors: null, + aspectRatio: null, + dimensions: null, + previewDescription: null, + previewDescriptionBgColor: null, + previewFooter: null, + previewTitle: null, + uploadedImage: null, + uploadedImageMimeType: null, + tempBgColor: null, + }; + } + let ngbModalRef = new MockNgbModalRef(); - component.disabled = false; - component.allowedBgColors = ['#ff9933']; + component.disabled = false; + component.allowedBgColors = ['#ff9933']; - spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ - componentInstance: ngbModalRef, - result: Promise.reject('cancel') - } as NgbModalRef); - }); + spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + componentInstance: ngbModalRef, + result: Promise.reject('cancel'), + } as NgbModalRef; + }); - component.showEditThumbnailModal(); + component.showEditThumbnailModal(); - expect(ngbModal.open).toHaveBeenCalledWith( - EditThumbnailModalComponent, - {backdrop: 'static'} - ); - })); + expect(ngbModal.open).toHaveBeenCalledWith(EditThumbnailModalComponent, { + backdrop: 'static', + }); + }) + ); it('should raise an alert if an empty file is uploaded', () => { spyOn(contextService, 'getEntityType').and.returnValue('exploration'); spyOn(contextService, 'getEntityId').and.returnValue('expId'); spyOn(alertsService, 'addWarning'); - spyOn(imageUploadHelperService, 'convertImageDataToImageFile') - .and.returnValue(null); + spyOn( + imageUploadHelperService, + 'convertImageDataToImageFile' + ).and.returnValue(null); component.saveThumbnailImageData('imageUrl', () => {}); expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Could not get resampled file.'); + 'Could not get resampled file.' + ); }); it('should throw error if no image is present for a preview', () => { spyOn(contextService, 'getEntityType').and.returnValue(undefined); // This is just a mock base 64 in order to test the FileReader event. let dataBase64Mock = 'VEhJUyBJUyBUSEUgQU5TV0VSCg=='; - const arrayBuffer = Uint8Array.from( - window.atob(dataBase64Mock), c => c.charCodeAt(0)); + const arrayBuffer = Uint8Array.from(window.atob(dataBase64Mock), c => + c.charCodeAt(0) + ); let file = new File([arrayBuffer], 'filename.mp3'); expect(() => { diff --git a/core/templates/components/forms/custom-forms-directives/thumbnail-uploader.component.ts b/core/templates/components/forms/custom-forms-directives/thumbnail-uploader.component.ts index 8eb44d9fa9cb..97c327dfd11c 100644 --- a/core/templates/components/forms/custom-forms-directives/thumbnail-uploader.component.ts +++ b/core/templates/components/forms/custom-forms-directives/thumbnail-uploader.component.ts @@ -16,22 +16,22 @@ * @fileoverview Component for uploading images. */ -import { EventEmitter, OnInit, Output } from '@angular/core'; -import { OnChanges, SimpleChanges } from '@angular/core'; -import { Component, Input } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { AlertsService } from 'services/alerts.service'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; -import { ContextService } from 'services/context.service'; -import { ImageLocalStorageService } from 'services/image-local-storage.service'; -import { ImageUploadHelperService } from 'services/image-upload-helper.service'; -import { EditThumbnailModalComponent } from './edit-thumbnail-modal.component'; +import {EventEmitter, OnInit, Output} from '@angular/core'; +import {OnChanges, SimpleChanges} from '@angular/core'; +import {Component, Input} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {AlertsService} from 'services/alerts.service'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; +import {ContextService} from 'services/context.service'; +import {ImageLocalStorageService} from 'services/image-local-storage.service'; +import {ImageUploadHelperService} from 'services/image-upload-helper.service'; +import {EditThumbnailModalComponent} from './edit-thumbnail-modal.component'; @Component({ selector: 'oppia-thumbnail-uploader', - templateUrl: './thumbnail-uploader.component.html' + templateUrl: './thumbnail-uploader.component.html', }) export class ThumbnailUploaderComponent implements OnInit, OnChanges { @Output() updateBgColor: EventEmitter = new EventEmitter(); @@ -54,7 +54,7 @@ export class ThumbnailUploaderComponent implements OnInit, OnChanges { tempImageName!: string; uploadedImage!: string; uploadedImageMimeType!: string; - dimensions!: { height: number; width: number }; + dimensions!: {height: number; width: number}; // Set resampled file returned to null when blob is not of type // 'image', blob size is zero or dataURI is null. resampledFile!: Blob | null; @@ -67,14 +67,14 @@ export class ThumbnailUploaderComponent implements OnInit, OnChanges { openInUploadMode: boolean = false; hidePlaceholder: boolean = true; thumbnailIsLoading: boolean = true; - placeholderImageUrl: string = ( - this.urlInterpolationService.getStaticImageUrl( - '/icons/story-image-icon.png')); + placeholderImageUrl: string = this.urlInterpolationService.getStaticImageUrl( + '/icons/story-image-icon.png' + ); - placeholderImageDataUrl: string = ( + placeholderImageDataUrl: string = this.urlInterpolationService.getStaticImageUrl( - '/icons/story-image-icon.png')); - + '/icons/story-image-icon.png' + ); constructor( private imageUploadHelperService: ImageUploadHelperService, @@ -87,19 +87,22 @@ export class ThumbnailUploaderComponent implements OnInit, OnChanges { ) {} ngOnInit(): void { - if (this.filename !== null && - this.filename !== undefined && - this.filename !== '') { + if ( + this.filename !== null && + this.filename !== undefined && + this.filename !== '' + ) { this.hidePlaceholder = false; let entityType = this.contextService.getEntityType(); if (entityType === undefined) { throw new Error('No image present for preview'); } - this.editableThumbnailDataUrl = ( + this.editableThumbnailDataUrl = this.imageUploadHelperService.getTrustedResourceUrlForThumbnailFilename( this.filename, entityType, - this.contextService.getEntityId())); + this.contextService.getEntityId() + ); this.uploadedImage = this.editableThumbnailDataUrl; this.thumbnailIsLoading = false; } @@ -117,7 +120,8 @@ export class ThumbnailUploaderComponent implements OnInit, OnChanges { ngOnChanges(changes: SimpleChanges): void { if ( changes.filename && - changes.filename.currentValue !== changes.filename.previousValue) { + changes.filename.currentValue !== changes.filename.previousValue + ) { this.thumbnailIsLoading = true; const newValue = changes.filename.currentValue; const previousValue = changes.filename.previousValue; @@ -131,12 +135,12 @@ export class ThumbnailUploaderComponent implements OnInit, OnChanges { if (entityType === undefined) { throw new Error('No image present for preview'); } - this.editableThumbnailDataUrl = ( - this.imageUploadHelperService - .getTrustedResourceUrlForThumbnailFilename( - newFilename, - entityType, - this.contextService.getEntityId())); + this.editableThumbnailDataUrl = + this.imageUploadHelperService.getTrustedResourceUrlForThumbnailFilename( + newFilename, + entityType, + this.contextService.getEntityId() + ); this.uploadedImage = this.editableThumbnailDataUrl; } this.thumbnailIsLoading = false; @@ -152,9 +156,8 @@ export class ThumbnailUploaderComponent implements OnInit, OnChanges { saveThumbnailImageData(imageURI: string, callback: () => void): void { this.resampledFile = null; - this.resampledFile = ( - this.imageUploadHelperService.convertImageDataToImageFile( - imageURI)); + this.resampledFile = + this.imageUploadHelperService.convertImageDataToImageFile(imageURI); this.encodedImageURI = imageURI; if (this.resampledFile === null) { this.alertsService.addWarning('Could not get resampled file.'); @@ -169,16 +172,23 @@ export class ThumbnailUploaderComponent implements OnInit, OnChanges { throw new Error('No image present for preview'); } let entityId = this.contextService.getEntityId(); - const result = this.assetsBackendApiService.postThumbnailFile( - resampledFile, this.tempImageName, entityType, entityId).toPromise(); - result.then((data) => { + const result = this.assetsBackendApiService + .postThumbnailFile( + resampledFile, + this.tempImageName, + entityType, + entityId + ) + .toPromise(); + result.then(data => { let entityType = this.contextService.getEntityType(); if (entityType) { - this.editableThumbnailDataUrl = ( - this.imageUploadHelperService - .getTrustedResourceUrlForThumbnailFilename( - data.filename, entityType, - this.contextService.getEntityId())); + this.editableThumbnailDataUrl = + this.imageUploadHelperService.getTrustedResourceUrlForThumbnailFilename( + data.filename, + entityType, + this.contextService.getEntityId() + ); } callback(); }); @@ -193,72 +203,83 @@ export class ThumbnailUploaderComponent implements OnInit, OnChanges { } // This refers to the temporary thumbnail background // color used for preview. - this.tempBgColor = ( - this.bgColor || - this.allowedBgColors[0]); + this.tempBgColor = this.bgColor || this.allowedBgColors[0]; this.tempImageName = ''; this.uploadedImageMimeType = ''; this.dimensions = { height: 0, - width: 0 + width: 0, }; - const modalRef = this.ngbModal.open( - EditThumbnailModalComponent, - {backdrop: 'static'}); + const modalRef = this.ngbModal.open(EditThumbnailModalComponent, { + backdrop: 'static', + }); modalRef.componentInstance.bgColor = this.tempBgColor; modalRef.componentInstance.allowedBgColors = this.allowedBgColors; modalRef.componentInstance.aspectRatio = this.aspectRatio; modalRef.componentInstance.dimensions = this.dimensions; - modalRef.componentInstance.previewDescription = - this.previewDescription; + modalRef.componentInstance.previewDescription = this.previewDescription; modalRef.componentInstance.previewDescriptionBgColor = - this.previewDescriptionBgColor; + this.previewDescriptionBgColor; modalRef.componentInstance.previewFooter = this.previewFooter; modalRef.componentInstance.previewTitle = this.previewTitle; modalRef.componentInstance.openInUploadMode = this.openInUploadMode; modalRef.componentInstance.uploadedImage = this.uploadedImage; modalRef.componentInstance.uploadedImageMimeType = - this.uploadedImageMimeType; + this.uploadedImageMimeType; modalRef.componentInstance.tempBgColor = this.tempBgColor; - modalRef.result.then((data) => { - this.thumbnailIsLoading = true; - let generatedImageFilename = - this.imageUploadHelperService.generateImageFilename( - data.dimensions.height, data.dimensions.width, 'svg'); - this.newThumbnailDataUrl = data.newThumbnailDataUrl; - this.hidePlaceholder = false; - if (!this.useLocalStorage) { - if (data.openInUploadMode) { - this.tempImageName = ( - this.imageUploadHelperService.generateImageFilename( - data.dimensions.height, data.dimensions.width, 'svg')); - this.saveThumbnailImageData(data.newThumbnailDataUrl, () => { - this.uploadedImage = data.newThumbnailDataUrl; - this.updateFilename.emit(this.tempImageName); + modalRef.result.then( + data => { + this.thumbnailIsLoading = true; + let generatedImageFilename = + this.imageUploadHelperService.generateImageFilename( + data.dimensions.height, + data.dimensions.width, + 'svg' + ); + this.newThumbnailDataUrl = data.newThumbnailDataUrl; + this.hidePlaceholder = false; + if (!this.useLocalStorage) { + if (data.openInUploadMode) { + this.tempImageName = + this.imageUploadHelperService.generateImageFilename( + data.dimensions.height, + data.dimensions.width, + 'svg' + ); + this.saveThumbnailImageData(data.newThumbnailDataUrl, () => { + this.uploadedImage = data.newThumbnailDataUrl; + this.updateFilename.emit(this.tempImageName); + this.saveThumbnailBgColor(data.newBgColor); + this.thumbnailIsLoading = false; + }); + } else { this.saveThumbnailBgColor(data.newBgColor); this.thumbnailIsLoading = false; - }); + } } else { - this.saveThumbnailBgColor(data.newBgColor); this.thumbnailIsLoading = false; + this.imageLocalStorageService.saveImage( + generatedImageFilename, + data.newThumbnailDataUrl + ); + this.localStorageBgcolor = data.newBgColor; + this.imageLocalStorageService.setThumbnailBgColor(data.newBgColor); + this.imageSave.emit(); } - } else { - this.thumbnailIsLoading = false; - this.imageLocalStorageService.saveImage( - generatedImageFilename, data.newThumbnailDataUrl); - this.localStorageBgcolor = data.newBgColor; - this.imageLocalStorageService.setThumbnailBgColor(data.newBgColor); - this.imageSave.emit(); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. } - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + ); } } -angular.module('oppia').directive( - 'oppiaThumbnailUploader', downgradeComponent( - {component: ThumbnailUploaderComponent})); +angular + .module('oppia') + .directive( + 'oppiaThumbnailUploader', + downgradeComponent({component: ThumbnailUploaderComponent}) + ); diff --git a/core/templates/components/forms/forms-templates/mark-audio-as-needing-update-modal.component.spec.ts b/core/templates/components/forms/forms-templates/mark-audio-as-needing-update-modal.component.spec.ts index 32dabb2a44a8..f2d1510560ca 100644 --- a/core/templates/components/forms/forms-templates/mark-audio-as-needing-update-modal.component.spec.ts +++ b/core/templates/components/forms/forms-templates/mark-audio-as-needing-update-modal.component.spec.ts @@ -16,10 +16,10 @@ * @fileoverview Unit Test for Mark Audio As Needing Update Modal Component. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { MarkAudioAsNeedingUpdateModalComponent } from './mark-audio-as-needing-update-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {MarkAudioAsNeedingUpdateModalComponent} from './mark-audio-as-needing-update-modal.component'; class MockActiveModal { close(): void { @@ -37,14 +37,14 @@ describe('Delete Exploration Modal Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - MarkAudioAsNeedingUpdateModalComponent + declarations: [MarkAudioAsNeedingUpdateModalComponent], + providers: [ + { + provide: NgbActiveModal, + useClass: MockActiveModal, + }, ], - providers: [{ - provide: NgbActiveModal, - useClass: MockActiveModal - }], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(MarkAudioAsNeedingUpdateModalComponent); diff --git a/core/templates/components/forms/forms-templates/mark-audio-as-needing-update-modal.component.ts b/core/templates/components/forms/forms-templates/mark-audio-as-needing-update-modal.component.ts index 39a5a7bbf7e4..abf77f5cc6f8 100644 --- a/core/templates/components/forms/forms-templates/mark-audio-as-needing-update-modal.component.ts +++ b/core/templates/components/forms/forms-templates/mark-audio-as-needing-update-modal.component.ts @@ -16,19 +16,16 @@ * @fileoverview Modal for marking audio as needing update. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-mark-audio-as-needing-update-modal', - templateUrl: './mark-audio-as-needing-update-modal.component.html' + templateUrl: './mark-audio-as-needing-update-modal.component.html', }) -export class MarkAudioAsNeedingUpdateModalComponent - extends ConfirmOrCancelModal { - constructor( - private ngbActiveModal: NgbActiveModal, - ) { +export class MarkAudioAsNeedingUpdateModalComponent extends ConfirmOrCancelModal { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } } diff --git a/core/templates/components/forms/forms-templates/mark-translations-as-needing-update-modal.component.spec.ts b/core/templates/components/forms/forms-templates/mark-translations-as-needing-update-modal.component.spec.ts index 501a5b089af1..ab81ffdd87d0 100644 --- a/core/templates/components/forms/forms-templates/mark-translations-as-needing-update-modal.component.spec.ts +++ b/core/templates/components/forms/forms-templates/mark-translations-as-needing-update-modal.component.spec.ts @@ -16,11 +16,11 @@ * @fileoverview Unit Test for Mark Audio As Needing Update Modal Component. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { MarkTranslationsAsNeedingUpdateModalComponent } from './mark-translations-as-needing-update-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {MarkTranslationsAsNeedingUpdateModalComponent} from './mark-translations-as-needing-update-modal.component'; class MockActiveModal { close(): void { @@ -34,25 +34,25 @@ class MockActiveModal { describe('Mark Translations As Needing Update Modal Component', () => { let component: MarkTranslationsAsNeedingUpdateModalComponent; - let fixture: ( - ComponentFixture); + let fixture: ComponentFixture; let ngbActiveModal: NgbActiveModal; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - MarkTranslationsAsNeedingUpdateModalComponent + declarations: [MarkTranslationsAsNeedingUpdateModalComponent], + providers: [ + { + provide: NgbActiveModal, + useClass: MockActiveModal, + }, ], - providers: [{ - provide: NgbActiveModal, - useClass: MockActiveModal - }], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent( - MarkTranslationsAsNeedingUpdateModalComponent); + MarkTranslationsAsNeedingUpdateModalComponent + ); component = fixture.componentInstance; fixture.detectChanges(); diff --git a/core/templates/components/forms/forms-templates/mark-translations-as-needing-update-modal.component.ts b/core/templates/components/forms/forms-templates/mark-translations-as-needing-update-modal.component.ts index 08c3d9bf8e8c..a3af91e4385c 100644 --- a/core/templates/components/forms/forms-templates/mark-translations-as-needing-update-modal.component.ts +++ b/core/templates/components/forms/forms-templates/mark-translations-as-needing-update-modal.component.ts @@ -17,23 +17,20 @@ * changes for the translation. */ -import { Component, Input } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component, Input} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-mark-translations-as-needing-update-modal', - templateUrl: './mark-translations-as-needing-update-modal.component.html' + templateUrl: './mark-translations-as-needing-update-modal.component.html', }) -export class MarkTranslationsAsNeedingUpdateModalComponent - extends ConfirmOrCancelModal { +export class MarkTranslationsAsNeedingUpdateModalComponent extends ConfirmOrCancelModal { @Input() contentId!: string; @Input() markNeedsUpdateHandler!: (contentId: string) => void; @Input() removeHandler!: (contentId: string) => void; - constructor( - private ngbActiveModal: NgbActiveModal - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } diff --git a/core/templates/components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec.ts b/core/templates/components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec.ts index fc281e3297ca..399bd5401239 100644 --- a/core/templates/components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec.ts +++ b/core/templates/components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec.ts @@ -16,41 +16,48 @@ * @fileoverview Integration tests for Schema Based Editors */ -import { DebugElement } from '@angular/core'; -import { ComponentFixture, fakeAsync, flush, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { FormsModule, NgModel, ReactiveFormsModule } from '@angular/forms'; -import { MatInputModule } from '@angular/material/input'; -import { By } from '@angular/platform-browser'; -import { NgbTooltipModule, NgbModalModule } from '@ng-bootstrap/ng-bootstrap'; -import { TranslateService } from '@ngx-translate/core'; -import { DynamicContentModule } from 'components/interaction-display/dynamic-content.module'; -import { OppiaCkEditor4Module } from 'components/ck-editor-helpers/ckeditor4.module'; -import { CodeMirrorModule } from 'components/code-mirror/codemirror.module'; -import { ApplyValidationDirective } from 'components/forms/custom-forms-directives/apply-validation.directive'; -import { CustomFormsComponentsModule } from 'components/forms/custom-forms-directives/custom-form-components.module'; -import { ObjectEditorComponent } from 'components/forms/custom-forms-directives/object-editor.directive'; -import { AudioSliderComponent } from 'components/forms/slider/audio-slider.component'; -import { DirectivesModule } from 'directives/directives.module'; -import { SharedPipesModule } from 'filters/shared-pipes.module'; -import { MaterialModule } from 'modules/material.module'; -import { DictSchema, UnicodeSchema } from 'services/schema-default-value.service'; -import { MockTranslateModule } from 'tests/unit-test-utils'; -import { SchemaBasedBoolEditorComponent } from '../schema-based-bool-editor.component'; -import { SchemaBasedChoicesEditorComponent } from '../schema-based-choices-editor.component'; -import { SchemaBasedCustomEditorComponent } from '../schema-based-custom-editor.component'; -import { SchemaBasedDictEditorComponent } from '../schema-based-dict-editor.component'; -import { SchemaBasedEditorComponent } from '../schema-based-editor.component'; -import { SchemaBasedExpressionEditorComponent } from '../schema-based-expression-editor.component'; -import { SchemaBasedFloatEditorComponent } from '../schema-based-float-editor.component'; -import { SchemaBasedHtmlEditorComponent } from '../schema-based-html-editor.component'; -import { SchemaBasedIntEditorComponent } from '../schema-based-int-editor.component'; -import { SchemaBasedListEditorComponent } from '../schema-based-list-editor.component'; -import { SchemaBasedUnicodeEditor } from '../schema-based-unicode-editor.component'; +import {DebugElement} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + flush, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {FormsModule, NgModel, ReactiveFormsModule} from '@angular/forms'; +import {MatInputModule} from '@angular/material/input'; +import {By} from '@angular/platform-browser'; +import {NgbTooltipModule, NgbModalModule} from '@ng-bootstrap/ng-bootstrap'; +import {TranslateService} from '@ngx-translate/core'; +import {DynamicContentModule} from 'components/interaction-display/dynamic-content.module'; +import {OppiaCkEditor4Module} from 'components/ck-editor-helpers/ckeditor4.module'; +import {CodeMirrorModule} from 'components/code-mirror/codemirror.module'; +import {ApplyValidationDirective} from 'components/forms/custom-forms-directives/apply-validation.directive'; +import {CustomFormsComponentsModule} from 'components/forms/custom-forms-directives/custom-form-components.module'; +import {ObjectEditorComponent} from 'components/forms/custom-forms-directives/object-editor.directive'; +import {AudioSliderComponent} from 'components/forms/slider/audio-slider.component'; +import {DirectivesModule} from 'directives/directives.module'; +import {SharedPipesModule} from 'filters/shared-pipes.module'; +import {MaterialModule} from 'modules/material.module'; +import {DictSchema, UnicodeSchema} from 'services/schema-default-value.service'; +import {MockTranslateModule} from 'tests/unit-test-utils'; +import {SchemaBasedBoolEditorComponent} from '../schema-based-bool-editor.component'; +import {SchemaBasedChoicesEditorComponent} from '../schema-based-choices-editor.component'; +import {SchemaBasedCustomEditorComponent} from '../schema-based-custom-editor.component'; +import {SchemaBasedDictEditorComponent} from '../schema-based-dict-editor.component'; +import {SchemaBasedEditorComponent} from '../schema-based-editor.component'; +import {SchemaBasedExpressionEditorComponent} from '../schema-based-expression-editor.component'; +import {SchemaBasedFloatEditorComponent} from '../schema-based-float-editor.component'; +import {SchemaBasedHtmlEditorComponent} from '../schema-based-html-editor.component'; +import {SchemaBasedIntEditorComponent} from '../schema-based-int-editor.component'; +import {SchemaBasedListEditorComponent} from '../schema-based-list-editor.component'; +import {SchemaBasedUnicodeEditor} from '../schema-based-unicode-editor.component'; // eslint-disable-next-line func-style export function findComponent( fixture: ComponentFixture, - selector: string, + selector: string ): DebugElement { return fixture.debugElement.query(By.css(selector)); } @@ -81,7 +88,7 @@ describe('Schema based editor', () => { NgbModalModule, ReactiveFormsModule, SharedPipesModule, - MockTranslateModule + MockTranslateModule, ], declarations: [ AudioSliderComponent, @@ -97,11 +104,9 @@ describe('Schema based editor', () => { SchemaBasedIntEditorComponent, SchemaBasedListEditorComponent, SchemaBasedUnicodeEditor, - ObjectEditorComponent + ObjectEditorComponent, ], - providers: [ - {provide: TranslateService, useClass: MockTranslateService} - ] + providers: [{provide: TranslateService, useClass: MockTranslateService}], }).compileComponents(); })); @@ -117,20 +122,23 @@ describe('Schema based editor', () => { validators: [ { id: 'hasLengthAtLeast', - minValue: 4 - }, { + minValue: 4, + }, + { id: 'hasLengthAtMost', - maxValue: 10 - } - ] } as UnicodeSchema + maxValue: 10, + }, + ], + } as UnicodeSchema, }, - { name: 'real', schema: { type: 'float' } } - ] + {name: 'real', schema: {type: 'float'}}, + ], }; const schemaBasedEditorFixture = TestBed.createComponent( - SchemaBasedEditorComponent); - const schemaBasedEditorComponent = ( - schemaBasedEditorFixture.componentInstance); + SchemaBasedEditorComponent + ); + const schemaBasedEditorComponent = + schemaBasedEditorFixture.componentInstance; schemaBasedEditorComponent.schema = schema; schemaBasedEditorComponent.localValue = {}; schemaBasedEditorFixture.detectChanges(); @@ -153,24 +161,33 @@ describe('Schema based editor', () => { }; // eslint-disable-next-line max-len - const expectTopLevelComponentValueToBe = (fieldNameValue: string, real: number) => { - const localValue = ( - schemaBasedEditorComponent.localValue - ) as {fieldName: string; real: number}; + const expectTopLevelComponentValueToBe = ( + fieldNameValue: string, + real: number + ) => { + const localValue = schemaBasedEditorComponent.localValue as { + fieldName: string; + real: number; + }; expect(localValue.fieldName).toBe(fieldNameValue); expect(localValue.real).toBe(real); }; // Check that the initial values for the UI fields are populated correctly. const schemaBasedUnicodeEditorInput = findComponent( - schemaBasedEditorFixture, 'schema-based-unicode-editor' + schemaBasedEditorFixture, + 'schema-based-unicode-editor' ).query(By.css('input')).nativeElement; const schemaBasedFloatEditorInput = findComponent( - schemaBasedEditorFixture, 'schema-based-float-editor' + schemaBasedEditorFixture, + 'schema-based-float-editor' ).query(By.css('input')).nativeElement; const unicodeInputFormController = findComponent( - schemaBasedEditorFixture, 'schema-based-unicode-editor' - ).query(By.css('input')).injector.get(NgModel); + schemaBasedEditorFixture, + 'schema-based-unicode-editor' + ) + .query(By.css('input')) + .injector.get(NgModel); expect(schemaBasedUnicodeEditorInput.value).toBe(''); expect(schemaBasedFloatEditorInput.value).toBe(''); diff --git a/core/templates/components/forms/schema-based-editors/schema-based-bool-editor.component.spec.ts b/core/templates/components/forms/schema-based-editors/schema-based-bool-editor.component.spec.ts index 94136a62515e..ada8b99af0ce 100644 --- a/core/templates/components/forms/schema-based-editors/schema-based-bool-editor.component.spec.ts +++ b/core/templates/components/forms/schema-based-editors/schema-based-bool-editor.component.spec.ts @@ -16,10 +16,15 @@ * @fileoverview Unit tests for Schema Based Bool Editor Component */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { FormControl, FormsModule } from '@angular/forms'; -import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing'; -import { SchemaBasedBoolEditorComponent } from './schema-based-bool-editor.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {FormControl, FormsModule} from '@angular/forms'; +import { + ComponentFixture, + fakeAsync, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import {SchemaBasedBoolEditorComponent} from './schema-based-bool-editor.component'; describe('Schema Based Bool Editor Component', () => { let component: SchemaBasedBoolEditorComponent; @@ -28,10 +33,8 @@ describe('Schema Based Bool Editor Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [FormsModule], - declarations: [ - SchemaBasedBoolEditorComponent - ], - schemas: [NO_ERRORS_SCHEMA] + declarations: [SchemaBasedBoolEditorComponent], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -41,7 +44,7 @@ describe('Schema Based Bool Editor Component', () => { }); it('should set component properties on initialization', fakeAsync(() => { - let mockFunction = function(value: boolean) { + let mockFunction = function (value: boolean) { return value; }; component.registerOnChange(mockFunction); @@ -54,12 +57,12 @@ describe('Schema Based Bool Editor Component', () => { })); it('should return errors for invalid value type', () => { - expect( - component.validate(new FormControl(2)) - ).toEqual({invalidType: 'number'}); - expect( - component.validate(new FormControl('true')) - ).toEqual({invalidType: 'string'}); + expect(component.validate(new FormControl(2))).toEqual({ + invalidType: 'number', + }); + expect(component.validate(new FormControl('true'))).toEqual({ + invalidType: 'string', + }); expect(component.validate(new FormControl(false))).toEqual(null); }); @@ -81,15 +84,13 @@ describe('Schema Based Bool Editor Component', () => { expect(component.localValue).toBeFalse(); }); - it( - 'should not update value when local value is the same as the new value', - () => { - component.localValue = true; + it('should not update value when local value is the same as the new value', () => { + component.localValue = true; - expect(component.localValue).toBeTrue(); + expect(component.localValue).toBeTrue(); - component.updateValue(true); + component.updateValue(true); - expect(component.localValue).toBeTrue(); - }); + expect(component.localValue).toBeTrue(); + }); }); diff --git a/core/templates/components/forms/schema-based-editors/schema-based-bool-editor.component.ts b/core/templates/components/forms/schema-based-editors/schema-based-bool-editor.component.ts index 5bf3ea701933..4f54e1dcb460 100644 --- a/core/templates/components/forms/schema-based-editors/schema-based-bool-editor.component.ts +++ b/core/templates/components/forms/schema-based-editors/schema-based-bool-editor.component.ts @@ -16,9 +16,16 @@ * @fileoverview Component for a schema-based editor for booleans. */ -import { Component, forwardRef, Input } from '@angular/core'; -import { NG_VALUE_ACCESSOR, NG_VALIDATORS, ControlValueAccessor, Validator, AbstractControl, ValidationErrors } from '@angular/forms'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component, forwardRef, Input} from '@angular/core'; +import { + NG_VALUE_ACCESSOR, + NG_VALIDATORS, + ControlValueAccessor, + Validator, + AbstractControl, + ValidationErrors, +} from '@angular/forms'; +import {downgradeComponent} from '@angular/upgrade/static'; @Component({ selector: 'schema-based-bool-editor', @@ -27,17 +34,18 @@ import { downgradeComponent } from '@angular/upgrade/static'; { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SchemaBasedBoolEditorComponent), - multi: true + multi: true, }, { provide: NG_VALIDATORS, useExisting: forwardRef(() => SchemaBasedBoolEditorComponent), - multi: true + multi: true, }, - ] + ], }) export class SchemaBasedBoolEditorComponent -implements ControlValueAccessor, Validator { + implements ControlValueAccessor, Validator +{ // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -57,8 +65,7 @@ implements ControlValueAccessor, Validator { } // Implemented as a part of ControlValueAccessor interface. - registerOnTouched(): void { - } + registerOnTouched(): void {} // Implemented as a part of Validator interface. validate(control: AbstractControl): ValidationErrors | null { @@ -77,6 +84,9 @@ implements ControlValueAccessor, Validator { } } -angular.module('oppia').directive('schemaBasedBoolEditor', downgradeComponent({ - component: SchemaBasedBoolEditorComponent -}) as angular.IDirectiveFactory); +angular.module('oppia').directive( + 'schemaBasedBoolEditor', + downgradeComponent({ + component: SchemaBasedBoolEditorComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/components/forms/schema-based-editors/schema-based-choices-editor.component.spec.ts b/core/templates/components/forms/schema-based-editors/schema-based-choices-editor.component.spec.ts index 53324333502b..f3256213923b 100644 --- a/core/templates/components/forms/schema-based-editors/schema-based-choices-editor.component.spec.ts +++ b/core/templates/components/forms/schema-based-editors/schema-based-choices-editor.component.spec.ts @@ -16,9 +16,14 @@ * @fileoverview Unit tests for Schema Based Choices Editor Component */ -import { FormControl, FormsModule } from '@angular/forms'; -import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing'; -import { SchemaBasedChoicesEditorComponent } from './schema-based-choices-editor.component'; +import {FormControl, FormsModule} from '@angular/forms'; +import { + ComponentFixture, + fakeAsync, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import {SchemaBasedChoicesEditorComponent} from './schema-based-choices-editor.component'; describe('Schema Based Choices Editor Component', () => { let component: SchemaBasedChoicesEditorComponent; @@ -27,9 +32,7 @@ describe('Schema Based Choices Editor Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [FormsModule], - declarations: [ - SchemaBasedChoicesEditorComponent - ] + declarations: [SchemaBasedChoicesEditorComponent], }).compileComponents(); })); @@ -41,7 +44,7 @@ describe('Schema Based Choices Editor Component', () => { }); it('should set component properties on initialization', fakeAsync(() => { - let mockFunction = function(value: string) { + let mockFunction = function (value: string) { return value; }; component.registerOnChange(mockFunction); diff --git a/core/templates/components/forms/schema-based-editors/schema-based-choices-editor.component.ts b/core/templates/components/forms/schema-based-editors/schema-based-choices-editor.component.ts index 0896af461434..fd8b03dc25d7 100644 --- a/core/templates/components/forms/schema-based-editors/schema-based-choices-editor.component.ts +++ b/core/templates/components/forms/schema-based-editors/schema-based-choices-editor.component.ts @@ -16,10 +16,17 @@ * @fileoverview Component for a schema-based editor for multiple choice. */ -import { Component, forwardRef, Input, OnInit } from '@angular/core'; -import { NG_VALUE_ACCESSOR, NG_VALIDATORS, ControlValueAccessor, Validator, AbstractControl, ValidationErrors } from '@angular/forms'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { Schema } from 'services/schema-default-value.service'; +import {Component, forwardRef, Input, OnInit} from '@angular/core'; +import { + NG_VALUE_ACCESSOR, + NG_VALIDATORS, + ControlValueAccessor, + Validator, + AbstractControl, + ValidationErrors, +} from '@angular/forms'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {Schema} from 'services/schema-default-value.service'; @Component({ selector: 'schema-based-choices-editor', @@ -28,17 +35,18 @@ import { Schema } from 'services/schema-default-value.service'; { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SchemaBasedChoicesEditorComponent), - multi: true + multi: true, }, { provide: NG_VALIDATORS, useExisting: forwardRef(() => SchemaBasedChoicesEditorComponent), - multi: true + multi: true, }, - ] + ], }) export class SchemaBasedChoicesEditorComponent -implements ControlValueAccessor, OnInit, Validator { + implements ControlValueAccessor, OnInit, Validator +{ // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -61,8 +69,7 @@ implements ControlValueAccessor, OnInit, Validator { } // Implemented as a part of ControlValueAccessor interface. - registerOnTouched(): void { - } + registerOnTouched(): void {} // Implemented as a part of Validator interface. validate(control: AbstractControl): ValidationErrors { @@ -78,12 +85,12 @@ implements ControlValueAccessor, OnInit, Validator { this.onChange(val); } - ngOnInit(): void { } + ngOnInit(): void {} } angular.module('oppia').directive( 'schemaBasedChoicesEditor', downgradeComponent({ - component: SchemaBasedChoicesEditorComponent + component: SchemaBasedChoicesEditorComponent, }) ); diff --git a/core/templates/components/forms/schema-based-editors/schema-based-custom-editor.component.spec.ts b/core/templates/components/forms/schema-based-editors/schema-based-custom-editor.component.spec.ts index 22b66158005f..6fdc06932728 100644 --- a/core/templates/components/forms/schema-based-editors/schema-based-custom-editor.component.spec.ts +++ b/core/templates/components/forms/schema-based-editors/schema-based-custom-editor.component.spec.ts @@ -16,13 +16,34 @@ * @fileoverview Unit tests for schema-based editor component for custom values */ -import { Component, EventEmitter, NO_ERRORS_SCHEMA, forwardRef } from '@angular/core'; -import { ControlValueAccessor, FormControl, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { SchemaBasedCustomEditorComponent } from './schema-based-custom-editor.component'; -import { SchemaDefaultValue } from 'services/schema-default-value.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TranslateFakeLoader, TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; +import { + Component, + EventEmitter, + NO_ERRORS_SCHEMA, + forwardRef, +} from '@angular/core'; +import { + ControlValueAccessor, + FormControl, + FormsModule, + NG_VALUE_ACCESSOR, +} from '@angular/forms'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {SchemaBasedCustomEditorComponent} from './schema-based-custom-editor.component'; +import {SchemaDefaultValue} from 'services/schema-default-value.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import { + TranslateFakeLoader, + TranslateLoader, + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; // Trying to use the actual Object editor component in the tests would require // a lot of mocking to be done (given the size of the component and how many @@ -35,9 +56,9 @@ import { TranslateFakeLoader, TranslateLoader, TranslateModule, TranslateService { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => MockObjectEditorComponent), - multi: true - } - ] + multi: true, + }, + ], }) export class MockObjectEditorComponent implements ControlValueAccessor { writeValue(value: string | number | null): void {} @@ -52,19 +73,22 @@ describe('Schema Based Custom Editor Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [FormsModule, HttpClientTestingModule, + imports: [ + FormsModule, + HttpClientTestingModule, TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: TranslateFakeLoader - } - })], + useClass: TranslateFakeLoader, + }, + }), + ], declarations: [ SchemaBasedCustomEditorComponent, - MockObjectEditorComponent + MockObjectEditorComponent, ], schemas: [NO_ERRORS_SCHEMA], - providers: [TranslateService] + providers: [TranslateService], }).compileComponents(); })); @@ -74,7 +98,7 @@ describe('Schema Based Custom Editor Component', () => { }); it('should set component properties on initialization', fakeAsync(() => { - let mockFunction = function(value: SchemaDefaultValue) { + let mockFunction = function (value: SchemaDefaultValue) { return value; }; @@ -125,33 +149,30 @@ describe('Schema Based Custom Editor Component', () => { expect(component.localValue).toBe('true'); }); - it( - 'should register validator and call it when the form validation changes', - fakeAsync(() => { - component.schema = { obj_type: 'UnicodeString', type: 'custom' }; - fixture.detectChanges(); - const onValidatorChangeSpy = jasmine.createSpy('validator onchange spy'); - - // The statusChanges property in the form used in the component is an - // observable which is triggered by changes to the form state in the - // template. Since we are not doing template-based testing, we need to - // mock the statusChanges property of the form. - let mockFormStatusChangeEmitter = new EventEmitter(); - spyOnProperty( - component.hybridForm, 'statusChanges' - ).and.returnValue( - mockFormStatusChangeEmitter); - component.registerOnValidatorChange(onValidatorChangeSpy); - component.ngAfterViewInit(); - - expect(onValidatorChangeSpy).not.toHaveBeenCalled(); - - component.validate(new FormControl()); - mockFormStatusChangeEmitter.emit(); - // The subscription to statusChanges is asynchronous, so we need to - // tick() to trigger the callback. - tick(); - - expect(onValidatorChangeSpy).toHaveBeenCalled(); - })); + it('should register validator and call it when the form validation changes', fakeAsync(() => { + component.schema = {obj_type: 'UnicodeString', type: 'custom'}; + fixture.detectChanges(); + const onValidatorChangeSpy = jasmine.createSpy('validator onchange spy'); + + // The statusChanges property in the form used in the component is an + // observable which is triggered by changes to the form state in the + // template. Since we are not doing template-based testing, we need to + // mock the statusChanges property of the form. + let mockFormStatusChangeEmitter = new EventEmitter(); + spyOnProperty(component.hybridForm, 'statusChanges').and.returnValue( + mockFormStatusChangeEmitter + ); + component.registerOnValidatorChange(onValidatorChangeSpy); + component.ngAfterViewInit(); + + expect(onValidatorChangeSpy).not.toHaveBeenCalled(); + + component.validate(new FormControl()); + mockFormStatusChangeEmitter.emit(); + // The subscription to statusChanges is asynchronous, so we need to + // tick() to trigger the callback. + tick(); + + expect(onValidatorChangeSpy).toHaveBeenCalled(); + })); }); diff --git a/core/templates/components/forms/schema-based-editors/schema-based-custom-editor.component.ts b/core/templates/components/forms/schema-based-editors/schema-based-custom-editor.component.ts index 8b4a77afb565..38b118d28b57 100644 --- a/core/templates/components/forms/schema-based-editors/schema-based-custom-editor.component.ts +++ b/core/templates/components/forms/schema-based-editors/schema-based-custom-editor.component.ts @@ -16,10 +16,29 @@ * @fileoverview Component for a schema-based editor for custom values. */ -import { AfterViewInit, Component, EventEmitter, forwardRef, Input, Output, ViewChild } from '@angular/core'; -import { AbstractControl, ControlValueAccessor, NgForm, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator } from '@angular/forms'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { CustomSchema, SchemaDefaultValue } from 'services/schema-default-value.service'; +import { + AfterViewInit, + Component, + EventEmitter, + forwardRef, + Input, + Output, + ViewChild, +} from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + NgForm, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, +} from '@angular/forms'; +import {downgradeComponent} from '@angular/upgrade/static'; +import { + CustomSchema, + SchemaDefaultValue, +} from 'services/schema-default-value.service'; @Component({ selector: 'schema-based-custom-editor', @@ -28,17 +47,18 @@ import { CustomSchema, SchemaDefaultValue } from 'services/schema-default-value. { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SchemaBasedCustomEditorComponent), - multi: true + multi: true, }, { provide: NG_VALIDATORS, multi: true, useExisting: forwardRef(() => SchemaBasedCustomEditorComponent), }, - ] + ], }) export class SchemaBasedCustomEditorComponent -implements ControlValueAccessor, Validator, AfterViewInit { + implements ControlValueAccessor, Validator, AfterViewInit +{ // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -80,7 +100,7 @@ implements ControlValueAccessor, Validator, AfterViewInit { // object. However, when we move to reactive forms, that validation should // be moved here instead (see the Todo below). // TODO(#15458): Move template driven validation into code. - return this.hybridForm.valid ? null : { invalid: true }; + return this.hybridForm.valid ? null : {invalid: true}; } updateValue(value: SchemaDefaultValue): void { @@ -114,6 +134,6 @@ implements ControlValueAccessor, Validator, AfterViewInit { angular.module('oppia').directive( 'schemaBasedCustomEditor', downgradeComponent({ - component: SchemaBasedCustomEditorComponent + component: SchemaBasedCustomEditorComponent, }) ); diff --git a/core/templates/components/forms/schema-based-editors/schema-based-dict-editor.component.spec.ts b/core/templates/components/forms/schema-based-editors/schema-based-dict-editor.component.spec.ts index 84e92be1e3d5..84421ad1bc8e 100644 --- a/core/templates/components/forms/schema-based-editors/schema-based-dict-editor.component.spec.ts +++ b/core/templates/components/forms/schema-based-editors/schema-based-dict-editor.component.spec.ts @@ -16,13 +16,21 @@ * @fileoverview Unit tests for Schema Based Dict Editor Component */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing'; -import { FormControl, FormsModule } from '@angular/forms'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { IdGenerationService } from 'services/id-generation.service'; -import { SchemaBasedDictEditorComponent } from './schema-based-dict-editor.component'; -import { Schema, SchemaDefaultValue } from 'services/schema-default-value.service'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import {FormControl, FormsModule} from '@angular/forms'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {IdGenerationService} from 'services/id-generation.service'; +import {SchemaBasedDictEditorComponent} from './schema-based-dict-editor.component'; +import { + Schema, + SchemaDefaultValue, +} from 'services/schema-default-value.service'; describe('Schema Based Dict Editor Component', () => { let component: SchemaBasedDictEditorComponent; @@ -32,14 +40,9 @@ describe('Schema Based Dict Editor Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [FormsModule], - declarations: [ - SchemaBasedDictEditorComponent - ], - providers: [ - FocusManagerService, - IdGenerationService - ], - schemas: [NO_ERRORS_SCHEMA] + declarations: [SchemaBasedDictEditorComponent], + providers: [FocusManagerService, IdGenerationService], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -50,7 +53,7 @@ describe('Schema Based Dict Editor Component', () => { }); it('should set component properties on initialization', fakeAsync(() => { - let mockFunction = function(value: Record) { + let mockFunction = function (value: Record) { return value; }; component.registerOnChange(mockFunction); @@ -67,34 +70,31 @@ describe('Schema Based Dict Editor Component', () => { { name: 'Name1', schema: { - type: 'int' - } + type: 'int', + }, }, { name: 'Name2', schema: { - type: 'int' - } - } + type: 'int', + }, + }, ]; - spyOn(idGenerationService, 'generateNewId') - .and.returnValues('id1', 'id2'); + spyOn(idGenerationService, 'generateNewId').and.returnValues('id1', 'id2'); expect(component.fieldIds).toEqual({}); component.ngOnInit(); - expect(component.fieldIds).toEqual( - { - Name1: 'id1', - Name2: 'id2' - } - ); + expect(component.fieldIds).toEqual({ + Name1: 'id1', + Name2: 'id2', + }); }); it('should write value', () => { component.localValue = { - first: 'false' + first: 'false', }; component.writeValue({first: 'true'}); @@ -103,7 +103,7 @@ describe('Schema Based Dict Editor Component', () => { it('should update value when local value change', () => { let localValue = { - first: 'true' + first: 'true', }; component.localValue = localValue; @@ -117,7 +117,7 @@ describe('Schema Based Dict Editor Component', () => { it('should not update value when local value not change', () => { let localValue = { - first: 'true' + first: 'true', }; component.localValue = localValue; @@ -135,14 +135,14 @@ describe('Schema Based Dict Editor Component', () => { it('should get schema', () => { const HTML_SCHEMA = { - type: 'html' + type: 'html', } as Schema; component.propertySchemas = [ { name: 'id1', - schema: HTML_SCHEMA - } + schema: HTML_SCHEMA, + }, ]; expect(component.getSchema(0)).toBe(HTML_SCHEMA); @@ -155,9 +155,11 @@ describe('Schema Based Dict Editor Component', () => { }); it('should get human readable property description', () => { - expect(component.getHumanReadablePropertyDescription({ - description: 'This is the property description', - name: 'Property Name' - })).toBe('This is the property description'); + expect( + component.getHumanReadablePropertyDescription({ + description: 'This is the property description', + name: 'Property Name', + }) + ).toBe('This is the property description'); }); }); diff --git a/core/templates/components/forms/schema-based-editors/schema-based-dict-editor.component.ts b/core/templates/components/forms/schema-based-editors/schema-based-dict-editor.component.ts index 172d3728f1d3..9d2b31a58c10 100644 --- a/core/templates/components/forms/schema-based-editors/schema-based-dict-editor.component.ts +++ b/core/templates/components/forms/schema-based-editors/schema-based-dict-editor.component.ts @@ -16,11 +16,21 @@ * @fileoverview Component for a schema-based editor for dicts. */ -import { Component, forwardRef, Input, OnInit } from '@angular/core'; -import { NG_VALUE_ACCESSOR, NG_VALIDATORS, ControlValueAccessor, Validator, AbstractControl, ValidationErrors } from '@angular/forms'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { IdGenerationService } from 'services/id-generation.service'; -import { Schema, SchemaDefaultValue } from 'services/schema-default-value.service'; +import {Component, forwardRef, Input, OnInit} from '@angular/core'; +import { + NG_VALUE_ACCESSOR, + NG_VALIDATORS, + ControlValueAccessor, + Validator, + AbstractControl, + ValidationErrors, +} from '@angular/forms'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {IdGenerationService} from 'services/id-generation.service'; +import { + Schema, + SchemaDefaultValue, +} from 'services/schema-default-value.service'; @Component({ selector: 'schema-based-dict-editor', @@ -29,18 +39,18 @@ import { Schema, SchemaDefaultValue } from 'services/schema-default-value.servic { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SchemaBasedDictEditorComponent), - multi: true + multi: true, }, { provide: NG_VALIDATORS, useExisting: forwardRef(() => SchemaBasedDictEditorComponent), - multi: true + multi: true, }, - ] + ], }) - export class SchemaBasedDictEditorComponent -implements ControlValueAccessor, OnInit, Validator { + implements ControlValueAccessor, OnInit, Validator +{ // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -55,7 +65,7 @@ implements ControlValueAccessor, OnInit, Validator { fieldIds: Record = {}; JSON = JSON; onChange: (val: Record) => void = () => {}; - constructor(private idGenerationService: IdGenerationService) { } + constructor(private idGenerationService: IdGenerationService) {} // Implemented as a part of ControlValueAccessor interface. writeValue(value: Record): void { @@ -64,14 +74,13 @@ implements ControlValueAccessor, OnInit, Validator { // Implemented as a part of ControlValueAccessor interface. registerOnChange( - fn: (val: Record) => void + fn: (val: Record) => void ): void { this.onChange = fn; } // Implemented as a part of ControlValueAccessor interface. - registerOnTouched(): void { - } + registerOnTouched(): void {} // Implemented as a part of Validator interface. validate(control: AbstractControl): ValidationErrors { @@ -87,8 +96,8 @@ implements ControlValueAccessor, OnInit, Validator { this.fieldIds = {}; for (let i = 0; i < this.propertySchemas.length; i++) { // Generate random IDs for each field. - this.fieldIds[this.propertySchemas[i].name] = ( - this.idGenerationService.generateNewId()); + this.fieldIds[this.propertySchemas[i].name] = + this.idGenerationService.generateNewId(); } } @@ -105,13 +114,17 @@ implements ControlValueAccessor, OnInit, Validator { return this.labelForFocusTarget; } - getHumanReadablePropertyDescription( - property: {description: string; name: string} - ): string { + getHumanReadablePropertyDescription(property: { + description: string; + name: string; + }): string { return property.description || '[' + property.name + ']'; } } -angular.module('oppia').directive('schemaBasedDictEditor', downgradeComponent({ - component: SchemaBasedDictEditorComponent -})); +angular.module('oppia').directive( + 'schemaBasedDictEditor', + downgradeComponent({ + component: SchemaBasedDictEditorComponent, + }) +); diff --git a/core/templates/components/forms/schema-based-editors/schema-based-editor.component.spec.ts b/core/templates/components/forms/schema-based-editors/schema-based-editor.component.spec.ts index 6f8180e6cdfa..46db85edfe1d 100644 --- a/core/templates/components/forms/schema-based-editors/schema-based-editor.component.spec.ts +++ b/core/templates/components/forms/schema-based-editors/schema-based-editor.component.spec.ts @@ -16,23 +16,27 @@ * @fileoverview Unit tests for Schema Based Editor Component */ -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { FormControl, FormsModule } from '@angular/forms'; -import { SchemaDefaultValue } from 'services/schema-default-value.service'; -import { SchemaBasedEditorComponent } from './schema-based-editor.component'; - -describe('Schema based editor component', function() { +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {FormControl, FormsModule} from '@angular/forms'; +import {SchemaDefaultValue} from 'services/schema-default-value.service'; +import {SchemaBasedEditorComponent} from './schema-based-editor.component'; + +describe('Schema based editor component', function () { let component: SchemaBasedEditorComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [FormsModule], - declarations: [ - SchemaBasedEditorComponent - ], - schemas: [NO_ERRORS_SCHEMA] + declarations: [SchemaBasedEditorComponent], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -42,7 +46,7 @@ describe('Schema based editor component', function() { component.schema = { type: 'float', - choices: [12, 23] + choices: [12, 23], }; fixture.detectChanges(); @@ -53,7 +57,7 @@ describe('Schema based editor component', function() { }); it('should set component properties on initialization', fakeAsync(() => { - let mockFunction = function(value: SchemaDefaultValue) { + let mockFunction = function (value: SchemaDefaultValue) { return value; }; component.registerOnChange(mockFunction); @@ -77,11 +81,9 @@ describe('Schema based editor component', function() { it('should set form validity', fakeAsync(() => { let mockEmitter = new EventEmitter(); - let form = jasmine.createSpyObj( - 'form', ['$setValidity']); + let form = jasmine.createSpyObj('form', ['$setValidity']); - spyOnProperty(component.form, 'statusChanges') - .and.returnValue(mockEmitter); + spyOnProperty(component.form, 'statusChanges').and.returnValue(mockEmitter); spyOn(angular, 'element').and.returnValue( // This throws "Type '{ top: number; }' is not assignable to type // 'JQLite | Coordinates'". We need to suppress this error because @@ -90,7 +92,7 @@ describe('Schema based editor component', function() { { controller: (formString: string) => { return form; - } + }, } ); diff --git a/core/templates/components/forms/schema-based-editors/schema-based-editor.component.ts b/core/templates/components/forms/schema-based-editors/schema-based-editor.component.ts index ef35773f222c..40ad2229f82a 100644 --- a/core/templates/components/forms/schema-based-editors/schema-based-editor.component.ts +++ b/core/templates/components/forms/schema-based-editors/schema-based-editor.component.ts @@ -16,11 +16,31 @@ * @fileoverview Component for general schema-based editors. */ -import { Input, Output, EventEmitter, Component, forwardRef, AfterViewInit, ViewChild, ElementRef } from '@angular/core'; -import { NG_VALUE_ACCESSOR, NG_VALIDATORS, ControlValueAccessor, Validator, AbstractControl, ValidationErrors, NgForm } from '@angular/forms'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { Schema, SchemaDefaultValue } from 'services/schema-default-value.service'; -import { VALIDATION_STATUS_INVALID } from 'utility/forms'; +import { + Input, + Output, + EventEmitter, + Component, + forwardRef, + AfterViewInit, + ViewChild, + ElementRef, +} from '@angular/core'; +import { + NG_VALUE_ACCESSOR, + NG_VALIDATORS, + ControlValueAccessor, + Validator, + AbstractControl, + ValidationErrors, + NgForm, +} from '@angular/forms'; +import {downgradeComponent} from '@angular/upgrade/static'; +import { + Schema, + SchemaDefaultValue, +} from 'services/schema-default-value.service'; +import {VALIDATION_STATUS_INVALID} from 'utility/forms'; @Component({ selector: 'schema-based-editor', @@ -29,17 +49,18 @@ import { VALIDATION_STATUS_INVALID } from 'utility/forms'; { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SchemaBasedEditorComponent), - multi: true + multi: true, }, { provide: NG_VALIDATORS, useExisting: forwardRef(() => SchemaBasedEditorComponent), - multi: true + multi: true, }, - ] + ], }) export class SchemaBasedEditorComponent - implements AfterViewInit, ControlValueAccessor, Validator { + implements AfterViewInit, ControlValueAccessor, Validator +{ // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -51,7 +72,7 @@ export class SchemaBasedEditorComponent @Output() inputBlur = new EventEmitter(); @Output() inputFocus = new EventEmitter(); @Input() notRequired!: boolean; - onChange: (val: SchemaDefaultValue) => void = () => { }; + onChange: (val: SchemaDefaultValue) => void = () => {}; onValidatorChange: () => void = () => {}; get localValue(): SchemaDefaultValue { return this._localValue; @@ -64,7 +85,7 @@ export class SchemaBasedEditorComponent } @Output() localValueChange = new EventEmitter(); - constructor(private elementRef: ElementRef) { } + constructor(private elementRef: ElementRef) {} // Implemented as a part of ControlValueAccessor interface. writeValue(value: SchemaDefaultValue): void { @@ -80,8 +101,7 @@ export class SchemaBasedEditorComponent } // Implemented as a part of ControlValueAccessor interface. - registerOnTouched(): void { - } + registerOnTouched(): void {} registerOnValidatorChange(fn: () => void): void { this.onValidatorChange = fn; @@ -92,12 +112,13 @@ export class SchemaBasedEditorComponent if (!this.form) { return null; } - return this.form.valid ? null : { invalid: true }; + return this.form.valid ? null : {invalid: true}; } ngAfterViewInit(): void { - let angularJsFormController: angular.IFormController | undefined = ( - angular?.element(this.elementRef.nativeElement).controller('form')); + let angularJsFormController: angular.IFormController | undefined = angular + ?.element(this.elementRef.nativeElement) + .controller('form'); // The 'statusChanges' property is an Observable that emits an event every // time the status of the control changes. The NgForm class, which our // component is using, initializes 'this.form' (which is an instance of @@ -113,24 +134,34 @@ export class SchemaBasedEditorComponent // because we are confident that 'statusChanges' will not be null when we // use it in our component. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.form.statusChanges!.subscribe((validationStatus) => { + this.form.statusChanges!.subscribe(validationStatus => { this.onValidatorChange(); - if (angularJsFormController === null || - angularJsFormController === undefined) { + if ( + angularJsFormController === null || + angularJsFormController === undefined + ) { return; } if (validationStatus === VALIDATION_STATUS_INVALID) { angularJsFormController.$setValidity( - 'schema', false, angularJsFormController); + 'schema', + false, + angularJsFormController + ); } else { angularJsFormController.$setValidity( - 'schema', true, angularJsFormController); + 'schema', + true, + angularJsFormController + ); } }); } } - -angular.module('oppia').directive('schemaBasedEditor', downgradeComponent({ - component: SchemaBasedEditorComponent -})); +angular.module('oppia').directive( + 'schemaBasedEditor', + downgradeComponent({ + component: SchemaBasedEditorComponent, + }) +); diff --git a/core/templates/components/forms/schema-based-editors/schema-based-expression-editor.component.spec.ts b/core/templates/components/forms/schema-based-editors/schema-based-expression-editor.component.spec.ts index 9c3330ce7193..8de7ed24ec86 100644 --- a/core/templates/components/forms/schema-based-editors/schema-based-expression-editor.component.spec.ts +++ b/core/templates/components/forms/schema-based-editors/schema-based-expression-editor.component.spec.ts @@ -16,14 +16,20 @@ * @fileoverview Unit tests for Schema Based Editor Component */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { SchemaFormSubmittedService } from 'services/schema-form-submitted.service'; -import { SchemaBasedExpressionEditorComponent } from './schema-based-expression-editor.component'; -import { FormControl } from '@angular/forms'; -import { SchemaDefaultValue } from 'services/schema-default-value.service'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {SchemaFormSubmittedService} from 'services/schema-form-submitted.service'; +import {SchemaBasedExpressionEditorComponent} from './schema-based-expression-editor.component'; +import {FormControl} from '@angular/forms'; +import {SchemaDefaultValue} from 'services/schema-default-value.service'; describe('Schema Based Expression Editor Component', () => { let component: SchemaBasedExpressionEditorComponent; @@ -33,14 +39,9 @@ describe('Schema Based Expression Editor Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - SchemaBasedExpressionEditorComponent - ], - providers: [ - FocusManagerService, - SchemaFormSubmittedService, - ], - schemas: [NO_ERRORS_SCHEMA] + declarations: [SchemaBasedExpressionEditorComponent], + providers: [FocusManagerService, SchemaFormSubmittedService], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -51,7 +52,7 @@ describe('Schema Based Expression Editor Component', () => { }); it('should set component properties on initialization', fakeAsync(() => { - let mockFunction = function(value: SchemaDefaultValue) { + let mockFunction = function (value: SchemaDefaultValue) { return value; }; component.registerOnChange(mockFunction); diff --git a/core/templates/components/forms/schema-based-editors/schema-based-expression-editor.component.ts b/core/templates/components/forms/schema-based-editors/schema-based-expression-editor.component.ts index 8b9e2a2bfb9b..65de0f784b9c 100644 --- a/core/templates/components/forms/schema-based-editors/schema-based-expression-editor.component.ts +++ b/core/templates/components/forms/schema-based-editors/schema-based-expression-editor.component.ts @@ -16,19 +16,25 @@ * @fileoverview Component for a schema-based editor for expressions. */ -import { Component, Input, OnInit } from '@angular/core'; -import { AbstractControl, ControlValueAccessor, ValidationErrors, Validator } from '@angular/forms'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { SchemaDefaultValue } from 'services/schema-default-value.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { SchemaBasedDictEditorComponent } from './schema-based-dict-editor.component'; +import {Component, Input, OnInit} from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + ValidationErrors, + Validator, +} from '@angular/forms'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {SchemaDefaultValue} from 'services/schema-default-value.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {SchemaBasedDictEditorComponent} from './schema-based-dict-editor.component'; @Component({ selector: 'oppia-schema-based-editor', - templateUrl: './schema-based-expression-editor.component.html' + templateUrl: './schema-based-expression-editor.component.html', }) export class SchemaBasedExpressionEditorComponent -implements ControlValueAccessor, Validator, OnInit { + implements ControlValueAccessor, Validator, OnInit +{ localValue!: SchemaDefaultValue; @Input() disabled!: boolean; @Input() outputType!: 'bool' | 'int' | 'float'; @@ -54,8 +60,7 @@ implements ControlValueAccessor, Validator, OnInit { } // Implemented as a part of ControlValueAccessor interface. - registerOnTouched(): void { - } + registerOnTouched(): void {} // Implemented as a part of Validator interface. validate(control: AbstractControl): ValidationErrors { @@ -79,6 +84,6 @@ implements ControlValueAccessor, Validator, OnInit { angular.module('oppia').directive( 'schemaBasedExpressionEditor', downgradeComponent({ - component: SchemaBasedDictEditorComponent + component: SchemaBasedDictEditorComponent, }) as angular.IDirectiveFactory ); diff --git a/core/templates/components/forms/schema-based-editors/schema-based-float-editor.component.spec.ts b/core/templates/components/forms/schema-based-editors/schema-based-float-editor.component.spec.ts index 114c579c289c..6c1a1f204e77 100644 --- a/core/templates/components/forms/schema-based-editors/schema-based-float-editor.component.spec.ts +++ b/core/templates/components/forms/schema-based-editors/schema-based-float-editor.component.spec.ts @@ -16,15 +16,21 @@ * @fileoverview Unit tests for Schema Based Float Editor Component */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { FormControl, FormsModule } from '@angular/forms'; -import { NumericInputValidationService } from 'interactions/NumericInput/directives/numeric-input-validation.service'; -import { SchemaFormSubmittedService } from 'services/schema-form-submitted.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { SchemaBasedFloatEditorComponent } from './schema-based-float-editor.component'; -import { NumberConversionService } from 'services/number-conversion.service'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {FormControl, FormsModule} from '@angular/forms'; +import {NumericInputValidationService} from 'interactions/NumericInput/directives/numeric-input-validation.service'; +import {SchemaFormSubmittedService} from 'services/schema-form-submitted.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {SchemaBasedFloatEditorComponent} from './schema-based-float-editor.component'; +import {NumberConversionService} from 'services/number-conversion.service'; class MockFocusManagerService { setFocusWithoutScroll(value: string) {} @@ -36,7 +42,7 @@ class MockFocusManagerService { setFocus(value: string) {} } -describe('Schema based float editor component', function() { +describe('Schema based float editor component', function () { let component: SchemaBasedFloatEditorComponent; let fixture: ComponentFixture; let schemaFormSubmittedService: SchemaFormSubmittedService; @@ -47,19 +53,16 @@ describe('Schema based float editor component', function() { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [FormsModule], - declarations: [ - SchemaBasedFloatEditorComponent, - MockTranslatePipe - ], + declarations: [SchemaBasedFloatEditorComponent, MockTranslatePipe], providers: [ { provide: FocusManagerService, - useClass: MockFocusManagerService + useClass: MockFocusManagerService, }, NumericInputValidationService, - SchemaFormSubmittedService + SchemaFormSubmittedService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -71,17 +74,18 @@ describe('Schema based float editor component', function() { { id: 'is_at_least', min_value: 1.1, - max_value: 2.2 + max_value: 2.2, }, { id: 'is_at_most', max_value: 3.5, - min_value: 4.4 - } + min_value: 4.4, + }, ]; schemaFormSubmittedService = TestBed.inject(SchemaFormSubmittedService); - numericInputValidationService = - TestBed.inject(NumericInputValidationService); + numericInputValidationService = TestBed.inject( + NumericInputValidationService + ); numberConversionService = TestBed.inject(NumberConversionService); validator = TestBed.inject(NumericInputValidationService); fixture.detectChanges(); @@ -105,7 +109,7 @@ describe('Schema based float editor component', function() { expect(component.minValue).toBe(0); tick(); - let mockFunction = function(value: number | null) {}; + let mockFunction = function (value: number | null) {}; component.registerOnChange(mockFunction); component.writeValue(2); component.registerOnTouched(null); @@ -143,7 +147,7 @@ describe('Schema based float editor component', function() { it('should register that the user is typing on keypress', fakeAsync(() => { let evt = new KeyboardEvent('', { - keyCode: 14 + keyCode: 14, }); component.userIsCurrentlyTyping = false; @@ -155,7 +159,7 @@ describe('Schema based float editor component', function() { it('should not submit form if there is an error', fakeAsync(() => { let evt = new KeyboardEvent('', { - keyCode: 13 + keyCode: 13, }); let formvalue = new FormControl(null); @@ -169,10 +173,12 @@ describe('Schema based float editor component', function() { })); it('should not submit form if there is an error', fakeAsync(() => { - spyOn(schemaFormSubmittedService.onSubmittedSchemaBasedForm, 'emit') - .and.stub(); + spyOn( + schemaFormSubmittedService.onSubmittedSchemaBasedForm, + 'emit' + ).and.stub(); let evt = new KeyboardEvent('', { - keyCode: 13 + keyCode: 13, }); let formvalue = new FormControl(null); @@ -183,43 +189,48 @@ describe('Schema based float editor component', function() { component.onKeypress(evt); expect(component.userIsCurrentlyTyping).toBe(true); - expect(schemaFormSubmittedService.onSubmittedSchemaBasedForm.emit) - .toHaveBeenCalled(); + expect( + schemaFormSubmittedService.onSubmittedSchemaBasedForm.emit + ).toHaveBeenCalled(); })); it('should generate error for wrong input', fakeAsync(() => { expect(component.errorStringI18nKey).toBe(null); - spyOn(numericInputValidationService, 'validateNumber') - .and.returnValue('I18N_INTERACTIONS_NUMERIC_INPUT_INVALID_NUMBER'); + spyOn(numericInputValidationService, 'validateNumber').and.returnValue( + 'I18N_INTERACTIONS_NUMERIC_INPUT_INVALID_NUMBER' + ); component.localValue = null; component.generateErrors(); - expect(component.errorStringI18nKey) - .toBe('I18N_INTERACTIONS_NUMERIC_INPUT_INVALID_NUMBER'); + expect(component.errorStringI18nKey).toBe( + 'I18N_INTERACTIONS_NUMERIC_INPUT_INVALID_NUMBER' + ); })); it('should validate value', () => { component.uiConfig = {checkRequireNonnegativeInput: false}; - expect(component.validate(new FormControl(null))) - .toEqual({error: 'invalid'}); - expect(component.validate(new FormControl(undefined))) - .toEqual({error: 'invalid'}); - expect(component.validate(new FormControl(''))) - .toEqual({error: 'invalid'}); + expect(component.validate(new FormControl(null))).toEqual({ + error: 'invalid', + }); + expect(component.validate(new FormControl(undefined))).toEqual({ + error: 'invalid', + }); + expect(component.validate(new FormControl(''))).toEqual({error: 'invalid'}); const numericInputValidationServiceSpy = spyOn( - numericInputValidationService, 'validateNumber'); - expect(component.validate(new FormControl(2))) - .toEqual({}); - expect(numericInputValidationServiceSpy).toHaveBeenCalledWith( - 2, false + numericInputValidationService, + 'validateNumber' ); + expect(component.validate(new FormControl(2))).toEqual({}); + expect(numericInputValidationServiceSpy).toHaveBeenCalledWith(2, false); }); it('should get current decimal separator', () => { - spyOn(numberConversionService, 'currentDecimalSeparator') - .and.returnValues('.', ','); + spyOn(numberConversionService, 'currentDecimalSeparator').and.returnValues( + '.', + ',' + ); expect(component.getCurrentDecimalSeparator()).toEqual('.'); expect(component.getCurrentDecimalSeparator()).toEqual(','); diff --git a/core/templates/components/forms/schema-based-editors/schema-based-float-editor.component.ts b/core/templates/components/forms/schema-based-editors/schema-based-float-editor.component.ts index 1b5c263ff569..071dd995b443 100644 --- a/core/templates/components/forms/schema-based-editors/schema-based-float-editor.component.ts +++ b/core/templates/components/forms/schema-based-editors/schema-based-float-editor.component.ts @@ -16,19 +16,35 @@ * @fileoverview Component for a schema-based editor for floats. */ -import { Component, EventEmitter, forwardRef, Input, OnInit, Output, ViewChild } from '@angular/core'; -import { AbstractControl, ControlValueAccessor, NgForm, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator } from '@angular/forms'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NumericInputValidationService } from 'interactions/NumericInput/directives/numeric-input-validation.service'; -import { NumberConversionService } from 'services/number-conversion.service'; -import { SchemaDefaultValue } from 'services/schema-default-value.service'; -import { SchemaFormSubmittedService } from 'services/schema-form-submitted.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; +import { + Component, + EventEmitter, + forwardRef, + Input, + OnInit, + Output, + ViewChild, +} from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + NgForm, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, +} from '@angular/forms'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NumericInputValidationService} from 'interactions/NumericInput/directives/numeric-input-validation.service'; +import {NumberConversionService} from 'services/number-conversion.service'; +import {SchemaDefaultValue} from 'services/schema-default-value.service'; +import {SchemaFormSubmittedService} from 'services/schema-form-submitted.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; interface OppiaValidator { id: string; - 'min_value': number; - 'max_value': number; + min_value: number; + max_value: number; } @Component({ @@ -39,17 +55,18 @@ interface OppiaValidator { { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SchemaBasedFloatEditorComponent), - multi: true + multi: true, }, { provide: NG_VALIDATORS, useExisting: forwardRef(() => SchemaBasedFloatEditorComponent), - multi: true + multi: true, }, - ] + ], }) export class SchemaBasedFloatEditorComponent -implements ControlValueAccessor, OnInit, Validator { + implements ControlValueAccessor, OnInit, Validator +{ @Output() inputBlur = new EventEmitter(); @Output() inputFocus = new EventEmitter(); // These properties are initialized using Angular lifecycle hooks @@ -59,7 +76,7 @@ implements ControlValueAccessor, OnInit, Validator { @Input() validators!: OppiaValidator[]; @Input() labelForFocusTarget!: string; @Input() uiConfig!: {checkRequireNonnegativeInput: boolean}; - @ViewChild('floatform', {'static': true}) floatForm!: NgForm; + @ViewChild('floatform', {static: true}) floatForm!: NgForm; // If input is empty, the number value should be null. localValue!: number | null; labelForErrorFocusTarget!: string; @@ -78,7 +95,7 @@ implements ControlValueAccessor, OnInit, Validator { private numberConversionService: NumberConversionService, private numericInputValidationService: NumericInputValidationService, private schemaFormSubmittedService: SchemaFormSubmittedService - ) { } + ) {} // Implemented as a part of ControlValueAccessor interface. writeValue(value: number): void { @@ -91,8 +108,7 @@ implements ControlValueAccessor, OnInit, Validator { } // Implemented as a part of ControlValueAccessor interface. - registerOnTouched(fn: SchemaDefaultValue): void { - } + registerOnTouched(fn: SchemaDefaultValue): void {} // Implemented as a part of Validator interface. validate(control: AbstractControl): ValidationErrors { @@ -100,14 +116,14 @@ implements ControlValueAccessor, OnInit, Validator { if (this._validate(control.value, this.uiConfig)) { return {}; } - return { error: 'invalid' }; + return {error: 'invalid'}; } private _validate( - localValue: number | string, - customizationArg: {checkRequireNonnegativeInput: SchemaDefaultValue} + localValue: number | string, + customizationArg: {checkRequireNonnegativeInput: SchemaDefaultValue} ): boolean { - let { checkRequireNonnegativeInput } = customizationArg || {}; + let {checkRequireNonnegativeInput} = customizationArg || {}; // TODO(#15462): Move the type base checks (like the ones done below) to // schema-validator's isFloat method. @@ -116,8 +132,10 @@ implements ControlValueAccessor, OnInit, Validator { localValue !== null && localValue !== '' && this.numericInputValidationService.validateNumber( - +localValue, Boolean(checkRequireNonnegativeInput || false) - ) === undefined); + +localValue, + Boolean(checkRequireNonnegativeInput || false) + ) === undefined + ); } updateLocalValue(value: string): void { @@ -130,9 +148,8 @@ implements ControlValueAccessor, OnInit, Validator { this.hasLoaded = false; this.userIsCurrentlyTyping = false; this.userHasFocusedAtLeastOnce = false; - this.labelForErrorFocusTarget = ( - this.focusManagerService.generateFocusLabel() - ); + this.labelForErrorFocusTarget = + this.focusManagerService.generateFocusLabel(); if (this.localValue === undefined) { this.localValue = 0.0; } @@ -141,10 +158,11 @@ implements ControlValueAccessor, OnInit, Validator { } // To check checkRequireNonnegativeInput customization argument // value of numeric input interaction. - let { checkRequireNonnegativeInput } = this.uiConfig || {}; - this.checkRequireNonnegativeInputValue = ( - checkRequireNonnegativeInput === undefined ? false : - checkRequireNonnegativeInput); + let {checkRequireNonnegativeInput} = this.uiConfig || {}; + this.checkRequireNonnegativeInputValue = + checkRequireNonnegativeInput === undefined + ? false + : checkRequireNonnegativeInput; // If customization argument of numeric input interaction is true, // set Min value as 0 to not let down key go below 0. if (checkRequireNonnegativeInput) { @@ -193,20 +211,19 @@ implements ControlValueAccessor, OnInit, Validator { } generateErrors(): void { - this.errorStringI18nKey = ( + this.errorStringI18nKey = this.numericInputValidationService.validateNumber( this.localValue, this.checkRequireNonnegativeInputValue, - this.getCurrentDecimalSeparator())) || null; + this.getCurrentDecimalSeparator() + ) || null; } onKeypress(evt: KeyboardEvent): void { if (evt.keyCode === 13) { if ( this.floatForm.form.controls.floatValue.errors !== null && - Object.keys( - this.floatForm.form.controls.floatValue.errors - ).length !== 0 + Object.keys(this.floatForm.form.controls.floatValue.errors).length !== 0 ) { this.userIsCurrentlyTyping = false; this.focusManagerService.setFocus(this.labelForErrorFocusTarget); @@ -230,17 +247,18 @@ implements ControlValueAccessor, OnInit, Validator { this.errorStringI18nKey = null; } else { // Make sure number is in a correct format. - let error = this.numericInputValidationService - .validateNumericString( - this.localStringValue, - this.getCurrentDecimalSeparator()); + let error = this.numericInputValidationService.validateNumericString( + this.localStringValue, + this.getCurrentDecimalSeparator() + ); if (error !== undefined) { this.localValue = null; this.errorStringI18nKey = error || null; } else { // Parse number if the string is in proper format. - this.localValue = this.numberConversionService - .convertToEnglishDecimal(this.localStringValue); + this.localValue = this.numberConversionService.convertToEnglishDecimal( + this.localStringValue + ); // Generate errors (if any). this.generateErrors(); @@ -249,6 +267,9 @@ implements ControlValueAccessor, OnInit, Validator { } } -angular.module('oppia').directive('schemaBasedFloatEditor', downgradeComponent({ - component: SchemaBasedFloatEditorComponent -})); +angular.module('oppia').directive( + 'schemaBasedFloatEditor', + downgradeComponent({ + component: SchemaBasedFloatEditorComponent, + }) +); diff --git a/core/templates/components/forms/schema-based-editors/schema-based-html-editor.component.spec.ts b/core/templates/components/forms/schema-based-editors/schema-based-html-editor.component.spec.ts index 3097c674bf5f..f879ccb60fc0 100644 --- a/core/templates/components/forms/schema-based-editors/schema-based-html-editor.component.spec.ts +++ b/core/templates/components/forms/schema-based-editors/schema-based-html-editor.component.spec.ts @@ -16,12 +16,17 @@ * @fileoverview Unit tests for Schema Based Html Editor Component */ -import { FormControl, FormsModule } from '@angular/forms'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { SchemaFormSubmittedService } from 'services/schema-form-submitted.service'; -import { SchemaBasedHtmlEditorComponent } from './schema-based-html-editor.component'; +import {FormControl, FormsModule} from '@angular/forms'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {SchemaFormSubmittedService} from 'services/schema-form-submitted.service'; +import {SchemaBasedHtmlEditorComponent} from './schema-based-html-editor.component'; describe('Schema Based Html Editor Component', () => { let component: SchemaBasedHtmlEditorComponent; @@ -30,14 +35,9 @@ describe('Schema Based Html Editor Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [FormsModule], - declarations: [ - SchemaBasedHtmlEditorComponent - ], - providers: [ - FocusManagerService, - SchemaFormSubmittedService, - ], - schemas: [NO_ERRORS_SCHEMA] + declarations: [SchemaBasedHtmlEditorComponent], + providers: [FocusManagerService, SchemaFormSubmittedService], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -47,7 +47,7 @@ describe('Schema Based Html Editor Component', () => { }); it('should set component properties on initialization', fakeAsync(() => { - let mockFunction = function(value: string) { + let mockFunction = function (value: string) { return value; }; component.registerOnChange(mockFunction); @@ -58,7 +58,7 @@ describe('Schema Based Html Editor Component', () => { expect(component.onChange).toEqual(mockFunction); })); - it('should test the case when the input isn\'t valid', () => { + it("should test the case when the input isn't valid", () => { expect(component.validate(new FormControl(1))).toEqual({}); }); diff --git a/core/templates/components/forms/schema-based-editors/schema-based-html-editor.component.ts b/core/templates/components/forms/schema-based-editors/schema-based-html-editor.component.ts index bfe54488458b..c9d27d3d745b 100644 --- a/core/templates/components/forms/schema-based-editors/schema-based-html-editor.component.ts +++ b/core/templates/components/forms/schema-based-editors/schema-based-html-editor.component.ts @@ -16,9 +16,16 @@ * @fileoverview Component for a schema-based editor for HTML. */ -import { Component, forwardRef, Input, OnInit } from '@angular/core'; -import { NG_VALUE_ACCESSOR, NG_VALIDATORS, AbstractControl, ControlValueAccessor, ValidationErrors, Validator } from '@angular/forms'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component, forwardRef, Input, OnInit} from '@angular/core'; +import { + NG_VALUE_ACCESSOR, + NG_VALIDATORS, + AbstractControl, + ControlValueAccessor, + ValidationErrors, + Validator, +} from '@angular/forms'; +import {downgradeComponent} from '@angular/upgrade/static'; @Component({ selector: 'schema-based-html-editor', @@ -27,24 +34,24 @@ import { downgradeComponent } from '@angular/upgrade/static'; { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SchemaBasedHtmlEditorComponent), - multi: true + multi: true, }, { provide: NG_VALIDATORS, useExisting: forwardRef(() => SchemaBasedHtmlEditorComponent), - multi: true + multi: true, }, - ] + ], }) - export class SchemaBasedHtmlEditorComponent -implements ControlValueAccessor, OnInit, Validator { + implements ControlValueAccessor, OnInit, Validator +{ // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @Input() disabled!: boolean; @Input() labelForFocusTarget!: string; - @Input() uiConfig!: {'add_element_text': string}; + @Input() uiConfig!: {add_element_text: string}; localValue!: string; onChange: (val: string) => void = () => {}; @@ -59,8 +66,7 @@ implements ControlValueAccessor, OnInit, Validator { } // Implemented as a part of ControlValueAccessor interface. - registerOnTouched(): void { - } + registerOnTouched(): void {} // Implemented as a part of Validator interface. validate(control: AbstractControl): ValidationErrors { @@ -72,7 +78,7 @@ implements ControlValueAccessor, OnInit, Validator { return {}; } - ngOnInit(): void { } + ngOnInit(): void {} updateValue(value: string): void { this.onChange(value); @@ -82,6 +88,9 @@ implements ControlValueAccessor, OnInit, Validator { } } -angular.module('oppia').directive('schemaBasedHtmlEditor', downgradeComponent({ - component: SchemaBasedHtmlEditorComponent -})); +angular.module('oppia').directive( + 'schemaBasedHtmlEditor', + downgradeComponent({ + component: SchemaBasedHtmlEditorComponent, + }) +); diff --git a/core/templates/components/forms/schema-based-editors/schema-based-int-editor.component.spec.ts b/core/templates/components/forms/schema-based-editors/schema-based-int-editor.component.spec.ts index 83ff65adbb46..4d6f6322f384 100644 --- a/core/templates/components/forms/schema-based-editors/schema-based-int-editor.component.spec.ts +++ b/core/templates/components/forms/schema-based-editors/schema-based-int-editor.component.spec.ts @@ -16,12 +16,18 @@ * @fileoverview Unit tests for Schema Based Int Editor Component */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { FormControl, FormsModule } from '@angular/forms'; -import { SchemaBasedIntEditorComponent } from './schema-based-int-editor.component'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { SchemaFormSubmittedService } from 'services/schema-form-submitted.service'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {FormControl, FormsModule} from '@angular/forms'; +import {SchemaBasedIntEditorComponent} from './schema-based-int-editor.component'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {SchemaFormSubmittedService} from 'services/schema-form-submitted.service'; describe('Schema Based Int Editor Component', () => { let component: SchemaBasedIntEditorComponent; @@ -32,14 +38,9 @@ describe('Schema Based Int Editor Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [FormsModule], - declarations: [ - SchemaBasedIntEditorComponent - ], - providers: [ - FocusManagerService, - SchemaFormSubmittedService, - ], - schemas: [NO_ERRORS_SCHEMA] + declarations: [SchemaBasedIntEditorComponent], + providers: [FocusManagerService, SchemaFormSubmittedService], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -53,7 +54,7 @@ describe('Schema Based Int Editor Component', () => { }); it('should set component properties on initialization', fakeAsync(() => { - let mockFunction = function(value: number) { + let mockFunction = function (value: number) { return value; }; component.registerOnChange(mockFunction); @@ -63,17 +64,20 @@ describe('Schema Based Int Editor Component', () => { expect(component.onChange).toEqual(mockFunction); })); - it('should set local value on initialization and set focus' + - ' on the input field', fakeAsync(() => { - spyOn(focusManagerService, 'setFocusWithoutScroll'); - expect(component.localValue).toBe(undefined); + it( + 'should set local value on initialization and set focus' + + ' on the input field', + fakeAsync(() => { + spyOn(focusManagerService, 'setFocusWithoutScroll'); + expect(component.localValue).toBe(undefined); - component.ngOnInit(); - tick(50); + component.ngOnInit(); + tick(50); - expect(component.localValue).toBe(0); - expect(focusManagerService.setFocusWithoutScroll).toHaveBeenCalled(); - })); + expect(component.localValue).toBe(0); + expect(focusManagerService.setFocusWithoutScroll).toHaveBeenCalled(); + }) + ); it('should write value', () => { component.localValue = 10; @@ -94,7 +98,7 @@ describe('Schema Based Int Editor Component', () => { expect(component.localValue).toBe(1); }); - it('should not update value when local value doesn\'t change', () => { + it("should not update value when local value doesn't change", () => { component.localValue = 1; expect(component.localValue).toBe(1); @@ -107,22 +111,23 @@ describe('Schema Based Int Editor Component', () => { it('should submit form on key press', () => { spyOn(schemaFormSubmittedService.onSubmittedSchemaBasedForm, 'emit'); let evt = new KeyboardEvent('', { - keyCode: 13 + keyCode: 13, }); component.onKeypress(evt); - expect(schemaFormSubmittedService.onSubmittedSchemaBasedForm.emit) - .toHaveBeenCalled(); + expect( + schemaFormSubmittedService.onSubmittedSchemaBasedForm.emit + ).toHaveBeenCalled(); }); it('should return errors for invalid value type', () => { - expect( - component.validate(new FormControl(false)) - ).toEqual({invalidType: 'boolean'}); - expect( - component.validate(new FormControl('4')) - ).toEqual({invalidType: 'string'}); + expect(component.validate(new FormControl(false))).toEqual({ + invalidType: 'boolean', + }); + expect(component.validate(new FormControl('4'))).toEqual({ + invalidType: 'string', + }); expect(component.validate(new FormControl(3))).toEqual(null); }); }); diff --git a/core/templates/components/forms/schema-based-editors/schema-based-int-editor.component.ts b/core/templates/components/forms/schema-based-editors/schema-based-int-editor.component.ts index 06507be57e53..68115c3ab347 100644 --- a/core/templates/components/forms/schema-based-editors/schema-based-int-editor.component.ts +++ b/core/templates/components/forms/schema-based-editors/schema-based-int-editor.component.ts @@ -16,14 +16,27 @@ * @fileoverview Component for a schema-based editor for integers. */ -import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'; -import { NG_VALUE_ACCESSOR, NG_VALIDATORS, ControlValueAccessor, Validator, AbstractControl, ValidationErrors } from '@angular/forms'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { SchemaFormSubmittedService } from 'services/schema-form-submitted.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { validate } from 'components/forms/validators/schema-validators'; -import { Validator as OppiaValidator } from 'interactions/TextInput/directives/text-input-validation.service'; - +import { + Component, + EventEmitter, + forwardRef, + Input, + OnInit, + Output, +} from '@angular/core'; +import { + NG_VALUE_ACCESSOR, + NG_VALIDATORS, + ControlValueAccessor, + Validator, + AbstractControl, + ValidationErrors, +} from '@angular/forms'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {SchemaFormSubmittedService} from 'services/schema-form-submitted.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {validate} from 'components/forms/validators/schema-validators'; +import {Validator as OppiaValidator} from 'interactions/TextInput/directives/text-input-validation.service'; @Component({ selector: 'schema-based-int-editor', @@ -33,17 +46,18 @@ import { Validator as OppiaValidator } from 'interactions/TextInput/directives/t { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SchemaBasedIntEditorComponent), - multi: true + multi: true, }, { provide: NG_VALIDATORS, useExisting: forwardRef(() => SchemaBasedIntEditorComponent), - multi: true + multi: true, }, - ] + ], }) export class SchemaBasedIntEditorComponent -implements ControlValueAccessor, OnInit, Validator { + implements ControlValueAccessor, OnInit, Validator +{ @Output() inputBlur = new EventEmitter(); @Output() inputFocus = new EventEmitter(); // These properties are initialized using Angular lifecycle hooks @@ -58,7 +72,7 @@ implements ControlValueAccessor, OnInit, Validator { constructor( private focusManagerService: FocusManagerService, private schemaFormSubmittedService: SchemaFormSubmittedService - ) { } + ) {} // Implemented as a part of ControlValueAccessor interface. writeValue(value: number): void { @@ -71,8 +85,7 @@ implements ControlValueAccessor, OnInit, Validator { } // Implemented as a part of ControlValueAccessor interface. - registerOnTouched(): void { - } + registerOnTouched(): void {} // Implemented as a part of Validator interface. validate(control: AbstractControl): ValidationErrors | null { @@ -105,6 +118,9 @@ implements ControlValueAccessor, OnInit, Validator { } } -angular.module('oppia').directive('schemaBasedIntEditor', downgradeComponent({ - component: SchemaBasedIntEditorComponent -}) as angular.IDirectiveFactory); +angular.module('oppia').directive( + 'schemaBasedIntEditor', + downgradeComponent({ + component: SchemaBasedIntEditorComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/components/forms/schema-based-editors/schema-based-list-editor.component.spec.ts b/core/templates/components/forms/schema-based-editors/schema-based-list-editor.component.spec.ts index 296c1f925cef..5821cb6c28de 100644 --- a/core/templates/components/forms/schema-based-editors/schema-based-list-editor.component.spec.ts +++ b/core/templates/components/forms/schema-based-editors/schema-based-list-editor.component.spec.ts @@ -16,16 +16,24 @@ * @fileoverview Unit tests for Schema Based List Editor Component */ -import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing'; -import { SchemaBasedListEditorComponent } from './schema-based-list-editor.component'; -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { FormControl, FormsModule } from '@angular/forms'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; - -import { Validator as OppiaValidator } from 'interactions/TextInput/directives/text-input-validation.service'; -import { SchemaDefaultValue, SchemaDefaultValueService } from 'services/schema-default-value.service'; -import { SchemaFormSubmittedService } from 'services/schema-form-submitted.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; +import { + ComponentFixture, + fakeAsync, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import {SchemaBasedListEditorComponent} from './schema-based-list-editor.component'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import {FormControl, FormsModule} from '@angular/forms'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; + +import {Validator as OppiaValidator} from 'interactions/TextInput/directives/text-input-validation.service'; +import { + SchemaDefaultValue, + SchemaDefaultValueService, +} from 'services/schema-default-value.service'; +import {SchemaFormSubmittedService} from 'services/schema-form-submitted.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; describe('Schema Based List Editor Component', () => { let component: SchemaBasedListEditorComponent; @@ -37,16 +45,13 @@ describe('Schema Based List Editor Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [FormsModule], - declarations: [ - SchemaBasedListEditorComponent, - MockTranslatePipe - ], + declarations: [SchemaBasedListEditorComponent, MockTranslatePipe], providers: [ SchemaDefaultValueService, SchemaFormSubmittedService, FocusManagerService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -62,26 +67,27 @@ describe('Schema Based List Editor Component', () => { items: [], ui_config: { coding_mode: true, - rows: 3 - } + rows: 3, + }, }; component.uiConfig = { - add_element_text: 'Add element' + add_element_text: 'Add element', }; component.validators = [ { id: 'has_length_at_most', max_value: 11, - } + }, ]; component.localValue = ['item1']; - spyOn(schemaDefaultValueService, 'getDefaultValue') - .and.returnValue('default'); + spyOn(schemaDefaultValueService, 'getDefaultValue').and.returnValue( + 'default' + ); }); it('should set component properties on initialization', fakeAsync(() => { - let mockFunction = function(value: SchemaDefaultValue[]) { + let mockFunction = function (value: SchemaDefaultValue[]) { return value; }; component.registerOnChange(mockFunction); @@ -117,26 +123,33 @@ describe('Schema Based List Editor Component', () => { expect(component.isAddItemButtonPresent).toBeTrue(); }); - it('should delete last element if user clicks outside the text input box' + - ' without entering any text', () => { - component.localValue = [ - // This throws "Type 'undefined' is not assignable to type - // 'SchemaDefaultValue'." We need to suppress this error - // because of the need to test validations. This error - // is thrown because the type of validators is - // OppiaValidator[] but we are assigning an object to it. - // @ts-ignore - 'item1', undefined]; + it( + 'should delete last element if user clicks outside the text input box' + + ' without entering any text', + () => { + component.localValue = [ + 'item1', + // This throws "Type 'undefined' is not assignable to type + // 'SchemaDefaultValue'." We need to suppress this error + // because of the need to test validations. This error + // is thrown because the type of validators is + // OppiaValidator[] but we are assigning an object to it. + // @ts-ignore + undefined, + ]; - component.lastElementOnBlur(); + component.lastElementOnBlur(); - const expectedValue = ['item1']; - expect(component.localValue).toEqual(expectedValue); - }); + const expectedValue = ['item1']; + expect(component.localValue).toEqual(expectedValue); + } + ); it('should add element to the item list', () => { let focusSpy = spyOnProperty( - focusManagerService, 'schemaBasedListEditorIsActive', 'set' + focusManagerService, + 'schemaBasedListEditorIsActive', + 'set' ).and.callThrough(); component.isOneLineInput = true; component.localValue = ['item1']; @@ -165,8 +178,8 @@ describe('Schema Based List Editor Component', () => { type: 'unicode', ui_config: { coding_mode: true, - rows: 3 - } + rows: 3, + }, }; component.ngOnInit(); @@ -180,62 +193,67 @@ describe('Schema Based List Editor Component', () => { type: 'unicode', ui_config: { coding_mode: false, - rows: 3 - } + rows: 3, + }, }; component.ngOnInit(); expect(component.isOneLineInput).toBeFalse(); }); - it('should fill item list with dummy elements if list length is less than' + - ' minimum length', () => { - component.validators = [ - { - id: 'has_length_at_least', - min_value: 3 - } - ]; - component.localValue = ['item1']; + it( + 'should fill item list with dummy elements if list length is less than' + + ' minimum length', + () => { + component.validators = [ + { + id: 'has_length_at_least', + min_value: 3, + }, + ]; + component.localValue = ['item1']; - component.ngOnInit(); + component.ngOnInit(); - const expectedValue = ['item1', 'default', 'default']; - expect(component.localValue).toEqual(expectedValue); - }); + const expectedValue = ['item1', 'default', 'default']; + expect(component.localValue).toEqual(expectedValue); + } + ); it( 'should not enable the showDuplicatesWarning flag when ' + - 'the validators don\'t include "is_uniquified"', + 'the validators don\'t include "is_uniquified"', () => { component.showDuplicatesWarning = false; component.validators = [ { id: 'has_length_at_least', - min_value: 3 - } + min_value: 3, + }, ]; component.ngOnInit(); expect(component.showDuplicatesWarning).toBeFalse(); - }); + } + ); it( 'should enable the showDuplicatesWarning flag when ' + - 'the validators include "is_uniquified"', + 'the validators include "is_uniquified"', () => { component.showDuplicatesWarning = false; component.validators = [ { id: 'is_uniquified', - } as unknown as OppiaValidator + } as unknown as OppiaValidator, ]; component.ngOnInit(); expect(component.showDuplicatesWarning).toBeTrue(); - }); + } + ); it('should hide item button if last added element is empty', () => { component.isAddItemButtonPresent = true; @@ -255,59 +273,70 @@ describe('Schema Based List Editor Component', () => { expect(component.localValue).toEqual(expectedValue); }); - it('should add element on child form submission when form submission' + - ' happens on the last item of the set', () => { - let onChildFormSubmitEmitter = new EventEmitter(); - spyOnProperty(schemaFormSubmittedService, 'onSubmittedSchemaBasedForm') - .and.returnValue(onChildFormSubmitEmitter); - component.validators = [ - { - id: 'has_length_at_least', - min_value: 3 - } - ]; + it( + 'should add element on child form submission when form submission' + + ' happens on the last item of the set', + () => { + let onChildFormSubmitEmitter = new EventEmitter(); + spyOnProperty( + schemaFormSubmittedService, + 'onSubmittedSchemaBasedForm' + ).and.returnValue(onChildFormSubmitEmitter); + component.validators = [ + { + id: 'has_length_at_least', + min_value: 3, + }, + ]; - component.ngOnInit(); + component.ngOnInit(); - component.localValue = ['item']; - component.isAddItemButtonPresent = false; + component.localValue = ['item']; + component.isAddItemButtonPresent = false; - onChildFormSubmitEmitter.emit(); + onChildFormSubmitEmitter.emit(); - const expectedValue = ['item', 'default']; - expect(component.localValue).toEqual(expectedValue); - }); + const expectedValue = ['item', 'default']; + expect(component.localValue).toEqual(expectedValue); + } + ); - it('should remove focus from element when form submission' + - ' does not happen on the last item of the set', () => { - let element: HTMLElement = document.createElement('button'); - element.setAttribute('class', 'oppia-skip-to-content'); - document.body.append(element); - element.focus(); - spyOn(element, 'blur'); - let onChildFormSubmitEmitter = new EventEmitter(); - spyOnProperty(schemaFormSubmittedService, 'onSubmittedSchemaBasedForm') - .and.returnValue(onChildFormSubmitEmitter); + it( + 'should remove focus from element when form submission' + + ' does not happen on the last item of the set', + () => { + let element: HTMLElement = document.createElement('button'); + element.setAttribute('class', 'oppia-skip-to-content'); + document.body.append(element); + element.focus(); + spyOn(element, 'blur'); + let onChildFormSubmitEmitter = new EventEmitter(); + spyOnProperty( + schemaFormSubmittedService, + 'onSubmittedSchemaBasedForm' + ).and.returnValue(onChildFormSubmitEmitter); - component.ngOnInit(); + component.ngOnInit(); - onChildFormSubmitEmitter.emit(); + onChildFormSubmitEmitter.emit(); - expect(element.blur).toHaveBeenCalled(); - }); + expect(element.blur).toHaveBeenCalled(); + } + ); it('should throw error when list editor length is invalid', () => { component.len = -1; - expect(() => component.ngOnInit()) - .toThrowError('Invalid length for list editor: -1'); + expect(() => component.ngOnInit()).toThrowError( + 'Invalid length for list editor: -1' + ); component.len = 5; component.localValue = ['a']; - expect(() => component.ngOnInit()) - .toThrowError( - 'List editor length does not match length of input value: 5 a'); + expect(() => component.ngOnInit()).toThrowError( + 'List editor length does not match length of input value: 5 a' + ); }); it('should change values when html is updated', () => { diff --git a/core/templates/components/forms/schema-based-editors/schema-based-list-editor.component.ts b/core/templates/components/forms/schema-based-editors/schema-based-list-editor.component.ts index 89abe9555d80..6bb431e5ab5f 100644 --- a/core/templates/components/forms/schema-based-editors/schema-based-list-editor.component.ts +++ b/core/templates/components/forms/schema-based-editors/schema-based-list-editor.component.ts @@ -16,16 +16,27 @@ * @fileoverview Component for a schema-based editor for lists. */ -import { Component, forwardRef, Input } from '@angular/core'; -import { AbstractControl, ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator } from '@angular/forms'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { Subscription } from 'rxjs'; -import { Validator as OppiaValidator } from 'interactions/TextInput/directives/text-input-validation.service'; -import { IdGenerationService } from 'services/id-generation.service'; -import { Schema, SchemaDefaultValue, SchemaDefaultValueService } from 'services/schema-default-value.service'; -import { SchemaFormSubmittedService } from 'services/schema-form-submitted.service'; -import { SchemaUndefinedLastElementService } from 'services/schema-undefined-last-element.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; +import {Component, forwardRef, Input} from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, +} from '@angular/forms'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {Subscription} from 'rxjs'; +import {Validator as OppiaValidator} from 'interactions/TextInput/directives/text-input-validation.service'; +import {IdGenerationService} from 'services/id-generation.service'; +import { + Schema, + SchemaDefaultValue, + SchemaDefaultValueService, +} from 'services/schema-default-value.service'; +import {SchemaFormSubmittedService} from 'services/schema-form-submitted.service'; +import {SchemaUndefinedLastElementService} from 'services/schema-undefined-last-element.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; @Component({ selector: 'schema-based-list-editor', templateUrl: './schema-based-list-editor.component.html', @@ -33,17 +44,18 @@ import { FocusManagerService } from 'services/stateful/focus-manager.service'; { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SchemaBasedListEditorComponent), - multi: true + multi: true, }, { provide: NG_VALIDATORS, useExisting: forwardRef(() => SchemaBasedListEditorComponent), - multi: true + multi: true, }, - ] + ], }) export class SchemaBasedListEditorComponent -implements ControlValueAccessor, Validator { + implements ControlValueAccessor, Validator +{ _localValue: SchemaDefaultValue[] = []; @Input() set localValue(val: SchemaDefaultValue[]) { this._localValue = val; @@ -60,13 +72,13 @@ implements ControlValueAccessor, Validator { @Input() disabled!: boolean; // Read-only property. The schema definition for each item in the list. @Input() itemSchema!: { - 'ui_config': {'coding_mode': boolean; rows: number}; + ui_config: {coding_mode: boolean; rows: number}; } & Schema; // The length of the list. If not specified, the list is of arbitrary // length. @Input() len!: number; - @Input() uiConfig!: {'add_element_text': string}; + @Input() uiConfig!: {add_element_text: string}; @Input() validators!: OppiaValidator[]; @Input() labelForFocusTarget!: string; addElementText!: string; @@ -77,7 +89,8 @@ implements ControlValueAccessor, Validator { // length. maxListLength!: number | null; onChange: (value: SchemaDefaultValue[]) => void = ( - (_: SchemaDefaultValue[]) => {}); + _: SchemaDefaultValue[] + ) => {}; isAddItemButtonPresent: boolean = false; isOneLineInput: boolean = false; @@ -100,8 +113,7 @@ implements ControlValueAccessor, Validator { this.onChange = fn; } - registerOnTouched(fn: () => void): void { - } + registerOnTouched(fn: () => void): void {} validate(control: AbstractControl): ValidationErrors { return {}; @@ -117,9 +129,9 @@ implements ControlValueAccessor, Validator { // sub-element 0 > 1 will have the same label as sub-element 1 > 0. // But we will assume (for now) that nested lists won't be used -- if // they are, this will need to be changed. - return ( - index === 0 ? this.baseFocusLabel : this.baseFocusLabel + index.toString() - ); + return index === 0 + ? this.baseFocusLabel + : this.baseFocusLabel + index.toString(); } private _deleteEmptyElements(): void { @@ -156,9 +168,11 @@ implements ControlValueAccessor, Validator { } this.localValue.push( - this.schemaDefaultValueService.getDefaultValue(this.itemSchema)); + this.schemaDefaultValueService.getDefaultValue(this.itemSchema) + ); this.focusManagerService.setFocus( - this.getFocusLabel(this.localValue.length - 1)); + this.getFocusLabel(this.localValue.length - 1) + ); // This is to prevent the autofocus behaviour of the input field to scroll // to top of the page when the user is adding a new element. this.focusManagerService.schemaBasedListEditorIsActive = true; @@ -166,12 +180,12 @@ implements ControlValueAccessor, Validator { private _deleteLastElementIfUndefined(): void { const lastValueIndex = this.localValue.length - 1; - const valueToConsiderUndefined = ( - this.schemaUndefinedLastElementService.getUndefinedValue( - this.itemSchema)); - if (this.localValue[lastValueIndex] === - valueToConsiderUndefined && - this.localValue[lastValueIndex] !== '') { + const valueToConsiderUndefined = + this.schemaUndefinedLastElementService.getUndefinedValue(this.itemSchema); + if ( + this.localValue[lastValueIndex] === valueToConsiderUndefined && + this.localValue[lastValueIndex] !== '' + ) { this.deleteElement(lastValueIndex); } } @@ -192,9 +206,11 @@ implements ControlValueAccessor, Validator { * the add item button is absent) then automatically add the * element to the list. */ - if ((this.maxListLength === null || - this.localValue.length < this.maxListLength) && - !!this.localValue[this.localValue.length - 1]) { + if ( + (this.maxListLength === null || + this.localValue.length < this.maxListLength) && + !!this.localValue[this.localValue.length - 1] + ) { this.addElement(); } } else { @@ -221,9 +237,9 @@ implements ControlValueAccessor, Validator { } ngOnInit(): void { - this.baseFocusLabel = ( + this.baseFocusLabel = this.labelForFocusTarget || - this.idGenerationService.generateNewId() + '-'); + this.idGenerationService.generateNewId() + '-'; this.isAddItemButtonPresent = true; this.addElementText = 'Add element'; if (this.uiConfig && this.uiConfig.add_element_text) { @@ -243,7 +259,8 @@ implements ControlValueAccessor, Validator { this.isOneLineInput = false; } else if ( this.itemSchema.ui_config.hasOwnProperty('rows') && - this.itemSchema.ui_config.rows > 2) { + this.itemSchema.ui_config.rows > 2 + ) { this.isOneLineInput = false; } } @@ -271,7 +288,8 @@ implements ControlValueAccessor, Validator { this.localValue.length < this.minListLength ) { this.localValue.push( - this.schemaDefaultValueService.getDefaultValue(this.itemSchema)); + this.schemaDefaultValueService.getDefaultValue(this.itemSchema) + ); } if (this.len === undefined) { @@ -288,18 +306,23 @@ implements ControlValueAccessor, Validator { ); } else { if (this.len <= 0) { - throw new Error( - 'Invalid length for list editor: ' + this.len); + throw new Error('Invalid length for list editor: ' + this.len); } if (this.len !== this.localValue.length) { throw new Error( 'List editor length does not match length of input value: ' + - this.len + ' ' + this.localValue); + this.len + + ' ' + + this.localValue + ); } } } } -angular.module('oppia').directive('schemaBasedListEditor', downgradeComponent({ - component: SchemaBasedListEditorComponent -}) as angular.IDirectiveFactory); +angular.module('oppia').directive( + 'schemaBasedListEditor', + downgradeComponent({ + component: SchemaBasedListEditorComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/components/forms/schema-based-editors/schema-based-unicode-editor.component.spec.ts b/core/templates/components/forms/schema-based-editors/schema-based-unicode-editor.component.spec.ts index 4640a9288694..e804913b289f 100644 --- a/core/templates/components/forms/schema-based-editors/schema-based-unicode-editor.component.spec.ts +++ b/core/templates/components/forms/schema-based-editors/schema-based-unicode-editor.component.spec.ts @@ -16,15 +16,26 @@ * @fileoverview Unit tests for Schema Based Unicode Editor Component */ -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { FormControl, FormsModule } from '@angular/forms'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { TranslateFakeLoader, TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; -import { StateCustomizationArgsService } from 'components/state-editor/state-editor-properties-services/state-customization-args.service'; -import { DeviceInfoService } from 'services/contextual/device-info.service'; -import { SchemaFormSubmittedService } from 'services/schema-form-submitted.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { SchemaBasedUnicodeEditor } from './schema-based-unicode-editor.component'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import {FormControl, FormsModule} from '@angular/forms'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import { + TranslateFakeLoader, + TranslateLoader, + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; +import {StateCustomizationArgsService} from 'components/state-editor/state-editor-properties-services/state-customization-args.service'; +import {DeviceInfoService} from 'services/contextual/device-info.service'; +import {SchemaFormSubmittedService} from 'services/schema-form-submitted.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {SchemaBasedUnicodeEditor} from './schema-based-unicode-editor.component'; describe('Schema Based Unicode Editor', () => { let component: SchemaBasedUnicodeEditor; @@ -40,21 +51,19 @@ describe('Schema Based Unicode Editor', () => { TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: TranslateFakeLoader - } - }) - ], - declarations: [ - SchemaBasedUnicodeEditor + useClass: TranslateFakeLoader, + }, + }), ], + declarations: [SchemaBasedUnicodeEditor], providers: [ DeviceInfoService, FocusManagerService, SchemaFormSubmittedService, StateCustomizationArgsService, - TranslateService + TranslateService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -63,8 +72,9 @@ describe('Schema Based Unicode Editor', () => { component = fixture.componentInstance; deviceInfoService = TestBed.inject(DeviceInfoService); schemaFormSubmittedService = TestBed.inject(SchemaFormSubmittedService); - stateCustomizationArgsService = ( - TestBed.inject(StateCustomizationArgsService)); + stateCustomizationArgsService = TestBed.inject( + StateCustomizationArgsService + ); component.uiConfig = { rows: ['Row 1', 'Row 2'], @@ -88,15 +98,15 @@ describe('Schema Based Unicode Editor', () => { getDoc: () => { return { getCursor: () => {}, - setCursor: () => {} + setCursor: () => {}, }; - } + }, } as unknown as CodeMirror.Editor; spyOn(cm, 'replaceSelection'); component.ngOnInit(); component.registerOnTouched(null); - let mockFunction = function(value: string) { + let mockFunction = function (value: string) { return value; }; component.registerOnChange(mockFunction); @@ -117,22 +127,23 @@ describe('Schema Based Unicode Editor', () => { expect(component.codemirrorStatus).toBeTrue(); })); - it('should flip the codemirror status flag when form view is opened', - fakeAsync(() => { - let onSchemaBasedFormsShownEmitter = new EventEmitter(); - spyOnProperty(stateCustomizationArgsService, 'onSchemaBasedFormsShown') - .and.returnValue(onSchemaBasedFormsShownEmitter); + it('should flip the codemirror status flag when form view is opened', fakeAsync(() => { + let onSchemaBasedFormsShownEmitter = new EventEmitter(); + spyOnProperty( + stateCustomizationArgsService, + 'onSchemaBasedFormsShown' + ).and.returnValue(onSchemaBasedFormsShownEmitter); - component.ngOnInit(); - tick(200); + component.ngOnInit(); + tick(200); - expect(component.codemirrorStatus).toBeTrue(); + expect(component.codemirrorStatus).toBeTrue(); - onSchemaBasedFormsShownEmitter.emit(); - tick(200); + onSchemaBasedFormsShownEmitter.emit(); + tick(200); - expect(component.codemirrorStatus).toBeFalse(); - })); + expect(component.codemirrorStatus).toBeFalse(); + })); it('should get empty object on validating', () => { expect(component.validate(new FormControl(1))).toEqual(null); @@ -171,7 +182,8 @@ describe('Schema Based Unicode Editor', () => { }; expect(component.getPlaceholder()).toBe( - 'I18N_PLAYER_DEFAULT_MOBILE_PLACEHOLDER'); + 'I18N_PLAYER_DEFAULT_MOBILE_PLACEHOLDER' + ); component.uiConfig = undefined; @@ -181,12 +193,13 @@ describe('Schema Based Unicode Editor', () => { it('should submit form on keypress', () => { spyOn(schemaFormSubmittedService.onSubmittedSchemaBasedForm, 'emit'); let evt = new KeyboardEvent('', { - keyCode: 13 + keyCode: 13, }); component.onKeypress(evt); - expect(schemaFormSubmittedService.onSubmittedSchemaBasedForm.emit) - .toHaveBeenCalled(); + expect( + schemaFormSubmittedService.onSubmittedSchemaBasedForm.emit + ).toHaveBeenCalled(); }); }); diff --git a/core/templates/components/forms/schema-based-editors/schema-based-unicode-editor.component.ts b/core/templates/components/forms/schema-based-editors/schema-based-unicode-editor.component.ts index f9595faa0860..a17b7cdc21e9 100644 --- a/core/templates/components/forms/schema-based-editors/schema-based-unicode-editor.component.ts +++ b/core/templates/components/forms/schema-based-editors/schema-based-unicode-editor.component.ts @@ -20,21 +20,34 @@ // build to not complain. // TODO(#16309): Fix relative imports. import '../../../third-party-imports/ui-codemirror.import'; -import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'; -import { AbstractControl, ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator } from '@angular/forms'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { TranslateService } from '@ngx-translate/core'; +import { + Component, + EventEmitter, + forwardRef, + Input, + OnInit, + Output, +} from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, +} from '@angular/forms'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {TranslateService} from '@ngx-translate/core'; import CodeMirror from 'codemirror'; import 'components/code-mirror/codemirror.component'; -import { StateCustomizationArgsService } from 'components/state-editor/state-editor-properties-services/state-customization-args.service'; -import { Subscription } from 'rxjs'; -import { DeviceInfoService } from 'services/contextual/device-info.service'; -import { SchemaDefaultValue } from 'services/schema-default-value.service'; -import { SchemaFormSubmittedService } from 'services/schema-form-submitted.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { validate } from 'components/forms/validators/schema-validators'; -import { Validator as OppiaValidator } from 'interactions/TextInput/directives/text-input-validation.service'; - +import {StateCustomizationArgsService} from 'components/state-editor/state-editor-properties-services/state-customization-args.service'; +import {Subscription} from 'rxjs'; +import {DeviceInfoService} from 'services/contextual/device-info.service'; +import {SchemaDefaultValue} from 'services/schema-default-value.service'; +import {SchemaFormSubmittedService} from 'services/schema-form-submitted.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {validate} from 'components/forms/validators/schema-validators'; +import {Validator as OppiaValidator} from 'interactions/TextInput/directives/text-input-validation.service'; @Component({ selector: 'schema-based-unicode-editor', @@ -43,26 +56,31 @@ import { Validator as OppiaValidator } from 'interactions/TextInput/directives/t { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SchemaBasedUnicodeEditor), - multi: true + multi: true, }, { provide: NG_VALIDATORS, useExisting: forwardRef(() => SchemaBasedUnicodeEditor), - multi: true + multi: true, }, - ] + ], }) export class SchemaBasedUnicodeEditor -implements ControlValueAccessor, OnInit, Validator { + implements ControlValueAccessor, OnInit, Validator +{ @Output() inputBlur: EventEmitter = new EventEmitter(); @Output() inputFocus: EventEmitter = new EventEmitter(); // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @Input() disabled!: boolean; - @Input() uiConfig!: { - rows: string[]; placeholder: string; 'coding_mode': string; - } | undefined; + @Input() uiConfig!: + | { + rows: string[]; + placeholder: string; + coding_mode: string; + } + | undefined; @Input() validators!: OppiaValidator[]; @Input() labelForFocusTarget!: string; @@ -71,30 +89,31 @@ implements ControlValueAccessor, OnInit, Validator { directiveSubscriptions = new Subscription(); codemirrorStatus: boolean = false; codemirrorOptions: { - extraKeys: { Tab: (cm: CodeMirror.Editor) => void }; + extraKeys: {Tab: (cm: CodeMirror.Editor) => void}; indentWithTabs: boolean; lineNumbers: boolean; readOnly?: string; mode?: string; } = { // Convert tabs to spaces. - extraKeys: { - Tab: (cm) => { - var spaces = Array( - // This throws "Object is possibly undefined." The type undefined - // comes here from code mirror dependency. We need to suppress this - // error because of strict type checking. - // @ts-ignore - cm.getOption('indentUnit') + 1).join(' '); - cm.replaceSelection(spaces); - // Move the cursor to the end of the selection. - var endSelectionPos = cm.getDoc().getCursor('head'); - cm.getDoc().setCursor(endSelectionPos); - } + extraKeys: { + Tab: cm => { + var spaces = Array( + // This throws "Object is possibly undefined." The type undefined + // comes here from code mirror dependency. We need to suppress this + // error because of strict type checking. + // @ts-ignore + cm.getOption('indentUnit') + 1 + ).join(' '); + cm.replaceSelection(spaces); + // Move the cursor to the end of the selection. + var endSelectionPos = cm.getDoc().getCursor('head'); + cm.getDoc().setCursor(endSelectionPos); }, - indentWithTabs: false, - lineNumbers: true - }; + }, + indentWithTabs: false, + lineNumbers: true, + }; constructor( private deviceInfoService: DeviceInfoService, @@ -118,8 +137,7 @@ implements ControlValueAccessor, OnInit, Validator { this.onChange = fn; } - registerOnTouched(fn: SchemaDefaultValue): void { - } + registerOnTouched(fn: SchemaDefaultValue): void {} validate(control: AbstractControl): ValidationErrors | null { return validate(control, this.validators); @@ -157,7 +175,8 @@ implements ControlValueAccessor, OnInit, Validator { setTimeout(() => { this.codemirrorStatus = !this.codemirrorStatus; }, 200); - }) + } + ) ); } } @@ -172,10 +191,13 @@ implements ControlValueAccessor, OnInit, Validator { if (!this.uiConfig) { return ''; } else { - if (!this.uiConfig.placeholder && - this.deviceInfoService.hasTouchEvents()) { + if ( + !this.uiConfig.placeholder && + this.deviceInfoService.hasTouchEvents() + ) { return this.translateService.instant( - 'I18N_PLAYER_DEFAULT_MOBILE_PLACEHOLDER'); + 'I18N_PLAYER_DEFAULT_MOBILE_PLACEHOLDER' + ); } return this.uiConfig.placeholder; } @@ -205,6 +227,6 @@ implements ControlValueAccessor, OnInit, Validator { angular.module('oppia').directive( 'schemaBasedUnicodeEditor', downgradeComponent({ - component: SchemaBasedUnicodeEditor + component: SchemaBasedUnicodeEditor, }) ); diff --git a/core/templates/components/forms/shared-forms.module.ts b/core/templates/components/forms/shared-forms.module.ts index 2d9bbfb852fc..670868b35f38 100644 --- a/core/templates/components/forms/shared-forms.module.ts +++ b/core/templates/components/forms/shared-forms.module.ts @@ -18,35 +18,35 @@ import 'core-js/es7/reflect'; import 'zone.js'; -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { NgbTooltipModule, NgbModalModule } from '@ng-bootstrap/ng-bootstrap'; -import { DynamicContentModule } from 'components/interaction-display/dynamic-content.module'; -import { MaterialModule } from 'modules/material.module'; -import { SharedPipesModule } from 'filters/shared-pipes.module'; -import { TranslateModule } from '@ngx-translate/core'; -import { CustomFormsComponentsModule } from './custom-forms-directives/custom-form-components.module'; -import { SchemaBasedEditorComponent } from './schema-based-editors/schema-based-editor.component'; -import { AudioSliderComponent } from './slider/audio-slider.component'; -import { ObjectEditorComponent } from './custom-forms-directives/object-editor.directive'; -import { DirectivesModule } from 'directives/directives.module'; -import { SchemaBasedIntEditorComponent } from './schema-based-editors/schema-based-int-editor.component'; -import { ApplyValidationDirective } from './custom-forms-directives/apply-validation.directive'; -import { MatInputModule } from '@angular/material/input'; -import { SchemaBasedFloatEditorComponent } from './schema-based-editors/schema-based-float-editor.component'; -import { SchemaBasedBoolEditorComponent } from './schema-based-editors/schema-based-bool-editor.component'; -import { SchemaBasedChoicesEditorComponent } from './schema-based-editors/schema-based-choices-editor.component'; -import { SchemaBasedCustomEditorComponent } from './schema-based-editors/schema-based-custom-editor.component'; -import { SchemaBasedDictEditorComponent } from './schema-based-editors/schema-based-dict-editor.component'; -import { SchemaBasedHtmlEditorComponent } from './schema-based-editors/schema-based-html-editor.component'; -import { OppiaCkEditor4Module } from 'components/ck-editor-helpers/ckeditor4.module'; -import { MarkAudioAsNeedingUpdateModalComponent } from 'components/forms/forms-templates/mark-audio-as-needing-update-modal.component'; -import { SchemaBasedListEditorComponent } from './schema-based-editors/schema-based-list-editor.component'; -import { SchemaBasedExpressionEditorComponent } from './schema-based-editors/schema-based-expression-editor.component'; -import { SchemaBasedUnicodeEditor } from './schema-based-editors/schema-based-unicode-editor.component'; -import { CodeMirrorModule } from 'components/code-mirror/codemirror.module'; -import { MarkTranslationsAsNeedingUpdateModalComponent } from './forms-templates/mark-translations-as-needing-update-modal.component'; +import {CommonModule} from '@angular/common'; +import {NgModule} from '@angular/core'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {NgbTooltipModule, NgbModalModule} from '@ng-bootstrap/ng-bootstrap'; +import {DynamicContentModule} from 'components/interaction-display/dynamic-content.module'; +import {MaterialModule} from 'modules/material.module'; +import {SharedPipesModule} from 'filters/shared-pipes.module'; +import {TranslateModule} from '@ngx-translate/core'; +import {CustomFormsComponentsModule} from './custom-forms-directives/custom-form-components.module'; +import {SchemaBasedEditorComponent} from './schema-based-editors/schema-based-editor.component'; +import {AudioSliderComponent} from './slider/audio-slider.component'; +import {ObjectEditorComponent} from './custom-forms-directives/object-editor.directive'; +import {DirectivesModule} from 'directives/directives.module'; +import {SchemaBasedIntEditorComponent} from './schema-based-editors/schema-based-int-editor.component'; +import {ApplyValidationDirective} from './custom-forms-directives/apply-validation.directive'; +import {MatInputModule} from '@angular/material/input'; +import {SchemaBasedFloatEditorComponent} from './schema-based-editors/schema-based-float-editor.component'; +import {SchemaBasedBoolEditorComponent} from './schema-based-editors/schema-based-bool-editor.component'; +import {SchemaBasedChoicesEditorComponent} from './schema-based-editors/schema-based-choices-editor.component'; +import {SchemaBasedCustomEditorComponent} from './schema-based-editors/schema-based-custom-editor.component'; +import {SchemaBasedDictEditorComponent} from './schema-based-editors/schema-based-dict-editor.component'; +import {SchemaBasedHtmlEditorComponent} from './schema-based-editors/schema-based-html-editor.component'; +import {OppiaCkEditor4Module} from 'components/ck-editor-helpers/ckeditor4.module'; +import {MarkAudioAsNeedingUpdateModalComponent} from 'components/forms/forms-templates/mark-audio-as-needing-update-modal.component'; +import {SchemaBasedListEditorComponent} from './schema-based-editors/schema-based-list-editor.component'; +import {SchemaBasedExpressionEditorComponent} from './schema-based-editors/schema-based-expression-editor.component'; +import {SchemaBasedUnicodeEditor} from './schema-based-editors/schema-based-unicode-editor.component'; +import {CodeMirrorModule} from 'components/code-mirror/codemirror.module'; +import {MarkTranslationsAsNeedingUpdateModalComponent} from './forms-templates/mark-translations-as-needing-update-modal.component'; @NgModule({ imports: [ @@ -63,7 +63,7 @@ import { MarkTranslationsAsNeedingUpdateModalComponent } from './forms-templates NgbModalModule, ReactiveFormsModule, SharedPipesModule, - TranslateModule + TranslateModule, ], declarations: [ AudioSliderComponent, @@ -81,7 +81,7 @@ import { MarkTranslationsAsNeedingUpdateModalComponent } from './forms-templates SchemaBasedIntEditorComponent, SchemaBasedListEditorComponent, SchemaBasedUnicodeEditor, - ObjectEditorComponent + ObjectEditorComponent, ], entryComponents: [ AudioSliderComponent, @@ -117,8 +117,7 @@ import { MarkTranslationsAsNeedingUpdateModalComponent } from './forms-templates SchemaBasedUnicodeEditor, MarkTranslationsAsNeedingUpdateModalComponent, MarkAudioAsNeedingUpdateModalComponent, - ObjectEditorComponent + ObjectEditorComponent, ], }) - -export class SharedFormsModule { } +export class SharedFormsModule {} diff --git a/core/templates/components/forms/slider/audio-slider.component.spec.ts b/core/templates/components/forms/slider/audio-slider.component.spec.ts index d522f1a25594..5c1fc9fc3dab 100644 --- a/core/templates/components/forms/slider/audio-slider.component.spec.ts +++ b/core/templates/components/forms/slider/audio-slider.component.spec.ts @@ -16,9 +16,14 @@ * @fileoverview Unit tests for audio slider component. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing'; -import { AudioSliderComponent } from './audio-slider.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import {AudioSliderComponent} from './audio-slider.component'; describe('Audio Slider Component', () => { let fixture: ComponentFixture; @@ -27,7 +32,7 @@ describe('Audio Slider Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [AudioSliderComponent], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -38,7 +43,7 @@ describe('Audio Slider Component', () => { it('should emit value when slider value changes', fakeAsync(() => { const valueChangeSpy = spyOn(component.valueChange, 'emit'); - const eventPayload = { value: 5 }; + const eventPayload = {value: 5}; component.setDuration(eventPayload); expect(valueChangeSpy).toHaveBeenCalledWith(eventPayload); })); diff --git a/core/templates/components/forms/slider/audio-slider.component.ts b/core/templates/components/forms/slider/audio-slider.component.ts index 722e043f8207..9e2283fbe737 100644 --- a/core/templates/components/forms/slider/audio-slider.component.ts +++ b/core/templates/components/forms/slider/audio-slider.component.ts @@ -16,8 +16,8 @@ * @fileoverview Wrapper over mat-slider for audio-bar. */ -import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component, EventEmitter, Input, Output} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; @Component({ selector: 'oppia-audio-slider', @@ -30,14 +30,18 @@ import { downgradeComponent } from '@angular/upgrade/static'; (change)="setDuration($event)" tick-interval="auto" [step]="1" - aria-label="audio-slider"> + aria-label="audio-slider" + > `, - styles: [` - .mat-accent /deep/ .mat-slider-track-fill, - .mat-accent /deep/ .mat-slider-thumb, - .mat-accent /deep/ .mat-slider-thumb-label { - background: #ff4081; - }`] + styles: [ + ` + .mat-accent /deep/ .mat-slider-track-fill, + .mat-accent /deep/ .mat-slider-thumb, + .mat-accent /deep/ .mat-slider-thumb-label { + background: #ff4081; + } + `, + ], }) export class AudioSliderComponent { // These properties are initialized using component interactions @@ -46,14 +50,17 @@ export class AudioSliderComponent { @Input() value!: number; @Input() max!: number; @Input() thumbLabel = false; - @Output() valueChange = new EventEmitter<{ value: number }>(); - constructor() { } + @Output() valueChange = new EventEmitter<{value: number}>(); + constructor() {} setDuration(event: {value: number}): void { this.valueChange.emit(event); } } -angular.module('oppia').directive('oppiaAudioSlider', downgradeComponent({ - component: AudioSliderComponent -}) as angular.IDirectiveFactory); +angular.module('oppia').directive( + 'oppiaAudioSlider', + downgradeComponent({ + component: AudioSliderComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/components/forms/validators/is-float.filter.spec.ts b/core/templates/components/forms/validators/is-float.filter.spec.ts index 899d6e17103c..4203e5c1ce5b 100644 --- a/core/templates/components/forms/validators/is-float.filter.spec.ts +++ b/core/templates/components/forms/validators/is-float.filter.spec.ts @@ -18,57 +18,65 @@ // TODO(#7222): Remove the following block of unnnecessary imports once // the code corresponding to the spec is upgraded to Angular 8. -import { UpgradedServices } from 'services/UpgradedServices'; +import {UpgradedServices} from 'services/UpgradedServices'; // ^^^ This block is to be removed. require('components/forms/validators/is-float.filter.ts'); -describe('Normalizer tests', function() { +describe('Normalizer tests', function () { var filterName = 'isFloat'; beforeEach(angular.mock.module('oppia')); - beforeEach(angular.mock.module('oppia', function($provide) { - var ugs = new UpgradedServices(); - for (let [key, value] of Object.entries(ugs.getUpgradedServices())) { - $provide.value(key, value); - } - })); + beforeEach( + angular.mock.module('oppia', function ($provide) { + var ugs = new UpgradedServices(); + for (let [key, value] of Object.entries(ugs.getUpgradedServices())) { + $provide.value(key, value); + } + }) + ); - it('should have the relevant filters', angular.mock.inject(function($filter) { - expect($filter(filterName)).not.toEqual(null); - })); + it( + 'should have the relevant filters', + angular.mock.inject(function ($filter) { + expect($filter(filterName)).not.toEqual(null); + }) + ); - it('should validate floats correctly', angular.mock.inject(function($filter) { - var filter = $filter('isFloat'); - expect(filter('1.23')).toEqual(1.23); - expect(filter('-1.23')).toEqual(-1.23); - expect(filter('0')).toEqual(0); - expect(filter('-1')).toEqual(-1); - expect(filter('-1.0')).toEqual(-1); - expect(filter('1,5')).toEqual(1.5); - expect(filter('1%')).toEqual(0.01); - expect(filter('1.5%')).toEqual(0.015); - expect(filter('-5%')).toEqual(-0.05); - expect(filter('.35')).toEqual(0.35); - expect(filter(',3')).toEqual(0.3); - expect(filter('.3%')).toEqual(0.003); - expect(filter('2,5%')).toEqual(0.025); - expect(filter('3.2% ')).toEqual(0.032); - expect(filter(' 3.2% ')).toEqual(0.032); - expect(filter('0.')).toEqual(0); + it( + 'should validate floats correctly', + angular.mock.inject(function ($filter) { + var filter = $filter('isFloat'); + expect(filter('1.23')).toEqual(1.23); + expect(filter('-1.23')).toEqual(-1.23); + expect(filter('0')).toEqual(0); + expect(filter('-1')).toEqual(-1); + expect(filter('-1.0')).toEqual(-1); + expect(filter('1,5')).toEqual(1.5); + expect(filter('1%')).toEqual(0.01); + expect(filter('1.5%')).toEqual(0.015); + expect(filter('-5%')).toEqual(-0.05); + expect(filter('.35')).toEqual(0.35); + expect(filter(',3')).toEqual(0.3); + expect(filter('.3%')).toEqual(0.003); + expect(filter('2,5%')).toEqual(0.025); + expect(filter('3.2% ')).toEqual(0.032); + expect(filter(' 3.2% ')).toEqual(0.032); + expect(filter('0.')).toEqual(0); - expect(filter('3%%')).toBeUndefined(); - expect(filter('-')).toBeUndefined(); - expect(filter('.')).toBeUndefined(); - expect(filter(',')).toBeUndefined(); - expect(filter('5%,')).toBeUndefined(); - expect(filter('')).toBeUndefined(); - expect(filter('1.23a')).toBeUndefined(); - expect(filter('abc')).toBeUndefined(); - expect(filter('2+3')).toBeUndefined(); - expect(filter('--1.23')).toBeUndefined(); - expect(filter('=1.23')).toBeUndefined(); - expect(filter(undefined)).toBeUndefined(); - })); + expect(filter('3%%')).toBeUndefined(); + expect(filter('-')).toBeUndefined(); + expect(filter('.')).toBeUndefined(); + expect(filter(',')).toBeUndefined(); + expect(filter('5%,')).toBeUndefined(); + expect(filter('')).toBeUndefined(); + expect(filter('1.23a')).toBeUndefined(); + expect(filter('abc')).toBeUndefined(); + expect(filter('2+3')).toBeUndefined(); + expect(filter('--1.23')).toBeUndefined(); + expect(filter('=1.23')).toBeUndefined(); + expect(filter(undefined)).toBeUndefined(); + }) + ); }); diff --git a/core/templates/components/forms/validators/is-float.filter.ts b/core/templates/components/forms/validators/is-float.filter.ts index 6c98281e0c80..65844286d067 100644 --- a/core/templates/components/forms/validators/is-float.filter.ts +++ b/core/templates/components/forms/validators/is-float.filter.ts @@ -16,36 +16,40 @@ * @fileoverview Validator to check if input is float. */ -angular.module('oppia').filter('isFloat', [function() { - return function(input: { toString: () => string }) { - var FLOAT_REGEXP = /(?=.*\d)^\-?\d*(\.|\,)?\d*\%?$/; - // This regex accepts floats in the following formats: - // 0. - // 0.55.. - // -0.55.. - // .555.. - // -.555.. - // All examples above with '.' replaced with ',' are also valid. - // Expressions containing % are also valid (5.1% etc). +angular.module('oppia').filter('isFloat', [ + function () { + return function (input: {toString: () => string}) { + var FLOAT_REGEXP = /(?=.*\d)^\-?\d*(\.|\,)?\d*\%?$/; + // This regex accepts floats in the following formats: + // 0. + // 0.55.. + // -0.55.. + // .555.. + // -.555.. + // All examples above with '.' replaced with ',' are also valid. + // Expressions containing % are also valid (5.1% etc). - var viewValue = ''; - try { - viewValue = input.toString().trim(); - } catch (e) { - return undefined; - } + var viewValue = ''; + try { + viewValue = input.toString().trim(); + } catch (e) { + return undefined; + } - if (viewValue !== '' && FLOAT_REGEXP.test(viewValue)) { - if (viewValue.slice(-1) === '%') { - // This is a percentage, so the input needs to be divided by 100. - return parseFloat( - viewValue.substring(0, viewValue.length - 1).replace(',', '.') - ) / 100.0; + if (viewValue !== '' && FLOAT_REGEXP.test(viewValue)) { + if (viewValue.slice(-1) === '%') { + // This is a percentage, so the input needs to be divided by 100. + return ( + parseFloat( + viewValue.substring(0, viewValue.length - 1).replace(',', '.') + ) / 100.0 + ); + } else { + return parseFloat(viewValue.replace(',', '.')); + } } else { - return parseFloat(viewValue.replace(',', '.')); + return undefined; } - } else { - return undefined; - } - }; -}]); + }; + }, +]); diff --git a/core/templates/components/forms/validators/schema-validators.spec.ts b/core/templates/components/forms/validators/schema-validators.spec.ts index 48501e3c5524..bfa589c30511 100644 --- a/core/templates/components/forms/validators/schema-validators.spec.ts +++ b/core/templates/components/forms/validators/schema-validators.spec.ts @@ -16,9 +16,9 @@ * @fileoverview Tests for schema-validators. */ -import { AbstractControl, ValidationErrors } from '@angular/forms'; -import { SchemaDefaultValue } from 'services/schema-default-value.service'; -import { SchemaValidators } from './schema-validators'; +import {AbstractControl, ValidationErrors} from '@angular/forms'; +import {SchemaDefaultValue} from 'services/schema-default-value.service'; +import {SchemaValidators} from './schema-validators'; class MockFormControl extends AbstractControl { value: SchemaDefaultValue = '1'; @@ -43,29 +43,29 @@ describe('Schema validators', () => { control.setValue('1'); const args = { - minValue: 3 + minValue: 3, }; const testCases = [ - { controlValue: '12', expectedResult: false }, - { controlValue: '123', expectedResult: true }, - { controlValue: '1234', expectedResult: true }, - { controlValue: ['1', '2'], expectedResult: false }, - { controlValue: undefined, expectedResult: false }, - { controlValue: ['1', '2', '3'], expectedResult: true }, - { controlValue: ['1', '2', '3', '4'], expectedResult: true }, + {controlValue: '12', expectedResult: false}, + {controlValue: '123', expectedResult: true}, + {controlValue: '1234', expectedResult: true}, + {controlValue: ['1', '2'], expectedResult: false}, + {controlValue: undefined, expectedResult: false}, + {controlValue: ['1', '2', '3'], expectedResult: true}, + {controlValue: ['1', '2', '3', '4'], expectedResult: true}, ]; const filter = SchemaValidators.hasLengthAtLeast(args); - testCases.forEach((testCase) => { + testCases.forEach(testCase => { control.setValue(testCase.controlValue); const errorsReturned = filter(control); if (testCase.expectedResult === true) { expect(errorsReturned).toBe(null, testCase.toString()); return; } - expect( - errorsReturned.hasLengthAtLeast - ).withContext(testCase.toString()).toBeDefined(); + expect(errorsReturned.hasLengthAtLeast) + .withContext(testCase.toString()) + .toBeDefined(); }); }); it('should throw an error when the value is not a string', () => { @@ -73,12 +73,13 @@ describe('Schema validators', () => { control.setValue(1); const args = { - minValue: 3 + minValue: 3, }; const filter = SchemaValidators.hasLengthAtLeast(args); expect(() => filter(control)).toThrowError( - 'Invalid value passed in control. Expecting a string or Array'); + 'Invalid value passed in control. Expecting a string or Array' + ); }); }); describe('when validating "has-length-at-most"', () => { @@ -87,44 +88,44 @@ describe('Schema validators', () => { control.setValue('1'); const args = { - maxValue: 3 + maxValue: 3, }; const testCases = [ - { controlValue: '12', expectedResult: true}, - { controlValue: '123', expectedResult: true}, - { controlValue: '1234', expectedResult: false}, - { controlValue: undefined, expectedResult: false}, - { controlValue: ['1', '2'], expectedResult: true}, - { controlValue: ['1', '2', '3'], expectedResult: true}, - { controlValue: ['1', '2', '3', '4'], expectedResult: false}, + {controlValue: '12', expectedResult: true}, + {controlValue: '123', expectedResult: true}, + {controlValue: '1234', expectedResult: false}, + {controlValue: undefined, expectedResult: false}, + {controlValue: ['1', '2'], expectedResult: true}, + {controlValue: ['1', '2', '3'], expectedResult: true}, + {controlValue: ['1', '2', '3', '4'], expectedResult: false}, ]; const filter = SchemaValidators.hasLengthAtMost(args); - testCases.forEach((testCase) => { + testCases.forEach(testCase => { control.setValue(testCase.controlValue); const errorsReturned = filter(control); if (testCase.expectedResult === true) { expect(errorsReturned).toBe(null, testCase.toString()); return; } - expect( - errorsReturned.hasLengthAtMost - ).withContext(testCase.toString()).toBeDefined(); + expect(errorsReturned.hasLengthAtMost) + .withContext(testCase.toString()) + .toBeDefined(); }); - } - ); + }); it('should throw an error when the value is not a string', () => { const control: MockFormControl = new MockFormControl([], []); control.setValue(1); const args = { - maxValue: 3 + maxValue: 3, }; const filter = SchemaValidators.hasLengthAtMost(args); expect(() => filter(control)).toThrowError( - 'Invalid value passed in control. Expecting a string or Array'); + 'Invalid value passed in control. Expecting a string or Array' + ); }); }); @@ -134,31 +135,30 @@ describe('Schema validators', () => { control.setValue(1); const args = { - minValue: -2.0 + minValue: -2.0, }; const testCases = [ {controlValue: 1.23, expectedResult: true}, {controlValue: -1.23, expectedResult: true}, - {controlValue: -1.99, expectedResult: true }, + {controlValue: -1.99, expectedResult: true}, {controlValue: -2, expectedResult: true}, {controlValue: -2.01, expectedResult: false}, {controlValue: -3, expectedResult: false}, ]; const filter = SchemaValidators.isAtLeast(args); - testCases.forEach((testCase) => { + testCases.forEach(testCase => { control.setValue(testCase.controlValue); const errorsReturned = filter(control); if (testCase.expectedResult === true) { expect(errorsReturned).toBe(null, testCase.toString()); return; } - expect( - errorsReturned.isAtLeast - ).withContext(testCase.toString()).toBeDefined(); + expect(errorsReturned.isAtLeast) + .withContext(testCase.toString()) + .toBeDefined(); }); - } - ); + }); }); describe('when validating "is-at-most"', () => { @@ -167,7 +167,7 @@ describe('Schema validators', () => { control.setValue(1); const args = { - maxValue: -2.0 + maxValue: -2.0, }; const testCases = [ @@ -179,19 +179,18 @@ describe('Schema validators', () => { {controlValue: -3, expectedResult: true}, ]; const filter = SchemaValidators.isAtMost(args); - testCases.forEach((testCase) => { + testCases.forEach(testCase => { control.setValue(testCase.controlValue); const errorsReturned = filter(control); if (testCase.expectedResult === true) { expect(errorsReturned).toBe(null, testCase.toString()); return; } - expect( - errorsReturned.isAtMost - ).withContext(testCase.toString()).toBeDefined(); + expect(errorsReturned.isAtMost) + .withContext(testCase.toString()) + .toBeDefined(); }); - } - ); + }); }); describe('when validating float', () => { @@ -199,7 +198,6 @@ describe('Schema validators', () => { const control: MockFormControl = new MockFormControl([], []); control.setValue(1); - const testCases = [ {controlValue: '1.23', expectedResult: true}, {controlValue: '-1.23', expectedResult: true}, @@ -228,10 +226,10 @@ describe('Schema validators', () => { {controlValue: 'abc', expectedResult: false}, {controlValue: '2+3', expectedResult: false}, {controlValue: '--1.23', expectedResult: false}, - {controlValue: '=1.23', expectedResult: false} + {controlValue: '=1.23', expectedResult: false}, ]; const filter = SchemaValidators.isFloat(); - testCases.forEach((testCase) => { + testCases.forEach(testCase => { control.setValue(testCase.controlValue); const errorsReturned = filter(control); if (testCase.expectedResult === true) { @@ -241,12 +239,11 @@ describe('Schema validators', () => { if (errorsReturned === null) { throw new Error(testCase.controlValue); } - expect( - errorsReturned.isFloat - ).withContext(testCase.toString()).toBeDefined(); + expect(errorsReturned.isFloat) + .withContext(testCase.toString()) + .toBeDefined(); }); - } - ); + }); }); describe('when validating integer', () => { @@ -262,19 +259,18 @@ describe('Schema validators', () => { ]; const filter = SchemaValidators.isInteger(); - testCases.forEach((testCase) => { + testCases.forEach(testCase => { control.setValue(testCase.controlValue); const errorsReturned = filter(control); if (testCase.expectedResult === true) { expect(errorsReturned).toBe(null, testCase.toString()); return; } - expect( - errorsReturned.isInteger - ).withContext(testCase.toString()).toBeDefined(); + expect(errorsReturned.isInteger) + .withContext(testCase.toString()) + .toBeDefined(); }); - } - ); + }); }); describe('when validating non-empty', () => { @@ -284,30 +280,29 @@ describe('Schema validators', () => { const testCases = [ {controlValue: 'a', expectedResult: true}, - {controlValue: '', expectedResult: false} + {controlValue: '', expectedResult: false}, ]; const filter = SchemaValidators.isNonempty(); - testCases.forEach((testCase) => { + testCases.forEach(testCase => { control.setValue(testCase.controlValue); const errorsReturned = filter(control); if (testCase.expectedResult === true) { expect(errorsReturned).toBe(null, testCase.toString()); return; } - expect( - errorsReturned.isNonempty - ).withContext(testCase.toString()).toBeDefined(); + expect(errorsReturned.isNonempty) + .withContext(testCase.toString()) + .toBeDefined(); }); - } - ); + }); }); describe('when validating isRegexMatched', () => { let filter!: (control: AbstractControl) => ValidationErrors | null; const control: MockFormControl = new MockFormControl([], []); const errorMsg = { - isRegexMatched: 'Control Value doesn\'t match given regex' + isRegexMatched: "Control Value doesn't match given regex", }; const getFilter = (regex: string): typeof filter => { @@ -323,41 +318,39 @@ describe('Schema validators', () => { control.setValue(controlValue); expect(filter(control)).toEqual(errorMsg); }; - it('should pass if the string matches the given regular expression', - () => { - filter = getFilter('a.$'); - expectValidationToPass('a '); - expectValidationToPass('a$'); - expectValidationToPass('a2'); - - filter = getFilter('g(oog)+le'); - expectValidationToPass('google '); - expectValidationToPass('googoogle'); - expectValidationToPass('googoogoogoogle'); - - filter = getFilter('(^https:\\/\\/.*)|(^(?!.*:\\/\\/)(.*))'); - expectValidationToPass('https://'); - expectValidationToPass('https://any-string'); - expectValidationToPass('https://www.oppia.com'); - expectValidationToPass('www.oppia.com'); - }); + it('should pass if the string matches the given regular expression', () => { + filter = getFilter('a.$'); + expectValidationToPass('a '); + expectValidationToPass('a$'); + expectValidationToPass('a2'); + + filter = getFilter('g(oog)+le'); + expectValidationToPass('google '); + expectValidationToPass('googoogle'); + expectValidationToPass('googoogoogoogle'); + + filter = getFilter('(^https:\\/\\/.*)|(^(?!.*:\\/\\/)(.*))'); + expectValidationToPass('https://'); + expectValidationToPass('https://any-string'); + expectValidationToPass('https://www.oppia.com'); + expectValidationToPass('www.oppia.com'); + }); - it('should fail if the string does not match the given regular expression', - () => { - filter = getFilter('a.$'); - expectValidationToFail('a'); - expectValidationToFail('a$a'); - expectValidationToFail('bb'); - - filter = getFilter('g(oog)+le'); - expectValidationToFail('gooogle '); - expectValidationToFail('gle'); - expectValidationToFail('goole'); - - filter = getFilter('(^https:\\/\\/.*)|(^(?!.*:\\/\\/)(.*))'); - expectValidationToFail('http://'); - expectValidationToFail('abc://www.oppia.com'); - }); + it('should fail if the string does not match the given regular expression', () => { + filter = getFilter('a.$'); + expectValidationToFail('a'); + expectValidationToFail('a$a'); + expectValidationToFail('bb'); + + filter = getFilter('g(oog)+le'); + expectValidationToFail('gooogle '); + expectValidationToFail('gle'); + expectValidationToFail('goole'); + + filter = getFilter('(^https:\\/\\/.*)|(^(?!.*:\\/\\/)(.*))'); + expectValidationToFail('http://'); + expectValidationToFail('abc://www.oppia.com'); + }); }); describe('when validating isUrlFragment', () => { @@ -366,7 +359,7 @@ describe('Schema validators', () => { beforeEach(() => { filter = SchemaValidators.isUrlFragment({ - charLimit: 20 + charLimit: 20, }); }); @@ -394,49 +387,48 @@ describe('Schema validators', () => { expect(filter(control)).toEqual(errorMsg); }); - it('should fail when there are special characters other than hyphen', - () => { - const control: MockFormControl = new MockFormControl([], []); - const testCases = [ - {controlValue: 'special~chars', expectedResult: false }, - {controlValue: 'special`chars', expectedResult: false }, - {controlValue: 'special!chars', expectedResult: false }, - {controlValue: 'special@chars', expectedResult: false }, - {controlValue: 'special#chars', expectedResult: false }, - {controlValue: 'special$chars', expectedResult: false }, - {controlValue: 'special%chars', expectedResult: false }, - {controlValue: 'special^chars', expectedResult: false }, - {controlValue: 'special&chars', expectedResult: false }, - {controlValue: 'special*chars', expectedResult: false }, - {controlValue: 'special(chars', expectedResult: false }, - {controlValue: 'special)chars', expectedResult: false }, - {controlValue: 'special_chars', expectedResult: false }, - {controlValue: 'special+chars', expectedResult: false }, - {controlValue: 'special=chars', expectedResult: false }, - {controlValue: 'special{chars', expectedResult: false }, - {controlValue: 'special}chars', expectedResult: false }, - {controlValue: 'special[chars', expectedResult: false }, - {controlValue: 'special]chars', expectedResult: false }, - {controlValue: 'special:chars', expectedResult: false }, - {controlValue: 'special;chars', expectedResult: false }, - {controlValue: 'special"chars', expectedResult: false }, - {controlValue: 'special\'chars', expectedResult: false }, - {controlValue: 'special|chars', expectedResult: false }, - {controlValue: 'specialchars', expectedResult: false }, - {controlValue: 'special.chars', expectedResult: false }, - {controlValue: 'special?chars', expectedResult: false }, - {controlValue: 'special/chars', expectedResult: false }, - {controlValue: 'special\\chars', expectedResult: false }, - ]; - testCases.forEach((testCase) => { - control.setValue(testCase.controlValue); - expect( - filter(control) - ).withContext(testCase.toString()).toEqual(errorMsg); - }); + it('should fail when there are special characters other than hyphen', () => { + const control: MockFormControl = new MockFormControl([], []); + const testCases = [ + {controlValue: 'special~chars', expectedResult: false}, + {controlValue: 'special`chars', expectedResult: false}, + {controlValue: 'special!chars', expectedResult: false}, + {controlValue: 'special@chars', expectedResult: false}, + {controlValue: 'special#chars', expectedResult: false}, + {controlValue: 'special$chars', expectedResult: false}, + {controlValue: 'special%chars', expectedResult: false}, + {controlValue: 'special^chars', expectedResult: false}, + {controlValue: 'special&chars', expectedResult: false}, + {controlValue: 'special*chars', expectedResult: false}, + {controlValue: 'special(chars', expectedResult: false}, + {controlValue: 'special)chars', expectedResult: false}, + {controlValue: 'special_chars', expectedResult: false}, + {controlValue: 'special+chars', expectedResult: false}, + {controlValue: 'special=chars', expectedResult: false}, + {controlValue: 'special{chars', expectedResult: false}, + {controlValue: 'special}chars', expectedResult: false}, + {controlValue: 'special[chars', expectedResult: false}, + {controlValue: 'special]chars', expectedResult: false}, + {controlValue: 'special:chars', expectedResult: false}, + {controlValue: 'special;chars', expectedResult: false}, + {controlValue: 'special"chars', expectedResult: false}, + {controlValue: "special'chars", expectedResult: false}, + {controlValue: 'special|chars', expectedResult: false}, + {controlValue: 'specialchars', expectedResult: false}, + {controlValue: 'special.chars', expectedResult: false}, + {controlValue: 'special?chars', expectedResult: false}, + {controlValue: 'special/chars', expectedResult: false}, + {controlValue: 'special\\chars', expectedResult: false}, + ]; + testCases.forEach(testCase => { + control.setValue(testCase.controlValue); + expect(filter(control)) + .withContext(testCase.toString()) + .toEqual(errorMsg); }); + }); it('should fail when there are spaces', () => { const control: MockFormControl = new MockFormControl([], []); @@ -446,22 +438,18 @@ describe('Schema validators', () => { expect(filter(control)).toEqual(errorMsg); }); - it( - 'should fail when the length of the input is greater than the char limit', - () => { - const control: MockFormControl = new MockFormControl([], []); - control.setValue('a-lengthy-url-fragment'); - expect(filter(control)).toEqual(errorMsg); - }); + it('should fail when the length of the input is greater than the char limit', () => { + const control: MockFormControl = new MockFormControl([], []); + control.setValue('a-lengthy-url-fragment'); + expect(filter(control)).toEqual(errorMsg); + }); - it('should pass when the passed value is a valid url fragment', - () => { - const control: MockFormControl = new MockFormControl([], []); - control.setValue('math'); - expect(filter(control)).toBe(null); - control.setValue('computer-sciencet'); - expect(filter(control)).toBe(null); - } - ); + it('should pass when the passed value is a valid url fragment', () => { + const control: MockFormControl = new MockFormControl([], []); + control.setValue('math'); + expect(filter(control)).toBe(null); + control.setValue('computer-sciencet'); + expect(filter(control)).toBe(null); + }); }); }); diff --git a/core/templates/components/forms/validators/schema-validators.ts b/core/templates/components/forms/validators/schema-validators.ts index efbd02f7bfb2..a2195f7a4796 100644 --- a/core/templates/components/forms/validators/schema-validators.ts +++ b/core/templates/components/forms/validators/schema-validators.ts @@ -16,18 +16,19 @@ * @fileoverview Group of all validators */ -import { AbstractControl, ValidationErrors } from '@angular/forms'; -import { AppConstants } from 'app.constants'; -import { Validator as OppiaValidator } from 'interactions/TextInput/directives/text-input-validation.service'; +import {AbstractControl, ValidationErrors} from '@angular/forms'; +import {AppConstants} from 'app.constants'; +import {Validator as OppiaValidator} from 'interactions/TextInput/directives/text-input-validation.service'; import cloneDeep from 'lodash/cloneDeep'; -import { UnderscoresToCamelCasePipe } from 'filters/string-utility-filters/underscores-to-camel-case.pipe'; +import {UnderscoresToCamelCasePipe} from 'filters/string-utility-filters/underscores-to-camel-case.pipe'; type ValidatorKeyType = keyof Omit; type ValidatorFunctionType = (typeof SchemaValidators)[ValidatorKeyType]; type FilterArgsType = Parameters[0]; export const validate = ( - control: AbstractControl, validators: OppiaValidator[] + control: AbstractControl, + validators: OppiaValidator[] ): ValidationErrors | null => { let underscoresToCamelCasePipe = new UnderscoresToCamelCasePipe(); if (!validators || validators.length === 0) { @@ -42,19 +43,15 @@ export const validate = ( const filterArgs = Object.fromEntries( Object.entries(validatorSpec) .filter(([key, _]) => key !== 'id') - .map( - ([key, value]) => { - return [ - underscoresToCamelCasePipe.transform(key), - cloneDeep(value) - ]; - }) + .map(([key, value]) => { + return [underscoresToCamelCasePipe.transform(key), cloneDeep(value)]; + }) ) as FilterArgsType; if (SchemaValidators[validatorName]) { const error = ( - SchemaValidators[ - validatorName - ] as (arg: FilterArgsType) => ReturnType + SchemaValidators[validatorName] as ( + arg: FilterArgsType + ) => ReturnType )(filterArgs)(control); if (error !== null) { errorsPresent = true; @@ -71,77 +68,84 @@ export const validate = ( }; export class SchemaValidators { - static hasLengthAtLeast( - args: {minValue: number} - ): (control: AbstractControl) => ValidationErrors | null { + static hasLengthAtLeast(args: { + minValue: number; + }): (control: AbstractControl) => ValidationErrors | null { return (control: AbstractControl): ValidationErrors | null => { const value = control.value; if (value === null || value === undefined) { return { - hasLengthAtLeast: {minValue: args.minValue, actual: control.value} + hasLengthAtLeast: {minValue: args.minValue, actual: control.value}, }; } if (!(typeof value === 'string' || Array.isArray(value))) { throw new Error( - 'Invalid value passed in control. Expecting a string or Array'); + 'Invalid value passed in control. Expecting a string or Array' + ); } if (value.length >= args.minValue) { return null; } return { hasLengthAtLeast: { - minValue: args.minValue, actual: control.value.length} + minValue: args.minValue, + actual: control.value.length, + }, }; }; } - static hasLengthAtMost( - args: {maxValue: number} - ): (control: AbstractControl) => ValidationErrors | null { + static hasLengthAtMost(args: { + maxValue: number; + }): (control: AbstractControl) => ValidationErrors | null { return (control: AbstractControl): ValidationErrors | null => { const value = control.value; if (value === null || value === undefined) { return { - hasLengthAtMost: {minValue: args.maxValue, actual: control.value} + hasLengthAtMost: {minValue: args.maxValue, actual: control.value}, }; } if (!(typeof value === 'string' || Array.isArray(value))) { throw new Error( - 'Invalid value passed in control. Expecting a string or Array'); + 'Invalid value passed in control. Expecting a string or Array' + ); } if (value.length <= args.maxValue) { return null; } return { - hasLengthAtMost: {maxValue: args.maxValue, actual: control.value.length} + hasLengthAtMost: { + maxValue: args.maxValue, + actual: control.value.length, + }, }; }; } - static isAtLeast( - args: {minValue: number} - ): (control: AbstractControl) => ValidationErrors | null { + static isAtLeast(args: { + minValue: number; + }): (control: AbstractControl) => ValidationErrors | null { return (control: AbstractControl): ValidationErrors | null => { const value = parseFloat(control.value); if (!isNaN(value) && value >= args.minValue) { return null; } return { - isAtLeast: {minValue: args.minValue, actual: control.value} + isAtLeast: {minValue: args.minValue, actual: control.value}, }; }; } - static isAtMost( - args: {maxValue: number} - ): (control: AbstractControl) => ValidationErrors | null { + static isAtMost(args: { + maxValue: number; + }): (control: AbstractControl) => ValidationErrors | null { return (control: AbstractControl): ValidationErrors | null => { const value = parseFloat(control.value); if (!isNaN(value) && value <= args.maxValue) { return null; } return { - isAtMost: {maxValue: args.maxValue, actual: control.value} + isAtMost: {maxValue: args.maxValue, actual: control.value}, }; }; } @@ -169,16 +173,21 @@ export class SchemaValidators { if (FLOAT_REGEXP.test(viewValue)) { if (viewValue.slice(-1) === '%') { // This is a percentage, so the input needs to be divided by 100. - return isNaN(parseFloat( - // TODO(#15455): Use the numeric-service to get the current - // decimal separator. - viewValue.substring(0, viewValue.length - 1).replace(',', '.') - ) / 100.0) ? {isFloat: 'Not float', actual: control.value} : null; + return isNaN( + parseFloat( + // TODO(#15455): Use the numeric-service to get the current + // decimal separator. + viewValue.substring(0, viewValue.length - 1).replace(',', '.') + ) / 100.0 + ) + ? {isFloat: 'Not float', actual: control.value} + : null; } else { // TODO(#15455): Use the numeric-service to get the current // decimal separator. - return isNaN(parseFloat(viewValue.replace(',', '.'))) ? - {isFloat: 'Not float', actual: control.value} : null; + return isNaN(parseFloat(viewValue.replace(',', '.'))) + ? {isFloat: 'Not float', actual: control.value} + : null; } } else { return {isFloat: 'Not float', actual: control.value}; @@ -206,24 +215,25 @@ export class SchemaValidators { }; } - static isRegexMatched( - args: {regexPattern: string} - ): (control: AbstractControl) => ValidationErrors | null { + static isRegexMatched(args: { + regexPattern: string; + }): (control: AbstractControl) => ValidationErrors | null { const re = new RegExp(args.regexPattern); return (control: AbstractControl): ValidationErrors | null => { if (control.value === null || re.test(control.value)) { return null; } - return {isRegexMatched: 'Control Value doesn\'t match given regex'}; + return {isRegexMatched: "Control Value doesn't match given regex"}; }; } - static isUrlFragment( - args: {charLimit: number} - ): (control: AbstractControl) => ValidationErrors | null { + static isUrlFragment(args: { + charLimit: number; + }): (control: AbstractControl) => ValidationErrors | null { return (control: AbstractControl) => { const VALID_URL_FRAGMENT_REGEX = new RegExp( - AppConstants.VALID_URL_FRAGMENT_REGEX); + AppConstants.VALID_URL_FRAGMENT_REGEX + ); if ( control.value && VALID_URL_FRAGMENT_REGEX.test(control.value) && diff --git a/core/templates/components/graph-services/graph-layout.service.spec.ts b/core/templates/components/graph-services/graph-layout.service.spec.ts index e0ad6fcfc195..815641425d27 100644 --- a/core/templates/components/graph-services/graph-layout.service.spec.ts +++ b/core/templates/components/graph-services/graph-layout.service.spec.ts @@ -16,11 +16,11 @@ * @fileoverview Unit test for StateGraphLayoutService. */ -import { TestBed } from '@angular/core/testing'; -import { AppConstants } from 'app.constants'; +import {TestBed} from '@angular/core/testing'; +import {AppConstants} from 'app.constants'; -import { GraphLink, GraphNodes } from 'services/compute-graph.service'; -import { StateGraphLayoutService } from './graph-layout.service'; +import {GraphLink, GraphNodes} from 'services/compute-graph.service'; +import {StateGraphLayoutService} from './graph-layout.service'; describe('Graph Layout Service', () => { let sgls: StateGraphLayoutService; @@ -72,7 +72,7 @@ describe('Graph Layout Service', () => { style: 'string', secondaryLabel: 'string', nodeClass: 'string', - canDelete: false + canDelete: false, }, State2: { depth: 1, @@ -90,7 +90,7 @@ describe('Graph Layout Service', () => { style: 'string', secondaryLabel: 'string', nodeClass: 'string', - canDelete: false + canDelete: false, }, State3: { depth: 1, @@ -108,7 +108,7 @@ describe('Graph Layout Service', () => { style: 'string', secondaryLabel: 'string', nodeClass: 'string', - canDelete: false + canDelete: false, }, State4: { depth: 4, @@ -126,7 +126,7 @@ describe('Graph Layout Service', () => { style: 'string', secondaryLabel: 'string', nodeClass: 'string', - canDelete: false + canDelete: false, }, State5: { depth: 1, @@ -144,7 +144,7 @@ describe('Graph Layout Service', () => { style: 'string', secondaryLabel: 'string', nodeClass: 'string', - canDelete: false + canDelete: false, }, State6: { depth: 2, @@ -162,7 +162,7 @@ describe('Graph Layout Service', () => { style: 'string', secondaryLabel: 'string', nodeClass: 'string', - canDelete: false + canDelete: false, }, State7: { depth: 2, @@ -180,7 +180,7 @@ describe('Graph Layout Service', () => { style: 'string', secondaryLabel: 'string', nodeClass: 'string', - canDelete: false + canDelete: false, }, State8: { depth: 3, @@ -198,7 +198,7 @@ describe('Graph Layout Service', () => { style: 'string', secondaryLabel: 'string', nodeClass: 'string', - canDelete: false + canDelete: false, }, State9: { depth: 1, @@ -216,7 +216,7 @@ describe('Graph Layout Service', () => { style: 'string', secondaryLabel: 'string', nodeClass: 'string', - canDelete: false + canDelete: false, }, Orphaned: { depth: 5, @@ -234,81 +234,81 @@ describe('Graph Layout Service', () => { style: 'string', secondaryLabel: 'string', nodeClass: 'string', - canDelete: false - } + canDelete: false, + }, }; let links1: GraphLink[] = [ { source: 'State1', target: 'State1', linkProperty: '', - connectsDestIfStuck: false + connectsDestIfStuck: false, }, { source: 'State1', target: 'State2', linkProperty: '', - connectsDestIfStuck: false + connectsDestIfStuck: false, }, { source: 'State1', target: 'State3', linkProperty: '', - connectsDestIfStuck: false + connectsDestIfStuck: false, }, { source: 'State1', target: 'State5', linkProperty: '', - connectsDestIfStuck: false + connectsDestIfStuck: false, }, { source: 'State1', target: 'State6', linkProperty: '', - connectsDestIfStuck: false + connectsDestIfStuck: false, }, { source: 'State1', target: 'State7', linkProperty: '', - connectsDestIfStuck: false + connectsDestIfStuck: false, }, { source: 'State1', target: 'State8', linkProperty: '', - connectsDestIfStuck: false + connectsDestIfStuck: false, }, { source: 'State1', target: 'State9', linkProperty: '', - connectsDestIfStuck: false + connectsDestIfStuck: false, }, { source: 'State2', target: 'State4', linkProperty: '', - connectsDestIfStuck: false + connectsDestIfStuck: false, }, { source: 'State3', target: 'State4', linkProperty: '', - connectsDestIfStuck: false + connectsDestIfStuck: false, }, { source: 'State9', target: 'State8', linkProperty: '', - connectsDestIfStuck: false + connectsDestIfStuck: false, }, { source: 'State8', target: 'State4', linkProperty: '', - connectsDestIfStuck: false + connectsDestIfStuck: false, }, ]; @@ -317,31 +317,31 @@ describe('Graph Layout Service', () => { source: 'State1', target: 'State1', linkProperty: '', - connectsDestIfStuck: false + connectsDestIfStuck: false, }, { source: 'State1', target: 'State2', linkProperty: '', - connectsDestIfStuck: false + connectsDestIfStuck: false, }, { source: 'State1', target: 'State3', linkProperty: '', - connectsDestIfStuck: false + connectsDestIfStuck: false, }, { source: 'State2', target: 'State4', linkProperty: '', - connectsDestIfStuck: false + connectsDestIfStuck: false, }, { source: 'State3', target: 'State4', linkProperty: '', - connectsDestIfStuck: false + connectsDestIfStuck: false, }, ]; @@ -360,11 +360,12 @@ describe('Graph Layout Service', () => { State1: ['State2', 'State3'], State2: ['State4'], State3: ['State4'], - State4: [] + State4: [], }; - expect(sgls.getGraphAsAdjacencyLists(nodes, links2)) - .toEqual(expectedAdjacencyLists); + expect(sgls.getGraphAsAdjacencyLists(nodes, links2)).toEqual( + expectedAdjacencyLists + ); }); it('should return indentation levels for a segment of nodes', () => { @@ -373,7 +374,7 @@ describe('Graph Layout Service', () => { State2: ['State3', 'State4'], State3: ['State4', 'State5'], State4: ['State5'], - State5: [] + State5: [], }; let longestPathIds: string[] = [ @@ -381,91 +382,96 @@ describe('Graph Layout Service', () => { 'State2', 'State3', 'State4', - 'State5' + 'State5', ]; - expect(sgls.getIndentationLevels(adjacencyLists, longestPathIds)).toEqual( - [0, 0.5, 1, 0, 0]); + expect(sgls.getIndentationLevels(adjacencyLists, longestPathIds)).toEqual([ + 0, 0.5, 1, 0, 0, + ]); - let shortestPathIds: string[] = [ - 'State1', - 'State4', - 'State5' - ]; - expect(sgls.getIndentationLevels(adjacencyLists, shortestPathIds)).toEqual( - [0, 0, 0]); + let shortestPathIds: string[] = ['State1', 'State4', 'State5']; + expect(sgls.getIndentationLevels(adjacencyLists, shortestPathIds)).toEqual([ + 0, 0, 0, + ]); }); - it('should not return indentation level greater' + - ' than MAX_INDENTATION_LEVEL', () => { - // ┌───────────┐ - // │ State1 ├──┐ - // └────┬──────┘ │ - // │ ▼ - // │ ┌───────────┐ - // │ │ State2 ├──┐ - // │ └┬──────────┘ │ - // │ │ ▼ - // │ │ ┌───────────┐ - // │ │ ┌───┤ State6 ├──┐ - // │ │ │ └───────────┘ │ - // │ │ │ ▼ - // │ │ │ ┌───────────┐ - // │ │ │ ┌────────┤ State8 ├──┐ - // │ │ │ │ └───────────┘ │ - // │ │ │ │ ▼ - // │ │ │ │ ┌───────────┐ - // │ │ │ │ ┌─────────────┤ State7 ├──┐ - // │ │ │ │ │ └───────────┘ │ - // │ │ │ │ │ ▼ - // │ │ │ │ │ ┌───────────┐ - // │ │ │ │ │ ┌──┤ State9 │ - // │ │ │ │ │ │ └─────┬─────┘ - // │ │ │ │ │ │ │ - // │ │ │ │ │ │ ┌─────▼─────┐ - // │ │ │ │ │ │ │ State10 │ - // ▼ ▼ ▼ ▼ ▼ │ └─┬─────────┘ - // ┌───────────┐ ◄─────────────────┘ │ - // │ State3 │ │ - // └─────┬─────┘◄───────────────────────┘ - // │ - // ┌─────▼─────┐ - // │ State5 │ - // └───────────┘ - // Here, State1, State2, State6, State8, State7, State9 have indentation - // level equal to 0, 0.5, 1, 1.5, 2, 2.5. But, State10 does not have - // indentation level equal to 3, as it is placed right below State9. - // So, the indentation level of State10 is also 2.5. - let adjacencyLists = { - State1: ['State2', 'State3'], - State2: ['State3', 'State6'], - State6: ['State3', 'State8'], - State8: ['State3', 'State7'], - State7: ['State3', 'State9'], - State9: ['State3', 'State10'], - State10: ['State3'], - State3: ['State5'], - State5: [] - }; - - let trunkNodeIds: string[] = [ - 'State1', - 'State2', - 'State6', - 'State8', - 'State7', - 'State9', - 'State10', - 'State3', - 'State5' - ]; - - let returnedIndentationLevels = sgls.getIndentationLevels( - adjacencyLists, trunkNodeIds); - returnedIndentationLevels.forEach(indentationLevel => { - expect(indentationLevel).toBeLessThanOrEqual(sgls.MAX_INDENTATION_LEVEL); - }); - }); + it( + 'should not return indentation level greater' + + ' than MAX_INDENTATION_LEVEL', + () => { + // ┌───────────┐ + // │ State1 ├──┐ + // └────┬──────┘ │ + // │ ▼ + // │ ┌───────────┐ + // │ │ State2 ├──┐ + // │ └┬──────────┘ │ + // │ │ ▼ + // │ │ ┌───────────┐ + // │ │ ┌───┤ State6 ├──┐ + // │ │ │ └───────────┘ │ + // │ │ │ ▼ + // │ │ │ ┌───────────┐ + // │ │ │ ┌────────┤ State8 ├──┐ + // │ │ │ │ └───────────┘ │ + // │ │ │ │ ▼ + // │ │ │ │ ┌───────────┐ + // │ │ │ │ ┌─────────────┤ State7 ├──┐ + // │ │ │ │ │ └───────────┘ │ + // │ │ │ │ │ ▼ + // │ │ │ │ │ ┌───────────┐ + // │ │ │ │ │ ┌──┤ State9 │ + // │ │ │ │ │ │ └─────┬─────┘ + // │ │ │ │ │ │ │ + // │ │ │ │ │ │ ┌─────▼─────┐ + // │ │ │ │ │ │ │ State10 │ + // ▼ ▼ ▼ ▼ ▼ │ └─┬─────────┘ + // ┌───────────┐ ◄─────────────────┘ │ + // │ State3 │ │ + // └─────┬─────┘◄───────────────────────┘ + // │ + // ┌─────▼─────┐ + // │ State5 │ + // └───────────┘ + // Here, State1, State2, State6, State8, State7, State9 have indentation + // level equal to 0, 0.5, 1, 1.5, 2, 2.5. But, State10 does not have + // indentation level equal to 3, as it is placed right below State9. + // So, the indentation level of State10 is also 2.5. + let adjacencyLists = { + State1: ['State2', 'State3'], + State2: ['State3', 'State6'], + State6: ['State3', 'State8'], + State8: ['State3', 'State7'], + State7: ['State3', 'State9'], + State9: ['State3', 'State10'], + State10: ['State3'], + State3: ['State5'], + State5: [], + }; + + let trunkNodeIds: string[] = [ + 'State1', + 'State2', + 'State6', + 'State8', + 'State7', + 'State9', + 'State10', + 'State3', + 'State5', + ]; + + let returnedIndentationLevels = sgls.getIndentationLevels( + adjacencyLists, + trunkNodeIds + ); + returnedIndentationLevels.forEach(indentationLevel => { + expect(indentationLevel).toBeLessThanOrEqual( + sgls.MAX_INDENTATION_LEVEL + ); + }); + } + ); it('should return augmented links with bezier curves', () => { let nodeData = { @@ -485,7 +491,7 @@ describe('Graph Layout Service', () => { style: 'style', secondaryLabel: 'secondaryLabel', nodeClass: 'nodeClass', - canDelete: false + canDelete: false, }, State2: { depth: 1, @@ -503,7 +509,7 @@ describe('Graph Layout Service', () => { style: 'style', secondaryLabel: 'secondaryLabel', nodeClass: 'nodeClass', - canDelete: false + canDelete: false, }, State3: { depth: 1, @@ -521,7 +527,7 @@ describe('Graph Layout Service', () => { style: 'style', secondaryLabel: 'secondaryLabel', nodeClass: 'nodeClass', - canDelete: false + canDelete: false, }, State4: { depth: 2, @@ -539,8 +545,8 @@ describe('Graph Layout Service', () => { style: 'style', secondaryLabel: 'secondaryLabel', nodeClass: 'nodeClass', - canDelete: false - } + canDelete: false, + }, }; // Here, bezier curve follows the format 'M%f %f Q %f %f %f %f'. The @@ -549,11 +555,11 @@ describe('Graph Layout Service', () => { let expectedBezierCurveValues = [ 'M0.1625 0.31333333333333335 Q 0.2025 0.3666666666666667 0.1625 0.42', 'M0.23 0.31333333333333335 Q 0.30557165934031566' + - ' 0.3408718290982754 0.32 0.42', + ' 0.3408718290982754 0.32 0.42', 'M0.1625 0.5800000000000001 Q 0.2025 0.6333333333333333' + - ' 0.1625 0.6866666666666665', + ' 0.1625 0.6866666666666665', 'M0.32 0.5800000000000001 Q 0.30557165934031566' + - ' 0.6591281709017246 0.23000000000000004 0.6866666666666665' + ' 0.6591281709017246 0.23000000000000004 0.6866666666666665', ]; let returnedAugmentedLinks = sgls.getAugmentedLinks(nodeData, links2); @@ -572,60 +578,63 @@ describe('Graph Layout Service', () => { expect(returnedBezierCurveValues).toEqual(expectedBezierCurveValues); }); - it('should return undefined when source and target nodes overlap' + - ' while processing augmented links', () => { - // The nodes State1 and State2 overlap as State1.xLabel === State2.xLabel - // and State1.yLabel === State2.yLabel . - let nodeData = { - State1: { - depth: 0, - offset: 0, - reachable: true, - x0: 0.07250000000000001, - y0: 0.15333333333333335, - xLabel: 0.1625, - yLabel: 0.23333333333333334, - id: 'State1', - label: 'State1', - height: 0.16, - width: 0.18000000000000002, - reachableFromEnd: false, - style: 'style', - secondaryLabel: 'secondaryLabel', - nodeClass: 'nodeClass', - canDelete: false - }, - State2: { - depth: 1, - offset: 0, - reachable: true, - x0: 0.07250000000000001, - y0: 0.42000000000000004, - xLabel: 0.1625, - yLabel: 0.23333333333333334, - id: 'State2', - label: 'State2', - height: 0.16, - width: 0.18000000000000002, - reachableFromEnd: false, - style: 'style', - secondaryLabel: 'secondaryLabel', - nodeClass: 'nodeClass', - canDelete: false - } - }; - - let links = [ - { - source: 'State1', - target: 'State2', - linkProperty: '', - connectsDestIfStuck: false - } - ]; - - expect(sgls.getAugmentedLinks(nodeData, links)).toEqual([]); - }); + it( + 'should return undefined when source and target nodes overlap' + + ' while processing augmented links', + () => { + // The nodes State1 and State2 overlap as State1.xLabel === State2.xLabel + // and State1.yLabel === State2.yLabel . + let nodeData = { + State1: { + depth: 0, + offset: 0, + reachable: true, + x0: 0.07250000000000001, + y0: 0.15333333333333335, + xLabel: 0.1625, + yLabel: 0.23333333333333334, + id: 'State1', + label: 'State1', + height: 0.16, + width: 0.18000000000000002, + reachableFromEnd: false, + style: 'style', + secondaryLabel: 'secondaryLabel', + nodeClass: 'nodeClass', + canDelete: false, + }, + State2: { + depth: 1, + offset: 0, + reachable: true, + x0: 0.07250000000000001, + y0: 0.42000000000000004, + xLabel: 0.1625, + yLabel: 0.23333333333333334, + id: 'State2', + label: 'State2', + height: 0.16, + width: 0.18000000000000002, + reachableFromEnd: false, + style: 'style', + secondaryLabel: 'secondaryLabel', + nodeClass: 'nodeClass', + canDelete: false, + }, + }; + + let links = [ + { + source: 'State1', + target: 'State2', + linkProperty: '', + connectsDestIfStuck: false, + }, + ]; + + expect(sgls.getAugmentedLinks(nodeData, links)).toEqual([]); + } + ); it('should get correct graph width and height', () => { let nodeData = { @@ -645,7 +654,7 @@ describe('Graph Layout Service', () => { style: 'style', secondaryLabel: 'secondaryLabel', nodeClass: 'nodeClass', - canDelete: false + canDelete: false, }, Introduction: { depth: 0, @@ -663,7 +672,7 @@ describe('Graph Layout Service', () => { style: 'style', secondaryLabel: 'secondaryLabel', nodeClass: 'nodeClass', - canDelete: false + canDelete: false, }, End: { depth: 2, @@ -681,7 +690,7 @@ describe('Graph Layout Service', () => { style: 'style', secondaryLabel: 'secondaryLabel', nodeClass: 'nodeClass', - canDelete: false + canDelete: false, }, State2: { depth: 1, @@ -699,12 +708,14 @@ describe('Graph Layout Service', () => { style: 'style', secondaryLabel: 'secondaryLabel', nodeClass: 'nodeClass', - canDelete: false - } + canDelete: false, + }, }; let graphWidthUpperBoundInPixels = sgls.getGraphWidth( - AppConstants.MAX_NODES_PER_ROW, AppConstants.MAX_NODE_LABEL_LENGTH); + AppConstants.MAX_NODES_PER_ROW, + AppConstants.MAX_NODE_LABEL_LENGTH + ); let graphHeightInPixels = sgls.getGraphHeight(nodeData); // 10.5 is a rough upper bound for the width of a single letter in pixels, @@ -716,22 +727,27 @@ describe('Graph Layout Service', () => { expect(graphHeightInPixels).toBe(210); }); - it('should get graph width and height when nodes' + - ' overflow to next row', () => { - let graphWidthInPixels = sgls.getGraphWidth( - AppConstants.MAX_NODES_PER_ROW, AppConstants.MAX_NODE_LABEL_LENGTH); - let graphHeightInPixels = sgls.getGraphHeight(nodeData1); - - // 10.5 is a rough upper bound for the width of a single letter in pixels, - // used as a scaling factor to determine width of graph nodes. - expect(graphWidthInPixels).toBe( - AppConstants.MAX_NODES_PER_ROW * AppConstants.MAX_NODE_LABEL_LENGTH * 10.5 - ); - - // Here, graphHeightInPixels = 70 * (maxDepth + 1), here maxDepth is 5. - expect(graphHeightInPixels).toBe(420); - }); - + it( + 'should get graph width and height when nodes' + ' overflow to next row', + () => { + let graphWidthInPixels = sgls.getGraphWidth( + AppConstants.MAX_NODES_PER_ROW, + AppConstants.MAX_NODE_LABEL_LENGTH + ); + let graphHeightInPixels = sgls.getGraphHeight(nodeData1); + + // 10.5 is a rough upper bound for the width of a single letter in pixels, + // used as a scaling factor to determine width of graph nodes. + expect(graphWidthInPixels).toBe( + AppConstants.MAX_NODES_PER_ROW * + AppConstants.MAX_NODE_LABEL_LENGTH * + 10.5 + ); + + // Here, graphHeightInPixels = 70 * (maxDepth + 1), here maxDepth is 5. + expect(graphHeightInPixels).toBe(420); + } + ); it('should compute graph layout', () => { spyOn(sgls, 'computeLayout').and.returnValue(nodeData1); @@ -746,57 +762,65 @@ describe('Graph Layout Service', () => { State7: 'State7', State8: 'State8', State9: 'State9', - Orphaned: 'Orphaned' + Orphaned: 'Orphaned', }; let initNodeId: string = 'State1'; let finalNodeIds: string[] = ['State4']; expect(sgls.computeLayout(nodes, links1, initNodeId, finalNodeIds)).toEqual( - nodeData1); + nodeData1 + ); }); - it('should overflow nodes to next row if there are' + - ' too many nodes at a depth', () => { - let MAX_NODES_PER_ROW = AppConstants.MAX_NODES_PER_ROW; - let nodes: GraphNodes = { - State0: 'State0', - End: 'End' - }; - - let initNodeId: string = 'State0'; - let finalNodeIds: string[] = ['End']; - let links = []; - - for (let i = 1; i <= MAX_NODES_PER_ROW + 1; i++) { - let stateName = 'State' + (i + 1); - nodes[stateName] = stateName; - - links.push({ - source: 'State0', - target: stateName, - linkProperty: '', - connectsDestIfStuck: false - }); - links.push({ - source: stateName, - target: 'End', - linkProperty: '', - connectsDestIfStuck: false - }); - } + it( + 'should overflow nodes to next row if there are' + + ' too many nodes at a depth', + () => { + let MAX_NODES_PER_ROW = AppConstants.MAX_NODES_PER_ROW; + let nodes: GraphNodes = { + State0: 'State0', + End: 'End', + }; + + let initNodeId: string = 'State0'; + let finalNodeIds: string[] = ['End']; + let links = []; + + for (let i = 1; i <= MAX_NODES_PER_ROW + 1; i++) { + let stateName = 'State' + (i + 1); + nodes[stateName] = stateName; + + links.push({ + source: 'State0', + target: stateName, + linkProperty: '', + connectsDestIfStuck: false, + }); + links.push({ + source: stateName, + target: 'End', + linkProperty: '', + connectsDestIfStuck: false, + }); + } - let returnedLayoutNodeData = sgls.computeLayout( - nodes, links, initNodeId, finalNodeIds); - let countNodesDepthOne: number = 0; - for (let nodeId in nodes) { - if (returnedLayoutNodeData[nodeId].depth === 1) { - countNodesDepthOne++; + let returnedLayoutNodeData = sgls.computeLayout( + nodes, + links, + initNodeId, + finalNodeIds + ); + let countNodesDepthOne: number = 0; + for (let nodeId in nodes) { + if (returnedLayoutNodeData[nodeId].depth === 1) { + countNodesDepthOne++; + } } - } - expect(countNodesDepthOne).toEqual(MAX_NODES_PER_ROW); - }); + expect(countNodesDepthOne).toEqual(MAX_NODES_PER_ROW); + } + ); it('should place orhpaned node at max depth while computing layout', () => { let nodes = { @@ -807,7 +831,7 @@ describe('Graph Layout Service', () => { State2: 'State2', State3: 'State3', State4: 'State4', - State5: 'State5' + State5: 'State5', }; let links = [ @@ -815,74 +839,78 @@ describe('Graph Layout Service', () => { source: 'State5', target: 'End', linkProperty: '', - connectsDestIfStuck: false + connectsDestIfStuck: false, }, { source: 'State4', target: 'End', linkProperty: '', - connectsDestIfStuck: false + connectsDestIfStuck: false, }, { source: 'State3', target: 'End', linkProperty: '', - connectsDestIfStuck: false + connectsDestIfStuck: false, }, { source: 'State2', target: 'End', linkProperty: '', - connectsDestIfStuck: false + connectsDestIfStuck: false, }, { source: 'State1', target: 'End', linkProperty: '', - connectsDestIfStuck: false + connectsDestIfStuck: false, }, { source: 'State0', target: 'State1', linkProperty: '', - connectsDestIfStuck: false + connectsDestIfStuck: false, }, { source: 'State0', target: 'State2', linkProperty: '', - connectsDestIfStuck: false + connectsDestIfStuck: false, }, { source: 'State0', target: 'State3', linkProperty: '', - connectsDestIfStuck: false + connectsDestIfStuck: false, }, { source: 'State0', target: 'State4', linkProperty: '', - connectsDestIfStuck: false + connectsDestIfStuck: false, }, { source: 'State0', target: 'State5', linkProperty: '', - connectsDestIfStuck: false + connectsDestIfStuck: false, }, { source: 'State0', target: 'State0', linkProperty: '', - connectsDestIfStuck: false - } + connectsDestIfStuck: false, + }, ]; let initNodeId = 'State0'; let finalNodeIds = ['End']; let returnedLayout = sgls.computeLayout( - nodes, links, initNodeId, finalNodeIds); + nodes, + links, + initNodeId, + finalNodeIds + ); expect(returnedLayout.End.depth).toBe(3); expect(returnedLayout.Orphan.depth).toBe(4); @@ -899,7 +927,7 @@ describe('Graph Layout Service', () => { State7: 'State7', State8: 'State8', State9: 'State9', - Orphaned: 'Orphaned' + Orphaned: 'Orphaned', }; let initNodeId: string = 'State1'; @@ -908,480 +936,505 @@ describe('Graph Layout Service', () => { expect(sgls.getLastComputedArrangement()).toBe(null); let computedLayout = sgls.computeLayout( - nodes, links1, initNodeId, finalNodeIds); + nodes, + links1, + initNodeId, + finalNodeIds + ); expect(sgls.getLastComputedArrangement()).toEqual(computedLayout); }); - it('should return graph boundaries with width less than equal to' + - ' maximum allowed graph width', () => { - // Here, nodeDataWithPositionValueInPixels1, 2 and 3 have position values - // (x0, xLabel, width etc.) in terms of pixels. - // nodeDataWithPositionInPixels1, 2 and 3 are node data of graphs with - // MAX_NODE_PER_ROW - 1, MAX_NODE_PER_ROW and MAX_NODE_PER_ROW + 1 nodes in - // a row. Right now, MAX_NODE_PER_ROW is 4. - - // ┌──────────────┐ - // │ Introduction │ - // └──┬────────┬──┴──────┐ - // │ │ │ - // ┌──▼───┐ ┌──▼───┐ ┌───▼──┐ - // │State1│ │State2│ │State3│ - // └──────┘ └──────┘ └──────┘. - let nodeDataWithPositionValueInPixels1 = { - State1: { - depth: 1, - offset: 0, - reachable: true, - x0: 45.675000000000004, - y0: 81.19999999999999, - xLabel: 102.375, - yLabel: 98.00000000000001, - id: 'State1', - label: 'State1', - height: 33.6, - width: 126, - reachableFromEnd: false, - style: 'style', - secondaryLabel: 'secondaryLabel', - nodeClass: 'nodeClass', - canDelete: false - }, - State2: { - depth: 1, - offset: 1, - reachable: true, - x0: 187.42500000000004, - y0: 81.19999999999999, - xLabel: 244.125, - yLabel: 98.00000000000001, - id: 'State2', - label: 'State2', - height: 33.6, - width: 126, - reachableFromEnd: false, - style: 'style', - secondaryLabel: 'secondaryLabel', - nodeClass: 'nodeClass', - canDelete: false - }, - State3: { - depth: 1, - offset: 2, - reachable: true, - x0: 329.17500000000007, - y0: 81.19999999999999, - xLabel: 385.875, - yLabel: 98.00000000000001, - id: 'State3', - label: 'State3', - height: 33.6, - width: 126, - reachableFromEnd: false, - style: 'style', - secondaryLabel: 'secondaryLabel', - nodeClass: 'nodeClass', - canDelete: false - }, - Introduction: { - depth: 0, - offset: 0, - reachable: true, - x0: 45.675000000000004, - y0: 25.200000000000003, - xLabel: 102.375, - yLabel: 42.00000000000001, - id: 'Introduction', - label: 'Introduction', - height: 33.6, - width: 126, - reachableFromEnd: false, - style: 'style', - secondaryLabel: 'secondaryLabel', - nodeClass: 'nodeClass', - canDelete: false - } - }; - - // ┌──────────────┬───────────────┐ - // │ Introduction │ │ - // └──┬────────┬──┴──────┐ │ - // │ │ │ │ - // ┌──▼───┐ ┌──▼───┐ ┌───▼──┐ ┌──▼───┐ - // │State1│ │State2│ │State3│ │State4│ - // └──────┘ └──────┘ └──────┘ └──────┘. - let nodeDataWithPositionValueInPixels2 = { - State1: { - depth: 1, - offset: 0, - reachable: true, - x0: 45.675000000000004, - y0: 81.19999999999999, - xLabel: 102.375, - yLabel: 98.00000000000001, - id: 'State1', - label: 'State1', - height: 33.6, - width: 126, - reachableFromEnd: false, - style: 'style', - secondaryLabel: 'secondaryLabel', - nodeClass: 'nodeClass', - canDelete: false - }, - State2: { - depth: 1, - offset: 1, - reachable: true, - x0: 187.42500000000004, - y0: 81.19999999999999, - xLabel: 244.125, - yLabel: 98.00000000000001, - id: 'State2', - label: 'State2', - height: 33.6, - width: 126, - reachableFromEnd: false, - style: 'style', - secondaryLabel: 'secondaryLabel', - nodeClass: 'nodeClass', - canDelete: false - }, - State3: { - depth: 1, - offset: 2, - reachable: true, - x0: 329.17500000000007, - y0: 81.19999999999999, - xLabel: 385.875, - yLabel: 98.00000000000001, - id: 'State3', - label: 'State3', - height: 33.6, - width: 126, - reachableFromEnd: false, - style: 'style', - secondaryLabel: 'secondaryLabel', - nodeClass: 'nodeClass', - canDelete: false - }, - Introduction: { - depth: 0, - offset: 0, - reachable: true, - x0: 45.675000000000004, - y0: 25.200000000000003, - xLabel: 102.375, - yLabel: 42.00000000000001, - id: 'Introduction', - label: 'Introduction', - height: 33.6, - width: 126, - reachableFromEnd: false, - style: 'style', - secondaryLabel: 'secondaryLabel', - nodeClass: 'nodeClass', - canDelete: false - }, - State4: { - depth: 1, - offset: 3, - reachable: true, - x0: 470.925, - y0: 81.19999999999999, - xLabel: 527.625, - yLabel: 98.00000000000001, - id: 'State4', - label: 'State4', - height: 33.6, - width: 126, - reachableFromEnd: false, - style: 'style', - secondaryLabel: 'secondaryLabel', - nodeClass: 'nodeClass', - canDelete: false - } - }; - - // ┌──────────────┬───────────────┐ - // ┌────┤ Introduction │ │ - // │ └──┬────────┬──┴──────┐ │ - // │ │ │ │ │ - // │ ┌──▼───┐ ┌──▼───┐ ┌───▼──┐ ┌──▼───┐ - // │ │State1│ │State2│ │State3│ │State4│ - // │ └──────┘ └──────┘ └──────┘ └──────┘ - // │ ┌──────┐ - // └─────────────►State5│ - // └──────┘ - // So, here State5 moves on to the next row. - let nodeDataWithPositionValueInPixels3 = { - State1: { - depth: 1, - offset: 0, - reachable: true, - x0: 45.675000000000004, - y0: 88.2, - xLabel: 102.375, - yLabel: 105, - id: 'State1', - label: 'State1', - height: 33.6, - width: 126, - reachableFromEnd: false, - style: 'style', - secondaryLabel: 'secondaryLabel', - nodeClass: 'nodeClass', - canDelete: false - }, - State2: { - depth: 1, - offset: 1, - reachable: true, - x0: 187.42500000000004, - y0: 88.2, - xLabel: 244.125, - yLabel: 105, - id: 'State2', - label: 'State2', - height: 33.6, - width: 126, - reachableFromEnd: false, - style: 'style', - secondaryLabel: 'secondaryLabel', - nodeClass: 'nodeClass', - canDelete: false - }, - State3: { - depth: 1, - offset: 2, - reachable: true, - x0: 329.17500000000007, - y0: 88.2, - xLabel: 385.875, - yLabel: 105, - id: 'State3', - label: 'State3', - height: 33.6, - width: 126, - reachableFromEnd: false, - style: 'style', - secondaryLabel: 'secondaryLabel', - nodeClass: 'nodeClass', - canDelete: false - }, - Introduction: { - depth: 0, - offset: 0, - reachable: true, - x0: 45.675000000000004, - y0: 32.2, - xLabel: 102.375, - yLabel: 49, - id: 'Introduction', - label: 'Introduction', - height: 33.6, - width: 126, - reachableFromEnd: false, - style: 'style', - secondaryLabel: 'secondaryLabel', - nodeClass: 'nodeClass', - canDelete: false - }, - State4: { - depth: 1, - offset: 3, - reachable: true, - x0: 470.925, - y0: 88.2, - xLabel: 527.625, - yLabel: 105, - id: 'State4', - label: 'State4', - height: 33.6, - width: 126, - reachableFromEnd: false, - style: 'style', - secondaryLabel: 'secondaryLabel', - nodeClass: 'nodeClass', - canDelete: false - }, - State5: { - depth: 2, - offset: 1, - reachable: true, - x0: 187.42500000000004, - y0: 144.2, - xLabel: 244.125, - yLabel: 161, - id: 'State5', - label: 'State5', - height: 33.6, - width: 126, - reachableFromEnd: false, - style: 'style', - secondaryLabel: 'secondaryLabel', - nodeClass: 'nodeClass', - canDelete: false - } - }; - - let actualGraphBoundariesInPixels1 = sgls.getGraphBoundaries( - nodeDataWithPositionValueInPixels1); - let actualGraphBoundariesInPixels2 = sgls.getGraphBoundaries( - nodeDataWithPositionValueInPixels2); - let actualGraphBoundariesInPixels3 = sgls.getGraphBoundaries( - nodeDataWithPositionValueInPixels3); - - // The width of graph is calculated as difference between left and right - // edge. - let actualWidthInPixels1 = actualGraphBoundariesInPixels1.right - - actualGraphBoundariesInPixels1.left; - let actualWidthInPixels2 = actualGraphBoundariesInPixels2.right - - actualGraphBoundariesInPixels2.left; - let actualWidthInPixels3 = actualGraphBoundariesInPixels3.right - - actualGraphBoundariesInPixels3.left; - - // This is the maximum upper bound for graph width taking padding fraction - // into consideration. First we calculate the x0 of the rightmost node. - // rightMostNode.x0 = HORIZONTAL_EDGE_PADDING_FRACTION + - // fractionalGridWidth * offsetInGridRectangle, where - // HORIZONTAL_EDGE_PADDING_FRACTION = 0.05, - // fractionalGridWidth = (1.0 - HORIZONTAL_EDGE_PADDING_FRACTION * 2) / - // totalColumns = (1.0 - 0.05 * 2)/4 = 0.225, and - // offsetInGridRectangle = rightMostNode.offset + - // GRID_NODE_X_PADDING_FRACTION = 3 + 0.1 = 3.1 - // So, rightMostNode.x0 = 0.05 + 0.225 * 3.1 = 0.7475. Converting it to - // pixel, rightMostNode.x0 = 630 * 0.747 = 470.925. Now, the rightEdge = - // rightMostNode.x0 + BORDER_PADDING + rightMostNode.width = 470.925 + 5 + - // 126 = 601.925. - // Now, if we calculate the leftEdge of leftMostState (here State1), we'll - // get 40.675000000000004. So width = 601.925 - 40.675000000000004 = 561.25. - let widthUpperBoundInPixels = 561.25; - - // The height of graph calculated as difference b/w bottom and right top. - let actualHeightInPixels1 = actualGraphBoundariesInPixels1.bottom - - actualGraphBoundariesInPixels1.top; - let actualHeightInPixels2 = actualGraphBoundariesInPixels2.bottom - - actualGraphBoundariesInPixels2.top; - let actualHeightInPixels3 = actualGraphBoundariesInPixels3.bottom - - actualGraphBoundariesInPixels3.top; - - // Here, we see that for 3 nodes the graph width is less than - // the upper bound, for 4 nodes the graph width is equal to the - // upper bound and the width becomes constant for nodes > 4. - expect(actualWidthInPixels1).toBeLessThan(actualWidthInPixels2); - expect(actualWidthInPixels2).toEqual(widthUpperBoundInPixels); - expect(actualWidthInPixels3).toEqual(widthUpperBoundInPixels); - - // As height does not have an upper bound, it increases as a node overflows - // to the next row. - expect(actualHeightInPixels1).toEqual(actualHeightInPixels2); - expect(actualHeightInPixels2).toBeLessThan(actualHeightInPixels3); - }); - - it('should return graph boundaries with height equal to' + - ' the graph height', () => { - let nodeData = { - State1: { - depth: 1, - offset: 0, - reachable: true, - x0: 0.07250000000000001, - y0: 0.42000000000000004, - xLabel: 0.1625, - yLabel: 0.5, - id: 'State1', - label: 'State1', - height: 0.16, - width: 0.18000000000000002, - reachableFromEnd: false, - style: 'style', - secondaryLabel: 'secondaryLabel', - nodeClass: 'nodeClass', - canDelete: false - }, - Introduction: { - depth: 0, - offset: 0, - reachable: true, - x0: 0.07250000000000001, - y0: 0.15333333333333335, - xLabel: 0.1625, - yLabel: 0.23333333333333334, - id: 'Introduction', - label: 'Introduction', - height: 0.16, - width: 0.18000000000000002, - reachableFromEnd: false, - style: 'style', - secondaryLabel: 'secondaryLabel', - nodeClass: 'nodeClass', - canDelete: false - }, - End: { - depth: 2, - offset: 0, - reachable: true, - x0: 0.07250000000000001, - y0: 0.6866666666666666, - xLabel: 0.1625, - yLabel: 0.7666666666666666, - id: 'End', - label: 'End', - height: 0.16, - width: 0.18000000000000002, - reachableFromEnd: false, - style: 'style', - secondaryLabel: 'secondaryLabel', - nodeClass: 'nodeClass', - canDelete: false - }, - State2: { - depth: 1, - offset: 1, - reachable: true, - x0: 0.29750000000000004, - y0: 0.42000000000000004, - xLabel: 0.3875, - yLabel: 0.5, - id: 'State2', - label: 'State2', - height: 0.16, - width: 0.18000000000000002, - reachableFromEnd: false, - style: 'style', - secondaryLabel: 'secondaryLabel', - nodeClass: 'nodeClass', - canDelete: false - } - }; - let graphWidthUpperBoundInPixels = sgls.getGraphWidth( - AppConstants.MAX_NODES_PER_ROW, AppConstants.MAX_NODE_LABEL_LENGTH); - let graphHeightInPixels = sgls.getGraphHeight(nodeData); - let nodeDataWithPositionValueInPixels = sgls.modifyPositionValues( - nodeData, graphWidthUpperBoundInPixels, graphHeightInPixels); - - // The expectedGraphBoundariesInPixels are calculated from the x0 and y0 - // values from nodeDataWithPositionValueInPixels, where leftEdge is - // minimum(nodeDataWithPositionValueInPixels[nodeId].x0 - BORDER_PADDING) - // of all nodes, and rightEdge is - // maximum(nodeDataWithPositionValueInPixels[nodeId].x0 + BORDER_PADDING + - // nodeDataWithPositionValueInPixels[nodeId].width) of all nodes. Similarly, - // bottomEdge and topEdge are calculated using y0 and node height. - let expectedGraphBoundariesInPixels = { - bottom: 182.79999999999998, - left: 40.675000000000004, - right: 305.82500000000005, - top: 27.200000000000003 - }; - - let expectedHeightInPixels = expectedGraphBoundariesInPixels.bottom - - expectedGraphBoundariesInPixels.top; - - expect(expectedHeightInPixels).toBeLessThanOrEqual(graphHeightInPixels); - expect(sgls.getGraphBoundaries(nodeDataWithPositionValueInPixels)) - .toEqual(expectedGraphBoundariesInPixels); - }); + it( + 'should return graph boundaries with width less than equal to' + + ' maximum allowed graph width', + () => { + // Here, nodeDataWithPositionValueInPixels1, 2 and 3 have position values + // (x0, xLabel, width etc.) in terms of pixels. + // nodeDataWithPositionInPixels1, 2 and 3 are node data of graphs with + // MAX_NODE_PER_ROW - 1, MAX_NODE_PER_ROW and MAX_NODE_PER_ROW + 1 nodes in + // a row. Right now, MAX_NODE_PER_ROW is 4. + + // ┌──────────────┐ + // │ Introduction │ + // └──┬────────┬──┴──────┐ + // │ │ │ + // ┌──▼───┐ ┌──▼───┐ ┌───▼──┐ + // │State1│ │State2│ │State3│ + // └──────┘ └──────┘ └──────┘. + let nodeDataWithPositionValueInPixels1 = { + State1: { + depth: 1, + offset: 0, + reachable: true, + x0: 45.675000000000004, + y0: 81.19999999999999, + xLabel: 102.375, + yLabel: 98.00000000000001, + id: 'State1', + label: 'State1', + height: 33.6, + width: 126, + reachableFromEnd: false, + style: 'style', + secondaryLabel: 'secondaryLabel', + nodeClass: 'nodeClass', + canDelete: false, + }, + State2: { + depth: 1, + offset: 1, + reachable: true, + x0: 187.42500000000004, + y0: 81.19999999999999, + xLabel: 244.125, + yLabel: 98.00000000000001, + id: 'State2', + label: 'State2', + height: 33.6, + width: 126, + reachableFromEnd: false, + style: 'style', + secondaryLabel: 'secondaryLabel', + nodeClass: 'nodeClass', + canDelete: false, + }, + State3: { + depth: 1, + offset: 2, + reachable: true, + x0: 329.17500000000007, + y0: 81.19999999999999, + xLabel: 385.875, + yLabel: 98.00000000000001, + id: 'State3', + label: 'State3', + height: 33.6, + width: 126, + reachableFromEnd: false, + style: 'style', + secondaryLabel: 'secondaryLabel', + nodeClass: 'nodeClass', + canDelete: false, + }, + Introduction: { + depth: 0, + offset: 0, + reachable: true, + x0: 45.675000000000004, + y0: 25.200000000000003, + xLabel: 102.375, + yLabel: 42.00000000000001, + id: 'Introduction', + label: 'Introduction', + height: 33.6, + width: 126, + reachableFromEnd: false, + style: 'style', + secondaryLabel: 'secondaryLabel', + nodeClass: 'nodeClass', + canDelete: false, + }, + }; + + // ┌──────────────┬───────────────┐ + // │ Introduction │ │ + // └──┬────────┬──┴──────┐ │ + // │ │ │ │ + // ┌──▼───┐ ┌──▼───┐ ┌───▼──┐ ┌──▼───┐ + // │State1│ │State2│ │State3│ │State4│ + // └──────┘ └──────┘ └──────┘ └──────┘. + let nodeDataWithPositionValueInPixels2 = { + State1: { + depth: 1, + offset: 0, + reachable: true, + x0: 45.675000000000004, + y0: 81.19999999999999, + xLabel: 102.375, + yLabel: 98.00000000000001, + id: 'State1', + label: 'State1', + height: 33.6, + width: 126, + reachableFromEnd: false, + style: 'style', + secondaryLabel: 'secondaryLabel', + nodeClass: 'nodeClass', + canDelete: false, + }, + State2: { + depth: 1, + offset: 1, + reachable: true, + x0: 187.42500000000004, + y0: 81.19999999999999, + xLabel: 244.125, + yLabel: 98.00000000000001, + id: 'State2', + label: 'State2', + height: 33.6, + width: 126, + reachableFromEnd: false, + style: 'style', + secondaryLabel: 'secondaryLabel', + nodeClass: 'nodeClass', + canDelete: false, + }, + State3: { + depth: 1, + offset: 2, + reachable: true, + x0: 329.17500000000007, + y0: 81.19999999999999, + xLabel: 385.875, + yLabel: 98.00000000000001, + id: 'State3', + label: 'State3', + height: 33.6, + width: 126, + reachableFromEnd: false, + style: 'style', + secondaryLabel: 'secondaryLabel', + nodeClass: 'nodeClass', + canDelete: false, + }, + Introduction: { + depth: 0, + offset: 0, + reachable: true, + x0: 45.675000000000004, + y0: 25.200000000000003, + xLabel: 102.375, + yLabel: 42.00000000000001, + id: 'Introduction', + label: 'Introduction', + height: 33.6, + width: 126, + reachableFromEnd: false, + style: 'style', + secondaryLabel: 'secondaryLabel', + nodeClass: 'nodeClass', + canDelete: false, + }, + State4: { + depth: 1, + offset: 3, + reachable: true, + x0: 470.925, + y0: 81.19999999999999, + xLabel: 527.625, + yLabel: 98.00000000000001, + id: 'State4', + label: 'State4', + height: 33.6, + width: 126, + reachableFromEnd: false, + style: 'style', + secondaryLabel: 'secondaryLabel', + nodeClass: 'nodeClass', + canDelete: false, + }, + }; + + // ┌──────────────┬───────────────┐ + // ┌────┤ Introduction │ │ + // │ └──┬────────┬──┴──────┐ │ + // │ │ │ │ │ + // │ ┌──▼───┐ ┌──▼───┐ ┌───▼──┐ ┌──▼───┐ + // │ │State1│ │State2│ │State3│ │State4│ + // │ └──────┘ └──────┘ └──────┘ └──────┘ + // │ ┌──────┐ + // └─────────────►State5│ + // └──────┘ + // So, here State5 moves on to the next row. + let nodeDataWithPositionValueInPixels3 = { + State1: { + depth: 1, + offset: 0, + reachable: true, + x0: 45.675000000000004, + y0: 88.2, + xLabel: 102.375, + yLabel: 105, + id: 'State1', + label: 'State1', + height: 33.6, + width: 126, + reachableFromEnd: false, + style: 'style', + secondaryLabel: 'secondaryLabel', + nodeClass: 'nodeClass', + canDelete: false, + }, + State2: { + depth: 1, + offset: 1, + reachable: true, + x0: 187.42500000000004, + y0: 88.2, + xLabel: 244.125, + yLabel: 105, + id: 'State2', + label: 'State2', + height: 33.6, + width: 126, + reachableFromEnd: false, + style: 'style', + secondaryLabel: 'secondaryLabel', + nodeClass: 'nodeClass', + canDelete: false, + }, + State3: { + depth: 1, + offset: 2, + reachable: true, + x0: 329.17500000000007, + y0: 88.2, + xLabel: 385.875, + yLabel: 105, + id: 'State3', + label: 'State3', + height: 33.6, + width: 126, + reachableFromEnd: false, + style: 'style', + secondaryLabel: 'secondaryLabel', + nodeClass: 'nodeClass', + canDelete: false, + }, + Introduction: { + depth: 0, + offset: 0, + reachable: true, + x0: 45.675000000000004, + y0: 32.2, + xLabel: 102.375, + yLabel: 49, + id: 'Introduction', + label: 'Introduction', + height: 33.6, + width: 126, + reachableFromEnd: false, + style: 'style', + secondaryLabel: 'secondaryLabel', + nodeClass: 'nodeClass', + canDelete: false, + }, + State4: { + depth: 1, + offset: 3, + reachable: true, + x0: 470.925, + y0: 88.2, + xLabel: 527.625, + yLabel: 105, + id: 'State4', + label: 'State4', + height: 33.6, + width: 126, + reachableFromEnd: false, + style: 'style', + secondaryLabel: 'secondaryLabel', + nodeClass: 'nodeClass', + canDelete: false, + }, + State5: { + depth: 2, + offset: 1, + reachable: true, + x0: 187.42500000000004, + y0: 144.2, + xLabel: 244.125, + yLabel: 161, + id: 'State5', + label: 'State5', + height: 33.6, + width: 126, + reachableFromEnd: false, + style: 'style', + secondaryLabel: 'secondaryLabel', + nodeClass: 'nodeClass', + canDelete: false, + }, + }; + + let actualGraphBoundariesInPixels1 = sgls.getGraphBoundaries( + nodeDataWithPositionValueInPixels1 + ); + let actualGraphBoundariesInPixels2 = sgls.getGraphBoundaries( + nodeDataWithPositionValueInPixels2 + ); + let actualGraphBoundariesInPixels3 = sgls.getGraphBoundaries( + nodeDataWithPositionValueInPixels3 + ); + + // The width of graph is calculated as difference between left and right + // edge. + let actualWidthInPixels1 = + actualGraphBoundariesInPixels1.right - + actualGraphBoundariesInPixels1.left; + let actualWidthInPixels2 = + actualGraphBoundariesInPixels2.right - + actualGraphBoundariesInPixels2.left; + let actualWidthInPixels3 = + actualGraphBoundariesInPixels3.right - + actualGraphBoundariesInPixels3.left; + + // This is the maximum upper bound for graph width taking padding fraction + // into consideration. First we calculate the x0 of the rightmost node. + // rightMostNode.x0 = HORIZONTAL_EDGE_PADDING_FRACTION + + // fractionalGridWidth * offsetInGridRectangle, where + // HORIZONTAL_EDGE_PADDING_FRACTION = 0.05, + // fractionalGridWidth = (1.0 - HORIZONTAL_EDGE_PADDING_FRACTION * 2) / + // totalColumns = (1.0 - 0.05 * 2)/4 = 0.225, and + // offsetInGridRectangle = rightMostNode.offset + + // GRID_NODE_X_PADDING_FRACTION = 3 + 0.1 = 3.1 + // So, rightMostNode.x0 = 0.05 + 0.225 * 3.1 = 0.7475. Converting it to + // pixel, rightMostNode.x0 = 630 * 0.747 = 470.925. Now, the rightEdge = + // rightMostNode.x0 + BORDER_PADDING + rightMostNode.width = 470.925 + 5 + + // 126 = 601.925. + // Now, if we calculate the leftEdge of leftMostState (here State1), we'll + // get 40.675000000000004. So width = 601.925 - 40.675000000000004 = 561.25. + let widthUpperBoundInPixels = 561.25; + + // The height of graph calculated as difference b/w bottom and right top. + let actualHeightInPixels1 = + actualGraphBoundariesInPixels1.bottom - + actualGraphBoundariesInPixels1.top; + let actualHeightInPixels2 = + actualGraphBoundariesInPixels2.bottom - + actualGraphBoundariesInPixels2.top; + let actualHeightInPixels3 = + actualGraphBoundariesInPixels3.bottom - + actualGraphBoundariesInPixels3.top; + + // Here, we see that for 3 nodes the graph width is less than + // the upper bound, for 4 nodes the graph width is equal to the + // upper bound and the width becomes constant for nodes > 4. + expect(actualWidthInPixels1).toBeLessThan(actualWidthInPixels2); + expect(actualWidthInPixels2).toEqual(widthUpperBoundInPixels); + expect(actualWidthInPixels3).toEqual(widthUpperBoundInPixels); + + // As height does not have an upper bound, it increases as a node overflows + // to the next row. + expect(actualHeightInPixels1).toEqual(actualHeightInPixels2); + expect(actualHeightInPixels2).toBeLessThan(actualHeightInPixels3); + } + ); + + it( + 'should return graph boundaries with height equal to' + ' the graph height', + () => { + let nodeData = { + State1: { + depth: 1, + offset: 0, + reachable: true, + x0: 0.07250000000000001, + y0: 0.42000000000000004, + xLabel: 0.1625, + yLabel: 0.5, + id: 'State1', + label: 'State1', + height: 0.16, + width: 0.18000000000000002, + reachableFromEnd: false, + style: 'style', + secondaryLabel: 'secondaryLabel', + nodeClass: 'nodeClass', + canDelete: false, + }, + Introduction: { + depth: 0, + offset: 0, + reachable: true, + x0: 0.07250000000000001, + y0: 0.15333333333333335, + xLabel: 0.1625, + yLabel: 0.23333333333333334, + id: 'Introduction', + label: 'Introduction', + height: 0.16, + width: 0.18000000000000002, + reachableFromEnd: false, + style: 'style', + secondaryLabel: 'secondaryLabel', + nodeClass: 'nodeClass', + canDelete: false, + }, + End: { + depth: 2, + offset: 0, + reachable: true, + x0: 0.07250000000000001, + y0: 0.6866666666666666, + xLabel: 0.1625, + yLabel: 0.7666666666666666, + id: 'End', + label: 'End', + height: 0.16, + width: 0.18000000000000002, + reachableFromEnd: false, + style: 'style', + secondaryLabel: 'secondaryLabel', + nodeClass: 'nodeClass', + canDelete: false, + }, + State2: { + depth: 1, + offset: 1, + reachable: true, + x0: 0.29750000000000004, + y0: 0.42000000000000004, + xLabel: 0.3875, + yLabel: 0.5, + id: 'State2', + label: 'State2', + height: 0.16, + width: 0.18000000000000002, + reachableFromEnd: false, + style: 'style', + secondaryLabel: 'secondaryLabel', + nodeClass: 'nodeClass', + canDelete: false, + }, + }; + let graphWidthUpperBoundInPixels = sgls.getGraphWidth( + AppConstants.MAX_NODES_PER_ROW, + AppConstants.MAX_NODE_LABEL_LENGTH + ); + let graphHeightInPixels = sgls.getGraphHeight(nodeData); + let nodeDataWithPositionValueInPixels = sgls.modifyPositionValues( + nodeData, + graphWidthUpperBoundInPixels, + graphHeightInPixels + ); + + // The expectedGraphBoundariesInPixels are calculated from the x0 and y0 + // values from nodeDataWithPositionValueInPixels, where leftEdge is + // minimum(nodeDataWithPositionValueInPixels[nodeId].x0 - BORDER_PADDING) + // of all nodes, and rightEdge is + // maximum(nodeDataWithPositionValueInPixels[nodeId].x0 + BORDER_PADDING + + // nodeDataWithPositionValueInPixels[nodeId].width) of all nodes. Similarly, + // bottomEdge and topEdge are calculated using y0 and node height. + let expectedGraphBoundariesInPixels = { + bottom: 182.79999999999998, + left: 40.675000000000004, + right: 305.82500000000005, + top: 27.200000000000003, + }; + + let expectedHeightInPixels = + expectedGraphBoundariesInPixels.bottom - + expectedGraphBoundariesInPixels.top; + + expect(expectedHeightInPixels).toBeLessThanOrEqual(graphHeightInPixels); + expect( + sgls.getGraphBoundaries(nodeDataWithPositionValueInPixels) + ).toEqual(expectedGraphBoundariesInPixels); + } + ); it('should modify position values in node data to use pixels', () => { let nodeData = { @@ -1401,7 +1454,7 @@ describe('Graph Layout Service', () => { style: 'style', secondaryLabel: 'secondaryLabel', nodeClass: 'nodeClass', - canDelete: false + canDelete: false, }, State2: { depth: 1, @@ -1419,7 +1472,7 @@ describe('Graph Layout Service', () => { style: 'style', secondaryLabel: 'secondaryLabel', nodeClass: 'nodeClass', - canDelete: false + canDelete: false, }, State3: { depth: 1, @@ -1437,7 +1490,7 @@ describe('Graph Layout Service', () => { style: 'style', secondaryLabel: 'secondaryLabel', nodeClass: 'nodeClass', - canDelete: false + canDelete: false, }, State4: { depth: 4, @@ -1455,7 +1508,7 @@ describe('Graph Layout Service', () => { style: 'style', secondaryLabel: 'secondaryLabel', nodeClass: 'nodeClass', - canDelete: false + canDelete: false, }, State5: { depth: 1, @@ -1473,7 +1526,7 @@ describe('Graph Layout Service', () => { style: 'style', secondaryLabel: 'secondaryLabel', nodeClass: 'nodeClass', - canDelete: false + canDelete: false, }, State6: { depth: 2, @@ -1491,7 +1544,7 @@ describe('Graph Layout Service', () => { style: 'style', secondaryLabel: 'secondaryLabel', nodeClass: 'nodeClass', - canDelete: false + canDelete: false, }, State7: { depth: 2, @@ -1509,7 +1562,7 @@ describe('Graph Layout Service', () => { style: 'style', secondaryLabel: 'secondaryLabel', nodeClass: 'nodeClass', - canDelete: false + canDelete: false, }, State8: { depth: 3, @@ -1527,7 +1580,7 @@ describe('Graph Layout Service', () => { style: 'style', secondaryLabel: 'secondaryLabel', nodeClass: 'nodeClass', - canDelete: false + canDelete: false, }, State9: { depth: 1, @@ -1545,7 +1598,7 @@ describe('Graph Layout Service', () => { style: 'style', secondaryLabel: 'secondaryLabel', nodeClass: 'nodeClass', - canDelete: false + canDelete: false, }, Orphaned: { depth: 5, @@ -1563,12 +1616,14 @@ describe('Graph Layout Service', () => { style: 'style', secondaryLabel: 'secondaryLabel', nodeClass: 'nodeClass', - canDelete: false - } + canDelete: false, + }, }; let graphWidthUpperBoundInPixels = sgls.getGraphWidth( - AppConstants.MAX_NODES_PER_ROW, AppConstants.MAX_NODE_LABEL_LENGTH); + AppConstants.MAX_NODES_PER_ROW, + AppConstants.MAX_NODE_LABEL_LENGTH + ); let graphHeightInPixels = sgls.getGraphHeight(nodeData); // Here, modifiedNodeData is nodeData with position values in pixels. @@ -1577,7 +1632,10 @@ describe('Graph Layout Service', () => { // with graph width. So, modifiedNodeDate.State1.x0 = 0.07250000000000001 * // graphWidthUpperBoundInPixels (630) = 47.675000000000004. let modifiedNodeData = sgls.modifyPositionValues( - nodeData, graphWidthUpperBoundInPixels, graphHeightInPixels); + nodeData, + graphWidthUpperBoundInPixels, + graphHeightInPixels + ); // The expectedPositionValues are calculated similarly as given above. let expectedPositionValuesInPixels = { @@ -1587,22 +1645,28 @@ describe('Graph Layout Service', () => { xLabel: 102.375, yLabel: 70.00000000000001, width: 126, - height: 33.6 - } + height: 33.6, + }, }; // Verifying the position values of State1. expect(modifiedNodeData.State1.x0).toEqual( - expectedPositionValuesInPixels.State1.x0); + expectedPositionValuesInPixels.State1.x0 + ); expect(modifiedNodeData.State1.y0).toEqual( - expectedPositionValuesInPixels.State1.y0); + expectedPositionValuesInPixels.State1.y0 + ); expect(modifiedNodeData.State1.xLabel).toEqual( - expectedPositionValuesInPixels.State1.xLabel); + expectedPositionValuesInPixels.State1.xLabel + ); expect(modifiedNodeData.State1.yLabel).toEqual( - expectedPositionValuesInPixels.State1.yLabel); + expectedPositionValuesInPixels.State1.yLabel + ); expect(modifiedNodeData.State1.width).toEqual( - expectedPositionValuesInPixels.State1.width); + expectedPositionValuesInPixels.State1.width + ); expect(modifiedNodeData.State1.height).toEqual( - expectedPositionValuesInPixels.State1.height); + expectedPositionValuesInPixels.State1.height + ); }); }); diff --git a/core/templates/components/graph-services/graph-layout.service.ts b/core/templates/components/graph-services/graph-layout.service.ts index 3e1a6d09c4da..bcb612bddd93 100644 --- a/core/templates/components/graph-services/graph-layout.service.ts +++ b/core/templates/components/graph-services/graph-layout.service.ts @@ -18,11 +18,11 @@ import cloneDeep from 'lodash/cloneDeep'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { GraphLink, GraphNodes } from 'services/compute-graph.service'; +import {AppConstants} from 'app.constants'; +import {GraphLink, GraphNodes} from 'services/compute-graph.service'; export interface GraphBoundaries { bottom: number; @@ -72,7 +72,7 @@ export interface NodeDataDict { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class StateGraphLayoutService { MAX_INDENTATION_LEVEL = 2.5; @@ -84,7 +84,9 @@ export class StateGraphLayoutService { lastComputedArrangement: NodeDataDict | null = null; getGraphAsAdjacencyLists( - nodes: GraphNodes, links: GraphLink[]): GraphAdjacencyLists { + nodes: GraphNodes, + links: GraphLink[] + ): GraphAdjacencyLists { var adjacencyLists: GraphAdjacencyLists = {}; for (var nodeId in nodes) { @@ -103,7 +105,9 @@ export class StateGraphLayoutService { } getIndentationLevels( - adjacencyLists: GraphAdjacencyLists, trunkNodeIds: string[]): number[] { + adjacencyLists: GraphAdjacencyLists, + trunkNodeIds: string[] + ): number[] { var indentationLevels: number[] = []; // Recursively find and indent the longest shortcut for the segment of // nodes ranging from trunkNodeIds[startInd] to trunkNodeIds[endInd] @@ -112,8 +116,10 @@ export class StateGraphLayoutService { // this interval, in which case we indent all nodes from A + 1 onwards. // NOTE: this mutates indentationLevels as a side-effect. var indentLongestShortcut = (startInd: number, endInd: number) => { - if (startInd >= endInd || - indentationLevels[startInd] >= this.MAX_INDENTATION_LEVEL) { + if ( + startInd >= endInd || + indentationLevels[startInd] >= this.MAX_INDENTATION_LEVEL + ) { return; } @@ -124,7 +130,8 @@ export class StateGraphLayoutService { var sourceNodeId = trunkNodeIds[sourceInd]; for (var i = 0; i < adjacencyLists[sourceNodeId].length; i++) { var possibleTargetInd = trunkNodeIds.indexOf( - adjacencyLists[sourceNodeId][i]); + adjacencyLists[sourceNodeId][i] + ); if (possibleTargetInd !== -1 && sourceInd < possibleTargetInd) { var targetInd = Math.min(possibleTargetInd, endInd + 1); if (targetInd - sourceInd > bestTargetInd - bestSourceInd) { @@ -180,8 +187,11 @@ export class StateGraphLayoutService { // - id: a unique id for the node. // - label: the full label of the node. computeLayout( - nodes: GraphNodes, links: GraphLink[], initNodeId: string, - finalNodeIds: string[]): NodeDataDict { + nodes: GraphNodes, + links: GraphLink[], + initNodeId: string, + finalNodeIds: string[] + ): NodeDataDict { var adjacencyLists = this.getGraphAsAdjacencyLists(nodes, links); // Find a long path through the graph from the initial state to a @@ -208,8 +218,7 @@ export class StateGraphLayoutService { numBacktrackingCalls++; if (numBacktrackingCalls <= MAX_BACKTRACKING_CALLS) { for (var i = 0; i < adjacencyLists[currentNodeId].length; i++) { - if (currentPath.indexOf( - adjacencyLists[currentNodeId][i]) === -1) { + if (currentPath.indexOf(adjacencyLists[currentNodeId][i]) === -1) { backtrack(adjacencyLists[currentNodeId][i]); } } @@ -257,10 +266,12 @@ export class StateGraphLayoutService { var maxDepth = 0; var maxOffsetInEachLevel: {[id: number]: number} = { - 0: 0 + 0: 0, }; var trunkNodesIndentationLevels = this.getIndentationLevels( - adjacencyLists, bestPath); + adjacencyLists, + bestPath + ); for (var i = 0; i < bestPath.length; i++) { nodeData[bestPath[i]].depth = maxDepth; @@ -307,13 +318,14 @@ export class StateGraphLayoutService { if (nodeData[linkTarget].depth === SENTINEL_DEPTH) { nodeData[linkTarget].depth = nodeData[currNodeId].depth + 1; - nodeData[linkTarget].offset = ( - nodeData[linkTarget].depth in maxOffsetInEachLevel ? - maxOffsetInEachLevel[nodeData[linkTarget].depth] + 1 : 0); + nodeData[linkTarget].offset = + nodeData[linkTarget].depth in maxOffsetInEachLevel + ? maxOffsetInEachLevel[nodeData[linkTarget].depth] + 1 + : 0; maxDepth = Math.max(maxDepth, nodeData[linkTarget].depth); - maxOffsetInEachLevel[nodeData[linkTarget].depth] = ( - nodeData[linkTarget].offset); + maxOffsetInEachLevel[nodeData[linkTarget].depth] = + nodeData[linkTarget].offset; } if (queue.indexOf(linkTarget) === -1) { @@ -349,7 +361,7 @@ export class StateGraphLayoutService { if (nodeData[nodeId].depth !== SENTINEL_DEPTH) { nodePositionsToIds[nodeData[nodeId].depth].push({ nodeId: nodeId, - offset: nodeData[nodeId].offset + offset: nodeData[nodeId].offset, }); } } @@ -383,8 +395,7 @@ export class StateGraphLayoutService { } nodeData[nodePositionsToIds[i][j].nodeId].depth = currentDepth; - nodeData[nodePositionsToIds[i][j].nodeId].offset = ( - currentLeftOffset); + nodeData[nodePositionsToIds[i][j].nodeId].offset = currentLeftOffset; currentLeftOffset += 1; } @@ -423,42 +434,48 @@ export class StateGraphLayoutService { // fraction of the total width, given a horizontal offset in terms of // grid rectangles. var getHorizontalPosition = (offsetInGridRectangles: number) => { - var fractionalGridWidth = ( - (1.0 - HORIZONTAL_EDGE_PADDING_FRACTION * 2) / totalColumns); + var fractionalGridWidth = + (1.0 - HORIZONTAL_EDGE_PADDING_FRACTION * 2) / totalColumns; return ( HORIZONTAL_EDGE_PADDING_FRACTION + - fractionalGridWidth * offsetInGridRectangles); + fractionalGridWidth * offsetInGridRectangles + ); }; // Helper function that returns a vertical position, in terms of a // fraction of the total height, given a vertical offset in terms of // grid rectangles. var getVerticalPosition = (offsetInGridRectangles: number) => { - var fractionalGridHeight = ( - (1.0 - VERTICAL_EDGE_PADDING_FRACTION * 2) / totalRows); + var fractionalGridHeight = + (1.0 - VERTICAL_EDGE_PADDING_FRACTION * 2) / totalRows; return ( VERTICAL_EDGE_PADDING_FRACTION + - fractionalGridHeight * offsetInGridRectangles); + fractionalGridHeight * offsetInGridRectangles + ); }; for (var nodeId in nodeData) { nodeData[nodeId].y0 = getVerticalPosition( - nodeData[nodeId].depth + GRID_NODE_Y_PADDING_FRACTION); + nodeData[nodeId].depth + GRID_NODE_Y_PADDING_FRACTION + ); nodeData[nodeId].x0 = getHorizontalPosition( - nodeData[nodeId].offset + GRID_NODE_X_PADDING_FRACTION); + nodeData[nodeId].offset + GRID_NODE_X_PADDING_FRACTION + ); nodeData[nodeId].yLabel = getVerticalPosition( - nodeData[nodeId].depth + 0.5); - nodeData[nodeId].xLabel = getHorizontalPosition( - nodeData[nodeId].offset + 0.5) + X_LABEL_OFFSET_CHECKPOINT_ICON; - - nodeData[nodeId].height = ( - (1.0 - VERTICAL_EDGE_PADDING_FRACTION * 2) / totalRows - ) * (1.0 - GRID_NODE_Y_PADDING_FRACTION * 2); - nodeData[nodeId].width = ( - (1.0 - HORIZONTAL_EDGE_PADDING_FRACTION * 2) / totalColumns - ) * (1.0 - GRID_NODE_X_PADDING_FRACTION * 2) + - WIDTH_OFFSET_CHECKPOINT_ICON; + nodeData[nodeId].depth + 0.5 + ); + nodeData[nodeId].xLabel = + getHorizontalPosition(nodeData[nodeId].offset + 0.5) + + X_LABEL_OFFSET_CHECKPOINT_ICON; + + nodeData[nodeId].height = + ((1.0 - VERTICAL_EDGE_PADDING_FRACTION * 2) / totalRows) * + (1.0 - GRID_NODE_Y_PADDING_FRACTION * 2); + nodeData[nodeId].width = + ((1.0 - HORIZONTAL_EDGE_PADDING_FRACTION * 2) / totalColumns) * + (1.0 - GRID_NODE_X_PADDING_FRACTION * 2) + + WIDTH_OFFSET_CHECKPOINT_ICON; } // Assign id and label to each node. @@ -477,8 +494,10 @@ export class StateGraphLayoutService { queue.shift(); for (var i = 0; i < links.length; i++) { - if (links[i].target === currNodeId && - !nodeData[links[i].source].reachableFromEnd) { + if ( + links[i].target === currNodeId && + !nodeData[links[i].source].reachableFromEnd + ) { nodeData[links[i].source].reachableFromEnd = true; queue.push(links[i].source); } @@ -506,23 +525,23 @@ export class StateGraphLayoutService { var rightEdge = -INFINITY; for (var nodeId in nodeData) { - leftEdge = Math.min( - nodeData[nodeId].x0 - BORDER_PADDING, leftEdge); - topEdge = Math.min( - nodeData[nodeId].y0 - BORDER_PADDING, topEdge); + leftEdge = Math.min(nodeData[nodeId].x0 - BORDER_PADDING, leftEdge); + topEdge = Math.min(nodeData[nodeId].y0 - BORDER_PADDING, topEdge); rightEdge = Math.max( nodeData[nodeId].x0 + BORDER_PADDING + nodeData[nodeId].width, - rightEdge); + rightEdge + ); bottomEdge = Math.max( nodeData[nodeId].y0 + BORDER_PADDING + nodeData[nodeId].height, - bottomEdge); + bottomEdge + ); } return { bottom: bottomEdge, left: leftEdge, right: rightEdge, - top: topEdge + top: topEdge, }; } @@ -530,15 +549,15 @@ export class StateGraphLayoutService { State1.xLabel === State2.xLabel and State1.yLabel === State2.yLabel where State1 and State2 refers to objects inside nodeData. */ getAugmentedLinks( - nodeData: NodeDataDict, - nodeLinks: GraphLink[] + nodeData: NodeDataDict, + nodeLinks: GraphLink[] ): AugmentedLink[] { var links = cloneDeep(nodeLinks); var augmentedLinks: AugmentedLink[] = links.map(link => { return { source: cloneDeep(nodeData[link.source]), target: cloneDeep(nodeData[link.target]), - connectsDestIfStuck: cloneDeep(link.connectsDestIfStuck) + connectsDestIfStuck: cloneDeep(link.connectsDestIfStuck), }; }); @@ -565,15 +584,17 @@ export class StateGraphLayoutService { /* Fractional amount of truncation to be applied to the end of each link. */ - var startCutoff = (sourceWidth / 2) / Math.abs(dx); - var endCutoff = (targetWidth / 2) / Math.abs(dx); + var startCutoff = sourceWidth / 2 / Math.abs(dx); + var endCutoff = targetWidth / 2 / Math.abs(dx); if (dx === 0 || dy !== 0) { - startCutoff = ( - (dx === 0) ? (sourceHeight / 2) / Math.abs(dy) : - Math.min(startCutoff, (sourceHeight / 2) / Math.abs(dy))); - endCutoff = ( - (dx === 0) ? (targetHeight / 2) / Math.abs(dy) : - Math.min(endCutoff, (targetHeight / 2) / Math.abs(dy))); + startCutoff = + dx === 0 + ? sourceHeight / 2 / Math.abs(dy) + : Math.min(startCutoff, sourceHeight / 2 / Math.abs(dy)); + endCutoff = + dx === 0 + ? targetHeight / 2 / Math.abs(dy) + : Math.min(endCutoff, targetHeight / 2 / Math.abs(dy)); } var dxperp = targety - sourcey; @@ -590,18 +611,28 @@ export class StateGraphLayoutService { var endy = targety - endCutoff * dy; // Draw a quadratic bezier curve. - augmentedLinks[i].d = ( - 'M' + startx + ' ' + starty + ' Q ' + midx + ' ' + midy + - ' ' + endx + ' ' + endy); + augmentedLinks[i].d = + 'M' + + startx + + ' ' + + starty + + ' Q ' + + midx + + ' ' + + midy + + ' ' + + endx + + ' ' + + endy; } } return augmentedLinks; } modifyPositionValues( - nodeData: NodeDataDict, - graphWidth: number, - graphHeight: number + nodeData: NodeDataDict, + graphWidth: number, + graphHeight: number ): NodeDataDict { Object.keys(nodeData).forEach(nodeId => { nodeData[nodeId].x0 *= graphWidth; @@ -633,5 +664,9 @@ export class StateGraphLayoutService { } // Service for computing layout of state graph nodes. -angular.module('oppia').factory( - 'StateGraphLayoutService', downgradeInjectable(StateGraphLayoutService)); +angular + .module('oppia') + .factory( + 'StateGraphLayoutService', + downgradeInjectable(StateGraphLayoutService) + ); diff --git a/core/templates/components/interaction-display/dynamic-content.module.ts b/core/templates/components/interaction-display/dynamic-content.module.ts index f0e241c9b0df..3689b01b2763 100644 --- a/core/templates/components/interaction-display/dynamic-content.module.ts +++ b/core/templates/components/interaction-display/dynamic-content.module.ts @@ -18,22 +18,14 @@ import 'core-js/es7/reflect'; import 'zone.js'; -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { InteractionDisplayComponent } from './interaction-display.component'; +import {CommonModule} from '@angular/common'; +import {NgModule} from '@angular/core'; +import {InteractionDisplayComponent} from './interaction-display.component'; @NgModule({ - imports: [ - CommonModule, - ], - declarations: [ - InteractionDisplayComponent - ], - entryComponents: [ - ], - exports: [ - InteractionDisplayComponent - ], + imports: [CommonModule], + declarations: [InteractionDisplayComponent], + entryComponents: [], + exports: [InteractionDisplayComponent], }) - -export class DynamicContentModule { } +export class DynamicContentModule {} diff --git a/core/templates/components/interaction-display/interaction-display.component.spec.ts b/core/templates/components/interaction-display/interaction-display.component.spec.ts index a25b46ac2ba9..f581205ecf44 100644 --- a/core/templates/components/interaction-display/interaction-display.component.spec.ts +++ b/core/templates/components/interaction-display/interaction-display.component.spec.ts @@ -16,9 +16,14 @@ * @fileoverview Unit tests for interaction display component. */ -import { ComponentFactoryResolver, ComponentRef, SimpleChange, ViewContainerRef } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { InteractionDisplayComponent } from './interaction-display.component'; +import { + ComponentFactoryResolver, + ComponentRef, + SimpleChange, + ViewContainerRef, +} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {InteractionDisplayComponent} from './interaction-display.component'; describe('Interaction display', () => { let fixture: ComponentFixture; @@ -27,7 +32,7 @@ describe('Interaction display', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [InteractionDisplayComponent] + declarations: [InteractionDisplayComponent], }).compileComponents(); fixture = TestBed.createComponent(InteractionDisplayComponent); @@ -40,28 +45,28 @@ describe('Interaction display', () => { }); it('should build interaction using htmlData', () => { - componentInstance.htmlData = ( + componentInstance.htmlData = ''); + '[last-answer]="null">'; let setAttributeSpy = jasmine.createSpy('setAttribute'); let mockComponentRef = { changeDetectorRef: { - detectChanges: () => {} + detectChanges: () => {}, }, location: { nativeElement: { - setAttribute: setAttributeSpy - } + setAttribute: setAttributeSpy, + }, }, instance: { - placeholderWithValue: '' - } + placeholderWithValue: '', + }, }; componentInstance.viewContainerRef = { - createComponent: null + createComponent: null, } as ViewContainerRef; spyOn(componentFactoryResolver, 'resolveComponentFactory'); spyOn(componentInstance.viewContainerRef, 'createComponent') @@ -73,35 +78,36 @@ describe('Interaction display', () => { componentInstance.buildInteraction(); expect(setAttributeSpy).toHaveBeenCalled(); - expect(mockComponentRef.instance.placeholderWithValue.length) - .toBeGreaterThan(0); + expect( + mockComponentRef.instance.placeholderWithValue.length + ).toBeGreaterThan(0); }); it('should build interaction using htmlData and parentScope', () => { let lastAnswer = 'last-answer'; - componentInstance.htmlData = ( + componentInstance.htmlData = ''); + '[last-answer]="lastAnswer">'; let setAttributeSpy = jasmine.createSpy('setAttribute'); let mockComponentRef = { changeDetectorRef: { - detectChanges: () => {} + detectChanges: () => {}, }, location: { nativeElement: { - setAttribute: setAttributeSpy - } + setAttribute: setAttributeSpy, + }, }, instance: { placeholderWithValue: '', lastAnswer: '', - } + }, }; componentInstance.viewContainerRef = { - createComponent: null + createComponent: null, } as ViewContainerRef; componentInstance.parentScope = { lastAnswer, @@ -117,8 +123,9 @@ describe('Interaction display', () => { componentInstance.buildInteraction(); expect(setAttributeSpy).toHaveBeenCalled(); - expect(mockComponentRef.instance.placeholderWithValue.length) - .toBeGreaterThan(0); + expect( + mockComponentRef.instance.placeholderWithValue.length + ).toBeGreaterThan(0); expect(mockComponentRef.instance.lastAnswer).toEqual(lastAnswer); }); @@ -132,12 +139,12 @@ describe('Interaction display', () => { it('should rebuild interaction if htmlData is updated', () => { componentInstance.viewContainerRef = { - clear: () => {} + clear: () => {}, } as ViewContainerRef; spyOn(componentInstance, 'buildInteraction'); componentInstance.ngOnChanges({ - htmlData: new SimpleChange('previousValue', 'newValue', true) + htmlData: new SimpleChange('previousValue', 'newValue', true), }); expect(componentInstance.buildInteraction).toHaveBeenCalled(); diff --git a/core/templates/components/interaction-display/interaction-display.component.ts b/core/templates/components/interaction-display/interaction-display.component.ts index a2c6e7aa4c35..372e18ed5f38 100644 --- a/core/templates/components/interaction-display/interaction-display.component.ts +++ b/core/templates/components/interaction-display/interaction-display.component.ts @@ -16,13 +16,18 @@ * @fileoverview Component for dynamically building and showing interactions. */ -import { ChangeDetectorRef, Component, ComponentFactoryResolver, Input, +import { + ChangeDetectorRef, + Component, + ComponentFactoryResolver, + Input, SimpleChange, - ViewChild, ViewContainerRef } - from '@angular/core'; + ViewChild, + ViewContainerRef, +} from '@angular/core'; import camelCaseFromHyphen from 'utility/string-utility'; -import { TAG_TO_INTERACTION_MAPPING } from 'interactions/tag-to-interaction-mapping'; +import {TAG_TO_INTERACTION_MAPPING} from 'interactions/tag-to-interaction-mapping'; @Component({ selector: 'oppia-interaction-display', @@ -41,7 +46,9 @@ export class InteractionDisplayComponent { @Input() parentScope!: unknown; @ViewChild('interactionContainer', { - read: ViewContainerRef}) viewContainerRef!: ViewContainerRef; + read: ViewContainerRef, + }) + viewContainerRef!: ViewContainerRef; constructor( private componentFactoryResolver: ComponentFactoryResolver, @@ -57,22 +64,22 @@ export class InteractionDisplayComponent { let domparser = new DOMParser(); let dom = domparser.parseFromString(this.htmlData, 'text/html'); - if (dom.body.firstElementChild && - TAG_TO_INTERACTION_MAPPING[ - dom.body.firstElementChild.tagName]) { - let interaction = TAG_TO_INTERACTION_MAPPING[ - dom.body.firstElementChild.tagName]; + if ( + dom.body.firstElementChild && + TAG_TO_INTERACTION_MAPPING[dom.body.firstElementChild.tagName] + ) { + let interaction = + TAG_TO_INTERACTION_MAPPING[dom.body.firstElementChild.tagName]; - const componentFactory = this.componentFactoryResolver - .resolveComponentFactory(interaction); - const componentRef = this.viewContainerRef.createComponent( - componentFactory); + const componentFactory = + this.componentFactoryResolver.resolveComponentFactory(interaction); + const componentRef = + this.viewContainerRef.createComponent(componentFactory); let attributes = dom.body.firstElementChild.attributes; Array.from(attributes).forEach(attribute => { - let attributeNameInCamelCase = camelCaseFromHyphen( - attribute.name); + let attributeNameInCamelCase = camelCaseFromHyphen(attribute.name); let attributeValue = attribute.value; @@ -91,7 +98,9 @@ export class InteractionDisplayComponent { } } else { componentRef.location.nativeElement.setAttribute( - attribute.name, attributeValue); + attribute.name, + attributeValue + ); } componentRef.instance[attributeNameInCamelCase] = attributeValue; @@ -103,9 +112,11 @@ export class InteractionDisplayComponent { } } - ngOnChanges(changes: { htmlData: SimpleChange }): void { - if (changes.htmlData.currentValue !== changes.htmlData.previousValue && - this.viewContainerRef) { + ngOnChanges(changes: {htmlData: SimpleChange}): void { + if ( + changes.htmlData.currentValue !== changes.htmlData.previousValue && + this.viewContainerRef + ) { this.viewContainerRef.clear(); this.buildInteraction(); } diff --git a/core/templates/components/keyboard-shortcut-help/keyboard-shortcut-help-modal.component.spec.ts b/core/templates/components/keyboard-shortcut-help/keyboard-shortcut-help-modal.component.spec.ts index 29b7aeb0a49a..51b45fc3ef1f 100644 --- a/core/templates/components/keyboard-shortcut-help/keyboard-shortcut-help-modal.component.spec.ts +++ b/core/templates/components/keyboard-shortcut-help/keyboard-shortcut-help-modal.component.spec.ts @@ -16,15 +16,18 @@ * @fileoverview Unit tests for KeyboardShortcutHelpModalComponent. */ -import { ComponentFixture, fakeAsync, TestBed, async } from - '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { UrlService } from 'services/contextual/url.service'; -import { ContextService } from 'services/context.service'; +import { + ComponentFixture, + fakeAsync, + TestBed, + async, +} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {UrlService} from 'services/contextual/url.service'; +import {ContextService} from 'services/context.service'; -import { KeyboardShortcutHelpModalComponent } from - './keyboard-shortcut-help-modal.component'; +import {KeyboardShortcutHelpModalComponent} from './keyboard-shortcut-help-modal.component'; class MockActiveModal { dismiss(): void { @@ -44,9 +47,9 @@ describe('KeyboardShortcutHelpModalComponent', () => { providers: [ { provide: NgbActiveModal, - useClass: MockActiveModal + useClass: MockActiveModal, }, - ] + ], }).compileComponents(); })); @@ -59,15 +62,18 @@ describe('KeyboardShortcutHelpModalComponent', () => { }); it('should load the library page shortcut descriptions', () => { - const mockLibraryPage = spyOn( - urlService, 'getPathname').and.returnValue('/community-library'); + const mockLibraryPage = spyOn(urlService, 'getPathname').and.returnValue( + '/community-library' + ); component.ngOnInit(); expect(mockLibraryPage).toHaveBeenCalled(); }); it('should load the exploration player shortcut descriptions', () => { const mockExplorationPlayerPage = spyOn( - contextService, 'isInExplorationPlayerPage').and.returnValue(true); + contextService, + 'isInExplorationPlayerPage' + ).and.returnValue(true); component.ngOnInit(); expect(mockExplorationPlayerPage).toHaveBeenCalled(); }); @@ -75,7 +81,8 @@ describe('KeyboardShortcutHelpModalComponent', () => { it('should dismiss the modal when clicked on cancel', fakeAsync(() => { const dismissSpy = spyOn(ngbActiveModal, 'dismiss').and.callThrough(); const closeButtonDE = fixture.debugElement.query( - By.css('.modal-footer .btn.btn-secondary')); + By.css('.modal-footer .btn.btn-secondary') + ); closeButtonDE.nativeElement.click(); diff --git a/core/templates/components/keyboard-shortcut-help/keyboard-shortcut-help-modal.component.ts b/core/templates/components/keyboard-shortcut-help/keyboard-shortcut-help-modal.component.ts index 823fab299b84..ceed71f6459b 100644 --- a/core/templates/components/keyboard-shortcut-help/keyboard-shortcut-help-modal.component.ts +++ b/core/templates/components/keyboard-shortcut-help/keyboard-shortcut-help-modal.component.ts @@ -16,21 +16,22 @@ * @fileoverview Controller for keyboard shortcut help modal. */ -import { Component, OnInit } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { UrlService } from 'services/contextual/url.service'; -import { ContextService } from 'services/context.service'; +import {Component, OnInit} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {UrlService} from 'services/contextual/url.service'; +import {ContextService} from 'services/context.service'; @Component({ selector: 'keyboard-shortcut-help-modal', templateUrl: './keyboard-shortcut-help-modal.component.html', - styleUrls: [] + styleUrls: [], }) export class KeyboardShortcutHelpModalComponent implements OnInit { constructor( private activeModal: NgbActiveModal, private urlService: UrlService, - private contextService: ContextService) {} + private contextService: ContextService + ) {} KEYBOARD_SHORTCUTS = {}; diff --git a/core/templates/components/on-screen-keyboard/on-screen-keyboard.component.spec.ts b/core/templates/components/on-screen-keyboard/on-screen-keyboard.component.spec.ts index 1730c1c24c41..32c77c284fdb 100644 --- a/core/templates/components/on-screen-keyboard/on-screen-keyboard.component.spec.ts +++ b/core/templates/components/on-screen-keyboard/on-screen-keyboard.component.spec.ts @@ -16,11 +16,13 @@ * @fileoverview Unit tests for the on screen keyboard component. */ -import { DeviceInfoService } from 'services/contextual/device-info.service'; -import { GuppyInitializationService, GuppyObject } from - 'services/guppy-initialization.service'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { OnScreenKeyboardComponent } from './on-screen-keyboard.component'; +import {DeviceInfoService} from 'services/contextual/device-info.service'; +import { + GuppyInitializationService, + GuppyObject, +} from 'services/guppy-initialization.service'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {OnScreenKeyboardComponent} from './on-screen-keyboard.component'; describe('On Screen Keyboard', () => { let guppyInitializationService: GuppyInitializationService; @@ -28,7 +30,7 @@ describe('On Screen Keyboard', () => { let fixture: ComponentFixture; let componentInstance: OnScreenKeyboardComponent; let guppy: {isActive: boolean} = { - isActive: false + isActive: false, }; class MockGuppy implements Guppy { @@ -71,9 +73,7 @@ describe('On Screen Keyboard', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - OnScreenKeyboardComponent - ] + declarations: [OnScreenKeyboardComponent], }); })); @@ -99,80 +99,82 @@ describe('On Screen Keyboard', () => { spyOn(deviceInfoService, 'isMobileUserAgent').and.returnValue(true); spyOn(deviceInfoService, 'hasTouchEvents').and.returnValue(true); spyOn(guppyInitializationService, 'findActiveGuppyObject').and.returnValue( - undefined); + undefined + ); expect(componentInstance.showOSK()).toBeFalse(); }); it('should set showOSK value to false upon hiding the OSK', () => { spyOn(deviceInfoService, 'isMobileUserAgent').and.returnValue(true); spyOn(deviceInfoService, 'hasTouchEvents').and.returnValue(true); - spyOn(guppyInitializationService, 'findActiveGuppyObject').and - .returnValue(new GuppyObject('divId', new MockGuppy())); + spyOn(guppyInitializationService, 'findActiveGuppyObject').and.returnValue( + new GuppyObject('divId', new MockGuppy()) + ); expect(guppyInitializationService.getShowOSK()).toBeTrue(); componentInstance.hideOSK(); expect(guppyInitializationService.getShowOSK()).toBeFalse(); }); - it('should activate the instance upon each key press function call', - () => { - spyOn(deviceInfoService, 'isMobileUserAgent').and.returnValue(true); - spyOn(deviceInfoService, 'hasTouchEvents').and.returnValue(true); - spyOn(guppyInitializationService, 'findActiveGuppyObject') - .and.returnValue(new GuppyObject('divId', new MockGuppy())); - expect(componentInstance.showOSK()).toBeTrue(); + it('should activate the instance upon each key press function call', () => { + spyOn(deviceInfoService, 'isMobileUserAgent').and.returnValue(true); + spyOn(deviceInfoService, 'hasTouchEvents').and.returnValue(true); + spyOn(guppyInitializationService, 'findActiveGuppyObject').and.returnValue( + new GuppyObject('divId', new MockGuppy()) + ); + expect(componentInstance.showOSK()).toBeTrue(); - expect(guppy.isActive).toBeFalse(); - componentInstance.activateGuppy(); - expect(guppy.isActive).toBeTrue(); + expect(guppy.isActive).toBeFalse(); + componentInstance.activateGuppy(); + expect(guppy.isActive).toBeTrue(); - guppy.isActive = false; + guppy.isActive = false; - expect(guppy.isActive).toBeFalse(); - componentInstance.changeTab('newTab'); - expect(guppy.isActive).toBeTrue(); + expect(guppy.isActive).toBeFalse(); + componentInstance.changeTab('newTab'); + expect(guppy.isActive).toBeTrue(); - guppy.isActive = false; + guppy.isActive = false; - expect(guppy.isActive).toBeFalse(); - componentInstance.insertString('x'); - componentInstance.insertString('α'); - expect(guppy.isActive).toBeTrue(); + expect(guppy.isActive).toBeFalse(); + componentInstance.insertString('x'); + componentInstance.insertString('α'); + expect(guppy.isActive).toBeTrue(); - guppy.isActive = false; + guppy.isActive = false; - expect(guppy.isActive).toBeFalse(); - componentInstance.insertSymbol('x'); - expect(guppy.isActive).toBeTrue(); + expect(guppy.isActive).toBeFalse(); + componentInstance.insertSymbol('x'); + expect(guppy.isActive).toBeTrue(); - guppy.isActive = false; + guppy.isActive = false; - expect(guppy.isActive).toBeFalse(); - componentInstance.backspace(); - expect(guppy.isActive).toBeTrue(); + expect(guppy.isActive).toBeFalse(); + componentInstance.backspace(); + expect(guppy.isActive).toBeTrue(); - guppy.isActive = false; + guppy.isActive = false; - expect(guppy.isActive).toBeFalse(); - componentInstance.left(); - expect(guppy.isActive).toBeTrue(); + expect(guppy.isActive).toBeFalse(); + componentInstance.left(); + expect(guppy.isActive).toBeTrue(); - guppy.isActive = false; + guppy.isActive = false; - expect(guppy.isActive).toBeFalse(); - componentInstance.right(); - expect(guppy.isActive).toBeTrue(); + expect(guppy.isActive).toBeFalse(); + componentInstance.right(); + expect(guppy.isActive).toBeTrue(); - guppy.isActive = false; + guppy.isActive = false; - expect(guppy.isActive).toBeFalse(); - componentInstance.exponent('2'); - expect(guppy.isActive).toBeTrue(); - } - ); + expect(guppy.isActive).toBeFalse(); + componentInstance.exponent('2'); + expect(guppy.isActive).toBeTrue(); + }); it('should get static image url', () => { let imagePath = '/path/to/image.png'; expect(componentInstance.getStaticImageUrl(imagePath)).toBe( - '/assets/images/path/to/image.png'); + '/assets/images/path/to/image.png' + ); }); }); diff --git a/core/templates/components/on-screen-keyboard/on-screen-keyboard.component.ts b/core/templates/components/on-screen-keyboard/on-screen-keyboard.component.ts index 8374a31fba5c..98f722a09fe2 100644 --- a/core/templates/components/on-screen-keyboard/on-screen-keyboard.component.ts +++ b/core/templates/components/on-screen-keyboard/on-screen-keyboard.component.ts @@ -17,24 +17,24 @@ * interactions. */ -import { Component } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { AppConstants } from 'app.constants'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { DeviceInfoService } from 'services/contextual/device-info.service'; -import { GuppyInitializationService } from 'services/guppy-initialization.service'; +import {Component} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {AppConstants} from 'app.constants'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {DeviceInfoService} from 'services/contextual/device-info.service'; +import {GuppyInitializationService} from 'services/guppy-initialization.service'; @Component({ selector: 'oppia-on-screen-keyboard', - templateUrl: './on-screen-keyboard.component.html' + templateUrl: './on-screen-keyboard.component.html', }) export class OnScreenKeyboardComponent { // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 engine!: { - 'insert_string': (arg0: string) => void; - 'insert_symbol': (arg0: string) => void; + insert_string: (arg0: string) => void; + insert_symbol: (arg0: string) => void; backspace: () => void; left: () => void; right: () => void; @@ -48,16 +48,31 @@ export class OnScreenKeyboardComponent { lettersTab: string = AppConstants.OSK_LETTERS_TAB; mainTab: string = AppConstants.OSK_MAIN_TAB; greekSymbols: string[] = Object.values( - AppConstants.GREEK_LETTER_NAMES_TO_SYMBOLS); + AppConstants.GREEK_LETTER_NAMES_TO_SYMBOLS + ); greekLetters: string[] = Object.keys( - AppConstants.GREEK_LETTER_NAMES_TO_SYMBOLS); + AppConstants.GREEK_LETTER_NAMES_TO_SYMBOLS + ); currentTab: string = this.mainTab; lettersInKeyboardLayout: string[] = ['qwertyuiop', 'asdfghjkl', 'zxcvbnm']; functions: string[] = [ - 'log', 'ln', 'sin', 'cos', 'tan', 'sec', 'csc', 'cot', 'arcsin', - 'arccos', 'arctan', 'sinh', 'cosh', 'tanh']; + 'log', + 'ln', + 'sin', + 'cos', + 'tan', + 'sec', + 'csc', + 'cot', + 'arcsin', + 'arccos', + 'arctan', + 'sinh', + 'cosh', + 'tanh', + ]; constructor( private deviceInfoService: DeviceInfoService, @@ -121,12 +136,13 @@ export class OnScreenKeyboardComponent { showOSK(): boolean { if ( !this.deviceInfoService.isMobileUserAgent() || - !this.deviceInfoService.hasTouchEvents()) { + !this.deviceInfoService.hasTouchEvents() + ) { return false; } let showOSK = this.guppyInitializationService.getShowOSK(); - let activeGuppyObject = ( - this.guppyInitializationService.findActiveGuppyObject()); + let activeGuppyObject = + this.guppyInitializationService.findActiveGuppyObject(); if (showOSK && activeGuppyObject !== undefined) { this.guppyInstance = activeGuppyObject.guppyInstance; this.engine = this.guppyInstance.engine; @@ -139,5 +155,9 @@ export class OnScreenKeyboardComponent { } } -angular.module('oppia').directive('oppiaOnScreenKeyboard', - downgradeComponent({ component: OnScreenKeyboardComponent })); +angular + .module('oppia') + .directive( + 'oppiaOnScreenKeyboard', + downgradeComponent({component: OnScreenKeyboardComponent}) + ); diff --git a/core/templates/components/oppia-angular-root.component.spec.ts b/core/templates/components/oppia-angular-root.component.spec.ts index dd54195eca21..9d077b789f68 100644 --- a/core/templates/components/oppia-angular-root.component.spec.ts +++ b/core/templates/components/oppia-angular-root.component.spec.ts @@ -16,24 +16,26 @@ * @fileoverview Unit tests for the OppiaAngularRootComponent. */ -import { ComponentFixture, TestBed, async } from - '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { AngularFireAuth } from '@angular/fire/auth'; -import { CookieModule } from 'ngx-cookie'; -import { OppiaAngularRootComponent, registerCustomElements } from './oppia-angular-root.component'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { Injector, NO_ERRORS_SCHEMA } from '@angular/core'; +import {ComponentFixture, TestBed, async} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {AngularFireAuth} from '@angular/fire/auth'; +import {CookieModule} from 'ngx-cookie'; +import { + OppiaAngularRootComponent, + registerCustomElements, +} from './oppia-angular-root.component'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {Injector, NO_ERRORS_SCHEMA} from '@angular/core'; // This throws "TS2307". We need to // suppress this error because rte-text-components are not strictly typed yet. // @ts-ignore -import { RichTextComponentsModule } from 'rich_text_components/rich-text-components.module'; -import { CkEditorInitializerService } from './ck-editor-helpers/ck-editor-4-widgets.initializer'; -import { MetaTagCustomizationService } from 'services/contextual/meta-tag-customization.service'; -import { DocumentAttributeCustomizationService } from 'services/contextual/document-attribute-customization.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { I18nService } from 'i18n/i18n.service'; -import { MockI18nService } from 'tests/unit-test-utils'; +import {RichTextComponentsModule} from 'rich_text_components/rich-text-components.module'; +import {CkEditorInitializerService} from './ck-editor-helpers/ck-editor-4-widgets.initializer'; +import {MetaTagCustomizationService} from 'services/contextual/meta-tag-customization.service'; +import {DocumentAttributeCustomizationService} from 'services/contextual/document-attribute-customization.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {I18nService} from 'i18n/i18n.service'; +import {MockI18nService} from 'tests/unit-test-utils'; let component: OppiaAngularRootComponent; let fixture: ComponentFixture; @@ -44,11 +46,11 @@ class MockWindowRef { reload: () => {}, toString: () => { return 'http://localhost:8181/?lang=es'; - } + }, }, history: { - pushState(data, title: string, url?: string | null) {} - } + pushState(data, title: string, url?: string | null) {}, + }, }; } @@ -58,54 +60,60 @@ class WordCount extends HTMLParagraphElement { } } -describe('OppiaAngularRootComponent', function() { +describe('OppiaAngularRootComponent', function () { let i18nService: I18nService; let emitSpy: jasmine.Spy; beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule, + imports: [ + HttpClientTestingModule, RichTextComponentsModule, - CookieModule.forRoot()], + CookieModule.forRoot(), + ], declarations: [OppiaAngularRootComponent], providers: [ { provide: AngularFireAuth, - useValue: null + useValue: null, }, { provide: WindowRef, - useClass: MockWindowRef + useClass: MockWindowRef, }, MetaTagCustomizationService, { provide: DocumentAttributeCustomizationService, useValue: { - addAttribute: (attr, code) => {} - } + addAttribute: (attr, code) => {}, + }, }, { provide: I18nService, - useClass: MockI18nService - } + useClass: MockI18nService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(OppiaAngularRootComponent); component = fixture.componentInstance; - let metaTagCustomizationService = ( - TestBed.inject(MetaTagCustomizationService)); + let metaTagCustomizationService = TestBed.inject( + MetaTagCustomizationService + ); emitSpy = spyOn(component.initialized, 'emit'); - spyOn(metaTagCustomizationService, 'addOrReplaceMetaTags') - .and.returnValue(); + spyOn( + metaTagCustomizationService, + 'addOrReplaceMetaTags' + ).and.returnValue(); i18nService = TestBed.inject(I18nService); })); it('should only intialize rteElements once', () => { expect(OppiaAngularRootComponent.rteElementsAreInitialized).toBeTrue(); const componentInstance = TestBed.createComponent( - OppiaAngularRootComponent).componentInstance; + OppiaAngularRootComponent + ).componentInstance; expect(componentInstance).toBeDefined(); spyOn(customElements, 'get').and.callFake(() => WordCount); registerCustomElements(TestBed.inject(Injector)); @@ -113,7 +121,8 @@ describe('OppiaAngularRootComponent', function() { it('should emit once ngAfterViewInit is called', () => { spyOn(CkEditorInitializerService, 'ckEditorInitializer').and.callFake( - () => {}); + () => {} + ); component.ngAfterViewInit(); TestBed.inject(I18nLanguageCodeService).setI18nLanguageCode('en'); diff --git a/core/templates/components/oppia-angular-root.component.ts b/core/templates/components/oppia-angular-root.component.ts index aa95a8ece672..dd3bc026fb8d 100644 --- a/core/templates/components/oppia-angular-root.component.ts +++ b/core/templates/components/oppia-angular-root.component.ts @@ -61,42 +61,44 @@ * loading */ -import { Component, Output, AfterViewInit, EventEmitter, Injector, NgZone } from '@angular/core'; -import { createCustomElement } from '@angular/elements'; -import { ClassroomBackendApiService } from - 'domain/classroom/classroom-backend-api.service'; -import { ContextService } from 'services/context.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { PageTitleService } from 'services/page-title.service'; -import { ProfilePageBackendApiService } from - 'pages/profile-page/profile-page-backend-api.service'; -import { RatingComputationService } from - 'components/ratings/rating-computation/rating-computation.service'; -import { ReviewTestBackendApiService } from - 'domain/review_test/review-test-backend-api.service'; -import { StoryViewerBackendApiService } from - 'domain/story_viewer/story-viewer-backend-api.service'; -import { ServicesConstants } from 'services/services.constants'; +import { + Component, + Output, + AfterViewInit, + EventEmitter, + Injector, + NgZone, +} from '@angular/core'; +import {createCustomElement} from '@angular/elements'; +import {ClassroomBackendApiService} from 'domain/classroom/classroom-backend-api.service'; +import {ContextService} from 'services/context.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {PageTitleService} from 'services/page-title.service'; +import {ProfilePageBackendApiService} from 'pages/profile-page/profile-page-backend-api.service'; +import {RatingComputationService} from 'components/ratings/rating-computation/rating-computation.service'; +import {ReviewTestBackendApiService} from 'domain/review_test/review-test-backend-api.service'; +import {StoryViewerBackendApiService} from 'domain/story_viewer/story-viewer-backend-api.service'; +import {ServicesConstants} from 'services/services.constants'; // Relative path used as an work around to get the angular compiler and webpack // build to not complain. // TODO(#16309): Fix relative imports. import '../third-party-imports/ckeditor.import'; -import { NoninteractiveCollapsible } from 'rich_text_components/Collapsible/directives/oppia-noninteractive-collapsible.component'; -import { NoninteractiveImage } from 'rich_text_components/Image/directives/oppia-noninteractive-image.component'; -import { NoninteractiveLink } from 'rich_text_components/Link/directives/oppia-noninteractive-link.component'; -import { NoninteractiveMath } from 'rich_text_components/Math/directives/oppia-noninteractive-math.component'; -import { NoninteractiveSkillreview } from 'rich_text_components/Skillreview/directives/oppia-noninteractive-skillreview.component'; -import { NoninteractiveTabs } from 'rich_text_components/Tabs/directives/oppia-noninteractive-tabs.component'; -import { NoninteractiveVideo } from 'rich_text_components/Video/directives/oppia-noninteractive-video.component'; -import { CkEditorInitializerService } from './ck-editor-helpers/ck-editor-4-widgets.initializer'; -import { HtmlEscaperService } from 'services/html-escaper.service'; -import { MetaTagCustomizationService } from 'services/contextual/meta-tag-customization.service'; -import { AppConstants } from 'app.constants'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { UrlService } from 'services/contextual/url.service'; -import { I18nService } from 'i18n/i18n.service'; -import { RteHelperService } from 'services/rte-helper.service'; +import {NoninteractiveCollapsible} from 'rich_text_components/Collapsible/directives/oppia-noninteractive-collapsible.component'; +import {NoninteractiveImage} from 'rich_text_components/Image/directives/oppia-noninteractive-image.component'; +import {NoninteractiveLink} from 'rich_text_components/Link/directives/oppia-noninteractive-link.component'; +import {NoninteractiveMath} from 'rich_text_components/Math/directives/oppia-noninteractive-math.component'; +import {NoninteractiveSkillreview} from 'rich_text_components/Skillreview/directives/oppia-noninteractive-skillreview.component'; +import {NoninteractiveTabs} from 'rich_text_components/Tabs/directives/oppia-noninteractive-tabs.component'; +import {NoninteractiveVideo} from 'rich_text_components/Video/directives/oppia-noninteractive-video.component'; +import {CkEditorInitializerService} from './ck-editor-helpers/ck-editor-4-widgets.initializer'; +import {HtmlEscaperService} from 'services/html-escaper.service'; +import {MetaTagCustomizationService} from 'services/contextual/meta-tag-customization.service'; +import {AppConstants} from 'app.constants'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {UrlService} from 'services/contextual/url.service'; +import {I18nService} from 'i18n/i18n.service'; +import {RteHelperService} from 'services/rte-helper.service'; const componentMap = { Collapsible: { @@ -119,14 +121,15 @@ const componentMap = { }, Video: { component_class: NoninteractiveVideo, - } + }, }; export const registerCustomElements = (injector: Injector): void => { for (const rteKey of Object.keys(ServicesConstants.RTE_COMPONENT_SPECS)) { const rteElement = createCustomElement( componentMap[rteKey].component_class, - {injector}); + {injector} + ); // Check if the custom elements have been previously defined. We can't // redefine custom elements with the same id. Root cause for the element // being already defined is not yet known. Can possibly be a side effect of @@ -135,14 +138,14 @@ export const registerCustomElements = (injector: Injector): void => { if ( customElements.get( 'oppia-noninteractive-ckeditor-' + - ServicesConstants.RTE_COMPONENT_SPECS[rteKey].frontend_id + ServicesConstants.RTE_COMPONENT_SPECS[rteKey].frontend_id ) !== undefined ) { continue; } customElements.define( 'oppia-noninteractive-ckeditor-' + - ServicesConstants.RTE_COMPONENT_SPECS[rteKey].frontend_id, + ServicesConstants.RTE_COMPONENT_SPECS[rteKey].frontend_id, rteElement ); } @@ -150,7 +153,7 @@ export const registerCustomElements = (injector: Injector): void => { @Component({ selector: 'oppia-angular-root', - templateUrl: './oppia-angular-root.component.html' + templateUrl: './oppia-angular-root.component.html', }) export class OppiaAngularRootComponent implements AfterViewInit { @Output() public initialized: EventEmitter = new EventEmitter(); @@ -204,20 +207,20 @@ export class OppiaAngularRootComponent implements AfterViewInit { this.ngZone ); }); - OppiaAngularRootComponent.classroomBackendApiService = ( - this.classroomBackendApiService); - OppiaAngularRootComponent.i18nLanguageCodeService = ( - this.i18nLanguageCodeService); + OppiaAngularRootComponent.classroomBackendApiService = + this.classroomBackendApiService; + OppiaAngularRootComponent.i18nLanguageCodeService = + this.i18nLanguageCodeService; OppiaAngularRootComponent.ngZone = this.ngZone; OppiaAngularRootComponent.pageTitleService = this.pageTitleService; - OppiaAngularRootComponent.profilePageBackendApiService = ( - this.profilePageBackendApiService); - OppiaAngularRootComponent.ratingComputationService = ( - this.ratingComputationService); - OppiaAngularRootComponent.reviewTestBackendApiService = ( - this.reviewTestBackendApiService); - OppiaAngularRootComponent.storyViewerBackendApiService = ( - this.storyViewerBackendApiService); + OppiaAngularRootComponent.profilePageBackendApiService = + this.profilePageBackendApiService; + OppiaAngularRootComponent.ratingComputationService = + this.ratingComputationService; + OppiaAngularRootComponent.reviewTestBackendApiService = + this.reviewTestBackendApiService; + OppiaAngularRootComponent.storyViewerBackendApiService = + this.storyViewerBackendApiService; OppiaAngularRootComponent.injector = this.injector; // Initialize dynamic meta tags. @@ -225,47 +228,48 @@ export class OppiaAngularRootComponent implements AfterViewInit { { propertyType: 'name', propertyValue: 'application-name', - content: AppConstants.SITE_NAME + content: AppConstants.SITE_NAME, }, { propertyType: 'name', propertyValue: 'msapplication-square310x310logo', content: this.getAssetUrl( - '/assets/images/logo/msapplication-large.png') + '/assets/images/logo/msapplication-large.png' + ), }, { propertyType: 'name', propertyValue: 'msapplication-wide310x150logo', - content: this.getAssetUrl( - '/assets/images/logo/msapplication-wide.png') + content: this.getAssetUrl('/assets/images/logo/msapplication-wide.png'), }, { propertyType: 'name', propertyValue: 'msapplication-square150x150logo', content: this.getAssetUrl( - '/assets/images/logo/msapplication-square.png') + '/assets/images/logo/msapplication-square.png' + ), }, { propertyType: 'name', propertyValue: 'msapplication-square70x70logo', - content: this.getAssetUrl( - '/assets/images/logo/msapplication-tiny.png') + content: this.getAssetUrl('/assets/images/logo/msapplication-tiny.png'), }, { propertyType: 'property', propertyValue: 'og:url', - content: this.urlService.getCurrentLocation().href + content: this.urlService.getCurrentLocation().href, }, { propertyType: 'property', propertyValue: 'og:image', content: this.urlInterpolationService.getStaticImageUrl( - '/logo/288x288_logo_mint.webp') - } + '/logo/288x288_logo_mint.webp' + ), + }, ]); // Initialize translations. - this.i18nService.directionChangeEventEmitter.subscribe((direction) => { + this.i18nService.directionChangeEventEmitter.subscribe(direction => { this.direction = direction; }); this.i18nService.initialize(); diff --git a/core/templates/components/profile-link-directives/profile-link-image.component.spec.ts b/core/templates/components/profile-link-directives/profile-link-image.component.spec.ts index 1533b7d49c7f..fa5c05d0526c 100644 --- a/core/templates/components/profile-link-directives/profile-link-image.component.spec.ts +++ b/core/templates/components/profile-link-directives/profile-link-image.component.spec.ts @@ -12,14 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { RouterModule } from '@angular/router'; -import { APP_BASE_HREF } from '@angular/common'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {RouterModule} from '@angular/router'; +import {APP_BASE_HREF} from '@angular/common'; -import { UserService } from 'services/user.service'; -import { SmartRouterModule } from 'hybrid-router-module-provider'; -import { ProfileLinkImageComponent } from './profile-link-image.component'; +import {UserService} from 'services/user.service'; +import {SmartRouterModule} from 'hybrid-router-module-provider'; +import {ProfileLinkImageComponent} from './profile-link-image.component'; /** * @fileoverview Unit tests for ProfileLinkImageComponent. @@ -37,15 +43,15 @@ describe('ProfileLinkImageComponent', () => { // TODO(#13443): Remove hybrid router module provider once all pages are // migrated to angular router. SmartRouterModule, - RouterModule.forRoot([]) + RouterModule.forRoot([]), ], - declarations: [ - ProfileLinkImageComponent + declarations: [ProfileLinkImageComponent], + providers: [ + { + provide: APP_BASE_HREF, + useValue: '/', + }, ], - providers: [{ - provide: APP_BASE_HREF, - useValue: '/' - }] }).compileComponents(); })); @@ -57,8 +63,10 @@ describe('ProfileLinkImageComponent', () => { it('should show profile picture on initialisation', fakeAsync(() => { component.username = 'user1'; - spyOn(userService, 'getProfileImageDataUrl').and.returnValue( - ['default-image-url-png', 'default-image-url-webp']); + spyOn(userService, 'getProfileImageDataUrl').and.returnValue([ + 'default-image-url-png', + 'default-image-url-webp', + ]); component.ngOnInit(); tick(); diff --git a/core/templates/components/profile-link-directives/profile-link-image.component.ts b/core/templates/components/profile-link-directives/profile-link-image.component.ts index e7c7e148eb8c..00f129d57b9f 100644 --- a/core/templates/components/profile-link-directives/profile-link-image.component.ts +++ b/core/templates/components/profile-link-directives/profile-link-image.component.ts @@ -16,15 +16,15 @@ * @fileoverview Directive for creating image links to a user's profile page. */ -import { Component, OnInit, Input } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { AppConstants } from 'app.constants'; -import { UserService } from 'services/user.service'; +import {Component, OnInit, Input} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {AppConstants} from 'app.constants'; +import {UserService} from 'services/user.service'; @Component({ selector: 'profile-link-image', templateUrl: './profile-link-image.component.html', - styleUrls: [] + styleUrls: [], }) export class ProfileLinkImageComponent implements OnInit { // These properties are initialized using Angular lifecycle hooks @@ -33,30 +33,34 @@ export class ProfileLinkImageComponent implements OnInit { @Input() username!: string; profilePicturePngDataUrl!: string; profilePictureWebpDataUrl!: string; - profileUrl = ( - '/' + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PROFILE.ROUTE.replace( - ':username_fragment', this.username - ) - ); + profileUrl = + '/' + + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PROFILE.ROUTE.replace( + ':username_fragment', + this.username + ); - constructor( - private userService: UserService - ) {} + constructor(private userService: UserService) {} isUsernameLinkable(username: string): boolean { return ['admin', 'OppiaMigrationBot'].indexOf(username) === -1; } ngOnInit(): void { - this.profileUrl = ( - '/' + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PROFILE.ROUTE.replace( - ':username_fragment', this.username - ) - ); - [this.profilePicturePngDataUrl, this.profilePictureWebpDataUrl] = ( - this.userService.getProfileImageDataUrl(this.username)); + this.profileUrl = + '/' + + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PROFILE.ROUTE.replace( + ':username_fragment', + this.username + ); + [this.profilePicturePngDataUrl, this.profilePictureWebpDataUrl] = + this.userService.getProfileImageDataUrl(this.username); } } -angular.module('oppia').directive('profileLinkImage', downgradeComponent( - {component: ProfileLinkImageComponent})); +angular + .module('oppia') + .directive( + 'profileLinkImage', + downgradeComponent({component: ProfileLinkImageComponent}) + ); diff --git a/core/templates/components/profile-link-directives/profile-link-text.component.spec.ts b/core/templates/components/profile-link-directives/profile-link-text.component.spec.ts index 74b9b481ff7d..cc321f032e55 100644 --- a/core/templates/components/profile-link-directives/profile-link-text.component.spec.ts +++ b/core/templates/components/profile-link-directives/profile-link-text.component.spec.ts @@ -12,14 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { AppConstants } from 'app.constants'; +import {AppConstants} from 'app.constants'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { RouterModule } from '@angular/router'; -import { APP_BASE_HREF } from '@angular/common'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {RouterModule} from '@angular/router'; +import {APP_BASE_HREF} from '@angular/common'; -import { SmartRouterModule } from 'hybrid-router-module-provider'; -import { ProfileLinkTextComponent } from './profile-link-text.component'; +import {SmartRouterModule} from 'hybrid-router-module-provider'; +import {ProfileLinkTextComponent} from './profile-link-text.component'; /** * @fileoverview Unit tests for ProfileLinkTextComponent @@ -35,13 +35,15 @@ describe('ProfileLinkTextComponent', () => { // TODO(#13443): Remove hybrid router module provider once all pages are // migrated to angular router. SmartRouterModule, - RouterModule.forRoot([]) + RouterModule.forRoot([]), ], declarations: [ProfileLinkTextComponent], - providers: [{ - provide: APP_BASE_HREF, - useValue: '/' - }] + providers: [ + { + provide: APP_BASE_HREF, + useValue: '/', + }, + ], }).compileComponents(); })); @@ -58,7 +60,6 @@ describe('ProfileLinkTextComponent', () => { expect(component.isUsernameLinkable('linkableUsername')).toBe(true); }); - it('should set profile URL when the username is set', () => { expect(component.profileUrl).not.toBeDefined(); @@ -66,8 +67,10 @@ describe('ProfileLinkTextComponent', () => { expect(component.profileUrl).toEqual( '/' + - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PROFILE.ROUTE.replace( - ':username_fragment', 'dummy') + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PROFILE.ROUTE.replace( + ':username_fragment', + 'dummy' + ) ); }); diff --git a/core/templates/components/profile-link-directives/profile-link-text.component.ts b/core/templates/components/profile-link-directives/profile-link-text.component.ts index 858a05c9d76e..6b94556d0fc5 100644 --- a/core/templates/components/profile-link-directives/profile-link-text.component.ts +++ b/core/templates/components/profile-link-directives/profile-link-text.component.ts @@ -16,14 +16,14 @@ * @fileoverview Component for creating text links to a user's profile page. */ -import { Component, Input } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { AppConstants } from 'app.constants'; +import {Component, Input} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {AppConstants} from 'app.constants'; @Component({ selector: 'profile-link-text', templateUrl: './profile-link-text.component.html', - styleUrls: [] + styleUrls: [], }) export class ProfileLinkTextComponent { // This property is initialized using Angular lifecycle hooks @@ -34,11 +34,12 @@ export class ProfileLinkTextComponent { @Input() set username(username: string) { this._username = username; - this._profileUrl = ( - '/' + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PROFILE.ROUTE.replace( - ':username_fragment', username - ) - ); + this._profileUrl = + '/' + + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PROFILE.ROUTE.replace( + ':username_fragment', + username + ); } get username(): string { @@ -55,6 +56,9 @@ export class ProfileLinkTextComponent { } } -angular.module('oppia').directive( - 'profileLinkText', downgradeComponent( - {component: ProfileLinkTextComponent})); +angular + .module('oppia') + .directive( + 'profileLinkText', + downgradeComponent({component: ProfileLinkTextComponent}) + ); diff --git a/core/templates/components/question-difficulty-selector/question-difficulty-selector.component.spec.ts b/core/templates/components/question-difficulty-selector/question-difficulty-selector.component.spec.ts index c927ad8b9950..1f22f6352a51 100644 --- a/core/templates/components/question-difficulty-selector/question-difficulty-selector.component.spec.ts +++ b/core/templates/components/question-difficulty-selector/question-difficulty-selector.component.spec.ts @@ -12,11 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { MatRadioChange, _MatRadioButtonBase } from '@angular/material/radio'; -import { SkillDifficulty } from 'domain/skill/skill-difficulty.model'; -import { QuestionDifficultySelectorComponent } from './question-difficulty-selector.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {MatRadioChange, _MatRadioButtonBase} from '@angular/material/radio'; +import {SkillDifficulty} from 'domain/skill/skill-difficulty.model'; +import {QuestionDifficultySelectorComponent} from './question-difficulty-selector.component'; /** * @fileoverview Unit tests for QuestionDifficultySelectorComponent @@ -29,7 +29,7 @@ describe('QuestionDifficultySelectorComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [QuestionDifficultySelectorComponent], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -46,12 +46,12 @@ describe('QuestionDifficultySelectorComponent', () => { expect(component.availableDifficultyValues).toEqual([0.3, 0.6, 0.9]); }); - it('should update skill\'s difficulty', () => { + it("should update skill's difficulty", () => { component.skillWithDifficulty = new SkillDifficulty('id', '', 0.6); spyOn(component.skillWithDifficultyChange, 'emit'); let mockMatRadioChange: MatRadioChange = { source: {} as _MatRadioButtonBase, - value: 0.9 + value: 0.9, }; expect(component.skillWithDifficulty.getDifficulty()).toBe(0.6); diff --git a/core/templates/components/question-difficulty-selector/question-difficulty-selector.component.ts b/core/templates/components/question-difficulty-selector/question-difficulty-selector.component.ts index e1764e033f7c..90cae8a032ac 100644 --- a/core/templates/components/question-difficulty-selector/question-difficulty-selector.component.ts +++ b/core/templates/components/question-difficulty-selector/question-difficulty-selector.component.ts @@ -16,19 +16,19 @@ * @fileoverview Component for the question difficulty selector. */ -import { Component, Input, Output, EventEmitter } from '@angular/core'; -import { MatRadioChange } from '@angular/material/radio'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { AppConstants } from 'app.constants'; -import { Rubric } from 'domain/skill/rubric.model'; -import { SkillDifficulty } from 'domain/skill/skill-difficulty.model'; +import {Component, Input, Output, EventEmitter} from '@angular/core'; +import {MatRadioChange} from '@angular/material/radio'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {AppConstants} from 'app.constants'; +import {Rubric} from 'domain/skill/rubric.model'; +import {SkillDifficulty} from 'domain/skill/skill-difficulty.model'; -type SkillLabelToFloatKey = ( - keyof typeof AppConstants.SKILL_DIFFICULTY_LABEL_TO_FLOAT); +type SkillLabelToFloatKey = + keyof typeof AppConstants.SKILL_DIFFICULTY_LABEL_TO_FLOAT; @Component({ selector: 'oppia-question-difficulty-selector', - templateUrl: './question-difficulty-selector.component.html' + templateUrl: './question-difficulty-selector.component.html', }) export class QuestionDifficultySelectorComponent { // These properties are initialized using Angular lifecycle hooks @@ -36,8 +36,8 @@ export class QuestionDifficultySelectorComponent { // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @Input() skillIdToRubricsObject!: Record; @Input() skillWithDifficulty!: SkillDifficulty; - @Output() skillWithDifficultyChange: EventEmitter = ( - new EventEmitter()); + @Output() skillWithDifficultyChange: EventEmitter = + new EventEmitter(); availableDifficultyValues: number[] = []; @@ -45,7 +45,9 @@ export class QuestionDifficultySelectorComponent { for (let difficulty in AppConstants.SKILL_DIFFICULTY_LABEL_TO_FLOAT) { this.availableDifficultyValues.push( AppConstants.SKILL_DIFFICULTY_LABEL_TO_FLOAT[ - difficulty as SkillLabelToFloatKey]); + difficulty as SkillLabelToFloatKey + ] + ); } } @@ -55,7 +57,9 @@ export class QuestionDifficultySelectorComponent { } } -angular.module('oppia').directive('oppiaQuestionDifficultySelector', +angular.module('oppia').directive( + 'oppiaQuestionDifficultySelector', downgradeComponent({ - component: QuestionDifficultySelectorComponent - }) as angular.IDirectiveFactory); + component: QuestionDifficultySelectorComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/components/question-directives/modal-templates/confirm-question-exit-modal.component.spec.ts b/core/templates/components/question-directives/modal-templates/confirm-question-exit-modal.component.spec.ts index 22396bc97a00..dedf9f8d60b8 100644 --- a/core/templates/components/question-directives/modal-templates/confirm-question-exit-modal.component.spec.ts +++ b/core/templates/components/question-directives/modal-templates/confirm-question-exit-modal.component.spec.ts @@ -17,10 +17,10 @@ * modal component. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmQuestionExitModalComponent } from './confirm-question-exit-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmQuestionExitModalComponent} from './confirm-question-exit-modal.component'; describe('Confirm Question Exit Modal Component', () => { let component: ConfirmQuestionExitModalComponent; @@ -28,11 +28,9 @@ describe('Confirm Question Exit Modal Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - ConfirmQuestionExitModalComponent - ], + declarations: [ConfirmQuestionExitModalComponent], providers: [NgbActiveModal], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); diff --git a/core/templates/components/question-directives/modal-templates/confirm-question-exit-modal.component.ts b/core/templates/components/question-directives/modal-templates/confirm-question-exit-modal.component.ts index c1132df3931a..4f2199cfe2d4 100644 --- a/core/templates/components/question-directives/modal-templates/confirm-question-exit-modal.component.ts +++ b/core/templates/components/question-directives/modal-templates/confirm-question-exit-modal.component.ts @@ -16,18 +16,16 @@ * @fileoverview Component for confirm question exit modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-confirm-question-exit-modal', - templateUrl: './confirm-question-exit-modal.component.html' + templateUrl: './confirm-question-exit-modal.component.html', }) export class ConfirmQuestionExitModalComponent extends ConfirmOrCancelModal { - constructor( - private ngbActiveModal: NgbActiveModal - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } } diff --git a/core/templates/components/question-directives/modal-templates/question-editor-save-modal.component.spec.ts b/core/templates/components/question-directives/modal-templates/question-editor-save-modal.component.spec.ts index f44b3fb2ad9b..527519486e8c 100644 --- a/core/templates/components/question-directives/modal-templates/question-editor-save-modal.component.spec.ts +++ b/core/templates/components/question-directives/modal-templates/question-editor-save-modal.component.spec.ts @@ -17,22 +17,20 @@ * component. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { QuestionEditorSaveModalComponent } from './question-editor-save-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {QuestionEditorSaveModalComponent} from './question-editor-save-modal.component'; -describe('Question Editor Save Modal Component', function() { +describe('Question Editor Save Modal Component', function () { let component: QuestionEditorSaveModalComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - QuestionEditorSaveModalComponent - ], + declarations: [QuestionEditorSaveModalComponent], providers: [NgbActiveModal], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); diff --git a/core/templates/components/question-directives/modal-templates/question-editor-save-modal.component.ts b/core/templates/components/question-directives/modal-templates/question-editor-save-modal.component.ts index 790057292b91..d0e84965ad37 100644 --- a/core/templates/components/question-directives/modal-templates/question-editor-save-modal.component.ts +++ b/core/templates/components/question-directives/modal-templates/question-editor-save-modal.component.ts @@ -16,17 +16,19 @@ * @fileoverview Component for question editor save modal. */ -import { Component, OnInit } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { AppConstants } from 'app.constants'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component, OnInit} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {AppConstants} from 'app.constants'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-question-editor-save-modal', - templateUrl: './question-editor-save-modal.component.html' + templateUrl: './question-editor-save-modal.component.html', }) -export class QuestionEditorSaveModalComponent extends ConfirmOrCancelModal - implements OnInit { +export class QuestionEditorSaveModalComponent + extends ConfirmOrCancelModal + implements OnInit +{ // This property is initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -38,7 +40,6 @@ export class QuestionEditorSaveModalComponent extends ConfirmOrCancelModal } ngOnInit(): void { - this.MAX_COMMIT_MESSAGE_LENGTH = ( - AppConstants.MAX_COMMIT_MESSAGE_LENGTH); + this.MAX_COMMIT_MESSAGE_LENGTH = AppConstants.MAX_COMMIT_MESSAGE_LENGTH; } } diff --git a/core/templates/components/question-directives/modal-templates/remove-question-skill-link-modal.comonent.spec.ts b/core/templates/components/question-directives/modal-templates/remove-question-skill-link-modal.comonent.spec.ts index 5c6dcc7ea1b3..6b1c63b23da6 100644 --- a/core/templates/components/question-directives/modal-templates/remove-question-skill-link-modal.comonent.spec.ts +++ b/core/templates/components/question-directives/modal-templates/remove-question-skill-link-modal.comonent.spec.ts @@ -16,13 +16,22 @@ * @fileoverview Unit tests for Remove Question Modal. */ -import { ComponentFixture, fakeAsync, TestBed, waitForAsync, tick } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { AssignedSkillBackendDict, AssignedSkill } from 'domain/skill/assigned-skill.model'; -import { TopicsAndSkillsDashboardBackendApiService } from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; -import { RemoveQuestionSkillLinkModalComponent } from './remove-question-skill-link-modal.component'; -import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; -import { SkillBackendApiService } from 'domain/skill/skill-backend-api.service'; +import { + ComponentFixture, + fakeAsync, + TestBed, + waitForAsync, + tick, +} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import { + AssignedSkillBackendDict, + AssignedSkill, +} from 'domain/skill/assigned-skill.model'; +import {TopicsAndSkillsDashboardBackendApiService} from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; +import {RemoveQuestionSkillLinkModalComponent} from './remove-question-skill-link-modal.component'; +import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; +import {SkillBackendApiService} from 'domain/skill/skill-backend-api.service'; describe('Question deletion modal', () => { let fixture: ComponentFixture; @@ -33,17 +42,17 @@ describe('Question deletion modal', () => { topic_id: 'test_id_1', topic_name: 'Addition', topic_version: 1, - subtopic_id: 2 + subtopic_id: 2, }; let skillBackendDictForFractions: AssignedSkillBackendDict = { topic_id: 'test_id_2', topic_name: 'Fractions', topic_version: 1, - subtopic_id: 2 + subtopic_id: 2, }; const testSkills: AssignedSkill[] = [ AssignedSkill.createFromBackendDict(skillBackendDictForAddition), - AssignedSkill.createFromBackendDict(skillBackendDictForFractions) + AssignedSkill.createFromBackendDict(skillBackendDictForFractions), ]; const topicNames: string[] = ['Fractions']; class MockTopicsAndSkillsDashboardBackendApiService { @@ -51,7 +60,7 @@ describe('Question deletion modal', () => { return { then: (callback: (resp: AssignedSkill[]) => void) => { callback(testSkills); - } + }, }; } } @@ -61,30 +70,26 @@ describe('Question deletion modal', () => { return { then: (callback: (resp: string[]) => void) => { callback(topicNames); - } + }, }; } } beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - MatProgressSpinnerModule - ], - declarations: [ - RemoveQuestionSkillLinkModalComponent - ], + imports: [MatProgressSpinnerModule], + declarations: [RemoveQuestionSkillLinkModalComponent], providers: [ NgbActiveModal, { provide: TopicsAndSkillsDashboardBackendApiService, - useClass: MockTopicsAndSkillsDashboardBackendApiService + useClass: MockTopicsAndSkillsDashboardBackendApiService, }, { provide: SkillBackendApiService, - useClass: MockSkillBackendApiService - } - ] + useClass: MockSkillBackendApiService, + }, + ], }).compileComponents(); })); beforeEach(() => { @@ -112,22 +117,21 @@ describe('Question deletion modal', () => { it('should get topic editor url', () => { expect(componentInstance.getTopicEditorUrl('topicID')).toEqual( - '/topic_editor/topicID#/'); + '/topic_editor/topicID#/' + ); }); - it( - 'should not be able to remove questions when user have not enough rights', - fakeAsync(() => { - expect(componentInstance.questionRemovalIsAllowed).toBeTrue(); - componentInstance.canEditQuestion = false; - componentInstance.fetchTopicAssignmentsForSkill(); - tick(); - expect(componentInstance.questionRemovalIsAllowed).toBeFalse(); - })); + it('should not be able to remove questions when user have not enough rights', fakeAsync(() => { + expect(componentInstance.questionRemovalIsAllowed).toBeTrue(); + componentInstance.canEditQuestion = false; + componentInstance.fetchTopicAssignmentsForSkill(); + tick(); + expect(componentInstance.questionRemovalIsAllowed).toBeFalse(); + })); it( 'should not be able to remove questions when skill is assigned to ' + - 'the diagnostic test and question count is less than equal to 2', + 'the diagnostic test and question count is less than equal to 2', fakeAsync(() => { expect(componentInstance.questionRemovalIsAllowed).toBeTrue(); componentInstance.canEditQuestion = true; @@ -145,11 +149,12 @@ describe('Question deletion modal', () => { componentInstance.fetchTopicAssignmentsForSkill(); tick(); expect(componentInstance.questionRemovalIsAllowed).toBeFalse(); - })); + }) + ); it( 'should be able to remove questions when skill is assigned to ' + - 'the diagnostic test and question count is greater than 3', + 'the diagnostic test and question count is greater than 3', fakeAsync(() => { expect(componentInstance.questionRemovalIsAllowed).toBeTrue(); componentInstance.canEditQuestion = true; @@ -172,11 +177,12 @@ describe('Question deletion modal', () => { componentInstance.fetchTopicAssignmentsForSkill(); tick(); expect(componentInstance.questionRemovalIsAllowed).toBeTrue(); - })); + }) + ); it( 'should be able to remove questions when skill is not assigned to ' + - 'the diagnostic test', + 'the diagnostic test', fakeAsync(() => { componentInstance.canEditQuestion = true; componentInstance.numberOfQuestions = 2; @@ -204,5 +210,6 @@ describe('Question deletion modal', () => { tick(); expect(componentInstance.questionRemovalIsAllowed).toBeTrue(); - })); + }) + ); }); diff --git a/core/templates/components/question-directives/modal-templates/remove-question-skill-link-modal.component.ts b/core/templates/components/question-directives/modal-templates/remove-question-skill-link-modal.component.ts index 4b900c53ee3e..debad038bf0b 100644 --- a/core/templates/components/question-directives/modal-templates/remove-question-skill-link-modal.component.ts +++ b/core/templates/components/question-directives/modal-templates/remove-question-skill-link-modal.component.ts @@ -16,14 +16,14 @@ * @fileoverview Component for Remove Question Modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { AppConstants } from 'app.constants'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { AssignedSkill } from 'domain/skill/assigned-skill.model'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { TopicsAndSkillsDashboardBackendApiService } from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; -import { SkillBackendApiService } from 'domain/skill/skill-backend-api.service'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {AppConstants} from 'app.constants'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {AssignedSkill} from 'domain/skill/assigned-skill.model'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {TopicsAndSkillsDashboardBackendApiService} from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; +import {SkillBackendApiService} from 'domain/skill/skill-backend-api.service'; export interface TopicNameToTopicId { [key: string]: string; @@ -31,10 +31,9 @@ export interface TopicNameToTopicId { @Component({ selector: 'oppia-remove-question-skill-link-modal', - templateUrl: './remove-question-skill-link-modal.component.html' + templateUrl: './remove-question-skill-link-modal.component.html', }) -export class RemoveQuestionSkillLinkModalComponent - extends ConfirmOrCancelModal { +export class RemoveQuestionSkillLinkModalComponent extends ConfirmOrCancelModal { // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -48,8 +47,7 @@ export class RemoveQuestionSkillLinkModalComponent constructor( private ngbActiveModal: NgbActiveModal, - private topicsAndSkillsDashboardBackendApiService: - TopicsAndSkillsDashboardBackendApiService, + private topicsAndSkillsDashboardBackendApiService: TopicsAndSkillsDashboardBackendApiService, private skillBackendApiService: SkillBackendApiService, private urlInterpolationService: UrlInterpolationService ) { @@ -57,7 +55,9 @@ export class RemoveQuestionSkillLinkModalComponent } getTopicNameToTopicId( - topicAssignments: AssignedSkill[], topicNames: string[]): void { + topicAssignments: AssignedSkill[], + topicNames: string[] + ): void { this.topicNameToTopicId = {}; for (let topic of topicAssignments) { @@ -76,11 +76,13 @@ export class RemoveQuestionSkillLinkModalComponent return; } this.skillBackendApiService - .getTopicNamesWithGivenSkillAssignedForDiagnosticTest( - this.skillId).then((topicNames) => { - if ((topicNames.length > 0) && ( + .getTopicNamesWithGivenSkillAssignedForDiagnosticTest(this.skillId) + .then(topicNames => { + if ( + topicNames.length > 0 && this.numberOfQuestions <= - AppConstants.MIN_QUESTION_COUNT_FOR_A_DIAGNOSTIC_TEST_SKILL)) { + AppConstants.MIN_QUESTION_COUNT_FOR_A_DIAGNOSTIC_TEST_SKILL + ) { this.getTopicNameToTopicId(topicAssignments, topicNames); this.questionRemovalIsAllowed = false; } @@ -89,9 +91,8 @@ export class RemoveQuestionSkillLinkModalComponent fetchTopicAssignmentsForSkill(): void { this.topicsAndSkillsDashboardBackendApiService - .fetchTopicAssignmentsForSkillAsync( - this.skillId - ).then((response: AssignedSkill[]) => { + .fetchTopicAssignmentsForSkillAsync(this.skillId) + .then((response: AssignedSkill[]) => { this.isQuestionRemovalAllowed(response); this.topicsAssignmentsAreFetched = true; }); @@ -100,9 +101,11 @@ export class RemoveQuestionSkillLinkModalComponent getTopicEditorUrl(topicId: string): string { const TOPIC_EDITOR_URL_TEMPLATE = '/topic_editor/#/'; return this.urlInterpolationService.interpolateUrl( - TOPIC_EDITOR_URL_TEMPLATE, { - topic_id: topicId - }); + TOPIC_EDITOR_URL_TEMPLATE, + { + topic_id: topicId, + } + ); } ngOnInit(): void { diff --git a/core/templates/components/question-directives/question-editor/question-editor.component.spec.ts b/core/templates/components/question-directives/question-editor/question-editor.component.spec.ts index 4ac9447c557c..b92d84fdc5ad 100644 --- a/core/templates/components/question-directives/question-editor/question-editor.component.spec.ts +++ b/core/templates/components/question-directives/question-editor/question-editor.component.spec.ts @@ -16,19 +16,25 @@ * @fileoverview Unit tests for Question Editor Component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { StateInteractionIdService } from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; -import { Outcome } from 'domain/exploration/OutcomeObjectFactory'; -import { Solution } from 'domain/exploration/SolutionObjectFactory'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { QuestionUpdateService } from 'domain/question/question-update.service'; -import { QuestionObjectFactory } from 'domain/question/QuestionObjectFactory'; -import { EditabilityService } from 'services/editability.service'; -import { GenerateContentIdService } from 'services/generate-content-id.service'; -import { QuestionEditorComponent } from './question-editor.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {StateInteractionIdService} from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; +import {Outcome} from 'domain/exploration/OutcomeObjectFactory'; +import {Solution} from 'domain/exploration/SolutionObjectFactory'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {QuestionUpdateService} from 'domain/question/question-update.service'; +import {QuestionObjectFactory} from 'domain/question/QuestionObjectFactory'; +import {EditabilityService} from 'services/editability.service'; +import {GenerateContentIdService} from 'services/generate-content-id.service'; +import {QuestionEditorComponent} from './question-editor.component'; describe('Question Editor Component', () => { let component: QuestionEditorComponent; @@ -44,18 +50,16 @@ describe('Question Editor Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - QuestionEditorComponent - ], + declarations: [QuestionEditorComponent], providers: [ QuestionObjectFactory, EditabilityService, StateEditorService, StateInteractionIdService, QuestionUpdateService, - GenerateContentIdService + GenerateContentIdService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -75,66 +79,70 @@ describe('Question Editor Component', () => { question_state_data: { content: { html: 'Question 1', - content_id: 'content_1' + content_id: 'content_1', }, interaction: { - answer_groups: [{ - outcome: { - dest: 'outcome 1', - dest_if_really_stuck: null, - feedback: { - content_id: 'content_5', - html: '' + answer_groups: [ + { + outcome: { + dest: 'outcome 1', + dest_if_really_stuck: null, + feedback: { + content_id: 'content_5', + html: '', + }, + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, }, - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, }, - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null - }], + ], confirmed_unclassified_answers: [], customization_args: { placeholder: { value: { content_id: 'ca_placeholder_0', - unicode_str: '' - } + unicode_str: '', + }, }, - rows: { value: 1 }, + rows: {value: 1}, catchMisspellings: { - value: false - } + value: false, + }, }, default_outcome: { dest: 'dest', dest_if_really_stuck: null, feedback: { html: 'Correct Answer', - content_id: 'content_2' + content_id: 'content_2', }, param_changes: [], labelled_as_correct: true, missing_prerequisite_skill_id: null, - refresher_exploration_id: null + refresher_exploration_id: null, }, - hints: [{ - hint_content: { - html: 'Hint 1', - content_id: 'content_3' - } - }], + hints: [ + { + hint_content: { + html: 'Hint 1', + content_id: 'content_3', + }, + }, + ], solution: { correct_answer: 'This is the correct answer', answer_is_exclusive: false, explanation: { html: 'Solution explanation', - content_id: 'content_4' - } + content_id: 'content_4', + }, }, - id: 'TextInput' + id: 'TextInput', }, param_changes: [], recorded_voiceovers: { @@ -144,16 +152,16 @@ describe('Question Editor Component', () => { filename: 'filename1.mp3', file_size_bytes: 100000, needs_update: false, - duration_secs: 10.0 + duration_secs: 10.0, }, hi: { filename: 'filename2.mp3', file_size_bytes: 11000, needs_update: false, - duration_secs: 0.11 - } - } - } + duration_secs: 0.11, + }, + }, + }, }, classifier_model_id: null, solicit_answer_details: false, @@ -165,21 +173,21 @@ describe('Question Editor Component', () => { language_code: 'en', linked_skill_ids: [], question_state_data_schema_version: 44, - version: 45 + version: 45, }); component.question = question; component.questionStateData = question.getStateData(); - spyOn(questionUpdateService, 'setQuestionStateData') - .and.callFake((question, update) => { + spyOn(questionUpdateService, 'setQuestionStateData').and.callFake( + (question, update) => { update(); - }); + } + ); component.userCanEditQuestion = true; component.misconceptionsBySkill = {}; }); - afterEach(() => { component.ngOnDestroy(); }); @@ -191,8 +199,9 @@ describe('Question Editor Component', () => { component.ngOnInit(); - expect(component.oppiaBlackImgUrl) - .toBe('/assets/images/avatar/oppia_avatar_100px.svg'); + expect(component.oppiaBlackImgUrl).toBe( + '/assets/images/avatar/oppia_avatar_100px.svg' + ); expect(component.interactionIsShown).toBe(true); expect(component.stateEditorIsInitialized).toBe(true); }); @@ -206,96 +215,112 @@ describe('Question Editor Component', () => { expect(editabilityService.markEditable).toHaveBeenCalled(); }); - it('should mark editability service as false if question is not' + - ' editable', () => { - component.userCanEditQuestion = false; - spyOn(editabilityService, 'markNotEditable'); - - component.ngOnInit(); - - expect(editabilityService.markNotEditable).toHaveBeenCalled(); - }); - - it('should initialize component properties when state editor directive' + - ' is initialized', () => { - let onStateEditorDirectiveInitializedEmitter = new EventEmitter(); - spyOnProperty( - stateEditorService, 'onStateEditorDirectiveInitialized') - .and.returnValue(onStateEditorDirectiveInitializedEmitter); - - component.ngOnInit(); - - component.interactionIsShown = false; - component.stateEditorIsInitialized = false; - - onStateEditorDirectiveInitializedEmitter.emit(); - - expect(component.interactionIsShown).toBe(true); - expect(component.stateEditorIsInitialized).toBe(true); - }); - - it('should initialize component properties when interaction editor' + - ' is initialized', fakeAsync(() => { - let onInteractionEditorInitializedEmitter = new EventEmitter(); - spyOnProperty( - stateEditorService, 'onInteractionEditorInitialized') - .and.returnValue(onInteractionEditorInitializedEmitter); - - component.ngOnInit(); - - component.interactionIsShown = false; - component.stateEditorIsInitialized = false; - - onInteractionEditorInitializedEmitter.emit(); - tick(); - - expect(component.interactionIsShown).toBe(true); - expect(component.stateEditorIsInitialized).toBe(true); - })); - - it('should initialize component properties when interaction id' + - ' is changed', () => { - let onInteractionIdChangedEmitter = new EventEmitter(); - spyOnProperty( - stateInteractionIdService, 'onInteractionIdChanged') - .and.returnValue(onInteractionIdChangedEmitter); - - component.ngOnInit(); - - component.interactionIsShown = false; - component.stateEditorIsInitialized = false; - - onInteractionIdChangedEmitter.emit(); - - expect(component.interactionIsShown).toBe(true); - expect(component.stateEditorIsInitialized).toBe(true); - }); + it( + 'should mark editability service as false if question is not' + ' editable', + () => { + component.userCanEditQuestion = false; + spyOn(editabilityService, 'markNotEditable'); + + component.ngOnInit(); + + expect(editabilityService.markNotEditable).toHaveBeenCalled(); + } + ); + + it( + 'should initialize component properties when state editor directive' + + ' is initialized', + () => { + let onStateEditorDirectiveInitializedEmitter = new EventEmitter(); + spyOnProperty( + stateEditorService, + 'onStateEditorDirectiveInitialized' + ).and.returnValue(onStateEditorDirectiveInitializedEmitter); + + component.ngOnInit(); + + component.interactionIsShown = false; + component.stateEditorIsInitialized = false; + + onStateEditorDirectiveInitializedEmitter.emit(); + + expect(component.interactionIsShown).toBe(true); + expect(component.stateEditorIsInitialized).toBe(true); + } + ); + + it( + 'should initialize component properties when interaction editor' + + ' is initialized', + fakeAsync(() => { + let onInteractionEditorInitializedEmitter = new EventEmitter(); + spyOnProperty( + stateEditorService, + 'onInteractionEditorInitialized' + ).and.returnValue(onInteractionEditorInitializedEmitter); + + component.ngOnInit(); + + component.interactionIsShown = false; + component.stateEditorIsInitialized = false; + + onInteractionEditorInitializedEmitter.emit(); + tick(); + + expect(component.interactionIsShown).toBe(true); + expect(component.stateEditorIsInitialized).toBe(true); + }) + ); + + it( + 'should initialize component properties when interaction id' + + ' is changed', + () => { + let onInteractionIdChangedEmitter = new EventEmitter(); + spyOnProperty( + stateInteractionIdService, + 'onInteractionIdChanged' + ).and.returnValue(onInteractionIdChangedEmitter); + + component.ngOnInit(); + + component.interactionIsShown = false; + component.stateEditorIsInitialized = false; + + onInteractionIdChangedEmitter.emit(); + + expect(component.interactionIsShown).toBe(true); + expect(component.stateEditorIsInitialized).toBe(true); + } + ); it('should get state content save button placeholder', () => { - expect(component.getStateContentSaveButtonPlaceholder()) - .toBe('Save Question'); + expect(component.getStateContentSaveButtonPlaceholder()).toBe( + 'Save Question' + ); }); it('should get state content placeholder', () => { - expect(component.getStateContentPlaceholder()) - .toBe('Type your question here.'); + expect(component.getStateContentPlaceholder()).toBe( + 'Type your question here.' + ); }); it('should save state content when user clicks on save', () => { expect(component.interactionIsShown).toBe(undefined); - expect(component.questionStateData.content) - .toEqual(SubtitledHtml.createFromBackendDict({ + expect(component.questionStateData.content).toEqual( + SubtitledHtml.createFromBackendDict({ html: 'Question 1', - content_id: 'content_1' - })); - - component.saveStateContent( - new SubtitledHtml('New content', 'New content') + content_id: 'content_1', + }) ); + component.saveStateContent(new SubtitledHtml('New content', 'New content')); + expect(component.interactionIsShown).toBe(true); expect(component.questionStateData.content).toEqual( - new SubtitledHtml('New content', 'New content')); + new SubtitledHtml('New content', 'New content') + ); }); it('should save interaction data when interaction is saved', () => { @@ -304,14 +329,16 @@ describe('Question Editor Component', () => { let newInteractionData = { interactionId: 'Text Input', - customizationArgs: 'Customization Args' + customizationArgs: 'Customization Args', }; component.saveInteractionData(newInteractionData); - expect(stateEditorService.setInteractionId) - .toHaveBeenCalledWith(newInteractionData.interactionId); - expect(stateEditorService.setInteractionCustomizationArgs) - .toHaveBeenCalledWith(newInteractionData.customizationArgs); + expect(stateEditorService.setInteractionId).toHaveBeenCalledWith( + newInteractionData.interactionId + ); + expect( + stateEditorService.setInteractionCustomizationArgs + ).toHaveBeenCalledWith(newInteractionData.customizationArgs); }); it('should save interaction answer groups when interaction is saved', () => { @@ -324,15 +351,18 @@ describe('Question Editor Component', () => { ); }); - it('should save interaction default outcome when' + - ' interaction is saved', () => { - spyOn(stateEditorService, 'setInteractionDefaultOutcome'); + it( + 'should save interaction default outcome when' + ' interaction is saved', + () => { + spyOn(stateEditorService, 'setInteractionDefaultOutcome'); - component.saveInteractionDefaultOutcome({dest: 'New outcome'} as Outcome); + component.saveInteractionDefaultOutcome({dest: 'New outcome'} as Outcome); - expect(stateEditorService.setInteractionDefaultOutcome) - .toHaveBeenCalledWith({dest: 'New outcome'} as Outcome); - }); + expect( + stateEditorService.setInteractionDefaultOutcome + ).toHaveBeenCalledWith({dest: 'New outcome'} as Outcome); + } + ); it('should set interaction solution when interaction is saved', () => { spyOn(stateEditorService, 'setInteractionSolution'); @@ -345,8 +375,9 @@ describe('Question Editor Component', () => { let solution = new Solution(null, null, null, null); component.saveSolution(solution); - expect(stateEditorService.setInteractionSolution) - .toHaveBeenCalledWith(solution); + expect(stateEditorService.setInteractionSolution).toHaveBeenCalledWith( + solution + ); }); it('should save hints when interaction is saved', () => { @@ -354,19 +385,22 @@ describe('Question Editor Component', () => { component.saveHints([]); - expect(stateEditorService.setInteractionHints) - .toHaveBeenCalledWith([]); + expect(stateEditorService.setInteractionHints).toHaveBeenCalledWith([]); }); - it('should save inapplicable skill misconception ID when interaction' + - ' is saved', () => { - spyOn(stateEditorService, 'setInapplicableSkillMisconceptionIds'); + it( + 'should save inapplicable skill misconception ID when interaction' + + ' is saved', + () => { + spyOn(stateEditorService, 'setInapplicableSkillMisconceptionIds'); - component.saveInapplicableSkillMisconceptionIds(['InapplicableID']); + component.saveInapplicableSkillMisconceptionIds(['InapplicableID']); - expect(stateEditorService.setInapplicableSkillMisconceptionIds) - .toHaveBeenCalledWith(['InapplicableID']); - }); + expect( + stateEditorService.setInapplicableSkillMisconceptionIds + ).toHaveBeenCalledWith(['InapplicableID']); + } + ); it('should save next content ID index after generating new id', () => { component.ngOnInit(); diff --git a/core/templates/components/question-directives/question-editor/question-editor.component.ts b/core/templates/components/question-directives/question-editor/question-editor.component.ts index 84df1b0139e2..7f17fae276cf 100644 --- a/core/templates/components/question-directives/question-editor/question-editor.component.ts +++ b/core/templates/components/question-directives/question-editor/question-editor.component.ts @@ -16,31 +16,39 @@ * @fileoverview Component for the questions editor tab. */ -import { Component, ChangeDetectorRef, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import { + Component, + ChangeDetectorRef, + EventEmitter, + Input, + OnDestroy, + OnInit, + Output, +} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; import cloneDeep from 'lodash/cloneDeep'; -import { Subscription } from 'rxjs'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { StateInteractionIdService } from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; -import { MisconceptionSkillMap } from 'domain/skill/MisconceptionObjectFactory'; -import { Outcome } from 'domain/exploration/OutcomeObjectFactory'; -import { Question } from 'domain/question/QuestionObjectFactory'; -import { QuestionUpdateService } from 'domain/question/question-update.service'; -import { Solution } from 'domain/exploration/SolutionObjectFactory'; -import { Hint } from 'domain/exploration/hint-object.model'; -import { AnswerGroup } from 'domain/exploration/AnswerGroupObjectFactory'; -import { State } from 'domain/state/StateObjectFactory'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { SolutionValidityService } from 'pages/exploration-editor-page/editor-tab/services/solution-validity.service'; -import { EditabilityService } from 'services/editability.service'; -import { InteractionData } from 'interactions/customization-args-defs'; -import { LoaderService } from 'services/loader.service'; -import { GenerateContentIdService } from 'services/generate-content-id.service'; +import {Subscription} from 'rxjs'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {StateInteractionIdService} from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; +import {MisconceptionSkillMap} from 'domain/skill/MisconceptionObjectFactory'; +import {Outcome} from 'domain/exploration/OutcomeObjectFactory'; +import {Question} from 'domain/question/QuestionObjectFactory'; +import {QuestionUpdateService} from 'domain/question/question-update.service'; +import {Solution} from 'domain/exploration/SolutionObjectFactory'; +import {Hint} from 'domain/exploration/hint-object.model'; +import {AnswerGroup} from 'domain/exploration/AnswerGroupObjectFactory'; +import {State} from 'domain/state/StateObjectFactory'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {SolutionValidityService} from 'pages/exploration-editor-page/editor-tab/services/solution-validity.service'; +import {EditabilityService} from 'services/editability.service'; +import {InteractionData} from 'interactions/customization-args-defs'; +import {LoaderService} from 'services/loader.service'; +import {GenerateContentIdService} from 'services/generate-content-id.service'; @Component({ selector: 'oppia-question-editor', - templateUrl: './question-editor.component.html' + templateUrl: './question-editor.component.html', }) export class QuestionEditorComponent implements OnInit, OnDestroy { @Output() questionChange = new EventEmitter(); @@ -69,42 +77,47 @@ export class QuestionEditorComponent implements OnInit, OnDestroy { private solutionValidityService: SolutionValidityService, private stateEditorService: StateEditorService, private stateInteractionIdService: StateInteractionIdService, - private urlInterpolationService: UrlInterpolationService, - ) { } + private urlInterpolationService: UrlInterpolationService + ) {} saveInteractionAnswerGroups(newAnswerGroups: AnswerGroup[]): void { this._updateQuestion(() => { this.stateEditorService.setInteractionAnswerGroups( - cloneDeep(newAnswerGroups)); + cloneDeep(newAnswerGroups) + ); }); } saveInteractionDefaultOutcome(newOutcome: Outcome): void { this._updateQuestion(() => { this.stateEditorService.setInteractionDefaultOutcome( - cloneDeep(newOutcome)); + cloneDeep(newOutcome) + ); }); } saveInteractionData(displayedValue: InteractionData): void { this._updateQuestion(() => { this.stateEditorService.setInteractionId( - cloneDeep(displayedValue.interactionId)); + cloneDeep(displayedValue.interactionId) + ); this.stateEditorService.setInteractionCustomizationArgs( - cloneDeep(displayedValue.customizationArgs)); + cloneDeep(displayedValue.customizationArgs) + ); }); } saveNextContentIdIndex(): void { this.questionUpdateService.setQuestionNextContentIdIndex( - this.question, this.nextContentIdIndexDisplayedValue); + this.question, + this.nextContentIdIndexDisplayedValue + ); this.nextContentIdIndexMemento = this.nextContentIdIndexDisplayedValue; } saveSolution(displayedValue: Solution): void { this._updateQuestion(() => { - this.stateEditorService.setInteractionSolution( - cloneDeep(displayedValue)); + this.stateEditorService.setInteractionSolution(cloneDeep(displayedValue)); }); this.changeDetectionRef.detectChanges(); @@ -112,17 +125,18 @@ export class QuestionEditorComponent implements OnInit, OnDestroy { saveHints(displayedValue: Hint[]): void { this._updateQuestion(() => { - this.stateEditorService.setInteractionHints( - cloneDeep(displayedValue)); + this.stateEditorService.setInteractionHints(cloneDeep(displayedValue)); }); } - saveInapplicableSkillMisconceptionIds( - displayedValue: string[]): void { + saveInapplicableSkillMisconceptionIds(displayedValue: string[]): void { this.stateEditorService.setInapplicableSkillMisconceptionIds( - cloneDeep(displayedValue)); + cloneDeep(displayedValue) + ); this.questionUpdateService.setQuestionInapplicableSkillMisconceptionIds( - this.question, displayedValue); + this.question, + displayedValue + ); } getStateContentPlaceholder(): string { @@ -136,7 +150,9 @@ export class QuestionEditorComponent implements OnInit, OnDestroy { _updateQuestion(updateFunction: Function): void { this.questionChange.emit(); this.questionUpdateService.setQuestionStateData( - this.question, updateFunction); + this.question, + updateFunction + ); } saveStateContent(displayedValue: SubtitledHtml): void { @@ -154,18 +170,21 @@ export class QuestionEditorComponent implements OnInit, OnDestroy { this.stateEditorService.setInQuestionMode(true); if (this.question) { this.stateEditorService.setInapplicableSkillMisconceptionIds( - this.question.getInapplicableSkillMisconceptionIds()); + this.question.getInapplicableSkillMisconceptionIds() + ); } this.solutionValidityService.init(['question']); - this.generateContentIdService.init(() => { - let indexToUse = this.nextContentIdIndexDisplayedValue; - this.nextContentIdIndexDisplayedValue += 1; - return indexToUse; - }, () => { - this.nextContentIdIndexDisplayedValue = ( - this.nextContentIdIndexMemento); - }); + this.generateContentIdService.init( + () => { + let indexToUse = this.nextContentIdIndexDisplayedValue; + this.nextContentIdIndexDisplayedValue += 1; + return indexToUse; + }, + () => { + this.nextContentIdIndexDisplayedValue = this.nextContentIdIndexMemento; + } + ); const stateData = this.questionStateData; const outcome = stateData.interaction.defaultOutcome; @@ -186,18 +205,18 @@ export class QuestionEditorComponent implements OnInit, OnDestroy { ngOnInit(): void { this.componentSubscriptions.add( - this.stateEditorService.onStateEditorDirectiveInitialized.subscribe( - () => this._init() + this.stateEditorService.onStateEditorDirectiveInitialized.subscribe(() => + this._init() ) ); this.componentSubscriptions.add( - this.stateEditorService.onInteractionEditorInitialized.subscribe( - () => this._init() + this.stateEditorService.onInteractionEditorInitialized.subscribe(() => + this._init() ) ); this.componentSubscriptions.add( - this.stateInteractionIdService.onInteractionIdChanged.subscribe( - () => this._init() + this.stateInteractionIdService.onInteractionIdChanged.subscribe(() => + this._init() ) ); @@ -208,16 +227,18 @@ export class QuestionEditorComponent implements OnInit, OnDestroy { } this.stateEditorService.setActiveStateName('question'); this.stateEditorService.setMisconceptionsBySkill( - this.misconceptionsBySkill); + this.misconceptionsBySkill + ); this.oppiaBlackImgUrl = this.urlInterpolationService.getStaticImageUrl( - '/avatar/oppia_avatar_100px.svg'); + '/avatar/oppia_avatar_100px.svg' + ); this.interactionIsShown = false; this.stateEditorIsInitialized = false; this.nextContentIdIndexMemento = this.question.getNextContentIdIndex(); - this.nextContentIdIndexDisplayedValue = ( - this.question.getNextContentIdIndex()); + this.nextContentIdIndexDisplayedValue = + this.question.getNextContentIdIndex(); this._init(); } @@ -227,7 +248,9 @@ export class QuestionEditorComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive('oppiaQuestionEditor', +angular.module('oppia').directive( + 'oppiaQuestionEditor', downgradeComponent({ - component: QuestionEditorComponent - }) as angular.IDirectiveFactory); + component: QuestionEditorComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/components/question-directives/question-misconception-editor/question-misconception-editor.component.spec.ts b/core/templates/components/question-directives/question-misconception-editor/question-misconception-editor.component.spec.ts index afedde9cea70..11b2bd3d7fba 100644 --- a/core/templates/components/question-directives/question-misconception-editor/question-misconception-editor.component.spec.ts +++ b/core/templates/components/question-directives/question-misconception-editor/question-misconception-editor.component.spec.ts @@ -16,18 +16,30 @@ * @fileoverview Unit tests for the question misconception editor component. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { Outcome, QuestionMisconceptionEditorComponent } from './question-misconception-editor.component'; -import { ExternalSaveService } from 'services/external-save.service'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { MisconceptionSkillMap, MisconceptionObjectFactory } from 'domain/skill/MisconceptionObjectFactory'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import { + Outcome, + QuestionMisconceptionEditorComponent, +} from './question-misconception-editor.component'; +import {ExternalSaveService} from 'services/external-save.service'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import { + MisconceptionSkillMap, + MisconceptionObjectFactory, +} from 'domain/skill/MisconceptionObjectFactory'; class MockNgbModalRef { componentInstance = { - taggedSkillMisconceptionId: null + taggedSkillMisconceptionId: null, }; } @@ -42,21 +54,16 @@ describe('Question Misconception Editor Component', () => { let outcome = { feedback: { content_id: null, - html: '' - } + html: '', + }, } as Outcome; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - QuestionMisconceptionEditorComponent - ], - providers: [ - StateEditorService, - ExternalSaveService - ], - schemas: [NO_ERRORS_SCHEMA] + declarations: [QuestionMisconceptionEditorComponent], + providers: [StateEditorService, ExternalSaveService], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -72,10 +79,20 @@ describe('Question Misconception Editor Component', () => { mockMisconceptionObject = { abc: [ misconceptionObjectFactory.create( - 1, 'misc1', 'notes1', 'feedback1', true), + 1, + 'misc1', + 'notes1', + 'feedback1', + true + ), misconceptionObjectFactory.create( - 2, 'misc2', 'notes2', 'feedback1', true) - ] + 2, + 'misc2', + 'notes2', + 'feedback1', + true + ), + ], }; spyOn(stateEditorService, 'getMisconceptionsBySkill').and.callFake(() => { return mockMisconceptionObject; @@ -85,22 +102,22 @@ describe('Question Misconception Editor Component', () => { fixture.detectChanges(); }); - it( - 'should initialize correctly when tagged misconception is provided', - () => { - expect(component.misconceptionEditorIsOpen).toBeFalse(); - expect(component.misconceptionName).toEqual('misc1'); - expect(component.selectedMisconception).toEqual( - mockMisconceptionObject.abc[0]); - expect(component.selectedMisconceptionSkillId).toEqual('abc'); - expect(component.feedbackIsUsed).toBeTrue(); - }); + it('should initialize correctly when tagged misconception is provided', () => { + expect(component.misconceptionEditorIsOpen).toBeFalse(); + expect(component.misconceptionName).toEqual('misc1'); + expect(component.selectedMisconception).toEqual( + mockMisconceptionObject.abc[0] + ); + expect(component.selectedMisconceptionSkillId).toEqual('abc'); + expect(component.feedbackIsUsed).toBeTrue(); + }); it('should throw an error if tagged misconception id is invalid', () => { component.taggedSkillMisconceptionId = 'invalidId'; expect(() => component.ngOnInit()).toThrowError( - 'Expected skillMisconceptionId to be -.'); + 'Expected skillMisconceptionId to be -.' + ); }); it('should use feedback by default', () => { @@ -128,18 +145,19 @@ describe('Question Misconception Editor Component', () => { let mockResultObject = { misconception: mockMisconceptionObject.abc[1], misconceptionSkillId: 'abc', - feedbackIsUsed: false + feedbackIsUsed: false, }; spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ( - { componentInstance: MockNgbModalRef, - result: Promise.resolve(mockResultObject) - }) as NgbModalRef; + return { + componentInstance: MockNgbModalRef, + result: Promise.resolve(mockResultObject), + } as NgbModalRef; }); expect(component.misconceptionName).toEqual('misc1'); expect(component.selectedMisconception).toEqual( - mockMisconceptionObject.abc[0]); + mockMisconceptionObject.abc[0] + ); expect(component.selectedMisconceptionSkillId).toEqual('abc'); expect(component.feedbackIsUsed).toBeTrue(); @@ -148,7 +166,8 @@ describe('Question Misconception Editor Component', () => { expect(component.misconceptionName).toEqual('misc2'); expect(component.selectedMisconception).toEqual( - mockMisconceptionObject.abc[1]); + mockMisconceptionObject.abc[1] + ); expect(component.selectedMisconceptionSkillId).toEqual('abc'); expect(component.feedbackIsUsed).toBeFalse(); expect(component.misconceptionEditorIsOpen).toBeFalse(); @@ -156,15 +175,16 @@ describe('Question Misconception Editor Component', () => { it('should not tag a misconception if the modal was dismissed', () => { spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ( - { componentInstance: MockNgbModalRef, - result: Promise.reject() - }) as NgbModalRef; + return { + componentInstance: MockNgbModalRef, + result: Promise.reject(), + } as NgbModalRef; }); expect(component.misconceptionName).toEqual('misc1'); expect(component.selectedMisconception).toEqual( - mockMisconceptionObject.abc[0]); + mockMisconceptionObject.abc[0] + ); expect(component.selectedMisconceptionSkillId).toEqual('abc'); expect(component.feedbackIsUsed).toBeTrue(); @@ -172,7 +192,8 @@ describe('Question Misconception Editor Component', () => { expect(component.misconceptionName).toEqual('misc1'); expect(component.selectedMisconception).toEqual( - mockMisconceptionObject.abc[0]); + mockMisconceptionObject.abc[0] + ); expect(component.selectedMisconceptionSkillId).toEqual('abc'); expect(component.feedbackIsUsed).toBeTrue(); }); @@ -201,14 +222,16 @@ describe('Question Misconception Editor Component', () => { }; expect(component.selectedMisconception).toEqual( - mockMisconceptionObject.abc[0]); + mockMisconceptionObject.abc[0] + ); expect(component.selectedMisconceptionSkillId).toEqual('abc'); expect(component.feedbackIsUsed).toBeTrue(); component.updateValues(updatedValues); expect(component.selectedMisconception).toEqual( - mockMisconceptionObject.abc[1]); + mockMisconceptionObject.abc[1] + ); expect(component.selectedMisconceptionSkillId).toEqual('id'); expect(component.feedbackIsUsed).toBeFalse(); }); diff --git a/core/templates/components/question-directives/question-misconception-editor/question-misconception-editor.component.ts b/core/templates/components/question-directives/question-misconception-editor/question-misconception-editor.component.ts index eacbb7997192..195bf5138ccb 100644 --- a/core/templates/components/question-directives/question-misconception-editor/question-misconception-editor.component.ts +++ b/core/templates/components/question-directives/question-misconception-editor/question-misconception-editor.component.ts @@ -16,16 +16,20 @@ * @fileoverview Component for the question misconception editor. */ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; import cloneDeep from 'lodash/cloneDeep'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { Misconception, MisconceptionSkillMap, TaggedMisconception } from 'domain/skill/MisconceptionObjectFactory'; -import { ExternalSaveService } from 'services/external-save.service'; -import { TagMisconceptionModalComponent } from './tag-misconception-modal-component'; -import { SubtitledHtmlBackendDict } from 'domain/exploration/subtitled-html.model'; -import { Rule } from 'domain/exploration/rule.model'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import { + Misconception, + MisconceptionSkillMap, + TaggedMisconception, +} from 'domain/skill/MisconceptionObjectFactory'; +import {ExternalSaveService} from 'services/external-save.service'; +import {TagMisconceptionModalComponent} from './tag-misconception-modal-component'; +import {SubtitledHtmlBackendDict} from 'domain/exploration/subtitled-html.model'; +import {Rule} from 'domain/exploration/rule.model'; export interface MisconceptionUpdatedValues { misconception: Misconception; @@ -40,14 +44,13 @@ export interface Outcome { @Component({ selector: 'oppia-question-misconception-editor', - templateUrl: './question-misconception-editor.component.html' + templateUrl: './question-misconception-editor.component.html', }) export class QuestionMisconceptionEditorComponent implements OnInit { - @Output() saveAnswerGroupFeedback: - EventEmitter = (new EventEmitter()); + @Output() saveAnswerGroupFeedback: EventEmitter = new EventEmitter(); - @Output() saveTaggedMisconception: - EventEmitter = (new EventEmitter()); + @Output() saveTaggedMisconception: EventEmitter = + new EventEmitter(); // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see @@ -70,20 +73,21 @@ export class QuestionMisconceptionEditorComponent implements OnInit { ) {} ngOnInit(): void { - this.misconceptionsBySkill = ( - this.stateEditorService.getMisconceptionsBySkill()); + this.misconceptionsBySkill = + this.stateEditorService.getMisconceptionsBySkill(); this.misconceptionEditorIsOpen = false; let skillMisconceptionId = this.taggedSkillMisconceptionId; if (skillMisconceptionId) { - if (typeof skillMisconceptionId === 'string' && - skillMisconceptionId.split('-').length === 2) { + if ( + typeof skillMisconceptionId === 'string' && + skillMisconceptionId.split('-').length === 2 + ) { let skillId = skillMisconceptionId.split('-')[0]; let misconceptionId = skillMisconceptionId.split('-')[1]; let misconceptions = this.misconceptionsBySkill[skillId]; for (let i = 0; i < misconceptions.length; i++) { - if (misconceptions[i].getId().toString() === - misconceptionId) { + if (misconceptions[i].getId().toString() === misconceptionId) { this.misconceptionName = misconceptions[i].getName(); this.selectedMisconception = misconceptions[i]; this.selectedMisconceptionSkillId = skillId; @@ -92,7 +96,8 @@ export class QuestionMisconceptionEditorComponent implements OnInit { } else { throw new Error( 'Expected skillMisconceptionId to be ' + - '-.'); + '-.' + ); } } this.feedbackIsUsed = true; @@ -100,7 +105,7 @@ export class QuestionMisconceptionEditorComponent implements OnInit { containsMisconceptions(): boolean { let containsMisconceptions = false; - Object.keys(this.misconceptionsBySkill).forEach((skillId) => { + Object.keys(this.misconceptionsBySkill).forEach(skillId => { if (this.misconceptionsBySkill[skillId].length > 0) { containsMisconceptions = true; } @@ -109,46 +114,47 @@ export class QuestionMisconceptionEditorComponent implements OnInit { } updateValues(newValues: MisconceptionUpdatedValues): void { - this.selectedMisconception = ( - newValues.misconception); - this.selectedMisconceptionSkillId = ( - newValues.skillId); - this.feedbackIsUsed = ( - newValues.feedbackIsUsed); + this.selectedMisconception = newValues.misconception; + this.selectedMisconceptionSkillId = newValues.skillId; + this.feedbackIsUsed = newValues.feedbackIsUsed; } tagAnswerGroupWithMisconception(): void { const modalRef: NgbModalRef = this.ngbModal.open( - TagMisconceptionModalComponent, { + TagMisconceptionModalComponent, + { backdrop: 'static', backdropClass: 'forced-modal-backdrop-stack-over', - windowClass: 'forced-modal-stack-over' - }); - modalRef.componentInstance.taggedSkillMisconceptionId = ( - this.taggedSkillMisconceptionId); - modalRef.result.then((returnObject) => { - this.selectedMisconception = returnObject.misconception; - this.selectedMisconceptionSkillId = returnObject.misconceptionSkillId; - this.feedbackIsUsed = returnObject.feedbackIsUsed; - this.updateMisconception(); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + windowClass: 'forced-modal-stack-over', + } + ); + modalRef.componentInstance.taggedSkillMisconceptionId = + this.taggedSkillMisconceptionId; + modalRef.result.then( + returnObject => { + this.selectedMisconception = returnObject.misconception; + this.selectedMisconceptionSkillId = returnObject.misconceptionSkillId; + this.feedbackIsUsed = returnObject.feedbackIsUsed; + this.updateMisconception(); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } updateMisconception(): void { let taggedMisconception = { skillId: this.selectedMisconceptionSkillId, - misconceptionId: this.selectedMisconception.getId() + misconceptionId: this.selectedMisconception.getId(), }; this.saveTaggedMisconception.emit(taggedMisconception); this.misconceptionName = this.selectedMisconception.getName(); let outcome = cloneDeep(this.outcome); if (this.feedbackIsUsed) { - outcome.feedback.html = ( - this.selectedMisconception.getFeedback()); + outcome.feedback.html = this.selectedMisconception.getFeedback(); this.saveAnswerGroupFeedback.emit(outcome); this.externalSaveService.onExternalSave.emit(); } @@ -160,5 +166,9 @@ export class QuestionMisconceptionEditorComponent implements OnInit { } } -angular.module('oppia').directive('oppiaQuestionMisconceptionEditor', - downgradeComponent({component: QuestionMisconceptionEditorComponent})); +angular + .module('oppia') + .directive( + 'oppiaQuestionMisconceptionEditor', + downgradeComponent({component: QuestionMisconceptionEditorComponent}) + ); diff --git a/core/templates/components/question-directives/question-misconception-editor/tag-misconception-modal-component.spec.ts b/core/templates/components/question-directives/question-misconception-editor/tag-misconception-modal-component.spec.ts index 4f45ecf1b091..cdb8f3ab7d46 100644 --- a/core/templates/components/question-directives/question-misconception-editor/tag-misconception-modal-component.spec.ts +++ b/core/templates/components/question-directives/question-misconception-editor/tag-misconception-modal-component.spec.ts @@ -16,13 +16,16 @@ * @fileoverview Unit tests for tag misconception modal component. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { MisconceptionObjectFactory, MisconceptionSkillMap } from 'domain/skill/MisconceptionObjectFactory'; -import { TagMisconceptionModalComponent } from './tag-misconception-modal-component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import { + MisconceptionObjectFactory, + MisconceptionSkillMap, +} from 'domain/skill/MisconceptionObjectFactory'; +import {TagMisconceptionModalComponent} from './tag-misconception-modal-component'; class MockActiveModal { close(): void { @@ -46,17 +49,15 @@ describe('Tag Misconception Modal Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - TagMisconceptionModalComponent - ], + declarations: [TagMisconceptionModalComponent], providers: [ StateEditorService, { provide: NgbActiveModal, - useClass: MockActiveModal + useClass: MockActiveModal, }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -70,12 +71,22 @@ describe('Tag Misconception Modal Component', () => { mockMisconceptionObject = { abc: [ misconceptionObjectFactory.create( - 1, 'misc1', 'notes1', 'feedback1', true) + 1, + 'misc1', + 'notes1', + 'feedback1', + true + ), ], def: [ misconceptionObjectFactory.create( - 2, 'misc2', 'notes2', 'feedback1', true) - ] + 2, + 'misc2', + 'notes2', + 'feedback1', + true + ), + ], }; spyOn(stateEditorService, 'getMisconceptionsBySkill').and.callFake(() => { return mockMisconceptionObject; @@ -105,7 +116,8 @@ describe('Tag Misconception Modal Component', () => { component.updateValues(updatedValues); expect(component.tempSelectedMisconception).toEqual( - mockMisconceptionObject.def[0]); + mockMisconceptionObject.def[0] + ); expect(component.tempSelectedMisconceptionSkillId).toEqual('id'); expect(component.tempMisconceptionFeedbackIsUsed).toBeFalse(); }); @@ -120,7 +132,7 @@ describe('Tag Misconception Modal Component', () => { expect(ngbActiveModal.close).toHaveBeenCalledWith({ misconception: mockMisconceptionObject.abc[0], misconceptionSkillId: 'abc', - feedbackIsUsed: true + feedbackIsUsed: true, }); }); }); diff --git a/core/templates/components/question-directives/question-misconception-editor/tag-misconception-modal-component.ts b/core/templates/components/question-directives/question-misconception-editor/tag-misconception-modal-component.ts index a18b0e4a5cc8..4f6afa76256d 100644 --- a/core/templates/components/question-directives/question-misconception-editor/tag-misconception-modal-component.ts +++ b/core/templates/components/question-directives/question-misconception-editor/tag-misconception-modal-component.ts @@ -16,19 +16,24 @@ * @fileoverview Component for tag misconception modal. */ -import { Component, Input, OnInit } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { Misconception, MisconceptionSkillMap } from 'domain/skill/MisconceptionObjectFactory'; -import { MisconceptionUpdatedValues } from './question-misconception-editor.component'; +import {Component, Input, OnInit} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import { + Misconception, + MisconceptionSkillMap, +} from 'domain/skill/MisconceptionObjectFactory'; +import {MisconceptionUpdatedValues} from './question-misconception-editor.component'; @Component({ selector: 'oppia-tag-misconception-modal', - templateUrl: './tag-misconception-modal.component.html' + templateUrl: './tag-misconception-modal.component.html', }) export class TagMisconceptionModalComponent - extends ConfirmOrCancelModal implements OnInit { + extends ConfirmOrCancelModal + implements OnInit +{ // These properties below are initialized using Angular lifecycle hooks // where we need to do non-null assertion. For more information see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -49,27 +54,24 @@ export class TagMisconceptionModalComponent } ngOnInit(): void { - this.misconceptionsBySkill = ( - this.stateEditorService.getMisconceptionsBySkill()); + this.misconceptionsBySkill = + this.stateEditorService.getMisconceptionsBySkill(); this.tempSelectedMisconception = null; this.tempSelectedMisconceptionSkillId = null; this.tempMisconceptionFeedbackIsUsed = true; } updateValues(newValues: MisconceptionUpdatedValues): void { - this.tempSelectedMisconception = ( - newValues.misconception); - this.tempSelectedMisconceptionSkillId = ( - newValues.skillId); - this.tempMisconceptionFeedbackIsUsed = ( - newValues.feedbackIsUsed); + this.tempSelectedMisconception = newValues.misconception; + this.tempSelectedMisconceptionSkillId = newValues.skillId; + this.tempMisconceptionFeedbackIsUsed = newValues.feedbackIsUsed; } done(): void { this.ngbActiveModal.close({ misconception: this.tempSelectedMisconception, misconceptionSkillId: this.tempSelectedMisconceptionSkillId, - feedbackIsUsed: this.tempMisconceptionFeedbackIsUsed + feedbackIsUsed: this.tempMisconceptionFeedbackIsUsed, }); } } diff --git a/core/templates/components/question-directives/question-misconception-selector/question-misconception-selector.component.spec.ts b/core/templates/components/question-directives/question-misconception-selector/question-misconception-selector.component.spec.ts index 1a6c48505c35..657a690f886f 100644 --- a/core/templates/components/question-directives/question-misconception-selector/question-misconception-selector.component.spec.ts +++ b/core/templates/components/question-directives/question-misconception-selector/question-misconception-selector.component.spec.ts @@ -16,12 +16,15 @@ * @fileoverview Unit tests for the question misconception selector component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { MisconceptionSkillMap, MisconceptionObjectFactory } from 'domain/skill/MisconceptionObjectFactory'; -import { QuestionMisconceptionSelectorComponent } from './question-misconception-selector.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import { + MisconceptionSkillMap, + MisconceptionObjectFactory, +} from 'domain/skill/MisconceptionObjectFactory'; +import {QuestionMisconceptionSelectorComponent} from './question-misconception-selector.component'; describe('Question Misconception Selector Component', () => { let component: QuestionMisconceptionSelectorComponent; @@ -34,13 +37,9 @@ describe('Question Misconception Selector Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - QuestionMisconceptionSelectorComponent - ], - providers: [ - StateEditorService - ], - schemas: [NO_ERRORS_SCHEMA] + declarations: [QuestionMisconceptionSelectorComponent], + providers: [StateEditorService], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -56,12 +55,22 @@ describe('Question Misconception Selector Component', () => { mockMisconceptionObject = { abc: [ misconceptionObjectFactory.create( - 1, 'misc1', 'notes1', 'feedback1', true) + 1, + 'misc1', + 'notes1', + 'feedback1', + true + ), ], def: [ misconceptionObjectFactory.create( - 2, 'misc2', 'notes2', 'feedback1', true) - ] + 2, + 'misc2', + 'notes2', + 'feedback1', + true + ), + ], }; spyOn(stateEditorService, 'getMisconceptionsBySkill').and.callFake(() => { return mockMisconceptionObject; @@ -87,13 +96,15 @@ describe('Question Misconception Selector Component', () => { it('should set selected misconception correctly', () => { expect(component.selectedMisconception).toEqual( - mockMisconceptionObject.abc[0]); + mockMisconceptionObject.abc[0] + ); expect(component.selectedMisconceptionSkillId).toEqual('abc'); component.selectMisconception(mockMisconceptionObject.def[0], 'def'); expect(component.selectedMisconception).toEqual( - mockMisconceptionObject.def[0]); + mockMisconceptionObject.def[0] + ); expect(component.selectedMisconceptionSkillId).toEqual('def'); }); }); diff --git a/core/templates/components/question-directives/question-misconception-selector/question-misconception-selector.component.ts b/core/templates/components/question-directives/question-misconception-selector/question-misconception-selector.component.ts index 9eb06c0de224..943cac39862f 100644 --- a/core/templates/components/question-directives/question-misconception-selector/question-misconception-selector.component.ts +++ b/core/templates/components/question-directives/question-misconception-selector/question-misconception-selector.component.ts @@ -16,10 +16,13 @@ * @fileoverview Component for the question misconception selector. */ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; import cloneDeep from 'lodash/cloneDeep'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { Misconception, MisconceptionSkillMap } from 'domain/skill/MisconceptionObjectFactory'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import { + Misconception, + MisconceptionSkillMap, +} from 'domain/skill/MisconceptionObjectFactory'; interface UpdatedValues { misconception: Misconception; @@ -29,11 +32,11 @@ interface UpdatedValues { @Component({ selector: 'oppia-question-misconception-selector', - templateUrl: './question-misconception-selector.component.html' + templateUrl: './question-misconception-selector.component.html', }) export class QuestionMisconceptionSelectorComponent implements OnInit { - @Output() updateMisconceptionValues: - EventEmitter = (new EventEmitter()); + @Output() updateMisconceptionValues: EventEmitter = + new EventEmitter(); // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see @@ -44,14 +47,12 @@ export class QuestionMisconceptionSelectorComponent implements OnInit { @Input() taggedSkillMisconceptionId!: string; misconceptionsBySkill!: MisconceptionSkillMap; - constructor( - private stateEditorService: StateEditorService, - ) {} + constructor(private stateEditorService: StateEditorService) {} ngOnInit(): void { this.misconceptionFeedbackIsUsed = true; - this.misconceptionsBySkill = ( - this.stateEditorService.getMisconceptionsBySkill()); + this.misconceptionsBySkill = + this.stateEditorService.getMisconceptionsBySkill(); } selectMisconception(misconception: Misconception, skillId: string): void { @@ -60,13 +61,12 @@ export class QuestionMisconceptionSelectorComponent implements OnInit { let updatedValues = { misconception: this.selectedMisconception, skillId: this.selectedMisconceptionSkillId, - feedbackIsUsed: this.misconceptionFeedbackIsUsed + feedbackIsUsed: this.misconceptionFeedbackIsUsed, }; this.updateMisconceptionValues.emit(updatedValues); } toggleMisconceptionFeedbackUsage(): void { - this.misconceptionFeedbackIsUsed = ( - !this.misconceptionFeedbackIsUsed); + this.misconceptionFeedbackIsUsed = !this.misconceptionFeedbackIsUsed; } } diff --git a/core/templates/components/question-directives/question-player/question-player-concept-card-modal.component.spec.ts b/core/templates/components/question-directives/question-player/question-player-concept-card-modal.component.spec.ts index d170e9b1c5ed..3ea056d1ba13 100644 --- a/core/templates/components/question-directives/question-player/question-player-concept-card-modal.component.spec.ts +++ b/core/templates/components/question-directives/question-player/question-player-concept-card-modal.component.spec.ts @@ -16,13 +16,19 @@ * @fileoverview Unit tests for QuestionPlayerConceptCardModalComponent. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed, fakeAsync, tick } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { SkillObjectFactory } from 'domain/skill/SkillObjectFactory'; -import { UrlService } from 'services/contextual/url.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { QuestionPlayerConceptCardModalComponent } from './question-player-concept-card-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + waitForAsync, + TestBed, + fakeAsync, + tick, +} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {SkillObjectFactory} from 'domain/skill/SkillObjectFactory'; +import {UrlService} from 'services/contextual/url.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {QuestionPlayerConceptCardModalComponent} from './question-player-concept-card-modal.component'; class MockActiveModal { close(): void { @@ -52,33 +58,31 @@ describe('Question Player Concept Card Modal component', () => { let mockWindow = { nativeWindow: { location: { - replace: jasmine.createSpy('replace') - } - } + replace: jasmine.createSpy('replace'), + }, + }, }; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - QuestionPlayerConceptCardModalComponent - ], + declarations: [QuestionPlayerConceptCardModalComponent], providers: [ SkillObjectFactory, UrlService, { provide: UrlService, - useClass: MockUrlService + useClass: MockUrlService, }, { provide: NgbActiveModal, - useClass: MockActiveModal + useClass: MockActiveModal, }, { provide: WindowRef, - useValue: mockWindow - } + useValue: mockWindow, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -91,30 +95,32 @@ describe('Question Player Concept Card Modal component', () => { fixture.detectChanges(); }); - it('should initialize component properties after controller is initialized', - () => { - component.skills = ['name1', 'name2']; - component.ngOnInit(); + it('should initialize component properties after controller is initialized', () => { + component.skills = ['name1', 'name2']; + component.ngOnInit(); - expect(component.index).toBe(0); - expect(component.modalHeader).toEqual('name1'); - }); + expect(component.index).toBe(0); + expect(component.modalHeader).toEqual('name1'); + }); - it('should go to next concept card, and identify when it is the last' + - ' concept card.', fakeAsync(() => { - component.index = 1; - component.skills = ['name1', 'name2']; + it( + 'should go to next concept card, and identify when it is the last' + + ' concept card.', + fakeAsync(() => { + component.index = 1; + component.skills = ['name1', 'name2']; - component.goToNextConceptCard(); - tick(); + component.goToNextConceptCard(); + tick(); - expect(component.index).toEqual(2); - expect(component.isLastConceptCard()).toBe(false); - })); + expect(component.index).toEqual(2); + expect(component.isLastConceptCard()).toBe(false); + }) + ); it('should refresh page when retrying a practice test', () => { spyOn(urlService, 'getUrlParams').and.returnValue({ - selected_subtopic_ids: 'selected_subtopic_ids' + selected_subtopic_ids: 'selected_subtopic_ids', }); spyOn(urlService, 'getPathname').and.returnValue('pathName'); diff --git a/core/templates/components/question-directives/question-player/question-player-concept-card-modal.component.ts b/core/templates/components/question-directives/question-player/question-player-concept-card-modal.component.ts index 2f2579bd89b6..17b0680ffcb0 100644 --- a/core/templates/components/question-directives/question-player/question-player-concept-card-modal.component.ts +++ b/core/templates/components/question-directives/question-player/question-player-concept-card-modal.component.ts @@ -16,19 +16,21 @@ * @fileoverview Component for question player concept card modal. */ -import { Component, Input, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { UrlService } from 'services/contextual/url.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import {Component, Input, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {UrlService} from 'services/contextual/url.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; @Component({ selector: 'oppia-question-player-concept-card-modal', - templateUrl: './question-player-concept-card-modal.component.html' + templateUrl: './question-player-concept-card-modal.component.html', }) export class QuestionPlayerConceptCardModalComponent - extends ConfirmOrCancelModal implements OnInit { + extends ConfirmOrCancelModal + implements OnInit +{ @Input() skillIds: string[] = []; @Input() skills: string[] = []; @@ -36,9 +38,9 @@ export class QuestionPlayerConceptCardModalComponent modalHeader: string = ''; constructor( - private ngbActiveModal: NgbActiveModal, - private windowRef: WindowRef, - private urlService: UrlService, + private ngbActiveModal: NgbActiveModal, + private windowRef: WindowRef, + private urlService: UrlService ) { super(ngbActiveModal); } @@ -58,16 +60,20 @@ export class QuestionPlayerConceptCardModalComponent } retryTest(): void { - const selectedSubtopics = ( - this.urlService.getUrlParams().selected_subtopic_ids); + const selectedSubtopics = + this.urlService.getUrlParams().selected_subtopic_ids; this.windowRef.nativeWindow.location.replace( - this.urlService.getPathname() + '?selected_subtopic_ids=' + - selectedSubtopics); + this.urlService.getPathname() + + '?selected_subtopic_ids=' + + selectedSubtopics + ); } } -angular.module('oppia').directive('oppiaQuestionPlayerConceptCardModal', +angular.module('oppia').directive( + 'oppiaQuestionPlayerConceptCardModal', downgradeComponent({ - component: QuestionPlayerConceptCardModalComponent - }) as angular.IDirectiveFactory); + component: QuestionPlayerConceptCardModalComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/components/question-directives/question-player/question-player.component.spec.ts b/core/templates/components/question-directives/question-player/question-player.component.spec.ts index e715809eeb06..ca3bf20870fa 100644 --- a/core/templates/components/question-directives/question-player/question-player.component.spec.ts +++ b/core/templates/components/question-directives/question-player/question-player.component.spec.ts @@ -16,26 +16,36 @@ * @fileoverview Unit tests for Question Player Component. */ - -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { ExplorationPlayerStateService } from 'pages/exploration-player-page/services/exploration-player-state.service'; -import { PlayerPositionService } from 'pages/exploration-player-page/services/player-position.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { PreventPageUnloadEventService } from 'services/prevent-page-unload-event.service'; -import { UserService } from 'services/user.service'; -import { Answer, QuestionData, QuestionPlayerComponent, QuestionPlayerConfig } from './question-player.component'; -import { QuestionPlayerStateService } from './services/question-player-state.service'; -import { Location } from '@angular/common'; -import { UserInfo } from 'domain/user/user-info.model'; -import { UrlService } from 'services/contextual/url.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {ExplorationPlayerStateService} from 'pages/exploration-player-page/services/exploration-player-state.service'; +import {PlayerPositionService} from 'pages/exploration-player-page/services/player-position.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {PreventPageUnloadEventService} from 'services/prevent-page-unload-event.service'; +import {UserService} from 'services/user.service'; +import { + Answer, + QuestionData, + QuestionPlayerComponent, + QuestionPlayerConfig, +} from './question-player.component'; +import {QuestionPlayerStateService} from './services/question-player-state.service'; +import {Location} from '@angular/common'; +import {UserInfo} from 'domain/user/user-info.model'; +import {UrlService} from 'services/contextual/url.service'; class MockNgbModal { open() { return { - result: Promise.resolve() + result: Promise.resolve(), }; } } @@ -55,17 +65,26 @@ describe('Question Player Component', () => { let questionPlayerStateServiceEmitter = new EventEmitter(); let urlService: UrlService; let userInfo = new UserInfo( - [], true, false, false, false, - true, 'en', 'username1', 'tester@example.org', true); + [], + true, + false, + false, + false, + true, + 'en', + 'username1', + 'tester@example.org', + true + ); class MockWindowRef { nativeWindow = { location: { href: '', - hash: null + hash: null, }, addEventListener: () => {}, - gtag: () => {} + gtag: () => {}, }; } @@ -91,39 +110,37 @@ describe('Question Player Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - QuestionPlayerComponent - ], + declarations: [QuestionPlayerComponent], providers: [ { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, { provide: WindowRef, - useClass: MockWindowRef + useClass: MockWindowRef, }, { provide: PlayerPositionService, - useClass: MockPlayerPositionService + useClass: MockPlayerPositionService, }, { provide: ExplorationPlayerStateService, - useClass: MockExplorationPlayerStateService + useClass: MockExplorationPlayerStateService, }, { provide: QuestionPlayerStateService, - useClass: MockQuestionPlayerStateService + useClass: MockQuestionPlayerStateService, }, { provide: Location, - useClass: MockLocation + useClass: MockLocation, }, PreventPageUnloadEventService, UserService, - UrlService + UrlService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -133,10 +150,12 @@ describe('Question Player Component', () => { ngbModal = TestBed.inject(NgbModal); playerPositionService = TestBed.inject(PlayerPositionService); - preventPageUnloadEventService = ( - TestBed.inject(PreventPageUnloadEventService)); - explorationPlayerStateService = ( - TestBed.inject(ExplorationPlayerStateService)); + preventPageUnloadEventService = TestBed.inject( + PreventPageUnloadEventService + ); + explorationPlayerStateService = TestBed.inject( + ExplorationPlayerStateService + ); questionPlayerStateService = TestBed.inject(QuestionPlayerStateService); userService = TestBed.inject(UserService); windowRef = TestBed.inject(WindowRef); @@ -164,8 +183,9 @@ describe('Question Player Component', () => { expect(component.totalScore).toBe(0.0); expect(component.scorePerSkillMapping).toEqual({}); expect(component.testIsPassed).toBe(true); - expect(questionPlayerStateService.resultsPageIsLoadedEventEmitter.emit) - .toHaveBeenCalledWith(false); + expect( + questionPlayerStateService.resultsPageIsLoadedEventEmitter.emit + ).toHaveBeenCalledWith(false); }); it('should add subscriptions on initialization', fakeAsync(() => { @@ -181,12 +201,15 @@ describe('Question Player Component', () => { questionPlayerStateServiceEmitter.emit('result'); tick(); - expect(playerPositionService.onCurrentQuestionChange.subscribe) - .toHaveBeenCalled(); - expect(explorationPlayerStateService.onTotalQuestionsReceived.subscribe) - .toHaveBeenCalled(); - expect(questionPlayerStateService.onQuestionSessionCompleted.subscribe) - .toHaveBeenCalled(); + expect( + playerPositionService.onCurrentQuestionChange.subscribe + ).toHaveBeenCalled(); + expect( + explorationPlayerStateService.onTotalQuestionsReceived.subscribe + ).toHaveBeenCalled(); + expect( + questionPlayerStateService.onQuestionSessionCompleted.subscribe + ).toHaveBeenCalled(); })); it('should update current question when current question is changed', () => { @@ -202,33 +225,33 @@ describe('Question Player Component', () => { expect(component.currentProgress).toBe(80); }); - it('should update total number of questions when count is received', - fakeAsync(() => { - component.ngOnInit(); - - expect(component.totalQuestions).toBe(0); + it('should update total number of questions when count is received', fakeAsync(() => { + component.ngOnInit(); - explorationPlayerStateServiceEmitter.emit(3); - tick(); - questionPlayerStateServiceEmitter.emit('new uri'); - tick(); + expect(component.totalQuestions).toBe(0); - expect(component.totalQuestions).toBe(3); - })); + explorationPlayerStateServiceEmitter.emit(3); + tick(); + questionPlayerStateServiceEmitter.emit('new uri'); + tick(); + expect(component.totalQuestions).toBe(3); + })); it('should get user info on initialization', fakeAsync(() => { spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl').and.returnValue( - 'classroom_url_fragment'); + 'classroom_url_fragment' + ); spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue( - 'topic_url_fragment'); + 'topic_url_fragment' + ); spyOn(component, 'calculateScores').and.stub(); spyOn(component, 'calculateMasteryDegrees').and.stub(); spyOn(component, 'hasUserPassedTest').and.returnValue(true); component.userIsLoggedIn = true; let data = JSON.stringify({ - state: {} + state: {}, }); windowRef.nativeWindow.location.hash = 'question-player-result=' + data; component.ngOnInit(); @@ -241,24 +264,28 @@ describe('Question Player Component', () => { })); it('should get the inner class name for action button', () => { - expect(component.getActionButtonInnerClass('REVIEW_LOWEST_SCORED_SKILL')) - .toBe('review-lowest-scored-skill-inner'); - expect(component.getActionButtonInnerClass('RETRY_SESSION')) - .toBe('new-session-inner'); - expect(component.getActionButtonInnerClass('DASHBOARD')) - .toBe('my-dashboard-inner'); - expect(component.getActionButtonInnerClass('INVALID_TYPE')) - .toBe(''); + expect( + component.getActionButtonInnerClass('REVIEW_LOWEST_SCORED_SKILL') + ).toBe('review-lowest-scored-skill-inner'); + expect(component.getActionButtonInnerClass('RETRY_SESSION')).toBe( + 'new-session-inner' + ); + expect(component.getActionButtonInnerClass('DASHBOARD')).toBe( + 'my-dashboard-inner' + ); + expect(component.getActionButtonInnerClass('INVALID_TYPE')).toBe(''); }); it('should get html for action button icon', () => { - expect(component.getActionButtonIconHtml( - 'REVIEW_LOWEST_SCORED_SKILL').toString()) - .toBe(''); - expect(component.getActionButtonIconHtml('RETRY_SESSION').toString()) - .toBe(''); - expect(component.getActionButtonIconHtml('DASHBOARD').toString()) - .toBe(''); + expect( + component.getActionButtonIconHtml('REVIEW_LOWEST_SCORED_SKILL').toString() + ).toBe(''); + expect(component.getActionButtonIconHtml('RETRY_SESSION').toString()).toBe( + '' + ); + expect(component.getActionButtonIconHtml('DASHBOARD').toString()).toBe( + '' + ); }); it('should close review lowest scored skill modal', () => { @@ -270,7 +297,7 @@ describe('Question Player Component', () => { masteryPerSkillMapping: null, skillId: null, userIsLoggedIn: null, - openConceptCardModal: mockEmitter + openConceptCardModal: mockEmitter, }, result: Promise.reject(), } as NgbModalRef); @@ -279,18 +306,18 @@ describe('Question Player Component', () => { skill1: { score: 5, total: 8, - description: '' + description: '', }, skill2: { score: 8, total: 8, - description: '' - } + description: '', + }, }; component.performAction({ type: 'REVIEW_LOWEST_SCORED_SKILL', - url: '' + url: '', }); expect(ngbModal.open).toHaveBeenCalled(); @@ -301,7 +328,7 @@ describe('Question Player Component', () => { component.performAction({ url: '/url', - type: '' + type: '', }); expect(windowRef.nativeWindow.location.href).toBe('/url'); @@ -309,7 +336,7 @@ describe('Question Player Component', () => { it('should check if action buttons footer is to be shown or not', () => { component.questionPlayerConfig = { - resultActionButtons: ['first'] + resultActionButtons: ['first'], } as QuestionPlayerConfig; expect(component.showActionButtonsFooter()).toBe(true); @@ -321,7 +348,7 @@ describe('Question Player Component', () => { passCutoff: 0, }, skillDescriptions: [], - skillList: [] + skillList: [], }; expect(component.showActionButtonsFooter()).toBe(false); }); @@ -330,20 +357,20 @@ describe('Question Player Component', () => { component.questionPlayerConfig = { questionPlayerMode: { modeType: 'PASS_FAIL', - passCutoff: 1.5 - } + passCutoff: 1.5, + }, } as QuestionPlayerConfig; component.scorePerSkillMapping = { skill1: { score: 5, total: 8, - description: '' + description: '', }, skill2: { score: 8, total: 8, - description: '' - } + description: '', + }, }; expect(component.hasUserPassedTest()).toBe(false); @@ -351,50 +378,58 @@ describe('Question Player Component', () => { component.questionPlayerConfig = { questionPlayerMode: { modeType: 'PASS_FAIL', - passCutoff: 0.5 - } + passCutoff: 0.5, + }, } as QuestionPlayerConfig; expect(component.hasUserPassedTest()).toBe(true); }); it('should get score percentage to set score bar width', () => { - expect(component.getScorePercentage({ - score: 5, - total: 10, - description: '' - })).toBe(50); - expect(component.getScorePercentage({ - score: 3, - total: 10, - description: '' - })).toBe(30); + expect( + component.getScorePercentage({ + score: 5, + total: 10, + description: '', + }) + ).toBe(50); + expect( + component.getScorePercentage({ + score: 3, + total: 10, + description: '', + }) + ).toBe(30); }); it('should calculate score based on question state data', () => { spyOn(urlService, 'getClassroomUrlFragmentFromUrl').and.returnValue( - 'classroom_url_fragment'); + 'classroom_url_fragment' + ); spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue( - 'topic_url_fragment'); + 'topic_url_fragment' + ); let questionStateData = { ques1: { answers: [], usedHints: [], viewedSolution: false, - linkedSkillIds: [] + linkedSkillIds: [], }, ques2: { - answers: [{ - isCorrect: false, - taggedSkillMisconceptionId: 'skillId1-misconception1' - } as Answer, { - isCorrect: true, - } as Answer + answers: [ + { + isCorrect: false, + taggedSkillMisconceptionId: 'skillId1-misconception1', + } as Answer, + { + isCorrect: true, + } as Answer, ], usedHints: ['hint1'], viewedSolution: true, - linkedSkillIds: ['skillId1', 'skillId2'] - } + linkedSkillIds: ['skillId1', 'skillId2'], + }, }; component.questionPlayerConfig = { resultActionButtons: [], @@ -403,42 +438,48 @@ describe('Question Player Component', () => { passCutoff: 0, }, skillList: ['skillId1'], - skillDescriptions: ['description1'] + skillDescriptions: ['description1'], } as QuestionPlayerConfig; component.totalScore = 0.0; component.calculateScores( - questionStateData as {[key: string]: QuestionData}); + questionStateData as {[key: string]: QuestionData} + ); expect(component.totalScore).toBe(55); - expect(questionPlayerStateService.resultsPageIsLoadedEventEmitter.emit) - .toHaveBeenCalledWith(true); + expect( + questionPlayerStateService.resultsPageIsLoadedEventEmitter.emit + ).toHaveBeenCalledWith(true); }); it('should calculate score based on question state data', () => { spyOn(urlService, 'getClassroomUrlFragmentFromUrl').and.returnValue( - 'classroom_url_fragment'); + 'classroom_url_fragment' + ); spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue( - 'topic_url_fragment'); + 'topic_url_fragment' + ); let questionStateData = { ques1: { answers: [], usedHints: [], viewedSolution: false, - linkedSkillIds: [] + linkedSkillIds: [], }, ques2: { - answers: [{ - isCorrect: false, - taggedSkillMisconceptionId: 'skillId1-misconception1' - } as Answer, { - isCorrect: true, - } as Answer + answers: [ + { + isCorrect: false, + taggedSkillMisconceptionId: 'skillId1-misconception1', + } as Answer, + { + isCorrect: true, + } as Answer, ], usedHints: ['hint1'], viewedSolution: true, - linkedSkillIds: [] - } + linkedSkillIds: [], + }, }; component.questionPlayerConfig = { resultActionButtons: [], @@ -447,58 +488,66 @@ describe('Question Player Component', () => { passCutoff: 0, }, skillList: ['skillId1'], - skillDescriptions: ['description1'] + skillDescriptions: ['description1'], } as QuestionPlayerConfig; component.totalScore = 0.0; component.calculateScores( - questionStateData as {[key: string]: QuestionData}); + questionStateData as {[key: string]: QuestionData} + ); - expect(questionPlayerStateService.resultsPageIsLoadedEventEmitter.emit) - .not.toHaveBeenCalledWith(false); + expect( + questionPlayerStateService.resultsPageIsLoadedEventEmitter.emit + ).not.toHaveBeenCalledWith(false); }); it('should calculate mastery degrees', () => { component.questionPlayerConfig = { skillList: ['skillId1'], - skillDescriptions: ['description1'] + skillDescriptions: ['description1'], } as QuestionPlayerConfig; let questionStateData = { ques1: { answers: [], usedHints: ['hint1'], viewedSolution: false, - linkedSkillIds: [] + linkedSkillIds: [], }, ques2: { - answers: [{ - isCorrect: false, - taggedSkillMisconceptionId: 'skillId1-misconception1' - } as Answer, { - isCorrect: true, - } as Answer + answers: [ + { + isCorrect: false, + taggedSkillMisconceptionId: 'skillId1-misconception1', + } as Answer, + { + isCorrect: true, + } as Answer, ], usedHints: ['hint1'], viewedSolution: true, - linkedSkillIds: ['skillId1', 'skillId2'] + linkedSkillIds: ['skillId1', 'skillId2'], }, ques3: { - answers: [{ - isCorrect: false, - taggedSkillMisconceptionId: 'skillId1-misconception1' - } as Answer], + answers: [ + { + isCorrect: false, + taggedSkillMisconceptionId: 'skillId1-misconception1', + } as Answer, + ], usedHints: ['hint1'], viewedSolution: false, - linkedSkillIds: ['skillId1'] + linkedSkillIds: ['skillId1'], }, ques4: { - answers: [{ - isCorrect: false, - } as Answer], + answers: [ + { + isCorrect: false, + } as Answer, + ], usedHints: ['hint1'], viewedSolution: false, - linkedSkillIds: ['skillId1'] - } + linkedSkillIds: ['skillId1'], + }, }; expect(component.masteryPerSkillMapping).toEqual(undefined); @@ -506,40 +555,45 @@ describe('Question Player Component', () => { component.calculateMasteryDegrees(questionStateData); expect(component.masteryPerSkillMapping).toEqual({ - skillId1: -0.04000000000000001 + skillId1: -0.04000000000000001, }); }); - it('should open concept card modal when user clicks on review' + - ' and retry', () => { - spyOn(component, 'openConceptCardModal').and.stub(); - component.failedSkillIds = ['skillId1']; + it( + 'should open concept card modal when user clicks on review' + ' and retry', + () => { + spyOn(component, 'openConceptCardModal').and.stub(); + component.failedSkillIds = ['skillId1']; - component.reviewConceptCardAndRetryTest(); + component.reviewConceptCardAndRetryTest(); - expect(component.openConceptCardModal).toHaveBeenCalled(); - }); + expect(component.openConceptCardModal).toHaveBeenCalled(); + } + ); - it('should throw error when user clicks on review and retry' + - ' and there are no failed skills', () => { - component.failedSkillIds = []; + it( + 'should throw error when user clicks on review and retry' + + ' and there are no failed skills', + () => { + component.failedSkillIds = []; - expect(() => component.reviewConceptCardAndRetryTest()).toThrowError( - 'No failed skills' - ); - }); + expect(() => component.reviewConceptCardAndRetryTest()).toThrowError( + 'No failed skills' + ); + } + ); it('should get color for score based on score per skill', () => { let scorePerSkill = { score: 5, total: 7, - description: '' + description: '', }; component.questionPlayerConfig = { questionPlayerMode: { modeType: 'NOT_PASS_FAIL', - passCutoff: 1.5 - } + passCutoff: 1.5, + }, } as QuestionPlayerConfig; expect(component.getColorForScore(scorePerSkill)).toBe('rgb(0, 150, 136)'); @@ -547,8 +601,8 @@ describe('Question Player Component', () => { component.questionPlayerConfig = { questionPlayerMode: { modeType: 'PASS_FAIL', - passCutoff: 1.5 - } + passCutoff: 1.5, + }, } as QuestionPlayerConfig; expect(component.getColorForScore(scorePerSkill)).toBe('rgb(217, 92, 12)'); @@ -556,8 +610,8 @@ describe('Question Player Component', () => { component.questionPlayerConfig = { questionPlayerMode: { modeType: 'PASS_FAIL', - passCutoff: 0.5 - } + passCutoff: 0.5, + }, } as QuestionPlayerConfig; expect(component.getColorForScore(scorePerSkill)).toBe('rgb(0, 150, 136)'); @@ -567,149 +621,154 @@ describe('Question Player Component', () => { let scorePerSkill = { score: 5, total: 7, - description: '' + description: '', }; component.questionPlayerConfig = { questionPlayerMode: { modeType: 'NOT_PASS_FAIL', - passCutoff: 1.5 - } + passCutoff: 1.5, + }, } as QuestionPlayerConfig; - expect( - component.getColorForScoreBar(scorePerSkill)).toBe('rgb(32, 93, 134)'); + expect(component.getColorForScoreBar(scorePerSkill)).toBe( + 'rgb(32, 93, 134)' + ); component.questionPlayerConfig = { questionPlayerMode: { modeType: 'PASS_FAIL', - passCutoff: 1.5 - } + passCutoff: 1.5, + }, } as QuestionPlayerConfig; - expect( - component.getColorForScoreBar(scorePerSkill)).toBe('rgb(217, 92, 12)'); + expect(component.getColorForScoreBar(scorePerSkill)).toBe( + 'rgb(217, 92, 12)' + ); component.questionPlayerConfig = { questionPlayerMode: { modeType: 'PASS_FAIL', - passCutoff: 0.5 - } + passCutoff: 0.5, + }, } as QuestionPlayerConfig; - expect( - component.getColorForScoreBar(scorePerSkill)).toBe('rgb(32, 93, 134)'); + expect(component.getColorForScoreBar(scorePerSkill)).toBe( + 'rgb(32, 93, 134)' + ); }); - it('should open skill mastery modal when user clicks on skill', - fakeAsync(() => { - let masteryPerSkillMapping = { - skillId1: -0.1 - }; - let skillId; - component.scorePerSkillMapping = { - skill1: { - score: 5, - total: 8, - description: '' - }, - skill2: { - score: 8, - total: 8, - description: '' - } - }; - - let mockEmitter = new EventEmitter(); - spyOn(ngbModal, 'open').and.returnValue({ + it('should open skill mastery modal when user clicks on skill', fakeAsync(() => { + let masteryPerSkillMapping = { + skillId1: -0.1, + }; + let skillId; + component.scorePerSkillMapping = { + skill1: { + score: 5, + total: 8, + description: '', + }, + skill2: { + score: 8, + total: 8, + description: '', + }, + }; + + let mockEmitter = new EventEmitter(); + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: { + skills: null, + skillIds: skillId, + masteryPerSkillMapping: masteryPerSkillMapping, + skillId: 'skillId1', + userIsLoggedIn: true, + openConceptCardModal: mockEmitter, + }, + result: Promise.resolve(), + } as NgbModalRef); + + component.masteryPerSkillMapping = { + skillId1: -0.1, + }; + component.scorePerSkillMapping = { + skill1: { + score: 10, + total: 5, + description: '', + }, + skill2: { + score: 5, + total: 10, + description: '', + }, + }; + tick(); + component.openConceptCardModal(['skill1', 'skill2']); + tick(); + component.userIsLoggedIn = true; + tick(); + + component.scorePerSkillMapping = { + skill1: { + score: 10, + total: 5, + description: '', + }, + skill2: { + score: 5, + total: 10, + description: '', + }, + }; + component.openSkillMasteryModal('skillId1'); + tick(); + + expect(masteryPerSkillMapping).toEqual({skillId1: -0.1}); + expect(component.userIsLoggedIn).toBe(true); + })); + + it('should close skill master modal when user clicks cancel', fakeAsync(() => { + component.masteryPerSkillMapping = { + skillId1: -0.1, + }; + + let mockEmitter = new EventEmitter(); + spyOn(component, 'openConceptCardModal').and.stub(); + spyOn(ngbModal, 'open').and.callFake(options => { + return { componentInstance: { skills: null, - skillIds: skillId, - masteryPerSkillMapping: masteryPerSkillMapping, - skillId: 'skillId1', - userIsLoggedIn: true, - openConceptCardModal: mockEmitter - }, - result: Promise.resolve() - } as NgbModalRef); - - component.masteryPerSkillMapping = { - skillId1: -0.1 - }; - component.scorePerSkillMapping = { - skill1: { - score: 10, - total: 5, - description: '' + skillIds: null, + masteryPerSkillMapping: null, + skillId: null, + userIsLoggedIn: null, + openConceptCardModal: mockEmitter, }, - skill2: { - score: 5, - total: 10, - description: '' - } - }; - tick(); - component.openConceptCardModal(['skill1', 'skill2']); - tick(); - component.userIsLoggedIn = true; - tick(); - - component.scorePerSkillMapping = { - skill1: { - score: 10, - total: 5, - description: '' - }, - skill2: { - score: 5, - total: 10, - description: '' - } - }; - component.openSkillMasteryModal('skillId1'); - tick(); - - expect(masteryPerSkillMapping).toEqual({skillId1: -0.1}); - expect(component.userIsLoggedIn).toBe(true); - })); - - it('should close skill master modal when user clicks cancel', - fakeAsync(() => { - component.masteryPerSkillMapping = { - skillId1: -0.1 - }; - - let mockEmitter = new EventEmitter(); - spyOn(component, 'openConceptCardModal').and.stub(); - spyOn(ngbModal, 'open').and.callFake((options) => { - return { - componentInstance: { - skills: null, - skillIds: null, - masteryPerSkillMapping: null, - skillId: null, - userIsLoggedIn: null, - openConceptCardModal: mockEmitter - }, - result: Promise.reject() - } as NgbModalRef; - }); - - component.openSkillMasteryModal('skillId1'); - mockEmitter.emit('skillId1'); - tick(); - - expect(ngbModal.open).toHaveBeenCalled(); - expect(component.openConceptCardModal).toHaveBeenCalled(); - })); + result: Promise.reject(), + } as NgbModalRef; + }); - it('should prevent page reload or exit in between' + - 'practice session', () => { - spyOn(preventPageUnloadEventService, 'addListener').and - .callFake((callback: () => boolean) => callback() as boolean); + component.openSkillMasteryModal('skillId1'); + mockEmitter.emit('skillId1'); + tick(); - component.ngOnInit(); + expect(ngbModal.open).toHaveBeenCalled(); + expect(component.openConceptCardModal).toHaveBeenCalled(); + })); - expect(preventPageUnloadEventService.addListener) - .toHaveBeenCalledWith(jasmine.any(Function)); - }); + it( + 'should prevent page reload or exit in between' + 'practice session', + () => { + spyOn(preventPageUnloadEventService, 'addListener').and.callFake( + (callback: () => boolean) => callback() as boolean + ); + + component.ngOnInit(); + + expect(preventPageUnloadEventService.addListener).toHaveBeenCalledWith( + jasmine.any(Function) + ); + } + ); }); diff --git a/core/templates/components/question-directives/question-player/question-player.component.ts b/core/templates/components/question-directives/question-player/question-player.component.ts index 8ed7efbabb8e..633772714df7 100644 --- a/core/templates/components/question-directives/question-player/question-player.component.ts +++ b/core/templates/components/question-directives/question-player/question-player.component.ts @@ -16,25 +16,31 @@ * @fileoverview Component for the questions player. */ -import { Component, Input, OnDestroy, OnInit, SecurityContext } from '@angular/core'; -import { Location } from '@angular/common'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { DomSanitizer } from '@angular/platform-browser'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { Subscription } from 'rxjs'; -import { SkillMasteryBackendApiService } from 'domain/skill/skill-mastery-backend-api.service'; -import { ExplorationPlayerStateService } from 'pages/exploration-player-page/services/exploration-player-state.service'; -import { PlayerPositionService } from 'pages/exploration-player-page/services/player-position.service'; -import { PreventPageUnloadEventService } from 'services/prevent-page-unload-event.service'; -import { QuestionPlayerConceptCardModalComponent } from './question-player-concept-card-modal.component'; -import { QuestionPlayerConstants } from 'components/question-directives/question-player/question-player.constants'; -import { SkillMasteryModalComponent } from './skill-mastery-modal.component'; -import { UserService } from 'services/user.service'; -import { QuestionPlayerStateService } from './services/question-player-state.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { ContextService } from 'services/context.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { UrlService } from 'services/contextual/url.service'; +import { + Component, + Input, + OnDestroy, + OnInit, + SecurityContext, +} from '@angular/core'; +import {Location} from '@angular/common'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {DomSanitizer} from '@angular/platform-browser'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {Subscription} from 'rxjs'; +import {SkillMasteryBackendApiService} from 'domain/skill/skill-mastery-backend-api.service'; +import {ExplorationPlayerStateService} from 'pages/exploration-player-page/services/exploration-player-state.service'; +import {PlayerPositionService} from 'pages/exploration-player-page/services/player-position.service'; +import {PreventPageUnloadEventService} from 'services/prevent-page-unload-event.service'; +import {QuestionPlayerConceptCardModalComponent} from './question-player-concept-card-modal.component'; +import {QuestionPlayerConstants} from 'components/question-directives/question-player/question-player.constants'; +import {SkillMasteryModalComponent} from './skill-mastery-modal.component'; +import {UserService} from 'services/user.service'; +import {QuestionPlayerStateService} from './services/question-player-state.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {ContextService} from 'services/context.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {UrlService} from 'services/contextual/url.service'; export interface QuestionData { linkedSkillIds: string[]; @@ -84,7 +90,7 @@ export interface QuestionPlayerConfig { @Component({ selector: 'oppia-question-player', - templateUrl: './question-player.component.html' + templateUrl: './question-player.component.html', }) export class QuestionPlayerComponent implements OnInit, OnDestroy { // These properties below are initialized using Angular lifecycle hooks @@ -131,23 +137,26 @@ export class QuestionPlayerComponent implements OnInit, OnDestroy { let totalHintsPenalty = 0.0; let wrongAnswerPenalty = 0.0; if (questionData.answers) { - wrongAnswerPenalty = ( + wrongAnswerPenalty = (questionData.answers.length - 1) * - QuestionPlayerConstants.WRONG_ANSWER_PENALTY); + QuestionPlayerConstants.WRONG_ANSWER_PENALTY; } if (questionData.usedHints) { - totalHintsPenalty = ( + totalHintsPenalty = questionData.usedHints.length * - QuestionPlayerConstants.VIEW_HINT_PENALTY); + QuestionPlayerConstants.VIEW_HINT_PENALTY; } let questionScore = Number( - QuestionPlayerConstants.MAX_SCORE_PER_QUESTION); + QuestionPlayerConstants.MAX_SCORE_PER_QUESTION + ); if (questionData.viewedSolution) { questionScore = 0.0; } else { // If questionScore goes negative, set it to 0. questionScore = Math.max( - 0, questionScore - totalHintsPenalty - wrongAnswerPenalty); + 0, + questionScore - totalHintsPenalty - wrongAnswerPenalty + ); } // Calculate total score. this.totalScore += questionScore; @@ -168,29 +177,29 @@ export class QuestionPlayerComponent implements OnInit, OnDestroy { } } this.finalCorrect = this.totalScore; - this.totalScore = Math.round( - this.totalScore * 100 / totalQuestions); + this.totalScore = Math.round((this.totalScore * 100) / totalQuestions); this.resultsLoaded = true; this.questionPlayerStateService.resultsPageIsLoadedEventEmitter.emit( - this.resultsLoaded); + this.resultsLoaded + ); } getMasteryChangeForWrongAnswers( - answers: Answer[], - masteryChangePerQuestion: MasteryChangePerQuestion): - MasteryChangePerQuestion { - answers.forEach((answer) => { + answers: Answer[], + masteryChangePerQuestion: MasteryChangePerQuestion + ): MasteryChangePerQuestion { + answers.forEach(answer => { if (!answer.isCorrect) { if (answer.taggedSkillMisconceptionId) { let skillId = answer.taggedSkillMisconceptionId.split('-')[0]; if (masteryChangePerQuestion.hasOwnProperty(skillId)) { masteryChangePerQuestion[skillId] -= - QuestionPlayerConstants.WRONG_ANSWER_PENALTY_FOR_MASTERY; + QuestionPlayerConstants.WRONG_ANSWER_PENALTY_FOR_MASTERY; } } else { for (let masterySkillId in masteryChangePerQuestion) { masteryChangePerQuestion[masterySkillId] -= - QuestionPlayerConstants.WRONG_ANSWER_PENALTY_FOR_MASTERY; + QuestionPlayerConstants.WRONG_ANSWER_PENALTY_FOR_MASTERY; } } } @@ -200,7 +209,8 @@ export class QuestionPlayerComponent implements OnInit, OnDestroy { } updateMasteryPerSkillMapping( - masteryChangePerQuestion: MasteryChangePerQuestion): void { + masteryChangePerQuestion: MasteryChangePerQuestion + ): void { for (let skillId in masteryChangePerQuestion) { if (!(skillId in this.masteryPerSkillMapping)) { continue; @@ -208,37 +218,40 @@ export class QuestionPlayerComponent implements OnInit, OnDestroy { // Set the lowest bound of mastery change for each question. this.masteryPerSkillMapping[skillId] += Math.max( masteryChangePerQuestion[skillId], - QuestionPlayerConstants.MAX_MASTERY_LOSS_PER_QUESTION); + QuestionPlayerConstants.MAX_MASTERY_LOSS_PER_QUESTION + ); } } - calculateMasteryDegrees( - questionStateData: {[key: string]: QuestionData} - ): void { + calculateMasteryDegrees(questionStateData: { + [key: string]: QuestionData; + }): void { this.createMasteryPerSkillMapping(); for (let question in questionStateData) { let questionData = questionStateData[question]; if (questionData.linkedSkillIds) { let masteryChangePerQuestion = - this.createMasteryChangePerQuestion(questionData); + this.createMasteryChangePerQuestion(questionData); if (questionData.viewedSolution) { for (let skillId in masteryChangePerQuestion) { masteryChangePerQuestion[skillId] = - QuestionPlayerConstants.MAX_MASTERY_LOSS_PER_QUESTION; + QuestionPlayerConstants.MAX_MASTERY_LOSS_PER_QUESTION; } } else { if (questionData.usedHints) { for (let skillId in masteryChangePerQuestion) { - masteryChangePerQuestion[skillId] -= ( + masteryChangePerQuestion[skillId] -= questionData.usedHints.length * - QuestionPlayerConstants.VIEW_HINT_PENALTY_FOR_MASTERY); + QuestionPlayerConstants.VIEW_HINT_PENALTY_FOR_MASTERY; } } if (questionData.answers) { masteryChangePerQuestion = this.getMasteryChangeForWrongAnswers( - questionData.answers, masteryChangePerQuestion); + questionData.answers, + masteryChangePerQuestion + ); } } this.updateMasteryPerSkillMapping(masteryChangePerQuestion); @@ -246,18 +259,22 @@ export class QuestionPlayerComponent implements OnInit, OnDestroy { } this.skillMasteryBackendApiService.updateSkillMasteryDegreesAsync( - this.masteryPerSkillMapping); + this.masteryPerSkillMapping + ); } hasUserPassedTest(): boolean { let testIsPassed: boolean = true; let failedSkillIds: string[] = []; if (this.isInPassOrFailMode()) { - Object.keys(this.scorePerSkillMapping).forEach((skillId) => { - let correctionRate = this.scorePerSkillMapping[skillId].score / + Object.keys(this.scorePerSkillMapping).forEach(skillId => { + let correctionRate = + this.scorePerSkillMapping[skillId].score / this.scorePerSkillMapping[skillId].total; - if (correctionRate < - this.questionPlayerConfig.questionPlayerMode.passCutoff) { + if ( + correctionRate < + this.questionPlayerConfig.questionPlayerMode.passCutoff + ) { testIsPassed = false; failedSkillIds.push(skillId); } @@ -272,7 +289,7 @@ export class QuestionPlayerComponent implements OnInit, OnDestroy { } getScorePercentage(scorePerSkill: ScorePerSkill): number { - return scorePerSkill.score / scorePerSkill.total * 100; + return (scorePerSkill.score / scorePerSkill.total) * 100; } getColorForScore(scorePerSkill: ScorePerSkill): string { @@ -280,8 +297,9 @@ export class QuestionPlayerComponent implements OnInit, OnDestroy { return QuestionPlayerConstants.COLORS_FOR_PASS_FAIL_MODE.PASSED_COLOR; } let correctionRate = scorePerSkill.score / scorePerSkill.total; - if (correctionRate >= - this.questionPlayerConfig.questionPlayerMode.passCutoff) { + if ( + correctionRate >= this.questionPlayerConfig.questionPlayerMode.passCutoff + ) { return QuestionPlayerConstants.COLORS_FOR_PASS_FAIL_MODE.PASSED_COLOR; } else { return QuestionPlayerConstants.COLORS_FOR_PASS_FAIL_MODE.FAILED_COLOR; @@ -293,8 +311,9 @@ export class QuestionPlayerComponent implements OnInit, OnDestroy { return QuestionPlayerConstants.COLORS_FOR_PASS_FAIL_MODE.PASSED_COLOR_BAR; } let correctionRate = scorePerSkill.score / scorePerSkill.total; - if (correctionRate >= - this.questionPlayerConfig.questionPlayerMode.passCutoff) { + if ( + correctionRate >= this.questionPlayerConfig.questionPlayerMode.passCutoff + ) { return QuestionPlayerConstants.COLORS_FOR_PASS_FAIL_MODE.PASSED_COLOR_BAR; } else { return QuestionPlayerConstants.COLORS_FOR_PASS_FAIL_MODE.FAILED_COLOR; @@ -316,8 +335,7 @@ export class QuestionPlayerComponent implements OnInit, OnDestroy { backdrop: true, }); - modelRef.componentInstance.masteryPerSkillMapping = ( - masteryPerSkillMapping); + modelRef.componentInstance.masteryPerSkillMapping = masteryPerSkillMapping; modelRef.componentInstance.skillId = skillId; modelRef.componentInstance.userIsLoggedIn = this.userIsLoggedIn; modelRef.componentInstance.openConceptCardModal.subscribe( @@ -326,11 +344,14 @@ export class QuestionPlayerComponent implements OnInit, OnDestroy { } ); - modelRef.result.then(() => {}, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + modelRef.result.then( + () => {}, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } reviewLowestScoredSkillsModal(): void { @@ -365,8 +386,8 @@ export class QuestionPlayerComponent implements OnInit, OnDestroy { updateQuestionProgression(): void { if (this.getTotalQuestions() > 0) { - this.currentProgress = ( - this.getCurrentQuestion() * 100 / this.getTotalQuestions()); + this.currentProgress = + (this.getCurrentQuestion() * 100) / this.getTotalQuestions(); } else { this.currentProgress = 0; } @@ -384,22 +405,21 @@ export class QuestionPlayerComponent implements OnInit, OnDestroy { return ( this.questionPlayerConfig.questionPlayerMode && this.questionPlayerConfig.questionPlayerMode.modeType === - QuestionPlayerConstants.QUESTION_PLAYER_MODE.PASS_FAIL_MODE); + QuestionPlayerConstants.QUESTION_PLAYER_MODE.PASS_FAIL_MODE + ); } createScorePerSkillMapping(): void { let scorePerSkillMapping: Record = {}; if (this.questionPlayerConfig.skillList) { - for (let i = 0; - i < this.questionPlayerConfig.skillList.length; i++) { + for (let i = 0; i < this.questionPlayerConfig.skillList.length; i++) { let skillId = this.questionPlayerConfig.skillList[i]; - let description = - this.questionPlayerConfig.skillDescriptions[i]; + let description = this.questionPlayerConfig.skillDescriptions[i]; scorePerSkillMapping[skillId] = { description: description, score: 0.0, - total: 0.0 + total: 0.0, }; } } @@ -410,8 +430,7 @@ export class QuestionPlayerComponent implements OnInit, OnDestroy { createMasteryPerSkillMapping(): void { let masteryPerSkillMapping: Record = {}; if (this.questionPlayerConfig.skillList) { - for (let i = 0; - i < this.questionPlayerConfig.skillList.length; i++) { + for (let i = 0; i < this.questionPlayerConfig.skillList.length; i++) { let skillId = this.questionPlayerConfig.skillList[i]; masteryPerSkillMapping[skillId] = 0.0; } @@ -420,12 +439,13 @@ export class QuestionPlayerComponent implements OnInit, OnDestroy { } createMasteryChangePerQuestion( - questionData: QuestionData): MasteryChangePerQuestion { + questionData: QuestionData + ): MasteryChangePerQuestion { let masteryChangePerQuestion: Record = {}; for (let i = 0; i < questionData.linkedSkillIds.length; i++) { let skillId = questionData.linkedSkillIds[i]; masteryChangePerQuestion[skillId] = - QuestionPlayerConstants.MAX_MASTERY_GAIN_PER_QUESTION; + QuestionPlayerConstants.MAX_MASTERY_GAIN_PER_QUESTION; } return masteryChangePerQuestion; } @@ -443,17 +463,16 @@ export class QuestionPlayerComponent implements OnInit, OnDestroy { getActionButtonIconHtml(actionButtonType: string): string { let iconHtml = ''; if (actionButtonType === 'REVIEW_LOWEST_SCORED_SKILL') { - iconHtml = ''; + iconHtml = + ''; } else if (actionButtonType === 'RETRY_SESSION') { - iconHtml = ''; + iconHtml = + ''; } else if (actionButtonType === 'DASHBOARD') { - iconHtml = ''; + iconHtml = + ''; } - return this._sanitizer.sanitize( - SecurityContext.HTML, iconHtml) as string; + return this._sanitizer.sanitize(SecurityContext.HTML, iconHtml) as string; } performAction(actionButton: ActionButton): void { @@ -467,13 +486,14 @@ export class QuestionPlayerComponent implements OnInit, OnDestroy { showActionButtonsFooter(): boolean { return ( this.questionPlayerConfig.resultActionButtons && - this.questionPlayerConfig.resultActionButtons.length > 0); + this.questionPlayerConfig.resultActionButtons.length > 0 + ); } getWorstSkillIds(): string[] { let minScore: number = 0.95; let worstSkillIds: [number, string][] = []; - Object.keys(this.scorePerSkillMapping).forEach((skillId) => { + Object.keys(this.scorePerSkillMapping).forEach(skillId => { let skillScoreData = this.scorePerSkillMapping[skillId]; let scorePercentage = skillScoreData.score / skillScoreData.total; if (scorePercentage < minScore) { @@ -481,29 +501,36 @@ export class QuestionPlayerComponent implements OnInit, OnDestroy { } }); - return worstSkillIds.sort().slice(0, 3).map(info => info[1]); + return worstSkillIds + .sort() + .slice(0, 3) + .map(info => info[1]); } openConceptCardModal(skillIds: string[]): void { let skills: string[] = []; - skillIds.forEach((skillId) => { - skills.push( - this.scorePerSkillMapping[skillId].description); + skillIds.forEach(skillId => { + skills.push(this.scorePerSkillMapping[skillId].description); }); const modelRef = this.ngbModal.open( - QuestionPlayerConceptCardModalComponent, { + QuestionPlayerConceptCardModalComponent, + { backdrop: true, - }); + } + ); modelRef.componentInstance.skills = skills; modelRef.componentInstance.skillIds = skillIds; - modelRef.result.then(() => {}, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + modelRef.result.then( + () => {}, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } initResults(): void { @@ -521,8 +548,8 @@ export class QuestionPlayerComponent implements OnInit, OnDestroy { ngOnInit(): void { { this.componentSubscription.add( - this.playerPositionService.onCurrentQuestionChange.subscribe( - result => this.updateCurrentQuestion(result + 1) + this.playerPositionService.onCurrentQuestionChange.subscribe(result => + this.updateCurrentQuestion(result + 1) ) ); @@ -534,26 +561,31 @@ export class QuestionPlayerComponent implements OnInit, OnDestroy { this.componentSubscription.add( this.questionPlayerStateService.onQuestionSessionCompleted.subscribe( - (result) => { - this.windowRef.nativeWindow.location.hash = ( + result => { + this.windowRef.nativeWindow.location.hash = QuestionPlayerConstants.HASH_PARAM + - encodeURIComponent(JSON.stringify(result))); + encodeURIComponent(JSON.stringify(result)); this.contextService.removeCustomEntityContext(); - }) + } + ) ); this.location.onUrlChange(() => { let hashContent = this.windowRef.nativeWindow.location.hash; - if (!hashContent || hashContent.indexOf( - QuestionPlayerConstants.HASH_PARAM) === -1) { + if ( + !hashContent || + hashContent.indexOf(QuestionPlayerConstants.HASH_PARAM) === -1 + ) { return; } let resultHashString = decodeURIComponent( - hashContent.substring(hashContent.indexOf( - QuestionPlayerConstants.HASH_PARAM) + - QuestionPlayerConstants.HASH_PARAM.length)); + hashContent.substring( + hashContent.indexOf(QuestionPlayerConstants.HASH_PARAM) + + QuestionPlayerConstants.HASH_PARAM.length + ) + ); if (resultHashString) { this.initResults(); @@ -575,7 +607,7 @@ export class QuestionPlayerComponent implements OnInit, OnDestroy { }); this.userIsLoggedIn = false; - this.userService.getUserInfoAsync().then((userInfo) => { + this.userService.getUserInfoAsync().then(userInfo => { this.canCreateCollections = userInfo.canCreateCollections(); this.userIsLoggedIn = userInfo.isLoggedIn(); }); @@ -583,12 +615,11 @@ export class QuestionPlayerComponent implements OnInit, OnDestroy { // called in this.$on when some external events are triggered. this.initResults(); this.questionPlayerStateService.resultsPageIsLoadedEventEmitter.emit( - this.resultsLoaded); - this.preventPageUnloadEventService.addListener( - () => { - return (this.getCurrentQuestion() > 1); - } + this.resultsLoaded ); + this.preventPageUnloadEventService.addListener(() => { + return this.getCurrentQuestion() > 1; + }); } } @@ -597,7 +628,9 @@ export class QuestionPlayerComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive('oppiaQuestionPlayer', +angular.module('oppia').directive( + 'oppiaQuestionPlayer', downgradeComponent({ - component: QuestionPlayerComponent - }) as angular.IDirectiveFactory); + component: QuestionPlayerComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/components/question-directives/question-player/question-player.constants.ajs.ts b/core/templates/components/question-directives/question-player/question-player.constants.ajs.ts index e63392b5da98..17cb39dbb6eb 100644 --- a/core/templates/components/question-directives/question-player/question-player.constants.ajs.ts +++ b/core/templates/components/question-directives/question-player/question-player.constants.ajs.ts @@ -18,40 +18,67 @@ // TODO(#7092): Delete this file once migration is complete and these AngularJS // equivalents of the Angular constants are no longer needed. -import { QuestionPlayerConstants } from - 'components/question-directives/question-player/question-player.constants'; +import {QuestionPlayerConstants} from 'components/question-directives/question-player/question-player.constants'; -angular.module('oppia').constant( - 'HASH_PARAM', QuestionPlayerConstants.HASH_PARAM); -angular.module('oppia').constant( - 'MAX_SCORE_PER_QUESTION', QuestionPlayerConstants.MAX_SCORE_PER_QUESTION); +angular + .module('oppia') + .constant('HASH_PARAM', QuestionPlayerConstants.HASH_PARAM); +angular + .module('oppia') + .constant( + 'MAX_SCORE_PER_QUESTION', + QuestionPlayerConstants.MAX_SCORE_PER_QUESTION + ); -angular.module('oppia').constant( - 'MAX_MASTERY_GAIN_PER_QUESTION', - QuestionPlayerConstants.MAX_MASTERY_GAIN_PER_QUESTION); +angular + .module('oppia') + .constant( + 'MAX_MASTERY_GAIN_PER_QUESTION', + QuestionPlayerConstants.MAX_MASTERY_GAIN_PER_QUESTION + ); -angular.module('oppia').constant( - 'MAX_MASTERY_LOSS_PER_QUESTION', - QuestionPlayerConstants.MAX_MASTERY_LOSS_PER_QUESTION); +angular + .module('oppia') + .constant( + 'MAX_MASTERY_LOSS_PER_QUESTION', + QuestionPlayerConstants.MAX_MASTERY_LOSS_PER_QUESTION + ); +angular + .module('oppia') + .constant( + 'COLORS_FOR_PASS_FAIL_MODE', + QuestionPlayerConstants.COLORS_FOR_PASS_FAIL_MODE + ); -angular.module('oppia').constant( - 'COLORS_FOR_PASS_FAIL_MODE', - QuestionPlayerConstants.COLORS_FOR_PASS_FAIL_MODE); +angular + .module('oppia') + .constant( + 'QUESTION_PLAYER_MODE', + QuestionPlayerConstants.QUESTION_PLAYER_MODE + ); -angular.module('oppia').constant( - 'QUESTION_PLAYER_MODE', QuestionPlayerConstants.QUESTION_PLAYER_MODE); +angular + .module('oppia') + .constant('VIEW_HINT_PENALTY', QuestionPlayerConstants.VIEW_HINT_PENALTY); -angular.module('oppia').constant( - 'VIEW_HINT_PENALTY', QuestionPlayerConstants.VIEW_HINT_PENALTY); +angular + .module('oppia') + .constant( + 'VIEW_HINT_PENALTY_FOR_MASTERY', + QuestionPlayerConstants.VIEW_HINT_PENALTY_FOR_MASTERY + ); -angular.module('oppia').constant( - 'VIEW_HINT_PENALTY_FOR_MASTERY', - QuestionPlayerConstants.VIEW_HINT_PENALTY_FOR_MASTERY); +angular + .module('oppia') + .constant( + 'WRONG_ANSWER_PENALTY', + QuestionPlayerConstants.WRONG_ANSWER_PENALTY + ); -angular.module('oppia').constant( - 'WRONG_ANSWER_PENALTY', QuestionPlayerConstants.WRONG_ANSWER_PENALTY); - -angular.module('oppia').constant( - 'WRONG_ANSWER_PENALTY_FOR_MASTERY', - QuestionPlayerConstants.WRONG_ANSWER_PENALTY_FOR_MASTERY); +angular + .module('oppia') + .constant( + 'WRONG_ANSWER_PENALTY_FOR_MASTERY', + QuestionPlayerConstants.WRONG_ANSWER_PENALTY_FOR_MASTERY + ); diff --git a/core/templates/components/question-directives/question-player/question-player.constants.ts b/core/templates/components/question-directives/question-player/question-player.constants.ts index 1ca463cb2f18..c99201bc6a92 100644 --- a/core/templates/components/question-directives/question-player/question-player.constants.ts +++ b/core/templates/components/question-directives/question-player/question-player.constants.ts @@ -33,11 +33,11 @@ export const QuestionPlayerConstants = { // Color blue. PASSED_COLOR_BAR: 'rgb(32, 93, 134)', // Color shallow green. - PASSED_COLOR_OUTER: 'rgb(143, 217, 209)' + PASSED_COLOR_OUTER: 'rgb(143, 217, 209)', }, QUESTION_PLAYER_MODE: { - PASS_FAIL_MODE: 'PASS_FAIL' + PASS_FAIL_MODE: 'PASS_FAIL', }, VIEW_HINT_PENALTY: 0.1, diff --git a/core/templates/components/question-directives/question-player/services/question-player-state.service.spec.ts b/core/templates/components/question-directives/question-player/services/question-player-state.service.spec.ts index 2ba75c5bfcbc..da69fda4dd08 100644 --- a/core/templates/components/question-directives/question-player/services/question-player-state.service.spec.ts +++ b/core/templates/components/question-directives/question-player/services/question-player-state.service.spec.ts @@ -16,10 +16,10 @@ * @fileoverview Unit tests for question player state service. */ -import { TestBed, waitForAsync } from '@angular/core/testing'; -import { Question } from 'domain/question/QuestionObjectFactory'; -import { StateObjectFactory } from 'domain/state/StateObjectFactory'; -import { QuestionPlayerStateService } from './question-player-state.service'; +import {TestBed, waitForAsync} from '@angular/core/testing'; +import {Question} from 'domain/question/QuestionObjectFactory'; +import {StateObjectFactory} from 'domain/state/StateObjectFactory'; +import {QuestionPlayerStateService} from './question-player-state.service'; describe('Question player state service', () => { let qpss: QuestionPlayerStateService; @@ -33,7 +33,12 @@ describe('Question player state service', () => { question = new Question( questionId, stateObject.createDefaultState('state', 'content_0', 'default_outcome_1'), - '', 7, [], [], 2); + '', + 7, + [], + [], + 2 + ); })); beforeEach(() => { diff --git a/core/templates/components/question-directives/question-player/services/question-player-state.service.ts b/core/templates/components/question-directives/question-player/services/question-player-state.service.ts index c59e80661bb9..39c306e750de 100644 --- a/core/templates/components/question-directives/question-player/services/question-player-state.service.ts +++ b/core/templates/components/question-directives/question-player/services/question-player-state.service.ts @@ -17,9 +17,9 @@ * in the test session. */ -import { EventEmitter, Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Question } from 'domain/question/QuestionObjectFactory'; +import {EventEmitter, Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Question} from 'domain/question/QuestionObjectFactory'; interface UsedHintOrSolution { timestamp: number; @@ -43,7 +43,7 @@ interface QuestionPlayerState { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class QuestionPlayerStateService { questionPlayerState: QuestionPlayerState = {}; @@ -55,14 +55,14 @@ export class QuestionPlayerStateService { } private _createNewQuestionPlayerState( - questionId: string, - linkedSkillIds: string[] + questionId: string, + linkedSkillIds: string[] ): void { this.questionPlayerState[questionId] = { linkedSkillIds: linkedSkillIds, answers: [], usedHints: [], - viewedSolution: undefined + viewedSolution: undefined, }; } @@ -70,10 +70,12 @@ export class QuestionPlayerStateService { let questionId = question.getId() as string; if (!this.questionPlayerState[questionId]) { this._createNewQuestionPlayerState( - questionId, question.getLinkedSkillIds()); + questionId, + question.getLinkedSkillIds() + ); } this.questionPlayerState[questionId].usedHints.push({ - timestamp: this._getCurrentTime() + timestamp: this._getCurrentTime(), }); } @@ -81,34 +83,37 @@ export class QuestionPlayerStateService { let questionId = question.getId() as string; if (!this.questionPlayerState[questionId]) { this._createNewQuestionPlayerState( - questionId, question.getLinkedSkillIds()); + questionId, + question.getLinkedSkillIds() + ); } this.questionPlayerState[questionId].viewedSolution = { - timestamp: this._getCurrentTime() + timestamp: this._getCurrentTime(), }; } answerSubmitted( - question: Question, - isCorrect: boolean, - taggedSkillMisconceptionId: string): void { + question: Question, + isCorrect: boolean, + taggedSkillMisconceptionId: string + ): void { let questionId = question.getId() as string; if (!this.questionPlayerState[questionId]) { this._createNewQuestionPlayerState( - questionId, question.getLinkedSkillIds()); + questionId, + question.getLinkedSkillIds() + ); } // Don't store a correct answer in the case where // the learner viewed the solution for this question. if (isCorrect && this.questionPlayerState[questionId].viewedSolution) { return; } - this.questionPlayerState[questionId].answers.push( - { - isCorrect: isCorrect, - timestamp: this._getCurrentTime(), - taggedSkillMisconceptionId: taggedSkillMisconceptionId - } - ); + this.questionPlayerState[questionId].answers.push({ + isCorrect: isCorrect, + timestamp: this._getCurrentTime(), + taggedSkillMisconceptionId: taggedSkillMisconceptionId, + }); } getQuestionPlayerStateData(): object { @@ -124,5 +129,9 @@ export class QuestionPlayerStateService { } } -angular.module('oppia').factory('QuestionPlayerStateService', - downgradeInjectable(QuestionPlayerStateService)); +angular + .module('oppia') + .factory( + 'QuestionPlayerStateService', + downgradeInjectable(QuestionPlayerStateService) + ); diff --git a/core/templates/components/question-directives/question-player/skill-mastery-modal.component.spec.ts b/core/templates/components/question-directives/question-player/skill-mastery-modal.component.spec.ts index 7f5d5443cd74..570a15fb54b8 100644 --- a/core/templates/components/question-directives/question-player/skill-mastery-modal.component.spec.ts +++ b/core/templates/components/question-directives/question-player/skill-mastery-modal.component.spec.ts @@ -16,10 +16,10 @@ * @fileoverview Unit tests for SkillMasteryModalController. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { SkillMasteryModalComponent } from './skill-mastery-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {SkillMasteryModalComponent} from './skill-mastery-modal.component'; class MockActiveModal { close(): void { @@ -37,16 +37,14 @@ describe('Skill Mastery Modal Controller', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - SkillMasteryModalComponent - ], + declarations: [SkillMasteryModalComponent], providers: [ { provide: NgbActiveModal, - useClass: MockActiveModal - } + useClass: MockActiveModal, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -57,33 +55,36 @@ describe('Skill Mastery Modal Controller', () => { fixture.detectChanges(); }); - it('should initialize $scope properties after controller is initialized', - () => { - spyOn(component.openConceptCardModal, 'emit').and.stub(); + it('should initialize $scope properties after controller is initialized', () => { + spyOn(component.openConceptCardModal, 'emit').and.stub(); - expect(component.userIsLoggedIn).toEqual(false); - expect(component.skillId).toEqual(''); - expect(component.masteryChange).toEqual(0); + expect(component.userIsLoggedIn).toEqual(false); + expect(component.skillId).toEqual(''); + expect(component.masteryChange).toEqual(0); - component.conceptCardModalOpen(); + component.conceptCardModalOpen(); - expect(component.openConceptCardModal.emit).toHaveBeenCalledWith(['']); - }); + expect(component.openConceptCardModal.emit).toHaveBeenCalledWith(['']); + }); - it('should open concept card with the skill id when clicking button to' + - ' open concept card', () => { - spyOn(component.openConceptCardModal, 'emit').and.stub(); + it( + 'should open concept card with the skill id when clicking button to' + + ' open concept card', + () => { + spyOn(component.openConceptCardModal, 'emit').and.stub(); - component.userIsLoggedIn = true; - component.skillId = 'skillId'; - component.masteryPerSkillMapping = { - skillId: 2 - }; + component.userIsLoggedIn = true; + component.skillId = 'skillId'; + component.masteryPerSkillMapping = { + skillId: 2, + }; - component.ngOnInit(); - component.conceptCardModalOpen(); + component.ngOnInit(); + component.conceptCardModalOpen(); - expect(component.openConceptCardModal.emit).toHaveBeenCalledWith( - ['skillId']); - }); + expect(component.openConceptCardModal.emit).toHaveBeenCalledWith([ + 'skillId', + ]); + } + ); }); diff --git a/core/templates/components/question-directives/question-player/skill-mastery-modal.component.ts b/core/templates/components/question-directives/question-player/skill-mastery-modal.component.ts index b1cdaf1c77a9..929b661c8981 100644 --- a/core/templates/components/question-directives/question-player/skill-mastery-modal.component.ts +++ b/core/templates/components/question-directives/question-player/skill-mastery-modal.component.ts @@ -16,17 +16,19 @@ * @fileoverview Component for skill mastery modal. */ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-skill-mastery-modal', - templateUrl: './skill-mastery-modal.component.html' + templateUrl: './skill-mastery-modal.component.html', }) export class SkillMasteryModalComponent - extends ConfirmOrCancelModal implements OnInit { + extends ConfirmOrCancelModal + implements OnInit +{ @Input() skillId: string = ''; @Input() userIsLoggedIn: boolean = false; @Input() masteryPerSkillMapping: { @@ -37,9 +39,7 @@ export class SkillMasteryModalComponent masteryChange: number = 0; - constructor( - private ngbActiveModal: NgbActiveModal, - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } @@ -49,13 +49,14 @@ export class SkillMasteryModalComponent ngOnInit(): void { if (this.userIsLoggedIn) { - this.masteryChange = this.masteryPerSkillMapping[( - this.skillId as string)]; + this.masteryChange = this.masteryPerSkillMapping[this.skillId as string]; } } } -angular.module('oppia').directive('oppiaSkillMasteryModal', +angular.module('oppia').directive( + 'oppiaSkillMasteryModal', downgradeComponent({ - component: SkillMasteryModalComponent - }) as angular.IDirectiveFactory); + component: SkillMasteryModalComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/components/question-directives/questions-list/questions-list.component.spec.ts b/core/templates/components/question-directives/questions-list/questions-list.component.spec.ts index 1d536b9d34c8..d98fcd5ba12e 100644 --- a/core/templates/components/question-directives/questions-list/questions-list.component.spec.ts +++ b/core/templates/components/question-directives/questions-list/questions-list.component.spec.ts @@ -16,29 +16,39 @@ * @fileoverview Unit test for Questions List Component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { QuestionUndoRedoService } from 'domain/editor/undo_redo/question-undo-redo.service'; -import { EditableQuestionBackendApiService, FetchQuestionResponse, SkillLinkageModificationsArray } from 'domain/question/editable-question-backend-api.service'; -import { QuestionSummary } from 'domain/question/question-summary-object.model'; -import { QuestionObjectFactory } from 'domain/question/QuestionObjectFactory'; -import { MisconceptionObjectFactory } from 'domain/skill/MisconceptionObjectFactory'; -import { ShortSkillSummary } from 'domain/skill/short-skill-summary.model'; -import { SkillBackendApiService } from 'domain/skill/skill-backend-api.service'; -import { SkillDifficulty } from 'domain/skill/skill-difficulty.model'; -import { SkillObjectFactory } from 'domain/skill/SkillObjectFactory'; -import { State } from 'domain/state/StateObjectFactory'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { SkillEditorRoutingService } from 'pages/skill-editor-page/services/skill-editor-routing.service'; -import { AlertsService } from 'services/alerts.service'; -import { ContextService } from 'services/context.service'; -import { LoggerService } from 'services/contextual/logger.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { QuestionValidationService } from 'services/question-validation.service'; -import { QuestionsListService } from 'services/questions-list.service'; -import { QuestionsListComponent } from './questions-list.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {QuestionUndoRedoService} from 'domain/editor/undo_redo/question-undo-redo.service'; +import { + EditableQuestionBackendApiService, + FetchQuestionResponse, + SkillLinkageModificationsArray, +} from 'domain/question/editable-question-backend-api.service'; +import {QuestionSummary} from 'domain/question/question-summary-object.model'; +import {QuestionObjectFactory} from 'domain/question/QuestionObjectFactory'; +import {MisconceptionObjectFactory} from 'domain/skill/MisconceptionObjectFactory'; +import {ShortSkillSummary} from 'domain/skill/short-skill-summary.model'; +import {SkillBackendApiService} from 'domain/skill/skill-backend-api.service'; +import {SkillDifficulty} from 'domain/skill/skill-difficulty.model'; +import {SkillObjectFactory} from 'domain/skill/SkillObjectFactory'; +import {State} from 'domain/state/StateObjectFactory'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {SkillEditorRoutingService} from 'pages/skill-editor-page/services/skill-editor-routing.service'; +import {AlertsService} from 'services/alerts.service'; +import {ContextService} from 'services/context.service'; +import {LoggerService} from 'services/contextual/logger.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {QuestionValidationService} from 'services/question-validation.service'; +import {QuestionsListService} from 'services/questions-list.service'; +import {QuestionsListComponent} from './questions-list.component'; class MockNgbModalRef { componentInstance = { @@ -46,14 +56,14 @@ class MockNgbModalRef { skillsInSameTopicCount: null, categorizedSkills: null, allowSkillsFromOtherTopics: null, - untriagedSkillSummaries: null + untriagedSkillSummaries: null, }; } class MockNgbModal { open() { return { - result: Promise.resolve() + result: Promise.resolve(), }; } } @@ -88,13 +98,11 @@ describe('Questions List Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - QuestionsListComponent - ], + declarations: [QuestionsListComponent], providers: [ { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, WindowDimensionsService, QuestionsListService, @@ -106,12 +114,12 @@ describe('Questions List Component', () => { QuestionUndoRedoService, { provide: UrlInterpolationService, - useClass: MockUrlInterpolationService + useClass: MockUrlInterpolationService, }, ContextService, QuestionValidationService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -129,8 +137,9 @@ describe('Questions List Component', () => { skillBackendApiService = TestBed.inject(SkillBackendApiService); alertsService = TestBed.inject(AlertsService); questionObjectFactory = TestBed.inject(QuestionObjectFactory); - editableQuestionBackendApiService = ( - TestBed.inject(EditableQuestionBackendApiService)); + editableQuestionBackendApiService = TestBed.inject( + EditableQuestionBackendApiService + ); questionUndoRedoService = TestBed.inject(QuestionUndoRedoService); loggerService = TestBed.inject(LoggerService); contextService = TestBed.inject(ContextService); @@ -141,70 +150,74 @@ describe('Questions List Component', () => { question_state_data: { content: { html: 'Question 1', - content_id: 'content_1' + content_id: 'content_1', }, interaction: { - answer_groups: [{ - outcome: { - dest: 'outcome 1', - dest_if_really_stuck: null, - feedback: { - content_id: 'content_5', - html: '' + answer_groups: [ + { + outcome: { + dest: 'outcome 1', + dest_if_really_stuck: null, + feedback: { + content_id: 'content_5', + html: '', + }, + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, }, - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, + rule_specs: [], + training_data: null, + tagged_skill_misconception_id: null, }, - rule_specs: [], - training_data: null, - tagged_skill_misconception_id: null - }], + ], confirmed_unclassified_answers: [], customization_args: { placeholder: { value: { content_id: 'ca_placeholder_0', - unicode_str: '' - } + unicode_str: '', + }, }, - rows: { value: 1 }, + rows: {value: 1}, catchMisspellings: { - value: false - } + value: false, + }, }, default_outcome: { dest: null, dest_if_really_stuck: null, feedback: { html: 'Correct Answer', - content_id: 'content_2' + content_id: 'content_2', }, param_changes: [], labelled_as_correct: true, missing_prerequisite_skill_id: null, - refresher_exploration_id: null + refresher_exploration_id: null, }, - hints: [{ - hint_content: { - html: 'Hint 1', - content_id: 'content_3' - } - }], + hints: [ + { + hint_content: { + html: 'Hint 1', + content_id: 'content_3', + }, + }, + ], solution: { correct_answer: 'This is the correct answer', answer_is_exclusive: false, explanation: { html: 'Solution explanation', - content_id: 'content_4' - } + content_id: 'content_4', + }, }, - id: 'TextInput' + id: 'TextInput', }, param_changes: [], recorded_voiceovers: { - voiceovers_mapping: {} + voiceovers_mapping: {}, }, classifier_model_id: null, solicit_answer_details: false, @@ -216,7 +229,7 @@ describe('Questions List Component', () => { linked_skill_ids: [], next_content_id_index: 5, question_state_data_schema_version: 44, - version: 45 + version: 45, }); questionStateData = question.getStateData(); @@ -224,13 +237,15 @@ describe('Questions List Component', () => { skill = skillObjectFactory.createFromBackendDict({ id: 'skillId1', description: 'test description 1', - misconceptions: [{ - id: 2, - name: 'test name', - notes: 'test notes', - feedback: 'test feedback', - must_be_addressed: true - }], + misconceptions: [ + { + id: 2, + name: 'test name', + notes: 'test notes', + feedback: 'test feedback', + must_be_addressed: true, + }, + ], rubrics: [], skill_contents: { explanation: { @@ -239,82 +254,104 @@ describe('Questions List Component', () => { }, worked_examples: [], recorded_voiceovers: { - voiceovers_mapping: {} - } + voiceovers_mapping: {}, + }, }, language_code: 'en', version: 3, prerequisite_skill_ids: [], all_questions_merged: null, next_misconception_id: null, - superseding_skill_id: null + superseding_skill_id: null, }); component.selectedSkillId = 'skillId1'; }); - it('should subscribe to question summaries init event on' + - ' component initialization', () => { - spyOn(questionsListService.onQuestionSummariesInitialized, 'subscribe'); - - component.ngOnInit(); - - expect(questionsListService.onQuestionSummariesInitialized.subscribe) - .toHaveBeenCalled(); - }); + it( + 'should subscribe to question summaries init event on' + + ' component initialization', + () => { + spyOn(questionsListService.onQuestionSummariesInitialized, 'subscribe'); - it('should reset history and fetch question summaries on' + - ' initialization', () => { - let resetHistoryAndFetch = true; - spyOn(questionsListService, 'getQuestionSummariesAsync'); + component.ngOnInit(); + expect( + questionsListService.onQuestionSummariesInitialized.subscribe + ).toHaveBeenCalled(); + } + ); - component.ngOnInit(); + it( + 'should reset history and fetch question summaries on' + ' initialization', + () => { + let resetHistoryAndFetch = true; + spyOn(questionsListService, 'getQuestionSummariesAsync'); - expect(questionsListService.getQuestionSummariesAsync).toHaveBeenCalledWith( - 'skillId1', resetHistoryAndFetch, resetHistoryAndFetch - ); - }); + component.ngOnInit(); - it('should not reset history and fetch question summaries when question' + - ' summaries are initialized', () => { - let resetHistoryAndFetch = false; - let questionSummariesInitializedEmitter = new EventEmitter(); - spyOnProperty(questionsListService, 'onQuestionSummariesInitialized') - .and.returnValue(questionSummariesInitializedEmitter); - spyOn(questionsListService, 'getQuestionSummariesAsync'); + expect( + questionsListService.getQuestionSummariesAsync + ).toHaveBeenCalledWith( + 'skillId1', + resetHistoryAndFetch, + resetHistoryAndFetch + ); + } + ); - component.ngOnInit(); + it( + 'should not reset history and fetch question summaries when question' + + ' summaries are initialized', + () => { + let resetHistoryAndFetch = false; + let questionSummariesInitializedEmitter = new EventEmitter(); + spyOnProperty( + questionsListService, + 'onQuestionSummariesInitialized' + ).and.returnValue(questionSummariesInitializedEmitter); + spyOn(questionsListService, 'getQuestionSummariesAsync'); - questionSummariesInitializedEmitter.emit(); + component.ngOnInit(); - expect(questionsListService.getQuestionSummariesAsync).toHaveBeenCalledWith( - 'skillId1', resetHistoryAndFetch, resetHistoryAndFetch - ); - }); + questionSummariesInitializedEmitter.emit(); - it('should fetch misconception ids for selected skill on' + - ' initialization', fakeAsync(() => { - component.selectedSkillId = 'true'; - spyOn(skillBackendApiService, 'fetchSkillAsync').and.returnValue( - Promise.resolve({ - skill: skill, - assignedSkillTopicData: {}, - groupedSkillSummaries: {} - })); + expect( + questionsListService.getQuestionSummariesAsync + ).toHaveBeenCalledWith( + 'skillId1', + resetHistoryAndFetch, + resetHistoryAndFetch + ); + } + ); - expect(component.misconceptionIdsForSelectedSkill).toEqual(undefined); + it( + 'should fetch misconception ids for selected skill on' + ' initialization', + fakeAsync(() => { + component.selectedSkillId = 'true'; + spyOn(skillBackendApiService, 'fetchSkillAsync').and.returnValue( + Promise.resolve({ + skill: skill, + assignedSkillTopicData: {}, + groupedSkillSummaries: {}, + }) + ); - component.ngOnInit(); - tick(); + expect(component.misconceptionIdsForSelectedSkill).toEqual(undefined); + component.ngOnInit(); + tick(); - expect(component.misconceptionIdsForSelectedSkill).toEqual([2]); - })); + expect(component.misconceptionIdsForSelectedSkill).toEqual([2]); + }) + ); it('should start creating question on navigating to question editor', () => { - spyOn(skillEditorRoutingService, 'navigateToQuestionEditor') - .and.returnValue(true); + spyOn( + skillEditorRoutingService, + 'navigateToQuestionEditor' + ).and.returnValue(true); spyOn(component, 'createQuestion').and.stub(); component.ngOnInit(); @@ -322,17 +359,16 @@ describe('Questions List Component', () => { expect(component.createQuestion).toHaveBeenCalled(); }); - it('should not start creating a question if there are alerts', fakeAsync( - () => { - alertsService.addWarning('a warning'); - spyOn(loggerService, 'error').and.stub(); + it('should not start creating a question if there are alerts', fakeAsync(() => { + alertsService.addWarning('a warning'); + spyOn(loggerService, 'error').and.stub(); - component.createQuestion(); + component.createQuestion(); - expect(loggerService.error).toHaveBeenCalledWith( - 'Could not create new question due to warnings: a warning'); - } - )); + expect(loggerService.error).toHaveBeenCalledWith( + 'Could not create new question due to warnings: a warning' + ); + })); it('should get selected skill id when a question is created', () => { // When modal is not shown, then newQuestionSkillIds get the values of @@ -350,60 +386,61 @@ describe('Questions List Component', () => { expect(component.newQuestionSkillIds).toEqual(['skillId1']); }); - it('should populate misconceptions when a question is created', - fakeAsync(() => { - const skill = skillObjectFactory.createFromBackendDict({ - id: 'skillId1', - description: 'test description 1', - misconceptions: [{ + it('should populate misconceptions when a question is created', fakeAsync(() => { + const skill = skillObjectFactory.createFromBackendDict({ + id: 'skillId1', + description: 'test description 1', + misconceptions: [ + { id: 2, name: 'test name', notes: 'test notes', feedback: 'test feedback', - must_be_addressed: true - }], - rubrics: [], - skill_contents: { - explanation: { - html: 'test explanation', - content_id: 'explanation', - }, - worked_examples: [], - recorded_voiceovers: { - voiceovers_mapping: {} - } + must_be_addressed: true, }, - language_code: 'en', - version: 3, - prerequisite_skill_ids: [], - all_questions_merged: null, - next_misconception_id: null, - superseding_skill_id: null - }); - spyOn(skillBackendApiService, 'fetchMultiSkillsAsync').and.returnValue( - Promise.resolve([skill]) - ); - component.linkedSkillsWithDifficulty = [ - SkillDifficulty.create('skillId1', '', 1) - ]; + ], + rubrics: [], + skill_contents: { + explanation: { + html: 'test explanation', + content_id: 'explanation', + }, + worked_examples: [], + recorded_voiceovers: { + voiceovers_mapping: {}, + }, + }, + language_code: 'en', + version: 3, + prerequisite_skill_ids: [], + all_questions_merged: null, + next_misconception_id: null, + superseding_skill_id: null, + }); + spyOn(skillBackendApiService, 'fetchMultiSkillsAsync').and.returnValue( + Promise.resolve([skill]) + ); + component.linkedSkillsWithDifficulty = [ + SkillDifficulty.create('skillId1', '', 1), + ]; - expect(component.misconceptionsBySkill).toEqual(undefined); + expect(component.misconceptionsBySkill).toEqual(undefined); - component.createQuestion(); - tick(); + component.createQuestion(); + tick(); - expect(component.misconceptionsBySkill).toEqual({ - skillId1: [ - misconceptionObjectFactory.createFromBackendDict({ - id: 2, - name: 'test name', - notes: 'test notes', - feedback: 'test feedback', - must_be_addressed: true - }) - ] - }); - })); + expect(component.misconceptionsBySkill).toEqual({ + skillId1: [ + misconceptionObjectFactory.createFromBackendDict({ + id: 2, + name: 'test name', + notes: 'test notes', + feedback: 'test feedback', + must_be_addressed: true, + }), + ], + }); + })); it('should show warning message if fetching skills fails', () => { spyOn(alertsService, 'addWarning'); @@ -433,7 +470,9 @@ describe('Questions List Component', () => { expect(questionsListService.incrementPageNumber).toHaveBeenCalled(); expect(questionsListService.getQuestionSummariesAsync).toHaveBeenCalledWith( - 'skillId1', true, false + 'skillId1', + true, + false ); }); @@ -446,29 +485,38 @@ describe('Questions List Component', () => { expect(questionsListService.decrementPageNumber).toHaveBeenCalled(); expect(questionsListService.getQuestionSummariesAsync).toHaveBeenCalledWith( - 'skillId1', false, false + 'skillId1', + false, + false ); }); - it('should check if warning is to be shown for unaddressed skill' + - ' misconceptions', () => { - // The selected skill id is skillId1. - component.misconceptionIdsForSelectedSkill = [1, 2]; - - expect(component.showUnaddressedSkillMisconceptionWarning([ - 'skillId1-1', - 'skillId1-2', - ])).toBe(true); - expect(component.showUnaddressedSkillMisconceptionWarning([ - 'skillId1-1', - 'skillId2-2', - ])).toBe(false); - }); - + it( + 'should check if warning is to be shown for unaddressed skill' + + ' misconceptions', + () => { + // The selected skill id is skillId1. + component.misconceptionIdsForSelectedSkill = [1, 2]; + + expect( + component.showUnaddressedSkillMisconceptionWarning([ + 'skillId1-1', + 'skillId1-2', + ]) + ).toBe(true); + expect( + component.showUnaddressedSkillMisconceptionWarning([ + 'skillId1-1', + 'skillId2-2', + ]) + ).toBe(false); + } + ); - it('should get skill editor\'s URL', () => { - expect( - component.getSkillEditorUrl('skillId1')).toBe('/skill_editor/skillId1'); + it("should get skill editor's URL", () => { + expect(component.getSkillEditorUrl('skillId1')).toBe( + '/skill_editor/skillId1' + ); }); it('should check if current page is the last one', () => { @@ -477,200 +525,260 @@ describe('Questions List Component', () => { expect(component.isLastPage()).toBe(true); }); - it('should not save and publish question if there are' + - ' validation errors', () => { - component.question = question; - spyOn(alertsService, 'addWarning'); - spyOn(questionValidationService, 'getValidationErrorMessage') - .and.returnValue('Error'); - spyOn(component.question, 'getUnaddressedMisconceptionNames') - .and.returnValue(['misconception1', 'misconception2']); + it( + 'should not save and publish question if there are' + ' validation errors', + () => { + component.question = question; + spyOn(alertsService, 'addWarning'); + spyOn( + questionValidationService, + 'getValidationErrorMessage' + ).and.returnValue('Error'); + spyOn( + component.question, + 'getUnaddressedMisconceptionNames' + ).and.returnValue(['misconception1', 'misconception2']); - component.saveAndPublishQuestion('Commit'); + component.saveAndPublishQuestion('Commit'); - expect(alertsService.addWarning).toHaveBeenCalledWith('Error'); - }); + expect(alertsService.addWarning).toHaveBeenCalledWith('Error'); + } + ); - it('should show an error and not save question if there are' + - ' errors from question backend api service', fakeAsync(() => { - component.question = question; - component.questionIsBeingUpdated = false; - spyOn(editableQuestionBackendApiService, 'createQuestionAsync') - .and.returnValue(Promise.reject('Error')); - spyOn(component.question, 'getUnaddressedMisconceptionNames') - .and.returnValue([]); - spyOn(alertsService, 'addWarning'); + it( + 'should show an error and not save question if there are' + + ' errors from question backend api service', + fakeAsync(() => { + component.question = question; + component.questionIsBeingUpdated = false; + spyOn( + editableQuestionBackendApiService, + 'createQuestionAsync' + ).and.returnValue(Promise.reject('Error')); + spyOn( + component.question, + 'getUnaddressedMisconceptionNames' + ).and.returnValue([]); + spyOn(alertsService, 'addWarning'); - component.saveAndPublishQuestion(null); - tick(); + component.saveAndPublishQuestion(null); + tick(); - expect(alertsService.addWarning).toHaveBeenCalledWith('Error'); - })); + expect(alertsService.addWarning).toHaveBeenCalledWith('Error'); + }) + ); - it('should create new question in the backend if there are no validation' + - ' error on saving and publishing a question when question is not already' + - ' being updated', fakeAsync(() => { - component.question = question; - component.questionIsBeingUpdated = false; - component.skillLinkageModificationsArray = ( - [ + it( + 'should create new question in the backend if there are no validation' + + ' error on saving and publishing a question when question is not already' + + ' being updated', + fakeAsync(() => { + component.question = question; + component.questionIsBeingUpdated = false; + component.skillLinkageModificationsArray = [ { id: '1', task: null, - difficulty: 1 + difficulty: 1, }, { id: '2', task: null, - difficulty: 2 + difficulty: 2, }, { id: '1', task: null, - difficulty: 1 - }]); - - spyOn(questionValidationService, 'getValidationErrorMessage') - .and.returnValue(''); - spyOn(component.question, 'getUnaddressedMisconceptionNames') - .and.returnValue([]); - spyOn(editableQuestionBackendApiService, 'createQuestionAsync') - .and.returnValue(Promise.resolve({ - questionId: 'qId' - })); - spyOn(editableQuestionBackendApiService, 'editQuestionSkillLinksAsync'); - - component.saveAndPublishQuestion('Commit'); - tick(); - - expect(editableQuestionBackendApiService.editQuestionSkillLinksAsync) - .toHaveBeenCalledWith( - 'qId', - [ - { - id: '1', - task: null, - difficulty: 1 - }, - { - id: '2', - task: null, - difficulty: 2 - }, - { - id: '1', - task: null, - difficulty: 1 - }]); - })); - - it('should save question when another question is being updated', - fakeAsync(() => { - component.question = question; - component.questionIsBeingUpdated = true; + difficulty: 1, + }, + ]; - spyOn(questionValidationService, 'getValidationErrorMessage') - .and.returnValue(''); - spyOn(component.question, 'getUnaddressedMisconceptionNames') - .and.returnValue([]); - spyOn(questionUndoRedoService, 'hasChanges').and.returnValue(true); - spyOn(editableQuestionBackendApiService, 'updateQuestionAsync') - .and.returnValue(Promise.resolve(null)); - spyOn(questionUndoRedoService, 'clearChanges'); - spyOn(questionsListService, 'getQuestionSummariesAsync'); + spyOn( + questionValidationService, + 'getValidationErrorMessage' + ).and.returnValue(''); + spyOn( + component.question, + 'getUnaddressedMisconceptionNames' + ).and.returnValue([]); + spyOn( + editableQuestionBackendApiService, + 'createQuestionAsync' + ).and.returnValue( + Promise.resolve({ + questionId: 'qId', + }) + ); + spyOn(editableQuestionBackendApiService, 'editQuestionSkillLinksAsync'); component.saveAndPublishQuestion('Commit'); tick(); - expect(questionUndoRedoService.clearChanges).toHaveBeenCalled(); - expect(questionsListService.getQuestionSummariesAsync) - .toHaveBeenCalledWith('skillId1', true, true); - })); + expect( + editableQuestionBackendApiService.editQuestionSkillLinksAsync + ).toHaveBeenCalledWith('qId', [ + { + id: '1', + task: null, + difficulty: 1, + }, + { + id: '2', + task: null, + difficulty: 2, + }, + { + id: '1', + task: null, + difficulty: 1, + }, + ]); + }) + ); - it('should show error if saving question fails when another question' + - ' is being updated', fakeAsync(() => { + it('should save question when another question is being updated', fakeAsync(() => { component.question = question; component.questionIsBeingUpdated = true; - spyOn(questionValidationService, 'getValidationErrorMessage') - .and.returnValue(''); - spyOn(component.question, 'getUnaddressedMisconceptionNames') - .and.returnValue([]); + + spyOn( + questionValidationService, + 'getValidationErrorMessage' + ).and.returnValue(''); + spyOn( + component.question, + 'getUnaddressedMisconceptionNames' + ).and.returnValue([]); spyOn(questionUndoRedoService, 'hasChanges').and.returnValue(true); - spyOn(questionUndoRedoService, 'getCommittableChangeList'); - spyOn(editableQuestionBackendApiService, 'updateQuestionAsync') - .and.returnValue(Promise.reject()); + spyOn( + editableQuestionBackendApiService, + 'updateQuestionAsync' + ).and.returnValue(Promise.resolve(null)); spyOn(questionUndoRedoService, 'clearChanges'); spyOn(questionsListService, 'getQuestionSummariesAsync'); - spyOn(alertsService, 'addWarning'); component.saveAndPublishQuestion('Commit'); tick(); - expect(questionUndoRedoService.clearChanges).not.toHaveBeenCalled(); - expect(questionsListService.getQuestionSummariesAsync) - .not.toHaveBeenCalled(); - expect(alertsService.addWarning).toHaveBeenCalledWith( - 'There was an error saving the question.'); + expect(questionUndoRedoService.clearChanges).toHaveBeenCalled(); + expect(questionsListService.getQuestionSummariesAsync).toHaveBeenCalledWith( + 'skillId1', + true, + true + ); })); - it('should display warning if commit message is not given while saving' + - ' a question', fakeAsync(() => { - component.question = question; - component.questionIsBeingUpdated = true; - spyOn(questionValidationService, 'getValidationErrorMessage') - .and.returnValue(''); - spyOn(component.question, 'getUnaddressedMisconceptionNames') - .and.returnValue([]); - spyOn(questionUndoRedoService, 'hasChanges').and.returnValue(true); - spyOn(alertsService, 'addWarning'); + it( + 'should show error if saving question fails when another question' + + ' is being updated', + fakeAsync(() => { + component.question = question; + component.questionIsBeingUpdated = true; + spyOn( + questionValidationService, + 'getValidationErrorMessage' + ).and.returnValue(''); + spyOn( + component.question, + 'getUnaddressedMisconceptionNames' + ).and.returnValue([]); + spyOn(questionUndoRedoService, 'hasChanges').and.returnValue(true); + spyOn(questionUndoRedoService, 'getCommittableChangeList'); + spyOn( + editableQuestionBackendApiService, + 'updateQuestionAsync' + ).and.returnValue(Promise.reject()); + spyOn(questionUndoRedoService, 'clearChanges'); + spyOn(questionsListService, 'getQuestionSummariesAsync'); + spyOn(alertsService, 'addWarning'); - component.saveAndPublishQuestion(null); - tick(); + component.saveAndPublishQuestion('Commit'); + tick(); - expect(alertsService.addWarning) - .toHaveBeenCalledWith('Please provide a valid commit message.'); - })); + expect(questionUndoRedoService.clearChanges).not.toHaveBeenCalled(); + expect( + questionsListService.getQuestionSummariesAsync + ).not.toHaveBeenCalled(); + expect(alertsService.addWarning).toHaveBeenCalledWith( + 'There was an error saving the question.' + ); + }) + ); - it('should show \'confirm question modal exit\' modal when user ' + - 'clicks cancel', fakeAsync(() => { - spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ - result: Promise.resolve() - } as NgbModalRef); - }); + it( + 'should display warning if commit message is not given while saving' + + ' a question', + fakeAsync(() => { + component.question = question; + component.questionIsBeingUpdated = true; + spyOn( + questionValidationService, + 'getValidationErrorMessage' + ).and.returnValue(''); + spyOn( + component.question, + 'getUnaddressedMisconceptionNames' + ).and.returnValue([]); + spyOn(questionUndoRedoService, 'hasChanges').and.returnValue(true); + spyOn(alertsService, 'addWarning'); - component.cancel(); - tick(); + component.saveAndPublishQuestion(null); + tick(); - expect(ngbModal.open).toHaveBeenCalled(); - })); + expect(alertsService.addWarning).toHaveBeenCalledWith( + 'Please provide a valid commit message.' + ); + }) + ); - it('should reset image save destination when user clicks confirm on' + - ' \'confirm question modal exit\' modal', fakeAsync(() => { - spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.resolve('confirm') - } as NgbModalRef); - spyOn(contextService, 'resetImageSaveDestination').and.stub(); + it( + "should show 'confirm question modal exit' modal when user " + + 'clicks cancel', + fakeAsync(() => { + spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + result: Promise.resolve(), + } as NgbModalRef; + }); - component.cancel(); - tick(); + component.cancel(); + tick(); - expect(contextService.resetImageSaveDestination).toHaveBeenCalled(); - })); + expect(ngbModal.open).toHaveBeenCalled(); + }) + ); - it('should close \'confirm question modal exit\' modal when user clicks' + - ' cancel', fakeAsync(() => { - spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ - result: Promise.reject() + it( + 'should reset image save destination when user clicks confirm on' + + " 'confirm question modal exit' modal", + fakeAsync(() => { + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.resolve('confirm'), } as NgbModalRef); - }); + spyOn(contextService, 'resetImageSaveDestination').and.stub(); - component.cancel(); - tick(); + component.cancel(); + tick(); - expect(ngbModal.open).toHaveBeenCalled(); - })); + expect(contextService.resetImageSaveDestination).toHaveBeenCalled(); + }) + ); + + it( + "should close 'confirm question modal exit' modal when user clicks" + + ' cancel', + fakeAsync(() => { + spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + result: Promise.reject(), + } as NgbModalRef; + }); + + component.cancel(); + tick(); + + expect(ngbModal.open).toHaveBeenCalled(); + }) + ); it('should update skill difficulty when user selects a difficulty', () => { let skill = SkillDifficulty.create('skillId1', '', 0.9); @@ -686,8 +794,8 @@ describe('Questions List Component', () => { { id: 'skillId1', task: 'update_difficulty', - difficulty: 0.9 - } + difficulty: 0.9, + }, ]); component.newQuestionSkillIds = []; @@ -697,26 +805,24 @@ describe('Questions List Component', () => { component.updateSkillWithDifficulty(skill, 0); - expect(component.newQuestionSkillIds).toEqual( - ['skillId1']); + expect(component.newQuestionSkillIds).toEqual(['skillId1']); expect(component.newQuestionSkillDifficulties).toEqual([0.9]); expect(component.skillLinkageModificationsArray).toEqual([ { id: 'skillId1', task: 'update_difficulty', - difficulty: 0.9 - } + difficulty: 0.9, + }, ]); }); describe('when user clicks on edit question', () => { - let questionSummaryForOneSkill = QuestionSummary - .createFromBackendDict({ - id: 'qId', - interaction_id: '', - misconception_ids: [], - question_content: '' - }); + let questionSummaryForOneSkill = QuestionSummary.createFromBackendDict({ + id: 'qId', + interaction_id: '', + misconception_ids: [], + question_content: '', + }); let skillDescription = 'Skill Description'; let difficulty: 0.9; @@ -726,166 +832,191 @@ describe('Questions List Component', () => { expect(component.editQuestion(null, null, null)).toBe(undefined); }); - it('should warning if user does not have rights to delete a' + - ' question', () => { - component.canEditQuestion = (false); - spyOn(alertsService, 'addWarning'); - - component.editQuestion(null, null, null); - - expect(alertsService.addWarning).toHaveBeenCalledWith( - 'User does not have enough rights to edit the question'); - }); + it( + 'should warning if user does not have rights to delete a' + ' question', + () => { + component.canEditQuestion = false; + spyOn(alertsService, 'addWarning'); - it('should fetch question data from backend and set new ' + - 'question\'s properties', fakeAsync(() => { - component.editorIsOpen = false; - component.canEditQuestion = true; - component.selectSkillModalIsShown = true; - - spyOn(editableQuestionBackendApiService, 'fetchQuestionAsync') - .and.returnValue(Promise.resolve({ - associated_skill_dicts: [{ - id: 'skillId1', - misconceptions: [{ - id: 1, - feedback: '', - must_be_addressed: false, - notes: '', - name: 'MIsconception 1' - }], - description: '' - }], - questionObject: question - } as FetchQuestionResponse)); + component.editQuestion(null, null, null); - component.editQuestion( - questionSummaryForOneSkill, skillDescription, difficulty); - tick(); - - expect(component.question).toEqual(question); - expect(component.questionId).toBe('1'); - expect(component.questionStateData).toEqual(questionStateData); - })); + expect(alertsService.addWarning).toHaveBeenCalledWith( + 'User does not have enough rights to edit the question' + ); + } + ); - it('should display warning if fetching from backend fails', + it( + 'should fetch question data from backend and set new ' + + "question's properties", fakeAsync(() => { component.editorIsOpen = false; - component.canEditQuestion = true; - component.selectSkillModalIsShown = (false); - spyOn(editableQuestionBackendApiService, 'fetchQuestionAsync') - .and.returnValue(Promise.reject({ - error: 'Failed to fetch question.' - })); - spyOn(alertsService, 'addWarning'); + component.selectSkillModalIsShown = true; + + spyOn( + editableQuestionBackendApiService, + 'fetchQuestionAsync' + ).and.returnValue( + Promise.resolve({ + associated_skill_dicts: [ + { + id: 'skillId1', + misconceptions: [ + { + id: 1, + feedback: '', + must_be_addressed: false, + notes: '', + name: 'MIsconception 1', + }, + ], + description: '', + }, + ], + questionObject: question, + } as FetchQuestionResponse) + ); component.editQuestion( - questionSummaryForOneSkill, skillDescription, difficulty); + questionSummaryForOneSkill, + skillDescription, + difficulty + ); tick(); - expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Failed to fetch question.' - ); - })); - }); + expect(component.question).toEqual(question); + expect(component.questionId).toBe('1'); + expect(component.questionStateData).toEqual(questionStateData); + }) + ); - it('should save image destination to local storage if question editor is' + - ' opened while a question is already being created', () => { - component.newQuestionIsBeingCreated = true; - spyOn(contextService, 'setImageSaveDestinationToLocalStorage'); + it('should display warning if fetching from backend fails', fakeAsync(() => { + component.editorIsOpen = false; - component.openQuestionEditor(); + component.canEditQuestion = true; + component.selectSkillModalIsShown = false; + spyOn( + editableQuestionBackendApiService, + 'fetchQuestionAsync' + ).and.returnValue( + Promise.reject({ + error: 'Failed to fetch question.', + }) + ); + spyOn(alertsService, 'addWarning'); - expect(contextService.setImageSaveDestinationToLocalStorage) - .toHaveBeenCalled(); + component.editQuestion( + questionSummaryForOneSkill, + skillDescription, + difficulty + ); + tick(); + + expect(alertsService.addWarning).toHaveBeenCalledWith( + 'Failed to fetch question.' + ); + })); }); + it( + 'should save image destination to local storage if question editor is' + + ' opened while a question is already being created', + () => { + component.newQuestionIsBeingCreated = true; + spyOn(contextService, 'setImageSaveDestinationToLocalStorage'); + + component.openQuestionEditor(); + + expect( + contextService.setImageSaveDestinationToLocalStorage + ).toHaveBeenCalled(); + } + ); + describe('when removing question from skill', () => { let questionId = 'qId'; - it('should remove question when user is in the skill editor', - fakeAsync(() => { - component.selectedSkillId = 'skillId1'; - component.deletedQuestionIds = []; - spyOn(alertsService, 'addSuccessMessage'); - spyOn(ngbModal, 'open').and.returnValue( - { - componentInstance: { - skillId: 'skillId', - canEditQuestion: true, - numberOfQuestions: 3 - }, - result: Promise.resolve() - } as NgbModalRef - ); - component.allSkillSummaries = []; - spyOn(editableQuestionBackendApiService, 'editQuestionSkillLinksAsync') - .and.returnValue(Promise.resolve()); + it('should remove question when user is in the skill editor', fakeAsync(() => { + component.selectedSkillId = 'skillId1'; + component.deletedQuestionIds = []; + spyOn(alertsService, 'addSuccessMessage'); + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: { + skillId: 'skillId', + canEditQuestion: true, + numberOfQuestions: 3, + }, + result: Promise.resolve(), + } as NgbModalRef); + component.allSkillSummaries = []; + spyOn( + editableQuestionBackendApiService, + 'editQuestionSkillLinksAsync' + ).and.returnValue(Promise.resolve()); - component.removeQuestionFromSkill(questionId); - tick(); + component.removeQuestionFromSkill(questionId); + tick(); - expect(ngbModal.open).toHaveBeenCalled(); - expect(alertsService.addSuccessMessage).toHaveBeenCalledWith( - 'Question Removed' - ); - })); + expect(ngbModal.open).toHaveBeenCalled(); + expect(alertsService.addSuccessMessage).toHaveBeenCalledWith( + 'Question Removed' + ); + })); - it('should remove question when user is not in the skill editor', - fakeAsync(() => { - component.selectedSkillId = 'skillId1'; - component.deletedQuestionIds = []; - spyOn(alertsService, 'addSuccessMessage'); - spyOn(ngbModal, 'open').and.returnValue( - { - componentInstance: { - skillId: 'skillId', - canEditQuestion: true, - numberOfQuestions: 3 - }, - result: Promise.resolve() - } as NgbModalRef - ); - component.allSkillSummaries = ([ - ShortSkillSummary.createFromBackendDict({ - skill_id: '1', - skill_description: 'Skill Description' - }) - ]); - spyOn(editableQuestionBackendApiService, 'editQuestionSkillLinksAsync') - .and.returnValue(Promise.resolve()); - - component.removeQuestionFromSkill(questionId); - tick(); + it('should remove question when user is not in the skill editor', fakeAsync(() => { + component.selectedSkillId = 'skillId1'; + component.deletedQuestionIds = []; + spyOn(alertsService, 'addSuccessMessage'); + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: { + skillId: 'skillId', + canEditQuestion: true, + numberOfQuestions: 3, + }, + result: Promise.resolve(), + } as NgbModalRef); + component.allSkillSummaries = [ + ShortSkillSummary.createFromBackendDict({ + skill_id: '1', + skill_description: 'Skill Description', + }), + ]; + spyOn( + editableQuestionBackendApiService, + 'editQuestionSkillLinksAsync' + ).and.returnValue(Promise.resolve()); - expect(ngbModal.open).toHaveBeenCalled(); - expect(alertsService.addSuccessMessage).toHaveBeenCalledWith( - 'Question Removed' - ); - })); + component.removeQuestionFromSkill(questionId); + tick(); + + expect(ngbModal.open).toHaveBeenCalled(); + expect(alertsService.addSuccessMessage).toHaveBeenCalledWith( + 'Question Removed' + ); + })); it('should cancel remove question modal', fakeAsync(() => { component.deletedQuestionIds = []; spyOn(alertsService, 'addInfoMessage'); - component.allSkillSummaries = ([ + component.allSkillSummaries = [ ShortSkillSummary.createFromBackendDict({ skill_id: '1', - skill_description: 'Skill Description' - }) - ]); - spyOn(ngbModal, 'open').and.returnValue( - { - componentInstance: { - skillId: 'skillId', - canEditQuestion: true, - numberOfQuestions: 3 - }, - result: Promise.reject() - } as NgbModalRef - ); - spyOn(editableQuestionBackendApiService, 'editQuestionSkillLinksAsync') - .and.returnValue(Promise.resolve()); + skill_description: 'Skill Description', + }), + ]; + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: { + skillId: 'skillId', + canEditQuestion: true, + numberOfQuestions: 3, + }, + result: Promise.reject(), + } as NgbModalRef); + spyOn( + editableQuestionBackendApiService, + 'editQuestionSkillLinksAsync' + ).and.returnValue(Promise.resolve()); spyOn(component, 'removeQuestionSkillLinkAsync'); component.removeQuestionFromSkill(questionId); @@ -899,27 +1030,28 @@ describe('Questions List Component', () => { component.associatedSkillSummaries = [ ShortSkillSummary.createFromBackendDict({ skill_id: '1', - skill_description: 'Skill Description' - }) + skill_description: 'Skill Description', + }), ]; spyOn(alertsService, 'addInfoMessage'); component.removeSkill(null); expect(alertsService.addInfoMessage).toHaveBeenCalledWith( - 'A question should be linked to at least one skill.'); + 'A question should be linked to at least one skill.' + ); }); it('should remove skill linked to a question', () => { component.associatedSkillSummaries = [ ShortSkillSummary.createFromBackendDict({ skill_id: '1', - skill_description: 'Skill Description' + skill_description: 'Skill Description', }), ShortSkillSummary.createFromBackendDict({ skill_id: '2', - skill_description: 'Skill Description' - }) + skill_description: 'Skill Description', + }), ]; component.skillLinkageModificationsArray = []; component.removeSkill('1'); @@ -927,33 +1059,37 @@ describe('Questions List Component', () => { expect(component.associatedSkillSummaries).toEqual([ ShortSkillSummary.createFromBackendDict({ skill_id: '2', - skill_description: 'Skill Description' - }) + skill_description: 'Skill Description', + }), ]); expect(component.skillLinkageModificationsArray).toEqual([ { id: '1', task: 'remove', - difficulty: component.difficulty - } as SkillLinkageModificationsArray + difficulty: component.difficulty, + } as SkillLinkageModificationsArray, ]); }); - it('should check that question is not savable if there are no' + - ' changes', () => { - component.skillLinkageModificationsArray = []; - component.isSkillDifficultyChanged = false; - spyOn(questionUndoRedoService, 'hasChanges').and.returnValue(false); + it( + 'should check that question is not savable if there are no' + ' changes', + () => { + component.skillLinkageModificationsArray = []; + component.isSkillDifficultyChanged = false; + spyOn(questionUndoRedoService, 'hasChanges').and.returnValue(false); - expect(component.isQuestionSavable()).toBe(false); - }); + expect(component.isQuestionSavable()).toBe(false); + } + ); it('should check if question is savable', () => { component.questionIsBeingUpdated = false; component.newQuestionSkillDifficulties = [0.9]; spyOn(questionUndoRedoService, 'hasChanges').and.returnValue(true); - spyOn(questionValidationService, 'isQuestionValid') - .and.returnValues(true, false); + spyOn(questionValidationService, 'isQuestionValid').and.returnValues( + true, + false + ); expect(component.isQuestionSavable()).toBe(true); @@ -965,8 +1101,8 @@ describe('Questions List Component', () => { component.question = question; spyOn(component.question, 'getStateData').and.returnValue({ interaction: { - id: 'TextInput' - } + id: 'TextInput', + }, } as State); expect(component.showSolutionCheckpoint()).toBe(true); @@ -975,104 +1111,97 @@ describe('Questions List Component', () => { expect(component.showSolutionCheckpoint()).toBe(false); }); - it('should show info message if skills is already linked to question', - fakeAsync(() => { - var skillSummaryDict = { - id: 'skillId1', - description: 'description1', - language_code: 'en', - version: 1, - misconception_count: 3, - worked_examples_count: 3, - skill_model_created_on: 1593138898626.193, - skill_model_last_updated: 1593138898626.193 - }; - component.associatedSkillSummaries = [ - ShortSkillSummary.createFromBackendDict({ - skill_id: 'skillId1', - skill_description: 'Skill Description' - }), - ShortSkillSummary.createFromBackendDict({ - skill_id: 'skillId2', - skill_description: 'Skill Description' - }) - ]; - component.groupedSkillSummaries = ({ - current: [], - others: [skillSummaryDict] - }); - spyOn(ngbModal, 'open').and.returnValue( - { - componentInstance: new MockNgbModalRef(), - result: Promise.resolve(skillSummaryDict) - } as NgbModalRef - ); - spyOn(alertsService, 'addInfoMessage'); - - component.addSkill(); + it('should show info message if skills is already linked to question', fakeAsync(() => { + var skillSummaryDict = { + id: 'skillId1', + description: 'description1', + language_code: 'en', + version: 1, + misconception_count: 3, + worked_examples_count: 3, + skill_model_created_on: 1593138898626.193, + skill_model_last_updated: 1593138898626.193, + }; + component.associatedSkillSummaries = [ + ShortSkillSummary.createFromBackendDict({ + skill_id: 'skillId1', + skill_description: 'Skill Description', + }), + ShortSkillSummary.createFromBackendDict({ + skill_id: 'skillId2', + skill_description: 'Skill Description', + }), + ]; + component.groupedSkillSummaries = { + current: [], + others: [skillSummaryDict], + }; + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: new MockNgbModalRef(), + result: Promise.resolve(skillSummaryDict), + } as NgbModalRef); + spyOn(alertsService, 'addInfoMessage'); + component.addSkill(); - expect(ngbModal.open).toHaveBeenCalled(); - })); + expect(ngbModal.open).toHaveBeenCalled(); + })); - it('should link skill if it is not already linked to question', - fakeAsync(() => { - var skillSummaryDict = { - id: 'skillId1', - description: 'description1', - language_code: 'en', - version: 1, - misconception_count: 3, - worked_examples_count: 3, - skill_model_created_on: 1593138898626.193, - skill_model_last_updated: 1593138898626.193 - }; - component.associatedSkillSummaries = [ - ShortSkillSummary.createFromBackendDict({ - skill_id: 'skillId2', - skill_description: 'Skill Description' - }), - ShortSkillSummary.createFromBackendDict({ - skill_id: 'skillId3', - skill_description: 'Skill Description' - }) - ]; - component.groupedSkillSummaries = ({ - current: [], - others: [skillSummaryDict] - }); - spyOn(ngbModal, 'open').and.returnValue( - { - componentInstance: new MockNgbModalRef(), - result: Promise.resolve(skillSummaryDict) - } as NgbModalRef - ); + it('should link skill if it is not already linked to question', fakeAsync(() => { + var skillSummaryDict = { + id: 'skillId1', + description: 'description1', + language_code: 'en', + version: 1, + misconception_count: 3, + worked_examples_count: 3, + skill_model_created_on: 1593138898626.193, + skill_model_last_updated: 1593138898626.193, + }; + component.associatedSkillSummaries = [ + ShortSkillSummary.createFromBackendDict({ + skill_id: 'skillId2', + skill_description: 'Skill Description', + }), + ShortSkillSummary.createFromBackendDict({ + skill_id: 'skillId3', + skill_description: 'Skill Description', + }), + ]; + component.groupedSkillSummaries = { + current: [], + others: [skillSummaryDict], + }; + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: new MockNgbModalRef(), + result: Promise.resolve(skillSummaryDict), + } as NgbModalRef); - component.addSkill(); - tick(); + component.addSkill(); + tick(); - expect(component.associatedSkillSummaries).toEqual( - [ - ShortSkillSummary.createFromBackendDict({ - skill_id: 'skillId2', - skill_description: 'Skill Description' - }), - ShortSkillSummary.createFromBackendDict({ - skill_id: 'skillId3', - skill_description: 'Skill Description' - }), - ShortSkillSummary.createFromBackendDict({ - skill_id: 'skillId1', - skill_description: 'description1' - }) - ] - ); - expect(component.skillLinkageModificationsArray).toEqual([{ + expect(component.associatedSkillSummaries).toEqual([ + ShortSkillSummary.createFromBackendDict({ + skill_id: 'skillId2', + skill_description: 'Skill Description', + }), + ShortSkillSummary.createFromBackendDict({ + skill_id: 'skillId3', + skill_description: 'Skill Description', + }), + ShortSkillSummary.createFromBackendDict({ + skill_id: 'skillId1', + skill_description: 'description1', + }), + ]); + expect(component.skillLinkageModificationsArray).toEqual([ + { id: 'skillId1', task: 'add', - difficulty: 0.6 - }]); - })); + difficulty: 0.6, + }, + ]); + })); it('should close modal when user clicks on cancel', fakeAsync(() => { var skillSummaryDict = { @@ -1083,29 +1212,27 @@ describe('Questions List Component', () => { misconception_count: 3, worked_examples_count: 3, skill_model_created_on: 1593138898626.193, - skill_model_last_updated: 1593138898626.193 + skill_model_last_updated: 1593138898626.193, }; component.associatedSkillSummaries = [ ShortSkillSummary.createFromBackendDict({ skill_id: 'skillId2', - skill_description: 'Skill Description' + skill_description: 'Skill Description', }), ShortSkillSummary.createFromBackendDict({ skill_id: 'skillId3', - skill_description: 'Skill Description' - }) + skill_description: 'Skill Description', + }), ]; component.groupedSkillSummaries = { current: [], - others: [skillSummaryDict] + others: [skillSummaryDict], }; - spyOn(ngbModal, 'open').and.returnValue( - { - componentInstance: new MockNgbModalRef(), - result: Promise.reject(skillSummaryDict) - } as NgbModalRef - ); + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: new MockNgbModalRef(), + result: Promise.reject(skillSummaryDict), + } as NgbModalRef); spyOn(alertsService, 'addInfoMessage'); component.addSkill(); @@ -1114,32 +1241,35 @@ describe('Questions List Component', () => { expect(ngbModal.open).toHaveBeenCalled(); })); - it('should save and publish question after updating linked skill', - fakeAsync(() => { - spyOn(editableQuestionBackendApiService, 'editQuestionSkillLinksAsync') - .and.returnValue(Promise.resolve()); - spyOn(questionsListService, 'getQuestionSummariesAsync'); - spyOn(component, 'saveAndPublishQuestion'); + it('should save and publish question after updating linked skill', fakeAsync(() => { + spyOn( + editableQuestionBackendApiService, + 'editQuestionSkillLinksAsync' + ).and.returnValue(Promise.resolve()); + spyOn(questionsListService, 'getQuestionSummariesAsync'); + spyOn(component, 'saveAndPublishQuestion'); - component.updateSkillLinkageAndQuestions('commit'); + component.updateSkillLinkageAndQuestions('commit'); - tick(500); + tick(500); - expect(questionsListService.getQuestionSummariesAsync).toHaveBeenCalled(); - expect(component.editorIsOpen).toBe(false); - expect(component.saveAndPublishQuestion).toHaveBeenCalledWith('commit'); - })); + expect(questionsListService.getQuestionSummariesAsync).toHaveBeenCalled(); + expect(component.editorIsOpen).toBe(false); + expect(component.saveAndPublishQuestion).toHaveBeenCalledWith('commit'); + })); it('should update skill linkage correctly', fakeAsync(() => { component.skillLinkageModificationsArray = [ { id: 'skillId1', task: 'update_difficulty', - difficulty: 0.9 - } + difficulty: 0.9, + }, ]; - spyOn(editableQuestionBackendApiService, 'editQuestionSkillLinksAsync') - .and.returnValue(Promise.resolve()); + spyOn( + editableQuestionBackendApiService, + 'editQuestionSkillLinksAsync' + ).and.returnValue(Promise.resolve()); component.updateSkillLinkage(); @@ -1148,84 +1278,88 @@ describe('Questions List Component', () => { expect(component.skillLinkageModificationsArray).toEqual([]); })); - it('should open question editor save modal if question' + - ' is being updated when user click on \'SAVE\' button', fakeAsync(() => { - component.questionIsBeingUpdated = true; - spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.resolve('commit') - } as NgbModalRef); - spyOn(component, 'updateSkillLinkageAndQuestions'); - spyOn(component, 'saveAndPublishQuestion'); - - // If skillLinkageModificationsArray is present. - component.skillLinkageModificationsArray = ( - [{ - id: '1', - task: null, - difficulty: 1, - }]); - - component.saveQuestion(); - tick(); - + it( + 'should open question editor save modal if question' + + " is being updated when user click on 'SAVE' button", + fakeAsync(() => { + component.questionIsBeingUpdated = true; + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.resolve('commit'), + } as NgbModalRef); + spyOn(component, 'updateSkillLinkageAndQuestions'); + spyOn(component, 'saveAndPublishQuestion'); - expect( - component.updateSkillLinkageAndQuestions).toHaveBeenCalledWith('commit'); + // If skillLinkageModificationsArray is present. + component.skillLinkageModificationsArray = [ + { + id: '1', + task: null, + difficulty: 1, + }, + ]; - // If skillLinkageModificationsArray is not present. - component.skillLinkageModificationsArray = []; + component.saveQuestion(); + tick(); - component.saveQuestion(); - tick(); + expect(component.updateSkillLinkageAndQuestions).toHaveBeenCalledWith( + 'commit' + ); + // If skillLinkageModificationsArray is not present. + component.skillLinkageModificationsArray = []; - expect(component.saveAndPublishQuestion).toHaveBeenCalledWith('commit'); - })); + component.saveQuestion(); + tick(); - it('should create new question if user clicks on \'SAVE\' and if question' + - ' is not being updates', () => { - component.questionIsBeingUpdated = false; - spyOn(skillEditorRoutingService, 'creatingNewQuestion'); - spyOn(component, 'saveAndPublishQuestion'); + expect(component.saveAndPublishQuestion).toHaveBeenCalledWith('commit'); + }) + ); - component.saveQuestion(); + it( + "should create new question if user clicks on 'SAVE' and if question" + + ' is not being updates', + () => { + component.questionIsBeingUpdated = false; + spyOn(skillEditorRoutingService, 'creatingNewQuestion'); + spyOn(component, 'saveAndPublishQuestion'); - expect(component.saveAndPublishQuestion).toHaveBeenCalled(); - expect(skillEditorRoutingService.creatingNewQuestion).toHaveBeenCalled(); - }); + component.saveQuestion(); - it('should close question editor save modal if user clicks cancel', - fakeAsync(() => { - component.questionIsBeingUpdated = true; - spyOn(component, 'saveAndPublishQuestion'); - component.skillLinkageModificationsArray = ( - [ - { - id: '1', - task: null, - difficulty: 1, - }, - { - id: '2', - task: null, - difficulty: 2, - } - ]); + expect(component.saveAndPublishQuestion).toHaveBeenCalled(); + expect(skillEditorRoutingService.creatingNewQuestion).toHaveBeenCalled(); + } + ); - spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.reject() - } as NgbModalRef); + it('should close question editor save modal if user clicks cancel', fakeAsync(() => { + component.questionIsBeingUpdated = true; + spyOn(component, 'saveAndPublishQuestion'); + component.skillLinkageModificationsArray = [ + { + id: '1', + task: null, + difficulty: 1, + }, + { + id: '2', + task: null, + difficulty: 2, + }, + ]; - component.saveQuestion(); - tick(); + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.reject(), + } as NgbModalRef); + component.saveQuestion(); + tick(); - expect(component.saveAndPublishQuestion).not.toHaveBeenCalled(); - })); + expect(component.saveAndPublishQuestion).not.toHaveBeenCalled(); + })); it('should get cached question summaries for one skill', () => { spyOn(questionsListService, 'getCachedQuestionSummaries').and.returnValue( - undefined); + undefined + ); expect(component.getQuestionSummariesForOneSkill()).toEqual(undefined); }); diff --git a/core/templates/components/question-directives/questions-list/questions-list.component.ts b/core/templates/components/question-directives/questions-list/questions-list.component.ts index e1b4bef4c9e1..61b8a702bf76 100644 --- a/core/templates/components/question-directives/questions-list/questions-list.component.ts +++ b/core/templates/components/question-directives/questions-list/questions-list.component.ts @@ -16,40 +16,58 @@ * @fileoverview Component for the questions list. */ -import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { Subscription } from 'rxjs'; -import { AlertsService } from 'services/alerts.service'; -import { AppConstants } from 'app.constants'; +import { + ChangeDetectorRef, + Component, + Input, + OnDestroy, + OnInit, +} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {Subscription} from 'rxjs'; +import {AlertsService} from 'services/alerts.service'; +import {AppConstants} from 'app.constants'; import cloneDeep from 'lodash/cloneDeep'; -import { QuestionSummary } from 'domain/question/question-summary-object.model'; -import { QuestionSummaryForOneSkill } from 'domain/question/question-summary-for-one-skill-object.model'; -import { QuestionUndoRedoService } from 'domain/editor/undo_redo/question-undo-redo.service'; -import { ShortSkillSummary } from 'domain/skill/short-skill-summary.model'; -import { SkillBackendApiService } from 'domain/skill/skill-backend-api.service'; -import { SkillDifficulty } from 'domain/skill/skill-difficulty.model'; -import { SkillLinkageModificationsArray } from 'domain/question/editable-question-backend-api.service'; -import { SkillSummary, SkillSummaryBackendDict } from 'domain/skill/skill-summary.model'; -import { MisconceptionObjectFactory, MisconceptionSkillMap } from 'domain/skill/MisconceptionObjectFactory'; -import { Question, QuestionObjectFactory } from 'domain/question/QuestionObjectFactory'; -import { State } from 'domain/state/StateObjectFactory'; -import { Rubric } from 'domain/skill/rubric.model'; -import { EditableQuestionBackendApiService } from 'domain/question/editable-question-backend-api.service'; -import { CategorizedSkills, SelectSkillModalComponent } from 'components/skill-selector/select-skill-modal.component'; -import { ConfirmQuestionExitModalComponent } from '../modal-templates/confirm-question-exit-modal.component'; -import { QuestionEditorSaveModalComponent } from '../modal-templates/question-editor-save-modal.component'; -import { ContextService } from 'services/context.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { ImageLocalStorageService } from 'services/image-local-storage.service'; -import { QuestionsListService } from 'services/questions-list.service'; -import { QuestionValidationService } from 'services/question-validation.service'; -import { SkillEditorRoutingService } from 'pages/skill-editor-page/services/skill-editor-routing.service'; -import { UtilsService } from 'services/utils.service'; -import { LoggerService } from 'services/contextual/logger.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { RemoveQuestionSkillLinkModalComponent } from '../modal-templates/remove-question-skill-link-modal.component'; +import {QuestionSummary} from 'domain/question/question-summary-object.model'; +import {QuestionSummaryForOneSkill} from 'domain/question/question-summary-for-one-skill-object.model'; +import {QuestionUndoRedoService} from 'domain/editor/undo_redo/question-undo-redo.service'; +import {ShortSkillSummary} from 'domain/skill/short-skill-summary.model'; +import {SkillBackendApiService} from 'domain/skill/skill-backend-api.service'; +import {SkillDifficulty} from 'domain/skill/skill-difficulty.model'; +import {SkillLinkageModificationsArray} from 'domain/question/editable-question-backend-api.service'; +import { + SkillSummary, + SkillSummaryBackendDict, +} from 'domain/skill/skill-summary.model'; +import { + MisconceptionObjectFactory, + MisconceptionSkillMap, +} from 'domain/skill/MisconceptionObjectFactory'; +import { + Question, + QuestionObjectFactory, +} from 'domain/question/QuestionObjectFactory'; +import {State} from 'domain/state/StateObjectFactory'; +import {Rubric} from 'domain/skill/rubric.model'; +import {EditableQuestionBackendApiService} from 'domain/question/editable-question-backend-api.service'; +import { + CategorizedSkills, + SelectSkillModalComponent, +} from 'components/skill-selector/select-skill-modal.component'; +import {ConfirmQuestionExitModalComponent} from '../modal-templates/confirm-question-exit-modal.component'; +import {QuestionEditorSaveModalComponent} from '../modal-templates/question-editor-save-modal.component'; +import {ContextService} from 'services/context.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {ImageLocalStorageService} from 'services/image-local-storage.service'; +import {QuestionsListService} from 'services/questions-list.service'; +import {QuestionValidationService} from 'services/question-validation.service'; +import {SkillEditorRoutingService} from 'pages/skill-editor-page/services/skill-editor-routing.service'; +import {UtilsService} from 'services/utils.service'; +import {LoggerService} from 'services/contextual/logger.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {RemoveQuestionSkillLinkModalComponent} from '../modal-templates/remove-question-skill-link-modal.component'; import INTERACTION_SPECS from 'interactions/interaction_specs.json'; interface GroupedSkillSummaries { @@ -59,7 +77,7 @@ interface GroupedSkillSummaries { @Component({ selector: 'oppia-questions-list', - templateUrl: './questions-list.component.html' + templateUrl: './questions-list.component.html', }) export class QuestionsListComponent implements OnInit, OnDestroy { @Input() allSkillSummaries: ShortSkillSummary[]; @@ -99,8 +117,7 @@ export class QuestionsListComponent implements OnInit, OnDestroy { private alertsService: AlertsService, private changeDetectorRef: ChangeDetectorRef, private contextService: ContextService, - private editableQuestionBackendApiService: - EditableQuestionBackendApiService, + private editableQuestionBackendApiService: EditableQuestionBackendApiService, private focusManagerService: FocusManagerService, private imageLocalStorageService: ImageLocalStorageService, private loggerService: LoggerService, @@ -114,14 +131,15 @@ export class QuestionsListComponent implements OnInit, OnDestroy { private skillEditorRoutingService: SkillEditorRoutingService, private utilsService: UtilsService, private windowDimensionsService: WindowDimensionsService, - private windowRef: WindowRef, - ) { } + private windowRef: WindowRef + ) {} createQuestion(): void { if (this.alertsService.warnings.length > 0) { this.loggerService.error( 'Could not create new question due to warnings: ' + - this.alertsService.warnings[0].content); + this.alertsService.warnings[0].content + ); return; } @@ -129,7 +147,11 @@ export class QuestionsListComponent implements OnInit, OnDestroy { this.associatedSkillSummaries = []; this.linkedSkillsWithDifficulty = [ SkillDifficulty.create( - this.selectedSkillId, '', AppConstants.DEFAULT_SKILL_DIFFICULTY)]; + this.selectedSkillId, + '', + AppConstants.DEFAULT_SKILL_DIFFICULTY + ), + ]; this.newQuestionSkillDifficulties = this.linkedSkillsWithDifficulty.map( linkedSkillWithDifficulty => linkedSkillWithDifficulty.getDifficulty() ); @@ -140,7 +162,8 @@ export class QuestionsListComponent implements OnInit, OnDestroy { this.imageLocalStorageService.flushStoredImagesData(); this.contextService.setImageSaveDestinationToLocalStorage(); this.question = this.questionObjectFactory.createDefaultQuestion( - this.newQuestionSkillIds); + this.newQuestionSkillIds + ); this.questionId = this.question.getId(); this.questionStateData = this.question.getStateData(); this.questionIsBeingUpdated = false; @@ -159,47 +182,49 @@ export class QuestionsListComponent implements OnInit, OnDestroy { changeLinkedSkillDifficulty(): void { this.isSkillDifficultyChanged = true; if (this.newQuestionSkillIds.length === 1) { - this.newQuestionSkillDifficulties = ( - [this.linkedSkillsWithDifficulty[0].getDifficulty()]); + this.newQuestionSkillDifficulties = [ + this.linkedSkillsWithDifficulty[0].getDifficulty(), + ]; } else { - this.linkedSkillsWithDifficulty.forEach( - (linkedSkillWithDifficulty) => { - if (!this.newQuestionSkillIds.includes( - linkedSkillWithDifficulty.getId())) { - this.newQuestionSkillIds.push( - linkedSkillWithDifficulty.getId()); - this.newQuestionSkillDifficulties.push( - linkedSkillWithDifficulty.getDifficulty()); - } - }); + this.linkedSkillsWithDifficulty.forEach(linkedSkillWithDifficulty => { + if ( + !this.newQuestionSkillIds.includes(linkedSkillWithDifficulty.getId()) + ) { + this.newQuestionSkillIds.push(linkedSkillWithDifficulty.getId()); + this.newQuestionSkillDifficulties.push( + linkedSkillWithDifficulty.getDifficulty() + ); + } + }); } - this.linkedSkillsWithDifficulty.forEach( - (linkedSkillWithDifficulty) => { - this.skillLinkageModificationsArray.push({ - id: linkedSkillWithDifficulty.getId(), - task: 'update_difficulty', - difficulty: linkedSkillWithDifficulty.getDifficulty() - }); + this.linkedSkillsWithDifficulty.forEach(linkedSkillWithDifficulty => { + this.skillLinkageModificationsArray.push({ + id: linkedSkillWithDifficulty.getId(), + task: 'update_difficulty', + difficulty: linkedSkillWithDifficulty.getDifficulty(), }); + }); } populateMisconceptions(skillIds: string[]): void { this.misconceptionsBySkill = {}; - this.skillBackendApiService.fetchMultiSkillsAsync( - skillIds).then( - (skills) => { - skills.forEach((skill) => { - this.misconceptionsBySkill[skill.getId()] = - skill.getMisconceptions(); + this.skillBackendApiService.fetchMultiSkillsAsync(skillIds).then( + skills => { + skills.forEach(skill => { + this.misconceptionsBySkill[skill.getId()] = skill.getMisconceptions(); }); - }, (error) => { + }, + error => { this.alertsService.addWarning(error); - }); + } + ); } editQuestion( - questionSummaryForOneSkill: QuestionSummary, - skillDescription: string, difficulty: number): void { + questionSummaryForOneSkill: QuestionSummary, + skillDescription: string, + difficulty: number + ): void { this.skillLinkageModificationsArray = []; this.isSkillDifficultyChanged = false; if (this.editorIsOpen) { @@ -207,45 +232,51 @@ export class QuestionsListComponent implements OnInit, OnDestroy { } if (!this.canEditQuestion) { this.alertsService.addWarning( - 'User does not have enough rights to edit the question'); + 'User does not have enough rights to edit the question' + ); return; } this.newQuestionSkillIds = []; this.newQuestionSkillIds = [this.selectedSkillId]; this.linkedSkillsWithDifficulty = []; - this.newQuestionSkillIds.forEach((skillId) => { + this.newQuestionSkillIds.forEach(skillId => { this.linkedSkillsWithDifficulty.push( - SkillDifficulty.create( - skillId, skillDescription, difficulty)); + SkillDifficulty.create(skillId, skillDescription, difficulty) + ); }); this.difficulty = difficulty; this.misconceptionsBySkill = {}; this.associatedSkillSummaries = []; - this.editableQuestionBackendApiService.fetchQuestionAsync( - questionSummaryForOneSkill.getQuestionId()).then( - (response) => { - if (response.associated_skill_dicts) { - response.associated_skill_dicts.forEach((skillDict) => { - this.misconceptionsBySkill[skillDict.id] = - skillDict.misconceptions.map((misconception) => { - return this.misconceptionObjectFactory.createFromBackendDict( - misconception); - }); - this.associatedSkillSummaries.push( - ShortSkillSummary.create( - skillDict.id, skillDict.description)); - }); + this.editableQuestionBackendApiService + .fetchQuestionAsync(questionSummaryForOneSkill.getQuestionId()) + .then( + response => { + if (response.associated_skill_dicts) { + response.associated_skill_dicts.forEach(skillDict => { + this.misconceptionsBySkill[skillDict.id] = + skillDict.misconceptions.map(misconception => { + return this.misconceptionObjectFactory.createFromBackendDict( + misconception + ); + }); + this.associatedSkillSummaries.push( + ShortSkillSummary.create(skillDict.id, skillDict.description) + ); + }); + } + this.question = cloneDeep(response.questionObject); + this.questionId = this.question.getId(); + this.questionStateData = this.question.getStateData(); + this.questionIsBeingUpdated = true; + this.newQuestionIsBeingCreated = false; + this.openQuestionEditor(); + }, + errorResponse => { + this.alertsService.addWarning( + errorResponse.error || 'Failed to fetch question.' + ); } - this.question = cloneDeep(response.questionObject); - this.questionId = this.question.getId(); - this.questionStateData = this.question.getStateData(); - this.questionIsBeingUpdated = true; - this.newQuestionIsBeingCreated = false; - this.openQuestionEditor(); - }, (errorResponse) => { - this.alertsService.addWarning( - errorResponse.error || 'Failed to fetch question.'); - }); + ); } openQuestionEditor(): void { @@ -260,48 +291,58 @@ export class QuestionsListComponent implements OnInit, OnDestroy { } removeQuestionSkillLinkAsync( - questionId: string, - skillId: string, - skillDifficulty: number): void { - this.editableQuestionBackendApiService.editQuestionSkillLinksAsync( - questionId, [ + questionId: string, + skillId: string, + skillDifficulty: number + ): void { + this.editableQuestionBackendApiService + .editQuestionSkillLinksAsync(questionId, [ { id: skillId, task: 'remove', - difficulty: skillDifficulty - } as SkillLinkageModificationsArray - ] - ).then(() => { - this.questionsListService.resetPageNumber(); - this.questionsListService.getQuestionSummariesAsync( - this.selectedSkillId, true, true); - this.alertsService.addSuccessMessage('Question Removed'); - this._removeArrayElement(questionId); - }); + difficulty: skillDifficulty, + } as SkillLinkageModificationsArray, + ]) + .then(() => { + this.questionsListService.resetPageNumber(); + this.questionsListService.getQuestionSummariesAsync( + this.selectedSkillId, + true, + true + ); + this.alertsService.addSuccessMessage('Question Removed'); + this._removeArrayElement(questionId); + }); } removeQuestionFromSkill(questionId: string, skillDifficulty: number): void { - let modalRef: NgbModalRef = this.ngbModal. - open(RemoveQuestionSkillLinkModalComponent, { - backdrop: 'static' - }); + let modalRef: NgbModalRef = this.ngbModal.open( + RemoveQuestionSkillLinkModalComponent, + { + backdrop: 'static', + } + ); modalRef.componentInstance.skillId = this.selectedSkillId; modalRef.componentInstance.canEditQuestion = this.canEditQuestion; - modalRef.componentInstance.numberOfQuestions = ( - this.questionSummariesForOneSkill.length); - - modalRef.result.then(() => { - this.deletedQuestionIds.push(questionId); - this.removeQuestionSkillLinkAsync( - questionId, - this.selectedSkillId, - skillDifficulty); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + modalRef.componentInstance.numberOfQuestions = + this.questionSummariesForOneSkill.length; + + modalRef.result.then( + () => { + this.deletedQuestionIds.push(questionId); + this.removeQuestionSkillLinkAsync( + questionId, + this.selectedSkillId, + skillDifficulty + ); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } _removeArrayElement(questionId: string): void { @@ -314,39 +355,46 @@ export class QuestionsListComponent implements OnInit, OnDestroy { removeSkill(skillId: string): void { if (this.associatedSkillSummaries.length === 1) { this.alertsService.addInfoMessage( - 'A question should be linked to at least one skill.'); + 'A question should be linked to at least one skill.' + ); return; } this.skillLinkageModificationsArray.push({ id: skillId, task: 'remove', - difficulty: this.difficulty + difficulty: this.difficulty, } as SkillLinkageModificationsArray); - this.associatedSkillSummaries = - this.associatedSkillSummaries.filter((summary) => { - return summary.getId() !== skillId; - }); + this.associatedSkillSummaries = this.associatedSkillSummaries.filter( + summary => { + return summary.getId() !== skillId; + } + ); this.updateSkillLinkage(); } isQuestionSavable(): boolean { // Not savable if there are no changes. - if (!this.questionUndoRedoService.hasChanges() && ( + if ( + !this.questionUndoRedoService.hasChanges() && this.skillLinkageModificationsArray && - this.skillLinkageModificationsArray.length === 0 - ) && !this.isSkillDifficultyChanged) { + this.skillLinkageModificationsArray.length === 0 && + !this.isSkillDifficultyChanged + ) { return false; } let questionIdValid = this.questionValidationService.isQuestionValid( - this.question, this.misconceptionsBySkill); + this.question, + this.misconceptionsBySkill + ); if (!this.questionIsBeingUpdated) { return Boolean( questionIdValid && - this.newQuestionSkillDifficulties && - this.newQuestionSkillDifficulties.length); + this.newQuestionSkillDifficulties && + this.newQuestionSkillDifficulties.length + ); } return questionIdValid; } @@ -357,167 +405,199 @@ export class QuestionsListComponent implements OnInit, OnDestroy { } const interactionId = this.question.getStateData().interaction.id; - return ( - interactionId && INTERACTION_SPECS[ - interactionId].can_have_solution); + return interactionId && INTERACTION_SPECS[interactionId].can_have_solution; } addSkill(): void { - let skillsInSameTopicCount = - this.groupedSkillSummaries.current.length; - let sortedSkillSummaries = - this.groupedSkillSummaries.current.concat( - this.groupedSkillSummaries.others); + let skillsInSameTopicCount = this.groupedSkillSummaries.current.length; + let sortedSkillSummaries = this.groupedSkillSummaries.current.concat( + this.groupedSkillSummaries.others + ); let allowSkillsFromOtherTopics = true; - let modalRef: NgbModalRef = this.ngbModal.open( - SelectSkillModalComponent, { - backdrop: 'static', - windowClass: 'skill-select-modal', - size: 'xl' - }); + let modalRef: NgbModalRef = this.ngbModal.open(SelectSkillModalComponent, { + backdrop: 'static', + windowClass: 'skill-select-modal', + size: 'xl', + }); modalRef.componentInstance.skillSummaries = sortedSkillSummaries; - modalRef.componentInstance.skillsInSameTopicCount = ( - skillsInSameTopicCount); - modalRef.componentInstance.categorizedSkills = ( - this.skillsCategorizedByTopics); - modalRef.componentInstance.allowSkillsFromOtherTopics = ( - allowSkillsFromOtherTopics); - modalRef.componentInstance.untriagedSkillSummaries = ( - this.untriagedSkillSummaries); - modalRef.componentInstance.associatedSkillSummaries = ( - this.associatedSkillSummaries); - - modalRef.result.then((summary) => { - for (let idx in this.associatedSkillSummaries) { - if ( - this.associatedSkillSummaries[idx].getId() === - summary.id) { - this.alertsService.addInfoMessage( - 'Skill already linked to question'); - return; + modalRef.componentInstance.skillsInSameTopicCount = skillsInSameTopicCount; + modalRef.componentInstance.categorizedSkills = + this.skillsCategorizedByTopics; + modalRef.componentInstance.allowSkillsFromOtherTopics = + allowSkillsFromOtherTopics; + modalRef.componentInstance.untriagedSkillSummaries = + this.untriagedSkillSummaries; + modalRef.componentInstance.associatedSkillSummaries = + this.associatedSkillSummaries; + + modalRef.result.then( + summary => { + for (let idx in this.associatedSkillSummaries) { + if (this.associatedSkillSummaries[idx].getId() === summary.id) { + this.alertsService.addInfoMessage( + 'Skill already linked to question' + ); + return; + } } - } - this.associatedSkillSummaries.push( - ShortSkillSummary.create( - summary.id, summary.description)); - this.skillLinkageModificationsArray = []; - this.skillLinkageModificationsArray.push({ - id: summary.id, - task: 'add', - difficulty: AppConstants.DEFAULT_SKILL_DIFFICULTY - }); - this.updateSkillLinkage(); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is - // clicked. No further action is needed. - }); + this.associatedSkillSummaries.push( + ShortSkillSummary.create(summary.id, summary.description) + ); + this.skillLinkageModificationsArray = []; + this.skillLinkageModificationsArray.push({ + id: summary.id, + task: 'add', + difficulty: AppConstants.DEFAULT_SKILL_DIFFICULTY, + }); + this.updateSkillLinkage(); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is + // clicked. No further action is needed. + } + ); } getQuestionIndex(index: number): number { return ( this.questionsListService.getCurrentPageNumber() * - AppConstants.NUM_QUESTIONS_PER_PAGE + index + 1); + AppConstants.NUM_QUESTIONS_PER_PAGE + + index + + 1 + ); } goToNextPage(): void { this.questionsListService.incrementPageNumber(); this.questionsListService.getQuestionSummariesAsync( - this.selectedSkillId, true, false + this.selectedSkillId, + true, + false ); } goToPreviousPage(): void { this.questionsListService.decrementPageNumber(); this.questionsListService.getQuestionSummariesAsync( - this.selectedSkillId, false, false + this.selectedSkillId, + false, + false ); } showUnaddressedSkillMisconceptionWarning( - skillMisconceptionIds: string[]): boolean { + skillMisconceptionIds: string[] + ): boolean { let skillId = this.selectedSkillId; - let expectedMisconceptionIds = ( - this.misconceptionIdsForSelectedSkill); - let actualMisconceptionIds = ( - skillMisconceptionIds.map(skillMisconceptionId => { + let expectedMisconceptionIds = this.misconceptionIdsForSelectedSkill; + let actualMisconceptionIds = skillMisconceptionIds.map( + skillMisconceptionId => { if (skillMisconceptionId.startsWith(skillId)) { return parseInt(skillMisconceptionId.split('-')[1]); } - })); + } + ); return this.utilsService.isEquivalent( - actualMisconceptionIds.sort(), expectedMisconceptionIds.sort()); + actualMisconceptionIds.sort(), + expectedMisconceptionIds.sort() + ); } saveAndPublishQuestion(commitMessage: string | null): void { - let validationErrors = ( - this.questionValidationService.getValidationErrorMessage( - this.question)); - let unaddressedMisconceptions = ( + let validationErrors = + this.questionValidationService.getValidationErrorMessage(this.question); + let unaddressedMisconceptions = this.question.getUnaddressedMisconceptionNames( - this.misconceptionsBySkill)); - let unaddressedMisconceptionsErrorString = ( - `Remaining misconceptions that need to be addressed: ${ - unaddressedMisconceptions.join(', ')}`); + this.misconceptionsBySkill + ); + let unaddressedMisconceptionsErrorString = `Remaining misconceptions that need to be addressed: ${unaddressedMisconceptions.join( + ', ' + )}`; if (validationErrors || unaddressedMisconceptions.length) { this.alertsService.addWarning( - validationErrors || unaddressedMisconceptionsErrorString); + validationErrors || unaddressedMisconceptionsErrorString + ); return; } if (!this.questionIsBeingUpdated) { let imagesData = this.imageLocalStorageService.getStoredImagesData(); this.imageLocalStorageService.flushStoredImagesData(); - this.editableQuestionBackendApiService.createQuestionAsync( - this.newQuestionSkillIds, this.newQuestionSkillDifficulties, - (this.question.toBackendDict(true)), imagesData - ).then((response) => { - if (this.skillLinkageModificationsArray && - this.skillLinkageModificationsArray.length > 0) { - this.editableQuestionBackendApiService.editQuestionSkillLinksAsync( - response.questionId, this.skillLinkageModificationsArray - ); - } - this.questionsListService.resetPageNumber(); - this.questionsListService.getQuestionSummariesAsync( - this.selectedSkillId, true, true + this.editableQuestionBackendApiService + .createQuestionAsync( + this.newQuestionSkillIds, + this.newQuestionSkillDifficulties, + this.question.toBackendDict(true), + imagesData + ) + .then( + response => { + if ( + this.skillLinkageModificationsArray && + this.skillLinkageModificationsArray.length > 0 + ) { + this.editableQuestionBackendApiService.editQuestionSkillLinksAsync( + response.questionId, + this.skillLinkageModificationsArray + ); + } + this.questionsListService.resetPageNumber(); + this.questionsListService.getQuestionSummariesAsync( + this.selectedSkillId, + true, + true + ); + this.editorIsOpen = false; + this.questionIsBeingSaved = false; + this.alertsService.addSuccessMessage( + 'Question created successfully.' + ); + this._initTab(true); + }, + error => { + this.alertsService.addWarning( + error || 'There was an error saving the question.' + ); + this.questionIsBeingSaved = false; + } ); - this.editorIsOpen = false; - this.questionIsBeingSaved = false; - this.alertsService.addSuccessMessage( - 'Question created successfully.'); - this._initTab(true); - }, (error) => { - this.alertsService.addWarning( - error || 'There was an error saving the question.'); - this.questionIsBeingSaved = false; - }); } else { if (this.questionUndoRedoService.hasChanges()) { if (commitMessage) { - this.editableQuestionBackendApiService.updateQuestionAsync( - this.questionId, String(this.question.getVersion()), commitMessage, - this.questionUndoRedoService - .getCommittableChangeList()).then( - () => { - this.questionUndoRedoService.clearChanges(); - this.editorIsOpen = false; - this.questionIsBeingSaved = false; - this.questionsListService.getQuestionSummariesAsync( - this.selectedSkillId, true, true - ); - }, (error) => { - this.alertsService.addWarning( - error || 'There was an error saving the question.'); - this.editorIsOpen = false; - this.questionIsBeingSaved = false; - }); + this.editableQuestionBackendApiService + .updateQuestionAsync( + this.questionId, + String(this.question.getVersion()), + commitMessage, + this.questionUndoRedoService.getCommittableChangeList() + ) + .then( + () => { + this.questionUndoRedoService.clearChanges(); + this.editorIsOpen = false; + this.questionIsBeingSaved = false; + this.questionsListService.getQuestionSummariesAsync( + this.selectedSkillId, + true, + true + ); + }, + error => { + this.alertsService.addWarning( + error || 'There was an error saving the question.' + ); + this.editorIsOpen = false; + this.questionIsBeingSaved = false; + } + ); } else { this.alertsService.addWarning( - 'Please provide a valid commit message.'); + 'Please provide a valid commit message.' + ); this.editorIsOpen = false; this.questionIsBeingSaved = false; } @@ -534,30 +614,39 @@ export class QuestionsListComponent implements OnInit, OnDestroy { } cancel(): void { - this.ngbModal.open(ConfirmQuestionExitModalComponent, { - backdrop: true, - }).result.then(() => { - this.contextService.resetImageSaveDestination(); - this.editorIsOpen = false; - this.windowRef.nativeWindow.location.hash = null; - this.skillEditorRoutingService.questionIsBeingCreated = false; - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is - // clicked. No further action is needed. - }); + this.ngbModal + .open(ConfirmQuestionExitModalComponent, { + backdrop: true, + }) + .result.then( + () => { + this.contextService.resetImageSaveDestination(); + this.editorIsOpen = false; + this.windowRef.nativeWindow.location.hash = null; + this.skillEditorRoutingService.questionIsBeingCreated = false; + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is + // clicked. No further action is needed. + } + ); } updateSkillLinkageAndQuestions(commitMsg: string): void { - this.editableQuestionBackendApiService.editQuestionSkillLinksAsync( - this.questionId, this.skillLinkageModificationsArray - ).then( - data => { + this.editableQuestionBackendApiService + .editQuestionSkillLinksAsync( + this.questionId, + this.skillLinkageModificationsArray + ) + .then(data => { this.skillLinkageModificationsArray = []; setTimeout(() => { this.questionsListService.resetPageNumber(); this.questionsListService.getQuestionSummariesAsync( - this.selectedSkillId, true, true + this.selectedSkillId, + true, + true ); this.editorIsOpen = false; this.saveAndPublishQuestion(commitMsg); @@ -566,10 +655,12 @@ export class QuestionsListComponent implements OnInit, OnDestroy { } updateSkillLinkage(): void { - this.editableQuestionBackendApiService.editQuestionSkillLinksAsync( - this.questionId, this.skillLinkageModificationsArray - ).then( - data => { + this.editableQuestionBackendApiService + .editQuestionSkillLinksAsync( + this.questionId, + this.skillLinkageModificationsArray + ) + .then(data => { this.skillLinkageModificationsArray = []; }); } @@ -579,21 +670,28 @@ export class QuestionsListComponent implements OnInit, OnDestroy { this.contextService.resetImageSaveDestination(); this.windowRef.nativeWindow.location.hash = null; if (this.questionIsBeingUpdated) { - this.ngbModal.open(QuestionEditorSaveModalComponent, { - backdrop: 'static', - }).result.then((commitMessage) => { - if (this.skillLinkageModificationsArray && - this.skillLinkageModificationsArray.length > 0) { - this.updateSkillLinkageAndQuestions(commitMessage); - } else { - this.contextService.resetImageSaveDestination(); - this.saveAndPublishQuestion(commitMessage); - } - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is - // clicked. No further action is needed. - }); + this.ngbModal + .open(QuestionEditorSaveModalComponent, { + backdrop: 'static', + }) + .result.then( + commitMessage => { + if ( + this.skillLinkageModificationsArray && + this.skillLinkageModificationsArray.length > 0 + ) { + this.updateSkillLinkageAndQuestions(commitMessage); + } else { + this.contextService.resetImageSaveDestination(); + this.saveAndPublishQuestion(commitMessage); + } + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is + // clicked. No further action is needed. + } + ); } else { this.contextService.resetImageSaveDestination(); this.saveAndPublishQuestion(null); @@ -602,8 +700,8 @@ export class QuestionsListComponent implements OnInit, OnDestroy { } getQuestionSummariesForOneSkill(): void { - this.questionSummariesForOneSkill = ( - this.questionsListService.getCachedQuestionSummaries()); + this.questionSummariesForOneSkill = + this.questionsListService.getCachedQuestionSummaries(); } getCurrentPageNumber(): number { @@ -623,20 +721,21 @@ export class QuestionsListComponent implements OnInit, OnDestroy { this.misconceptionIdsForSelectedSkill = []; if (this.selectedSkillId) { - this.skillBackendApiService.fetchSkillAsync( - this.selectedSkillId - ).then(responseObject => { - this.misconceptionIdsForSelectedSkill = ( - responseObject.skill.getMisconceptions().map( - misconception => misconception.getId())); - }); + this.skillBackendApiService + .fetchSkillAsync(this.selectedSkillId) + .then(responseObject => { + this.misconceptionIdsForSelectedSkill = responseObject.skill + .getMisconceptions() + .map(misconception => misconception.getId()); + }); } if (this.skillEditorRoutingService.navigateToQuestionEditor()) { this.createQuestion(); } else { this.questionsListService.getQuestionSummariesAsync( - this.selectedSkillId, resetHistoryAndFetch, + this.selectedSkillId, + resetHistoryAndFetch, resetHistoryAndFetch ); } @@ -646,17 +745,16 @@ export class QuestionsListComponent implements OnInit, OnDestroy { ngOnInit(): void { this.directiveSubscriptions.add( - this.questionsListService.onQuestionSummariesInitialized.subscribe( - () => { - this._initTab(false); - this.focusManagerService.setFocus('newQuestionBtn'); - this.getQuestionSummariesForOneSkill(); - this.changeDetectorRef.detectChanges(); - })); + this.questionsListService.onQuestionSummariesInitialized.subscribe(() => { + this._initTab(false); + this.focusManagerService.setFocus('newQuestionBtn'); + this.getQuestionSummariesForOneSkill(); + this.changeDetectorRef.detectChanges(); + }) + ); this.showDifficultyChoices = false; - this.difficultyCardIsShown = ( - !this.windowDimensionsService.isWindowNarrow()); + this.difficultyCardIsShown = !this.windowDimensionsService.isWindowNarrow(); this.associatedSkillSummaries = []; this.editorIsOpen = false; this.deletedQuestionIds = []; @@ -670,7 +768,9 @@ export class QuestionsListComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive('oppiaQuestionsList', +angular.module('oppia').directive( + 'oppiaQuestionsList', downgradeComponent({ - component: QuestionsListComponent - }) as angular.IDirectiveFactory); + component: QuestionsListComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/components/question-directives/questions-list/questions-list.constants.ajs.ts b/core/templates/components/question-directives/questions-list/questions-list.constants.ajs.ts index a9368e13d803..87e43bf0ad08 100644 --- a/core/templates/components/question-directives/questions-list/questions-list.constants.ajs.ts +++ b/core/templates/components/question-directives/questions-list/questions-list.constants.ajs.ts @@ -16,12 +16,20 @@ * @fileoverview Constants for the question player directive. */ -import { QuestionsListConstants } from - 'components/question-directives/questions-list/questions-list.constants'; +import {QuestionsListConstants} from 'components/question-directives/questions-list/questions-list.constants'; -angular.module('oppia').constant( - 'DEFAULT_SKILL_DIFFICULTY', QuestionsListConstants.DEFAULT_SKILL_DIFFICULTY); -angular.module('oppia').constant( - 'MODE_SELECT_DIFFICULTY', QuestionsListConstants.MODE_SELECT_DIFFICULTY); -angular.module('oppia').constant( - 'MODE_SELECT_SKILL', QuestionsListConstants.MODE_SELECT_SKILL); +angular + .module('oppia') + .constant( + 'DEFAULT_SKILL_DIFFICULTY', + QuestionsListConstants.DEFAULT_SKILL_DIFFICULTY + ); +angular + .module('oppia') + .constant( + 'MODE_SELECT_DIFFICULTY', + QuestionsListConstants.MODE_SELECT_DIFFICULTY + ); +angular + .module('oppia') + .constant('MODE_SELECT_SKILL', QuestionsListConstants.MODE_SELECT_SKILL); diff --git a/core/templates/components/ratings/rating-computation/rating-computation.service.spec.ts b/core/templates/components/ratings/rating-computation/rating-computation.service.spec.ts index 8846283e7890..a4a6ecba4a61 100644 --- a/core/templates/components/ratings/rating-computation/rating-computation.service.spec.ts +++ b/core/templates/components/ratings/rating-computation/rating-computation.service.spec.ts @@ -16,8 +16,7 @@ * @fileoverview Tests that average ratings are being computed correctly. */ -import { RatingComputationService } from - 'components/ratings/rating-computation/rating-computation.service'; +import {RatingComputationService} from 'components/ratings/rating-computation/rating-computation.service'; describe('Rating computation service', () => { let ratingComputationService: RatingComputationService; @@ -26,45 +25,50 @@ describe('Rating computation service', () => { ratingComputationService = new RatingComputationService(); }); - it( - 'should show an average rating only if there are enough individual ones', - () => { - // Don't show an average rating if there are too few ratings. - expect(ratingComputationService.computeAverageRating({ + it('should show an average rating only if there are enough individual ones', () => { + // Don't show an average rating if there are too few ratings. + expect( + ratingComputationService.computeAverageRating({ 1: 0, 2: 0, 3: 0, 4: 0, - 5: 0 - })).toBeNull(); + 5: 0, + }) + ).toBeNull(); - // Show an average rating once the minimum is reached. - expect(ratingComputationService.computeAverageRating({ + // Show an average rating once the minimum is reached. + expect( + ratingComputationService.computeAverageRating({ 1: 1, 2: 0, 3: 0, 4: 0, - 5: 0 - })).toBe(1.0); + 5: 0, + }) + ).toBe(1.0); - // Continue showing an average rating if additional ratings are added. - expect(ratingComputationService.computeAverageRating({ + // Continue showing an average rating if additional ratings are added. + expect( + ratingComputationService.computeAverageRating({ 1: 1, 2: 0, 3: 0, 4: 0, - 5: 1 - })).toBe(3.0); - } - ); + 5: 1, + }) + ).toBe(3.0); + }); it('should compute average ratings correctly', () => { - expect(ratingComputationService.computeAverageRating({ - 1: 6, - 2: 3, - 3: 8, - 4: 12, - 5: 11 - })).toBe(3.475); + expect( + ratingComputationService.computeAverageRating({ + 1: 6, + 2: 3, + 3: 8, + 4: 12, + 5: 11, + }) + ).toBe(3.475); }); }); diff --git a/core/templates/components/ratings/rating-computation/rating-computation.service.ts b/core/templates/components/ratings/rating-computation/rating-computation.service.ts index 6b8b8bdead08..1009328f4426 100644 --- a/core/templates/components/ratings/rating-computation/rating-computation.service.ts +++ b/core/templates/components/ratings/rating-computation/rating-computation.service.ts @@ -16,16 +16,15 @@ * @fileoverview Service for computing the average rating. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; -import { ExplorationRatings } from - 'domain/summary/learner-exploration-summary.model'; +import {ExplorationRatings} from 'domain/summary/learner-exploration-summary.model'; type ExplorationRatingsKey = keyof ExplorationRatings; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class RatingComputationService { static areRatingsShown(ratingFrequencies: ExplorationRatings): boolean { @@ -42,8 +41,7 @@ export class RatingComputationService { // Returns 'null' if the ratings are less than the // minimum acceptable number of ratings. The average should // not be computed in this case. - computeAverageRating( - ratingFrequencies: ExplorationRatings): number | null { + computeAverageRating(ratingFrequencies: ExplorationRatings): number | null { if (!RatingComputationService.areRatingsShown(ratingFrequencies)) { return null; } else { @@ -60,5 +58,9 @@ export class RatingComputationService { } } -angular.module('oppia').factory( - 'RatingComputationService', downgradeInjectable(RatingComputationService)); +angular + .module('oppia') + .factory( + 'RatingComputationService', + downgradeInjectable(RatingComputationService) + ); diff --git a/core/templates/components/ratings/rating-display/rating-display.component.spec.ts b/core/templates/components/ratings/rating-display/rating-display.component.spec.ts index 01b91fb93c99..977d007e3470 100644 --- a/core/templates/components/ratings/rating-display/rating-display.component.spec.ts +++ b/core/templates/components/ratings/rating-display/rating-display.component.spec.ts @@ -16,9 +16,9 @@ * @fileoverview Unit tests for rating display component. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { RatingDisplayComponent } from './rating-display.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {RatingDisplayComponent} from './rating-display.component'; describe('Rating display component', () => { let componentInstance: RatingDisplayComponent; @@ -27,11 +27,9 @@ describe('Rating display component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [], - declarations: [ - RatingDisplayComponent - ], + declarations: [RatingDisplayComponent], providers: [], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -55,7 +53,6 @@ describe('Rating display component', () => { componentInstance.displayValue(4); - expect(componentInstance.stars).toBeDefined(); }); @@ -69,7 +66,8 @@ describe('Rating display component', () => { componentInstance.clickStar(4); expect(componentInstance.status).toEqual( - componentInstance.STATUS_RATING_SET); + componentInstance.STATUS_RATING_SET + ); expect(componentInstance.ratingValue).toEqual(4); expect(componentInstance.edit.emit).toHaveBeenCalled(); }); diff --git a/core/templates/components/ratings/rating-display/rating-display.component.ts b/core/templates/components/ratings/rating-display/rating-display.component.ts index 092806322504..d6e571499ac5 100644 --- a/core/templates/components/ratings/rating-display/rating-display.component.ts +++ b/core/templates/components/ratings/rating-display/rating-display.component.ts @@ -16,8 +16,8 @@ * @fileoverview Component for displaying summary rating information. */ -import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component, EventEmitter, Input, Output} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; interface StarDict { cssClass: string; @@ -26,7 +26,7 @@ interface StarDict { @Component({ selector: 'oppia-rating-display', - templateUrl: './rating-display.component.html' + templateUrl: './rating-display.component.html', }) export class RatingDisplayComponent { // This will display a star-rating based on the given data. The attributes @@ -50,10 +50,10 @@ export class RatingDisplayComponent { STATUS_RATING_SET = 'rating_set'; ngOnInit(): void { - this.stars = this.POSSIBLE_RATINGS.map((starValue) => { + this.stars = this.POSSIBLE_RATINGS.map(starValue => { return { cssClass: 'far fa-star', - value: starValue + value: starValue, }; }); this.status = this.STATUS_INACTIVE; @@ -62,14 +62,19 @@ export class RatingDisplayComponent { displayValue(ratingValue: number): void { for (let i = 0; i < this.stars.length; i++) { - this.stars[i].cssClass = ( - ratingValue === undefined ? 'far fa-star' : - ratingValue < this.stars[i].value - 0.75 ? 'far fa-star' : - ratingValue < this.stars[i].value - 0.25 ? 'far fa-star-half' : - 'fas fa-star'); + this.stars[i].cssClass = + ratingValue === undefined + ? 'far fa-star' + : ratingValue < this.stars[i].value - 0.75 + ? 'far fa-star' + : ratingValue < this.stars[i].value - 0.25 + ? 'far fa-star-half' + : 'fas fa-star'; - if (this.status === this.STATUS_ACTIVE && - ratingValue >= this.stars[i].value) { + if ( + this.status === this.STATUS_ACTIVE && + ratingValue >= this.stars[i].value + ) { this.stars[i].cssClass += ' oppia-rating-star-active'; } } @@ -85,10 +90,9 @@ export class RatingDisplayComponent { } enterStar(starValue: number): void { - let starsHaveNotBeenClicked = ( + let starsHaveNotBeenClicked = this.status === this.STATUS_ACTIVE || - this.status === this.STATUS_INACTIVE - ); + this.status === this.STATUS_INACTIVE; if (this.isEditable && starsHaveNotBeenClicked) { this.status = this.STATUS_ACTIVE; @@ -102,7 +106,9 @@ export class RatingDisplayComponent { } } -angular.module('oppia').directive('oppiaRatingDisplay', +angular.module('oppia').directive( + 'oppiaRatingDisplay', downgradeComponent({ - component: RatingDisplayComponent - }) as angular.IDirectiveFactory); + component: RatingDisplayComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/components/recommendations/post-chapter-recommendations.component.spec.ts b/core/templates/components/recommendations/post-chapter-recommendations.component.spec.ts index bc09fecbef26..9e3fa3bb31eb 100644 --- a/core/templates/components/recommendations/post-chapter-recommendations.component.spec.ts +++ b/core/templates/components/recommendations/post-chapter-recommendations.component.spec.ts @@ -16,13 +16,13 @@ * @fileoverview Unit tests for the post chapter recommendations component. */ -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { PostChapterRecommendationsComponent } from './post-chapter-recommendations.component'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { UrlService } from 'services/contextual/url.service'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {PostChapterRecommendationsComponent} from './post-chapter-recommendations.component'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {UrlService} from 'services/contextual/url.service'; -describe('End chapter check mark component', function() { +describe('End chapter check mark component', function () { let component: PostChapterRecommendationsComponent; let fixture: ComponentFixture; let urlInterpolationService: UrlInterpolationService; @@ -43,44 +43,53 @@ describe('End chapter check mark component', function() { }); it('should get static image url', () => { - spyOn(urlInterpolationService, 'getStaticImageUrl') - .and.returnValue('image_url'); + spyOn(urlInterpolationService, 'getStaticImageUrl').and.returnValue( + 'image_url' + ); - expect(component.getStaticImageUrl('practice_session_image_path')) - .toBe('image_url'); + expect(component.getStaticImageUrl('practice_session_image_path')).toBe( + 'image_url' + ); expect(urlInterpolationService.getStaticImageUrl).toHaveBeenCalledWith( - 'practice_session_image_path'); + 'practice_session_image_path' + ); }); it('should get practice tab url', () => { - spyOn(urlInterpolationService, 'interpolateUrl') - .and.returnValue('topic_page'); + spyOn(urlInterpolationService, 'interpolateUrl').and.returnValue( + 'topic_page' + ); spyOn(urlService, 'getUrlParams').and.returnValue({ topic_url_fragment: 'topic_url_fragment', - classroom_url_fragment: 'classroom_url_fragment' + classroom_url_fragment: 'classroom_url_fragment', }); expect(component.getPracticeTabUrl()).toBe('topic_page/practice'); expect(urlInterpolationService.interpolateUrl).toHaveBeenCalledWith( - '/learn//', { + '/learn//', + { topic_url_fragment: 'topic_url_fragment', - classroom_url_fragment: 'classroom_url_fragment' - }); + classroom_url_fragment: 'classroom_url_fragment', + } + ); }); it('should get revision tab url', () => { - spyOn(urlInterpolationService, 'interpolateUrl') - .and.returnValue('topic_page'); + spyOn(urlInterpolationService, 'interpolateUrl').and.returnValue( + 'topic_page' + ); spyOn(urlService, 'getUrlParams').and.returnValue({ topic_url_fragment: 'topic_url_fragment', - classroom_url_fragment: 'classroom_url_fragment' + classroom_url_fragment: 'classroom_url_fragment', }); expect(component.getRevisionTabUrl()).toBe('topic_page/revision'); expect(urlInterpolationService.interpolateUrl).toHaveBeenCalledWith( - '/learn//', { + '/learn//', + { topic_url_fragment: 'topic_url_fragment', - classroom_url_fragment: 'classroom_url_fragment' - }); + classroom_url_fragment: 'classroom_url_fragment', + } + ); }); }); diff --git a/core/templates/components/recommendations/post-chapter-recommendations.component.ts b/core/templates/components/recommendations/post-chapter-recommendations.component.ts index fd7eb389a604..c85199d2ac3d 100644 --- a/core/templates/components/recommendations/post-chapter-recommendations.component.ts +++ b/core/templates/components/recommendations/post-chapter-recommendations.component.ts @@ -16,10 +16,10 @@ * @fileoverview Component for the post chapter recommendations component. */ -import { Component, Input } from '@angular/core'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { UrlService } from 'services/contextual/url.service'; -import { PracticeSessionPageConstants } from 'pages/practice-session-page/practice-session-page.constants'; +import {Component, Input} from '@angular/core'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {UrlService} from 'services/contextual/url.service'; +import {PracticeSessionPageConstants} from 'pages/practice-session-page/practice-session-page.constants'; @Component({ selector: 'oppia-post-chapter-recommendations', templateUrl: './post-chapter-recommendations.component.html', @@ -51,20 +51,28 @@ export class PostChapterRecommendationsComponent { } getPracticeTabUrl(): string { - return this.urlInterpolationService.interpolateUrl( - PracticeSessionPageConstants.TOPIC_VIEWER_PAGE, { - topic_url_fragment: this.urlService.getUrlParams().topic_url_fragment, - classroom_url_fragment: ( - this.urlService.getUrlParams().classroom_url_fragment) - }) + '/practice'; + return ( + this.urlInterpolationService.interpolateUrl( + PracticeSessionPageConstants.TOPIC_VIEWER_PAGE, + { + topic_url_fragment: this.urlService.getUrlParams().topic_url_fragment, + classroom_url_fragment: + this.urlService.getUrlParams().classroom_url_fragment, + } + ) + '/practice' + ); } getRevisionTabUrl(): string { - return this.urlInterpolationService.interpolateUrl( - PracticeSessionPageConstants.TOPIC_VIEWER_PAGE, { - topic_url_fragment: this.urlService.getUrlParams().topic_url_fragment, - classroom_url_fragment: ( - this.urlService.getUrlParams().classroom_url_fragment) - }) + '/revision'; + return ( + this.urlInterpolationService.interpolateUrl( + PracticeSessionPageConstants.TOPIC_VIEWER_PAGE, + { + topic_url_fragment: this.urlService.getUrlParams().topic_url_fragment, + classroom_url_fragment: + this.urlService.getUrlParams().classroom_url_fragment, + } + ) + '/revision' + ); } } diff --git a/core/templates/components/recommendations/recommendations.module.ts b/core/templates/components/recommendations/recommendations.module.ts index d36cd7ffdeec..afd8ac5e39a2 100644 --- a/core/templates/components/recommendations/recommendations.module.ts +++ b/core/templates/components/recommendations/recommendations.module.ts @@ -12,28 +12,19 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Module for the recommendations components. */ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { TranslateModule } from '@ngx-translate/core'; +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {TranslateModule} from '@ngx-translate/core'; -import { PostChapterRecommendationsComponent } from './post-chapter-recommendations.component'; +import {PostChapterRecommendationsComponent} from './post-chapter-recommendations.component'; @NgModule({ - imports: [ - CommonModule, - TranslateModule, - ], - declarations: [ - PostChapterRecommendationsComponent, - ], - exports: [ - PostChapterRecommendationsComponent, - ] + imports: [CommonModule, TranslateModule], + declarations: [PostChapterRecommendationsComponent], + exports: [PostChapterRecommendationsComponent], }) - -export class RecommendationsModule { } +export class RecommendationsModule {} diff --git a/core/templates/components/review-material-editor/review-material-editor.component.spec.ts b/core/templates/components/review-material-editor/review-material-editor.component.spec.ts index 59fe2718fcbc..2dd605998766 100644 --- a/core/templates/components/review-material-editor/review-material-editor.component.spec.ts +++ b/core/templates/components/review-material-editor/review-material-editor.component.spec.ts @@ -16,11 +16,11 @@ * @fileoverview Unit tests for Review Material Editor Component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ChangeDetectorRef, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { ReviewMaterialEditorComponent } from './review-material-editor.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ChangeDetectorRef, NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {ReviewMaterialEditorComponent} from './review-material-editor.component'; describe('Review Material Editor Component', () => { let component: ReviewMaterialEditorComponent; @@ -29,13 +29,9 @@ describe('Review Material Editor Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - ReviewMaterialEditorComponent - ], - providers: [ - ChangeDetectorRef - ], - schemas: [NO_ERRORS_SCHEMA] + declarations: [ReviewMaterialEditorComponent], + providers: [ChangeDetectorRef], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -45,36 +41,42 @@ describe('Review Material Editor Component', () => { component.bindableDict = { displayedConceptCardExplanation: 'Explanation', - displayedWorkedExamples: 'Examples' + displayedWorkedExamples: 'Examples', }; fixture.detectChanges(); }); it('should set component properties on initialization', () => { expect(component.HTML_SCHEMA).toEqual({ - type: 'html' + type: 'html', }); expect(component.editableExplanation).toBe('Explanation'); expect(component.conceptCardExplanationEditorIsShown).toBe(false); }); - it('should open concept card explanation editor when user' + - ' clicks to edit concept card', () => { - component.conceptCardExplanationEditorIsShown = false; + it( + 'should open concept card explanation editor when user' + + ' clicks to edit concept card', + () => { + component.conceptCardExplanationEditorIsShown = false; - component.openConceptCardExplanationEditor(); + component.openConceptCardExplanationEditor(); - expect(component.conceptCardExplanationEditorIsShown).toBe(true); - }); + expect(component.conceptCardExplanationEditorIsShown).toBe(true); + } + ); - it('should close concept card explanation editor when user' + - ' clicks on close', () => { - component.conceptCardExplanationEditorIsShown = true; + it( + 'should close concept card explanation editor when user' + + ' clicks on close', + () => { + component.conceptCardExplanationEditorIsShown = true; - component.closeConceptCardExplanationEditor(); + component.closeConceptCardExplanationEditor(); - expect(component.conceptCardExplanationEditorIsShown).toBe(false); - }); + expect(component.conceptCardExplanationEditorIsShown).toBe(false); + } + ); it('should save concept card explanation when user clicks on save', () => { spyOn(component.onSaveExplanation, 'emit'); @@ -82,14 +84,13 @@ describe('Review Material Editor Component', () => { component.saveConceptCardExplanation(); - expect(component.onSaveExplanation.emit) - .toHaveBeenCalledWith(SubtitledHtml.createDefault( - component.editableExplanation, 'explanation')); + expect(component.onSaveExplanation.emit).toHaveBeenCalledWith( + SubtitledHtml.createDefault(component.editableExplanation, 'explanation') + ); }); it('should get schema', () => { - expect(component.getSchema()) - .toEqual(component.HTML_SCHEMA); + expect(component.getSchema()).toEqual(component.HTML_SCHEMA); }); it('should update editableExplanation', () => { diff --git a/core/templates/components/review-material-editor/review-material-editor.component.ts b/core/templates/components/review-material-editor/review-material-editor.component.ts index 2b14cb15c34c..44c6c31751bb 100644 --- a/core/templates/components/review-material-editor/review-material-editor.component.ts +++ b/core/templates/components/review-material-editor/review-material-editor.component.ts @@ -16,27 +16,33 @@ * @fileoverview Component for the skill review material editor. */ -import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { AppConstants } from 'app.constants'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; +import { + ChangeDetectorRef, + Component, + EventEmitter, + Input, + OnInit, + Output, +} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {AppConstants} from 'app.constants'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; interface HtmlSchema { type: 'html'; } interface BindableDict { - 'displayedConceptCardExplanation': string; - 'displayedWorkedExamples': string; + displayedConceptCardExplanation: string; + displayedWorkedExamples: string; } @Component({ selector: 'oppia-review-material-editor', - templateUrl: './review-material-editor.component.html' + templateUrl: './review-material-editor.component.html', }) export class ReviewMaterialEditorComponent implements OnInit { - @Output() onSaveExplanation: - EventEmitter = (new EventEmitter()); + @Output() onSaveExplanation: EventEmitter = new EventEmitter(); // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see @@ -47,18 +53,15 @@ export class ReviewMaterialEditorComponent implements OnInit { COMPONENT_NAME_EXPLANATION!: string; conceptCardExplanationEditorIsShown: boolean = false; HTML_SCHEMA: HtmlSchema = { - type: 'html' + type: 'html', }; - constructor( - private changeDetectorRef: ChangeDetectorRef - ) {} + constructor(private changeDetectorRef: ChangeDetectorRef) {} ngOnInit(): void { - this.COMPONENT_NAME_EXPLANATION = ( - AppConstants.COMPONENT_NAME_EXPLANATION); - this.editableExplanation = ( - this.bindableDict.displayedConceptCardExplanation); + this.COMPONENT_NAME_EXPLANATION = AppConstants.COMPONENT_NAME_EXPLANATION; + this.editableExplanation = + this.bindableDict.displayedConceptCardExplanation; this.conceptCardExplanationEditorIsShown = false; } @@ -88,10 +91,16 @@ export class ReviewMaterialEditorComponent implements OnInit { saveConceptCardExplanation(): void { this.conceptCardExplanationEditorIsShown = false; let explanationObject = SubtitledHtml.createDefault( - this.editableExplanation, this.COMPONENT_NAME_EXPLANATION); + this.editableExplanation, + this.COMPONENT_NAME_EXPLANATION + ); this.onSaveExplanation.emit(explanationObject); } } -angular.module('oppia').directive('oppiaReviewMaterialEditor', - downgradeComponent({component: ReviewMaterialEditorComponent})); +angular + .module('oppia') + .directive( + 'oppiaReviewMaterialEditor', + downgradeComponent({component: ReviewMaterialEditorComponent}) + ); diff --git a/core/templates/components/rubrics-editor/rubrics-editor.component.spec.ts b/core/templates/components/rubrics-editor/rubrics-editor.component.spec.ts index f9710f055a40..e700fda1daa4 100644 --- a/core/templates/components/rubrics-editor/rubrics-editor.component.spec.ts +++ b/core/templates/components/rubrics-editor/rubrics-editor.component.spec.ts @@ -16,12 +16,12 @@ * @fileoverview Unit tests for RubricsEditorComponent. */ -import { ChangeDetectorRef, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { FormsModule } from '@angular/forms'; -import { SkillCreationService } from 'components/entity-creation-services/skill-creation.service'; -import { Rubric } from 'domain/skill/rubric.model'; -import { RubricsEditorComponent } from './rubrics-editor.component'; +import {ChangeDetectorRef, NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {FormsModule} from '@angular/forms'; +import {SkillCreationService} from 'components/entity-creation-services/skill-creation.service'; +import {Rubric} from 'domain/skill/rubric.model'; +import {RubricsEditorComponent} from './rubrics-editor.component'; describe('Rubrics Editor Component', () => { let fixture: ComponentFixture; @@ -30,23 +30,16 @@ describe('Rubrics Editor Component', () => { let rubrics: Rubric[] = [ new Rubric('easy', []), new Rubric(difficulty, []), - new Rubric('hard', []) + new Rubric('hard', []), ]; let skillCreationService: SkillCreationService; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - FormsModule - ], - declarations: [ - RubricsEditorComponent - ], - providers: [ - SkillCreationService, - ChangeDetectorRef - ], - schemas: [NO_ERRORS_SCHEMA] + imports: [FormsModule], + declarations: [RubricsEditorComponent], + providers: [SkillCreationService, ChangeDetectorRef], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -67,8 +60,9 @@ describe('Rubrics Editor Component', () => { }); it('should get schema', () => { - expect(componentInstance.getSchema()) - .toEqual(componentInstance.EXPLANATION_FORM_SCHEMA); + expect(componentInstance.getSchema()).toEqual( + componentInstance.EXPLANATION_FORM_SCHEMA + ); }); it('should get explanation status', () => { @@ -80,54 +74,55 @@ describe('Rubrics Editor Component', () => { let index: number = 2; componentInstance.ngOnInit(); componentInstance.openExplanationEditor(difficulty, index); - expect(componentInstance.explanationEditorIsOpen[difficulty][index]) - .toBeTrue(); + expect( + componentInstance.explanationEditorIsOpen[difficulty][index] + ).toBeTrue(); }); it('should get explanation validation status', () => { let index: number = 2; componentInstance.ngOnInit(); componentInstance.editableExplanations[difficulty][index] = 'not_empty'; - expect(componentInstance.isExplanationValid(difficulty, index)) - .toBeTrue(); + expect(componentInstance.isExplanationValid(difficulty, index)).toBeTrue(); }); it('should check if explanations are at most 300 characters long', () => { let index: number = 2; componentInstance.ngOnInit(); componentInstance.editableExplanations[difficulty][index] = 'a'.repeat(300); - expect(componentInstance.isExplanationLengthValid(difficulty, index)) - .toBeTrue(); + expect( + componentInstance.isExplanationLengthValid(difficulty, index) + ).toBeTrue(); componentInstance.editableExplanations[difficulty][index] = 'a'.repeat(301); - expect(componentInstance.isExplanationLengthValid(difficulty, index)) - .toBeFalse(); - }); - - it('should check if medium level rubrics' + - ' have atleast one explantion', - () => { - let index: number = 0; - expect(componentInstance.isMediumLevelExplanationValid()).toBeFalse; - componentInstance.ngOnInit(); - componentInstance.editableExplanations[difficulty][index] = 'not_empty'; - expect(componentInstance.isMediumLevelExplanationValid()).toBeTrue; - }); - - it('should check if total number of explanations' + - ' have reached the limit', - () => { - let index: number = 0; - componentInstance.ngOnInit(); - componentInstance.editableExplanations[difficulty][index] = - 'not_empty'; - expect(componentInstance.hasReachedExplanationCountLimit()) - .toBeFalse(); - for (let index = 0; index < 11; index++) { - componentInstance.editableExplanations[difficulty].push('not_empty'); + expect( + componentInstance.isExplanationLengthValid(difficulty, index) + ).toBeFalse(); + }); + + it( + 'should check if medium level rubrics' + ' have atleast one explantion', + () => { + let index: number = 0; + expect(componentInstance.isMediumLevelExplanationValid()).toBeFalse; + componentInstance.ngOnInit(); + componentInstance.editableExplanations[difficulty][index] = 'not_empty'; + expect(componentInstance.isMediumLevelExplanationValid()).toBeTrue; } - expect(componentInstance.hasReachedExplanationCountLimit()) - .toBeTrue(); - }); + ); + + it( + 'should check if total number of explanations' + ' have reached the limit', + () => { + let index: number = 0; + componentInstance.ngOnInit(); + componentInstance.editableExplanations[difficulty][index] = 'not_empty'; + expect(componentInstance.hasReachedExplanationCountLimit()).toBeFalse(); + for (let index = 0; index < 11; index++) { + componentInstance.editableExplanations[difficulty].push('not_empty'); + } + expect(componentInstance.hasReachedExplanationCountLimit()).toBeTrue(); + } + ); it('should update explanation', () => { let index: number = 0; @@ -136,8 +131,9 @@ describe('Rubrics Editor Component', () => { componentInstance.editableExplanations[difficulty][index] = ''; componentInstance.rubric = rubrics[1]; componentInstance.updateExplanation(newExplanation, index); - expect(componentInstance.editableExplanations[difficulty][index]) - .toEqual(newExplanation); + expect(componentInstance.editableExplanations[difficulty][index]).toEqual( + newExplanation + ); }); it('should change rubric', () => { @@ -153,18 +149,23 @@ describe('Rubrics Editor Component', () => { spyOn(componentInstance.saveRubric, 'emit'); componentInstance.explanationsMemento[difficulty] = []; componentInstance.explanationsMemento[difficulty][0] = 'different'; - componentInstance - .saveExplanation(componentInstance.skillDifficultyMedium, 0); - expect(skillCreationService.disableSkillDescriptionStatusMarker) - .toHaveBeenCalled(); - expect(componentInstance.explanationEditorIsOpen[difficulty][0]) - .toBeFalse(); + componentInstance.saveExplanation( + componentInstance.skillDifficultyMedium, + 0 + ); + expect( + skillCreationService.disableSkillDescriptionStatusMarker + ).toHaveBeenCalled(); + expect( + componentInstance.explanationEditorIsOpen[difficulty][0] + ).toBeFalse(); expect(componentInstance.saveRubric.emit).toHaveBeenCalledWith({ difficulty: difficulty, - data: componentInstance.editableExplanations[difficulty] + data: componentInstance.editableExplanations[difficulty], }); expect(componentInstance.explanationsMemento[difficulty][0]).toEqual( - componentInstance.editableExplanations[difficulty][0]); + componentInstance.editableExplanations[difficulty][0] + ); }); it('should cancel edit explanation', () => { @@ -173,8 +174,9 @@ describe('Rubrics Editor Component', () => { componentInstance.explanationsMemento[difficulty][0] = ''; componentInstance.cancelEditExplanation(difficulty, 0); expect(componentInstance.deleteExplanation).toHaveBeenCalled(); - expect(componentInstance.explanationEditorIsOpen[difficulty][0]) - .toBeFalse(); + expect( + componentInstance.explanationEditorIsOpen[difficulty][0] + ).toBeFalse(); }); it('should add explanation for difficulty', () => { @@ -183,10 +185,11 @@ describe('Rubrics Editor Component', () => { componentInstance.addExplanationForDifficulty(difficulty); expect(componentInstance.saveRubric.emit).toHaveBeenCalledWith({ difficulty, - data: componentInstance.editableExplanations[difficulty] + data: componentInstance.editableExplanations[difficulty], }); - expect(componentInstance.explanationsMemento) - .toEqual(componentInstance.editableExplanations); + expect(componentInstance.explanationsMemento).toEqual( + componentInstance.editableExplanations + ); }); it('should delete explanation', () => { @@ -194,26 +197,30 @@ describe('Rubrics Editor Component', () => { componentInstance.ngOnInit(); componentInstance.skillDifficultyMedium = difficulty; componentInstance.deleteExplanation(difficulty, 0); - expect(componentInstance.explanationEditorIsOpen[difficulty][0]) - .toBeFalse(); + expect( + componentInstance.explanationEditorIsOpen[difficulty][0] + ).toBeFalse(); expect(componentInstance.saveRubric.emit).toHaveBeenCalledWith({ difficulty, - data: componentInstance.editableExplanations[difficulty] + data: componentInstance.editableExplanations[difficulty], }); - expect(componentInstance.explanationsMemento[difficulty]) - .toEqual(componentInstance.editableExplanations[difficulty]); + expect(componentInstance.explanationsMemento[difficulty]).toEqual( + componentInstance.editableExplanations[difficulty] + ); }); it('should give status of empty explanation', () => { spyOn(componentInstance, 'isExplanationEmpty').and.returnValue(true); componentInstance.explanationsMemento[difficulty] = ['']; - expect(componentInstance.isAnyExplanationEmptyForDifficulty(difficulty)) - .toBeTrue(); + expect( + componentInstance.isAnyExplanationEmptyForDifficulty(difficulty) + ).toBeTrue(); }); it('should give false when explanation is not empty', () => { spyOn(componentInstance, 'isExplanationEmpty').and.returnValue(false); - expect(componentInstance.isAnyExplanationEmptyForDifficulty(difficulty)) - .toBeFalse(); + expect( + componentInstance.isAnyExplanationEmptyForDifficulty(difficulty) + ).toBeFalse(); }); }); diff --git a/core/templates/components/rubrics-editor/rubrics-editor.component.ts b/core/templates/components/rubrics-editor/rubrics-editor.component.ts index f958ea4ac1dc..68aecb5b7941 100644 --- a/core/templates/components/rubrics-editor/rubrics-editor.component.ts +++ b/core/templates/components/rubrics-editor/rubrics-editor.component.ts @@ -16,13 +16,18 @@ * @fileoverview Component for the rubric editor for skills. */ -import { ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { AppConstants } from 'app.constants'; -import { SkillCreationService } from 'components/entity-creation-services/skill-creation.service'; -import { Rubric } from 'domain/skill/rubric.model'; -import { TopicsAndSkillsDashboardPageConstants } from 'pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.constants'; - +import { + ChangeDetectorRef, + Component, + EventEmitter, + Input, + Output, +} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {AppConstants} from 'app.constants'; +import {SkillCreationService} from 'components/entity-creation-services/skill-creation.service'; +import {Rubric} from 'domain/skill/rubric.model'; +import {TopicsAndSkillsDashboardPageConstants} from 'pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.constants'; interface Explanation { [key: string]: string[]; @@ -30,7 +35,7 @@ interface Explanation { interface ExplanationFormSchema { type: string; - 'ui_config': object; + ui_config: object; } interface RubricsOptions { @@ -51,11 +56,10 @@ interface SkillDescriptionStatusValuesInterface { @Component({ selector: 'oppia-rubrics-editor', - templateUrl: './rubrics-editor.component.html' + templateUrl: './rubrics-editor.component.html', }) export class RubricsEditorComponent { - @Output() saveRubric: EventEmitter = ( - new EventEmitter()); + @Output() saveRubric: EventEmitter = new EventEmitter(); // These properties below are initialized using Angular lifecycle hooks // where we need to do non-null assertion. For more information see @@ -66,17 +70,18 @@ export class RubricsEditorComponent { rubricsOptions!: RubricsOptions[]; rubric!: Rubric; - skillDescriptionStatusValues: SkillDescriptionStatusValuesInterface = ( - TopicsAndSkillsDashboardPageConstants.SKILL_DESCRIPTION_STATUS_VALUES); + skillDescriptionStatusValues: SkillDescriptionStatusValuesInterface = + TopicsAndSkillsDashboardPageConstants.SKILL_DESCRIPTION_STATUS_VALUES; - skillDifficultyMedium: string = ( - AppConstants.SKILL_DIFFICULTY_MEDIUM); + skillDifficultyMedium: string = AppConstants.SKILL_DIFFICULTY_MEDIUM; explanationsMemento: Record = {}; explanationEditorIsOpen: Record = {}; editableExplanations: Explanation = {}; - EXPLANATION_FORM_SCHEMA: ExplanationFormSchema = {type: 'html', - ui_config: {}}; + EXPLANATION_FORM_SCHEMA: ExplanationFormSchema = { + type: 'html', + ui_config: {}, + }; maximumNumberofExplanations: number = 10; maximumCharacterLengthOfExplanation: number = 300; @@ -108,16 +113,18 @@ export class RubricsEditorComponent { } isExplanationLengthValid(difficulty: string, idx: number): boolean { - return this.editableExplanations[difficulty][idx].length <= - this.maximumCharacterLengthOfExplanation; + return ( + this.editableExplanations[difficulty][idx].length <= + this.maximumCharacterLengthOfExplanation + ); } isMediumLevelExplanationValid(): boolean { // Checking if medium level rubrics have at least one explantion. return ( this.rubrics[this.MEDIUM_EXPLANATION_INDEX] && - this.rubrics[this.MEDIUM_EXPLANATION_INDEX].getExplanations().length >= - 1); + this.rubrics[this.MEDIUM_EXPLANATION_INDEX].getExplanations().length >= 1 + ); } hasReachedExplanationCountLimit(): boolean { @@ -131,8 +138,9 @@ export class RubricsEditorComponent { } updateExplanation($event: string, idx: number): void { - if (this.editableExplanations[this.rubric.getDifficulty()][idx] !== - $event) { + if ( + this.editableExplanations[this.rubric.getDifficulty()][idx] !== $event + ) { this.editableExplanations[this.rubric.getDifficulty()][idx] = $event; this.changeDetectorRef.detectChanges(); } @@ -143,24 +151,24 @@ export class RubricsEditorComponent { this.skillCreationService.disableSkillDescriptionStatusMarker(); } this.explanationEditorIsOpen[difficulty][index] = false; - let explanationHasChanged: boolean = ( + let explanationHasChanged: boolean = this.editableExplanations[difficulty][index] !== - this.explanationsMemento[difficulty][index]); + this.explanationsMemento[difficulty][index]; if (explanationHasChanged) { const rubricData: RubricData = { difficulty: difficulty, - data: this.editableExplanations[difficulty] + data: this.editableExplanations[difficulty], }; this.saveRubric.emit(rubricData); - this.explanationsMemento[difficulty][index] = ( - this.editableExplanations[difficulty][index]); + this.explanationsMemento[difficulty][index] = + this.editableExplanations[difficulty][index]; } } cancelEditExplanation(difficulty: string, index: number): void { - this.editableExplanations[difficulty][index] = ( - this.explanationsMemento[difficulty][index]); + this.editableExplanations[difficulty][index] = + this.explanationsMemento[difficulty][index]; if (!this.editableExplanations[difficulty][index]) { this.deleteExplanation(difficulty, index); } @@ -171,14 +179,15 @@ export class RubricsEditorComponent { this.editableExplanations[difficulty].push(''); const rubricData: RubricData = { difficulty: difficulty, - data: this.editableExplanations[difficulty] + data: this.editableExplanations[difficulty], }; this.saveRubric.emit(rubricData); this.explanationsMemento[difficulty] = [ - ...this.editableExplanations[difficulty]]; - this.explanationEditorIsOpen[ - difficulty][ - this.editableExplanations[difficulty].length - 1] = true; + ...this.editableExplanations[difficulty], + ]; + this.explanationEditorIsOpen[difficulty][ + this.editableExplanations[difficulty].length - 1 + ] = true; } deleteExplanation(difficulty: string, index: number): void { @@ -189,19 +198,17 @@ export class RubricsEditorComponent { this.editableExplanations[difficulty].splice(index, 1); const rubricData: RubricData = { difficulty: difficulty, - data: this.editableExplanations[difficulty] + data: this.editableExplanations[difficulty], }; this.saveRubric.emit(rubricData); this.explanationsMemento[difficulty] = [ - ...this.editableExplanations[difficulty]]; + ...this.editableExplanations[difficulty], + ]; } - isAnyExplanationEmptyForDifficulty(difficulty: string): boolean { for (let idx in this.explanationsMemento[difficulty]) { - if ( - this.isExplanationEmpty( - this.explanationsMemento[difficulty][idx])) { + if (this.isExplanationEmpty(this.explanationsMemento[difficulty][idx])) { return true; } } @@ -213,14 +220,15 @@ export class RubricsEditorComponent { let explanations = this.rubrics[idx].getExplanations(); let difficulty = this.rubrics[idx].getDifficulty(); this.explanationsMemento[difficulty] = [...explanations]; - this.explanationEditorIsOpen[difficulty] = ( - Array(explanations.length).fill(false)); + this.explanationEditorIsOpen[difficulty] = Array( + explanations.length + ).fill(false); this.editableExplanations[difficulty] = [...explanations]; } this.rubricsOptions = [ {id: 0, difficulty: 'Easy'}, {id: 1, difficulty: 'Medium'}, - {id: 2, difficulty: 'Hard'} + {id: 2, difficulty: 'Hard'}, ]; this.selectedRubricIndex = 1; this.rubric = this.rubrics[1]; @@ -231,5 +239,9 @@ export class RubricsEditorComponent { } } -angular.module('oppia').directive('oppiaRubricsEditor', - downgradeComponent({ component: RubricsEditorComponent })); +angular + .module('oppia') + .directive( + 'oppiaRubricsEditor', + downgradeComponent({component: RubricsEditorComponent}) + ); diff --git a/core/templates/components/save-pending-changes/save-pending-changes-modal.component.spec.ts b/core/templates/components/save-pending-changes/save-pending-changes-modal.component.spec.ts index 3946282f9323..96071b8868a4 100644 --- a/core/templates/components/save-pending-changes/save-pending-changes-modal.component.spec.ts +++ b/core/templates/components/save-pending-changes/save-pending-changes-modal.component.spec.ts @@ -16,9 +16,9 @@ * @fileoverview Unit tests for Save Pending Changes Modal. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { SavePendingChangesModalComponent } from './save-pending-changes-modal.component'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {SavePendingChangesModalComponent} from './save-pending-changes-modal.component'; describe('Save pending changes modal', () => { let componentInstance: SavePendingChangesModalComponent; @@ -27,9 +27,7 @@ describe('Save pending changes modal', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [SavePendingChangesModalComponent], - providers: [ - NgbActiveModal - ] + providers: [NgbActiveModal], }).compileComponents(); })); diff --git a/core/templates/components/save-pending-changes/save-pending-changes-modal.component.ts b/core/templates/components/save-pending-changes/save-pending-changes-modal.component.ts index 4f174eef4667..0349ef103705 100644 --- a/core/templates/components/save-pending-changes/save-pending-changes-modal.component.ts +++ b/core/templates/components/save-pending-changes/save-pending-changes-modal.component.ts @@ -16,25 +16,21 @@ * @fileoverview Component for the save pending changes modal. */ -import { Component, Input } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component, Input} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-save-pending-changes-modal', - templateUrl: './save-pending-changes-modal.component.html' + templateUrl: './save-pending-changes-modal.component.html', }) - -export class SavePendingChangesModalComponent extends - ConfirmOrCancelModal { +export class SavePendingChangesModalComponent extends ConfirmOrCancelModal { // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @Input() body!: string; - constructor( - private ngbActiveModal: NgbActiveModal - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } } diff --git a/core/templates/components/score-ring/score-ring.component.spec.ts b/core/templates/components/score-ring/score-ring.component.spec.ts index 8f1adaf283c0..b57916ffda2f 100644 --- a/core/templates/components/score-ring/score-ring.component.spec.ts +++ b/core/templates/components/score-ring/score-ring.component.spec.ts @@ -16,11 +16,16 @@ * @fileoverview Unit tests for Score Ring Component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA, SimpleChanges } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { ScoreRingComponent } from './score-ring.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA, SimpleChanges} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {ScoreRingComponent} from './score-ring.component'; describe('Score Ring Component', () => { let fixture: ComponentFixture; @@ -28,15 +33,10 @@ describe('Score Ring Component', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - ScoreRingComponent, - MockTranslatePipe - ], + imports: [HttpClientTestingModule], + declarations: [ScoreRingComponent, MockTranslatePipe], providers: [], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); @@ -72,9 +72,9 @@ describe('Score Ring Component', () => { // @ts-expect-error style: { strokeDasharray: '', - strokeDashoffset: '' - } - } + strokeDashoffset: '', + }, + }, }; component.ngAfterViewInit(); @@ -86,26 +86,30 @@ describe('Score Ring Component', () => { component.score = 35; component.testIsPassed = true; - expect(component.getScoreRingColor()) - .toEqual(component.COLORS_FOR_PASS_FAIL_MODE.PASSED_COLOR); + expect(component.getScoreRingColor()).toEqual( + component.COLORS_FOR_PASS_FAIL_MODE.PASSED_COLOR + ); component.testIsPassed = false; - expect(component.getScoreRingColor()) - .toEqual(component.COLORS_FOR_PASS_FAIL_MODE.FAILED_COLOR); + expect(component.getScoreRingColor()).toEqual( + component.COLORS_FOR_PASS_FAIL_MODE.FAILED_COLOR + ); }); it('should get score outer ring color', () => { component.score = 35; component.testIsPassed = true; - expect(component.getScoreOuterRingColor()) - .toEqual(component.COLORS_FOR_PASS_FAIL_MODE.PASSED_COLOR_OUTER); + expect(component.getScoreOuterRingColor()).toEqual( + component.COLORS_FOR_PASS_FAIL_MODE.PASSED_COLOR_OUTER + ); component.testIsPassed = false; - expect(component.getScoreOuterRingColor()) - .toEqual(component.COLORS_FOR_PASS_FAIL_MODE.FAILED_COLOR_OUTER); + expect(component.getScoreOuterRingColor()).toEqual( + component.COLORS_FOR_PASS_FAIL_MODE.FAILED_COLOR_OUTER + ); }); it('should set the new score if it changes', fakeAsync(() => { @@ -114,8 +118,8 @@ describe('Score Ring Component', () => { currentValue: 75, previousValue: 35, firstChange: true, - isFirstChange: () => true - } + isFirstChange: () => true, + }, }; component.scoreRingElement = { nativeElement: { @@ -141,9 +145,9 @@ describe('Score Ring Component', () => { // @ts-expect-error style: { strokeDasharray: '', - strokeDashoffset: '' - } - } + strokeDashoffset: '', + }, + }, }; expect(component.score).toEqual(35); @@ -152,7 +156,8 @@ describe('Score Ring Component', () => { component.ngOnChanges(changes); tick(2000); - expect(Math.round(parseFloat(component.circle.style.strokeDashoffset))) - .toEqual(196); + expect( + Math.round(parseFloat(component.circle.style.strokeDashoffset)) + ).toEqual(196); })); }); diff --git a/core/templates/components/score-ring/score-ring.component.ts b/core/templates/components/score-ring/score-ring.component.ts index c0b214a58295..c99c300ac6db 100644 --- a/core/templates/components/score-ring/score-ring.component.ts +++ b/core/templates/components/score-ring/score-ring.component.ts @@ -16,13 +16,21 @@ * @fileoverview Component for the animated score ring. */ -import { Component, Input, OnChanges, AfterViewInit, ViewChild, ElementRef, SimpleChanges } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { QuestionPlayerConstants } from '../question-directives/question-player/question-player.constants'; +import { + Component, + Input, + OnChanges, + AfterViewInit, + ViewChild, + ElementRef, + SimpleChanges, +} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {QuestionPlayerConstants} from '../question-directives/question-player/question-player.constants'; @Component({ selector: 'oppia-score-ring', - templateUrl: './score-ring.component.html' + templateUrl: './score-ring.component.html', }) export class ScoreRingComponent implements AfterViewInit, OnChanges { constructor() {} @@ -39,7 +47,7 @@ export class ScoreRingComponent implements AfterViewInit, OnChanges { setScore(percent: number): void { setTimeout(() => { - const offset = this.circumference - percent / 100 * this.circumference; + const offset = this.circumference - (percent / 100) * this.circumference; this.circle.style.strokeDashoffset = offset.toString(); }, 2000); } @@ -75,10 +83,9 @@ export class ScoreRingComponent implements AfterViewInit, OnChanges { ngAfterViewInit(): void { this.circle = this.scoreRingElement.nativeElement; this.radius = this.circle.r.baseVal.value; - this.circumference = (this.radius * 2 * Math.PI); + this.circumference = this.radius * 2 * Math.PI; - this.circle.style.strokeDasharray = ( - `${this.circumference} ${this.circumference}`); + this.circle.style.strokeDasharray = `${this.circumference} ${this.circumference}`; this.circle.style.strokeDashoffset = this.circumference.toString(); // A reflow needs to be triggered so that the browser picks up the // assigned starting values of stroke-dashoffset and stroke-dasharray before @@ -92,6 +99,8 @@ export class ScoreRingComponent implements AfterViewInit, OnChanges { } angular.module('oppia').directive( - 'oppiaScoreRing', downgradeComponent({ - component: ScoreRingComponent - }) as angular.IDirectiveFactory); + 'oppiaScoreRing', + downgradeComponent({ + component: ScoreRingComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/components/shared-component.module.ts b/core/templates/components/shared-component.module.ts index 1a2093061a8d..2fa70d647bd5 100644 --- a/core/templates/components/shared-component.module.ts +++ b/core/templates/components/shared-component.module.ts @@ -19,170 +19,173 @@ import 'core-js/es7/reflect'; import 'zone.js'; // Modules. -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { AngularFireModule } from '@angular/fire'; -import { AngularFireAuth, AngularFireAuthModule, USE_EMULATOR } from '@angular/fire/auth'; -import { CustomFormsComponentsModule } from './forms/custom-forms-directives/custom-form-components.module'; -import { DynamicContentModule } from './interaction-display/dynamic-content.module'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { MaterialModule } from 'modules/material.module'; -import { ObjectComponentsModule } from 'objects/object-components.module'; -import { SharedFormsModule } from './forms/shared-forms.module'; -import { RecommendationsModule } from './recommendations/recommendations.module'; -import { CommonElementsModule } from './common-layout-directives/common-elements/common-elements.module'; -import { RichTextComponentsModule } from 'rich_text_components/rich-text-components.module'; -import { CodeMirrorModule } from './code-mirror/codemirror.module'; -import { OppiaCkEditor4Module } from './ck-editor-helpers/ckeditor4.module'; -import { BaseModule } from 'base-components/base.module'; -import { NgBootstrapModule } from 'modules/ng-boostrap.module'; -import { DragDropModule } from '@angular/cdk/drag-drop'; +import {CommonModule} from '@angular/common'; +import {NgModule} from '@angular/core'; +import {AngularFireModule} from '@angular/fire'; +import { + AngularFireAuth, + AngularFireAuthModule, + USE_EMULATOR, +} from '@angular/fire/auth'; +import {CustomFormsComponentsModule} from './forms/custom-forms-directives/custom-form-components.module'; +import {DynamicContentModule} from './interaction-display/dynamic-content.module'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {MaterialModule} from 'modules/material.module'; +import {ObjectComponentsModule} from 'objects/object-components.module'; +import {SharedFormsModule} from './forms/shared-forms.module'; +import {RecommendationsModule} from './recommendations/recommendations.module'; +import {CommonElementsModule} from './common-layout-directives/common-elements/common-elements.module'; +import {RichTextComponentsModule} from 'rich_text_components/rich-text-components.module'; +import {CodeMirrorModule} from './code-mirror/codemirror.module'; +import {OppiaCkEditor4Module} from './ck-editor-helpers/ckeditor4.module'; +import {BaseModule} from 'base-components/base.module'; +import {NgBootstrapModule} from 'modules/ng-boostrap.module'; +import {DragDropModule} from '@angular/cdk/drag-drop'; // Components. -import { AudioBarComponent } from 'pages/exploration-player-page/layout-directives/audio-bar.component'; -import { DeleteAnswerGroupModalComponent } from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-answer-group-modal.component'; -import { ExplorationEmbedButtonModalComponent } from './button-directives/exploration-embed-button-modal.component'; -import { CheckpointCelebrationModalComponent } from './checkpoint-celebration-modal/checkpoint-celebration-modal.component'; -import { BackgroundBannerModule } from './common-layout-directives/common-elements/background-banner.module'; -import { AttributionGuideComponent } from './common-layout-directives/common-elements/attribution-guide.component'; -import { LazyLoadingComponent } from './common-layout-directives/common-elements/lazy-loading.component'; -import { KeyboardShortcutHelpModalComponent } from 'components/keyboard-shortcut-help/keyboard-shortcut-help-modal.component'; -import { StateSkillEditorComponent } from 'components/state-editor/state-skill-editor/state-skill-editor.component'; -import { SelectSkillModalComponent } from './skill-selector/select-skill-modal.component'; -import { SharingLinksComponent } from './common-layout-directives/common-elements/sharing-links.component'; -import { SkillSelectorComponent } from './skill-selector/skill-selector.component'; -import { ProfileLinkImageComponent } from 'components/profile-link-directives/profile-link-image.component'; -import { ProfileLinkTextComponent } from 'components/profile-link-directives/profile-link-text.component'; -import { AudioFileUploaderComponent } from './forms/custom-forms-directives/audio-file-uploader.component'; -import { ThumbnailDisplayComponent } from './forms/custom-forms-directives/thumbnail-display.component'; -import { SkillMasteryViewerComponent } from './skill-mastery/skill-mastery.component'; -import { ExplorationSummaryTileComponent } from './summary-tile/exploration-summary-tile.component'; -import { PracticeTabComponent } from 'pages/topic-viewer-page/practice-tab/practice-tab.component'; -import { CollectionSummaryTileComponent } from './summary-tile/collection-summary-tile.component'; -import { TakeBreakModalComponent } from 'pages/exploration-player-page/templates/take-break-modal.component'; -import { TopicsAndSkillsDashboardNavbarBreadcrumbComponent } from 'pages/topics-and-skills-dashboard-page/navbar/topics-and-skills-dashboard-navbar-breadcrumb.component'; -import { ThreadTableComponent } from 'pages/exploration-editor-page/feedback-tab/thread-table/thread-table.component'; -import { SummaryListHeaderComponent } from './state-directives/answer-group-editor/summary-list-header.component'; -import { LearnerDashboardIconsComponent } from 'pages/learner-dashboard-page/learner-dashboard-icons.component'; -import { OutcomeEditorComponent } from './state-directives/outcome-editor/outcome-editor.component'; -import { OutcomeFeedbackEditorComponent } from './state-directives/outcome-editor/outcome-feedback-editor.component'; -import { OnScreenKeyboardComponent } from './on-screen-keyboard/on-screen-keyboard.component'; -import { RubricsEditorComponent } from './rubrics-editor/rubrics-editor.component'; -import { CreateNewSkillModalComponent } from 'pages/topics-and-skills-dashboard-page/modals/create-new-skill-modal.component'; -import { CreateActivityModalComponent } from 'pages/creator-dashboard-page/modal-templates/create-activity-modal.component'; -import { UploadActivityModalComponent } from 'pages/creator-dashboard-page/modal-templates/upload-activity-modal.component'; -import { ThumbnailUploaderComponent } from './forms/custom-forms-directives/thumbnail-uploader.component'; -import { EditThumbnailModalComponent } from './forms/custom-forms-directives/edit-thumbnail-modal.component'; -import { CorrectnessFooterComponent } from 'pages/exploration-player-page/layout-directives/correctness-footer.component'; -import { ContinueButtonComponent } from 'pages/exploration-player-page/learner-experience/continue-button.component'; -import { DeleteInteractionModalComponent } from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-interaction-modal.component'; -import { DeleteHintModalComponent } from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-hint-modal.component'; -import { DeleteLastHintModalComponent } from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-last-hint-modal.component'; -import { DeleteSolutionModalComponent } from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-solution-modal.component'; -import { ProgressNavComponent } from 'pages/exploration-player-page/layout-directives/progress-nav.component'; -import { QuestionDifficultySelectorComponent } from './question-difficulty-selector/question-difficulty-selector.component'; -import { PreviewThumbnailComponent } from 'pages/topic-editor-page/modal-templates/preview-thumbnail.component'; -import { InputResponsePairComponent } from 'pages/exploration-player-page/learner-experience/input-response-pair.component'; -import { StorySummaryTileComponent } from './summary-tile/story-summary-tile.component'; -import { ExplorationFooterComponent } from 'pages/exploration-player-page/layout-directives/exploration-footer.component'; -import { DisplaySolutionModalComponent } from 'pages/exploration-player-page/modals/display-solution-modal.component'; -import { DisplaySolutionInterstititalModalComponent } from 'pages/exploration-player-page/modals/display-solution-interstitial-modal.component'; -import { DisplayHintModalComponent } from 'pages/exploration-player-page/modals/display-hint-modal.component'; -import { HintAndSolutionButtonsComponent } from './button-directives/hint-and-solution-buttons.component'; -import { SearchBarModule } from 'pages/library-page/search-bar/search-bar.module'; -import { SubtopicSummaryTileComponent } from './summary-tile/subtopic-summary-tile.component'; -import { FilteredChoicesFieldComponent } from './filter-fields/filtered-choices-field/filtered-choices-field.component'; -import { MultiSelectionFieldComponent } from './filter-fields/multi-selection-field/multi-selection-field.component'; -import { ConceptCardComponent } from './concept-card/concept-card.component'; -import { ScoreRingComponent } from './score-ring/score-ring.component'; -import { CompletionGraphComponent } from './statistics-directives/completion-graph.component'; -import { TutorCardComponent } from 'pages/exploration-player-page/learner-experience/tutor-card.component'; -import { ContentLanguageSelectorComponent } from 'pages/exploration-player-page/layout-directives/content-language-selector.component'; -import { RatingDisplayComponent } from './ratings/rating-display/rating-display.component'; -import { SupplementalCardComponent } from 'pages/exploration-player-page/learner-experience/supplemental-card.component'; -import { AddOrUpdateSolutionModalComponent } from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/add-or-update-solution-modal.component'; -import { SavePendingChangesModalComponent } from './save-pending-changes/save-pending-changes-modal.component'; -import { AddHintModalComponent } from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/add-hint-modal.component'; -import { QuestionMisconceptionSelectorComponent } from './question-directives/question-misconception-selector/question-misconception-selector.component'; -import { ConversationSkinComponent } from 'pages/exploration-player-page/learner-experience/conversation-skin.component'; -import { EndChapterCheckMarkComponent } from 'pages/exploration-player-page/learner-experience/end-chapter-check-mark.component'; -import { EndChapterConfettiComponent } from 'pages/exploration-player-page/learner-experience/end-chapter-confetti.component'; -import { RatingsAndRecommendationsComponent } from 'pages/exploration-player-page/learner-experience/ratings-and-recommendations.component'; -import { LearnerAnswerInfoCard } from 'pages/exploration-player-page/learner-experience/learner-answer-info-card.component'; -import { FeedbackPopupComponent } from 'pages/exploration-player-page/layout-directives/feedback-popup.component'; -import { ConfirmQuestionExitModalComponent } from './question-directives/modal-templates/confirm-question-exit-modal.component'; -import { QuestionsOpportunitiesSelectDifficultyModalComponent } from 'pages/topic-editor-page/modal-templates/questions-opportunities-select-difficulty-modal.component'; -import { QuestionsListSelectSkillAndDifficultyModalComponent } from 'pages/topic-editor-page/modal-templates/questions-list-select-skill-and-difficulty-modal.component'; -import { QuestionEditorSaveModalComponent } from './question-directives/modal-templates/question-editor-save-modal.component'; -import { HintEditorComponent } from 'components/state-directives/hint-editor/hint-editor.component'; -import { ResponseHeaderComponent } from './state-directives/response-header/response-header.component'; -import { StateContentEditorComponent } from './state-editor/state-content-editor/state-content-editor.component'; -import { StateHintsEditorComponent } from 'components/state-editor/state-hints-editor/state-hints-editor.component'; -import { ReviewMaterialEditorComponent } from './review-material-editor/review-material-editor.component'; -import { ConfirmLeaveModalComponent } from 'pages/exploration-editor-page/modal-templates/confirm-leave-modal.component'; -import { CustomizeInteractionModalComponent } from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/customize-interaction-modal.component'; -import { TagMisconceptionModalComponent } from './question-directives/question-misconception-editor/tag-misconception-modal-component'; -import { QuestionMisconceptionEditorComponent } from './question-directives/question-misconception-editor/question-misconception-editor.component'; -import { SolutionExplanationEditor } from './state-directives/solution-editor/solution-explanation-editor.component'; -import { SolutionEditor } from './state-directives/solution-editor/solution-editor.component'; -import { OutcomeDestinationEditorComponent } from './state-directives/outcome-editor/outcome-destination-editor.component'; -import { OutcomeIfStuckDestinationEditorComponent } from './state-directives/outcome-editor/outcome-if-stuck-destination-editor.component'; -import { StateSolutionEditorComponent } from './state-editor/state-solution-editor/state-solution-editor.component'; -import { StateInteractionEditorComponent } from './state-editor/state-interaction-editor/state-interaction-editor.component'; -import { TrainingPanelComponent } from 'pages/exploration-editor-page/editor-tab/training-panel/training-panel.component'; -import { TrainingModalComponent } from 'pages/exploration-editor-page/editor-tab/training-panel/training-modal.component'; -import { TrainingDataEditorPanelComponent } from 'pages/exploration-editor-page/editor-tab/training-panel/training-data-editor-panel-modal.component'; -import { TestInteractionPanel } from 'pages/exploration-editor-page/editor-tab/test-interaction-panel/test-interaction-panel.component'; -import { RuleEditorComponent } from './state-directives/rule-editor/rule-editor.component'; -import { HtmlSelectComponent } from './forms/custom-forms-directives/html-select.component'; -import { RuleTypeSelector } from './state-directives/rule-editor/rule-type-selector.directive'; -import { AddAnswerGroupModalComponent } from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/add-answer-group-modal.component'; -import { AnswerGroupEditor } from './state-directives/answer-group-editor/answer-group-editor.component'; -import { StateResponsesComponent } from './state-editor/state-responses-editor/state-responses.component'; -import { StateEditorComponent } from './state-editor/state-editor.component'; -import { QuestionEditorComponent } from './question-directives/question-editor/question-editor.component'; -import { QuestionPlayerConceptCardModalComponent } from './question-directives/question-player/question-player-concept-card-modal.component'; -import { QuestionPlayerComponent } from './question-directives/question-player/question-player.component'; -import { QuestionsListComponent } from './question-directives/questions-list/questions-list.component'; -import { RemoveQuestionSkillLinkModalComponent } from './question-directives/modal-templates/remove-question-skill-link-modal.component'; -import { SkillMasteryModalComponent } from './question-directives/question-player/skill-mastery-modal.component'; -import { StateGraphVisualization } from 'pages/exploration-editor-page/editor-tab/graph-directives/state-graph-visualization.component'; -import { VersionDiffVisualizationComponent } from './version-diff-visualization/version-diff-visualization.component'; -import { QuestionSuggestionEditorModalComponent } from 'pages/contributor-dashboard-page/modal-templates/question-suggestion-editor-modal.component'; -import { QuestionSuggestionReviewModalComponent } from 'pages/contributor-dashboard-page/modal-templates/question-suggestion-review-modal.component'; -import { ReviewTestPageComponent } from 'pages/review-test-page/review-test-page.component'; -import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; -import { AddOutcomeModalComponent } from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/add-outcome-modal.component'; -import { AnswerContentModalComponent } from './common-layout-directives/common-elements/answer-content-modal.component'; -import { VisualizationSortedTilesComponent } from 'visualizations/oppia-visualization-sorted-tiles.component'; -import { OppiaVisualizationClickHexbinsComponent } from 'visualizations/oppia-visualization-click-hexbins.directive'; -import { OppiaVisualizationFrequencyTableComponent } from 'visualizations/oppia-visualization-frequency-table.directive'; -import { OppiaVisualizationEnumeratedFrequencyTableComponent } from 'visualizations/oppia-visualization-enumerated-frequency-table.directive'; -import { RandomSelectorComponent } from 'value_generators/templates/random-selector.component'; -import { CopierComponent } from 'value_generators/templates/copier.component'; -import { PrimaryButtonComponent } from './button-directives/primary-button.component'; +import {AudioBarComponent} from 'pages/exploration-player-page/layout-directives/audio-bar.component'; +import {DeleteAnswerGroupModalComponent} from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-answer-group-modal.component'; +import {ExplorationEmbedButtonModalComponent} from './button-directives/exploration-embed-button-modal.component'; +import {CheckpointCelebrationModalComponent} from './checkpoint-celebration-modal/checkpoint-celebration-modal.component'; +import {BackgroundBannerModule} from './common-layout-directives/common-elements/background-banner.module'; +import {AttributionGuideComponent} from './common-layout-directives/common-elements/attribution-guide.component'; +import {LazyLoadingComponent} from './common-layout-directives/common-elements/lazy-loading.component'; +import {KeyboardShortcutHelpModalComponent} from 'components/keyboard-shortcut-help/keyboard-shortcut-help-modal.component'; +import {StateSkillEditorComponent} from 'components/state-editor/state-skill-editor/state-skill-editor.component'; +import {SelectSkillModalComponent} from './skill-selector/select-skill-modal.component'; +import {SharingLinksComponent} from './common-layout-directives/common-elements/sharing-links.component'; +import {SkillSelectorComponent} from './skill-selector/skill-selector.component'; +import {ProfileLinkImageComponent} from 'components/profile-link-directives/profile-link-image.component'; +import {ProfileLinkTextComponent} from 'components/profile-link-directives/profile-link-text.component'; +import {AudioFileUploaderComponent} from './forms/custom-forms-directives/audio-file-uploader.component'; +import {ThumbnailDisplayComponent} from './forms/custom-forms-directives/thumbnail-display.component'; +import {SkillMasteryViewerComponent} from './skill-mastery/skill-mastery.component'; +import {ExplorationSummaryTileComponent} from './summary-tile/exploration-summary-tile.component'; +import {PracticeTabComponent} from 'pages/topic-viewer-page/practice-tab/practice-tab.component'; +import {CollectionSummaryTileComponent} from './summary-tile/collection-summary-tile.component'; +import {TakeBreakModalComponent} from 'pages/exploration-player-page/templates/take-break-modal.component'; +import {TopicsAndSkillsDashboardNavbarBreadcrumbComponent} from 'pages/topics-and-skills-dashboard-page/navbar/topics-and-skills-dashboard-navbar-breadcrumb.component'; +import {ThreadTableComponent} from 'pages/exploration-editor-page/feedback-tab/thread-table/thread-table.component'; +import {SummaryListHeaderComponent} from './state-directives/answer-group-editor/summary-list-header.component'; +import {LearnerDashboardIconsComponent} from 'pages/learner-dashboard-page/learner-dashboard-icons.component'; +import {OutcomeEditorComponent} from './state-directives/outcome-editor/outcome-editor.component'; +import {OutcomeFeedbackEditorComponent} from './state-directives/outcome-editor/outcome-feedback-editor.component'; +import {OnScreenKeyboardComponent} from './on-screen-keyboard/on-screen-keyboard.component'; +import {RubricsEditorComponent} from './rubrics-editor/rubrics-editor.component'; +import {CreateNewSkillModalComponent} from 'pages/topics-and-skills-dashboard-page/modals/create-new-skill-modal.component'; +import {CreateActivityModalComponent} from 'pages/creator-dashboard-page/modal-templates/create-activity-modal.component'; +import {UploadActivityModalComponent} from 'pages/creator-dashboard-page/modal-templates/upload-activity-modal.component'; +import {ThumbnailUploaderComponent} from './forms/custom-forms-directives/thumbnail-uploader.component'; +import {EditThumbnailModalComponent} from './forms/custom-forms-directives/edit-thumbnail-modal.component'; +import {CorrectnessFooterComponent} from 'pages/exploration-player-page/layout-directives/correctness-footer.component'; +import {ContinueButtonComponent} from 'pages/exploration-player-page/learner-experience/continue-button.component'; +import {DeleteInteractionModalComponent} from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-interaction-modal.component'; +import {DeleteHintModalComponent} from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-hint-modal.component'; +import {DeleteLastHintModalComponent} from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-last-hint-modal.component'; +import {DeleteSolutionModalComponent} from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-solution-modal.component'; +import {ProgressNavComponent} from 'pages/exploration-player-page/layout-directives/progress-nav.component'; +import {QuestionDifficultySelectorComponent} from './question-difficulty-selector/question-difficulty-selector.component'; +import {PreviewThumbnailComponent} from 'pages/topic-editor-page/modal-templates/preview-thumbnail.component'; +import {InputResponsePairComponent} from 'pages/exploration-player-page/learner-experience/input-response-pair.component'; +import {StorySummaryTileComponent} from './summary-tile/story-summary-tile.component'; +import {ExplorationFooterComponent} from 'pages/exploration-player-page/layout-directives/exploration-footer.component'; +import {DisplaySolutionModalComponent} from 'pages/exploration-player-page/modals/display-solution-modal.component'; +import {DisplaySolutionInterstititalModalComponent} from 'pages/exploration-player-page/modals/display-solution-interstitial-modal.component'; +import {DisplayHintModalComponent} from 'pages/exploration-player-page/modals/display-hint-modal.component'; +import {HintAndSolutionButtonsComponent} from './button-directives/hint-and-solution-buttons.component'; +import {SearchBarModule} from 'pages/library-page/search-bar/search-bar.module'; +import {SubtopicSummaryTileComponent} from './summary-tile/subtopic-summary-tile.component'; +import {FilteredChoicesFieldComponent} from './filter-fields/filtered-choices-field/filtered-choices-field.component'; +import {MultiSelectionFieldComponent} from './filter-fields/multi-selection-field/multi-selection-field.component'; +import {ConceptCardComponent} from './concept-card/concept-card.component'; +import {ScoreRingComponent} from './score-ring/score-ring.component'; +import {CompletionGraphComponent} from './statistics-directives/completion-graph.component'; +import {TutorCardComponent} from 'pages/exploration-player-page/learner-experience/tutor-card.component'; +import {ContentLanguageSelectorComponent} from 'pages/exploration-player-page/layout-directives/content-language-selector.component'; +import {RatingDisplayComponent} from './ratings/rating-display/rating-display.component'; +import {SupplementalCardComponent} from 'pages/exploration-player-page/learner-experience/supplemental-card.component'; +import {AddOrUpdateSolutionModalComponent} from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/add-or-update-solution-modal.component'; +import {SavePendingChangesModalComponent} from './save-pending-changes/save-pending-changes-modal.component'; +import {AddHintModalComponent} from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/add-hint-modal.component'; +import {QuestionMisconceptionSelectorComponent} from './question-directives/question-misconception-selector/question-misconception-selector.component'; +import {ConversationSkinComponent} from 'pages/exploration-player-page/learner-experience/conversation-skin.component'; +import {EndChapterCheckMarkComponent} from 'pages/exploration-player-page/learner-experience/end-chapter-check-mark.component'; +import {EndChapterConfettiComponent} from 'pages/exploration-player-page/learner-experience/end-chapter-confetti.component'; +import {RatingsAndRecommendationsComponent} from 'pages/exploration-player-page/learner-experience/ratings-and-recommendations.component'; +import {LearnerAnswerInfoCard} from 'pages/exploration-player-page/learner-experience/learner-answer-info-card.component'; +import {FeedbackPopupComponent} from 'pages/exploration-player-page/layout-directives/feedback-popup.component'; +import {ConfirmQuestionExitModalComponent} from './question-directives/modal-templates/confirm-question-exit-modal.component'; +import {QuestionsOpportunitiesSelectDifficultyModalComponent} from 'pages/topic-editor-page/modal-templates/questions-opportunities-select-difficulty-modal.component'; +import {QuestionsListSelectSkillAndDifficultyModalComponent} from 'pages/topic-editor-page/modal-templates/questions-list-select-skill-and-difficulty-modal.component'; +import {QuestionEditorSaveModalComponent} from './question-directives/modal-templates/question-editor-save-modal.component'; +import {HintEditorComponent} from 'components/state-directives/hint-editor/hint-editor.component'; +import {ResponseHeaderComponent} from './state-directives/response-header/response-header.component'; +import {StateContentEditorComponent} from './state-editor/state-content-editor/state-content-editor.component'; +import {StateHintsEditorComponent} from 'components/state-editor/state-hints-editor/state-hints-editor.component'; +import {ReviewMaterialEditorComponent} from './review-material-editor/review-material-editor.component'; +import {ConfirmLeaveModalComponent} from 'pages/exploration-editor-page/modal-templates/confirm-leave-modal.component'; +import {CustomizeInteractionModalComponent} from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/customize-interaction-modal.component'; +import {TagMisconceptionModalComponent} from './question-directives/question-misconception-editor/tag-misconception-modal-component'; +import {QuestionMisconceptionEditorComponent} from './question-directives/question-misconception-editor/question-misconception-editor.component'; +import {SolutionExplanationEditor} from './state-directives/solution-editor/solution-explanation-editor.component'; +import {SolutionEditor} from './state-directives/solution-editor/solution-editor.component'; +import {OutcomeDestinationEditorComponent} from './state-directives/outcome-editor/outcome-destination-editor.component'; +import {OutcomeIfStuckDestinationEditorComponent} from './state-directives/outcome-editor/outcome-if-stuck-destination-editor.component'; +import {StateSolutionEditorComponent} from './state-editor/state-solution-editor/state-solution-editor.component'; +import {StateInteractionEditorComponent} from './state-editor/state-interaction-editor/state-interaction-editor.component'; +import {TrainingPanelComponent} from 'pages/exploration-editor-page/editor-tab/training-panel/training-panel.component'; +import {TrainingModalComponent} from 'pages/exploration-editor-page/editor-tab/training-panel/training-modal.component'; +import {TrainingDataEditorPanelComponent} from 'pages/exploration-editor-page/editor-tab/training-panel/training-data-editor-panel-modal.component'; +import {TestInteractionPanel} from 'pages/exploration-editor-page/editor-tab/test-interaction-panel/test-interaction-panel.component'; +import {RuleEditorComponent} from './state-directives/rule-editor/rule-editor.component'; +import {HtmlSelectComponent} from './forms/custom-forms-directives/html-select.component'; +import {RuleTypeSelector} from './state-directives/rule-editor/rule-type-selector.directive'; +import {AddAnswerGroupModalComponent} from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/add-answer-group-modal.component'; +import {AnswerGroupEditor} from './state-directives/answer-group-editor/answer-group-editor.component'; +import {StateResponsesComponent} from './state-editor/state-responses-editor/state-responses.component'; +import {StateEditorComponent} from './state-editor/state-editor.component'; +import {QuestionEditorComponent} from './question-directives/question-editor/question-editor.component'; +import {QuestionPlayerConceptCardModalComponent} from './question-directives/question-player/question-player-concept-card-modal.component'; +import {QuestionPlayerComponent} from './question-directives/question-player/question-player.component'; +import {QuestionsListComponent} from './question-directives/questions-list/questions-list.component'; +import {RemoveQuestionSkillLinkModalComponent} from './question-directives/modal-templates/remove-question-skill-link-modal.component'; +import {SkillMasteryModalComponent} from './question-directives/question-player/skill-mastery-modal.component'; +import {StateGraphVisualization} from 'pages/exploration-editor-page/editor-tab/graph-directives/state-graph-visualization.component'; +import {VersionDiffVisualizationComponent} from './version-diff-visualization/version-diff-visualization.component'; +import {QuestionSuggestionEditorModalComponent} from 'pages/contributor-dashboard-page/modal-templates/question-suggestion-editor-modal.component'; +import {QuestionSuggestionReviewModalComponent} from 'pages/contributor-dashboard-page/modal-templates/question-suggestion-review-modal.component'; +import {ReviewTestPageComponent} from 'pages/review-test-page/review-test-page.component'; +import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; +import {AddOutcomeModalComponent} from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/add-outcome-modal.component'; +import {AnswerContentModalComponent} from './common-layout-directives/common-elements/answer-content-modal.component'; +import {VisualizationSortedTilesComponent} from 'visualizations/oppia-visualization-sorted-tiles.component'; +import {OppiaVisualizationClickHexbinsComponent} from 'visualizations/oppia-visualization-click-hexbins.directive'; +import {OppiaVisualizationFrequencyTableComponent} from 'visualizations/oppia-visualization-frequency-table.directive'; +import {OppiaVisualizationEnumeratedFrequencyTableComponent} from 'visualizations/oppia-visualization-enumerated-frequency-table.directive'; +import {RandomSelectorComponent} from 'value_generators/templates/random-selector.component'; +import {CopierComponent} from 'value_generators/templates/copier.component'; +import {PrimaryButtonComponent} from './button-directives/primary-button.component'; // Pipes. -import { StringUtilityPipesModule } from 'filters/string-utility-filters/string-utility-pipes.module'; -import { SummarizeNonnegativeNumberPipe } from 'filters/summarize-nonnegative-number.pipe'; - +import {StringUtilityPipesModule} from 'filters/string-utility-filters/string-utility-pipes.module'; +import {SummarizeNonnegativeNumberPipe} from 'filters/summarize-nonnegative-number.pipe'; // Services. -import { AuthService } from 'services/auth.service'; +import {AuthService} from 'services/auth.service'; // Miscellaneous. -import { JoyrideModule } from 'ngx-joyride'; -import { SmartRouterModule } from 'hybrid-router-module-provider'; -import { StaleTabInfoModalComponent } from './stale-tab-info/stale-tab-info-modal.component'; -import { UnsavedChangesStatusInfoModalComponent } from './unsaved-changes-status-info/unsaved-changes-status-info-modal.component'; -import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap'; -import { TranslateModule } from '@ngx-translate/core'; -import { ComponentOverviewComponent } from './copy-url/copy-url.component'; -import { MatMenuModule} from '@angular/material/menu'; -import { DynamicComponentModule } from 'value_generators/templates/dynamic-component.module'; -import { ThanksForDonatingModalComponent } from 'pages/donate-page/thanks-for-donating-modal.component'; -import { RteHelperModalComponent } from 'services/rte-helper-modal.controller'; -import { DirectivesModule } from 'directives/directives.module'; +import {JoyrideModule} from 'ngx-joyride'; +import {SmartRouterModule} from 'hybrid-router-module-provider'; +import {StaleTabInfoModalComponent} from './stale-tab-info/stale-tab-info-modal.component'; +import {UnsavedChangesStatusInfoModalComponent} from './unsaved-changes-status-info/unsaved-changes-status-info-modal.component'; +import {NgbModalModule} from '@ng-bootstrap/ng-bootstrap'; +import {TranslateModule} from '@ngx-translate/core'; +import {ComponentOverviewComponent} from './copy-url/copy-url.component'; +import {MatMenuModule} from '@angular/material/menu'; +import {DynamicComponentModule} from 'value_generators/templates/dynamic-component.module'; +import {ThanksForDonatingModalComponent} from 'pages/donate-page/thanks-for-donating-modal.component'; +import {RteHelperModalComponent} from 'services/rte-helper-modal.controller'; +import {DirectivesModule} from 'directives/directives.module'; @NgModule({ imports: [ @@ -217,15 +220,15 @@ import { DirectivesModule } from 'directives/directives.module'; NgbModalModule, TranslateModule, DynamicComponentModule, - DirectivesModule + DirectivesModule, ], providers: [ AngularFireAuth, { provide: USE_EMULATOR, - useValue: AuthService.firebaseEmulatorConfig - } + useValue: AuthService.firebaseEmulatorConfig, + }, ], declarations: [ @@ -354,7 +357,7 @@ import { DirectivesModule } from 'directives/directives.module'; ComponentOverviewComponent, VisualizationSortedTilesComponent, RteHelperModalComponent, - PrimaryButtonComponent + PrimaryButtonComponent, ], entryComponents: [ @@ -386,7 +389,8 @@ import { DirectivesModule } from 'directives/directives.module'; QuestionEditorSaveModalComponent, CollectionSummaryTileComponent, SharingLinksComponent, - SkillMasteryViewerComponent, AttributionGuideComponent, + SkillMasteryViewerComponent, + AttributionGuideComponent, LazyLoadingComponent, OnScreenKeyboardComponent, ProfileLinkImageComponent, @@ -485,7 +489,7 @@ import { DirectivesModule } from 'directives/directives.module'; CopierComponent, RandomSelectorComponent, RteHelperModalComponent, - PrimaryButtonComponent + PrimaryButtonComponent, ], exports: [ @@ -626,8 +630,7 @@ import { DirectivesModule } from 'directives/directives.module'; TranslateModule, VisualizationSortedTilesComponent, RteHelperModalComponent, - PrimaryButtonComponent + PrimaryButtonComponent, ], }) - -export class SharedComponentsModule { } +export class SharedComponentsModule {} diff --git a/core/templates/components/skill-mastery/skill-mastery.component.spec.ts b/core/templates/components/skill-mastery/skill-mastery.component.spec.ts index 14154d06e868..33334bfa344e 100644 --- a/core/templates/components/skill-mastery/skill-mastery.component.spec.ts +++ b/core/templates/components/skill-mastery/skill-mastery.component.spec.ts @@ -16,30 +16,28 @@ * @fileoverview Unit tests for SkillMasteryViewerComponent. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; -import { SkillMasteryViewerComponent } from './skill-mastery.component'; -import { SkillMasteryListConstants } from 'components/skills-mastery-list/skills-mastery-list.constants'; -import { SkillMasteryBackendApiService } from 'domain/skill/skill-mastery-backend-api.service'; -import { SkillMastery } from 'domain/skill/skill-mastery.model'; +import {SkillMasteryViewerComponent} from './skill-mastery.component'; +import {SkillMasteryListConstants} from 'components/skills-mastery-list/skills-mastery-list.constants'; +import {SkillMasteryBackendApiService} from 'domain/skill/skill-mastery-backend-api.service'; +import {SkillMastery} from 'domain/skill/skill-mastery.model'; let component: SkillMasteryViewerComponent; let fixture: ComponentFixture; describe('SkillMasteryViewerComponent', () => { let skillMasteryBackendApiService: SkillMasteryBackendApiService; - const mockSkillMastery = SkillMastery.createFromBackendDict( - { - skillId1: 1.0, - } - ); + const mockSkillMastery = SkillMastery.createFromBackendDict({ + skillId1: 1.0, + }); beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [SkillMasteryViewerComponent], providers: [SkillMasteryBackendApiService], - imports: [HttpClientTestingModule] + imports: [HttpClientTestingModule], }).compileComponents(); skillMasteryBackendApiService = TestBed.get(SkillMasteryBackendApiService); })); @@ -52,11 +50,14 @@ describe('SkillMasteryViewerComponent', () => { it('should initialize skillMasteryDegree on ngOnInit', () => { component.skillId = 'skillId1'; - spyOn(skillMasteryBackendApiService, 'fetchSkillMasteryDegreesAsync') - .and.returnValue(Promise.resolve(mockSkillMastery)); + spyOn( + skillMasteryBackendApiService, + 'fetchSkillMasteryDegreesAsync' + ).and.returnValue(Promise.resolve(mockSkillMastery)); component.ngOnInit(); - expect(skillMasteryBackendApiService.fetchSkillMasteryDegreesAsync). - toHaveBeenCalledWith(['skillId1']); + expect( + skillMasteryBackendApiService.fetchSkillMasteryDegreesAsync + ).toHaveBeenCalledWith(['skillId1']); }); it('should get skill mastery percentage', () => { @@ -79,23 +80,27 @@ describe('SkillMasteryViewerComponent', () => { component.masteryChange = 0; expect(component.getLearningTips()).toBe( 'Looks like your mastery of this skill has dropped. ' + - 'To improve it, try reviewing the concept card below and ' + - 'then practicing more questions for the skill.'); + 'To improve it, try reviewing the concept card below and ' + + 'then practicing more questions for the skill.' + ); component.masteryChange = -1; expect(component.getLearningTips()).toBe( 'Looks like your mastery of this skill has dropped. ' + - 'To improve it, try reviewing the concept card below and ' + - 'then practicing more questions for the skill.'); + 'To improve it, try reviewing the concept card below and ' + + 'then practicing more questions for the skill.' + ); component.masteryChange = 1; - component.skillMasteryDegree = ( - SkillMasteryListConstants.MASTERY_CUTOFF.GOOD_CUTOFF); + component.skillMasteryDegree = + SkillMasteryListConstants.MASTERY_CUTOFF.GOOD_CUTOFF; expect(component.getLearningTips()).toBe( 'You have mastered this skill very well! ' + - 'You can work on other skills or learn new skills.'); - component.skillMasteryDegree = ( - SkillMasteryListConstants.MASTERY_CUTOFF.MEDIUM_CUTOFF); + 'You can work on other skills or learn new skills.' + ); + component.skillMasteryDegree = + SkillMasteryListConstants.MASTERY_CUTOFF.MEDIUM_CUTOFF; expect(component.getLearningTips()).toBe( 'You have made progress! You can increase your ' + - 'mastery level by doing more practice sessions.'); + 'mastery level by doing more practice sessions.' + ); }); }); diff --git a/core/templates/components/skill-mastery/skill-mastery.component.ts b/core/templates/components/skill-mastery/skill-mastery.component.ts index 926bfe728866..c81cee04b175 100644 --- a/core/templates/components/skill-mastery/skill-mastery.component.ts +++ b/core/templates/components/skill-mastery/skill-mastery.component.ts @@ -16,18 +16,16 @@ * @fileoverview Component for the skill mastery viewer. */ -import { Component, Input, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component, Input, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; -import { SkillMasteryListConstants } from - 'components/skills-mastery-list/skills-mastery-list.constants'; -import { SkillMasteryBackendApiService } from - 'domain/skill/skill-mastery-backend-api.service'; +import {SkillMasteryListConstants} from 'components/skills-mastery-list/skills-mastery-list.constants'; +import {SkillMasteryBackendApiService} from 'domain/skill/skill-mastery-backend-api.service'; @Component({ selector: 'skill-mastery-viewer', templateUrl: './skill-mastery.component.html', - styleUrls: [] + styleUrls: [], }) export class SkillMasteryViewerComponent implements OnInit { // These properties are initialized using Angular lifecycle hooks @@ -45,9 +43,14 @@ export class SkillMasteryViewerComponent implements OnInit { ngOnInit(): void { this.skillMasteryDegree = 0.0; - this.skillMasteryBackendApiService.fetchSkillMasteryDegreesAsync( - [this.skillId]).then(degreesOfMastery => this.skillMasteryDegree = ( - degreesOfMastery.getMasteryDegree(this.skillId))); + this.skillMasteryBackendApiService + .fetchSkillMasteryDegreesAsync([this.skillId]) + .then( + degreesOfMastery => + (this.skillMasteryDegree = degreesOfMastery.getMasteryDegree( + this.skillId + )) + ); } getSkillMasteryPercentage(): number { @@ -67,20 +70,28 @@ export class SkillMasteryViewerComponent implements OnInit { return ( 'Looks like your mastery of this skill has dropped. ' + 'To improve it, try reviewing the concept card below and ' + - 'then practicing more questions for the skill.'); + 'then practicing more questions for the skill.' + ); } - if (this.skillMasteryDegree >= - SkillMasteryListConstants.MASTERY_CUTOFF.GOOD_CUTOFF) { + if ( + this.skillMasteryDegree >= + SkillMasteryListConstants.MASTERY_CUTOFF.GOOD_CUTOFF + ) { return ( 'You have mastered this skill very well! ' + - 'You can work on other skills or learn new skills.'); + 'You can work on other skills or learn new skills.' + ); } return ( 'You have made progress! You can increase your ' + - 'mastery level by doing more practice sessions.'); + 'mastery level by doing more practice sessions.' + ); } } -angular.module('oppia').directive( - 'skillMasteryViewer', downgradeComponent( - {component: SkillMasteryViewerComponent})); +angular + .module('oppia') + .directive( + 'skillMasteryViewer', + downgradeComponent({component: SkillMasteryViewerComponent}) + ); diff --git a/core/templates/components/skill-selector/merge-skill-modal.component.spec.ts b/core/templates/components/skill-selector/merge-skill-modal.component.spec.ts index 5a824cef1b16..4a445a0a81d6 100644 --- a/core/templates/components/skill-selector/merge-skill-modal.component.spec.ts +++ b/core/templates/components/skill-selector/merge-skill-modal.component.spec.ts @@ -16,15 +16,15 @@ * @fileoverview Unit tests for Merge Skill Modal. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { FormsModule } from '@angular/forms'; -import { MatCardModule } from '@angular/material/card'; -import { MatCheckboxModule } from '@angular/material/checkbox'; -import { MatRadioModule } from '@angular/material/radio'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { MergeSkillModalComponent } from './merge-skill-modal.component'; -import { SkillSelectorComponent } from './skill-selector.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {FormsModule} from '@angular/forms'; +import {MatCardModule} from '@angular/material/card'; +import {MatCheckboxModule} from '@angular/material/checkbox'; +import {MatRadioModule} from '@angular/material/radio'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {MergeSkillModalComponent} from './merge-skill-modal.component'; +import {SkillSelectorComponent} from './skill-selector.component'; describe('Merge Skill Modal', () => { let fixture: ComponentFixture; @@ -38,15 +38,10 @@ describe('Merge Skill Modal', () => { MatRadioModule, MatCheckboxModule, FormsModule, - HttpClientTestingModule + HttpClientTestingModule, ], - declarations: [ - MergeSkillModalComponent, - SkillSelectorComponent - ], - providers: [ - NgbActiveModal - ] + declarations: [MergeSkillModalComponent, SkillSelectorComponent], + providers: [NgbActiveModal], }).compileComponents(); })); @@ -65,7 +60,7 @@ describe('Merge Skill Modal', () => { componentInstance.confirm(); expect(ngbActiveModal.close).toHaveBeenCalledWith({ skill: componentInstance.skill, - supersedingSkillId: componentInstance.selectedSkillId + supersedingSkillId: componentInstance.selectedSkillId, }); }); diff --git a/core/templates/components/skill-selector/merge-skill-modal.component.ts b/core/templates/components/skill-selector/merge-skill-modal.component.ts index a78801f91dcf..2b582e2304e2 100644 --- a/core/templates/components/skill-selector/merge-skill-modal.component.ts +++ b/core/templates/components/skill-selector/merge-skill-modal.component.ts @@ -16,16 +16,16 @@ * @fileoverview Component for merge skill modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { AugmentedSkillSummary } from 'domain/skill/augmented-skill-summary.model'; -import { SkillSummary } from 'domain/skill/skill-summary.model'; -import { SkillsCategorizedByTopics } from 'pages/topics-and-skills-dashboard-page/skills-list/skills-list.component'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {AugmentedSkillSummary} from 'domain/skill/augmented-skill-summary.model'; +import {SkillSummary} from 'domain/skill/skill-summary.model'; +import {SkillsCategorizedByTopics} from 'pages/topics-and-skills-dashboard-page/skills-list/skills-list.component'; @Component({ selector: 'oppia-merge-skill', - templateUrl: './merge-skill-modal.component.html' + templateUrl: './merge-skill-modal.component.html', }) export class MergeSkillModalComponent extends ConfirmOrCancelModal { // These properties below are initialized using Angular lifecycle hooks @@ -38,16 +38,14 @@ export class MergeSkillModalComponent extends ConfirmOrCancelModal { selectedSkillId!: string; allowSkillsFromOtherTopics: boolean = true; - constructor( - private ngbActiveModal: NgbActiveModal - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } confirm(): void { this.ngbActiveModal.close({ skill: this.skill, - supersedingSkillId: this.selectedSkillId + supersedingSkillId: this.selectedSkillId, }); } diff --git a/core/templates/components/skill-selector/select-skill-modal.component.spec.ts b/core/templates/components/skill-selector/select-skill-modal.component.spec.ts index cc9958e5c5eb..17d5ab7510e1 100644 --- a/core/templates/components/skill-selector/select-skill-modal.component.spec.ts +++ b/core/templates/components/skill-selector/select-skill-modal.component.spec.ts @@ -16,19 +16,21 @@ * @fileoverview Unit tests for SelectSkillModalComponent. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { FormsModule } from '@angular/forms'; -import { MatCardModule } from '@angular/material/card'; -import { MatCheckboxModule } from '@angular/material/checkbox'; -import { MatRadioModule } from '@angular/material/radio'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { CategorizedSkills, SelectSkillModalComponent } from './select-skill-modal.component'; -import { SkillSelectorComponent } from './skill-selector.component'; -import { SkillSummaryBackendDict } from 'domain/skill/skill-summary.model'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ShortSkillSummary } from 'domain/skill/short-skill-summary.model'; -import { MaterialModule } from 'modules/material.module'; - +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {FormsModule} from '@angular/forms'; +import {MatCardModule} from '@angular/material/card'; +import {MatCheckboxModule} from '@angular/material/checkbox'; +import {MatRadioModule} from '@angular/material/radio'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import { + CategorizedSkills, + SelectSkillModalComponent, +} from './select-skill-modal.component'; +import {SkillSelectorComponent} from './skill-selector.component'; +import {SkillSummaryBackendDict} from 'domain/skill/skill-summary.model'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ShortSkillSummary} from 'domain/skill/short-skill-summary.model'; +import {MaterialModule} from 'modules/material.module'; describe('Select Skill Modal', () => { let fixture: ComponentFixture; @@ -44,21 +46,20 @@ describe('Select Skill Modal', () => { misconception_count: 0, worked_examples_count: 0, skill_model_created_on: 2, - skill_model_last_updated: 3 + skill_model_last_updated: 3, }; let shortSkillSummary: SkillSummaryBackendDict = skillSummaryBackendDict; let categorizedSkills: CategorizedSkills = { 'Dummy Topic': { - Subtopic1: [shortSkillSummary] - } + Subtopic1: [shortSkillSummary], + }, }; let untriagedSkillSummaries: SkillSummaryBackendDict[] = [ - skillSummaryBackendDict + skillSummaryBackendDict, ]; let skillSummaries: SkillSummaryBackendDict[] = [skillSummaryBackendDict]; let associatedSkillSummaries: ShortSkillSummary[]; - beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ @@ -67,15 +68,10 @@ describe('Select Skill Modal', () => { MatCheckboxModule, MaterialModule, FormsModule, - HttpClientTestingModule + HttpClientTestingModule, ], - declarations: [ - SelectSkillModalComponent, - SkillSelectorComponent - ], - providers: [ - NgbActiveModal - ] + declarations: [SelectSkillModalComponent, SkillSelectorComponent], + providers: [NgbActiveModal], }).compileComponents(); })); @@ -104,11 +100,13 @@ describe('Select Skill Modal', () => { for (let topic in componentInstance.categorizedSkills) { for (let subtopic in componentInstance.categorizedSkills[topic]) { totalSkills.push( - ...componentInstance.categorizedSkills[topic][subtopic]); + ...componentInstance.categorizedSkills[topic][subtopic] + ); } } let summary = totalSkills.find( - summary => summary.id === componentInstance.selectedSkillId); + summary => summary.id === componentInstance.selectedSkillId + ); componentInstance.confirm(); expect(ngbActiveModal.close).toHaveBeenCalledWith(summary); @@ -123,12 +121,12 @@ describe('Select Skill Modal', () => { componentInstance.associatedSkillSummaries = [ ShortSkillSummary.createFromBackendDict({ skill_id: 'skillId1', - skill_description: 'Skill Description' + skill_description: 'Skill Description', }), ShortSkillSummary.createFromBackendDict({ skill_id: 'skillId2', - skill_description: 'Skill Description' - }) + skill_description: 'Skill Description', + }), ]; componentInstance.setSelectedSkillId('skillId1'); diff --git a/core/templates/components/skill-selector/select-skill-modal.component.ts b/core/templates/components/skill-selector/select-skill-modal.component.ts index 1132f2d60c1f..fc7174228209 100644 --- a/core/templates/components/skill-selector/select-skill-modal.component.ts +++ b/core/templates/components/skill-selector/select-skill-modal.component.ts @@ -15,11 +15,11 @@ /** * @fileoverview Component for select skill modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { SkillSummaryBackendDict } from 'domain/skill/skill-summary.model'; -import { ShortSkillSummary } from 'domain/skill/short-skill-summary.model'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {SkillSummaryBackendDict} from 'domain/skill/skill-summary.model'; +import {ShortSkillSummary} from 'domain/skill/short-skill-summary.model'; export interface CategorizedSkills { [topic: string]: { @@ -45,9 +45,7 @@ export class SelectSkillModalComponent extends ConfirmOrCancelModal { errorMessage: string = 'This skill is already linked to the current question.'; - constructor( - private ngbActiveModal: NgbActiveModal - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } @@ -66,7 +64,8 @@ export class SelectSkillModalComponent extends ConfirmOrCancelModal { } let summary = totalSkills.find( - summary => summary.id === this.selectedSkillId); + summary => summary.id === this.selectedSkillId + ); this.ngbActiveModal.close(summary); } @@ -77,9 +76,7 @@ export class SelectSkillModalComponent extends ConfirmOrCancelModal { isSaveButtonEnabled(): boolean { for (let idx in this.associatedSkillSummaries) { - if ( - this.associatedSkillSummaries[idx].getId() === - this.selectedSkillId) { + if (this.associatedSkillSummaries[idx].getId() === this.selectedSkillId) { return false; } } diff --git a/core/templates/components/skill-selector/skill-selector.component.spec.ts b/core/templates/components/skill-selector/skill-selector.component.spec.ts index c503f0efbd5a..00e40a3f5120 100644 --- a/core/templates/components/skill-selector/skill-selector.component.spec.ts +++ b/core/templates/components/skill-selector/skill-selector.component.spec.ts @@ -12,14 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { ShortSkillSummary } from 'domain/skill/short-skill-summary.model'; -import { SkillSummary } from 'domain/skill/skill-summary.model'; -import { UserService } from 'services/user.service'; -import { SkillSelectorComponent } from './skill-selector.component'; - +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {ShortSkillSummary} from 'domain/skill/short-skill-summary.model'; +import {SkillSummary} from 'domain/skill/skill-summary.model'; +import {UserService} from 'services/user.service'; +import {SkillSelectorComponent} from './skill-selector.component'; /** * @fileoverview Unit tests for SkillSelectorComponent. @@ -32,16 +31,10 @@ describe('SkillSelectorComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - SkillSelectorComponent - ], - providers: [ - UserService - ], - schemas: [NO_ERRORS_SCHEMA] + imports: [HttpClientTestingModule], + declarations: [SkillSelectorComponent], + providers: [UserService], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -52,24 +45,20 @@ describe('SkillSelectorComponent', () => { }); beforeEach(() => { - spyOn( - userService, 'canUserAccessTopicsAndSkillsDashboard' - ).and.returnValue(Promise.resolve(true)); + spyOn(userService, 'canUserAccessTopicsAndSkillsDashboard').and.returnValue( + Promise.resolve(true) + ); }); it('should initialize topic and subtopic filters to unchecked state', () => { component.categorizedSkills = { topic1: { uncategorized: [ - ShortSkillSummary.create('skill1', 'Skill 1 description.') + ShortSkillSummary.create('skill1', 'Skill 1 description.'), ], - subtopic1: [ - ShortSkillSummary.create('skill2', 'Skill 2 description.') - ], - subtopic2: [ - ShortSkillSummary.create('skill3', 'Skill 3 description.') - ] - } + subtopic1: [ShortSkillSummary.create('skill2', 'Skill 2 description.')], + subtopic2: [ShortSkillSummary.create('skill3', 'Skill 3 description.')], + }, }; expect(component.topicFilterList).toEqual([]); @@ -81,24 +70,24 @@ describe('SkillSelectorComponent', () => { expect(component.topicFilterList).toEqual([ { topicName: 'topic1', - checked: false - } + checked: false, + }, ]); expect(component.subTopicFilterDict).toEqual({ topic1: [ { subTopicName: 'uncategorized', - checked: false + checked: false, }, { subTopicName: 'subtopic1', - checked: false + checked: false, }, { subTopicName: 'subtopic2', - checked: false - } - ] + checked: false, + }, + ], }); }); @@ -106,19 +95,19 @@ describe('SkillSelectorComponent', () => { let categorizedSkills = { topic1: { uncategorized: [ - ShortSkillSummary.create('skill1', 'Skill 1 description.') + ShortSkillSummary.create('skill1', 'Skill 1 description.'), ], - subtopic1: [ - ShortSkillSummary.create('skill2', 'Skill 2 description.') - ], - subtopic2: [] - } + subtopic1: [ShortSkillSummary.create('skill2', 'Skill 2 description.')], + subtopic2: [], + }, }; - expect(component.checkIfEmpty( - categorizedSkills.topic1.subtopic1)).toBe(false); - expect(component.checkIfEmpty( - categorizedSkills.topic1.subtopic2)).toBe(true); + expect(component.checkIfEmpty(categorizedSkills.topic1.subtopic1)).toBe( + false + ); + expect(component.checkIfEmpty(categorizedSkills.topic1.subtopic2)).toBe( + true + ); }); it('should check if topic is empty', () => { @@ -126,15 +115,13 @@ describe('SkillSelectorComponent', () => { component.currCategorizedSkills = { topic1: { uncategorized: [ - ShortSkillSummary.create('skill1', 'Skill 1 description.') + ShortSkillSummary.create('skill1', 'Skill 1 description.'), ], - subtopic1: [ - ShortSkillSummary.create('skill2', 'Skill 2 description.') - ] + subtopic1: [ShortSkillSummary.create('skill2', 'Skill 2 description.')], }, topic2: { - uncategorized: [] - } + uncategorized: [], + }, }; expect(component.checkTopicIsNotEmpty('topic1')).toBe(true); @@ -150,75 +137,70 @@ describe('SkillSelectorComponent', () => { expect(component.selectedSkillIdChange.emit).toHaveBeenCalledWith('skill1'); }); - it('should display subtopics from all topics in the subtopic filter if' + - ' no topic is checked', () => { - component.categorizedSkills = { - topic1: { - uncategorized: [ - ShortSkillSummary.create('skill1', 'Skill 1 description.') - ], - subtopic1: [ - ShortSkillSummary.create('skill2', 'Skill 2 description.') - ] - }, - topic2: { - uncategorized: [ - ShortSkillSummary.create('skill4', 'Skill 4 description.') - ] - } - }; + it( + 'should display subtopics from all topics in the subtopic filter if' + + ' no topic is checked', + () => { + component.categorizedSkills = { + topic1: { + uncategorized: [ + ShortSkillSummary.create('skill1', 'Skill 1 description.'), + ], + subtopic1: [ + ShortSkillSummary.create('skill2', 'Skill 2 description.'), + ], + }, + topic2: { + uncategorized: [ + ShortSkillSummary.create('skill4', 'Skill 4 description.'), + ], + }, + }; - component.ngOnInit(); + component.ngOnInit(); - component.subTopicFilterDict = {}; + component.subTopicFilterDict = {}; - component.updateSkillsListOnTopicFilterChange(); + component.updateSkillsListOnTopicFilterChange(); - // All subtopics from all topics is included in the subtopic filter dict. - expect((component.subTopicFilterDict)).toEqual({ - topic1: [ - { - subTopicName: 'uncategorized', - checked: false - }, - { - subTopicName: 'subtopic1', - checked: false - } - ], - topic2: [ - { - subTopicName: 'uncategorized', - checked: false - } - ] - }); - }); + // All subtopics from all topics is included in the subtopic filter dict. + expect(component.subTopicFilterDict).toEqual({ + topic1: [ + { + subTopicName: 'uncategorized', + checked: false, + }, + { + subTopicName: 'subtopic1', + checked: false, + }, + ], + topic2: [ + { + subTopicName: 'uncategorized', + checked: false, + }, + ], + }); + } + ); it('should update skill list when user filters skills by only topics', () => { component.categorizedSkills = { topic1: { uncategorized: [ - ShortSkillSummary.create('skill1', 'Skill 1 description.') - ], - subtopic1: [ - ShortSkillSummary.create('skill2', 'Skill 2 description.') + ShortSkillSummary.create('skill1', 'Skill 1 description.'), ], - subtopic2: [ - ShortSkillSummary.create('skill3', 'Skill 3 description.') - ] + subtopic1: [ShortSkillSummary.create('skill2', 'Skill 2 description.')], + subtopic2: [ShortSkillSummary.create('skill3', 'Skill 3 description.')], }, topic2: { uncategorized: [ - ShortSkillSummary.create('skill4', 'Skill 4 description.') + ShortSkillSummary.create('skill4', 'Skill 4 description.'), ], - subtopic3: [ - ShortSkillSummary.create('skill5', 'Skill 5 description.') - ], - subtopic4: [ - ShortSkillSummary.create('skill6', 'Skill 6 description.') - ] - } + subtopic3: [ShortSkillSummary.create('skill5', 'Skill 5 description.')], + subtopic4: [ShortSkillSummary.create('skill6', 'Skill 6 description.')], + }, }; component.ngOnInit(); @@ -226,37 +208,29 @@ describe('SkillSelectorComponent', () => { component.topicFilterList = [ { topicName: 'topic1', - checked: true + checked: true, }, { topicName: 'topic2', - checked: false - } + checked: false, + }, ]; expect(component.currCategorizedSkills).toEqual({ topic1: { uncategorized: [ - ShortSkillSummary.create('skill1', 'Skill 1 description.') + ShortSkillSummary.create('skill1', 'Skill 1 description.'), ], - subtopic1: [ - ShortSkillSummary.create('skill2', 'Skill 2 description.') - ], - subtopic2: [ - ShortSkillSummary.create('skill3', 'Skill 3 description.') - ] + subtopic1: [ShortSkillSummary.create('skill2', 'Skill 2 description.')], + subtopic2: [ShortSkillSummary.create('skill3', 'Skill 3 description.')], }, topic2: { uncategorized: [ - ShortSkillSummary.create('skill4', 'Skill 4 description.') - ], - subtopic3: [ - ShortSkillSummary.create('skill5', 'Skill 5 description.') + ShortSkillSummary.create('skill4', 'Skill 4 description.'), ], - subtopic4: [ - ShortSkillSummary.create('skill6', 'Skill 6 description.') - ] - } + subtopic3: [ShortSkillSummary.create('skill5', 'Skill 5 description.')], + subtopic4: [ShortSkillSummary.create('skill6', 'Skill 6 description.')], + }, }); component.updateSkillsListOnTopicFilterChange(); @@ -264,187 +238,193 @@ describe('SkillSelectorComponent', () => { expect(component.currCategorizedSkills).toEqual({ topic1: { uncategorized: [ - ShortSkillSummary.create('skill1', 'Skill 1 description.') - ], - subtopic1: [ - ShortSkillSummary.create('skill2', 'Skill 2 description.') + ShortSkillSummary.create('skill1', 'Skill 1 description.'), ], - subtopic2: [ - ShortSkillSummary.create('skill3', 'Skill 3 description.') - ] - } + subtopic1: [ShortSkillSummary.create('skill2', 'Skill 2 description.')], + subtopic2: [ShortSkillSummary.create('skill3', 'Skill 3 description.')], + }, }); }); - it('should update skill list when user filters skills by' + - ' topics and subtopics', () => { - component.categorizedSkills = { - topic1: { - uncategorized: [ - ShortSkillSummary.create('skill1', 'Skill 1 description.') - ], - subtopic1: [ - ShortSkillSummary.create('skill2', 'Skill 2 description.') - ], - subtopic2: [ - ShortSkillSummary.create('skill3', 'Skill 3 description.') - ] - }, - topic2: { - uncategorized: [ - ShortSkillSummary.create('skill4', 'Skill 4 description.') - ], - subtopic3: [ - ShortSkillSummary.create('skill5', 'Skill 5 description.') - ], - subtopic4: [ - ShortSkillSummary.create('skill6', 'Skill 6 description.') - ] - } - }; + it( + 'should update skill list when user filters skills by' + + ' topics and subtopics', + () => { + component.categorizedSkills = { + topic1: { + uncategorized: [ + ShortSkillSummary.create('skill1', 'Skill 1 description.'), + ], + subtopic1: [ + ShortSkillSummary.create('skill2', 'Skill 2 description.'), + ], + subtopic2: [ + ShortSkillSummary.create('skill3', 'Skill 3 description.'), + ], + }, + topic2: { + uncategorized: [ + ShortSkillSummary.create('skill4', 'Skill 4 description.'), + ], + subtopic3: [ + ShortSkillSummary.create('skill5', 'Skill 5 description.'), + ], + subtopic4: [ + ShortSkillSummary.create('skill6', 'Skill 6 description.'), + ], + }, + }; - component.ngOnInit(); + component.ngOnInit(); - // User checks 'topic1' radio button and 'subtopic1' radio button. - component.topicFilterList = [ - { - topicName: 'topic1', - checked: true - } - ]; - component.subTopicFilterDict = { - topic1: [ + // User checks 'topic1' radio button and 'subtopic1' radio button. + component.topicFilterList = [ { - subTopicName: 'subtopic1', - checked: true - } - ] - }; - - expect(component.currCategorizedSkills).toEqual({ - topic1: { - uncategorized: [ - ShortSkillSummary.create('skill1', 'Skill 1 description.') - ], - subtopic1: [ - ShortSkillSummary.create('skill2', 'Skill 2 description.') - ], - subtopic2: [ - ShortSkillSummary.create('skill3', 'Skill 3 description.') - ] - }, - topic2: { - uncategorized: [ - ShortSkillSummary.create('skill4', 'Skill 4 description.') - ], - subtopic3: [ - ShortSkillSummary.create('skill5', 'Skill 5 description.') + topicName: 'topic1', + checked: true, + }, + ]; + component.subTopicFilterDict = { + topic1: [ + { + subTopicName: 'subtopic1', + checked: true, + }, ], - subtopic4: [ - ShortSkillSummary.create('skill6', 'Skill 6 description.') - ] - } - }); - - component.updateSkillsListOnSubtopicFilterChange(); + }; + + expect(component.currCategorizedSkills).toEqual({ + topic1: { + uncategorized: [ + ShortSkillSummary.create('skill1', 'Skill 1 description.'), + ], + subtopic1: [ + ShortSkillSummary.create('skill2', 'Skill 2 description.'), + ], + subtopic2: [ + ShortSkillSummary.create('skill3', 'Skill 3 description.'), + ], + }, + topic2: { + uncategorized: [ + ShortSkillSummary.create('skill4', 'Skill 4 description.'), + ], + subtopic3: [ + ShortSkillSummary.create('skill5', 'Skill 5 description.'), + ], + subtopic4: [ + ShortSkillSummary.create('skill6', 'Skill 6 description.'), + ], + }, + }); - expect((component.currCategorizedSkills)).toEqual({ - topic1: { - uncategorized: [], - subtopic1: [ - ShortSkillSummary.create('skill2', 'Skill 2 description.') - ] - } - }); - }); + component.updateSkillsListOnSubtopicFilterChange(); - it('should clear all filters when user clicks on \'Clear All' + - ' Filters\'', () => { - component.topicFilterList = [ - { - topicName: 'topic1', - checked: true - } - ]; - component.subTopicFilterDict = { - topic1: [ + expect(component.currCategorizedSkills).toEqual({ + topic1: { + uncategorized: [], + subtopic1: [ + ShortSkillSummary.create('skill2', 'Skill 2 description.'), + ], + }, + }); + } + ); + + it( + "should clear all filters when user clicks on 'Clear All" + " Filters'", + () => { + component.topicFilterList = [ { - subTopicName: 'subtopic1', - checked: true - } - ] - }; + topicName: 'topic1', + checked: true, + }, + ]; + component.subTopicFilterDict = { + topic1: [ + { + subTopicName: 'subtopic1', + checked: true, + }, + ], + }; - component.clearAllFilters(); + component.clearAllFilters(); - expect(component.topicFilterList).toEqual([ - { - topicName: 'topic1', - checked: false - } - ]); - expect(component.subTopicFilterDict).toEqual({ - topic1: [ + expect(component.topicFilterList).toEqual([ { - subTopicName: 'subtopic1', - checked: false - } - ] - }); - }); + topicName: 'topic1', + checked: false, + }, + ]); + expect(component.subTopicFilterDict).toEqual({ + topic1: [ + { + subTopicName: 'subtopic1', + checked: false, + }, + ], + }); + } + ); it('should search in subtopic skills and return filtered skills', () => { let inputShortSkillSummaries: ShortSkillSummary[] = [ ShortSkillSummary.create('skill1', 'Skill 1 description.'), ShortSkillSummary.create('skill2', 'Skill 2 description.'), - ShortSkillSummary.create('skill3', 'Skill 2 and 3 description.') + ShortSkillSummary.create('skill3', 'Skill 2 and 3 description.'), ]; let searchText = 'skill 2'; - expect(component.searchInSubtopicSkills( - inputShortSkillSummaries, searchText)).toEqual([ + expect( + component.searchInSubtopicSkills(inputShortSkillSummaries, searchText) + ).toEqual([ ShortSkillSummary.create('skill2', 'Skill 2 description.'), - ShortSkillSummary.create('skill3', 'Skill 2 and 3 description.') + ShortSkillSummary.create('skill3', 'Skill 2 and 3 description.'), ]); }); - it('should search in untriaged skill summaries and return' + - ' filtered skills', () => { - component.untriagedSkillSummaries = [ - SkillSummary.createFromBackendDict({ - id: '1', - description: 'This is untriaged skill summary 1', - language_code: '', - version: 1, - misconception_count: 2, - worked_examples_count: 2, - skill_model_created_on: 121212, - skill_model_last_updated: 124444 - }), - SkillSummary.createFromBackendDict({ - id: '2', - description: 'This is untriaged skill summary 2', - language_code: '', - version: 1, - misconception_count: 2, - worked_examples_count: 2, - skill_model_created_on: 121212, - skill_model_last_updated: 124444 - }) - ]; - - expect(component.searchInUntriagedSkillSummaries( - 'skill summary 2')).toEqual([ - SkillSummary.createFromBackendDict({ - id: '2', - description: 'This is untriaged skill summary 2', - language_code: '', - version: 1, - misconception_count: 2, - worked_examples_count: 2, - skill_model_created_on: 121212, - skill_model_last_updated: 124444 - }) - ]); - }); + it( + 'should search in untriaged skill summaries and return' + + ' filtered skills', + () => { + component.untriagedSkillSummaries = [ + SkillSummary.createFromBackendDict({ + id: '1', + description: 'This is untriaged skill summary 1', + language_code: '', + version: 1, + misconception_count: 2, + worked_examples_count: 2, + skill_model_created_on: 121212, + skill_model_last_updated: 124444, + }), + SkillSummary.createFromBackendDict({ + id: '2', + description: 'This is untriaged skill summary 2', + language_code: '', + version: 1, + misconception_count: 2, + worked_examples_count: 2, + skill_model_created_on: 121212, + skill_model_last_updated: 124444, + }), + ]; + + expect( + component.searchInUntriagedSkillSummaries('skill summary 2') + ).toEqual([ + SkillSummary.createFromBackendDict({ + id: '2', + description: 'This is untriaged skill summary 2', + language_code: '', + version: 1, + misconception_count: 2, + worked_examples_count: 2, + skill_model_created_on: 121212, + skill_model_last_updated: 124444, + }), + ]); + } + ); }); diff --git a/core/templates/components/skill-selector/skill-selector.component.ts b/core/templates/components/skill-selector/skill-selector.component.ts index 9e322a748b06..58f4bc055215 100644 --- a/core/templates/components/skill-selector/skill-selector.component.ts +++ b/core/templates/components/skill-selector/skill-selector.component.ts @@ -16,18 +16,18 @@ * @fileoverview Controller for the skill-selector component. */ -import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { ShortSkillSummary } from 'core/templates/domain/skill/short-skill-summary.model'; -import { SkillSummary } from 'core/templates/domain/skill/skill-summary.model'; -import { CategorizedSkills } from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; -import { FilterForMatchingSubstringPipe } from 'filters/string-utility-filters/filter-for-matching-substring.pipe'; +import {Component, OnInit, Input, Output, EventEmitter} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {ShortSkillSummary} from 'core/templates/domain/skill/short-skill-summary.model'; +import {SkillSummary} from 'core/templates/domain/skill/skill-summary.model'; +import {CategorizedSkills} from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; +import {FilterForMatchingSubstringPipe} from 'filters/string-utility-filters/filter-for-matching-substring.pipe'; import cloneDeep from 'lodash/cloneDeep'; -import { GroupedSkillSummaries } from 'pages/skill-editor-page/services/skill-editor-state.service'; -import { UserService } from 'services/user.service'; +import {GroupedSkillSummaries} from 'pages/skill-editor-page/services/skill-editor-state.service'; +import {UserService} from 'services/user.service'; interface SubTopicFilterDict { - [topicName: string]: { subTopicName: string; checked: boolean }[]; + [topicName: string]: {subTopicName: string; checked: boolean}[]; } @Component({ @@ -52,7 +52,7 @@ export class SkillSelectorComponent implements OnInit { currCategorizedSkills!: CategorizedSkills; selectedSkill!: string; skillFilterText: string = ''; - topicFilterList: { topicName: string ; checked: boolean }[] = []; + topicFilterList: {topicName: string; checked: boolean}[] = []; subTopicFilterDict: SubTopicFilterDict = {}; initialSubTopicFilterDict: SubTopicFilterDict = {}; userCanEditSkills: boolean = false; @@ -67,7 +67,7 @@ export class SkillSelectorComponent implements OnInit { for (let topicName in this.currCategorizedSkills) { let topicNameDict = { topicName: topicName, - checked: false + checked: false, }; this.topicFilterList.push(topicNameDict); let subTopics = this.currCategorizedSkills[topicName]; @@ -75,15 +75,16 @@ export class SkillSelectorComponent implements OnInit { for (let subTopic in subTopics) { let subTopicNameDict = { subTopicName: subTopic, - checked: false + checked: false, }; this.subTopicFilterDict[topicName].push(subTopicNameDict); } } this.initialSubTopicFilterDict = cloneDeep(this.subTopicFilterDict); - this.userService.canUserAccessTopicsAndSkillsDashboard() - .then((canUserAccessTopicsAndSkillsDashboard) => { + this.userService + .canUserAccessTopicsAndSkillsDashboard() + .then(canUserAccessTopicsAndSkillsDashboard => { this.userCanEditSkills = canUserAccessTopicsAndSkillsDashboard; }); } @@ -115,7 +116,7 @@ export class SkillSelectorComponent implements OnInit { for (var i = 0; i < subTopics.length; i++) { if (subTopics[i].checked) { if (!updatedSkillsDict.hasOwnProperty(topicName)) { - updatedSkillsDict[topicName] = { uncategorized: [] }; + updatedSkillsDict[topicName] = {uncategorized: []}; } let tempCategorizedSkills: CategorizedSkills = this.categorizedSkills; let subTopicName: string = subTopics[i].subTopicName; @@ -158,8 +159,9 @@ export class SkillSelectorComponent implements OnInit { for (var i = 0; i < this.topicFilterList.length; i++) { if (this.topicFilterList[i].checked) { let topicName = this.topicFilterList[i].topicName; - updatedSubTopicFilterList[topicName] = ( - cloneDeep(this.initialSubTopicFilterDict[topicName])); + updatedSubTopicFilterList[topicName] = cloneDeep( + this.initialSubTopicFilterDict[topicName] + ); isAnyTopicChecked = true; } } @@ -168,26 +170,30 @@ export class SkillSelectorComponent implements OnInit { // display subtopics from all the topics in the subtopic filter. for (let topic in this.initialSubTopicFilterDict) { if (!this.subTopicFilterDict.hasOwnProperty(topic)) { - this.subTopicFilterDict[topic] = ( - cloneDeep(this.initialSubTopicFilterDict[topic])); + this.subTopicFilterDict[topic] = cloneDeep( + this.initialSubTopicFilterDict[topic] + ); } } } else { - this.subTopicFilterDict = - cloneDeep(updatedSubTopicFilterList); + this.subTopicFilterDict = cloneDeep(updatedSubTopicFilterList); } // After we update the subtopic filter list, we need to update // the main skills list. this.updateSkillsListOnSubtopicFilterChange(); } - searchInSubtopicSkills(input: ShortSkillSummary[], searchText: string): - ShortSkillSummary[] { + searchInSubtopicSkills( + input: ShortSkillSummary[], + searchText: string + ): ShortSkillSummary[] { let skills: string[] = input.map(val => { return val.getDescription(); }); - let filteredSkills = this.filterForMatchingSubstringPipe - .transform(skills, searchText); + let filteredSkills = this.filterForMatchingSubstringPipe.transform( + skills, + searchText + ); return input.filter(val => { return filteredSkills.includes(val.description); }); @@ -197,8 +203,10 @@ export class SkillSelectorComponent implements OnInit { let skills: string[] = this.untriagedSkillSummaries.map(val => { return val.description; }); - let filteredSkills = this.filterForMatchingSubstringPipe - .transform(skills, searchText); + let filteredSkills = this.filterForMatchingSubstringPipe.transform( + skills, + searchText + ); return this.untriagedSkillSummaries.filter(val => { return filteredSkills.includes(val.description); }); @@ -218,8 +226,9 @@ export class SkillSelectorComponent implements OnInit { } } -angular.module('oppia').directive( - 'oppiaSkillSelector', downgradeComponent( - { component: SkillSelectorComponent } - ) -); +angular + .module('oppia') + .directive( + 'oppiaSkillSelector', + downgradeComponent({component: SkillSelectorComponent}) + ); diff --git a/core/templates/components/skills-mastery-list/skills-mastery-list-concept-card-modal.controller.spec.ts b/core/templates/components/skills-mastery-list/skills-mastery-list-concept-card-modal.controller.spec.ts index 7320eb6a9174..624727a2ff20 100644 --- a/core/templates/components/skills-mastery-list/skills-mastery-list-concept-card-modal.controller.spec.ts +++ b/core/templates/components/skills-mastery-list/skills-mastery-list-concept-card-modal.controller.spec.ts @@ -16,32 +16,35 @@ * @fileoverview Unit tests for SkillsMasteryListConceptCardModal. */ -describe('Skills Mastery List Concept Card Modal Controller', function() { +describe('Skills Mastery List Concept Card Modal Controller', function () { var $scope = null; var $uibModalInstance = null; var skillDescription = 'This is a skill description'; var skillId = '1'; beforeEach(angular.mock.module('oppia')); - beforeEach(angular.mock.inject(function($injector, $controller) { - var $rootScope = $injector.get('$rootScope'); + beforeEach( + angular.mock.inject(function ($injector, $controller) { + var $rootScope = $injector.get('$rootScope'); - $uibModalInstance = jasmine.createSpyObj( - '$uibModalInstance', ['close', 'dismiss']); + $uibModalInstance = jasmine.createSpyObj('$uibModalInstance', [ + 'close', + 'dismiss', + ]); - $scope = $rootScope.$new(); - $controller('SkillsMasteryListConceptCardModal', { - $scope: $scope, - $uibModalInstance: $uibModalInstance, - skillDescription: skillDescription, - skillId: skillId - }); - })); + $scope = $rootScope.$new(); + $controller('SkillsMasteryListConceptCardModal', { + $scope: $scope, + $uibModalInstance: $uibModalInstance, + skillDescription: skillDescription, + skillId: skillId, + }); + }) + ); - it('should initialize $scope properties after controller is initialized', - function() { - expect($scope.skillIds).toEqual([skillId]); - expect($scope.index).toEqual(0); - expect($scope.modalHeader).toEqual(skillDescription); - }); + it('should initialize $scope properties after controller is initialized', function () { + expect($scope.skillIds).toEqual([skillId]); + expect($scope.index).toEqual(0); + expect($scope.modalHeader).toEqual(skillDescription); + }); }); diff --git a/core/templates/components/skills-mastery-list/skills-mastery-list-concept-card-modal.controller.ts b/core/templates/components/skills-mastery-list/skills-mastery-list-concept-card-modal.controller.ts index 86df62bfd460..91b7395fef9d 100644 --- a/core/templates/components/skills-mastery-list/skills-mastery-list-concept-card-modal.controller.ts +++ b/core/templates/components/skills-mastery-list/skills-mastery-list-concept-card-modal.controller.ts @@ -18,18 +18,22 @@ require( 'components/common-layout-directives/common-elements/' + - 'confirm-or-cancel-modal.controller.ts'); + 'confirm-or-cancel-modal.controller.ts' +); angular.module('oppia').controller('SkillsMasteryListConceptCardModal', [ - '$controller', '$scope', '$uibModalInstance', 'skillDescription', 'skillId', - function( - $controller, $scope, $uibModalInstance, skillDescription, skillId) { + '$controller', + '$scope', + '$uibModalInstance', + 'skillDescription', + 'skillId', + function ($controller, $scope, $uibModalInstance, skillDescription, skillId) { $controller('ConfirmOrCancelModalController', { $scope: $scope, - $uibModalInstance: $uibModalInstance + $uibModalInstance: $uibModalInstance, }); $scope.skillIds = [skillId]; $scope.index = 0; $scope.modalHeader = skillDescription; - } + }, ]); diff --git a/core/templates/components/skills-mastery-list/skills-mastery-list.constants.ajs.ts b/core/templates/components/skills-mastery-list/skills-mastery-list.constants.ajs.ts index 4862f8d4f84e..d3b5e61779b1 100644 --- a/core/templates/components/skills-mastery-list/skills-mastery-list.constants.ajs.ts +++ b/core/templates/components/skills-mastery-list/skills-mastery-list.constants.ajs.ts @@ -16,13 +16,12 @@ * @fileoverview Constants for the skills mastery list. */ -import {SkillMasteryListConstants} from - 'components/skills-mastery-list/skills-mastery-list.constants'; +import {SkillMasteryListConstants} from 'components/skills-mastery-list/skills-mastery-list.constants'; -angular.module('oppia').constant( - 'MASTERY_CUTOFF', - SkillMasteryListConstants.MASTERY_CUTOFF); +angular + .module('oppia') + .constant('MASTERY_CUTOFF', SkillMasteryListConstants.MASTERY_CUTOFF); -angular.module('oppia').constant( - 'MASTERY_COLORS', - SkillMasteryListConstants.MASTERY_COLORS); +angular + .module('oppia') + .constant('MASTERY_COLORS', SkillMasteryListConstants.MASTERY_COLORS); diff --git a/core/templates/components/skills-mastery-list/skills-mastery-list.constants.ts b/core/templates/components/skills-mastery-list/skills-mastery-list.constants.ts index aa56bc6402da..c839d21d6229 100644 --- a/core/templates/components/skills-mastery-list/skills-mastery-list.constants.ts +++ b/core/templates/components/skills-mastery-list/skills-mastery-list.constants.ts @@ -19,7 +19,7 @@ export const SkillMasteryListConstants = { MASTERY_CUTOFF: { GOOD_CUTOFF: 0.7, - MEDIUM_CUTOFF: 0.4 + MEDIUM_CUTOFF: 0.4, }, MASTERY_COLORS: { @@ -28,6 +28,6 @@ export const SkillMasteryListConstants = { // Color orange. MEDIUM_MASTERY_COLOR: 'rgb(217, 92, 12)', // Color red. - BAD_MASTERY_COLOR: 'rgb(201, 80, 66)' + BAD_MASTERY_COLOR: 'rgb(201, 80, 66)', }, } as const; diff --git a/core/templates/components/stale-tab-info/stale-tab-info-modal.component.spec.ts b/core/templates/components/stale-tab-info/stale-tab-info-modal.component.spec.ts index ad8ef356e6fa..e5403e129adf 100644 --- a/core/templates/components/stale-tab-info/stale-tab-info-modal.component.spec.ts +++ b/core/templates/components/stale-tab-info/stale-tab-info-modal.component.spec.ts @@ -16,9 +16,9 @@ * @fileoverview Unit tests for stale tab information component. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { StaleTabInfoModalComponent } from './stale-tab-info-modal.component'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {StaleTabInfoModalComponent} from './stale-tab-info-modal.component'; describe('Stale tab info modal component', () => { let component: StaleTabInfoModalComponent; @@ -27,12 +27,8 @@ describe('Stale tab info modal component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - StaleTabInfoModalComponent - ], - providers: [ - NgbActiveModal - ] + declarations: [StaleTabInfoModalComponent], + providers: [NgbActiveModal], }).compileComponents(); })); diff --git a/core/templates/components/stale-tab-info/stale-tab-info-modal.component.ts b/core/templates/components/stale-tab-info/stale-tab-info-modal.component.ts index b45d38cc2d8f..ba4d4a3d2780 100644 --- a/core/templates/components/stale-tab-info/stale-tab-info-modal.component.ts +++ b/core/templates/components/stale-tab-info/stale-tab-info-modal.component.ts @@ -16,8 +16,8 @@ * @fileoverview Component for stale tab information component editor. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; @Component({ selector: 'oppia-stale-tab-info-modal', @@ -29,9 +29,7 @@ export class StaleTabInfoModalComponent { // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 entity!: string; - constructor( - private ngbActiveModal: NgbActiveModal - ) {} + constructor(private ngbActiveModal: NgbActiveModal) {} closeModal(): void { this.ngbActiveModal.close(); diff --git a/core/templates/components/state-directives/answer-group-editor/answer-group-editor.component.spec.ts b/core/templates/components/state-directives/answer-group-editor/answer-group-editor.component.spec.ts index dabdb6cf1d04..c2a44545545b 100644 --- a/core/templates/components/state-directives/answer-group-editor/answer-group-editor.component.spec.ts +++ b/core/templates/components/state-directives/answer-group-editor/answer-group-editor.component.spec.ts @@ -16,18 +16,24 @@ * @fileoverview Unit test for Answer Group Editor Component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed, fakeAsync, tick } from '@angular/core/testing'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { StateInteractionIdService } from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; -import { Rule } from 'domain/exploration/rule.model'; -import { ParameterizeRuleDescriptionPipe } from 'filters/parameterize-rule-description.pipe'; -import { ResponsesService } from 'pages/exploration-editor-page/editor-tab/services/responses.service'; -import { TrainingDataEditorPanelService } from 'pages/exploration-editor-page/editor-tab/training-panel/training-data-editor-panel.service'; -import { AlertsService } from 'services/alerts.service'; -import { ExternalSaveService } from 'services/external-save.service'; -import { AnswerGroupEditor } from './answer-group-editor.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + waitForAsync, + TestBed, + fakeAsync, + tick, +} from '@angular/core/testing'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {StateInteractionIdService} from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; +import {Rule} from 'domain/exploration/rule.model'; +import {ParameterizeRuleDescriptionPipe} from 'filters/parameterize-rule-description.pipe'; +import {ResponsesService} from 'pages/exploration-editor-page/editor-tab/services/responses.service'; +import {TrainingDataEditorPanelService} from 'pages/exploration-editor-page/editor-tab/training-panel/training-data-editor-panel.service'; +import {AlertsService} from 'services/alerts.service'; +import {ExternalSaveService} from 'services/external-save.service'; +import {AnswerGroupEditor} from './answer-group-editor.component'; describe('Answer Group Editor Component', () => { let component: AnswerGroupEditor; @@ -59,13 +65,8 @@ describe('Answer Group Editor Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - ], - declarations: [ - AnswerGroupEditor, - ParameterizeRuleDescriptionPipe - ], + imports: [HttpClientTestingModule], + declarations: [AnswerGroupEditor, ParameterizeRuleDescriptionPipe], providers: [ ExternalSaveService, StateEditorService, @@ -74,7 +75,7 @@ describe('Answer Group Editor Component', () => { AlertsService, TrainingDataEditorPanelService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -88,14 +89,18 @@ describe('Answer Group Editor Component', () => { responsesService = TestBed.inject(ResponsesService); alertsService = TestBed.inject(AlertsService); trainingDataEditorPanelService = TestBed.inject( - TrainingDataEditorPanelService); - - spyOn(externalSaveService, 'onExternalSave') - .and.returnValue(mockOnExternalSave); - spyOn(stateEditorService, 'onUpdateAnswerChoices') - .and.returnValue(mockOnUpdateAnswerChoices); - spyOn(stateInteractionIdService, 'onInteractionIdChanged') - .and.returnValue(mockOnInteractionIdChanged); + TrainingDataEditorPanelService + ); + + spyOn(externalSaveService, 'onExternalSave').and.returnValue( + mockOnExternalSave + ); + spyOn(stateEditorService, 'onUpdateAnswerChoices').and.returnValue( + mockOnUpdateAnswerChoices + ); + spyOn(stateInteractionIdService, 'onInteractionIdChanged').and.returnValue( + mockOnInteractionIdChanged + ); }); it('should set component properties on initialization', () => { @@ -117,54 +122,64 @@ describe('Answer Group Editor Component', () => { component.ngOnDestroy(); }); - it('should save rules when current rule is valid and user' + - ' triggers an external save', fakeAsync(() => { - let externalSaveEmitter = new EventEmitter(); - spyOnProperty(externalSaveService, 'onExternalSave').and.returnValue( - externalSaveEmitter); - spyOn(stateEditorService, 'checkCurrentRuleInputIsValid').and.returnValue( - true); - spyOn(component, 'saveRules').and.stub(); + it( + 'should save rules when current rule is valid and user' + + ' triggers an external save', + fakeAsync(() => { + let externalSaveEmitter = new EventEmitter(); + spyOnProperty(externalSaveService, 'onExternalSave').and.returnValue( + externalSaveEmitter + ); + spyOn(stateEditorService, 'checkCurrentRuleInputIsValid').and.returnValue( + true + ); + spyOn(component, 'saveRules').and.stub(); - component.ngOnInit(); - component.activeRuleIndex = 1; - component.sendOnSaveTaggedMisconception(null); - component.sendOnSaveAnswerGroupCorrectnessLabel(null); - component.sendOnSaveAnswerGroupFeedback(null); + component.ngOnInit(); + component.activeRuleIndex = 1; + component.sendOnSaveTaggedMisconception(null); + component.sendOnSaveAnswerGroupCorrectnessLabel(null); + component.sendOnSaveAnswerGroupFeedback(null); - externalSaveEmitter.emit(); - tick(); + externalSaveEmitter.emit(); + tick(); - expect(component.saveRules).toHaveBeenCalled(); + expect(component.saveRules).toHaveBeenCalled(); - component.ngOnDestroy(); - })); + component.ngOnDestroy(); + }) + ); - it('should warning message when current rule is invalid and user' + - ' triggers an external save', fakeAsync(() => { - let externalSaveEmitter = new EventEmitter(); - spyOnProperty(externalSaveService, 'onExternalSave').and.returnValue( - externalSaveEmitter); - spyOn(stateEditorService, 'checkCurrentRuleInputIsValid').and.returnValue( - false); - spyOn(alertsService, 'addInfoMessage'); + it( + 'should warning message when current rule is invalid and user' + + ' triggers an external save', + fakeAsync(() => { + let externalSaveEmitter = new EventEmitter(); + spyOnProperty(externalSaveService, 'onExternalSave').and.returnValue( + externalSaveEmitter + ); + spyOn(stateEditorService, 'checkCurrentRuleInputIsValid').and.returnValue( + false + ); + spyOn(alertsService, 'addInfoMessage'); - component.ngOnInit(); - component.activeRuleIndex = 1; - alertsService.addMessage('info', 'Some other message', 0); - component.sendOnSaveAnswerGroupDest(null); - component.sendOnSaveAnswerGroupDestIfStuck(null); + component.ngOnInit(); + component.activeRuleIndex = 1; + alertsService.addMessage('info', 'Some other message', 0); + component.sendOnSaveAnswerGroupDest(null); + component.sendOnSaveAnswerGroupDestIfStuck(null); - externalSaveEmitter.emit(); - tick(); + externalSaveEmitter.emit(); + tick(); - expect(alertsService.addInfoMessage).toHaveBeenCalledWith( - 'There was an unsaved rule input which was invalid' + - ' and has been discarded.' - ); + expect(alertsService.addInfoMessage).toHaveBeenCalledWith( + 'There was an unsaved rule input which was invalid' + + ' and has been discarded.' + ); - component.ngOnDestroy(); - })); + component.ngOnDestroy(); + }) + ); it('should return back when ruleTypes length is 0', () => { spyOn(component, 'getCurrentInteractionId').and.returnValue('Continue'); @@ -175,50 +190,55 @@ describe('Answer Group Editor Component', () => { expect(component.changeActiveRuleIndex).not.toHaveBeenCalled(); }); - it('should get answer choices when user updates answer choices', - fakeAsync(() => { - let updateAnswerChoicesEmitter = new EventEmitter(); - spyOnProperty(stateEditorService, 'onUpdateAnswerChoices') - .and.returnValue(updateAnswerChoicesEmitter); - spyOn(responsesService, 'getAnswerChoices') - .and.returnValue(answerChoices); - - component.ngOnInit(); - updateAnswerChoicesEmitter.emit(); - tick(); - - expect(component.answerChoices).toEqual(answerChoices); - - component.ngOnDestroy(); - })); - - it('should save rules and get answer choices when interaction' + - ' is changed', fakeAsync(() => { - let interactionIdChangedEmitter = new EventEmitter(); - spyOnProperty(stateInteractionIdService, 'onInteractionIdChanged') - .and.returnValue(interactionIdChangedEmitter); - spyOn(component, 'saveRules').and.stub(); + it('should get answer choices when user updates answer choices', fakeAsync(() => { + let updateAnswerChoicesEmitter = new EventEmitter(); + spyOnProperty(stateEditorService, 'onUpdateAnswerChoices').and.returnValue( + updateAnswerChoicesEmitter + ); spyOn(responsesService, 'getAnswerChoices').and.returnValue(answerChoices); component.ngOnInit(); - component.activeRuleIndex = 1; - - interactionIdChangedEmitter.emit(); + updateAnswerChoicesEmitter.emit(); tick(); - expect(component.saveRules).toHaveBeenCalled(); expect(component.answerChoices).toEqual(answerChoices); component.ngOnDestroy(); })); + it( + 'should save rules and get answer choices when interaction' + ' is changed', + fakeAsync(() => { + let interactionIdChangedEmitter = new EventEmitter(); + spyOnProperty( + stateInteractionIdService, + 'onInteractionIdChanged' + ).and.returnValue(interactionIdChangedEmitter); + spyOn(component, 'saveRules').and.stub(); + spyOn(responsesService, 'getAnswerChoices').and.returnValue( + answerChoices + ); + + component.ngOnInit(); + component.activeRuleIndex = 1; + + interactionIdChangedEmitter.emit(); + tick(); + + expect(component.saveRules).toHaveBeenCalled(); + expect(component.answerChoices).toEqual(answerChoices); + + component.ngOnDestroy(); + }) + ); + it('should check if editor is in question mode', () => { spyOn(stateEditorService, 'isInQuestionMode').and.returnValue(true); expect(component.isInQuestionMode()).toBe(true); }); - it('should get current interaction\'s ID', () => { + it("should get current interaction's ID", () => { stateInteractionIdService.savedMemento = 'TextIput'; expect(component.getCurrentInteractionId()).toBe('TextIput'); @@ -239,71 +259,81 @@ describe('Answer Group Editor Component', () => { code: '', error: '', evaluation: '', - output: '' + output: '', }); - expect(component.getDefaultInputValue('CoordTwoDim')).toEqual([ - 0, 0 - ]); + expect(component.getDefaultInputValue('CoordTwoDim')).toEqual([0, 0]); expect(component.getDefaultInputValue('MusicPhrase')).toEqual([]); expect(component.getDefaultInputValue('CheckedProof')).toEqual({ assumptions_string: '', correct: false, proof_string: '', - target_string: '' + target_string: '', }); expect(component.getDefaultInputValue('Graph')).toEqual({ edges: [], isDirected: false, isLabeled: false, isWeighted: false, - vertices: [] + vertices: [], }); expect(component.getDefaultInputValue('NormalizedRectangle2D')).toEqual([ [0, 0], - [0, 0] + [0, 0], ]); expect(component.getDefaultInputValue('ImageRegion')).toEqual({ - area: [[0, 0], [0, 0]], - regionType: '' + area: [ + [0, 0], + [0, 0], + ], + regionType: '', }); expect(component.getDefaultInputValue('ImageWithRegions')).toEqual({ imagePath: '', - labeledRegions: [] + labeledRegions: [], }); expect(component.getDefaultInputValue('ClickOnImage')).toEqual({ clickPosition: [0, 0], - clickedRegions: [] + clickedRegions: [], + }); + expect( + component.getDefaultInputValue('TranslatableSetOfNormalizedString') + ).toEqual({ + contentId: null, + normalizedStrSet: [], + }); + expect( + component.getDefaultInputValue('TranslatableSetOfUnicodeString') + ).toEqual({ + contentId: null, + normalizedStrSet: [], }); - expect(component.getDefaultInputValue('TranslatableSetOfNormalizedString')) - .toEqual({ - contentId: null, - normalizedStrSet: [] - }); - expect(component.getDefaultInputValue('TranslatableSetOfUnicodeString')) - .toEqual({ - contentId: null, - normalizedStrSet: [] - }); }); - it('should add new rule when user click on \'+ Add Another' + - ' Possible Answer\'', () => { - component.rules = []; - stateInteractionIdService.savedMemento = 'TextInput'; - - component.addNewRule(); - - expect(component.rules).toEqual([ - new Rule('StartsWith', { - x: { - contentId: null, - normalizedStrSet: [] - } - }, { - x: 'TranslatableSetOfNormalizedString' - }) - ]); - }); + it( + "should add new rule when user click on '+ Add Another" + + " Possible Answer'", + () => { + component.rules = []; + stateInteractionIdService.savedMemento = 'TextInput'; + + component.addNewRule(); + + expect(component.rules).toEqual([ + new Rule( + 'StartsWith', + { + x: { + contentId: null, + normalizedStrSet: [], + }, + }, + { + x: 'TranslatableSetOfNormalizedString', + } + ), + ]); + } + ); it('should not add rule for interaction specs without description', () => { stateInteractionIdService.savedMemento = 'MultipleChoiceInput'; @@ -313,51 +343,67 @@ describe('Answer Group Editor Component', () => { it('should delete rule when user clicks on delete', () => { component.originalContentIdToContent = { - id1: 'content' + id1: 'content', }; component.rules = [ - new Rule('StartsWith', { - x: { - contentId: 'id1', - normalizedStrSet: [] + new Rule( + 'StartsWith', + { + x: { + contentId: 'id1', + normalizedStrSet: [], + }, + }, + { + x: 'TranslatableSetOfNormalizedString', } - }, { - x: 'TranslatableSetOfNormalizedString' - }), - new Rule('StartsWith', { - x: { - contentId: 'id2', - normalizedStrSet: [] + ), + new Rule( + 'StartsWith', + { + x: { + contentId: 'id2', + normalizedStrSet: [], + }, + }, + { + x: 'TranslatableSetOfNormalizedString', } - }, { - x: 'TranslatableSetOfNormalizedString' - }) + ), ]; component.deleteRule(1); expect(component.rules).toEqual([ - new Rule('StartsWith', { - x: { - contentId: 'id1', - normalizedStrSet: [] + new Rule( + 'StartsWith', + { + x: { + contentId: 'id1', + normalizedStrSet: [], + }, + }, + { + x: 'TranslatableSetOfNormalizedString', } - }, { - x: 'TranslatableSetOfNormalizedString' - }) + ), ]); }); it('should show warning if user deletes the only existing rule', () => { component.rules = [ - new Rule('StartsWith', { - x: { - contentId: 'id1', - normalizedStrSet: [] + new Rule( + 'StartsWith', + { + x: { + contentId: 'id1', + normalizedStrSet: [], + }, + }, + { + x: 'TranslatableSetOfNormalizedString', } - }, { - x: 'TranslatableSetOfNormalizedString' - }) + ), ]; spyOn(alertsService, 'addWarning'); @@ -370,22 +416,30 @@ describe('Answer Group Editor Component', () => { }); it('should cancel active rule edits, when user clicks on cancel', () => { - let rule1 = new Rule('StartsWith', { - x: { - contentId: 'id1', - normalizedStrSet: [] + let rule1 = new Rule( + 'StartsWith', + { + x: { + contentId: 'id1', + normalizedStrSet: [], + }, + }, + { + x: 'TranslatableSetOfNormalizedString', } - }, { - x: 'TranslatableSetOfNormalizedString' - }); - let rule2 = new Rule('StartsWith', { - x: { - contentId: 'id2', - normalizedStrSet: [] + ); + let rule2 = new Rule( + 'StartsWith', + { + x: { + contentId: 'id2', + normalizedStrSet: [], + }, + }, + { + x: 'TranslatableSetOfNormalizedString', } - }, { - x: 'TranslatableSetOfNormalizedString' - }); + ); component.rules = [rule1]; component.rulesMemento = [rule2]; @@ -399,15 +453,19 @@ describe('Answer Group Editor Component', () => { expect(component.isMLEnabled()).toBe(false); }); - it('should open training data editor when user click on' + - ' \'Modify Training Data\'', () => { - spyOn(trainingDataEditorPanelService, 'openTrainingDataEditor'); + it( + 'should open training data editor when user click on' + + " 'Modify Training Data'", + () => { + spyOn(trainingDataEditorPanelService, 'openTrainingDataEditor'); - component.openTrainingDataEditor(); + component.openTrainingDataEditor(); - expect(trainingDataEditorPanelService.openTrainingDataEditor) - .toHaveBeenCalled(); - }); + expect( + trainingDataEditorPanelService.openTrainingDataEditor + ).toHaveBeenCalled(); + } + ); it('should check if current interaction is trainable', () => { // We set the current interaction as TextInput, which is trainable. @@ -425,30 +483,38 @@ describe('Answer Group Editor Component', () => { stateInteractionIdService.savedMemento = 'InvalidInteraction'; component.rules = []; component.rules.push( - new Rule('dummyRule1', { - x: { - contentId: null, - normalizedStrSet: [] + new Rule( + 'dummyRule1', + { + x: { + contentId: null, + normalizedStrSet: [], + }, + }, + { + x: 'dummyInputType1', } - }, { - x: 'dummyInputType1' - }) + ) ); component.rules.push( - new Rule('dummyRule2', { - x: { - contentId: null, - normalizedStrSet: [] + new Rule( + 'dummyRule2', + { + x: { + contentId: null, + normalizedStrSet: [], + }, + }, + { + x: 'dummyInputType2', } - }, { - x: 'dummyInputType2' - }) + ) ); - expect(() => component.isCurrentInteractionTrainable()) - .toThrowError( - 'Invalid interaction id - InvalidInteraction. Answer group rules: ' + - 'dummyRule1, dummyRule2'); + expect(() => component.isCurrentInteractionTrainable()).toThrowError( + 'Invalid interaction id - InvalidInteraction. Answer group rules: ' + + 'dummyRule1, dummyRule2' + ); }); it('should not open rule editor if it is in read-only mode', () => { @@ -461,14 +527,18 @@ describe('Answer Group Editor Component', () => { }); it('should open rule editor if it is not in read-only mode', () => { - let rule1 = new Rule('StartsWith', { - x: { - contentId: 'id1', - normalizedStrSet: [] + let rule1 = new Rule( + 'StartsWith', + { + x: { + contentId: 'id1', + normalizedStrSet: [], + }, + }, + { + x: 'TranslatableSetOfNormalizedString', } - }, { - x: 'TranslatableSetOfNormalizedString' - }); + ); component.rules = [rule1]; spyOn(component, 'changeActiveRuleIndex'); diff --git a/core/templates/components/state-directives/answer-group-editor/answer-group-editor.component.ts b/core/templates/components/state-directives/answer-group-editor/answer-group-editor.component.ts index eff0f182af85..8dcfea88004f 100644 --- a/core/templates/components/state-directives/answer-group-editor/answer-group-editor.component.ts +++ b/core/templates/components/state-directives/answer-group-editor/answer-group-editor.component.ts @@ -16,22 +16,32 @@ * @fileoverview Component for the answer group editor. */ -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { AnswerChoice, StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { StateInteractionIdService } from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; -import { Rule } from 'domain/exploration/rule.model'; +import { + Component, + EventEmitter, + Input, + OnDestroy, + OnInit, + Output, +} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import { + AnswerChoice, + StateEditorService, +} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {StateInteractionIdService} from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; +import {Rule} from 'domain/exploration/rule.model'; import isEqual from 'lodash/isEqual'; -import { ResponsesService } from 'pages/exploration-editor-page/editor-tab/services/responses.service'; -import { TrainingDataEditorPanelService } from 'pages/exploration-editor-page/editor-tab/training-panel/training-data-editor-panel.service'; +import {ResponsesService} from 'pages/exploration-editor-page/editor-tab/services/responses.service'; +import {TrainingDataEditorPanelService} from 'pages/exploration-editor-page/editor-tab/training-panel/training-data-editor-panel.service'; import INTERACTION_SPECS from 'interactions/interaction_specs.json'; -import { Subscription } from 'rxjs'; -import { AlertsService } from 'services/alerts.service'; +import {Subscription} from 'rxjs'; +import {AlertsService} from 'services/alerts.service'; import cloneDeep from 'lodash/cloneDeep'; -import { AppConstants } from 'app.constants'; -import { ExternalSaveService } from 'services/external-save.service'; -import { Outcome } from 'domain/exploration/OutcomeObjectFactory'; -import { BaseTranslatableObject } from 'interactions/rule-input-defs'; +import {AppConstants} from 'app.constants'; +import {ExternalSaveService} from 'services/external-save.service'; +import {Outcome} from 'domain/exploration/OutcomeObjectFactory'; +import {BaseTranslatableObject} from 'interactions/rule-input-defs'; interface TaggedMisconception { skillId: string; @@ -40,7 +50,7 @@ interface TaggedMisconception { @Component({ selector: 'oppia-answer-group-editor', - templateUrl: './answer-group-editor.component.html' + templateUrl: './answer-group-editor.component.html', }) export class AnswerGroupEditor implements OnInit, OnDestroy { @Input() displayFeedback: boolean; @@ -71,7 +81,7 @@ export class AnswerGroupEditor implements OnInit, OnDestroy { private stateInteractionIdService: StateInteractionIdService, private alertsService: AlertsService, private trainingDataEditorPanelService: TrainingDataEditorPanelService, - private externalSaveService: ExternalSaveService, + private externalSaveService: ExternalSaveService ) {} sendOnSaveTaggedMisconception(event: TaggedMisconception): void { @@ -107,8 +117,8 @@ export class AnswerGroupEditor implements OnInit, OnDestroy { } getDefaultInputValue( - varType: string): null | boolean | - number | number[] | string | object | object[] { + varType: string + ): null | boolean | number | number[] | string | object | object[] { // TODO(bhenning): Typed objects in the backend should be required // to provide a default value specific for their type. switch (varType) { @@ -136,12 +146,13 @@ export class AnswerGroupEditor implements OnInit, OnDestroy { code: this.getDefaultInputValue('UnicodeString'), error: this.getDefaultInputValue('UnicodeString'), evaluation: this.getDefaultInputValue('UnicodeString'), - output: this.getDefaultInputValue('UnicodeString') + output: this.getDefaultInputValue('UnicodeString'), }; case 'CoordTwoDim': return [ this.getDefaultInputValue('Real'), - this.getDefaultInputValue('Real')]; + this.getDefaultInputValue('Real'), + ]; case 'ListOfUnicodeString': case 'SetOfAlgebraicIdentifier': case 'SetOfUnicodeString': @@ -153,7 +164,7 @@ export class AnswerGroupEditor implements OnInit, OnDestroy { assumptions_string: this.getDefaultInputValue('UnicodeString'), correct: this.getDefaultInputValue('Boolean'), proof_string: this.getDefaultInputValue('UnicodeString'), - target_string: this.getDefaultInputValue('UnicodeString') + target_string: this.getDefaultInputValue('UnicodeString'), }; case 'Graph': return { @@ -161,47 +172,46 @@ export class AnswerGroupEditor implements OnInit, OnDestroy { isDirected: this.getDefaultInputValue('Boolean'), isLabeled: this.getDefaultInputValue('Boolean'), isWeighted: this.getDefaultInputValue('Boolean'), - vertices: [] + vertices: [], }; case 'NormalizedRectangle2D': return [ [ this.getDefaultInputValue('Real'), - this.getDefaultInputValue('Real') + this.getDefaultInputValue('Real'), ], [ this.getDefaultInputValue('Real'), - this.getDefaultInputValue('Real') - ]]; + this.getDefaultInputValue('Real'), + ], + ]; case 'ImageRegion': return { area: this.getDefaultInputValue('NormalizedRectangle2D'), - regionType: this.getDefaultInputValue('UnicodeString') + regionType: this.getDefaultInputValue('UnicodeString'), }; case 'ImageWithRegions': return { imagePath: this.getDefaultInputValue('Filepath'), - labeledRegions: [] + labeledRegions: [], }; case 'ClickOnImage': return { clickPosition: [ this.getDefaultInputValue('Real'), - this.getDefaultInputValue('Real') + this.getDefaultInputValue('Real'), ], - clickedRegions: [] + clickedRegions: [], }; case 'TranslatableSetOfNormalizedString': return { contentId: null, - normalizedStrSet: - this.getDefaultInputValue('SetOfNormalizedString') + normalizedStrSet: this.getDefaultInputValue('SetOfNormalizedString'), }; case 'TranslatableSetOfUnicodeString': return { contentId: null, - normalizedStrSet: - this.getDefaultInputValue('SetOfUnicodeString') + normalizedStrSet: this.getDefaultInputValue('SetOfUnicodeString'), }; } } @@ -209,8 +219,7 @@ export class AnswerGroupEditor implements OnInit, OnDestroy { addNewRule(): void { // Build an initial blank set of inputs for the initial rule. let interactionId = this.getCurrentInteractionId(); - let ruleDescriptions = ( - INTERACTION_SPECS[interactionId].rule_descriptions); + let ruleDescriptions = INTERACTION_SPECS[interactionId].rule_descriptions; let ruleTypes = Object.keys(ruleDescriptions); if (ruleTypes.length === 0) { // This should never happen. An interaction must have at least @@ -243,8 +252,7 @@ export class AnswerGroupEditor implements OnInit, OnDestroy { // TODO(bhenning): Should use functionality in ruleEditor.js, but // move it to ResponsesService in StateResponses.js to properly // form a new rule. - const rule = Rule.createNew( - ruleType, inputs, inputTypes); + const rule = Rule.createNew(ruleType, inputs, inputTypes); this.rules.push(rule); this.changeActiveRuleIndex(this.rules.length - 1); } @@ -255,7 +263,8 @@ export class AnswerGroupEditor implements OnInit, OnDestroy { if (this.rules.length === 0) { this.alertsService.addWarning( - 'All answer groups must have at least one rule.'); + 'All answer groups must have at least one rule.' + ); } } @@ -269,14 +278,11 @@ export class AnswerGroupEditor implements OnInit, OnDestroy { saveRules(): void { if (this.originalContentIdToContent !== undefined) { - const updatedContentIdToContent = ( - this.getTranslatableRulesContentIdToContentMap() - ); + const updatedContentIdToContent = + this.getTranslatableRulesContentIdToContentMap(); const contentIdsWithModifiedContent = []; - Object.keys( - this.originalContentIdToContent - ).forEach(contentId => { + Object.keys(this.originalContentIdToContent).forEach(contentId => { if ( this.originalContentIdToContent.hasOwnProperty(contentId) && updatedContentIdToContent.hasOwnProperty(contentId) && @@ -307,9 +313,8 @@ export class AnswerGroupEditor implements OnInit, OnDestroy { return; } - this.originalContentIdToContent = ( - this.getTranslatableRulesContentIdToContentMap() - ); + this.originalContentIdToContent = + this.getTranslatableRulesContentIdToContentMap(); this.rulesMemento = cloneDeep(this.rules); this.changeActiveRuleIndex(index); } @@ -322,9 +327,11 @@ export class AnswerGroupEditor implements OnInit, OnDestroy { let interactionId = this.getCurrentInteractionId(); if (!INTERACTION_SPECS.hasOwnProperty(interactionId)) { throw new Error( - 'Invalid interaction id - ' + interactionId + - '. Answer group rules: ' + - this.rules.map(rule => rule.type).join(', ')); + 'Invalid interaction id - ' + + interactionId + + '. Answer group rules: ' + + this.rules.map(rule => rule.type).join(', ') + ); } return INTERACTION_SPECS[interactionId].is_trainable; } @@ -352,8 +359,9 @@ export class AnswerGroupEditor implements OnInit, OnDestroy { // BaseTranslatableObject having dict structure with contentId // as a key. if (ruleInput && ruleInput.hasOwnProperty('contentId')) { - contentIdToContentMap[( - ruleInput as BaseTranslatableObject).contentId] = ruleInput; + contentIdToContentMap[ + (ruleInput as BaseTranslatableObject).contentId + ] = ruleInput; } }); }); @@ -372,11 +380,14 @@ export class AnswerGroupEditor implements OnInit, OnDestroy { if (this.stateEditorService.checkCurrentRuleInputIsValid()) { this.saveRules(); } else { - let messageContent = ( + let messageContent = 'There was an unsaved rule input which was invalid and ' + - 'has been discarded.'); - if (!this.alertsService.messages.some(messageObject => ( - messageObject.content === messageContent))) { + 'has been discarded.'; + if ( + !this.alertsService.messages.some( + messageObject => messageObject.content === messageContent + ) + ) { this.alertsService.addInfoMessage(messageContent); } } @@ -391,14 +402,12 @@ export class AnswerGroupEditor implements OnInit, OnDestroy { ); this.directiveSubscriptions.add( - this.stateInteractionIdService.onInteractionIdChanged.subscribe( - () => { - if (this.isRuleEditorOpen()) { - this.saveRules(); - } - this.answerChoices = this.getAnswerChoices(); + this.stateInteractionIdService.onInteractionIdChanged.subscribe(() => { + if (this.isRuleEditorOpen()) { + this.saveRules(); } - ) + this.answerChoices = this.getAnswerChoices(); + }) ); this.rulesMemento = null; @@ -412,7 +421,9 @@ export class AnswerGroupEditor implements OnInit, OnDestroy { } } -angular.module('oppia').directive('oppiaAnswerGroupEditor', +angular.module('oppia').directive( + 'oppiaAnswerGroupEditor', downgradeComponent({ - component: AnswerGroupEditor - }) as angular.IDirectiveFactory); + component: AnswerGroupEditor, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/components/state-directives/answer-group-editor/summary-list-header.component.spec.ts b/core/templates/components/state-directives/answer-group-editor/summary-list-header.component.spec.ts index 7da306401eaf..d511e1d2fd46 100644 --- a/core/templates/components/state-directives/answer-group-editor/summary-list-header.component.spec.ts +++ b/core/templates/components/state-directives/answer-group-editor/summary-list-header.component.spec.ts @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { SummaryListHeaderComponent } from './summary-list-header.component'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {SummaryListHeaderComponent} from './summary-list-header.component'; /** * @fileoverview Unit tests for SummaryListHeaderComponent. @@ -25,7 +25,7 @@ describe('SummaryListHeaderComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [SummaryListHeaderComponent] + declarations: [SummaryListHeaderComponent], }).compileComponents(); })); @@ -43,7 +43,7 @@ describe('SummaryListHeaderComponent', () => { expect(component.summaryDelete.emit).toHaveBeenCalledWith({ index: 1, - event: itemDeletionEvent + event: itemDeletionEvent, }); }); }); diff --git a/core/templates/components/state-directives/answer-group-editor/summary-list-header.component.ts b/core/templates/components/state-directives/answer-group-editor/summary-list-header.component.ts index 76b6b4f259d0..6159dd8ce961 100644 --- a/core/templates/components/state-directives/answer-group-editor/summary-list-header.component.ts +++ b/core/templates/components/state-directives/answer-group-editor/summary-list-header.component.ts @@ -16,8 +16,8 @@ * @fileoverview Component for the header of items in a list. */ -import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component, EventEmitter, Input, Output} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; interface DeleteSummaryEventData { index: number; @@ -26,7 +26,7 @@ interface DeleteSummaryEventData { @Component({ selector: 'oppia-summary-list-header', - templateUrl: './summary-list-header.component.html' + templateUrl: './summary-list-header.component.html', }) export class SummaryListHeaderComponent { // These properties are initialized using Angular lifecycle hooks @@ -37,8 +37,8 @@ export class SummaryListHeaderComponent { @Input() summary!: string; @Input() shortSummary!: string; @Input() isActive: boolean = false; - @Output() summaryDelete: - EventEmitter = (new EventEmitter()); + @Output() summaryDelete: EventEmitter = + new EventEmitter(); @Input() isDeleteAvailable: boolean = false; @Input() numItems!: number; @@ -46,11 +46,15 @@ export class SummaryListHeaderComponent { deleteItem(evt: Event): void { let eventData = { index: this.index, - event: evt + event: evt, }; this.summaryDelete.emit(eventData); } } -angular.module('oppia').directive('oppiaSummaryListHeader', - downgradeComponent({ component: SummaryListHeaderComponent })); +angular + .module('oppia') + .directive( + 'oppiaSummaryListHeader', + downgradeComponent({component: SummaryListHeaderComponent}) + ); diff --git a/core/templates/components/state-directives/hint-editor/hint-editor.component.spec.ts b/core/templates/components/state-directives/hint-editor/hint-editor.component.spec.ts index 877510466141..b8a2b543c48f 100644 --- a/core/templates/components/state-directives/hint-editor/hint-editor.component.spec.ts +++ b/core/templates/components/state-directives/hint-editor/hint-editor.component.spec.ts @@ -16,14 +16,20 @@ * @fileoverview Unit test for Hint Editor Component. */ -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed, fakeAsync, tick } from '@angular/core/testing'; -import { HintEditorComponent } from './hint-editor.component'; -import { ContextService } from 'services/context.service'; -import { EditabilityService } from 'services/editability.service'; -import { ExternalSaveService } from 'services/external-save.service'; -import { Hint } from 'domain/exploration/hint-object.model'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + waitForAsync, + TestBed, + fakeAsync, + tick, +} from '@angular/core/testing'; +import {HintEditorComponent} from './hint-editor.component'; +import {ContextService} from 'services/context.service'; +import {EditabilityService} from 'services/editability.service'; +import {ExternalSaveService} from 'services/external-save.service'; +import {Hint} from 'domain/exploration/hint-object.model'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; describe('HintEditorComponent', () => { let component: HintEditorComponent; @@ -33,15 +39,9 @@ describe('HintEditorComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - HintEditorComponent - ], - providers: [ - ContextService, - EditabilityService, - ExternalSaveService, - ], - schemas: [NO_ERRORS_SCHEMA] + declarations: [HintEditorComponent], + providers: [ContextService, EditabilityService, ExternalSaveService], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -53,7 +53,8 @@ describe('HintEditorComponent', () => { externalSaveService = TestBed.inject(ExternalSaveService); component.hint = new Hint( - SubtitledHtml.createDefault('html text', 'contentID')); + SubtitledHtml.createDefault('html text', 'contentID') + ); fixture.detectChanges(); }); @@ -73,26 +74,29 @@ describe('HintEditorComponent', () => { it('should save hint when external save event is triggered', fakeAsync(() => { let onExternalSaveEmitter = new EventEmitter(); - spyOnProperty(externalSaveService, 'onExternalSave') - .and.returnValue(onExternalSaveEmitter); + spyOnProperty(externalSaveService, 'onExternalSave').and.returnValue( + onExternalSaveEmitter + ); component.ngOnInit(); component.hintEditorIsOpen = true; - component.hint = new Hint( - SubtitledHtml.createDefault('change', 'data')); + component.hint = new Hint(SubtitledHtml.createDefault('change', 'data')); component.hintMemento = new Hint( - SubtitledHtml.createDefault('html text', 'contentID')); + SubtitledHtml.createDefault('html text', 'contentID') + ); onExternalSaveEmitter.emit(); tick(); })); - it('should open hint editor when user clicks on \'Edit hint\'', () => { + it("should open hint editor when user clicks on 'Edit hint'", () => { component.isEditable = true; component.hintMemento = new Hint( - SubtitledHtml.createDefault('html text', 'contentID')); + SubtitledHtml.createDefault('html text', 'contentID') + ); component.hint = new Hint( - SubtitledHtml.createDefault('html text edited', 'contentID')); + SubtitledHtml.createDefault('html text edited', 'contentID') + ); component.hintEditorIsOpen = false; component.openHintEditor(); @@ -101,14 +105,16 @@ describe('HintEditorComponent', () => { expect(component.hintEditorIsOpen).toBe(true); }); - it('should cancel hint edit if user clicks on \'Cancel\'', () => { + it("should cancel hint edit if user clicks on 'Cancel'", () => { jasmine.createSpy('valid').and.returnValue(true); component.hintEditorIsOpen = true; - const earlierHint = component.hintMemento = new Hint( - SubtitledHtml.createDefault('html text', 'contentID')); + const earlierHint = (component.hintMemento = new Hint( + SubtitledHtml.createDefault('html text', 'contentID') + )); component.hint = new Hint( - SubtitledHtml.createDefault('html text edited', 'contentID')); + SubtitledHtml.createDefault('html text edited', 'contentID') + ); component.cancelThisHintEdit(); @@ -118,21 +124,23 @@ describe('HintEditorComponent', () => { it('should check if hint HTML length exceeds 500 characters', () => { component.hint = new Hint( - SubtitledHtml.createDefault(`

${'a'.repeat(500)}

`, 'contentID')); + SubtitledHtml.createDefault(`

${'a'.repeat(500)}

`, 'contentID') + ); expect(component.isHintLengthExceeded()).toBe(false); component.hint = new Hint( - SubtitledHtml.createDefault(`

${'a'.repeat(501)}

`, 'contentID')); + SubtitledHtml.createDefault(`

${'a'.repeat(501)}

`, 'contentID') + ); expect(component.isHintLengthExceeded()).toBe(true); }); it('should check if hint HTML is updating', () => { - const schema = component.HINT_FORM_SCHEMA = { + const schema = (component.HINT_FORM_SCHEMA = { type: 'html', ui_config: { - hide_complex_extensions: true - } - }; + hide_complex_extensions: true, + }, + }); expect(component.getSchema()).toBe(schema); diff --git a/core/templates/components/state-directives/hint-editor/hint-editor.component.ts b/core/templates/components/state-directives/hint-editor/hint-editor.component.ts index ef635149fc76..6d167ee43853 100644 --- a/core/templates/components/state-directives/hint-editor/hint-editor.component.ts +++ b/core/templates/components/state-directives/hint-editor/hint-editor.component.ts @@ -16,26 +16,36 @@ * @fileoverview Component for the hint editor. */ -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { FormControl, FormGroup } from '@angular/forms'; -import { Subscription } from 'rxjs'; +import { + Component, + EventEmitter, + Input, + OnDestroy, + OnInit, + Output, +} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {FormControl, FormGroup} from '@angular/forms'; +import {Subscription} from 'rxjs'; import cloneDeep from 'lodash/cloneDeep'; -import { ContextService } from 'services/context.service'; -import { EditabilityService } from 'services/editability.service'; -import { ExternalSaveService } from 'services/external-save.service'; -import { Hint } from 'domain/exploration/hint-object.model'; -import { ExplorationEditorPageConstants } from 'pages/exploration-editor-page/exploration-editor-page.constants'; -import { CALCULATION_TYPE_CHARACTER, HtmlLengthService } from 'services/html-length.service'; +import {ContextService} from 'services/context.service'; +import {EditabilityService} from 'services/editability.service'; +import {ExternalSaveService} from 'services/external-save.service'; +import {Hint} from 'domain/exploration/hint-object.model'; +import {ExplorationEditorPageConstants} from 'pages/exploration-editor-page/exploration-editor-page.constants'; +import { + CALCULATION_TYPE_CHARACTER, + HtmlLengthService, +} from 'services/html-length.service'; interface HintFormSchema { type: string; - 'ui_config': object; + ui_config: object; } @Component({ selector: 'oppia-hint-editor', - templateUrl: './hint-editor.component.html' + templateUrl: './hint-editor.component.html', }) export class HintEditorComponent implements OnInit, OnDestroy { @Output() saveHint = new EventEmitter(); @@ -55,7 +65,7 @@ export class HintEditorComponent implements OnInit, OnDestroy { private contextService: ContextService, private editabilityService: EditabilityService, private externalSaveService: ExternalSaveService, - private htmlLengthService: HtmlLengthService, + private htmlLengthService: HtmlLengthService ) {} getSchema(): HintFormSchema { @@ -76,8 +86,10 @@ export class HintEditorComponent implements OnInit, OnDestroy { isHintLengthExceeded(): boolean { return Boolean( this.htmlLengthService.computeHtmlLength( - this.hint.hintContent._html, CALCULATION_TYPE_CHARACTER) > - ExplorationEditorPageConstants.HINT_CHARACTER_LIMIT); + this.hint.hintContent._html, + CALCULATION_TYPE_CHARACTER + ) > ExplorationEditorPageConstants.HINT_CHARACTER_LIMIT + ); } saveThisHint(): void { @@ -100,15 +112,16 @@ export class HintEditorComponent implements OnInit, OnDestroy { if (this.hintEditorIsOpen && this.editHintForm.valid) { this.saveThisHint(); } - })); + }) + ); this.isEditable = this.editabilityService.isEditable(); this.hintEditorIsOpen = false; this.HINT_FORM_SCHEMA = { type: 'html', ui_config: { - hide_complex_extensions: ( - this.contextService.getEntityType() === 'question') - } + hide_complex_extensions: + this.contextService.getEntityType() === 'question', + }, }; } @@ -117,5 +130,9 @@ export class HintEditorComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive('oppiaHintEditor', - downgradeComponent({component: HintEditorComponent})); +angular + .module('oppia') + .directive( + 'oppiaHintEditor', + downgradeComponent({component: HintEditorComponent}) + ); diff --git a/core/templates/components/state-directives/outcome-editor/outcome-destination-editor.component.spec.ts b/core/templates/components/state-directives/outcome-editor/outcome-destination-editor.component.spec.ts index e655a5501c23..502abc3fc694 100644 --- a/core/templates/components/state-directives/outcome-editor/outcome-destination-editor.component.spec.ts +++ b/core/templates/components/state-directives/outcome-editor/outcome-destination-editor.component.spec.ts @@ -16,19 +16,25 @@ * @fileoverview Unit tests for outcome destination editor component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { FormsModule } from '@angular/forms'; -import { StateGraphLayoutService } from 'components/graph-services/graph-layout.service'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { Outcome } from 'domain/exploration/OutcomeObjectFactory'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { EditorFirstTimeEventsService } from 'pages/exploration-editor-page/services/editor-first-time-events.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { UserService } from 'services/user.service'; -import { UserInfo } from 'domain/user/user-info.model'; -import { OutcomeDestinationEditorComponent } from './outcome-destination-editor.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {FormsModule} from '@angular/forms'; +import {StateGraphLayoutService} from 'components/graph-services/graph-layout.service'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {Outcome} from 'domain/exploration/OutcomeObjectFactory'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {EditorFirstTimeEventsService} from 'pages/exploration-editor-page/services/editor-first-time-events.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {UserService} from 'services/user.service'; +import {UserInfo} from 'domain/user/user-info.model'; +import {OutcomeDestinationEditorComponent} from './outcome-destination-editor.component'; describe('Outcome Destination Editor', () => { let component: OutcomeDestinationEditorComponent; @@ -43,21 +49,16 @@ describe('Outcome Destination Editor', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - FormsModule - ], - declarations: [ - OutcomeDestinationEditorComponent - ], + imports: [HttpClientTestingModule, FormsModule], + declarations: [OutcomeDestinationEditorComponent], providers: [ EditorFirstTimeEventsService, FocusManagerService, StateEditorService, StateGraphLayoutService, - UserService + UserService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -76,7 +77,7 @@ describe('Outcome Destination Editor', () => { false, [], null, - null, + null ); spyOn(stateEditorService, 'isExplorationCurated').and.returnValue(true); @@ -91,25 +92,35 @@ describe('Outcome Destination Editor', () => { { Introduction: 'Introduction', State1: 'State1', - End: 'End' - }, [ + End: 'End', + }, + [ { source: 'Introduction', target: 'State1', linkProperty: '', - connectsDestIfStuck: false + connectsDestIfStuck: false, }, { source: 'State1', target: 'End', linkProperty: '', - connectsDestIfStuck: false - } - ], 'Introduction', ['End']); - spyOn(stateEditorService, 'getStateNames') - .and.returnValue(['Introduction', 'State1', 'NewState', 'End']); - spyOn(stateGraphLayoutService, 'getLastComputedArrangement') - .and.returnValue(computedLayout); + connectsDestIfStuck: false, + }, + ], + 'Introduction', + ['End'] + ); + spyOn(stateEditorService, 'getStateNames').and.returnValue([ + 'Introduction', + 'State1', + 'NewState', + 'End', + ]); + spyOn( + stateGraphLayoutService, + 'getLastComputedArrangement' + ).and.returnValue(computedLayout); spyOn(stateEditorService, 'getActiveStateName').and.returnValue('Hola'); component.ngOnInit(); @@ -118,81 +129,107 @@ describe('Outcome Destination Editor', () => { expect(component.canAddPrerequisiteSkill).toBeFalse(); expect(component.canEditRefresherExplorationId).toBeFalse(); expect(component.newStateNamePattern).toEqual(/^[a-zA-Z0-9.\s-]+$/); - expect(component.destinationChoices).toEqual([{ - id: 'Hola', - text: '(try again)' - }, { - id: 'Introduction', - text: 'Introduction' - }, { - id: 'State1', - text: 'State1' - }, { - id: 'End', - text: 'End' - }, { - id: 'NewState', - text: 'NewState' - }, { - id: '/', - text: 'A New Card Called...' - }]); + expect(component.destinationChoices).toEqual([ + { + id: 'Hola', + text: '(try again)', + }, + { + id: 'Introduction', + text: 'Introduction', + }, + { + id: 'State1', + text: 'State1', + }, + { + id: 'End', + text: 'End', + }, + { + id: 'NewState', + text: 'NewState', + }, + { + id: '/', + text: 'A New Card Called...', + }, + ]); })); - it('should set outcome destination as active state if it is a self loop' + - ' when outcome destination details are saved', fakeAsync(() => { - component.outcome.dest = 'Hola'; - - let onSaveOutcomeDestDetailsEmitter = new EventEmitter(); - spyOnProperty(stateEditorService, 'onSaveOutcomeDestDetails') - .and.returnValue(onSaveOutcomeDestDetailsEmitter); - spyOn(stateEditorService, 'getActiveStateName').and.returnValues( - 'Hola', 'Introduction'); - - component.ngOnInit(); - tick(10); - - onSaveOutcomeDestDetailsEmitter.emit(); - - expect(component.outcome.dest).toBe('Hola'); - })); + it( + 'should set outcome destination as active state if it is a self loop' + + ' when outcome destination details are saved', + fakeAsync(() => { + component.outcome.dest = 'Hola'; + + let onSaveOutcomeDestDetailsEmitter = new EventEmitter(); + spyOnProperty( + stateEditorService, + 'onSaveOutcomeDestDetails' + ).and.returnValue(onSaveOutcomeDestDetailsEmitter); + spyOn(stateEditorService, 'getActiveStateName').and.returnValues( + 'Hola', + 'Introduction' + ); - it('should add new state if outcome destination is a placeholder when' + - ' outcome destination details are saved', fakeAsync(() => { - component.outcomeNewStateName = 'End'; - let onSaveOutcomeDestDetailsEmitter = new EventEmitter(); - spyOnProperty(stateEditorService, 'onSaveOutcomeDestDetails') - .and.returnValue(onSaveOutcomeDestDetailsEmitter); - spyOn(stateEditorService, 'getActiveStateName').and.returnValue('Hola'); - spyOn(editorFirstTimeEventsService, 'registerFirstCreateSecondStateEvent'); + component.ngOnInit(); + tick(10); - component.ngOnInit(); - tick(10); + onSaveOutcomeDestDetailsEmitter.emit(); + + expect(component.outcome.dest).toBe('Hola'); + }) + ); + + it( + 'should add new state if outcome destination is a placeholder when' + + ' outcome destination details are saved', + fakeAsync(() => { + component.outcomeNewStateName = 'End'; + let onSaveOutcomeDestDetailsEmitter = new EventEmitter(); + spyOnProperty( + stateEditorService, + 'onSaveOutcomeDestDetails' + ).and.returnValue(onSaveOutcomeDestDetailsEmitter); + spyOn(stateEditorService, 'getActiveStateName').and.returnValue('Hola'); + spyOn( + editorFirstTimeEventsService, + 'registerFirstCreateSecondStateEvent' + ); - onSaveOutcomeDestDetailsEmitter.emit(); + component.ngOnInit(); + tick(10); - expect(component.outcome.dest).toBe('End'); - expect(editorFirstTimeEventsService.registerFirstCreateSecondStateEvent) - .toHaveBeenCalled(); - })); + onSaveOutcomeDestDetailsEmitter.emit(); + + expect(component.outcome.dest).toBe('End'); + expect( + editorFirstTimeEventsService.registerFirstCreateSecondStateEvent + ).toHaveBeenCalled(); + }) + ); + + it( + 'should allow admin and moderators to edit refresher' + ' exploration id', + fakeAsync(() => { + let userInfo = { + isCurriculumAdmin: () => true, + isModerator: () => false, + } as UserInfo; + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve(userInfo) + ); + spyOn(stateEditorService, 'getActiveStateName').and.returnValue('Hola'); - it('should allow admin and moderators to edit refresher' + - ' exploration id', fakeAsync(() => { - let userInfo = { - isCurriculumAdmin: () => true, - isModerator: () => false - } as UserInfo; - spyOn(userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(userInfo)); - spyOn(stateEditorService, 'getActiveStateName').and.returnValue('Hola'); + expect(component.canEditRefresherExplorationId).toBeFalse(); - expect(component.canEditRefresherExplorationId).toBeFalse(); + component.ngOnInit(); + tick(10); - component.ngOnInit(); - tick(10); - - expect(component.canEditRefresherExplorationId).toBeTrue(); - })); + expect(component.canEditRefresherExplorationId).toBeTrue(); + }) + ); it('should update option names when state name is changed', fakeAsync(() => { let onStateNamesChangedEmitter = new EventEmitter(); @@ -200,70 +237,89 @@ describe('Outcome Destination Editor', () => { { Introduction: 'Introduction', State1: 'State1', - End: 'End' - }, [ + End: 'End', + }, + [ { source: 'Introduction', target: 'State1', linkProperty: '', - connectsDestIfStuck: false + connectsDestIfStuck: false, }, { source: 'State1', target: 'End', linkProperty: '', - connectsDestIfStuck: false - } - ], 'Introduction', ['End']); - spyOnProperty(stateEditorService, 'onStateNamesChanged') - .and.returnValue(onStateNamesChangedEmitter); - spyOn(stateEditorService, 'getStateNames') - .and.returnValues( - ['Introduction', 'State1', 'End'], - ['Introduction', 'State2', 'End']); - spyOn(stateGraphLayoutService, 'getLastComputedArrangement') - .and.returnValue(computedLayout); + connectsDestIfStuck: false, + }, + ], + 'Introduction', + ['End'] + ); + spyOnProperty(stateEditorService, 'onStateNamesChanged').and.returnValue( + onStateNamesChangedEmitter + ); + spyOn(stateEditorService, 'getStateNames').and.returnValues( + ['Introduction', 'State1', 'End'], + ['Introduction', 'State2', 'End'] + ); + spyOn( + stateGraphLayoutService, + 'getLastComputedArrangement' + ).and.returnValue(computedLayout); spyOn(stateEditorService, 'getActiveStateName').and.returnValue('Hola'); component.ngOnInit(); tick(10); - expect(component.destinationChoices).toEqual([{ - id: 'Hola', - text: '(try again)' - }, { - id: 'Introduction', - text: 'Introduction' - }, { - id: 'State1', - text: 'State1' - }, { - id: 'End', - text: 'End' - }, { - id: '/', - text: 'A New Card Called...' - }]); + expect(component.destinationChoices).toEqual([ + { + id: 'Hola', + text: '(try again)', + }, + { + id: 'Introduction', + text: 'Introduction', + }, + { + id: 'State1', + text: 'State1', + }, + { + id: 'End', + text: 'End', + }, + { + id: '/', + text: 'A New Card Called...', + }, + ]); onStateNamesChangedEmitter.emit(); tick(10); - expect(component.destinationChoices).toEqual([{ - id: 'Hola', - text: '(try again)' - }, { - id: 'Introduction', - text: 'Introduction' - }, { - id: 'End', - text: 'End' - }, { - id: 'State2', - text: 'State2' - }, { - id: '/', - text: 'A New Card Called...' - }]); + expect(component.destinationChoices).toEqual([ + { + id: 'Hola', + text: '(try again)', + }, + { + id: 'Introduction', + text: 'Introduction', + }, + { + id: 'End', + text: 'End', + }, + { + id: 'State2', + text: 'State2', + }, + { + id: '/', + text: 'A New Card Called...', + }, + ]); })); it('should throw error if active state name is null', fakeAsync(() => { @@ -275,16 +331,19 @@ describe('Outcome Destination Editor', () => { }).toThrowError('Active state name is null'); })); - it('should set focus to new state name input field on destination' + - ' selector change', () => { - spyOn(focusManagerService, 'setFocus'); + it( + 'should set focus to new state name input field on destination' + + ' selector change', + () => { + spyOn(focusManagerService, 'setFocus'); - component.onDestSelectorChange(); + component.onDestSelectorChange(); - expect(focusManagerService.setFocus).toHaveBeenCalledWith( - 'newStateNameInputField' - ); - }); + expect(focusManagerService.setFocus).toHaveBeenCalledWith( + 'newStateNameInputField' + ); + } + ); it('should check if new state is being created', () => { expect(component.isCreatingNewState()).toBeTrue(); @@ -296,7 +355,7 @@ describe('Outcome Destination Editor', () => { false, [], null, - null, + null ); expect(component.isCreatingNewState()).toBeFalse(); @@ -310,7 +369,7 @@ describe('Outcome Destination Editor', () => { false, [], null, - null, + null ); spyOn(component.getChanges, 'emit'); @@ -329,19 +388,18 @@ describe('Outcome Destination Editor', () => { expect(component.outcomeNewStateName).toBe('New State'); }); - it('should return true if outcome destination is a current state name', - () => { - component.outcome = new Outcome( - 'Introduction', - null, - new SubtitledHtml('

HTML string

', 'Id'), - false, - [], - null, - null, - ); - component.currentStateName = 'Introduction'; + it('should return true if outcome destination is a current state name', () => { + component.outcome = new Outcome( + 'Introduction', + null, + new SubtitledHtml('

HTML string

', 'Id'), + false, + [], + null, + null + ); + component.currentStateName = 'Introduction'; - expect(component.isSelfLoop()).toBeTrue(); - }); + expect(component.isSelfLoop()).toBeTrue(); + }); }); diff --git a/core/templates/components/state-directives/outcome-editor/outcome-destination-editor.component.ts b/core/templates/components/state-directives/outcome-editor/outcome-destination-editor.component.ts index fcab25e2e813..6599fb7dc50c 100644 --- a/core/templates/components/state-directives/outcome-editor/outcome-destination-editor.component.ts +++ b/core/templates/components/state-directives/outcome-editor/outcome-destination-editor.component.ts @@ -16,17 +16,17 @@ * @fileoverview Component for the outcome destination editor. */ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { Subscription } from 'rxjs'; +import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {Subscription} from 'rxjs'; import cloneDeep from 'lodash/cloneDeep'; -import { StateGraphLayoutService } from 'components/graph-services/graph-layout.service'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { EditorFirstTimeEventsService } from 'pages/exploration-editor-page/services/editor-first-time-events.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { UserService } from 'services/user.service'; -import { AppConstants } from 'app.constants'; -import { Outcome } from 'domain/exploration/OutcomeObjectFactory'; +import {StateGraphLayoutService} from 'components/graph-services/graph-layout.service'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {EditorFirstTimeEventsService} from 'pages/exploration-editor-page/services/editor-first-time-events.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {UserService} from 'services/user.service'; +import {AppConstants} from 'app.constants'; +import {Outcome} from 'domain/exploration/OutcomeObjectFactory'; interface DestinationChoice { id: string; @@ -39,7 +39,7 @@ interface DestValidation { } @Component({ selector: 'oppia-outcome-destination-editor', - templateUrl: './outcome-destination-editor.component.html' + templateUrl: './outcome-destination-editor.component.html', }) export class OutcomeDestinationEditorComponent implements OnInit { @Output() addState: EventEmitter = new EventEmitter(); @@ -58,24 +58,21 @@ export class OutcomeDestinationEditorComponent implements OnInit { directiveSubscriptions: Subscription = new Subscription(); canAddPrerequisiteSkill: boolean = false; canEditRefresherExplorationId: boolean = false; - ENABLE_PREREQUISITE_SKILLS: boolean = ( - AppConstants.ENABLE_PREREQUISITE_SKILLS); + ENABLE_PREREQUISITE_SKILLS: boolean = AppConstants.ENABLE_PREREQUISITE_SKILLS; - EXPLORATION_AND_SKILL_ID_PATTERN: RegExp = ( - AppConstants.EXPLORATION_AND_SKILL_ID_PATTERN); + EXPLORATION_AND_SKILL_ID_PATTERN: RegExp = + AppConstants.EXPLORATION_AND_SKILL_ID_PATTERN; - MAX_STATE_NAME_LENGTH: number = ( - AppConstants.MAX_STATE_NAME_LENGTH); + MAX_STATE_NAME_LENGTH: number = AppConstants.MAX_STATE_NAME_LENGTH; - PLACEHOLDER_OUTCOME_DEST: string = ( - AppConstants.PLACEHOLDER_OUTCOME_DEST); + PLACEHOLDER_OUTCOME_DEST: string = AppConstants.PLACEHOLDER_OUTCOME_DEST; constructor( private editorFirstTimeEventsService: EditorFirstTimeEventsService, private focusManagerService: FocusManagerService, private stateEditorService: StateEditorService, private stateGraphLayoutService: StateGraphLayoutService, - private userService: UserService, + private userService: UserService ) {} isSelfLoop(): boolean { @@ -89,7 +86,7 @@ export class OutcomeDestinationEditorComponent implements OnInit { let validation = { isCreatingNewState: this.isCreatingNewState(), - value: $event + value: $event, }; this.getChanges.emit(validation); } @@ -101,7 +98,7 @@ export class OutcomeDestinationEditorComponent implements OnInit { let validation = { isCreatingNewState: this.isCreatingNewState(), - value: this.outcomeNewStateName + value: this.outcomeNewStateName, }; this.getChanges.emit(validation); } @@ -122,15 +119,17 @@ export class OutcomeDestinationEditorComponent implements OnInit { // This is a list of objects, each with an ID and name. These // represent all states, as well as an option to create a // new state. - this.destinationChoices = [{ - id: this.currentStateName, - text: '(try again)' - }]; + this.destinationChoices = [ + { + id: this.currentStateName, + text: '(try again)', + }, + ]; // Arrange the remaining states based on their order in the state // graph. - let lastComputedArrangement = ( - this.stateGraphLayoutService.getLastComputedArrangement()); + let lastComputedArrangement = + this.stateGraphLayoutService.getLastComputedArrangement(); let allStateNames = this.stateEditorService.getStateNames(); // It is possible that lastComputedArrangement is null if the @@ -143,9 +142,13 @@ export class OutcomeDestinationEditorComponent implements OnInit { let maxOffset = 0; for (let stateName in lastComputedArrangement) { maxDepth = Math.max( - maxDepth, lastComputedArrangement[stateName].depth); + maxDepth, + lastComputedArrangement[stateName].depth + ); maxOffset = Math.max( - maxOffset, lastComputedArrangement[stateName].offset); + maxOffset, + lastComputedArrangement[stateName].offset + ); } // Higher scores come later. @@ -154,16 +157,15 @@ export class OutcomeDestinationEditorComponent implements OnInit { for (let i = 0; i < allStateNames.length; i++) { stateName = allStateNames[i]; if (lastComputedArrangement.hasOwnProperty(stateName)) { - allStateScores[stateName] = ( - lastComputedArrangement[stateName].depth * - (maxOffset + 1) + - lastComputedArrangement[stateName].offset); + allStateScores[stateName] = + lastComputedArrangement[stateName].depth * (maxOffset + 1) + + lastComputedArrangement[stateName].offset; } else { // States that have just been added in the rule 'create new' // modal are not yet included as part of // lastComputedArrangement so we account for them here. - allStateScores[stateName] = ( - (maxDepth + 1) * (maxOffset + 1) + unarrangedStateCount); + allStateScores[stateName] = + (maxDepth + 1) * (maxOffset + 1) + unarrangedStateCount; unarrangedStateCount++; } } @@ -177,16 +179,16 @@ export class OutcomeDestinationEditorComponent implements OnInit { if (stateNames[i] !== this.currentStateName) { this.destinationChoices.push({ id: stateNames[i], - text: stateNames[i] + text: stateNames[i], }); } } this.destinationChoices.push({ id: this.PLACEHOLDER_OUTCOME_DEST, - text: 'A New Card Called...' + text: 'A New Card Called...', }); - // This value of 10ms is arbitrary, it has no significance. + // This value of 10ms is arbitrary, it has no significance. }, 10); } @@ -195,33 +197,33 @@ export class OutcomeDestinationEditorComponent implements OnInit { this.stateEditorService.onSaveOutcomeDestDetails.subscribe(() => { // Create new state if specified. if (this.outcome.dest === this.PLACEHOLDER_OUTCOME_DEST) { - this.editorFirstTimeEventsService - .registerFirstCreateSecondStateEvent(); + this.editorFirstTimeEventsService.registerFirstCreateSecondStateEvent(); let newStateName = this.outcomeNewStateName.trim(); this.outcome.dest = newStateName; this.addState.emit(newStateName); } - })); + }) + ); this.updateOptionNames(); this.directiveSubscriptions.add( this.stateEditorService.onStateNamesChanged.subscribe(() => { this.updateOptionNames(); - })); - this.canAddPrerequisiteSkill = ( + }) + ); + this.canAddPrerequisiteSkill = this.ENABLE_PREREQUISITE_SKILLS && - this.stateEditorService.isExplorationCurated()); + this.stateEditorService.isExplorationCurated(); this.canEditRefresherExplorationId = false; - this.userService.getUserInfoAsync().then((userInfo) => { + this.userService.getUserInfoAsync().then(userInfo => { // We restrict editing of refresher exploration IDs to // admins/moderators for now, since the feature is still in // development. - this.canEditRefresherExplorationId = ( - userInfo.isCurriculumAdmin() || userInfo.isModerator()); + this.canEditRefresherExplorationId = + userInfo.isCurriculumAdmin() || userInfo.isModerator(); }); - this.explorationAndSkillIdPattern = ( - this.EXPLORATION_AND_SKILL_ID_PATTERN); + this.explorationAndSkillIdPattern = this.EXPLORATION_AND_SKILL_ID_PATTERN; this.newStateNamePattern = /^[a-zA-Z0-9.\s-]+$/; this.destinationChoices = []; } @@ -231,7 +233,9 @@ export class OutcomeDestinationEditorComponent implements OnInit { } } -angular.module('oppia').directive('oppiaOutcomeDestinationEditor', +angular.module('oppia').directive( + 'oppiaOutcomeDestinationEditor', downgradeComponent({ - component: OutcomeDestinationEditorComponent - }) as angular.IDirectiveFactory); + component: OutcomeDestinationEditorComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/components/state-directives/outcome-editor/outcome-editor.component.spec.ts b/core/templates/components/state-directives/outcome-editor/outcome-editor.component.spec.ts index fc3e1c730e97..6d94376ab50b 100644 --- a/core/templates/components/state-directives/outcome-editor/outcome-editor.component.spec.ts +++ b/core/templates/components/state-directives/outcome-editor/outcome-editor.component.spec.ts @@ -16,19 +16,27 @@ * @fileoverview Unit tests for outcome editor component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { StateInteractionIdService } from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; -import { Outcome, OutcomeObjectFactory } from 'domain/exploration/OutcomeObjectFactory'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { AddOutcomeModalComponent } from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/add-outcome-modal.component'; -import { of } from 'rxjs'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { ExternalSaveService } from 'services/external-save.service'; -import { OutcomeEditorComponent } from './outcome-editor.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {StateInteractionIdService} from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; +import { + Outcome, + OutcomeObjectFactory, +} from 'domain/exploration/OutcomeObjectFactory'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {AddOutcomeModalComponent} from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/add-outcome-modal.component'; +import {of} from 'rxjs'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {ExternalSaveService} from 'services/external-save.service'; +import {OutcomeEditorComponent} from './outcome-editor.component'; class MockWindowDimensionsService { getResizeEvent() { @@ -53,23 +61,18 @@ describe('Outcome Editor Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - ], - declarations: [ - OutcomeEditorComponent, - AddOutcomeModalComponent - ], + imports: [HttpClientTestingModule], + declarations: [OutcomeEditorComponent, AddOutcomeModalComponent], providers: [ ExternalSaveService, StateEditorService, StateInteractionIdService, { provide: WindowDimensionsService, - useClass: MockWindowDimensionsService - } + useClass: MockWindowDimensionsService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -98,12 +101,14 @@ describe('Outcome Editor Component', () => { true, [], null, - null, + null ); component.outcome = outcome; const windowResizeSpy = spyOn( - windowDimensionsService, 'getResizeEvent').and.callThrough(); + windowDimensionsService, + 'getResizeEvent' + ).and.callThrough(); expect(component.savedOutcome).toBeUndefined(); @@ -116,95 +121,116 @@ describe('Outcome Editor Component', () => { expect(component.onMobile).toBeTrue(); }); - it('should save feedback on external save event when editFeedbackForm is' + - ' valid and state is not invalid after feedback save', () => { - let onExternalSaveEmitter = new EventEmitter(); - spyOnProperty(externalSaveService, 'onExternalSave') - .and.returnValue(onExternalSaveEmitter); - spyOn(component, 'invalidStateAfterFeedbackSave').and.returnValue(false); - spyOn(component, 'saveThisFeedback'); - - component.ngOnInit(); - - component.feedbackEditorIsOpen = true; - - onExternalSaveEmitter.emit(); - - expect(component.saveThisFeedback).toHaveBeenCalled(); - }); - - it('should cancel feedback edit on external save event when' + - ' editFeedbackForm is not valid or state us not valid after' + - ' feedback save', () => { - let onExternalSaveEmitter = new EventEmitter(); - spyOnProperty(externalSaveService, 'onExternalSave') - .and.returnValue(onExternalSaveEmitter); - spyOn(component, 'invalidStateAfterFeedbackSave').and.returnValue(true); - - component.ngOnInit(); - - // Setup. No pre-check as we are setting up values below. - component.feedbackEditorIsOpen = true; - component.savedOutcome = new Outcome( - 'Introduction', - null, - new SubtitledHtml('

Saved Outcome

', 'Id'), - true, - [], - null, - null, - ); - component.outcome = new Outcome( - 'Introduction', - null, - new SubtitledHtml('

Outcome

', 'Id'), - true, - [], - null, - null, - ); - - // Action. - onExternalSaveEmitter.emit(); - - // Post-check. - expect(component.feedbackEditorIsOpen).toBeFalse(); - expect(component.outcome.feedback).toEqual( - new SubtitledHtml('

Saved Outcome

', 'Id')); - }); - - it('should save destination on interaction change when edit destination' + - ' form is valid and state is not invalid after destination save', () => { - let onInteractionIdChangedEmitter = new EventEmitter(); - spyOnProperty(stateInteractionIdService, 'onInteractionIdChanged') - .and.returnValue(onInteractionIdChangedEmitter); - spyOn(component, 'invalidStateAfterDestinationSave').and.returnValue(false); - spyOn(component, 'saveThisDestination'); - - component.ngOnInit(); - - component.destinationEditorIsOpen = true; - - onInteractionIdChangedEmitter.emit(); - - expect(component.saveThisDestination).toHaveBeenCalled(); - }); - - it('should save destination for the stuck learner on interaction change' + - ' when state is not invalid after destination save', () => { - let onInteractionIdChangedEmitter = new EventEmitter(); - spyOnProperty(stateInteractionIdService, 'onInteractionIdChanged') - .and.returnValue(onInteractionIdChangedEmitter); - spyOn(component, 'saveThisIfStuckDestination'); - - component.ngOnInit(); - - component.destinationIfStuckEditorIsOpen = true; - - onInteractionIdChangedEmitter.emit(); - - expect(component.saveThisIfStuckDestination).toHaveBeenCalled(); - }); + it( + 'should save feedback on external save event when editFeedbackForm is' + + ' valid and state is not invalid after feedback save', + () => { + let onExternalSaveEmitter = new EventEmitter(); + spyOnProperty(externalSaveService, 'onExternalSave').and.returnValue( + onExternalSaveEmitter + ); + spyOn(component, 'invalidStateAfterFeedbackSave').and.returnValue(false); + spyOn(component, 'saveThisFeedback'); + + component.ngOnInit(); + + component.feedbackEditorIsOpen = true; + + onExternalSaveEmitter.emit(); + + expect(component.saveThisFeedback).toHaveBeenCalled(); + } + ); + + it( + 'should cancel feedback edit on external save event when' + + ' editFeedbackForm is not valid or state us not valid after' + + ' feedback save', + () => { + let onExternalSaveEmitter = new EventEmitter(); + spyOnProperty(externalSaveService, 'onExternalSave').and.returnValue( + onExternalSaveEmitter + ); + spyOn(component, 'invalidStateAfterFeedbackSave').and.returnValue(true); + + component.ngOnInit(); + + // Setup. No pre-check as we are setting up values below. + component.feedbackEditorIsOpen = true; + component.savedOutcome = new Outcome( + 'Introduction', + null, + new SubtitledHtml('

Saved Outcome

', 'Id'), + true, + [], + null, + null + ); + component.outcome = new Outcome( + 'Introduction', + null, + new SubtitledHtml('

Outcome

', 'Id'), + true, + [], + null, + null + ); + + // Action. + onExternalSaveEmitter.emit(); + + // Post-check. + expect(component.feedbackEditorIsOpen).toBeFalse(); + expect(component.outcome.feedback).toEqual( + new SubtitledHtml('

Saved Outcome

', 'Id') + ); + } + ); + + it( + 'should save destination on interaction change when edit destination' + + ' form is valid and state is not invalid after destination save', + () => { + let onInteractionIdChangedEmitter = new EventEmitter(); + spyOnProperty( + stateInteractionIdService, + 'onInteractionIdChanged' + ).and.returnValue(onInteractionIdChangedEmitter); + spyOn(component, 'invalidStateAfterDestinationSave').and.returnValue( + false + ); + spyOn(component, 'saveThisDestination'); + + component.ngOnInit(); + + component.destinationEditorIsOpen = true; + + onInteractionIdChangedEmitter.emit(); + + expect(component.saveThisDestination).toHaveBeenCalled(); + } + ); + + it( + 'should save destination for the stuck learner on interaction change' + + ' when state is not invalid after destination save', + () => { + let onInteractionIdChangedEmitter = new EventEmitter(); + spyOnProperty( + stateInteractionIdService, + 'onInteractionIdChanged' + ).and.returnValue(onInteractionIdChangedEmitter); + spyOn(component, 'saveThisIfStuckDestination'); + + component.ngOnInit(); + + component.destinationIfStuckEditorIsOpen = true; + + onInteractionIdChangedEmitter.emit(); + + expect(component.saveThisIfStuckDestination).toHaveBeenCalled(); + } + ); it('should cancel destination if-stuck edit correctly', () => { component.ngOnInit(); @@ -218,7 +244,7 @@ describe('Outcome Editor Component', () => { true, [], '', - '', + '' ); component.outcome = new Outcome( 'Saved Outcome', @@ -227,7 +253,7 @@ describe('Outcome Editor Component', () => { true, [], '', - '', + '' ); // Action. @@ -238,46 +264,53 @@ describe('Outcome Editor Component', () => { expect(component.outcome.destIfReallyStuck).toBe('Stuck state'); }); - it('should cancel destination edit on interaction change when edit' + - ' destination form is not valid or state is invalid after' + - ' destination save', () => { - let onInteractionIdChangedEmitter = new EventEmitter(); - spyOnProperty(stateInteractionIdService, 'onInteractionIdChanged') - .and.returnValue(onInteractionIdChangedEmitter); - spyOn(component, 'invalidStateAfterDestinationSave').and.returnValue(true); - - component.ngOnInit(); - - // Setup. No pre-check as we are setting up values below. - component.destinationEditorIsOpen = true; - component.savedOutcome = new Outcome( - 'Introduction', - null, - new SubtitledHtml('

Saved Outcome

', 'Id'), - true, - [], - 'ExpId', - 'SkillId', - ); - component.outcome = new Outcome( - 'Saved Outcome', - null, - new SubtitledHtml('

Outcome

', 'Id'), - true, - [], - '', - '', - ); - - // Action. - onInteractionIdChangedEmitter.emit(); - - // Post-check. - expect(component.destinationEditorIsOpen).toBeFalse(); - expect(component.outcome.dest).toBe('Introduction'); - expect(component.outcome.refresherExplorationId).toBe('ExpId'); - expect(component.outcome.missingPrerequisiteSkillId).toBe('SkillId'); - }); + it( + 'should cancel destination edit on interaction change when edit' + + ' destination form is not valid or state is invalid after' + + ' destination save', + () => { + let onInteractionIdChangedEmitter = new EventEmitter(); + spyOnProperty( + stateInteractionIdService, + 'onInteractionIdChanged' + ).and.returnValue(onInteractionIdChangedEmitter); + spyOn(component, 'invalidStateAfterDestinationSave').and.returnValue( + true + ); + + component.ngOnInit(); + + // Setup. No pre-check as we are setting up values below. + component.destinationEditorIsOpen = true; + component.savedOutcome = new Outcome( + 'Introduction', + null, + new SubtitledHtml('

Saved Outcome

', 'Id'), + true, + [], + 'ExpId', + 'SkillId' + ); + component.outcome = new Outcome( + 'Saved Outcome', + null, + new SubtitledHtml('

Outcome

', 'Id'), + true, + [], + '', + '' + ); + + // Action. + onInteractionIdChangedEmitter.emit(); + + // Post-check. + expect(component.destinationEditorIsOpen).toBeFalse(); + expect(component.outcome.dest).toBe('Introduction'); + expect(component.outcome.refresherExplorationId).toBe('ExpId'); + expect(component.outcome.missingPrerequisiteSkillId).toBe('SkillId'); + } + ); it('should check if state is in question mode', () => { spyOn(stateEditorService, 'isInQuestionMode').and.returnValue(true); @@ -285,7 +318,7 @@ describe('Outcome Editor Component', () => { expect(component.isInQuestionMode()).toBeTrue(); }); - it('should get current interaction\'s ID', () => { + it("should get current interaction's ID", () => { stateInteractionIdService.savedMemento = 'TextInput'; expect(component.getCurrentInteractionId()).toBe('TextInput'); @@ -311,11 +344,10 @@ describe('Outcome Editor Component', () => { true, [], null, - null, + null ); component.outcome = outcome; - spyOn(stateEditorService, 'getActiveStateName') - .and.returnValue('Hola'); + spyOn(stateEditorService, 'getActiveStateName').and.returnValue('Hola'); expect(component.isSelfLoop(outcome)).toBeTrue(); @@ -326,49 +358,70 @@ describe('Outcome Editor Component', () => { true, [], null, - null, + null ); component.outcome = outcome; expect(component.isSelfLoop(outcome)).toBeFalse(); }); it('should check if state if of self loop with no feedback', () => { - spyOn(stateEditorService, 'getActiveStateName') - .and.returnValue('State Name'); - let outcome = outcomeObjectFactory.createNew( - 'State Name', '1', '', []); + spyOn(stateEditorService, 'getActiveStateName').and.returnValue( + 'State Name' + ); + let outcome = outcomeObjectFactory.createNew('State Name', '1', '', []); expect(component.isSelfLoopWithNoFeedback(outcome)).toBe(true); - outcome = outcomeObjectFactory.createNew( - '', '', '', []); + outcome = outcomeObjectFactory.createNew('', '', '', []); expect(component.isSelfLoopWithNoFeedback(outcome)).toBe(false); }); - it('should check if state will become invalid after feedback' + - ' is saved', () => { - spyOn(stateEditorService, 'getActiveStateName') - .and.returnValue('State Name'); - component.outcome = outcomeObjectFactory.createNew( - 'Introduction', '1', '', []); - component.savedOutcome = outcomeObjectFactory.createNew( - 'State Name', '1', '', []); - - expect(component.invalidStateAfterFeedbackSave()).toBeTrue(); - }); - - it('should check if state will become invalid after destination' + - ' is saved', () => { - spyOn(stateEditorService, 'getActiveStateName') - .and.returnValue('Introduction'); - component.outcome = outcomeObjectFactory.createNew( - 'Introduction', '1', '', []); - component.savedOutcome = outcomeObjectFactory.createNew( - 'State Name', '1', '', []); - - expect(component.invalidStateAfterDestinationSave()).toBeTrue(); - }); + it( + 'should check if state will become invalid after feedback' + ' is saved', + () => { + spyOn(stateEditorService, 'getActiveStateName').and.returnValue( + 'State Name' + ); + component.outcome = outcomeObjectFactory.createNew( + 'Introduction', + '1', + '', + [] + ); + component.savedOutcome = outcomeObjectFactory.createNew( + 'State Name', + '1', + '', + [] + ); + + expect(component.invalidStateAfterFeedbackSave()).toBeTrue(); + } + ); + + it( + 'should check if state will become invalid after destination' + ' is saved', + () => { + spyOn(stateEditorService, 'getActiveStateName').and.returnValue( + 'Introduction' + ); + component.outcome = outcomeObjectFactory.createNew( + 'Introduction', + '1', + '', + [] + ); + component.savedOutcome = outcomeObjectFactory.createNew( + 'State Name', + '1', + '', + [] + ); + + expect(component.invalidStateAfterDestinationSave()).toBeTrue(); + } + ); it('should open feedback editor if it is editable', () => { component.feedbackEditorIsOpen = false; @@ -387,19 +440,19 @@ describe('Outcome Editor Component', () => { true, [], null, - null, + null ); spyOn(ngbModal, 'open').and.returnValue({ componentInstance: { - outcome: outcome + outcome: outcome, }, result: Promise.resolve({ - outcome: outcome - }) + outcome: outcome, + }), } as NgbModalRef); - spyOn(component, 'saveThisFeedback').and.callFake(()=>{ + spyOn(component, 'saveThisFeedback').and.callFake(() => { component.feedbackEditorIsOpen = false; }); @@ -429,7 +482,7 @@ describe('Outcome Editor Component', () => { false, [], 'ExpId', - 'SkillId', + 'SkillId' ); component.outcome = new Outcome( 'Introduction', @@ -438,7 +491,7 @@ describe('Outcome Editor Component', () => { true, [], '', - '', + '' ); component.openDestinationIfStuckEditor(); expect(component.destinationIfStuckEditorIsOpen).toBeTrue(); @@ -453,7 +506,7 @@ describe('Outcome Editor Component', () => { false, [], 'ExpId', - 'SkillId', + 'SkillId' ); component.outcome = new Outcome( 'Introduction', @@ -462,7 +515,7 @@ describe('Outcome Editor Component', () => { true, [], '', - '', + '' ); component.onChangeCorrectnessLabel(); @@ -478,7 +531,7 @@ describe('Outcome Editor Component', () => { false, [], 'ExpId', - 'SkillId', + 'SkillId' ); component.outcome = new Outcome( 'Dest', @@ -487,7 +540,7 @@ describe('Outcome Editor Component', () => { true, [], '', - '', + '' ); spyOn(stateEditorService, 'isInQuestionMode').and.returnValue(false); spyOn(stateEditorService, 'getActiveStateName').and.returnValue('Hola'); @@ -505,7 +558,7 @@ describe('Outcome Editor Component', () => { false, [], 'ExpId', - 'SkillId', + 'SkillId' ); component.outcome = new Outcome( 'Dest', @@ -514,7 +567,7 @@ describe('Outcome Editor Component', () => { true, [], '', - '', + '' ); spyOn(stateEditorService, 'isInQuestionMode').and.returnValue(false); spyOn(stateEditorService, 'getActiveStateName').and.returnValue(null); @@ -524,89 +577,98 @@ describe('Outcome Editor Component', () => { }).toThrowError('The active state name is null in the outcome editor.'); }); - it('should set refresher exploration ID as null on saving destination' + - ' when state is not in self loop', () => { - component.savedOutcome = new Outcome( - 'Saved Dest', - null, - new SubtitledHtml('

Saved Outcome

', 'savedContentId'), - false, - [], - 'ExpId', - '', - ); - component.outcome = new Outcome( - 'Dest', - null, - new SubtitledHtml('

Outcome

', 'contentId'), - true, - [], - 'OutcomeExpId', - 'SkillId', - ); - spyOn(stateEditorService, 'getActiveStateName').and.returnValue('Dest1'); - - component.saveThisDestination(); - - expect(component.outcome.refresherExplorationId).toBe(null); - expect(component.savedOutcome.refresherExplorationId).toBe(null); - expect(component.savedOutcome.missingPrerequisiteSkillId).toBe('SkillId'); - }); - - it('should set labelled as correct to false on saving destination' + - ' when state is in self loop', () => { - component.savedOutcome = new Outcome( - 'Saved Dest', - null, - new SubtitledHtml('

Saved Outcome

', 'savedContentId'), - false, - [], - 'ExpId', - '', - ); - component.outcome = new Outcome( - 'Dest1', - null, - new SubtitledHtml('

Outcome

', 'contentId'), - true, - [], - 'OutcomeExpId', - 'SkillId', - ); - spyOn(stateEditorService, 'getActiveStateName').and.returnValue('Dest1'); - const changeCorrectnessSpy = spyOn(component, 'onChangeCorrectnessLabel'); - - component.saveThisDestination(); - - expect(component.outcome.labelledAsCorrect).toBe(false); - expect(changeCorrectnessSpy).toHaveBeenCalled(); - }); - - it('should set the dest_if_really_stuck property correctly' + - 'when the destination for the stuck learner is saved.', () => { - component.savedOutcome = new Outcome( - 'Dest', - null, - new SubtitledHtml('

Saved Outcome

', 'savedContentId'), - false, - [], - 'ExpId', - '', - ); - component.outcome = new Outcome( - 'Dest', - 'Stuck state', - new SubtitledHtml('

Outcome

', 'contentId'), - true, - [], - 'OutcomeExpId', - 'SkillId', - ); - - component.saveThisIfStuckDestination(); - - expect(component.savedOutcome.destIfReallyStuck).toBe('Stuck state'); - }); + it( + 'should set refresher exploration ID as null on saving destination' + + ' when state is not in self loop', + () => { + component.savedOutcome = new Outcome( + 'Saved Dest', + null, + new SubtitledHtml('

Saved Outcome

', 'savedContentId'), + false, + [], + 'ExpId', + '' + ); + component.outcome = new Outcome( + 'Dest', + null, + new SubtitledHtml('

Outcome

', 'contentId'), + true, + [], + 'OutcomeExpId', + 'SkillId' + ); + spyOn(stateEditorService, 'getActiveStateName').and.returnValue('Dest1'); + + component.saveThisDestination(); + + expect(component.outcome.refresherExplorationId).toBe(null); + expect(component.savedOutcome.refresherExplorationId).toBe(null); + expect(component.savedOutcome.missingPrerequisiteSkillId).toBe('SkillId'); + } + ); + + it( + 'should set labelled as correct to false on saving destination' + + ' when state is in self loop', + () => { + component.savedOutcome = new Outcome( + 'Saved Dest', + null, + new SubtitledHtml('

Saved Outcome

', 'savedContentId'), + false, + [], + 'ExpId', + '' + ); + component.outcome = new Outcome( + 'Dest1', + null, + new SubtitledHtml('

Outcome

', 'contentId'), + true, + [], + 'OutcomeExpId', + 'SkillId' + ); + spyOn(stateEditorService, 'getActiveStateName').and.returnValue('Dest1'); + const changeCorrectnessSpy = spyOn(component, 'onChangeCorrectnessLabel'); + + component.saveThisDestination(); + + expect(component.outcome.labelledAsCorrect).toBe(false); + expect(changeCorrectnessSpy).toHaveBeenCalled(); + } + ); + + it( + 'should set the dest_if_really_stuck property correctly' + + 'when the destination for the stuck learner is saved.', + () => { + component.savedOutcome = new Outcome( + 'Dest', + null, + new SubtitledHtml('

Saved Outcome

', 'savedContentId'), + false, + [], + 'ExpId', + '' + ); + component.outcome = new Outcome( + 'Dest', + 'Stuck state', + new SubtitledHtml('

Outcome

', 'contentId'), + true, + [], + 'OutcomeExpId', + 'SkillId' + ); + + component.saveThisIfStuckDestination(); + + expect(component.savedOutcome.destIfReallyStuck).toBe('Stuck state'); + } + ); it('should check if outcome feedback exceeds 10000 characters', () => { component.outcome = new Outcome( @@ -616,7 +678,7 @@ describe('Outcome Editor Component', () => { true, [], 'OutcomeExpId', - 'SkillId', + 'SkillId' ); expect(component.isFeedbackLengthExceeded()).toBeFalse(); @@ -627,7 +689,7 @@ describe('Outcome Editor Component', () => { true, [], 'OutcomeExpId', - 'SkillId', + 'SkillId' ); expect(component.isFeedbackLengthExceeded()).toBeTrue(); }); diff --git a/core/templates/components/state-directives/outcome-editor/outcome-editor.component.ts b/core/templates/components/state-directives/outcome-editor/outcome-editor.component.ts index 5287690973cd..0b6a71f2fe94 100644 --- a/core/templates/components/state-directives/outcome-editor/outcome-editor.component.ts +++ b/core/templates/components/state-directives/outcome-editor/outcome-editor.component.ts @@ -16,21 +16,27 @@ * @fileoverview Component for the outcome editor. */ -import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import { + ChangeDetectorRef, + Component, + EventEmitter, + Input, + OnInit, + Output, +} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; import cloneDeep from 'lodash/cloneDeep'; -import { AppConstants } from 'app.constants'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { StateInteractionIdService } from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; -import { Outcome } from 'domain/exploration/OutcomeObjectFactory'; -import { Subscription } from 'rxjs'; -import { ExternalSaveService } from 'services/external-save.service'; +import {AppConstants} from 'app.constants'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {StateInteractionIdService} from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; +import {Outcome} from 'domain/exploration/OutcomeObjectFactory'; +import {Subscription} from 'rxjs'; +import {ExternalSaveService} from 'services/external-save.service'; import INTERACTION_SPECS from 'interactions/interaction_specs.json'; -import { InteractionSpecsKey } from 'pages/interaction-specs.constants'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { AddOutcomeModalComponent } from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/add-outcome-modal.component'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; - +import {InteractionSpecsKey} from 'pages/interaction-specs.constants'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {AddOutcomeModalComponent} from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/add-outcome-modal.component'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; interface AddOutcomeModalResponse { outcome: Outcome; @@ -38,7 +44,7 @@ interface AddOutcomeModalResponse { @Component({ selector: 'oppia-outcome-editor', - templateUrl: './outcome-editor.component.html' + templateUrl: './outcome-editor.component.html', }) export class OutcomeEditorComponent implements OnInit { @Output() saveDest: EventEmitter = new EventEmitter(); @@ -82,19 +88,23 @@ export class OutcomeEditorComponent implements OnInit { } shouldShowDestIfReallyStuck(): boolean { - return !this.savedOutcome.labelledAsCorrect || - this.savedOutcome.destIfReallyStuck !== null; + return ( + !this.savedOutcome.labelledAsCorrect || + this.savedOutcome.destIfReallyStuck !== null + ); } isFeedbackLengthExceeded(): boolean { // TODO(#13764): Edit this check after appropriate limits are found. - return (this.outcome.feedback._html.length > 10000); + return this.outcome.feedback._html.length > 10000; } isCurrentInteractionLinear(): boolean { let interactionId = this.getCurrentInteractionId(); - return Boolean(interactionId) && INTERACTION_SPECS[ - interactionId as InteractionSpecsKey].is_linear; + return ( + Boolean(interactionId) && + INTERACTION_SPECS[interactionId as InteractionSpecsKey].is_linear + ); } onExternalSave(): void { @@ -129,9 +139,9 @@ export class OutcomeEditorComponent implements OnInit { } isSelfLoop(outcome: Outcome): boolean { - return Boolean ( - outcome && - outcome.dest === this.stateEditorService.getActiveStateName()); + return Boolean( + outcome && outcome.dest === this.stateEditorService.getActiveStateName() + ); } getCurrentInteractionId(): string { @@ -139,9 +149,7 @@ export class OutcomeEditorComponent implements OnInit { } isSelfLoopWithNoFeedback(outcome: Outcome): boolean { - return Boolean ( - this.isSelfLoop(outcome) && - !outcome.hasNonemptyFeedback()); + return Boolean(this.isSelfLoop(outcome) && !outcome.hasNonemptyFeedback()); } invalidStateAfterFeedbackSave(): boolean { @@ -165,14 +173,17 @@ export class OutcomeEditorComponent implements OnInit { let currentOutcome = cloneDeep(this.outcome); modalRef.componentInstance.outcome = currentOutcome; - modalRef.result.then((result: AddOutcomeModalResponse): void => { - this.outcome = result.outcome; - this.saveThisFeedback(); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + modalRef.result.then( + (result: AddOutcomeModalResponse): void => { + this.outcome = result.outcome; + this.saveThisFeedback(); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } } @@ -196,21 +207,19 @@ export class OutcomeEditorComponent implements OnInit { saveThisFeedback(): void { this.feedbackEditorIsOpen = false; - this.savedOutcome.feedback = cloneDeep( - this.outcome.feedback); + this.savedOutcome.feedback = cloneDeep(this.outcome.feedback); if ( !this.stateEditorService.isInQuestionMode() && this.savedOutcome.dest === this.outcome.dest && - !this.stateEditorService.getStateNames().includes( - this.outcome.dest)) { + !this.stateEditorService.getStateNames().includes(this.outcome.dest) + ) { // If the stateName has changed and previously saved // destination points to the older name, update it to // the active state name. let activeStateName = this.stateEditorService.getActiveStateName(); if (activeStateName === null) { - throw new Error( - 'The active state name is null in the outcome editor.'); + throw new Error('The active state name is null in the outcome editor.'); } this.savedOutcome.dest = activeStateName; } @@ -229,8 +238,8 @@ export class OutcomeEditorComponent implements OnInit { this.onChangeCorrectnessLabel(); } } - this.savedOutcome.refresherExplorationId = ( - this.outcome.refresherExplorationId); + this.savedOutcome.refresherExplorationId = + this.outcome.refresherExplorationId; this.savedOutcome.missingPrerequisiteSkillId = this.outcome.missingPrerequisiteSkillId; @@ -240,63 +249,65 @@ export class OutcomeEditorComponent implements OnInit { saveThisIfStuckDestination(): void { this.stateEditorService.onSaveOutcomeDestIfStuckDetails.emit(); this.destinationIfStuckEditorIsOpen = false; - this.savedOutcome.destIfReallyStuck = ( - cloneDeep(this.outcome.destIfReallyStuck)); + this.savedOutcome.destIfReallyStuck = cloneDeep( + this.outcome.destIfReallyStuck + ); this.saveDestIfStuck.emit(this.savedOutcome); } onChangeCorrectnessLabel(): void { - this.savedOutcome.labelledAsCorrect = ( - this.outcome.labelledAsCorrect); + this.savedOutcome.labelledAsCorrect = this.outcome.labelledAsCorrect; this.saveCorrectnessLabel.emit(this.savedOutcome); } cancelThisFeedbackEdit(): void { - this.outcome.feedback = cloneDeep( - this.savedOutcome.feedback); + this.outcome.feedback = cloneDeep(this.savedOutcome.feedback); this.feedbackEditorIsOpen = false; } cancelThisDestinationEdit(): void { this.outcome.dest = cloneDeep(this.savedOutcome.dest); - this.outcome.refresherExplorationId = ( - this.savedOutcome.refresherExplorationId); + this.outcome.refresherExplorationId = + this.savedOutcome.refresherExplorationId; this.outcome.missingPrerequisiteSkillId = this.savedOutcome.missingPrerequisiteSkillId; this.destinationEditorIsOpen = false; } cancelThisIfStuckDestinationEdit(): void { - this.outcome.destIfReallyStuck = ( - cloneDeep(this.savedOutcome.destIfReallyStuck)); + this.outcome.destIfReallyStuck = cloneDeep( + this.savedOutcome.destIfReallyStuck + ); this.destinationIfStuckEditorIsOpen = false; } ngOnInit(): void { this.directiveSubscriptions.add( - this.externalSaveService.onExternalSave.subscribe( - () => this.onExternalSave() + this.externalSaveService.onExternalSave.subscribe(() => + this.onExternalSave() ) ); this.directiveSubscriptions.add( - this.stateInteractionIdService.onInteractionIdChanged.subscribe( - () => this.onExternalSave()) + this.stateInteractionIdService.onInteractionIdChanged.subscribe(() => + this.onExternalSave() + ) ); - this.canAddPrerequisiteSkill = ( + this.canAddPrerequisiteSkill = this.ENABLE_PREREQUISITE_SKILLS && - this.stateEditorService.isExplorationCurated()); + this.stateEditorService.isExplorationCurated(); this.feedbackEditorIsOpen = false; this.destinationEditorIsOpen = false; this.correctnessLabelEditorIsOpen = false; this.savedOutcome = cloneDeep(this.outcome); - this.onMobile = ( - this.windowDimensionsService.getWidth() <= this.mobileBreakpoint); - this.resizeSubscription = this.windowDimensionsService.getResizeEvent() + this.onMobile = + this.windowDimensionsService.getWidth() <= this.mobileBreakpoint; + this.resizeSubscription = this.windowDimensionsService + .getResizeEvent() .subscribe(event => { - this.onMobile = ( - this.windowDimensionsService.getWidth() <= this.mobileBreakpoint); + this.onMobile = + this.windowDimensionsService.getWidth() <= this.mobileBreakpoint; }); } @@ -305,7 +316,9 @@ export class OutcomeEditorComponent implements OnInit { } } -angular.module('oppia').directive('oppiaOutcomeEditor', -downgradeComponent({ - component: OutcomeEditorComponent -}) as angular.IDirectiveFactory); +angular.module('oppia').directive( + 'oppiaOutcomeEditor', + downgradeComponent({ + component: OutcomeEditorComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/components/state-directives/outcome-editor/outcome-feedback-editor.component.spec.ts b/core/templates/components/state-directives/outcome-editor/outcome-feedback-editor.component.spec.ts index 56c8ec40ae82..04fe50fe5d35 100644 --- a/core/templates/components/state-directives/outcome-editor/outcome-feedback-editor.component.spec.ts +++ b/core/templates/components/state-directives/outcome-editor/outcome-feedback-editor.component.spec.ts @@ -16,12 +16,12 @@ * @fileoverview Unit tests for OutcomeFeedbackEditorComponent. */ -import { ChangeDetectorRef, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { Outcome } from 'domain/exploration/OutcomeObjectFactory'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { ContextService } from 'services/context.service'; -import { OutcomeFeedbackEditorComponent } from './outcome-feedback-editor.component'; +import {ChangeDetectorRef, NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {Outcome} from 'domain/exploration/OutcomeObjectFactory'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {ContextService} from 'services/context.service'; +import {OutcomeFeedbackEditorComponent} from './outcome-feedback-editor.component'; describe('Outcome Feedback Editor Component', () => { let fixture: ComponentFixture; @@ -31,14 +31,9 @@ describe('Outcome Feedback Editor Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [], - declarations: [ - OutcomeFeedbackEditorComponent - ], - providers: [ - ChangeDetectorRef, - ContextService - ], - schemas: [NO_ERRORS_SCHEMA] + declarations: [OutcomeFeedbackEditorComponent], + providers: [ChangeDetectorRef, ContextService], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -57,16 +52,18 @@ describe('Outcome Feedback Editor Component', () => { expect(component.OUTCOME_FEEDBACK_SCHEMA).toEqual({ type: 'html', ui_config: { - hide_complex_extensions: false - } + hide_complex_extensions: false, + }, }); }); it('should update html', () => { - const changeDetectorRef = fixture.debugElement.injector.get( - ChangeDetectorRef); + const changeDetectorRef = + fixture.debugElement.injector.get(ChangeDetectorRef); const detectChangesSpy = spyOn( - changeDetectorRef.constructor.prototype, 'detectChanges'); + changeDetectorRef.constructor.prototype, + 'detectChanges' + ); component.outcome = new Outcome( 'default', null, @@ -74,11 +71,12 @@ describe('Outcome Feedback Editor Component', () => { false, [], null, - null, + null ); expect(component.outcome.feedback.html).toBe( - '

Previous HTML string

'); + '

Previous HTML string

' + ); component.updateHtml('

New HTML string

'); @@ -86,26 +84,31 @@ describe('Outcome Feedback Editor Component', () => { expect(detectChangesSpy).toHaveBeenCalled(); }); - it('should not update html if the new and old html' + - ' strings are the same', () => { - const changeDetectorRef = fixture.debugElement.injector.get( - ChangeDetectorRef); - const detectChangesSpy = spyOn( - changeDetectorRef.constructor.prototype, 'detectChanges'); - component.outcome = new Outcome( - 'default', - null, - new SubtitledHtml('

Previous HTML string

', 'Id'), - false, - [], - null, - null, - ); + it( + 'should not update html if the new and old html' + ' strings are the same', + () => { + const changeDetectorRef = + fixture.debugElement.injector.get(ChangeDetectorRef); + const detectChangesSpy = spyOn( + changeDetectorRef.constructor.prototype, + 'detectChanges' + ); + component.outcome = new Outcome( + 'default', + null, + new SubtitledHtml('

Previous HTML string

', 'Id'), + false, + [], + null, + null + ); - expect(component.outcome.feedback.html).toBe( - '

Previous HTML string

'); + expect(component.outcome.feedback.html).toBe( + '

Previous HTML string

' + ); - component.updateHtml('

Previous HTML string

'); - expect(detectChangesSpy).not.toHaveBeenCalled(); - }); + component.updateHtml('

Previous HTML string

'); + expect(detectChangesSpy).not.toHaveBeenCalled(); + } + ); }); diff --git a/core/templates/components/state-directives/outcome-editor/outcome-feedback-editor.component.ts b/core/templates/components/state-directives/outcome-editor/outcome-feedback-editor.component.ts index 2df919cd6a74..fa93f3857c7b 100644 --- a/core/templates/components/state-directives/outcome-editor/outcome-feedback-editor.component.ts +++ b/core/templates/components/state-directives/outcome-editor/outcome-feedback-editor.component.ts @@ -16,10 +16,17 @@ * @fileoverview Component for the outcome feedback editor. */ -import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { Outcome } from 'domain/exploration/OutcomeObjectFactory'; -import { ContextService } from 'services/context.service'; +import { + ChangeDetectorRef, + Component, + EventEmitter, + Input, + OnInit, + Output, +} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {Outcome} from 'domain/exploration/OutcomeObjectFactory'; +import {ContextService} from 'services/context.service'; @Component({ selector: 'oppia-outcome-feedback-editor', @@ -34,15 +41,16 @@ export class OutcomeFeedbackEditorComponent implements OnInit { OUTCOME_FEEDBACK_SCHEMA!: object; constructor( private readonly changeDetectorRef: ChangeDetectorRef, - private contextService: ContextService) {} + private contextService: ContextService + ) {} ngOnInit(): void { this.OUTCOME_FEEDBACK_SCHEMA = { type: 'html', ui_config: { - hide_complex_extensions: ( - this.contextService.getEntityType() === 'question') - } + hide_complex_extensions: + this.contextService.getEntityType() === 'question', + }, }; } @@ -54,6 +62,9 @@ export class OutcomeFeedbackEditorComponent implements OnInit { } } } -angular.module('oppia').directive( - 'oppiaOutcomeFeedbackEditor', downgradeComponent( - {component: OutcomeFeedbackEditorComponent})); +angular + .module('oppia') + .directive( + 'oppiaOutcomeFeedbackEditor', + downgradeComponent({component: OutcomeFeedbackEditorComponent}) + ); diff --git a/core/templates/components/state-directives/outcome-editor/outcome-if-stuck-destination-editor.component.spec.ts b/core/templates/components/state-directives/outcome-editor/outcome-if-stuck-destination-editor.component.spec.ts index 913dc6b57edd..97614649ac0e 100644 --- a/core/templates/components/state-directives/outcome-editor/outcome-if-stuck-destination-editor.component.spec.ts +++ b/core/templates/components/state-directives/outcome-editor/outcome-if-stuck-destination-editor.component.spec.ts @@ -16,19 +16,28 @@ * @fileoverview Unit tests for outcome if stuck destination editor component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { FormsModule } from '@angular/forms'; -import { NodeDataDict, StateGraphLayoutService } from 'components/graph-services/graph-layout.service'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { Outcome } from 'domain/exploration/OutcomeObjectFactory'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { EditorFirstTimeEventsService } from 'pages/exploration-editor-page/services/editor-first-time-events.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { UserService } from 'services/user.service'; -import { OutcomeIfStuckDestinationEditorComponent } from './outcome-if-stuck-destination-editor.component'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {FormsModule} from '@angular/forms'; +import { + NodeDataDict, + StateGraphLayoutService, +} from 'components/graph-services/graph-layout.service'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {Outcome} from 'domain/exploration/OutcomeObjectFactory'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {EditorFirstTimeEventsService} from 'pages/exploration-editor-page/services/editor-first-time-events.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {UserService} from 'services/user.service'; +import {OutcomeIfStuckDestinationEditorComponent} from './outcome-if-stuck-destination-editor.component'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; describe('Outcome Destination If Stuck Editor', () => { let component: OutcomeIfStuckDestinationEditorComponent; @@ -43,10 +52,7 @@ describe('Outcome Destination If Stuck Editor', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - FormsModule - ], + imports: [HttpClientTestingModule, FormsModule], declarations: [ OutcomeIfStuckDestinationEditorComponent, MockTranslatePipe, @@ -56,9 +62,9 @@ describe('Outcome Destination If Stuck Editor', () => { FocusManagerService, StateEditorService, StateGraphLayoutService, - UserService + UserService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -73,21 +79,25 @@ describe('Outcome Destination If Stuck Editor', () => { { Introduction: 'Introduction', State1: 'State1', - End: 'End' - }, [ + End: 'End', + }, + [ { source: 'Introduction', target: 'State1', linkProperty: '', - connectsDestIfStuck: false + connectsDestIfStuck: false, }, { source: 'State1', target: 'End', linkProperty: '', - connectsDestIfStuck: false - } - ], 'Introduction', ['End']); + connectsDestIfStuck: false, + }, + ], + 'Introduction', + ['End'] + ); }); afterEach(() => { @@ -96,136 +106,176 @@ describe('Outcome Destination If Stuck Editor', () => { it('should set component properties on initialization', fakeAsync(() => { let computedLayout = DEFAULT_LAYOUT; - spyOn(stateEditorService, 'getStateNames') - .and.returnValue(['Introduction', 'State1', 'NewState', 'End']); - spyOn(stateGraphLayoutService, 'getLastComputedArrangement') - .and.returnValue(computedLayout); + spyOn(stateEditorService, 'getStateNames').and.returnValue([ + 'Introduction', + 'State1', + 'NewState', + 'End', + ]); + spyOn( + stateGraphLayoutService, + 'getLastComputedArrangement' + ).and.returnValue(computedLayout); component.ngOnInit(); tick(10); expect(component.newStateNamePattern).toEqual(/^[a-zA-Z0-9.\s-]+$/); - expect(component.destinationChoices).toEqual([{ - id: null, - text: 'None' - }, { - id: 'Introduction', - text: 'Introduction' - }, { - id: 'State1', - text: 'State1' - }, { - id: 'End', - text: 'End' - }, { - id: 'NewState', - text: 'NewState' - }, { - id: '/', - text: 'A New Card Called...' - }]); + expect(component.destinationChoices).toEqual([ + { + id: null, + text: 'None', + }, + { + id: 'Introduction', + text: 'Introduction', + }, + { + id: 'State1', + text: 'State1', + }, + { + id: 'End', + text: 'End', + }, + { + id: 'NewState', + text: 'NewState', + }, + { + id: '/', + text: 'A New Card Called...', + }, + ]); })); - it('should add new state if outcome destination if stuck is a placeholder' + - ' when outcome destination if stuck details are saved', fakeAsync(() => { - component.outcome = new Outcome( - 'Dest', - PLACEHOLDER_OUTCOME_DEST_IF_STUCK, - new SubtitledHtml('

HTML string

', 'Id'), - false, - [], - null, - null, - ); - component.outcomeNewStateName = 'End'; - let onSaveOutcomeDestIfStuckDetailsEmitter = new EventEmitter(); - spyOnProperty(stateEditorService, 'onSaveOutcomeDestIfStuckDetails') - .and.returnValue(onSaveOutcomeDestIfStuckDetailsEmitter); - spyOn(stateEditorService, 'getActiveStateName').and.returnValue('Hola'); - spyOn(editorFirstTimeEventsService, 'registerFirstCreateSecondStateEvent'); - - component.ngOnInit(); - tick(10); - - onSaveOutcomeDestIfStuckDetailsEmitter.emit(); - - expect(component.outcome.destIfReallyStuck).toBe('End'); - expect(editorFirstTimeEventsService.registerFirstCreateSecondStateEvent) - .toHaveBeenCalled(); - })); + it( + 'should add new state if outcome destination if stuck is a placeholder' + + ' when outcome destination if stuck details are saved', + fakeAsync(() => { + component.outcome = new Outcome( + 'Dest', + PLACEHOLDER_OUTCOME_DEST_IF_STUCK, + new SubtitledHtml('

HTML string

', 'Id'), + false, + [], + null, + null + ); + component.outcomeNewStateName = 'End'; + let onSaveOutcomeDestIfStuckDetailsEmitter = new EventEmitter(); + spyOnProperty( + stateEditorService, + 'onSaveOutcomeDestIfStuckDetails' + ).and.returnValue(onSaveOutcomeDestIfStuckDetailsEmitter); + spyOn(stateEditorService, 'getActiveStateName').and.returnValue('Hola'); + spyOn( + editorFirstTimeEventsService, + 'registerFirstCreateSecondStateEvent' + ); + + component.ngOnInit(); + tick(10); + + onSaveOutcomeDestIfStuckDetailsEmitter.emit(); + + expect(component.outcome.destIfReallyStuck).toBe('End'); + expect( + editorFirstTimeEventsService.registerFirstCreateSecondStateEvent + ).toHaveBeenCalled(); + }) + ); it('should update option names when state name is changed', fakeAsync(() => { let onStateNamesChangedEmitter = new EventEmitter(); let computedLayout = DEFAULT_LAYOUT; - spyOnProperty(stateEditorService, 'onStateNamesChanged') - .and.returnValue(onStateNamesChangedEmitter); - spyOn(stateEditorService, 'getStateNames') - .and.returnValues( - ['Introduction', 'State1', 'End'], - ['Introduction', 'State2', 'End']); - spyOn(stateGraphLayoutService, 'getLastComputedArrangement') - .and.returnValue(computedLayout); + spyOnProperty(stateEditorService, 'onStateNamesChanged').and.returnValue( + onStateNamesChangedEmitter + ); + spyOn(stateEditorService, 'getStateNames').and.returnValues( + ['Introduction', 'State1', 'End'], + ['Introduction', 'State2', 'End'] + ); + spyOn( + stateGraphLayoutService, + 'getLastComputedArrangement' + ).and.returnValue(computedLayout); component.ngOnInit(); tick(10); - expect(component.destinationChoices).toEqual([{ - id: null, - text: 'None' - }, { - id: 'Introduction', - text: 'Introduction' - }, { - id: 'State1', - text: 'State1' - }, { - id: 'End', - text: 'End' - }, { - id: '/', - text: 'A New Card Called...' - }]); + expect(component.destinationChoices).toEqual([ + { + id: null, + text: 'None', + }, + { + id: 'Introduction', + text: 'Introduction', + }, + { + id: 'State1', + text: 'State1', + }, + { + id: 'End', + text: 'End', + }, + { + id: '/', + text: 'A New Card Called...', + }, + ]); onStateNamesChangedEmitter.emit(); tick(10); - expect(component.destinationChoices).toEqual([{ - id: null, - text: 'None' - }, { - id: 'Introduction', - text: 'Introduction' - }, { - id: 'End', - text: 'End' - }, { - id: 'State2', - text: 'State2' - }, { - id: '/', - text: 'A New Card Called...' - }]); + expect(component.destinationChoices).toEqual([ + { + id: null, + text: 'None', + }, + { + id: 'Introduction', + text: 'Introduction', + }, + { + id: 'End', + text: 'End', + }, + { + id: 'State2', + text: 'State2', + }, + { + id: '/', + text: 'A New Card Called...', + }, + ]); })); - it('should set focus to new state name input field on destination' + - ' selector change', () => { - component.outcome = new Outcome( - 'Dest', - PLACEHOLDER_OUTCOME_DEST_IF_STUCK, - new SubtitledHtml('

HTML string

', 'Id'), - false, - [], - null, - null, - ); - spyOn(focusManagerService, 'setFocus'); - - component.onDestIfStuckSelectorChange(); - - expect(focusManagerService.setFocus).toHaveBeenCalledWith( - 'newStateNameInputField' - ); - }); + it( + 'should set focus to new state name input field on destination' + + ' selector change', + () => { + component.outcome = new Outcome( + 'Dest', + PLACEHOLDER_OUTCOME_DEST_IF_STUCK, + new SubtitledHtml('

HTML string

', 'Id'), + false, + [], + null, + null + ); + spyOn(focusManagerService, 'setFocus'); + + component.onDestIfStuckSelectorChange(); + + expect(focusManagerService.setFocus).toHaveBeenCalledWith( + 'newStateNameInputField' + ); + } + ); it('should check if new state is being created', () => { component.outcome = new Outcome( @@ -235,7 +285,7 @@ describe('Outcome Destination If Stuck Editor', () => { false, [], null, - null, + null ); expect(component.isCreatingNewState()).toBeTrue(); @@ -247,7 +297,7 @@ describe('Outcome Destination If Stuck Editor', () => { false, [], null, - null, + null ); expect(component.isCreatingNewState()).toBeFalse(); @@ -272,7 +322,7 @@ describe('Outcome Destination If Stuck Editor', () => { false, [], null, - null, + null ); spyOn(component.getChanges, 'emit'); @@ -283,28 +333,39 @@ describe('Outcome Destination If Stuck Editor', () => { it('should not show active state', fakeAsync(() => { let computedLayout = DEFAULT_LAYOUT; - spyOn(stateGraphLayoutService, 'getLastComputedArrangement') - .and.returnValue(computedLayout); - spyOn(stateEditorService, 'getActiveStateName') - .and.returnValue('Introduction'); - spyOn(stateEditorService, 'getStateNames') - .and.returnValues(['Introduction', 'State1', 'End']); + spyOn( + stateGraphLayoutService, + 'getLastComputedArrangement' + ).and.returnValue(computedLayout); + spyOn(stateEditorService, 'getActiveStateName').and.returnValue( + 'Introduction' + ); + spyOn(stateEditorService, 'getStateNames').and.returnValues([ + 'Introduction', + 'State1', + 'End', + ]); component.ngOnInit(); tick(10); - expect(component.destinationChoices).toEqual([{ - id: null, - text: 'None' - }, { - id: 'State1', - text: 'State1' - }, { - id: 'End', - text: 'End' - }, { - id: '/', - text: 'A New Card Called...' - }]); + expect(component.destinationChoices).toEqual([ + { + id: null, + text: 'None', + }, + { + id: 'State1', + text: 'State1', + }, + { + id: 'End', + text: 'End', + }, + { + id: '/', + text: 'A New Card Called...', + }, + ]); })); }); diff --git a/core/templates/components/state-directives/outcome-editor/outcome-if-stuck-destination-editor.component.ts b/core/templates/components/state-directives/outcome-editor/outcome-if-stuck-destination-editor.component.ts index 97a07ef1f85c..22b5517fdb0b 100644 --- a/core/templates/components/state-directives/outcome-editor/outcome-if-stuck-destination-editor.component.ts +++ b/core/templates/components/state-directives/outcome-editor/outcome-if-stuck-destination-editor.component.ts @@ -16,16 +16,16 @@ * @fileoverview Component for the outcome if stuck destination editor. */ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { Subscription } from 'rxjs'; +import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {Subscription} from 'rxjs'; import cloneDeep from 'lodash/cloneDeep'; -import { StateGraphLayoutService } from 'components/graph-services/graph-layout.service'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { EditorFirstTimeEventsService } from 'pages/exploration-editor-page/services/editor-first-time-events.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { AppConstants } from 'app.constants'; -import { Outcome } from 'domain/exploration/OutcomeObjectFactory'; +import {StateGraphLayoutService} from 'components/graph-services/graph-layout.service'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {EditorFirstTimeEventsService} from 'pages/exploration-editor-page/services/editor-first-time-events.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {AppConstants} from 'app.constants'; +import {Outcome} from 'domain/exploration/OutcomeObjectFactory'; interface DestinationChoice { id: string | null; @@ -39,7 +39,7 @@ interface DestValidation { @Component({ selector: 'oppia-outcome-if-stuck-destination-editor', - templateUrl: './outcome-if-stuck-destination-editor.component.html' + templateUrl: './outcome-if-stuck-destination-editor.component.html', }) export class OutcomeIfStuckDestinationEditorComponent implements OnInit { @Output() addState: EventEmitter = new EventEmitter(); @@ -56,11 +56,10 @@ export class OutcomeIfStuckDestinationEditorComponent implements OnInit { currentStateName: string | null = null; directiveSubscriptions: Subscription = new Subscription(); - MAX_STATE_NAME_LENGTH: number = ( - AppConstants.MAX_STATE_NAME_LENGTH); + MAX_STATE_NAME_LENGTH: number = AppConstants.MAX_STATE_NAME_LENGTH; - PLACEHOLDER_OUTCOME_DEST_IF_STUCK: string = ( - AppConstants.PLACEHOLDER_OUTCOME_DEST_IF_STUCK); + PLACEHOLDER_OUTCOME_DEST_IF_STUCK: string = + AppConstants.PLACEHOLDER_OUTCOME_DEST_IF_STUCK; constructor( private editorFirstTimeEventsService: EditorFirstTimeEventsService, @@ -74,21 +73,22 @@ export class OutcomeIfStuckDestinationEditorComponent implements OnInit { this.outcomeNewStateName = $event; let validation = { isCreatingNewState: this.isCreatingNewState(), - value: $event + value: $event, }; this.getChanges.emit(validation); } } onDestIfStuckSelectorChange(): void { - if (this.outcome.destIfReallyStuck === - this.PLACEHOLDER_OUTCOME_DEST_IF_STUCK) { + if ( + this.outcome.destIfReallyStuck === this.PLACEHOLDER_OUTCOME_DEST_IF_STUCK + ) { this.focusManagerService.setFocus('newStateNameInputField'); } let validation = { isCreatingNewState: this.isCreatingNewState(), - value: this.outcomeNewStateName + value: this.outcomeNewStateName, }; this.getChanges.emit(validation); } @@ -96,8 +96,8 @@ export class OutcomeIfStuckDestinationEditorComponent implements OnInit { isCreatingNewState(): boolean { this.maxLen = this.MAX_STATE_NAME_LENGTH; return ( - this.outcome.destIfReallyStuck === - this.PLACEHOLDER_OUTCOME_DEST_IF_STUCK); + this.outcome.destIfReallyStuck === this.PLACEHOLDER_OUTCOME_DEST_IF_STUCK + ); } updateOptionNames(): void { @@ -108,15 +108,17 @@ export class OutcomeIfStuckDestinationEditorComponent implements OnInit { // This is a list of objects, each with an ID and name. These // represent all states, as well as an option to create a // new state. - this.destinationChoices = [{ - id: null, - text: 'None' - }]; + this.destinationChoices = [ + { + id: null, + text: 'None', + }, + ]; // Arrange the remaining states based on their order in the state // graph. - let lastComputedArrangement = ( - this.stateGraphLayoutService.getLastComputedArrangement()); + let lastComputedArrangement = + this.stateGraphLayoutService.getLastComputedArrangement(); let allStateNames = this.stateEditorService.getStateNames(); // It is possible that lastComputedArrangement is null if the @@ -129,9 +131,13 @@ export class OutcomeIfStuckDestinationEditorComponent implements OnInit { let maxOffset = 0; for (let stateName in lastComputedArrangement) { maxDepth = Math.max( - maxDepth, lastComputedArrangement[stateName].depth); + maxDepth, + lastComputedArrangement[stateName].depth + ); maxOffset = Math.max( - maxOffset, lastComputedArrangement[stateName].offset); + maxOffset, + lastComputedArrangement[stateName].offset + ); } // Higher scores come later. @@ -140,16 +146,15 @@ export class OutcomeIfStuckDestinationEditorComponent implements OnInit { for (let i = 0; i < allStateNames.length; i++) { stateName = allStateNames[i]; if (lastComputedArrangement.hasOwnProperty(stateName)) { - allStateScores[stateName] = ( - lastComputedArrangement[stateName].depth * - (maxOffset + 1) + - lastComputedArrangement[stateName].offset); + allStateScores[stateName] = + lastComputedArrangement[stateName].depth * (maxOffset + 1) + + lastComputedArrangement[stateName].offset; } else { // States that have just been added in the rule 'create new' // modal are not yet included as part of // lastComputedArrangement so we account for them here. - allStateScores[stateName] = ( - (maxDepth + 1) * (maxOffset + 1) + unarrangedStateCount); + allStateScores[stateName] = + (maxDepth + 1) * (maxOffset + 1) + unarrangedStateCount; unarrangedStateCount++; } } @@ -163,7 +168,7 @@ export class OutcomeIfStuckDestinationEditorComponent implements OnInit { if (stateNames[i] !== this.currentStateName) { this.destinationChoices.push({ id: stateNames[i], - text: stateNames[i] + text: stateNames[i], }); } } @@ -171,33 +176,35 @@ export class OutcomeIfStuckDestinationEditorComponent implements OnInit { if (!questionModeEnabled) { this.destinationChoices.push({ id: this.PLACEHOLDER_OUTCOME_DEST_IF_STUCK, - text: 'A New Card Called...' + text: 'A New Card Called...', }); } - // This value of 10ms is arbitrary, it has no significance. + // This value of 10ms is arbitrary, it has no significance. }, 10); } ngOnInit(): void { this.directiveSubscriptions.add( - this.stateEditorService.onSaveOutcomeDestIfStuckDetails. - subscribe(() => { - // Create new state if specified. - if (this.outcome.destIfReallyStuck === - this.PLACEHOLDER_OUTCOME_DEST_IF_STUCK) { - this.editorFirstTimeEventsService - .registerFirstCreateSecondStateEvent(); - - let newStateName = this.outcomeNewStateName; - this.outcome.destIfReallyStuck = newStateName; - this.addState.emit(newStateName); - } - })); + this.stateEditorService.onSaveOutcomeDestIfStuckDetails.subscribe(() => { + // Create new state if specified. + if ( + this.outcome.destIfReallyStuck === + this.PLACEHOLDER_OUTCOME_DEST_IF_STUCK + ) { + this.editorFirstTimeEventsService.registerFirstCreateSecondStateEvent(); + + let newStateName = this.outcomeNewStateName; + this.outcome.destIfReallyStuck = newStateName; + this.addState.emit(newStateName); + } + }) + ); this.updateOptionNames(); this.directiveSubscriptions.add( this.stateEditorService.onStateNamesChanged.subscribe(() => { this.updateOptionNames(); - })); + }) + ); this.newStateNamePattern = /^[a-zA-Z0-9.\s-]+$/; this.destinationChoices = []; @@ -208,7 +215,9 @@ export class OutcomeIfStuckDestinationEditorComponent implements OnInit { } } -angular.module('oppia').directive('oppiaOutcomeIfStuckDestinationEditor', +angular.module('oppia').directive( + 'oppiaOutcomeIfStuckDestinationEditor', downgradeComponent({ - component: OutcomeIfStuckDestinationEditorComponent - }) as angular.IDirectiveFactory); + component: OutcomeIfStuckDestinationEditorComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/components/state-directives/response-header/response-header.component.spec.ts b/core/templates/components/state-directives/response-header/response-header.component.spec.ts index b10afa6245f9..9c31d1219d7a 100644 --- a/core/templates/components/state-directives/response-header/response-header.component.spec.ts +++ b/core/templates/components/state-directives/response-header/response-header.component.spec.ts @@ -16,18 +16,18 @@ * @fileoverview Unit tests for Response Header Component. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ResponseHeaderComponent } from './response-header.component'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { StateInteractionIdService } from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ResponseHeaderComponent} from './response-header.component'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {StateInteractionIdService} from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; import INTERACTION_SPECS from 'interactions/interaction_specs.json'; -import { OutcomeObjectFactory } from 'domain/exploration/OutcomeObjectFactory'; +import {OutcomeObjectFactory} from 'domain/exploration/OutcomeObjectFactory'; const mockInteractionState = { TextInput: { - is_linear: false - } + is_linear: false, + }, }; class MockStateInteractionIdService { savedMemento = 'TextInput'; @@ -41,22 +41,20 @@ describe('Response Header Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - ResponseHeaderComponent - ], + declarations: [ResponseHeaderComponent], providers: [ StateEditorService, OutcomeObjectFactory, { provide: INTERACTION_SPECS, - useValue: mockInteractionState + useValue: mockInteractionState, }, { provide: StateInteractionIdService, - useClass: MockStateInteractionIdService - } + useClass: MockStateInteractionIdService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); diff --git a/core/templates/components/state-directives/response-header/response-header.component.ts b/core/templates/components/state-directives/response-header/response-header.component.ts index 0fc02e7a93e4..7c952a70c3d6 100644 --- a/core/templates/components/state-directives/response-header/response-header.component.ts +++ b/core/templates/components/state-directives/response-header/response-header.component.ts @@ -16,22 +16,22 @@ * @fileoverview Component for the header of the response tiles. */ -import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { StateInteractionIdService } from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; -import { EditabilityService } from 'services/editability.service'; +import {Component, EventEmitter, Input, Output} from '@angular/core'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {StateInteractionIdService} from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; +import {EditabilityService} from 'services/editability.service'; import INTERACTION_SPECS from 'interactions/interaction_specs.json'; -import { AppConstants } from 'app.constants'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { Outcome } from 'domain/exploration/OutcomeObjectFactory'; -import { InteractionSpecsKey } from 'pages/interaction-specs.constants'; +import {AppConstants} from 'app.constants'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {Outcome} from 'domain/exploration/OutcomeObjectFactory'; +import {InteractionSpecsKey} from 'pages/interaction-specs.constants'; interface DeleteValue { index: number; evt: Event; } @Component({ selector: 'oppia-response-header', - templateUrl: './response-header.component.html' + templateUrl: './response-header.component.html', }) export class ResponseHeaderComponent { @Output() delete = new EventEmitter(); @@ -52,7 +52,7 @@ export class ResponseHeaderComponent { constructor( private stateEditorService: StateEditorService, private stateInteractionIdService: StateInteractionIdService, - public editabilityService: EditabilityService, + public editabilityService: EditabilityService ) {} returnToState(): void { @@ -69,8 +69,10 @@ export class ResponseHeaderComponent { isCurrentInteractionLinear(): boolean { let interactionId = this.getCurrentInteractionId(); - return Boolean(interactionId) && INTERACTION_SPECS[ - interactionId as InteractionSpecsKey].is_linear; + return ( + Boolean(interactionId) && + INTERACTION_SPECS[interactionId as InteractionSpecsKey].is_linear + ); } isCorrect(): boolean { @@ -80,7 +82,7 @@ export class ResponseHeaderComponent { isOutcomeLooping(): boolean { const outcome = this.outcome; const activeStateName = this.stateEditorService.getActiveStateName(); - return outcome && (outcome.dest === activeStateName); + return outcome && outcome.dest === activeStateName; } isCreatingNewState(): boolean { @@ -91,14 +93,16 @@ export class ResponseHeaderComponent { deleteResponse(evt: Event): void { const value: DeleteValue = { index: this.index, - evt: evt + evt: evt, }; this.delete.emit(value); } } -angular.module('oppia').directive('oppiaResponseHeader', +angular.module('oppia').directive( + 'oppiaResponseHeader', downgradeComponent({ - component: ResponseHeaderComponent - }) as angular.IDirectiveFactory); + component: ResponseHeaderComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/components/state-directives/rule-editor/rule-editor.component.spec.ts b/core/templates/components/state-directives/rule-editor/rule-editor.component.spec.ts index c0614d05513d..cf6985519244 100644 --- a/core/templates/components/state-directives/rule-editor/rule-editor.component.spec.ts +++ b/core/templates/components/state-directives/rule-editor/rule-editor.component.spec.ts @@ -16,25 +16,35 @@ * @fileoverview Unit tests for rule editor. */ -import { ComponentFixture, fakeAsync, flush, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { RuleDescriptionFragment, RuleEditorComponent } from './rule-editor.component'; -import { ObjectFormValidityChangeEvent } from 'app-events/app-events'; -import { EventBusGroup, EventBusService } from 'app-events/event-bus.service'; -import { StateInteractionIdService } from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; -import { ResponsesService } from 'pages/exploration-editor-page/editor-tab/services/responses.service'; -import { PopulateRuleContentIdsService } from 'pages/exploration-editor-page/services/populate-rule-content-ids.service'; -import { ChangeDetectorRef, NO_ERRORS_SCHEMA, Pipe } from '@angular/core'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { Rule } from 'domain/exploration/rule.model'; - -@Pipe({ name: 'truncate' }) +import { + ComponentFixture, + fakeAsync, + flush, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import { + RuleDescriptionFragment, + RuleEditorComponent, +} from './rule-editor.component'; +import {ObjectFormValidityChangeEvent} from 'app-events/app-events'; +import {EventBusGroup, EventBusService} from 'app-events/event-bus.service'; +import {StateInteractionIdService} from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; +import {ResponsesService} from 'pages/exploration-editor-page/editor-tab/services/responses.service'; +import {PopulateRuleContentIdsService} from 'pages/exploration-editor-page/services/populate-rule-content-ids.service'; +import {ChangeDetectorRef, NO_ERRORS_SCHEMA, Pipe} from '@angular/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {Rule} from 'domain/exploration/rule.model'; + +@Pipe({name: 'truncate'}) class MockTruncatePipe { transform(value: string, params: number): string { return value; } } -@Pipe({ name: 'convertToPlainText' }) +@Pipe({name: 'convertToPlainText'}) class MockConvertToPlainTextPipe { transform(value: string): string { return value; @@ -67,7 +77,7 @@ describe('Rule Editor Component', () => { ResponsesService, { provide: ChangeDetectorRef, - useClass: MockChangeDetectorRef + useClass: MockChangeDetectorRef, }, PopulateRuleContentIdsService, ], @@ -76,90 +86,91 @@ describe('Rule Editor Component', () => { })); beforeEach(() => { - fixture = TestBed.createComponent( - RuleEditorComponent); + fixture = TestBed.createComponent(RuleEditorComponent); component = fixture.componentInstance; eventBusService = TestBed.inject(EventBusService); stateInteractionIdService = TestBed.inject(StateInteractionIdService); responsesService = TestBed.inject(ResponsesService); populateRuleContentIdsService = TestBed.inject( - PopulateRuleContentIdsService); + PopulateRuleContentIdsService + ); }); afterEach(() => { component.ngOnDestroy(); }); - it('should intitialize properties of ListOfSetsOfTranslatableHtmlContentIds', - fakeAsync(() => { - spyOn(component, 'computeRuleDescriptionFragments').and.stub(); - component.rule = new Rule( - 'Equals', { - x: [], - }, { - x: 'ListOfSetsOfTranslatableHtmlContentIds' - }); + it('should intitialize properties of ListOfSetsOfTranslatableHtmlContentIds', fakeAsync(() => { + spyOn(component, 'computeRuleDescriptionFragments').and.stub(); + component.rule = new Rule( + 'Equals', + { + x: [], + }, + { + x: 'ListOfSetsOfTranslatableHtmlContentIds', + } + ); - component.ruleDescriptionChoices = [ - { - id: '1', - val: 'data 1', - }, - { - id: '2', - val: 'data 2', - }, - { - id: '3', - val: 'data 3', - } - ]; - stateInteractionIdService.savedMemento = 'DragAndDropSortInput'; + component.ruleDescriptionChoices = [ + { + id: '1', + val: 'data 1', + }, + { + id: '2', + val: 'data 2', + }, + { + id: '3', + val: 'data 3', + }, + ]; + stateInteractionIdService.savedMemento = 'DragAndDropSortInput'; - tick(); - component.ngOnInit(); + tick(); + component.ngOnInit(); - expect(component.currentInteractionId).toBe('DragAndDropSortInput'); - expect(component.editRuleForm).toEqual({}); - expect(component.rule.inputs.x).toEqual( - [ - ['data 1'], - ['data 2'], - ['data 3'] - ] - ); - })); + expect(component.currentInteractionId).toBe('DragAndDropSortInput'); + expect(component.editRuleForm).toEqual({}); + expect(component.rule.inputs.x).toEqual([ + ['data 1'], + ['data 2'], + ['data 3'], + ]); + })); - it('should intitialize properties of TranslatableHtmlContentId', - fakeAsync(() => { - component.rule = new Rule( - 'HasElementXAtPositionY', { - x: null, - y: 1 - }, { - x: 'TranslatableHtmlContentId', - y: 'DragAndDropPositiveInt' - }); - - component.ruleDescriptionChoices = [ - { - id: '1', - val: 'data 1', - } - ]; - stateInteractionIdService.savedMemento = 'DragAndDropSortInput'; + it('should intitialize properties of TranslatableHtmlContentId', fakeAsync(() => { + component.rule = new Rule( + 'HasElementXAtPositionY', + { + x: null, + y: 1, + }, + { + x: 'TranslatableHtmlContentId', + y: 'DragAndDropPositiveInt', + } + ); - tick(); - component.ngOnInit(); + component.ruleDescriptionChoices = [ + { + id: '1', + val: 'data 1', + }, + ]; + stateInteractionIdService.savedMemento = 'DragAndDropSortInput'; - expect(component.currentInteractionId).toBe('DragAndDropSortInput'); - expect(component.rule.inputs.x).toEqual('data 1'); - flush(); - })); + tick(); + component.ngOnInit(); + + expect(component.currentInteractionId).toBe('DragAndDropSortInput'); + expect(component.rule.inputs.x).toEqual('data 1'); + flush(); + })); it('should set component properties on initialization', () => { - component.rule = new Rule( - null, null, null); + component.rule = new Rule(null, null, null); stateInteractionIdService.savedMemento = 'TextInput'; @@ -172,33 +183,38 @@ describe('Rule Editor Component', () => { expect(component.editRuleForm).toEqual({}); }); - it('should set change validity on form valid' + - ' change event', fakeAsync(() => { - const eventBusGroup = new EventBusGroup(eventBusService); - component.rule = new Rule( - null, null, null); + it( + 'should set change validity on form valid' + ' change event', + fakeAsync(() => { + const eventBusGroup = new EventBusGroup(eventBusService); + component.rule = new Rule(null, null, null); - expect(component.isInvalid).toBe(undefined); + expect(component.isInvalid).toBe(undefined); - component.isEditingRuleInline = true; - component.ngOnInit(); + component.isEditingRuleInline = true; + component.ngOnInit(); - expect(component.isInvalid).toBe(false); + expect(component.isInvalid).toBe(false); - component.modalId = Symbol(); - eventBusGroup.emit(new ObjectFormValidityChangeEvent({ - value: true, modalId: component.modalId - })); - tick(); - component.ngAfterViewChecked(); + component.modalId = Symbol(); + eventBusGroup.emit( + new ObjectFormValidityChangeEvent({ + value: true, + modalId: component.modalId, + }) + ); + tick(); + component.ngAfterViewChecked(); - expect(component.isInvalid).toBe(true); - })); + expect(component.isInvalid).toBe(true); + }) + ); - it('should change rule type when user selects' + - ' new rule type and answer choice is present 1', fakeAsync(() => { - spyOn(responsesService, 'getAnswerChoices').and.returnValue( - [ + it( + 'should change rule type when user selects' + + ' new rule type and answer choice is present 1', + fakeAsync(() => { + spyOn(responsesService, 'getAnswerChoices').and.returnValue([ { val: 'c', label: '', @@ -211,88 +227,97 @@ describe('Rule Editor Component', () => { val: 'a', label: '', }, - ] - ); - component.rule = new Rule( - 'Equals', - { x: 'c' }, - { - contentId: null, - normalizedStrSet: '' - }); - - component.currentInteractionId = 'TextInput'; - - component.onSelectNewRuleType('StartsWith'); - flush(10); - tick(); - - expect(component.rule).toEqual( - new Rule( - 'StartsWith', - { x: 'c' }, + ]); + component.rule = new Rule( + 'Equals', + {x: 'c'}, { - x: 'TranslatableSetOfNormalizedString' - })); - })); + contentId: null, + normalizedStrSet: '', + } + ); - it('should change rule type when user selects' + - ' new rule type and answer choice is not present', fakeAsync(() => { - spyOn(responsesService, 'getAnswerChoices') - .and.returnValue(undefined); - let componentRule = new Rule( - 'Equals', - { x: 'TranslatableSetOfNormalizedString' }, - null); + component.currentInteractionId = 'TextInput'; - component.rule = componentRule; - component.currentInteractionId = 'TextInput'; + component.onSelectNewRuleType('StartsWith'); + flush(10); + tick(); - component.onSelectNewRuleType('StartsWith'); - flush(10); + expect(component.rule).toEqual( + new Rule( + 'StartsWith', + {x: 'c'}, + { + x: 'TranslatableSetOfNormalizedString', + } + ) + ); + }) + ); - expect(component.rule).toEqual(componentRule); - })); + it( + 'should change rule type when user selects' + + ' new rule type and answer choice is not present', + fakeAsync(() => { + spyOn(responsesService, 'getAnswerChoices').and.returnValue(undefined); + let componentRule = new Rule( + 'Equals', + {x: 'TranslatableSetOfNormalizedString'}, + null + ); - it('should change rule type when user selects' + - ' new rule type and answer choice is not present 2', fakeAsync(() => { - spyOn(responsesService, 'getAnswerChoices') - .and.returnValue(undefined); - component.rule = new Rule( - 'MatchesExactlyWith', - { x: 'AlgebraicExpression' }, - { - contentId: null, - normalizedStrSet: '' - }); - component.rule.inputs = { - x: 'AlgebraicExpression' - }; - component.rule.inputTypes = { - x: 'AlgebraicExpression' - }; + component.rule = componentRule; + component.currentInteractionId = 'TextInput'; - component.currentInteractionId = 'AlgebraicExpressionInput'; + component.onSelectNewRuleType('StartsWith'); + flush(10); - component.onSelectNewRuleType('MatchesExactlyWith'); - flush(10); + expect(component.rule).toEqual(componentRule); + }) + ); - expect(component.rule).toEqual(new Rule( - 'MatchesExactlyWith', - { x: 'AlgebraicExpression' }, - { x: 'AlgebraicExpression' })); - })); + it( + 'should change rule type when user selects' + + ' new rule type and answer choice is not present 2', + fakeAsync(() => { + spyOn(responsesService, 'getAnswerChoices').and.returnValue(undefined); + component.rule = new Rule( + 'MatchesExactlyWith', + {x: 'AlgebraicExpression'}, + { + contentId: null, + normalizedStrSet: '', + } + ); + component.rule.inputs = { + x: 'AlgebraicExpression', + }; + component.rule.inputTypes = { + x: 'AlgebraicExpression', + }; + + component.currentInteractionId = 'AlgebraicExpressionInput'; + + component.onSelectNewRuleType('MatchesExactlyWith'); + flush(10); + + expect(component.rule).toEqual( + new Rule( + 'MatchesExactlyWith', + {x: 'AlgebraicExpression'}, + {x: 'AlgebraicExpression'} + ) + ); + }) + ); it('should cancel edit when user clicks cancel button', () => { const item = { type: null, - varName: 'varName' + varName: 'varName', }; - component.rule = new Rule( - null, - {varName: 2}, - null); + component.rule = new Rule(null, {varName: 2}, null); spyOn(component.onCancelRuleEdit, 'emit'); @@ -303,166 +328,173 @@ describe('Rule Editor Component', () => { }); it('should save rule when user clicks save button', () => { - component.rule = new Rule( - null, - null, - null); + component.rule = new Rule(null, null, null); spyOn(component.onSaveRule, 'emit').and.stub(); - spyOn(populateRuleContentIdsService, 'populateNullRuleContentIds') - .and.stub(); + spyOn( + populateRuleContentIdsService, + 'populateNullRuleContentIds' + ).and.stub(); component.saveThisRule(); expect(component.onSaveRule.emit).toHaveBeenCalled(); - expect(populateRuleContentIdsService.populateNullRuleContentIds) - .toHaveBeenCalled(); + expect( + populateRuleContentIdsService.populateNullRuleContentIds + ).toHaveBeenCalled(); }); - it('should set ruleDescriptionFragments for' + - ' SetOfTranslatableHtmlContentIds', fakeAsync(() => { - spyOn(responsesService, 'getAnswerChoices').and.returnValue( - [ + it( + 'should set ruleDescriptionFragments for' + + ' SetOfTranslatableHtmlContentIds', + fakeAsync(() => { + spyOn(responsesService, 'getAnswerChoices').and.returnValue([ { val: 'c', label: '', - } - ] - ); - component.rule = new Rule( - 'Equals', - null, - null); + }, + ]); + component.rule = new Rule('Equals', null, null); - component.currentInteractionId = 'ItemSelectionInput'; + component.currentInteractionId = 'ItemSelectionInput'; - component.onSelectNewRuleType('Equals'); - flush(); + component.onSelectNewRuleType('Equals'); + flush(); - expect(component.ruleDescriptionFragments).toEqual([{ - text: '', - type: 'noneditable' - }, { - type: 'checkboxes', - varName: 'x' - }, { - text: '', - type: 'noneditable' - }] as RuleDescriptionFragment[]); - })); + expect(component.ruleDescriptionFragments).toEqual([ + { + text: '', + type: 'noneditable', + }, + { + type: 'checkboxes', + varName: 'x', + }, + { + text: '', + type: 'noneditable', + }, + ] as RuleDescriptionFragment[]); + }) + ); - it('should set ruleDescriptionFragments for' + - ' ListOfSetsOfTranslatableHtmlContentIds', fakeAsync(() => { - spyOn(responsesService, 'getAnswerChoices').and.returnValue( - [ + it( + 'should set ruleDescriptionFragments for' + + ' ListOfSetsOfTranslatableHtmlContentIds', + fakeAsync(() => { + spyOn(responsesService, 'getAnswerChoices').and.returnValue([ { val: 'c', label: '', - } - ] - ); + }, + ]); - component.rule = new Rule( - 'IsEqualToOrderingWithOneItemAtIncorrectPosition', - null, - null - ); + component.rule = new Rule( + 'IsEqualToOrderingWithOneItemAtIncorrectPosition', + null, + null + ); - component.currentInteractionId = 'DragAndDropSortInput'; + component.currentInteractionId = 'DragAndDropSortInput'; - component.onSelectNewRuleType( - 'IsEqualToOrderingWithOneItemAtIncorrectPosition'); - flush(); + component.onSelectNewRuleType( + 'IsEqualToOrderingWithOneItemAtIncorrectPosition' + ); + flush(); - expect(component.ruleDescriptionFragments).toEqual([{ - text: '', - type: 'noneditable' - }, { - type: 'dropdown', - varName: 'x' - }, { - text: '', - type: 'noneditable' - }] as RuleDescriptionFragment[]); - })); + expect(component.ruleDescriptionFragments).toEqual([ + { + text: '', + type: 'noneditable', + }, + { + type: 'dropdown', + varName: 'x', + }, + { + text: '', + type: 'noneditable', + }, + ] as RuleDescriptionFragment[]); + }) + ); - it('should set ruleDescriptionFragments for' + - ' TranslatableHtmlContentId', fakeAsync(() => { - spyOn(responsesService, 'getAnswerChoices').and.returnValue( - [ + it( + 'should set ruleDescriptionFragments for' + ' TranslatableHtmlContentId', + fakeAsync(() => { + spyOn(responsesService, 'getAnswerChoices').and.returnValue([ { val: 'c', label: '', - } - ] - ); - component.rule = new Rule( - 'IsEqualToOrdering', - null, - null - ); + }, + ]); + component.rule = new Rule('IsEqualToOrdering', null, null); - component.currentInteractionId = 'DragAndDropSortInput'; - component.onSelectNewRuleType('IsEqualToOrdering'); - flush(); + component.currentInteractionId = 'DragAndDropSortInput'; + component.onSelectNewRuleType('IsEqualToOrdering'); + flush(); - expect(component.ruleDescriptionFragments).toEqual([{ - text: '', - type: 'noneditable' - }, { - type: 'dropdown', - varName: 'x' - }, { - text: '', - type: 'noneditable' - }] as RuleDescriptionFragment[]); - })); + expect(component.ruleDescriptionFragments).toEqual([ + { + text: '', + type: 'noneditable', + }, + { + type: 'dropdown', + varName: 'x', + }, + { + text: '', + type: 'noneditable', + }, + ] as RuleDescriptionFragment[]); + }) + ); - it('should set ruleDescriptionFragments for' + - ' DragAndDropPositiveInt', fakeAsync(() => { - spyOn(responsesService, 'getAnswerChoices').and.returnValue( - [ + it( + 'should set ruleDescriptionFragments for' + ' DragAndDropPositiveInt', + fakeAsync(() => { + spyOn(responsesService, 'getAnswerChoices').and.returnValue([ { val: 'c', label: '', - } - ] - ); - component.rule = new Rule( - 'HasElementXAtPositionY', - null, - null - ); - component.currentInteractionId = 'DragAndDropSortInput'; + }, + ]); + component.rule = new Rule('HasElementXAtPositionY', null, null); + component.currentInteractionId = 'DragAndDropSortInput'; - component.onSelectNewRuleType('HasElementXAtPositionY'); - flush(); + component.onSelectNewRuleType('HasElementXAtPositionY'); + flush(); - expect(component.ruleDescriptionFragments.length).toEqual(5); - })); + expect(component.ruleDescriptionFragments.length).toEqual(5); + }) + ); - it('should set ruleDescriptionFragments as noneditable when answer' + - ' choices are empty', fakeAsync(() => { - spyOn(responsesService, 'getAnswerChoices').and.returnValue([]); - component.rule = new Rule( - 'MatchesExactlyWith', - null, - null - ); - component.currentInteractionId = 'AlgebraicExpressionInput'; + it( + 'should set ruleDescriptionFragments as noneditable when answer' + + ' choices are empty', + fakeAsync(() => { + spyOn(responsesService, 'getAnswerChoices').and.returnValue([]); + component.rule = new Rule('MatchesExactlyWith', null, null); + component.currentInteractionId = 'AlgebraicExpressionInput'; - component.onSelectNewRuleType('MatchesExactlyWith'); - flush(); + component.onSelectNewRuleType('MatchesExactlyWith'); + flush(); - expect(component.ruleDescriptionFragments).toEqual([{ - text: '', - type: 'noneditable' - }, { - text: ' [Error: No choices available] ', - type: 'noneditable' - }, { - text: '', - type: 'noneditable' - }] as RuleDescriptionFragment[]); - })); + expect(component.ruleDescriptionFragments).toEqual([ + { + text: '', + type: 'noneditable', + }, + { + text: ' [Error: No choices available] ', + type: 'noneditable', + }, + { + text: '', + type: 'noneditable', + }, + ] as RuleDescriptionFragment[]); + }) + ); }); diff --git a/core/templates/components/state-directives/rule-editor/rule-editor.component.ts b/core/templates/components/state-directives/rule-editor/rule-editor.component.ts index 7aad9e192761..67ca618baeb2 100644 --- a/core/templates/components/state-directives/rule-editor/rule-editor.component.ts +++ b/core/templates/components/state-directives/rule-editor/rule-editor.component.ts @@ -16,19 +16,28 @@ * @fileoverview Component for the rule editor. */ -import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output, AfterViewChecked } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import { + ChangeDetectorRef, + Component, + EventEmitter, + Input, + OnDestroy, + OnInit, + Output, + AfterViewChecked, +} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; import cloneDeep from 'lodash/cloneDeep'; import isEqual from 'lodash/isEqual'; -import { EventBusGroup, EventBusService } from 'app-events/event-bus.service'; -import { StateInteractionIdService } from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; -import { ResponsesService } from 'pages/exploration-editor-page/editor-tab/services/responses.service'; -import { PopulateRuleContentIdsService } from 'pages/exploration-editor-page/services/populate-rule-content-ids.service'; -import { ObjectFormValidityChangeEvent } from 'app-events/app-events'; +import {EventBusGroup, EventBusService} from 'app-events/event-bus.service'; +import {StateInteractionIdService} from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; +import {ResponsesService} from 'pages/exploration-editor-page/editor-tab/services/responses.service'; +import {PopulateRuleContentIdsService} from 'pages/exploration-editor-page/services/populate-rule-content-ids.service'; +import {ObjectFormValidityChangeEvent} from 'app-events/app-events'; import DEFAULT_OBJECT_VALUES from 'objects/object_defaults.json'; import INTERACTION_SPECS from 'interactions/interaction_specs.json'; -import { Rule } from 'domain/exploration/rule.model'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; +import {Rule} from 'domain/exploration/rule.model'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; interface SelectItem { type: string; @@ -48,10 +57,11 @@ export interface RuleDescriptionFragment { @Component({ selector: 'oppia-rule-editor', - templateUrl: './rule-editor.component.html' + templateUrl: './rule-editor.component.html', }) export class RuleEditorComponent - implements OnInit, OnDestroy, AfterViewChecked { + implements OnInit, OnDestroy, AfterViewChecked +{ @Input() isEditable: boolean; @Input() isEditingRuleInline: boolean; @Output() onCancelRuleEdit = new EventEmitter(); @@ -67,11 +77,11 @@ export class RuleEditorComponent editRuleForm: object; constructor( - private eventBusService: EventBusService, - private stateInteractionIdService: StateInteractionIdService, - private responsesService: ResponsesService, - private populateRuleContentIdsService: PopulateRuleContentIdsService, - private readonly changeDetectorRef: ChangeDetectorRef, + private eventBusService: EventBusService, + private stateInteractionIdService: StateInteractionIdService, + private responsesService: ResponsesService, + private populateRuleContentIdsService: PopulateRuleContentIdsService, + private readonly changeDetectorRef: ChangeDetectorRef ) { this.eventBusGroup = new EventBusGroup(this.eventBusService); } @@ -82,10 +92,10 @@ export class RuleEditorComponent return ''; } - let ruleDescription = ( - INTERACTION_SPECS[ - this.currentInteractionId - ].rule_descriptions[this.rule.type]); + let ruleDescription = + INTERACTION_SPECS[this.currentInteractionId].rule_descriptions[ + this.rule.type + ]; let PATTERN = /\{\{\s*(\w+)\s*\|\s*(\w+)\s*\}\}/; let finalInputArray = ruleDescription.split(PATTERN); @@ -95,7 +105,7 @@ export class RuleEditorComponent result.push({ // Omit the leading noneditable string. text: i !== 0 ? finalInputArray[i] : '', - type: 'noneditable' + type: 'noneditable', }); if (i === finalInputArray.length - 1) { break; @@ -108,90 +118,75 @@ export class RuleEditorComponent // selection interaction. // TODO(sll): Remove the need for this special case. if (answerChoices.length > 0) { - if ( - finalInputArray[2] === 'SetOfTranslatableHtmlContentIds' - ) { - this.ruleDescriptionChoices = answerChoices.map( - choice => ({ - id: choice.label, - val: choice.val - }) - ); + if (finalInputArray[2] === 'SetOfTranslatableHtmlContentIds') { + this.ruleDescriptionChoices = answerChoices.map(choice => ({ + id: choice.label, + val: choice.val, + })); result.push({ type: 'checkboxes', - varName: finalInputArray[i + 1] + varName: finalInputArray[i + 1], }); } else if ( - finalInputArray[2] === - 'ListOfSetsOfTranslatableHtmlContentIds' + finalInputArray[2] === 'ListOfSetsOfTranslatableHtmlContentIds' ) { - this.ruleDescriptionChoices = answerChoices.map( - choice => ({ - id: choice.label, - val: choice.val - }) - ); + this.ruleDescriptionChoices = answerChoices.map(choice => ({ + id: choice.label, + val: choice.val, + })); result.push({ type: 'dropdown', - varName: finalInputArray[i + 1] + varName: finalInputArray[i + 1], + }); + } else if (finalInputArray[i + 2] === 'TranslatableHtmlContentId') { + this.ruleDescriptionChoices = answerChoices.map(function (choice) { + return { + id: choice.label, + val: choice.val, + }; }); - } else if ( - finalInputArray[i + 2] === 'TranslatableHtmlContentId') { - this.ruleDescriptionChoices = answerChoices.map( - function(choice) { - return { - id: choice.label, - val: choice.val - }; - } - ); result.push({ type: 'dragAndDropHtmlStringSelect', - varName: finalInputArray[i + 1] + varName: finalInputArray[i + 1], + }); + } else if (finalInputArray[i + 2] === 'DragAndDropPositiveInt') { + this.ruleDescriptionChoices = answerChoices.map(function (choice) { + return { + id: choice.label, + val: choice.val, + }; }); - } else if ( - finalInputArray[i + 2] === 'DragAndDropPositiveInt') { - this.ruleDescriptionChoices = answerChoices.map( - function(choice) { - return { - id: choice.label, - val: choice.val - }; - } - ); result.push({ type: 'dragAndDropPositiveIntSelect', - varName: finalInputArray[i + 1] + varName: finalInputArray[i + 1], }); } else { - this.ruleDescriptionChoices = answerChoices.map( - function(choice) { - return { - id: choice.val as string, - val: choice.label - }; - } - ); + this.ruleDescriptionChoices = answerChoices.map(function (choice) { + return { + id: choice.val as string, + val: choice.label, + }; + }); result.push({ type: 'select', - varName: finalInputArray[i + 1] + varName: finalInputArray[i + 1], }); if (!this.rule.inputs[finalInputArray[i + 1]]) { - this.rule.inputs[finalInputArray[i + 1]] = ( - this.ruleDescriptionChoices[0].id); + this.rule.inputs[finalInputArray[i + 1]] = + this.ruleDescriptionChoices[0].id; } } } else { this.ruleDescriptionChoices = []; result.push({ text: ' [Error: No choices available] ', - type: 'noneditable' + type: 'noneditable', }); } } else { result.push({ type: finalInputArray[i + 2], - varName: finalInputArray[i + 1] + varName: finalInputArray[i + 1], }); } } @@ -249,19 +244,19 @@ export class RuleEditorComponent if (isEqual(DEFAULT_OBJECT_VALUES[varType], [])) { this.rule.inputs[varName] = []; } else if (answerChoices && answerChoices.length > 0) { - this.rule.inputs[varName] = cloneDeep( - answerChoices[0].val); + this.rule.inputs[varName] = cloneDeep(answerChoices[0].val); } else { - this.rule.inputs[varName] = cloneDeep( - DEFAULT_OBJECT_VALUES[varType]); + this.rule.inputs[varName] = cloneDeep(DEFAULT_OBJECT_VALUES[varType]); } tmpRuleDescription = tmpRuleDescription.replace(PATTERN, ' '); } for (let key in this.rule.inputs) { - if (oldRuleInputs.hasOwnProperty(key) && - oldRuleInputTypes[key] === this.rule.inputTypes[key]) { + if ( + oldRuleInputs.hasOwnProperty(key) && + oldRuleInputTypes[key] === this.rule.inputTypes[key] + ) { this.rule.inputs[key] = oldRuleInputs[key]; } } @@ -282,23 +277,21 @@ export class RuleEditorComponent ngOnInit(): void { this.isInvalid = false; /** - * Rule editors are usually used in two ways. Inline or in a modal. - * When in a modal, the save button is in the modal html and when - * inline it is in the rule editors template. When listening to the - * object validity change event, we need to know which button to - * disable. If we are inline, we disable the button in the - * rule-editor template. Which is why we using the if condition - * below. - */ + * Rule editors are usually used in two ways. Inline or in a modal. + * When in a modal, the save button is in the modal html and when + * inline it is in the rule editors template. When listening to the + * object validity change event, we need to know which button to + * disable. If we are inline, we disable the button in the + * rule-editor template. Which is why we using the if condition + * below. + */ if (this.isEditingRuleInline) { this.modalId = Symbol(); - this.eventBusGroup.on( - ObjectFormValidityChangeEvent, - event => { - if (event.message.modalId === this.modalId) { - this.isInvalid = event.message.value; - } - }); + this.eventBusGroup.on(ObjectFormValidityChangeEvent, event => { + if (event.message.modalId === this.modalId) { + this.isInvalid = event.message.value; + } + }); } this.currentInteractionId = this.stateInteractionIdService.savedMemento; this.editRuleForm = {}; @@ -311,10 +304,12 @@ export class RuleEditorComponent // List-of-sets-of-translatable-html-content-ids-editor // could not able to assign this.rule.inputTypes.x default values. if (this.rule.inputTypes.x === 'ListOfSetsOfTranslatableHtmlContentIds') { - if (this.rule.inputs.x[0] === undefined || - this.rule.inputs.x[0]?.length === 0) { + if ( + this.rule.inputs.x[0] === undefined || + this.rule.inputs.x[0]?.length === 0 + ) { let box = []; - (this.ruleDescriptionChoices).map(choice => { + this.ruleDescriptionChoices.map(choice => { box.push([choice.val]); }); this.rule.inputs.x = box; @@ -337,7 +332,9 @@ export class RuleEditorComponent } } -angular.module('oppia').directive('oppiaRuleEditor', - downgradeComponent({ - component: RuleEditorComponent - }) as angular.IDirectiveFactory); +angular.module('oppia').directive( + 'oppiaRuleEditor', + downgradeComponent({ + component: RuleEditorComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/components/state-directives/rule-editor/rule-type-selector.directive.ts b/core/templates/components/state-directives/rule-editor/rule-type-selector.directive.ts index 0cae042e6750..bc5571e36c8e 100644 --- a/core/templates/components/state-directives/rule-editor/rule-type-selector.directive.ts +++ b/core/templates/components/state-directives/rule-editor/rule-type-selector.directive.ts @@ -16,12 +16,12 @@ * @fileoverview Component for the rule type selector. */ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { StateInteractionIdService } from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; -import { ReplaceInputsWithEllipsesPipe } from 'filters/string-utility-filters/replace-inputs-with-ellipses.pipe'; +import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {StateInteractionIdService} from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; +import {ReplaceInputsWithEllipsesPipe} from 'filters/string-utility-filters/replace-inputs-with-ellipses.pipe'; import INTERACTION_SPECS from 'interactions/interaction_specs.json'; -import { InteractionSpecsKey } from 'pages/interaction-specs.constants'; +import {InteractionSpecsKey} from 'pages/interaction-specs.constants'; interface Choice { id: string; @@ -30,7 +30,7 @@ interface Choice { @Component({ selector: 'oppia-rule-type-selector', - templateUrl: './rule-type-selector.component.html' + templateUrl: './rule-type-selector.component.html', }) export class RuleTypeSelector implements OnInit { // This property is initialized using Angular lifecycle hooks @@ -42,7 +42,7 @@ export class RuleTypeSelector implements OnInit { constructor( private stateInteractionIdService: StateInteractionIdService, - private replaceInputsWithEllipsesPipe: ReplaceInputsWithEllipsesPipe, + private replaceInputsWithEllipsesPipe: ReplaceInputsWithEllipsesPipe ) {} selectedRule($event: Event): void { @@ -50,9 +50,10 @@ export class RuleTypeSelector implements OnInit { } ngOnInit(): void { - let ruleTypesToDescriptions = INTERACTION_SPECS[ - this.stateInteractionIdService.savedMemento as InteractionSpecsKey - ].rule_descriptions; + let ruleTypesToDescriptions = + INTERACTION_SPECS[ + this.stateInteractionIdService.savedMemento as InteractionSpecsKey + ].rule_descriptions; type RuleTypeToDescription = { [key in keyof typeof ruleTypesToDescriptions]: string; @@ -68,7 +69,8 @@ export class RuleTypeSelector implements OnInit { this.choices.push({ id: ruleType, text: this.replaceInputsWithEllipsesPipe.transform( - ruleTypesToDescriptions[ruleType as keyof RuleTypeToDescription]) + ruleTypesToDescriptions[ruleType as keyof RuleTypeToDescription] + ), }); idx++; } @@ -77,7 +79,9 @@ export class RuleTypeSelector implements OnInit { // editor. if (equalToIndex) { [this.choices[0], this.choices[equalToIndex]] = [ - this.choices[equalToIndex], this.choices[0]]; + this.choices[equalToIndex], + this.choices[0], + ]; } if (this.localValue === null || this.localValue === undefined) { @@ -88,8 +92,9 @@ export class RuleTypeSelector implements OnInit { } } - -angular.module('oppia').directive('oppiaRuleTypeSelector', +angular.module('oppia').directive( + 'oppiaRuleTypeSelector', downgradeComponent({ - component: RuleTypeSelector - }) as angular.IDirectiveFactory); + component: RuleTypeSelector, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/components/state-directives/solution-editor/solution-editor.component.spec.ts b/core/templates/components/state-directives/solution-editor/solution-editor.component.spec.ts index e37cd8e0f5b8..d489e93d7156 100644 --- a/core/templates/components/state-directives/solution-editor/solution-editor.component.spec.ts +++ b/core/templates/components/state-directives/solution-editor/solution-editor.component.spec.ts @@ -16,15 +16,15 @@ * @fileoverview Unit test for Solution Editor Component. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { SolutionEditor } from './solution-editor.component'; -import { EditabilityService } from 'services/editability.service'; -import { SolutionObjectFactory } from 'domain/exploration/SolutionObjectFactory'; -import { StateCustomizationArgsService } from 'components/state-editor/state-editor-properties-services/state-customization-args.service'; -import { ExplorationHtmlFormatterService } from 'services/exploration-html-formatter.service'; -import { StateInteractionIdService } from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; -import { StateSolutionService } from 'components/state-editor/state-editor-properties-services/state-solution.service'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {SolutionEditor} from './solution-editor.component'; +import {EditabilityService} from 'services/editability.service'; +import {SolutionObjectFactory} from 'domain/exploration/SolutionObjectFactory'; +import {StateCustomizationArgsService} from 'components/state-editor/state-editor-properties-services/state-customization-args.service'; +import {ExplorationHtmlFormatterService} from 'services/exploration-html-formatter.service'; +import {StateInteractionIdService} from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; +import {StateSolutionService} from 'components/state-editor/state-editor-properties-services/state-solution.service'; class MockStateCustomizationArgsService { savedMemento = 'data3'; @@ -36,7 +36,7 @@ class MockStateInteractionIdService { class MockStateSolutionService { savedMemento = { - correctAnswer: 'data1' + correctAnswer: 'data1', }; } @@ -61,33 +61,31 @@ describe('Solution editor component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - SolutionEditor - ], + declarations: [SolutionEditor], providers: [ SolutionObjectFactory, { provide: EditabilityService, - useClass: MockEditabilityService + useClass: MockEditabilityService, }, { provide: ExplorationHtmlFormatterService, - useClass: MockExplorationHtmlFormatterService + useClass: MockExplorationHtmlFormatterService, }, { provide: StateSolutionService, - useClass: MockStateSolutionService + useClass: MockStateSolutionService, }, { provide: StateInteractionIdService, - useClass: MockStateInteractionIdService + useClass: MockStateInteractionIdService, }, { provide: StateCustomizationArgsService, - useClass: MockStateCustomizationArgsService - } + useClass: MockStateCustomizationArgsService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -100,7 +98,10 @@ describe('Solution editor component', () => { stateSolutionService = TestBed.inject(StateSolutionService); stateSolutionService.savedMemento = solutionObjectFactory.createNew( - true, 'correct_answer', '

Hint Index 0

', '0' + true, + 'correct_answer', + '

Hint Index 0

', + '0' ); fixture.detectChanges(); }); @@ -111,12 +112,10 @@ describe('Solution editor component', () => { component.ngOnInit(); expect(editabilityService.isEditable).toHaveBeenCalled(); - expect(component.EXPLANATION_FORM_SCHEMA).toEqual( - { - type: 'html', - ui_config: {} - } - ); + expect(component.EXPLANATION_FORM_SCHEMA).toEqual({ + type: 'html', + ui_config: {}, + }); }); it('should open editor modal', () => { @@ -129,7 +128,11 @@ describe('Solution editor component', () => { it('should save new solution', () => { let solution = solutionObjectFactory.createNew( - true, 'answer', 'Html', 'XyzID'); + true, + 'answer', + 'Html', + 'XyzID' + ); spyOn(component.saveSolution, 'emit').and.stub(); component.updateNewSolution(solution); @@ -145,12 +148,11 @@ describe('Solution editor component', () => { expect(component.getAnswerHtml).toHaveBeenCalled(); }); - it('should throw error during get answer html if solution is not saved yet', - () => { - stateSolutionService.savedMemento = null; + it('should throw error during get answer html if solution is not saved yet', () => { + stateSolutionService.savedMemento = null; - expect(() => { - component.getAnswerHtml(); - }).toThrowError('Expected solution to be defined'); - }); + expect(() => { + component.getAnswerHtml(); + }).toThrowError('Expected solution to be defined'); + }); }); diff --git a/core/templates/components/state-directives/solution-editor/solution-editor.component.ts b/core/templates/components/state-directives/solution-editor/solution-editor.component.ts index 7c09339225b8..f9f5f6e140f6 100644 --- a/core/templates/components/state-directives/solution-editor/solution-editor.component.ts +++ b/core/templates/components/state-directives/solution-editor/solution-editor.component.ts @@ -16,14 +16,14 @@ * @fileoverview Component for the solution editor. */ -import { Component, EventEmitter, OnInit, Output } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { StateCustomizationArgsService } from 'components/state-editor/state-editor-properties-services/state-customization-args.service'; -import { EditabilityService } from 'services/editability.service'; -import { ExplorationHtmlFormatterService } from 'services/exploration-html-formatter.service'; -import { StateInteractionIdService } from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; -import { StateSolutionService } from 'components/state-editor/state-editor-properties-services/state-solution.service'; -import { Solution } from 'domain/exploration/SolutionObjectFactory'; +import {Component, EventEmitter, OnInit, Output} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {StateCustomizationArgsService} from 'components/state-editor/state-editor-properties-services/state-customization-args.service'; +import {EditabilityService} from 'services/editability.service'; +import {ExplorationHtmlFormatterService} from 'services/exploration-html-formatter.service'; +import {StateInteractionIdService} from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; +import {StateSolutionService} from 'components/state-editor/state-editor-properties-services/state-solution.service'; +import {Solution} from 'domain/exploration/SolutionObjectFactory'; interface ExplanationFormSchema { type: string; @@ -32,7 +32,7 @@ interface ExplanationFormSchema { @Component({ selector: 'oppia-solution-editor', - templateUrl: './solution-editor.component.html' + templateUrl: './solution-editor.component.html', }) export class SolutionEditor implements OnInit { @Output() saveSolution: EventEmitter = new EventEmitter(); @@ -50,7 +50,7 @@ export class SolutionEditor implements OnInit { private explorationHtmlFormatterService: ExplorationHtmlFormatterService, private stateCustomizationArgsService: StateCustomizationArgsService, private stateInteractionIdService: StateInteractionIdService, - public stateSolutionService: StateSolutionService, + public stateSolutionService: StateSolutionService ) {} getAnswerHtml(): string { @@ -60,7 +60,8 @@ export class SolutionEditor implements OnInit { return this.explorationHtmlFormatterService.getAnswerHtml( this.stateSolutionService.savedMemento.correctAnswer, this.stateInteractionIdService.savedMemento, - this.stateCustomizationArgsService.savedMemento); + this.stateCustomizationArgsService.savedMemento + ); } updateNewSolution(value: Solution): void { @@ -76,12 +77,14 @@ export class SolutionEditor implements OnInit { this.EXPLANATION_FORM_SCHEMA = { type: 'html', - ui_config: {} + ui_config: {}, }; } } -angular.module('oppia').directive('oppiaSolutionEditor', +angular.module('oppia').directive( + 'oppiaSolutionEditor', downgradeComponent({ - component: SolutionEditor - }) as angular.IDirectiveFactory); + component: SolutionEditor, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/components/state-directives/solution-editor/solution-explanation-editor.component.spec.ts b/core/templates/components/state-directives/solution-editor/solution-explanation-editor.component.spec.ts index a0cac2eff5e7..51acc3a2bbbc 100644 --- a/core/templates/components/state-directives/solution-editor/solution-explanation-editor.component.spec.ts +++ b/core/templates/components/state-directives/solution-editor/solution-explanation-editor.component.spec.ts @@ -16,14 +16,23 @@ * @fileoverview Unit test for Solution Explanation Editor Component. */ -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { EditabilityService } from 'services/editability.service'; -import { ContextService } from 'services/context.service'; -import { SolutionExplanationEditor } from './solution-explanation-editor.component'; -import { ExternalSaveService } from 'services/external-save.service'; -import { StateSolutionService } from 'components/state-editor/state-editor-properties-services/state-solution.service'; -import { Solution, SolutionObjectFactory } from 'domain/exploration/SolutionObjectFactory'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import {EditabilityService} from 'services/editability.service'; +import {ContextService} from 'services/context.service'; +import {SolutionExplanationEditor} from './solution-explanation-editor.component'; +import {ExternalSaveService} from 'services/external-save.service'; +import {StateSolutionService} from 'components/state-editor/state-editor-properties-services/state-solution.service'; +import { + Solution, + SolutionObjectFactory, +} from 'domain/exploration/SolutionObjectFactory'; class MockStateSolutionService { displayed = { @@ -32,8 +41,8 @@ class MockStateSolutionService { contentId: 'contentId', get html(): string { return '

Hello world

'; - } - } + }, + }, }; savedMemento = { @@ -42,8 +51,8 @@ class MockStateSolutionService { contentId: 'xyz', get html(): string { return '

Hello world 2

'; - } - } + }, + }, }; saveDisplayedValue() {} @@ -61,9 +70,7 @@ describe('Solution explanation editor', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - SolutionExplanationEditor - ], + declarations: [SolutionExplanationEditor], providers: [ ContextService, EditabilityService, @@ -71,10 +78,10 @@ describe('Solution explanation editor', () => { SolutionObjectFactory, { provide: StateSolutionService, - useClass: MockStateSolutionService - } + useClass: MockStateSolutionService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -87,9 +94,9 @@ describe('Solution explanation editor', () => { stateSolutionService = TestBed.inject(StateSolutionService); externalSaveService = TestBed.inject(ExternalSaveService); - - spyOnProperty(externalSaveService, 'onExternalSave') - .and.returnValue(externalSaveServiceEmitter); + spyOnProperty(externalSaveService, 'onExternalSave').and.returnValue( + externalSaveServiceEmitter + ); spyOn(contextService, 'getEntityType').and.returnValue('question'); spyOn(editabilityService, 'isEditable').and.returnValue(true); @@ -105,8 +112,8 @@ describe('Solution explanation editor', () => { const schema = { type: 'html', ui_config: { - hide_complex_extensions: true - } + hide_complex_extensions: true, + }, }; expect(component.isEditable).toEqual(true); @@ -126,8 +133,8 @@ describe('Solution explanation editor', () => { const schema = { type: 'html', ui_config: { - hide_complex_extensions: true - } + hide_complex_extensions: true, + }, }; component.openExplanationEditor(); diff --git a/core/templates/components/state-directives/solution-editor/solution-explanation-editor.component.ts b/core/templates/components/state-directives/solution-editor/solution-explanation-editor.component.ts index 42e94d963e1c..46eb06f12c88 100644 --- a/core/templates/components/state-directives/solution-editor/solution-explanation-editor.component.ts +++ b/core/templates/components/state-directives/solution-editor/solution-explanation-editor.component.ts @@ -16,15 +16,24 @@ * @fileoverview Component for the solution explanation editor. */ -import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { Subscription } from 'rxjs'; -import { ContextService } from 'services/context.service'; -import { EditabilityService } from 'services/editability.service'; -import { ExternalSaveService } from 'services/external-save.service'; -import { StateSolutionService } from 'components/state-editor/state-editor-properties-services/state-solution.service'; -import { Solution } from 'domain/exploration/SolutionObjectFactory'; -import { CALCULATION_TYPE_CHARACTER, HtmlLengthService } from 'services/html-length.service'; +import { + Component, + EventEmitter, + OnDestroy, + OnInit, + Output, +} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {Subscription} from 'rxjs'; +import {ContextService} from 'services/context.service'; +import {EditabilityService} from 'services/editability.service'; +import {ExternalSaveService} from 'services/external-save.service'; +import {StateSolutionService} from 'components/state-editor/state-editor-properties-services/state-solution.service'; +import {Solution} from 'domain/exploration/SolutionObjectFactory'; +import { + CALCULATION_TYPE_CHARACTER, + HtmlLengthService, +} from 'services/html-length.service'; interface ExplanationFormSchema { type: string; @@ -33,10 +42,9 @@ interface ExplanationFormSchema { @Component({ selector: 'oppia-solution-explanation-editor', - templateUrl: './solution-explanation-editor.component.html' + templateUrl: './solution-explanation-editor.component.html', }) -export class SolutionExplanationEditor - implements OnDestroy, OnInit { +export class SolutionExplanationEditor implements OnDestroy, OnInit { @Output() saveSolution: EventEmitter = new EventEmitter(); // These properties are initialized using Angular lifecycle hooks @@ -52,7 +60,7 @@ export class SolutionExplanationEditor private editabilityService: EditabilityService, private externalSaveService: ExternalSaveService, private stateSolutionService: StateSolutionService, - private htmlLengthService: HtmlLengthService, + private htmlLengthService: HtmlLengthService ) {} updateExplanationHtml(newHtmlString: string): void { @@ -79,7 +87,9 @@ export class SolutionExplanationEditor return Boolean( this.htmlLengthService.computeHtmlLength( this.stateSolutionService.displayed.explanation.html, - CALCULATION_TYPE_CHARACTER) > 3000); + CALCULATION_TYPE_CHARACTER + ) > 3000 + ); } saveThisExplanation(): void { @@ -117,14 +127,16 @@ export class SolutionExplanationEditor this.EXPLANATION_FORM_SCHEMA = { type: 'html', ui_config: { - hide_complex_extensions: ( - this.contextService.getEntityType() === 'question') - } + hide_complex_extensions: + this.contextService.getEntityType() === 'question', + }, }; } } -angular.module('oppia').directive('oppiaSolutionExplanationEditor', +angular.module('oppia').directive( + 'oppiaSolutionExplanationEditor', downgradeComponent({ - component: SolutionExplanationEditor - }) as angular.IDirectiveFactory); + component: SolutionExplanationEditor, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/components/state-editor/state-content-editor/state-content-editor.component.spec.ts b/core/templates/components/state-editor/state-content-editor/state-content-editor.component.spec.ts index 95ca8371908c..118285653523 100644 --- a/core/templates/components/state-editor/state-content-editor/state-content-editor.component.spec.ts +++ b/core/templates/components/state-editor/state-content-editor/state-content-editor.component.spec.ts @@ -16,18 +16,18 @@ * @fileoverview Unit tests for the state content editor directive. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { fakeAsync, tick } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import {fakeAsync, tick} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; -import { StateContentEditorComponent } from './state-content-editor.component'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; +import {StateContentEditorComponent} from './state-content-editor.component'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; -import { ChangeListService } from 'pages/exploration-editor-page/services/change-list.service'; -import { ExternalSaveService } from 'services/external-save.service'; -import { StateContentService } from 'components/state-editor/state-editor-properties-services/state-content.service'; +import {ChangeListService} from 'pages/exploration-editor-page/services/change-list.service'; +import {ExternalSaveService} from 'services/external-save.service'; +import {StateContentService} from 'components/state-editor/state-editor-properties-services/state-content.service'; describe('StateHintsEditorComponent', () => { let component: StateContentEditorComponent; @@ -36,28 +36,19 @@ describe('StateHintsEditorComponent', () => { let externalSaveService: ExternalSaveService; let stateContentService: StateContentService; - let _getContent = function(contentId: string, contentString: string) { + let _getContent = function (contentId: string, contentString: string) { return SubtitledHtml.createFromBackendDict({ content_id: contentId, - html: contentString + html: contentString, }); }; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - StateContentEditorComponent, - MockTranslatePipe - ], - providers: [ - ChangeListService, - ExternalSaveService, - StateContentService, - ], - schemas: [NO_ERRORS_SCHEMA] + imports: [HttpClientTestingModule], + declarations: [StateContentEditorComponent, MockTranslatePipe], + providers: [ChangeListService, ExternalSaveService, StateContentService], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -72,7 +63,7 @@ describe('StateHintsEditorComponent', () => { fixture.detectChanges(); }); - it('should start with the content editor not being open', function() { + it('should start with the content editor not being open', function () { component.ngOnInit(); expect(component.contentEditorIsOpen).toBeFalse(); @@ -80,10 +71,10 @@ describe('StateHintsEditorComponent', () => { it('should save hint when external save event is triggered', fakeAsync(() => { let onExternalSaveEmitter = new EventEmitter(); - spyOnProperty(externalSaveService, 'onExternalSave') - .and.returnValue(onExternalSaveEmitter); - spyOn(component.saveStateContent, 'emit') - .and.callThrough(); + spyOnProperty(externalSaveService, 'onExternalSave').and.returnValue( + onExternalSaveEmitter + ); + spyOn(component.saveStateContent, 'emit').and.callThrough(); component.ngOnInit(); component.contentEditorIsOpen = true; @@ -91,44 +82,42 @@ describe('StateHintsEditorComponent', () => { onExternalSaveEmitter.emit(); tick(); - expect(component.saveStateContent.emit) - .toHaveBeenCalled(); + expect(component.saveStateContent.emit).toHaveBeenCalled(); })); - it('should hide card height limit warning', function() { + it('should hide card height limit warning', function () { component.cardHeightLimitWarningIsShown = true; component.hideCardHeightLimitWarning(); expect(component.cardHeightLimitWarningIsShown).toBeFalse(); }); - it('should show card height limit warning', function() { - stateContentService.displayed = ( - _getContent('content', '')); + it('should show card height limit warning', function () { + stateContentService.displayed = _getContent('content', ''); expect(component.isCardContentLengthLimitReached()).toBeFalse(); }); - it('should correctly handle no-op edits', function() { + it('should correctly handle no-op edits', function () { component.ngOnInit(); expect(component.contentEditorIsOpen).toBeFalse(); - expect(stateContentService.savedMemento).toEqual(_getContent( - 'content', '')); + expect(stateContentService.savedMemento).toEqual( + _getContent('content', '') + ); component.openStateContentEditor(); expect(component.contentEditorIsOpen).toBeTrue(); - stateContentService.displayed = ( - _getContent('content', '')); + stateContentService.displayed = _getContent('content', ''); component.onSaveContentButtonClicked(); expect(component.contentEditorIsOpen).toBeFalse(); expect(changeListService.getChangeList()).toEqual([]); }); - it('should check that content edits are saved correctly', function() { + it('should check that content edits are saved correctly', function () { spyOn(component.saveStateContent, 'emit'); component.ngOnInit(); @@ -139,18 +128,19 @@ describe('StateHintsEditorComponent', () => { stateContentService.displayed = _getContent('content', 'babababa'); component.onSaveContentButtonClicked(); - expect(component.saveStateContent.emit) - .toHaveBeenCalled(); + expect(component.saveStateContent.emit).toHaveBeenCalled(); component.openStateContentEditor(); stateContentService.displayed = _getContent( - 'content', 'And now for something completely different.'); + 'content', + 'And now for something completely different.' + ); component.onSaveContentButtonClicked(); expect(component.saveStateContent.emit).toHaveBeenCalled(); }); - it('should not save changes to content when edit is cancelled', function() { + it('should not save changes to content when edit is cancelled', function () { component.ngOnInit(); var contentBeforeEdit = angular.copy(stateContentService.savedMemento); @@ -163,19 +153,19 @@ describe('StateHintsEditorComponent', () => { expect(stateContentService.displayed).toEqual(contentBeforeEdit); }); - it('should call the callback function on-save', function() { + it('should call the callback function on-save', function () { spyOn(component.saveStateContent, 'emit'); component.onSaveContentButtonClicked(); - expect(component.saveStateContent.emit) - .toHaveBeenCalled(); + expect(component.saveStateContent.emit).toHaveBeenCalled(); }); it('should update when card height limit is reached', () => { component.cardHeightLimitReached = false; spyOn(component, 'isCardHeightLimitReached').and.returnValue( - !component.cardHeightLimitReached); + !component.cardHeightLimitReached + ); component.ngAfterViewChecked(); diff --git a/core/templates/components/state-editor/state-content-editor/state-content-editor.component.ts b/core/templates/components/state-editor/state-content-editor/state-content-editor.component.ts index d16923ddbbf1..b451b4e1840b 100644 --- a/core/templates/components/state-editor/state-content-editor/state-content-editor.component.ts +++ b/core/templates/components/state-editor/state-content-editor/state-content-editor.component.ts @@ -16,18 +16,25 @@ * @fileoverview Component for the state content editor. */ -import { Component, OnInit, ChangeDetectorRef, Input, Output, EventEmitter } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; - -import { ContextService } from 'services/context.service'; -import { EditabilityService } from 'services/editability.service'; -import { EditorFirstTimeEventsService } from 'pages/exploration-editor-page/services/editor-first-time-events.service'; -import { ExternalSaveService } from 'services/external-save.service'; -import { StateContentService } from 'components/state-editor/state-editor-properties-services/state-content.service'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; - -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { Subscription } from 'rxjs'; +import { + Component, + OnInit, + ChangeDetectorRef, + Input, + Output, + EventEmitter, +} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; + +import {ContextService} from 'services/context.service'; +import {EditabilityService} from 'services/editability.service'; +import {EditorFirstTimeEventsService} from 'pages/exploration-editor-page/services/editor-first-time-events.service'; +import {ExternalSaveService} from 'services/external-save.service'; +import {StateContentService} from 'components/state-editor/state-editor-properties-services/state-content.service'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; + +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {Subscription} from 'rxjs'; interface HTMLSchema { type: string; @@ -69,9 +76,9 @@ export class StateContentEditorComponent implements OnInit { this.HTML_SCHEMA = { type: 'html', ui_config: { - hide_complex_extensions: ( - this.contextService.getEntityType() === 'question') - } + hide_complex_extensions: + this.contextService.getEntityType() === 'question', + }, }; if (this.stateContentService.displayed) { this.contentId = this.stateContentService.displayed.contentId; @@ -79,20 +86,18 @@ export class StateContentEditorComponent implements OnInit { this.cardHeightLimitWarningIsShown = true; this.directiveSubscriptions.add( - this.externalSaveService.onExternalSave.subscribe( - () => { - if (this.contentEditorIsOpen) { - this.saveContent(); - } + this.externalSaveService.onExternalSave.subscribe(() => { + if (this.contentEditorIsOpen) { + this.saveContent(); } - ) + }) ); this.stateEditorService.updateStateContentEditorInitialised(); } isCardContentLengthLimitReached(): boolean { let content = this.stateContentService.displayed.html; - return (content.length > 4500); + return content.length > 4500; } isCardHeightLimitReached(): boolean { @@ -100,7 +105,7 @@ export class StateContentEditorComponent implements OnInit { '.oppia-shadow-preview-card .oppia-learner-view-card-top-section' ); let height = shadowPreviewCard.height() as number; - return (height > 630); + return height > 630; } ngAfterViewChecked(): void { @@ -146,7 +151,9 @@ export class StateContentEditorComponent implements OnInit { } } -angular.module('oppia').directive('oppiaStateContentEditor', -downgradeComponent({ - component: StateContentEditorComponent -}) as angular.IDirectiveFactory); +angular.module('oppia').directive( + 'oppiaStateContentEditor', + downgradeComponent({ + component: StateContentEditorComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/components/state-editor/state-editor-properties-services/state-card-is-checkpoint.service.ts b/core/templates/components/state-editor/state-editor-properties-services/state-card-is-checkpoint.service.ts index 89cd444cd458..5b2895bcb295 100644 --- a/core/templates/components/state-editor/state-editor-properties-services/state-card-is-checkpoint.service.ts +++ b/core/templates/components/state-editor/state-editor-properties-services/state-card-is-checkpoint.service.ts @@ -15,26 +15,29 @@ /** * @fileoverview A data service that stores the card is checkpoint value. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { AlertsService } from 'services/alerts.service'; -import { StatePropertyService } from +import {AlertsService} from 'services/alerts.service'; +import { + StatePropertyService, // eslint-disable-next-line max-len - 'components/state-editor/state-editor-properties-services/state-property.service'; -import { UtilsService } from 'services/utils.service'; +} from 'components/state-editor/state-editor-properties-services/state-property.service'; +import {UtilsService} from 'services/utils.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) -export class StateCardIsCheckpointService extends - StatePropertyService { +export class StateCardIsCheckpointService extends StatePropertyService { constructor(alertsService: AlertsService, utilsService: UtilsService) { super(alertsService, utilsService); this.setterMethodKey = 'saveCardIsCheckpoint'; } } -angular.module('oppia').factory( - 'StateCardIsCheckpointService', downgradeInjectable( - StateCardIsCheckpointService)); +angular + .module('oppia') + .factory( + 'StateCardIsCheckpointService', + downgradeInjectable(StateCardIsCheckpointService) + ); diff --git a/core/templates/components/state-editor/state-editor-properties-services/state-content.service.ts b/core/templates/components/state-editor/state-editor-properties-services/state-content.service.ts index cc1c4dcc0eb9..9832918ee036 100644 --- a/core/templates/components/state-editor/state-editor-properties-services/state-content.service.ts +++ b/core/templates/components/state-editor/state-editor-properties-services/state-content.service.ts @@ -15,18 +15,19 @@ /** * @fileoverview A data service that stores the current state content. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { AlertsService } from 'services/alerts.service'; -import { StatePropertyService } from +import {AlertsService} from 'services/alerts.service'; +import { + StatePropertyService, // eslint-disable-next-line max-len - 'components/state-editor/state-editor-properties-services/state-property.service'; -import { UtilsService } from 'services/utils.service'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; +} from 'components/state-editor/state-editor-properties-services/state-property.service'; +import {UtilsService} from 'services/utils.service'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) // TODO(sll): Add validation. export class StateContentService extends StatePropertyService { @@ -43,5 +44,6 @@ export class StateContentService extends StatePropertyService { } } -angular.module('oppia').factory( - 'StateContentService', downgradeInjectable(StateContentService)); +angular + .module('oppia') + .factory('StateContentService', downgradeInjectable(StateContentService)); diff --git a/core/templates/components/state-editor/state-editor-properties-services/state-customization-args.service.ts b/core/templates/components/state-editor/state-editor-properties-services/state-customization-args.service.ts index 61234300fe60..7282eb50143a 100644 --- a/core/templates/components/state-editor/state-editor-properties-services/state-customization-args.service.ts +++ b/core/templates/components/state-editor/state-editor-properties-services/state-customization-args.service.ts @@ -18,23 +18,22 @@ * to dicts of the form {value: customization_arg_value}. */ -import { EventEmitter, Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {EventEmitter, Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; -import { AlertsService } from 'services/alerts.service'; -import { StatePropertyService } from +import {AlertsService} from 'services/alerts.service'; +import { + StatePropertyService, // eslint-disable-next-line max-len - 'components/state-editor/state-editor-properties-services/state-property.service'; -import { UtilsService } from 'services/utils.service'; -import { InteractionCustomizationArgs } from - 'interactions/customization-args-defs'; +} from 'components/state-editor/state-editor-properties-services/state-property.service'; +import {UtilsService} from 'services/utils.service'; +import {InteractionCustomizationArgs} from 'interactions/customization-args-defs'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) // TODO(sll): Add validation. -export class StateCustomizationArgsService extends - StatePropertyService { +export class StateCustomizationArgsService extends StatePropertyService { private _schemaBasedFormsShownEventEmitter = new EventEmitter(); constructor(alertsService: AlertsService, utilsService: UtilsService) { super(alertsService, utilsService); @@ -46,6 +45,9 @@ export class StateCustomizationArgsService extends } } -angular.module('oppia').factory( - 'StateCustomizationArgsService', downgradeInjectable( - StateCustomizationArgsService)); +angular + .module('oppia') + .factory( + 'StateCustomizationArgsService', + downgradeInjectable(StateCustomizationArgsService) + ); diff --git a/core/templates/components/state-editor/state-editor-properties-services/state-editor.service.spec.ts b/core/templates/components/state-editor/state-editor-properties-services/state-editor.service.spec.ts index cb2cf5acf50a..b6ebec28e0c4 100644 --- a/core/templates/components/state-editor/state-editor-properties-services/state-editor.service.spec.ts +++ b/core/templates/components/state-editor/state-editor-properties-services/state-editor.service.spec.ts @@ -16,20 +16,24 @@ * @fileoverview Unit test for the Editor state service. */ -import { TestBed } from '@angular/core/testing'; +import {TestBed} from '@angular/core/testing'; -import { StateEditorService } from +import { + StateEditorService, // eslint-disable-next-line max-len - 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { AnswerGroupObjectFactory } from 'domain/exploration/AnswerGroupObjectFactory'; -import { Hint } from 'domain/exploration/hint-object.model'; -import { Interaction, InteractionObjectFactory } from 'domain/exploration/InteractionObjectFactory'; -import { OutcomeObjectFactory } from 'domain/exploration/OutcomeObjectFactory'; -import { SolutionObjectFactory } from 'domain/exploration/SolutionObjectFactory'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { SubtitledUnicodeObjectFactory } from 'domain/exploration/SubtitledUnicodeObjectFactory'; -import { SolutionValidityService } from 'pages/exploration-editor-page/editor-tab/services/solution-validity.service'; -import { Subscription } from 'rxjs'; +} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {AnswerGroupObjectFactory} from 'domain/exploration/AnswerGroupObjectFactory'; +import {Hint} from 'domain/exploration/hint-object.model'; +import { + Interaction, + InteractionObjectFactory, +} from 'domain/exploration/InteractionObjectFactory'; +import {OutcomeObjectFactory} from 'domain/exploration/OutcomeObjectFactory'; +import {SolutionObjectFactory} from 'domain/exploration/SolutionObjectFactory'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {SubtitledUnicodeObjectFactory} from 'domain/exploration/SubtitledUnicodeObjectFactory'; +import {SolutionValidityService} from 'pages/exploration-editor-page/editor-tab/services/solution-validity.service'; +import {Subscription} from 'rxjs'; describe('Editor state service', () => { let ecs: StateEditorService; @@ -54,7 +58,7 @@ describe('Editor state service', () => { beforeEach(() => { TestBed.configureTestingModule({ - providers: [StateEditorService] + providers: [StateEditorService], }); ecs = TestBed.inject(StateEditorService); @@ -106,15 +110,15 @@ describe('Editor state service', () => { placeholder: { value: { content_id: 'cid', - unicode_str: '1' - } + unicode_str: '1', + }, }, rows: { - value: 1 + value: 1, }, catchMisspellings: { - value: false - } + value: false, + }, }, hints: [], solution: { @@ -131,11 +135,14 @@ describe('Editor state service', () => { beforeEach(() => { stateEditorInitializedSpy = jasmine.createSpy('stateEditorInitialized'); stateEditorDirectiveInitializedSpy = jasmine.createSpy( - 'stateEditorDirectiveInitialized'); + 'stateEditorDirectiveInitialized' + ); interactionEditorInitializedSpy = jasmine.createSpy( - 'interactionEditorInitialized'); + 'interactionEditorInitialized' + ); showTranslationTabBusyModalSpy = jasmine.createSpy( - 'showTranslationTabBusyModal'); + 'showTranslationTabBusyModal' + ); refreshStateTranslationSpy = jasmine.createSpy('refreshStateTranslation'); updateAnswerChoicesSpy = jasmine.createSpy('updateAnswerChoices'); saveOutcomeDestDetailsSpy = jasmine.createSpy('saveOutcomeDestDetails'); @@ -144,24 +151,39 @@ describe('Editor state service', () => { testSubscriptions = new Subscription(); - testSubscriptions.add(ecs.onStateEditorInitialized.subscribe( - stateEditorInitializedSpy)); - testSubscriptions.add(ecs.onStateEditorDirectiveInitialized.subscribe( - stateEditorDirectiveInitializedSpy)); - testSubscriptions.add(ecs.onInteractionEditorInitialized.subscribe( - interactionEditorInitializedSpy)); - testSubscriptions.add(ecs.onShowTranslationTabBusyModal.subscribe( - showTranslationTabBusyModalSpy)); - testSubscriptions.add(ecs.onRefreshStateTranslation.subscribe( - refreshStateTranslationSpy)); - testSubscriptions.add(ecs.onUpdateAnswerChoices.subscribe( - updateAnswerChoicesSpy)); - testSubscriptions.add(ecs.onSaveOutcomeDestDetails.subscribe( - saveOutcomeDestDetailsSpy)); - testSubscriptions.add(ecs.onHandleCustomArgsUpdate.subscribe( - handleCustomArgsUpdateSpy)); - testSubscriptions.add(ecs.onObjectFormValidityChange.subscribe( - objectFormValidityChangeSpy)); + testSubscriptions.add( + ecs.onStateEditorInitialized.subscribe(stateEditorInitializedSpy) + ); + testSubscriptions.add( + ecs.onStateEditorDirectiveInitialized.subscribe( + stateEditorDirectiveInitializedSpy + ) + ); + testSubscriptions.add( + ecs.onInteractionEditorInitialized.subscribe( + interactionEditorInitializedSpy + ) + ); + testSubscriptions.add( + ecs.onShowTranslationTabBusyModal.subscribe( + showTranslationTabBusyModalSpy + ) + ); + testSubscriptions.add( + ecs.onRefreshStateTranslation.subscribe(refreshStateTranslationSpy) + ); + testSubscriptions.add( + ecs.onUpdateAnswerChoices.subscribe(updateAnswerChoicesSpy) + ); + testSubscriptions.add( + ecs.onSaveOutcomeDestDetails.subscribe(saveOutcomeDestDetailsSpy) + ); + testSubscriptions.add( + ecs.onHandleCustomArgsUpdate.subscribe(handleCustomArgsUpdateSpy) + ); + testSubscriptions.add( + ecs.onObjectFormValidityChange.subscribe(objectFormValidityChangeSpy) + ); }); afterAll(() => { @@ -197,7 +219,7 @@ describe('Editor state service', () => { it('should correctly set and get misconceptionsBySkill', () => { const misconceptionsBySkill = { skillId1: [0], - skillId2: [1, 2] + skillId2: [1, 2], }; expect(ecs.getMisconceptionsBySkill()).toEqual({}); ecs.setMisconceptionsBySkill(misconceptionsBySkill); @@ -216,91 +238,118 @@ describe('Editor state service', () => { choices: { value: [ new SubtitledHtml('Choice 1', ''), - new SubtitledHtml('Choice 2', '') - ] - } + new SubtitledHtml('Choice 2', ''), + ], + }, }; expect( ecs.getAnswerChoices( - 'MultipleChoiceInput', customizationArgsForMultipleChoiceInput) - ).toEqual([{ - val: 0, - label: 'Choice 1', - }, { - val: 1, - label: 'Choice 2', - }]); + 'MultipleChoiceInput', + customizationArgsForMultipleChoiceInput + ) + ).toEqual([ + { + val: 0, + label: 'Choice 1', + }, + { + val: 1, + label: 'Choice 2', + }, + ]); const customizationArgsForImageClickInput = { imageAndRegions: { value: { - labeledRegions: [{ - label: 'Label 1' - }, { - label: 'Label 2' - }] - } - } + labeledRegions: [ + { + label: 'Label 1', + }, + { + label: 'Label 2', + }, + ], + }, + }, }; expect( ecs.getAnswerChoices( - 'ImageClickInput', customizationArgsForImageClickInput) - ).toEqual([{ - val: 'Label 1', - label: 'Label 1', - }, { - val: 'Label 2', - label: 'Label 2', - }]); + 'ImageClickInput', + customizationArgsForImageClickInput + ) + ).toEqual([ + { + val: 'Label 1', + label: 'Label 1', + }, + { + val: 'Label 2', + label: 'Label 2', + }, + ]); const customizationArgsForItemSelectionAndDragAndDropInput = { choices: { value: [ new SubtitledHtml('Choice 1', 'ca_choices_0'), - new SubtitledHtml('Choice 2', 'ca_choices_1') - ] - } + new SubtitledHtml('Choice 2', 'ca_choices_1'), + ], + }, }; expect( ecs.getAnswerChoices( 'ItemSelectionInput', - customizationArgsForItemSelectionAndDragAndDropInput) - ).toEqual([{ - val: 'ca_choices_0', - label: 'Choice 1', - }, { - val: 'ca_choices_1', - label: 'Choice 2', - }]); + customizationArgsForItemSelectionAndDragAndDropInput + ) + ).toEqual([ + { + val: 'ca_choices_0', + label: 'Choice 1', + }, + { + val: 'ca_choices_1', + label: 'Choice 2', + }, + ]); expect( ecs.getAnswerChoices( 'DragAndDropSortInput', - customizationArgsForItemSelectionAndDragAndDropInput) - ).toEqual([{ - val: 'ca_choices_0', - label: 'Choice 1', - }, { - val: 'ca_choices_1', - label: 'Choice 2', - }]); + customizationArgsForItemSelectionAndDragAndDropInput + ) + ).toEqual([ + { + val: 'ca_choices_0', + label: 'Choice 1', + }, + { + val: 'ca_choices_1', + label: 'Choice 2', + }, + ]); expect( ecs.getAnswerChoices( 'NotDragAndDropSortInput', - customizationArgsForItemSelectionAndDragAndDropInput) + customizationArgsForItemSelectionAndDragAndDropInput + ) ).toEqual(null); }); - it('should return null when getting answer choices' + - ' if interactionID is empty', () => { - expect(ecs.getAnswerChoices('', { - choices: { - value: [ - new SubtitledHtml('Choice 1', ''), - new SubtitledHtml('Choice 2', '') - ] - } - })).toBeNull(); - }); + it( + 'should return null when getting answer choices' + + ' if interactionID is empty', + () => { + expect( + ecs.getAnswerChoices('', { + choices: { + value: [ + new SubtitledHtml('Choice 1', ''), + new SubtitledHtml('Choice 2', ''), + ], + }, + }) + ).toBeNull(); + } + ); it('should return if exploration is curated or not', () => { expect(ecs.isExplorationCurated()).toBeFalse(); @@ -424,28 +473,35 @@ describe('Editor state service', () => { ]; ecs.setInteraction(mockInteraction); - expect(ecs.interaction.answerGroups).toEqual( - [ - answerGroupObjectFactory.createNew( - [], - outcomeObjectFactory.createNew( - 'State', 'This is a new feedback text', '', []), - [], - '' + expect(ecs.interaction.answerGroups).toEqual([ + answerGroupObjectFactory.createNew( + [], + outcomeObjectFactory.createNew( + 'State', + 'This is a new feedback text', + '', + [] ), - ] - ); + [], + '' + ), + ]); ecs.setInteractionAnswerGroups(newAnswerGroups); expect(ecs.interaction.answerGroups).toEqual(newAnswerGroups); }); it('should set interaction default outcome', () => { let newDefaultOutcome = outcomeObjectFactory.createNew( - 'Hola1', '', 'Feedback text', []); + 'Hola1', + '', + 'Feedback text', + [] + ); ecs.setInteraction(mockInteraction); expect(ecs.interaction.defaultOutcome).toEqual( - outcomeObjectFactory.createNew('Hola', '', '', [])); + outcomeObjectFactory.createNew('Hola', '', '', []) + ); ecs.setInteractionDefaultOutcome(newDefaultOutcome); expect(ecs.interaction.defaultOutcome).toEqual(newDefaultOutcome); }); @@ -457,7 +513,7 @@ describe('Editor state service', () => { }, placeholder: { value: suof.createDefault('2', ''), - } + }, }; ecs.setInteraction(mockInteraction); expect(ecs.interaction.customizationArgs).toEqual({ @@ -465,11 +521,11 @@ describe('Editor state service', () => { value: suof.createDefault('1', 'cid'), }, rows: { - value: 1 + value: 1, }, catchMisspellings: { - value: false - } + value: false, + }, }); ecs.setInteractionCustomizationArgs(newCustomizationArgs); expect(ecs.interaction.customizationArgs).toEqual(newCustomizationArgs); @@ -485,25 +541,29 @@ describe('Editor state service', () => { }, }); ecs.setInteraction(mockInteraction); - expect(ecs.interaction.solution).toEqual(sof.createFromBackendDict({ - answer_is_exclusive: true, - correct_answer: 'test_answer', - explanation: { - content_id: '2', - html: 'test_explanation1', - }, - })); + expect(ecs.interaction.solution).toEqual( + sof.createFromBackendDict({ + answer_is_exclusive: true, + correct_answer: 'test_answer', + explanation: { + content_id: '2', + html: 'test_explanation1', + }, + }) + ); ecs.setInteractionSolution(newSolution); expect(ecs.interaction.solution).toEqual(newSolution); }); it('should set interaction hints', () => { - let newHints = [Hint.createFromBackendDict({ - hint_content: { - content_id: '', - html: 'This is a hint' - } - })]; + let newHints = [ + Hint.createFromBackendDict({ + hint_content: { + content_id: '', + html: 'This is a hint', + }, + }), + ]; ecs.setInteraction(mockInteraction); expect(ecs.interaction.hints).toEqual([]); ecs.setInteractionHints(newHints); @@ -544,15 +604,18 @@ describe('Editor state service', () => { expect(ecs.isCurrentSolutionValid()).toBeFalse(); }); - it('should throw error on deletion of current solution validity' + - ' if activeStateName is null', () => { - ecs.activeStateName = null; - expect(ecs.isCurrentSolutionValid()).toBeFalse(); - expect(() => { - ecs.deleteCurrentSolutionValidity(); - }).toThrowError('Active State for this solution is not set'); - expect(ecs.isCurrentSolutionValid()).toBeFalse(); - }); + it( + 'should throw error on deletion of current solution validity' + + ' if activeStateName is null', + () => { + ecs.activeStateName = null; + expect(ecs.isCurrentSolutionValid()).toBeFalse(); + expect(() => { + ecs.deleteCurrentSolutionValidity(); + }).toThrowError('Active State for this solution is not set'); + expect(ecs.isCurrentSolutionValid()).toBeFalse(); + } + ); it('should correctly set and get initActiveContentId', () => { ecs.setInitActiveContentId('content_id'); diff --git a/core/templates/components/state-editor/state-editor-properties-services/state-editor.service.ts b/core/templates/components/state-editor/state-editor-properties-services/state-editor.service.ts index 83398cf0ff09..d5ce32aca45f 100644 --- a/core/templates/components/state-editor/state-editor-properties-services/state-editor.service.ts +++ b/core/templates/components/state-editor/state-editor-properties-services/state-editor.service.ts @@ -18,40 +18,38 @@ */ import cloneDeep from 'lodash/cloneDeep'; -import { Observable } from 'rxjs'; +import {Observable} from 'rxjs'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { EventEmitter, Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {EventEmitter, Injectable} from '@angular/core'; -import { AnswerGroup } from - 'domain/exploration/AnswerGroupObjectFactory'; -import { Hint } from 'domain/exploration/hint-object.model'; -import { SubtitledHtml } from - 'domain/exploration/subtitled-html.model'; +import {AnswerGroup} from 'domain/exploration/AnswerGroupObjectFactory'; +import {Hint} from 'domain/exploration/hint-object.model'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; import { DragAndDropSortInputCustomizationArgs, ImageClickInputCustomizationArgs, InteractionCustomizationArgs, ItemSelectionInputCustomizationArgs, - MultipleChoiceInputCustomizationArgs + MultipleChoiceInputCustomizationArgs, } from 'extensions/interactions/customization-args-defs'; -import { Interaction } from 'domain/exploration/InteractionObjectFactory'; -import { Outcome } from 'domain/exploration/OutcomeObjectFactory'; -import { Solution } from 'domain/exploration/SolutionObjectFactory'; -import { SolutionValidityService } from - 'pages/exploration-editor-page/editor-tab/services/solution-validity.service'; -import { State } from 'domain/state/StateObjectFactory'; +import {Interaction} from 'domain/exploration/InteractionObjectFactory'; +import {Outcome} from 'domain/exploration/OutcomeObjectFactory'; +import {Solution} from 'domain/exploration/SolutionObjectFactory'; +import {SolutionValidityService} from 'pages/exploration-editor-page/editor-tab/services/solution-validity.service'; +import {State} from 'domain/state/StateObjectFactory'; export interface AnswerChoice { val: string | number | SubtitledHtml; label: string; } -type CustomizationArgs = ( - ItemSelectionInputCustomizationArgs | DragAndDropSortInputCustomizationArgs); +type CustomizationArgs = + | ItemSelectionInputCustomizationArgs + | DragAndDropSortInputCustomizationArgs; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class StateEditorService { constructor(private solutionValidityService: SolutionValidityService) {} @@ -63,13 +61,13 @@ export class StateEditorService { private _interactionEditorInitializedEventEmitter = new EventEmitter(); private _showTranslationTabBusyModalEventEmitter = new EventEmitter(); private _refreshStateTranslationEventEmitter = new EventEmitter(); - private _updateAnswerChoicesEventEmitter = - new EventEmitter(); + private _updateAnswerChoicesEventEmitter = new EventEmitter(); private _saveOutcomeDestDetailsEventEmitter = new EventEmitter(); private _saveOutcomeDestIfStuckDetailsEventEmitter = new EventEmitter(); - private _handleCustomArgsUpdateEventEmitter = - new EventEmitter(); + private _handleCustomArgsUpdateEventEmitter = new EventEmitter< + AnswerChoice[] + >(); private _stateNamesChangedEventEmitter = new EventEmitter(); private _objectFormValidityChangeEventEmitter = new EventEmitter(); @@ -140,7 +138,8 @@ export class StateEditorService { return ( this.stateInteractionEditorInitialised && this.stateResponsesInitialised && - this.stateEditorDirectiveInitialised); + this.stateEditorDirectiveInitialised + ); } getActiveStateName(): string | null { @@ -191,8 +190,7 @@ export class StateEditorService { this.interaction.setDefaultOutcome(newOutcome); } - setInteractionCustomizationArgs( - newArgs: InteractionCustomizationArgs): void { + setInteractionCustomizationArgs(newArgs: InteractionCustomizationArgs): void { this.interaction.setCustomizationArgs(newArgs); } @@ -212,8 +210,8 @@ export class StateEditorService { // equivalent to 'MultipleChoiceInput', 'ItemSelectionInput', // 'DragAndDropSortInput'. getAnswerChoices( - interactionId: string, - customizationArgs: InteractionCustomizationArgs + interactionId: string, + customizationArgs: InteractionCustomizationArgs ): AnswerChoice[] | null { if (!interactionId) { return null; @@ -222,19 +220,19 @@ export class StateEditorService { if (interactionId === 'MultipleChoiceInput') { return ( customizationArgs as MultipleChoiceInputCustomizationArgs - ).choices.value.map((val, ind) => ( - { val: ind, label: val.html } - )) as AnswerChoice[]; + ).choices.value.map((val, ind) => ({ + val: ind, + label: val.html, + })) as AnswerChoice[]; } else if (interactionId === 'ImageClickInput') { var _answerChoices = []; var imageWithRegions = ( - customizationArgs as ImageClickInputCustomizationArgs) - .imageAndRegions.value; - for ( - var j = 0; j < imageWithRegions.labeledRegions.length; j++) { + customizationArgs as ImageClickInputCustomizationArgs + ).imageAndRegions.value; + for (var j = 0; j < imageWithRegions.labeledRegions.length; j++) { _answerChoices.push({ val: imageWithRegions.labeledRegions[j].label, - label: imageWithRegions.labeledRegions[j].label + label: imageWithRegions.labeledRegions[j].label, }); } return _answerChoices; @@ -242,12 +240,11 @@ export class StateEditorService { interactionId === 'ItemSelectionInput' || interactionId === 'DragAndDropSortInput' ) { - return ( - customizationArgs as CustomizationArgs - ).choices.value.map( + return (customizationArgs as CustomizationArgs).choices.value.map( val => ({ - val: val.contentId, label: val.html} - ) + val: val.contentId, + label: val.html, + }) ) as AnswerChoice[]; } else { return null; @@ -288,9 +285,10 @@ export class StateEditorService { } setInapplicableSkillMisconceptionIds( - newInapplicableSkillMisconceptionIds: string[]): void { - this.inapplicableSkillMisconceptionIds = ( - newInapplicableSkillMisconceptionIds); + newInapplicableSkillMisconceptionIds: string[] + ): void { + this.inapplicableSkillMisconceptionIds = + newInapplicableSkillMisconceptionIds; } getInapplicableSkillMisconceptionIds(): string[] { @@ -360,5 +358,6 @@ export class StateEditorService { } } -angular.module('oppia').factory( - 'StateEditorService', downgradeInjectable(StateEditorService)); +angular + .module('oppia') + .factory('StateEditorService', downgradeInjectable(StateEditorService)); diff --git a/core/templates/components/state-editor/state-editor-properties-services/state-hints.service.spec.ts b/core/templates/components/state-editor/state-editor-properties-services/state-hints.service.spec.ts index 2cf851a08008..6fb0b43549e2 100644 --- a/core/templates/components/state-editor/state-editor-properties-services/state-hints.service.spec.ts +++ b/core/templates/components/state-editor/state-editor-properties-services/state-hints.service.spec.ts @@ -16,40 +16,42 @@ * @fileoverview Unit test for the State Hints service. */ -import { TestBed } from '@angular/core/testing'; -import { Hint } from 'domain/exploration/hint-object.model'; -import { StateHintsService } from 'components/state-editor/state-editor-properties-services/state-hints.service'; +import {TestBed} from '@angular/core/testing'; +import {Hint} from 'domain/exploration/hint-object.model'; +import {StateHintsService} from 'components/state-editor/state-editor-properties-services/state-hints.service'; describe('State hints service', () => { let shs: StateHintsService; beforeEach(() => { TestBed.configureTestingModule({ - providers: [StateHintsService] + providers: [StateHintsService], }); shs = TestBed.get(StateHintsService); }); - it('should called the constructor', () =>{ + it('should called the constructor', () => { expect(shs.displayed).toEqual([]); expect(shs.setterMethodKey).toBe('saveHints'); }); - it('should called setActiveHintIndex after init', () =>{ + it('should called setActiveHintIndex after init', () => { spyOn(shs, 'setActiveHintIndex'); const StateName = 'Introduction'; - const value = [{ - hint_content: { - html: '

math

', - content_id: 'hint_1' - } - }].map(item => Hint.createFromBackendDict(item)); + const value = [ + { + hint_content: { + html: '

math

', + content_id: 'hint_1', + }, + }, + ].map(item => Hint.createFromBackendDict(item)); shs.init(StateName, value); expect(shs.setActiveHintIndex).toHaveBeenCalled(); }); - it('should set and get activeHintIndex correctly', () =>{ + it('should set and get activeHintIndex correctly', () => { shs.setActiveHintIndex(1); expect(shs.getActiveHintIndex()).toBe(1); shs.setActiveHintIndex(2); diff --git a/core/templates/components/state-editor/state-editor-properties-services/state-hints.service.ts b/core/templates/components/state-editor/state-editor-properties-services/state-hints.service.ts index 153c0a31a92c..b044d8ba805c 100644 --- a/core/templates/components/state-editor/state-editor-properties-services/state-hints.service.ts +++ b/core/templates/components/state-editor/state-editor-properties-services/state-hints.service.ts @@ -16,16 +16,16 @@ * @fileoverview A data service that stores the current interaction hints. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { AlertsService } from 'services/alerts.service'; -import { Hint } from 'domain/exploration/hint-object.model'; -import { StatePropertyService } from 'components/state-editor/state-editor-properties-services/state-property.service'; -import { UtilsService } from 'services/utils.service'; +import {AlertsService} from 'services/alerts.service'; +import {Hint} from 'domain/exploration/hint-object.model'; +import {StatePropertyService} from 'components/state-editor/state-editor-properties-services/state-property.service'; +import {UtilsService} from 'services/utils.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class StateHintsService extends StatePropertyService { // 'activeHintIndex' is initialized with null when no hints exist. @@ -51,5 +51,6 @@ export class StateHintsService extends StatePropertyService { } } -angular.module('oppia').factory( - 'StateHintsService', downgradeInjectable(StateHintsService)); +angular + .module('oppia') + .factory('StateHintsService', downgradeInjectable(StateHintsService)); diff --git a/core/templates/components/state-editor/state-editor-properties-services/state-interaction-id.service.spec.ts b/core/templates/components/state-editor/state-editor-properties-services/state-interaction-id.service.spec.ts index c82d65e5883e..4a0c2f9b59cd 100644 --- a/core/templates/components/state-editor/state-editor-properties-services/state-interaction-id.service.spec.ts +++ b/core/templates/components/state-editor/state-editor-properties-services/state-interaction-id.service.spec.ts @@ -16,19 +16,20 @@ * @fileoverview Unit test for the state interaction id service. */ -import { EventEmitter } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; +import {EventEmitter} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; -import { StateInteractionIdService } from +import { + StateInteractionIdService, // eslint-disable-next-line max-len - 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; +} from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; describe('State Interaction Id Service', () => { let stateInteractionIdService: StateInteractionIdService; beforeEach(() => { TestBed.configureTestingModule({ - providers: [StateInteractionIdService] + providers: [StateInteractionIdService], }); stateInteractionIdService = TestBed.get(StateInteractionIdService); @@ -37,6 +38,7 @@ describe('State Interaction Id Service', () => { it('should fetch event emitter for change in interaction id', () => { let mockEventEmitter = new EventEmitter(); expect(stateInteractionIdService.onInteractionIdChanged).toEqual( - mockEventEmitter); + mockEventEmitter + ); }); }); diff --git a/core/templates/components/state-editor/state-editor-properties-services/state-interaction-id.service.ts b/core/templates/components/state-editor/state-editor-properties-services/state-interaction-id.service.ts index f588fb26754f..385628d1f3e0 100644 --- a/core/templates/components/state-editor/state-editor-properties-services/state-interaction-id.service.ts +++ b/core/templates/components/state-editor/state-editor-properties-services/state-interaction-id.service.ts @@ -15,17 +15,18 @@ /** * @fileoverview A data service that stores the current interaction id. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable, EventEmitter } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable, EventEmitter} from '@angular/core'; -import { AlertsService } from 'services/alerts.service'; -import { StatePropertyService } from +import {AlertsService} from 'services/alerts.service'; +import { + StatePropertyService, // eslint-disable-next-line max-len - 'components/state-editor/state-editor-properties-services/state-property.service'; -import { UtilsService } from 'services/utils.service'; +} from 'components/state-editor/state-editor-properties-services/state-property.service'; +import {UtilsService} from 'services/utils.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) // TODO(sll): Add validation. export class StateInteractionIdService extends StatePropertyService { @@ -41,5 +42,9 @@ export class StateInteractionIdService extends StatePropertyService { } } -angular.module('oppia').factory( - 'StateInteractionIdService', downgradeInjectable(StateInteractionIdService)); +angular + .module('oppia') + .factory( + 'StateInteractionIdService', + downgradeInjectable(StateInteractionIdService) + ); diff --git a/core/templates/components/state-editor/state-editor-properties-services/state-name.service.spec.ts b/core/templates/components/state-editor/state-editor-properties-services/state-name.service.spec.ts index 9cf62d9f934f..ed2b43083897 100644 --- a/core/templates/components/state-editor/state-editor-properties-services/state-name.service.spec.ts +++ b/core/templates/components/state-editor/state-editor-properties-services/state-name.service.spec.ts @@ -16,24 +16,22 @@ * @fileoverview Unit test for the State name service. */ -import { TestBed } from '@angular/core/testing'; - -import { StateNameService } from - 'components/state-editor/state-editor-properties-services/state-name.service'; +import {TestBed} from '@angular/core/testing'; +import {StateNameService} from 'components/state-editor/state-editor-properties-services/state-name.service'; describe('State name service', () => { let sns: StateNameService; beforeEach(() => { TestBed.configureTestingModule({ - providers: [StateNameService] + providers: [StateNameService], }); sns = TestBed.get(StateNameService); }); - it('should evaluate properties before the initialization', () =>{ + it('should evaluate properties before the initialization', () => { expect(sns.getStateNameSavedMemento()).toBeNull(); expect(sns.isStateNameEditorShown()).toBeFalse(); }); diff --git a/core/templates/components/state-editor/state-editor-properties-services/state-name.service.ts b/core/templates/components/state-editor/state-editor-properties-services/state-name.service.ts index 7a3a4954b5c7..8af1ae33dc13 100644 --- a/core/templates/components/state-editor/state-editor-properties-services/state-name.service.ts +++ b/core/templates/components/state-editor/state-editor-properties-services/state-name.service.ts @@ -15,11 +15,11 @@ /** * @fileoverview A data service that stores the current state name. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class StateNameService { private stateNameEditorIsShown: boolean = false; @@ -59,5 +59,6 @@ export class StateNameService { } } -angular.module('oppia').factory( - 'StateNameService', downgradeInjectable(StateNameService)); +angular + .module('oppia') + .factory('StateNameService', downgradeInjectable(StateNameService)); diff --git a/core/templates/components/state-editor/state-editor-properties-services/state-param-changes.service.spec.ts b/core/templates/components/state-editor/state-editor-properties-services/state-param-changes.service.spec.ts index 7a8fb7f277d0..96567c35a34d 100644 --- a/core/templates/components/state-editor/state-editor-properties-services/state-param-changes.service.spec.ts +++ b/core/templates/components/state-editor/state-editor-properties-services/state-param-changes.service.spec.ts @@ -16,22 +16,21 @@ * @fileoverview Unit test for the State Param Changes service. */ -import { TestBed } from '@angular/core/testing'; -import { StateParamChangesService } from 'components/state-editor/state-editor-properties-services/state-param-changes.service'; +import {TestBed} from '@angular/core/testing'; +import {StateParamChangesService} from 'components/state-editor/state-editor-properties-services/state-param-changes.service'; describe('State Param Changes service', () => { let spcs: StateParamChangesService; beforeEach(() => { TestBed.configureTestingModule({ - providers: [StateParamChangesService] + providers: [StateParamChangesService], }); spcs = TestBed.get(StateParamChangesService); }); - - it('should call service constructor', () =>{ + it('should call service constructor', () => { expect(spcs.setterMethodKey).toBe('saveStateParamChanges'); }); }); diff --git a/core/templates/components/state-editor/state-editor-properties-services/state-param-changes.service.ts b/core/templates/components/state-editor/state-editor-properties-services/state-param-changes.service.ts index 73274c4b487c..78f7df7e52ed 100644 --- a/core/templates/components/state-editor/state-editor-properties-services/state-param-changes.service.ts +++ b/core/templates/components/state-editor/state-editor-properties-services/state-param-changes.service.ts @@ -16,27 +16,33 @@ * @fileoverview A data service that stores the current list of * state parameter changes. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { AlertsService } from 'services/alerts.service'; -import { ParamChange } from 'domain/exploration/ParamChangeObjectFactory'; -import { StatePropertyService } from +import {AlertsService} from 'services/alerts.service'; +import {ParamChange} from 'domain/exploration/ParamChangeObjectFactory'; +import { + StatePropertyService, // eslint-disable-next-line max-len - 'components/state-editor/state-editor-properties-services/state-property.service'; -import { UtilsService } from 'services/utils.service'; +} from 'components/state-editor/state-editor-properties-services/state-property.service'; +import {UtilsService} from 'services/utils.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) // TODO(sll): Add validation. -export class StateParamChangesService extends - StatePropertyService { +export class StateParamChangesService extends StatePropertyService< + ParamChange[] +> { constructor(alertsService: AlertsService, utilsService: UtilsService) { super(alertsService, utilsService); this.setterMethodKey = 'saveStateParamChanges'; } } -angular.module('oppia').factory( - 'StateParamChangesService', downgradeInjectable(StateParamChangesService)); +angular + .module('oppia') + .factory( + 'StateParamChangesService', + downgradeInjectable(StateParamChangesService) + ); diff --git a/core/templates/components/state-editor/state-editor-properties-services/state-property.service.spec.ts b/core/templates/components/state-editor/state-editor-properties-services/state-property.service.spec.ts index 2b381bf5d629..e43dedd210e6 100644 --- a/core/templates/components/state-editor/state-editor-properties-services/state-property.service.spec.ts +++ b/core/templates/components/state-editor/state-editor-properties-services/state-property.service.spec.ts @@ -17,13 +17,12 @@ * editor page. */ -import { TestBed } from '@angular/core/testing'; -import { AlertsService } from 'services/alerts.service'; -import { UtilsService } from 'services/utils.service'; -import { StatePropertyService } from './state-property.service'; +import {TestBed} from '@angular/core/testing'; +import {AlertsService} from 'services/alerts.service'; +import {UtilsService} from 'services/utils.service'; +import {StatePropertyService} from './state-property.service'; -class MockStatePropertyService extends - StatePropertyService { +class MockStatePropertyService extends StatePropertyService { restoreFromMomento() { return; } @@ -36,7 +35,7 @@ describe('State Property Service', () => { beforeEach(() => { TestBed.configureTestingModule({ - providers: [MockStatePropertyService] + providers: [MockStatePropertyService], }); sps = TestBed.inject(MockStatePropertyService); @@ -44,7 +43,7 @@ describe('State Property Service', () => { utilsService = TestBed.inject(UtilsService); }); - it('should initialize class properties', () =>{ + it('should initialize class properties', () => { sps.setterMethodKey = 'Some setter method key'; spyOn(sps.statePropertyInitializedEmitter, 'emit'); sps.init('stateName', 'stateProperty'); diff --git a/core/templates/components/state-editor/state-editor-properties-services/state-property.service.ts b/core/templates/components/state-editor/state-editor-properties-services/state-property.service.ts index 4f13a8d9daa8..358762e4efb2 100644 --- a/core/templates/components/state-editor/state-editor-properties-services/state-property.service.ts +++ b/core/templates/components/state-editor/state-editor-properties-services/state-property.service.ts @@ -16,13 +16,13 @@ * @fileoverview Standalone services for the general state editor page. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { EventEmitter, Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {EventEmitter, Injectable} from '@angular/core'; import cloneDeep from 'lodash/cloneDeep'; -import { AlertsService } from 'services/alerts.service'; -import { UtilsService } from 'services/utils.service'; +import {AlertsService} from 'services/alerts.service'; +import {UtilsService} from 'services/utils.service'; /** * NOTE TO DEVELOPERS: This class should not be used to create objects directly. @@ -38,7 +38,7 @@ import { UtilsService } from 'services/utils.service'; * savedMomento etc. to be string. */ @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class StatePropertyService { // These properties are initialized using Angular lifecycle hooks @@ -54,7 +54,8 @@ export class StatePropertyService { constructor( private alertsService: AlertsService, - private utilsService: UtilsService) { + private utilsService: UtilsService + ) { this.setterMethodKey = null; this.statePropertyInitializedEmitter = new EventEmitter(); } @@ -119,6 +120,6 @@ export class StatePropertyService { } } -angular.module('oppia').factory( - 'StatePropertyService', downgradeInjectable( - StatePropertyService)); +angular + .module('oppia') + .factory('StatePropertyService', downgradeInjectable(StatePropertyService)); diff --git a/core/templates/components/state-editor/state-editor-properties-services/state-recorded-voiceovers.service.ts b/core/templates/components/state-editor/state-editor-properties-services/state-recorded-voiceovers.service.ts index 9b0065ff88d6..61589057d1be 100644 --- a/core/templates/components/state-editor/state-editor-properties-services/state-recorded-voiceovers.service.ts +++ b/core/templates/components/state-editor/state-editor-properties-services/state-recorded-voiceovers.service.ts @@ -16,28 +16,30 @@ * @fileoverview A data service that stores the content ids * to audio translations. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { AlertsService } from 'services/alerts.service'; -import { StatePropertyService } from +import {AlertsService} from 'services/alerts.service'; +import { + StatePropertyService, // eslint-disable-next-line max-len - 'components/state-editor/state-editor-properties-services/state-property.service'; -import { RecordedVoiceovers } from - 'domain/exploration/recorded-voiceovers.model'; -import { UtilsService } from 'services/utils.service'; +} from 'components/state-editor/state-editor-properties-services/state-property.service'; +import {RecordedVoiceovers} from 'domain/exploration/recorded-voiceovers.model'; +import {UtilsService} from 'services/utils.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) -export class StateRecordedVoiceoversService extends - StatePropertyService { +export class StateRecordedVoiceoversService extends StatePropertyService { constructor(alertsService: AlertsService, utilsService: UtilsService) { super(alertsService, utilsService); this.setterMethodKey = 'saveRecordedVoiceovers'; } } -angular.module('oppia').factory( - 'StateRecordedVoiceoversService', downgradeInjectable( - StateRecordedVoiceoversService)); +angular + .module('oppia') + .factory( + 'StateRecordedVoiceoversService', + downgradeInjectable(StateRecordedVoiceoversService) + ); diff --git a/core/templates/components/state-editor/state-editor-properties-services/state-skill.service.ts b/core/templates/components/state-editor/state-editor-properties-services/state-skill.service.ts index 94ba72e179d6..236d58d9cab9 100644 --- a/core/templates/components/state-editor/state-editor-properties-services/state-skill.service.ts +++ b/core/templates/components/state-editor/state-editor-properties-services/state-skill.service.ts @@ -16,21 +16,23 @@ * @fileoverview A data service that stores the current interaction skill. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { EventEmitter, Injectable } from '@angular/core'; -import { AlertsService } from 'services/alerts.service'; -import { StatePropertyService } from +import {downgradeInjectable} from '@angular/upgrade/static'; +import {EventEmitter, Injectable} from '@angular/core'; +import {AlertsService} from 'services/alerts.service'; +import { + StatePropertyService, // eslint-disable-next-line max-len - 'components/state-editor/state-editor-properties-services/state-property.service'; -import { UtilsService } from 'services/utils.service'; +} from 'components/state-editor/state-editor-properties-services/state-property.service'; +import {UtilsService} from 'services/utils.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) -export class StateLinkedSkillIdService - // Until a skill is selected, the state attribute is null. Also used to - // avoid circular dependencies. - extends StatePropertyService { +// Until a skill is selected, the state attribute is null. Also used to +// avoid circular dependencies. +export class StateLinkedSkillIdService extends StatePropertyService< + string | null +> { constructor(alertsService: AlertsService, utilsService: UtilsService) { super(alertsService, utilsService); this.setterMethodKey = 'saveLinkedSkillId'; @@ -41,5 +43,9 @@ export class StateLinkedSkillIdService } } -angular.module('oppia').factory( - 'StateLinkedSkillIdService', downgradeInjectable(StateLinkedSkillIdService)); +angular + .module('oppia') + .factory( + 'StateLinkedSkillIdService', + downgradeInjectable(StateLinkedSkillIdService) + ); diff --git a/core/templates/components/state-editor/state-editor-properties-services/state-solicit-answer-details.service.ts b/core/templates/components/state-editor/state-editor-properties-services/state-solicit-answer-details.service.ts index 74eea1445459..72731385a158 100644 --- a/core/templates/components/state-editor/state-editor-properties-services/state-solicit-answer-details.service.ts +++ b/core/templates/components/state-editor/state-editor-properties-services/state-solicit-answer-details.service.ts @@ -15,26 +15,29 @@ /** * @fileoverview A data service that stores the solicit answer details value. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { AlertsService } from 'services/alerts.service'; -import { StatePropertyService } from +import {AlertsService} from 'services/alerts.service'; +import { + StatePropertyService, // eslint-disable-next-line max-len - 'components/state-editor/state-editor-properties-services/state-property.service'; -import { UtilsService } from 'services/utils.service'; +} from 'components/state-editor/state-editor-properties-services/state-property.service'; +import {UtilsService} from 'services/utils.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) -export class StateSolicitAnswerDetailsService extends - StatePropertyService { +export class StateSolicitAnswerDetailsService extends StatePropertyService { constructor(alertsService: AlertsService, utilsService: UtilsService) { super(alertsService, utilsService); this.setterMethodKey = 'saveSolicitAnswerDetails'; } } -angular.module('oppia').factory( - 'StateSolicitAnswerDetailsService', downgradeInjectable( - StateSolicitAnswerDetailsService)); +angular + .module('oppia') + .factory( + 'StateSolicitAnswerDetailsService', + downgradeInjectable(StateSolicitAnswerDetailsService) + ); diff --git a/core/templates/components/state-editor/state-editor-properties-services/state-solution.service.ts b/core/templates/components/state-editor/state-editor-properties-services/state-solution.service.ts index 8a39a5f4435a..2ace437c3e6b 100644 --- a/core/templates/components/state-editor/state-editor-properties-services/state-solution.service.ts +++ b/core/templates/components/state-editor/state-editor-properties-services/state-solution.service.ts @@ -15,25 +15,25 @@ /** * @fileoverview A data service that stores the current interaction solution. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { AlertsService } from 'services/alerts.service'; -import { Solution } from 'domain/exploration/SolutionObjectFactory'; -import { StatePropertyService } from 'components/state-editor/state-editor-properties-services/state-property.service'; -import { UtilsService } from 'services/utils.service'; +import {AlertsService} from 'services/alerts.service'; +import {Solution} from 'domain/exploration/SolutionObjectFactory'; +import {StatePropertyService} from 'components/state-editor/state-editor-properties-services/state-property.service'; +import {UtilsService} from 'services/utils.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) -export class StateSolutionService - // The state property is null until a solution is specified or removed. - extends StatePropertyService { +// The state property is null until a solution is specified or removed. +export class StateSolutionService extends StatePropertyService { constructor(alertsService: AlertsService, utilsService: UtilsService) { super(alertsService, utilsService); this.setterMethodKey = 'saveSolution'; } } -angular.module('oppia').factory( - 'StateSolutionService', downgradeInjectable(StateSolutionService)); +angular + .module('oppia') + .factory('StateSolutionService', downgradeInjectable(StateSolutionService)); diff --git a/core/templates/components/state-editor/state-editor-properties-services/state-written-translations.service.ts b/core/templates/components/state-editor/state-editor-properties-services/state-written-translations.service.ts index 761f76cc298c..cca2a8c09c37 100644 --- a/core/templates/components/state-editor/state-editor-properties-services/state-written-translations.service.ts +++ b/core/templates/components/state-editor/state-editor-properties-services/state-written-translations.service.ts @@ -15,28 +15,30 @@ /** * @fileoverview A data service that stores the written translations. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { AlertsService } from 'services/alerts.service'; -import { StatePropertyService } from +import {AlertsService} from 'services/alerts.service'; +import { + StatePropertyService, // eslint-disable-next-line max-len - 'components/state-editor/state-editor-properties-services/state-property.service'; -import { UtilsService } from 'services/utils.service'; -import { WrittenTranslations } from - 'domain/exploration/WrittenTranslationsObjectFactory'; +} from 'components/state-editor/state-editor-properties-services/state-property.service'; +import {UtilsService} from 'services/utils.service'; +import {WrittenTranslations} from 'domain/exploration/WrittenTranslationsObjectFactory'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) -export class StateWrittenTranslationsService extends - StatePropertyService { +export class StateWrittenTranslationsService extends StatePropertyService { constructor(alertsService: AlertsService, utilsService: UtilsService) { super(alertsService, utilsService); this.setterMethodKey = 'saveWrittenTranslation'; } } -angular.module('oppia').factory( - 'StateWrittenTranslationsService', downgradeInjectable( - StateWrittenTranslationsService)); +angular + .module('oppia') + .factory( + 'StateWrittenTranslationsService', + downgradeInjectable(StateWrittenTranslationsService) + ); diff --git a/core/templates/components/state-editor/state-editor.component.spec.ts b/core/templates/components/state-editor/state-editor.component.spec.ts index b5503ac12efa..732b6b7a97e9 100644 --- a/core/templates/components/state-editor/state-editor.component.spec.ts +++ b/core/templates/components/state-editor/state-editor.component.spec.ts @@ -16,18 +16,24 @@ * @fileoverview Unit test for State Editor Component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { Outcome } from 'domain/exploration/OutcomeObjectFactory'; -import { Solution } from 'domain/exploration/SolutionObjectFactory'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { State } from 'domain/state/StateObjectFactory'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { ExplorationHtmlFormatterService } from 'services/exploration-html-formatter.service'; -import { StateEditorService } from './state-editor-properties-services/state-editor.service'; -import { StateInteractionIdService } from './state-editor-properties-services/state-interaction-id.service'; -import { StateEditorComponent } from './state-editor.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {Outcome} from 'domain/exploration/OutcomeObjectFactory'; +import {Solution} from 'domain/exploration/SolutionObjectFactory'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {State} from 'domain/state/StateObjectFactory'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {ExplorationHtmlFormatterService} from 'services/exploration-html-formatter.service'; +import {StateEditorService} from './state-editor-properties-services/state-editor.service'; +import {StateInteractionIdService} from './state-editor-properties-services/state-interaction-id.service'; +import {StateEditorComponent} from './state-editor.component'; describe('State Editor Component', () => { let component: StateEditorComponent; @@ -40,15 +46,13 @@ describe('State Editor Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - StateEditorComponent - ], + declarations: [StateEditorComponent], providers: [ WindowDimensionsService, StateEditorService, StateInteractionIdService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -59,11 +63,13 @@ describe('State Editor Component', () => { windowDimensionsService = TestBed.inject(WindowDimensionsService); stateEditorService = TestBed.inject(StateEditorService); stateInteractionIdService = TestBed.inject(StateInteractionIdService); - explorationHtmlFormatterService = - TestBed.inject(ExplorationHtmlFormatterService); + explorationHtmlFormatterService = TestBed.inject( + ExplorationHtmlFormatterService + ); spyOn(stateEditorService, 'getActiveStateName').and.returnValue( - 'Introduction'); + 'Introduction' + ); }); afterEach(() => { @@ -90,32 +96,37 @@ describe('State Editor Component', () => { component.sendOnSaveSolicitAnswerDetails(false); component.sendOnSaveHints([]); component.sendRefreshWarnings(); - component.sendOnSaveInteractionDefaultOutcome(new Outcome( - 'Hola', - null, - new SubtitledHtml('

Previous HTML string

', 'Id'), - true, - [], - null, - null, - )); + component.sendOnSaveInteractionDefaultOutcome( + new Outcome( + 'Hola', + null, + new SubtitledHtml('

Previous HTML string

', 'Id'), + true, + [], + null, + null + ) + ); component.sendOnSaveInteractionAnswerGroups([]); component.sendOnSaveInapplicableSkillMisconceptionIds([]); component.sendNavigateToState(''); - component.sendOnSaveSolution(new Solution( - explorationHtmlFormatterService, - true, - [], - new SubtitledHtml('

Previous HTML string

', 'Id'), - )); + component.sendOnSaveSolution( + new Solution( + explorationHtmlFormatterService, + true, + [], + new SubtitledHtml('

Previous HTML string

', 'Id') + ) + ); component.sendOnSaveNextContentIdIndex(0); let interactionData = { interactionId: null, - customizationArgs: {} + customizationArgs: {}, }; component.sendOnSaveInteractionData(interactionData); component.sendOnSaveStateContent( - new SubtitledHtml('

Previous HTML string

', 'Id')); + new SubtitledHtml('

Previous HTML string

', 'Id') + ); expect(component.recomputeGraph.emit).toHaveBeenCalled(); expect(component.onSaveLinkedSkillId.emit).toHaveBeenCalled(); @@ -124,8 +135,9 @@ describe('State Editor Component', () => { expect(component.refreshWarnings.emit).toHaveBeenCalled(); expect(component.onSaveInteractionDefaultOutcome.emit).toHaveBeenCalled(); expect(component.onSaveInteractionAnswerGroups.emit).toHaveBeenCalled(); - expect(component.onSaveInapplicableSkillMisconceptionIds.emit) - .toHaveBeenCalled(); + expect( + component.onSaveInapplicableSkillMisconceptionIds.emit + ).toHaveBeenCalled(); expect(component.navigateToState.emit).toHaveBeenCalled(); expect(component.onSaveSolution.emit).toHaveBeenCalled(); expect(component.onSaveNextContentIdIndex.emit).toHaveBeenCalled(); @@ -145,8 +157,9 @@ describe('State Editor Component', () => { component.ngOnInit(); - expect(component.oppiaBlackImgUrl) - .toBe('/assets/images/avatar/oppia_avatar_100px.svg'); + expect(component.oppiaBlackImgUrl).toBe( + '/assets/images/avatar/oppia_avatar_100px.svg' + ); expect(component.currentStateIsTerminal).toBe(false); expect(component.conceptCardIsShown).toBe(true); expect(component.windowIsNarrow).toBe(false); @@ -156,8 +169,10 @@ describe('State Editor Component', () => { it('should update interaction visibility when interaction is changed', () => { let onInteractionIdChangedEmitter = new EventEmitter(); - spyOnProperty(stateInteractionIdService, 'onInteractionIdChanged') - .and.returnValue(onInteractionIdChangedEmitter); + spyOnProperty( + stateInteractionIdService, + 'onInteractionIdChanged' + ).and.returnValue(onInteractionIdChangedEmitter); expect(component.interactionIdIsSet).toBeFalse(); expect(component.currentInteractionCanHaveSolution).toBeFalse(); @@ -192,7 +207,7 @@ describe('State Editor Component', () => { voiceovers_mapping: { content: {}, default_outcome: {}, - } + }, }, interaction: { id: null, @@ -202,19 +217,21 @@ describe('State Editor Component', () => { dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: '' + html: '', }, labelled_as_correct: false, param_changes: [], - refresher_exploration_id: null + refresher_exploration_id: null, }, - hints: [] + hints: [], }, param_changes: [], - solicit_answer_details: false + solicit_answer_details: false, }; - spyOnProperty(stateEditorService, 'onStateEditorInitialized') - .and.returnValue(onStateEditorInitializedEmitter); + spyOnProperty( + stateEditorService, + 'onStateEditorInitialized' + ).and.returnValue(onStateEditorInitializedEmitter); component.ngOnInit(); @@ -225,20 +242,25 @@ describe('State Editor Component', () => { expect(component.servicesInitialized).toBe(true); }); - it('should throw error if state data is not defined and' + - ' component is reinitialized', fakeAsync(() => { - let onStateEditorInitializedEmitter = new EventEmitter(); - let stateData: State | null = null; - spyOnProperty(stateEditorService, 'onStateEditorInitialized') - .and.returnValue(onStateEditorInitializedEmitter); - - component.ngOnInit(); - - expect(() => { - onStateEditorInitializedEmitter.emit(stateData as State); - tick(); - }).toThrowError('Expected stateData to be defined but received null'); - })); + it( + 'should throw error if state data is not defined and' + + ' component is reinitialized', + fakeAsync(() => { + let onStateEditorInitializedEmitter = new EventEmitter(); + let stateData: State | null = null; + spyOnProperty( + stateEditorService, + 'onStateEditorInitialized' + ).and.returnValue(onStateEditorInitializedEmitter); + + component.ngOnInit(); + + expect(() => { + onStateEditorInitializedEmitter.emit(stateData as State); + tick(); + }).toThrowError('Expected stateData to be defined but received null'); + }) + ); it('should reinitialize editor when responses change', () => { spyOn(stateEditorService.onStateEditorInitialized, 'emit').and.stub(); diff --git a/core/templates/components/state-editor/state-editor.component.ts b/core/templates/components/state-editor/state-editor.component.ts index 9452de68e787..eb2907956582 100644 --- a/core/templates/components/state-editor/state-editor.component.ts +++ b/core/templates/components/state-editor/state-editor.component.ts @@ -16,46 +16,52 @@ * @fileoverview Component for the state editor Component. */ -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { State } from 'domain/state/StateObjectFactory'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { Subscription } from 'rxjs'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { StateCardIsCheckpointService } from './state-editor-properties-services/state-card-is-checkpoint.service'; -import { StateContentService } from './state-editor-properties-services/state-content.service'; -import { StateCustomizationArgsService } from './state-editor-properties-services/state-customization-args.service'; -import { StateEditorService } from './state-editor-properties-services/state-editor.service'; -import { StateHintsService } from './state-editor-properties-services/state-hints.service'; -import { StateInteractionIdService } from './state-editor-properties-services/state-interaction-id.service'; -import { StateNameService } from './state-editor-properties-services/state-name.service'; -import { StateParamChangesService } from './state-editor-properties-services/state-param-changes.service'; -import { StateLinkedSkillIdService } from './state-editor-properties-services/state-skill.service'; -import { StateSolicitAnswerDetailsService } from './state-editor-properties-services/state-solicit-answer-details.service'; -import { StateSolutionService } from './state-editor-properties-services/state-solution.service'; +import { + Component, + EventEmitter, + Input, + OnDestroy, + OnInit, + Output, +} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {State} from 'domain/state/StateObjectFactory'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {Subscription} from 'rxjs'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {StateCardIsCheckpointService} from './state-editor-properties-services/state-card-is-checkpoint.service'; +import {StateContentService} from './state-editor-properties-services/state-content.service'; +import {StateCustomizationArgsService} from './state-editor-properties-services/state-customization-args.service'; +import {StateEditorService} from './state-editor-properties-services/state-editor.service'; +import {StateHintsService} from './state-editor-properties-services/state-hints.service'; +import {StateInteractionIdService} from './state-editor-properties-services/state-interaction-id.service'; +import {StateNameService} from './state-editor-properties-services/state-name.service'; +import {StateParamChangesService} from './state-editor-properties-services/state-param-changes.service'; +import {StateLinkedSkillIdService} from './state-editor-properties-services/state-skill.service'; +import {StateSolicitAnswerDetailsService} from './state-editor-properties-services/state-solicit-answer-details.service'; +import {StateSolutionService} from './state-editor-properties-services/state-solution.service'; import INTERACTION_SPECS from 'interactions/interaction_specs.json'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { Solution } from 'domain/exploration/SolutionObjectFactory'; -import { Outcome } from 'domain/exploration/OutcomeObjectFactory'; -import { InteractionData } from 'interactions/customization-args-defs'; -import { Hint } from 'domain/exploration/hint-object.model'; -import { InteractionSpecsKey } from 'pages/interaction-specs.constants'; -import { AnswerGroup } from 'domain/exploration/AnswerGroupObjectFactory'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {Solution} from 'domain/exploration/SolutionObjectFactory'; +import {Outcome} from 'domain/exploration/OutcomeObjectFactory'; +import {InteractionData} from 'interactions/customization-args-defs'; +import {Hint} from 'domain/exploration/hint-object.model'; +import {InteractionSpecsKey} from 'pages/interaction-specs.constants'; +import {AnswerGroup} from 'domain/exploration/AnswerGroupObjectFactory'; @Component({ selector: 'oppia-state-editor', - templateUrl: './state-editor.component.html' + templateUrl: './state-editor.component.html', }) export class StateEditorComponent implements OnInit, OnDestroy { @Output() onSaveHints = new EventEmitter(); - @Output() onSaveInapplicableSkillMisconceptionIds = ( - new EventEmitter()); + @Output() onSaveInapplicableSkillMisconceptionIds = new EventEmitter< + string[] + >(); - @Output() onSaveInteractionAnswerGroups = ( - new EventEmitter()); + @Output() onSaveInteractionAnswerGroups = new EventEmitter(); - @Output() onSaveInteractionData = ( - new EventEmitter()); + @Output() onSaveInteractionData = new EventEmitter(); @Output() onSaveInteractionDefaultOutcome = new EventEmitter(); @Output() onSaveLinkedSkillId = new EventEmitter(); @@ -76,7 +82,6 @@ export class StateEditorComponent implements OnInit, OnDestroy { @Input() stateContentSaveButtonPlaceholder!: string; @Input() stateContentPlaceholder!: string; - oppiaBlackImgUrl!: string; // State name is null if their is no state selected or have no active state. // This is the case when the user is creating a new state. @@ -103,8 +108,8 @@ export class StateEditorComponent implements OnInit, OnDestroy { private stateSolicitAnswerDetailsService: StateSolicitAnswerDetailsService, private stateSolutionService: StateSolutionService, private urlInterpolationService: UrlInterpolationService, - private windowDimensionsService: WindowDimensionsService, - ) { } + private windowDimensionsService: WindowDimensionsService + ) {} sendRecomputeGraph(): void { this.recomputeGraph.emit(); @@ -162,11 +167,13 @@ export class StateEditorComponent implements OnInit, OnDestroy { this.interactionIdIsSet = Boolean(newInteractionId); this.currentInteractionCanHaveSolution = Boolean( this.interactionIdIsSet && - INTERACTION_SPECS[ - newInteractionId as InteractionSpecsKey].can_have_solution); + INTERACTION_SPECS[newInteractionId as InteractionSpecsKey] + .can_have_solution + ); this.currentStateIsTerminal = Boolean( - this.interactionIdIsSet && INTERACTION_SPECS[ - newInteractionId as InteractionSpecsKey].is_terminal); + this.interactionIdIsSet && + INTERACTION_SPECS[newInteractionId as InteractionSpecsKey].is_terminal + ); } reinitializeEditor(): void { @@ -179,7 +186,8 @@ export class StateEditorComponent implements OnInit, OnDestroy { ngOnInit(): void { this.oppiaBlackImgUrl = this.urlInterpolationService.getStaticImageUrl( - '/avatar/oppia_avatar_100px.svg'); + '/avatar/oppia_avatar_100px.svg' + ); this.currentStateIsTerminal = false; this.windowIsNarrow = this.windowDimensionsService.isWindowNarrow(); this.interactionIdIsSet = false; @@ -187,46 +195,59 @@ export class StateEditorComponent implements OnInit, OnDestroy { this.stateName = this.stateEditorService.getActiveStateName(); this.directiveSubscriptions.add( this.stateInteractionIdService.onInteractionIdChanged.subscribe( - (newInteractionId) => { + newInteractionId => { this.updateInteractionVisibility(newInteractionId); } ) ); this.directiveSubscriptions.add( - this.stateEditorService.onStateEditorInitialized.subscribe( - (stateData) => { - if (stateData === undefined || $.isEmptyObject(stateData)) { - throw new Error( - 'Expected stateData to be defined but ' + - 'received ' + stateData); - } - this.stateData = stateData; - this.stateName = this.stateEditorService.getActiveStateName(); - this.stateEditorService.setInteraction(stateData.interaction); - this.stateContentService.init( - this.stateName, stateData.content); - this.stateLinkedSkillIdService.init( - this.stateName, stateData.linkedSkillId); - this.stateHintsService.init( - this.stateName, stateData.interaction.hints); - this.stateInteractionIdService.init( - this.stateName, stateData.interaction.id); - this.stateCustomizationArgsService.init( - this.stateName, stateData.interaction.customizationArgs); - this.stateNameService.init(); - this.stateParamChangesService.init( - this.stateName, stateData.paramChanges); - this.stateSolicitAnswerDetailsService.init( - this.stateName, stateData.solicitAnswerDetails); - this.stateCardIsCheckpointService.init( - this.stateName, stateData.cardIsCheckpoint); - this.stateSolutionService.init( - this.stateName, stateData.interaction.solution); - this.updateInteractionVisibility(stateData.interaction.id); - this.servicesInitialized = true; + this.stateEditorService.onStateEditorInitialized.subscribe(stateData => { + if (stateData === undefined || $.isEmptyObject(stateData)) { + throw new Error( + 'Expected stateData to be defined but ' + 'received ' + stateData + ); } - ) + this.stateData = stateData; + this.stateName = this.stateEditorService.getActiveStateName(); + this.stateEditorService.setInteraction(stateData.interaction); + this.stateContentService.init(this.stateName, stateData.content); + this.stateLinkedSkillIdService.init( + this.stateName, + stateData.linkedSkillId + ); + this.stateHintsService.init( + this.stateName, + stateData.interaction.hints + ); + this.stateInteractionIdService.init( + this.stateName, + stateData.interaction.id + ); + this.stateCustomizationArgsService.init( + this.stateName, + stateData.interaction.customizationArgs + ); + this.stateNameService.init(); + this.stateParamChangesService.init( + this.stateName, + stateData.paramChanges + ); + this.stateSolicitAnswerDetailsService.init( + this.stateName, + stateData.solicitAnswerDetails + ); + this.stateCardIsCheckpointService.init( + this.stateName, + stateData.cardIsCheckpoint + ); + this.stateSolutionService.init( + this.stateName, + stateData.interaction.solution + ); + this.updateInteractionVisibility(stateData.interaction.id); + this.servicesInitialized = true; + }) ); this.stateEditorService.onStateEditorDirectiveInitialized.emit(); this.stateEditorService.updateStateEditorDirectiveInitialised(); @@ -237,7 +258,9 @@ export class StateEditorComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive('oppiaStateEditor', +angular.module('oppia').directive( + 'oppiaStateEditor', downgradeComponent({ - component: StateEditorComponent - }) as angular.IDirectiveFactory); + component: StateEditorComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/components/state-editor/state-editor.constants.ajs.ts b/core/templates/components/state-editor/state-editor.constants.ajs.ts index 675b0686c474..5708c92b070d 100644 --- a/core/templates/components/state-editor/state-editor.constants.ajs.ts +++ b/core/templates/components/state-editor/state-editor.constants.ajs.ts @@ -18,9 +18,11 @@ // TODO(#7092): Delete this file once migration is complete and these AngularJS // equivalents of the Angular constants are no longer needed. -import { StateEditorConstants } from - 'components/state-editor/state-editor.constants'; +import {StateEditorConstants} from 'components/state-editor/state-editor.constants'; -angular.module('oppia').constant( - 'INFO_MESSAGE_SOLUTION_IS_INVALID_FOR_QUESTION', - StateEditorConstants.INFO_MESSAGE_SOLUTION_IS_INVALID_FOR_QUESTION); +angular + .module('oppia') + .constant( + 'INFO_MESSAGE_SOLUTION_IS_INVALID_FOR_QUESTION', + StateEditorConstants.INFO_MESSAGE_SOLUTION_IS_INVALID_FOR_QUESTION + ); diff --git a/core/templates/components/state-editor/state-editor.constants.ts b/core/templates/components/state-editor/state-editor.constants.ts index de1e9cc2f502..3603fd665f22 100644 --- a/core/templates/components/state-editor/state-editor.constants.ts +++ b/core/templates/components/state-editor/state-editor.constants.ts @@ -18,5 +18,5 @@ export const StateEditorConstants = { INFO_MESSAGE_SOLUTION_IS_INVALID_FOR_QUESTION: - 'The current solution does not correspond to a correct answer.' + 'The current solution does not correspond to a correct answer.', } as const; diff --git a/core/templates/components/state-editor/state-hints-editor/state-hints-editor.component.spec.ts b/core/templates/components/state-editor/state-hints-editor/state-hints-editor.component.spec.ts index 5c5027b175a5..1f34b14c4cf4 100644 --- a/core/templates/components/state-editor/state-hints-editor/state-hints-editor.component.spec.ts +++ b/core/templates/components/state-editor/state-hints-editor/state-hints-editor.component.spec.ts @@ -16,22 +16,22 @@ * @fileoverview Unit test for State Hints Editor Component. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { StateHintsEditorComponent } from './state-hints-editor.component'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { fakeAsync, tick } from '@angular/core/testing'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { EditabilityService } from 'services/editability.service'; -import { StateHintsService } from '../state-editor-properties-services/state-hints.service'; -import { ExternalSaveService } from 'services/external-save.service'; -import { StateInteractionIdService } from '../state-editor-properties-services/state-interaction-id.service'; -import { StateSolutionService } from '../state-editor-properties-services/state-solution.service'; -import { AlertsService } from 'services/alerts.service'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { Hint, HintBackendDict } from 'domain/exploration/hint-object.model'; -import { SolutionObjectFactory } from 'domain/exploration/SolutionObjectFactory'; -import { CdkDragSortEvent } from '@angular/cdk/drag-drop'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {StateHintsEditorComponent} from './state-hints-editor.component'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {fakeAsync, tick} from '@angular/core/testing'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {EditabilityService} from 'services/editability.service'; +import {StateHintsService} from '../state-editor-properties-services/state-hints.service'; +import {ExternalSaveService} from 'services/external-save.service'; +import {StateInteractionIdService} from '../state-editor-properties-services/state-interaction-id.service'; +import {StateSolutionService} from '../state-editor-properties-services/state-solution.service'; +import {AlertsService} from 'services/alerts.service'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {Hint, HintBackendDict} from 'domain/exploration/hint-object.model'; +import {SolutionObjectFactory} from 'domain/exploration/SolutionObjectFactory'; +import {CdkDragSortEvent} from '@angular/cdk/drag-drop'; class MockStateHintsService { displayed = [ @@ -39,58 +39,56 @@ class MockStateHintsService { hintContent: SubtitledHtml.createDefault('

work

', '1'), toBackendDict(): HintBackendDict { return { - hint_content: this.hintContent.toBackendDict() + hint_content: this.hintContent.toBackendDict(), }; - } + }, }, { hintContent: SubtitledHtml.createDefault('

work

', '1'), toBackendDict(): HintBackendDict { return { - hint_content: this.hintContent.toBackendDict() + hint_content: this.hintContent.toBackendDict(), }; - } + }, }, { hintContent: SubtitledHtml.createDefault('

work

', '1'), toBackendDict(): HintBackendDict { return { - hint_content: this.hintContent.toBackendDict() + hint_content: this.hintContent.toBackendDict(), }; - } + }, }, { hintContent: SubtitledHtml.createDefault('

work

', '1'), toBackendDict(): HintBackendDict { return { - hint_content: this.hintContent.toBackendDict() + hint_content: this.hintContent.toBackendDict(), }; - } - } + }, + }, ]; getActiveHintIndex(): number { return 1; } - saveDisplayedValue(): void { - } + saveDisplayedValue(): void {} savedMemento = [ { hintContent: { - html: null - } + html: null, + }, }, { hintContent: { - html: '

Hint

' - } - } + html: '

Hint

', + }, + }, ]; - setActiveHintIndex(): void { - } + setActiveHintIndex(): void {} } describe('StateHintsEditorComponent', () => { @@ -108,15 +106,13 @@ describe('StateHintsEditorComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - StateHintsEditorComponent - ], + declarations: [StateHintsEditorComponent], providers: [ WindowDimensionsService, EditabilityService, { provide: StateHintsService, - useClass: MockStateHintsService + useClass: MockStateHintsService, }, NgbModal, ExternalSaveService, @@ -125,7 +121,7 @@ describe('StateHintsEditorComponent', () => { AlertsService, SolutionObjectFactory, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -143,15 +139,18 @@ describe('StateHintsEditorComponent', () => { solutionObjectFactory = TestBed.inject(SolutionObjectFactory); stateSolutionService.savedMemento = solutionObjectFactory.createNew( - true, 'correct_answer', '

Hint Index 0

', '0' + true, + 'correct_answer', + '

Hint Index 0

', + '0' ); spyOn(windowDimensionsService, 'isWindowNarrow').and.returnValue(true); spyOn(editabilityService, 'isEditable').and.returnValue(true); ngbModalSpy = spyOn(ngbModal, 'open').and.callFake(() => { - return ({ - result: Promise.resolve() - }) as NgbModalRef; + return { + result: Promise.resolve(), + } as NgbModalRef; }); fixture.detectChanges(); @@ -175,8 +174,9 @@ describe('StateHintsEditorComponent', () => { it('should set component properties on initialization', () => { expect(component.hintCardIsShown).toBe(true); expect(component.canEdit).toBe(true); - expect(component.getStaticImageUrl('/demo/img')) - .toBe('/assets/images/demo/img'); + expect(component.getStaticImageUrl('/demo/img')).toBe( + '/assets/images/demo/img' + ); }); it('should toggle hint card when user clicks on hint header', () => { @@ -215,7 +215,7 @@ describe('StateHintsEditorComponent', () => { new Hint(SubtitledHtml.createDefault('

work

', '1')), new Hint(SubtitledHtml.createDefault('

work

', '1')), new Hint(SubtitledHtml.createDefault('

work

', '1')), - new Hint(SubtitledHtml.createDefault('

work

', '1')) + new Hint(SubtitledHtml.createDefault('

work

', '1')), ]; expect(component.getHintButtonText()).toBe('Limit Reached'); @@ -227,36 +227,40 @@ describe('StateHintsEditorComponent', () => { expect(component.getHintSummary(hint)).toBe('Hint'); }); - it('should open delete last hint modal if only one hint exists while' + - ' changing active hint index', fakeAsync(() => { - spyOn(stateHintsService, 'getActiveHintIndex').and.returnValue(0); - stateHintsService.displayed = [ - new Hint(SubtitledHtml.createDefault('', '1'))]; - - component.changeActiveHintIndex(0); - tick(); - - expect(stateSolutionService.displayed).toBe(null); - expect(stateHintsService.displayed).toEqual([]); - })); - - it('should delete empty hint when changing active hint index', + it( + 'should open delete last hint modal if only one hint exists while' + + ' changing active hint index', fakeAsync(() => { spyOn(stateHintsService, 'getActiveHintIndex').and.returnValue(0); - spyOn(alertsService, 'addInfoMessage'); stateHintsService.displayed = [ new Hint(SubtitledHtml.createDefault('', '1')), - new Hint(SubtitledHtml.createDefault('', '1')) ]; - stateSolutionService.savedMemento = null; component.changeActiveHintIndex(0); tick(); - expect(alertsService.addInfoMessage) - .toHaveBeenCalledWith('Deleting empty hint.'); - expect(stateHintsService.displayed.length).toEqual(1); - })); + expect(stateSolutionService.displayed).toBe(null); + expect(stateHintsService.displayed).toEqual([]); + }) + ); + + it('should delete empty hint when changing active hint index', fakeAsync(() => { + spyOn(stateHintsService, 'getActiveHintIndex').and.returnValue(0); + spyOn(alertsService, 'addInfoMessage'); + stateHintsService.displayed = [ + new Hint(SubtitledHtml.createDefault('', '1')), + new Hint(SubtitledHtml.createDefault('', '1')), + ]; + stateSolutionService.savedMemento = null; + + component.changeActiveHintIndex(0); + tick(); + + expect(alertsService.addInfoMessage).toHaveBeenCalledWith( + 'Deleting empty hint.' + ); + expect(stateHintsService.displayed.length).toEqual(1); + })); it('should set new hint index if no hint is opened', () => { spyOn(stateHintsService, 'getActiveHintIndex').and.returnValue(null); @@ -267,52 +271,54 @@ describe('StateHintsEditorComponent', () => { expect(stateHintsService.setActiveHintIndex).toHaveBeenCalledWith(0); }); - it('should not open add hints modal if number of hint is greater than' + - ' or equal to 5', () => { + it( + 'should not open add hints modal if number of hint is greater than' + + ' or equal to 5', + () => { + stateHintsService.displayed = [ + new Hint(SubtitledHtml.createDefault('

work

', '1')), + new Hint(SubtitledHtml.createDefault('

work

', '1')), + new Hint(SubtitledHtml.createDefault('

work

', '1')), + new Hint(SubtitledHtml.createDefault('

work

', '1')), + new Hint(SubtitledHtml.createDefault('

work

', '1')), + ]; + + component.openAddHintModal(); + + expect(ngbModal.open).not.toHaveBeenCalled(); + } + ); + + it('should open add hints modal when user clicks on add hint button', fakeAsync(() => { stateHintsService.displayed = [ new Hint(SubtitledHtml.createDefault('

work

', '1')), - new Hint(SubtitledHtml.createDefault('

work

', '1')), - new Hint(SubtitledHtml.createDefault('

work

', '1')), - new Hint(SubtitledHtml.createDefault('

work

', '1')), - new Hint(SubtitledHtml.createDefault('

work

', '1')) ]; + ngbModalSpy.and.returnValue({ + result: Promise.resolve({ + hint: { + hintContent: SubtitledHtml.createDefault('

work

', '1'), + toBackendDict(): HintBackendDict { + return { + hint_content: this.hintContent.toBackendDict(), + }; + }, + }, + }), + } as NgbModalRef); component.openAddHintModal(); + tick(); - expect(ngbModal.open).not.toHaveBeenCalled(); - }); - - it('should open add hints modal when user clicks on add hint button', - fakeAsync(() => { - stateHintsService.displayed = [ - new Hint(SubtitledHtml.createDefault('

work

', '1')) - ]; - ngbModalSpy.and.returnValue({ - result: Promise.resolve({ - hint: { - hintContent: SubtitledHtml.createDefault('

work

', '1'), - toBackendDict(): HintBackendDict { - return { - hint_content: this.hintContent.toBackendDict() - }; - } - } - }) - } as NgbModalRef); - - component.openAddHintModal(); - tick(); - - expect(stateHintsService.displayed.length).toEqual(2); - })); + expect(stateHintsService.displayed.length).toEqual(2); + })); it('should close add hint modal when user clicks cancel', () => { stateHintsService.displayed = [ - new Hint(SubtitledHtml.createDefault('

work

', '1')) + new Hint(SubtitledHtml.createDefault('

work

', '1')), ]; ngbModalSpy.and.returnValue({ - result: Promise.reject() + result: Promise.reject(), } as NgbModalRef); component.openAddHintModal(); @@ -320,57 +326,59 @@ describe('StateHintsEditorComponent', () => { expect(ngbModal.open).toHaveBeenCalled(); }); - it('should open delete hint modal when user clicks on' + - ' delete hint button', fakeAsync(() => { - spyOn(stateHintsService, 'getActiveHintIndex').and.returnValue(0); - stateHintsService.displayed = [ - new Hint(SubtitledHtml.createDefault('

work

', '1')) - ]; - stateHintsService.savedMemento = stateHintsService.displayed; + it( + 'should open delete hint modal when user clicks on' + ' delete hint button', + fakeAsync(() => { + spyOn(stateHintsService, 'getActiveHintIndex').and.returnValue(0); + stateHintsService.displayed = [ + new Hint(SubtitledHtml.createDefault('

work

', '1')), + ]; + stateHintsService.savedMemento = stateHintsService.displayed; - const value = { - index: 0, - evt: new Event('') - }; + const value = { + index: 0, + evt: new Event(''), + }; - component.deleteHint(value); - tick(); + component.deleteHint(value); + tick(); - expect(ngbModal.open).toHaveBeenCalled(); - expect(stateHintsService.displayed).toEqual([]); - })); + expect(ngbModal.open).toHaveBeenCalled(); + expect(stateHintsService.displayed).toEqual([]); + }) + ); - it('should delete hint when user clicks on' + - ' delete hint button', fakeAsync(() => { - spyOn(stateHintsService, 'getActiveHintIndex').and.returnValue(0); - stateHintsService.displayed = [ - new Hint(SubtitledHtml.createDefault('

work

', '0')), - new Hint(SubtitledHtml.createDefault('

work

', '1')) - ]; - stateHintsService.savedMemento = stateHintsService.displayed; + it( + 'should delete hint when user clicks on' + ' delete hint button', + fakeAsync(() => { + spyOn(stateHintsService, 'getActiveHintIndex').and.returnValue(0); + stateHintsService.displayed = [ + new Hint(SubtitledHtml.createDefault('

work

', '0')), + new Hint(SubtitledHtml.createDefault('

work

', '1')), + ]; + stateHintsService.savedMemento = stateHintsService.displayed; - const value = { - index: 0, - evt: new Event('') - }; + const value = { + index: 0, + evt: new Event(''), + }; - component.deleteHint(value); - tick(); + component.deleteHint(value); + tick(); - expect(stateHintsService.displayed.length).toEqual(1); - })); + expect(stateHintsService.displayed.length).toEqual(1); + }) + ); it('should close delete hint modal when user clicks on cancel', () => { spyOn(alertsService, 'clearWarnings').and.callThrough(); - ngbModalSpy.and.returnValue( - { - result: Promise.reject() - } as NgbModalRef - ); + ngbModalSpy.and.returnValue({ + result: Promise.reject(), + } as NgbModalRef); const value = { index: 0, - evt: new Event('') + evt: new Event(''), }; component.deleteHint(value); @@ -380,11 +388,9 @@ describe('StateHintsEditorComponent', () => { it('should close delete last hint modal when user clicks on cancel', () => { spyOn(alertsService, 'clearWarnings').and.callThrough(); - ngbModalSpy.and.returnValue( - { - result: Promise.reject() - } as NgbModalRef - ); + ngbModalSpy.and.returnValue({ + result: Promise.reject(), + } as NgbModalRef); component.openDeleteLastHintModal(); diff --git a/core/templates/components/state-editor/state-hints-editor/state-hints-editor.component.ts b/core/templates/components/state-editor/state-hints-editor/state-hints-editor.component.ts index 56e3cd5ffea5..468a9f7bb061 100644 --- a/core/templates/components/state-editor/state-hints-editor/state-hints-editor.component.ts +++ b/core/templates/components/state-editor/state-hints-editor/state-hints-editor.component.ts @@ -17,26 +17,26 @@ * editor. */ -import { Component, EventEmitter, OnInit, Output } from '@angular/core'; -import { CdkDragSortEvent, moveItemInArray} from '@angular/cdk/drag-drop'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { Hint } from 'domain/exploration/hint-object.model'; +import {Component, EventEmitter, OnInit, Output} from '@angular/core'; +import {CdkDragSortEvent, moveItemInArray} from '@angular/cdk/drag-drop'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {Hint} from 'domain/exploration/hint-object.model'; import INTERACTION_SPECS from 'interactions/interaction_specs.json'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { StateHintsService } from 'components/state-editor/state-editor-properties-services/state-hints.service'; -import { StateInteractionIdService } from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; -import { StateSolutionService } from 'components/state-editor/state-editor-properties-services/state-solution.service'; -import { FormatRtePreviewPipe } from 'filters/format-rte-preview.pipe'; -import { AlertsService } from 'services/alerts.service'; -import { EditabilityService } from 'services/editability.service'; -import { ExternalSaveService } from 'services/external-save.service'; -import { AddHintModalComponent } from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/add-hint-modal.component'; -import { DeleteHintModalComponent } from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-hint-modal.component'; -import { DeleteLastHintModalComponent } from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-last-hint-modal.component'; -import { Solution } from 'domain/exploration/SolutionObjectFactory'; -import { InteractionSpecsKey } from 'pages/interaction-specs.constants'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {StateHintsService} from 'components/state-editor/state-editor-properties-services/state-hints.service'; +import {StateInteractionIdService} from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; +import {StateSolutionService} from 'components/state-editor/state-editor-properties-services/state-solution.service'; +import {FormatRtePreviewPipe} from 'filters/format-rte-preview.pipe'; +import {AlertsService} from 'services/alerts.service'; +import {EditabilityService} from 'services/editability.service'; +import {ExternalSaveService} from 'services/external-save.service'; +import {AddHintModalComponent} from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/add-hint-modal.component'; +import {DeleteHintModalComponent} from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-hint-modal.component'; +import {DeleteLastHintModalComponent} from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-last-hint-modal.component'; +import {Solution} from 'domain/exploration/SolutionObjectFactory'; +import {InteractionSpecsKey} from 'pages/interaction-specs.constants'; interface DeleteValueResponse { index: number; @@ -49,7 +49,7 @@ interface AddHintModalResponse { @Component({ selector: 'oppia-state-hints-editor', - templateUrl: './state-hints-editor.component.html' + templateUrl: './state-hints-editor.component.html', }) export class StateHintsEditorComponent implements OnInit { @Output() onSaveNextContentIdIndex = new EventEmitter(); @@ -70,13 +70,15 @@ export class StateHintsEditorComponent implements OnInit { private stateHintsService: StateHintsService, private stateInteractionIdService: StateInteractionIdService, private stateSolutionService: StateSolutionService, - private urlInterpolationService: UrlInterpolationService, + private urlInterpolationService: UrlInterpolationService ) {} drop(event: CdkDragSortEvent): void { moveItemInArray( - this.stateHintsService.displayed, event.previousIndex, - event.currentIndex); + this.stateHintsService.displayed, + event.previousIndex, + event.currentIndex + ); this.stateHintsService.saveDisplayedValue(); this.onSaveHints.emit(this.stateHintsService.displayed); } @@ -97,11 +99,14 @@ export class StateHintsEditorComponent implements OnInit { changeActiveHintIndex(newIndex: number): void { const currentActiveIndex = this.stateHintsService.getActiveHintIndex(); - if (currentActiveIndex !== null && ( - !this.stateHintsService.displayed[currentActiveIndex] - .hintContent.html)) { - if (this.stateSolutionService.savedMemento && - this.stateHintsService.displayed.length === 1) { + if ( + currentActiveIndex !== null && + !this.stateHintsService.displayed[currentActiveIndex].hintContent.html + ) { + if ( + this.stateSolutionService.savedMemento && + this.stateHintsService.displayed.length === 1 + ) { this.openDeleteLastHintModal(); return; } else { @@ -135,37 +140,47 @@ export class StateHintsEditorComponent implements OnInit { this.alertsService.clearWarnings(); this.externalSaveService.onExternalSave.emit(); - this.ngbModal.open(AddHintModalComponent, { - backdrop: 'static', - windowClass: 'add-hint-modal' - }).result.then((result: AddHintModalResponse): void => { - this.stateHintsService.displayed.push(result.hint); - this.stateHintsService.saveDisplayedValue(); - this.onSaveHints.emit(this.stateHintsService.displayed); - this.onSaveNextContentIdIndex.emit(); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + this.ngbModal + .open(AddHintModalComponent, { + backdrop: 'static', + windowClass: 'add-hint-modal', + }) + .result.then( + (result: AddHintModalResponse): void => { + this.stateHintsService.displayed.push(result.hint); + this.stateHintsService.saveDisplayedValue(); + this.onSaveHints.emit(this.stateHintsService.displayed); + this.onSaveNextContentIdIndex.emit(); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } openDeleteLastHintModal = (): void => { this.alertsService.clearWarnings(); - this.ngbModal.open(DeleteLastHintModalComponent, { - backdrop: true, - }).result.then((): void => { - this.stateSolutionService.displayed = null; - this.stateSolutionService.saveDisplayedValue(); - this.onSaveSolution.emit(this.stateSolutionService.displayed); - - this.stateHintsService.displayed = []; - this.stateHintsService.saveDisplayedValue(); - this.onSaveHints.emit(this.stateHintsService.displayed); - }, (): void => { - this.alertsService.clearWarnings(); - }); + this.ngbModal + .open(DeleteLastHintModalComponent, { + backdrop: true, + }) + .result.then( + (): void => { + this.stateSolutionService.displayed = null; + this.stateSolutionService.saveDisplayedValue(); + this.onSaveSolution.emit(this.stateSolutionService.displayed); + + this.stateHintsService.displayed = []; + this.stateHintsService.saveDisplayedValue(); + this.onSaveHints.emit(this.stateHintsService.displayed); + }, + (): void => { + this.alertsService.clearWarnings(); + } + ); }; deleteHint(value: DeleteValueResponse): void { @@ -174,24 +189,31 @@ export class StateHintsEditorComponent implements OnInit { value.evt.stopPropagation(); this.alertsService.clearWarnings(); - this.ngbModal.open(DeleteHintModalComponent, { - backdrop: true, - }).result.then((): void => { - if (this.stateSolutionService.savedMemento && - this.stateHintsService.savedMemento.length === 1) { - this.openDeleteLastHintModal(); - } else { - this.stateHintsService.displayed.splice(value.index, 1); - this.stateHintsService.saveDisplayedValue(); - this.onSaveHints.emit(this.stateHintsService.displayed); - } - - if (value.index === this.stateHintsService.getActiveHintIndex()) { - this.stateHintsService.setActiveHintIndex(null); - } - }, (): void => { - this.alertsService.clearWarnings(); - }); + this.ngbModal + .open(DeleteHintModalComponent, { + backdrop: true, + }) + .result.then( + (): void => { + if ( + this.stateSolutionService.savedMemento && + this.stateHintsService.savedMemento.length === 1 + ) { + this.openDeleteLastHintModal(); + } else { + this.stateHintsService.displayed.splice(value.index, 1); + this.stateHintsService.saveDisplayedValue(); + this.onSaveHints.emit(this.stateHintsService.displayed); + } + + if (value.index === this.stateHintsService.getActiveHintIndex()) { + this.stateHintsService.setActiveHintIndex(null); + } + }, + (): void => { + this.alertsService.clearWarnings(); + } + ); } onSaveInlineHint(): void { @@ -219,7 +241,9 @@ export class StateHintsEditorComponent implements OnInit { } } -angular.module('oppia').directive('oppiaStateHintsEditor', -downgradeComponent({ - component: StateHintsEditorComponent -}) as angular.IDirectiveFactory); +angular.module('oppia').directive( + 'oppiaStateHintsEditor', + downgradeComponent({ + component: StateHintsEditorComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/components/state-editor/state-interaction-editor/state-interaction-editor.component.spec.ts b/core/templates/components/state-editor/state-interaction-editor/state-interaction-editor.component.spec.ts index b52c5fa1357a..ea0b834cb26d 100644 --- a/core/templates/components/state-editor/state-interaction-editor/state-interaction-editor.component.spec.ts +++ b/core/templates/components/state-editor/state-interaction-editor/state-interaction-editor.component.spec.ts @@ -12,32 +12,37 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Unit tests for 'State Interaction Editor Component'. */ -import { EventEmitter, NO_ERRORS_SCHEMA, ElementRef } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { StateEditorService } from '../state-editor-properties-services/state-editor.service'; -import { StateInteractionEditorComponent } from './state-interaction-editor.component'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { State } from 'domain/state/StateObjectFactory'; -import { Interaction } from 'domain/exploration/InteractionObjectFactory'; -import { ResponsesService } from 'pages/exploration-editor-page/editor-tab/services/responses.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { EditabilityService } from 'services/editability.service'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { CustomizeInteractionModalComponent } from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/customize-interaction-modal.component'; -import { StateInteractionIdService } from '../state-editor-properties-services/state-interaction-id.service'; -import { StateContentService } from '../state-editor-properties-services/state-content.service'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { ContextService } from 'services/context.service'; -import { StateCustomizationArgsService } from '../state-editor-properties-services/state-customization-args.service'; -import { StateSolutionService } from '../state-editor-properties-services/state-solution.service'; -import { ExplorationHtmlFormatterService } from 'services/exploration-html-formatter.service'; -import { InteractionDetailsCacheService } from 'pages/exploration-editor-page/editor-tab/services/interaction-details-cache.service'; -import { GenerateContentIdService } from 'services/generate-content-id.service'; +import {EventEmitter, NO_ERRORS_SCHEMA, ElementRef} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {StateEditorService} from '../state-editor-properties-services/state-editor.service'; +import {StateInteractionEditorComponent} from './state-interaction-editor.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {State} from 'domain/state/StateObjectFactory'; +import {Interaction} from 'domain/exploration/InteractionObjectFactory'; +import {ResponsesService} from 'pages/exploration-editor-page/editor-tab/services/responses.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {EditabilityService} from 'services/editability.service'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {CustomizeInteractionModalComponent} from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/customize-interaction-modal.component'; +import {StateInteractionIdService} from '../state-editor-properties-services/state-interaction-id.service'; +import {StateContentService} from '../state-editor-properties-services/state-content.service'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {ContextService} from 'services/context.service'; +import {StateCustomizationArgsService} from '../state-editor-properties-services/state-customization-args.service'; +import {StateSolutionService} from '../state-editor-properties-services/state-solution.service'; +import {ExplorationHtmlFormatterService} from 'services/exploration-html-formatter.service'; +import {InteractionDetailsCacheService} from 'pages/exploration-editor-page/editor-tab/services/interaction-details-cache.service'; +import {GenerateContentIdService} from 'services/generate-content-id.service'; class MockNgbModal { modal: string; @@ -48,24 +53,21 @@ class MockNgbModal { result: { componentInstance: {}, then: ( - successCallback: (result) => void, - cancelCallback: () => void + successCallback: (result) => void, + cancelCallback: () => void ) => { if (this.success) { successCallback({}); } else { cancelCallback(); } - } - } + }, + }, }; } else if (this.modal === 'delete_interaction') { return { result: { - then: ( - successCallback: () => void, - errorCallback: () => void - ) => { + then: (successCallback: () => void, errorCallback: () => void) => { if (this.success) { successCallback(); } else { @@ -74,10 +76,10 @@ class MockNgbModal { return { then: (callback: () => void) => { callback(); - } + }, }; - } - } + }, + }, }; } } @@ -102,12 +104,8 @@ describe('State Interaction component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - ], - declarations: [ - StateInteractionEditorComponent, - ], + imports: [HttpClientTestingModule], + declarations: [StateInteractionEditorComponent], providers: [ ContextService, CustomizeInteractionModalComponent, @@ -116,7 +114,7 @@ describe('State Interaction component', () => { InteractionDetailsCacheService, { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, ResponsesService, StateContentService, @@ -124,9 +122,9 @@ describe('State Interaction component', () => { StateEditorService, StateInteractionIdService, StateSolutionService, - UrlInterpolationService + UrlInterpolationService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -136,16 +134,19 @@ describe('State Interaction component', () => { contextService = TestBed.inject(ContextService); editabilityService = TestBed.inject(EditabilityService); - explorationHtmlFormatterService = - TestBed.inject(ExplorationHtmlFormatterService); + explorationHtmlFormatterService = TestBed.inject( + ExplorationHtmlFormatterService + ); generateContentIdService = TestBed.inject(GenerateContentIdService); - interactionDetailsCacheService = - TestBed.inject(InteractionDetailsCacheService); - mockNgbModal = (TestBed.inject(NgbModal) as unknown) as MockNgbModal; + interactionDetailsCacheService = TestBed.inject( + InteractionDetailsCacheService + ); + mockNgbModal = TestBed.inject(NgbModal) as unknown as MockNgbModal; responsesService = TestBed.inject(ResponsesService); stateContentService = TestBed.inject(StateContentService); - stateCustomizationArgsService = - TestBed.inject(StateCustomizationArgsService); + stateCustomizationArgsService = TestBed.inject( + StateCustomizationArgsService + ); stateEditorService = TestBed.inject(StateEditorService); stateInteractionIdService = TestBed.inject(StateInteractionIdService); stateSolutionService = TestBed.inject(StateSolutionService); @@ -154,130 +155,141 @@ describe('State Interaction component', () => { fixture.detectChanges(); }); - it('should keep non-empty content when setting an interaction ' + - 'and throw error if state is undefined', fakeAsync(() => { - spyOn(component, 'throwError').and.stub(); - spyOn(stateEditorService, 'updateStateInteractionEditorInitialised') - .and.stub(); - - component.ngOnInit(); - tick(); - stateEditorService.onStateEditorInitialized.emit(undefined); - tick(); - - expect(component.interactionIsDisabled).toBe(false); - expect(stateEditorService.updateStateInteractionEditorInitialised) - .toHaveBeenCalled(); - })); - - it('should keep non-empty content when setting an interaction ' + - 'and update changed state', () => { - spyOn(responsesService.onInitializeAnswerGroups, 'emit').and.stub(); - - const state = new State( - 'shivam', 'id', 'some', null, - new Interaction([], [], null, null, [], 'id', null), - null, null, true, true); + it( + 'should keep non-empty content when setting an interaction ' + + 'and throw error if state is undefined', + fakeAsync(() => { + spyOn(component, 'throwError').and.stub(); + spyOn( + stateEditorService, + 'updateStateInteractionEditorInitialised' + ).and.stub(); - component.ngOnInit(); - stateEditorService.onStateEditorInitialized.emit(state); + component.ngOnInit(); + tick(); + stateEditorService.onStateEditorInitialized.emit(undefined); + tick(); - expect(component.interactionIsDisabled).toBe(false); - expect(component.hasLoaded).toBe(true); - expect(responsesService.onInitializeAnswerGroups.emit) - .toHaveBeenCalled(); - }); + expect(component.interactionIsDisabled).toBe(false); + expect( + stateEditorService.updateStateInteractionEditorInitialised + ).toHaveBeenCalled(); + }) + ); + + it( + 'should keep non-empty content when setting an interaction ' + + 'and update changed state', + () => { + spyOn(responsesService.onInitializeAnswerGroups, 'emit').and.stub(); + + const state = new State( + 'shivam', + 'id', + 'some', + null, + new Interaction([], [], null, null, [], 'id', null), + null, + null, + true, + true + ); + + component.ngOnInit(); + stateEditorService.onStateEditorInitialized.emit(state); + + expect(component.interactionIsDisabled).toBe(false); + expect(component.hasLoaded).toBe(true); + expect(responsesService.onInitializeAnswerGroups.emit).toHaveBeenCalled(); + } + ); it('should show interaction when interaction is made', () => { const interactionCustomizationArgsValue = { placeholder: { - value: null + value: null, }, rows: { - value: 0 + value: 0, }, catchMisspellings: { - value: false - } + value: false, + }, }; component.interactionEditorIsShown = true; spyOn(urlInterpolationService, 'getStaticImageUrl').and.returnValue( 'image' ); - spyOn(explorationHtmlFormatterService, 'getInteractionHtml') - .and.returnValue('htmlValue'); + spyOn( + explorationHtmlFormatterService, + 'getInteractionHtml' + ).and.returnValue('htmlValue'); stateInteractionIdService.savedMemento = 'interactionID'; component.toggleInteractionEditor(); expect(component.getStaticImageUrl('image')).toEqual('image'); expect(component.interactionEditorIsShown).toBe(false); - expect(component._getInteractionPreviewTag( - interactionCustomizationArgsValue)).toBe('htmlValue'); + expect( + component._getInteractionPreviewTag(interactionCustomizationArgsValue) + ).toBe('htmlValue'); }); - it('should delete interaction when user click on delete btn', - fakeAsync(() => { - mockNgbModal.modal = 'delete_interaction'; - - spyOn(stateSolutionService, 'saveDisplayedValue').and.stub(); - spyOn(mockNgbModal, 'open').and.callFake((dlg, opt) => { - return ( - { - result: Promise.resolve('success') - } - ) as NgbModalRef; - }); - - component.deleteInteraction(); - tick(); + it('should delete interaction when user click on delete btn', fakeAsync(() => { + mockNgbModal.modal = 'delete_interaction'; - expect(stateSolutionService.saveDisplayedValue) - .toHaveBeenCalled(); - })); + spyOn(stateSolutionService, 'saveDisplayedValue').and.stub(); + spyOn(mockNgbModal, 'open').and.callFake((dlg, opt) => { + return { + result: Promise.resolve('success'), + } as NgbModalRef; + }); - it('should not delete interaction when user click on cancel btn', - fakeAsync(() => { - mockNgbModal.modal = 'delete_interaction'; - - spyOn(stateSolutionService, 'saveDisplayedValue').and.stub(); - spyOn(mockNgbModal, 'open').and.callFake((dlg, opt) => { - return ( - { - result: Promise.reject('success') - } - ) as NgbModalRef; - }); - - component.deleteInteraction(); - tick(); + component.deleteInteraction(); + tick(); - expect(stateSolutionService.saveDisplayedValue) - .not.toHaveBeenCalled(); - })); + expect(stateSolutionService.saveDisplayedValue).toHaveBeenCalled(); + })); + + it('should not delete interaction when user click on cancel btn', fakeAsync(() => { + mockNgbModal.modal = 'delete_interaction'; + + spyOn(stateSolutionService, 'saveDisplayedValue').and.stub(); + spyOn(mockNgbModal, 'open').and.callFake((dlg, opt) => { + return { + result: Promise.reject('success'), + } as NgbModalRef; + }); + + component.deleteInteraction(); + tick(); + + expect(stateSolutionService.saveDisplayedValue).not.toHaveBeenCalled(); + })); it('should close modal when user click cancel', fakeAsync(() => { const mockEventEmitter = new EventEmitter(); - generateContentIdService.init(() => 1, () => {}); + generateContentIdService.init( + () => 1, + () => {} + ); mockNgbModal.modal = 'add_interaction'; stateContentService.savedMemento = new SubtitledHtml('html', 'contentID'); stateCustomizationArgsService.savedMemento = { useFractionForDivision: false, allowedVariables: { - value: ['wrok', 'done'] - } + value: ['wrok', 'done'], + }, }; component.interactionId = 'EndExploration'; component.interactionIsDisabled = false; component.updateDefaultTerminalStateContentIfEmpty(); spyOn(mockNgbModal, 'open').and.callFake((dlg, opt) => { - return ( - { - componentInstance: {}, - result: Promise.reject('reject') - } - ) as NgbModalRef; + return { + componentInstance: {}, + result: Promise.reject('reject'), + } as NgbModalRef; }); spyOn(editabilityService, 'isEditable').and.returnValue(true); spyOn(contextService, 'isExplorationLinkedToStory').and.returnValue(true); @@ -293,84 +305,91 @@ describe('State Interaction component', () => { })); it('should focus on customize interaction button when tab is pressed', () => { - const event = new KeyboardEvent('keydown', { key: 'Tab' }); + const event = new KeyboardEvent('keydown', {key: 'Tab'}); const customizeInteractionButtonRef = new ElementRef( - document.createElement('button')); - component.customizeInteractionButton = - customizeInteractionButtonRef; - spyOn(component, 'getCurrentInteractionName') - .and.returnValue('Introduction'); + document.createElement('button') + ); + component.customizeInteractionButton = customizeInteractionButtonRef; + spyOn(component, 'getCurrentInteractionName').and.returnValue( + 'Introduction' + ); component.interactionEditorIsShown = true; spyOn(customizeInteractionButtonRef.nativeElement, 'focus'); component.focusOnCustomizeInteraction(event); expect( - customizeInteractionButtonRef - .nativeElement.focus).toHaveBeenCalled(); + customizeInteractionButtonRef.nativeElement.focus + ).toHaveBeenCalled(); }); - it('should focus on customize interaction title when ' + - 'shift + tab are pressed', () => { - const event = new KeyboardEvent('keydown', { key: 'Tab', shiftKey: true }); - const collapseAnswersAndResponsesButtonRef = new ElementRef( - document.createElement('button')); - component.collapseAnswersAndResponsesButton = - collapseAnswersAndResponsesButtonRef; - spyOn( - component.collapseAnswersAndResponsesButton - .nativeElement, 'focus'); - - component.focusOnCollapseAnswersAndResponses(event); - - expect( - component.collapseAnswersAndResponsesButton - .nativeElement.focus).toHaveBeenCalled(); - }); + it( + 'should focus on customize interaction title when ' + + 'shift + tab are pressed', + () => { + const event = new KeyboardEvent('keydown', {key: 'Tab', shiftKey: true}); + const collapseAnswersAndResponsesButtonRef = new ElementRef( + document.createElement('button') + ); + component.collapseAnswersAndResponsesButton = + collapseAnswersAndResponsesButtonRef; + spyOn(component.collapseAnswersAndResponsesButton.nativeElement, 'focus'); + + component.focusOnCollapseAnswersAndResponses(event); + + expect( + component.collapseAnswersAndResponsesButton.nativeElement.focus + ).toHaveBeenCalled(); + } + ); - it('should open Interaction Customizer Modal ' + - 'when enter is pressed', () => { - const event = new KeyboardEvent('keydown', { key: 'Enter' }); - spyOn(component, 'openInteractionCustomizerModal'); + it( + 'should open Interaction Customizer Modal ' + 'when enter is pressed', + () => { + const event = new KeyboardEvent('keydown', {key: 'Enter'}); + spyOn(component, 'openInteractionCustomizerModal'); - component.focusOnCollapseAnswersAndResponses(event); + component.focusOnCollapseAnswersAndResponses(event); - expect(component.openInteractionCustomizerModal).toHaveBeenCalled(); - }); + expect(component.openInteractionCustomizerModal).toHaveBeenCalled(); + } + ); it('should save interaction when user click save', fakeAsync(() => { stateInteractionIdService.displayed = 'EndExploration'; stateInteractionIdService.savedMemento = 'InteractiveMap'; component.DEFAULT_TERMINAL_STATE_CONTENT = 'HTML Content'; stateContentService.savedMemento = SubtitledHtml.createDefault( - '', 'contentID'); + '', + 'contentID' + ); stateContentService.displayed = SubtitledHtml.createDefault( - '', 'contentID2'); + '', + 'contentID2' + ); stateCustomizationArgsService.savedMemento = { latitude: { - value: 35 + value: 35, }, longitude: { - value: 20 + value: 20, }, zoom: { - value: 8 - } + value: 8, + }, }; stateCustomizationArgsService.displayed = { recommendedExplorationIds: { - value: ['null'] - } + value: ['null'], + }, }; mockNgbModal.modal = 'add_interaction'; spyOn(mockNgbModal, 'open').and.callFake((dlg, opt) => { - return ( - { - componentInstance: {}, - result: Promise.resolve('success') - } - ) as NgbModalRef; + return { + componentInstance: {}, + result: Promise.resolve('success'), + } as NgbModalRef; }); spyOn(interactionDetailsCacheService, 'set').and.stub(); diff --git a/core/templates/components/state-editor/state-interaction-editor/state-interaction-editor.component.ts b/core/templates/components/state-editor/state-interaction-editor/state-interaction-editor.component.ts index 06ee55acc8f9..4f7b438bc986 100644 --- a/core/templates/components/state-editor/state-interaction-editor/state-interaction-editor.component.ts +++ b/core/templates/components/state-editor/state-interaction-editor/state-interaction-editor.component.ts @@ -12,40 +12,50 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Component for the interaction editor section in the state * editor. */ -import { Component, EventEmitter, OnDestroy, OnInit, Output, ViewChild, ElementRef } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { InteractionDetailsCacheService } from 'pages/exploration-editor-page/editor-tab/services/interaction-details-cache.service'; -import { ResponsesService } from 'pages/exploration-editor-page/editor-tab/services/responses.service'; -import { CustomizeInteractionModalComponent } from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/customize-interaction-modal.component'; -import { DeleteInteractionModalComponent } from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-interaction-modal.component'; -import { Subscription } from 'rxjs'; -import { AlertsService } from 'services/alerts.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { EditabilityService } from 'services/editability.service'; -import { StateCustomizationArgsService } from '../state-editor-properties-services/state-customization-args.service'; -import { StateEditorService } from '../state-editor-properties-services/state-editor.service'; -import { StateInteractionIdService } from '../state-editor-properties-services/state-interaction-id.service'; -import { StateSolutionService } from '../state-editor-properties-services/state-solution.service'; -import { StateContentService } from '../state-editor-properties-services/state-content.service'; -import { ContextService } from 'services/context.service'; -import { ExplorationHtmlFormatterService } from 'services/exploration-html-formatter.service'; -import { InteractionCustomizationArgs, InteractionData } from 'interactions/customization-args-defs'; -import { Solution } from 'domain/exploration/SolutionObjectFactory'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; +import { + Component, + EventEmitter, + OnDestroy, + OnInit, + Output, + ViewChild, + ElementRef, +} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {InteractionDetailsCacheService} from 'pages/exploration-editor-page/editor-tab/services/interaction-details-cache.service'; +import {ResponsesService} from 'pages/exploration-editor-page/editor-tab/services/responses.service'; +import {CustomizeInteractionModalComponent} from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/customize-interaction-modal.component'; +import {DeleteInteractionModalComponent} from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-interaction-modal.component'; +import {Subscription} from 'rxjs'; +import {AlertsService} from 'services/alerts.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {EditabilityService} from 'services/editability.service'; +import {StateCustomizationArgsService} from '../state-editor-properties-services/state-customization-args.service'; +import {StateEditorService} from '../state-editor-properties-services/state-editor.service'; +import {StateInteractionIdService} from '../state-editor-properties-services/state-interaction-id.service'; +import {StateSolutionService} from '../state-editor-properties-services/state-solution.service'; +import {StateContentService} from '../state-editor-properties-services/state-content.service'; +import {ContextService} from 'services/context.service'; +import {ExplorationHtmlFormatterService} from 'services/exploration-html-formatter.service'; +import { + InteractionCustomizationArgs, + InteractionData, +} from 'interactions/customization-args-defs'; +import {Solution} from 'domain/exploration/SolutionObjectFactory'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; import INTERACTION_SPECS from 'interactions/interaction_specs.json'; -import { State } from 'domain/state/StateObjectFactory'; -import { AnswerGroup } from 'domain/exploration/AnswerGroupObjectFactory'; -import { Outcome } from 'domain/exploration/OutcomeObjectFactory'; -import { InteractionAnswer } from 'interactions/answer-defs'; -import { GenerateContentIdService } from 'services/generate-content-id.service'; +import {State} from 'domain/state/StateObjectFactory'; +import {AnswerGroup} from 'domain/exploration/AnswerGroupObjectFactory'; +import {Outcome} from 'domain/exploration/OutcomeObjectFactory'; +import {InteractionAnswer} from 'interactions/answer-defs'; +import {GenerateContentIdService} from 'services/generate-content-id.service'; export interface InitializeAnswerGroups { interactionId: string; @@ -56,12 +66,12 @@ export interface InitializeAnswerGroups { @Component({ selector: 'oppia-state-interaction-editor', - templateUrl: './state-interaction-editor.component.html' + templateUrl: './state-interaction-editor.component.html', }) -export class StateInteractionEditorComponent - implements OnInit, OnDestroy { - @Output() markAllAudioAsNeedingUpdateModalIfRequired = - new EventEmitter(); +export class StateInteractionEditorComponent implements OnInit, OnDestroy { + @Output() markAllAudioAsNeedingUpdateModalIfRequired = new EventEmitter< + string[] + >(); @Output() onSaveInteractionData = new EventEmitter(); @Output() onSaveNextContentIdIndex = new EventEmitter(); @@ -70,10 +80,10 @@ export class StateInteractionEditorComponent @Output() recomputeGraph = new EventEmitter(); @ViewChild('customizeInteractionButton') - customizeInteractionButton!: ElementRef; + customizeInteractionButton!: ElementRef; @ViewChild('collapseAnswersAndResponsesButton') - collapseAnswersAndResponsesButton!: ElementRef; + collapseAnswersAndResponsesButton!: ElementRef; customizationModalReopened: boolean; DEFAULT_TERMINAL_STATE_CONTENT: string; @@ -100,25 +110,29 @@ export class StateInteractionEditorComponent private generateContentIdService: GenerateContentIdService, private stateSolutionService: StateSolutionService, private urlInterpolationService: UrlInterpolationService, - private windowDimensionsService: WindowDimensionsService, + private windowDimensionsService: WindowDimensionsService ) {} getCurrentInteractionName(): string { - return ( - this.stateInteractionIdService.savedMemento ? - INTERACTION_SPECS[this.stateInteractionIdService.savedMemento].name : - ''); + return this.stateInteractionIdService.savedMemento + ? INTERACTION_SPECS[this.stateInteractionIdService.savedMemento].name + : ''; } _getInteractionPreviewTag( - interactionCustomizationArgs: InteractionCustomizationArgs): string { + interactionCustomizationArgs: InteractionCustomizationArgs + ): string { if (!this.stateInteractionIdService.savedMemento) { return ''; } return this.explorationHtmlFormatterService.getInteractionHtml( this.stateInteractionIdService.savedMemento, - interactionCustomizationArgs, false, null, null); + interactionCustomizationArgs, + false, + null, + null + ); } _updateInteractionPreview(): void { @@ -127,17 +141,20 @@ export class StateInteractionEditorComponent let currentCustomizationArgs = this.stateCustomizationArgsService.savedMemento; this.interactionPreviewHtml = this._getInteractionPreviewTag( - currentCustomizationArgs); - this.interactionIsDisabled = ( + currentCustomizationArgs + ); + this.interactionIsDisabled = this.interactionId === 'EndExploration' && - this.contextService.isExplorationLinkedToStory()); + this.contextService.isExplorationLinkedToStory(); } _updateAnswerChoices(): void { this.stateEditorService.onUpdateAnswerChoices.emit( this.stateEditorService.getAnswerChoices( this.interactionId, - this.stateCustomizationArgsService.savedMemento)); + this.stateCustomizationArgsService.savedMemento + ) + ); } // If a terminal interaction is selected for a state with no content, @@ -155,13 +172,14 @@ export class StateInteractionEditorComponent this.stateContentService.displayed.html = this.DEFAULT_TERMINAL_STATE_CONTENT; this.stateContentService.saveDisplayedValue(); - this.onSaveStateContent.emit( - this.stateContentService.displayed); + this.onSaveStateContent.emit(this.stateContentService.displayed); } focusOnCustomizeInteraction(event: KeyboardEvent): void { - if (this.getCurrentInteractionName() !== '' && - this.interactionEditorIsShown) { + if ( + this.getCurrentInteractionName() !== '' && + this.interactionEditorIsShown + ) { event.preventDefault(); this.customizeInteractionButton.nativeElement.focus(); } @@ -179,12 +197,13 @@ export class StateInteractionEditorComponent } onCustomizationModalSavePostHook(): void { - let hasInteractionIdChanged = ( + let hasInteractionIdChanged = this.stateInteractionIdService.displayed !== - this.stateInteractionIdService.savedMemento); + this.stateInteractionIdService.savedMemento; if (hasInteractionIdChanged) { - if (INTERACTION_SPECS[this.stateInteractionIdService.displayed] - .is_terminal) { + if ( + INTERACTION_SPECS[this.stateInteractionIdService.displayed].is_terminal + ) { this.updateDefaultTerminalStateContentIfEmpty(); } this.stateInteractionIdService.saveDisplayedValue(); @@ -193,14 +212,15 @@ export class StateInteractionEditorComponent let interactionData: InteractionData = { interactionId: this.stateInteractionIdService.displayed, - customizationArgs: this.stateCustomizationArgsService.displayed + customizationArgs: this.stateCustomizationArgsService.displayed, }; this.onSaveInteractionData.emit(interactionData); this.onSaveNextContentIdIndex.emit(); this.interactionDetailsCacheService.set( this.stateInteractionIdService.savedMemento, - this.stateCustomizationArgsService.savedMemento); + this.stateCustomizationArgsService.savedMemento + ); // This must be called here so that the rules are updated before the // state graph is recomputed. @@ -215,7 +235,8 @@ export class StateInteractionEditorComponent this.stateEditorService.onHandleCustomArgsUpdate.emit( this.stateEditorService.getAnswerChoices( this.interactionId, - this.stateCustomizationArgsService.savedMemento) + this.stateCustomizationArgsService.savedMemento + ) ); } @@ -226,12 +247,11 @@ export class StateInteractionEditorComponent if (this.editabilityService.isEditable()) { this.alertsService.clearWarnings(); - const modalRef = this.ngbModal - .open(CustomizeInteractionModalComponent, { - keyboard: false, - backdrop: 'static', - windowClass: 'customize-interaction-modal' - }); + const modalRef = this.ngbModal.open(CustomizeInteractionModalComponent, { + keyboard: false, + backdrop: 'static', + windowClass: 'customize-interaction-modal', + }); modalRef.result.then( () => { @@ -241,41 +261,48 @@ export class StateInteractionEditorComponent this.stateInteractionIdService.restoreFromMemento(); this.stateCustomizationArgsService.restoreFromMemento(); this.generateContentIdService.revertUnusedContentIdIndex(); - }); + } + ); } } deleteInteraction(): void { this.alertsService.clearWarnings(); - this.ngbModal.open(DeleteInteractionModalComponent, { - backdrop: true, - }).result.then(() => { - this.stateInteractionIdService.displayed = null; - this.stateCustomizationArgsService.displayed = {}; - this.stateSolutionService.displayed = null; - this.interactionDetailsCacheService.removeDetails( - this.stateInteractionIdService.savedMemento); - this.stateInteractionIdService.saveDisplayedValue(); - this.stateCustomizationArgsService.saveDisplayedValue(); - - let interactionData: InteractionData = { - interactionId: this.stateInteractionIdService.displayed, - customizationArgs: this.stateCustomizationArgsService.displayed - }; - this.onSaveInteractionData.emit(interactionData); - - this.stateSolutionService.saveDisplayedValue(); - this.onSaveSolution.emit(this.stateSolutionService.displayed); - - this.stateInteractionIdService.onInteractionIdChanged.emit( - this.stateInteractionIdService.savedMemento + this.ngbModal + .open(DeleteInteractionModalComponent, { + backdrop: true, + }) + .result.then( + () => { + this.stateInteractionIdService.displayed = null; + this.stateCustomizationArgsService.displayed = {}; + this.stateSolutionService.displayed = null; + this.interactionDetailsCacheService.removeDetails( + this.stateInteractionIdService.savedMemento + ); + this.stateInteractionIdService.saveDisplayedValue(); + this.stateCustomizationArgsService.saveDisplayedValue(); + + let interactionData: InteractionData = { + interactionId: this.stateInteractionIdService.displayed, + customizationArgs: this.stateCustomizationArgsService.displayed, + }; + this.onSaveInteractionData.emit(interactionData); + + this.stateSolutionService.saveDisplayedValue(); + this.onSaveSolution.emit(this.stateSolutionService.displayed); + + this.stateInteractionIdService.onInteractionIdChanged.emit( + this.stateInteractionIdService.savedMemento + ); + this.recomputeGraph.emit(); + this._updateInteractionPreview(); + this._updateAnswerChoices(); + }, + () => { + this.alertsService.clearWarnings(); + } ); - this.recomputeGraph.emit(); - this._updateInteractionPreview(); - this._updateAnswerChoices(); - }, () => { - this.alertsService.clearWarnings(); - }); } toggleInteractionEditor(): void { @@ -288,42 +315,39 @@ export class StateInteractionEditorComponent throwError(stateData: State): void { throw new Error( - 'Expected stateData to be defined but ' + - 'received ' + stateData); + 'Expected stateData to be defined but ' + 'received ' + stateData + ); } ngOnInit(): void { this.interactionIsDisabled = false; - this.DEFAULT_TERMINAL_STATE_CONTENT = - 'Congratulations, you have finished!'; + this.DEFAULT_TERMINAL_STATE_CONTENT = 'Congratulations, you have finished!'; this.windowIsNarrow = this.windowDimensionsService.isWindowNarrow(); this.interactionEditorIsShown = true; this.hasLoaded = false; this.customizationModalReopened = false; this.directiveSubscriptions.add( - this.stateEditorService.onStateEditorInitialized.subscribe( - (stateData) => { - if (stateData === undefined || Object.keys(stateData).length === 0) { - this.throwError(stateData); - return; - } - - this.hasLoaded = false; - this.interactionDetailsCacheService.reset(); - this.responsesService.onInitializeAnswerGroups.emit({ - interactionId: stateData.interaction.id, - answerGroups: stateData.interaction.answerGroups, - defaultOutcome: stateData.interaction.defaultOutcome, - confirmedUnclassifiedAnswers: ( - stateData.interaction.confirmedUnclassifiedAnswers), - }); - - this._updateInteractionPreview(); - this._updateAnswerChoices(); - this.hasLoaded = true; + this.stateEditorService.onStateEditorInitialized.subscribe(stateData => { + if (stateData === undefined || Object.keys(stateData).length === 0) { + this.throwError(stateData); + return; } - ) + + this.hasLoaded = false; + this.interactionDetailsCacheService.reset(); + this.responsesService.onInitializeAnswerGroups.emit({ + interactionId: stateData.interaction.id, + answerGroups: stateData.interaction.answerGroups, + defaultOutcome: stateData.interaction.defaultOutcome, + confirmedUnclassifiedAnswers: + stateData.interaction.confirmedUnclassifiedAnswers, + }); + + this._updateInteractionPreview(); + this._updateAnswerChoices(); + this.hasLoaded = true; + }) ); this.stateEditorService.onInteractionEditorInitialized.emit(); @@ -335,7 +359,9 @@ export class StateInteractionEditorComponent } } -angular.module('oppia').directive('oppiaStateInteractionEditor', -downgradeComponent({ - component: StateInteractionEditorComponent -}) as angular.IDirectiveFactory); +angular.module('oppia').directive( + 'oppiaStateInteractionEditor', + downgradeComponent({ + component: StateInteractionEditorComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/components/state-editor/state-responses-editor/state-responses.component.spec.ts b/core/templates/components/state-editor/state-responses-editor/state-responses.component.spec.ts index ba3e657ad968..8cf2234fb071 100644 --- a/core/templates/components/state-editor/state-responses-editor/state-responses.component.spec.ts +++ b/core/templates/components/state-editor/state-responses-editor/state-responses.component.spec.ts @@ -12,57 +12,76 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Unit tests for State Responses Component. */ -import { EventEmitter, NO_ERRORS_SCHEMA, Pipe } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { AnswerGroup, AnswerGroupObjectFactory } from 'domain/exploration/AnswerGroupObjectFactory'; -import { Interaction, InteractionObjectFactory } from 'domain/exploration/InteractionObjectFactory'; -import { Outcome, OutcomeObjectFactory } from 'domain/exploration/OutcomeObjectFactory'; -import { Rule } from 'domain/exploration/rule.model'; -import { MisconceptionObjectFactory } from 'domain/skill/MisconceptionObjectFactory'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { ResponsesService } from 'pages/exploration-editor-page/editor-tab/services/responses.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { StateCustomizationArgsService } from '../state-editor-properties-services/state-customization-args.service'; -import { AnswerChoice, StateEditorService } from '../state-editor-properties-services/state-editor.service'; -import { StateInteractionIdService } from '../state-editor-properties-services/state-interaction-id.service'; -import { AlertsService } from 'services/alerts.service'; -import { ExternalSaveService } from 'services/external-save.service'; -import { StateSolicitAnswerDetailsService } from '../state-editor-properties-services/state-solicit-answer-details.service'; -import { StateResponsesComponent } from './state-responses.component'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ParameterizeRuleDescriptionPipe } from 'filters/parameterize-rule-description.pipe'; -import { WrapTextWithEllipsisPipe } from 'filters/string-utility-filters/wrap-text-with-ellipsis.pipe'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { CdkDragSortEvent } from '@angular/cdk/drag-drop'; - -@Pipe({ name: 'parameterizeRuleDescriptionPipe' }) +import {EventEmitter, NO_ERRORS_SCHEMA, Pipe} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import { + AnswerGroup, + AnswerGroupObjectFactory, +} from 'domain/exploration/AnswerGroupObjectFactory'; +import { + Interaction, + InteractionObjectFactory, +} from 'domain/exploration/InteractionObjectFactory'; +import { + Outcome, + OutcomeObjectFactory, +} from 'domain/exploration/OutcomeObjectFactory'; +import {Rule} from 'domain/exploration/rule.model'; +import {MisconceptionObjectFactory} from 'domain/skill/MisconceptionObjectFactory'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {ResponsesService} from 'pages/exploration-editor-page/editor-tab/services/responses.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {StateCustomizationArgsService} from '../state-editor-properties-services/state-customization-args.service'; +import { + AnswerChoice, + StateEditorService, +} from '../state-editor-properties-services/state-editor.service'; +import {StateInteractionIdService} from '../state-editor-properties-services/state-interaction-id.service'; +import {AlertsService} from 'services/alerts.service'; +import {ExternalSaveService} from 'services/external-save.service'; +import {StateSolicitAnswerDetailsService} from '../state-editor-properties-services/state-solicit-answer-details.service'; +import {StateResponsesComponent} from './state-responses.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ParameterizeRuleDescriptionPipe} from 'filters/parameterize-rule-description.pipe'; +import {WrapTextWithEllipsisPipe} from 'filters/string-utility-filters/wrap-text-with-ellipsis.pipe'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {CdkDragSortEvent} from '@angular/cdk/drag-drop'; + +@Pipe({name: 'parameterizeRuleDescriptionPipe'}) class MockParameterizeRuleDescriptionPipe { transform( - rule: Rule | null, interactionId: string | null, - choices: AnswerChoice[] | null): string { + rule: Rule | null, + interactionId: string | null, + choices: AnswerChoice[] | null + ): string { return ''; } } -@Pipe({ name: 'wrapTextWithEllipsis' }) +@Pipe({name: 'wrapTextWithEllipsis'}) class MockWrapTextWithEllipsisPipe { transform(input: string, characterCount: number): string { return ''; } } -@Pipe({ name: 'truncate' }) +@Pipe({name: 'truncate'}) class MockTruncatePipe { transform(value: string, params: number): string { return value; } } -@Pipe({ name: 'convertToPlainText' }) +@Pipe({name: 'convertToPlainText'}) class MockConvertToPlainTextPipe { transform(value: string): string { return value; @@ -72,7 +91,7 @@ class MockConvertToPlainTextPipe { class MockNgbModal { open() { return { - result: Promise.resolve() + result: Promise.resolve(), }; } } @@ -103,25 +122,25 @@ describe('State Responses Component', () => { dest_if_really_stuck: null, feedback: { content_id: 'feedback_1', - html: '' + html: '', }, param_changes: [], refresher_exploration_id: null, labelled_as_correct: false, - missing_prerequisite_skill_id: '' + missing_prerequisite_skill_id: '', }, { dest: 'State 5', dest_if_really_stuck: null, feedback: { content_id: 'feedback_2', - html: "Let's go to state 5 ImageAndRegion" + html: "Let's go to state 5 ImageAndRegion", }, param_changes: [], refresher_exploration_id: null, labelled_as_correct: false, - missing_prerequisite_skill_id: '' - } + missing_prerequisite_skill_id: '', + }, ]; beforeEach(waitForAsync(() => { @@ -132,7 +151,7 @@ describe('State Responses Component', () => { MockParameterizeRuleDescriptionPipe, MockTruncatePipe, MockConvertToPlainTextPipe, - MockWrapTextWithEllipsisPipe + MockWrapTextWithEllipsisPipe, ], providers: [ WindowDimensionsService, @@ -149,18 +168,18 @@ describe('State Responses Component', () => { MisconceptionObjectFactory, { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, { provide: ParameterizeRuleDescriptionPipe, - useClass: MockParameterizeRuleDescriptionPipe + useClass: MockParameterizeRuleDescriptionPipe, }, { provide: WrapTextWithEllipsisPipe, - useClass: MockWrapTextWithEllipsisPipe - } + useClass: MockWrapTextWithEllipsisPipe, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -177,11 +196,13 @@ describe('State Responses Component', () => { stateEditorService = TestBed.inject(StateEditorService); responsesService = TestBed.inject(ResponsesService); stateInteractionIdService = TestBed.inject(StateInteractionIdService); - stateSolicitAnswerDetailsService = - TestBed.inject(StateSolicitAnswerDetailsService); + stateSolicitAnswerDetailsService = TestBed.inject( + StateSolicitAnswerDetailsService + ); alertsService = TestBed.inject(AlertsService); - stateCustomizationArgsService = - TestBed.inject(StateCustomizationArgsService); + stateCustomizationArgsService = TestBed.inject( + StateCustomizationArgsService + ); externalSaveService = TestBed.inject(ExternalSaveService); interactionData = interactionObjectFactory.createFromBackendDict({ @@ -200,13 +221,17 @@ describe('State Responses Component', () => { labelled_as_correct: false, param_changes: [], }, - rule_specs: [{ - rule_type: 'Contains', - inputs: {x: { - contentId: 'rule_input', - normalizedStrSet: ['abc'] - }} - }], + rule_specs: [ + { + rule_type: 'Contains', + inputs: { + x: { + contentId: 'rule_input', + normalizedStrSet: ['abc'], + }, + }, + }, + ], training_data: [], tagged_skill_misconception_id: 'misconception1', }, @@ -232,8 +257,8 @@ describe('State Responses Component', () => { value: 1, }, catchMisspellings: { - value: false - } + value: false, + }, }, hints: [], solution: { @@ -246,57 +271,62 @@ describe('State Responses Component', () => { }, }); - answerGroups = [answerGroupObjectFactory - .createFromBackendDict({ - rule_specs: [{ - rule_type: 'Contains', - inputs: {x: { - contentId: 'rule_input', - normalizedStrSet: ['abc'] - }} - }], - outcome: { - dest: 'State', - dest_if_really_stuck: null, - feedback: { - html: '', - content_id: 'This is a new feedback text' + answerGroups = [ + answerGroupObjectFactory.createFromBackendDict( + { + rule_specs: [ + { + rule_type: 'Contains', + inputs: { + x: { + contentId: 'rule_input', + normalizedStrSet: ['abc'], + }, + }, + }, + ], + outcome: { + dest: 'State', + dest_if_really_stuck: null, + feedback: { + html: '', + content_id: 'This is a new feedback text', + }, + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: 'test', + missing_prerequisite_skill_id: 'test_skill_id', }, - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: 'test', - missing_prerequisite_skill_id: 'test_skill_id' + training_data: [], + tagged_skill_misconception_id: 'misconception1', }, - training_data: [], - tagged_skill_misconception_id: 'misconception1' - }, 'TextInput') + 'TextInput' + ), ]; defaultOutcome = outcomeObjectFactory.createFromBackendDict({ dest: 'Hola', dest_if_really_stuck: null, feedback: { content_id: '', - html: '' + html: '', }, labelled_as_correct: true, param_changes: [], refresher_exploration_id: 'test', - missing_prerequisite_skill_id: 'test_skill_id' + missing_prerequisite_skill_id: 'test_skill_id', }); }); - it('should sort state responses properly', () => { component.answerGroups = answerGroups; - spyOn(responsesService, 'save').and.callFake( - (value, values, callback) => { - // This throws "Argument of type 'null' is not assignable to - // parameter of type 'AnswerGroup[]'." We need to suppress this error - // because of the need to test validations. This throws an error - // because the value passed is null. - // @ts-ignore - callback(null, null); - }); + spyOn(responsesService, 'save').and.callFake((value, values, callback) => { + // This throws "Argument of type 'null' is not assignable to + // parameter of type 'AnswerGroup[]'." We need to suppress this error + // because of the need to test validations. This throws an error + // because the value passed is null. + // @ts-ignore + callback(null, null); + }); spyOn(component.onSaveNextContentIdIndex, 'emit').and.stub(); const event = { @@ -312,8 +342,10 @@ describe('State Responses Component', () => { it('should set component properties on initialization', () => { spyOn(windowDimensionsService, 'isWindowNarrow').and.returnValue(true); spyOn(stateEditorService, 'getActiveStateName').and.returnValue('Hola'); - spyOn(stateEditorService, 'getInapplicableSkillMisconceptionIds') - .and.returnValue(['id1']); + spyOn( + stateEditorService, + 'getInapplicableSkillMisconceptionIds' + ).and.returnValue(['id1']); expect(component.responseCardIsShown).toBe(false); expect(component.enableSolicitAnswerDetailsFeature).toBe(false); @@ -343,128 +375,160 @@ describe('State Responses Component', () => { component.ngOnInit(); - expect(responsesService.onInitializeAnswerGroups.subscribe) - .toHaveBeenCalled(); - expect(stateInteractionIdService.onInteractionIdChanged.subscribe) - .toHaveBeenCalled(); + expect( + responsesService.onInitializeAnswerGroups.subscribe + ).toHaveBeenCalled(); + expect( + stateInteractionIdService.onInteractionIdChanged.subscribe + ).toHaveBeenCalled(); expect(responsesService.onAnswerGroupsChanged.subscribe).toHaveBeenCalled(); - expect(stateEditorService.onUpdateAnswerChoices.subscribe) - .toHaveBeenCalled(); - expect(stateEditorService.onHandleCustomArgsUpdate.subscribe) - .toHaveBeenCalled(); - expect(stateEditorService.onStateEditorInitialized.subscribe) - .toHaveBeenCalled(); - - component.ngOnDestroy(); - }); - - it('should set answer group and default answer when answer' + - ' groups are initialized', () => { - let onInitializeAnswerGroupsEmitter = new EventEmitter(); - spyOnProperty(responsesService, 'onInitializeAnswerGroups') - .and.returnValue(onInitializeAnswerGroupsEmitter); - spyOn(responsesService, 'changeActiveAnswerGroupIndex'); - spyOn(component, 'isCurrentInteractionLinear').and.returnValue(true); - - component.ngOnInit(); - - onInitializeAnswerGroupsEmitter.emit(interactionData); - - expect(component.defaultOutcome).toEqual(defaultOutcome); - expect(component.answerGroups).toEqual(answerGroups); - expect(responsesService.changeActiveAnswerGroupIndex) - .toHaveBeenCalledWith(0); - - component.ngOnDestroy(); - }); - - it('should re-initialize properties and open add answer group modal when' + - ' interaction is changed to a non-linear and non-terminal one', () => { - let onInteractionIdChangedEmitter = new EventEmitter(); - spyOn(responsesService, 'getAnswerGroups').and.returnValue(answerGroups); - spyOn(responsesService, 'getActiveAnswerGroupIndex').and.returnValue(0); - spyOnProperty(stateInteractionIdService, 'onInteractionIdChanged') - .and.returnValue(onInteractionIdChangedEmitter); - spyOn(responsesService, 'onInteractionIdChanged').and.callFake( - (options, callback) => { - // This throws "Argument of type 'null' is not assignable to - // parameter of type 'AnswerGroup[]'." We need to suppress this error - // because of the need to test validations. This throws an error - // because the value passed is null. - // @ts-ignore - callback(null, null); - }); - spyOn(stateEditorService, 'isInQuestionMode').and.returnValue(true); - spyOn(responsesService, 'getDefaultOutcome').and.returnValue( - defaultOutcome); - spyOn(component, 'openAddAnswerGroupModal'); - - expect(component.answerGroups).toEqual([]); - expect(component.defaultOutcome).toEqual(undefined); - expect(component.activeAnswerGroupIndex).toBe(undefined); - - component.ngOnInit(); - onInteractionIdChangedEmitter.emit('ImageClickInput'); - - expect(component.answerGroups).toEqual(answerGroups); - expect(component.defaultOutcome).toEqual(defaultOutcome); - expect(component.activeAnswerGroupIndex).toBe(0); - expect(component.openAddAnswerGroupModal).toHaveBeenCalled(); - - component.ngOnDestroy(); - }); - - it('should not open add answer group modal when interaction is' + - ' changed to a linear and terminal one', () => { - let onInteractionIdChangedEmitter = new EventEmitter(); - spyOnProperty(stateInteractionIdService, 'onInteractionIdChanged') - .and.returnValue(onInteractionIdChangedEmitter); - spyOn(component, 'openAddAnswerGroupModal'); - - component.ngOnInit(); - onInteractionIdChangedEmitter.emit('Continue'); - - expect(component.openAddAnswerGroupModal).not.toHaveBeenCalled(); + expect( + stateEditorService.onUpdateAnswerChoices.subscribe + ).toHaveBeenCalled(); + expect( + stateEditorService.onHandleCustomArgsUpdate.subscribe + ).toHaveBeenCalled(); + expect( + stateEditorService.onStateEditorInitialized.subscribe + ).toHaveBeenCalled(); component.ngOnDestroy(); }); - it('should get new answer groups, default outcome and verify/update' + - ' inapplicable skill misconception ids on answer groups change', () => { - let onAnswerGroupsChangedEmitter = new EventEmitter(); - spyOn(responsesService, 'getAnswerGroups').and.returnValue(answerGroups); - spyOnProperty(responsesService, 'onAnswerGroupsChanged') - .and.returnValue(onAnswerGroupsChangedEmitter); - spyOn(responsesService, 'getActiveAnswerGroupIndex').and.returnValue(0); - spyOn(responsesService, 'getDefaultOutcome').and.returnValue( - defaultOutcome); - spyOn(stateEditorService, 'getInapplicableSkillMisconceptionIds') - .and.returnValue(['misconception1']); - - expect(component.answerGroups).toEqual([]); - expect(component.defaultOutcome).toBeUndefined(); - expect(component.activeAnswerGroupIndex).toBeUndefined(); - expect(component.inapplicableSkillMisconceptionIds).toBeUndefined(); - - component.ngOnInit(); + it( + 'should set answer group and default answer when answer' + + ' groups are initialized', + () => { + let onInitializeAnswerGroupsEmitter = new EventEmitter(); + spyOnProperty( + responsesService, + 'onInitializeAnswerGroups' + ).and.returnValue(onInitializeAnswerGroupsEmitter); + spyOn(responsesService, 'changeActiveAnswerGroupIndex'); + spyOn(component, 'isCurrentInteractionLinear').and.returnValue(true); + + component.ngOnInit(); + + onInitializeAnswerGroupsEmitter.emit(interactionData); + + expect(component.defaultOutcome).toEqual(defaultOutcome); + expect(component.answerGroups).toEqual(answerGroups); + expect( + responsesService.changeActiveAnswerGroupIndex + ).toHaveBeenCalledWith(0); + + component.ngOnDestroy(); + } + ); + + it( + 'should re-initialize properties and open add answer group modal when' + + ' interaction is changed to a non-linear and non-terminal one', + () => { + let onInteractionIdChangedEmitter = new EventEmitter(); + spyOn(responsesService, 'getAnswerGroups').and.returnValue(answerGroups); + spyOn(responsesService, 'getActiveAnswerGroupIndex').and.returnValue(0); + spyOnProperty( + stateInteractionIdService, + 'onInteractionIdChanged' + ).and.returnValue(onInteractionIdChangedEmitter); + spyOn(responsesService, 'onInteractionIdChanged').and.callFake( + (options, callback) => { + // This throws "Argument of type 'null' is not assignable to + // parameter of type 'AnswerGroup[]'." We need to suppress this error + // because of the need to test validations. This throws an error + // because the value passed is null. + // @ts-ignore + callback(null, null); + } + ); + spyOn(stateEditorService, 'isInQuestionMode').and.returnValue(true); + spyOn(responsesService, 'getDefaultOutcome').and.returnValue( + defaultOutcome + ); + spyOn(component, 'openAddAnswerGroupModal'); + + expect(component.answerGroups).toEqual([]); + expect(component.defaultOutcome).toEqual(undefined); + expect(component.activeAnswerGroupIndex).toBe(undefined); + + component.ngOnInit(); + onInteractionIdChangedEmitter.emit('ImageClickInput'); + + expect(component.answerGroups).toEqual(answerGroups); + expect(component.defaultOutcome).toEqual(defaultOutcome); + expect(component.activeAnswerGroupIndex).toBe(0); + expect(component.openAddAnswerGroupModal).toHaveBeenCalled(); + + component.ngOnDestroy(); + } + ); - expect(component.inapplicableSkillMisconceptionIds) - .toEqual(['misconception1']); + it( + 'should not open add answer group modal when interaction is' + + ' changed to a linear and terminal one', + () => { + let onInteractionIdChangedEmitter = new EventEmitter(); + spyOnProperty( + stateInteractionIdService, + 'onInteractionIdChanged' + ).and.returnValue(onInteractionIdChangedEmitter); + spyOn(component, 'openAddAnswerGroupModal'); - onAnswerGroupsChangedEmitter.emit(); + component.ngOnInit(); + onInteractionIdChangedEmitter.emit('Continue'); - expect(component.answerGroups).toEqual(answerGroups); - expect(component.defaultOutcome).toEqual(defaultOutcome); - expect(component.activeAnswerGroupIndex).toBe(0); - expect(component.inapplicableSkillMisconceptionIds).toEqual([]); + expect(component.openAddAnswerGroupModal).not.toHaveBeenCalled(); - component.ngOnDestroy(); - }); + component.ngOnDestroy(); + } + ); + + it( + 'should get new answer groups, default outcome and verify/update' + + ' inapplicable skill misconception ids on answer groups change', + () => { + let onAnswerGroupsChangedEmitter = new EventEmitter(); + spyOn(responsesService, 'getAnswerGroups').and.returnValue(answerGroups); + spyOnProperty(responsesService, 'onAnswerGroupsChanged').and.returnValue( + onAnswerGroupsChangedEmitter + ); + spyOn(responsesService, 'getActiveAnswerGroupIndex').and.returnValue(0); + spyOn(responsesService, 'getDefaultOutcome').and.returnValue( + defaultOutcome + ); + spyOn( + stateEditorService, + 'getInapplicableSkillMisconceptionIds' + ).and.returnValue(['misconception1']); + + expect(component.answerGroups).toEqual([]); + expect(component.defaultOutcome).toBeUndefined(); + expect(component.activeAnswerGroupIndex).toBeUndefined(); + expect(component.inapplicableSkillMisconceptionIds).toBeUndefined(); + + component.ngOnInit(); + + expect(component.inapplicableSkillMisconceptionIds).toEqual([ + 'misconception1', + ]); + + onAnswerGroupsChangedEmitter.emit(); + + expect(component.answerGroups).toEqual(answerGroups); + expect(component.defaultOutcome).toEqual(defaultOutcome); + expect(component.activeAnswerGroupIndex).toBe(0); + expect(component.inapplicableSkillMisconceptionIds).toEqual([]); + + component.ngOnDestroy(); + } + ); it('should update answer choices', () => { let onUpdateAnswerChoicesEmitter = new EventEmitter(); - spyOnProperty(stateEditorService, 'onUpdateAnswerChoices') - .and.returnValue(onUpdateAnswerChoicesEmitter); + spyOnProperty(stateEditorService, 'onUpdateAnswerChoices').and.returnValue( + onUpdateAnswerChoicesEmitter + ); spyOn(responsesService, 'updateAnswerChoices'); component.ngOnInit(); @@ -477,8 +541,10 @@ describe('State Responses Component', () => { it('should update custom arguments', () => { let onHandleCustomArgsUpdateEmitter = new EventEmitter(); - spyOnProperty(stateEditorService, 'onHandleCustomArgsUpdate') - .and.returnValue(onHandleCustomArgsUpdateEmitter); + spyOnProperty( + stateEditorService, + 'onHandleCustomArgsUpdate' + ).and.returnValue(onHandleCustomArgsUpdateEmitter); spyOn(responsesService, 'handleCustomArgsUpdate').and.callFake( (newAnswerChoices, callback) => { // This throws "Argument of type 'null' is not assignable to @@ -501,13 +567,21 @@ describe('State Responses Component', () => { it('should set misconceptions when state editor is initialized', () => { let onStateEditorInitializedEmitter = new EventEmitter(); - spyOnProperty(stateEditorService, 'onStateEditorInitialized') - .and.returnValue(onStateEditorInitializedEmitter); - spyOn(stateEditorService, 'getMisconceptionsBySkill') - .and.returnValue({ - skill1: [misconceptionObjectFactory.create( - 1, 'Misconception 1', 'note', '', false)] - }); + spyOnProperty( + stateEditorService, + 'onStateEditorInitialized' + ).and.returnValue(onStateEditorInitializedEmitter); + spyOn(stateEditorService, 'getMisconceptionsBySkill').and.returnValue({ + skill1: [ + misconceptionObjectFactory.create( + 1, + 'Misconception 1', + 'note', + '', + false + ), + ], + }); expect(component.misconceptionsBySkill).toBe(undefined); expect(component.containsOptionalMisconceptions).toBeFalse(); @@ -516,8 +590,15 @@ describe('State Responses Component', () => { onStateEditorInitializedEmitter.emit(); expect(component.misconceptionsBySkill).toEqual({ - skill1: [misconceptionObjectFactory.create( - 1, 'Misconception 1', 'note', '', false)] + skill1: [ + misconceptionObjectFactory.create( + 1, + 'Misconception 1', + 'note', + '', + false + ), + ], }); expect(component.containsOptionalMisconceptions).toBe(true); @@ -527,8 +608,9 @@ describe('State Responses Component', () => { it('should get static image URL', () => { component.ngOnInit(); - expect(component.getStaticImageUrl('/image/url')) - .toBe('/assets/images/image/url'); + expect(component.getStaticImageUrl('/image/url')).toBe( + '/assets/images/image/url' + ); component.ngOnDestroy(); }); @@ -539,119 +621,158 @@ describe('State Responses Component', () => { expect(component.isInQuestionMode()).toBe(true); }); - it('should suppress default answer group warnings if each choice' + - ' has been handled by at least one answer group for multiple' + - ' choice interaction', () => { - // This contains 2 AnswerGroup for a MultipleChoiceInteraction. - let answerGroups = [ - answerGroupObjectFactory.createFromBackendDict({ - outcome: defaultsOutcomesToSuppressWarnings[0], - rule_specs: [{ - rule_type: 'Equals', - inputs: {x: 0} - }], - training_data: [], - tagged_skill_misconception_id: '' - }, 'MultipleChoiceInput'), - answerGroupObjectFactory.createFromBackendDict({ - outcome: defaultsOutcomesToSuppressWarnings[1], - rule_specs: [{ - rule_type: 'Equals', - inputs: {x: 1} - }], - training_data: [], - tagged_skill_misconception_id: '' - }, 'MultipleChoiceInput') - ]; - // This contains 2 AnswerChoice for MultipleChoiceInteraction. - let answerChoices = [ - { - val: 0, - label: 'label1' - }, - { - val: 1, - label: 'label2' - } - ]; - stateInteractionIdService.savedMemento = 'MultipleChoiceInput'; - spyOn(responsesService, 'getAnswerGroups').and.returnValue(answerGroups); - spyOn(responsesService, 'getAnswerChoices').and.returnValue(answerChoices); - - expect(component.shouldHideDefaultAnswerGroup()).toBe(true); - }); - - it('should suppress default answer group warnings if each choice' + - ' has been handled by at least one answer group for item' + - ' selection input interaction', () => { - // This contains 2 AnswerGroup for a ItemSelectionInput. - let answerGroups = [ - answerGroupObjectFactory.createFromBackendDict({ - outcome: defaultsOutcomesToSuppressWarnings[0], - rule_specs: [{ - rule_type: 'Equals', - inputs: {x: ['ca_0', 'ca_1']} - }], - training_data: [], - tagged_skill_misconception_id: '' - }, 'ItemSelectionInput'), - answerGroupObjectFactory.createFromBackendDict({ - outcome: defaultsOutcomesToSuppressWarnings[1], - rule_specs: [{ - rule_type: 'DoesNotContainAtLeastOneOf', - inputs: {x: ['ca_0', 'ca_1', 'ca_2']} - }], - training_data: [], - tagged_skill_misconception_id: '' - }, 'ItemSelectionInput') - ]; - // This contains 2 AnswerChoice for ItemSelectionInput. - let answerChoices = [ - { - val: new SubtitledHtml('

Choice 1

', 'ca_choices_3'), - label: '' - } - ]; - stateInteractionIdService.savedMemento = 'ItemSelectionInput'; - stateCustomizationArgsService.savedMemento = { - maxAllowableSelectionCount: { - value: 1 - } - }; - spyOn(responsesService, 'getAnswerGroups').and.returnValue(answerGroups); - spyOn(responsesService, 'getAnswerChoices').and.returnValue( - answerChoices as AnswerChoice[]); + it( + 'should suppress default answer group warnings if each choice' + + ' has been handled by at least one answer group for multiple' + + ' choice interaction', + () => { + // This contains 2 AnswerGroup for a MultipleChoiceInteraction. + let answerGroups = [ + answerGroupObjectFactory.createFromBackendDict( + { + outcome: defaultsOutcomesToSuppressWarnings[0], + rule_specs: [ + { + rule_type: 'Equals', + inputs: {x: 0}, + }, + ], + training_data: [], + tagged_skill_misconception_id: '', + }, + 'MultipleChoiceInput' + ), + answerGroupObjectFactory.createFromBackendDict( + { + outcome: defaultsOutcomesToSuppressWarnings[1], + rule_specs: [ + { + rule_type: 'Equals', + inputs: {x: 1}, + }, + ], + training_data: [], + tagged_skill_misconception_id: '', + }, + 'MultipleChoiceInput' + ), + ]; + // This contains 2 AnswerChoice for MultipleChoiceInteraction. + let answerChoices = [ + { + val: 0, + label: 'label1', + }, + { + val: 1, + label: 'label2', + }, + ]; + stateInteractionIdService.savedMemento = 'MultipleChoiceInput'; + spyOn(responsesService, 'getAnswerGroups').and.returnValue(answerGroups); + spyOn(responsesService, 'getAnswerChoices').and.returnValue( + answerChoices + ); + + expect(component.shouldHideDefaultAnswerGroup()).toBe(true); + } + ); + + it( + 'should suppress default answer group warnings if each choice' + + ' has been handled by at least one answer group for item' + + ' selection input interaction', + () => { + // This contains 2 AnswerGroup for a ItemSelectionInput. + let answerGroups = [ + answerGroupObjectFactory.createFromBackendDict( + { + outcome: defaultsOutcomesToSuppressWarnings[0], + rule_specs: [ + { + rule_type: 'Equals', + inputs: {x: ['ca_0', 'ca_1']}, + }, + ], + training_data: [], + tagged_skill_misconception_id: '', + }, + 'ItemSelectionInput' + ), + answerGroupObjectFactory.createFromBackendDict( + { + outcome: defaultsOutcomesToSuppressWarnings[1], + rule_specs: [ + { + rule_type: 'DoesNotContainAtLeastOneOf', + inputs: {x: ['ca_0', 'ca_1', 'ca_2']}, + }, + ], + training_data: [], + tagged_skill_misconception_id: '', + }, + 'ItemSelectionInput' + ), + ]; + // This contains 2 AnswerChoice for ItemSelectionInput. + let answerChoices = [ + { + val: new SubtitledHtml('

Choice 1

', 'ca_choices_3'), + label: '', + }, + ]; + stateInteractionIdService.savedMemento = 'ItemSelectionInput'; + stateCustomizationArgsService.savedMemento = { + maxAllowableSelectionCount: { + value: 1, + }, + }; + spyOn(responsesService, 'getAnswerGroups').and.returnValue(answerGroups); + spyOn(responsesService, 'getAnswerChoices').and.returnValue( + answerChoices as AnswerChoice[] + ); - expect(component.shouldHideDefaultAnswerGroup()).toBe(true); - }); + expect(component.shouldHideDefaultAnswerGroup()).toBe(true); + } + ); - it('should not suppress warnings for interactions other than multiple' + - ' choice input or item selection input', () => { - stateInteractionIdService.savedMemento = 'TextInput'; + it( + 'should not suppress warnings for interactions other than multiple' + + ' choice input or item selection input', + () => { + stateInteractionIdService.savedMemento = 'TextInput'; - expect(component.shouldHideDefaultAnswerGroup()).toBe(false); - }); + expect(component.shouldHideDefaultAnswerGroup()).toBe(false); + } + ); - it('should save displayed value when solicit answer details' + - ' are changed', () => { - spyOn(stateSolicitAnswerDetailsService, 'saveDisplayedValue'); - spyOn(component.onSaveSolicitAnswerDetails, 'emit').and.stub(); - component.ngOnInit(); + it( + 'should save displayed value when solicit answer details' + ' are changed', + () => { + spyOn(stateSolicitAnswerDetailsService, 'saveDisplayedValue'); + spyOn(component.onSaveSolicitAnswerDetails, 'emit').and.stub(); + component.ngOnInit(); - component.onChangeSolicitAnswerDetails(); + component.onChangeSolicitAnswerDetails(); - expect(component.onSaveSolicitAnswerDetails.emit).toHaveBeenCalled(); - expect(stateSolicitAnswerDetailsService.saveDisplayedValue) - .toHaveBeenCalled(); - }); + expect(component.onSaveSolicitAnswerDetails.emit).toHaveBeenCalled(); + expect( + stateSolicitAnswerDetailsService.saveDisplayedValue + ).toHaveBeenCalled(); + } + ); it('should check if outcome has no feedback with self loop', () => { spyOn(stateEditorService, 'getActiveStateName').and.returnValue( - 'State Name'); - let outcome1 = outcomeObjectFactory.createNew( - 'State Name', '1', '', []); + 'State Name' + ); + let outcome1 = outcomeObjectFactory.createNew('State Name', '1', '', []); let outcome2 = outcomeObjectFactory.createNew( - 'State Name', '1', 'Feedback Text', []); + 'State Name', + '1', + 'Feedback Text', + [] + ); expect(component.isSelfLoopWithNoFeedback(outcome1)).toBe(true); expect(component.isSelfLoopWithNoFeedback(outcome2)).toBe(false); @@ -671,40 +792,46 @@ describe('State Responses Component', () => { dest_if_really_stuck: null, feedback: { content_id: '', - html: '' + html: '', }, labelled_as_correct: true, param_changes: [], refresher_exploration_id: 'test', - missing_prerequisite_skill_id: 'test_skill_id' + missing_prerequisite_skill_id: 'test_skill_id', }); spyOn(stateEditorService, 'getActiveStateName').and.returnValues( - 'State Name', 'Hola'); + 'State Name', + 'Hola' + ); expect(component.isSelfLoopThatIsMarkedCorrect(outcome)).toBe(true); expect(component.isSelfLoopThatIsMarkedCorrect(outcome)).toBe(false); }); - it('should check if outcome marked as correct has self loop and return' + - ' true if correctness feedback is enabled', () => { - let outcome = outcomeObjectFactory.createFromBackendDict({ - dest: 'State Name', - dest_if_really_stuck: null, - feedback: { - content_id: '', - html: '' - }, - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: 'test', - missing_prerequisite_skill_id: 'test_skill_id' - }); - spyOn(stateEditorService, 'getActiveStateName').and.returnValue( - 'State Name'); + it( + 'should check if outcome marked as correct has self loop and return' + + ' true if correctness feedback is enabled', + () => { + let outcome = outcomeObjectFactory.createFromBackendDict({ + dest: 'State Name', + dest_if_really_stuck: null, + feedback: { + content_id: '', + html: '', + }, + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: 'test', + missing_prerequisite_skill_id: 'test_skill_id', + }); + spyOn(stateEditorService, 'getActiveStateName').and.returnValue( + 'State Name' + ); - expect(component.isSelfLoopThatIsMarkedCorrect(outcome)).toBe(true); - }); + expect(component.isSelfLoopThatIsMarkedCorrect(outcome)).toBe(true); + } + ); it('should show state name input if user is creating new state', () => { let outcome1 = outcomeObjectFactory.createNew('/', '', '', []); @@ -753,32 +880,36 @@ describe('State Responses Component', () => { dest_if_really_stuck: null, feedback: { content_id: '', - html: '' + html: '', }, labelled_as_correct: true, param_changes: [], refresher_exploration_id: 'test', - missing_prerequisite_skill_id: 'test_skill_id' + missing_prerequisite_skill_id: 'test_skill_id', }); spyOn(stateEditorService, 'getActiveStateName').and.returnValue( - 'State Name'); + 'State Name' + ); expect(component.getOutcomeTooltip(outcome)).toBe( - 'Self-loops should not be labelled as correct.'); + 'Self-loops should not be labelled as correct.' + ); // When interaction is linear with no feedback. stateInteractionIdService.savedMemento = 'Continue'; let outcome1 = outcomeObjectFactory.createNew('Hola', '', '', []); expect(component.getOutcomeTooltip(outcome1)).toBe( - 'Please direct the learner to a different card.'); + 'Please direct the learner to a different card.' + ); // When interaction is not linear. stateInteractionIdService.savedMemento = 'TextInput'; expect(component.getOutcomeTooltip(outcome1)).toBe( 'Please give Oppia something useful to say,' + - ' or direct the learner to a different card.'); + ' or direct the learner to a different card.' + ); }); it('should open openAddAnswerGroupModal', fakeAsync(() => { @@ -787,30 +918,36 @@ describe('State Responses Component', () => { spyOn(externalSaveService.onExternalSave, 'emit').and.stub(); spyOn(alertsService, 'clearWarnings').and.stub(); spyOn(answerGroupObjectFactory, 'createNew').and.returnValue( - answerGroupObjectFactory - .createFromBackendDict({ - rule_specs: [{ - rule_type: 'Contains', - inputs: {x: { - contentId: 'rule_input', - normalizedStrSet: ['abc'] - }} - }], + answerGroupObjectFactory.createFromBackendDict( + { + rule_specs: [ + { + rule_type: 'Contains', + inputs: { + x: { + contentId: 'rule_input', + normalizedStrSet: ['abc'], + }, + }, + }, + ], outcome: { dest: 'State', dest_if_really_stuck: null, feedback: { html: '', - content_id: 'This is a new feedback text' + content_id: 'This is a new feedback text', }, labelled_as_correct: false, param_changes: [], refresher_exploration_id: 'test', - missing_prerequisite_skill_id: 'test_skill_id' + missing_prerequisite_skill_id: 'test_skill_id', }, training_data: [], - tagged_skill_misconception_id: 'misconception1' - }, 'TextInput') + tagged_skill_misconception_id: 'misconception1', + }, + 'TextInput' + ) ); stateInteractionIdService.savedMemento = 'MultipleChoiceInput'; spyOn(stateEditorService, 'getActiveStateName').and.returnValue('none'); @@ -819,56 +956,73 @@ describe('State Responses Component', () => { callback(answerGroups, defaultOutcome); } ); - spyOn(ngbModal, 'open').and.returnValues({ - componentInstance: { - addState: { - subscribe(value: () => void) { - value(); - } + spyOn(ngbModal, 'open').and.returnValues( + { + componentInstance: { + addState: { + subscribe(value: () => void) { + value(); + }, + }, + currentInteractionId: 'currentInteractionId', + stateName: 'stateName', }, - currentInteractionId: 'currentInteractionId', - stateName: 'stateName' - }, - result: Promise.resolve({ - reopen: true, - tmpRule: new Rule('', { - x: { - contentId: null, - normalizedStrSet: [] - } - }, { - x: 'TranslatableSetOfNormalizedString' + result: Promise.resolve({ + reopen: true, + tmpRule: new Rule( + '', + { + x: { + contentId: null, + normalizedStrSet: [], + }, + }, + { + x: 'TranslatableSetOfNormalizedString', + } + ), + tmpOutcome: outcomeObjectFactory.createNew( + 'Hola', + '1', + 'Feedback text', + [] + ), + tmpTaggedSkillMisconceptionId: '', }), - tmpOutcome: outcomeObjectFactory - .createNew('Hola', '1', 'Feedback text', []), - tmpTaggedSkillMisconceptionId: '' - }) - } as NgbModalRef, - { - componentInstance: { - addState: { - subscribe(value: () => void) { - value(); - } + } as NgbModalRef, + { + componentInstance: { + addState: { + subscribe(value: () => void) { + value(); + }, + }, + currentInteractionId: 'currentInteractionId', + stateName: 'stateName', }, - currentInteractionId: 'currentInteractionId', - stateName: 'stateName' - }, - result: Promise.resolve({ - reopen: false, - tmpRule: new Rule('', { - x: { - contentId: null, - normalizedStrSet: [] - } - }, { - x: 'TranslatableSetOfNormalizedString' + result: Promise.resolve({ + reopen: false, + tmpRule: new Rule( + '', + { + x: { + contentId: null, + normalizedStrSet: [], + }, + }, + { + x: 'TranslatableSetOfNormalizedString', + } + ), + tmpOutcome: outcomeObjectFactory.createNew( + 'Hola', + '1', + 'Feedback text', + [] + ), + tmpTaggedSkillMisconceptionId: '', }), - tmpOutcome: outcomeObjectFactory - .createNew('Hola', '1', 'Feedback text', []), - tmpTaggedSkillMisconceptionId: '' - }) - } as NgbModalRef + } as NgbModalRef ); component.openAddAnswerGroupModal(); @@ -884,12 +1038,12 @@ describe('State Responses Component', () => { addState: { subscribe(value: () => void) { return; - } + }, }, currentInteractionId: 'currentInteractionId', - stateName: 'stateName' + stateName: 'stateName', }, - result: Promise.reject() + result: Promise.reject(), } as NgbModalRef); component.openAddAnswerGroupModal(); @@ -897,16 +1051,19 @@ describe('State Responses Component', () => { expect(ngbModal.open).toHaveBeenCalled(); }); - it('should open delete answer group modal when user clicks' + - ' on delete button', () => { - spyOn(ngbModal, 'open').and.callThrough(); + it( + 'should open delete answer group modal when user clicks' + + ' on delete button', + () => { + spyOn(ngbModal, 'open').and.callThrough(); - const event = new Event(''); + const event = new Event(''); - component.deleteAnswerGroup(event, 0); + component.deleteAnswerGroup(event, 0); - expect(ngbModal.open).toHaveBeenCalled(); - }); + expect(ngbModal.open).toHaveBeenCalled(); + } + ); it('should delete answer group after modal is opened', fakeAsync(() => { spyOn(responsesService, 'deleteAnswerGroup').and.callFake( @@ -917,12 +1074,11 @@ describe('State Responses Component', () => { // because the callback is called with null as an argument. // @ts-ignore callback(null); - }); - spyOn(ngbModal, 'open').and.returnValue( - { - result: Promise.resolve() - } as NgbModalRef + } ); + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.resolve(), + } as NgbModalRef); const event = new Event(''); @@ -930,17 +1086,14 @@ describe('State Responses Component', () => { tick(); expect(ngbModal.open).toHaveBeenCalled(); - expect(responsesService.deleteAnswerGroup) - .toHaveBeenCalled(); + expect(responsesService.deleteAnswerGroup).toHaveBeenCalled(); })); it('should clear warnings when delete answer group modal is closed', () => { spyOn(alertsService, 'clearWarnings'); - spyOn(ngbModal, 'open').and.returnValue( - { - result: Promise.reject() - } as NgbModalRef - ); + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.reject(), + } as NgbModalRef); const event = new Event(''); @@ -962,12 +1115,10 @@ describe('State Responses Component', () => { ); spyOn(component.onSaveInteractionAnswerGroups, 'emit').and.stub(); - component.saveTaggedMisconception( - { - misconceptionId: 1, - skillId: 'skill1' - } - ); + component.saveTaggedMisconception({ + misconceptionId: 1, + skillId: 'skill1', + }); expect(component.onSaveInteractionAnswerGroups.emit).toHaveBeenCalled(); }); @@ -991,15 +1142,16 @@ describe('State Responses Component', () => { }); it('should update active answer group when destination is changed', () => { - spyOn(responsesService, 'updateActiveAnswerGroup') - .and.callFake((dest, callback) => { + spyOn(responsesService, 'updateActiveAnswerGroup').and.callFake( + (dest, callback) => { // This throws "Argument of type 'null' is not assignable to // parameter of type 'AnswerGroup[]'." We need to suppress this error // because of the need to test validations. This throws an error // because the callback is called with null as an argument. // @ts-ignore callback(null); - }); + } + ); spyOn(component.onSaveInteractionAnswerGroups, 'emit').and.stub(); component.saveActiveAnswerGroupDest(defaultOutcome); @@ -1008,26 +1160,8 @@ describe('State Responses Component', () => { }); it('should update active answer group when destination is changed', () => { - spyOn(responsesService, 'updateActiveAnswerGroup') - .and.callFake((destIfReallyStuck, callback) => { - // This throws "Argument of type 'null' is not assignable to - // parameter of type 'AnswerGroup[]'." We need to suppress this error - // because of the need to test validations. This throws an error - // because the callback is called with null as an argument. - // @ts-ignore - callback(null); - }); - spyOn(component.onSaveInteractionAnswerGroups, 'emit').and.stub(); - - component.saveActiveAnswerGroupDestIfStuck(defaultOutcome); - - expect(component.onSaveInteractionAnswerGroups.emit).toHaveBeenCalled(); - }); - - it('should update active answer group when correctness' + - ' label is changed', () => { spyOn(responsesService, 'updateActiveAnswerGroup').and.callFake( - (labelledAsCorrect, callback) => { + (destIfReallyStuck, callback) => { // This throws "Argument of type 'null' is not assignable to // parameter of type 'AnswerGroup[]'." We need to suppress this error // because of the need to test validations. This throws an error @@ -1038,11 +1172,31 @@ describe('State Responses Component', () => { ); spyOn(component.onSaveInteractionAnswerGroups, 'emit').and.stub(); - component.saveActiveAnswerGroupCorrectnessLabel(defaultOutcome); + component.saveActiveAnswerGroupDestIfStuck(defaultOutcome); expect(component.onSaveInteractionAnswerGroups.emit).toHaveBeenCalled(); }); + it( + 'should update active answer group when correctness' + ' label is changed', + () => { + spyOn(responsesService, 'updateActiveAnswerGroup').and.callFake( + (labelledAsCorrect, callback) => { + // This throws "Argument of type 'null' is not assignable to + // parameter of type 'AnswerGroup[]'." We need to suppress this error + // because of the need to test validations. This throws an error + // because the callback is called with null as an argument. + // @ts-ignore + callback(null); + } + ); + spyOn(component.onSaveInteractionAnswerGroups, 'emit').and.stub(); + + component.saveActiveAnswerGroupCorrectnessLabel(defaultOutcome); + + expect(component.onSaveInteractionAnswerGroups.emit).toHaveBeenCalled(); + } + ); it('should update active answer group when answer rules are changed', () => { spyOn(responsesService, 'updateActiveAnswerGroup').and.callFake( @@ -1057,166 +1211,225 @@ describe('State Responses Component', () => { ); spyOn(component.onSaveInteractionAnswerGroups, 'emit').and.stub(); - component.saveActiveAnswerGroupRules([new Rule('', { - x: { - contentId: null, - normalizedStrSet: [] - } - }, { - x: 'TranslatableSetOfNormalizedString' - })]); + component.saveActiveAnswerGroupRules([ + new Rule( + '', + { + x: { + contentId: null, + normalizedStrSet: [], + }, + }, + { + x: 'TranslatableSetOfNormalizedString', + } + ), + ]); expect(component.onSaveInteractionAnswerGroups.emit).toHaveBeenCalled(); }); + it( + 'should update default outcome when default' + + ' outcome feedback is changed', + () => { + spyOn(responsesService, 'updateDefaultOutcome').and.callFake( + ({feedback, dest}, callback) => { + // This throws "Argument of type 'null' is not assignable to + // parameter of type 'AnswerGroup[]'." We need to suppress this error + // because of the need to test validations. This throws an error + // because the callback is called with null as an argument. + // @ts-ignore + callback(null); + } + ); + spyOn(component.onSaveInteractionDefaultOutcome, 'emit').and.stub(); - it('should update default outcome when default' + - ' outcome feedback is changed', () => { - spyOn(responsesService, 'updateDefaultOutcome').and.callFake( - ({feedback, dest}, callback) => { - // This throws "Argument of type 'null' is not assignable to - // parameter of type 'AnswerGroup[]'." We need to suppress this error - // because of the need to test validations. This throws an error - // because the callback is called with null as an argument. - // @ts-ignore - callback(null); - } - ); - spyOn(component.onSaveInteractionDefaultOutcome, 'emit').and.stub(); - - component.saveDefaultOutcomeFeedback(defaultOutcome); - - expect(component.onSaveInteractionDefaultOutcome.emit).toHaveBeenCalled(); - }); - - it('should update default outcome when default' + - ' outcome destination is changed', () => { - spyOn(responsesService, 'updateDefaultOutcome') - .and.callFake((dest, callback) => { - // This throws "Argument of type 'null' is not assignable to - // parameter of type 'AnswerGroup[]'." We need to suppress this error - // because of the need to test validations. This throws an error - // because the callback is called with null as an argument. - // @ts-ignore - callback(null); - }); - spyOn(component.onSaveInteractionDefaultOutcome, 'emit').and.stub(); - - component.saveDefaultOutcomeDest(defaultOutcome); + component.saveDefaultOutcomeFeedback(defaultOutcome); - expect(component.onSaveInteractionDefaultOutcome.emit).toHaveBeenCalled(); - }); + expect(component.onSaveInteractionDefaultOutcome.emit).toHaveBeenCalled(); + } + ); + + it( + 'should update default outcome when default' + + ' outcome destination is changed', + () => { + spyOn(responsesService, 'updateDefaultOutcome').and.callFake( + (dest, callback) => { + // This throws "Argument of type 'null' is not assignable to + // parameter of type 'AnswerGroup[]'." We need to suppress this error + // because of the need to test validations. This throws an error + // because the callback is called with null as an argument. + // @ts-ignore + callback(null); + } + ); + spyOn(component.onSaveInteractionDefaultOutcome, 'emit').and.stub(); - it('should update default outcome when default' + - ' outcome destination for stuck learner is changed', () => { - spyOn(responsesService, 'updateDefaultOutcome') - .and.callFake((destIfReallyStuck, callback) => { - // This throws "Argument of type 'null' is not assignable to - // parameter of type 'AnswerGroup[]'." We need to suppress this error - // because of the need to test validations. This throws an error - // because the callback is called with null as an argument. - // @ts-ignore - callback(null); - }); - spyOn(component.onSaveInteractionDefaultOutcome, 'emit').and.stub(); + component.saveDefaultOutcomeDest(defaultOutcome); - component.saveDefaultOutcomeDestIfStuck(defaultOutcome); + expect(component.onSaveInteractionDefaultOutcome.emit).toHaveBeenCalled(); + } + ); + + it( + 'should update default outcome when default' + + ' outcome destination for stuck learner is changed', + () => { + spyOn(responsesService, 'updateDefaultOutcome').and.callFake( + (destIfReallyStuck, callback) => { + // This throws "Argument of type 'null' is not assignable to + // parameter of type 'AnswerGroup[]'." We need to suppress this error + // because of the need to test validations. This throws an error + // because the callback is called with null as an argument. + // @ts-ignore + callback(null); + } + ); + spyOn(component.onSaveInteractionDefaultOutcome, 'emit').and.stub(); - expect(component.onSaveInteractionDefaultOutcome.emit).toHaveBeenCalled(); - }); + component.saveDefaultOutcomeDestIfStuck(defaultOutcome); - it('should update default outcome when default' + - ' outcome correctness label is changed', () => { - spyOn(responsesService, 'updateDefaultOutcome').and.callFake( - ({labelledAsCorrect}, callback) => { - // This throws "Argument of type 'null' is not assignable to - // parameter of type 'AnswerGroup[]'." We need to suppress this error - // because of the need to test validations. This throws an error - // because the callback is called with null as an argument. - // @ts-ignore - callback(null); - } - ); - spyOn(component.onSaveInteractionDefaultOutcome, 'emit').and.stub(); + expect(component.onSaveInteractionDefaultOutcome.emit).toHaveBeenCalled(); + } + ); + + it( + 'should update default outcome when default' + + ' outcome correctness label is changed', + () => { + spyOn(responsesService, 'updateDefaultOutcome').and.callFake( + ({labelledAsCorrect}, callback) => { + // This throws "Argument of type 'null' is not assignable to + // parameter of type 'AnswerGroup[]'." We need to suppress this error + // because of the need to test validations. This throws an error + // because the callback is called with null as an argument. + // @ts-ignore + callback(null); + } + ); + spyOn(component.onSaveInteractionDefaultOutcome, 'emit').and.stub(); - component.saveDefaultOutcomeCorrectnessLabel(defaultOutcome); + component.saveDefaultOutcomeCorrectnessLabel(defaultOutcome); - expect(component.onSaveInteractionDefaultOutcome.emit).toHaveBeenCalled(); - }); + expect(component.onSaveInteractionDefaultOutcome.emit).toHaveBeenCalled(); + } + ); it('should get answer choices', () => { const answerChoices = [ { val: 0, - label: 'label1' + label: 'label1', }, { val: 1, - label: 'label2' - } + label: 'label2', + }, ]; spyOn(responsesService, 'getAnswerChoices').and.returnValue(answerChoices); - expect(component.getAnswerChoices()) - .toEqual(answerChoices); + expect(component.getAnswerChoices()).toEqual(answerChoices); }); it('should return summary of answer group', () => { - expect(component.summarizeAnswerGroup( - answerGroupObjectFactory.createNew( + expect( + component.summarizeAnswerGroup( + answerGroupObjectFactory.createNew( + [], + outcomeObjectFactory.createNew('unused', '1', 'Feedback text', []), + [], + '0' + ), + '1', [], - outcomeObjectFactory.createNew('unused', '1', 'Feedback text', []), - [], '0'), '1', [], true)) - .toBe('[] Feedback text'); - - expect(component.summarizeAnswerGroup( - answerGroupObjectFactory.createNew( + true + ) + ).toBe('[] Feedback text'); + + expect( + component.summarizeAnswerGroup( + answerGroupObjectFactory.createNew( + [], + outcomeObjectFactory.createNew('unused', '1', 'Feedback text', []), + [], + '0' + ), + '1', [], - outcomeObjectFactory.createNew('unused', '1', 'Feedback text', []), - [], '0'), '1', [], false)) - .toBe('[Answer ] Feedback text'); + false + ) + ).toBe('[Answer ] Feedback text'); }); it('should get summary default outcome when outcome is linear', () => { - expect(component.summarizeDefaultOutcome( - outcomeObjectFactory.createNew( - 'unused', '1', 'Feedback Text', []), 'Continue', 0, true)) - .toBe('[] Feedback Text'); + expect( + component.summarizeDefaultOutcome( + outcomeObjectFactory.createNew('unused', '1', 'Feedback Text', []), + 'Continue', + 0, + true + ) + ).toBe('[] Feedback Text'); }); - it('should get summary default outcome when answer group count' + - ' is greater than 0', () => { - expect(component.summarizeDefaultOutcome( - outcomeObjectFactory.createNew( - 'unused', '1', 'Feedback Text', []), 'TextInput', 1, true)) - .toBe('[] Feedback Text'); - }); - - it('should get summary default outcome when answer group count' + - ' is equal to 0', () => { - expect(component.summarizeDefaultOutcome( - outcomeObjectFactory.createNew( - 'unused', '1', 'Feedback Text', []), 'TextInput', 0, true)) - .toBe('[] Feedback Text'); - }); - - it('should get an empty summary when default outcome' + - ' is a falsy value', () => { - // This throws "Argument of type 'null' is not assignable to parameter of - // type 'Outcome'." We need to suppress this error because of the need to - // test validations. This throws an error because the callback is called - // with null as an argument. - // @ts-ignore - expect(component.summarizeDefaultOutcome(null, 'Continue', 0, true)) - .toBe(''); - }); + it( + 'should get summary default outcome when answer group count' + + ' is greater than 0', + () => { + expect( + component.summarizeDefaultOutcome( + outcomeObjectFactory.createNew('unused', '1', 'Feedback Text', []), + 'TextInput', + 1, + true + ) + ).toBe('[] Feedback Text'); + } + ); + + it( + 'should get summary default outcome when answer group count' + + ' is equal to 0', + () => { + expect( + component.summarizeDefaultOutcome( + outcomeObjectFactory.createNew('unused', '1', 'Feedback Text', []), + 'TextInput', + 0, + true + ) + ).toBe('[] Feedback Text'); + } + ); + + it( + 'should get an empty summary when default outcome' + ' is a falsy value', + () => { + // This throws "Argument of type 'null' is not assignable to parameter of + // type 'Outcome'." We need to suppress this error because of the need to + // test validations. This throws an error because the callback is called + // with null as an argument. + // @ts-ignore + expect(component.summarizeDefaultOutcome(null, 'Continue', 0, true)).toBe( + '' + ); + } + ); it('should check if outcome is looping', () => { spyOn(stateEditorService, 'getActiveStateName').and.returnValue('Hola'); - expect(component.isOutcomeLooping(outcomeObjectFactory.createNew( - 'Hola', '', '', []))).toBe(true); - expect(component.isOutcomeLooping(outcomeObjectFactory.createNew( - 'Second Last', '', '', []))).toBe(false); + expect( + component.isOutcomeLooping( + outcomeObjectFactory.createNew('Hola', '', '', []) + ) + ).toBe(true); + expect( + component.isOutcomeLooping( + outcomeObjectFactory.createNew('Second Last', '', '', []) + ) + ).toBe(false); }); it('should toggle response card', () => { @@ -1244,37 +1457,56 @@ describe('State Responses Component', () => { expect(component.isNoActionExpected('misconceptions')).toBe(false); }); - it('should update optional misconception id status when user' + - ' marks it as applicable', () => { - component.inapplicableSkillMisconceptionIds = ['misconception1']; + it( + 'should update optional misconception id status when user' + + ' marks it as applicable', + () => { + component.inapplicableSkillMisconceptionIds = ['misconception1']; - component.updateOptionalMisconceptionIdStatus('misconception1', true); + component.updateOptionalMisconceptionIdStatus('misconception1', true); - expect(component.inapplicableSkillMisconceptionIds).toEqual([]); - }); + expect(component.inapplicableSkillMisconceptionIds).toEqual([]); + } + ); - it('should update optional misconception id status when user' + - ' marks it as applicable', () => { - component.inapplicableSkillMisconceptionIds = ['misconception1']; + it( + 'should update optional misconception id status when user' + + ' marks it as applicable', + () => { + component.inapplicableSkillMisconceptionIds = ['misconception1']; - component.updateOptionalMisconceptionIdStatus('misconception2', false); + component.updateOptionalMisconceptionIdStatus('misconception2', false); - expect(component.inapplicableSkillMisconceptionIds) - .toEqual(['misconception1', 'misconception2']); - }); + expect(component.inapplicableSkillMisconceptionIds).toEqual([ + 'misconception1', + 'misconception2', + ]); + } + ); it('should get unaddressed misconception names', () => { spyOn(responsesService, 'getAnswerGroups').and.returnValue(answerGroups); component.misconceptionsBySkill = { skill1: [ misconceptionObjectFactory.create( - 1, 'Misconception 1', 'note', '', false), + 1, + 'Misconception 1', + 'note', + '', + false + ), misconceptionObjectFactory.create( - 2, 'Misconception 2', 'note', '', true) - ] + 2, + 'Misconception 2', + 'note', + '', + true + ), + ], }; - expect(component.getUnaddressedMisconceptionNames()) - .toEqual(['Misconception 2']); + expect(component.getUnaddressedMisconceptionNames()).toEqual([ + 'Misconception 2', + ]); }); }); diff --git a/core/templates/components/state-editor/state-responses-editor/state-responses.component.ts b/core/templates/components/state-editor/state-responses-editor/state-responses.component.ts index 772d35deae87..73b897cb2bde 100644 --- a/core/templates/components/state-editor/state-responses-editor/state-responses.component.ts +++ b/core/templates/components/state-editor/state-responses-editor/state-responses.component.ts @@ -17,39 +17,55 @@ * editor. */ -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { AddAnswerGroupModalComponent } from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/add-answer-group-modal.component'; -import { DeleteAnswerGroupModalComponent } from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-answer-group-modal.component'; -import { Misconception, MisconceptionSkillMap, TaggedMisconception } from 'domain/skill/MisconceptionObjectFactory'; -import { Subscription } from 'rxjs'; -import { AnswerChoice, StateEditorService } from '../state-editor-properties-services/state-editor.service'; -import { ResponsesService } from 'pages/exploration-editor-page/editor-tab/services/responses.service'; -import { StateSolicitAnswerDetailsService } from '../state-editor-properties-services/state-solicit-answer-details.service'; -import { ExternalSaveService } from 'services/external-save.service'; -import { StateInteractionIdService } from '../state-editor-properties-services/state-interaction-id.service'; -import { AppConstants } from 'app.constants'; +import { + Component, + EventEmitter, + Input, + OnDestroy, + OnInit, + Output, +} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {AddAnswerGroupModalComponent} from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/add-answer-group-modal.component'; +import {DeleteAnswerGroupModalComponent} from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-answer-group-modal.component'; +import { + Misconception, + MisconceptionSkillMap, + TaggedMisconception, +} from 'domain/skill/MisconceptionObjectFactory'; +import {Subscription} from 'rxjs'; +import { + AnswerChoice, + StateEditorService, +} from '../state-editor-properties-services/state-editor.service'; +import {ResponsesService} from 'pages/exploration-editor-page/editor-tab/services/responses.service'; +import {StateSolicitAnswerDetailsService} from '../state-editor-properties-services/state-solicit-answer-details.service'; +import {ExternalSaveService} from 'services/external-save.service'; +import {StateInteractionIdService} from '../state-editor-properties-services/state-interaction-id.service'; +import {AppConstants} from 'app.constants'; import INTERACTION_SPECS from 'interactions/interaction_specs.json'; -import { Outcome } from 'domain/exploration/OutcomeObjectFactory'; -import { AlertsService } from 'services/alerts.service'; -import { AnswerGroup, AnswerGroupObjectFactory } from 'domain/exploration/AnswerGroupObjectFactory'; -import { Interaction } from 'domain/exploration/InteractionObjectFactory'; -import { Rule } from 'domain/exploration/rule.model'; -import { ParameterizeRuleDescriptionPipe } from 'filters/parameterize-rule-description.pipe'; -import { ConvertToPlainTextPipe } from 'filters/string-utility-filters/convert-to-plain-text.pipe'; -import { TruncatePipe } from 'filters/string-utility-filters/truncate.pipe'; -import { WrapTextWithEllipsisPipe } from 'filters/string-utility-filters/wrap-text-with-ellipsis.pipe'; -import { CdkDragSortEvent, moveItemInArray} from '@angular/cdk/drag-drop'; -import { EditabilityService } from 'services/editability.service'; -import { GenerateContentIdService } from 'services/generate-content-id.service'; -import { InteractionSpecsKey } from 'pages/interaction-specs.constants'; - +import {Outcome} from 'domain/exploration/OutcomeObjectFactory'; +import {AlertsService} from 'services/alerts.service'; +import { + AnswerGroup, + AnswerGroupObjectFactory, +} from 'domain/exploration/AnswerGroupObjectFactory'; +import {Interaction} from 'domain/exploration/InteractionObjectFactory'; +import {Rule} from 'domain/exploration/rule.model'; +import {ParameterizeRuleDescriptionPipe} from 'filters/parameterize-rule-description.pipe'; +import {ConvertToPlainTextPipe} from 'filters/string-utility-filters/convert-to-plain-text.pipe'; +import {TruncatePipe} from 'filters/string-utility-filters/truncate.pipe'; +import {WrapTextWithEllipsisPipe} from 'filters/string-utility-filters/wrap-text-with-ellipsis.pipe'; +import {CdkDragSortEvent, moveItemInArray} from '@angular/cdk/drag-drop'; +import {EditabilityService} from 'services/editability.service'; +import {GenerateContentIdService} from 'services/generate-content-id.service'; +import {InteractionSpecsKey} from 'pages/interaction-specs.constants'; @Component({ selector: 'oppia-state-responses', - templateUrl: './state-responses.component.html' + templateUrl: './state-responses.component.html', }) export class StateResponsesComponent implements OnInit, OnDestroy { // These properties are initialized using Angular lifecycle hooks @@ -64,19 +80,21 @@ export class StateResponsesComponent implements OnInit, OnDestroy { // State name is null if their is no state selected or have no active state. // This is the case when the user is creating a new state. @Output() onResponsesInitialized = new EventEmitter(); - @Output() onSaveInteractionAnswerGroups = ( - new EventEmitter()); + @Output() onSaveInteractionAnswerGroups = new EventEmitter< + AnswerGroup[] | AnswerGroup + >(); - @Output() onSaveInteractionDefaultOutcome = ( - new EventEmitter()); + @Output() onSaveInteractionDefaultOutcome = + new EventEmitter(); @Output() onSaveNextContentIdIndex = new EventEmitter(); @Output() onSaveSolicitAnswerDetails = new EventEmitter(); @Output() navigateToState = new EventEmitter(); @Output() refreshWarnings = new EventEmitter(); - @Output() onSaveInapplicableSkillMisconceptionIds = ( - new EventEmitter()); + @Output() onSaveInapplicableSkillMisconceptionIds = new EventEmitter< + string[] + >(); directiveSubscriptions = new Subscription(); activeEditOption: boolean = false; @@ -101,7 +119,7 @@ export class StateResponsesComponent implements OnInit, OnDestroy { private parameterizeRuleDescription: ParameterizeRuleDescriptionPipe, private truncate: TruncatePipe, private wrapTextWithEllipsis: WrapTextWithEllipsisPipe, - private editabilityService: EditabilityService, + private editabilityService: EditabilityService ) {} sendOnSaveNextContentIdIndex(event: number): void { @@ -109,17 +127,17 @@ export class StateResponsesComponent implements OnInit, OnDestroy { } drop(event: CdkDragSortEvent): void { - moveItemInArray( - this.answerGroups, event.previousIndex, - event.currentIndex); + moveItemInArray(this.answerGroups, event.previousIndex, event.currentIndex); this.responsesService.save( - this.answerGroups, this.defaultOutcome, + this.answerGroups, + this.defaultOutcome, (newAnswerGroups, newDefaultOutcome) => { this.onSaveInteractionAnswerGroups.emit(newAnswerGroups); this.onSaveInteractionDefaultOutcome.emit(newDefaultOutcome); this.refreshWarnings.emit(); - }); + } + ); } _initializeTrainingData(): void { @@ -138,7 +156,8 @@ export class StateResponsesComponent implements OnInit, OnDestroy { onChangeSolicitAnswerDetails(): void { this.onSaveSolicitAnswerDetails.emit( - this.stateSolicitAnswerDetailsService.displayed); + this.stateSolicitAnswerDetailsService.displayed + ); this.stateSolicitAnswerDetailsService.saveDisplayedValue(); } @@ -148,8 +167,12 @@ export class StateResponsesComponent implements OnInit, OnDestroy { isSelfLoopWithNoFeedback(outcome: Outcome): boolean | void { let currentStateName = this.getActiveStateName(); - if (outcome && typeof outcome === 'object' && currentStateName && - outcome.constructor.name === 'Outcome') { + if ( + outcome && + typeof outcome === 'object' && + currentStateName && + outcome.constructor.name === 'Outcome' + ) { return outcome.isConfusing(currentStateName); } } @@ -159,17 +182,15 @@ export class StateResponsesComponent implements OnInit, OnDestroy { return false; } else { const currentStateName = this.getActiveStateName(); - return ( - (outcome.dest === currentStateName) && - outcome.labelledAsCorrect); + return outcome.dest === currentStateName && outcome.labelledAsCorrect; } } changeActiveAnswerGroupIndex(newIndex: number): void { this.externalSaveService.onExternalSave.emit(); this.responsesService.changeActiveAnswerGroupIndex(newIndex); - this.activeAnswerGroupIndex = ( - this.responsesService.getActiveAnswerGroupIndex()); + this.activeAnswerGroupIndex = + this.responsesService.getActiveAnswerGroupIndex(); } getCurrentInteractionId(): string { @@ -177,31 +198,37 @@ export class StateResponsesComponent implements OnInit, OnDestroy { } isCreatingNewState(outcome: Outcome): boolean { - return (outcome && outcome.dest === AppConstants.PLACEHOLDER_OUTCOME_DEST); + return outcome && outcome.dest === AppConstants.PLACEHOLDER_OUTCOME_DEST; } // This returns false if the current interaction ID is null. isCurrentInteractionLinear(): boolean { let interactionId = this.getCurrentInteractionId(); - return Boolean(interactionId) && INTERACTION_SPECS[ - interactionId as InteractionSpecsKey].is_linear; + return ( + Boolean(interactionId) && + INTERACTION_SPECS[interactionId as InteractionSpecsKey].is_linear + ); } isCurrentInteractionTrivial(): boolean { let interactionId = this.getCurrentInteractionId(); let array: string[] = [ - ...AppConstants.INTERACTION_IDS_WITHOUT_ANSWER_DETAILS]; - return array.indexOf( - interactionId) !== -1; + ...AppConstants.INTERACTION_IDS_WITHOUT_ANSWER_DETAILS, + ]; + return array.indexOf(interactionId) !== -1; } isLinearWithNoFeedback(outcome: Outcome): boolean | void { // Returns false if current interaction is linear and has no // feedback. - if (outcome && typeof outcome === 'object' && - outcome.constructor.name === 'Outcome') { - return this.isCurrentInteractionLinear() && - !outcome.hasNonemptyFeedback(); + if ( + outcome && + typeof outcome === 'object' && + outcome.constructor.name === 'Outcome' + ) { + return ( + this.isCurrentInteractionLinear() && !outcome.hasNonemptyFeedback() + ); } } @@ -214,8 +241,10 @@ export class StateResponsesComponent implements OnInit, OnDestroy { if (this.isLinearWithNoFeedback(outcome)) { return 'Please direct the learner to a different card.'; } else { - return 'Please give Oppia something useful to say,' + - ' or direct the learner to a different card.'; + return ( + 'Please give Oppia something useful to say,' + + ' or direct the learner to a different card.' + ); } } @@ -230,40 +259,48 @@ export class StateResponsesComponent implements OnInit, OnDestroy { backdrop: 'static', }); - modalRef.componentInstance.addState.subscribe( - (value: string) => { - addState(value); - }); + modalRef.componentInstance.addState.subscribe((value: string) => { + addState(value); + }); modalRef.componentInstance.currentInteractionId = currentInteractionId; modalRef.componentInstance.stateName = stateName; - modalRef.result.then((result) => { - this.onSaveNextContentIdIndex.emit(); - - // Create a new answer group. - this.answerGroups.push(this.answerGroupObjectFactory.createNew( - [result.tmpRule], result.tmpOutcome, [], - result.tmpTaggedSkillMisconceptionId)); - this.responsesService.save( - this.answerGroups, this.defaultOutcome, - (newAnswerGroups, newDefaultOutcome) => { - this.onSaveInteractionAnswerGroups.emit(newAnswerGroups); - this.onSaveInteractionDefaultOutcome.emit(newDefaultOutcome); - this.refreshWarnings.emit(); - }); - this.changeActiveAnswerGroupIndex( - this.answerGroups.length - 1); - - // After saving it, check if the modal should be reopened right - // away. - if (result.reopen) { - this.openAddAnswerGroupModal(); + modalRef.result.then( + result => { + this.onSaveNextContentIdIndex.emit(); + + // Create a new answer group. + this.answerGroups.push( + this.answerGroupObjectFactory.createNew( + [result.tmpRule], + result.tmpOutcome, + [], + result.tmpTaggedSkillMisconceptionId + ) + ); + this.responsesService.save( + this.answerGroups, + this.defaultOutcome, + (newAnswerGroups, newDefaultOutcome) => { + this.onSaveInteractionAnswerGroups.emit(newAnswerGroups); + this.onSaveInteractionDefaultOutcome.emit(newDefaultOutcome); + this.refreshWarnings.emit(); + } + ); + this.changeActiveAnswerGroupIndex(this.answerGroups.length - 1); + + // After saving it, check if the modal should be reopened right + // away. + if (result.reopen) { + this.openAddAnswerGroupModal(); + } + }, + () => { + this.alertsService.clearWarnings(); + this.generateContentIdService.revertUnusedContentIdIndex(); } - }, () => { - this.alertsService.clearWarnings(); - this.generateContentIdService.revertUnusedContentIdIndex(); - }); + ); } deleteAnswerGroup(evt: Event, index: number): void { @@ -272,141 +309,173 @@ export class StateResponsesComponent implements OnInit, OnDestroy { evt.stopPropagation(); this.alertsService.clearWarnings(); - this.ngbModal.open(DeleteAnswerGroupModalComponent, { - backdrop: true, - }).result.then(() => { - this.responsesService.deleteAnswerGroup( - index, (newAnswerGroups) => { - this.onSaveInteractionAnswerGroups.emit(newAnswerGroups); - this.refreshWarnings.emit(); - }); - }, () => { - this.alertsService.clearWarnings(); - }); + this.ngbModal + .open(DeleteAnswerGroupModalComponent, { + backdrop: true, + }) + .result.then( + () => { + this.responsesService.deleteAnswerGroup(index, newAnswerGroups => { + this.onSaveInteractionAnswerGroups.emit(newAnswerGroups); + this.refreshWarnings.emit(); + }); + }, + () => { + this.alertsService.clearWarnings(); + } + ); } verifyAndUpdateInapplicableSkillMisconceptionIds(): void { let answerGroups = this.responsesService.getAnswerGroups(); let taggedSkillMisconceptionIds: string[] = []; for (let i = 0; i < answerGroups.length; i++) { - let taggedSkillMisconceptionId = ( - answerGroups[i].taggedSkillMisconceptionId); - if (!answerGroups[i].outcome.labelledAsCorrect && - taggedSkillMisconceptionId !== null) { - taggedSkillMisconceptionIds.push( - taggedSkillMisconceptionId); + let taggedSkillMisconceptionId = + answerGroups[i].taggedSkillMisconceptionId; + if ( + !answerGroups[i].outcome.labelledAsCorrect && + taggedSkillMisconceptionId !== null + ) { + taggedSkillMisconceptionIds.push(taggedSkillMisconceptionId); } } - let commonSkillMisconceptionIds = ( - taggedSkillMisconceptionIds.filter( - skillMisconceptionId => ( - this.inapplicableSkillMisconceptionIds.includes( - skillMisconceptionId)))); + let commonSkillMisconceptionIds = taggedSkillMisconceptionIds.filter( + skillMisconceptionId => + this.inapplicableSkillMisconceptionIds.includes(skillMisconceptionId) + ); if (commonSkillMisconceptionIds.length) { - commonSkillMisconceptionIds.forEach((skillMisconceptionId => { - this.inapplicableSkillMisconceptionIds = ( + commonSkillMisconceptionIds.forEach(skillMisconceptionId => { + this.inapplicableSkillMisconceptionIds = this.inapplicableSkillMisconceptionIds.filter( - item => item !== skillMisconceptionId)); - })); + item => item !== skillMisconceptionId + ); + }); this.onSaveInapplicableSkillMisconceptionIds.emit( - this.inapplicableSkillMisconceptionIds); + this.inapplicableSkillMisconceptionIds + ); } } saveTaggedMisconception(taggedMisconception: TaggedMisconception): void { - const { skillId, misconceptionId } = taggedMisconception; - this.responsesService.updateActiveAnswerGroup({ - taggedSkillMisconceptionId: skillId + '-' + misconceptionId - } as AnswerGroup, (newAnswerGroups) => { - this.onSaveInteractionAnswerGroups.emit(newAnswerGroups); - this.refreshWarnings.emit(); - }); + const {skillId, misconceptionId} = taggedMisconception; + this.responsesService.updateActiveAnswerGroup( + { + taggedSkillMisconceptionId: skillId + '-' + misconceptionId, + } as AnswerGroup, + newAnswerGroups => { + this.onSaveInteractionAnswerGroups.emit(newAnswerGroups); + this.refreshWarnings.emit(); + } + ); } saveActiveAnswerGroupFeedback(updatedOutcome: Outcome): void { - this.responsesService.updateActiveAnswerGroup({ - feedback: updatedOutcome.feedback - }, (newAnswerGroups) => { - this.onSaveInteractionAnswerGroups.emit(newAnswerGroups); - this.refreshWarnings.emit(); - }); + this.responsesService.updateActiveAnswerGroup( + { + feedback: updatedOutcome.feedback, + }, + newAnswerGroups => { + this.onSaveInteractionAnswerGroups.emit(newAnswerGroups); + this.refreshWarnings.emit(); + } + ); } saveActiveAnswerGroupDest(updatedOutcome: Outcome): void { - this.responsesService.updateActiveAnswerGroup({ - dest: updatedOutcome.dest, - refresherExplorationId: updatedOutcome.refresherExplorationId, - missingPrerequisiteSkillId: - updatedOutcome.missingPrerequisiteSkillId - }, (newAnswerGroups) => { - this.onSaveInteractionAnswerGroups.emit(newAnswerGroups); - this.refreshWarnings.emit(); - }); + this.responsesService.updateActiveAnswerGroup( + { + dest: updatedOutcome.dest, + refresherExplorationId: updatedOutcome.refresherExplorationId, + missingPrerequisiteSkillId: updatedOutcome.missingPrerequisiteSkillId, + }, + newAnswerGroups => { + this.onSaveInteractionAnswerGroups.emit(newAnswerGroups); + this.refreshWarnings.emit(); + } + ); } saveActiveAnswerGroupDestIfStuck(updatedOutcome: Outcome): void { - this.responsesService.updateActiveAnswerGroup({ - destIfReallyStuck: updatedOutcome.destIfReallyStuck, - } as typeof updatedOutcome, (newAnswerGroups) => { - this.onSaveInteractionAnswerGroups.emit(newAnswerGroups); - this.refreshWarnings.emit(); - }); + this.responsesService.updateActiveAnswerGroup( + { + destIfReallyStuck: updatedOutcome.destIfReallyStuck, + } as typeof updatedOutcome, + newAnswerGroups => { + this.onSaveInteractionAnswerGroups.emit(newAnswerGroups); + this.refreshWarnings.emit(); + } + ); } - saveActiveAnswerGroupCorrectnessLabel( - updatedOutcome: Outcome): void { - this.responsesService.updateActiveAnswerGroup({ - labelledAsCorrect: updatedOutcome.labelledAsCorrect - }, (newAnswerGroups) => { - this.onSaveInteractionAnswerGroups.emit(newAnswerGroups); - this.refreshWarnings.emit(); - }); + saveActiveAnswerGroupCorrectnessLabel(updatedOutcome: Outcome): void { + this.responsesService.updateActiveAnswerGroup( + { + labelledAsCorrect: updatedOutcome.labelledAsCorrect, + }, + newAnswerGroups => { + this.onSaveInteractionAnswerGroups.emit(newAnswerGroups); + this.refreshWarnings.emit(); + } + ); } saveActiveAnswerGroupRules(updatedRules: Rule[]): void { - this.responsesService.updateActiveAnswerGroup({ - rules: updatedRules - } as AnswerGroup, (newAnswerGroups) => { - this.onSaveInteractionAnswerGroups.emit(newAnswerGroups); - this.refreshWarnings.emit(); - }); + this.responsesService.updateActiveAnswerGroup( + { + rules: updatedRules, + } as AnswerGroup, + newAnswerGroups => { + this.onSaveInteractionAnswerGroups.emit(newAnswerGroups); + this.refreshWarnings.emit(); + } + ); } saveDefaultOutcomeFeedback(updatedOutcome: Outcome): void { - this.responsesService.updateDefaultOutcome({ - feedback: updatedOutcome.feedback, - dest: updatedOutcome.dest - } as Outcome, (newDefaultOutcome) => { - this.onSaveInteractionDefaultOutcome.emit(newDefaultOutcome); - }); + this.responsesService.updateDefaultOutcome( + { + feedback: updatedOutcome.feedback, + dest: updatedOutcome.dest, + } as Outcome, + newDefaultOutcome => { + this.onSaveInteractionDefaultOutcome.emit(newDefaultOutcome); + } + ); } saveDefaultOutcomeDest(updatedOutcome: Outcome): void { - this.responsesService.updateDefaultOutcome({ - dest: updatedOutcome.dest, - refresherExplorationId: updatedOutcome.refresherExplorationId, - missingPrerequisiteSkillId: - updatedOutcome.missingPrerequisiteSkillId - } as Outcome, (newDefaultOutcome) => { - this.onSaveInteractionDefaultOutcome.emit(newDefaultOutcome); - }); + this.responsesService.updateDefaultOutcome( + { + dest: updatedOutcome.dest, + refresherExplorationId: updatedOutcome.refresherExplorationId, + missingPrerequisiteSkillId: updatedOutcome.missingPrerequisiteSkillId, + } as Outcome, + newDefaultOutcome => { + this.onSaveInteractionDefaultOutcome.emit(newDefaultOutcome); + } + ); } saveDefaultOutcomeDestIfStuck(updatedOutcome: Outcome): void { - this.responsesService.updateDefaultOutcome({ - destIfReallyStuck: updatedOutcome.destIfReallyStuck - } as Outcome, (newDefaultOutcome) => { - this.onSaveInteractionDefaultOutcome.emit(newDefaultOutcome); - }); + this.responsesService.updateDefaultOutcome( + { + destIfReallyStuck: updatedOutcome.destIfReallyStuck, + } as Outcome, + newDefaultOutcome => { + this.onSaveInteractionDefaultOutcome.emit(newDefaultOutcome); + } + ); } - saveDefaultOutcomeCorrectnessLabel( - updatedOutcome: Outcome): void { - this.responsesService.updateDefaultOutcome({ - labelledAsCorrect: updatedOutcome.labelledAsCorrect - } as Outcome, (newDefaultOutcome) => { - this.onSaveInteractionDefaultOutcome.emit(newDefaultOutcome); - }); + saveDefaultOutcomeCorrectnessLabel(updatedOutcome: Outcome): void { + this.responsesService.updateDefaultOutcome( + { + labelledAsCorrect: updatedOutcome.labelledAsCorrect, + } as Outcome, + newDefaultOutcome => { + this.onSaveInteractionDefaultOutcome.emit(newDefaultOutcome); + } + ); } getAnswerChoices(): AnswerChoice[] { @@ -414,8 +483,10 @@ export class StateResponsesComponent implements OnInit, OnDestroy { } summarizeAnswerGroup( - answerGroup: AnswerGroup, interactionId: string, - answerChoices: AnswerChoice[], shortenRule: boolean + answerGroup: AnswerGroup, + interactionId: string, + answerChoices: AnswerChoice[], + shortenRule: boolean ): string { let summary = ''; let outcome = answerGroup.outcome; @@ -424,28 +495,35 @@ export class StateResponsesComponent implements OnInit, OnDestroy { if (answerGroup.rules) { let firstRule = this.convertToPlainText.transform( this.parameterizeRuleDescription.transform( - answerGroup.rules[0], interactionId, answerChoices)); + answerGroup.rules[0], + interactionId, + answerChoices + ) + ); summary = 'Answer ' + firstRule; if (hasFeedback && shortenRule) { summary = this.wrapTextWithEllipsis.transform( - summary, AppConstants.RULE_SUMMARY_WRAP_CHARACTER_COUNT); + summary, + AppConstants.RULE_SUMMARY_WRAP_CHARACTER_COUNT + ); } summary = '[' + summary + '] '; } if (hasFeedback) { - summary += ( - shortenRule ? - this.truncate.transform(outcome.feedback.html, 30) : - this.convertToPlainText.transform(outcome.feedback.html)); + summary += shortenRule + ? this.truncate.transform(outcome.feedback.html, 30) + : this.convertToPlainText.transform(outcome.feedback.html); } return summary; } summarizeDefaultOutcome( - defaultOutcome: Outcome, interactionId: string, - answerGroupCount: number, shortenRule: boolean + defaultOutcome: Outcome, + interactionId: string, + answerGroupCount: number, + shortenRule: boolean ): string { if (!defaultOutcome) { return ''; @@ -454,10 +532,13 @@ export class StateResponsesComponent implements OnInit, OnDestroy { let summary = ''; let hasFeedback = defaultOutcome.hasNonemptyFeedback(); - if (interactionId && INTERACTION_SPECS[ - interactionId as InteractionSpecsKey].is_linear) { - let defaultOutcomeHeading = INTERACTION_SPECS[ - interactionId as InteractionSpecsKey].default_outcome_heading; + if ( + interactionId && + INTERACTION_SPECS[interactionId as InteractionSpecsKey].is_linear + ) { + let defaultOutcomeHeading = + INTERACTION_SPECS[interactionId as InteractionSpecsKey] + .default_outcome_heading; if (defaultOutcomeHeading) { summary = defaultOutcomeHeading; } @@ -469,20 +550,23 @@ export class StateResponsesComponent implements OnInit, OnDestroy { if (hasFeedback && shortenRule) { summary = this.wrapTextWithEllipsis.transform( - summary, AppConstants.RULE_SUMMARY_WRAP_CHARACTER_COUNT); + summary, + AppConstants.RULE_SUMMARY_WRAP_CHARACTER_COUNT + ); } summary = '[' + summary + '] '; if (hasFeedback) { - summary += - this.convertToPlainText.transform(defaultOutcome.feedback.html); + summary += this.convertToPlainText.transform( + defaultOutcome.feedback.html + ); } return summary; } isOutcomeLooping(outcome: Outcome): boolean { let activeStateName = this.getActiveStateName(); - return outcome && (outcome.dest === activeStateName); + return outcome && outcome.dest === activeStateName; } toggleResponseCard(): void { @@ -493,67 +577,74 @@ export class StateResponsesComponent implements OnInit, OnDestroy { let answerGroups = this.responsesService.getAnswerGroups(); let taggedSkillMisconceptionIds: Record = {}; for (let i = 0; i < answerGroups.length; i++) { - let taggedSkillMisconceptionId = ( - answerGroups[i].taggedSkillMisconceptionId); - if (!answerGroups[i].outcome.labelledAsCorrect && - taggedSkillMisconceptionId !== null) { + let taggedSkillMisconceptionId = + answerGroups[i].taggedSkillMisconceptionId; + if ( + !answerGroups[i].outcome.labelledAsCorrect && + taggedSkillMisconceptionId !== null + ) { taggedSkillMisconceptionIds[taggedSkillMisconceptionId] = true; } } let unaddressedMisconceptionNames: string[] = []; - Object.keys(this.misconceptionsBySkill).forEach( - (skillId) => { - let misconceptions = this.misconceptionsBySkill[skillId]; - for (let i = 0; i < misconceptions.length; i++) { - if (!misconceptions[i].isMandatory()) { - continue; - } - let skillMisconceptionId = ( - skillId + '-' + misconceptions[i].getId()); - if (!taggedSkillMisconceptionIds.hasOwnProperty( - skillMisconceptionId)) { - unaddressedMisconceptionNames.push( - misconceptions[i].getName()); - } + Object.keys(this.misconceptionsBySkill).forEach(skillId => { + let misconceptions = this.misconceptionsBySkill[skillId]; + for (let i = 0; i < misconceptions.length; i++) { + if (!misconceptions[i].isMandatory()) { + continue; } - }); + let skillMisconceptionId = skillId + '-' + misconceptions[i].getId(); + if (!taggedSkillMisconceptionIds.hasOwnProperty(skillMisconceptionId)) { + unaddressedMisconceptionNames.push(misconceptions[i].getName()); + } + } + }); return unaddressedMisconceptionNames; } getOptionalSkillMisconceptionStatus( - optionalSkillMisconceptionId: string): string { + optionalSkillMisconceptionId: string + ): string { let answerGroups = this.responsesService.getAnswerGroups(); let taggedSkillMisconceptionIds = []; for (let i = 0; i < answerGroups.length; i++) { - let taggedSkillMisconceptionId = ( - answerGroups[i].taggedSkillMisconceptionId); - if (!answerGroups[i].outcome.labelledAsCorrect && - taggedSkillMisconceptionId !== null) { + let taggedSkillMisconceptionId = + answerGroups[i].taggedSkillMisconceptionId; + if ( + !answerGroups[i].outcome.labelledAsCorrect && + taggedSkillMisconceptionId !== null + ) { taggedSkillMisconceptionIds.push(taggedSkillMisconceptionId); } } - let skillMisconceptionIdIsAssigned = ( - taggedSkillMisconceptionIds.includes( - optionalSkillMisconceptionId)); + let skillMisconceptionIdIsAssigned = taggedSkillMisconceptionIds.includes( + optionalSkillMisconceptionId + ); if (skillMisconceptionIdIsAssigned) { return 'Assigned'; } return this.inapplicableSkillMisconceptionIds.includes( - optionalSkillMisconceptionId) ? 'Not Applicable' : ''; + optionalSkillMisconceptionId + ) + ? 'Not Applicable' + : ''; } updateOptionalMisconceptionIdStatus( - skillMisconceptionId: string, isApplicable: boolean): void { + skillMisconceptionId: string, + isApplicable: boolean + ): void { if (isApplicable) { - this.inapplicableSkillMisconceptionIds = ( + this.inapplicableSkillMisconceptionIds = this.inapplicableSkillMisconceptionIds.filter( - item => item !== skillMisconceptionId)); + item => item !== skillMisconceptionId + ); } else { - this.inapplicableSkillMisconceptionIds.push( - skillMisconceptionId); + this.inapplicableSkillMisconceptionIds.push(skillMisconceptionId); } this.onSaveInapplicableSkillMisconceptionIds.emit( - this.inapplicableSkillMisconceptionIds); + this.inapplicableSkillMisconceptionIds + ); this.setActiveEditOption(false); } @@ -563,8 +654,8 @@ export class StateResponsesComponent implements OnInit, OnDestroy { isNoActionExpected(skillMisconceptionId: string): boolean { return ['Assigned', 'Not Applicable'].includes( - this.getOptionalSkillMisconceptionStatus( - skillMisconceptionId)); + this.getOptionalSkillMisconceptionStatus(skillMisconceptionId) + ); } getStaticImageUrl(imagePath: string): string { @@ -572,14 +663,14 @@ export class StateResponsesComponent implements OnInit, OnDestroy { } ngOnInit(): void { - this.SHOW_TRAINABLE_UNRESOLVED_ANSWERS = ( - AppConstants.SHOW_TRAINABLE_UNRESOLVED_ANSWERS); + this.SHOW_TRAINABLE_UNRESOLVED_ANSWERS = + AppConstants.SHOW_TRAINABLE_UNRESOLVED_ANSWERS; this.responseCardIsShown = true; - this.enableSolicitAnswerDetailsFeature = ( - AppConstants.ENABLE_SOLICIT_ANSWER_DETAILS_FEATURE); + this.enableSolicitAnswerDetailsFeature = + AppConstants.ENABLE_SOLICIT_ANSWER_DETAILS_FEATURE; this.misconceptionsBySkill = {}; this.directiveSubscriptions.add( - this.responsesService.onInitializeAnswerGroups.subscribe((data) => { + this.responsesService.onInitializeAnswerGroups.subscribe(data => { this.responsesService.init(data as Interaction); this.answerGroups = this.responsesService.getAnswerGroups(); this.defaultOutcome = this.responsesService.getDefaultOutcome(); @@ -595,43 +686,44 @@ export class StateResponsesComponent implements OnInit, OnDestroy { // Initialize training data for these answer groups. this._initializeTrainingData(); - this.activeAnswerGroupIndex = ( - this.responsesService.getActiveAnswerGroupIndex()); + this.activeAnswerGroupIndex = + this.responsesService.getActiveAnswerGroupIndex(); this.externalSaveService.onExternalSave.emit(); }) ); this.directiveSubscriptions.add( this.stateInteractionIdService.onInteractionIdChanged.subscribe( - (newInteractionId) => { + newInteractionId => { this.externalSaveService.onExternalSave.emit(); this.responsesService.onInteractionIdChanged( newInteractionId, (newAnswerGroups, newDefaultOutcome) => { - this.onSaveInteractionDefaultOutcome.emit( - newDefaultOutcome); + this.onSaveInteractionDefaultOutcome.emit(newDefaultOutcome); this.onSaveInteractionAnswerGroups.emit(newAnswerGroups); this.refreshWarnings.emit(); this.answerGroups = this.responsesService.getAnswerGroups(); - this.defaultOutcome = - this.responsesService.getDefaultOutcome(); + this.defaultOutcome = this.responsesService.getDefaultOutcome(); // Reinitialize training data if the interaction ID is // changed. this._initializeTrainingData(); - this.activeAnswerGroupIndex = ( - this.responsesService.getActiveAnswerGroupIndex()); - }); + this.activeAnswerGroupIndex = + this.responsesService.getActiveAnswerGroupIndex(); + } + ); // Prompt the user to create a new response if it is not a // linear or non-terminal interaction and if an actual // interaction is specified (versus one being deleted). - if (newInteractionId && - !INTERACTION_SPECS[ - newInteractionId as InteractionSpecsKey].is_linear && - !INTERACTION_SPECS[ - newInteractionId as InteractionSpecsKey].is_terminal) { + if ( + newInteractionId && + !INTERACTION_SPECS[newInteractionId as InteractionSpecsKey] + .is_linear && + !INTERACTION_SPECS[newInteractionId as InteractionSpecsKey] + .is_terminal + ) { this.openAddAnswerGroupModal(); } } @@ -639,53 +731,55 @@ export class StateResponsesComponent implements OnInit, OnDestroy { ); this.directiveSubscriptions.add( - this.responsesService.onAnswerGroupsChanged.subscribe( - () => { - this.answerGroups = this.responsesService.getAnswerGroups(); - this.defaultOutcome = this.responsesService.getDefaultOutcome(); - this.activeAnswerGroupIndex = + this.responsesService.onAnswerGroupsChanged.subscribe(() => { + this.answerGroups = this.responsesService.getAnswerGroups(); + this.defaultOutcome = this.responsesService.getDefaultOutcome(); + this.activeAnswerGroupIndex = this.responsesService.getActiveAnswerGroupIndex(); - this.verifyAndUpdateInapplicableSkillMisconceptionIds(); - } - )); + this.verifyAndUpdateInapplicableSkillMisconceptionIds(); + }) + ); this.directiveSubscriptions.add( this.stateEditorService.onUpdateAnswerChoices.subscribe( - (newAnswerChoices) => { + newAnswerChoices => { this.responsesService.updateAnswerChoices(newAnswerChoices); - }) + } + ) ); this.directiveSubscriptions.add( this.stateEditorService.onHandleCustomArgsUpdate.subscribe( - (newAnswerChoices) => { + newAnswerChoices => { this.responsesService.handleCustomArgsUpdate( - newAnswerChoices, (newAnswerGroups) => { + newAnswerChoices, + newAnswerGroups => { this.onSaveInteractionAnswerGroups.emit(newAnswerGroups); this.refreshWarnings.emit(); - }); + } + ); } ) ); this.directiveSubscriptions.add( - this.stateEditorService.onStateEditorInitialized.subscribe( - () => { - this.misconceptionsBySkill = ( - this.stateEditorService.getMisconceptionsBySkill()); - - this.containsOptionalMisconceptions = ( - Object.values(this.misconceptionsBySkill).some( - (misconceptions: Misconception[]) => misconceptions.some( - misconception => !misconception.isMandatory()))); - }) + this.stateEditorService.onStateEditorInitialized.subscribe(() => { + this.misconceptionsBySkill = + this.stateEditorService.getMisconceptionsBySkill(); + + this.containsOptionalMisconceptions = Object.values( + this.misconceptionsBySkill + ).some((misconceptions: Misconception[]) => + misconceptions.some(misconception => !misconception.isMandatory()) + ); + }) ); if (this.stateEditorService.isInQuestionMode()) { this.onResponsesInitialized.emit(); } this.stateEditorService.updateStateResponsesInitialised(); - this.inapplicableSkillMisconceptionIds = ( - this.stateEditorService.getInapplicableSkillMisconceptionIds()); + this.inapplicableSkillMisconceptionIds = + this.stateEditorService.getInapplicableSkillMisconceptionIds(); this.activeEditOption = false; } @@ -694,7 +788,9 @@ export class StateResponsesComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive('oppiaStateResponses', +angular.module('oppia').directive( + 'oppiaStateResponses', downgradeComponent({ - component: StateResponsesComponent - }) as angular.IDirectiveFactory); + component: StateResponsesComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/components/state-editor/state-skill-editor/state-skill-editor.component.spec.ts b/core/templates/components/state-editor/state-skill-editor/state-skill-editor.component.spec.ts index 069ed7c64eff..f136576fe702 100644 --- a/core/templates/components/state-editor/state-skill-editor/state-skill-editor.component.spec.ts +++ b/core/templates/components/state-editor/state-skill-editor/state-skill-editor.component.spec.ts @@ -16,31 +16,46 @@ * @fileoverview Unit tests for the State Skill Editor Component. */ -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { MatCardModule } from '@angular/material/card'; -import { NgbModal, NgbModalOptions, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { TopicsAndSkillsDashboardBackendApiService, TopicsAndSkillDashboardData } from +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {MatCardModule} from '@angular/material/card'; +import { + NgbModal, + NgbModalOptions, + NgbModalRef, +} from '@ng-bootstrap/ng-bootstrap'; +import { + TopicsAndSkillsDashboardBackendApiService, + TopicsAndSkillDashboardData, // eslint-disable-next-line max-len - 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; -import { SelectSkillModalComponent } from 'components/skill-selector/select-skill-modal.component'; -import { DeleteStateSkillModalComponent } from +} from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; +import {SelectSkillModalComponent} from 'components/skill-selector/select-skill-modal.component'; +import { + DeleteStateSkillModalComponent, // eslint-disable-next-line max-len - 'pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-state-skill-modal.component'; -import { StateLinkedSkillIdService } from '../state-editor-properties-services/state-skill.service'; -import { StateSkillEditorComponent } from './state-skill-editor.component'; -import { MatCheckboxModule } from '@angular/material/checkbox'; -import { MatRadioModule } from '@angular/material/radio'; -import { FormsModule } from '@angular/forms'; -import { SkillSummary, SkillSummaryBackendDict } from 'domain/skill/skill-summary.model'; -import { SkillSelectorComponent } from 'components/skill-selector/skill-selector.component'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { CreatorTopicSummary } from 'domain/topic/creator-topic-summary.model'; -import { UserService } from 'services/user.service'; -import { SkillBackendApiService } from 'domain/skill/skill-backend-api.service'; -import { MaterialModule } from 'modules/material.module'; -import { SkillObjectFactory } from 'domain/skill/SkillObjectFactory'; - +} from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-state-skill-modal.component'; +import {StateLinkedSkillIdService} from '../state-editor-properties-services/state-skill.service'; +import {StateSkillEditorComponent} from './state-skill-editor.component'; +import {MatCheckboxModule} from '@angular/material/checkbox'; +import {MatRadioModule} from '@angular/material/radio'; +import {FormsModule} from '@angular/forms'; +import { + SkillSummary, + SkillSummaryBackendDict, +} from 'domain/skill/skill-summary.model'; +import {SkillSelectorComponent} from 'components/skill-selector/skill-selector.component'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {CreatorTopicSummary} from 'domain/topic/creator-topic-summary.model'; +import {UserService} from 'services/user.service'; +import {SkillBackendApiService} from 'domain/skill/skill-backend-api.service'; +import {MaterialModule} from 'modules/material.module'; +import {SkillObjectFactory} from 'domain/skill/SkillObjectFactory'; describe('State Skill Editor Component', () => { let fixture: ComponentFixture; @@ -60,11 +75,12 @@ describe('State Skill Editor Component', () => { misconception_count: 0, worked_examples_count: 1, skill_model_created_on: 2, - skill_model_last_updated: 3 + skill_model_last_updated: 3, }; let untriagedSkillSummariesData: SkillSummary[] = [ - SkillSummary.createFromBackendDict(skillSummaryBackendDict)]; + SkillSummary.createFromBackendDict(skillSummaryBackendDict), + ]; class MockNgbModal { modal!: string; @@ -77,29 +93,26 @@ describe('State Skill Editor Component', () => { skillsInSameTopicCount: null, categorizedSkills: null, allowSkillsFromOtherTopics: null, - untriagedSkillSummaries: null + untriagedSkillSummaries: null, }, result: { then: ( - successCallback: (result: {}) => void, - cancelCallback: () => void + successCallback: (result: {}) => void, + cancelCallback: () => void ) => { if (this.success) { successCallback({}); } else { cancelCallback(); } - } - } + }, + }, }; } else if (this.modal === 'delete_skill') { return { componentInstance: {}, result: { - then: ( - successCallback: () => void, - errorCallback: () => void - ) => { + then: (successCallback: () => void, errorCallback: () => void) => { if (this.success) { successCallback(); } else { @@ -108,19 +121,17 @@ describe('State Skill Editor Component', () => { return { then: (callback: () => void) => { callback(); - } + }, }; - } - } + }, + }, }; } } } const topicsAndSkillsDashboardData: TopicsAndSkillDashboardData = { - allClassroomNames: [ - 'math' - ], + allClassroomNames: ['math'], canDeleteTopic: true, canCreateTopic: true, canDeleteSkill: true, @@ -135,17 +146,38 @@ describe('State Skill Editor Component', () => { misconceptionCount: 0, workedExamplesCount: 0, skillModelCreatedOn: 1622827020924.104, - skillModelLastUpdated: 1622827020924.109 - } + skillModelLastUpdated: 1622827020924.109, + }, ], totalSkillCount: 1, topicSummaries: [ new CreatorTopicSummary( - 'dummy2', 'division', 2, 2, 3, 3, 0, - 'es', 'dummy2', 1, 1, 1, 1, true, - true, 'math', 'public/img1.png', 'green', 'div', 1, 1, [5, 4], [3, 4]) + 'dummy2', + 'division', + 2, + 2, + 3, + 3, + 0, + 'es', + 'dummy2', + 1, + 1, + 1, + 1, + true, + true, + 'math', + 'public/img1.png', + 'green', + 'div', + 1, + 1, + [5, 4], + [3, 4] + ), ], - categorizedSkillsDict: {} + categorizedSkillsDict: {}, }; class MockTopicsAndSkillsDashboardBackendApiService { @@ -154,7 +186,7 @@ describe('State Skill Editor Component', () => { return { then: (callback: (resp: TopicsAndSkillDashboardData) => void) => { callback(topicsAndSkillsDashboardData); - } + }, }; } } @@ -167,13 +199,13 @@ describe('State Skill Editor Component', () => { MatRadioModule, FormsModule, MaterialModule, - HttpClientTestingModule + HttpClientTestingModule, ], declarations: [ StateSkillEditorComponent, DeleteStateSkillModalComponent, SelectSkillModalComponent, - SkillSelectorComponent + SkillSelectorComponent, ], providers: [ TopicsAndSkillsDashboardBackendApiService, @@ -183,13 +215,13 @@ describe('State Skill Editor Component', () => { SkillBackendApiService, { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, { provide: TopicsAndSkillsDashboardBackendApiService, - useClass: MockTopicsAndSkillsDashboardBackendApiService - } - ] + useClass: MockTopicsAndSkillsDashboardBackendApiService, + }, + ], }).compileComponents(); })); @@ -199,7 +231,7 @@ describe('State Skill Editor Component', () => { fixture.detectChanges(); componentInstance.untriagedSkillSummaries = []; urlInterpolationService = TestBed.inject(UrlInterpolationService); - mockNgbModal = (TestBed.inject(NgbModal) as unknown) as MockNgbModal; + mockNgbModal = TestBed.inject(NgbModal) as unknown as MockNgbModal; stateLinkedSkillIdService = TestBed.inject(StateLinkedSkillIdService); userService = TestBed.inject(UserService); skillBackendApiService = TestBed.inject(SkillBackendApiService); @@ -207,9 +239,9 @@ describe('State Skill Editor Component', () => { }); beforeEach(() => { - spyOn( - userService, 'canUserAccessTopicsAndSkillsDashboard' - ).and.returnValue(Promise.resolve(true)); + spyOn(userService, 'canUserAccessTopicsAndSkillsDashboard').and.returnValue( + Promise.resolve(true) + ); }); it('should create', () => { @@ -219,11 +251,10 @@ describe('State Skill Editor Component', () => { it('should open add skill modal for adding skill', () => { mockNgbModal.modal = 'add_skill'; const modalSpy = spyOn(mockNgbModal, 'open').and.callFake((dlg, opt) => { - return ( - { componentInstance: MockNgbModal, - result: Promise.resolve('success') - } - ) as NgbModalRef; + return { + componentInstance: MockNgbModal, + result: Promise.resolve('success'), + } as NgbModalRef; }); componentInstance.addSkill(); fixture.detectChanges(); @@ -240,11 +271,10 @@ describe('State Skill Editor Component', () => { it('should open delete skill modal for deleting skill', () => { mockNgbModal.modal = 'delete_skill'; const modalSpy = spyOn(mockNgbModal, 'open').and.callFake((dlg, opt) => { - return ( - { componentInstance: MockNgbModal, - result: Promise.resolve('success') - } - ) as NgbModalRef; + return { + componentInstance: MockNgbModal, + result: Promise.resolve('success'), + } as NgbModalRef; }); componentInstance.deleteSkill(); fixture.detectChanges(); @@ -260,8 +290,9 @@ describe('State Skill Editor Component', () => { it('should call getSkillEditorUrl and return skillEditor URL', () => { const urlSpy = spyOn( - urlInterpolationService, 'interpolateUrl') - .and.returnValue('/skill_editor/skill_1'); + urlInterpolationService, + 'interpolateUrl' + ).and.returnValue('/skill_editor/skill_1'); stateLinkedSkillIdService.displayed = 'skill_1'; componentInstance.getSkillEditorUrl(); @@ -269,13 +300,16 @@ describe('State Skill Editor Component', () => { expect(urlSpy).toHaveBeenCalled(); }); - it('should throw error on call getSkillEditorUrl when there is no skill' + - 'is selected', () => { - stateLinkedSkillIdService.displayed = null; - expect(() => { - componentInstance.getSkillEditorUrl(); - }).toThrowError('Expected a skill id to be displayed'); - }); + it( + 'should throw error on call getSkillEditorUrl when there is no skill' + + 'is selected', + () => { + stateLinkedSkillIdService.displayed = null; + expect(() => { + componentInstance.getSkillEditorUrl(); + }).toThrowError('Expected a skill id to be displayed'); + } + ); it('should toggle skillEditorIsShown', () => { componentInstance.skillEditorIsShown = true; @@ -284,46 +318,45 @@ describe('State Skill Editor Component', () => { expect(componentInstance.skillEditorIsShown).toEqual(false); }); - it('should fetch the linked skill name to be displayed from linked skill id', - fakeAsync(() => { - const skillBackendDict = { - id: 'skill_1', - description: 'skill 1', - misconceptions: [], - rubrics: [], - skill_contents: { - explanation: { - html: 'test explanation', - content_id: 'explanation', - }, - worked_examples: [], - recorded_voiceovers: { - voiceovers_mapping: {} - } + it('should fetch the linked skill name to be displayed from linked skill id', fakeAsync(() => { + const skillBackendDict = { + id: 'skill_1', + description: 'skill 1', + misconceptions: [], + rubrics: [], + skill_contents: { + explanation: { + html: 'test explanation', + content_id: 'explanation', }, - language_code: 'en', - version: 3, - prerequisite_skill_ids: [], - all_questions_merged: false, - next_misconception_id: 0, - superseding_skill_id: '2', - }; - const fetchSkillResponse = { - skill: skillObjectFactory.createFromBackendDict(skillBackendDict), - assignedSkillTopicData: {}, - groupedSkillSummaries: {} - }; - spyOn( - skillBackendApiService, 'fetchSkillAsync' - ).and.returnValue(Promise.resolve(fetchSkillResponse)); - stateLinkedSkillIdService.displayed = 'skill_1'; + worked_examples: [], + recorded_voiceovers: { + voiceovers_mapping: {}, + }, + }, + language_code: 'en', + version: 3, + prerequisite_skill_ids: [], + all_questions_merged: false, + next_misconception_id: 0, + superseding_skill_id: '2', + }; + const fetchSkillResponse = { + skill: skillObjectFactory.createFromBackendDict(skillBackendDict), + assignedSkillTopicData: {}, + groupedSkillSummaries: {}, + }; + spyOn(skillBackendApiService, 'fetchSkillAsync').and.returnValue( + Promise.resolve(fetchSkillResponse) + ); + stateLinkedSkillIdService.displayed = 'skill_1'; - expect(componentInstance.skillName).toBeUndefined(); + expect(componentInstance.skillName).toBeUndefined(); - componentInstance.ngOnInit(); - stateLinkedSkillIdService.onStateLinkedSkillIdInitialized.emit(); - tick(); + componentInstance.ngOnInit(); + stateLinkedSkillIdService.onStateLinkedSkillIdInitialized.emit(); + tick(); - expect(componentInstance.skillName).toEqual('skill 1'); - })); + expect(componentInstance.skillName).toEqual('skill 1'); + })); }); diff --git a/core/templates/components/state-editor/state-skill-editor/state-skill-editor.component.ts b/core/templates/components/state-editor/state-skill-editor/state-skill-editor.component.ts index 33cbfe3c1746..9b698d5e13ab 100644 --- a/core/templates/components/state-editor/state-skill-editor/state-skill-editor.component.ts +++ b/core/templates/components/state-editor/state-skill-editor/state-skill-editor.component.ts @@ -14,37 +14,42 @@ /** * @fileoverview Component for the skill editor section in the state editor. -*/ + */ -import { Component, EventEmitter, OnInit, Output } from '@angular/core'; -import { SkillSummaryBackendDict } from 'core/templates/domain/skill/skill-summary.model'; -import { SelectSkillModalComponent } from 'components/skill-selector/select-skill-modal.component'; -import { DeleteStateSkillModalComponent } from +import {Component, EventEmitter, OnInit, Output} from '@angular/core'; +import {SkillSummaryBackendDict} from 'core/templates/domain/skill/skill-summary.model'; +import {SelectSkillModalComponent} from 'components/skill-selector/select-skill-modal.component'; +import { + DeleteStateSkillModalComponent, // eslint-disable-next-line max-len - 'pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-state-skill-modal.component'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { TopicsAndSkillsDashboardBackendApiService, CategorizedAndUntriagedSkillsData } from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; -import { StoryEditorStateService } from 'pages/story-editor-page/services/story-editor-state.service'; -import { AlertsService } from 'services/alerts.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { StateLinkedSkillIdService } from '../state-editor-properties-services/state-skill.service'; -import { SkillsCategorizedByTopics } from 'pages/topics-and-skills-dashboard-page/skills-list/skills-list.component'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { SkillBackendApiService } from 'domain/skill/skill-backend-api.service'; -import { UserService } from 'services/user.service'; -import { ShortSkillSummary } from 'domain/skill/short-skill-summary.model'; +} from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-state-skill-modal.component'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import { + TopicsAndSkillsDashboardBackendApiService, + CategorizedAndUntriagedSkillsData, +} from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; +import {StoryEditorStateService} from 'pages/story-editor-page/services/story-editor-state.service'; +import {AlertsService} from 'services/alerts.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {StateLinkedSkillIdService} from '../state-editor-properties-services/state-skill.service'; +import {SkillsCategorizedByTopics} from 'pages/topics-and-skills-dashboard-page/skills-list/skills-list.component'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {SkillBackendApiService} from 'domain/skill/skill-backend-api.service'; +import {UserService} from 'services/user.service'; +import {ShortSkillSummary} from 'domain/skill/short-skill-summary.model'; @Component({ selector: 'state-skill-editor', - templateUrl: './state-skill-editor.component.html' + templateUrl: './state-skill-editor.component.html', }) export class StateSkillEditorComponent implements OnInit { - @Output() onSaveLinkedSkillId: EventEmitter = ( - new EventEmitter()); + @Output() onSaveLinkedSkillId: EventEmitter = new EventEmitter< + string | null + >(); - @Output() onSaveStateContent: EventEmitter = ( - new EventEmitter()); + @Output() onSaveStateContent: EventEmitter = + new EventEmitter(); categorizedSkills!: SkillsCategorizedByTopics; untriagedSkillSummaries!: ShortSkillSummary[]; @@ -53,8 +58,7 @@ export class StateSkillEditorComponent implements OnInit { userCanEditSkills: boolean = false; constructor( - private topicsAndSkillsDashboardBackendApiService: - TopicsAndSkillsDashboardBackendApiService, + private topicsAndSkillsDashboardBackendApiService: TopicsAndSkillsDashboardBackendApiService, private storyEditorStateService: StoryEditorStateService, private alertsService: AlertsService, private windowDimensionsService: WindowDimensionsService, @@ -66,83 +70,93 @@ export class StateSkillEditorComponent implements OnInit { ) {} ngOnInit(): void { - this.skillEditorIsShown = (!this.windowDimensionsService.isWindowNarrow()); + this.skillEditorIsShown = !this.windowDimensionsService.isWindowNarrow(); this.topicsAndSkillsDashboardBackendApiService .fetchCategorizedAndUntriagedSkillsDataAsync() .then((response: CategorizedAndUntriagedSkillsData) => { this.categorizedSkills = response.categorizedSkillsDict; this.untriagedSkillSummaries = response.untriagedSkillSummaries; }); - this.userService.canUserAccessTopicsAndSkillsDashboard() - .then((canUserAccessTopicsAndSkillsDashboard) => { + this.userService + .canUserAccessTopicsAndSkillsDashboard() + .then(canUserAccessTopicsAndSkillsDashboard => { this.userCanEditSkills = canUserAccessTopicsAndSkillsDashboard; }); this.stateLinkedSkillIdService.onStateLinkedSkillIdInitialized.subscribe( () => { if (this.stateLinkedSkillIdService.displayed) { - this.skillBackendApiService.fetchSkillAsync( - this.stateLinkedSkillIdService.displayed - ).then((skill) => { - this.skillName = skill.skill.getDescription(); - }); + this.skillBackendApiService + .fetchSkillAsync(this.stateLinkedSkillIdService.displayed) + .then(skill => { + this.skillName = skill.skill.getDescription(); + }); } - }); + } + ); } addSkill(): void { - let sortedSkillSummaries = ( - this.storyEditorStateService.getSkillSummaries() - ) as SkillSummaryBackendDict[]; + let sortedSkillSummaries = + this.storyEditorStateService.getSkillSummaries() as SkillSummaryBackendDict[]; let allowSkillsFromOtherTopics = true; let skillsInSameTopicCount = 0; - let modalRef: NgbModalRef = this.ngbModal.open( - SelectSkillModalComponent, { - backdrop: 'static', - windowClass: 'skill-select-modal', - size: 'xl' - }); + let modalRef: NgbModalRef = this.ngbModal.open(SelectSkillModalComponent, { + backdrop: 'static', + windowClass: 'skill-select-modal', + size: 'xl', + }); modalRef.componentInstance.skillSummaries = sortedSkillSummaries; - modalRef.componentInstance.skillsInSameTopicCount = ( - skillsInSameTopicCount); + modalRef.componentInstance.skillsInSameTopicCount = skillsInSameTopicCount; modalRef.componentInstance.categorizedSkills = this.categorizedSkills; - modalRef.componentInstance.allowSkillsFromOtherTopics = ( - allowSkillsFromOtherTopics); - modalRef.componentInstance.untriagedSkillSummaries = ( - this.untriagedSkillSummaries); - modalRef.result.then((result) => { - this.skillName = result.description; - this.stateLinkedSkillIdService.displayed = result.id; - this.stateLinkedSkillIdService.saveDisplayedValue(); - this.onSaveLinkedSkillId.emit(result.id); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + modalRef.componentInstance.allowSkillsFromOtherTopics = + allowSkillsFromOtherTopics; + modalRef.componentInstance.untriagedSkillSummaries = + this.untriagedSkillSummaries; + modalRef.result.then( + result => { + this.skillName = result.description; + this.stateLinkedSkillIdService.displayed = result.id; + this.stateLinkedSkillIdService.saveDisplayedValue(); + this.onSaveLinkedSkillId.emit(result.id); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } deleteSkill(): void { this.alertsService.clearWarnings(); - this.ngbModal.open( - DeleteStateSkillModalComponent, { + this.ngbModal + .open(DeleteStateSkillModalComponent, { backdrop: true, - }).result.then(() => { - this.stateLinkedSkillIdService.displayed = null; - this.stateLinkedSkillIdService.saveDisplayedValue(); - this.onSaveLinkedSkillId.emit(this.stateLinkedSkillIdService.displayed); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + }) + .result.then( + () => { + this.stateLinkedSkillIdService.displayed = null; + this.stateLinkedSkillIdService.saveDisplayedValue(); + this.onSaveLinkedSkillId.emit( + this.stateLinkedSkillIdService.displayed + ); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } getSkillEditorUrl(): string { if (this.stateLinkedSkillIdService.displayed) { return this.urlInterpolationService.interpolateUrl( - '/skill_editor/', { - skill_id: this.stateLinkedSkillIdService.displayed - }); + '/skill_editor/', + { + skill_id: this.stateLinkedSkillIdService.displayed, + } + ); } throw new Error('Expected a skill id to be displayed'); } @@ -152,6 +166,9 @@ export class StateSkillEditorComponent implements OnInit { } } -angular.module('oppia').directive( - 'stateSkillEditor', downgradeComponent( - {component: StateSkillEditorComponent})); +angular + .module('oppia') + .directive( + 'stateSkillEditor', + downgradeComponent({component: StateSkillEditorComponent}) + ); diff --git a/core/templates/components/state-editor/state-solution-editor/state-solution-editor.component.spec.ts b/core/templates/components/state-editor/state-solution-editor/state-solution-editor.component.spec.ts index 1dd1e19acc42..d89eb88f2415 100644 --- a/core/templates/components/state-editor/state-solution-editor/state-solution-editor.component.spec.ts +++ b/core/templates/components/state-editor/state-solution-editor/state-solution-editor.component.spec.ts @@ -16,27 +16,36 @@ * @fileoverview Unit test for state solution editor component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { Hint } from 'domain/exploration/hint-object.model'; -import { Solution, SolutionObjectFactory } from 'domain/exploration/SolutionObjectFactory'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { ConvertToPlainTextPipe } from 'filters/string-utility-filters/convert-to-plain-text.pipe'; -import { SolutionValidityService } from 'pages/exploration-editor-page/editor-tab/services/solution-validity.service'; -import { SolutionVerificationService } from 'pages/exploration-editor-page/editor-tab/services/solution-verification.service'; -import { AlertsService } from 'services/alerts.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { EditabilityService } from 'services/editability.service'; -import { ExplorationHtmlFormatterService } from 'services/exploration-html-formatter.service'; -import { GenerateContentIdService } from 'services/generate-content-id.service'; -import { StateEditorService } from '../state-editor-properties-services/state-editor.service'; -import { StateHintsService } from '../state-editor-properties-services/state-hints.service'; -import { StateInteractionIdService } from '../state-editor-properties-services/state-interaction-id.service'; -import { StateSolutionService } from '../state-editor-properties-services/state-solution.service'; -import { StateSolutionEditorComponent } from './state-solution-editor.component'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {Hint} from 'domain/exploration/hint-object.model'; +import { + Solution, + SolutionObjectFactory, +} from 'domain/exploration/SolutionObjectFactory'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {ConvertToPlainTextPipe} from 'filters/string-utility-filters/convert-to-plain-text.pipe'; +import {SolutionValidityService} from 'pages/exploration-editor-page/editor-tab/services/solution-validity.service'; +import {SolutionVerificationService} from 'pages/exploration-editor-page/editor-tab/services/solution-verification.service'; +import {AlertsService} from 'services/alerts.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {EditabilityService} from 'services/editability.service'; +import {ExplorationHtmlFormatterService} from 'services/exploration-html-formatter.service'; +import {GenerateContentIdService} from 'services/generate-content-id.service'; +import {StateEditorService} from '../state-editor-properties-services/state-editor.service'; +import {StateHintsService} from '../state-editor-properties-services/state-hints.service'; +import {StateInteractionIdService} from '../state-editor-properties-services/state-interaction-id.service'; +import {StateSolutionService} from '../state-editor-properties-services/state-solution.service'; +import {StateSolutionEditorComponent} from './state-solution-editor.component'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; describe('State Solution Editor Component', () => { let component: StateSolutionEditorComponent; @@ -61,10 +70,7 @@ describe('State Solution Editor Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - StateSolutionEditorComponent, - MockTranslatePipe - ], + declarations: [StateSolutionEditorComponent, MockTranslatePipe], providers: [ AlertsService, EditabilityService, @@ -76,9 +82,9 @@ describe('State Solution Editor Component', () => { StateSolutionService, ConvertToPlainTextPipe, StateInteractionIdService, - WindowDimensionsService + WindowDimensionsService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -89,7 +95,8 @@ describe('State Solution Editor Component', () => { convertToPlainTextPipe = TestBed.inject(ConvertToPlainTextPipe); explorationHtmlFormatterService = TestBed.inject( - ExplorationHtmlFormatterService); + ExplorationHtmlFormatterService + ); editabilityService = TestBed.inject(EditabilityService); windowDimensionsService = TestBed.inject(WindowDimensionsService); stateEditorService = TestBed.inject(StateEditorService); @@ -101,19 +108,24 @@ describe('State Solution Editor Component', () => { solutionVerificationService = TestBed.inject(SolutionVerificationService); solutionObjectFactory = TestBed.inject(SolutionObjectFactory); generateContentIdService = TestBed.inject(GenerateContentIdService); - generateContentIdService.init(() => 0, () => { }); + generateContentIdService.init( + () => 0, + () => {} + ); solution = solutionObjectFactory.createFromBackendDict({ answer_is_exclusive: false, correct_answer: 'This is a correct answer!', explanation: { content_id: 'solution', - html: 'This is the explanation to the answer' - } + html: 'This is the explanation to the answer', + }, }); - spyOn(explorationHtmlFormatterService, 'getInteractionHtml') - .and.returnValue('answerEditorHtml'); + spyOn( + explorationHtmlFormatterService, + 'getInteractionHtml' + ).and.returnValue('answerEditorHtml'); spyOn(windowDimensionsService, 'isWindowNarrow').and.returnValue(false); }); @@ -123,7 +135,8 @@ describe('State Solution Editor Component', () => { expect(component.solutionCardIsShown).toBeTrue(); expect(component.inlineSolutionEditorIsActive).toBeFalse(); expect(component.SOLUTION_EDITOR_FOCUS_LABEL).toBe( - 'currentCorrectAnswerEditorHtmlForSolutionEditor'); + 'currentCorrectAnswerEditorHtmlForSolutionEditor' + ); expect(component.correctAnswerEditorHtml).toEqual('answerEditorHtml'); }); @@ -139,62 +152,55 @@ describe('State Solution Editor Component', () => { expect(component.solutionCardIsShown).toBeTrue(); }); - it('should open delete solution modal when user clicks on delete', - fakeAsync(() => { - spyOn(ngbModal, 'open').and.returnValue( - { - result: Promise.resolve() - } as NgbModalRef - ); - spyOn(stateEditorService, 'deleteCurrentSolutionValidity'); - spyOn(stateSolutionService, 'saveDisplayedValue'); + it('should open delete solution modal when user clicks on delete', fakeAsync(() => { + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.resolve(), + } as NgbModalRef); + spyOn(stateEditorService, 'deleteCurrentSolutionValidity'); + spyOn(stateSolutionService, 'saveDisplayedValue'); - const value = { - index: 0, - evt: new Event('') - }; + const value = { + index: 0, + evt: new Event(''), + }; - component.deleteSolution(value); - tick(); + component.deleteSolution(value); + tick(); - expect( - stateEditorService.deleteCurrentSolutionValidity).toHaveBeenCalled(); - expect(stateSolutionService.saveDisplayedValue).toHaveBeenCalled(); - })); + expect(stateEditorService.deleteCurrentSolutionValidity).toHaveBeenCalled(); + expect(stateSolutionService.saveDisplayedValue).toHaveBeenCalled(); + })); - it('should close delete solution modal when user clicks cancel', - fakeAsync(() => { - spyOn(ngbModal, 'open').and.returnValue( - { - result: Promise.reject() - } as NgbModalRef - ); + it('should close delete solution modal when user clicks cancel', fakeAsync(() => { + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.reject(), + } as NgbModalRef); - const value = { - index: 0, - evt: new Event('') - }; + const value = { + index: 0, + evt: new Event(''), + }; - component.deleteSolution(value); - tick(); + component.deleteSolution(value); + tick(); - expect(ngbModal.open).toHaveBeenCalled(); - })); + expect(ngbModal.open).toHaveBeenCalled(); + })); it('should inject invalid solution tooltip text', () => { spyOn(stateEditorService, 'isInQuestionMode').and.returnValues(true, false); // When in question mode. expect(component.getInvalidSolutionTooltip()).toBe( - 'This solution doesn\'t correspond to an answer ' + - 'marked as correct. Verify the rules specified for the ' + - 'answers or change the solution.' + "This solution doesn't correspond to an answer " + + 'marked as correct. Verify the rules specified for the ' + + 'answers or change the solution.' ); // When not in question mode. expect(component.getInvalidSolutionTooltip()).toBe( 'This solution does not lead to another card. Verify the ' + - 'responses specified or change the solution.' + 'responses specified or change the solution.' ); }); @@ -207,17 +213,17 @@ describe('State Solution Editor Component', () => { it('should return the number of displayed hints', () => { stateHintsService.displayed = [ new Hint(SubtitledHtml.createDefault('

work

', '0')), - new Hint(SubtitledHtml.createDefault('

work

', '0')) + new Hint(SubtitledHtml.createDefault('

work

', '0')), ]; expect(component.displayedHintsLength()).toBe(2); }); it('should check if in editable tutorial mode or not', () => { - spyOn(editabilityService, 'isEditableOutsideTutorialMode') - .and.returnValue(true); - spyOn(editabilityService, 'isEditable') - .and.returnValue(true); + spyOn(editabilityService, 'isEditableOutsideTutorialMode').and.returnValue( + true + ); + spyOn(editabilityService, 'isEditable').and.returnValue(true); expect(component.isEditable()).toBeTrue(); }); @@ -242,13 +248,14 @@ describe('State Solution Editor Component', () => { it('should inject summary of solution', () => { stateSolutionService.savedMemento = solution; - spyOn(convertToPlainTextPipe, 'transform').and.callFake((response) => { + spyOn(convertToPlainTextPipe, 'transform').and.callFake(response => { return response; }); expect(component.getSolutionSummary()).toBe( 'One solution is ""This is a correct answer!"".' + - ' This is the explanation to the answer.'); + ' This is the explanation to the answer.' + ); }); it('should throw error if solution is not saved yet', () => { @@ -265,47 +272,60 @@ describe('State Solution Editor Component', () => { expect(component.isCurrentInteractionLinear()).toBeFalse(); }); - it('should open add or update solution modal when user clicks on' + - ' \'+ ADD SOLUTION\'', fakeAsync(() => { - spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.resolve({ - solution: solution - }) - } as NgbModalRef); - spyOn(solutionVerificationService, 'verifySolution').and.returnValue(false); - spyOn(solutionValidityService, 'updateValidity').and.stub(); - spyOn(stateEditorService, 'isInQuestionMode').and.returnValues(true, false); - spyOn(alertsService, 'addInfoMessage'); - spyOn(stateEditorService, 'getActiveStateName').and.returnValue('State 1'); + it( + 'should open add or update solution modal when user clicks on' + + " '+ ADD SOLUTION'", + fakeAsync(() => { + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.resolve({ + solution: solution, + }), + } as NgbModalRef); + spyOn(solutionVerificationService, 'verifySolution').and.returnValue( + false + ); + spyOn(solutionValidityService, 'updateValidity').and.stub(); + spyOn(stateEditorService, 'isInQuestionMode').and.returnValues( + true, + false + ); + spyOn(alertsService, 'addInfoMessage'); + spyOn(stateEditorService, 'getActiveStateName').and.returnValue( + 'State 1' + ); - // In question mode. - component.openAddOrUpdateSolutionModal(); - tick(); + // In question mode. + component.openAddOrUpdateSolutionModal(); + tick(); - expect(alertsService.addInfoMessage).toHaveBeenCalledWith( - 'The current solution does not correspond to a correct answer.', 4000 - ); + expect(alertsService.addInfoMessage).toHaveBeenCalledWith( + 'The current solution does not correspond to a correct answer.', + 4000 + ); - // Not in question mode. - component.openAddOrUpdateSolutionModal(); - tick(); + // Not in question mode. + component.openAddOrUpdateSolutionModal(); + tick(); - expect(alertsService.addInfoMessage).toHaveBeenCalledWith( - 'The current solution does not lead to another card.', 4000 - ); - })); + expect(alertsService.addInfoMessage).toHaveBeenCalledWith( + 'The current solution does not lead to another card.', + 4000 + ); + }) + ); it('should throw error if active state name is invalid', fakeAsync(() => { spyOn(ngbModal, 'open').and.returnValue({ result: { - then: (successCallback: ( - arg: {solution: Solution} - ) => void, errorCallback) => { + then: ( + successCallback: (arg: {solution: Solution}) => void, + errorCallback + ) => { successCallback({ - solution: solution + solution: solution, }); - } - } + }, + }, } as NgbModalRef); spyOn(solutionVerificationService, 'verifySolution').and.returnValue(false); spyOn(solutionValidityService, 'updateValidity').and.stub(); @@ -313,23 +333,22 @@ describe('State Solution Editor Component', () => { spyOn(alertsService, 'addInfoMessage'); spyOn(stateEditorService, 'getActiveStateName').and.returnValue(null); - expect(function() { + expect(function () { component.openAddOrUpdateSolutionModal(); tick(); }).toThrowError('Expected active state name to be non-null.'); })); - it('should close add or update solution modal if user clicks cancel', - fakeAsync(() => { - spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.reject() - } as NgbModalRef); + it('should close add or update solution modal if user clicks cancel', fakeAsync(() => { + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.reject(), + } as NgbModalRef); - component.openAddOrUpdateSolutionModal(); - tick(); + component.openAddOrUpdateSolutionModal(); + tick(); - expect(ngbModal.open).toHaveBeenCalled(); - })); + expect(ngbModal.open).toHaveBeenCalled(); + })); it('should save solution when user click', () => { spyOn(component.saveSolution, 'emit'); diff --git a/core/templates/components/state-editor/state-solution-editor/state-solution-editor.component.ts b/core/templates/components/state-editor/state-solution-editor/state-solution-editor.component.ts index 32c0fd63048c..82b4b1b599ec 100644 --- a/core/templates/components/state-editor/state-solution-editor/state-solution-editor.component.ts +++ b/core/templates/components/state-editor/state-solution-editor/state-solution-editor.component.ts @@ -17,29 +17,29 @@ * state editor. */ -import { Component, EventEmitter, OnInit, Output } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { SolutionValidityService } from 'pages/exploration-editor-page/editor-tab/services/solution-validity.service'; -import { SolutionVerificationService } from 'pages/exploration-editor-page/editor-tab/services/solution-verification.service'; -import { AddOrUpdateSolutionModalComponent } from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/add-or-update-solution-modal.component'; -import { DeleteSolutionModalComponent } from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-solution-modal.component'; -import { AlertsService } from 'services/alerts.service'; -import { EditabilityService } from 'services/editability.service'; -import { ExplorationHtmlFormatterService } from 'services/exploration-html-formatter.service'; -import { ExternalSaveService } from 'services/external-save.service'; -import { StateCustomizationArgsService } from '../state-editor-properties-services/state-customization-args.service'; -import { StateEditorService } from '../state-editor-properties-services/state-editor.service'; -import { StateHintsService } from '../state-editor-properties-services/state-hints.service'; -import { StateInteractionIdService } from '../state-editor-properties-services/state-interaction-id.service'; -import { StateSolutionService } from '../state-editor-properties-services/state-solution.service'; -import { Solution } from 'domain/exploration/SolutionObjectFactory'; -import { AppConstants } from 'app.constants'; -import { StateEditorConstants } from '../state-editor.constants'; -import { ConvertToPlainTextPipe } from 'filters/string-utility-filters/convert-to-plain-text.pipe'; +import {Component, EventEmitter, OnInit, Output} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {SolutionValidityService} from 'pages/exploration-editor-page/editor-tab/services/solution-validity.service'; +import {SolutionVerificationService} from 'pages/exploration-editor-page/editor-tab/services/solution-verification.service'; +import {AddOrUpdateSolutionModalComponent} from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/add-or-update-solution-modal.component'; +import {DeleteSolutionModalComponent} from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-solution-modal.component'; +import {AlertsService} from 'services/alerts.service'; +import {EditabilityService} from 'services/editability.service'; +import {ExplorationHtmlFormatterService} from 'services/exploration-html-formatter.service'; +import {ExternalSaveService} from 'services/external-save.service'; +import {StateCustomizationArgsService} from '../state-editor-properties-services/state-customization-args.service'; +import {StateEditorService} from '../state-editor-properties-services/state-editor.service'; +import {StateHintsService} from '../state-editor-properties-services/state-hints.service'; +import {StateInteractionIdService} from '../state-editor-properties-services/state-interaction-id.service'; +import {StateSolutionService} from '../state-editor-properties-services/state-solution.service'; +import {Solution} from 'domain/exploration/SolutionObjectFactory'; +import {AppConstants} from 'app.constants'; +import {StateEditorConstants} from '../state-editor.constants'; +import {ConvertToPlainTextPipe} from 'filters/string-utility-filters/convert-to-plain-text.pipe'; import INTERACTION_SPECS from 'interactions/interaction_specs.json'; -import { GenerateContentIdService } from 'services/generate-content-id.service'; -import { InteractionSpecsKey } from 'pages/interaction-specs.constants'; +import {GenerateContentIdService} from 'services/generate-content-id.service'; +import {InteractionSpecsKey} from 'pages/interaction-specs.constants'; interface DeleteValue { index: number; @@ -48,7 +48,7 @@ interface DeleteValue { @Component({ selector: 'oppia-state-solution-editor', - templateUrl: './state-solution-editor.component.html' + templateUrl: './state-solution-editor.component.html', }) export class StateSolutionEditorComponent implements OnInit { // The state property is null until a solution is specified or removed. @@ -63,11 +63,11 @@ export class StateSolutionEditorComponent implements OnInit { correctAnswerEditorHtml!: string; inlineSolutionEditorIsActive: boolean = false; solutionCardIsShown: boolean = false; - INFO_MESSAGE_SOLUTION_IS_INVALID_FOR_QUESTION: string = ( - StateEditorConstants.INFO_MESSAGE_SOLUTION_IS_INVALID_FOR_QUESTION); + INFO_MESSAGE_SOLUTION_IS_INVALID_FOR_QUESTION: string = + StateEditorConstants.INFO_MESSAGE_SOLUTION_IS_INVALID_FOR_QUESTION; - INFO_MESSAGE_SOLUTION_IS_INVALID_FOR_EXPLORATION: string = ( - AppConstants.INFO_MESSAGE_SOLUTION_IS_INVALID_FOR_EXPLORATION); + INFO_MESSAGE_SOLUTION_IS_INVALID_FOR_EXPLORATION: string = + AppConstants.INFO_MESSAGE_SOLUTION_IS_INVALID_FOR_EXPLORATION; constructor( private alertsService: AlertsService, @@ -83,21 +83,23 @@ export class StateSolutionEditorComponent implements OnInit { private stateEditorService: StateEditorService, private stateHintsService: StateHintsService, private stateInteractionIdService: StateInteractionIdService, - private stateSolutionService: StateSolutionService, + private stateSolutionService: StateSolutionService ) {} ngOnInit(): void { this.solutionCardIsShown = true; this.inlineSolutionEditorIsActive = false; - this.SOLUTION_EDITOR_FOCUS_LABEL = ( - 'currentCorrectAnswerEditorHtmlForSolutionEditor'); + this.SOLUTION_EDITOR_FOCUS_LABEL = + 'currentCorrectAnswerEditorHtmlForSolutionEditor'; this.stateEditorService.updateStateSolutionEditorInitialised(); - this.correctAnswerEditorHtml = ( + this.correctAnswerEditorHtml = this.explorationHtmlFormatterService.getInteractionHtml( this.stateInteractionIdService.savedMemento, this.stateCustomizationArgsService.savedMemento, false, - this.SOLUTION_EDITOR_FOCUS_LABEL, null)); + this.SOLUTION_EDITOR_FOCUS_LABEL, + null + ); } displayedHintsLength(): number { @@ -106,12 +108,16 @@ export class StateSolutionEditorComponent implements OnInit { getInvalidSolutionTooltip(): string { if (this.stateEditorService.isInQuestionMode()) { - return 'This solution doesn\'t correspond to an answer ' + + return ( + "This solution doesn't correspond to an answer " + 'marked as correct. Verify the rules specified for the ' + - 'answers or change the solution.'; + 'answers or change the solution.' + ); } - return 'This solution does not lead to another card. Verify the ' + - 'responses specified or change the solution.'; + return ( + 'This solution does not lead to another card. Verify the ' + + 'responses specified or change the solution.' + ); } isSolutionValid(): boolean { @@ -121,12 +127,12 @@ export class StateSolutionEditorComponent implements OnInit { isEditable(): boolean { return ( this.editabilityService.isEditable() && - this.editabilityService.isEditableOutsideTutorialMode()); + this.editabilityService.isEditableOutsideTutorialMode() + ); } toggleInlineSolutionEditorIsActive(): void { - this.inlineSolutionEditorIsActive = ( - !this.inlineSolutionEditorIsActive); + this.inlineSolutionEditorIsActive = !this.inlineSolutionEditorIsActive; } getSolutionSummary(): string { @@ -135,11 +141,12 @@ export class StateSolutionEditorComponent implements OnInit { if (solution === null) { throw new Error('Expected solution to be non-null.'); } - const solutionSummary = ( - solution.getSummary( - interactionId, this.stateCustomizationArgsService.savedMemento)); - const solutionAsPlainText = ( - this.convertToPlainText.transform(solutionSummary)); + const solutionSummary = solution.getSummary( + interactionId, + this.stateCustomizationArgsService.savedMemento + ); + const solutionAsPlainText = + this.convertToPlainText.transform(solutionSummary); return solutionAsPlainText; } @@ -151,8 +158,10 @@ export class StateSolutionEditorComponent implements OnInit { // This returns false if the current interaction ID is null. isCurrentInteractionLinear(): boolean { let savedMemento = this.stateInteractionIdService.savedMemento; - return (Boolean(savedMemento) && INTERACTION_SPECS[ - savedMemento as InteractionSpecsKey].is_linear); + return ( + Boolean(savedMemento) && + INTERACTION_SPECS[savedMemento as InteractionSpecsKey].is_linear + ); } onSaveSolution(value: Solution | null): void { @@ -163,63 +172,79 @@ export class StateSolutionEditorComponent implements OnInit { this.alertsService.clearWarnings(); this.externalSaveService.onExternalSave.emit(); this.inlineSolutionEditorIsActive = false; - this.ngbModal.open(AddOrUpdateSolutionModalComponent, { - backdrop: 'static' - }).result.then((result) => { - this.stateSolutionService.displayed = result.solution; - this.stateSolutionService.saveDisplayedValue(); - this.onSaveSolution(this.stateSolutionService.displayed); - let activeStateName = this.stateEditorService.getActiveStateName(); - if (activeStateName === null) { - throw new Error('Expected active state name to be non-null.'); - } - let solutionIsValid = this.solutionVerificationService.verifySolution( - activeStateName, - this.stateEditorService.getInteraction(), - result.solution.correctAnswer - ); + this.ngbModal + .open(AddOrUpdateSolutionModalComponent, { + backdrop: 'static', + }) + .result.then( + result => { + this.stateSolutionService.displayed = result.solution; + this.stateSolutionService.saveDisplayedValue(); + this.onSaveSolution(this.stateSolutionService.displayed); + let activeStateName = this.stateEditorService.getActiveStateName(); + if (activeStateName === null) { + throw new Error('Expected active state name to be non-null.'); + } + let solutionIsValid = this.solutionVerificationService.verifySolution( + activeStateName, + this.stateEditorService.getInteraction(), + result.solution.correctAnswer + ); - this.solutionValidityService.updateValidity( - activeStateName, solutionIsValid); - this.refreshWarnings.emit(); - this.getSolutionChange.emit(); - if (!solutionIsValid) { - if (this.stateEditorService.isInQuestionMode()) { - this.alertsService.addInfoMessage( - this.INFO_MESSAGE_SOLUTION_IS_INVALID_FOR_QUESTION, 4000); - } else { - this.alertsService.addInfoMessage( - this.INFO_MESSAGE_SOLUTION_IS_INVALID_FOR_EXPLORATION, 4000); + this.solutionValidityService.updateValidity( + activeStateName, + solutionIsValid + ); + this.refreshWarnings.emit(); + this.getSolutionChange.emit(); + if (!solutionIsValid) { + if (this.stateEditorService.isInQuestionMode()) { + this.alertsService.addInfoMessage( + this.INFO_MESSAGE_SOLUTION_IS_INVALID_FOR_QUESTION, + 4000 + ); + } else { + this.alertsService.addInfoMessage( + this.INFO_MESSAGE_SOLUTION_IS_INVALID_FOR_EXPLORATION, + 4000 + ); + } + } + }, + () => { + this.generateContentIdService.revertUnusedContentIdIndex(); + this.alertsService.clearWarnings(); + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. } - } - }, () => { - this.generateContentIdService.revertUnusedContentIdIndex(); - this.alertsService.clearWarnings(); - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + ); } deleteSolution(value: DeleteValue): void { value.evt.stopPropagation(); this.alertsService.clearWarnings(); - this.ngbModal.open(DeleteSolutionModalComponent, { - backdrop: true, - }).result.then(() => { - this.stateSolutionService.displayed = null; - this.stateSolutionService.saveDisplayedValue(); - this.onSaveSolution(this.stateSolutionService.displayed); - this.stateEditorService.deleteCurrentSolutionValidity(); - this.refreshWarnings.emit(); - this.getSolutionChange.emit(); - }, () => { - this.alertsService.clearWarnings(); - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + this.ngbModal + .open(DeleteSolutionModalComponent, { + backdrop: true, + }) + .result.then( + () => { + this.stateSolutionService.displayed = null; + this.stateSolutionService.saveDisplayedValue(); + this.onSaveSolution(this.stateSolutionService.displayed); + this.stateEditorService.deleteCurrentSolutionValidity(); + this.refreshWarnings.emit(); + this.getSolutionChange.emit(); + }, + () => { + this.alertsService.clearWarnings(); + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } toggleSolutionCard(): void { @@ -227,7 +252,9 @@ export class StateSolutionEditorComponent implements OnInit { } } -angular.module('oppia').directive('oppiaStateSolutionEditor', -downgradeComponent({ - component: StateSolutionEditorComponent -}) as angular.IDirectiveFactory); +angular.module('oppia').directive( + 'oppiaStateSolutionEditor', + downgradeComponent({ + component: StateSolutionEditorComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/components/statistics-directives/completion-graph.component.spec.ts b/core/templates/components/statistics-directives/completion-graph.component.spec.ts index a4a536521528..eb4f6b5465f1 100644 --- a/core/templates/components/statistics-directives/completion-graph.component.spec.ts +++ b/core/templates/components/statistics-directives/completion-graph.component.spec.ts @@ -17,12 +17,12 @@ * the improvements tab. */ -import { ImprovementsConstants } from 'domain/improvements/improvements.constants'; +import {ImprovementsConstants} from 'domain/improvements/improvements.constants'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { CompletionGraphComponent } from './completion-graph.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {CompletionGraphComponent} from './completion-graph.component'; describe('Completion Graph Component', () => { let fixture: ComponentFixture; @@ -30,14 +30,10 @@ describe('Completion Graph Component', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - CompletionGraphComponent, - ], + imports: [HttpClientTestingModule], + declarations: [CompletionGraphComponent], providers: [], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); @@ -51,9 +47,11 @@ describe('Completion Graph Component', () => { component.ngOnInit(); expect(component.completionBarStyle['stroke-dasharray']).toBeCloseTo( - ImprovementsConstants.COMPLETION_BAR_ARC_LENGTH); + ImprovementsConstants.COMPLETION_BAR_ARC_LENGTH + ); expect(component.completionBarStyle['stroke-dashoffset']).toBeCloseTo( ImprovementsConstants.COMPLETION_BAR_ARC_LENGTH * - (1 - component.completionRate)); + (1 - component.completionRate) + ); }); }); diff --git a/core/templates/components/statistics-directives/completion-graph.component.ts b/core/templates/components/statistics-directives/completion-graph.component.ts index 8d95a2964c56..1e93387e1200 100644 --- a/core/templates/components/statistics-directives/completion-graph.component.ts +++ b/core/templates/components/statistics-directives/completion-graph.component.ts @@ -16,9 +16,9 @@ * @fileoverview Component for the completion graph of the improvements tab. */ -import { Component, Input, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { ImprovementsConstants } from 'domain/improvements/improvements.constants'; +import {Component, Input, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {ImprovementsConstants} from 'domain/improvements/improvements.constants'; @Component({ selector: 'oppia-completion-graph', @@ -39,14 +39,16 @@ export class CompletionGraphComponent implements OnInit { ngOnInit(): void { this.completionBarStyle = { 'stroke-dasharray': ImprovementsConstants.COMPLETION_BAR_ARC_LENGTH, - 'stroke-dashoffset': ( + 'stroke-dashoffset': (1.0 - this.completionRate) * - ImprovementsConstants.COMPLETION_BAR_ARC_LENGTH), + ImprovementsConstants.COMPLETION_BAR_ARC_LENGTH, }; } } -angular.module('oppia').directive('oppiaCompletionGraph', +angular.module('oppia').directive( + 'oppiaCompletionGraph', downgradeComponent({ - component: CompletionGraphComponent - }) as angular.IDirectiveFactory); + component: CompletionGraphComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/components/summary-tile/collection-summary-tile.component.spec.ts b/core/templates/components/summary-tile/collection-summary-tile.component.spec.ts index 4d0eaca29f2c..8023ccc8b5c3 100644 --- a/core/templates/components/summary-tile/collection-summary-tile.component.spec.ts +++ b/core/templates/components/summary-tile/collection-summary-tile.component.spec.ts @@ -16,29 +16,32 @@ * @fileoverview Unit tests for for CollectionSummaryTileComponent. */ -import { async, ComponentFixture, fakeAsync, TestBed, tick } from - '@angular/core/testing'; -import { Component, NO_ERRORS_SCHEMA, Pipe } from '@angular/core'; -import { MaterialModule } from 'modules/material.module'; -import { FormsModule } from '@angular/forms'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; - -import { WindowRef } from 'services/contextual/window-ref.service'; -import { CollectionSummaryTileComponent } from './collection-summary-tile.component'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { UrlService } from 'services/contextual/url.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { of } from 'rxjs'; -import { DateTimeFormatService } from 'services/date-time-format.service'; -import { UserInfo } from 'domain/user/user-info.model'; -import { UserService } from 'services/user.service'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; - +import { + async, + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {Component, NO_ERRORS_SCHEMA, Pipe} from '@angular/core'; +import {MaterialModule} from 'modules/material.module'; +import {FormsModule} from '@angular/forms'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; + +import {WindowRef} from 'services/contextual/window-ref.service'; +import {CollectionSummaryTileComponent} from './collection-summary-tile.component'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {UrlService} from 'services/contextual/url.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {of} from 'rxjs'; +import {DateTimeFormatService} from 'services/date-time-format.service'; +import {UserInfo} from 'domain/user/user-info.model'; +import {UserService} from 'services/user.service'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; @Component({selector: 'learner-dashboard-icons', template: ''}) -class LearnerDashboardIconsComponentStub { -} +class LearnerDashboardIconsComponentStub {} @Pipe({name: 'truncateAndCapitalize'}) class MockTruncteAndCapitalizePipe { @@ -73,8 +76,16 @@ describe('Collection Summary Tile Component', () => { let windowDimensionsService: MockWindowDimensionsService; let userInfo: UserInfo = new UserInfo( - ['USER_ROLE'], true, false, false, false, true, - 'en', 'username1', 'tester@example.com', true + ['USER_ROLE'], + true, + false, + false, + false, + true, + 'en', + 'username1', + 'tester@example.com', + true ); beforeEach(async(() => { @@ -83,13 +94,13 @@ describe('Collection Summary Tile Component', () => { BrowserAnimationsModule, MaterialModule, FormsModule, - HttpClientTestingModule + HttpClientTestingModule, ], declarations: [ CollectionSummaryTileComponent, MockTruncteAndCapitalizePipe, LearnerDashboardIconsComponentStub, - MockTranslatePipe + MockTranslatePipe, ], providers: [ WindowRef, @@ -98,14 +109,14 @@ describe('Collection Summary Tile Component', () => { UserService, { provide: UrlService, - useClass: MockUrlService + useClass: MockUrlService, }, { provide: WindowDimensionsService, - useClass: MockWindowDimensionsService - } + useClass: MockWindowDimensionsService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -135,10 +146,13 @@ describe('Collection Summary Tile Component', () => { it('should intialize the component and set values', fakeAsync(() => { const userServiceSpy = spyOn( - userService, 'getUserInfoAsync') - .and.returnValue(Promise.resolve(userInfo)); + userService, + 'getUserInfoAsync' + ).and.returnValue(Promise.resolve(userInfo)); const windowResizeSpy = spyOn( - windowDimensionsService, 'getResizeEvent').and.callThrough(); + windowDimensionsService, + 'getResizeEvent' + ).and.callThrough(); component.mobileCutoffPx = 536; component.ngOnInit(); @@ -152,41 +166,48 @@ describe('Collection Summary Tile Component', () => { expect(component.resizeSubscription).not.toBe(undefined); })); - it('should remove all subscriptions when ngOnDestroy is called', - fakeAsync(()=> { - component.resizeSubscription = of(new Event('resize')).subscribe(); - tick(); - fixture.detectChanges(); - - component.ngOnDestroy(); + it('should remove all subscriptions when ngOnDestroy is called', fakeAsync(() => { + component.resizeSubscription = of(new Event('resize')).subscribe(); + tick(); + fixture.detectChanges(); - tick(); - fixture.detectChanges(); - expect(component.resizeSubscription.closed).toBe(true); - })); + component.ngOnDestroy(); - it('should set mobileCutoffPx to 0 if it is not ' + - 'specified', fakeAsync(() => { - const userServiceSpy = spyOn( - userService, 'getUserInfoAsync') - .and.returnValue(Promise.resolve(userInfo)); - const windowResizeSpy = spyOn( - windowDimensionsService, 'getResizeEvent').and.callThrough(); - - component.ngOnInit(); tick(); fixture.detectChanges(); - - expect(userServiceSpy).toHaveBeenCalled(); - expect(windowResizeSpy).toHaveBeenCalled(); - expect(component.mobileCutoffPx).toBe(0); + expect(component.resizeSubscription.closed).toBe(true); })); + it( + 'should set mobileCutoffPx to 0 if it is not ' + 'specified', + fakeAsync(() => { + const userServiceSpy = spyOn( + userService, + 'getUserInfoAsync' + ).and.returnValue(Promise.resolve(userInfo)); + const windowResizeSpy = spyOn( + windowDimensionsService, + 'getResizeEvent' + ).and.callThrough(); + + component.ngOnInit(); + tick(); + fixture.detectChanges(); + + expect(userServiceSpy).toHaveBeenCalled(); + expect(windowResizeSpy).toHaveBeenCalled(); + expect(component.mobileCutoffPx).toBe(0); + }) + ); + it('should check if mobile card should be shown', () => { - const urlServiceSpy = spyOn(urlService, 'getPathname').and - .returnValue('/community-library'); + const urlServiceSpy = spyOn(urlService, 'getPathname').and.returnValue( + '/community-library' + ); const windowWidthSpy = spyOn( - windowDimensionsService, 'getWidth').and.returnValue(530); + windowDimensionsService, + 'getWidth' + ).and.returnValue(530); component.mobileCutoffPx = 537; component.checkIfMobileCardToBeShown(); @@ -204,8 +225,9 @@ describe('Collection Summary Tile Component', () => { it('should get the last updated Date & time', () => { const dateTimeSpy = spyOn( - dateTimeFormatService, 'getLocaleAbbreviatedDatetimeString') - .and.returnValue('1:30 am'); + dateTimeFormatService, + 'getLocaleAbbreviatedDatetimeString' + ).and.returnValue('1:30 am'); component.getLastUpdatedDatetime(); fixture.detectChanges(); @@ -214,8 +236,10 @@ describe('Collection Summary Tile Component', () => { }); it('should get relative last updated Date & time', () => { - const dateTimeSpy = spyOn(dateTimeFormatService, 'getRelativeTimeFromNow') - .and.returnValue('a few seconds ago'); + const dateTimeSpy = spyOn( + dateTimeFormatService, + 'getRelativeTimeFromNow' + ).and.returnValue('a few seconds ago'); // Date.now() returns the current time in milliseconds since the // Epoch. @@ -242,8 +266,9 @@ describe('Collection Summary Tile Component', () => { it('should get the collection link url for editor page', () => { const urlSpy = spyOn( - urlInterpolationService, 'interpolateUrl') - .and.returnValue('/collection_editor/create/1'); + urlInterpolationService, + 'interpolateUrl' + ).and.returnValue('/collection_editor/create/1'); component.getCollectionLink(); fixture.detectChanges(); @@ -253,8 +278,9 @@ describe('Collection Summary Tile Component', () => { it('should get the collection link url for viewer page', () => { const urlSpy = spyOn( - urlInterpolationService, 'interpolateUrl') - .and.returnValue('/collection/1'); + urlInterpolationService, + 'interpolateUrl' + ).and.returnValue('/collection/1'); component.isLinkedToEditorPage = false; fixture.detectChanges(); @@ -267,8 +293,9 @@ describe('Collection Summary Tile Component', () => { it('should get the thumbnail url', () => { const urlSpy = spyOn( - urlInterpolationService, 'getStaticImageUrl') - .and.returnValue('thumbnailUrl'); + urlInterpolationService, + 'getStaticImageUrl' + ).and.returnValue('thumbnailUrl'); component.getThumbnailIconUrl = 'thumbnailUrl'; component.getCompleteThumbnailIconUrl(); @@ -279,8 +306,9 @@ describe('Collection Summary Tile Component', () => { it('should get the image url', () => { const urlSpy = spyOn( - urlInterpolationService, 'getStaticImageUrl') - .and.returnValue('imageUrl'); + urlInterpolationService, + 'getStaticImageUrl' + ).and.returnValue('imageUrl'); component.getCompleteThumbnailIconUrl(); fixture.detectChanges(); diff --git a/core/templates/components/summary-tile/collection-summary-tile.component.ts b/core/templates/components/summary-tile/collection-summary-tile.component.ts index b397655fa113..7d3092263225 100644 --- a/core/templates/components/summary-tile/collection-summary-tile.component.ts +++ b/core/templates/components/summary-tile/collection-summary-tile.component.ts @@ -16,17 +16,17 @@ * @fileoverview Component for a collection summary tile. */ -import { Component, Input, OnInit, OnDestroy } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; - -import { AppConstants } from 'app.constants'; -import { CollectionSummaryTileConstants } from 'components/summary-tile/collection-summary-tile.constants'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { DateTimeFormatService } from 'services/date-time-format.service'; -import { UserService } from 'services/user.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { UrlService } from 'services/contextual/url.service'; -import { Subscription } from 'rxjs'; +import {Component, Input, OnInit, OnDestroy} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; + +import {AppConstants} from 'app.constants'; +import {CollectionSummaryTileConstants} from 'components/summary-tile/collection-summary-tile.constants'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {DateTimeFormatService} from 'services/date-time-format.service'; +import {UserService} from 'services/user.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {UrlService} from 'services/contextual/url.service'; +import {Subscription} from 'rxjs'; @Component({ selector: 'oppia-collection-summary-tile', @@ -80,7 +80,8 @@ export class CollectionSummaryTileComponent implements OnInit, OnDestroy { this.checkIfMobileCardToBeShown(); this.defaultEmptyTitle = CollectionSummaryTileConstants.DEFAULT_EMPTY_TITLE; this.activityTypeCollection = AppConstants.ACTIVITY_TYPE_COLLECTION; - this.resizeSubscription = this.windowDimensionsService.getResizeEvent() + this.resizeSubscription = this.windowDimensionsService + .getResizeEvent() .subscribe(event => { this.checkIfMobileCardToBeShown(); }); @@ -97,39 +98,38 @@ export class CollectionSummaryTileComponent implements OnInit, OnDestroy { let mobileViewActive = this.windowDimensionsService.getWidth() < this.mobileCutoffPx; let currentPageUrl = this.urlService.getPathname(); - this.mobileCardToBeShown = ( - mobileViewActive && (currentPageUrl === '/community-library')); + this.mobileCardToBeShown = + mobileViewActive && currentPageUrl === '/community-library'; } getLastUpdatedDatetime(): string { return this.dateTimeFormatService.getLocaleAbbreviatedDatetimeString( - this.getLastUpdatedMsec); + this.getLastUpdatedMsec + ); } getRelativeLastUpdatedDateTime(): string | null { if (this.getLastUpdatedMsec) { return this.dateTimeFormatService.getRelativeTimeFromNow( - this.getLastUpdatedMsec); + this.getLastUpdatedMsec + ); } return null; } getCollectionLink(): string | null { - let targetUrl = ( - this.isLinkedToEditorPage ? - CollectionSummaryTileConstants.COLLECTION_EDITOR_URL : - CollectionSummaryTileConstants.COLLECTION_VIEWER_URL - ); - return this.urlInterpolationService.interpolateUrl( - targetUrl, { - collection_id: this.getCollectionId - } - ); + let targetUrl = this.isLinkedToEditorPage + ? CollectionSummaryTileConstants.COLLECTION_EDITOR_URL + : CollectionSummaryTileConstants.COLLECTION_VIEWER_URL; + return this.urlInterpolationService.interpolateUrl(targetUrl, { + collection_id: this.getCollectionId, + }); } getCompleteThumbnailIconUrl(): string { return this.urlInterpolationService.getStaticImageUrl( - this.getThumbnailIconUrl); + this.getThumbnailIconUrl + ); } getStaticImageUrl(imagePath: string): string { @@ -141,6 +141,9 @@ export class CollectionSummaryTileComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive( - 'oppiaCollectionSummaryTile', downgradeComponent( - {component: CollectionSummaryTileComponent})); +angular + .module('oppia') + .directive( + 'oppiaCollectionSummaryTile', + downgradeComponent({component: CollectionSummaryTileComponent}) + ); diff --git a/core/templates/components/summary-tile/collection-summary-tile.constants.ajs.ts b/core/templates/components/summary-tile/collection-summary-tile.constants.ajs.ts index 672d10eac562..85b5fcdfe963 100644 --- a/core/templates/components/summary-tile/collection-summary-tile.constants.ajs.ts +++ b/core/templates/components/summary-tile/collection-summary-tile.constants.ajs.ts @@ -18,12 +18,17 @@ // TODO(#7092): Delete this file once migration is complete and these AngularJS // equivalents of the Angular constants are no longer needed. -import { CollectionSummaryTileConstants } from - 'components/summary-tile/collection-summary-tile.constants'; +import {CollectionSummaryTileConstants} from 'components/summary-tile/collection-summary-tile.constants'; -angular.module('oppia').constant( - 'COLLECTION_VIEWER_URL', - CollectionSummaryTileConstants.COLLECTION_VIEWER_URL); -angular.module('oppia').constant( - 'COLLECTION_EDITOR_URL', - CollectionSummaryTileConstants.COLLECTION_EDITOR_URL); +angular + .module('oppia') + .constant( + 'COLLECTION_VIEWER_URL', + CollectionSummaryTileConstants.COLLECTION_VIEWER_URL + ); +angular + .module('oppia') + .constant( + 'COLLECTION_EDITOR_URL', + CollectionSummaryTileConstants.COLLECTION_EDITOR_URL + ); diff --git a/core/templates/components/summary-tile/collection-summary-tile.constants.ts b/core/templates/components/summary-tile/collection-summary-tile.constants.ts index e5d5a526dcd3..ac55639dbcee 100644 --- a/core/templates/components/summary-tile/collection-summary-tile.constants.ts +++ b/core/templates/components/summary-tile/collection-summary-tile.constants.ts @@ -18,7 +18,6 @@ export const CollectionSummaryTileConstants = { COLLECTION_VIEWER_URL: '/collection/', - COLLECTION_EDITOR_URL: - '/collection_editor/create/', - DEFAULT_EMPTY_TITLE: 'Untitled' + COLLECTION_EDITOR_URL: '/collection_editor/create/', + DEFAULT_EMPTY_TITLE: 'Untitled', } as const; diff --git a/core/templates/components/summary-tile/exploration-summary-tile.component.spec.ts b/core/templates/components/summary-tile/exploration-summary-tile.component.spec.ts index 1b7180edecd7..07d8516b255a 100644 --- a/core/templates/components/summary-tile/exploration-summary-tile.component.spec.ts +++ b/core/templates/components/summary-tile/exploration-summary-tile.component.spec.ts @@ -16,30 +16,34 @@ * @fileoverview Unit tests for for ExplorationSummaryTileComponent. */ -import { async, ComponentFixture, fakeAsync, TestBed, tick } from - '@angular/core/testing'; -import { Component, NO_ERRORS_SCHEMA, Pipe } from '@angular/core'; -import { MaterialModule } from 'modules/material.module'; -import { FormsModule } from '@angular/forms'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; - -import { WindowRef } from 'services/contextual/window-ref.service'; -import { ExplorationSummaryTileComponent } from './exploration-summary-tile.component'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { DateTimeFormatService } from 'services/date-time-format.service'; -import { UserService } from 'services/user.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { of } from 'rxjs'; -import { UrlParamsType, UrlService } from 'services/contextual/url.service'; -import { RatingComputationService } from 'components/ratings/rating-computation/rating-computation.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { UserInfo } from 'domain/user/user-info.model'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; +import { + async, + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {Component, NO_ERRORS_SCHEMA, Pipe} from '@angular/core'; +import {MaterialModule} from 'modules/material.module'; +import {FormsModule} from '@angular/forms'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; + +import {WindowRef} from 'services/contextual/window-ref.service'; +import {ExplorationSummaryTileComponent} from './exploration-summary-tile.component'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {DateTimeFormatService} from 'services/date-time-format.service'; +import {UserService} from 'services/user.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {of} from 'rxjs'; +import {UrlParamsType, UrlService} from 'services/contextual/url.service'; +import {RatingComputationService} from 'components/ratings/rating-computation/rating-computation.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {UserInfo} from 'domain/user/user-info.model'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; @Component({selector: 'learner-dashboard-icons', template: ''}) -class LearnerDashboardIconsComponentStub { -} +class LearnerDashboardIconsComponentStub {} @Pipe({name: 'truncateAndCapitalize'}) class MockTruncteAndCapitalizePipe { @@ -83,7 +87,7 @@ class MockWindowRef { set href(val) { this._href = val; }, - reload: (val: string) => val + reload: (val: string) => val, }, get onhashchange() { return this.location._hashChange; @@ -91,7 +95,7 @@ class MockWindowRef { set onhashchange(val) { this.location._hashChange = val; - } + }, }; get nativeWindow() { @@ -103,8 +107,13 @@ class MockUrlService { addField(url: string, fieldName: string, fieldValue: string): string { let encodedFieldValue = fieldValue; let encodedFieldName = fieldName; - return url + (url.indexOf('?') !== -1 ? '&' : '?') + encodedFieldName + - '=' + encodedFieldValue; + return ( + url + + (url.indexOf('?') !== -1 ? '&' : '?') + + encodedFieldName + + '=' + + encodedFieldValue + ); } getPathname(): string { @@ -138,8 +147,16 @@ describe('Exploration Summary Tile Component', () => { let i18nLanguageCodeService: I18nLanguageCodeService; let userInfo = new UserInfo( - ['USER_ROLE'], true, false, false, false, true, - 'en', 'username1', 'tester@example.com', true + ['USER_ROLE'], + true, + false, + false, + false, + true, + 'en', + 'username1', + 'tester@example.com', + true ); beforeEach(async(() => { @@ -149,7 +166,7 @@ describe('Exploration Summary Tile Component', () => { BrowserAnimationsModule, MaterialModule, FormsModule, - HttpClientTestingModule + HttpClientTestingModule, ], declarations: [ ExplorationSummaryTileComponent, @@ -166,21 +183,21 @@ describe('Exploration Summary Tile Component', () => { RatingComputationService, { provide: UrlService, - useClass: MockUrlService + useClass: MockUrlService, }, { provide: WindowDimensionsService, useValue: { getWidth: () => 1000, - getResizeEvent: () => of(resizeEvent) - } + getResizeEvent: () => of(resizeEvent), + }, }, { provide: WindowRef, - useValue: windowRef - } + useValue: windowRef, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -208,7 +225,7 @@ describe('Exploration Summary Tile Component', () => { }, username2: { num_commits: 2, - } + }, }; component.thumbnailIconUrl = '/subjects/Welcome'; component.thumbnailBgColor = 'blue'; @@ -225,16 +242,25 @@ describe('Exploration Summary Tile Component', () => { it('should intialize the component and set values', fakeAsync(() => { const userServiceSpy = spyOn( - userService, 'getUserInfoAsync') - .and.returnValue(Promise.resolve(userInfo)); + userService, + 'getUserInfoAsync' + ).and.returnValue(Promise.resolve(userInfo)); const windowResizeSpy = spyOn( - windowDimensionsService, 'getResizeEvent').and.callThrough(); + windowDimensionsService, + 'getResizeEvent' + ).and.callThrough(); const windowWidthSpy = spyOn( - windowDimensionsService, 'getWidth').and.callThrough(); + windowDimensionsService, + 'getWidth' + ).and.callThrough(); component.mobileCutoffPx = 536; - spyOn(i18nLanguageCodeService, 'getExplorationTranslationKey') - .and.returnValues( - 'I18N_EXPLORATION_123ab_TITLE', 'I18N_EXPLORATION_123ab_DESCRIPTION'); + spyOn( + i18nLanguageCodeService, + 'getExplorationTranslationKey' + ).and.returnValues( + 'I18N_EXPLORATION_123ab_TITLE', + 'I18N_EXPLORATION_123ab_DESCRIPTION' + ); component.ngOnInit(); tick(); @@ -244,82 +270,99 @@ describe('Exploration Summary Tile Component', () => { expect(component.isRefresherExploration).toBe(true); expect(component.isWindowLarge).toBe(true); expect(component.expTitleTranslationKey).toBe( - 'I18N_EXPLORATION_123ab_TITLE'); + 'I18N_EXPLORATION_123ab_TITLE' + ); expect(component.expObjectiveTranslationKey).toBe( - 'I18N_EXPLORATION_123ab_DESCRIPTION'); + 'I18N_EXPLORATION_123ab_DESCRIPTION' + ); expect(userServiceSpy).toHaveBeenCalled(); expect(windowResizeSpy).toHaveBeenCalled(); expect(windowWidthSpy).toHaveBeenCalled(); })); - it('should check whether hacky translations are displayed or not' - , fakeAsync(() => { - const userServiceSpy = spyOn( - userService, 'getUserInfoAsync') - .and.returnValue(Promise.resolve(userInfo)); - const windowResizeSpy = spyOn( - windowDimensionsService, 'getResizeEvent').and.callThrough(); - const windowWidthSpy = spyOn( - windowDimensionsService, 'getWidth').and.callThrough(); - spyOn(i18nLanguageCodeService, 'isHackyTranslationAvailable') - .and.returnValues(false, true); - spyOn(i18nLanguageCodeService, 'isCurrentLanguageEnglish') - .and.returnValues(false, false); - - component.ngOnInit(); - tick(); - fixture.detectChanges(); - - expect(userServiceSpy).toHaveBeenCalled(); - expect(windowResizeSpy).toHaveBeenCalled(); - expect(windowWidthSpy).toHaveBeenCalled(); - let hackyTranslationIsDisplayed = - component.isHackyExpTitleTranslationDisplayed(); - expect(hackyTranslationIsDisplayed).toBe(false); - hackyTranslationIsDisplayed = - component.isHackyExpObjectiveTranslationDisplayed(); - expect(hackyTranslationIsDisplayed).toBe(true); - })); - - it('should intialize the component and set mobileCutoffPx to 0' + - ' if it is undefined', fakeAsync(() => { + it('should check whether hacky translations are displayed or not', fakeAsync(() => { const userServiceSpy = spyOn( - userService, 'getUserInfoAsync') - .and.returnValue(Promise.resolve(userInfo)); + userService, + 'getUserInfoAsync' + ).and.returnValue(Promise.resolve(userInfo)); const windowResizeSpy = spyOn( - windowDimensionsService, 'getResizeEvent').and.callThrough(); + windowDimensionsService, + 'getResizeEvent' + ).and.callThrough(); const windowWidthSpy = spyOn( - windowDimensionsService, 'getWidth').and.callThrough(); + windowDimensionsService, + 'getWidth' + ).and.callThrough(); + spyOn( + i18nLanguageCodeService, + 'isHackyTranslationAvailable' + ).and.returnValues(false, true); + spyOn(i18nLanguageCodeService, 'isCurrentLanguageEnglish').and.returnValues( + false, + false + ); component.ngOnInit(); tick(); fixture.detectChanges(); - expect(component.mobileCutoffPx).toBe(0); - expect(userServiceSpy).toHaveBeenCalled(); expect(windowResizeSpy).toHaveBeenCalled(); expect(windowWidthSpy).toHaveBeenCalled(); + let hackyTranslationIsDisplayed = + component.isHackyExpTitleTranslationDisplayed(); + expect(hackyTranslationIsDisplayed).toBe(false); + hackyTranslationIsDisplayed = + component.isHackyExpObjectiveTranslationDisplayed(); + expect(hackyTranslationIsDisplayed).toBe(true); })); - it('should remove all subscriptions when calling ngOnDestroy', + it( + 'should intialize the component and set mobileCutoffPx to 0' + + ' if it is undefined', fakeAsync(() => { - component.resizeSubscription = of(resizeEvent).subscribe(); + const userServiceSpy = spyOn( + userService, + 'getUserInfoAsync' + ).and.returnValue(Promise.resolve(userInfo)); + const windowResizeSpy = spyOn( + windowDimensionsService, + 'getResizeEvent' + ).and.callThrough(); + const windowWidthSpy = spyOn( + windowDimensionsService, + 'getWidth' + ).and.callThrough(); + + component.ngOnInit(); tick(); fixture.detectChanges(); - component.ngOnDestroy(); + expect(component.mobileCutoffPx).toBe(0); - tick(); - fixture.detectChanges(); - expect(component.resizeSubscription.closed).toBe(true); + expect(userServiceSpy).toHaveBeenCalled(); + expect(windowResizeSpy).toHaveBeenCalled(); + expect(windowWidthSpy).toHaveBeenCalled(); }) ); + it('should remove all subscriptions when calling ngOnDestroy', fakeAsync(() => { + component.resizeSubscription = of(resizeEvent).subscribe(); + tick(); + fixture.detectChanges(); + + component.ngOnDestroy(); + + tick(); + fixture.detectChanges(); + expect(component.resizeSubscription.closed).toBe(true); + })); + it('should check if mobile card is to be shown', () => { - const urlPathSpy = spyOn(urlService, 'getPathname') - .and.returnValue('/community-library'); + const urlPathSpy = spyOn(urlService, 'getPathname').and.returnValue( + '/community-library' + ); component.isWindowLarge = false; component.checkIfMobileCardToBeShown(); @@ -349,8 +392,10 @@ describe('Exploration Summary Tile Component', () => { }); it('should navigate to parent exploration', fakeAsync(() => { - const explorationLinkSpy = spyOn(component, 'getExplorationLink') - .and.returnValue('/parent/id/1'); + const explorationLinkSpy = spyOn( + component, + 'getExplorationLink' + ).and.returnValue('/parent/id/1'); spyOnProperty(windowRef, 'nativeWindow').and.returnValue({ location: { @@ -380,11 +425,12 @@ describe('Exploration Summary Tile Component', () => { 2: 0, 3: 0, 4: 0, - 5: 1 + 5: 1, }; const ratingsSpy = spyOn( - ratingComputationService, 'computeAverageRating') - .and.returnValue(3); + ratingComputationService, + 'computeAverageRating' + ).and.returnValue(3); let averageRatings = component.getAverageRating(); tick(); @@ -394,24 +440,29 @@ describe('Exploration Summary Tile Component', () => { expect(averageRatings).toBe(3); })); - it('should fail to get the average ratings of the exploration' + - ' if rating are undefined', fakeAsync(() => { - const ratingsSpy = spyOn( - ratingComputationService, 'computeAverageRating') - .and.returnValue(null); + it( + 'should fail to get the average ratings of the exploration' + + ' if rating are undefined', + fakeAsync(() => { + const ratingsSpy = spyOn( + ratingComputationService, + 'computeAverageRating' + ).and.returnValue(null); - let averageRatings = component.getAverageRating(); - tick(); - fixture.detectChanges(); + let averageRatings = component.getAverageRating(); + tick(); + fixture.detectChanges(); - expect(ratingsSpy).not.toHaveBeenCalled(); - expect(averageRatings).toBeNull(); - })); + expect(ratingsSpy).not.toHaveBeenCalled(); + expect(averageRatings).toBeNull(); + }) + ); it('should get last updated Date & time', () => { const dateTimeSpy = spyOn( - dateTimeFormatService, 'getLocaleAbbreviatedDatetimeString') - .and.returnValue('1:30 am'); + dateTimeFormatService, + 'getLocaleAbbreviatedDatetimeString' + ).and.returnValue('1:30 am'); component.lastUpdatedMsec = 1000; let dateTime = component.getLastUpdatedDatetime(); @@ -429,8 +480,10 @@ describe('Exploration Summary Tile Component', () => { }); it('should get relative last updated Date & time', () => { - const dateTimeSpy = spyOn(dateTimeFormatService, 'getRelativeTimeFromNow') - .and.returnValue('a few seconds ago'); + const dateTimeSpy = spyOn( + dateTimeFormatService, + 'getRelativeTimeFromNow' + ).and.returnValue('a few seconds ago'); component.lastUpdatedMsec = Date.now(); let relativeLastUpdatedDateTime = @@ -454,8 +507,9 @@ describe('Exploration Summary Tile Component', () => { it('should get the thumbnail url', () => { const urlSpy = spyOn( - urlInterpolationService, 'getStaticImageUrl') - .and.returnValue('thumbnailUrl'); + urlInterpolationService, + 'getStaticImageUrl' + ).and.returnValue('thumbnailUrl'); component.thumbnailIconUrl = 'thumbnailUrl'; component.getCompleteThumbnailIconUrl(); @@ -472,65 +526,78 @@ describe('Exploration Summary Tile Component', () => { expect(result).toBe('#'); }); - it('should return the url for the exploration' + - ' given collectionId and explorationId', fakeAsync(() => { - const urlParamsSpy = spyOn(urlService, 'getUrlParams').and.returnValue({ - collection_id: '1', - }); - const addFieldSpy = spyOn(urlService, 'addField').and.callThrough(); - const result = component.getExplorationLink(); - - tick(); - fixture.detectChanges(); - - expect(result).toBe('/explore/1?collection_id=1&parent=1&parent=2'); - expect(urlParamsSpy).toHaveBeenCalled(); - expect(addFieldSpy).toHaveBeenCalled(); - })); - + it( + 'should return the url for the exploration' + + ' given collectionId and explorationId', + fakeAsync(() => { + const urlParamsSpy = spyOn(urlService, 'getUrlParams').and.returnValue({ + collection_id: '1', + }); + const addFieldSpy = spyOn(urlService, 'addField').and.callThrough(); + const result = component.getExplorationLink(); - it('should return the url for the exploration' + - ' given explorationId and storyId', fakeAsync(() => { - const urlParamsSpy = spyOn(urlService, 'getUrlParams').and.returnValue({ - }); - const urlPathSpy = spyOn(urlService, 'getPathname').and.returnValue( - '/story/fhfhvhgvhvvh'); - const storyIdSpy = spyOn(urlService, 'getStoryIdFromViewerUrl') - .and.returnValue('1'); - const addFieldSpy = spyOn(urlService, 'addField').and.callThrough(); + tick(); + fixture.detectChanges(); - const result = component.getExplorationLink(); + expect(result).toBe('/explore/1?collection_id=1&parent=1&parent=2'); + expect(urlParamsSpy).toHaveBeenCalled(); + expect(addFieldSpy).toHaveBeenCalled(); + }) + ); - tick(); - fixture.detectChanges(); + it( + 'should return the url for the exploration' + + ' given explorationId and storyId', + fakeAsync(() => { + const urlParamsSpy = spyOn(urlService, 'getUrlParams').and.returnValue( + {} + ); + const urlPathSpy = spyOn(urlService, 'getPathname').and.returnValue( + '/story/fhfhvhgvhvvh' + ); + const storyIdSpy = spyOn( + urlService, + 'getStoryIdFromViewerUrl' + ).and.returnValue('1'); + const addFieldSpy = spyOn(urlService, 'addField').and.callThrough(); + + const result = component.getExplorationLink(); - expect(result).toBe( - '/explore/1?collection_id=1&parent=1&parent=2&story_id=1&node_id=1'); - expect(urlParamsSpy).toHaveBeenCalled(); - expect(urlPathSpy).toHaveBeenCalled(); - expect(storyIdSpy).toHaveBeenCalled(); - expect(addFieldSpy).toHaveBeenCalled(); - })); + tick(); + fixture.detectChanges(); - it('should return the url for the exploration' + - ' given nodeId and storyId', fakeAsync(() => { - const urlParamsSpy = spyOn(urlService, 'getUrlParams').and.returnValue({ - story_id: '1', - node_id: '1', - }); - const urlPathSpy = spyOn(urlService, 'getPathname').and.returnValue( - '/story/fhfhvhgvhvvh'); - const addFieldSpy = spyOn(urlService, 'addField').and.callThrough(); + expect(result).toBe( + '/explore/1?collection_id=1&parent=1&parent=2&story_id=1&node_id=1' + ); + expect(urlParamsSpy).toHaveBeenCalled(); + expect(urlPathSpy).toHaveBeenCalled(); + expect(storyIdSpy).toHaveBeenCalled(); + expect(addFieldSpy).toHaveBeenCalled(); + }) + ); - component.storyNodeId = ''; - const result = component.getExplorationLink(); + it( + 'should return the url for the exploration' + ' given nodeId and storyId', + fakeAsync(() => { + const urlParamsSpy = spyOn(urlService, 'getUrlParams').and.returnValue({ + story_id: '1', + node_id: '1', + }); + const urlPathSpy = spyOn(urlService, 'getPathname').and.returnValue( + '/story/fhfhvhgvhvvh' + ); + const addFieldSpy = spyOn(urlService, 'addField').and.callThrough(); + + component.storyNodeId = ''; + const result = component.getExplorationLink(); - tick(); - fixture.detectChanges(); + tick(); + fixture.detectChanges(); - expect(result).toBe('/explore/1?collection_id=1&parent=1&parent=2'); - expect(urlParamsSpy).toHaveBeenCalled(); - expect(urlPathSpy).toHaveBeenCalled(); - expect(addFieldSpy).toHaveBeenCalled(); - })); + expect(result).toBe('/explore/1?collection_id=1&parent=1&parent=2'); + expect(urlParamsSpy).toHaveBeenCalled(); + expect(urlPathSpy).toHaveBeenCalled(); + expect(addFieldSpy).toHaveBeenCalled(); + }) + ); }); diff --git a/core/templates/components/summary-tile/exploration-summary-tile.component.ts b/core/templates/components/summary-tile/exploration-summary-tile.component.ts index dc52cb8bce91..dc6bf9bbd701 100644 --- a/core/templates/components/summary-tile/exploration-summary-tile.component.ts +++ b/core/templates/components/summary-tile/exploration-summary-tile.component.ts @@ -16,28 +16,31 @@ * @fileoverview Component for an exploration summary tile. */ -import { Component, Input, OnDestroy, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component, Input, OnDestroy, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; -import { AppConstants } from 'app.constants'; -import { RatingComputationService } from 'components/ratings/rating-computation/rating-computation.service'; -import { ExplorationRatings } from 'domain/summary/learner-exploration-summary.model'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { UrlService } from 'services/contextual/url.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { DateTimeFormatService } from 'services/date-time-format.service'; -import { UserService } from 'services/user.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { Subscription } from 'rxjs'; -import { HumanReadableContributorsSummary } from 'domain/summary/creator-exploration-summary.model'; -import { I18nLanguageCodeService, TranslationKeyType } from 'services/i18n-language-code.service'; +import {AppConstants} from 'app.constants'; +import {RatingComputationService} from 'components/ratings/rating-computation/rating-computation.service'; +import {ExplorationRatings} from 'domain/summary/learner-exploration-summary.model'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {UrlService} from 'services/contextual/url.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {DateTimeFormatService} from 'services/date-time-format.service'; +import {UserService} from 'services/user.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {Subscription} from 'rxjs'; +import {HumanReadableContributorsSummary} from 'domain/summary/creator-exploration-summary.model'; +import { + I18nLanguageCodeService, + TranslationKeyType, +} from 'services/i18n-language-code.service'; import './exploration-summary-tile.component.css'; @Component({ selector: 'oppia-exploration-summary-tile', templateUrl: './exploration-summary-tile.component.html', - styleUrls: ['./exploration-summary-tile.component.css'] + styleUrls: ['./exploration-summary-tile.component.css'], }) export class ExplorationSummaryTileComponent implements OnInit, OnDestroy { // These properties are initialized using Angular lifecycle hooks @@ -108,50 +111,51 @@ export class ExplorationSummaryTileComponent implements OnInit, OnDestroy { this.userIsLoggedIn = userInfo.isLoggedIn(); }); this.activityType = AppConstants.ACTIVITY_TYPE_EXPLORATION; - let contributorsSummary: HumanReadableContributorsSummary = ( - this.contributorsSummary || {}); - this.contributors = Object.keys( - contributorsSummary).sort( + let contributorsSummary: HumanReadableContributorsSummary = + this.contributorsSummary || {}; + this.contributors = Object.keys(contributorsSummary).sort( (contributorUsername1, contributorUsername2) => { - let commitsOfContributor1 = contributorsSummary[ - contributorUsername1].num_commits; - let commitsOfContributor2 = contributorsSummary[ - contributorUsername2].num_commits; + let commitsOfContributor1 = + contributorsSummary[contributorUsername1].num_commits; + let commitsOfContributor2 = + contributorsSummary[contributorUsername2].num_commits; return commitsOfContributor2 - commitsOfContributor1; } ); this.isRefresherExploration = false; if (this.parentExplorationIds) { - this.isRefresherExploration = ( - this.parentExplorationIds.length > 0); + this.isRefresherExploration = this.parentExplorationIds.length > 0; } if (!this.mobileCutoffPx) { this.mobileCutoffPx = 0; } - this.isWindowLarge = ( - this.windowDimensionsService.getWidth() >= this.mobileCutoffPx); + this.isWindowLarge = + this.windowDimensionsService.getWidth() >= this.mobileCutoffPx; this.checkIfMobileCardToBeShown(); - this.resizeSubscription = this.windowDimensionsService.getResizeEvent(). - subscribe(evt => { - this.isWindowLarge = ( - this.windowDimensionsService.getWidth() >= this.mobileCutoffPx); + this.resizeSubscription = this.windowDimensionsService + .getResizeEvent() + .subscribe(evt => { + this.isWindowLarge = + this.windowDimensionsService.getWidth() >= this.mobileCutoffPx; this.checkIfMobileCardToBeShown(); }); this.lastUpdatedDateTime = this.getLastUpdatedDatetime(); this.relativeLastUpdatedDateTime = this.getRelativeLastUpdatedDateTime(); this.avgRating = this.getAverageRating(); this.thumbnailIcon = this.getCompleteThumbnailIconUrl(); - this.expTitleTranslationKey = ( + this.expTitleTranslationKey = this.i18nLanguageCodeService.getExplorationTranslationKey( - this.explorationId, TranslationKeyType.TITLE) - ); - this.expObjectiveTranslationKey = ( + this.explorationId, + TranslationKeyType.TITLE + ); + this.expObjectiveTranslationKey = this.i18nLanguageCodeService.getExplorationTranslationKey( - this.explorationId, TranslationKeyType.DESCRIPTION) - ); + this.explorationId, + TranslationKeyType.DESCRIPTION + ); } ngOnDestroy(): void { @@ -162,10 +166,10 @@ export class ExplorationSummaryTileComponent implements OnInit, OnDestroy { checkIfMobileCardToBeShown(): void { let currentPageUrl = this.urlService.getPathname(); - this.mobileCardToBeShown = ( - !this.isWindowLarge && (( - currentPageUrl === '/community-library') || - currentPageUrl.includes('/explore'))); + this.mobileCardToBeShown = + !this.isWindowLarge && + (currentPageUrl === '/community-library' || + currentPageUrl.includes('/explore')); } setHoverState(hoverState: boolean): void { @@ -179,8 +183,7 @@ export class ExplorationSummaryTileComponent implements OnInit, OnDestroy { // Function will return null when Exploration Ratings are not present. getAverageRating(): number | null { if (this.ratings) { - return this.ratingComputationService.computeAverageRating( - this.ratings); + return this.ratingComputationService.computeAverageRating(this.ratings); } return null; } @@ -190,7 +193,8 @@ export class ExplorationSummaryTileComponent implements OnInit, OnDestroy { getLastUpdatedDatetime(): string | null { if (this.lastUpdatedMsec) { return this.dateTimeFormatService.getLocaleAbbreviatedDatetimeString( - this.lastUpdatedMsec); + this.lastUpdatedMsec + ); } return null; } @@ -198,7 +202,8 @@ export class ExplorationSummaryTileComponent implements OnInit, OnDestroy { getRelativeLastUpdatedDateTime(): string | null { if (this.lastUpdatedMsec) { return this.dateTimeFormatService.getRelativeTimeFromNow( - this.lastUpdatedMsec); + this.lastUpdatedMsec + ); } return null; } @@ -216,35 +221,41 @@ export class ExplorationSummaryTileComponent implements OnInit, OnDestroy { let storyNodeIdToAdd = null; // Replace the collection ID with the one in the URL if it exists // in urlParams. - if (parentExplorationIds && - urlParams.hasOwnProperty('collection_id')) { + if (parentExplorationIds && urlParams.hasOwnProperty('collection_id')) { collectionIdToAdd = urlParams.collection_id; } else if ( this.urlService.getPathname().match(/\/story\/(\w|-){12}/g) && - this.storyNodeId) { + this.storyNodeId + ) { storyIdToAdd = this.urlService.getStoryIdFromViewerUrl(); storyNodeIdToAdd = this.storyNodeId; } else if ( urlParams.hasOwnProperty('story_id') && - urlParams.hasOwnProperty('node_id')) { + urlParams.hasOwnProperty('node_id') + ) { storyIdToAdd = urlParams.story_id; storyNodeIdToAdd = this.storyNodeId; } if (collectionIdToAdd) { result = this.urlService.addField( - result, 'collection_id', collectionIdToAdd); + result, + 'collection_id', + collectionIdToAdd + ); } if (parentExplorationIds) { for (let i = 0; i < parentExplorationIds.length - 1; i++) { result = this.urlService.addField( - result, 'parent', parentExplorationIds[i]); + result, + 'parent', + parentExplorationIds[i] + ); } } if (storyIdToAdd && storyNodeIdToAdd) { result = this.urlService.addField(result, 'story_id', storyIdToAdd); - result = this.urlService.addField( - result, 'node_id', storyNodeIdToAdd); + result = this.urlService.addField(result, 'node_id', storyNodeIdToAdd); } return result; } @@ -252,7 +263,8 @@ export class ExplorationSummaryTileComponent implements OnInit, OnDestroy { getCompleteThumbnailIconUrl(): string { return this.urlInterpolationService.getStaticImageUrl( - this.thumbnailIconUrl); + this.thumbnailIconUrl + ); } isHackyExpTitleTranslationDisplayed(): boolean { @@ -272,6 +284,9 @@ export class ExplorationSummaryTileComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive( - 'oppiaExplorationSummaryTile', downgradeComponent( - {component: ExplorationSummaryTileComponent})); +angular + .module('oppia') + .directive( + 'oppiaExplorationSummaryTile', + downgradeComponent({component: ExplorationSummaryTileComponent}) + ); diff --git a/core/templates/components/summary-tile/learner-story-summary-tile.component.spec.ts b/core/templates/components/summary-tile/learner-story-summary-tile.component.spec.ts index 067384e31e49..eb1b60742d24 100644 --- a/core/templates/components/summary-tile/learner-story-summary-tile.component.spec.ts +++ b/core/templates/components/summary-tile/learner-story-summary-tile.component.spec.ts @@ -16,18 +16,16 @@ * @fileoverview Unit tests for LearnerStorySummaryTileComponent. */ -import { async, ComponentFixture, TestBed } from - '@angular/core/testing'; -import { MaterialModule } from 'modules/material.module'; -import { FormsModule } from '@angular/forms'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; - -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { StorySummary} from 'domain/story/story-summary.model'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { LearnerStorySummaryTileComponent } from './learner-story-summary-tile.component'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {MaterialModule} from 'modules/material.module'; +import {FormsModule} from '@angular/forms'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {StorySummary} from 'domain/story/story-summary.model'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {LearnerStorySummaryTileComponent} from './learner-story-summary-tile.component'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; describe('Learner Story Summary Tile Component', () => { let component: LearnerStorySummaryTileComponent; @@ -40,15 +38,11 @@ describe('Learner Story Summary Tile Component', () => { BrowserAnimationsModule, MaterialModule, FormsModule, - HttpClientTestingModule - ], - declarations: [ - LearnerStorySummaryTileComponent, + HttpClientTestingModule, ], - providers: [ - UrlInterpolationService, - ], - schemas: [NO_ERRORS_SCHEMA] + declarations: [LearnerStorySummaryTileComponent], + providers: [UrlInterpolationService], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -69,17 +63,19 @@ describe('Learner Story Summary Tile Component', () => { all_node_dicts: [], topic_name: 'Topic', classroom_url_fragment: 'math', - topic_url_fragment: 'topic' + topic_url_fragment: 'topic', }; component.storySummary = StorySummary.createFromBackendDict( - sampleStorySummaryBackendDict); + sampleStorySummaryBackendDict + ); fixture.detectChanges(); }); it('should get the story link url for story page', () => { const urlSpy = spyOn( - urlInterpolationService, 'interpolateUrl') - .and.returnValue('/learn/math/topic/story/story-title'); + urlInterpolationService, + 'interpolateUrl' + ).and.returnValue('/learn/math/topic/story/story-title'); component.getStoryLink(); fixture.detectChanges(); @@ -104,7 +100,7 @@ describe('Learner Story Summary Tile Component', () => { planned_publication_date_msecs: 100, last_modified_msecs: 100, first_publication_date_msecs: 200, - unpublishing_reason: null + unpublishing_reason: null, }; const sampleStorySummaryBackendDict = { id: '0', @@ -119,10 +115,11 @@ describe('Learner Story Summary Tile Component', () => { all_node_dicts: [nodeDict], topic_name: 'Topic', classroom_url_fragment: 'math', - topic_url_fragment: 'topic' + topic_url_fragment: 'topic', }; component.storySummary = StorySummary.createFromBackendDict( - sampleStorySummaryBackendDict); + sampleStorySummaryBackendDict + ); component.ngOnInit(); expect(component.nextIncompleteNodeTitle).toEqual('Chapter 1: Chapter 1'); }); @@ -131,7 +128,8 @@ describe('Learner Story Summary Tile Component', () => { component.cardIsHovered = true; component.displayArea = 'homeTab'; expect(component.isCardHovered()).toBe( - '-webkit-filter: blur(2px); filter: blur(2px);'); + '-webkit-filter: blur(2px); filter: blur(2px);' + ); }); it('should get story link url for exploration page on homeTab', () => { @@ -152,7 +150,7 @@ describe('Learner Story Summary Tile Component', () => { planned_publication_date_msecs: 100, last_modified_msecs: 100, first_publication_date_msecs: 200, - unpublishing_reason: null + unpublishing_reason: null, }; const sampleStorySummaryBackendDict = { id: '0', @@ -167,15 +165,17 @@ describe('Learner Story Summary Tile Component', () => { all_node_dicts: [nodeDict], topic_name: 'Topic', classroom_url_fragment: 'math', - topic_url_fragment: 'topic' + topic_url_fragment: 'topic', }; component.storySummary = StorySummary.createFromBackendDict( - sampleStorySummaryBackendDict); + sampleStorySummaryBackendDict + ); component.completedNodeCount = 0; expect(component.getStoryLink()).toBe( '/explore/test?topic_url_fragment=topic&' + - 'classroom_url_fragment=math&story_url_fragment=story&' + - 'node_id=node_1'); + 'classroom_url_fragment=math&story_url_fragment=story&' + + 'node_id=node_1' + ); }); it('should get # as story link url for story page', () => { @@ -192,12 +192,12 @@ describe('Learner Story Summary Tile Component', () => { all_node_dicts: [], topic_name: 'Topic', classroom_url_fragment: undefined, - topic_url_fragment: 'topic' + topic_url_fragment: 'topic', }; component.storySummary = StorySummary.createFromBackendDict( - sampleStorySummaryBackendDict); - const urlSpy = spyOn( - urlInterpolationService, 'interpolateUrl'); + sampleStorySummaryBackendDict + ); + const urlSpy = spyOn(urlInterpolationService, 'interpolateUrl'); component.getStoryLink(); fixture.detectChanges(); @@ -207,8 +207,9 @@ describe('Learner Story Summary Tile Component', () => { it('should get static image url', () => { const urlSpy = spyOn( - urlInterpolationService, 'getStaticImageUrl') - .and.returnValue('/assets/images/learner_dashboard/star.svg'); + urlInterpolationService, + 'getStaticImageUrl' + ).and.returnValue('/assets/images/learner_dashboard/star.svg'); component.getStaticImageUrl('/learner_dashboard/star.svg'); fixture.detectChanges(); diff --git a/core/templates/components/summary-tile/learner-story-summary-tile.component.ts b/core/templates/components/summary-tile/learner-story-summary-tile.component.ts index c0192034da63..3fc1a9e183d6 100644 --- a/core/templates/components/summary-tile/learner-story-summary-tile.component.ts +++ b/core/templates/components/summary-tile/learner-story-summary-tile.component.ts @@ -16,19 +16,19 @@ * @fileoverview Component for a canonical story tile. */ -import { Component, OnInit } from '@angular/core'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { TopicViewerDomainConstants } from 'domain/topic_viewer/topic-viewer-domain.constants'; -import { Input } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; -import { AppConstants } from 'app.constants'; -import { StorySummary } from 'domain/story/story-summary.model'; -import { UrlService } from 'services/contextual/url.service'; +import {Component, OnInit} from '@angular/core'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {TopicViewerDomainConstants} from 'domain/topic_viewer/topic-viewer-domain.constants'; +import {Input} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; +import {AppConstants} from 'app.constants'; +import {StorySummary} from 'domain/story/story-summary.model'; +import {UrlService} from 'services/contextual/url.service'; @Component({ selector: 'oppia-learner-story-summary-tile', - templateUrl: './learner-story-summary-tile.component.html' + templateUrl: './learner-story-summary-tile.component.html', }) export class LearnerStorySummaryTileComponent implements OnInit { // These properties are initialized using Angular lifecycle hooks @@ -53,7 +53,7 @@ export class LearnerStorySummaryTileComponent implements OnInit { constructor( private urlInterpolationService: UrlInterpolationService, private assetsBackendApiService: AssetsBackendApiService, - private urlService: UrlService, + private urlService: UrlService ) {} getStoryLink(): string { @@ -69,53 +69,66 @@ export class LearnerStorySummaryTileComponent implements OnInit { let explorationId = node.getExplorationId(); if (explorationId) { let result = this.urlInterpolationService.interpolateUrl( - '/explore/', { - exp_id: explorationId - }); + '/explore/', + { + exp_id: explorationId, + } + ); result = this.urlService.addField( - result, 'topic_url_fragment', topicUrlFragment); + result, + 'topic_url_fragment', + topicUrlFragment + ); result = this.urlService.addField( - result, 'classroom_url_fragment', classroomUrlFragment); + result, + 'classroom_url_fragment', + classroomUrlFragment + ); result = this.urlService.addField( - result, 'story_url_fragment', - this.storySummary.getUrlFragment()); - result = this.urlService.addField( - result, 'node_id', node.getId()); + result, + 'story_url_fragment', + this.storySummary.getUrlFragment() + ); + result = this.urlService.addField(result, 'node_id', node.getId()); return result; } } } return this.urlInterpolationService.interpolateUrl( - TopicViewerDomainConstants.STORY_VIEWER_URL_TEMPLATE, { + TopicViewerDomainConstants.STORY_VIEWER_URL_TEMPLATE, + { classroom_url_fragment: classroomUrlFragment, story_url_fragment: this.storySummary.getUrlFragment(), - topic_url_fragment: topicUrlFragment - }); + topic_url_fragment: topicUrlFragment, + } + ); } ngOnInit(): void { this.nodeCount = this.storySummary.getNodeTitles().length; this.completedNodeCount = this.storySummary.getCompletedNodeTitles().length; this.storyProgress = Math.floor( - (this.completedNodeCount / this.nodeCount) * 100); + (this.completedNodeCount / this.nodeCount) * 100 + ); if (this.storyProgress === 100) { this.storyCompleted = true; } if (this.storySummary.getThumbnailFilename()) { - this.thumbnailUrl = ( + this.thumbnailUrl = this.assetsBackendApiService.getThumbnailUrlForPreview( - AppConstants.ENTITY_TYPE.STORY, this.storySummary.getId(), - this.storySummary.getThumbnailFilename())); + AppConstants.ENTITY_TYPE.STORY, + this.storySummary.getId(), + this.storySummary.getThumbnailFilename() + ); } this.storyLink = this.getStoryLink(); this.storyTitle = this.storySummary.getTitle(); this.thumbnailBgColor = this.storySummary.getThumbnailBgColor(); if (this.nodeCount !== this.completedNodeCount) { - let nextIncompleteNode = this.storySummary.getNodeTitles()[ - this.completedNodeCount]; - this.nextIncompleteNodeTitle = ( - `Chapter ${this.completedNodeCount + 1}: ${nextIncompleteNode}`); + let nextIncompleteNode = + this.storySummary.getNodeTitles()[this.completedNodeCount]; + this.nextIncompleteNodeTitle = `Chapter ${this.completedNodeCount + 1}: ${nextIncompleteNode}`; } this.starImageUrl = this.getStaticImageUrl('/learner_dashboard/star.svg'); } @@ -139,6 +152,9 @@ export class LearnerStorySummaryTileComponent implements OnInit { } } -angular.module('oppia').directive( - 'oppiaLearnerStorySummaryTile', downgradeComponent( - {component: LearnerStorySummaryTileComponent})); +angular + .module('oppia') + .directive( + 'oppiaLearnerStorySummaryTile', + downgradeComponent({component: LearnerStorySummaryTileComponent}) + ); diff --git a/core/templates/components/summary-tile/learner-topic-goals-summary-tile.component.spec.ts b/core/templates/components/summary-tile/learner-topic-goals-summary-tile.component.spec.ts index 063317a8fd31..d3d164eb8c33 100644 --- a/core/templates/components/summary-tile/learner-topic-goals-summary-tile.component.spec.ts +++ b/core/templates/components/summary-tile/learner-topic-goals-summary-tile.component.spec.ts @@ -16,21 +16,17 @@ * @fileoverview Unit tests for LearnerTopicGoalsSummaryTileComponent. */ -import { async, ComponentFixture, TestBed } from - '@angular/core/testing'; -import { MaterialModule } from 'modules/material.module'; -import { FormsModule } from '@angular/forms'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; - -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { LearnerTopicGoalsSummaryTileComponent } from - './learner-topic-goals-summary-tile.component'; -import { LearnerTopicSummary } from 'domain/topic/learner-topic-summary.model'; -import { StoryNode } from 'domain/story/story-node.model'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {MaterialModule} from 'modules/material.module'; +import {FormsModule} from '@angular/forms'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {LearnerTopicGoalsSummaryTileComponent} from './learner-topic-goals-summary-tile.component'; +import {LearnerTopicSummary} from 'domain/topic/learner-topic-summary.model'; +import {StoryNode} from 'domain/story/story-node.model'; describe('Learner Topic Goals Summary Tile Component', () => { let component: LearnerTopicGoalsSummaryTileComponent; @@ -42,15 +38,11 @@ describe('Learner Topic Goals Summary Tile Component', () => { BrowserAnimationsModule, MaterialModule, FormsModule, - HttpClientTestingModule - ], - declarations: [ - LearnerTopicGoalsSummaryTileComponent, - ], - providers: [ - UrlInterpolationService, + HttpClientTestingModule, ], - schemas: [NO_ERRORS_SCHEMA] + declarations: [LearnerTopicGoalsSummaryTileComponent], + providers: [UrlInterpolationService], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -64,7 +56,7 @@ describe('Learner Topic Goals Summary Tile Component', () => { title: 'subtopic_name', thumbnail_filename: 'image.svg', thumbnail_bg_color: '#F8BF74', - url_fragment: 'subtopic-name' + url_fragment: 'subtopic-name', }; let nodeDict1 = { id: 'node_1', @@ -82,7 +74,7 @@ describe('Learner Topic Goals Summary Tile Component', () => { planned_publication_date_msecs: 100, last_modified_msecs: 100, first_publication_date_msecs: 200, - unpublishing_reason: null + unpublishing_reason: null, }; let nodeDict2 = { id: 'node_2', @@ -100,7 +92,7 @@ describe('Learner Topic Goals Summary Tile Component', () => { planned_publication_date_msecs: 100, last_modified_msecs: 100, first_publication_date_msecs: 200, - unpublishing_reason: null + unpublishing_reason: null, }; const learnerTopicSummaryBackendDict1 = { id: 'sample_topic_id', @@ -114,45 +106,52 @@ describe('Learner Topic Goals Summary Tile Component', () => { thumbnail_bg_color: '#C6DCDA', classroom: 'math', practice_tab_is_displayed: false, - canonical_story_summary_dict: [{ - id: '0', - title: 'Story Title', - description: 'Story Description', - node_titles: ['Chapter 1', 'Chapter 2'], - thumbnail_filename: 'image.svg', - thumbnail_bg_color: '#F8BF74', - story_is_published: true, - completed_node_titles: ['Chapter 2'], - all_node_dicts: [nodeDict1, nodeDict2], - url_fragment: 'story-title', - topic_name: 'Topic Name', - classroom_url_fragment: 'math', - topic_url_fragment: 'topic-name' - }], + canonical_story_summary_dict: [ + { + id: '0', + title: 'Story Title', + description: 'Story Description', + node_titles: ['Chapter 1', 'Chapter 2'], + thumbnail_filename: 'image.svg', + thumbnail_bg_color: '#F8BF74', + story_is_published: true, + completed_node_titles: ['Chapter 2'], + all_node_dicts: [nodeDict1, nodeDict2], + url_fragment: 'story-title', + topic_name: 'Topic Name', + classroom_url_fragment: 'math', + topic_url_fragment: 'topic-name', + }, + ], url_fragment: 'topic-name', subtopics: [subtopic], degrees_of_mastery: { skill_id_1: 0.5, - skill_id_2: 0.3 + skill_id_2: 0.3, }, skill_descriptions: { skill_id_1: 'Skill Description 1', - skill_id_2: 'Skill Description 2' - } + skill_id_2: 'Skill Description 2', + }, }; component.topicSummary = LearnerTopicSummary.createFromBackendDict( - learnerTopicSummaryBackendDict1); + learnerTopicSummaryBackendDict1 + ); fixture.detectChanges(); }); - it('should get # as story node link url if all chapters' + - ' are read', () => { - component.storyNodeToDisplay = StoryNode.createFromIdAndTitle( - '1', 'Story node title'); - let storyNodeLink = component.getStoryNodeLink(); - fixture.detectChanges(); - expect(storyNodeLink).toBe('#'); - }); + it( + 'should get # as story node link url if all chapters' + ' are read', + () => { + component.storyNodeToDisplay = StoryNode.createFromIdAndTitle( + '1', + 'Story node title' + ); + let storyNodeLink = component.getStoryNodeLink(); + fixture.detectChanges(); + expect(storyNodeLink).toBe('#'); + } + ); it('should get the story and story node title on init', () => { component.ngOnInit(); @@ -163,15 +162,20 @@ describe('Learner Topic Goals Summary Tile Component', () => { it('should make the tile blurred if it is hovered', () => { component.cardIsHovered = true; expect(component.isCardHovered()).toBe( - '-webkit-filter: blur(2px); filter: blur(2px);'); + '-webkit-filter: blur(2px); filter: blur(2px);' + ); }); - it('should get story node link url for exploration page if unread' + - ' chapters are present in topic', () => { - let storyNodeLink = '/explore/exp_1?topic_url_fragment=topic-name&' + - 'classroom_url_fragment=math&story_url_fragment=story-title&' + - 'node_id=node_1'; - component.ngOnInit(); - expect(component.getStoryNodeLink()).toBe(storyNodeLink); - }); + it( + 'should get story node link url for exploration page if unread' + + ' chapters are present in topic', + () => { + let storyNodeLink = + '/explore/exp_1?topic_url_fragment=topic-name&' + + 'classroom_url_fragment=math&story_url_fragment=story-title&' + + 'node_id=node_1'; + component.ngOnInit(); + expect(component.getStoryNodeLink()).toBe(storyNodeLink); + } + ); }); diff --git a/core/templates/components/summary-tile/learner-topic-goals-summary-tile.component.ts b/core/templates/components/summary-tile/learner-topic-goals-summary-tile.component.ts index 731caba71dea..948d1e9d3162 100644 --- a/core/templates/components/summary-tile/learner-topic-goals-summary-tile.component.ts +++ b/core/templates/components/summary-tile/learner-topic-goals-summary-tile.component.ts @@ -16,20 +16,20 @@ * @fileoverview Component for a learner topic goals summary tile. */ -import { Component, OnInit } from '@angular/core'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { Input } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; -import { AppConstants } from 'app.constants'; -import { LearnerTopicSummary } from 'domain/topic/learner-topic-summary.model'; -import { UrlService } from 'services/contextual/url.service'; -import { StoryNode } from 'domain/story/story-node.model'; -import { StorySummary } from 'domain/story/story-summary.model'; +import {Component, OnInit} from '@angular/core'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {Input} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; +import {AppConstants} from 'app.constants'; +import {LearnerTopicSummary} from 'domain/topic/learner-topic-summary.model'; +import {UrlService} from 'services/contextual/url.service'; +import {StoryNode} from 'domain/story/story-node.model'; +import {StorySummary} from 'domain/story/story-summary.model'; @Component({ selector: 'oppia-learner-topic-goals-summary-tile', - templateUrl: './learner-topic-goals-summary-tile.component.html' + templateUrl: './learner-topic-goals-summary-tile.component.html', }) export class LearnerTopicGoalsSummaryTileComponent implements OnInit { // These properties are initialized using Angular lifecycle hooks @@ -56,22 +56,20 @@ export class LearnerTopicGoalsSummaryTileComponent implements OnInit { constructor( private urlInterpolationService: UrlInterpolationService, private assetsBackendApiService: AssetsBackendApiService, - private urlService: UrlService, + private urlService: UrlService ) {} getAllIncompleteStoryNodes(): StoryNode[] { - let allStorySummaries: StorySummary[] = ( - this.topicSummary.getCanonicalStorySummaryDicts() - ); + let allStorySummaries: StorySummary[] = + this.topicSummary.getCanonicalStorySummaryDicts(); let allIncompleteStoryNodes: StoryNode[] = []; allStorySummaries.map(storySummary => { if (allIncompleteStoryNodes.length === 0) { let allNodes = storySummary.getAllNodes(); - let completedStoryNodes: string[] = ( - storySummary.getCompletedNodeTitles() - ); + let completedStoryNodes: string[] = + storySummary.getCompletedNodeTitles(); allIncompleteStoryNodes = allNodes.filter(node => { - return (!completedStoryNodes.includes(node.getTitle())); + return !completedStoryNodes.includes(node.getTitle()); }); this.storySummaryToDisplay = storySummary; } @@ -80,25 +78,41 @@ export class LearnerTopicGoalsSummaryTileComponent implements OnInit { } getStoryNodeLink(): string { - const classroomUrlFragment = ( - this.storySummaryToDisplay.getClassroomUrlFragment()); + const classroomUrlFragment = + this.storySummaryToDisplay.getClassroomUrlFragment(); const topicUrlFragment = this.storySummaryToDisplay.getTopicUrlFragment(); const explorationId = this.storyNodeToDisplay.getExplorationId(); - if (classroomUrlFragment === undefined || topicUrlFragment === undefined || - explorationId === null) { + if ( + classroomUrlFragment === undefined || + topicUrlFragment === undefined || + explorationId === null + ) { return '#'; } let result = this.urlInterpolationService.interpolateUrl( - '/explore/', { exp_id: explorationId }); + '/explore/', + {exp_id: explorationId} + ); result = this.urlService.addField( - result, 'topic_url_fragment', topicUrlFragment); + result, + 'topic_url_fragment', + topicUrlFragment + ); result = this.urlService.addField( - result, 'classroom_url_fragment', classroomUrlFragment); + result, + 'classroom_url_fragment', + classroomUrlFragment + ); result = this.urlService.addField( - result, 'story_url_fragment', - this.storySummaryToDisplay.getUrlFragment()); + result, + 'story_url_fragment', + this.storySummaryToDisplay.getUrlFragment() + ); result = this.urlService.addField( - result, 'node_id', this.storyNodeToDisplay.getId()); + result, + 'node_id', + this.storyNodeToDisplay.getId() + ); return result; } @@ -109,10 +123,12 @@ export class LearnerTopicGoalsSummaryTileComponent implements OnInit { let thumbnailFilename = this.storyNodeToDisplay.getThumbnailFilename(); if (thumbnailFilename) { - this.thumbnailUrl = ( + this.thumbnailUrl = this.assetsBackendApiService.getThumbnailUrlForPreview( - AppConstants.ENTITY_TYPE.STORY, this.storySummaryToDisplay.getId(), - thumbnailFilename)); + AppConstants.ENTITY_TYPE.STORY, + this.storySummaryToDisplay.getId(), + thumbnailFilename + ); } this.storyNodeTitle = this.storyNodeToDisplay.getTitle(); @@ -123,14 +139,13 @@ export class LearnerTopicGoalsSummaryTileComponent implements OnInit { this.storyName = this.storySummaryToDisplay.getTitle(); this.storyNodeLink = this.getStoryNodeLink(); - let totalStoryNodesCount = ( - this.storySummaryToDisplay.getAllNodes().length - ); - let completedNodesCount = ( - this.storySummaryToDisplay.getCompletedNodeTitles().length - ); + let totalStoryNodesCount = + this.storySummaryToDisplay.getAllNodes().length; + let completedNodesCount = + this.storySummaryToDisplay.getCompletedNodeTitles().length; this.storyProgress = Math.floor( - (completedNodesCount / totalStoryNodesCount) * 100); + (completedNodesCount / totalStoryNodesCount) * 100 + ); } } @@ -142,6 +157,9 @@ export class LearnerTopicGoalsSummaryTileComponent implements OnInit { } } -angular.module('oppia').directive( - 'oppiaLearnerTopicSummaryTile', downgradeComponent( - {component: LearnerTopicGoalsSummaryTileComponent})); +angular + .module('oppia') + .directive( + 'oppiaLearnerTopicSummaryTile', + downgradeComponent({component: LearnerTopicGoalsSummaryTileComponent}) + ); diff --git a/core/templates/components/summary-tile/learner-topic-summary-tile.component.spec.ts b/core/templates/components/summary-tile/learner-topic-summary-tile.component.spec.ts index bce71bc3c456..3b2c6de6d2df 100644 --- a/core/templates/components/summary-tile/learner-topic-summary-tile.component.spec.ts +++ b/core/templates/components/summary-tile/learner-topic-summary-tile.component.spec.ts @@ -16,16 +16,14 @@ * @fileoverview Unit tests for LearnerStorySummaryTileComponent. */ -import { async, ComponentFixture, TestBed } from - '@angular/core/testing'; -import { MaterialModule } from 'modules/material.module'; -import { FormsModule } from '@angular/forms'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { LearnerTopicSummary } from 'domain/topic/learner-topic-summary.model'; -import { LearnerTopicSummaryTileComponent } from './learner-topic-summary-tile.component'; - +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {MaterialModule} from 'modules/material.module'; +import {FormsModule} from '@angular/forms'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {LearnerTopicSummary} from 'domain/topic/learner-topic-summary.model'; +import {LearnerTopicSummaryTileComponent} from './learner-topic-summary-tile.component'; describe('Learner Topic Summary Tile Component', () => { let component: LearnerTopicSummaryTileComponent; @@ -34,18 +32,10 @@ describe('Learner Topic Summary Tile Component', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [ - MaterialModule, - FormsModule, - HttpClientTestingModule - ], - declarations: [ - LearnerTopicSummaryTileComponent, - ], - providers: [ - UrlInterpolationService, - ], - schemas: [NO_ERRORS_SCHEMA] + imports: [MaterialModule, FormsModule, HttpClientTestingModule], + declarations: [LearnerTopicSummaryTileComponent], + providers: [UrlInterpolationService], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -59,7 +49,7 @@ describe('Learner Topic Summary Tile Component', () => { title: 'subtopic_name', thumbnail_filename: 'image.svg', thumbnail_bg_color: '#F8BF74', - url_fragment: 'subtopic-name' + url_fragment: 'subtopic-name', }; let nodeDict = { @@ -78,7 +68,7 @@ describe('Learner Topic Summary Tile Component', () => { planned_publication_date_msecs: 100, last_modified_msecs: 100, first_publication_date_msecs: 200, - unpublishing_reason: null + unpublishing_reason: null, }; const learnerTopicSummaryBackendDict = { id: 'sample_topic_id', @@ -92,38 +82,42 @@ describe('Learner Topic Summary Tile Component', () => { thumbnail_bg_color: '#C6DCDA', classroom: 'math', practice_tab_is_displayed: false, - canonical_story_summary_dict: [{ - id: '0', - title: 'Story Title', - description: 'Story Description', - node_titles: ['Chapter 1'], - thumbnail_filename: 'image.svg', - thumbnail_bg_color: '#F8BF74', - story_is_published: true, - completed_node_titles: ['Chapter 1'], - url_fragment: 'story-title', - all_node_dicts: [nodeDict] - }], + canonical_story_summary_dict: [ + { + id: '0', + title: 'Story Title', + description: 'Story Description', + node_titles: ['Chapter 1'], + thumbnail_filename: 'image.svg', + thumbnail_bg_color: '#F8BF74', + story_is_published: true, + completed_node_titles: ['Chapter 1'], + url_fragment: 'story-title', + all_node_dicts: [nodeDict], + }, + ], url_fragment: 'topic-name', subtopics: [subtopic], degrees_of_mastery: { skill_id_1: 0.5, - skill_id_2: 0.3 + skill_id_2: 0.3, }, skill_descriptions: { skill_id_1: 'Skill Description 1', - skill_id_2: 'Skill Description 2' - } + skill_id_2: 'Skill Description 2', + }, }; component.topicSummary = LearnerTopicSummary.createFromBackendDict( - learnerTopicSummaryBackendDict); + learnerTopicSummaryBackendDict + ); fixture.detectChanges(); }); it('should get the topic link url for topic page', () => { const urlSpy = spyOn( - urlInterpolationService, 'interpolateUrl') - .and.returnValue('/learn/math/topic'); + urlInterpolationService, + 'interpolateUrl' + ).and.returnValue('/learn/math/topic'); component.getTopicLink(); fixture.detectChanges(); @@ -138,7 +132,7 @@ describe('Learner Topic Summary Tile Component', () => { title: 'subtopic_name', thumbnail_filename: 'image.svg', thumbnail_bg_color: '#F8BF74', - url_fragment: 'subtopic-name' + url_fragment: 'subtopic-name', }; let nodeDict = { @@ -157,7 +151,7 @@ describe('Learner Topic Summary Tile Component', () => { planned_publication_date_msecs: 100, last_modified_msecs: 100, first_publication_date_msecs: 200, - unpublishing_reason: null + unpublishing_reason: null, }; const learnerTopicSummaryBackendDict = { id: 'sample_topic_id', @@ -171,33 +165,35 @@ describe('Learner Topic Summary Tile Component', () => { thumbnail_bg_color: '#C6DCDA', classroom: '', practice_tab_is_displayed: false, - canonical_story_summary_dict: [{ - id: '0', - title: 'Story Title', - description: 'Story Description', - node_titles: ['Chapter 1'], - thumbnail_filename: 'image.svg', - thumbnail_bg_color: '#F8BF74', - story_is_published: true, - completed_node_titles: ['Chapter 1'], - url_fragment: 'story-title', - all_node_dicts: [nodeDict] - }], + canonical_story_summary_dict: [ + { + id: '0', + title: 'Story Title', + description: 'Story Description', + node_titles: ['Chapter 1'], + thumbnail_filename: 'image.svg', + thumbnail_bg_color: '#F8BF74', + story_is_published: true, + completed_node_titles: ['Chapter 1'], + url_fragment: 'story-title', + all_node_dicts: [nodeDict], + }, + ], url_fragment: '', subtopics: [subtopic], degrees_of_mastery: { skill_id_1: 0.5, - skill_id_2: 0.3 + skill_id_2: 0.3, }, skill_descriptions: { skill_id_1: 'Skill Description 1', - skill_id_2: 'Skill Description 2' - } + skill_id_2: 'Skill Description 2', + }, }; component.topicSummary = LearnerTopicSummary.createFromBackendDict( - learnerTopicSummaryBackendDict); - const urlSpy = spyOn( - urlInterpolationService, 'interpolateUrl'); + learnerTopicSummaryBackendDict + ); + const urlSpy = spyOn(urlInterpolationService, 'interpolateUrl'); component.getTopicLink(); fixture.detectChanges(); diff --git a/core/templates/components/summary-tile/learner-topic-summary-tile.component.ts b/core/templates/components/summary-tile/learner-topic-summary-tile.component.ts index 9b2752bb631d..b740aeb80e10 100644 --- a/core/templates/components/summary-tile/learner-topic-summary-tile.component.ts +++ b/core/templates/components/summary-tile/learner-topic-summary-tile.component.ts @@ -16,18 +16,18 @@ * @fileoverview Component for a learner topic summary tile. */ -import { Component, OnInit } from '@angular/core'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { Input } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; -import { AppConstants } from 'app.constants'; -import { LearnerTopicSummary } from 'domain/topic/learner-topic-summary.model'; -import { ClassroomDomainConstants } from 'domain/classroom/classroom-domain.constants'; +import {Component, OnInit} from '@angular/core'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {Input} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; +import {AppConstants} from 'app.constants'; +import {LearnerTopicSummary} from 'domain/topic/learner-topic-summary.model'; +import {ClassroomDomainConstants} from 'domain/classroom/classroom-domain.constants'; @Component({ selector: 'oppia-learner-topic-summary-tile', - templateUrl: './learner-topic-summary-tile.component.html' + templateUrl: './learner-topic-summary-tile.component.html', }) export class LearnerTopicSummaryTileComponent implements OnInit { // These properties are initialized using Angular lifecycle hooks @@ -51,18 +51,22 @@ export class LearnerTopicSummaryTileComponent implements OnInit { return '#'; } return this.urlInterpolationService.interpolateUrl( - ClassroomDomainConstants.TOPIC_VIEWER_URL_TEMPLATE, { + ClassroomDomainConstants.TOPIC_VIEWER_URL_TEMPLATE, + { topic_url_fragment: this.topicSummary.urlFragment, - classroom_url_fragment: this.topicSummary.classroom - }); + classroom_url_fragment: this.topicSummary.classroom, + } + ); } ngOnInit(): void { if (this.topicSummary.getThumbnailFilename()) { - this.thumbnailUrl = ( + this.thumbnailUrl = this.assetsBackendApiService.getThumbnailUrlForPreview( - AppConstants.ENTITY_TYPE.TOPIC, this.topicSummary.getId(), - this.topicSummary.getThumbnailFilename())); + AppConstants.ENTITY_TYPE.TOPIC, + this.topicSummary.getId(), + this.topicSummary.getThumbnailFilename() + ); } this.topicLink = this.getTopicLink(); this.topicTitle = this.topicSummary.name; @@ -71,6 +75,9 @@ export class LearnerTopicSummaryTileComponent implements OnInit { } } -angular.module('oppia').directive( - 'oppiaLearnerTopicSummaryTile', downgradeComponent( - {component: LearnerTopicSummaryTileComponent})); +angular + .module('oppia') + .directive( + 'oppiaLearnerTopicSummaryTile', + downgradeComponent({component: LearnerTopicSummaryTileComponent}) + ); diff --git a/core/templates/components/summary-tile/story-summary-tile.component.spec.ts b/core/templates/components/summary-tile/story-summary-tile.component.spec.ts index f2b04d79f437..6e93c0b1f30d 100644 --- a/core/templates/components/summary-tile/story-summary-tile.component.spec.ts +++ b/core/templates/components/summary-tile/story-summary-tile.component.spec.ts @@ -16,20 +16,20 @@ * @fileoverview Unit tests for StorySummaryTileComponent. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { StorySummary } from 'domain/story/story-summary.model'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { StorySummaryTileComponent } from './story-summary-tile.component'; -import { PlatformFeatureService } from '../../services/platform-feature.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {StorySummary} from 'domain/story/story-summary.model'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {StorySummaryTileComponent} from './story-summary-tile.component'; +import {PlatformFeatureService} from '../../services/platform-feature.service'; class MockPlatformFeatureService { status = { SerialChapterLaunchCurriculumAdminView: { - isEnabled: false - } + isEnabled: false, + }, }; } @@ -43,16 +43,13 @@ describe('StorySummaryTileComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - StorySummaryTileComponent, - MockTranslatePipe - ], + declarations: [StorySummaryTileComponent, MockTranslatePipe], providers: [ { provide: PlatformFeatureService, - useValue: mockPlatformFeatureService - } - ] + useValue: mockPlatformFeatureService, + }, + ], }).compileComponents(); })); @@ -63,14 +60,15 @@ describe('StorySummaryTileComponent', () => { i18nLanguageCodeService = TestBed.inject(I18nLanguageCodeService); spyOn(i18nLanguageCodeService, 'isCurrentLanguageRTL').and.returnValue( - true); + true + ); }); it('should get status of Serial Chapter Launch Feature flag', () => { expect(component.isSerialChapterFeatureFlagEnabled()).toEqual(false); - mockPlatformFeatureService. - status.SerialChapterLaunchCurriculumAdminView.isEnabled = true; + mockPlatformFeatureService.status.SerialChapterLaunchCurriculumAdminView.isEnabled = + true; expect(component.isSerialChapterFeatureFlagEnabled()).toEqual(true); }); @@ -104,8 +102,9 @@ describe('StorySummaryTileComponent', () => { planned_publication_date_msecs: null, last_modified_msecs: null, first_publication_date_msecs: null, - unpublishing_reason: null - }, { + unpublishing_reason: null, + }, + { id: 'node_2', title: 'node2', description: 'This is node 2', @@ -121,8 +120,9 @@ describe('StorySummaryTileComponent', () => { planned_publication_date_msecs: null, last_modified_msecs: null, first_publication_date_msecs: null, - unpublishing_reason: null - }, { + unpublishing_reason: null, + }, + { id: 'node_3', title: 'node3', description: 'This is node 3', @@ -138,19 +138,26 @@ describe('StorySummaryTileComponent', () => { planned_publication_date_msecs: null, last_modified_msecs: null, first_publication_date_msecs: null, - unpublishing_reason: null - } - ] + unpublishing_reason: null, + }, + ], }); - spyOn(i18nLanguageCodeService, 'getStoryTranslationKey') - .and.returnValue('I18N_STORY_storyId_TITLE'); - spyOn(i18nLanguageCodeService, 'getExplorationTranslationKey') - .and.returnValue('I18N_EXPLORATION_explId_TITLE'); - spyOn(i18nLanguageCodeService, 'isHackyTranslationAvailable') - .and.returnValues(false, true); - spyOn(i18nLanguageCodeService, 'isCurrentLanguageEnglish') - .and.returnValues(false, false); + spyOn(i18nLanguageCodeService, 'getStoryTranslationKey').and.returnValue( + 'I18N_STORY_storyId_TITLE' + ); + spyOn( + i18nLanguageCodeService, + 'getExplorationTranslationKey' + ).and.returnValue('I18N_EXPLORATION_explId_TITLE'); + spyOn( + i18nLanguageCodeService, + 'isHackyTranslationAvailable' + ).and.returnValues(false, true); + spyOn(i18nLanguageCodeService, 'isCurrentLanguageEnglish').and.returnValues( + false, + false + ); component.ngOnInit(); @@ -160,13 +167,13 @@ describe('StorySummaryTileComponent', () => { expect(component.storyLink).toBe('#'); expect(component.storyTitle).toBe('Story Title'); expect(component.storyTitleTranslationKey).toEqual( - 'I18N_STORY_storyId_TITLE'); - expect(component.nodeTitlesTranslationKeys).toEqual( - [ - 'I18N_EXPLORATION_explId_TITLE', - 'I18N_EXPLORATION_explId_TITLE', - 'I18N_EXPLORATION_explId_TITLE' - ]); + 'I18N_STORY_storyId_TITLE' + ); + expect(component.nodeTitlesTranslationKeys).toEqual([ + 'I18N_EXPLORATION_explId_TITLE', + 'I18N_EXPLORATION_explId_TITLE', + 'I18N_EXPLORATION_explId_TITLE', + ]); // Translation is only displayed if the language is not English // and it's hacky translation is available. let hackyStoryTitleTranslationIsDisplayed = @@ -185,7 +192,8 @@ describe('StorySummaryTileComponent', () => { // and the last node's value is the difference of (2 * 20 * Math.PI) and the // first value. expect(component.completedStrokeDashArrayValues).toBe( - '57.83185307179586 67.83185307179586'); + '57.83185307179586 67.83185307179586' + ); expect(component.thumbnailBgColor).toBe('#FF9933'); expect(component.nodeTitles).toEqual(['node1', 'node2', 'node3']); expect(component.availableNodeCount).toBe(2); @@ -204,7 +212,7 @@ describe('StorySummaryTileComponent', () => { story_is_published: true, completed_node_titles: ['node1'], url_fragment: 'story1', - all_node_dicts: [] + all_node_dicts: [], }); expect(component.thumbnailUrl).toBe(null); @@ -226,85 +234,94 @@ describe('StorySummaryTileComponent', () => { story_is_published: true, completed_node_titles: ['node1'], url_fragment: 'story1', - all_node_dicts: [] + all_node_dicts: [], }); expect(component.thumbnailUrl).toBe(null); component.ngOnInit(); - expect(component.thumbnailUrl) - .toBe('/assetsdevhandler/story/storyId/assets/thumbnail/thumbnail.jpg'); - }); - - it('should display only 1 chapters if window width is less' + - ' than equal to 500px', () => { - component.storySummary = StorySummary.createFromBackendDict({ - id: 'storyId', - title: 'Story Title', - node_titles: ['node1', 'node2', 'node3'], - thumbnail_filename: 'thumbnail.jpg', - thumbnail_bg_color: '#FF9933', - description: 'This is the story description', - story_is_published: true, - completed_node_titles: ['node1'], - url_fragment: 'story1', - all_node_dicts: [] - }); - spyOn(wds, 'getWidth').and.returnValue(460); - - expect(component.chaptersDisplayed).toBe(undefined); - - component.ngOnInit(); - - expect(component.chaptersDisplayed).toBe(1); - }); - - it('should display only 2 chapters if window width is greater' + - ' than 500px and less than 768px', () => { - component.storySummary = StorySummary.createFromBackendDict({ - id: 'storyId', - title: 'Story Title', - node_titles: ['node1', 'node2', 'node3'], - thumbnail_filename: 'thumbnail.jpg', - thumbnail_bg_color: '#FF9933', - description: 'This is the story description', - story_is_published: true, - completed_node_titles: ['node1'], - url_fragment: 'story1', - all_node_dicts: [] - }); - spyOn(wds, 'getWidth').and.returnValue(650); - - expect(component.chaptersDisplayed).toBe(undefined); - - component.ngOnInit(); - - expect(component.chaptersDisplayed).toBe(2); + expect(component.thumbnailUrl).toBe( + '/assetsdevhandler/story/storyId/assets/thumbnail/thumbnail.jpg' + ); }); - it('should display 3 chapters if window width is greater' + - ' than 768px', () => { - component.storySummary = StorySummary.createFromBackendDict({ - id: 'storyId', - title: 'Story Title', - node_titles: ['node1', 'node2', 'node3'], - thumbnail_filename: 'thumbnail.jpg', - thumbnail_bg_color: '#FF9933', - description: 'This is the story description', - story_is_published: true, - completed_node_titles: ['node1'], - url_fragment: 'story1', - all_node_dicts: [] - }); - spyOn(wds, 'getWidth').and.returnValue(800); - - expect(component.chaptersDisplayed).toBe(undefined); - - component.ngOnInit(); - - expect(component.chaptersDisplayed).toBe(3); - }); + it( + 'should display only 1 chapters if window width is less' + + ' than equal to 500px', + () => { + component.storySummary = StorySummary.createFromBackendDict({ + id: 'storyId', + title: 'Story Title', + node_titles: ['node1', 'node2', 'node3'], + thumbnail_filename: 'thumbnail.jpg', + thumbnail_bg_color: '#FF9933', + description: 'This is the story description', + story_is_published: true, + completed_node_titles: ['node1'], + url_fragment: 'story1', + all_node_dicts: [], + }); + spyOn(wds, 'getWidth').and.returnValue(460); + + expect(component.chaptersDisplayed).toBe(undefined); + + component.ngOnInit(); + + expect(component.chaptersDisplayed).toBe(1); + } + ); + + it( + 'should display only 2 chapters if window width is greater' + + ' than 500px and less than 768px', + () => { + component.storySummary = StorySummary.createFromBackendDict({ + id: 'storyId', + title: 'Story Title', + node_titles: ['node1', 'node2', 'node3'], + thumbnail_filename: 'thumbnail.jpg', + thumbnail_bg_color: '#FF9933', + description: 'This is the story description', + story_is_published: true, + completed_node_titles: ['node1'], + url_fragment: 'story1', + all_node_dicts: [], + }); + spyOn(wds, 'getWidth').and.returnValue(650); + + expect(component.chaptersDisplayed).toBe(undefined); + + component.ngOnInit(); + + expect(component.chaptersDisplayed).toBe(2); + } + ); + + it( + 'should display 3 chapters if window width is greater' + ' than 768px', + () => { + component.storySummary = StorySummary.createFromBackendDict({ + id: 'storyId', + title: 'Story Title', + node_titles: ['node1', 'node2', 'node3'], + thumbnail_filename: 'thumbnail.jpg', + thumbnail_bg_color: '#FF9933', + description: 'This is the story description', + story_is_published: true, + completed_node_titles: ['node1'], + url_fragment: 'story1', + all_node_dicts: [], + }); + spyOn(wds, 'getWidth').and.returnValue(800); + + expect(component.chaptersDisplayed).toBe(undefined); + + component.ngOnInit(); + + expect(component.chaptersDisplayed).toBe(3); + } + ); it('should return correct story status', () => { component.storyProgress = 0; @@ -327,248 +344,260 @@ describe('StorySummaryTileComponent', () => { expect(component.checkTabletView()).toBe(false); }); - it('should show \'View All\' button if number of nodes is not same as the' + - ' number of chapters displayed', () => { - // StorySummary with 3 nodes. - component.storySummary = StorySummary.createFromBackendDict({ - id: 'storyId', - title: 'Story Title', - node_titles: ['node1', 'node2', 'node3'], - thumbnail_filename: 'thumbnail.jpg', - thumbnail_bg_color: '#FF9933', - description: 'This is the story description', - story_is_published: true, - completed_node_titles: ['node1'], - url_fragment: 'story1', - all_node_dicts: [] - }); - - // We return width equal to 801 (greater than 800), so that 3 chapters are - // displayed instead of 2. - spyOn(wds, 'getWidth').and.returnValue(801); - - expect(component.showButton).toBe(false); - - component.ngOnInit(); - - expect(component.showButton).toBe(false); - - // StorySummary with 5 nodes. - component.storySummary = StorySummary.createFromBackendDict({ - id: 'storyId', - title: 'Story Title', - node_titles: ['node1', 'node2', 'node3', 'node4', 'node5'], - thumbnail_filename: 'thumbnail.jpg', - thumbnail_bg_color: '#FF9933', - description: 'This is the story description', - story_is_published: true, - completed_node_titles: ['node1'], - url_fragment: 'story1', - all_node_dicts: [] - }); - - component.ngOnInit(); - - // Will show 'View All' button as number of nodes (5) is more than chapters - // displayed (3). - expect(component.showButton).toBe(true); - }); - - it('should show the number of completed chapters through' + - ' progress circle', () => { - // StorySummary with 1 node with 0 completed stories. - component.storySummary = StorySummary.createFromBackendDict({ - id: 'storyId', - title: 'Story Title', - node_titles: ['node1'], - thumbnail_filename: 'thumbnail.jpg', - thumbnail_bg_color: '#FF9933', - description: 'This is the story description', - story_is_published: true, - completed_node_titles: [], - url_fragment: 'story1', - all_node_dicts: [ - { - id: 'node_1', - title: 'node1', - description: 'This is node 1', - destination_node_ids: [], - prerequisite_skill_ids: [], - acquired_skill_ids: [], - outline: '', - outline_is_finalized: true, - exploration_id: null, - thumbnail_bg_color: null, - thumbnail_filename: null, - status: 'Published', - planned_publication_date_msecs: null, - last_modified_msecs: null, - first_publication_date_msecs: null, - unpublishing_reason: null - } - ] - }); - let circumference = (2 * 20 * Math.PI).toString(); - - component.ngOnInit(); - - expect(component.strokeDashArrayValues).toBe(''); - expect(component.completedStrokeDashArrayValues).toBe('0 ' + circumference); - - // StorySummary with 1 node with 1 completed stories. - component.storySummary = StorySummary.createFromBackendDict({ - id: 'storyId', - title: 'Story Title', - node_titles: ['node1'], - thumbnail_filename: 'thumbnail.jpg', - thumbnail_bg_color: '#FF9933', - description: 'This is the story description', - story_is_published: true, - completed_node_titles: ['node1'], - url_fragment: 'story1', - all_node_dicts: [ - { - id: 'node_1', - title: 'node1', - description: 'This is node 1', - destination_node_ids: [], - prerequisite_skill_ids: [], - acquired_skill_ids: [], - outline: '', - outline_is_finalized: true, - exploration_id: null, - thumbnail_bg_color: null, - thumbnail_filename: null, - status: 'Published', - planned_publication_date_msecs: null, - last_modified_msecs: null, - first_publication_date_msecs: null, - unpublishing_reason: null - } - ] - }); - - component.ngOnInit(); - - expect(component.strokeDashArrayValues).toBe(''); - expect(component.completedStrokeDashArrayValues).toBe(''); - - // StorySummary with 5 node with 3 completed stories. - component.storySummary = StorySummary.createFromBackendDict({ - id: 'storyId', - title: 'Story Title', - node_titles: ['node1', 'node2', 'node3', 'node4', 'node5'], - thumbnail_filename: 'thumbnail.jpg', - thumbnail_bg_color: '#FF9933', - description: 'This is the story description', - story_is_published: true, - completed_node_titles: ['node1', 'node2', 'node3'], - url_fragment: 'story1', - all_node_dicts: [ - { - id: 'node_1', - title: 'node1', - description: 'This is node 1', - destination_node_ids: [], - prerequisite_skill_ids: [], - acquired_skill_ids: [], - outline: '', - outline_is_finalized: true, - exploration_id: null, - thumbnail_bg_color: null, - thumbnail_filename: null, - status: 'Published', - planned_publication_date_msecs: null, - last_modified_msecs: null, - first_publication_date_msecs: null, - unpublishing_reason: null - }, { - id: 'node_2', - title: 'node2', - description: 'This is node 2', - destination_node_ids: [], - prerequisite_skill_ids: [], - acquired_skill_ids: [], - outline: '', - outline_is_finalized: true, - exploration_id: null, - thumbnail_bg_color: null, - thumbnail_filename: null, - status: 'Published', - planned_publication_date_msecs: null, - last_modified_msecs: null, - first_publication_date_msecs: null, - unpublishing_reason: null - }, { - id: 'node_3', - title: 'node3', - description: 'This is node 3', - destination_node_ids: [], - prerequisite_skill_ids: [], - acquired_skill_ids: [], - outline: '', - outline_is_finalized: true, - exploration_id: null, - thumbnail_bg_color: null, - thumbnail_filename: null, - status: 'Published', - planned_publication_date_msecs: null, - last_modified_msecs: null, - first_publication_date_msecs: null, - unpublishing_reason: null - }, { - id: 'node_4', - title: 'node4', - description: 'This is node 4', - destination_node_ids: [], - prerequisite_skill_ids: [], - acquired_skill_ids: [], - outline: '', - outline_is_finalized: true, - exploration_id: null, - thumbnail_bg_color: null, - thumbnail_filename: null, - status: 'Published', - planned_publication_date_msecs: null, - last_modified_msecs: null, - first_publication_date_msecs: null, - unpublishing_reason: null - }, { - id: 'node_5', - title: 'node5', - description: 'This is node 5', - destination_node_ids: [], - prerequisite_skill_ids: [], - acquired_skill_ids: [], - outline: '', - outline_is_finalized: true, - exploration_id: null, - thumbnail_bg_color: null, - thumbnail_filename: null, - status: 'Published', - planned_publication_date_msecs: null, - last_modified_msecs: null, - first_publication_date_msecs: null, - unpublishing_reason: null - } - ] - }); - - component.ngOnInit(); - - // Here the value is calculated by the formula -> (circumference - - // (nodeCount * gapLength))/nodeCount = (2 * 20 * Math.PI - (5*5)) / 5 - // = 20.132741228718345. Along with this value, gapLength (5) is also - // concatenated to the string. - expect(component.strokeDashArrayValues).toBe('20.132741228718345 5'); - - // Here the value for completed nodes is calculated with the same formula. - // The last is calculated by subtracting the segment length (the value - // calculated using the formula above) from the circumference . - expect(component.completedStrokeDashArrayValues).toBe( - '20.132741228718345 5 20.132741228718345 5' + - ' 20.132741228718345 55.26548245743669'); - }); + it( + "should show 'View All' button if number of nodes is not same as the" + + ' number of chapters displayed', + () => { + // StorySummary with 3 nodes. + component.storySummary = StorySummary.createFromBackendDict({ + id: 'storyId', + title: 'Story Title', + node_titles: ['node1', 'node2', 'node3'], + thumbnail_filename: 'thumbnail.jpg', + thumbnail_bg_color: '#FF9933', + description: 'This is the story description', + story_is_published: true, + completed_node_titles: ['node1'], + url_fragment: 'story1', + all_node_dicts: [], + }); + + // We return width equal to 801 (greater than 800), so that 3 chapters are + // displayed instead of 2. + spyOn(wds, 'getWidth').and.returnValue(801); + + expect(component.showButton).toBe(false); + + component.ngOnInit(); + + expect(component.showButton).toBe(false); + + // StorySummary with 5 nodes. + component.storySummary = StorySummary.createFromBackendDict({ + id: 'storyId', + title: 'Story Title', + node_titles: ['node1', 'node2', 'node3', 'node4', 'node5'], + thumbnail_filename: 'thumbnail.jpg', + thumbnail_bg_color: '#FF9933', + description: 'This is the story description', + story_is_published: true, + completed_node_titles: ['node1'], + url_fragment: 'story1', + all_node_dicts: [], + }); + + component.ngOnInit(); + + // Will show 'View All' button as number of nodes (5) is more than chapters + // displayed (3). + expect(component.showButton).toBe(true); + } + ); + + it( + 'should show the number of completed chapters through' + ' progress circle', + () => { + // StorySummary with 1 node with 0 completed stories. + component.storySummary = StorySummary.createFromBackendDict({ + id: 'storyId', + title: 'Story Title', + node_titles: ['node1'], + thumbnail_filename: 'thumbnail.jpg', + thumbnail_bg_color: '#FF9933', + description: 'This is the story description', + story_is_published: true, + completed_node_titles: [], + url_fragment: 'story1', + all_node_dicts: [ + { + id: 'node_1', + title: 'node1', + description: 'This is node 1', + destination_node_ids: [], + prerequisite_skill_ids: [], + acquired_skill_ids: [], + outline: '', + outline_is_finalized: true, + exploration_id: null, + thumbnail_bg_color: null, + thumbnail_filename: null, + status: 'Published', + planned_publication_date_msecs: null, + last_modified_msecs: null, + first_publication_date_msecs: null, + unpublishing_reason: null, + }, + ], + }); + let circumference = (2 * 20 * Math.PI).toString(); + + component.ngOnInit(); + + expect(component.strokeDashArrayValues).toBe(''); + expect(component.completedStrokeDashArrayValues).toBe( + '0 ' + circumference + ); + + // StorySummary with 1 node with 1 completed stories. + component.storySummary = StorySummary.createFromBackendDict({ + id: 'storyId', + title: 'Story Title', + node_titles: ['node1'], + thumbnail_filename: 'thumbnail.jpg', + thumbnail_bg_color: '#FF9933', + description: 'This is the story description', + story_is_published: true, + completed_node_titles: ['node1'], + url_fragment: 'story1', + all_node_dicts: [ + { + id: 'node_1', + title: 'node1', + description: 'This is node 1', + destination_node_ids: [], + prerequisite_skill_ids: [], + acquired_skill_ids: [], + outline: '', + outline_is_finalized: true, + exploration_id: null, + thumbnail_bg_color: null, + thumbnail_filename: null, + status: 'Published', + planned_publication_date_msecs: null, + last_modified_msecs: null, + first_publication_date_msecs: null, + unpublishing_reason: null, + }, + ], + }); + + component.ngOnInit(); + + expect(component.strokeDashArrayValues).toBe(''); + expect(component.completedStrokeDashArrayValues).toBe(''); + + // StorySummary with 5 node with 3 completed stories. + component.storySummary = StorySummary.createFromBackendDict({ + id: 'storyId', + title: 'Story Title', + node_titles: ['node1', 'node2', 'node3', 'node4', 'node5'], + thumbnail_filename: 'thumbnail.jpg', + thumbnail_bg_color: '#FF9933', + description: 'This is the story description', + story_is_published: true, + completed_node_titles: ['node1', 'node2', 'node3'], + url_fragment: 'story1', + all_node_dicts: [ + { + id: 'node_1', + title: 'node1', + description: 'This is node 1', + destination_node_ids: [], + prerequisite_skill_ids: [], + acquired_skill_ids: [], + outline: '', + outline_is_finalized: true, + exploration_id: null, + thumbnail_bg_color: null, + thumbnail_filename: null, + status: 'Published', + planned_publication_date_msecs: null, + last_modified_msecs: null, + first_publication_date_msecs: null, + unpublishing_reason: null, + }, + { + id: 'node_2', + title: 'node2', + description: 'This is node 2', + destination_node_ids: [], + prerequisite_skill_ids: [], + acquired_skill_ids: [], + outline: '', + outline_is_finalized: true, + exploration_id: null, + thumbnail_bg_color: null, + thumbnail_filename: null, + status: 'Published', + planned_publication_date_msecs: null, + last_modified_msecs: null, + first_publication_date_msecs: null, + unpublishing_reason: null, + }, + { + id: 'node_3', + title: 'node3', + description: 'This is node 3', + destination_node_ids: [], + prerequisite_skill_ids: [], + acquired_skill_ids: [], + outline: '', + outline_is_finalized: true, + exploration_id: null, + thumbnail_bg_color: null, + thumbnail_filename: null, + status: 'Published', + planned_publication_date_msecs: null, + last_modified_msecs: null, + first_publication_date_msecs: null, + unpublishing_reason: null, + }, + { + id: 'node_4', + title: 'node4', + description: 'This is node 4', + destination_node_ids: [], + prerequisite_skill_ids: [], + acquired_skill_ids: [], + outline: '', + outline_is_finalized: true, + exploration_id: null, + thumbnail_bg_color: null, + thumbnail_filename: null, + status: 'Published', + planned_publication_date_msecs: null, + last_modified_msecs: null, + first_publication_date_msecs: null, + unpublishing_reason: null, + }, + { + id: 'node_5', + title: 'node5', + description: 'This is node 5', + destination_node_ids: [], + prerequisite_skill_ids: [], + acquired_skill_ids: [], + outline: '', + outline_is_finalized: true, + exploration_id: null, + thumbnail_bg_color: null, + thumbnail_filename: null, + status: 'Published', + planned_publication_date_msecs: null, + last_modified_msecs: null, + first_publication_date_msecs: null, + unpublishing_reason: null, + }, + ], + }); + + component.ngOnInit(); + + // Here the value is calculated by the formula -> (circumference - + // (nodeCount * gapLength))/nodeCount = (2 * 20 * Math.PI - (5*5)) / 5 + // = 20.132741228718345. Along with this value, gapLength (5) is also + // concatenated to the string. + expect(component.strokeDashArrayValues).toBe('20.132741228718345 5'); + + // Here the value for completed nodes is calculated with the same formula. + // The last is calculated by subtracting the segment length (the value + // calculated using the formula above) from the circumference . + expect(component.completedStrokeDashArrayValues).toBe( + '20.132741228718345 5 20.132741228718345 5' + + ' 20.132741228718345 55.26548245743669' + ); + } + ); it('should return the chapter URL when the chapter title is provided', () => { component.storySummary = StorySummary.createFromBackendDict({ @@ -598,9 +627,9 @@ describe('StorySummaryTileComponent', () => { planned_publication_date_msecs: null, last_modified_msecs: null, first_publication_date_msecs: null, - unpublishing_reason: null - } - ] + unpublishing_reason: null, + }, + ], }); component.ngOnInit(); @@ -608,7 +637,8 @@ describe('StorySummaryTileComponent', () => { expect(component.getChapterUrl('Node which is not present')).toBe(''); expect(component.getChapterUrl('Node 1')).toBe( '/explore/null?story_url_fragment=story1&topic_url_fragment=' + - 'undefined&classroom_url_fragment=undefined&node_id=node1'); + 'undefined&classroom_url_fragment=undefined&node_id=node1' + ); }); it('should populate the story URL when URL fragments are set', () => { @@ -624,7 +654,7 @@ describe('StorySummaryTileComponent', () => { story_is_published: true, completed_node_titles: ['node1'], url_fragment: 'story1', - all_node_dicts: [] + all_node_dicts: [], }); expect(component.getStoryLink()).toBe('/learn/math/fractions/story/story1'); @@ -641,7 +671,7 @@ describe('StorySummaryTileComponent', () => { story_is_published: true, completed_node_titles: ['node1'], url_fragment: 'story1', - all_node_dicts: [] + all_node_dicts: [], }); component.ngOnInit(); @@ -661,7 +691,7 @@ describe('StorySummaryTileComponent', () => { story_is_published: true, completed_node_titles: ['node1'], url_fragment: 'story1', - all_node_dicts: [] + all_node_dicts: [], }); component.ngOnInit(); @@ -672,7 +702,7 @@ describe('StorySummaryTileComponent', () => { expect(component.isPreviousChapterCompleted(2)).toBe(false); }); - it('should show all chapters when user click on \'View All\' button', () => { + it("should show all chapters when user click on 'View All' button", () => { spyOn(wds, 'getWidth').and.returnValue(460); component.storySummary = StorySummary.createFromBackendDict({ id: 'storyId', @@ -684,7 +714,7 @@ describe('StorySummaryTileComponent', () => { story_is_published: true, completed_node_titles: ['node1'], url_fragment: 'story1', - all_node_dicts: [] + all_node_dicts: [], }); expect(component.initialCount).toBe(undefined); @@ -701,34 +731,36 @@ describe('StorySummaryTileComponent', () => { expect(component.chaptersDisplayed).toBe(3); }); - it('should hide extra chapters when user click on \'View less\'' + - ' button', () => { - spyOn(wds, 'getWidth').and.returnValue(460); - component.storySummary = StorySummary.createFromBackendDict({ - id: 'storyId', - title: 'Story Title', - node_titles: ['node1', 'node2', 'node3'], - thumbnail_filename: 'thumbnail.jpg', - thumbnail_bg_color: '#FF9933', - description: 'This is the story description', - story_is_published: true, - completed_node_titles: ['node1'], - url_fragment: 'story1', - all_node_dicts: [] - }); + it( + "should hide extra chapters when user click on 'View less'" + ' button', + () => { + spyOn(wds, 'getWidth').and.returnValue(460); + component.storySummary = StorySummary.createFromBackendDict({ + id: 'storyId', + title: 'Story Title', + node_titles: ['node1', 'node2', 'node3'], + thumbnail_filename: 'thumbnail.jpg', + thumbnail_bg_color: '#FF9933', + description: 'This is the story description', + story_is_published: true, + completed_node_titles: ['node1'], + url_fragment: 'story1', + all_node_dicts: [], + }); - expect(component.chaptersDisplayed).toBe(undefined); + expect(component.chaptersDisplayed).toBe(undefined); - component.ngOnInit(); + component.ngOnInit(); - expect(component.chaptersDisplayed).toBe(1); + expect(component.chaptersDisplayed).toBe(1); - component.showAllChapters(); + component.showAllChapters(); - expect(component.chaptersDisplayed).toBe(3); + expect(component.chaptersDisplayed).toBe(3); - component.hideExtraChapters(); + component.hideExtraChapters(); - expect(component.chaptersDisplayed).toBe(1); - }); + expect(component.chaptersDisplayed).toBe(1); + } + ); }); diff --git a/core/templates/components/summary-tile/story-summary-tile.component.ts b/core/templates/components/summary-tile/story-summary-tile.component.ts index d711a6b74c0f..baac50a37bb3 100644 --- a/core/templates/components/summary-tile/story-summary-tile.component.ts +++ b/core/templates/components/summary-tile/story-summary-tile.component.ts @@ -16,28 +16,30 @@ * @fileoverview Component for a canonical story tile. */ -import { Component, OnInit } from '@angular/core'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { TopicViewerDomainConstants } from 'domain/topic_viewer/topic-viewer-domain.constants'; -import { Input } from '@angular/core'; -import { UrlService } from 'services/contextual/url.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; -import { AppConstants } from 'app.constants'; -import { StorySummary } from 'domain/story/story-summary.model'; -import { I18nLanguageCodeService, TranslationKeyType } from 'services/i18n-language-code.service'; -import { StoryNode } from 'domain/story/story-node.model'; -import { PlatformFeatureService } from 'services/platform-feature.service'; +import {Component, OnInit} from '@angular/core'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {TopicViewerDomainConstants} from 'domain/topic_viewer/topic-viewer-domain.constants'; +import {Input} from '@angular/core'; +import {UrlService} from 'services/contextual/url.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; +import {AppConstants} from 'app.constants'; +import {StorySummary} from 'domain/story/story-summary.model'; +import { + I18nLanguageCodeService, + TranslationKeyType, +} from 'services/i18n-language-code.service'; +import {StoryNode} from 'domain/story/story-node.model'; +import {PlatformFeatureService} from 'services/platform-feature.service'; import './story-summary-tile.component.css'; import constants from 'assets/constants'; - @Component({ selector: 'oppia-story-summary-tile', templateUrl: './story-summary-tile.component.html', - styleUrls: ['./story-summary-tile.component.css'] + styleUrls: ['./story-summary-tile.component.css'], }) export class StorySummaryTileComponent implements OnInit { // These properties are initialized using Angular lifecycle hooks @@ -64,7 +66,7 @@ export class StorySummaryTileComponent implements OnInit { storyLink!: string; thumbnailUrl: string | null = null; showButton: boolean = false; - circumference = (20 * 2 * Math.PI); + circumference = 20 * 2 * Math.PI; gapLength = 5; EXPLORE_PAGE_PREFIX = '/explore/'; availableNodeCount = 0; @@ -76,7 +78,7 @@ export class StorySummaryTileComponent implements OnInit { private urlService: UrlService, private windowDimensionsService: WindowDimensionsService, private assetsBackendApiService: AssetsBackendApiService, - private platformFeatureService: PlatformFeatureService, + private platformFeatureService: PlatformFeatureService ) {} checkTabletView(): boolean { @@ -84,9 +86,8 @@ export class StorySummaryTileComponent implements OnInit { } isSerialChapterFeatureFlagEnabled(): boolean { - return ( - this.platformFeatureService. - status.SerialChapterLaunchCurriculumAdminView.isEnabled); + return this.platformFeatureService.status + .SerialChapterLaunchCurriculumAdminView.isEnabled; } getStoryLink(): string { @@ -96,11 +97,13 @@ export class StorySummaryTileComponent implements OnInit { return '#'; } let storyLink = this.urlInterpolationService.interpolateUrl( - TopicViewerDomainConstants.STORY_VIEWER_URL_TEMPLATE, { + TopicViewerDomainConstants.STORY_VIEWER_URL_TEMPLATE, + { classroom_url_fragment: this.classroomUrlFragment, story_url_fragment: this.storySummary.getUrlFragment(), - topic_url_fragment: this.topicUrlFragment - }); + topic_url_fragment: this.topicUrlFragment, + } + ); return storyLink; } @@ -122,8 +125,7 @@ export class StorySummaryTileComponent implements OnInit { if (index === 0) { return true; } - let previousNodeTitle = ( - this.storySummary.getNodeTitles()[index - 1]); + let previousNodeTitle = this.storySummary.getNodeTitles()[index - 1]; return this.storySummary.isNodeCompleted(previousNodeTitle); } @@ -140,10 +142,9 @@ export class StorySummaryTileComponent implements OnInit { if (this.nodeCount === 1) { return ''; } - let segmentLength = ( - ( - this.circumference - - (this.availableNodeCount * this.gapLength)) / this.availableNodeCount); + let segmentLength = + (this.circumference - this.availableNodeCount * this.gapLength) / + this.availableNodeCount; return segmentLength.toString() + ' ' + this.gapLength.toString(); } @@ -156,15 +157,22 @@ export class StorySummaryTileComponent implements OnInit { return ''; } let urlParams = this.urlService.addField( - '', 'story_url_fragment', this.storySummary.getUrlFragment()); - urlParams = this.urlService.addField( - urlParams, 'topic_url_fragment', this.topicUrlFragment); + '', + 'story_url_fragment', + this.storySummary.getUrlFragment() + ); urlParams = this.urlService.addField( - urlParams, 'classroom_url_fragment', this.classroomUrlFragment); + urlParams, + 'topic_url_fragment', + this.topicUrlFragment + ); urlParams = this.urlService.addField( - urlParams, 'node_id', node.getId()); - return ( - `${this.EXPLORE_PAGE_PREFIX}${node.getExplorationId()}${urlParams}`); + urlParams, + 'classroom_url_fragment', + this.classroomUrlFragment + ); + urlParams = this.urlService.addField(urlParams, 'node_id', node.getId()); + return `${this.EXPLORE_PAGE_PREFIX}${node.getExplorationId()}${urlParams}`; } getCompletedStrokeDashArrayValues(): string { @@ -176,18 +184,18 @@ export class StorySummaryTileComponent implements OnInit { if (this.completedStoriesCount === 1 && this.nodeCount === 1) { return ''; } - let segmentLength = ( - ( - this.circumference - - (this.availableNodeCount * this.gapLength)) / this.availableNodeCount); + let segmentLength = + (this.circumference - this.availableNodeCount * this.gapLength) / + this.availableNodeCount; for (let i = 1; i <= this.completedStoriesCount - 1; i++) { - completedStrokeValues += ( - segmentLength.toString() + ' ' + this.gapLength.toString() + ' '); - remainingCircumference -= (segmentLength + this.gapLength); + completedStrokeValues += + segmentLength.toString() + ' ' + this.gapLength.toString() + ' '; + remainingCircumference -= segmentLength + this.gapLength; } - completedStrokeValues += ( - segmentLength.toString() + ' ' + - (remainingCircumference - segmentLength).toString()); + completedStrokeValues += + segmentLength.toString() + + ' ' + + (remainingCircumference - segmentLength).toString(); return completedStrokeValues; } @@ -197,7 +205,9 @@ export class StorySummaryTileComponent implements OnInit { for (let idx in this.storySummary.getNodeTitles()) { if ( this.storySummary.isNodeCompleted( - this.storySummary.getNodeTitles()[idx])) { + this.storySummary.getNodeTitles()[idx] + ) + ) { this.completedStoriesCount++; } } @@ -212,38 +222,49 @@ export class StorySummaryTileComponent implements OnInit { } } this.storyProgress = Math.floor( - (this.completedStoriesCount / this.availableNodeCount) * 100); + (this.completedStoriesCount / this.availableNodeCount) * 100 + ); this.chaptersDisplayed = this.allChaptersAreShown ? this.nodeCount : 3; - if (this.windowDimensionsService.getWidth() <= 768 && + if ( + this.windowDimensionsService.getWidth() <= 768 && this.windowDimensionsService.getWidth() > 500 && - !this.allChaptersAreShown) { + !this.allChaptersAreShown + ) { this.chaptersDisplayed = 2; } - if (this.windowDimensionsService.getWidth() <= 500 && - !this.allChaptersAreShown) { + if ( + this.windowDimensionsService.getWidth() <= 500 && + !this.allChaptersAreShown + ) { this.chaptersDisplayed = 1; } this.showButton = false; - if (!this.allChaptersAreShown && - this.chaptersDisplayed !== this.nodeCount) { + if ( + !this.allChaptersAreShown && + this.chaptersDisplayed !== this.nodeCount + ) { this.showButton = true; } if (this.storySummary.getThumbnailFilename()) { - this.thumbnailUrl = ( + this.thumbnailUrl = this.assetsBackendApiService.getThumbnailUrlForPreview( - AppConstants.ENTITY_TYPE.STORY, this.storySummary.getId(), - this.storySummary.getThumbnailFilename())); + AppConstants.ENTITY_TYPE.STORY, + this.storySummary.getId(), + this.storySummary.getThumbnailFilename() + ); } else { this.thumbnailUrl = null; } this.getStrokeDashArrayValues(); this.storyLink = this.getStoryLink(); this.storyTitle = this.storySummary.getTitle(); - this.storyTitleTranslationKey = this.i18nLanguageCodeService - .getStoryTranslationKey( - this.storySummary.getId(), TranslationKeyType.TITLE); + this.storyTitleTranslationKey = + this.i18nLanguageCodeService.getStoryTranslationKey( + this.storySummary.getId(), + TranslationKeyType.TITLE + ); this.strokeDashArrayValues = this.getStrokeDashArrayValues(); this.completedStrokeDashArrayValues = this.getCompletedStrokeDashArrayValues(); @@ -252,9 +273,11 @@ export class StorySummaryTileComponent implements OnInit { this.nodes = this.storySummary.getAllNodes(); for (let idx in this.storySummary.getAllNodes()) { let storyNode: StoryNode = this.storySummary.getAllNodes()[idx]; - let storyNodeTranslationKey = this.i18nLanguageCodeService. - getExplorationTranslationKey( - storyNode.getExplorationId() as string, TranslationKeyType.TITLE); + let storyNodeTranslationKey = + this.i18nLanguageCodeService.getExplorationTranslationKey( + storyNode.getExplorationId() as string, + TranslationKeyType.TITLE + ); this.nodeTitlesTranslationKeys.push(storyNodeTranslationKey); } this.getStoryStatus(); @@ -277,6 +300,9 @@ export class StorySummaryTileComponent implements OnInit { } } -angular.module('oppia').directive( - 'oppiaStorySummaryTile', downgradeComponent( - {component: StorySummaryTileComponent})); +angular + .module('oppia') + .directive( + 'oppiaStorySummaryTile', + downgradeComponent({component: StorySummaryTileComponent}) + ); diff --git a/core/templates/components/summary-tile/subtopic-summary-tile.component.spec.ts b/core/templates/components/summary-tile/subtopic-summary-tile.component.spec.ts index c7dff0841d25..a38f5bfa3070 100644 --- a/core/templates/components/summary-tile/subtopic-summary-tile.component.spec.ts +++ b/core/templates/components/summary-tile/subtopic-summary-tile.component.spec.ts @@ -16,19 +16,19 @@ * @fileoverview Unit tests for SubtopicSummaryTileComponent. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { Subtopic } from 'domain/topic/subtopic.model'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { SubtopicSummaryTileComponent } from './subtopic-summary-tile.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {Subtopic} from 'domain/topic/subtopic.model'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {SubtopicSummaryTileComponent} from './subtopic-summary-tile.component'; class MockWindowRef { nativeWindow = { - open: (url: string) => {} + open: (url: string) => {}, }; } @@ -43,16 +43,13 @@ describe('SubtopicSummaryTileComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - SubtopicSummaryTileComponent, - MockTranslatePipe - ], + declarations: [SubtopicSummaryTileComponent, MockTranslatePipe], providers: [ { provide: WindowRef, - useClass: MockWindowRef - } - ] + useClass: MockWindowRef, + }, + ], }).compileComponents(); })); @@ -64,46 +61,56 @@ describe('SubtopicSummaryTileComponent', () => { urlInterpolationService = TestBed.inject(UrlInterpolationService); i18nLanguageCodeService = TestBed.inject(I18nLanguageCodeService); - component.subtopic = Subtopic.create({ - id: 1, - title: 'Title', - skill_ids: ['skill_1'], - thumbnail_filename: 'img.png', - thumbnail_bg_color: '#a11f40', - url_fragment: 'title' - }, { - skill_1: 'Description 1' - }); + component.subtopic = Subtopic.create( + { + id: 1, + title: 'Title', + skill_ids: ['skill_1'], + thumbnail_filename: 'img.png', + thumbnail_bg_color: '#a11f40', + url_fragment: 'title', + }, + { + skill_1: 'Description 1', + } + ); component.classroomUrlFragment = 'math'; component.topicUrlFragment = 'topic'; }); it('should set component properties on initialization', () => { spyOn(abas, 'getThumbnailUrlForPreview').and.returnValue('/thumbnail/url'); - spyOn(i18nLanguageCodeService, 'getSubtopicTranslationKey') - .and.returnValue('I18N_SUBTOPIC_123abcd_title_TITLE'); + spyOn(i18nLanguageCodeService, 'getSubtopicTranslationKey').and.returnValue( + 'I18N_SUBTOPIC_123abcd_title_TITLE' + ); component.ngOnInit(); expect(component.subtopicTitle).toBe('Title'); expect(component.thumbnailUrl).toBe('/thumbnail/url'); expect(component.subtopicTitleTranslationKey).toBe( - 'I18N_SUBTOPIC_123abcd_title_TITLE'); + 'I18N_SUBTOPIC_123abcd_title_TITLE' + ); }); it('should check if subtopic translation is displayed correctly', () => { spyOn(abas, 'getThumbnailUrlForPreview').and.returnValue('/thumbnail/url'); - spyOn(i18nLanguageCodeService, 'getSubtopicTranslationKey') - .and.returnValue('I18N_SUBTOPIC_123abcd_test_TITLE'); - spyOn(i18nLanguageCodeService, 'isCurrentLanguageEnglish') - .and.returnValue(false); - spyOn(i18nLanguageCodeService, 'isHackyTranslationAvailable') - .and.returnValue(true); + spyOn(i18nLanguageCodeService, 'getSubtopicTranslationKey').and.returnValue( + 'I18N_SUBTOPIC_123abcd_test_TITLE' + ); + spyOn(i18nLanguageCodeService, 'isCurrentLanguageEnglish').and.returnValue( + false + ); + spyOn( + i18nLanguageCodeService, + 'isHackyTranslationAvailable' + ).and.returnValue(true); component.ngOnInit(); expect(component.subtopicTitleTranslationKey).toBe( - 'I18N_SUBTOPIC_123abcd_test_TITLE'); + 'I18N_SUBTOPIC_123abcd_test_TITLE' + ); let hackySubtopicTitleTranslationIsDisplayed = component.isHackySubtopicTitleTranslationDisplayed(); expect(hackySubtopicTitleTranslationIsDisplayed).toBe(true); @@ -111,8 +118,9 @@ describe('SubtopicSummaryTileComponent', () => { it('should throw error if subtopic url is null', () => { spyOn(abas, 'getThumbnailUrlForPreview').and.returnValue('/thumbnail/url'); - spyOn(i18nLanguageCodeService, 'getSubtopicTranslationKey') - .and.returnValue('I18N_SUBTOPIC_123abcd_title_TITLE'); + spyOn(i18nLanguageCodeService, 'getSubtopicTranslationKey').and.returnValue( + 'I18N_SUBTOPIC_123abcd_title_TITLE' + ); component.subtopic = Subtopic.createFromTitle(1, 'Title'); expect(() => { @@ -120,11 +128,14 @@ describe('SubtopicSummaryTileComponent', () => { }).toThrowError('Expected subtopic to have a URL fragment'); }); - it('should not open subtopic page if classroom or topic url' + - ' does not exist', () => { - component.subtopic = Subtopic.createFromTitle(1, 'Title'); - expect(component.openSubtopicPage()).toBe(undefined); - }); + it( + 'should not open subtopic page if classroom or topic url' + + ' does not exist', + () => { + component.subtopic = Subtopic.createFromTitle(1, 'Title'); + expect(component.openSubtopicPage()).toBe(undefined); + } + ); it('should open subtopic page when user clicks on subtopic card', () => { spyOn(urlInterpolationService, 'interpolateUrl').and.returnValue('/url'); diff --git a/core/templates/components/summary-tile/subtopic-summary-tile.component.ts b/core/templates/components/summary-tile/subtopic-summary-tile.component.ts index 008336a35ac2..0aed41f19d66 100644 --- a/core/templates/components/summary-tile/subtopic-summary-tile.component.ts +++ b/core/templates/components/summary-tile/subtopic-summary-tile.component.ts @@ -16,19 +16,22 @@ * @fileoverview Component for a subtopic tile. */ -import { Component, Input, OnInit } from '@angular/core'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { TopicViewerDomainConstants } from 'domain/topic_viewer/topic-viewer-domain.constants'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; -import { AppConstants } from 'app.constants'; -import { Subtopic } from 'domain/topic/subtopic.model'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { I18nLanguageCodeService, TranslationKeyType } from 'services/i18n-language-code.service'; +import {Component, Input, OnInit} from '@angular/core'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {TopicViewerDomainConstants} from 'domain/topic_viewer/topic-viewer-domain.constants'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; +import {AppConstants} from 'app.constants'; +import {Subtopic} from 'domain/topic/subtopic.model'; +import {downgradeComponent} from '@angular/upgrade/static'; +import { + I18nLanguageCodeService, + TranslationKeyType, +} from 'services/i18n-language-code.service'; @Component({ selector: 'oppia-subtopic-summary-tile', - templateUrl: './subtopic-summary-tile.component.html' + templateUrl: './subtopic-summary-tile.component.html', }) export class SubtopicSummaryTileComponent implements OnInit { // These properties are initialized using Angular lifecycle hooks @@ -61,12 +64,14 @@ export class SubtopicSummaryTileComponent implements OnInit { } this.windowRef.nativeWindow.open( this.urlInterpolationService.interpolateUrl( - TopicViewerDomainConstants.SUBTOPIC_VIEWER_URL_TEMPLATE, { + TopicViewerDomainConstants.SUBTOPIC_VIEWER_URL_TEMPLATE, + { classroom_url_fragment: this.classroomUrlFragment, topic_url_fragment: this.topicUrlFragment, - subtopic_url_fragment: urlFragment + subtopic_url_fragment: urlFragment, } - ), '_self' + ), + '_self' ); } @@ -75,17 +80,23 @@ export class SubtopicSummaryTileComponent implements OnInit { this.subtopicTitle = this.subtopic.getTitle(); let thumbnailFileName = this.subtopic.getThumbnailFilename(); if (thumbnailFileName) { - this.thumbnailUrl = ( + this.thumbnailUrl = this.assetsBackendApiService.getThumbnailUrlForPreview( - AppConstants.ENTITY_TYPE.TOPIC, this.topicId, thumbnailFileName)); + AppConstants.ENTITY_TYPE.TOPIC, + this.topicId, + thumbnailFileName + ); } const urlFragment = this.subtopic.getUrlFragment(); if (urlFragment === null) { throw new Error('Expected subtopic to have a URL fragment'); } - this.subtopicTitleTranslationKey = this.i18nLanguageCodeService. - getSubtopicTranslationKey( - this.topicId, urlFragment, TranslationKeyType.TITLE); + this.subtopicTitleTranslationKey = + this.i18nLanguageCodeService.getSubtopicTranslationKey( + this.topicId, + urlFragment, + TranslationKeyType.TITLE + ); } isHackySubtopicTitleTranslationDisplayed(): boolean { @@ -98,6 +109,8 @@ export class SubtopicSummaryTileComponent implements OnInit { } angular.module('oppia').directive( - 'oppiaSubtopicSummaryTile', downgradeComponent({ - component: SubtopicSummaryTileComponent - }) as angular.IDirectiveFactory); + 'oppiaSubtopicSummaryTile', + downgradeComponent({ + component: SubtopicSummaryTileComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/components/summary-tile/summary-tile.module.ts b/core/templates/components/summary-tile/summary-tile.module.ts index e75b4d758eb1..a29a66f300c7 100644 --- a/core/templates/components/summary-tile/summary-tile.module.ts +++ b/core/templates/components/summary-tile/summary-tile.module.ts @@ -18,27 +18,16 @@ import 'core-js/es7/reflect'; import 'zone.js'; -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { TranslateModule } from '@ngx-translate/core'; -import { StringUtilityPipesModule } from 'filters/string-utility-filters/string-utility-pipes.module'; -import { TopicSummaryTileComponent } from './topic-summary-tile.component'; +import {CommonModule} from '@angular/common'; +import {NgModule} from '@angular/core'; +import {TranslateModule} from '@ngx-translate/core'; +import {StringUtilityPipesModule} from 'filters/string-utility-filters/string-utility-pipes.module'; +import {TopicSummaryTileComponent} from './topic-summary-tile.component'; @NgModule({ - imports: [ - CommonModule, - StringUtilityPipesModule, - TranslateModule, - ], - declarations: [ - TopicSummaryTileComponent - ], - entryComponents: [ - TopicSummaryTileComponent - ], - exports: [ - TopicSummaryTileComponent - ], + imports: [CommonModule, StringUtilityPipesModule, TranslateModule], + declarations: [TopicSummaryTileComponent], + entryComponents: [TopicSummaryTileComponent], + exports: [TopicSummaryTileComponent], }) - -export class SummaryTilesModule { } +export class SummaryTilesModule {} diff --git a/core/templates/components/summary-tile/topic-summary-tile.component.spec.ts b/core/templates/components/summary-tile/topic-summary-tile.component.spec.ts index 09cfaa9cf308..e2aa956a1649 100644 --- a/core/templates/components/summary-tile/topic-summary-tile.component.spec.ts +++ b/core/templates/components/summary-tile/topic-summary-tile.component.spec.ts @@ -16,14 +16,14 @@ * @fileoverview Unit tests for TopicSummaryTileComponent. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { CreatorTopicSummary } from 'domain/topic/creator-topic-summary.model'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { TopicSummaryTileComponent } from './topic-summary-tile.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {CreatorTopicSummary} from 'domain/topic/creator-topic-summary.model'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {TopicSummaryTileComponent} from './topic-summary-tile.component'; describe('TopicSummaryTileCompoennt', () => { let component: TopicSummaryTileComponent; @@ -36,10 +36,7 @@ describe('TopicSummaryTileCompoennt', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - TopicSummaryTileComponent, - MockTranslatePipe - ] + declarations: [TopicSummaryTileComponent, MockTranslatePipe], }).compileComponents(); })); @@ -73,7 +70,7 @@ describe('TopicSummaryTileCompoennt', () => { total_upcoming_chapters_count: 1, total_overdue_chapters_count: 1, total_chapter_counts_for_each_story: [5, 4], - published_chapter_counts_for_each_story: [3, 4] + published_chapter_counts_for_each_story: [3, 4], }); }); @@ -87,7 +84,8 @@ describe('TopicSummaryTileCompoennt', () => { it('should get topic page url', () => { spyOn(urlInterpolationService, 'interpolateUrl').and.returnValue( - '/topic/page/url'); + '/topic/page/url' + ); expect(component.getTopicPageUrl()).toBe('/topic/page/url'); }); @@ -102,25 +100,26 @@ describe('TopicSummaryTileCompoennt', () => { }); it('should get topic name translation key correctly', () => { - spyOn(i18nLanguageCodeService, 'getTopicTranslationKey') - .and.returnValue('I18N_TOPIC_abc1234_TITLE'); + spyOn(i18nLanguageCodeService, 'getTopicTranslationKey').and.returnValue( + 'I18N_TOPIC_abc1234_TITLE' + ); component.ngOnInit(); - expect(component.topicNameTranslationKey).toBe( - 'I18N_TOPIC_abc1234_TITLE'); + expect(component.topicNameTranslationKey).toBe('I18N_TOPIC_abc1234_TITLE'); }); - it('should check if hacky topic name translation is displayed correctly', - () => { - spyOn(i18nLanguageCodeService, 'isHackyTranslationAvailable') - .and.returnValue(true); - spyOn(i18nLanguageCodeService, 'isCurrentLanguageEnglish') - .and.returnValue(false); + it('should check if hacky topic name translation is displayed correctly', () => { + spyOn( + i18nLanguageCodeService, + 'isHackyTranslationAvailable' + ).and.returnValue(true); + spyOn(i18nLanguageCodeService, 'isCurrentLanguageEnglish').and.returnValue( + false + ); - component.ngOnInit(); + component.ngOnInit(); - expect(component.isHackyTopicNameTranslationDisplayed()).toBe(true); - } - ); + expect(component.isHackyTopicNameTranslationDisplayed()).toBe(true); + }); }); diff --git a/core/templates/components/summary-tile/topic-summary-tile.component.ts b/core/templates/components/summary-tile/topic-summary-tile.component.ts index 163ae454906a..8a3a546aff5d 100644 --- a/core/templates/components/summary-tile/topic-summary-tile.component.ts +++ b/core/templates/components/summary-tile/topic-summary-tile.component.ts @@ -16,18 +16,21 @@ * @fileoverview Component for a topic tile. */ -import { Component, Input } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { AppConstants } from 'app.constants'; -import { ClassroomDomainConstants } from 'domain/classroom/classroom-domain.constants'; -import { CreatorTopicSummary } from 'domain/topic/creator-topic-summary.model'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; -import { I18nLanguageCodeService, TranslationKeyType } from 'services/i18n-language-code.service'; +import {Component, Input} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {AppConstants} from 'app.constants'; +import {ClassroomDomainConstants} from 'domain/classroom/classroom-domain.constants'; +import {CreatorTopicSummary} from 'domain/topic/creator-topic-summary.model'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; +import { + I18nLanguageCodeService, + TranslationKeyType, +} from 'services/i18n-language-code.service'; @Component({ selector: 'oppia-topic-summary-tile', - templateUrl: './topic-summary-tile.component.html' + templateUrl: './topic-summary-tile.component.html', }) export class TopicSummaryTileComponent { // These properties are initialized using Angular lifecycle hooks @@ -47,31 +50,36 @@ export class TopicSummaryTileComponent { ngOnInit(): void { if (this.topicSummary.getThumbnailFilename()) { - this.thumbnailUrl = this.assetsBackendApiService - .getThumbnailUrlForPreview( - AppConstants.ENTITY_TYPE.TOPIC, this.topicSummary.getId(), - this.topicSummary.getThumbnailFilename()); + this.thumbnailUrl = + this.assetsBackendApiService.getThumbnailUrlForPreview( + AppConstants.ENTITY_TYPE.TOPIC, + this.topicSummary.getId(), + this.topicSummary.getThumbnailFilename() + ); } - this.topicNameTranslationKey = ( + this.topicNameTranslationKey = this.i18nLanguageCodeService.getTopicTranslationKey( - this.topicSummary.getId(), TranslationKeyType.TITLE)); + this.topicSummary.getId(), + TranslationKeyType.TITLE + ); } getTopicPageUrl(): string { return this.urlInterpolationService.interpolateUrl( - ClassroomDomainConstants.TOPIC_VIEWER_URL_TEMPLATE, { + ClassroomDomainConstants.TOPIC_VIEWER_URL_TEMPLATE, + { topic_url_fragment: this.topicSummary.getUrlFragment(), - classroom_url_fragment: this.classroomUrlFragment + classroom_url_fragment: this.classroomUrlFragment, } ); } getColorValueInHexForm(colorValue: number): string { - colorValue = (colorValue < 0) ? 0 : colorValue; + colorValue = colorValue < 0 ? 0 : colorValue; let colorValueString = colorValue.toString(16); - return ( - (colorValueString.length === 1) ? - '0' + colorValueString : colorValueString); + return colorValueString.length === 1 + ? '0' + colorValueString + : colorValueString; } getDarkerThumbnailBgColor(): string { @@ -81,11 +89,14 @@ export class TopicSummaryTileComponent { // Get RGB values of new darker color. let newRValue = this.getColorValueInHexForm( - parseInt(bgColor.substring(0, 2), 16) - 100); + parseInt(bgColor.substring(0, 2), 16) - 100 + ); let newGValue = this.getColorValueInHexForm( - parseInt(bgColor.substring(2, 4), 16) - 100); + parseInt(bgColor.substring(2, 4), 16) - 100 + ); let newBValue = this.getColorValueInHexForm( - parseInt(bgColor.substring(4, 6), 16) - 100); + parseInt(bgColor.substring(4, 6), 16) - 100 + ); return '#' + newRValue + newGValue + newBValue; } @@ -99,7 +110,9 @@ export class TopicSummaryTileComponent { } } -angular.module('oppia').directive('oppiaTopicSummaryTile', +angular.module('oppia').directive( + 'oppiaTopicSummaryTile', downgradeComponent({ - component: TopicSummaryTileComponent - }) as angular.IDirectiveFactory); + component: TopicSummaryTileComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/components/unsaved-changes-status-info/unsaved-changes-status-info-modal.component.ts b/core/templates/components/unsaved-changes-status-info/unsaved-changes-status-info-modal.component.ts index fb15945b8f55..45b1e9a09012 100644 --- a/core/templates/components/unsaved-changes-status-info/unsaved-changes-status-info-modal.component.ts +++ b/core/templates/components/unsaved-changes-status-info/unsaved-changes-status-info-modal.component.ts @@ -16,7 +16,7 @@ * @fileoverview Component for unsaved changes status information modal. */ -import { Component } from '@angular/core'; +import {Component} from '@angular/core'; @Component({ selector: 'oppia-unsaved-changes-status-info-modal', diff --git a/core/templates/components/version-diff-visualization/version-diff-visualization.component.spec.ts b/core/templates/components/version-diff-visualization/version-diff-visualization.component.spec.ts index 5cacd5391422..346ac2db2d90 100644 --- a/core/templates/components/version-diff-visualization/version-diff-visualization.component.spec.ts +++ b/core/templates/components/version-diff-visualization/version-diff-visualization.component.spec.ts @@ -16,17 +16,17 @@ * @fileoverview Unit tests for Version diff visualization component. */ -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { VersionDiffVisualizationComponent } from './version-diff-visualization.component'; -import { State } from 'domain/state/StateObjectFactory'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {VersionDiffVisualizationComponent} from './version-diff-visualization.component'; +import {State} from 'domain/state/StateObjectFactory'; class MockNgbModal { open() { return { - result: Promise.resolve() + result: Promise.resolve(), }; } } @@ -39,16 +39,14 @@ describe('Version Diff Visualization Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - VersionDiffVisualizationComponent - ], + declarations: [VersionDiffVisualizationComponent], providers: [ { provide: NgbModal, - useClass: MockNgbModal - } + useClass: MockNgbModal, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -67,42 +65,42 @@ describe('Version Diff Visualization Component', () => { 1: { newestStateName: 'A', stateProperty: 'changed', - originalStateName: 'A' + originalStateName: 'A', }, 2: { newestStateName: 'B', stateProperty: 'added', - originalStateName: 'A' + originalStateName: 'A', }, 3: { newestStateName: 'C', stateProperty: 'deleted', - originalStateName: 'B' + originalStateName: 'B', }, 4: { newestStateName: 'D', stateProperty: 'unchanged', - originalStateName: 'B' + originalStateName: 'B', }, 5: { newestStateName: 'E', stateProperty: 'changed', - originalStateName: 'B' + originalStateName: 'B', }, 6: { newestStateName: 'F', stateProperty: 'unchanged', - originalStateName: 'F' + originalStateName: 'F', }, }, v2States: { C: {} as State, - D: {} as State + D: {} as State, }, v1States: { A: {} as State, B: {} as State, - } + }, }; }); @@ -117,7 +115,7 @@ describe('Version Diff Visualization Component', () => { expect(component.diffGraphSecondaryLabels).toEqual({ 4: '(was: B)', - 5: '(was: B)' + 5: '(was: B)', }); expect(component.diffGraphNodeColors).toEqual({ 1: '#1E90FF', @@ -125,15 +123,15 @@ describe('Version Diff Visualization Component', () => { 3: '#DC143C', 4: '#FFD700', 5: '#1E90FF', - 6: 'beige' + 6: 'beige', }); expect(component.v1InitStateId).toEqual(0); - expect(component.diffGraphData).toEqual( - { - nodes: { 1: 'A', 2: 'B', 3: 'B', 4: 'D', 5: 'E', 6: 'F' }, - links: [], initStateId: 1, finalStateIds: ['C', 'D'] - } - ); + expect(component.diffGraphData).toEqual({ + nodes: {1: 'A', 2: 'B', 3: 'B', 4: 'D', 5: 'E', 6: 'F'}, + links: [], + initStateId: 1, + finalStateIds: ['C', 'D'], + }); expect(component.legendGraph).toEqual({ nodes: { Added: 'Added', @@ -141,94 +139,102 @@ describe('Version Diff Visualization Component', () => { Changed: 'Changed', Unchanged: 'Unchanged', Renamed: 'Renamed', - 'Changed/renamed': 'Changed/renamed' + 'Changed/renamed': 'Changed/renamed', }, - links: [{ - source: 'Added', - target: 'Deleted', - linkProperty: 'hidden' - }, { - source: 'Deleted', - target: 'Changed', - linkProperty: 'hidden' - }, { - source: 'Changed', - target: 'Unchanged', - linkProperty: 'hidden' - }, { - source: 'Unchanged', - target: 'Renamed', - linkProperty: 'hidden' - }, { - source: 'Renamed', - target: 'Changed/renamed', - linkProperty: 'hidden' - }], + links: [ + { + source: 'Added', + target: 'Deleted', + linkProperty: 'hidden', + }, + { + source: 'Deleted', + target: 'Changed', + linkProperty: 'hidden', + }, + { + source: 'Changed', + target: 'Unchanged', + linkProperty: 'hidden', + }, + { + source: 'Unchanged', + target: 'Renamed', + linkProperty: 'hidden', + }, + { + source: 'Renamed', + target: 'Changed/renamed', + linkProperty: 'hidden', + }, + ], initStateId: 'Changed/renamed', - finalStateIds: ['Changed/renamed'] + finalStateIds: ['Changed/renamed'], }); }); it('should throw error if state property is invalid', () => { - component.diffData = ( - { - nodes: { - 1: { - newestStateName: 'A', - stateProperty: 'invalid', - originalStateName: 'A' - } + component.diffData = { + nodes: { + 1: { + newestStateName: 'A', + stateProperty: 'invalid', + originalStateName: 'A', }, - v2States: {}, - v1States: {}, - finalStateIds: [], - v2InitStateId: 0, - links: [], - v1InitStateId: 0, - } - ); + }, + v2States: {}, + v1States: {}, + finalStateIds: [], + v2InitStateId: 0, + links: [], + v1InitStateId: 0, + }; expect(() => component.ngOnInit()).toThrowError('Invalid state property.'); }); - it('should open state diff modal when user clicks on a state in' + - ' difference graph', () => { - class MockComponentInstance { - compoenentInstance!: { - newState: null; - newStateName: 'A'; - oldState: null; - oldStateName: 'B'; - headers: { - leftPane: undefined; - rightPane: undefined; + it( + 'should open state diff modal when user clicks on a state in' + + ' difference graph', + () => { + class MockComponentInstance { + compoenentInstance!: { + newState: null; + newStateName: 'A'; + oldState: null; + oldStateName: 'B'; + headers: { + leftPane: undefined; + rightPane: undefined; + }; }; - }; - } + } - let spyObj = spyOn(ngbModal, 'open').and.callFake(() => { - return ({ - componentInstance: MockComponentInstance, - result: Promise.resolve() - }) as NgbModalRef; - }); + let spyObj = spyOn(ngbModal, 'open').and.callFake(() => { + return { + componentInstance: MockComponentInstance, + result: Promise.resolve(), + } as NgbModalRef; + }); - component.ngOnInit(); - component.onClickStateInDiffGraph('2'); + component.ngOnInit(); + component.onClickStateInDiffGraph('2'); - expect(spyObj).toHaveBeenCalled(); - }); + expect(spyObj).toHaveBeenCalled(); + } + ); - it('should open state diff modal and return old and new states when' + - ' when user clicks on a state in difference graph', () => { - component.diffData = ( - { + it( + 'should open state diff modal and return old and new states when' + + ' when user clicks on a state in difference graph', + () => { + component.diffData = { nodes: { 1: { newestStateName: 'A', stateProperty: 'changed', - originalStateName: 'B' - } + originalStateName: 'B', + }, }, v2States: { A: {} as State, @@ -242,41 +248,41 @@ describe('Version Diff Visualization Component', () => { v2InitStateId: 0, links: [], v1InitStateId: 0, - } - ); + }; - class MockComponentInstance { - compoenentInstance!: { - newState: {}; - newStateName: 'A'; - oldState: {}; - oldStateName: 'B'; - headers: { - leftPane: undefined; - rightPane: undefined; + class MockComponentInstance { + compoenentInstance!: { + newState: {}; + newStateName: 'A'; + oldState: {}; + oldStateName: 'B'; + headers: { + leftPane: undefined; + rightPane: undefined; + }; }; - }; - } + } - let spyObj = spyOn(ngbModal, 'open').and.callFake(() => { - return ({ - componentInstance: MockComponentInstance, - result: Promise.resolve() - }) as NgbModalRef; - }); + let spyObj = spyOn(ngbModal, 'open').and.callFake(() => { + return { + componentInstance: MockComponentInstance, + result: Promise.resolve(), + } as NgbModalRef; + }); - component.ngOnInit(); - component.onClickStateInDiffGraph('1'); + component.ngOnInit(); + component.onClickStateInDiffGraph('1'); - expect(spyObj).toHaveBeenCalled(); - }); + expect(spyObj).toHaveBeenCalled(); + } + ); it('should close state diff modal when user clicks cancel', () => { let spyObj = spyOn(ngbModal, 'open').and.callFake(() => { - return ({ + return { componentInstance: {}, - result: Promise.reject() - }) as NgbModalRef; + result: Promise.reject(), + } as NgbModalRef; }); component.ngOnInit(); diff --git a/core/templates/components/version-diff-visualization/version-diff-visualization.component.ts b/core/templates/components/version-diff-visualization/version-diff-visualization.component.ts index ad2a6338750f..79c8d0068b77 100644 --- a/core/templates/components/version-diff-visualization/version-diff-visualization.component.ts +++ b/core/templates/components/version-diff-visualization/version-diff-visualization.component.ts @@ -17,13 +17,13 @@ * versions of an exploration. */ -import { Component, Input, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { State } from 'domain/state/StateObjectFactory'; -import { StateDiffModalComponent } from 'pages/exploration-editor-page/modal-templates/state-diff-modal.component'; -import { StateLink } from 'pages/exploration-editor-page/services/exploration-diff.service'; +import {Component, Input, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {State} from 'domain/state/StateObjectFactory'; +import {StateDiffModalComponent} from 'pages/exploration-editor-page/modal-templates/state-diff-modal.component'; +import {StateLink} from 'pages/exploration-editor-page/services/exploration-diff.service'; interface NodesData { [key: string]: { @@ -94,7 +94,7 @@ interface DiffGraphData { @Component({ selector: 'oppia-version-diff-visualization', - templateUrl: './version-diff-visualization.component.html' + templateUrl: './version-diff-visualization.component.html', }) export class VersionDiffVisualizationComponent implements OnInit { // An object with the following properties: @@ -171,26 +171,29 @@ export class VersionDiffVisualizationComponent implements OnInit { legendGraph!: LegendGraph; LEGEND_GRAPH_COLORS!: LEGEND_GRAPH_COLORS | Record; LEGEND_GRAPH_SECONDARY_LABELS!: - LEGEND_GRAPH_SECONDARY_LABELS | Record; + | LEGEND_GRAPH_SECONDARY_LABELS + | Record; LEGEND_GRAPH_LINK_PROPERTY_MAPPING!: LEGEND_GRAPH_LINK_PROPERTY_MAPPING; - constructor( - private ngbModal: NgbModal, - ) {} + constructor(private ngbModal: NgbModal) {} // Opens the modal showing the history diff for a given state. // stateId is the unique ID assigned to a state during the // calculation of the state graph. onClickStateInDiffGraph(stateId: string): void { let oldStateName = null; - if (this.nodesData[stateId].newestStateName !== - this.nodesData[stateId].originalStateName) { + if ( + this.nodesData[stateId].newestStateName !== + this.nodesData[stateId].originalStateName + ) { oldStateName = this.nodesData[stateId].originalStateName; } this.showStateDiffModal( this.nodesData[stateId].newestStateName, - oldStateName, this.nodesData[stateId].stateProperty); + oldStateName, + this.nodesData[stateId].stateProperty + ); } // Shows a modal comparing changes on a state between 2 versions. @@ -203,29 +206,33 @@ export class VersionDiffVisualizationComponent implements OnInit { // - stateProperty is whether the state is added, changed, unchanged or // deleted. showStateDiffModal( - newStateName: string, - oldStateName: string | null, - stateProperty: string + newStateName: string, + oldStateName: string | null, + stateProperty: string ): void { let modalRef: NgbModalRef = this.ngbModal.open(StateDiffModalComponent, { backdrop: true, windowClass: 'state-diff-modal', - size: 'xl' + size: 'xl', }); modalRef.componentInstance.newStateName = newStateName; modalRef.componentInstance.oldStateName = oldStateName; let newState = null; - if (stateProperty !== this.STATE_PROPERTY_DELETED && - this.diffData.v2States.hasOwnProperty(newStateName)) { + if ( + stateProperty !== this.STATE_PROPERTY_DELETED && + this.diffData.v2States.hasOwnProperty(newStateName) + ) { newState = this.diffData.v2States[newStateName]; } let oldState = null; let stateNameToRetrieve = oldStateName || newStateName; - if (stateProperty !== this.STATE_PROPERTY_ADDED && - this.diffData.v1States.hasOwnProperty(stateNameToRetrieve)) { + if ( + stateProperty !== this.STATE_PROPERTY_ADDED && + this.diffData.v1States.hasOwnProperty(stateNameToRetrieve) + ) { oldState = this.diffData.v1States[stateNameToRetrieve]; } @@ -233,13 +240,15 @@ export class VersionDiffVisualizationComponent implements OnInit { modalRef.componentInstance.oldState = oldState; modalRef.componentInstance.headers = { leftPane: this.earlierVersionHeader, - rightPane: this.laterVersionHeader + rightPane: this.laterVersionHeader, }; - modalRef.result.then(() => {}, () => {}); + modalRef.result.then( + () => {}, + () => {} + ); } - ngOnInit(): void { this.nodesData = this.diffData.nodes; this._stateTypeUsed[this.NODE_TYPE_ADDED] = false; @@ -253,26 +262,26 @@ export class VersionDiffVisualizationComponent implements OnInit { this.LEGEND_GRAPH_COLORS[this.NODE_TYPE_DELETED] = this.COLOR_DELETED; this.LEGEND_GRAPH_COLORS[this.NODE_TYPE_CHANGED] = this.COLOR_CHANGED; this.LEGEND_GRAPH_COLORS[this.NODE_TYPE_UNCHANGED] = this.COLOR_UNCHANGED; - this.LEGEND_GRAPH_COLORS[this.NODE_TYPE_RENAMED] = ( - this.COLOR_RENAMED_UNCHANGED); - this.LEGEND_GRAPH_COLORS[this.NODE_TYPE_CHANGED_RENAMED] = ( - this.COLOR_CHANGED); + this.LEGEND_GRAPH_COLORS[this.NODE_TYPE_RENAMED] = + this.COLOR_RENAMED_UNCHANGED; + this.LEGEND_GRAPH_COLORS[this.NODE_TYPE_CHANGED_RENAMED] = + this.COLOR_CHANGED; this.LEGEND_GRAPH_SECONDARY_LABELS = {}; - this.LEGEND_GRAPH_SECONDARY_LABELS[this.NODE_TYPE_CHANGED_RENAMED] = ( - '(was: Old name)'); - this.LEGEND_GRAPH_SECONDARY_LABELS[this.NODE_TYPE_RENAMED] = ( - '(was: Old name)'); + this.LEGEND_GRAPH_SECONDARY_LABELS[this.NODE_TYPE_CHANGED_RENAMED] = + '(was: Old name)'; + this.LEGEND_GRAPH_SECONDARY_LABELS[this.NODE_TYPE_RENAMED] = + '(was: Old name)'; this.LEGEND_GRAPH_LINK_PROPERTY_MAPPING = { - hidden: 'stroke: none; marker-end: none;' + hidden: 'stroke: none; marker-end: none;', }; this.DIFF_GRAPH_LINK_PROPERTY_MAPPING = { - added: ( + added: 'stroke: #1F7D1F; stroke-opacity: 0.8; ' + - 'marker-end: url(#arrowhead-green)'), - deleted: ( + 'marker-end: url(#arrowhead-green)', + deleted: 'stroke: #B22222; stroke-opacity: 0.8; ' + - 'marker-end: url(#arrowhead-red)') + 'marker-end: url(#arrowhead-red)', }; this.diffGraphSecondaryLabels = {}; this.diffGraphNodeColors = {}; @@ -290,10 +299,12 @@ export class VersionDiffVisualizationComponent implements OnInit { } else if (nodeStateProperty === this.STATE_PROPERTY_CHANGED) { this.diffGraphNodes[nodeId] = this.nodesData[nodeId].originalStateName; this.diffGraphNodeColors[nodeId] = this.COLOR_CHANGED; - if (this.nodesData[nodeId].originalStateName !== - this.nodesData[nodeId].newestStateName) { - this.diffGraphSecondaryLabels[nodeId] = '(was: ' + - this.nodesData[nodeId].originalStateName + ')'; + if ( + this.nodesData[nodeId].originalStateName !== + this.nodesData[nodeId].newestStateName + ) { + this.diffGraphSecondaryLabels[nodeId] = + '(was: ' + this.nodesData[nodeId].originalStateName + ')'; this.diffGraphNodes[nodeId] = this.nodesData[nodeId].newestStateName; this._stateTypeUsed[this.NODE_TYPE_CHANGED_RENAMED] = true; } else { @@ -302,10 +313,12 @@ export class VersionDiffVisualizationComponent implements OnInit { } else if (nodeStateProperty === this.STATE_PROPERTY_UNCHANGED) { this.diffGraphNodes[nodeId] = this.nodesData[nodeId].originalStateName; this.diffGraphNodeColors[nodeId] = this.COLOR_UNCHANGED; - if (this.nodesData[nodeId].originalStateName !== - this.nodesData[nodeId].newestStateName) { - this.diffGraphSecondaryLabels[nodeId] = '(was: ' + - this.nodesData[nodeId].originalStateName + ')'; + if ( + this.nodesData[nodeId].originalStateName !== + this.nodesData[nodeId].newestStateName + ) { + this.diffGraphSecondaryLabels[nodeId] = + '(was: ' + this.nodesData[nodeId].originalStateName + ')'; this.diffGraphNodes[nodeId] = this.nodesData[nodeId].newestStateName; this.diffGraphNodeColors[nodeId] = this.COLOR_RENAMED_UNCHANGED; this._stateTypeUsed[this.NODE_TYPE_RENAMED] = true; @@ -323,7 +336,7 @@ export class VersionDiffVisualizationComponent implements OnInit { nodes: this.diffGraphNodes, links: this.diffData.links, initStateId: this.diffData.v2InitStateId, - finalStateIds: this.diffData.finalStateIds + finalStateIds: this.diffData.finalStateIds, }; // Generate the legend graph. @@ -331,7 +344,7 @@ export class VersionDiffVisualizationComponent implements OnInit { nodes: {}, links: [], finalStateIds: [], - initStateId: '' + initStateId: '', }; // Last used state type is null by default. @@ -343,7 +356,7 @@ export class VersionDiffVisualizationComponent implements OnInit { this.legendGraph.links.push({ source: _lastUsedStateType, target: stateProperty, - linkProperty: 'hidden' + linkProperty: 'hidden', }); } _lastUsedStateType = stateProperty; @@ -355,7 +368,9 @@ export class VersionDiffVisualizationComponent implements OnInit { } } } -angular.module('oppia').directive('oppiaVersionDiffVisualization', +angular.module('oppia').directive( + 'oppiaVersionDiffVisualization', downgradeComponent({ - component: VersionDiffVisualizationComponent - }) as angular.IDirectiveFactory); + component: VersionDiffVisualizationComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/css/README.md b/core/templates/css/README.md index 5ffe9222b997..ec6328e498d6 100644 --- a/core/templates/css/README.md +++ b/core/templates/css/README.md @@ -1,23 +1,29 @@ # CSS + ## Overview + This folder contains all the global style sheets for the oppia frontend. ## Files: + 1. oppia.css — Custom css styles written by the oppia-devs for the frontend. 2. oppia-material.css — Material CSS for the oppia-codebase. This css file is generated and shouldn't be modified at all. ## Modification procedure: + 1. oppia.css can be modified with proper reasoning in the pr that modifies the file. 2. oppia-material.css shouldn't be changed at any cost. No changes are accepted in this file. If, at any time, the oppia-material.css is overriding the styles in oppia.css, create a style tag in the directive and make the selectors more specific. ## Oppia Material + **If at any time the css file is regenerated please update the pr number here** - Introduced in: #9577 - Updated in: N/A (Comma separated pr numbers). ### Info + More info regarding the oppia-material.css can be found in this doc: Material CSS doc: https://docs.google.com/document/d/1UoCOC7XNhCZrWIMPAoR5Xex28OYWzteqXrqCU9gRUHQ/edit?usp=sharing diff --git a/core/templates/directives/angular-html-bind.directive.ts b/core/templates/directives/angular-html-bind.directive.ts index af64bbf52297..c1047aa1abb3 100644 --- a/core/templates/directives/angular-html-bind.directive.ts +++ b/core/templates/directives/angular-html-bind.directive.ts @@ -21,14 +21,15 @@ // HTML bind directive that trusts the value it is given and also evaluates // custom directive tags in the provided value. angular.module('oppia').directive('angularHtmlBind', [ - '$compile', function($compile) { + '$compile', + function ($compile) { return { restrict: 'E', - link: function(scope, elm, attrs) { + link: function (scope, elm, attrs) { // Clean up old scopes if the html changes. // Reference: https://stackoverflow.com/a/42927814 var newScope: ng.IScope; - scope.$watch(attrs.htmlData, function(newValue: string) { + scope.$watch(attrs.htmlData, function (newValue: string) { if (newScope) { newScope.$destroy(); } @@ -54,6 +55,7 @@ angular.module('oppia').directive('angularHtmlBind', [ elm.html(newValue as string); $compile(elm.contents())(newScope); }); - } + }, }; - }]); + }, +]); diff --git a/core/templates/directives/directives.module.ts b/core/templates/directives/directives.module.ts index d5439640e815..5bdd84afbf19 100644 --- a/core/templates/directives/directives.module.ts +++ b/core/templates/directives/directives.module.ts @@ -18,20 +18,17 @@ import 'core-js/es7/reflect'; import 'zone.js'; -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { FormsModule } from '@angular/forms'; +import {CommonModule} from '@angular/common'; +import {NgModule} from '@angular/core'; +import {FormsModule} from '@angular/forms'; -import { FocusOnDirective } from './focus-on.directive'; -import { HeadroomDirective } from './headroom.directive'; -import { MathJaxDirective } from './mathjax.directive'; -import { NgInitDirective } from './ng-init.directive'; +import {FocusOnDirective} from './focus-on.directive'; +import {HeadroomDirective} from './headroom.directive'; +import {MathJaxDirective} from './mathjax.directive'; +import {NgInitDirective} from './ng-init.directive'; @NgModule({ - imports: [ - CommonModule, - FormsModule, - ], + imports: [CommonModule, FormsModule], declarations: [ FocusOnDirective, HeadroomDirective, @@ -46,5 +43,4 @@ import { NgInitDirective } from './ng-init.directive'; NgInitDirective, ], }) - -export class DirectivesModule { } +export class DirectivesModule {} diff --git a/core/templates/directives/focus-on.directive.spec.ts b/core/templates/directives/focus-on.directive.spec.ts index 5cafd14519a6..ffd9001df5b0 100644 --- a/core/templates/directives/focus-on.directive.spec.ts +++ b/core/templates/directives/focus-on.directive.spec.ts @@ -16,15 +16,21 @@ * @fileoverview Unit tests for Focus on directive. */ -import { Component, EventEmitter } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { FocusOnDirective } from './focus-on.directive'; +import {Component, EventEmitter} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {FocusOnDirective} from './focus-on.directive'; @Component({ selector: 'mock-comp-a', - template: '
' + template: '
', }) class MockCompA { label!: 'label'; @@ -39,17 +45,19 @@ describe('Focus on component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [MockCompA, FocusOnDirective] + declarations: [MockCompA, FocusOnDirective], }).compileComponents(); focusManagerService = TestBed.inject(FocusManagerService); - focusSpy = spyOnProperty(focusManagerService, 'onFocus') - .and.returnValue(mockEventEmitter); + focusSpy = spyOnProperty(focusManagerService, 'onFocus').and.returnValue( + mockEventEmitter + ); fixture = TestBed.createComponent(MockCompA); fixture.detectChanges(); const directiveEl = fixture.debugElement.query( - By.directive(FocusOnDirective)); + By.directive(FocusOnDirective) + ); expect(directiveEl).not.toBeNull(); directiveInstance = directiveEl.injector.get(FocusOnDirective); })); @@ -58,26 +66,24 @@ describe('Focus on component', () => { directiveInstance.ngOnDestroy(); }); - it('should successfully compile the given component', fakeAsync( - () => { - let el = fixture.debugElement.nativeElement - .getElementsByClassName('focus-label')[0].innerHtml = ( - '
'); + it('should successfully compile the given component', fakeAsync(() => { + let el = (fixture.debugElement.nativeElement.getElementsByClassName( + 'focus-label' + )[0].innerHtml = '
'); - directiveInstance.focusOn = 'label'; - mockEventEmitter.emit('labelForClearingFocus'); - tick(); + directiveInstance.focusOn = 'label'; + mockEventEmitter.emit('labelForClearingFocus'); + tick(); - expect(el).toEqual('
'); - })); + expect(el).toEqual('
'); + })); - it('should focus on the given label', fakeAsync( - () => { - directiveInstance.focusOn = 'label'; + it('should focus on the given label', fakeAsync(() => { + directiveInstance.focusOn = 'label'; - mockEventEmitter.emit('label'); - tick(); + mockEventEmitter.emit('label'); + tick(); - expect(focusSpy).toHaveBeenCalled(); - })); + expect(focusSpy).toHaveBeenCalled(); + })); }); diff --git a/core/templates/directives/focus-on.directive.ts b/core/templates/directives/focus-on.directive.ts index 3316b0eacb19..dbb056f08bf6 100644 --- a/core/templates/directives/focus-on.directive.ts +++ b/core/templates/directives/focus-on.directive.ts @@ -29,15 +29,15 @@ * AngularJS codebase. */ -import { Directive, ElementRef, Input, OnDestroy } from '@angular/core'; +import {Directive, ElementRef, Input, OnDestroy} from '@angular/core'; -import { Subscription } from 'rxjs'; +import {Subscription} from 'rxjs'; -import { AppConstants } from 'app.constants'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; +import {AppConstants} from 'app.constants'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; @Directive({ - selector: '[oppiaFocusOn]' + selector: '[oppiaFocusOn]', }) export class FocusOnDirective implements OnDestroy { // This property is initialized using component interactions @@ -46,21 +46,21 @@ export class FocusOnDirective implements OnDestroy { @Input('oppiaFocusOn') focusOn!: string; directiveSubscriptions = new Subscription(); constructor( - private el: ElementRef, private focusManagerService: FocusManagerService) { + private el: ElementRef, + private focusManagerService: FocusManagerService + ) { this.directiveSubscriptions.add( - this.focusManagerService.onFocus.subscribe( - (name: string) => { - if (name === this.focusOn) { - this.el.nativeElement.focus(); - } + this.focusManagerService.onFocus.subscribe((name: string) => { + if (name === this.focusOn) { + this.el.nativeElement.focus(); + } - // If the purpose of the focus switch was to clear focus, blur the - // element. - if (name === AppConstants.LABEL_FOR_CLEARING_FOCUS) { - this.el.nativeElement.blur(); - } + // If the purpose of the focus switch was to clear focus, blur the + // element. + if (name === AppConstants.LABEL_FOR_CLEARING_FOCUS) { + this.el.nativeElement.blur(); } - ) + }) ); } diff --git a/core/templates/directives/headroom.directive.spec.ts b/core/templates/directives/headroom.directive.spec.ts index 2094a2b3cd2c..511acb00fd2f 100644 --- a/core/templates/directives/headroom.directive.spec.ts +++ b/core/templates/directives/headroom.directive.spec.ts @@ -16,14 +16,14 @@ * @fileoverview Unit tests for headroom directive */ -import { Component } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { HeadroomDirective } from './headroom.directive'; +import {Component} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; +import {HeadroomDirective} from './headroom.directive'; @Component({ selector: 'mock-comp-a', - template: ' ' + template: ' ', }) class MockCompA {} @@ -33,14 +33,15 @@ describe('Headroom Directive', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [MockCompA, HeadroomDirective] + declarations: [MockCompA, HeadroomDirective], }).compileComponents(); })); beforeEach(waitForAsync(() => { fixture = TestBed.createComponent(MockCompA); const directiveEl = fixture.debugElement.query( - By.directive(HeadroomDirective)); + By.directive(HeadroomDirective) + ); expect(directiveEl).not.toBeNull(); directiveInstance = directiveEl.injector.get(HeadroomDirective); diff --git a/core/templates/directives/headroom.directive.ts b/core/templates/directives/headroom.directive.ts index 00671b6b9d69..abd26dcfcba3 100644 --- a/core/templates/directives/headroom.directive.ts +++ b/core/templates/directives/headroom.directive.ts @@ -18,34 +18,40 @@ * NB: Reusable component directives should go in the components/ folder. */ -import { Directive, ElementRef, EventEmitter, Input, OnDestroy, Output } from '@angular/core'; +import { + Directive, + ElementRef, + EventEmitter, + Input, + OnDestroy, + Output, +} from '@angular/core'; import Headroom from 'headroom.js'; @Directive({ - selector: '[headroom]' + selector: '[headroom]', }) export class HeadroomDirective implements OnDestroy { @Input() tolerance?: Headroom.Tolerance; - @Output() toleranceChange: EventEmitter = ( - new EventEmitter()); + @Output() toleranceChange: EventEmitter = + new EventEmitter(); @Input() offset?: number; @Output() offsetChange?: number; - @Input() classes?: { [key: string]: string }; - @Output() classesChange: EventEmitter<{ [key: string]: string }> = ( - new EventEmitter()); + @Input() classes?: {[key: string]: string}; + @Output() classesChange: EventEmitter<{[key: string]: string}> = + new EventEmitter(); @Input() scroller?: ElementRef; headroom: Headroom; - constructor( - private el: ElementRef - ) { + constructor(private el: ElementRef) { let headroomOptions: Headroom.HeadroomOptions = { tolerance: this.tolerance ? this.tolerance : Headroom.options.tolerance, offset: this.offset ? this.offset : Headroom.options.offset, - scroller: this.scroller ? document.querySelector( - this.scroller.nativeElement) : Headroom.options.scroller, + scroller: this.scroller + ? document.querySelector(this.scroller.nativeElement) + : Headroom.options.scroller, classes: this.classes ? this.classes : Headroom.options.classes, }; diff --git a/core/templates/directives/mathjax-bind.directive.ts b/core/templates/directives/mathjax-bind.directive.ts index 33749aa7232c..59a3f3be8cf7 100644 --- a/core/templates/directives/mathjax-bind.directive.ts +++ b/core/templates/directives/mathjax-bind.directive.ts @@ -20,26 +20,31 @@ require('mathjaxConfig.ts'); -angular.module('oppia').directive('mathjaxBind', [function() { - return { - restrict: 'E', - controller: [ - '$attrs', '$element', '$scope', function($attrs, $element, $scope) { - var ctrl = this; - ctrl.$onInit = function() { - $scope.$watch($attrs.mathjaxData, function(value) { - // TODO(#10197): Upgrade to MathJax 3, after proper investigation - // and testing. MathJax 3 provides a faster and more cleaner way to - // convert a LaTeX string to an SVG. - var $script = angular.element( - '' - }])).toEqual('abc<script></script>'); - expect(expressionInterpolationService.processHtml( - 'abc{{a}}', [{}]) + 'abc' + ); + expect( + expressionInterpolationService.processHtml('abc{{a}}', [ + { + a: 'b', + }, + ]) + ).toEqual('abcb'); + expect( + expressionInterpolationService.processHtml('abc{{a}}', [ + { + a: '', + }, + ]) + ).toEqual('abc<script></script>'); + expect( + expressionInterpolationService.processHtml('abc{{a}}', [{}]) ).toEqual('abc'); - expect(expressionInterpolationService.processHtml('abc{{a{{b}}}}', [{ - a: '1', - b: '2' - }])).toEqual( - 'abc}}'); + expect( + expressionInterpolationService.processHtml('abc{{a{{b}}}}', [ + { + a: '1', + b: '2', + }, + ]) + ).toEqual('abc}}'); - expect(expressionInterpolationService.processHtml('abc{{a+b}}', [{ - a: '1', - b: '2' - }])).toEqual('abc3'); - expect(expressionInterpolationService.processHtml('abc{{a+b}}', [{ - a: '1', - b: 'hello' - }])).toEqual( - 'abc'); + expect( + expressionInterpolationService.processHtml('abc{{a+b}}', [ + { + a: '1', + b: '2', + }, + ]) + ).toEqual('abc3'); + expect( + expressionInterpolationService.processHtml('abc{{a+b}}', [ + { + a: '1', + b: 'hello', + }, + ]) + ).toEqual('abc'); }); - it('should correctly interpolate unicode strings', ()=> { - expect(expressionInterpolationService.processUnicode( - 'abc', [{}])).toEqual('abc'); - expect(expressionInterpolationService.processUnicode('abc{{a}}', [{ - a: 'b' - }])).toEqual('abcb'); - expect(expressionInterpolationService.processUnicode('abc{{a}}', [{ - a: '' - }])).toEqual('abc'); - expect(expressionInterpolationService.processUnicode( - 'abc{{a}}', [{}])).toBeNull(); + it('should correctly interpolate unicode strings', () => { + expect(expressionInterpolationService.processUnicode('abc', [{}])).toEqual( + 'abc' + ); + expect( + expressionInterpolationService.processUnicode('abc{{a}}', [ + { + a: 'b', + }, + ]) + ).toEqual('abcb'); + expect( + expressionInterpolationService.processUnicode('abc{{a}}', [ + { + a: '', + }, + ]) + ).toEqual('abc'); + expect( + expressionInterpolationService.processUnicode('abc{{a}}', [{}]) + ).toBeNull(); - expect(expressionInterpolationService.processUnicode('abc{{a+b}}', [{ - a: '1', - b: '2' - }])).toEqual('abc3'); - expect(expressionInterpolationService.processUnicode('abc{{a+b}}', [{ - a: '1', - b: 'hello' - }])).toBeNull(); + expect( + expressionInterpolationService.processUnicode('abc{{a+b}}', [ + { + a: '1', + b: '2', + }, + ]) + ).toEqual('abc3'); + expect( + expressionInterpolationService.processUnicode('abc{{a+b}}', [ + { + a: '1', + b: 'hello', + }, + ]) + ).toBeNull(); }); - it('should correctly get params from strings', ()=> { - expect(expressionInterpolationService.getParamsFromString( - 'abc')).toEqual([]); - expect(expressionInterpolationService.getParamsFromString( - 'abc{{a}}')).toEqual(['a']); - expect(expressionInterpolationService.getParamsFromString( - 'abc{{a+b}}')).toEqual(['a', 'b']); + it('should correctly get params from strings', () => { + expect(expressionInterpolationService.getParamsFromString('abc')).toEqual( + [] + ); + expect( + expressionInterpolationService.getParamsFromString('abc{{a}}') + ).toEqual(['a']); + expect( + expressionInterpolationService.getParamsFromString('abc{{a+b}}') + ).toEqual(['a', 'b']); }); }); diff --git a/core/templates/expressions/expression-interpolation.service.ts b/core/templates/expressions/expression-interpolation.service.ts index 8c75ec353467..40a3a7f79574 100644 --- a/core/templates/expressions/expression-interpolation.service.ts +++ b/core/templates/expressions/expression-interpolation.service.ts @@ -16,19 +16,17 @@ * @fileoverview Service for interpolating expressions. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; -import { convertHtmlToUnicode } from 'filters/convert-html-to-unicode.filter'; -import { ExpressionEvaluatorService } from - 'expressions/expression-evaluator.service'; -import { ExpressionParserService } from 'expressions/expression-parser.service'; -import { ExpressionSyntaxTreeService } from - 'expressions/expression-syntax-tree.service'; -import { HtmlEscaperService } from 'services/html-escaper.service'; +import {convertHtmlToUnicode} from 'filters/convert-html-to-unicode.filter'; +import {ExpressionEvaluatorService} from 'expressions/expression-evaluator.service'; +import {ExpressionParserService} from 'expressions/expression-parser.service'; +import {ExpressionSyntaxTreeService} from 'expressions/expression-syntax-tree.service'; +import {HtmlEscaperService} from 'services/html-escaper.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ExpressionInterpolationService { constructor( @@ -39,19 +37,24 @@ export class ExpressionInterpolationService { ) {} processHtml(sourceHtml: string, envs: Record[]): string { - return sourceHtml.replace(/{{([^}]*)}}/g, (match, p1)=> { + return sourceHtml.replace(/{{([^}]*)}}/g, (match, p1) => { try { // TODO(sll): Remove the call to $filter once we have a // custom UI for entering expressions. It is only needed because // expressions are currently input inline via the RTE. return this.htmlEscaperService.unescapedStrToEscapedStr( this.expressionEvaluatorService.evaluateExpression( - convertHtmlToUnicode(p1), envs) as string); + convertHtmlToUnicode(p1), + envs + ) as string + ); } catch (e) { - const EXPRESSION_ERROR_TAG = ( - ''); - if ((e instanceof this.expressionParserService.SyntaxError) || - (e instanceof this.expressionSyntaxTreeService.ExpressionError)) { + const EXPRESSION_ERROR_TAG = + ''; + if ( + e instanceof this.expressionParserService.SyntaxError || + e instanceof this.expressionSyntaxTreeService.ExpressionError + ) { return EXPRESSION_ERROR_TAG; } throw e; @@ -61,18 +64,24 @@ export class ExpressionInterpolationService { // Function returns null if there is some error in the expression or syntax. processUnicode( - sourceUnicode: string, envs: Record[]): string | null { + sourceUnicode: string, + envs: Record[] + ): string | null { try { - return sourceUnicode.replace(/{{([^}]*)}}/g, (match, p1)=> { + return sourceUnicode.replace(/{{([^}]*)}}/g, (match, p1) => { // TODO(sll): Remove the call to $filter once we have a // custom UI for entering expressions. It is only needed because // expressions are currently input inline via the RTE. return this.expressionEvaluatorService.evaluateExpression( - convertHtmlToUnicode(p1), envs) as string; + convertHtmlToUnicode(p1), + envs + ) as string; }); } catch (e) { - if ((e instanceof this.expressionParserService.SyntaxError) || - (e instanceof this.expressionSyntaxTreeService.ExpressionError)) { + if ( + e instanceof this.expressionParserService.SyntaxError || + e instanceof this.expressionSyntaxTreeService.ExpressionError + ) { return null; } throw e; @@ -88,7 +97,8 @@ export class ExpressionInterpolationService { matches[i] = matches[i].substring(2, matches[i].length - 2); let params = this.expressionSyntaxTreeService.getParamsUsedInExpression( - convertHtmlToUnicode(matches[i])); + convertHtmlToUnicode(matches[i]) + ); for (let j = 0; j < params.length; j++) { if (allParams.indexOf(params[j]) === -1) { @@ -101,6 +111,9 @@ export class ExpressionInterpolationService { } } -angular.module('oppia').factory( - 'ExpressionInterpolationService', - downgradeInjectable(ExpressionInterpolationService)); +angular + .module('oppia') + .factory( + 'ExpressionInterpolationService', + downgradeInjectable(ExpressionInterpolationService) + ); diff --git a/core/templates/expressions/expression-parser.service.spec.ts b/core/templates/expressions/expression-parser.service.spec.ts index 454c7afffb8c..7877f10c4f38 100644 --- a/core/templates/expressions/expression-parser.service.spec.ts +++ b/core/templates/expressions/expression-parser.service.spec.ts @@ -16,7 +16,7 @@ * @fileoverview Unit tests for Expression Parser Service. */ -import { ExpressionParserService } from 'expressions/expression-parser.service'; +import {ExpressionParserService} from 'expressions/expression-parser.service'; describe('Expression parser service', () => { let eps: ExpressionParserService; @@ -32,49 +32,41 @@ describe('Expression parser service', () => { [0.001, '1e-3'], [0.35, '.35'], ['abc', '"abc"'], - ['a\'b\'c', '"a\'b\'c"'], + ["a'b'c", '"a\'b\'c"'], [null, 'null'], [true, 'true'], [false, 'false'], - [['#', 'abc'], - 'abc'], - [['#', 'あいうえお'], - 'あいうえお'], - [['abc'], - 'abc()'], - [['abc', 1], - 'abc(1)'], - [['abc', 1, 2], - 'abc(1, 2)'], - [[[['abc', 1, 2]], 3], - 'abc(1, 2)()(3)'], + [['#', 'abc'], 'abc'], + [['#', 'あいうえお'], 'あいうえお'], + [['abc'], 'abc()'], + [['abc', 1], 'abc(1)'], + [['abc', 1, 2], 'abc(1, 2)'], + [[[['abc', 1, 2]], 3], 'abc(1, 2)()(3)'], - [['+', 10], - '+10'], - [['-', ['#', 'abc']], - '-abc'], + [['+', 10], '+10'], + [['-', ['#', 'abc']], '-abc'], [['-', 0.35], '-.35'], [['+', 1, 2], '1 + 2'], // There is a double width space after '+'. [['+', 1, 2], '\t1 + 2 '], - [['*', ['/', 3, 4], 5], - '3 / 4 * 5'], - [['-', ['+', 2, ['*', ['/', 3, 4], 5]], 6], - '2 + 3 / 4 * 5 - 6'], + [['*', ['/', 3, 4], 5], '3 / 4 * 5'], + [['-', ['+', 2, ['*', ['/', 3, 4], 5]], 6], '2 + 3 / 4 * 5 - 6'], - [['||', ['&&', ['<', 2, 3], ['==', 4, 6]], true], - '2 < 3 && 4 == 6 || true'], + [ + ['||', ['&&', ['<', 2, 3], ['==', 4, 6]], true], + '2 < 3 && 4 == 6 || true', + ], // Expected to produce parser error. [undefined, 'a1a-'], [undefined, '0.3.4'], [undefined, 'abc()('], [undefined, '()'], - [undefined, '*100'] - ].forEach((test) => { + [undefined, '*100'], + ].forEach(test => { // 'expected' should be either a JavaScript primitive value that would be // the result of evaluating 'expression', or undefined (which means // that the parser is expected to fail). diff --git a/core/templates/expressions/expression-parser.service.ts b/core/templates/expressions/expression-parser.service.ts index 984da3e65ec3..91f59069341f 100644 --- a/core/templates/expressions/expression-parser.service.ts +++ b/core/templates/expressions/expression-parser.service.ts @@ -17,19 +17,19 @@ * oppia/core/templates/expressions/README.txt for further details. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; import parser from 'expressions/parser'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ExpressionParserService { parse = parser.parse; SyntaxError = parser.SyntaxError; } -angular.module('oppia').factory( - 'ServiceName', downgradeInjectable(ExpressionParserService) -); +angular + .module('oppia') + .factory('ServiceName', downgradeInjectable(ExpressionParserService)); diff --git a/core/templates/expressions/expression-syntax-tree.service.spec.ts b/core/templates/expressions/expression-syntax-tree.service.spec.ts index dc0c846f5e34..efe1523f18d7 100644 --- a/core/templates/expressions/expression-syntax-tree.service.spec.ts +++ b/core/templates/expressions/expression-syntax-tree.service.spec.ts @@ -15,14 +15,14 @@ /** * @fileoverview Unit tests for expression-syntax-tree.service.ts */ -import { TestBed } from '@angular/core/testing'; +import {TestBed} from '@angular/core/testing'; import { ExpressionSyntaxTreeService, ExpressionError, ExprUndefinedVarError, ExprWrongArgTypeError, - ExprWrongNumArgsError + ExprWrongNumArgsError, } from 'expressions/expression-syntax-tree.service'; describe('Expression syntax tree service', () => { @@ -34,17 +34,18 @@ describe('Expression syntax tree service', () => { }); it('should throw if environment is not found', () => { - expect(() => expressionSyntaxTreeService.lookupEnvs('test', [{}])) - .toThrowMatching( - // Jasmine has no built-in matcher for classes derived from Error. - e => e.toString() === 'ExprUndefinedVarError: test not found in [{}]' - ); + expect(() => + expressionSyntaxTreeService.lookupEnvs('test', [{}]) + ).toThrowMatching( + // Jasmine has no built-in matcher for classes derived from Error. + e => e.toString() === 'ExprUndefinedVarError: test not found in [{}]' + ); }); it('should return the correct environment if exists', () => { const expected = 'bar'; const actual = expressionSyntaxTreeService.lookupEnvs('foo', [ - {foo: 'bar'} + {foo: 'bar'}, ]) as string; expect(expected).toBe(actual); diff --git a/core/templates/expressions/expression-syntax-tree.service.ts b/core/templates/expressions/expression-syntax-tree.service.ts index 1ec8b3c6c97f..a87b744f023f 100644 --- a/core/templates/expressions/expression-syntax-tree.service.ts +++ b/core/templates/expressions/expression-syntax-tree.service.ts @@ -16,10 +16,10 @@ * @fileoverview Expression syntax tree service. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { ExpressionParserService } from 'expressions/expression-parser.service'; +import {ExpressionParserService} from 'expressions/expression-parser.service'; export type Expr = string | number | boolean; @@ -48,33 +48,43 @@ export class ExpressionError extends Error { } export class ExprUndefinedVarError extends ExpressionError { - constructor(public varname: string, public envs: EnvDict[]) { + constructor( + public varname: string, + public envs: EnvDict[] + ) { super(varname + ' not found in ' + angular.toJson(envs)); } } export class ExprWrongNumArgsError extends ExpressionError { constructor( - public args: (number|string)[], - public expectedMin: number, public expectedMax: number) { + public args: (number | string)[], + public expectedMin: number, + public expectedMax: number + ) { super( - '{' + args + '} not in range [' + expectedMin + ', ' + expectedMax + ']'); + '{' + args + '} not in range [' + expectedMin + ', ' + expectedMax + ']' + ); } } export class ExprWrongArgTypeError extends ExpressionError { constructor( - public arg: number|string, - public actualType: string, public expectedType: string) { + public arg: number | string, + public actualType: string, + public expectedType: string + ) { super( - ( - arg !== null ? - (arg + ' has type ' + actualType + ' which') : ('Type ' + actualType)) + - ' does not match expected type ' + expectedType); + (arg !== null + ? arg + ' has type ' + actualType + ' which' + : 'Type ' + actualType) + + ' does not match expected type ' + + expectedType + ); } } -@Injectable({ providedIn: 'root' }) +@Injectable({providedIn: 'root'}) export class ExpressionSyntaxTreeService { constructor(private expressionParserService: ExpressionParserService) {} @@ -89,8 +99,10 @@ export class ExpressionSyntaxTreeService { } public applyFunctionToParseTree( - parsed: Expr | Expr[], envs: EnvDict[], - func: (parsed: Expr | Expr[], envs: EnvDict[]) => Expr): Expr { + parsed: Expr | Expr[], + envs: EnvDict[], + func: (parsed: Expr | Expr[], envs: EnvDict[]) => Expr + ): Expr { return func(parsed, envs.concat(this.system)); } @@ -123,24 +135,29 @@ export class ExpressionSyntaxTreeService { // an error if not. If optional expectedMax is specified, it verifies the // number of args is in the inclusive range: [expectedMin, expectedMax]. private verifyNumArgs( - args: string[], - expectedMin: number, expectedMax: number = expectedMin): void { + args: string[], + expectedMin: number, + expectedMax: number = expectedMin + ): void { if (args.length < expectedMin || args.length > expectedMax) { throw new ExprWrongNumArgsError(args, expectedMin, expectedMax); } } // Coerces the argument to a Number, and throws an error if the result is NaN. - private coerceToNumber(originalValue: string|number): number { + private coerceToNumber(originalValue: string | number): number { const coercedValue = +originalValue; if (isNaN(coercedValue)) { throw new ExprWrongArgTypeError( - originalValue, typeof originalValue, 'Number'); + originalValue, + typeof originalValue, + 'Number' + ); } return coercedValue; } - private coerceAllArgsToNumber(args: (number|string)[]): number[] { + private coerceAllArgsToNumber(args: (number | string)[]): number[] { return args.map(this.coerceToNumber); } @@ -158,20 +175,20 @@ export class ExpressionSyntaxTreeService { eval: (args: string[]): number => { this.verifyNumArgs(args, 1, 2); const numericArgs = this.coerceAllArgsToNumber(args); - return numericArgs.length === 1 ? - numericArgs[0] : - numericArgs[0] + numericArgs[1]; - } + return numericArgs.length === 1 + ? numericArgs[0] + : numericArgs[0] + numericArgs[1]; + }, }, '-': { eval: (args: string[]): number => { this.verifyNumArgs(args, 1, 2); const numericArgs = this.coerceAllArgsToNumber(args); - return numericArgs.length === 1 ? - -numericArgs[0] : - numericArgs[0] - numericArgs[1]; - } + return numericArgs.length === 1 + ? -numericArgs[0] + : numericArgs[0] - numericArgs[1]; + }, }, '*': { @@ -179,7 +196,7 @@ export class ExpressionSyntaxTreeService { this.verifyNumArgs(args, 2); const numericArgs = this.coerceAllArgsToNumber(args); return numericArgs[0] * numericArgs[1]; - } + }, }, '/': { @@ -187,7 +204,7 @@ export class ExpressionSyntaxTreeService { this.verifyNumArgs(args, 2); const numericArgs = this.coerceAllArgsToNumber(args); return numericArgs[0] / numericArgs[1]; - } + }, }, '%': { @@ -195,7 +212,7 @@ export class ExpressionSyntaxTreeService { this.verifyNumArgs(args, 2); const numericArgs = this.coerceAllArgsToNumber(args); return numericArgs[0] % numericArgs[1]; - } + }, }, '<=': { @@ -203,7 +220,7 @@ export class ExpressionSyntaxTreeService { this.verifyNumArgs(args, 2); const numericArgs = this.coerceAllArgsToNumber(args); return numericArgs[0] <= numericArgs[1]; - } + }, }, '>=': { @@ -211,7 +228,7 @@ export class ExpressionSyntaxTreeService { this.verifyNumArgs(args, 2); const numericArgs = this.coerceAllArgsToNumber(args); return numericArgs[0] >= numericArgs[1]; - } + }, }, '<': { @@ -219,7 +236,7 @@ export class ExpressionSyntaxTreeService { this.verifyNumArgs(args, 2); const numericArgs = this.coerceAllArgsToNumber(args); return numericArgs[0] < numericArgs[1]; - } + }, }, '>': { @@ -227,70 +244,74 @@ export class ExpressionSyntaxTreeService { this.verifyNumArgs(args, 2); const numericArgs = this.coerceAllArgsToNumber(args); return numericArgs[0] > numericArgs[1]; - } + }, }, '!': { eval: (args: string[]): boolean => { this.verifyNumArgs(args, 1); return !args[0]; - } + }, }, '==': { eval: (args: string[]): boolean => { this.verifyNumArgs(args, 2); return args[0] === args[1]; - } + }, }, '!=': { eval: (args: string[]): boolean => { this.verifyNumArgs(args, 2); return args[0] !== args[1]; - } + }, }, '&&': { eval: (args: string[]): boolean => { this.verifyNumArgs(args, 2); return Boolean(args[0] && args[1]); - } + }, }, '||': { eval: (args: string[]): boolean => { this.verifyNumArgs(args, 2); return Boolean(args[0] || args[1]); - } + }, }, // NOTE TO DEVELOPERS: Removing the quotation marks from the following keys // causes issues with minification when running the deployment scripts. - 'if': { // eslint-disable-line quote-props + if: { + // eslint-disable-line quote-props eval: (args: string[]): string => { this.verifyNumArgs(args, 3); return args[0] ? args[1] : args[2]; - } + }, }, - 'floor': { // eslint-disable-line quote-props + floor: { + // eslint-disable-line quote-props eval: (args: string[]): number => { this.verifyNumArgs(args, 1); const numericArgs = this.coerceAllArgsToNumber(args); return Math.floor(numericArgs[0]); - } + }, }, - 'pow': { // eslint-disable-line quote-props + pow: { + // eslint-disable-line quote-props eval: (args: string[]): number => { this.verifyNumArgs(args, 2); const numericArgs = this.coerceAllArgsToNumber(args); return Math.pow(numericArgs[0], numericArgs[1]); - } + }, }, - 'log': { // eslint-disable-line quote-props + log: { + // eslint-disable-line quote-props eval: (args: string[]): number => { this.verifyNumArgs(args, 2); const numericArgs = this.coerceAllArgsToNumber(args); @@ -298,19 +319,23 @@ export class ExpressionSyntaxTreeService { // We round answers to 9 decimal places, so that we don't run into // issues like log(9, 3) = 2.0000000000004. return Math.round(preciseAns * Math.pow(10, 9)) / Math.pow(10, 9); - } + }, }, - 'abs': { // eslint-disable-line quote-props + abs: { + // eslint-disable-line quote-props eval: (args: string[]): number => { this.verifyNumArgs(args, 1); const numericArgs = this.coerceAllArgsToNumber(args); return Math.abs(numericArgs[0]); - } - } + }, + }, }; } -angular.module('oppia').factory( - 'ExpressionSyntaxTreeService', - downgradeInjectable(ExpressionSyntaxTreeService)); +angular + .module('oppia') + .factory( + 'ExpressionSyntaxTreeService', + downgradeInjectable(ExpressionSyntaxTreeService) + ); diff --git a/core/templates/filters/convert-html-to-unicode.filter.spec.ts b/core/templates/filters/convert-html-to-unicode.filter.spec.ts index 7d4d0ec11fe0..215e34ffb950 100644 --- a/core/templates/filters/convert-html-to-unicode.filter.spec.ts +++ b/core/templates/filters/convert-html-to-unicode.filter.spec.ts @@ -18,22 +18,24 @@ // TODO(#7222): Remove the following block of unnnecessary imports once // the code corresponding to the spec is upgraded to Angular 8. -import { UpgradedServices } from 'services/UpgradedServices'; +import {UpgradedServices} from 'services/UpgradedServices'; // ^^^ This block is to be removed. -require( - 'filters/convert-html-to-unicode.filter.ts'); +require('filters/convert-html-to-unicode.filter.ts'); -describe('HTML to text', function() { +describe('HTML to text', function () { beforeEach(angular.mock.module('oppia')); - beforeEach(angular.mock.module( - 'oppia', - function($provide: { value: (arg0: string, arg1: string) => void }) { - var ugs = new UpgradedServices(); - for (let [key, value] of Object.entries(ugs.getUpgradedServices())) { - $provide.value(key, value as string); + beforeEach( + angular.mock.module( + 'oppia', + function ($provide: {value: (arg0: string, arg1: string) => void}) { + var ugs = new UpgradedServices(); + for (let [key, value] of Object.entries(ugs.getUpgradedServices())) { + $provide.value(key, value as string); + } } - })); + ) + ); var htmlUnicodeHtmlPairings = [ ['abc', 'abc', 'abc'], @@ -42,14 +44,15 @@ describe('HTML to text', function() { ['
a', 'a', 'a'], ['
a', 'a', 'a'], ['

a', 'a', 'a'], - ['abc a', 'abc a', 'abc a'] + ['abc a', 'abc a', 'abc a'], ]; - it('should convert HTML to and from raw text correctly', angular.mock.inject( - function($filter) { - htmlUnicodeHtmlPairings.forEach(function(pairing) { + it( + 'should convert HTML to and from raw text correctly', + angular.mock.inject(function ($filter) { + htmlUnicodeHtmlPairings.forEach(function (pairing) { expect($filter('convertHtmlToUnicode')(pairing[0])).toEqual(pairing[1]); }); - } - )); + }) + ); }); diff --git a/core/templates/filters/convert-html-to-unicode.filter.ts b/core/templates/filters/convert-html-to-unicode.filter.ts index a9bbc63c793f..019f8dcd90a4 100644 --- a/core/templates/filters/convert-html-to-unicode.filter.ts +++ b/core/templates/filters/convert-html-to-unicode.filter.ts @@ -20,11 +20,13 @@ export const convertHtmlToUnicode = (html: string): string => { const domparser = new DOMParser(); const dom = domparser.parseFromString(html, 'text/html'); - return (dom.querySelector('body')?.innerText) as string; + return dom.querySelector('body')?.innerText as string; }; -angular.module('oppia').filter('convertHtmlToUnicode', [function() { - return function(html: string) { - return convertHtmlToUnicode(html); - }; -}]); +angular.module('oppia').filter('convertHtmlToUnicode', [ + function () { + return function (html: string) { + return convertHtmlToUnicode(html); + }; + }, +]); diff --git a/core/templates/filters/convert-unicode-to-html.pipe.spec.ts b/core/templates/filters/convert-unicode-to-html.pipe.spec.ts index 04446811c75a..6d609fe8d795 100644 --- a/core/templates/filters/convert-unicode-to-html.pipe.spec.ts +++ b/core/templates/filters/convert-unicode-to-html.pipe.spec.ts @@ -16,16 +16,15 @@ * @fileoverview Tests for the convert unicode to html filter. */ +import {TestBed} from '@angular/core/testing'; +import {ConvertUnicodeToHtml} from 'filters/convert-unicode-to-html.pipe'; -import { TestBed } from '@angular/core/testing'; -import { ConvertUnicodeToHtml } from 'filters/convert-unicode-to-html.pipe'; - -describe('HTML to text', function() { +describe('HTML to text', function () { let pipe: ConvertUnicodeToHtml; beforeEach(() => { TestBed.configureTestingModule({ - providers: [ConvertUnicodeToHtml] + providers: [ConvertUnicodeToHtml], }); pipe = TestBed.inject(ConvertUnicodeToHtml); @@ -38,11 +37,11 @@ describe('HTML to text', function() { ['
a', 'a', 'a'], ['
a', 'a', 'a'], ['

a', 'a', 'a'], - ['abc a', 'abc a', 'abc a'] + ['abc a', 'abc a', 'abc a'], ]; it('should convert HTML from raw text correctly', () => { - htmlUnicodeHtmlPairings.forEach((pairing) => { + htmlUnicodeHtmlPairings.forEach(pairing => { expect(pipe.transform(pairing[1])).toEqual(pairing[2]); }); }); diff --git a/core/templates/filters/convert-unicode-to-html.pipe.ts b/core/templates/filters/convert-unicode-to-html.pipe.ts index ab48fb61cec3..dc4f33f6b4a3 100644 --- a/core/templates/filters/convert-unicode-to-html.pipe.ts +++ b/core/templates/filters/convert-unicode-to-html.pipe.ts @@ -16,9 +16,9 @@ * @fileoverview Converts unicode to HTML Pipe. */ -import { Pipe, PipeTransform, SecurityContext } from '@angular/core'; -import { DomSanitizer } from '@angular/platform-browser'; -import { HtmlEscaperService } from 'services/html-escaper.service'; +import {Pipe, PipeTransform, SecurityContext} from '@angular/core'; +import {DomSanitizer} from '@angular/platform-browser'; +import {HtmlEscaperService} from 'services/html-escaper.service'; @Pipe({name: 'convertUnicodeToHtml'}) export class ConvertUnicodeToHtml implements PipeTransform { @@ -30,6 +30,7 @@ export class ConvertUnicodeToHtml implements PipeTransform { transform(text: string): string | null { return this._sanitizer.sanitize( SecurityContext.HTML, - this.htmlEscaperService.unescapedStrToEscapedStr(text)); + this.htmlEscaperService.unescapedStrToEscapedStr(text) + ); } } diff --git a/core/templates/filters/format-rte-preview.pipe.spec.ts b/core/templates/filters/format-rte-preview.pipe.spec.ts index 46a614318e47..78df7bf5d5cd 100644 --- a/core/templates/filters/format-rte-preview.pipe.spec.ts +++ b/core/templates/filters/format-rte-preview.pipe.spec.ts @@ -16,8 +16,8 @@ * @fileoverview Tests for FormatRtePreview pipe for Oppia. */ -import { TestBed } from '@angular/core/testing'; -import { FormatRtePreviewPipe } from 'filters/format-rte-preview.pipe'; +import {TestBed} from '@angular/core/testing'; +import {FormatRtePreviewPipe} from 'filters/format-rte-preview.pipe'; describe('Testing CamelCaseToHyphensPipe', () => { let pipe: FormatRtePreviewPipe; @@ -35,30 +35,34 @@ describe('Testing CamelCaseToHyphensPipe', () => { expect( pipe.transform( '

' + - 'Text input

' - )).toEqual('[Math] Text input'); + 'Text input

' + ) + ).toEqual('[Math] Text input'); expect( pipe.transform( '

' + - 'Text input' + - 'Text input 2

' - )).toEqual('[Math] Text input [Collapsible] Text input 2'); + 'Text input' + + 'Text input 2

' + ) + ).toEqual('[Math] Text input [Collapsible] Text input 2'); expect( pipe.transform( '

' + - 'Text input' + - 'Text input 2' + - '

' - )).toEqual('[Math] Text input [Collapsible] Text input 2'); + 'Text input' + + 'Text input 2' + + '

' + ) + ).toEqual('[Math] Text input [Collapsible] Text input 2'); expect( pipe.transform( '' + - 'Text input' + - 'Text input 2' + - '' + - ' Text Input 3 ' - )).toEqual( - '[Math] Text input [Collapsible] Text input 2 [Image] ' + - 'Text Input 3'); + 'Text input' + + 'Text input 2' + + '' + + ' Text Input 3 ' + ) + ).toEqual( + '[Math] Text input [Collapsible] Text input 2 [Image] ' + 'Text Input 3' + ); }); }); diff --git a/core/templates/filters/format-rte-preview.pipe.ts b/core/templates/filters/format-rte-preview.pipe.ts index 90ba37e5e2d5..e55d1087b63a 100644 --- a/core/templates/filters/format-rte-preview.pipe.ts +++ b/core/templates/filters/format-rte-preview.pipe.ts @@ -16,11 +16,11 @@ * @fileoverview FormatRtePreview pipe for Oppia. */ -import { Injectable } from '@angular/core'; -import { CapitalizePipe } from 'filters/string-utility-filters/capitalize.pipe'; +import {Injectable} from '@angular/core'; +import {CapitalizePipe} from 'filters/string-utility-filters/capitalize.pipe'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) /* The following filter replaces each RTE element occurrence in the input html by its corresponding name in square brackets and returns a string @@ -32,14 +32,15 @@ export class FormatRtePreviewPipe { constructor(private capitalizePipe: CapitalizePipe) {} transform(html: string): string { - html = html.replace(/ /ig, ' '); - html = html.replace(/"/ig, ''); + html = html.replace(/ /gi, ' '); + html = html.replace(/"/gi, ''); // Replace all html tags other than ones to ''. html = html.replace(/<(?!oppia-noninteractive\s*?)[^>]+>/g, ''); let formattedOutput = html.replace(/(<([^>]+)>)/g, rteTag => { - let replaceString = ( - this.capitalizePipe.transform(rteTag.split('-')[2].split(' ')[0])); + let replaceString = this.capitalizePipe.transform( + rteTag.split('-')[2].split(' ')[0] + ); if (replaceString[replaceString.length - 1] === '>') { replaceString = replaceString.slice(0, -1); diff --git a/core/templates/filters/format-timer.pipe.spec.ts b/core/templates/filters/format-timer.pipe.spec.ts index 5b32c4ca1e55..6d62971c3db9 100644 --- a/core/templates/filters/format-timer.pipe.spec.ts +++ b/core/templates/filters/format-timer.pipe.spec.ts @@ -16,7 +16,7 @@ * @fileoverview Tests for FormatTime pipe for Oppia. */ -import { FormatTimePipe } from './format-timer.pipe'; +import {FormatTimePipe} from './format-timer.pipe'; describe('Testing FormatTimePipe', () => { let pipe: FormatTimePipe; @@ -28,7 +28,7 @@ describe('Testing FormatTimePipe', () => { expect(pipe).not.toEqual(null); }); - it('should correctly format time', () =>{ + it('should correctly format time', () => { expect(pipe.transform(200)).toEqual('03:20'); expect(pipe.transform(474)).toEqual('07:54'); expect(pipe.transform(556)).toEqual('09:16'); diff --git a/core/templates/filters/format-timer.pipe.ts b/core/templates/filters/format-timer.pipe.ts index ad6a0759e7d2..5a3023b0db1f 100644 --- a/core/templates/filters/format-timer.pipe.ts +++ b/core/templates/filters/format-timer.pipe.ts @@ -16,20 +16,20 @@ * @fileoverview FormatTime filter for Oppia. */ -import { Injectable, Pipe, PipeTransform} from '@angular/core'; +import {Injectable, Pipe, PipeTransform} from '@angular/core'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) @Pipe({name: 'formatTime'}) export class FormatTimePipe implements PipeTransform { transform(input: number): string { - let formatNum = function(n: number) { + let formatNum = function (n: number) { return (n < 10 ? '0' : '') + n; }; let seconds = input % 60; let minutes = Math.floor(input / 60); - return (formatNum(minutes) + ':' + formatNum(seconds)); + return formatNum(minutes) + ':' + formatNum(seconds); } } diff --git a/core/templates/filters/limit-to.pipe.spec.ts b/core/templates/filters/limit-to.pipe.spec.ts index f264acf64af3..13ef99c8412f 100644 --- a/core/templates/filters/limit-to.pipe.spec.ts +++ b/core/templates/filters/limit-to.pipe.spec.ts @@ -16,7 +16,7 @@ * @fileoverview LimitTo filter for Oppia. */ -import { LimitToPipe } from './limit-to.pipe'; +import {LimitToPipe} from './limit-to.pipe'; describe('LimitTo Pipe', () => { const limitToPipe = new LimitToPipe(); @@ -24,8 +24,13 @@ describe('LimitTo Pipe', () => { it('should reduce number elements of array to given limit', () => { let list: string[] = ['a', 'b', 'c', 'd', 'e', 'f', 'g']; let limit: number = 5; - expect(limitToPipe.transform(list, limit)) - .toEqual(['a', 'b', 'c', 'd', 'e']); + expect(limitToPipe.transform(list, limit)).toEqual([ + 'a', + 'b', + 'c', + 'd', + 'e', + ]); let list2: number[] = [1, 2, 3]; expect(limitToPipe.transform(list2, limit)).toEqual(list2); }); diff --git a/core/templates/filters/limit-to.pipe.ts b/core/templates/filters/limit-to.pipe.ts index 71694b1a2149..fe6b5f78fbde 100644 --- a/core/templates/filters/limit-to.pipe.ts +++ b/core/templates/filters/limit-to.pipe.ts @@ -16,10 +16,10 @@ * @fileoverview A pipe to limit the number of elements in an array. */ -import { Pipe, PipeTransform } from '@angular/core'; +import {Pipe, PipeTransform} from '@angular/core'; @Pipe({ - name: 'limitTo' + name: 'limitTo', }) export class LimitToPipe implements PipeTransform { transform(value: T[], limit: number): T[] { diff --git a/core/templates/filters/parameterize-rule-description.pipe.spec.ts b/core/templates/filters/parameterize-rule-description.pipe.spec.ts index 1f28573f4455..5055a186f62d 100644 --- a/core/templates/filters/parameterize-rule-description.pipe.spec.ts +++ b/core/templates/filters/parameterize-rule-description.pipe.spec.ts @@ -16,10 +16,10 @@ * @fileoverview ParameterizeRuleDescription Pipe for Oppia. */ -import { ParameterizeRuleDescriptionPipe } from './parameterize-rule-description.pipe'; -import { fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { Rule } from 'domain/exploration/rule.model'; -import { TranslatableSetOfNormalizedString } from 'interactions/rule-input-defs'; +import {ParameterizeRuleDescriptionPipe} from './parameterize-rule-description.pipe'; +import {fakeAsync, TestBed, tick} from '@angular/core/testing'; +import {Rule} from 'domain/exploration/rule.model'; +import {TranslatableSetOfNormalizedString} from 'interactions/rule-input-defs'; describe('ParameterizeRuleDescriptionPipe', () => { let parameterizeRuleDescriptionPipe: ParameterizeRuleDescriptionPipe; @@ -27,365 +27,470 @@ describe('ParameterizeRuleDescriptionPipe', () => { beforeEach(() => { TestBed.configureTestingModule({ - providers: [ - ParameterizeRuleDescriptionPipe - ] + providers: [ParameterizeRuleDescriptionPipe], }); - parameterizeRuleDescriptionPipe = ( - TestBed.inject(ParameterizeRuleDescriptionPipe)); + parameterizeRuleDescriptionPipe = TestBed.inject( + ParameterizeRuleDescriptionPipe + ); - rule = Rule.createNew('Equals', { - Equals: { - x: '2' + rule = Rule.createNew( + 'Equals', + { + Equals: { + x: '2', + }, + }, + { + Equals: 'ruleSome', } - }, { - Equals: 'ruleSome' - }); + ); }); it('should send black string when rule is null', () => { let result = parameterizeRuleDescriptionPipe.transform( - null, 'interactionId', []); + null, + 'interactionId', + [] + ); expect(result).toEqual(''); }); it('should send black string when rule is null', () => { let result = parameterizeRuleDescriptionPipe.transform( - rule, 'interactionId', []); + rule, + 'interactionId', + [] + ); expect(result).toEqual(''); }); it('should send black string when rule is null', () => { - let testRule = Rule.createNew('none', { - rule: { - x: '2' + let testRule = Rule.createNew( + 'none', + { + rule: { + x: '2', + }, + }, + { + rule: 'ruleSome', } - }, { - rule: 'ruleSome' - }); + ); let result = parameterizeRuleDescriptionPipe.transform( - testRule, 'Continue', []); + testRule, + 'Continue', + [] + ); expect(result).toEqual(''); }); it('should send black string when rule is null', () => { let result = parameterizeRuleDescriptionPipe.transform( - rule, 'NumericInput', [{ - val: 5, - label: 'string' - }]); + rule, + 'NumericInput', + [ + { + val: 5, + label: 'string', + }, + ] + ); expect(result).toEqual('is equal to [INVALID]'); }); it('should correctly parameterize for TextInput', fakeAsync(() => { - var rules = Rule.createNew('Equals', { - x: ({ - normalizedStrSet: ['first', 'secound'], - contentId: null - } as TranslatableSetOfNormalizedString) - }, { - x: 'TextInput' - }); + var rules = Rule.createNew( + 'Equals', + { + x: { + normalizedStrSet: ['first', 'secound'], + contentId: null, + } as TranslatableSetOfNormalizedString, + }, + { + x: 'TextInput', + } + ); var interactionIdMultipleChoice = 'TextInput'; tick(); let result = parameterizeRuleDescriptionPipe.transform( rules, interactionIdMultipleChoice, - null); + null + ); tick(); expect(result).toEqual( 'is equal to at least one of' + - ' [first, secound], without taking case into account'); + ' [first, secound], without taking case into account' + ); })); it('should correctly parameterize for Graph', () => { - var rules = Rule.createNew('IsIsomorphicTo', { - inputs: { - x: 0 + var rules = Rule.createNew( + 'IsIsomorphicTo', + { + inputs: { + x: 0, + }, + }, + { + inputs: 'ruleSome', } - }, { - inputs: 'ruleSome' - }); + ); let result = parameterizeRuleDescriptionPipe.transform( rules, 'GraphInput', - null); + null + ); expect(result).toEqual( - 'is isomorphic to [reference graph], including matching labels'); + 'is isomorphic to [reference graph], including matching labels' + ); }); it('should correctly parameterize for Fraction', () => { var rules = Rule.createNew( - 'IsEquivalentToAndInSimplestForm', { + 'IsEquivalentToAndInSimplestForm', + { f: { isNegative: false, wholeNumber: 5, numerator: 2, denominator: 3, - } - }, { - f: 'ruleSome' - }); + }, + }, + { + f: 'ruleSome', + } + ); let result = parameterizeRuleDescriptionPipe.transform( rules, 'FractionInput', - null); + null + ); - expect(result).toEqual( - 'is equivalent to 5 2/3 and in simplest form'); + expect(result).toEqual('is equivalent to 5 2/3 and in simplest form'); }); it('should correctly parameterize for NumberWithUnits', () => { - var rules = Rule.createNew('IsEqualTo', { - f: { - type: 'string', - real: 5, - fraction: { - isNegative: false, - wholeNumber: 2, - numerator: 3, - denominator: 5 + var rules = Rule.createNew( + 'IsEqualTo', + { + f: { + type: 'string', + real: 5, + fraction: { + isNegative: false, + wholeNumber: 2, + numerator: 3, + denominator: 5, + }, + units: [], }, - units: [] + }, + { + f: 'ruleSome', } - }, { - f: 'ruleSome' - }); + ); var interactionIdMultipleChoice = 'NumberWithUnits'; let result = parameterizeRuleDescriptionPipe.transform( rules, interactionIdMultipleChoice, - null); + null + ); - expect(result).toEqual( - 'is equal to '); + expect(result).toEqual('is equal to '); }); it('should correctly parameterize for MusicPhrase', () => { - var rules = Rule.createNew('IsEqualToExceptFor', { - x: { - 0: { - readableNoteName: '0' - }, - 1: { - readableNoteName: '1' - }, - 2: { - readableNoteName: '0' - }, - 3: { - readableNoteName: '1' + var rules = Rule.createNew( + 'IsEqualToExceptFor', + { + x: { + 0: { + readableNoteName: '0', + }, + 1: { + readableNoteName: '1', + }, + 2: { + readableNoteName: '0', + }, + 3: { + readableNoteName: '1', + }, }, + k: -5, }, - k: -5 - }, { - x: 'ruleSome', - k: '' - }); + { + x: 'ruleSome', + k: '', + } + ); let result = parameterizeRuleDescriptionPipe.transform( rules, 'MusicNotesInput', - null); + null + ); - expect(result).toEqual( - 'is equal to [0, 1, 0, 1] except for -5 notes'); + expect(result).toEqual('is equal to [0, 1, 0, 1] except for -5 notes'); }); it('should correctly parameterize for InteractiveMap', () => { - var rules = Rule.createNew('Within', { - d: 10, - p: 9 - }, { - d: '10', - p: '9' - }); + var rules = Rule.createNew( + 'Within', + { + d: 10, + p: 9, + }, + { + d: '10', + p: '9', + } + ); let result = parameterizeRuleDescriptionPipe.transform( rules, 'InteractiveMap', - null); + null + ); - expect(result).toEqual( - 'is within 10 km of (0°S, 0°W)'); + expect(result).toEqual('is within 10 km of (0°S, 0°W)'); }); it('should correctly parameterize for SetInput', () => { - var rules = Rule.createNew('Equals', { - x: { - unicodeStrSet: ['first', 'secound'], - contentId: null + var rules = Rule.createNew( + 'Equals', + { + x: { + unicodeStrSet: ['first', 'secound'], + contentId: null, + }, + }, + { + x: 'ruleSome', } - }, { - x: 'ruleSome' - }); + ); let result = parameterizeRuleDescriptionPipe.transform( rules, 'SetInput', - null); + null + ); - expect(result).toEqual( - 'is equal to [first, secound]'); + expect(result).toEqual('is equal to [first, secound]'); }); it('should correctly parameterize for MathEquationInput', () => { - var rules = Rule.createNew('MatchesExactlyWith', { - x: '2 + 3', - y: 'rhs' - }, { - x: '2 + 3', - y: 'rhs' - }); + var rules = Rule.createNew( + 'MatchesExactlyWith', + { + x: '2 + 3', + y: 'rhs', + }, + { + x: '2 + 3', + y: 'rhs', + } + ); let result = parameterizeRuleDescriptionPipe.transform( rules, 'MathEquationInput', - null); + null + ); - expect(result).toEqual( - 'matches exactly with 2 + 3 on Right Hand Side'); + expect(result).toEqual('matches exactly with 2 + 3 on Right Hand Side'); }); it('should correctly parameterize for RatioExpressionInput', () => { - var rules = Rule.createNew('Equals', { - x: [2, 3] - }, { - x: '[2 , 3]' - }); + var rules = Rule.createNew( + 'Equals', + { + x: [2, 3], + }, + { + x: '[2 , 3]', + } + ); let result = parameterizeRuleDescriptionPipe.transform( rules, 'RatioExpressionInput', - null); + null + ); - expect(result).toEqual( - 'is equal to 2:3'); + expect(result).toEqual('is equal to 2:3'); }); - it('should correctly parameterize for SetOfTranslatableHtmlContentIds' + - ' with INVALID', () => { - var rules = Rule.createNew('Equals', { - x: ['1', '2', '3', '4'] - }, { - x: 'data' - }); + it( + 'should correctly parameterize for SetOfTranslatableHtmlContentIds' + + ' with INVALID', + () => { + var rules = Rule.createNew( + 'Equals', + { + x: ['1', '2', '3', '4'], + }, + { + x: 'data', + } + ); - let result = parameterizeRuleDescriptionPipe.transform( - rules, 'ItemSelectionInput', [{ - val: 1, - label: 'string' - }, - { - val: 5, - label: 'string' - }]); + let result = parameterizeRuleDescriptionPipe.transform( + rules, + 'ItemSelectionInput', + [ + { + val: 1, + label: 'string', + }, + { + val: 5, + label: 'string', + }, + ] + ); - expect(result).toEqual('is equal to [INVALID,INVALID,INVALID,INVALID]'); - }); + expect(result).toEqual('is equal to [INVALID,INVALID,INVALID,INVALID]'); + } + ); - it('should correctly parameterize for SetOfTranslatableHtmlContentIds', - () => { - var rules = Rule.createNew('Equals', { - x: ['1', '2'] - }, { - x: 'data' - }); + it('should correctly parameterize for SetOfTranslatableHtmlContentIds', () => { + var rules = Rule.createNew( + 'Equals', + { + x: ['1', '2'], + }, + { + x: 'data', + } + ); - let result = parameterizeRuleDescriptionPipe.transform( - rules, 'ItemSelectionInput', [{ + let result = parameterizeRuleDescriptionPipe.transform( + rules, + 'ItemSelectionInput', + [ + { val: '1', - label: 'one item' + label: 'one item', }, { val: '2', - label: 'two item' - }]); + label: 'two item', + }, + ] + ); - expect(result).toEqual('is equal to [one item,two item]'); - }); + expect(result).toEqual('is equal to [one item,two item]'); + }); - it('should correctly parameterize for ListOfSetsOfTranslatableHtmlContentIds', - () => { - var rules = Rule.createNew('IsEqualToOrdering', { - x: [ - ['1'], - ['2'], - ] - }, { - x: 'data' - }); + it('should correctly parameterize for ListOfSetsOfTranslatableHtmlContentIds', () => { + var rules = Rule.createNew( + 'IsEqualToOrdering', + { + x: [['1'], ['2']], + }, + { + x: 'data', + } + ); - let result = parameterizeRuleDescriptionPipe.transform( - rules, 'DragAndDropSortInput', [{ + let result = parameterizeRuleDescriptionPipe.transform( + rules, + 'DragAndDropSortInput', + [ + { val: '1', - label: 'one item' + label: 'one item', }, { val: '2', - label: 'two item' - }]); + label: 'two item', + }, + ] + ); - expect(result).toEqual('is equal to ordering [[one item],[two item]]'); - }); + expect(result).toEqual('is equal to ordering [[one item],[two item]]'); + }); - it('should correctly parameterize for' + - 'ListOfSetsOfTranslatableHtmlContentIds with INVALID', () => { - var rules = Rule.createNew('IsEqualToOrdering', { - x: [ - ['1'], - ['5'], - ] - }, { - x: 'data' - }); + it( + 'should correctly parameterize for' + + 'ListOfSetsOfTranslatableHtmlContentIds with INVALID', + () => { + var rules = Rule.createNew( + 'IsEqualToOrdering', + { + x: [['1'], ['5']], + }, + { + x: 'data', + } + ); - let result = parameterizeRuleDescriptionPipe.transform( - rules, 'DragAndDropSortInput', [{ - val: 1, - label: 'string' - }, - { - val: 5, - label: 'string' - }]); + let result = parameterizeRuleDescriptionPipe.transform( + rules, + 'DragAndDropSortInput', + [ + { + val: 1, + label: 'string', + }, + { + val: 5, + label: 'string', + }, + ] + ); - expect(result).toEqual( - 'is equal to ordering [[INVALID],[INVALID]]'); - }); + expect(result).toEqual('is equal to ordering [[INVALID],[INVALID]]'); + } + ); - it('should correctly parameterize for' + - 'DragAndDropPositiveInt with INVALID', () => { - var rules = Rule.createNew('HasElementXAtPositionY', { - x: 'TranslatableHtmlContentId', - y: '2' - }, { - x: 'data', - y: '3' - }); + it( + 'should correctly parameterize for' + 'DragAndDropPositiveInt with INVALID', + () => { + var rules = Rule.createNew( + 'HasElementXAtPositionY', + { + x: 'TranslatableHtmlContentId', + y: '2', + }, + { + x: 'data', + y: '3', + } + ); - let result = parameterizeRuleDescriptionPipe.transform( - rules, 'DragAndDropSortInput', [{ - val: 'none', - label: 'string' - }, - { - val: 'TranslatableHtmlContentId', - label: 'TranslatableHtmlContentId' - }]); + let result = parameterizeRuleDescriptionPipe.transform( + rules, + 'DragAndDropSortInput', + [ + { + val: 'none', + label: 'string', + }, + { + val: 'TranslatableHtmlContentId', + label: 'TranslatableHtmlContentId', + }, + ] + ); - expect(result).toEqual( - 'has element \'TranslatableHtmlContentId\' at position 2'); - }); + expect(result).toEqual( + "has element 'TranslatableHtmlContentId' at position 2" + ); + } + ); }); diff --git a/core/templates/filters/parameterize-rule-description.pipe.ts b/core/templates/filters/parameterize-rule-description.pipe.ts index 6a6f1bc5241b..faf1ce8e67b9 100644 --- a/core/templates/filters/parameterize-rule-description.pipe.ts +++ b/core/templates/filters/parameterize-rule-description.pipe.ts @@ -16,31 +16,41 @@ * @fileoverview ParameterizeRuleDescription Pipe for Oppia. */ -import { Pipe, PipeTransform } from '@angular/core'; -import { Fraction } from 'domain/objects/fraction.model'; -import { Ratio } from 'domain/objects/ratio.model'; -import { FormatRtePreviewPipe } from 'filters/format-rte-preview.pipe'; -import { NumberWithUnitsObjectFactory } from 'domain/objects/NumberWithUnitsObjectFactory'; -import { Rule } from 'domain/exploration/rule.model'; -import { FractionAnswer, MusicNotesAnswer, NumberWithUnitsAnswer, RatioInputAnswer } from 'interactions/answer-defs'; -import { TranslatableSetOfNormalizedString, TranslatableSetOfUnicodeString } from 'interactions/rule-input-defs'; -import { AnswerChoice } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { AppConstants } from 'app.constants'; +import {Pipe, PipeTransform} from '@angular/core'; +import {Fraction} from 'domain/objects/fraction.model'; +import {Ratio} from 'domain/objects/ratio.model'; +import {FormatRtePreviewPipe} from 'filters/format-rte-preview.pipe'; +import {NumberWithUnitsObjectFactory} from 'domain/objects/NumberWithUnitsObjectFactory'; +import {Rule} from 'domain/exploration/rule.model'; +import { + FractionAnswer, + MusicNotesAnswer, + NumberWithUnitsAnswer, + RatioInputAnswer, +} from 'interactions/answer-defs'; +import { + TranslatableSetOfNormalizedString, + TranslatableSetOfUnicodeString, +} from 'interactions/rule-input-defs'; +import {AnswerChoice} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {AppConstants} from 'app.constants'; import INTERACTION_SPECS from 'interactions/interaction_specs.json'; -import { InteractionSpecsKey } from 'pages/interaction-specs.constants'; +import {InteractionSpecsKey} from 'pages/interaction-specs.constants'; @Pipe({ - name: 'parameterizeRuleDescriptionPipe' + name: 'parameterizeRuleDescriptionPipe', }) export class ParameterizeRuleDescriptionPipe implements PipeTransform { constructor( - private formatRtePreviewPipe: FormatRtePreviewPipe, - private numberWithUnitsObjectFactory: NumberWithUnitsObjectFactory, - ) { } + private formatRtePreviewPipe: FormatRtePreviewPipe, + private numberWithUnitsObjectFactory: NumberWithUnitsObjectFactory + ) {} transform( - rule: Rule | null, interactionId: string | null, - choices: AnswerChoice[] | null): string { + rule: Rule | null, + interactionId: string | null, + choices: AnswerChoice[] | null + ): string { if (!rule || !interactionId) { return ''; } @@ -50,19 +60,22 @@ export class ParameterizeRuleDescriptionPipe implements PipeTransform { return ''; } - let ruleTypesToDescriptions = INTERACTION_SPECS[ - interactionId as InteractionSpecsKey].rule_descriptions; + let ruleTypesToDescriptions = + INTERACTION_SPECS[interactionId as InteractionSpecsKey].rule_descriptions; type RuleTypeToDescription = { [key in keyof typeof ruleTypesToDescriptions]: string; }; - let description: string = ruleTypesToDescriptions[ - rule.type as keyof RuleTypeToDescription]; + let description: string = + ruleTypesToDescriptions[rule.type as keyof RuleTypeToDescription]; if (!description) { console.error( - 'Cannot find description for rule ' + rule.type + - ' for interaction ' + interactionId); + 'Cannot find description for rule ' + + rule.type + + ' for interaction ' + + interactionId + ); return ''; } @@ -99,7 +112,8 @@ export class ParameterizeRuleDescriptionPipe implements PipeTransform { replacementText += 'INVALID'; } else { replacementText += this.formatRtePreviewPipe.transform( - choices[choiceIndex].label); + choices[choiceIndex].label + ); } if (i < key.length - 1) { replacementText += ','; @@ -119,7 +133,8 @@ export class ParameterizeRuleDescriptionPipe implements PipeTransform { replacementText += 'INVALID'; } else { replacementText += this.formatRtePreviewPipe.transform( - choices[choiceIndex].label); + choices[choiceIndex].label + ); } } replacementText += ']'; @@ -135,9 +150,10 @@ export class ParameterizeRuleDescriptionPipe implements PipeTransform { // TranslatableHtmlContentId. for (var i = 0; i < choices.length; i++) { if (choices[i].val === inputs[varName]) { - var filteredLabelText = - this.formatRtePreviewPipe.transform(choices[i].label); - replacementText = '\'' + filteredLabelText + '\''; + var filteredLabelText = this.formatRtePreviewPipe.transform( + choices[i].label + ); + replacementText = "'" + filteredLabelText + "'"; } } } @@ -158,79 +174,92 @@ export class ParameterizeRuleDescriptionPipe implements PipeTransform { let latitude = key[0] || 0.0; let longitude = key[1] || 0.0; replacementText = '('; - replacementText += ( - key[0] >= 0.0 ? - latitude.toFixed(2) + '°N' : - -latitude.toFixed(2) + '°S'); + replacementText += + key[0] >= 0.0 + ? latitude.toFixed(2) + '°N' + : -latitude.toFixed(2) + '°S'; replacementText += ', '; - replacementText += ( - key[1] >= 0.0 ? - longitude.toFixed(2) + '°E' : - -longitude.toFixed(2) + '°W'); + replacementText += + key[1] >= 0.0 + ? longitude.toFixed(2) + '°E' + : -longitude.toFixed(2) + '°W'; replacementText += ')'; } else if (varType === 'Graph') { replacementText = '[reference graph]'; } else if (varType === 'Fraction') { replacementText = Fraction.fromDict( - (inputs[varName]) as FractionAnswer).toString(); + inputs[varName] as FractionAnswer + ).toString(); } else if (varType === 'NumberWithUnits') { replacementText = this.numberWithUnitsObjectFactory - .fromDict((inputs[varName]) as NumberWithUnitsAnswer).toString(); + .fromDict(inputs[varName] as NumberWithUnitsAnswer) + .toString(); } else if (varType === 'TranslatableSetOfNormalizedString') { replacementText = '['; - for (var i = 0; i < ( - inputs[varName] as TranslatableSetOfNormalizedString) - .normalizedStrSet.length; i++) { + for ( + var i = 0; + i < + (inputs[varName] as TranslatableSetOfNormalizedString) + .normalizedStrSet.length; + i++ + ) { if (i !== 0) { replacementText += ', '; } replacementText += ( - (inputs[varName]) as TranslatableSetOfNormalizedString) - .normalizedStrSet[i]; + inputs[varName] as TranslatableSetOfNormalizedString + ).normalizedStrSet[i]; } replacementText += ']'; } else if (varType === 'TranslatableSetOfUnicodeString') { replacementText = '['; - for (var i = 0; i < ( - inputs[varName] as TranslatableSetOfUnicodeString) - .unicodeStrSet.length; i++) { + for ( + var i = 0; + i < + (inputs[varName] as TranslatableSetOfUnicodeString).unicodeStrSet + .length; + i++ + ) { if (i !== 0) { replacementText += ', '; } - replacementText += ( - inputs[varName] as TranslatableSetOfUnicodeString) + replacementText += (inputs[varName] as TranslatableSetOfUnicodeString) .unicodeStrSet[i]; } replacementText += ']'; } else if ( varType === 'Real' || - varType === 'NonnegativeInt' || - varType === 'Int' || - varType === 'PositiveInt' + varType === 'NonnegativeInt' || + varType === 'Int' || + varType === 'PositiveInt' ) { replacementText = inputs[varName] + ''; } else if ( varType === 'CodeString' || - varType === 'UnicodeString' || - varType === 'NormalizedString' || - varType === 'AlgebraicExpression' || - varType === 'MathEquation' || - varType === 'NumericExpression' + varType === 'UnicodeString' || + varType === 'NormalizedString' || + varType === 'AlgebraicExpression' || + varType === 'MathEquation' || + varType === 'NumericExpression' ) { replacementText = String(inputs[varName]); } else if (varType === 'PositionOfTerms') { - for (var i = 0; i < AppConstants.POSITION_OF_TERMS_MAPPING.length; - i++) { + for ( + var i = 0; + i < AppConstants.POSITION_OF_TERMS_MAPPING.length; + i++ + ) { if ( - AppConstants.POSITION_OF_TERMS_MAPPING[i].name === - inputs[varName]) { + AppConstants.POSITION_OF_TERMS_MAPPING[i].name === inputs[varName] + ) { replacementText = - AppConstants.POSITION_OF_TERMS_MAPPING[i].humanReadableName; + AppConstants.POSITION_OF_TERMS_MAPPING[i].humanReadableName; } } } else if (varType === 'RatioExpression') { replacementText = Ratio.fromList( - (inputs[varName]) as RatioInputAnswer).toAnswerString(); + inputs[varName] as RatioInputAnswer + ).toAnswerString(); } // Replaces all occurances of $ with $$. diff --git a/core/templates/filters/remove-duplicates-in-array.pipe.spec.ts b/core/templates/filters/remove-duplicates-in-array.pipe.spec.ts index 8e71f50c88ce..bcffb6b139fc 100644 --- a/core/templates/filters/remove-duplicates-in-array.pipe.spec.ts +++ b/core/templates/filters/remove-duplicates-in-array.pipe.spec.ts @@ -16,7 +16,7 @@ * @fileoverview Tests for RemoveDuplicatesInArray pipe for Oppia. */ -import { RemoveDuplicatesInArrayPipe } from './remove-duplicates-in-array.pipe'; +import {RemoveDuplicatesInArrayPipe} from './remove-duplicates-in-array.pipe'; describe('Testing RemoveDuplicatesInArrayPipe', () => { let pipe: RemoveDuplicatesInArrayPipe; @@ -28,16 +28,22 @@ describe('Testing RemoveDuplicatesInArrayPipe', () => { expect(pipe).not.toEqual(null); }); - it('should correctly remove duplicates in array', () =>{ - expect(pipe.transform( - ['ca_choices_1', 'ca_choices_2', 'ca_choices_1'])) - .toEqual(['ca_choices_1', 'ca_choices_2']); - expect(pipe.transform( - ['ca_choices_1', 'ca_choices_2'])) - .toEqual(['ca_choices_1', 'ca_choices_2']); - expect(pipe.transform( - ['ca_choices_1', 'ca_choices_2', 'ca_choices_1', 'ca_choices_2'])) - .toEqual(['ca_choices_1', 'ca_choices_2']); + it('should correctly remove duplicates in array', () => { + expect( + pipe.transform(['ca_choices_1', 'ca_choices_2', 'ca_choices_1']) + ).toEqual(['ca_choices_1', 'ca_choices_2']); + expect(pipe.transform(['ca_choices_1', 'ca_choices_2'])).toEqual([ + 'ca_choices_1', + 'ca_choices_2', + ]); + expect( + pipe.transform([ + 'ca_choices_1', + 'ca_choices_2', + 'ca_choices_1', + 'ca_choices_2', + ]) + ).toEqual(['ca_choices_1', 'ca_choices_2']); }); it('should throw error when the input is invalid', () => { @@ -49,7 +55,7 @@ describe('Testing RemoveDuplicatesInArrayPipe', () => { // '123' is not a string array. // @ts-ignore pipe.transform({ - filter: undefined + filter: undefined, } as string[]); }).toThrowError('Bad input for removeDuplicatesInArray: {}'); }); diff --git a/core/templates/filters/remove-duplicates-in-array.pipe.ts b/core/templates/filters/remove-duplicates-in-array.pipe.ts index 90befac9b0bc..dbe874f30762 100644 --- a/core/templates/filters/remove-duplicates-in-array.pipe.ts +++ b/core/templates/filters/remove-duplicates-in-array.pipe.ts @@ -16,14 +16,15 @@ * @fileoverview RemoveDuplicatesInArray filter for Oppia. */ -import { Pipe, PipeTransform } from '@angular/core'; +import {Pipe, PipeTransform} from '@angular/core'; @Pipe({name: 'removeDuplicatesInArray'}) export class RemoveDuplicatesInArrayPipe implements PipeTransform { transform(input: string[]): string[] { if (!input.filter) { throw new Error( - 'Bad input for removeDuplicatesInArray: ' + JSON.stringify(input)); + 'Bad input for removeDuplicatesInArray: ' + JSON.stringify(input) + ); } return input.filter((val, pos) => { return input.indexOf(val) === pos; diff --git a/core/templates/filters/shared-pipes.module.ts b/core/templates/filters/shared-pipes.module.ts index b6cc1646d3c6..9c7e2427e23e 100644 --- a/core/templates/filters/shared-pipes.module.ts +++ b/core/templates/filters/shared-pipes.module.ts @@ -18,25 +18,21 @@ import 'core-js/es7/reflect'; import 'zone.js'; -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { FormsModule } from '@angular/forms'; +import {CommonModule} from '@angular/common'; +import {NgModule} from '@angular/core'; +import {FormsModule} from '@angular/forms'; -import { LimitToPipe } from './limit-to.pipe'; -import { StringUtilityPipesModule } from 'filters/string-utility-filters/string-utility-pipes.module'; -import { ParameterizeRuleDescriptionPipe } from './parameterize-rule-description.pipe'; -import { ConvertToPlainTextPipe } from './string-utility-filters/convert-to-plain-text.pipe'; -import { ReplaceInputsWithEllipsesPipe } from './string-utility-filters/replace-inputs-with-ellipses.pipe'; -import { TruncatePipe } from './string-utility-filters/truncate.pipe'; -import { WrapTextWithEllipsisPipe } from './string-utility-filters/wrap-text-with-ellipsis.pipe'; -import { FormatTimePipe } from './format-timer.pipe'; +import {LimitToPipe} from './limit-to.pipe'; +import {StringUtilityPipesModule} from 'filters/string-utility-filters/string-utility-pipes.module'; +import {ParameterizeRuleDescriptionPipe} from './parameterize-rule-description.pipe'; +import {ConvertToPlainTextPipe} from './string-utility-filters/convert-to-plain-text.pipe'; +import {ReplaceInputsWithEllipsesPipe} from './string-utility-filters/replace-inputs-with-ellipses.pipe'; +import {TruncatePipe} from './string-utility-filters/truncate.pipe'; +import {WrapTextWithEllipsisPipe} from './string-utility-filters/wrap-text-with-ellipsis.pipe'; +import {FormatTimePipe} from './format-timer.pipe'; @NgModule({ - imports: [ - CommonModule, - FormsModule, - StringUtilityPipesModule, - ], + imports: [CommonModule, FormsModule, StringUtilityPipesModule], providers: [ ReplaceInputsWithEllipsesPipe, ParameterizeRuleDescriptionPipe, @@ -44,15 +40,7 @@ import { FormatTimePipe } from './format-timer.pipe'; TruncatePipe, WrapTextWithEllipsisPipe, ], - declarations: [ - LimitToPipe, - FormatTimePipe - ], - exports: [ - LimitToPipe, - StringUtilityPipesModule, - FormatTimePipe - ], + declarations: [LimitToPipe, FormatTimePipe], + exports: [LimitToPipe, StringUtilityPipesModule, FormatTimePipe], }) - -export class SharedPipesModule { } +export class SharedPipesModule {} diff --git a/core/templates/filters/string-utility-filters/camel-case-to-hyphens.pipe.spec.ts b/core/templates/filters/string-utility-filters/camel-case-to-hyphens.pipe.spec.ts index d8c6b21f0359..9af11d982da0 100644 --- a/core/templates/filters/string-utility-filters/camel-case-to-hyphens.pipe.spec.ts +++ b/core/templates/filters/string-utility-filters/camel-case-to-hyphens.pipe.spec.ts @@ -16,8 +16,7 @@ * @fileoverview Tests for CamelCaseToHyphens pipe for Oppia. */ -import { CamelCaseToHyphensPipe } from - 'filters/string-utility-filters/camel-case-to-hyphens.pipe'; +import {CamelCaseToHyphensPipe} from 'filters/string-utility-filters/camel-case-to-hyphens.pipe'; describe('Testing CamelCaseToHyphensPipe', () => { let pipe: CamelCaseToHyphensPipe; diff --git a/core/templates/filters/string-utility-filters/camel-case-to-hyphens.pipe.ts b/core/templates/filters/string-utility-filters/camel-case-to-hyphens.pipe.ts index aed4f0ab4b65..20a2b824d46e 100644 --- a/core/templates/filters/string-utility-filters/camel-case-to-hyphens.pipe.ts +++ b/core/templates/filters/string-utility-filters/camel-case-to-hyphens.pipe.ts @@ -16,10 +16,10 @@ * @fileoverview CamelCaseToHyphens pipe for Oppia. */ -import { Injectable, Pipe, PipeTransform } from '@angular/core'; +import {Injectable, Pipe, PipeTransform} from '@angular/core'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) @Pipe({name: 'camelCaseToHyphens'}) export class CamelCaseToHyphensPipe implements PipeTransform { diff --git a/core/templates/filters/string-utility-filters/capitalize.filter.spec.ts b/core/templates/filters/string-utility-filters/capitalize.filter.spec.ts index 08317080b095..627caae9fcf1 100644 --- a/core/templates/filters/string-utility-filters/capitalize.filter.spec.ts +++ b/core/templates/filters/string-utility-filters/capitalize.filter.spec.ts @@ -18,29 +18,36 @@ // TODO(#7222): Remove the following block of unnnecessary imports once // the code corresponding to the spec is upgraded to Angular 8. -import { UpgradedServices } from 'services/UpgradedServices'; +import {UpgradedServices} from 'services/UpgradedServices'; // ^^^ This block is to be removed. require('filters/string-utility-filters/capitalize.filter.ts'); -describe('Testing filters', function() { +describe('Testing filters', function () { var filterName = 'capitalize'; beforeEach(angular.mock.module('oppia')); - beforeEach(angular.mock.module( - 'oppia', - function($provide: { value: (arg0: string, arg1: string) => void }) { - var ugs = new UpgradedServices(); - for (let [key, value] of Object.entries(ugs.getUpgradedServices())) { - $provide.value(key, value as string); + beforeEach( + angular.mock.module( + 'oppia', + function ($provide: {value: (arg0: string, arg1: string) => void}) { + var ugs = new UpgradedServices(); + for (let [key, value] of Object.entries(ugs.getUpgradedServices())) { + $provide.value(key, value as string); + } } - })); + ) + ); - it('should have all expected filters', angular.mock.inject(function($filter) { - expect($filter(filterName)).not.toEqual(null); - })); + it( + 'should have all expected filters', + angular.mock.inject(function ($filter) { + expect($filter(filterName)).not.toEqual(null); + }) + ); - it('should correctly capitalize strings', angular.mock.inject( - function($filter) { + it( + 'should correctly capitalize strings', + angular.mock.inject(function ($filter) { var filter = $filter('capitalize'); expect(filter('')).toEqual(''); @@ -56,7 +63,8 @@ describe('Testing filters', function() { expect(filter(' a b ')).toEqual('A b'); expect(filter(' ab c ')).toEqual('Ab c'); expect(filter(' only First lettEr is Affected ')).toEqual( - 'Only First lettEr is Affected'); - } - )); + 'Only First lettEr is Affected' + ); + }) + ); }); diff --git a/core/templates/filters/string-utility-filters/capitalize.filter.ts b/core/templates/filters/string-utility-filters/capitalize.filter.ts index 4d6ab94ccf63..28f4fe9201d4 100644 --- a/core/templates/filters/string-utility-filters/capitalize.filter.ts +++ b/core/templates/filters/string-utility-filters/capitalize.filter.ts @@ -16,13 +16,15 @@ * @fileoverview Capitalize filter for Oppia. */ -angular.module('oppia').filter('capitalize', [function() { - return function(input: string) { - if (!input) { - return input; - } +angular.module('oppia').filter('capitalize', [ + function () { + return function (input: string) { + if (!input) { + return input; + } - var trimmedInput = input.trim(); - return trimmedInput.charAt(0).toUpperCase() + trimmedInput.slice(1); - }; -}]); + var trimmedInput = input.trim(); + return trimmedInput.charAt(0).toUpperCase() + trimmedInput.slice(1); + }; + }, +]); diff --git a/core/templates/filters/string-utility-filters/capitalize.pipe.spec.ts b/core/templates/filters/string-utility-filters/capitalize.pipe.spec.ts index c2da653589a0..2a2b16045558 100644 --- a/core/templates/filters/string-utility-filters/capitalize.pipe.spec.ts +++ b/core/templates/filters/string-utility-filters/capitalize.pipe.spec.ts @@ -16,7 +16,7 @@ * @fileoverview Tests for Capitalize pipe for Oppia. */ -import { CapitalizePipe } from 'filters/string-utility-filters/capitalize.pipe'; +import {CapitalizePipe} from 'filters/string-utility-filters/capitalize.pipe'; describe('Testing filters', () => { let pipe: CapitalizePipe; @@ -28,7 +28,7 @@ describe('Testing filters', () => { expect(pipe).not.toEqual(null); }); - it('should correctly capitalize strings', () =>{ + it('should correctly capitalize strings', () => { expect(pipe.transform('')).toEqual(''); expect(pipe.transform('a')).toEqual('A'); @@ -40,6 +40,7 @@ describe('Testing filters', () => { expect(pipe.transform(' a b ')).toEqual('A b'); expect(pipe.transform(' ab c ')).toEqual('Ab c'); expect(pipe.transform(' only First lettEr is Affected ')).toEqual( - 'Only First lettEr is Affected'); + 'Only First lettEr is Affected' + ); }); }); diff --git a/core/templates/filters/string-utility-filters/capitalize.pipe.ts b/core/templates/filters/string-utility-filters/capitalize.pipe.ts index 8affed29cbf2..e84cd86428d6 100644 --- a/core/templates/filters/string-utility-filters/capitalize.pipe.ts +++ b/core/templates/filters/string-utility-filters/capitalize.pipe.ts @@ -16,11 +16,11 @@ * @fileoverview Capitalize pipe for Oppia. */ -import { Injectable, Pipe, PipeTransform } from '@angular/core'; +import {Injectable, Pipe, PipeTransform} from '@angular/core'; @Pipe({name: 'capitalizePipe'}) @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class CapitalizePipe implements PipeTransform { transform(input: string): string { diff --git a/core/templates/filters/string-utility-filters/convert-to-plain-text.filter.spec.ts b/core/templates/filters/string-utility-filters/convert-to-plain-text.filter.spec.ts index 713ce15d1c1d..2f84902b921f 100644 --- a/core/templates/filters/string-utility-filters/convert-to-plain-text.filter.spec.ts +++ b/core/templates/filters/string-utility-filters/convert-to-plain-text.filter.spec.ts @@ -18,31 +18,35 @@ // TODO(#7222): Remove the following block of unnnecessary imports once // the code corresponding to the spec is upgraded to Angular 8. -import { UpgradedServices } from 'services/UpgradedServices'; +import {UpgradedServices} from 'services/UpgradedServices'; // ^^^ This block is to be removed. require('filters/string-utility-filters/convert-to-plain-text.filter.ts'); -describe('Testing filters', function() { +describe('Testing filters', function () { beforeEach(angular.mock.module('oppia')); - beforeEach(angular.mock.module( - 'oppia', - function($provide: { value: (arg0: string, arg1: string) => void }) { - var ugs = new UpgradedServices(); - for (let [key, value] of Object.entries(ugs.getUpgradedServices())) { - $provide.value(key, value as string); + beforeEach( + angular.mock.module( + 'oppia', + function ($provide: {value: (arg0: string, arg1: string) => void}) { + var ugs = new UpgradedServices(); + for (let [key, value] of Object.entries(ugs.getUpgradedServices())) { + $provide.value(key, value as string); + } } - })); + ) + ); - it('should correctly convertToPlainText strings', angular.mock.inject( - function($filter) { + it( + 'should correctly convertToPlainText strings', + angular.mock.inject(function ($filter) { var filter = $filter('convertToPlainText'); - expect(filter(' "test test" ')) - .toBe('test test'); - expect(filter('" "')) - .toBe(' '); + expect(filter(' "test test" ')).toBe( + 'test test' + ); + expect(filter('" "')).toBe(' '); expect(filter('')).toEqual(''); - } - )); + }) + ); }); diff --git a/core/templates/filters/string-utility-filters/convert-to-plain-text.filter.ts b/core/templates/filters/string-utility-filters/convert-to-plain-text.filter.ts index fef66bec766b..109a3807de03 100644 --- a/core/templates/filters/string-utility-filters/convert-to-plain-text.filter.ts +++ b/core/templates/filters/string-utility-filters/convert-to-plain-text.filter.ts @@ -16,17 +16,19 @@ * @fileoverview ConvertToPlainText filter for Oppia. */ -angular.module('oppia').filter('convertToPlainText', [function() { - return function(input: string) { - var strippedText = input.replace(/(<([^>]+)>)/ig, ''); - strippedText = strippedText.replace(/ /ig, ' '); - strippedText = strippedText.replace(/"/ig, ''); +angular.module('oppia').filter('convertToPlainText', [ + function () { + return function (input: string) { + var strippedText = input.replace(/(<([^>]+)>)/gi, ''); + strippedText = strippedText.replace(/ /gi, ' '); + strippedText = strippedText.replace(/"/gi, ''); - var trimmedText = strippedText.trim(); - if (trimmedText.length === 0) { - return strippedText; - } else { - return trimmedText; - } - }; -}]); + var trimmedText = strippedText.trim(); + if (trimmedText.length === 0) { + return strippedText; + } else { + return trimmedText; + } + }; + }, +]); diff --git a/core/templates/filters/string-utility-filters/convert-to-plain-text.pipe.spec.ts b/core/templates/filters/string-utility-filters/convert-to-plain-text.pipe.spec.ts index f93b394da713..fc613307f8e0 100644 --- a/core/templates/filters/string-utility-filters/convert-to-plain-text.pipe.spec.ts +++ b/core/templates/filters/string-utility-filters/convert-to-plain-text.pipe.spec.ts @@ -16,7 +16,7 @@ * @fileoverview Tests for ConvertToPlainText pipe for Oppia. */ -import { ConvertToPlainTextPipe } from './convert-to-plain-text.pipe'; +import {ConvertToPlainTextPipe} from './convert-to-plain-text.pipe'; describe('Testing ConvertToPlainTextPipe', () => { let pipe: ConvertToPlainTextPipe; @@ -28,10 +28,10 @@ describe('Testing ConvertToPlainTextPipe', () => { expect(pipe).not.toEqual(null); }); - it('should correctly convert to plain text', () =>{ - expect(pipe.transform(' "test test" ')) - .toBe('test test'); - expect(pipe.transform('" "')) - .toBe(' '); + it('should correctly convert to plain text', () => { + expect(pipe.transform(' "test test" ')).toBe( + 'test test' + ); + expect(pipe.transform('" "')).toBe(' '); }); }); diff --git a/core/templates/filters/string-utility-filters/convert-to-plain-text.pipe.ts b/core/templates/filters/string-utility-filters/convert-to-plain-text.pipe.ts index 08171574d9dc..1e47ac7f8dc3 100644 --- a/core/templates/filters/string-utility-filters/convert-to-plain-text.pipe.ts +++ b/core/templates/filters/string-utility-filters/convert-to-plain-text.pipe.ts @@ -16,17 +16,17 @@ * @fileoverview ConvertToPlainText pipe for Oppia. */ -import { Injectable, Pipe, PipeTransform } from '@angular/core'; +import {Injectable, Pipe, PipeTransform} from '@angular/core'; @Pipe({name: 'convertToPlainText'}) @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ConvertToPlainTextPipe implements PipeTransform { transform(input: string): string { - let strippedText = input.replace(/(<([^>]+)>)/ig, ''); - strippedText = strippedText.replace(/ /ig, ' '); - strippedText = strippedText.replace(/"/ig, ''); + let strippedText = input.replace(/(<([^>]+)>)/gi, ''); + strippedText = strippedText.replace(/ /gi, ' '); + strippedText = strippedText.replace(/"/gi, ''); let trimmedText = strippedText.trim(); if (trimmedText.length === 0) { diff --git a/core/templates/filters/string-utility-filters/filter-for-matching-substring.pipe.spec.ts b/core/templates/filters/string-utility-filters/filter-for-matching-substring.pipe.spec.ts index f6c67029069b..01a6a55b3e99 100644 --- a/core/templates/filters/string-utility-filters/filter-for-matching-substring.pipe.spec.ts +++ b/core/templates/filters/string-utility-filters/filter-for-matching-substring.pipe.spec.ts @@ -16,7 +16,7 @@ * @fileoverview Tests for CamelCaseToHyphens pipe for Oppia. */ -import { FilterForMatchingSubstringPipe } from 'filters/string-utility-filters/filter-for-matching-substring.pipe'; +import {FilterForMatchingSubstringPipe} from 'filters/string-utility-filters/filter-for-matching-substring.pipe'; describe('Testing FilterForMatchingSubstringPipe', () => { let pipe: FilterForMatchingSubstringPipe; @@ -54,8 +54,11 @@ describe('Testing FilterForMatchingSubstringPipe', () => { it('should get items when input is a space', () => { let list = ['cat and dog', 'dog', 'caterpillar', 'cat and dog', ' ']; - expect(pipe.transform(list, ' ')) - .toEqual(['cat and dog', 'cat and dog', ' ']); + expect(pipe.transform(list, ' ')).toEqual([ + 'cat and dog', + 'cat and dog', + ' ', + ]); expect(pipe.transform(list, ' ')).toEqual(['cat and dog', ' ']); }); diff --git a/core/templates/filters/string-utility-filters/filter-for-matching-substring.pipe.ts b/core/templates/filters/string-utility-filters/filter-for-matching-substring.pipe.ts index f47d7dcdbe14..0f1b48161d9c 100644 --- a/core/templates/filters/string-utility-filters/filter-for-matching-substring.pipe.ts +++ b/core/templates/filters/string-utility-filters/filter-for-matching-substring.pipe.ts @@ -17,13 +17,13 @@ * matching strings. */ -import { Injectable, Pipe, PipeTransform } from '@angular/core'; +import {Injectable, Pipe, PipeTransform} from '@angular/core'; @Pipe({ - name: 'filterForMatchingSubtring' + name: 'filterForMatchingSubtring', }) @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class FilterForMatchingSubstringPipe implements PipeTransform { transform(input: string[], matcher: string): string[] { diff --git a/core/templates/filters/string-utility-filters/get-abbreviated-text.pipe.spec.ts b/core/templates/filters/string-utility-filters/get-abbreviated-text.pipe.spec.ts index a5221b08fc8c..bf538b6d1f36 100644 --- a/core/templates/filters/string-utility-filters/get-abbreviated-text.pipe.spec.ts +++ b/core/templates/filters/string-utility-filters/get-abbreviated-text.pipe.spec.ts @@ -16,10 +16,9 @@ * @fileoverview Tests for GetAbbreviatedText pipe for Oppia. */ -import { GetAbbreviatedTextPipe } from - 'filters/string-utility-filters/get-abbreviated-text.pipe'; +import {GetAbbreviatedTextPipe} from 'filters/string-utility-filters/get-abbreviated-text.pipe'; -describe('Testing filters', function() { +describe('Testing filters', function () { let pipe: GetAbbreviatedTextPipe; beforeEach(() => { pipe = new GetAbbreviatedTextPipe(); @@ -30,25 +29,38 @@ describe('Testing filters', function() { }); it('should not shorten the length of text', () => { - expect(pipe.transform('It will remain unchanged.', 50)) - .toBe('It will remain unchanged.'); - expect(pipe.transform( - 'Itisjustaverylongsinglewordfortesting', - 50)).toBe('Itisjustaverylongsinglewordfortesting'); + expect(pipe.transform('It will remain unchanged.', 50)).toBe( + 'It will remain unchanged.' + ); + expect(pipe.transform('Itisjustaverylongsinglewordfortesting', 50)).toBe( + 'Itisjustaverylongsinglewordfortesting' + ); }); it('should shorten the length of text', () => { - expect(pipe.transform( - 'It has to convert to a substring as it exceeds the character limit.', - 50)).toBe('It has to convert to a substring as it exceeds...'); - expect(pipe.transform( - 'ItisjustaverylongsinglewordfortestinggetAbbreviatedText', - 50)).toBe('ItisjustaverylongsinglewordfortestinggetAbbreviate...'); - expect(pipe.transform( - 'â, ??î or ôu🕧� n☁i✑💴++$-💯 ♓!🇪🚑🌚‼⁉4⃣od; /⏬®;😁☕😁:☝)😁😁😍1!@#', - 50)).toBe('â, ??î or ôu🕧� n☁i✑💴++$-💯 ♓!🇪🚑🌚‼⁉4⃣od;...'); - expect(pipe.transform( - 'It is just a very long singlewordfortestinggetAbbreviatedText', - 50)).toBe('It is just a very long...'); + expect( + pipe.transform( + 'It has to convert to a substring as it exceeds the character limit.', + 50 + ) + ).toBe('It has to convert to a substring as it exceeds...'); + expect( + pipe.transform( + 'ItisjustaverylongsinglewordfortestinggetAbbreviatedText', + 50 + ) + ).toBe('ItisjustaverylongsinglewordfortestinggetAbbreviate...'); + expect( + pipe.transform( + 'â, ??î or ôu🕧� n☁i✑💴++$-💯 ♓!🇪🚑🌚‼⁉4⃣od; /⏬®;😁☕😁:☝)😁😁😍1!@#', + 50 + ) + ).toBe('â, ??î or ôu🕧� n☁i✑💴++$-💯 ♓!🇪🚑🌚‼⁉4⃣od;...'); + expect( + pipe.transform( + 'It is just a very long singlewordfortestinggetAbbreviatedText', + 50 + ) + ).toBe('It is just a very long...'); }); }); diff --git a/core/templates/filters/string-utility-filters/get-abbreviated-text.pipe.ts b/core/templates/filters/string-utility-filters/get-abbreviated-text.pipe.ts index bcae2b4deaca..6ff6b4ba5997 100644 --- a/core/templates/filters/string-utility-filters/get-abbreviated-text.pipe.ts +++ b/core/templates/filters/string-utility-filters/get-abbreviated-text.pipe.ts @@ -16,7 +16,7 @@ * @fileoverview GetAbbreviatedText pipe for Oppia. */ -import { Pipe, PipeTransform } from '@angular/core'; +import {Pipe, PipeTransform} from '@angular/core'; @Pipe({name: 'getAbbreviatedText'}) export class GetAbbreviatedTextPipe implements PipeTransform { diff --git a/core/templates/filters/string-utility-filters/normalize-whitespace-punctuation-and-case.pipe.spec.ts b/core/templates/filters/string-utility-filters/normalize-whitespace-punctuation-and-case.pipe.spec.ts index fa713917dc9a..67b18aeff8a2 100644 --- a/core/templates/filters/string-utility-filters/normalize-whitespace-punctuation-and-case.pipe.spec.ts +++ b/core/templates/filters/string-utility-filters/normalize-whitespace-punctuation-and-case.pipe.spec.ts @@ -16,15 +16,14 @@ * @fileoverview Tests for NormalizeWhitespacePunctuationAndCase pipe for Oppia. */ -import { NormalizeWhitespacePunctuationAndCasePipe } from - './normalize-whitespace-punctuation-and-case.pipe'; -import { TestBed } from '@angular/core/testing'; +import {NormalizeWhitespacePunctuationAndCasePipe} from './normalize-whitespace-punctuation-and-case.pipe'; +import {TestBed} from '@angular/core/testing'; describe('Testing NormalizeWhitespacePunctuationAndCasePipe', () => { let nwpcp: NormalizeWhitespacePunctuationAndCasePipe; beforeEach(() => { TestBed.configureTestingModule({ - providers: [NormalizeWhitespacePunctuationAndCasePipe] + providers: [NormalizeWhitespacePunctuationAndCasePipe], }); nwpcp = TestBed.inject(NormalizeWhitespacePunctuationAndCasePipe); }); @@ -36,26 +35,22 @@ describe('Testing NormalizeWhitespacePunctuationAndCasePipe', () => { it('should normalize spaces and turn characters to lower case', () => { expect(nwpcp.transform('')).toEqual(''); - expect(nwpcp.transform(' remove ')) - .toEqual('remove'); + expect(nwpcp.transform(' remove ')).toEqual('remove'); // Should remove the space if it does not // separate two alphanumeric "words". - expect(nwpcp.transform(' remove ? ')) - .toEqual('remove?'); - expect(nwpcp.transform(' Hello, world ')) - .toEqual('hello,world'); - expect(nwpcp.transform(' Test1 tesT2 teSt3 ')) - .toEqual('test1 test2 test3'); - expect(nwpcp.transform(' Test1 tesT2! teSt3 ')) - .toEqual('test1 test2!test3'); - - expect(nwpcp.transform(' teSTstrinG12 ')) - .toEqual('teststring12'); - expect(nwpcp.transform(' tesT1 teSt2 ')) - .toEqual('test1 test2'); - - expect(nwpcp.transform('tesT1\n teSt2')) - .toEqual('test1\ntest2'); + expect(nwpcp.transform(' remove ? ')).toEqual('remove?'); + expect(nwpcp.transform(' Hello, world ')).toEqual('hello,world'); + expect(nwpcp.transform(' Test1 tesT2 teSt3 ')).toEqual( + 'test1 test2 test3' + ); + expect(nwpcp.transform(' Test1 tesT2! teSt3 ')).toEqual( + 'test1 test2!test3' + ); + + expect(nwpcp.transform(' teSTstrinG12 ')).toEqual('teststring12'); + expect(nwpcp.transform(' tesT1 teSt2 ')).toEqual('test1 test2'); + + expect(nwpcp.transform('tesT1\n teSt2')).toEqual('test1\ntest2'); }); }); diff --git a/core/templates/filters/string-utility-filters/normalize-whitespace-punctuation-and-case.pipe.ts b/core/templates/filters/string-utility-filters/normalize-whitespace-punctuation-and-case.pipe.ts index c3119ea2a534..a63948128a77 100644 --- a/core/templates/filters/string-utility-filters/normalize-whitespace-punctuation-and-case.pipe.ts +++ b/core/templates/filters/string-utility-filters/normalize-whitespace-punctuation-and-case.pipe.ts @@ -16,22 +16,26 @@ * @fileoverview NormalizeWhitespacePunctuationAndCase pipe for Oppia. */ -import { Injectable, Pipe, PipeTransform } from '@angular/core'; +import {Injectable, Pipe, PipeTransform} from '@angular/core'; // Filter that takes a string, trims and normalizes spaces within each // line, and removes blank lines. Note that any spaces whose removal does not // result in two alphanumeric "words" being joined together are also removed, // so "hello ? " becomes "hello?". @Injectable({ - providedIn: 'root' + providedIn: 'root', }) @Pipe({name: 'normalizeWhitespacePunctuationAndCase'}) export class NormalizeWhitespacePunctuationAndCasePipe -implements PipeTransform { + implements PipeTransform +{ transform(input: string): string { - let isAlphanumeric = function(character: string) { - return 'qwertyuiopasdfghjklzxcvbnm0123456789'.indexOf( - character.toLowerCase()) !== -1; + let isAlphanumeric = function (character: string) { + return ( + 'qwertyuiopasdfghjklzxcvbnm0123456789'.indexOf( + character.toLowerCase() + ) !== -1 + ); }; input = input.trim(); @@ -44,9 +48,12 @@ implements PipeTransform { for (let j: number = 0; j < inputLine.length; j++) { let currentChar: string = inputLine.charAt(j).toLowerCase(); if (currentChar === ' ') { - if (j > 0 && j < inputLine.length - 1 && - isAlphanumeric(inputLine.charAt(j - 1)) && - isAlphanumeric(inputLine.charAt(j + 1))) { + if ( + j > 0 && + j < inputLine.length - 1 && + isAlphanumeric(inputLine.charAt(j - 1)) && + isAlphanumeric(inputLine.charAt(j + 1)) + ) { result += currentChar; } } else { diff --git a/core/templates/filters/string-utility-filters/normalize-whitespace.pipe.spec.ts b/core/templates/filters/string-utility-filters/normalize-whitespace.pipe.spec.ts index 2761064dbe85..c3a38fe55e70 100644 --- a/core/templates/filters/string-utility-filters/normalize-whitespace.pipe.spec.ts +++ b/core/templates/filters/string-utility-filters/normalize-whitespace.pipe.spec.ts @@ -16,15 +16,14 @@ * @fileoverview Tests for NormalizeWhitespace pipe for Oppia. */ -import { NormalizeWhitespacePipe } from - 'filters/string-utility-filters/normalize-whitespace.pipe'; -import { TestBed } from '@angular/core/testing'; +import {NormalizeWhitespacePipe} from 'filters/string-utility-filters/normalize-whitespace.pipe'; +import {TestBed} from '@angular/core/testing'; describe('Testing NormalizeWhitespacePipe', () => { let nwp: NormalizeWhitespacePipe; beforeEach(() => { TestBed.configureTestingModule({ - providers: [NormalizeWhitespacePipe] + providers: [NormalizeWhitespacePipe], }); nwp = TestBed.inject(NormalizeWhitespacePipe); }); diff --git a/core/templates/filters/string-utility-filters/normalize-whitespace.pipe.ts b/core/templates/filters/string-utility-filters/normalize-whitespace.pipe.ts index 7b364285b3d8..f58d00e72013 100644 --- a/core/templates/filters/string-utility-filters/normalize-whitespace.pipe.ts +++ b/core/templates/filters/string-utility-filters/normalize-whitespace.pipe.ts @@ -16,13 +16,13 @@ * @fileoverview NormalizeWhitespace pipe for Oppia. */ -import { Injectable, Pipe, PipeTransform } from '@angular/core'; -import { UtilsService } from 'services/utils.service'; +import {Injectable, Pipe, PipeTransform} from '@angular/core'; +import {UtilsService} from 'services/utils.service'; // Pipe that removes whitespace from the beginning and end of a string, and // replaces interior whitespace with a single space character. @Injectable({ - providedIn: 'root' + providedIn: 'root', }) @Pipe({name: 'normalizeWhitespace'}) export class NormalizeWhitespacePipe implements PipeTransform { diff --git a/core/templates/filters/string-utility-filters/replace-inputs-with-ellipses.pipe.spec.ts b/core/templates/filters/string-utility-filters/replace-inputs-with-ellipses.pipe.spec.ts index 9a6b53bb7e19..a642b0f8178c 100644 --- a/core/templates/filters/string-utility-filters/replace-inputs-with-ellipses.pipe.spec.ts +++ b/core/templates/filters/string-utility-filters/replace-inputs-with-ellipses.pipe.spec.ts @@ -16,10 +16,9 @@ * @fileoverview Tests for ReplaceInputsWithEllipses pipe for Oppia. */ -import { ReplaceInputsWithEllipsesPipe } from - 'filters/string-utility-filters/replace-inputs-with-ellipses.pipe'; +import {ReplaceInputsWithEllipsesPipe} from 'filters/string-utility-filters/replace-inputs-with-ellipses.pipe'; -describe('Testing filters', function() { +describe('Testing filters', function () { let pipe: ReplaceInputsWithEllipsesPipe; beforeEach(() => { pipe = new ReplaceInputsWithEllipsesPipe(); diff --git a/core/templates/filters/string-utility-filters/replace-inputs-with-ellipses.pipe.ts b/core/templates/filters/string-utility-filters/replace-inputs-with-ellipses.pipe.ts index 1d0164e5134c..9b72ca790ed1 100644 --- a/core/templates/filters/string-utility-filters/replace-inputs-with-ellipses.pipe.ts +++ b/core/templates/filters/string-utility-filters/replace-inputs-with-ellipses.pipe.ts @@ -16,7 +16,7 @@ * @fileoverview ReplaceInputsWithEllipses pipe for Oppia. */ -import { Pipe, PipeTransform } from '@angular/core'; +import {Pipe, PipeTransform} from '@angular/core'; // Filter that replaces all {{...}} in a string with '...'. @Pipe({name: 'replaceInputsWithEllipses'}) diff --git a/core/templates/filters/string-utility-filters/sort-by.pipe.spec.ts b/core/templates/filters/string-utility-filters/sort-by.pipe.spec.ts index 354abfb44b0f..ef5bbada56a6 100644 --- a/core/templates/filters/string-utility-filters/sort-by.pipe.spec.ts +++ b/core/templates/filters/string-utility-filters/sort-by.pipe.spec.ts @@ -16,35 +16,37 @@ * @fileoverview Tests for SortBy pipe for Oppia. */ -import { SortByPipe } from 'filters/string-utility-filters/sort-by.pipe'; -import { TestBed } from '@angular/core/testing'; +import {SortByPipe} from 'filters/string-utility-filters/sort-by.pipe'; +import {TestBed} from '@angular/core/testing'; describe('Sort By Pipe', () => { - let multidimensionalArray = [{ - impact: 0, - picture_data_url: 'creatorA-url', - username: 'Bucky', - }, - { - impact: 1, - picture_data_url: 'creatorB-url', - username: 'Arrow', - }, - { - impact: 3, - picture_data_url: 'same-url', - username: 'Deadpool', - }, - { - impact: 2, - picture_data_url: 'same-url', - username: 'Captain America', - }]; + let multidimensionalArray = [ + { + impact: 0, + picture_data_url: 'creatorA-url', + username: 'Bucky', + }, + { + impact: 1, + picture_data_url: 'creatorB-url', + username: 'Arrow', + }, + { + impact: 3, + picture_data_url: 'same-url', + username: 'Deadpool', + }, + { + impact: 2, + picture_data_url: 'same-url', + username: 'Captain America', + }, + ]; let sortByPipe: SortByPipe; beforeEach(() => { TestBed.configureTestingModule({ - providers: [SortByPipe] + providers: [SortByPipe], }); sortByPipe = TestBed.inject(SortByPipe); }); @@ -53,138 +55,166 @@ describe('Sort By Pipe', () => { expect(sortByPipe).not.toEqual(null); }); - it('should sort single dimension integer' + - ' array in ascending order', () => { - let numberArray = [1, 4, 3, 5, 2]; - let result = sortByPipe.transform(numberArray, false); - expect(result).toEqual([1, 2, 3, 4, 5]); - }); + it( + 'should sort single dimension integer' + ' array in ascending order', + () => { + let numberArray = [1, 4, 3, 5, 2]; + let result = sortByPipe.transform(numberArray, false); + expect(result).toEqual([1, 2, 3, 4, 5]); + } + ); - it('should sort single dimension integer array' + - ' in descending order', () => { - let numberArray = [1, 4, 3, 5, 2]; - let result = sortByPipe.transform(numberArray, true); - expect(result).toEqual([5, 4, 3, 2, 1]); - }); + it( + 'should sort single dimension integer array' + ' in descending order', + () => { + let numberArray = [1, 4, 3, 5, 2]; + let result = sortByPipe.transform(numberArray, true); + expect(result).toEqual([5, 4, 3, 2, 1]); + } + ); - it('should sort single dimension character array' + - ' in ascending order', () => { - let charArray = ['b', 'c', 'a', 'd', 'e']; - let result = sortByPipe.transform(charArray, false); - expect(result).toEqual(['a', 'b', 'c', 'd', 'e']); - }); + it( + 'should sort single dimension character array' + ' in ascending order', + () => { + let charArray = ['b', 'c', 'a', 'd', 'e']; + let result = sortByPipe.transform(charArray, false); + expect(result).toEqual(['a', 'b', 'c', 'd', 'e']); + } + ); - it('should sort single dimension character array' + - ' in descending order', () => { - let charArray = ['b', 'c', 'a', 'd', 'e']; - let result = sortByPipe.transform(charArray, true); - expect(result).toEqual(['e', 'd', 'c', 'b', 'a']); - }); + it( + 'should sort single dimension character array' + ' in descending order', + () => { + let charArray = ['b', 'c', 'a', 'd', 'e']; + let result = sortByPipe.transform(charArray, true); + expect(result).toEqual(['e', 'd', 'c', 'b', 'a']); + } + ); - it('should sort multi dimensional array with integer attribute' + - ' in ascending order', () => { - let result = sortByPipe.transform( - multidimensionalArray, false, 'impact'); - result.forEach((value, index) => { - if (index === 0) { - expect(value.impact).toBe(0); - } - if (index === 1) { - expect(value.impact).toBe(1); - } - if (index === 2) { - expect(value.impact).toBe(2); - } - if (index === 3) { - expect(value.impact).toBe(3); - } - }); - }); + it( + 'should sort multi dimensional array with integer attribute' + + ' in ascending order', + () => { + let result = sortByPipe.transform(multidimensionalArray, false, 'impact'); + result.forEach((value, index) => { + if (index === 0) { + expect(value.impact).toBe(0); + } + if (index === 1) { + expect(value.impact).toBe(1); + } + if (index === 2) { + expect(value.impact).toBe(2); + } + if (index === 3) { + expect(value.impact).toBe(3); + } + }); + } + ); - it('should sort multi dimensional array with integer attribute' + - ' in descending order', () => { - let result = sortByPipe.transform( - multidimensionalArray, true, 'impact'); - result.forEach((value, index) => { - if (index === 0) { - expect(value.impact).toBe(3); - } - if (index === 1) { - expect(value.impact).toBe(2); - } - if (index === 2) { - expect(value.impact).toBe(1); - } - if (index === 3) { - expect(value.impact).toBe(0); - } - }); - }); + it( + 'should sort multi dimensional array with integer attribute' + + ' in descending order', + () => { + let result = sortByPipe.transform(multidimensionalArray, true, 'impact'); + result.forEach((value, index) => { + if (index === 0) { + expect(value.impact).toBe(3); + } + if (index === 1) { + expect(value.impact).toBe(2); + } + if (index === 2) { + expect(value.impact).toBe(1); + } + if (index === 3) { + expect(value.impact).toBe(0); + } + }); + } + ); - it('should sort multi dimensional array with string attribute' + - ' in ascending order', () => { - let result = sortByPipe.transform( - multidimensionalArray, false, 'username'); - result.forEach((value, index) => { - if (index === 0) { - expect(value.username).toBe('Arrow'); - } - if (index === 1) { - expect(value.username).toBe('Bucky'); - } - if (index === 2) { - expect(value.username).toBe('Captain America'); - } - if (index === 3) { - expect(value.username).toBe('Deadpool'); - } - }); - }); + it( + 'should sort multi dimensional array with string attribute' + + ' in ascending order', + () => { + let result = sortByPipe.transform( + multidimensionalArray, + false, + 'username' + ); + result.forEach((value, index) => { + if (index === 0) { + expect(value.username).toBe('Arrow'); + } + if (index === 1) { + expect(value.username).toBe('Bucky'); + } + if (index === 2) { + expect(value.username).toBe('Captain America'); + } + if (index === 3) { + expect(value.username).toBe('Deadpool'); + } + }); + } + ); - it('should sort multi dimensional array with string attribute' + - ' in descending order', () => { - let result = sortByPipe.transform( - multidimensionalArray, true, 'username'); - result.forEach((value, index) => { - if (index === 0) { - expect(value.username).toBe('Deadpool'); - } - if (index === 1) { - expect(value.username).toBe('Captain America'); - } - if (index === 2) { - expect(value.username).toBe('Bucky'); - } - if (index === 3) { - expect(value.username).toBe('Arrow'); - } - }); - }); + it( + 'should sort multi dimensional array with string attribute' + + ' in descending order', + () => { + let result = sortByPipe.transform( + multidimensionalArray, + true, + 'username' + ); + result.forEach((value, index) => { + if (index === 0) { + expect(value.username).toBe('Deadpool'); + } + if (index === 1) { + expect(value.username).toBe('Captain America'); + } + if (index === 2) { + expect(value.username).toBe('Bucky'); + } + if (index === 3) { + expect(value.username).toBe('Arrow'); + } + }); + } + ); - it('should return back the current value if' + - ' sortKey values matches', () => { - let result = sortByPipe.transform( - multidimensionalArray, false, 'picture_data_url'); - result.forEach((value, index) => { - if (index === 0) { - expect(value.picture_data_url).toBe('creatorA-url'); - } - if (index === 1) { - expect(value.picture_data_url).toBe('creatorB-url'); - } - if (index === 2) { - expect(value.picture_data_url).toBe('same-url'); - } - if (index === 3) { - expect(value.picture_data_url).toBe('same-url'); - } - }); - expect(result).toEqual(multidimensionalArray); - }); + it( + 'should return back the current value if' + ' sortKey values matches', + () => { + let result = sortByPipe.transform( + multidimensionalArray, + false, + 'picture_data_url' + ); + result.forEach((value, index) => { + if (index === 0) { + expect(value.picture_data_url).toBe('creatorA-url'); + } + if (index === 1) { + expect(value.picture_data_url).toBe('creatorB-url'); + } + if (index === 2) { + expect(value.picture_data_url).toBe('same-url'); + } + if (index === 3) { + expect(value.picture_data_url).toBe('same-url'); + } + }); + expect(result).toEqual(multidimensionalArray); + } + ); it('should return the reversed array if given sortKey is default', () => { - let result = sortByPipe.transform( - multidimensionalArray, true, 'default'); + let result = sortByPipe.transform(multidimensionalArray, true, 'default'); expect(result).toEqual(multidimensionalArray.reverse()); }); }); diff --git a/core/templates/filters/string-utility-filters/sort-by.pipe.ts b/core/templates/filters/string-utility-filters/sort-by.pipe.ts index f29ae1c88aa1..3296594c1a0e 100644 --- a/core/templates/filters/string-utility-filters/sort-by.pipe.ts +++ b/core/templates/filters/string-utility-filters/sort-by.pipe.ts @@ -16,7 +16,7 @@ * @fileoverview Sort By pipe for Oppia. */ -import { Injectable, Pipe, PipeTransform } from '@angular/core'; +import {Injectable, Pipe, PipeTransform} from '@angular/core'; // SortBy pipe is a replica of angular js orderBy filter. // The first check of this pipe is to filter out whether the // received 'value' is a single dimensional array or a multidimensional array. @@ -29,12 +29,10 @@ import { Injectable, Pipe, PipeTransform } from '@angular/core'; name: 'sortBy', }) @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class SortByPipe implements PipeTransform { - transform( - value: T[], isDescending: boolean, - sortKey?: string): T[] { + transform(value: T[], isDescending: boolean, sortKey?: string): T[] { if (sortKey === 'default') { return value.reverse(); } @@ -47,7 +45,8 @@ export class SortByPipe implements PipeTransform { stringArray = value.filter(item => typeof item === 'string').sort(); } else { const _sortKey = sortKey as keyof T; - numberArray = value.filter(item => typeof item[_sortKey] === 'number') + numberArray = value + .filter(item => typeof item[_sortKey] === 'number') .sort((a, b) => Number(a[_sortKey]) - Number(b[_sortKey])); stringArray = value .filter(item => typeof item[_sortKey] === 'string') diff --git a/core/templates/filters/string-utility-filters/string-utility-pipes.module.ts b/core/templates/filters/string-utility-filters/string-utility-pipes.module.ts index 1a5899e008ec..7e7abe1b7bd0 100644 --- a/core/templates/filters/string-utility-filters/string-utility-pipes.module.ts +++ b/core/templates/filters/string-utility-filters/string-utility-pipes.module.ts @@ -16,26 +16,26 @@ * @fileoverview Module for the background banner component. */ -import { NgModule } from '@angular/core'; +import {NgModule} from '@angular/core'; -import { CamelCaseToHyphensPipe } from 'filters/string-utility-filters/camel-case-to-hyphens.pipe'; -import { CapitalizePipe } from 'filters/string-utility-filters/capitalize.pipe'; -import { ConvertToPlainTextPipe } from 'filters/string-utility-filters/convert-to-plain-text.pipe'; -import { FilterForMatchingSubstringPipe } from 'filters/string-utility-filters/filter-for-matching-substring.pipe'; -import { GetAbbreviatedTextPipe } from 'filters/string-utility-filters/get-abbreviated-text.pipe'; -import { NormalizeWhitespacePipe } from 'filters/string-utility-filters/normalize-whitespace.pipe'; -import { NormalizeWhitespacePunctuationAndCasePipe } from 'filters/string-utility-filters/normalize-whitespace-punctuation-and-case.pipe'; -import { ReplaceInputsWithEllipsesPipe } from 'filters/string-utility-filters/replace-inputs-with-ellipses.pipe'; -import { SortByPipe } from 'filters/string-utility-filters/sort-by.pipe'; -import { TruncatePipe } from 'filters/string-utility-filters/truncate.pipe'; -import { TruncateAndCapitalizePipe } from 'filters/string-utility-filters/truncate-and-capitalize.pipe'; -import { TruncateAtFirstEllipsisPipe } from 'filters/string-utility-filters/truncate-at-first-ellipsis.pipe'; -import { TruncateAtFirstLinePipe } from 'filters/string-utility-filters/truncate-at-first-line.pipe'; -import { UnderscoresToCamelCasePipe } from 'filters/string-utility-filters/underscores-to-camel-case.pipe'; -import { WrapTextWithEllipsisPipe } from 'filters/string-utility-filters/wrap-text-with-ellipsis.pipe'; -import { TruncateInputBasedOnInteractionAnswerTypePipe } from 'filters/truncate-input-based-on-interaction-answer-type.pipe'; -import { ParameterizeRuleDescriptionPipe } from 'filters/parameterize-rule-description.pipe'; -import { ConvertUnicodeToHtml } from 'filters/convert-unicode-to-html.pipe'; +import {CamelCaseToHyphensPipe} from 'filters/string-utility-filters/camel-case-to-hyphens.pipe'; +import {CapitalizePipe} from 'filters/string-utility-filters/capitalize.pipe'; +import {ConvertToPlainTextPipe} from 'filters/string-utility-filters/convert-to-plain-text.pipe'; +import {FilterForMatchingSubstringPipe} from 'filters/string-utility-filters/filter-for-matching-substring.pipe'; +import {GetAbbreviatedTextPipe} from 'filters/string-utility-filters/get-abbreviated-text.pipe'; +import {NormalizeWhitespacePipe} from 'filters/string-utility-filters/normalize-whitespace.pipe'; +import {NormalizeWhitespacePunctuationAndCasePipe} from 'filters/string-utility-filters/normalize-whitespace-punctuation-and-case.pipe'; +import {ReplaceInputsWithEllipsesPipe} from 'filters/string-utility-filters/replace-inputs-with-ellipses.pipe'; +import {SortByPipe} from 'filters/string-utility-filters/sort-by.pipe'; +import {TruncatePipe} from 'filters/string-utility-filters/truncate.pipe'; +import {TruncateAndCapitalizePipe} from 'filters/string-utility-filters/truncate-and-capitalize.pipe'; +import {TruncateAtFirstEllipsisPipe} from 'filters/string-utility-filters/truncate-at-first-ellipsis.pipe'; +import {TruncateAtFirstLinePipe} from 'filters/string-utility-filters/truncate-at-first-line.pipe'; +import {UnderscoresToCamelCasePipe} from 'filters/string-utility-filters/underscores-to-camel-case.pipe'; +import {WrapTextWithEllipsisPipe} from 'filters/string-utility-filters/wrap-text-with-ellipsis.pipe'; +import {TruncateInputBasedOnInteractionAnswerTypePipe} from 'filters/truncate-input-based-on-interaction-answer-type.pipe'; +import {ParameterizeRuleDescriptionPipe} from 'filters/parameterize-rule-description.pipe'; +import {ConvertUnicodeToHtml} from 'filters/convert-unicode-to-html.pipe'; @NgModule({ declarations: [ @@ -56,7 +56,7 @@ import { ConvertUnicodeToHtml } from 'filters/convert-unicode-to-html.pipe'; WrapTextWithEllipsisPipe, TruncateInputBasedOnInteractionAnswerTypePipe, ParameterizeRuleDescriptionPipe, - ConvertUnicodeToHtml + ConvertUnicodeToHtml, ], exports: [ CamelCaseToHyphensPipe, @@ -76,7 +76,7 @@ import { ConvertUnicodeToHtml } from 'filters/convert-unicode-to-html.pipe'; WrapTextWithEllipsisPipe, TruncateInputBasedOnInteractionAnswerTypePipe, ParameterizeRuleDescriptionPipe, - ConvertUnicodeToHtml - ] + ConvertUnicodeToHtml, + ], }) export class StringUtilityPipesModule {} diff --git a/core/templates/filters/string-utility-filters/truncate-and-capitalize.pipe.spec.ts b/core/templates/filters/string-utility-filters/truncate-and-capitalize.pipe.spec.ts index e0e405976653..a4d596ab9ddc 100644 --- a/core/templates/filters/string-utility-filters/truncate-and-capitalize.pipe.spec.ts +++ b/core/templates/filters/string-utility-filters/truncate-and-capitalize.pipe.spec.ts @@ -16,10 +16,9 @@ * @fileoverview Tests for TruncateAndCapitalize pipe for Oppia. */ -import { TruncateAndCapitalizePipe } from - 'filters/string-utility-filters/truncate-and-capitalize.pipe'; +import {TruncateAndCapitalizePipe} from 'filters/string-utility-filters/truncate-and-capitalize.pipe'; -describe('Testing filters', function() { +describe('Testing filters', function () { let truncateAndCapitalizePipe: TruncateAndCapitalizePipe; beforeEach(() => { truncateAndCapitalizePipe = new TruncateAndCapitalizePipe(); @@ -33,39 +32,52 @@ describe('Testing filters', function() { expect(truncateAndCapitalizePipe).not.toEqual(null); }); - it('should capitalize first letter and truncate string at a word break', - () => { + it('should capitalize first letter and truncate string at a word break', () => { // The first word always appears in the result. - expect(truncateAndCapitalizePipe.transform(' remove new Line', 4)) - .toEqual('Remove...'); - expect(truncateAndCapitalizePipe.transform('remove New line', 4)) - .toEqual('Remove...'); + expect(truncateAndCapitalizePipe.transform(' remove new Line', 4)).toEqual( + 'Remove...' + ); + expect(truncateAndCapitalizePipe.transform('remove New line', 4)).toEqual( + 'Remove...' + ); - expect(truncateAndCapitalizePipe.transform('remove New line', 6)) - .toEqual('Remove...'); + expect(truncateAndCapitalizePipe.transform('remove New line', 6)).toEqual( + 'Remove...' + ); - expect(truncateAndCapitalizePipe.transform(' remove new Line', 10)) - .toEqual('Remove new...'); - expect(truncateAndCapitalizePipe.transform('remove New line', 10)) - .toEqual('Remove New...'); + expect( + truncateAndCapitalizePipe.transform(' remove new Line', 10) + ).toEqual('Remove new...'); + expect(truncateAndCapitalizePipe.transform('remove New line', 10)).toEqual( + 'Remove New...' + ); - expect(truncateAndCapitalizePipe.transform(' remove new Line', 15)) - .toEqual('Remove new Line'); - expect(truncateAndCapitalizePipe.transform('remove New line', 15)) - .toEqual('Remove New line'); + expect( + truncateAndCapitalizePipe.transform(' remove new Line', 15) + ).toEqual('Remove new Line'); + expect(truncateAndCapitalizePipe.transform('remove New line', 15)).toEqual( + 'Remove New line' + ); - // Strings starting with digits are not affected by the capitalization. - expect(truncateAndCapitalizePipe.transform(' 123456 a bc d', 12)) - .toEqual('123456 a bc...'); + // Strings starting with digits are not affected by the capitalization. + expect(truncateAndCapitalizePipe.transform(' 123456 a bc d', 12)).toEqual( + '123456 a bc...' + ); - expect(truncateAndCapitalizePipe.transform( - 'a single sentence with more than twenty one characters', 21 - )).toEqual('A single sentence...'); + expect( + truncateAndCapitalizePipe.transform( + 'a single sentence with more than twenty one characters', + 21 + ) + ).toEqual('A single sentence...'); - // If maximum characters is greater than objective length - // return whole objective. - expect(truncateAndCapitalizePipe.transform( - 'please do not test empty string', 100)).toEqual( - 'Please do not test empty string'); - }); + // If maximum characters is greater than objective length + // return whole objective. + expect( + truncateAndCapitalizePipe.transform( + 'please do not test empty string', + 100 + ) + ).toEqual('Please do not test empty string'); + }); }); diff --git a/core/templates/filters/string-utility-filters/truncate-and-capitalize.pipe.ts b/core/templates/filters/string-utility-filters/truncate-and-capitalize.pipe.ts index 5d988b642f1c..e6a871e07241 100644 --- a/core/templates/filters/string-utility-filters/truncate-and-capitalize.pipe.ts +++ b/core/templates/filters/string-utility-filters/truncate-and-capitalize.pipe.ts @@ -16,7 +16,7 @@ * @fileoverview TruncateAndCapitalize pipe for Oppia. */ -import { Pipe, PipeTransform } from '@angular/core'; +import {Pipe, PipeTransform} from '@angular/core'; // Note that this filter does not truncate at the middle of a word. @Pipe({name: 'truncateAndCapitalize'}) @@ -37,8 +37,10 @@ export class TruncateAndCapitalizePipe implements PipeTransform { // Add the remaining words to the result until the character limit is // reached. for (let i = 1; i < words.length; i++) { - if (!maxNumberOfCharacters || - result.length + 1 + words[i].length <= maxNumberOfCharacters) { + if ( + !maxNumberOfCharacters || + result.length + 1 + words[i].length <= maxNumberOfCharacters + ) { result += ' '; result += words[i]; } else { diff --git a/core/templates/filters/string-utility-filters/truncate-at-first-ellipsis.pipe.spec.ts b/core/templates/filters/string-utility-filters/truncate-at-first-ellipsis.pipe.spec.ts index e1be19d943ee..89c435768677 100644 --- a/core/templates/filters/string-utility-filters/truncate-at-first-ellipsis.pipe.spec.ts +++ b/core/templates/filters/string-utility-filters/truncate-at-first-ellipsis.pipe.spec.ts @@ -16,10 +16,9 @@ * @fileoverview Tests for TruncateAtFirstEllipsis pipe for Oppia. */ -import { TruncateAtFirstEllipsisPipe } from - 'filters/string-utility-filters/truncate-at-first-ellipsis.pipe'; +import {TruncateAtFirstEllipsisPipe} from 'filters/string-utility-filters/truncate-at-first-ellipsis.pipe'; -describe('Testing filters', function() { +describe('Testing filters', function () { let truncateAtFirstEllipsisPipe: TruncateAtFirstEllipsisPipe; beforeEach(() => { truncateAtFirstEllipsisPipe = new TruncateAtFirstEllipsisPipe(); @@ -29,13 +28,13 @@ describe('Testing filters', function() { expect(truncateAtFirstEllipsisPipe).not.toEqual(null); }); - it('should truncate a string when it first sees a \'...\'', () => { + it("should truncate a string when it first sees a '...'", () => { expect(truncateAtFirstEllipsisPipe.transform('')).toEqual(''); expect(truncateAtFirstEllipsisPipe.transform('hello')).toEqual('hello'); - expect(truncateAtFirstEllipsisPipe.transform('...')) - .toEqual(''); - expect(truncateAtFirstEllipsisPipe.transform('say ... and ...')) - .toEqual('say '); + expect(truncateAtFirstEllipsisPipe.transform('...')).toEqual(''); + expect(truncateAtFirstEllipsisPipe.transform('say ... and ...')).toEqual( + 'say ' + ); expect(truncateAtFirstEllipsisPipe.transform('... and ...')).toEqual(''); expect(truncateAtFirstEllipsisPipe.transform('{{}}...')).toEqual('{{}}'); }); diff --git a/core/templates/filters/string-utility-filters/truncate-at-first-ellipsis.pipe.ts b/core/templates/filters/string-utility-filters/truncate-at-first-ellipsis.pipe.ts index 254ce795f760..eab3cc575538 100644 --- a/core/templates/filters/string-utility-filters/truncate-at-first-ellipsis.pipe.ts +++ b/core/templates/filters/string-utility-filters/truncate-at-first-ellipsis.pipe.ts @@ -16,7 +16,7 @@ * @fileoverview TruncateAtFirstEllipsis pipe for Oppia. */ -import { Pipe, PipeTransform } from '@angular/core'; +import {Pipe, PipeTransform} from '@angular/core'; // Filter that truncates a string at the first '...'. @Pipe({name: 'truncateAtFirstEllipsis'}) @@ -27,6 +27,6 @@ export class TruncateAtFirstEllipsisPipe implements PipeTransform { return ''; } let matchLocation = input.search(this.pattern); - return matchLocation === -1 ? input : (input.substring(0, matchLocation)); + return matchLocation === -1 ? input : input.substring(0, matchLocation); } } diff --git a/core/templates/filters/string-utility-filters/truncate-at-first-line.pipe.spec.ts b/core/templates/filters/string-utility-filters/truncate-at-first-line.pipe.spec.ts index 04d4a616273a..b06f3bfe375a 100644 --- a/core/templates/filters/string-utility-filters/truncate-at-first-line.pipe.spec.ts +++ b/core/templates/filters/string-utility-filters/truncate-at-first-line.pipe.spec.ts @@ -16,10 +16,9 @@ * @fileoverview Tests for TruncateAtFirstLine filter for Oppia. */ -import { TruncateAtFirstLinePipe } from - 'filters/string-utility-filters/truncate-at-first-line.pipe'; +import {TruncateAtFirstLinePipe} from 'filters/string-utility-filters/truncate-at-first-line.pipe'; -describe('Testing filters', function() { +describe('Testing filters', function () { let truncateAtFirstLinePipe: TruncateAtFirstLinePipe; beforeEach(() => { truncateAtFirstLinePipe = new TruncateAtFirstLinePipe(); @@ -32,76 +31,99 @@ describe('Testing filters', function() { it('should truncate multi-line text to the first non-empty line', () => { expect(truncateAtFirstLinePipe.transform('')).toEqual(''); - expect(truncateAtFirstLinePipe.transform( - ' A single line with spaces at either end. ')).toEqual( - ' A single line with spaces at either end. '); + expect( + truncateAtFirstLinePipe.transform( + ' A single line with spaces at either end. ' + ) + ).toEqual(' A single line with spaces at either end. '); expect(truncateAtFirstLinePipe.transform('a\nb\nc')).toEqual('a...'); - expect(truncateAtFirstLinePipe.transform( - 'Removes newline at end\n')).toEqual('Removes newline at end'); - expect(truncateAtFirstLinePipe.transform('\nRemoves newline at beginning.')) - .toEqual( - 'Removes newline at beginning.'); + expect( + truncateAtFirstLinePipe.transform('Removes newline at end\n') + ).toEqual('Removes newline at end'); + expect( + truncateAtFirstLinePipe.transform('\nRemoves newline at beginning.') + ).toEqual('Removes newline at beginning.'); expect(truncateAtFirstLinePipe.transform('\n')).toEqual(''); expect(truncateAtFirstLinePipe.transform('\n\n\n')).toEqual(''); // ---- Windows ---- - expect(truncateAtFirstLinePipe.transform( - 'Single line\r\nWindows EOL')).toEqual('Single line...'); - expect(truncateAtFirstLinePipe.transform( - 'Single line\u000D\u000AEOL')).toEqual('Single line...'); - expect(truncateAtFirstLinePipe.transform( - 'Single line\x0D\x0AEOL')).toEqual('Single line...'); - expect(truncateAtFirstLinePipe.transform( - 'Single line\u000D\x0AEOL')).toEqual('Single line...'); - expect(truncateAtFirstLinePipe.transform( - 'Single line\x0D\u000AEOL')).toEqual('Single line...'); + expect( + truncateAtFirstLinePipe.transform('Single line\r\nWindows EOL') + ).toEqual('Single line...'); + expect( + truncateAtFirstLinePipe.transform('Single line\u000D\u000AEOL') + ).toEqual('Single line...'); + expect(truncateAtFirstLinePipe.transform('Single line\x0D\x0AEOL')).toEqual( + 'Single line...' + ); + expect( + truncateAtFirstLinePipe.transform('Single line\u000D\x0AEOL') + ).toEqual('Single line...'); + expect( + truncateAtFirstLinePipe.transform('Single line\x0D\u000AEOL') + ).toEqual('Single line...'); // ---- Mac ---- - expect(truncateAtFirstLinePipe.transform( - 'Single line\rEOL')).toEqual('Single line...'); - expect(truncateAtFirstLinePipe.transform( - 'Single line\u000DEOL')).toEqual('Single line...'); - expect(truncateAtFirstLinePipe.transform( - 'Single line\x0DEOL')).toEqual('Single line...'); + expect(truncateAtFirstLinePipe.transform('Single line\rEOL')).toEqual( + 'Single line...' + ); + expect(truncateAtFirstLinePipe.transform('Single line\u000DEOL')).toEqual( + 'Single line...' + ); + expect(truncateAtFirstLinePipe.transform('Single line\x0DEOL')).toEqual( + 'Single line...' + ); // ---- Linux ---- - expect(truncateAtFirstLinePipe.transform( - 'Single line\nEOL')).toEqual('Single line...'); - expect(truncateAtFirstLinePipe.transform( - 'Single line\u000AEOL')).toEqual('Single line...'); - expect(truncateAtFirstLinePipe.transform( - 'Single line\x0AEOL')).toEqual('Single line...'); + expect(truncateAtFirstLinePipe.transform('Single line\nEOL')).toEqual( + 'Single line...' + ); + expect(truncateAtFirstLinePipe.transform('Single line\u000AEOL')).toEqual( + 'Single line...' + ); + expect(truncateAtFirstLinePipe.transform('Single line\x0AEOL')).toEqual( + 'Single line...' + ); // ---- Vertical Tab ---- - expect(truncateAtFirstLinePipe.transform( - 'Vertical Tab\vEOL')).toEqual('Vertical Tab...'); - expect(truncateAtFirstLinePipe.transform( - 'Vertical Tab\u000BEOL')).toEqual('Vertical Tab...'); - expect(truncateAtFirstLinePipe.transform( - 'Vertical Tab\x0BEOL')).toEqual('Vertical Tab...'); + expect(truncateAtFirstLinePipe.transform('Vertical Tab\vEOL')).toEqual( + 'Vertical Tab...' + ); + expect(truncateAtFirstLinePipe.transform('Vertical Tab\u000BEOL')).toEqual( + 'Vertical Tab...' + ); + expect(truncateAtFirstLinePipe.transform('Vertical Tab\x0BEOL')).toEqual( + 'Vertical Tab...' + ); // ---- Form Feed ---- - expect(truncateAtFirstLinePipe.transform( - 'Form Feed\fEOL')).toEqual('Form Feed...'); - expect(truncateAtFirstLinePipe.transform( - 'Form Feed\u000CEOL')).toEqual('Form Feed...'); - expect(truncateAtFirstLinePipe.transform( - 'Form Feed\x0CEOL')).toEqual('Form Feed...'); + expect(truncateAtFirstLinePipe.transform('Form Feed\fEOL')).toEqual( + 'Form Feed...' + ); + expect(truncateAtFirstLinePipe.transform('Form Feed\u000CEOL')).toEqual( + 'Form Feed...' + ); + expect(truncateAtFirstLinePipe.transform('Form Feed\x0CEOL')).toEqual( + 'Form Feed...' + ); // ---- Next Line ---- - expect(truncateAtFirstLinePipe.transform( - 'Next Line\u0085EOL')).toEqual('Next Line...'); - expect(truncateAtFirstLinePipe.transform( - 'Next Line\x85EOL')).toEqual('Next Line...'); + expect(truncateAtFirstLinePipe.transform('Next Line\u0085EOL')).toEqual( + 'Next Line...' + ); + expect(truncateAtFirstLinePipe.transform('Next Line\x85EOL')).toEqual( + 'Next Line...' + ); // ---- Line Separator ---- - expect(truncateAtFirstLinePipe.transform( - 'Line Separator\u2028EOL')).toEqual('Line Separator...'); + expect( + truncateAtFirstLinePipe.transform('Line Separator\u2028EOL') + ).toEqual('Line Separator...'); // ---- Paragraph Separator ---- - expect(truncateAtFirstLinePipe.transform( - 'Paragraph Separator\u2029EOL')).toEqual( - 'Paragraph Separator...'); + expect( + truncateAtFirstLinePipe.transform('Paragraph Separator\u2029EOL') + ).toEqual('Paragraph Separator...'); }); }); diff --git a/core/templates/filters/string-utility-filters/truncate-at-first-line.pipe.ts b/core/templates/filters/string-utility-filters/truncate-at-first-line.pipe.ts index ec6878e6d7e7..2f2da5440d39 100644 --- a/core/templates/filters/string-utility-filters/truncate-at-first-line.pipe.ts +++ b/core/templates/filters/string-utility-filters/truncate-at-first-line.pipe.ts @@ -16,7 +16,7 @@ * @fileoverview TruncateAtFirstLine pipe for Oppia. */ -import { Pipe, PipeTransform } from '@angular/core'; +import {Pipe, PipeTransform} from '@angular/core'; @Pipe({name: 'truncateAtFirstLine'}) export class TruncateAtFirstLinePipe implements PipeTransform { @@ -41,8 +41,8 @@ export class TruncateAtFirstLinePipe implements PipeTransform { } } let suffix = otherNonemptyLinesExist ? '...' : ''; - return ( - firstNonemptyLineIndex !== -1 ? - lines[firstNonemptyLineIndex] + suffix : ''); + return firstNonemptyLineIndex !== -1 + ? lines[firstNonemptyLineIndex] + suffix + : ''; } } diff --git a/core/templates/filters/string-utility-filters/truncate.pipe.spec.ts b/core/templates/filters/string-utility-filters/truncate.pipe.spec.ts index fc6d94825f89..10784c85011a 100644 --- a/core/templates/filters/string-utility-filters/truncate.pipe.spec.ts +++ b/core/templates/filters/string-utility-filters/truncate.pipe.spec.ts @@ -16,11 +16,10 @@ * @fileoverview Tests for Truncate pipe for Oppia. */ -import { TruncatePipe } from 'filters/string-utility-filters/truncate.pipe'; -import { ConvertToPlainTextPipe } from - 'filters/string-utility-filters/convert-to-plain-text.pipe'; +import {TruncatePipe} from 'filters/string-utility-filters/truncate.pipe'; +import {ConvertToPlainTextPipe} from 'filters/string-utility-filters/convert-to-plain-text.pipe'; -describe('Testing filters', function() { +describe('Testing filters', function () { let truncatePipe: TruncatePipe; beforeEach(() => { truncatePipe = new TruncatePipe(new ConvertToPlainTextPipe()); @@ -32,13 +31,12 @@ describe('Testing filters', function() { it('should correctly truncate', () => { expect(truncatePipe.transform('testcool', 7)).toBe('test...'); - expect(truncatePipe.transform(Array(80).join('a'), NaN)) - .toBe(Array(68).join('a') + '...'); + expect(truncatePipe.transform(Array(80).join('a'), NaN)).toBe( + Array(68).join('a') + '...' + ); expect(truncatePipe.transform('HelloWorld', 10)).toBe('HelloWorld'); - expect(truncatePipe.transform('HelloWorld', 8, 'contd')) - .toBe('Helcontd'); + expect(truncatePipe.transform('HelloWorld', 8, 'contd')).toBe('Helcontd'); expect(truncatePipe.transform('', 10)).toBe(''); - expect(truncatePipe.transform((12345678), 7)) - .toBe('1234...'); + expect(truncatePipe.transform(12345678, 7)).toBe('1234...'); }); }); diff --git a/core/templates/filters/string-utility-filters/truncate.pipe.ts b/core/templates/filters/string-utility-filters/truncate.pipe.ts index 97b94c376cd3..3fbc625ab148 100644 --- a/core/templates/filters/string-utility-filters/truncate.pipe.ts +++ b/core/templates/filters/string-utility-filters/truncate.pipe.ts @@ -16,13 +16,13 @@ * @fileoverview Truncate filter for Oppia. */ -import { Injectable, Pipe, PipeTransform } from '@angular/core'; -import { ConvertToPlainTextPipe } from './convert-to-plain-text.pipe'; +import {Injectable, Pipe, PipeTransform} from '@angular/core'; +import {ConvertToPlainTextPipe} from './convert-to-plain-text.pipe'; // Pipe that truncates long descriptors. @Pipe({name: 'truncate'}) @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class TruncatePipe implements PipeTransform { constructor(private convertToPlainTextPipe: ConvertToPlainTextPipe) {} @@ -41,8 +41,8 @@ export class TruncatePipe implements PipeTransform { input = String(input); } input = this.convertToPlainTextPipe.transform(input); - return ( - input.length <= length ? input : ( - input.substring(0, length - suffix.length) + suffix)); + return input.length <= length + ? input + : input.substring(0, length - suffix.length) + suffix; } } diff --git a/core/templates/filters/string-utility-filters/underscores-to-camel-case.pipe.spec.ts b/core/templates/filters/string-utility-filters/underscores-to-camel-case.pipe.spec.ts index 6fe4a4de25bc..7038aedcb1c7 100644 --- a/core/templates/filters/string-utility-filters/underscores-to-camel-case.pipe.spec.ts +++ b/core/templates/filters/string-utility-filters/underscores-to-camel-case.pipe.spec.ts @@ -16,10 +16,9 @@ * @fileoverview Tests for UnderscoresToCamelCasePipe for Oppia. */ -import { UnderscoresToCamelCasePipe } from - 'filters/string-utility-filters/underscores-to-camel-case.pipe'; +import {UnderscoresToCamelCasePipe} from 'filters/string-utility-filters/underscores-to-camel-case.pipe'; -describe('Testing filters', function() { +describe('Testing filters', function () { let underscoresToCamelCase: UnderscoresToCamelCasePipe; beforeEach(() => { underscoresToCamelCase = new UnderscoresToCamelCasePipe(); @@ -33,17 +32,21 @@ describe('Testing filters', function() { expect(underscoresToCamelCase.transform('Test')).toEqual('Test'); expect(underscoresToCamelCase.transform('test')).toEqual('test'); expect(underscoresToCamelCase.transform('test_app')).toEqual('testApp'); - expect(underscoresToCamelCase.transform('Test_App_Two')) - .toEqual('TestAppTwo'); - expect(underscoresToCamelCase.transform('test_App_Two')) - .toEqual('testAppTwo'); - expect(underscoresToCamelCase.transform('test_app_two')) - .toEqual('testAppTwo'); + expect(underscoresToCamelCase.transform('Test_App_Two')).toEqual( + 'TestAppTwo' + ); + expect(underscoresToCamelCase.transform('test_App_Two')).toEqual( + 'testAppTwo' + ); + expect(underscoresToCamelCase.transform('test_app_two')).toEqual( + 'testAppTwo' + ); expect(underscoresToCamelCase.transform('test__App')).toEqual('testApp'); // Trailing underscores at the beginning and end should never happen -- // they will give weird results. expect(underscoresToCamelCase.transform('_test_App')).toEqual('TestApp'); - expect(underscoresToCamelCase.transform('__Test_ App_')) - .toEqual('Test App_'); + expect(underscoresToCamelCase.transform('__Test_ App_')).toEqual( + 'Test App_' + ); }); }); diff --git a/core/templates/filters/string-utility-filters/underscores-to-camel-case.pipe.ts b/core/templates/filters/string-utility-filters/underscores-to-camel-case.pipe.ts index 9d0a25a70603..3bdcc0fdea5f 100644 --- a/core/templates/filters/string-utility-filters/underscores-to-camel-case.pipe.ts +++ b/core/templates/filters/string-utility-filters/underscores-to-camel-case.pipe.ts @@ -16,12 +16,12 @@ * @fileoverview UnderscoresToCamelCase pipe for Oppia. */ -import { Pipe, PipeTransform } from '@angular/core'; +import {Pipe, PipeTransform} from '@angular/core'; @Pipe({name: 'underscoresToCamelCase'}) export class UnderscoresToCamelCasePipe implements PipeTransform { transform(input: string): string { - return input.replace(/_+(.)/g, function(match, group1) { + return input.replace(/_+(.)/g, function (match, group1) { return group1.toUpperCase(); }); } diff --git a/core/templates/filters/string-utility-filters/wrap-text-with-ellipsis.pipe.spec.ts b/core/templates/filters/string-utility-filters/wrap-text-with-ellipsis.pipe.spec.ts index d66c4d754e6f..48a680d0fa2b 100644 --- a/core/templates/filters/string-utility-filters/wrap-text-with-ellipsis.pipe.spec.ts +++ b/core/templates/filters/string-utility-filters/wrap-text-with-ellipsis.pipe.spec.ts @@ -16,18 +16,16 @@ * @fileoverview Tests for the WrapTextWithEllipsisPipe for Oppia. */ -import { TestBed } from '@angular/core/testing'; +import {TestBed} from '@angular/core/testing'; -import { NormalizeWhitespacePipe } from - 'filters/string-utility-filters/normalize-whitespace.pipe'; -import { WrapTextWithEllipsisPipe } from - 'filters/string-utility-filters/wrap-text-with-ellipsis.pipe'; +import {NormalizeWhitespacePipe} from 'filters/string-utility-filters/normalize-whitespace.pipe'; +import {WrapTextWithEllipsisPipe} from 'filters/string-utility-filters/wrap-text-with-ellipsis.pipe'; -describe('Testing filters', function() { +describe('Testing filters', function () { let wrapTextWithEllipsis: WrapTextWithEllipsisPipe; beforeEach(() => { TestBed.configureTestingModule({ - providers: [WrapTextWithEllipsisPipe, NormalizeWhitespacePipe] + providers: [WrapTextWithEllipsisPipe, NormalizeWhitespacePipe], }); wrapTextWithEllipsis = TestBed.get(WrapTextWithEllipsisPipe); @@ -40,17 +38,20 @@ describe('Testing filters', function() { expect(wrapTextWithEllipsis.transform('testing', 3)).toEqual('...'); expect(wrapTextWithEllipsis.transform('testing', 4)).toEqual('t...'); expect(wrapTextWithEllipsis.transform('testing', 7)).toEqual('testing'); - expect(wrapTextWithEllipsis.transform( - 'Long sentence which goes on and on.', 80)).toEqual( - 'Long sentence which goes on and on.'); - expect(wrapTextWithEllipsis.transform( - 'Long sentence which goes on and on.', 20)).toEqual( - 'Long sentence whi...'); - expect(wrapTextWithEllipsis.transform( - 'Sentence with long spacing.', 20)).toEqual( - 'Sentence with lon...'); - expect(wrapTextWithEllipsis.transform( - 'With space before ellipsis.', 21)).toEqual( - 'With space before...'); + expect( + wrapTextWithEllipsis.transform('Long sentence which goes on and on.', 80) + ).toEqual('Long sentence which goes on and on.'); + expect( + wrapTextWithEllipsis.transform('Long sentence which goes on and on.', 20) + ).toEqual('Long sentence whi...'); + expect( + wrapTextWithEllipsis.transform( + 'Sentence with long spacing.', + 20 + ) + ).toEqual('Sentence with lon...'); + expect( + wrapTextWithEllipsis.transform('With space before ellipsis.', 21) + ).toEqual('With space before...'); }); }); diff --git a/core/templates/filters/string-utility-filters/wrap-text-with-ellipsis.pipe.ts b/core/templates/filters/string-utility-filters/wrap-text-with-ellipsis.pipe.ts index 7fc9f88743cc..7cf3b3bde76c 100644 --- a/core/templates/filters/string-utility-filters/wrap-text-with-ellipsis.pipe.ts +++ b/core/templates/filters/string-utility-filters/wrap-text-with-ellipsis.pipe.ts @@ -16,17 +16,17 @@ * @fileoverview WrapTextWithEllipsis filter for Oppia. */ -import { Pipe, PipeTransform } from '@angular/core'; +import {Pipe, PipeTransform} from '@angular/core'; -import { NormalizeWhitespacePipe } from - 'filters/string-utility-filters/normalize-whitespace.pipe'; -import { UtilsService } from 'services/utils.service'; +import {NormalizeWhitespacePipe} from 'filters/string-utility-filters/normalize-whitespace.pipe'; +import {UtilsService} from 'services/utils.service'; @Pipe({name: 'wrapTextWithEllipsis'}) export class WrapTextWithEllipsisPipe implements PipeTransform { constructor( private utilsService: UtilsService, - private normalizeWhitespacePipe: NormalizeWhitespacePipe) {} + private normalizeWhitespacePipe: NormalizeWhitespacePipe + ) {} transform(input: string, characterCount: number): string { if (this.utilsService.isString(input)) { diff --git a/core/templates/filters/summarize-nonnegative-number.pipe.ts b/core/templates/filters/summarize-nonnegative-number.pipe.ts index 82ae81439b80..1e0f51dead0e 100644 --- a/core/templates/filters/summarize-nonnegative-number.pipe.ts +++ b/core/templates/filters/summarize-nonnegative-number.pipe.ts @@ -15,7 +15,7 @@ /** * @fileoverview SummarizeNonnegativeNumber pipe for Oppia. */ -import { Pipe, PipeTransform } from '@angular/core'; +import {Pipe, PipeTransform} from '@angular/core'; // Filter that summarizes a large number to a decimal followed by // the appropriate metric prefix (K, M or B). For example, 167656 @@ -28,10 +28,12 @@ export class SummarizeNonnegativeNumberPipe implements PipeTransform { // Six zeros for millions (e.g. 146008788 --> 146.0M). // Three zeros for thousands (e.g. 146008 --> 146.0K). // No change for small numbers (e.g. 12 --> 12). - return ( - input >= 1.0e+9 ? (input / 1.0e+9).toFixed(1) + 'B' : - input >= 1.0e+6 ? (input / 1.0e+6).toFixed(1) + 'M' : - input >= 1.0e+3 ? (input / 1.0e+3).toFixed(1) + 'K' : - input); + return input >= 1.0e9 + ? (input / 1.0e9).toFixed(1) + 'B' + : input >= 1.0e6 + ? (input / 1.0e6).toFixed(1) + 'M' + : input >= 1.0e3 + ? (input / 1.0e3).toFixed(1) + 'K' + : input; } } diff --git a/core/templates/filters/truncate-input-based-on-interaction-answer-type.pipe.spec.ts b/core/templates/filters/truncate-input-based-on-interaction-answer-type.pipe.spec.ts index 86dd3e361931..21f9da15c429 100644 --- a/core/templates/filters/truncate-input-based-on-interaction-answer-type.pipe.spec.ts +++ b/core/templates/filters/truncate-input-based-on-interaction-answer-type.pipe.spec.ts @@ -17,16 +17,17 @@ * Pipe for Oppia. */ -import { TruncateInputBasedOnInteractionAnswerTypePipe } from './truncate-input-based-on-interaction-answer-type.pipe'; -import { TruncatePipe } from 'filters/string-utility-filters/truncate.pipe'; -import { ConvertToPlainTextPipe } from 'filters/string-utility-filters/convert-to-plain-text.pipe'; +import {TruncateInputBasedOnInteractionAnswerTypePipe} from './truncate-input-based-on-interaction-answer-type.pipe'; +import {TruncatePipe} from 'filters/string-utility-filters/truncate.pipe'; +import {ConvertToPlainTextPipe} from 'filters/string-utility-filters/convert-to-plain-text.pipe'; describe('Testing TruncateInputBasedOnInteractionAnswerTypePipe', () => { let pipe: TruncateInputBasedOnInteractionAnswerTypePipe; beforeEach(() => { pipe = new TruncateInputBasedOnInteractionAnswerTypePipe( - new TruncatePipe(new ConvertToPlainTextPipe())); + new TruncatePipe(new ConvertToPlainTextPipe()) + ); }); it('should correctly truncate input data', () => { @@ -37,10 +38,10 @@ describe('Testing TruncateInputBasedOnInteractionAnswerTypePipe', () => { error: 'error', }; - expect(pipe.transform('Hey oppia users!', 'TextInput', 8)) - .toBe('Hey o...'); - expect(pipe.transform(data, 'CodeRepl', 8)) - .toBe('Hey o...'); + expect(pipe.transform('Hey oppia users!', 'TextInput', 8)).toBe( + 'Hey o...' + ); + expect(pipe.transform(data, 'CodeRepl', 8)).toBe('Hey o...'); expect(() => { pipe.transform(data, 'ImageClickInput', 8); }).toThrowError('Unknown interaction answer type'); diff --git a/core/templates/filters/truncate-input-based-on-interaction-answer-type.pipe.ts b/core/templates/filters/truncate-input-based-on-interaction-answer-type.pipe.ts index cb1b77e13edc..210b4f94f753 100644 --- a/core/templates/filters/truncate-input-based-on-interaction-answer-type.pipe.ts +++ b/core/templates/filters/truncate-input-based-on-interaction-answer-type.pipe.ts @@ -16,23 +16,24 @@ * @fileoverview TruncateInputBasedOnInteractionAnswerType Pipe for Oppia. */ -import { Pipe, PipeTransform } from '@angular/core'; +import {Pipe, PipeTransform} from '@angular/core'; import INTERACTION_SPECS from 'interactions/interaction_specs.json'; -import { TruncatePipe } from 'filters/string-utility-filters/truncate.pipe'; -import { InteractionAnswer } from 'interactions/answer-defs'; +import {TruncatePipe} from 'filters/string-utility-filters/truncate.pipe'; +import {InteractionAnswer} from 'interactions/answer-defs'; @Pipe({ - name: 'truncateInputBasedOnInteractionAnswerTypePipe' + name: 'truncateInputBasedOnInteractionAnswerTypePipe', }) export class TruncateInputBasedOnInteractionAnswerTypePipe - implements PipeTransform { - constructor( - private truncatePipe: TruncatePipe, - ) { } + implements PipeTransform +{ + constructor(private truncatePipe: TruncatePipe) {} transform( - input: InteractionAnswer, - interactionId: string, length: number): string { + input: InteractionAnswer, + interactionId: string, + length: number + ): string { let answerType = INTERACTION_SPECS[interactionId].answer_type; let actualInputToTruncate = ''; let inputUpdate; @@ -46,9 +47,9 @@ export class TruncateInputBasedOnInteractionAnswerTypePipe // we can do this in later stage. // For now i am using the if block logic to do the task. // by doing so we don't need to change this in whole codebase. - if (typeof (input) !== 'object') { + if (typeof input !== 'object') { inputUpdate = { - code: input + code: input, }; } else { inputUpdate = input; diff --git a/core/templates/hybrid-router-module-provider.spec.ts b/core/templates/hybrid-router-module-provider.spec.ts index 73428f4824e6..bc55ac414f23 100644 --- a/core/templates/hybrid-router-module-provider.spec.ts +++ b/core/templates/hybrid-router-module-provider.spec.ts @@ -16,19 +16,19 @@ * @fileoverview Unit tests for hybrid router module provider. */ -import { Component } from '@angular/core'; -import { fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; +import {Component} from '@angular/core'; +import {fakeAsync, TestBed, tick, waitForAsync} from '@angular/core/testing'; +import {RouterTestingModule} from '@angular/router/testing'; -import { SmartRouterLink } from 'hybrid-router-module-provider'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import {SmartRouterLink} from 'hybrid-router-module-provider'; +import {WindowRef} from 'services/contextual/window-ref.service'; @Component({ selector: 'mock-comp-a', // As template is only used for testing we ignore the lint check, otherwise, // we would have to create HTML files for each mock component. // eslint-disable-next-line angular/no-inline-template - template: '' + template: '', }) class MockCompA {} @@ -37,9 +37,8 @@ class MockCompA {} // As template is only used for testing we ignore the lint check, otherwise, // we would have to create HTML files for each mock component. // eslint-disable-next-line angular/no-inline-template - template: ( - '' - ) + template: + '', }) class MockCompB {} @@ -48,10 +47,9 @@ class MockCompB {} // As template is only used for testing we ignore the lint check, otherwise, // we would have to create HTML files for each mock component. // eslint-disable-next-line angular/no-inline-template - template: ( + template: '' + - '' - ) + '', }) class MockCompC {} @@ -60,7 +58,7 @@ class MockCompC {} // As template is only used for testing we ignore the lint check, otherwise, // we would have to create HTML files for each mock component. // eslint-disable-next-line angular/no-inline-template - template: '' + template: '', }) class MockCompD {} @@ -69,16 +67,15 @@ class MockCompD {} // As template is only used for testing we ignore the lint check, otherwise, // we would have to create HTML files for each mock component. // eslint-disable-next-line angular/no-inline-template - template: ( - '' - ) + template: + '', }) class MockCompE {} class MockWindowRef { nativeWindow = { location: { - href: '' + href: '', }, }; } @@ -91,11 +88,11 @@ describe('Smart router link directive', () => { TestBed.configureTestingModule({ imports: [ RouterTestingModule.withRoutes([ - { path: 'contact', component: MockCompA }, - { path: 'contact', component: MockCompB }, - { path: 'contact', component: MockCompC }, - { path: '', component: MockCompD }, - { path: '', component: MockCompE }, + {path: 'contact', component: MockCompA}, + {path: 'contact', component: MockCompB}, + {path: 'contact', component: MockCompC}, + {path: '', component: MockCompD}, + {path: '', component: MockCompE}, ]), ], declarations: [ @@ -109,16 +106,16 @@ describe('Smart router link directive', () => { providers: [ { provide: WindowRef, - useValue: mockWindowRef - } - ] + useValue: mockWindowRef, + }, + ], }).compileComponents(); })); it('should navigate by refreshing from non-router page', fakeAsync(() => { let mockCompAFixture = TestBed.createComponent(MockCompA); - let mockCompALink = ( - mockCompAFixture.debugElement.nativeElement.querySelector('a')); + let mockCompALink = + mockCompAFixture.debugElement.nativeElement.querySelector('a'); mockCompAFixture.detectChanges(); tick(); @@ -131,79 +128,66 @@ describe('Smart router link directive', () => { expect(mockWindowRef.nativeWindow.location.href).toBe('/contact'); })); - it( - 'should navigate without refreshing inside normal router', - fakeAsync(() => { - let mockCompBFixture = TestBed.createComponent(MockCompB); - let mockCompBLink = ( - mockCompBFixture.debugElement.nativeElement.querySelector('a')); - - mockCompBFixture.detectChanges(); - tick(); - expect(mockWindowRef.nativeWindow.location.href).toBe(''); - - mockCompBLink.click(); - mockCompBFixture.detectChanges(); - tick(); - - expect(mockWindowRef.nativeWindow.location.href).toBe(''); - }) - ); - - it( - 'should navigate by refreshing from lightweight router to normal router', - fakeAsync(() => { - let mockCompCFixture = TestBed.createComponent(MockCompC); - let mockCompCLink = ( - mockCompCFixture.debugElement.nativeElement.querySelector('a')); - mockCompCFixture.detectChanges(); - tick(); - expect(mockWindowRef.nativeWindow.location.href).toBe(''); - - mockCompCLink.click(); - mockCompCFixture.detectChanges(); - tick(); - - expect(mockWindowRef.nativeWindow.location.href).toBe('/contact'); - }) - ); - - it( - 'should navigate by refreshing from normal router to lightweight router', - fakeAsync(() => { - let mockCompDFixture = TestBed.createComponent(MockCompD); - let mockCompDLink = ( - mockCompDFixture.debugElement.nativeElement.querySelector('a')); - - mockCompDFixture.detectChanges(); - tick(); - expect(mockWindowRef.nativeWindow.location.href).toBe(''); - - mockCompDLink.click(); - mockCompDFixture.detectChanges(); - tick(); - - expect(mockWindowRef.nativeWindow.location.href).toBe('/'); - }) - ); - - - it( - 'should navigate without refreshing inside lightweight router', - fakeAsync(() => { - let mockCompEFixture = TestBed.createComponent(MockCompE); - let mockCompELink = ( - mockCompEFixture.debugElement.nativeElement.querySelector('a')); - - mockCompEFixture.detectChanges(); - tick(); - expect(mockWindowRef.nativeWindow.location.href).toBe(''); - - mockCompELink.click(); - mockCompEFixture.detectChanges(); - tick(); - - expect(mockWindowRef.nativeWindow.location.href).toBe(''); - }) - ); + it('should navigate without refreshing inside normal router', fakeAsync(() => { + let mockCompBFixture = TestBed.createComponent(MockCompB); + let mockCompBLink = + mockCompBFixture.debugElement.nativeElement.querySelector('a'); + + mockCompBFixture.detectChanges(); + tick(); + expect(mockWindowRef.nativeWindow.location.href).toBe(''); + + mockCompBLink.click(); + mockCompBFixture.detectChanges(); + tick(); + + expect(mockWindowRef.nativeWindow.location.href).toBe(''); + })); + + it('should navigate by refreshing from lightweight router to normal router', fakeAsync(() => { + let mockCompCFixture = TestBed.createComponent(MockCompC); + let mockCompCLink = + mockCompCFixture.debugElement.nativeElement.querySelector('a'); + mockCompCFixture.detectChanges(); + tick(); + expect(mockWindowRef.nativeWindow.location.href).toBe(''); + + mockCompCLink.click(); + mockCompCFixture.detectChanges(); + tick(); + + expect(mockWindowRef.nativeWindow.location.href).toBe('/contact'); + })); + + it('should navigate by refreshing from normal router to lightweight router', fakeAsync(() => { + let mockCompDFixture = TestBed.createComponent(MockCompD); + let mockCompDLink = + mockCompDFixture.debugElement.nativeElement.querySelector('a'); + + mockCompDFixture.detectChanges(); + tick(); + expect(mockWindowRef.nativeWindow.location.href).toBe(''); + + mockCompDLink.click(); + mockCompDFixture.detectChanges(); + tick(); + + expect(mockWindowRef.nativeWindow.location.href).toBe('/'); + })); + + it('should navigate without refreshing inside lightweight router', fakeAsync(() => { + let mockCompEFixture = TestBed.createComponent(MockCompE); + let mockCompELink = + mockCompEFixture.debugElement.nativeElement.querySelector('a'); + + mockCompEFixture.detectChanges(); + tick(); + expect(mockWindowRef.nativeWindow.location.href).toBe(''); + + mockCompELink.click(); + mockCompEFixture.detectChanges(); + tick(); + + expect(mockWindowRef.nativeWindow.location.href).toBe(''); + })); }); diff --git a/core/templates/hybrid-router-module-provider.ts b/core/templates/hybrid-router-module-provider.ts index e2f1944adc37..92d90da2bcbd 100644 --- a/core/templates/hybrid-router-module-provider.ts +++ b/core/templates/hybrid-router-module-provider.ts @@ -17,25 +17,30 @@ * in components which registered both on hybrid and angular pages. */ -import { Directive, HostListener, Input, NgModule } from '@angular/core'; -import { LocationStrategy } from '@angular/common'; -import { RouterModule, RouterLinkWithHref, Router, ActivatedRoute } from '@angular/router'; +import {Directive, HostListener, Input, NgModule} from '@angular/core'; +import {LocationStrategy} from '@angular/common'; +import { + RouterModule, + RouterLinkWithHref, + Router, + ActivatedRoute, +} from '@angular/router'; -import { AppConstants } from 'app.constants'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import {AppConstants} from 'app.constants'; +import {WindowRef} from 'services/contextual/window-ref.service'; // TODO(#13443): Remove hybrid router module provider once all pages are // migrated to angular router. @Directive({ - selector: '[smartRouterLink]' + selector: '[smartRouterLink]', }) export class SmartRouterLink extends RouterLinkWithHref { constructor( - router: Router, - route: ActivatedRoute, - locationStrategy: LocationStrategy, - private windowRef: WindowRef + router: Router, + route: ActivatedRoute, + locationStrategy: LocationStrategy, + private windowRef: WindowRef ) { super(router, route, locationStrategy); } @@ -50,24 +55,23 @@ export class SmartRouterLink extends RouterLinkWithHref { '$event.ctrlKey', '$event.shiftKey', '$event.altKey', - '$event.metaKey' + '$event.metaKey', ]) onClick( - button: number, - ctrlKey: boolean, - shiftKey: boolean, - altKey: boolean, - metaKey: boolean + button: number, + ctrlKey: boolean, + shiftKey: boolean, + altKey: boolean, + metaKey: boolean ): boolean { let bodyContent = window.document.querySelector('body'); - let currentPageIsInRouter = ( + let currentPageIsInRouter = // eslint-disable-next-line oppia/no-inner-html - bodyContent && bodyContent.innerHTML.includes('')); - let currentPageIsInLightweightRouter = ( + bodyContent && bodyContent.innerHTML.includes(''); + let currentPageIsInLightweightRouter = bodyContent && // eslint-disable-next-line oppia/no-inner-html - bodyContent.innerHTML.includes('') - ); + bodyContent.innerHTML.includes(''); if (!currentPageIsInRouter && !currentPageIsInLightweightRouter) { this.windowRef.nativeWindow.location.href = this.urlTree.toString(); return false; @@ -75,9 +79,9 @@ export class SmartRouterLink extends RouterLinkWithHref { let lightweightRouterPagesRoutes = []; let routerPagesRoutes = []; - for ( - let page of Object.values(AppConstants.PAGES_REGISTERED_WITH_FRONTEND) - ) { + for (let page of Object.values( + AppConstants.PAGES_REGISTERED_WITH_FRONTEND + )) { let routeRegex = '^'; for (let partOfRoute of page.ROUTE.split('/')) { if (partOfRoute.startsWith(':')) { @@ -117,14 +121,8 @@ export class SmartRouterLink extends RouterLinkWithHref { } @NgModule({ - imports: [ - RouterModule - ], - declarations: [ - SmartRouterLink - ], - exports: [ - SmartRouterLink - ] + imports: [RouterModule], + declarations: [SmartRouterLink], + exports: [SmartRouterLink], }) export class SmartRouterModule {} diff --git a/core/templates/i18n/i18n.module.ts b/core/templates/i18n/i18n.module.ts index 34aa72d38dbd..44159f15cc80 100644 --- a/core/templates/i18n/i18n.module.ts +++ b/core/templates/i18n/i18n.module.ts @@ -16,22 +16,30 @@ * @fileoverview Module for the Internationalization. */ -import { NgModule } from '@angular/core'; +import {NgModule} from '@angular/core'; // eslint-disable-next-line oppia/disallow-httpclient -import { HttpClient } from '@angular/common/http'; -import { TranslateCacheModule, TranslateCacheService, - TranslateCacheSettings } from 'ngx-translate-cache'; -import { MissingTranslationHandler, TranslateDefaultParser, - TranslateLoader, TranslateModule, TranslateParser, - TranslateService } from '@ngx-translate/core'; +import {HttpClient} from '@angular/common/http'; +import { + TranslateCacheModule, + TranslateCacheService, + TranslateCacheSettings, +} from 'ngx-translate-cache'; +import { + MissingTranslationHandler, + TranslateDefaultParser, + TranslateLoader, + TranslateModule, + TranslateParser, + TranslateService, +} from '@ngx-translate/core'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { TranslateLoaderFactory } from 'i18n/translate-loader.factory'; -import { TranslateCacheFactory } from 'i18n/translate-cache.factory'; -import { TranslateCustomParser } from 'i18n/translate-custom-parser'; -import { MissingTranslationCustomHandler } from 'i18n/missing-translation-custom-handler'; -import { AppConstants } from 'app.constants'; -import { I18nService } from './i18n.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {TranslateLoaderFactory} from 'i18n/translate-loader.factory'; +import {TranslateCacheFactory} from 'i18n/translate-cache.factory'; +import {TranslateCustomParser} from 'i18n/translate-custom-parser'; +import {MissingTranslationCustomHandler} from 'i18n/missing-translation-custom-handler'; +import {AppConstants} from 'app.constants'; +import {I18nService} from './i18n.service'; /** * The Translate Module will look for translations in the following order: @@ -48,7 +56,7 @@ import { I18nService } from './i18n.service'; defaultLanguage: AppConstants.DEFAULT_LANGUAGE_CODE, missingTranslationHandler: { provide: MissingTranslationHandler, - useClass: MissingTranslationCustomHandler + useClass: MissingTranslationCustomHandler, }, loader: { provide: TranslateLoader, @@ -58,26 +66,20 @@ import { I18nService } from './i18n.service'; parser: { provide: TranslateParser, useClass: TranslateCustomParser, - deps: [TranslateDefaultParser, I18nLanguageCodeService] - } + deps: [TranslateDefaultParser, I18nLanguageCodeService], + }, }), TranslateCacheModule.forRoot({ cacheService: { provide: TranslateCacheService, useFactory: TranslateCacheFactory.createTranslateCacheService, - deps: [TranslateService, TranslateCacheSettings] - } - }) + deps: [TranslateService, TranslateCacheSettings], + }, + }), ], - providers: [ - TranslateDefaultParser, - I18nService - ], + providers: [TranslateDefaultParser, I18nService], - exports: [ - TranslateModule, - TranslateCacheModule - ] + exports: [TranslateModule, TranslateCacheModule], }) export class I18nModule {} diff --git a/core/templates/i18n/i18n.service.spec.ts b/core/templates/i18n/i18n.service.spec.ts index 0edb01053693..f965545a13c7 100644 --- a/core/templates/i18n/i18n.service.spec.ts +++ b/core/templates/i18n/i18n.service.spec.ts @@ -16,19 +16,24 @@ * @fileoverview Unit tests for i18n service. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { EventEmitter } from '@angular/core'; -import { fakeAsync, flushMicrotasks, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; -import { UserInfo } from 'domain/user/user-info.model'; -import { CookieModule, CookieService } from 'ngx-cookie'; -import { TranslateCacheService } from 'ngx-translate-cache'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { UserBackendApiService } from 'services/user-backend-api.service'; -import { UserService } from 'services/user.service'; -import { I18nService } from './i18n.service'; - +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {EventEmitter} from '@angular/core'; +import { + fakeAsync, + flushMicrotasks, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; +import {UserInfo} from 'domain/user/user-info.model'; +import {CookieModule, CookieService} from 'ngx-cookie'; +import {TranslateCacheService} from 'ngx-translate-cache'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {UserBackendApiService} from 'services/user-backend-api.service'; +import {UserService} from 'services/user.service'; +import {I18nService} from './i18n.service'; describe('I18n service', () => { let i18nService: I18nService; @@ -43,20 +48,20 @@ describe('I18n service', () => { nativeWindow = { document: { documentElement: { - setAttribute: () => {} - } + setAttribute: () => {}, + }, }, location: { toString: () => { return 'http://localhost:8181/'; }, reload: () => {}, - href: 'http://localhost:8181' + href: 'http://localhost:8181', }, history: { - pushState: () => {} + pushState: () => {}, }, - gtag: () => {} + gtag: () => {}, }; } @@ -72,24 +77,21 @@ describe('I18n service', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - CookieModule.forRoot() - ], + imports: [HttpClientTestingModule, CookieModule.forRoot()], providers: [ { provide: TranslateService, - useClass: MockTranslateService + useClass: MockTranslateService, }, { provide: TranslateCacheService, - useClass: MockTranslateCacheService + useClass: MockTranslateCacheService, }, { provide: WindowRef, - useClass: MockWindowRef - } - ] + useClass: MockWindowRef, + }, + ], }).compileComponents(); })); @@ -111,15 +113,17 @@ describe('I18n service', () => { // when initialized. windowRef.nativeWindow.location.href = 'http://localhost:8181/?lang=es'; expect( - i18nService.localStorage ? - i18nService.localStorage.getItem(CACHE_KEY_LANG) : null) - .toBe('en'); + i18nService.localStorage + ? i18nService.localStorage.getItem(CACHE_KEY_LANG) + : null + ).toBe('en'); i18nService.initialize(); expect( - i18nService.localStorage ? - i18nService.localStorage.getItem(CACHE_KEY_LANG) : null) - .toBe('es'); + i18nService.localStorage + ? i18nService.localStorage.getItem(CACHE_KEY_LANG) + : null + ).toBe('es'); flushMicrotasks(); })); @@ -129,35 +133,44 @@ describe('I18n service', () => { } // This sets the url to 'http://localhost:8181/?lang=invalid' // when initialized. - windowRef.nativeWindow.location.href = ( - 'http://localhost:8181/?lang=invalid'); + windowRef.nativeWindow.location.href = + 'http://localhost:8181/?lang=invalid'; i18nService.initialize(); // Translation cache should not be updated as the language param // is invalid. expect( - i18nService.localStorage ? - i18nService.localStorage.getItem(CACHE_KEY_LANG) : null).toBe('en'); + i18nService.localStorage + ? i18nService.localStorage.getItem(CACHE_KEY_LANG) + : null + ).toBe('en'); flushMicrotasks(); })); - it('should not update translation cache if no language param is present in' + - ' URL', fakeAsync(() => { - if (i18nService.localStorage) { - i18nService.localStorage.setItem(CACHE_KEY_LANG, 'en'); - i18nService.localStorage.setItem(CACHE_KEY_DIRECTION, 'ltr'); - } - expect( - i18nService.localStorage ? - i18nService.localStorage.getItem(CACHE_KEY_LANG) : null).toBe('en'); - i18nService.initialize(); - tick(); + it( + 'should not update translation cache if no language param is present in' + + ' URL', + fakeAsync(() => { + if (i18nService.localStorage) { + i18nService.localStorage.setItem(CACHE_KEY_LANG, 'en'); + i18nService.localStorage.setItem(CACHE_KEY_DIRECTION, 'ltr'); + } + expect( + i18nService.localStorage + ? i18nService.localStorage.getItem(CACHE_KEY_LANG) + : null + ).toBe('en'); + i18nService.initialize(); + tick(); - expect( - i18nService.localStorage ? - i18nService.localStorage.getItem(CACHE_KEY_LANG) : null).toBe('en'); - flushMicrotasks(); - })); + expect( + i18nService.localStorage + ? i18nService.localStorage.getItem(CACHE_KEY_LANG) + : null + ).toBe('en'); + flushMicrotasks(); + }) + ); it('should remove url lang param', fakeAsync(() => { i18nService.url = new URL('http://localhost/about?lang=es'); @@ -166,20 +179,37 @@ describe('I18n service', () => { i18nService.removeUrlLangParam(); expect(windowRef.nativeWindow.history.pushState).toHaveBeenCalledWith( - {}, '', 'http://localhost/about'); + {}, + '', + 'http://localhost/about' + ); flushMicrotasks(); })); it('should update user preferred language', fakeAsync(() => { let userInfo = new UserInfo( - [], true, true, true, true, true, 'es', '', '', true); + [], + true, + true, + true, + true, + true, + 'es', + '', + '', + true + ); spyOn(userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(userInfo)); + Promise.resolve(userInfo) + ); spyOn( - userBackendApiService, 'updatePreferredSiteLanguageAsync' - ).and.returnValue(Promise.resolve({ - site_language_code: 'es' - })); + userBackendApiService, + 'updatePreferredSiteLanguageAsync' + ).and.returnValue( + Promise.resolve({ + site_language_code: 'es', + }) + ); spyOn(i18nService, 'removeUrlLangParam'); let newLangCode = 'en'; @@ -187,81 +217,121 @@ describe('I18n service', () => { expect(userInfo.getPreferredSiteLanguageCode()).toBe('es'); i18nService.updateUserPreferredLanguage(newLangCode); tick(); - expect(userBackendApiService.updatePreferredSiteLanguageAsync) - .toHaveBeenCalledWith(newLangCode); + expect( + userBackendApiService.updatePreferredSiteLanguageAsync + ).toHaveBeenCalledWith(newLangCode); expect(i18nService.removeUrlLangParam).toHaveBeenCalled(); flushMicrotasks(); })); - it( - 'should set the new language code when language changes', - fakeAsync(() => { - let userInfo = new UserInfo( - [], true, true, true, true, true, 'es', '', '', false); - spyOn(userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(userInfo)); - spyOn(i18nLanguageCodeService, 'setI18nLanguageCode'); - spyOn(i18nService, 'removeUrlLangParam'); - - let newLangCode = 'ar'; - - i18nLanguageCodeService.setI18nLanguageCode('es'); - i18nService.updateUserPreferredLanguage(newLangCode); - tick(); - expect(i18nLanguageCodeService.setI18nLanguageCode).toHaveBeenCalledWith( - newLangCode); - expect(i18nService.removeUrlLangParam).toHaveBeenCalled(); - flushMicrotasks(); - }) - ); - - it('should update view to user preferred site language', fakeAsync(() => { - let preferredLanguage = 'es'; + it('should set the new language code when language changes', fakeAsync(() => { let userInfo = new UserInfo( - [], true, true, true, true, true, preferredLanguage, '', '', false); + [], + true, + true, + true, + true, + true, + 'es', + '', + '', + false + ); spyOn(userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(userInfo)); - spyOn( - i18nLanguageCodeService, 'setI18nLanguageCode' - ).and.callFake(() => {}); + Promise.resolve(userInfo) + ); + spyOn(i18nLanguageCodeService, 'setI18nLanguageCode'); spyOn(i18nService, 'removeUrlLangParam'); - i18nService.updateViewToUserPreferredSiteLanguage(); - tick(); + let newLangCode = 'ar'; - expect(userService.getUserInfoAsync).toHaveBeenCalled(); + i18nLanguageCodeService.setI18nLanguageCode('es'); + i18nService.updateUserPreferredLanguage(newLangCode); + tick(); expect(i18nLanguageCodeService.setI18nLanguageCode).toHaveBeenCalledWith( - preferredLanguage); + newLangCode + ); expect(i18nService.removeUrlLangParam).toHaveBeenCalled(); flushMicrotasks(); })); - it('should not update site language if user does not have' + - ' a preferred language', fakeAsync(() => { - let preferredLanguage = null; + it('should update view to user preferred site language', fakeAsync(() => { + let preferredLanguage = 'es'; let userInfo = new UserInfo( - [], true, true, true, true, true, preferredLanguage, '', '', false); + [], + true, + true, + true, + true, + true, + preferredLanguage, + '', + '', + false + ); spyOn(userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(userInfo)); - spyOn(i18nLanguageCodeService, 'setI18nLanguageCode'); + Promise.resolve(userInfo) + ); + spyOn(i18nLanguageCodeService, 'setI18nLanguageCode').and.callFake( + () => {} + ); spyOn(i18nService, 'removeUrlLangParam'); i18nService.updateViewToUserPreferredSiteLanguage(); tick(); expect(userService.getUserInfoAsync).toHaveBeenCalled(); - expect(i18nLanguageCodeService.setI18nLanguageCode).not. - toHaveBeenCalledWith(preferredLanguage); - expect(i18nService.removeUrlLangParam).not.toHaveBeenCalled(); + expect(i18nLanguageCodeService.setI18nLanguageCode).toHaveBeenCalledWith( + preferredLanguage + ); + expect(i18nService.removeUrlLangParam).toHaveBeenCalled(); flushMicrotasks(); })); + it( + 'should not update site language if user does not have' + + ' a preferred language', + fakeAsync(() => { + let preferredLanguage = null; + let userInfo = new UserInfo( + [], + true, + true, + true, + true, + true, + preferredLanguage, + '', + '', + false + ); + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve(userInfo) + ); + spyOn(i18nLanguageCodeService, 'setI18nLanguageCode'); + spyOn(i18nService, 'removeUrlLangParam'); + + i18nService.updateViewToUserPreferredSiteLanguage(); + tick(); + + expect(userService.getUserInfoAsync).toHaveBeenCalled(); + expect( + i18nLanguageCodeService.setI18nLanguageCode + ).not.toHaveBeenCalledWith(preferredLanguage); + expect(i18nService.removeUrlLangParam).not.toHaveBeenCalled(); + flushMicrotasks(); + }) + ); + it('should update site language', fakeAsync(() => { let mockI18nLanguageCodeService = new EventEmitter(); - spyOn(windowRef.nativeWindow.location, 'toString') - .and.returnValue('http://localhost:8181'); - spyOnProperty(i18nLanguageCodeService, 'onI18nLanguageCodeChange') - .and.returnValue(mockI18nLanguageCodeService); + spyOn(windowRef.nativeWindow.location, 'toString').and.returnValue( + 'http://localhost:8181' + ); + spyOnProperty( + i18nLanguageCodeService, + 'onI18nLanguageCodeChange' + ).and.returnValue(mockI18nLanguageCodeService); i18nService.initialize(); mockI18nLanguageCodeService.emit('en'); tick(); @@ -270,26 +340,35 @@ describe('I18n service', () => { it( 'should reload the website if language direction changes with lang param' + - ' when cookies are not acknowledged', + ' when cookies are not acknowledged', fakeAsync(() => { let mockI18nLanguageCodeServiceSubject = new EventEmitter(); - spyOn(windowRef.nativeWindow.location, 'toString') - .and.returnValue('http://localhost:8181'); - spyOnProperty(i18nLanguageCodeService, 'onI18nLanguageCodeChange') - .and.returnValue(mockI18nLanguageCodeServiceSubject); + spyOn(windowRef.nativeWindow.location, 'toString').and.returnValue( + 'http://localhost:8181' + ); + spyOnProperty( + i18nLanguageCodeService, + 'onI18nLanguageCodeChange' + ).and.returnValue(mockI18nLanguageCodeServiceSubject); const prevLangCode = I18nLanguageCodeService.prevLangCode; I18nLanguageCodeService.prevLangCode = 'en'; spyOn(windowRef.nativeWindow.location, 'reload'); i18nService.initialize(); mockI18nLanguageCodeServiceSubject.emit('ar'); tick(); - expect(windowRef.nativeWindow.location.href).toBe('http://localhost:8181/?dir=rtl'); + expect(windowRef.nativeWindow.location.href).toBe( + 'http://localhost:8181/?dir=rtl' + ); mockI18nLanguageCodeServiceSubject.emit('en'); tick(); - expect(windowRef.nativeWindow.location.href).toBe('http://localhost:8181/?dir=ltr'); + expect(windowRef.nativeWindow.location.href).toBe( + 'http://localhost:8181/?dir=ltr' + ); mockI18nLanguageCodeServiceSubject.emit('es'); tick(); - expect(windowRef.nativeWindow.location.href).toBe('http://localhost:8181/?dir=ltr'); + expect(windowRef.nativeWindow.location.href).toBe( + 'http://localhost:8181/?dir=ltr' + ); I18nLanguageCodeService.prevLangCode = prevLangCode; flushMicrotasks(); }) @@ -297,15 +376,16 @@ describe('I18n service', () => { it( 'should reload the website if language direction changes when cookies ' + - 'acknowledged', + 'acknowledged', fakeAsync(() => { let currentDateInUnixTimeMsecs = new Date().valueOf(); - spyOn(windowRef.nativeWindow.location, 'toString') - .and.returnValue('http://localhost:8181'); + spyOn(windowRef.nativeWindow.location, 'toString').and.returnValue( + 'http://localhost:8181' + ); let cookieOptions = { expires: new Date(currentDateInUnixTimeMsecs + 360000), secure: true, - sameSite: 'none' as const + sameSite: 'none' as const, }; const cookieService = TestBed.inject(CookieService); cookieService.put( @@ -314,8 +394,10 @@ describe('I18n service', () => { cookieOptions ); let mockI18nLanguageCodeServiceSubject = new EventEmitter(); - spyOnProperty(i18nLanguageCodeService, 'onI18nLanguageCodeChange') - .and.returnValue(mockI18nLanguageCodeServiceSubject); + spyOnProperty( + i18nLanguageCodeService, + 'onI18nLanguageCodeChange' + ).and.returnValue(mockI18nLanguageCodeServiceSubject); const prevLangCode = I18nLanguageCodeService.prevLangCode; I18nLanguageCodeService.prevLangCode = 'en'; spyOn(windowRef.nativeWindow.location, 'reload'); @@ -327,11 +409,15 @@ describe('I18n service', () => { // reload. To get around this in the short term we emit 'en' first and // then 'ar' so that the dir value in cookie changes. mockI18nLanguageCodeServiceSubject.emit('en'); - expect(windowRef.nativeWindow.location.href).toBe('http://localhost:8181'); + expect(windowRef.nativeWindow.location.href).toBe( + 'http://localhost:8181' + ); mockI18nLanguageCodeServiceSubject.emit('ar'); I18nLanguageCodeService.prevLangCode = prevLangCode; expect(windowRef.nativeWindow.location.reload).toHaveBeenCalled(); - expect(windowRef.nativeWindow.location.href).toBe('http://localhost:8181'); + expect(windowRef.nativeWindow.location.href).toBe( + 'http://localhost:8181' + ); cookieService.removeAll(); flushMicrotasks(); }) diff --git a/core/templates/i18n/i18n.service.ts b/core/templates/i18n/i18n.service.ts index 77c1402de704..06c724e70ea6 100644 --- a/core/templates/i18n/i18n.service.ts +++ b/core/templates/i18n/i18n.service.ts @@ -16,31 +16,34 @@ * @fileoverview Service containing all i18n logic. */ -import { EventEmitter, Injectable } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; -import { AppConstants } from 'app.constants'; -import { TranslateCacheService } from 'ngx-translate-cache'; -import { DocumentAttributeCustomizationService } from 'services/contextual/document-attribute-customization.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { I18nLanguageCodeService, LanguageInfo } from 'services/i18n-language-code.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { UserBackendApiService } from 'services/user-backend-api.service'; -import { CookieService } from 'ngx-cookie'; -import { UserService } from 'services/user.service'; +import {EventEmitter, Injectable} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; +import {AppConstants} from 'app.constants'; +import {TranslateCacheService} from 'ngx-translate-cache'; +import {DocumentAttributeCustomizationService} from 'services/contextual/document-attribute-customization.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import { + I18nLanguageCodeService, + LanguageInfo, +} from 'services/i18n-language-code.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {UserBackendApiService} from 'services/user-backend-api.service'; +import {CookieService} from 'ngx-cookie'; +import {UserService} from 'services/user.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class I18nService { - private _directionChangeEventEmitter: EventEmitter = ( - new EventEmitter()); + private _directionChangeEventEmitter: EventEmitter = + new EventEmitter(); COOKIE_NAME_COOKIES_ACKNOWLEDGED = 'OPPIA_COOKIES_ACKNOWLEDGED'; url!: URL; // Check that local storage exists and works as expected. // If it does storage stores the localStorage object, // else storage is undefined or false. - localStorage = (function() { + localStorage = (function () { let test = 'test'; let result; try { @@ -49,21 +52,19 @@ export class I18nService { localStorage.removeItem(test); return result && localStorage; } catch (exception) {} - }()); + })(); supportedSiteLanguageCodes = Object.assign( {}, ...AppConstants.SUPPORTED_SITE_LANGUAGES.map( - (languageInfo: LanguageInfo) => ( - {[languageInfo.id]: languageInfo.direction} - ) + (languageInfo: LanguageInfo) => ({ + [languageInfo.id]: languageInfo.direction, + }) ) ); - constructor( - private documentAttributeCustomizationService: - DocumentAttributeCustomizationService, + private documentAttributeCustomizationService: DocumentAttributeCustomizationService, private cookieService: CookieService, private i18nLanguageCodeService: I18nLanguageCodeService, private userBackendApiService: UserBackendApiService, @@ -78,53 +79,50 @@ export class I18nService { if (this.localStorage) { this.localStorage.setItem('lang', langCode); this.localStorage.setItem( - 'direction', this.supportedSiteLanguageCodes[langCode] + 'direction', + this.supportedSiteLanguageCodes[langCode] ); } } initialize(): void { - this.i18nLanguageCodeService.onI18nLanguageCodeChange.subscribe( - (code) => { - const cookieSetDateMsecs = this.cookieService.get( - this.COOKIE_NAME_COOKIES_ACKNOWLEDGED); - const langDirection = ( - this.i18nLanguageCodeService.isLanguageRTL(code) ? 'rtl' : 'ltr'); - let prevLangDirection = ( - this.i18nLanguageCodeService.isLanguageRTL( - I18nLanguageCodeService.prevLangCode - ) ? 'rtl' : 'ltr'); - this.translateService.use(code); - this.documentAttributeCustomizationService.addAttribute('lang', code); - if (!!cookieSetDateMsecs && - +cookieSetDateMsecs > AppConstants.COOKIE_POLICY_LAST_UPDATED_MSECS - ) { - prevLangDirection = ( - this.cookieService.get('dir') || prevLangDirection - ); - this.cookieService.put( - 'dir', - langDirection - ); - this.cookieService.put('lang', code); - if (prevLangDirection !== langDirection) { - this.windowRef.nativeWindow.location.reload(); - } - } else { - const parser = new URL(this.windowRef.nativeWindow.location.href); - let urlParamDir = parser.searchParams.get('dir'); - if (urlParamDir === null) { - urlParamDir = 'ltr'; - } - if (urlParamDir === langDirection) { - return; - } - parser.searchParams.set('dir', langDirection); - this.windowRef.nativeWindow.location.href = ( - parser.href); + this.i18nLanguageCodeService.onI18nLanguageCodeChange.subscribe(code => { + const cookieSetDateMsecs = this.cookieService.get( + this.COOKIE_NAME_COOKIES_ACKNOWLEDGED + ); + const langDirection = this.i18nLanguageCodeService.isLanguageRTL(code) + ? 'rtl' + : 'ltr'; + let prevLangDirection = this.i18nLanguageCodeService.isLanguageRTL( + I18nLanguageCodeService.prevLangCode + ) + ? 'rtl' + : 'ltr'; + this.translateService.use(code); + this.documentAttributeCustomizationService.addAttribute('lang', code); + if ( + !!cookieSetDateMsecs && + +cookieSetDateMsecs > AppConstants.COOKIE_POLICY_LAST_UPDATED_MSECS + ) { + prevLangDirection = this.cookieService.get('dir') || prevLangDirection; + this.cookieService.put('dir', langDirection); + this.cookieService.put('lang', code); + if (prevLangDirection !== langDirection) { + this.windowRef.nativeWindow.location.reload(); + } + } else { + const parser = new URL(this.windowRef.nativeWindow.location.href); + let urlParamDir = parser.searchParams.get('dir'); + if (urlParamDir === null) { + urlParamDir = 'ltr'; } + if (urlParamDir === langDirection) { + return; + } + parser.searchParams.set('dir', langDirection); + this.windowRef.nativeWindow.location.href = parser.href; } - ); + }); // Loads site language according to the language parameter in URL // if present. @@ -143,7 +141,8 @@ export class I18nService { // cache, and avoids race conditions. this.setLocalStorageKeys(siteLanguageCode); this._updateDirection( - this.supportedSiteLanguageCodes[siteLanguageCode]); + this.supportedSiteLanguageCodes[siteLanguageCode] + ); } else { // In the case where the URL contains an invalid language code, we // load the site using last cached language code and remove the language @@ -158,12 +157,12 @@ export class I18nService { // and the language code stored in the local storage. this.translateCacheService.init(); - const cachedLanguageCode = ( - this.translateCacheService.getCachedLanguage()); + const cachedLanguageCode = this.translateCacheService.getCachedLanguage(); if (cachedLanguageCode) { this.i18nLanguageCodeService.setI18nLanguageCode(cachedLanguageCode); this._updateDirection( - this.supportedSiteLanguageCodes[cachedLanguageCode]); + this.supportedSiteLanguageCodes[cachedLanguageCode] + ); } } @@ -171,25 +170,26 @@ export class I18nService { if (this.url.searchParams.has('lang')) { this.url.searchParams.delete('lang'); this.windowRef.nativeWindow.history.pushState( - {}, '', this.url.toString()); + {}, + '', + this.url.toString() + ); } } - updateUserPreferredLanguage(newLangCode: string): void { this.siteAnalyticsService.registerSiteLanguageChangeEvent(newLangCode); this.setLocalStorageKeys(newLangCode); - - this.userService.getUserInfoAsync().then((userInfo) => { + this.userService.getUserInfoAsync().then(userInfo => { // If user is logged in first save the language and then reload, // otherwise just reload. if (userInfo.isLoggedIn()) { - this.userBackendApiService.updatePreferredSiteLanguageAsync( - newLangCode - ).then(() => { - this.i18nLanguageCodeService.setI18nLanguageCode(newLangCode); - }); + this.userBackendApiService + .updatePreferredSiteLanguageAsync(newLangCode) + .then(() => { + this.i18nLanguageCodeService.setI18nLanguageCode(newLangCode); + }); } else { this.i18nLanguageCodeService.setI18nLanguageCode(newLangCode); } @@ -202,7 +202,7 @@ export class I18nService { } updateViewToUserPreferredSiteLanguage(): void { - this.userService.getUserInfoAsync().then((userInfo) => { + this.userService.getUserInfoAsync().then(userInfo => { let preferredLangCode = userInfo.getPreferredSiteLanguageCode() || ''; if (preferredLangCode) { diff --git a/core/templates/i18n/missing-translation-custom-handler.spec.ts b/core/templates/i18n/missing-translation-custom-handler.spec.ts index 9242e98c9ebf..e71a0f953b5a 100644 --- a/core/templates/i18n/missing-translation-custom-handler.spec.ts +++ b/core/templates/i18n/missing-translation-custom-handler.spec.ts @@ -16,26 +16,29 @@ * @fileoverview Unit tests for the Missing Translations Handler. */ -import { TranslateService } from '@ngx-translate/core'; -import { AppConstants } from 'app.constants'; -import { MissingTranslationCustomHandler } from './missing-translation-custom-handler'; +import {TranslateService} from '@ngx-translate/core'; +import {AppConstants} from 'app.constants'; +import {MissingTranslationCustomHandler} from './missing-translation-custom-handler'; describe('Missing Translations Custom Handler', () => { let mth = new MissingTranslationCustomHandler(); - it('should return correct translation for existing default translations', - () => { - expect(mth.handle({ + it('should return correct translation for existing default translations', () => { + expect( + mth.handle({ key: 'I18N_SIGNUP_PAGE_SUBTITLE', translateService: {} as TranslateService, - })).toEqual(AppConstants.DEFAULT_TRANSLATIONS.I18N_SIGNUP_PAGE_SUBTITLE); - }); + }) + ).toEqual(AppConstants.DEFAULT_TRANSLATIONS.I18N_SIGNUP_PAGE_SUBTITLE); + }); - it('should return key if correct value is not available in app constants', - () => { - let key = 'KEY_NOT_AVAILABLE_IN_APP_CONSTANTS'; - expect(mth.handle({ - key, translateService: {} as TranslateService - })).toEqual(key); - }); + it('should return key if correct value is not available in app constants', () => { + let key = 'KEY_NOT_AVAILABLE_IN_APP_CONSTANTS'; + expect( + mth.handle({ + key, + translateService: {} as TranslateService, + }) + ).toEqual(key); + }); }); diff --git a/core/templates/i18n/missing-translation-custom-handler.ts b/core/templates/i18n/missing-translation-custom-handler.ts index 5a32678e10d0..97d279cad893 100644 --- a/core/templates/i18n/missing-translation-custom-handler.ts +++ b/core/templates/i18n/missing-translation-custom-handler.ts @@ -16,18 +16,22 @@ * @fileoverview The custom handler to handle the missing translations. */ -import { MissingTranslationHandler, MissingTranslationHandlerParams } from '@ngx-translate/core'; -import { AppConstants } from 'app.constants'; +import { + MissingTranslationHandler, + MissingTranslationHandlerParams, +} from '@ngx-translate/core'; +import {AppConstants} from 'app.constants'; type DefaultTranslationsKey = keyof typeof AppConstants.DEFAULT_TRANSLATIONS; -export class MissingTranslationCustomHandler implements - MissingTranslationHandler { +export class MissingTranslationCustomHandler + implements MissingTranslationHandler +{ handle(params: MissingTranslationHandlerParams): string { if (params.key in AppConstants.DEFAULT_TRANSLATIONS) { - return ( - AppConstants.DEFAULT_TRANSLATIONS[params.key as - DefaultTranslationsKey]); + return AppConstants.DEFAULT_TRANSLATIONS[ + params.key as DefaultTranslationsKey + ]; } return params.key; } diff --git a/core/templates/i18n/translate-cache.factory.spec.ts b/core/templates/i18n/translate-cache.factory.spec.ts index 96ed4b272055..6b0576682d7b 100644 --- a/core/templates/i18n/translate-cache.factory.spec.ts +++ b/core/templates/i18n/translate-cache.factory.spec.ts @@ -16,10 +16,13 @@ * @fileoverview Unit tests for the Translate Cache Factory. */ -import { TestBed, waitForAsync } from '@angular/core/testing'; -import { TranslateCacheService, TranslateCacheSettings } from 'ngx-translate-cache'; -import { TranslateService } from '@ngx-translate/core'; -import { TranslateCacheFactory } from './translate-cache.factory'; +import {TestBed, waitForAsync} from '@angular/core/testing'; +import { + TranslateCacheService, + TranslateCacheSettings, +} from 'ngx-translate-cache'; +import {TranslateService} from '@ngx-translate/core'; +import {TranslateCacheFactory} from './translate-cache.factory'; describe('Translate Cache Factory', () => { class MockTranslateCacheService { @@ -29,21 +32,27 @@ describe('Translate Cache Factory', () => { ) {} } let mockTranslateCacheService = new MockTranslateCacheService( - {} as TranslateService, {} as TranslateCacheSettings); + {} as TranslateService, + {} as TranslateCacheSettings + ); beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ providers: [ { provide: TranslateCacheService, - useValue: mockTranslateCacheService - } - ] + useValue: mockTranslateCacheService, + }, + ], }).compileComponents(); })); it('should create translate cache service', () => { - expect(TranslateCacheFactory.createTranslateCacheService( - {} as TranslateService, {} as TranslateCacheSettings)).toBeDefined(); + expect( + TranslateCacheFactory.createTranslateCacheService( + {} as TranslateService, + {} as TranslateCacheSettings + ) + ).toBeDefined(); }); }); diff --git a/core/templates/i18n/translate-cache.factory.ts b/core/templates/i18n/translate-cache.factory.ts index 985d844e57bd..aa703270241f 100644 --- a/core/templates/i18n/translate-cache.factory.ts +++ b/core/templates/i18n/translate-cache.factory.ts @@ -16,13 +16,17 @@ * @fileoverview Factory for creating translation cache service. */ -import { TranslateService } from '@ngx-translate/core'; -import { TranslateCacheService, TranslateCacheSettings } from 'ngx-translate-cache'; +import {TranslateService} from '@ngx-translate/core'; +import { + TranslateCacheService, + TranslateCacheSettings, +} from 'ngx-translate-cache'; export class TranslateCacheFactory { static createTranslateCacheService( - translateService: TranslateService, - translateCacheSettings: TranslateCacheSettings): TranslateCacheService { + translateService: TranslateService, + translateCacheSettings: TranslateCacheSettings + ): TranslateCacheService { return new TranslateCacheService(translateService, translateCacheSettings); } } diff --git a/core/templates/i18n/translate-custom-parser.spec.ts b/core/templates/i18n/translate-custom-parser.spec.ts index e5e5c84e1b9b..9f3191aa739f 100644 --- a/core/templates/i18n/translate-custom-parser.spec.ts +++ b/core/templates/i18n/translate-custom-parser.spec.ts @@ -12,15 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Unit tests for the Translate Custom Parser. */ -import { TestBed, waitForAsync } from '@angular/core/testing'; -import { TranslateDefaultParser, TranslateModule, TranslateParser } from '@ngx-translate/core'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { TranslateCustomParser } from './translate-custom-parser'; +import {TestBed, waitForAsync} from '@angular/core/testing'; +import { + TranslateDefaultParser, + TranslateModule, + TranslateParser, +} from '@ngx-translate/core'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {TranslateCustomParser} from './translate-custom-parser'; describe('Translate Custom Parser', () => { let translateCustomParser: TranslateCustomParser; @@ -33,46 +36,54 @@ describe('Translate Custom Parser', () => { useDefaultLang: true, isolate: false, extend: false, - defaultLanguage: 'en' - }) + defaultLanguage: 'en', + }), ], providers: [ I18nLanguageCodeService, TranslateDefaultParser, - TranslateParser - ] + TranslateParser, + ], }).compileComponents(); })); beforeEach(() => { translateDefaultParser = TestBed.inject(TranslateDefaultParser); - translateCustomParser = new TranslateCustomParser( - translateDefaultParser); + translateCustomParser = new TranslateCustomParser(translateDefaultParser); }); it('should interpolate with preferred language', () => { - expect(translateCustomParser.interpolate( - '{minChoiceNumber, plural, one{Please select one or more choices.}' + - 'other{Please select # or more choices.}}', { - minChoiceNumber: 1, messageFormat: true })) - .toEqual('Please select one or more choices.'); - let params = { text: 'text' }; - expect(translateCustomParser.interpolate(`<[${params.text}]>`, params)) - .toEqual(params.text); + expect( + translateCustomParser.interpolate( + '{minChoiceNumber, plural, one{Please select one or more choices.}' + + 'other{Please select # or more choices.}}', + { + minChoiceNumber: 1, + messageFormat: true, + } + ) + ).toEqual('Please select one or more choices.'); + let params = {text: 'text'}; + expect( + translateCustomParser.interpolate(`<[${params.text}]>`, params) + ).toEqual(params.text); }); - it ('should handle cases when params is not defined', () => { + it('should handle cases when params is not defined', () => { expect(translateCustomParser.interpolate('<[ KEY ]>')).toEqual('<[ KEY ]>'); - expect(translateCustomParser.interpolate('<[ KEY ]>', {})) - .toEqual('<[ KEY ]>'); + expect(translateCustomParser.interpolate('<[ KEY ]>', {})).toEqual( + '<[ KEY ]>' + ); }); - it('should handle cases where messageFormat has value other than true', - () => { - expect(translateCustomParser.interpolate( - '<[ testName ]>', { testName: 'test_name', messageFormat: 2 })) - .toEqual('test_name'); - }); + it('should handle cases where messageFormat has value other than true', () => { + expect( + translateCustomParser.interpolate('<[ testName ]>', { + testName: 'test_name', + messageFormat: 2, + }) + ).toEqual('test_name'); + }); it('should test getters', () => { expect(translateCustomParser.messageFormat).toBeDefined(); diff --git a/core/templates/i18n/translate-custom-parser.ts b/core/templates/i18n/translate-custom-parser.ts index 5a5ce4b81358..fee924faef0b 100644 --- a/core/templates/i18n/translate-custom-parser.ts +++ b/core/templates/i18n/translate-custom-parser.ts @@ -16,15 +16,13 @@ * @fileoverview Custom parser for translations. */ -import { TranslateDefaultParser, TranslateParser } from '@ngx-translate/core'; -import { AppConstants } from 'app.constants'; +import {TranslateDefaultParser, TranslateParser} from '@ngx-translate/core'; +import {AppConstants} from 'app.constants'; import MessageFormat from 'messageformat'; export class TranslateCustomParser extends TranslateParser { private _messageFormat: MessageFormat; - constructor( - private translateDefaultParser: TranslateDefaultParser - ) { + constructor(private translateDefaultParser: TranslateDefaultParser) { super(); /** * The default parser expects {{}} as delimiters. @@ -36,10 +34,13 @@ export class TranslateCustomParser extends TranslateParser { } interpolate( - expr: string | Function, - params?: { [key: string]: number | string | boolean }): string { + expr: string | Function, + params?: {[key: string]: number | string | boolean} + ): string { let interpolatedValue = this.translateDefaultParser.interpolate( - expr, params); + expr, + params + ); if (!(params && interpolatedValue !== undefined)) { return interpolatedValue; diff --git a/core/templates/i18n/translate-loader.factory.spec.ts b/core/templates/i18n/translate-loader.factory.spec.ts index 7e8b4c376fb6..77ca1d780b8e 100644 --- a/core/templates/i18n/translate-loader.factory.spec.ts +++ b/core/templates/i18n/translate-loader.factory.spec.ts @@ -12,17 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Unit tests for the Translate Loader Factory. */ // eslint-disable-next-line oppia/disallow-httpclient -import { HttpClient } from '@angular/common/http'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TestBed, waitForAsync } from '@angular/core/testing'; -import { TranslateModule } from '@ngx-translate/core'; -import { TranslateLoaderFactory } from './translate-loader.factory'; +import {HttpClient} from '@angular/common/http'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TestBed, waitForAsync} from '@angular/core/testing'; +import {TranslateModule} from '@ngx-translate/core'; +import {TranslateLoaderFactory} from './translate-loader.factory'; describe('Translate Loader Factory', () => { let httpClient: HttpClient; @@ -35,9 +34,9 @@ describe('Translate Loader Factory', () => { useDefaultLang: true, isolate: false, extend: false, - defaultLanguage: 'en' + defaultLanguage: 'en', }), - ] + ], }).compileComponents(); })); diff --git a/core/templates/i18n/translate-loader.factory.ts b/core/templates/i18n/translate-loader.factory.ts index 9e6bc7d7f16e..d631308dab94 100644 --- a/core/templates/i18n/translate-loader.factory.ts +++ b/core/templates/i18n/translate-loader.factory.ts @@ -17,8 +17,8 @@ */ // eslint-disable-next-line oppia/disallow-httpclient -import { HttpClient } from '@angular/common/http'; -import { TranslateHttpLoader } from '@ngx-translate/http-loader'; +import {HttpClient} from '@angular/common/http'; +import {TranslateHttpLoader} from '@ngx-translate/http-loader'; export class TranslateLoaderFactory { static createHttpLoader(httpClient: HttpClient): TranslateHttpLoader { diff --git a/core/templates/karma.module.ts b/core/templates/karma.module.ts index 89dc4f0bd3b5..227fc6a3d3bb 100644 --- a/core/templates/karma.module.ts +++ b/core/templates/karma.module.ts @@ -22,7 +22,13 @@ import 'third-party-imports/ui-tree.import'; declare var angular: ng.IAngularStatic; angular.module('oppia', [ - require('angular-cookies'), 'ngAnimate', - 'ngMaterial', 'ngSanitize', 'ngTouch', 'pascalprecht.translate', - 'ui.bootstrap', 'ui.tree', uiValidate + require('angular-cookies'), + 'ngAnimate', + 'ngMaterial', + 'ngSanitize', + 'ngTouch', + 'pascalprecht.translate', + 'ui.bootstrap', + 'ui.tree', + uiValidate, ]); diff --git a/core/templates/mathjaxConfig.ts b/core/templates/mathjaxConfig.ts index d784345f81c9..a6dd3c5afd1c 100644 --- a/core/templates/mathjaxConfig.ts +++ b/core/templates/mathjaxConfig.ts @@ -16,13 +16,13 @@ window.MathJax = { useGlobalCache: false, linebreaks: { automatic: true, - width: '500px' + width: '500px', }, scale: 91, showMathMenu: false, - useFontCache: false + useFontCache: false, }, TeX: { - extensions: ['AMSmath.js', 'AMSsymbols.js', 'autoload-all.js'] - } + extensions: ['AMSmath.js', 'AMSsymbols.js', 'autoload-all.js'], + }, }; diff --git a/core/templates/modules/material.module.ts b/core/templates/modules/material.module.ts index f3aafde92e97..3959916b8285 100644 --- a/core/templates/modules/material.module.ts +++ b/core/templates/modules/material.module.ts @@ -16,30 +16,30 @@ * @fileoverview Module for Angular Material. */ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { NgxMatSelectSearchModule } from 'ngx-mat-select-search'; -import { MatButtonModule } from '@angular/material/button'; -import { MatCardModule } from '@angular/material/card'; -import { MatCheckboxModule } from '@angular/material/checkbox'; -import { MatChipsModule } from '@angular/material/chips'; -import { MatExpansionModule } from '@angular/material/expansion'; -import { MatGridListModule } from '@angular/material/grid-list'; -import { MatListModule } from '@angular/material/list'; -import { MatProgressBarModule } from '@angular/material/progress-bar'; -import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; -import { MatRadioModule } from '@angular/material/radio'; -import { MatTabsModule } from '@angular/material/tabs'; -import { MatSliderModule } from '@angular/material/slider'; -import { MatSlideToggleModule} from '@angular/material/slide-toggle'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatInputModule } from '@angular/material/input'; -import { MatDividerModule } from '@angular/material/divider'; -import { MatIconModule } from '@angular/material/icon'; -import { MatAutocompleteModule } from '@angular/material/autocomplete'; -import { MatSelectModule } from '@angular/material/select'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { MatMenuModule } from '@angular/material/menu'; +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {NgxMatSelectSearchModule} from 'ngx-mat-select-search'; +import {MatButtonModule} from '@angular/material/button'; +import {MatCardModule} from '@angular/material/card'; +import {MatCheckboxModule} from '@angular/material/checkbox'; +import {MatChipsModule} from '@angular/material/chips'; +import {MatExpansionModule} from '@angular/material/expansion'; +import {MatGridListModule} from '@angular/material/grid-list'; +import {MatListModule} from '@angular/material/list'; +import {MatProgressBarModule} from '@angular/material/progress-bar'; +import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; +import {MatRadioModule} from '@angular/material/radio'; +import {MatTabsModule} from '@angular/material/tabs'; +import {MatSliderModule} from '@angular/material/slider'; +import {MatSlideToggleModule} from '@angular/material/slide-toggle'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatInputModule} from '@angular/material/input'; +import {MatDividerModule} from '@angular/material/divider'; +import {MatIconModule} from '@angular/material/icon'; +import {MatAutocompleteModule} from '@angular/material/autocomplete'; +import {MatSelectModule} from '@angular/material/select'; +import {MatTooltipModule} from '@angular/material/tooltip'; +import {MatMenuModule} from '@angular/material/menu'; @NgModule({ imports: [ @@ -67,7 +67,7 @@ import { MatMenuModule } from '@angular/material/menu'; MatInputModule, MatDividerModule, MatTooltipModule, - MatMenuModule + MatMenuModule, ], exports: [ CommonModule, @@ -92,7 +92,7 @@ import { MatMenuModule } from '@angular/material/menu'; MatInputModule, MatDividerModule, MatTooltipModule, - MatMenuModule - ] + MatMenuModule, + ], }) export class MaterialModule {} diff --git a/core/templates/modules/ng-boostrap.module.ts b/core/templates/modules/ng-boostrap.module.ts index 7d5b3d2d06dd..853f6add68e9 100644 --- a/core/templates/modules/ng-boostrap.module.ts +++ b/core/templates/modules/ng-boostrap.module.ts @@ -16,11 +16,16 @@ * @fileoverview Module for Ng Bootstrap. */ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { NgbDropdownModule, NgbTooltipModule, NgbNavModule, - NgbModalModule, NgbPopoverModule, - NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap'; +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import { + NgbDropdownModule, + NgbTooltipModule, + NgbNavModule, + NgbModalModule, + NgbPopoverModule, + NgbTypeaheadModule, +} from '@ng-bootstrap/ng-bootstrap'; @NgModule({ imports: [ @@ -30,7 +35,7 @@ import { NgbDropdownModule, NgbTooltipModule, NgbNavModule, NgbNavModule, NgbModalModule, NgbPopoverModule, - NgbTypeaheadModule + NgbTypeaheadModule, ], exports: [ CommonModule, @@ -39,7 +44,7 @@ import { NgbDropdownModule, NgbTooltipModule, NgbNavModule, NgbNavModule, NgbModalModule, NgbPopoverModule, - NgbTypeaheadModule - ] + NgbTypeaheadModule, + ], }) export class NgBootstrapModule {} diff --git a/core/templates/pages/Base.ts b/core/templates/pages/Base.ts index db70fbbadb13..b2b5fe810bd8 100644 --- a/core/templates/pages/Base.ts +++ b/core/templates/pages/Base.ts @@ -28,19 +28,28 @@ require('app.constants.ajs.ts'); */ angular.module('oppia').controller('Base', [ - '$rootScope', '$scope', - 'CsrfTokenService', 'DocumentAttributeCustomizationService', 'LoaderService', - 'UrlInterpolationService', 'SUPPORTED_SITE_LANGUAGES', - function( - $rootScope, $scope, - CsrfTokenService, DocumentAttributeCustomizationService, LoaderService, - UrlInterpolationService, SUPPORTED_SITE_LANGUAGES) { + '$rootScope', + '$scope', + 'CsrfTokenService', + 'DocumentAttributeCustomizationService', + 'LoaderService', + 'UrlInterpolationService', + 'SUPPORTED_SITE_LANGUAGES', + function ( + $rootScope, + $scope, + CsrfTokenService, + DocumentAttributeCustomizationService, + LoaderService, + UrlInterpolationService, + SUPPORTED_SITE_LANGUAGES + ) { var ctrl = this; - $scope.getAssetUrl = function(path) { + $scope.getAssetUrl = function (path) { return UrlInterpolationService.getFullStaticAssetUrl(path); }; - ctrl.$onInit = function() { + ctrl.$onInit = function () { $scope.currentLang = 'en'; $scope.direction = 'ltr'; // If this is nonempty, the whole page goes into 'Loading...' mode. @@ -49,7 +58,7 @@ angular.module('oppia').controller('Base', [ CsrfTokenService.initializeToken(); // Listener function to catch the change in language preference. - $rootScope.$on('$translateChangeSuccess', function(evt, response) { + $rootScope.$on('$translateChangeSuccess', function (evt, response) { $scope.currentLang = response.language; for (var i = 0; i < SUPPORTED_SITE_LANGUAGES.length; i++) { if (SUPPORTED_SITE_LANGUAGES[i].id === $scope.currentLang) { @@ -60,7 +69,9 @@ angular.module('oppia').controller('Base', [ }); DocumentAttributeCustomizationService.addAttribute( - 'lang', $scope.currentLang); + 'lang', + $scope.currentLang + ); }; - } + }, ]); diff --git a/core/templates/pages/about-foundation-page/about-foundation-page-root.component.spec.ts b/core/templates/pages/about-foundation-page/about-foundation-page-root.component.spec.ts index 450cd1250010..7316b53cbfc5 100644 --- a/core/templates/pages/about-foundation-page/about-foundation-page-root.component.spec.ts +++ b/core/templates/pages/about-foundation-page/about-foundation-page-root.component.spec.ts @@ -16,13 +16,13 @@ * @fileoverview Unit tests for the about foundation page root component. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { AboutFoundationPageRootComponent } from './about-foundation-page-root.component'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {AboutFoundationPageRootComponent} from './about-foundation-page-root.component'; describe('About foundation Page Root', () => { let fixture: ComponentFixture; @@ -31,14 +31,9 @@ describe('About foundation Page Root', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - AboutFoundationPageRootComponent, - MockTranslatePipe - ], - providers: [ - PageHeadService - ], - schemas: [NO_ERRORS_SCHEMA] + declarations: [AboutFoundationPageRootComponent, MockTranslatePipe], + providers: [PageHeadService], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -57,6 +52,7 @@ describe('About foundation Page Root', () => { component.ngOnInit(); expect(pageHeadService.updateTitleAndMetaTags).toHaveBeenCalledWith( AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ABOUT_FOUNDATION.TITLE, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ABOUT_FOUNDATION.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ABOUT_FOUNDATION.META + ); }); }); diff --git a/core/templates/pages/about-foundation-page/about-foundation-page-root.component.ts b/core/templates/pages/about-foundation-page/about-foundation-page-root.component.ts index 3d14349bd40d..868392e0278a 100644 --- a/core/templates/pages/about-foundation-page/about-foundation-page-root.component.ts +++ b/core/templates/pages/about-foundation-page/about-foundation-page-root.component.ts @@ -16,22 +16,21 @@ * @fileoverview Root Component for about foundation page. */ -import { Component } from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; +import {Component} from '@angular/core'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; @Component({ selector: 'oppia-about-foundation-page-root', - templateUrl: './about-foundation-page-root.component.html' + templateUrl: './about-foundation-page-root.component.html', }) export class AboutFoundationPageRootComponent { - constructor( - private pageHeadService: PageHeadService - ) {} + constructor(private pageHeadService: PageHeadService) {} ngOnInit(): void { this.pageHeadService.updateTitleAndMetaTags( AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ABOUT_FOUNDATION.TITLE, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ABOUT_FOUNDATION.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ABOUT_FOUNDATION.META + ); } } diff --git a/core/templates/pages/about-foundation-page/about-foundation-page-routing.module.ts b/core/templates/pages/about-foundation-page/about-foundation-page-routing.module.ts index dc171b8fc249..cc49a4c757fc 100644 --- a/core/templates/pages/about-foundation-page/about-foundation-page-routing.module.ts +++ b/core/templates/pages/about-foundation-page/about-foundation-page-routing.module.ts @@ -16,24 +16,19 @@ * @fileoverview Routing module for about foundation page. */ -import { NgModule } from '@angular/core'; -import { Route, RouterModule } from '@angular/router'; -import { AboutFoundationPageRootComponent } from './about-foundation-page-root.component'; +import {NgModule} from '@angular/core'; +import {Route, RouterModule} from '@angular/router'; +import {AboutFoundationPageRootComponent} from './about-foundation-page-root.component'; const routes: Route[] = [ { path: '', - component: AboutFoundationPageRootComponent - } + component: AboutFoundationPageRootComponent, + }, ]; @NgModule({ - imports: [ - RouterModule.forChild(routes) - ], - exports: [ - RouterModule - ] + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], }) - export class AboutFoundationPageRoutingModule {} diff --git a/core/templates/pages/about-foundation-page/about-foundation-page.component.spec.ts b/core/templates/pages/about-foundation-page/about-foundation-page.component.spec.ts index fc869dd49629..80c3870d2312 100644 --- a/core/templates/pages/about-foundation-page/about-foundation-page.component.spec.ts +++ b/core/templates/pages/about-foundation-page/about-foundation-page.component.spec.ts @@ -16,14 +16,14 @@ * @fileoverview Unit tests for about foundation page. */ -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; -import { AboutFoundationPageComponent } from './about-foundation-page.component'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { PageTitleService } from 'services/page-title.service'; +import {AboutFoundationPageComponent} from './about-foundation-page.component'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {PageTitleService} from 'services/page-title.service'; class MockTranslateService { onLangChange: EventEmitter = new EventEmitter(); @@ -35,21 +35,18 @@ class MockTranslateService { describe('About foundation page', () => { let translateService: TranslateService; let pageTitleService: PageTitleService; - beforeEach(async() => { + beforeEach(async () => { TestBed.configureTestingModule({ - declarations: [ - AboutFoundationPageComponent, - MockTranslatePipe - ], + declarations: [AboutFoundationPageComponent, MockTranslatePipe], providers: [ UrlInterpolationService, PageTitleService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); @@ -57,16 +54,16 @@ describe('About foundation page', () => { beforeEach(() => { const aboutFoundationPageComponent = TestBed.createComponent( - AboutFoundationPageComponent); + AboutFoundationPageComponent + ); component = aboutFoundationPageComponent.componentInstance; translateService = TestBed.inject(TranslateService); pageTitleService = TestBed.inject(PageTitleService); }); - it('should successfully instantiate the component from beforeEach block', - () => { - expect(component).toBeDefined(); - }); + it('should successfully instantiate the component from beforeEach block', () => { + expect(component).toBeDefined(); + }); it('should set component properties when ngOnInit() is called', () => { spyOn(translateService.onLangChange, 'subscribe'); @@ -76,15 +73,18 @@ describe('About foundation page', () => { expect(translateService.onLangChange.subscribe).toHaveBeenCalled(); }); - it('should obtain translated page title whenever the selected' + - 'language changes', () => { - component.ngOnInit(); - spyOn(component, 'setPageTitle'); + it( + 'should obtain translated page title whenever the selected' + + 'language changes', + () => { + component.ngOnInit(); + spyOn(component, 'setPageTitle'); - translateService.onLangChange.emit(); + translateService.onLangChange.emit(); - expect(component.setPageTitle).toHaveBeenCalled(); - }); + expect(component.setPageTitle).toHaveBeenCalled(); + } + ); it('should set new page title', () => { spyOn(translateService, 'instant').and.callThrough(); @@ -93,9 +93,11 @@ describe('About foundation page', () => { component.setPageTitle(); expect(translateService.instant).toHaveBeenCalledWith( - 'I18N_ABOUT_FOUNDATION_PAGE_TITLE'); + 'I18N_ABOUT_FOUNDATION_PAGE_TITLE' + ); expect(pageTitleService.setDocumentTitle).toHaveBeenCalledWith( - 'I18N_ABOUT_FOUNDATION_PAGE_TITLE'); + 'I18N_ABOUT_FOUNDATION_PAGE_TITLE' + ); }); it('should unsubscribe on component destruction', () => { diff --git a/core/templates/pages/about-foundation-page/about-foundation-page.component.ts b/core/templates/pages/about-foundation-page/about-foundation-page.component.ts index ca9ee961bcbb..11d5b8c6cf98 100644 --- a/core/templates/pages/about-foundation-page/about-foundation-page.component.ts +++ b/core/templates/pages/about-foundation-page/about-foundation-page.component.ts @@ -16,19 +16,18 @@ * @fileoverview Component for the about foundation page. */ -import { Component, OnInit, OnDestroy } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; - -import { PageTitleService } from 'services/page-title.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; +import {Component, OnInit, OnDestroy} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; +import {PageTitleService} from 'services/page-title.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; @Component({ selector: 'about-foundation-page', templateUrl: './about-foundation-page.component.html', - styleUrls: [] + styleUrls: [], }) export class AboutFoundationPageComponent implements OnInit, OnDestroy { directiveSubscriptions = new Subscription(); @@ -48,7 +47,8 @@ export class AboutFoundationPageComponent implements OnInit, OnDestroy { setPageTitle(): void { let translatedTitle = this.translateService.instant( - 'I18N_ABOUT_FOUNDATION_PAGE_TITLE'); + 'I18N_ABOUT_FOUNDATION_PAGE_TITLE' + ); this.pageTitleService.setDocumentTitle(translatedTitle); } @@ -57,6 +57,9 @@ export class AboutFoundationPageComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive( - 'aboutFoundationPage', - downgradeComponent({component: AboutFoundationPageComponent})); +angular + .module('oppia') + .directive( + 'aboutFoundationPage', + downgradeComponent({component: AboutFoundationPageComponent}) + ); diff --git a/core/templates/pages/about-foundation-page/about-foundation-page.module.ts b/core/templates/pages/about-foundation-page/about-foundation-page.module.ts index 96f0ea796e51..6b290298f092 100644 --- a/core/templates/pages/about-foundation-page/about-foundation-page.module.ts +++ b/core/templates/pages/about-foundation-page/about-foundation-page.module.ts @@ -16,26 +16,26 @@ * @fileoverview Module for the about foundation page. */ -import { NgModule } from '@angular/core'; -import { AboutFoundationPageComponent } from './about-foundation-page.component'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { AboutFoundationPageRootComponent } from './about-foundation-page-root.component'; -import { CommonModule } from '@angular/common'; -import { AboutFoundationPageRoutingModule } from './about-foundation-page-routing.module'; +import {NgModule} from '@angular/core'; +import {AboutFoundationPageComponent} from './about-foundation-page.component'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {AboutFoundationPageRootComponent} from './about-foundation-page-root.component'; +import {CommonModule} from '@angular/common'; +import {AboutFoundationPageRoutingModule} from './about-foundation-page-routing.module'; @NgModule({ imports: [ CommonModule, SharedComponentsModule, - AboutFoundationPageRoutingModule + AboutFoundationPageRoutingModule, ], declarations: [ AboutFoundationPageComponent, - AboutFoundationPageRootComponent + AboutFoundationPageRootComponent, ], entryComponents: [ AboutFoundationPageComponent, - AboutFoundationPageRootComponent - ] + AboutFoundationPageRootComponent, + ], }) export class AboutFoundationPageModule {} diff --git a/core/templates/pages/about-page/about-page-root.component.spec.ts b/core/templates/pages/about-page/about-page-root.component.spec.ts index 294b556e87ca..81fd9372ee49 100644 --- a/core/templates/pages/about-page/about-page-root.component.spec.ts +++ b/core/templates/pages/about-page/about-page-root.component.spec.ts @@ -16,17 +16,17 @@ * @fileoverview Unit tests for the about page root component. */ -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; -import { AppConstants } from 'app.constants'; -import { MetaTagCustomizationService } from 'services/contextual/meta-tag-customization.service'; -import { PageHeadService } from 'services/page-head.service'; -import { PageTitleService } from 'services/page-title.service'; +import {AppConstants} from 'app.constants'; +import {MetaTagCustomizationService} from 'services/contextual/meta-tag-customization.service'; +import {PageHeadService} from 'services/page-head.service'; +import {PageTitleService} from 'services/page-title.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { AboutPageRootComponent } from './about-page-root.component'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {AboutPageRootComponent} from './about-page-root.component'; class MockTranslateService { onLangChange: EventEmitter = new EventEmitter(); @@ -43,19 +43,16 @@ describe('About Root Page', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - AboutPageRootComponent, - MockTranslatePipe - ], + declarations: [AboutPageRootComponent, MockTranslatePipe], providers: [ PageTitleService, MetaTagCustomizationService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -66,10 +63,9 @@ describe('About Root Page', () => { translateService = TestBed.inject(TranslateService); }); - it('should successfully instantiate the component', - () => { - expect(component).toBeDefined(); - }); + it('should successfully instantiate the component', () => { + expect(component).toBeDefined(); + }); it('should initialize and subscribe to onLangChange', () => { spyOn(translateService.onLangChange, 'subscribe'); @@ -95,10 +91,12 @@ describe('About Root Page', () => { component.setPageTitleAndMetaTags(); expect(translateService.instant).toHaveBeenCalledWith( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ABOUT.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ABOUT.TITLE + ); expect(pageHeadService.updateTitleAndMetaTags).toHaveBeenCalledWith( AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ABOUT.TITLE, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ABOUT.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ABOUT.META + ); }); it('should unsubscribe on component destruction', () => { diff --git a/core/templates/pages/about-page/about-page-root.component.ts b/core/templates/pages/about-page/about-page-root.component.ts index 9dc4ed8d7282..b5a386b4cf9b 100644 --- a/core/templates/pages/about-page/about-page-root.component.ts +++ b/core/templates/pages/about-page/about-page-root.component.ts @@ -16,18 +16,16 @@ * @fileoverview About-page-root component. */ -import { Component, OnDestroy } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; - - -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; +import {Component, OnDestroy} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; @Component({ selector: 'oppia-about-page-root', - templateUrl: './about-page-root.component.html' + templateUrl: './about-page-root.component.html', }) export class AboutPageRootComponent implements OnDestroy { directiveSubscriptions = new Subscription(); @@ -38,10 +36,12 @@ export class AboutPageRootComponent implements OnDestroy { setPageTitleAndMetaTags(): void { let translatedTitle = this.translateService.instant( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ABOUT.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ABOUT.TITLE + ); this.pageHeadService.updateTitleAndMetaTags( translatedTitle, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ABOUT.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ABOUT.META + ); } ngOnInit(): void { diff --git a/core/templates/pages/about-page/about-page-routing.module.ts b/core/templates/pages/about-page/about-page-routing.module.ts index b8c8f444bb48..a1f3f7f9c8a1 100644 --- a/core/templates/pages/about-page/about-page-routing.module.ts +++ b/core/templates/pages/about-page/about-page-routing.module.ts @@ -16,24 +16,19 @@ * @fileoverview Routing module for about page. */ -import { NgModule } from '@angular/core'; -import { Route, RouterModule } from '@angular/router'; -import { AboutPageRootComponent } from './about-page-root.component'; +import {NgModule} from '@angular/core'; +import {Route, RouterModule} from '@angular/router'; +import {AboutPageRootComponent} from './about-page-root.component'; const routes: Route[] = [ { path: '', - component: AboutPageRootComponent - } + component: AboutPageRootComponent, + }, ]; @NgModule({ - imports: [ - RouterModule.forChild(routes) - ], - exports: [ - RouterModule - ] + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], }) - export class AboutPageRoutingModule {} diff --git a/core/templates/pages/about-page/about-page.component.spec.ts b/core/templates/pages/about-page/about-page.component.spec.ts index 4043fcdfbe52..0005e273eb7d 100644 --- a/core/templates/pages/about-page/about-page.component.spec.ts +++ b/core/templates/pages/about-page/about-page.component.spec.ts @@ -16,16 +16,15 @@ * @fileoverview Unit tests for the about page. */ -import { TestBed } from '@angular/core/testing'; +import {TestBed} from '@angular/core/testing'; -import { AboutPageComponent } from './about-page.component'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { PrimaryButtonComponent } from '../../components/button-directives/primary-button.component'; +import {AboutPageComponent} from './about-page.component'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {PrimaryButtonComponent} from '../../components/button-directives/primary-button.component'; class MockWindowRef { nativeWindow = { @@ -34,9 +33,9 @@ class MockWindowRef { }, sessionStorage: { last_uploaded_audio_lang: 'en', - removeItem: (name: string) => {} + removeItem: (name: string) => {}, }, - gtag: () => {} + gtag: () => {}, }; } @@ -46,22 +45,22 @@ describe('About Page', () => { let siteAnalyticsService: SiteAnalyticsService; let i18nLanguageCodeService: I18nLanguageCodeService; - beforeEach(async() => { + beforeEach(async () => { windowRef = new MockWindowRef(); TestBed.configureTestingModule({ declarations: [ AboutPageComponent, MockTranslatePipe, - PrimaryButtonComponent + PrimaryButtonComponent, ], providers: [ SiteAnalyticsService, UrlInterpolationService, { provide: WindowRef, - useValue: windowRef - } - ] + useValue: windowRef, + }, + ], }).compileComponents(); const aboutPageComponent = TestBed.createComponent(AboutPageComponent); siteAnalyticsService = TestBed.inject(SiteAnalyticsService); @@ -69,52 +68,59 @@ describe('About Page', () => { component = aboutPageComponent.componentInstance; spyOn(i18nLanguageCodeService, 'isCurrentLanguageRTL').and.returnValue( - true); + true + ); }); beforeEach(angular.mock.module('oppia')); - it('should successfully instantiate the component', - () => { - expect(component).toBeDefined(); - }); - - it('should return correct static image url when calling getStaticImageUrl', - () => { - expect(component.getStaticImageUrl('/path/to/image')).toBe( - '/assets/images/path/to/image'); - }); - - it('should redirect guest user to the login page when they click' + - 'create lesson button', () => { - spyOn( - siteAnalyticsService, 'registerCreateLessonButtonEvent') - .and.callThrough(); - component.onClickCreateLessonButton(); + it('should successfully instantiate the component', () => { + expect(component).toBeDefined(); + }); - expect(siteAnalyticsService.registerCreateLessonButtonEvent) - .toHaveBeenCalledWith(); + it('should return correct static image url when calling getStaticImageUrl', () => { + expect(component.getStaticImageUrl('/path/to/image')).toBe( + '/assets/images/path/to/image' + ); }); - it('should register correct event on calling onClickVisitClassroomButton', + it( + 'should redirect guest user to the login page when they click' + + 'create lesson button', () => { spyOn( - siteAnalyticsService, 'registerClickVisitClassroomButtonEvent') - .and.callThrough(); - component.onClickVisitClassroomButton(); - - expect(siteAnalyticsService.registerClickVisitClassroomButtonEvent) - .toHaveBeenCalledWith(); - }); + siteAnalyticsService, + 'registerCreateLessonButtonEvent' + ).and.callThrough(); + component.onClickCreateLessonButton(); + + expect( + siteAnalyticsService.registerCreateLessonButtonEvent + ).toHaveBeenCalledWith(); + } + ); + + it('should register correct event on calling onClickVisitClassroomButton', () => { + spyOn( + siteAnalyticsService, + 'registerClickVisitClassroomButtonEvent' + ).and.callThrough(); + component.onClickVisitClassroomButton(); + + expect( + siteAnalyticsService.registerClickVisitClassroomButtonEvent + ).toHaveBeenCalledWith(); + }); - it('should register correct event on calling onClickBrowseLibraryButton', - () => { - spyOn( - siteAnalyticsService, 'registerClickBrowseLibraryButtonEvent') - .and.callThrough(); + it('should register correct event on calling onClickBrowseLibraryButton', () => { + spyOn( + siteAnalyticsService, + 'registerClickBrowseLibraryButtonEvent' + ).and.callThrough(); - component.onClickBrowseLibraryButton(); + component.onClickBrowseLibraryButton(); - expect(siteAnalyticsService.registerClickBrowseLibraryButtonEvent) - .toHaveBeenCalledWith(); - }); + expect( + siteAnalyticsService.registerClickBrowseLibraryButtonEvent + ).toHaveBeenCalledWith(); + }); }); diff --git a/core/templates/pages/about-page/about-page.component.ts b/core/templates/pages/about-page/about-page.component.ts index 2b2e8c5ef5e8..e5636cfcf53d 100644 --- a/core/templates/pages/about-page/about-page.component.ts +++ b/core/templates/pages/about-page/about-page.component.ts @@ -16,41 +16,46 @@ * @fileoverview Component for the about page. */ -import { Component } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; import './about-page.component.css'; @Component({ selector: 'about-page', templateUrl: './about-page.component.html', - styleUrls: ['./about-page.component.css'] + styleUrls: ['./about-page.component.css'], }) export class AboutPageComponent { - features = [{ - i18nDescription: 'I18N_ABOUT_PAGE_AUDIO_SUBTITLES_FEATURE', - imageFilename: '/about/cc.svg', - }, { - i18nDescription: 'I18N_ABOUT_PAGE_LESSON_FEATURE', - imageFilename: '/about/lesson_icon.svg' - }, { - i18nDescription: 'I18N_ABOUT_PAGE_MOBILE_FEATURE', - imageFilename: '/about/mobile_alt_solid.svg' - }, { - i18nDescription: 'I18N_ABOUT_PAGE_WIFI_FEATURE', - imageFilename: '/about/wifi_solid.svg' - }, { - i18nDescription: 'I18N_ABOUT_PAGE_LANGUAGE_FEATURE', - imageFilename: '/about/language_icon.svg' - }]; + features = [ + { + i18nDescription: 'I18N_ABOUT_PAGE_AUDIO_SUBTITLES_FEATURE', + imageFilename: '/about/cc.svg', + }, + { + i18nDescription: 'I18N_ABOUT_PAGE_LESSON_FEATURE', + imageFilename: '/about/lesson_icon.svg', + }, + { + i18nDescription: 'I18N_ABOUT_PAGE_MOBILE_FEATURE', + imageFilename: '/about/mobile_alt_solid.svg', + }, + { + i18nDescription: 'I18N_ABOUT_PAGE_WIFI_FEATURE', + imageFilename: '/about/wifi_solid.svg', + }, + { + i18nDescription: 'I18N_ABOUT_PAGE_LANGUAGE_FEATURE', + imageFilename: '/about/language_icon.svg', + }, + ]; constructor( private urlInterpolationService: UrlInterpolationService, - private siteAnalyticsService: SiteAnalyticsService, + private siteAnalyticsService: SiteAnalyticsService ) {} getStaticImageUrl(imagePath: string): string { @@ -69,5 +74,6 @@ export class AboutPageComponent { this.siteAnalyticsService.registerCreateLessonButtonEvent(); } } -angular.module('oppia').directive( - 'aboutPage', downgradeComponent({component: AboutPageComponent})); +angular + .module('oppia') + .directive('aboutPage', downgradeComponent({component: AboutPageComponent})); diff --git a/core/templates/pages/about-page/about-page.constants.ajs.ts b/core/templates/pages/about-page/about-page.constants.ajs.ts index cb5b5a7827a5..e49f20fb4b99 100644 --- a/core/templates/pages/about-page/about-page.constants.ajs.ts +++ b/core/templates/pages/about-page/about-page.constants.ajs.ts @@ -18,7 +18,8 @@ // TODO(#7092): Delete this file once migration is complete and these AngularJS // equivalents of the Angular constants are no longer needed. -import { AboutPageConstants } from 'pages/about-page/about-page.constants'; +import {AboutPageConstants} from 'pages/about-page/about-page.constants'; -angular.module('oppia').constant( - 'CREDITS_CONSTANTS', AboutPageConstants.CREDITS_CONSTANTS); +angular + .module('oppia') + .constant('CREDITS_CONSTANTS', AboutPageConstants.CREDITS_CONSTANTS); diff --git a/core/templates/pages/about-page/about-page.constants.ts b/core/templates/pages/about-page/about-page.constants.ts index 7061a3949130..3427a3b05411 100644 --- a/core/templates/pages/about-page/about-page.constants.ts +++ b/core/templates/pages/about-page/about-page.constants.ts @@ -199,7 +199,7 @@ export const AboutPageConstants = { 'Elizabeth Kemp', 'Emil Brynielsson', 'Emily Glue', - 'Eric L\'Heureux', + "Eric L'Heureux", 'Eric Lou', 'Eric Yang', 'Eshaan Aggarwal', @@ -707,5 +707,5 @@ export const AboutPageConstants = { 'Zhan Liang', 'Zhu Chu', 'Zoe Madden-Wood', - ] + ], } as const; diff --git a/core/templates/pages/about-page/about-page.module.ts b/core/templates/pages/about-page/about-page.module.ts index 9cc2cbcaef31..39b761f05bba 100644 --- a/core/templates/pages/about-page/about-page.module.ts +++ b/core/templates/pages/about-page/about-page.module.ts @@ -16,22 +16,15 @@ * @fileoverview Module for the about page. */ -import { NgModule } from '@angular/core'; -import { AboutPageComponent } from './about-page.component'; -import { AboutPageRootComponent } from './about-page-root.component'; -import { AboutPageRoutingModule } from './about-page-routing.module'; -import { CommonModule } from '@angular/common'; -import { SharedComponentsModule } from 'components/shared-component.module'; +import {NgModule} from '@angular/core'; +import {AboutPageComponent} from './about-page.component'; +import {AboutPageRootComponent} from './about-page-root.component'; +import {AboutPageRoutingModule} from './about-page-routing.module'; +import {CommonModule} from '@angular/common'; +import {SharedComponentsModule} from 'components/shared-component.module'; @NgModule({ - imports: [ - CommonModule, - SharedComponentsModule, - AboutPageRoutingModule, - ], - declarations: [ - AboutPageComponent, - AboutPageRootComponent - ], + imports: [CommonModule, SharedComponentsModule, AboutPageRoutingModule], + declarations: [AboutPageComponent, AboutPageRootComponent], }) export class AboutPageModule {} diff --git a/core/templates/pages/admin-page/activities-tab/admin-dev-mode-activities-tab.component.spec.ts b/core/templates/pages/admin-page/activities-tab/admin-dev-mode-activities-tab.component.spec.ts index d88ec14fbc0a..31b5609a639b 100644 --- a/core/templates/pages/admin-page/activities-tab/admin-dev-mode-activities-tab.component.spec.ts +++ b/core/templates/pages/admin-page/activities-tab/admin-dev-mode-activities-tab.component.spec.ts @@ -16,17 +16,20 @@ * @fileoverview Unit tests for the admin dev mode activities tab component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormsModule } from '@angular/forms'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { MaterialModule } from 'modules/material.module'; - -import { AdminBackendApiService, AdminPageData } from 'domain/admin/admin-backend-api.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { AdminDataService } from '../services/admin-data.service'; -import { AdminTaskManagerService } from '../services/admin-task-manager.service'; -import { AdminDevModeActivitiesTabComponent } from './admin-dev-mode-activities-tab.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {FormsModule} from '@angular/forms'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {MaterialModule} from 'modules/material.module'; + +import { + AdminBackendApiService, + AdminPageData, +} from 'domain/admin/admin-backend-api.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {AdminDataService} from '../services/admin-data.service'; +import {AdminTaskManagerService} from '../services/admin-task-manager.service'; +import {AdminDevModeActivitiesTabComponent} from './admin-dev-mode-activities-tab.component'; describe('Admin dev mode activities tab', () => { let component: AdminDevModeActivitiesTabComponent; @@ -37,15 +40,8 @@ describe('Admin dev mode activities tab', () => { let windowRef: WindowRef; let adminDataObject = { demoExplorationIds: ['expId'], - demoExplorations: [ - [ - '0', - 'welcome.yaml' - ] - ], - demoCollections: [ - ['collectionId'] - ], + demoExplorations: [['0', 'welcome.yaml']], + demoCollections: [['collectionId']], } as AdminPageData; let mockConfirmResult: (val: boolean) => void; @@ -55,11 +51,9 @@ describe('Admin dev mode activities tab', () => { BrowserAnimationsModule, MaterialModule, FormsModule, - HttpClientTestingModule + HttpClientTestingModule, ], - declarations: [ - AdminDevModeActivitiesTabComponent - ] + declarations: [AdminDevModeActivitiesTabComponent], }).compileComponents(); })); @@ -75,10 +69,10 @@ describe('Admin dev mode activities tab', () => { let confirmResult = true; spyOnProperty(windowRef, 'nativeWindow').and.returnValue({ - confirm: () => confirmResult + confirm: () => confirmResult, } as Window); - mockConfirmResult = (val) => { + mockConfirmResult = val => { confirmResult = val; }; @@ -100,18 +94,21 @@ describe('Admin dev mode activities tab', () => { describe('.reloadExploration', () => { it('should not reload a specific exploration if task is running', () => { let adminBackendSpy = spyOn( - adminBackendApiService, 'reloadExplorationAsync'); - spyOn( - adminTaskManagerService, 'isTaskRunning').and.returnValue(true); + adminBackendApiService, + 'reloadExplorationAsync' + ); + spyOn(adminTaskManagerService, 'isTaskRunning').and.returnValue(true); expect(component.reloadExploration('expId')).toBeUndefined(); expect(adminBackendSpy).not.toHaveBeenCalled(); }); - it('should not procees if user doesn\'t confirm', () => { + it("should not procees if user doesn't confirm", () => { mockConfirmResult(false); let adminBackendSpy = spyOn( - adminBackendApiService, 'reloadExplorationAsync'); + adminBackendApiService, + 'reloadExplorationAsync' + ); expect(component.reloadExploration('expId')).toBeUndefined(); expect(adminBackendSpy).not.toHaveBeenCalled(); @@ -120,90 +117,111 @@ describe('Admin dev mode activities tab', () => { it('should load explorations', async(() => { const expId = component.demoExplorationIds[0]; - spyOn(adminBackendApiService, 'reloadExplorationAsync') - .and.returnValue(Promise.resolve()); + spyOn(adminBackendApiService, 'reloadExplorationAsync').and.returnValue( + Promise.resolve() + ); spyOn(adminTaskManagerService, 'isTaskRunning').and.returnValue(false); spyOn(component.setStatusMessage, 'emit'); mockConfirmResult(true); component.reloadExploration(expId); - expect(component.setStatusMessage.emit) - .toHaveBeenCalledWith('Processing...'); + expect(component.setStatusMessage.emit).toHaveBeenCalledWith( + 'Processing...' + ); fixture.whenStable().then(() => { - expect(component.setStatusMessage.emit) - .toHaveBeenCalledWith('Data reloaded successfully.'); + expect(component.setStatusMessage.emit).toHaveBeenCalledWith( + 'Data reloaded successfully.' + ); }); })); it('should not load explorations with wrong exploration ID', async(() => { const expId = 'wrong-exp-id'; - spyOn(adminBackendApiService, 'reloadExplorationAsync') - .and.returnValue(Promise.reject('Exploration not found.')); + spyOn(adminBackendApiService, 'reloadExplorationAsync').and.returnValue( + Promise.reject('Exploration not found.') + ); spyOn(adminTaskManagerService, 'isTaskRunning').and.returnValue(false); spyOn(component.setStatusMessage, 'emit'); mockConfirmResult(true); component.reloadExploration(expId); - expect(component.setStatusMessage.emit) - .toHaveBeenCalledWith('Processing...'); + expect(component.setStatusMessage.emit).toHaveBeenCalledWith( + 'Processing...' + ); fixture.whenStable().then(() => { - expect(component.setStatusMessage.emit) - .toHaveBeenCalledWith('Server error: Exploration not found.'); + expect(component.setStatusMessage.emit).toHaveBeenCalledWith( + 'Server error: Exploration not found.' + ); }); })); }); describe('.printResult', () => { - it('should print correct message when numTried' + - 'is less the number of exploration', () => { - const numSucceeded = 0; - const numFailed = 0; - const numTried = 0; + it( + 'should print correct message when numTried' + + 'is less the number of exploration', + () => { + const numSucceeded = 0; + const numFailed = 0; + const numTried = 0; - spyOn(component.setStatusMessage, 'emit'); + spyOn(component.setStatusMessage, 'emit'); - component.printResult(numSucceeded, numFailed, numTried); + component.printResult(numSucceeded, numFailed, numTried); - expect(component.setStatusMessage.emit) - .toHaveBeenCalledWith('Processing...0/1'); - }); + expect(component.setStatusMessage.emit).toHaveBeenCalledWith( + 'Processing...0/1' + ); + } + ); - it('should print correct message when numTried' + - 'is not less than the number of exploration', () => { - const numSucceeded = 1; - const numFailed = 0; - const numTried = 1; + it( + 'should print correct message when numTried' + + 'is not less than the number of exploration', + () => { + const numSucceeded = 1; + const numFailed = 0; + const numTried = 1; - spyOn(component.setStatusMessage, 'emit'); + spyOn(component.setStatusMessage, 'emit'); - component.printResult(numSucceeded, numFailed, numTried); + component.printResult(numSucceeded, numFailed, numTried); - expect(component.setStatusMessage.emit).toHaveBeenCalledWith( - 'Reloaded 1 explorations: 1 succeeded, 0 failed.'); - }); + expect(component.setStatusMessage.emit).toHaveBeenCalledWith( + 'Reloaded 1 explorations: 1 succeeded, 0 failed.' + ); + } + ); }); describe('.reloadAllExplorations', () => { - it('should not reload all exploration if' + - 'reloading all exploration is not possible', () => { - component.reloadingAllExplorationPossible = false; - let adminBackendSpy = spyOn( - adminBackendApiService, 'reloadExplorationAsync'); + it( + 'should not reload all exploration if' + + 'reloading all exploration is not possible', + () => { + component.reloadingAllExplorationPossible = false; + let adminBackendSpy = spyOn( + adminBackendApiService, + 'reloadExplorationAsync' + ); - component.reloadAllExplorations(); + component.reloadAllExplorations(); - expect(adminBackendSpy).not.toHaveBeenCalled(); - expect(component.reloadAllExplorations()).toBeUndefined(); - }); + expect(adminBackendSpy).not.toHaveBeenCalled(); + expect(component.reloadAllExplorations()).toBeUndefined(); + } + ); it('should not reload all exploration if any task is running', () => { let adminBackendSpy = spyOn( - adminBackendApiService, 'reloadExplorationAsync'); + adminBackendApiService, + 'reloadExplorationAsync' + ); spyOn(adminTaskManagerService, 'isTaskRunning').and.returnValue(true); component.reloadAllExplorations(); @@ -212,9 +230,11 @@ describe('Admin dev mode activities tab', () => { expect(adminBackendSpy).not.toHaveBeenCalled(); }); - it('should not reload all exploration without user\'s confirmation', () => { + it("should not reload all exploration without user's confirmation", () => { let adminBackendSpy = spyOn( - adminBackendApiService, 'reloadExplorationAsync'); + adminBackendApiService, + 'reloadExplorationAsync' + ); spyOn(adminTaskManagerService, 'isTaskRunning').and.returnValue(false); component.reloadingAllExplorationPossible = true; @@ -231,316 +251,388 @@ describe('Admin dev mode activities tab', () => { component.demoExplorationIds = demoExplorationIds; component.reloadingAllExplorationPossible = true; - spyOn(adminBackendApiService, 'reloadExplorationAsync') - .and.returnValue(Promise.resolve()); + spyOn(adminBackendApiService, 'reloadExplorationAsync').and.returnValue( + Promise.resolve() + ); spyOn(adminTaskManagerService, 'isTaskRunning').and.returnValue(false); spyOn(component.setStatusMessage, 'emit'); mockConfirmResult(true); component.reloadAllExplorations(); - expect(component.setStatusMessage.emit) - .toHaveBeenCalledWith('Processing...'); + expect(component.setStatusMessage.emit).toHaveBeenCalledWith( + 'Processing...' + ); fixture.whenStable().then(() => { expect(component.setStatusMessage.emit).toHaveBeenCalledWith( - 'Reloaded 1 explorations: 1 succeeded, 0 failed.'); + 'Reloaded 1 explorations: 1 succeeded, 0 failed.' + ); }); })); - it('should not reload all exploration if exploration ID is wrong', - async(() => { - const demoExplorationIds = ['wrongId']; - component.demoExplorationIds = demoExplorationIds; - component.reloadingAllExplorationPossible = true; + it('should not reload all exploration if exploration ID is wrong', async(() => { + const demoExplorationIds = ['wrongId']; + component.demoExplorationIds = demoExplorationIds; + component.reloadingAllExplorationPossible = true; - spyOn(adminBackendApiService, 'reloadExplorationAsync') - .and.returnValue(Promise.reject('Exploration not found.')); - spyOn(adminTaskManagerService, 'isTaskRunning').and.returnValue(false); - spyOn(component.setStatusMessage, 'emit'); + spyOn(adminBackendApiService, 'reloadExplorationAsync').and.returnValue( + Promise.reject('Exploration not found.') + ); + spyOn(adminTaskManagerService, 'isTaskRunning').and.returnValue(false); + spyOn(component.setStatusMessage, 'emit'); - mockConfirmResult(true); - component.reloadAllExplorations(); + mockConfirmResult(true); + component.reloadAllExplorations(); - expect(component.setStatusMessage.emit) - .toHaveBeenCalledWith('Processing...'); + expect(component.setStatusMessage.emit).toHaveBeenCalledWith( + 'Processing...' + ); - fixture.whenStable().then(() => { - expect(component.setStatusMessage.emit).toHaveBeenCalledWith( - 'Reloaded 1 explorations: 0 succeeded, 1 failed.'); - }); - })); + fixture.whenStable().then(() => { + expect(component.setStatusMessage.emit).toHaveBeenCalledWith( + 'Reloaded 1 explorations: 0 succeeded, 1 failed.' + ); + }); + })); }); describe('.generateDummyExploration', () => { - it('should not generate dummy exploration if publish count is greater' + - 'than generate count', () => { - let adminBackendSpy = spyOn( - adminBackendApiService, 'generateDummyExplorationsAsync'); + it( + 'should not generate dummy exploration if publish count is greater' + + 'than generate count', + () => { + let adminBackendSpy = spyOn( + adminBackendApiService, + 'generateDummyExplorationsAsync' + ); - component.numDummyExpsToPublish = 2; - component.numDummyExpsToGenerate = 1; + component.numDummyExpsToPublish = 2; + component.numDummyExpsToGenerate = 1; - spyOn(component.setStatusMessage, 'emit'); + spyOn(component.setStatusMessage, 'emit'); - component.generateDummyExplorations(); + component.generateDummyExplorations(); - expect(component.setStatusMessage.emit).toHaveBeenCalledWith( - 'Publish count should be less than or equal to generate count'); - expect(adminBackendSpy).not.toHaveBeenCalled(); - }); + expect(component.setStatusMessage.emit).toHaveBeenCalledWith( + 'Publish count should be less than or equal to generate count' + ); + expect(adminBackendSpy).not.toHaveBeenCalled(); + } + ); it('should generate dummy explorations', async(() => { component.numDummyExpsToPublish = 1; component.numDummyExpsToGenerate = 2; - spyOn(adminBackendApiService, 'generateDummyExplorationsAsync') - .and.returnValue(Promise.resolve()); + spyOn( + adminBackendApiService, + 'generateDummyExplorationsAsync' + ).and.returnValue(Promise.resolve()); spyOn(component.setStatusMessage, 'emit'); component.generateDummyExplorations(); - expect(component.setStatusMessage.emit) - .toHaveBeenCalledWith('Processing...'); + expect(component.setStatusMessage.emit).toHaveBeenCalledWith( + 'Processing...' + ); fixture.whenStable().then(() => { expect(component.setStatusMessage.emit).toHaveBeenCalledWith( - 'Dummy explorations generated successfully.'); + 'Dummy explorations generated successfully.' + ); }); })); - it('should show error message when dummy explorations' + - 'are not generated', async(() => { - component.numDummyExpsToPublish = 2; - component.numDummyExpsToGenerate = 2; - - spyOn(adminBackendApiService, 'generateDummyExplorationsAsync') - .and.returnValue(Promise.reject('Dummy explorations not generated.')); - spyOn(component.setStatusMessage, 'emit'); + it( + 'should show error message when dummy explorations' + 'are not generated', + async(() => { + component.numDummyExpsToPublish = 2; + component.numDummyExpsToGenerate = 2; - component.generateDummyExplorations(); + spyOn( + adminBackendApiService, + 'generateDummyExplorationsAsync' + ).and.returnValue(Promise.reject('Dummy explorations not generated.')); + spyOn(component.setStatusMessage, 'emit'); - expect(component.setStatusMessage.emit) - .toHaveBeenCalledWith('Processing...'); + component.generateDummyExplorations(); - fixture.whenStable().then(() => { expect(component.setStatusMessage.emit).toHaveBeenCalledWith( - 'Server error: Dummy explorations not generated.'); - }); - })); + 'Processing...' + ); + + fixture.whenStable().then(() => { + expect(component.setStatusMessage.emit).toHaveBeenCalledWith( + 'Server error: Dummy explorations not generated.' + ); + }); + }) + ); }); describe('.generateDummyBlogPost', () => { it('should generate dummy blog post', async(() => { - spyOn(adminBackendApiService, 'generateDummyBlogPostAsync') - .and.returnValue(Promise.resolve()); + spyOn( + adminBackendApiService, + 'generateDummyBlogPostAsync' + ).and.returnValue(Promise.resolve()); spyOn(component.setStatusMessage, 'emit'); component.generateNewBlogPost('Education'); - expect(component.setStatusMessage.emit) - .toHaveBeenCalledWith('Processing...'); + expect(component.setStatusMessage.emit).toHaveBeenCalledWith( + 'Processing...' + ); fixture.whenStable().then(() => { expect(component.setStatusMessage.emit).toHaveBeenCalledWith( - 'Dummy Blog Post generated successfully.'); + 'Dummy Blog Post generated successfully.' + ); }); })); - it('should show error message if new dummy blog post ' + - 'title is empty', async(() => { - spyOn(component.setStatusMessage, 'emit'); - - component.generateNewBlogPost(''); + it( + 'should show error message if new dummy blog post ' + 'title is empty', + async(() => { + spyOn(component.setStatusMessage, 'emit'); - expect(component.setStatusMessage.emit) - .toHaveBeenCalledWith('Internal error: blogPostTitle is empty'); - })); + component.generateNewBlogPost(''); - it('should show error message if new dummy blog post ' + - 'is not generated', async(() => { - spyOn(adminBackendApiService, 'generateDummyBlogPostAsync') - .and.returnValue(Promise.reject('Dummy Blog not generated.')); - spyOn(component.setStatusMessage, 'emit'); + expect(component.setStatusMessage.emit).toHaveBeenCalledWith( + 'Internal error: blogPostTitle is empty' + ); + }) + ); - component.generateNewBlogPost('Education'); + it( + 'should show error message if new dummy blog post ' + 'is not generated', + async(() => { + spyOn( + adminBackendApiService, + 'generateDummyBlogPostAsync' + ).and.returnValue(Promise.reject('Dummy Blog not generated.')); + spyOn(component.setStatusMessage, 'emit'); - expect(component.setStatusMessage.emit) - .toHaveBeenCalledWith('Processing...'); + component.generateNewBlogPost('Education'); - fixture.whenStable().then(() => { expect(component.setStatusMessage.emit).toHaveBeenCalledWith( - 'Server error: Dummy Blog not generated.'); - }); - })); + 'Processing...' + ); + + fixture.whenStable().then(() => { + expect(component.setStatusMessage.emit).toHaveBeenCalledWith( + 'Server error: Dummy Blog not generated.' + ); + }); + }) + ); }); describe('.loadNewStructuresData', () => { it('should generate structures data', async(() => { - spyOn(adminBackendApiService, 'generateDummyNewStructuresDataAsync') - .and.returnValue(Promise.resolve()); + spyOn( + adminBackendApiService, + 'generateDummyNewStructuresDataAsync' + ).and.returnValue(Promise.resolve()); spyOn(component.setStatusMessage, 'emit'); component.loadNewStructuresData(); - expect(component.setStatusMessage.emit) - .toHaveBeenCalledWith('Processing...'); + expect(component.setStatusMessage.emit).toHaveBeenCalledWith( + 'Processing...' + ); fixture.whenStable().then(() => { expect(component.setStatusMessage.emit).toHaveBeenCalledWith( - 'Dummy new structures data generated successfully.'); + 'Dummy new structures data generated successfully.' + ); }); })); - it('should show error message if new structues data' + - 'is not generated', async(() => { - spyOn(adminBackendApiService, 'generateDummyNewStructuresDataAsync') - .and.returnValue(Promise.reject('New structures not generated.')); - spyOn(component.setStatusMessage, 'emit'); - component.loadNewStructuresData(); - - expect(component.setStatusMessage.emit) - .toHaveBeenCalledWith('Processing...'); + it( + 'should show error message if new structues data' + 'is not generated', + async(() => { + spyOn( + adminBackendApiService, + 'generateDummyNewStructuresDataAsync' + ).and.returnValue(Promise.reject('New structures not generated.')); + spyOn(component.setStatusMessage, 'emit'); + component.loadNewStructuresData(); - fixture.whenStable().then(() => { expect(component.setStatusMessage.emit).toHaveBeenCalledWith( - 'Server error: New structures not generated.'); - }); - })); + 'Processing...' + ); + + fixture.whenStable().then(() => { + expect(component.setStatusMessage.emit).toHaveBeenCalledWith( + 'Server error: New structures not generated.' + ); + }); + }) + ); }); describe('.generateNewSkillData', () => { it('should generate structures data', async(() => { - spyOn(adminBackendApiService, 'generateDummyNewSkillDataAsync') - .and.returnValue(Promise.resolve()); + spyOn( + adminBackendApiService, + 'generateDummyNewSkillDataAsync' + ).and.returnValue(Promise.resolve()); spyOn(component.setStatusMessage, 'emit'); component.generateNewSkillData(); - expect(component.setStatusMessage.emit) - .toHaveBeenCalledWith('Processing...'); + expect(component.setStatusMessage.emit).toHaveBeenCalledWith( + 'Processing...' + ); fixture.whenStable().then(() => { expect(component.setStatusMessage.emit).toHaveBeenCalledWith( - 'Dummy new skill and questions generated successfully.'); + 'Dummy new skill and questions generated successfully.' + ); }); })); - it('should show error message if new structues data' + - 'is not generated', async(() => { - spyOn(adminBackendApiService, 'generateDummyNewSkillDataAsync') - .and.returnValue(Promise.reject('New skill data not generated.')); - spyOn(component.setStatusMessage, 'emit'); - component.generateNewSkillData(); - - expect(component.setStatusMessage.emit) - .toHaveBeenCalledWith('Processing...'); + it( + 'should show error message if new structues data' + 'is not generated', + async(() => { + spyOn( + adminBackendApiService, + 'generateDummyNewSkillDataAsync' + ).and.returnValue(Promise.reject('New skill data not generated.')); + spyOn(component.setStatusMessage, 'emit'); + component.generateNewSkillData(); - fixture.whenStable().then(() => { expect(component.setStatusMessage.emit).toHaveBeenCalledWith( - 'Server error: New skill data not generated.'); - }); - })); + 'Processing...' + ); + + fixture.whenStable().then(() => { + expect(component.setStatusMessage.emit).toHaveBeenCalledWith( + 'Server error: New skill data not generated.' + ); + }); + }) + ); }); describe('.generateNewClassroom', () => { it('should generate classroom data', async(() => { - spyOn(adminBackendApiService, 'generateDummyClassroomDataAsync') - .and.returnValue(Promise.resolve()); + spyOn( + adminBackendApiService, + 'generateDummyClassroomDataAsync' + ).and.returnValue(Promise.resolve()); spyOn(component.setStatusMessage, 'emit'); component.generateNewClassroom(); - expect(component.setStatusMessage.emit) - .toHaveBeenCalledWith('Processing...'); + expect(component.setStatusMessage.emit).toHaveBeenCalledWith( + 'Processing...' + ); // The status message changes after the completion of the asynchronous // call, thus the whenStable method is used to detect the changes and // validate accordingly. fixture.whenStable().then(() => { expect(component.setStatusMessage.emit).toHaveBeenCalledWith( - 'Dummy new classroom generated successfully.'); + 'Dummy new classroom generated successfully.' + ); }); })); - it( - 'should show error message if new classroom data is not generated', - async(() => { - spyOn(adminBackendApiService, 'generateDummyClassroomDataAsync') - .and.returnValue(Promise.reject('New classroom data not generated.')); - spyOn(component.setStatusMessage, 'emit'); - component.generateNewClassroom(); + it('should show error message if new classroom data is not generated', async(() => { + spyOn( + adminBackendApiService, + 'generateDummyClassroomDataAsync' + ).and.returnValue(Promise.reject('New classroom data not generated.')); + spyOn(component.setStatusMessage, 'emit'); + component.generateNewClassroom(); - expect(component.setStatusMessage.emit) - .toHaveBeenCalledWith('Processing...'); + expect(component.setStatusMessage.emit).toHaveBeenCalledWith( + 'Processing...' + ); - // The status message changes after the completion of the asynchronous - // call, thus the whenStable method is used to detect the changes and - // validate accordingly. - fixture.whenStable().then(() => { - expect(component.setStatusMessage.emit).toHaveBeenCalledWith( - 'Server error: New classroom data not generated.'); - }); - })); + // The status message changes after the completion of the asynchronous + // call, thus the whenStable method is used to detect the changes and + // validate accordingly. + fixture.whenStable().then(() => { + expect(component.setStatusMessage.emit).toHaveBeenCalledWith( + 'Server error: New classroom data not generated.' + ); + }); + })); }); describe('.reloadCollection', () => { it('should not reload collection if a task is already running', () => { let adminBackendSpy = spyOn( - adminBackendApiService, 'reloadCollectionAsync'); + adminBackendApiService, + 'reloadCollectionAsync' + ); spyOn(adminTaskManagerService, 'isTaskRunning').and.returnValue(true); - expect(component.reloadCollection(component.DEMO_COLLECTIONS[0][0])) - .toBeUndefined(); + expect( + component.reloadCollection(component.DEMO_COLLECTIONS[0][0]) + ).toBeUndefined(); expect(adminBackendSpy).not.toHaveBeenCalled(); }); - it('should not reload collection without user\'s confirmation', () => { + it("should not reload collection without user's confirmation", () => { let adminBackendSpy = spyOn( - adminBackendApiService, 'reloadCollectionAsync'); + adminBackendApiService, + 'reloadCollectionAsync' + ); spyOn(adminTaskManagerService, 'isTaskRunning').and.returnValue(false); mockConfirmResult(false); component.reloadCollection(component.DEMO_COLLECTIONS[0][0]); - expect(component.reloadCollection(component.DEMO_COLLECTIONS[0][0])) - .toBeUndefined(); + expect( + component.reloadCollection(component.DEMO_COLLECTIONS[0][0]) + ).toBeUndefined(); expect(adminBackendSpy).not.toHaveBeenCalled(); }); it('should reload collection', async(() => { - spyOn(adminBackendApiService, 'reloadCollectionAsync') - .and.returnValue(Promise.resolve()); + spyOn(adminBackendApiService, 'reloadCollectionAsync').and.returnValue( + Promise.resolve() + ); spyOn(component.setStatusMessage, 'emit'); spyOn(adminTaskManagerService, 'isTaskRunning').and.returnValue(false); mockConfirmResult(true); component.reloadCollection(component.DEMO_COLLECTIONS[0][0]); - expect(component.setStatusMessage.emit) - .toHaveBeenCalledWith('Processing...'); + expect(component.setStatusMessage.emit).toHaveBeenCalledWith( + 'Processing...' + ); fixture.whenStable().then(() => { expect(component.setStatusMessage.emit).toHaveBeenCalledWith( - 'Data reloaded successfully.'); + 'Data reloaded successfully.' + ); }); })); it('should show error message is collection is not reloaded', async(() => { const wrongCollectionId = 'wrongCollectionId'; - spyOn(adminBackendApiService, 'reloadCollectionAsync') - .and.returnValue(Promise.reject('Wrong collection ID.')); + spyOn(adminBackendApiService, 'reloadCollectionAsync').and.returnValue( + Promise.reject('Wrong collection ID.') + ); spyOn(component.setStatusMessage, 'emit'); spyOn(adminTaskManagerService, 'isTaskRunning').and.returnValue(false); mockConfirmResult(true); component.reloadCollection(wrongCollectionId); - expect(component.setStatusMessage.emit) - .toHaveBeenCalledWith('Processing...'); + expect(component.setStatusMessage.emit).toHaveBeenCalledWith( + 'Processing...' + ); fixture.whenStable().then(() => { expect(component.setStatusMessage.emit).toHaveBeenCalledWith( - 'Server error: Wrong collection ID.'); + 'Server error: Wrong collection ID.' + ); }); })); }); diff --git a/core/templates/pages/admin-page/activities-tab/admin-dev-mode-activities-tab.component.ts b/core/templates/pages/admin-page/activities-tab/admin-dev-mode-activities-tab.component.ts index 33661d6cf32f..0d7e376cdc46 100644 --- a/core/templates/pages/admin-page/activities-tab/admin-dev-mode-activities-tab.component.ts +++ b/core/templates/pages/admin-page/activities-tab/admin-dev-mode-activities-tab.component.ts @@ -17,12 +17,12 @@ * is in developer mode. */ -import { Component, Output, OnInit, EventEmitter } from '@angular/core'; +import {Component, Output, OnInit, EventEmitter} from '@angular/core'; -import { AdminBackendApiService } from 'domain/admin/admin-backend-api.service'; -import { AdminDataService } from 'pages/admin-page/services/admin-data.service'; -import { AdminTaskManagerService } from 'pages/admin-page/services/admin-task-manager.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import {AdminBackendApiService} from 'domain/admin/admin-backend-api.service'; +import {AdminDataService} from 'pages/admin-page/services/admin-data.service'; +import {AdminTaskManagerService} from 'pages/admin-page/services/admin-task-manager.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; @Component({ selector: 'oppia-admin-dev-mode-activities-tab', @@ -53,8 +53,11 @@ export class AdminDevModeActivitiesTabComponent implements OnInit { if (this.adminTaskManagerService.isTaskRunning()) { return; } - if (!this.windowRef.nativeWindow.confirm( - 'This action is irreversible. Are you sure?')) { + if ( + !this.windowRef.nativeWindow.confirm( + 'This action is irreversible. Are you sure?' + ) + ) { return; } @@ -62,29 +65,34 @@ export class AdminDevModeActivitiesTabComponent implements OnInit { this.adminTaskManagerService.startTask(); - this.adminBackendApiService.reloadExplorationAsync( - explorationId - ).then(() => { - this.setStatusMessage.emit('Data reloaded successfully.'); - this.adminTaskManagerService.finishTask(); - }, (errorResponse) => { - this.setStatusMessage.emit( - 'Server error: ' + errorResponse); - this.adminTaskManagerService.finishTask(); - }); + this.adminBackendApiService.reloadExplorationAsync(explorationId).then( + () => { + this.setStatusMessage.emit('Data reloaded successfully.'); + this.adminTaskManagerService.finishTask(); + }, + errorResponse => { + this.setStatusMessage.emit('Server error: ' + errorResponse); + this.adminTaskManagerService.finishTask(); + } + ); } printResult(numSucceeded: number, numFailed: number, numTried: number): void { if (numTried < this.demoExplorationIds.length) { this.setStatusMessage.emit( - 'Processing...' + numTried + '/' + - this.demoExplorationIds.length); + 'Processing...' + numTried + '/' + this.demoExplorationIds.length + ); return; } this.setStatusMessage.emit( - 'Reloaded ' + this.demoExplorationIds.length + - ' explorations: ' + numSucceeded + ' succeeded, ' + numFailed + - ' failed.'); + 'Reloaded ' + + this.demoExplorationIds.length + + ' explorations: ' + + numSucceeded + + ' succeeded, ' + + numFailed + + ' failed.' + ); this.adminTaskManagerService.finishTask(); } @@ -95,8 +103,11 @@ export class AdminDevModeActivitiesTabComponent implements OnInit { if (this.adminTaskManagerService.isTaskRunning()) { return; } - if (!this.windowRef.nativeWindow.confirm( - 'This action is irreversible. Are you sure?')) { + if ( + !this.windowRef.nativeWindow.confirm( + 'This action is irreversible. Are you sure?' + ) + ) { return; } @@ -110,17 +121,18 @@ export class AdminDevModeActivitiesTabComponent implements OnInit { for (var i = 0; i < this.demoExplorationIds.length; ++i) { var explorationId = this.demoExplorationIds[i]; - this.adminBackendApiService.reloadExplorationAsync( - explorationId - ).then(() => { - ++numSucceeded; - ++numTried; - this.printResult(numSucceeded, numFailed, numTried); - }, () => { - ++numFailed; - ++numTried; - this.printResult(numSucceeded, numFailed, numTried); - }); + this.adminBackendApiService.reloadExplorationAsync(explorationId).then( + () => { + ++numSucceeded; + ++numTried; + this.printResult(numSucceeded, numFailed, numTried); + }, + () => { + ++numFailed; + ++numTried; + this.printResult(numSucceeded, numFailed, numTried); + } + ); } } @@ -128,20 +140,27 @@ export class AdminDevModeActivitiesTabComponent implements OnInit { // Generate dummy explorations with random title. if (this.numDummyExpsToPublish > this.numDummyExpsToGenerate) { this.setStatusMessage.emit( - 'Publish count should be less than or equal to generate count'); + 'Publish count should be less than or equal to generate count' + ); return; } this.adminTaskManagerService.startTask(); this.setStatusMessage.emit('Processing...'); - this.adminBackendApiService.generateDummyExplorationsAsync( - this.numDummyExpsToGenerate, this.numDummyExpsToPublish - ).then(() => { - this.setStatusMessage.emit( - 'Dummy explorations generated successfully.'); - }, (errorResponse) => { - this.setStatusMessage.emit( - 'Server error: ' + errorResponse); - }); + this.adminBackendApiService + .generateDummyExplorationsAsync( + this.numDummyExpsToGenerate, + this.numDummyExpsToPublish + ) + .then( + () => { + this.setStatusMessage.emit( + 'Dummy explorations generated successfully.' + ); + }, + errorResponse => { + this.setStatusMessage.emit('Server error: ' + errorResponse); + } + ); this.adminTaskManagerService.finishTask(); } @@ -149,14 +168,16 @@ export class AdminDevModeActivitiesTabComponent implements OnInit { this.adminTaskManagerService.startTask(); this.setStatusMessage.emit('Processing...'); - this.adminBackendApiService.generateDummyNewStructuresDataAsync() - .then(() => { - this.setStatusMessage.emit( - 'Dummy new structures data generated successfully.'); - }, (errorResponse) => { + this.adminBackendApiService.generateDummyNewStructuresDataAsync().then( + () => { this.setStatusMessage.emit( - 'Server error: ' + errorResponse); - }); + 'Dummy new structures data generated successfully.' + ); + }, + errorResponse => { + this.setStatusMessage.emit('Server error: ' + errorResponse); + } + ); this.adminTaskManagerService.finishTask(); } @@ -164,14 +185,16 @@ export class AdminDevModeActivitiesTabComponent implements OnInit { this.adminTaskManagerService.startTask(); this.setStatusMessage.emit('Processing...'); - this.adminBackendApiService.generateDummyNewSkillDataAsync() - .then(() => { + this.adminBackendApiService.generateDummyNewSkillDataAsync().then( + () => { this.setStatusMessage.emit( - 'Dummy new skill and questions generated successfully.'); - }, (errorResponse) => { - this.setStatusMessage.emit( - 'Server error: ' + errorResponse); - }); + 'Dummy new skill and questions generated successfully.' + ); + }, + errorResponse => { + this.setStatusMessage.emit('Server error: ' + errorResponse); + } + ); this.adminTaskManagerService.finishTask(); } @@ -181,12 +204,14 @@ export class AdminDevModeActivitiesTabComponent implements OnInit { } this.adminTaskManagerService.startTask(); this.setStatusMessage.emit('Processing...'); - this.adminBackendApiService.generateDummyBlogPostAsync(blogPostTitle) - .then(() => { + this.adminBackendApiService.generateDummyBlogPostAsync(blogPostTitle).then( + () => { this.setStatusMessage.emit('Dummy Blog Post generated successfully.'); - }, (errorResponse) => { + }, + errorResponse => { this.setStatusMessage.emit('Server error: ' + errorResponse); - }); + } + ); this.adminTaskManagerService.finishTask(); } @@ -194,12 +219,16 @@ export class AdminDevModeActivitiesTabComponent implements OnInit { this.adminTaskManagerService.startTask(); this.setStatusMessage.emit('Processing...'); - this.adminBackendApiService.generateDummyClassroomDataAsync().then(() => { - this.setStatusMessage.emit('Dummy new classroom generated successfully.'); - }, (errorResponse) => { - this.setStatusMessage.emit( - 'Server error: ' + errorResponse); - }); + this.adminBackendApiService.generateDummyClassroomDataAsync().then( + () => { + this.setStatusMessage.emit( + 'Dummy new classroom generated successfully.' + ); + }, + errorResponse => { + this.setStatusMessage.emit('Server error: ' + errorResponse); + } + ); this.adminTaskManagerService.finishTask(); } @@ -207,8 +236,11 @@ export class AdminDevModeActivitiesTabComponent implements OnInit { if (this.adminTaskManagerService.isTaskRunning()) { return; } - if (!this.windowRef.nativeWindow.confirm( - 'This action is irreversible. Are you sure?')) { + if ( + !this.windowRef.nativeWindow.confirm( + 'This action is irreversible. Are you sure?' + ) + ) { return; } @@ -216,13 +248,14 @@ export class AdminDevModeActivitiesTabComponent implements OnInit { this.adminTaskManagerService.startTask(); - this.adminBackendApiService.reloadCollectionAsync(collectionId) - .then(() => { + this.adminBackendApiService.reloadCollectionAsync(collectionId).then( + () => { this.setStatusMessage.emit('Data reloaded successfully.'); - }, (errorResponse) => { - this.setStatusMessage.emit( - 'Server error: ' + errorResponse); - }); + }, + errorResponse => { + this.setStatusMessage.emit('Server error: ' + errorResponse); + } + ); this.adminTaskManagerService.finishTask(); } diff --git a/core/templates/pages/admin-page/activities-tab/admin-prod-mode-activities-tab.component.ts b/core/templates/pages/admin-page/activities-tab/admin-prod-mode-activities-tab.component.ts index f5ee0dbfaa48..85be86bf2780 100644 --- a/core/templates/pages/admin-page/activities-tab/admin-prod-mode-activities-tab.component.ts +++ b/core/templates/pages/admin-page/activities-tab/admin-prod-mode-activities-tab.component.ts @@ -16,11 +16,11 @@ * @fileoverview Component for the activities tab in the admin panel when Oppia * is in production mode. */ -import { Component } from '@angular/core'; +import {Component} from '@angular/core'; @Component({ selector: 'oppia-admin-prod-mode-activities-tab', templateUrl: './admin-prod-mode-activities-tab.component.html', - styleUrls: [] + styleUrls: [], }) export class OppiaAdminProdModeActivitiesTabComponent {} diff --git a/core/templates/pages/admin-page/admin-auth.guard.spec.ts b/core/templates/pages/admin-page/admin-auth.guard.spec.ts index da2970059564..61407ead8161 100644 --- a/core/templates/pages/admin-page/admin-auth.guard.spec.ts +++ b/core/templates/pages/admin-page/admin-auth.guard.spec.ts @@ -16,14 +16,19 @@ * @fileoverview Tests for AdminAuthGuard */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TestBed } from '@angular/core/testing'; -import { ActivatedRouteSnapshot, Router, RouterStateSnapshot, NavigationExtras } from '@angular/router'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TestBed} from '@angular/core/testing'; +import { + ActivatedRouteSnapshot, + Router, + RouterStateSnapshot, + NavigationExtras, +} from '@angular/router'; -import { AppConstants } from 'app.constants'; -import { UserInfo } from 'domain/user/user-info.model'; -import { UserService } from 'services/user.service'; -import { AdminAuthGuard } from './admin-auth.guard'; +import {AppConstants} from 'app.constants'; +import {UserInfo} from 'domain/user/user-info.model'; +import {UserService} from 'services/user.service'; +import {AdminAuthGuard} from './admin-auth.guard'; class MockRouter { navigate(commands: string[], extras?: NavigationExtras): Promise { @@ -39,7 +44,7 @@ describe('AdminAuthGuard', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [UserService, { provide: Router, useClass: MockRouter }], + providers: [UserService, {provide: Router, useClass: MockRouter}], }).compileComponents(); guard = TestBed.inject(AdminAuthGuard); @@ -47,35 +52,39 @@ describe('AdminAuthGuard', () => { router = TestBed.inject(Router); }); - it('should redirect user to 401 page if user is not super admin', (done) => { + it('should redirect user to 401 page if user is not super admin', done => { const getUserInfoAsyncSpy = spyOn( - userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(UserInfo.createDefault()) - ); + userService, + 'getUserInfoAsync' + ).and.returnValue(Promise.resolve(UserInfo.createDefault())); const navigateSpy = spyOn(router, 'navigate').and.callThrough(); - guard.canActivate( - new ActivatedRouteSnapshot(), {} as RouterStateSnapshot).then( - (canActivate) => { + guard + .canActivate(new ActivatedRouteSnapshot(), {} as RouterStateSnapshot) + .then(canActivate => { expect(canActivate).toBeFalse(); expect(getUserInfoAsyncSpy).toHaveBeenCalledTimes(1); expect(navigateSpy).toHaveBeenCalledWith([ - `${AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ERROR.ROUTE}/401`]); + `${AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ERROR.ROUTE}/401`, + ]); done(); }); }); - it('should not redirect user to 401 page if user is super admin', (done) => { + it('should not redirect user to 401 page if user is super admin', done => { const getUserInfoAsyncSpy = spyOn( - userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(new UserInfo( - [], false, false, true, false, false, '', '', '', true)) + userService, + 'getUserInfoAsync' + ).and.returnValue( + Promise.resolve( + new UserInfo([], false, false, true, false, false, '', '', '', true) + ) ); const navigateSpy = spyOn(router, 'navigate').and.callThrough(); - guard.canActivate( - new ActivatedRouteSnapshot(), {} as RouterStateSnapshot).then( - (canActivate) => { + guard + .canActivate(new ActivatedRouteSnapshot(), {} as RouterStateSnapshot) + .then(canActivate => { expect(canActivate).toBeTrue(); expect(getUserInfoAsyncSpy).toHaveBeenCalledTimes(1); expect(navigateSpy).not.toHaveBeenCalled(); diff --git a/core/templates/pages/admin-page/admin-auth.guard.ts b/core/templates/pages/admin-page/admin-auth.guard.ts index 62c74dd2a11c..58c3d433a3fd 100644 --- a/core/templates/pages/admin-page/admin-auth.guard.ts +++ b/core/templates/pages/admin-page/admin-auth.guard.ts @@ -17,8 +17,8 @@ * if the user is not a super admin. */ -import { Location } from '@angular/common'; -import { Injectable } from '@angular/core'; +import {Location} from '@angular/common'; +import {Injectable} from '@angular/core'; import { ActivatedRouteSnapshot, CanActivate, @@ -26,11 +26,11 @@ import { RouterStateSnapshot, } from '@angular/router'; -import { AppConstants } from 'app.constants'; -import { UserService } from 'services/user.service'; +import {AppConstants} from 'app.constants'; +import {UserService} from 'services/user.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class AdminAuthGuard implements CanActivate { constructor( @@ -40,19 +40,21 @@ export class AdminAuthGuard implements CanActivate { ) {} async canActivate( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot ): Promise { const userInfo = await this.userService.getUserInfoAsync(); if (userInfo.isSuperAdmin()) { return true; } - this.router.navigate( - [`${AppConstants.PAGES_REGISTERED_WITH_FRONTEND - .ERROR.ROUTE}/401`]).then(() => { - this.location.replaceState(state.url); - }); + this.router + .navigate([ + `${AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ERROR.ROUTE}/401`, + ]) + .then(() => { + this.location.replaceState(state.url); + }); return false; } } diff --git a/core/templates/pages/admin-page/admin-blog-admin-common.module.ts b/core/templates/pages/admin-page/admin-blog-admin-common.module.ts index faceb0b22f9e..117a6321270f 100644 --- a/core/templates/pages/admin-page/admin-blog-admin-common.module.ts +++ b/core/templates/pages/admin-page/admin-blog-admin-common.module.ts @@ -19,25 +19,15 @@ import 'core-js/es7/reflect'; import 'zone.js'; -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { RolesAndActionsVisualizerComponent } from './roles-tab/roles-and-actions-visualizer.component'; -import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import {CommonModule} from '@angular/common'; +import {NgModule} from '@angular/core'; +import {RolesAndActionsVisualizerComponent} from './roles-tab/roles-and-actions-visualizer.component'; +import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; @NgModule({ - imports: [ - CommonModule, - MatProgressSpinnerModule - ], - declarations: [ - RolesAndActionsVisualizerComponent - ], - entryComponents: [ - RolesAndActionsVisualizerComponent - ], - exports: [ - RolesAndActionsVisualizerComponent - ], + imports: [CommonModule, MatProgressSpinnerModule], + declarations: [RolesAndActionsVisualizerComponent], + entryComponents: [RolesAndActionsVisualizerComponent], + exports: [RolesAndActionsVisualizerComponent], }) - -export class AdminBlogAdminCommonModule { } +export class AdminBlogAdminCommonModule {} diff --git a/core/templates/pages/admin-page/admin-page-root.component.spec.ts b/core/templates/pages/admin-page/admin-page-root.component.spec.ts index f905aec8479c..7b2000621be9 100644 --- a/core/templates/pages/admin-page/admin-page-root.component.spec.ts +++ b/core/templates/pages/admin-page/admin-page-root.component.spec.ts @@ -16,14 +16,14 @@ * @fileoverview Unit tests for Admin Page Root component. */ -import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { TranslateModule } from '@ngx-translate/core'; +import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {TranslateModule} from '@ngx-translate/core'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; -import { AdminPageRootComponent } from './admin-page-root.component'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; +import {AdminPageRootComponent} from './admin-page-root.component'; describe('AdminPageRootComponent', () => { let fixture: ComponentFixture; @@ -43,8 +43,10 @@ describe('AdminPageRootComponent', () => { it('should have the title and meta tags set', () => { expect(component.title).toEqual( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ADMIN.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ADMIN.TITLE + ); expect(component.meta).toEqual( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ADMIN.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ADMIN.META + ); }); }); diff --git a/core/templates/pages/admin-page/admin-page-root.component.ts b/core/templates/pages/admin-page/admin-page-root.component.ts index a39896afce31..f45483c9f118 100644 --- a/core/templates/pages/admin-page/admin-page-root.component.ts +++ b/core/templates/pages/admin-page/admin-page-root.component.ts @@ -16,9 +16,9 @@ * @fileoverview Admin page root component. */ -import { Component } from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { BaseRootComponent, MetaTagData } from 'pages/base-root.component'; +import {Component} from '@angular/core'; +import {AppConstants} from 'app.constants'; +import {BaseRootComponent, MetaTagData} from 'pages/base-root.component'; @Component({ selector: 'oppia-admin-page-root', @@ -26,7 +26,6 @@ import { BaseRootComponent, MetaTagData } from 'pages/base-root.component'; }) export class AdminPageRootComponent extends BaseRootComponent { title: string = AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ADMIN.TITLE; - meta: MetaTagData[] = - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ADMIN.META as - unknown as Readonly[]; + meta: MetaTagData[] = AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ADMIN + .META as unknown as Readonly[]; } diff --git a/core/templates/pages/admin-page/admin-page.component.spec.ts b/core/templates/pages/admin-page/admin-page.component.spec.ts index dfd149de50f7..8f73b1b37bb8 100644 --- a/core/templates/pages/admin-page/admin-page.component.spec.ts +++ b/core/templates/pages/admin-page/admin-page.component.spec.ts @@ -16,13 +16,13 @@ * @fileoverview UnitTests for Admin Page component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ChangeDetectorRef, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ChangeDetectorRef, NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { AdminPageComponent } from './admin-page.component'; -import { AdminRouterService } from './services/admin-router.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {AdminPageComponent} from './admin-page.component'; +import {AdminRouterService} from './services/admin-router.service'; class MockWindowRef { nativeWindow = { @@ -34,14 +34,14 @@ class MockWindowRef { href: 'href', pathname: 'pathname', search: 'search', - hash: 'hash' + hash: 'hash', }, open() { return; }, onhashchange() { return; - } + }, }; } @@ -62,10 +62,10 @@ describe('Admin Page component ', () => { ChangeDetectorRef, { provide: WindowRef, - useValue: mockWindowRef - } + useValue: mockWindowRef, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(AdminPageComponent); @@ -123,7 +123,7 @@ describe('Admin Page component ', () => { expect(result).toBe(true); }); - it('should set status message when calling \'setStatusMessage\'', () => { + it("should set status message when calling 'setStatusMessage'", () => { expect(component.statusMessage).toBe(''); component.ngOnInit(); diff --git a/core/templates/pages/admin-page/admin-page.component.ts b/core/templates/pages/admin-page/admin-page.component.ts index f6f85903c2c5..3be29cbcd7a3 100644 --- a/core/templates/pages/admin-page/admin-page.component.ts +++ b/core/templates/pages/admin-page/admin-page.component.ts @@ -16,14 +16,14 @@ * @fileoverview Data and component for the Oppia admin page. */ -import { ChangeDetectorRef, Component, EventEmitter } from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { AdminRouterService } from './services/admin-router.service'; +import {ChangeDetectorRef, Component, EventEmitter} from '@angular/core'; +import {AppConstants} from 'app.constants'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {AdminRouterService} from './services/admin-router.service'; @Component({ selector: 'oppia-admin-page', - templateUrl: './admin-page.component.html' + templateUrl: './admin-page.component.html', }) export class AdminPageComponent { statusMessage = ''; @@ -37,11 +37,11 @@ export class AdminPageComponent { ) {} ngOnInit(): void { - this.adminRouterService.showTab( - this.windowRef.nativeWindow.location.hash); + this.adminRouterService.showTab(this.windowRef.nativeWindow.location.hash); this.windowRef.nativeWindow.onhashchange = () => { this.adminRouterService.showTab( - this.windowRef.nativeWindow.location.hash); + this.windowRef.nativeWindow.location.hash + ); }; } diff --git a/core/templates/pages/admin-page/admin-page.constants.ajs.ts b/core/templates/pages/admin-page/admin-page.constants.ajs.ts index f9c6b6cea1c3..c3c6a6d3c3e6 100644 --- a/core/templates/pages/admin-page/admin-page.constants.ajs.ts +++ b/core/templates/pages/admin-page/admin-page.constants.ajs.ts @@ -18,26 +18,43 @@ // TODO(#7092): Delete this file once migration is complete and these AngularJS // equivalents of the Angular constants are no longer needed. -import { AdminPageConstants } from 'pages/admin-page/admin-page.constants'; - -angular.module('oppia').constant( - 'ADMIN_ROLE_HANDLER_URL', AdminPageConstants.ADMIN_ROLE_HANDLER_URL); - -angular.module('oppia').constant( - 'ADMIN_HANDLER_URL', AdminPageConstants.ADMIN_HANDLER_URL); -angular.module('oppia').constant( - 'ADMIN_TOPICS_CSV_DOWNLOAD_HANDLER_URL', - AdminPageConstants.ADMIN_TOPICS_CSV_DOWNLOAD_HANDLER_URL); - -angular.module('oppia').constant( - 'ADMIN_BANNED_USERS_HANDLER', AdminPageConstants.ADMIN_BANNED_USERS_HANDLER); - -angular.module('oppia').constant( - 'ADMIN_JOB_OUTPUT_URL_TEMPLATE', - AdminPageConstants.ADMIN_JOB_OUTPUT_URL_TEMPLATE); - -angular.module('oppia').constant( - 'ADMIN_TAB_URLS', AdminPageConstants.ADMIN_TAB_URLS); - -angular.module('oppia').constant( - 'PROFILE_URL_TEMPLATE', AdminPageConstants.PROFILE_URL_TEMPLATE); +import {AdminPageConstants} from 'pages/admin-page/admin-page.constants'; + +angular + .module('oppia') + .constant( + 'ADMIN_ROLE_HANDLER_URL', + AdminPageConstants.ADMIN_ROLE_HANDLER_URL + ); + +angular + .module('oppia') + .constant('ADMIN_HANDLER_URL', AdminPageConstants.ADMIN_HANDLER_URL); +angular + .module('oppia') + .constant( + 'ADMIN_TOPICS_CSV_DOWNLOAD_HANDLER_URL', + AdminPageConstants.ADMIN_TOPICS_CSV_DOWNLOAD_HANDLER_URL + ); + +angular + .module('oppia') + .constant( + 'ADMIN_BANNED_USERS_HANDLER', + AdminPageConstants.ADMIN_BANNED_USERS_HANDLER + ); + +angular + .module('oppia') + .constant( + 'ADMIN_JOB_OUTPUT_URL_TEMPLATE', + AdminPageConstants.ADMIN_JOB_OUTPUT_URL_TEMPLATE + ); + +angular + .module('oppia') + .constant('ADMIN_TAB_URLS', AdminPageConstants.ADMIN_TAB_URLS); + +angular + .module('oppia') + .constant('PROFILE_URL_TEMPLATE', AdminPageConstants.PROFILE_URL_TEMPLATE); diff --git a/core/templates/pages/admin-page/admin-page.constants.ts b/core/templates/pages/admin-page/admin-page.constants.ts index e539d7ae8462..4f9da76361d3 100644 --- a/core/templates/pages/admin-page/admin-page.constants.ts +++ b/core/templates/pages/admin-page/admin-page.constants.ts @@ -43,10 +43,10 @@ export const AdminPageConstants = { CONFIG: '#/config', PLATFORM_PARAMETERS: '#/platform-parameters', ROLES: '#/roles', - MISC: '#/misc' + MISC: '#/misc', }, EXPLORATION_INTERACTIONS_HANDLER: '/interactions', - PROFILE_URL_TEMPLATE: '/profile/' + PROFILE_URL_TEMPLATE: '/profile/', } as const; diff --git a/core/templates/pages/admin-page/admin-page.import.ts b/core/templates/pages/admin-page/admin-page.import.ts index 422614c1e180..fd783f876926 100644 --- a/core/templates/pages/admin-page/admin-page.import.ts +++ b/core/templates/pages/admin-page/admin-page.import.ts @@ -22,9 +22,14 @@ import 'zone.js'; import uiValidate from 'angular-ui-validate'; angular.module('oppia', [ - require('angular-cookies'), 'ngAnimate', - 'ngMaterial', 'ngSanitize', 'ngTouch', 'pascalprecht.translate', - 'ui.bootstrap', uiValidate + require('angular-cookies'), + 'ngAnimate', + 'ngMaterial', + 'ngSanitize', + 'ngTouch', + 'pascalprecht.translate', + 'ui.bootstrap', + uiValidate, ]); require('Polyfills.ts'); diff --git a/core/templates/pages/admin-page/admin-page.module.ts b/core/templates/pages/admin-page/admin-page.module.ts index 0161cf78e470..8a2d98145bad 100644 --- a/core/templates/pages/admin-page/admin-page.module.ts +++ b/core/templates/pages/admin-page/admin-page.module.ts @@ -16,28 +16,28 @@ * @fileoverview Module for the admin page. */ -import { NgModule } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { RouterModule } from '@angular/router'; -import { CommonModule } from '@angular/common'; -import { ToastrModule } from 'ngx-toastr'; +import {NgModule} from '@angular/core'; +import {FormsModule} from '@angular/forms'; +import {RouterModule} from '@angular/router'; +import {CommonModule} from '@angular/common'; +import {ToastrModule} from 'ngx-toastr'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { TopicManagerRoleEditorModalComponent } from './roles-tab/topic-manager-role-editor-modal.component'; -import { TranslationCoordinatorRoleEditorModalComponent } from './roles-tab/translation-coordinator-role-editor-modal.component'; -import { SharedFormsModule } from 'components/forms/shared-forms.module'; -import { toastrConfig } from 'pages/oppia-root/app.module'; -import { AdminPlatformParametersTabComponent } from './platform-parameters-tab/admin-platform-parameters-tab.component'; -import { AdminPageRootComponent } from './admin-page-root.component'; -import { AdminBlogAdminCommonModule } from './admin-blog-admin-common.module'; -import { AdminAuthGuard } from './admin-auth.guard'; -import { AdminNavbarComponent } from './navbar/admin-navbar.component'; -import { AdminDevModeActivitiesTabComponent } from './activities-tab/admin-dev-mode-activities-tab.component'; -import { OppiaAdminProdModeActivitiesTabComponent } from './activities-tab/admin-prod-mode-activities-tab.component'; -import { AdminMiscTabComponent } from './misc-tab/admin-misc-tab.component'; -import { AdminRolesTabComponent } from './roles-tab/admin-roles-tab.component'; -import { AdminConfigTabComponent } from './config-tab/admin-config-tab.component'; -import { AdminPageComponent } from './admin-page.component'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {TopicManagerRoleEditorModalComponent} from './roles-tab/topic-manager-role-editor-modal.component'; +import {TranslationCoordinatorRoleEditorModalComponent} from './roles-tab/translation-coordinator-role-editor-modal.component'; +import {SharedFormsModule} from 'components/forms/shared-forms.module'; +import {toastrConfig} from 'pages/oppia-root/app.module'; +import {AdminPlatformParametersTabComponent} from './platform-parameters-tab/admin-platform-parameters-tab.component'; +import {AdminPageRootComponent} from './admin-page-root.component'; +import {AdminBlogAdminCommonModule} from './admin-blog-admin-common.module'; +import {AdminAuthGuard} from './admin-auth.guard'; +import {AdminNavbarComponent} from './navbar/admin-navbar.component'; +import {AdminDevModeActivitiesTabComponent} from './activities-tab/admin-dev-mode-activities-tab.component'; +import {OppiaAdminProdModeActivitiesTabComponent} from './activities-tab/admin-prod-mode-activities-tab.component'; +import {AdminMiscTabComponent} from './misc-tab/admin-misc-tab.component'; +import {AdminRolesTabComponent} from './roles-tab/admin-roles-tab.component'; +import {AdminConfigTabComponent} from './config-tab/admin-config-tab.component'; +import {AdminPageComponent} from './admin-page.component'; @NgModule({ imports: [ diff --git a/core/templates/pages/admin-page/config-tab/admin-config-tab.component.spec.ts b/core/templates/pages/admin-page/config-tab/admin-config-tab.component.spec.ts index 89962a062b68..05f40b27dc7f 100644 --- a/core/templates/pages/admin-page/config-tab/admin-config-tab.component.spec.ts +++ b/core/templates/pages/admin-page/config-tab/admin-config-tab.component.spec.ts @@ -16,15 +16,23 @@ * @fileoverview Tests for Admin config tab component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { FormsModule } from '@angular/forms'; - -import { AdminBackendApiService, AdminPageData } from 'domain/admin/admin-backend-api.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { AdminTaskManagerService } from '../services/admin-task-manager.service'; -import { AdminConfigTabComponent } from './admin-config-tab.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {FormsModule} from '@angular/forms'; + +import { + AdminBackendApiService, + AdminPageData, +} from 'domain/admin/admin-backend-api.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {AdminTaskManagerService} from '../services/admin-task-manager.service'; +import {AdminConfigTabComponent} from './admin-config-tab.component'; class MockWindowRef { nativeWindow = { @@ -36,11 +44,11 @@ class MockWindowRef { href: 'href', pathname: 'pathname', search: 'search', - hash: 'hash' + hash: 'hash', }, open() { return; - } + }, }; } @@ -52,24 +60,16 @@ describe('Admin config tab component ', () => { let adminTaskManagerService: AdminTaskManagerService; let mockWindowRef: MockWindowRef; - let statusMessageSpy: jasmine.Spy; let confirmSpy: jasmine.Spy; const adminPageData: AdminPageData = { demoExplorationIds: ['expId'], - demoExplorations: [ - [ - '0', - 'welcome.yaml' - ] - ], - demoCollections: [ - ['collectionId'] - ], + demoExplorations: [['0', 'welcome.yaml']], + demoCollections: [['collectionId']], updatableRoles: ['MODERATOR'], roleToActions: { - Admin: ['Accept any suggestion', 'Access creator dashboard'] + Admin: ['Accept any suggestion', 'Access creator dashboard'], }, configProperties: { configProperty1: { @@ -78,7 +78,7 @@ describe('Admin config tab component ', () => { type: 'custom', obj_type: 'CustomType', }, - value: 'val1' + value: 'val1', }, configProperty2: { description: 'description2', @@ -86,7 +86,7 @@ describe('Admin config tab component ', () => { type: 'custom', obj_type: 'CustomType', }, - value: 'val2' + value: 'val2', }, configProperty3: { description: 'description3', @@ -94,34 +94,31 @@ describe('Admin config tab component ', () => { type: 'custom', obj_type: 'CustomType', }, - value: 'val3' - } + value: 'val3', + }, }, viewableRoles: ['MODERATOR'], humanReadableRoles: { - MODERATOR: 'moderator' + MODERATOR: 'moderator', }, topicSummaries: [], - platformParameters: [] + platformParameters: [], }; beforeEach(() => { mockWindowRef = new MockWindowRef(); TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - FormsModule - ], + imports: [HttpClientTestingModule, FormsModule], declarations: [AdminConfigTabComponent], providers: [ AdminBackendApiService, AdminTaskManagerService, { provide: WindowRef, - useValue: mockWindowRef - } + useValue: mockWindowRef, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(AdminConfigTabComponent); @@ -132,8 +129,10 @@ describe('Admin config tab component ', () => { adminBackendApiService = TestBed.inject(AdminBackendApiService); adminTaskManagerService = TestBed.inject(AdminTaskManagerService); - statusMessageSpy = spyOn(component.setStatusMessage, 'emit') - .and.callThrough(); + statusMessageSpy = spyOn( + component.setStatusMessage, + 'emit' + ).and.callThrough(); spyOn(adminTaskManagerService, 'startTask').and.callThrough(); spyOn(adminTaskManagerService, 'finishTask').and.callThrough(); spyOn(adminBackendApiService, 'getDataAsync').and.resolveTo(adminPageData); @@ -149,117 +148,154 @@ describe('Admin config tab component ', () => { expect(component.configProperties).toEqual(adminPageData.configProperties); })); - it('should check whether an object is non empty when calling ' + - '\'isNonemptyObject\'', () => { - let result = component.isNonemptyObject({}); - expect(result).toBe(false); + it( + 'should check whether an object is non empty when calling ' + + "'isNonemptyObject'", + () => { + let result = component.isNonemptyObject({}); + expect(result).toBe(false); - result = component.isNonemptyObject({description: 'description'}); - expect(result).toBe(true); - }); + result = component.isNonemptyObject({description: 'description'}); + expect(result).toBe(true); + } + ); - it('should return schema callback when calling ' + - '\'getSchemaCallback\'', () => { - let result = component.getSchemaCallback({type: 'bool'}); - expect(result()).toEqual({type: 'bool'}); - }); + it( + 'should return schema callback when calling ' + "'getSchemaCallback'", + () => { + let result = component.getSchemaCallback({type: 'bool'}); + expect(result()).toEqual({type: 'bool'}); + } + ); describe('when clicking on revert to default button ', () => { - it('should revert to default config property ' + - 'successfully', fakeAsync(() => { - // Setting confirm button clicked to be true. - confirmSpy.and.returnValue(true); - spyOn(adminBackendApiService, 'revertConfigPropertyAsync') - .and.returnValue(Promise.resolve()); - - component.revertToDefaultConfigPropertyValue('configId1'); - tick(); - - expect(statusMessageSpy).toHaveBeenCalledWith( - 'Config property reverted successfully.'); - })); - - it('should not revert to default config property ' + - 'in case of backend error', fakeAsync(() => { - // Setting confirm button clicked to be true. - confirmSpy.and.returnValue(true); - spyOn(adminBackendApiService, 'revertConfigPropertyAsync') - .and.returnValue(Promise.reject('Internal Server Error.')); - - component.revertToDefaultConfigPropertyValue('configId1'); - tick(); - - expect(statusMessageSpy).toHaveBeenCalledWith( - 'Server error: Internal Server Error.'); - })); - - it('should not request backend to revert to default config property ' + - 'if cancel button is clicked in the alert', fakeAsync(() => { - // Setting confirm button clicked to be false. - confirmSpy.and.returnValue(false); - let revertConfigSpy = spyOn( - adminBackendApiService, 'revertConfigPropertyAsync'); - - component.revertToDefaultConfigPropertyValue('configId1'); - tick(); - - expect(revertConfigSpy).not.toHaveBeenCalled(); - })); + it( + 'should revert to default config property ' + 'successfully', + fakeAsync(() => { + // Setting confirm button clicked to be true. + confirmSpy.and.returnValue(true); + spyOn( + adminBackendApiService, + 'revertConfigPropertyAsync' + ).and.returnValue(Promise.resolve()); + + component.revertToDefaultConfigPropertyValue('configId1'); + tick(); + + expect(statusMessageSpy).toHaveBeenCalledWith( + 'Config property reverted successfully.' + ); + }) + ); + + it( + 'should not revert to default config property ' + + 'in case of backend error', + fakeAsync(() => { + // Setting confirm button clicked to be true. + confirmSpy.and.returnValue(true); + spyOn( + adminBackendApiService, + 'revertConfigPropertyAsync' + ).and.returnValue(Promise.reject('Internal Server Error.')); + + component.revertToDefaultConfigPropertyValue('configId1'); + tick(); + + expect(statusMessageSpy).toHaveBeenCalledWith( + 'Server error: Internal Server Error.' + ); + }) + ); + + it( + 'should not request backend to revert to default config property ' + + 'if cancel button is clicked in the alert', + fakeAsync(() => { + // Setting confirm button clicked to be false. + confirmSpy.and.returnValue(false); + let revertConfigSpy = spyOn( + adminBackendApiService, + 'revertConfigPropertyAsync' + ); + + component.revertToDefaultConfigPropertyValue('configId1'); + tick(); + + expect(revertConfigSpy).not.toHaveBeenCalled(); + }) + ); }); describe('when clicking on save button ', () => { it('should save config properties successfully', fakeAsync(() => { // Setting confirm button clicked to be true. confirmSpy.and.returnValue(true); - spyOn(adminBackendApiService, 'saveConfigPropertiesAsync') - .and.returnValue(Promise.resolve()); + spyOn( + adminBackendApiService, + 'saveConfigPropertiesAsync' + ).and.returnValue(Promise.resolve()); component.configProperties = adminPageData.configProperties; component.saveConfigProperties(); tick(); - expect(statusMessageSpy).toHaveBeenCalledWith( - 'Data saved successfully.'); - })); - - it('should not save config properties ' + - 'in case of backend error', fakeAsync(() => { - // Setting confirm button clicked to be true. - confirmSpy.and.returnValue(true); - spyOn(adminBackendApiService, 'saveConfigPropertiesAsync') - .and.returnValue(Promise.reject('Internal Server Error.')); - - component.saveConfigProperties(); - tick(); - - expect(statusMessageSpy).toHaveBeenCalledWith( - 'Server error: Internal Server Error.'); - })); - - it('should not save config properties ' + - 'if a task is still running in the queue', fakeAsync(() => { - // Setting task is still running to be true. - spyOn(adminTaskManagerService, 'isTaskRunning').and.returnValue(true); - let saveConfigSpy = spyOn( - adminBackendApiService, 'saveConfigPropertiesAsync'); - - component.saveConfigProperties(); - tick(); - - expect(saveConfigSpy).not.toHaveBeenCalled(); + expect(statusMessageSpy).toHaveBeenCalledWith('Data saved successfully.'); })); - it('should not request backend to save config properties ' + - 'if cancel button is clicked in the alert', fakeAsync(() => { - // Setting confirm button clicked to be false. - confirmSpy.and.returnValue(false); - let saveConfigSpy = spyOn( - adminBackendApiService, 'saveConfigPropertiesAsync'); - - component.saveConfigProperties(); - tick(); - - expect(saveConfigSpy).not.toHaveBeenCalled(); - })); + it( + 'should not save config properties ' + 'in case of backend error', + fakeAsync(() => { + // Setting confirm button clicked to be true. + confirmSpy.and.returnValue(true); + spyOn( + adminBackendApiService, + 'saveConfigPropertiesAsync' + ).and.returnValue(Promise.reject('Internal Server Error.')); + + component.saveConfigProperties(); + tick(); + + expect(statusMessageSpy).toHaveBeenCalledWith( + 'Server error: Internal Server Error.' + ); + }) + ); + + it( + 'should not save config properties ' + + 'if a task is still running in the queue', + fakeAsync(() => { + // Setting task is still running to be true. + spyOn(adminTaskManagerService, 'isTaskRunning').and.returnValue(true); + let saveConfigSpy = spyOn( + adminBackendApiService, + 'saveConfigPropertiesAsync' + ); + + component.saveConfigProperties(); + tick(); + + expect(saveConfigSpy).not.toHaveBeenCalled(); + }) + ); + + it( + 'should not request backend to save config properties ' + + 'if cancel button is clicked in the alert', + fakeAsync(() => { + // Setting confirm button clicked to be false. + confirmSpy.and.returnValue(false); + let saveConfigSpy = spyOn( + adminBackendApiService, + 'saveConfigPropertiesAsync' + ); + + component.saveConfigProperties(); + tick(); + + expect(saveConfigSpy).not.toHaveBeenCalled(); + }) + ); }); }); diff --git a/core/templates/pages/admin-page/config-tab/admin-config-tab.component.ts b/core/templates/pages/admin-page/config-tab/admin-config-tab.component.ts index 8a349c92e694..b697fd810616 100644 --- a/core/templates/pages/admin-page/config-tab/admin-config-tab.component.ts +++ b/core/templates/pages/admin-page/config-tab/admin-config-tab.component.ts @@ -16,16 +16,20 @@ * @fileoverview Component for the configuration tab in the admin panel. */ -import { Component, EventEmitter, Output } from '@angular/core'; -import { AdminBackendApiService, ConfigPropertiesBackendResponse, NewConfigPropertyValues } from 'domain/admin/admin-backend-api.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { Schema } from 'services/schema-default-value.service'; -import { AdminDataService } from '../services/admin-data.service'; -import { AdminTaskManagerService } from '../services/admin-task-manager.service'; +import {Component, EventEmitter, Output} from '@angular/core'; +import { + AdminBackendApiService, + ConfigPropertiesBackendResponse, + NewConfigPropertyValues, +} from 'domain/admin/admin-backend-api.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {Schema} from 'services/schema-default-value.service'; +import {AdminDataService} from '../services/admin-data.service'; +import {AdminTaskManagerService} from '../services/admin-task-manager.service'; @Component({ selector: 'oppia-admin-config-tab', - templateUrl: './admin-config-tab.component.html' + templateUrl: './admin-config-tab.component.html', }) export class AdminConfigTabComponent { @Output() setStatusMessage: EventEmitter = new EventEmitter(); @@ -53,32 +57,42 @@ export class AdminConfigTabComponent { } reloadConfigProperties(): void { - this.adminDataService.getDataAsync().then((adminDataObject) => { + this.adminDataService.getDataAsync().then(adminDataObject => { this.configProperties = adminDataObject.configProperties; }); } revertToDefaultConfigPropertyValue(configPropertyId: string): void { - if (!this.windowRef.nativeWindow.confirm( - 'This action is irreversible. Are you sure?')) { + if ( + !this.windowRef.nativeWindow.confirm( + 'This action is irreversible. Are you sure?' + ) + ) { return; } - this.adminBackendApiService.revertConfigPropertyAsync(configPropertyId) - .then(() => { - this.setStatusMessage.emit('Config property reverted successfully.'); - this.reloadConfigProperties(); - }, errorResponse => { - this.setStatusMessage.emit('Server error: ' + errorResponse); - }); + this.adminBackendApiService + .revertConfigPropertyAsync(configPropertyId) + .then( + () => { + this.setStatusMessage.emit('Config property reverted successfully.'); + this.reloadConfigProperties(); + }, + errorResponse => { + this.setStatusMessage.emit('Server error: ' + errorResponse); + } + ); } saveConfigProperties(): void { if (this.adminTaskManagerService.isTaskRunning()) { return; } - if (!this.windowRef.nativeWindow.confirm( - 'This action is irreversible. Are you sure?')) { + if ( + !this.windowRef.nativeWindow.confirm( + 'This action is irreversible. Are you sure?' + ) + ) { return; } @@ -90,13 +104,17 @@ export class AdminConfigTabComponent { newConfigPropertyValues[property] = this.configProperties[property].value; } - this.adminBackendApiService.saveConfigPropertiesAsync( - newConfigPropertyValues).then(() => { - this.setStatusMessage.emit('Data saved successfully.'); - this.adminTaskManagerService.finishTask(); - }, errorResponse => { - this.setStatusMessage.emit('Server error: ' + errorResponse); - this.adminTaskManagerService.finishTask(); - }); + this.adminBackendApiService + .saveConfigPropertiesAsync(newConfigPropertyValues) + .then( + () => { + this.setStatusMessage.emit('Data saved successfully.'); + this.adminTaskManagerService.finishTask(); + }, + errorResponse => { + this.setStatusMessage.emit('Server error: ' + errorResponse); + this.adminTaskManagerService.finishTask(); + } + ); } } diff --git a/core/templates/pages/admin-page/misc-tab/admin-misc-tab.component.spec.ts b/core/templates/pages/admin-page/misc-tab/admin-misc-tab.component.spec.ts index 45160158cfad..e3feed083bca 100644 --- a/core/templates/pages/admin-page/misc-tab/admin-misc-tab.component.spec.ts +++ b/core/templates/pages/admin-page/misc-tab/admin-misc-tab.component.spec.ts @@ -16,15 +16,20 @@ * @fileoverview UnitTests for Admin misc tab component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { FormsModule } from '@angular/forms'; - -import { AdminBackendApiService } from 'domain/admin/admin-backend-api.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { AdminTaskManagerService } from '../services/admin-task-manager.service'; -import { AdminMiscTabComponent } from './admin-misc-tab.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {FormsModule} from '@angular/forms'; + +import {AdminBackendApiService} from 'domain/admin/admin-backend-api.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {AdminTaskManagerService} from '../services/admin-task-manager.service'; +import {AdminMiscTabComponent} from './admin-misc-tab.component'; class MockWindowRef { nativeWindow = { @@ -36,17 +41,17 @@ class MockWindowRef { href: 'href', pathname: 'pathname', search: 'search', - hash: 'hash' + hash: 'hash', }, open() { return; - } + }, }; } class MockReaderObject { result = null; - onload: {(arg0: { target: { result: string}}): void; (): string}; + onload: {(arg0: {target: {result: string}}): void; (): string}; constructor() { this.onload = () => { return 'Fake onload executed'; @@ -73,20 +78,17 @@ describe('Admin misc tab component ', () => { beforeEach(() => { mockWindowRef = new MockWindowRef(); TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - FormsModule - ], + imports: [HttpClientTestingModule, FormsModule], declarations: [AdminMiscTabComponent], providers: [ AdminBackendApiService, AdminTaskManagerService, { provide: WindowRef, - useValue: mockWindowRef - } + useValue: mockWindowRef, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(AdminMiscTabComponent); @@ -97,8 +99,10 @@ describe('Admin misc tab component ', () => { adminBackendApiService = TestBed.inject(AdminBackendApiService); adminTaskManagerService = TestBed.inject(AdminTaskManagerService); - statusMessageSpy = spyOn(component.setStatusMessage, 'emit') - .and.returnValue(); + statusMessageSpy = spyOn( + component.setStatusMessage, + 'emit' + ).and.returnValue(); spyOn(adminTaskManagerService, 'startTask').and.returnValue(); spyOn(adminTaskManagerService, 'finishTask').and.returnValue(); confirmSpy = spyOn(mockWindowRef.nativeWindow, 'confirm'); @@ -114,67 +118,84 @@ describe('Admin misc tab component ', () => { it('should clear search index successfully', fakeAsync(() => { confirmSpy.and.returnValue(true); let clearSearchIndexSpy = spyOn( - adminBackendApiService, 'clearSearchIndexAsync') - .and.resolveTo(); + adminBackendApiService, + 'clearSearchIndexAsync' + ).and.resolveTo(); component.clearSearchIndex(); tick(); expect(clearSearchIndexSpy).toHaveBeenCalled(); expect(statusMessageSpy).toHaveBeenCalledWith( - 'Index successfully cleared.'); - })); - - it('should not clear search index in case of backend ' + - 'error', fakeAsync(() => { - confirmSpy.and.returnValue(true); - let clearSearchIndexSpy = spyOn( - adminBackendApiService, 'clearSearchIndexAsync') - .and.rejectWith('Internal Server Error.'); - - component.clearSearchIndex(); - tick(); - - expect(clearSearchIndexSpy).toHaveBeenCalled(); - expect(statusMessageSpy).toHaveBeenCalledWith( - 'Server error: Internal Server Error.'); + 'Index successfully cleared.' + ); })); - it('should not request backend to clear search index if ' + - 'a task is still running in the queue', fakeAsync(() => { - confirmSpy.and.returnValue(true); - let clearSearchIndexSpy = spyOn( - adminBackendApiService, 'clearSearchIndexAsync'); - - // Setting task is still running to be true. - spyOn(adminTaskManagerService, 'isTaskRunning').and.returnValue(true); - - component.clearSearchIndex(); - tick(); + it( + 'should not clear search index in case of backend ' + 'error', + fakeAsync(() => { + confirmSpy.and.returnValue(true); + let clearSearchIndexSpy = spyOn( + adminBackendApiService, + 'clearSearchIndexAsync' + ).and.rejectWith('Internal Server Error.'); - expect(clearSearchIndexSpy).not.toHaveBeenCalled(); - })); + component.clearSearchIndex(); + tick(); - it('should not request backend to clear search index if ' + - 'cancel button is clicked in the alert', fakeAsync(() => { - confirmSpy.and.returnValue(false); - let clearSearchIndexSpy = spyOn( - adminBackendApiService, 'clearSearchIndexAsync'); - // Setting cancel button clicked to be true. + expect(clearSearchIndexSpy).toHaveBeenCalled(); + expect(statusMessageSpy).toHaveBeenCalledWith( + 'Server error: Internal Server Error.' + ); + }) + ); + + it( + 'should not request backend to clear search index if ' + + 'a task is still running in the queue', + fakeAsync(() => { + confirmSpy.and.returnValue(true); + let clearSearchIndexSpy = spyOn( + adminBackendApiService, + 'clearSearchIndexAsync' + ); + + // Setting task is still running to be true. + spyOn(adminTaskManagerService, 'isTaskRunning').and.returnValue(true); + + component.clearSearchIndex(); + tick(); - component.clearSearchIndex(); - tick(); + expect(clearSearchIndexSpy).not.toHaveBeenCalled(); + }) + ); + + it( + 'should not request backend to clear search index if ' + + 'cancel button is clicked in the alert', + fakeAsync(() => { + confirmSpy.and.returnValue(false); + let clearSearchIndexSpy = spyOn( + adminBackendApiService, + 'clearSearchIndexAsync' + ); + // Setting cancel button clicked to be true. + + component.clearSearchIndex(); + tick(); - expect(clearSearchIndexSpy).not.toHaveBeenCalled(); - })); + expect(clearSearchIndexSpy).not.toHaveBeenCalled(); + }) + ); }); describe('when clicking on regenerate topic button ', () => { it('should regenerate topic successfully', fakeAsync(() => { confirmSpy.and.returnValue(true); let regenerateTopicSpy = spyOn( - adminBackendApiService, 'regenerateOpportunitiesRelatedToTopicAsync') - .and.returnValue(Promise.resolve(10)); + adminBackendApiService, + 'regenerateOpportunitiesRelatedToTopicAsync' + ).and.returnValue(Promise.resolve(10)); component.regenerateOpportunitiesRelatedToTopic(); tick(); @@ -185,114 +206,145 @@ describe('Admin misc tab component ', () => { ); })); - it('should not regenerate topic in case of backend ' + - 'error', fakeAsync(() => { - confirmSpy.and.returnValue(true); - let regenerateTopicSpy = spyOn( - adminBackendApiService, 'regenerateOpportunitiesRelatedToTopicAsync') - .and.rejectWith('Internal Server Error.'); - - component.regenerateOpportunitiesRelatedToTopic(); - tick(); - - expect(regenerateTopicSpy).toHaveBeenCalled(); - expect(statusMessageSpy).toHaveBeenCalledWith( - 'Server error: Internal Server Error.'); - })); - - it('should not request backend to regenerate topic if ' + - 'a task is still running in the queue', fakeAsync(() => { - confirmSpy.and.returnValue(true); - let regenerateTopicSpy = spyOn( - adminBackendApiService, 'regenerateOpportunitiesRelatedToTopicAsync'); - - // Setting task is still running to be true. - spyOn(adminTaskManagerService, 'isTaskRunning').and.returnValue(true); + it( + 'should not regenerate topic in case of backend ' + 'error', + fakeAsync(() => { + confirmSpy.and.returnValue(true); + let regenerateTopicSpy = spyOn( + adminBackendApiService, + 'regenerateOpportunitiesRelatedToTopicAsync' + ).and.rejectWith('Internal Server Error.'); - component.regenerateOpportunitiesRelatedToTopic(); - tick(); - - expect(regenerateTopicSpy).not.toHaveBeenCalled(); - })); + component.regenerateOpportunitiesRelatedToTopic(); + tick(); - it('should not request backend to regenerate topic if ' + - 'cancel button is clicked in the alert', fakeAsync(() => { - confirmSpy.and.returnValue(false); - let regenerateTopicSpy = spyOn( - adminBackendApiService, 'regenerateOpportunitiesRelatedToTopicAsync'); - // Setting cancel button clicked to be true. + expect(regenerateTopicSpy).toHaveBeenCalled(); + expect(statusMessageSpy).toHaveBeenCalledWith( + 'Server error: Internal Server Error.' + ); + }) + ); + + it( + 'should not request backend to regenerate topic if ' + + 'a task is still running in the queue', + fakeAsync(() => { + confirmSpy.and.returnValue(true); + let regenerateTopicSpy = spyOn( + adminBackendApiService, + 'regenerateOpportunitiesRelatedToTopicAsync' + ); + + // Setting task is still running to be true. + spyOn(adminTaskManagerService, 'isTaskRunning').and.returnValue(true); + + component.regenerateOpportunitiesRelatedToTopic(); + tick(); - component.regenerateOpportunitiesRelatedToTopic(); - tick(); + expect(regenerateTopicSpy).not.toHaveBeenCalled(); + }) + ); + + it( + 'should not request backend to regenerate topic if ' + + 'cancel button is clicked in the alert', + fakeAsync(() => { + confirmSpy.and.returnValue(false); + let regenerateTopicSpy = spyOn( + adminBackendApiService, + 'regenerateOpportunitiesRelatedToTopicAsync' + ); + // Setting cancel button clicked to be true. + + component.regenerateOpportunitiesRelatedToTopic(); + tick(); - expect(regenerateTopicSpy).not.toHaveBeenCalled(); - })); + expect(regenerateTopicSpy).not.toHaveBeenCalled(); + }) + ); }); describe('when clicking on rollback exploration button ', () => { it('should rollback exploration successfully', fakeAsync(() => { confirmSpy.and.returnValue(true); let regenerateTopicSpy = spyOn( - adminBackendApiService, 'rollbackExplorationToSafeState') - .and.returnValue(Promise.resolve(10)); + adminBackendApiService, + 'rollbackExplorationToSafeState' + ).and.returnValue(Promise.resolve(10)); component.rollbackExploration(); tick(); expect(regenerateTopicSpy).toHaveBeenCalled(); expect(statusMessageSpy).toHaveBeenCalledWith( - 'Exploration rolledback to version: 10'); - })); - - it('should not rollback exploration in case of backend ' + - 'error', fakeAsync(() => { - confirmSpy.and.returnValue(true); - let regenerateTopicSpy = spyOn( - adminBackendApiService, 'rollbackExplorationToSafeState') - .and.rejectWith('Internal Server Error.'); - - component.rollbackExploration(); - tick(); - - expect(regenerateTopicSpy).toHaveBeenCalled(); - expect(statusMessageSpy).toHaveBeenCalledWith( - 'Server error: Internal Server Error.'); + 'Exploration rolledback to version: 10' + ); })); - it('should not request backend to rollback exploration if ' + - 'a task is still running in the queue', fakeAsync(() => { - confirmSpy.and.returnValue(true); - let regenerateTopicSpy = spyOn( - adminBackendApiService, 'rollbackExplorationToSafeState'); - - // Setting task is still running to be true. - spyOn(adminTaskManagerService, 'isTaskRunning').and.returnValue(true); - - component.rollbackExploration(); - tick(); + it( + 'should not rollback exploration in case of backend ' + 'error', + fakeAsync(() => { + confirmSpy.and.returnValue(true); + let regenerateTopicSpy = spyOn( + adminBackendApiService, + 'rollbackExplorationToSafeState' + ).and.rejectWith('Internal Server Error.'); - expect(regenerateTopicSpy).not.toHaveBeenCalled(); - })); + component.rollbackExploration(); + tick(); - it('should not request backend to rollback exploration if ' + - 'cancel button is clicked in the alert', fakeAsync(() => { - confirmSpy.and.returnValue(false); - let regenerateTopicSpy = spyOn( - adminBackendApiService, 'rollbackExplorationToSafeState'); - // Setting cancel button clicked to be true. + expect(regenerateTopicSpy).toHaveBeenCalled(); + expect(statusMessageSpy).toHaveBeenCalledWith( + 'Server error: Internal Server Error.' + ); + }) + ); + + it( + 'should not request backend to rollback exploration if ' + + 'a task is still running in the queue', + fakeAsync(() => { + confirmSpy.and.returnValue(true); + let regenerateTopicSpy = spyOn( + adminBackendApiService, + 'rollbackExplorationToSafeState' + ); + + // Setting task is still running to be true. + spyOn(adminTaskManagerService, 'isTaskRunning').and.returnValue(true); + + component.rollbackExploration(); + tick(); - component.rollbackExploration(); - tick(); + expect(regenerateTopicSpy).not.toHaveBeenCalled(); + }) + ); + + it( + 'should not request backend to rollback exploration if ' + + 'cancel button is clicked in the alert', + fakeAsync(() => { + confirmSpy.and.returnValue(false); + let regenerateTopicSpy = spyOn( + adminBackendApiService, + 'rollbackExplorationToSafeState' + ); + // Setting cancel button clicked to be true. + + component.rollbackExploration(); + tick(); - expect(regenerateTopicSpy).not.toHaveBeenCalled(); - })); + expect(regenerateTopicSpy).not.toHaveBeenCalled(); + }) + ); }); describe('when clicking on upload csv button ', () => { it('should upload csv file successfully', fakeAsync(() => { let uploadCsvSpy = spyOn( - adminBackendApiService, 'uploadTopicSimilaritiesAsync') - .and.returnValue(Promise.resolve()); + adminBackendApiService, + 'uploadTopicSimilaritiesAsync' + ).and.returnValue(Promise.resolve()); component.uploadTopicSimilaritiesFile(); tick(); @@ -303,19 +355,23 @@ describe('Admin misc tab component ', () => { ); })); - it('should not upload csv file in case of backend ' + - 'error', fakeAsync(() => { - let uploadCsvSpy = spyOn( - adminBackendApiService, 'uploadTopicSimilaritiesAsync') - .and.rejectWith('Internal Server Error.'); + it( + 'should not upload csv file in case of backend ' + 'error', + fakeAsync(() => { + let uploadCsvSpy = spyOn( + adminBackendApiService, + 'uploadTopicSimilaritiesAsync' + ).and.rejectWith('Internal Server Error.'); - component.uploadTopicSimilaritiesFile(); - tick(); + component.uploadTopicSimilaritiesFile(); + tick(); - expect(uploadCsvSpy).toHaveBeenCalled(); - expect(statusMessageSpy).toHaveBeenCalledWith( - 'Server error: Internal Server Error.'); - })); + expect(uploadCsvSpy).toHaveBeenCalled(); + expect(statusMessageSpy).toHaveBeenCalledWith( + 'Server error: Internal Server Error.' + ); + }) + ); it('should throw error if no label is found for uploading files', () => { spyOn(document, 'getElementById').and.returnValue(null); @@ -334,7 +390,7 @@ describe('Admin misc tab component ', () => { // @ts-expect-error spyOn(document, 'getElementById').and.callFake(() => { return { - files: null + files: null, }; }); expect(() => { @@ -343,54 +399,66 @@ describe('Admin misc tab component ', () => { }); }); - it('should download topic similarities csv file ' + - 'on clicking download button', () => { - let downloadHandler = '/admintopicscsvdownloadhandler'; - expect(mockWindowRef.nativeWindow.location.href).toEqual('href'); - component.downloadTopicSimilaritiesFile(); - expect(mockWindowRef.nativeWindow.location.href).toEqual(downloadHandler); - }); + it( + 'should download topic similarities csv file ' + + 'on clicking download button', + () => { + let downloadHandler = '/admintopicscsvdownloadhandler'; + expect(mockWindowRef.nativeWindow.location.href).toEqual('href'); + component.downloadTopicSimilaritiesFile(); + expect(mockWindowRef.nativeWindow.location.href).toEqual(downloadHandler); + } + ); - it('should set data extraction query message ' + - 'on clicking submit query button', () => { - let message = 'message'; - // Pre-checks. - expect(component.showDataExtractionQueryStatus).toBeFalse(); - expect(component.dataExtractionQueryStatusMessage).toBeUndefined(); + it( + 'should set data extraction query message ' + + 'on clicking submit query button', + () => { + let message = 'message'; + // Pre-checks. + expect(component.showDataExtractionQueryStatus).toBeFalse(); + expect(component.dataExtractionQueryStatusMessage).toBeUndefined(); - component.setDataExtractionQueryStatusMessage(message); + component.setDataExtractionQueryStatusMessage(message); - expect(component.showDataExtractionQueryStatus).toBeTrue(); - expect(component.dataExtractionQueryStatusMessage).toBe(message); - }); + expect(component.showDataExtractionQueryStatus).toBeTrue(); + expect(component.dataExtractionQueryStatusMessage).toBe(message); + } + ); describe('when clicking on send mail to admin button ', () => { it('should send mail successfully', fakeAsync(() => { let sendMailSpy = spyOn( - adminBackendApiService, 'sendDummyMailToAdminAsync') - .and.returnValue(Promise.resolve()); + adminBackendApiService, + 'sendDummyMailToAdminAsync' + ).and.returnValue(Promise.resolve()); component.sendDummyMailToAdmin(); tick(); expect(sendMailSpy).toHaveBeenCalled(); expect(statusMessageSpy).toHaveBeenCalledWith( - 'Success! Mail sent to admin.'); + 'Success! Mail sent to admin.' + ); })); - it('should not send mail to admin in case of backend ' + - 'error', fakeAsync(() => { - let sendMailSpy = spyOn( - adminBackendApiService, 'sendDummyMailToAdminAsync') - .and.rejectWith('Internal Server Error.'); + it( + 'should not send mail to admin in case of backend ' + 'error', + fakeAsync(() => { + let sendMailSpy = spyOn( + adminBackendApiService, + 'sendDummyMailToAdminAsync' + ).and.rejectWith('Internal Server Error.'); - component.sendDummyMailToAdmin(); - tick(); + component.sendDummyMailToAdmin(); + tick(); - expect(sendMailSpy).toHaveBeenCalled(); - expect(statusMessageSpy).toHaveBeenCalledWith( - 'Server error: Internal Server Error.'); - })); + expect(sendMailSpy).toHaveBeenCalled(); + expect(statusMessageSpy).toHaveBeenCalledWith( + 'Server error: Internal Server Error.' + ); + }) + ); }); describe('when clicking on update username button ', () => { @@ -398,32 +466,38 @@ describe('Admin misc tab component ', () => { component.oldUsername = 'oldUsername'; component.newUsername = 'newUsername'; let updateUsernameSpy = spyOn( - adminBackendApiService, 'updateUserNameAsync') - .and.returnValue(Promise.resolve()); + adminBackendApiService, + 'updateUserNameAsync' + ).and.returnValue(Promise.resolve()); component.updateUsername(); tick(); expect(updateUsernameSpy).toHaveBeenCalled(); expect(statusMessageSpy).toHaveBeenCalledWith( - 'Successfully renamed oldUsername to newUsername!'); + 'Successfully renamed oldUsername to newUsername!' + ); })); - it('should not update username in case of backend ' + - 'error', fakeAsync(() => { - component.oldUsername = 'oldUsername'; - component.newUsername = 'newUsername'; - let updateUsernameSpy = spyOn( - adminBackendApiService, 'updateUserNameAsync') - .and.rejectWith('Internal Server Error.'); - - component.updateUsername(); - tick(); + it( + 'should not update username in case of backend ' + 'error', + fakeAsync(() => { + component.oldUsername = 'oldUsername'; + component.newUsername = 'newUsername'; + let updateUsernameSpy = spyOn( + adminBackendApiService, + 'updateUserNameAsync' + ).and.rejectWith('Internal Server Error.'); + + component.updateUsername(); + tick(); - expect(updateUsernameSpy).toHaveBeenCalled(); - expect(statusMessageSpy).toHaveBeenCalledWith( - 'Server error: Internal Server Error.'); - })); + expect(updateUsernameSpy).toHaveBeenCalled(); + expect(statusMessageSpy).toHaveBeenCalledWith( + 'Server error: Internal Server Error.' + ); + }) + ); }); describe('when clicking on update blog post button ', () => { @@ -432,70 +506,85 @@ describe('Admin misc tab component ', () => { component.publishedOn = '05/09/2001'; component.blogPostId = '123456sample'; let updateBlogPostSpy = spyOn( - adminBackendApiService, 'updateBlogPostDataAsync') - .and.returnValue(Promise.resolve()); + adminBackendApiService, + 'updateBlogPostDataAsync' + ).and.returnValue(Promise.resolve()); component.updateBlogPostData(); tick(); expect(updateBlogPostSpy).toHaveBeenCalled(); expect(statusMessageSpy).toHaveBeenCalledWith( - 'Successfully updated blog post data'); + 'Successfully updated blog post data' + ); })); - it('should not update blog post in case of backend ' + - 'error', fakeAsync(() => { - component.authorUsername = 'username'; - component.publishedOn = '05/09/2001'; - component.blogPostId = '123456sample'; - let updateBlogPostSpy = spyOn( - adminBackendApiService, 'updateBlogPostDataAsync') - .and.rejectWith('Internal Server Error.'); - - component.updateBlogPostData(); - tick(); + it( + 'should not update blog post in case of backend ' + 'error', + fakeAsync(() => { + component.authorUsername = 'username'; + component.publishedOn = '05/09/2001'; + component.blogPostId = '123456sample'; + let updateBlogPostSpy = spyOn( + adminBackendApiService, + 'updateBlogPostDataAsync' + ).and.rejectWith('Internal Server Error.'); + + component.updateBlogPostData(); + tick(); - expect(updateBlogPostSpy).toHaveBeenCalled(); - expect(statusMessageSpy).toHaveBeenCalledWith( - 'Server error: Internal Server Error.'); - })); + expect(updateBlogPostSpy).toHaveBeenCalled(); + expect(statusMessageSpy).toHaveBeenCalledWith( + 'Server error: Internal Server Error.' + ); + }) + ); }); describe('when clicking on number of deletion requests button ', () => { it('should show number of deletion requests successfully', fakeAsync(() => { let updateUsernameSpy = spyOn( - adminBackendApiService, 'getNumberOfPendingDeletionRequestAsync') - .and.returnValue(Promise.resolve( - {number_of_pending_deletion_models: '5'})); + adminBackendApiService, + 'getNumberOfPendingDeletionRequestAsync' + ).and.returnValue( + Promise.resolve({number_of_pending_deletion_models: '5'}) + ); component.getNumberOfPendingDeletionRequestModels(); tick(); expect(updateUsernameSpy).toHaveBeenCalled(); expect(statusMessageSpy).toHaveBeenCalledWith( - 'The number of users that are being deleted is: 5'); + 'The number of users that are being deleted is: 5' + ); })); - it('should not show number of deletion requests in case of backend ' + - 'error', fakeAsync(() => { - let updateUsernameSpy = spyOn( - adminBackendApiService, 'getNumberOfPendingDeletionRequestAsync') - .and.rejectWith('Internal Server Error.'); + it( + 'should not show number of deletion requests in case of backend ' + + 'error', + fakeAsync(() => { + let updateUsernameSpy = spyOn( + adminBackendApiService, + 'getNumberOfPendingDeletionRequestAsync' + ).and.rejectWith('Internal Server Error.'); - component.getNumberOfPendingDeletionRequestModels(); - tick(); + component.getNumberOfPendingDeletionRequestModels(); + tick(); - expect(updateUsernameSpy).toHaveBeenCalled(); - expect(statusMessageSpy).toHaveBeenCalledWith( - 'Server error: Internal Server Error.'); - })); + expect(updateUsernameSpy).toHaveBeenCalled(); + expect(statusMessageSpy).toHaveBeenCalledWith( + 'Server error: Internal Server Error.' + ); + }) + ); }); describe('when clicking on grant permissions button ', () => { it('should grant super admin permissions successfully', fakeAsync(() => { let permissionsSpy = spyOn( - adminBackendApiService, 'grantSuperAdminPrivilegesAsync') - .and.returnValue(Promise.resolve()); + adminBackendApiService, + 'grantSuperAdminPrivilegesAsync' + ).and.returnValue(Promise.resolve()); component.grantSuperAdminPrivileges(); tick(); @@ -504,26 +593,31 @@ describe('Admin misc tab component ', () => { expect(statusMessageSpy).toHaveBeenCalledWith('Success!'); })); - it('should not grant super admin permissions in case of backend ' + - 'error', fakeAsync(() => { - let permissionsSpy = spyOn( - adminBackendApiService, 'grantSuperAdminPrivilegesAsync') - .and.rejectWith({ error: {error: 'Internal Server Error.'}}); + it( + 'should not grant super admin permissions in case of backend ' + 'error', + fakeAsync(() => { + let permissionsSpy = spyOn( + adminBackendApiService, + 'grantSuperAdminPrivilegesAsync' + ).and.rejectWith({error: {error: 'Internal Server Error.'}}); - component.grantSuperAdminPrivileges(); - tick(); + component.grantSuperAdminPrivileges(); + tick(); - expect(permissionsSpy).toHaveBeenCalled(); - expect(statusMessageSpy).toHaveBeenCalledWith( - 'Server error: Internal Server Error.'); - })); + expect(permissionsSpy).toHaveBeenCalled(); + expect(statusMessageSpy).toHaveBeenCalledWith( + 'Server error: Internal Server Error.' + ); + }) + ); }); describe('when clicking on revoke permissions button ', () => { it('should revoke super admin permissions successfully', fakeAsync(() => { let permissionsSpy = spyOn( - adminBackendApiService, 'revokeSuperAdminPrivilegesAsync') - .and.returnValue(Promise.resolve()); + adminBackendApiService, + 'revokeSuperAdminPrivilegesAsync' + ).and.returnValue(Promise.resolve()); component.revokeSuperAdminPrivileges(); tick(); @@ -532,102 +626,119 @@ describe('Admin misc tab component ', () => { expect(statusMessageSpy).toHaveBeenCalledWith('Success!'); })); - it('should not revoke super admin permissions in case of backend ' + - 'error', fakeAsync(() => { - let permissionsSpy = spyOn( - adminBackendApiService, 'revokeSuperAdminPrivilegesAsync') - .and.rejectWith({ error: {error: 'Internal Server Error.'}}); + it( + 'should not revoke super admin permissions in case of backend ' + 'error', + fakeAsync(() => { + let permissionsSpy = spyOn( + adminBackendApiService, + 'revokeSuperAdminPrivilegesAsync' + ).and.rejectWith({error: {error: 'Internal Server Error.'}}); - component.revokeSuperAdminPrivileges(); - tick(); + component.revokeSuperAdminPrivileges(); + tick(); - expect(permissionsSpy).toHaveBeenCalled(); - expect(statusMessageSpy).toHaveBeenCalledWith( - 'Server error: Internal Server Error.'); - })); + expect(permissionsSpy).toHaveBeenCalled(); + expect(statusMessageSpy).toHaveBeenCalledWith( + 'Server error: Internal Server Error.' + ); + }) + ); }); - describe('when clicking on get models related to users button ', () => { it('should return user models successfully if they exist', fakeAsync(() => { let uesrModelSpy = spyOn( - adminBackendApiService, 'getModelsRelatedToUserAsync') - .and.returnValue(Promise.resolve(true)); + adminBackendApiService, + 'getModelsRelatedToUserAsync' + ).and.returnValue(Promise.resolve(true)); component.getModelsRelatedToUser(); tick(); expect(uesrModelSpy).toHaveBeenCalled(); expect(statusMessageSpy).toHaveBeenCalledWith( - 'Some related models exist, see logs ' + - 'to find out the exact models'); + 'Some related models exist, see logs ' + 'to find out the exact models' + ); })); it('should not return user models if they does not exist', fakeAsync(() => { let uesrModelSpy = spyOn( - adminBackendApiService, 'getModelsRelatedToUserAsync') - .and.returnValue(Promise.resolve(false)); + adminBackendApiService, + 'getModelsRelatedToUserAsync' + ).and.returnValue(Promise.resolve(false)); component.getModelsRelatedToUser(); tick(); expect(uesrModelSpy).toHaveBeenCalled(); - expect(statusMessageSpy).toHaveBeenCalledWith( - 'No related models exist'); + expect(statusMessageSpy).toHaveBeenCalledWith('No related models exist'); })); - it('should not return user models in case of backend ' + - 'error', fakeAsync(() => { - let uesrModelSpy = spyOn( - adminBackendApiService, 'getModelsRelatedToUserAsync') - .and.rejectWith('Internal Server Error.'); + it( + 'should not return user models in case of backend ' + 'error', + fakeAsync(() => { + let uesrModelSpy = spyOn( + adminBackendApiService, + 'getModelsRelatedToUserAsync' + ).and.rejectWith('Internal Server Error.'); - component.getModelsRelatedToUser(); - tick(); + component.getModelsRelatedToUser(); + tick(); - expect(uesrModelSpy).toHaveBeenCalled(); - expect(statusMessageSpy).toHaveBeenCalledWith( - 'Server error: Internal Server Error.'); - })); + expect(uesrModelSpy).toHaveBeenCalled(); + expect(statusMessageSpy).toHaveBeenCalledWith( + 'Server error: Internal Server Error.' + ); + }) + ); }); describe('when clicking on delete user button ', () => { it('should delete the user account successfully', fakeAsync(() => { let uesrModelSpy = spyOn( - adminBackendApiService, 'deleteUserAsync') - .and.returnValue(Promise.resolve()); + adminBackendApiService, + 'deleteUserAsync' + ).and.returnValue(Promise.resolve()); component.deleteUser(); tick(); expect(uesrModelSpy).toHaveBeenCalled(); expect(statusMessageSpy).toHaveBeenCalledWith( - 'The deletion process was started.'); + 'The deletion process was started.' + ); })); - it('should not delete user account in case of backend ' + - 'error', fakeAsync(() => { - let uesrModelSpy = spyOn( - adminBackendApiService, 'deleteUserAsync') - .and.rejectWith('Internal Server Error.'); + it( + 'should not delete user account in case of backend ' + 'error', + fakeAsync(() => { + let uesrModelSpy = spyOn( + adminBackendApiService, + 'deleteUserAsync' + ).and.rejectWith('Internal Server Error.'); - component.deleteUser(); - tick(); + component.deleteUser(); + tick(); - expect(uesrModelSpy).toHaveBeenCalled(); - expect(statusMessageSpy).toHaveBeenCalledWith( - 'Server error: Internal Server Error.'); - })); + expect(uesrModelSpy).toHaveBeenCalled(); + expect(statusMessageSpy).toHaveBeenCalledWith( + 'Server error: Internal Server Error.' + ); + }) + ); }); it('should sumbit query when clicking on submit query button', () => { let setQueryDataSpy = spyOn( - component, 'setDataExtractionQueryStatusMessage').and.callThrough(); + component, + 'setDataExtractionQueryStatusMessage' + ).and.callThrough(); component.submitQuery(); expect(setQueryDataSpy).toHaveBeenCalledWith( - 'Data extraction query has been submitted. Please wait.'); + 'Data extraction query has been submitted. Please wait.' + ); }); it('should reset form data on clicking reset button', () => { @@ -644,71 +755,77 @@ describe('Admin misc tab component ', () => { expect(component.stateName).toBe(''); }); - describe('when clicking on the Lookup Exploration Interaction IDs button' - , () => { - it('should return interaction IDs if the exploration exists' - , fakeAsync(() => { - let interactionSpy = spyOn( - adminBackendApiService, 'retrieveExplorationInteractionIdsAsync') - .and.returnValue(Promise.resolve( - { interactions: [{id: 'EndExploration'}] })); - - component.retrieveExplorationInteractionIds(); - tick(); - - expect(interactionSpy).toHaveBeenCalled(); - expect(statusMessageSpy).toHaveBeenCalledWith( - 'Successfully fetched interactions in exploration.'); - expect(component.explorationInteractions) - .toEqual([{id: 'EndExploration'}]); - })); - - it('should return empty interaction IDs' + - ' if no interactions are found in the exploration' - , fakeAsync(() => { + describe('when clicking on the Lookup Exploration Interaction IDs button', () => { + it('should return interaction IDs if the exploration exists', fakeAsync(() => { + let interactionSpy = spyOn( + adminBackendApiService, + 'retrieveExplorationInteractionIdsAsync' + ).and.returnValue( + Promise.resolve({interactions: [{id: 'EndExploration'}]}) + ); + + component.retrieveExplorationInteractionIds(); + tick(); + + expect(interactionSpy).toHaveBeenCalled(); + expect(statusMessageSpy).toHaveBeenCalledWith( + 'Successfully fetched interactions in exploration.' + ); + expect(component.explorationInteractions).toEqual([ + {id: 'EndExploration'}, + ]); + })); + + it( + 'should return empty interaction IDs' + + ' if no interactions are found in the exploration', + fakeAsync(() => { let interactionSpy = spyOn( - adminBackendApiService, 'retrieveExplorationInteractionIdsAsync') - .and.returnValue(Promise.resolve( - { interactions: [] })); + adminBackendApiService, + 'retrieveExplorationInteractionIdsAsync' + ).and.returnValue(Promise.resolve({interactions: []})); component.retrieveExplorationInteractionIds(); tick(); expect(interactionSpy).toHaveBeenCalled(); expect(statusMessageSpy).toHaveBeenCalledWith( - 'No interactions found in exploration.'); - expect(component.explorationInteractions) - .toEqual([]); - })); - - it('should handle the case when the exploration does not exist' - , fakeAsync(() => { - let intSpy = spyOn( - adminBackendApiService, 'retrieveExplorationInteractionIdsAsync') - .and.rejectWith('Exploration does not exist'); - - component.retrieveExplorationInteractionIds(); - tick(); - - expect(intSpy).toHaveBeenCalled(); - expect(statusMessageSpy).toHaveBeenCalledWith( - 'Server error: Exploration does not exist'); - expect(component.explorationInteractions).toEqual([]); - })); - - it('should handle the case when expIdToGetInteractions is empty' - , fakeAsync(() => { - let intSpy = spyOn( - adminBackendApiService, 'retrieveExplorationInteractionIdsAsync') - .and.rejectWith('Exploration does not exist'); - - component.retrieveExplorationInteractionIds(); - tick(); - - expect(intSpy).toHaveBeenCalled(); - expect(statusMessageSpy).toHaveBeenCalledWith( - 'Server error: Exploration does not exist'); - expect(component.explorationInteractions).toEqual([]); - })); - }); + 'No interactions found in exploration.' + ); + expect(component.explorationInteractions).toEqual([]); + }) + ); + + it('should handle the case when the exploration does not exist', fakeAsync(() => { + let intSpy = spyOn( + adminBackendApiService, + 'retrieveExplorationInteractionIdsAsync' + ).and.rejectWith('Exploration does not exist'); + + component.retrieveExplorationInteractionIds(); + tick(); + + expect(intSpy).toHaveBeenCalled(); + expect(statusMessageSpy).toHaveBeenCalledWith( + 'Server error: Exploration does not exist' + ); + expect(component.explorationInteractions).toEqual([]); + })); + + it('should handle the case when expIdToGetInteractions is empty', fakeAsync(() => { + let intSpy = spyOn( + adminBackendApiService, + 'retrieveExplorationInteractionIdsAsync' + ).and.rejectWith('Exploration does not exist'); + + component.retrieveExplorationInteractionIds(); + tick(); + + expect(intSpy).toHaveBeenCalled(); + expect(statusMessageSpy).toHaveBeenCalledWith( + 'Server error: Exploration does not exist' + ); + expect(component.explorationInteractions).toEqual([]); + })); + }); }); diff --git a/core/templates/pages/admin-page/misc-tab/admin-misc-tab.component.ts b/core/templates/pages/admin-page/misc-tab/admin-misc-tab.component.ts index 36165a626641..05ecdbca5bc7 100644 --- a/core/templates/pages/admin-page/misc-tab/admin-misc-tab.component.ts +++ b/core/templates/pages/admin-page/misc-tab/admin-misc-tab.component.ts @@ -16,24 +16,27 @@ * @fileoverview Component for the miscellaneous tab in the admin panel. */ -import { Component, EventEmitter, Output } from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { AdminBackendApiService, Interaction } from 'domain/admin/admin-backend-api.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { AdminPageConstants } from '../admin-page.constants'; -import { AdminTaskManagerService } from '../services/admin-task-manager.service'; +import {Component, EventEmitter, Output} from '@angular/core'; +import {AppConstants} from 'app.constants'; +import { + AdminBackendApiService, + Interaction, +} from 'domain/admin/admin-backend-api.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {AdminPageConstants} from '../admin-page.constants'; +import {AdminTaskManagerService} from '../services/admin-task-manager.service'; @Component({ selector: 'oppia-admin-misc-tab', - templateUrl: './admin-misc-tab.component.html' + templateUrl: './admin-misc-tab.component.html', }) export class AdminMiscTabComponent { @Output() setStatusMessage: EventEmitter = new EventEmitter(); - DATA_EXTRACTION_QUERY_HANDLER_URL: string = ( - '/explorationdataextractionhandler'); + DATA_EXTRACTION_QUERY_HANDLER_URL: string = + '/explorationdataextractionhandler'; - irreversibleActionMessage: string = ( - 'This action is irreversible. Are you sure?'); + irreversibleActionMessage: string = + 'This action is irreversible. Are you sure?'; // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see @@ -78,14 +81,16 @@ export class AdminMiscTabComponent { this.setStatusMessage.emit('Clearing search index...'); this.adminTaskManagerService.startTask(); - this.adminBackendApiService.clearSearchIndexAsync() - .then(() => { + this.adminBackendApiService.clearSearchIndexAsync().then( + () => { this.setStatusMessage.emit('Index successfully cleared.'); this.adminTaskManagerService.finishTask(); - }, errorResponse => { + }, + errorResponse => { this.setStatusMessage.emit('Server error: ' + errorResponse); this.adminTaskManagerService.finishTask(); - }); + } + ); } regenerateOpportunitiesRelatedToTopic(): void { @@ -96,14 +101,20 @@ export class AdminMiscTabComponent { return; } this.setStatusMessage.emit('Regenerating opportunities...'); - this.adminBackendApiService.regenerateOpportunitiesRelatedToTopicAsync( - this.topicIdForRegeneratingOpportunities).then(response => { - this.setStatusMessage.emit( - 'No. of opportunities model created: ' + - response); - }, errorResponse => { - this.setStatusMessage.emit('Server error: ' + errorResponse); - }); + this.adminBackendApiService + .regenerateOpportunitiesRelatedToTopicAsync( + this.topicIdForRegeneratingOpportunities + ) + .then( + response => { + this.setStatusMessage.emit( + 'No. of opportunities model created: ' + response + ); + }, + errorResponse => { + this.setStatusMessage.emit('Server error: ' + errorResponse); + } + ); } rollbackExploration(): void { @@ -114,15 +125,20 @@ export class AdminMiscTabComponent { return; } this.setStatusMessage.emit( - `Rollingback exploration ${this.expIdToRollback}...`); - this.adminBackendApiService.rollbackExplorationToSafeState( - this.expIdToRollback - ).then(response => { - this.setStatusMessage.emit( - 'Exploration rolledback to version: ' + response); - }, errorResponse => { - this.setStatusMessage.emit('Server error: ' + errorResponse); - }); + `Rollingback exploration ${this.expIdToRollback}...` + ); + this.adminBackendApiService + .rollbackExplorationToSafeState(this.expIdToRollback) + .then( + response => { + this.setStatusMessage.emit( + 'Exploration rolledback to version: ' + response + ); + }, + errorResponse => { + this.setStatusMessage.emit('Server error: ' + errorResponse); + } + ); } uploadTopicSimilaritiesFile(): void { @@ -139,22 +155,27 @@ export class AdminMiscTabComponent { } let file = element.files[0]; let reader = new FileReader(); - reader.onload = (e) => { + reader.onload = e => { let data = (e.target as FileReader).result; - this.adminBackendApiService.uploadTopicSimilaritiesAsync(data as string) - .then(() => { - this.setStatusMessage.emit( - 'Topic similarities uploaded successfully.'); - }, errorResponse => { - this.setStatusMessage.emit('Server error: ' + errorResponse); - }); + this.adminBackendApiService + .uploadTopicSimilaritiesAsync(data as string) + .then( + () => { + this.setStatusMessage.emit( + 'Topic similarities uploaded successfully.' + ); + }, + errorResponse => { + this.setStatusMessage.emit('Server error: ' + errorResponse); + } + ); }; reader.readAsText(file); } downloadTopicSimilaritiesFile(): void { - this.windowRef.nativeWindow.location.href = ( - AdminPageConstants.ADMIN_TOPICS_CSV_DOWNLOAD_HANDLER_URL); + this.windowRef.nativeWindow.location.href = + AdminPageConstants.ADMIN_TOPICS_CSV_DOWNLOAD_HANDLER_URL; } setDataExtractionQueryStatusMessage(message: string): void { @@ -163,125 +184,150 @@ export class AdminMiscTabComponent { } sendDummyMailToAdmin(): void { - this.adminBackendApiService.sendDummyMailToAdminAsync() - .then(() => { + this.adminBackendApiService.sendDummyMailToAdminAsync().then( + () => { this.setStatusMessage.emit('Success! Mail sent to admin.'); - }, errorResponse => { + }, + errorResponse => { this.setStatusMessage.emit('Server error: ' + errorResponse); - }); + } + ); } updateUsername(): void { this.setStatusMessage.emit('Updating username...'); - this.adminBackendApiService.updateUserNameAsync( - this.oldUsername, this.newUsername) - .then(() => { - this.setStatusMessage.emit( - 'Successfully renamed ' + this.oldUsername + ' to ' + - this.newUsername + '!'); - }, errorResponse => { - this.setStatusMessage.emit('Server error: ' + errorResponse); - }); + this.adminBackendApiService + .updateUserNameAsync(this.oldUsername, this.newUsername) + .then( + () => { + this.setStatusMessage.emit( + 'Successfully renamed ' + + this.oldUsername + + ' to ' + + this.newUsername + + '!' + ); + }, + errorResponse => { + this.setStatusMessage.emit('Server error: ' + errorResponse); + } + ); } updateBlogPostData(): void { this.setStatusMessage.emit('Updating blog post data...'); - this.adminBackendApiService.updateBlogPostDataAsync( - this.blogPostId, this.authorUsername, this.publishedOn) - .then(() => { - this.setStatusMessage.emit( - 'Successfully updated blog post data'); - }, (errorResponse) => { - this.setStatusMessage.emit('Server error: ' + errorResponse); - }); + this.adminBackendApiService + .updateBlogPostDataAsync( + this.blogPostId, + this.authorUsername, + this.publishedOn + ) + .then( + () => { + this.setStatusMessage.emit('Successfully updated blog post data'); + }, + errorResponse => { + this.setStatusMessage.emit('Server error: ' + errorResponse); + } + ); } getNumberOfPendingDeletionRequestModels(): void { this.setStatusMessage.emit( - 'Getting the number of users that are being deleted...'); - this.adminBackendApiService.getNumberOfPendingDeletionRequestAsync() - .then(pendingDeletionRequests => { + 'Getting the number of users that are being deleted...' + ); + this.adminBackendApiService.getNumberOfPendingDeletionRequestAsync().then( + pendingDeletionRequests => { this.setStatusMessage.emit( 'The number of users that are being deleted is: ' + - pendingDeletionRequests.number_of_pending_deletion_models); - }, errorResponse => { + pendingDeletionRequests.number_of_pending_deletion_models + ); + }, + errorResponse => { this.setStatusMessage.emit('Server error: ' + errorResponse); } - ); + ); } grantSuperAdminPrivileges(): void { this.setStatusMessage.emit('Communicating with Firebase server...'); - this.adminBackendApiService.grantSuperAdminPrivilegesAsync( - this.usernameToGrant - ).then( - () => { - this.setStatusMessage.emit('Success!'); - }, errorResponse => { - this.setStatusMessage.emit( - 'Server error: ' + errorResponse.error.error); - }); + this.adminBackendApiService + .grantSuperAdminPrivilegesAsync(this.usernameToGrant) + .then( + () => { + this.setStatusMessage.emit('Success!'); + }, + errorResponse => { + this.setStatusMessage.emit( + 'Server error: ' + errorResponse.error.error + ); + } + ); } revokeSuperAdminPrivileges(): void { this.setStatusMessage.emit('Communicating with Firebase server...'); - this.adminBackendApiService.revokeSuperAdminPrivilegesAsync( - this.usernameToRevoke - ).then( - () => { - this.setStatusMessage.emit('Success!'); - }, errorResponse => { - this.setStatusMessage.emit( - 'Server error: ' + errorResponse.error.error); - }); + this.adminBackendApiService + .revokeSuperAdminPrivilegesAsync(this.usernameToRevoke) + .then( + () => { + this.setStatusMessage.emit('Success!'); + }, + errorResponse => { + this.setStatusMessage.emit( + 'Server error: ' + errorResponse.error.error + ); + } + ); } getModelsRelatedToUser(): void { this.setStatusMessage.emit('Getting the models related to user...'); - this.adminBackendApiService.getModelsRelatedToUserAsync(this.userIdToGet) - .then(isModal => { - if (isModal) { - this.setStatusMessage.emit( - 'Some related models exist, see logs ' + - 'to find out the exact models' - ); - } else { - this.setStatusMessage.emit('No related models exist'); + this.adminBackendApiService + .getModelsRelatedToUserAsync(this.userIdToGet) + .then( + isModal => { + if (isModal) { + this.setStatusMessage.emit( + 'Some related models exist, see logs ' + + 'to find out the exact models' + ); + } else { + this.setStatusMessage.emit('No related models exist'); + } + }, + errorResponse => { + this.setStatusMessage.emit('Server error: ' + errorResponse); } - }, errorResponse => { - this.setStatusMessage.emit( - 'Server error: ' + errorResponse); - } ); } deleteUser(): void { this.setStatusMessage.emit('Starting the deletion of the user...'); - this.adminBackendApiService.deleteUserAsync( - this.userIdToDelete, this.usernameToDelete) - .then(() => { - this.setStatusMessage.emit('The deletion process was started.'); - }, errorResponse => { - this.setStatusMessage.emit('Server error: ' + errorResponse); - } + this.adminBackendApiService + .deleteUserAsync(this.userIdToDelete, this.usernameToDelete) + .then( + () => { + this.setStatusMessage.emit('The deletion process was started.'); + }, + errorResponse => { + this.setStatusMessage.emit('Server error: ' + errorResponse); + } ); } submitQuery(): void { - let STATUS_PENDING = ( - 'Data extraction query has been submitted. Please wait.'); + let STATUS_PENDING = + 'Data extraction query has been submitted. Please wait.'; this.setDataExtractionQueryStatusMessage(STATUS_PENDING); - let downloadUrl = (this.DATA_EXTRACTION_QUERY_HANDLER_URL + '?'); + let downloadUrl = this.DATA_EXTRACTION_QUERY_HANDLER_URL + '?'; downloadUrl += 'exp_id=' + encodeURIComponent(this.expId); - downloadUrl += '&exp_version=' + encodeURIComponent( - this.expVersion); - downloadUrl += '&state_name=' + encodeURIComponent( - this.stateName); - downloadUrl += '&num_answers=' + encodeURIComponent( - this.numAnswers); + downloadUrl += '&exp_version=' + encodeURIComponent(this.expVersion); + downloadUrl += '&state_name=' + encodeURIComponent(this.stateName); + downloadUrl += '&num_answers=' + encodeURIComponent(this.numAnswers); this.windowRef.nativeWindow.open(downloadUrl); } @@ -289,21 +335,22 @@ export class AdminMiscTabComponent { retrieveExplorationInteractionIds(): void { this.explorationInteractions = []; this.setStatusMessage.emit('Retrieving interactions in exploration ...'); - this.adminBackendApiService.retrieveExplorationInteractionIdsAsync( - this.expIdToGetInteractions) - .then(response => { - if (response.interactions.length > 0) { - this.setStatusMessage.emit( - 'Successfully fetched interactions in exploration.'); - this.explorationInteractions = response.interactions; - } else { - this.setStatusMessage.emit( - 'No interactions found in exploration.'); + this.adminBackendApiService + .retrieveExplorationInteractionIdsAsync(this.expIdToGetInteractions) + .then( + response => { + if (response.interactions.length > 0) { + this.setStatusMessage.emit( + 'Successfully fetched interactions in exploration.' + ); + this.explorationInteractions = response.interactions; + } else { + this.setStatusMessage.emit('No interactions found in exploration.'); + } + }, + errorResponse => { + this.setStatusMessage.emit('Server error: ' + errorResponse); } - }, errorResponse => { - this.setStatusMessage.emit( - 'Server error: ' + errorResponse); - } ); } diff --git a/core/templates/pages/admin-page/navbar/admin-navbar.component.spec.ts b/core/templates/pages/admin-page/navbar/admin-navbar.component.spec.ts index 2e4365115b3b..243a1d5fbae0 100644 --- a/core/templates/pages/admin-page/navbar/admin-navbar.component.spec.ts +++ b/core/templates/pages/admin-page/navbar/admin-navbar.component.spec.ts @@ -16,16 +16,22 @@ * @fileoverview Unit tests for admin navbar component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { APP_BASE_HREF } from '@angular/common'; -import { RouterModule } from '@angular/router'; -import { UserInfo } from 'domain/user/user-info.model'; -import { SmartRouterModule } from 'hybrid-router-module-provider'; - -import { UserService } from 'services/user.service'; -import { AdminRouterService } from '../services/admin-router.service'; -import { AdminNavbarComponent } from './admin-navbar.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import { + async, + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {APP_BASE_HREF} from '@angular/common'; +import {RouterModule} from '@angular/router'; +import {UserInfo} from 'domain/user/user-info.model'; +import {SmartRouterModule} from 'hybrid-router-module-provider'; + +import {UserService} from 'services/user.service'; +import {AdminRouterService} from '../services/admin-router.service'; +import {AdminNavbarComponent} from './admin-navbar.component'; describe('Admin Navbar component', () => { let component: AdminNavbarComponent; @@ -36,7 +42,7 @@ describe('Admin Navbar component', () => { let userInfo = { isModerator: () => true, getUsername: () => 'username1', - isSuperAdmin: () => true + isSuperAdmin: () => true, }; let imagePath = '/path/to/image.png'; let profileUrl = '/profile/username1'; @@ -49,13 +55,15 @@ describe('Admin Navbar component', () => { // TODO(#13443): Remove hybrid router module provider once all pages are // migrated to angular router. SmartRouterModule, - RouterModule.forRoot([]) + RouterModule.forRoot([]), ], declarations: [AdminNavbarComponent], - providers: [{ - provide: APP_BASE_HREF, - useValue: '/' - }] + providers: [ + { + provide: APP_BASE_HREF, + useValue: '/', + }, + ], }).compileComponents(); fixture = TestBed.createComponent(AdminNavbarComponent); @@ -64,13 +72,14 @@ describe('Admin Navbar component', () => { adminRouterService = TestBed.get(AdminRouterService); fixture.detectChanges(); - spyOn(userService, 'getProfileImageDataUrl') - .and.returnValue([userProfilePngImage, userProfileWebpImage]); + spyOn(userService, 'getProfileImageDataUrl').and.returnValue([ + userProfilePngImage, + userProfileWebpImage, + ]); })); it('should initialize component properties correctly', fakeAsync(() => { - spyOn(userService, 'getUserInfoAsync') - .and.resolveTo(userInfo as UserInfo); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo as UserInfo); component.ngOnInit(); tick(); @@ -78,7 +87,8 @@ describe('Admin Navbar component', () => { expect(component.profilePicturePngDataUrl).toBe(userProfilePngImage); expect(component.profilePictureWebpDataUrl).toBe(userProfileWebpImage); expect(component.getStaticImageUrl(imagePath)).toBe( - '/assets/images/path/to/image.png'); + '/assets/images/path/to/image.png' + ); expect(component.username).toBe('username1'); expect(component.isModerator).toBe(true); expect(component.isSuperAdmin).toBe(true); @@ -92,10 +102,9 @@ describe('Admin Navbar component', () => { let userInfo = { isModerator: () => true, getUsername: () => null, - isSuperAdmin: () => true + isSuperAdmin: () => true, }; - spyOn(userService, 'getUserInfoAsync') - .and.resolveTo(userInfo as UserInfo); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo as UserInfo); expect(() => { component.ngOnInit(); @@ -104,8 +113,7 @@ describe('Admin Navbar component', () => { })); it('should be routed to the activities tab by default', fakeAsync(() => { - spyOn(userService, 'getUserInfoAsync') - .and.resolveTo(userInfo as UserInfo); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo as UserInfo); component.ngOnInit(); tick(); @@ -118,8 +126,7 @@ describe('Admin Navbar component', () => { })); it('should be routed to the config tab', fakeAsync(() => { - spyOn(userService, 'getUserInfoAsync') - .and.resolveTo(userInfo as UserInfo); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo as UserInfo); component.ngOnInit(); tick(); @@ -140,8 +147,7 @@ describe('Admin Navbar component', () => { })); it('should be routed to the platform params tab', fakeAsync(() => { - spyOn(userService, 'getUserInfoAsync') - .and.resolveTo(userInfo as UserInfo); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo as UserInfo); component.ngOnInit(); tick(); @@ -162,8 +168,7 @@ describe('Admin Navbar component', () => { })); it('should be routed to the roles tab', fakeAsync(() => { - spyOn(userService, 'getUserInfoAsync') - .and.resolveTo(userInfo as UserInfo); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo as UserInfo); component.ngOnInit(); tick(); @@ -184,8 +189,7 @@ describe('Admin Navbar component', () => { })); it('should be routed to the misc tab', fakeAsync(() => { - spyOn(userService, 'getUserInfoAsync') - .and.resolveTo(userInfo as UserInfo); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo as UserInfo); component.ngOnInit(); tick(); @@ -206,8 +210,7 @@ describe('Admin Navbar component', () => { })); it('should set profileDropdownIsActive to true', fakeAsync(() => { - spyOn(userService, 'getUserInfoAsync') - .and.resolveTo(userInfo as UserInfo); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo as UserInfo); component.ngOnInit(); tick(); @@ -220,8 +223,7 @@ describe('Admin Navbar component', () => { })); it('should set profileDropdownIsActive to false', fakeAsync(() => { - spyOn(userService, 'getUserInfoAsync') - .and.resolveTo(userInfo as UserInfo); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo as UserInfo); component.ngOnInit(); tick(); @@ -236,8 +238,7 @@ describe('Admin Navbar component', () => { })); it('should set dropdownMenuIsActive to true', fakeAsync(() => { - spyOn(userService, 'getUserInfoAsync') - .and.resolveTo(userInfo as UserInfo); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo as UserInfo); component.ngOnInit(); tick(); @@ -250,8 +251,7 @@ describe('Admin Navbar component', () => { })); it('should set dropdownMenuIsActive to false', fakeAsync(() => { - spyOn(userService, 'getUserInfoAsync') - .and.resolveTo(userInfo as UserInfo); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo as UserInfo); component.ngOnInit(); tick(); diff --git a/core/templates/pages/admin-page/navbar/admin-navbar.component.ts b/core/templates/pages/admin-page/navbar/admin-navbar.component.ts index 685f6c8ed3a6..d69412da45fb 100644 --- a/core/templates/pages/admin-page/navbar/admin-navbar.component.ts +++ b/core/templates/pages/admin-page/navbar/admin-navbar.component.ts @@ -16,13 +16,13 @@ * @fileoverview Component for the navigation bar in the admin panel. */ -import { Component, OnInit } from '@angular/core'; +import {Component, OnInit} from '@angular/core'; -import { AdminRouterService } from 'pages/admin-page/services/admin-router.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { UserService } from 'services/user.service'; -import { AdminPageConstants } from 'pages/admin-page/admin-page.constants'; -import { AppConstants } from 'app.constants'; +import {AdminRouterService} from 'pages/admin-page/services/admin-router.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {UserService} from 'services/user.service'; +import {AdminPageConstants} from 'pages/admin-page/admin-page.constants'; +import {AppConstants} from 'app.constants'; @Component({ selector: 'oppia-admin-navbar', @@ -44,13 +44,12 @@ export class AdminNavbarComponent implements OnInit { logoutUrl = '/' + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LOGOUT.ROUTE; profileDropdownIsActive = false; dropdownMenuIsActive = false; - PAGES_REGISTERED_WITH_FRONTEND = ( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND); + PAGES_REGISTERED_WITH_FRONTEND = AppConstants.PAGES_REGISTERED_WITH_FRONTEND; constructor( private adminRouterService: AdminRouterService, private urlInterpolationService: UrlInterpolationService, - private userService: UserService, + private userService: UserService ) {} getStaticImageUrl(imagePath: string): string { @@ -78,19 +77,19 @@ export class AdminNavbarComponent implements OnInit { } activateProfileDropdown(): boolean { - return this.profileDropdownIsActive = true; + return (this.profileDropdownIsActive = true); } deactivateProfileDropdown(): boolean { - return this.profileDropdownIsActive = false; + return (this.profileDropdownIsActive = false); } activateDropdownMenu(): boolean { - return this.dropdownMenuIsActive = true; + return (this.dropdownMenuIsActive = true); } deactivateDropdownMenu(): boolean { - return this.dropdownMenuIsActive = false; + return (this.dropdownMenuIsActive = false); } async getUserInfoAsync(): Promise { @@ -103,14 +102,14 @@ export class AdminNavbarComponent implements OnInit { this.isModerator = userInfo.isModerator(); this.isSuperAdmin = userInfo.isSuperAdmin(); - this.profileUrl = ( - this.urlInterpolationService.interpolateUrl( - AdminPageConstants.PROFILE_URL_TEMPLATE, { - username: this.username - }) + this.profileUrl = this.urlInterpolationService.interpolateUrl( + AdminPageConstants.PROFILE_URL_TEMPLATE, + { + username: this.username, + } ); - [this.profilePicturePngDataUrl, this.profilePictureWebpDataUrl] = ( - this.userService.getProfileImageDataUrl(this.username)); + [this.profilePicturePngDataUrl, this.profilePictureWebpDataUrl] = + this.userService.getProfileImageDataUrl(this.username); } ngOnInit(): void { diff --git a/core/templates/pages/admin-page/platform-parameters-tab/admin-platform-parameters-tab.component.spec.ts b/core/templates/pages/admin-page/platform-parameters-tab/admin-platform-parameters-tab.component.spec.ts index 19140d547007..e3899d532067 100644 --- a/core/templates/pages/admin-page/platform-parameters-tab/admin-platform-parameters-tab.component.spec.ts +++ b/core/templates/pages/admin-page/platform-parameters-tab/admin-platform-parameters-tab.component.spec.ts @@ -16,29 +16,32 @@ * @fileoverview Tests for Admin Platform Parameters tab component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, async, TestBed, flushMicrotasks, tick } from - '@angular/core/testing'; -import { FormsModule } from '@angular/forms'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + async, + TestBed, + flushMicrotasks, + tick, +} from '@angular/core/testing'; +import {FormsModule} from '@angular/forms'; import cloneDeep from 'lodash/cloneDeep'; -import { AdminPageData } from 'domain/admin/admin-backend-api.service'; -import { AdminDataService } from 'pages/admin-page/services/admin-data.service'; -import { AdminTaskManagerService } from - 'pages/admin-page/services/admin-task-manager.service'; -import { AdminFeaturesTabConstants } from - 'pages/release-coordinator-page/features-tab/features-tab.constants'; -import { PlatformParameterAdminBackendApiService } from - 'domain/platform-parameter/platform-parameter-admin-backend-api.service'; -import { AdminPlatformParametersTabComponent } from +import {AdminPageData} from 'domain/admin/admin-backend-api.service'; +import {AdminDataService} from 'pages/admin-page/services/admin-data.service'; +import {AdminTaskManagerService} from 'pages/admin-page/services/admin-task-manager.service'; +import {AdminFeaturesTabConstants} from 'pages/release-coordinator-page/features-tab/features-tab.constants'; +import {PlatformParameterAdminBackendApiService} from 'domain/platform-parameter/platform-parameter-admin-backend-api.service'; +import { + AdminPlatformParametersTabComponent, // eslint-disable-next-line max-len - 'pages/admin-page/platform-parameters-tab/admin-platform-parameters-tab.component'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { PlatformParameterFilterType } from - 'domain/platform-parameter/platform-parameter-filter.model'; -import { PlatformParameter } from 'domain/platform-parameter/platform-parameter.model'; -import { HttpErrorResponse } from '@angular/common/http'; +} from 'pages/admin-page/platform-parameters-tab/admin-platform-parameters-tab.component'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {PlatformParameterFilterType} from 'domain/platform-parameter/platform-parameter-filter.model'; +import {PlatformParameter} from 'domain/platform-parameter/platform-parameter.model'; +import {HttpErrorResponse} from '@angular/common/http'; class MockWindowRef { nativeWindow = { @@ -50,11 +53,10 @@ class MockWindowRef { }, prompt() { return 'mock msg'; - } + }, }; } - describe('Admin page platform parameters tab', () => { let component: AdminPlatformParametersTabComponent; let fixture: ComponentFixture; @@ -74,10 +76,10 @@ describe('Admin page platform parameters tab', () => { AdminTaskManagerService, { provide: WindowRef, - useValue: mockWindowRef - } + useValue: mockWindowRef, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(AdminPlatformParametersTabComponent); @@ -94,24 +96,28 @@ describe('Admin page platform parameters tab', () => { description: 'This is a dummy platform parameter.', name: 'dummy_platform_parameter', rule_schema_version: 1, - rules: [{ - filters: [ - { - type: PlatformParameterFilterType.PlatformType, - conditions: [['=', 'Web']] - } - ], - // This does not match the data type of platform param, but this is - // intended as string values are more suitable for - // identifying rules in the following tests. - value_when_matched: 'original', - }], - }) - ] + rules: [ + { + filters: [ + { + type: PlatformParameterFilterType.PlatformType, + conditions: [['=', 'Web']], + }, + ], + // This does not match the data type of platform param, but this is + // intended as string values are more suitable for + // identifying rules in the following tests. + value_when_matched: 'original', + }, + ], + }), + ], } as AdminPageData); - updateApiSpy = spyOn(parameterApiService, 'updatePlatformParameter') - .and.resolveTo(); + updateApiSpy = spyOn( + parameterApiService, + 'updatePlatformParameter' + ).and.resolveTo(); component.ngOnInit(); })); @@ -119,56 +125,77 @@ describe('Admin page platform parameters tab', () => { it('should load platform parameters on init', () => { expect(component.platformParameters.length).toBe(1); expect(component.platformParameters[0].name).toEqual( - 'dummy_platform_parameter'); + 'dummy_platform_parameter' + ); }); describe('.getPlatformParamSchema', () => { it('should return unicode schema for string data type', () => { const schema = component.getPlatformParamSchema( - 'string', component.platformParameters[0].name); + 'string', + component.platformParameters[0].name + ); expect(schema).toEqual({type: 'unicode'}); }); - it('should return correct unicode schema for email_footer ' + - 'platform parameter', () => { - const schema = component.getPlatformParamSchema( - 'string', 'email_footer'); - expect(schema).toEqual( - {type: 'unicode', ui_config: {rows: 5}}); - }); + it( + 'should return correct unicode schema for email_footer ' + + 'platform parameter', + () => { + const schema = component.getPlatformParamSchema( + 'string', + 'email_footer' + ); + expect(schema).toEqual({type: 'unicode', ui_config: {rows: 5}}); + } + ); - it('should return correct unicode schema for signup_email_body_content ' + - 'platform parameter', () => { - const schema = component.getPlatformParamSchema( - 'string', 'signup_email_body_content'); - expect(schema).toEqual( - {type: 'unicode', ui_config: {rows: 20}}); - }); + it( + 'should return correct unicode schema for signup_email_body_content ' + + 'platform parameter', + () => { + const schema = component.getPlatformParamSchema( + 'string', + 'signup_email_body_content' + ); + expect(schema).toEqual({type: 'unicode', ui_config: {rows: 20}}); + } + ); - it('should return correct unicode schema for unpublish_exploration_email' + - '_html_body platform parameter', () => { - const schema = component.getPlatformParamSchema( - 'string', 'unpublish_exploration_email_html_body'); - expect(schema).toEqual( - {type: 'unicode', ui_config: {rows: 20}}); - }); + it( + 'should return correct unicode schema for unpublish_exploration_email' + + '_html_body platform parameter', + () => { + const schema = component.getPlatformParamSchema( + 'string', + 'unpublish_exploration_email_html_body' + ); + expect(schema).toEqual({type: 'unicode', ui_config: {rows: 20}}); + } + ); it('should return float schema for number data type', () => { const schema = component.getPlatformParamSchema( - 'number', component.platformParameters[0].name); + 'number', + component.platformParameters[0].name + ); expect(schema).toEqual({type: 'float'}); }); it('should return bool schema for bool data type', () => { const schema = component.getPlatformParamSchema( - 'bool', component.platformParameters[0].name); + 'bool', + component.platformParameters[0].name + ); expect(schema).toEqual({type: 'bool'}); }); it('should raise error for unknown schema', () => { expect(() => { component.getPlatformParamSchema( - 'float', component.platformParameters[0].name); + 'float', + component.platformParameters[0].name + ); }).toThrowError(); }); }); @@ -249,10 +276,12 @@ describe('Admin page platform parameters tab', () => { // Original filter list: ['platform_type'] // Verifies it's ['platform_type', 'app_version'] after adding a new // filter to the end. - expect(rule.filters[0].type) - .toEqual(PlatformParameterFilterType.PlatformType); - expect(rule.filters[1].type) - .toEqual(PlatformParameterFilterType.AppVersion); + expect(rule.filters[0].type).toEqual( + PlatformParameterFilterType.PlatformType + ); + expect(rule.filters[1].type).toEqual( + PlatformParameterFilterType.AppVersion + ); }); }); @@ -267,8 +296,9 @@ describe('Admin page platform parameters tab', () => { // Original filter list: ['platform_type', 'app_version'] // Verifies it's ['app_version'] after removing the first filter. expect(rule.filters.length).toBe(1); - expect(rule.filters[0].type) - .toEqual(PlatformParameterFilterType.AppVersion); + expect(rule.filters[0].type).toEqual( + PlatformParameterFilterType.AppVersion + ); }); }); @@ -283,10 +313,8 @@ describe('Admin page platform parameters tab', () => { // Original condition list: ['=dev'] // Verifies it's ['=dev', '=mock'] after adding. - expect(filter.conditions[0]) - .toEqual(['=', 'Web']); - expect(filter.conditions[1]) - .toEqual(['=', 'mock']); + expect(filter.conditions[0]).toEqual(['=', 'Web']); + expect(filter.conditions[1]).toEqual(['=', 'mock']); }); }); @@ -317,16 +345,18 @@ describe('Admin page platform parameters tab', () => { }); describe('.clearChanges', () => { - it('should not do anything when no changes are done to platform ' + - 'param', () => { - const platformParameter = component.platformParameters[0]; + it( + 'should not do anything when no changes are done to platform ' + 'param', + () => { + const platformParameter = component.platformParameters[0]; - expect(platformParameter.rules.length).toBe(1); + expect(platformParameter.rules.length).toBe(1); - component.clearChanges(platformParameter); + component.clearChanges(platformParameter); - expect(platformParameter.rules.length).toBe(1); - }); + expect(platformParameter.rules.length).toBe(1); + } + ); it('should clear changes', () => { spyOn(mockWindowRef.nativeWindow, 'confirm').and.returnValue(true); @@ -340,7 +370,7 @@ describe('Admin page platform parameters tab', () => { expect(platformParameter.rules).toEqual(originalRules); }); - it('should not proceed if the user doesn\'t confirm', () => { + it("should not proceed if the user doesn't confirm", () => { spyOn(mockWindowRef.nativeWindow, 'confirm').and.returnValue(false); const platformParameter = component.platformParameters[0]; @@ -356,15 +386,13 @@ describe('Admin page platform parameters tab', () => { describe('.shiftToEditMode', () => { it('should shift to edit mode', () => { const platformParameter = component.platformParameters[0]; - expect( - component.platformParametersInEditMode.get(platformParameter.name) - ).toBeFalse; + expect(component.platformParametersInEditMode.get(platformParameter.name)) + .toBeFalse; component.shiftToEditMode(platformParameter); - expect( - component.platformParametersInEditMode.get(platformParameter.name) - ).toBeTrue; + expect(component.platformParametersInEditMode.get(platformParameter.name)) + .toBeTrue; }); }); @@ -372,37 +400,41 @@ describe('Admin page platform parameters tab', () => { it('should shift to read mode', () => { const platformParameter = component.platformParameters[0]; component.shiftToEditMode(platformParameter); - expect( - component.platformParametersInEditMode.get(platformParameter.name) - ).toBeTrue; + expect(component.platformParametersInEditMode.get(platformParameter.name)) + .toBeTrue; component.shiftToReadMode(platformParameter); - expect( - component.platformParametersInEditMode.get(platformParameter.name) - ).toBeFalse; + expect(component.platformParametersInEditMode.get(platformParameter.name)) + .toBeFalse; }); }); describe('.getReadonlyFilterValues', () => { - it('should get read only value of the rule with one filter and one ' + - 'condition', () => { - const platformParameter = component.platformParameters[0]; + it( + 'should get read only value of the rule with one filter and one ' + + 'condition', + () => { + const platformParameter = component.platformParameters[0]; - expect( - component.getReadonlyFilterValues(platformParameter.rules[0]) - ).toBe('Platform Type in [Web]'); - }); + expect( + component.getReadonlyFilterValues(platformParameter.rules[0]) + ).toBe('Platform Type in [Web]'); + } + ); - it('should get read only value of the rule with one filter and multiple ' + - 'condition', () => { - const platformParameter = component.platformParameters[0]; - component.addNewCondition(platformParameter.rules[0].filters[0]); + it( + 'should get read only value of the rule with one filter and multiple ' + + 'condition', + () => { + const platformParameter = component.platformParameters[0]; + component.addNewCondition(platformParameter.rules[0].filters[0]); - expect( - component.getReadonlyFilterValues(platformParameter.rules[0]) - ).toBe('Platform Type in [Web, Web]'); - }); + expect( + component.getReadonlyFilterValues(platformParameter.rules[0]) + ).toBe('Platform Type in [Web, Web]'); + } + ); it('should get read only value of the rule with multiple filter', () => { const platformParameter = component.platformParameters[0]; @@ -433,7 +465,8 @@ describe('Admin page platform parameters tab', () => { expect(warnings.length).toEqual(1); expect(warnings[0]).toEqual( - 'In rule 1, there should be at least one filter.'); + 'In rule 1, there should be at least one filter.' + ); }); }); @@ -456,7 +489,7 @@ describe('Admin page platform parameters tab', () => { expect(updateApiSpy).toHaveBeenCalled(); })); - it('should not proceed if the user doesn\'t confirm', fakeAsync(() => { + it("should not proceed if the user doesn't confirm", fakeAsync(() => { spyOn(mockWindowRef.nativeWindow, 'confirm').and.returnValue(false); component.saveDefaultValueToStorage(); @@ -486,59 +519,63 @@ describe('Admin page platform parameters tab', () => { component.addNewRuleToBottom(platformParameter); platformParameter.rules[1].filters[0].conditions = [ - ['=', AdminFeaturesTabConstants.ALLOWED_PLATFORM_TYPES[1]] + ['=', AdminFeaturesTabConstants.ALLOWED_PLATFORM_TYPES[1]], ]; component.updateParameterRulesAsync(platformParameter); flushMicrotasks(); expect(updateApiSpy).toHaveBeenCalledWith( - platformParameter.name, 'mock msg', platformParameter.rules, false); + platformParameter.name, + 'mock msg', + platformParameter.rules, + false + ); expect(setStatusSpy).toHaveBeenCalledWith('Saved successfully.'); })); - it('should update platform param backup after update succeeds', - fakeAsync(() => { - promptSpy.and.returnValue('mock msg'); + it('should update platform param backup after update succeeds', fakeAsync(() => { + promptSpy.and.returnValue('mock msg'); - const platformParameter = component.platformParameters[0]; + const platformParameter = component.platformParameters[0]; - component.addNewRuleToBottom(platformParameter); - platformParameter.rules[1].filters[0].conditions = [ - ['=', AdminFeaturesTabConstants.ALLOWED_PLATFORM_TYPES[1]] - ]; - component.updateParameterRulesAsync(platformParameter); + component.addNewRuleToBottom(platformParameter); + platformParameter.rules[1].filters[0].conditions = [ + ['=', AdminFeaturesTabConstants.ALLOWED_PLATFORM_TYPES[1]], + ]; + component.updateParameterRulesAsync(platformParameter); - flushMicrotasks(); + flushMicrotasks(); - expect(component.platformParameterNameToBackupMap.get( - platformParameter.name)).toEqual(platformParameter); - })); + expect( + component.platformParameterNameToBackupMap.get(platformParameter.name) + ).toEqual(platformParameter); + })); - it('should not update platform param backup if update fails', - fakeAsync(() => { - promptSpy.and.returnValue('mock msg'); - const errorResponse = new HttpErrorResponse({ - error: 'Error loading exploration 1.', - status: 500, - statusText: 'Internal Server Error' - }); - updateApiSpy.and.rejectWith(errorResponse); + it('should not update platform param backup if update fails', fakeAsync(() => { + promptSpy.and.returnValue('mock msg'); + const errorResponse = new HttpErrorResponse({ + error: 'Error loading exploration 1.', + status: 500, + statusText: 'Internal Server Error', + }); + updateApiSpy.and.rejectWith(errorResponse); - const platformParameter = component.platformParameters[0]; - const originalFeatureFlag = cloneDeep(platformParameter); + const platformParameter = component.platformParameters[0]; + const originalFeatureFlag = cloneDeep(platformParameter); - component.addNewRuleToBottom(platformParameter); - platformParameter.rules[1].filters[0].conditions = [ - ['=', AdminFeaturesTabConstants.ALLOWED_PLATFORM_TYPES[1]] - ]; - component.updateParameterRulesAsync(platformParameter); + component.addNewRuleToBottom(platformParameter); + platformParameter.rules[1].filters[0].conditions = [ + ['=', AdminFeaturesTabConstants.ALLOWED_PLATFORM_TYPES[1]], + ]; + component.updateParameterRulesAsync(platformParameter); - flushMicrotasks(); + flushMicrotasks(); - expect(component.platformParameterNameToBackupMap.get( - platformParameter.name)).toEqual(originalFeatureFlag); - })); + expect( + component.platformParameterNameToBackupMap.get(platformParameter.name) + ).toEqual(originalFeatureFlag); + })); it('should not proceed if there is another task running', fakeAsync(() => { promptSpy.and.returnValue('mock msg'); @@ -549,7 +586,7 @@ describe('Admin page platform parameters tab', () => { component.addNewRuleToBottom(platformParameter); platformParameter.rules[1].filters[0].conditions = [ - ['=', AdminFeaturesTabConstants.ALLOWED_PLATFORM_TYPES[1]] + ['=', AdminFeaturesTabConstants.ALLOWED_PLATFORM_TYPES[1]], ]; component.updateParameterRulesAsync(platformParameter); @@ -565,24 +602,22 @@ describe('Admin page platform parameters tab', () => { adminTaskManagerService.finishTask(); })); - it('should not proceed if the user cancels the prompt', fakeAsync( - () => { - promptSpy.and.returnValue(null); + it('should not proceed if the user cancels the prompt', fakeAsync(() => { + promptSpy.and.returnValue(null); - const platformParameter = component.platformParameters[0]; + const platformParameter = component.platformParameters[0]; - component.addNewRuleToBottom(platformParameter); - platformParameter.rules[1].filters[0].conditions = [ - ['=', AdminFeaturesTabConstants.ALLOWED_PLATFORM_TYPES[1]] - ]; - component.updateParameterRulesAsync(platformParameter); + component.addNewRuleToBottom(platformParameter); + platformParameter.rules[1].filters[0].conditions = [ + ['=', AdminFeaturesTabConstants.ALLOWED_PLATFORM_TYPES[1]], + ]; + component.updateParameterRulesAsync(platformParameter); - flushMicrotasks(); + flushMicrotasks(); - expect(updateApiSpy).not.toHaveBeenCalled(); - expect(setStatusSpy).not.toHaveBeenCalled(); - }) - ); + expect(updateApiSpy).not.toHaveBeenCalled(); + expect(setStatusSpy).not.toHaveBeenCalled(); + })); it('should not proceed if there is any validation issue', fakeAsync(() => { promptSpy.and.returnValue(null); @@ -606,14 +641,14 @@ describe('Admin page platform parameters tab', () => { const errorResponse = new HttpErrorResponse({ error: 'Error loading exploration 1.', status: 500, - statusText: 'Internal Server Error' + statusText: 'Internal Server Error', }); updateApiSpy.and.rejectWith(errorResponse); const platformParameter = component.platformParameters[0]; component.addNewRuleToBottom(platformParameter); platformParameter.rules[1].filters[0].conditions = [ - ['=', AdminFeaturesTabConstants.ALLOWED_PLATFORM_TYPES[1]] + ['=', AdminFeaturesTabConstants.ALLOWED_PLATFORM_TYPES[1]], ]; component.updateParameterRulesAsync(platformParameter); @@ -628,17 +663,17 @@ describe('Admin page platform parameters tab', () => { const errorResponse = new HttpErrorResponse({ error: { - error: 'validation error.' + error: 'validation error.', }, status: 500, - statusText: 'Internal Server Error' + statusText: 'Internal Server Error', }); updateApiSpy.and.rejectWith(errorResponse); const platformParameter = component.platformParameters[0]; component.addNewRuleToBottom(platformParameter); platformParameter.rules[1].filters[0].conditions = [ - ['=', AdminFeaturesTabConstants.ALLOWED_PLATFORM_TYPES[1]] + ['=', AdminFeaturesTabConstants.ALLOWED_PLATFORM_TYPES[1]], ]; component.updateParameterRulesAsync(platformParameter); @@ -646,7 +681,8 @@ describe('Admin page platform parameters tab', () => { expect(updateApiSpy).toHaveBeenCalled(); expect(setStatusSpy).toHaveBeenCalledWith( - 'Update failed: validation error.'); + 'Update failed: validation error.' + ); })); it('should throw error if error resonse is unexpected', fakeAsync(() => { @@ -663,26 +699,19 @@ describe('Admin page platform parameters tab', () => { }); describe('.isPlatformParamChanged', () => { - it('should return false if the parameter is same as the backup instance', - () => { - const platformParameter = component.platformParameters[0]; + it('should return false if the parameter is same as the backup instance', () => { + const platformParameter = component.platformParameters[0]; - expect(component.isPlatformParamChanged(platformParameter)) - .toBeFalse(); - } - ); + expect(component.isPlatformParamChanged(platformParameter)).toBeFalse(); + }); - it( - 'should return true if the parameter is different from backup instance', - () => { - const platformParameter = component.platformParameters[0]; + it('should return true if the parameter is different from backup instance', () => { + const platformParameter = component.platformParameters[0]; - component.addNewRuleToBottom(platformParameter); + component.addNewRuleToBottom(platformParameter); - expect(component.isPlatformParamChanged(platformParameter)) - .toBeTrue(); - } - ); + expect(component.isPlatformParamChanged(platformParameter)).toBeTrue(); + }); it('should return true when default value is changed', () => { const platformParameter = component.platformParameters[0]; @@ -703,15 +732,15 @@ describe('Admin page platform parameters tab', () => { filters: [ { type: PlatformParameterFilterType.PlatformType, - conditions: [['=', 'Web']] - } + conditions: [['=', 'Web']], + }, ], value_when_matched: true, }, { filters: [], - value_when_matched: true - } + value_when_matched: true, + }, ], }); @@ -736,9 +765,9 @@ describe('Admin page platform parameters tab', () => { { type: PlatformParameterFilterType.PlatformType, conditions: [ - ['=', AdminFeaturesTabConstants.ALLOWED_PLATFORM_TYPES[0]] - ] - } + ['=', AdminFeaturesTabConstants.ALLOWED_PLATFORM_TYPES[0]], + ], + }, ], value_when_matched: true, }, @@ -747,12 +776,12 @@ describe('Admin page platform parameters tab', () => { { type: PlatformParameterFilterType.PlatformType, conditions: [ - ['=', AdminFeaturesTabConstants.ALLOWED_PLATFORM_TYPES[1]] - ] - } + ['=', AdminFeaturesTabConstants.ALLOWED_PLATFORM_TYPES[1]], + ], + }, ], - value_when_matched: true - } + value_when_matched: true, + }, ], }) ); @@ -774,22 +803,22 @@ describe('Admin page platform parameters tab', () => { { type: PlatformParameterFilterType.PlatformType, conditions: [ - ['=', AdminFeaturesTabConstants.ALLOWED_PLATFORM_TYPES[1]] - ] - } + ['=', AdminFeaturesTabConstants.ALLOWED_PLATFORM_TYPES[1]], + ], + }, ], - value_when_matched: true + value_when_matched: true, }, { filters: [ { type: PlatformParameterFilterType.PlatformType, conditions: [ - ['=', AdminFeaturesTabConstants.ALLOWED_PLATFORM_TYPES[1]] - ] - } + ['=', AdminFeaturesTabConstants.ALLOWED_PLATFORM_TYPES[1]], + ], + }, ], - value_when_matched: true + value_when_matched: true, }, ], }) @@ -811,21 +840,20 @@ describe('Admin page platform parameters tab', () => { filters: [ { type: PlatformParameterFilterType.PlatformType, - conditions: [['=', 'Web']] + conditions: [['=', 'Web']], }, { type: PlatformParameterFilterType.PlatformType, - conditions: [['=', 'Web']] - } + conditions: [['=', 'Web']], + }, ], - value_when_matched: true + value_when_matched: true, }, ], }) ); - expect(issues).toEqual([ - 'In rule 1, filters 1 & 2 are identical.']); + expect(issues).toEqual(['In rule 1, filters 1 & 2 are identical.']); }); it('should return issues if there are identical conditions', () => { @@ -841,17 +869,21 @@ describe('Admin page platform parameters tab', () => { filters: [ { type: PlatformParameterFilterType.PlatformType, - conditions: [['=', 'Web'], ['=', 'Web']] + conditions: [ + ['=', 'Web'], + ['=', 'Web'], + ], }, ], - value_when_matched: true + value_when_matched: true, }, ], }) ); expect(issues).toEqual([ - 'In rule 1, filter 1, conditions 1 & 2 are identical.']); + 'In rule 1, filter 1, conditions 1 & 2 are identical.', + ]); }); it('should return issues if filter has no condition', () => { @@ -867,17 +899,18 @@ describe('Admin page platform parameters tab', () => { filters: [ { type: PlatformParameterFilterType.PlatformType, - conditions: [] + conditions: [], }, ], - value_when_matched: true + value_when_matched: true, }, ], }) ); - expect(issues).toEqual( - ['In rule 1, filter 1 should have at least one condition.']); + expect(issues).toEqual([ + 'In rule 1, filter 1 should have at least one condition.', + ]); }); it('should return issues if there is no filter in the rule', () => { @@ -891,14 +924,15 @@ describe('Admin page platform parameters tab', () => { rules: [ { filters: [], - value_when_matched: true + value_when_matched: true, }, ], }) ); - expect(issues).toEqual( - ['In rule 1, there should be at least one filter.']); + expect(issues).toEqual([ + 'In rule 1, there should be at least one filter.', + ]); }); it('should return issues if the app version condition is empty', () => { @@ -914,17 +948,18 @@ describe('Admin page platform parameters tab', () => { filters: [ { type: PlatformParameterFilterType.AppVersion, - conditions: [['=', '']] + conditions: [['=', '']], }, ], - value_when_matched: true + value_when_matched: true, }, ], }) ); - expect(issues).toEqual( - ['In rule 1, filter 1, condition 1, the app version is empty.']); + expect(issues).toEqual([ + 'In rule 1, filter 1, condition 1, the app version is empty.', + ]); }); }); }); diff --git a/core/templates/pages/admin-page/platform-parameters-tab/admin-platform-parameters-tab.component.ts b/core/templates/pages/admin-page/platform-parameters-tab/admin-platform-parameters-tab.component.ts index 94656d1e01f8..d6aad7ae41b1 100644 --- a/core/templates/pages/admin-page/platform-parameters-tab/admin-platform-parameters-tab.component.ts +++ b/core/templates/pages/admin-page/platform-parameters-tab/admin-platform-parameters-tab.component.ts @@ -16,52 +16,46 @@ * @fileoverview Component for the Platform Parameters tab in the admin panel. */ -import { Component, OnInit, EventEmitter, Output } from '@angular/core'; +import {Component, OnInit, EventEmitter, Output} from '@angular/core'; import cloneDeep from 'lodash/cloneDeep'; import isEqual from 'lodash/isEqual'; -import { Subscription } from 'rxjs'; - -import { AdminFeaturesTabConstants } from - 'pages/release-coordinator-page/features-tab/features-tab.constants'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { AdminDataService } from - 'pages/admin-page/services/admin-data.service'; -import { AdminTaskManagerService } from - 'pages/admin-page/services/admin-task-manager.service'; -import { LoaderService } from 'services/loader.service'; -import { PlatformParameterAdminBackendApiService } from - 'domain/platform-parameter/platform-parameter-admin-backend-api.service'; +import {Subscription} from 'rxjs'; + +import {AdminFeaturesTabConstants} from 'pages/release-coordinator-page/features-tab/features-tab.constants'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {AdminDataService} from 'pages/admin-page/services/admin-data.service'; +import {AdminTaskManagerService} from 'pages/admin-page/services/admin-task-manager.service'; +import {LoaderService} from 'services/loader.service'; +import {PlatformParameterAdminBackendApiService} from 'domain/platform-parameter/platform-parameter-admin-backend-api.service'; import { PlatformParameterFilterType, PlatformParameterFilter, } from 'domain/platform-parameter/platform-parameter-filter.model'; -import { PlatformParameter } from - 'domain/platform-parameter/platform-parameter.model'; -import { PlatformParameterRule } from - 'domain/platform-parameter/platform-parameter-rule.model'; -import { HttpErrorResponse } from '@angular/common/http'; +import {PlatformParameter} from 'domain/platform-parameter/platform-parameter.model'; +import {PlatformParameterRule} from 'domain/platform-parameter/platform-parameter-rule.model'; +import {HttpErrorResponse} from '@angular/common/http'; interface PlatformSchema { - 'type': string; - 'ui_config'?: { 'rows': number }; + type: string; + ui_config?: {rows: number}; } type FilterType = keyof typeof PlatformParameterFilterType; @Component({ selector: 'oppia-admin-platform-parameters-tab', - templateUrl: './admin-platform-parameters-tab.component.html' + templateUrl: './admin-platform-parameters-tab.component.html', }) export class AdminPlatformParametersTabComponent implements OnInit { @Output() setStatusMessage = new EventEmitter(); - readonly availableFilterTypes: PlatformParameterFilterType[] = Object - .keys(PlatformParameterFilterType) - .map(key => { - var filterType = key as FilterType; - return PlatformParameterFilterType[filterType]; - }); + readonly availableFilterTypes: PlatformParameterFilterType[] = Object.keys( + PlatformParameterFilterType + ).map(key => { + var filterType = key as FilterType; + return PlatformParameterFilterType[filterType]; + }); readonly filterTypeToContext: { [key in PlatformParameterFilterType]: { @@ -70,32 +64,31 @@ export class AdminPlatformParametersTabComponent implements OnInit { options?: readonly string[]; placeholder?: string; inputRegex?: RegExp; - } - } = { - [PlatformParameterFilterType.PlatformType]: { - displayName: 'Platform Type', - options: AdminFeaturesTabConstants.ALLOWED_PLATFORM_TYPES, - operators: ['='] - }, - [PlatformParameterFilterType.AppVersion]: { - displayName: 'App Version', - operators: ['=', '<', '>', '<=', '>='], - placeholder: 'e.g. 1.0.0', - inputRegex: AdminFeaturesTabConstants.APP_VERSION_REGEXP - }, - [PlatformParameterFilterType.AppVersionFlavor]: { - displayName: 'App Version Flavor', - options: AdminFeaturesTabConstants.ALLOWED_APP_VERSION_FLAVORS, - operators: ['=', '<', '>', '<=', '>='] - } }; - - private readonly defaultNewFilter: PlatformParameterFilter = ( + } = { + [PlatformParameterFilterType.PlatformType]: { + displayName: 'Platform Type', + options: AdminFeaturesTabConstants.ALLOWED_PLATFORM_TYPES, + operators: ['='], + }, + [PlatformParameterFilterType.AppVersion]: { + displayName: 'App Version', + operators: ['=', '<', '>', '<=', '>='], + placeholder: 'e.g. 1.0.0', + inputRegex: AdminFeaturesTabConstants.APP_VERSION_REGEXP, + }, + [PlatformParameterFilterType.AppVersionFlavor]: { + displayName: 'App Version Flavor', + options: AdminFeaturesTabConstants.ALLOWED_APP_VERSION_FLAVORS, + operators: ['=', '<', '>', '<=', '>='], + }, + }; + + private readonly defaultNewFilter: PlatformParameterFilter = PlatformParameterFilter.createFromBackendDict({ type: PlatformParameterFilterType.PlatformType, - conditions: [] - }) - ); + conditions: [], + }); // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see @@ -114,17 +107,19 @@ export class AdminPlatformParametersTabComponent implements OnInit { private adminDataService: AdminDataService, private adminTaskManager: AdminTaskManagerService, private apiService: PlatformParameterAdminBackendApiService, - private loaderService: LoaderService, - ) { } + private loaderService: LoaderService + ) {} async reloadPlatformParametersAsync(): Promise { const data = await this.adminDataService.getDataAsync(); this.platformParametersAreFetched = false; this.platformParameters = data.platformParameters; this.platformParameterNameToBackupMap = new Map( - this.platformParameters.map(param => [param.name, cloneDeep(param)])); + this.platformParameters.map(param => [param.name, cloneDeep(param)]) + ); this.platformParametersInEditMode = new Map( - this.platformParameters.map(param => [param.name, false])); + this.platformParameters.map(param => [param.name, false]) + ); for (const platformParameter of this.platformParameters) { this.updateFilterValuesForDisplay(platformParameter); } @@ -134,7 +129,7 @@ export class AdminPlatformParametersTabComponent implements OnInit { getdefaultNewRule(param: PlatformParameter): PlatformParameterRule { return PlatformParameterRule.createFromBackendDict({ filters: [this.defaultNewFilter.toBackendDict()], - value_when_matched: param.defaultValue + value_when_matched: param.defaultValue, }); } @@ -155,15 +150,16 @@ export class AdminPlatformParametersTabComponent implements OnInit { return {type: 'bool'}; } throw new Error( - 'Unexpected data type value, must be one of string, number or bool.'); + 'Unexpected data type value, must be one of string, number or bool.' + ); } getReadonlyFilterValues(rule: PlatformParameterRule): string { let resultantString: string = ''; const filters: PlatformParameterFilter[] = rule.filters; for (let filterIdx = 0; filterIdx < filters.length; filterIdx++) { - const filterName: string = ( - this.filterTypeToContext[filters[filterIdx].type].displayName); + const filterName: string = + this.filterTypeToContext[filters[filterIdx].type].displayName; if (filters[filterIdx].conditions.length === 0) { resultantString += `${filterName} in [ ]`; } else { @@ -191,7 +187,9 @@ export class AdminPlatformParametersTabComponent implements OnInit { ruleReadOnlyValue.push(this.getReadonlyFilterValues(parameterRule)); } this.platformParameterNameToRulesReadonlyData.set( - platformParameter.name, ruleReadOnlyValue); + platformParameter.name, + ruleReadOnlyValue + ); } addNewRuleToBottom(param: PlatformParameter): void { @@ -207,7 +205,7 @@ export class AdminPlatformParametersTabComponent implements OnInit { const context = this.filterTypeToContext[filter.type]; filter.conditions.push([ context.operators[0], - context.options ? context.options[0] : '' + context.options ? context.options[0] : '', ]); } @@ -220,7 +218,9 @@ export class AdminPlatformParametersTabComponent implements OnInit { } removeCondition( - filter: PlatformParameterFilter, conditionIndex: number): void { + filter: PlatformParameterFilter, + conditionIndex: number + ): void { filter.conditions.splice(conditionIndex, 1); } @@ -247,8 +247,7 @@ export class AdminPlatformParametersTabComponent implements OnInit { } async saveDefaultValueToStorage(): Promise { - if (!this.windowRef.nativeWindow.confirm( - 'This action is irreversible.')) { + if (!this.windowRef.nativeWindow.confirm('This action is irreversible.')) { return; } for (const param of this.platformParameters) { @@ -258,21 +257,27 @@ export class AdminPlatformParametersTabComponent implements OnInit { } async updatePlatformParameter( - param: PlatformParameter, commitMessage: string): Promise { + param: PlatformParameter, + commitMessage: string + ): Promise { try { this.adminTaskManager.startTask(); await this.apiService.updatePlatformParameter( - param.name, commitMessage, param.rules, param.defaultValue); + param.name, + commitMessage, + param.rules, + param.defaultValue + ); this.platformParameterNameToBackupMap.set(param.name, cloneDeep(param)); this.updateFilterValuesForDisplay(param); this.setStatusMessage.emit('Saved successfully.'); - // We use unknown type because we are unsure of the type of error - // that was thrown. Since the catch block cannot identify the - // specific type of error, we are unable to further optimise the - // code by introducing more types of errors. + // We use unknown type because we are unsure of the type of error + // that was thrown. Since the catch block cannot identify the + // specific type of error, we are unable to further optimise the + // code by introducing more types of errors. } catch (e: unknown) { if (e instanceof HttpErrorResponse) { if (e.error && e.error.error) { @@ -289,8 +294,8 @@ export class AdminPlatformParametersTabComponent implements OnInit { } async updateParameterRulesAsync(param: PlatformParameter): Promise { - const issues = ( - AdminPlatformParametersTabComponent.validatePlatformParam(param)); + const issues = + AdminPlatformParametersTabComponent.validatePlatformParam(param); if (issues.length > 0) { this.windowRef.nativeWindow.alert(issues.join('\n')); return; @@ -300,7 +305,7 @@ export class AdminPlatformParametersTabComponent implements OnInit { } const commitMessage = this.windowRef.nativeWindow.prompt( 'This action is irreversible. If you insist to proceed, please enter ' + - 'the commit message for the update', + 'the commit message for the update', `Update parameter '${param.name}'.` ); if (commitMessage === null) { @@ -312,8 +317,11 @@ export class AdminPlatformParametersTabComponent implements OnInit { clearChanges(param: PlatformParameter): void { if (this.isPlatformParamChanged(param)) { - if (!this.windowRef.nativeWindow.confirm( - 'This will revert all changes you made. Are you sure?')) { + if ( + !this.windowRef.nativeWindow.confirm( + 'This will revert all changes you made. Are you sure?' + ) + ) { return; } const backup = this.platformParameterNameToBackupMap.get(param.name); @@ -329,9 +337,7 @@ export class AdminPlatformParametersTabComponent implements OnInit { } isPlatformParamChanged(param: PlatformParameter): boolean { - const original = this.platformParameterNameToBackupMap.get( - param.name - ); + const original = this.platformParameterNameToBackupMap.get(param.name); if (original === undefined) { throw new Error('Backup not found for platform params: ' + param.name); } @@ -354,23 +360,27 @@ export class AdminPlatformParametersTabComponent implements OnInit { const seenRules: PlatformParameterRule[] = []; for (const [ruleIndex, rule] of param.rules.entries()) { - const sameRuleIndex = seenRules.findIndex( - seenRule => isEqual(seenRule, rule)); + const sameRuleIndex = seenRules.findIndex(seenRule => + isEqual(seenRule, rule) + ); if (sameRuleIndex !== -1) { issues.push( - `Rules ${sameRuleIndex + 1} & ${ruleIndex + 1} are identical.`); + `Rules ${sameRuleIndex + 1} & ${ruleIndex + 1} are identical.` + ); continue; } seenRules.push(rule); const seenFilters: PlatformParameterFilter[] = []; for (const [filterIndex, filter] of rule.filters.entries()) { - const sameFilterIndex = seenFilters.findIndex( - seenFilter => isEqual(seenFilter, filter)); + const sameFilterIndex = seenFilters.findIndex(seenFilter => + isEqual(seenFilter, filter) + ); if (sameFilterIndex !== -1) { issues.push( `In rule ${ruleIndex + 1}, filters ${sameFilterIndex + 1}` + - ` & ${filterIndex + 1} are identical.`); + ` & ${filterIndex + 1} are identical.` + ); continue; } seenFilters.push(filter); @@ -378,20 +388,22 @@ export class AdminPlatformParametersTabComponent implements OnInit { if (filter.conditions.length === 0) { issues.push( `In rule ${ruleIndex + 1}, filter ${filterIndex + 1} ` + - 'should have at least one condition.'); + 'should have at least one condition.' + ); continue; } const seenConditions: [string, string][] = []; - for (const [conditionIndex, condition] of filter.conditions - .entries()) { - const sameCondIndex = seenConditions.findIndex( - seenCond => isEqual(seenCond, condition)); + for (const [conditionIndex, condition] of filter.conditions.entries()) { + const sameCondIndex = seenConditions.findIndex(seenCond => + isEqual(seenCond, condition) + ); if (sameCondIndex !== -1) { issues.push( `In rule ${ruleIndex + 1}, filter ${filterIndex + 1},` + - ` conditions ${sameCondIndex + 1} & ` + - `${conditionIndex + 1} are identical.`); + ` conditions ${sameCondIndex + 1} & ` + + `${conditionIndex + 1} are identical.` + ); continue; } @@ -399,7 +411,7 @@ export class AdminPlatformParametersTabComponent implements OnInit { if (condition[1] === '') { issues.push( `In rule ${ruleIndex + 1}, filter ${filterIndex + 1}, ` + - `condition ${conditionIndex + 1}, the app version is empty.` + `condition ${conditionIndex + 1}, the app version is empty.` ); continue; } @@ -411,8 +423,8 @@ export class AdminPlatformParametersTabComponent implements OnInit { if (rule.filters.length === 0) { issues.push( - `In rule ${ruleIndex + 1}, there should be at least ` + - 'one filter.'); + `In rule ${ruleIndex + 1}, there should be at least ` + 'one filter.' + ); continue; } } @@ -429,11 +441,10 @@ export class AdminPlatformParametersTabComponent implements OnInit { ngOnInit(): void { this.directiveSubscriptions.add( - this.loaderService.onLoadingMessageChange.subscribe( - (message: string) => { - this.loadingMessage = message; - } - )); + this.loaderService.onLoadingMessageChange.subscribe((message: string) => { + this.loadingMessage = message; + }) + ); this.platformParametersAreFetched = true; this.loaderService.showLoadingScreen('Loading'); this.reloadPlatformParametersAsync(); diff --git a/core/templates/pages/admin-page/roles-tab/admin-roles-tab.component.spec.ts b/core/templates/pages/admin-page/roles-tab/admin-roles-tab.component.spec.ts index 7a37374ae613..a5334122fa5b 100644 --- a/core/templates/pages/admin-page/roles-tab/admin-roles-tab.component.spec.ts +++ b/core/templates/pages/admin-page/roles-tab/admin-roles-tab.component.spec.ts @@ -16,22 +16,31 @@ * @fileoverview Tests for Admin roles tab component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { FormsModule } from '@angular/forms'; - -import { AdminBackendApiService, AdminPageData, UserRolesBackendResponse } from 'domain/admin/admin-backend-api.service'; -import { CreatorTopicSummary } from 'domain/topic/creator-topic-summary.model'; -import { AdminDataService } from '../services/admin-data.service'; -import { AdminRolesTabComponent} from './admin-roles-tab.component'; -import { AlertsService } from 'services/alerts.service'; - -import { TopicManagerRoleEditorModalComponent } from './topic-manager-role-editor-modal.component'; -import { TranslationCoordinatorRoleEditorModalComponent } from './translation-coordinator-role-editor-modal.component'; - -describe('Admin roles tab component ', function() { +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {FormsModule} from '@angular/forms'; + +import { + AdminBackendApiService, + AdminPageData, + UserRolesBackendResponse, +} from 'domain/admin/admin-backend-api.service'; +import {CreatorTopicSummary} from 'domain/topic/creator-topic-summary.model'; +import {AdminDataService} from '../services/admin-data.service'; +import {AdminRolesTabComponent} from './admin-roles-tab.component'; +import {AlertsService} from 'services/alerts.service'; + +import {TopicManagerRoleEditorModalComponent} from './topic-manager-role-editor-modal.component'; +import {TranslationCoordinatorRoleEditorModalComponent} from './translation-coordinator-role-editor-modal.component'; + +describe('Admin roles tab component ', function () { let component: AdminRolesTabComponent; let fixture: ComponentFixture; @@ -62,52 +71,37 @@ describe('Admin roles tab component ', function() { total_upcoming_chapters_count: 1, total_overdue_chapters_count: 1, total_chapter_counts_for_each_story: [5, 4], - published_chapter_counts_for_each_story: [3, 4] + published_chapter_counts_for_each_story: [3, 4], }; - const sampleTopicSummary: CreatorTopicSummary = ( + const sampleTopicSummary: CreatorTopicSummary = CreatorTopicSummary.createFromBackendDict( - sampleCreatorTopicSummaryBackendDict)); + sampleCreatorTopicSummaryBackendDict + ); const adminPageData: AdminPageData = { demoExplorationIds: ['expId'], - demoExplorations: [ - [ - '0', - 'welcome.yaml' - ] - ], - demoCollections: [ - ['collectionId'] - ], + demoExplorations: [['0', 'welcome.yaml']], + demoCollections: [['collectionId']], updatableRoles: ['MODERATOR'], roleToActions: { - Admin: ['Accept any suggestion', 'Access creator dashboard'] + Admin: ['Accept any suggestion', 'Access creator dashboard'], }, configProperties: {}, viewableRoles: ['MODERATOR', 'TOPIC_MANAGER'], humanReadableRoles: { - FULL_USER: 'full user' + FULL_USER: 'full user', }, - topicSummaries: [ - sampleTopicSummary - ], - platformParameters: [] + topicSummaries: [sampleTopicSummary], + platformParameters: [], }; beforeEach(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - FormsModule - ], + imports: [HttpClientTestingModule, FormsModule], declarations: [AdminRolesTabComponent], - providers: [ - AdminBackendApiService, - AdminDataService, - AlertsService - ], - schemas: [NO_ERRORS_SCHEMA] + providers: [AdminBackendApiService, AdminDataService, AlertsService], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(AdminRolesTabComponent); @@ -120,20 +114,24 @@ describe('Admin roles tab component ', function() { alertsService = TestBed.inject(AlertsService); }); - it('should retrieve data from the backend and ' + - 'set properties when initialized', fakeAsync(() => { - spyOn(adminDataService, 'getDataAsync').and.returnValue( - Promise.resolve(adminPageData)); + it( + 'should retrieve data from the backend and ' + + 'set properties when initialized', + fakeAsync(() => { + spyOn(adminDataService, 'getDataAsync').and.returnValue( + Promise.resolve(adminPageData) + ); - component.ngOnInit(); - tick(); - fixture.detectChanges(); + component.ngOnInit(); + tick(); + fixture.detectChanges(); - expect(component.UPDATABLE_ROLES).toEqual(adminPageData.updatableRoles); - expect(component.roleToActions).toEqual(adminPageData.roleToActions); - expect(component.VIEWABLE_ROLES).toEqual(adminPageData.viewableRoles); - expect(component.topicSummaries).toEqual(adminPageData.topicSummaries); - })); + expect(component.UPDATABLE_ROLES).toEqual(adminPageData.updatableRoles); + expect(component.roleToActions).toEqual(adminPageData.roleToActions); + expect(component.VIEWABLE_ROLES).toEqual(adminPageData.viewableRoles); + expect(component.topicSummaries).toEqual(adminPageData.topicSummaries); + }) + ); it('should flush the value to properties on calling clearEditor', () => { component.userRoles = ['MODERATOR']; @@ -147,15 +145,15 @@ describe('Admin roles tab component ', function() { expect(component.userIsBanned).toEqual(false); }); - describe('on startEditing', function() { + describe('on startEditing', function () { let successPromise: Promise; - beforeEach(function() { + beforeEach(function () { successPromise = Promise.resolve({ roles: ['TOPIC_MANAGER'], managed_topic_ids: ['topic_id_1'], banned: false, - coordinated_language_ids: [] + coordinated_language_ids: [], }); }); @@ -208,25 +206,26 @@ describe('Admin roles tab component ', function() { })); }); - describe('on calling markUserBanned', function() { - it('should enable bannedStatusChangeInProgress until user is banned', - fakeAsync(() => { - spyOn(adminBackendApiService, 'markUserBannedAsync') - .and.returnValue(Promise.resolve()); + describe('on calling markUserBanned', function () { + it('should enable bannedStatusChangeInProgress until user is banned', fakeAsync(() => { + spyOn(adminBackendApiService, 'markUserBannedAsync').and.returnValue( + Promise.resolve() + ); - expect(component.bannedStatusChangeInProgress).toBeFalse(); + expect(component.bannedStatusChangeInProgress).toBeFalse(); - component.markUserBanned(); - expect(component.bannedStatusChangeInProgress).toBeTrue(); + component.markUserBanned(); + expect(component.bannedStatusChangeInProgress).toBeTrue(); - tick(); + tick(); - expect(component.bannedStatusChangeInProgress).toBeFalse(); - })); + expect(component.bannedStatusChangeInProgress).toBeFalse(); + })); it('should set userIsBanned to true', fakeAsync(() => { - spyOn(adminBackendApiService, 'markUserBannedAsync') - .and.returnValue(Promise.resolve()); + spyOn(adminBackendApiService, 'markUserBannedAsync').and.returnValue( + Promise.resolve() + ); expect(component.userIsBanned).toBeFalse(); @@ -238,8 +237,9 @@ describe('Admin roles tab component ', function() { it('should set alert warning on failed request', fakeAsync(() => { spyOn(alertsService, 'addWarning'); - spyOn(adminBackendApiService, 'markUserBannedAsync') - .and.returnValue(Promise.reject('Failed!')); + spyOn(adminBackendApiService, 'markUserBannedAsync').and.returnValue( + Promise.reject('Failed!') + ); component.markUserBanned(); tick(); @@ -248,23 +248,23 @@ describe('Admin roles tab component ', function() { })); }); - describe('on calling unmarkUserBanned', function() { - beforeEach(function() { - spyOn(adminBackendApiService, 'unmarkUserBannedAsync') - .and.returnValue(Promise.resolve()); + describe('on calling unmarkUserBanned', function () { + beforeEach(function () { + spyOn(adminBackendApiService, 'unmarkUserBannedAsync').and.returnValue( + Promise.resolve() + ); }); - it('should enable bannedStatusChangeInProgress until user is unbanned', - fakeAsync(() => { - expect(component.bannedStatusChangeInProgress).toBeFalse(); + it('should enable bannedStatusChangeInProgress until user is unbanned', fakeAsync(() => { + expect(component.bannedStatusChangeInProgress).toBeFalse(); - component.unmarkUserBanned(); - expect(component.bannedStatusChangeInProgress).toBeTrue(); + component.unmarkUserBanned(); + expect(component.bannedStatusChangeInProgress).toBeTrue(); - tick(); + tick(); - expect(component.bannedStatusChangeInProgress).toBeFalse(); - })); + expect(component.bannedStatusChangeInProgress).toBeFalse(); + })); it('should set userIsBanned to false', fakeAsync(() => { component.userIsBanned = true; @@ -276,63 +276,64 @@ describe('Admin roles tab component ', function() { })); }); - describe('on calling removeRole', function() { - beforeEach(function() { - spyOn(adminBackendApiService, 'removeUserRoleAsync') - .and.returnValue(Promise.resolve()); + describe('on calling removeRole', function () { + beforeEach(function () { + spyOn(adminBackendApiService, 'removeUserRoleAsync').and.returnValue( + Promise.resolve() + ); }); - it('should remove the given role', - fakeAsync(() => { - component.userRoles = ['MODERATOR', 'TOPIC_MANAGER']; - - component.removeRole('MODERATOR'); - tick(); - - expect(component.userRoles).toEqual(['TOPIC_MANAGER']); - })); + it('should remove the given role', fakeAsync(() => { + component.userRoles = ['MODERATOR', 'TOPIC_MANAGER']; - it('should flush managedTopicIds while removing topic manager role', - fakeAsync(() => { - component.userRoles = ['MODERATOR', 'TOPIC_MANAGER']; - component.managedTopicIds = ['topic_1', 'topic_2']; - - component.removeRole('TOPIC_MANAGER'); - tick(); + component.removeRole('MODERATOR'); + tick(); - expect(component.userRoles).toEqual(['MODERATOR']); - expect(component.managedTopicIds).toEqual([]); - })); + expect(component.userRoles).toEqual(['TOPIC_MANAGER']); + })); - it('should flush coordinated_language_ids while removing' + - ' translation coordinator role', - fakeAsync(() => { - component.userRoles = ['MODERATOR', 'TRANSLATION_COORDINATOR']; - component.coordinatedLanguageIds = ['en', 'hi']; + it('should flush managedTopicIds while removing topic manager role', fakeAsync(() => { + component.userRoles = ['MODERATOR', 'TOPIC_MANAGER']; + component.managedTopicIds = ['topic_1', 'topic_2']; - component.removeRole('TRANSLATION_COORDINATOR'); + component.removeRole('TOPIC_MANAGER'); tick(); expect(component.userRoles).toEqual(['MODERATOR']); - expect(component.coordinatedLanguageIds).toEqual([]); + expect(component.managedTopicIds).toEqual([]); })); + + it( + 'should flush coordinated_language_ids while removing' + + ' translation coordinator role', + fakeAsync(() => { + component.userRoles = ['MODERATOR', 'TRANSLATION_COORDINATOR']; + component.coordinatedLanguageIds = ['en', 'hi']; + + component.removeRole('TRANSLATION_COORDINATOR'); + tick(); + + expect(component.userRoles).toEqual(['MODERATOR']); + expect(component.coordinatedLanguageIds).toEqual([]); + }) + ); }); - describe('on calling addNewRole', function() { - beforeEach(function() { - spyOn(adminBackendApiService, 'addUserRoleAsync') - .and.returnValue(Promise.resolve()); + describe('on calling addNewRole', function () { + beforeEach(function () { + spyOn(adminBackendApiService, 'addUserRoleAsync').and.returnValue( + Promise.resolve() + ); }); - it('should add the given role', - fakeAsync(() => { - component.userRoles = ['TOPIC_MANAGER']; + it('should add the given role', fakeAsync(() => { + component.userRoles = ['TOPIC_MANAGER']; - component.addNewRole('MODERATOR'); - tick(); + component.addNewRole('MODERATOR'); + tick(); - expect(component.userRoles).toEqual(['TOPIC_MANAGER', 'MODERATOR']); - })); + expect(component.userRoles).toEqual(['TOPIC_MANAGER', 'MODERATOR']); + })); it('should open topic manager modal on adding topic manager role', () => { spyOn(component, 'openTopicManagerRoleEditor').and.returnValue(); @@ -344,36 +345,40 @@ describe('Admin roles tab component ', function() { it( 'should open translation coordinator modal on adding' + - ' translation coordinator role', () => { + ' translation coordinator role', + () => { spyOn( component, - 'openTranslationCoordinatorRoleEditor').and.returnValue(); + 'openTranslationCoordinatorRoleEditor' + ).and.returnValue(); component.addNewRole('TRANSLATION_COORDINATOR'); expect( - component.openTranslationCoordinatorRoleEditor).toHaveBeenCalled(); - }); + component.openTranslationCoordinatorRoleEditor + ).toHaveBeenCalled(); + } + ); }); - describe('on calling openTopicManagerRoleEditor', function() { + describe('on calling openTopicManagerRoleEditor', function () { let ngbModal: NgbModal; class MockNgbModalRef { componentInstance!: {}; } - beforeEach(function() { + beforeEach(function () { ngbModal = TestBed.inject(NgbModal); component.topicSummaries = [sampleTopicSummary]; }); it('should open the TopicManagerRoleEditorModal', fakeAsync(() => { let modalSpy = spyOn(ngbModal, 'open').and.callFake(() => { - return ({ + return { componentInstance: MockNgbModalRef, - result: Promise.resolve(['topic_id_1']) - }) as NgbModalRef; + result: Promise.resolve(['topic_id_1']), + } as NgbModalRef; }); component.userRoles = ['MODERATOR']; @@ -383,50 +388,51 @@ describe('Admin roles tab component ', function() { tick(); expect(modalSpy).toHaveBeenCalledWith( - TopicManagerRoleEditorModalComponent); + TopicManagerRoleEditorModalComponent + ); expect(component.managedTopicIds).toEqual(['topic_id_1']); expect(component.userRoles).toEqual(['MODERATOR', 'TOPIC_MANAGER']); })); - it('should not read topic manager role if user is already a manager', - fakeAsync(() => { - let modalSpy = spyOn(ngbModal, 'open').and.callFake(() => { - return ({ - componentInstance: MockNgbModalRef, - result: Promise.resolve(['topic_id_1']) - }) as NgbModalRef; - }); + it('should not read topic manager role if user is already a manager', fakeAsync(() => { + let modalSpy = spyOn(ngbModal, 'open').and.callFake(() => { + return { + componentInstance: MockNgbModalRef, + result: Promise.resolve(['topic_id_1']), + } as NgbModalRef; + }); - component.userRoles = ['MODERATOR', 'TOPIC_MANAGER']; - component.managedTopicIds = []; + component.userRoles = ['MODERATOR', 'TOPIC_MANAGER']; + component.managedTopicIds = []; - component.openTopicManagerRoleEditor(); - tick(); + component.openTopicManagerRoleEditor(); + tick(); - expect(modalSpy).toHaveBeenCalledWith( - TopicManagerRoleEditorModalComponent); - expect(component.managedTopicIds).toEqual(['topic_id_1']); - expect(component.userRoles).toEqual(['MODERATOR', 'TOPIC_MANAGER']); - })); + expect(modalSpy).toHaveBeenCalledWith( + TopicManagerRoleEditorModalComponent + ); + expect(component.managedTopicIds).toEqual(['topic_id_1']); + expect(component.userRoles).toEqual(['MODERATOR', 'TOPIC_MANAGER']); + })); }); - describe('on calling openTranslationCoordinatorRoleEditor', function() { + describe('on calling openTranslationCoordinatorRoleEditor', function () { let ngbModal: NgbModal; class MockNgbModalRef { componentInstance!: {}; } - beforeEach(function() { + beforeEach(function () { ngbModal = TestBed.inject(NgbModal); }); it('should open the TranslationCoordinatorRoleEditor', fakeAsync(() => { let modalSpy = spyOn(ngbModal, 'open').and.callFake(() => { - return ({ + return { componentInstance: MockNgbModalRef, - result: Promise.resolve(['en']) - }) as NgbModalRef; + result: Promise.resolve(['en']), + } as NgbModalRef; }); component.userRoles = ['MODERATOR']; @@ -436,37 +442,45 @@ describe('Admin roles tab component ', function() { tick(); expect(modalSpy).toHaveBeenCalledWith( - TranslationCoordinatorRoleEditorModalComponent); + TranslationCoordinatorRoleEditorModalComponent + ); expect(component.coordinatedLanguageIds).toEqual(['en']); - expect(component.userRoles).toEqual( - ['MODERATOR', 'TRANSLATION_COORDINATOR']); + expect(component.userRoles).toEqual([ + 'MODERATOR', + 'TRANSLATION_COORDINATOR', + ]); })); - it('should not read translation coordinator role if user is already a' + - ' coordinator', - fakeAsync(() => { - let modalSpy = spyOn(ngbModal, 'open').and.callFake(() => { - return ({ - componentInstance: MockNgbModalRef, - result: Promise.resolve(['en']) - }) as NgbModalRef; - }); + it( + 'should not read translation coordinator role if user is already a' + + ' coordinator', + fakeAsync(() => { + let modalSpy = spyOn(ngbModal, 'open').and.callFake(() => { + return { + componentInstance: MockNgbModalRef, + result: Promise.resolve(['en']), + } as NgbModalRef; + }); - component.userRoles = ['MODERATOR', 'TRANSLATION_COORDINATOR']; - component.coordinatedLanguageIds = []; + component.userRoles = ['MODERATOR', 'TRANSLATION_COORDINATOR']; + component.coordinatedLanguageIds = []; - component.openTranslationCoordinatorRoleEditor(); - tick(); + component.openTranslationCoordinatorRoleEditor(); + tick(); - expect(modalSpy).toHaveBeenCalledWith( - TranslationCoordinatorRoleEditorModalComponent); - expect(component.coordinatedLanguageIds).toEqual(['en']); - expect(component.userRoles).toEqual( - ['MODERATOR', 'TRANSLATION_COORDINATOR']); - })); + expect(modalSpy).toHaveBeenCalledWith( + TranslationCoordinatorRoleEditorModalComponent + ); + expect(component.coordinatedLanguageIds).toEqual(['en']); + expect(component.userRoles).toEqual([ + 'MODERATOR', + 'TRANSLATION_COORDINATOR', + ]); + }) + ); }); - describe('on calling showNewRoleSelector', function() { + describe('on calling showNewRoleSelector', function () { it('should enable roleSelectorIsShown', () => { component.roleSelectorIsShown = false; component.userRoles = ['FULL_USER', 'MODERATOR']; diff --git a/core/templates/pages/admin-page/roles-tab/admin-roles-tab.component.ts b/core/templates/pages/admin-page/roles-tab/admin-roles-tab.component.ts index d98c73164f0a..9f2fe53ddd2f 100644 --- a/core/templates/pages/admin-page/roles-tab/admin-roles-tab.component.ts +++ b/core/templates/pages/admin-page/roles-tab/admin-roles-tab.component.ts @@ -16,19 +16,23 @@ * @fileoverview Component for editing user roles. */ -import { Component, EventEmitter, OnInit, Output } from '@angular/core'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { AdminDataService } from '../services/admin-data.service'; -import { AdminBackendApiService, HumanReadableRolesBackendResponse, RoleToActionsBackendResponse } from 'domain/admin/admin-backend-api.service'; -import { TopicManagerRoleEditorModalComponent } from './topic-manager-role-editor-modal.component'; -import { TranslationCoordinatorRoleEditorModalComponent } from './translation-coordinator-role-editor-modal.component'; -import { AlertsService } from 'services/alerts.service'; -import { CreatorTopicSummary } from 'domain/topic/creator-topic-summary.model'; +import {Component, EventEmitter, OnInit, Output} from '@angular/core'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {AdminDataService} from '../services/admin-data.service'; +import { + AdminBackendApiService, + HumanReadableRolesBackendResponse, + RoleToActionsBackendResponse, +} from 'domain/admin/admin-backend-api.service'; +import {TopicManagerRoleEditorModalComponent} from './topic-manager-role-editor-modal.component'; +import {TranslationCoordinatorRoleEditorModalComponent} from './translation-coordinator-role-editor-modal.component'; +import {AlertsService} from 'services/alerts.service'; +import {CreatorTopicSummary} from 'domain/topic/creator-topic-summary.model'; import constants from 'assets/constants'; @Component({ selector: 'oppia-admin-roles-tab', - templateUrl: './admin-roles-tab.component.html' + templateUrl: './admin-roles-tab.component.html', }) export class AdminRolesTabComponent implements OnInit { @Output() setStatusMessage: EventEmitter = new EventEmitter(); @@ -66,29 +70,31 @@ export class AdminRolesTabComponent implements OnInit { addWarning(errorMessage: string): void { this.alertsService.addWarning( - errorMessage || 'Error communicating with server.'); + errorMessage || 'Error communicating with server.' + ); } startEditing(): void { this.roleIsCurrentlyBeingEdited = true; - this.adminBackendApiService.viewUsersRoleAsync( - this.username - ).then((userRoles) => { - this.rolesFetched = true; - this.userRoles = userRoles.roles; - this.managedTopicIds = userRoles.managed_topic_ids; - this.coordinatedLanguageIds = userRoles.coordinated_language_ids; - this.userIsBanned = userRoles.banned; - }, - (errorResponse) => { - this.roleIsCurrentlyBeingEdited = false; - this.setStatusMessage.emit(errorResponse); - }); + this.adminBackendApiService.viewUsersRoleAsync(this.username).then( + userRoles => { + this.rolesFetched = true; + this.userRoles = userRoles.roles; + this.managedTopicIds = userRoles.managed_topic_ids; + this.coordinatedLanguageIds = userRoles.coordinated_language_ids; + this.userIsBanned = userRoles.banned; + }, + errorResponse => { + this.roleIsCurrentlyBeingEdited = false; + this.setStatusMessage.emit(errorResponse); + } + ); } showNewRoleSelector(): void { this.possibleRolesToAdd = this.UPDATABLE_ROLES.filter( - role => !this.userRoles.includes(role)).sort(); + role => !this.userRoles.includes(role) + ).sort(); this.roleSelectorIsShown = true; } @@ -96,34 +102,34 @@ export class AdminRolesTabComponent implements OnInit { this.roleCurrentlyBeingUpdatedInBackend = roleToRemove; var roleIndex = this.userRoles.indexOf(roleToRemove); - this.adminBackendApiService.removeUserRoleAsync( - roleToRemove, this.username).then(() => { - if (roleToRemove === 'TOPIC_MANAGER') { - this.managedTopicIds = []; - } - if (roleToRemove === 'TRANSLATION_COORDINATOR') { - this.coordinatedLanguageIds = []; - } - this.userRoles.splice(roleIndex, 1); - this.roleCurrentlyBeingUpdatedInBackend = null; - }); + this.adminBackendApiService + .removeUserRoleAsync(roleToRemove, this.username) + .then(() => { + if (roleToRemove === 'TOPIC_MANAGER') { + this.managedTopicIds = []; + } + if (roleToRemove === 'TRANSLATION_COORDINATOR') { + this.coordinatedLanguageIds = []; + } + this.userRoles.splice(roleIndex, 1); + this.roleCurrentlyBeingUpdatedInBackend = null; + }); } openTopicManagerRoleEditor(): void { const modalRef = this.modalService.open( - TopicManagerRoleEditorModalComponent); - modalRef.componentInstance.managedTopicIds = ( - this.managedTopicIds); + TopicManagerRoleEditorModalComponent + ); + modalRef.componentInstance.managedTopicIds = this.managedTopicIds; modalRef.componentInstance.username = this.username; let topicIdToName: Record = {}; this.topicSummaries.forEach( - topicSummary => topicIdToName[topicSummary.id] = topicSummary.name); + topicSummary => (topicIdToName[topicSummary.id] = topicSummary.name) + ); modalRef.componentInstance.topicIdToName = topicIdToName; modalRef.result.then(managedTopicIds => { this.managedTopicIds = managedTopicIds; - if ( - !this.userRoles.includes('TOPIC_MANAGER') && - managedTopicIds.length) { + if (!this.userRoles.includes('TOPIC_MANAGER') && managedTopicIds.length) { this.userRoles.push('TOPIC_MANAGER'); } this.roleSelectorIsShown = false; @@ -132,19 +138,22 @@ export class AdminRolesTabComponent implements OnInit { openTranslationCoordinatorRoleEditor(): void { const modalRef = this.modalService.open( - TranslationCoordinatorRoleEditorModalComponent); - modalRef.componentInstance.coordinatedLanguageIds = ( - this.coordinatedLanguageIds); + TranslationCoordinatorRoleEditorModalComponent + ); + modalRef.componentInstance.coordinatedLanguageIds = + this.coordinatedLanguageIds; modalRef.componentInstance.username = this.username; let languageIdToName: Record = {}; constants.SUPPORTED_AUDIO_LANGUAGES.forEach( - language => languageIdToName[language.id] = language.description); + language => (languageIdToName[language.id] = language.description) + ); modalRef.componentInstance.languageIdToName = languageIdToName; modalRef.result.then(coordinatedLanguageIds => { this.coordinatedLanguageIds = coordinatedLanguageIds; if ( !this.userRoles.includes('TRANSLATION_COORDINATOR') && - coordinatedLanguageIds.length) { + coordinatedLanguageIds.length + ) { this.userRoles.push('TRANSLATION_COORDINATOR'); } this.roleSelectorIsShown = false; @@ -166,10 +175,11 @@ export class AdminRolesTabComponent implements OnInit { this.userRoles.push(role); this.roleSelectorIsShown = false; - this.adminBackendApiService.addUserRoleAsync( - role, this.username).then(() => { - this.roleCurrentlyBeingUpdatedInBackend = null; - }, this.addWarning.bind(this)); + this.adminBackendApiService + .addUserRoleAsync(role, this.username) + .then(() => { + this.roleCurrentlyBeingUpdatedInBackend = null; + }, this.addWarning.bind(this)); } markUserBanned(): void { @@ -183,12 +193,13 @@ export class AdminRolesTabComponent implements OnInit { unmarkUserBanned(): void { this.bannedStatusChangeInProgress = true; - this.adminBackendApiService.unmarkUserBannedAsync( - this.username).then(() => { - this.bannedStatusChangeInProgress = false; - this.userIsBanned = false; - this.startEditing(); - }, this.addWarning.bind(this)); + this.adminBackendApiService + .unmarkUserBannedAsync(this.username) + .then(() => { + this.bannedStatusChangeInProgress = false; + this.userIsBanned = false; + this.startEditing(); + }, this.addWarning.bind(this)); } clearEditor(): void { diff --git a/core/templates/pages/admin-page/roles-tab/roles-and-actions-visualizer.component.spec.ts b/core/templates/pages/admin-page/roles-tab/roles-and-actions-visualizer.component.spec.ts index e3a5f2d5268d..dd5bc0b7453a 100644 --- a/core/templates/pages/admin-page/roles-tab/roles-and-actions-visualizer.component.spec.ts +++ b/core/templates/pages/admin-page/roles-tab/roles-and-actions-visualizer.component.spec.ts @@ -16,27 +16,29 @@ * @fileoverview Tests for roles-and-actions-visualizer component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { FormsModule } from '@angular/forms'; -import { ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing'; - -import { RolesAndActionsVisualizerComponent } from './roles-and-actions-visualizer.component'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { MaterialModule } from 'modules/material.module'; -import { AdminBackendApiService } from 'domain/admin/admin-backend-api.service'; - -describe('Roles and actions visualizer component', function() { +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {FormsModule} from '@angular/forms'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; + +import {RolesAndActionsVisualizerComponent} from './roles-and-actions-visualizer.component'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {MaterialModule} from 'modules/material.module'; +import {AdminBackendApiService} from 'domain/admin/admin-backend-api.service'; + +describe('Roles and actions visualizer component', function () { let component: RolesAndActionsVisualizerComponent; let fixture: ComponentFixture; let adminBackendApiService: AdminBackendApiService; beforeEach(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - FormsModule, - MaterialModule - ], declarations: [RolesAndActionsVisualizerComponent], + imports: [HttpClientTestingModule, FormsModule, MaterialModule], + declarations: [RolesAndActionsVisualizerComponent], providers: [UrlInterpolationService], }).compileComponents(); fixture = TestBed.createComponent(RolesAndActionsVisualizerComponent); @@ -46,41 +48,45 @@ describe('Roles and actions visualizer component', function() { component.humanReadableRoles = { MODERATOR: 'moderator', FULL_USER: 'full user', - MOBILE_LEARNER: 'mobile learner' + MOBILE_LEARNER: 'mobile learner', }; component.roleToActions = { MODERATOR: ['allowed action 1', 'allowed action 2'], MOBILE_LEARNER: ['allowed action 3', 'allowed action 4'], TRANSLATION_ADMIN: ['allowed action 5', 'allowed action 6'], - FULL_USER: ['allowed action 7', 'allowed action 8'] + FULL_USER: ['allowed action 7', 'allowed action 8'], }; }); - it('should intialize correct active role', function() { + it('should intialize correct active role', function () { component.ngOnInit(); expect(component.activeRole).toEqual('TRANSLATION_ADMIN'); }); - it('should intialize roles with all the roles', function() { + it('should intialize roles with all the roles', function () { component.ngOnInit(); - expect(component.roles).toEqual( - ['FULL_USER', 'MOBILE_LEARNER', 'MODERATOR', 'TRANSLATION_ADMIN']); + expect(component.roles).toEqual([ + 'FULL_USER', + 'MOBILE_LEARNER', + 'MODERATOR', + 'TRANSLATION_ADMIN', + ]); }); - it('should intialize roleToReadableActions correctly', function() { + it('should intialize roleToReadableActions correctly', function () { component.ngOnInit(); expect(component.roleToReadableActions).toEqual({ MODERATOR: ['Allowed action 1', 'Allowed action 2'], MOBILE_LEARNER: ['Allowed action 3', 'Allowed action 4'], TRANSLATION_ADMIN: ['Allowed action 5', 'Allowed action 6'], - FULL_USER: ['Allowed action 7', 'Allowed action 8'] + FULL_USER: ['Allowed action 7', 'Allowed action 8'], }); }); - it('should set active role correctly', function() { + it('should set active role correctly', function () { component.ngOnInit(); component.activeRole = 'MOBILE_LEARNER'; @@ -91,18 +97,19 @@ describe('Roles and actions visualizer component', function() { expect(component.activeTab).toEqual(component.TAB_ACTIONS); }); - it('should update assignUsersToActiveRole when calling showAssignedUsers', - fakeAsync(() => { - spyOn( - adminBackendApiService, 'fetchUsersAssignedToRoleAsync').and.resolveTo({ - usernames: ['userA', 'userB'] - }); - component.activeRole = 'MOBILE_LEARNER'; - component.assignUsersToActiveRole = []; + it('should update assignUsersToActiveRole when calling showAssignedUsers', fakeAsync(() => { + spyOn( + adminBackendApiService, + 'fetchUsersAssignedToRoleAsync' + ).and.resolveTo({ + usernames: ['userA', 'userB'], + }); + component.activeRole = 'MOBILE_LEARNER'; + component.assignUsersToActiveRole = []; - component.showAssignedUsers(); - tick(); + component.showAssignedUsers(); + tick(); - expect(component.assignUsersToActiveRole).toEqual(['userA', 'userB']); - })); + expect(component.assignUsersToActiveRole).toEqual(['userA', 'userB']); + })); }); diff --git a/core/templates/pages/admin-page/roles-tab/roles-and-actions-visualizer.component.ts b/core/templates/pages/admin-page/roles-tab/roles-and-actions-visualizer.component.ts index 9346e40d8f7d..4b7caf5a856c 100644 --- a/core/templates/pages/admin-page/roles-tab/roles-and-actions-visualizer.component.ts +++ b/core/templates/pages/admin-page/roles-tab/roles-and-actions-visualizer.component.ts @@ -16,14 +16,14 @@ * @fileoverview Component for displaying roles and actions. */ -import { Component, Input, OnInit } from '@angular/core'; +import {Component, Input, OnInit} from '@angular/core'; -import { AdminBackendApiService } from 'domain/admin/admin-backend-api.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; +import {AdminBackendApiService} from 'domain/admin/admin-backend-api.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; @Component({ selector: 'oppia-roles-and-actions-visualizer', - templateUrl: './roles-and-actions-visualizer.component.html' + templateUrl: './roles-and-actions-visualizer.component.html', }) export class RolesAndActionsVisualizerComponent implements OnInit { // These properties are initialized using Angular lifecycle hooks @@ -45,7 +45,8 @@ export class RolesAndActionsVisualizerComponent implements OnInit { constructor( private urlInterpolationService: UrlInterpolationService, - private adminBackendApiService: AdminBackendApiService) {} + private adminBackendApiService: AdminBackendApiService + ) {} setActiveRole(role: string): void { this.activeRole = role; @@ -54,32 +55,39 @@ export class RolesAndActionsVisualizerComponent implements OnInit { showAssignedUsers(): void { this.loadingAssignedUsernames = true; - this.adminBackendApiService.fetchUsersAssignedToRoleAsync( - this.activeRole).then((response) => { - this.assignUsersToActiveRole = response.usernames; - this.loadingAssignedUsernames = false; - }); + this.adminBackendApiService + .fetchUsersAssignedToRoleAsync(this.activeRole) + .then(response => { + this.assignUsersToActiveRole = response.usernames; + this.loadingAssignedUsernames = false; + }); } ngOnInit(): void { this.loadingAssignedUsernames = false; this.avatarPictureUrl = this.urlInterpolationService.getStaticImageUrl( - '/avatar/user_blue_72px.png'); + '/avatar/user_blue_72px.png' + ); this.activeTab = this.TAB_ACTIONS; let getSortedReadableTexts = (texts: string[]): string[] => { let readableTexts: string[] = []; texts.forEach(text => { readableTexts.push( - text.toLowerCase().split('_').join(' ').replace( - /^\w/, (c) => c.toUpperCase())); + text + .toLowerCase() + .split('_') + .join(' ') + .replace(/^\w/, c => c.toUpperCase()) + ); }); return readableTexts.sort(); }; for (let role in this.roleToActions) { this.roleToReadableActions[role] = getSortedReadableTexts( - this.roleToActions[role]); + this.roleToActions[role] + ); } this.roles = Object.keys(this.roleToActions).sort(); diff --git a/core/templates/pages/admin-page/roles-tab/topic-manager-role-editor-modal.component.spec.ts b/core/templates/pages/admin-page/roles-tab/topic-manager-role-editor-modal.component.spec.ts index 8c71579d4c34..9775a20facdd 100644 --- a/core/templates/pages/admin-page/roles-tab/topic-manager-role-editor-modal.component.spec.ts +++ b/core/templates/pages/admin-page/roles-tab/topic-manager-role-editor-modal.component.spec.ts @@ -16,16 +16,22 @@ * @fileoverview Unit tests for TopicManagerRoleEditorModalComponent. */ -import { ComponentFixture, fakeAsync, TestBed, async, tick } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { FormsModule } from '@angular/forms'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { MaterialModule } from 'modules/material.module'; - -import { AdminBackendApiService } from 'domain/admin/admin-backend-api.service'; -import { AlertsService } from 'services/alerts.service'; - -import { TopicManagerRoleEditorModalComponent } from './topic-manager-role-editor-modal.component'; +import { + ComponentFixture, + fakeAsync, + TestBed, + async, + tick, +} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {FormsModule} from '@angular/forms'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {MaterialModule} from 'modules/material.module'; + +import {AdminBackendApiService} from 'domain/admin/admin-backend-api.service'; +import {AlertsService} from 'services/alerts.service'; + +import {TopicManagerRoleEditorModalComponent} from './topic-manager-role-editor-modal.component'; describe('TopicManagerRoleEditorModalComponent', () => { let component: TopicManagerRoleEditorModalComponent; @@ -36,17 +42,9 @@ describe('TopicManagerRoleEditorModalComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [ - FormsModule, - MaterialModule, - HttpClientTestingModule - ], + imports: [FormsModule, MaterialModule, HttpClientTestingModule], declarations: [TopicManagerRoleEditorModalComponent], - providers: [ - NgbActiveModal, - AdminBackendApiService, - AlertsService - ] + providers: [NgbActiveModal, AdminBackendApiService, AlertsService], }).compileComponents(); })); @@ -61,7 +59,7 @@ describe('TopicManagerRoleEditorModalComponent', () => { topic000: 'Topic 000', topic123: 'Topic 123', topic456: 'Topic 456', - topic789: 'Topic 789' + topic789: 'Topic 789', }; fixture.detectChanges(); }); @@ -81,7 +79,9 @@ describe('TopicManagerRoleEditorModalComponent', () => { it('should make request to add topic', fakeAsync(() => { spyOn( - adminBackendApiService, 'assignManagerToTopicAsync').and.resolveTo(); + adminBackendApiService, + 'assignManagerToTopicAsync' + ).and.resolveTo(); component.newTopicId = 'topic000'; component.addTopic(); @@ -89,13 +89,15 @@ describe('TopicManagerRoleEditorModalComponent', () => { tick(); expect( - adminBackendApiService.assignManagerToTopicAsync).toHaveBeenCalled(); + adminBackendApiService.assignManagerToTopicAsync + ).toHaveBeenCalled(); })); it('should alert warning if request fails', fakeAsync(() => { spyOn( - adminBackendApiService, 'assignManagerToTopicAsync').and.returnValue( - Promise.reject()); + adminBackendApiService, + 'assignManagerToTopicAsync' + ).and.returnValue(Promise.reject()); spyOn(alertsService, 'addWarning').and.callThrough(); component.newTopicId = 'topic000'; @@ -123,21 +125,23 @@ describe('TopicManagerRoleEditorModalComponent', () => { it('should make request to remove topic', fakeAsync(() => { spyOn( adminBackendApiService, - 'deassignManagerFromTopicAsync').and.resolveTo(); + 'deassignManagerFromTopicAsync' + ).and.resolveTo(); component.removeTopicId('topic123'); expect(component.topicIdInUpdate).toEqual('topic123'); tick(); expect( - adminBackendApiService - .deassignManagerFromTopicAsync).toHaveBeenCalled(); + adminBackendApiService.deassignManagerFromTopicAsync + ).toHaveBeenCalled(); })); it('should alert warning if request fails', fakeAsync(() => { spyOn( adminBackendApiService, - 'deassignManagerFromTopicAsync').and.returnValue(Promise.reject()); + 'deassignManagerFromTopicAsync' + ).and.returnValue(Promise.reject()); spyOn(alertsService, 'addWarning').and.callThrough(); component.removeTopicId('topic123'); diff --git a/core/templates/pages/admin-page/roles-tab/topic-manager-role-editor-modal.component.ts b/core/templates/pages/admin-page/roles-tab/topic-manager-role-editor-modal.component.ts index 0bd581e64f74..524d1f267c6d 100644 --- a/core/templates/pages/admin-page/roles-tab/topic-manager-role-editor-modal.component.ts +++ b/core/templates/pages/admin-page/roles-tab/topic-manager-role-editor-modal.component.ts @@ -16,12 +16,11 @@ * @fileoverview Component for editing user roles. */ -import { Component, OnInit, Input } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; - -import { AdminBackendApiService } from 'domain/admin/admin-backend-api.service'; -import { AlertsService } from 'services/alerts.service'; +import {Component, OnInit, Input} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {AdminBackendApiService} from 'domain/admin/admin-backend-api.service'; +import {AlertsService} from 'services/alerts.service'; @Component({ selector: 'oppia-topic-manager-role-editor-modal', @@ -49,7 +48,8 @@ export class TopicManagerRoleEditorModalComponent implements OnInit { private updateTopicIdsForSelection(): void { this.topicIdsForSelection = Object.keys(this.topicIdToName).filter( - topicId => !this.managedTopicIds.includes(topicId)); + topicId => !this.managedTopicIds.includes(topicId) + ); this.newTopicId = this.topicIdsForSelection[0]; } @@ -60,33 +60,44 @@ export class TopicManagerRoleEditorModalComponent implements OnInit { this.managedTopicIds.push(this.newTopicId); this.topicIdInUpdate = this.newTopicId; this.newTopicId = null; - this.adminBackendApiService.assignManagerToTopicAsync( - this.username, this.topicIdInUpdate).then(()=> { - this.topicIdInUpdate = null; - this.updateTopicIdsForSelection(); - }, errorMessage => { - if (this.topicIdInUpdate !== null) { - let topicIdIndex = this.managedTopicIds.indexOf( - this.topicIdInUpdate); - this.managedTopicIds.splice(topicIdIndex, 1); - } - this.alertsService.addWarning( - errorMessage || 'Error communicating with server.'); - }); + this.adminBackendApiService + .assignManagerToTopicAsync(this.username, this.topicIdInUpdate) + .then( + () => { + this.topicIdInUpdate = null; + this.updateTopicIdsForSelection(); + }, + errorMessage => { + if (this.topicIdInUpdate !== null) { + let topicIdIndex = this.managedTopicIds.indexOf( + this.topicIdInUpdate + ); + this.managedTopicIds.splice(topicIdIndex, 1); + } + this.alertsService.addWarning( + errorMessage || 'Error communicating with server.' + ); + } + ); } removeTopicId(topicIdToRemove: string): void { let topicIdIndex = this.managedTopicIds.indexOf(topicIdToRemove); this.topicIdInUpdate = topicIdToRemove; - this.adminBackendApiService.deassignManagerFromTopicAsync( - this.username, topicIdToRemove).then(() => { - this.managedTopicIds.splice(topicIdIndex, 1); - this.topicIdInUpdate = null; - this.updateTopicIdsForSelection(); - }, errorMessage => { - this.alertsService.addWarning( - errorMessage || 'Error communicating with server.'); - }); + this.adminBackendApiService + .deassignManagerFromTopicAsync(this.username, topicIdToRemove) + .then( + () => { + this.managedTopicIds.splice(topicIdIndex, 1); + this.topicIdInUpdate = null; + this.updateTopicIdsForSelection(); + }, + errorMessage => { + this.alertsService.addWarning( + errorMessage || 'Error communicating with server.' + ); + } + ); } close(): void { diff --git a/core/templates/pages/admin-page/roles-tab/translation-coordinator-role-editor-modal.component.spec.ts b/core/templates/pages/admin-page/roles-tab/translation-coordinator-role-editor-modal.component.spec.ts index 968acdd31294..da7aee6ca614 100644 --- a/core/templates/pages/admin-page/roles-tab/translation-coordinator-role-editor-modal.component.spec.ts +++ b/core/templates/pages/admin-page/roles-tab/translation-coordinator-role-editor-modal.component.spec.ts @@ -16,16 +16,22 @@ * @fileoverview Unit tests for TranslationCoordinatorRoleEditorModalComponent. */ -import { ComponentFixture, fakeAsync, TestBed, async, tick } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { FormsModule } from '@angular/forms'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { MaterialModule } from 'modules/material.module'; - -import { AdminBackendApiService } from 'domain/admin/admin-backend-api.service'; -import { AlertsService } from 'services/alerts.service'; - -import { TranslationCoordinatorRoleEditorModalComponent } from './translation-coordinator-role-editor-modal.component'; +import { + ComponentFixture, + fakeAsync, + TestBed, + async, + tick, +} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {FormsModule} from '@angular/forms'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {MaterialModule} from 'modules/material.module'; + +import {AdminBackendApiService} from 'domain/admin/admin-backend-api.service'; +import {AlertsService} from 'services/alerts.service'; + +import {TranslationCoordinatorRoleEditorModalComponent} from './translation-coordinator-role-editor-modal.component'; describe('TranslationCoordinatorRoleEditorModalComponent', () => { let component: TranslationCoordinatorRoleEditorModalComponent; @@ -36,23 +42,16 @@ describe('TranslationCoordinatorRoleEditorModalComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [ - FormsModule, - MaterialModule, - HttpClientTestingModule - ], + imports: [FormsModule, MaterialModule, HttpClientTestingModule], declarations: [TranslationCoordinatorRoleEditorModalComponent], - providers: [ - NgbActiveModal, - AdminBackendApiService, - AlertsService - ] + providers: [NgbActiveModal, AdminBackendApiService, AlertsService], }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent( - TranslationCoordinatorRoleEditorModalComponent); + TranslationCoordinatorRoleEditorModalComponent + ); component = fixture.componentInstance; ngbActiveModal = TestBed.get(NgbActiveModal); adminBackendApiService = TestBed.inject(AdminBackendApiService); @@ -62,7 +61,7 @@ describe('TranslationCoordinatorRoleEditorModalComponent', () => { en: 'English', hi: 'Hindi', ak: 'Ákán (Akan)', - sk: 'shqip (Albanian)' + sk: 'shqip (Albanian)', }; fixture.detectChanges(); }); @@ -82,7 +81,9 @@ describe('TranslationCoordinatorRoleEditorModalComponent', () => { it('should make request to add topic', fakeAsync(() => { spyOn( - adminBackendApiService, 'assignTranslationCoordinator').and.resolveTo(); + adminBackendApiService, + 'assignTranslationCoordinator' + ).and.resolveTo(); component.newLanguageId = 'en'; component.addLanguage(); @@ -90,13 +91,15 @@ describe('TranslationCoordinatorRoleEditorModalComponent', () => { tick(); expect( - adminBackendApiService.assignTranslationCoordinator).toHaveBeenCalled(); + adminBackendApiService.assignTranslationCoordinator + ).toHaveBeenCalled(); })); it('should alert warning if request fails', fakeAsync(() => { spyOn( - adminBackendApiService, 'assignTranslationCoordinator').and.returnValue( - Promise.reject()); + adminBackendApiService, + 'assignTranslationCoordinator' + ).and.returnValue(Promise.reject()); spyOn(alertsService, 'addWarning').and.callThrough(); component.newLanguageId = 'en'; @@ -124,21 +127,23 @@ describe('TranslationCoordinatorRoleEditorModalComponent', () => { it('should make request to remove topic', fakeAsync(() => { spyOn( adminBackendApiService, - 'deassignTranslationCoordinator').and.resolveTo(); + 'deassignTranslationCoordinator' + ).and.resolveTo(); component.removeLanguageId('hi'); expect(component.languageIdInUpdate).toEqual('hi'); tick(); expect( - adminBackendApiService - .deassignTranslationCoordinator).toHaveBeenCalled(); + adminBackendApiService.deassignTranslationCoordinator + ).toHaveBeenCalled(); })); it('should alert warning if request fails', fakeAsync(() => { spyOn( adminBackendApiService, - 'deassignTranslationCoordinator').and.returnValue(Promise.reject()); + 'deassignTranslationCoordinator' + ).and.returnValue(Promise.reject()); spyOn(alertsService, 'addWarning').and.callThrough(); component.removeLanguageId('hi'); diff --git a/core/templates/pages/admin-page/roles-tab/translation-coordinator-role-editor-modal.component.ts b/core/templates/pages/admin-page/roles-tab/translation-coordinator-role-editor-modal.component.ts index 07e1e5394f6b..499b3c7b9cca 100644 --- a/core/templates/pages/admin-page/roles-tab/translation-coordinator-role-editor-modal.component.ts +++ b/core/templates/pages/admin-page/roles-tab/translation-coordinator-role-editor-modal.component.ts @@ -16,12 +16,11 @@ * @fileoverview Component for editing user roles. */ -import { Component, OnInit, Input } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; - -import { AdminBackendApiService } from 'domain/admin/admin-backend-api.service'; -import { AlertsService } from 'services/alerts.service'; +import {Component, OnInit, Input} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {AdminBackendApiService} from 'domain/admin/admin-backend-api.service'; +import {AlertsService} from 'services/alerts.service'; @Component({ selector: 'oppia-translation-coordinator-role-editor-modal', @@ -49,7 +48,8 @@ export class TranslationCoordinatorRoleEditorModalComponent implements OnInit { private updatelanguageIdsForSelection(): void { this.languageIdsForSelection = Object.keys(this.languageIdToName).filter( - languageId => !this.coordinatedLanguageIds.includes(languageId)); + languageId => !this.coordinatedLanguageIds.includes(languageId) + ); this.newLanguageId = this.languageIdsForSelection[0]; } @@ -60,34 +60,45 @@ export class TranslationCoordinatorRoleEditorModalComponent implements OnInit { this.coordinatedLanguageIds.push(this.newLanguageId); this.languageIdInUpdate = this.newLanguageId; this.newLanguageId = null; - this.adminBackendApiService.assignTranslationCoordinator( - this.username, this.languageIdInUpdate).then(()=> { - this.languageIdInUpdate = null; - this.updatelanguageIdsForSelection(); - }, errorMessage => { - if (this.languageIdInUpdate !== null) { - let languageIdIndex = this.coordinatedLanguageIds.indexOf( - this.languageIdInUpdate); - this.coordinatedLanguageIds.splice(languageIdIndex, 1); - } - this.alertsService.addWarning( - errorMessage || 'Error communicating with server.'); - }); + this.adminBackendApiService + .assignTranslationCoordinator(this.username, this.languageIdInUpdate) + .then( + () => { + this.languageIdInUpdate = null; + this.updatelanguageIdsForSelection(); + }, + errorMessage => { + if (this.languageIdInUpdate !== null) { + let languageIdIndex = this.coordinatedLanguageIds.indexOf( + this.languageIdInUpdate + ); + this.coordinatedLanguageIds.splice(languageIdIndex, 1); + } + this.alertsService.addWarning( + errorMessage || 'Error communicating with server.' + ); + } + ); } removeLanguageId(languageIdToRemove: string): void { - let languageIdIndex = this.coordinatedLanguageIds.indexOf( - languageIdToRemove); + let languageIdIndex = + this.coordinatedLanguageIds.indexOf(languageIdToRemove); this.languageIdInUpdate = languageIdToRemove; - this.adminBackendApiService.deassignTranslationCoordinator( - this.username, languageIdToRemove).then(() => { - this.coordinatedLanguageIds.splice(languageIdIndex, 1); - this.languageIdInUpdate = null; - this.updatelanguageIdsForSelection(); - }, errorMessage => { - this.alertsService.addWarning( - errorMessage || 'Error communicating with server.'); - }); + this.adminBackendApiService + .deassignTranslationCoordinator(this.username, languageIdToRemove) + .then( + () => { + this.coordinatedLanguageIds.splice(languageIdIndex, 1); + this.languageIdInUpdate = null; + this.updatelanguageIdsForSelection(); + }, + errorMessage => { + this.alertsService.addWarning( + errorMessage || 'Error communicating with server.' + ); + } + ); } close(): void { diff --git a/core/templates/pages/admin-page/services/admin-data.service.spec.ts b/core/templates/pages/admin-page/services/admin-data.service.spec.ts index be5264e316a4..1ef7d611f5f5 100644 --- a/core/templates/pages/admin-page/services/admin-data.service.spec.ts +++ b/core/templates/pages/admin-page/services/admin-data.service.spec.ts @@ -16,27 +16,27 @@ * @fileoverview Tests for AdminDataService. */ -import { HttpClientTestingModule, HttpTestingController } from - '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; - -import { PlatformParameterFilterType } from - 'domain/platform-parameter/platform-parameter-filter.model'; -import { PlatformParameter } from - 'domain/platform-parameter/platform-parameter.model'; -import { AdminDataService } from - 'pages/admin-page/services/admin-data.service'; -import { AdminPageData, AdminPageDataBackendDict } from - 'domain/admin/admin-backend-api.service'; -import { CreatorTopicSummary } from 'domain/topic/creator-topic-summary.model'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; +import {PlatformParameterFilterType} from 'domain/platform-parameter/platform-parameter-filter.model'; +import {PlatformParameter} from 'domain/platform-parameter/platform-parameter.model'; +import {AdminDataService} from 'pages/admin-page/services/admin-data.service'; +import { + AdminPageData, + AdminPageDataBackendDict, +} from 'domain/admin/admin-backend-api.service'; +import {CreatorTopicSummary} from 'domain/topic/creator-topic-summary.model'; describe('Admin Data Service', () => { let adminDataService: AdminDataService; let httpTestingController: HttpTestingController; var sampleAdminData: AdminPageDataBackendDict = { role_to_actions: { - guest: ['action for guest'] + guest: ['action for guest'], }, topic_summaries: [ { @@ -61,8 +61,8 @@ describe('Admin Data Service', () => { total_upcoming_chapters_count: 1, total_overdue_chapters_count: 1, total_chapter_counts_for_each_story: [5, 4], - published_chapter_counts_for_each_story: [3, 4] - } + published_chapter_counts_for_each_story: [3, 4], + }, ], updatable_roles: ['TOPIC_MANAGER'], human_readable_current_time: 'June 03 15:31:20', @@ -70,45 +70,46 @@ describe('Admin Data Service', () => { config_properties: { record_playthrough_probability: { schema: { - type: 'float' + type: 'float', }, value: 0.2, - description: 'The record_playthrough_probability.' - } + description: 'The record_playthrough_probability.', + }, }, demo_exploration_ids: ['19'], - demo_explorations: [ - [ - '0', - 'welcome.yaml' - ] - ], + demo_explorations: [['0', 'welcome.yaml']], viewable_roles: ['TOPIC_MANAGER'], human_readable_roles: { FULL_USER: 'full user', - TOPIC_MANAGER: 'topic manager' + TOPIC_MANAGER: 'topic manager', }, - platform_params_dicts: [{ - name: 'dummy_parameter', - description: 'This is a dummy platform parameter.', - data_type: 'string', - rules: [{ - filters: [{ - type: PlatformParameterFilterType.PlatformType, - conditions: [['=', 'Web'] as [string, string]] - }], - value_when_matched: '' - }], - rule_schema_version: 1, - default_value: '' - }], + platform_params_dicts: [ + { + name: 'dummy_parameter', + description: 'This is a dummy platform parameter.', + data_type: 'string', + rules: [ + { + filters: [ + { + type: PlatformParameterFilterType.PlatformType, + conditions: [['=', 'Web'] as [string, string]], + }, + ], + value_when_matched: '', + }, + ], + rule_schema_version: 1, + default_value: '', + }, + ], }; let adminDataResponse: AdminPageData; beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [AdminDataService] + providers: [AdminDataService], }); adminDataService = TestBed.get(AdminDataService); httpTestingController = TestBed.get(HttpTestingController); @@ -122,9 +123,11 @@ describe('Admin Data Service', () => { viewableRoles: sampleAdminData.viewable_roles, humanReadableRoles: sampleAdminData.human_readable_roles, topicSummaries: sampleAdminData.topic_summaries.map( - CreatorTopicSummary.createFromBackendDict), - platformParameters: sampleAdminData.platform_params_dicts.map( - dict => PlatformParameter.createFromBackendDict(dict)) + CreatorTopicSummary.createFromBackendDict + ), + platformParameters: sampleAdminData.platform_params_dicts.map(dict => + PlatformParameter.createFromBackendDict(dict) + ), }; }); @@ -133,34 +136,30 @@ describe('Admin Data Service', () => { }); it('should return the correct admin data', fakeAsync(() => { - adminDataService.getDataAsync().then(function(response) { + adminDataService.getDataAsync().then(function (response) { expect(response).toEqual(adminDataResponse); }); - var req = httpTestingController.expectOne( - '/adminhandler'); + var req = httpTestingController.expectOne('/adminhandler'); expect(req.request.method).toEqual('GET'); req.flush(sampleAdminData); flushMicrotasks(); })); - it('should cache the response and not make a second request', - fakeAsync(() => { - adminDataService.getDataAsync(); + it('should cache the response and not make a second request', fakeAsync(() => { + adminDataService.getDataAsync(); - var req = httpTestingController.expectOne( - '/adminhandler'); - expect(req.request.method).toEqual('GET'); - req.flush(sampleAdminData); + var req = httpTestingController.expectOne('/adminhandler'); + expect(req.request.method).toEqual('GET'); + req.flush(sampleAdminData); - flushMicrotasks(); + flushMicrotasks(); - adminDataService.getDataAsync().then(function(response) { - expect(response).toEqual(adminDataResponse); - }); + adminDataService.getDataAsync().then(function (response) { + expect(response).toEqual(adminDataResponse); + }); - httpTestingController.expectNone('/adminhandler'); - }) - ); + httpTestingController.expectNone('/adminhandler'); + })); }); diff --git a/core/templates/pages/admin-page/services/admin-data.service.ts b/core/templates/pages/admin-page/services/admin-data.service.ts index fd899090758d..8910d4e0388d 100644 --- a/core/templates/pages/admin-page/services/admin-data.service.ts +++ b/core/templates/pages/admin-page/services/admin-data.service.ts @@ -16,8 +16,8 @@ * @fileoverview Service that manages admin data. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; import { AdminPageData, @@ -25,7 +25,7 @@ import { } from 'domain/admin/admin-backend-api.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class AdminDataService { // This property is initialized using private methods @@ -33,9 +33,7 @@ export class AdminDataService { // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 dataPromise!: Promise; - constructor( - private adminBackendApiService: AdminBackendApiService - ) {} + constructor(private adminBackendApiService: AdminBackendApiService) {} async _getDataAsync(): Promise { if (this.dataPromise) { @@ -52,6 +50,6 @@ export class AdminDataService { } } -angular.module('oppia').factory( - 'AdminDataService', - downgradeInjectable(AdminDataService)); +angular + .module('oppia') + .factory('AdminDataService', downgradeInjectable(AdminDataService)); diff --git a/core/templates/pages/admin-page/services/admin-router.service.spec.ts b/core/templates/pages/admin-page/services/admin-router.service.spec.ts index df6bd0228119..5173ddc97563 100644 --- a/core/templates/pages/admin-page/services/admin-router.service.spec.ts +++ b/core/templates/pages/admin-page/services/admin-router.service.spec.ts @@ -16,7 +16,7 @@ * @fileoverview Tests for AdminRouterService. */ -import { AdminRouterService } from 'pages/admin-page/services/admin-router.service'; +import {AdminRouterService} from 'pages/admin-page/services/admin-router.service'; describe('Admin router service', () => { let ars: AdminRouterService; diff --git a/core/templates/pages/admin-page/services/admin-router.service.ts b/core/templates/pages/admin-page/services/admin-router.service.ts index 0996bae66f8e..7784652e645c 100644 --- a/core/templates/pages/admin-page/services/admin-router.service.ts +++ b/core/templates/pages/admin-page/services/admin-router.service.ts @@ -17,16 +17,15 @@ * provide routing functionality, and store all available tab states. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { AdminPageConstants } from 'pages/admin-page/admin-page.constants'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {AdminPageConstants} from 'pages/admin-page/admin-page.constants'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class AdminRouterService { - currentTabHash: string = ( - AdminPageConstants.ADMIN_TAB_URLS.ACTIVITIES); + currentTabHash: string = AdminPageConstants.ADMIN_TAB_URLS.ACTIVITIES; /** * Iterates through the ADMIN_TAB_URLS map and returns the @@ -37,7 +36,8 @@ export class AdminRouterService { */ getTabNameByHash(tabHash: string): string | null { for (const [tabName, tabUrl] of Object.entries( - AdminPageConstants.ADMIN_TAB_URLS)) { + AdminPageConstants.ADMIN_TAB_URLS + )) { if (tabUrl === tabHash) { return tabName; } @@ -59,8 +59,7 @@ export class AdminRouterService { * @returns {boolean} Whether the activities tab is open. */ isActivitiesTabOpen(): boolean { - return this.currentTabHash === ( - AdminPageConstants.ADMIN_TAB_URLS.ACTIVITIES); + return this.currentTabHash === AdminPageConstants.ADMIN_TAB_URLS.ACTIVITIES; } /** @@ -69,7 +68,8 @@ export class AdminRouterService { isPlatformParamsTabOpen(): boolean { return ( this.currentTabHash === - AdminPageConstants.ADMIN_TAB_URLS.PLATFORM_PARAMETERS); + AdminPageConstants.ADMIN_TAB_URLS.PLATFORM_PARAMETERS + ); } /** @@ -94,5 +94,6 @@ export class AdminRouterService { } } -angular.module('oppia').factory( - 'AdminRouterService', downgradeInjectable(AdminRouterService)); +angular + .module('oppia') + .factory('AdminRouterService', downgradeInjectable(AdminRouterService)); diff --git a/core/templates/pages/admin-page/services/admin-task-manager.service.spec.ts b/core/templates/pages/admin-page/services/admin-task-manager.service.spec.ts index ed79c3305d1f..9eb1b2edcd2c 100644 --- a/core/templates/pages/admin-page/services/admin-task-manager.service.spec.ts +++ b/core/templates/pages/admin-page/services/admin-task-manager.service.spec.ts @@ -16,8 +16,7 @@ * @fileoverview Tests for AdminTaskManagerService. */ -import { AdminTaskManagerService } from - 'pages/admin-page/services/admin-task-manager.service'; +import {AdminTaskManagerService} from 'pages/admin-page/services/admin-task-manager.service'; describe('Admin task manager service', () => { let adminTaskManagerService: AdminTaskManagerService; diff --git a/core/templates/pages/admin-page/services/admin-task-manager.service.ts b/core/templates/pages/admin-page/services/admin-task-manager.service.ts index 257cd565ffd7..5c1252c0213a 100644 --- a/core/templates/pages/admin-page/services/admin-task-manager.service.ts +++ b/core/templates/pages/admin-page/services/admin-task-manager.service.ts @@ -17,11 +17,11 @@ * page. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class AdminTaskManagerService { static taskIsRunning: boolean = false; @@ -48,5 +48,9 @@ export class AdminTaskManagerService { } } -angular.module('oppia').factory( - 'AdminTaskManagerService', downgradeInjectable(AdminTaskManagerService)); +angular + .module('oppia') + .factory( + 'AdminTaskManagerService', + downgradeInjectable(AdminTaskManagerService) + ); diff --git a/core/templates/pages/android-page/android-page-root.component.spec.ts b/core/templates/pages/android-page/android-page-root.component.spec.ts index 5347ae1f9bcf..a70285cfeebe 100644 --- a/core/templates/pages/android-page/android-page-root.component.spec.ts +++ b/core/templates/pages/android-page/android-page-root.component.spec.ts @@ -16,14 +16,14 @@ * @fileoverview Unit tests for the Android page root component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { AndroidPageRootComponent } from './android-page-root.component'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {AndroidPageRootComponent} from './android-page-root.component'; describe('Android Page Root', () => { let fixture: ComponentFixture; @@ -32,15 +32,10 @@ describe('Android Page Root', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - AndroidPageRootComponent, - MockTranslatePipe - ], + declarations: [AndroidPageRootComponent, MockTranslatePipe], imports: [HttpClientTestingModule], - providers: [ - PageHeadService - ], - schemas: [NO_ERRORS_SCHEMA] + providers: [PageHeadService], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -56,14 +51,15 @@ describe('Android Page Root', () => { it('should initialize', () => { spyOn(pageHeadService, 'updateTitleAndMetaTags'); - const componentInstance = ( - TestBed.createComponent(AndroidPageRootComponent).componentInstance - ); + const componentInstance = TestBed.createComponent( + AndroidPageRootComponent + ).componentInstance; componentInstance.ngOnInit(); expect(pageHeadService.updateTitleAndMetaTags).toHaveBeenCalledWith( AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ANDROID.TITLE, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ANDROID.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ANDROID.META + ); }); }); diff --git a/core/templates/pages/android-page/android-page-root.component.ts b/core/templates/pages/android-page/android-page-root.component.ts index 6945d4a73704..5052212564ba 100644 --- a/core/templates/pages/android-page/android-page-root.component.ts +++ b/core/templates/pages/android-page/android-page-root.component.ts @@ -16,22 +16,21 @@ * @fileoverview Root Component for Android page. */ -import { Component } from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; +import {Component} from '@angular/core'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; @Component({ selector: 'oppia-android-page-root', - templateUrl: './android-page-root.component.html' + templateUrl: './android-page-root.component.html', }) export class AndroidPageRootComponent { - constructor( - private pageHeadService: PageHeadService, - ) {} + constructor(private pageHeadService: PageHeadService) {} ngOnInit(): void { this.pageHeadService.updateTitleAndMetaTags( AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ANDROID.TITLE, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ANDROID.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ANDROID.META + ); } } diff --git a/core/templates/pages/android-page/android-page-routing.module.ts b/core/templates/pages/android-page/android-page-routing.module.ts index 4cc4df455142..cbf8c53b3f7c 100644 --- a/core/templates/pages/android-page/android-page-routing.module.ts +++ b/core/templates/pages/android-page/android-page-routing.module.ts @@ -16,24 +16,19 @@ * @fileoverview Routing module for Android page. */ -import { NgModule } from '@angular/core'; -import { Route, RouterModule } from '@angular/router'; -import { AndroidPageRootComponent } from './android-page-root.component'; +import {NgModule} from '@angular/core'; +import {Route, RouterModule} from '@angular/router'; +import {AndroidPageRootComponent} from './android-page-root.component'; const routes: Route[] = [ { path: '', - component: AndroidPageRootComponent - } + component: AndroidPageRootComponent, + }, ]; @NgModule({ - imports: [ - RouterModule.forChild(routes) - ], - exports: [ - RouterModule - ] + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], }) - export class AndroidPageRoutingModule {} diff --git a/core/templates/pages/android-page/android-page.component.spec.ts b/core/templates/pages/android-page/android-page.component.spec.ts index d9d3f7424db9..bd370cdde398 100644 --- a/core/templates/pages/android-page/android-page.component.spec.ts +++ b/core/templates/pages/android-page/android-page.component.spec.ts @@ -16,18 +16,17 @@ * @fileoverview Unit tests for Android page. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { TestBed, fakeAsync, tick, flushMicrotasks } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; -import { MailingListBackendApiService } from 'domain/mailing-list/mailing-list-backend-api.service'; - -import { AndroidPageComponent } from './android-page.component'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { PageTitleService } from 'services/page-title.service'; -import { AlertsService } from 'services/alerts.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; - +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import {TestBed, fakeAsync, tick, flushMicrotasks} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; +import {MailingListBackendApiService} from 'domain/mailing-list/mailing-list-backend-api.service'; + +import {AndroidPageComponent} from './android-page.component'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {PageTitleService} from 'services/page-title.service'; +import {AlertsService} from 'services/alerts.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; class MockTranslateService { onLangChange: EventEmitter = new EventEmitter(); @@ -42,7 +41,7 @@ describe('Android page', () => { let mailingListBackendApiService: MailingListBackendApiService; let alertsService: AlertsService; - beforeEach(async() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], declarations: [AndroidPageComponent, MockTranslatePipe], @@ -53,30 +52,27 @@ describe('Android page', () => { PageTitleService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); let component: AndroidPageComponent; beforeEach(() => { - const androidPageComponent = TestBed.createComponent( - AndroidPageComponent); + const androidPageComponent = TestBed.createComponent(AndroidPageComponent); component = androidPageComponent.componentInstance; alertsService = TestBed.inject(AlertsService); - mailingListBackendApiService = TestBed.inject( - MailingListBackendApiService); + mailingListBackendApiService = TestBed.inject(MailingListBackendApiService); translateService = TestBed.inject(TranslateService); pageTitleService = TestBed.inject(PageTitleService); }); - it('should successfully instantiate the component from beforeEach block', - () => { - expect(component).toBeDefined(); - }); + it('should successfully instantiate the component from beforeEach block', () => { + expect(component).toBeDefined(); + }); it('should set component properties when ngOnInit() is called', () => { spyOn(translateService.onLangChange, 'subscribe'); @@ -94,15 +90,18 @@ describe('Android page', () => { expect(component.setPageTitle).toHaveBeenCalled(); }); - it('should obtain translated page title whenever the selected' + - 'language changes', () => { - component.ngOnInit(); - spyOn(component, 'setPageTitle'); + it( + 'should obtain translated page title whenever the selected' + + 'language changes', + () => { + component.ngOnInit(); + spyOn(component, 'setPageTitle'); - translateService.onLangChange.emit(); + translateService.onLangChange.emit(); - expect(component.setPageTitle).toHaveBeenCalled(); - }); + expect(component.setPageTitle).toHaveBeenCalled(); + } + ); it('should set new page title', () => { spyOn(translateService, 'instant').and.callThrough(); @@ -111,9 +110,11 @@ describe('Android page', () => { component.setPageTitle(); expect(translateService.instant).toHaveBeenCalledWith( - 'I18N_ANDROID_PAGE_TITLE'); + 'I18N_ANDROID_PAGE_TITLE' + ); expect(pageTitleService.setDocumentTitle).toHaveBeenCalledWith( - 'I18N_ANDROID_PAGE_TITLE'); + 'I18N_ANDROID_PAGE_TITLE' + ); }); it('should unsubscribe on component destruction', () => { @@ -145,59 +146,66 @@ describe('Android page', () => { expect(component.validateEmailAddress()).toBeTrue(); }); - it('should add user to android mailing list and return status', - fakeAsync(() => { - spyOn(alertsService, 'addInfoMessage'); - component.ngOnInit(); - tick(); - component.emailAddress = 'validEmail@example.com'; - component.name = 'validName'; - spyOn(mailingListBackendApiService, 'subscribeUserToMailingList') - .and.returnValue(Promise.resolve(true)); - component.userHasSubscribed = false; + it('should add user to android mailing list and return status', fakeAsync(() => { + spyOn(alertsService, 'addInfoMessage'); + component.ngOnInit(); + tick(); + component.emailAddress = 'validEmail@example.com'; + component.name = 'validName'; + spyOn( + mailingListBackendApiService, + 'subscribeUserToMailingList' + ).and.returnValue(Promise.resolve(true)); + component.userHasSubscribed = false; - component.subscribeToAndroidList(); + component.subscribeToAndroidList(); - flushMicrotasks(); + flushMicrotasks(); - expect(component.userHasSubscribed).toBeTrue(); - })); + expect(component.userHasSubscribed).toBeTrue(); + })); - it('should fail to add user to android mailing list and return status', - fakeAsync(() => { - spyOn(alertsService, 'addInfoMessage'); - component.ngOnInit(); - tick(); - component.emailAddress = 'validEmail@example.com'; - component.name = 'validName'; - spyOn(mailingListBackendApiService, 'subscribeUserToMailingList') - .and.returnValue(Promise.resolve(false)); + it('should fail to add user to android mailing list and return status', fakeAsync(() => { + spyOn(alertsService, 'addInfoMessage'); + component.ngOnInit(); + tick(); + component.emailAddress = 'validEmail@example.com'; + component.name = 'validName'; + spyOn( + mailingListBackendApiService, + 'subscribeUserToMailingList' + ).and.returnValue(Promise.resolve(false)); - component.subscribeToAndroidList(); + component.subscribeToAndroidList(); - flushMicrotasks(); + flushMicrotasks(); - expect(alertsService.addInfoMessage).toHaveBeenCalledWith( - 'Sorry, an unexpected error occurred. Please email admin@oppia.org ' + - 'to be added to the mailing list.', 10000); - })); + expect(alertsService.addInfoMessage).toHaveBeenCalledWith( + 'Sorry, an unexpected error occurred. Please email admin@oppia.org ' + + 'to be added to the mailing list.', + 10000 + ); + })); - it('should reject request to the android mailing list correctly', - fakeAsync(() => { - spyOn(alertsService, 'addInfoMessage'); - component.ngOnInit(); - tick(); - component.emailAddress = 'validEmail@example.com'; - component.name = 'validName'; - spyOn(mailingListBackendApiService, 'subscribeUserToMailingList') - .and.returnValue(Promise.reject(false)); + it('should reject request to the android mailing list correctly', fakeAsync(() => { + spyOn(alertsService, 'addInfoMessage'); + component.ngOnInit(); + tick(); + component.emailAddress = 'validEmail@example.com'; + component.name = 'validName'; + spyOn( + mailingListBackendApiService, + 'subscribeUserToMailingList' + ).and.returnValue(Promise.reject(false)); - component.subscribeToAndroidList(); + component.subscribeToAndroidList(); - flushMicrotasks(); + flushMicrotasks(); - expect(alertsService.addInfoMessage).toHaveBeenCalledWith( - 'Sorry, an unexpected error occurred. Please email admin@oppia.org ' + - 'to be added to the mailing list.', 10000); - })); + expect(alertsService.addInfoMessage).toHaveBeenCalledWith( + 'Sorry, an unexpected error occurred. Please email admin@oppia.org ' + + 'to be added to the mailing list.', + 10000 + ); + })); }); diff --git a/core/templates/pages/android-page/android-page.component.ts b/core/templates/pages/android-page/android-page.component.ts index 40e5263a330d..4f36f245a8bc 100644 --- a/core/templates/pages/android-page/android-page.component.ts +++ b/core/templates/pages/android-page/android-page.component.ts @@ -16,18 +16,30 @@ * @fileoverview Component for the Android page. */ -import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; -import { AlertsService } from 'services/alerts.service'; - -import { PageTitleService } from 'services/page-title.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { animate, keyframes, style, transition, trigger } from '@angular/animations'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { AppConstants } from 'app.constants'; -import { MailingListBackendApiService } from 'domain/mailing-list/mailing-list-backend-api.service'; +import { + Component, + OnInit, + OnDestroy, + ViewChild, + ElementRef, +} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; +import {AlertsService} from 'services/alerts.service'; + +import {PageTitleService} from 'services/page-title.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import { + animate, + keyframes, + style, + transition, + trigger, +} from '@angular/animations'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {AppConstants} from 'app.constants'; +import {MailingListBackendApiService} from 'domain/mailing-list/mailing-list-backend-api.service'; import './android-page.component.css'; @@ -38,30 +50,30 @@ import './android-page.component.css'; animations: [ trigger('fadeIn', [ transition(':enter', [ - style({ opacity: 0 }), - animate('1s ease', keyframes([ - style({ opacity: 0 }), - style({ opacity: 1 }) - ])) - ]) + style({opacity: 0}), + animate( + '1s ease', + keyframes([style({opacity: 0}), style({opacity: 1})]) + ), + ]), ]), trigger('delayedFadeIn', [ transition(':enter', [ - style({ opacity: 0 }), - animate('1s 1s ease', keyframes([ - style({ opacity: 0 }), - style({ opacity: 1 }) - ])) - ]) - ]) - ] + style({opacity: 0}), + animate( + '1s 1s ease', + keyframes([style({opacity: 0}), style({opacity: 1})]) + ), + ]), + ]), + ], }) export class AndroidPageComponent implements OnInit, OnDestroy { // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 - @ViewChild('androidUpdatesSection') androidUpdatesSectionRef!: ( - ElementRef); + @ViewChild('androidUpdatesSection') + androidUpdatesSectionRef!: ElementRef; @ViewChild('featuresMainText') featuresMainTextRef!: ElementRef; @@ -81,14 +93,13 @@ export class AndroidPageComponent implements OnInit, OnDestroy { userCanSubscribe: boolean = false; userHasSubscribed: boolean = false; - OPPIA_AVATAR_IMAGE_URL = ( - this.urlInterpolationService - .getStaticImageUrl('/avatar/oppia_avatar_large_100px.svg')); - - ANDROID_APP_URL = ( - 'https://play.google.com/store/apps/details?id=org.oppia.android' + OPPIA_AVATAR_IMAGE_URL = this.urlInterpolationService.getStaticImageUrl( + '/avatar/oppia_avatar_large_100px.svg' ); + ANDROID_APP_URL = + 'https://play.google.com/store/apps/details?id=org.oppia.android'; + directiveSubscriptions = new Subscription(); constructor( private alertsService: AlertsService, @@ -123,28 +134,36 @@ export class AndroidPageComponent implements OnInit, OnDestroy { } subscribeToAndroidList(): void { - this.mailingListBackendApiService.subscribeUserToMailingList( - String(this.emailAddress), - String(this.name), - AppConstants.MAILING_LIST_ANDROID_TAG - ).then((status) => { - if (status) { - this.userHasSubscribed = true; - } else { + this.mailingListBackendApiService + .subscribeUserToMailingList( + String(this.emailAddress), + String(this.name), + AppConstants.MAILING_LIST_ANDROID_TAG + ) + .then(status => { + if (status) { + this.userHasSubscribed = true; + } else { + this.alertsService.addInfoMessage( + 'Sorry, an unexpected error occurred. Please email admin@oppia.org ' + + 'to be added to the mailing list.', + 10000 + ); + } + }) + .catch(errorResponse => { this.alertsService.addInfoMessage( 'Sorry, an unexpected error occurred. Please email admin@oppia.org ' + - 'to be added to the mailing list.', 10000); - } - }).catch(errorResponse => { - this.alertsService.addInfoMessage( - 'Sorry, an unexpected error occurred. Please email admin@oppia.org ' + - 'to be added to the mailing list.', 10000); - }); + 'to be added to the mailing list.', + 10000 + ); + }); } setPageTitle(): void { let translatedTitle = this.translateService.instant( - 'I18N_ANDROID_PAGE_TITLE'); + 'I18N_ANDROID_PAGE_TITLE' + ); this.pageTitleService.setDocumentTitle(translatedTitle); } @@ -153,6 +172,9 @@ export class AndroidPageComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive( - 'androidPage', - downgradeComponent({component: AndroidPageComponent})); +angular + .module('oppia') + .directive( + 'androidPage', + downgradeComponent({component: AndroidPageComponent}) + ); diff --git a/core/templates/pages/android-page/android-page.module.ts b/core/templates/pages/android-page/android-page.module.ts index 46ebc0b084c5..19247f030a8d 100644 --- a/core/templates/pages/android-page/android-page.module.ts +++ b/core/templates/pages/android-page/android-page.module.ts @@ -16,15 +16,15 @@ * @fileoverview Module for the Android page. */ -import { HttpClientModule } from '@angular/common/http'; -import { NgModule } from '@angular/core'; -import { AndroidPageComponent } from './android-page.component'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { AndroidPageRootComponent } from './android-page-root.component'; -import { CommonModule } from '@angular/common'; -import { AndroidPageRoutingModule } from './android-page-routing.module'; -import { Error404PageModule } from 'pages/error-pages/error-404/error-404-page.module'; -import { I18nModule } from 'i18n/i18n.module'; +import {HttpClientModule} from '@angular/common/http'; +import {NgModule} from '@angular/core'; +import {AndroidPageComponent} from './android-page.component'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {AndroidPageRootComponent} from './android-page-root.component'; +import {CommonModule} from '@angular/common'; +import {AndroidPageRoutingModule} from './android-page-routing.module'; +import {Error404PageModule} from 'pages/error-pages/error-404/error-404-page.module'; +import {I18nModule} from 'i18n/i18n.module'; @NgModule({ imports: [ @@ -35,13 +35,7 @@ import { I18nModule } from 'i18n/i18n.module'; Error404PageModule, I18nModule, ], - declarations: [ - AndroidPageComponent, - AndroidPageRootComponent - ], - entryComponents: [ - AndroidPageComponent, - AndroidPageRootComponent - ] + declarations: [AndroidPageComponent, AndroidPageRootComponent], + entryComponents: [AndroidPageComponent, AndroidPageRootComponent], }) export class AndroidPageModule {} diff --git a/core/templates/pages/base-root.component.spec.ts b/core/templates/pages/base-root.component.spec.ts index 8f399ce96a19..bb346858ae33 100644 --- a/core/templates/pages/base-root.component.spec.ts +++ b/core/templates/pages/base-root.component.spec.ts @@ -16,21 +16,28 @@ * @fileoverview UnitTests for base root component. */ -import { EventEmitter } from '@angular/core'; -import { ComponentFixture, fakeAsync, flush, TestBed} from '@angular/core/testing'; -import { LangChangeEvent, TranslateModule, TranslateService } from '@ngx-translate/core'; -import { of, Subscription } from 'rxjs'; - -import { AppConstants } from 'app.constants'; -import { BaseRootComponent, MetaTagData } from './base-root.component'; -import { PageHeadService } from 'services/page-head.service'; +import {EventEmitter} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + flush, + TestBed, +} from '@angular/core/testing'; +import { + LangChangeEvent, + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; +import {of, Subscription} from 'rxjs'; +import {AppConstants} from 'app.constants'; +import {BaseRootComponent, MetaTagData} from './base-root.component'; +import {PageHeadService} from 'services/page-head.service'; class MockComponent extends BaseRootComponent { title: string = AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ADMIN.TITLE; - meta: MetaTagData[] = - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ADMIN.META as - unknown as Readonly[]; + meta: MetaTagData[] = AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ADMIN + .META as unknown as Readonly[]; } describe('Base root component', () => { @@ -43,10 +50,7 @@ describe('Base root component', () => { TestBed.configureTestingModule({ imports: [TranslateModule.forRoot()], declarations: [MockComponent], - providers: [ - PageHeadService, - TranslateService - ], + providers: [PageHeadService, TranslateService], }).compileComponents(); fixture = TestBed.createComponent(MockComponent); @@ -55,29 +59,40 @@ describe('Base root component', () => { translateServie = TestBed.inject(TranslateService); }); - it('should subscribe to language ' + - 'changes and update title and metadata', fakeAsync(() => { - const translatedTitle = 'translatedTitle'; - const instantSpy = spyOn( - translateServie, 'instant').and.returnValue(translatedTitle); - const updateTitleAndMetaTagsSpy = spyOn( - pageHeadService, 'updateTitleAndMetaTags'); - spyOnProperty(translateServie, 'onLangChange', 'get').and.returnValue( - of(null) as unknown as EventEmitter); + it( + 'should subscribe to language ' + 'changes and update title and metadata', + fakeAsync(() => { + const translatedTitle = 'translatedTitle'; + const instantSpy = spyOn(translateServie, 'instant').and.returnValue( + translatedTitle + ); + const updateTitleAndMetaTagsSpy = spyOn( + pageHeadService, + 'updateTitleAndMetaTags' + ); + spyOnProperty(translateServie, 'onLangChange', 'get').and.returnValue( + of(null) as unknown as EventEmitter + ); - component.ngOnInit(); - flush(); + component.ngOnInit(); + flush(); - expect(instantSpy).toHaveBeenCalledOnceWith( - component.title, component.titleInterpolationParams); - expect(updateTitleAndMetaTagsSpy).toHaveBeenCalledWith( - translatedTitle, component.meta); - })); + expect(instantSpy).toHaveBeenCalledOnceWith( + component.title, + component.titleInterpolationParams + ); + expect(updateTitleAndMetaTagsSpy).toHaveBeenCalledWith( + translatedTitle, + component.meta + ); + }) + ); it('should unsubscribe on destroy', () => { const mockUnsubscribe = jasmine.createSpy(); component.directiveSubscriptions = { - unsubscribe: mockUnsubscribe} as unknown as Subscription; + unsubscribe: mockUnsubscribe, + } as unknown as Subscription; component.ngOnDestroy(); diff --git a/core/templates/pages/base-root.component.ts b/core/templates/pages/base-root.component.ts index 9a15944d90b9..2ac771562131 100644 --- a/core/templates/pages/base-root.component.ts +++ b/core/templates/pages/base-root.component.ts @@ -16,11 +16,11 @@ * @fileoverview Base root component for all pages. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; -import { PageHeadService } from 'services/page-head.service'; +import {PageHeadService} from 'services/page-head.service'; export interface MetaTagData { readonly PROPERTY_TYPE: string; @@ -29,7 +29,7 @@ export interface MetaTagData { } @Component({ - template: '' + template: '', }) export abstract class BaseRootComponent implements OnInit, OnDestroy { directiveSubscriptions = new Subscription(); @@ -38,8 +38,8 @@ export abstract class BaseRootComponent implements OnInit, OnDestroy { constructor( protected pageHeadService: PageHeadService, - protected translateService: TranslateService, - ) { } + protected translateService: TranslateService + ) {} get titleInterpolationParams(): Object { return {}; @@ -48,10 +48,9 @@ export abstract class BaseRootComponent implements OnInit, OnDestroy { setPageTitleAndMetaTags(): void { const translatedTitle = this.translateService.instant( this.title, - this.titleInterpolationParams); - this.pageHeadService.updateTitleAndMetaTags( - translatedTitle, - this.meta); + this.titleInterpolationParams + ); + this.pageHeadService.updateTitleAndMetaTags(translatedTitle, this.meta); } ngOnInit(): void { diff --git a/core/templates/pages/blog-admin-page/blog-admin-auth.guard.spec.ts b/core/templates/pages/blog-admin-page/blog-admin-auth.guard.spec.ts index 6989b1af7f06..c1c5814fddf7 100644 --- a/core/templates/pages/blog-admin-page/blog-admin-auth.guard.spec.ts +++ b/core/templates/pages/blog-admin-page/blog-admin-auth.guard.spec.ts @@ -16,14 +16,19 @@ * @fileoverview Tests for BlogAdminAuthGuard */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TestBed } from '@angular/core/testing'; -import { ActivatedRouteSnapshot, Router, RouterStateSnapshot, NavigationExtras } from '@angular/router'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TestBed} from '@angular/core/testing'; +import { + ActivatedRouteSnapshot, + Router, + RouterStateSnapshot, + NavigationExtras, +} from '@angular/router'; -import { AppConstants } from 'app.constants'; -import { UserInfo } from 'domain/user/user-info.model'; -import { UserService } from 'services/user.service'; -import { BlogAdminAuthGuard } from './blog-admin-auth.guard'; +import {AppConstants} from 'app.constants'; +import {UserInfo} from 'domain/user/user-info.model'; +import {UserService} from 'services/user.service'; +import {BlogAdminAuthGuard} from './blog-admin-auth.guard'; class MockRouter { navigate(commands: string[], extras?: NavigationExtras): Promise { @@ -39,7 +44,7 @@ describe('BlogAdminAuthGuard', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [UserService, { provide: Router, useClass: MockRouter }], + providers: [UserService, {provide: Router, useClass: MockRouter}], }).compileComponents(); guard = TestBed.inject(BlogAdminAuthGuard); @@ -47,35 +52,50 @@ describe('BlogAdminAuthGuard', () => { router = TestBed.inject(Router); }); - it('should redirect user to 401 page if user is not blog admin', (done) => { + it('should redirect user to 401 page if user is not blog admin', done => { const getUserInfoAsyncSpy = spyOn( - userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(UserInfo.createDefault()) - ); + userService, + 'getUserInfoAsync' + ).and.returnValue(Promise.resolve(UserInfo.createDefault())); const navigateSpy = spyOn(router, 'navigate').and.callThrough(); - guard.canActivate( - new ActivatedRouteSnapshot(), {} as RouterStateSnapshot).then( - (canActivate) => { + guard + .canActivate(new ActivatedRouteSnapshot(), {} as RouterStateSnapshot) + .then(canActivate => { expect(canActivate).toBeFalse(); expect(getUserInfoAsyncSpy).toHaveBeenCalledTimes(1); expect(navigateSpy).toHaveBeenCalledWith([ - `${AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ERROR.ROUTE}/401`]); + `${AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ERROR.ROUTE}/401`, + ]); done(); }); }); - it('should not redirect user to 401 page if user is blog admin', (done) => { + it('should not redirect user to 401 page if user is blog admin', done => { const getUserInfoAsyncSpy = spyOn( - userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(new UserInfo( - ['BLOG_ADMIN'], false, false, false, false, false, '', '', '', true)) + userService, + 'getUserInfoAsync' + ).and.returnValue( + Promise.resolve( + new UserInfo( + ['BLOG_ADMIN'], + false, + false, + false, + false, + false, + '', + '', + '', + true + ) + ) ); const navigateSpy = spyOn(router, 'navigate').and.callThrough(); - guard.canActivate( - new ActivatedRouteSnapshot(), {} as RouterStateSnapshot).then( - (canActivate) => { + guard + .canActivate(new ActivatedRouteSnapshot(), {} as RouterStateSnapshot) + .then(canActivate => { expect(canActivate).toBeTrue(); expect(getUserInfoAsyncSpy).toHaveBeenCalledTimes(1); expect(navigateSpy).not.toHaveBeenCalled(); diff --git a/core/templates/pages/blog-admin-page/blog-admin-auth.guard.ts b/core/templates/pages/blog-admin-page/blog-admin-auth.guard.ts index b21b6267e26d..48701b863359 100644 --- a/core/templates/pages/blog-admin-page/blog-admin-auth.guard.ts +++ b/core/templates/pages/blog-admin-page/blog-admin-auth.guard.ts @@ -17,8 +17,8 @@ * if the user is not a blog admin. */ -import { Location } from '@angular/common'; -import { Injectable } from '@angular/core'; +import {Location} from '@angular/common'; +import {Injectable} from '@angular/core'; import { ActivatedRouteSnapshot, CanActivate, @@ -26,11 +26,11 @@ import { RouterStateSnapshot, } from '@angular/router'; -import { AppConstants } from 'app.constants'; -import { UserService } from 'services/user.service'; +import {AppConstants} from 'app.constants'; +import {UserService} from 'services/user.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class BlogAdminAuthGuard implements CanActivate { constructor( @@ -40,19 +40,21 @@ export class BlogAdminAuthGuard implements CanActivate { ) {} async canActivate( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot ): Promise { const userInfo = await this.userService.getUserInfoAsync(); if (userInfo.isBlogAdmin()) { return true; } - this.router.navigate( - [`${AppConstants.PAGES_REGISTERED_WITH_FRONTEND - .ERROR.ROUTE}/401`]).then(() => { - this.location.replaceState(state.url); - }); + this.router + .navigate([ + `${AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ERROR.ROUTE}/401`, + ]) + .then(() => { + this.location.replaceState(state.url); + }); return false; } } diff --git a/core/templates/pages/blog-admin-page/blog-admin-page-root.component.spec.ts b/core/templates/pages/blog-admin-page/blog-admin-page-root.component.spec.ts index 3988d8d5f7cf..f7dfe23c907e 100644 --- a/core/templates/pages/blog-admin-page/blog-admin-page-root.component.spec.ts +++ b/core/templates/pages/blog-admin-page/blog-admin-page-root.component.spec.ts @@ -16,14 +16,14 @@ * @fileoverview Unit tests for blog admin page root component. */ -import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { TranslateModule } from '@ngx-translate/core'; +import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {TranslateModule} from '@ngx-translate/core'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; -import { BlogAdminPageRootComponent } from './blog-admin-page-root.component'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; +import {BlogAdminPageRootComponent} from './blog-admin-page-root.component'; describe('BlogAdminPageRootComponent', () => { let fixture: ComponentFixture; @@ -43,8 +43,10 @@ describe('BlogAdminPageRootComponent', () => { it('should have the title and meta tags set', () => { expect(component.title).toEqual( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.BLOG_ADMIN.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.BLOG_ADMIN.TITLE + ); expect(component.meta).toEqual( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.BLOG_ADMIN.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.BLOG_ADMIN.META + ); }); }); diff --git a/core/templates/pages/blog-admin-page/blog-admin-page-root.component.ts b/core/templates/pages/blog-admin-page/blog-admin-page-root.component.ts index 63c31fa8b3f5..1726b047a80a 100644 --- a/core/templates/pages/blog-admin-page/blog-admin-page-root.component.ts +++ b/core/templates/pages/blog-admin-page/blog-admin-page-root.component.ts @@ -16,9 +16,9 @@ * @fileoverview Blog admin page root component. */ -import { Component } from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { BaseRootComponent, MetaTagData } from 'pages/base-root.component'; +import {Component} from '@angular/core'; +import {AppConstants} from 'app.constants'; +import {BaseRootComponent, MetaTagData} from 'pages/base-root.component'; @Component({ selector: 'oppia-blog-admin-page-root', @@ -26,7 +26,6 @@ import { BaseRootComponent, MetaTagData } from 'pages/base-root.component'; }) export class BlogAdminPageRootComponent extends BaseRootComponent { title: string = AppConstants.PAGES_REGISTERED_WITH_FRONTEND.BLOG_ADMIN.TITLE; - meta: MetaTagData[] = - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.BLOG_ADMIN.META as - unknown as Readonly[]; + meta: MetaTagData[] = AppConstants.PAGES_REGISTERED_WITH_FRONTEND.BLOG_ADMIN + .META as unknown as Readonly[]; } diff --git a/core/templates/pages/blog-admin-page/blog-admin-page.component.spec.ts b/core/templates/pages/blog-admin-page/blog-admin-page.component.spec.ts index 6dbf581a8545..0c55eed8e828 100644 --- a/core/templates/pages/blog-admin-page/blog-admin-page.component.spec.ts +++ b/core/templates/pages/blog-admin-page/blog-admin-page.component.spec.ts @@ -16,15 +16,24 @@ * @fileoverview Tests for Blog Admin tab component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, flushMicrotasks, TestBed, tick } from '@angular/core/testing'; -import { FormsModule } from '@angular/forms'; - -import { BlogAdminBackendApiService, BlogAdminPageData } from 'domain/blog-admin/blog-admin-backend-api.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { AdminTaskManagerService } from 'pages/admin-page/services/admin-task-manager.service'; -import { BlogAdminPageComponent } from 'pages/blog-admin-page/blog-admin-page.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + flushMicrotasks, + TestBed, + tick, +} from '@angular/core/testing'; +import {FormsModule} from '@angular/forms'; + +import { + BlogAdminBackendApiService, + BlogAdminPageData, +} from 'domain/blog-admin/blog-admin-backend-api.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {AdminTaskManagerService} from 'pages/admin-page/services/admin-task-manager.service'; +import {BlogAdminPageComponent} from 'pages/blog-admin-page/blog-admin-page.component'; class MockWindowRef { nativeWindow = { confirm() { @@ -35,11 +44,11 @@ class MockWindowRef { href: 'href', pathname: 'pathname', search: 'search', - hash: 'hash' + hash: 'hash', }, open() { return; - } + }, }; } @@ -57,37 +66,34 @@ describe('Blog Admin Page component ', () => { const blogAdminPageData: BlogAdminPageData = { roleToActions: { - blog_post_editor: ['action for editor'] + blog_post_editor: ['action for editor'], }, platformParameters: { max_number_of_tags_assigned_to_blog_post: { description: 'Max number of tags.', value: 10, - schema: {type: 'number'} - } + schema: {type: 'number'}, + }, }, updatableRoles: { - blog_post_editor: 'blog_post_editor' - } + blog_post_editor: 'blog_post_editor', + }, }; beforeEach(() => { mockWindowRef = new MockWindowRef(); TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - FormsModule - ], + imports: [HttpClientTestingModule, FormsModule], declarations: [BlogAdminPageComponent], providers: [ BlogAdminBackendApiService, AdminTaskManagerService, { provide: WindowRef, - useValue: mockWindowRef - } + useValue: mockWindowRef, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(BlogAdminPageComponent); @@ -100,8 +106,9 @@ describe('Blog Admin Page component ', () => { startTaskSpy = spyOn(adminTaskManagerService, 'startTask'); finishTaskSpy = spyOn(adminTaskManagerService, 'finishTask'); - spyOn(blogAdminBackendApiService, 'getDataAsync') - .and.resolveTo(blogAdminPageData); + spyOn(blogAdminBackendApiService, 'getDataAsync').and.resolveTo( + blogAdminPageData + ); confirmSpy = spyOn(mockWindowRef.nativeWindow, 'confirm'); }); @@ -116,19 +123,16 @@ describe('Blog Admin Page component ', () => { expect(component.formData.removeEditorRole.username).toBe(''); })); - it('should set correct values for properties when initialized', - fakeAsync(() => { - expect(component.UPDATABLE_ROLES).toEqual({}); - expect(component.roleToActions).toBe(undefined); + it('should set correct values for properties when initialized', fakeAsync(() => { + expect(component.UPDATABLE_ROLES).toEqual({}); + expect(component.roleToActions).toBe(undefined); - component.ngOnInit(); - tick(); + component.ngOnInit(); + tick(); - expect(component.UPDATABLE_ROLES).toEqual( - blogAdminPageData.updatableRoles); - expect(component.roleToActions).toEqual( - blogAdminPageData.roleToActions); - })); + expect(component.UPDATABLE_ROLES).toEqual(blogAdminPageData.updatableRoles); + expect(component.roleToActions).toEqual(blogAdminPageData.roleToActions); + })); it('should reload platform parameters when initialized', fakeAsync(() => { expect(component.platformParameters).toEqual({}); @@ -137,14 +141,17 @@ describe('Blog Admin Page component ', () => { tick(); expect(component.platformParameters).toEqual( - blogAdminPageData.platformParameters); + blogAdminPageData.platformParameters + ); })); - it('should return schema callback when calling ' + - '\'getSchemaCallback\'', () => { - let result = component.getSchemaCallback({type: 'bool'}); - expect(result()).toEqual({type: 'bool'}); - }); + it( + 'should return schema callback when calling ' + "'getSchemaCallback'", + () => { + let result = component.getSchemaCallback({type: 'bool'}); + expect(result()).toEqual({type: 'bool'}); + } + ); describe('when updating role of user ', () => { it('should submit update role form successfully', fakeAsync(() => { @@ -152,8 +159,9 @@ describe('Blog Admin Page component ', () => { tick(); component.formData.updateRole.newRole = 'BLOG_ADMIN'; component.formData.updateRole.username = 'username'; - spyOn(blogAdminBackendApiService, 'updateUserRoleAsync') - .and.returnValue(Promise.resolve()); + spyOn(blogAdminBackendApiService, 'updateUserRoleAsync').and.returnValue( + Promise.resolve() + ); spyOn(adminTaskManagerService, 'isTaskRunning').and.returnValue(false); component.submitUpdateRoleForm(component.formData.updateRole); @@ -163,79 +171,76 @@ describe('Blog Admin Page component ', () => { flushMicrotasks(); expect(component.statusMessage).toBe( - 'Role of username successfully updated to BLOG_ADMIN'); + 'Role of username successfully updated to BLOG_ADMIN' + ); expect(finishTaskSpy).toHaveBeenCalled(); })); - it('should not submit update role form if already a task is in queue', - fakeAsync(() => { - component.ngOnInit(); - tick(); - component.formData.updateRole.newRole = 'BLOG_ADMIN'; - component.formData.updateRole.username = 'username'; - spyOn(adminTaskManagerService, 'isTaskRunning').and.returnValue(true); + it('should not submit update role form if already a task is in queue', fakeAsync(() => { + component.ngOnInit(); + tick(); + component.formData.updateRole.newRole = 'BLOG_ADMIN'; + component.formData.updateRole.username = 'username'; + spyOn(adminTaskManagerService, 'isTaskRunning').and.returnValue(true); - component.submitUpdateRoleForm(component.formData.updateRole); + component.submitUpdateRoleForm(component.formData.updateRole); - expect(startTaskSpy).not.toHaveBeenCalled(); - expect(finishTaskSpy).not.toHaveBeenCalled(); - })); + expect(startTaskSpy).not.toHaveBeenCalled(); + expect(finishTaskSpy).not.toHaveBeenCalled(); + })); - it('should not submit update role form in case of backend error', - fakeAsync(() => { - component.ngOnInit(); - tick(); - component.formData.updateRole.newRole = 'BLOG_ADMIN'; - component.formData.updateRole.username = 'username'; - spyOn(blogAdminBackendApiService, 'updateUserRoleAsync') - .and.returnValue(Promise.reject('The user already has this role.')); - spyOn(adminTaskManagerService, 'isTaskRunning').and.returnValue(false); - component.submitUpdateRoleForm(component.formData.updateRole); + it('should not submit update role form in case of backend error', fakeAsync(() => { + component.ngOnInit(); + tick(); + component.formData.updateRole.newRole = 'BLOG_ADMIN'; + component.formData.updateRole.username = 'username'; + spyOn(blogAdminBackendApiService, 'updateUserRoleAsync').and.returnValue( + Promise.reject('The user already has this role.') + ); + spyOn(adminTaskManagerService, 'isTaskRunning').and.returnValue(false); + component.submitUpdateRoleForm(component.formData.updateRole); - expect(startTaskSpy).toHaveBeenCalled(); - expect(component.statusMessage).toBe('Updating User Role'); + expect(startTaskSpy).toHaveBeenCalled(); + expect(component.statusMessage).toBe('Updating User Role'); - flushMicrotasks(); + flushMicrotasks(); - expect(component.statusMessage).toBe( - 'The user already has this role.'); - expect(finishTaskSpy).toHaveBeenCalled(); - })); + expect(component.statusMessage).toBe('The user already has this role.'); + expect(finishTaskSpy).toHaveBeenCalled(); + })); - it('should not enable update role button if the input values are invalid', - fakeAsync(() => { - component.ngOnInit(); - tick(); - component.formData.updateRole.newRole = 'BLOG_ADMIN'; - component.formData.updateRole.username = ''; + it('should not enable update role button if the input values are invalid', fakeAsync(() => { + component.ngOnInit(); + tick(); + component.formData.updateRole.newRole = 'BLOG_ADMIN'; + component.formData.updateRole.username = ''; - expect(component.formData.updateRole.isValid()).toBe(false); + expect(component.formData.updateRole.isValid()).toBe(false); - component.formData.updateRole.newRole = 'BLOG'; - component.formData.updateRole.username = 'username'; + component.formData.updateRole.newRole = 'BLOG'; + component.formData.updateRole.username = 'username'; - expect(component.formData.updateRole.isValid()).toBe(false); + expect(component.formData.updateRole.isValid()).toBe(false); - component.formData.updateRole.newRole = 'ADMIN'; - component.formData.updateRole.newRole = ''; + component.formData.updateRole.newRole = 'ADMIN'; + component.formData.updateRole.newRole = ''; - expect(component.formData.updateRole.isValid()).toBe(false); + expect(component.formData.updateRole.isValid()).toBe(false); - component.formData.updateRole.newRole = null; - component.formData.updateRole.username = 'username'; + component.formData.updateRole.newRole = null; + component.formData.updateRole.username = 'username'; - expect(component.formData.updateRole.isValid()).toBe(false); - })); + expect(component.formData.updateRole.isValid()).toBe(false); + })); - it('should enable update role button if the input values are valid', - fakeAsync(() => { - component.ngOnInit(); - tick(); - component.formData.updateRole.newRole = 'BLOG_POST_EDITOR'; - component.formData.updateRole.username = 'username'; + it('should enable update role button if the input values are valid', fakeAsync(() => { + component.ngOnInit(); + tick(); + component.formData.updateRole.newRole = 'BLOG_POST_EDITOR'; + component.formData.updateRole.username = 'username'; - expect(component.formData.updateRole.isValid()).toBe(true); - })); + expect(component.formData.updateRole.isValid()).toBe(true); + })); }); describe('when removing a blog editor', () => { @@ -243,8 +248,10 @@ describe('Blog Admin Page component ', () => { component.ngOnInit(); tick(); component.formData.removeEditorRole.username = 'username'; - spyOn(blogAdminBackendApiService, 'removeBlogEditorAsync') - .and.returnValue(Promise.resolve(Object)); + spyOn( + blogAdminBackendApiService, + 'removeBlogEditorAsync' + ).and.returnValue(Promise.resolve(Object)); spyOn(adminTaskManagerService, 'isTaskRunning').and.returnValue(false); component.submitRemoveEditorRoleForm(component.formData.updateRole); @@ -257,114 +264,135 @@ describe('Blog Admin Page component ', () => { expect(finishTaskSpy).toHaveBeenCalled(); })); - it('should not submit remove editor role form if already' + - 'a task is in queue', fakeAsync(() => { - component.ngOnInit(); - tick(); - component.formData.removeEditorRole.username = 'username'; - spyOn(adminTaskManagerService, 'isTaskRunning').and.returnValue(true); - - component.submitRemoveEditorRoleForm(component.formData.updateRole); - - expect(startTaskSpy).not.toHaveBeenCalled(); - expect(finishTaskSpy).not.toHaveBeenCalled(); - })); - - it('should not submit remove editor role form in case of backend error', + it( + 'should not submit remove editor role form if already' + + 'a task is in queue', fakeAsync(() => { component.ngOnInit(); tick(); component.formData.removeEditorRole.username = 'username'; - spyOn(blogAdminBackendApiService, 'removeBlogEditorAsync') - .and.returnValue(Promise.reject({ - error: { error: 'Internal Server Error.'} - })); - spyOn(adminTaskManagerService, 'isTaskRunning').and.returnValue(false); + spyOn(adminTaskManagerService, 'isTaskRunning').and.returnValue(true); component.submitRemoveEditorRoleForm(component.formData.updateRole); - expect(startTaskSpy).toHaveBeenCalled(); - expect(component.statusMessage).toBe('Processing query...'); + expect(startTaskSpy).not.toHaveBeenCalled(); + expect(finishTaskSpy).not.toHaveBeenCalled(); + }) + ); - flushMicrotasks(); + it('should not submit remove editor role form in case of backend error', fakeAsync(() => { + component.ngOnInit(); + tick(); + component.formData.removeEditorRole.username = 'username'; + spyOn( + blogAdminBackendApiService, + 'removeBlogEditorAsync' + ).and.returnValue( + Promise.reject({ + error: {error: 'Internal Server Error.'}, + }) + ); + spyOn(adminTaskManagerService, 'isTaskRunning').and.returnValue(false); - expect(component.statusMessage).toBe( - 'Server error: Internal Server Error.'); - expect(finishTaskSpy).toHaveBeenCalled(); - })); + component.submitRemoveEditorRoleForm(component.formData.updateRole); - it('should not enable remove role button if the input values are invalid', - fakeAsync(() => { - component.ngOnInit(); - tick(); - component.formData.removeEditorRole.username = ''; + expect(startTaskSpy).toHaveBeenCalled(); + expect(component.statusMessage).toBe('Processing query...'); - expect(component.formData.removeEditorRole.isValid()).toBe(false); - })); + flushMicrotasks(); - it('should enable remove role button if the input values are valid', - fakeAsync(() => { - component.ngOnInit(); - tick(); - component.formData.removeEditorRole.username = 'username'; + expect(component.statusMessage).toBe( + 'Server error: Internal Server Error.' + ); + expect(finishTaskSpy).toHaveBeenCalled(); + })); - expect(component.formData.removeEditorRole.isValid()).toBe(true); - })); - }); + it('should not enable remove role button if the input values are invalid', fakeAsync(() => { + component.ngOnInit(); + tick(); + component.formData.removeEditorRole.username = ''; - describe('when clicking on save button ', () => { - it('should save platform parameters successfully', fakeAsync(() => { - // Setting confirm button clicked to be true. - confirmSpy.and.returnValue(true); - spyOn(blogAdminBackendApiService, 'savePlatformParametersAsync') - .and.returnValue(Promise.resolve()); + expect(component.formData.removeEditorRole.isValid()).toBe(false); + })); - component.platformParameters = blogAdminPageData.platformParameters; - component.savePlatformParameters(); + it('should enable remove role button if the input values are valid', fakeAsync(() => { + component.ngOnInit(); tick(); + component.formData.removeEditorRole.username = 'username'; - expect(component.statusMessage).toBe( - 'Data saved successfully.'); + expect(component.formData.removeEditorRole.isValid()).toBe(true); })); + }); - it('should not save platform parameters ' + - 'in case of backend error', fakeAsync(() => { + describe('when clicking on save button ', () => { + it('should save platform parameters successfully', fakeAsync(() => { // Setting confirm button clicked to be true. confirmSpy.and.returnValue(true); - spyOn(blogAdminBackendApiService, 'savePlatformParametersAsync') - .and.returnValue(Promise.reject('Internal Server Error.')); + spyOn( + blogAdminBackendApiService, + 'savePlatformParametersAsync' + ).and.returnValue(Promise.resolve()); + component.platformParameters = blogAdminPageData.platformParameters; component.savePlatformParameters(); tick(); - expect(component.statusMessage).toBe( - 'Server error: Internal Server Error.'); + expect(component.statusMessage).toBe('Data saved successfully.'); })); - it('should not save platform parameters ' + - 'if a task is still running in the queue', fakeAsync(() => { - // Setting task is still running to be true. - spyOn(adminTaskManagerService, 'isTaskRunning').and.returnValue(true); - let saveConfigSpy = spyOn( - blogAdminBackendApiService, 'savePlatformParametersAsync'); + it( + 'should not save platform parameters ' + 'in case of backend error', + fakeAsync(() => { + // Setting confirm button clicked to be true. + confirmSpy.and.returnValue(true); + spyOn( + blogAdminBackendApiService, + 'savePlatformParametersAsync' + ).and.returnValue(Promise.reject('Internal Server Error.')); + + component.savePlatformParameters(); + tick(); - component.savePlatformParameters(); - tick(); + expect(component.statusMessage).toBe( + 'Server error: Internal Server Error.' + ); + }) + ); + + it( + 'should not save platform parameters ' + + 'if a task is still running in the queue', + fakeAsync(() => { + // Setting task is still running to be true. + spyOn(adminTaskManagerService, 'isTaskRunning').and.returnValue(true); + let saveConfigSpy = spyOn( + blogAdminBackendApiService, + 'savePlatformParametersAsync' + ); - expect(saveConfigSpy).not.toHaveBeenCalled(); - })); + component.savePlatformParameters(); + tick(); - it('should not request backend to save platform parameters ' + - 'if cancel button is clicked in the alert', fakeAsync(() => { - // Setting confirm button clicked to be false. - confirmSpy.and.returnValue(false); - let saveConfigSpy = spyOn( - blogAdminBackendApiService, 'savePlatformParametersAsync'); + expect(saveConfigSpy).not.toHaveBeenCalled(); + }) + ); - component.savePlatformParameters(); - tick(); + it( + 'should not request backend to save platform parameters ' + + 'if cancel button is clicked in the alert', + fakeAsync(() => { + // Setting confirm button clicked to be false. + confirmSpy.and.returnValue(false); + let saveConfigSpy = spyOn( + blogAdminBackendApiService, + 'savePlatformParametersAsync' + ); + + component.savePlatformParameters(); + tick(); - expect(saveConfigSpy).not.toHaveBeenCalled(); - })); + expect(saveConfigSpy).not.toHaveBeenCalled(); + }) + ); }); }); diff --git a/core/templates/pages/blog-admin-page/blog-admin-page.component.ts b/core/templates/pages/blog-admin-page/blog-admin-page.component.ts index 1f214863a533..655e5d626b8c 100644 --- a/core/templates/pages/blog-admin-page/blog-admin-page.component.ts +++ b/core/templates/pages/blog-admin-page/blog-admin-page.component.ts @@ -16,14 +16,17 @@ * @fileoverview Component for the blog admin page. */ -import { Component, OnInit } from '@angular/core'; -import { BlogAdminBackendApiService, PlatformParameterBackendResponse, PlatformParameterValues } - from 'domain/blog-admin/blog-admin-backend-api.service'; -import { BlogAdminDataService } from 'pages/blog-admin-page/services/blog-admin-data.service'; -import { AdminTaskManagerService } from 'pages/admin-page/services/admin-task-manager.service'; -import { Schema } from 'services/schema-default-value.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { RoleToActionsBackendResponse } from 'domain/admin/admin-backend-api.service'; +import {Component, OnInit} from '@angular/core'; +import { + BlogAdminBackendApiService, + PlatformParameterBackendResponse, + PlatformParameterValues, +} from 'domain/blog-admin/blog-admin-backend-api.service'; +import {BlogAdminDataService} from 'pages/blog-admin-page/services/blog-admin-data.service'; +import {AdminTaskManagerService} from 'pages/admin-page/services/admin-task-manager.service'; +import {Schema} from 'services/schema-default-value.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {RoleToActionsBackendResponse} from 'domain/admin/admin-backend-api.service'; interface UpdateRoleAction { // 'newRole' is 'null' when the form is refreshed. @@ -42,8 +45,10 @@ interface FormData { removeEditorRole: RemoveEditorRole; } -type PlatformParameterValuesRecord = ( - Record); +type PlatformParameterValuesRecord = Record< + keyof PlatformParameterValues, + string[] | number +>; @Component({ selector: 'oppia-blog-admin-page', @@ -62,12 +67,12 @@ export class BlogAdminPageComponent implements OnInit { private backendApiService: BlogAdminBackendApiService, private blogAdminDataService: BlogAdminDataService, private adminTaskManagerService: AdminTaskManagerService, - private windowRef: WindowRef, + private windowRef: WindowRef ) {} ngOnInit(): void { this.refreshFormData(); - this.blogAdminDataService.getDataAsync().then((DataObject) => { + this.blogAdminDataService.getDataAsync().then(DataObject => { this.UPDATABLE_ROLES = DataObject.updatableRoles; this.roleToActions = DataObject.roleToActions; }); @@ -86,7 +91,7 @@ export class BlogAdminPageComponent implements OnInit { return Boolean(this.username); } return false; - } + }, }, removeEditorRole: { username: '', @@ -95,8 +100,8 @@ export class BlogAdminPageComponent implements OnInit { return false; } return true; - } - } + }, + }, }; } @@ -106,42 +111,50 @@ export class BlogAdminPageComponent implements OnInit { } this.statusMessage = 'Updating User Role'; this.adminTaskManagerService.startTask(); - this.backendApiService.updateUserRoleAsync( - // Update role button will not be enabled if 'newRole' is 'null'. - // hence whenever this method is called 'newRole' will exist and - // we can safely typecast it to a 'string'. - formResponse.newRole as string, formResponse.username, - ).then(() => { - this.statusMessage = ( - 'Role of ' + formResponse.username + ' successfully updated to ' + - formResponse.newRole); - this.refreshFormData(); - }, errorResponse => { - this.statusMessage = errorResponse; - }); + this.backendApiService + .updateUserRoleAsync( + // Update role button will not be enabled if 'newRole' is 'null'. + // hence whenever this method is called 'newRole' will exist and + // we can safely typecast it to a 'string'. + formResponse.newRole as string, + formResponse.username + ) + .then( + () => { + this.statusMessage = + 'Role of ' + + formResponse.username + + ' successfully updated to ' + + formResponse.newRole; + this.refreshFormData(); + }, + errorResponse => { + this.statusMessage = errorResponse; + } + ); this.adminTaskManagerService.finishTask(); } - submitRemoveEditorRoleForm( - formResponse: RemoveEditorRole): void { + submitRemoveEditorRoleForm(formResponse: RemoveEditorRole): void { if (this.adminTaskManagerService.isTaskRunning()) { return; } this.statusMessage = 'Processing query...'; this.adminTaskManagerService.startTask(); - this.backendApiService.removeBlogEditorAsync( - formResponse.username - ).then(() => { - this.statusMessage = 'Success.'; - this.refreshFormData(); - }, error => { - this.statusMessage = 'Server error: ' + error.error.error; - }); + this.backendApiService.removeBlogEditorAsync(formResponse.username).then( + () => { + this.statusMessage = 'Success.'; + this.refreshFormData(); + }, + error => { + this.statusMessage = 'Server error: ' + error.error.error; + } + ); this.adminTaskManagerService.finishTask(); } reloadPlatformParameters(): void { - this.blogAdminDataService.getDataAsync().then((DataObject) => { + this.blogAdminDataService.getDataAsync().then(DataObject => { this.platformParameters = DataObject.platformParameters; }); } @@ -156,8 +169,11 @@ export class BlogAdminPageComponent implements OnInit { if (this.adminTaskManagerService.isTaskRunning()) { return; } - if (!this.windowRef.nativeWindow.confirm( - 'This action is irreversible. Are you sure?')) { + if ( + !this.windowRef.nativeWindow.confirm( + 'This action is irreversible. Are you sure?' + ) + ) { return; } @@ -170,13 +186,19 @@ export class BlogAdminPageComponent implements OnInit { newPlatformParameterValues[prop] = this.platformParameters[prop].value; } - this.backendApiService.savePlatformParametersAsync( - newPlatformParameterValues as PlatformParameterValues).then(() => { - this.statusMessage = 'Data saved successfully.'; - this.adminTaskManagerService.finishTask(); - }, errorResponse => { - this.statusMessage = 'Server error: ' + errorResponse; - this.adminTaskManagerService.finishTask(); - }); + this.backendApiService + .savePlatformParametersAsync( + newPlatformParameterValues as PlatformParameterValues + ) + .then( + () => { + this.statusMessage = 'Data saved successfully.'; + this.adminTaskManagerService.finishTask(); + }, + errorResponse => { + this.statusMessage = 'Server error: ' + errorResponse; + this.adminTaskManagerService.finishTask(); + } + ); } } diff --git a/core/templates/pages/blog-admin-page/blog-admin-page.import.ts b/core/templates/pages/blog-admin-page/blog-admin-page.import.ts index 67a51126c371..ae0bcb9b1ac4 100644 --- a/core/templates/pages/blog-admin-page/blog-admin-page.import.ts +++ b/core/templates/pages/blog-admin-page/blog-admin-page.import.ts @@ -22,9 +22,14 @@ import 'zone.js'; import uiValidate from 'angular-ui-validate'; angular.module('oppia', [ - require('angular-cookies'), 'ngAnimate', - 'ngMaterial', 'ngSanitize', 'ngTouch', 'pascalprecht.translate', - 'ui.bootstrap', uiValidate + require('angular-cookies'), + 'ngAnimate', + 'ngMaterial', + 'ngSanitize', + 'ngTouch', + 'pascalprecht.translate', + 'ui.bootstrap', + uiValidate, ]); require('Polyfills.ts'); diff --git a/core/templates/pages/blog-admin-page/blog-admin-page.module.ts b/core/templates/pages/blog-admin-page/blog-admin-page.module.ts index 8b2da1d0be0b..f10eb776b250 100644 --- a/core/templates/pages/blog-admin-page/blog-admin-page.module.ts +++ b/core/templates/pages/blog-admin-page/blog-admin-page.module.ts @@ -16,19 +16,19 @@ * @fileoverview Module for the blog-admin page. */ -import { NgModule } from '@angular/core'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { MatCardModule } from '@angular/material/card'; -import { RouterModule } from '@angular/router'; -import { CommonModule } from '@angular/common'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { BlogAdminNavbarComponent } from 'pages/blog-admin-page/navbar/blog-admin-navbar.component'; -import { ToastrModule } from 'ngx-toastr'; -import { toastrConfig } from 'pages/oppia-root/app.module'; -import { AdminBlogAdminCommonModule } from 'pages/admin-page/admin-blog-admin-common.module'; -import { BlogAdminPageComponent } from './blog-admin-page.component'; -import { BlogAdminPageRootComponent } from './blog-admin-page-root.component'; -import { BlogAdminAuthGuard } from './blog-admin-auth.guard'; +import {NgModule} from '@angular/core'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {MatCardModule} from '@angular/material/card'; +import {RouterModule} from '@angular/router'; +import {CommonModule} from '@angular/common'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {BlogAdminNavbarComponent} from 'pages/blog-admin-page/navbar/blog-admin-navbar.component'; +import {ToastrModule} from 'ngx-toastr'; +import {toastrConfig} from 'pages/oppia-root/app.module'; +import {AdminBlogAdminCommonModule} from 'pages/admin-page/admin-blog-admin-common.module'; +import {BlogAdminPageComponent} from './blog-admin-page.component'; +import {BlogAdminPageRootComponent} from './blog-admin-page-root.component'; +import {BlogAdminAuthGuard} from './blog-admin-auth.guard'; @NgModule({ imports: [ @@ -50,11 +50,8 @@ import { BlogAdminAuthGuard } from './blog-admin-auth.guard'; declarations: [ BlogAdminNavbarComponent, BlogAdminPageComponent, - BlogAdminPageRootComponent - ], - entryComponents: [ - BlogAdminNavbarComponent, - BlogAdminPageComponent + BlogAdminPageRootComponent, ], + entryComponents: [BlogAdminNavbarComponent, BlogAdminPageComponent], }) export class BlogAdminPageModule {} diff --git a/core/templates/pages/blog-admin-page/navbar/blog-admin-navbar.component.spec.ts b/core/templates/pages/blog-admin-page/navbar/blog-admin-navbar.component.spec.ts index c698c053f5e6..a25ae68b3451 100644 --- a/core/templates/pages/blog-admin-page/navbar/blog-admin-navbar.component.spec.ts +++ b/core/templates/pages/blog-admin-page/navbar/blog-admin-navbar.component.spec.ts @@ -16,16 +16,20 @@ * @fileoverview Unit tests for blog admin navbar component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { RouterModule } from '@angular/router'; -import { APP_BASE_HREF } from '@angular/common'; - -import { UserService } from 'services/user.service'; -import { BlogAdminNavbarComponent } from 'pages/blog-admin-page/navbar/blog-admin-navbar.component'; -import { SmartRouterModule } from 'hybrid-router-module-provider'; -import { UserInfo } from 'domain/user/user-info.model'; - +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {RouterModule} from '@angular/router'; +import {APP_BASE_HREF} from '@angular/common'; + +import {UserService} from 'services/user.service'; +import {BlogAdminNavbarComponent} from 'pages/blog-admin-page/navbar/blog-admin-navbar.component'; +import {SmartRouterModule} from 'hybrid-router-module-provider'; +import {UserInfo} from 'domain/user/user-info.model'; describe('Blog Admin navbar component', () => { let component: BlogAdminNavbarComponent; @@ -34,7 +38,7 @@ describe('Blog Admin navbar component', () => { let userProfileWebpImage = 'path-to-webp-profile-pic'; let userInfo = { getUsername: () => 'username1', - isSuperAdmin: () => true + isSuperAdmin: () => true, }; let profileUrl = '/profile/username1'; let fixture: ComponentFixture; @@ -45,13 +49,15 @@ describe('Blog Admin navbar component', () => { // TODO(#13443): Remove hybrid router module provider once all pages are // migrated to angular router. SmartRouterModule, - RouterModule.forRoot([]) + RouterModule.forRoot([]), ], declarations: [BlogAdminNavbarComponent], - providers: [{ - provide: APP_BASE_HREF, - useValue: '/' - }] + providers: [ + { + provide: APP_BASE_HREF, + useValue: '/', + }, + ], }).compileComponents(); fixture = TestBed.createComponent(BlogAdminNavbarComponent); @@ -59,13 +65,14 @@ describe('Blog Admin navbar component', () => { userService = TestBed.inject(UserService); fixture.detectChanges(); - spyOn(userService, 'getProfileImageDataUrl') - .and.returnValue([userProfilePngImage, userProfileWebpImage]); + spyOn(userService, 'getProfileImageDataUrl').and.returnValue([ + userProfilePngImage, + userProfileWebpImage, + ]); })); it('should initialize component properties correctly', fakeAsync(() => { - spyOn(userService, 'getUserInfoAsync') - .and.resolveTo(userInfo as UserInfo); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo as UserInfo); component.ngOnInit(); tick(); @@ -79,10 +86,9 @@ describe('Blog Admin navbar component', () => { it('should throw error if username is invalid', fakeAsync(() => { let userInfo = { getUsername: () => null, - isSuperAdmin: () => true + isSuperAdmin: () => true, }; - spyOn(userService, 'getUserInfoAsync') - .and.resolveTo(userInfo as UserInfo); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo as UserInfo); expect(() => { component.ngOnInit(); @@ -91,8 +97,7 @@ describe('Blog Admin navbar component', () => { })); it('should set profileDropdownIsActive to true', fakeAsync(() => { - spyOn(userService, 'getUserInfoAsync') - .and.resolveTo(userInfo as UserInfo); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo as UserInfo); component.ngOnInit(); tick(); @@ -105,8 +110,7 @@ describe('Blog Admin navbar component', () => { })); it('should set profileDropdownIsActive to false', fakeAsync(() => { - spyOn(userService, 'getUserInfoAsync') - .and.resolveTo(userInfo as UserInfo); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo as UserInfo); component.ngOnInit(); tick(); diff --git a/core/templates/pages/blog-admin-page/navbar/blog-admin-navbar.component.ts b/core/templates/pages/blog-admin-page/navbar/blog-admin-navbar.component.ts index 6c903f109c90..5ac1cbf72112 100644 --- a/core/templates/pages/blog-admin-page/navbar/blog-admin-navbar.component.ts +++ b/core/templates/pages/blog-admin-page/navbar/blog-admin-navbar.component.ts @@ -17,11 +17,10 @@ * panel. */ -import { Component, OnInit } from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { UserService } from 'services/user.service'; - +import {Component, OnInit} from '@angular/core'; +import {AppConstants} from 'app.constants'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {UserService} from 'services/user.service'; @Component({ selector: 'oppia-blog-admin-navbar', @@ -37,22 +36,21 @@ export class BlogAdminNavbarComponent implements OnInit { username!: string | null; logoWebpImageSrc!: string; logoPngImageSrc!: string; - PAGES_REGISTERED_WITH_FRONTEND = ( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND); + PAGES_REGISTERED_WITH_FRONTEND = AppConstants.PAGES_REGISTERED_WITH_FRONTEND; profileDropdownIsActive: boolean = false; constructor( private urlInterpolationService: UrlInterpolationService, - private userService: UserService, + private userService: UserService ) {} activateProfileDropdown(): boolean { - return this.profileDropdownIsActive = true; + return (this.profileDropdownIsActive = true); } deactivateProfileDropdown(): boolean { - return this.profileDropdownIsActive = false; + return (this.profileDropdownIsActive = false); } async getUserInfoAsync(): Promise { @@ -62,21 +60,24 @@ export class BlogAdminNavbarComponent implements OnInit { if (this.username === null) { throw new Error('Cannot fetch username.'); } - this.profileUrl = ( - this.urlInterpolationService.interpolateUrl( - '/profile/', { - username: this.username - })); - [this.profilePicturePngDataUrl, this.profilePictureWebpDataUrl] = ( - this.userService.getProfileImageDataUrl(this.username)); + this.profileUrl = this.urlInterpolationService.interpolateUrl( + '/profile/', + { + username: this.username, + } + ); + [this.profilePicturePngDataUrl, this.profilePictureWebpDataUrl] = + this.userService.getProfileImageDataUrl(this.username); } ngOnInit(): void { this.getUserInfoAsync(); this.logoPngImageSrc = this.urlInterpolationService.getStaticImageUrl( - '/logo/288x128_logo_white.png'); + '/logo/288x128_logo_white.png' + ); this.logoWebpImageSrc = this.urlInterpolationService.getStaticImageUrl( - '/logo/288x128_logo_white.webp'); + '/logo/288x128_logo_white.webp' + ); } } diff --git a/core/templates/pages/blog-admin-page/services/blog-admin-data.service.spec.ts b/core/templates/pages/blog-admin-page/services/blog-admin-data.service.spec.ts index 56358f93703a..27c4b5df4d43 100644 --- a/core/templates/pages/blog-admin-page/services/blog-admin-data.service.spec.ts +++ b/core/templates/pages/blog-admin-page/services/blog-admin-data.service.spec.ts @@ -16,36 +16,42 @@ * @fileoverview Tests for BlogAdminDataService. */ -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; -import { BlogAdminDataService } from 'pages/blog-admin-page/services/blog-admin-data.service'; -import { BlogAdminPageData, BlogAdminPageDataBackendDict } from 'domain/blog-admin/blog-admin-backend-api.service'; +import {BlogAdminDataService} from 'pages/blog-admin-page/services/blog-admin-data.service'; +import { + BlogAdminPageData, + BlogAdminPageDataBackendDict, +} from 'domain/blog-admin/blog-admin-backend-api.service'; describe('Blog Admin Data Service', () => { let blogAdminDataService: BlogAdminDataService; let httpTestingController: HttpTestingController; let sampleBlogAdminData: BlogAdminPageDataBackendDict = { role_to_actions: { - blog_post_editor: ['action for editor'] + blog_post_editor: ['action for editor'], }, platform_parameters: { max_number_of_tags_assigned_to_blog_post: { description: 'Max number of tags.', value: 10, - schema: {type: 'number'} - } + schema: {type: 'number'}, + }, }, updatable_roles: { - blog_post_editor: 'blog_post_editor' - } + blog_post_editor: 'blog_post_editor', + }, }; let blogAdminDataResponse: BlogAdminPageData; beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [BlogAdminDataService] + providers: [BlogAdminDataService], }); blogAdminDataService = TestBed.inject(BlogAdminDataService); httpTestingController = TestBed.inject(HttpTestingController); @@ -61,12 +67,11 @@ describe('Blog Admin Data Service', () => { }); it('should return the correct blog admin data', fakeAsync(() => { - blogAdminDataService.getDataAsync().then((response) => { + blogAdminDataService.getDataAsync().then(response => { expect(response).toEqual(blogAdminDataResponse); }); - var req = httpTestingController.expectOne( - '/blogadminhandler'); + var req = httpTestingController.expectOne('/blogadminhandler'); expect(req.request.method).toEqual('GET'); req.flush(sampleBlogAdminData); diff --git a/core/templates/pages/blog-admin-page/services/blog-admin-data.service.ts b/core/templates/pages/blog-admin-page/services/blog-admin-data.service.ts index ac80bb91f111..214af301bfca 100644 --- a/core/templates/pages/blog-admin-page/services/blog-admin-data.service.ts +++ b/core/templates/pages/blog-admin-page/services/blog-admin-data.service.ts @@ -16,13 +16,16 @@ * @fileoverview Service that manages blog admin data. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { BlogAdminPageData, BlogAdminBackendApiService } from 'domain/blog-admin/blog-admin-backend-api.service'; +import { + BlogAdminPageData, + BlogAdminBackendApiService, +} from 'domain/blog-admin/blog-admin-backend-api.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class BlogAdminDataService { // This property is initialized using private methods @@ -30,8 +33,7 @@ export class BlogAdminDataService { // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 dataPromise!: Promise; - constructor( - private BackendApiService: BlogAdminBackendApiService) {} + constructor(private BackendApiService: BlogAdminBackendApiService) {} async _getDataAsync(): Promise { this.dataPromise = this.BackendApiService.getDataAsync(); @@ -44,6 +46,6 @@ export class BlogAdminDataService { } } -angular.module('oppia').factory( - 'BlogAdminDataService', - downgradeInjectable(BlogAdminDataService)); +angular + .module('oppia') + .factory('BlogAdminDataService', downgradeInjectable(BlogAdminDataService)); diff --git a/core/templates/pages/blog-author-profile-page/blog-author-profile-page-root.component.spec.ts b/core/templates/pages/blog-author-profile-page/blog-author-profile-page-root.component.spec.ts index feac5b2af266..1d6df1d05665 100644 --- a/core/templates/pages/blog-author-profile-page/blog-author-profile-page-root.component.spec.ts +++ b/core/templates/pages/blog-author-profile-page/blog-author-profile-page-root.component.spec.ts @@ -16,19 +16,25 @@ * @fileoverview Unit tests for the blog author profile page root component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; -import { AppConstants } from 'app.constants'; -import { AccessValidationBackendApiService } from 'pages/oppia-root/routing/access-validation-backend-api.service'; -import { MetaTagCustomizationService } from 'services/contextual/meta-tag-customization.service'; -import { UrlService } from 'services/contextual/url.service'; -import { LoaderService } from 'services/loader.service'; -import { PageHeadService } from 'services/page-head.service'; -import { UserService } from 'services/user.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { BlogAuthorProfilePageRootComponent } from './blog-author-profile-page-root.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; +import {AppConstants} from 'app.constants'; +import {AccessValidationBackendApiService} from 'pages/oppia-root/routing/access-validation-backend-api.service'; +import {MetaTagCustomizationService} from 'services/contextual/meta-tag-customization.service'; +import {UrlService} from 'services/contextual/url.service'; +import {LoaderService} from 'services/loader.service'; +import {PageHeadService} from 'services/page-head.service'; +import {UserService} from 'services/user.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {BlogAuthorProfilePageRootComponent} from './blog-author-profile-page-root.component'; class MockTranslateService { onLangChange: EventEmitter = new EventEmitter(); @@ -49,13 +55,8 @@ describe('Blog Author Profile Page Root', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - BlogAuthorProfilePageRootComponent, - MockTranslatePipe - ], + imports: [HttpClientTestingModule], + declarations: [BlogAuthorProfilePageRootComponent, MockTranslatePipe], providers: [ PageHeadService, MetaTagCustomizationService, @@ -63,10 +64,10 @@ describe('Blog Author Profile Page Root', () => { UserService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -76,77 +77,79 @@ describe('Blog Author Profile Page Root', () => { pageHeadService = TestBed.inject(PageHeadService); loaderService = TestBed.inject(LoaderService); accessValidationBackendApiService = TestBed.inject( - AccessValidationBackendApiService); + AccessValidationBackendApiService + ); userService = TestBed.inject(UserService); translateService = TestBed.inject(TranslateService); urlService = TestBed.inject(UrlService); - spyOn(urlService, 'getBlogAuthorUsernameFromUrl') - .and.returnValue('author'); + spyOn(urlService, 'getBlogAuthorUsernameFromUrl').and.returnValue('author'); }); - it('should successfully instantiate the component', - () => { - expect(component).toBeDefined(); - }); - - it('should initialize and show page when access is valid and blog project ' + - 'feature is enabled', fakeAsync(() => { - spyOn(userService, 'canUserEditBlogPosts').and.returnValue( - Promise.resolve(false)); - spyOn( - accessValidationBackendApiService, - 'validateAccessToBlogAuthorProfilePage' - ).and.returnValue(Promise.resolve()); - spyOn(loaderService, 'showLoadingScreen'); - spyOn(loaderService, 'hideLoadingScreen'); - - component.ngOnInit(); - // We first call asynchronous function userService.canUserEditBlogPosts to - // check if the user can edit blog posts and then validate access to - // page using another asynchronous function - // accessValidationBackendApiService.validateAccessToBlogAuthorProfilePage. - // Therefore we require 2 ticks here. - tick(); - tick(); - - expect(loaderService.showLoadingScreen).toHaveBeenCalled(); - expect( - accessValidationBackendApiService.validateAccessToBlogAuthorProfilePage) - .toHaveBeenCalledWith('author'); - expect(component.pageIsShown).toBeTrue(); - expect(component.errorPageIsShown).toBeFalse(); - expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); - })); + it('should successfully instantiate the component', () => { + expect(component).toBeDefined(); + }); - it('should initialize and show error page when server respond with error', + it( + 'should initialize and show page when access is valid and blog project ' + + 'feature is enabled', fakeAsync(() => { spyOn(userService, 'canUserEditBlogPosts').and.returnValue( - Promise.resolve(false)); + Promise.resolve(false) + ); spyOn( accessValidationBackendApiService, 'validateAccessToBlogAuthorProfilePage' - ).and.returnValue(Promise.reject()); + ).and.returnValue(Promise.resolve()); spyOn(loaderService, 'showLoadingScreen'); spyOn(loaderService, 'hideLoadingScreen'); component.ngOnInit(); // We first call asynchronous function userService.canUserEditBlogPosts to - // check if the user can edit blog posts and then validate access to page - // using another asynchronous function in - // validateAccessToBlogAuthorProfilePage in - // AccessValidationBackendApiService. Therefore we require 2 ticks here. + // check if the user can edit blog posts and then validate access to + // page using another asynchronous function + // accessValidationBackendApiService.validateAccessToBlogAuthorProfilePage. + // Therefore we require 2 ticks here. tick(); tick(); expect(loaderService.showLoadingScreen).toHaveBeenCalled(); expect( - accessValidationBackendApiService - .validateAccessToBlogAuthorProfilePage) - .toHaveBeenCalled(); - expect(component.pageIsShown).toBeFalse(); - expect(component.errorPageIsShown).toBeTrue(); + accessValidationBackendApiService.validateAccessToBlogAuthorProfilePage + ).toHaveBeenCalledWith('author'); + expect(component.pageIsShown).toBeTrue(); + expect(component.errorPageIsShown).toBeFalse(); expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); - })); + }) + ); + + it('should initialize and show error page when server respond with error', fakeAsync(() => { + spyOn(userService, 'canUserEditBlogPosts').and.returnValue( + Promise.resolve(false) + ); + spyOn( + accessValidationBackendApiService, + 'validateAccessToBlogAuthorProfilePage' + ).and.returnValue(Promise.reject()); + spyOn(loaderService, 'showLoadingScreen'); + spyOn(loaderService, 'hideLoadingScreen'); + + component.ngOnInit(); + // We first call asynchronous function userService.canUserEditBlogPosts to + // check if the user can edit blog posts and then validate access to page + // using another asynchronous function in + // validateAccessToBlogAuthorProfilePage in + // AccessValidationBackendApiService. Therefore we require 2 ticks here. + tick(); + tick(); + + expect(loaderService.showLoadingScreen).toHaveBeenCalled(); + expect( + accessValidationBackendApiService.validateAccessToBlogAuthorProfilePage + ).toHaveBeenCalled(); + expect(component.pageIsShown).toBeFalse(); + expect(component.errorPageIsShown).toBeTrue(); + expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); + })); it('should initialize and subscribe to onLangChange', fakeAsync(() => { spyOn( @@ -186,8 +189,8 @@ describe('Blog Author Profile Page Root', () => { AppConstants.PAGES_REGISTERED_WITH_FRONTEND.BLOG_AUTHOR_PROFILE_PAGE.TITLE ); expect(pageHeadService.updateTitleAndMetaTags).toHaveBeenCalledWith( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND - .BLOG_AUTHOR_PROFILE_PAGE.TITLE, + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.BLOG_AUTHOR_PROFILE_PAGE + .TITLE, AppConstants.PAGES_REGISTERED_WITH_FRONTEND.BLOG_AUTHOR_PROFILE_PAGE.META ); }); diff --git a/core/templates/pages/blog-author-profile-page/blog-author-profile-page-root.component.ts b/core/templates/pages/blog-author-profile-page/blog-author-profile-page-root.component.ts index 19de53eb5e26..4cc6e90eb8b7 100644 --- a/core/templates/pages/blog-author-profile-page/blog-author-profile-page-root.component.ts +++ b/core/templates/pages/blog-author-profile-page/blog-author-profile-page-root.component.ts @@ -16,19 +16,19 @@ * @fileoverview Root component for blog author profile page. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; -import { AppConstants } from 'app.constants'; -import { AccessValidationBackendApiService } from 'pages/oppia-root/routing/access-validation-backend-api.service'; -import { LoaderService } from 'services/loader.service'; -import { PageHeadService } from 'services/page-head.service'; -import { UrlService } from 'services/contextual/url.service'; -import { UserService } from 'services/user.service'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; +import {AppConstants} from 'app.constants'; +import {AccessValidationBackendApiService} from 'pages/oppia-root/routing/access-validation-backend-api.service'; +import {LoaderService} from 'services/loader.service'; +import {PageHeadService} from 'services/page-head.service'; +import {UrlService} from 'services/contextual/url.service'; +import {UserService} from 'services/user.service'; @Component({ selector: 'oppia-blog-author-profile-page-root', - templateUrl: './blog-author-profile-page-root.component.html' + templateUrl: './blog-author-profile-page-root.component.html', }) export class BlogAuthorProfilePageRootComponent implements OnDestroy, OnInit { directiveSubscriptions = new Subscription(); @@ -37,13 +37,12 @@ export class BlogAuthorProfilePageRootComponent implements OnDestroy, OnInit { authorUsername!: string; constructor( - private accessValidationBackendApiService: - AccessValidationBackendApiService, + private accessValidationBackendApiService: AccessValidationBackendApiService, private loaderService: LoaderService, private pageHeadService: PageHeadService, private translateService: TranslateService, private urlService: UrlService, - private userService: UserService, + private userService: UserService ) {} ngOnInit(): void { @@ -57,14 +56,18 @@ export class BlogAuthorProfilePageRootComponent implements OnDestroy, OnInit { this.authorUsername = this.urlService.getBlogAuthorUsernameFromUrl(); this.loaderService.showLoadingScreen('Loading'); - this.userService.canUserEditBlogPosts().then((userCanEditBlogPost) => { + this.userService.canUserEditBlogPosts().then(userCanEditBlogPost => { this.accessValidationBackendApiService .validateAccessToBlogAuthorProfilePage(this.authorUsername) + .then( + () => { + this.pageIsShown = true; + }, + () => { + this.errorPageIsShown = true; + } + ) .then(() => { - this.pageIsShown = true; - }, () => { - this.errorPageIsShown = true; - }).then(() => { this.loaderService.hideLoadingScreen(); }); }); @@ -74,9 +77,12 @@ export class BlogAuthorProfilePageRootComponent implements OnDestroy, OnInit { const blogAuthorProfilePage = AppConstants.PAGES_REGISTERED_WITH_FRONTEND.BLOG_AUTHOR_PROFILE_PAGE; const translatedTitle = this.translateService.instant( - blogAuthorProfilePage.TITLE); + blogAuthorProfilePage.TITLE + ); this.pageHeadService.updateTitleAndMetaTags( - translatedTitle, blogAuthorProfilePage.META); + translatedTitle, + blogAuthorProfilePage.META + ); } ngOnDestroy(): void { diff --git a/core/templates/pages/blog-author-profile-page/blog-author-profile-page-routing.module.ts b/core/templates/pages/blog-author-profile-page/blog-author-profile-page-routing.module.ts index 4155bff9e3ee..2a3e76f34136 100644 --- a/core/templates/pages/blog-author-profile-page/blog-author-profile-page-routing.module.ts +++ b/core/templates/pages/blog-author-profile-page/blog-author-profile-page-routing.module.ts @@ -16,24 +16,19 @@ * @fileoverview Routing module for blog author page. */ -import { NgModule } from '@angular/core'; -import { Route, RouterModule } from '@angular/router'; -import { BlogAuthorProfilePageRootComponent } from './blog-author-profile-page-root.component'; +import {NgModule} from '@angular/core'; +import {Route, RouterModule} from '@angular/router'; +import {BlogAuthorProfilePageRootComponent} from './blog-author-profile-page-root.component'; const routes: Route[] = [ { path: '', - component: BlogAuthorProfilePageRootComponent - } + component: BlogAuthorProfilePageRootComponent, + }, ]; @NgModule({ - imports: [ - RouterModule.forChild(routes) - ], - exports: [ - RouterModule - ] + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], }) - export class BlogAuthorProfilePageRoutingModule {} diff --git a/core/templates/pages/blog-author-profile-page/blog-author-profile-page.component.spec.ts b/core/templates/pages/blog-author-profile-page/blog-author-profile-page.component.spec.ts index d59ba1f6ec1c..e4af5a9054a2 100644 --- a/core/templates/pages/blog-author-profile-page/blog-author-profile-page.component.spec.ts +++ b/core/templates/pages/blog-author-profile-page/blog-author-profile-page.component.spec.ts @@ -16,27 +16,39 @@ * @fileoverview Unit tests for the Blog Author Profile Page component. */ -import { Pipe } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync} from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { MaterialModule } from 'modules/material.module'; -import { FormsModule } from '@angular/forms'; -import { BlogAuthorProfilePageComponent } from 'pages/blog-author-profile-page/blog-author-profile-page.component'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { LoaderService } from 'services/loader.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { BlogAuthorProfilePageData, BlogHomePageBackendApiService } from 'domain/blog/blog-homepage-backend-api.service'; -import { UrlService } from 'services/contextual/url.service'; -import { BlogCardComponent } from 'pages/blog-dashboard-page/blog-card/blog-card.component'; -import { BlogAuthorProfilePageConstants } from './blog-author-profile-page.constants'; -import { BlogPostSummary, BlogPostSummaryBackendDict } from 'domain/blog/blog-post-summary.model'; -import { AlertsService } from 'services/alerts.service'; -import { UserService } from 'services/user.service'; +import {Pipe} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; +import {MaterialModule} from 'modules/material.module'; +import {FormsModule} from '@angular/forms'; +import {BlogAuthorProfilePageComponent} from 'pages/blog-author-profile-page/blog-author-profile-page.component'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {LoaderService} from 'services/loader.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import { + BlogAuthorProfilePageData, + BlogHomePageBackendApiService, +} from 'domain/blog/blog-homepage-backend-api.service'; +import {UrlService} from 'services/contextual/url.service'; +import {BlogCardComponent} from 'pages/blog-dashboard-page/blog-card/blog-card.component'; +import {BlogAuthorProfilePageConstants} from './blog-author-profile-page.constants'; +import { + BlogPostSummary, + BlogPostSummaryBackendDict, +} from 'domain/blog/blog-post-summary.model'; +import {AlertsService} from 'services/alerts.service'; +import {UserService} from 'services/user.service'; // This throws "TS2307". We need to // suppress this error because rte-text-components are not strictly typed yet. // @ts-ignore -import { RichTextComponentsModule } from 'rich_text_components/rich-text-components.module'; +import {RichTextComponentsModule} from 'rich_text_components/rich-text-components.module'; @Pipe({name: 'truncate'}) class MockTruncatePipe { @@ -89,14 +101,14 @@ describe('Blog home page component', () => { BlogAuthorProfilePageComponent, BlogCardComponent, MockTranslatePipe, - MockTruncatePipe + MockTruncatePipe, ], providers: [ { provide: WindowDimensionsService, - useClass: MockWindowDimensionsService + useClass: MockWindowDimensionsService, }, - LoaderService + LoaderService, ], }).compileComponents(); })); @@ -107,13 +119,13 @@ describe('Blog home page component', () => { windowDimensionsService = TestBed.inject(WindowDimensionsService); alertsService = TestBed.inject(AlertsService); blogHomePageBackendApiService = TestBed.inject( - BlogHomePageBackendApiService); + BlogHomePageBackendApiService + ); urlService = TestBed.inject(UrlService); loaderService = TestBed.inject(LoaderService); userService = TestBed.inject(UserService); - blogPostSummaryObject = BlogPostSummary.createFromBackendDict( - blogPostSummary - ); + blogPostSummaryObject = + BlogPostSummary.createFromBackendDict(blogPostSummary); blogAuthorProfilePageDataObject = { displayedAuthorName: 'author', authorBio: 'author Bio', @@ -123,10 +135,10 @@ describe('Blog home page component', () => { spyOn(loaderService, 'showLoadingScreen'); spyOn(loaderService, 'hideLoadingScreen'); spyOn(urlService, 'getBlogAuthorUsernameFromUrl').and.returnValue( - 'authorUsername'); + 'authorUsername' + ); }); - it('should initialize', () => { spyOn(component, 'loadInitialBlogAuthorProfilePageData'); @@ -135,105 +147,118 @@ describe('Blog home page component', () => { expect(loaderService.showLoadingScreen).toHaveBeenCalled(); expect(component.authorUsername).toBe('authorUsername'); expect(component.MAX_NUM_CARD_TO_DISPLAY_ON_PAGE).toBe( - BlogAuthorProfilePageConstants - .MAX_NUM_CARDS_TO_DISPLAY_ON_BLOG_AUTHOR_PROFILE_PAGE + BlogAuthorProfilePageConstants.MAX_NUM_CARDS_TO_DISPLAY_ON_BLOG_AUTHOR_PROFILE_PAGE ); expect(component.loadInitialBlogAuthorProfilePageData).toHaveBeenCalled(); }); - it('should load blog author profile page data with no published blog post ' + - 'summary', fakeAsync(() => { - spyOn(blogHomePageBackendApiService, 'fetchBlogAuthorProfilePageDataAsync') - .and.returnValue(Promise.resolve(blogAuthorProfilePageDataObject)); - - expect(component.noResultsFound).toBeUndefined(); - - component.ngOnInit(); - component.loadInitialBlogAuthorProfilePageData(); - - expect(blogHomePageBackendApiService.fetchBlogAuthorProfilePageDataAsync) - .toHaveBeenCalledWith('authorUsername', '0'); - - tick(); - expect(component.noResultsFound).toBeTrue(); - - expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); - })); - - it('should load blog author profile page data with published blog post ' + - 'summary', fakeAsync(() => { - blogAuthorProfilePageDataObject.blogPostSummaries = [blogPostSummaryObject]; - blogAuthorProfilePageDataObject.numOfBlogPostSummaries = 1; + it( + 'should load blog author profile page data with no published blog post ' + + 'summary', + fakeAsync(() => { + spyOn( + blogHomePageBackendApiService, + 'fetchBlogAuthorProfilePageDataAsync' + ).and.returnValue(Promise.resolve(blogAuthorProfilePageDataObject)); - spyOn(blogHomePageBackendApiService, 'fetchBlogAuthorProfilePageDataAsync') - .and.returnValue(Promise.resolve(blogAuthorProfilePageDataObject)); - spyOn(userService, 'getProfileImageDataUrl').and.returnValue( - ['default-image-url-png', 'default-image-url-webp']); + expect(component.noResultsFound).toBeUndefined(); - component.ngOnInit(); - component.loadInitialBlogAuthorProfilePageData(); + component.ngOnInit(); + component.loadInitialBlogAuthorProfilePageData(); - expect(blogHomePageBackendApiService.fetchBlogAuthorProfilePageDataAsync) - .toHaveBeenCalledWith('authorUsername', '0'); + expect( + blogHomePageBackendApiService.fetchBlogAuthorProfilePageDataAsync + ).toHaveBeenCalledWith('authorUsername', '0'); - tick(); + tick(); + expect(component.noResultsFound).toBeTrue(); - expect(component.noResultsFound).toBeFalse(); - expect(component.totalBlogPosts).toBe(1); - expect(component.authorName).toBe('author'); - expect(component.authorBio).toBe('author Bio'); - expect(component.blogPostSummaries).toEqual([blogPostSummaryObject]); - expect(component.authorProfilePicPngUrl).toBe('default-image-url-png'); - expect(component.authorProfilePicWebpUrl).toBe('default-image-url-webp'); - expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); - })); + expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); + }) + ); - it('should succesfully load multiple blog home pages data', + it( + 'should load blog author profile page data with published blog post ' + + 'summary', fakeAsync(() => { - component.MAX_NUM_CARD_TO_DISPLAY_ON_PAGE = 1; - component.authorUsername = 'authorUsername'; - blogAuthorProfilePageDataObject.numOfBlogPostSummaries = 3; blogAuthorProfilePageDataObject.blogPostSummaries = [ - blogPostSummaryObject + blogPostSummaryObject, ]; - spyOn(alertsService, 'addWarning'); + blogAuthorProfilePageDataObject.numOfBlogPostSummaries = 1; + spyOn( blogHomePageBackendApiService, 'fetchBlogAuthorProfilePageDataAsync' ).and.returnValue(Promise.resolve(blogAuthorProfilePageDataObject)); + spyOn(userService, 'getProfileImageDataUrl').and.returnValue([ + 'default-image-url-png', + 'default-image-url-webp', + ]); + component.ngOnInit(); component.loadInitialBlogAuthorProfilePageData(); + + expect( + blogHomePageBackendApiService.fetchBlogAuthorProfilePageDataAsync + ).toHaveBeenCalledWith('authorUsername', '0'); + tick(); - expect(component.totalBlogPosts).toBe(3); expect(component.noResultsFound).toBeFalse(); + expect(component.totalBlogPosts).toBe(1); + expect(component.authorName).toBe('author'); + expect(component.authorBio).toBe('author Bio'); expect(component.blogPostSummaries).toEqual([blogPostSummaryObject]); - expect(component.blogPostSummariesToShow).toEqual( - [blogPostSummaryObject]); - expect(component.lastPostOnPageNum).toBe(1); + expect(component.authorProfilePicPngUrl).toBe('default-image-url-png'); + expect(component.authorProfilePicWebpUrl).toBe('default-image-url-webp'); + expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); + }) + ); + + it('should succesfully load multiple blog home pages data', fakeAsync(() => { + component.MAX_NUM_CARD_TO_DISPLAY_ON_PAGE = 1; + component.authorUsername = 'authorUsername'; + blogAuthorProfilePageDataObject.numOfBlogPostSummaries = 3; + blogAuthorProfilePageDataObject.blogPostSummaries = [blogPostSummaryObject]; + spyOn(alertsService, 'addWarning'); + spyOn( + blogHomePageBackendApiService, + 'fetchBlogAuthorProfilePageDataAsync' + ).and.returnValue(Promise.resolve(blogAuthorProfilePageDataObject)); - component.page = 2; - component.loadMoreBlogPostSummaries(1); - tick(); + component.loadInitialBlogAuthorProfilePageData(); + tick(); - expect(blogHomePageBackendApiService.fetchBlogAuthorProfilePageDataAsync) - .toHaveBeenCalledWith('authorUsername', '1'); - expect(component.totalBlogPosts).toBe(3); - expect(component.blogPostSummaries).toEqual( - [blogPostSummaryObject, blogPostSummaryObject]); - expect(component.blogPostSummariesToShow).toEqual( - [blogPostSummaryObject]); - expect(component.lastPostOnPageNum).toBe(2); + expect(component.totalBlogPosts).toBe(3); + expect(component.noResultsFound).toBeFalse(); + expect(component.blogPostSummaries).toEqual([blogPostSummaryObject]); + expect(component.blogPostSummariesToShow).toEqual([blogPostSummaryObject]); + expect(component.lastPostOnPageNum).toBe(1); - expect(alertsService.addWarning).not.toHaveBeenCalled(); - })); + component.page = 2; + component.loadMoreBlogPostSummaries(1); + tick(); + + expect( + blogHomePageBackendApiService.fetchBlogAuthorProfilePageDataAsync + ).toHaveBeenCalledWith('authorUsername', '1'); + expect(component.totalBlogPosts).toBe(3); + expect(component.blogPostSummaries).toEqual([ + blogPostSummaryObject, + blogPostSummaryObject, + ]); + expect(component.blogPostSummariesToShow).toEqual([blogPostSummaryObject]); + expect(component.lastPostOnPageNum).toBe(2); + + expect(alertsService.addWarning).not.toHaveBeenCalled(); + })); it('should load data for page on changing page', () => { spyOn(component, 'loadMoreBlogPostSummaries'); component.totalBlogPosts = 5; component.blogPostSummaries = [ blogPostSummaryObject, - blogPostSummaryObject + blogPostSummaryObject, ]; component.ngOnInit(); component.MAX_NUM_CARD_TO_DISPLAY_ON_PAGE = 2; @@ -254,7 +279,7 @@ describe('Blog home page component', () => { // Adding blog post summaries for page 2. component.blogPostSummaries = component.blogPostSummaries.concat([ blogPostSummaryObject, - blogPostSummaryObject + blogPostSummaryObject, ]); component.showBlogPostCardsLoadingScreen = false; @@ -269,7 +294,7 @@ describe('Blog home page component', () => { expect(component.lastPostOnPageNum).toBe(5); // Adding blog post summaries for page 3. component.blogPostSummaries = component.blogPostSummaries.concat([ - blogPostSummaryObject + blogPostSummaryObject, ]); component.showBlogPostCardsLoadingScreen = false; @@ -286,53 +311,67 @@ describe('Blog home page component', () => { expect(component.blogPostSummariesToShow.length).toBe(2); }); - it('should use reject handler if fetching blog home page data fails', + it('should use reject handler if fetching blog home page data fails', fakeAsync(() => { + spyOn(alertsService, 'addWarning'); + spyOn( + blogHomePageBackendApiService, + 'fetchBlogAuthorProfilePageDataAsync' + ).and.returnValue( + Promise.reject({ + error: {error: 'Backend error'}, + status: 500, + }) + ); + component.authorUsername = 'authorUsername'; + + component.loadInitialBlogAuthorProfilePageData(); + + expect( + blogHomePageBackendApiService.fetchBlogAuthorProfilePageDataAsync + ).toHaveBeenCalledWith('authorUsername', '0'); + + tick(); + + expect(alertsService.addWarning).toHaveBeenCalledWith( + 'Failed to get blog author profile page data.Error: Backend error' + ); + })); + + it( + 'should use reject handler if fetching data for loading more published' + + 'blog post fails', fakeAsync(() => { spyOn(alertsService, 'addWarning'); spyOn( blogHomePageBackendApiService, 'fetchBlogAuthorProfilePageDataAsync' - ).and.returnValue(Promise.reject({ - error: {error: 'Backend error'}, - status: 500 - })); - component.authorUsername = 'authorUsername'; + ).and.returnValue( + Promise.reject({ + error: {error: 'Backend error'}, + status: 500, + }) + ); - component.loadInitialBlogAuthorProfilePageData(); + component.authorUsername = 'authorUsername'; + component.loadMoreBlogPostSummaries(1); - expect(blogHomePageBackendApiService.fetchBlogAuthorProfilePageDataAsync) - .toHaveBeenCalledWith('authorUsername', '0'); + expect( + blogHomePageBackendApiService.fetchBlogAuthorProfilePageDataAsync + ).toHaveBeenCalledWith('authorUsername', '1'); tick(); expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Failed to get blog author profile page data.Error: Backend error'); - })); - - it('should use reject handler if fetching data for loading more published' + - 'blog post fails', fakeAsync(() => { - spyOn(alertsService, 'addWarning'); - spyOn(blogHomePageBackendApiService, 'fetchBlogAuthorProfilePageDataAsync') - .and.returnValue(Promise.reject({ - error: {error: 'Backend error'}, - status: 500 - })); - - component.authorUsername = 'authorUsername'; - component.loadMoreBlogPostSummaries(1); - - expect(blogHomePageBackendApiService.fetchBlogAuthorProfilePageDataAsync) - .toHaveBeenCalledWith('authorUsername', '1'); - - tick(); - - expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Failed to get blog author page data.Error: Backend error'); - })); + 'Failed to get blog author page data.Error: Backend error' + ); + }) + ); it('should determine if small screen view is active', () => { - const windowWidthSpy = - spyOn(windowDimensionsService, 'getWidth').and.returnValue(766); + const windowWidthSpy = spyOn( + windowDimensionsService, + 'getWidth' + ).and.returnValue(766); expect(component.isSmallScreenViewActive()).toBe(true); windowWidthSpy.and.returnValue(1028); expect(component.isSmallScreenViewActive()).toBe(false); diff --git a/core/templates/pages/blog-author-profile-page/blog-author-profile-page.component.ts b/core/templates/pages/blog-author-profile-page/blog-author-profile-page.component.ts index cededb5e2835..8cd45bf623ce 100755 --- a/core/templates/pages/blog-author-profile-page/blog-author-profile-page.component.ts +++ b/core/templates/pages/blog-author-profile-page/blog-author-profile-page.component.ts @@ -16,22 +16,25 @@ * @fileoverview Data and component for the blog post page. */ -import { Component, OnInit } from '@angular/core'; -import { BlogPostSummary } from 'domain/blog/blog-post-summary.model'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { BlogAuthorProfilePageData, BlogHomePageBackendApiService } from 'domain/blog/blog-homepage-backend-api.service'; -import { BlogAuthorProfilePageConstants } from './blog-author-profile-page.constants'; -import { LoaderService } from 'services/loader.service'; -import { UrlService } from 'services/contextual/url.service'; -import { AlertsService } from 'services/alerts.service'; -import { AppConstants } from 'app.constants'; -import { UserService } from 'services/user.service'; +import {Component, OnInit} from '@angular/core'; +import {BlogPostSummary} from 'domain/blog/blog-post-summary.model'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import { + BlogAuthorProfilePageData, + BlogHomePageBackendApiService, +} from 'domain/blog/blog-homepage-backend-api.service'; +import {BlogAuthorProfilePageConstants} from './blog-author-profile-page.constants'; +import {LoaderService} from 'services/loader.service'; +import {UrlService} from 'services/contextual/url.service'; +import {AlertsService} from 'services/alerts.service'; +import {AppConstants} from 'app.constants'; +import {UserService} from 'services/user.service'; import './blog-author-profile-page.component.css'; @Component({ selector: 'oppia-blog-author-page', - templateUrl: './blog-author-profile-page.component.html' + templateUrl: './blog-author-profile-page.component.html', }) export class BlogAuthorProfilePageComponent implements OnInit { // These properties are initialized using Angular lifecycle hooks @@ -62,57 +65,68 @@ export class BlogAuthorProfilePageComponent implements OnInit { ngOnInit(): void { this.loaderService.showLoadingScreen('Loading'); - this.MAX_NUM_CARD_TO_DISPLAY_ON_PAGE = ( - BlogAuthorProfilePageConstants - .MAX_NUM_CARDS_TO_DISPLAY_ON_BLOG_AUTHOR_PROFILE_PAGE - ); + this.MAX_NUM_CARD_TO_DISPLAY_ON_PAGE = + BlogAuthorProfilePageConstants.MAX_NUM_CARDS_TO_DISPLAY_ON_BLOG_AUTHOR_PROFILE_PAGE; this.authorUsername = this.urlService.getBlogAuthorUsernameFromUrl(); this.loadInitialBlogAuthorProfilePageData(); } loadInitialBlogAuthorProfilePageData(): void { - this.blogHomePageBackendApiService.fetchBlogAuthorProfilePageDataAsync( - this.authorUsername, '0' - ).then((data: BlogAuthorProfilePageData) => { - if (data.numOfBlogPostSummaries) { - this.totalBlogPosts = data.numOfBlogPostSummaries; - this.authorName = data.displayedAuthorName; - this.authorBio = data.authorBio; - this.noResultsFound = false; - this.blogPostSummaries = data.blogPostSummaries; - this.blogPostSummariesToShow = this.blogPostSummaries; - this.calculateLastPostOnPageNum(); - [this.authorProfilePicPngUrl, this.authorProfilePicWebpUrl] = ( - this.userService.getProfileImageDataUrl(this.authorUsername)); - } else { - this.noResultsFound = true; - } - this.loaderService.hideLoadingScreen(); - }, (errorResponse) => { - if (AppConstants.FATAL_ERROR_CODES.indexOf(errorResponse.status) !== -1) { - this.alertsService.addWarning( - 'Failed to get blog author profile page data.Error: ' + - `${errorResponse.error.error}`); - } - }); + this.blogHomePageBackendApiService + .fetchBlogAuthorProfilePageDataAsync(this.authorUsername, '0') + .then( + (data: BlogAuthorProfilePageData) => { + if (data.numOfBlogPostSummaries) { + this.totalBlogPosts = data.numOfBlogPostSummaries; + this.authorName = data.displayedAuthorName; + this.authorBio = data.authorBio; + this.noResultsFound = false; + this.blogPostSummaries = data.blogPostSummaries; + this.blogPostSummariesToShow = this.blogPostSummaries; + this.calculateLastPostOnPageNum(); + [this.authorProfilePicPngUrl, this.authorProfilePicWebpUrl] = + this.userService.getProfileImageDataUrl(this.authorUsername); + } else { + this.noResultsFound = true; + } + this.loaderService.hideLoadingScreen(); + }, + errorResponse => { + if ( + AppConstants.FATAL_ERROR_CODES.indexOf(errorResponse.status) !== -1 + ) { + this.alertsService.addWarning( + 'Failed to get blog author profile page data.Error: ' + + `${errorResponse.error.error}` + ); + } + } + ); } loadMoreBlogPostSummaries(offset: number): void { - this.blogHomePageBackendApiService.fetchBlogAuthorProfilePageDataAsync( - this.authorUsername, String(offset) - ).then((data: BlogAuthorProfilePageData) => { - this.blogPostSummaries = this.blogPostSummaries.concat( - data.blogPostSummaries); - this.blogPostSummariesToShow = data.blogPostSummaries; - this.calculateLastPostOnPageNum(); - this.showBlogPostCardsLoadingScreen = false; - }, (errorResponse) => { - if (AppConstants.FATAL_ERROR_CODES.indexOf(errorResponse.status) !== -1) { - this.alertsService.addWarning( - 'Failed to get blog author page data.Error:' + - ` ${errorResponse.error.error}`); - } - }); + this.blogHomePageBackendApiService + .fetchBlogAuthorProfilePageDataAsync(this.authorUsername, String(offset)) + .then( + (data: BlogAuthorProfilePageData) => { + this.blogPostSummaries = this.blogPostSummaries.concat( + data.blogPostSummaries + ); + this.blogPostSummariesToShow = data.blogPostSummaries; + this.calculateLastPostOnPageNum(); + this.showBlogPostCardsLoadingScreen = false; + }, + errorResponse => { + if ( + AppConstants.FATAL_ERROR_CODES.indexOf(errorResponse.status) !== -1 + ) { + this.alertsService.addWarning( + 'Failed to get blog author page data.Error:' + + ` ${errorResponse.error.error}` + ); + } + } + ); } loadPage(): void { @@ -121,18 +135,22 @@ export class BlogAuthorProfilePageComponent implements OnInit { this.loadMoreBlogPostSummaries(this.firstPostOnPageNum - 1); } else { this.blogPostSummariesToShow = this.blogPostSummaries.slice( - (this.firstPostOnPageNum - 1), this.lastPostOnPageNum); + this.firstPostOnPageNum - 1, + this.lastPostOnPageNum + ); } } calculateFirstPostOnPageNum(page = this.page): void { - this.firstPostOnPageNum = ( - ((page - 1) * this.MAX_NUM_CARD_TO_DISPLAY_ON_PAGE) + 1); + this.firstPostOnPageNum = + (page - 1) * this.MAX_NUM_CARD_TO_DISPLAY_ON_PAGE + 1; } calculateLastPostOnPageNum(page = this.page): void { this.lastPostOnPageNum = Math.min( - page * this.MAX_NUM_CARD_TO_DISPLAY_ON_PAGE, this.totalBlogPosts); + page * this.MAX_NUM_CARD_TO_DISPLAY_ON_PAGE, + this.totalBlogPosts + ); } onPageChange(): void { diff --git a/core/templates/pages/blog-author-profile-page/blog-author-profile-page.constants.ts b/core/templates/pages/blog-author-profile-page/blog-author-profile-page.constants.ts index 582e7b23738a..d28fd16fc35e 100644 --- a/core/templates/pages/blog-author-profile-page/blog-author-profile-page.constants.ts +++ b/core/templates/pages/blog-author-profile-page/blog-author-profile-page.constants.ts @@ -21,5 +21,6 @@ export const BlogAuthorProfilePageConstants = { MAX_NUM_CARDS_TO_DISPLAY_ON_BLOG_AUTHOR_PROFILE_PAGE: 12, BLOG_AUTHOR_PROFILE_PAGE_URL_TEMPLATE: '/blog/author/', - BLOG_AUTHOR_PROFILE_PAGE_DATA_URL_TEMPLATE: '/blog/author/data/', // eslint-disable-line max-len + BLOG_AUTHOR_PROFILE_PAGE_DATA_URL_TEMPLATE: + '/blog/author/data/', // eslint-disable-line max-len } as const; diff --git a/core/templates/pages/blog-author-profile-page/blog-author-profile-page.module.ts b/core/templates/pages/blog-author-profile-page/blog-author-profile-page.module.ts index 2189e21a591d..206b07b1703a 100644 --- a/core/templates/pages/blog-author-profile-page/blog-author-profile-page.module.ts +++ b/core/templates/pages/blog-author-profile-page/blog-author-profile-page.module.ts @@ -16,22 +16,22 @@ * @fileoverview Module for the blog home page. */ -import { NgModule } from '@angular/core'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { InfiniteScrollModule } from 'ngx-infinite-scroll'; -import { FormsModule } from '@angular/forms'; -import { ReactiveFormsModule } from '@angular/forms'; -import { TranslateModule } from '@ngx-translate/core'; -import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import {NgModule} from '@angular/core'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {InfiniteScrollModule} from 'ngx-infinite-scroll'; +import {FormsModule} from '@angular/forms'; +import {ReactiveFormsModule} from '@angular/forms'; +import {TranslateModule} from '@ngx-translate/core'; +import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; -import { BlogAuthorProfilePageRootComponent } from './blog-author-profile-page-root.component'; -import { BlogAuthorProfilePageComponent } from './blog-author-profile-page.component'; -import { CommonModule } from '@angular/common'; -import { MatMenuModule } from '@angular/material/menu'; -import { BlogAuthorProfilePageRoutingModule } from './blog-author-profile-page-routing.module'; -import { StringUtilityPipesModule } from 'filters/string-utility-filters/string-utility-pipes.module'; -import { Error404PageModule } from 'pages/error-pages/error-404/error-404-page.module'; -import { SharedBlogComponentsModule } from 'pages/blog-dashboard-page/shared-blog-components.module'; +import {BlogAuthorProfilePageRootComponent} from './blog-author-profile-page-root.component'; +import {BlogAuthorProfilePageComponent} from './blog-author-profile-page.component'; +import {CommonModule} from '@angular/common'; +import {MatMenuModule} from '@angular/material/menu'; +import {BlogAuthorProfilePageRoutingModule} from './blog-author-profile-page-routing.module'; +import {StringUtilityPipesModule} from 'filters/string-utility-filters/string-utility-pipes.module'; +import {Error404PageModule} from 'pages/error-pages/error-404/error-404-page.module'; +import {SharedBlogComponentsModule} from 'pages/blog-dashboard-page/shared-blog-components.module'; @NgModule({ imports: [ @@ -46,15 +46,15 @@ import { SharedBlogComponentsModule } from 'pages/blog-dashboard-page/shared-blo TranslateModule, ReactiveFormsModule, Error404PageModule, - SharedBlogComponentsModule + SharedBlogComponentsModule, ], declarations: [ BlogAuthorProfilePageComponent, - BlogAuthorProfilePageRootComponent + BlogAuthorProfilePageRootComponent, ], entryComponents: [ BlogAuthorProfilePageComponent, - BlogAuthorProfilePageRootComponent - ] + BlogAuthorProfilePageRootComponent, + ], }) export class BlogAuthorProfilePageModule {} diff --git a/core/templates/pages/blog-dashboard-page/blog-card/blog-card.component.spec.ts b/core/templates/pages/blog-dashboard-page/blog-card/blog-card.component.spec.ts index 3092c99578d2..62a09e8a73b5 100644 --- a/core/templates/pages/blog-dashboard-page/blog-card/blog-card.component.spec.ts +++ b/core/templates/pages/blog-dashboard-page/blog-card/blog-card.component.spec.ts @@ -16,17 +16,20 @@ * @fileoverview Unit tests for Blog Card component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { CapitalizePipe } from 'filters/string-utility-filters/capitalize.pipe'; -import { MockTranslatePipe, MockCapitalizePipe } from 'tests/unit-test-utils'; -import { BlogCardComponent } from './blog-card.component'; -import { BlogPostSummaryBackendDict, BlogPostSummary } from 'domain/blog/blog-post-summary.model'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { ContextService } from 'services/context.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { UserService } from 'services/user.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {CapitalizePipe} from 'filters/string-utility-filters/capitalize.pipe'; +import {MockTranslatePipe, MockCapitalizePipe} from 'tests/unit-test-utils'; +import {BlogCardComponent} from './blog-card.component'; +import { + BlogPostSummaryBackendDict, + BlogPostSummary, +} from 'domain/blog/blog-post-summary.model'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {ContextService} from 'services/context.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {UserService} from 'services/user.service'; describe('Blog Dashboard Tile Component', () => { let component: BlogCardComponent; @@ -40,7 +43,7 @@ describe('Blog Dashboard Tile Component', () => { location: { href: '', hash: '/', - reload: () => { } + reload: () => {}, }, }; } @@ -48,26 +51,21 @@ describe('Blog Dashboard Tile Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - BlogCardComponent, - MockTranslatePipe, - ], + imports: [HttpClientTestingModule], + declarations: [BlogCardComponent, MockTranslatePipe], providers: [ { provide: CapitalizePipe, - useClass: MockCapitalizePipe + useClass: MockCapitalizePipe, }, { provide: WindowRef, - useClass: MockWindowRef + useClass: MockWindowRef, }, UrlInterpolationService, ContextService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -90,33 +88,32 @@ describe('Blog Dashboard Tile Component', () => { last_updated: '11/21/2014', published_on: '11/21/2014', }; - spyOn(userService, 'getProfileImageDataUrl').and.returnValue( - ['default-image-url-png', 'default-image-url-webp']); + spyOn(userService, 'getProfileImageDataUrl').and.returnValue([ + 'default-image-url-png', + 'default-image-url-webp', + ]); }); it('should create', () => { expect(component).toBeDefined(); }); - it('should get formatted date string from the timestamp in milliseconds', - () => { - // This corresponds to Fri, 21 Nov 2014 09:45:00 GMT. - let DATE = '11/21/2014'; - expect(component.getDateStringInWords(DATE)) - .toBe('November 21, 2014'); + it('should get formatted date string from the timestamp in milliseconds', () => { + // This corresponds to Fri, 21 Nov 2014 09:45:00 GMT. + let DATE = '11/21/2014'; + expect(component.getDateStringInWords(DATE)).toBe('November 21, 2014'); - DATE = '01/16/2027'; - expect(component.getDateStringInWords(DATE)) - .toBe('January 16, 2027'); + DATE = '01/16/2027'; + expect(component.getDateStringInWords(DATE)).toBe('January 16, 2027'); - DATE = '02/02/2018'; - expect(component.getDateStringInWords(DATE)) - .toBe('February 2, 2018'); - }); + DATE = '02/02/2018'; + expect(component.getDateStringInWords(DATE)).toBe('February 2, 2018'); + }); it('should initialize', () => { component.blogPostSummary = BlogPostSummary.createFromBackendDict( - sampleBlogPostSummary); + sampleBlogPostSummary + ); spyOn(contextService, 'isInBlogPostEditorPage').and.returnValue(true); component.ngOnInit(); @@ -124,8 +121,8 @@ describe('Blog Dashboard Tile Component', () => { expect(component.authorProfilePicPngUrl).toEqual('default-image-url-png'); expect(component.authorProfilePicWebpUrl).toEqual('default-image-url-webp'); expect(component.thumbnailUrl).toBe( - '/assetsdevhandler/blog_post/sampleId/assets/' + - 'thumbnail/image.png'); + '/assetsdevhandler/blog_post/sampleId/assets/' + 'thumbnail/image.png' + ); expect(component.publishedDateString).toBe('November 21, 2014'); expect(component.blogCardPreviewModeIsActive).toBeTrue(); }); @@ -143,7 +140,8 @@ describe('Blog Dashboard Tile Component', () => { last_updated: '11/21/2014', }; component.blogPostSummary = BlogPostSummary.createFromBackendDict( - invalidBlogPostSummary); + invalidBlogPostSummary + ); expect(() => { component.ngOnInit(); @@ -154,7 +152,8 @@ describe('Blog Dashboard Tile Component', () => { spyOn(contextService, 'isInBlogPostEditorPage').and.returnValue(true); sampleBlogPostSummary.thumbnail_filename = null; component.blogPostSummary = BlogPostSummary.createFromBackendDict( - sampleBlogPostSummary); + sampleBlogPostSummary + ); expect(component.thumbnailUrl).toBe(''); @@ -165,14 +164,17 @@ describe('Blog Dashboard Tile Component', () => { it('should navigate to the blog post page', () => { component.blogPostSummary = BlogPostSummary.createFromBackendDict( - sampleBlogPostSummary); + sampleBlogPostSummary + ); spyOn(contextService, 'isInBlogPostEditorPage').and.returnValue(false); spyOn(urlInterpolationService, 'interpolateUrl').and.returnValue( - '/blog/sample-blog-post-url'); + '/blog/sample-blog-post-url' + ); component.navigateToBlogPostPage(); expect(mockWindowRef.nativeWindow.location.href).toEqual( - '/blog/sample-blog-post-url'); + '/blog/sample-blog-post-url' + ); }); }); diff --git a/core/templates/pages/blog-dashboard-page/blog-card/blog-card.component.ts b/core/templates/pages/blog-dashboard-page/blog-card/blog-card.component.ts index 300c3bec49d9..d8ee249cd92e 100644 --- a/core/templates/pages/blog-dashboard-page/blog-card/blog-card.component.ts +++ b/core/templates/pages/blog-dashboard-page/blog-card/blog-card.component.ts @@ -16,20 +16,20 @@ * @fileoverview Component for a blog card. */ -import { Component, Input, OnInit } from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { BlogPostSummary } from 'domain/blog/blog-post-summary.model'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { BlogPostPageConstants } from 'pages/blog-post-page/blog-post-page.constants'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { ContextService } from 'services/context.service'; +import {Component, Input, OnInit} from '@angular/core'; +import {AppConstants} from 'app.constants'; +import {BlogPostSummary} from 'domain/blog/blog-post-summary.model'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {BlogPostPageConstants} from 'pages/blog-post-page/blog-post-page.constants'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {ContextService} from 'services/context.service'; import dayjs from 'dayjs'; -import { UserService } from 'services/user.service'; +import {UserService} from 'services/user.service'; @Component({ selector: 'oppia-blog-card', - templateUrl: './blog-card.component.html' + templateUrl: './blog-card.component.html', }) export class BlogCardComponent implements OnInit { // These properties are initialized using Angular lifecycle hooks @@ -53,33 +53,35 @@ export class BlogCardComponent implements OnInit { ngOnInit(): void { if (this.blogPostSummary.thumbnailFilename) { - this.thumbnailUrl = this.assetsBackendApiService - .getThumbnailUrlForPreview( - AppConstants.ENTITY_TYPE.BLOG_POST, this.blogPostSummary.id, - this.blogPostSummary.thumbnailFilename); + this.thumbnailUrl = + this.assetsBackendApiService.getThumbnailUrlForPreview( + AppConstants.ENTITY_TYPE.BLOG_POST, + this.blogPostSummary.id, + this.blogPostSummary.thumbnailFilename + ); } - [this.authorProfilePicPngUrl, this.authorProfilePicWebpUrl] = ( + [this.authorProfilePicPngUrl, this.authorProfilePicWebpUrl] = this.userService.getProfileImageDataUrl( - this.blogPostSummary.authorUsername)); + this.blogPostSummary.authorUsername + ); const publishedOn = this.blogPostSummary.publishedOn; if (publishedOn === undefined) { throw new Error('Blog Post Summary published date is not defined'); } this.publishedDateString = this.getDateStringInWords(publishedOn); - this.blogCardPreviewModeIsActive = ( - this.contextService.isInBlogPostEditorPage()); + this.blogCardPreviewModeIsActive = + this.contextService.isInBlogPostEditorPage(); } getDateStringInWords(naiveDate: string): string { - return dayjs( - naiveDate.split(',')[0], 'MM-DD-YYYY').format('MMMM D, YYYY'); + return dayjs(naiveDate.split(',')[0], 'MM-DD-YYYY').format('MMMM D, YYYY'); } navigateToBlogPostPage(): void { if (!this.blogCardPreviewModeIsActive) { let blogPostUrl = this.urlInterpolationService.interpolateUrl( BlogPostPageConstants.BLOG_POST_PAGE_URL_TEMPLATE, - { blog_post_url: this.blogPostSummary.urlFragment } + {blog_post_url: this.blogPostSummary.urlFragment} ); this.windowRef.nativeWindow.location.href = blogPostUrl; } diff --git a/core/templates/pages/blog-dashboard-page/blog-dashboard-page-auth.guard.spec.ts b/core/templates/pages/blog-dashboard-page/blog-dashboard-page-auth.guard.spec.ts index 5183c9c2d0c6..15419d9fd2db 100644 --- a/core/templates/pages/blog-dashboard-page/blog-dashboard-page-auth.guard.spec.ts +++ b/core/templates/pages/blog-dashboard-page/blog-dashboard-page-auth.guard.spec.ts @@ -16,14 +16,19 @@ * @fileoverview Tests for BlogDashboardPageAuthGuard */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TestBed } from '@angular/core/testing'; -import { ActivatedRouteSnapshot, Router, RouterStateSnapshot, NavigationExtras } from '@angular/router'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TestBed} from '@angular/core/testing'; +import { + ActivatedRouteSnapshot, + Router, + RouterStateSnapshot, + NavigationExtras, +} from '@angular/router'; -import { AppConstants } from 'app.constants'; -import { UserInfo, UserRoles } from 'domain/user/user-info.model'; -import { UserService } from 'services/user.service'; -import { BlogDashboardPageAuthGuard } from './blog-dashboard-page-auth.guard'; +import {AppConstants} from 'app.constants'; +import {UserInfo, UserRoles} from 'domain/user/user-info.model'; +import {UserService} from 'services/user.service'; +import {BlogDashboardPageAuthGuard} from './blog-dashboard-page-auth.guard'; class MockRouter { navigate(commands: string[], extras?: NavigationExtras): Promise { @@ -39,7 +44,7 @@ describe('BlogDashboardPageAuthGuard', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [UserService, { provide: Router, useClass: MockRouter }], + providers: [UserService, {provide: Router, useClass: MockRouter}], }).compileComponents(); guard = TestBed.inject(BlogDashboardPageAuthGuard); @@ -47,36 +52,50 @@ describe('BlogDashboardPageAuthGuard', () => { router = TestBed.inject(Router); }); - it('should redirect user to 401 page if user is not blog admin', (done) => { + it('should redirect user to 401 page if user is not blog admin', done => { const getUserInfoAsyncSpy = spyOn( - userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(UserInfo.createDefault()) - ); + userService, + 'getUserInfoAsync' + ).and.returnValue(Promise.resolve(UserInfo.createDefault())); const navigateSpy = spyOn(router, 'navigate').and.callThrough(); - guard.canActivate( - new ActivatedRouteSnapshot(), {} as RouterStateSnapshot).then( - (canActivate) => { + guard + .canActivate(new ActivatedRouteSnapshot(), {} as RouterStateSnapshot) + .then(canActivate => { expect(canActivate).toBeFalse(); expect(getUserInfoAsyncSpy).toHaveBeenCalledTimes(1); expect(navigateSpy).toHaveBeenCalledWith([ - `${AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ERROR.ROUTE}/401`]); + `${AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ERROR.ROUTE}/401`, + ]); done(); }); }); - it('should not redirect user to 401 page if user is blog admin', (done) => { + it('should not redirect user to 401 page if user is blog admin', done => { const getUserInfoAsyncSpy = spyOn( - userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(new UserInfo( - [UserRoles.BLOG_ADMIN], - false, true, false, false, false, '', '', '', true)) + userService, + 'getUserInfoAsync' + ).and.returnValue( + Promise.resolve( + new UserInfo( + [UserRoles.BLOG_ADMIN], + false, + true, + false, + false, + false, + '', + '', + '', + true + ) + ) ); const navigateSpy = spyOn(router, 'navigate').and.callThrough(); - guard.canActivate( - new ActivatedRouteSnapshot(), {} as RouterStateSnapshot).then( - (canActivate) => { + guard + .canActivate(new ActivatedRouteSnapshot(), {} as RouterStateSnapshot) + .then(canActivate => { expect(canActivate).toBeTrue(); expect(getUserInfoAsyncSpy).toHaveBeenCalledTimes(1); expect(navigateSpy).not.toHaveBeenCalled(); diff --git a/core/templates/pages/blog-dashboard-page/blog-dashboard-page-auth.guard.ts b/core/templates/pages/blog-dashboard-page/blog-dashboard-page-auth.guard.ts index 4ae6482232eb..04a6cb23f769 100644 --- a/core/templates/pages/blog-dashboard-page/blog-dashboard-page-auth.guard.ts +++ b/core/templates/pages/blog-dashboard-page/blog-dashboard-page-auth.guard.ts @@ -17,8 +17,8 @@ * if the user is not a Blog Admin or Blog Editor. */ -import { Location } from '@angular/common'; -import { Injectable } from '@angular/core'; +import {Location} from '@angular/common'; +import {Injectable} from '@angular/core'; import { ActivatedRouteSnapshot, CanActivate, @@ -26,11 +26,11 @@ import { RouterStateSnapshot, } from '@angular/router'; -import { AppConstants } from 'app.constants'; -import { UserService } from 'services/user.service'; +import {AppConstants} from 'app.constants'; +import {UserService} from 'services/user.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class BlogDashboardPageAuthGuard implements CanActivate { constructor( @@ -40,19 +40,21 @@ export class BlogDashboardPageAuthGuard implements CanActivate { ) {} async canActivate( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot ): Promise { const userInfo = await this.userService.getUserInfoAsync(); if (userInfo.isBlogPostEditor() || userInfo.isBlogAdmin()) { return true; } - this.router.navigate( - [`${AppConstants.PAGES_REGISTERED_WITH_FRONTEND - .ERROR.ROUTE}/401`]).then(() => { - this.location.replaceState(state.url); - }); + this.router + .navigate([ + `${AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ERROR.ROUTE}/401`, + ]) + .then(() => { + this.location.replaceState(state.url); + }); return false; } } diff --git a/core/templates/pages/blog-dashboard-page/blog-dashboard-page-root.component.spec.ts b/core/templates/pages/blog-dashboard-page/blog-dashboard-page-root.component.spec.ts index 78390404cd96..2bb21f5a3b85 100644 --- a/core/templates/pages/blog-dashboard-page/blog-dashboard-page-root.component.spec.ts +++ b/core/templates/pages/blog-dashboard-page/blog-dashboard-page-root.component.spec.ts @@ -16,14 +16,14 @@ * @fileoverview Unit tests for Blog Page Root component. */ -import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { TranslateModule } from '@ngx-translate/core'; +import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {TranslateModule} from '@ngx-translate/core'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; -import { BlogDashboardPageRootComponent } from './blog-dashboard-page-root.component'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; +import {BlogDashboardPageRootComponent} from './blog-dashboard-page-root.component'; describe('BlogDashboardPageRootComponent', () => { let fixture: ComponentFixture; @@ -43,8 +43,10 @@ describe('BlogDashboardPageRootComponent', () => { it('should have the title and meta tags set', () => { expect(component.title).toEqual( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.BLOG_DASHBOARD.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.BLOG_DASHBOARD.TITLE + ); expect(component.meta).toEqual( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.BLOG_DASHBOARD.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.BLOG_DASHBOARD.META + ); }); }); diff --git a/core/templates/pages/blog-dashboard-page/blog-dashboard-page-root.component.ts b/core/templates/pages/blog-dashboard-page/blog-dashboard-page-root.component.ts index c55e96676c3f..0795ad7d2b41 100644 --- a/core/templates/pages/blog-dashboard-page/blog-dashboard-page-root.component.ts +++ b/core/templates/pages/blog-dashboard-page/blog-dashboard-page-root.component.ts @@ -16,20 +16,18 @@ * @fileoverview Blog dashboard page root component. */ -import { Component } from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { BaseRootComponent, MetaTagData } from 'pages/base-root.component'; +import {Component} from '@angular/core'; +import {AppConstants} from 'app.constants'; +import {BaseRootComponent, MetaTagData} from 'pages/base-root.component'; @Component({ selector: 'oppia-blog-dashboard-page-root', templateUrl: './blog-dashboard-page-root.component.html', }) export class BlogDashboardPageRootComponent extends BaseRootComponent { - title: string = ( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND - .BLOG_DASHBOARD.TITLE); + title: string = + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.BLOG_DASHBOARD.TITLE; - meta: MetaTagData[] = - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.BLOG_DASHBOARD.META as - unknown as Readonly[]; + meta: MetaTagData[] = AppConstants.PAGES_REGISTERED_WITH_FRONTEND + .BLOG_DASHBOARD.META as unknown as Readonly[]; } diff --git a/core/templates/pages/blog-dashboard-page/blog-dashboard-page.component.spec.ts b/core/templates/pages/blog-dashboard-page/blog-dashboard-page.component.spec.ts index 18340ce8957d..79df6a9750e4 100644 --- a/core/templates/pages/blog-dashboard-page/blog-dashboard-page.component.spec.ts +++ b/core/templates/pages/blog-dashboard-page/blog-dashboard-page.component.spec.ts @@ -16,25 +16,35 @@ * @fileoverview Unit tests for Blog Dashboard page component. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { MatTabsModule } from '@angular/material/tabs'; -import { CapitalizePipe } from 'filters/string-utility-filters/capitalize.pipe'; -import { MockTranslatePipe, MockCapitalizePipe } from 'tests/unit-test-utils'; -import { NgbModal, NgbModalModule, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { BlogAuthorDetailsEditorComponent } from './modal-templates/author-detail-editor-modal.component'; -import { LoaderService } from 'services/loader.service'; -import { AlertsService } from 'services/alerts.service'; -import { BlogDashboardBackendApiService } from 'domain/blog/blog-dashboard-backend-api.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { BlogDashboardPageService } from './services/blog-dashboard-page.service'; -import { BlogDashboardPageComponent } from './blog-dashboard-page.component'; -import { BlogPostSummary } from 'domain/blog/blog-post-summary.model'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { of } from 'rxjs'; -import { UserService } from 'services/user.service'; -import { UserInfo } from 'domain/user/user-info.model'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {MatTabsModule} from '@angular/material/tabs'; +import {CapitalizePipe} from 'filters/string-utility-filters/capitalize.pipe'; +import {MockTranslatePipe, MockCapitalizePipe} from 'tests/unit-test-utils'; +import { + NgbModal, + NgbModalModule, + NgbModalRef, +} from '@ng-bootstrap/ng-bootstrap'; +import {BlogAuthorDetailsEditorComponent} from './modal-templates/author-detail-editor-modal.component'; +import {LoaderService} from 'services/loader.service'; +import {AlertsService} from 'services/alerts.service'; +import {BlogDashboardBackendApiService} from 'domain/blog/blog-dashboard-backend-api.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {BlogDashboardPageService} from './services/blog-dashboard-page.service'; +import {BlogDashboardPageComponent} from './blog-dashboard-page.component'; +import {BlogPostSummary} from 'domain/blog/blog-post-summary.model'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {of} from 'rxjs'; +import {UserService} from 'services/user.service'; +import {UserInfo} from 'domain/user/user-info.model'; describe('Blog Dashboard Page Component', () => { let alertsService: AlertsService; @@ -63,7 +73,7 @@ describe('Blog Dashboard Page Component', () => { href: '', hash: '/', _hashChange: null, - reload: () => {} + reload: () => {}, }, open: (url: string) => {}, onhashchange() { @@ -74,38 +84,34 @@ describe('Blog Dashboard Page Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - MatTabsModule, - NgbModalModule, - ], + imports: [HttpClientTestingModule, MatTabsModule, NgbModalModule], declarations: [ BlogDashboardPageComponent, BlogAuthorDetailsEditorComponent, - MockTranslatePipe + MockTranslatePipe, ], providers: [ AlertsService, { provide: CapitalizePipe, - useClass: MockCapitalizePipe + useClass: MockCapitalizePipe, }, { provide: WindowRef, - useClass: MockWindowRef + useClass: MockWindowRef, }, { provide: WindowDimensionsService, useValue: { isWindowNarrow: () => true, - getResizeEvent: () => of(resizeEvent) - } + getResizeEvent: () => of(resizeEvent), + }, }, BlogDashboardBackendApiService, BlogDashboardPageService, LoaderService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -117,7 +123,8 @@ describe('Blog Dashboard Page Component', () => { blogDashboardPageService = TestBed.inject(BlogDashboardPageService); loaderService = TestBed.inject(LoaderService); blogDashboardBackendApiService = TestBed.inject( - BlogDashboardBackendApiService); + BlogDashboardBackendApiService + ); windowDimensionsService = TestBed.inject(WindowDimensionsService); alertsService = TestBed.inject(AlertsService); userService = TestBed.inject(UserService); @@ -137,30 +144,34 @@ describe('Blog Dashboard Page Component', () => { component.ngOnInit(); - expect(blogDashboardPageService.updateViewEventEmitter.subscribe) - .toHaveBeenCalled(); + expect( + blogDashboardPageService.updateViewEventEmitter.subscribe + ).toHaveBeenCalled(); }); - it('should set correct activeTab value when update view ' + - 'event is emitted.', fakeAsync(() => { - component.ngOnInit(); + it( + 'should set correct activeTab value when update view ' + + 'event is emitted.', + fakeAsync(() => { + component.ngOnInit(); - expect(component.activeTab).toBe('main'); + expect(component.activeTab).toBe('main'); - // Changing active tab to blog post editor. - blogDashboardPageService.navigateToEditorTabWithId('123456sample'); - mockWindowRef.nativeWindow.onhashchange(); - tick(); + // Changing active tab to blog post editor. + blogDashboardPageService.navigateToEditorTabWithId('123456sample'); + mockWindowRef.nativeWindow.onhashchange(); + tick(); - expect(component.activeTab).toBe('editor_tab'); + expect(component.activeTab).toBe('editor_tab'); - // Changing active tab back to main tab. - mockWindowRef.nativeWindow.location.hash = '/'; - mockWindowRef.nativeWindow.onhashchange(); - tick(); + // Changing active tab back to main tab. + mockWindowRef.nativeWindow.location.hash = '/'; + mockWindowRef.nativeWindow.onhashchange(); + tick(); - expect(component.activeTab).toBe('main'); - })); + expect(component.activeTab).toBe('main'); + }) + ); it('should call initMainTab if active tab is main', () => { spyOn(component, 'initMainTab'); @@ -170,19 +181,18 @@ describe('Blog Dashboard Page Component', () => { expect(component.initMainTab).toHaveBeenCalled(); }); - it('should not call initMainTab if active tab is editor_tab', - fakeAsync(() => { - spyOn(component, 'initMainTab'); - // Changing active tab to blog post editor. - blogDashboardPageService.navigateToEditorTabWithId('123456sample'); - mockWindowRef.nativeWindow.onhashchange(); + it('should not call initMainTab if active tab is editor_tab', fakeAsync(() => { + spyOn(component, 'initMainTab'); + // Changing active tab to blog post editor. + blogDashboardPageService.navigateToEditorTabWithId('123456sample'); + mockWindowRef.nativeWindow.onhashchange(); - component.ngOnInit(); - tick(); + component.ngOnInit(); + tick(); - expect(component.activeTab).toBe('editor_tab'); - expect(component.initMainTab).not.toHaveBeenCalled(); - })); + expect(component.activeTab).toBe('editor_tab'); + expect(component.initMainTab).not.toHaveBeenCalled(); + })); it('should initialize main tab', fakeAsync(() => { const sampleUserInfoBackendObject = { @@ -195,19 +205,25 @@ describe('Blog Dashboard Page Component', () => { preferred_site_language_code: null, username: 'tester', email: 'test@test.com', - user_is_logged_in: true + user_is_logged_in: true, }; const sampleUserInfo = UserInfo.createFromBackendDict( - sampleUserInfoBackendObject); - spyOn(userService, 'getProfileImageDataUrl').and.returnValue( - ['default-image-url-png', 'default-image-url-webp']); + sampleUserInfoBackendObject + ); + spyOn(userService, 'getProfileImageDataUrl').and.returnValue([ + 'default-image-url-png', + 'default-image-url-webp', + ]); spyOn(component, 'showAuthorDetailsEditor'); spyOn(loaderService, 'showLoadingScreen'); spyOn(loaderService, 'hideLoadingScreen'); - spyOn(blogDashboardBackendApiService, 'fetchBlogDashboardDataAsync') - .and.returnValue(Promise.resolve(blogDashboardData)); + spyOn( + blogDashboardBackendApiService, + 'fetchBlogDashboardDataAsync' + ).and.returnValue(Promise.resolve(blogDashboardData)); spyOn(userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(sampleUserInfo)); + Promise.resolve(sampleUserInfo) + ); component.initMainTab(); // As loading screen should be shown irrespective of the response @@ -217,94 +233,109 @@ describe('Blog Dashboard Page Component', () => { tick(); expect(component.blogDashboardData).toEqual(blogDashboardData); - expect(blogDashboardBackendApiService.fetchBlogDashboardDataAsync) - .toHaveBeenCalled(); + expect( + blogDashboardBackendApiService.fetchBlogDashboardDataAsync + ).toHaveBeenCalled(); expect(component.authorProfilePicPngUrl).toEqual('default-image-url-png'); - expect(component.authorProfilePicWebpUrl).toEqual( - 'default-image-url-webp'); + expect(component.authorProfilePicWebpUrl).toEqual('default-image-url-webp'); expect(component.showAuthorDetailsEditor).toHaveBeenCalled(); expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); expect(windowDimensionsService.isWindowNarrow()).toHaveBeenCalled; expect(component.windowIsNarrow).toBe(true); })); - it('should set default profile pictures when username is null', - fakeAsync(() => { - let userInfo = { - getUsername: () => null, - isSuperAdmin: () => true - }; - spyOn(component, 'showAuthorDetailsEditor'); - spyOn(loaderService, 'showLoadingScreen'); - spyOn(loaderService, 'hideLoadingScreen'); - spyOn(blogDashboardBackendApiService, 'fetchBlogDashboardDataAsync') - .and.returnValue(Promise.resolve(blogDashboardData)); - spyOn(userService, 'getUserInfoAsync') - .and.resolveTo(userInfo as UserInfo); - - component.initMainTab(); - tick(); + it('should set default profile pictures when username is null', fakeAsync(() => { + let userInfo = { + getUsername: () => null, + isSuperAdmin: () => true, + }; + spyOn(component, 'showAuthorDetailsEditor'); + spyOn(loaderService, 'showLoadingScreen'); + spyOn(loaderService, 'hideLoadingScreen'); + spyOn( + blogDashboardBackendApiService, + 'fetchBlogDashboardDataAsync' + ).and.returnValue(Promise.resolve(blogDashboardData)); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo as UserInfo); - expect(component.authorProfilePicPngUrl).toEqual( - '/assets/images/avatar/user_blue_150px.png'); - expect(component.authorProfilePicWebpUrl).toEqual( - '/assets/images/avatar/user_blue_150px.webp'); - })); + component.initMainTab(); + tick(); - it('should display alert when unable to fetch blog dashboard data', - fakeAsync(() => { - spyOn(loaderService, 'showLoadingScreen'); - spyOn(component, 'getUserInfoAsync').and.returnValue(Promise.resolve()); - spyOn(blogDashboardBackendApiService, 'fetchBlogDashboardDataAsync') - .and.returnValue(Promise.reject(500)); - spyOn(alertsService, 'addWarning'); + expect(component.authorProfilePicPngUrl).toEqual( + '/assets/images/avatar/user_blue_150px.png' + ); + expect(component.authorProfilePicWebpUrl).toEqual( + '/assets/images/avatar/user_blue_150px.webp' + ); + })); - component.ngOnInit(); - tick(); + it('should display alert when unable to fetch blog dashboard data', fakeAsync(() => { + spyOn(loaderService, 'showLoadingScreen'); + spyOn(component, 'getUserInfoAsync').and.returnValue(Promise.resolve()); + spyOn( + blogDashboardBackendApiService, + 'fetchBlogDashboardDataAsync' + ).and.returnValue(Promise.reject(500)); + spyOn(alertsService, 'addWarning'); - expect(loaderService.showLoadingScreen).toHaveBeenCalled(); - expect(blogDashboardBackendApiService.fetchBlogDashboardDataAsync) - .toHaveBeenCalled(); - expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Failed to get blog dashboard data'); - })); + component.ngOnInit(); + tick(); + + expect(loaderService.showLoadingScreen).toHaveBeenCalled(); + expect( + blogDashboardBackendApiService.fetchBlogDashboardDataAsync + ).toHaveBeenCalled(); + expect(alertsService.addWarning).toHaveBeenCalledWith( + 'Failed to get blog dashboard data' + ); + })); it('should succesfully create new blog post', fakeAsync(() => { - spyOn(blogDashboardBackendApiService, 'createBlogPostAsync') - .and.returnValue(Promise.resolve('123456abcdef')); + spyOn( + blogDashboardBackendApiService, + 'createBlogPostAsync' + ).and.returnValue(Promise.resolve('123456abcdef')); spyOn(blogDashboardPageService, 'navigateToEditorTabWithId'); component.createNewBlogPost(); tick(); - expect(blogDashboardBackendApiService.createBlogPostAsync) - .toHaveBeenCalled(); - expect(blogDashboardPageService.navigateToEditorTabWithId) - .toHaveBeenCalledWith('123456abcdef'); + expect( + blogDashboardBackendApiService.createBlogPostAsync + ).toHaveBeenCalled(); + expect( + blogDashboardPageService.navigateToEditorTabWithId + ).toHaveBeenCalledWith('123456abcdef'); })); - it('should display alert when unable to create new blog post.', - fakeAsync(() => { - spyOn(blogDashboardBackendApiService, 'createBlogPostAsync') - .and.returnValue(Promise.reject( - 'To many collisions with existing blog post ids.')); - spyOn(blogDashboardPageService, 'navigateToEditorTabWithId'); - spyOn(alertsService, 'addWarning'); + it('should display alert when unable to create new blog post.', fakeAsync(() => { + spyOn( + blogDashboardBackendApiService, + 'createBlogPostAsync' + ).and.returnValue( + Promise.reject('To many collisions with existing blog post ids.') + ); + spyOn(blogDashboardPageService, 'navigateToEditorTabWithId'); + spyOn(alertsService, 'addWarning'); - component.createNewBlogPost(); - tick(); + component.createNewBlogPost(); + tick(); + + expect( + blogDashboardBackendApiService.createBlogPostAsync + ).toHaveBeenCalled(); + expect(alertsService.addWarning).toHaveBeenCalledWith( + 'Unable to create new blog post.Error: ' + + 'To many collisions with existing blog post ids.' + ); + })); - expect(blogDashboardBackendApiService.createBlogPostAsync) - .toHaveBeenCalled(); - expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Unable to create new blog post.Error: ' + - 'To many collisions with existing blog post ids.'); - })); - - it('should remove unpublish blog post from published list and' + - ' add it to drafts list', () => { - let summaryObject = BlogPostSummary.createFromBackendDict( - { id: 'sampleId', + it( + 'should remove unpublish blog post from published list and' + + ' add it to drafts list', + () => { + let summaryObject = BlogPostSummary.createFromBackendDict({ + id: 'sampleId', author_username: 'test_username', displayed_author_name: 'test_user', title: 'Title', @@ -315,32 +346,37 @@ describe('Blog Dashboard Page Component', () => { last_updated: '3232323', published_on: '3232323', }); - let blogDashboardData = { - displayedAuthorName: 'test_user', - authorBio: 'bio', - numOfPublishedBlogPosts: 1, - numOfDraftBlogPosts: 0, - publishedBlogPostSummaryDicts: [summaryObject], - draftBlogPostSummaryDicts: [], - }; - component.blogDashboardData = blogDashboardData; - - component.unpublishedBlogPost(summaryObject); - - // BlogPostSummary should now be a part of draft list whereas - // publish blogPostSummary list should be empty. - expect(component.blogDashboardData.draftBlogPostSummaryDicts) - .toEqual([summaryObject]); - expect(component.blogDashboardData.publishedBlogPostSummaryDicts) - .toEqual([]); - expect(component.blogDashboardData.numOfPublishedBlogPosts).toEqual(0); - expect(component.blogDashboardData.numOfDraftBlogPosts).toEqual(1); - }); + let blogDashboardData = { + displayedAuthorName: 'test_user', + authorBio: 'bio', + numOfPublishedBlogPosts: 1, + numOfDraftBlogPosts: 0, + publishedBlogPostSummaryDicts: [summaryObject], + draftBlogPostSummaryDicts: [], + }; + component.blogDashboardData = blogDashboardData; + + component.unpublishedBlogPost(summaryObject); + + // BlogPostSummary should now be a part of draft list whereas + // publish blogPostSummary list should be empty. + expect(component.blogDashboardData.draftBlogPostSummaryDicts).toEqual([ + summaryObject, + ]); + expect(component.blogDashboardData.publishedBlogPostSummaryDicts).toEqual( + [] + ); + expect(component.blogDashboardData.numOfPublishedBlogPosts).toEqual(0); + expect(component.blogDashboardData.numOfDraftBlogPosts).toEqual(1); + } + ); - it('should successfully remove blog post summary when blog post' + - 'published blog post is deleted', () => { - let summaryObject = BlogPostSummary.createFromBackendDict( - { id: 'sampleId', + it( + 'should successfully remove blog post summary when blog post' + + 'published blog post is deleted', + () => { + let summaryObject = BlogPostSummary.createFromBackendDict({ + id: 'sampleId', author_username: 'test_username', displayed_author_name: 'test_user', title: 'Title', @@ -351,27 +387,33 @@ describe('Blog Dashboard Page Component', () => { last_updated: '3232323', published_on: '3232323', }); - let blogDashboardData = { - displayedAuthorName: 'test_user', - authorBio: 'Bio', - numOfPublishedBlogPosts: 0, - numOfDraftBlogPosts: 0, - publishedBlogPostSummaryDicts: [summaryObject], - draftBlogPostSummaryDicts: [], - }; - component.blogDashboardData = blogDashboardData; - component.blogDashboardData.publishedBlogPostSummaryDicts = [summaryObject]; + let blogDashboardData = { + displayedAuthorName: 'test_user', + authorBio: 'Bio', + numOfPublishedBlogPosts: 0, + numOfDraftBlogPosts: 0, + publishedBlogPostSummaryDicts: [summaryObject], + draftBlogPostSummaryDicts: [], + }; + component.blogDashboardData = blogDashboardData; + component.blogDashboardData.publishedBlogPostSummaryDicts = [ + summaryObject, + ]; - component.removeBlogPost(summaryObject, true); + component.removeBlogPost(summaryObject, true); - expect(component.blogDashboardData.publishedBlogPostSummaryDicts).toEqual( - []); - }); + expect(component.blogDashboardData.publishedBlogPostSummaryDicts).toEqual( + [] + ); + } + ); - it('should successfully remove blog post summary when blog post' + - 'draft blog post is deleted', () => { - let summaryObject = BlogPostSummary.createFromBackendDict( - { id: 'sampleId', + it( + 'should successfully remove blog post summary when blog post' + + 'draft blog post is deleted', + () => { + let summaryObject = BlogPostSummary.createFromBackendDict({ + id: 'sampleId', author_username: 'test_username', displayed_author_name: 'test_user', title: 'Title', @@ -382,69 +424,74 @@ describe('Blog Dashboard Page Component', () => { last_updated: '3232323', published_on: '3232323', }); - let blogDashboardData = { - displayedAuthorName: 'test_user', - authorBio: 'Bio', - numOfPublishedBlogPosts: 0, - numOfDraftBlogPosts: 0, - publishedBlogPostSummaryDicts: [], - draftBlogPostSummaryDicts: [summaryObject], - }; - component.blogDashboardData = blogDashboardData; + let blogDashboardData = { + displayedAuthorName: 'test_user', + authorBio: 'Bio', + numOfPublishedBlogPosts: 0, + numOfDraftBlogPosts: 0, + publishedBlogPostSummaryDicts: [], + draftBlogPostSummaryDicts: [summaryObject], + }; + component.blogDashboardData = blogDashboardData; - component.removeBlogPost(summaryObject, false); + component.removeBlogPost(summaryObject, false); - expect(component.blogDashboardData.draftBlogPostSummaryDicts).toEqual( - []); - }); + expect(component.blogDashboardData.draftBlogPostSummaryDicts).toEqual([]); + } + ); - it('should display alert when unable to update author details', fakeAsync( - () => { - component.authorName = 'new username'; - component.authorBio = 'Oppia Blog Author'; - spyOn(blogDashboardBackendApiService, 'updateAuthorDetailsAsync') - .and.returnValue(Promise.reject( - 'Server responded with backend error.')); - spyOn(alertsService, 'addWarning'); - - component.updateAuthorDetails(); - tick(); + it('should display alert when unable to update author details', fakeAsync(() => { + component.authorName = 'new username'; + component.authorBio = 'Oppia Blog Author'; + spyOn( + blogDashboardBackendApiService, + 'updateAuthorDetailsAsync' + ).and.returnValue(Promise.reject('Server responded with backend error.')); + spyOn(alertsService, 'addWarning'); - expect(blogDashboardBackendApiService.updateAuthorDetailsAsync) - .toHaveBeenCalled(); - expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Unable to update author details. Error: Server responded with' + - ' backend error.'); - }) - ); + component.updateAuthorDetails(); + tick(); + + expect( + blogDashboardBackendApiService.updateAuthorDetailsAsync + ).toHaveBeenCalled(); + expect(alertsService.addWarning).toHaveBeenCalledWith( + 'Unable to update author details. Error: Server responded with' + + ' backend error.' + ); + })); it('should successfully update author details', fakeAsync(() => { component.authorName = 'new username'; component.authorBio = 'Oppia Blog Author'; let BlogAuthorDetails = { displayedAuthorName: 'new username', - authorBio: 'Oppia Blog Author' + authorBio: 'Oppia Blog Author', }; - spyOn(blogDashboardBackendApiService, 'updateAuthorDetailsAsync') - .and.returnValue(Promise.resolve(BlogAuthorDetails)); + spyOn( + blogDashboardBackendApiService, + 'updateAuthorDetailsAsync' + ).and.returnValue(Promise.resolve(BlogAuthorDetails)); spyOn(alertsService, 'addSuccessMessage'); component.updateAuthorDetails(); tick(); - expect(blogDashboardBackendApiService.updateAuthorDetailsAsync) - .toHaveBeenCalled(); + expect( + blogDashboardBackendApiService.updateAuthorDetailsAsync + ).toHaveBeenCalled(); expect(alertsService.addSuccessMessage).toHaveBeenCalledWith( - 'Author Details saved successfully.'); + 'Author Details saved successfully.' + ); })); it('should cancel updating author details', fakeAsync(() => { spyOn(ngbModal, 'open').and.returnValue({ componentInstance: { authorName: '', - authorBio: '' + authorBio: '', }, - result: Promise.reject() + result: Promise.reject(), } as NgbModalRef); spyOn(component, 'updateAuthorDetails'); component.authorBio = ''; @@ -456,29 +503,27 @@ describe('Blog Dashboard Page Component', () => { expect(component.updateAuthorDetails).not.toHaveBeenCalled(); })); - it('should successfully place call to update author details', fakeAsync( - () => { - component.authorBio = ''; - component.authorName = 'test username'; - let updatedAuthorDetails = { - authorName: 'username', - authorBio: 'general bio' - }; - spyOn(ngbModal, 'open').and.returnValue({ - componentInstance: { - authorName: '', - authorBio: '' - }, - result: Promise.resolve(updatedAuthorDetails) - } as NgbModalRef); - spyOn(component, 'updateAuthorDetails'); + it('should successfully place call to update author details', fakeAsync(() => { + component.authorBio = ''; + component.authorName = 'test username'; + let updatedAuthorDetails = { + authorName: 'username', + authorBio: 'general bio', + }; + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: { + authorName: '', + authorBio: '', + }, + result: Promise.resolve(updatedAuthorDetails), + } as NgbModalRef); + spyOn(component, 'updateAuthorDetails'); - component.showAuthorDetailsEditor(); - tick(); + component.showAuthorDetailsEditor(); + tick(); - expect(component.updateAuthorDetails).toHaveBeenCalled(); - expect(component.authorBio).toBe('general bio'); - expect(component.authorName).toBe('username'); - }) - ); + expect(component.updateAuthorDetails).toHaveBeenCalled(); + expect(component.authorBio).toBe('general bio'); + expect(component.authorName).toBe('username'); + })); }); diff --git a/core/templates/pages/blog-dashboard-page/blog-dashboard-page.component.ts b/core/templates/pages/blog-dashboard-page/blog-dashboard-page.component.ts index 8849efddeec3..94d26b96c325 100644 --- a/core/templates/pages/blog-dashboard-page/blog-dashboard-page.component.ts +++ b/core/templates/pages/blog-dashboard-page/blog-dashboard-page.component.ts @@ -16,22 +16,25 @@ * @fileoverview Component for the navbar breadcrumb of the blog dashboard. */ -import { Component, OnDestroy, OnInit} from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { AlertsService } from 'services/alerts.service'; -import { BlogDashboardData, BlogDashboardBackendApiService } from 'domain/blog/blog-dashboard-backend-api.service'; -import { LoaderService } from 'services/loader.service'; -import { Subscription } from 'rxjs'; -import { BlogDashboardPageService } from 'pages/blog-dashboard-page/services/blog-dashboard-page.service'; -import { BlogPostSummary } from 'domain/blog/blog-post-summary.model'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { BlogAuthorDetailsEditorComponent } from './modal-templates/author-detail-editor-modal.component'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { UserService } from 'services/user.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {AppConstants} from 'app.constants'; +import {AlertsService} from 'services/alerts.service'; +import { + BlogDashboardData, + BlogDashboardBackendApiService, +} from 'domain/blog/blog-dashboard-backend-api.service'; +import {LoaderService} from 'services/loader.service'; +import {Subscription} from 'rxjs'; +import {BlogDashboardPageService} from 'pages/blog-dashboard-page/services/blog-dashboard-page.service'; +import {BlogPostSummary} from 'domain/blog/blog-post-summary.model'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {BlogAuthorDetailsEditorComponent} from './modal-templates/author-detail-editor-modal.component'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {UserService} from 'services/user.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; @Component({ selector: 'oppia-blog-dashboard-page', - templateUrl: './blog-dashboard-page.component.html' + templateUrl: './blog-dashboard-page.component.html', }) export class BlogDashboardPageComponent implements OnInit, OnDestroy { // These properties are initialized using Angular lifecycle hooks @@ -70,14 +73,12 @@ export class BlogDashboardPageComponent implements OnInit, OnDestroy { }); this.directiveSubscriptions.add( - this.blogDashboardPageService.updateViewEventEmitter.subscribe( - () => { - this.activeTab = this.blogDashboardPageService.activeTab; - if (this.activeTab === 'main') { - this.initMainTab(); - } + this.blogDashboardPageService.updateViewEventEmitter.subscribe(() => { + this.activeTab = this.blogDashboardPageService.activeTab; + if (this.activeTab === 'main') { + this.initMainTab(); } - ) + }) ); } @@ -90,24 +91,25 @@ export class BlogDashboardPageComponent implements OnInit, OnDestroy { this.username = userInfo.getUsername(); if (this.username !== null) { - [this.authorProfilePicPngUrl, this.authorProfilePicWebpUrl] = ( - this.userService.getProfileImageDataUrl(this.username)); + [this.authorProfilePicPngUrl, this.authorProfilePicWebpUrl] = + this.userService.getProfileImageDataUrl(this.username); } else { - this.authorProfilePicWebpUrl = ( + this.authorProfilePicWebpUrl = this.urlInterpolationService.getStaticImageUrl( - AppConstants.DEFAULT_PROFILE_IMAGE_WEBP_PATH)); - this.authorProfilePicPngUrl = ( + AppConstants.DEFAULT_PROFILE_IMAGE_WEBP_PATH + ); + this.authorProfilePicPngUrl = this.urlInterpolationService.getStaticImageUrl( - AppConstants.DEFAULT_PROFILE_IMAGE_PNG_PATH)); + AppConstants.DEFAULT_PROFILE_IMAGE_PNG_PATH + ); } } - async initMainTab(): Promise { this.loaderService.showLoadingScreen('Loading'); await this.getUserInfoAsync(); this.blogDashboardBackendService.fetchBlogDashboardDataAsync().then( - (dashboardData) => { + dashboardData => { this.blogDashboardData = dashboardData; this.authorName = dashboardData.displayedAuthorName; this.authorBio = dashboardData.authorBio; @@ -115,39 +117,43 @@ export class BlogDashboardPageComponent implements OnInit, OnDestroy { if (this.authorBio.length === 0) { this.showAuthorDetailsEditor(); } - }, (errorResponse) => { + }, + errorResponse => { if (AppConstants.FATAL_ERROR_CODES.indexOf(errorResponse) !== -1) { this.alertsService.addWarning('Failed to get blog dashboard data'); } - }); + } + ); } createNewBlogPost(): void { this.blogDashboardBackendService.createBlogPostAsync().then( - (blogPostId) => { + blogPostId => { this.blogDashboardPageService.navigateToEditorTabWithId(blogPostId); - }, (error) => { + }, + error => { this.alertsService.addWarning( - `Unable to create new blog post.Error: ${error}`); + `Unable to create new blog post.Error: ${error}` + ); } ); } unpublishedBlogPost(blogPostSummary: BlogPostSummary): void { let summaryDicts = this.blogDashboardData.publishedBlogPostSummaryDicts; - let index = summaryDicts.indexOf( - blogPostSummary); + let index = summaryDicts.indexOf(blogPostSummary); if (index > -1) { summaryDicts.splice(index, 1); } - this.blogDashboardData.draftBlogPostSummaryDicts.unshift( - blogPostSummary); + this.blogDashboardData.draftBlogPostSummaryDicts.unshift(blogPostSummary); this.blogDashboardData.numOfDraftBlogPosts += 1; this.blogDashboardData.numOfPublishedBlogPosts -= 1; } removeBlogPost( - blogPostSummary: BlogPostSummary, blogPostWasPublished: boolean): void { + blogPostSummary: BlogPostSummary, + blogPostWasPublished: boolean + ): void { let summaryDicts: BlogPostSummary[]; if (blogPostWasPublished) { summaryDicts = this.blogDashboardData.publishedBlogPostSummaryDicts; @@ -156,8 +162,7 @@ export class BlogDashboardPageComponent implements OnInit, OnDestroy { summaryDicts = this.blogDashboardData.draftBlogPostSummaryDicts; this.blogDashboardData.numOfDraftBlogPosts -= 1; } - let index = summaryDicts.indexOf( - blogPostSummary); + let index = summaryDicts.indexOf(blogPostSummary); if (index > -1) { summaryDicts.splice(index, 1); } @@ -171,26 +176,34 @@ export class BlogDashboardPageComponent implements OnInit, OnDestroy { modelRef.componentInstance.authorName = this.authorName; modelRef.componentInstance.authorBio = this.authorBio; modelRef.componentInstance.prevAuthorBio = this.authorBio; - modelRef.result.then((authorDetails) => { - this.authorName = authorDetails.authorName; - this.authorBio = authorDetails.authorBio; - this.updateAuthorDetails(); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + modelRef.result.then( + authorDetails => { + this.authorName = authorDetails.authorName; + this.authorBio = authorDetails.authorBio; + this.updateAuthorDetails(); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } updateAuthorDetails(): void { - this.blogDashboardBackendService.updateAuthorDetailsAsync( - this.authorName, this.authorBio).then(() => { - this.alertsService.addSuccessMessage( - 'Author Details saved successfully.' + this.blogDashboardBackendService + .updateAuthorDetailsAsync(this.authorName, this.authorBio) + .then( + () => { + this.alertsService.addSuccessMessage( + 'Author Details saved successfully.' + ); + }, + error => { + this.alertsService.addWarning( + `Unable to update author details. Error: ${error}` + ); + } ); - }, (error) => { - this.alertsService.addWarning( - `Unable to update author details. Error: ${error}`); - }); } } diff --git a/core/templates/pages/blog-dashboard-page/blog-dashboard-page.constants.ts b/core/templates/pages/blog-dashboard-page/blog-dashboard-page.constants.ts index 23639dce8949..d2da395af7e9 100644 --- a/core/templates/pages/blog-dashboard-page/blog-dashboard-page.constants.ts +++ b/core/templates/pages/blog-dashboard-page/blog-dashboard-page.constants.ts @@ -26,12 +26,12 @@ export const BlogDashboardPageConstants = { BLOG_DASHBOARD_TAB_URLS: { PUBLISHED: '#/published', DRAFTS: '#/drafts', - BLOG_POST_EDITOR: '#/blog_post_editor/' + BLOG_POST_EDITOR: '#/blog_post_editor/', }, BLOG_POST_ACTIONS: { DELETE: 'delete', UNPUBLISH: 'unpublish', - PUBLISH: 'publish' - } + PUBLISH: 'publish', + }, } as const; diff --git a/core/templates/pages/blog-dashboard-page/blog-dashboard-page.import.ts b/core/templates/pages/blog-dashboard-page/blog-dashboard-page.import.ts index 59e81d301113..846e2d79e39d 100644 --- a/core/templates/pages/blog-dashboard-page/blog-dashboard-page.import.ts +++ b/core/templates/pages/blog-dashboard-page/blog-dashboard-page.import.ts @@ -22,26 +22,29 @@ import 'zone.js'; import uiValidate from 'angular-ui-validate'; angular.module('oppia', [ - require('angular-cookies'), 'ngAnimate', - 'ngMaterial', 'ngSanitize', 'ngTouch', 'pascalprecht.translate', - 'ui.bootstrap', uiValidate + require('angular-cookies'), + 'ngAnimate', + 'ngMaterial', + 'ngSanitize', + 'ngTouch', + 'pascalprecht.translate', + 'ui.bootstrap', + uiValidate, ]); require('Polyfills.ts'); // The module needs to be loaded directly after jquery since it defines the // main module the elements are attached to. -require( - 'pages/blog-dashboard-page/blog-dashboard-page.module.ts'); +require('pages/blog-dashboard-page/blog-dashboard-page.module.ts'); require('App.ts'); require('base-components/oppia-root.directive.ts'); -require( - 'pages/blog-dashboard-page/' + - 'blog-dashboard-page.component.ts' -); +require('pages/blog-dashboard-page/' + 'blog-dashboard-page.component.ts'); require( 'pages/blog-dashboard-page/navbar/' + - 'navbar-breadcrumb/blog-dashboard-navbar-breadcrumb.component.ts'); + 'navbar-breadcrumb/blog-dashboard-navbar-breadcrumb.component.ts' +); require( 'pages/blog-dashboard-page/navbar/' + - 'navbar-pre-logo-action/blog-post-editor-pre-logo-action.component.ts'); + 'navbar-pre-logo-action/blog-post-editor-pre-logo-action.component.ts' +); diff --git a/core/templates/pages/blog-dashboard-page/blog-dashboard-page.module.ts b/core/templates/pages/blog-dashboard-page/blog-dashboard-page.module.ts index b5d658e24a0f..5095e5c5ecb6 100644 --- a/core/templates/pages/blog-dashboard-page/blog-dashboard-page.module.ts +++ b/core/templates/pages/blog-dashboard-page/blog-dashboard-page.module.ts @@ -16,20 +16,20 @@ * @fileoverview Module for the blog-dashboard page. */ -import { MatTabsModule } from '@angular/material/tabs'; -import { MatMenuModule } from '@angular/material/menu'; -import { MatButtonToggleModule } from '@angular/material/button-toggle'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { RouterModule } from '@angular/router'; -import { BlogDashboardPageComponent } from 'pages/blog-dashboard-page/blog-dashboard-page.component'; -import { SharedBlogComponentsModule } from 'pages/blog-dashboard-page/shared-blog-components.module'; -import { NgModule } from '@angular/core'; -import { ToastrModule } from 'ngx-toastr'; -import { toastrConfig } from 'pages/oppia-root/app.module'; -import { BlogDashboardPageRootComponent } from './blog-dashboard-page-root.component'; -import { BlogAuthorDetailsEditorComponent } from './modal-templates/author-detail-editor-modal.component'; -import { BlogDashboardPageAuthGuard } from './blog-dashboard-page-auth.guard'; +import {MatTabsModule} from '@angular/material/tabs'; +import {MatMenuModule} from '@angular/material/menu'; +import {MatButtonToggleModule} from '@angular/material/button-toggle'; +import {MatTooltipModule} from '@angular/material/tooltip'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {RouterModule} from '@angular/router'; +import {BlogDashboardPageComponent} from 'pages/blog-dashboard-page/blog-dashboard-page.component'; +import {SharedBlogComponentsModule} from 'pages/blog-dashboard-page/shared-blog-components.module'; +import {NgModule} from '@angular/core'; +import {ToastrModule} from 'ngx-toastr'; +import {toastrConfig} from 'pages/oppia-root/app.module'; +import {BlogDashboardPageRootComponent} from './blog-dashboard-page-root.component'; +import {BlogAuthorDetailsEditorComponent} from './modal-templates/author-detail-editor-modal.component'; +import {BlogDashboardPageAuthGuard} from './blog-dashboard-page-auth.guard'; @NgModule({ imports: [ @@ -43,20 +43,19 @@ import { BlogDashboardPageAuthGuard } from './blog-dashboard-page-auth.guard'; { path: '', component: BlogDashboardPageRootComponent, - canActivate: [BlogDashboardPageAuthGuard] - } + canActivate: [BlogDashboardPageAuthGuard], + }, ]), MatButtonToggleModule, ], declarations: [ BlogDashboardPageComponent, BlogAuthorDetailsEditorComponent, - BlogDashboardPageRootComponent + BlogDashboardPageRootComponent, ], entryComponents: [ BlogDashboardPageComponent, - BlogAuthorDetailsEditorComponent - + BlogAuthorDetailsEditorComponent, ], }) export class BlogDashboardPageModule {} diff --git a/core/templates/pages/blog-dashboard-page/blog-dashboard-tile/blog-dashboard-tile.component.spec.ts b/core/templates/pages/blog-dashboard-page/blog-dashboard-tile/blog-dashboard-tile.component.spec.ts index 8c58de110684..9211f57ac222 100644 --- a/core/templates/pages/blog-dashboard-page/blog-dashboard-tile/blog-dashboard-tile.component.spec.ts +++ b/core/templates/pages/blog-dashboard-page/blog-dashboard-tile/blog-dashboard-tile.component.spec.ts @@ -16,20 +16,33 @@ * @fileoverview Unit tests for Blog Dashboard Tile component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { CapitalizePipe } from 'filters/string-utility-filters/capitalize.pipe'; -import { MockTranslatePipe, MockCapitalizePipe } from 'tests/unit-test-utils'; -import { BlogDashboardTileComponent } from './blog-dashboard-tile.component'; -import { BlogPostSummaryBackendDict, BlogPostSummary } from 'domain/blog/blog-post-summary.model'; -import { MatCardModule } from '@angular/material/card'; -import { MatMenuModule } from '@angular/material/menu'; -import { BlogDashboardPageService } from '../services/blog-dashboard-page.service'; -import { BlogPostEditorBackendApiService } from 'domain/blog/blog-post-editor-backend-api.service'; -import { NgbModal, NgbModalModule, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { BlogPostData } from 'domain/blog/blog-post.model'; -import { AlertsService } from 'services/alerts.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {CapitalizePipe} from 'filters/string-utility-filters/capitalize.pipe'; +import {MockTranslatePipe, MockCapitalizePipe} from 'tests/unit-test-utils'; +import {BlogDashboardTileComponent} from './blog-dashboard-tile.component'; +import { + BlogPostSummaryBackendDict, + BlogPostSummary, +} from 'domain/blog/blog-post-summary.model'; +import {MatCardModule} from '@angular/material/card'; +import {MatMenuModule} from '@angular/material/menu'; +import {BlogDashboardPageService} from '../services/blog-dashboard-page.service'; +import {BlogPostEditorBackendApiService} from 'domain/blog/blog-post-editor-backend-api.service'; +import { + NgbModal, + NgbModalModule, + NgbModalRef, +} from '@ng-bootstrap/ng-bootstrap'; +import {BlogPostData} from 'domain/blog/blog-post.model'; +import {AlertsService} from 'services/alerts.service'; describe('Blog Dashboard Tile Component', () => { let component: BlogDashboardTileComponent; @@ -56,22 +69,19 @@ describe('Blog Dashboard Tile Component', () => { HttpClientTestingModule, MatCardModule, MatMenuModule, - NgbModalModule - ], - declarations: [ - BlogDashboardTileComponent, - MockTranslatePipe + NgbModalModule, ], + declarations: [BlogDashboardTileComponent, MockTranslatePipe], providers: [ { provide: CapitalizePipe, - useClass: MockCapitalizePipe + useClass: MockCapitalizePipe, }, BlogDashboardPageService, BlogPostEditorBackendApiService, - AlertsService + AlertsService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -80,10 +90,12 @@ describe('Blog Dashboard Tile Component', () => { component = fixture.componentInstance; blogDashboardPageService = TestBed.inject(BlogDashboardPageService); blogPostEditorBackendApiService = TestBed.inject( - BlogPostEditorBackendApiService); + BlogPostEditorBackendApiService + ); ngbModal = TestBed.inject(NgbModal); sampleBlogPostData = BlogPostData.createFromBackendDict( - sampleBlogPostBackendDict); + sampleBlogPostBackendDict + ); alertsService = TestBed.inject(AlertsService); sampleBlogPostSummary = { id: 'sampleId', @@ -105,7 +117,8 @@ describe('Blog Dashboard Tile Component', () => { it('should initialize', () => { component.blogPostSummary = BlogPostSummary.createFromBackendDict( - sampleBlogPostSummary); + sampleBlogPostSummary + ); component.ngOnInit(); @@ -132,7 +145,8 @@ describe('Blog Dashboard Tile Component', () => { }; component.blogPostSummary = BlogPostSummary.createFromBackendDict( - sampleBlogPostSummary); + sampleBlogPostSummary + ); expect(() => { component.ngOnInit(); @@ -140,119 +154,128 @@ describe('Blog Dashboard Tile Component', () => { }).toThrowError(); })); - it('should get formatted date string from the timestamp in milliseconds', - () => { - // This corresponds to Fri, 21 Nov 2014 09:45:00 GMT. - let DATE = '11/21/2014'; - expect(component.getDateStringInWords(DATE)) - .toBe('Nov 21, 2014'); + it('should get formatted date string from the timestamp in milliseconds', () => { + // This corresponds to Fri, 21 Nov 2014 09:45:00 GMT. + let DATE = '11/21/2014'; + expect(component.getDateStringInWords(DATE)).toBe('Nov 21, 2014'); - DATE = '01/16/2027'; - expect(component.getDateStringInWords(DATE)) - .toBe('Jan 16, 2027'); + DATE = '01/16/2027'; + expect(component.getDateStringInWords(DATE)).toBe('Jan 16, 2027'); - DATE = '02/02/2018'; - expect(component.getDateStringInWords(DATE)) - .toBe('Feb 2, 2018'); - }); + DATE = '02/02/2018'; + expect(component.getDateStringInWords(DATE)).toBe('Feb 2, 2018'); + }); it('should navigate to blog post editor interface on clicking edit', () => { spyOn(blogDashboardPageService, 'navigateToEditorTabWithId'); component.blogPostSummary = BlogPostSummary.createFromBackendDict( - sampleBlogPostSummary); + sampleBlogPostSummary + ); component.editBlogPost(); - expect(blogDashboardPageService.navigateToEditorTabWithId) - .toHaveBeenCalledWith('sampleId'); + expect( + blogDashboardPageService.navigateToEditorTabWithId + ).toHaveBeenCalledWith('sampleId'); }); - it('should successfully place call to delete blog post model', fakeAsync( - () => { - spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.resolve() - } as NgbModalRef); - spyOn(blogDashboardPageService, 'deleteBlogPost'); - component.blogPostSummary = BlogPostSummary.createFromBackendDict( - sampleBlogPostSummary); + it('should successfully place call to delete blog post model', fakeAsync(() => { + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.resolve(), + } as NgbModalRef); + spyOn(blogDashboardPageService, 'deleteBlogPost'); + component.blogPostSummary = BlogPostSummary.createFromBackendDict( + sampleBlogPostSummary + ); - component.deleteBlogPost(); - tick(); + component.deleteBlogPost(); + tick(); - expect(blogDashboardPageService.deleteBlogPost).toHaveBeenCalled(); - })); + expect(blogDashboardPageService.deleteBlogPost).toHaveBeenCalled(); + })); it('should cancel delete blog post model', fakeAsync(() => { spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.reject() + result: Promise.reject(), } as NgbModalRef); spyOn(blogDashboardPageService, 'deleteBlogPost'); component.deleteBlogPost(); tick(); - expect(blogDashboardPageService.deleteBlogPost) - .not.toHaveBeenCalled(); + expect(blogDashboardPageService.deleteBlogPost).not.toHaveBeenCalled(); })); it('should unpublish blog post data successfully.', fakeAsync(() => { spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.resolve() + result: Promise.resolve(), } as NgbModalRef); component.blogPostSummary = BlogPostSummary.createFromBackendDict( - sampleBlogPostSummary); - spyOn(blogPostEditorBackendApiService, 'updateBlogPostDataAsync') - .and.returnValue(Promise.resolve({blogPostDict: sampleBlogPostData})); + sampleBlogPostSummary + ); + spyOn( + blogPostEditorBackendApiService, + 'updateBlogPostDataAsync' + ).and.returnValue(Promise.resolve({blogPostDict: sampleBlogPostData})); spyOn(component.unpublisedBlogPost, 'emit'); component.unpublishBlogPost(); tick(); - expect(blogPostEditorBackendApiService.updateBlogPostDataAsync) - .toHaveBeenCalled(); + expect( + blogPostEditorBackendApiService.updateBlogPostDataAsync + ).toHaveBeenCalled(); tick(); expect(component.unpublisedBlogPost.emit).toHaveBeenCalled(); })); + it( + 'should display error when unable to unpublish blog post data' + + ' successfully.', + fakeAsync(() => { + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.resolve(), + } as NgbModalRef); - it('should display error when unable to unpublish blog post data' + - ' successfully.', fakeAsync(() => { - spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.resolve() - } as NgbModalRef); - - component.blogPostSummary = BlogPostSummary.createFromBackendDict( - sampleBlogPostSummary); - spyOn(blogPostEditorBackendApiService, 'updateBlogPostDataAsync') - .and.returnValue(Promise.reject('status: 500')); - spyOn(component, 'unpublisedBlogPost'); - spyOn(alertsService, 'addWarning'); - - component.unpublishBlogPost(); - tick(); + component.blogPostSummary = BlogPostSummary.createFromBackendDict( + sampleBlogPostSummary + ); + spyOn( + blogPostEditorBackendApiService, + 'updateBlogPostDataAsync' + ).and.returnValue(Promise.reject('status: 500')); + spyOn(component, 'unpublisedBlogPost'); + spyOn(alertsService, 'addWarning'); + + component.unpublishBlogPost(); + tick(); - expect(blogPostEditorBackendApiService.updateBlogPostDataAsync) - .toHaveBeenCalled(); + expect( + blogPostEditorBackendApiService.updateBlogPostDataAsync + ).toHaveBeenCalled(); - tick(); + tick(); - expect(component.unpublisedBlogPost).not.toHaveBeenCalled(); - expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Failed to unpublish Blog Post. Internal Error: status: 500'); - })); + expect(component.unpublisedBlogPost).not.toHaveBeenCalled(); + expect(alertsService.addWarning).toHaveBeenCalledWith( + 'Failed to unpublish Blog Post. Internal Error: status: 500' + ); + }) + ); it('should not unpublish blog post data if cancelled.', fakeAsync(() => { spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.reject() + result: Promise.reject(), } as NgbModalRef); spyOn(blogPostEditorBackendApiService, 'updateBlogPostDataAsync'); component.unpublishBlogPost(); tick(); - expect(blogPostEditorBackendApiService.updateBlogPostDataAsync) - .not.toHaveBeenCalled(); + expect( + blogPostEditorBackendApiService.updateBlogPostDataAsync + ).not.toHaveBeenCalled(); })); }); diff --git a/core/templates/pages/blog-dashboard-page/blog-dashboard-tile/blog-dashboard-tile.component.ts b/core/templates/pages/blog-dashboard-page/blog-dashboard-tile/blog-dashboard-tile.component.ts index 8142a6402eaa..280ac7ae1dc3 100644 --- a/core/templates/pages/blog-dashboard-page/blog-dashboard-tile/blog-dashboard-tile.component.ts +++ b/core/templates/pages/blog-dashboard-page/blog-dashboard-tile/blog-dashboard-tile.component.ts @@ -16,19 +16,19 @@ * @fileoverview Component for a blog dashboard card. */ -import { Component, Input, OnInit, Output, EventEmitter } from '@angular/core'; -import { BlogPostSummary } from 'domain/blog/blog-post-summary.model'; +import {Component, Input, OnInit, Output, EventEmitter} from '@angular/core'; +import {BlogPostSummary} from 'domain/blog/blog-post-summary.model'; import dayjs from 'dayjs'; -import { BlogDashboardPageService } from '../services/blog-dashboard-page.service'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { BlogPostActionConfirmationModalComponent } from 'pages/blog-dashboard-page/blog-post-action-confirmation/blog-post-action-confirmation.component'; -import { BlogPostEditorBackendApiService } from 'domain/blog/blog-post-editor-backend-api.service'; -import { AlertsService } from 'services/alerts.service'; -import { TruncatePipe } from 'filters/string-utility-filters/truncate.pipe'; +import {BlogDashboardPageService} from '../services/blog-dashboard-page.service'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {BlogPostActionConfirmationModalComponent} from 'pages/blog-dashboard-page/blog-post-action-confirmation/blog-post-action-confirmation.component'; +import {BlogPostEditorBackendApiService} from 'domain/blog/blog-post-editor-backend-api.service'; +import {AlertsService} from 'services/alerts.service'; +import {TruncatePipe} from 'filters/string-utility-filters/truncate.pipe'; @Component({ selector: 'oppia-blog-dashboard-tile', - templateUrl: './blog-dashboard-tile.component.html' + templateUrl: './blog-dashboard-tile.component.html', }) export class BlogDashboardTileComponent implements OnInit { // These properties are initialized using Angular lifecycle hooks @@ -46,7 +46,7 @@ export class BlogDashboardTileComponent implements OnInit { private blogPostEditorBackendService: BlogPostEditorBackendApiService, private ngbModal: NgbModal, private alertsService: AlertsService, - private truncatePipe: TruncatePipe, + private truncatePipe: TruncatePipe ) {} ngOnInit(): void { @@ -58,54 +58,69 @@ export class BlogDashboardTileComponent implements OnInit { // Truncating the summary to 220 characters to avoid display in blog // dashboard tile to avoid overflow of text outside the tile. this.summaryContent = this.truncatePipe.transform( - this.blogPostSummary.summary, 220); + this.blogPostSummary.summary, + 220 + ); } getDateStringInWords(naiveDate: string): string { - return dayjs( - naiveDate.split(',')[0], 'MM-DD-YYYY').format('MMM D, YYYY'); + return dayjs(naiveDate.split(',')[0], 'MM-DD-YYYY').format('MMM D, YYYY'); } editBlogPost(): void { this.blogDashboardPageService.navigateToEditorTabWithId( - this.blogPostSummary.id); + this.blogPostSummary.id + ); } deleteBlogPost(): void { this.blogDashboardPageService.blogPostAction = 'delete'; - this.ngbModal.open(BlogPostActionConfirmationModalComponent, { - backdrop: 'static', - keyboard: false, - }).result.then(() => { - this.blogDashboardPageService.blogPostId = this.blogPostSummary.id; - this.blogDashboardPageService.deleteBlogPost(); - this.deletedBlogPost.emit(); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + this.ngbModal + .open(BlogPostActionConfirmationModalComponent, { + backdrop: 'static', + keyboard: false, + }) + .result.then( + () => { + this.blogDashboardPageService.blogPostId = this.blogPostSummary.id; + this.blogDashboardPageService.deleteBlogPost(); + this.deletedBlogPost.emit(); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } unpublishBlogPost(): void { this.blogDashboardPageService.blogPostAction = 'unpublish'; - this.ngbModal.open(BlogPostActionConfirmationModalComponent, { - backdrop: 'static', - keyboard: false, - }).result.then(() => { - this.blogPostEditorBackendService.updateBlogPostDataAsync( - this.blogPostSummary.id, false, {}).then( + this.ngbModal + .open(BlogPostActionConfirmationModalComponent, { + backdrop: 'static', + keyboard: false, + }) + .result.then( + () => { + this.blogPostEditorBackendService + .updateBlogPostDataAsync(this.blogPostSummary.id, false, {}) + .then( + () => { + this.unpublisedBlogPost.emit(); + }, + errorResponse => { + this.alertsService.addWarning( + `Failed to unpublish Blog Post. Internal Error: ${errorResponse}` + ); + } + ); + }, () => { - this.unpublisedBlogPost.emit(); - }, (errorResponse) => { - this.alertsService.addWarning( - `Failed to unpublish Blog Post. Internal Error: ${errorResponse}`); + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. } ); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); } } diff --git a/core/templates/pages/blog-dashboard-page/blog-post-action-confirmation/blog-post-action-confirmation.component.spec.ts b/core/templates/pages/blog-dashboard-page/blog-post-action-confirmation/blog-post-action-confirmation.component.spec.ts index 5fb41b04353e..07bfa7432607 100644 --- a/core/templates/pages/blog-dashboard-page/blog-post-action-confirmation/blog-post-action-confirmation.component.spec.ts +++ b/core/templates/pages/blog-dashboard-page/blog-post-action-confirmation/blog-post-action-confirmation.component.spec.ts @@ -16,11 +16,11 @@ * @fileoverview Unit tests for blog post action confirmation component. */ -import { NgbActiveModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { BlogPostActionConfirmationModalComponent } from './blog-post-action-confirmation.component'; -import { BlogDashboardPageService } from 'pages/blog-dashboard-page/services/blog-dashboard-page.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; +import {NgbActiveModal, NgbModalModule} from '@ng-bootstrap/ng-bootstrap'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {BlogPostActionConfirmationModalComponent} from './blog-post-action-confirmation.component'; +import {BlogDashboardPageService} from 'pages/blog-dashboard-page/services/blog-dashboard-page.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; class MockActiveModal { dismiss(): void { @@ -35,27 +35,22 @@ class MockActiveModal { describe('Blog Post Action Confirmation Modal Component', () => { let component: BlogPostActionConfirmationModalComponent; let blogDashboardPageService: BlogDashboardPageService; - let fixture: ComponentFixture< - BlogPostActionConfirmationModalComponent>; + let fixture: ComponentFixture; let ngbActiveModal: NgbActiveModal; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - NgbModalModule, - ], + imports: [HttpClientTestingModule, NgbModalModule], declarations: [BlogPostActionConfirmationModalComponent], providers: [ BlogDashboardPageService, { provide: NgbActiveModal, - useClass: MockActiveModal - } - ] + useClass: MockActiveModal, + }, + ], }).compileComponents(); - fixture = TestBed.createComponent( - BlogPostActionConfirmationModalComponent); + fixture = TestBed.createComponent(BlogPostActionConfirmationModalComponent); component = fixture.componentInstance; ngbActiveModal = TestBed.inject(NgbActiveModal); blogDashboardPageService = TestBed.inject(BlogDashboardPageService); diff --git a/core/templates/pages/blog-dashboard-page/blog-post-action-confirmation/blog-post-action-confirmation.component.ts b/core/templates/pages/blog-dashboard-page/blog-post-action-confirmation/blog-post-action-confirmation.component.ts index 69cf17e3df5b..b657732b2a72 100644 --- a/core/templates/pages/blog-dashboard-page/blog-post-action-confirmation/blog-post-action-confirmation.component.ts +++ b/core/templates/pages/blog-dashboard-page/blog-post-action-confirmation/blog-post-action-confirmation.component.ts @@ -16,26 +16,28 @@ * @fileoverview Component for confirming blog post actions. */ -import { Component, OnInit } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { BlogDashboardPageService } from 'pages/blog-dashboard-page/services/blog-dashboard-page.service'; -import { BlogDashboardPageConstants } from 'pages/blog-dashboard-page/blog-dashboard-page.constants'; +import {Component, OnInit} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {BlogDashboardPageService} from 'pages/blog-dashboard-page/services/blog-dashboard-page.service'; +import {BlogDashboardPageConstants} from 'pages/blog-dashboard-page/blog-dashboard-page.constants'; @Component({ selector: 'oppia-blog-post-action-confirmation-modal', templateUrl: './blog-post-action-confirmation.component.html', - styleUrls: [] + styleUrls: [], }) export class BlogPostActionConfirmationModalComponent - extends ConfirmOrCancelModal implements OnInit { + extends ConfirmOrCancelModal + implements OnInit +{ // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 blogPostAction!: string; constructor( - ngbActiveModal: NgbActiveModal, - private blogDashboardPageService: BlogDashboardPageService, + ngbActiveModal: NgbActiveModal, + private blogDashboardPageService: BlogDashboardPageService ) { super(ngbActiveModal); } @@ -45,17 +47,23 @@ export class BlogPostActionConfirmationModalComponent } isActionDelete(): boolean { - return this.blogPostAction === ( - BlogDashboardPageConstants.BLOG_POST_ACTIONS.DELETE); + return ( + this.blogPostAction === + BlogDashboardPageConstants.BLOG_POST_ACTIONS.DELETE + ); } isActionPublish(): boolean { - return this.blogPostAction === ( - BlogDashboardPageConstants.BLOG_POST_ACTIONS.PUBLISH); + return ( + this.blogPostAction === + BlogDashboardPageConstants.BLOG_POST_ACTIONS.PUBLISH + ); } isActionUnpublish(): boolean { - return this.blogPostAction === ( - BlogDashboardPageConstants.BLOG_POST_ACTIONS.UNPUBLISH); + return ( + this.blogPostAction === + BlogDashboardPageConstants.BLOG_POST_ACTIONS.UNPUBLISH + ); } } diff --git a/core/templates/pages/blog-dashboard-page/blog-post-editor/blog-post-editor.component.spec.ts b/core/templates/pages/blog-dashboard-page/blog-post-editor/blog-post-editor.component.spec.ts index 379406379686..a241b5da30f7 100644 --- a/core/templates/pages/blog-dashboard-page/blog-post-editor/blog-post-editor.component.spec.ts +++ b/core/templates/pages/blog-dashboard-page/blog-post-editor/blog-post-editor.component.spec.ts @@ -16,33 +16,43 @@ * @fileoverview Unit tests for blog post editor. */ -import { ChangeDetectorRef, ElementRef, NO_ERRORS_SCHEMA } from '@angular/core'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, TestBed, waitForAsync, fakeAsync, tick } from '@angular/core/testing'; -import { MatButtonToggleModule } from '@angular/material/button-toggle'; -import { MatCardModule } from '@angular/material/card'; -import { NgbModal, NgbModalModule, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { CapitalizePipe } from 'filters/string-utility-filters/capitalize.pipe'; -import { MaterialModule } from 'modules/material.module'; -import { BlogDashboardPageService } from 'pages/blog-dashboard-page/services/blog-dashboard-page.service'; -import { BlogPostEditorComponent } from './blog-post-editor.component'; -import { BlogPostEditorBackendApiService } from 'domain/blog/blog-post-editor-backend-api.service'; -import { LoaderService } from 'services/loader.service'; -import { AlertsService } from 'services/alerts.service'; -import { MockTranslatePipe, MockCapitalizePipe } from 'tests/unit-test-utils'; -import { BlogPostData } from 'domain/blog/blog-post.model'; -import { AppConstants } from 'app.constants'; -import { UrlService } from 'services/contextual/url.service'; -import { BlogPostUpdateService } from 'domain/blog/blog-post-update.service'; -import { ImageLocalStorageService } from 'services/image-local-storage.service'; -import { FormsModule } from '@angular/forms'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { UploadBlogPostThumbnailComponent } from '../modal-templates/upload-blog-post-thumbnail.component'; -import { ImageUploaderComponent } from 'components/forms/custom-forms-directives/image-uploader.component'; -import { PreventPageUnloadEventService } from 'services/prevent-page-unload-event.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { UserService } from 'services/user.service'; -import { UserInfo } from 'domain/user/user-info.model'; +import {ChangeDetectorRef, ElementRef, NO_ERRORS_SCHEMA} from '@angular/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import { + ComponentFixture, + TestBed, + waitForAsync, + fakeAsync, + tick, +} from '@angular/core/testing'; +import {MatButtonToggleModule} from '@angular/material/button-toggle'; +import {MatCardModule} from '@angular/material/card'; +import { + NgbModal, + NgbModalModule, + NgbModalRef, +} from '@ng-bootstrap/ng-bootstrap'; +import {CapitalizePipe} from 'filters/string-utility-filters/capitalize.pipe'; +import {MaterialModule} from 'modules/material.module'; +import {BlogDashboardPageService} from 'pages/blog-dashboard-page/services/blog-dashboard-page.service'; +import {BlogPostEditorComponent} from './blog-post-editor.component'; +import {BlogPostEditorBackendApiService} from 'domain/blog/blog-post-editor-backend-api.service'; +import {LoaderService} from 'services/loader.service'; +import {AlertsService} from 'services/alerts.service'; +import {MockTranslatePipe, MockCapitalizePipe} from 'tests/unit-test-utils'; +import {BlogPostData} from 'domain/blog/blog-post.model'; +import {AppConstants} from 'app.constants'; +import {UrlService} from 'services/contextual/url.service'; +import {BlogPostUpdateService} from 'domain/blog/blog-post-update.service'; +import {ImageLocalStorageService} from 'services/image-local-storage.service'; +import {FormsModule} from '@angular/forms'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {UploadBlogPostThumbnailComponent} from '../modal-templates/upload-blog-post-thumbnail.component'; +import {ImageUploaderComponent} from 'components/forms/custom-forms-directives/image-uploader.component'; +import {PreventPageUnloadEventService} from 'services/prevent-page-unload-event.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {UserService} from 'services/user.service'; +import {UserInfo} from 'domain/user/user-info.model'; describe('Blog Post Editor Component', () => { let fixture: ComponentFixture; @@ -77,11 +87,11 @@ describe('Blog Post Editor Component', () => { location: { href: '', hash: '/', - reload: () => {} + reload: () => {}, }, sessionStorage: { clear: () => {}, - } + }, }; } @@ -99,16 +109,16 @@ describe('Blog Post Editor Component', () => { BlogPostEditorComponent, UploadBlogPostThumbnailComponent, ImageUploaderComponent, - MockTranslatePipe + MockTranslatePipe, ], providers: [ { provide: CapitalizePipe, - useClass: MockCapitalizePipe + useClass: MockCapitalizePipe, }, { provide: WindowRef, - useClass: MockWindowRef + useClass: MockWindowRef, }, { provide: WindowDimensionsService, @@ -119,12 +129,12 @@ describe('Blog Post Editor Component', () => { subscribe: (callb: () => void) => { callb(); return { - unsubscribe() {} + unsubscribe() {}, }; - } + }, }; - } - } + }, + }, }, PreventPageUnloadEventService, BlogDashboardPageService, @@ -134,7 +144,7 @@ describe('Blog Post Editor Component', () => { AlertsService, UrlService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -145,22 +155,27 @@ describe('Blog Post Editor Component', () => { blogDashboardPageService = TestBed.inject(BlogDashboardPageService); loaderService = TestBed.inject(LoaderService); blogPostEditorBackendApiService = TestBed.inject( - BlogPostEditorBackendApiService); + BlogPostEditorBackendApiService + ); alertsService = TestBed.inject(AlertsService); blogPostUpdateService = TestBed.inject(BlogPostUpdateService); imageLocalStorageService = TestBed.inject(ImageLocalStorageService); windowDimensionsService = TestBed.inject(WindowDimensionsService); preventPageUnloadEventService = TestBed.inject( - PreventPageUnloadEventService); + PreventPageUnloadEventService + ); ngbModal = TestBed.inject(NgbModal); userService = TestBed.inject(UserService); sampleBlogPostData = BlogPostData.createFromBackendDict( - sampleBlogPostBackendDict); + sampleBlogPostBackendDict + ); spyOn(urlService, 'getBlogPostIdFromUrl').and.returnValue('sampleBlogId'); spyOn(preventPageUnloadEventService, 'addListener'); spyOn(preventPageUnloadEventService, 'removeListener'); - spyOn(userService, 'getProfileImageDataUrl').and.returnValue( - ['default-image-url-png', 'default-image-url-webp']); + spyOn(userService, 'getProfileImageDataUrl').and.returnValue([ + 'default-image-url-png', + 'default-image-url-webp', + ]); component.ngOnInit(); component.titleInput = new ElementRef(document.createElement('div')); }); @@ -169,7 +184,7 @@ describe('Blog Post Editor Component', () => { expect(component).toBeDefined(); }); - it('should initialize', fakeAsync (() => { + it('should initialize', fakeAsync(() => { const sampleUserInfoBackendObject = { roles: ['USER_ROLE'], is_moderator: false, @@ -180,16 +195,18 @@ describe('Blog Post Editor Component', () => { preferred_site_language_code: null, username: 'tester', email: 'test@test.com', - user_is_logged_in: true + user_is_logged_in: true, }; const sampleUserInfo = UserInfo.createFromBackendDict( - sampleUserInfoBackendObject); + sampleUserInfoBackendObject + ); spyOn(loaderService, 'showLoadingScreen'); spyOn(loaderService, 'hideLoadingScreen'); spyOn(component, 'initEditor'); spyOn(windowDimensionsService, 'isWindowNarrow').and.callThrough(); spyOn(userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(sampleUserInfo)); + Promise.resolve(sampleUserInfo) + ); component.ngOnInit(); tick(); @@ -197,37 +214,37 @@ describe('Blog Post Editor Component', () => { expect(loaderService.showLoadingScreen).toHaveBeenCalled(); expect(component.blogPostId).toEqual(''); expect(component.MAX_CHARS_IN_BLOG_POST_TITLE).toBe( - AppConstants.MAX_CHARS_IN_BLOG_POST_TITLE); + AppConstants.MAX_CHARS_IN_BLOG_POST_TITLE + ); expect(component.initEditor).toHaveBeenCalled; expect(component.authorProfilePicPngUrl).toEqual('default-image-url-png'); - expect(component.authorProfilePicWebpUrl).toEqual( - 'default-image-url-webp'); + expect(component.authorProfilePicWebpUrl).toEqual('default-image-url-webp'); expect(windowDimensionsService.isWindowNarrow).toHaveBeenCalled(); expect(component.windowIsNarrow).toBe(true); expect(loaderService.hideLoadingScreen).not.toHaveBeenCalled(); })); - it('should set default profile pictures when username is null', - fakeAsync(() => { - let userInfo = { - getUsername: () => null, - isSuperAdmin: () => true - }; - spyOn(component, 'initEditor'); - spyOn(loaderService, 'showLoadingScreen'); - spyOn(loaderService, 'hideLoadingScreen'); - spyOn(windowDimensionsService, 'isWindowNarrow').and.callThrough(); - spyOn(userService, 'getUserInfoAsync') - .and.resolveTo(userInfo as UserInfo); - - component.ngOnInit(); - tick(); + it('should set default profile pictures when username is null', fakeAsync(() => { + let userInfo = { + getUsername: () => null, + isSuperAdmin: () => true, + }; + spyOn(component, 'initEditor'); + spyOn(loaderService, 'showLoadingScreen'); + spyOn(loaderService, 'hideLoadingScreen'); + spyOn(windowDimensionsService, 'isWindowNarrow').and.callThrough(); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo as UserInfo); + + component.ngOnInit(); + tick(); - expect(component.authorProfilePicPngUrl).toEqual( - '/assets/images/avatar/user_blue_150px.png'); - expect(component.authorProfilePicWebpUrl).toEqual( - '/assets/images/avatar/user_blue_150px.webp'); - })); + expect(component.authorProfilePicPngUrl).toEqual( + '/assets/images/avatar/user_blue_150px.png' + ); + expect(component.authorProfilePicWebpUrl).toEqual( + '/assets/images/avatar/user_blue_150px.webp' + ); + })); it('should set image uploader window size', () => { component.uploadedImageDataUrl = 'image.png'; @@ -239,8 +256,7 @@ describe('Blog Post Editor Component', () => { }); it('should get schema', () => { - expect(component.getSchema()) - .toEqual(component.HTML_SCHEMA); + expect(component.getSchema()).toEqual(component.HTML_SCHEMA); }); it('should successfully fetch blog post editor data', fakeAsync(() => { @@ -252,24 +268,28 @@ describe('Blog Post Editor Component', () => { }; component.blogPostId = 'sampleBlogId'; component.titleEditorIsActive = false; - spyOn(blogPostEditorBackendApiService, 'fetchBlogPostEditorData') - .and.returnValue(Promise.resolve(blogPostEditorData)); + spyOn( + blogPostEditorBackendApiService, + 'fetchBlogPostEditorData' + ).and.returnValue(Promise.resolve(blogPostEditorData)); component.initEditor(); tick(); - expect(blogPostEditorBackendApiService.fetchBlogPostEditorData) - .toHaveBeenCalled(); + expect( + blogPostEditorBackendApiService.fetchBlogPostEditorData + ).toHaveBeenCalled(); expect(component.authorName).toEqual('test_user'); expect(component.blogPostData).toEqual(sampleBlogPostData); expect(component.defaultTagsList).toEqual(['news', 'Learners']); expect(component.maxAllowedTags).toEqual(2); expect(component.thumbnailDataUrl).toEqual( - '/assetsdevhandler/blog_post/sampleBlogId/assets/thumbnail' + - '/image.png'); + '/assetsdevhandler/blog_post/sampleBlogId/assets/thumbnail' + '/image.png' + ); expect(blogDashboardPageService.imageUploaderIsNarrow).toBeTrue(); expect(component.dateTimeLastSaved).toEqual( - 'November 21, 2014 at 04:52 AM'); + 'November 21, 2014 at 04:52 AM' + ); expect(component.title).toEqual('sample title'); expect(component.contentEditorIsActive).toBeFalse(); expect(component.lastChangesWerePublished).toBeTrue(); @@ -277,117 +297,130 @@ describe('Blog Post Editor Component', () => { expect(preventPageUnloadEventService.removeListener).toHaveBeenCalled(); })); - it('should activate title editor if blog post does not have a title when it' + - ' loads', fakeAsync(() => { - let sampleBackendDict = { - id: 'sampleBlogId', - displayed_author_name: 'test_user', - title: '', - content: '', - thumbnail_filename: null, - tags: [], - url_fragment: '', - }; - let blogPostEditorData = { - displayedAuthorName: 'test_user', - listOfDefaulTags: ['news', 'Learners'], - maxNumOfTags: 2, - blogPostDict: BlogPostData.createFromBackendDict( - sampleBackendDict), - }; - component.blogPostId = 'sampleBlogId'; - component.titleEditorIsActive = false; - spyOn(blogPostEditorBackendApiService, 'fetchBlogPostEditorData') - .and.returnValue(Promise.resolve(blogPostEditorData)); - - component.initEditor(); - tick(); - - expect(blogPostEditorBackendApiService.fetchBlogPostEditorData) - .toHaveBeenCalled(); - expect(component.titleEditorIsActive).toBeTrue(); - })); - - - it('should display alert when unable to fetch blog post editor data', - fakeAsync(() => { - spyOn(blogPostEditorBackendApiService, 'fetchBlogPostEditorData') - .and.returnValue(Promise.reject(500)); - spyOn(blogDashboardPageService, 'navigateToMainTab'); - spyOn(alertsService, 'addWarning'); - - component.initEditor(); - tick(); - - expect(blogPostEditorBackendApiService.fetchBlogPostEditorData) - .toHaveBeenCalled(); - expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Failed to get Blog Post Data. The Blog Post was either' + - ' deleted or the Blog Post ID is invalid.'); - expect(blogDashboardPageService.navigateToMainTab).toHaveBeenCalled(); - })); - - it('should update local title value when title is unique and valid', + it( + 'should activate title editor if blog post does not have a title when it' + + ' loads', fakeAsync(() => { + let sampleBackendDict = { + id: 'sampleBlogId', + displayed_author_name: 'test_user', + title: '', + content: '', + thumbnail_filename: null, + tags: [], + url_fragment: '', + }; + let blogPostEditorData = { + displayedAuthorName: 'test_user', + listOfDefaulTags: ['news', 'Learners'], + maxNumOfTags: 2, + blogPostDict: BlogPostData.createFromBackendDict(sampleBackendDict), + }; + component.blogPostId = 'sampleBlogId'; + component.titleEditorIsActive = false; spyOn( blogPostEditorBackendApiService, - 'doesPostWithGivenTitleAlreadyExistAsync' - ).and.returnValue(Promise.resolve(false)); - spyOn(blogPostUpdateService, 'setBlogPostTitle'); - component.title = 'Sample title changed'; + 'fetchBlogPostEditorData' + ).and.returnValue(Promise.resolve(blogPostEditorData)); - component.blogPostData = sampleBlogPostData; - component.updateLocalTitleValue(); + component.initEditor(); + tick(); - expect(blogPostUpdateService.setBlogPostTitle).toHaveBeenCalledWith( - sampleBlogPostData, 'Sample title changed'); - expect(preventPageUnloadEventService.addListener).toHaveBeenCalled(); - expect(component.blogPostData.titleIsDuplicate).toBeFalse(); + expect( + blogPostEditorBackendApiService.fetchBlogPostEditorData + ).toHaveBeenCalled(); + expect(component.titleEditorIsActive).toBeTrue(); }) ); - it('should not update title and should raise error when the title is' + - ' duplicate', fakeAsync(() => { + it('should display alert when unable to fetch blog post editor data', fakeAsync(() => { spyOn( blogPostEditorBackendApiService, - 'doesPostWithGivenTitleAlreadyExistAsync' - ).and.returnValue(Promise.resolve(true)); + 'fetchBlogPostEditorData' + ).and.returnValue(Promise.reject(500)); + spyOn(blogDashboardPageService, 'navigateToMainTab'); spyOn(alertsService, 'addWarning'); - component.title = 'Sample title changed'; - component.blogPostData = sampleBlogPostData; - component.updateLocalTitleValue(); + component.initEditor(); tick(); + expect( + blogPostEditorBackendApiService.fetchBlogPostEditorData + ).toHaveBeenCalled(); expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Blog Post with the given title exists already. Please use a' + - ' different title.' + 'Failed to get Blog Post Data. The Blog Post was either' + + ' deleted or the Blog Post ID is invalid.' ); - expect(preventPageUnloadEventService.addListener).toHaveBeenCalled(); - expect(component.blogPostData.titleIsDuplicate).toBeTrue(); + expect(blogDashboardPageService.navigateToMainTab).toHaveBeenCalled(); })); - it('should not update title and should raise error when the checking for' + - 'uniqueness of the title fails', fakeAsync(() => { + it('should update local title value when title is unique and valid', fakeAsync(() => { spyOn( blogPostEditorBackendApiService, 'doesPostWithGivenTitleAlreadyExistAsync' - ).and.returnValue(Promise.reject('Internal Server Error')); - spyOn(alertsService, 'addWarning'); + ).and.returnValue(Promise.resolve(false)); + spyOn(blogPostUpdateService, 'setBlogPostTitle'); component.title = 'Sample title changed'; component.blogPostData = sampleBlogPostData; component.updateLocalTitleValue(); - tick(); - expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Failed to check if title is unique. ' + - 'Internal Error: Internal Server Error' + expect(blogPostUpdateService.setBlogPostTitle).toHaveBeenCalledWith( + sampleBlogPostData, + 'Sample title changed' ); expect(preventPageUnloadEventService.addListener).toHaveBeenCalled(); expect(component.blogPostData.titleIsDuplicate).toBeFalse(); })); + it( + 'should not update title and should raise error when the title is' + + ' duplicate', + fakeAsync(() => { + spyOn( + blogPostEditorBackendApiService, + 'doesPostWithGivenTitleAlreadyExistAsync' + ).and.returnValue(Promise.resolve(true)); + spyOn(alertsService, 'addWarning'); + component.title = 'Sample title changed'; + + component.blogPostData = sampleBlogPostData; + component.updateLocalTitleValue(); + tick(); + + expect(alertsService.addWarning).toHaveBeenCalledWith( + 'Blog Post with the given title exists already. Please use a' + + ' different title.' + ); + expect(preventPageUnloadEventService.addListener).toHaveBeenCalled(); + expect(component.blogPostData.titleIsDuplicate).toBeTrue(); + }) + ); + + it( + 'should not update title and should raise error when the checking for' + + 'uniqueness of the title fails', + fakeAsync(() => { + spyOn( + blogPostEditorBackendApiService, + 'doesPostWithGivenTitleAlreadyExistAsync' + ).and.returnValue(Promise.reject('Internal Server Error')); + spyOn(alertsService, 'addWarning'); + component.title = 'Sample title changed'; + + component.blogPostData = sampleBlogPostData; + component.updateLocalTitleValue(); + tick(); + + expect(alertsService.addWarning).toHaveBeenCalledWith( + 'Failed to check if title is unique. ' + + 'Internal Error: Internal Server Error' + ); + expect(preventPageUnloadEventService.addListener).toHaveBeenCalled(); + expect(component.blogPostData.titleIsDuplicate).toBeFalse(); + }) + ); + it('should validate title pattern', () => { // A valid title contains words (containing a-zA-Z0-9) separated by spaces, // hyphens(-), ampersand(&) and colon(:). @@ -408,14 +441,15 @@ describe('Blog Post Editor Component', () => { it('should update local edited content', () => { const changeDetectorRef = - fixture.debugElement.injector.get(ChangeDetectorRef); - const detectChangesSpy = - spyOn(changeDetectorRef.constructor.prototype, 'detectChanges'); + fixture.debugElement.injector.get(ChangeDetectorRef); + const detectChangesSpy = spyOn( + changeDetectorRef.constructor.prototype, + 'detectChanges' + ); component.localEditedContent = ''; component.updateLocalEditedContent('

Hello Worlds

'); - expect(component.localEditedContent).toBe( - '

Hello Worlds

'); + expect(component.localEditedContent).toBe('

Hello Worlds

'); expect(detectChangesSpy).toHaveBeenCalled(); }); @@ -428,7 +462,9 @@ describe('Blog Post Editor Component', () => { tick(); expect(blogPostUpdateService.setBlogPostContent).toHaveBeenCalledWith( - sampleBlogPostData, '

Sample content changed

'); + sampleBlogPostData, + '

Sample content changed

' + ); expect(component.contentEditorIsActive).toBe(false); expect(preventPageUnloadEventService.addListener).toHaveBeenCalled(); })); @@ -442,139 +478,152 @@ describe('Blog Post Editor Component', () => { expect(component.contentEditorIsActive).toBe(false); }); - it('should cancel edit of blog post content and should not' + - ' close RTE if content is empty', () => { - component.blogPostData = sampleBlogPostData; - component.blogPostData.content = ''; - component.contentEditorIsActive = true; + it( + 'should cancel edit of blog post content and should not' + + ' close RTE if content is empty', + () => { + component.blogPostData = sampleBlogPostData; + component.blogPostData.content = ''; + component.contentEditorIsActive = true; - component.cancelEdit(); + component.cancelEdit(); - expect(component.contentEditorIsActive).toBe(true); - }); + expect(component.contentEditorIsActive).toBe(true); + } + ); - it('should call update blog post if blog post passes validation' + - 'when user saves blog post as draft', () => { - spyOn(component, 'updateBlogPostData'); - spyOn(sampleBlogPostData, 'validate').and.returnValue([]); - component.blogPostData = sampleBlogPostData; + it( + 'should call update blog post if blog post passes validation' + + 'when user saves blog post as draft', + () => { + spyOn(component, 'updateBlogPostData'); + spyOn(sampleBlogPostData, 'validate').and.returnValue([]); + component.blogPostData = sampleBlogPostData; - component.saveDraft(); + component.saveDraft(); - expect(component.saveInProgress).toBeTrue(); - expect(component.updateBlogPostData).toHaveBeenCalledWith(false); - }); + expect(component.saveInProgress).toBeTrue(); + expect(component.updateBlogPostData).toHaveBeenCalledWith(false); + } + ); - it('should call raise errors if blog post does not pass validation' + - 'when user saves blog post as draft', () => { - spyOn(component, 'updateBlogPostData'); - spyOn(alertsService, 'addWarning'); + it( + 'should call raise errors if blog post does not pass validation' + + 'when user saves blog post as draft', + () => { + spyOn(component, 'updateBlogPostData'); + spyOn(alertsService, 'addWarning'); + component.blogPostData = sampleBlogPostData; + component.blogPostData.title = ''; + component.saveInProgress = true; + + component.saveDraft(); + + expect(alertsService.addWarning).toHaveBeenCalledWith( + 'Please fix the errors.' + ); + expect(component.saveInProgress).toBe(false); + } + ); + + it('should update blog post data successfully in the backend', fakeAsync(() => { component.blogPostData = sampleBlogPostData; - component.blogPostData.title = ''; + component.blogPostId = sampleBlogPostData.id; component.saveInProgress = true; + component.publishingInProgress = true; + spyOn(blogPostUpdateService, 'getBlogPostChangeDict').and.returnValue({}); + spyOn(blogPostUpdateService, 'setBlogPostTags'); + spyOn( + blogPostEditorBackendApiService, + 'updateBlogPostDataAsync' + ).and.returnValue(Promise.resolve({blogPostDict: sampleBlogPostData})); + spyOn(alertsService, 'addSuccessMessage'); - component.saveDraft(); + component.updateBlogPostData(false); + tick(); - expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Please fix the errors.'); expect(component.saveInProgress).toBe(false); - }); - - it('should update blog post data successfully in the backend', - fakeAsync(() => { - component.blogPostData = sampleBlogPostData; - component.blogPostId = sampleBlogPostData.id; - component.saveInProgress = true; - component.publishingInProgress = true; - spyOn(blogPostUpdateService, 'getBlogPostChangeDict') - .and.returnValue({}); - spyOn(blogPostUpdateService, 'setBlogPostTags'); - spyOn(blogPostEditorBackendApiService, 'updateBlogPostDataAsync') - .and.returnValue(Promise.resolve({blogPostDict: sampleBlogPostData})); - spyOn(alertsService, 'addSuccessMessage'); - - component.updateBlogPostData(false); - tick(); - - expect(component.saveInProgress).toBe(false); - expect(blogPostUpdateService.setBlogPostTags).toHaveBeenCalled(); - expect(alertsService.addSuccessMessage).toHaveBeenCalledWith( - 'Blog Post Saved Successfully.'); - expect(preventPageUnloadEventService.removeListener).toHaveBeenCalled(); + expect(blogPostUpdateService.setBlogPostTags).toHaveBeenCalled(); + expect(alertsService.addSuccessMessage).toHaveBeenCalledWith( + 'Blog Post Saved Successfully.' + ); + expect(preventPageUnloadEventService.removeListener).toHaveBeenCalled(); - component.updateBlogPostData(true); - tick(); + component.updateBlogPostData(true); + tick(); - expect(component.publishingInProgress).toBe(false); - expect(alertsService.addSuccessMessage).toHaveBeenCalledWith( - 'Blog Post Saved and Published Successfully.'); - })); + expect(component.publishingInProgress).toBe(false); + expect(alertsService.addSuccessMessage).toHaveBeenCalledWith( + 'Blog Post Saved and Published Successfully.' + ); + })); - it('should display alert when unable to update blog post data', - fakeAsync(() => { - component.blogPostData = sampleBlogPostData; - component.blogPostId = sampleBlogPostData.id; - component.saveInProgress = true; - component.publishingInProgress = true; - spyOn(blogPostUpdateService, 'getBlogPostChangeDict') - .and.returnValue({}); - spyOn(blogPostEditorBackendApiService, 'updateBlogPostDataAsync') - .and.returnValue(Promise.reject('status: 500')); - spyOn(alertsService, 'addWarning'); + it('should display alert when unable to update blog post data', fakeAsync(() => { + component.blogPostData = sampleBlogPostData; + component.blogPostId = sampleBlogPostData.id; + component.saveInProgress = true; + component.publishingInProgress = true; + spyOn(blogPostUpdateService, 'getBlogPostChangeDict').and.returnValue({}); + spyOn( + blogPostEditorBackendApiService, + 'updateBlogPostDataAsync' + ).and.returnValue(Promise.reject('status: 500')); + spyOn(alertsService, 'addWarning'); - component.updateBlogPostData(false); - tick(); + component.updateBlogPostData(false); + tick(); - expect(blogPostEditorBackendApiService.updateBlogPostDataAsync) - .toHaveBeenCalled(); - expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Failed to save Blog Post. Internal Error: status: 500'); - expect(component.publishingInProgress).toBe(false); - expect(component.saveInProgress).toBe(false); - })); + expect( + blogPostEditorBackendApiService.updateBlogPostDataAsync + ).toHaveBeenCalled(); + expect(alertsService.addWarning).toHaveBeenCalledWith( + 'Failed to save Blog Post. Internal Error: status: 500' + ); + expect(component.publishingInProgress).toBe(false); + expect(component.saveInProgress).toBe(false); + })); - it('should get formatted date string from the timestamp in milliseconds', - () => { - // This corresponds to Fri, 21 Nov 2014 04:52 AM GMT. - let DATE = '11/21/2014, 04:52:46:713463'; - expect(component.getDateStringInWords(DATE)) - .toBe('November 21, 2014 at 04:52 AM'); + it('should get formatted date string from the timestamp in milliseconds', () => { + // This corresponds to Fri, 21 Nov 2014 04:52 AM GMT. + let DATE = '11/21/2014, 04:52:46:713463'; + expect(component.getDateStringInWords(DATE)).toBe( + 'November 21, 2014 at 04:52 AM' + ); - DATE = '01/16/2027, 09:45:46:600000'; - expect(component.getDateStringInWords(DATE)) - .toBe('January 16, 2027 at 09:45 AM'); + DATE = '01/16/2027, 09:45:46:600000'; + expect(component.getDateStringInWords(DATE)).toBe( + 'January 16, 2027 at 09:45 AM' + ); - DATE = '02/02/2018, 12:30:46:608990'; - expect(component.getDateStringInWords(DATE)) - .toBe('February 2, 2018 at 12:30 PM'); - }); + DATE = '02/02/2018, 12:30:46:608990'; + expect(component.getDateStringInWords(DATE)).toBe( + 'February 2, 2018 at 12:30 PM' + ); + }); it('should cancel delete blog post model', fakeAsync(() => { spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.reject() + result: Promise.reject(), } as NgbModalRef); spyOn(blogDashboardPageService, 'deleteBlogPost'); component.deleteBlogPost(); tick(); - expect(blogDashboardPageService.deleteBlogPost) - .not.toHaveBeenCalled(); + expect(blogDashboardPageService.deleteBlogPost).not.toHaveBeenCalled(); })); - it('should successfully place call to delete blog post model', fakeAsync( - () => { - spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.resolve() - } as NgbModalRef); - spyOn(blogDashboardPageService, 'deleteBlogPost'); - - component.deleteBlogPost(); - tick(); + it('should successfully place call to delete blog post model', fakeAsync(() => { + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.resolve(), + } as NgbModalRef); + spyOn(blogDashboardPageService, 'deleteBlogPost'); - expect(blogDashboardPageService.deleteBlogPost).toHaveBeenCalled(); - })); + component.deleteBlogPost(); + tick(); + expect(blogDashboardPageService.deleteBlogPost).toHaveBeenCalled(); + })); it('should open preview of the blog post model', () => { spyOn(ngbModal, 'open'); @@ -582,48 +631,45 @@ describe('Blog Post Editor Component', () => { component.showPreview(); - expect(blogDashboardPageService.blogPostData).toEqual( - sampleBlogPostData); + expect(blogDashboardPageService.blogPostData).toEqual(sampleBlogPostData); expect(ngbModal.open).toHaveBeenCalled(); }); - it('should cancel publishing blog post model', fakeAsync( - () => { - component.blogPostData = sampleBlogPostData; - component.maxAllowedTags = 3; - spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.reject() - } as NgbModalRef); - spyOn(component, 'updateBlogPostData'); + it('should cancel publishing blog post model', fakeAsync(() => { + component.blogPostData = sampleBlogPostData; + component.maxAllowedTags = 3; + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.reject(), + } as NgbModalRef); + spyOn(component, 'updateBlogPostData'); - component.publishBlogPost(); + component.publishBlogPost(); - expect(component.publishingInProgress).toBe(true); + expect(component.publishingInProgress).toBe(true); - tick(); + tick(); - expect(component.updateBlogPostData).not.toHaveBeenCalled(); - expect(component.publishingInProgress).toBe(false); - })); + expect(component.updateBlogPostData).not.toHaveBeenCalled(); + expect(component.publishingInProgress).toBe(false); + })); - it('should successfully place call to publish blog post model', fakeAsync( - () => { - component.blogPostData = sampleBlogPostData; - component.maxAllowedTags = 3; - spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.resolve() - } as NgbModalRef); - spyOn(component, 'updateBlogPostData'); + it('should successfully place call to publish blog post model', fakeAsync(() => { + component.blogPostData = sampleBlogPostData; + component.maxAllowedTags = 3; + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.resolve(), + } as NgbModalRef); + spyOn(component, 'updateBlogPostData'); - component.publishBlogPost(); - tick(); + component.publishBlogPost(); + tick(); - expect(component.updateBlogPostData).toHaveBeenCalledWith(true); - })); + expect(component.updateBlogPostData).toHaveBeenCalledWith(true); + })); it('should cancel blog post thumbnail upload', fakeAsync(() => { spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.reject() + result: Promise.reject(), } as NgbModalRef); spyOn(component, 'postImageDataToServer'); @@ -633,82 +679,94 @@ describe('Blog Post Editor Component', () => { expect(component.postImageDataToServer).not.toHaveBeenCalled(); })); - it('should successfully place call to post thumbnail to server', fakeAsync( - () => { - component.thumbnailDataUrl = 'sample.png'; - spyOn(imageLocalStorageService, 'flushStoredImagesData'); - spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.resolve('sample-url-string') - } as NgbModalRef); - spyOn(component, 'postImageDataToServer'); - - component.showuploadThumbnailModal(); - tick(); + it('should successfully place call to post thumbnail to server', fakeAsync(() => { + component.thumbnailDataUrl = 'sample.png'; + spyOn(imageLocalStorageService, 'flushStoredImagesData'); + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.resolve('sample-url-string'), + } as NgbModalRef); + spyOn(component, 'postImageDataToServer'); - expect(component.postImageDataToServer).toHaveBeenCalledWith(); - expect(component.thumbnailDataUrl).toEqual('sample-url-string'); - expect(imageLocalStorageService.flushStoredImagesData).toHaveBeenCalled(); - })); + component.showuploadThumbnailModal(); + tick(); + + expect(component.postImageDataToServer).toHaveBeenCalledWith(); + expect(component.thumbnailDataUrl).toEqual('sample-url-string'); + expect(imageLocalStorageService.flushStoredImagesData).toHaveBeenCalled(); + })); it('should change tags for blog post successfully', () => { component.blogPostData = sampleBlogPostData; component.maxAllowedTags = 4; component.onTagChange('sampleTag'); - expect(component.blogPostData.tags).toEqual( - ['learners', 'news', 'sampleTag']); + expect(component.blogPostData.tags).toEqual([ + 'learners', + 'news', + 'sampleTag', + ]); component.onTagChange('sampleTag'); - expect(component.blogPostData.tags).toEqual( - ['learners', 'news']); + expect(component.blogPostData.tags).toEqual(['learners', 'news']); expect(preventPageUnloadEventService.addListener).toHaveBeenCalled(); }); - it('should display alert when unable to post thumbnail data', - fakeAsync(() => { - component.blogPostData = sampleBlogPostData; - let imagesData = [{ + it('should display alert when unable to post thumbnail data', fakeAsync(() => { + component.blogPostData = sampleBlogPostData; + let imagesData = [ + { filename: 'imageFilename1', - imageBlob: new Blob([''], { type: 'image/jpeg' }) - }]; - spyOn(blogPostEditorBackendApiService, 'postThumbnailDataAsync') - .and.returnValue(Promise.reject('status: 500')); - spyOn(alertsService, 'addWarning'); - spyOn(imageLocalStorageService, 'getStoredImagesData') - .and.returnValue(imagesData); - - component.postImageDataToServer(); - tick(); + imageBlob: new Blob([''], {type: 'image/jpeg'}), + }, + ]; + spyOn( + blogPostEditorBackendApiService, + 'postThumbnailDataAsync' + ).and.returnValue(Promise.reject('status: 500')); + spyOn(alertsService, 'addWarning'); + spyOn(imageLocalStorageService, 'getStoredImagesData').and.returnValue( + imagesData + ); - expect(blogPostEditorBackendApiService.postThumbnailDataAsync) - .toHaveBeenCalled(); - expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Failed to save thumbnail data. Internal Error: status: 500'); - })); + component.postImageDataToServer(); + tick(); + expect( + blogPostEditorBackendApiService.postThumbnailDataAsync + ).toHaveBeenCalled(); + expect(alertsService.addWarning).toHaveBeenCalledWith( + 'Failed to save thumbnail data. Internal Error: status: 500' + ); + })); - it('should update thumbnail data successfully in the backend', - fakeAsync(() => { - let imagesData = [{ + it('should update thumbnail data successfully in the backend', fakeAsync(() => { + let imagesData = [ + { filename: 'imageFilename1', - imageBlob: new Blob([''], { type: 'image/jpeg' }) - }]; - spyOn(blogPostEditorBackendApiService, 'postThumbnailDataAsync') - .and.returnValue(Promise.resolve()); - spyOn(alertsService, 'addSuccessMessage'); - spyOn(imageLocalStorageService, 'getStoredImagesData') - .and.returnValue(imagesData); - component.blogPostData = sampleBlogPostData; + imageBlob: new Blob([''], {type: 'image/jpeg'}), + }, + ]; + spyOn( + blogPostEditorBackendApiService, + 'postThumbnailDataAsync' + ).and.returnValue(Promise.resolve()); + spyOn(alertsService, 'addSuccessMessage'); + spyOn(imageLocalStorageService, 'getStoredImagesData').and.returnValue( + imagesData + ); + component.blogPostData = sampleBlogPostData; - component.postImageDataToServer(); - tick(); + component.postImageDataToServer(); + tick(); - expect(blogPostEditorBackendApiService.postThumbnailDataAsync) - .toHaveBeenCalled(); - expect(blogDashboardPageService.imageUploaderIsNarrow).toBe(true); - expect(alertsService.addSuccessMessage).toHaveBeenCalledWith( - 'Thumbnail Saved Successfully.'); - })); + expect( + blogPostEditorBackendApiService.postThumbnailDataAsync + ).toHaveBeenCalled(); + expect(blogDashboardPageService.imageUploaderIsNarrow).toBe(true); + expect(alertsService.addSuccessMessage).toHaveBeenCalledWith( + 'Thumbnail Saved Successfully.' + ); + })); it('should activate title editor', () => { component.titleEditorIsActive = false; @@ -723,7 +781,11 @@ describe('Blog Post Editor Component', () => { it('should correctly return if the publish button is disabled or not', () => { component.blogPostData = sampleBlogPostData; spyOn(component.blogPostData, 'prepublishValidate').and.returnValues( - [], [], [], ['some issues']); + [], + [], + [], + ['some issues'] + ); component.newChangesAreMade = true; component.lastChangesWerePublished = true; diff --git a/core/templates/pages/blog-dashboard-page/blog-post-editor/blog-post-editor.component.ts b/core/templates/pages/blog-dashboard-page/blog-post-editor/blog-post-editor.component.ts index 647efeb61cdb..3a14a002bb7f 100644 --- a/core/templates/pages/blog-dashboard-page/blog-post-editor/blog-post-editor.component.ts +++ b/core/templates/pages/blog-dashboard-page/blog-post-editor/blog-post-editor.component.ts @@ -18,33 +18,42 @@ interface EditorSchema { type: string; - 'ui_config': object; + ui_config: object; } -import { AppConstants } from 'app.constants'; -import { ChangeDetectorRef, Component, ElementRef, OnInit, ViewChild } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { AlertsService } from 'services/alerts.service'; -import { BlogPostEditorData, BlogPostEditorBackendApiService } from 'domain/blog/blog-post-editor-backend-api.service'; -import { BlogPostUpdateService } from 'domain/blog/blog-post-update.service'; -import { BlogDashboardPageConstants } from 'pages/blog-dashboard-page/blog-dashboard-page.constants'; -import { BlogDashboardPageService } from 'pages/blog-dashboard-page/services/blog-dashboard-page.service'; -import { BlogPostData } from 'domain/blog/blog-post.model'; -import { LoaderService } from 'services/loader.service'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { BlogPostActionConfirmationModalComponent } from 'pages/blog-dashboard-page/blog-post-action-confirmation/blog-post-action-confirmation.component'; -import { UploadBlogPostThumbnailModalComponent } from 'pages/blog-dashboard-page/modal-templates/upload-blog-post-thumbnail-modal.component'; -import { ImageLocalStorageService } from 'services/image-local-storage.service'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; +import {AppConstants} from 'app.constants'; +import { + ChangeDetectorRef, + Component, + ElementRef, + OnInit, + ViewChild, +} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {AlertsService} from 'services/alerts.service'; +import { + BlogPostEditorData, + BlogPostEditorBackendApiService, +} from 'domain/blog/blog-post-editor-backend-api.service'; +import {BlogPostUpdateService} from 'domain/blog/blog-post-update.service'; +import {BlogDashboardPageConstants} from 'pages/blog-dashboard-page/blog-dashboard-page.constants'; +import {BlogDashboardPageService} from 'pages/blog-dashboard-page/services/blog-dashboard-page.service'; +import {BlogPostData} from 'domain/blog/blog-post.model'; +import {LoaderService} from 'services/loader.service'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {BlogPostActionConfirmationModalComponent} from 'pages/blog-dashboard-page/blog-post-action-confirmation/blog-post-action-confirmation.component'; +import {UploadBlogPostThumbnailModalComponent} from 'pages/blog-dashboard-page/modal-templates/upload-blog-post-thumbnail-modal.component'; +import {ImageLocalStorageService} from 'services/image-local-storage.service'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; import dayjs from 'dayjs'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { BlogCardPreviewModalComponent } from 'pages/blog-dashboard-page/modal-templates/blog-card-preview-modal.component'; -import { PreventPageUnloadEventService } from 'services/prevent-page-unload-event.service'; -import { UserService } from 'services/user.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {BlogCardPreviewModalComponent} from 'pages/blog-dashboard-page/modal-templates/blog-card-preview-modal.component'; +import {PreventPageUnloadEventService} from 'services/prevent-page-unload-event.service'; +import {UserService} from 'services/user.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; @Component({ selector: 'oppia-blog-post-editor', - templateUrl: './blog-post-editor.component.html' + templateUrl: './blog-post-editor.component.html', }) export class BlogPostEditorComponent implements OnInit { @ViewChild('titleInput') titleInput!: ElementRef; @@ -78,8 +87,8 @@ export class BlogPostEditorComponent implements OnInit { type: 'html', ui_config: { hide_complex_extensions: false, - startupFocusEnabled: false - } + startupFocusEnabled: false, + }, }; BLOG_POST_TITLE_PATTERN: string = AppConstants.VALID_BLOG_POST_TITLE_REGEX; @@ -104,15 +113,17 @@ export class BlogPostEditorComponent implements OnInit { const userInfo = await this.userService.getUserInfoAsync(); this.username = userInfo.getUsername(); if (this.username !== null) { - [this.authorProfilePicPngUrl, this.authorProfilePicWebpUrl] = ( - this.userService.getProfileImageDataUrl(this.username)); + [this.authorProfilePicPngUrl, this.authorProfilePicWebpUrl] = + this.userService.getProfileImageDataUrl(this.username); } else { - this.authorProfilePicWebpUrl = ( + this.authorProfilePicWebpUrl = this.urlInterpolationService.getStaticImageUrl( - AppConstants.DEFAULT_PROFILE_IMAGE_WEBP_PATH)); - this.authorProfilePicPngUrl = ( + AppConstants.DEFAULT_PROFILE_IMAGE_WEBP_PATH + ); + this.authorProfilePicPngUrl = this.urlInterpolationService.getStaticImageUrl( - AppConstants.DEFAULT_PROFILE_IMAGE_PNG_PATH)); + AppConstants.DEFAULT_PROFILE_IMAGE_PNG_PATH + ); } } @@ -121,10 +132,10 @@ export class BlogPostEditorComponent implements OnInit { this.getUserInfoAsync(); this.blogPostId = this.blogDashboardPageService.blogPostId; this.initEditor(); - this.MAX_CHARS_IN_BLOG_POST_TITLE = ( - AppConstants.MAX_CHARS_IN_BLOG_POST_TITLE); - this.MIN_CHARS_IN_BLOG_POST_TITLE = ( - AppConstants.MIN_CHARS_IN_BLOG_POST_TITLE); + this.MAX_CHARS_IN_BLOG_POST_TITLE = + AppConstants.MAX_CHARS_IN_BLOG_POST_TITLE; + this.MIN_CHARS_IN_BLOG_POST_TITLE = + AppConstants.MIN_CHARS_IN_BLOG_POST_TITLE; this.windowIsNarrow = this.windowDimensionService.isWindowNarrow(); this.windowDimensionService.getResizeEvent().subscribe(() => { this.windowIsNarrow = this.windowDimensionService.isWindowNarrow(); @@ -139,7 +150,8 @@ export class BlogPostEditorComponent implements OnInit { } initEditor(): void { - this.blogPostEditorBackendService.fetchBlogPostEditorData(this.blogPostId) + this.blogPostEditorBackendService + .fetchBlogPostEditorData(this.blogPostId) .then( (editorData: BlogPostEditorData) => { this.blogPostData = editorData.blogPostDict; @@ -155,76 +167,87 @@ export class BlogPostEditorComponent implements OnInit { this.dateTimeLastSaved = this.getDateStringInWords(lastUpdated); } this.contentEditorIsActive = Boolean( - this.blogPostData.content.length === 0); + this.blogPostData.content.length === 0 + ); if (this.blogPostData.thumbnailFilename) { - this.thumbnailDataUrl = this.assetsBackendApiService - .getThumbnailUrlForPreview( - AppConstants.ENTITY_TYPE.BLOG_POST, this.blogPostId, - this.blogPostData.thumbnailFilename); + this.thumbnailDataUrl = + this.assetsBackendApiService.getThumbnailUrlForPreview( + AppConstants.ENTITY_TYPE.BLOG_POST, + this.blogPostId, + this.blogPostData.thumbnailFilename + ); if (this.windowIsNarrow) { this.blogDashboardPageService.imageUploaderIsNarrow = true; } } if (this.blogPostData.publishedOn && this.blogPostData.lastUpdated) { - if (this.blogPostData.lastUpdated.slice(0, -8) === ( - this.blogPostData.publishedOn.slice(0, -8))) { + if ( + this.blogPostData.lastUpdated.slice(0, -8) === + this.blogPostData.publishedOn.slice(0, -8) + ) { this.lastChangesWerePublished = true; } } this.blogDashboardPageService.setNavTitle( - this.lastChangesWerePublished, this.title); + this.lastChangesWerePublished, + this.title + ); this.newChangesAreMade = false; this.preventPageUnloadEventService.removeListener(); this.loaderService.hideLoadingScreen(); - }, (errorResponse) => { - if ( - AppConstants.FATAL_ERROR_CODES.indexOf( - errorResponse) !== -1) { + }, + errorResponse => { + if (AppConstants.FATAL_ERROR_CODES.indexOf(errorResponse) !== -1) { this.alertsService.addWarning( 'Failed to get Blog Post Data. The Blog Post was either' + - ' deleted or the Blog Post ID is invalid.'); + ' deleted or the Blog Post ID is invalid.' + ); this.blogDashboardPageService.navigateToMainTab(); } - }); + } + ); } getDateStringInWords(naiveDateTime: string): string { let datestring = naiveDateTime.substring(0, naiveDateTime.length - 7); - return dayjs( - datestring, 'MM-DD-YYYY, HH:mm:ss').format('MMMM D, YYYY [at] hh:mm A'); + return dayjs(datestring, 'MM-DD-YYYY, HH:mm:ss').format( + 'MMMM D, YYYY [at] hh:mm A' + ); } updateLocalTitleValue(): void { - this.blogPostUpdateService.setBlogPostTitle( - this.blogPostData, this.title - ); + this.blogPostUpdateService.setBlogPostTitle(this.blogPostData, this.title); this.titleEditorIsActive = false; if ( this.isTitlePatternValid() && this.title.length <= this.MAX_CHARS_IN_BLOG_POST_TITLE && this.title.length >= this.MIN_CHARS_IN_BLOG_POST_TITLE ) { - this.blogPostEditorBackendService.doesPostWithGivenTitleAlreadyExistAsync( - this.blogPostId, this.title - ).then((response: boolean) => { - if (!response) { - this.blogPostData.titleIsDuplicate = false; - this.newChangesAreMade = true; - this.blogDashboardPageService.setNavTitle( - this.lastChangesWerePublished, this.title - ); - } else { - this.blogPostData.titleIsDuplicate = true; - this.alertsService.addWarning( - 'Blog Post with the given title exists already. Please use a ' + - 'different title.' - ); - } - }, error => { - this.alertsService.addWarning( - `Failed to check if title is unique. Internal Error: ${error}` + this.blogPostEditorBackendService + .doesPostWithGivenTitleAlreadyExistAsync(this.blogPostId, this.title) + .then( + (response: boolean) => { + if (!response) { + this.blogPostData.titleIsDuplicate = false; + this.newChangesAreMade = true; + this.blogDashboardPageService.setNavTitle( + this.lastChangesWerePublished, + this.title + ); + } else { + this.blogPostData.titleIsDuplicate = true; + this.alertsService.addWarning( + 'Blog Post with the given title exists already. Please use a ' + + 'different title.' + ); + } + }, + error => { + this.alertsService.addWarning( + `Failed to check if title is unique. Internal Error: ${error}` + ); + } ); - }); this.preventPageUnloadEventService.addListener(); } } @@ -254,7 +277,9 @@ export class BlogPostEditorComponent implements OnInit { updateContentValue(): void { this.blogPostUpdateService.setBlogPostContent( - this.blogPostData, this.localEditedContent); + this.blogPostData, + this.localEditedContent + ); if (this.blogPostData.content.length > 0) { this.contentEditorIsActive = false; } @@ -268,9 +293,7 @@ export class BlogPostEditorComponent implements OnInit { if (issues.length === 0) { this.updateBlogPostData(false); } else { - this.alertsService.addWarning( - 'Please fix the errors.' - ); + this.alertsService.addWarning('Please fix the errors.'); this.saveInProgress = false; } } @@ -279,92 +302,114 @@ export class BlogPostEditorComponent implements OnInit { this.publishingInProgress = true; let issues = this.blogPostData.prepublishValidate(this.maxAllowedTags); if (issues.length === 0) { - this.blogDashboardPageService.blogPostAction = ( - BlogDashboardPageConstants.BLOG_POST_ACTIONS.PUBLISH); - this.ngbModal.open(BlogPostActionConfirmationModalComponent, { - backdrop: 'static', - keyboard: false, - }).result.then(() => { - this.updateBlogPostData(true); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - this.publishingInProgress = false; - }); + this.blogDashboardPageService.blogPostAction = + BlogDashboardPageConstants.BLOG_POST_ACTIONS.PUBLISH; + this.ngbModal + .open(BlogPostActionConfirmationModalComponent, { + backdrop: 'static', + keyboard: false, + }) + .result.then( + () => { + this.updateBlogPostData(true); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + this.publishingInProgress = false; + } + ); } } updateBlogPostData(isBlogPostPublished: boolean): void { this.blogPostUpdateService.setBlogPostTags( - this.blogPostData, this.blogPostData.tags); + this.blogPostData, + this.blogPostData.tags + ); let changeDict = this.blogPostUpdateService.getBlogPostChangeDict(); - this.blogPostEditorBackendService.updateBlogPostDataAsync( - this.blogPostId, isBlogPostPublished, changeDict).then( - () => { - if (isBlogPostPublished) { - this.alertsService.addSuccessMessage( - 'Blog Post Saved and Published Successfully.' + this.blogPostEditorBackendService + .updateBlogPostDataAsync(this.blogPostId, isBlogPostPublished, changeDict) + .then( + () => { + if (isBlogPostPublished) { + this.alertsService.addSuccessMessage( + 'Blog Post Saved and Published Successfully.' + ); + this.lastChangesWerePublished = true; + this.publishingInProgress = false; + } else { + this.alertsService.addSuccessMessage( + 'Blog Post Saved Successfully.' + ); + this.lastChangesWerePublished = false; + this.saveInProgress = false; + } + this.newChangesAreMade = false; + this.blogDashboardPageService.setNavTitle( + this.lastChangesWerePublished, + this.title + ); + this.preventPageUnloadEventService.removeListener(); + }, + errorResponse => { + this.alertsService.addWarning( + `Failed to save Blog Post. Internal Error: ${errorResponse}` ); - this.lastChangesWerePublished = true; - this.publishingInProgress = false; - } else { - this.alertsService.addSuccessMessage( - 'Blog Post Saved Successfully.'); - this.lastChangesWerePublished = false; this.saveInProgress = false; + this.publishingInProgress = false; } - this.newChangesAreMade = false; - this.blogDashboardPageService.setNavTitle( - this.lastChangesWerePublished, this.title); - this.preventPageUnloadEventService.removeListener(); - }, (errorResponse) => { - this.alertsService.addWarning( - `Failed to save Blog Post. Internal Error: ${errorResponse}`); - this.saveInProgress = false; - this.publishingInProgress = false; - } - ); + ); } postImageDataToServer(): void { let imagesData = this.imageLocalStorageService.getStoredImagesData(); this.blogPostUpdateService.setBlogPostThumbnail( - this.blogPostData, imagesData); - this.blogPostEditorBackendService.postThumbnailDataAsync( - this.blogPostId, imagesData).then( - () => { - if (this.windowIsNarrow) { - this.blogDashboardPageService.imageUploaderIsNarrow = true; + this.blogPostData, + imagesData + ); + this.blogPostEditorBackendService + .postThumbnailDataAsync(this.blogPostId, imagesData) + .then( + () => { + if (this.windowIsNarrow) { + this.blogDashboardPageService.imageUploaderIsNarrow = true; + } + this.alertsService.addSuccessMessage('Thumbnail Saved Successfully.'); + }, + errorResponse => { + this.alertsService.addWarning( + `Failed to save thumbnail data. Internal Error: ${errorResponse}` + ); + this.imageLocalStorageService.flushStoredImagesData(); + this.thumbnailDataUrl = ''; } - this.alertsService.addSuccessMessage( - 'Thumbnail Saved Successfully.'); - }, (errorResponse) => { - this.alertsService.addWarning( - `Failed to save thumbnail data. Internal Error: ${errorResponse}` - ); - this.imageLocalStorageService.flushStoredImagesData(); - this.thumbnailDataUrl = ''; - }); + ); } deleteBlogPost(): void { this.blogDashboardPageService.blogPostAction = 'delete'; - this.ngbModal.open(BlogPostActionConfirmationModalComponent, { - backdrop: 'static', - keyboard: false, - }).result.then(() => { - this.blogDashboardPageService.deleteBlogPost(); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + this.ngbModal + .open(BlogPostActionConfirmationModalComponent, { + backdrop: 'static', + keyboard: false, + }) + .result.then( + () => { + this.blogDashboardPageService.deleteBlogPost(); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } onTagChange(tag: string): void { - if ((this.blogPostData.tags).includes(tag)) { + if (this.blogPostData.tags.includes(tag)) { this.blogPostData.removeTag(tag); - } else if ((this.blogPostData.tags).length < this.maxAllowedTags) { + } else if (this.blogPostData.tags.length < this.maxAllowedTags) { this.blogPostData.addTag(tag); } this.newChangesAreMade = true; @@ -373,18 +418,21 @@ export class BlogPostEditorComponent implements OnInit { showuploadThumbnailModal(): void { let modalRef = this.ngbModal.open(UploadBlogPostThumbnailModalComponent, { - backdrop: 'static' + backdrop: 'static', }); if (this.thumbnailDataUrl) { this.imageLocalStorageService.flushStoredImagesData(); } - modalRef.result.then((imageDataUrl) => { - this.saveBlogPostThumbnail(imageDataUrl); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + modalRef.result.then( + imageDataUrl => { + this.saveBlogPostThumbnail(imageDataUrl); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } saveBlogPostThumbnail(thumbnailDataUrl: string): void { @@ -397,7 +445,7 @@ export class BlogPostEditorComponent implements OnInit { showPreview(): void { this.blogDashboardPageService.blogPostData = this.blogPostData; this.ngbModal.open(BlogCardPreviewModalComponent, { - backdrop: 'static' + backdrop: 'static', }); } @@ -418,7 +466,9 @@ export class BlogPostEditorComponent implements OnInit { } } -angular.module('oppia').directive('oppiaBlogPostEditor', - downgradeComponent({ - component: BlogPostEditorComponent - }) as angular.IDirectiveFactory); +angular.module('oppia').directive( + 'oppiaBlogPostEditor', + downgradeComponent({ + component: BlogPostEditorComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/blog-dashboard-page/modal-templates/author-detail-editor-modal.component.spec.ts b/core/templates/pages/blog-dashboard-page/modal-templates/author-detail-editor-modal.component.spec.ts index 53536f56b4dd..710a9c6662d4 100644 --- a/core/templates/pages/blog-dashboard-page/modal-templates/author-detail-editor-modal.component.spec.ts +++ b/core/templates/pages/blog-dashboard-page/modal-templates/author-detail-editor-modal.component.spec.ts @@ -16,13 +16,12 @@ * @fileoverview Unit tests for blog author details component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { BlogAuthorDetailsEditorComponent } from './author-detail-editor-modal.component'; - +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal, NgbModalModule} from '@ng-bootstrap/ng-bootstrap'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {BlogAuthorDetailsEditorComponent} from './author-detail-editor-modal.component'; describe('Upload Blog Post Thumbnail Modal Component', () => { let fixture: ComponentFixture; @@ -33,18 +32,10 @@ describe('Upload Blog Post Thumbnail Modal Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - NgbModalModule, - ], - declarations: [ - MockTranslatePipe, - BlogAuthorDetailsEditorComponent - ], - providers: [ - NgbActiveModal, - ], - schemas: [NO_ERRORS_SCHEMA] + imports: [HttpClientTestingModule, NgbModalModule], + declarations: [MockTranslatePipe, BlogAuthorDetailsEditorComponent], + providers: [NgbActiveModal], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -63,36 +54,45 @@ describe('Upload Blog Post Thumbnail Modal Component', () => { expect(dismissSpy).toHaveBeenCalled(); }); - it('should not dismiss the modal on calling cancel function if author bio' + - 'is empty', () => { - component.prevAuthorBio = ''; - component.cancel(); - - expect(dismissSpy).not.toHaveBeenCalled(); - }); - - it('should close the modal on calling save function with valid author' + - 'details', () => { - component.authorName = 'test username'; - component.authorBio = 'general bio'; - let expectedAuthorDetails = { - authorName: 'test username', - authorBio: 'general bio' - }; - component.save(); - - expect(confirmSpy).toHaveBeenCalledWith(expectedAuthorDetails); - }); - - it('should not close the modal on calling save function with invalid author' + - 'details', () => { - component.authorName = ''; - component.authorBio = ''; - - component.save(); - - expect(confirmSpy).not.toHaveBeenCalledWith(); - }); + it( + 'should not dismiss the modal on calling cancel function if author bio' + + 'is empty', + () => { + component.prevAuthorBio = ''; + component.cancel(); + + expect(dismissSpy).not.toHaveBeenCalled(); + } + ); + + it( + 'should close the modal on calling save function with valid author' + + 'details', + () => { + component.authorName = 'test username'; + component.authorBio = 'general bio'; + let expectedAuthorDetails = { + authorName: 'test username', + authorBio: 'general bio', + }; + component.save(); + + expect(confirmSpy).toHaveBeenCalledWith(expectedAuthorDetails); + } + ); + + it( + 'should not close the modal on calling save function with invalid author' + + 'details', + () => { + component.authorName = ''; + component.authorBio = ''; + + component.save(); + + expect(confirmSpy).not.toHaveBeenCalledWith(); + } + ); it('should validate author details', () => { component.authorName = 'test username'; @@ -103,14 +103,14 @@ describe('Upload Blog Post Thumbnail Modal Component', () => { component.authorBio = 'general bio'; expect(component.validateAuthorDetails().length).toBe(1); expect(component.validateAuthorDetails()).toEqual([ - 'Author Name should not be empty.' + 'Author Name should not be empty.', ]); component.authorName = 'test_username'; component.authorBio = 'general bio'; expect(component.validateAuthorDetails().length).toBe(1); expect(component.validateAuthorDetails()).toEqual([ - 'Author Name can only have alphanumeric characters and spaces.' + 'Author Name can only have alphanumeric characters and spaces.', ]); component.authorName = ''; @@ -118,7 +118,7 @@ describe('Upload Blog Post Thumbnail Modal Component', () => { expect(component.validateAuthorDetails().length).toBe(2); expect(component.validateAuthorDetails()).toEqual([ 'Author Name should not be empty.', - 'Author Bio should not be empty.' + 'Author Bio should not be empty.', ]); component.authorName = 'A'; @@ -135,13 +135,14 @@ describe('Upload Blog Post Thumbnail Modal Component', () => { 'Author Bio should not be less than 5 characters.', ]); - component.authorName = 'Author name exceeding character limit of 35' + - ' characters should raise error.'; + component.authorName = + 'Author name exceeding character limit of 35' + + ' characters should raise error.'; component.authorBio = 'Author bio exceeding char limit of 35'.repeat(550); expect(component.validateAuthorDetails().length).toBe(2); expect(component.validateAuthorDetails()).toEqual([ 'Author Name should not be more than 35 characters.', - 'Author Bio should not be more than 250 characters.' + 'Author Bio should not be more than 250 characters.', ]); }); }); diff --git a/core/templates/pages/blog-dashboard-page/modal-templates/author-detail-editor-modal.component.ts b/core/templates/pages/blog-dashboard-page/modal-templates/author-detail-editor-modal.component.ts index e1ca6a64feb3..1936d775ad16 100644 --- a/core/templates/pages/blog-dashboard-page/modal-templates/author-detail-editor-modal.component.ts +++ b/core/templates/pages/blog-dashboard-page/modal-templates/author-detail-editor-modal.component.ts @@ -16,10 +16,10 @@ * @fileoverview Component for editing blog author details. */ -import { Component } from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component} from '@angular/core'; +import {AppConstants} from 'app.constants'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-blog-author-details-editor', @@ -34,9 +34,7 @@ export class BlogAuthorDetailsEditorComponent extends ConfirmOrCancelModal { authorName!: string; authorBio!: string; prevAuthorBio!: string; - constructor( - ngbActiveModal: NgbActiveModal, - ) { + constructor(ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } @@ -48,43 +46,38 @@ export class BlogAuthorDetailsEditorComponent extends ConfirmOrCancelModal { validateAuthorDetails(): string[] { let issues: string[] = []; - const validAuthorNameRegex = ( - new RegExp(AppConstants.VALID_AUTHOR_NAME_REGEX) + const validAuthorNameRegex = new RegExp( + AppConstants.VALID_AUTHOR_NAME_REGEX ); if (this.authorName.trim().length === 0) { - issues.push( - 'Author Name should not be empty.'); - } else if ( - this.authorName.length < AppConstants.MIN_AUTHOR_NAME_LENGTH) { + issues.push('Author Name should not be empty.'); + } else if (this.authorName.length < AppConstants.MIN_AUTHOR_NAME_LENGTH) { issues.push( 'Author Name should not be less than ' + - `${AppConstants.MIN_AUTHOR_NAME_LENGTH} characters.` + `${AppConstants.MIN_AUTHOR_NAME_LENGTH} characters.` ); - } else if ( - this.authorName.length > AppConstants.MAX_AUTHOR_NAME_LENGTH) { + } else if (this.authorName.length > AppConstants.MAX_AUTHOR_NAME_LENGTH) { issues.push( 'Author Name should not be more than ' + - `${AppConstants.MAX_AUTHOR_NAME_LENGTH} characters.` + `${AppConstants.MAX_AUTHOR_NAME_LENGTH} characters.` ); - } else if ( - !validAuthorNameRegex.test(this.authorName) - ) { + } else if (!validAuthorNameRegex.test(this.authorName)) { issues.push( - 'Author Name can only have alphanumeric characters and spaces.'); + 'Author Name can only have alphanumeric characters and spaces.' + ); } if (this.authorBio.trim().length === 0) { - issues.push( - 'Author Bio should not be empty.'); + issues.push('Author Bio should not be empty.'); } else if (this.authorBio.length < AppConstants.MIN_CHARS_IN_AUTHOR_BIO) { issues.push( 'Author Bio should not be less than ' + - `${AppConstants.MIN_CHARS_IN_AUTHOR_BIO} characters.` + `${AppConstants.MIN_CHARS_IN_AUTHOR_BIO} characters.` ); } else if (this.authorBio.length > AppConstants.MAX_CHARS_IN_AUTHOR_BIO) { issues.push( 'Author Bio should not be more than ' + - `${AppConstants.MAX_CHARS_IN_AUTHOR_BIO} characters.` + `${AppConstants.MAX_CHARS_IN_AUTHOR_BIO} characters.` ); } return issues; @@ -93,7 +86,7 @@ export class BlogAuthorDetailsEditorComponent extends ConfirmOrCancelModal { save(): void { let authorDetails = { authorName: this.authorName, - authorBio: this.authorBio + authorBio: this.authorBio, }; if (this.validateAuthorDetails().length === 0) { super.confirm(authorDetails); diff --git a/core/templates/pages/blog-dashboard-page/modal-templates/blog-card-preview-modal.component.spec.ts b/core/templates/pages/blog-dashboard-page/modal-templates/blog-card-preview-modal.component.spec.ts index 02792589b2a5..1826a7d31026 100644 --- a/core/templates/pages/blog-dashboard-page/modal-templates/blog-card-preview-modal.component.spec.ts +++ b/core/templates/pages/blog-dashboard-page/modal-templates/blog-card-preview-modal.component.spec.ts @@ -16,23 +16,23 @@ * @fileoverview Unit tests for blog card preview modal component. */ -import { NgbActiveModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { BlogCardPreviewModalComponent } from './blog-card-preview-modal.component'; -import { BlogDashboardPageService } from 'pages/blog-dashboard-page/services/blog-dashboard-page.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TruncatePipe } from 'filters/string-utility-filters/truncate.pipe'; +import {NgbActiveModal, NgbModalModule} from '@ng-bootstrap/ng-bootstrap'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {BlogCardPreviewModalComponent} from './blog-card-preview-modal.component'; +import {BlogDashboardPageService} from 'pages/blog-dashboard-page/services/blog-dashboard-page.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TruncatePipe} from 'filters/string-utility-filters/truncate.pipe'; // This throws "TS2307". We need to // suppress this error because rte-text-components are not strictly typed yet. // @ts-ignore -import { RichTextComponentsModule } from 'rich_text_components/rich-text-components.module'; -import { Pipe } from '@angular/core'; -import { BlogPostBackendDict, BlogPostData } from 'domain/blog/blog-post.model'; -import { BlogPostSummary } from 'domain/blog/blog-post-summary.model'; -import { MatCardModule } from '@angular/material/card'; -import { MatIconModule } from '@angular/material/icon'; -import { BlogCardComponent } from '../blog-card/blog-card.component'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; +import {RichTextComponentsModule} from 'rich_text_components/rich-text-components.module'; +import {Pipe} from '@angular/core'; +import {BlogPostBackendDict, BlogPostData} from 'domain/blog/blog-post.model'; +import {BlogPostSummary} from 'domain/blog/blog-post-summary.model'; +import {MatCardModule} from '@angular/material/card'; +import {MatIconModule} from '@angular/material/icon'; +import {BlogCardComponent} from '../blog-card/blog-card.component'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; class MockActiveModal { dismiss(): void { @@ -51,7 +51,6 @@ class MockTruncatePipe { } } - describe('Blog Card Preview Modal Component', () => { let component: BlogCardPreviewModalComponent; let blogDashboardPageService: BlogDashboardPageService; @@ -81,21 +80,21 @@ describe('Blog Card Preview Modal Component', () => { declarations: [ BlogCardPreviewModalComponent, BlogCardComponent, - MockTranslatePipe], + MockTranslatePipe, + ], providers: [ BlogDashboardPageService, { provide: NgbActiveModal, - useClass: MockActiveModal + useClass: MockActiveModal, }, { provide: TruncatePipe, useClass: MockTruncatePipe, - } - ] + }, + ], }).compileComponents(); - fixture = TestBed.createComponent( - BlogCardPreviewModalComponent); + fixture = TestBed.createComponent(BlogCardPreviewModalComponent); component = fixture.componentInstance; blogDashboardPageService = TestBed.inject(BlogDashboardPageService); })); @@ -103,8 +102,9 @@ describe('Blog Card Preview Modal Component', () => { it('should initialize correctly when blog post is not published', () => { sampleBlogPostBackendDict.published_on = ''; blogPostData = BlogPostData.createFromBackendDict( - sampleBlogPostBackendDict); - let expectedBlogPostSummary = new BlogPostSummary ( + sampleBlogPostBackendDict + ); + let expectedBlogPostSummary = new BlogPostSummary( blogPostData.id, '', blogPostData.displayedAuthorName, @@ -114,20 +114,21 @@ describe('Blog Card Preview Modal Component', () => { blogPostData.thumbnailFilename, blogPostData.urlFragment, blogPostData.lastUpdated, - blogPostData.lastUpdated); + blogPostData.lastUpdated + ); blogDashboardPageService.blogPostData = blogPostData; component.ngOnInit(); - expect(component.blogPostSummary).toEqual( - expectedBlogPostSummary); + expect(component.blogPostSummary).toEqual(expectedBlogPostSummary); }); it('should initialize correctly when blog post is published', () => { sampleBlogPostBackendDict.published_on = '11/21/2014, 09:45:00'; blogPostData = BlogPostData.createFromBackendDict( - sampleBlogPostBackendDict); - let expectedBlogPostSummary = new BlogPostSummary ( + sampleBlogPostBackendDict + ); + let expectedBlogPostSummary = new BlogPostSummary( blogPostData.id, '', blogPostData.displayedAuthorName, @@ -137,12 +138,12 @@ describe('Blog Card Preview Modal Component', () => { blogPostData.thumbnailFilename, blogPostData.urlFragment, blogPostData.lastUpdated, - blogPostData.publishedOn); + blogPostData.publishedOn + ); blogDashboardPageService.blogPostData = blogPostData; component.ngOnInit(); - expect(component.blogPostSummary).toEqual( - expectedBlogPostSummary); + expect(component.blogPostSummary).toEqual(expectedBlogPostSummary); }); }); diff --git a/core/templates/pages/blog-dashboard-page/modal-templates/blog-card-preview-modal.component.ts b/core/templates/pages/blog-dashboard-page/modal-templates/blog-card-preview-modal.component.ts index 8caf3b532601..c97aceb3a6f8 100644 --- a/core/templates/pages/blog-dashboard-page/modal-templates/blog-card-preview-modal.component.ts +++ b/core/templates/pages/blog-dashboard-page/modal-templates/blog-card-preview-modal.component.ts @@ -16,20 +16,22 @@ * @fileoverview Component for confirming blog post actions. */ -import { Component, OnInit } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { BlogDashboardPageService } from '../services/blog-dashboard-page.service'; -import { BlogPostData } from 'domain/blog/blog-post.model'; -import { BlogPostSummary } from 'domain/blog/blog-post-summary.model'; -import { TruncatePipe } from 'filters/string-utility-filters/truncate.pipe'; +import {Component, OnInit} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {BlogDashboardPageService} from '../services/blog-dashboard-page.service'; +import {BlogPostData} from 'domain/blog/blog-post.model'; +import {BlogPostSummary} from 'domain/blog/blog-post-summary.model'; +import {TruncatePipe} from 'filters/string-utility-filters/truncate.pipe'; @Component({ selector: 'oppia-blog-card-preview-modal', templateUrl: './blog-card-preview-modal.component.html', - styleUrls: [] + styleUrls: [], }) export class BlogCardPreviewModalComponent - extends ConfirmOrCancelModal implements OnInit { + extends ConfirmOrCancelModal + implements OnInit +{ // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -38,19 +40,19 @@ export class BlogCardPreviewModalComponent summaryContent!: string; blogHomePageLink: string = ''; constructor( - ngbActiveModal: NgbActiveModal, - private blogDashboardPageService: BlogDashboardPageService, - private truncatePipe: TruncatePipe, + ngbActiveModal: NgbActiveModal, + private blogDashboardPageService: BlogDashboardPageService, + private truncatePipe: TruncatePipe ) { super(ngbActiveModal); } ngOnInit(): void { this.blogPostData = this.blogDashboardPageService.blogPostData; - let rawContent = this.blogPostData.content.replace( - /(.*?)<\/strong>/g, ' ').replace(/

(.*?)<\/h1>/g, ' '); - this.summaryContent = this.truncatePipe.transform( - rawContent, 300); + let rawContent = this.blogPostData.content + .replace(/(.*?)<\/strong>/g, ' ') + .replace(/

(.*?)<\/h1>/g, ' '); + this.summaryContent = this.truncatePipe.transform(rawContent, 300); let dateString; if (this.blogPostData.publishedOn) { dateString = this.blogPostData.publishedOn; @@ -58,7 +60,7 @@ export class BlogCardPreviewModalComponent dateString = this.blogPostData.lastUpdated; } - this.blogPostSummary = new BlogPostSummary ( + this.blogPostSummary = new BlogPostSummary( this.blogPostData.id, '', this.blogPostData.displayedAuthorName, @@ -68,12 +70,12 @@ export class BlogCardPreviewModalComponent this.blogPostData.thumbnailFilename, this.blogPostData.urlFragment, this.blogPostData.lastUpdated, - dateString, + dateString ); - this.blogHomePageLink = ( - '' + + this.blogHomePageLink = + '' + '( link ' + - ' )'); + 'class="fas fa-external-link-alt oppia-open-new-tab-icon">' + + ' )'; } } diff --git a/core/templates/pages/blog-dashboard-page/modal-templates/upload-blog-post-thumbnail-modal.component.spec.ts b/core/templates/pages/blog-dashboard-page/modal-templates/upload-blog-post-thumbnail-modal.component.spec.ts index a61e6961d3bf..64d506d9d52a 100644 --- a/core/templates/pages/blog-dashboard-page/modal-templates/upload-blog-post-thumbnail-modal.component.spec.ts +++ b/core/templates/pages/blog-dashboard-page/modal-templates/upload-blog-post-thumbnail-modal.component.spec.ts @@ -16,13 +16,13 @@ * @fileoverview Unit tests for upload blog post tumbnail modal. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { UploadBlogPostThumbnailModalComponent } from './upload-blog-post-thumbnail-modal.component'; -import { UploadBlogPostThumbnailComponent } from './upload-blog-post-thumbnail.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal, NgbModalModule} from '@ng-bootstrap/ng-bootstrap'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {UploadBlogPostThumbnailModalComponent} from './upload-blog-post-thumbnail-modal.component'; +import {UploadBlogPostThumbnailComponent} from './upload-blog-post-thumbnail.component'; describe('Upload Blog Post Thumbnail Modal Component', () => { let fixture: ComponentFixture; @@ -33,19 +33,14 @@ describe('Upload Blog Post Thumbnail Modal Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - NgbModalModule, - ], + imports: [HttpClientTestingModule, NgbModalModule], declarations: [ MockTranslatePipe, UploadBlogPostThumbnailComponent, - UploadBlogPostThumbnailModalComponent - ], - providers: [ - NgbActiveModal, + UploadBlogPostThumbnailModalComponent, ], - schemas: [NO_ERRORS_SCHEMA] + providers: [NgbActiveModal], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); diff --git a/core/templates/pages/blog-dashboard-page/modal-templates/upload-blog-post-thumbnail-modal.component.ts b/core/templates/pages/blog-dashboard-page/modal-templates/upload-blog-post-thumbnail-modal.component.ts index 1df0a33ff2be..275c4d443bfd 100644 --- a/core/templates/pages/blog-dashboard-page/modal-templates/upload-blog-post-thumbnail-modal.component.ts +++ b/core/templates/pages/blog-dashboard-page/modal-templates/upload-blog-post-thumbnail-modal.component.ts @@ -16,22 +16,19 @@ * @fileoverview Component for uploading thumbnail image modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { AppConstants } from 'app.constants'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {AppConstants} from 'app.constants'; @Component({ selector: 'oppia-blog-post-thumbnail-upload-modal', - templateUrl: './upload-blog-post-thumbnail-modal.component.html' + templateUrl: './upload-blog-post-thumbnail-modal.component.html', }) -export class UploadBlogPostThumbnailModalComponent - extends ConfirmOrCancelModal { +export class UploadBlogPostThumbnailModalComponent extends ConfirmOrCancelModal { ALLOWED_IMAGE_EXTENSIONS = AppConstants.ALLOWED_IMAGE_FORMATS; ALLOWED_IMAGE_SIZE_IN_KB = AppConstants.MAX_ALLOWED_IMAGE_SIZE_IN_KB_FOR_BLOG; - constructor( - ngbActiveModal: NgbActiveModal, - ) { + constructor(ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } diff --git a/core/templates/pages/blog-dashboard-page/modal-templates/upload-blog-post-thumbnail.component.spec.ts b/core/templates/pages/blog-dashboard-page/modal-templates/upload-blog-post-thumbnail.component.spec.ts index e371f0763f2c..a865c958e15d 100644 --- a/core/templates/pages/blog-dashboard-page/modal-templates/upload-blog-post-thumbnail.component.spec.ts +++ b/core/templates/pages/blog-dashboard-page/modal-templates/upload-blog-post-thumbnail.component.spec.ts @@ -16,15 +16,15 @@ * @fileoverview Unit tests for upload blog post tumbnail modal. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ChangeDetectorRef, ElementRef, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { SvgSanitizerService } from 'services/svg-sanitizer.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { UploadBlogPostThumbnailComponent } from './upload-blog-post-thumbnail.component'; -import { ImageUploaderComponent } from 'components/forms/custom-forms-directives/image-uploader.component'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { of } from 'rxjs'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ChangeDetectorRef, ElementRef, NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {SvgSanitizerService} from 'services/svg-sanitizer.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {UploadBlogPostThumbnailComponent} from './upload-blog-post-thumbnail.component'; +import {ImageUploaderComponent} from 'components/forms/custom-forms-directives/image-uploader.component'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {of} from 'rxjs'; describe('Upload Blog Post Thumbnail Component', () => { let fixture: ComponentFixture; @@ -33,34 +33,32 @@ describe('Upload Blog Post Thumbnail Component', () => { let resizeEvent = new Event('resize'); class MockChangeDetectorRef { - detectChanges(): void { } + detectChanges(): void {} } beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - ], + imports: [HttpClientTestingModule], declarations: [ UploadBlogPostThumbnailComponent, ImageUploaderComponent, - MockTranslatePipe + MockTranslatePipe, ], providers: [ SvgSanitizerService, { provide: ChangeDetectorRef, - useClass: MockChangeDetectorRef + useClass: MockChangeDetectorRef, }, { provide: WindowDimensionsService, useValue: { isWindowNarrow: () => true, - getResizeEvent: () => of(resizeEvent) - } + getResizeEvent: () => of(resizeEvent), + }, }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -75,11 +73,11 @@ describe('Upload Blog Post Thumbnail Component', () => { }); it('should initialize cropper when window is not narrow', () => { - spyOn(windowDimensionsService, 'isWindowNarrow') - .and.returnValue(false); + spyOn(windowDimensionsService, 'isWindowNarrow').and.returnValue(false); fixture.detectChanges(); - componentInstance.croppableImageRef = ( - new ElementRef(document.createElement('img'))); + componentInstance.croppableImageRef = new ElementRef( + document.createElement('img') + ); componentInstance.initializeCropper(); @@ -87,11 +85,11 @@ describe('Upload Blog Post Thumbnail Component', () => { }); it('should initialize cropper when window is narrow', () => { - spyOn(windowDimensionsService, 'isWindowNarrow') - .and.returnValue(true); + spyOn(windowDimensionsService, 'isWindowNarrow').and.returnValue(true); fixture.detectChanges(); - componentInstance.croppableImageRef = ( - new ElementRef(document.createElement('img'))); + componentInstance.croppableImageRef = new ElementRef( + document.createElement('img') + ); componentInstance.initializeCropper(); @@ -109,8 +107,9 @@ describe('Upload Blog Post Thumbnail Component', () => { spyOn(componentInstance, 'initializeCropper'); // This is just a mock base 64 in order to test the FileReader event. let dataBase64Mock = 'VEhJUyBJUyBUSEUgQU5TV0VSCg=='; - const arrayBuffer = Uint8Array.from( - window.atob(dataBase64Mock), c => c.charCodeAt(0)); + const arrayBuffer = Uint8Array.from(window.atob(dataBase64Mock), c => + c.charCodeAt(0) + ); let file = new File([arrayBuffer], 'filename.mp3'); componentInstance.onFileChanged(file); @@ -122,7 +121,7 @@ describe('Upload Blog Post Thumbnail Component', () => { it('should remove invalid tags and attributes', () => { componentInstance.ngOnInit(); - const svgString = ( + const svgString = ' { 'h="1" d="M52289Q59 331 106 386T222 442Q257 442 2864Q412 404 406 402' + 'Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T' + '463 140Q466 150 469 151T485 153H489Q504 153 504 145284 52 289Z" ' + - 'data-name="dataName"/>' - ); + 'data-name="dataName"/>'; let file = new File([svgString], 'test.svg', {type: 'image/svg+xml'}); componentInstance.invalidImageWarningIsShown = false; @@ -153,9 +151,9 @@ describe('Upload Blog Post Thumbnail Component', () => { componentInstance.cropper = { getCroppedCanvas: () => { return { - toDataURL: () => pictureDataUrl + toDataURL: () => pictureDataUrl, }; - } + }, } as Cropper; componentInstance.save(); @@ -165,7 +163,9 @@ describe('Upload Blog Post Thumbnail Component', () => { it('should initialize', () => { const windowResizeSpy = spyOn( - windowDimensionsService, 'getResizeEvent').and.callThrough(); + windowDimensionsService, + 'getResizeEvent' + ).and.callThrough(); componentInstance.ngOnInit(); expect(componentInstance.windowIsNarrow).toBe(true); @@ -176,9 +176,7 @@ describe('Upload Blog Post Thumbnail Component', () => { it('should cancel', () => { spyOn(componentInstance.cancelThumbnailUpload, 'emit'); - componentInstance.uploadedImage = true, - - componentInstance.cancel(); + (componentInstance.uploadedImage = true), componentInstance.cancel(); expect(componentInstance.uploadedImage).toEqual(null); expect(componentInstance.cancelThumbnailUpload.emit).toHaveBeenCalled(); diff --git a/core/templates/pages/blog-dashboard-page/modal-templates/upload-blog-post-thumbnail.component.ts b/core/templates/pages/blog-dashboard-page/modal-templates/upload-blog-post-thumbnail.component.ts index b5e9b5f87a6b..c8a3c1d142ec 100644 --- a/core/templates/pages/blog-dashboard-page/modal-templates/upload-blog-post-thumbnail.component.ts +++ b/core/templates/pages/blog-dashboard-page/modal-templates/upload-blog-post-thumbnail.component.ts @@ -16,18 +16,26 @@ * @fileoverview Component for uploading thumbnail image. */ -import { ChangeDetectorRef, Component, ElementRef, Output, ViewChild, EventEmitter, OnInit } from '@angular/core'; -import { SafeResourceUrl } from '@angular/platform-browser'; -import { AppConstants } from 'app.constants'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { ImageLocalStorageService } from 'services/image-local-storage.service'; -import { SvgSanitizerService } from 'services/svg-sanitizer.service'; +import { + ChangeDetectorRef, + Component, + ElementRef, + Output, + ViewChild, + EventEmitter, + OnInit, +} from '@angular/core'; +import {SafeResourceUrl} from '@angular/platform-browser'; +import {AppConstants} from 'app.constants'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {ImageLocalStorageService} from 'services/image-local-storage.service'; +import {SvgSanitizerService} from 'services/svg-sanitizer.service'; import Cropper from 'cropperjs'; require('cropperjs/dist/cropper.min.css'); @Component({ selector: 'oppia-upload-blog-post-thumbnail', - templateUrl: './upload-blog-post-thumbnail.component.html' + templateUrl: './upload-blog-post-thumbnail.component.html', }) export class UploadBlogPostThumbnailComponent implements OnInit { // These properties are initialized using Angular lifecycle hooks @@ -36,7 +44,7 @@ export class UploadBlogPostThumbnailComponent implements OnInit { croppedFilename!: string; cropper!: Cropper; @ViewChild('croppableImage') croppableImageRef!: ElementRef; - invalidTagsAndAttributes!: { tags: string[]; attrs: string[] }; + invalidTagsAndAttributes!: {tags: string[]; attrs: string[]}; // This property will be null when the SVG uploaded is not valid or when // the image is not yet uploaded. uploadedImage: SafeResourceUrl | null = null; @@ -50,8 +58,8 @@ export class UploadBlogPostThumbnailComponent implements OnInit { private changeDetectorRef: ChangeDetectorRef, private imageLocalStorageService: ImageLocalStorageService, private svgSanitizerService: SvgSanitizerService, - private windowDimensionService: WindowDimensionsService, - ) { } + private windowDimensionService: WindowDimensionsService + ) {} initializeCropper(): void { let thumbnail = this.croppableImageRef.nativeElement; @@ -59,13 +67,13 @@ export class UploadBlogPostThumbnailComponent implements OnInit { this.cropper = new Cropper(thumbnail, { minContainerWidth: 500, minContainerHeight: 350, - aspectRatio: 4 + aspectRatio: 4, }); } else { this.cropper = new Cropper(thumbnail, { minContainerWidth: 200, minContainerHeight: 200, - aspectRatio: 4 + aspectRatio: 4, }); } } @@ -74,28 +82,29 @@ export class UploadBlogPostThumbnailComponent implements OnInit { let originalFilename = file.name; // The cropper always returns a jpeg file, thus the extension should be // changed to jpeg for the final image type to match the extension. - this.croppedFilename = ( - originalFilename - .replace(/\.([^.]*?)(?=\?|#|$)/, '.jpeg') - .replace(/ /g, '_') - ); + this.croppedFilename = originalFilename + .replace(/\.([^.]*?)(?=\?|#|$)/, '.jpeg') + .replace(/ /g, '_'); this.invalidImageWarningIsShown = false; let reader = new FileReader(); - reader.onload = (e) => { + reader.onload = e => { this.invalidTagsAndAttributes = { tags: [], - attrs: [] + attrs: [], }; let imageData = (e.target as FileReader).result as string; if (this.svgSanitizerService.isBase64Svg(imageData)) { - this.invalidTagsAndAttributes = this.svgSanitizerService - .getInvalidSvgTagsAndAttrsFromDataUri(imageData); - this.uploadedImage = this.svgSanitizerService.getTrustedSvgResourceUrl( - imageData); + this.invalidTagsAndAttributes = + this.svgSanitizerService.getInvalidSvgTagsAndAttrsFromDataUri( + imageData + ); + this.uploadedImage = + this.svgSanitizerService.getTrustedSvgResourceUrl(imageData); } if (!this.uploadedImage) { this.uploadedImage = decodeURIComponent( - (e.target as FileReader).result as string); + (e.target as FileReader).result as string + ); } try { this.changeDetectorRef.detectChanges(); @@ -115,7 +124,7 @@ export class UploadBlogPostThumbnailComponent implements OnInit { this.cropppedImageDataUrl = ''; this.invalidTagsAndAttributes = { tags: [], - attrs: [] + attrs: [], }; } @@ -125,14 +134,17 @@ export class UploadBlogPostThumbnailComponent implements OnInit { } save(): void { - this.cropppedImageDataUrl = ( - this.cropper.getCroppedCanvas({ + this.cropppedImageDataUrl = this.cropper + .getCroppedCanvas({ height: 200, width: 800, fillColor: '#fff', - }).toDataURL('image/jpeg')); + }) + .toDataURL('image/jpeg'); this.imageLocalStorageService.saveImage( - this.croppedFilename, this.cropppedImageDataUrl); + this.croppedFilename, + this.cropppedImageDataUrl + ); this.imageLocallySaved.emit(this.cropppedImageDataUrl); this.uploadedImage = null; } @@ -141,7 +153,7 @@ export class UploadBlogPostThumbnailComponent implements OnInit { this.uploadedImage = null; this.invalidTagsAndAttributes = { tags: [], - attrs: [] + attrs: [], }; this.cancelThumbnailUpload.emit(); } @@ -149,7 +161,7 @@ export class UploadBlogPostThumbnailComponent implements OnInit { ngOnInit(): void { this.invalidTagsAndAttributes = { tags: [], - attrs: [] + attrs: [], }; this.windowIsNarrow = this.windowDimensionService.isWindowNarrow(); diff --git a/core/templates/pages/blog-dashboard-page/navbar/navbar-breadcrumb/blog-dashboard-navbar-breadcrumb.component.spec.ts b/core/templates/pages/blog-dashboard-page/navbar/navbar-breadcrumb/blog-dashboard-navbar-breadcrumb.component.spec.ts index 3b1901559a5d..ef6caf05588b 100644 --- a/core/templates/pages/blog-dashboard-page/navbar/navbar-breadcrumb/blog-dashboard-navbar-breadcrumb.component.spec.ts +++ b/core/templates/pages/blog-dashboard-page/navbar/navbar-breadcrumb/blog-dashboard-navbar-breadcrumb.component.spec.ts @@ -16,13 +16,18 @@ * @fileoverview Unit tests for the blog dashboard navbar breadcrumb. */ -import { ComponentFixture, TestBed, fakeAsync, waitForAsync, tick } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { BlogDashboardNavbarBreadcrumbComponent } from 'pages/blog-dashboard-page/navbar/navbar-breadcrumb/blog-dashboard-navbar-breadcrumb.component'; -import { BlogDashboardPageService } from 'pages/blog-dashboard-page/services/blog-dashboard-page.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { EventEmitter } from '@angular/core'; - +import { + ComponentFixture, + TestBed, + fakeAsync, + waitForAsync, + tick, +} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {BlogDashboardNavbarBreadcrumbComponent} from 'pages/blog-dashboard-page/navbar/navbar-breadcrumb/blog-dashboard-navbar-breadcrumb.component'; +import {BlogDashboardPageService} from 'pages/blog-dashboard-page/services/blog-dashboard-page.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {EventEmitter} from '@angular/core'; describe('Blog Dashboard Page Component', () => { let blogDashboardPageService: BlogDashboardPageService; @@ -35,9 +40,9 @@ describe('Blog Dashboard Page Component', () => { location: { href: '', hash: '/', - _hashChange: null + _hashChange: null, }, - open: (url: string) => { }, + open: (url: string) => {}, onhashchange() { return this.location._hashChange; }, @@ -46,25 +51,20 @@ describe('Blog Dashboard Page Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - ], - declarations: [ - BlogDashboardNavbarBreadcrumbComponent, - ], + imports: [HttpClientTestingModule], + declarations: [BlogDashboardNavbarBreadcrumbComponent], providers: [ BlogDashboardPageService, { provide: WindowRef, - useClass: MockWindowRef + useClass: MockWindowRef, }, ], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent( - BlogDashboardNavbarBreadcrumbComponent); + fixture = TestBed.createComponent(BlogDashboardNavbarBreadcrumbComponent); component = fixture.componentInstance; mockWindowRef = TestBed.inject(WindowRef) as unknown as MockWindowRef; blogDashboardPageService = TestBed.inject(BlogDashboardPageService); @@ -74,7 +74,6 @@ describe('Blog Dashboard Page Component', () => { component.ngOnDestroy(); }); - it('should create', () => { expect(component).toBeDefined(); }); @@ -91,8 +90,10 @@ describe('Blog Dashboard Page Component', () => { })); it('should set title when title change event is emitted', fakeAsync(() => { - spyOn(blogDashboardPageService, 'updateNavTitleEventEmitter') - .and.returnValue(new EventEmitter()); + spyOn( + blogDashboardPageService, + 'updateNavTitleEventEmitter' + ).and.returnValue(new EventEmitter()); component.ngOnInit(); blogDashboardPageService.updateNavTitleEventEmitter.emit('new title'); @@ -107,9 +108,11 @@ describe('Blog Dashboard Page Component', () => { component.ngOnInit(); - expect(blogDashboardPageService.updateViewEventEmitter.subscribe) - .toHaveBeenCalled(); - expect(blogDashboardPageService.updateNavTitleEventEmitter.subscribe) - .toHaveBeenCalled(); + expect( + blogDashboardPageService.updateViewEventEmitter.subscribe + ).toHaveBeenCalled(); + expect( + blogDashboardPageService.updateNavTitleEventEmitter.subscribe + ).toHaveBeenCalled(); }); }); diff --git a/core/templates/pages/blog-dashboard-page/navbar/navbar-breadcrumb/blog-dashboard-navbar-breadcrumb.component.ts b/core/templates/pages/blog-dashboard-page/navbar/navbar-breadcrumb/blog-dashboard-navbar-breadcrumb.component.ts index 0da98f006363..be6ae76a8b6e 100644 --- a/core/templates/pages/blog-dashboard-page/navbar/navbar-breadcrumb/blog-dashboard-navbar-breadcrumb.component.ts +++ b/core/templates/pages/blog-dashboard-page/navbar/navbar-breadcrumb/blog-dashboard-navbar-breadcrumb.component.ts @@ -16,40 +16,37 @@ * @fileoverview Component for the navbar breadcrumb of the blog dashboard. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { Subscription } from 'rxjs'; -import { BlogDashboardPageService } from 'pages/blog-dashboard-page/services/blog-dashboard-page.service'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {Subscription} from 'rxjs'; +import {BlogDashboardPageService} from 'pages/blog-dashboard-page/services/blog-dashboard-page.service'; @Component({ selector: 'oppia-blog-dashboard-navbar-breadcrumb', - templateUrl: './blog-dashboard-navbar-breadcrumb.component.html' + templateUrl: './blog-dashboard-navbar-breadcrumb.component.html', }) export class BlogDashboardNavbarBreadcrumbComponent -implements OnInit, OnDestroy { + implements OnInit, OnDestroy +{ // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 activeTab!: string; title!: string; directiveSubscriptions = new Subscription(); - constructor( - private blogDashboardPageService: BlogDashboardPageService, - ) {} + constructor(private blogDashboardPageService: BlogDashboardPageService) {} ngOnInit(): void { this.activeTab = this.blogDashboardPageService.activeTab; this.directiveSubscriptions.add( - this.blogDashboardPageService.updateViewEventEmitter.subscribe( - () => { - this.activeTab = this.blogDashboardPageService.activeTab; - } - ) + this.blogDashboardPageService.updateViewEventEmitter.subscribe(() => { + this.activeTab = this.blogDashboardPageService.activeTab; + }) ); this.directiveSubscriptions.add( this.blogDashboardPageService.updateNavTitleEventEmitter.subscribe( - (title) => { + title => { this.title = title; } ) @@ -61,7 +58,9 @@ implements OnInit, OnDestroy { } } -angular.module('oppia').directive('oppiaBlogDashboardNavbarBreadcrumb', +angular.module('oppia').directive( + 'oppiaBlogDashboardNavbarBreadcrumb', downgradeComponent({ - component: BlogDashboardNavbarBreadcrumbComponent - })); + component: BlogDashboardNavbarBreadcrumbComponent, + }) +); diff --git a/core/templates/pages/blog-dashboard-page/navbar/navbar-pre-logo-action/blog-post-editor-pre-logo-action.component.spec.ts b/core/templates/pages/blog-dashboard-page/navbar/navbar-pre-logo-action/blog-post-editor-pre-logo-action.component.spec.ts index dfb55db36295..2a1a88dd984b 100644 --- a/core/templates/pages/blog-dashboard-page/navbar/navbar-pre-logo-action/blog-post-editor-pre-logo-action.component.spec.ts +++ b/core/templates/pages/blog-dashboard-page/navbar/navbar-pre-logo-action/blog-post-editor-pre-logo-action.component.spec.ts @@ -16,12 +16,17 @@ * @fileoverview Unit tests for the blog post editor navbar pre logo action. */ -import { ComponentFixture, TestBed, fakeAsync, waitForAsync, tick } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { BlogPostEditorNavbarPreLogoActionComponent } from 'pages/blog-dashboard-page/navbar/navbar-pre-logo-action/blog-post-editor-pre-logo-action.component'; -import { BlogDashboardPageService } from 'pages/blog-dashboard-page/services/blog-dashboard-page.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; - +import { + ComponentFixture, + TestBed, + fakeAsync, + waitForAsync, + tick, +} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {BlogPostEditorNavbarPreLogoActionComponent} from 'pages/blog-dashboard-page/navbar/navbar-pre-logo-action/blog-post-editor-pre-logo-action.component'; +import {BlogDashboardPageService} from 'pages/blog-dashboard-page/services/blog-dashboard-page.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; describe('Blog Dashboard Page Component', () => { let blogDashboardPageService: BlogDashboardPageService; @@ -34,9 +39,9 @@ describe('Blog Dashboard Page Component', () => { location: { href: '', hash: '/', - _hashChange: null + _hashChange: null, }, - open: (url: string) => { }, + open: (url: string) => {}, onhashchange() { return this.location._hashChange; }, @@ -45,17 +50,13 @@ describe('Blog Dashboard Page Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - ], - declarations: [ - BlogPostEditorNavbarPreLogoActionComponent, - ], + imports: [HttpClientTestingModule], + declarations: [BlogPostEditorNavbarPreLogoActionComponent], providers: [ BlogDashboardPageService, { provide: WindowRef, - useClass: MockWindowRef + useClass: MockWindowRef, }, ], }).compileComponents(); @@ -63,7 +64,8 @@ describe('Blog Dashboard Page Component', () => { beforeEach(() => { fixture = TestBed.createComponent( - BlogPostEditorNavbarPreLogoActionComponent); + BlogPostEditorNavbarPreLogoActionComponent + ); component = fixture.componentInstance; mockWindowRef = TestBed.inject(WindowRef) as unknown as MockWindowRef; blogDashboardPageService = TestBed.inject(BlogDashboardPageService); @@ -73,7 +75,6 @@ describe('Blog Dashboard Page Component', () => { component.ngOnDestroy(); }); - it('should create', () => { expect(component).toBeDefined(); }); @@ -94,7 +95,8 @@ describe('Blog Dashboard Page Component', () => { component.ngOnInit(); - expect(blogDashboardPageService.updateViewEventEmitter.subscribe) - .toHaveBeenCalled(); + expect( + blogDashboardPageService.updateViewEventEmitter.subscribe + ).toHaveBeenCalled(); }); }); diff --git a/core/templates/pages/blog-dashboard-page/navbar/navbar-pre-logo-action/blog-post-editor-pre-logo-action.component.ts b/core/templates/pages/blog-dashboard-page/navbar/navbar-pre-logo-action/blog-post-editor-pre-logo-action.component.ts index 9c0893c74bc1..f322b52f5365 100644 --- a/core/templates/pages/blog-dashboard-page/navbar/navbar-pre-logo-action/blog-post-editor-pre-logo-action.component.ts +++ b/core/templates/pages/blog-dashboard-page/navbar/navbar-pre-logo-action/blog-post-editor-pre-logo-action.component.ts @@ -17,34 +17,31 @@ * of the blog dashboard. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { Subscription } from 'rxjs'; -import { BlogDashboardPageService } from 'pages/blog-dashboard-page/services/blog-dashboard-page.service'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {Subscription} from 'rxjs'; +import {BlogDashboardPageService} from 'pages/blog-dashboard-page/services/blog-dashboard-page.service'; @Component({ selector: 'oppia-blog-post-editor-pre-logo-action', - templateUrl: './blog-post-editor-pre-logo-action.component.html' + templateUrl: './blog-post-editor-pre-logo-action.component.html', }) export class BlogPostEditorNavbarPreLogoActionComponent -implements OnInit, OnDestroy { + implements OnInit, OnDestroy +{ // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 activeTab!: string; directiveSubscriptions = new Subscription(); - constructor( - private blogDashboardPageService: BlogDashboardPageService, - ) {} + constructor(private blogDashboardPageService: BlogDashboardPageService) {} ngOnInit(): void { this.activeTab = this.blogDashboardPageService.activeTab; this.directiveSubscriptions.add( - this.blogDashboardPageService.updateViewEventEmitter.subscribe( - () => { - this.activeTab = this.blogDashboardPageService.activeTab; - } - ) + this.blogDashboardPageService.updateViewEventEmitter.subscribe(() => { + this.activeTab = this.blogDashboardPageService.activeTab; + }) ); } @@ -52,5 +49,9 @@ implements OnInit, OnDestroy { return this.directiveSubscriptions.unsubscribe(); } } -angular.module('oppia').directive('oppiaBlogPostEditorPreLogoAction', - downgradeComponent({component: BlogPostEditorNavbarPreLogoActionComponent})); +angular + .module('oppia') + .directive( + 'oppiaBlogPostEditorPreLogoAction', + downgradeComponent({component: BlogPostEditorNavbarPreLogoActionComponent}) + ); diff --git a/core/templates/pages/blog-dashboard-page/services/blog-dashboard-page.service.spec.ts b/core/templates/pages/blog-dashboard-page/services/blog-dashboard-page.service.spec.ts index 37df5d4cd449..47f28a2f0c85 100644 --- a/core/templates/pages/blog-dashboard-page/services/blog-dashboard-page.service.spec.ts +++ b/core/templates/pages/blog-dashboard-page/services/blog-dashboard-page.service.spec.ts @@ -16,13 +16,13 @@ * @fileoverview Unit Tests for Blog Dashboard Page service. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { fakeAsync, TestBed, tick, waitForAsync} from '@angular/core/testing'; -import { BlogPostEditorBackendApiService } from 'domain/blog/blog-post-editor-backend-api.service'; -import { AlertsService } from 'services/alerts.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { BlogDashboardPageService } from './blog-dashboard-page.service'; -import { BlogPostData } from 'domain/blog/blog-post.model'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {fakeAsync, TestBed, tick, waitForAsync} from '@angular/core/testing'; +import {BlogPostEditorBackendApiService} from 'domain/blog/blog-post-editor-backend-api.service'; +import {AlertsService} from 'services/alerts.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {BlogDashboardPageService} from './blog-dashboard-page.service'; +import {BlogPostData} from 'domain/blog/blog-post.model'; describe('Blog Post Page service', () => { let alertsService: AlertsService; @@ -34,7 +34,7 @@ describe('Blog Post Page service', () => { location: { href: '', hash: '/', - reload: () => {} + reload: () => {}, }, open: (url: string) => {}, onhashchange() {}, @@ -43,22 +43,21 @@ describe('Blog Post Page service', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - ], + imports: [HttpClientTestingModule], providers: [ BlogPostEditorBackendApiService, { provide: WindowRef, - useClass: MockWindowRef + useClass: MockWindowRef, }, - ] + ], }).compileComponents(); })); beforeEach(() => { blogPostEditorBackendApiService = TestBed.inject( - BlogPostEditorBackendApiService); + BlogPostEditorBackendApiService + ); blogDashboardPageService = TestBed.inject(BlogDashboardPageService); mockWindowRef = TestBed.inject(WindowRef) as unknown as MockWindowRef; alertsService = TestBed.inject(AlertsService); @@ -81,7 +80,6 @@ describe('Blog Post Page service', () => { expect(mockWindowRef.nativeWindow.location.href).toBe('/blog-dashboard'); }); - it('should handle calls with unexpect paths', () => { expect(blogDashboardPageService.activeTab).toEqual('main'); @@ -105,56 +103,67 @@ describe('Blog Post Page service', () => { expect(blogDashboardPageService.imageUploaderIsNarrow).toEqual(false); }); - it('should display alert when unable to delete blog post data', - fakeAsync(() => { - spyOn(blogPostEditorBackendApiService, 'deleteBlogPostAsync') - .and.returnValue(Promise.reject({status: 500})); - spyOn(alertsService, 'addWarning'); - - blogDashboardPageService.deleteBlogPost(); - tick(); - - expect(blogPostEditorBackendApiService.deleteBlogPostAsync) - .toHaveBeenCalled(); - expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Failed to delete blog post.'); - })); - - it('should successfully delete blog post data from blog post editor', - fakeAsync(() => { - // Setting active tab as blog post editor. - blogDashboardPageService.navigateToEditorTabWithId('sampleId1234'); - mockWindowRef.nativeWindow.onhashchange(); - spyOn(blogPostEditorBackendApiService, 'deleteBlogPostAsync') - .and.returnValue(Promise.resolve(200)); - spyOn(alertsService, 'addSuccessMessage'); - - blogDashboardPageService.deleteBlogPost(); - tick(); - - expect(blogPostEditorBackendApiService.deleteBlogPostAsync) - .toHaveBeenCalled(); - expect(alertsService.addSuccessMessage).toHaveBeenCalledWith( - 'Blog Post Deleted Successfully.', 5000); - expect(blogDashboardPageService.activeTab).toBe('editor_tab'); - })); - - it('should successfully delete blog post data from dashboard', - fakeAsync(() => { - spyOn(blogPostEditorBackendApiService, 'deleteBlogPostAsync') - .and.returnValue(Promise.resolve(200)); - spyOn(blogDashboardPageService, 'navigateToMainTab'); - spyOn(alertsService, 'addSuccessMessage'); - - blogDashboardPageService.deleteBlogPost(); - tick(); - - expect(blogPostEditorBackendApiService.deleteBlogPostAsync) - .toHaveBeenCalled(); - expect(alertsService.addSuccessMessage).toHaveBeenCalledWith( - 'Blog Post Deleted Successfully.', 5000); - expect(blogDashboardPageService.navigateToMainTab).not.toHaveBeenCalled(); - })); + it('should display alert when unable to delete blog post data', fakeAsync(() => { + spyOn( + blogPostEditorBackendApiService, + 'deleteBlogPostAsync' + ).and.returnValue(Promise.reject({status: 500})); + spyOn(alertsService, 'addWarning'); + + blogDashboardPageService.deleteBlogPost(); + tick(); + + expect( + blogPostEditorBackendApiService.deleteBlogPostAsync + ).toHaveBeenCalled(); + expect(alertsService.addWarning).toHaveBeenCalledWith( + 'Failed to delete blog post.' + ); + })); + + it('should successfully delete blog post data from blog post editor', fakeAsync(() => { + // Setting active tab as blog post editor. + blogDashboardPageService.navigateToEditorTabWithId('sampleId1234'); + mockWindowRef.nativeWindow.onhashchange(); + spyOn( + blogPostEditorBackendApiService, + 'deleteBlogPostAsync' + ).and.returnValue(Promise.resolve(200)); + spyOn(alertsService, 'addSuccessMessage'); + + blogDashboardPageService.deleteBlogPost(); + tick(); + + expect( + blogPostEditorBackendApiService.deleteBlogPostAsync + ).toHaveBeenCalled(); + expect(alertsService.addSuccessMessage).toHaveBeenCalledWith( + 'Blog Post Deleted Successfully.', + 5000 + ); + expect(blogDashboardPageService.activeTab).toBe('editor_tab'); + })); + + it('should successfully delete blog post data from dashboard', fakeAsync(() => { + spyOn( + blogPostEditorBackendApiService, + 'deleteBlogPostAsync' + ).and.returnValue(Promise.resolve(200)); + spyOn(blogDashboardPageService, 'navigateToMainTab'); + spyOn(alertsService, 'addSuccessMessage'); + + blogDashboardPageService.deleteBlogPost(); + tick(); + + expect( + blogPostEditorBackendApiService.deleteBlogPostAsync + ).toHaveBeenCalled(); + expect(alertsService.addSuccessMessage).toHaveBeenCalledWith( + 'Blog Post Deleted Successfully.', + 5000 + ); + expect(blogDashboardPageService.navigateToMainTab).not.toHaveBeenCalled(); + })); it('should succesfully set the blog post title in navbar', fakeAsync(() => { spyOn(blogDashboardPageService.updateNavTitleEventEmitter, 'emit'); @@ -162,20 +171,23 @@ describe('Blog Post Page service', () => { blogDashboardPageService.setNavTitle(false, ''); tick(); - expect(blogDashboardPageService.updateNavTitleEventEmitter.emit) - .toHaveBeenCalledWith('New Post - Untitled'); + expect( + blogDashboardPageService.updateNavTitleEventEmitter.emit + ).toHaveBeenCalledWith('New Post - Untitled'); blogDashboardPageService.setNavTitle(false, 'Sample Title'); tick(); - expect(blogDashboardPageService.updateNavTitleEventEmitter.emit) - .toHaveBeenCalledWith('Draft - Sample Title'); + expect( + blogDashboardPageService.updateNavTitleEventEmitter.emit + ).toHaveBeenCalledWith('Draft - Sample Title'); blogDashboardPageService.setNavTitle(true, 'Sample Title'); tick(); - expect(blogDashboardPageService.updateNavTitleEventEmitter.emit) - .toHaveBeenCalledWith('Published - Sample Title'); + expect( + blogDashboardPageService.updateNavTitleEventEmitter.emit + ).toHaveBeenCalledWith('Published - Sample Title'); })); it('should set and retrieve blogPostId correctly', () => { @@ -185,17 +197,17 @@ describe('Blog Post Page service', () => { }); it('should set and retrieve blog post data correctly', () => { - let summaryObject = BlogPostData.createFromBackendDict( - { id: 'sampleId', - displayed_author_name: 'test_user', - title: 'Title', - content: 'Hello World', - tags: ['news'], - thumbnail_filename: 'image.png', - url_fragment: 'title', - last_updated: '3232323', - published_on: '3232323', - }); + let summaryObject = BlogPostData.createFromBackendDict({ + id: 'sampleId', + displayed_author_name: 'test_user', + title: 'Title', + content: 'Hello World', + tags: ['news'], + thumbnail_filename: 'image.png', + url_fragment: 'title', + last_updated: '3232323', + published_on: '3232323', + }); blogDashboardPageService.blogPostData = summaryObject; diff --git a/core/templates/pages/blog-dashboard-page/services/blog-dashboard-page.service.ts b/core/templates/pages/blog-dashboard-page/services/blog-dashboard-page.service.ts index 9a6f5debd634..6e538696d850 100644 --- a/core/templates/pages/blog-dashboard-page/services/blog-dashboard-page.service.ts +++ b/core/templates/pages/blog-dashboard-page/services/blog-dashboard-page.service.ts @@ -16,19 +16,19 @@ * @fileoverview Service that handles data and routing on blog dashboard page. */ -import { Injectable, EventEmitter } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { AlertsService } from 'services/alerts.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { UrlService } from 'services/contextual/url.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { BlogDashboardPageConstants } from 'pages/blog-dashboard-page/blog-dashboard-page.constants'; -import { BlogPostEditorBackendApiService } from 'domain/blog/blog-post-editor-backend-api.service'; -import { PreventPageUnloadEventService } from 'services/prevent-page-unload-event.service'; -import { BlogPostData } from 'domain/blog/blog-post.model'; +import {Injectable, EventEmitter} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {AlertsService} from 'services/alerts.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {UrlService} from 'services/contextual/url.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {BlogDashboardPageConstants} from 'pages/blog-dashboard-page/blog-dashboard-page.constants'; +import {BlogPostEditorBackendApiService} from 'domain/blog/blog-post-editor-backend-api.service'; +import {PreventPageUnloadEventService} from 'services/prevent-page-unload-event.service'; +import {BlogPostData} from 'domain/blog/blog-post.model'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class BlogDashboardPageService { // This property is initialized using getters and setters @@ -36,8 +36,8 @@ export class BlogDashboardPageService { // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 private _blogPostData!: BlogPostData; private _blogPostId: string = ''; - private _BLOG_POST_EDITOR_URL_TEMPLATE = ( - BlogDashboardPageConstants.BLOG_DASHBOARD_TAB_URLS.BLOG_POST_EDITOR); + private _BLOG_POST_EDITOR_URL_TEMPLATE = + BlogDashboardPageConstants.BLOG_DASHBOARD_TAB_URLS.BLOG_POST_EDITOR; private _activeTab = 'main'; private _blogPostAction: string = ''; @@ -51,7 +51,7 @@ export class BlogDashboardPageService { private urlInterpolationService: UrlInterpolationService, private urlService: UrlService, private windowRef: WindowRef, - private preventPageUnloadEventService: PreventPageUnloadEventService, + private preventPageUnloadEventService: PreventPageUnloadEventService ) { let currentHash: string = this.windowRef.nativeWindow.location.hash; this._setActiveTab(currentHash); @@ -78,9 +78,11 @@ export class BlogDashboardPageService { navigateToEditorTabWithId(blogPostId: string): void { let blogPostEditorUrl = this.urlInterpolationService.interpolateUrl( - this._BLOG_POST_EDITOR_URL_TEMPLATE, { - blog_post_id: blogPostId - }); + this._BLOG_POST_EDITOR_URL_TEMPLATE, + { + blog_post_id: blogPostId, + } + ); this.windowRef.nativeWindow.location.hash = blogPostEditorUrl; } @@ -133,22 +135,25 @@ export class BlogDashboardPageService { } deleteBlogPost(): void { - this.blogPostEditorBackendService.deleteBlogPostAsync(this._blogPostId) + this.blogPostEditorBackendService + .deleteBlogPostAsync(this._blogPostId) .then( () => { this.alertsService.addSuccessMessage( - 'Blog Post Deleted Successfully.', 5000); + 'Blog Post Deleted Successfully.', + 5000 + ); if (this.activeTab === 'editor_tab') { this.navigateToMainTab(); } - }, (errorResponse) => { + }, + errorResponse => { this.alertsService.addWarning('Failed to delete blog post.'); } ); } - setNavTitle( - blogPostIsPublished: boolean, title: string): void { + setNavTitle(blogPostIsPublished: boolean, title: string): void { if (title) { if (blogPostIsPublished) { return this.updateNavTitleEventEmitter.emit(`Published - ${title}`); @@ -161,5 +166,9 @@ export class BlogDashboardPageService { } } -angular.module('oppia').factory('BlogDashboardPageService', - downgradeInjectable(BlogDashboardPageService)); +angular + .module('oppia') + .factory( + 'BlogDashboardPageService', + downgradeInjectable(BlogDashboardPageService) + ); diff --git a/core/templates/pages/blog-dashboard-page/shared-blog-components.module.ts b/core/templates/pages/blog-dashboard-page/shared-blog-components.module.ts index cde84cf9c1c9..142d6ec576ea 100644 --- a/core/templates/pages/blog-dashboard-page/shared-blog-components.module.ts +++ b/core/templates/pages/blog-dashboard-page/shared-blog-components.module.ts @@ -16,23 +16,23 @@ * @fileoverview Module for the shared blog-dashboard components. */ -import { NgModule} from '@angular/core'; -import { MatTabsModule } from '@angular/material/tabs'; -import { MatMenuModule } from '@angular/material/menu'; -import { MatButtonToggleModule } from '@angular/material/button-toggle'; -import { HttpClientModule } from '@angular/common/http'; -import { SharedComponentsModule } from 'components/shared-component.module'; +import {NgModule} from '@angular/core'; +import {MatTabsModule} from '@angular/material/tabs'; +import {MatMenuModule} from '@angular/material/menu'; +import {MatButtonToggleModule} from '@angular/material/button-toggle'; +import {HttpClientModule} from '@angular/common/http'; +import {SharedComponentsModule} from 'components/shared-component.module'; -import { BlogPostActionConfirmationModalComponent } from 'pages/blog-dashboard-page/blog-post-action-confirmation/blog-post-action-confirmation.component'; -import { BlogCardComponent } from 'pages/blog-dashboard-page/blog-card/blog-card.component'; -import { BlogDashboardTileComponent } from 'pages/blog-dashboard-page/blog-dashboard-tile/blog-dashboard-tile.component'; -import { BlogDashboardNavbarBreadcrumbComponent } from 'pages/blog-dashboard-page/navbar/navbar-breadcrumb/blog-dashboard-navbar-breadcrumb.component'; -import { BlogPostEditorComponent } from 'pages/blog-dashboard-page/blog-post-editor/blog-post-editor.component'; -import { UploadBlogPostThumbnailModalComponent } from 'pages/blog-dashboard-page/modal-templates/upload-blog-post-thumbnail-modal.component'; -import { BlogCardPreviewModalComponent } from 'pages/blog-dashboard-page/modal-templates/blog-card-preview-modal.component'; -import { UploadBlogPostThumbnailComponent } from 'pages/blog-dashboard-page/modal-templates/upload-blog-post-thumbnail.component'; -import { BlogPostEditorNavbarPreLogoActionComponent } from 'pages/blog-dashboard-page/navbar/navbar-pre-logo-action/blog-post-editor-pre-logo-action.component'; -import { CommonModule } from '@angular/common'; +import {BlogPostActionConfirmationModalComponent} from 'pages/blog-dashboard-page/blog-post-action-confirmation/blog-post-action-confirmation.component'; +import {BlogCardComponent} from 'pages/blog-dashboard-page/blog-card/blog-card.component'; +import {BlogDashboardTileComponent} from 'pages/blog-dashboard-page/blog-dashboard-tile/blog-dashboard-tile.component'; +import {BlogDashboardNavbarBreadcrumbComponent} from 'pages/blog-dashboard-page/navbar/navbar-breadcrumb/blog-dashboard-navbar-breadcrumb.component'; +import {BlogPostEditorComponent} from 'pages/blog-dashboard-page/blog-post-editor/blog-post-editor.component'; +import {UploadBlogPostThumbnailModalComponent} from 'pages/blog-dashboard-page/modal-templates/upload-blog-post-thumbnail-modal.component'; +import {BlogCardPreviewModalComponent} from 'pages/blog-dashboard-page/modal-templates/blog-card-preview-modal.component'; +import {UploadBlogPostThumbnailComponent} from 'pages/blog-dashboard-page/modal-templates/upload-blog-post-thumbnail.component'; +import {BlogPostEditorNavbarPreLogoActionComponent} from 'pages/blog-dashboard-page/navbar/navbar-pre-logo-action/blog-post-editor-pre-logo-action.component'; +import {CommonModule} from '@angular/common'; @NgModule({ imports: [ @@ -41,7 +41,7 @@ import { CommonModule } from '@angular/common'; SharedComponentsModule, MatTabsModule, MatMenuModule, - MatButtonToggleModule + MatButtonToggleModule, ], declarations: [ BlogDashboardNavbarBreadcrumbComponent, @@ -52,7 +52,7 @@ import { CommonModule } from '@angular/common'; UploadBlogPostThumbnailModalComponent, BlogCardPreviewModalComponent, UploadBlogPostThumbnailComponent, - BlogPostEditorNavbarPreLogoActionComponent + BlogPostEditorNavbarPreLogoActionComponent, ], entryComponents: [ BlogDashboardNavbarBreadcrumbComponent, @@ -63,7 +63,7 @@ import { CommonModule } from '@angular/common'; UploadBlogPostThumbnailModalComponent, BlogCardPreviewModalComponent, UploadBlogPostThumbnailComponent, - BlogPostEditorNavbarPreLogoActionComponent + BlogPostEditorNavbarPreLogoActionComponent, ], exports: [ BlogDashboardNavbarBreadcrumbComponent, @@ -74,7 +74,7 @@ import { CommonModule } from '@angular/common'; UploadBlogPostThumbnailModalComponent, BlogCardPreviewModalComponent, UploadBlogPostThumbnailComponent, - BlogPostEditorNavbarPreLogoActionComponent + BlogPostEditorNavbarPreLogoActionComponent, ], }) export class SharedBlogComponentsModule {} diff --git a/core/templates/pages/blog-home-page/blog-home-page-root.component.spec.ts b/core/templates/pages/blog-home-page/blog-home-page-root.component.spec.ts index e1d5a61bd2d4..8a7e15765dbc 100644 --- a/core/templates/pages/blog-home-page/blog-home-page-root.component.spec.ts +++ b/core/templates/pages/blog-home-page/blog-home-page-root.component.spec.ts @@ -16,20 +16,26 @@ * @fileoverview Unit tests for the blog home page root component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; - -import { AppConstants } from 'app.constants'; -import { AccessValidationBackendApiService } from 'pages/oppia-root/routing/access-validation-backend-api.service'; -import { MetaTagCustomizationService } from 'services/contextual/meta-tag-customization.service'; -import { LoaderService } from 'services/loader.service'; -import { PageHeadService } from 'services/page-head.service'; - -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { BlogHomePageRootComponent } from './blog-home-page-root.component'; -import { UserService } from 'services/user.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; + +import {AppConstants} from 'app.constants'; +import {AccessValidationBackendApiService} from 'pages/oppia-root/routing/access-validation-backend-api.service'; +import {MetaTagCustomizationService} from 'services/contextual/meta-tag-customization.service'; +import {LoaderService} from 'services/loader.service'; +import {PageHeadService} from 'services/page-head.service'; + +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {BlogHomePageRootComponent} from './blog-home-page-root.component'; +import {UserService} from 'services/user.service'; class MockTranslateService { onLangChange: EventEmitter = new EventEmitter(); @@ -49,23 +55,18 @@ describe('Blog Home Page Root', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - BlogHomePageRootComponent, - MockTranslatePipe - ], + imports: [HttpClientTestingModule], + declarations: [BlogHomePageRootComponent, MockTranslatePipe], providers: [ PageHeadService, UserService, MetaTagCustomizationService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -75,22 +76,24 @@ describe('Blog Home Page Root', () => { pageHeadService = TestBed.inject(PageHeadService); loaderService = TestBed.inject(LoaderService); accessValidationBackendApiService = TestBed.inject( - AccessValidationBackendApiService); + AccessValidationBackendApiService + ); userService = TestBed.inject(UserService); translateService = TestBed.inject(TranslateService); }); - it('should successfully instantiate the component', - () => { - expect(component).toBeDefined(); - }); + it('should successfully instantiate the component', () => { + expect(component).toBeDefined(); + }); it('should initialize and show page when access is valid', fakeAsync(() => { spyOn(userService, 'canUserEditBlogPosts').and.returnValue( - Promise.resolve(false)); + Promise.resolve(false) + ); spyOn( - accessValidationBackendApiService, 'validateAccessToBlogHomePage') - .and.returnValue(Promise.resolve()); + accessValidationBackendApiService, + 'validateAccessToBlogHomePage' + ).and.returnValue(Promise.resolve()); spyOn(loaderService, 'showLoadingScreen'); spyOn(loaderService, 'hideLoadingScreen'); @@ -99,37 +102,41 @@ describe('Blog Home Page Root', () => { tick(); expect(loaderService.showLoadingScreen).toHaveBeenCalled(); - expect(accessValidationBackendApiService.validateAccessToBlogHomePage) - .toHaveBeenCalled(); + expect( + accessValidationBackendApiService.validateAccessToBlogHomePage + ).toHaveBeenCalled(); expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); })); - it('should initialize and show error page when server respond with error', - fakeAsync(() => { - spyOn(userService, 'canUserEditBlogPosts').and.returnValue( - Promise.resolve(false)); - spyOn( - accessValidationBackendApiService, 'validateAccessToBlogHomePage') - .and.returnValue(Promise.reject()); - spyOn(loaderService, 'showLoadingScreen'); - spyOn(loaderService, 'hideLoadingScreen'); - - component.ngOnInit(); - tick(); - tick(); - - expect(loaderService.showLoadingScreen).toHaveBeenCalled(); - expect(accessValidationBackendApiService.validateAccessToBlogHomePage) - .toHaveBeenCalled(); - expect(component.pageIsShown).toBeFalse(); - expect(component.errorPageIsShown).toBeTrue(); - expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); - })); + it('should initialize and show error page when server respond with error', fakeAsync(() => { + spyOn(userService, 'canUserEditBlogPosts').and.returnValue( + Promise.resolve(false) + ); + spyOn( + accessValidationBackendApiService, + 'validateAccessToBlogHomePage' + ).and.returnValue(Promise.reject()); + spyOn(loaderService, 'showLoadingScreen'); + spyOn(loaderService, 'hideLoadingScreen'); + + component.ngOnInit(); + tick(); + tick(); + + expect(loaderService.showLoadingScreen).toHaveBeenCalled(); + expect( + accessValidationBackendApiService.validateAccessToBlogHomePage + ).toHaveBeenCalled(); + expect(component.pageIsShown).toBeFalse(); + expect(component.errorPageIsShown).toBeTrue(); + expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); + })); it('should initialize and subscribe to onLangChange', fakeAsync(() => { spyOn( - accessValidationBackendApiService, 'validateAccessToBlogHomePage') - .and.returnValue(Promise.resolve()); + accessValidationBackendApiService, + 'validateAccessToBlogHomePage' + ).and.returnValue(Promise.resolve()); spyOn(component.directiveSubscriptions, 'add'); spyOn(translateService.onLangChange, 'subscribe'); @@ -142,8 +149,9 @@ describe('Blog Home Page Root', () => { it('should update page title whenever the language changes', () => { spyOn( - accessValidationBackendApiService, 'validateAccessToBlogHomePage') - .and.returnValue(Promise.resolve()); + accessValidationBackendApiService, + 'validateAccessToBlogHomePage' + ).and.returnValue(Promise.resolve()); component.ngOnInit(); spyOn(component, 'setPageTitleAndMetaTags'); @@ -159,7 +167,8 @@ describe('Blog Home Page Root', () => { component.setPageTitleAndMetaTags(); expect(translateService.instant).toHaveBeenCalledWith( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.BLOG_HOMEPAGE.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.BLOG_HOMEPAGE.TITLE + ); expect(pageHeadService.updateTitleAndMetaTags).toHaveBeenCalledWith( AppConstants.PAGES_REGISTERED_WITH_FRONTEND.BLOG_HOMEPAGE.TITLE, AppConstants.PAGES_REGISTERED_WITH_FRONTEND.BLOG_HOMEPAGE.META diff --git a/core/templates/pages/blog-home-page/blog-home-page-root.component.ts b/core/templates/pages/blog-home-page/blog-home-page-root.component.ts index 972e087a034f..f4843fbcca60 100644 --- a/core/templates/pages/blog-home-page/blog-home-page-root.component.ts +++ b/core/templates/pages/blog-home-page/blog-home-page-root.component.ts @@ -16,19 +16,19 @@ * @fileoverview Root component for blog home page. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; -import { AppConstants } from 'app.constants'; -import { AccessValidationBackendApiService } from 'pages/oppia-root/routing/access-validation-backend-api.service'; -import { LoaderService } from 'services/loader.service'; -import { PageHeadService } from 'services/page-head.service'; -import { UserService } from 'services/user.service'; +import {AppConstants} from 'app.constants'; +import {AccessValidationBackendApiService} from 'pages/oppia-root/routing/access-validation-backend-api.service'; +import {LoaderService} from 'services/loader.service'; +import {PageHeadService} from 'services/page-head.service'; +import {UserService} from 'services/user.service'; @Component({ selector: 'oppia-blog-home-page-root', - templateUrl: './blog-home-page-root.component.html' + templateUrl: './blog-home-page-root.component.html', }) export class BlogHomePageRootComponent implements OnDestroy, OnInit { directiveSubscriptions = new Subscription(); @@ -36,12 +36,11 @@ export class BlogHomePageRootComponent implements OnDestroy, OnInit { pageIsShown: boolean = false; constructor( - private accessValidationBackendApiService: - AccessValidationBackendApiService, + private accessValidationBackendApiService: AccessValidationBackendApiService, private loaderService: LoaderService, private pageHeadService: PageHeadService, private translateService: TranslateService, - private userService: UserService, + private userService: UserService ) {} ngOnInit(): void { @@ -54,14 +53,18 @@ export class BlogHomePageRootComponent implements OnDestroy, OnInit { ); this.loaderService.showLoadingScreen('Loading'); - this.userService.canUserEditBlogPosts().then((userCanEditBlogPost) => { + this.userService.canUserEditBlogPosts().then(userCanEditBlogPost => { this.accessValidationBackendApiService .validateAccessToBlogHomePage() - .then((resp) => { - this.pageIsShown = true; - }, (err) => { - this.errorPageIsShown = true; - }).then(() => { + .then( + resp => { + this.pageIsShown = true; + }, + err => { + this.errorPageIsShown = true; + } + ) + .then(() => { this.loaderService.hideLoadingScreen(); }); }); @@ -70,10 +73,11 @@ export class BlogHomePageRootComponent implements OnDestroy, OnInit { setPageTitleAndMetaTags(): void { const blogHomePage = AppConstants.PAGES_REGISTERED_WITH_FRONTEND.BLOG_HOMEPAGE; - const translatedTitle = this.translateService.instant( - blogHomePage.TITLE); + const translatedTitle = this.translateService.instant(blogHomePage.TITLE); this.pageHeadService.updateTitleAndMetaTags( - translatedTitle, blogHomePage.META); + translatedTitle, + blogHomePage.META + ); } ngOnDestroy(): void { diff --git a/core/templates/pages/blog-home-page/blog-home-page-routing.module.ts b/core/templates/pages/blog-home-page/blog-home-page-routing.module.ts index e60434c1e21a..0e012b0a50c5 100644 --- a/core/templates/pages/blog-home-page/blog-home-page-routing.module.ts +++ b/core/templates/pages/blog-home-page/blog-home-page-routing.module.ts @@ -16,24 +16,19 @@ * @fileoverview Routing module for blog home page. */ -import { NgModule } from '@angular/core'; -import { Route, RouterModule } from '@angular/router'; -import { BlogHomePageRootComponent } from './blog-home-page-root.component'; +import {NgModule} from '@angular/core'; +import {Route, RouterModule} from '@angular/router'; +import {BlogHomePageRootComponent} from './blog-home-page-root.component'; const routes: Route[] = [ { path: '', - component: BlogHomePageRootComponent - } + component: BlogHomePageRootComponent, + }, ]; @NgModule({ - imports: [ - RouterModule.forChild(routes) - ], - exports: [ - RouterModule - ] + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], }) - export class BlogHomePageRoutingModule {} diff --git a/core/templates/pages/blog-home-page/blog-home-page.component.spec.ts b/core/templates/pages/blog-home-page/blog-home-page.component.spec.ts index 9042f67cef71..f5067b2627bd 100644 --- a/core/templates/pages/blog-home-page/blog-home-page.component.spec.ts +++ b/core/templates/pages/blog-home-page/blog-home-page.component.spec.ts @@ -16,31 +16,47 @@ * @fileoverview Unit tests for Blog Home Page Component. */ -import { EventEmitter, Pipe } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync} from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { MaterialModule } from 'modules/material.module'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { BlogHomePageComponent } from 'pages/blog-home-page/blog-home-page.component'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { LoaderService } from 'services/loader.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { BlogPostSearchService, UrlSearchQuery } from 'services/blog-search.service'; -import { BlogHomePageBackendApiService, BlogHomePageData, SearchResponseData } from 'domain/blog/blog-homepage-backend-api.service'; -import { UrlService } from 'services/contextual/url.service'; -import { Subject } from 'rxjs/internal/Subject'; -import { BlogCardComponent } from 'pages/blog-dashboard-page/blog-card/blog-card.component'; -import { TagFilterComponent } from './tag-filter/tag-filter.component'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { BlogHomePageConstants } from './blog-home-page.constants'; -import { BlogPostSummary, BlogPostSummaryBackendDict } from 'domain/blog/blog-post-summary.model'; -import { AlertsService } from 'services/alerts.service'; +import {EventEmitter, Pipe} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; +import {MaterialModule} from 'modules/material.module'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {BlogHomePageComponent} from 'pages/blog-home-page/blog-home-page.component'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {LoaderService} from 'services/loader.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import { + BlogPostSearchService, + UrlSearchQuery, +} from 'services/blog-search.service'; +import { + BlogHomePageBackendApiService, + BlogHomePageData, + SearchResponseData, +} from 'domain/blog/blog-homepage-backend-api.service'; +import {UrlService} from 'services/contextual/url.service'; +import {Subject} from 'rxjs/internal/Subject'; +import {BlogCardComponent} from 'pages/blog-dashboard-page/blog-card/blog-card.component'; +import {TagFilterComponent} from './tag-filter/tag-filter.component'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {BlogHomePageConstants} from './blog-home-page.constants'; +import { + BlogPostSummary, + BlogPostSummaryBackendDict, +} from 'domain/blog/blog-post-summary.model'; +import {AlertsService} from 'services/alerts.service'; // This throws "TS2307". We need to // suppress this error because rte-text-components are not strictly typed yet. // @ts-ignore -import { RichTextComponentsModule } from 'rich_text_components/rich-text-components.module'; +import {RichTextComponentsModule} from 'rich_text_components/rich-text-components.module'; @Pipe({name: 'truncate'}) class MockTruncatePipe { @@ -59,8 +75,8 @@ class MockWindowRef { }, }, history: { - pushState(data: object, title: string, url?: string | null) {} - } + pushState(data: object, title: string, url?: string | null) {}, + }, }; } @@ -84,8 +100,7 @@ describe('Blog home page component', () => { let searchResponseData: SearchResponseData; let component: BlogHomePageComponent; let fixture: ComponentFixture; - let mockOnInitialSearchResultsLoaded = ( - new EventEmitter()); + let mockOnInitialSearchResultsLoaded = new EventEmitter(); let blogPostSummary: BlogPostSummaryBackendDict = { id: 'sampleBlogId', @@ -116,18 +131,18 @@ describe('Blog home page component', () => { BlogCardComponent, TagFilterComponent, MockTranslatePipe, - MockTruncatePipe + MockTruncatePipe, ], providers: [ { provide: WindowRef, - useClass: MockWindowRef + useClass: MockWindowRef, }, { provide: WindowDimensionsService, - useClass: MockWindowDimensionsService + useClass: MockWindowDimensionsService, }, - LoaderService + LoaderService, ], }).compileComponents(); })); @@ -138,22 +153,24 @@ describe('Blog home page component', () => { searchService = TestBed.inject(BlogPostSearchService); alertsService = TestBed.inject(AlertsService); blogHomePageBackendApiService = TestBed.inject( - BlogHomePageBackendApiService); + BlogHomePageBackendApiService + ); windowRef = TestBed.inject(WindowRef); windowDimensionsService = TestBed.inject(WindowDimensionsService); urlService = TestBed.inject(UrlService); urlInterpolationService = TestBed.inject(UrlInterpolationService); loaderService = TestBed.inject(LoaderService); - blogPostSummaryObject = BlogPostSummary.createFromBackendDict( - blogPostSummary - ); + blogPostSummaryObject = + BlogPostSummary.createFromBackendDict(blogPostSummary); spyOn(loaderService, 'showLoadingScreen'); spyOn(loaderService, 'hideLoadingScreen'); }); it('should determine if small screen view is active', () => { - const windowWidthSpy = - spyOn(windowDimensionsService, 'getWidth').and.returnValue(766); + const windowWidthSpy = spyOn( + windowDimensionsService, + 'getWidth' + ).and.returnValue(766); expect(component.isSmallScreenViewActive()).toBe(true); windowWidthSpy.and.returnValue(1028); expect(component.isSmallScreenViewActive()).toBe(false); @@ -161,79 +178,93 @@ describe('Blog home page component', () => { it('should handle search query change with language param in URL', () => { spyOn(searchService, 'executeSearchQuery').and.callFake( - ( - searchQuery: string, tags: object, callb: () => void) => { + (searchQuery: string, tags: object, callb: () => void) => { callb(); - }); + } + ); spyOn(searchService, 'getSearchUrlQueryString').and.returnValue( - 'search_query'); + 'search_query' + ); spyOn(windowRef.nativeWindow.history, 'pushState'); windowRef.nativeWindow.location = new URL( - 'http://localhost/blog/search/find?lang=en'); + 'http://localhost/blog/search/find?lang=en' + ); component.onSearchQueryChangeExec(); expect(loaderService.showLoadingScreen).toHaveBeenCalled(); expect(windowRef.nativeWindow.history.pushState).toHaveBeenCalled(); windowRef.nativeWindow.location = new URL( - 'http://localhost/blog/not/search/find?lang=en'); + 'http://localhost/blog/not/search/find?lang=en' + ); component.onSearchQueryChangeExec(); expect(windowRef.nativeWindow.location.href).toEqual( - 'http://localhost/blog/search/find?q=search_query&lang=en'); + 'http://localhost/blog/search/find?q=search_query&lang=en' + ); }); it('should handle search query change without language param in URL', () => { spyOn(searchService, 'executeSearchQuery').and.callFake( - ( - searchQuery: string, tags: object, callb: () => void) => { + (searchQuery: string, tags: object, callb: () => void) => { callb(); - }); + } + ); spyOn(searchService, 'getSearchUrlQueryString').and.returnValue( - 'search_query'); + 'search_query' + ); spyOn(windowRef.nativeWindow.history, 'pushState'); windowRef.nativeWindow.location = new URL( - 'http://localhost/blog/search/find'); + 'http://localhost/blog/search/find' + ); component.onSearchQueryChangeExec(); expect(loaderService.showLoadingScreen).toHaveBeenCalled(); expect(windowRef.nativeWindow.history.pushState).toHaveBeenCalled(); windowRef.nativeWindow.location = new URL( - 'http://localhost/blog/not/search/find'); + 'http://localhost/blog/not/search/find' + ); component.onSearchQueryChangeExec(); expect(windowRef.nativeWindow.location.href).toEqual( - 'http://localhost/blog/search/find?q=search_query'); + 'http://localhost/blog/search/find?q=search_query' + ); }); - it('should display alert when fetching search results fail during search' + - 'query execution', () => { - spyOn(searchService, 'executeSearchQuery').and.callFake( - ( + it( + 'should display alert when fetching search results fail during search' + + 'query execution', + () => { + spyOn(searchService, 'executeSearchQuery').and.callFake( + ( searchQuery: string, tags: object, callb: () => void, errorCallb: (reason: string) => void - ) => { - errorCallb('Internal Server Error'); - }); - spyOn(alertsService, 'addWarning'); + ) => { + errorCallb('Internal Server Error'); + } + ); + spyOn(alertsService, 'addWarning'); - component.onSearchQueryChangeExec(); + component.onSearchQueryChangeExec(); - expect(loaderService.showLoadingScreen).toHaveBeenCalled(); - expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Unable to fetch search results.Error: Internal Server Error'); - }); + expect(loaderService.showLoadingScreen).toHaveBeenCalled(); + expect(alertsService.addWarning).toHaveBeenCalledWith( + 'Unable to fetch search results.Error: Internal Server Error' + ); + } + ); it('should update search fields based on url query for new query', () => { let searchQuery: UrlSearchQuery = { searchQuery: 'search_query', - selectedTags: ['tag1', 'tag2'] + selectedTags: ['tag1', 'tag2'], }; - spyOn(searchService, 'updateSearchFieldsBasedOnUrlQuery') - .and.returnValue(searchQuery); + spyOn(searchService, 'updateSearchFieldsBasedOnUrlQuery').and.returnValue( + searchQuery + ); spyOn(component, 'onSearchQueryChangeExec'); expect(component.searchQuery).toEqual(''); expect(component.selectedTags).toEqual([]); @@ -250,10 +281,12 @@ describe('Blog home page component', () => { let searchQuery: UrlSearchQuery = { searchQuery: 'search_query', - selectedTags: ['tag1', 'tag2'] + selectedTags: ['tag1', 'tag2'], }; - spyOn(searchService, 'updateSearchFieldsBasedOnUrlQuery') - .and.returnValues(searchQuery, searchQuery); + spyOn(searchService, 'updateSearchFieldsBasedOnUrlQuery').and.returnValues( + searchQuery, + searchQuery + ); expect(component.searchQuery).toEqual(''); expect(component.selectedTags).toEqual([]); @@ -277,28 +310,28 @@ describe('Blog home page component', () => { return { subscribe(callb: () => void) { callb(); - } + }, }; - } + }, } as Subject; component.ngOnInit(); expect(component.onSearchQueryChangeExec).toHaveBeenCalled(); expect(component.loadInitialBlogHomePageData).toHaveBeenCalled(); expect(component.searchPageIsActive).toBeFalse(); - expect(component.updateSearchFieldsBasedOnUrlQuery) - .not.toHaveBeenCalled(); + expect(component.updateSearchFieldsBasedOnUrlQuery).not.toHaveBeenCalled(); }); describe(' when loading search results page', () => { beforeEach(() => { - spyOn(urlService, 'getUrlParams').and.returnValue( - {q: 'search_query'} - ); + spyOn(urlService, 'getUrlParams').and.returnValue({q: 'search_query'}); spyOn(urlInterpolationService, 'getStaticImageUrl').and.returnValue( - 'image_url'); - spyOnProperty(searchService, 'onInitialSearchResultsLoaded') - .and.returnValue(mockOnInitialSearchResultsLoaded); + 'image_url' + ); + spyOnProperty( + searchService, + 'onInitialSearchResultsLoaded' + ).and.returnValue(mockOnInitialSearchResultsLoaded); spyOn(component, 'onSearchQueryChangeExec'); spyOn(component, 'updateSearchFieldsBasedOnUrlQuery'); searchResponseData = { @@ -321,8 +354,7 @@ describe('Blog home page component', () => { BlogHomePageConstants.MAX_NUM_CARDS_TO_DISPLAY_ON_BLOG_HOMEPAGE ); expect(component.MAX_NUM_CARDS_TO_DISPLAY_ON_BLOG_HOMEPAGE_SEARCH).toBe( - BlogHomePageConstants - .MAX_NUM_CARDS_TO_DISPLAY_ON_BLOG_SEARCH_RESULTS_PAGE + BlogHomePageConstants.MAX_NUM_CARDS_TO_DISPLAY_ON_BLOG_SEARCH_RESULTS_PAGE ); expect(component.loadInitialBlogHomePageData).not.toHaveBeenCalled(); expect(component.onSearchQueryChangeExec).not.toHaveBeenCalled(); @@ -335,145 +367,165 @@ describe('Blog home page component', () => { expect(urlService.getUrlParams).toHaveBeenCalled(); }); - it('should load data after initial search is performed' + - ' with no matching results', fakeAsync(() => { - spyOn(component, 'loadSearchResultsPageData'); - component.ngOnInit(); - - expect(component.searchPageIsActive).toBeTrue(); - expect(component.updateSearchFieldsBasedOnUrlQuery).toHaveBeenCalled(); - expect(component.noResultsFound).toBeUndefined(); - - mockOnInitialSearchResultsLoaded.emit(searchResponseData); - tick(); - - expect(component.noResultsFound).toBeTrue(); - expect(component.loadSearchResultsPageData).not.toHaveBeenCalled(); - expect(component.searchPageIsActive).toBeTrue(); - })); - - it('should load data after initial search is performed' + - ' with one matching result and no offset', fakeAsync(() => { - searchResponseData.blogPostSummariesList = [blogPostSummaryObject]; + it( + 'should load data after initial search is performed' + + ' with no matching results', + fakeAsync(() => { + spyOn(component, 'loadSearchResultsPageData'); + component.ngOnInit(); - component.ngOnInit(); + expect(component.searchPageIsActive).toBeTrue(); + expect(component.updateSearchFieldsBasedOnUrlQuery).toHaveBeenCalled(); + expect(component.noResultsFound).toBeUndefined(); - expect(loaderService.showLoadingScreen).toHaveBeenCalled(); - expect(component.searchPageIsActive).toBeTrue(); - expect(component.updateSearchFieldsBasedOnUrlQuery).toHaveBeenCalled(); - expect(component.noResultsFound).toBeUndefined(); - expect(component.blogPostSummaries.length).toBe(0); + mockOnInitialSearchResultsLoaded.emit(searchResponseData); + tick(); - mockOnInitialSearchResultsLoaded.emit(searchResponseData); - tick(); + expect(component.noResultsFound).toBeTrue(); + expect(component.loadSearchResultsPageData).not.toHaveBeenCalled(); + expect(component.searchPageIsActive).toBeTrue(); + }) + ); - expect(component.noResultsFound).toBeFalse(); - expect(component.searchPageIsActive).toBeTrue(); - expect(component.blogPostSummaries).toEqual([blogPostSummaryObject]); - expect(component.searchOffset).toEqual(null); - expect(component.totalBlogPosts).toBe(1); - expect(component.lastPostOnPageNum).toBe(1); - expect(component.blogPostSummariesToShow).toEqual( - [blogPostSummaryObject]); - expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); - })); + it( + 'should load data after initial search is performed' + + ' with one matching result and no offset', + fakeAsync(() => { + searchResponseData.blogPostSummariesList = [blogPostSummaryObject]; - it('should load data after initial search is performed' + - ' with one matching result and with search offset', fakeAsync(() => { - searchResponseData.blogPostSummariesList = [blogPostSummaryObject]; - searchResponseData.searchOffset = 1; - component.ngOnInit(); + component.ngOnInit(); - expect(loaderService.showLoadingScreen).toHaveBeenCalled(); - expect(component.updateSearchFieldsBasedOnUrlQuery).toHaveBeenCalled(); - expect(component.noResultsFound).toBeUndefined(); - expect(component.blogPostSummaries.length).toBe(0); + expect(loaderService.showLoadingScreen).toHaveBeenCalled(); + expect(component.searchPageIsActive).toBeTrue(); + expect(component.updateSearchFieldsBasedOnUrlQuery).toHaveBeenCalled(); + expect(component.noResultsFound).toBeUndefined(); + expect(component.blogPostSummaries.length).toBe(0); - mockOnInitialSearchResultsLoaded.emit(searchResponseData); - tick(); + mockOnInitialSearchResultsLoaded.emit(searchResponseData); + tick(); - expect(component.noResultsFound).toBeFalse(); - expect(component.listOfDefaultTags).toEqual( - searchResponseData.listOfDefaultTags); - expect(component.blogPostSummaries).toEqual([blogPostSummaryObject]); - expect(component.searchOffset).toEqual(1); - // As search offset is not null, there are more search result pages to - // load. Therefore for pagination to show that more results are available, - // total number of blog post is one more than the number of blog posts - // loaded as number of pages is automatically calculated using total - // collection size and number of blog posts to show on a page. - expect(component.totalBlogPosts).toBe(2); - expect(component.blogPostSummariesToShow).toEqual( - [blogPostSummaryObject]); - expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); - })); + expect(component.noResultsFound).toBeFalse(); + expect(component.searchPageIsActive).toBeTrue(); + expect(component.blogPostSummaries).toEqual([blogPostSummaryObject]); + expect(component.searchOffset).toEqual(null); + expect(component.totalBlogPosts).toBe(1); + expect(component.lastPostOnPageNum).toBe(1); + expect(component.blogPostSummariesToShow).toEqual([ + blogPostSummaryObject, + ]); + expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); + }) + ); - it('should succesfully load multiple search results pages data', + it( + 'should load data after initial search is performed' + + ' with one matching result and with search offset', fakeAsync(() => { + searchResponseData.blogPostSummariesList = [blogPostSummaryObject]; searchResponseData.searchOffset = 1; - searchResponseData.blogPostSummariesList = [ - blogPostSummaryObject, blogPostSummaryObject]; - spyOn(alertsService, 'addWarning'); - spyOn(searchService, 'loadMoreData').and.callFake( - ( - callb: (SearchResponseData: SearchResponseData) => void, - ) => { - callb(searchResponseData); - }); component.ngOnInit(); - component.MAX_NUM_CARDS_TO_DISPLAY_ON_BLOG_HOMEPAGE_SEARCH = 2; - // Loading page 1. + expect(loaderService.showLoadingScreen).toHaveBeenCalled(); + expect(component.updateSearchFieldsBasedOnUrlQuery).toHaveBeenCalled(); + expect(component.noResultsFound).toBeUndefined(); + expect(component.blogPostSummaries.length).toBe(0); + mockOnInitialSearchResultsLoaded.emit(searchResponseData); tick(); - expect(component.blogPostSummaries.length).toBe(2); + expect(component.noResultsFound).toBeFalse(); + expect(component.listOfDefaultTags).toEqual( + searchResponseData.listOfDefaultTags + ); + expect(component.blogPostSummaries).toEqual([blogPostSummaryObject]); expect(component.searchOffset).toEqual(1); - expect(component.blogPostSummariesToShow).toEqual( - [blogPostSummaryObject, blogPostSummaryObject]); - expect(component.blogPostSummariesToShow.length).toBe(2); - expect(component.lastPostOnPageNum).toBe(2); - - // Changing to page 2. - component.page = 2; - component.onPageChange(); - tick(); + // As search offset is not null, there are more search result pages to + // load. Therefore for pagination to show that more results are available, + // total number of blog post is one more than the number of blog posts + // loaded as number of pages is automatically calculated using total + // collection size and number of blog posts to show on a page. + expect(component.totalBlogPosts).toBe(2); + expect(component.blogPostSummariesToShow).toEqual([ + blogPostSummaryObject, + ]); + expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); + }) + ); - expect(component.firstPostOnPageNum).toBe(3); - expect(component.blogPostSummaries.length).toBe(4); - expect(component.blogPostSummariesToShow.length).toBe(2); - expect(component.lastPostOnPageNum).toBe(4); + it('should succesfully load multiple search results pages data', fakeAsync(() => { + searchResponseData.searchOffset = 1; + searchResponseData.blogPostSummariesList = [ + blogPostSummaryObject, + blogPostSummaryObject, + ]; + spyOn(alertsService, 'addWarning'); + spyOn(searchService, 'loadMoreData').and.callFake( + (callb: (SearchResponseData: SearchResponseData) => void) => { + callb(searchResponseData); + } + ); + component.ngOnInit(); + component.MAX_NUM_CARDS_TO_DISPLAY_ON_BLOG_HOMEPAGE_SEARCH = 2; - // Changing back to page 1. - component.page = 1; - component.onPageChange(); + // Loading page 1. + mockOnInitialSearchResultsLoaded.emit(searchResponseData); + tick(); - expect(component.firstPostOnPageNum).toBe(1); - expect(component.blogPostSummaries.length).toBe(4); - expect(component.blogPostSummariesToShow.length).toBe(2); - expect(component.lastPostOnPageNum).toBe(2); + expect(component.blogPostSummaries.length).toBe(2); + expect(component.searchOffset).toEqual(1); + expect(component.blogPostSummariesToShow).toEqual([ + blogPostSummaryObject, + blogPostSummaryObject, + ]); + expect(component.blogPostSummariesToShow.length).toBe(2); + expect(component.lastPostOnPageNum).toBe(2); - expect(alertsService.addWarning).not.toHaveBeenCalled(); - })); + // Changing to page 2. + component.page = 2; + component.onPageChange(); + tick(); - it('should raise warning for trying to load more search after end of' + - ' search results has been reached.', () => { - component.ngOnInit(); - component.blogPostSummaries = [blogPostSummaryObject]; - component.firstPostOnPageNum = 3; - spyOn(alertsService, 'addWarning'); - spyOn(searchService, 'loadMoreData').and.callFake( - ( + expect(component.firstPostOnPageNum).toBe(3); + expect(component.blogPostSummaries.length).toBe(4); + expect(component.blogPostSummariesToShow.length).toBe(2); + expect(component.lastPostOnPageNum).toBe(4); + + // Changing back to page 1. + component.page = 1; + component.onPageChange(); + + expect(component.firstPostOnPageNum).toBe(1); + expect(component.blogPostSummaries.length).toBe(4); + expect(component.blogPostSummariesToShow.length).toBe(2); + expect(component.lastPostOnPageNum).toBe(2); + + expect(alertsService.addWarning).not.toHaveBeenCalled(); + })); + + it( + 'should raise warning for trying to load more search after end of' + + ' search results has been reached.', + () => { + component.ngOnInit(); + component.blogPostSummaries = [blogPostSummaryObject]; + component.firstPostOnPageNum = 3; + spyOn(alertsService, 'addWarning'); + spyOn(searchService, 'loadMoreData').and.callFake( + ( callb: (SearchResponseData: SearchResponseData) => void, - failCallb: (arg0: boolean) => void) => { - failCallb(true); - }); + failCallb: (arg0: boolean) => void + ) => { + failCallb(true); + } + ); - component.loadPage(); + component.loadPage(); - expect(alertsService.addWarning).toHaveBeenCalledWith( - 'No more search resutls found. End of search results.'); - }); + expect(alertsService.addWarning).toHaveBeenCalledWith( + 'No more search resutls found. End of search results.' + ); + } + ); }); it('should execute search query when search query changes', () => { @@ -485,16 +537,15 @@ describe('Blog home page component', () => { return { subscribe(callb: () => void) { callb(); - } + }, }; - } + }, } as Subject; component.ngOnInit(); expect(component.onSearchQueryChangeExec).toHaveBeenCalled(); }); - describe('when loading blog home page', () => { beforeEach(() => { spyOn(component, 'onSearchQueryChangeExec'); @@ -513,9 +564,11 @@ describe('Blog home page component', () => { spyOn(component, 'loadInitialBlogHomePageData'); spyOn(searchService.onSearchBarLoaded, 'emit'); spyOn(urlInterpolationService, 'getStaticImageUrl').and.returnValue( - 'image_url'); - spyOn(searchService, 'onInitialSearchResultsLoaded') - .and.returnValue(mockOnInitialSearchResultsLoaded); + 'image_url' + ); + spyOn(searchService, 'onInitialSearchResultsLoaded').and.returnValue( + mockOnInitialSearchResultsLoaded + ); spyOn(searchService.onInitialSearchResultsLoaded, 'subscribe'); component.ngOnInit(); @@ -526,12 +579,12 @@ describe('Blog home page component', () => { BlogHomePageConstants.MAX_NUM_CARDS_TO_DISPLAY_ON_BLOG_HOMEPAGE ); expect(component.MAX_NUM_CARDS_TO_DISPLAY_ON_BLOG_HOMEPAGE_SEARCH).toBe( - BlogHomePageConstants - .MAX_NUM_CARDS_TO_DISPLAY_ON_BLOG_SEARCH_RESULTS_PAGE + BlogHomePageConstants.MAX_NUM_CARDS_TO_DISPLAY_ON_BLOG_SEARCH_RESULTS_PAGE ); expect(component.loadInitialBlogHomePageData).toHaveBeenCalled(); - expect(component.updateSearchFieldsBasedOnUrlQuery) - .not.toHaveBeenCalled(); + expect( + component.updateSearchFieldsBasedOnUrlQuery + ).not.toHaveBeenCalled(); expect(searchService.onSearchBarLoaded.emit).toHaveBeenCalled(); expect( searchService.onInitialSearchResultsLoaded.subscribe @@ -540,54 +593,60 @@ describe('Blog home page component', () => { expect(component.onSearchQueryChangeExec).not.toHaveBeenCalled(); }); - it('should load blog home page data with no published blog post summary', - fakeAsync(() => { - spyOn(blogHomePageBackendApiService, 'fetchBlogHomePageDataAsync') - .and.returnValue(Promise.resolve(blogHomePageDataObject)); - expect(component.noResultsFound).toBeUndefined(); + it('should load blog home page data with no published blog post summary', fakeAsync(() => { + spyOn( + blogHomePageBackendApiService, + 'fetchBlogHomePageDataAsync' + ).and.returnValue(Promise.resolve(blogHomePageDataObject)); + expect(component.noResultsFound).toBeUndefined(); - component.loadInitialBlogHomePageData(); + component.loadInitialBlogHomePageData(); - expect(blogHomePageBackendApiService.fetchBlogHomePageDataAsync) - .toHaveBeenCalledWith('0'); + expect( + blogHomePageBackendApiService.fetchBlogHomePageDataAsync + ).toHaveBeenCalledWith('0'); - tick(); - expect(component.noResultsFound).toBeTrue(); + tick(); + expect(component.noResultsFound).toBeTrue(); - expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); - })); + expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); + })); - it('should load blog home page data with 1 published blog post summary', - fakeAsync(() => { - blogHomePageDataObject.numOfPublishedBlogPosts = 1; - blogHomePageDataObject.blogPostSummaryDicts = [blogPostSummaryObject]; - spyOn(blogHomePageBackendApiService, 'fetchBlogHomePageDataAsync') - .and.returnValue(Promise.resolve(blogHomePageDataObject)); + it('should load blog home page data with 1 published blog post summary', fakeAsync(() => { + blogHomePageDataObject.numOfPublishedBlogPosts = 1; + blogHomePageDataObject.blogPostSummaryDicts = [blogPostSummaryObject]; + spyOn( + blogHomePageBackendApiService, + 'fetchBlogHomePageDataAsync' + ).and.returnValue(Promise.resolve(blogHomePageDataObject)); - component.loadInitialBlogHomePageData(); + component.loadInitialBlogHomePageData(); - expect(blogHomePageBackendApiService.fetchBlogHomePageDataAsync) - .toHaveBeenCalledWith('0'); + expect( + blogHomePageBackendApiService.fetchBlogHomePageDataAsync + ).toHaveBeenCalledWith('0'); - tick(); - expect(component.totalBlogPosts).toBe(1); - expect(component.noResultsFound).toBeFalse(); - expect(component.blogPostSummaries).toEqual([blogPostSummaryObject]); - expect(component.blogPostSummariesToShow).toEqual( - [blogPostSummaryObject]); - expect(component.lastPostOnPageNum).toBe(1); + tick(); + expect(component.totalBlogPosts).toBe(1); + expect(component.noResultsFound).toBeFalse(); + expect(component.blogPostSummaries).toEqual([blogPostSummaryObject]); + expect(component.blogPostSummariesToShow).toEqual([ + blogPostSummaryObject, + ]); + expect(component.lastPostOnPageNum).toBe(1); - expect(component.listOfDefaultTags).toEqual( - blogHomePageDataObject.listOfDefaultTags); - expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); - })); + expect(component.listOfDefaultTags).toEqual( + blogHomePageDataObject.listOfDefaultTags + ); + expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); + })); - it ('should search', () => { + it('should search', () => { component.searchButtonIsActive = true; const search = { target: { - value: 'search' - } + value: 'search', + }, }; component.searchToBeExec(search); @@ -597,47 +656,53 @@ describe('Blog home page component', () => { expect(component.searchQueryChanged.next).toHaveBeenCalled(); }); - it('should succesfully load multiple blog home pages data', - fakeAsync(() => { - component.MAX_NUM_CARDS_TO_DISPLAY_ON_BLOG_HOMEPAGE = 1; - blogHomePageDataObject.numOfPublishedBlogPosts = 3; - blogHomePageDataObject.blogPostSummaryDicts = [blogPostSummaryObject]; - spyOn(alertsService, 'addWarning'); - spyOn(blogHomePageBackendApiService, 'fetchBlogHomePageDataAsync') - .and.returnValue(Promise.resolve(blogHomePageDataObject)); + it('should succesfully load multiple blog home pages data', fakeAsync(() => { + component.MAX_NUM_CARDS_TO_DISPLAY_ON_BLOG_HOMEPAGE = 1; + blogHomePageDataObject.numOfPublishedBlogPosts = 3; + blogHomePageDataObject.blogPostSummaryDicts = [blogPostSummaryObject]; + spyOn(alertsService, 'addWarning'); + spyOn( + blogHomePageBackendApiService, + 'fetchBlogHomePageDataAsync' + ).and.returnValue(Promise.resolve(blogHomePageDataObject)); - component.loadInitialBlogHomePageData(); - tick(); + component.loadInitialBlogHomePageData(); + tick(); - expect(component.totalBlogPosts).toBe(3); - expect(component.noResultsFound).toBeFalse(); - expect(component.blogPostSummaries).toEqual([blogPostSummaryObject]); - expect(component.blogPostSummariesToShow).toEqual( - [blogPostSummaryObject]); - expect(component.lastPostOnPageNum).toBe(1); + expect(component.totalBlogPosts).toBe(3); + expect(component.noResultsFound).toBeFalse(); + expect(component.blogPostSummaries).toEqual([blogPostSummaryObject]); + expect(component.blogPostSummariesToShow).toEqual([ + blogPostSummaryObject, + ]); + expect(component.lastPostOnPageNum).toBe(1); - component.page = 2; - component.loadMoreBlogPostSummaries(1); - tick(); + component.page = 2; + component.loadMoreBlogPostSummaries(1); + tick(); - expect(blogHomePageBackendApiService.fetchBlogHomePageDataAsync) - .toHaveBeenCalledWith('1'); - expect(component.totalBlogPosts).toBe(3); - expect(component.blogPostSummaries).toEqual( - [blogPostSummaryObject, blogPostSummaryObject]); - expect(component.blogPostSummariesToShow).toEqual( - [blogPostSummaryObject]); - expect(component.lastPostOnPageNum).toBe(2); + expect( + blogHomePageBackendApiService.fetchBlogHomePageDataAsync + ).toHaveBeenCalledWith('1'); + expect(component.totalBlogPosts).toBe(3); + expect(component.blogPostSummaries).toEqual([ + blogPostSummaryObject, + blogPostSummaryObject, + ]); + expect(component.blogPostSummariesToShow).toEqual([ + blogPostSummaryObject, + ]); + expect(component.lastPostOnPageNum).toBe(2); - expect(alertsService.addWarning).not.toHaveBeenCalled(); - })); + expect(alertsService.addWarning).not.toHaveBeenCalled(); + })); it('should load data for page on changing page', () => { spyOn(component, 'loadMoreBlogPostSummaries'); component.totalBlogPosts = 5; component.blogPostSummaries = [ blogPostSummaryObject, - blogPostSummaryObject + blogPostSummaryObject, ]; component.ngOnInit(); component.MAX_NUM_CARDS_TO_DISPLAY_ON_BLOG_HOMEPAGE = 2; @@ -657,7 +722,7 @@ describe('Blog home page component', () => { // Adding blog post summaries for page 2. component.blogPostSummaries = component.blogPostSummaries.concat([ blogPostSummaryObject, - blogPostSummaryObject + blogPostSummaryObject, ]); component.showBlogPostCardsLoadingScreen = false; @@ -671,7 +736,7 @@ describe('Blog home page component', () => { expect(component.lastPostOnPageNum).toBe(5); // Adding blog post summaries for page 3. component.blogPostSummaries = component.blogPostSummaries.concat([ - blogPostSummaryObject + blogPostSummaryObject, ]); component.showBlogPostCardsLoadingScreen = false; @@ -687,45 +752,59 @@ describe('Blog home page component', () => { expect(component.blogPostSummariesToShow.length).toBe(2); }); - it('should use reject handler if fetching blog home page data fails', - fakeAsync(() => { - spyOn(alertsService, 'addWarning'); - spyOn(blogHomePageBackendApiService, 'fetchBlogHomePageDataAsync') - .and.returnValue(Promise.reject({ - error: {error: 'Backend error'}, - status: 500 - })); - - component.loadInitialBlogHomePageData(); - - expect(blogHomePageBackendApiService.fetchBlogHomePageDataAsync) - .toHaveBeenCalledWith('0'); - - tick(); - - expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Failed to get blog home page data.Error: Backend error'); - })); - - it('should use reject handler if fetching data for loading more published' + - 'blog post fails', fakeAsync(() => { + it('should use reject handler if fetching blog home page data fails', fakeAsync(() => { spyOn(alertsService, 'addWarning'); - spyOn(blogHomePageBackendApiService, 'fetchBlogHomePageDataAsync') - .and.returnValue(Promise.reject({ + spyOn( + blogHomePageBackendApiService, + 'fetchBlogHomePageDataAsync' + ).and.returnValue( + Promise.reject({ error: {error: 'Backend error'}, - status: 500 - })); + status: 500, + }) + ); - component.loadMoreBlogPostSummaries(1); + component.loadInitialBlogHomePageData(); - expect(blogHomePageBackendApiService.fetchBlogHomePageDataAsync) - .toHaveBeenCalledWith('1'); + expect( + blogHomePageBackendApiService.fetchBlogHomePageDataAsync + ).toHaveBeenCalledWith('0'); tick(); expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Failed to get blog home page data.Error: Backend error'); + 'Failed to get blog home page data.Error: Backend error' + ); })); + + it( + 'should use reject handler if fetching data for loading more published' + + 'blog post fails', + fakeAsync(() => { + spyOn(alertsService, 'addWarning'); + spyOn( + blogHomePageBackendApiService, + 'fetchBlogHomePageDataAsync' + ).and.returnValue( + Promise.reject({ + error: {error: 'Backend error'}, + status: 500, + }) + ); + + component.loadMoreBlogPostSummaries(1); + + expect( + blogHomePageBackendApiService.fetchBlogHomePageDataAsync + ).toHaveBeenCalledWith('1'); + + tick(); + + expect(alertsService.addWarning).toHaveBeenCalledWith( + 'Failed to get blog home page data.Error: Backend error' + ); + }) + ); }); it('should tell searching status', () => { @@ -734,8 +813,9 @@ describe('Blog home page component', () => { }); it('should get static asset image url', () => { - spyOn(urlInterpolationService, 'getStaticAssetUrl') - .and.returnValue('image_url'); + spyOn(urlInterpolationService, 'getStaticAssetUrl').and.returnValue( + 'image_url' + ); expect(component.getStaticImageUrl('url')).toBe('image_url'); }); diff --git a/core/templates/pages/blog-home-page/blog-home-page.component.ts b/core/templates/pages/blog-home-page/blog-home-page.component.ts index e175701bb2a4..8d838fef9fcc 100755 --- a/core/templates/pages/blog-home-page/blog-home-page.component.ts +++ b/core/templates/pages/blog-home-page/blog-home-page.component.ts @@ -16,28 +16,34 @@ * @fileoverview Data and component for the blog home page. */ -import { Component, OnInit } from '@angular/core'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { Subject } from 'rxjs'; -import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; -import { AlertsService } from 'services/alerts.service'; -import { Subscription } from 'rxjs'; -import { AppConstants } from 'app.constants'; -import { UrlSearchQuery, BlogPostSearchService } from 'services/blog-search.service'; -import { BlogHomePageData, BlogHomePageBackendApiService } from 'domain/blog/blog-homepage-backend-api.service'; -import { SearchResponseData } from 'domain/blog/blog-homepage-backend-api.service'; -import { BlogPostSummary } from 'domain/blog/blog-post-summary.model'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { LoaderService } from 'services/loader.service'; -import { UrlService } from 'services/contextual/url.service'; -import { BlogHomePageConstants } from './blog-home-page.constants'; +import {Component, OnInit} from '@angular/core'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {Subject} from 'rxjs'; +import {debounceTime, distinctUntilChanged} from 'rxjs/operators'; +import {AlertsService} from 'services/alerts.service'; +import {Subscription} from 'rxjs'; +import {AppConstants} from 'app.constants'; +import { + UrlSearchQuery, + BlogPostSearchService, +} from 'services/blog-search.service'; +import { + BlogHomePageData, + BlogHomePageBackendApiService, +} from 'domain/blog/blog-homepage-backend-api.service'; +import {SearchResponseData} from 'domain/blog/blog-homepage-backend-api.service'; +import {BlogPostSummary} from 'domain/blog/blog-post-summary.model'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {LoaderService} from 'services/loader.service'; +import {UrlService} from 'services/contextual/url.service'; +import {BlogHomePageConstants} from './blog-home-page.constants'; import './blog-home-page.component.css'; @Component({ selector: 'oppia-blog-home-page', - templateUrl: './blog-home-page.component.html' + templateUrl: './blog-home-page.component.html', }) export class BlogHomePageComponent implements OnInit { // These properties are initialized using Angular lifecycle hooks @@ -75,25 +81,26 @@ export class BlogHomePageComponent implements OnInit { private blogHomePageBackendApiService: BlogHomePageBackendApiService, private alertsService: AlertsService, private loaderService: LoaderService, - private urlService: UrlService, + private urlService: UrlService ) {} ngOnInit(): void { this.loaderService.showLoadingScreen('Loading'); this.oppiaAvatarImgUrl = this.urlInterpolationService.getStaticImageUrl( - '/avatar/oppia_avatar_100px.svg'); - this.MAX_NUM_CARDS_TO_DISPLAY_ON_BLOG_HOMEPAGE = ( - BlogHomePageConstants.MAX_NUM_CARDS_TO_DISPLAY_ON_BLOG_HOMEPAGE); - this.MAX_NUM_CARDS_TO_DISPLAY_ON_BLOG_HOMEPAGE_SEARCH = ( - BlogHomePageConstants.MAX_NUM_CARDS_TO_DISPLAY_ON_BLOG_SEARCH_RESULTS_PAGE + '/avatar/oppia_avatar_100px.svg' ); + this.MAX_NUM_CARDS_TO_DISPLAY_ON_BLOG_HOMEPAGE = + BlogHomePageConstants.MAX_NUM_CARDS_TO_DISPLAY_ON_BLOG_HOMEPAGE; + this.MAX_NUM_CARDS_TO_DISPLAY_ON_BLOG_HOMEPAGE_SEARCH = + BlogHomePageConstants.MAX_NUM_CARDS_TO_DISPLAY_ON_BLOG_SEARCH_RESULTS_PAGE; if (this.urlService.getUrlParams().hasOwnProperty('q')) { this.searchPageIsActive = true; this.updateSearchFieldsBasedOnUrlQuery(); } else { this.loadInitialBlogHomePageData(); } - this.searchQueryChanged.pipe(debounceTime(1000), distinctUntilChanged()) + this.searchQueryChanged + .pipe(debounceTime(1000), distinctUntilChanged()) .subscribe(model => { this.searchQuery = model; this.onSearchQueryChangeExec(); @@ -130,7 +137,8 @@ export class BlogHomePageComponent implements OnInit { loadSearchResultsPageData(data: SearchResponseData): void { this.blogPostSummaries = this.blogPostSummaries.concat( - data.blogPostSummariesList); + data.blogPostSummariesList + ); this.searchOffset = data.searchOffset; if (this.searchOffset) { // As search offset is not null, there are more search result pages to @@ -152,50 +160,62 @@ export class BlogHomePageComponent implements OnInit { } loadInitialBlogHomePageData(): void { - this.blogHomePageBackendApiService.fetchBlogHomePageDataAsync( - '0').then((data: BlogHomePageData) => { - if (data.numOfPublishedBlogPosts) { - this.totalBlogPosts = data.numOfPublishedBlogPosts; - this.noResultsFound = false; - this.blogPostSummaries = data.blogPostSummaryDicts; - this.blogPostSummariesToShow = this.blogPostSummaries; - this.calculateLastPostOnPageNum( - this.page, - this.MAX_NUM_CARDS_TO_DISPLAY_ON_BLOG_HOMEPAGE - ); - } else { - this.noResultsFound = true; - } - this.listOfDefaultTags = data.listOfDefaultTags; - this.loaderService.hideLoadingScreen(); - }, (errorResponse) => { - if (AppConstants.FATAL_ERROR_CODES.indexOf(errorResponse.status) !== -1) { - this.alertsService.addWarning( - 'Failed to get blog home page data.Error: ' + - `${errorResponse.error.error}`); + this.blogHomePageBackendApiService.fetchBlogHomePageDataAsync('0').then( + (data: BlogHomePageData) => { + if (data.numOfPublishedBlogPosts) { + this.totalBlogPosts = data.numOfPublishedBlogPosts; + this.noResultsFound = false; + this.blogPostSummaries = data.blogPostSummaryDicts; + this.blogPostSummariesToShow = this.blogPostSummaries; + this.calculateLastPostOnPageNum( + this.page, + this.MAX_NUM_CARDS_TO_DISPLAY_ON_BLOG_HOMEPAGE + ); + } else { + this.noResultsFound = true; + } + this.listOfDefaultTags = data.listOfDefaultTags; + this.loaderService.hideLoadingScreen(); + }, + errorResponse => { + if ( + AppConstants.FATAL_ERROR_CODES.indexOf(errorResponse.status) !== -1 + ) { + this.alertsService.addWarning( + 'Failed to get blog home page data.Error: ' + + `${errorResponse.error.error}` + ); + } } - }); + ); } loadMoreBlogPostSummaries(offset: number): void { - this.blogHomePageBackendApiService.fetchBlogHomePageDataAsync( - String(offset) - ).then((data: BlogHomePageData) => { - this.blogPostSummaries = this.blogPostSummaries.concat( - data.blogPostSummaryDicts); - this.selectBlogPostSummariesToShow(); - this.calculateLastPostOnPageNum( - this.page, - this.MAX_NUM_CARDS_TO_DISPLAY_ON_BLOG_HOMEPAGE + this.blogHomePageBackendApiService + .fetchBlogHomePageDataAsync(String(offset)) + .then( + (data: BlogHomePageData) => { + this.blogPostSummaries = this.blogPostSummaries.concat( + data.blogPostSummaryDicts + ); + this.selectBlogPostSummariesToShow(); + this.calculateLastPostOnPageNum( + this.page, + this.MAX_NUM_CARDS_TO_DISPLAY_ON_BLOG_HOMEPAGE + ); + this.showBlogPostCardsLoadingScreen = false; + }, + errorResponse => { + if ( + AppConstants.FATAL_ERROR_CODES.indexOf(errorResponse.status) !== -1 + ) { + this.alertsService.addWarning( + 'Failed to get blog home page data.Error:' + + ` ${errorResponse.error.error}` + ); + } + } ); - this.showBlogPostCardsLoadingScreen = false; - }, (errorResponse) => { - if (AppConstants.FATAL_ERROR_CODES.indexOf(errorResponse.status) !== -1) { - this.alertsService.addWarning( - 'Failed to get blog home page data.Error:' + - ` ${errorResponse.error.error}`); - } - }); } loadPage(): void { @@ -204,12 +224,16 @@ export class BlogHomePageComponent implements OnInit { if (!this.searchPageIsActive) { this.loadMoreBlogPostSummaries(this.firstPostOnPageNum - 1); } else { - this.blogPostSearchService.loadMoreData((data) => { - this.loadSearchResultsPageData(data); - }, (_isCurrentlyFetchingResults) => { - this.alertsService.addWarning( - 'No more search resutls found. End of search results.'); - }); + this.blogPostSearchService.loadMoreData( + data => { + this.loadSearchResultsPageData(data); + }, + _isCurrentlyFetchingResults => { + this.alertsService.addWarning( + 'No more search resutls found. End of search results.' + ); + } + ); } } else { this.selectBlogPostSummariesToShow(); @@ -242,11 +266,13 @@ export class BlogHomePageComponent implements OnInit { selectBlogPostSummariesToShow(): void { this.blogPostSummariesToShow = this.blogPostSummaries.slice( - (this.firstPostOnPageNum - 1), this.lastPostOnPageNum); + this.firstPostOnPageNum - 1, + this.lastPostOnPageNum + ); } calculateFirstPostOnPageNum(pageNum: number, pageSize: number): void { - this.firstPostOnPageNum = ((pageNum - 1) * pageSize) + 1; + this.firstPostOnPageNum = (pageNum - 1) * pageSize + 1; } calculateLastPostOnPageNum(pageNum: number, pageSize: number): void { @@ -266,18 +292,19 @@ export class BlogHomePageComponent implements OnInit { onSearchQueryChangeExec(): void { this.loaderService.showLoadingScreen('Loading'); this.blogPostSearchService.executeSearchQuery( - this.searchQuery, this.selectedTags, () => { - let searchUrlQueryString = ( + this.searchQuery, + this.selectedTags, + () => { + let searchUrlQueryString = this.blogPostSearchService.getSearchUrlQueryString( - this.searchQuery, this.selectedTags - ) - ); + this.searchQuery, + this.selectedTags + ); let url = new URL(this.windowRef.nativeWindow.location.toString()); let siteLangCode: string | null = url.searchParams.get('lang'); url.search = '?q=' + searchUrlQueryString; if ( - this.windowRef.nativeWindow.location.pathname === ( - '/blog/search/find') + this.windowRef.nativeWindow.location.pathname === '/blog/search/find' ) { if (siteLangCode) { url.searchParams.append('lang', siteLangCode); @@ -290,10 +317,13 @@ export class BlogHomePageComponent implements OnInit { } this.windowRef.nativeWindow.location.href = url.toString(); } - }, (errorResponse) => { + }, + errorResponse => { this.alertsService.addWarning( - `Unable to fetch search results.Error: ${errorResponse}`); - }); + `Unable to fetch search results.Error: ${errorResponse}` + ); + } + ); } isSmallScreenViewActive(): boolean { @@ -302,14 +332,14 @@ export class BlogHomePageComponent implements OnInit { updateSearchFieldsBasedOnUrlQuery(): void { let newSearchQuery: UrlSearchQuery; - newSearchQuery = ( + newSearchQuery = this.blogPostSearchService.updateSearchFieldsBasedOnUrlQuery( - this.windowRef.nativeWindow.location.search) - ); + this.windowRef.nativeWindow.location.search + ); if ( - this.searchQuery !== newSearchQuery.searchQuery || ( - this.selectedTags !== newSearchQuery.selectedTags) + this.searchQuery !== newSearchQuery.searchQuery || + this.selectedTags !== newSearchQuery.selectedTags ) { this.searchQuery = newSearchQuery.searchQuery; this.selectedTags = newSearchQuery.selectedTags; diff --git a/core/templates/pages/blog-home-page/blog-home-page.module.ts b/core/templates/pages/blog-home-page/blog-home-page.module.ts index c0d0289061fa..545df376006b 100644 --- a/core/templates/pages/blog-home-page/blog-home-page.module.ts +++ b/core/templates/pages/blog-home-page/blog-home-page.module.ts @@ -16,22 +16,22 @@ * @fileoverview Module for the blog home page. */ -import { NgModule } from '@angular/core'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { InfiniteScrollModule } from 'ngx-infinite-scroll'; -import { FormsModule } from '@angular/forms'; -import { ReactiveFormsModule } from '@angular/forms'; -import { TranslateModule } from '@ngx-translate/core'; -import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import {NgModule} from '@angular/core'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {InfiniteScrollModule} from 'ngx-infinite-scroll'; +import {FormsModule} from '@angular/forms'; +import {ReactiveFormsModule} from '@angular/forms'; +import {TranslateModule} from '@ngx-translate/core'; +import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; -import { BlogHomePageRootComponent } from './blog-home-page-root.component'; -import { BlogHomePageComponent } from './blog-home-page.component'; -import { CommonModule } from '@angular/common'; -import { BlogHomePageRoutingModule } from './blog-home-page-routing.module'; -import { TagFilterComponent } from './tag-filter/tag-filter.component'; -import { StringUtilityPipesModule } from 'filters/string-utility-filters/string-utility-pipes.module'; -import { Error404PageModule } from 'pages/error-pages/error-404/error-404-page.module'; -import { SharedBlogComponentsModule } from 'pages/blog-dashboard-page/shared-blog-components.module'; +import {BlogHomePageRootComponent} from './blog-home-page-root.component'; +import {BlogHomePageComponent} from './blog-home-page.component'; +import {CommonModule} from '@angular/common'; +import {BlogHomePageRoutingModule} from './blog-home-page-routing.module'; +import {TagFilterComponent} from './tag-filter/tag-filter.component'; +import {StringUtilityPipesModule} from 'filters/string-utility-filters/string-utility-pipes.module'; +import {Error404PageModule} from 'pages/error-pages/error-404/error-404-page.module'; +import {SharedBlogComponentsModule} from 'pages/blog-dashboard-page/shared-blog-components.module'; @NgModule({ imports: [ @@ -45,17 +45,17 @@ import { SharedBlogComponentsModule } from 'pages/blog-dashboard-page/shared-blo TranslateModule, ReactiveFormsModule, SharedBlogComponentsModule, - Error404PageModule + Error404PageModule, ], declarations: [ BlogHomePageComponent, BlogHomePageRootComponent, - TagFilterComponent + TagFilterComponent, ], entryComponents: [ BlogHomePageComponent, BlogHomePageRootComponent, - TagFilterComponent - ] + TagFilterComponent, + ], }) export class BlogHomePageModule {} diff --git a/core/templates/pages/blog-home-page/tag-filter/tag-filter.component.spec.ts b/core/templates/pages/blog-home-page/tag-filter/tag-filter.component.spec.ts index b2e6e77e3a2c..99b50e18436c 100644 --- a/core/templates/pages/blog-home-page/tag-filter/tag-filter.component.spec.ts +++ b/core/templates/pages/blog-home-page/tag-filter/tag-filter.component.spec.ts @@ -11,17 +11,23 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -import { ElementRef } from '@angular/core'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync} from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { MaterialModule } from 'modules/material.module'; -import { TagFilterComponent } from './tag-filter.component'; -import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; -import { BlogPostSearchService } from 'services/blog-search.service'; -import { BlogHomePageConstants } from '../blog-home-page.constants'; +import {ElementRef} from '@angular/core'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {MaterialModule} from 'modules/material.module'; +import {TagFilterComponent} from './tag-filter.component'; +import {MatAutocompleteTrigger} from '@angular/material/autocomplete'; +import {BlogPostSearchService} from 'services/blog-search.service'; +import {BlogHomePageConstants} from '../blog-home-page.constants'; /** * @fileoverview Unit tests for Tag Filter Component. */ @@ -37,15 +43,10 @@ describe('Tag Filter component', () => { FormsModule, ReactiveFormsModule, MaterialModule, - BrowserAnimationsModule + BrowserAnimationsModule, ], - declarations: [ - TagFilterComponent, - MockTranslatePipe, - ], - providers: [ - BlogPostSearchService, - ] + declarations: [TagFilterComponent, MockTranslatePipe], + providers: [BlogPostSearchService], }).compileComponents(); })); @@ -55,7 +56,7 @@ describe('Tag Filter component', () => { component.autoTrigger = { closePanel() { return; - } + }, } as MatAutocompleteTrigger; }); @@ -63,38 +64,36 @@ describe('Tag Filter component', () => { expect(component).toBeDefined(); }); - it('should initialize tag filter and select a tag and exec search', fakeAsync( - () => { - spyOn(component.selectionsChange, 'emit'); - component.selectedTags = ['tag1', 'tag2']; - component.listOfDefaultTags = ['tag1', 'tag2', 'tag3', 'tag4']; + it('should initialize tag filter and select a tag and exec search', fakeAsync(() => { + spyOn(component.selectionsChange, 'emit'); + component.selectedTags = ['tag1', 'tag2']; + component.listOfDefaultTags = ['tag1', 'tag2', 'tag3', 'tag4']; - fixture.detectChanges(); - component.ngOnInit(); + fixture.detectChanges(); + component.ngOnInit(); - expect(component.filteredTags).toBeDefined(); - expect(component.tagFilter).toBeDefined(); - expect(component.searchDropDownTags).toEqual(['tag3', 'tag4']); + expect(component.filteredTags).toBeDefined(); + expect(component.tagFilter).toBeDefined(); + expect(component.searchDropDownTags).toEqual(['tag3', 'tag4']); - component.tagFilterInput = { - nativeElement: { - value: '' - } - } as ElementRef; - component.selectTag(({ option: { viewValue: 'tag3'}})); - // Search with applied tags will be executed only when no change in tag - // filter is done for 1500ms. We add 1ms extra to avoid flaking of test. - tick(BlogHomePageConstants.DEBOUNCE_TIME + 1); + component.tagFilterInput = { + nativeElement: { + value: '', + }, + } as ElementRef; + component.selectTag({option: {viewValue: 'tag3'}}); + // Search with applied tags will be executed only when no change in tag + // filter is done for 1500ms. We add 1ms extra to avoid flaking of test. + tick(BlogHomePageConstants.DEBOUNCE_TIME + 1); - expect(component.selectedTags).toEqual(['tag1', 'tag2', 'tag3']); - expect(component.searchDropDownTags).toEqual(['tag4']); + expect(component.selectedTags).toEqual(['tag1', 'tag2', 'tag3']); + expect(component.searchDropDownTags).toEqual(['tag4']); - component.selectTag(({ option: { viewValue: 'noTag'}})); - tick(1600); + component.selectTag({option: {viewValue: 'noTag'}}); + tick(1600); - expect(component.selectionsChange.emit).toHaveBeenCalled(); - }) - ); + expect(component.selectionsChange.emit).toHaveBeenCalled(); + })); it('should filter tags', () => { component.searchDropDownTags = ['math']; diff --git a/core/templates/pages/blog-home-page/tag-filter/tag-filter.component.ts b/core/templates/pages/blog-home-page/tag-filter/tag-filter.component.ts index 18b0ded07297..6315eafdaa9e 100644 --- a/core/templates/pages/blog-home-page/tag-filter/tag-filter.component.ts +++ b/core/templates/pages/blog-home-page/tag-filter/tag-filter.component.ts @@ -16,22 +16,34 @@ * @fileoverview Tag filter component for the blog home page. */ -import { Component, OnInit, ViewChild, ElementRef, Input, Output, EventEmitter} from '@angular/core'; -import { COMMA, ENTER } from '@angular/cdk/keycodes'; -import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; -import { FormControl } from '@angular/forms'; -import { debounceTime, distinctUntilChanged, map, startWith } from 'rxjs/operators'; -import { Observable } from 'rxjs'; -import { BlogPostSearchService } from 'services/blog-search.service'; -import { BlogHomePageConstants } from '../blog-home-page.constants'; +import { + Component, + OnInit, + ViewChild, + ElementRef, + Input, + Output, + EventEmitter, +} from '@angular/core'; +import {COMMA, ENTER} from '@angular/cdk/keycodes'; +import {MatAutocompleteTrigger} from '@angular/material/autocomplete'; +import {FormControl} from '@angular/forms'; +import { + debounceTime, + distinctUntilChanged, + map, + startWith, +} from 'rxjs/operators'; +import {Observable} from 'rxjs'; +import {BlogPostSearchService} from 'services/blog-search.service'; +import {BlogHomePageConstants} from '../blog-home-page.constants'; import isEqual from 'lodash/isEqual'; import '../blog-home-page.component.css'; @Component({ selector: 'oppia-tag-filter', - templateUrl: './tag-filter.component.html' + templateUrl: './tag-filter.component.html', }) - export class TagFilterComponent implements OnInit { // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see @@ -39,8 +51,7 @@ export class TagFilterComponent implements OnInit { @Input() listOfDefaultTags!: string[]; @Input() smallScreenViewIsActive: boolean = false; @Input() selectedTags: string[] = []; - @Output() selectionsChange: EventEmitter = ( - new EventEmitter()); + @Output() selectionsChange: EventEmitter = new EventEmitter(); separatorKeysCodes: number[] = [ENTER, COMMA]; tagFilter = new FormControl(''); @@ -50,21 +61,21 @@ export class TagFilterComponent implements OnInit { @ViewChild('tagFilterInput') tagFilterInput!: ElementRef; @ViewChild('trigger') autoTrigger!: MatAutocompleteTrigger; - constructor( - private blogPostSearchService: BlogPostSearchService - ) { + constructor(private blogPostSearchService: BlogPostSearchService) { this.filteredTags = this.tagFilter.valueChanges.pipe( startWith(null), - map((tag: string | null) => ( - tag ? this.filter(tag) : this.searchDropDownTags.slice())), + map((tag: string | null) => + tag ? this.filter(tag) : this.searchDropDownTags.slice() + ) ); } filter(value: string): string[] { const filterValue = value.toLowerCase(); - return this.searchDropDownTags.filter( - tag => tag.toLowerCase().includes(filterValue)); + return this.searchDropDownTags.filter(tag => + tag.toLowerCase().includes(filterValue) + ); } removeTag(tag: string, tagsList: string[]): void { @@ -80,7 +91,7 @@ export class TagFilterComponent implements OnInit { this.tagFilter.setValue(null); } - selectTag(event: { option: { viewValue: string}}): void { + selectTag(event: {option: {viewValue: string}}): void { this.selectedTags.push(event.option.viewValue); this.refreshSearchDropDownTags(); this.tagFilterInput.nativeElement.value = ''; @@ -98,15 +109,21 @@ export class TagFilterComponent implements OnInit { ngOnInit(): void { this.refreshSearchDropDownTags(); - this.filteredTags.pipe( - debounceTime(BlogHomePageConstants.DEBOUNCE_TIME), distinctUntilChanged() - ).subscribe(() => { - if (!isEqual( - this.blogPostSearchService.lastSelectedTags, this.selectedTags - )) { - this.autoTrigger.closePanel(); - this.selectionsChange.emit(this.selectedTags); - } - }); + this.filteredTags + .pipe( + debounceTime(BlogHomePageConstants.DEBOUNCE_TIME), + distinctUntilChanged() + ) + .subscribe(() => { + if ( + !isEqual( + this.blogPostSearchService.lastSelectedTags, + this.selectedTags + ) + ) { + this.autoTrigger.closePanel(); + this.selectionsChange.emit(this.selectedTags); + } + }); } } diff --git a/core/templates/pages/blog-post-page/blog-post-page-root.component.spec.ts b/core/templates/pages/blog-post-page/blog-post-page-root.component.spec.ts index ad84e3a0ebd7..f0f5d786b4f1 100644 --- a/core/templates/pages/blog-post-page/blog-post-page-root.component.spec.ts +++ b/core/templates/pages/blog-post-page/blog-post-page-root.component.spec.ts @@ -16,25 +16,34 @@ * @fileoverview Unit tests for the blog home page root component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; - -import { AppConstants } from 'app.constants'; -import { BlogHomePageBackendApiService, BlogPostPageData } from 'domain/blog/blog-homepage-backend-api.service'; - -import { BlogPostBackendDict, BlogPostData } from 'domain/blog/blog-post.model'; -import { AccessValidationBackendApiService } from 'pages/oppia-root/routing/access-validation-backend-api.service'; -import { AlertsService } from 'services/alerts.service'; -import { MetaTagCustomizationService } from 'services/contextual/meta-tag-customization.service'; -import { UrlService } from 'services/contextual/url.service'; -import { LoaderService } from 'services/loader.service'; -import { PageHeadService } from 'services/page-head.service'; -import { PageTitleService } from 'services/page-title.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { BlogPostPageRootComponent } from './blog-post-page-root.component'; -import { UserService } from 'services/user.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; + +import {AppConstants} from 'app.constants'; +import { + BlogHomePageBackendApiService, + BlogPostPageData, +} from 'domain/blog/blog-homepage-backend-api.service'; + +import {BlogPostBackendDict, BlogPostData} from 'domain/blog/blog-post.model'; +import {AccessValidationBackendApiService} from 'pages/oppia-root/routing/access-validation-backend-api.service'; +import {AlertsService} from 'services/alerts.service'; +import {MetaTagCustomizationService} from 'services/contextual/meta-tag-customization.service'; +import {UrlService} from 'services/contextual/url.service'; +import {LoaderService} from 'services/loader.service'; +import {PageHeadService} from 'services/page-head.service'; +import {PageTitleService} from 'services/page-title.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {BlogPostPageRootComponent} from './blog-post-page-root.component'; +import {UserService} from 'services/user.service'; class MockTranslateService { onLangChange: EventEmitter = new EventEmitter(); @@ -70,13 +79,8 @@ describe('Blog Post Page Root', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - BlogPostPageRootComponent, - MockTranslatePipe - ], + imports: [HttpClientTestingModule], + declarations: [BlogPostPageRootComponent, MockTranslatePipe], providers: [ PageHeadService, MetaTagCustomizationService, @@ -86,10 +90,10 @@ describe('Blog Post Page Root', () => { BlogHomePageBackendApiService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -100,55 +104,36 @@ describe('Blog Post Page Root', () => { pageHeadService = TestBed.inject(PageHeadService); pageTitleService = TestBed.inject(PageTitleService); blogHomePageBackendApiService = TestBed.inject( - BlogHomePageBackendApiService); + BlogHomePageBackendApiService + ); loaderService = TestBed.inject(LoaderService); urlService = TestBed.inject(UrlService); accessValidationBackendApiService = TestBed.inject( - AccessValidationBackendApiService); + AccessValidationBackendApiService + ); translateService = TestBed.inject(TranslateService); sampleBlogPost = BlogPostData.createFromBackendDict( - sampleBlogPostBackendDict); + sampleBlogPostBackendDict + ); userService = TestBed.inject(UserService); - spyOn(urlService, 'getBlogPostUrlFromUrl') - .and.returnValue('sample-post'); + spyOn(urlService, 'getBlogPostUrlFromUrl').and.returnValue('sample-post'); }); - it('should successfully instantiate the component', - () => { - expect(component).toBeDefined(); - }); - - it('should initialize and show page when access is valid and blog project' + - ' feature is enabled', fakeAsync(() => { - spyOn(userService, 'canUserEditBlogPosts').and.returnValue( - Promise.resolve(false)); - spyOn( - accessValidationBackendApiService, 'validateAccessToBlogPostPage') - .and.returnValue(Promise.resolve()); - spyOn(loaderService, 'showLoadingScreen'); - spyOn(loaderService, 'hideLoadingScreen'); - spyOn(component, 'fetchBlogPostData'); - - component.ngOnInit(); - tick(); - tick(); - - expect(loaderService.showLoadingScreen).toHaveBeenCalled(); - expect(component.blogPostUrlFragment).toBe('sample-post'); - expect(accessValidationBackendApiService.validateAccessToBlogPostPage) - .toHaveBeenCalled(); - expect(component.fetchBlogPostData).toHaveBeenCalledWith('sample-post'); - expect(component.errorPageIsShown).toBeFalse(); - expect(loaderService.hideLoadingScreen).not.toHaveBeenCalled(); - })); + it('should successfully instantiate the component', () => { + expect(component).toBeDefined(); + }); - it('should initialize and show error page when server respond with error', + it( + 'should initialize and show page when access is valid and blog project' + + ' feature is enabled', fakeAsync(() => { spyOn(userService, 'canUserEditBlogPosts').and.returnValue( - Promise.resolve(false)); + Promise.resolve(false) + ); spyOn( - accessValidationBackendApiService, 'validateAccessToBlogPostPage') - .and.returnValue(Promise.reject()); + accessValidationBackendApiService, + 'validateAccessToBlogPostPage' + ).and.returnValue(Promise.resolve()); spyOn(loaderService, 'showLoadingScreen'); spyOn(loaderService, 'hideLoadingScreen'); spyOn(component, 'fetchBlogPostData'); @@ -156,20 +141,49 @@ describe('Blog Post Page Root', () => { component.ngOnInit(); tick(); tick(); - tick(); expect(loaderService.showLoadingScreen).toHaveBeenCalled(); - expect(accessValidationBackendApiService.validateAccessToBlogPostPage) - .toHaveBeenCalledWith('sample-post'); - expect(component.fetchBlogPostData).not.toHaveBeenCalled(); - expect(component.errorPageIsShown).toBeTrue(); - expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); - })); + expect(component.blogPostUrlFragment).toBe('sample-post'); + expect( + accessValidationBackendApiService.validateAccessToBlogPostPage + ).toHaveBeenCalled(); + expect(component.fetchBlogPostData).toHaveBeenCalledWith('sample-post'); + expect(component.errorPageIsShown).toBeFalse(); + expect(loaderService.hideLoadingScreen).not.toHaveBeenCalled(); + }) + ); + + it('should initialize and show error page when server respond with error', fakeAsync(() => { + spyOn(userService, 'canUserEditBlogPosts').and.returnValue( + Promise.resolve(false) + ); + spyOn( + accessValidationBackendApiService, + 'validateAccessToBlogPostPage' + ).and.returnValue(Promise.reject()); + spyOn(loaderService, 'showLoadingScreen'); + spyOn(loaderService, 'hideLoadingScreen'); + spyOn(component, 'fetchBlogPostData'); + + component.ngOnInit(); + tick(); + tick(); + tick(); + + expect(loaderService.showLoadingScreen).toHaveBeenCalled(); + expect( + accessValidationBackendApiService.validateAccessToBlogPostPage + ).toHaveBeenCalledWith('sample-post'); + expect(component.fetchBlogPostData).not.toHaveBeenCalled(); + expect(component.errorPageIsShown).toBeTrue(); + expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); + })); it('should initialize and subscribe to onLangChange', fakeAsync(() => { spyOn( - accessValidationBackendApiService, 'validateAccessToBlogPostPage') - .and.returnValue(Promise.resolve()); + accessValidationBackendApiService, + 'validateAccessToBlogPostPage' + ).and.returnValue(Promise.resolve()); spyOn(component.directiveSubscriptions, 'add'); spyOn(translateService.onLangChange, 'subscribe'); @@ -182,8 +196,9 @@ describe('Blog Post Page Root', () => { it('should update page title whenever the language changes', () => { spyOn( - accessValidationBackendApiService, 'validateAccessToBlogPostPage') - .and.returnValue(Promise.resolve()); + accessValidationBackendApiService, + 'validateAccessToBlogPostPage' + ).and.returnValue(Promise.resolve()); component.ngOnInit(); spyOn(component, 'setPageTitleAndMetaTags'); @@ -202,60 +217,68 @@ describe('Blog Post Page Root', () => { expect(translateService.instant).toHaveBeenCalledWith( AppConstants.PAGES_REGISTERED_WITH_FRONTEND.BLOG_POST_PAGE.TITLE, - { blogPostTitle: 'sampleTitle' }); + {blogPostTitle: 'sampleTitle'} + ); expect(pageHeadService.updateTitleAndMetaTags).toHaveBeenCalledWith( 'I18N_BLOG_POST_PAGE_TITLE', AppConstants.PAGES_REGISTERED_WITH_FRONTEND.BLOG_POST_PAGE.META ); expect(pageTitleService.addMetaTag).toHaveBeenCalledWith({ name: 'keywords', - content: 'news, pandas' + content: 'news, pandas', }); }); - it('should successfully load fetch blog post data from backend', fakeAsync( - () => { - let sampleBlogPostPageData: BlogPostPageData = { - authorUsername: 'test_username', - blogPostDict: sampleBlogPost, - summaryDicts: [], - }; - spyOn(blogHomePageBackendApiService, 'fetchBlogPostPageDataAsync') - .and.returnValue(Promise.resolve(sampleBlogPostPageData)); - spyOn(component, 'setPageTitleAndMetaTags'); - spyOn(loaderService, 'hideLoadingScreen'); + it('should successfully load fetch blog post data from backend', fakeAsync(() => { + let sampleBlogPostPageData: BlogPostPageData = { + authorUsername: 'test_username', + blogPostDict: sampleBlogPost, + summaryDicts: [], + }; + spyOn( + blogHomePageBackendApiService, + 'fetchBlogPostPageDataAsync' + ).and.returnValue(Promise.resolve(sampleBlogPostPageData)); + spyOn(component, 'setPageTitleAndMetaTags'); + spyOn(loaderService, 'hideLoadingScreen'); - component.fetchBlogPostData('sample-post'); + component.fetchBlogPostData('sample-post'); - expect(blogHomePageBackendApiService.fetchBlogPostPageDataAsync) - .toHaveBeenCalledWith('sample-post'); + expect( + blogHomePageBackendApiService.fetchBlogPostPageDataAsync + ).toHaveBeenCalledWith('sample-post'); - tick(); + tick(); - expect(component.blogPost).toEqual(sampleBlogPost); - expect(component.blogPostPageData).toEqual(sampleBlogPostPageData); - expect(component.setPageTitleAndMetaTags).toHaveBeenCalled(); - expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); - } - )); + expect(component.blogPost).toEqual(sampleBlogPost); + expect(component.blogPostPageData).toEqual(sampleBlogPostPageData); + expect(component.setPageTitleAndMetaTags).toHaveBeenCalled(); + expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); + })); it('should use reject handler if fetching blog post data', fakeAsync(() => { spyOn(alertsService, 'addWarning'); - spyOn(blogHomePageBackendApiService, 'fetchBlogPostPageDataAsync') - .and.returnValue(Promise.reject({ + spyOn( + blogHomePageBackendApiService, + 'fetchBlogPostPageDataAsync' + ).and.returnValue( + Promise.reject({ error: {error: 'Backend error'}, - status: 500 - })); + status: 500, + }) + ); component.fetchBlogPostData('sample-post'); - expect(blogHomePageBackendApiService.fetchBlogPostPageDataAsync) - .toHaveBeenCalledWith('sample-post'); + expect( + blogHomePageBackendApiService.fetchBlogPostPageDataAsync + ).toHaveBeenCalledWith('sample-post'); tick(); expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Unable to fetch blog post data.Error: Backend error'); + 'Unable to fetch blog post data.Error: Backend error' + ); })); it('should unsubscribe on component destruction', () => { diff --git a/core/templates/pages/blog-post-page/blog-post-page-root.component.ts b/core/templates/pages/blog-post-page/blog-post-page-root.component.ts index 0506f58c8384..eea5ef9bc19e 100644 --- a/core/templates/pages/blog-post-page/blog-post-page-root.component.ts +++ b/core/templates/pages/blog-post-page/blog-post-page-root.component.ts @@ -16,23 +16,26 @@ * @fileoverview Root component for blog home page. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; -import { AppConstants } from 'app.constants'; -import { AlertsService } from 'services/alerts.service'; -import { AccessValidationBackendApiService } from 'pages/oppia-root/routing/access-validation-backend-api.service'; -import { BlogPostPageData, BlogHomePageBackendApiService } from 'domain/blog/blog-homepage-backend-api.service'; -import { LoaderService } from 'services/loader.service'; -import { PageHeadService } from 'services/page-head.service'; -import { UrlService } from 'services/contextual/url.service'; -import { BlogPostData } from 'domain/blog/blog-post.model'; -import { PageTitleService } from 'services/page-title.service'; -import { UserService } from 'services/user.service'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; +import {AppConstants} from 'app.constants'; +import {AlertsService} from 'services/alerts.service'; +import {AccessValidationBackendApiService} from 'pages/oppia-root/routing/access-validation-backend-api.service'; +import { + BlogPostPageData, + BlogHomePageBackendApiService, +} from 'domain/blog/blog-homepage-backend-api.service'; +import {LoaderService} from 'services/loader.service'; +import {PageHeadService} from 'services/page-head.service'; +import {UrlService} from 'services/contextual/url.service'; +import {BlogPostData} from 'domain/blog/blog-post.model'; +import {PageTitleService} from 'services/page-title.service'; +import {UserService} from 'services/user.service'; @Component({ selector: 'oppia-blog-post-page-root', - templateUrl: './blog-post-page-root.component.html' + templateUrl: './blog-post-page-root.component.html', }) export class BlogPostPageRootComponent implements OnDestroy, OnInit { directiveSubscriptions = new Subscription(); @@ -43,8 +46,7 @@ export class BlogPostPageRootComponent implements OnDestroy, OnInit { blogPostPageData!: BlogPostPageData; constructor( - private accessValidationBackendApiService: - AccessValidationBackendApiService, + private accessValidationBackendApiService: AccessValidationBackendApiService, private blogHomePageBackendApiService: BlogHomePageBackendApiService, private loaderService: LoaderService, private alertsService: AlertsService, @@ -52,7 +54,7 @@ export class BlogPostPageRootComponent implements OnDestroy, OnInit { private translateService: TranslateService, private pageTitleService: PageTitleService, private urlService: UrlService, - private userService: UserService, + private userService: UserService ) {} ngOnInit(): void { @@ -63,48 +65,56 @@ export class BlogPostPageRootComponent implements OnDestroy, OnInit { ); this.blogPostUrlFragment = this.urlService.getBlogPostUrlFromUrl(); this.loaderService.showLoadingScreen('Loading'); - this.userService.canUserEditBlogPosts().then((userCanEditBlogPost) => { + this.userService.canUserEditBlogPosts().then(userCanEditBlogPost => { this.accessValidationBackendApiService .validateAccessToBlogPostPage(this.blogPostUrlFragment) - .then((resp) => { - this.fetchBlogPostData(this.blogPostUrlFragment); - }, (err) => { - this.errorPageIsShown = true; - this.loaderService.hideLoadingScreen(); - }); + .then( + resp => { + this.fetchBlogPostData(this.blogPostUrlFragment); + }, + err => { + this.errorPageIsShown = true; + this.loaderService.hideLoadingScreen(); + } + ); }); } setPageTitleAndMetaTags(): void { let blogPostPage = AppConstants.PAGES_REGISTERED_WITH_FRONTEND.BLOG_POST_PAGE; - const translatedTitle = this.translateService.instant( - blogPostPage.TITLE, { - blogPostTitle: this.blogPost.title - }); + const translatedTitle = this.translateService.instant(blogPostPage.TITLE, { + blogPostTitle: this.blogPost.title, + }); this.pageHeadService.updateTitleAndMetaTags( - translatedTitle, blogPostPage.META); + translatedTitle, + blogPostPage.META + ); this.pageTitleService.addMetaTag({ name: 'keywords', - content: this.blogPost.tags.join(', ') + content: this.blogPost.tags.join(', '), }); } // We fetch the blog post data in root component as we need to set page title // and meta tags. fetchBlogPostData(blogPostUrl: string): void { - this.blogHomePageBackendApiService.fetchBlogPostPageDataAsync( - blogPostUrl - ).then((response: BlogPostPageData) => { - this.blogPost = response.blogPostDict; - this.blogPostPageData = response; - this.pageIsShown = true; - this.setPageTitleAndMetaTags(); - this.loaderService.hideLoadingScreen(); - }, (error) => { - this.alertsService.addWarning( - `Unable to fetch blog post data.Error: ${error.error.error}`); - }); + this.blogHomePageBackendApiService + .fetchBlogPostPageDataAsync(blogPostUrl) + .then( + (response: BlogPostPageData) => { + this.blogPost = response.blogPostDict; + this.blogPostPageData = response; + this.pageIsShown = true; + this.setPageTitleAndMetaTags(); + this.loaderService.hideLoadingScreen(); + }, + error => { + this.alertsService.addWarning( + `Unable to fetch blog post data.Error: ${error.error.error}` + ); + } + ); } ngOnDestroy(): void { diff --git a/core/templates/pages/blog-post-page/blog-post-page-routing.module.ts b/core/templates/pages/blog-post-page/blog-post-page-routing.module.ts index 07f34096da40..2504005bfec4 100644 --- a/core/templates/pages/blog-post-page/blog-post-page-routing.module.ts +++ b/core/templates/pages/blog-post-page/blog-post-page-routing.module.ts @@ -16,24 +16,19 @@ * @fileoverview Routing module for blog home page. */ -import { NgModule } from '@angular/core'; -import { Route, RouterModule } from '@angular/router'; -import { BlogPostPageRootComponent } from './blog-post-page-root.component'; +import {NgModule} from '@angular/core'; +import {Route, RouterModule} from '@angular/router'; +import {BlogPostPageRootComponent} from './blog-post-page-root.component'; const routes: Route[] = [ { path: '', - component: BlogPostPageRootComponent - } + component: BlogPostPageRootComponent, + }, ]; @NgModule({ - imports: [ - RouterModule.forChild(routes) - ], - exports: [ - RouterModule - ] + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], }) - export class BlogPostPageRoutingModule {} diff --git a/core/templates/pages/blog-post-page/blog-post-page.component.spec.ts b/core/templates/pages/blog-post-page/blog-post-page.component.spec.ts index e65bf0869cd9..85f14c793805 100644 --- a/core/templates/pages/blog-post-page/blog-post-page.component.spec.ts +++ b/core/templates/pages/blog-post-page/blog-post-page.component.spec.ts @@ -16,29 +16,29 @@ * @fileoverview Unit tests for Blog Home Page Component. */ -import { Pipe } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { MaterialModule } from 'modules/material.module'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { BlogPostPageComponent } from 'pages/blog-post-page/blog-post-page.component'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { LoaderService } from 'services/loader.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { UrlService } from 'services/contextual/url.service'; -import { BlogCardComponent } from 'pages/blog-dashboard-page/blog-card/blog-card.component'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { UserService } from 'services/user.service'; +import {Pipe} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; +import {MaterialModule} from 'modules/material.module'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {BlogPostPageComponent} from 'pages/blog-post-page/blog-post-page.component'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {LoaderService} from 'services/loader.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {UrlService} from 'services/contextual/url.service'; +import {BlogCardComponent} from 'pages/blog-dashboard-page/blog-card/blog-card.component'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {UserService} from 'services/user.service'; // This throws "TS2307". We need to // suppress this error because rte-text-components are not strictly typed yet. // @ts-ignore -import { RichTextComponentsModule } from 'rich_text_components/rich-text-components.module'; -import { BlogPostBackendDict, BlogPostData } from 'domain/blog/blog-post.model'; -import { SharingLinksComponent } from 'components/common-layout-directives/common-elements/sharing-links.component'; -import { BlogPostPageService } from './services/blog-post-page.service'; +import {RichTextComponentsModule} from 'rich_text_components/rich-text-components.module'; +import {BlogPostBackendDict, BlogPostData} from 'domain/blog/blog-post.model'; +import {SharingLinksComponent} from 'components/common-layout-directives/common-elements/sharing-links.component'; +import {BlogPostPageService} from './services/blog-post-page.service'; @Pipe({name: 'truncate'}) class MockTruncatePipe { @@ -55,8 +55,8 @@ class MockWindowRef { toString() { return 'http://localhost/test_path'; }, - reload: () => { } - } + reload: () => {}, + }, }; } @@ -92,19 +92,19 @@ describe('Blog home page component', () => { BlogCardComponent, MockTranslatePipe, MockTruncatePipe, - SharingLinksComponent + SharingLinksComponent, ], providers: [ { provide: WindowRef, - useClass: MockWindowRef + useClass: MockWindowRef, }, { provide: WindowDimensionsService, - useClass: MockWindowDimensionsService + useClass: MockWindowDimensionsService, }, LoaderService, - BlogPostPageService + BlogPostPageService, ], }).compileComponents(); })); @@ -124,9 +124,7 @@ describe('Blog home page component', () => { }); it('should get the blog post page url', () => { - expect(component.getPageUrl()).toBe( - 'http://localhost/blog/blog-test' - ); + expect(component.getPageUrl()).toBe('http://localhost/blog/blog-test'); }); it('should run the copy command successfully', () => { @@ -136,34 +134,32 @@ describe('Blog home page component', () => { dummyDivElement.appendChild(dummyTextNode); let dummyDocumentFragment = document.createDocumentFragment(); dummyDocumentFragment.appendChild(dummyDivElement); - spyOn( - document, 'getElementsByClassName' - ).withArgs('class-name').and.returnValue(dummyDocumentFragment.children); + spyOn(document, 'getElementsByClassName') + .withArgs('class-name') + .and.returnValue(dummyDocumentFragment.children); spyOn(document, 'execCommand').withArgs('copy'); spyOn($.fn, 'tooltip'); component.copyLink('class-name'); expect(document.execCommand).toHaveBeenCalled(); }); - it('should get formatted date string from the timestamp in milliseconds', - () => { - // This corresponds to Fri, 21 Nov 2014 09:45:00 GMT. - let DATE = '11/21/2014'; - expect(component.getDateStringInWords(DATE)) - .toBe('November 21, 2014'); + it('should get formatted date string from the timestamp in milliseconds', () => { + // This corresponds to Fri, 21 Nov 2014 09:45:00 GMT. + let DATE = '11/21/2014'; + expect(component.getDateStringInWords(DATE)).toBe('November 21, 2014'); - DATE = '01/16/2027'; - expect(component.getDateStringInWords(DATE)) - .toBe('January 16, 2027'); + DATE = '01/16/2027'; + expect(component.getDateStringInWords(DATE)).toBe('January 16, 2027'); - DATE = '02/02/2018'; - expect(component.getDateStringInWords(DATE)) - .toBe('February 2, 2018'); - }); + DATE = '02/02/2018'; + expect(component.getDateStringInWords(DATE)).toBe('February 2, 2018'); + }); it('should determine if small screen view is active', () => { - const windowWidthSpy = - spyOn(windowDimensionsService, 'getWidth').and.returnValue(766); + const windowWidthSpy = spyOn( + windowDimensionsService, + 'getWidth' + ).and.returnValue(766); expect(component.isSmallScreenViewActive()).toBe(true); windowWidthSpy.and.returnValue(1028); expect(component.isSmallScreenViewActive()).toBe(false); @@ -182,16 +178,20 @@ describe('Blog home page component', () => { published_on: '11/21/2014, 09:45:00', }; let blogPostData = BlogPostData.createFromBackendDict( - sampleBlogPostBackendDict); + sampleBlogPostBackendDict + ); component.blogPostPageData = { authorUsername: 'test_username', blogPostDict: blogPostData, summaryDicts: [], }; - spyOn(urlService, 'getBlogPostUrlFromUrl'). - and.returnValue('sample-post-url'); - spyOn(userService, 'getProfileImageDataUrl').and.returnValue( - ['default-image-url-png', 'default-image-url-webp']); + spyOn(urlService, 'getBlogPostUrlFromUrl').and.returnValue( + 'sample-post-url' + ); + spyOn(userService, 'getProfileImageDataUrl').and.returnValue([ + 'default-image-url-png', + 'default-image-url-webp', + ]); component.ngOnInit(); @@ -207,12 +207,14 @@ describe('Blog home page component', () => { it('should navigate to author profile page', () => { spyOn(urlInterpolationService, 'interpolateUrl').and.returnValue( - '/blog/author/test-username'); + '/blog/author/test-username' + ); component.authorUsername = 'test-username'; component.navigateToAuthorProfilePage(); expect(mockWindowRef.nativeWindow.location.href).toEqual( - '/blog/author/test-username'); + '/blog/author/test-username' + ); }); }); diff --git a/core/templates/pages/blog-post-page/blog-post-page.component.ts b/core/templates/pages/blog-post-page/blog-post-page.component.ts index 3a3c9cc498e5..ee8d801575f2 100755 --- a/core/templates/pages/blog-post-page/blog-post-page.component.ts +++ b/core/templates/pages/blog-post-page/blog-post-page.component.ts @@ -16,24 +16,24 @@ * @fileoverview Data and component for the blog post page. */ -import { Component, OnInit, Input } from '@angular/core'; -import { BlogPostPageData } from 'domain/blog/blog-homepage-backend-api.service'; -import { BlogPostSummary } from 'domain/blog/blog-post-summary.model'; -import { BlogPostData } from 'domain/blog/blog-post.model'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { UrlService } from 'services/contextual/url.service'; -import { BlogPostPageConstants } from './blog-post-page.constants'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { BlogPostPageService } from './services/blog-post-page.service'; -import { UserService } from 'services/user.service'; +import {Component, OnInit, Input} from '@angular/core'; +import {BlogPostPageData} from 'domain/blog/blog-homepage-backend-api.service'; +import {BlogPostSummary} from 'domain/blog/blog-post-summary.model'; +import {BlogPostData} from 'domain/blog/blog-post.model'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {UrlService} from 'services/contextual/url.service'; +import {BlogPostPageConstants} from './blog-post-page.constants'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {BlogPostPageService} from './services/blog-post-page.service'; +import {UserService} from 'services/user.service'; import dayjs from 'dayjs'; import './blog-post-page.component.css'; @Component({ selector: 'oppia-blog-post-page', - templateUrl: './blog-post-page.component.html' + templateUrl: './blog-post-page.component.html', }) export class BlogPostPageComponent implements OnInit { @Input() blogPostPageData!: BlogPostPageData; @@ -61,18 +61,19 @@ export class BlogPostPageComponent implements OnInit { ) {} ngOnInit(): void { - this.MAX_POSTS_TO_RECOMMEND_AT_END_OF_BLOG_POST = ( - BlogPostPageConstants.MAX_POSTS_TO_RECOMMEND_AT_END_OF_BLOG_POST); + this.MAX_POSTS_TO_RECOMMEND_AT_END_OF_BLOG_POST = + BlogPostPageConstants.MAX_POSTS_TO_RECOMMEND_AT_END_OF_BLOG_POST; this.blogPostUrlFragment = this.urlService.getBlogPostUrlFromUrl(); this.authorUsername = this.blogPostPageData.authorUsername; this.blogPost = this.blogPostPageData.blogPostDict; this.blogPostPageService.blogPostId = this.blogPostPageData.blogPostDict.id; this.postsToRecommend = this.blogPostPageData.summaryDicts; - [this.authorProfilePicPngUrl, this.authorProfilePicWebpUrl] = ( - this.userService.getProfileImageDataUrl(this.authorUsername)); + [this.authorProfilePicPngUrl, this.authorProfilePicWebpUrl] = + this.userService.getProfileImageDataUrl(this.authorUsername); if (this.blogPost.publishedOn) { this.publishedDateString = this.getDateStringInWords( - this.blogPost.publishedOn); + this.blogPost.publishedOn + ); } } @@ -95,8 +96,7 @@ export class BlogPostPageComponent implements OnInit { } getDateStringInWords(naiveDate: string): string { - return dayjs( - naiveDate.split(',')[0], 'MM-DD-YYYY').format('MMMM D, YYYY'); + return dayjs(naiveDate.split(',')[0], 'MM-DD-YYYY').format('MMMM D, YYYY'); } isSmallScreenViewActive(): boolean { @@ -104,11 +104,10 @@ export class BlogPostPageComponent implements OnInit { } navigateToAuthorProfilePage(): void { - this.windowRef.nativeWindow.location.href = ( + this.windowRef.nativeWindow.location.href = this.urlInterpolationService.interpolateUrl( BlogPostPageConstants.BLOG_AUTHOR_PROFILE_PAGE_URL_TEMPLATE, - { author_username: this.authorUsername } - ) - ); + {author_username: this.authorUsername} + ); } } diff --git a/core/templates/pages/blog-post-page/blog-post-page.module.ts b/core/templates/pages/blog-post-page/blog-post-page.module.ts index 06947fd7fe12..bf1a97586f57 100644 --- a/core/templates/pages/blog-post-page/blog-post-page.module.ts +++ b/core/templates/pages/blog-post-page/blog-post-page.module.ts @@ -16,23 +16,23 @@ * @fileoverview Module for the blog home page. */ -import { NgModule } from '@angular/core'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { InfiniteScrollModule } from 'ngx-infinite-scroll'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { FormsModule } from '@angular/forms'; -import { ReactiveFormsModule } from '@angular/forms'; -import { TranslateModule } from '@ngx-translate/core'; -import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import {NgModule} from '@angular/core'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {InfiniteScrollModule} from 'ngx-infinite-scroll'; +import {MatTooltipModule} from '@angular/material/tooltip'; +import {FormsModule} from '@angular/forms'; +import {ReactiveFormsModule} from '@angular/forms'; +import {TranslateModule} from '@ngx-translate/core'; +import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; -import { BlogPostPageRootComponent } from './blog-post-page-root.component'; -import { BlogPostPageComponent } from './blog-post-page.component'; -import { CommonModule } from '@angular/common'; -import { MatMenuModule } from '@angular/material/menu'; -import { BlogPostPageRoutingModule } from './blog-post-page-routing.module'; -import { StringUtilityPipesModule } from 'filters/string-utility-filters/string-utility-pipes.module'; -import { Error404PageModule } from 'pages/error-pages/error-404/error-404-page.module'; -import { SharedBlogComponentsModule } from 'pages/blog-dashboard-page/shared-blog-components.module'; +import {BlogPostPageRootComponent} from './blog-post-page-root.component'; +import {BlogPostPageComponent} from './blog-post-page.component'; +import {CommonModule} from '@angular/common'; +import {MatMenuModule} from '@angular/material/menu'; +import {BlogPostPageRoutingModule} from './blog-post-page-routing.module'; +import {StringUtilityPipesModule} from 'filters/string-utility-filters/string-utility-pipes.module'; +import {Error404PageModule} from 'pages/error-pages/error-404/error-404-page.module'; +import {SharedBlogComponentsModule} from 'pages/blog-dashboard-page/shared-blog-components.module'; @NgModule({ imports: [ @@ -48,15 +48,9 @@ import { SharedBlogComponentsModule } from 'pages/blog-dashboard-page/shared-blo ReactiveFormsModule, Error404PageModule, SharedBlogComponentsModule, - MatTooltipModule + MatTooltipModule, ], - declarations: [ - BlogPostPageComponent, - BlogPostPageRootComponent - ], - entryComponents: [ - BlogPostPageComponent, - BlogPostPageRootComponent - ] + declarations: [BlogPostPageComponent, BlogPostPageRootComponent], + entryComponents: [BlogPostPageComponent, BlogPostPageRootComponent], }) export class BlogPostPageModule {} diff --git a/core/templates/pages/blog-post-page/services/blog-post-page.service.spec.ts b/core/templates/pages/blog-post-page/services/blog-post-page.service.spec.ts index dd41ef04680b..49647c3f4748 100644 --- a/core/templates/pages/blog-post-page/services/blog-post-page.service.spec.ts +++ b/core/templates/pages/blog-post-page/services/blog-post-page.service.spec.ts @@ -16,19 +16,17 @@ * @fileoverview Unit Tests for Blog Post Page service. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TestBed, waitForAsync} from '@angular/core/testing'; -import { BlogPostPageService } from 'pages/blog-post-page/services/blog-post-page.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TestBed, waitForAsync} from '@angular/core/testing'; +import {BlogPostPageService} from 'pages/blog-post-page/services/blog-post-page.service'; describe('Blog Post Page service', () => { let blogPostPageService: BlogPostPageService; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - ], - providers: [] + imports: [HttpClientTestingModule], + providers: [], }).compileComponents(); })); diff --git a/core/templates/pages/blog-post-page/services/blog-post-page.service.ts b/core/templates/pages/blog-post-page/services/blog-post-page.service.ts index 23ce95e58e79..2ca666c4fdc2 100644 --- a/core/templates/pages/blog-post-page/services/blog-post-page.service.ts +++ b/core/templates/pages/blog-post-page/services/blog-post-page.service.ts @@ -16,11 +16,11 @@ * @fileoverview Service that handles data on blog post page. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class BlogPostPageService { private _blogPostId: string = ''; @@ -36,5 +36,6 @@ export class BlogPostPageService { } } -angular.module('oppia').factory('BlogPostPageService', - downgradeInjectable(BlogPostPageService)); +angular + .module('oppia') + .factory('BlogPostPageService', downgradeInjectable(BlogPostPageService)); diff --git a/core/templates/pages/classroom-admin-page/classroom-admin-auth.guard.spec.ts b/core/templates/pages/classroom-admin-page/classroom-admin-auth.guard.spec.ts index 4ffd75265476..d68a87157510 100644 --- a/core/templates/pages/classroom-admin-page/classroom-admin-auth.guard.spec.ts +++ b/core/templates/pages/classroom-admin-page/classroom-admin-auth.guard.spec.ts @@ -16,14 +16,19 @@ * @fileoverview Tests for ClassroomAdminAuthGuard */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TestBed } from '@angular/core/testing'; -import { ActivatedRouteSnapshot, Router, RouterStateSnapshot, NavigationExtras } from '@angular/router'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TestBed} from '@angular/core/testing'; +import { + ActivatedRouteSnapshot, + Router, + RouterStateSnapshot, + NavigationExtras, +} from '@angular/router'; -import { AppConstants } from 'app.constants'; -import { UserInfo } from 'domain/user/user-info.model'; -import { UserService } from 'services/user.service'; -import { ClassroomAdminAuthGuard } from './classroom-admin-auth.guard'; +import {AppConstants} from 'app.constants'; +import {UserInfo} from 'domain/user/user-info.model'; +import {UserService} from 'services/user.service'; +import {ClassroomAdminAuthGuard} from './classroom-admin-auth.guard'; class MockRouter { navigate(commands: string[], extras?: NavigationExtras): Promise { @@ -39,7 +44,7 @@ describe('ClassroomAdminAuthGuard', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [UserService, { provide: Router, useClass: MockRouter }], + providers: [UserService, {provide: Router, useClass: MockRouter}], }).compileComponents(); guard = TestBed.inject(ClassroomAdminAuthGuard); @@ -47,40 +52,42 @@ describe('ClassroomAdminAuthGuard', () => { router = TestBed.inject(Router); }); - it('should redirect user to 401 page if user is not curriculum admin', - (done) => { - const getUserInfoAsyncSpy = spyOn( - userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(UserInfo.createDefault()) - ); - const navigateSpy = spyOn(router, 'navigate').and.callThrough(); + it('should redirect user to 401 page if user is not curriculum admin', done => { + const getUserInfoAsyncSpy = spyOn( + userService, + 'getUserInfoAsync' + ).and.returnValue(Promise.resolve(UserInfo.createDefault())); + const navigateSpy = spyOn(router, 'navigate').and.callThrough(); - guard.canActivate( - new ActivatedRouteSnapshot(), {} as RouterStateSnapshot).then( - (canActivate) => { - expect(canActivate).toBeFalse(); - expect(getUserInfoAsyncSpy).toHaveBeenCalledTimes(1); - expect(navigateSpy).toHaveBeenCalledWith([ - `${AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ERROR.ROUTE}/401`]); - done(); - }); - }); - it('should not redirect user to 401 page if user is curriculum admin', - (done) => { - const getUserInfoAsyncSpy = spyOn( - userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(new UserInfo( - [], false, true, false, false, false, '', '', '', true)) - ); - const navigateSpy = spyOn(router, 'navigate').and.callThrough(); + guard + .canActivate(new ActivatedRouteSnapshot(), {} as RouterStateSnapshot) + .then(canActivate => { + expect(canActivate).toBeFalse(); + expect(getUserInfoAsyncSpy).toHaveBeenCalledTimes(1); + expect(navigateSpy).toHaveBeenCalledWith([ + `${AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ERROR.ROUTE}/401`, + ]); + done(); + }); + }); + it('should not redirect user to 401 page if user is curriculum admin', done => { + const getUserInfoAsyncSpy = spyOn( + userService, + 'getUserInfoAsync' + ).and.returnValue( + Promise.resolve( + new UserInfo([], false, true, false, false, false, '', '', '', true) + ) + ); + const navigateSpy = spyOn(router, 'navigate').and.callThrough(); - guard.canActivate( - new ActivatedRouteSnapshot(), {} as RouterStateSnapshot).then( - (canActivate) => { - expect(canActivate).toBeTrue(); - expect(getUserInfoAsyncSpy).toHaveBeenCalledTimes(1); - expect(navigateSpy).not.toHaveBeenCalled(); - done(); - }); - }); + guard + .canActivate(new ActivatedRouteSnapshot(), {} as RouterStateSnapshot) + .then(canActivate => { + expect(canActivate).toBeTrue(); + expect(getUserInfoAsyncSpy).toHaveBeenCalledTimes(1); + expect(navigateSpy).not.toHaveBeenCalled(); + done(); + }); + }); }); diff --git a/core/templates/pages/classroom-admin-page/classroom-admin-auth.guard.ts b/core/templates/pages/classroom-admin-page/classroom-admin-auth.guard.ts index feadcf526899..41528c8b6b11 100644 --- a/core/templates/pages/classroom-admin-page/classroom-admin-auth.guard.ts +++ b/core/templates/pages/classroom-admin-page/classroom-admin-auth.guard.ts @@ -17,8 +17,8 @@ * if the user is not a curriculum admin. */ -import { Location } from '@angular/common'; -import { Injectable } from '@angular/core'; +import {Location} from '@angular/common'; +import {Injectable} from '@angular/core'; import { ActivatedRouteSnapshot, CanActivate, @@ -26,11 +26,11 @@ import { RouterStateSnapshot, } from '@angular/router'; -import { AppConstants } from 'app.constants'; -import { UserService } from 'services/user.service'; +import {AppConstants} from 'app.constants'; +import {UserService} from 'services/user.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ClassroomAdminAuthGuard implements CanActivate { constructor( @@ -40,19 +40,21 @@ export class ClassroomAdminAuthGuard implements CanActivate { ) {} async canActivate( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot ): Promise { const userInfo = await this.userService.getUserInfoAsync(); if (userInfo.isCurriculumAdmin()) { return true; } - this.router.navigate( - [`${AppConstants.PAGES_REGISTERED_WITH_FRONTEND - .ERROR.ROUTE}/401`]).then(() => { - this.location.replaceState(state.url); - }); + this.router + .navigate([ + `${AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ERROR.ROUTE}/401`, + ]) + .then(() => { + this.location.replaceState(state.url); + }); return false; } } diff --git a/core/templates/pages/classroom-admin-page/classroom-admin-page-root.component.spec.ts b/core/templates/pages/classroom-admin-page/classroom-admin-page-root.component.spec.ts index e8671e7323fe..a2b1bd0b186f 100644 --- a/core/templates/pages/classroom-admin-page/classroom-admin-page-root.component.spec.ts +++ b/core/templates/pages/classroom-admin-page/classroom-admin-page-root.component.spec.ts @@ -16,14 +16,14 @@ * @fileoverview Unit tests for classroom admin page root component. */ -import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { TranslateModule } from '@ngx-translate/core'; +import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {TranslateModule} from '@ngx-translate/core'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; -import { ClassroomAdminPageRootComponent } from './classroom-admin-page-root.component'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; +import {ClassroomAdminPageRootComponent} from './classroom-admin-page-root.component'; describe('ClassroomAdminPageRootComponent', () => { let fixture: ComponentFixture; @@ -43,8 +43,10 @@ describe('ClassroomAdminPageRootComponent', () => { it('should have the title and meta tags set', () => { expect(component.title).toEqual( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.CURRICULUM_ADMIN.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.CURRICULUM_ADMIN.TITLE + ); expect(component.meta).toEqual( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.CURRICULUM_ADMIN.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.CURRICULUM_ADMIN.META + ); }); }); diff --git a/core/templates/pages/classroom-admin-page/classroom-admin-page-root.component.ts b/core/templates/pages/classroom-admin-page/classroom-admin-page-root.component.ts index 2df6946f94dd..dca6319bc6f4 100644 --- a/core/templates/pages/classroom-admin-page/classroom-admin-page-root.component.ts +++ b/core/templates/pages/classroom-admin-page/classroom-admin-page-root.component.ts @@ -16,9 +16,9 @@ * @fileoverview Classroom admin page root component. */ -import { Component } from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { BaseRootComponent, MetaTagData } from 'pages/base-root.component'; +import {Component} from '@angular/core'; +import {AppConstants} from 'app.constants'; +import {BaseRootComponent, MetaTagData} from 'pages/base-root.component'; @Component({ selector: 'oppia-classroom-admin-page-root', @@ -28,7 +28,6 @@ export class ClassroomAdminPageRootComponent extends BaseRootComponent { title: string = AppConstants.PAGES_REGISTERED_WITH_FRONTEND.CURRICULUM_ADMIN.TITLE; - meta: MetaTagData[] = - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.CURRICULUM_ADMIN.META as - unknown as Readonly[]; + meta: MetaTagData[] = AppConstants.PAGES_REGISTERED_WITH_FRONTEND + .CURRICULUM_ADMIN.META as unknown as Readonly[]; } diff --git a/core/templates/pages/classroom-admin-page/classroom-admin-page.component.spec.ts b/core/templates/pages/classroom-admin-page/classroom-admin-page.component.spec.ts index 057070bb977c..3dd438553d9a 100644 --- a/core/templates/pages/classroom-admin-page/classroom-admin-page.component.spec.ts +++ b/core/templates/pages/classroom-admin-page/classroom-admin-page.component.spec.ts @@ -16,28 +16,32 @@ * @fileoverview Tests for the classroom admin component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { MatAutocompleteModule } from '@angular/material/autocomplete'; -import { EditableTopicBackendApiService } from 'domain/topic/editable-topic-backend-api.service'; -import { ClassroomAdminPageComponent } from 'pages/classroom-admin-page/classroom-admin-page.component'; -import { ClassroomBackendApiService} from '../../domain/classroom/classroom-backend-api.service'; -import { AlertsService } from 'services/alerts.service'; -import { ExistingClassroomData } from './existing-classroom.model'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { MaterialModule } from 'modules/material.module'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { CdkDragDrop } from '@angular/cdk/drag-drop'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {MatAutocompleteModule} from '@angular/material/autocomplete'; +import {EditableTopicBackendApiService} from 'domain/topic/editable-topic-backend-api.service'; +import {ClassroomAdminPageComponent} from 'pages/classroom-admin-page/classroom-admin-page.component'; +import {ClassroomBackendApiService} from '../../domain/classroom/classroom-backend-api.service'; +import {AlertsService} from 'services/alerts.service'; +import {ExistingClassroomData} from './existing-classroom.model'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {MaterialModule} from 'modules/material.module'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {CdkDragDrop} from '@angular/cdk/drag-drop'; import cloneDeep from 'lodash/cloneDeep'; - class MockNgbModal { open() { return { - result: Promise.resolve() + result: Promise.resolve(), }; } } @@ -61,20 +65,17 @@ describe('Classroom Admin Page component ', () => { MatAutocompleteModule, ReactiveFormsModule, ], - declarations: [ - ClassroomAdminPageComponent, - MockTranslatePipe - ], + declarations: [ClassroomAdminPageComponent, MockTranslatePipe], providers: [ AlertsService, ClassroomBackendApiService, EditableTopicBackendApiService, { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(ClassroomAdminPageComponent); component = fixture.componentInstance; @@ -83,7 +84,8 @@ describe('Classroom Admin Page component ', () => { beforeEach(() => { classroomBackendApiService = TestBed.inject(ClassroomBackendApiService); editableTopicBackendApiService = TestBed.inject( - EditableTopicBackendApiService); + EditableTopicBackendApiService + ); ngbModal = TestBed.inject(NgbModal); alertsService = TestBed.inject(AlertsService); }); @@ -91,7 +93,7 @@ describe('Classroom Admin Page component ', () => { it('should initialize the component', fakeAsync(() => { let response = { mathClassroomId: 'math', - physicsClassroomId: 'physics' + physicsClassroomId: 'physics', }; spyOn( classroomBackendApiService, @@ -108,125 +110,121 @@ describe('Classroom Admin Page component ', () => { expect(component.classroomCount).toEqual(2); })); - it( - 'should open classroom detail and update classroom properties', - fakeAsync(() => { - let response = { - classroomDict: { - classroomId: 'classroomId', - name: 'math', - urlFragment: 'math', - courseDetails: '', - topicListIntro: '', - topicIdToPrerequisiteTopicIds: {} - } - }; - spyOn(classroomBackendApiService, 'getClassroomDataAsync') - .and.returnValue(Promise.resolve(response)); + it('should open classroom detail and update classroom properties', fakeAsync(() => { + let response = { + classroomDict: { + classroomId: 'classroomId', + name: 'math', + urlFragment: 'math', + courseDetails: '', + topicListIntro: '', + topicIdToPrerequisiteTopicIds: {}, + }, + }; + spyOn(classroomBackendApiService, 'getClassroomDataAsync').and.returnValue( + Promise.resolve(response) + ); - expect(component.classroomViewerMode).toBeFalse(); - expect(component.classroomDetailsIsShown).toBeFalse(); - component.ngOnInit(); + expect(component.classroomViewerMode).toBeFalse(); + expect(component.classroomDetailsIsShown).toBeFalse(); + component.ngOnInit(); - component.getClassroomData('classroomId'); - tick(); + component.getClassroomData('classroomId'); + tick(); - expect(component.classroomViewerMode).toBeTrue(); - expect(component.classroomDetailsIsShown).toBeTrue(); - })); + expect(component.classroomViewerMode).toBeTrue(); + expect(component.classroomDetailsIsShown).toBeTrue(); + })); - it('should display alert when unable to fetch classroom data', - fakeAsync(() => { - spyOn(classroomBackendApiService, 'getClassroomDataAsync') - .and.returnValue(Promise.reject(400)); - spyOn(alertsService, 'addWarning'); + it('should display alert when unable to fetch classroom data', fakeAsync(() => { + spyOn(classroomBackendApiService, 'getClassroomDataAsync').and.returnValue( + Promise.reject(400) + ); + spyOn(alertsService, 'addWarning'); - component.getClassroomData('classroomId'); - tick(); + component.getClassroomData('classroomId'); + tick(); - expect( - classroomBackendApiService.getClassroomDataAsync).toHaveBeenCalled(); - expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Failed to get classroom data'); - })); + expect(classroomBackendApiService.getClassroomDataAsync).toHaveBeenCalled(); + expect(alertsService.addWarning).toHaveBeenCalledWith( + 'Failed to get classroom data' + ); + })); - it( - 'should close classroom details when already in view mode', - fakeAsync(() => { - let response = { - classroomDict: { - classroomId: 'classroomId', - name: 'math', - urlFragment: 'math', - courseDetails: '', - topicListIntro: '', - topicIdToPrerequisiteTopicIds: {} - } - }; - spyOn(classroomBackendApiService, 'getClassroomDataAsync') - .and.returnValue(Promise.resolve(response)); - component.classroomViewerMode = true; - component.classroomDetailsIsShown = true; + it('should close classroom details when already in view mode', fakeAsync(() => { + let response = { + classroomDict: { + classroomId: 'classroomId', + name: 'math', + urlFragment: 'math', + courseDetails: '', + topicListIntro: '', + topicIdToPrerequisiteTopicIds: {}, + }, + }; + spyOn(classroomBackendApiService, 'getClassroomDataAsync').and.returnValue( + Promise.resolve(response) + ); + component.classroomViewerMode = true; + component.classroomDetailsIsShown = true; - component.tempClassroomData = ( - ExistingClassroomData.createClassroomFromDict(response.classroomDict)); + component.tempClassroomData = ExistingClassroomData.createClassroomFromDict( + response.classroomDict + ); - component.getClassroomData('classroomId'); - tick(); + component.getClassroomData('classroomId'); + tick(); - expect(component.classroomDetailsIsShown).toBeFalse(); - expect(component.classroomViewerMode).toBeFalse(); - })); + expect(component.classroomDetailsIsShown).toBeFalse(); + expect(component.classroomViewerMode).toBeFalse(); + })); - it( - 'should not close classroom details while editing classroom properties', - fakeAsync(() => { - let response = { - classroomDict: { - classroomId: 'classroomId', - name: 'math', - urlFragment: 'math', - courseDetails: '', - topicListIntro: '', - topicIdToPrerequisiteTopicIds: {} - } - }; - spyOn(classroomBackendApiService, 'getClassroomDataAsync') - .and.returnValue(Promise.resolve(response)); + it('should not close classroom details while editing classroom properties', fakeAsync(() => { + let response = { + classroomDict: { + classroomId: 'classroomId', + name: 'math', + urlFragment: 'math', + courseDetails: '', + topicListIntro: '', + topicIdToPrerequisiteTopicIds: {}, + }, + }; + spyOn(classroomBackendApiService, 'getClassroomDataAsync').and.returnValue( + Promise.resolve(response) + ); - component.classroomEditorMode = true; - component.classroomViewerMode = false; - component.classroomDetailsIsShown = true; + component.classroomEditorMode = true; + component.classroomViewerMode = false; + component.classroomDetailsIsShown = true; - component.getClassroomData('classroomId'); - tick(); + component.getClassroomData('classroomId'); + tick(); - expect(component.classroomDetailsIsShown).toBeTrue(); - expect(component.classroomEditorMode).toBeTrue(); - expect(component.classroomViewerMode).toBeFalse(); - })); + expect(component.classroomDetailsIsShown).toBeTrue(); + expect(component.classroomEditorMode).toBeTrue(); + expect(component.classroomViewerMode).toBeFalse(); + })); - it( - 'should get classroom ID to classroom name and update classroom count', - fakeAsync(() => { - let response = { - mathClassroomId: 'math', - physicsClassroomId: 'physics' - }; - spyOn( - classroomBackendApiService, - 'getAllClassroomIdToClassroomNameDictAsync' - ).and.returnValue(Promise.resolve(response)); + it('should get classroom ID to classroom name and update classroom count', fakeAsync(() => { + let response = { + mathClassroomId: 'math', + physicsClassroomId: 'physics', + }; + spyOn( + classroomBackendApiService, + 'getAllClassroomIdToClassroomNameDictAsync' + ).and.returnValue(Promise.resolve(response)); - expect(component.pageIsInitialized).toBeFalse(); + expect(component.pageIsInitialized).toBeFalse(); - component.getAllClassroomIdToClassroomName(); - tick(); + component.getAllClassroomIdToClassroomName(); + tick(); - expect(component.pageIsInitialized).toBeTrue(); - expect(component.classroomIdToClassroomName).toEqual(response); - expect(component.classroomCount).toEqual(2); - })); + expect(component.pageIsInitialized).toBeTrue(); + expect(component.classroomIdToClassroomName).toEqual(response); + expect(component.classroomCount).toEqual(2); + })); it('should be able to update the classroom name', () => { const response = { @@ -236,13 +234,15 @@ describe('Classroom Admin Page component ', () => { urlFragment: 'math', courseDetails: '', topicListIntro: '', - topicIdToPrerequisiteTopicIds: {} - } + topicIdToPrerequisiteTopicIds: {}, + }, }; component.tempClassroomData = ExistingClassroomData.createClassroomFromDict( - response.classroomDict); + response.classroomDict + ); component.classroomData = ExistingClassroomData.createClassroomFromDict( - response.classroomDict); + response.classroomDict + ); component.tempClassroomData.setClassroomName('Discrete maths'); component.classroomDataIsChanged = false; @@ -253,7 +253,8 @@ describe('Classroom Admin Page component ', () => { it( 'should not update the classroom field if the current changes match ' + - 'with existing ones', () => { + 'with existing ones', + () => { const response = { classroomDict: { classroomId: 'classroomId', @@ -261,13 +262,14 @@ describe('Classroom Admin Page component ', () => { urlFragment: 'math', courseDetails: '', topicListIntro: '', - topicIdToPrerequisiteTopicIds: {} - } + topicIdToPrerequisiteTopicIds: {}, + }, }; - component.tempClassroomData = ( - ExistingClassroomData.createClassroomFromDict(response.classroomDict)); + component.tempClassroomData = + ExistingClassroomData.createClassroomFromDict(response.classroomDict); component.classroomData = ExistingClassroomData.createClassroomFromDict( - response.classroomDict); + response.classroomDict + ); component.tempClassroomData.setClassroomName('Discrete maths'); component.classroomDataIsChanged = false; @@ -280,7 +282,8 @@ describe('Classroom Admin Page component ', () => { component.updateClassroomField(); expect(component.classroomDataIsChanged).toBeFalse(); - }); + } + ); it('should be able to update the classroom url fragment', () => { let response = { @@ -290,13 +293,15 @@ describe('Classroom Admin Page component ', () => { urlFragment: 'math', courseDetails: '', topicListIntro: '', - topicIdToPrerequisiteTopicIds: {} - } + topicIdToPrerequisiteTopicIds: {}, + }, }; component.tempClassroomData = ExistingClassroomData.createClassroomFromDict( - response.classroomDict); + response.classroomDict + ); component.classroomData = ExistingClassroomData.createClassroomFromDict( - response.classroomDict); + response.classroomDict + ); component.tempClassroomData.setUrlFragment('newMathUrl'); component.classroomDataIsChanged = false; @@ -313,15 +318,18 @@ describe('Classroom Admin Page component ', () => { urlFragment: 'math', courseDetails: '', topicListIntro: '', - topicIdToPrerequisiteTopicIds: {} - } + topicIdToPrerequisiteTopicIds: {}, + }, }; component.tempClassroomData = ExistingClassroomData.createClassroomFromDict( - response.classroomDict); + response.classroomDict + ); component.classroomData = ExistingClassroomData.createClassroomFromDict( - response.classroomDict); + response.classroomDict + ); component.tempClassroomData.setCourseDetails( - 'Oppia\'s curated maths lesson.'); + "Oppia's curated maths lesson." + ); component.classroomDataIsChanged = false; component.updateClassroomField(); @@ -337,15 +345,18 @@ describe('Classroom Admin Page component ', () => { urlFragment: 'math', courseDetails: '', topicListIntro: '', - topicIdToPrerequisiteTopicIds: {} - } + topicIdToPrerequisiteTopicIds: {}, + }, }; component.tempClassroomData = ExistingClassroomData.createClassroomFromDict( - response.classroomDict); + response.classroomDict + ); component.classroomData = ExistingClassroomData.createClassroomFromDict( - response.classroomDict); + response.classroomDict + ); component.tempClassroomData.setTopicListIntro( - 'Start from the basics with our first topic.'); + 'Start from the basics with our first topic.' + ); component.classroomDataIsChanged = false; component.updateClassroomField(); @@ -358,35 +369,34 @@ describe('Classroom Admin Page component ', () => { classroomId: 'classroomId', name: 'math', urlFragment: 'math', - courseDetails: 'Oppia\'s curated maths lesson.', + courseDetails: "Oppia's curated maths lesson.", topicListIntro: 'Start from the basics with our first topic.', - topicIdToPrerequisiteTopicIds: {} + topicIdToPrerequisiteTopicIds: {}, }; let classroomBackendDict = { classroom_id: 'classroomId', name: 'math', url_fragment: 'math', - course_details: 'Oppia\'s curated maths lesson.', + course_details: "Oppia's curated maths lesson.", topic_list_intro: 'Start from the basics with our first topic.', - topic_id_to_prerequisite_topic_ids: {} + topic_id_to_prerequisite_topic_ids: {}, }; - expect(component.convertClassroomDictToBackendForm( - classroomDict)).toEqual(classroomBackendDict); + expect(component.convertClassroomDictToBackendForm(classroomDict)).toEqual( + classroomBackendDict + ); }); - it( - 'should be able to close classroom viewer and open classroom editor', - () => { - component.classroomViewerMode = true; - component.classroomEditorMode = false; + it('should be able to close classroom viewer and open classroom editor', () => { + component.classroomViewerMode = true; + component.classroomEditorMode = false; - component.openClassroomInEditorMode(); + component.openClassroomInEditorMode(); - expect(component.classroomViewerMode).toBeFalse(); - expect(component.classroomEditorMode).toBeTrue(); - }); + expect(component.classroomViewerMode).toBeFalse(); + expect(component.classroomEditorMode).toBeTrue(); + }); it('should be able to save classroom data', fakeAsync(() => { component.classroomViewerMode = false; @@ -397,14 +407,14 @@ describe('Classroom Admin Page component ', () => { classroomId: 'classroomId', name: 'math', urlFragment: 'math', - courseDetails: 'Oppia\'s curated maths lesson.', + courseDetails: "Oppia's curated maths lesson.", topicListIntro: 'Start from the basics with our first topic.', - topicIdToPrerequisiteTopicIds: {} + topicIdToPrerequisiteTopicIds: {}, }; - component.tempClassroomData = ExistingClassroomData.createClassroomFromDict( - classroomDict); - component.classroomData = ExistingClassroomData.createClassroomFromDict( - classroomDict); + component.tempClassroomData = + ExistingClassroomData.createClassroomFromDict(classroomDict); + component.classroomData = + ExistingClassroomData.createClassroomFromDict(classroomDict); spyOn( classroomBackendApiService, @@ -419,38 +429,37 @@ describe('Classroom Admin Page component ', () => { expect(component.classroomDataIsChanged).toBeFalse(); })); - it( - 'should be able handle rejection handler while saving classroom data', - fakeAsync(() => { - let classroomDict = { - classroomId: 'classroomId', - name: 'math', - urlFragment: 'math', - courseDetails: 'Oppia\'s curated maths lesson.', - topicListIntro: 'Start from the basics with our first topic.', - topicIdToPrerequisiteTopicIds: {} - }; - component.tempClassroomData = ( - ExistingClassroomData.createClassroomFromDict(classroomDict)); - component.classroomData = ExistingClassroomData.createClassroomFromDict( - classroomDict); + it('should be able handle rejection handler while saving classroom data', fakeAsync(() => { + let classroomDict = { + classroomId: 'classroomId', + name: 'math', + urlFragment: 'math', + courseDetails: "Oppia's curated maths lesson.", + topicListIntro: 'Start from the basics with our first topic.', + topicIdToPrerequisiteTopicIds: {}, + }; + component.tempClassroomData = + ExistingClassroomData.createClassroomFromDict(classroomDict); + component.classroomData = + ExistingClassroomData.createClassroomFromDict(classroomDict); - spyOn( - classroomBackendApiService, - 'updateClassroomDataAsync' - ).and.returnValue(Promise.reject()); + spyOn( + classroomBackendApiService, + 'updateClassroomDataAsync' + ).and.returnValue(Promise.reject()); - component.tempClassroomData.setClassroomName('Discrete maths'); + component.tempClassroomData.setClassroomName('Discrete maths'); - component.saveClassroomData('classroomId'); - tick(); + component.saveClassroomData('classroomId'); + tick(); - expect(component.tempClassroomData).toEqual(component.classroomData); - })); + expect(component.tempClassroomData).toEqual(component.classroomData); + })); it( 'should present a confirmation modal before exiting editor mode if ' + - 'any classroom propeties are already modified', fakeAsync(() => { + 'any classroom propeties are already modified', + fakeAsync(() => { component.classroomDataIsChanged = true; component.classroomEditorMode = true; component.classroomViewerMode = false; @@ -458,16 +467,14 @@ describe('Classroom Admin Page component ', () => { classroomId: 'classroomId', name: 'math', urlFragment: 'math', - courseDetails: 'Oppia\'s curated maths lesson.', + courseDetails: "Oppia's curated maths lesson.", topicListIntro: 'Start from the basics with our first topic.', - topicIdToPrerequisiteTopicIds: {} + topicIdToPrerequisiteTopicIds: {}, }); - spyOn(ngbModal, 'open').and.returnValue( - { - componentInstance: {}, - result: Promise.resolve() - } as NgbModalRef - ); + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: {}, + result: Promise.resolve(), + } as NgbModalRef); component.closeClassroomConfigEditor(); tick(); @@ -476,20 +483,20 @@ describe('Classroom Admin Page component ', () => { expect(component.classroomEditorMode).toBeFalse(); expect(component.classroomViewerMode).toBeTrue(); expect(component.classroomDataIsChanged).toBeFalse(); - })); + }) + ); it( 'should be able to cancel the exit editor confirmation modal and ' + - 'continue editing', () => { + 'continue editing', + () => { component.classroomDataIsChanged = true; component.classroomEditorMode = true; component.classroomViewerMode = false; - spyOn(ngbModal, 'open').and.returnValue( - { - componentInstance: {}, - result: Promise.reject() - } as NgbModalRef - ); + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: {}, + result: Promise.reject(), + } as NgbModalRef); component.closeClassroomConfigEditor(); @@ -497,47 +504,47 @@ describe('Classroom Admin Page component ', () => { expect(component.classroomDataIsChanged).toBeTrue(); expect(component.classroomEditorMode).toBeTrue(); expect(component.classroomViewerMode).toBeFalse(); - }); + } + ); it( 'should not present a confirmation modal if none of the classroom ' + - 'properties were updated', () => { + 'properties were updated', + () => { component.classroomDataIsChanged = false; component.classroomEditorMode = true; component.classroomViewerMode = false; - spyOn(ngbModal, 'open').and.returnValue( - { - componentInstance: {}, - result: Promise.resolve() - } as NgbModalRef - ); + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: {}, + result: Promise.resolve(), + } as NgbModalRef); component.closeClassroomConfigEditor(); expect(ngbModal.open).not.toHaveBeenCalled(); expect(component.classroomEditorMode).toBeFalse(); expect(component.classroomViewerMode).toBeTrue(); - }); + } + ); it('should be able to delete classroom', fakeAsync(() => { component.classroomIdToClassroomName = { mathClassroomId: 'math', chemistryClassroomId: 'chemistry', - physicsClassroomId: 'physics' + physicsClassroomId: 'physics', }; let expectedClassroom = { chemistryClassroomId: 'chemistry', - physicsClassroomId: 'physics' + physicsClassroomId: 'physics', }; component.classroomCount = 3; - spyOn(ngbModal, 'open').and.returnValue( - { - componentInstance: {}, - result: Promise.resolve() - } as NgbModalRef + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: {}, + result: Promise.resolve(), + } as NgbModalRef); + spyOn(classroomBackendApiService, 'deleteClassroomAsync').and.returnValue( + Promise.resolve() ); - spyOn(classroomBackendApiService, 'deleteClassroomAsync') - .and.returnValue(Promise.resolve()); component.deleteClassroom('mathClassroomId'); tick(); @@ -547,47 +554,45 @@ describe('Classroom Admin Page component ', () => { expect(component.classroomCount).toEqual(2); })); - it( - 'should be able to cancel modal for not deleting the classroom', - fakeAsync(() => { - component.classroomIdToClassroomName = { - mathClassroomId: 'math', - chemistryClassroomId: 'chemistry', - physicsClassroomId: 'physics' - }; - let expectedClassroomIdToName = { - mathClassroomId: 'math', - chemistryClassroomId: 'chemistry', - physicsClassroomId: 'physics' - }; - component.classroomCount = 3; - spyOn(ngbModal, 'open').and.returnValue( - { - componentInstance: {}, - result: Promise.reject() - } as NgbModalRef - ); - spyOn(classroomBackendApiService, 'deleteClassroomAsync') - .and.returnValue(Promise.resolve()); + it('should be able to cancel modal for not deleting the classroom', fakeAsync(() => { + component.classroomIdToClassroomName = { + mathClassroomId: 'math', + chemistryClassroomId: 'chemistry', + physicsClassroomId: 'physics', + }; + let expectedClassroomIdToName = { + mathClassroomId: 'math', + chemistryClassroomId: 'chemistry', + physicsClassroomId: 'physics', + }; + component.classroomCount = 3; + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: {}, + result: Promise.reject(), + } as NgbModalRef); + spyOn(classroomBackendApiService, 'deleteClassroomAsync').and.returnValue( + Promise.resolve() + ); - component.deleteClassroom('mathClassroomId'); - tick(); + component.deleteClassroom('mathClassroomId'); + tick(); - expect(ngbModal.open).toHaveBeenCalled(); - expect(component.classroomIdToClassroomName).toEqual( - expectedClassroomIdToName); - expect(component.classroomCount).toEqual(3); - })); + expect(ngbModal.open).toHaveBeenCalled(); + expect(component.classroomIdToClassroomName).toEqual( + expectedClassroomIdToName + ); + expect(component.classroomCount).toEqual(3); + })); it('should be able to create new classroom', fakeAsync(() => { component.classroomIdToClassroomName = { mathClassroomId: 'math', - chemistryClassroomId: 'chemistry' + chemistryClassroomId: 'chemistry', }; let expectedClassroomIdToName = { mathClassroomId: 'math', chemistryClassroomId: 'chemistry', - physicsClassroomId: 'physics' + physicsClassroomId: 'physics', }; let classroomDict = { classroom_id: 'physicsClassroomId', @@ -595,140 +600,144 @@ describe('Classroom Admin Page component ', () => { url_fragment: '', course_details: '', topic_list_intro: '', - topic_id_to_prerequisite_topic_ids: {} + topic_id_to_prerequisite_topic_ids: {}, }; - spyOn(ngbModal, 'open').and.returnValue( - { - componentInstance: { - existingClassroomNames: ['math', 'chemistry'] - }, - result: Promise.resolve(classroomDict) - } as NgbModalRef - ); + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: { + existingClassroomNames: ['math', 'chemistry'], + }, + result: Promise.resolve(classroomDict), + } as NgbModalRef); component.createNewClassroom(); tick(); expect(ngbModal.open).toHaveBeenCalled(); expect(component.classroomIdToClassroomName).toEqual( - expectedClassroomIdToName); + expectedClassroomIdToName + ); })); it('should be able to cancel create classsroom modal', fakeAsync(() => { component.classroomIdToClassroomName = { mathClassroomId: 'math', - chemistryClassroomId: 'chemistry' + chemistryClassroomId: 'chemistry', }; let expectedClassroomIdToName = { mathClassroomId: 'math', - chemistryClassroomId: 'chemistry' + chemistryClassroomId: 'chemistry', }; - spyOn(ngbModal, 'open').and.returnValue( - { - componentInstance: { - existingClassroomNames: ['math', 'chemistry'] - }, - result: Promise.reject() - } as NgbModalRef - ); + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: { + existingClassroomNames: ['math', 'chemistry'], + }, + result: Promise.reject(), + } as NgbModalRef); component.createNewClassroom(); tick(); expect(ngbModal.open).toHaveBeenCalled(); expect(component.classroomIdToClassroomName).toEqual( - expectedClassroomIdToName); + expectedClassroomIdToName + ); })); - it( - 'should convert the topic dependencies from topic ID form to topic name', - fakeAsync(() => { - const topicIdTotopicName = { - topicId1: 'Dummy topic 1', - topicId2: 'Dummy topic 2', - topicId3: 'Dummy topic 3' - }; - const topicIdToPrerequisiteTopicIds = { - topicId1: [], - topicId2: ['topicId1'], - topicId3: ['topicId1'] - }; - const topicNameToPrerequisiteTopicNames = { - 'Dummy topic 1': [], - 'Dummy topic 2': ['Dummy topic 1'], - 'Dummy topic 3': ['Dummy topic 1'] - }; + it('should convert the topic dependencies from topic ID form to topic name', fakeAsync(() => { + const topicIdTotopicName = { + topicId1: 'Dummy topic 1', + topicId2: 'Dummy topic 2', + topicId3: 'Dummy topic 3', + }; + const topicIdToPrerequisiteTopicIds = { + topicId1: [], + topicId2: ['topicId1'], + topicId3: ['topicId1'], + }; + const topicNameToPrerequisiteTopicNames = { + 'Dummy topic 1': [], + 'Dummy topic 2': ['Dummy topic 1'], + 'Dummy topic 3': ['Dummy topic 1'], + }; - component.tempClassroomData = ( - ExistingClassroomData.createClassroomFromDict({ - classroomId: 'classroomId', - name: 'math', - urlFragment: 'math', - courseDetails: '', - topicListIntro: '', - topicIdToPrerequisiteTopicIds: {} - })); + component.tempClassroomData = ExistingClassroomData.createClassroomFromDict( + { + classroomId: 'classroomId', + name: 'math', + urlFragment: 'math', + courseDetails: '', + topicListIntro: '', + topicIdToPrerequisiteTopicIds: {}, + } + ); - spyOn(editableTopicBackendApiService, 'getTopicIdToTopicNameAsync') - .and.returnValue(Promise.resolve(topicIdTotopicName)); + spyOn( + editableTopicBackendApiService, + 'getTopicIdToTopicNameAsync' + ).and.returnValue(Promise.resolve(topicIdTotopicName)); - component.setTopicDependencyByTopicName(topicIdToPrerequisiteTopicIds); + component.setTopicDependencyByTopicName(topicIdToPrerequisiteTopicIds); - tick(); + tick(); - expect(component.topicNameToPrerequisiteTopicNames).toEqual( - topicNameToPrerequisiteTopicNames); - })); + expect(component.topicNameToPrerequisiteTopicNames).toEqual( + topicNameToPrerequisiteTopicNames + ); + })); - it( - 'should be able to add new topic ID to classroom', fakeAsync(() => { - component.tempClassroomData = ( - ExistingClassroomData.createClassroomFromDict({ - classroomId: 'classroomId', - name: 'math', - urlFragment: 'math', - courseDetails: '', - topicListIntro: '', - topicIdToPrerequisiteTopicIds: {} - })); - expect(component.tempClassroomData.getTopicsCount()).toEqual(0); - expect(component.tempClassroomData.getTopicIdToPrerequisiteTopicId()) - .toEqual({}); - expect(component.topicNameToPrerequisiteTopicNames).toEqual({}); - const topicIdToTopicName = { - topicId1: 'Dummy topic 1' - }; + it('should be able to add new topic ID to classroom', fakeAsync(() => { + component.tempClassroomData = ExistingClassroomData.createClassroomFromDict( + { + classroomId: 'classroomId', + name: 'math', + urlFragment: 'math', + courseDetails: '', + topicListIntro: '', + topicIdToPrerequisiteTopicIds: {}, + } + ); + expect(component.tempClassroomData.getTopicsCount()).toEqual(0); + expect( + component.tempClassroomData.getTopicIdToPrerequisiteTopicId() + ).toEqual({}); + expect(component.topicNameToPrerequisiteTopicNames).toEqual({}); + const topicIdToTopicName = { + topicId1: 'Dummy topic 1', + }; - spyOn(editableTopicBackendApiService, 'getTopicIdToTopicNameAsync') - .and.returnValue(Promise.resolve(topicIdToTopicName)); + spyOn( + editableTopicBackendApiService, + 'getTopicIdToTopicNameAsync' + ).and.returnValue(Promise.resolve(topicIdToTopicName)); - component.addTopicId('topicId1'); + component.addTopicId('topicId1'); - tick(); + tick(); - expect(component.tempClassroomData.getTopicIdToPrerequisiteTopicId()) - .toEqual({topicId1: []}); - expect(component.topicNameToPrerequisiteTopicNames).toEqual({ - 'Dummy topic 1': [] - }); - expect(component.tempClassroomData.getTopicsCount()).toEqual(1); - })); + expect( + component.tempClassroomData.getTopicIdToPrerequisiteTopicId() + ).toEqual({topicId1: []}); + expect(component.topicNameToPrerequisiteTopicNames).toEqual({ + 'Dummy topic 1': [], + }); + expect(component.tempClassroomData.getTopicsCount()).toEqual(1); + })); - it( - 'should be able to show error when new topic ID does not exist', - fakeAsync(() => { - component.topicWithGivenIdExists = true; + it('should be able to show error when new topic ID does not exist', fakeAsync(() => { + component.topicWithGivenIdExists = true; - spyOn(editableTopicBackendApiService, 'getTopicIdToTopicNameAsync') - .and.returnValue(Promise.reject()); + spyOn( + editableTopicBackendApiService, + 'getTopicIdToTopicNameAsync' + ).and.returnValue(Promise.reject()); - component.addTopicId('topicId1'); + component.addTopicId('topicId1'); - tick(); + tick(); - expect(component.topicWithGivenIdExists).toBeFalse(); - })); + expect(component.topicWithGivenIdExists).toBeFalse(); + })); it('should be able to show and remove new topic input field', () => { expect(component.newTopicCanBeAdded).toBeFalse(); @@ -754,17 +763,17 @@ describe('Classroom Admin Page component ', () => { component.topicIdsToTopicName = { topicId1: 'Dummy topic 1', topicId2: 'Dummy topic 2', - topicId3: 'Dummy topic 3' + topicId3: 'Dummy topic 3', }; component.topicNameToPrerequisiteTopicNames = { 'Dummy topic 1': [], 'Dummy topic 2': ['Dummy topic 1'], - 'Dummy topic 3': ['Dummy topic 2'] + 'Dummy topic 3': ['Dummy topic 2'], }; - component.tempClassroomData = ( - ExistingClassroomData.createClassroomFromDict({ + component.tempClassroomData = ExistingClassroomData.createClassroomFromDict( + { classroomId: 'classroomId', name: 'math', urlFragment: 'math', @@ -773,15 +782,16 @@ describe('Classroom Admin Page component ', () => { topicIdToPrerequisiteTopicIds: { topicId1: [], topicId2: ['topicId1'], - topicId3: ['topicId2'] - } - })); + topicId3: ['topicId2'], + }, + } + ); component.classroomData = cloneDeep(component.tempClassroomData); const expectedTopicNameToPrerequisiteTopicNames = { 'Dummy topic 1': [], 'Dummy topic 2': ['Dummy topic 1'], - 'Dummy topic 3': ['Dummy topic 1', 'Dummy topic 2'] + 'Dummy topic 3': ['Dummy topic 1', 'Dummy topic 2'], }; component.addDependencyForTopic('Dummy topic 3', 'Dummy topic 1'); @@ -789,30 +799,32 @@ describe('Classroom Admin Page component ', () => { const expectedTopicIdToPrerequisiteTopicIds = { topicId1: [], topicId2: ['topicId1'], - topicId3: ['topicId2', 'topicId1'] + topicId3: ['topicId2', 'topicId1'], }; - expect(component.tempClassroomData.getTopicIdToPrerequisiteTopicId()) - .toEqual(expectedTopicIdToPrerequisiteTopicIds); + expect( + component.tempClassroomData.getTopicIdToPrerequisiteTopicId() + ).toEqual(expectedTopicIdToPrerequisiteTopicIds); expect(component.topicNameToPrerequisiteTopicNames).toEqual( - expectedTopicNameToPrerequisiteTopicNames); + expectedTopicNameToPrerequisiteTopicNames + ); }); it('should not add prerequisite that is already added for a topic', () => { component.topicIdsToTopicName = { topicId1: 'Dummy topic 1', topicId2: 'Dummy topic 2', - topicId3: 'Dummy topic 3' + topicId3: 'Dummy topic 3', }; component.topicNameToPrerequisiteTopicNames = { 'Dummy topic 1': [], 'Dummy topic 2': ['Dummy topic 1'], - 'Dummy topic 3': ['Dummy topic 2'] + 'Dummy topic 3': ['Dummy topic 2'], }; - component.tempClassroomData = ( - ExistingClassroomData.createClassroomFromDict({ + component.tempClassroomData = ExistingClassroomData.createClassroomFromDict( + { classroomId: 'classroomId', name: 'math', urlFragment: 'math', @@ -821,15 +833,16 @@ describe('Classroom Admin Page component ', () => { topicIdToPrerequisiteTopicIds: { topicId1: [], topicId2: ['topicId1'], - topicId3: ['topicId2'] - } - })); + topicId3: ['topicId2'], + }, + } + ); component.classroomData = cloneDeep(component.tempClassroomData); const expectedTopicNameToPrerequisiteTopicNames = { 'Dummy topic 1': [], 'Dummy topic 2': ['Dummy topic 1'], - 'Dummy topic 3': ['Dummy topic 2'] + 'Dummy topic 3': ['Dummy topic 2'], }; component.addDependencyForTopic('Dummy topic 3', 'Dummy topic 2'); @@ -837,30 +850,32 @@ describe('Classroom Admin Page component ', () => { const expectedTopicIdToPrerequisiteTopicIds = { topicId1: [], topicId2: ['topicId1'], - topicId3: ['topicId2'] + topicId3: ['topicId2'], }; - expect(component.tempClassroomData.getTopicIdToPrerequisiteTopicId()) - .toEqual(expectedTopicIdToPrerequisiteTopicIds); + expect( + component.tempClassroomData.getTopicIdToPrerequisiteTopicId() + ).toEqual(expectedTopicIdToPrerequisiteTopicIds); expect(component.topicNameToPrerequisiteTopicNames).toEqual( - expectedTopicNameToPrerequisiteTopicNames); + expectedTopicNameToPrerequisiteTopicNames + ); }); it('should be able to remove prerequisite from a topic', () => { component.topicIdsToTopicName = { topicId1: 'Dummy topic 1', topicId2: 'Dummy topic 2', - topicId3: 'Dummy topic 3' + topicId3: 'Dummy topic 3', }; component.topicNameToPrerequisiteTopicNames = { 'Dummy topic 1': [], 'Dummy topic 2': ['Dummy topic 1'], - 'Dummy topic 3': ['Dummy topic 2', 'Dummy topic 1'] + 'Dummy topic 3': ['Dummy topic 2', 'Dummy topic 1'], }; - component.tempClassroomData = ( - ExistingClassroomData.createClassroomFromDict({ + component.tempClassroomData = ExistingClassroomData.createClassroomFromDict( + { classroomId: 'classroomId', name: 'math', urlFragment: 'math', @@ -869,9 +884,10 @@ describe('Classroom Admin Page component ', () => { topicIdToPrerequisiteTopicIds: { topicId1: [], topicId2: ['topicId1'], - topicId3: ['topicId2', 'topicId1'] - } - })); + topicId3: ['topicId2', 'topicId1'], + }, + } + ); component.classroomData = cloneDeep(component.tempClassroomData); @@ -880,52 +896,59 @@ describe('Classroom Admin Page component ', () => { const expectedTopicIdToPrerequisiteTopicIds = { topicId1: [], topicId2: ['topicId1'], - topicId3: ['topicId2'] + topicId3: ['topicId2'], }; const expectedTopicNameToPrerequisiteTopicNames = { 'Dummy topic 1': [], 'Dummy topic 2': ['Dummy topic 1'], - 'Dummy topic 3': ['Dummy topic 2'] + 'Dummy topic 3': ['Dummy topic 2'], }; - expect(component.tempClassroomData.getTopicIdToPrerequisiteTopicId()) - .toEqual(expectedTopicIdToPrerequisiteTopicIds); + expect( + component.tempClassroomData.getTopicIdToPrerequisiteTopicId() + ).toEqual(expectedTopicIdToPrerequisiteTopicIds); expect(component.topicNameToPrerequisiteTopicNames).toEqual( - expectedTopicNameToPrerequisiteTopicNames); + expectedTopicNameToPrerequisiteTopicNames + ); }); it('should be able to get available prerequisite for topic names', () => { component.topicNameToPrerequisiteTopicNames = { 'Dummy topic 1': [], 'Dummy topic 2': ['Dummy topic 1'], - 'Dummy topic 3': ['Dummy topic 2'] + 'Dummy topic 3': ['Dummy topic 2'], }; component.getEligibleTopicPrerequisites('Dummy topic 2'); - expect(component.eligibleTopicNamesForPrerequisites).toEqual( - ['Dummy topic 3']); - expect(component.tempEligibleTopicNamesForPrerequisites).toEqual( - ['Dummy topic 3']); + expect(component.eligibleTopicNamesForPrerequisites).toEqual([ + 'Dummy topic 3', + ]); + expect(component.tempEligibleTopicNamesForPrerequisites).toEqual([ + 'Dummy topic 3', + ]); }); it('should be able to filter prerequisite dropdown as input changes', () => { - component.eligibleTopicNamesForPrerequisites = ( - ['Dummy topic 1', 'Topic 2']); - component.tempEligibleTopicNamesForPrerequisites = ( - ['Dummy topic 1', 'Topic 2']); + component.eligibleTopicNamesForPrerequisites = ['Dummy topic 1', 'Topic 2']; + component.tempEligibleTopicNamesForPrerequisites = [ + 'Dummy topic 1', + 'Topic 2', + ]; component.prerequisiteInput = 'Dummy'; component.onPrerequisiteInputChange(); - expect(component.tempEligibleTopicNamesForPrerequisites).toEqual( - ['Dummy topic 1']); + expect(component.tempEligibleTopicNamesForPrerequisites).toEqual([ + 'Dummy topic 1', + ]); component.prerequisiteInput = 'Topic'; component.onPrerequisiteInputChange(); - expect(component.tempEligibleTopicNamesForPrerequisites).toEqual( - ['Topic 2']); + expect(component.tempEligibleTopicNamesForPrerequisites).toEqual([ + 'Topic 2', + ]); component.prerequisiteInput = 'xyz'; component.onPrerequisiteInputChange(); @@ -933,97 +956,93 @@ describe('Classroom Admin Page component ', () => { expect(component.tempEligibleTopicNamesForPrerequisites).toEqual([]); }); - it( - 'should be able to show and remove edit and delete functionality box', - () => { - component.topicDependencyEditOptionIsShown = false; + it('should be able to show and remove edit and delete functionality box', () => { + component.topicDependencyEditOptionIsShown = false; - component.editDependency('topicName'); + component.editDependency('topicName'); - expect(component.topicDependencyEditOptionIsShown).toBeTrue(); + expect(component.topicDependencyEditOptionIsShown).toBeTrue(); - component.editDependency('topicName'); + component.editDependency('topicName'); - expect(component.topicDependencyEditOptionIsShown).toBeFalse(); - }); + expect(component.topicDependencyEditOptionIsShown).toBeFalse(); + }); - it( - 'should be able to delete a topic from the classroom on modal confirmation', - fakeAsync(() => { - spyOn(ngbModal, 'open').and.returnValue( - { - componentInstance: {}, - result: Promise.resolve() - } as NgbModalRef - ); + it('should be able to delete a topic from the classroom on modal confirmation', fakeAsync(() => { + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: {}, + result: Promise.resolve(), + } as NgbModalRef); - component.topicIdsToTopicName = { - topicId1: 'Dummy topic 1', - topicId2: 'Dummy topic 2', - topicId3: 'Dummy topic 3' - }; + component.topicIdsToTopicName = { + topicId1: 'Dummy topic 1', + topicId2: 'Dummy topic 2', + topicId3: 'Dummy topic 3', + }; - component.topicNameToPrerequisiteTopicNames = { - 'Dummy topic 1': [], - 'Dummy topic 2': ['Dummy topic 1'], - 'Dummy topic 3': ['Dummy topic 2', 'Dummy topic 1'] - }; + component.topicNameToPrerequisiteTopicNames = { + 'Dummy topic 1': [], + 'Dummy topic 2': ['Dummy topic 1'], + 'Dummy topic 3': ['Dummy topic 2', 'Dummy topic 1'], + }; - component.tempClassroomData = ( - ExistingClassroomData.createClassroomFromDict({ - classroomId: 'classroomId', - name: 'math', - urlFragment: 'math', - courseDetails: '', - topicListIntro: '', - topicIdToPrerequisiteTopicIds: { - topicId1: [], - topicId2: ['topicId1'], - topicId3: ['topicId2', 'topicId1'] - } - })); - component.classroomData = cloneDeep(component.tempClassroomData); + component.tempClassroomData = ExistingClassroomData.createClassroomFromDict( + { + classroomId: 'classroomId', + name: 'math', + urlFragment: 'math', + courseDetails: '', + topicListIntro: '', + topicIdToPrerequisiteTopicIds: { + topicId1: [], + topicId2: ['topicId1'], + topicId3: ['topicId2', 'topicId1'], + }, + } + ); + component.classroomData = cloneDeep(component.tempClassroomData); - component.deleteTopic('Dummy topic 3'); + component.deleteTopic('Dummy topic 3'); - const expectedTopicIdToPrerequisiteTopicIds = { - topicId1: [], - topicId2: ['topicId1'] - }; + const expectedTopicIdToPrerequisiteTopicIds = { + topicId1: [], + topicId2: ['topicId1'], + }; - const expectedTopicNameToPrerequisiteTopicNames = { - 'Dummy topic 1': [], - 'Dummy topic 2': ['Dummy topic 1'] - }; + const expectedTopicNameToPrerequisiteTopicNames = { + 'Dummy topic 1': [], + 'Dummy topic 2': ['Dummy topic 1'], + }; - component.deleteTopic('Dummy topic 3'); - tick(); + component.deleteTopic('Dummy topic 3'); + tick(); - expect(component.tempClassroomData.getTopicIdToPrerequisiteTopicId()) - .toEqual(expectedTopicIdToPrerequisiteTopicIds); - expect(component.topicNameToPrerequisiteTopicNames).toEqual( - expectedTopicNameToPrerequisiteTopicNames); - })); + expect( + component.tempClassroomData.getTopicIdToPrerequisiteTopicId() + ).toEqual(expectedTopicIdToPrerequisiteTopicIds); + expect(component.topicNameToPrerequisiteTopicNames).toEqual( + expectedTopicNameToPrerequisiteTopicNames + ); + })); it( 'should mark the topic dependency flag to false when all topics ' + - 'are deleted', fakeAsync(() => { - spyOn(ngbModal, 'open').and.returnValue( - { - componentInstance: {}, - result: Promise.resolve() - } as NgbModalRef - ); + 'are deleted', + fakeAsync(() => { + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: {}, + result: Promise.resolve(), + } as NgbModalRef); component.topicIdsToTopicName = { topicId1: 'Dummy topic 1', }; component.topicNameToPrerequisiteTopicNames = { - 'Dummy topic 1': [] + 'Dummy topic 1': [], }; - component.tempClassroomData = ( + component.tempClassroomData = ExistingClassroomData.createClassroomFromDict({ classroomId: 'classroomId', name: 'math', @@ -1031,9 +1050,9 @@ describe('Classroom Admin Page component ', () => { courseDetails: '', topicListIntro: '', topicIdToPrerequisiteTopicIds: { - topicId1: [] - } - })); + topicId1: [], + }, + }); component.classroomData = cloneDeep(component.tempClassroomData); component.topicDependencyIsLoaded = true; @@ -1043,68 +1062,68 @@ describe('Classroom Admin Page component ', () => { expect(component.topicIdsToTopicName).toEqual({}); expect(component.topicDependencyIsLoaded).toBeFalse(); - })); + }) + ); - it( - 'should be able to handle rejection handler on topic deletion modal', - fakeAsync(() => { - spyOn(ngbModal, 'open').and.returnValue( - { - componentInstance: {}, - result: Promise.reject() - } as NgbModalRef - ); + it('should be able to handle rejection handler on topic deletion modal', fakeAsync(() => { + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: {}, + result: Promise.reject(), + } as NgbModalRef); - component.topicIdsToTopicName = { - topicId1: 'Dummy topic 1', - topicId2: 'Dummy topic 2', - topicId3: 'Dummy topic 3' - }; + component.topicIdsToTopicName = { + topicId1: 'Dummy topic 1', + topicId2: 'Dummy topic 2', + topicId3: 'Dummy topic 3', + }; - component.topicNameToPrerequisiteTopicNames = { - 'Dummy topic 1': [], - 'Dummy topic 2': ['Dummy topic 1'], - 'Dummy topic 3': ['Dummy topic 2', 'Dummy topic 1'] - }; + component.topicNameToPrerequisiteTopicNames = { + 'Dummy topic 1': [], + 'Dummy topic 2': ['Dummy topic 1'], + 'Dummy topic 3': ['Dummy topic 2', 'Dummy topic 1'], + }; - component.tempClassroomData = ( - ExistingClassroomData.createClassroomFromDict({ - classroomId: 'classroomId', - name: 'math', - urlFragment: 'math', - courseDetails: '', - topicListIntro: '', - topicIdToPrerequisiteTopicIds: { - topicId1: [], - topicId2: ['topicId1'], - topicId3: ['topicId2', 'topicId1'] - } - })); - - const expectedTopicIdToPrerequisiteTopicIds = { - topicId1: [], - topicId2: ['topicId1'], - topicId3: ['topicId2', 'topicId1'] - }; + component.tempClassroomData = ExistingClassroomData.createClassroomFromDict( + { + classroomId: 'classroomId', + name: 'math', + urlFragment: 'math', + courseDetails: '', + topicListIntro: '', + topicIdToPrerequisiteTopicIds: { + topicId1: [], + topicId2: ['topicId1'], + topicId3: ['topicId2', 'topicId1'], + }, + } + ); - const expectedTopicNameToPrerequisiteTopicNames = { - 'Dummy topic 1': [], - 'Dummy topic 2': ['Dummy topic 1'], - 'Dummy topic 3': ['Dummy topic 2', 'Dummy topic 1'] - }; + const expectedTopicIdToPrerequisiteTopicIds = { + topicId1: [], + topicId2: ['topicId1'], + topicId3: ['topicId2', 'topicId1'], + }; - component.deleteTopic('Dummy topic 1'); - tick(); + const expectedTopicNameToPrerequisiteTopicNames = { + 'Dummy topic 1': [], + 'Dummy topic 2': ['Dummy topic 1'], + 'Dummy topic 3': ['Dummy topic 2', 'Dummy topic 1'], + }; - expect(component.tempClassroomData.getTopicIdToPrerequisiteTopicId()) - .toEqual(expectedTopicIdToPrerequisiteTopicIds); - expect(component.topicNameToPrerequisiteTopicNames).toEqual( - expectedTopicNameToPrerequisiteTopicNames); - })); + component.deleteTopic('Dummy topic 1'); + tick(); + + expect( + component.tempClassroomData.getTopicIdToPrerequisiteTopicId() + ).toEqual(expectedTopicIdToPrerequisiteTopicIds); + expect(component.topicNameToPrerequisiteTopicNames).toEqual( + expectedTopicNameToPrerequisiteTopicNames + ); + })); it('should change list oder', () => { - component.tempClassroomData = ( - ExistingClassroomData.createClassroomFromDict({ + component.tempClassroomData = ExistingClassroomData.createClassroomFromDict( + { classroomId: 'classroomId', name: 'math', urlFragment: 'math', @@ -1113,18 +1132,19 @@ describe('Classroom Admin Page component ', () => { topicIdToPrerequisiteTopicIds: { topicId1: [], topicId2: ['topicId1'], - topicId3: ['topicId2', 'topicId1'] - } - })); + topicId3: ['topicId2', 'topicId1'], + }, + } + ); component.topicIdsToTopicName = { topicId1: 'Topic1', topicId2: 'Topic2', - topicId3: 'Topic3' + topicId3: 'Topic3', }; component.topicNameToPrerequisiteTopicNames = { Topic1: [], Topic2: ['Topic1'], - Topic3: ['Topic2', 'Topic1'] + Topic3: ['Topic2', 'Topic1'], }; component.topicNames = ['Topic1', 'Topic2', 'Topic3']; component.classroomData = cloneDeep(component.tempClassroomData); @@ -1142,80 +1162,75 @@ describe('Classroom Admin Page component ', () => { ).toHaveBeenCalled(); }); - it( - 'should present a graph modal before topics dependency visualization', - fakeAsync(() => { - component.tempClassroomData = ( - ExistingClassroomData.createClassroomFromDict({ - classroomId: 'classroomId', - name: 'math', - urlFragment: 'math', - courseDetails: '', - topicListIntro: '', - topicIdToPrerequisiteTopicIds: { - topicId1: [], - topicId2: ['topicId1'], - topicId3: ['topicId2', 'topicId1'] - } - })); - component.topicIdsToTopicName = { - topicId1: 'Topic 1', - topicId2: 'Topic 2', - topicId3: 'Topic 3' - }; + it('should present a graph modal before topics dependency visualization', fakeAsync(() => { + component.tempClassroomData = ExistingClassroomData.createClassroomFromDict( + { + classroomId: 'classroomId', + name: 'math', + urlFragment: 'math', + courseDetails: '', + topicListIntro: '', + topicIdToPrerequisiteTopicIds: { + topicId1: [], + topicId2: ['topicId1'], + topicId3: ['topicId2', 'topicId1'], + }, + } + ); + component.topicIdsToTopicName = { + topicId1: 'Topic 1', + topicId2: 'Topic 2', + topicId3: 'Topic 3', + }; - spyOn(ngbModal, 'open').and.returnValue( - { - componentInstance: {}, - result: Promise.resolve() - } as NgbModalRef - ); + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: {}, + result: Promise.resolve(), + } as NgbModalRef); - component.viewGraph(); - tick(); + component.viewGraph(); + tick(); - expect(ngbModal.open).toHaveBeenCalled(); - })); + expect(ngbModal.open).toHaveBeenCalled(); + })); - it( - 'should be able to cancel the topics graph visualization', fakeAsync(() => { - component.tempClassroomData = ( - ExistingClassroomData.createClassroomFromDict({ - classroomId: 'classroomId', - name: 'math', - urlFragment: 'math', - courseDetails: '', - topicListIntro: '', - topicIdToPrerequisiteTopicIds: { - topicId1: [], - topicId2: ['topicId1'], - topicId3: ['topicId2', 'topicId1'] - } - })); - component.topicIdsToTopicName = { - topicId1: 'Topic 1', - topicId2: 'Topic 2', - topicId3: 'Topic 3' - }; + it('should be able to cancel the topics graph visualization', fakeAsync(() => { + component.tempClassroomData = ExistingClassroomData.createClassroomFromDict( + { + classroomId: 'classroomId', + name: 'math', + urlFragment: 'math', + courseDetails: '', + topicListIntro: '', + topicIdToPrerequisiteTopicIds: { + topicId1: [], + topicId2: ['topicId1'], + topicId3: ['topicId2', 'topicId1'], + }, + } + ); + component.topicIdsToTopicName = { + topicId1: 'Topic 1', + topicId2: 'Topic 2', + topicId3: 'Topic 3', + }; - spyOn(ngbModal, 'open').and.returnValue( - { - componentInstance: {}, - result: Promise.reject() - } as NgbModalRef - ); + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: {}, + result: Promise.reject(), + } as NgbModalRef); - component.viewGraph(); - tick(); + component.viewGraph(); + tick(); - expect(ngbModal.open).toHaveBeenCalled(); - })); + expect(ngbModal.open).toHaveBeenCalled(); + })); it('should be able to get length of prerequisites', () => { component.topicNameToPrerequisiteTopicNames = { 'Dummy topic 1': [], 'Dummy topic 2': ['Dummy topic 1'], - 'Dummy topic 3': ['Dummy topic 1'] + 'Dummy topic 3': ['Dummy topic 1'], }; expect(component.getPrerequisiteLength('Dummy topic 1')).toEqual(0); diff --git a/core/templates/pages/classroom-admin-page/classroom-admin-page.component.ts b/core/templates/pages/classroom-admin-page/classroom-admin-page.component.ts index fb8f7abf9c0b..f2c53642df30 100644 --- a/core/templates/pages/classroom-admin-page/classroom-admin-page.component.ts +++ b/core/templates/pages/classroom-admin-page/classroom-admin-page.component.ts @@ -17,21 +17,28 @@ */ import cloneDeep from 'lodash/cloneDeep'; -import { Component, OnInit } from '@angular/core'; -import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; -import { AlertsService } from 'services/alerts.service'; -import { AppConstants } from 'app.constants'; -import { ClassroomBackendApiService, ClassroomBackendDict, ClassroomDict } from '../../domain/classroom/classroom-backend-api.service'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { ClassroomEditorConfirmModalComponent } from './modals/classroom-editor-confirm-modal.component'; -import { DeleteClassroomConfirmModalComponent } from './modals/delete-classroom-confirm-modal.component'; -import { CreateNewClassroomModalComponent } from './modals/create-new-classroom-modal.component'; -import { DeleteTopicFromClassroomModalComponent } from './modals/delete-topic-from-classroom-modal.component'; -import { EditableTopicBackendApiService } from 'domain/topic/editable-topic-backend-api.service'; -import { TopicsDependencyGraphModalComponent } from './modals/topic-dependency-graph-viz-modal.component'; -import { ExistingClassroomData, TopicIdToPrerequisiteTopicIds, TopicIdToTopicName } from './existing-classroom.model'; -import { ClassroomAdminDataService } from './services/classroom-admin-data.service'; - +import {Component, OnInit} from '@angular/core'; +import {CdkDragDrop, moveItemInArray} from '@angular/cdk/drag-drop'; +import {AlertsService} from 'services/alerts.service'; +import {AppConstants} from 'app.constants'; +import { + ClassroomBackendApiService, + ClassroomBackendDict, + ClassroomDict, +} from '../../domain/classroom/classroom-backend-api.service'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {ClassroomEditorConfirmModalComponent} from './modals/classroom-editor-confirm-modal.component'; +import {DeleteClassroomConfirmModalComponent} from './modals/delete-classroom-confirm-modal.component'; +import {CreateNewClassroomModalComponent} from './modals/create-new-classroom-modal.component'; +import {DeleteTopicFromClassroomModalComponent} from './modals/delete-topic-from-classroom-modal.component'; +import {EditableTopicBackendApiService} from 'domain/topic/editable-topic-backend-api.service'; +import {TopicsDependencyGraphModalComponent} from './modals/topic-dependency-graph-viz-modal.component'; +import { + ExistingClassroomData, + TopicIdToPrerequisiteTopicIds, + TopicIdToTopicName, +} from './existing-classroom.model'; +import {ClassroomAdminDataService} from './services/classroom-admin-data.service'; export interface TopicNameToPrerequisiteTopicNames { [topicName: string]: string[]; @@ -93,23 +100,23 @@ export class ClassroomAdminPageComponent implements OnInit { for (let topicName of topicNames) { if ( topicName !== currentTopicName && - this.topicNameToPrerequisiteTopicNames[currentTopicName] - .indexOf(topicName) === -1 + this.topicNameToPrerequisiteTopicNames[currentTopicName].indexOf( + topicName + ) === -1 ) { this.eligibleTopicNamesForPrerequisites.push(topicName); } } - this.tempEligibleTopicNamesForPrerequisites = ( - this.eligibleTopicNamesForPrerequisites); + this.tempEligibleTopicNamesForPrerequisites = + this.eligibleTopicNamesForPrerequisites; this.currentTopicOnEdit = currentTopicName; } onPrerequisiteInputChange(): void { - this.tempEligibleTopicNamesForPrerequisites = ( - this.eligibleTopicNamesForPrerequisites.filter( - option => option.includes(this.prerequisiteInput) - ) - ); + this.tempEligibleTopicNamesForPrerequisites = + this.eligibleTopicNamesForPrerequisites.filter(option => + option.includes(this.prerequisiteInput) + ); } getClassroomData(classroomId: string): void { @@ -118,8 +125,8 @@ export class ClassroomAdminPageComponent implements OnInit { } if ( - this.tempClassroomData && ( - this.tempClassroomData.getClassroomId() === classroomId) && + this.tempClassroomData && + this.tempClassroomData.getClassroomId() === classroomId && this.classroomViewerMode ) { this.classroomDetailsIsShown = false; @@ -132,41 +139,49 @@ export class ClassroomAdminPageComponent implements OnInit { this.classroomBackendApiService.getClassroomDataAsync(classroomId).then( response => { this.classroomData = ExistingClassroomData.createClassroomFromDict( - cloneDeep(response.classroomDict)); + cloneDeep(response.classroomDict) + ); this.tempClassroomData = ExistingClassroomData.createClassroomFromDict( - cloneDeep(response.classroomDict)); + cloneDeep(response.classroomDict) + ); this.classroomDataIsChanged = false; - this.existingClassroomNames = ( - Object.values(this.classroomIdToClassroomName)); + this.existingClassroomNames = Object.values( + this.classroomIdToClassroomName + ); const index = this.existingClassroomNames.indexOf( - this.tempClassroomData.getClassroomName()); + this.tempClassroomData.getClassroomName() + ); this.existingClassroomNames.splice(index, 1); this.classroomDetailsIsShown = true; this.classroomViewerMode = true; - this.classroomAdminDataService.existingClassroomNames = ( - this.existingClassroomNames); + this.classroomAdminDataService.existingClassroomNames = + this.existingClassroomNames; this.classroomAdminDataService.validateClassroom( - this.tempClassroomData, this.classroomData); + this.tempClassroomData, + this.classroomData + ); this.setTopicDependencyByTopicName( - this.tempClassroomData.getTopicIdToPrerequisiteTopicId()); - }, (errorResponse) => { - if ( - AppConstants.FATAL_ERROR_CODES.indexOf( - errorResponse) !== -1) { + this.tempClassroomData.getTopicIdToPrerequisiteTopicId() + ); + }, + errorResponse => { + if (AppConstants.FATAL_ERROR_CODES.indexOf(errorResponse) !== -1) { this.alertsService.addWarning('Failed to get classroom data'); } - }); + } + ); } getAllClassroomIdToClassroomName(): void { this.classroomBackendApiService - .getAllClassroomIdToClassroomNameDictAsync().then(response => { + .getAllClassroomIdToClassroomNameDictAsync() + .then(response => { this.pageIsInitialized = true; this.classroomIdToClassroomName = response; this.classroomCount = Object.keys(response).length; @@ -174,28 +189,23 @@ export class ClassroomAdminPageComponent implements OnInit { } updateClassroomField(): void { - const classroomNameIsChanged = ( + const classroomNameIsChanged = this.tempClassroomData.getClassroomName() !== - this.classroomData.getClassroomName() - ); - const classroomUrlIsChanged = ( + this.classroomData.getClassroomName(); + const classroomUrlIsChanged = this.tempClassroomData.getClassroomUrlFragment() !== - this.classroomData.getClassroomUrlFragment() - ); - const classroomTopicListIntroIsChanged = ( + this.classroomData.getClassroomUrlFragment(); + const classroomTopicListIntroIsChanged = this.tempClassroomData.getTopicListIntro() !== - this.classroomData.getTopicListIntro() - ); - const classroomCourseDetailsIsChanged = ( + this.classroomData.getTopicListIntro(); + const classroomCourseDetailsIsChanged = this.tempClassroomData.getCourseDetails() !== - this.classroomData.getCourseDetails() - ); - const topicDependencyIsChanged = ( - JSON.stringify( - this.tempClassroomData.getTopicIdToPrerequisiteTopicId()) !== + this.classroomData.getCourseDetails(); + const topicDependencyIsChanged = JSON.stringify( - this.classroomData.getTopicIdToPrerequisiteTopicId()) - ); + this.tempClassroomData.getTopicIdToPrerequisiteTopicId() + ) !== + JSON.stringify(this.classroomData.getTopicIdToPrerequisiteTopicId()); if ( classroomNameIsChanged || @@ -211,57 +221,69 @@ export class ClassroomAdminPageComponent implements OnInit { } convertClassroomDictToBackendForm( - classroomDict: ClassroomDict): ClassroomBackendDict { + classroomDict: ClassroomDict + ): ClassroomBackendDict { return { classroom_id: classroomDict.classroomId, name: classroomDict.name, url_fragment: classroomDict.urlFragment, course_details: classroomDict.courseDetails, topic_list_intro: classroomDict.topicListIntro, - topic_id_to_prerequisite_topic_ids: ( - classroomDict.topicIdToPrerequisiteTopicIds) + topic_id_to_prerequisite_topic_ids: + classroomDict.topicIdToPrerequisiteTopicIds, }; } saveClassroomData(classroomId: string): void { this.classroomDataSaveInProgress = true; const backendDict = this.convertClassroomDictToBackendForm( - this.tempClassroomData.getClassroomDict()); + this.tempClassroomData.getClassroomDict() + ); this.openClassroomInViewerMode(); this.classroomDataIsChanged = false; - this.classroomBackendApiService.updateClassroomDataAsync( - classroomId, backendDict).then(() => { - this.classroomIdToClassroomName[ - this.tempClassroomData.getClassroomId()] = ( - this.tempClassroomData.getClassroomName() + this.classroomBackendApiService + .updateClassroomDataAsync(classroomId, backendDict) + .then( + () => { + this.classroomIdToClassroomName[ + this.tempClassroomData.getClassroomId() + ] = this.tempClassroomData.getClassroomName(); + this.classroomData = cloneDeep(this.tempClassroomData); + this.classroomDataSaveInProgress = false; + }, + () => { + this.tempClassroomData = cloneDeep(this.classroomData); + this.setTopicDependencyByTopicName( + this.tempClassroomData.getTopicIdToPrerequisiteTopicId() + ); + } ); - this.classroomData = cloneDeep(this.tempClassroomData); - this.classroomDataSaveInProgress = false; - }, () => { - this.tempClassroomData = cloneDeep(this.classroomData); - this.setTopicDependencyByTopicName( - this.tempClassroomData.getTopicIdToPrerequisiteTopicId()); - }); } deleteClassroom(classroomId: string): void { - let modalRef: NgbModalRef = this.ngbModal. - open(DeleteClassroomConfirmModalComponent, { - backdrop: 'static' - }); - modalRef.result.then(() => { - this.classroomBackendApiService.deleteClassroomAsync(classroomId).then( - () => { - delete this.classroomIdToClassroomName[classroomId]; - this.classroomCount--; - }); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is - // clicked. No further action is needed. - }); + let modalRef: NgbModalRef = this.ngbModal.open( + DeleteClassroomConfirmModalComponent, + { + backdrop: 'static', + } + ); + modalRef.result.then( + () => { + this.classroomBackendApiService + .deleteClassroomAsync(classroomId) + .then(() => { + delete this.classroomIdToClassroomName[classroomId]; + this.classroomCount--; + }); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is + // clicked. No further action is needed. + } + ); } openClassroomInEditorMode(): void { @@ -276,23 +298,29 @@ export class ClassroomAdminPageComponent implements OnInit { closeClassroomConfigEditor(): void { if (this.classroomDataIsChanged) { - let modalRef: NgbModalRef = this.ngbModal. - open(ClassroomEditorConfirmModalComponent, { - backdrop: 'static' - }); - modalRef.result.then(() => { - this.tempClassroomData = cloneDeep(this.classroomData); - this.setTopicDependencyByTopicName( - this.tempClassroomData.getTopicIdToPrerequisiteTopicId()); - - this.classroomDataIsChanged = false; - this.classroomAdminDataService.reinitializeErrorMsgs(); - this.openClassroomInViewerMode(); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is - // clicked. No further action is needed. - }); + let modalRef: NgbModalRef = this.ngbModal.open( + ClassroomEditorConfirmModalComponent, + { + backdrop: 'static', + } + ); + modalRef.result.then( + () => { + this.tempClassroomData = cloneDeep(this.classroomData); + this.setTopicDependencyByTopicName( + this.tempClassroomData.getTopicIdToPrerequisiteTopicId() + ); + + this.classroomDataIsChanged = false; + this.classroomAdminDataService.reinitializeErrorMsgs(); + this.openClassroomInViewerMode(); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is + // clicked. No further action is needed. + } + ); } else { this.openClassroomInViewerMode(); } @@ -302,20 +330,25 @@ export class ClassroomAdminPageComponent implements OnInit { createNewClassroom(): void { this.classroomViewerMode = false; this.classroomDetailsIsShown = false; - let modalRef: NgbModalRef = this.ngbModal. - open(CreateNewClassroomModalComponent, { - backdrop: 'static' - }); - modalRef.componentInstance.existingClassroomNames = ( - Object.values(this.classroomIdToClassroomName) + let modalRef: NgbModalRef = this.ngbModal.open( + CreateNewClassroomModalComponent, + { + backdrop: 'static', + } + ); + modalRef.componentInstance.existingClassroomNames = Object.values( + this.classroomIdToClassroomName + ); + modalRef.result.then( + classroomDict => { + this.classroomIdToClassroomName[classroomDict.classroom_id] = + classroomDict.name; + this.classroomCount++; + }, + () => { + this.classroomAdminDataService.reinitializeErrorMsgs(); + } ); - modalRef.result.then((classroomDict) => { - this.classroomIdToClassroomName[classroomDict.classroom_id] = ( - classroomDict.name); - this.classroomCount++; - }, () => { - this.classroomAdminDataService.reinitializeErrorMsgs(); - }); } ngOnInit(): void { @@ -323,57 +356,61 @@ export class ClassroomAdminPageComponent implements OnInit { } setTopicDependencyByTopicName( - topicIdToPrerequisiteTopicIds: TopicIdToPrerequisiteTopicIds + topicIdToPrerequisiteTopicIds: TopicIdToPrerequisiteTopicIds ): void { this.topicDependencyIsLoaded = false; let topicIds = Object.keys(topicIdToPrerequisiteTopicIds); - this.editableTopicBackendApiService.getTopicIdToTopicNameAsync( - topicIds).then(topicIdsToTopicName => { - this.topicNameToPrerequisiteTopicNames = {}; + this.editableTopicBackendApiService + .getTopicIdToTopicNameAsync(topicIds) + .then(topicIdsToTopicName => { + this.topicNameToPrerequisiteTopicNames = {}; - for (let currentTopicId in topicIdToPrerequisiteTopicIds) { - let currentTopicName = topicIdsToTopicName[currentTopicId]; + for (let currentTopicId in topicIdToPrerequisiteTopicIds) { + let currentTopicName = topicIdsToTopicName[currentTopicId]; - let prerequisiteTopicIds = ( - topicIdToPrerequisiteTopicIds[currentTopicId]); - let prerequisiteTopicNames = []; + let prerequisiteTopicIds = + topicIdToPrerequisiteTopicIds[currentTopicId]; + let prerequisiteTopicNames = []; - for (let topicId of prerequisiteTopicIds) { - prerequisiteTopicNames.push( - topicIdsToTopicName[topicId]); - } + for (let topicId of prerequisiteTopicIds) { + prerequisiteTopicNames.push(topicIdsToTopicName[topicId]); + } - this.tempClassroomData._topicIdToTopicName = topicIdsToTopicName; + this.tempClassroomData._topicIdToTopicName = topicIdsToTopicName; - this.topicNameToPrerequisiteTopicNames[currentTopicName] = ( - prerequisiteTopicNames); - this.topicIdsToTopicName = topicIdsToTopicName; - this.topicNames = Object.values(this.topicIdsToTopicName); - this.topicDependencyIsLoaded = true; - } - }); + this.topicNameToPrerequisiteTopicNames[currentTopicName] = + prerequisiteTopicNames; + this.topicIdsToTopicName = topicIdsToTopicName; + this.topicNames = Object.values(this.topicIdsToTopicName); + this.topicDependencyIsLoaded = true; + } + }); } addTopicId(topicId: string): void { - this.editableTopicBackendApiService.getTopicIdToTopicNameAsync( - [topicId]).then(topicIdToTopicName => { - const topicName = topicIdToTopicName[topicId]; - - this.topicIdsToTopicName[topicId] = topicName; - this.tempClassroomData.addNewTopicId(topicId); - this.topicNameToPrerequisiteTopicNames[topicName] = []; - this.topicNames.push(topicName); - this.topicDependencyIsLoaded = true; - - this.classroomDataIsChanged = true; - this.newTopicCanBeAdded = false; - this.topicWithGivenIdExists = true; - - this.newTopicId = ''; - }, () => { - this.topicWithGivenIdExists = false; - }); + this.editableTopicBackendApiService + .getTopicIdToTopicNameAsync([topicId]) + .then( + topicIdToTopicName => { + const topicName = topicIdToTopicName[topicId]; + + this.topicIdsToTopicName[topicId] = topicName; + this.tempClassroomData.addNewTopicId(topicId); + this.topicNameToPrerequisiteTopicNames[topicName] = []; + this.topicNames.push(topicName); + this.topicDependencyIsLoaded = true; + + this.classroomDataIsChanged = true; + this.newTopicCanBeAdded = false; + this.topicWithGivenIdExists = true; + + this.newTopicId = ''; + }, + () => { + this.topicWithGivenIdExists = false; + } + ); } showNewTopicInputField(): void { @@ -404,45 +441,60 @@ export class ClassroomAdminPageComponent implements OnInit { } addDependencyForTopic( - currentTopicName: string, prerequisiteTopicName: string): void { + currentTopicName: string, + prerequisiteTopicName: string + ): void { let prerequisiteTopicNames = cloneDeep( - this.topicNameToPrerequisiteTopicNames[currentTopicName]); + this.topicNameToPrerequisiteTopicNames[currentTopicName] + ); let currentTopicId = this.getTopicIdFromTopicName(currentTopicName); let prerequisiteTopicId = this.getTopicIdFromTopicName( - prerequisiteTopicName); + prerequisiteTopicName + ); if (prerequisiteTopicNames.indexOf(prerequisiteTopicName) !== -1) { return; } this.topicNameToPrerequisiteTopicNames[currentTopicName].push( - prerequisiteTopicName); + prerequisiteTopicName + ); this.topicNameToPrerequisiteTopicNames[currentTopicName].sort(); this.tempClassroomData.addPrerequisiteTopicId( - currentTopicId, prerequisiteTopicId); + currentTopicId, + prerequisiteTopicId + ); this.classroomAdminDataService.validateClassroom( - this.tempClassroomData, this.classroomData); + this.tempClassroomData, + this.classroomData + ); this.updateClassroomField(); } removeDependencyFromTopic( - currentTopicName: string, prerequisiteTopicName: string + currentTopicName: string, + prerequisiteTopicName: string ): void { let currentTopicId = this.getTopicIdFromTopicName(currentTopicName); let prerequisiteTopicId = this.getTopicIdFromTopicName( - prerequisiteTopicName); + prerequisiteTopicName + ); this.tempClassroomData.removeDependency( - currentTopicId, prerequisiteTopicId); + currentTopicId, + prerequisiteTopicId + ); - let prerequisiteTopicNames = ( - this.topicNameToPrerequisiteTopicNames[currentTopicName]); + let prerequisiteTopicNames = + this.topicNameToPrerequisiteTopicNames[currentTopicName]; const index = prerequisiteTopicNames.indexOf(prerequisiteTopicName); prerequisiteTopicNames.splice(index, 1); this.classroomAdminDataService.validateClassroom( - this.tempClassroomData, this.classroomData); + this.tempClassroomData, + this.classroomData + ); this.updateClassroomField(); } @@ -464,36 +516,42 @@ export class ClassroomAdminPageComponent implements OnInit { } } - let modalRef: NgbModalRef = this.ngbModal. - open(DeleteTopicFromClassroomModalComponent, { - backdrop: 'static' - }); - modalRef.componentInstance.prerequisiteTopics = ( - Object.values(childTopicNodes) + let modalRef: NgbModalRef = this.ngbModal.open( + DeleteTopicFromClassroomModalComponent, + { + backdrop: 'static', + } ); + modalRef.componentInstance.prerequisiteTopics = + Object.values(childTopicNodes); modalRef.componentInstance.topicName = topicNameToDelete; - modalRef.result.then(() => { - const topicId = this.getTopicIdFromTopicName(topicNameToDelete); - this.tempClassroomData.removeTopic(topicId); + modalRef.result.then( + () => { + const topicId = this.getTopicIdFromTopicName(topicNameToDelete); + this.tempClassroomData.removeTopic(topicId); - delete this.topicNameToPrerequisiteTopicNames[topicNameToDelete]; - delete this.topicIdsToTopicName[topicId]; + delete this.topicNameToPrerequisiteTopicNames[topicNameToDelete]; + delete this.topicIdsToTopicName[topicId]; - this.topicNames = Object.keys(this.topicNameToPrerequisiteTopicNames); + this.topicNames = Object.keys(this.topicNameToPrerequisiteTopicNames); - this.classroomAdminDataService.validateClassroom( - this.tempClassroomData, this.classroomData); + this.classroomAdminDataService.validateClassroom( + this.tempClassroomData, + this.classroomData + ); - this.classroomDataIsChanged = true; + this.classroomDataIsChanged = true; - if (this.tempClassroomData.getTopicsCount() === 0) { - this.topicDependencyIsLoaded = false; + if (this.tempClassroomData.getTopicsCount() === 0) { + this.topicDependencyIsLoaded = false; + } + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is + // clicked. No further action is needed. } - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is - // clicked. No further action is needed. - }); + ); } drop(event: CdkDragDrop): void { @@ -502,39 +560,45 @@ export class ClassroomAdminPageComponent implements OnInit { let tempTopicIdToPrerequisiteTopicIds: TopicIdToPrerequisiteTopicIds = {}; for (let topicName of this.topicNames) { - const prerequisiteTopicNames = ( - this.topicNameToPrerequisiteTopicNames[topicName]); + const prerequisiteTopicNames = + this.topicNameToPrerequisiteTopicNames[topicName]; const topicId = this.getTopicIdFromTopicName(topicName); let prerequisiteTopicIds = []; for (let prerequisiteTopicName of prerequisiteTopicNames) { - prerequisiteTopicIds.push(this.getTopicIdFromTopicName( - prerequisiteTopicName)); + prerequisiteTopicIds.push( + this.getTopicIdFromTopicName(prerequisiteTopicName) + ); } tempTopicIdToPrerequisiteTopicIds[topicId] = prerequisiteTopicIds; } this.tempClassroomData.setTopicIdToPrerequisiteTopicId( - tempTopicIdToPrerequisiteTopicIds); + tempTopicIdToPrerequisiteTopicIds + ); this.updateClassroomField(); } viewGraph(): void { - let modalRef: NgbModalRef = this.ngbModal. - open(TopicsDependencyGraphModalComponent, { + let modalRef: NgbModalRef = this.ngbModal.open( + TopicsDependencyGraphModalComponent, + { backdrop: true, - windowClass: 'oppia-large-modal-window' - }); - modalRef.componentInstance.topicIdToPrerequisiteTopicIds = ( - this.tempClassroomData.getTopicIdToPrerequisiteTopicId()); + windowClass: 'oppia-large-modal-window', + } + ); + modalRef.componentInstance.topicIdToPrerequisiteTopicIds = + this.tempClassroomData.getTopicIdToPrerequisiteTopicId(); modalRef.componentInstance.topicIdToTopicName = this.topicIdsToTopicName; - modalRef.result.then(() => { - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is - // clicked. No further action is needed. - }); + modalRef.result.then( + () => {}, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is + // clicked. No further action is needed. + } + ); } getPrerequisiteLength(topicName: string): number { diff --git a/core/templates/pages/classroom-admin-page/classroom-admin-page.import.ts b/core/templates/pages/classroom-admin-page/classroom-admin-page.import.ts index 2bdbe8fad2e7..3b33869485c8 100644 --- a/core/templates/pages/classroom-admin-page/classroom-admin-page.import.ts +++ b/core/templates/pages/classroom-admin-page/classroom-admin-page.import.ts @@ -22,9 +22,14 @@ import 'zone.js'; import 'angular-ui-validate'; angular.module('oppia', [ - require('angular-cookies'), 'ngAnimate', - 'ngMaterial', 'ngSanitize', 'ngTouch', 'pascalprecht.translate', - 'ui.bootstrap', 'ui.validate' + require('angular-cookies'), + 'ngAnimate', + 'ngMaterial', + 'ngSanitize', + 'ngTouch', + 'pascalprecht.translate', + 'ui.bootstrap', + 'ui.validate', ]); require('Polyfills.ts'); diff --git a/core/templates/pages/classroom-admin-page/classroom-admin-page.module.ts b/core/templates/pages/classroom-admin-page/classroom-admin-page.module.ts index 658ab8797681..4c9b0e2ccc0b 100644 --- a/core/templates/pages/classroom-admin-page/classroom-admin-page.module.ts +++ b/core/templates/pages/classroom-admin-page/classroom-admin-page.module.ts @@ -16,27 +16,25 @@ * @fileoverview Module for the classroom-admin page. */ +import {NgModule} from '@angular/core'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {MatCardModule} from '@angular/material/card'; +import {MatTooltipModule} from '@angular/material/tooltip'; +import {RouterModule} from '@angular/router'; -import { NgModule } from '@angular/core'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { MatCardModule } from '@angular/material/card'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { RouterModule } from '@angular/router'; - - -import { SharedComponentsModule } from 'components/shared-component.module'; -import { ClassroomAdminNavbarComponent } from './navbar/classroom-admin-navbar.component'; -import { ClassroomAdminPageComponent } from './classroom-admin-page.component'; -import { ToastrModule } from 'ngx-toastr'; -import { toastrConfig } from 'pages/oppia-root/app.module'; -import { ClassroomEditorConfirmModalComponent } from './modals/classroom-editor-confirm-modal.component'; -import { DeleteClassroomConfirmModalComponent } from './modals/delete-classroom-confirm-modal.component'; -import { CreateNewClassroomModalComponent } from './modals/create-new-classroom-modal.component'; -import { DeleteTopicFromClassroomModalComponent } from './modals/delete-topic-from-classroom-modal.component'; -import { TopicsDependencyGraphModalComponent } from './modals/topic-dependency-graph-viz-modal.component'; -import { CommonModule } from '@angular/common'; -import { ClassroomAdminPageRootComponent } from './classroom-admin-page-root.component'; -import { ClassroomAdminAuthGuard } from './classroom-admin-auth.guard'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {ClassroomAdminNavbarComponent} from './navbar/classroom-admin-navbar.component'; +import {ClassroomAdminPageComponent} from './classroom-admin-page.component'; +import {ToastrModule} from 'ngx-toastr'; +import {toastrConfig} from 'pages/oppia-root/app.module'; +import {ClassroomEditorConfirmModalComponent} from './modals/classroom-editor-confirm-modal.component'; +import {DeleteClassroomConfirmModalComponent} from './modals/delete-classroom-confirm-modal.component'; +import {CreateNewClassroomModalComponent} from './modals/create-new-classroom-modal.component'; +import {DeleteTopicFromClassroomModalComponent} from './modals/delete-topic-from-classroom-modal.component'; +import {TopicsDependencyGraphModalComponent} from './modals/topic-dependency-graph-viz-modal.component'; +import {CommonModule} from '@angular/common'; +import {ClassroomAdminPageRootComponent} from './classroom-admin-page-root.component'; +import {ClassroomAdminAuthGuard} from './classroom-admin-auth.guard'; @NgModule({ imports: [ diff --git a/core/templates/pages/classroom-admin-page/existing-classroom.model.spec.ts b/core/templates/pages/classroom-admin-page/existing-classroom.model.spec.ts index efede3d5d94d..12f33ebc3851 100644 --- a/core/templates/pages/classroom-admin-page/existing-classroom.model.spec.ts +++ b/core/templates/pages/classroom-admin-page/existing-classroom.model.spec.ts @@ -16,17 +16,15 @@ * @fileoverview Tests for existing classroom model. */ - -import { TestBed } from '@angular/core/testing'; -import { ExistingClassroomData } from './existing-classroom.model'; - +import {TestBed} from '@angular/core/testing'; +import {ExistingClassroomData} from './existing-classroom.model'; describe('Classroom admin model', () => { let existingClassroomData: ExistingClassroomData; beforeEach(() => { TestBed.configureTestingModule({ imports: [], - providers: [] + providers: [], }); existingClassroomData = new ExistingClassroomData( @@ -41,22 +39,26 @@ describe('Classroom admin model', () => { it('should be able to get and set course details', () => { expect(existingClassroomData.getCourseDetails()).toEqual( - 'Curated math foundations course.'); + 'Curated math foundations course.' + ); existingClassroomData.setCourseDetails('Test data for course details.'); expect(existingClassroomData.getCourseDetails()).toEqual( - 'Test data for course details.'); + 'Test data for course details.' + ); }); it('should be able to get and set topic list intro', () => { expect(existingClassroomData.getTopicListIntro()).toEqual( - 'Start from the basics with our first topic.'); + 'Start from the basics with our first topic.' + ); existingClassroomData.setTopicListIntro('Test data for topic list intro.'); expect(existingClassroomData.getTopicListIntro()).toEqual( - 'Test data for topic list intro.'); + 'Test data for topic list intro.' + ); }); it('should be able to get and set topic dependency', () => { @@ -65,14 +67,16 @@ describe('Classroom admin model', () => { const sampleTopicIdToprerequisiteTopicIds = { topic1: [], topic2: ['topic1'], - topic3: ['topic2'] + topic3: ['topic2'], }; existingClassroomData.setTopicIdToPrerequisiteTopicId( - sampleTopicIdToprerequisiteTopicIds); + sampleTopicIdToprerequisiteTopicIds + ); expect(existingClassroomData.getTopicIdToPrerequisiteTopicId()).toEqual( - sampleTopicIdToprerequisiteTopicIds); + sampleTopicIdToprerequisiteTopicIds + ); }); it('should be able to create existing classroom model from dict', () => { @@ -84,12 +88,12 @@ describe('Classroom admin model', () => { topicListIntro: 'Test topic intro', topicIdToPrerequisiteTopicIds: { topic1: [], - topic2: ['topic1'] - } + topic2: ['topic1'], + }, }; - let classroom: ExistingClassroomData = ( - ExistingClassroomData.createClassroomFromDict(classroomDict)); + let classroom: ExistingClassroomData = + ExistingClassroomData.createClassroomFromDict(classroomDict); expect(classroom.getClassroomId()).toEqual('pysicsClassroomId'); expect(classroom.getClassroomName()).toEqual('physics'); @@ -98,7 +102,7 @@ describe('Classroom admin model', () => { expect(classroom.getTopicListIntro()).toEqual('Test topic intro'); expect(classroom.getTopicIdToPrerequisiteTopicId()).toEqual({ topic1: [], - topic2: ['topic1'] + topic2: ['topic1'], }); }); @@ -109,18 +113,19 @@ describe('Classroom admin model', () => { urlFragment: 'math', courseDetails: 'Curated math foundations course.', topicListIntro: 'Start from the basics with our first topic.', - topicIdToPrerequisiteTopicIds: {} + topicIdToPrerequisiteTopicIds: {}, }; expect(existingClassroomData.getClassroomDict()).toEqual( - expectedClassroomDict); + expectedClassroomDict + ); }); it('should not present error for valid dependency graph', () => { existingClassroomData.setTopicIdToPrerequisiteTopicId({ topic_id_1: ['topic_id_2', 'topic_id_3'], topic_id_2: [], - topic_id_3: ['topic_id_2'] + topic_id_3: ['topic_id_2'], }); expect(existingClassroomData.validateDependencyGraph()).toEqual(''); @@ -128,7 +133,7 @@ describe('Classroom admin model', () => { existingClassroomData.setTopicIdToPrerequisiteTopicId({ topic_id_1: [], topic_id_2: ['topic_id_1'], - topic_id_3: ['topic_id_2'] + topic_id_3: ['topic_id_2'], }); expect(existingClassroomData.validateDependencyGraph()).toEqual(''); @@ -136,7 +141,7 @@ describe('Classroom admin model', () => { existingClassroomData.setTopicIdToPrerequisiteTopicId({ topic_id_1: [], topic_id_2: ['topic_id_1'], - topic_id_3: ['topic_id_2', 'topic_id_1'] + topic_id_3: ['topic_id_2', 'topic_id_1'], }); expect(existingClassroomData.validateDependencyGraph()).toEqual(''); @@ -146,15 +151,18 @@ describe('Classroom admin model', () => { existingClassroomData.setTopicIdToPrerequisiteTopicId({ topic_id_1: ['topic_id_3'], topic_id_2: ['topic_id_1'], - topic_id_3: ['topic_id_2'] + topic_id_3: ['topic_id_2'], }); existingClassroomData.setTopicIdToTopicName({ topic_id_1: 'Topic1', topic_id_2: 'Topic2', - topic_id_3: 'Topic3' + topic_id_3: 'Topic3', }); - const errorMsg = existingClassroomData.generateGraphErrorMsg( - ['Topic2', 'Topic3', 'Topic1']); + const errorMsg = existingClassroomData.generateGraphErrorMsg([ + 'Topic2', + 'Topic3', + 'Topic1', + ]); expect(existingClassroomData.validateDependencyGraph()).toEqual(errorMsg); }); @@ -163,10 +171,11 @@ describe('Classroom admin model', () => { existingClassroomData.setTopicIdToPrerequisiteTopicId({ topic_id_1: ['topic_id_2', 'topic_id_3'], topic_id_2: [], - topic_id_3: ['topic_id_2'] + topic_id_3: ['topic_id_2'], }); expect(existingClassroomData.getPrerequisiteTopicIds('topic_id_1')).toEqual( - ['topic_id_2', 'topic_id_3']); + ['topic_id_2', 'topic_id_3'] + ); }); }); diff --git a/core/templates/pages/classroom-admin-page/existing-classroom.model.ts b/core/templates/pages/classroom-admin-page/existing-classroom.model.ts index 6df7526476d2..41fa5b6a2d9d 100644 --- a/core/templates/pages/classroom-admin-page/existing-classroom.model.ts +++ b/core/templates/pages/classroom-admin-page/existing-classroom.model.ts @@ -16,11 +16,10 @@ * @fileoverview Existing classroom model. */ -import { ClassroomDict } from '../../domain/classroom/classroom-backend-api.service'; -import { NewClassroom, NewClassroomData } from './new-classroom.model'; +import {ClassroomDict} from '../../domain/classroom/classroom-backend-api.service'; +import {NewClassroom, NewClassroomData} from './new-classroom.model'; import cloneDeep from 'lodash/cloneDeep'; - export interface TopicIdToPrerequisiteTopicIds { [topicId: string]: string[]; } @@ -29,7 +28,6 @@ export interface TopicIdToTopicName { [topicId: string]: string; } - interface ExistingClassroom extends NewClassroom { _courseDetails: string; _topicListIntro: string; @@ -45,9 +43,10 @@ interface ExistingClassroom extends NewClassroom { export type ClassroomData = ExistingClassroom | NewClassroom; - -export class ExistingClassroomData extends - NewClassroomData implements ExistingClassroom { +export class ExistingClassroomData + extends NewClassroomData + implements ExistingClassroom +{ _courseDetails: string; _topicListIntro: string; _topicIdToPrerequisiteTopicIds: TopicIdToPrerequisiteTopicIds; @@ -55,12 +54,12 @@ export class ExistingClassroomData extends _topicIdToTopicName!: TopicIdToTopicName; constructor( - classroomId: string, - name: string, - urlFragment: string, - courseDetails: string, - topicListIntro: string, - topicIdToPrerequisiteTopicIds: TopicIdToPrerequisiteTopicIds + classroomId: string, + name: string, + urlFragment: string, + courseDetails: string, + topicListIntro: string, + topicIdToPrerequisiteTopicIds: TopicIdToPrerequisiteTopicIds ) { super(classroomId, name, urlFragment); this._courseDetails = courseDetails; @@ -68,7 +67,8 @@ export class ExistingClassroomData extends this._topicIdToPrerequisiteTopicIds = topicIdToPrerequisiteTopicIds; this._topicsCountInClassroom = 0; this._topicsCountInClassroom = Object.keys( - this._topicIdToPrerequisiteTopicIds).length; + this._topicIdToPrerequisiteTopicIds + ).length; } getCourseDetails(): string { @@ -92,13 +92,13 @@ export class ExistingClassroomData extends } setTopicIdToPrerequisiteTopicId( - topicIdToPrerequisiteTopicIds: TopicIdToPrerequisiteTopicIds + topicIdToPrerequisiteTopicIds: TopicIdToPrerequisiteTopicIds ): void { this._topicIdToPrerequisiteTopicIds = topicIdToPrerequisiteTopicIds; } static createClassroomFromDict( - classroomDict: ClassroomDict + classroomDict: ClassroomDict ): ExistingClassroomData { return new ExistingClassroomData( classroomDict.classroomId, @@ -117,7 +117,7 @@ export class ExistingClassroomData extends urlFragment: this._urlFragment, courseDetails: this._courseDetails, topicListIntro: this._topicListIntro, - topicIdToPrerequisiteTopicIds: this._topicIdToPrerequisiteTopicIds + topicIdToPrerequisiteTopicIds: this._topicIdToPrerequisiteTopicIds, }; return classroomDict; } @@ -125,13 +125,12 @@ export class ExistingClassroomData extends generateGraphErrorMsg(circularlyDependentTopics: string[]): string { let errorMsg = 'There is a cycle in the prerequisite dependencies. \n'; for (let topicName of circularlyDependentTopics) { - errorMsg += (topicName + ' \u2192 '); + errorMsg += topicName + ' \u2192 '; } - errorMsg += (circularlyDependentTopics[0] + '.'); - errorMsg += ( + errorMsg += circularlyDependentTopics[0] + '.'; + errorMsg += ' Please remove the circular dependency. You can click ' + - 'on "View Graph" below to see a visualization of the dependencies.' - ); + 'on "View Graph" below to see a visualization of the dependencies.'; return errorMsg; } @@ -140,7 +139,8 @@ export class ExistingClassroomData extends for (let currentTopicId in this._topicIdToPrerequisiteTopicIds) { topicIdToChildTopicId = {}; let ancestors = cloneDeep( - this._topicIdToPrerequisiteTopicIds[currentTopicId]); + this._topicIdToPrerequisiteTopicIds[currentTopicId] + ); for (let topicId of ancestors) { topicIdToChildTopicId[topicId] = currentTopicId; @@ -173,20 +173,19 @@ export class ExistingClassroomData extends ancestors.splice(lengthOfAncestor - 1, 1); if ( - visitedTopicIdsForCurrentTopic.indexOf( - lastTopicIdInAncestor) !== -1 + visitedTopicIdsForCurrentTopic.indexOf(lastTopicIdInAncestor) !== -1 ) { continue; } ancestors = ancestors.concat( - this._topicIdToPrerequisiteTopicIds[lastTopicIdInAncestor]); + this._topicIdToPrerequisiteTopicIds[lastTopicIdInAncestor] + ); visitedTopicIdsForCurrentTopic.push(lastTopicIdInAncestor); - for ( - let topicId of - this._topicIdToPrerequisiteTopicIds[lastTopicIdInAncestor] - ) { + for (let topicId of this._topicIdToPrerequisiteTopicIds[ + lastTopicIdInAncestor + ]) { topicIdToChildTopicId[topicId] = lastTopicIdInAncestor; } } @@ -205,23 +204,20 @@ export class ExistingClassroomData extends } addPrerequisiteTopicId( - currentTopicId: string, - prerequisiteTopicId: string + currentTopicId: string, + prerequisiteTopicId: string ): void { this._topicIdToPrerequisiteTopicIds[currentTopicId].push( - prerequisiteTopicId); + prerequisiteTopicId + ); } - removeDependency( - currentTopicId: string, - prerequisiteTopicId: string - ): void { - const index = ( + removeDependency(currentTopicId: string, prerequisiteTopicId: string): void { + const index = this._topicIdToPrerequisiteTopicIds[currentTopicId].indexOf( - prerequisiteTopicId) - ); - this._topicIdToPrerequisiteTopicIds[currentTopicId].splice( - index, 1); + prerequisiteTopicId + ); + this._topicIdToPrerequisiteTopicIds[currentTopicId].splice(index, 1); } getPrerequisiteTopicIds(topicId: string): string[] { diff --git a/core/templates/pages/classroom-admin-page/modals/classroom-editor-confirm-modal.component.spec.ts b/core/templates/pages/classroom-admin-page/modals/classroom-editor-confirm-modal.component.spec.ts index b7e3cdf3e618..6cb79065fba0 100644 --- a/core/templates/pages/classroom-admin-page/modals/classroom-editor-confirm-modal.component.spec.ts +++ b/core/templates/pages/classroom-admin-page/modals/classroom-editor-confirm-modal.component.spec.ts @@ -16,10 +16,9 @@ * @fileoverview Tests for classroom editor confirmation modal. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ClassroomEditorConfirmModalComponent } from './classroom-editor-confirm-modal.component'; - +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ClassroomEditorConfirmModalComponent} from './classroom-editor-confirm-modal.component'; describe('Classroom Editor Close Confirmation Modal', () => { let fixture: ComponentFixture; @@ -29,12 +28,8 @@ describe('Classroom Editor Close Confirmation Modal', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - ClassroomEditorConfirmModalComponent - ], - providers: [ - NgbActiveModal, - ] + declarations: [ClassroomEditorConfirmModalComponent], + providers: [NgbActiveModal], }).compileComponents(); })); diff --git a/core/templates/pages/classroom-admin-page/modals/classroom-editor-confirm-modal.component.ts b/core/templates/pages/classroom-admin-page/modals/classroom-editor-confirm-modal.component.ts index 1d57a29fab17..6007dff94eca 100644 --- a/core/templates/pages/classroom-admin-page/modals/classroom-editor-confirm-modal.component.ts +++ b/core/templates/pages/classroom-admin-page/modals/classroom-editor-confirm-modal.component.ts @@ -16,20 +16,16 @@ * @fileoverview Close classroom editor confirmation modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; - +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-classroom-editor-confirm-modal', - templateUrl: './classroom-editor-confirm-modal.component.html' + templateUrl: './classroom-editor-confirm-modal.component.html', }) -export class ClassroomEditorConfirmModalComponent - extends ConfirmOrCancelModal { - constructor( - private ngbActiveModal: NgbActiveModal - ) { +export class ClassroomEditorConfirmModalComponent extends ConfirmOrCancelModal { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } diff --git a/core/templates/pages/classroom-admin-page/modals/create-new-classroom-modal.component.spec.ts b/core/templates/pages/classroom-admin-page/modals/create-new-classroom-modal.component.spec.ts index 8cf0b346fa26..615ea490b438 100644 --- a/core/templates/pages/classroom-admin-page/modals/create-new-classroom-modal.component.spec.ts +++ b/core/templates/pages/classroom-admin-page/modals/create-new-classroom-modal.component.spec.ts @@ -16,15 +16,20 @@ * @fileoverview Tests for new classroom creation modal. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { FormsModule } from '@angular/forms'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { CreateNewClassroomModalComponent } from './create-new-classroom-modal.component'; -import { ClassroomBackendApiService } from '../../../domain/classroom/classroom-backend-api.service'; - +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {FormsModule} from '@angular/forms'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {CreateNewClassroomModalComponent} from './create-new-classroom-modal.component'; +import {ClassroomBackendApiService} from '../../../domain/classroom/classroom-backend-api.service'; describe('Create new topic modal', () => { let fixture: ComponentFixture; @@ -37,7 +42,7 @@ describe('Create new topic modal', () => { return { then: (callback: () => void) => { callback(); - } + }, }; } @@ -45,7 +50,7 @@ describe('Create new topic modal', () => { return { then: (callback: () => void) => { callback(); - } + }, }; } } @@ -53,33 +58,28 @@ describe('Create new topic modal', () => { class MockWindowRef { nativeWindow = { location: { - hostname: '' - } + hostname: '', + }, }; } beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - FormsModule - ], - declarations: [ - CreateNewClassroomModalComponent, - ], + imports: [HttpClientTestingModule, FormsModule], + declarations: [CreateNewClassroomModalComponent], providers: [ NgbActiveModal, { provide: WindowRef, - useClass: MockWindowRef + useClass: MockWindowRef, }, { provide: ClassroomBackendApiService, - useClass: MockClassroomBackendApiService + useClass: MockClassroomBackendApiService, }, - ClassroomBackendApiService + ClassroomBackendApiService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -96,13 +96,14 @@ describe('Create new topic modal', () => { it('should be able to save new classroom name', fakeAsync(() => { spyOn(ngbActiveModal, 'close'); - spyOn(classroomBackendApiService, 'updateClassroomDataAsync') - .and.returnValue(Promise.resolve()); - spyOn( classroomBackendApiService, - 'getNewClassroomIdAsync' - ).and.returnValue(Promise.resolve('newClassroomId')); + 'updateClassroomDataAsync' + ).and.returnValue(Promise.resolve()); + + spyOn(classroomBackendApiService, 'getNewClassroomIdAsync').and.returnValue( + Promise.resolve('newClassroomId') + ); componentInstance.existingClassroomNames = ['math', 'chemistry']; componentInstance.ngOnInit(); @@ -118,7 +119,7 @@ describe('Create new topic modal', () => { url_fragment: 'physics', course_details: '', topic_list_intro: '', - topic_id_to_prerequisite_topic_ids: {} + topic_id_to_prerequisite_topic_ids: {}, }; expect(ngbActiveModal.close).toHaveBeenCalledWith(expectedDefaultClassroom); diff --git a/core/templates/pages/classroom-admin-page/modals/create-new-classroom-modal.component.ts b/core/templates/pages/classroom-admin-page/modals/create-new-classroom-modal.component.ts index f270108af8fe..2102be6b4b76 100644 --- a/core/templates/pages/classroom-admin-page/modals/create-new-classroom-modal.component.ts +++ b/core/templates/pages/classroom-admin-page/modals/create-new-classroom-modal.component.ts @@ -16,20 +16,18 @@ * @fileoverview Create new classroom modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { ClassroomBackendApiService } from '../../../domain/classroom/classroom-backend-api.service'; -import { NewClassroomData } from '../new-classroom.model'; -import { ClassroomAdminDataService } from '../services/classroom-admin-data.service'; - +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {ClassroomBackendApiService} from '../../../domain/classroom/classroom-backend-api.service'; +import {NewClassroomData} from '../new-classroom.model'; +import {ClassroomAdminDataService} from '../services/classroom-admin-data.service'; @Component({ selector: 'oppia-create-new-classroom-modal', - templateUrl: './create-new-classroom-modal.component.html' + templateUrl: './create-new-classroom-modal.component.html', }) -export class CreateNewClassroomModalComponent - extends ConfirmOrCancelModal { +export class CreateNewClassroomModalComponent extends ConfirmOrCancelModal { constructor( private classroomBackendApiService: ClassroomBackendApiService, public classroomAdminDataService: ClassroomAdminDataService, @@ -49,31 +47,32 @@ export class CreateNewClassroomModalComponent this.tempClassroom = new NewClassroomData('', '', ''); this.classroom = new NewClassroomData('', '', ''); - this.classroomAdminDataService.existingClassroomNames = ( - this.existingClassroomNames); + this.classroomAdminDataService.existingClassroomNames = + this.existingClassroomNames; } createClassroom(): void { this.newClassroomCreationInProgress = true; this.classroomUrlFragmentIsDuplicate = false; - this.classroomBackendApiService.getNewClassroomIdAsync().then( - classroomId => { + this.classroomBackendApiService + .getNewClassroomIdAsync() + .then(classroomId => { const defaultClassroomDict = { classroom_id: classroomId, name: this.tempClassroom.getClassroomName(), url_fragment: this.tempClassroom.getClassroomUrlFragment(), course_details: '', topic_list_intro: '', - topic_id_to_prerequisite_topic_ids: {} + topic_id_to_prerequisite_topic_ids: {}, }; - this.classroomBackendApiService.updateClassroomDataAsync( - classroomId, defaultClassroomDict).then(() => { - this.ngbActiveModal.close(defaultClassroomDict); - this.newClassroomCreationInProgress = false; - }); - } - ); + this.classroomBackendApiService + .updateClassroomDataAsync(classroomId, defaultClassroomDict) + .then(() => { + this.ngbActiveModal.close(defaultClassroomDict); + this.newClassroomCreationInProgress = false; + }); + }); } } diff --git a/core/templates/pages/classroom-admin-page/modals/delete-classroom-confirm-modal.component.spec.ts b/core/templates/pages/classroom-admin-page/modals/delete-classroom-confirm-modal.component.spec.ts index b11e33623c6b..2dcbf3a1cdf5 100644 --- a/core/templates/pages/classroom-admin-page/modals/delete-classroom-confirm-modal.component.spec.ts +++ b/core/templates/pages/classroom-admin-page/modals/delete-classroom-confirm-modal.component.spec.ts @@ -16,10 +16,9 @@ * @fileoverview Tests for delete classroom confirmation modal. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { DeleteClassroomConfirmModalComponent } from './delete-classroom-confirm-modal.component'; - +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {DeleteClassroomConfirmModalComponent} from './delete-classroom-confirm-modal.component'; describe('Delete Classroom Confirmation Modal', () => { let fixture: ComponentFixture; @@ -29,12 +28,8 @@ describe('Delete Classroom Confirmation Modal', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - DeleteClassroomConfirmModalComponent - ], - providers: [ - NgbActiveModal, - ] + declarations: [DeleteClassroomConfirmModalComponent], + providers: [NgbActiveModal], }).compileComponents(); })); diff --git a/core/templates/pages/classroom-admin-page/modals/delete-classroom-confirm-modal.component.ts b/core/templates/pages/classroom-admin-page/modals/delete-classroom-confirm-modal.component.ts index 26a4b8b0b181..974a01f764a0 100644 --- a/core/templates/pages/classroom-admin-page/modals/delete-classroom-confirm-modal.component.ts +++ b/core/templates/pages/classroom-admin-page/modals/delete-classroom-confirm-modal.component.ts @@ -16,20 +16,16 @@ * @fileoverview Delete classroom confirmation modal component. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; - +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-delete-classroom-confirm-modal', - templateUrl: './delete-classroom-confirm-modal.component.html' + templateUrl: './delete-classroom-confirm-modal.component.html', }) -export class DeleteClassroomConfirmModalComponent - extends ConfirmOrCancelModal { - constructor( - private ngbActiveModal: NgbActiveModal - ) { +export class DeleteClassroomConfirmModalComponent extends ConfirmOrCancelModal { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } diff --git a/core/templates/pages/classroom-admin-page/modals/delete-topic-from-classroom-modal.component.spec.ts b/core/templates/pages/classroom-admin-page/modals/delete-topic-from-classroom-modal.component.spec.ts index c5b1dfc04d2d..3c37e57d37ae 100644 --- a/core/templates/pages/classroom-admin-page/modals/delete-topic-from-classroom-modal.component.spec.ts +++ b/core/templates/pages/classroom-admin-page/modals/delete-topic-from-classroom-modal.component.spec.ts @@ -16,10 +16,9 @@ * @fileoverview Tests for delete topic from classroom confirmation modal. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { DeleteTopicFromClassroomModalComponent } from './delete-topic-from-classroom-modal.component'; - +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {DeleteTopicFromClassroomModalComponent} from './delete-topic-from-classroom-modal.component'; describe('Delete Topic From Classroom Confirmation Modal', () => { let fixture: ComponentFixture; @@ -29,12 +28,8 @@ describe('Delete Topic From Classroom Confirmation Modal', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - DeleteTopicFromClassroomModalComponent - ], - providers: [ - NgbActiveModal, - ] + declarations: [DeleteTopicFromClassroomModalComponent], + providers: [NgbActiveModal], }).compileComponents(); })); diff --git a/core/templates/pages/classroom-admin-page/modals/delete-topic-from-classroom-modal.component.ts b/core/templates/pages/classroom-admin-page/modals/delete-topic-from-classroom-modal.component.ts index 37336e03b370..bde72e4b84c0 100644 --- a/core/templates/pages/classroom-admin-page/modals/delete-topic-from-classroom-modal.component.ts +++ b/core/templates/pages/classroom-admin-page/modals/delete-topic-from-classroom-modal.component.ts @@ -16,20 +16,16 @@ * @fileoverview Delete topic from classroom confirmation modal component. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; - +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-delete-topic-from-classroom-modal', - templateUrl: './delete-topic-from-classroom-modal.component.html' + templateUrl: './delete-topic-from-classroom-modal.component.html', }) -export class DeleteTopicFromClassroomModalComponent - extends ConfirmOrCancelModal { - constructor( - private ngbActiveModal: NgbActiveModal - ) { +export class DeleteTopicFromClassroomModalComponent extends ConfirmOrCancelModal { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } diff --git a/core/templates/pages/classroom-admin-page/modals/topic-dependency-graph-viz-modal.component.spec.ts b/core/templates/pages/classroom-admin-page/modals/topic-dependency-graph-viz-modal.component.spec.ts index 9e9b8f833878..9ac3936a3b4a 100644 --- a/core/templates/pages/classroom-admin-page/modals/topic-dependency-graph-viz-modal.component.spec.ts +++ b/core/templates/pages/classroom-admin-page/modals/topic-dependency-graph-viz-modal.component.spec.ts @@ -16,9 +16,9 @@ * @fileoverview Tests for the topic dependency graph viz modal component. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { TopicsDependencyGraphModalComponent } from './topic-dependency-graph-viz-modal.component'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {TopicsDependencyGraphModalComponent} from './topic-dependency-graph-viz-modal.component'; describe('Topic Dependency Graph Visualization Modal Component', () => { let fixture: ComponentFixture; @@ -28,12 +28,8 @@ describe('Topic Dependency Graph Visualization Modal Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - TopicsDependencyGraphModalComponent - ], - providers: [ - NgbActiveModal, - ] + declarations: [TopicsDependencyGraphModalComponent], + providers: [NgbActiveModal], }).compileComponents(); })); @@ -66,7 +62,7 @@ describe('Topic Dependency Graph Visualization Modal Component', () => { 2: ['1'], 3: ['1'], 4: ['2'], - 5: ['3'] + 5: ['3'], }; const expectedGraphData = { finalStateIds: ['4', '5'], @@ -76,23 +72,26 @@ describe('Topic Dependency Graph Visualization Modal Component', () => { source: '1', target: '2', linkProperty: null, - connectsDestIfStuck: false - }, { + connectsDestIfStuck: false, + }, + { source: '1', target: '3', linkProperty: null, - connectsDestIfStuck: false - }, { + connectsDestIfStuck: false, + }, + { source: '2', target: '4', linkProperty: null, - connectsDestIfStuck: false - }, { + connectsDestIfStuck: false, + }, + { source: '3', target: '5', linkProperty: null, - connectsDestIfStuck: false - } + connectsDestIfStuck: false, + }, ], nodes: { 1: 'Dummy Topic 1', @@ -100,7 +99,7 @@ describe('Topic Dependency Graph Visualization Modal Component', () => { 3: 'Dummy Topic 3', 4: 'Dummy Topic 4', 5: 'Dummy Topic 5', - } + }, }; componentInstance.ngOnInit(); @@ -110,7 +109,8 @@ describe('Topic Dependency Graph Visualization Modal Component', () => { it( 'should be able to create graph data from the topics prerequisite ' + - 'when all topics can be initial topic ID', () => { + 'when all topics can be initial topic ID', + () => { componentInstance.topicIdToTopicName = { 1: 'Dummy Topic 1', 2: 'Dummy Topic 2', @@ -123,7 +123,7 @@ describe('Topic Dependency Graph Visualization Modal Component', () => { 2: ['1'], 3: ['2'], 4: ['3'], - 5: ['4'] + 5: ['4'], }; const expectedGraphData = { finalStateIds: [], @@ -133,28 +133,32 @@ describe('Topic Dependency Graph Visualization Modal Component', () => { source: '5', target: '1', linkProperty: null, - connectsDestIfStuck: false - }, { + connectsDestIfStuck: false, + }, + { source: '1', target: '2', linkProperty: null, - connectsDestIfStuck: false - }, { + connectsDestIfStuck: false, + }, + { source: '2', target: '3', linkProperty: null, - connectsDestIfStuck: false - }, { + connectsDestIfStuck: false, + }, + { source: '3', target: '4', linkProperty: null, - connectsDestIfStuck: false - }, { + connectsDestIfStuck: false, + }, + { source: '4', target: '5', linkProperty: null, - connectsDestIfStuck: false - } + connectsDestIfStuck: false, + }, ], nodes: { 1: 'Dummy Topic 1', @@ -162,16 +166,18 @@ describe('Topic Dependency Graph Visualization Modal Component', () => { 3: 'Dummy Topic 3', 4: 'Dummy Topic 4', 5: 'Dummy Topic 5', - } + }, }; componentInstance.ngOnInit(); expect(componentInstance.graphData).toEqual(expectedGraphData); - }); + } + ); it('should get truncated label with truncate filter', () => { - expect(componentInstance.getTruncatedLabel( - 'This is a label for node 3')).toBe('This is a la...'); + expect( + componentInstance.getTruncatedLabel('This is a label for node 3') + ).toBe('This is a la...'); }); }); diff --git a/core/templates/pages/classroom-admin-page/modals/topic-dependency-graph-viz-modal.component.ts b/core/templates/pages/classroom-admin-page/modals/topic-dependency-graph-viz-modal.component.ts index e3d6b5fbf84d..717e5ac47d51 100644 --- a/core/templates/pages/classroom-admin-page/modals/topic-dependency-graph-viz-modal.component.ts +++ b/core/templates/pages/classroom-admin-page/modals/topic-dependency-graph-viz-modal.component.ts @@ -16,17 +16,20 @@ * @fileoverview Topic dependency graph vizualization modal component. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { StateGraphLayoutService, AugmentedLink, NodeDataDict } from 'components/graph-services/graph-layout.service'; -import { GraphNodes, GraphLink, GraphData } from 'services/compute-graph.service'; -import { AppConstants } from 'app.constants'; -import { TruncatePipe } from 'filters/string-utility-filters/truncate.pipe'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import { + StateGraphLayoutService, + AugmentedLink, + NodeDataDict, +} from 'components/graph-services/graph-layout.service'; +import {GraphNodes, GraphLink, GraphData} from 'services/compute-graph.service'; +import {AppConstants} from 'app.constants'; +import {TruncatePipe} from 'filters/string-utility-filters/truncate.pipe'; import cloneDeep from 'lodash/cloneDeep'; -import { TopicIdToPrerequisiteTopicIds } from '../existing-classroom.model'; -import { TopicIdToTopicName } from '../existing-classroom.model'; - +import {TopicIdToPrerequisiteTopicIds} from '../existing-classroom.model'; +import {TopicIdToTopicName} from '../existing-classroom.model'; interface NodeData { y0: number; @@ -41,14 +44,13 @@ interface NodeData { @Component({ selector: 'oppia-topics-dependency-graph', - templateUrl: './topic-dependency-graph-viz-modal.component.html' + templateUrl: './topic-dependency-graph-viz-modal.component.html', }) -export class TopicsDependencyGraphModalComponent - extends ConfirmOrCancelModal { +export class TopicsDependencyGraphModalComponent extends ConfirmOrCancelModal { constructor( private ngbActiveModal: NgbActiveModal, private stateGraphLayoutService: StateGraphLayoutService, - private truncate: TruncatePipe, + private truncate: TruncatePipe ) { super(ngbActiveModal); } @@ -57,7 +59,7 @@ export class TopicsDependencyGraphModalComponent bottom: 0, left: 0, top: 0, - right: 0 + right: 0, }; graphData!: GraphData; @@ -79,36 +81,53 @@ export class TopicsDependencyGraphModalComponent ngOnInit(): void { this.normalizeTopicDependencyGraph(); this.drawGraph( - this.graphData.nodes, this.graphData.links, - this.graphData.initStateId, this.graphData.finalStateIds + this.graphData.nodes, + this.graphData.links, + this.graphData.initStateId, + this.graphData.finalStateIds ); } drawGraph( - nodes: GraphNodes, originalLinks: GraphLink[], - initStateId: string, finalStateIds: string[] + nodes: GraphNodes, + originalLinks: GraphLink[], + initStateId: string, + finalStateIds: string[] ): void { this.initStateId = initStateId; this.finalStateIds = finalStateIds; let links = cloneDeep(originalLinks); this.nodeData = this.stateGraphLayoutService.computeLayout( - nodes, links, initStateId, cloneDeep(finalStateIds)); + nodes, + links, + initStateId, + cloneDeep(finalStateIds) + ); this.GRAPH_WIDTH = this.stateGraphLayoutService.getGraphWidth( - AppConstants.MAX_NODES_PER_ROW, AppConstants.MAX_NODE_LABEL_LENGTH); + AppConstants.MAX_NODES_PER_ROW, + AppConstants.MAX_NODE_LABEL_LENGTH + ); this.GRAPH_HEIGHT = this.stateGraphLayoutService.getGraphHeight( - this.nodeData); + this.nodeData + ); this.nodeData = this.stateGraphLayoutService.modifyPositionValues( - this.nodeData, this.GRAPH_WIDTH, this.GRAPH_HEIGHT); + this.nodeData, + this.GRAPH_WIDTH, + this.GRAPH_HEIGHT + ); this.graphBounds = this.stateGraphLayoutService.getGraphBoundaries( - this.nodeData); + this.nodeData + ); this.augmentedLinks = this.stateGraphLayoutService.getAugmentedLinks( - this.nodeData, links); + this.nodeData, + links + ); this.nodeList = []; for (let nodeId in this.nodeData) { @@ -119,14 +138,17 @@ export class TopicsDependencyGraphModalComponent getTruncatedLabel(nodeLabel: string): string { return this.truncate.transform( nodeLabel, - AppConstants.MAX_NODE_LABEL_LENGTH); + AppConstants.MAX_NODE_LABEL_LENGTH + ); } normalizeTopicDependencyGraph(): void { const intialTopicIds = this.computeInitialTopicIds( - this.topicIdToPrerequisiteTopicIds); + this.topicIdToPrerequisiteTopicIds + ); const finalTopics = this.computeFinalTopicIds( - this.topicIdToPrerequisiteTopicIds); + this.topicIdToPrerequisiteTopicIds + ); const links = this.computeEdges(this.topicIdToPrerequisiteTopicIds); const nodes = this.topicIdToTopicName; @@ -134,12 +156,12 @@ export class TopicsDependencyGraphModalComponent finalStateIds: finalTopics, initStateId: intialTopicIds[0], links: links, - nodes: nodes + nodes: nodes, }; } computeInitialTopicIds( - topicIdToPrerequisiteTopicIds: TopicIdToPrerequisiteTopicIds + topicIdToPrerequisiteTopicIds: TopicIdToPrerequisiteTopicIds ): string[] { let initialTopicIds = []; for (let topicId in topicIdToPrerequisiteTopicIds) { @@ -154,16 +176,14 @@ export class TopicsDependencyGraphModalComponent } computeEdges( - topicIdToPrerequisiteTopicIds: TopicIdToPrerequisiteTopicIds + topicIdToPrerequisiteTopicIds: TopicIdToPrerequisiteTopicIds ): GraphLink[] { let edgeSet = []; for (let currentTopicId in topicIdToPrerequisiteTopicIds) { - let prerequisiteTopics = ( - topicIdToPrerequisiteTopicIds[currentTopicId]); + let prerequisiteTopics = topicIdToPrerequisiteTopicIds[currentTopicId]; for (let topicId of prerequisiteTopics) { - edgeSet.push(this.computeSingleEdge( - topicId, currentTopicId)); + edgeSet.push(this.computeSingleEdge(topicId, currentTopicId)); } } return edgeSet; @@ -174,18 +194,19 @@ export class TopicsDependencyGraphModalComponent source: sourceTopic, target: destTopic, linkProperty: null, - connectsDestIfStuck: false + connectsDestIfStuck: false, }; } computeFinalTopicIds( - topicIdToPrerequisiteTopicIds: TopicIdToPrerequisiteTopicIds + topicIdToPrerequisiteTopicIds: TopicIdToPrerequisiteTopicIds ): string[] { let prerequisitesOfAllTopics: string[] = []; let finalTopicIds = []; for (let topicId in topicIdToPrerequisiteTopicIds) { prerequisitesOfAllTopics = prerequisitesOfAllTopics.concat( - topicIdToPrerequisiteTopicIds[topicId]); + topicIdToPrerequisiteTopicIds[topicId] + ); } for (let topicId in topicIdToPrerequisiteTopicIds) { diff --git a/core/templates/pages/classroom-admin-page/navbar/classroom-admin-navbar.component.spec.ts b/core/templates/pages/classroom-admin-page/navbar/classroom-admin-navbar.component.spec.ts index a896e4f60cfa..290867684de5 100644 --- a/core/templates/pages/classroom-admin-page/navbar/classroom-admin-navbar.component.spec.ts +++ b/core/templates/pages/classroom-admin-page/navbar/classroom-admin-navbar.component.spec.ts @@ -16,23 +16,27 @@ * @fileoverview Unit tests for classroom admin navbar component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { RouterModule } from '@angular/router'; -import { APP_BASE_HREF } from '@angular/common'; - -import { UserService } from 'services/user.service'; -import { ClassroomAdminNavbarComponent } from 'pages/classroom-admin-page/navbar/classroom-admin-navbar.component'; -import { SmartRouterModule } from 'hybrid-router-module-provider'; -import { UserInfo } from 'domain/user/user-info.model'; - +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {RouterModule} from '@angular/router'; +import {APP_BASE_HREF} from '@angular/common'; + +import {UserService} from 'services/user.service'; +import {ClassroomAdminNavbarComponent} from 'pages/classroom-admin-page/navbar/classroom-admin-navbar.component'; +import {SmartRouterModule} from 'hybrid-router-module-provider'; +import {UserInfo} from 'domain/user/user-info.model'; describe('Classroom Admin navbar component', () => { let component: ClassroomAdminNavbarComponent; let userService: UserService; let userInfo = { getUsername: () => 'username1', - isSuperAdmin: () => true + isSuperAdmin: () => true, }; let profileUrl = '/profile/username1'; let fixture: ComponentFixture; @@ -43,33 +47,37 @@ describe('Classroom Admin navbar component', () => { // TODO(#13443): Remove hybrid router module provider once all pages are // migrated to angular router. SmartRouterModule, - RouterModule.forRoot([]) + RouterModule.forRoot([]), ], declarations: [ClassroomAdminNavbarComponent], - providers: [{ - provide: APP_BASE_HREF, - useValue: '/' - }] + providers: [ + { + provide: APP_BASE_HREF, + useValue: '/', + }, + ], }).compileComponents(); fixture = TestBed.createComponent(ClassroomAdminNavbarComponent); component = fixture.componentInstance; userService = TestBed.inject(UserService); fixture.detectChanges(); - spyOn(userService, 'getProfileImageDataUrl').and.returnValue( - ['default-image-url-png', 'default-image-url-webp']); + spyOn(userService, 'getProfileImageDataUrl').and.returnValue([ + 'default-image-url-png', + 'default-image-url-webp', + ]); })); it('should initialize component properties correctly', fakeAsync(() => { - spyOn(userService, 'getUserInfoAsync') - .and.resolveTo(userInfo as UserInfo); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo as UserInfo); component.ngOnInit(); tick(); expect(component.profilePicturePngDataUrl).toEqual('default-image-url-png'); expect(component.profilePictureWebpDataUrl).toEqual( - 'default-image-url-webp'); + 'default-image-url-webp' + ); expect(component.profileUrl).toBe(profileUrl); expect(component.profileDropdownIsActive).toBe(false); })); @@ -77,10 +85,9 @@ describe('Classroom Admin navbar component', () => { it('should throw error if username is invalid', fakeAsync(() => { let userInfo = { getUsername: () => null, - isSuperAdmin: () => true + isSuperAdmin: () => true, }; - spyOn(userService, 'getUserInfoAsync') - .and.resolveTo(userInfo as UserInfo); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo as UserInfo); expect(() => { component.ngOnInit(); @@ -89,8 +96,7 @@ describe('Classroom Admin navbar component', () => { })); it('should set profileDropdownIsActive to true', fakeAsync(() => { - spyOn(userService, 'getUserInfoAsync') - .and.resolveTo(userInfo as UserInfo); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo as UserInfo); component.ngOnInit(); tick(); @@ -103,8 +109,7 @@ describe('Classroom Admin navbar component', () => { })); it('should set profileDropdownIsActive to false', fakeAsync(() => { - spyOn(userService, 'getUserInfoAsync') - .and.resolveTo(userInfo as UserInfo); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo as UserInfo); component.ngOnInit(); tick(); diff --git a/core/templates/pages/classroom-admin-page/navbar/classroom-admin-navbar.component.ts b/core/templates/pages/classroom-admin-page/navbar/classroom-admin-navbar.component.ts index f996011f205f..b5e6dd07f871 100644 --- a/core/templates/pages/classroom-admin-page/navbar/classroom-admin-navbar.component.ts +++ b/core/templates/pages/classroom-admin-page/navbar/classroom-admin-navbar.component.ts @@ -17,12 +17,11 @@ * panel. */ -import { Component, OnInit } from '@angular/core'; - -import { AppConstants } from 'app.constants'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { UserService } from 'services/user.service'; +import {Component, OnInit} from '@angular/core'; +import {AppConstants} from 'app.constants'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {UserService} from 'services/user.service'; @Component({ selector: 'oppia-classroom-admin-navbar', @@ -38,22 +37,21 @@ export class ClassroomAdminNavbarComponent implements OnInit { username!: string | null; logoWebpImageSrc!: string; logoPngImageSrc!: string; - PAGES_REGISTERED_WITH_FRONTEND = ( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND); + PAGES_REGISTERED_WITH_FRONTEND = AppConstants.PAGES_REGISTERED_WITH_FRONTEND; profileDropdownIsActive: boolean = false; constructor( private urlInterpolationService: UrlInterpolationService, - private userService: UserService, + private userService: UserService ) {} activateProfileDropdown(): boolean { - return this.profileDropdownIsActive = true; + return (this.profileDropdownIsActive = true); } deactivateProfileDropdown(): boolean { - return this.profileDropdownIsActive = false; + return (this.profileDropdownIsActive = false); } async getUserInfoAsync(): Promise { @@ -63,21 +61,24 @@ export class ClassroomAdminNavbarComponent implements OnInit { if (this.username === null) { throw new Error('Cannot fetch username.'); } - this.profileUrl = ( - this.urlInterpolationService.interpolateUrl( - '/profile/', { - username: this.username - })); - [this.profilePicturePngDataUrl, this.profilePictureWebpDataUrl] = ( - this.userService.getProfileImageDataUrl(this.username)); + this.profileUrl = this.urlInterpolationService.interpolateUrl( + '/profile/', + { + username: this.username, + } + ); + [this.profilePicturePngDataUrl, this.profilePictureWebpDataUrl] = + this.userService.getProfileImageDataUrl(this.username); } ngOnInit(): void { this.getUserInfoAsync(); this.logoPngImageSrc = this.urlInterpolationService.getStaticImageUrl( - '/logo/288x128_logo_white.png'); + '/logo/288x128_logo_white.png' + ); this.logoWebpImageSrc = this.urlInterpolationService.getStaticImageUrl( - '/logo/288x128_logo_white.webp'); + '/logo/288x128_logo_white.webp' + ); } } diff --git a/core/templates/pages/classroom-admin-page/new-classroom.model.spec.ts b/core/templates/pages/classroom-admin-page/new-classroom.model.spec.ts index 1f03a3f14c4e..b4b4977ddce8 100644 --- a/core/templates/pages/classroom-admin-page/new-classroom.model.spec.ts +++ b/core/templates/pages/classroom-admin-page/new-classroom.model.spec.ts @@ -16,86 +16,74 @@ * @fileoverview Tests for new classroom model. */ - -import { TestBed } from '@angular/core/testing'; -import { NewClassroomData } from './new-classroom.model'; - +import {TestBed} from '@angular/core/testing'; +import {NewClassroomData} from './new-classroom.model'; describe('Classroom admin model', () => { let classroomData: NewClassroomData; beforeEach(() => { TestBed.configureTestingModule({ imports: [], - providers: [] + providers: [], }); classroomData = new NewClassroomData('classroomId', 'math', 'math'); }); - it( - 'should return error messgage when classroom name exceeds max len', - () => { - classroomData.setClassroomName( - 'Long classroom name with some random texts abcdefghi'); + it('should return error messgage when classroom name exceeds max len', () => { + classroomData.setClassroomName( + 'Long classroom name with some random texts abcdefghi' + ); - expect(classroomData.getClassroomNameValidationErrors()).toEqual( - 'The classroom name should contain at most 39 characters.' - ); - }); + expect(classroomData.getClassroomNameValidationErrors()).toEqual( + 'The classroom name should contain at most 39 characters.' + ); + }); - it( - 'should present error messgae when classroom name is empty', - () => { - classroomData.setClassroomName(''); + it('should present error messgae when classroom name is empty', () => { + classroomData.setClassroomName(''); - expect(classroomData.getClassroomNameValidationErrors()).toEqual( - 'The classroom name should not be empty.' - ); - }); + expect(classroomData.getClassroomNameValidationErrors()).toEqual( + 'The classroom name should not be empty.' + ); + }); - it( - 'should not present any error when classroom name is valid', () => { - classroomData.setClassroomName('Discrete maths'); + it('should not present any error when classroom name is valid', () => { + classroomData.setClassroomName('Discrete maths'); - expect(classroomData.getClassroomNameValidationErrors()).toEqual(''); - }); + expect(classroomData.getClassroomNameValidationErrors()).toEqual(''); + }); - it( - 'should present error messgae when clasroom url fragment is empty', () => { - classroomData.setUrlFragment(''); + it('should present error messgae when clasroom url fragment is empty', () => { + classroomData.setUrlFragment(''); - expect(classroomData.getClassroomUrlValidationErrors()).toEqual( - 'The classroom URL fragment should not be empty.' - ); - }); + expect(classroomData.getClassroomUrlValidationErrors()).toEqual( + 'The classroom URL fragment should not be empty.' + ); + }); - it( - 'should present error message when classroom url fragment exceeds max len', - () => { - classroomData.setUrlFragment('long-url-fragment-for-raising-error-msg'); + it('should present error message when classroom url fragment exceeds max len', () => { + classroomData.setUrlFragment('long-url-fragment-for-raising-error-msg'); - expect(classroomData.getClassroomUrlValidationErrors()).toEqual( - 'The classroom URL fragment should contain at most 20 characters.' - ); - }); + expect(classroomData.getClassroomUrlValidationErrors()).toEqual( + 'The classroom URL fragment should contain at most 20 characters.' + ); + }); - it( - 'should present error message when classroom url fragment is invalid', - () => { - classroomData.setUrlFragment('Incorrect-url'); + it('should present error message when classroom url fragment is invalid', () => { + classroomData.setUrlFragment('Incorrect-url'); - expect(classroomData.getClassroomUrlValidationErrors()).toEqual( - 'The classroom URL fragment should only contain lowercase ' + + expect(classroomData.getClassroomUrlValidationErrors()).toEqual( + 'The classroom URL fragment should only contain lowercase ' + 'letters separated by hyphens.' - ); - }); + ); + }); - it( - 'should not present error for valid classroom url fragment', () => { - classroomData.setUrlFragment('physics-url-fragment'); + it('should not present error for valid classroom url fragment', () => { + classroomData.setUrlFragment('physics-url-fragment'); - expect(classroomData.getClassroomUrlValidationErrors()).toEqual(''); - }); + expect(classroomData.getClassroomUrlValidationErrors()).toEqual(''); + }); it('should be able to set and get classroom validity flag', () => { classroomData.setClassroomValidityFlag(false); diff --git a/core/templates/pages/classroom-admin-page/new-classroom.model.ts b/core/templates/pages/classroom-admin-page/new-classroom.model.ts index f77f9275f5c4..884c28a2910f 100644 --- a/core/templates/pages/classroom-admin-page/new-classroom.model.ts +++ b/core/templates/pages/classroom-admin-page/new-classroom.model.ts @@ -16,8 +16,7 @@ * @fileoverview New classroom model. */ -import { AppConstants } from 'app.constants'; - +import {AppConstants} from 'app.constants'; export interface NewClassroom { _classroomId: string; @@ -35,18 +34,13 @@ export interface NewClassroom { setClassroomValidityFlag: (classroomDataIsValid: boolean) => void; } - export class NewClassroomData implements NewClassroom { _classroomId: string; _name: string; _urlFragment: string; _classroomDataIsValid!: boolean; - constructor( - classroomId: string, - name: string, - urlFragment: string - ) { + constructor(classroomId: string, name: string, urlFragment: string) { this._classroomId = classroomId; this._name = name; this._urlFragment = urlFragment; @@ -93,21 +87,21 @@ export class NewClassroomData implements NewClassroom { getClassroomUrlValidationErrors(): string { let errorMsg = ''; const validUrlFragmentRegex = new RegExp( - AppConstants.VALID_URL_FRAGMENT_REGEX); + AppConstants.VALID_URL_FRAGMENT_REGEX + ); if (this._urlFragment === '') { errorMsg = 'The classroom URL fragment should not be empty.'; } else if ( this._urlFragment.length > - AppConstants.MAX_CHARS_IN_CLASSROOM_URL_FRAGMENT + AppConstants.MAX_CHARS_IN_CLASSROOM_URL_FRAGMENT ) { - errorMsg = ( - 'The classroom URL fragment should contain at most 20 characters.' - ); + errorMsg = + 'The classroom URL fragment should contain at most 20 characters.'; } else if (!validUrlFragmentRegex.test(this._urlFragment)) { - errorMsg = ( + errorMsg = 'The classroom URL fragment should only contain lowercase ' + - 'letters separated by hyphens.'); + 'letters separated by hyphens.'; } return errorMsg; } diff --git a/core/templates/pages/classroom-admin-page/services/classroom-admin-data.service.spec.ts b/core/templates/pages/classroom-admin-page/services/classroom-admin-data.service.spec.ts index da65d46fe0c9..23d0bd59ca8a 100644 --- a/core/templates/pages/classroom-admin-page/services/classroom-admin-data.service.spec.ts +++ b/core/templates/pages/classroom-admin-page/services/classroom-admin-data.service.spec.ts @@ -16,12 +16,11 @@ * @fileoverview Tests for classroom admin data service. */ -import { ClassroomAdminDataService } from './classroom-admin-data.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { fakeAsync, TestBed, tick, waitForAsync} from '@angular/core/testing'; -import { ClassroomBackendApiService } from 'domain/classroom/classroom-backend-api.service'; -import { ExistingClassroomData } from '../existing-classroom.model'; - +import {ClassroomAdminDataService} from './classroom-admin-data.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {fakeAsync, TestBed, tick, waitForAsync} from '@angular/core/testing'; +import {ClassroomBackendApiService} from 'domain/classroom/classroom-backend-api.service'; +import {ExistingClassroomData} from '../existing-classroom.model'; describe('Classroom Admin Data Service', () => { let classroomAdminDataService: ClassroomAdminDataService; @@ -30,18 +29,13 @@ describe('Classroom Admin Data Service', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - ], - providers: [ - ClassroomBackendApiService, - ] + imports: [HttpClientTestingModule], + providers: [ClassroomBackendApiService], }).compileComponents(); })); beforeEach(() => { - classroomBackendApiService = TestBed.inject( - ClassroomBackendApiService); + classroomBackendApiService = TestBed.inject(ClassroomBackendApiService); classroomAdminDataService = TestBed.inject(ClassroomAdminDataService); classroomData = new ExistingClassroomData( 'classroomId', @@ -54,72 +48,79 @@ describe('Classroom Admin Data Service', () => { }); it('should return classroom name error coming from model', () => { - spyOn(classroomData, 'getClassroomNameValidationErrors') - .and.returnValue('Name error from model.'); + spyOn(classroomData, 'getClassroomNameValidationErrors').and.returnValue( + 'Name error from model.' + ); classroomAdminDataService.onClassroomNameChange(classroomData); expect(classroomAdminDataService.nameValidationError).toEqual( - 'Name error from model.'); + 'Name error from model.' + ); }); it('should be able to validate duplicate classroom name', () => { classroomAdminDataService.existingClassroomNames = ['chemistry', 'physics']; - spyOn(classroomData, 'getClassroomNameValidationErrors') - .and.returnValue(''); + spyOn(classroomData, 'getClassroomNameValidationErrors').and.returnValue( + '' + ); classroomData.setClassroomName('chemistry'); classroomAdminDataService.onClassroomNameChange(classroomData); expect(classroomAdminDataService.nameValidationError).toEqual( - 'A classroom with this name already exists.'); + 'A classroom with this name already exists.' + ); }); it('should be able return classroom URL error coming from model', () => { - spyOn(classroomData, 'getClassroomUrlValidationErrors') - .and.returnValue('URL error from model.'); + spyOn(classroomData, 'getClassroomUrlValidationErrors').and.returnValue( + 'URL error from model.' + ); classroomAdminDataService.onClassroomUrlChange(classroomData, ''); expect(classroomAdminDataService.urlValidationError).toEqual( - 'URL error from model.'); + 'URL error from model.' + ); }); it('should be able to validate duplicate classroom URL', fakeAsync(() => { - spyOn(classroomData, 'getClassroomUrlValidationErrors') - .and.returnValue(''); - spyOn(classroomBackendApiService, 'doesClassroomWithUrlFragmentExistAsync') - .and.returnValue(Promise.resolve(true)); + spyOn(classroomData, 'getClassroomUrlValidationErrors').and.returnValue(''); + spyOn( + classroomBackendApiService, + 'doesClassroomWithUrlFragmentExistAsync' + ).and.returnValue(Promise.resolve(true)); classroomAdminDataService.onClassroomUrlChange(classroomData, ''); tick(); expect(classroomAdminDataService.urlValidationError).toEqual( - 'A classroom with this name already exists.'); + 'A classroom with this name already exists.' + ); })); - it( - 'should be able to call setClassroomValidityFlag method from the model', - () => { - spyOn(classroomData, 'setClassroomValidityFlag'); - - let existingClassroom = new ExistingClassroomData( - 'classroomID', - 'physics', - 'physics', - 'Curated math foundations course.', - 'Start from the basics with our first topic.', - {} - ); - classroomAdminDataService.existingClassroomNames = [ - 'chemistry', 'physics']; - - classroomAdminDataService.validateClassroom( - classroomData, existingClassroom); - - expect(classroomData.setClassroomValidityFlag).toHaveBeenCalled(); - }); + it('should be able to call setClassroomValidityFlag method from the model', () => { + spyOn(classroomData, 'setClassroomValidityFlag'); + + let existingClassroom = new ExistingClassroomData( + 'classroomID', + 'physics', + 'physics', + 'Curated math foundations course.', + 'Start from the basics with our first topic.', + {} + ); + classroomAdminDataService.existingClassroomNames = ['chemistry', 'physics']; + + classroomAdminDataService.validateClassroom( + classroomData, + existingClassroom + ); + + expect(classroomData.setClassroomValidityFlag).toHaveBeenCalled(); + }); it('should be able to reinitialize name and URL variables', () => { classroomAdminDataService.nameValidationError = 'Name error'; diff --git a/core/templates/pages/classroom-admin-page/services/classroom-admin-data.service.ts b/core/templates/pages/classroom-admin-page/services/classroom-admin-data.service.ts index 8e56adbffe86..e3197e1dcbe8 100644 --- a/core/templates/pages/classroom-admin-page/services/classroom-admin-data.service.ts +++ b/core/templates/pages/classroom-admin-page/services/classroom-admin-data.service.ts @@ -16,18 +16,18 @@ * @fileoverview Service that handles validation for the classroom data. */ -import { Injectable } from '@angular/core'; -import { ClassroomData, ExistingClassroomData } from '../existing-classroom.model'; -import { ClassroomBackendApiService } from 'domain/classroom/classroom-backend-api.service'; - +import {Injectable} from '@angular/core'; +import { + ClassroomData, + ExistingClassroomData, +} from '../existing-classroom.model'; +import {ClassroomBackendApiService} from 'domain/classroom/classroom-backend-api.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ClassroomAdminDataService { - constructor( - private classroomBackendApiService: ClassroomBackendApiService - ) {} + constructor(private classroomBackendApiService: ClassroomBackendApiService) {} classroom!: ClassroomData; existingClassroomNames: string[] = []; @@ -44,17 +44,15 @@ export class ClassroomAdminDataService { } if ( - this.existingClassroomNames.indexOf( - classroom.getClassroomName()) !== -1 + this.existingClassroomNames.indexOf(classroom.getClassroomName()) !== -1 ) { - this.nameValidationError = ( - 'A classroom with this name already exists.'); + this.nameValidationError = 'A classroom with this name already exists.'; } } onClassroomUrlChange( - classroom: ClassroomData, - existingClassroomUrl: string + classroom: ClassroomData, + existingClassroomUrl: string ): void { this.urlValidationError = classroom.getClassroomUrlValidationErrors(); @@ -62,40 +60,42 @@ export class ClassroomAdminDataService { return; } - this.classroomBackendApiService.doesClassroomWithUrlFragmentExistAsync( - classroom.getClassroomUrlFragment() - ).then((response: boolean) => { - if ( - response && ( - classroom.getClassroomUrlFragment() !== - existingClassroomUrl) - ) { - this.urlValidationError = ( - 'A classroom with this name already exists.'); - } - }); + this.classroomBackendApiService + .doesClassroomWithUrlFragmentExistAsync( + classroom.getClassroomUrlFragment() + ) + .then((response: boolean) => { + if ( + response && + classroom.getClassroomUrlFragment() !== existingClassroomUrl + ) { + this.urlValidationError = + 'A classroom with this name already exists.'; + } + }); } onTopicDependencyChange(classroom: ExistingClassroomData): void { - this.topicsGraphValidationError = ( - classroom.validateDependencyGraph()); + this.topicsGraphValidationError = classroom.validateDependencyGraph(); } validateClassroom( - tempClassroom: ClassroomData, - existingClassroom: ClassroomData + tempClassroom: ClassroomData, + existingClassroom: ClassroomData ): void { this.onClassroomNameChange(tempClassroom); this.onClassroomUrlChange( - tempClassroom, existingClassroom.getClassroomUrlFragment()); + tempClassroom, + existingClassroom.getClassroomUrlFragment() + ); if (tempClassroom instanceof ExistingClassroomData) { this.onTopicDependencyChange(tempClassroom); } tempClassroom.setClassroomValidityFlag( this.nameValidationError === '' && - this.urlValidationError === '' && - this.topicsGraphValidationError === '' + this.urlValidationError === '' && + this.topicsGraphValidationError === '' ); } diff --git a/core/templates/pages/classroom-page/classroom-page-root.component.spec.ts b/core/templates/pages/classroom-page/classroom-page-root.component.spec.ts index 3e73a908fec8..349ee63836ad 100644 --- a/core/templates/pages/classroom-page/classroom-page-root.component.spec.ts +++ b/core/templates/pages/classroom-page/classroom-page-root.component.spec.ts @@ -16,16 +16,22 @@ * @fileoverview Unit tests for the classroom page root component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync, tick, fakeAsync } from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, + tick, + fakeAsync, +} from '@angular/core/testing'; -import { PageHeadService } from 'services/page-head.service'; +import {PageHeadService} from 'services/page-head.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { ClassroomPageRootComponent } from './classroom-page-root.component'; -import { AccessValidationBackendApiService } from 'pages/oppia-root/routing/access-validation-backend-api.service'; -import { UrlService } from 'services/contextual/url.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {ClassroomPageRootComponent} from './classroom-page-root.component'; +import {AccessValidationBackendApiService} from 'pages/oppia-root/routing/access-validation-backend-api.service'; +import {UrlService} from 'services/contextual/url.service'; describe('Classroom Root Page', () => { let fixture: ComponentFixture; @@ -36,18 +42,10 @@ describe('Classroom Root Page', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - ClassroomPageRootComponent, - MockTranslatePipe - ], - providers: [ - PageHeadService, - UrlService - ], - schemas: [NO_ERRORS_SCHEMA] + imports: [HttpClientTestingModule], + declarations: [ClassroomPageRootComponent, MockTranslatePipe], + providers: [PageHeadService, UrlService], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -56,36 +54,44 @@ describe('Classroom Root Page', () => { component = fixture.componentInstance; pageHeadService = TestBed.inject(PageHeadService); accessValidationBackendApiService = TestBed.inject( - AccessValidationBackendApiService); + AccessValidationBackendApiService + ); urlService = TestBed.inject(UrlService); }); - it('should successfully instantiate the component', - () => { - spyOn(accessValidationBackendApiService, 'validateAccessToClassroomPage') - .and.returnValue(Promise.resolve()); - expect(component).toBeDefined(); - }); + it('should successfully instantiate the component', () => { + spyOn( + accessValidationBackendApiService, + 'validateAccessToClassroomPage' + ).and.returnValue(Promise.resolve()); + expect(component).toBeDefined(); + }); it('should initialize', () => { spyOn(pageHeadService, 'updateTitleAndMetaTags'); spyOn(urlService, 'getClassroomUrlFragmentFromUrl').and.returnValue( - 'classroom_url_fragment'); - spyOn(accessValidationBackendApiService, 'validateAccessToClassroomPage') - .and.returnValue(Promise.resolve()); + 'classroom_url_fragment' + ); + spyOn( + accessValidationBackendApiService, + 'validateAccessToClassroomPage' + ).and.returnValue(Promise.resolve()); component.ngOnInit(); expect( - accessValidationBackendApiService.validateAccessToClassroomPage) - .toHaveBeenCalledWith('classroom_url_fragment'); + accessValidationBackendApiService.validateAccessToClassroomPage + ).toHaveBeenCalledWith('classroom_url_fragment'); }); it('should show error when classroom does not exist', fakeAsync(() => { spyOn(pageHeadService, 'updateTitleAndMetaTags'); spyOn(urlService, 'getClassroomUrlFragmentFromUrl').and.returnValue( - 'classroom_url_fragment'); - spyOn(accessValidationBackendApiService, 'validateAccessToClassroomPage') - .and.returnValue(Promise.reject()); + 'classroom_url_fragment' + ); + spyOn( + accessValidationBackendApiService, + 'validateAccessToClassroomPage' + ).and.returnValue(Promise.reject()); expect(component.errorPageIsShown).toBeFalse(); expect(component.pageIsShown).toBeFalse(); diff --git a/core/templates/pages/classroom-page/classroom-page-root.component.ts b/core/templates/pages/classroom-page/classroom-page-root.component.ts index ca8558e6e599..b9572db12ec3 100644 --- a/core/templates/pages/classroom-page/classroom-page-root.component.ts +++ b/core/templates/pages/classroom-page/classroom-page-root.component.ts @@ -16,23 +16,22 @@ * @fileoverview Root component for Classroom Page. */ -import { Component } from '@angular/core'; +import {Component} from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { AccessValidationBackendApiService } from 'pages/oppia-root/routing/access-validation-backend-api.service'; -import { PageHeadService } from 'services/page-head.service'; -import { UrlService } from 'services/contextual/url.service'; +import {AppConstants} from 'app.constants'; +import {AccessValidationBackendApiService} from 'pages/oppia-root/routing/access-validation-backend-api.service'; +import {PageHeadService} from 'services/page-head.service'; +import {UrlService} from 'services/contextual/url.service'; @Component({ selector: 'oppia-classroom-page-root', - templateUrl: './classroom-page-root.component.html' + templateUrl: './classroom-page-root.component.html', }) export class ClassroomPageRootComponent { constructor( - private accessValidationBackendApiService: - AccessValidationBackendApiService, + private accessValidationBackendApiService: AccessValidationBackendApiService, private pageHeadService: PageHeadService, - private urlService: UrlService, + private urlService: UrlService ) {} errorPageIsShown: boolean = false; @@ -40,18 +39,23 @@ export class ClassroomPageRootComponent { classroomUrlFragment!: string; ngOnInit(): void { - this.classroomUrlFragment = ( - this.urlService.getClassroomUrlFragmentFromUrl()); + this.classroomUrlFragment = + this.urlService.getClassroomUrlFragmentFromUrl(); - this.accessValidationBackendApiService.validateAccessToClassroomPage( - this.classroomUrlFragment).then(() => { - this.errorPageIsShown = false; - this.pageIsShown = true; - this.pageHeadService.updateTitleAndMetaTags( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.CLASSROOM.TITLE, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.CLASSROOM.META); - }, () => { - this.errorPageIsShown = true; - }); + this.accessValidationBackendApiService + .validateAccessToClassroomPage(this.classroomUrlFragment) + .then( + () => { + this.errorPageIsShown = false; + this.pageIsShown = true; + this.pageHeadService.updateTitleAndMetaTags( + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.CLASSROOM.TITLE, + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.CLASSROOM.META + ); + }, + () => { + this.errorPageIsShown = true; + } + ); } } diff --git a/core/templates/pages/classroom-page/classroom-page-routing.module.ts b/core/templates/pages/classroom-page/classroom-page-routing.module.ts index fc1cb9c1d9b9..d6749e85c211 100644 --- a/core/templates/pages/classroom-page/classroom-page-routing.module.ts +++ b/core/templates/pages/classroom-page/classroom-page-routing.module.ts @@ -16,24 +16,19 @@ * @fileoverview Routing module for classroom page. */ -import { NgModule } from '@angular/core'; -import { Route, RouterModule } from '@angular/router'; -import { ClassroomPageRootComponent } from './classroom-page-root.component'; +import {NgModule} from '@angular/core'; +import {Route, RouterModule} from '@angular/router'; +import {ClassroomPageRootComponent} from './classroom-page-root.component'; const routes: Route[] = [ { path: '', - component: ClassroomPageRootComponent - } + component: ClassroomPageRootComponent, + }, ]; @NgModule({ - imports: [ - RouterModule.forChild(routes) - ], - exports: [ - RouterModule - ] + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], }) - export class ClassroomPageRoutingModule {} diff --git a/core/templates/pages/classroom-page/classroom-page.component.spec.ts b/core/templates/pages/classroom-page/classroom-page.component.spec.ts index 69e81870c960..b1e2980e0b43 100644 --- a/core/templates/pages/classroom-page/classroom-page.component.spec.ts +++ b/core/templates/pages/classroom-page/classroom-page.component.spec.ts @@ -16,25 +16,31 @@ * @fileoverview Unit tests for classroom page component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; -import { ClassroomBackendApiService } from 'domain/classroom/classroom-backend-api.service'; -import { ClassroomData } from 'domain/classroom/classroom-data.model'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { CapitalizePipe } from 'filters/string-utility-filters/capitalize.pipe'; -import { AccessValidationBackendApiService } from 'pages/oppia-root/routing/access-validation-backend-api.service'; -import { AlertsService } from 'services/alerts.service'; -import { UrlService } from 'services/contextual/url.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { LoaderService } from 'services/loader.service'; -import { PageTitleService } from 'services/page-title.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { ClassroomPageComponent } from './classroom-page.component'; -import { PlatformFeatureService } from 'services/platform-feature.service'; +import {ClassroomBackendApiService} from 'domain/classroom/classroom-backend-api.service'; +import {ClassroomData} from 'domain/classroom/classroom-data.model'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {CapitalizePipe} from 'filters/string-utility-filters/capitalize.pipe'; +import {AccessValidationBackendApiService} from 'pages/oppia-root/routing/access-validation-backend-api.service'; +import {AlertsService} from 'services/alerts.service'; +import {UrlService} from 'services/contextual/url.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {LoaderService} from 'services/loader.service'; +import {PageTitleService} from 'services/page-title.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {ClassroomPageComponent} from './classroom-page.component'; +import {PlatformFeatureService} from 'services/platform-feature.service'; class MockCapitalizePipe { transform(input: string): string { @@ -52,8 +58,8 @@ class MockTranslateService { class MockPlatformFeatureService { status = { DiagnosticTest: { - isEnabled: false - } + isEnabled: false, + }, }; } @@ -74,26 +80,21 @@ describe('Classroom Page Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - ClassroomPageComponent, - MockTranslatePipe - ], + imports: [HttpClientTestingModule], + declarations: [ClassroomPageComponent, MockTranslatePipe], providers: [ AlertsService, { provide: CapitalizePipe, - useClass: MockCapitalizePipe + useClass: MockCapitalizePipe, }, { provide: TranslateService, - useClass: MockTranslateService + useClass: MockTranslateService, }, { provide: PlatformFeatureService, - useValue: mockPlatformFeatureService + useValue: mockPlatformFeatureService, }, ClassroomBackendApiService, LoaderService, @@ -102,7 +103,7 @@ describe('Classroom Page Component', () => { UrlInterpolationService, UrlService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -118,11 +119,13 @@ describe('Classroom Page Component', () => { alertsService = TestBed.inject(AlertsService); i18nLanguageCodeService = TestBed.inject(I18nLanguageCodeService); accessValidationBackendApiService = TestBed.inject( - AccessValidationBackendApiService); + AccessValidationBackendApiService + ); translateService = TestBed.inject(TranslateService); spyOn(i18nLanguageCodeService, 'isCurrentLanguageRTL').and.returnValue( - true); + true + ); }); it('should create', () => { @@ -131,117 +134,148 @@ describe('Classroom Page Component', () => { it('should provide static image url', () => { let imageUrl = 'image_url'; - spyOn(urlInterpolationService, 'getStaticImageUrl') - .and.returnValue(imageUrl); + spyOn(urlInterpolationService, 'getStaticImageUrl').and.returnValue( + imageUrl + ); expect(component.getStaticImageUrl('test')).toEqual(imageUrl); }); it('should initialize', fakeAsync(() => { let classroomUrlFragment = 'math'; let bannerImageUrl = 'banner_image_url'; - spyOn(urlService, 'getClassroomUrlFragmentFromUrl') - .and.returnValue(classroomUrlFragment); - spyOn(urlInterpolationService, 'getStaticImageUrl') - .and.returnValue(bannerImageUrl); + spyOn(urlService, 'getClassroomUrlFragmentFromUrl').and.returnValue( + classroomUrlFragment + ); + spyOn(urlInterpolationService, 'getStaticImageUrl').and.returnValue( + bannerImageUrl + ); spyOn(loaderService, 'showLoadingScreen'); spyOn(component, 'setPageTitle'); spyOn(component, 'subscribeToOnLangChange'); spyOn(loaderService, 'hideLoadingScreen'); spyOn(classroomBackendApiService.onInitializeTranslation, 'emit'); spyOn(siteAnalyticsService, 'registerClassroomPageViewed'); - let topicSummaryDicts = [{ - id: 'topic1', - name: 'Topic name', - description: 'Topic description', - canonical_story_count: 4, - subtopic_count: 5, - total_skill_count: 20, - uncategorized_skill_count: 5, - thumbnail_filename: 'image.svg', - thumbnail_bg_color: '#C6DCDA', - language_code: 'en', - version: 1, - additional_story_count: 0, - total_published_node_count: 4, - topic_model_created_on: 20160101, - topic_model_last_updated: 20160110, - can_edit_topic: true, - is_published: true, - url_fragment: 'some-url-fragment', - classroom: 'math', - total_upcoming_chapters_count: 1, - total_overdue_chapters_count: 1, - total_chapter_counts_for_each_story: [5, 4], - published_chapter_counts_for_each_story: [3, 4] - }]; + let topicSummaryDicts = [ + { + id: 'topic1', + name: 'Topic name', + description: 'Topic description', + canonical_story_count: 4, + subtopic_count: 5, + total_skill_count: 20, + uncategorized_skill_count: 5, + thumbnail_filename: 'image.svg', + thumbnail_bg_color: '#C6DCDA', + language_code: 'en', + version: 1, + additional_story_count: 0, + total_published_node_count: 4, + topic_model_created_on: 20160101, + topic_model_last_updated: 20160110, + can_edit_topic: true, + is_published: true, + url_fragment: 'some-url-fragment', + classroom: 'math', + total_upcoming_chapters_count: 1, + total_overdue_chapters_count: 1, + total_chapter_counts_for_each_story: [5, 4], + published_chapter_counts_for_each_story: [3, 4], + }, + ]; let classroomData = ClassroomData.createFromBackendData( - 'Math', topicSummaryDicts, 'Course details', 'Topics covered'); - spyOn(accessValidationBackendApiService, 'validateAccessToClassroomPage') - .and.returnValue(Promise.resolve()); - spyOn(classroomBackendApiService, 'fetchClassroomDataAsync') - .and.returnValue(Promise.resolve(classroomData)); - spyOn(i18nLanguageCodeService, 'getClassroomTranslationKey') - .and.returnValue('I18N_CLASSROOM_MATH_TITLE'); - spyOn(i18nLanguageCodeService, 'isHackyTranslationAvailable') - .and.returnValue(true); - spyOn(i18nLanguageCodeService, 'isCurrentLanguageEnglish') - .and.returnValue(false); + 'Math', + topicSummaryDicts, + 'Course details', + 'Topics covered' + ); + spyOn( + accessValidationBackendApiService, + 'validateAccessToClassroomPage' + ).and.returnValue(Promise.resolve()); + spyOn( + classroomBackendApiService, + 'fetchClassroomDataAsync' + ).and.returnValue(Promise.resolve(classroomData)); + spyOn( + i18nLanguageCodeService, + 'getClassroomTranslationKey' + ).and.returnValue('I18N_CLASSROOM_MATH_TITLE'); + spyOn( + i18nLanguageCodeService, + 'isHackyTranslationAvailable' + ).and.returnValue(true); + spyOn(i18nLanguageCodeService, 'isCurrentLanguageEnglish').and.returnValue( + false + ); component.ngOnInit(); tick(); tick(); expect(component.classroomUrlFragment).toEqual(classroomUrlFragment); expect(component.bannerImageFileUrl).toEqual(bannerImageUrl); expect(loaderService.showLoadingScreen).toHaveBeenCalled(); - expect(classroomBackendApiService.fetchClassroomDataAsync) - .toHaveBeenCalled(); + expect( + classroomBackendApiService.fetchClassroomDataAsync + ).toHaveBeenCalled(); expect(component.classroomData).toEqual(classroomData); expect(component.classroomDisplayName).toEqual(classroomData.getName()); expect(component.classroomNameTranslationKey).toBe( - 'I18N_CLASSROOM_MATH_TITLE'); + 'I18N_CLASSROOM_MATH_TITLE' + ); expect(component.isHackyClassroomTranslationDisplayed()).toBe(true); expect(component.setPageTitle).toHaveBeenCalled(); expect(component.subscribeToOnLangChange).toHaveBeenCalled(); expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); - expect(classroomBackendApiService.onInitializeTranslation.emit) - .toHaveBeenCalled(); + expect( + classroomBackendApiService.onInitializeTranslation.emit + ).toHaveBeenCalled(); expect(siteAnalyticsService.registerClassroomPageViewed).toHaveBeenCalled(); })); - it('should display alert when unable to fetch classroom data', - fakeAsync(() => { - let classroomUrlFragment = 'test_fragment'; - let bannerImageUrl = 'banner_image_url'; - spyOn(urlService, 'getClassroomUrlFragmentFromUrl') - .and.returnValue(classroomUrlFragment); - spyOn(urlInterpolationService, 'getStaticImageUrl') - .and.returnValue(bannerImageUrl); - spyOn(loaderService, 'showLoadingScreen'); - spyOn(accessValidationBackendApiService, 'validateAccessToClassroomPage') - .and.returnValue(Promise.resolve()); - spyOn(classroomBackendApiService, 'fetchClassroomDataAsync') - .and.returnValue(Promise.reject({ status: 500 })); - spyOn(alertsService, 'addWarning'); - component.ngOnInit(); - tick(); - expect(component.classroomUrlFragment).toEqual(classroomUrlFragment); - expect(component.bannerImageFileUrl).toEqual(bannerImageUrl); - expect(loaderService.showLoadingScreen).toHaveBeenCalled(); - expect(classroomBackendApiService.fetchClassroomDataAsync) - .toHaveBeenCalled(); - expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Failed to get dashboard data'); - })); + it('should display alert when unable to fetch classroom data', fakeAsync(() => { + let classroomUrlFragment = 'test_fragment'; + let bannerImageUrl = 'banner_image_url'; + spyOn(urlService, 'getClassroomUrlFragmentFromUrl').and.returnValue( + classroomUrlFragment + ); + spyOn(urlInterpolationService, 'getStaticImageUrl').and.returnValue( + bannerImageUrl + ); + spyOn(loaderService, 'showLoadingScreen'); + spyOn( + accessValidationBackendApiService, + 'validateAccessToClassroomPage' + ).and.returnValue(Promise.resolve()); + spyOn( + classroomBackendApiService, + 'fetchClassroomDataAsync' + ).and.returnValue(Promise.reject({status: 500})); + spyOn(alertsService, 'addWarning'); + component.ngOnInit(); + tick(); + expect(component.classroomUrlFragment).toEqual(classroomUrlFragment); + expect(component.bannerImageFileUrl).toEqual(bannerImageUrl); + expect(loaderService.showLoadingScreen).toHaveBeenCalled(); + expect( + classroomBackendApiService.fetchClassroomDataAsync + ).toHaveBeenCalled(); + expect(alertsService.addWarning).toHaveBeenCalledWith( + 'Failed to get dashboard data' + ); + })); - it('should obtain translated page title whenever the selected' + - 'language changes', () => { - component.subscribeToOnLangChange(); - spyOn(component, 'setPageTitle'); - translateService.onLangChange.emit(); + it( + 'should obtain translated page title whenever the selected' + + 'language changes', + () => { + component.subscribeToOnLangChange(); + spyOn(component, 'setPageTitle'); + translateService.onLangChange.emit(); - expect(component.directiveSubscriptions.closed).toBe(false); - expect(component.setPageTitle).toHaveBeenCalled(); - }); + expect(component.directiveSubscriptions.closed).toBe(false); + expect(component.setPageTitle).toHaveBeenCalled(); + } + ); it('should set new page title', () => { spyOn(translateService, 'instant').and.callThrough(); @@ -250,11 +284,14 @@ describe('Classroom Page Component', () => { component.setPageTitle(); expect(translateService.instant).toHaveBeenCalledWith( - 'I18N_CLASSROOM_PAGE_TITLE', { - classroomName: 'dummy_name' - }); + 'I18N_CLASSROOM_PAGE_TITLE', + { + classroomName: 'dummy_name', + } + ); expect(pageTitleService.setDocumentTitle).toHaveBeenCalledWith( - 'I18N_CLASSROOM_PAGE_TITLE'); + 'I18N_CLASSROOM_PAGE_TITLE' + ); }); it('should unsubscribe on component destruction', () => { diff --git a/core/templates/pages/classroom-page/classroom-page.component.ts b/core/templates/pages/classroom-page/classroom-page.component.ts index b7910d9dab3f..ed4b4aae66d1 100644 --- a/core/templates/pages/classroom-page/classroom-page.component.ts +++ b/core/templates/pages/classroom-page/classroom-page.component.ts @@ -16,31 +16,31 @@ * @fileoverview Component for the classroom page. */ -import { Component, OnDestroy } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; - -import { AppConstants } from 'app.constants'; -import { ClassroomBackendApiService } from 'domain/classroom/classroom-backend-api.service'; -import { ClassroomData } from 'domain/classroom/classroom-data.model'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { CapitalizePipe } from 'filters/string-utility-filters/capitalize.pipe'; -import { AccessValidationBackendApiService } from 'pages/oppia-root/routing/access-validation-backend-api.service'; -import { AlertsService } from 'services/alerts.service'; -import { UrlService } from 'services/contextual/url.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { LoaderService } from 'services/loader.service'; -import { PageTitleService } from 'services/page-title.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { PlatformFeatureService } from 'services/platform-feature.service'; +import {Component, OnDestroy} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; + +import {AppConstants} from 'app.constants'; +import {ClassroomBackendApiService} from 'domain/classroom/classroom-backend-api.service'; +import {ClassroomData} from 'domain/classroom/classroom-data.model'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {CapitalizePipe} from 'filters/string-utility-filters/capitalize.pipe'; +import {AccessValidationBackendApiService} from 'pages/oppia-root/routing/access-validation-backend-api.service'; +import {AlertsService} from 'services/alerts.service'; +import {UrlService} from 'services/contextual/url.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {LoaderService} from 'services/loader.service'; +import {PageTitleService} from 'services/page-title.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {PlatformFeatureService} from 'services/platform-feature.service'; import './classroom-page.component.css'; @Component({ selector: 'oppia-classroom-page', templateUrl: './classroom-page.component.html', - styleUrls: ['./classroom-page.component.css'] + styleUrls: ['./classroom-page.component.css'], }) export class ClassroomPageComponent implements OnDestroy { directiveSubscriptions = new Subscription(); @@ -57,8 +57,7 @@ export class ClassroomPageComponent implements OnDestroy { firstTopicUrl: string = ''; constructor( - private accessValidationBackendApiService: - AccessValidationBackendApiService, + private accessValidationBackendApiService: AccessValidationBackendApiService, private alertsService: AlertsService, private capitalizePipe: CapitalizePipe, private classroomBackendApiService: ClassroomBackendApiService, @@ -74,59 +73,79 @@ export class ClassroomPageComponent implements OnDestroy { ) {} ngOnInit(): void { - this.classroomUrlFragment = ( - this.urlService.getClassroomUrlFragmentFromUrl()); - this.bannerImageFileUrl = this.urlInterpolationService.getStaticImageUrl( - '/splash/books.svg'); + this.classroomUrlFragment = + this.urlService.getClassroomUrlFragmentFromUrl(); + this.bannerImageFileUrl = + this.urlInterpolationService.getStaticImageUrl('/splash/books.svg'); this.loaderService.showLoadingScreen('Loading'); this.isDiagnosticTestFeatureFlagEnabled(); - this.accessValidationBackendApiService.validateAccessToClassroomPage( - this.classroomUrlFragment).then(() => { - this.classroomBackendApiService.fetchClassroomDataAsync( - this.classroomUrlFragment).then((classroomData) => { - this.classroomData = classroomData; - this.classroomDisplayName = this.capitalizePipe.transform( - classroomData.getName()); - this.classroomNameTranslationKey = this.i18nLanguageCodeService. - getClassroomTranslationKey(this.classroomDisplayName); - this.setPageTitle(); - this.subscribeToOnLangChange(); - this.loaderService.hideLoadingScreen(); - this.classroomBackendApiService.onInitializeTranslation.emit(); - this.siteAnalyticsService.registerClassroomPageViewed(); - if (classroomData && classroomData.getTopicSummaries().length > 0) { - let firstTopic = classroomData.getTopicSummaries()[0].name; - this.firstTopicUrl = '/learn/math/' + ( - classroomData.getTopicSummaries()[0].urlFragment); - - this.beginWithFirstTopicButtonText = this.translateService.instant( - 'I18N_CLASSROOM_PAGE_BEGIN_WITH_FIRST_TOPIC_BUTTON', { - firstTopic: firstTopic - } - ); - - this.begineWithFirstTopicDescriptionText = ( - this.translateService.instant( - 'I18N_CLASSROOM_PAGE_NEW_TO_MATH_TEXT', { - firstTopic: firstTopic + this.accessValidationBackendApiService + .validateAccessToClassroomPage(this.classroomUrlFragment) + .then( + () => { + this.classroomBackendApiService + .fetchClassroomDataAsync(this.classroomUrlFragment) + .then( + classroomData => { + this.classroomData = classroomData; + this.classroomDisplayName = this.capitalizePipe.transform( + classroomData.getName() + ); + this.classroomNameTranslationKey = + this.i18nLanguageCodeService.getClassroomTranslationKey( + this.classroomDisplayName + ); + this.setPageTitle(); + this.subscribeToOnLangChange(); + this.loaderService.hideLoadingScreen(); + this.classroomBackendApiService.onInitializeTranslation.emit(); + this.siteAnalyticsService.registerClassroomPageViewed(); + if ( + classroomData && + classroomData.getTopicSummaries().length > 0 + ) { + let firstTopic = classroomData.getTopicSummaries()[0].name; + this.firstTopicUrl = + '/learn/math/' + + classroomData.getTopicSummaries()[0].urlFragment; + + this.beginWithFirstTopicButtonText = + this.translateService.instant( + 'I18N_CLASSROOM_PAGE_BEGIN_WITH_FIRST_TOPIC_BUTTON', + { + firstTopic: firstTopic, + } + ); + + this.begineWithFirstTopicDescriptionText = + this.translateService.instant( + 'I18N_CLASSROOM_PAGE_NEW_TO_MATH_TEXT', + { + firstTopic: firstTopic, + } + ); + } + }, + errorResponse => { + if ( + AppConstants.FATAL_ERROR_CODES.indexOf( + errorResponse.status + ) !== -1 + ) { + this.alertsService.addWarning('Failed to get dashboard data'); + } } - ) - ); - } - }, (errorResponse) => { - if (AppConstants.FATAL_ERROR_CODES.indexOf( - errorResponse.status) !== -1) { - this.alertsService.addWarning('Failed to get dashboard data'); + ); + }, + err => { + // Note to developers: + // This callback is triggered when the provided classroom does not exist, + // this will raise page not found exception. + // No further action is needed. } - }); - }, (err) => { - // Note to developers: - // This callback is triggered when the provided classroom does not exist, - // this will raise page not found exception. - // No further action is needed. - }); + ); } subscribeToOnLangChange(): void { @@ -139,9 +158,11 @@ export class ClassroomPageComponent implements OnDestroy { setPageTitle(): void { let translatedTitle = this.translateService.instant( - 'I18N_CLASSROOM_PAGE_TITLE', { - classroomName: this.classroomDisplayName - }); + 'I18N_CLASSROOM_PAGE_TITLE', + { + classroomName: this.classroomDisplayName, + } + ); this.pageTitleService.setDocumentTitle(translatedTitle); } @@ -168,7 +189,9 @@ export class ClassroomPageComponent implements OnDestroy { } } -angular.module('oppia').directive('oppiaClassroomPage', +angular.module('oppia').directive( + 'oppiaClassroomPage', downgradeComponent({ - component: ClassroomPageComponent - }) as angular.IDirectiveFactory); + component: ClassroomPageComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/classroom-page/classroom-page.module.ts b/core/templates/pages/classroom-page/classroom-page.module.ts index 55df3a8ca08b..85b21372dc8a 100644 --- a/core/templates/pages/classroom-page/classroom-page.module.ts +++ b/core/templates/pages/classroom-page/classroom-page.module.ts @@ -16,21 +16,21 @@ * @fileoverview Module for the classroom page. */ -import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; -import { NgModule } from '@angular/core'; -import { TranslateModule } from '@ngx-translate/core'; +import {CommonModule} from '@angular/common'; +import {FormsModule} from '@angular/forms'; +import {NgModule} from '@angular/core'; +import {TranslateModule} from '@ngx-translate/core'; -import { BackgroundBannerModule } from 'components/common-layout-directives/common-elements/background-banner.module'; -import { BaseModule } from 'base-components/base.module'; -import { ClassroomPageComponent } from './classroom-page.component'; -import { ClassroomPageRootComponent } from './classroom-page-root.component'; -import { ClassroomPageRoutingModule } from './classroom-page-routing.module'; -import { RichTextComponentsModule } from 'rich_text_components/rich-text-components.module'; -import { SearchBarModule } from 'pages/library-page/search-bar/search-bar.module'; -import { StringUtilityPipesModule } from 'filters/string-utility-filters/string-utility-pipes.module'; -import { SummaryTilesModule } from 'components/summary-tile/summary-tile.module'; -import { Error404PageModule } from 'pages/error-pages/error-404/error-404-page.module'; +import {BackgroundBannerModule} from 'components/common-layout-directives/common-elements/background-banner.module'; +import {BaseModule} from 'base-components/base.module'; +import {ClassroomPageComponent} from './classroom-page.component'; +import {ClassroomPageRootComponent} from './classroom-page-root.component'; +import {ClassroomPageRoutingModule} from './classroom-page-routing.module'; +import {RichTextComponentsModule} from 'rich_text_components/rich-text-components.module'; +import {SearchBarModule} from 'pages/library-page/search-bar/search-bar.module'; +import {StringUtilityPipesModule} from 'filters/string-utility-filters/string-utility-pipes.module'; +import {SummaryTilesModule} from 'components/summary-tile/summary-tile.module'; +import {Error404PageModule} from 'pages/error-pages/error-404/error-404-page.module'; @NgModule({ imports: [ @@ -44,15 +44,9 @@ import { Error404PageModule } from 'pages/error-pages/error-404/error-404-page.m StringUtilityPipesModule, SummaryTilesModule, TranslateModule, - Error404PageModule + Error404PageModule, ], - declarations: [ - ClassroomPageComponent, - ClassroomPageRootComponent, - ], - entryComponents: [ - ClassroomPageComponent, - ClassroomPageRootComponent - ] + declarations: [ClassroomPageComponent, ClassroomPageRootComponent], + entryComponents: [ClassroomPageComponent, ClassroomPageRootComponent], }) export class ClassroomPageModule {} diff --git a/core/templates/pages/collection-editor-page/collection-editor-page.component.spec.ts b/core/templates/pages/collection-editor-page/collection-editor-page.component.spec.ts index ea35510c8511..e4e34716aaea 100644 --- a/core/templates/pages/collection-editor-page/collection-editor-page.component.spec.ts +++ b/core/templates/pages/collection-editor-page/collection-editor-page.component.spec.ts @@ -16,17 +16,17 @@ * @fileoverview Unit tests for collection editor page component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; - -import { Collection } from 'domain/collection/collection.model'; -import { UrlService } from 'services/contextual/url.service'; -import { PageTitleService } from 'services/page-title.service'; -import { CollectionEditorPageComponent } from './collection-editor-page.component'; -import { CollectionEditorRoutingService } from './services/collection-editor-routing.service'; -import { CollectionEditorStateService } from './services/collection-editor-state.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; + +import {Collection} from 'domain/collection/collection.model'; +import {UrlService} from 'services/contextual/url.service'; +import {PageTitleService} from 'services/page-title.service'; +import {CollectionEditorPageComponent} from './collection-editor-page.component'; +import {CollectionEditorRoutingService} from './services/collection-editor-routing.service'; +import {CollectionEditorStateService} from './services/collection-editor-state.service'; class MockTranslateService { onLangChange: EventEmitter = new EventEmitter(); @@ -47,12 +47,8 @@ describe('Collection editor page component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - CollectionEditorPageComponent - ], + imports: [HttpClientTestingModule], + declarations: [CollectionEditorPageComponent], providers: [ CollectionEditorRoutingService, CollectionEditorStateService, @@ -60,10 +56,10 @@ describe('Collection editor page component', () => { UrlService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -72,7 +68,8 @@ describe('Collection editor page component', () => { componentInstance = fixture.componentInstance; collectionEditorStateService = TestBed.inject(CollectionEditorStateService); collectionEditorRoutingService = TestBed.inject( - CollectionEditorRoutingService); + CollectionEditorRoutingService + ); pageTitleService = TestBed.inject(PageTitleService); urlService = TestBed.inject(UrlService); translateService = TestBed.inject(TranslateService); @@ -86,13 +83,16 @@ describe('Collection editor page component', () => { let mockOnCollectionInitializedEventEmitter = new EventEmitter(); let collectionId = 'collection_id'; - spyOnProperty(collectionEditorStateService, 'onCollectionInitialized') - .and.returnValue(mockOnCollectionInitializedEventEmitter); + spyOnProperty( + collectionEditorStateService, + 'onCollectionInitialized' + ).and.returnValue(mockOnCollectionInitializedEventEmitter); spyOn(collectionEditorStateService, 'loadCollection'); spyOn(componentInstance, 'setTitle'); spyOn(componentInstance, 'subscribeToOnLangChange'); spyOn(urlService, 'getCollectionIdFromEditorUrl').and.returnValue( - collectionId); + collectionId + ); componentInstance.ngOnInit(); mockOnCollectionInitializedEventEmitter.emit(); @@ -101,14 +101,16 @@ describe('Collection editor page component', () => { expect(componentInstance.setTitle).toHaveBeenCalled(); expect(componentInstance.subscribeToOnLangChange).toHaveBeenCalled(); expect(collectionEditorStateService.loadCollection).toHaveBeenCalledWith( - collectionId); + collectionId + ); }); it('should destroy', () => { spyOn(componentInstance.directiveSubscriptions, 'unsubscribe'); componentInstance.ngOnDestroy(); expect( - componentInstance.directiveSubscriptions.unsubscribe).toHaveBeenCalled(); + componentInstance.directiveSubscriptions.unsubscribe + ).toHaveBeenCalled(); }); it('should set page title whenever the selected language changes', () => { @@ -122,12 +124,14 @@ describe('Collection editor page component', () => { it('should obtain translation and set page title', () => { let pageTitle = 'test title'; - spyOn(collectionEditorStateService, 'getCollection').and.returnValues({ - getTitle: () => pageTitle - } as Collection, - { - getTitle: () => '' - } as Collection); + spyOn(collectionEditorStateService, 'getCollection').and.returnValues( + { + getTitle: () => pageTitle, + } as Collection, + { + getTitle: () => '', + } as Collection + ); spyOn(translateService, 'instant').and.callThrough(); spyOn(pageTitleService, 'setDocumentTitle'); @@ -136,23 +140,28 @@ describe('Collection editor page component', () => { expect(translateService.instant).toHaveBeenCalledTimes(2); expect(translateService.instant).toHaveBeenCalledWith( - 'I18N_COLLECTION_EDITOR_PAGE_TITLE', { - collectionTitle: pageTitle + 'I18N_COLLECTION_EDITOR_PAGE_TITLE', + { + collectionTitle: pageTitle, } ); expect(translateService.instant).toHaveBeenCalledWith( - 'I18N_COLLECTION_EDITOR_UNTITLED_COLLECTION_PAGE_TITLE'); + 'I18N_COLLECTION_EDITOR_UNTITLED_COLLECTION_PAGE_TITLE' + ); expect(pageTitleService.setDocumentTitle).toHaveBeenCalledTimes(2); expect(pageTitleService.setDocumentTitle).toHaveBeenCalledWith( - 'I18N_COLLECTION_EDITOR_PAGE_TITLE'); + 'I18N_COLLECTION_EDITOR_PAGE_TITLE' + ); expect(pageTitleService.setDocumentTitle).toHaveBeenCalledWith( - 'I18N_COLLECTION_EDITOR_UNTITLED_COLLECTION_PAGE_TITLE'); + 'I18N_COLLECTION_EDITOR_UNTITLED_COLLECTION_PAGE_TITLE' + ); }); it('should tell active tab name', () => { let activeTabName = 'Active Tab'; spyOn(collectionEditorRoutingService, 'getActiveTabName').and.returnValue( - activeTabName); + activeTabName + ); expect(componentInstance.getActiveTabName()).toEqual(activeTabName); }); }); diff --git a/core/templates/pages/collection-editor-page/collection-editor-page.component.ts b/core/templates/pages/collection-editor-page/collection-editor-page.component.ts index ba286a1c2719..26d6ac184f06 100644 --- a/core/templates/pages/collection-editor-page/collection-editor-page.component.ts +++ b/core/templates/pages/collection-editor-page/collection-editor-page.component.ts @@ -16,19 +16,19 @@ * @fileoverview Primary component for the collection editor page. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; -import { UrlService } from 'services/contextual/url.service'; -import { PageTitleService } from 'services/page-title.service'; -import { CollectionEditorRoutingService } from './services/collection-editor-routing.service'; -import { CollectionEditorStateService } from './services/collection-editor-state.service'; +import {UrlService} from 'services/contextual/url.service'; +import {PageTitleService} from 'services/page-title.service'; +import {CollectionEditorRoutingService} from './services/collection-editor-routing.service'; +import {CollectionEditorStateService} from './services/collection-editor-state.service'; @Component({ selector: 'oppia-collection-editor-page', - templateUrl: './collection-editor-page.component.html' + templateUrl: './collection-editor-page.component.html', }) export class CollectionEditorPageComponent implements OnInit, OnDestroy { directiveSubscriptions = new Subscription(); @@ -56,7 +56,8 @@ export class CollectionEditorPageComponent implements OnInit, OnDestroy { ); // Load the collection to be edited. this.collectionEditorStateService.loadCollection( - this.urlService.getCollectionIdFromEditorUrl()); + this.urlService.getCollectionIdFromEditorUrl() + ); } ngOnDestroy(): void { @@ -72,17 +73,19 @@ export class CollectionEditorPageComponent implements OnInit, OnDestroy { } setTitle(): void { - var title = ( - this.collectionEditorStateService.getCollection().getTitle()); + var title = this.collectionEditorStateService.getCollection().getTitle(); let translatedTitle: string; if (title) { translatedTitle = this.translateService.instant( - 'I18N_COLLECTION_EDITOR_PAGE_TITLE', { - collectionTitle: title - }); + 'I18N_COLLECTION_EDITOR_PAGE_TITLE', + { + collectionTitle: title, + } + ); } else { translatedTitle = this.translateService.instant( - 'I18N_COLLECTION_EDITOR_UNTITLED_COLLECTION_PAGE_TITLE'); + 'I18N_COLLECTION_EDITOR_UNTITLED_COLLECTION_PAGE_TITLE' + ); } this.pageTitleService.setDocumentTitle(translatedTitle); } @@ -92,7 +95,9 @@ export class CollectionEditorPageComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive('oppiaCollectionEditorPage', +angular.module('oppia').directive( + 'oppiaCollectionEditorPage', downgradeComponent({ - component: CollectionEditorPageComponent - }) as angular.IDirectiveFactory); + component: CollectionEditorPageComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/collection-editor-page/collection-editor-page.constants.ts b/core/templates/pages/collection-editor-page/collection-editor-page.constants.ts index 26fe1b522fa5..ee0c11a26709 100644 --- a/core/templates/pages/collection-editor-page/collection-editor-page.constants.ts +++ b/core/templates/pages/collection-editor-page/collection-editor-page.constants.ts @@ -22,6 +22,5 @@ export const CollectionEditorPageConstants = { COLLECTION_RIGHTS_URL_TEMPLATE: '/collection_editor_handler/rights/', - COLLECTION_TITLE_INPUT_FOCUS_LABEL: - 'collectionTitleInputFocusLabel' + COLLECTION_TITLE_INPUT_FOCUS_LABEL: 'collectionTitleInputFocusLabel', } as const; diff --git a/core/templates/pages/collection-editor-page/collection-editor-page.import.ts b/core/templates/pages/collection-editor-page/collection-editor-page.import.ts index 9221372a04c5..0f39caeba146 100644 --- a/core/templates/pages/collection-editor-page/collection-editor-page.import.ts +++ b/core/templates/pages/collection-editor-page/collection-editor-page.import.ts @@ -22,9 +22,14 @@ import 'zone.js'; import uiValidate from 'angular-ui-validate'; angular.module('oppia', [ - require('angular-cookies'), 'ngAnimate', - 'ngMaterial', 'ngSanitize', 'ngTouch', 'pascalprecht.translate', - 'ui.bootstrap', uiValidate + require('angular-cookies'), + 'ngAnimate', + 'ngMaterial', + 'ngSanitize', + 'ngTouch', + 'pascalprecht.translate', + 'ui.bootstrap', + uiValidate, ]); require('Polyfills.ts'); @@ -39,7 +44,9 @@ require('base-components/base-content.component.ts'); require('pages/collection-editor-page/collection-editor-page.component.ts'); require( 'pages/collection-editor-page/navbar/' + - 'collection-editor-navbar-breadcrumb.component.ts'); + 'collection-editor-navbar-breadcrumb.component.ts' +); require( 'pages/collection-editor-page/navbar/' + - 'collection-editor-navbar.component.ts'); + 'collection-editor-navbar.component.ts' +); diff --git a/core/templates/pages/collection-editor-page/collection-editor-page.module.ts b/core/templates/pages/collection-editor-page/collection-editor-page.module.ts index f02e37b5e872..89e59ba3ba90 100644 --- a/core/templates/pages/collection-editor-page/collection-editor-page.module.ts +++ b/core/templates/pages/collection-editor-page/collection-editor-page.module.ts @@ -16,38 +16,39 @@ * @fileoverview Module for the collection editor page. */ -import { APP_INITIALIZER, NgModule, StaticProvider } from '@angular/core'; -import { BrowserModule, HAMMER_GESTURE_CONFIG } from '@angular/platform-browser'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { HttpClientModule } from '@angular/common/http'; -import { HTTP_INTERCEPTORS } from '@angular/common/http'; -import { RouterModule } from '@angular/router'; -import { APP_BASE_HREF } from '@angular/common'; -import { RequestInterceptor } from 'services/request-interceptor.service'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { OppiaAngularRootComponent } from - 'components/oppia-angular-root.component'; +import {APP_INITIALIZER, NgModule, StaticProvider} from '@angular/core'; +import {BrowserModule, HAMMER_GESTURE_CONFIG} from '@angular/platform-browser'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {HttpClientModule} from '@angular/common/http'; +import {HTTP_INTERCEPTORS} from '@angular/common/http'; +import {RouterModule} from '@angular/router'; +import {APP_BASE_HREF} from '@angular/common'; +import {RequestInterceptor} from 'services/request-interceptor.service'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {OppiaAngularRootComponent} from 'components/oppia-angular-root.component'; -import { CollectionHistoryTabComponent } from - 'pages/collection-editor-page/history-tab/collection-history-tab.component'; -import { CollectionNodeEditorComponent } from './editor-tab/collection-node-editor.component'; -import { CollectionSettingsTabComponent } from 'pages/collection-editor-page/settings-tab/collection-settings-tab.component'; -import { CollectionStatisticsTabComponent } from 'pages/collection-editor-page/statistics-tab/collection-statistics-tab.component'; -import { platformFeatureInitFactory, PlatformFeatureService } from 'services/platform-feature.service'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { CollectionDetailsEditorComponent } from './settings-tab/collection-details-editor.component'; -import { CollectionPermissionsCardComponent } from './settings-tab/collection-permissions-card.component'; -import { CollectionEditorNavbarBreadcrumbComponent } from './navbar/collection-editor-navbar-breadcrumb.component'; -import { CollectionEditorNavbarComponent } from './navbar/collection-editor-navbar.component'; -import { CollectionNodeCreatorComponent } from './editor-tab/collection-node-creator.component'; -import { CollectionEditorTabComponent } from './editor-tab/collection-editor-tab.component'; -import { CollectionEditorSaveModalComponent } from './modals/collection-editor-save-modal.component'; -import { CollectionEditorPrePublishModalComponent } from './modals/collection-editor-pre-publish-modal.component'; -import { ToastrModule } from 'ngx-toastr'; -import { MyHammerConfig, toastrConfig } from 'pages/oppia-root/app.module'; -import { CollectionEditorPageComponent } from './collection-editor-page.component'; -import { SmartRouterModule } from 'hybrid-router-module-provider'; -import { AppErrorHandlerProvider } from 'pages/oppia-root/app-error-handler'; +import {CollectionHistoryTabComponent} from 'pages/collection-editor-page/history-tab/collection-history-tab.component'; +import {CollectionNodeEditorComponent} from './editor-tab/collection-node-editor.component'; +import {CollectionSettingsTabComponent} from 'pages/collection-editor-page/settings-tab/collection-settings-tab.component'; +import {CollectionStatisticsTabComponent} from 'pages/collection-editor-page/statistics-tab/collection-statistics-tab.component'; +import { + platformFeatureInitFactory, + PlatformFeatureService, +} from 'services/platform-feature.service'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {CollectionDetailsEditorComponent} from './settings-tab/collection-details-editor.component'; +import {CollectionPermissionsCardComponent} from './settings-tab/collection-permissions-card.component'; +import {CollectionEditorNavbarBreadcrumbComponent} from './navbar/collection-editor-navbar-breadcrumb.component'; +import {CollectionEditorNavbarComponent} from './navbar/collection-editor-navbar.component'; +import {CollectionNodeCreatorComponent} from './editor-tab/collection-node-creator.component'; +import {CollectionEditorTabComponent} from './editor-tab/collection-editor-tab.component'; +import {CollectionEditorSaveModalComponent} from './modals/collection-editor-save-modal.component'; +import {CollectionEditorPrePublishModalComponent} from './modals/collection-editor-pre-publish-modal.component'; +import {ToastrModule} from 'ngx-toastr'; +import {MyHammerConfig, toastrConfig} from 'pages/oppia-root/app.module'; +import {CollectionEditorPageComponent} from './collection-editor-page.component'; +import {SmartRouterModule} from 'hybrid-router-module-provider'; +import {AppErrorHandlerProvider} from 'pages/oppia-root/app-error-handler'; @NgModule({ imports: [ @@ -60,7 +61,7 @@ import { AppErrorHandlerProvider } from 'pages/oppia-root/app-error-handler'; RouterModule.forRoot([]), SharedComponentsModule, FormsModule, - ToastrModule.forRoot(toastrConfig) + ToastrModule.forRoot(toastrConfig), ], declarations: [ CollectionNodeCreatorComponent, @@ -75,7 +76,7 @@ import { AppErrorHandlerProvider } from 'pages/oppia-root/app-error-handler'; CollectionNodeEditorComponent, CollectionPermissionsCardComponent, CollectionSettingsTabComponent, - CollectionStatisticsTabComponent + CollectionStatisticsTabComponent, ], entryComponents: [ CollectionNodeCreatorComponent, @@ -95,35 +96,35 @@ import { AppErrorHandlerProvider } from 'pages/oppia-root/app-error-handler'; { provide: HTTP_INTERCEPTORS, useClass: RequestInterceptor, - multi: true + multi: true, }, { provide: APP_INITIALIZER, useFactory: platformFeatureInitFactory, deps: [PlatformFeatureService], - multi: true + multi: true, }, { provide: HAMMER_GESTURE_CONFIG, - useClass: MyHammerConfig + useClass: MyHammerConfig, }, AppErrorHandlerProvider, { provide: APP_BASE_HREF, - useValue: '/' - } - ] + useValue: '/', + }, + ], }) class CollectionEditorPageModule { // Empty placeholder method to satisfy the `Compiler`. ngDoBootstrap() {} } -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { downgradeModule } from '@angular/upgrade/static'; -import { FormsModule } from '@angular/forms'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {downgradeModule} from '@angular/upgrade/static'; +import {FormsModule} from '@angular/forms'; -const bootstrapFnAsync = async(extraProviders: StaticProvider[]) => { +const bootstrapFnAsync = async (extraProviders: StaticProvider[]) => { const platformRef = platformBrowserDynamic(extraProviders); return platformRef.bootstrapModule(CollectionEditorPageModule); }; @@ -138,5 +139,6 @@ angular.module('oppia').directive( // bootstrap the Angular 8. 'oppiaAngularRoot', downgradeComponent({ - component: OppiaAngularRootComponent - }) as angular.IDirectiveFactory); + component: OppiaAngularRootComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/collection-editor-page/editor-tab/collection-editor-tab.component.spec.ts b/core/templates/pages/collection-editor-page/editor-tab/collection-editor-tab.component.spec.ts index c0814fc1d6aa..c7f504c1c37e 100644 --- a/core/templates/pages/collection-editor-page/editor-tab/collection-editor-tab.component.spec.ts +++ b/core/templates/pages/collection-editor-page/editor-tab/collection-editor-tab.component.spec.ts @@ -16,15 +16,15 @@ * @fileoverview Unit tests for collection editor tab component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { CollectionNode } from 'domain/collection/collection-node.model'; -import { CollectionPlaythrough } from 'domain/collection/collection-playthrough.model'; -import { Collection } from 'domain/collection/collection.model'; -import { CollectionEditorStateService } from '../services/collection-editor-state.service'; -import { CollectionLinearizerService } from '../services/collection-linearizer.service'; -import { CollectionEditorTabComponent } from './collection-editor-tab.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {CollectionNode} from 'domain/collection/collection-node.model'; +import {CollectionPlaythrough} from 'domain/collection/collection-playthrough.model'; +import {Collection} from 'domain/collection/collection.model'; +import {CollectionEditorStateService} from '../services/collection-editor-state.service'; +import {CollectionLinearizerService} from '../services/collection-linearizer.service'; +import {CollectionEditorTabComponent} from './collection-editor-tab.component'; describe('Collection editor tab component', () => { let componentInstance: CollectionEditorTabComponent; @@ -34,22 +34,24 @@ describe('Collection editor tab component', () => { let collectionLinearizerService: CollectionLinearizerService; let mockCollection = new Collection( - '', '', '', '', [], new CollectionPlaythrough(null, []), '', 0, 1, []); - + '', + '', + '', + '', + [], + new CollectionPlaythrough(null, []), + '', + 0, + 1, + [] + ); beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - CollectionEditorTabComponent - ], - providers: [ - CollectionEditorStateService, - CollectionLinearizerService - ], - schemas: [NO_ERRORS_SCHEMA] + imports: [HttpClientTestingModule], + declarations: [CollectionEditorTabComponent], + providers: [CollectionEditorStateService, CollectionLinearizerService], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -63,7 +65,8 @@ describe('Collection editor tab component', () => { it('should initialize', () => { spyOn(collectionEditorStateService, 'getCollection').and.returnValue( - mockCollection); + mockCollection + ); componentInstance.ngOnInit(); @@ -74,19 +77,24 @@ describe('Collection editor tab component', () => { componentInstance.collection = mockCollection; let collectionNode = new CollectionNode({ exploration_id: 'id', - exploration_summary: null + exploration_summary: null, }); - spyOn(collectionLinearizerService, 'getCollectionNodesInPlayableOrder') - .and.returnValue([collectionNode]); + spyOn( + collectionLinearizerService, + 'getCollectionNodesInPlayableOrder' + ).and.returnValue([collectionNode]); - expect(componentInstance.getLinearlySortedNodes()).toEqual( - [collectionNode]); + expect(componentInstance.getLinearlySortedNodes()).toEqual([ + collectionNode, + ]); }); it('should tell if collection is loaded', () => { - spyOn(collectionEditorStateService, 'hasLoadedCollection') - .and.returnValues(true, false); + spyOn(collectionEditorStateService, 'hasLoadedCollection').and.returnValues( + true, + false + ); expect(componentInstance.hasLoadedCollection()).toBeTrue(); expect(componentInstance.hasLoadedCollection()).toBeFalse(); }); diff --git a/core/templates/pages/collection-editor-page/editor-tab/collection-editor-tab.component.ts b/core/templates/pages/collection-editor-page/editor-tab/collection-editor-tab.component.ts index c46fa9ccfd40..eead7672fa9b 100644 --- a/core/templates/pages/collection-editor-page/editor-tab/collection-editor-tab.component.ts +++ b/core/templates/pages/collection-editor-page/editor-tab/collection-editor-tab.component.ts @@ -16,15 +16,15 @@ * @fileoverview Component for the main tab of the collection editor. */ -import { Component } from '@angular/core'; -import { CollectionNode } from 'domain/collection/collection-node.model'; -import { Collection } from 'domain/collection/collection.model'; -import { CollectionEditorStateService } from '../services/collection-editor-state.service'; -import { CollectionLinearizerService } from '../services/collection-linearizer.service'; +import {Component} from '@angular/core'; +import {CollectionNode} from 'domain/collection/collection-node.model'; +import {Collection} from 'domain/collection/collection.model'; +import {CollectionEditorStateService} from '../services/collection-editor-state.service'; +import {CollectionLinearizerService} from '../services/collection-linearizer.service'; @Component({ selector: 'oppia-collection-editor-tab', - templateUrl: './collection-editor-tab.component.html' + templateUrl: './collection-editor-tab.component.html', }) export class CollectionEditorTabComponent { // This property is initialized using Angular lifecycle hooks @@ -43,7 +43,8 @@ export class CollectionEditorTabComponent { getLinearlySortedNodes(): CollectionNode[] { return this.collectionLinearizerService.getCollectionNodesInPlayableOrder( - this.collection); + this.collection + ); } hasLoadedCollection(): boolean { diff --git a/core/templates/pages/collection-editor-page/editor-tab/collection-node-creator.component.spec.ts b/core/templates/pages/collection-editor-page/editor-tab/collection-node-creator.component.spec.ts index 16a9cc3c7a9c..63b2ca31ed7b 100644 --- a/core/templates/pages/collection-editor-page/editor-tab/collection-node-creator.component.spec.ts +++ b/core/templates/pages/collection-editor-page/editor-tab/collection-node-creator.component.spec.ts @@ -16,21 +16,27 @@ * @fileoverview Unit tests for collection node creator component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { ExplorationCreationBackendApiService } from 'components/entity-creation-services/exploration-creation-backend-api.service'; -import { CollectionPlaythrough } from 'domain/collection/collection-playthrough.model'; -import { Collection } from 'domain/collection/collection.model'; -import { SearchExplorationsBackendApiService } from 'domain/collection/search-explorations-backend-api.service'; -import { ExplorationSummaryBackendApiService } from 'domain/summary/exploration-summary-backend-api.service'; -import { NormalizeWhitespacePipe } from 'filters/string-utility-filters/normalize-whitespace.pipe'; -import { AlertsService } from 'services/alerts.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { ValidatorsService } from 'services/validators.service'; -import { CollectionEditorStateService } from '../services/collection-editor-state.service'; -import { CollectionLinearizerService } from '../services/collection-linearizer.service'; -import { CollectionNodeCreatorComponent } from './collection-node-creator.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {ExplorationCreationBackendApiService} from 'components/entity-creation-services/exploration-creation-backend-api.service'; +import {CollectionPlaythrough} from 'domain/collection/collection-playthrough.model'; +import {Collection} from 'domain/collection/collection.model'; +import {SearchExplorationsBackendApiService} from 'domain/collection/search-explorations-backend-api.service'; +import {ExplorationSummaryBackendApiService} from 'domain/summary/exploration-summary-backend-api.service'; +import {NormalizeWhitespacePipe} from 'filters/string-utility-filters/normalize-whitespace.pipe'; +import {AlertsService} from 'services/alerts.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {ValidatorsService} from 'services/validators.service'; +import {CollectionEditorStateService} from '../services/collection-editor-state.service'; +import {CollectionLinearizerService} from '../services/collection-linearizer.service'; +import {CollectionNodeCreatorComponent} from './collection-node-creator.component'; describe('Collection node creator component', () => { let fixture: ComponentFixture; @@ -41,22 +47,26 @@ describe('Collection node creator component', () => { let collectionLinearizerService: CollectionLinearizerService; let normalizeWhitespacePipe: NormalizeWhitespacePipe; let validatorsService: ValidatorsService; - let explorationCreationBackendApiService: - ExplorationCreationBackendApiService; + let explorationCreationBackendApiService: ExplorationCreationBackendApiService; let siteAnalyticsService: SiteAnalyticsService; let mockCollection = new Collection( - 'collection_id', 'collection_title', 'collection_objective', 'en', [], - new CollectionPlaythrough(null, []), '', 2, 3, []); + 'collection_id', + 'collection_title', + 'collection_objective', + 'en', + [], + new CollectionPlaythrough(null, []), + '', + 2, + 3, + [] + ); beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - CollectionNodeCreatorComponent - ], + imports: [HttpClientTestingModule], + declarations: [CollectionNodeCreatorComponent], providers: [ AlertsService, CollectionEditorStateService, @@ -66,9 +76,9 @@ describe('Collection node creator component', () => { SearchExplorationsBackendApiService, SiteAnalyticsService, ValidatorsService, - NormalizeWhitespacePipe + NormalizeWhitespacePipe, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -78,18 +88,21 @@ describe('Collection node creator component', () => { collectionEditorStateService = TestBed.inject(CollectionEditorStateService); alertsService = TestBed.inject(AlertsService); explorationSummaryBackendApiService = TestBed.inject( - ExplorationSummaryBackendApiService); + ExplorationSummaryBackendApiService + ); collectionLinearizerService = TestBed.inject(CollectionLinearizerService); normalizeWhitespacePipe = TestBed.inject(NormalizeWhitespacePipe); validatorsService = TestBed.inject(ValidatorsService); explorationCreationBackendApiService = TestBed.inject( - ExplorationCreationBackendApiService); + ExplorationCreationBackendApiService + ); siteAnalyticsService = TestBed.inject(SiteAnalyticsService); }); it('should initialize', () => { spyOn(collectionEditorStateService, 'getCollection').and.returnValue( - mockCollection); + mockCollection + ); componentInstance.ngOnInit(); expect(componentInstance.collection).toEqual(mockCollection); }); @@ -99,124 +112,160 @@ describe('Collection node creator component', () => { let expId = 'new_id'; spyOn( explorationSummaryBackendApiService, - 'loadPublicAndPrivateExplorationSummariesAsync') - .and.returnValue(Promise.resolve({ - summaries: [{ - category: '', - community_owned: true, - human_readable_contributors_summary: {}, - id: expId, - language_code: '', - num_views: 1, - objective: '', - status: '', - tags: [], - thumbnail_bg_color: '', - thumbnail_icon_url: '', - title: '' - }] - })); + 'loadPublicAndPrivateExplorationSummariesAsync' + ).and.returnValue( + Promise.resolve({ + summaries: [ + { + category: '', + community_owned: true, + human_readable_contributors_summary: {}, + id: expId, + language_code: '', + num_views: 1, + objective: '', + status: '', + tags: [], + thumbnail_bg_color: '', + thumbnail_icon_url: '', + title: '', + }, + ], + }) + ); spyOn(collectionLinearizerService, 'appendCollectionNode'); - spyOn(componentInstance.collection, 'containsCollectionNode') - .and.returnValue(false); + spyOn( + componentInstance.collection, + 'containsCollectionNode' + ).and.returnValue(false); componentInstance.addExplorationToCollection(expId); tick(); expect(collectionLinearizerService.appendCollectionNode).toHaveBeenCalled(); })); - it('should should not add exploration to collection if exploration' + - ' id is empty', () => { - spyOn(alertsService, 'addWarning'); - componentInstance.addExplorationToCollection(''); - expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Cannot add an empty exploration ID.'); - }); + it( + 'should should not add exploration to collection if exploration' + + ' id is empty', + () => { + spyOn(alertsService, 'addWarning'); + componentInstance.addExplorationToCollection(''); + expect(alertsService.addWarning).toHaveBeenCalledWith( + 'Cannot add an empty exploration ID.' + ); + } + ); it('should not add exploration to collection if it is already added', () => { componentInstance.collection = mockCollection; spyOn(alertsService, 'addWarning'); - spyOn(componentInstance.collection, 'containsCollectionNode') - .and.returnValue(true); + spyOn( + componentInstance.collection, + 'containsCollectionNode' + ).and.returnValue(true); componentInstance.addExplorationToCollection('exp'); expect(alertsService.addWarning).toHaveBeenCalledWith( - 'There is already an exploration in this collection with that id.'); + 'There is already an exploration in this collection with that id.' + ); }); - it('should show warning if request to backend fails while adding ' + - 'exploration to collection', fakeAsync(() => { - componentInstance.collection = mockCollection; - let expId = 'new_id'; - spyOn(alertsService, 'addWarning'); - spyOn( - explorationSummaryBackendApiService, - 'loadPublicAndPrivateExplorationSummariesAsync') - .and.returnValue(Promise.reject()); - spyOn(collectionLinearizerService, 'appendCollectionNode'); - spyOn(componentInstance.collection, 'containsCollectionNode') - .and.returnValue(false); - componentInstance.addExplorationToCollection(expId); - tick(); - expect(alertsService.addWarning).toHaveBeenCalledWith( - 'There was an error while adding an exploration to the collection.'); - })); - - it('should show an alert if exploration doesnot exist in collection', + it( + 'should show warning if request to backend fails while adding ' + + 'exploration to collection', fakeAsync(() => { componentInstance.collection = mockCollection; let expId = 'new_id'; spyOn(alertsService, 'addWarning'); spyOn( explorationSummaryBackendApiService, - 'loadPublicAndPrivateExplorationSummariesAsync') - .and.returnValue(Promise.resolve({ - summaries: [] - })); + 'loadPublicAndPrivateExplorationSummariesAsync' + ).and.returnValue(Promise.reject()); spyOn(collectionLinearizerService, 'appendCollectionNode'); - spyOn(componentInstance.collection, 'containsCollectionNode') - .and.returnValue(false); + spyOn( + componentInstance.collection, + 'containsCollectionNode' + ).and.returnValue(false); componentInstance.addExplorationToCollection(expId); tick(); expect(alertsService.addWarning).toHaveBeenCalledWith( - 'That exploration does not exist or you do not have edit access to it.' + 'There was an error while adding an exploration to the collection.' ); - })); + }) + ); + + it('should show an alert if exploration doesnot exist in collection', fakeAsync(() => { + componentInstance.collection = mockCollection; + let expId = 'new_id'; + spyOn(alertsService, 'addWarning'); + spyOn( + explorationSummaryBackendApiService, + 'loadPublicAndPrivateExplorationSummariesAsync' + ).and.returnValue( + Promise.resolve({ + summaries: [], + }) + ); + spyOn(collectionLinearizerService, 'appendCollectionNode'); + spyOn( + componentInstance.collection, + 'containsCollectionNode' + ).and.returnValue(false); + componentInstance.addExplorationToCollection(expId); + tick(); + expect(alertsService.addWarning).toHaveBeenCalledWith( + 'That exploration does not exist or you do not have edit access to it.' + ); + })); it('should create new exploration', fakeAsync(() => { let expTitle = 'Exp Title'; let expId = 'newExpId'; spyOn(normalizeWhitespacePipe, 'transform').and.returnValue(expTitle); spyOn(validatorsService, 'isValidExplorationTitle').and.returnValue(true); - spyOn(explorationCreationBackendApiService, 'registerNewExplorationAsync') - .and.returnValue(Promise.resolve({ - explorationId: expId - })); spyOn( - siteAnalyticsService, 'registerCreateNewExplorationInCollectionEvent'); + explorationCreationBackendApiService, + 'registerNewExplorationAsync' + ).and.returnValue( + Promise.resolve({ + explorationId: expId, + }) + ); + spyOn( + siteAnalyticsService, + 'registerCreateNewExplorationInCollectionEvent' + ); spyOn(componentInstance, 'addExplorationToCollection'); componentInstance.createNewExploration(); tick(); expect(normalizeWhitespacePipe.transform).toHaveBeenCalled(); expect(validatorsService.isValidExplorationTitle).toHaveBeenCalled(); - expect(explorationCreationBackendApiService.registerNewExplorationAsync) - .toHaveBeenCalled(); + expect( + explorationCreationBackendApiService.registerNewExplorationAsync + ).toHaveBeenCalled(); expect(componentInstance.newExplorationTitle).toEqual(''); - expect(siteAnalyticsService.registerCreateNewExplorationInCollectionEvent) - .toHaveBeenCalledWith(expId); + expect( + siteAnalyticsService.registerCreateNewExplorationInCollectionEvent + ).toHaveBeenCalledWith(expId); expect(componentInstance.addExplorationToCollection).toHaveBeenCalledWith( - expId); + expId + ); })); it('should not create exploration if title is not valid', fakeAsync(() => { spyOn(normalizeWhitespacePipe, 'transform').and.returnValue(''); spyOn(validatorsService, 'isValidExplorationTitle').and.returnValue(false); - spyOn(explorationCreationBackendApiService, 'registerNewExplorationAsync') - .and.returnValue(Promise.resolve({ - explorationId: '' - })); + spyOn( + explorationCreationBackendApiService, + 'registerNewExplorationAsync' + ).and.returnValue( + Promise.resolve({ + explorationId: '', + }) + ); componentInstance.createNewExploration(); tick(); - expect(explorationCreationBackendApiService.registerNewExplorationAsync) - .not.toHaveBeenCalled(); + expect( + explorationCreationBackendApiService.registerNewExplorationAsync + ).not.toHaveBeenCalled(); })); it('should check if id is malformed', () => { diff --git a/core/templates/pages/collection-editor-page/editor-tab/collection-node-creator.component.ts b/core/templates/pages/collection-editor-page/editor-tab/collection-node-creator.component.ts index 8f1b8863a8cc..d9d58d5d807f 100755 --- a/core/templates/pages/collection-editor-page/editor-tab/collection-node-creator.component.ts +++ b/core/templates/pages/collection-editor-page/editor-tab/collection-node-creator.component.ts @@ -16,21 +16,21 @@ * @fileoverview Component for creating a new collection node. */ -import { Component } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { ExplorationCreationBackendApiService } from 'components/entity-creation-services/exploration-creation-backend-api.service'; -import { Collection } from 'domain/collection/collection.model'; -import { ExplorationSummaryBackendApiService } from 'domain/summary/exploration-summary-backend-api.service'; -import { NormalizeWhitespacePipe } from 'filters/string-utility-filters/normalize-whitespace.pipe'; -import { AlertsService } from 'services/alerts.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { ValidatorsService } from 'services/validators.service'; -import { CollectionEditorStateService } from '../services/collection-editor-state.service'; -import { CollectionLinearizerService } from '../services/collection-linearizer.service'; +import {Component} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {ExplorationCreationBackendApiService} from 'components/entity-creation-services/exploration-creation-backend-api.service'; +import {Collection} from 'domain/collection/collection.model'; +import {ExplorationSummaryBackendApiService} from 'domain/summary/exploration-summary-backend-api.service'; +import {NormalizeWhitespacePipe} from 'filters/string-utility-filters/normalize-whitespace.pipe'; +import {AlertsService} from 'services/alerts.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {ValidatorsService} from 'services/validators.service'; +import {CollectionEditorStateService} from '../services/collection-editor-state.service'; +import {CollectionLinearizerService} from '../services/collection-linearizer.service'; @Component({ selector: 'oppia-collection-node-creator', - templateUrl: './collection-node-creator.component.html' + templateUrl: './collection-node-creator.component.html', }) export class CollectionNodeCreatorComponent { // This property is initialized using Angular lifecycle hooks @@ -45,10 +45,8 @@ export class CollectionNodeCreatorComponent { private alertsService: AlertsService, private collectionEditorStateService: CollectionEditorStateService, private collectionLinearizerService: CollectionLinearizerService, - private explorationCreationBackendApiService: - ExplorationCreationBackendApiService, - private explorationSummaryBackendApiService: - ExplorationSummaryBackendApiService, + private explorationCreationBackendApiService: ExplorationCreationBackendApiService, + private explorationSummaryBackendApiService: ExplorationSummaryBackendApiService, private siteAnalyticsService: SiteAnalyticsService, private validatorsService: ValidatorsService, private normalizeWhitespacePipe: NormalizeWhitespacePipe @@ -65,57 +63,67 @@ export class CollectionNodeCreatorComponent { } if (this.collection.containsCollectionNode(newExplorationId)) { this.alertsService.addWarning( - 'There is already an exploration in this collection ' + - 'with that id.'); + 'There is already an exploration in this collection ' + 'with that id.' + ); return; } this.explorationSummaryBackendApiService .loadPublicAndPrivateExplorationSummariesAsync([newExplorationId]) - .then((responseObject) => { - let summaries = responseObject.summaries; - let summaryBackendObject = null; - if (summaries.length !== 0 && - summaries[0].id === newExplorationId) { - summaryBackendObject = summaries[0]; - } - - if (summaryBackendObject) { - this.collectionLinearizerService.appendCollectionNode( - this.collection, newExplorationId, summaryBackendObject); - } else { + .then( + responseObject => { + let summaries = responseObject.summaries; + let summaryBackendObject = null; + if (summaries.length !== 0 && summaries[0].id === newExplorationId) { + summaryBackendObject = summaries[0]; + } + + if (summaryBackendObject) { + this.collectionLinearizerService.appendCollectionNode( + this.collection, + newExplorationId, + summaryBackendObject + ); + } else { + this.alertsService.addWarning( + 'That exploration does not exist or you do not have edit ' + + 'access to it.' + ); + } + }, + () => { this.alertsService.addWarning( - 'That exploration does not exist or you do not have edit ' + - 'access to it.'); + 'There was an error while adding an exploration to the ' + + 'collection.' + ); } - }, () => { - this.alertsService.addWarning( - 'There was an error while adding an exploration to the ' + - 'collection.'); - }); + ); } // Creates a new exploration, then adds it to the collection. createNewExploration(): void { - let title = ( - this.normalizeWhitespacePipe.transform(this.newExplorationTitle)); + let title = this.normalizeWhitespacePipe.transform( + this.newExplorationTitle + ); if (!this.validatorsService.isValidExplorationTitle(title, true)) { return; } // Create a new exploration with the given title. - this.explorationCreationBackendApiService.registerNewExplorationAsync({ - title: title - }).then((response) => { - this.newExplorationTitle = ''; - let newExplorationId = response.explorationId; - - this.siteAnalyticsService - .registerCreateNewExplorationInCollectionEvent( - newExplorationId); - this.addExplorationToCollection(newExplorationId); - }); + this.explorationCreationBackendApiService + .registerNewExplorationAsync({ + title: title, + }) + .then(response => { + this.newExplorationTitle = ''; + let newExplorationId = response.explorationId; + + this.siteAnalyticsService.registerCreateNewExplorationInCollectionEvent( + newExplorationId + ); + this.addExplorationToCollection(newExplorationId); + }); } // Checks whether the user has left a '#' at the end of their ID @@ -124,18 +132,19 @@ export class CollectionNodeCreatorComponent { isIdMalformed(typedExplorationId: string): boolean { return ( Boolean(typedExplorationId) && - typedExplorationId.lastIndexOf('#') === - typedExplorationId.length - 1); + typedExplorationId.lastIndexOf('#') === typedExplorationId.length - 1 + ); } - addExploration(): void { this.addExplorationToCollection(this.newExplorationId); this.newExplorationId = ''; } } -angular.module('oppia').directive('oppiaCollectionNodeCreator', +angular.module('oppia').directive( + 'oppiaCollectionNodeCreator', downgradeComponent({ - component: CollectionNodeCreatorComponent - }) as angular.IDirectiveFactory); + component: CollectionNodeCreatorComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/collection-editor-page/editor-tab/collection-node-editor.component.spec.ts b/core/templates/pages/collection-editor-page/editor-tab/collection-node-editor.component.spec.ts index 6a4103785f02..80b8a4819ff6 100644 --- a/core/templates/pages/collection-editor-page/editor-tab/collection-node-editor.component.spec.ts +++ b/core/templates/pages/collection-editor-page/editor-tab/collection-node-editor.component.spec.ts @@ -16,19 +16,21 @@ * @fileoverview Unit tests for for Collection node editor component. */ -import { async, ComponentFixture, TestBed } from - '@angular/core/testing'; -import { CollectionNodeEditorComponent } from './collection-node-editor.component'; -import { CollectionLinearizerService } from '../services/collection-linearizer.service'; -import { AlertsService } from 'services/alerts.service'; -import { CollectionEditorStateService } from '../services/collection-editor-state.service'; -import { Collection, CollectionBackendDict } from 'domain/collection/collection.model'; -import { CollectionNode } from 'domain/collection/collection-node.model'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { MaterialModule } from 'modules/material.module'; -import { FormsModule } from '@angular/forms'; -import { LearnerExplorationSummaryBackendDict } from 'domain/summary/learner-exploration-summary.model'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {CollectionNodeEditorComponent} from './collection-node-editor.component'; +import {CollectionLinearizerService} from '../services/collection-linearizer.service'; +import {AlertsService} from 'services/alerts.service'; +import {CollectionEditorStateService} from '../services/collection-editor-state.service'; +import { + Collection, + CollectionBackendDict, +} from 'domain/collection/collection.model'; +import {CollectionNode} from 'domain/collection/collection-node.model'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {MaterialModule} from 'modules/material.module'; +import {FormsModule} from '@angular/forms'; +import {LearnerExplorationSummaryBackendDict} from 'domain/summary/learner-exploration-summary.model'; describe('Collection node editor component ', () => { let component: CollectionNodeEditorComponent; @@ -38,8 +40,7 @@ describe('Collection node editor component ', () => { let collectionEditorStateService: CollectionEditorStateService; let sampleCollection: Collection; - let learnerExplorationSummaryBackendDict: - LearnerExplorationSummaryBackendDict; + let learnerExplorationSummaryBackendDict: LearnerExplorationSummaryBackendDict; beforeEach(async(() => { TestBed.configureTestingModule({ @@ -47,10 +48,10 @@ describe('Collection node editor component ', () => { BrowserAnimationsModule, MaterialModule, FormsModule, - HttpClientTestingModule + HttpClientTestingModule, ], declarations: [CollectionNodeEditorComponent], - providers: [] + providers: [], }).compileComponents(); })); @@ -61,8 +62,8 @@ describe('Collection node editor component ', () => { alertsService = TestBed.inject(AlertsService); collectionEditorStateService = TestBed.inject(CollectionEditorStateService); - component.collectionNode = CollectionNode.createFromExplorationId( - 'exp_id0'); + component.collectionNode = + CollectionNode.createFromExplorationId('exp_id0'); learnerExplorationSummaryBackendDict = { activity_type: 'exploration', @@ -76,7 +77,7 @@ describe('Collection node editor component ', () => { 2: 0, 3: 0, 4: 0, - 5: 0 + 5: 0, }, human_readable_contributors_summary: {}, language_code: 'en', @@ -86,7 +87,7 @@ describe('Collection node editor component ', () => { tags: [], thumbnail_bg_color: '#cc4b00', thumbnail_icon_url: '/subjects/Algebra.svg', - title: 'a title' + title: 'a title', }; let sampleCollectionBackendObject: CollectionBackendDict = { @@ -98,25 +99,27 @@ describe('Collection node editor component ', () => { category: 'a category', version: 1, schema_version: 1, - nodes: [{ - exploration_id: 'exp_id0', - exploration_summary: learnerExplorationSummaryBackendDict - }, - { - exploration_id: 'exp_id1', - exploration_summary: learnerExplorationSummaryBackendDict - }], + nodes: [ + { + exploration_id: 'exp_id0', + exploration_summary: learnerExplorationSummaryBackendDict, + }, + { + exploration_id: 'exp_id1', + exploration_summary: learnerExplorationSummaryBackendDict, + }, + ], playthrough_dict: { next_exploration_id: 'expId', - completed_exploration_ids: ['expId2'] - } + completed_exploration_ids: ['expId2'], + }, }; - sampleCollection = Collection.create( - sampleCollectionBackendObject); + sampleCollection = Collection.create(sampleCollectionBackendObject); - spyOn(collectionEditorStateService, 'getCollection') - .and.returnValue(sampleCollection); + spyOn(collectionEditorStateService, 'getCollection').and.returnValue( + sampleCollection + ); fixture.detectChanges(); }); @@ -125,72 +128,78 @@ describe('Collection node editor component ', () => { expect(component.collection).toEqual(sampleCollection); }); - it('should delete node when calling \'deleteNode\'', () => { - let removeSpy = spyOn(collectionLinearizerService, 'removeCollectionNode') - .and.returnValue(true); + it("should delete node when calling 'deleteNode'", () => { + let removeSpy = spyOn( + collectionLinearizerService, + 'removeCollectionNode' + ).and.returnValue(true); component.deleteNode(); expect(removeSpy).toHaveBeenCalled(); }); - it('should not delete node from collection in case ' + - 'of backend error', () => { - spyOn(collectionLinearizerService, 'removeCollectionNode') - .and.returnValue(false); - let alertsSpy = spyOn(alertsService, 'fatalWarning') - .and.returnValue(); - - component.deleteNode(); - - expect(alertsSpy).toHaveBeenCalledWith( - 'Internal collection editor error. Could not delete ' + - 'exploration by ID: exp_id0'); - }); - - it('should shift node to left when calling \'shiftNodeLeft\'', () => { - let shiftNodeLeftSpy = spyOn(collectionLinearizerService, 'shiftNodeLeft') - .and.returnValue(true); + it( + 'should not delete node from collection in case ' + 'of backend error', + () => { + spyOn( + collectionLinearizerService, + 'removeCollectionNode' + ).and.returnValue(false); + let alertsSpy = spyOn(alertsService, 'fatalWarning').and.returnValue(); + + component.deleteNode(); + + expect(alertsSpy).toHaveBeenCalledWith( + 'Internal collection editor error. Could not delete ' + + 'exploration by ID: exp_id0' + ); + } + ); + + it("should shift node to left when calling 'shiftNodeLeft'", () => { + let shiftNodeLeftSpy = spyOn( + collectionLinearizerService, + 'shiftNodeLeft' + ).and.returnValue(true); component.shiftNodeLeft(); expect(shiftNodeLeftSpy).toHaveBeenCalled(); }); - it('should not shift node to left in case ' + - 'of backend error', () => { - spyOn(collectionLinearizerService, 'shiftNodeLeft') - .and.returnValue(false); - let alertsSpy = spyOn(alertsService, 'fatalWarning') - .and.returnValue(); + it('should not shift node to left in case ' + 'of backend error', () => { + spyOn(collectionLinearizerService, 'shiftNodeLeft').and.returnValue(false); + let alertsSpy = spyOn(alertsService, 'fatalWarning').and.returnValue(); component.shiftNodeLeft(); expect(alertsSpy).toHaveBeenCalledWith( 'Internal collection editor error. Could not shift node left ' + - 'with ID: exp_id0'); + 'with ID: exp_id0' + ); }); - it('should shift node to right when calling \'shiftNodeRight\'', () => { - let shiftNodeRightSpy = spyOn(collectionLinearizerService, 'shiftNodeRight') - .and.returnValue(true); + it("should shift node to right when calling 'shiftNodeRight'", () => { + let shiftNodeRightSpy = spyOn( + collectionLinearizerService, + 'shiftNodeRight' + ).and.returnValue(true); component.shiftNodeRight(); expect(shiftNodeRightSpy).toHaveBeenCalled(); }); - it('should not shift node to right in case ' + - 'of backend error', () => { - spyOn(collectionLinearizerService, 'shiftNodeRight') - .and.returnValue(false); - let alertsSpy = spyOn(alertsService, 'fatalWarning') - .and.returnValue(); + it('should not shift node to right in case ' + 'of backend error', () => { + spyOn(collectionLinearizerService, 'shiftNodeRight').and.returnValue(false); + let alertsSpy = spyOn(alertsService, 'fatalWarning').and.returnValue(); component.shiftNodeRight(); expect(alertsSpy).toHaveBeenCalledWith( 'Internal collection editor error. Could not shift node right ' + - 'with ID: exp_id0'); + 'with ID: exp_id0' + ); }); }); diff --git a/core/templates/pages/collection-editor-page/editor-tab/collection-node-editor.component.ts b/core/templates/pages/collection-editor-page/editor-tab/collection-node-editor.component.ts index b79b1bc45fc9..945935c7fe76 100644 --- a/core/templates/pages/collection-editor-page/editor-tab/collection-node-editor.component.ts +++ b/core/templates/pages/collection-editor-page/editor-tab/collection-node-editor.component.ts @@ -18,17 +18,17 @@ * and also delete the collection node represented by this directive. */ -import { Component, Input, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { CollectionNode } from 'domain/collection/collection-node.model'; -import { Collection } from 'domain/collection/collection.model'; -import { AlertsService } from 'services/alerts.service'; -import { CollectionEditorStateService } from '../services/collection-editor-state.service'; -import { CollectionLinearizerService } from '../services/collection-linearizer.service'; +import {Component, Input, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {CollectionNode} from 'domain/collection/collection-node.model'; +import {Collection} from 'domain/collection/collection.model'; +import {AlertsService} from 'services/alerts.service'; +import {CollectionEditorStateService} from '../services/collection-editor-state.service'; +import {CollectionLinearizerService} from '../services/collection-linearizer.service'; @Component({ selector: 'oppia-collection-node-editor', - templateUrl: './collection-node-editor.component.html' + templateUrl: './collection-node-editor.component.html', }) export class CollectionNodeEditorComponent implements OnInit { // These properties are initialized using Angular lifecycle hooks @@ -43,18 +43,24 @@ export class CollectionNodeEditorComponent implements OnInit { constructor( private collectionLinearizerService: CollectionLinearizerService, private alertsService: AlertsService, - private collectionEditorStateService: CollectionEditorStateService, + private collectionEditorStateService: CollectionEditorStateService ) {} // Deletes this collection node from the frontend collection // object and also updates the changelist. deleteNode(): void { this.explorationId = this.collectionNode.getExplorationId(); - if (!this.collectionLinearizerService.removeCollectionNode( - this.collection, this.explorationId)) { + if ( + !this.collectionLinearizerService.removeCollectionNode( + this.collection, + this.explorationId + ) + ) { this.alertsService.fatalWarning( 'Internal collection editor error. Could not delete ' + - 'exploration by ID: ' + this.explorationId); + 'exploration by ID: ' + + this.explorationId + ); } } @@ -62,11 +68,17 @@ export class CollectionNodeEditorComponent implements OnInit { // collection, if possible. shiftNodeLeft(): void { this.explorationId = this.collectionNode.getExplorationId(); - if (!this.collectionLinearizerService.shiftNodeLeft( - this.collection, this.explorationId)) { + if ( + !this.collectionLinearizerService.shiftNodeLeft( + this.collection, + this.explorationId + ) + ) { this.alertsService.fatalWarning( 'Internal collection editor error. Could not shift node left ' + - 'with ID: ' + this.explorationId); + 'with ID: ' + + this.explorationId + ); } } @@ -74,11 +86,17 @@ export class CollectionNodeEditorComponent implements OnInit { // collection, if possible. shiftNodeRight(): void { this.explorationId = this.collectionNode.getExplorationId(); - if (!this.collectionLinearizerService.shiftNodeRight( - this.collection, this.explorationId)) { + if ( + !this.collectionLinearizerService.shiftNodeRight( + this.collection, + this.explorationId + ) + ) { this.alertsService.fatalWarning( 'Internal collection editor error. Could not shift node ' + - 'right with ID: ' + this.explorationId); + 'right with ID: ' + + this.explorationId + ); } } @@ -88,6 +106,9 @@ export class CollectionNodeEditorComponent implements OnInit { } } -angular.module('oppia').directive( - 'oppiaCollectionNodeEditor', downgradeComponent( - {component: CollectionNodeEditorComponent})); +angular + .module('oppia') + .directive( + 'oppiaCollectionNodeEditor', + downgradeComponent({component: CollectionNodeEditorComponent}) + ); diff --git a/core/templates/pages/collection-editor-page/history-tab/collection-history-tab.component.ts b/core/templates/pages/collection-editor-page/history-tab/collection-history-tab.component.ts index 00e6e7edb142..65038c0eeddc 100644 --- a/core/templates/pages/collection-editor-page/history-tab/collection-history-tab.component.ts +++ b/core/templates/pages/collection-editor-page/history-tab/collection-history-tab.component.ts @@ -16,17 +16,21 @@ * @fileoverview Component for the history tab of the collection editor. */ -import { Component } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; @Component({ selector: 'collection-history-tab', templateUrl: './collection-history-tab.component.html', - styleUrls: [] + styleUrls: [], }) export class CollectionHistoryTabComponent { constructor() {} } -angular.module('oppia').directive('collectionHistoryTab', downgradeComponent( - {component: CollectionHistoryTabComponent})); +angular + .module('oppia') + .directive( + 'collectionHistoryTab', + downgradeComponent({component: CollectionHistoryTabComponent}) + ); diff --git a/core/templates/pages/collection-editor-page/modals/collection-editor-pre-publish-modal.component.spec.ts b/core/templates/pages/collection-editor-page/modals/collection-editor-pre-publish-modal.component.spec.ts index 7087f38a9606..ea8974a615e6 100644 --- a/core/templates/pages/collection-editor-page/modals/collection-editor-pre-publish-modal.component.spec.ts +++ b/core/templates/pages/collection-editor-page/modals/collection-editor-pre-publish-modal.component.spec.ts @@ -16,16 +16,16 @@ * @fileoverview Unit tests for collection editor pre publish modal. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap'; -import { CollectionPlaythrough } from 'domain/collection/collection-playthrough.model'; -import { CollectionUpdateService } from 'domain/collection/collection-update.service'; -import { Collection } from 'domain/collection/collection.model'; -import { AlertsService } from 'services/alerts.service'; -import { CollectionEditorStateService } from '../services/collection-editor-state.service'; -import { CollectionEditorPrePublishModalComponent } from './collection-editor-pre-publish-modal.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal, NgbModalModule} from '@ng-bootstrap/ng-bootstrap'; +import {CollectionPlaythrough} from 'domain/collection/collection-playthrough.model'; +import {CollectionUpdateService} from 'domain/collection/collection-update.service'; +import {Collection} from 'domain/collection/collection.model'; +import {AlertsService} from 'services/alerts.service'; +import {CollectionEditorStateService} from '../services/collection-editor-state.service'; +import {CollectionEditorPrePublishModalComponent} from './collection-editor-pre-publish-modal.component'; describe('Collection editor pre publish modal component', () => { let fixture: ComponentFixture; @@ -37,20 +37,15 @@ describe('Collection editor pre publish modal component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - NgbModalModule - ], - declarations: [ - CollectionEditorPrePublishModalComponent - ], + imports: [HttpClientTestingModule, NgbModalModule], + declarations: [CollectionEditorPrePublishModalComponent], providers: [ AlertsService, CollectionEditorStateService, CollectionUpdateService, - NgbActiveModal + NgbActiveModal, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -67,10 +62,20 @@ describe('Collection editor pre publish modal component', () => { let collectionTitle = 'collection_title'; let collectionObjective = 'collection_objective'; let mockCollection = new Collection( - 'collection_id', collectionTitle, collectionObjective, 'en', [], - new CollectionPlaythrough(null, []), '', 2, 3, []); - spyOn(collectionEditorStateService, 'getCollection') - .and.returnValue(mockCollection); + 'collection_id', + collectionTitle, + collectionObjective, + 'en', + [], + new CollectionPlaythrough(null, []), + '', + 2, + 3, + [] + ); + spyOn(collectionEditorStateService, 'getCollection').and.returnValue( + mockCollection + ); componentInstance.ngOnInit(); expect(componentInstance.newTitle).toEqual(collectionTitle); expect(componentInstance.newObjective).toEqual(collectionObjective); @@ -93,26 +98,39 @@ describe('Collection editor pre publish modal component', () => { let collectionTitle = 'collection_title'; let collectionObjective = 'collection_objective'; let mockCollection = new Collection( - 'collection_id', collectionTitle, collectionObjective, 'en', [], - new CollectionPlaythrough(null, []), '', 2, 3, []); - spyOn(collectionEditorStateService, 'getCollection') - .and.returnValue(mockCollection); + 'collection_id', + collectionTitle, + collectionObjective, + 'en', + [], + new CollectionPlaythrough(null, []), + '', + 2, + 3, + [] + ); + spyOn(collectionEditorStateService, 'getCollection').and.returnValue( + mockCollection + ); componentInstance.ngOnInit(); spyOn(alertsService, 'addWarning'); componentInstance.newTitle = ''; componentInstance.save(); expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Please specify a title'); + 'Please specify a title' + ); componentInstance.newTitle = 'valid title'; componentInstance.newObjective = ''; componentInstance.save(); expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Please specify an objective'); + 'Please specify an objective' + ); componentInstance.newObjective = 'learn'; componentInstance.save(); expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Please specify a category'); + 'Please specify a category' + ); componentInstance.newCategory = 'ALGEBRA'; spyOn(collectionUpdateService, 'setCollectionObjective'); diff --git a/core/templates/pages/collection-editor-page/modals/collection-editor-pre-publish-modal.component.ts b/core/templates/pages/collection-editor-page/modals/collection-editor-pre-publish-modal.component.ts index 83c94894c531..a93399f08562 100644 --- a/core/templates/pages/collection-editor-page/modals/collection-editor-pre-publish-modal.component.ts +++ b/core/templates/pages/collection-editor-page/modals/collection-editor-pre-publish-modal.component.ts @@ -16,21 +16,20 @@ * @fileoverview Component for collection editor pre publish modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { AppConstants } from 'app.constants'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { CollectionUpdateService } from 'domain/collection/collection-update.service'; -import { Collection } from 'domain/collection/collection.model'; -import { AlertsService } from 'services/alerts.service'; -import { CollectionEditorStateService } from '../services/collection-editor-state.service'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {AppConstants} from 'app.constants'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {CollectionUpdateService} from 'domain/collection/collection-update.service'; +import {Collection} from 'domain/collection/collection.model'; +import {AlertsService} from 'services/alerts.service'; +import {CollectionEditorStateService} from '../services/collection-editor-state.service'; @Component({ selector: 'oppia-collection-editor-pre-publish-modal', - templateUrl: './collection-editor-pre-publish-modal.component.html' + templateUrl: './collection-editor-pre-publish-modal.component.html', }) -export class CollectionEditorPrePublishModalComponent - extends ConfirmOrCancelModal { +export class CollectionEditorPrePublishModalComponent extends ConfirmOrCancelModal { // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -87,17 +86,23 @@ export class CollectionEditorPrePublishModalComponent if (this.newTitle !== this._collection.getTitle()) { metadataList.push('title'); this.collectionUpdateService.setCollectionTitle( - this._collection, this.newTitle); + this._collection, + this.newTitle + ); } if (this.newObjective !== this._collection.getObjective()) { metadataList.push('objective'); this.collectionUpdateService.setCollectionObjective( - this._collection, this.newObjective); + this._collection, + this.newObjective + ); } if (this.newCategory !== this._collection.getCategory()) { metadataList.push('category'); this.collectionUpdateService.setCollectionCategory( - this._collection, this.newCategory); + this._collection, + this.newCategory + ); } this.ngbActiveModal.close(metadataList); diff --git a/core/templates/pages/collection-editor-page/modals/collection-editor-save-modal.component.spec.ts b/core/templates/pages/collection-editor-page/modals/collection-editor-save-modal.component.spec.ts index 0d09492bd73f..65d4b2c89b89 100644 --- a/core/templates/pages/collection-editor-page/modals/collection-editor-save-modal.component.spec.ts +++ b/core/templates/pages/collection-editor-page/modals/collection-editor-save-modal.component.spec.ts @@ -16,10 +16,10 @@ * @fileoverview Unit tests for collection editor save modal. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { CollectionEditorSaveModalComponent } from './collection-editor-save-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {CollectionEditorSaveModalComponent} from './collection-editor-save-modal.component'; describe('Collection editor save modal component', () => { let fixture: ComponentFixture; @@ -27,13 +27,9 @@ describe('Collection editor save modal component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - CollectionEditorSaveModalComponent - ], - providers: [ - NgbActiveModal - ], - schemas: [NO_ERRORS_SCHEMA] + declarations: [CollectionEditorSaveModalComponent], + providers: [NgbActiveModal], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); diff --git a/core/templates/pages/collection-editor-page/modals/collection-editor-save-modal.component.ts b/core/templates/pages/collection-editor-page/modals/collection-editor-save-modal.component.ts index ebf67d48839c..f0852d72f8f1 100644 --- a/core/templates/pages/collection-editor-page/modals/collection-editor-save-modal.component.ts +++ b/core/templates/pages/collection-editor-page/modals/collection-editor-save-modal.component.ts @@ -16,14 +16,14 @@ * @fileoverview Component for collection editor save modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { AppConstants } from 'app.constants'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {AppConstants} from 'app.constants'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-collection-editor-save-modal', - templateUrl: './collection-editor-save-modal.component.html' + templateUrl: './collection-editor-save-modal.component.html', }) export class CollectionEditorSaveModalComponent extends ConfirmOrCancelModal { collectionIsPrivate: boolean = false; diff --git a/core/templates/pages/collection-editor-page/navbar/collection-editor-navbar-breadcrumb.component.spec.ts b/core/templates/pages/collection-editor-page/navbar/collection-editor-navbar-breadcrumb.component.spec.ts index 15e2b4868efc..646fea4aa36f 100644 --- a/core/templates/pages/collection-editor-page/navbar/collection-editor-navbar-breadcrumb.component.spec.ts +++ b/core/templates/pages/collection-editor-page/navbar/collection-editor-navbar-breadcrumb.component.spec.ts @@ -16,16 +16,21 @@ * @fileoverview Unit tests for collection editor navbar breadcrumb component. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { TestBed, waitForAsync, ComponentFixture, fakeAsync } from '@angular/core/testing'; -import { CollectionEditorPageConstants } from '../collection-editor-page.constants'; -import { CollectionEditorNavbarBreadcrumbComponent } from './collection-editor-navbar-breadcrumb.component'; -import { CollectionEditorStateService } from '../services/collection-editor-state.service'; -import { CollectionEditorRoutingService } from '../services/collection-editor-routing.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { Collection } from 'domain/collection/collection.model'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { CollectionPlaythrough } from 'domain/collection/collection-playthrough.model'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + TestBed, + waitForAsync, + ComponentFixture, + fakeAsync, +} from '@angular/core/testing'; +import {CollectionEditorPageConstants} from '../collection-editor-page.constants'; +import {CollectionEditorNavbarBreadcrumbComponent} from './collection-editor-navbar-breadcrumb.component'; +import {CollectionEditorStateService} from '../services/collection-editor-state.service'; +import {CollectionEditorRoutingService} from '../services/collection-editor-routing.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {Collection} from 'domain/collection/collection.model'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {CollectionPlaythrough} from 'domain/collection/collection-playthrough.model'; describe('Collection editor navbar breadcrumb component', () => { let collectionEditorStateService: CollectionEditorStateService; @@ -46,74 +51,85 @@ describe('Collection editor navbar breadcrumb component', () => { category: 'Collection Category', version: 0, schemaVersion: 1, - nodes: [] + nodes: [], }; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - CollectionEditorNavbarBreadcrumbComponent - ], + imports: [HttpClientTestingModule], + declarations: [CollectionEditorNavbarBreadcrumbComponent], providers: [ CollectionEditorStateService, CollectionEditorRoutingService, - FocusManagerService + FocusManagerService, ], - schemas: [ - NO_ERRORS_SCHEMA - ] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent( - CollectionEditorNavbarBreadcrumbComponent); + CollectionEditorNavbarBreadcrumbComponent + ); component = fixture.componentInstance; collectionEditorStateService = TestBed.inject(CollectionEditorStateService); collectionEditorRoutingService = TestBed.inject( - CollectionEditorRoutingService); + CollectionEditorRoutingService + ); focusManagerService = TestBed.inject(FocusManagerService); }); beforeEach(() => { expectedCollection = new Collection( - collectionData.id, collectionData.title, collectionData.objective, - collectionData.languageCode, collectionData.tags, - collectionData.playthrough, collectionData.category, - collectionData.version, collectionData.schemaVersion, - collectionData.nodes); + collectionData.id, + collectionData.title, + collectionData.objective, + collectionData.languageCode, + collectionData.tags, + collectionData.playthrough, + collectionData.category, + collectionData.version, + collectionData.schemaVersion, + collectionData.nodes + ); }); beforeEach(() => { spyOn(collectionEditorStateService, 'getCollection').and.returnValue( - expectedCollection); + expectedCollection + ); spyOn(collectionEditorRoutingService, 'getActiveTabName').and.returnValue( - activeTab); + activeTab + ); spyOn(focusManagerService, 'setFocus'); component.ngOnInit(); }); - it('should load the component on opening collection editor page by clicking' + - 'on create collection', () => { - expect(component.collection).toBe(expectedCollection); - expect(component.activeTabName).toBe(activeTab); - }); + it( + 'should load the component on opening collection editor page by clicking' + + 'on create collection', + () => { + expect(component.collection).toBe(expectedCollection); + expect(component.activeTabName).toBe(activeTab); + } + ); it('should get the current tab name in readable form', () => { expect(component.getCurrentTabName()).toBe('Edit'); }); - it('should change the active tab to settings when clicked on' + - 'collection title if the title is empty', fakeAsync(() => { - expect(component.activeTabName).toBe(activeTab); + it( + 'should change the active tab to settings when clicked on' + + 'collection title if the title is empty', + fakeAsync(() => { + expect(component.activeTabName).toBe(activeTab); - component.editCollectionTitle(); + component.editCollectionTitle(); - expect(component.activeTabName).toBe('Settings'); - expect(focusManagerService.setFocus).toHaveBeenCalledWith( - CollectionEditorPageConstants.COLLECTION_TITLE_INPUT_FOCUS_LABEL); - })); + expect(component.activeTabName).toBe('Settings'); + expect(focusManagerService.setFocus).toHaveBeenCalledWith( + CollectionEditorPageConstants.COLLECTION_TITLE_INPUT_FOCUS_LABEL + ); + }) + ); }); diff --git a/core/templates/pages/collection-editor-page/navbar/collection-editor-navbar-breadcrumb.component.ts b/core/templates/pages/collection-editor-page/navbar/collection-editor-navbar-breadcrumb.component.ts index 28c5507af63e..dc41ad18bd4b 100644 --- a/core/templates/pages/collection-editor-page/navbar/collection-editor-navbar-breadcrumb.component.ts +++ b/core/templates/pages/collection-editor-page/navbar/collection-editor-navbar-breadcrumb.component.ts @@ -16,13 +16,13 @@ * @fileoverview Component for the navbar breadcrumb of the collection editor. */ -import { Component } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { Collection } from 'domain/collection/collection.model'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { CollectionEditorPageConstants } from '../collection-editor-page.constants'; -import { CollectionEditorRoutingService } from '../services/collection-editor-routing.service'; -import { CollectionEditorStateService } from '../services/collection-editor-state.service'; +import {Component} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {Collection} from 'domain/collection/collection.model'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {CollectionEditorPageConstants} from '../collection-editor-page.constants'; +import {CollectionEditorRoutingService} from '../services/collection-editor-routing.service'; +import {CollectionEditorStateService} from '../services/collection-editor-state.service'; // TODO(bhenning): After the navbar is moved to a directive, this directive // should be updated to say 'Loading...' if the collection editor's controller @@ -33,7 +33,7 @@ import { CollectionEditorStateService } from '../services/collection-editor-stat @Component({ selector: 'collection-editor-navbar-breadcrumb', - templateUrl: './collection-editor-navbar-breadcrumb.component.html' + templateUrl: './collection-editor-navbar-breadcrumb.component.html', }) export class CollectionEditorNavbarBreadcrumbComponent { // These properties are initialized using Angular lifecycle hooks @@ -62,7 +62,8 @@ export class CollectionEditorNavbarBreadcrumbComponent { editCollectionTitle(): void { this.activeTabName = this._TAB_NAMES_TO_HUMAN_READABLE_NAMES.settings; this.focusManagerService.setFocus( - CollectionEditorPageConstants.COLLECTION_TITLE_INPUT_FOCUS_LABEL); + CollectionEditorPageConstants.COLLECTION_TITLE_INPUT_FOCUS_LABEL + ); } ngOnInit(): void { @@ -71,7 +72,9 @@ export class CollectionEditorNavbarBreadcrumbComponent { } } -angular.module('oppia').directive('collectionEditorNavbarBreadcrumb', +angular.module('oppia').directive( + 'collectionEditorNavbarBreadcrumb', downgradeComponent({ - component: CollectionEditorNavbarBreadcrumbComponent - })); + component: CollectionEditorNavbarBreadcrumbComponent, + }) +); diff --git a/core/templates/pages/collection-editor-page/navbar/collection-editor-navbar.component.spec.ts b/core/templates/pages/collection-editor-page/navbar/collection-editor-navbar.component.spec.ts index 6d2addf94542..cccac8b8851b 100644 --- a/core/templates/pages/collection-editor-page/navbar/collection-editor-navbar.component.spec.ts +++ b/core/templates/pages/collection-editor-page/navbar/collection-editor-navbar.component.spec.ts @@ -16,21 +16,27 @@ * @fileoverview Unit test for collection editor navbar component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { UrlSerializer } from '@angular/router'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { CollectionPlaythrough } from 'domain/collection/collection-playthrough.model'; -import { CollectionRightsBackendApiService } from 'domain/collection/collection-rights-backend-api.service'; -import { CollectionRights } from 'domain/collection/collection-rights.model'; -import { CollectionValidationService } from 'domain/collection/collection-validation.service'; -import { Collection } from 'domain/collection/collection.model'; -import { UndoRedoService } from 'domain/editor/undo_redo/undo-redo.service'; -import { UrlService } from 'services/contextual/url.service'; -import { CollectionEditorRoutingService } from '../services/collection-editor-routing.service'; -import { CollectionEditorStateService } from '../services/collection-editor-state.service'; -import { CollectionEditorNavbarComponent } from './collection-editor-navbar.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {UrlSerializer} from '@angular/router'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {CollectionPlaythrough} from 'domain/collection/collection-playthrough.model'; +import {CollectionRightsBackendApiService} from 'domain/collection/collection-rights-backend-api.service'; +import {CollectionRights} from 'domain/collection/collection-rights.model'; +import {CollectionValidationService} from 'domain/collection/collection-validation.service'; +import {Collection} from 'domain/collection/collection.model'; +import {UndoRedoService} from 'domain/editor/undo_redo/undo-redo.service'; +import {UrlService} from 'services/contextual/url.service'; +import {CollectionEditorRoutingService} from '../services/collection-editor-routing.service'; +import {CollectionEditorStateService} from '../services/collection-editor-state.service'; +import {CollectionEditorNavbarComponent} from './collection-editor-navbar.component'; describe('Collection editor navbar component', () => { let fixture: ComponentFixture; @@ -51,34 +57,38 @@ describe('Collection editor navbar component', () => { let collectionTags = ['mock tag']; let collectionId = 'collection_id'; let mockCollection = new Collection( - collectionId, collectionTitle, collectionObjective, languageCode, - collectionTags, new CollectionPlaythrough(null, []), collectionCategory, - 0, 1, []); + collectionId, + collectionTitle, + collectionObjective, + languageCode, + collectionTags, + new CollectionPlaythrough(null, []), + collectionCategory, + 0, + 1, + [] + ); let mockPrivateCollectionRights = new CollectionRights({ collection_id: collectionId, can_edit: true, can_unpublish: true, is_private: true, - owner_names: [] + owner_names: [], }); beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - CollectionEditorNavbarComponent - ], + imports: [HttpClientTestingModule], + declarations: [CollectionEditorNavbarComponent], providers: [ CollectionEditorRoutingService, CollectionEditorStateService, CollectionRightsBackendApiService, CollectionValidationService, UndoRedoService, - UrlSerializer + UrlSerializer, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -87,10 +97,12 @@ describe('Collection editor navbar component', () => { componentInstance = fixture.componentInstance; collectionEditorRoutingService = TestBed.inject( - CollectionEditorRoutingService); + CollectionEditorRoutingService + ); collectionEditorStateService = TestBed.inject(CollectionEditorStateService); collectionRightsBackendApiService = TestBed.inject( - CollectionRightsBackendApiService); + CollectionRightsBackendApiService + ); collectionValidationService = TestBed.inject(CollectionValidationService); undoRedoService = TestBed.inject(UndoRedoService); urlService = TestBed.inject(UrlService); @@ -105,27 +117,36 @@ describe('Collection editor navbar component', () => { let mockOnCollectionEventEmitter = new EventEmitter(); let mockUndoRedoChangeAppliedEventEmitter = new EventEmitter(); - spyOnProperty(collectionEditorStateService, 'onCollectionInitialized') - .and.returnValue(mockOnCollectionEventEmitter); - spyOn(undoRedoService, 'getUndoRedoChangeEventEmitter') - .and.returnValue(mockUndoRedoChangeAppliedEventEmitter); + spyOnProperty( + collectionEditorStateService, + 'onCollectionInitialized' + ).and.returnValue(mockOnCollectionEventEmitter); + spyOn(undoRedoService, 'getUndoRedoChangeEventEmitter').and.returnValue( + mockUndoRedoChangeAppliedEventEmitter + ); spyOn(urlService, 'getCollectionIdFromEditorUrl').and.returnValue( - collectionId); + collectionId + ); spyOn(collectionEditorStateService, 'getCollection').and.returnValue( - mockCollection); - spyOn(collectionEditorStateService, 'getCollectionRights') - .and.returnValue(mockPrivateCollectionRights); + mockCollection + ); + spyOn(collectionEditorStateService, 'getCollectionRights').and.returnValue( + mockPrivateCollectionRights + ); spyOn( - collectionValidationService, 'findValidationIssuesForPrivateCollection') - .and.returnValue([]); + collectionValidationService, + 'findValidationIssuesForPrivateCollection' + ).and.returnValue([]); spyOn( - collectionValidationService, 'findValidationIssuesForPublicCollection') - .and.returnValue([]); + collectionValidationService, + 'findValidationIssuesForPublicCollection' + ).and.returnValue([]); componentInstance.ngOnInit(); - spyOn(componentInstance.collectionRights, 'isPrivate') - .and.returnValue(true); + spyOn(componentInstance.collectionRights, 'isPrivate').and.returnValue( + true + ); mockOnCollectionEventEmitter.emit(); tick(); @@ -145,28 +166,36 @@ describe('Collection editor navbar component', () => { can_edit: true, can_unpublish: true, is_private: false, - owner_names: [] + owner_names: [], }); let mockOnCollectionEventEmitter = new EventEmitter(); let mockUndoRedoChangeAppliedEventEmitter = new EventEmitter(); - spyOnProperty(collectionEditorStateService, 'onCollectionInitialized') - .and.returnValue(mockOnCollectionEventEmitter); - spyOn(undoRedoService, 'getUndoRedoChangeEventEmitter') - .and.returnValue(mockUndoRedoChangeAppliedEventEmitter); + spyOnProperty( + collectionEditorStateService, + 'onCollectionInitialized' + ).and.returnValue(mockOnCollectionEventEmitter); + spyOn(undoRedoService, 'getUndoRedoChangeEventEmitter').and.returnValue( + mockUndoRedoChangeAppliedEventEmitter + ); spyOn(urlService, 'getCollectionIdFromEditorUrl').and.returnValue( - collectionId); + collectionId + ); spyOn(collectionEditorStateService, 'getCollection').and.returnValue( - mockCollection); - spyOn(collectionEditorStateService, 'getCollectionRights') - .and.returnValue(mockPublicCollectionRights); + mockCollection + ); + spyOn(collectionEditorStateService, 'getCollectionRights').and.returnValue( + mockPublicCollectionRights + ); spyOn( - collectionValidationService, 'findValidationIssuesForPrivateCollection') - .and.returnValue([]); + collectionValidationService, + 'findValidationIssuesForPrivateCollection' + ).and.returnValue([]); spyOn( - collectionValidationService, 'findValidationIssuesForPublicCollection') - .and.returnValue([]); + collectionValidationService, + 'findValidationIssuesForPublicCollection' + ).and.returnValue([]); componentInstance.ngOnInit(); @@ -194,7 +223,7 @@ describe('Collection editor navbar component', () => { it('should test getters', () => { componentInstance.validationIssues = []; componentInstance.collectionRights = { - isPrivate: () => true + isPrivate: () => true, } as CollectionRights; let changeCount = 10; @@ -202,11 +231,16 @@ describe('Collection editor navbar component', () => { spyOn(undoRedoService, 'getChangeCount').and.returnValue(changeCount); spyOn(collectionEditorStateService, 'isLoadingCollection').and.returnValues( - true, false); + true, + false + ); spyOn(collectionEditorStateService, 'isSavingCollection').and.returnValues( - true, false); + true, + false + ); spyOn(collectionEditorRoutingService, 'getActiveTabName').and.returnValue( - activeTabName); + activeTabName + ); expect(componentInstance.getWarningsCount()).toEqual(0); expect(componentInstance.getChangeListCount()).toEqual(changeCount); @@ -231,12 +265,15 @@ describe('Collection editor navbar component', () => { componentInstance.selectHistoryTab(); expect(collectionEditorRoutingService.navigateToEditTab).toHaveBeenCalled(); - expect(collectionEditorRoutingService.navigateToSettingsTab) - .toHaveBeenCalled(); - expect(collectionEditorRoutingService.navigateToStatsTab) - .toHaveBeenCalled(); - expect(collectionEditorRoutingService.navigateToHistoryTab) - .toHaveBeenCalled(); + expect( + collectionEditorRoutingService.navigateToSettingsTab + ).toHaveBeenCalled(); + expect( + collectionEditorRoutingService.navigateToStatsTab + ).toHaveBeenCalled(); + expect( + collectionEditorRoutingService.navigateToHistoryTab + ).toHaveBeenCalled(); }); it('should save changes', () => { @@ -245,16 +282,17 @@ describe('Collection editor navbar component', () => { spyOn(ngbModal, 'open').and.returnValue({ componentInstance: { - isCollectionPrivate: false + isCollectionPrivate: false, }, result: { then: ( - successCallback: (commitMessage: string) => void, - errorCallback: () => void) => { + successCallback: (commitMessage: string) => void, + errorCallback: () => void + ) => { successCallback(commitMessage); errorCallback(); - } - } + }, + }, } as NgbModalRef); spyOn(collectionEditorStateService, 'saveCollection'); @@ -262,38 +300,53 @@ describe('Collection editor navbar component', () => { componentInstance.saveChanges(); expect(collectionEditorStateService.saveCollection).toHaveBeenCalledWith( - commitMessage); + commitMessage + ); }); it('should publish collection', fakeAsync(() => { componentInstance.collection = mockCollection; componentInstance.collectionRights = mockPrivateCollectionRights; - spyOn(collectionRightsBackendApiService, 'setCollectionPublicAsync') - .and.returnValue(Promise.resolve(mockPrivateCollectionRights)); + spyOn( + collectionRightsBackendApiService, + 'setCollectionPublicAsync' + ).and.returnValue(Promise.resolve(mockPrivateCollectionRights)); spyOn(collectionEditorStateService, 'setCollectionRights'); componentInstance.publishCollection(); tick(); - expect(collectionRightsBackendApiService.setCollectionPublicAsync) - .toHaveBeenCalled(); + expect( + collectionRightsBackendApiService.setCollectionPublicAsync + ).toHaveBeenCalled(); expect(collectionEditorStateService.setCollectionRights).toHaveBeenCalled(); let mockCollectionWithoutMetadata = new Collection( - 'id', '', '', '', [], new CollectionPlaythrough(null, []), '', 0, 1, []); + 'id', + '', + '', + '', + [], + new CollectionPlaythrough(null, []), + '', + 0, + 1, + [] + ); componentInstance.collection = mockCollectionWithoutMetadata; spyOn(ngbModal, 'open').and.returnValue({ result: { then: ( - successCallback: (commitMessage: string[]) => void, - errorCallback: () => void) => { + successCallback: (commitMessage: string[]) => void, + errorCallback: () => void + ) => { successCallback([]); errorCallback(); - } - } + }, + }, } as NgbModalRef); spyOn(collectionEditorStateService, 'saveCollection'); diff --git a/core/templates/pages/collection-editor-page/navbar/collection-editor-navbar.component.ts b/core/templates/pages/collection-editor-page/navbar/collection-editor-navbar.component.ts index 715f08e62090..68f0faa15466 100644 --- a/core/templates/pages/collection-editor-page/navbar/collection-editor-navbar.component.ts +++ b/core/templates/pages/collection-editor-page/navbar/collection-editor-navbar.component.ts @@ -16,24 +16,24 @@ * @fileoverview Component for the navbar of the collection editor. */ -import { Component } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { CollectionRightsBackendApiService } from 'domain/collection/collection-rights-backend-api.service'; -import { CollectionRights } from 'domain/collection/collection-rights.model'; -import { CollectionValidationService } from 'domain/collection/collection-validation.service'; -import { Collection } from 'domain/collection/collection.model'; -import { UndoRedoService } from 'domain/editor/undo_redo/undo-redo.service'; -import { Subscription } from 'rxjs'; -import { UrlService } from 'services/contextual/url.service'; -import { CollectionEditorRoutingService } from '../services/collection-editor-routing.service'; -import { CollectionEditorStateService } from '../services/collection-editor-state.service'; -import { CollectionEditorPrePublishModalComponent } from '../modals/collection-editor-pre-publish-modal.component'; -import { CollectionEditorSaveModalComponent } from '../modals/collection-editor-save-modal.component'; +import {Component} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {CollectionRightsBackendApiService} from 'domain/collection/collection-rights-backend-api.service'; +import {CollectionRights} from 'domain/collection/collection-rights.model'; +import {CollectionValidationService} from 'domain/collection/collection-validation.service'; +import {Collection} from 'domain/collection/collection.model'; +import {UndoRedoService} from 'domain/editor/undo_redo/undo-redo.service'; +import {Subscription} from 'rxjs'; +import {UrlService} from 'services/contextual/url.service'; +import {CollectionEditorRoutingService} from '../services/collection-editor-routing.service'; +import {CollectionEditorStateService} from '../services/collection-editor-state.service'; +import {CollectionEditorPrePublishModalComponent} from '../modals/collection-editor-pre-publish-modal.component'; +import {CollectionEditorSaveModalComponent} from '../modals/collection-editor-save-modal.component'; @Component({ selector: 'collection-editor-navbar', - templateUrl: './collection-editor-navbar.component.html' + templateUrl: './collection-editor-navbar.component.html', }) export class CollectionEditorNavbarComponent { directiveSubscriptions = new Subscription(); @@ -51,8 +51,7 @@ export class CollectionEditorNavbarComponent { private ngbModal: NgbModal, private collectionEditorRoutingService: CollectionEditorRoutingService, private collectionEditorStateService: CollectionEditorStateService, - private collectionRightsBackendApiService: - CollectionRightsBackendApiService, + private collectionRightsBackendApiService: CollectionRightsBackendApiService, private collectionValidationService: CollectionValidationService, private undoRedoService: UndoRedoService, private urlService: UrlService @@ -60,21 +59,21 @@ export class CollectionEditorNavbarComponent { ngOnInit(): void { this.directiveSubscriptions.add( - this.collectionEditorStateService.onCollectionInitialized.subscribe( - () => this._validateCollection() + this.collectionEditorStateService.onCollectionInitialized.subscribe(() => + this._validateCollection() ) ); this.directiveSubscriptions.add( - this.undoRedoService.getUndoRedoChangeEventEmitter().subscribe( - () => this._validateCollection() - ) + this.undoRedoService + .getUndoRedoChangeEventEmitter() + .subscribe(() => this._validateCollection()) ); this.collectionId = this.urlService.getCollectionIdFromEditorUrl(); this.collection = this.collectionEditorStateService.getCollection(); - this.collectionRights = ( - this.collectionEditorStateService.getCollectionRights()); + this.collectionRights = + this.collectionEditorStateService.getCollectionRights(); this.validationIssues = []; } @@ -93,25 +92,28 @@ export class CollectionEditorNavbarComponent { private _validateCollection() { if (this.collectionRights.isPrivate()) { - this.validationIssues = ( - this.collectionValidationService - .findValidationIssuesForPrivateCollection(this.collection)); + this.validationIssues = + this.collectionValidationService.findValidationIssuesForPrivateCollection( + this.collection + ); } else { - this.validationIssues = ( - this.collectionValidationService - .findValidationIssuesForPrivateCollection(this.collection)); + this.validationIssues = + this.collectionValidationService.findValidationIssuesForPrivateCollection( + this.collection + ); } } private _makeCollectionPublic(): void { // TODO(bhenning): This also needs a confirmation of destructive // action since it is not reversible. - this.collectionRightsBackendApiService.setCollectionPublicAsync( - this.collectionId, this.collection.getVersion()).then( - () => { + this.collectionRightsBackendApiService + .setCollectionPublicAsync(this.collectionId, this.collection.getVersion()) + .then(() => { this.collectionRights.setPublic(); this.collectionEditorStateService.setCollectionRights( - this.collectionRights); + this.collectionRights + ); }); } @@ -124,53 +126,64 @@ export class CollectionEditorNavbarComponent { } isCollectionSaveable(): boolean { - return ( - this.getChangeListCount() > 0 && - this.validationIssues.length === 0); + return this.getChangeListCount() > 0 && this.validationIssues.length === 0; } isCollectionPublishable(): boolean { return ( Boolean(this.collectionRights.isPrivate()) && this.getChangeListCount() === 0 && - this.validationIssues.length === 0); + this.validationIssues.length === 0 + ); } saveChanges(): void { let modalRef: NgbModalRef = this.ngbModal.open( - CollectionEditorSaveModalComponent, { - backdrop: 'static' - }); + CollectionEditorSaveModalComponent, + { + backdrop: 'static', + } + ); - modalRef.componentInstance.isCollectionPrivate = ( - this.collectionRights.isPrivate()); + modalRef.componentInstance.isCollectionPrivate = + this.collectionRights.isPrivate(); - modalRef.result.then((commitMessage: string) => { - this.collectionEditorStateService.saveCollection(commitMessage); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + modalRef.result.then( + (commitMessage: string) => { + this.collectionEditorStateService.saveCollection(commitMessage); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } publishCollection(): void { - let additionalMetadataNeeded = ( + let additionalMetadataNeeded = !this.collection.getTitle() || !this.collection.getObjective() || - !this.collection.getCategory()); + !this.collection.getCategory(); if (additionalMetadataNeeded) { let modalRef = this.ngbModal.open( - CollectionEditorPrePublishModalComponent, { - backdrop: 'static' - }); - - modalRef.result.then((metadataList) => { - let commitMessage = ('Add metadata: ' + metadataList.join(', ') + '.'); - this.collectionEditorStateService.saveCollection( - commitMessage, this._makeCollectionPublic.bind(this)); - }, () => {}); + CollectionEditorPrePublishModalComponent, + { + backdrop: 'static', + } + ); + + modalRef.result.then( + metadataList => { + let commitMessage = 'Add metadata: ' + metadataList.join(', ') + '.'; + this.collectionEditorStateService.saveCollection( + commitMessage, + this._makeCollectionPublic.bind(this) + ); + }, + () => {} + ); } else { this._makeCollectionPublic(); } @@ -205,7 +218,9 @@ export class CollectionEditorNavbarComponent { } } -angular.module('oppia').directive('collectionEditorNavbar', +angular.module('oppia').directive( + 'collectionEditorNavbar', downgradeComponent({ - component: CollectionEditorNavbarComponent - }) as angular.IDirectiveFactory); + component: CollectionEditorNavbarComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/collection-editor-page/services/collection-editor-routing.service.spec.ts b/core/templates/pages/collection-editor-page/services/collection-editor-routing.service.spec.ts index 4a3ae3aba054..de2028aea9a7 100644 --- a/core/templates/pages/collection-editor-page/services/collection-editor-routing.service.spec.ts +++ b/core/templates/pages/collection-editor-page/services/collection-editor-routing.service.spec.ts @@ -16,20 +16,21 @@ * @fileoverview Unit tests for collection editor routing service. */ -import { WindowRef } from 'services/contextual/window-ref.service'; -import { CollectionEditorRoutingService } from './collection-editor-routing.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {CollectionEditorRoutingService} from './collection-editor-routing.service'; class MockWindowRef { nativeWindow = { location: { - hash: '#edit' - } + hash: '#edit', + }, }; } describe('Collection editor routing service', () => { let cers = new CollectionEditorRoutingService( - new MockWindowRef() as WindowRef); + new MockWindowRef() as WindowRef + ); it('should create', () => { expect(cers).toBeDefined(); diff --git a/core/templates/pages/collection-editor-page/services/collection-editor-routing.service.ts b/core/templates/pages/collection-editor-page/services/collection-editor-routing.service.ts index ef059ff4f412..1de249899d1a 100644 --- a/core/templates/pages/collection-editor-page/services/collection-editor-routing.service.ts +++ b/core/templates/pages/collection-editor-page/services/collection-editor-routing.service.ts @@ -16,12 +16,12 @@ * @fileoverview Service that handles routing for the collection editor page. */ -import { Injectable, EventEmitter } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import {Injectable, EventEmitter} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {WindowRef} from 'services/contextual/window-ref.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class CollectionEditorRoutingService { // These properties are initialized using private functions @@ -34,9 +34,7 @@ export class CollectionEditorRoutingService { private _STATS_TAB = 'stats'; private _updateViewEventEmitter: EventEmitter = new EventEmitter(); - constructor( - private windowRef: WindowRef - ) { + constructor(private windowRef: WindowRef) { let currentHash: string = this.windowRef.nativeWindow.location.hash; this._changeTab(currentHash.substring(1, currentHash.length)); } @@ -81,5 +79,9 @@ export class CollectionEditorRoutingService { } } -angular.module('oppia').factory('CollectionEditorRoutingService', - downgradeInjectable(CollectionEditorRoutingService)); +angular + .module('oppia') + .factory( + 'CollectionEditorRoutingService', + downgradeInjectable(CollectionEditorRoutingService) + ); diff --git a/core/templates/pages/collection-editor-page/services/collection-editor-state.service.spec.ts b/core/templates/pages/collection-editor-page/services/collection-editor-state.service.spec.ts index b0ba27de9b8a..ed078e2da3aa 100644 --- a/core/templates/pages/collection-editor-page/services/collection-editor-state.service.spec.ts +++ b/core/templates/pages/collection-editor-page/services/collection-editor-state.service.spec.ts @@ -16,21 +16,26 @@ * @fileoverview Unit tests for CollectionEditorStateService. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { CollectionRightsBackendApiService } from 'domain/collection/collection-rights-backend-api.service'; -import { CollectionRights, CollectionRightsBackendDict } from 'domain/collection/collection-rights.model'; -import { Collection, CollectionBackendDict } from 'domain/collection/collection.model'; -import { EditableCollectionBackendApiService } from 'domain/collection/editable-collection-backend-api.service'; -import { UndoRedoService } from 'domain/editor/undo_redo/undo-redo.service'; -import { Subscription } from 'rxjs'; -import { AlertsService } from 'services/alerts.service'; -import { CollectionEditorStateService } from './collection-editor-state.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {fakeAsync, TestBed, tick, waitForAsync} from '@angular/core/testing'; +import {CollectionRightsBackendApiService} from 'domain/collection/collection-rights-backend-api.service'; +import { + CollectionRights, + CollectionRightsBackendDict, +} from 'domain/collection/collection-rights.model'; +import { + Collection, + CollectionBackendDict, +} from 'domain/collection/collection.model'; +import {EditableCollectionBackendApiService} from 'domain/collection/editable-collection-backend-api.service'; +import {UndoRedoService} from 'domain/editor/undo_redo/undo-redo.service'; +import {Subscription} from 'rxjs'; +import {AlertsService} from 'services/alerts.service'; +import {CollectionEditorStateService} from './collection-editor-state.service'; describe('Collection editor state service', () => { let collectionEditorStateService: CollectionEditorStateService; - let collectionRightsBackendApiService: - CollectionRightsBackendApiService; + let collectionRightsBackendApiService: CollectionRightsBackendApiService; let undoRedoService: UndoRedoService; let alertsService: AlertsService; let editableCollectionBackendApiService: EditableCollectionBackendApiService; @@ -50,16 +55,18 @@ describe('Collection editor state service', () => { providers: [ EditableCollectionBackendApiService, CollectionRightsBackendApiService, - AlertsService - ] + AlertsService, + ], }); collectionEditorStateService = TestBed.inject(CollectionEditorStateService); undoRedoService = TestBed.inject(UndoRedoService); editableCollectionBackendApiService = TestBed.inject( - EditableCollectionBackendApiService); + EditableCollectionBackendApiService + ); collectionRightsBackendApiService = TestBed.inject( - CollectionRightsBackendApiService); + CollectionRightsBackendApiService + ); alertsService = TestBed.inject(AlertsService); sampleCollectionBackendDict = { @@ -74,8 +81,8 @@ describe('Collection editor state service', () => { tags: null, playthrough_dict: { next_exploration_id: 'expId', - completed_exploration_ids: ['expId2'] - } + completed_exploration_ids: ['expId2'], + }, }; sampleCollection = Collection.create(sampleCollectionBackendDict); @@ -85,18 +92,21 @@ describe('Collection editor state service', () => { can_edit: true, can_unpublish: false, is_private: true, - owner_names: ['A'] + owner_names: ['A'], }; sampleCollectionRights = CollectionRights.create( - sampleCollectionRightsDict); + sampleCollectionRightsDict + ); })); beforeEach(() => { testSubscriptions = new Subscription(); testSubscriptions.add( collectionEditorStateService.onCollectionInitialized.subscribe( - collectionInitializedSpy)); + collectionInitializedSpy + ) + ); alertsSpy = spyOn(alertsService, 'addWarning').and.callThrough(); }); @@ -107,8 +117,7 @@ describe('Collection editor state service', () => { it('should fire an init event after loading the first collection', () => { collectionEditorStateService.loadCollection('5'); - } - ); + }); it('should fire an update event after loading more collections', () => { // Load initial collection. @@ -122,14 +131,12 @@ describe('Collection editor state service', () => { expect(collectionEditorStateService.isLoadingCollection()).toBe(true); }); - it('should report that a collection has loaded through loadCollection()', - () => { - expect(collectionEditorStateService.hasLoadedCollection()).toBe(false); + it('should report that a collection has loaded through loadCollection()', () => { + expect(collectionEditorStateService.hasLoadedCollection()).toBe(false); - collectionEditorStateService.loadCollection('5'); - expect(collectionEditorStateService.hasLoadedCollection()).toBe(false); - } - ); + collectionEditorStateService.loadCollection('5'); + expect(collectionEditorStateService.hasLoadedCollection()).toBe(false); + }); it('should initially return an empty collection', () => { let collection = collectionEditorStateService.getCollection(); @@ -151,8 +158,9 @@ describe('Collection editor state service', () => { it('should load a collection successfully', fakeAsync(() => { let fetchCollectionSpy = spyOn( - editableCollectionBackendApiService, 'fetchCollectionAsync') - .and.resolveTo(sampleCollection); + editableCollectionBackendApiService, + 'fetchCollectionAsync' + ).and.resolveTo(sampleCollection); // Load initial collection. collectionEditorStateService.loadCollection('sample_collection_id'); @@ -161,27 +169,32 @@ describe('Collection editor state service', () => { expect(fetchCollectionSpy).toHaveBeenCalled(); })); - it('should throw error if there was an error while ' + - 'loading collection', fakeAsync(() => { - let fetchCollectionSpy = spyOn( - editableCollectionBackendApiService, 'fetchCollectionAsync') - .and.rejectWith(); - const loadCollectionSuccessCb = jasmine.createSpy('success'); + it( + 'should throw error if there was an error while ' + 'loading collection', + fakeAsync(() => { + let fetchCollectionSpy = spyOn( + editableCollectionBackendApiService, + 'fetchCollectionAsync' + ).and.rejectWith(); + const loadCollectionSuccessCb = jasmine.createSpy('success'); - // Load initial collection. - collectionEditorStateService.loadCollection('sample_collection_id'); - tick(); + // Load initial collection. + collectionEditorStateService.loadCollection('sample_collection_id'); + tick(); - expect(fetchCollectionSpy).toHaveBeenCalled(); - expect(loadCollectionSuccessCb).not.toHaveBeenCalled(); - expect(alertsSpy).toHaveBeenCalledWith( - 'There was an error when loading the collection.'); - })); + expect(fetchCollectionSpy).toHaveBeenCalled(); + expect(loadCollectionSuccessCb).not.toHaveBeenCalled(); + expect(alertsSpy).toHaveBeenCalledWith( + 'There was an error when loading the collection.' + ); + }) + ); it('should load a collection rights successfully', fakeAsync(() => { let fetchCollectionSpy = spyOn( - collectionRightsBackendApiService, 'fetchCollectionRightsAsync') - .and.resolveTo(sampleCollectionRights); + collectionRightsBackendApiService, + 'fetchCollectionRightsAsync' + ).and.resolveTo(sampleCollectionRights); // Load initial collection. collectionEditorStateService.loadCollection('sample_collection_id'); @@ -191,96 +204,104 @@ describe('Collection editor state service', () => { expect(fetchCollectionSpy).toHaveBeenCalled(); })); - it('should throw error if there was an error while ' + - 'loading collection rights', fakeAsync(() => { - let fetchCollectionSpy = spyOn( - collectionRightsBackendApiService, 'fetchCollectionRightsAsync') - .and.rejectWith(); - const loadCollectionRightsSuccessCb = jasmine.createSpy('success'); - - // Load initial collection. - collectionEditorStateService.loadCollection('sample_collection_id'); - tick(); - - expect(fetchCollectionSpy).toHaveBeenCalled(); - expect(loadCollectionRightsSuccessCb).not.toHaveBeenCalled(); - expect(alertsSpy).toHaveBeenCalledWith( - 'There was an error when loading the collection rights.'); - })); - - it('should not save the collection if there are no pending changes', + it( + 'should throw error if there was an error while ' + + 'loading collection rights', fakeAsync(() => { - // Setting pending changes to be false. - spyOn(undoRedoService, 'hasChanges').and.returnValue(false); - const saveCollectionsuccessCb = jasmine.createSpy('success'); + let fetchCollectionSpy = spyOn( + collectionRightsBackendApiService, + 'fetchCollectionRightsAsync' + ).and.rejectWith(); + const loadCollectionRightsSuccessCb = jasmine.createSpy('success'); // Load initial collection. collectionEditorStateService.loadCollection('sample_collection_id'); tick(); - collectionEditorStateService.setCollection(sampleCollection); - - let savedChanges = collectionEditorStateService.saveCollection( - 'commit message'); - - expect(saveCollectionsuccessCb).not.toHaveBeenCalled(); - expect(savedChanges).toBe(false); - } - )); + expect(fetchCollectionSpy).toHaveBeenCalled(); + expect(loadCollectionRightsSuccessCb).not.toHaveBeenCalled(); + expect(alertsSpy).toHaveBeenCalledWith( + 'There was an error when loading the collection rights.' + ); + }) + ); - it('should save pending changes of a collection', fakeAsync(() => { - // Setting pending changes to be true. - spyOn(undoRedoService, 'hasChanges').and.returnValue(true); - spyOn(editableCollectionBackendApiService, 'updateCollectionAsync') - .and.resolveTo(sampleCollection); + it('should not save the collection if there are no pending changes', fakeAsync(() => { + // Setting pending changes to be false. + spyOn(undoRedoService, 'hasChanges').and.returnValue(false); const saveCollectionsuccessCb = jasmine.createSpy('success'); - // Load initial collection. collectionEditorStateService.loadCollection('sample_collection_id'); tick(); collectionEditorStateService.setCollection(sampleCollection); - let savedChanges = collectionEditorStateService.saveCollection( - 'commit message', saveCollectionsuccessCb); - tick(); + let savedChanges = + collectionEditorStateService.saveCollection('commit message'); - expect(saveCollectionsuccessCb).toHaveBeenCalled(); - expect(savedChanges).toBe(true); + expect(saveCollectionsuccessCb).not.toHaveBeenCalled(); + expect(savedChanges).toBe(false); })); - it('should fail to save collection in case of backend ' + - 'error', fakeAsync(() => { + it('should save pending changes of a collection', fakeAsync(() => { // Setting pending changes to be true. spyOn(undoRedoService, 'hasChanges').and.returnValue(true); - spyOn(editableCollectionBackendApiService, 'updateCollectionAsync') - .and.rejectWith(); + spyOn( + editableCollectionBackendApiService, + 'updateCollectionAsync' + ).and.resolveTo(sampleCollection); const saveCollectionsuccessCb = jasmine.createSpy('success'); - // Load initial collection. collectionEditorStateService.loadCollection('sample_collection_id'); tick(); collectionEditorStateService.setCollection(sampleCollection); - collectionEditorStateService.saveCollection( - 'commit message'); + let savedChanges = collectionEditorStateService.saveCollection( + 'commit message', + saveCollectionsuccessCb + ); tick(); - expect(saveCollectionsuccessCb).not.toHaveBeenCalled(); - expect(alertsSpy).toHaveBeenCalledWith( - 'There was an error when saving the collection.'); + expect(saveCollectionsuccessCb).toHaveBeenCalled(); + expect(savedChanges).toBe(true); })); - it('should fail to save the collection without first loading one', + it( + 'should fail to save collection in case of backend ' + 'error', fakeAsync(() => { - let savedChanges = collectionEditorStateService.saveCollection( - 'Commit message'); + // Setting pending changes to be true. + spyOn(undoRedoService, 'hasChanges').and.returnValue(true); + spyOn( + editableCollectionBackendApiService, + 'updateCollectionAsync' + ).and.rejectWith(); + const saveCollectionsuccessCb = jasmine.createSpy('success'); + + // Load initial collection. + collectionEditorStateService.loadCollection('sample_collection_id'); tick(); - expect(savedChanges).toBeFalse(); - })); + + collectionEditorStateService.setCollection(sampleCollection); + + collectionEditorStateService.saveCollection('commit message'); + tick(); + + expect(saveCollectionsuccessCb).not.toHaveBeenCalled(); + expect(alertsSpy).toHaveBeenCalledWith( + 'There was an error when saving the collection.' + ); + }) + ); + + it('should fail to save the collection without first loading one', fakeAsync(() => { + let savedChanges = + collectionEditorStateService.saveCollection('Commit message'); + tick(); + expect(savedChanges).toBeFalse(); + })); it('should check whether a collection is being saved', () => { let result = collectionEditorStateService.isSavingCollection(); diff --git a/core/templates/pages/collection-editor-page/services/collection-editor-state.service.ts b/core/templates/pages/collection-editor-page/services/collection-editor-state.service.ts index 3f0b23d3a0f1..3443df64fa82 100644 --- a/core/templates/pages/collection-editor-page/services/collection-editor-state.service.ts +++ b/core/templates/pages/collection-editor-page/services/collection-editor-state.service.ts @@ -18,37 +18,35 @@ * retrieving the collection, saving it, and listening for changes. */ -import { EventEmitter, Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { CollectionRightsBackendApiService } from 'domain/collection/collection-rights-backend-api.service'; -import { CollectionRights } from 'domain/collection/collection-rights.model'; -import { Collection } from 'domain/collection/collection.model'; -import { EditableCollectionBackendApiService } from 'domain/collection/editable-collection-backend-api.service'; -import { UndoRedoService } from 'domain/editor/undo_redo/undo-redo.service'; -import { AlertsService } from 'services/alerts.service'; +import {EventEmitter, Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {CollectionRightsBackendApiService} from 'domain/collection/collection-rights-backend-api.service'; +import {CollectionRights} from 'domain/collection/collection-rights.model'; +import {Collection} from 'domain/collection/collection.model'; +import {EditableCollectionBackendApiService} from 'domain/collection/editable-collection-backend-api.service'; +import {UndoRedoService} from 'domain/editor/undo_redo/undo-redo.service'; +import {AlertsService} from 'services/alerts.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class CollectionEditorStateService { private _collection: Collection = Collection.createEmptyCollection(); - private _collectionRights: CollectionRights = ( - CollectionRights.createEmptyCollectionRights()); + private _collectionRights: CollectionRights = + CollectionRights.createEmptyCollectionRights(); private _collectionIsInitialized: boolean = false; private _collectionIsLoading: boolean = false; private _collectionIsBeingSaved: boolean = false; - private _collectionInitializedEventEmitter: EventEmitter = ( - new EventEmitter()); + private _collectionInitializedEventEmitter: EventEmitter = + new EventEmitter(); constructor( private alertsService: AlertsService, - private collectionRightsBackendApiService: - CollectionRightsBackendApiService, - private editableCollectionBackendApiService: - EditableCollectionBackendApiService, + private collectionRightsBackendApiService: CollectionRightsBackendApiService, + private editableCollectionBackendApiService: EditableCollectionBackendApiService, private undoRedoService: UndoRedoService - ) { } + ) {} private _setCollection(collection: Collection) { this._collection.copyFromCollection(collection); @@ -75,26 +73,33 @@ export class CollectionEditorStateService { */ loadCollection(collectionId: string): void { this._collectionIsLoading = true; - this.editableCollectionBackendApiService.fetchCollectionAsync( - collectionId).then( - (newCollectionObject) => { - this._updateCollection(newCollectionObject); - }, - (error) => { - this.alertsService.addWarning( - error || 'There was an error when loading the collection.'); - this._collectionIsLoading = false; - }); - this.collectionRightsBackendApiService.fetchCollectionRightsAsync( - collectionId).then((newBackendCollectionRightsObject) => { - this._setCollectionRights(newBackendCollectionRightsObject); - this._collectionIsLoading = false; - }, (error) => { - this.alertsService.addWarning( - error || - 'There was an error when loading the collection rights.'); - this._collectionIsLoading = false; - }); + this.editableCollectionBackendApiService + .fetchCollectionAsync(collectionId) + .then( + newCollectionObject => { + this._updateCollection(newCollectionObject); + }, + error => { + this.alertsService.addWarning( + error || 'There was an error when loading the collection.' + ); + this._collectionIsLoading = false; + } + ); + this.collectionRightsBackendApiService + .fetchCollectionRightsAsync(collectionId) + .then( + newBackendCollectionRightsObject => { + this._setCollectionRights(newBackendCollectionRightsObject); + this._collectionIsLoading = false; + }, + error => { + this.alertsService.addWarning( + error || 'There was an error when loading the collection rights.' + ); + this._collectionIsLoading = false; + } + ); } /** @@ -172,11 +177,7 @@ export class CollectionEditorStateService { saveCollection(commitMessage: string, successCallback?: () => void): boolean { const collectionId = this._collection.getId(); const collectionVersion = this._collection.getVersion(); - if ( - !collectionId || - !this._collectionIsInitialized || - !collectionVersion - ) { + if (!collectionId || !this._collectionIsInitialized || !collectionVersion) { return false; } @@ -185,23 +186,30 @@ export class CollectionEditorStateService { return false; } this._collectionIsBeingSaved = true; - this.editableCollectionBackendApiService.updateCollectionAsync( - collectionId, collectionVersion, commitMessage, - this.undoRedoService.getCommittableChangeList() - ).then( - (collectionObject) => { - this._updateCollection(collectionObject); - this.undoRedoService.clearChanges(); - this._collectionIsBeingSaved = false; - - if (successCallback) { - successCallback(); + this.editableCollectionBackendApiService + .updateCollectionAsync( + collectionId, + collectionVersion, + commitMessage, + this.undoRedoService.getCommittableChangeList() + ) + .then( + collectionObject => { + this._updateCollection(collectionObject); + this.undoRedoService.clearChanges(); + this._collectionIsBeingSaved = false; + + if (successCallback) { + successCallback(); + } + }, + error => { + this.alertsService.addWarning( + error || 'There was an error when saving the collection.' + ); + this._collectionIsBeingSaved = false; } - }, (error) => { - this.alertsService.addWarning( - error || 'There was an error when saving the collection.'); - this._collectionIsBeingSaved = false; - }); + ); return true; } @@ -218,5 +226,9 @@ export class CollectionEditorStateService { } } -angular.module('oppia').factory('CollectionEditorStateService', - downgradeInjectable(CollectionEditorStateService)); +angular + .module('oppia') + .factory( + 'CollectionEditorStateService', + downgradeInjectable(CollectionEditorStateService) + ); diff --git a/core/templates/pages/collection-editor-page/services/collection-linearizer.service.spec.ts b/core/templates/pages/collection-editor-page/services/collection-linearizer.service.spec.ts index 870b3f34932d..f7e334b20d41 100644 --- a/core/templates/pages/collection-editor-page/services/collection-linearizer.service.spec.ts +++ b/core/templates/pages/collection-editor-page/services/collection-linearizer.service.spec.ts @@ -16,11 +16,14 @@ * @fileoverview Unit Tests for CollectionLinearizerService. */ -import { TestBed } from '@angular/core/testing'; -import { CollectionNode, CollectionNodeBackendDict } from 'domain/collection/collection-node.model'; -import { Collection } from 'domain/collection/collection.model'; -import { LearnerExplorationSummaryBackendDict } from 'domain/summary/learner-exploration-summary.model'; -import { CollectionLinearizerService } from './collection-linearizer.service'; +import {TestBed} from '@angular/core/testing'; +import { + CollectionNode, + CollectionNodeBackendDict, +} from 'domain/collection/collection-node.model'; +import {Collection} from 'domain/collection/collection.model'; +import {LearnerExplorationSummaryBackendDict} from 'domain/summary/learner-exploration-summary.model'; +import {CollectionLinearizerService} from './collection-linearizer.service'; describe('Collection linearizer service', () => { let collectionLinearizerService: CollectionLinearizerService; @@ -36,33 +39,36 @@ describe('Collection linearizer service', () => { exploration_summary: { title: 'exp title0', category: 'exp category', - objective: 'exp objective' - } + objective: 'exp objective', + }, }; firstCollectionNode = CollectionNode.create( - firstCollectionNodeBackendObject as CollectionNodeBackendDict); + firstCollectionNodeBackendObject as CollectionNodeBackendDict + ); let secondCollectionNodeBackendObject = { exploration_id: 'exp_id1', exploration_summary: { title: 'exp title1', category: 'exp category', - objective: 'exp objective' - } + objective: 'exp objective', + }, }; secondCollectionNode = CollectionNode.create( - secondCollectionNodeBackendObject as CollectionNodeBackendDict); + secondCollectionNodeBackendObject as CollectionNodeBackendDict + ); let thirdCollectionNodeBackendObject = { exploration_id: 'exp_id2', exploration_summary: { title: 'exp title2', category: 'exp category', - objective: 'exp objective' - } + objective: 'exp objective', + }, }; thirdCollectionNode = CollectionNode.create( - thirdCollectionNodeBackendObject as CollectionNodeBackendDict); + thirdCollectionNodeBackendObject as CollectionNodeBackendDict + ); }); // The linear order of explorations is: exp_id0 -> exp_id1 -> exp_id2. @@ -78,67 +84,88 @@ describe('Collection linearizer service', () => { }; describe('removeCollectionNode()', () => { - it('should not remove a non-existent node from a single node collection', - () => { - let collection = Collection.createEmptyCollection(); - collection.addCollectionNode(firstCollectionNode); - expect(collection.containsCollectionNode('exp_id0')).toBe(true); - expect( - collectionLinearizerService.removeCollectionNode( - collection, 'non_existent')).toBe(false); - expect(collection.containsCollectionNode('exp_id0')).toBe(true); - expect( - collectionLinearizerService.getCollectionNodesInPlayableOrder( - collection)).toEqual([firstCollectionNode]); - } - ); + it('should not remove a non-existent node from a single node collection', () => { + let collection = Collection.createEmptyCollection(); + collection.addCollectionNode(firstCollectionNode); + expect(collection.containsCollectionNode('exp_id0')).toBe(true); + expect( + collectionLinearizerService.removeCollectionNode( + collection, + 'non_existent' + ) + ).toBe(false); + expect(collection.containsCollectionNode('exp_id0')).toBe(true); + expect( + collectionLinearizerService.getCollectionNodesInPlayableOrder( + collection + ) + ).toEqual([firstCollectionNode]); + }); - it('should not remove a non-existent node from a multiple nodes collection', - () => { - let collection = createLinearCollection(); - expect( - collectionLinearizerService.getCollectionNodesInPlayableOrder( - collection)).toEqual( - [firstCollectionNode, secondCollectionNode, thirdCollectionNode]); - expect( - collectionLinearizerService.removeCollectionNode( - collection, 'non_existent')).toBe(false); - expect( - collectionLinearizerService.getCollectionNodesInPlayableOrder( - collection)).toEqual( - [firstCollectionNode, secondCollectionNode, thirdCollectionNode]); - } - ); + it('should not remove a non-existent node from a multiple nodes collection', () => { + let collection = createLinearCollection(); + expect( + collectionLinearizerService.getCollectionNodesInPlayableOrder( + collection + ) + ).toEqual([ + firstCollectionNode, + secondCollectionNode, + thirdCollectionNode, + ]); + expect( + collectionLinearizerService.removeCollectionNode( + collection, + 'non_existent' + ) + ).toBe(false); + expect( + collectionLinearizerService.getCollectionNodesInPlayableOrder( + collection + ) + ).toEqual([ + firstCollectionNode, + secondCollectionNode, + thirdCollectionNode, + ]); + }); - it('should correctly remove a node from a single node collection', - () => { - let collection = Collection.createEmptyCollection(); - collection.addCollectionNode(firstCollectionNode); - expect(collection.containsCollectionNode('exp_id0')).toBe(true); - expect( - collectionLinearizerService.removeCollectionNode( - collection, 'exp_id0')).toBe(true); - expect(collection.containsCollectionNode('exp_id0')).toBe(false); - expect( - collectionLinearizerService.getCollectionNodesInPlayableOrder( - collection)).toEqual([]); - } - ); + it('should correctly remove a node from a single node collection', () => { + let collection = Collection.createEmptyCollection(); + collection.addCollectionNode(firstCollectionNode); + expect(collection.containsCollectionNode('exp_id0')).toBe(true); + expect( + collectionLinearizerService.removeCollectionNode(collection, 'exp_id0') + ).toBe(true); + expect(collection.containsCollectionNode('exp_id0')).toBe(false); + expect( + collectionLinearizerService.getCollectionNodesInPlayableOrder( + collection + ) + ).toEqual([]); + }); it('should correctly remove the first node from a collection', () => { let collection = createLinearCollection(); expect(collection.containsCollectionNode('exp_id0')).toBe(true); expect( collectionLinearizerService.getCollectionNodesInPlayableOrder( - collection)).toEqual( - [firstCollectionNode, secondCollectionNode, thirdCollectionNode]); - expect( - collectionLinearizerService.removeCollectionNode( - collection, 'exp_id0')).toBe(true); + collection + ) + ).toEqual([ + firstCollectionNode, + secondCollectionNode, + thirdCollectionNode, + ]); + expect( + collectionLinearizerService.removeCollectionNode(collection, 'exp_id0') + ).toBe(true); expect(collection.containsCollectionNode('exp_id0')).toBe(false); expect( collectionLinearizerService.getCollectionNodesInPlayableOrder( - collection)).toEqual([secondCollectionNode, thirdCollectionNode]); + collection + ) + ).toEqual([secondCollectionNode, thirdCollectionNode]); }); it('should correctly remove the last node from a collection', () => { @@ -146,15 +173,22 @@ describe('Collection linearizer service', () => { expect(collection.containsCollectionNode('exp_id2')).toBe(true); expect( collectionLinearizerService.getCollectionNodesInPlayableOrder( - collection)).toEqual( - [firstCollectionNode, secondCollectionNode, thirdCollectionNode]); - expect( - collectionLinearizerService.removeCollectionNode( - collection, 'exp_id2')).toBe(true); + collection + ) + ).toEqual([ + firstCollectionNode, + secondCollectionNode, + thirdCollectionNode, + ]); + expect( + collectionLinearizerService.removeCollectionNode(collection, 'exp_id2') + ).toBe(true); expect(collection.containsCollectionNode('exp_id2')).toBe(false); expect( collectionLinearizerService.getCollectionNodesInPlayableOrder( - collection)).toEqual([firstCollectionNode, secondCollectionNode]); + collection + ) + ).toEqual([firstCollectionNode, secondCollectionNode]); }); it('should correctly remove a middle node from a collection', () => { @@ -162,15 +196,22 @@ describe('Collection linearizer service', () => { expect(collection.containsCollectionNode('exp_id1')).toBe(true); expect( collectionLinearizerService.getCollectionNodesInPlayableOrder( - collection)).toEqual( - [firstCollectionNode, secondCollectionNode, thirdCollectionNode]); - expect( - collectionLinearizerService.removeCollectionNode( - collection, 'exp_id1')).toBe(true); + collection + ) + ).toEqual([ + firstCollectionNode, + secondCollectionNode, + thirdCollectionNode, + ]); + expect( + collectionLinearizerService.removeCollectionNode(collection, 'exp_id1') + ).toBe(true); expect(collection.containsCollectionNode('exp_id1')).toBe(false); expect( collectionLinearizerService.getCollectionNodesInPlayableOrder( - collection)).toEqual([firstCollectionNode, thirdCollectionNode]); + collection + ) + ).toEqual([firstCollectionNode, thirdCollectionNode]); }); }); @@ -180,17 +221,21 @@ describe('Collection linearizer service', () => { expect(collection.containsCollectionNode('exp_id0')).toBe(false); expect( collectionLinearizerService.getCollectionNodesInPlayableOrder( - collection)).toEqual([]); + collection + ) + ).toEqual([]); collectionLinearizerService.appendCollectionNode( collection, 'exp_id0', - firstCollectionNode.getExplorationSummaryObject() as - LearnerExplorationSummaryBackendDict); - firstCollectionNode = collection.getCollectionNodeByExplorationId( - 'exp_id0'); + firstCollectionNode.getExplorationSummaryObject() as LearnerExplorationSummaryBackendDict + ); + firstCollectionNode = + collection.getCollectionNodeByExplorationId('exp_id0'); expect( collectionLinearizerService.getCollectionNodesInPlayableOrder( - collection)).toEqual([firstCollectionNode]); + collection + ) + ).toEqual([firstCollectionNode]); }); it('should correctly append a node to a non-empty collection', () => { @@ -200,234 +245,335 @@ describe('Collection linearizer service', () => { exploration_summary: { title: 'exp title3', category: 'exp category', - objective: 'exp objective' - } + objective: 'exp objective', + }, }; let newCollectionNode = CollectionNode.create( - newCollectionNodeBackendObject as CollectionNodeBackendDict); + newCollectionNodeBackendObject as CollectionNodeBackendDict + ); expect( collectionLinearizerService.getCollectionNodesInPlayableOrder( - collection)).toEqual( - [firstCollectionNode, secondCollectionNode, thirdCollectionNode]); + collection + ) + ).toEqual([ + firstCollectionNode, + secondCollectionNode, + thirdCollectionNode, + ]); collectionLinearizerService.appendCollectionNode( - collection, 'exp_id3', - newCollectionNode.getExplorationSummaryObject() as - LearnerExplorationSummaryBackendDict); - newCollectionNode = collection.getCollectionNodeByExplorationId( - 'exp_id3'); + collection, + 'exp_id3', + newCollectionNode.getExplorationSummaryObject() as LearnerExplorationSummaryBackendDict + ); + newCollectionNode = + collection.getCollectionNodeByExplorationId('exp_id3'); expect( collectionLinearizerService.getCollectionNodesInPlayableOrder( - collection)).toEqual([ + collection + ) + ).toEqual([ collection.getCollectionNodeByExplorationId('exp_id0'), collection.getCollectionNodeByExplorationId('exp_id1'), collection.getCollectionNodeByExplorationId('exp_id2'), - collection.getCollectionNodeByExplorationId('exp_id3')]); + collection.getCollectionNodeByExplorationId('exp_id3'), + ]); }); }); describe('shiftNodeLeft()', () => { - it('should correctly shift a node in a single node collection', - () => { - let collection = Collection.createEmptyCollection(); - collection.addCollectionNode(firstCollectionNode); - expect( - collectionLinearizerService.getCollectionNodesInPlayableOrder( - collection)).toEqual([firstCollectionNode]); - expect( - collectionLinearizerService.shiftNodeLeft( - collection, 'exp_id0')).toBe(true); - expect( - collectionLinearizerService.getCollectionNodesInPlayableOrder( - collection)).toEqual([firstCollectionNode]); - } - ); + it('should correctly shift a node in a single node collection', () => { + let collection = Collection.createEmptyCollection(); + collection.addCollectionNode(firstCollectionNode); + expect( + collectionLinearizerService.getCollectionNodesInPlayableOrder( + collection + ) + ).toEqual([firstCollectionNode]); + expect( + collectionLinearizerService.shiftNodeLeft(collection, 'exp_id0') + ).toBe(true); + expect( + collectionLinearizerService.getCollectionNodesInPlayableOrder( + collection + ) + ).toEqual([firstCollectionNode]); + }); it('should not shift a non-existent node', () => { let collection = createLinearCollection(); expect( collectionLinearizerService.getCollectionNodesInPlayableOrder( - collection)).toEqual( - [firstCollectionNode, secondCollectionNode, thirdCollectionNode]); - expect(collectionLinearizerService.shiftNodeLeft( - collection, 'non_existent')).toBe(false); + collection + ) + ).toEqual([ + firstCollectionNode, + secondCollectionNode, + thirdCollectionNode, + ]); + expect( + collectionLinearizerService.shiftNodeLeft(collection, 'non_existent') + ).toBe(false); expect( collectionLinearizerService.getCollectionNodesInPlayableOrder( - collection)).toEqual( - [firstCollectionNode, secondCollectionNode, thirdCollectionNode]); + collection + ) + ).toEqual([ + firstCollectionNode, + secondCollectionNode, + thirdCollectionNode, + ]); }); it('should correctly shift the first node', () => { let collection = createLinearCollection(); expect( collectionLinearizerService.getCollectionNodesInPlayableOrder( - collection)).toEqual( - [firstCollectionNode, secondCollectionNode, thirdCollectionNode]); + collection + ) + ).toEqual([ + firstCollectionNode, + secondCollectionNode, + thirdCollectionNode, + ]); expect( - collectionLinearizerService.shiftNodeLeft( - collection, 'exp_id0')).toBe(true); + collectionLinearizerService.shiftNodeLeft(collection, 'exp_id0') + ).toBe(true); expect( collectionLinearizerService.getCollectionNodesInPlayableOrder( - collection)).toEqual( - [firstCollectionNode, secondCollectionNode, thirdCollectionNode]); + collection + ) + ).toEqual([ + firstCollectionNode, + secondCollectionNode, + thirdCollectionNode, + ]); }); it('should correctly shift the last node', () => { let collection = createLinearCollection(); expect( collectionLinearizerService.getCollectionNodesInPlayableOrder( - collection)).toEqual( - [firstCollectionNode, secondCollectionNode, thirdCollectionNode]); + collection + ) + ).toEqual([ + firstCollectionNode, + secondCollectionNode, + thirdCollectionNode, + ]); expect( - collectionLinearizerService.shiftNodeLeft( - collection, 'exp_id2')).toBe(true); + collectionLinearizerService.shiftNodeLeft(collection, 'exp_id2') + ).toBe(true); expect( collectionLinearizerService.getCollectionNodesInPlayableOrder( - collection)).toEqual( - [firstCollectionNode, thirdCollectionNode, secondCollectionNode]); + collection + ) + ).toEqual([ + firstCollectionNode, + thirdCollectionNode, + secondCollectionNode, + ]); }); it('should correctly shift a middle node', () => { let collection = createLinearCollection(); expect( collectionLinearizerService.getCollectionNodesInPlayableOrder( - collection)).toEqual( - [firstCollectionNode, secondCollectionNode, thirdCollectionNode]); + collection + ) + ).toEqual([ + firstCollectionNode, + secondCollectionNode, + thirdCollectionNode, + ]); expect( - collectionLinearizerService.shiftNodeLeft( - collection, 'exp_id1')).toBe(true); + collectionLinearizerService.shiftNodeLeft(collection, 'exp_id1') + ).toBe(true); expect( collectionLinearizerService.getCollectionNodesInPlayableOrder( - collection)).toEqual( - [secondCollectionNode, firstCollectionNode, thirdCollectionNode]); + collection + ) + ).toEqual([ + secondCollectionNode, + firstCollectionNode, + thirdCollectionNode, + ]); }); }); describe('shiftNodeRight()', () => { - it('should correctly shift a node in a single node collection', - () => { - let collection = Collection.createEmptyCollection(); - collection.addCollectionNode(firstCollectionNode); - expect( - collectionLinearizerService.getCollectionNodesInPlayableOrder( - collection)).toEqual([firstCollectionNode]); - expect( - collectionLinearizerService.shiftNodeRight( - collection, 'exp_id0')).toBe(true); - expect( - collectionLinearizerService.getCollectionNodesInPlayableOrder( - collection)).toEqual([firstCollectionNode]); - } - ); + it('should correctly shift a node in a single node collection', () => { + let collection = Collection.createEmptyCollection(); + collection.addCollectionNode(firstCollectionNode); + expect( + collectionLinearizerService.getCollectionNodesInPlayableOrder( + collection + ) + ).toEqual([firstCollectionNode]); + expect( + collectionLinearizerService.shiftNodeRight(collection, 'exp_id0') + ).toBe(true); + expect( + collectionLinearizerService.getCollectionNodesInPlayableOrder( + collection + ) + ).toEqual([firstCollectionNode]); + }); it('should not shift a non-existent node', () => { let collection = createLinearCollection(); expect( collectionLinearizerService.getCollectionNodesInPlayableOrder( - collection)).toEqual( - [firstCollectionNode, secondCollectionNode, thirdCollectionNode]); + collection + ) + ).toEqual([ + firstCollectionNode, + secondCollectionNode, + thirdCollectionNode, + ]); expect( - collectionLinearizerService.shiftNodeRight( - collection, 'non_existent')).toBe(false); + collectionLinearizerService.shiftNodeRight(collection, 'non_existent') + ).toBe(false); expect( collectionLinearizerService.getCollectionNodesInPlayableOrder( - collection)).toEqual( - [firstCollectionNode, secondCollectionNode, thirdCollectionNode]); + collection + ) + ).toEqual([ + firstCollectionNode, + secondCollectionNode, + thirdCollectionNode, + ]); }); it('should correctly shift the first node', () => { let collection = createLinearCollection(); expect( collectionLinearizerService.getCollectionNodesInPlayableOrder( - collection)).toEqual( - [firstCollectionNode, secondCollectionNode, thirdCollectionNode]); + collection + ) + ).toEqual([ + firstCollectionNode, + secondCollectionNode, + thirdCollectionNode, + ]); expect( - collectionLinearizerService.shiftNodeRight( - collection, 'exp_id0')).toBe(true); + collectionLinearizerService.shiftNodeRight(collection, 'exp_id0') + ).toBe(true); expect( collectionLinearizerService.getCollectionNodesInPlayableOrder( - collection)).toEqual( - [secondCollectionNode, firstCollectionNode, thirdCollectionNode]); + collection + ) + ).toEqual([ + secondCollectionNode, + firstCollectionNode, + thirdCollectionNode, + ]); }); it('should correctly shift the last node', () => { let collection = createLinearCollection(); expect( collectionLinearizerService.getCollectionNodesInPlayableOrder( - collection)).toEqual( - [firstCollectionNode, secondCollectionNode, thirdCollectionNode]); + collection + ) + ).toEqual([ + firstCollectionNode, + secondCollectionNode, + thirdCollectionNode, + ]); expect( - collectionLinearizerService.shiftNodeRight( - collection, 'exp_id2')).toBe(true); + collectionLinearizerService.shiftNodeRight(collection, 'exp_id2') + ).toBe(true); expect( collectionLinearizerService.getCollectionNodesInPlayableOrder( - collection)).toEqual( - [firstCollectionNode, secondCollectionNode, thirdCollectionNode]); + collection + ) + ).toEqual([ + firstCollectionNode, + secondCollectionNode, + thirdCollectionNode, + ]); }); it('should correctly shift middle node', () => { let collection = createLinearCollection(); expect( collectionLinearizerService.getCollectionNodesInPlayableOrder( - collection)).toEqual( - [firstCollectionNode, secondCollectionNode, thirdCollectionNode]); + collection + ) + ).toEqual([ + firstCollectionNode, + secondCollectionNode, + thirdCollectionNode, + ]); expect( - collectionLinearizerService.shiftNodeRight( - collection, 'exp_id1')).toBe(true); + collectionLinearizerService.shiftNodeRight(collection, 'exp_id1') + ).toBe(true); expect( collectionLinearizerService.getCollectionNodesInPlayableOrder( - collection)).toEqual( - [firstCollectionNode, thirdCollectionNode, secondCollectionNode]); + collection + ) + ).toEqual([ + firstCollectionNode, + thirdCollectionNode, + secondCollectionNode, + ]); }); }); describe('getNextExplorationId()', () => { - it('should return no exploration ids for a completed linear collection', - () => { - let collection = createLinearCollection(); - expect( - collectionLinearizerService.getNextExplorationId( - collection, ['exp_id0', 'exp_id1', 'exp_id2'])).toEqual(null); - } - ); + it('should return no exploration ids for a completed linear collection', () => { + let collection = createLinearCollection(); + expect( + collectionLinearizerService.getNextExplorationId(collection, [ + 'exp_id0', + 'exp_id1', + 'exp_id2', + ]) + ).toEqual(null); + }); - it('should return next exploration id for a partially completed collection', - () => { - let collection = createLinearCollection(); - expect( - collectionLinearizerService.getNextExplorationId( - collection, ['exp_id0', 'exp_id1'])).toEqual('exp_id2'); - } - ); + it('should return next exploration id for a partially completed collection', () => { + let collection = createLinearCollection(); + expect( + collectionLinearizerService.getNextExplorationId(collection, [ + 'exp_id0', + 'exp_id1', + ]) + ).toEqual('exp_id2'); + }); }); describe('getCollectionNodesInPlayableOrder()', () => { - it('should correctly return an empty list for an empty collection', - () => { - let collection = Collection.createEmptyCollection(); - expect( - collectionLinearizerService.getCollectionNodesInPlayableOrder( - collection)).toEqual([]); - } - ); + it('should correctly return an empty list for an empty collection', () => { + let collection = Collection.createEmptyCollection(); + expect( + collectionLinearizerService.getCollectionNodesInPlayableOrder( + collection + ) + ).toEqual([]); + }); - it('should correctly return a list for a collection with a single node', - () => { - let collection = Collection.createEmptyCollection(); - collection.addCollectionNode(firstCollectionNode); - expect( - collectionLinearizerService.getCollectionNodesInPlayableOrder( - collection)).toEqual([firstCollectionNode]); - } - ); + it('should correctly return a list for a collection with a single node', () => { + let collection = Collection.createEmptyCollection(); + collection.addCollectionNode(firstCollectionNode); + expect( + collectionLinearizerService.getCollectionNodesInPlayableOrder( + collection + ) + ).toEqual([firstCollectionNode]); + }); - it('should correctly return a list for a collection with multiple nodes', - () => { - let collection = createLinearCollection(); - expect( - collectionLinearizerService.getCollectionNodesInPlayableOrder( - collection)).toEqual( - [firstCollectionNode, secondCollectionNode, thirdCollectionNode]); - } - ); + it('should correctly return a list for a collection with multiple nodes', () => { + let collection = createLinearCollection(); + expect( + collectionLinearizerService.getCollectionNodesInPlayableOrder( + collection + ) + ).toEqual([ + firstCollectionNode, + secondCollectionNode, + thirdCollectionNode, + ]); + }); }); }); diff --git a/core/templates/pages/collection-editor-page/services/collection-linearizer.service.ts b/core/templates/pages/collection-editor-page/services/collection-linearizer.service.ts index 26b150e9138c..a88cf4a353a1 100644 --- a/core/templates/pages/collection-editor-page/services/collection-linearizer.service.ts +++ b/core/templates/pages/collection-editor-page/services/collection-linearizer.service.ts @@ -18,25 +18,29 @@ * retrieving the collection, saving it, and listening for changes. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { CollectionNode } from 'domain/collection/collection-node.model'; -import { CollectionUpdateService } from 'domain/collection/collection-update.service'; -import { Collection } from 'domain/collection/collection.model'; -import { LearnerExplorationSummaryBackendDict } from 'domain/summary/learner-exploration-summary.model'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {CollectionNode} from 'domain/collection/collection-node.model'; +import {CollectionUpdateService} from 'domain/collection/collection-update.service'; +import {Collection} from 'domain/collection/collection.model'; +import {LearnerExplorationSummaryBackendDict} from 'domain/summary/learner-exploration-summary.model'; interface SwapFunction { ( collection: Collection, - linearNodeList: CollectionNode[], nodeIndex: number): void; + linearNodeList: CollectionNode[], + nodeIndex: number + ): void; ( collection: Collection, - linearNodeList: CollectionNode[], nodeIndex: number): void; + linearNodeList: CollectionNode[], + nodeIndex: number + ): void; (arg0: Collection, arg1: CollectionNode[], arg2: number): void; - } +} @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class CollectionLinearizerService { // These properties are initialized using private methods @@ -44,13 +48,12 @@ export class CollectionLinearizerService { // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 explorationIds!: string[]; explorationId!: number; - constructor( - private collectionUpdateService: CollectionUpdateService - ) {} + constructor(private collectionUpdateService: CollectionUpdateService) {} _getNextExplorationId( - collection: Collection, - completedExpIds: string[]): string | null { + collection: Collection, + completedExpIds: string[] + ): string | null { this.explorationIds = collection.getExplorationIds(); for (let i = 0; i < this.explorationIds.length; i++) { @@ -72,8 +75,9 @@ export class CollectionLinearizerService { } findNodeIndex( - linearNodeList: CollectionNode[], - explorationId: string): number { + linearNodeList: CollectionNode[], + explorationId: string + ): number { let index = -1; for (let i = 0; i < linearNodeList.length; i++) { if (linearNodeList[i].getExplorationId() === explorationId) { @@ -87,9 +91,10 @@ export class CollectionLinearizerService { // Swap the node at the specified index with the node immediately to the // left of it. swapLeft = ( - collection: Collection, - linearNodeList: CollectionNode[], - nodeIndex: number): void => { + collection: Collection, + linearNodeList: CollectionNode[], + nodeIndex: number + ): void => { let leftNodeIndex = nodeIndex > 0 ? nodeIndex - 1 : null; if (leftNodeIndex === null) { @@ -97,21 +102,30 @@ export class CollectionLinearizerService { } this.collectionUpdateService.swapNodes( - collection, leftNodeIndex, nodeIndex); + collection, + leftNodeIndex, + nodeIndex + ); }; swapRight = ( - collection: Collection, - linearNodeList: CollectionNode[], nodeIndex: number): void => { + collection: Collection, + linearNodeList: CollectionNode[], + nodeIndex: number + ): void => { let rightNodeIndex = nodeIndex + 1; this.collectionUpdateService.swapNodes( - collection, rightNodeIndex, nodeIndex + collection, + rightNodeIndex, + nodeIndex ); }; shiftNode( - collection: Collection, explorationId: string, - swapFunction: SwapFunction): boolean { + collection: Collection, + explorationId: string, + swapFunction: SwapFunction + ): boolean { // There is nothing to shift if the collection has only 1 node. if (collection.getCollectionNodeCount() > 1) { let linearNodeList = this._getCollectionNodesInPlayableOrder(collection); @@ -125,36 +139,43 @@ export class CollectionLinearizerService { } /** - * Given a collection and a list of completed exploration IDs within the - * context of that collection, returns a list of which explorations in the - * collection is immediately playable by the user. NOTE: This function - * does not assume that the collection is linear. - */ + * Given a collection and a list of completed exploration IDs within the + * context of that collection, returns a list of which explorations in the + * collection is immediately playable by the user. NOTE: This function + * does not assume that the collection is linear. + */ // Returns null if the collection is empty or next exploration is not found. getNextExplorationId( - collection: Collection, completedExpIds: string[]): string | null { + collection: Collection, + completedExpIds: string[] + ): string | null { return this._getNextExplorationId(collection, completedExpIds); } /** - * Given a collection, returns a linear list of collection nodes which - * represents a valid path for playing through this collection. - */ + * Given a collection, returns a linear list of collection nodes which + * represents a valid path for playing through this collection. + */ getCollectionNodesInPlayableOrder(collection: Collection): CollectionNode[] { return this._getCollectionNodesInPlayableOrder(collection); } /** - * Inserts a new collection node at the end of the collection's playable - * list of explorations, based on the specified exploration ID and - * exploration summary backend object. - */ + * Inserts a new collection node at the end of the collection's playable + * list of explorations, based on the specified exploration ID and + * exploration summary backend object. + */ appendCollectionNode( - collection: Collection, explorationId: string, - summaryBackendObject: LearnerExplorationSummaryBackendDict): void { + collection: Collection, + explorationId: string, + summaryBackendObject: LearnerExplorationSummaryBackendDict + ): void { let linearNodeList = this._getCollectionNodesInPlayableOrder(collection); this.collectionUpdateService.addCollectionNode( - collection, explorationId, summaryBackendObject); + collection, + explorationId, + summaryBackendObject + ); if (linearNodeList.length > 0) { let lastNode = linearNodeList[linearNodeList.length - 1]; this.addAfter(collection, lastNode.getExplorationId()); @@ -162,12 +183,12 @@ export class CollectionLinearizerService { } /** - * Remove a collection node from a given collection which maps to the - * specified exploration ID. This function ensures the linear structure of - * the collection is maintained. Returns whether the provided exploration - * ID is contained within the linearly playable path of the specified - * collection. - */ + * Remove a collection node from a given collection which maps to the + * specified exploration ID. This function ensures the linear structure of + * the collection is maintained. Returns whether the provided exploration + * ID is contained within the linearly playable path of the specified + * collection. + */ removeCollectionNode(collection: Collection, explorationId: string): boolean { if (!collection.containsCollectionNode(explorationId)) { return false; @@ -175,32 +196,38 @@ export class CollectionLinearizerService { // Delete the node. this.collectionUpdateService.deleteCollectionNode( - collection, explorationId); + collection, + explorationId + ); return true; } /** - * Looks up a collection node given an exploration ID in the specified - * collection and attempts to shift it left in the linear ordering of the - * collection. If the node is the first exploration played by the player, - * then this function is a no-op. Returns false if the specified - * exploration ID does not associate to any nodes in the collection. - */ + * Looks up a collection node given an exploration ID in the specified + * collection and attempts to shift it left in the linear ordering of the + * collection. If the node is the first exploration played by the player, + * then this function is a no-op. Returns false if the specified + * exploration ID does not associate to any nodes in the collection. + */ shiftNodeLeft(collection: Collection, explorationId: string): boolean { return this.shiftNode(collection, explorationId, this.swapLeft); } /** - * Looks up a collection node given an exploration ID in the specified - * collection and attempts to shift it right in the linear ordering of the - * collection. If the node is the last exploration played by the player, - * then this function is a no-op. Returns false if the specified - * exploration ID does not associate to any nodes in the collection. - */ + * Looks up a collection node given an exploration ID in the specified + * collection and attempts to shift it right in the linear ordering of the + * collection. If the node is the last exploration played by the player, + * then this function is a no-op. Returns false if the specified + * exploration ID does not associate to any nodes in the collection. + */ shiftNodeRight(collection: Collection, explorationId: string): boolean { return this.shiftNode(collection, explorationId, this.swapRight); } } -angular.module('oppia').factory('CollectionLinearizerService', - downgradeInjectable(CollectionLinearizerService)); +angular + .module('oppia') + .factory( + 'CollectionLinearizerService', + downgradeInjectable(CollectionLinearizerService) + ); diff --git a/core/templates/pages/collection-editor-page/settings-tab/collection-details-editor.component.spec.ts b/core/templates/pages/collection-editor-page/settings-tab/collection-details-editor.component.spec.ts index 6014313dd30d..da18ec0c935e 100644 --- a/core/templates/pages/collection-editor-page/settings-tab/collection-details-editor.component.spec.ts +++ b/core/templates/pages/collection-editor-page/settings-tab/collection-details-editor.component.spec.ts @@ -16,17 +16,17 @@ * @fileoverview Unit tests for collection details editor component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { AppConstants } from 'app.constants'; -import { CollectionPlaythrough } from 'domain/collection/collection-playthrough.model'; -import { CollectionUpdateService } from 'domain/collection/collection-update.service'; -import { CollectionValidationService } from 'domain/collection/collection-validation.service'; -import { Collection } from 'domain/collection/collection.model'; -import { AlertsService } from 'services/alerts.service'; -import { CollectionEditorStateService } from '../services/collection-editor-state.service'; -import { CollectionDetailsEditorComponent } from './collection-details-editor.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {AppConstants} from 'app.constants'; +import {CollectionPlaythrough} from 'domain/collection/collection-playthrough.model'; +import {CollectionUpdateService} from 'domain/collection/collection-update.service'; +import {CollectionValidationService} from 'domain/collection/collection-validation.service'; +import {Collection} from 'domain/collection/collection.model'; +import {AlertsService} from 'services/alerts.service'; +import {CollectionEditorStateService} from '../services/collection-editor-state.service'; +import {CollectionDetailsEditorComponent} from './collection-details-editor.component'; describe('Collection details editor component', () => { let fixture: ComponentFixture; @@ -43,24 +43,29 @@ describe('Collection details editor component', () => { let languageCode = 'en'; let collectionTags = ['mock tag']; let mockCollection = new Collection( - 'id', collectionTitle, collectionObjective, languageCode, collectionTags, - new CollectionPlaythrough(null, []), collectionCategory, 0, 1, []); + 'id', + collectionTitle, + collectionObjective, + languageCode, + collectionTags, + new CollectionPlaythrough(null, []), + collectionCategory, + 0, + 1, + [] + ); beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - CollectionDetailsEditorComponent - ], + imports: [HttpClientTestingModule], + declarations: [CollectionDetailsEditorComponent], providers: [ AlertsService, CollectionEditorStateService, CollectionUpdateService, - CollectionValidationService + CollectionValidationService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -81,11 +86,14 @@ describe('Collection details editor component', () => { it('should initialize', () => { let mockOnCollectionInitializedEventEmitter = new EventEmitter(); - spyOnProperty(collectionEditorStateService, 'onCollectionInitialized') - .and.returnValue(mockOnCollectionInitializedEventEmitter); + spyOnProperty( + collectionEditorStateService, + 'onCollectionInitialized' + ).and.returnValue(mockOnCollectionInitializedEventEmitter); spyOn(componentInstance, 'refreshSettingsTab'); spyOn(collectionEditorStateService, 'getCollection').and.returnValue( - mockCollection); + mockCollection + ); componentInstance.ngOnInit(); mockOnCollectionInitializedEventEmitter.emit(); @@ -105,9 +113,11 @@ describe('Collection details editor component', () => { expect(componentInstance.displayedCollectionTitle).toEqual(collectionTitle); expect(componentInstance.displayedCollectionObjective).toEqual( - collectionObjective); + collectionObjective + ); expect(componentInstance.displayedCollectionCategory).toEqual( - collectionCategory); + collectionCategory + ); expect(componentInstance.displayedCollectionLanguage).toEqual(languageCode); expect(componentInstance.displayedCollectionTags).toEqual(collectionTags); expect(componentInstance.CATEGORY_LIST).toEqual(categoryList); @@ -115,15 +125,19 @@ describe('Collection details editor component', () => { it('should tell page has loaded', () => { spyOn(collectionEditorStateService, 'hasLoadedCollection').and.returnValues( - true, false); + true, + false + ); expect(componentInstance.hasPageLoaded()).toBeTrue(); expect(componentInstance.hasPageLoaded()).toBeFalse(); }); it('should normlize tags', () => { let tags = [' category 1 ', 'category 2 ']; - expect(componentInstance.normalizeTags(tags)).toEqual( - ['category 1', 'category 2']); + expect(componentInstance.normalizeTags(tags)).toEqual([ + 'category 1', + 'category 2', + ]); }); it('should return empty list if there are no tags on normalization', () => { @@ -149,14 +163,23 @@ describe('Collection details editor component', () => { componentInstance.updateCollectionLanguageCode(); expect(collectionUpdateService.setCollectionTitle).toHaveBeenCalledWith( - mockCollection, componentInstance.displayedCollectionTitle); + mockCollection, + componentInstance.displayedCollectionTitle + ); expect(collectionUpdateService.setCollectionObjective).toHaveBeenCalledWith( - mockCollection, componentInstance.displayedCollectionObjective); + mockCollection, + componentInstance.displayedCollectionObjective + ); expect(collectionUpdateService.setCollectionCategory).toHaveBeenCalledWith( - mockCollection, componentInstance.displayedCollectionCategory); - expect(collectionUpdateService.setCollectionLanguageCode) - .toHaveBeenCalledWith( - mockCollection, componentInstance.displayedCollectionLanguage); + mockCollection, + componentInstance.displayedCollectionCategory + ); + expect( + collectionUpdateService.setCollectionLanguageCode + ).toHaveBeenCalledWith( + mockCollection, + componentInstance.displayedCollectionLanguage + ); }); it('should update collection tags', () => { @@ -170,7 +193,9 @@ describe('Collection details editor component', () => { componentInstance.updateCollectionTags(); expect(collectionUpdateService.setCollectionTags).toHaveBeenCalledWith( - mockCollection, collectionTags); + mockCollection, + collectionTags + ); }); it('should show alert when collection tags are not valid', () => { @@ -185,6 +210,7 @@ describe('Collection details editor component', () => { expect(alertsService.addWarning).toHaveBeenCalledWith( 'Please ensure that there are no duplicate tags and that all ' + - 'tags contain only lower case and spaces.'); + 'tags contain only lower case and spaces.' + ); }); }); diff --git a/core/templates/pages/collection-editor-page/settings-tab/collection-details-editor.component.ts b/core/templates/pages/collection-editor-page/settings-tab/collection-details-editor.component.ts index f53da876c914..e5ad652758cc 100644 --- a/core/templates/pages/collection-editor-page/settings-tab/collection-details-editor.component.ts +++ b/core/templates/pages/collection-editor-page/settings-tab/collection-details-editor.component.ts @@ -18,19 +18,19 @@ * adding a new exploration. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { CollectionUpdateService } from 'domain/collection/collection-update.service'; -import { CollectionValidationService } from 'domain/collection/collection-validation.service'; -import { Collection } from 'domain/collection/collection.model'; -import { Subscription } from 'rxjs'; -import { AlertsService } from 'services/alerts.service'; -import { CollectionEditorPageConstants } from '../collection-editor-page.constants'; -import { CollectionEditorStateService } from '../services/collection-editor-state.service'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {AppConstants} from 'app.constants'; +import {CollectionUpdateService} from 'domain/collection/collection-update.service'; +import {CollectionValidationService} from 'domain/collection/collection-validation.service'; +import {Collection} from 'domain/collection/collection.model'; +import {Subscription} from 'rxjs'; +import {AlertsService} from 'services/alerts.service'; +import {CollectionEditorPageConstants} from '../collection-editor-page.constants'; +import {CollectionEditorStateService} from '../services/collection-editor-state.service'; @Component({ selector: 'collection-details-editor', - templateUrl: './collection-details-editor.component.html' + templateUrl: './collection-details-editor.component.html', }) export class CollectionDetailsEditorComponent implements OnInit, OnDestroy { directiveSubscriptions = new Subscription(); @@ -44,8 +44,8 @@ export class CollectionDetailsEditorComponent implements OnInit, OnDestroy { displayedCollectionObjective!: string | null; displayedCollectionCategory!: string | null; displayedCollectionLanguage!: string | null; - COLLECTION_TITLE_INPUT_FOCUS_LABEL = ( - CollectionEditorPageConstants.COLLECTION_TITLE_INPUT_FOCUS_LABEL); + COLLECTION_TITLE_INPUT_FOCUS_LABEL = + CollectionEditorPageConstants.COLLECTION_TITLE_INPUT_FOCUS_LABEL; CATEGORY_LIST: string[] = [...AppConstants.ALL_CATEGORIES]; languageListForSelect = AppConstants.SUPPORTED_CONTENT_LANGUAGES; @@ -61,9 +61,10 @@ export class CollectionDetailsEditorComponent implements OnInit, OnDestroy { ngOnInit(): void { this.directiveSubscriptions.add( - this.collectionEditorStateService.onCollectionInitialized.subscribe( - () => this.refreshSettingsTab() - )); + this.collectionEditorStateService.onCollectionInitialized.subscribe(() => + this.refreshSettingsTab() + ) + ); this.collection = this.collectionEditorStateService.getCollection(); } @@ -74,7 +75,7 @@ export class CollectionDetailsEditorComponent implements OnInit, OnDestroy { this.displayedCollectionLanguage = this.collection.getLanguageCode(); this.displayedCollectionTags = this.collection.getTags(); - let categoryIsInList = this.CATEGORY_LIST.some((categoryItem) => { + let categoryIsInList = this.CATEGORY_LIST.some(categoryItem => { return categoryItem === this.collection.getCategory(); }); @@ -103,22 +104,30 @@ export class CollectionDetailsEditorComponent implements OnInit, OnDestroy { updateCollectionTitle(): void { this.collectionUpdateService.setCollectionTitle( - this.collection, this.displayedCollectionTitle); + this.collection, + this.displayedCollectionTitle + ); } updateCollectionObjective(): void { this.collectionUpdateService.setCollectionObjective( - this.collection, this.displayedCollectionObjective); + this.collection, + this.displayedCollectionObjective + ); } updateCollectionCategory(): void { this.collectionUpdateService.setCollectionCategory( - this.collection, this.displayedCollectionCategory); + this.collection, + this.displayedCollectionCategory + ); } updateCollectionLanguageCode(): void { this.collectionUpdateService.setCollectionLanguageCode( - this.collection, this.displayedCollectionLanguage); + this.collection, + this.displayedCollectionLanguage + ); } updateCollectionTags(): void { @@ -126,15 +135,19 @@ export class CollectionDetailsEditorComponent implements OnInit, OnDestroy { this.displayedCollectionTags ); - if (!this.collectionValidationService.isTagValid( - this.displayedCollectionTags)) { + if ( + !this.collectionValidationService.isTagValid(this.displayedCollectionTags) + ) { this.alertsService.addWarning( 'Please ensure that there are no duplicate tags and that all ' + - 'tags contain only lower case and spaces.'); + 'tags contain only lower case and spaces.' + ); return; } this.collectionUpdateService.setCollectionTags( - this.collection, this.displayedCollectionTags); + this.collection, + this.displayedCollectionTags + ); } ngOnDestroy(): void { diff --git a/core/templates/pages/collection-editor-page/settings-tab/collection-permissions-card.component.spec.ts b/core/templates/pages/collection-editor-page/settings-tab/collection-permissions-card.component.spec.ts index 57c6fefacef5..49b730a88da7 100644 --- a/core/templates/pages/collection-editor-page/settings-tab/collection-permissions-card.component.spec.ts +++ b/core/templates/pages/collection-editor-page/settings-tab/collection-permissions-card.component.spec.ts @@ -16,12 +16,12 @@ * @fileoverview Unit tests for collection permissions card component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { CollectionRights } from 'domain/collection/collection-rights.model'; -import { CollectionEditorStateService } from '../services/collection-editor-state.service'; -import { CollectionPermissionsCardComponent } from './collection-permissions-card.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {CollectionRights} from 'domain/collection/collection-rights.model'; +import {CollectionEditorStateService} from '../services/collection-editor-state.service'; +import {CollectionPermissionsCardComponent} from './collection-permissions-card.component'; describe('Collection permissions card component', () => { let fixture: ComponentFixture; @@ -30,16 +30,10 @@ describe('Collection permissions card component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - CollectionPermissionsCardComponent - ], - providers: [ - CollectionEditorStateService - ], - schemas: [NO_ERRORS_SCHEMA] + imports: [HttpClientTestingModule], + declarations: [CollectionPermissionsCardComponent], + providers: [CollectionEditorStateService], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -55,17 +49,20 @@ describe('Collection permissions card component', () => { can_edit: null, can_unpublish: null, is_private: null, - owner_names: [] + owner_names: [], }); spyOn(collectionEditorStateService, 'getCollectionRights').and.returnValue( - mockCollectionRights); + mockCollectionRights + ); componentInstance.ngOnInit(); expect(componentInstance.collectionRights).toEqual(mockCollectionRights); }); it('should tell if page has loaded', () => { spyOn(collectionEditorStateService, 'hasLoadedCollection').and.returnValues( - false, true); + false, + true + ); expect(componentInstance.hasPageLoaded()).toBeFalse(); expect(componentInstance.hasPageLoaded()).toBeTrue(); }); diff --git a/core/templates/pages/collection-editor-page/settings-tab/collection-permissions-card.component.ts b/core/templates/pages/collection-editor-page/settings-tab/collection-permissions-card.component.ts index c5406a777402..c858e4d40b24 100644 --- a/core/templates/pages/collection-editor-page/settings-tab/collection-permissions-card.component.ts +++ b/core/templates/pages/collection-editor-page/settings-tab/collection-permissions-card.component.ts @@ -17,13 +17,13 @@ * permissions. */ -import { Component } from '@angular/core'; -import { CollectionRights } from 'domain/collection/collection-rights.model'; -import { CollectionEditorStateService } from '../services/collection-editor-state.service'; +import {Component} from '@angular/core'; +import {CollectionRights} from 'domain/collection/collection-rights.model'; +import {CollectionEditorStateService} from '../services/collection-editor-state.service'; @Component({ selector: 'collection-permissions-card', - templateUrl: './collection-permissions-card.component.html' + templateUrl: './collection-permissions-card.component.html', }) export class CollectionPermissionsCardComponent { // This property is initialized using Angular lifecycle hooks @@ -40,7 +40,7 @@ export class CollectionPermissionsCardComponent { } ngOnInit(): void { - this.collectionRights = ( - this.collectionEditorStateService.getCollectionRights()); + this.collectionRights = + this.collectionEditorStateService.getCollectionRights(); } } diff --git a/core/templates/pages/collection-editor-page/settings-tab/collection-settings-tab.component.ts b/core/templates/pages/collection-editor-page/settings-tab/collection-settings-tab.component.ts index 4d564d068a9b..706a1d8c7110 100644 --- a/core/templates/pages/collection-editor-page/settings-tab/collection-settings-tab.component.ts +++ b/core/templates/pages/collection-editor-page/settings-tab/collection-settings-tab.component.ts @@ -16,17 +16,21 @@ * @fileoverview Component for the settings tab of the collection editor. */ -import { Component } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; @Component({ selector: 'collection-settings-tab', templateUrl: './collection-settings-tab.component.html', - styleUrls: [] + styleUrls: [], }) export class CollectionSettingsTabComponent { constructor() {} } -angular.module('oppia').directive('collectionSettingsTab', downgradeComponent( - {component: CollectionSettingsTabComponent})); +angular + .module('oppia') + .directive( + 'collectionSettingsTab', + downgradeComponent({component: CollectionSettingsTabComponent}) + ); diff --git a/core/templates/pages/collection-editor-page/statistics-tab/collection-statistics-tab.component.ts b/core/templates/pages/collection-editor-page/statistics-tab/collection-statistics-tab.component.ts index edfd08203942..aad8493256ce 100644 --- a/core/templates/pages/collection-editor-page/statistics-tab/collection-statistics-tab.component.ts +++ b/core/templates/pages/collection-editor-page/statistics-tab/collection-statistics-tab.component.ts @@ -16,17 +16,21 @@ * @fileoverview Component for the statistics tab of the collection editor. */ -import { Component } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; @Component({ selector: 'collection-statistics-tab', templateUrl: './collection-statistics-tab.component.html', - styleUrls: [] + styleUrls: [], }) export class CollectionStatisticsTabComponent { constructor() {} } -angular.module('oppia').directive('collectionStatisticsTab', downgradeComponent( - {component: CollectionStatisticsTabComponent})); +angular + .module('oppia') + .directive( + 'collectionStatisticsTab', + downgradeComponent({component: CollectionStatisticsTabComponent}) + ); diff --git a/core/templates/pages/collection-player-page/collection-footer/collection-footer.component.spec.ts b/core/templates/pages/collection-player-page/collection-footer/collection-footer.component.spec.ts index e30d565b9d0c..94144cefb264 100644 --- a/core/templates/pages/collection-player-page/collection-footer/collection-footer.component.spec.ts +++ b/core/templates/pages/collection-player-page/collection-footer/collection-footer.component.spec.ts @@ -16,14 +16,13 @@ * @fileoverview Unit tests for collection footer component. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { TestBed, waitForAsync, ComponentFixture } from '@angular/core/testing'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; -import { UrlService } from 'services/contextual/url.service'; -import { CollectionFooterComponent } from './collection-footer.component'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {TestBed, waitForAsync, ComponentFixture} from '@angular/core/testing'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {UrlService} from 'services/contextual/url.service'; +import {CollectionFooterComponent} from './collection-footer.component'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; describe('Collection footer component', () => { let urlInterpolationService: UrlInterpolationService; @@ -34,17 +33,9 @@ describe('Collection footer component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - CollectionFooterComponent, - MockTranslatePipe - ], - providers: [ - UrlInterpolationService, - UrlService - ], - schemas: [ - NO_ERRORS_SCHEMA - ] + declarations: [CollectionFooterComponent, MockTranslatePipe], + providers: [UrlInterpolationService, UrlService], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -56,13 +47,15 @@ describe('Collection footer component', () => { i18nLanguageCodeService = TestBed.inject(I18nLanguageCodeService); spyOn(i18nLanguageCodeService, 'isCurrentLanguageRTL').and.returnValue( - true); + true + ); }); beforeEach(() => { spyOn(urlService, 'getCollectionIdFromUrl').and.returnValue('abcdef'); spyOn(urlInterpolationService, 'getStaticImageUrl').and.returnValue( - '/assets/images/general/apple.svg'); + '/assets/images/general/apple.svg' + ); component.ngOnInit(); }); @@ -72,9 +65,11 @@ describe('Collection footer component', () => { }); it('should get the static image url from the image path', () => { - expect(component.getStaticImageUrl('/general/apple.svg')) - .toBe('/assets/images/general/apple.svg'); - expect(urlInterpolationService.getStaticImageUrl) - .toHaveBeenCalledWith('/general/apple.svg'); + expect(component.getStaticImageUrl('/general/apple.svg')).toBe( + '/assets/images/general/apple.svg' + ); + expect(urlInterpolationService.getStaticImageUrl).toHaveBeenCalledWith( + '/general/apple.svg' + ); }); }); diff --git a/core/templates/pages/collection-player-page/collection-footer/collection-footer.component.ts b/core/templates/pages/collection-player-page/collection-footer/collection-footer.component.ts index 488278182c8e..a782d5b4d632 100644 --- a/core/templates/pages/collection-player-page/collection-footer/collection-footer.component.ts +++ b/core/templates/pages/collection-player-page/collection-footer/collection-footer.component.ts @@ -17,20 +17,18 @@ * in collection player. */ -import { Component, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; -import { UrlService } from 'services/contextual/url.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {UrlService} from 'services/contextual/url.service'; import './collection-footer.component.css'; - @Component({ selector: 'collection-footer', templateUrl: './collection-footer.component.html', - styleUrls: ['./collection-footer.component.css'] + styleUrls: ['./collection-footer.component.css'], }) export class CollectionFooterComponent implements OnInit { collectionId: string = ''; @@ -48,6 +46,9 @@ export class CollectionFooterComponent implements OnInit { } } -angular.module('oppia').directive( - 'collectionFooter', downgradeComponent( - {component: CollectionFooterComponent})); +angular + .module('oppia') + .directive( + 'collectionFooter', + downgradeComponent({component: CollectionFooterComponent}) + ); diff --git a/core/templates/pages/collection-player-page/collection-local-nav/collection-local-nav.component.spec.ts b/core/templates/pages/collection-player-page/collection-local-nav/collection-local-nav.component.spec.ts index a1dc42c9f303..d92e8c23b6b8 100644 --- a/core/templates/pages/collection-player-page/collection-local-nav/collection-local-nav.component.spec.ts +++ b/core/templates/pages/collection-player-page/collection-local-nav/collection-local-nav.component.spec.ts @@ -16,13 +16,13 @@ * @fileoverview Unit tests for CollectionLocalNavComponent. */ -import { EventEmitter } from '@angular/core'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import {EventEmitter} from '@angular/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; -import { CollectionLocalNavComponent } from './collection-local-nav.component'; -import { ReadOnlyCollectionBackendApiService } from 'domain/collection/read-only-collection-backend-api.service'; -import { UrlService } from 'services/contextual/url.service'; +import {CollectionLocalNavComponent} from './collection-local-nav.component'; +import {ReadOnlyCollectionBackendApiService} from 'domain/collection/read-only-collection-backend-api.service'; +import {UrlService} from 'services/contextual/url.service'; describe('CollectionLocalNavComponent', () => { let component: CollectionLocalNavComponent; @@ -32,7 +32,7 @@ describe('CollectionLocalNavComponent', () => { let mockCollectionDetails = { canEdit: false, - title: 'Title of Collection' + title: 'Title of Collection', }; var mockCollectionLoadEventEmitter = new EventEmitter(); @@ -40,14 +40,15 @@ describe('CollectionLocalNavComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [CollectionLocalNavComponent], - imports: [HttpClientTestingModule] + imports: [HttpClientTestingModule], }).compileComponents(); rocbs = TestBed.get(ReadOnlyCollectionBackendApiService); urlService = TestBed.get(UrlService); spyOnProperty(rocbs, 'onCollectionLoad').and.returnValue( - mockCollectionLoadEventEmitter); + mockCollectionLoadEventEmitter + ); spyOn(rocbs, 'getCollectionDetails').and.returnValue(mockCollectionDetails); spyOn(urlService, 'getCollectionIdFromUrl').and.returnValue('1'); })); diff --git a/core/templates/pages/collection-player-page/collection-local-nav/collection-local-nav.component.ts b/core/templates/pages/collection-player-page/collection-local-nav/collection-local-nav.component.ts index bbbd0f8ee4c1..d97c5d5a49d4 100644 --- a/core/templates/pages/collection-player-page/collection-local-nav/collection-local-nav.component.ts +++ b/core/templates/pages/collection-player-page/collection-local-nav/collection-local-nav.component.ts @@ -16,19 +16,18 @@ * @fileoverview Component for the local navigation in the collection view. */ -import { Component, OnInit, OnDestroy } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component, OnInit, OnDestroy} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; -import { Subscription } from 'rxjs'; +import {Subscription} from 'rxjs'; -import { ReadOnlyCollectionBackendApiService } from - 'domain/collection/read-only-collection-backend-api.service'; -import { UrlService } from 'services/contextual/url.service'; +import {ReadOnlyCollectionBackendApiService} from 'domain/collection/read-only-collection-backend-api.service'; +import {UrlService} from 'services/contextual/url.service'; @Component({ selector: 'collection-local-nav', templateUrl: './collection-local-nav.component.html', - styleUrls: [] + styleUrls: [], }) export class CollectionLocalNavComponent implements OnInit, OnDestroy { canEdit: boolean = false; @@ -36,9 +35,8 @@ export class CollectionLocalNavComponent implements OnInit, OnDestroy { directiveSubscriptions = new Subscription(); constructor( - private readOnlyCollectionBackendApiService: - ReadOnlyCollectionBackendApiService, - private urlService: UrlService, + private readOnlyCollectionBackendApiService: ReadOnlyCollectionBackendApiService, + private urlService: UrlService ) {} ngOnInit(): void { @@ -46,9 +44,10 @@ export class CollectionLocalNavComponent implements OnInit, OnDestroy { this.directiveSubscriptions.add( this.readOnlyCollectionBackendApiService.onCollectionLoad.subscribe( () => { - var collectionDetails = ( + var collectionDetails = this.readOnlyCollectionBackendApiService.getCollectionDetails( - this.collectionId)); + this.collectionId + ); this.canEdit = collectionDetails.canEdit; } ) @@ -59,6 +58,9 @@ export class CollectionLocalNavComponent implements OnInit, OnDestroy { this.directiveSubscriptions.unsubscribe(); } } -angular.module('oppia').directive( - 'collectionLocalNav', - downgradeComponent({component: CollectionLocalNavComponent})); +angular + .module('oppia') + .directive( + 'collectionLocalNav', + downgradeComponent({component: CollectionLocalNavComponent}) + ); diff --git a/core/templates/pages/collection-player-page/collection-navbar/collection-navbar.component.spec.ts b/core/templates/pages/collection-player-page/collection-navbar/collection-navbar.component.spec.ts index 9e53ab69f5d9..20956a6a3b1d 100644 --- a/core/templates/pages/collection-player-page/collection-navbar/collection-navbar.component.spec.ts +++ b/core/templates/pages/collection-player-page/collection-navbar/collection-navbar.component.spec.ts @@ -16,12 +16,18 @@ * @fileoverview Unit tests for collection navbar component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { TestBed, ComponentFixture, waitForAsync, fakeAsync, tick } from '@angular/core/testing'; -import { CollectionNavbarComponent } from './collection-navbar.component'; -import { ReadOnlyCollectionBackendApiService } from 'domain/collection/read-only-collection-backend-api.service'; -import { UrlService } from 'services/contextual/url.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + TestBed, + ComponentFixture, + waitForAsync, + fakeAsync, + tick, +} from '@angular/core/testing'; +import {CollectionNavbarComponent} from './collection-navbar.component'; +import {ReadOnlyCollectionBackendApiService} from 'domain/collection/read-only-collection-backend-api.service'; +import {UrlService} from 'services/contextual/url.service'; describe('Collection navbar component', () => { let us: UrlService; @@ -31,19 +37,10 @@ describe('Collection navbar component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - CollectionNavbarComponent - ], - providers: [ - UrlService, - ReadOnlyCollectionBackendApiService - ], - schemas: [ - NO_ERRORS_SCHEMA - ] + imports: [HttpClientTestingModule], + declarations: [CollectionNavbarComponent], + providers: [UrlService, ReadOnlyCollectionBackendApiService], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -57,11 +54,12 @@ describe('Collection navbar component', () => { it('should load the component properly on playing a collection', () => { const expectedCollectionDetail = { canEdit: true, - title: 'Test title' + title: 'Test title', }; spyOn(us, 'getCollectionIdFromUrl').and.returnValue('abcdef'); spyOn(rocbas, 'getCollectionDetails').and.returnValue( - expectedCollectionDetail); + expectedCollectionDetail + ); component.ngOnInit(); rocbas.onCollectionLoad.emit(); @@ -74,11 +72,12 @@ describe('Collection navbar component', () => { it('should throw error if collection title is null', fakeAsync(() => { const expectedCollectionDetail = { canEdit: true, - title: null + title: null, }; spyOn(us, 'getCollectionIdFromUrl').and.returnValue('abcdef'); spyOn(rocbas, 'getCollectionDetails').and.returnValue( - expectedCollectionDetail); + expectedCollectionDetail + ); component.ngOnInit(); expect(() => { diff --git a/core/templates/pages/collection-player-page/collection-navbar/collection-navbar.component.ts b/core/templates/pages/collection-player-page/collection-navbar/collection-navbar.component.ts index 214b5451d85e..f370d6f35dbd 100644 --- a/core/templates/pages/collection-player-page/collection-navbar/collection-navbar.component.ts +++ b/core/templates/pages/collection-player-page/collection-navbar/collection-navbar.component.ts @@ -16,13 +16,13 @@ * @fileoverview Component for the collection player navbar */ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; -import { Subscription } from 'rxjs'; +import {Subscription} from 'rxjs'; -import { ReadOnlyCollectionBackendApiService } from 'domain/collection/read-only-collection-backend-api.service'; -import { UrlService } from 'services/contextual/url.service'; +import {ReadOnlyCollectionBackendApiService} from 'domain/collection/read-only-collection-backend-api.service'; +import {UrlService} from 'services/contextual/url.service'; @Component({ selector: 'collection-navbar', @@ -35,17 +35,17 @@ export class CollectionNavbarComponent implements OnInit, OnDestroy { constructor( private urlService: UrlService, - private readOnlyCollectionBackendApiService: - ReadOnlyCollectionBackendApiService + private readOnlyCollectionBackendApiService: ReadOnlyCollectionBackendApiService ) {} ngOnInit(): void { this.directiveSubscriptions.add( this.readOnlyCollectionBackendApiService.onCollectionLoad.subscribe( () => { - let title = ( + let title = this.readOnlyCollectionBackendApiService.getCollectionDetails( - this.urlService.getCollectionIdFromUrl()).title); + this.urlService.getCollectionIdFromUrl() + ).title; if (title === null) { throw new Error('Collection title is null'); } @@ -59,5 +59,9 @@ export class CollectionNavbarComponent implements OnInit, OnDestroy { return this.directiveSubscriptions.unsubscribe(); } } -angular.module('oppia').directive('collectionNavbar', downgradeComponent( - {component: CollectionNavbarComponent})); +angular + .module('oppia') + .directive( + 'collectionNavbar', + downgradeComponent({component: CollectionNavbarComponent}) + ); diff --git a/core/templates/pages/collection-player-page/collection-node-list/collection-node-list.component.ts b/core/templates/pages/collection-player-page/collection-node-list/collection-node-list.component.ts index df945352fb93..12d12aa1040b 100644 --- a/core/templates/pages/collection-player-page/collection-node-list/collection-node-list.component.ts +++ b/core/templates/pages/collection-player-page/collection-node-list/collection-node-list.component.ts @@ -16,16 +16,15 @@ * @fileoverview Component for creating a list of collection nodes which link to * playing the exploration in each node. */ -import { Component, Input } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component, Input} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; -import { CollectionNode } from - 'domain/collection/collection-node.model'; +import {CollectionNode} from 'domain/collection/collection-node.model'; @Component({ selector: 'collection-node-list', templateUrl: './collection-node-list.component.html', - styleUrls: [] + styleUrls: [], }) export class CollectionNodeListComponent { // These properties are initialized using Angular lifecycle hooks @@ -36,6 +35,9 @@ export class CollectionNodeListComponent { constructor() {} } -angular.module('oppia').directive( - 'collectionNodeList', downgradeComponent( - {component: CollectionNodeListComponent})); +angular + .module('oppia') + .directive( + 'collectionNodeList', + downgradeComponent({component: CollectionNodeListComponent}) + ); diff --git a/core/templates/pages/collection-player-page/collection-player-page.component.spec.ts b/core/templates/pages/collection-player-page/collection-player-page.component.spec.ts index 1515b259c30d..cd1ac3d543de 100644 --- a/core/templates/pages/collection-player-page/collection-player-page.component.spec.ts +++ b/core/templates/pages/collection-player-page/collection-player-page.component.spec.ts @@ -16,27 +16,39 @@ * @fileoverview Unit tests for the Collection player page component. */ -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/compiler'; -import { EventEmitter } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; - -import { AlertsService } from 'services/alerts.service'; -import { CollectionPlayerBackendApiService } from './services/collection-player-backend-api.service'; -import { GuestCollectionProgressService } from 'domain/collection/guest-collection-progress.service'; -import { ReadOnlyCollectionBackendApiService } from 'domain/collection/read-only-collection-backend-api.service'; -import { UserService } from 'services/user.service'; -import { UrlService } from 'services/contextual/url.service'; -import { Collection, CollectionBackendDict } from 'domain/collection/collection.model'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { CollectionPlaythrough } from 'domain/collection/collection-playthrough.model'; -import { UserInfo } from 'domain/user/user-info.model'; -import { CollectionPlayerPageComponent, IconParametersArray } from './collection-player-page.component'; -import { CollectionNodeBackendDict } from 'domain/collection/collection-node.model'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { PageTitleService } from 'services/page-title.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/compiler'; +import {EventEmitter} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; + +import {AlertsService} from 'services/alerts.service'; +import {CollectionPlayerBackendApiService} from './services/collection-player-backend-api.service'; +import {GuestCollectionProgressService} from 'domain/collection/guest-collection-progress.service'; +import {ReadOnlyCollectionBackendApiService} from 'domain/collection/read-only-collection-backend-api.service'; +import {UserService} from 'services/user.service'; +import {UrlService} from 'services/contextual/url.service'; +import { + Collection, + CollectionBackendDict, +} from 'domain/collection/collection.model'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {CollectionPlaythrough} from 'domain/collection/collection-playthrough.model'; +import {UserInfo} from 'domain/user/user-info.model'; +import { + CollectionPlayerPageComponent, + IconParametersArray, +} from './collection-player-page.component'; +import {CollectionNodeBackendDict} from 'domain/collection/collection-node.model'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {PageTitleService} from 'services/page-title.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; class MockTranslateService { onLangChange: EventEmitter = new EventEmitter(); @@ -50,10 +62,8 @@ describe('Collection player page component', () => { let component: CollectionPlayerPageComponent; let fixture: ComponentFixture; let collectionPlayerBackendApiService: CollectionPlayerBackendApiService; - let guestCollectionProgressService: - GuestCollectionProgressService; - let readOnlyCollectionBackendApiService: - ReadOnlyCollectionBackendApiService; + let guestCollectionProgressService: GuestCollectionProgressService; + let readOnlyCollectionBackendApiService: ReadOnlyCollectionBackendApiService; let userService: UserService; let urlService: UrlService; let urlInterpolationService: UrlInterpolationService; @@ -67,39 +77,47 @@ describe('Collection player page component', () => { let i18nLanguageCodeService: I18nLanguageCodeService; const userInfoForCollectionCreator = new UserInfo( - ['USER_ROLE'], true, false, false, false, true, - 'en', 'username1', 'tester@example.com', true + ['USER_ROLE'], + true, + false, + false, + false, + true, + 'en', + 'username1', + 'tester@example.com', + true ); beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - CollectionPlayerPageComponent, - MockTranslatePipe - ], + declarations: [CollectionPlayerPageComponent, MockTranslatePipe], providers: [ PageTitleService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); beforeEach(() => { alertsService = TestBed.inject(AlertsService); collectionPlayerBackendApiService = TestBed.inject( - CollectionPlayerBackendApiService); + CollectionPlayerBackendApiService + ); userService = TestBed.inject(UserService); urlService = TestBed.inject(UrlService); urlInterpolationService = TestBed.inject(UrlInterpolationService); guestCollectionProgressService = TestBed.inject( - GuestCollectionProgressService); + GuestCollectionProgressService + ); readOnlyCollectionBackendApiService = TestBed.inject( - ReadOnlyCollectionBackendApiService); + ReadOnlyCollectionBackendApiService + ); translateService = TestBed.inject(TranslateService); pageTitleService = TestBed.inject(PageTitleService); i18nLanguageCodeService = TestBed.inject(I18nLanguageCodeService); @@ -124,14 +142,14 @@ describe('Collection player page component', () => { 2: 0, 3: 0, 4: 0, - 5: 0 + 5: 0, }, status: 'public', tags: [], activity_type: 'exploration', category: 'Algebra', - title: 'Test Title' - } + title: 'Test Title', + }, }; sampleCollectionBackendObject = { @@ -146,15 +164,15 @@ describe('Collection player page component', () => { collectionNodeBackendObject, collectionNodeBackendObject, collectionNodeBackendObject, - collectionNodeBackendObject + collectionNodeBackendObject, ], language_code: null, schema_version: null, tags: null, playthrough_dict: { next_exploration_id: 'expId', - completed_exploration_ids: ['expId2'] - } + completed_exploration_ids: ['expId2'], + }, }; collectionNodesList = [ @@ -162,92 +180,107 @@ describe('Collection player page component', () => { thumbnailIconUrl: '/inverted_subjects/Algebra.svg', left: '225px', top: '35px', - thumbnailBgColor: '#cc4b00' + thumbnailBgColor: '#cc4b00', }, { thumbnailIconUrl: '/inverted_subjects/Algebra.svg', left: '390px', top: '145px', - thumbnailBgColor: '#cc4b00' + thumbnailBgColor: '#cc4b00', }, { thumbnailIconUrl: '/inverted_subjects/Algebra.svg', left: '225px', top: '255px', - thumbnailBgColor: '#cc4b00' + thumbnailBgColor: '#cc4b00', }, { thumbnailIconUrl: '/inverted_subjects/Algebra.svg', left: '60px', top: '365px', - thumbnailBgColor: '#cc4b00' + thumbnailBgColor: '#cc4b00', }, { thumbnailIconUrl: '/inverted_subjects/Algebra.svg', left: '225px', top: '475px', - thumbnailBgColor: '#cc4b00' + thumbnailBgColor: '#cc4b00', }, { thumbnailIconUrl: '/inverted_subjects/Algebra.svg', left: '390px', top: '585px', - thumbnailBgColor: '#cc4b00' - } + thumbnailBgColor: '#cc4b00', + }, ]; - sampleCollection = Collection.create( - sampleCollectionBackendObject); + sampleCollection = Collection.create(sampleCollectionBackendObject); - spyOn(collectionPlayerBackendApiService, 'fetchCollectionSummariesAsync') - .and.returnValue(Promise.resolve({ + spyOn( + collectionPlayerBackendApiService, + 'fetchCollectionSummariesAsync' + ).and.returnValue( + Promise.resolve({ is_admin: false, is_topic_manager: false, summaries: [], user_email: 'tester@example.com', - username: false - })); + username: false, + }) + ); spyOn(urlService, 'getCollectionIdFromUrl').and.returnValue('collectionId'); alertsSpy = spyOn(alertsService, 'addWarning').and.returnValue(null); spyOn(i18nLanguageCodeService, 'isCurrentLanguageRTL').and.returnValue( - true); + true + ); }); - it('should throw warning message when an invalid collection ' + - 'is fetched from backend', fakeAsync(() => { - spyOn(readOnlyCollectionBackendApiService, 'loadCollectionAsync') - .and.rejectWith(); - spyOn(userService, 'getUserInfoAsync') - .and.returnValue(Promise.resolve(userInfoForCollectionCreator)); + it( + 'should throw warning message when an invalid collection ' + + 'is fetched from backend', + fakeAsync(() => { + spyOn( + readOnlyCollectionBackendApiService, + 'loadCollectionAsync' + ).and.rejectWith(); + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve(userInfoForCollectionCreator) + ); + + component.ngOnInit(); + tick(); + + expect(alertsSpy).toHaveBeenCalledWith( + 'There was an error loading the collection.' + ); + }) + ); + it('should set page title and subscribe to the lang change emitter', fakeAsync(() => { + spyOn(component, 'setPageTitle'); + spyOn(component, 'subscribeToOnLangChange'); + spyOn( + readOnlyCollectionBackendApiService, + 'loadCollectionAsync' + ).and.returnValue(Promise.resolve(sampleCollection)); component.ngOnInit(); tick(); - expect(alertsSpy).toHaveBeenCalledWith( - 'There was an error loading the collection.'); + expect(component.setPageTitle).toHaveBeenCalled(); + expect(component.subscribeToOnLangChange).toHaveBeenCalled(); })); - it('should set page title and subscribe to the lang change emitter', - fakeAsync(() => { + it( + 'should obtain translated title and set it whenever the ' + + 'selected language changes', + () => { + component.subscribeToOnLangChange(); spyOn(component, 'setPageTitle'); - spyOn(component, 'subscribeToOnLangChange'); - spyOn(readOnlyCollectionBackendApiService, 'loadCollectionAsync') - .and.returnValue(Promise.resolve(sampleCollection)); - component.ngOnInit(); - tick(); + translateService.onLangChange.emit(); expect(component.setPageTitle).toHaveBeenCalled(); - expect(component.subscribeToOnLangChange).toHaveBeenCalled(); - })); - - it('should obtain translated title and set it whenever the ' + - 'selected language changes', () => { - component.subscribeToOnLangChange(); - spyOn(component, 'setPageTitle'); - translateService.onLangChange.emit(); - - expect(component.setPageTitle).toHaveBeenCalled(); - }); + } + ); it('should set page title', () => { spyOn(translateService, 'instant').and.callThrough(); @@ -256,11 +289,14 @@ describe('Collection player page component', () => { component.setPageTitle(); expect(translateService.instant).toHaveBeenCalledWith( - 'I18N_COLLECTION_PLAYER_PAGE_TITLE', { - collectionTitle: 'title' - }); + 'I18N_COLLECTION_PLAYER_PAGE_TITLE', + { + collectionTitle: 'title', + } + ); expect(pageTitleService.setDocumentTitle).toHaveBeenCalledWith( - 'I18N_COLLECTION_PLAYER_PAGE_TITLE'); + 'I18N_COLLECTION_PLAYER_PAGE_TITLE' + ); }); it('should unsubscribe upon component destruction', () => { @@ -272,28 +308,30 @@ describe('Collection player page component', () => { }); it('should stop event propagation when click event is emitted', () => { - let eventSpy = jasmine.createSpyObj( - 'event', ['stopPropagation']); + let eventSpy = jasmine.createSpyObj('event', ['stopPropagation']); component.onClickStopPropagation(eventSpy); expect(eventSpy.stopPropagation).toHaveBeenCalled(); }); - it('should return exploration title position given index ' + - 'when calling \'getExplorationTitlePosition\'', () => { - // Case 1. - let result = component.getExplorationTitlePosition(2); - expect(result).toBe('-13px'); - - // Case 2. - result = component.getExplorationTitlePosition(1); - expect(result).toBe('40px'); - - // Case 3. - result = component.getExplorationTitlePosition(3); - expect(result).toBe('-55px'); - }); + it( + 'should return exploration title position given index ' + + "when calling 'getExplorationTitlePosition'", + () => { + // Case 1. + let result = component.getExplorationTitlePosition(2); + expect(result).toBe('-13px'); + + // Case 2. + result = component.getExplorationTitlePosition(1); + expect(result).toBe('40px'); + + // Case 3. + result = component.getExplorationTitlePosition(3); + expect(result).toBe('-55px'); + } + ); it('should return exploration url given exploration id', () => { component.collectionId = 'colId1'; @@ -303,10 +341,13 @@ describe('Collection player page component', () => { }); it('should generate path icon parameters', fakeAsync(() => { - spyOn(readOnlyCollectionBackendApiService, 'loadCollectionAsync') - .and.resolveTo(sampleCollection); - spyOn(userService, 'getUserInfoAsync') - .and.returnValue(Promise.resolve(userInfoForCollectionCreator)); + spyOn( + readOnlyCollectionBackendApiService, + 'loadCollectionAsync' + ).and.resolveTo(sampleCollection); + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve(userInfoForCollectionCreator) + ); // Loading collections. component.ngOnInit(); @@ -316,130 +357,160 @@ describe('Collection player page component', () => { expect(pathIconParameters).toEqual(collectionNodesList); })); - it('should check whether the exploration is completed when ' + + it( + 'should check whether the exploration is completed when ' + 'collection playthrough is available', - fakeAsync(() => { - spyOn(readOnlyCollectionBackendApiService, 'loadCollectionAsync') - .and.resolveTo(sampleCollection); - spyOn(userService, 'getUserInfoAsync') - .and.returnValue(Promise.resolve(userInfoForCollectionCreator)); - - // Loading collections. - component.ngOnInit(); - tick(); - let res = component.isCompletedExploration('123'); + fakeAsync(() => { + spyOn( + readOnlyCollectionBackendApiService, + 'loadCollectionAsync' + ).and.resolveTo(sampleCollection); + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve(userInfoForCollectionCreator) + ); + + // Loading collections. + component.ngOnInit(); + tick(); + let res = component.isCompletedExploration('123'); - expect(res).toBeFalse(); - })); + expect(res).toBeFalse(); + }) + ); - it('should return false on checking whether exploration is completed ' + + it( + 'should return false on checking whether exploration is completed ' + 'when collection playthrough is not available', - fakeAsync(() => { - spyOn(readOnlyCollectionBackendApiService, 'loadCollectionAsync') - .and.resolveTo(sampleCollection); - spyOn(userService, 'getUserInfoAsync') - .and.returnValue(Promise.resolve(userInfoForCollectionCreator)); - - // Loading collections. - component.ngOnInit(); - tick(); - - // This happens when collection is not loaded. - component.collectionPlaythrough = undefined; - let res = component.isCompletedExploration('123'); - - expect(res).toBeFalse(); - })); - - it('should generate empty path parameters when collection ' + - 'node count is one', fakeAsync(() => { - sampleCollectionBackendObject.nodes = [collectionNodeBackendObject]; - sampleCollection = Collection.create( - sampleCollectionBackendObject); - spyOn(readOnlyCollectionBackendApiService, 'loadCollectionAsync') - .and.resolveTo(sampleCollection); - spyOn(userService, 'getUserInfoAsync') - .and.returnValue(Promise.resolve(userInfoForCollectionCreator)); - - // Loading collections. - component.ngOnInit(); - tick(); - component.generatePathParameters(); + fakeAsync(() => { + spyOn( + readOnlyCollectionBackendApiService, + 'loadCollectionAsync' + ).and.resolveTo(sampleCollection); + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve(userInfoForCollectionCreator) + ); + + // Loading collections. + component.ngOnInit(); + tick(); - expect(component.pathSvgParameters).toBe(''); - })); + // This happens when collection is not loaded. + component.collectionPlaythrough = undefined; + let res = component.isCompletedExploration('123'); - it('should generate path parameters when collection ' + - 'node count is two', fakeAsync(() => { - sampleCollectionBackendObject.nodes = [ - collectionNodeBackendObject, - collectionNodeBackendObject - ]; - sampleCollection = Collection.create( - sampleCollectionBackendObject); - spyOn(readOnlyCollectionBackendApiService, 'loadCollectionAsync') - .and.resolveTo(sampleCollection); - spyOn(userService, 'getUserInfoAsync') - .and.returnValue(Promise.resolve(userInfoForCollectionCreator)); + expect(res).toBeFalse(); + }) + ); - // Loading collections. - component.ngOnInit(); - tick(); - component.generatePathParameters(); + it( + 'should generate empty path parameters when collection ' + + 'node count is one', + fakeAsync(() => { + sampleCollectionBackendObject.nodes = [collectionNodeBackendObject]; + sampleCollection = Collection.create(sampleCollectionBackendObject); + spyOn( + readOnlyCollectionBackendApiService, + 'loadCollectionAsync' + ).and.resolveTo(sampleCollection); + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve(userInfoForCollectionCreator) + ); + + // Loading collections. + component.ngOnInit(); + tick(); + component.generatePathParameters(); - expect(component.pathSvgParameters).toBe( - 'M250 80 C 470 100, 470 280, 250 300'); - })); + expect(component.pathSvgParameters).toBe(''); + }) + ); - it('should generate path parameters when collection ' + - 'node count is three', fakeAsync(() => { - sampleCollectionBackendObject.nodes = [ - collectionNodeBackendObject, - collectionNodeBackendObject, - collectionNodeBackendObject - ]; - sampleCollection = Collection.create( - sampleCollectionBackendObject); - spyOn(readOnlyCollectionBackendApiService, 'loadCollectionAsync') - .and.resolveTo(sampleCollection); + it( + 'should generate path parameters when collection ' + 'node count is two', + fakeAsync(() => { + sampleCollectionBackendObject.nodes = [ + collectionNodeBackendObject, + collectionNodeBackendObject, + ]; + sampleCollection = Collection.create(sampleCollectionBackendObject); + spyOn( + readOnlyCollectionBackendApiService, + 'loadCollectionAsync' + ).and.resolveTo(sampleCollection); + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve(userInfoForCollectionCreator) + ); + + // Loading collections. + component.ngOnInit(); + tick(); + component.generatePathParameters(); - // Loading collections. - component.ngOnInit(); - tick(); - component.generatePathParameters(); + expect(component.pathSvgParameters).toBe( + 'M250 80 C 470 100, 470 280, 250 300' + ); + }) + ); - expect(component.pathSvgParameters).toBe( - 'M250 80 C 470 100, 470 280, 250 300'); - })); + it( + 'should generate path parameters when collection ' + 'node count is three', + fakeAsync(() => { + sampleCollectionBackendObject.nodes = [ + collectionNodeBackendObject, + collectionNodeBackendObject, + collectionNodeBackendObject, + ]; + sampleCollection = Collection.create(sampleCollectionBackendObject); + spyOn( + readOnlyCollectionBackendApiService, + 'loadCollectionAsync' + ).and.resolveTo(sampleCollection); + + // Loading collections. + component.ngOnInit(); + tick(); + component.generatePathParameters(); - it('should generate path parameters when collection ' + - 'node count is four', fakeAsync(() => { - sampleCollectionBackendObject.nodes = [ - collectionNodeBackendObject, - collectionNodeBackendObject, - collectionNodeBackendObject, - collectionNodeBackendObject - ]; - sampleCollection = Collection.create( - sampleCollectionBackendObject); - spyOn(readOnlyCollectionBackendApiService, 'loadCollectionAsync') - .and.resolveTo(sampleCollection); - spyOn(userService, 'getUserInfoAsync') - .and.returnValue(Promise.resolve(userInfoForCollectionCreator)); + expect(component.pathSvgParameters).toBe( + 'M250 80 C 470 100, 470 280, 250 300' + ); + }) + ); - // Loading collections. - component.ngOnInit(); - tick(); - component.generatePathParameters(); + it( + 'should generate path parameters when collection ' + 'node count is four', + fakeAsync(() => { + sampleCollectionBackendObject.nodes = [ + collectionNodeBackendObject, + collectionNodeBackendObject, + collectionNodeBackendObject, + collectionNodeBackendObject, + ]; + sampleCollection = Collection.create(sampleCollectionBackendObject); + spyOn( + readOnlyCollectionBackendApiService, + 'loadCollectionAsync' + ).and.resolveTo(sampleCollection); + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve(userInfoForCollectionCreator) + ); + + // Loading collections. + component.ngOnInit(); + tick(); + component.generatePathParameters(); - expect(component.pathSvgParameters).toBe( - 'M250 80 C 470 100, 470 280, 250 300 S 30 500, 250 520, '); - })); + expect(component.pathSvgParameters).toBe( + 'M250 80 C 470 100, 470 280, 250 300 S 30 500, 250 520, ' + ); + }) + ); it('should return static image url given image path', () => { let urlInterpolationSpy = spyOn( - urlInterpolationService, 'getStaticImageUrl') - .and.returnValue('imageUrl'); + urlInterpolationService, + 'getStaticImageUrl' + ).and.returnValue('imageUrl'); let url = component.getStaticImageUrl('/imagepath'); @@ -447,8 +518,7 @@ describe('Collection player page component', () => { expect(url).toBe('imageUrl'); }); - it('should toggle preview card when calling ' + - '\'togglePreviewCard\'', () => { + it('should toggle preview card when calling ' + "'togglePreviewCard'", () => { component.explorationCardIsShown = false; component.togglePreviewCard(); @@ -466,31 +536,39 @@ describe('Collection player page component', () => { expect(result).toEqual(sampleCollection.nodes[0]); }); - it('should update exploration preview card when calling ' + - '\'updateExplorationPreview\'', () => { - component.explorationCardIsShown = false; + it( + 'should update exploration preview card when calling ' + + "'updateExplorationPreview'", + () => { + component.explorationCardIsShown = false; - component.collection = sampleCollection; - component.updateExplorationPreview('exp_id'); + component.collection = sampleCollection; + component.updateExplorationPreview('exp_id'); - expect(component.explorationCardIsShown).toBe(true); - expect(component.currentExplorationId).toBe('exp_id'); - }); + expect(component.explorationCardIsShown).toBe(true); + expect(component.currentExplorationId).toBe('exp_id'); + } + ); - it('should show warning message if we try to ' + - 'load a collection with invalid id', () => { - component.collection = sampleCollection; + it( + 'should show warning message if we try to ' + + 'load a collection with invalid id', + () => { + component.collection = sampleCollection; - component.getCollectionNodeForExplorationId('invalidId'); + component.getCollectionNodeForExplorationId('invalidId'); - expect(alertsSpy).toHaveBeenCalledWith( - 'There was an error loading the collection.'); - }); + expect(alertsSpy).toHaveBeenCalledWith( + 'There was an error loading the collection.' + ); + } + ); it('should return next recommended collection node', fakeAsync(() => { component.collection = sampleCollection; - component.collectionPlaythrough = ( - CollectionPlaythrough.create('exp_id', ['exp_id0'])); + component.collectionPlaythrough = CollectionPlaythrough.create('exp_id', [ + 'exp_id0', + ]); let result = component.getNextRecommendedCollectionNodes(); @@ -499,70 +577,96 @@ describe('Collection player page component', () => { it('should return completed collection node', fakeAsync(() => { component.collection = sampleCollection; - component.collectionPlaythrough = ( - CollectionPlaythrough.create('exp_id0', ['exp_id'])); + component.collectionPlaythrough = CollectionPlaythrough.create('exp_id0', [ + 'exp_id', + ]); let result = component.getCompletedExplorationNodes(); expect(result).toEqual(sampleCollection.nodes[0]); })); - it('should return non recommended collection node ' + - 'count', fakeAsync(() => { - spyOn(readOnlyCollectionBackendApiService, 'loadCollectionAsync') - .and.resolveTo(sampleCollection); - spyOn(userService, 'getUserInfoAsync') - .and.returnValue(Promise.resolve(new UserInfo( - ['USER_ROLE'], true, false, false, false, true, - 'en', 'username1', 'tester@example.com', false - ))); - spyOn(guestCollectionProgressService, 'hasCompletedSomeExploration') - .and.returnValue(true); - - // Loading collections. - component.ngOnInit(); - tick(); + it( + 'should return non recommended collection node ' + 'count', + fakeAsync(() => { + spyOn( + readOnlyCollectionBackendApiService, + 'loadCollectionAsync' + ).and.resolveTo(sampleCollection); + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve( + new UserInfo( + ['USER_ROLE'], + true, + false, + false, + false, + true, + 'en', + 'username1', + 'tester@example.com', + false + ) + ) + ); + spyOn( + guestCollectionProgressService, + 'hasCompletedSomeExploration' + ).and.returnValue(true); + + // Loading collections. + component.ngOnInit(); + tick(); - let result = component.getNonRecommendedCollectionNodeCount(); - expect(result).toEqual(4); - })); + let result = component.getNonRecommendedCollectionNodeCount(); + expect(result).toEqual(4); + }) + ); - it('should close the exploration card and scroll into the' + + it( + 'should close the exploration card and scroll into the' + 'exploration icon location on clicking outside of the exploration card', - fakeAsync(() => { - spyOn(readOnlyCollectionBackendApiService, 'loadCollectionAsync') - .and.resolveTo(sampleCollection); - spyOn(userService, 'getUserInfoAsync') - .and.returnValue(Promise.resolve(userInfoForCollectionCreator)); - spyOn(component, 'scrollToLocation').and.callThrough(); - spyOn(component, 'closeOnClickingOutside').and.callThrough(); - spyOn(component, 'updateExplorationPreview').and.callThrough(); - - fixture.detectChanges(); - component.ngOnInit(); - tick(); - fixture.detectChanges(); - - // Opening the preview card. - let icons = fixture.nativeElement.querySelectorAll( - '.e2e-mobile-test-collection-exploration'); - let icon = icons[0]; - icon.dispatchEvent(new Event('click')); - - expect(component.scrollToLocation).toHaveBeenCalledWith( - 'mobile-path-anchor-0'); - expect(component.updateExplorationPreview).toHaveBeenCalledWith('exp_id'); - expect(component.explorationCardIsShown).toBeTrue(); - - fixture.detectChanges(); - - // Clicking outside the preview card. - let mask = fixture.nativeElement.querySelector( - '.oppia-activity-summary-tile-mobile-background-mask'); - mask.dispatchEvent(new Event('click')); - - expect(component.closeOnClickingOutside).toHaveBeenCalled(); - expect(component.scrollToLocation).toHaveBeenCalled(); - expect(component.explorationCardIsShown).toBeFalse(); - })); + fakeAsync(() => { + spyOn( + readOnlyCollectionBackendApiService, + 'loadCollectionAsync' + ).and.resolveTo(sampleCollection); + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve(userInfoForCollectionCreator) + ); + spyOn(component, 'scrollToLocation').and.callThrough(); + spyOn(component, 'closeOnClickingOutside').and.callThrough(); + spyOn(component, 'updateExplorationPreview').and.callThrough(); + + fixture.detectChanges(); + component.ngOnInit(); + tick(); + fixture.detectChanges(); + + // Opening the preview card. + let icons = fixture.nativeElement.querySelectorAll( + '.e2e-mobile-test-collection-exploration' + ); + let icon = icons[0]; + icon.dispatchEvent(new Event('click')); + + expect(component.scrollToLocation).toHaveBeenCalledWith( + 'mobile-path-anchor-0' + ); + expect(component.updateExplorationPreview).toHaveBeenCalledWith('exp_id'); + expect(component.explorationCardIsShown).toBeTrue(); + + fixture.detectChanges(); + + // Clicking outside the preview card. + let mask = fixture.nativeElement.querySelector( + '.oppia-activity-summary-tile-mobile-background-mask' + ); + mask.dispatchEvent(new Event('click')); + + expect(component.closeOnClickingOutside).toHaveBeenCalled(); + expect(component.scrollToLocation).toHaveBeenCalled(); + expect(component.explorationCardIsShown).toBeFalse(); + }) + ); }); diff --git a/core/templates/pages/collection-player-page/collection-player-page.component.ts b/core/templates/pages/collection-player-page/collection-player-page.component.ts index e9c1101deb62..2f0a8870a906 100644 --- a/core/templates/pages/collection-player-page/collection-player-page.component.ts +++ b/core/templates/pages/collection-player-page/collection-player-page.component.ts @@ -16,28 +16,27 @@ * @fileoverview Component for the learner's view of a collection. */ -import { Component, OnInit, OnDestroy } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; - -import { GuestCollectionProgressService } from 'domain/collection/guest-collection-progress.service'; -import { ReadOnlyCollectionBackendApiService } from 'domain/collection/read-only-collection-backend-api.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { AlertsService } from 'services/alerts.service'; -import { UrlService } from 'services/contextual/url.service'; -import { LoaderService } from 'services/loader.service'; -import { PageTitleService } from 'services/page-title.service'; -import { UserService } from 'services/user.service'; -import { CollectionNode } from 'domain/collection/collection-node.model'; -import { AppConstants } from 'app.constants'; -import { Collection } from 'domain/collection/collection.model'; -import { CollectionPlayerBackendApiService } from './services/collection-player-backend-api.service'; -import { LearnerExplorationSummaryBackendDict } from 'domain/summary/learner-exploration-summary.model'; +import {Component, OnInit, OnDestroy} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; + +import {GuestCollectionProgressService} from 'domain/collection/guest-collection-progress.service'; +import {ReadOnlyCollectionBackendApiService} from 'domain/collection/read-only-collection-backend-api.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {AlertsService} from 'services/alerts.service'; +import {UrlService} from 'services/contextual/url.service'; +import {LoaderService} from 'services/loader.service'; +import {PageTitleService} from 'services/page-title.service'; +import {UserService} from 'services/user.service'; +import {CollectionNode} from 'domain/collection/collection-node.model'; +import {AppConstants} from 'app.constants'; +import {Collection} from 'domain/collection/collection.model'; +import {CollectionPlayerBackendApiService} from './services/collection-player-backend-api.service'; +import {LearnerExplorationSummaryBackendDict} from 'domain/summary/learner-exploration-summary.model'; import './collection-player-page.component.css'; - export interface IconParametersArray { thumbnailIconUrl: string; left: string; @@ -46,32 +45,32 @@ export interface IconParametersArray { } export interface CollectionSummary { - 'is_admin': boolean; - 'summaries': string[]; - 'user_email': string; - 'is_topic_manager': boolean; - 'username': boolean; + is_admin: boolean; + summaries: string[]; + user_email: string; + is_topic_manager: boolean; + username: boolean; } export interface CollectionHandler { - 'can_edit': boolean; - 'collection': Collection; - 'is_admin': boolean; - 'is_logged_in': boolean; - 'is_moderator': boolean; - 'is_super_admin': boolean; - 'is_topic_manager': boolean; - 'meta_description': string; - 'meta_name': string; - 'session_id': string; - 'user_email': string; - 'username': string; + can_edit: boolean; + collection: Collection; + is_admin: boolean; + is_logged_in: boolean; + is_moderator: boolean; + is_super_admin: boolean; + is_topic_manager: boolean; + meta_description: string; + meta_name: string; + session_id: string; + user_email: string; + username: string; } @Component({ selector: 'oppia-collection-player-page', templateUrl: './collection-player-page.component.html', - styleUrls: ['./collection-player-page.component.css'] + styleUrls: ['./collection-player-page.component.css'], }) export class CollectionPlayerPageComponent implements OnInit, OnDestroy { directiveSubscriptions = new Subscription(); @@ -105,12 +104,10 @@ export class CollectionPlayerPageComponent implements OnInit, OnDestroy { private alertsService: AlertsService, private loaderService: LoaderService, private urlService: UrlService, - private readOnlyCollectionBackendApiService: - ReadOnlyCollectionBackendApiService, + private readOnlyCollectionBackendApiService: ReadOnlyCollectionBackendApiService, private pageTitleService: PageTitleService, private userService: UserService, - private collectionPlayerBackendApiService: - CollectionPlayerBackendApiService, + private collectionPlayerBackendApiService: CollectionPlayerBackendApiService, private translateService: TranslateService ) {} @@ -123,37 +120,43 @@ export class CollectionPlayerPageComponent implements OnInit, OnDestroy { } getCollectionNodeForExplorationId(explorationId: string): CollectionNode { - let collectionNode = ( - this.collection.getCollectionNodeByExplorationId(explorationId)); + let collectionNode = + this.collection.getCollectionNodeByExplorationId(explorationId); if (!collectionNode) { this.alertsService.addWarning( - 'There was an error loading the collection.'); + 'There was an error loading the collection.' + ); } return collectionNode; } getNextRecommendedCollectionNodes(): CollectionNode { return this.getCollectionNodeForExplorationId( - this.collectionPlaythrough.getNextExplorationId()); + this.collectionPlaythrough.getNextExplorationId() + ); } getCompletedExplorationNodes(): CollectionNode { return this.getCollectionNodeForExplorationId( - this.collectionPlaythrough.getCompletedExplorationIds()); + this.collectionPlaythrough.getCompletedExplorationIds() + ); } getNonRecommendedCollectionNodeCount(): number { - return this.collection.getCollectionNodeCount() - ( - this.collectionPlaythrough.getNextRecommendedCollectionNodeCount() + - this.collectionPlaythrough.getCompletedExplorationNodeCount() + return ( + this.collection.getCollectionNodeCount() - + (this.collectionPlaythrough.getNextRecommendedCollectionNodeCount() + + this.collectionPlaythrough.getCompletedExplorationNodeCount()) ); } updateExplorationPreview(explorationId: string): void { this.explorationCardIsShown = true; this.currentExplorationId = explorationId; - this.summaryToPreview = this.getCollectionNodeForExplorationId( - explorationId).getExplorationSummaryObject(); + this.summaryToPreview = + this.getCollectionNodeForExplorationId( + explorationId + ).getExplorationSummaryObject(); } // Calculates the SVG parameters required to draw the curved path. @@ -176,7 +179,7 @@ export class CollectionPlayerPageComponent implements OnInit, OnDestroy { // points for the bezier curve (path). this.y = 500; for (let i = 1; i < Math.floor(collectionNodeCount / 2); i++) { - let x = (i % 2) ? 30 : 470; + let x = i % 2 ? 30 : 470; sParameterExtension += x + ' ' + this.y + ', '; this.y += 20; sParameterExtension += 250 + ' ' + this.y + ', '; @@ -205,14 +208,13 @@ export class CollectionPlayerPageComponent implements OnInit, OnDestroy { let collectionNodes = this.collection.getCollectionNodes(); let iconParametersArray = []; iconParametersArray.push({ - thumbnailIconUrl: - collectionNodes[0].getExplorationSummaryObject( - ).thumbnail_icon_url.replace('subjects', 'inverted_subjects'), + thumbnailIconUrl: collectionNodes[0] + .getExplorationSummaryObject() + .thumbnail_icon_url.replace('subjects', 'inverted_subjects'), left: '225px', top: '35px', thumbnailBgColor: - collectionNodes[0].getExplorationSummaryObject( - ).thumbnail_bg_color + collectionNodes[0].getExplorationSummaryObject().thumbnail_bg_color, }); // Here x and y represent the co-ordinates for the icons in the @@ -235,24 +237,20 @@ export class CollectionPlayerPageComponent implements OnInit, OnDestroy { y += this.ICON_Y_INCREMENT_PX; } iconParametersArray.push({ - thumbnailIconUrl: - collectionNodes[i].getExplorationSummaryObject( - ).thumbnail_icon_url.replace( - 'subjects', 'inverted_subjects'), + thumbnailIconUrl: collectionNodes[i] + .getExplorationSummaryObject() + .thumbnail_icon_url.replace('subjects', 'inverted_subjects'), left: x + 'px', top: y + 'px', thumbnailBgColor: - collectionNodes[i].getExplorationSummaryObject( - ).thumbnail_bg_color + collectionNodes[i].getExplorationSummaryObject().thumbnail_bg_color, }); } return iconParametersArray; } getExplorationUrl(explorationId: string): string { - return ( - '/explore/' + explorationId + '?collection_id=' + - this.collectionId); + return '/explore/' + explorationId + '?collection_id=' + this.collectionId; } getExplorationTitlePosition(index: number): string { @@ -282,8 +280,8 @@ export class CollectionPlayerPageComponent implements OnInit, OnDestroy { isCompletedExploration(explorationId: string): boolean { if (this.collectionPlaythrough) { - let completedExplorationIds = ( - this.collectionPlaythrough.getCompletedExplorationIds()); + let completedExplorationIds = + this.collectionPlaythrough.getCompletedExplorationIds(); return completedExplorationIds.indexOf(explorationId) > -1; } return false; @@ -291,22 +289,19 @@ export class CollectionPlayerPageComponent implements OnInit, OnDestroy { async fetchSummaryAsync(collectionId: string): Promise { let summary = null; - this.collectionPlayerBackendApiService.fetchCollectionSummariesAsync( - collectionId - ).then((collectionSummary) => { - summary = collectionSummary; - if (summary) { - this.collectionSummary = summary.summaries[0]; - } - }); + this.collectionPlayerBackendApiService + .fetchCollectionSummariesAsync(collectionId) + .then(collectionSummary => { + summary = collectionSummary; + if (summary) { + this.collectionSummary = summary.summaries[0]; + } + }); } updateCollection(collection: Collection): void { this.collection = collection; - if ( - this.collection !== null && - this.collection.getCollectionNodeCount() - ) { + if (this.collection !== null && this.collection.getCollectionNodeCount()) { this.generatePathParameters(); } } @@ -321,9 +316,11 @@ export class CollectionPlayerPageComponent implements OnInit, OnDestroy { setPageTitle(): void { let translatedTitle = this.translateService.instant( - 'I18N_COLLECTION_PLAYER_PAGE_TITLE', { - collectionTitle: this.collection.getTitle() - }); + 'I18N_COLLECTION_PLAYER_PAGE_TITLE', + { + collectionTitle: this.collection.getTitle(), + } + ); this.pageTitleService.setDocumentTitle(translatedTitle); } @@ -346,43 +343,45 @@ export class CollectionPlayerPageComponent implements OnInit, OnDestroy { this.ICON_X_RIGHT_PX = 390; this.svgHeight = this.MIN_HEIGHT_FOR_PATH_SVG_PX; this.nextExplorationId = null; - this.allowedCollectionIdsForGuestProgress = ( - AppConstants.ALLOWED_COLLECTION_IDS_FOR_SAVING_GUEST_PROGRESS); + this.allowedCollectionIdsForGuestProgress = + AppConstants.ALLOWED_COLLECTION_IDS_FOR_SAVING_GUEST_PROGRESS; this.fetchSummaryAsync(this.collectionId); // Load the collection the learner wants to view. - this.readOnlyCollectionBackendApiService.loadCollectionAsync( - this.collectionId).then( - (collection) => { - this.updateCollection(collection); - // The onLangChange event is initially fired before the collection is - // loaded. Hence the first setpageTitle() call needs to made - // manually, and the onLangChange subscription is added after - // the collection is loaded. - this.setPageTitle(); - this.subscribeToOnLangChange(); - - // Load the user's current progress in the collection. If the - // user is a guest, then either the defaults from the server - // will be used or the user's local progress, if any has been - // made and the collection is whitelisted. - this.userService.getUserInfoAsync().then((userInfo) => { - this.loaderService.hideLoadingScreen(); - this.isLoggedIn = userInfo.isLoggedIn(); - this.collectionPlaythrough = collection.getPlaythrough(); - this.nextExplorationId = - this.collectionPlaythrough.getNextExplorationId(); - }); - }, - () => { - // NOTE TO DEVELOPERS: Check the backend console for an - // indication as to why this error occurred; sometimes the - // errors are noisy, so they are not shown to the user. - this.alertsService.addWarning( - 'There was an error loading the collection.'); - } - ); + this.readOnlyCollectionBackendApiService + .loadCollectionAsync(this.collectionId) + .then( + collection => { + this.updateCollection(collection); + // The onLangChange event is initially fired before the collection is + // loaded. Hence the first setpageTitle() call needs to made + // manually, and the onLangChange subscription is added after + // the collection is loaded. + this.setPageTitle(); + this.subscribeToOnLangChange(); + + // Load the user's current progress in the collection. If the + // user is a guest, then either the defaults from the server + // will be used or the user's local progress, if any has been + // made and the collection is whitelisted. + this.userService.getUserInfoAsync().then(userInfo => { + this.loaderService.hideLoadingScreen(); + this.isLoggedIn = userInfo.isLoggedIn(); + this.collectionPlaythrough = collection.getPlaythrough(); + this.nextExplorationId = + this.collectionPlaythrough.getNextExplorationId(); + }); + }, + () => { + // NOTE TO DEVELOPERS: Check the backend console for an + // indication as to why this error occurred; sometimes the + // errors are noisy, so they are not shown to the user. + this.alertsService.addWarning( + 'There was an error loading the collection.' + ); + } + ); } ngOnDestroy(): void { @@ -390,7 +389,9 @@ export class CollectionPlayerPageComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive('oppiaCollectionPlayerPage', +angular.module('oppia').directive( + 'oppiaCollectionPlayerPage', downgradeComponent({ - component: CollectionPlayerPageComponent - }) as angular.IDirectiveFactory); + component: CollectionPlayerPageComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/collection-player-page/collection-player-page.import.ts b/core/templates/pages/collection-player-page/collection-player-page.import.ts index f69878f9503f..089c9bb9aa5b 100644 --- a/core/templates/pages/collection-player-page/collection-player-page.import.ts +++ b/core/templates/pages/collection-player-page/collection-player-page.import.ts @@ -22,9 +22,14 @@ import 'zone.js'; import uiValidate from 'angular-ui-validate'; angular.module('oppia', [ - require('angular-cookies'), 'ngAnimate', - 'ngMaterial', 'ngSanitize', 'ngTouch', 'pascalprecht.translate', - 'ui.bootstrap', uiValidate + require('angular-cookies'), + 'ngAnimate', + 'ngMaterial', + 'ngSanitize', + 'ngTouch', + 'pascalprecht.translate', + 'ui.bootstrap', + uiValidate, ]); require('Polyfills.ts'); @@ -38,11 +43,14 @@ require('base-components/oppia-root.directive.ts'); require('base-components/base-content.component.ts'); require( 'pages/collection-player-page/collection-footer/' + - 'collection-footer.component.ts'); + 'collection-footer.component.ts' +); require( 'pages/collection-player-page/collection-local-nav/' + - 'collection-local-nav.component.ts'); + 'collection-local-nav.component.ts' +); require( 'pages/collection-player-page/collection-navbar/' + - 'collection-navbar.component.ts'); + 'collection-navbar.component.ts' +); require('pages/collection-player-page/collection-player-page.component.ts'); diff --git a/core/templates/pages/collection-player-page/collection-player-page.module.ts b/core/templates/pages/collection-player-page/collection-player-page.module.ts index 56f8569c433a..fe447bf9ff96 100644 --- a/core/templates/pages/collection-player-page/collection-player-page.module.ts +++ b/core/templates/pages/collection-player-page/collection-player-page.module.ts @@ -16,27 +16,30 @@ * @fileoverview Module for the collection player page. */ -import { APP_INITIALIZER, NgModule, StaticProvider } from '@angular/core'; -import { BrowserModule, HAMMER_GESTURE_CONFIG } from '@angular/platform-browser'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { HttpClientModule } from '@angular/common/http'; -import { HTTP_INTERCEPTORS } from '@angular/common/http'; -import { RouterModule } from '@angular/router'; -import { APP_BASE_HREF } from '@angular/common'; +import {APP_INITIALIZER, NgModule, StaticProvider} from '@angular/core'; +import {BrowserModule, HAMMER_GESTURE_CONFIG} from '@angular/platform-browser'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {HttpClientModule} from '@angular/common/http'; +import {HTTP_INTERCEPTORS} from '@angular/common/http'; +import {RouterModule} from '@angular/router'; +import {APP_BASE_HREF} from '@angular/common'; -import { RequestInterceptor } from 'services/request-interceptor.service'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { OppiaAngularRootComponent } from 'components/oppia-angular-root.component'; -import { CollectionFooterComponent } from './collection-footer/collection-footer.component'; -import { CollectionLocalNavComponent } from './collection-local-nav/collection-local-nav.component'; -import { CollectionNavbarComponent } from './collection-navbar/collection-navbar.component'; -import { CollectionNodeListComponent } from './collection-node-list/collection-node-list.component'; -import { CollectionPlayerPageComponent } from './collection-player-page.component'; -import { platformFeatureInitFactory, PlatformFeatureService } from 'services/platform-feature.service'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { MyHammerConfig, toastrConfig } from 'pages/oppia-root/app.module'; -import { SmartRouterModule } from 'hybrid-router-module-provider'; -import { AppErrorHandlerProvider } from 'pages/oppia-root/app-error-handler'; +import {RequestInterceptor} from 'services/request-interceptor.service'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {OppiaAngularRootComponent} from 'components/oppia-angular-root.component'; +import {CollectionFooterComponent} from './collection-footer/collection-footer.component'; +import {CollectionLocalNavComponent} from './collection-local-nav/collection-local-nav.component'; +import {CollectionNavbarComponent} from './collection-navbar/collection-navbar.component'; +import {CollectionNodeListComponent} from './collection-node-list/collection-node-list.component'; +import {CollectionPlayerPageComponent} from './collection-player-page.component'; +import { + platformFeatureInitFactory, + PlatformFeatureService, +} from 'services/platform-feature.service'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {MyHammerConfig, toastrConfig} from 'pages/oppia-root/app.module'; +import {SmartRouterModule} from 'hybrid-router-module-provider'; +import {AppErrorHandlerProvider} from 'pages/oppia-root/app-error-handler'; @NgModule({ imports: [ @@ -48,7 +51,7 @@ import { AppErrorHandlerProvider } from 'pages/oppia-root/app-error-handler'; SmartRouterModule, RouterModule.forRoot([]), SharedComponentsModule, - ToastrModule.forRoot(toastrConfig) + ToastrModule.forRoot(toastrConfig), ], declarations: [ CollectionFooterComponent, @@ -68,35 +71,35 @@ import { AppErrorHandlerProvider } from 'pages/oppia-root/app-error-handler'; { provide: HTTP_INTERCEPTORS, useClass: RequestInterceptor, - multi: true + multi: true, }, { provide: APP_INITIALIZER, useFactory: platformFeatureInitFactory, deps: [PlatformFeatureService], - multi: true + multi: true, }, { provide: HAMMER_GESTURE_CONFIG, - useClass: MyHammerConfig + useClass: MyHammerConfig, }, AppErrorHandlerProvider, { provide: APP_BASE_HREF, - useValue: '/' - } - ] + useValue: '/', + }, + ], }) class CollectionPlayerPageModule { // Empty placeholder method to satisfy the `Compiler`. ngDoBootstrap() {} } -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { downgradeModule } from '@angular/upgrade/static'; -import { ToastrModule } from 'ngx-toastr'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {downgradeModule} from '@angular/upgrade/static'; +import {ToastrModule} from 'ngx-toastr'; -const bootstrapFnAsync = async(extraProviders: StaticProvider[]) => { +const bootstrapFnAsync = async (extraProviders: StaticProvider[]) => { const platformRef = platformBrowserDynamic(extraProviders); return platformRef.bootstrapModule(CollectionPlayerPageModule); }; @@ -111,5 +114,6 @@ angular.module('oppia').directive( // bootstrap the Angular 8. 'oppiaAngularRoot', downgradeComponent({ - component: OppiaAngularRootComponent - }) as angular.IDirectiveFactory); + component: OppiaAngularRootComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/collection-player-page/services/collection-player-backend-api.service.spec.ts b/core/templates/pages/collection-player-page/services/collection-player-backend-api.service.spec.ts index 6377de3d4cd2..0555d6dc8af1 100644 --- a/core/templates/pages/collection-player-page/services/collection-player-backend-api.service.spec.ts +++ b/core/templates/pages/collection-player-page/services/collection-player-backend-api.service.spec.ts @@ -16,15 +16,26 @@ * @fileoverview Unit Tests for CollectionPlayerBackendApiService. */ -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { fakeAsync, flushMicrotasks, TestBed, waitForAsync } from '@angular/core/testing'; -import { CollectionNodeBackendDict } from 'domain/collection/collection-node.model'; -import { Collection, CollectionBackendDict } from 'domain/collection/collection.model'; -import { GuestCollectionProgressService } from 'domain/collection/guest-collection-progress.service'; -import { ReadOnlyCollectionBackendApiService } from 'domain/collection/read-only-collection-backend-api.service'; -import { UserInfo } from 'domain/user/user-info.model'; -import { UserService } from 'services/user.service'; -import { CollectionPlayerBackendApiService } from './collection-player-backend-api.service'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import { + fakeAsync, + flushMicrotasks, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import {CollectionNodeBackendDict} from 'domain/collection/collection-node.model'; +import { + Collection, + CollectionBackendDict, +} from 'domain/collection/collection.model'; +import {GuestCollectionProgressService} from 'domain/collection/guest-collection-progress.service'; +import {ReadOnlyCollectionBackendApiService} from 'domain/collection/read-only-collection-backend-api.service'; +import {UserInfo} from 'domain/user/user-info.model'; +import {UserService} from 'services/user.service'; +import {CollectionPlayerBackendApiService} from './collection-player-backend-api.service'; describe('Collection Player Backend Api Service', () => { let cpbas: CollectionPlayerBackendApiService; @@ -32,26 +43,35 @@ describe('Collection Player Backend Api Service', () => { let sampleCollection: Collection; let sampleCollectionBackendObject: CollectionBackendDict; let collectionNodeBackendObject: CollectionNodeBackendDict; - let readOnlyCollectionBackendApiService: - ReadOnlyCollectionBackendApiService; + let readOnlyCollectionBackendApiService: ReadOnlyCollectionBackendApiService; let userService: UserService; let guestCollectionProgressService: GuestCollectionProgressService; const userInfoForCollectionCreator = new UserInfo( - ['USER_ROLE'], true, false, false, false, true, - 'en', 'username1', 'tester@example.com', true + ['USER_ROLE'], + true, + false, + false, + false, + true, + 'en', + 'username1', + 'tester@example.com', + true ); beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] + imports: [HttpClientTestingModule], }); httpTestingController = TestBed.inject(HttpTestingController); cpbas = TestBed.inject(CollectionPlayerBackendApiService); - readOnlyCollectionBackendApiService = - TestBed.inject(ReadOnlyCollectionBackendApiService); + readOnlyCollectionBackendApiService = TestBed.inject( + ReadOnlyCollectionBackendApiService + ); guestCollectionProgressService = TestBed.inject( - GuestCollectionProgressService); + GuestCollectionProgressService + ); userService = TestBed.inject(UserService); collectionNodeBackendObject = { @@ -72,14 +92,14 @@ describe('Collection Player Backend Api Service', () => { 2: 0, 3: 0, 4: 0, - 5: 0 + 5: 0, }, status: 'public', tags: [], activity_type: 'exploration', category: 'Algebra', - title: 'Test Title' - } + title: 'Test Title', + }, }; sampleCollectionBackendObject = { @@ -94,21 +114,23 @@ describe('Collection Player Backend Api Service', () => { collectionNodeBackendObject, collectionNodeBackendObject, collectionNodeBackendObject, - collectionNodeBackendObject + collectionNodeBackendObject, ], language_code: null, schema_version: null, tags: null, playthrough_dict: { next_exploration_id: 'expId', - completed_exploration_ids: ['expId2'] - } + completed_exploration_ids: ['expId2'], + }, }; sampleCollection = Collection.create(sampleCollectionBackendObject); sampleCollectionBackendObject.nodes = [collectionNodeBackendObject]; - spyOn(readOnlyCollectionBackendApiService, 'loadCollectionAsync') - .and.resolveTo(sampleCollection); + spyOn( + readOnlyCollectionBackendApiService, + 'loadCollectionAsync' + ).and.resolveTo(sampleCollection); })); afterEach(() => { @@ -116,17 +138,20 @@ describe('Collection Player Backend Api Service', () => { }); it('should return response for collection summary', fakeAsync(() => { - spyOn(userService, 'getUserInfoAsync') - .and.returnValue(Promise.resolve(userInfoForCollectionCreator)); - spyOn(guestCollectionProgressService, 'hasCompletedSomeExploration') - .and.returnValue(true); - let requestUrl = '/collectionsummarieshandler/data?' + - 'stringified_collection_ids=%5B%22collectionId%22%5D'; + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve(userInfoForCollectionCreator) + ); + spyOn( + guestCollectionProgressService, + 'hasCompletedSomeExploration' + ).and.returnValue(true); + let requestUrl = + '/collectionsummarieshandler/data?' + + 'stringified_collection_ids=%5B%22collectionId%22%5D'; - cpbas.fetchCollectionSummariesAsync('collectionId').then( - (dataUrl) => { - expect(dataUrl).toEqual({ stringified_collection_ids: '' }); - }); + cpbas.fetchCollectionSummariesAsync('collectionId').then(dataUrl => { + expect(dataUrl).toEqual({stringified_collection_ids: ''}); + }); const req2 = httpTestingController.expectOne(requestUrl); expect(req2.request.method).toEqual('GET'); diff --git a/core/templates/pages/collection-player-page/services/collection-player-backend-api.service.ts b/core/templates/pages/collection-player-page/services/collection-player-backend-api.service.ts index e5d7d9724219..8da2f42e7804 100644 --- a/core/templates/pages/collection-player-page/services/collection-player-backend-api.service.ts +++ b/core/templates/pages/collection-player-page/services/collection-player-backend-api.service.ts @@ -16,31 +16,33 @@ * @fileoverview Backend Api Service for the Collection Player Page */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { CollectionSummary } from '../collection-player-page.component'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {CollectionSummary} from '../collection-player-page.component'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class CollectionPlayerBackendApiService { - constructor( - private http: HttpClient, - ) {} + constructor(private http: HttpClient) {} async fetchCollectionSummariesAsync( - collectionId: string + collectionId: string ): Promise { - return this.http.get( - '/collectionsummarieshandler/data', { + return this.http + .get('/collectionsummarieshandler/data', { params: { - stringified_collection_ids: JSON.stringify([collectionId]) - } - }).toPromise(); + stringified_collection_ids: JSON.stringify([collectionId]), + }, + }) + .toPromise(); } } -angular.module('oppia').factory( - 'CollectionPlayerBackendApiService', - downgradeInjectable(CollectionPlayerBackendApiService)); +angular + .module('oppia') + .factory( + 'CollectionPlayerBackendApiService', + downgradeInjectable(CollectionPlayerBackendApiService) + ); diff --git a/core/templates/pages/contact-page/contact-page-root.component.spec.ts b/core/templates/pages/contact-page/contact-page-root.component.spec.ts index 1afd29cc9cc3..a55afeab9b52 100644 --- a/core/templates/pages/contact-page/contact-page-root.component.spec.ts +++ b/core/templates/pages/contact-page/contact-page-root.component.spec.ts @@ -16,15 +16,15 @@ * @fileoverview Unit tests for the contact page root component. */ -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { ContactPageRootComponent } from './contact-page-root.component'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {ContactPageRootComponent} from './contact-page-root.component'; class MockTranslateService { onLangChange: EventEmitter = new EventEmitter(); @@ -41,18 +41,15 @@ describe('Contact Page Root', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - ContactPageRootComponent, - MockTranslatePipe - ], + declarations: [ContactPageRootComponent, MockTranslatePipe], providers: [ PageHeadService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -63,10 +60,9 @@ describe('Contact Page Root', () => { translateService = TestBed.inject(TranslateService); }); - it('should successfully instantiate the component', - () => { - expect(component).toBeDefined(); - }); + it('should successfully instantiate the component', () => { + expect(component).toBeDefined(); + }); it('should initialize and subscribe to onLangChange', () => { spyOn(translateService.onLangChange, 'subscribe'); @@ -92,10 +88,12 @@ describe('Contact Page Root', () => { component.setPageTitleAndMetaTags(); expect(translateService.instant).toHaveBeenCalledWith( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.CONTACT.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.CONTACT.TITLE + ); expect(pageHeadService.updateTitleAndMetaTags).toHaveBeenCalledWith( AppConstants.PAGES_REGISTERED_WITH_FRONTEND.CONTACT.TITLE, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.CONTACT.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.CONTACT.META + ); }); it('should unsubscribe on component destruction', () => { diff --git a/core/templates/pages/contact-page/contact-page-root.component.ts b/core/templates/pages/contact-page/contact-page-root.component.ts index e285f24b59d2..57830c83ea70 100644 --- a/core/templates/pages/contact-page/contact-page-root.component.ts +++ b/core/templates/pages/contact-page/contact-page-root.component.ts @@ -16,16 +16,16 @@ * @fileoverview Root component for contact page. */ -import { Component, OnDestroy } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; +import {Component, OnDestroy} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; @Component({ selector: 'oppia-contact-page-root', - templateUrl: './contact-page-root.component.html' + templateUrl: './contact-page-root.component.html', }) export class ContactPageRootComponent implements OnDestroy { directiveSubscriptions = new Subscription(); @@ -36,10 +36,12 @@ export class ContactPageRootComponent implements OnDestroy { setPageTitleAndMetaTags(): void { let translatedTitle = this.translateService.instant( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.CONTACT.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.CONTACT.TITLE + ); this.pageHeadService.updateTitleAndMetaTags( translatedTitle, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.CONTACT.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.CONTACT.META + ); } ngOnInit(): void { diff --git a/core/templates/pages/contact-page/contact-page-routing.module.ts b/core/templates/pages/contact-page/contact-page-routing.module.ts index 8cae31c790c2..aaacc84f61a1 100644 --- a/core/templates/pages/contact-page/contact-page-routing.module.ts +++ b/core/templates/pages/contact-page/contact-page-routing.module.ts @@ -16,24 +16,19 @@ * @fileoverview Routing module for contact page. */ -import { NgModule } from '@angular/core'; -import { Route, RouterModule } from '@angular/router'; -import { ContactPageRootComponent } from './contact-page-root.component'; +import {NgModule} from '@angular/core'; +import {Route, RouterModule} from '@angular/router'; +import {ContactPageRootComponent} from './contact-page-root.component'; const routes: Route[] = [ { path: '', - component: ContactPageRootComponent - } + component: ContactPageRootComponent, + }, ]; @NgModule({ - imports: [ - RouterModule.forChild(routes) - ], - exports: [ - RouterModule - ] + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], }) - export class ContactPageRoutingModule {} diff --git a/core/templates/pages/contact-page/contact-page.component.ts b/core/templates/pages/contact-page/contact-page.component.ts index ebd86ee9550a..a5121b6745bf 100644 --- a/core/templates/pages/contact-page/contact-page.component.ts +++ b/core/templates/pages/contact-page/contact-page.component.ts @@ -16,14 +16,18 @@ * @fileoverview Component for the contact page. */ -import { Component } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; @Component({ selector: 'oppia-contact-page', - templateUrl: './contact-page.component.html' + templateUrl: './contact-page.component.html', }) export class ContactPageComponent {} -angular.module('oppia').directive( - 'oppiaContactPage', downgradeComponent({component: ContactPageComponent})); +angular + .module('oppia') + .directive( + 'oppiaContactPage', + downgradeComponent({component: ContactPageComponent}) + ); diff --git a/core/templates/pages/contact-page/contact-page.module.ts b/core/templates/pages/contact-page/contact-page.module.ts index 5e21389a172d..7a2d5e80e537 100644 --- a/core/templates/pages/contact-page/contact-page.module.ts +++ b/core/templates/pages/contact-page/contact-page.module.ts @@ -16,26 +16,16 @@ * @fileoverview Module for the contact page. */ -import { NgModule } from '@angular/core'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { ContactPageComponent } from './contact-page.component'; -import { ContactPageRootComponent } from './contact-page-root.component'; -import { CommonModule } from '@angular/common'; -import { ContactPageRoutingModule } from './contact-page-routing.module'; +import {NgModule} from '@angular/core'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {ContactPageComponent} from './contact-page.component'; +import {ContactPageRootComponent} from './contact-page-root.component'; +import {CommonModule} from '@angular/common'; +import {ContactPageRoutingModule} from './contact-page-routing.module'; @NgModule({ - imports: [ - CommonModule, - SharedComponentsModule, - ContactPageRoutingModule - ], - declarations: [ - ContactPageComponent, - ContactPageRootComponent - ], - entryComponents: [ - ContactPageComponent, - ContactPageRootComponent - ] + imports: [CommonModule, SharedComponentsModule, ContactPageRoutingModule], + declarations: [ContactPageComponent, ContactPageRootComponent], + entryComponents: [ContactPageComponent, ContactPageRootComponent], }) export class ContactPageModule {} diff --git a/core/templates/pages/contributor-dashboard-admin-page/contributor-admin-dashboard-filter.model.spec.ts b/core/templates/pages/contributor-dashboard-admin-page/contributor-admin-dashboard-filter.model.spec.ts index 543a34e6acac..15bf83645f15 100644 --- a/core/templates/pages/contributor-dashboard-admin-page/contributor-admin-dashboard-filter.model.spec.ts +++ b/core/templates/pages/contributor-dashboard-admin-page/contributor-admin-dashboard-filter.model.spec.ts @@ -16,7 +16,7 @@ * @fileoverview Unit tests for ContributorAdminDashboardFilter model. */ -import { ContributorAdminDashboardFilter } from './contributor-admin-dashboard-filter.model'; +import {ContributorAdminDashboardFilter} from './contributor-admin-dashboard-filter.model'; describe('Contributor Admin Dashboard Filter Model', () => { let filter: ContributorAdminDashboardFilter; diff --git a/core/templates/pages/contributor-dashboard-admin-page/contributor-admin-dashboard-filter.model.ts b/core/templates/pages/contributor-dashboard-admin-page/contributor-admin-dashboard-filter.model.ts index 29407e73ae2f..58212ca98f41 100644 --- a/core/templates/pages/contributor-dashboard-admin-page/contributor-admin-dashboard-filter.model.ts +++ b/core/templates/pages/contributor-dashboard-admin-page/contributor-admin-dashboard-filter.model.ts @@ -17,7 +17,7 @@ * admin Dashboard. */ -import { ContributorDashboardAdminPageConstants as PageConstants } from './contributor-dashboard-admin-page.constants'; +import {ContributorDashboardAdminPageConstants as PageConstants} from './contributor-dashboard-admin-page.constants'; export class ContributorAdminDashboardFilter { topicIds: string[]; @@ -26,27 +26,31 @@ export class ContributorAdminDashboardFilter { lastActivity?: number; /** - * @param {String} languageCode - Language Code to filter for. - * @param {String[]} topicIds - keywords to filter for. - * @param {string} sort - sort options. - * @param {number} lastActivity - number of days since last activity. - */ + * @param {String} languageCode - Language Code to filter for. + * @param {String[]} topicIds - keywords to filter for. + * @param {string} sort - sort options. + * @param {number} lastActivity - number of days since last activity. + */ constructor( - topicIds: string[], languageCode?: string, - sort?: string | null, lastActivity?: number) { + topicIds: string[], + languageCode?: string, + sort?: string | null, + lastActivity?: number + ) { this.languageCode = languageCode; this.topicIds = topicIds; this.sort = sort; this.lastActivity = lastActivity; } - /** - * @returns {ContributorAdminDashboardFilter} - A new - * ContributorAdminDashboardFilter instance. - */ + * @returns {ContributorAdminDashboardFilter} - A new + * ContributorAdminDashboardFilter instance. + */ static createDefault(): ContributorAdminDashboardFilter { return new ContributorAdminDashboardFilter( - [], PageConstants.DEFAULT_LANGUAGE_FILTER); + [], + PageConstants.DEFAULT_LANGUAGE_FILTER + ); } } diff --git a/core/templates/pages/contributor-dashboard-admin-page/contributor-admin-dashboard-page.component.spec.ts b/core/templates/pages/contributor-dashboard-admin-page/contributor-admin-dashboard-page.component.spec.ts index b23a37aedf9d..b84ccf3ce74c 100644 --- a/core/templates/pages/contributor-dashboard-admin-page/contributor-admin-dashboard-page.component.spec.ts +++ b/core/templates/pages/contributor-dashboard-admin-page/contributor-admin-dashboard-page.component.spec.ts @@ -16,34 +16,40 @@ * @fileoverview Unit tests for contributor admin dashboard page component. */ -import { ComponentFixture, TestBed, fakeAsync, tick, waitForAsync } from '@angular/core/testing'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { CdAdminTranslationRoleEditorModal } from './translation-role-editor-modal/cd-admin-translation-role-editor-modal.component'; -import { ContributorAdminDashboardPageComponent } from './contributor-admin-dashboard-page.component'; -import { UserService } from 'services/user.service'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ContributorDashboardAdminBackendApiService } from './services/contributor-dashboard-admin-backend-api.service'; -import { CommunityContributionStatsBackendDict, ContributorDashboardAdminStatsBackendApiService } from './services/contributor-dashboard-admin-stats-backend-api.service'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { CdAdminQuestionRoleEditorModal } from './question-role-editor-modal/cd-admin-question-role-editor-modal.component'; -import { UsernameInputModal } from './username-input-modal/username-input-modal.component'; -import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; -import { UserInfo } from 'domain/user/user-info.model'; -import { ContributorAdminStatsTable } from './contributor-dashboard-tables/contributor-admin-stats-table.component'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { MatTableModule } from '@angular/material/table'; -import { By } from '@angular/platform-browser'; -import { WindowRef } from 'services/contextual/window-ref.service'; - +import { + ComponentFixture, + TestBed, + fakeAsync, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {CdAdminTranslationRoleEditorModal} from './translation-role-editor-modal/cd-admin-translation-role-editor-modal.component'; +import {ContributorAdminDashboardPageComponent} from './contributor-admin-dashboard-page.component'; +import {UserService} from 'services/user.service'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ContributorDashboardAdminBackendApiService} from './services/contributor-dashboard-admin-backend-api.service'; +import { + CommunityContributionStatsBackendDict, + ContributorDashboardAdminStatsBackendApiService, +} from './services/contributor-dashboard-admin-stats-backend-api.service'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {CdAdminQuestionRoleEditorModal} from './question-role-editor-modal/cd-admin-question-role-editor-modal.component'; +import {UsernameInputModal} from './username-input-modal/username-input-modal.component'; +import {BrowserDynamicTestingModule} from '@angular/platform-browser-dynamic/testing'; +import {UserInfo} from 'domain/user/user-info.model'; +import {ContributorAdminStatsTable} from './contributor-dashboard-tables/contributor-admin-stats-table.component'; +import {MatTooltipModule} from '@angular/material/tooltip'; +import {MatTableModule} from '@angular/material/table'; +import {By} from '@angular/platform-browser'; +import {WindowRef} from 'services/contextual/window-ref.service'; describe('Contributor dashboard Admin page', () => { let component: ContributorAdminDashboardPageComponent; let fixture: ComponentFixture; - let contributorDashboardAdminStatsBackendApiService: ( - ContributorDashboardAdminStatsBackendApiService); - let contributorDashboardAdminBackendApiService: ( - ContributorDashboardAdminBackendApiService); + let contributorDashboardAdminStatsBackendApiService: ContributorDashboardAdminStatsBackendApiService; + let contributorDashboardAdminBackendApiService: ContributorDashboardAdminBackendApiService; let ngbModal: NgbModal; let $window: WindowRef; @@ -56,53 +62,89 @@ describe('Contributor dashboard Admin page', () => { let fetchContributorAdminStatsSpy: jasmine.Spy; let ContAdminStatsSpy: jasmine.Spy; let translationCoordinatorInfo = new UserInfo( - ['USER_ROLE', 'TRANSLATION_COORDINATOR'], true, false, false, false, true, - 'en', 'username1', 'tester@example.com', true + ['USER_ROLE', 'TRANSLATION_COORDINATOR'], + true, + false, + false, + false, + true, + 'en', + 'username1', + 'tester@example.com', + true ); let nullUserInfo = new UserInfo( - ['USER_ROLE', 'TRANSLATION_COORDINATOR'], true, false, false, false, true, - 'en', null, 'tester@example.com', true + ['USER_ROLE', 'TRANSLATION_COORDINATOR'], + true, + false, + false, + false, + true, + 'en', + null, + 'tester@example.com', + true ); let questionCoordinatorInfo = new UserInfo( - ['USER_ROLE', 'QUESTION_COORDINATOR'], true, false, false, false, true, - 'en', 'username1', 'tester@example.com', true + ['USER_ROLE', 'QUESTION_COORDINATOR'], + true, + false, + false, + false, + true, + 'en', + 'username1', + 'tester@example.com', + true ); let fullAccessUserInfo = new UserInfo( - ['USER_ROLE', 'QUESTION_COORDINATOR', 'TRANSLATION_COORDINATOR'], true, - false, false, false, true, 'en', 'username1', 'tester@example.com', true + ['USER_ROLE', 'QUESTION_COORDINATOR', 'TRANSLATION_COORDINATOR'], + true, + false, + false, + false, + true, + 'en', + 'username1', + 'tester@example.com', + true ); beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule, + imports: [ + HttpClientTestingModule, BrowserAnimationsModule, MatTooltipModule, - MatTableModule], + MatTableModule, + ], declarations: [ CdAdminTranslationRoleEditorModal, ContributorAdminDashboardPageComponent, - ContributorAdminStatsTable + ContributorAdminStatsTable, ], providers: [ ContributorDashboardAdminStatsBackendApiService, ContributorDashboardAdminBackendApiService, ], - schemas: [NO_ERRORS_SCHEMA] - }).overrideModule(BrowserDynamicTestingModule, { - set: { - entryComponents: [ - CdAdminTranslationRoleEditorModal] - } - }).compileComponents(); - + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideModule(BrowserDynamicTestingModule, { + set: { + entryComponents: [CdAdminTranslationRoleEditorModal], + }, + }) + .compileComponents(); fixture = TestBed.createComponent(ContributorAdminDashboardPageComponent); component = fixture.componentInstance; contributorDashboardAdminStatsBackendApiService = TestBed.inject( - ContributorDashboardAdminStatsBackendApiService); + ContributorDashboardAdminStatsBackendApiService + ); userService = TestBed.inject(UserService); contributorDashboardAdminBackendApiService = TestBed.inject( - ContributorDashboardAdminBackendApiService); + ContributorDashboardAdminBackendApiService + ); ngbModal = TestBed.inject(NgbModal); // This approach was choosen because spyOn() doesn't work on properties @@ -115,41 +157,47 @@ describe('Contributor dashboard Admin page', () => { $window = TestBed.inject(WindowRef); Object.defineProperty($window.nativeWindow, 'innerWidth', { - get: () => 1000 + get: () => 1000, }); spyOn( - contributorDashboardAdminStatsBackendApiService, 'fetchCommunityStats') - .and.returnValue(Promise.resolve({ + contributorDashboardAdminStatsBackendApiService, + 'fetchCommunityStats' + ).and.returnValue( + Promise.resolve({ translation_reviewers_count: { en: 1, ar: 1, ms: 1, az: 1, - 'hi-en': 1 + 'hi-en': 1, }, - question_reviewers_count: 1 - } as CommunityContributionStatsBackendDict)); + question_reviewers_count: 1, + } as CommunityContributionStatsBackendDict) + ); fetchAssignedLanguageIdsSpy = spyOn( contributorDashboardAdminStatsBackendApiService, - 'fetchAssignedLanguageIds') - .and.returnValue(Promise.resolve(['en', 'ar', 'az', 'ms', 'hi-en'])); + 'fetchAssignedLanguageIds' + ).and.returnValue(Promise.resolve(['en', 'ar', 'az', 'ms', 'hi-en'])); spyOn( contributorDashboardAdminStatsBackendApiService, - 'fetchTopicChoices') - .and.returnValue(Promise.resolve([[ - { id: '1', topic: 'Science' }, - { id: '2', topic: 'Technology' }, - ], [ - { id: '1', topic: 'Science' }, - { id: '2', topic: 'Technology' }, - ]])); - - ContAdminStatsSpy = spyOn( - ContributorAdminStatsTable.prototype, - 'ngOnInit'); + 'fetchTopicChoices' + ).and.returnValue( + Promise.resolve([ + [ + {id: '1', topic: 'Science'}, + {id: '2', topic: 'Technology'}, + ], + [ + {id: '1', topic: 'Science'}, + {id: '2', topic: 'Technology'}, + ], + ]) + ); + + ContAdminStatsSpy = spyOn(ContributorAdminStatsTable.prototype, 'ngOnInit'); })); afterEach(() => { @@ -159,76 +207,93 @@ describe('Contributor dashboard Admin page', () => { describe('when user is not logged in', () => { beforeEach(() => { getUserInfoSpy = spyOn(userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(nullUserInfo)); + Promise.resolve(nullUserInfo) + ); fixture.detectChanges(); }); - it('should not update translation and question coordinator view' + - 'if username is null', fakeAsync(() => { - component.ngOnInit(); - tick(); - fixture.detectChanges(); + it( + 'should not update translation and question coordinator view' + + 'if username is null', + fakeAsync(() => { + component.ngOnInit(); + tick(); + fixture.detectChanges(); - expect(component.CONTRIBUTION_TYPES).toEqual([]); - })); + expect(component.CONTRIBUTION_TYPES).toEqual([]); + }) + ); }); describe('when no languages are available', () => { beforeEach(() => { getUserInfoSpy = spyOn(userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(fullAccessUserInfo)); + Promise.resolve(fullAccessUserInfo) + ); }); - it('should default to English language if no language is populated ' + - 'if user is logged in', fakeAsync(() => { - component.ngOnInit(); - tick(); - fixture.detectChanges(); - component.selectLanguage('French'); - - expect(component.selectedLanguage).toEqual({ - language: 'English', - id: 'en' - }); - })); + it( + 'should default to English language if no language is populated ' + + 'if user is logged in', + fakeAsync(() => { + component.ngOnInit(); + tick(); + fixture.detectChanges(); + component.selectLanguage('French'); + + expect(component.selectedLanguage).toEqual({ + language: 'English', + id: 'en', + }); + }) + ); }); describe('when user is logged in', () => { beforeEach(() => { getUserInfoSpy = spyOn(userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(fullAccessUserInfo)); + Promise.resolve(fullAccessUserInfo) + ); fixture.detectChanges(); component.ngOnInit(); }); - it('should initialize the contributor admin stats table only' + - ' once when the contributor dashboard admin component' + - ' is intialized', fakeAsync(() => { - component.ngOnInit(); - fixture.detectChanges(); - tick(); - const statsTable = fixture.debugElement.query( - By.directive(ContributorAdminStatsTable)); - expect(statsTable).toBeTruthy(); - expect(ContAdminStatsSpy).toHaveBeenCalledTimes(1); - })); - - it('should fetch contributor admin stats only once when' + - ' when the contributor dashboard admin component ' + - 'is initialized', fakeAsync(() => { - fetchContributorAdminStatsSpy = spyOn( - contributorDashboardAdminStatsBackendApiService, - 'fetchContributorAdminStats') - .and.returnValue(Promise.resolve( - {stats: [], nextOffset: 0, more: false})); - component.ngOnInit(); - fixture.detectChanges(); - tick(); - expect(fetchContributorAdminStatsSpy).toHaveBeenCalledTimes(1); - })); + it( + 'should initialize the contributor admin stats table only' + + ' once when the contributor dashboard admin component' + + ' is intialized', + fakeAsync(() => { + component.ngOnInit(); + fixture.detectChanges(); + tick(); + const statsTable = fixture.debugElement.query( + By.directive(ContributorAdminStatsTable) + ); + expect(statsTable).toBeTruthy(); + expect(ContAdminStatsSpy).toHaveBeenCalledTimes(1); + }) + ); + + it( + 'should fetch contributor admin stats only once when' + + ' when the contributor dashboard admin component ' + + 'is initialized', + fakeAsync(() => { + fetchContributorAdminStatsSpy = spyOn( + contributorDashboardAdminStatsBackendApiService, + 'fetchContributorAdminStats' + ).and.returnValue( + Promise.resolve({stats: [], nextOffset: 0, more: false}) + ); + component.ngOnInit(); + fixture.detectChanges(); + tick(); + expect(fetchContributorAdminStatsSpy).toHaveBeenCalledTimes(1); + }) + ); it('should initialize variables onInit', fakeAsync(() => { component.ngOnInit(); @@ -239,16 +304,20 @@ describe('Contributor dashboard Admin page', () => { expect(component.selectedLanguage.id).toEqual('en'); })); - it('should throw error if fetchAssignedLanguageIds returns invalid ' + - 'language', fakeAsync(() => { - fetchAssignedLanguageIdsSpy.and.returnValue(Promise.resolve( - ['invalid_language'])); + it( + 'should throw error if fetchAssignedLanguageIds returns invalid ' + + 'language', + fakeAsync(() => { + fetchAssignedLanguageIdsSpy.and.returnValue( + Promise.resolve(['invalid_language']) + ); - expect(() => { - component.ngOnInit(); - tick(); - }).toThrowError(); - })); + expect(() => { + component.ngOnInit(); + tick(); + }).toThrowError(); + }) + ); it('should open language dropdown', () => { expect(component.languageDropdownShown).toBeFalse(); @@ -272,11 +341,12 @@ describe('Contributor dashboard Admin page', () => { fixture.detectChanges(); const nonDefaultLanguage = { id: 'ar', - language: 'Arabic (العربية)' + language: 'Arabic (العربية)', }; component.selectLanguage(nonDefaultLanguage.language); - expect(component.selectedLanguage.language) - .toBe(nonDefaultLanguage.language); + expect(component.selectedLanguage.language).toBe( + nonDefaultLanguage.language + ); expect(component.selectedLanguage.id).toBe(nonDefaultLanguage.id); })); @@ -293,58 +363,66 @@ describe('Contributor dashboard Admin page', () => { expect(component.selectedLastActivity).toEqual(lastActivityDays); })); - it('should remove duplicates from available topics when component' + - ' is initialized', fakeAsync(() => { - // Duplicate topics are present in the topics returned - // by the stats service. - component.ngOnInit(); - tick(); - expect(component.topics).toEqual ([ - { id: '1', topic: 'Science' }, - { id: '2', topic: 'Technology' }, - ]); - })); - - it('should apply the topic filter to the topic dropdown' + - 'when a topic is chosen', fakeAsync(() => { - component.ngOnInit(); - tick(); - component.selectedTopicNames = ['Science']; - - fixture.detectChanges(); - tick(); - component.applyTopicFilter(); + it( + 'should remove duplicates from available topics when component' + + ' is initialized', + fakeAsync(() => { + // Duplicate topics are present in the topics returned + // by the stats service. + component.ngOnInit(); + tick(); + expect(component.topics).toEqual([ + {id: '1', topic: 'Science'}, + {id: '2', topic: 'Technology'}, + ]); + }) + ); + + it( + 'should apply the topic filter to the topic dropdown' + + 'when a topic is chosen', + fakeAsync(() => { + component.ngOnInit(); + tick(); + component.selectedTopicNames = ['Science']; - expect(component.selectedTopicIds).toEqual(['1']); - })); + fixture.detectChanges(); + tick(); + component.applyTopicFilter(); - it('should throw error when topic filter has chosen a topic id ' + - 'that is no longer valid', fakeAsync(() => { - component.ngOnInit(); - tick(); - component.selectedTopicNames = ['topic_with_no_id']; - component.topics = [ - { id: '1', topic: 'Science' }, - { id: '2', topic: 'Technology' }, - ]; + expect(component.selectedTopicIds).toEqual(['1']); + }) + ); - fixture.detectChanges(); - tick(); + it( + 'should throw error when topic filter has chosen a topic id ' + + 'that is no longer valid', + fakeAsync(() => { + component.ngOnInit(); + tick(); + component.selectedTopicNames = ['topic_with_no_id']; + component.topics = [ + {id: '1', topic: 'Science'}, + {id: '2', topic: 'Technology'}, + ]; - expect(() => { - component.applyTopicFilter(); + fixture.detectChanges(); tick(); - }).toThrowError( - 'Selected Topic Id doesn\'t match any valid topic.'); - })); + + expect(() => { + component.applyTopicFilter(); + tick(); + }).toThrowError("Selected Topic Id doesn't match any valid topic."); + }) + ); it('should apply topic and language filter both', fakeAsync(() => { component.ngOnInit(); tick(); component.selectedTopicNames = ['Science', 'Technology']; component.topics = [ - { id: '1', topic: 'Science' }, - { id: '2', topic: 'Technology' }, + {id: '1', topic: 'Science'}, + {id: '2', topic: 'Technology'}, ]; fixture.detectChanges(); @@ -356,11 +434,12 @@ describe('Contributor dashboard Admin page', () => { expect(component.selectedLanguage.id).toBe('en'); const nonDefaultLanguage = { id: 'ar', - language: 'Arabic (العربية)' + language: 'Arabic (العربية)', }; component.selectLanguage(nonDefaultLanguage.language); - expect(component.selectedLanguage.language) - .toBe(nonDefaultLanguage.language); + expect(component.selectedLanguage.language).toBe( + nonDefaultLanguage.language + ); expect(component.selectedLanguage.id).toBe(nonDefaultLanguage.id); expect(component.selectedTopicIds).toEqual(['1', '2']); })); @@ -377,276 +456,314 @@ describe('Contributor dashboard Admin page', () => { expect(component.selectedContributionType).toEqual('selection1'); }); - it('should set the translation coordinator variable when the user' + - ' is a translation coordinator', fakeAsync(() => { - getUserInfoSpy.and.returnValue( - Promise.resolve(translationCoordinatorInfo)); - - component.ngOnInit(); - tick(); - fixture.detectChanges(); - - expect(component.isTranslationCoordinator).toBeTrue(); - })); - - it('should set the question coordinator variable when the user' + - ' is a question coordinator', fakeAsync(() => { - getUserInfoSpy.and.returnValue( - Promise.resolve(questionCoordinatorInfo)); - - component.ngOnInit(); - tick(); - fixture.detectChanges(); - - expect(component.isQuestionCoordinator).toBeTrue(); - })); - - it('should update translation and question coordinator view', + it( + 'should set the translation coordinator variable when the user' + + ' is a translation coordinator', fakeAsync(() => { + getUserInfoSpy.and.returnValue( + Promise.resolve(translationCoordinatorInfo) + ); + component.ngOnInit(); tick(); fixture.detectChanges(); - expect(component.isQuestionCoordinator).toBeTrue(); expect(component.isTranslationCoordinator).toBeTrue(); - })); + }) + ); - it('should open the language dropdown when clicked away', + it( + 'should set the question coordinator variable when the user' + + ' is a question coordinator', fakeAsync(() => { - spyOn(component, 'checkMobileView').and.returnValue(false); - const fakeClickAwayEvent = new MouseEvent('click'); - Object.defineProperty( - fakeClickAwayEvent, - 'target', - {value: document.createElement('div')}); - - component.toggleLanguageDropdown(); - component.ngOnInit(); - tick(); - component.onDocumentClick(fakeClickAwayEvent); - fixture.detectChanges(); - - expect(component.languageDropdownShown).toBe(false); - })); + getUserInfoSpy.and.returnValue( + Promise.resolve(questionCoordinatorInfo) + ); - it('should hide opened activity dropdown when clicking away', - fakeAsync(() => { - spyOn(component, 'checkMobileView').and.returnValue(false); - const fakeClickAwayEvent = new MouseEvent('click'); - Object.defineProperty( - fakeClickAwayEvent, - 'target', - {value: document.createElement('div')}); - - component.toggleActivityDropdown(); component.ngOnInit(); tick(); - component.onDocumentClick(fakeClickAwayEvent); fixture.detectChanges(); - expect(component.activityDropdownShown).toBe(false); - })); + expect(component.isQuestionCoordinator).toBeTrue(); + }) + ); - it('should not hide opened activity dropdown when clicking away on mobile', - fakeAsync(() => { - spyOn(component, 'checkMobileView').and.returnValue(true); - const fakeClickAwayEvent = new MouseEvent('click'); - Object.defineProperty( - fakeClickAwayEvent, - 'target', - {value: document.createElement('div')}); - - component.toggleActivityDropdown(); - component.ngOnInit(); - tick(); - component.onDocumentClick(fakeClickAwayEvent); - fixture.detectChanges(); + it('should update translation and question coordinator view', fakeAsync(() => { + component.ngOnInit(); + tick(); + fixture.detectChanges(); - expect(component.activityDropdownShown).toBe(true); - })); + expect(component.isQuestionCoordinator).toBeTrue(); + expect(component.isTranslationCoordinator).toBeTrue(); + })); - it('should open question role editor modal when on question' + - ' submitter tab', fakeAsync(() => { - const removeRightsSpy = spyOn( - contributorDashboardAdminBackendApiService, - 'removeContributionReviewerAsync'); - component.activeTab = component.TAB_NAME_QUESTION_SUBMITTER; - fixture.detectChanges(); - spyOn( - contributorDashboardAdminBackendApiService, - 'contributionReviewerRightsAsync').and.returnValue(Promise.resolve({ - can_submit_questions: true, - can_review_questions: true, - can_review_translation_for_language_codes: [], - can_review_voiceover_for_language_codes: [] - })); - let modalSpy = spyOn(ngbModal, 'open').and.callFake(() => { - return ({ - componentInstance: MockNgbModalRef, - result: Promise.resolve({ - isQuestionSubmitter: false, - isQuestionReviewer: true - }) - }) as NgbModalRef; + it('should open the language dropdown when clicked away', fakeAsync(() => { + spyOn(component, 'checkMobileView').and.returnValue(false); + const fakeClickAwayEvent = new MouseEvent('click'); + Object.defineProperty(fakeClickAwayEvent, 'target', { + value: document.createElement('div'), }); - component.openRoleEditor('user1'); + component.toggleLanguageDropdown(); + component.ngOnInit(); tick(); + component.onDocumentClick(fakeClickAwayEvent); + fixture.detectChanges(); - expect(modalSpy).toHaveBeenCalledWith(CdAdminQuestionRoleEditorModal); - expect(removeRightsSpy).toHaveBeenCalled(); + expect(component.languageDropdownShown).toBe(false); })); - it('should open question role editor modal when on question' + - ' reviewer tabs', fakeAsync(() => { - const removeRightsSpy = spyOn( - contributorDashboardAdminBackendApiService, - 'removeContributionReviewerAsync'); - component.activeTab = component.TAB_NAME_QUESTION_REVIEWER; - fixture.detectChanges(); - spyOn( - contributorDashboardAdminBackendApiService, - 'contributionReviewerRightsAsync').and.returnValue(Promise.resolve({ - can_submit_questions: false, - can_review_questions: true, - can_review_translation_for_language_codes: ['en'], - can_review_voiceover_for_language_codes: [] - })); - let modalSpy = spyOn(ngbModal, 'open').and.callFake(() => { - return ({ - componentInstance: MockNgbModalRef, - result: Promise.resolve({ - isQuestionSubmitter: false, - isQuestionReviewer: false - }) - }) as NgbModalRef; + it('should hide opened activity dropdown when clicking away', fakeAsync(() => { + spyOn(component, 'checkMobileView').and.returnValue(false); + const fakeClickAwayEvent = new MouseEvent('click'); + Object.defineProperty(fakeClickAwayEvent, 'target', { + value: document.createElement('div'), }); - component.openRoleEditor('user1'); + component.toggleActivityDropdown(); + component.ngOnInit(); tick(); + component.onDocumentClick(fakeClickAwayEvent); + fixture.detectChanges(); - expect(modalSpy).toHaveBeenCalledWith(CdAdminQuestionRoleEditorModal); - expect(removeRightsSpy).toHaveBeenCalled(); + expect(component.activityDropdownShown).toBe(false); })); - it('should open question role editor modal and return changed' + - ' value of question reviewer', fakeAsync(() => { - const removeRightsSpy = spyOn( - contributorDashboardAdminBackendApiService, - 'removeContributionReviewerAsync'); - component.activeTab = component.TAB_NAME_QUESTION_SUBMITTER; - fixture.detectChanges(); - spyOn( - contributorDashboardAdminBackendApiService, - 'contributionReviewerRightsAsync').and.returnValue(Promise.resolve({ - can_submit_questions: true, - can_review_questions: true, - can_review_translation_for_language_codes: [], - can_review_voiceover_for_language_codes: [] - })); - let modalSpy = spyOn(ngbModal, 'open').and.callFake(() => { - return ({ - componentInstance: MockNgbModalRef, - result: Promise.resolve({ - isQuestionSubmitter: true, - isQuestionReviewer: false - }) - }) as NgbModalRef; + it('should not hide opened activity dropdown when clicking away on mobile', fakeAsync(() => { + spyOn(component, 'checkMobileView').and.returnValue(true); + const fakeClickAwayEvent = new MouseEvent('click'); + Object.defineProperty(fakeClickAwayEvent, 'target', { + value: document.createElement('div'), }); - component.openRoleEditor('user1'); + component.toggleActivityDropdown(); + component.ngOnInit(); tick(); + component.onDocumentClick(fakeClickAwayEvent); + fixture.detectChanges(); - expect(modalSpy).toHaveBeenCalledWith(CdAdminQuestionRoleEditorModal); - expect(removeRightsSpy).toHaveBeenCalled(); + expect(component.activityDropdownShown).toBe(true); })); - it('should open question role editor modal and return changed value of' + - ' question reviewer', fakeAsync(() => { - const addRightsSpy = spyOn( - contributorDashboardAdminBackendApiService, - 'addContributionReviewerAsync'); - component.activeTab = component.TAB_NAME_QUESTION_SUBMITTER; - fixture.detectChanges(); - spyOn( - contributorDashboardAdminBackendApiService, - 'contributionReviewerRightsAsync').and.returnValue(Promise.resolve({ - can_submit_questions: true, - can_review_questions: false, - can_review_translation_for_language_codes: [], - can_review_voiceover_for_language_codes: [] - })); - let modalSpy = spyOn(ngbModal, 'open').and.callFake(() => { - return ({ - componentInstance: MockNgbModalRef, - result: Promise.resolve({ - isQuestionSubmitter: true, - isQuestionReviewer: true + it( + 'should open question role editor modal when on question' + + ' submitter tab', + fakeAsync(() => { + const removeRightsSpy = spyOn( + contributorDashboardAdminBackendApiService, + 'removeContributionReviewerAsync' + ); + component.activeTab = component.TAB_NAME_QUESTION_SUBMITTER; + fixture.detectChanges(); + spyOn( + contributorDashboardAdminBackendApiService, + 'contributionReviewerRightsAsync' + ).and.returnValue( + Promise.resolve({ + can_submit_questions: true, + can_review_questions: true, + can_review_translation_for_language_codes: [], + can_review_voiceover_for_language_codes: [], }) - }) as NgbModalRef; - }); + ); + let modalSpy = spyOn(ngbModal, 'open').and.callFake(() => { + return { + componentInstance: MockNgbModalRef, + result: Promise.resolve({ + isQuestionSubmitter: false, + isQuestionReviewer: true, + }), + } as NgbModalRef; + }); + + component.openRoleEditor('user1'); + tick(); - component.openRoleEditor('user1'); - tick(); + expect(modalSpy).toHaveBeenCalledWith(CdAdminQuestionRoleEditorModal); + expect(removeRightsSpy).toHaveBeenCalled(); + }) + ); - expect(modalSpy).toHaveBeenCalledWith(CdAdminQuestionRoleEditorModal); - expect(addRightsSpy).toHaveBeenCalled(); - })); + it( + 'should open question role editor modal when on question' + + ' reviewer tabs', + fakeAsync(() => { + const removeRightsSpy = spyOn( + contributorDashboardAdminBackendApiService, + 'removeContributionReviewerAsync' + ); + component.activeTab = component.TAB_NAME_QUESTION_REVIEWER; + fixture.detectChanges(); + spyOn( + contributorDashboardAdminBackendApiService, + 'contributionReviewerRightsAsync' + ).and.returnValue( + Promise.resolve({ + can_submit_questions: false, + can_review_questions: true, + can_review_translation_for_language_codes: ['en'], + can_review_voiceover_for_language_codes: [], + }) + ); + let modalSpy = spyOn(ngbModal, 'open').and.callFake(() => { + return { + componentInstance: MockNgbModalRef, + result: Promise.resolve({ + isQuestionSubmitter: false, + isQuestionReviewer: false, + }), + } as NgbModalRef; + }); + + component.openRoleEditor('user1'); + tick(); - it('should open question role editor modal and return changed value' + - ' question submitter', fakeAsync(() => { - const addRightsSpy = spyOn( - contributorDashboardAdminBackendApiService, - 'addContributionReviewerAsync'); - component.activeTab = component.TAB_NAME_QUESTION_SUBMITTER; - fixture.detectChanges(); - spyOn( - contributorDashboardAdminBackendApiService, - 'contributionReviewerRightsAsync').and.returnValue(Promise.resolve({ - can_submit_questions: false, - can_review_questions: true, - can_review_translation_for_language_codes: [], - can_review_voiceover_for_language_codes: [] - })); - let modalSpy = spyOn(ngbModal, 'open').and.callFake(() => { - return ({ - componentInstance: MockNgbModalRef, - result: Promise.resolve({ - isQuestionSubmitter: true, - isQuestionReviewer: true + expect(modalSpy).toHaveBeenCalledWith(CdAdminQuestionRoleEditorModal); + expect(removeRightsSpy).toHaveBeenCalled(); + }) + ); + + it( + 'should open question role editor modal and return changed' + + ' value of question reviewer', + fakeAsync(() => { + const removeRightsSpy = spyOn( + contributorDashboardAdminBackendApiService, + 'removeContributionReviewerAsync' + ); + component.activeTab = component.TAB_NAME_QUESTION_SUBMITTER; + fixture.detectChanges(); + spyOn( + contributorDashboardAdminBackendApiService, + 'contributionReviewerRightsAsync' + ).and.returnValue( + Promise.resolve({ + can_submit_questions: true, + can_review_questions: true, + can_review_translation_for_language_codes: [], + can_review_voiceover_for_language_codes: [], }) - }) as NgbModalRef; - }); + ); + let modalSpy = spyOn(ngbModal, 'open').and.callFake(() => { + return { + componentInstance: MockNgbModalRef, + result: Promise.resolve({ + isQuestionSubmitter: true, + isQuestionReviewer: false, + }), + } as NgbModalRef; + }); + + component.openRoleEditor('user1'); + tick(); - component.openRoleEditor('user1'); - tick(); + expect(modalSpy).toHaveBeenCalledWith(CdAdminQuestionRoleEditorModal); + expect(removeRightsSpy).toHaveBeenCalled(); + }) + ); - expect(modalSpy).toHaveBeenCalledWith(CdAdminQuestionRoleEditorModal); - expect(addRightsSpy).toHaveBeenCalled(); - })); + it( + 'should open question role editor modal and return changed value of' + + ' question reviewer', + fakeAsync(() => { + const addRightsSpy = spyOn( + contributorDashboardAdminBackendApiService, + 'addContributionReviewerAsync' + ); + component.activeTab = component.TAB_NAME_QUESTION_SUBMITTER; + fixture.detectChanges(); + spyOn( + contributorDashboardAdminBackendApiService, + 'contributionReviewerRightsAsync' + ).and.returnValue( + Promise.resolve({ + can_submit_questions: true, + can_review_questions: false, + can_review_translation_for_language_codes: [], + can_review_voiceover_for_language_codes: [], + }) + ); + let modalSpy = spyOn(ngbModal, 'open').and.callFake(() => { + return { + componentInstance: MockNgbModalRef, + result: Promise.resolve({ + isQuestionSubmitter: true, + isQuestionReviewer: true, + }), + } as NgbModalRef; + }); + + component.openRoleEditor('user1'); + tick(); + + expect(modalSpy).toHaveBeenCalledWith(CdAdminQuestionRoleEditorModal); + expect(addRightsSpy).toHaveBeenCalled(); + }) + ); + + it( + 'should open question role editor modal and return changed value' + + ' question submitter', + fakeAsync(() => { + const addRightsSpy = spyOn( + contributorDashboardAdminBackendApiService, + 'addContributionReviewerAsync' + ); + component.activeTab = component.TAB_NAME_QUESTION_SUBMITTER; + fixture.detectChanges(); + spyOn( + contributorDashboardAdminBackendApiService, + 'contributionReviewerRightsAsync' + ).and.returnValue( + Promise.resolve({ + can_submit_questions: false, + can_review_questions: true, + can_review_translation_for_language_codes: [], + can_review_voiceover_for_language_codes: [], + }) + ); + let modalSpy = spyOn(ngbModal, 'open').and.callFake(() => { + return { + componentInstance: MockNgbModalRef, + result: Promise.resolve({ + isQuestionSubmitter: true, + isQuestionReviewer: true, + }), + } as NgbModalRef; + }); + + component.openRoleEditor('user1'); + tick(); + + expect(modalSpy).toHaveBeenCalledWith(CdAdminQuestionRoleEditorModal); + expect(addRightsSpy).toHaveBeenCalled(); + }) + ); it('should open translation role editor modal', fakeAsync(() => { component.activeTab = component.TAB_NAME_TRANSLATION_REVIEWER; fixture.detectChanges(); spyOn( contributorDashboardAdminBackendApiService, - 'contributionReviewerRightsAsync').and.returnValue(Promise.resolve({ - can_submit_questions: false, - can_review_questions: true, - can_review_translation_for_language_codes: ['en'], - can_review_voiceover_for_language_codes: [] - })); + 'contributionReviewerRightsAsync' + ).and.returnValue( + Promise.resolve({ + can_submit_questions: false, + can_review_questions: true, + can_review_translation_for_language_codes: ['en'], + can_review_voiceover_for_language_codes: [], + }) + ); let modalSpy = spyOn(ngbModal, 'open').and.callFake(() => { - return ({ - componentInstance: MockNgbModalRef - }) as NgbModalRef; + return { + componentInstance: MockNgbModalRef, + } as NgbModalRef; }); component.openRoleEditor('user1'); tick(); - expect(modalSpy).toHaveBeenCalledWith( - CdAdminTranslationRoleEditorModal); + expect(modalSpy).toHaveBeenCalledWith(CdAdminTranslationRoleEditorModal); })); it('should open username input modal', fakeAsync(() => { @@ -654,10 +771,10 @@ describe('Contributor dashboard Admin page', () => { fixture.detectChanges(); const openRoleEditorSpy = spyOn(component, 'openRoleEditor'); let modalSpy = spyOn(ngbModal, 'open').and.callFake(() => { - return ({ + return { componentInstance: MockNgbModalRef, - result: Promise.resolve('user1') - }) as NgbModalRef; + result: Promise.resolve('user1'), + } as NgbModalRef; }); component.openUsernameInputModal(); @@ -672,15 +789,15 @@ describe('Contributor dashboard Admin page', () => { tick(); expect(component.languageChoices).toContain({ id: 'ms', - language: 'Bahasa Melayu (بهاس ملايو)' + language: 'Bahasa Melayu (بهاس ملايو)', }); expect(component.languageChoices).toContain({ id: 'hi-en', - language: 'Hinglish' + language: 'Hinglish', }); expect(component.languageChoices).toContain({ id: 'az', - language: 'Azerbaijani (Azeri)' + language: 'Azerbaijani (Azeri)', }); })); }); diff --git a/core/templates/pages/contributor-dashboard-admin-page/contributor-admin-dashboard-page.component.ts b/core/templates/pages/contributor-dashboard-admin-page/contributor-admin-dashboard-page.component.ts index 822de7e5c144..68175b5a2737 100644 --- a/core/templates/pages/contributor-dashboard-admin-page/contributor-admin-dashboard-page.component.ts +++ b/core/templates/pages/contributor-dashboard-admin-page/contributor-admin-dashboard-page.component.ts @@ -12,25 +12,34 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Component for the feedback Updates page. */ -import { Component, OnInit, ChangeDetectorRef, ViewChild, ElementRef, HostListener } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import { + Component, + OnInit, + ChangeDetectorRef, + ViewChild, + ElementRef, + HostListener, +} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {WindowRef} from 'services/contextual/window-ref.service'; import './contributor-admin-dashboard-page.component.css'; -import { animate, state, style, transition, trigger } from '@angular/animations'; -import { ContributorDashboardAdminStatsBackendApiService, translationReviewersCount } from './services/contributor-dashboard-admin-stats-backend-api.service'; -import { AppConstants } from 'app.constants'; -import { ContributorAdminDashboardFilter } from './contributor-admin-dashboard-filter.model'; -import { UserService } from 'services/user.service'; -import { ContributorDashboardAdminBackendApiService } from './services/contributor-dashboard-admin-backend-api.service'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { UsernameInputModal } from './username-input-modal/username-input-modal.component'; -import { CdAdminQuestionRoleEditorModal } from './question-role-editor-modal/cd-admin-question-role-editor-modal.component'; -import { CdAdminTranslationRoleEditorModal } from './translation-role-editor-modal/cd-admin-translation-role-editor-modal.component'; +import {animate, state, style, transition, trigger} from '@angular/animations'; +import { + ContributorDashboardAdminStatsBackendApiService, + translationReviewersCount, +} from './services/contributor-dashboard-admin-stats-backend-api.service'; +import {AppConstants} from 'app.constants'; +import {ContributorAdminDashboardFilter} from './contributor-admin-dashboard-filter.model'; +import {UserService} from 'services/user.service'; +import {ContributorDashboardAdminBackendApiService} from './services/contributor-dashboard-admin-backend-api.service'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {UsernameInputModal} from './username-input-modal/username-input-modal.component'; +import {CdAdminQuestionRoleEditorModal} from './question-role-editor-modal/cd-admin-question-role-editor-modal.component'; +import {CdAdminTranslationRoleEditorModal} from './translation-role-editor-modal/cd-admin-translation-role-editor-modal.component'; import isEqual from 'lodash/isEqual'; export interface LanguageChoice { @@ -47,25 +56,25 @@ export interface TopicChoice { templateUrl: './contributor-admin-dashboard-page.component.html', animations: [ trigger('lastActivityDropdown', [ - state('expanded', style({ transform: 'rotate(180deg)' })), - state('collapsed', style({ transform: 'rotate(0)' })), + state('expanded', style({transform: 'rotate(180deg)'})), + state('collapsed', style({transform: 'rotate(0)'})), transition('expanded => collapsed', animate('200ms ease-out')), transition('collapsed => expanded', animate('200ms ease-in')), ]), trigger('languageDropdownTrigger', [ - state('expanded', style({ transform: 'rotate(180deg)' })), - state('collapsed', style({ transform: 'rotate(0)' })), + state('expanded', style({transform: 'rotate(180deg)'})), + state('collapsed', style({transform: 'rotate(0)'})), transition('expanded => collapsed', animate('200ms ease-out')), transition('collapsed => expanded', animate('200ms ease-in')), ]), ], }) export class ContributorAdminDashboardPageComponent implements OnInit { - @ViewChild( - 'languageDropdown', {'static': false}) languageDropdownRef!: ElementRef; + @ViewChild('languageDropdown', {static: false}) + languageDropdownRef!: ElementRef; - @ViewChild( - 'activityDropdown', {'static': false}) activityDropdownRef!: ElementRef; + @ViewChild('activityDropdown', {static: false}) + activityDropdownRef!: ElementRef; languageDropdownShown: boolean = false; activityDropdownShown: boolean = false; @@ -90,112 +99,110 @@ export class ContributorAdminDashboardPageComponent implements OnInit { languageChoices: LanguageChoice[] = []; topics: TopicChoice[] = []; topicsAreFetched: string = 'false'; - filter: ContributorAdminDashboardFilter = ( - ContributorAdminDashboardFilter.createDefault()); + filter: ContributorAdminDashboardFilter = + ContributorAdminDashboardFilter.createDefault(); selectedLanguage: LanguageChoice = { language: '', - id: '' + id: '', }; - constructor( private windowRef: WindowRef, private changeDetectorRef: ChangeDetectorRef, - private contributorDashboardAdminStatsBackendApiService: - ContributorDashboardAdminStatsBackendApiService, + private contributorDashboardAdminStatsBackendApiService: ContributorDashboardAdminStatsBackendApiService, private userService: UserService, - private contributorDashboardAdminBackendApiService: - ContributorDashboardAdminBackendApiService, - private modalService: NgbModal, + private contributorDashboardAdminBackendApiService: ContributorDashboardAdminBackendApiService, + private modalService: NgbModal ) {} ngOnInit(): void { this.loadingMessage = 'Loading'; this.activeTab = this.TAB_NAME_TRANSLATION_SUBMITTER; this.contributorDashboardAdminStatsBackendApiService - .fetchCommunityStats().then( - response => { - this.translationReviewersCountByLanguage = ( - response.translation_reviewers_count); - this.questionReviewersCount = response.question_reviewers_count; - - this.languageChoices = AppConstants.SUPPORTED_AUDIO_LANGUAGES.map( - languageItem => { - return { - id: languageItem.id, - language: - this.putEnglishLanguageNameAtFront(languageItem.description), - }; - } - ); - - this.selectedLastActivity = 0; - - this.userService.getUserInfoAsync().then(userInfo => { - const username = userInfo.getUsername(); + .fetchCommunityStats() + .then(response => { + this.translationReviewersCountByLanguage = + response.translation_reviewers_count; + this.questionReviewersCount = response.question_reviewers_count; + + this.languageChoices = AppConstants.SUPPORTED_AUDIO_LANGUAGES.map( + languageItem => { + return { + id: languageItem.id, + language: this.putEnglishLanguageNameAtFront( + languageItem.description + ), + }; + } + ); - if (username === null) { - return; - } - this.isQuestionCoordinator = userInfo.isQuestionCoordinator(); - this.isTranslationCoordinator = userInfo.isTranslationCoordinator(); - - if (this.isTranslationCoordinator) { - this.CONTRIBUTION_TYPES.push( - this.TAB_NAME_TRANSLATION_SUBMITTER, - this.TAB_NAME_TRANSLATION_REVIEWER); - - this.contributorDashboardAdminStatsBackendApiService - .fetchAssignedLanguageIds(username).then( - response => { - this.languageChoices = this.languageChoices.filter(( - languageItem) => - response.includes(languageItem.id)); - this.selectedLanguage = this.languageChoices[0]; - if (!this.selectedLanguage) { - throw new Error( - 'No languages are assigned to user.'); - } - this.translationReviewersCount = ( - this.translationReviewersCountByLanguage[ - this.selectedLanguage.id]); - }); - } - if (this.isQuestionCoordinator) { - this.CONTRIBUTION_TYPES.push( - this.TAB_NAME_QUESTION_SUBMITTER, - this.TAB_NAME_QUESTION_REVIEWER); - } + this.selectedLastActivity = 0; - this.createFilter(); + this.userService.getUserInfoAsync().then(userInfo => { + const username = userInfo.getUsername(); - this.updateSelectedContributionType(this.CONTRIBUTION_TYPES[0]); + if (username === null) { + return; + } + this.isQuestionCoordinator = userInfo.isQuestionCoordinator(); + this.isTranslationCoordinator = userInfo.isTranslationCoordinator(); - this.loadingMessage = ''; - this.changeDetectorRef.detectChanges(); - this.lastActivity = [0, 7, 30, 90]; + if (this.isTranslationCoordinator) { + this.CONTRIBUTION_TYPES.push( + this.TAB_NAME_TRANSLATION_SUBMITTER, + this.TAB_NAME_TRANSLATION_REVIEWER + ); this.contributorDashboardAdminStatsBackendApiService - .fetchTopicChoices().then( - response => { - this.topics = this.filterTopicChoices(response.flat()); - this.allTopicNames = this.topics.map( - topic => topic.topic); - this.applyTopicFilter(); - this.topicsAreFetched = 'true'; + .fetchAssignedLanguageIds(username) + .then(response => { + this.languageChoices = this.languageChoices.filter( + languageItem => response.includes(languageItem.id) + ); + this.selectedLanguage = this.languageChoices[0]; + if (!this.selectedLanguage) { + throw new Error('No languages are assigned to user.'); } - ); - }); + this.translationReviewersCount = + this.translationReviewersCountByLanguage[ + this.selectedLanguage.id + ]; + }); + } + if (this.isQuestionCoordinator) { + this.CONTRIBUTION_TYPES.push( + this.TAB_NAME_QUESTION_SUBMITTER, + this.TAB_NAME_QUESTION_REVIEWER + ); + } + + this.createFilter(); + + this.updateSelectedContributionType(this.CONTRIBUTION_TYPES[0]); + + this.loadingMessage = ''; + this.changeDetectorRef.detectChanges(); + this.lastActivity = [0, 7, 30, 90]; + + this.contributorDashboardAdminStatsBackendApiService + .fetchTopicChoices() + .then(response => { + this.topics = this.filterTopicChoices(response.flat()); + this.allTopicNames = this.topics.map(topic => topic.topic); + this.applyTopicFilter(); + this.topicsAreFetched = 'true'; + }); }); + }); } filterTopicChoices(topic: TopicChoice[]): TopicChoice[] { let filteredTopic: TopicChoice[] = []; - topic.forEach((topicItem) => { + topic.forEach(topicItem => { let isTopicPresent: boolean = false; - filteredTopic.forEach((filteredTopicItem) => { + filteredTopic.forEach(filteredTopicItem => { if (filteredTopicItem.id === topicItem.id) { isTopicPresent = true; } @@ -233,7 +240,8 @@ export class ContributorAdminDashboardPageComponent implements OnInit { this.selectedTopicIds, this.selectedLanguage.id, null, - this.selectedLastActivity); + this.selectedLastActivity + ); if (this.filter === undefined || !isEqual(tempFilter, this.filter)) { this.filter = tempFilter; @@ -241,28 +249,28 @@ export class ContributorAdminDashboardPageComponent implements OnInit { } applyTopicFilter(): void { - this.selectedTopicIds = this.selectedTopicNames.map( - selectedTopic => { - const matchingTopic = this.topics.find( - topicChoice => topicChoice.topic === selectedTopic); - if (!matchingTopic && this.selectedTopicNames.length) { - throw new Error( - 'Selected Topic Id doesn\'t match any valid topic.'); - } - return matchingTopic ? matchingTopic.id : ''; - }); + this.selectedTopicIds = this.selectedTopicNames.map(selectedTopic => { + const matchingTopic = this.topics.find( + topicChoice => topicChoice.topic === selectedTopic + ); + if (!matchingTopic && this.selectedTopicNames.length) { + throw new Error("Selected Topic Id doesn't match any valid topic."); + } + return matchingTopic ? matchingTopic.id : ''; + }); this.createFilter(); } selectLanguage(language: string): void { const currentOption: LanguageChoice = this.languageChoices.find( - option => option.language === language) || { + option => option.language === language + ) || { language: 'English', - id: 'en' + id: 'en', }; this.selectedLanguage = currentOption; - this.translationReviewersCount = this.translationReviewersCountByLanguage[ - this.selectedLanguage.id]; + this.translationReviewersCount = + this.translationReviewersCountByLanguage[this.selectedLanguage.id]; this.createFilter(); } @@ -277,116 +285,125 @@ export class ContributorAdminDashboardPageComponent implements OnInit { } checkMobileView(): boolean { - return (this.windowRef.nativeWindow.innerWidth < 800); + return this.windowRef.nativeWindow.innerWidth < 800; } - updateSelectedContributionType(selectedContributionType: string): void { this.selectedContributionType = selectedContributionType; this.setActiveTab(selectedContributionType); } openUsernameInputModal(): void { - const modalRef = this.modalService.open( - UsernameInputModal); + const modalRef = this.modalService.open(UsernameInputModal); modalRef.componentInstance.activeTab = this.activeTab; - modalRef.result.then(results => this.openRoleEditor(results), () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + modalRef.result.then( + results => this.openRoleEditor(results), + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } openCdAdminQuestionRoleEditorModal(username: string): void { this.contributorDashboardAdminBackendApiService - .contributionReviewerRightsAsync(username).then(response => { - const modelRef = this.modalService.open( - CdAdminQuestionRoleEditorModal); + .contributionReviewerRightsAsync(username) + .then(response => { + const modelRef = this.modalService.open(CdAdminQuestionRoleEditorModal); modelRef.componentInstance.username = username; modelRef.componentInstance.rights = { isQuestionSubmitter: response.can_submit_questions, - isQuestionReviewer: response.can_review_questions + isQuestionReviewer: response.can_review_questions, }; - modelRef.result.then(results => { - if (results.isQuestionSubmitter !== response.can_submit_questions) { - if (results.isQuestionSubmitter) { - this.contributorDashboardAdminBackendApiService - .addContributionReviewerAsync( + modelRef.result.then( + results => { + if (results.isQuestionSubmitter !== response.can_submit_questions) { + if (results.isQuestionSubmitter) { + this.contributorDashboardAdminBackendApiService.addContributionReviewerAsync( AppConstants.CD_USER_RIGHTS_CATEGORY_SUBMIT_QUESTION, username, null ); - } else { - this.contributorDashboardAdminBackendApiService - .removeContributionReviewerAsync( + } else { + this.contributorDashboardAdminBackendApiService.removeContributionReviewerAsync( username, AppConstants.CD_USER_RIGHTS_CATEGORY_SUBMIT_QUESTION, null ); + } } - } - if (results.isQuestionReviewer !== response.can_review_questions) { - if (results.isQuestionReviewer) { - this.contributorDashboardAdminBackendApiService - .addContributionReviewerAsync( + if (results.isQuestionReviewer !== response.can_review_questions) { + if (results.isQuestionReviewer) { + this.contributorDashboardAdminBackendApiService.addContributionReviewerAsync( AppConstants.CD_USER_RIGHTS_CATEGORY_REVIEW_QUESTION, username, null ); - } else { - this.contributorDashboardAdminBackendApiService - .removeContributionReviewerAsync( + } else { + this.contributorDashboardAdminBackendApiService.removeContributionReviewerAsync( username, AppConstants.CD_USER_RIGHTS_CATEGORY_REVIEW_QUESTION, null ); + } } + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. } - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + ); }); } openCdAdminTranslationRoleEditorModal(username: string): void { this.contributorDashboardAdminBackendApiService - .contributionReviewerRightsAsync(username).then(response => { + .contributionReviewerRightsAsync(username) + .then(response => { const modalRef = this.modalService.open( - CdAdminTranslationRoleEditorModal); + CdAdminTranslationRoleEditorModal + ); modalRef.componentInstance.username = username; - modalRef.componentInstance.assignedLanguageIds = ( - response.can_review_translation_for_language_codes); + modalRef.componentInstance.assignedLanguageIds = + response.can_review_translation_for_language_codes; let languageIdToName: Record = {}; AppConstants.SUPPORTED_AUDIO_LANGUAGES.forEach( - language => languageIdToName[language.id] = language.description); + language => (languageIdToName[language.id] = language.description) + ); modalRef.componentInstance.languageIdToName = languageIdToName; }); } openRoleEditor(username: string): void { - if (this.activeTab === this.TAB_NAME_TRANSLATION_REVIEWER || - this.activeTab === this.TAB_NAME_TRANSLATION_SUBMITTER) { + if ( + this.activeTab === this.TAB_NAME_TRANSLATION_REVIEWER || + this.activeTab === this.TAB_NAME_TRANSLATION_SUBMITTER + ) { this.openCdAdminTranslationRoleEditorModal(username); - } else if (this.activeTab === this.TAB_NAME_QUESTION_SUBMITTER || - this.activeTab === this.TAB_NAME_QUESTION_REVIEWER) { + } else if ( + this.activeTab === this.TAB_NAME_QUESTION_SUBMITTER || + this.activeTab === this.TAB_NAME_QUESTION_REVIEWER + ) { this.openCdAdminQuestionRoleEditorModal(username); } } /** - * Close dropdown when outside elements are clicked - * @param event mouse click event - */ + * Close dropdown when outside elements are clicked + * @param event mouse click event + */ @HostListener('document:click', ['$event']) onDocumentClick(event: MouseEvent): void { const targetElement = event.target as HTMLElement; if (this.checkMobileView()) { return; } - if (this.activeTab === this.TAB_NAME_TRANSLATION_SUBMITTER || - this.activeTab === this.TAB_NAME_TRANSLATION_REVIEWER) { + if ( + this.activeTab === this.TAB_NAME_TRANSLATION_SUBMITTER || + this.activeTab === this.TAB_NAME_TRANSLATION_REVIEWER + ) { if ( targetElement && !this.languageDropdownRef.nativeElement.contains(targetElement) @@ -403,7 +420,9 @@ export class ContributorAdminDashboardPageComponent implements OnInit { } } -angular.module('oppia').directive('contributorAdminDashboardPage', +angular.module('oppia').directive( + 'contributorAdminDashboardPage', downgradeComponent({ - component: ContributorAdminDashboardPageComponent - }) as angular.IDirectiveFactory); + component: ContributorAdminDashboardPageComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-auth.guard.spec.ts b/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-auth.guard.spec.ts index e5500336a87b..746fdf04076a 100644 --- a/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-auth.guard.spec.ts +++ b/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-auth.guard.spec.ts @@ -16,14 +16,19 @@ * @fileoverview Tests for ContributorDashboardAdminAuthGuard */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TestBed } from '@angular/core/testing'; -import { ActivatedRouteSnapshot, Router, RouterStateSnapshot, NavigationExtras } from '@angular/router'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TestBed} from '@angular/core/testing'; +import { + ActivatedRouteSnapshot, + Router, + RouterStateSnapshot, + NavigationExtras, +} from '@angular/router'; -import { AppConstants } from 'app.constants'; -import { UserInfo } from 'domain/user/user-info.model'; -import { UserService } from 'services/user.service'; -import { ContributorDashboardAdminAuthGuard } from './contributor-dashboard-admin-auth.guard'; +import {AppConstants} from 'app.constants'; +import {UserInfo} from 'domain/user/user-info.model'; +import {UserService} from 'services/user.service'; +import {ContributorDashboardAdminAuthGuard} from './contributor-dashboard-admin-auth.guard'; class MockRouter { navigate(commands: string[], extras?: NavigationExtras): Promise { @@ -39,7 +44,7 @@ describe('ContributorDashboardAdminAuthGuard', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [UserService, { provide: Router, useClass: MockRouter }], + providers: [UserService, {provide: Router, useClass: MockRouter}], }).compileComponents(); guard = TestBed.inject(ContributorDashboardAdminAuthGuard); @@ -47,37 +52,50 @@ describe('ContributorDashboardAdminAuthGuard', () => { router = TestBed.inject(Router); }); - it('should redirect user to 401 page if user is not cd-admin', (done) => { + it('should redirect user to 401 page if user is not cd-admin', done => { const getUserInfoAsyncSpy = spyOn( - userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(UserInfo.createDefault()) - ); + userService, + 'getUserInfoAsync' + ).and.returnValue(Promise.resolve(UserInfo.createDefault())); const navigateSpy = spyOn(router, 'navigate').and.callThrough(); - guard.canActivate( - new ActivatedRouteSnapshot(), {} as RouterStateSnapshot).then( - (canActivate) => { + guard + .canActivate(new ActivatedRouteSnapshot(), {} as RouterStateSnapshot) + .then(canActivate => { expect(canActivate).toBeFalse(); expect(getUserInfoAsyncSpy).toHaveBeenCalledTimes(1); expect(navigateSpy).toHaveBeenCalledWith([ - `${AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ERROR.ROUTE}/401`]); + `${AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ERROR.ROUTE}/401`, + ]); done(); }); }); - it('should not redirect user to 401 page if user is cd-admin', (done) => { + it('should not redirect user to 401 page if user is cd-admin', done => { const getUserInfoAsyncSpy = spyOn( - userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(new UserInfo( - ['USER_ROLE', 'TRANSLATION_COORDINATOR'], true, false, false, false, - true, 'en', null, 'tester@example.com', true - )) + userService, + 'getUserInfoAsync' + ).and.returnValue( + Promise.resolve( + new UserInfo( + ['USER_ROLE', 'TRANSLATION_COORDINATOR'], + true, + false, + false, + false, + true, + 'en', + null, + 'tester@example.com', + true + ) + ) ); const navigateSpy = spyOn(router, 'navigate').and.callThrough(); - guard.canActivate( - new ActivatedRouteSnapshot(), {} as RouterStateSnapshot).then( - (canActivate) => { + guard + .canActivate(new ActivatedRouteSnapshot(), {} as RouterStateSnapshot) + .then(canActivate => { expect(canActivate).toBeTrue(); expect(getUserInfoAsyncSpy).toHaveBeenCalledTimes(1); expect(navigateSpy).not.toHaveBeenCalled(); diff --git a/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-auth.guard.ts b/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-auth.guard.ts index 66fc43baa75c..3695377228e9 100644 --- a/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-auth.guard.ts +++ b/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-auth.guard.ts @@ -18,8 +18,8 @@ * contributor-dashboard admin page. */ -import { Location } from '@angular/common'; -import { Injectable } from '@angular/core'; +import {Location} from '@angular/common'; +import {Injectable} from '@angular/core'; import { ActivatedRouteSnapshot, CanActivate, @@ -27,11 +27,11 @@ import { RouterStateSnapshot, } from '@angular/router'; -import { AppConstants } from 'app.constants'; -import { UserService } from 'services/user.service'; +import {AppConstants} from 'app.constants'; +import {UserService} from 'services/user.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ContributorDashboardAdminAuthGuard implements CanActivate { constructor( @@ -41,20 +41,26 @@ export class ContributorDashboardAdminAuthGuard implements CanActivate { ) {} async canActivate( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot ): Promise { const userInfo = await this.userService.getUserInfoAsync(); - if (userInfo.isTranslationAdmin() || userInfo.isQuestionAdmin() || - userInfo.isQuestionCoordinator() || userInfo.isTranslationCoordinator()) { + if ( + userInfo.isTranslationAdmin() || + userInfo.isQuestionAdmin() || + userInfo.isQuestionCoordinator() || + userInfo.isTranslationCoordinator() + ) { return true; } - this.router.navigate( - [`${AppConstants.PAGES_REGISTERED_WITH_FRONTEND - .ERROR.ROUTE}/401`]).then(() => { - this.location.replaceState(state.url); - }); + this.router + .navigate([ + `${AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ERROR.ROUTE}/401`, + ]) + .then(() => { + this.location.replaceState(state.url); + }); return false; } } diff --git a/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-page-root.component.spec.ts b/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-page-root.component.spec.ts index d3cf38981a23..7ab53fd25e84 100644 --- a/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-page-root.component.spec.ts +++ b/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-page-root.component.spec.ts @@ -16,14 +16,14 @@ * @fileoverview Unit tests for Contributor Dashboard Admin Page Root component. */ -import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { TranslateModule } from '@ngx-translate/core'; +import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {TranslateModule} from '@ngx-translate/core'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; -import { ContributorDashboardAdminPageRootComponent } from './contributor-dashboard-admin-page-root.component'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; +import {ContributorDashboardAdminPageRootComponent} from './contributor-dashboard-admin-page-root.component'; describe('AdminPageRootComponent', () => { let fixture: ComponentFixture; @@ -45,12 +45,12 @@ describe('AdminPageRootComponent', () => { it('should have the title and meta tags set', () => { expect(component.title).toEqual( - AppConstants. - PAGES_REGISTERED_WITH_FRONTEND.CONTRIBUTOR_DASHBOARD_ADMIN.TITLE + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.CONTRIBUTOR_DASHBOARD_ADMIN + .TITLE ); expect(component.meta).toEqual( - AppConstants. - PAGES_REGISTERED_WITH_FRONTEND.CONTRIBUTOR_DASHBOARD_ADMIN.META + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.CONTRIBUTOR_DASHBOARD_ADMIN + .META ); }); }); diff --git a/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-page-root.component.ts b/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-page-root.component.ts index 0a12f7c2a1b4..6a6746a0f21c 100644 --- a/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-page-root.component.ts +++ b/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-page-root.component.ts @@ -16,20 +16,19 @@ * @fileoverview Contributor Dashboard Admin page root component. */ -import { Component } from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { BaseRootComponent, MetaTagData } from 'pages/base-root.component'; +import {Component} from '@angular/core'; +import {AppConstants} from 'app.constants'; +import {BaseRootComponent, MetaTagData} from 'pages/base-root.component'; @Component({ selector: 'oppia-contributor-dashboard-admin-page-root', templateUrl: './contributor-dashboard-admin-page-root.component.html', }) -export class ContributorDashboardAdminPageRootComponent - extends BaseRootComponent { - title: string = AppConstants.PAGES_REGISTERED_WITH_FRONTEND - .CONTRIBUTOR_DASHBOARD_ADMIN.TITLE; +export class ContributorDashboardAdminPageRootComponent extends BaseRootComponent { + title: string = + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.CONTRIBUTOR_DASHBOARD_ADMIN + .TITLE; - meta: MetaTagData[] = - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ADMIN.META as - unknown as Readonly[]; + meta: MetaTagData[] = AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ADMIN + .META as unknown as Readonly[]; } diff --git a/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-page.component.spec.ts b/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-page.component.spec.ts index c067c77863c9..51c41f14f517 100644 --- a/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-page.component.spec.ts +++ b/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-page.component.spec.ts @@ -12,25 +12,30 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Unit tests for the Contributor dashboard admin page. */ -import { ComponentFixture, TestBed, fakeAsync, tick, waitForAsync } from '@angular/core/testing'; -import { UserService } from 'services/user.service'; -import { ContributorDashboardAdminPageComponent } from './contributor-dashboard-admin-page.component'; -import { ContributorDashboardAdminBackendApiService } from './services/contributor-dashboard-admin-backend-api.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { PlatformFeatureService } from 'services/platform-feature.service'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { UserInfo } from 'domain/user/user-info.model'; +import { + ComponentFixture, + TestBed, + fakeAsync, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {UserService} from 'services/user.service'; +import {ContributorDashboardAdminPageComponent} from './contributor-dashboard-admin-page.component'; +import {ContributorDashboardAdminBackendApiService} from './services/contributor-dashboard-admin-backend-api.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {PlatformFeatureService} from 'services/platform-feature.service'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {UserInfo} from 'domain/user/user-info.model'; class MockPlatformFeatureService { status = { CdAdminDashboardNewUi: { - isEnabled: false - } + isEnabled: false, + }, }; } @@ -39,26 +44,23 @@ describe('ContributorDashboardAdminPageComponent', () => { let component: ContributorDashboardAdminPageComponent; let userService: UserService; let mockPlatformFeatureService = new MockPlatformFeatureService(); - let contributorDashboardAdminBackendApiService: - ContributorDashboardAdminBackendApiService; + let contributorDashboardAdminBackendApiService: ContributorDashboardAdminBackendApiService; let userInfo: UserInfo; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - ContributorDashboardAdminPageComponent - ], + declarations: [ContributorDashboardAdminPageComponent], providers: [ UserService, ContributorDashboardAdminBackendApiService, { provide: PlatformFeatureService, - useValue: mockPlatformFeatureService - } + useValue: mockPlatformFeatureService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -66,8 +68,9 @@ describe('ContributorDashboardAdminPageComponent', () => { fixture = TestBed.createComponent(ContributorDashboardAdminPageComponent); component = fixture.componentInstance; userService = TestBed.inject(UserService); - contributorDashboardAdminBackendApiService = - TestBed.inject(ContributorDashboardAdminBackendApiService); + contributorDashboardAdminBackendApiService = TestBed.inject( + ContributorDashboardAdminBackendApiService + ); userInfo = { _roles: ['USER_ROLE'], @@ -92,13 +95,14 @@ describe('ContributorDashboardAdminPageComponent', () => { getPreferredSiteLanguageCode: () => 'en', getUsername: () => 'username1', getEmail: () => 'tester@example.org', - isLoggedIn: () => true + isLoggedIn: () => true, } as UserInfo; }); it('should fetch user info when initialized', fakeAsync(() => { - const userInfoSpy = spyOn(userService, 'getUserInfoAsync') - .and.returnValue(Promise.resolve(userInfo)); + const userInfoSpy = spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve(userInfo) + ); component.ngOnInit(); tick(); @@ -107,8 +111,7 @@ describe('ContributorDashboardAdminPageComponent', () => { })); it('should account for feature flag when initialized', fakeAsync(() => { - mockPlatformFeatureService.status.CdAdminDashboardNewUi.isEnabled = ( - true); + mockPlatformFeatureService.status.CdAdminDashboardNewUi.isEnabled = true; component.ngOnInit(); tick(); @@ -120,13 +123,13 @@ describe('ContributorDashboardAdminPageComponent', () => { it('should successfully update the rights of the user', fakeAsync(() => { const contributorDashboardAdminBackendApiServiceSpy = spyOn( contributorDashboardAdminBackendApiService, - 'addContributionReviewerAsync') - .and.returnValue(Promise.resolve()); + 'addContributionReviewerAsync' + ).and.returnValue(Promise.resolve()); const addContributionRightsAction = { category: 'translation', isValid: () => true, languageCode: 'en', - username: 'user1' + username: 'user1', }; component.submitAddContributionRightsForm(addContributionRightsAction); @@ -136,205 +139,241 @@ describe('ContributorDashboardAdminPageComponent', () => { expect(component.statusMessage).toBe('Success.'); })); - it('should not send request to backend if a task ' + - 'is still running in the queue', fakeAsync(() => { - // Setting task running to be true. - component.taskRunningInBackground = true; - const contributorDashboardAdminBackendApiServiceSpy = spyOn( - contributorDashboardAdminBackendApiService, - 'addContributionReviewerAsync') - .and.returnValue(Promise.resolve()); - const addContributionRightsAction = { - category: 'translation', - isValid: () => true, - languageCode: 'en', - username: 'user1' - }; - - component.submitAddContributionRightsForm(addContributionRightsAction); - tick(); - - expect(contributorDashboardAdminBackendApiServiceSpy) - .not.toHaveBeenCalled(); - })); - - it('should not update the rights of the user in case of a backend error', + it( + 'should not send request to backend if a task ' + + 'is still running in the queue', fakeAsync(() => { + // Setting task running to be true. + component.taskRunningInBackground = true; const contributorDashboardAdminBackendApiServiceSpy = spyOn( contributorDashboardAdminBackendApiService, - 'addContributionReviewerAsync') - .and.returnValue(Promise.reject( - 'User user1 already has rights' + - ' to review translation in language code en')); + 'addContributionReviewerAsync' + ).and.returnValue(Promise.resolve()); const addContributionRightsAction = { category: 'translation', isValid: () => true, languageCode: 'en', - username: 'user1' + username: 'user1', }; component.submitAddContributionRightsForm(addContributionRightsAction); tick(); expect( - contributorDashboardAdminBackendApiServiceSpy).toHaveBeenCalled(); - expect(component.statusMessage).toBe( - 'Server error: User user1 already has rights' + - ' to review translation in language code en'); - })); + contributorDashboardAdminBackendApiServiceSpy + ).not.toHaveBeenCalled(); + }) + ); + + it('should not update the rights of the user in case of a backend error', fakeAsync(() => { + const contributorDashboardAdminBackendApiServiceSpy = spyOn( + contributorDashboardAdminBackendApiService, + 'addContributionReviewerAsync' + ).and.returnValue( + Promise.reject( + 'User user1 already has rights' + + ' to review translation in language code en' + ) + ); + const addContributionRightsAction = { + category: 'translation', + isValid: () => true, + languageCode: 'en', + username: 'user1', + }; + + component.submitAddContributionRightsForm(addContributionRightsAction); + tick(); + + expect(contributorDashboardAdminBackendApiServiceSpy).toHaveBeenCalled(); + expect(component.statusMessage).toBe( + 'Server error: User user1 already has rights' + + ' to review translation in language code en' + ); + })); }); describe('in the add contribution rights section ', () => { - it('should return true if there are no validation errors ' + - 'when updating user rights for category translation', fakeAsync(() => { - component.ngOnInit(); - // Setting category to be translation. - component.formData.addContributionReviewer.category = 'translation'; - component.formData.addContributionReviewer.languageCode = 'en'; - component.formData.addContributionReviewer.username = 'user1'; + it( + 'should return true if there are no validation errors ' + + 'when updating user rights for category translation', + fakeAsync(() => { + component.ngOnInit(); + // Setting category to be translation. + component.formData.addContributionReviewer.category = 'translation'; + component.formData.addContributionReviewer.languageCode = 'en'; + component.formData.addContributionReviewer.username = 'user1'; - const result = component.formData.addContributionReviewer.isValid(); + const result = component.formData.addContributionReviewer.isValid(); - expect(result).toBe(true); - })); + expect(result).toBe(true); + }) + ); - it('should return true if there are no validation errors ' + - 'when updating user rights for category voiceover', fakeAsync(() => { - component.ngOnInit(); - // Setting category to be voiceover. - component.formData.addContributionReviewer.category = 'voiceOver'; - component.formData.addContributionReviewer.username = 'user1'; + it( + 'should return true if there are no validation errors ' + + 'when updating user rights for category voiceover', + fakeAsync(() => { + component.ngOnInit(); + // Setting category to be voiceover. + component.formData.addContributionReviewer.category = 'voiceOver'; + component.formData.addContributionReviewer.username = 'user1'; - const result = component.formData.addContributionReviewer.isValid(); + const result = component.formData.addContributionReviewer.isValid(); - expect(result).toBe(true); - })); + expect(result).toBe(true); + }) + ); - it('should return false if there are validation errors ' + - 'when updating user rights', fakeAsync(() => { - component.ngOnInit(); - // Setting category to be null. - component.formData.addContributionReviewer.category = null; - component.formData.addContributionReviewer.username = 'user1'; + it( + 'should return false if there are validation errors ' + + 'when updating user rights', + fakeAsync(() => { + component.ngOnInit(); + // Setting category to be null. + component.formData.addContributionReviewer.category = null; + component.formData.addContributionReviewer.username = 'user1'; - const result = component.formData.addContributionReviewer.isValid(); + const result = component.formData.addContributionReviewer.isValid(); - expect(result).toBe(false); - })); + expect(result).toBe(false); + }) + ); - it('should return false if user name is empty ' + - 'when updating user rights', fakeAsync(() => { - component.ngOnInit(); - // Setting username to be empty. - component.formData.addContributionReviewer.username = ''; + it( + 'should return false if user name is empty ' + + 'when updating user rights', + fakeAsync(() => { + component.ngOnInit(); + // Setting username to be empty. + component.formData.addContributionReviewer.username = ''; - const result = component.formData.addContributionReviewer.isValid(); + const result = component.formData.addContributionReviewer.isValid(); - expect(result).toBe(false); - })); + expect(result).toBe(false); + }) + ); }); describe('on clicking view contributors button ', () => { - it('should successfully show rights of a user given the ' + - 'username', fakeAsync(() => { - // Note that username is filter criterion here. - const viewContributionReviewersAction = { - category: 'category', - filterCriterion: 'username', - isValid: () => true, - languageCode: 'en', - username: 'user1' - }; - const viewContributorsResponse = { - can_review_questions: true, - can_review_translation_for_language_codes: ['en', 'es'], - can_review_voiceover_for_language_codes: ['en', 'es'], - can_submit_questions: true - }; - spyOn(userService, 'getUserInfoAsync') - .and.returnValue(Promise.resolve(userInfo)); - spyOn( - contributorDashboardAdminBackendApiService, - 'contributionReviewerRightsAsync') - .and.returnValue(Promise.resolve(viewContributorsResponse)); - - expect(component.contributionReviewersDataFetched).toBe(false); + it( + 'should successfully show rights of a user given the ' + 'username', + fakeAsync(() => { + // Note that username is filter criterion here. + const viewContributionReviewersAction = { + category: 'category', + filterCriterion: 'username', + isValid: () => true, + languageCode: 'en', + username: 'user1', + }; + const viewContributorsResponse = { + can_review_questions: true, + can_review_translation_for_language_codes: ['en', 'es'], + can_review_voiceover_for_language_codes: ['en', 'es'], + can_submit_questions: true, + }; + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve(userInfo) + ); + spyOn( + contributorDashboardAdminBackendApiService, + 'contributionReviewerRightsAsync' + ).and.returnValue(Promise.resolve(viewContributorsResponse)); - component.ngOnInit(); - tick(); + expect(component.contributionReviewersDataFetched).toBe(false); - component.submitViewContributorUsersForm(viewContributionReviewersAction); - tick(); + component.ngOnInit(); + tick(); - expect(component.contributionReviewersDataFetched).toBe(true); - expect(component.statusMessage).toBe('Success.'); - })); + component.submitViewContributorUsersForm( + viewContributionReviewersAction + ); + tick(); - it('should successfully show users given the ' + - 'contributor rights', fakeAsync(() => { - // Note that rights is filter criterion here. - const viewContributionReviewersAction = { - category: 'category', - filterCriterion: 'role', - isValid: () => true, - languageCode: 'en', - username: 'user1' - }; - const viewContributorsResponse = { - usernames: ['user1'] - }; - spyOn(userService, 'getUserInfoAsync') - .and.returnValue(Promise.resolve(userInfo)); - spyOn( - contributorDashboardAdminBackendApiService, - 'viewContributionReviewersAsync') - .and.returnValue(Promise.resolve(viewContributorsResponse)); + expect(component.contributionReviewersDataFetched).toBe(true); + expect(component.statusMessage).toBe('Success.'); + }) + ); - expect(component.contributionReviewersResult).toEqual({ }); - expect(component.contributionReviewersDataFetched).toBe(false); + it( + 'should successfully show users given the ' + 'contributor rights', + fakeAsync(() => { + // Note that rights is filter criterion here. + const viewContributionReviewersAction = { + category: 'category', + filterCriterion: 'role', + isValid: () => true, + languageCode: 'en', + username: 'user1', + }; + const viewContributorsResponse = { + usernames: ['user1'], + }; + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve(userInfo) + ); + spyOn( + contributorDashboardAdminBackendApiService, + 'viewContributionReviewersAsync' + ).and.returnValue(Promise.resolve(viewContributorsResponse)); - component.ngOnInit(); - tick(); - component.submitViewContributorUsersForm(viewContributionReviewersAction); - tick(); + expect(component.contributionReviewersResult).toEqual({}); + expect(component.contributionReviewersDataFetched).toBe(false); - expect(component.contributionReviewersResult.usernames) - .toEqual(viewContributorsResponse.usernames); - expect(component.contributionReviewersDataFetched).toBe(true); - expect(component.statusMessage).toBe('Success.'); - })); + component.ngOnInit(); + tick(); + component.submitViewContributorUsersForm( + viewContributionReviewersAction + ); + tick(); - it('should not send request to backend if a task ' + - 'is still running in the queue', fakeAsync(() => { - // Setting task running to be true. - component.taskRunningInBackground = true; - const viewContributorsResponse = { - usernames: ['user1'] - }; - spyOn(userService, 'getUserInfoAsync') - .and.returnValue(Promise.resolve(userInfo)); - const contributorDashboardAdminBackendApiServiceSpy = spyOn( - contributorDashboardAdminBackendApiService, - 'viewContributionReviewersAsync') - .and.resolveTo(viewContributorsResponse); - const viewContributionReviewersAction = { - category: 'category', - filterCriterion: 'username', - isValid: () => true, - languageCode: 'en', - username: 'user1' - }; - expect(component.contributionReviewersDataFetched).toBe(false); + expect(component.contributionReviewersResult.usernames).toEqual( + viewContributorsResponse.usernames + ); + expect(component.contributionReviewersDataFetched).toBe(true); + expect(component.statusMessage).toBe('Success.'); + }) + ); + + it( + 'should not send request to backend if a task ' + + 'is still running in the queue', + fakeAsync(() => { + // Setting task running to be true. + component.taskRunningInBackground = true; + const viewContributorsResponse = { + usernames: ['user1'], + }; + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve(userInfo) + ); + const contributorDashboardAdminBackendApiServiceSpy = spyOn( + contributorDashboardAdminBackendApiService, + 'viewContributionReviewersAsync' + ).and.resolveTo(viewContributorsResponse); + const viewContributionReviewersAction = { + category: 'category', + filterCriterion: 'username', + isValid: () => true, + languageCode: 'en', + username: 'user1', + }; + expect(component.contributionReviewersDataFetched).toBe(false); - component.ngOnInit(); - tick(); - component.submitViewContributorUsersForm(viewContributionReviewersAction); - tick(); + component.ngOnInit(); + tick(); + component.submitViewContributorUsersForm( + viewContributionReviewersAction + ); + tick(); - expect(contributorDashboardAdminBackendApiServiceSpy) - .not.toHaveBeenCalled(); - expect(component.contributionReviewersDataFetched).toBe(false); - })); + expect( + contributorDashboardAdminBackendApiServiceSpy + ).not.toHaveBeenCalled(); + expect(component.contributionReviewersDataFetched).toBe(false); + }) + ); it('should handle backend failure appropriately', fakeAsync(() => { const viewContributionReviewersAction = { @@ -342,20 +381,21 @@ describe('ContributorDashboardAdminPageComponent', () => { filterCriterion: 'username', isValid: () => true, languageCode: 'en', - username: 'random' + username: 'random', }; const contributorDashboardAdminBackendApiServiceSpy = spyOn( contributorDashboardAdminBackendApiService, - 'contributionReviewerRightsAsync') - .and.returnValue(Promise.reject('Invalid username: random')); + 'contributionReviewerRightsAsync' + ).and.returnValue(Promise.reject('Invalid username: random')); component.submitViewContributorUsersForm(viewContributionReviewersAction); tick(); expect(contributorDashboardAdminBackendApiServiceSpy).toHaveBeenCalled(); expect(component.statusMessage).toBe( - 'Server error: Invalid username: random'); + 'Server error: Invalid username: random' + ); })); }); @@ -363,293 +403,350 @@ describe('ContributorDashboardAdminPageComponent', () => { it('should successfully remove the rights of the user', fakeAsync(() => { const contributorDashboardAdminBackendApiServiceSpy = spyOn( contributorDashboardAdminBackendApiService, - 'removeContributionReviewerAsync') - .and.returnValue(Promise.resolve()); + 'removeContributionReviewerAsync' + ).and.returnValue(Promise.resolve()); const removeContributionRightsAction = { category: 'translation', isValid: () => true, languageCode: 'en', method: 'all', - username: 'user1' + username: 'user1', }; component.ngOnInit(); component.submitRemoveContributionRightsForm( - removeContributionRightsAction); + removeContributionRightsAction + ); tick(); expect(contributorDashboardAdminBackendApiServiceSpy).toHaveBeenCalled(); expect(component.statusMessage).toBe('Success.'); })); - it('should not send request to backend if a task ' + - 'is still running in the queue', fakeAsync(() => { - // Setting task running to be true. - component.taskRunningInBackground = true; - const contributorDashboardAdminBackendApiServiceSpy = spyOn( - contributorDashboardAdminBackendApiService, - 'removeContributionReviewerAsync') - .and.returnValue(Promise.resolve()); - const removeContributionRightsAction = { - category: 'translation', - isValid: () => true, - languageCode: 'en', - method: 'all', - username: 'user1' - }; - - component.ngOnInit(); - component.submitRemoveContributionRightsForm( - removeContributionRightsAction); - tick(); - - expect(contributorDashboardAdminBackendApiServiceSpy) - .not.toHaveBeenCalled(); - })); - - it('should not remove the rights of the user in case of a backend error', + it( + 'should not send request to backend if a task ' + + 'is still running in the queue', fakeAsync(() => { + // Setting task running to be true. + component.taskRunningInBackground = true; const contributorDashboardAdminBackendApiServiceSpy = spyOn( contributorDashboardAdminBackendApiService, - 'removeContributionReviewerAsync') - .and.returnValue(Promise.reject('Invalid username: random')); + 'removeContributionReviewerAsync' + ).and.returnValue(Promise.resolve()); const removeContributionRightsAction = { category: 'translation', isValid: () => true, languageCode: 'en', method: 'all', - username: 'random' + username: 'user1', }; component.ngOnInit(); component.submitRemoveContributionRightsForm( - removeContributionRightsAction); + removeContributionRightsAction + ); tick(); expect( - contributorDashboardAdminBackendApiServiceSpy).toHaveBeenCalled(); - expect(component.statusMessage).toBe( - 'Server error: Invalid username: random'); - })); - }); + contributorDashboardAdminBackendApiServiceSpy + ).not.toHaveBeenCalled(); + }) + ); - describe('on clicking \'View Translation Stats\' button ', () => { - it('should successfully show the Translation contribution ' + - 'stats', fakeAsync(() => { + it('should not remove the rights of the user in case of a backend error', fakeAsync(() => { const contributorDashboardAdminBackendApiServiceSpy = spyOn( contributorDashboardAdminBackendApiService, - 'viewTranslationContributionStatsAsync') - .and.returnValue(Promise.resolve({ - translation_contribution_stats: [] - })); - const viewTranslationAction = { + 'removeContributionReviewerAsync' + ).and.returnValue(Promise.reject('Invalid username: random')); + const removeContributionRightsAction = { + category: 'translation', isValid: () => true, - username: 'user1' + languageCode: 'en', + method: 'all', + username: 'random', }; component.ngOnInit(); - component.submitViewTranslationContributionStatsForm( - viewTranslationAction); + component.submitRemoveContributionRightsForm( + removeContributionRightsAction + ); tick(); expect(contributorDashboardAdminBackendApiServiceSpy).toHaveBeenCalled(); - expect(component.statusMessage).toBe('Success.'); - })); - - it('should not send request to backend if a task ' + - 'is still running in the queue', fakeAsync(() => { - // Setting task running to be true. - component.taskRunningInBackground = true; - const contributorDashboardAdminBackendApiServiceSpy = spyOn( - contributorDashboardAdminBackendApiService, - 'viewTranslationContributionStatsAsync') - .and.returnValue(Promise.resolve()); - const viewTranslationAction = { - isValid: () => true, - username: 'user1' - }; - - component.ngOnInit(); - component.submitViewTranslationContributionStatsForm( - viewTranslationAction); - tick(); - - expect(contributorDashboardAdminBackendApiServiceSpy) - .not.toHaveBeenCalled(); - })); - - it('should show error message in case of ' + - 'backend error', fakeAsync(() => { - spyOn( - contributorDashboardAdminBackendApiService, - 'viewTranslationContributionStatsAsync') - .and.returnValue(Promise.reject('Internal Server Error.')); - const viewTranslationAction = { - isValid: () => true, - username: 'user1' - }; - - component.ngOnInit(); - component.submitViewTranslationContributionStatsForm( - viewTranslationAction); - tick(); - - expect(component.statusMessage) - .toBe('Server error: Internal Server Error.'); + expect(component.statusMessage).toBe( + 'Server error: Invalid username: random' + ); })); }); - // Note that 'refreshFormData()' is called whenever a change - // is detected in any of the forms. - describe('on validating form data ', () => { - describe('in the view contributor dashboard users section ', () => { - it('should return true if there are no validation errors ' + - 'when fetching user rights with filter criterion as ' + - 'rights and category as voiceover', fakeAsync(() => { - component.ngOnInit(); - - // Note that rights is filter criterion here. - component.formData.viewContributionReviewers.filterCriterion = 'role'; - component.formData.viewContributionReviewers.category = 'voiceOver'; - component.formData.viewContributionReviewers.username = 'user1'; - - const result = component.formData.viewContributionReviewers.isValid(); - - expect(result).toBe(true); - })); + describe("on clicking 'View Translation Stats' button ", () => { + it( + 'should successfully show the Translation contribution ' + 'stats', + fakeAsync(() => { + const contributorDashboardAdminBackendApiServiceSpy = spyOn( + contributorDashboardAdminBackendApiService, + 'viewTranslationContributionStatsAsync' + ).and.returnValue( + Promise.resolve({ + translation_contribution_stats: [], + }) + ); + const viewTranslationAction = { + isValid: () => true, + username: 'user1', + }; - it('should return false if there are no validation errors ' + - 'when fetching user rights', fakeAsync(() => { component.ngOnInit(); + component.submitViewTranslationContributionStatsForm( + viewTranslationAction + ); + tick(); - // Note that rights is filter criterion here. - component.formData.viewContributionReviewers.filterCriterion = 'role'; - // Setting category to null. - component.formData.viewContributionReviewers.category = null; - - const result = component.formData.viewContributionReviewers.isValid(); - - expect(result).toBe(false); - })); + expect( + contributorDashboardAdminBackendApiServiceSpy + ).toHaveBeenCalled(); + expect(component.statusMessage).toBe('Success.'); + }) + ); + + it( + 'should not send request to backend if a task ' + + 'is still running in the queue', + fakeAsync(() => { + // Setting task running to be true. + component.taskRunningInBackground = true; + const contributorDashboardAdminBackendApiServiceSpy = spyOn( + contributorDashboardAdminBackendApiService, + 'viewTranslationContributionStatsAsync' + ).and.returnValue(Promise.resolve()); + const viewTranslationAction = { + isValid: () => true, + username: 'user1', + }; - it('should return true if there are no validation errors ' + - 'when fetching user rights with filter criterion as ' + - 'rights and category as translation', fakeAsync(() => { component.ngOnInit(); + component.submitViewTranslationContributionStatsForm( + viewTranslationAction + ); + tick(); - // Note that rights is filter criterion here. - component.formData.viewContributionReviewers.filterCriterion = 'role'; - component.formData.viewContributionReviewers.category = 'translation'; - component.formData.viewContributionReviewers.languageCode = 'en'; - component.formData.viewContributionReviewers.username = 'user1'; - - const result = component.formData.viewContributionReviewers.isValid(); + expect( + contributorDashboardAdminBackendApiServiceSpy + ).not.toHaveBeenCalled(); + }) + ); - expect(result).toBe(true); - })); + it( + 'should show error message in case of ' + 'backend error', + fakeAsync(() => { + spyOn( + contributorDashboardAdminBackendApiService, + 'viewTranslationContributionStatsAsync' + ).and.returnValue(Promise.reject('Internal Server Error.')); + const viewTranslationAction = { + isValid: () => true, + username: 'user1', + }; - it('should return true if there are no validation errors ' + - 'when fetching user rights with filter criterion as ' + - 'username', fakeAsync(() => { component.ngOnInit(); + component.submitViewTranslationContributionStatsForm( + viewTranslationAction + ); + tick(); - // Note that username is filter criterion here. - component.formData.viewContributionReviewers.filterCriterion = ( - 'username'); - component.formData.viewContributionReviewers.username = 'user1'; - - const result = component.formData.viewContributionReviewers.isValid(); + expect(component.statusMessage).toBe( + 'Server error: Internal Server Error.' + ); + }) + ); + }); - expect(result).toBe(true); - })); + // Note that 'refreshFormData()' is called whenever a change + // is detected in any of the forms. + describe('on validating form data ', () => { + describe('in the view contributor dashboard users section ', () => { + it( + 'should return true if there are no validation errors ' + + 'when fetching user rights with filter criterion as ' + + 'rights and category as voiceover', + fakeAsync(() => { + component.ngOnInit(); + + // Note that rights is filter criterion here. + component.formData.viewContributionReviewers.filterCriterion = 'role'; + component.formData.viewContributionReviewers.category = 'voiceOver'; + component.formData.viewContributionReviewers.username = 'user1'; + + const result = component.formData.viewContributionReviewers.isValid(); + + expect(result).toBe(true); + }) + ); + + it( + 'should return false if there are no validation errors ' + + 'when fetching user rights', + fakeAsync(() => { + component.ngOnInit(); + + // Note that rights is filter criterion here. + component.formData.viewContributionReviewers.filterCriterion = 'role'; + // Setting category to null. + component.formData.viewContributionReviewers.category = null; + + const result = component.formData.viewContributionReviewers.isValid(); + + expect(result).toBe(false); + }) + ); + + it( + 'should return true if there are no validation errors ' + + 'when fetching user rights with filter criterion as ' + + 'rights and category as translation', + fakeAsync(() => { + component.ngOnInit(); + + // Note that rights is filter criterion here. + component.formData.viewContributionReviewers.filterCriterion = 'role'; + component.formData.viewContributionReviewers.category = 'translation'; + component.formData.viewContributionReviewers.languageCode = 'en'; + component.formData.viewContributionReviewers.username = 'user1'; + + const result = component.formData.viewContributionReviewers.isValid(); + + expect(result).toBe(true); + }) + ); + + it( + 'should return true if there are no validation errors ' + + 'when fetching user rights with filter criterion as ' + + 'username', + fakeAsync(() => { + component.ngOnInit(); + + // Note that username is filter criterion here. + component.formData.viewContributionReviewers.filterCriterion = + 'username'; + component.formData.viewContributionReviewers.username = 'user1'; + + const result = component.formData.viewContributionReviewers.isValid(); + + expect(result).toBe(true); + }) + ); }); describe('in the view translation contribution stats section ', () => { - it('should return true if there are no validation errors ' + - 'when fetching translation stats', fakeAsync(() => { - component.ngOnInit(); - component.formData.viewTranslationContributionStats.username = 'user1'; - - const result = - component.formData.viewTranslationContributionStats.isValid(); - - expect(result).toBe(true); - })); - - it('should return false if there are validation errors ' + - 'when fetching translation stats', fakeAsync(() => { - component.ngOnInit(); - - // Setting user name as empty. - component.formData.viewTranslationContributionStats.username = ''; - - const result = - component.formData.viewTranslationContributionStats.isValid(); - - expect(result).toBe(false); - })); + it( + 'should return true if there are no validation errors ' + + 'when fetching translation stats', + fakeAsync(() => { + component.ngOnInit(); + component.formData.viewTranslationContributionStats.username = + 'user1'; + + const result = + component.formData.viewTranslationContributionStats.isValid(); + + expect(result).toBe(true); + }) + ); + + it( + 'should return false if there are validation errors ' + + 'when fetching translation stats', + fakeAsync(() => { + component.ngOnInit(); + + // Setting user name as empty. + component.formData.viewTranslationContributionStats.username = ''; + + const result = + component.formData.viewTranslationContributionStats.isValid(); + + expect(result).toBe(false); + }) + ); }); describe('in the remove contribution rights section ', () => { - it('should return true if there are no validation errors ' + - 'when removing user rights for all categories', fakeAsync(() => { - component.ngOnInit(); - - // Setting method to all. - component.formData.removeContributionReviewer.category = 'all'; - component.formData.removeContributionReviewer.languageCode = 'en'; - component.formData.removeContributionReviewer.username = 'user1'; - component.formData.removeContributionReviewer.method = 'all'; - - const result = component.formData.removeContributionReviewer.isValid(); - - expect(result).toBe(true); - })); - - it('should return true if there are no validation errors ' + - 'when removing user rights for category translation', fakeAsync(() => { - component.ngOnInit(); - // Setting category to translation. - component.formData.removeContributionReviewer.category = 'translation'; - component.formData.removeContributionReviewer.languageCode = 'en'; - component.formData.removeContributionReviewer.username = 'user1'; - component.formData.removeContributionReviewer.method = 'specific'; - - - const result = component.formData.removeContributionReviewer.isValid(); - - expect(result).toBe(true); - })); - - it('should return true if there are no validation errors ' + - 'when removing user rights for category voiceover', fakeAsync(() => { - component.ngOnInit(); - // Setting category to voiceover. - component.formData.removeContributionReviewer.category = 'voiceOver'; - component.formData.removeContributionReviewer.username = 'user1'; - component.formData.removeContributionReviewer.method = 'specific'; - - const result = component.formData.removeContributionReviewer.isValid(); - - expect(result).toBe(true); - })); - - it('should return false if there are validation errors ' + - 'when removing user rights', fakeAsync(() => { - component.ngOnInit(); - // Setting category to be null. - component.formData.removeContributionReviewer.category = null; - component.formData.removeContributionReviewer.username = 'user1'; - component.formData.removeContributionReviewer.method = 'specific'; - - const result = component.formData.removeContributionReviewer.isValid(); - - expect(result).toBe(false); - })); + it( + 'should return true if there are no validation errors ' + + 'when removing user rights for all categories', + fakeAsync(() => { + component.ngOnInit(); + + // Setting method to all. + component.formData.removeContributionReviewer.category = 'all'; + component.formData.removeContributionReviewer.languageCode = 'en'; + component.formData.removeContributionReviewer.username = 'user1'; + component.formData.removeContributionReviewer.method = 'all'; + + const result = + component.formData.removeContributionReviewer.isValid(); + + expect(result).toBe(true); + }) + ); + + it( + 'should return true if there are no validation errors ' + + 'when removing user rights for category translation', + fakeAsync(() => { + component.ngOnInit(); + // Setting category to translation. + component.formData.removeContributionReviewer.category = + 'translation'; + component.formData.removeContributionReviewer.languageCode = 'en'; + component.formData.removeContributionReviewer.username = 'user1'; + component.formData.removeContributionReviewer.method = 'specific'; + + const result = + component.formData.removeContributionReviewer.isValid(); + + expect(result).toBe(true); + }) + ); + + it( + 'should return true if there are no validation errors ' + + 'when removing user rights for category voiceover', + fakeAsync(() => { + component.ngOnInit(); + // Setting category to voiceover. + component.formData.removeContributionReviewer.category = 'voiceOver'; + component.formData.removeContributionReviewer.username = 'user1'; + component.formData.removeContributionReviewer.method = 'specific'; + + const result = + component.formData.removeContributionReviewer.isValid(); + + expect(result).toBe(true); + }) + ); + + it( + 'should return false if there are validation errors ' + + 'when removing user rights', + fakeAsync(() => { + component.ngOnInit(); + // Setting category to be null. + component.formData.removeContributionReviewer.category = null; + component.formData.removeContributionReviewer.username = 'user1'; + component.formData.removeContributionReviewer.method = 'specific'; + + const result = + component.formData.removeContributionReviewer.isValid(); + + expect(result).toBe(false); + }) + ); }); }); - it('should clear result when calling \'clearResults\'', () => { + it("should clear result when calling 'clearResults'", () => { component.contributionReviewersDataFetched = true; component.contributionReviewersResult = { usernames: ['property1', 'property2'], diff --git a/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-page.component.ts b/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-page.component.ts index 6abc63e227dd..aa37429641f0 100644 --- a/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-page.component.ts +++ b/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-page.component.ts @@ -16,12 +16,12 @@ * @fileoverview Contributor dashboard admin page component. */ -import { Component, OnInit } from '@angular/core'; -import { LanguageUtilService } from 'domain/utilities/language-util.service'; -import { UserService } from 'services/user.service'; -import { PlatformFeatureService } from 'services/platform-feature.service'; -import { ContributorDashboardAdminBackendApiService } from './services/contributor-dashboard-admin-backend-api.service'; -import { AppConstants } from 'app.constants'; +import {Component, OnInit} from '@angular/core'; +import {LanguageUtilService} from 'domain/utilities/language-util.service'; +import {UserService} from 'services/user.service'; +import {PlatformFeatureService} from 'services/platform-feature.service'; +import {ContributorDashboardAdminBackendApiService} from './services/contributor-dashboard-admin-backend-api.service'; +import {AppConstants} from 'app.constants'; interface ViewContributionReviewers { filterCriterion: string; @@ -106,11 +106,9 @@ export class ContributorDashboardAdminPageComponent implements OnInit { constructor( private userService: UserService, private platformFeatureService: PlatformFeatureService, - private contributorDashboardAdminBackendApiService: - ContributorDashboardAdminBackendApiService, - private languageUtilService: LanguageUtilService, - ) { } - + private contributorDashboardAdminBackendApiService: ContributorDashboardAdminBackendApiService, + private languageUtilService: LanguageUtilService + ) {} refreshFormData(): void { this.formData = { @@ -120,24 +118,32 @@ export class ContributorDashboardAdminPageComponent implements OnInit { category: null, languageCode: null, isValid: () => { - if (this.formData.viewContributionReviewers.filterCriterion === - AppConstants.USER_FILTER_CRITERION_ROLE) { + if ( + this.formData.viewContributionReviewers.filterCriterion === + AppConstants.USER_FILTER_CRITERION_ROLE + ) { if (this.formData.viewContributionReviewers.category === null) { return false; } - if (this.isLanguageSpecificReviewCategory( - this.formData.viewContributionReviewers.category)) { + if ( + this.isLanguageSpecificReviewCategory( + this.formData.viewContributionReviewers.category + ) + ) { return Boolean( - this.formData.viewContributionReviewers.languageCode); + this.formData.viewContributionReviewers.languageCode + ); } return true; } - if (this.formData.viewContributionReviewers.filterCriterion === - AppConstants.USER_FILTER_CRITERION_USERNAME) { + if ( + this.formData.viewContributionReviewers.filterCriterion === + AppConstants.USER_FILTER_CRITERION_USERNAME + ) { return Boolean(this.formData.viewContributionReviewers.username); } - } + }, }, addContributionReviewer: { username: '', @@ -150,30 +156,39 @@ export class ContributorDashboardAdminPageComponent implements OnInit { if (this.formData.addContributionReviewer.category === null) { return false; } - if (this.isLanguageSpecificReviewCategory( - this.formData.addContributionReviewer.category)) { + if ( + this.isLanguageSpecificReviewCategory( + this.formData.addContributionReviewer.category + ) + ) { return Boolean(this.formData.addContributionReviewer.languageCode); } return true; - } + }, }, removeContributionReviewer: { username: '', category: null, languageCode: null, isValid: () => { - if (this.formData.removeContributionReviewer.username === '' || - this.formData.removeContributionReviewer.category === null) { + if ( + this.formData.removeContributionReviewer.username === '' || + this.formData.removeContributionReviewer.category === null + ) { return false; } - if (this.isLanguageSpecificReviewCategory( - this.formData.removeContributionReviewer.category)) { + if ( + this.isLanguageSpecificReviewCategory( + this.formData.removeContributionReviewer.category + ) + ) { return Boolean( - this.formData.removeContributionReviewer.languageCode); + this.formData.removeContributionReviewer.languageCode + ); } return true; }, - method: '' + method: '', }, viewTranslationContributionStats: { username: '', @@ -182,41 +197,39 @@ export class ContributorDashboardAdminPageComponent implements OnInit { return false; } return true; - } - } + }, + }, }; } ngOnInit(): void { - this.isNewUiEnabled = ( - this.platformFeatureService.status.CdAdminDashboardNewUi.isEnabled); - this.userService.getUserInfoAsync().then((userInfo) => { + this.isNewUiEnabled = + this.platformFeatureService.status.CdAdminDashboardNewUi.isEnabled; + this.userService.getUserInfoAsync().then(userInfo => { let translationCategories = {}; let questionCategories = {}; if (userInfo.isTranslationAdmin()) { this.UserIsTranslationAdmin = true; translationCategories = { - REVIEW_TRANSLATION: ( - AppConstants.CD_USER_RIGHTS_CATEGORY_REVIEW_TRANSLATION) + REVIEW_TRANSLATION: + AppConstants.CD_USER_RIGHTS_CATEGORY_REVIEW_TRANSLATION, }; } - if (userInfo.isQuestionAdmin() || - userInfo.isQuestionCoordinator()) { + if (userInfo.isQuestionAdmin() || userInfo.isQuestionCoordinator()) { questionCategories = { REVIEW_QUESTION: AppConstants.CD_USER_RIGHTS_CATEGORY_REVIEW_QUESTION, - SUBMIT_QUESTION: AppConstants.CD_USER_RIGHTS_CATEGORY_SUBMIT_QUESTION + SUBMIT_QUESTION: AppConstants.CD_USER_RIGHTS_CATEGORY_SUBMIT_QUESTION, }; } this.CD_USER_RIGHTS_CATEGORIES = { ...translationCategories, - ...questionCategories + ...questionCategories, }; }); this.USER_FILTER_CRITERION_USERNAME = - AppConstants.USER_FILTER_CRITERION_USERNAME; - this.USER_FILTER_CRITERION_ROLE = - AppConstants.USER_FILTER_CRITERION_ROLE; + AppConstants.USER_FILTER_CRITERION_USERNAME; + this.USER_FILTER_CRITERION_ROLE = AppConstants.USER_FILTER_CRITERION_ROLE; this.refreshFormData(); this.contributionReviewersDataFetched = false; @@ -225,31 +238,31 @@ export class ContributorDashboardAdminPageComponent implements OnInit { this.translationContributionStatsResults = []; this.statusMessage = ''; - this.languageCodesAndDescriptions = - this.languageUtilService.getAllVoiceoverLanguageCodes().map( - (languageCode) => { - return { - id: languageCode, - description: - this.languageUtilService.getAudioLanguageDescription(languageCode) - }; - } - ); + this.languageCodesAndDescriptions = this.languageUtilService + .getAllVoiceoverLanguageCodes() + .map(languageCode => { + return { + id: languageCode, + description: + this.languageUtilService.getAudioLanguageDescription(languageCode), + }; + }); } getLanguageDescriptions(languageCodes: string[]): string[] { const languageDescriptions: string[] = []; - languageCodes.forEach((languageCode) => { + languageCodes.forEach(languageCode => { languageDescriptions.push( - this.languageUtilService.getAudioLanguageDescription(languageCode)); + this.languageUtilService.getAudioLanguageDescription(languageCode) + ); }); return languageDescriptions; } isLanguageSpecificReviewCategory(reviewCategory: string): boolean { return ( - reviewCategory === - AppConstants.CD_USER_RIGHTS_CATEGORY_REVIEW_TRANSLATION); + reviewCategory === AppConstants.CD_USER_RIGHTS_CATEGORY_REVIEW_TRANSLATION + ); } submitAddContributionRightsForm(formResponse: AddContributionReviewer): void { @@ -261,18 +274,25 @@ export class ContributorDashboardAdminPageComponent implements OnInit { this.contributorDashboardAdminBackendApiService .addContributionReviewerAsync( - formResponse.category, formResponse.username, formResponse.languageCode - ).then(() => { - this.statusMessage = 'Success.'; - this.refreshFormData(); - }, errorResponse => { - this.statusMessage = 'Server error: ' + errorResponse; - }); + formResponse.category, + formResponse.username, + formResponse.languageCode + ) + .then( + () => { + this.statusMessage = 'Success.'; + this.refreshFormData(); + }, + errorResponse => { + this.statusMessage = 'Server error: ' + errorResponse; + } + ); this.taskRunningInBackground = false; } submitViewContributorUsersForm( - formResponse: ViewContributionReviewers): void { + formResponse: ViewContributionReviewers + ): void { if (this.taskRunningInBackground) { return; } @@ -280,12 +300,15 @@ export class ContributorDashboardAdminPageComponent implements OnInit { this.taskRunningInBackground = true; this.contributionReviewersResult = {}; - if (formResponse.filterCriterion === - AppConstants.USER_FILTER_CRITERION_ROLE) { + if ( + formResponse.filterCriterion === AppConstants.USER_FILTER_CRITERION_ROLE + ) { this.contributorDashboardAdminBackendApiService .viewContributionReviewersAsync( - formResponse.category, formResponse.languageCode - ).then((usersObject) => { + formResponse.category, + formResponse.languageCode + ) + .then(usersObject => { this.contributionReviewersResult.usernames = usersObject.usernames; this.contributionReviewersDataFetched = true; this.statusMessage = 'Success.'; @@ -295,37 +318,46 @@ export class ContributorDashboardAdminPageComponent implements OnInit { }); } else { this.contributorDashboardAdminBackendApiService - .contributionReviewerRightsAsync( - formResponse.username - ).then((contributionRights) => { - if (this.CD_USER_RIGHTS_CATEGORIES.hasOwnProperty( - 'REVIEW_TRANSLATION')) { - this.contributionReviewersResult = { - REVIEW_TRANSLATION: this.getLanguageDescriptions( - contributionRights.can_review_translation_for_language_codes) - }; - } - if (this.CD_USER_RIGHTS_CATEGORIES.hasOwnProperty( - 'REVIEW_QUESTION')) { - this.contributionReviewersResult.REVIEW_QUESTION = - contributionRights.can_review_questions; - this.contributionReviewersResult.SUBMIT_QUESTION = - contributionRights.can_submit_questions; + .contributionReviewerRightsAsync(formResponse.username) + .then( + contributionRights => { + if ( + this.CD_USER_RIGHTS_CATEGORIES.hasOwnProperty( + 'REVIEW_TRANSLATION' + ) + ) { + this.contributionReviewersResult = { + REVIEW_TRANSLATION: this.getLanguageDescriptions( + contributionRights.can_review_translation_for_language_codes + ), + }; + } + if ( + this.CD_USER_RIGHTS_CATEGORIES.hasOwnProperty('REVIEW_QUESTION') + ) { + this.contributionReviewersResult.REVIEW_QUESTION = + contributionRights.can_review_questions; + this.contributionReviewersResult.SUBMIT_QUESTION = + contributionRights.can_submit_questions; + } + this.contributionReviewersDataFetched = true; + this.statusMessage = 'Success.'; + const temp = + this.formData.viewContributionReviewers.filterCriterion; + this.refreshFormData(); + this.formData.viewContributionReviewers.filterCriterion = temp; + }, + errorResponse => { + this.statusMessage = 'Server error: ' + errorResponse; } - this.contributionReviewersDataFetched = true; - this.statusMessage = 'Success.'; - const temp = this.formData.viewContributionReviewers.filterCriterion; - this.refreshFormData(); - this.formData.viewContributionReviewers.filterCriterion = temp; - }, errorResponse => { - this.statusMessage = 'Server error: ' + errorResponse; - }); + ); } this.taskRunningInBackground = false; } submitRemoveContributionRightsForm( - formResponse: RemoveContributionReviewer): void { + formResponse: RemoveContributionReviewer + ): void { if (this.taskRunningInBackground) { return; } @@ -334,19 +366,26 @@ export class ContributorDashboardAdminPageComponent implements OnInit { this.contributorDashboardAdminBackendApiService .removeContributionReviewerAsync( - formResponse.username, formResponse.category, formResponse.languageCode - ).then(() => { - this.statusMessage = 'Success.'; - this.refreshFormData(); - }, errorResponse => { - this.statusMessage = 'Server error: ' + errorResponse; - }); + formResponse.username, + formResponse.category, + formResponse.languageCode + ) + .then( + () => { + this.statusMessage = 'Success.'; + this.refreshFormData(); + }, + errorResponse => { + this.statusMessage = 'Server error: ' + errorResponse; + } + ); this.taskRunningInBackground = false; } submitViewTranslationContributionStatsForm( - formResponse: ViewTranslationContributionStats): void { + formResponse: ViewTranslationContributionStats + ): void { if (this.taskRunningInBackground) { return; } @@ -354,17 +393,19 @@ export class ContributorDashboardAdminPageComponent implements OnInit { this.taskRunningInBackground = true; this.contributorDashboardAdminBackendApiService - .viewTranslationContributionStatsAsync( - formResponse.username - ).then(response => { - this.translationContributionStatsResults = - response.translation_contribution_stats; - this.translationContributionStatsFetched = true; - this.statusMessage = 'Success.'; - this.refreshFormData(); - }, errorResponse => { - this.statusMessage = 'Server error: ' + errorResponse; - }); + .viewTranslationContributionStatsAsync(formResponse.username) + .then( + response => { + this.translationContributionStatsResults = + response.translation_contribution_stats; + this.translationContributionStatsFetched = true; + this.statusMessage = 'Success.'; + this.refreshFormData(); + }, + errorResponse => { + this.statusMessage = 'Server error: ' + errorResponse; + } + ); this.taskRunningInBackground = false; } diff --git a/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-page.constants.ajs.ts b/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-page.constants.ajs.ts index c6e8bdf2a5f1..9c7dda77ff5e 100644 --- a/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-page.constants.ajs.ts +++ b/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-page.constants.ajs.ts @@ -18,17 +18,29 @@ // TODO(#7092): Delete this file once migration is complete and these AngularJS // equivalents of the Angular constants are no longer needed. -import { ContributorDashboardAdminPageConstants as PageConstants } from './contributor-dashboard-admin-page.constants'; +import {ContributorDashboardAdminPageConstants as PageConstants} from './contributor-dashboard-admin-page.constants'; -angular.module('oppia').constant( - 'CONTRIBUTION_RIGHTS_HANDLER_URL', - PageConstants.CONTRIBUTION_RIGHTS_HANDLER_URL); -angular.module('oppia').constant( - 'CONTRIBUTION_RIGHTS_DATA_HANDLER_URL', - PageConstants.CONTRIBUTION_RIGHTS_DATA_HANDLER_URL); -angular.module('oppia').constant( - 'GET_CONTRIBUTOR_USERS_HANDLER_URL', - PageConstants.GET_CONTRIBUTOR_USERS_HANDLER_URL); -angular.module('oppia').constant( - 'TRANSLATION_CONTRIBUTION_STATS_HANDLER_URL', - PageConstants.TRANSLATION_CONTRIBUTION_STATS_HANDLER_URL); +angular + .module('oppia') + .constant( + 'CONTRIBUTION_RIGHTS_HANDLER_URL', + PageConstants.CONTRIBUTION_RIGHTS_HANDLER_URL + ); +angular + .module('oppia') + .constant( + 'CONTRIBUTION_RIGHTS_DATA_HANDLER_URL', + PageConstants.CONTRIBUTION_RIGHTS_DATA_HANDLER_URL + ); +angular + .module('oppia') + .constant( + 'GET_CONTRIBUTOR_USERS_HANDLER_URL', + PageConstants.GET_CONTRIBUTOR_USERS_HANDLER_URL + ); +angular + .module('oppia') + .constant( + 'TRANSLATION_CONTRIBUTION_STATS_HANDLER_URL', + PageConstants.TRANSLATION_CONTRIBUTION_STATS_HANDLER_URL + ); diff --git a/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-page.constants.ts b/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-page.constants.ts index 70c676a83508..0149f5201c33 100644 --- a/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-page.constants.ts +++ b/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-page.constants.ts @@ -17,18 +17,15 @@ */ export const ContributorDashboardAdminPageConstants = { - CONTRIBUTION_RIGHTS_HANDLER_URL: ( - '/contributionrightshandler/'), + CONTRIBUTION_RIGHTS_HANDLER_URL: '/contributionrightshandler/', CONTRIBUTION_RIGHTS_DATA_HANDLER_URL: '/contributionrightsdatahandler', GET_CONTRIBUTOR_USERS_HANDLER_URL: '/getcontributorusershandler/', - TRANSLATION_CONTRIBUTION_STATS_HANDLER_URL: ( - '/translationcontributionstatshandler' - ), - CONTRIBUTOR_ADMIN_STATS_SUMMARIES_URL: ( + TRANSLATION_CONTRIBUTION_STATS_HANDLER_URL: + '/translationcontributionstatshandler', + CONTRIBUTOR_ADMIN_STATS_SUMMARIES_URL: '/contributor-dashboard-admin-stats/' + - '/' - ), + '/', COMMUNITY_CONTRIBUTION_STATS_URL: '/community-contribution-stats', ADMIN_ROLE_HANDLER_URL: '/adminrolehandler', - DEFAULT_LANGUAGE_FILTER: 'en' + DEFAULT_LANGUAGE_FILTER: 'en', } as const; diff --git a/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-page.import.ts b/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-page.import.ts index a075134d116e..66af721200ba 100644 --- a/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-page.import.ts +++ b/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-page.import.ts @@ -22,9 +22,14 @@ import 'zone.js'; import uiValidate from 'angular-ui-validate'; angular.module('oppia', [ - require('angular-cookies'), 'ngAnimate', - 'ngMaterial', 'ngSanitize', 'ngTouch', 'pascalprecht.translate', - 'ui.bootstrap', uiValidate + require('angular-cookies'), + 'ngAnimate', + 'ngMaterial', + 'ngSanitize', + 'ngTouch', + 'pascalprecht.translate', + 'ui.bootstrap', + uiValidate, ]); require('Polyfills.ts'); @@ -33,13 +38,16 @@ require('Polyfills.ts'); // main module the elements are attached to. require( 'pages/contributor-dashboard-admin-page/' + - 'contributor-dashboard-admin-page.module.ts'); + 'contributor-dashboard-admin-page.module.ts' +); require('App.ts'); require('base-components/oppia-root.directive.ts'); require( 'pages/contributor-dashboard-admin-page/' + - 'contributor-dashboard-admin-page.component.ts'); + 'contributor-dashboard-admin-page.component.ts' +); require( 'pages/contributor-dashboard-admin-page/' + - 'contributor-admin-dashboard-page.component.ts'); + 'contributor-admin-dashboard-page.component.ts' +); diff --git a/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-page.module.ts b/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-page.module.ts index 9835ef499a7b..22053e0b58c6 100644 --- a/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-page.module.ts +++ b/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-page.module.ts @@ -16,25 +16,25 @@ * @fileoverview Module for the contributor-dashboard-admin page. */ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { RouterModule } from '@angular/router'; -import { MatTableModule } from '@angular/material/table'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { CdAdminTranslationRoleEditorModal } from './translation-role-editor-modal/cd-admin-translation-role-editor-modal.component'; -import { CdAdminQuestionRoleEditorModal } from './question-role-editor-modal/cd-admin-question-role-editor-modal.component'; -import { UsernameInputModal } from './username-input-modal/username-input-modal.component'; -import { ContributorDashboardAdminNavbarComponent } from './navbar/contributor-dashboard-admin-navbar.component'; -import { ContributorAdminDashboardPageComponent } from './contributor-admin-dashboard-page.component'; -import { ContributorAdminStatsTable } from './contributor-dashboard-tables/contributor-admin-stats-table.component'; -import { TopicFilterComponent } from './topic-filter/topic-filter.component'; -import { ToastrModule } from 'ngx-toastr'; -import { toastrConfig } from 'pages/oppia-root/app.module'; -import { ContributorDashboardAdminPageRootComponent } from './contributor-dashboard-admin-page-root.component'; -import { ContributorDashboardAdminAuthGuard } from './contributor-dashboard-admin-auth.guard'; -import { ContributorDashboardAdminPageComponent } from './contributor-dashboard-admin-page.component'; +import {CommonModule} from '@angular/common'; +import {NgModule} from '@angular/core'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {RouterModule} from '@angular/router'; +import {MatTableModule} from '@angular/material/table'; +import {MatTooltipModule} from '@angular/material/tooltip'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {CdAdminTranslationRoleEditorModal} from './translation-role-editor-modal/cd-admin-translation-role-editor-modal.component'; +import {CdAdminQuestionRoleEditorModal} from './question-role-editor-modal/cd-admin-question-role-editor-modal.component'; +import {UsernameInputModal} from './username-input-modal/username-input-modal.component'; +import {ContributorDashboardAdminNavbarComponent} from './navbar/contributor-dashboard-admin-navbar.component'; +import {ContributorAdminDashboardPageComponent} from './contributor-admin-dashboard-page.component'; +import {ContributorAdminStatsTable} from './contributor-dashboard-tables/contributor-admin-stats-table.component'; +import {TopicFilterComponent} from './topic-filter/topic-filter.component'; +import {ToastrModule} from 'ngx-toastr'; +import {toastrConfig} from 'pages/oppia-root/app.module'; +import {ContributorDashboardAdminPageRootComponent} from './contributor-dashboard-admin-page-root.component'; +import {ContributorDashboardAdminAuthGuard} from './contributor-dashboard-admin-auth.guard'; +import {ContributorDashboardAdminPageComponent} from './contributor-dashboard-admin-page.component'; @NgModule({ imports: [ @@ -62,7 +62,7 @@ import { ContributorDashboardAdminPageComponent } from './contributor-dashboard- TopicFilterComponent, UsernameInputModal, ContributorDashboardAdminPageComponent, - ContributorDashboardAdminPageRootComponent + ContributorDashboardAdminPageRootComponent, ], entryComponents: [ CdAdminTranslationRoleEditorModal, @@ -72,8 +72,7 @@ import { ContributorDashboardAdminPageComponent } from './contributor-dashboard- ContributorDashboardAdminPageComponent, ContributorAdminStatsTable, TopicFilterComponent, - UsernameInputModal + UsernameInputModal, ], }) - export class ContributorDashboardAdminPageModule {} diff --git a/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-summary.model.spec.ts b/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-summary.model.spec.ts index 188059a00e3d..33f21d7620e5 100644 --- a/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-summary.model.spec.ts +++ b/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-summary.model.spec.ts @@ -20,7 +20,7 @@ import { QuestionReviewerStats, QuestionSubmitterStats, TranslationReviewerStats, - TranslationSubmitterStats + TranslationSubmitterStats, } from './contributor-dashboard-admin-summary.model'; describe('Translation Submitter Stats Model', () => { @@ -39,24 +39,26 @@ describe('Translation Submitter Stats Model', () => { rejected_translations_count: 2, rejected_translation_word_count: 2, first_contribution_date: 'firstcontributiondate', - last_contributed_in_days: 2 + last_contributed_in_days: 2, }; - let statsSummary = TranslationSubmitterStats - .createFromBackendDict(backendDict); + let statsSummary = + TranslationSubmitterStats.createFromBackendDict(backendDict); expect(statsSummary.languageCode).toEqual('en'); expect(statsSummary.contributorName).toEqual('user1'); - expect( - statsSummary.topicsWithTranslationSubmissions) - .toEqual(['topic1', 'topic2']); + expect(statsSummary.topicsWithTranslationSubmissions).toEqual([ + 'topic1', + 'topic2', + ]); expect(statsSummary.recentPerformance).toEqual(2); expect(statsSummary.overallAccuracy).toEqual(1.0); expect(statsSummary.submittedTranslationsCount).toEqual(2); expect(statsSummary.submittedTranslationWordCount).toEqual(2); expect(statsSummary.acceptedTranslationsCount).toEqual(2); - expect( - statsSummary.acceptedTranslationsWithoutReviewerEditsCount).toEqual(2); + expect(statsSummary.acceptedTranslationsWithoutReviewerEditsCount).toEqual( + 2 + ); expect(statsSummary.acceptedTranslationWordCount).toEqual(2); expect(statsSummary.rejectedTranslationsCount).toEqual(2); expect(statsSummary.firstContributionDate).toEqual('firstcontributiondate'); @@ -76,21 +78,21 @@ describe('Translation Reviewer Stats Model', () => { accepted_translation_word_count: 2, rejected_translations_count: 2, first_contribution_date: 'firstcontributiondate', - last_contributed_in_days: 2 + last_contributed_in_days: 2, }; - let statsSummary = TranslationReviewerStats - .createFromBackendDict(backendDict); + let statsSummary = + TranslationReviewerStats.createFromBackendDict(backendDict); expect(statsSummary.languageCode).toEqual('en'); expect(statsSummary.contributorName).toEqual('user1'); - expect( - statsSummary.topicsWithTranslationReviews) - .toEqual(['topic1', 'topic2']); + expect(statsSummary.topicsWithTranslationReviews).toEqual([ + 'topic1', + 'topic2', + ]); expect(statsSummary.reviewedTranslationsCount).toEqual(2); expect(statsSummary.acceptedTranslationsCount).toEqual(2); - expect( - statsSummary.acceptedTranslationsWithReviewerEditsCount).toEqual(2); + expect(statsSummary.acceptedTranslationsWithReviewerEditsCount).toEqual(2); expect(statsSummary.acceptedTranslationWordCount).toEqual(2); expect(statsSummary.rejectedTranslationsCount).toEqual(2); expect(statsSummary.firstContributionDate).toEqual('firstcontributiondate'); @@ -110,21 +112,21 @@ describe('Question Submitter Stats Model', () => { accepted_questions_without_reviewer_edits_count: 2, rejected_questions_count: 2, first_contribution_date: 'firstcontributiondate', - last_contributed_in_days: 2 + last_contributed_in_days: 2, }; - let statsSummary = QuestionSubmitterStats - .createFromBackendDict(backendDict); + let statsSummary = + QuestionSubmitterStats.createFromBackendDict(backendDict); expect(statsSummary.contributorName).toEqual('user1'); - expect( - statsSummary.topicsWithQuestionSubmissions) - .toEqual(['topic1', 'topic2']); + expect(statsSummary.topicsWithQuestionSubmissions).toEqual([ + 'topic1', + 'topic2', + ]); expect(statsSummary.recentPerformance).toEqual(2); expect(statsSummary.overallAccuracy).toEqual(1.0); expect(statsSummary.submittedQuestionsCount).toEqual(2); - expect( - statsSummary.acceptedQuestionsWithoutReviewerEditsCount).toEqual(2); + expect(statsSummary.acceptedQuestionsWithoutReviewerEditsCount).toEqual(2); expect(statsSummary.rejectedQuestionsCount).toEqual(2); expect(statsSummary.firstContributionDate).toEqual('firstcontributiondate'); expect(statsSummary.lastContributedInDays).toEqual(2); @@ -141,20 +143,19 @@ describe('Question Reviewer Stats Model', () => { accepted_questions_with_reviewer_edits_count: 2, rejected_questions_count: 2, first_contribution_date: 'firstcontributiondate', - last_contributed_in_days: 2 + last_contributed_in_days: 2, }; - let statsSummary = QuestionReviewerStats - .createFromBackendDict(backendDict); + let statsSummary = QuestionReviewerStats.createFromBackendDict(backendDict); expect(statsSummary.contributorName).toEqual('user1'); - expect( - statsSummary.topicsWithQuestionReviews) - .toEqual(['topic1', 'topic2']); + expect(statsSummary.topicsWithQuestionReviews).toEqual([ + 'topic1', + 'topic2', + ]); expect(statsSummary.reviewedQuestionsCount).toEqual(2); expect(statsSummary.acceptedQuestionsCount).toEqual(2); - expect( - statsSummary.acceptedQuestionsWithReviewerEditsCount).toEqual(2); + expect(statsSummary.acceptedQuestionsWithReviewerEditsCount).toEqual(2); expect(statsSummary.rejectedQuestionsCount).toEqual(2); expect(statsSummary.firstContributionDate).toEqual('firstcontributiondate'); expect(statsSummary.lastContributedInDays).toEqual(2); diff --git a/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-summary.model.ts b/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-summary.model.ts index f4e055001047..70d9ca42b5ee 100644 --- a/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-summary.model.ts +++ b/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-admin-summary.model.ts @@ -16,29 +16,34 @@ * @fileoverview Frontend model for contributor admin dashboard domain objects. */ -import { TranslationSubmitterBackendDict, TranslationReviewerBackendDict, - QuestionSubmitterBackendDict, QuestionReviewerBackendDict +import { + TranslationSubmitterBackendDict, + TranslationReviewerBackendDict, + QuestionSubmitterBackendDict, + QuestionReviewerBackendDict, } from './services/contributor-dashboard-admin-stats-backend-api.service'; export class TranslationSubmitterStats { constructor( - public contributorName: string, - public languageCode: string, - public topicsWithTranslationSubmissions: string[], - public recentPerformance: number, - public overallAccuracy: number, - public submittedTranslationsCount: number, - public submittedTranslationWordCount: number, - public acceptedTranslationsCount: number, - public acceptedTranslationsWithoutReviewerEditsCount: number, - public acceptedTranslationWordCount: number, - public rejectedTranslationsCount: number, - public rejectedTranslationWordCount: number, - public firstContributionDate: string, - public lastContributedInDays: number) { } + public contributorName: string, + public languageCode: string, + public topicsWithTranslationSubmissions: string[], + public recentPerformance: number, + public overallAccuracy: number, + public submittedTranslationsCount: number, + public submittedTranslationWordCount: number, + public acceptedTranslationsCount: number, + public acceptedTranslationsWithoutReviewerEditsCount: number, + public acceptedTranslationWordCount: number, + public rejectedTranslationsCount: number, + public rejectedTranslationWordCount: number, + public firstContributionDate: string, + public lastContributedInDays: number + ) {} - static createFromBackendDict(summaryDict: TranslationSubmitterBackendDict): - TranslationSubmitterStats { + static createFromBackendDict( + summaryDict: TranslationSubmitterBackendDict + ): TranslationSubmitterStats { return new TranslationSubmitterStats( summaryDict.contributor_name, summaryDict.language_code, @@ -60,19 +65,21 @@ export class TranslationSubmitterStats { export class TranslationReviewerStats { constructor( - public contributorName: string, - public languageCode: string, - public topicsWithTranslationReviews: string[], - public reviewedTranslationsCount: number, - public acceptedTranslationsCount: number, - public acceptedTranslationsWithReviewerEditsCount: number, - public acceptedTranslationWordCount: number, - public rejectedTranslationsCount: number, - public firstContributionDate: string, - public lastContributedInDays: number) { } + public contributorName: string, + public languageCode: string, + public topicsWithTranslationReviews: string[], + public reviewedTranslationsCount: number, + public acceptedTranslationsCount: number, + public acceptedTranslationsWithReviewerEditsCount: number, + public acceptedTranslationWordCount: number, + public rejectedTranslationsCount: number, + public firstContributionDate: string, + public lastContributedInDays: number + ) {} - static createFromBackendDict(summaryDict: TranslationReviewerBackendDict): - TranslationReviewerStats { + static createFromBackendDict( + summaryDict: TranslationReviewerBackendDict + ): TranslationReviewerStats { return new TranslationReviewerStats( summaryDict.contributor_name, summaryDict.language_code, @@ -90,19 +97,21 @@ export class TranslationReviewerStats { export class QuestionSubmitterStats { constructor( - public contributorName: string, - public topicsWithQuestionSubmissions: string[], - public recentPerformance: number, - public overallAccuracy: number, - public submittedQuestionsCount: number, - public acceptedQuestionsCount: number, - public acceptedQuestionsWithoutReviewerEditsCount: number, - public rejectedQuestionsCount: number, - public firstContributionDate: string, - public lastContributedInDays: number) { } + public contributorName: string, + public topicsWithQuestionSubmissions: string[], + public recentPerformance: number, + public overallAccuracy: number, + public submittedQuestionsCount: number, + public acceptedQuestionsCount: number, + public acceptedQuestionsWithoutReviewerEditsCount: number, + public rejectedQuestionsCount: number, + public firstContributionDate: string, + public lastContributedInDays: number + ) {} - static createFromBackendDict(summaryDict: QuestionSubmitterBackendDict): - QuestionSubmitterStats { + static createFromBackendDict( + summaryDict: QuestionSubmitterBackendDict + ): QuestionSubmitterStats { return new QuestionSubmitterStats( summaryDict.contributor_name, summaryDict.topic_names, @@ -120,17 +129,19 @@ export class QuestionSubmitterStats { export class QuestionReviewerStats { constructor( - public contributorName: string, - public topicsWithQuestionReviews: string[], - public reviewedQuestionsCount: number, - public acceptedQuestionsCount: number, - public acceptedQuestionsWithReviewerEditsCount: number, - public rejectedQuestionsCount: number, - public firstContributionDate: string, - public lastContributedInDays: number) { } + public contributorName: string, + public topicsWithQuestionReviews: string[], + public reviewedQuestionsCount: number, + public acceptedQuestionsCount: number, + public acceptedQuestionsWithReviewerEditsCount: number, + public rejectedQuestionsCount: number, + public firstContributionDate: string, + public lastContributedInDays: number + ) {} - static createFromBackendDict(summaryDict: QuestionReviewerBackendDict): - QuestionReviewerStats { + static createFromBackendDict( + summaryDict: QuestionReviewerBackendDict + ): QuestionReviewerStats { return new QuestionReviewerStats( summaryDict.contributor_name, summaryDict.topic_names, diff --git a/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-tables/contributor-admin-stats-table.component.spec.ts b/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-tables/contributor-admin-stats-table.component.spec.ts index 335364056e49..e8245ba4e594 100644 --- a/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-tables/contributor-admin-stats-table.component.spec.ts +++ b/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-tables/contributor-admin-stats-table.component.spec.ts @@ -16,28 +16,38 @@ * @fileoverview Unit tests for ContributorAdminStatsTable. */ -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { NO_ERRORS_SCHEMA, SimpleChanges } from '@angular/core'; -import { CdAdminTranslationRoleEditorModal } from '../translation-role-editor-modal/cd-admin-translation-role-editor-modal.component'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ContributorAdminStatsTable } from './contributor-admin-stats-table.component'; -import { ContributorDashboardAdminStatsBackendApiService, QuestionReviewerStatsData, QuestionSubmitterStatsData, TranslationReviewerStatsData, TranslationSubmitterStatsData } from '../services/contributor-dashboard-admin-stats-backend-api.service'; -import { ContributorDashboardAdminBackendApiService } from '../services/contributor-dashboard-admin-backend-api.service'; -import { MatTableModule } from '@angular/material/table'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { CdAdminQuestionRoleEditorModal } from '../question-role-editor-modal/cd-admin-question-role-editor-modal.component'; -import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {NO_ERRORS_SCHEMA, SimpleChanges} from '@angular/core'; +import {CdAdminTranslationRoleEditorModal} from '../translation-role-editor-modal/cd-admin-translation-role-editor-modal.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ContributorAdminStatsTable} from './contributor-admin-stats-table.component'; +import { + ContributorDashboardAdminStatsBackendApiService, + QuestionReviewerStatsData, + QuestionSubmitterStatsData, + TranslationReviewerStatsData, + TranslationSubmitterStatsData, +} from '../services/contributor-dashboard-admin-stats-backend-api.service'; +import {ContributorDashboardAdminBackendApiService} from '../services/contributor-dashboard-admin-backend-api.service'; +import {MatTableModule} from '@angular/material/table'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {MatTooltipModule} from '@angular/material/tooltip'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {CdAdminQuestionRoleEditorModal} from '../question-role-editor-modal/cd-admin-question-role-editor-modal.component'; +import {BrowserDynamicTestingModule} from '@angular/platform-browser-dynamic/testing'; describe('Contributor stats component', () => { let component: ContributorAdminStatsTable; let fixture: ComponentFixture; let $window: WindowRef; - let contributorDashboardAdminStatsBackendApiService: ( - ContributorDashboardAdminStatsBackendApiService); - let contributorDashboardAdminBackendApiService: ( - ContributorDashboardAdminBackendApiService); + let contributorDashboardAdminStatsBackendApiService: ContributorDashboardAdminStatsBackendApiService; + let contributorDashboardAdminBackendApiService: ContributorDashboardAdminBackendApiService; let ngbModal: NgbModal; class MockNgbModalRef { componentInstance!: {}; @@ -45,26 +55,23 @@ describe('Contributor stats component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - MatTableModule, - MatTooltipModule - ], + imports: [HttpClientTestingModule, MatTableModule, MatTooltipModule], declarations: [ CdAdminTranslationRoleEditorModal, - ContributorAdminStatsTable + ContributorAdminStatsTable, ], providers: [ ContributorDashboardAdminStatsBackendApiService, - ContributorDashboardAdminBackendApiService + ContributorDashboardAdminBackendApiService, ], - schemas: [NO_ERRORS_SCHEMA] - }).overrideModule(BrowserDynamicTestingModule, { - set: { - entryComponents: [ - CdAdminTranslationRoleEditorModal] - } - }).compileComponents(); + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideModule(BrowserDynamicTestingModule, { + set: { + entryComponents: [CdAdminTranslationRoleEditorModal], + }, + }) + .compileComponents(); })); beforeEach(waitForAsync(() => { @@ -73,9 +80,11 @@ describe('Contributor stats component', () => { component = fixture.componentInstance; contributorDashboardAdminStatsBackendApiService = TestBed.inject( - ContributorDashboardAdminStatsBackendApiService); + ContributorDashboardAdminStatsBackendApiService + ); contributorDashboardAdminBackendApiService = TestBed.inject( - ContributorDashboardAdminBackendApiService); + ContributorDashboardAdminBackendApiService + ); ngbModal = TestBed.inject(NgbModal); // This approach was choosen because spyOn() doesn't work on properties @@ -86,7 +95,7 @@ describe('Contributor stats component', () => { // ref: https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty // ref: https://github.com/jasmine/jasmine/issues/1415 Object.defineProperty($window.nativeWindow, 'innerWidth', { - get: () => undefined + get: () => undefined, }); component.ngOnInit(); @@ -103,12 +112,14 @@ describe('Contributor stats component', () => { it('should show translation submitter stats', fakeAsync(() => { spyOn( contributorDashboardAdminStatsBackendApiService, - 'fetchContributorAdminStats') - .and.returnValue(Promise.resolve({ + 'fetchContributorAdminStats' + ).and.returnValue( + Promise.resolve({ stats: [], nextOffset: 1, more: false, - } as TranslationSubmitterStatsData)); + } as TranslationSubmitterStatsData) + ); const changes: SimpleChanges = { activeTab: { @@ -129,19 +140,21 @@ describe('Contributor stats component', () => { 'overallAccuracy', 'submittedTranslationsCount', 'lastContributedInDays', - 'role' + 'role', ]); })); it('should show translation reviewer stats', fakeAsync(() => { spyOn( contributorDashboardAdminStatsBackendApiService, - 'fetchContributorAdminStats') - .and.returnValue(Promise.resolve({ + 'fetchContributorAdminStats' + ).and.returnValue( + Promise.resolve({ stats: [], nextOffset: 1, - more: false - } as TranslationReviewerStatsData)); + more: false, + } as TranslationReviewerStatsData) + ); const changes: SimpleChanges = { activeTab: { @@ -160,19 +173,21 @@ describe('Contributor stats component', () => { 'contributorName', 'reviewedTranslationsCount', 'lastContributedInDays', - 'role' + 'role', ]); })); it('should show question submitter stats', fakeAsync(() => { spyOn( contributorDashboardAdminStatsBackendApiService, - 'fetchContributorAdminStats') - .and.returnValue(Promise.resolve({ + 'fetchContributorAdminStats' + ).and.returnValue( + Promise.resolve({ stats: [], nextOffset: 1, - more: false - } as QuestionSubmitterStatsData)); + more: false, + } as QuestionSubmitterStatsData) + ); const changes: SimpleChanges = { activeTab: { @@ -193,19 +208,21 @@ describe('Contributor stats component', () => { 'overallAccuracy', 'submittedQuestionsCount', 'lastContributedInDays', - 'role' + 'role', ]); })); it('should show question reviewer stats', fakeAsync(() => { spyOn( contributorDashboardAdminStatsBackendApiService, - 'fetchContributorAdminStats') - .and.returnValue(Promise.resolve({ + 'fetchContributorAdminStats' + ).and.returnValue( + Promise.resolve({ stats: [], nextOffset: 1, - more: false - } as QuestionReviewerStatsData)); + more: false, + } as QuestionReviewerStatsData) + ); const changes: SimpleChanges = { activeTab: { @@ -224,7 +241,7 @@ describe('Contributor stats component', () => { 'contributorName', 'reviewedQuestionsCount', 'lastContributedInDays', - 'role' + 'role', ]); })); @@ -281,7 +298,7 @@ describe('Contributor stats component', () => { 'submittedTranslationsCount', 'lastContributedInDays', 'role', - 'chevron' + 'chevron', ]); })); @@ -303,7 +320,7 @@ describe('Contributor stats component', () => { 'reviewedTranslationsCount', 'lastContributedInDays', 'role', - 'chevron' + 'chevron', ]); })); @@ -327,7 +344,7 @@ describe('Contributor stats component', () => { 'submittedQuestionsCount', 'lastContributedInDays', 'role', - 'chevron' + 'chevron', ]); })); @@ -349,179 +366,209 @@ describe('Contributor stats component', () => { 'reviewedQuestionsCount', 'lastContributedInDays', 'role', - 'chevron' + 'chevron', ]); spyOn( contributorDashboardAdminStatsBackendApiService, - 'fetchContributorAdminStats') - .and.returnValue(Promise.resolve({ + 'fetchContributorAdminStats' + ).and.returnValue( + Promise.resolve({ stats: [], nextOffset: 1, - more: false - } as QuestionReviewerStatsData)); + more: false, + } as QuestionReviewerStatsData) + ); })); }); - it('should open question role editor modal and return changed value of' + - ' translation submitter', fakeAsync(() => { - const removeRightsSpy = spyOn( - contributorDashboardAdminBackendApiService, - 'removeContributionReviewerAsync'); - const changes: SimpleChanges = { - activeTab: { - currentValue: component.TAB_NAME_QUESTION_SUBMITTER, - previousValue: null, - firstChange: true, - isFirstChange: () => true, - }, - }; - component.inputs.activeTab = component.TAB_NAME_QUESTION_SUBMITTER; - component.ngOnChanges(changes); - spyOn( - contributorDashboardAdminBackendApiService, - 'contributionReviewerRightsAsync').and.returnValue(Promise.resolve({ - can_submit_questions: true, - can_review_questions: true, - can_review_translation_for_language_codes: [], - can_review_voiceover_for_language_codes: [] - })); - let modalSpy = spyOn(ngbModal, 'open').and.callFake(() => { - return ({ - componentInstance: MockNgbModalRef, - result: Promise.resolve({ - isQuestionSubmitter: false, - isQuestionReviewer: true + it( + 'should open question role editor modal and return changed value of' + + ' translation submitter', + fakeAsync(() => { + const removeRightsSpy = spyOn( + contributorDashboardAdminBackendApiService, + 'removeContributionReviewerAsync' + ); + const changes: SimpleChanges = { + activeTab: { + currentValue: component.TAB_NAME_QUESTION_SUBMITTER, + previousValue: null, + firstChange: true, + isFirstChange: () => true, + }, + }; + component.inputs.activeTab = component.TAB_NAME_QUESTION_SUBMITTER; + component.ngOnChanges(changes); + spyOn( + contributorDashboardAdminBackendApiService, + 'contributionReviewerRightsAsync' + ).and.returnValue( + Promise.resolve({ + can_submit_questions: true, + can_review_questions: true, + can_review_translation_for_language_codes: [], + can_review_voiceover_for_language_codes: [], }) - }) as NgbModalRef; - }); - - component.openRoleEditor('user1'); - tick(); - - expect(modalSpy).toHaveBeenCalledWith(CdAdminQuestionRoleEditorModal); - expect(removeRightsSpy).toHaveBeenCalled(); - })); - - it('should open question role editor modal and return false changed' + - ' value of translation reviewer', fakeAsync(() => { - const removeRightsSpy = spyOn( - contributorDashboardAdminBackendApiService, - 'removeContributionReviewerAsync'); - const changes: SimpleChanges = { - activeTab: { - currentValue: component.TAB_NAME_QUESTION_SUBMITTER, - previousValue: null, - firstChange: true, - isFirstChange: () => true, - }, - }; - component.inputs.activeTab = component.TAB_NAME_QUESTION_SUBMITTER; - component.ngOnChanges(changes); - spyOn( - contributorDashboardAdminBackendApiService, - 'contributionReviewerRightsAsync').and.returnValue(Promise.resolve({ - can_submit_questions: true, - can_review_questions: true, - can_review_translation_for_language_codes: [], - can_review_voiceover_for_language_codes: [] - })); - let modalSpy = spyOn(ngbModal, 'open').and.callFake(() => { - return ({ - componentInstance: MockNgbModalRef, - result: Promise.resolve({ - isQuestionSubmitter: true, - isQuestionReviewer: false + ); + let modalSpy = spyOn(ngbModal, 'open').and.callFake(() => { + return { + componentInstance: MockNgbModalRef, + result: Promise.resolve({ + isQuestionSubmitter: false, + isQuestionReviewer: true, + }), + } as NgbModalRef; + }); + + component.openRoleEditor('user1'); + tick(); + + expect(modalSpy).toHaveBeenCalledWith(CdAdminQuestionRoleEditorModal); + expect(removeRightsSpy).toHaveBeenCalled(); + }) + ); + + it( + 'should open question role editor modal and return false changed' + + ' value of translation reviewer', + fakeAsync(() => { + const removeRightsSpy = spyOn( + contributorDashboardAdminBackendApiService, + 'removeContributionReviewerAsync' + ); + const changes: SimpleChanges = { + activeTab: { + currentValue: component.TAB_NAME_QUESTION_SUBMITTER, + previousValue: null, + firstChange: true, + isFirstChange: () => true, + }, + }; + component.inputs.activeTab = component.TAB_NAME_QUESTION_SUBMITTER; + component.ngOnChanges(changes); + spyOn( + contributorDashboardAdminBackendApiService, + 'contributionReviewerRightsAsync' + ).and.returnValue( + Promise.resolve({ + can_submit_questions: true, + can_review_questions: true, + can_review_translation_for_language_codes: [], + can_review_voiceover_for_language_codes: [], }) - }) as NgbModalRef; - }); - - component.openRoleEditor('user1'); - tick(); - - expect(modalSpy).toHaveBeenCalledWith(CdAdminQuestionRoleEditorModal); - expect(removeRightsSpy).toHaveBeenCalled(); - })); - - it('should open question role editor modal and return true changed' + - ' value of translation reviewer', fakeAsync(() => { - const addRightsSpy = spyOn( - contributorDashboardAdminBackendApiService, - 'addContributionReviewerAsync'); - const changes: SimpleChanges = { - activeTab: { - currentValue: component.TAB_NAME_QUESTION_SUBMITTER, - previousValue: null, - firstChange: true, - isFirstChange: () => true, - }, - }; - component.inputs.activeTab = component.TAB_NAME_QUESTION_SUBMITTER; - component.ngOnChanges(changes); - spyOn( - contributorDashboardAdminBackendApiService, - 'contributionReviewerRightsAsync').and.returnValue(Promise.resolve({ - can_submit_questions: true, - can_review_questions: false, - can_review_translation_for_language_codes: [], - can_review_voiceover_for_language_codes: [] - })); - let modalSpy = spyOn(ngbModal, 'open').and.callFake(() => { - return ({ - componentInstance: MockNgbModalRef, - result: Promise.resolve({ - isQuestionSubmitter: true, - isQuestionReviewer: true + ); + let modalSpy = spyOn(ngbModal, 'open').and.callFake(() => { + return { + componentInstance: MockNgbModalRef, + result: Promise.resolve({ + isQuestionSubmitter: true, + isQuestionReviewer: false, + }), + } as NgbModalRef; + }); + + component.openRoleEditor('user1'); + tick(); + + expect(modalSpy).toHaveBeenCalledWith(CdAdminQuestionRoleEditorModal); + expect(removeRightsSpy).toHaveBeenCalled(); + }) + ); + + it( + 'should open question role editor modal and return true changed' + + ' value of translation reviewer', + fakeAsync(() => { + const addRightsSpy = spyOn( + contributorDashboardAdminBackendApiService, + 'addContributionReviewerAsync' + ); + const changes: SimpleChanges = { + activeTab: { + currentValue: component.TAB_NAME_QUESTION_SUBMITTER, + previousValue: null, + firstChange: true, + isFirstChange: () => true, + }, + }; + component.inputs.activeTab = component.TAB_NAME_QUESTION_SUBMITTER; + component.ngOnChanges(changes); + spyOn( + contributorDashboardAdminBackendApiService, + 'contributionReviewerRightsAsync' + ).and.returnValue( + Promise.resolve({ + can_submit_questions: true, + can_review_questions: false, + can_review_translation_for_language_codes: [], + can_review_voiceover_for_language_codes: [], }) - }) as NgbModalRef; - }); - - component.openRoleEditor('user1'); - tick(); - - expect(modalSpy).toHaveBeenCalledWith(CdAdminQuestionRoleEditorModal); - expect(addRightsSpy).toHaveBeenCalled(); - })); - - it('should open question role editor modal and return true changed' + - ' value of translation submitter', fakeAsync(() => { - const addRightsSpy = spyOn( - contributorDashboardAdminBackendApiService, - 'addContributionReviewerAsync'); - const changes: SimpleChanges = { - activeTab: { - currentValue: component.TAB_NAME_QUESTION_SUBMITTER, - previousValue: null, - firstChange: true, - isFirstChange: () => true, - }, - }; - component.inputs.activeTab = component.TAB_NAME_QUESTION_SUBMITTER; - component.ngOnChanges(changes); - spyOn( - contributorDashboardAdminBackendApiService, - 'contributionReviewerRightsAsync').and.returnValue(Promise.resolve({ - can_submit_questions: false, - can_review_questions: true, - can_review_translation_for_language_codes: [], - can_review_voiceover_for_language_codes: [] - })); - let modalSpy = spyOn(ngbModal, 'open').and.callFake(() => { - return ({ - componentInstance: MockNgbModalRef, - result: Promise.resolve({ - isQuestionSubmitter: true, - isQuestionReviewer: true + ); + let modalSpy = spyOn(ngbModal, 'open').and.callFake(() => { + return { + componentInstance: MockNgbModalRef, + result: Promise.resolve({ + isQuestionSubmitter: true, + isQuestionReviewer: true, + }), + } as NgbModalRef; + }); + + component.openRoleEditor('user1'); + tick(); + + expect(modalSpy).toHaveBeenCalledWith(CdAdminQuestionRoleEditorModal); + expect(addRightsSpy).toHaveBeenCalled(); + }) + ); + + it( + 'should open question role editor modal and return true changed' + + ' value of translation submitter', + fakeAsync(() => { + const addRightsSpy = spyOn( + contributorDashboardAdminBackendApiService, + 'addContributionReviewerAsync' + ); + const changes: SimpleChanges = { + activeTab: { + currentValue: component.TAB_NAME_QUESTION_SUBMITTER, + previousValue: null, + firstChange: true, + isFirstChange: () => true, + }, + }; + component.inputs.activeTab = component.TAB_NAME_QUESTION_SUBMITTER; + component.ngOnChanges(changes); + spyOn( + contributorDashboardAdminBackendApiService, + 'contributionReviewerRightsAsync' + ).and.returnValue( + Promise.resolve({ + can_submit_questions: false, + can_review_questions: true, + can_review_translation_for_language_codes: [], + can_review_voiceover_for_language_codes: [], }) - }) as NgbModalRef; - }); - - component.openRoleEditor('user1'); - tick(); - - expect(modalSpy).toHaveBeenCalledWith(CdAdminQuestionRoleEditorModal); - expect(addRightsSpy).toHaveBeenCalled(); - })); + ); + let modalSpy = spyOn(ngbModal, 'open').and.callFake(() => { + return { + componentInstance: MockNgbModalRef, + result: Promise.resolve({ + isQuestionSubmitter: true, + isQuestionReviewer: true, + }), + } as NgbModalRef; + }); + + component.openRoleEditor('user1'); + tick(); + + expect(modalSpy).toHaveBeenCalledWith(CdAdminQuestionRoleEditorModal); + expect(addRightsSpy).toHaveBeenCalled(); + }) + ); it('should open translation role editor modal', fakeAsync(() => { const changes: SimpleChanges = { @@ -536,16 +583,19 @@ describe('Contributor stats component', () => { component.ngOnChanges(changes); spyOn( contributorDashboardAdminBackendApiService, - 'contributionReviewerRightsAsync').and.returnValue(Promise.resolve({ - can_submit_questions: false, - can_review_questions: true, - can_review_translation_for_language_codes: [], - can_review_voiceover_for_language_codes: [] - })); + 'contributionReviewerRightsAsync' + ).and.returnValue( + Promise.resolve({ + can_submit_questions: false, + can_review_questions: true, + can_review_translation_for_language_codes: [], + can_review_voiceover_for_language_codes: [], + }) + ); let modalSpy = spyOn(ngbModal, 'open').and.callFake(() => { - return ({ - componentInstance: MockNgbModalRef - }) as NgbModalRef; + return { + componentInstance: MockNgbModalRef, + } as NgbModalRef; }); component.openRoleEditor('user1'); diff --git a/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-tables/contributor-admin-stats-table.component.ts b/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-tables/contributor-admin-stats-table.component.ts index 2f239666cee9..8dbfb4f1333b 100644 --- a/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-tables/contributor-admin-stats-table.component.ts +++ b/core/templates/pages/contributor-dashboard-admin-page/contributor-dashboard-tables/contributor-admin-stats-table.component.ts @@ -12,24 +12,28 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Component for the Contributor Admin Dashboard table. */ -import { Component, Input, OnInit, SimpleChanges } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component, Input, OnInit, SimpleChanges} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { animate, state, style, transition, trigger } from '@angular/animations'; -import { ContributorDashboardAdminStatsBackendApiService } from '../services/contributor-dashboard-admin-stats-backend-api.service'; -import { ContributorDashboardAdminBackendApiService } from '../services/contributor-dashboard-admin-backend-api.service'; -import { ContributorAdminDashboardFilter } from '../contributor-admin-dashboard-filter.model'; -import { AppConstants } from 'app.constants'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { QuestionReviewerStats, QuestionSubmitterStats, TranslationReviewerStats, TranslationSubmitterStats } from '../contributor-dashboard-admin-summary.model'; -import { CdAdminQuestionRoleEditorModal } from '../question-role-editor-modal/cd-admin-question-role-editor-modal.component'; -import { CdAdminTranslationRoleEditorModal } from '../translation-role-editor-modal/cd-admin-translation-role-editor-modal.component'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {animate, state, style, transition, trigger} from '@angular/animations'; +import {ContributorDashboardAdminStatsBackendApiService} from '../services/contributor-dashboard-admin-stats-backend-api.service'; +import {ContributorDashboardAdminBackendApiService} from '../services/contributor-dashboard-admin-backend-api.service'; +import {ContributorAdminDashboardFilter} from '../contributor-admin-dashboard-filter.model'; +import {AppConstants} from 'app.constants'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import { + QuestionReviewerStats, + QuestionSubmitterStats, + TranslationReviewerStats, + TranslationSubmitterStats, +} from '../contributor-dashboard-admin-summary.model'; +import {CdAdminQuestionRoleEditorModal} from '../question-role-editor-modal/cd-admin-question-role-editor-modal.component'; +import {CdAdminTranslationRoleEditorModal} from '../translation-role-editor-modal/cd-admin-translation-role-editor-modal.component'; import constants from 'assets/constants'; import isEqual from 'lodash/isEqual'; @@ -38,34 +42,38 @@ import isEqual from 'lodash/isEqual'; templateUrl: './contributor-admin-stats-table.component.html', animations: [ trigger('detailExpand', [ - state('collapsed', style( - {height: '0px', minHeight: '0', paddingBottom: '0'})), + state( + 'collapsed', + style({height: '0px', minHeight: '0', paddingBottom: '0'}) + ), state('expanded', style({height: '*'})), transition( 'expanded <=> collapsed', - animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')), + animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)') + ), ]), trigger('chevronExpand', [ - state('expanded', style({ transform: 'rotate(90deg)' })), - state('collapsed', style({ transform: 'rotate(0)' })), + state('expanded', style({transform: 'rotate(90deg)'})), + state('collapsed', style({transform: 'rotate(0)'})), transition('expanded => collapsed', animate('200ms ease-out')), transition('collapsed => expanded', animate('200ms ease-in')), ]), trigger('mobileChevronExpand', [ - state('expanded', style({ transform: 'rotate(-90deg)' })), - state('collapsed', style({ transform: 'rotate(90deg)' })), + state('expanded', style({transform: 'rotate(-90deg)'})), + state('collapsed', style({transform: 'rotate(90deg)'})), transition('expanded => collapsed', animate('200ms ease-out')), transition('collapsed => expanded', animate('200ms ease-in')), ]), ], }) export class ContributorAdminStatsTable implements OnInit { - @Input() inputs: { activeTab: string; - filter: ContributorAdminDashboardFilter; } = - { - activeTab: 'Translation Submitter', - filter: ContributorAdminDashboardFilter.createDefault() - }; + @Input() inputs: { + activeTab: string; + filter: ContributorAdminDashboardFilter; + } = { + activeTab: 'Translation Submitter', + filter: ContributorAdminDashboardFilter.createDefault(), + }; columnsToDisplay = [ 'chevron', @@ -74,21 +82,25 @@ export class ContributorAdminStatsTable implements OnInit { 'overallAccuracy', 'submittedTranslationsCount', 'lastContributedInDays', - 'role' + 'role', ]; - dataSource: TranslationSubmitterStats[] | - TranslationReviewerStats[] | - QuestionSubmitterStats[] | - QuestionReviewerStats[] = []; + dataSource: + | TranslationSubmitterStats[] + | TranslationReviewerStats[] + | QuestionSubmitterStats[] + | QuestionReviewerStats[] = []; nextOffset: number = 0; more: boolean = true; - expandedElement: TranslationSubmitterStats[] | - TranslationReviewerStats[] | - QuestionSubmitterStats[] | - QuestionReviewerStats[] | null | [] = null; + expandedElement: + | TranslationSubmitterStats[] + | TranslationReviewerStats[] + | QuestionSubmitterStats[] + | QuestionReviewerStats[] + | null + | [] = null; TAB_NAME_TRANSLATION_SUBMITTER: string = 'Translation Submitter'; TAB_NAME_TRANSLATION_REVIEWER: string = 'Translation Reviewer'; @@ -107,11 +119,9 @@ export class ContributorAdminStatsTable implements OnInit { constructor( private windowRef: WindowRef, - private ContributorDashboardAdminStatsBackendApiService: - ContributorDashboardAdminStatsBackendApiService, - private contributorDashboardAdminBackendApiService: - ContributorDashboardAdminBackendApiService, - private modalService: NgbModal, + private ContributorDashboardAdminStatsBackendApiService: ContributorDashboardAdminStatsBackendApiService, + private contributorDashboardAdminBackendApiService: ContributorDashboardAdminBackendApiService, + private modalService: NgbModal ) {} ngOnInit(): void { @@ -123,100 +133,109 @@ export class ContributorAdminStatsTable implements OnInit { openCdAdminQuestionRoleEditorModal(username: string): void { this.contributorDashboardAdminBackendApiService - .contributionReviewerRightsAsync(username).then(response => { - const modelRef = this.modalService.open( - CdAdminQuestionRoleEditorModal); + .contributionReviewerRightsAsync(username) + .then(response => { + const modelRef = this.modalService.open(CdAdminQuestionRoleEditorModal); modelRef.componentInstance.username = username; modelRef.componentInstance.rights = { isQuestionSubmitter: response.can_submit_questions, - isQuestionReviewer: response.can_review_questions + isQuestionReviewer: response.can_review_questions, }; - modelRef.result.then(results => { - if (results.isQuestionSubmitter !== response.can_submit_questions) { - if (results.isQuestionSubmitter) { - this.contributorDashboardAdminBackendApiService - .addContributionReviewerAsync( + modelRef.result.then( + results => { + if (results.isQuestionSubmitter !== response.can_submit_questions) { + if (results.isQuestionSubmitter) { + this.contributorDashboardAdminBackendApiService.addContributionReviewerAsync( constants.CD_USER_RIGHTS_CATEGORY_SUBMIT_QUESTION, username, null ); - } else { - this.contributorDashboardAdminBackendApiService - .removeContributionReviewerAsync( + } else { + this.contributorDashboardAdminBackendApiService.removeContributionReviewerAsync( username, constants.CD_USER_RIGHTS_CATEGORY_SUBMIT_QUESTION, null ); + } } - } - if (results.isQuestionReviewer !== response.can_review_questions) { - if (results.isQuestionReviewer) { - this.contributorDashboardAdminBackendApiService - .addContributionReviewerAsync( + if (results.isQuestionReviewer !== response.can_review_questions) { + if (results.isQuestionReviewer) { + this.contributorDashboardAdminBackendApiService.addContributionReviewerAsync( constants.CD_USER_RIGHTS_CATEGORY_REVIEW_QUESTION, username, null ); - } else { - this.contributorDashboardAdminBackendApiService - .removeContributionReviewerAsync( + } else { + this.contributorDashboardAdminBackendApiService.removeContributionReviewerAsync( username, constants.CD_USER_RIGHTS_CATEGORY_REVIEW_QUESTION, null ); + } } + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. } - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + ); }); } openCdAdminTranslationRoleEditorModal(username: string): void { this.contributorDashboardAdminBackendApiService - .contributionReviewerRightsAsync(username).then(response => { + .contributionReviewerRightsAsync(username) + .then(response => { const modalRef = this.modalService.open( - CdAdminTranslationRoleEditorModal); + CdAdminTranslationRoleEditorModal + ); modalRef.componentInstance.username = username; - modalRef.componentInstance.assignedLanguageIds = ( - response.can_review_translation_for_language_codes); + modalRef.componentInstance.assignedLanguageIds = + response.can_review_translation_for_language_codes; const languageIdToName: Record = {}; constants.SUPPORTED_AUDIO_LANGUAGES.forEach( - language => languageIdToName[language.id] = language.description); + language => (languageIdToName[language.id] = language.description) + ); modalRef.componentInstance.languageIdToName = languageIdToName; }); } getUpperLimitValueForPagination(): number { - return ( - Math.min(( - (this.statsPageNumber * this.itemsPerPage) + - this.itemsPerPage), (this.statsPageNumber * this.itemsPerPage) + - this.dataSource.length)); + return Math.min( + this.statsPageNumber * this.itemsPerPage + this.itemsPerPage, + this.statsPageNumber * this.itemsPerPage + this.dataSource.length + ); } openRoleEditor(username: string): void { this.expandedElement = null; - if (this.inputs.activeTab === this.TAB_NAME_TRANSLATION_REVIEWER || - this.inputs.activeTab === this.TAB_NAME_TRANSLATION_SUBMITTER) { + if ( + this.inputs.activeTab === this.TAB_NAME_TRANSLATION_REVIEWER || + this.inputs.activeTab === this.TAB_NAME_TRANSLATION_SUBMITTER + ) { this.openCdAdminTranslationRoleEditorModal(username); - } else if (this.inputs.activeTab === this.TAB_NAME_QUESTION_SUBMITTER || - this.inputs.activeTab === this.TAB_NAME_QUESTION_REVIEWER) { + } else if ( + this.inputs.activeTab === this.TAB_NAME_QUESTION_SUBMITTER || + this.inputs.activeTab === this.TAB_NAME_QUESTION_REVIEWER + ) { this.openCdAdminQuestionRoleEditorModal(username); } } checkMobileView(): boolean { - return (this.windowRef.nativeWindow.innerWidth < 800); + return this.windowRef.nativeWindow.innerWidth < 800; } changesExist(changes: SimpleChanges): boolean { let changesExist = false; for (let propName in changes) { - if (!isEqual(changes[propName].currentValue, - changes[propName].previousValue)) { + if ( + !isEqual( + changes[propName].currentValue, + changes[propName].previousValue + ) + ) { changesExist = true; break; } @@ -241,7 +260,7 @@ export class ContributorAdminStatsTable implements OnInit { 'overallAccuracy', 'submittedTranslationsCount', 'lastContributedInDays', - 'role' + 'role', ]; if (this.checkMobileView()) { this.columnsToDisplay = [ @@ -251,33 +270,32 @@ export class ContributorAdminStatsTable implements OnInit { 'submittedTranslationsCount', 'lastContributedInDays', 'role', - 'chevron' + 'chevron', ]; } - this.ContributorDashboardAdminStatsBackendApiService - .fetchContributorAdminStats( - this.inputs.filter, - this.itemsPerPage, - this.nextOffset, - AppConstants.CONTRIBUTION_STATS_TYPE_TRANSLATION, - AppConstants.CONTRIBUTION_STATS_SUBTYPE_SUBMISSION).then( - (response) => { - this.dataSource = response.stats; - this.nextOffset = response.nextOffset; - this.more = response.more; - this.loadingMessage = ''; - this.noDataMessage = ''; - if (this.dataSource.length === 0) { - this.noDataMessage = 'No statistics to display'; - } - }); + this.ContributorDashboardAdminStatsBackendApiService.fetchContributorAdminStats( + this.inputs.filter, + this.itemsPerPage, + this.nextOffset, + AppConstants.CONTRIBUTION_STATS_TYPE_TRANSLATION, + AppConstants.CONTRIBUTION_STATS_SUBTYPE_SUBMISSION + ).then(response => { + this.dataSource = response.stats; + this.nextOffset = response.nextOffset; + this.more = response.more; + this.loadingMessage = ''; + this.noDataMessage = ''; + if (this.dataSource.length === 0) { + this.noDataMessage = 'No statistics to display'; + } + }); } else if (this.inputs.activeTab === this.TAB_NAME_TRANSLATION_REVIEWER) { this.columnsToDisplay = [ 'chevron', 'contributorName', 'reviewedTranslationsCount', 'lastContributedInDays', - 'role' + 'role', ]; if (this.checkMobileView()) { this.columnsToDisplay = [ @@ -285,26 +303,25 @@ export class ContributorAdminStatsTable implements OnInit { 'reviewedTranslationsCount', 'lastContributedInDays', 'role', - 'chevron' + 'chevron', ]; } - this.ContributorDashboardAdminStatsBackendApiService - .fetchContributorAdminStats( - this.inputs.filter, - this.itemsPerPage, - this.nextOffset, - AppConstants.CONTRIBUTION_STATS_TYPE_TRANSLATION, - AppConstants.CONTRIBUTION_STATS_SUBTYPE_REVIEW).then( - (response) => { - this.dataSource = response.stats; - this.nextOffset = response.nextOffset; - this.more = response.more; - this.loadingMessage = ''; - this.noDataMessage = ''; - if (this.dataSource.length === 0) { - this.noDataMessage = 'No statistics to display'; - } - }); + this.ContributorDashboardAdminStatsBackendApiService.fetchContributorAdminStats( + this.inputs.filter, + this.itemsPerPage, + this.nextOffset, + AppConstants.CONTRIBUTION_STATS_TYPE_TRANSLATION, + AppConstants.CONTRIBUTION_STATS_SUBTYPE_REVIEW + ).then(response => { + this.dataSource = response.stats; + this.nextOffset = response.nextOffset; + this.more = response.more; + this.loadingMessage = ''; + this.noDataMessage = ''; + if (this.dataSource.length === 0) { + this.noDataMessage = 'No statistics to display'; + } + }); } else if (this.inputs.activeTab === this.TAB_NAME_QUESTION_SUBMITTER) { this.columnsToDisplay = [ 'chevron', @@ -313,7 +330,7 @@ export class ContributorAdminStatsTable implements OnInit { 'overallAccuracy', 'submittedQuestionsCount', 'lastContributedInDays', - 'role' + 'role', ]; if (this.checkMobileView()) { this.columnsToDisplay = [ @@ -323,33 +340,32 @@ export class ContributorAdminStatsTable implements OnInit { 'submittedQuestionsCount', 'lastContributedInDays', 'role', - 'chevron' + 'chevron', ]; } - this.ContributorDashboardAdminStatsBackendApiService - .fetchContributorAdminStats( - this.inputs.filter, - this.itemsPerPage, - this.nextOffset, - AppConstants.CONTRIBUTION_STATS_TYPE_QUESTION, - AppConstants.CONTRIBUTION_STATS_SUBTYPE_SUBMISSION).then( - (response) => { - this.dataSource = response.stats; - this.nextOffset = response.nextOffset; - this.more = response.more; - this.loadingMessage = ''; - this.noDataMessage = ''; - if (this.dataSource.length === 0) { - this.noDataMessage = 'No statistics to display'; - } - }); + this.ContributorDashboardAdminStatsBackendApiService.fetchContributorAdminStats( + this.inputs.filter, + this.itemsPerPage, + this.nextOffset, + AppConstants.CONTRIBUTION_STATS_TYPE_QUESTION, + AppConstants.CONTRIBUTION_STATS_SUBTYPE_SUBMISSION + ).then(response => { + this.dataSource = response.stats; + this.nextOffset = response.nextOffset; + this.more = response.more; + this.loadingMessage = ''; + this.noDataMessage = ''; + if (this.dataSource.length === 0) { + this.noDataMessage = 'No statistics to display'; + } + }); } else if (this.inputs.activeTab === this.TAB_NAME_QUESTION_REVIEWER) { this.columnsToDisplay = [ 'chevron', 'contributorName', 'reviewedQuestionsCount', 'lastContributedInDays', - 'role' + 'role', ]; if (this.checkMobileView()) { this.columnsToDisplay = [ @@ -357,26 +373,25 @@ export class ContributorAdminStatsTable implements OnInit { 'reviewedQuestionsCount', 'lastContributedInDays', 'role', - 'chevron' + 'chevron', ]; } - this.ContributorDashboardAdminStatsBackendApiService - .fetchContributorAdminStats( - this.inputs.filter, - this.itemsPerPage, - this.nextOffset, - AppConstants.CONTRIBUTION_STATS_TYPE_QUESTION, - AppConstants.CONTRIBUTION_STATS_SUBTYPE_REVIEW).then( - (response) => { - this.dataSource = response.stats; - this.nextOffset = response.nextOffset; - this.more = response.more; - this.loadingMessage = ''; - this.noDataMessage = ''; - if (this.dataSource.length === 0) { - this.noDataMessage = 'No statistics to display'; - } - }); + this.ContributorDashboardAdminStatsBackendApiService.fetchContributorAdminStats( + this.inputs.filter, + this.itemsPerPage, + this.nextOffset, + AppConstants.CONTRIBUTION_STATS_TYPE_QUESTION, + AppConstants.CONTRIBUTION_STATS_SUBTYPE_REVIEW + ).then(response => { + this.dataSource = response.stats; + this.nextOffset = response.nextOffset; + this.more = response.more; + this.loadingMessage = ''; + this.noDataMessage = ''; + if (this.dataSource.length === 0) { + this.noDataMessage = 'No statistics to display'; + } + }); } } @@ -391,7 +406,7 @@ export class ContributorAdminStatsTable implements OnInit { goToPageNumber(pageNumber: number): void { this.statsPageNumber = pageNumber; - this.nextOffset = (pageNumber * this.itemsPerPage); + this.nextOffset = pageNumber * this.itemsPerPage; this.updateColumnsToDisplay(); } @@ -406,8 +421,9 @@ export class ContributorAdminStatsTable implements OnInit { } } - -angular.module('oppia').directive('contributorAdminDashboardPage', +angular.module('oppia').directive( + 'contributorAdminDashboardPage', downgradeComponent({ - component: ContributorAdminStatsTable - }) as angular.IDirectiveFactory); + component: ContributorAdminStatsTable, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/contributor-dashboard-admin-page/navbar/contributor-dashboard-admin-navbar.component.spec.ts b/core/templates/pages/contributor-dashboard-admin-page/navbar/contributor-dashboard-admin-navbar.component.spec.ts index 6a7ac8f99169..91f0988a7c65 100644 --- a/core/templates/pages/contributor-dashboard-admin-page/navbar/contributor-dashboard-admin-navbar.component.spec.ts +++ b/core/templates/pages/contributor-dashboard-admin-page/navbar/contributor-dashboard-admin-navbar.component.spec.ts @@ -16,17 +16,22 @@ * @fileoverview Unit tests for contributor dashboard admin navbar component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { APP_BASE_HREF } from '@angular/common'; -import { SmartRouterModule } from 'hybrid-router-module-provider'; -import { UserInfo } from 'domain/user/user-info.model'; -import { RouterModule } from '@angular/router'; - -import { UserService } from 'services/user.service'; - -import { ContributorDashboardAdminNavbarComponent } from './contributor-dashboard-admin-navbar.component'; - +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import { + async, + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {APP_BASE_HREF} from '@angular/common'; +import {SmartRouterModule} from 'hybrid-router-module-provider'; +import {UserInfo} from 'domain/user/user-info.model'; +import {RouterModule} from '@angular/router'; + +import {UserService} from 'services/user.service'; + +import {ContributorDashboardAdminNavbarComponent} from './contributor-dashboard-admin-navbar.component'; describe('Contributor dashboard admin navbar component', () => { let component: ContributorDashboardAdminNavbarComponent; @@ -34,7 +39,7 @@ describe('Contributor dashboard admin navbar component', () => { let userInfo = { isModerator: () => true, getUsername: () => 'username1', - isSuperAdmin: () => true + isSuperAdmin: () => true, } as UserInfo; const profileUrl = '/profile/username1'; let fixture: ComponentFixture; @@ -46,33 +51,37 @@ describe('Contributor dashboard admin navbar component', () => { // TODO(#13443): Remove hybrid router module provider once all pages are // migrated to angular router. SmartRouterModule, - RouterModule.forRoot([]) + RouterModule.forRoot([]), ], declarations: [ContributorDashboardAdminNavbarComponent], - providers: [{ - provide: APP_BASE_HREF, - useValue: '/' - }] + providers: [ + { + provide: APP_BASE_HREF, + useValue: '/', + }, + ], }).compileComponents(); fixture = TestBed.createComponent(ContributorDashboardAdminNavbarComponent); component = fixture.componentInstance; userService = TestBed.get(UserService); fixture.detectChanges(); - spyOn(userService, 'getProfileImageDataUrl').and.returnValue( - ['default-image-url-png', 'default-image-url-webp']); + spyOn(userService, 'getProfileImageDataUrl').and.returnValue([ + 'default-image-url-png', + 'default-image-url-webp', + ]); })); it('should initialize component properties correctly', fakeAsync(() => { - spyOn(userService, 'getUserInfoAsync') - .and.resolveTo(userInfo); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo); component.ngOnInit(); tick(); expect(component.profilePicturePngDataUrl).toEqual('default-image-url-png'); expect(component.profilePictureWebpDataUrl).toEqual( - 'default-image-url-webp'); + 'default-image-url-webp' + ); expect(component.username).toBe('username1'); expect(component.profileUrl).toEqual(profileUrl); expect(component.logoutUrl).toEqual('/logout'); @@ -80,8 +89,7 @@ describe('Contributor dashboard admin navbar component', () => { })); it('should set profileDropdownIsActive to true', fakeAsync(() => { - spyOn(userService, 'getUserInfoAsync') - .and.resolveTo(userInfo); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo); component.ngOnInit(); tick(); @@ -94,8 +102,7 @@ describe('Contributor dashboard admin navbar component', () => { })); it('should set profileDropdownIsActive to false', fakeAsync(() => { - spyOn(userService, 'getUserInfoAsync') - .and.resolveTo(userInfo); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo); component.ngOnInit(); tick(); @@ -111,10 +118,9 @@ describe('Contributor dashboard admin navbar component', () => { let userInfo = { isModerator: () => true, getUsername: () => null, - isSuperAdmin: () => true + isSuperAdmin: () => true, } as UserInfo; - spyOn(userService, 'getUserInfoAsync') - .and.resolveTo(userInfo); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo); expect(() => { component.getUserInfoAsync(); diff --git a/core/templates/pages/contributor-dashboard-admin-page/navbar/contributor-dashboard-admin-navbar.component.ts b/core/templates/pages/contributor-dashboard-admin-page/navbar/contributor-dashboard-admin-navbar.component.ts index 474f385ff47a..12766b3428bc 100644 --- a/core/templates/pages/contributor-dashboard-admin-page/navbar/contributor-dashboard-admin-navbar.component.ts +++ b/core/templates/pages/contributor-dashboard-admin-page/navbar/contributor-dashboard-admin-navbar.component.ts @@ -17,11 +17,11 @@ * contributor dashboard admin panel. */ -import { Component, OnInit } from '@angular/core'; +import {Component, OnInit} from '@angular/core'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { UserService } from 'services/user.service'; -import { AppConstants } from 'app.constants'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {UserService} from 'services/user.service'; +import {AppConstants} from 'app.constants'; @Component({ selector: 'oppia-contributor-dashboard-admin-navbar', @@ -38,16 +38,15 @@ export class ContributorDashboardAdminNavbarComponent implements OnInit { logoPngImageSrc!: string; // User name is null if the user is not logged in. username: string | null = null; - logoutUrl: string = ( - '/' + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LOGOUT.ROUTE); + logoutUrl: string = + '/' + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LOGOUT.ROUTE; profileDropdownIsActive: boolean = false; - PAGES_REGISTERED_WITH_FRONTEND = ( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND); + PAGES_REGISTERED_WITH_FRONTEND = AppConstants.PAGES_REGISTERED_WITH_FRONTEND; constructor( private urlInterpolationService: UrlInterpolationService, - private userService: UserService, + private userService: UserService ) {} activateProfileDropdown(): void { @@ -64,13 +63,14 @@ export class ContributorDashboardAdminNavbarComponent implements OnInit { if (this.username === null) { throw new Error('User name is null.'); } else { - this.profileUrl = ( - this.urlInterpolationService.interpolateUrl('/profile/', { - username: this.username - }) + this.profileUrl = this.urlInterpolationService.interpolateUrl( + '/profile/', + { + username: this.username, + } ); - [this.profilePicturePngDataUrl, this.profilePictureWebpDataUrl] = ( - this.userService.getProfileImageDataUrl(this.username)); + [this.profilePicturePngDataUrl, this.profilePictureWebpDataUrl] = + this.userService.getProfileImageDataUrl(this.username); } } @@ -78,8 +78,10 @@ export class ContributorDashboardAdminNavbarComponent implements OnInit { this.getUserInfoAsync(); this.logoPngImageSrc = this.urlInterpolationService.getStaticImageUrl( - '/logo/288x128_logo_white.png'); + '/logo/288x128_logo_white.png' + ); this.logoWebpImageSrc = this.urlInterpolationService.getStaticImageUrl( - '/logo/288x128_logo_white.webp'); + '/logo/288x128_logo_white.webp' + ); } } diff --git a/core/templates/pages/contributor-dashboard-admin-page/question-role-editor-modal/cd-admin-question-role-editor-modal.component.spec.ts b/core/templates/pages/contributor-dashboard-admin-page/question-role-editor-modal/cd-admin-question-role-editor-modal.component.spec.ts index 1c87b094bd10..2d086baed616 100644 --- a/core/templates/pages/contributor-dashboard-admin-page/question-role-editor-modal/cd-admin-question-role-editor-modal.component.spec.ts +++ b/core/templates/pages/contributor-dashboard-admin-page/question-role-editor-modal/cd-admin-question-role-editor-modal.component.spec.ts @@ -16,12 +16,12 @@ * @fileoverview Unit tests for CdAdminQuestionRoleEditorModal. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { FormsModule } from '@angular/forms'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {FormsModule} from '@angular/forms'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; -import { CdAdminQuestionRoleEditorModal } from './cd-admin-question-role-editor-modal.component'; +import {CdAdminQuestionRoleEditorModal} from './cd-admin-question-role-editor-modal.component'; describe('CdAdminQuestionRoleEditorModal', () => { let component: CdAdminQuestionRoleEditorModal; @@ -30,22 +30,14 @@ describe('CdAdminQuestionRoleEditorModal', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - FormsModule, - HttpClientTestingModule - ], - declarations: [ - CdAdminQuestionRoleEditorModal - ], - providers: [ - NgbActiveModal - ] + imports: [FormsModule, HttpClientTestingModule], + declarations: [CdAdminQuestionRoleEditorModal], + providers: [NgbActiveModal], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent( - CdAdminQuestionRoleEditorModal); + fixture = TestBed.createComponent(CdAdminQuestionRoleEditorModal); component = fixture.componentInstance; ngbActiveModal = TestBed.get(NgbActiveModal); component.ngOnInit(); @@ -54,7 +46,7 @@ describe('CdAdminQuestionRoleEditorModal', () => { it('should properly toggle Question Submitter checkbox', () => { component.rights = { isQuestionSubmitter: true, - isQuestionReviewer: false + isQuestionReviewer: false, }; fixture.detectChanges(); @@ -62,14 +54,14 @@ describe('CdAdminQuestionRoleEditorModal', () => { expect(component.rights).toEqual({ isQuestionSubmitter: false, - isQuestionReviewer: false + isQuestionReviewer: false, }); }); it('should properly toggle Question reviewer checkbox', () => { component.rights = { isQuestionSubmitter: true, - isQuestionReviewer: false + isQuestionReviewer: false, }; fixture.detectChanges(); @@ -77,14 +69,14 @@ describe('CdAdminQuestionRoleEditorModal', () => { expect(component.rights).toEqual({ isQuestionSubmitter: true, - isQuestionReviewer: true + isQuestionReviewer: true, }); }); it('should save and close modal and return selected rights', () => { component.rights = { isQuestionSubmitter: true, - isQuestionReviewer: false + isQuestionReviewer: false, }; const modalCloseSpy = spyOn(ngbActiveModal, 'close').and.callThrough(); @@ -96,7 +88,7 @@ describe('CdAdminQuestionRoleEditorModal', () => { it('should close modal without returning anything', () => { component.rights = { isQuestionSubmitter: true, - isQuestionReviewer: false + isQuestionReviewer: false, }; const modalCloseSpy = spyOn(ngbActiveModal, 'dismiss').and.callThrough(); diff --git a/core/templates/pages/contributor-dashboard-admin-page/question-role-editor-modal/cd-admin-question-role-editor-modal.component.ts b/core/templates/pages/contributor-dashboard-admin-page/question-role-editor-modal/cd-admin-question-role-editor-modal.component.ts index abb44569e1e2..9d39f7be5f81 100644 --- a/core/templates/pages/contributor-dashboard-admin-page/question-role-editor-modal/cd-admin-question-role-editor-modal.component.ts +++ b/core/templates/pages/contributor-dashboard-admin-page/question-role-editor-modal/cd-admin-question-role-editor-modal.component.ts @@ -16,12 +16,12 @@ * @fileoverview Component for editing a user's question contribution rights. */ -import { Component, OnInit, Input } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import {Component, OnInit, Input} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; export interface QuestionRights { - isQuestionSubmitter: boolean; - isQuestionReviewer: boolean; + isQuestionSubmitter: boolean; + isQuestionReviewer: boolean; } @Component({ @@ -29,15 +29,13 @@ export interface QuestionRights { templateUrl: './cd-admin-question-role-editor-modal.component.html', }) export class CdAdminQuestionRoleEditorModal implements OnInit { -// These properties are initialized using Angular lifecycle hooks -// and we need to do non-null assertion. For more information, see -// https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 + // These properties are initialized using Angular lifecycle hooks + // and we need to do non-null assertion. For more information, see + // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @Input() username!: string; @Input() rights!: QuestionRights; - constructor( - private activeModal: NgbActiveModal, - ) {} + constructor(private activeModal: NgbActiveModal) {} ngOnInit(): void {} diff --git a/core/templates/pages/contributor-dashboard-admin-page/services/contributor-dashboard-admin-backend-api.service.spec.ts b/core/templates/pages/contributor-dashboard-admin-page/services/contributor-dashboard-admin-backend-api.service.spec.ts index aa3520aef4cc..e646ec753293 100644 --- a/core/templates/pages/contributor-dashboard-admin-page/services/contributor-dashboard-admin-backend-api.service.spec.ts +++ b/core/templates/pages/contributor-dashboard-admin-page/services/contributor-dashboard-admin-backend-api.service.spec.ts @@ -16,12 +16,14 @@ * @fileoverview Unit tests for ContributorDashboardAdminBackendApiService. */ -import { HttpClientTestingModule, HttpTestingController } from - '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; -import { ContributorDashboardAdminBackendApiService } from './contributor-dashboard-admin-backend-api.service'; -import { CsrfTokenService } from 'services/csrf-token.service'; +import {ContributorDashboardAdminBackendApiService} from './contributor-dashboard-admin-backend-api.service'; +import {CsrfTokenService} from 'services/csrf-token.service'; describe('Contributor dashboard admin backend api service', () => { let cdabas: ContributorDashboardAdminBackendApiService; @@ -32,7 +34,7 @@ describe('Contributor dashboard admin backend api service', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] + imports: [HttpClientTestingModule], }); cdabas = TestBed.get(ContributorDashboardAdminBackendApiService); @@ -41,7 +43,7 @@ describe('Contributor dashboard admin backend api service', () => { successHandler = jasmine.createSpy('success'); failHandler = jasmine.createSpy('fail'); - spyOn(csrfService, 'getTokenAsync').and.callFake(async() => { + spyOn(csrfService, 'getTokenAsync').and.callFake(async () => { return Promise.resolve('sample-csrf-token'); }); }); @@ -50,307 +52,367 @@ describe('Contributor dashboard admin backend api service', () => { httpTestingController.verify(); }); - it('should add contribution rights to the user given the' + - 'name when calling addContributionReviewerAsync', fakeAsync(() => { - let category = 'translation'; - let languageCode = 'en'; - let username = 'validUser'; - let payload = { - username: username, - language_code: languageCode - }; - cdabas.addContributionReviewerAsync( - category, username, languageCode - ).then(successHandler, failHandler); - - let req = httpTestingController.expectOne( - '/contributionrightshandler/translation'); - expect(req.request.method).toEqual('POST'); - expect(req.request.body).toEqual(payload); - - req.flush( - { status: 200, statusText: 'Success.'}); - flushMicrotasks(); - - expect(successHandler).toHaveBeenCalled(); - expect(failHandler).not.toHaveBeenCalled(); - } - )); - - it('should fail to add contribution rights to the user when user does' + - 'not exist when calling addContributionReviewerAsync', fakeAsync(() => { - let category = 'translation'; - let languageCode = 'en'; - let username = 'InvalidUser'; - let payload = { - username: username, - language_code: languageCode - }; - cdabas.addContributionReviewerAsync( - category, username, languageCode - ).then(successHandler, failHandler); - - let req = httpTestingController.expectOne( - '/contributionrightshandler/translation'); - expect(req.request.method).toEqual('POST'); - expect(req.request.body).toEqual(payload); - req.flush( - { error: 'User with given username does not exist'}, - { status: 500, statusText: 'Internal Server Error'}); - flushMicrotasks(); - - expect(successHandler).not.toHaveBeenCalled(); - expect(failHandler).toHaveBeenCalledWith( - 'User with given username does not exist'); - })); - - it('should get the data of contribution rights given the role' + - 'when calling viewContributionReviewersAsync', fakeAsync(() => { - let category = 'translation'; - let languageCode = 'en'; - let result = ['validUsername']; - cdabas.viewContributionReviewersAsync( - category, languageCode - ).then(successHandler, failHandler); - - let req = httpTestingController.expectOne( - '/getcontributorusershandler/translation?language_code=en'); - expect(req.request.method).toEqual('GET'); - - req.flush( - ['validUsername'], - { status: 200, statusText: 'Success.'}); - flushMicrotasks(); - - expect(successHandler).toHaveBeenCalledWith(result); - expect(failHandler).not.toHaveBeenCalled(); - - category = 'question'; - - cdabas.viewContributionReviewersAsync( - category, null - ).then(successHandler, failHandler); - - req = httpTestingController.expectOne( - '/getcontributorusershandler/question'); - expect(req.request.method).toEqual('GET'); - - req.flush( - ['validUsername'], - { status: 200, statusText: 'Success.'}); - flushMicrotasks(); - - expect(successHandler).toHaveBeenCalledWith(result); - expect(failHandler).not.toHaveBeenCalled(); - })); - - it('should fail to get the data of contribution rights when category does' + - 'not exist when calling viewContributionReviewersAsync', fakeAsync(() => { - let category = 'InvalidCategory'; - let languageCode = 'en'; - cdabas.viewContributionReviewersAsync( - category, languageCode - ).then(successHandler, failHandler); - - let req = httpTestingController.expectOne( - '/getcontributorusershandler/InvalidCategory?language_code=en'); - expect(req.request.method).toEqual('GET'); - - req.flush({ - error: 'Invalid Category' - }, { - status: 500, statusText: 'Internal Server Error' - }); - flushMicrotasks(); - - expect(successHandler).not.toHaveBeenCalled(); - expect(failHandler).toHaveBeenCalledWith('Invalid Category'); - })); - - it('should get the data of contribution rights given the' + - 'username when calling contributionReviewerRightsAsync', fakeAsync(() => { - let username = 'validUsername'; - let result = { - can_review_questions: false, - can_submit_questions: false - }; - cdabas.contributionReviewerRightsAsync(username) - .then(successHandler, failHandler); - - let req = httpTestingController.expectOne( - '/contributionrightsdatahandler' + - '?username=validUsername'); - expect(req.request.method).toEqual('GET'); - - req.flush({ - can_review_questions: false, - can_submit_questions: false - }, { - status: 200, statusText: 'Success.' - }); - flushMicrotasks(); - - expect(successHandler).toHaveBeenCalledWith(result); - expect(failHandler).not.toHaveBeenCalled(); - })); - - it('should fail to get the data of contribution rights when username does' + - 'not exist when calling contributionReviewerRightsAsync', fakeAsync(() => { - let username = 'InvalidUsername'; - cdabas.contributionReviewerRightsAsync(username) - .then(successHandler, failHandler); - - let req = httpTestingController.expectOne( - '/contributionrightsdatahandler' + - '?username=InvalidUsername'); - expect(req.request.method).toEqual('GET'); - - req.flush({ - error: 'User with given username does not exist.' - }, { - status: 500, statusText: 'Internal Server Error' - }); - flushMicrotasks(); - - expect(successHandler).not.toHaveBeenCalled(); - expect(failHandler).toHaveBeenCalledWith( - 'User with given username does not exist.'); - })); - - it('should remove user contribution rights given the username' + - 'when calling removeContributionReviewerAsync', fakeAsync(() => { - let category = 'translation'; - let languageCode = 'en'; - let username = 'validUser'; - let payload = { - username: username, - language_code: languageCode - }; - cdabas.removeContributionReviewerAsync( - username, category, languageCode).then(successHandler, failHandler); - - const query = new URLSearchParams(payload); - const url = ( - '/contributionrightshandler/' + category + '?' + query.toString()); - const req = httpTestingController.expectOne(url); - expect(req.request.method).toEqual('DELETE'); - req.flush( - { status: 200, statusText: 'Success.'}); - flushMicrotasks(); - - expect(successHandler).toHaveBeenCalled(); - expect(failHandler).not.toHaveBeenCalled(); - })); - - it('should fail to remove user contribution rights when user does' + - 'not exist when calling removeContributionReviewerAsync', fakeAsync(() => { - let category = 'translation'; - let languageCode = 'en'; - let username = 'InvalidUser'; - let payload = { - username: username, - language_code: languageCode - }; - cdabas.removeContributionReviewerAsync( - username, category, languageCode).then(successHandler, failHandler); - - const query = new URLSearchParams(payload); - const url = ( - '/contributionrightshandler/' + category + '?' + query.toString()); - const req = httpTestingController.expectOne(url); - expect(req.request.method).toEqual('DELETE'); - - req.flush({ - error: 'User with given username does not exist.' - }, { - status: 500, statusText: 'Internal Server Error' - }); - flushMicrotasks(); - - expect(successHandler).not.toHaveBeenCalled(); - expect(failHandler).toHaveBeenCalledWith( - 'User with given username does not exist.'); - })); - - it('should remove specific contribution rights given the' + - 'username when calling removeContributionReviewerAsync', fakeAsync(() => { - let category = 'submit_question'; - let username = 'validUser'; - let payload = { - username: username - }; - cdabas.removeContributionReviewerAsync( - username, category, null).then(successHandler, failHandler); - - const query = new URLSearchParams(payload); - const url = ( - '/contributionrightshandler/' + category + '?' + query.toString()); - const req = httpTestingController.expectOne(url); - expect(req.request.method).toEqual('DELETE'); - req.flush( - { status: 200, statusText: 'Success.'}); - flushMicrotasks(); - - expect(successHandler).toHaveBeenCalled(); - expect(failHandler).not.toHaveBeenCalled(); - })); - - it('should return translation contribution stats given the username when ' + - 'calling viewTranslationContributionStatsAsync', fakeAsync(() => { - const username = 'validUsername'; - const result = { - translation_contribution_stats: [ + it( + 'should add contribution rights to the user given the' + + 'name when calling addContributionReviewerAsync', + fakeAsync(() => { + let category = 'translation'; + let languageCode = 'en'; + let username = 'validUser'; + let payload = { + username: username, + language_code: languageCode, + }; + cdabas + .addContributionReviewerAsync(category, username, languageCode) + .then(successHandler, failHandler); + + let req = httpTestingController.expectOne( + '/contributionrightshandler/translation' + ); + expect(req.request.method).toEqual('POST'); + expect(req.request.body).toEqual(payload); + + req.flush({status: 200, statusText: 'Success.'}); + flushMicrotasks(); + + expect(successHandler).toHaveBeenCalled(); + expect(failHandler).not.toHaveBeenCalled(); + }) + ); + + it( + 'should fail to add contribution rights to the user when user does' + + 'not exist when calling addContributionReviewerAsync', + fakeAsync(() => { + let category = 'translation'; + let languageCode = 'en'; + let username = 'InvalidUser'; + let payload = { + username: username, + language_code: languageCode, + }; + cdabas + .addContributionReviewerAsync(category, username, languageCode) + .then(successHandler, failHandler); + + let req = httpTestingController.expectOne( + '/contributionrightshandler/translation' + ); + expect(req.request.method).toEqual('POST'); + expect(req.request.body).toEqual(payload); + req.flush( + {error: 'User with given username does not exist'}, + {status: 500, statusText: 'Internal Server Error'} + ); + flushMicrotasks(); + + expect(successHandler).not.toHaveBeenCalled(); + expect(failHandler).toHaveBeenCalledWith( + 'User with given username does not exist' + ); + }) + ); + + it( + 'should get the data of contribution rights given the role' + + 'when calling viewContributionReviewersAsync', + fakeAsync(() => { + let category = 'translation'; + let languageCode = 'en'; + let result = ['validUsername']; + cdabas + .viewContributionReviewersAsync(category, languageCode) + .then(successHandler, failHandler); + + let req = httpTestingController.expectOne( + '/getcontributorusershandler/translation?language_code=en' + ); + expect(req.request.method).toEqual('GET'); + + req.flush(['validUsername'], {status: 200, statusText: 'Success.'}); + flushMicrotasks(); + + expect(successHandler).toHaveBeenCalledWith(result); + expect(failHandler).not.toHaveBeenCalled(); + + category = 'question'; + + cdabas + .viewContributionReviewersAsync(category, null) + .then(successHandler, failHandler); + + req = httpTestingController.expectOne( + '/getcontributorusershandler/question' + ); + expect(req.request.method).toEqual('GET'); + + req.flush(['validUsername'], {status: 200, statusText: 'Success.'}); + flushMicrotasks(); + + expect(successHandler).toHaveBeenCalledWith(result); + expect(failHandler).not.toHaveBeenCalled(); + }) + ); + + it( + 'should fail to get the data of contribution rights when category does' + + 'not exist when calling viewContributionReviewersAsync', + fakeAsync(() => { + let category = 'InvalidCategory'; + let languageCode = 'en'; + cdabas + .viewContributionReviewersAsync(category, languageCode) + .then(successHandler, failHandler); + + let req = httpTestingController.expectOne( + '/getcontributorusershandler/InvalidCategory?language_code=en' + ); + expect(req.request.method).toEqual('GET'); + + req.flush( + { + error: 'Invalid Category', + }, { - language: 'English', - topic_name: 'Topic Name', - submitted_translations_count: 2, - submitted_translation_word_count: 50, - accepted_translations_count: 1, - accepted_translations_without_reviewer_edits_count: 1, - accepted_translation_word_count: 50, - rejected_translations_count: 1, - rejected_translation_word_count: 150, - contribution_months: ['May 2021', 'Jun 2021'] + status: 500, + statusText: 'Internal Server Error', } - ] - }; - cdabas.viewTranslationContributionStatsAsync(username) - .then(successHandler, failHandler); - - const req = httpTestingController.expectOne( - '/translationcontributionstatshandler' + - '?username=' + username); - expect(req.request.method).toEqual('GET'); - req.flush(result, { - status: 200, statusText: 'Success.' - }); - flushMicrotasks(); - - expect(successHandler).toHaveBeenCalledWith(result); - expect(failHandler).not.toHaveBeenCalled(); - } - )); - - it('should fail to get translation contribution stats for invalid username' + - ' when calling viewTranslationContributionStatsAsync', fakeAsync(() => { - const username = 'InvalidUsername'; - cdabas.viewTranslationContributionStatsAsync(username) - .then(successHandler, failHandler); - - const req = httpTestingController.expectOne( - '/translationcontributionstatshandler' + - '?username=' + username); - expect(req.request.method).toEqual('GET'); - const errorMessage = 'Invalid username: ' + username; - req.flush({ - error: errorMessage - }, { - status: 500, statusText: 'Internal Server Error' - }); - flushMicrotasks(); + ); + flushMicrotasks(); + + expect(successHandler).not.toHaveBeenCalled(); + expect(failHandler).toHaveBeenCalledWith('Invalid Category'); + }) + ); + + it( + 'should get the data of contribution rights given the' + + 'username when calling contributionReviewerRightsAsync', + fakeAsync(() => { + let username = 'validUsername'; + let result = { + can_review_questions: false, + can_submit_questions: false, + }; + cdabas + .contributionReviewerRightsAsync(username) + .then(successHandler, failHandler); + + let req = httpTestingController.expectOne( + '/contributionrightsdatahandler' + '?username=validUsername' + ); + expect(req.request.method).toEqual('GET'); + + req.flush( + { + can_review_questions: false, + can_submit_questions: false, + }, + { + status: 200, + statusText: 'Success.', + } + ); + flushMicrotasks(); + + expect(successHandler).toHaveBeenCalledWith(result); + expect(failHandler).not.toHaveBeenCalled(); + }) + ); + + it( + 'should fail to get the data of contribution rights when username does' + + 'not exist when calling contributionReviewerRightsAsync', + fakeAsync(() => { + let username = 'InvalidUsername'; + cdabas + .contributionReviewerRightsAsync(username) + .then(successHandler, failHandler); + + let req = httpTestingController.expectOne( + '/contributionrightsdatahandler' + '?username=InvalidUsername' + ); + expect(req.request.method).toEqual('GET'); + + req.flush( + { + error: 'User with given username does not exist.', + }, + { + status: 500, + statusText: 'Internal Server Error', + } + ); + flushMicrotasks(); + + expect(successHandler).not.toHaveBeenCalled(); + expect(failHandler).toHaveBeenCalledWith( + 'User with given username does not exist.' + ); + }) + ); + + it( + 'should remove user contribution rights given the username' + + 'when calling removeContributionReviewerAsync', + fakeAsync(() => { + let category = 'translation'; + let languageCode = 'en'; + let username = 'validUser'; + let payload = { + username: username, + language_code: languageCode, + }; + cdabas + .removeContributionReviewerAsync(username, category, languageCode) + .then(successHandler, failHandler); + + const query = new URLSearchParams(payload); + const url = + '/contributionrightshandler/' + category + '?' + query.toString(); + const req = httpTestingController.expectOne(url); + expect(req.request.method).toEqual('DELETE'); + req.flush({status: 200, statusText: 'Success.'}); + flushMicrotasks(); + + expect(successHandler).toHaveBeenCalled(); + expect(failHandler).not.toHaveBeenCalled(); + }) + ); + + it( + 'should fail to remove user contribution rights when user does' + + 'not exist when calling removeContributionReviewerAsync', + fakeAsync(() => { + let category = 'translation'; + let languageCode = 'en'; + let username = 'InvalidUser'; + let payload = { + username: username, + language_code: languageCode, + }; + cdabas + .removeContributionReviewerAsync(username, category, languageCode) + .then(successHandler, failHandler); + + const query = new URLSearchParams(payload); + const url = + '/contributionrightshandler/' + category + '?' + query.toString(); + const req = httpTestingController.expectOne(url); + expect(req.request.method).toEqual('DELETE'); + + req.flush( + { + error: 'User with given username does not exist.', + }, + { + status: 500, + statusText: 'Internal Server Error', + } + ); + flushMicrotasks(); + + expect(successHandler).not.toHaveBeenCalled(); + expect(failHandler).toHaveBeenCalledWith( + 'User with given username does not exist.' + ); + }) + ); + + it( + 'should remove specific contribution rights given the' + + 'username when calling removeContributionReviewerAsync', + fakeAsync(() => { + let category = 'submit_question'; + let username = 'validUser'; + let payload = { + username: username, + }; + cdabas + .removeContributionReviewerAsync(username, category, null) + .then(successHandler, failHandler); + + const query = new URLSearchParams(payload); + const url = + '/contributionrightshandler/' + category + '?' + query.toString(); + const req = httpTestingController.expectOne(url); + expect(req.request.method).toEqual('DELETE'); + req.flush({status: 200, statusText: 'Success.'}); + flushMicrotasks(); + + expect(successHandler).toHaveBeenCalled(); + expect(failHandler).not.toHaveBeenCalled(); + }) + ); + + it( + 'should return translation contribution stats given the username when ' + + 'calling viewTranslationContributionStatsAsync', + fakeAsync(() => { + const username = 'validUsername'; + const result = { + translation_contribution_stats: [ + { + language: 'English', + topic_name: 'Topic Name', + submitted_translations_count: 2, + submitted_translation_word_count: 50, + accepted_translations_count: 1, + accepted_translations_without_reviewer_edits_count: 1, + accepted_translation_word_count: 50, + rejected_translations_count: 1, + rejected_translation_word_count: 150, + contribution_months: ['May 2021', 'Jun 2021'], + }, + ], + }; + cdabas + .viewTranslationContributionStatsAsync(username) + .then(successHandler, failHandler); + + const req = httpTestingController.expectOne( + '/translationcontributionstatshandler' + '?username=' + username + ); + expect(req.request.method).toEqual('GET'); + req.flush(result, { + status: 200, + statusText: 'Success.', + }); + flushMicrotasks(); + + expect(successHandler).toHaveBeenCalledWith(result); + expect(failHandler).not.toHaveBeenCalled(); + }) + ); + + it( + 'should fail to get translation contribution stats for invalid username' + + ' when calling viewTranslationContributionStatsAsync', + fakeAsync(() => { + const username = 'InvalidUsername'; + cdabas + .viewTranslationContributionStatsAsync(username) + .then(successHandler, failHandler); + + const req = httpTestingController.expectOne( + '/translationcontributionstatshandler' + '?username=' + username + ); + expect(req.request.method).toEqual('GET'); + const errorMessage = 'Invalid username: ' + username; + req.flush( + { + error: errorMessage, + }, + { + status: 500, + statusText: 'Internal Server Error', + } + ); + flushMicrotasks(); - expect(successHandler).not.toHaveBeenCalled(); - expect(failHandler).toHaveBeenCalledWith(errorMessage); - } - )); + expect(successHandler).not.toHaveBeenCalled(); + expect(failHandler).toHaveBeenCalledWith(errorMessage); + }) + ); }); diff --git a/core/templates/pages/contributor-dashboard-admin-page/services/contributor-dashboard-admin-backend-api.service.ts b/core/templates/pages/contributor-dashboard-admin-page/services/contributor-dashboard-admin-backend-api.service.ts index 15cdb38df4a6..453ee11c1876 100644 --- a/core/templates/pages/contributor-dashboard-admin-page/services/contributor-dashboard-admin-backend-api.service.ts +++ b/core/templates/pages/contributor-dashboard-admin-page/services/contributor-dashboard-admin-backend-api.service.ts @@ -17,150 +17,197 @@ * calls. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { ContributorDashboardAdminPageConstants as PageConstants } from '../contributor-dashboard-admin-page.constants'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {ContributorDashboardAdminPageConstants as PageConstants} from '../contributor-dashboard-admin-page.constants'; export interface ViewContributionBackendResponse { usernames: string[]; } export interface ContributionRightsBackendResponse { - 'can_review_questions': boolean; - 'can_review_translation_for_language_codes': string[]; - 'can_review_voiceover_for_language_codes': string[]; - 'can_submit_questions': boolean; + can_review_questions: boolean; + can_review_translation_for_language_codes: string[]; + can_review_voiceover_for_language_codes: string[]; + can_submit_questions: boolean; } export interface TranslationContributionStatsBackendResponse { - 'translation_contribution_stats': TranslationContributionStats[]; + translation_contribution_stats: TranslationContributionStats[]; } interface TranslationContributionStats { - 'language': string; - 'topic_name': string; - 'submitted_translations_count': number; - 'submitted_translation_word_count': number; - 'accepted_translations_count': number; - 'accepted_translations_without_reviewer_edits_count': number; - 'accepted_translation_word_count': number; - 'rejected_translations_count': number; - 'rejected_translation_word_count': number; - 'contribution_months': string[]; + language: string; + topic_name: string; + submitted_translations_count: number; + submitted_translation_word_count: number; + accepted_translations_count: number; + accepted_translations_without_reviewer_edits_count: number; + accepted_translation_word_count: number; + rejected_translations_count: number; + rejected_translation_word_count: number; + contribution_months: string[]; } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ContributorDashboardAdminBackendApiService { constructor( private http: HttpClient, - private urlInterpolationService: UrlInterpolationService) {} + private urlInterpolationService: UrlInterpolationService + ) {} async addContributionReviewerAsync( - category: string, username: string, languageCode: string | null + category: string, + username: string, + languageCode: string | null ): Promise { return new Promise((resolve, reject) => { - this.http.post( - this.urlInterpolationService.interpolateUrl( - PageConstants.CONTRIBUTION_RIGHTS_HANDLER_URL, { category }), { - username: username, - language_code: languageCode - } - ).toPromise().then(response => { - resolve(response); - }, errorResponse => { - reject(errorResponse.error.error); - }); + this.http + .post( + this.urlInterpolationService.interpolateUrl( + PageConstants.CONTRIBUTION_RIGHTS_HANDLER_URL, + {category} + ), + { + username: username, + language_code: languageCode, + } + ) + .toPromise() + .then( + response => { + resolve(response); + }, + errorResponse => { + reject(errorResponse.error.error); + } + ); }); } async viewContributionReviewersAsync( - category: string, languageCode: string | null + category: string, + languageCode: string | null ): Promise { let params = {}; if (languageCode !== null) { params = { - language_code: languageCode + language_code: languageCode, }; } var url = this.urlInterpolationService.interpolateUrl( - PageConstants.GET_CONTRIBUTOR_USERS_HANDLER_URL, { category }); + PageConstants.GET_CONTRIBUTOR_USERS_HANDLER_URL, + {category} + ); return new Promise((resolve, reject) => { - this.http.get(url, { - params - } - ).toPromise().then(response => { - resolve(response); - }, errorResponse => { - reject(errorResponse.error.error); - }); + this.http + .get(url, { + params, + }) + .toPromise() + .then( + response => { + resolve(response); + }, + errorResponse => { + reject(errorResponse.error.error); + } + ); }); } async contributionReviewerRightsAsync( - username: string): Promise { + username: string + ): Promise { return new Promise((resolve, reject) => { - this.http.get( - PageConstants.CONTRIBUTION_RIGHTS_DATA_HANDLER_URL, { - params: { - username: username + this.http + .get( + PageConstants.CONTRIBUTION_RIGHTS_DATA_HANDLER_URL, + { + params: { + username: username, + }, } - } - ).toPromise().then(response => { - resolve(response); - }, errorResponse => { - reject(errorResponse.error.error); - }); + ) + .toPromise() + .then( + response => { + resolve(response); + }, + errorResponse => { + reject(errorResponse.error.error); + } + ); }); } async removeContributionReviewerAsync( - username: string, category: string, languageCode: string | null + username: string, + category: string, + languageCode: string | null ): Promise { const url = this.urlInterpolationService.interpolateUrl( - PageConstants.CONTRIBUTION_RIGHTS_HANDLER_URL, { category }); + PageConstants.CONTRIBUTION_RIGHTS_HANDLER_URL, + {category} + ); const params: { username: string; language_code?: string; } = { - username: username + username: username, }; if (languageCode !== null) { params.language_code = languageCode; } return new Promise((resolve, reject) => { - this.http.delete( - url, { params } as Object - ).toPromise().then(response => { - resolve(response); - }, errorResponse => { - reject(errorResponse.error.error); - }); + this.http + .delete(url, {params} as Object) + .toPromise() + .then( + response => { + resolve(response); + }, + errorResponse => { + reject(errorResponse.error.error); + } + ); }); } async viewTranslationContributionStatsAsync( - username: string): Promise { + username: string + ): Promise { return new Promise((resolve, reject) => { - this.http.get( - PageConstants.TRANSLATION_CONTRIBUTION_STATS_HANDLER_URL, { - params: { - username: username + this.http + .get( + PageConstants.TRANSLATION_CONTRIBUTION_STATS_HANDLER_URL, + { + params: { + username: username, + }, + } + ) + .toPromise() + .then( + response => { + resolve(response); + }, + errorResponse => { + reject(errorResponse.error.error); } - } - ).toPromise().then(response => { - resolve(response); - }, errorResponse => { - reject(errorResponse.error.error); - }); + ); }); } } -angular.module('oppia').factory( - 'ContributorDashboardAdminBackendApiService', - downgradeInjectable(ContributorDashboardAdminBackendApiService)); +angular + .module('oppia') + .factory( + 'ContributorDashboardAdminBackendApiService', + downgradeInjectable(ContributorDashboardAdminBackendApiService) + ); diff --git a/core/templates/pages/contributor-dashboard-admin-page/services/contributor-dashboard-admin-stats-backend-api.service.spec.ts b/core/templates/pages/contributor-dashboard-admin-page/services/contributor-dashboard-admin-stats-backend-api.service.spec.ts index bc2ee4c75d15..aedd84975b1d 100644 --- a/core/templates/pages/contributor-dashboard-admin-page/services/contributor-dashboard-admin-stats-backend-api.service.spec.ts +++ b/core/templates/pages/contributor-dashboard-admin-page/services/contributor-dashboard-admin-stats-backend-api.service.spec.ts @@ -16,16 +16,18 @@ * @fileoverview Unit tests for contributor admin dashboard backend service */ -import { fakeAsync, flushMicrotasks, TestBed } from '@angular/core/testing'; -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { ContributorDashboardAdminStatsBackendApiService } from './contributor-dashboard-admin-stats-backend-api.service'; -import { ContributorAdminDashboardFilter } from '../contributor-admin-dashboard-filter.model'; -import { AppConstants } from 'app.constants'; -import { CsrfTokenService } from 'services/csrf-token.service'; -import { ClassroomBackendApiService } from 'domain/classroom/classroom-backend-api.service'; -import { ClassroomData } from 'domain/classroom/classroom-data.model'; -import { CreatorTopicSummaryBackendDict } from 'domain/topic/creator-topic-summary.model'; - +import {fakeAsync, flushMicrotasks, TestBed} from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {ContributorDashboardAdminStatsBackendApiService} from './contributor-dashboard-admin-stats-backend-api.service'; +import {ContributorAdminDashboardFilter} from '../contributor-admin-dashboard-filter.model'; +import {AppConstants} from 'app.constants'; +import {CsrfTokenService} from 'services/csrf-token.service'; +import {ClassroomBackendApiService} from 'domain/classroom/classroom-backend-api.service'; +import {ClassroomData} from 'domain/classroom/classroom-data.model'; +import {CreatorTopicSummaryBackendDict} from 'domain/topic/creator-topic-summary.model'; describe('Contribution Admin dashboard stats service', () => { let cdasbas: ContributorDashboardAdminStatsBackendApiService; @@ -57,7 +59,7 @@ describe('Contribution Admin dashboard stats service', () => { total_upcoming_chapters_count: 1, total_overdue_chapters_count: 1, total_chapter_counts_for_each_story: [5, 4], - published_chapter_counts_for_each_story: [3, 4] + published_chapter_counts_for_each_story: [3, 4], }; let secondTopicSummaryDict: CreatorTopicSummaryBackendDict = { id: 'topic2', @@ -81,14 +83,14 @@ describe('Contribution Admin dashboard stats service', () => { total_upcoming_chapters_count: 1, total_overdue_chapters_count: 1, total_chapter_counts_for_each_story: [5, 4], - published_chapter_counts_for_each_story: [3, 4] + published_chapter_counts_for_each_story: [3, 4], }; let responseDictionaries = { name: 'Math', topic_summary_dicts: [firstTopicSummaryDict, secondTopicSummaryDict], course_details: 'Course Details', - topic_list_intro: 'Topics Covered' + topic_list_intro: 'Topics Covered', }; let sampleClassroomDataObject: ClassroomData; @@ -107,7 +109,7 @@ describe('Contribution Admin dashboard stats service', () => { rejected_translations_count: 2, rejected_translation_word_count: 2, first_contribution_date: 'firstcontributiondate', - last_contributed_in_days: 2 + last_contributed_in_days: 2, }; const translationReviewerStat = { language_code: 'en', @@ -119,7 +121,7 @@ describe('Contribution Admin dashboard stats service', () => { accepted_translation_word_count: 2, rejected_translations_count: 2, first_contribution_date: 'firstcontributiondate', - last_contributed_in_days: 2 + last_contributed_in_days: 2, }; const questionSubmitterStat = { contributor_name: 'user1', @@ -131,7 +133,7 @@ describe('Contribution Admin dashboard stats service', () => { accepted_questions_without_reviewer_edits_count: 2, rejected_questions_count: 2, first_contribution_date: 'firstcontributiondate', - last_contributed_in_days: 2 + last_contributed_in_days: 2, }; const questionReviewerStat = { contributor_name: 'user1', @@ -141,37 +143,37 @@ describe('Contribution Admin dashboard stats service', () => { accepted_questions_with_reviewer_edits_count: 2, rejected_questions_count: 2, first_contribution_date: 'firstcontributiondate', - last_contributed_in_days: 2 + last_contributed_in_days: 2, }; const fetchTranslationSubmitterStatResponse = { stats: [translationSubmitterStat], nextOffset: 1, - more: false + more: false, }; const fetchTranslationReviewerStatResponse = { stats: [translationReviewerStat], nextOffset: 1, - more: false + more: false, }; const fetchQuestionSubmitterStatResponse = { stats: [questionSubmitterStat], nextOffset: 1, - more: false + more: false, }; const fetchQuestionReviewerStatResponse = { stats: [questionReviewerStat], nextOffset: 1, - more: false + more: false, }; const fetchCommunityStatsResponse = { translation_reviewers_count: 1, - question_reviewers_count: 1 + question_reviewers_count: 1, }; beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] + imports: [HttpClientTestingModule], }); cdasbas = TestBed.inject(ContributorDashboardAdminStatsBackendApiService); http = TestBed.inject(HttpTestingController); @@ -181,14 +183,14 @@ describe('Contribution Admin dashboard stats service', () => { failHandler = jasmine.createSpy('fail'); // Sample topic object returnable from the backend. - sampleClassroomDataObject = ( - ClassroomData.createFromBackendData( - responseDictionaries.name, - responseDictionaries.topic_summary_dicts, - responseDictionaries.course_details, - responseDictionaries.topic_list_intro)); - - spyOn(csrfService, 'getTokenAsync').and.callFake(async() => { + sampleClassroomDataObject = ClassroomData.createFromBackendData( + responseDictionaries.name, + responseDictionaries.topic_summary_dicts, + responseDictionaries.course_details, + responseDictionaries.topic_list_intro + ); + + spyOn(csrfService, 'getTokenAsync').and.callFake(async () => { return Promise.resolve('sample-csrf-token'); }); }); @@ -197,352 +199,367 @@ describe('Contribution Admin dashboard stats service', () => { http.verify(); }); - it('should return available translation submitter stats', fakeAsync( - () => { - spyOn(cdasbas, 'fetchContributorAdminStats').and.callThrough(); - const url = ( - '/contributor-dashboard-admin-stats/translation/submission' + - '?page_size=20&offset=0&language_code=en'); + it('should return available translation submitter stats', fakeAsync(() => { + spyOn(cdasbas, 'fetchContributorAdminStats').and.callThrough(); + const url = + '/contributor-dashboard-admin-stats/translation/submission' + + '?page_size=20&offset=0&language_code=en'; - cdasbas.fetchContributorAdminStats( + cdasbas + .fetchContributorAdminStats( ContributorAdminDashboardFilter.createDefault(), 20, 0, AppConstants.CONTRIBUTION_STATS_TYPE_TRANSLATION, AppConstants.CONTRIBUTION_STATS_SUBTYPE_SUBMISSION - ).then(successHandler, failHandler); - let req = http.expectOne(url); - expect(req.request.method).toEqual('GET'); - req.flush( - fetchTranslationSubmitterStatResponse, - { status: 200, statusText: 'Success.'}); - flushMicrotasks(); + ) + .then(successHandler, failHandler); + let req = http.expectOne(url); + expect(req.request.method).toEqual('GET'); + req.flush(fetchTranslationSubmitterStatResponse, { + status: 200, + statusText: 'Success.', + }); + flushMicrotasks(); + + expect(cdasbas.fetchContributorAdminStats).toHaveBeenCalledWith( + ContributorAdminDashboardFilter.createDefault(), + 20, + 0, + AppConstants.CONTRIBUTION_STATS_TYPE_TRANSLATION, + AppConstants.CONTRIBUTION_STATS_SUBTYPE_SUBMISSION + ); + expect(successHandler).toHaveBeenCalled(); + expect(failHandler).not.toHaveBeenCalled(); + })); + + it( + 'should not return available translation submitter stats when' + + 'language code is invalid', + fakeAsync(() => { + spyOn(cdasbas, 'fetchContributorAdminStats').and.callThrough(); + const url = + '/contributor-dashboard-admin-stats/translation/' + + 'submission?page_size=20&offset=0&language_code=invalid'; - expect(cdasbas.fetchContributorAdminStats) - .toHaveBeenCalledWith( - ContributorAdminDashboardFilter.createDefault(), + cdasbas + .fetchContributorAdminStats( + new ContributorAdminDashboardFilter([], 'invalid'), 20, 0, AppConstants.CONTRIBUTION_STATS_TYPE_TRANSLATION, - AppConstants.CONTRIBUTION_STATS_SUBTYPE_SUBMISSION); - expect(successHandler).toHaveBeenCalled(); - expect(failHandler).not.toHaveBeenCalled(); - })); - - it('should not return available translation submitter stats when' + - 'language code is invalid', fakeAsync( - () => { - spyOn(cdasbas, 'fetchContributorAdminStats').and.callThrough(); - const url = ( - '/contributor-dashboard-admin-stats/translation/' + - 'submission?page_size=20&offset=0&language_code=invalid'); - - cdasbas.fetchContributorAdminStats( - new ContributorAdminDashboardFilter( - [], 'invalid'), - 20, - 0, - AppConstants.CONTRIBUTION_STATS_TYPE_TRANSLATION, - AppConstants.CONTRIBUTION_STATS_SUBTYPE_SUBMISSION - ).then(successHandler, failHandler); + AppConstants.CONTRIBUTION_STATS_SUBTYPE_SUBMISSION + ) + .then(successHandler, failHandler); let req = http.expectOne(url); expect(req.request.method).toEqual('GET'); req.flush( - { error: 'invalid'}, - { status: 500, statusText: 'Internal Server Error'}); + {error: 'invalid'}, + {status: 500, statusText: 'Internal Server Error'} + ); flushMicrotasks(); expect(successHandler).not.toHaveBeenCalled(); expect(failHandler).toHaveBeenCalled(); - })); + }) + ); - it('should return available translation reviewer stats', fakeAsync( - () => { - spyOn(cdasbas, 'fetchContributorAdminStats').and.callThrough(); - const url = ( - '/contributor-dashboard-admin-stats/translation/review' + - '?page_size=20&offset=0&language_code=en'); + it('should return available translation reviewer stats', fakeAsync(() => { + spyOn(cdasbas, 'fetchContributorAdminStats').and.callThrough(); + const url = + '/contributor-dashboard-admin-stats/translation/review' + + '?page_size=20&offset=0&language_code=en'; - cdasbas.fetchContributorAdminStats( + cdasbas + .fetchContributorAdminStats( ContributorAdminDashboardFilter.createDefault(), 20, 0, AppConstants.CONTRIBUTION_STATS_TYPE_TRANSLATION, AppConstants.CONTRIBUTION_STATS_SUBTYPE_REVIEW - ).then(successHandler, failHandler); - let req = http.expectOne(url); - expect(req.request.method).toEqual('GET'); - req.flush( - fetchTranslationReviewerStatResponse, - { status: 200, statusText: 'Success.'}); - flushMicrotasks(); + ) + .then(successHandler, failHandler); + let req = http.expectOne(url); + expect(req.request.method).toEqual('GET'); + req.flush(fetchTranslationReviewerStatResponse, { + status: 200, + statusText: 'Success.', + }); + flushMicrotasks(); + + expect(cdasbas.fetchContributorAdminStats).toHaveBeenCalledWith( + ContributorAdminDashboardFilter.createDefault(), + 20, + 0, + AppConstants.CONTRIBUTION_STATS_TYPE_TRANSLATION, + AppConstants.CONTRIBUTION_STATS_SUBTYPE_REVIEW + ); + expect(successHandler).toHaveBeenCalled(); + expect(failHandler).not.toHaveBeenCalled(); + })); + + it( + 'should not return available translation reviewer stats when' + + 'language code is invalid', + fakeAsync(() => { + spyOn(cdasbas, 'fetchContributorAdminStats').and.callThrough(); + const url = + '/contributor-dashboard-admin-stats/translation/' + + 'review?page_size=20&offset=0&language_code=invalid'; - expect(cdasbas.fetchContributorAdminStats) - .toHaveBeenCalledWith( - ContributorAdminDashboardFilter.createDefault(), + cdasbas + .fetchContributorAdminStats( + new ContributorAdminDashboardFilter([], 'invalid'), 20, 0, AppConstants.CONTRIBUTION_STATS_TYPE_TRANSLATION, - AppConstants.CONTRIBUTION_STATS_SUBTYPE_REVIEW); - expect(successHandler).toHaveBeenCalled(); - expect(failHandler).not.toHaveBeenCalled(); - })); - - it('should not return available translation reviewer stats when' + - 'language code is invalid', fakeAsync( - () => { - spyOn(cdasbas, 'fetchContributorAdminStats').and.callThrough(); - const url = ( - '/contributor-dashboard-admin-stats/translation/' + - 'review?page_size=20&offset=0&language_code=invalid'); - - cdasbas.fetchContributorAdminStats( - new ContributorAdminDashboardFilter( - [], 'invalid'), - 20, - 0, - AppConstants.CONTRIBUTION_STATS_TYPE_TRANSLATION, - AppConstants.CONTRIBUTION_STATS_SUBTYPE_REVIEW - ).then(successHandler, failHandler); + AppConstants.CONTRIBUTION_STATS_SUBTYPE_REVIEW + ) + .then(successHandler, failHandler); let req = http.expectOne(url); expect(req.request.method).toEqual('GET'); req.flush( - { error: 'invalid'}, - { status: 500, statusText: 'Internal Server Error'}); + {error: 'invalid'}, + {status: 500, statusText: 'Internal Server Error'} + ); flushMicrotasks(); expect(successHandler).not.toHaveBeenCalled(); expect(failHandler).toHaveBeenCalled(); - })); + }) + ); - it('should return available question submitter stats', fakeAsync( - () => { - spyOn(cdasbas, 'fetchContributorAdminStats').and.callThrough(); - const url = ( - '/contributor-dashboard-admin-stats/question/submission' + - '?page_size=20&offset=0&language_code=en'); + it('should return available question submitter stats', fakeAsync(() => { + spyOn(cdasbas, 'fetchContributorAdminStats').and.callThrough(); + const url = + '/contributor-dashboard-admin-stats/question/submission' + + '?page_size=20&offset=0&language_code=en'; - cdasbas.fetchContributorAdminStats( + cdasbas + .fetchContributorAdminStats( ContributorAdminDashboardFilter.createDefault(), 20, 0, AppConstants.CONTRIBUTION_STATS_TYPE_QUESTION, AppConstants.CONTRIBUTION_STATS_SUBTYPE_SUBMISSION - ).then(successHandler, failHandler); - let req = http.expectOne(url); - expect(req.request.method).toEqual('GET'); - req.flush( - fetchQuestionSubmitterStatResponse, - { status: 200, statusText: 'Success.'}); - flushMicrotasks(); + ) + .then(successHandler, failHandler); + let req = http.expectOne(url); + expect(req.request.method).toEqual('GET'); + req.flush(fetchQuestionSubmitterStatResponse, { + status: 200, + statusText: 'Success.', + }); + flushMicrotasks(); + + expect(cdasbas.fetchContributorAdminStats).toHaveBeenCalledWith( + ContributorAdminDashboardFilter.createDefault(), + 20, + 0, + AppConstants.CONTRIBUTION_STATS_TYPE_QUESTION, + AppConstants.CONTRIBUTION_STATS_SUBTYPE_SUBMISSION + ); + expect(successHandler).toHaveBeenCalled(); + expect(failHandler).not.toHaveBeenCalled(); + })); + + it( + 'should not return available question submitter stats when' + + 'language code is invalid', + fakeAsync(() => { + spyOn(cdasbas, 'fetchContributorAdminStats').and.callThrough(); + const url = + '/contributor-dashboard-admin-stats/question/' + + 'submission?page_size=20&offset=0&language_code=invalid'; - expect(cdasbas.fetchContributorAdminStats) - .toHaveBeenCalledWith( - ContributorAdminDashboardFilter.createDefault(), + cdasbas + .fetchContributorAdminStats( + new ContributorAdminDashboardFilter([], 'invalid'), 20, 0, AppConstants.CONTRIBUTION_STATS_TYPE_QUESTION, - AppConstants.CONTRIBUTION_STATS_SUBTYPE_SUBMISSION); - expect(successHandler).toHaveBeenCalled(); - expect(failHandler).not.toHaveBeenCalled(); - })); - - it('should not return available question submitter stats when' + - 'language code is invalid', fakeAsync( - () => { - spyOn(cdasbas, 'fetchContributorAdminStats').and.callThrough(); - const url = ( - '/contributor-dashboard-admin-stats/question/' + - 'submission?page_size=20&offset=0&language_code=invalid'); - - cdasbas.fetchContributorAdminStats( - new ContributorAdminDashboardFilter( - [], 'invalid'), - 20, - 0, - AppConstants.CONTRIBUTION_STATS_TYPE_QUESTION, - AppConstants.CONTRIBUTION_STATS_SUBTYPE_SUBMISSION - ).then(successHandler, failHandler); + AppConstants.CONTRIBUTION_STATS_SUBTYPE_SUBMISSION + ) + .then(successHandler, failHandler); let req = http.expectOne(url); expect(req.request.method).toEqual('GET'); req.flush( - { error: 'invalid'}, - { status: 500, statusText: 'Internal Server Error'}); + {error: 'invalid'}, + {status: 500, statusText: 'Internal Server Error'} + ); flushMicrotasks(); expect(successHandler).not.toHaveBeenCalled(); expect(failHandler).toHaveBeenCalled(); - })); + }) + ); - it('should return available question reviewer stats', fakeAsync( - () => { - spyOn(cdasbas, 'fetchContributorAdminStats').and.callThrough(); - const url = ( - '/contributor-dashboard-admin-stats/question/review' + - '?page_size=20&offset=0&language_code=en'); + it('should return available question reviewer stats', fakeAsync(() => { + spyOn(cdasbas, 'fetchContributorAdminStats').and.callThrough(); + const url = + '/contributor-dashboard-admin-stats/question/review' + + '?page_size=20&offset=0&language_code=en'; - cdasbas.fetchContributorAdminStats( + cdasbas + .fetchContributorAdminStats( ContributorAdminDashboardFilter.createDefault(), 20, 0, AppConstants.CONTRIBUTION_STATS_TYPE_QUESTION, AppConstants.CONTRIBUTION_STATS_SUBTYPE_REVIEW - ).then(successHandler, failHandler); - let req = http.expectOne(url); - expect(req.request.method).toEqual('GET'); - req.flush( - fetchQuestionReviewerStatResponse, - { status: 200, statusText: 'Success.'}); - flushMicrotasks(); + ) + .then(successHandler, failHandler); + let req = http.expectOne(url); + expect(req.request.method).toEqual('GET'); + req.flush(fetchQuestionReviewerStatResponse, { + status: 200, + statusText: 'Success.', + }); + flushMicrotasks(); + + expect(cdasbas.fetchContributorAdminStats).toHaveBeenCalledWith( + ContributorAdminDashboardFilter.createDefault(), + 20, + 0, + AppConstants.CONTRIBUTION_STATS_TYPE_QUESTION, + AppConstants.CONTRIBUTION_STATS_SUBTYPE_REVIEW + ); + expect(successHandler).toHaveBeenCalled(); + expect(failHandler).not.toHaveBeenCalled(); + })); + + it( + 'should not return available question reviewer stats when' + + 'language code is invalid', + fakeAsync(() => { + spyOn(cdasbas, 'fetchContributorAdminStats').and.callThrough(); + const url = + '/contributor-dashboard-admin-stats/question/' + + 'review?page_size=20&offset=0&language_code=invalid'; - expect(cdasbas.fetchContributorAdminStats) - .toHaveBeenCalledWith( - ContributorAdminDashboardFilter.createDefault(), + cdasbas + .fetchContributorAdminStats( + new ContributorAdminDashboardFilter([], 'invalid'), 20, 0, AppConstants.CONTRIBUTION_STATS_TYPE_QUESTION, - AppConstants.CONTRIBUTION_STATS_SUBTYPE_REVIEW); - expect(successHandler).toHaveBeenCalled(); - expect(failHandler).not.toHaveBeenCalled(); - })); - - it('should not return available question reviewer stats when' + - 'language code is invalid', fakeAsync( - () => { - spyOn(cdasbas, 'fetchContributorAdminStats').and.callThrough(); - const url = ( - '/contributor-dashboard-admin-stats/question/' + - 'review?page_size=20&offset=0&language_code=invalid'); - - cdasbas.fetchContributorAdminStats( - new ContributorAdminDashboardFilter( - [], 'invalid'), - 20, - 0, - AppConstants.CONTRIBUTION_STATS_TYPE_QUESTION, - AppConstants.CONTRIBUTION_STATS_SUBTYPE_REVIEW - ).then(successHandler, failHandler); + AppConstants.CONTRIBUTION_STATS_SUBTYPE_REVIEW + ) + .then(successHandler, failHandler); let req = http.expectOne(url); expect(req.request.method).toEqual('GET'); req.flush( - { error: 'invalid'}, - { status: 500, statusText: 'Internal Server Error'}); + {error: 'invalid'}, + {status: 500, statusText: 'Internal Server Error'} + ); flushMicrotasks(); expect(successHandler).not.toHaveBeenCalled(); expect(failHandler).toHaveBeenCalled(); - })); - - it('should return community contribution stats', fakeAsync( - () => { - spyOn(cdasbas, 'fetchCommunityStats').and.callThrough(); - const url = '/community-contribution-stats'; - - cdasbas.fetchCommunityStats().then(successHandler, failHandler); - let req = http.expectOne(url); - expect(req.request.method).toEqual('GET'); - req.flush( - fetchCommunityStatsResponse, - { status: 200, statusText: 'Success.'}); - flushMicrotasks(); - - expect(cdasbas.fetchCommunityStats).toHaveBeenCalled(); - - expect(successHandler).toHaveBeenCalled(); - expect(failHandler).not.toHaveBeenCalled(); - })); - - it('should return assigned languages to the user', fakeAsync( - () => { - spyOn(cdasbas, 'fetchAssignedLanguageIds').and.callThrough(); - const url = '/adminrolehandler?filter_criterion=username&username=user'; - - cdasbas.fetchAssignedLanguageIds('user').then( - successHandler, failHandler); - let req = http.expectOne(url); - expect(req.request.method).toEqual('GET'); - req.flush( - ['en', 'hi'], - { status: 200, statusText: 'Success.'}); - flushMicrotasks(); - - expect(cdasbas.fetchAssignedLanguageIds).toHaveBeenCalled(); - - expect(successHandler).toHaveBeenCalled(); - expect(failHandler).not.toHaveBeenCalled(); - })); - - it('should return classroom data for a classroom id', fakeAsync( - () => { - let response = { - classroomDict: { - classroomId: 'mathClassroomId', - name: 'math', - urlFragment: 'mat', - courseDetails: '', - topicListIntro: '', - topicIdToPrerequisiteTopicIds: {} - } - }; - let classroomId = '0'; - - spyOn(crbas, 'getClassroomDataAsync') - .and.returnValue(Promise.resolve(response)); - - spyOn(crbas, 'fetchClassroomDataAsync') - .and.returnValue(Promise.resolve(sampleClassroomDataObject)); - - cdasbas.fetchTopics(classroomId).then( - successHandler, failHandler - ); - flushMicrotasks(); - - expect(crbas.getClassroomDataAsync). - toHaveBeenCalledWith('0'); - expect(crbas.fetchClassroomDataAsync). - toHaveBeenCalledWith('mat'); - expect(successHandler).toHaveBeenCalled(); - expect(failHandler).not.toHaveBeenCalled(); - })); - - it('should return data for all classrooms', fakeAsync( - () => { - spyOn(crbas, 'getAllClassroomIdToClassroomNameDictAsync') - .and.returnValue(Promise.resolve({mathClassroomId: 'math'})); - spyOn(cdasbas, 'fetchTopics') - .and.returnValue(Promise.resolve([ - { id: '1', topic: 'Science' }, - { id: '2', topic: 'Technology' }, - ])); - - cdasbas.fetchTopicChoices().then( - successHandler, failHandler - ); - flushMicrotasks(); - - expect(crbas.getAllClassroomIdToClassroomNameDictAsync). - toHaveBeenCalled(); - expect(cdasbas.fetchTopics). - toHaveBeenCalledWith('mathClassroomId'); - - expect(successHandler).toHaveBeenCalled(); - expect(failHandler).not.toHaveBeenCalled(); - })); - - it('should return empty stats if contribution type is invalid', fakeAsync( - () => { - cdasbas.fetchContributorAdminStats( + }) + ); + + it('should return community contribution stats', fakeAsync(() => { + spyOn(cdasbas, 'fetchCommunityStats').and.callThrough(); + const url = '/community-contribution-stats'; + + cdasbas.fetchCommunityStats().then(successHandler, failHandler); + let req = http.expectOne(url); + expect(req.request.method).toEqual('GET'); + req.flush(fetchCommunityStatsResponse, { + status: 200, + statusText: 'Success.', + }); + flushMicrotasks(); + + expect(cdasbas.fetchCommunityStats).toHaveBeenCalled(); + + expect(successHandler).toHaveBeenCalled(); + expect(failHandler).not.toHaveBeenCalled(); + })); + + it('should return assigned languages to the user', fakeAsync(() => { + spyOn(cdasbas, 'fetchAssignedLanguageIds').and.callThrough(); + const url = '/adminrolehandler?filter_criterion=username&username=user'; + + cdasbas.fetchAssignedLanguageIds('user').then(successHandler, failHandler); + let req = http.expectOne(url); + expect(req.request.method).toEqual('GET'); + req.flush(['en', 'hi'], {status: 200, statusText: 'Success.'}); + flushMicrotasks(); + + expect(cdasbas.fetchAssignedLanguageIds).toHaveBeenCalled(); + + expect(successHandler).toHaveBeenCalled(); + expect(failHandler).not.toHaveBeenCalled(); + })); + + it('should return classroom data for a classroom id', fakeAsync(() => { + let response = { + classroomDict: { + classroomId: 'mathClassroomId', + name: 'math', + urlFragment: 'mat', + courseDetails: '', + topicListIntro: '', + topicIdToPrerequisiteTopicIds: {}, + }, + }; + let classroomId = '0'; + + spyOn(crbas, 'getClassroomDataAsync').and.returnValue( + Promise.resolve(response) + ); + + spyOn(crbas, 'fetchClassroomDataAsync').and.returnValue( + Promise.resolve(sampleClassroomDataObject) + ); + + cdasbas.fetchTopics(classroomId).then(successHandler, failHandler); + flushMicrotasks(); + + expect(crbas.getClassroomDataAsync).toHaveBeenCalledWith('0'); + expect(crbas.fetchClassroomDataAsync).toHaveBeenCalledWith('mat'); + expect(successHandler).toHaveBeenCalled(); + expect(failHandler).not.toHaveBeenCalled(); + })); + + it('should return data for all classrooms', fakeAsync(() => { + spyOn(crbas, 'getAllClassroomIdToClassroomNameDictAsync').and.returnValue( + Promise.resolve({mathClassroomId: 'math'}) + ); + spyOn(cdasbas, 'fetchTopics').and.returnValue( + Promise.resolve([ + {id: '1', topic: 'Science'}, + {id: '2', topic: 'Technology'}, + ]) + ); + + cdasbas.fetchTopicChoices().then(successHandler, failHandler); + flushMicrotasks(); + + expect(crbas.getAllClassroomIdToClassroomNameDictAsync).toHaveBeenCalled(); + expect(cdasbas.fetchTopics).toHaveBeenCalledWith('mathClassroomId'); + + expect(successHandler).toHaveBeenCalled(); + expect(failHandler).not.toHaveBeenCalled(); + })); + + it('should return empty stats if contribution type is invalid', fakeAsync(() => { + cdasbas + .fetchContributorAdminStats( ContributorAdminDashboardFilter.createDefault(), 20, 0, 'invalid', 'invalid_subtype' - ).then(result => { + ) + .then(result => { expect(result).toEqual({ stats: [], nextOffset: 0, - more: false + more: false, }); }); - })); + })); }); diff --git a/core/templates/pages/contributor-dashboard-admin-page/services/contributor-dashboard-admin-stats-backend-api.service.ts b/core/templates/pages/contributor-dashboard-admin-page/services/contributor-dashboard-admin-stats-backend-api.service.ts index 0ac8bb8d7b8e..2f8154b3a4e7 100644 --- a/core/templates/pages/contributor-dashboard-admin-page/services/contributor-dashboard-admin-stats-backend-api.service.ts +++ b/core/templates/pages/contributor-dashboard-admin-page/services/contributor-dashboard-admin-stats-backend-api.service.ts @@ -16,77 +16,80 @@ * @fileoverview Service for fetching contributor admin dashboard stats. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { ContributorAdminDashboardFilter } from '../contributor-admin-dashboard-filter.model'; -import { TranslationSubmitterStats, TranslationReviewerStats, - QuestionSubmitterStats, QuestionReviewerStats +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {ContributorAdminDashboardFilter} from '../contributor-admin-dashboard-filter.model'; +import { + TranslationSubmitterStats, + TranslationReviewerStats, + QuestionSubmitterStats, + QuestionReviewerStats, } from '../contributor-dashboard-admin-summary.model'; -import { AppConstants } from 'app.constants'; -import { ContributorDashboardAdminPageConstants as PageConstants } from '../contributor-dashboard-admin-page.constants'; -import { ClassroomBackendApiService } from 'domain/classroom/classroom-backend-api.service'; -import { UserRolesBackendResponse } from 'domain/admin/admin-backend-api.service'; -import { TopicChoice } from '../contributor-admin-dashboard-page.component'; +import {AppConstants} from 'app.constants'; +import {ContributorDashboardAdminPageConstants as PageConstants} from '../contributor-dashboard-admin-page.constants'; +import {ClassroomBackendApiService} from 'domain/classroom/classroom-backend-api.service'; +import {UserRolesBackendResponse} from 'domain/admin/admin-backend-api.service'; +import {TopicChoice} from '../contributor-admin-dashboard-page.component'; export interface TranslationSubmitterBackendDict { - 'language_code': string; - 'contributor_name': string; - 'topic_names': string[]; - 'recent_performance': number; - 'overall_accuracy': number; - 'submitted_translations_count': number; - 'submitted_translation_word_count': number; - 'accepted_translations_count': number; - 'accepted_translations_without_reviewer_edits_count': number; - 'accepted_translation_word_count': number; - 'rejected_translations_count': number; - 'rejected_translation_word_count': number; - 'first_contribution_date': string; - 'last_contributed_in_days': number; + language_code: string; + contributor_name: string; + topic_names: string[]; + recent_performance: number; + overall_accuracy: number; + submitted_translations_count: number; + submitted_translation_word_count: number; + accepted_translations_count: number; + accepted_translations_without_reviewer_edits_count: number; + accepted_translation_word_count: number; + rejected_translations_count: number; + rejected_translation_word_count: number; + first_contribution_date: string; + last_contributed_in_days: number; } export interface TranslationReviewerBackendDict { - 'language_code': string; - 'contributor_name': string; - 'topic_names': string[]; - 'reviewed_translations_count': number; - 'accepted_translations_count': number; - 'accepted_translations_with_reviewer_edits_count': number; - 'accepted_translation_word_count': number; - 'rejected_translations_count': number; - 'first_contribution_date': string; - 'last_contributed_in_days': number; + language_code: string; + contributor_name: string; + topic_names: string[]; + reviewed_translations_count: number; + accepted_translations_count: number; + accepted_translations_with_reviewer_edits_count: number; + accepted_translation_word_count: number; + rejected_translations_count: number; + first_contribution_date: string; + last_contributed_in_days: number; } export interface QuestionSubmitterBackendDict { - 'contributor_name': string; - 'topic_names': string[]; - 'recent_performance': number; - 'overall_accuracy': number; - 'submitted_questions_count': number; - 'accepted_questions_count': number; - 'accepted_questions_without_reviewer_edits_count': number; - 'rejected_questions_count': number; - 'first_contribution_date': string; - 'last_contributed_in_days': number; + contributor_name: string; + topic_names: string[]; + recent_performance: number; + overall_accuracy: number; + submitted_questions_count: number; + accepted_questions_count: number; + accepted_questions_without_reviewer_edits_count: number; + rejected_questions_count: number; + first_contribution_date: string; + last_contributed_in_days: number; } export interface QuestionReviewerBackendDict { - 'contributor_name': string; - 'topic_names': string[]; - 'reviewed_questions_count': number; - 'accepted_questions_count': number; - 'accepted_questions_with_reviewer_edits_count': number; - 'rejected_questions_count': number; - 'first_contribution_date': string; - 'last_contributed_in_days': number; + contributor_name: string; + topic_names: string[]; + reviewed_questions_count: number; + accepted_questions_count: number; + accepted_questions_with_reviewer_edits_count: number; + rejected_questions_count: number; + first_contribution_date: string; + last_contributed_in_days: number; } export interface CommunityContributionStatsBackendDict { - 'translation_reviewers_count': translationReviewersCount; - 'question_reviewers_count': number; + translation_reviewers_count: translationReviewersCount; + question_reviewers_count: number; } export interface translationReviewersCount { @@ -94,8 +97,8 @@ export interface translationReviewersCount { } export interface CommunityContributionStatsDict { - 'translation_reviewers_count': number; - 'question_reviewers_count': number; + translation_reviewers_count: number; + question_reviewers_count: number; } export interface TranslationSubmitterStatsData { @@ -157,191 +160,211 @@ export class ContributorDashboardAdminStatsBackendApiService { private classroomBackendApiService: ClassroomBackendApiService ) {} - async fetchCommunityStats(): - Promise { + async fetchCommunityStats(): Promise { return new Promise((resolve, reject) => { - this.http.get( - PageConstants.COMMUNITY_CONTRIBUTION_STATS_URL - ).toPromise().then(response => { - resolve({ - translation_reviewers_count: ( - response.translation_reviewers_count), - question_reviewers_count: response.question_reviewers_count + this.http + .get( + PageConstants.COMMUNITY_CONTRIBUTION_STATS_URL + ) + .toPromise() + .then(response => { + resolve({ + translation_reviewers_count: response.translation_reviewers_count, + question_reviewers_count: response.question_reviewers_count, + }); }); - }); }); } - async fetchAssignedLanguageIds(username: string): - Promise { + async fetchAssignedLanguageIds(username: string): Promise { return new Promise((resolve, reject) => { - this.http.get( - PageConstants.ADMIN_ROLE_HANDLER_URL, { + this.http + .get(PageConstants.ADMIN_ROLE_HANDLER_URL, { params: { filter_criterion: 'username', - username: username - } - } - ).toPromise().then( - response => - resolve( - response.coordinated_language_ids - )); + username: username, + }, + }) + .toPromise() + .then(response => resolve(response.coordinated_language_ids)); }); } async fetchContributorAdminStats( - filter: ContributorAdminDashboardFilter, - pageSize: number, - nextOffset: number | null, - contributionType: string, - contributionSubtype: string - ): - Promise { + filter: ContributorAdminDashboardFilter, + pageSize: number, + nextOffset: number | null, + contributionType: string, + contributionSubtype: string + ): Promise< + | TranslationSubmitterStatsData + | TranslationReviewerStatsData + | QuestionSubmitterStatsData + | QuestionReviewerStatsData + > { const url = this.urlInterpolationService.interpolateUrl( - PageConstants.CONTRIBUTOR_ADMIN_STATS_SUMMARIES_URL, { + PageConstants.CONTRIBUTOR_ADMIN_STATS_SUMMARIES_URL, + { contribution_type: contributionType, - contribution_subtype: contributionSubtype + contribution_subtype: contributionSubtype, } ); this.params = { page_size: pageSize, offset: nextOffset, - topic_ids: filter.topicIds.length > 0 ? JSON.stringify( - filter.topicIds - ) : [], - language_code: filter.languageCode ? filter.languageCode : ( - PageConstants.DEFAULT_LANGUAGE_FILTER - ), - ...( - filter.lastActivity ? { - max_days_since_last_activity: filter.lastActivity - } : {}), + topic_ids: + filter.topicIds.length > 0 ? JSON.stringify(filter.topicIds) : [], + language_code: filter.languageCode + ? filter.languageCode + : PageConstants.DEFAULT_LANGUAGE_FILTER, + ...(filter.lastActivity + ? { + max_days_since_last_activity: filter.lastActivity, + } + : {}), }; if (contributionType === AppConstants.CONTRIBUTION_STATS_TYPE_TRANSLATION) { if ( - contributionSubtype === ( - AppConstants.CONTRIBUTION_STATS_SUBTYPE_SUBMISSION)) { + contributionSubtype === + AppConstants.CONTRIBUTION_STATS_SUBTYPE_SUBMISSION + ) { return new Promise((resolve, reject) => { - this.http.get( - url, { - params: this.params - } as Object - ).toPromise().then(response => { - resolve({ - stats: response.stats.map( - backendDict => TranslationSubmitterStats - .createFromBackendDict(backendDict)), - nextOffset: response.next_offset, - more: response.more - }); - }, errorResponse => { - reject(errorResponse.error.error); - }); + this.http + .get(url, { + params: this.params, + } as Object) + .toPromise() + .then( + response => { + resolve({ + stats: response.stats.map(backendDict => + TranslationSubmitterStats.createFromBackendDict(backendDict) + ), + nextOffset: response.next_offset, + more: response.more, + }); + }, + errorResponse => { + reject(errorResponse.error.error); + } + ); }); } else if ( - contributionSubtype === ( - AppConstants.CONTRIBUTION_STATS_SUBTYPE_REVIEW) + contributionSubtype === AppConstants.CONTRIBUTION_STATS_SUBTYPE_REVIEW ) { return new Promise((resolve, reject) => { - this.http.get( - url, { - params: this.params - } as Object - ).toPromise().then(response => { - resolve({ - stats: response.stats.map( - backendDict => TranslationReviewerStats - .createFromBackendDict(backendDict)), - nextOffset: response.next_offset, - more: response.more - }); - }, errorResponse => { - reject(errorResponse.error.error); - }); + this.http + .get(url, { + params: this.params, + } as Object) + .toPromise() + .then( + response => { + resolve({ + stats: response.stats.map(backendDict => + TranslationReviewerStats.createFromBackendDict(backendDict) + ), + nextOffset: response.next_offset, + more: response.more, + }); + }, + errorResponse => { + reject(errorResponse.error.error); + } + ); }); } } else if ( - contributionType === AppConstants.CONTRIBUTION_STATS_TYPE_QUESTION) { + contributionType === AppConstants.CONTRIBUTION_STATS_TYPE_QUESTION + ) { if ( - contributionSubtype === ( - AppConstants.CONTRIBUTION_STATS_SUBTYPE_SUBMISSION)) { + contributionSubtype === + AppConstants.CONTRIBUTION_STATS_SUBTYPE_SUBMISSION + ) { return new Promise((resolve, reject) => { - this.http.get( - url, { - params: this.params - } as Object - ).toPromise().then(response => { - resolve({ - stats: response.stats.map( - backendDict => QuestionSubmitterStats - .createFromBackendDict(backendDict)), - nextOffset: response.next_offset, - more: response.more - }); - }, errorResponse => { - reject(errorResponse.error.error); - }); + this.http + .get(url, { + params: this.params, + } as Object) + .toPromise() + .then( + response => { + resolve({ + stats: response.stats.map(backendDict => + QuestionSubmitterStats.createFromBackendDict(backendDict) + ), + nextOffset: response.next_offset, + more: response.more, + }); + }, + errorResponse => { + reject(errorResponse.error.error); + } + ); }); } else if ( - contributionSubtype === ( - AppConstants.CONTRIBUTION_STATS_SUBTYPE_REVIEW) + contributionSubtype === AppConstants.CONTRIBUTION_STATS_SUBTYPE_REVIEW ) { return new Promise((resolve, reject) => { - this.http.get( - url, { - params: this.params - } as Object - ).toPromise().then(response => { - resolve({ - stats: response.stats.map( - backendDict => QuestionReviewerStats - .createFromBackendDict(backendDict)), - nextOffset: response.next_offset, - more: response.more - }); - }, errorResponse => { - reject(errorResponse.error.error); - }); + this.http + .get(url, { + params: this.params, + } as Object) + .toPromise() + .then( + response => { + resolve({ + stats: response.stats.map(backendDict => + QuestionReviewerStats.createFromBackendDict(backendDict) + ), + nextOffset: response.next_offset, + more: response.more, + }); + }, + errorResponse => { + reject(errorResponse.error.error); + } + ); }); } } return Promise.resolve({ stats: [], nextOffset: 0, - more: false + more: false, }); } async fetchTopics(classroomId: string): Promise { - const classroomPromise = this.classroomBackendApiService. - getClassroomDataAsync(classroomId).then( - classResponse => { - return this.classroomBackendApiService. - fetchClassroomDataAsync(classResponse.classroomDict.urlFragment); - }); - return (await classroomPromise)._topicSummaries.map( - (obj) => ({ - id: obj.id, - topic: obj.name - })); + const classroomPromise = this.classroomBackendApiService + .getClassroomDataAsync(classroomId) + .then(classResponse => { + return this.classroomBackendApiService.fetchClassroomDataAsync( + classResponse.classroomDict.urlFragment + ); + }); + return (await classroomPromise)._topicSummaries.map(obj => ({ + id: obj.id, + topic: obj.name, + })); } async fetchTopicChoices(): Promise { let topicPromises: Promise[] = []; return this.classroomBackendApiService - .getAllClassroomIdToClassroomNameDictAsync().then(classResponse => { - Object.keys(classResponse).forEach( - classroomId => - topicPromises.push(this.fetchTopics(classroomId))); + .getAllClassroomIdToClassroomNameDictAsync() + .then(classResponse => { + Object.keys(classResponse).forEach(classroomId => + topicPromises.push(this.fetchTopics(classroomId)) + ); return Promise.all(topicPromises); }); } } -angular.module('oppia').factory( - 'ContributorDashboardAdminStatsBackendApiService', - downgradeInjectable(ContributorDashboardAdminStatsBackendApiService)); +angular + .module('oppia') + .factory( + 'ContributorDashboardAdminStatsBackendApiService', + downgradeInjectable(ContributorDashboardAdminStatsBackendApiService) + ); diff --git a/core/templates/pages/contributor-dashboard-admin-page/topic-filter/topic-filter.component.spec.ts b/core/templates/pages/contributor-dashboard-admin-page/topic-filter/topic-filter.component.spec.ts index 4698a9e1114c..a8ca74290d13 100644 --- a/core/templates/pages/contributor-dashboard-admin-page/topic-filter/topic-filter.component.spec.ts +++ b/core/templates/pages/contributor-dashboard-admin-page/topic-filter/topic-filter.component.spec.ts @@ -11,15 +11,21 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -import { ElementRef } from '@angular/core'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync} from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { MaterialModule } from 'modules/material.module'; -import { TopicFilterComponent } from './topic-filter.component'; -import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; +import {ElementRef} from '@angular/core'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {MaterialModule} from 'modules/material.module'; +import {TopicFilterComponent} from './topic-filter.component'; +import {MatAutocompleteTrigger} from '@angular/material/autocomplete'; /** * @fileoverview Unit tests for Topic Filter Component. */ @@ -35,13 +41,10 @@ describe('Topic Filter component', () => { FormsModule, ReactiveFormsModule, MaterialModule, - BrowserAnimationsModule + BrowserAnimationsModule, ], - declarations: [ - TopicFilterComponent, - MockTranslatePipe, - ], - providers: [] + declarations: [TopicFilterComponent, MockTranslatePipe], + providers: [], }).compileComponents(); })); @@ -51,7 +54,7 @@ describe('Topic Filter component', () => { component.autoTrigger = { closePanel() { return; - } + }, } as MatAutocompleteTrigger; }); @@ -59,42 +62,41 @@ describe('Topic Filter component', () => { expect(component).toBeDefined(); }); - it('should initialize topic filter and select a topic and exec search', - fakeAsync( - () => { - spyOn(component.selectionsChange, 'emit'); - component.selectedTopicNames = ['topic1', 'topic2']; - component.defaultTopicNames = [ - 'topic1', 'topic2', 'topic3', 'topic4']; + it('should initialize topic filter and select a topic and exec search', fakeAsync(() => { + spyOn(component.selectionsChange, 'emit'); + component.selectedTopicNames = ['topic1', 'topic2']; + component.defaultTopicNames = ['topic1', 'topic2', 'topic3', 'topic4']; - fixture.detectChanges(); - component.ngOnInit(); + fixture.detectChanges(); + component.ngOnInit(); - expect(component.filteredTopics).toBeDefined(); - expect(component.topicFilter).toBeDefined(); - expect(component.searchDropDownTopics).toEqual(['topic3', 'topic4']); + expect(component.filteredTopics).toBeDefined(); + expect(component.topicFilter).toBeDefined(); + expect(component.searchDropDownTopics).toEqual(['topic3', 'topic4']); - component.topicFilterInput = { - nativeElement: { - value: '' - } - } as ElementRef; - component.selectTopic(({ option: { viewValue: 'topic3'}})); - // Search with applied topics will be executed only when no change in - // topic filter is done for 1500ms. We add 1ms extra to avoid flaking - // of test. - tick(1500 + 1); + component.topicFilterInput = { + nativeElement: { + value: '', + }, + } as ElementRef; + component.selectTopic({option: {viewValue: 'topic3'}}); + // Search with applied topics will be executed only when no change in + // topic filter is done for 1500ms. We add 1ms extra to avoid flaking + // of test. + tick(1500 + 1); - expect(component.selectedTopicNames).toEqual( - ['topic1', 'topic2', 'topic3']); - expect(component.searchDropDownTopics).toEqual(['topic4']); + expect(component.selectedTopicNames).toEqual([ + 'topic1', + 'topic2', + 'topic3', + ]); + expect(component.searchDropDownTopics).toEqual(['topic4']); - component.selectTopic(({ option: { viewValue: 'noTopic'}})); - tick(1600); + component.selectTopic({option: {viewValue: 'noTopic'}}); + tick(1600); - expect(component.selectionsChange.emit).toHaveBeenCalled(); - }) - ); + expect(component.selectionsChange.emit).toHaveBeenCalled(); + })); it('should filter topics', () => { component.searchDropDownTopics = ['math']; diff --git a/core/templates/pages/contributor-dashboard-admin-page/topic-filter/topic-filter.component.ts b/core/templates/pages/contributor-dashboard-admin-page/topic-filter/topic-filter.component.ts index bb50fc0d6f64..b6682c5bcb6a 100644 --- a/core/templates/pages/contributor-dashboard-admin-page/topic-filter/topic-filter.component.ts +++ b/core/templates/pages/contributor-dashboard-admin-page/topic-filter/topic-filter.component.ts @@ -16,49 +16,62 @@ * @fileoverview Topic filter component for the blog home page. */ -import { Component, OnInit, ViewChild, ElementRef, Input, Output, EventEmitter} from '@angular/core'; -import { COMMA, ENTER } from '@angular/cdk/keycodes'; -import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; -import { FormControl } from '@angular/forms'; -import { debounceTime, distinctUntilChanged, map, startWith } from 'rxjs/operators'; -import { Observable } from 'rxjs'; +import { + Component, + OnInit, + ViewChild, + ElementRef, + Input, + Output, + EventEmitter, +} from '@angular/core'; +import {COMMA, ENTER} from '@angular/cdk/keycodes'; +import {MatAutocompleteTrigger} from '@angular/material/autocomplete'; +import {FormControl} from '@angular/forms'; +import { + debounceTime, + distinctUntilChanged, + map, + startWith, +} from 'rxjs/operators'; +import {Observable} from 'rxjs'; @Component({ selector: 'oppia-topic-filter', - templateUrl: './topic-filter.component.html' + templateUrl: './topic-filter.component.html', }) - export class TopicFilterComponent implements OnInit { // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @Input() defaultTopicNames!: string[]; @Input() selectedTopicNames: string[] = []; - @Output() selectionsChange: EventEmitter = ( - new EventEmitter()); + @Output() selectionsChange: EventEmitter = new EventEmitter(); separatorKeysCodes: number[] = [ENTER, COMMA]; topicFilter = new FormControl(''); searchDropDownTopics: string[] = []; filteredTopics!: Observable; - @ViewChild('topicFilterInput') topicFilterInput!: ( - ElementRef); + @ViewChild('topicFilterInput') + topicFilterInput!: ElementRef; @ViewChild('trigger') autoTrigger!: MatAutocompleteTrigger; constructor() { this.filteredTopics = this.topicFilter.valueChanges.pipe( startWith(null), - map((topic: string | null) => ( - topic ? this.filter(topic) : this.searchDropDownTopics.slice())), + map((topic: string | null) => + topic ? this.filter(topic) : this.searchDropDownTopics.slice() + ) ); } filter(value: string): string[] { const filterValue = value.toLowerCase(); - return this.searchDropDownTopics.filter( - topic => topic.toLowerCase().includes(filterValue)); + return this.searchDropDownTopics.filter(topic => + topic.toLowerCase().includes(filterValue) + ); } removeTopic(topic: string, topicsList: string[]): void { @@ -75,7 +88,7 @@ export class TopicFilterComponent implements OnInit { this.topicFilter.setValue(null); } - selectTopic(event: { option: { viewValue: string}}): void { + selectTopic(event: {option: {viewValue: string}}): void { this.selectedTopicNames.push(event.option.viewValue); this.refreshSearchDropDownTopics(); this.topicFilterInput.nativeElement.value = ''; @@ -93,10 +106,10 @@ export class TopicFilterComponent implements OnInit { ngOnInit(): void { this.refreshSearchDropDownTopics(); - this.filteredTopics.pipe( - debounceTime(1500), distinctUntilChanged() - ).subscribe(() => { - this.selectionsChange.emit(this.selectedTopicNames); - }); + this.filteredTopics + .pipe(debounceTime(1500), distinctUntilChanged()) + .subscribe(() => { + this.selectionsChange.emit(this.selectedTopicNames); + }); } } diff --git a/core/templates/pages/contributor-dashboard-admin-page/translation-role-editor-modal/cd-admin-translation-role-editor-modal.component.spec.ts b/core/templates/pages/contributor-dashboard-admin-page/translation-role-editor-modal/cd-admin-translation-role-editor-modal.component.spec.ts index f6ac89679c93..3e017b598cf7 100644 --- a/core/templates/pages/contributor-dashboard-admin-page/translation-role-editor-modal/cd-admin-translation-role-editor-modal.component.spec.ts +++ b/core/templates/pages/contributor-dashboard-admin-page/translation-role-editor-modal/cd-admin-translation-role-editor-modal.component.spec.ts @@ -16,55 +16,56 @@ * @fileoverview Unit tests for CdAdminTranslationRoleEditorModal. */ -import { ComponentFixture, fakeAsync, TestBed, async, tick } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { FormsModule } from '@angular/forms'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { MaterialModule } from 'modules/material.module'; - -import { ContributorDashboardAdminBackendApiService } from '../services/contributor-dashboard-admin-backend-api.service'; -import { AlertsService } from 'services/alerts.service'; - -import { CdAdminTranslationRoleEditorModal } from './cd-admin-translation-role-editor-modal.component'; +import { + ComponentFixture, + fakeAsync, + TestBed, + async, + tick, +} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {FormsModule} from '@angular/forms'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {MaterialModule} from 'modules/material.module'; + +import {ContributorDashboardAdminBackendApiService} from '../services/contributor-dashboard-admin-backend-api.service'; +import {AlertsService} from 'services/alerts.service'; + +import {CdAdminTranslationRoleEditorModal} from './cd-admin-translation-role-editor-modal.component'; describe('CdAdminTranslationRoleEditorModal', () => { let component: CdAdminTranslationRoleEditorModal; let fixture: ComponentFixture; let ngbActiveModal: NgbActiveModal; - let contributorDashboardAdminBackendApiService: - ContributorDashboardAdminBackendApiService; + let contributorDashboardAdminBackendApiService: ContributorDashboardAdminBackendApiService; let alertsService: AlertsService; beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [ - FormsModule, - MaterialModule, - HttpClientTestingModule - ], + imports: [FormsModule, MaterialModule, HttpClientTestingModule], declarations: [CdAdminTranslationRoleEditorModal], providers: [ NgbActiveModal, ContributorDashboardAdminBackendApiService, - AlertsService - ] + AlertsService, + ], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent( - CdAdminTranslationRoleEditorModal); + fixture = TestBed.createComponent(CdAdminTranslationRoleEditorModal); component = fixture.componentInstance; ngbActiveModal = TestBed.get(NgbActiveModal); contributorDashboardAdminBackendApiService = TestBed.inject( - ContributorDashboardAdminBackendApiService); + ContributorDashboardAdminBackendApiService + ); alertsService = TestBed.inject(AlertsService); component.assignedLanguageIds = ['en', 'hi', 'ak']; component.languageIdToName = { en: 'English', hi: 'Hindi', ak: 'Ákán (Akan)', - sk: 'shqip (Albanian)' + sk: 'shqip (Albanian)', }; fixture.detectChanges(); component.ngOnInit(); @@ -102,8 +103,8 @@ describe('CdAdminTranslationRoleEditorModal', () => { it('should alert warning if request fails', fakeAsync(() => { spyOn( contributorDashboardAdminBackendApiService, - 'addContributionReviewerAsync').and.returnValue( - Promise.reject()); + 'addContributionReviewerAsync' + ).and.returnValue(Promise.reject()); spyOn(alertsService, 'addWarning').and.callThrough(); component.selectedLanguageId = 'en'; @@ -131,21 +132,23 @@ describe('CdAdminTranslationRoleEditorModal', () => { it('should make request to remove language', fakeAsync(() => { spyOn( contributorDashboardAdminBackendApiService, - 'removeContributionReviewerAsync').and.resolveTo(); + 'removeContributionReviewerAsync' + ).and.resolveTo(); component.removeLanguageId('hi'); expect(component.languageIdInUpdate).toEqual('hi'); tick(); expect( - contributorDashboardAdminBackendApiService - .removeContributionReviewerAsync).toHaveBeenCalled(); + contributorDashboardAdminBackendApiService.removeContributionReviewerAsync + ).toHaveBeenCalled(); })); it('should alert warning if request fails', fakeAsync(() => { spyOn( contributorDashboardAdminBackendApiService, - 'removeContributionReviewerAsync').and.returnValue(Promise.reject()); + 'removeContributionReviewerAsync' + ).and.returnValue(Promise.reject()); spyOn(alertsService, 'addWarning').and.callThrough(); component.removeLanguageId('hi'); diff --git a/core/templates/pages/contributor-dashboard-admin-page/translation-role-editor-modal/cd-admin-translation-role-editor-modal.component.ts b/core/templates/pages/contributor-dashboard-admin-page/translation-role-editor-modal/cd-admin-translation-role-editor-modal.component.ts index d96f4b78ec57..fd3899489abb 100644 --- a/core/templates/pages/contributor-dashboard-admin-page/translation-role-editor-modal/cd-admin-translation-role-editor-modal.component.ts +++ b/core/templates/pages/contributor-dashboard-admin-page/translation-role-editor-modal/cd-admin-translation-role-editor-modal.component.ts @@ -16,18 +16,17 @@ * @fileoverview Component for editing a user's translation contribution rights. */ -import { Component, OnInit, Input } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import {Component, OnInit, Input} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; -import { ContributorDashboardAdminBackendApiService } from '../services/contributor-dashboard-admin-backend-api.service'; -import { AlertsService } from 'services/alerts.service'; +import {ContributorDashboardAdminBackendApiService} from '../services/contributor-dashboard-admin-backend-api.service'; +import {AlertsService} from 'services/alerts.service'; import constants from 'assets/constants'; @Component({ selector: 'cd-admin-translation-role-editor-modal', templateUrl: './cd-admin-translation-role-editor-modal.component.html', }) - export class CdAdminTranslationRoleEditorModal implements OnInit { // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see @@ -44,14 +43,14 @@ export class CdAdminTranslationRoleEditorModal implements OnInit { constructor( private activeModal: NgbActiveModal, - private contributorDashboardAdminBackendApiService: - ContributorDashboardAdminBackendApiService, + private contributorDashboardAdminBackendApiService: ContributorDashboardAdminBackendApiService, private alertsService: AlertsService ) {} private updateLanguageIdsForSelection(): void { this.languageIdsForSelection = Object.keys(this.languageIdToName).filter( - languageId => !this.assignedLanguageIds.includes(languageId)); + languageId => !this.assignedLanguageIds.includes(languageId) + ); this.selectedLanguageId = this.languageIdsForSelection[0]; } @@ -65,36 +64,49 @@ export class CdAdminTranslationRoleEditorModal implements OnInit { this.contributorDashboardAdminBackendApiService .addContributionReviewerAsync( constants.CD_USER_RIGHTS_CATEGORY_REVIEW_TRANSLATION, - this.username, this.languageIdInUpdate).then(()=> { - this.languageIdInUpdate = null; - this.updateLanguageIdsForSelection(); - }, errorMessage => { - if (this.languageIdInUpdate !== null) { - let languageIdIndex = this.assignedLanguageIds.indexOf( - this.languageIdInUpdate); - this.assignedLanguageIds.splice(languageIdIndex, 1); + this.username, + this.languageIdInUpdate + ) + .then( + () => { + this.languageIdInUpdate = null; + this.updateLanguageIdsForSelection(); + }, + errorMessage => { + if (this.languageIdInUpdate !== null) { + let languageIdIndex = this.assignedLanguageIds.indexOf( + this.languageIdInUpdate + ); + this.assignedLanguageIds.splice(languageIdIndex, 1); + } + this.alertsService.addWarning( + errorMessage || 'Error communicating with server.' + ); } - this.alertsService.addWarning( - errorMessage || 'Error communicating with server.'); - }); + ); } removeLanguageId(languageIdToRemove: string): void { - let languageIdIndex = this.assignedLanguageIds.indexOf( - languageIdToRemove); + let languageIdIndex = this.assignedLanguageIds.indexOf(languageIdToRemove); this.languageIdInUpdate = languageIdToRemove; this.contributorDashboardAdminBackendApiService .removeContributionReviewerAsync( this.username, constants.CD_USER_RIGHTS_CATEGORY_REVIEW_TRANSLATION, - languageIdToRemove).then(() => { - this.assignedLanguageIds.splice(languageIdIndex, 1); - this.languageIdInUpdate = null; - this.updateLanguageIdsForSelection(); - }, errorMessage => { - this.alertsService.addWarning( - errorMessage || 'Error communicating with server.'); - }); + languageIdToRemove + ) + .then( + () => { + this.assignedLanguageIds.splice(languageIdIndex, 1); + this.languageIdInUpdate = null; + this.updateLanguageIdsForSelection(); + }, + errorMessage => { + this.alertsService.addWarning( + errorMessage || 'Error communicating with server.' + ); + } + ); } close(): void { diff --git a/core/templates/pages/contributor-dashboard-admin-page/username-input-modal/username-input-modal.component.spec.ts b/core/templates/pages/contributor-dashboard-admin-page/username-input-modal/username-input-modal.component.spec.ts index f45f19b1f1d6..254a276c0699 100644 --- a/core/templates/pages/contributor-dashboard-admin-page/username-input-modal/username-input-modal.component.spec.ts +++ b/core/templates/pages/contributor-dashboard-admin-page/username-input-modal/username-input-modal.component.spec.ts @@ -16,13 +16,19 @@ * @fileoverview Unit tests for UsernameInputModal. */ -import { ComponentFixture, TestBed, fakeAsync, tick, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { FormsModule } from '@angular/forms'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { + ComponentFixture, + TestBed, + fakeAsync, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {FormsModule} from '@angular/forms'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; -import { SignupPageBackendApiService } from '../../signup-page/services/signup-page-backend-api.service'; -import { UsernameInputModal } from './username-input-modal.component'; +import {SignupPageBackendApiService} from '../../signup-page/services/signup-page-backend-api.service'; +import {UsernameInputModal} from './username-input-modal.component'; describe('UsernameInputModal', () => { let component: UsernameInputModal; @@ -32,15 +38,9 @@ describe('UsernameInputModal', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - FormsModule, - HttpClientTestingModule - ], + imports: [FormsModule, HttpClientTestingModule], declarations: [UsernameInputModal], - providers: [ - NgbActiveModal, - SignupPageBackendApiService - ] + providers: [NgbActiveModal, SignupPageBackendApiService], }).compileComponents(); })); @@ -48,15 +48,15 @@ describe('UsernameInputModal', () => { fixture = TestBed.createComponent(UsernameInputModal); component = fixture.componentInstance; ngbActiveModal = TestBed.get(NgbActiveModal); - signupPageBackendApiService = TestBed.inject( - SignupPageBackendApiService); + signupPageBackendApiService = TestBed.inject(SignupPageBackendApiService); fixture.detectChanges(); component.ngOnInit(); }); it('should throw error when entered username is invaid', fakeAsync(() => { spyOn( - signupPageBackendApiService, 'checkUsernameAvailableAsync' + signupPageBackendApiService, + 'checkUsernameAvailableAsync' ).and.returnValue(Promise.resolve({username_is_taken: false})); component.saveAndClose(); @@ -67,7 +67,8 @@ describe('UsernameInputModal', () => { it('should save and close with correct username', fakeAsync(() => { spyOn( - signupPageBackendApiService, 'checkUsernameAvailableAsync' + signupPageBackendApiService, + 'checkUsernameAvailableAsync' ).and.returnValue(Promise.resolve({username_is_taken: true})); const modalCloseSpy = spyOn(ngbActiveModal, 'close').and.callThrough(); component.username = 'user1'; diff --git a/core/templates/pages/contributor-dashboard-admin-page/username-input-modal/username-input-modal.component.ts b/core/templates/pages/contributor-dashboard-admin-page/username-input-modal/username-input-modal.component.ts index 3d8c59fd39fc..9370b923ce28 100644 --- a/core/templates/pages/contributor-dashboard-admin-page/username-input-modal/username-input-modal.component.ts +++ b/core/templates/pages/contributor-dashboard-admin-page/username-input-modal/username-input-modal.component.ts @@ -16,16 +16,15 @@ * @fileoverview Component for looking up a user by username. */ -import { Component, OnInit, Input } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import {Component, OnInit, Input} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; -import { SignupPageBackendApiService } from '../../signup-page/services/signup-page-backend-api.service'; +import {SignupPageBackendApiService} from '../../signup-page/services/signup-page-backend-api.service'; @Component({ selector: 'oppia-username-input-modal', templateUrl: './username-input-modal.component.html', }) - export class UsernameInputModal implements OnInit { // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see @@ -47,9 +46,9 @@ export class UsernameInputModal implements OnInit { saveAndClose(): void { this.isChecking = true; this.isInvalidUsername = false; - this.signupPageBackendApiService.checkUsernameAvailableAsync( - this.username).then( - response => { + this.signupPageBackendApiService + .checkUsernameAvailableAsync(this.username) + .then(response => { if (!response.username_is_taken) { this.isInvalidUsername = true; this.username = ''; diff --git a/core/templates/pages/contributor-dashboard-page/badge/badge.component.spec.ts b/core/templates/pages/contributor-dashboard-page/badge/badge.component.spec.ts index fd323221ef96..362e791bef74 100644 --- a/core/templates/pages/contributor-dashboard-page/badge/badge.component.spec.ts +++ b/core/templates/pages/contributor-dashboard-page/badge/badge.component.spec.ts @@ -16,11 +16,16 @@ * @fileoverview Unit tests for BadgeComponent. */ -import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { BadgeComponent } from './badge.component'; -import { AppConstants } from 'app.constants'; +import { + ComponentFixture, + fakeAsync, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {BadgeComponent} from './badge.component'; +import {AppConstants} from 'app.constants'; describe('Badge component', () => { let component: BadgeComponent; @@ -29,11 +34,9 @@ describe('Badge component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - BadgeComponent - ], + declarations: [BadgeComponent], providers: [], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -48,8 +51,8 @@ describe('Badge component', () => { describe('when a submission badge is passed ', () => { it('should show submission badges', fakeAsync(() => { - component.contributionSubType = AppConstants - .CONTRIBUTION_STATS_SUBTYPE_SUBMISSION; + component.contributionSubType = + AppConstants.CONTRIBUTION_STATS_SUBTYPE_SUBMISSION; component.contributionCount = 1; component.language = 'Hindi'; @@ -60,8 +63,8 @@ describe('Badge component', () => { })); it('should show review badges', fakeAsync(() => { - component.contributionSubType = AppConstants - .CONTRIBUTION_STATS_SUBTYPE_REVIEW; + component.contributionSubType = + AppConstants.CONTRIBUTION_STATS_SUBTYPE_REVIEW; component.contributionCount = 1; component.language = 'Hindi'; @@ -72,8 +75,8 @@ describe('Badge component', () => { })); it('should show correction badges', fakeAsync(() => { - component.contributionSubType = AppConstants - .CONTRIBUTION_STATS_SUBTYPE_CORRECTION; + component.contributionSubType = + AppConstants.CONTRIBUTION_STATS_SUBTYPE_CORRECTION; component.contributionCount = 1; component.language = 'Hindi'; @@ -84,8 +87,8 @@ describe('Badge component', () => { })); it('should show the plural form of badge text', fakeAsync(() => { - component.contributionSubType = AppConstants - .CONTRIBUTION_STATS_SUBTYPE_SUBMISSION; + component.contributionSubType = + AppConstants.CONTRIBUTION_STATS_SUBTYPE_SUBMISSION; component.contributionCount = 10; component.language = 'Hindi'; @@ -98,8 +101,8 @@ describe('Badge component', () => { describe('when a long language text is given ', () => { it('should decrease the font size', fakeAsync(() => { - component.contributionSubType = AppConstants - .CONTRIBUTION_STATS_SUBTYPE_SUBMISSION; + component.contributionSubType = + AppConstants.CONTRIBUTION_STATS_SUBTYPE_SUBMISSION; component.contributionCount = 1; component.language = 'Netherlands'; @@ -110,8 +113,8 @@ describe('Badge component', () => { })); it('should decrease the line height', fakeAsync(() => { - component.contributionSubType = AppConstants - .CONTRIBUTION_STATS_SUBTYPE_SUBMISSION; + component.contributionSubType = + AppConstants.CONTRIBUTION_STATS_SUBTYPE_SUBMISSION; component.contributionCount = 1; component.language = 'Bahasa Indonesia'; diff --git a/core/templates/pages/contributor-dashboard-page/badge/badge.component.ts b/core/templates/pages/contributor-dashboard-page/badge/badge.component.ts index ef0e66145b66..065c57f9d4d7 100644 --- a/core/templates/pages/contributor-dashboard-page/badge/badge.component.ts +++ b/core/templates/pages/contributor-dashboard-page/badge/badge.component.ts @@ -16,8 +16,8 @@ * @fileoverview Component for the badge. */ -import { Component, Input } from '@angular/core'; -import { AppConstants } from 'app.constants'; +import {Component, Input} from '@angular/core'; +import {AppConstants} from 'app.constants'; interface ContributionSubTypeTexts { submission: string; @@ -28,7 +28,7 @@ interface ContributionSubTypeTexts { @Component({ selector: 'badge', templateUrl: './badge.component.html', - styleUrls: [] + styleUrls: [], }) export class BadgeComponent { @Input() contributionType!: string; @@ -43,14 +43,14 @@ export class BadgeComponent { CONTRIBUTION_SUB_TYPE_TEXTS: ContributionSubTypeTexts = { [AppConstants.CONTRIBUTION_STATS_SUBTYPE_SUBMISSION]: 'Submission', [AppConstants.CONTRIBUTION_STATS_SUBTYPE_REVIEW]: 'Review', - [AppConstants.CONTRIBUTION_STATS_SUBTYPE_CORRECTION]: 'Correction' + [AppConstants.CONTRIBUTION_STATS_SUBTYPE_CORRECTION]: 'Correction', }; constructor() {} ngOnInit(): void { - this.contributionSubTypeText = this.CONTRIBUTION_SUB_TYPE_TEXTS[ - this.contributionSubType]; + this.contributionSubTypeText = + this.CONTRIBUTION_SUB_TYPE_TEXTS[this.contributionSubType]; if (this.contributionCount > 1) { this.contributionSubTypeText += 's'; diff --git a/core/templates/pages/contributor-dashboard-page/contributions-and-review/contributions-and-review.component.spec.ts b/core/templates/pages/contributor-dashboard-page/contributions-and-review/contributions-and-review.component.spec.ts index 7268a7b0780e..d295337f4e19 100644 --- a/core/templates/pages/contributor-dashboard-page/contributions-and-review/contributions-and-review.component.spec.ts +++ b/core/templates/pages/contributor-dashboard-page/contributions-and-review/contributions-and-review.component.spec.ts @@ -16,36 +16,51 @@ * @fileoverview Unit tests for contributionsAndReview. */ -import { ComponentFixture, fakeAsync, flush, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ExplorationOpportunitySummary } from 'domain/opportunity/exploration-opportunity-summary.model'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { ContributionDetails, ContributionsAndReview, Opportunity, Suggestion, SuggestionDetails } from './contributions-and-review.component'; -import { SkillBackendApiService } from 'domain/skill/skill-backend-api.service'; -import { TranslationTopicService } from 'pages/exploration-editor-page/translation-tab/services/translation-topic.service'; -import { MisconceptionObjectFactory } from 'domain/skill/MisconceptionObjectFactory'; -import { SkillObjectFactory } from 'domain/skill/SkillObjectFactory'; -import { ContextService } from 'services/context.service'; -import { UserService } from 'services/user.service'; -import { ContributionAndReviewService } from '../services/contribution-and-review.service'; -import { ContributionOpportunitiesService } from '../services/contribution-opportunities.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { UserInfo } from 'domain/user/user-info.model'; -import { CsrfTokenService } from 'services/csrf-token.service'; -import { AlertsService } from 'services/alerts.service'; -import { QuestionObjectFactory } from 'domain/question/QuestionObjectFactory'; -import { FormatRtePreviewPipe } from 'filters/format-rte-preview.pipe'; -import { PlatformFeatureService } from 'services/platform-feature.service'; -import { OpportunitiesListComponent } from '../opportunities-list/opportunities-list.component'; -import { HtmlEscaperService } from 'services/html-escaper.service'; -import { MatIconModule } from '@angular/material/icon'; -import { MatSnackBarModule } from '@angular/material/snack-bar'; -import { MatSnackBar, MatSnackBarRef, MAT_SNACK_BAR_DATA } from '@angular/material/snack-bar'; -import {of, Subject } from 'rxjs'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { delay } from 'rxjs/operators'; - - +import { + ComponentFixture, + fakeAsync, + flush, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import {ExplorationOpportunitySummary} from 'domain/opportunity/exploration-opportunity-summary.model'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import { + ContributionDetails, + ContributionsAndReview, + Opportunity, + Suggestion, + SuggestionDetails, +} from './contributions-and-review.component'; +import {SkillBackendApiService} from 'domain/skill/skill-backend-api.service'; +import {TranslationTopicService} from 'pages/exploration-editor-page/translation-tab/services/translation-topic.service'; +import {MisconceptionObjectFactory} from 'domain/skill/MisconceptionObjectFactory'; +import {SkillObjectFactory} from 'domain/skill/SkillObjectFactory'; +import {ContextService} from 'services/context.service'; +import {UserService} from 'services/user.service'; +import {ContributionAndReviewService} from '../services/contribution-and-review.service'; +import {ContributionOpportunitiesService} from '../services/contribution-opportunities.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {UserInfo} from 'domain/user/user-info.model'; +import {CsrfTokenService} from 'services/csrf-token.service'; +import {AlertsService} from 'services/alerts.service'; +import {QuestionObjectFactory} from 'domain/question/QuestionObjectFactory'; +import {FormatRtePreviewPipe} from 'filters/format-rte-preview.pipe'; +import {PlatformFeatureService} from 'services/platform-feature.service'; +import {OpportunitiesListComponent} from '../opportunities-list/opportunities-list.component'; +import {HtmlEscaperService} from 'services/html-escaper.service'; +import {MatIconModule} from '@angular/material/icon'; +import {MatSnackBarModule} from '@angular/material/snack-bar'; +import { + MatSnackBar, + MatSnackBarRef, + MAT_SNACK_BAR_DATA, +} from '@angular/material/snack-bar'; +import {of, Subject} from 'rxjs'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {delay} from 'rxjs/operators'; class MockNgbModalRef { componentInstance: { @@ -59,7 +74,7 @@ class MockNgbModalRef { class MockNgbModal { open() { return { - result: Promise.resolve() + result: Promise.resolve(), }; } } @@ -67,12 +82,11 @@ class MockNgbModal { class MockPlatformFeatureService { status = { ContributorDashboardAccomplishments: { - isEnabled: false - } + isEnabled: false, + }, }; } - describe('Contributions and review component', () => { let component: ContributionsAndReview; let fixture: ComponentFixture; @@ -99,12 +113,14 @@ describe('Contributions and review component', () => { let snackBarRefMock; class MockMatSnackBarRef { - instance = { message: '' }; - afterDismissed = () => of({ action: '', dismissedByAction: false }); + instance = {message: ''}; + afterDismissed = () => of({action: '', dismissedByAction: false}); onAction = () => new Subject(); dismissWithAction = (a, b, c) => { contributionOpportunitiesService.pinReviewableTranslationOpportunityAsync( - a, b, c + a, + b, + c ); }; } @@ -114,18 +130,17 @@ describe('Contributions and review component', () => { MatIconModule, HttpClientTestingModule, MatSnackBarModule, - BrowserAnimationsModule], - declarations: [ - ContributionsAndReview + BrowserAnimationsModule, ], + declarations: [ContributionsAndReview], providers: [ { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, { provide: MatSnackBarRef, - useClass: MockMatSnackBarRef + useClass: MockMatSnackBarRef, }, ContextService, ContributionAndReviewService, @@ -140,14 +155,14 @@ describe('Contributions and review component', () => { TranslationTopicService, { provide: PlatformFeatureService, - useValue: mockPlatformFeatureService + useValue: mockPlatformFeatureService, }, UserService, OpportunitiesListComponent, MatSnackBar, - { provide: MAT_SNACK_BAR_DATA, useValue: {} } + {provide: MAT_SNACK_BAR_DATA, useValue: {}}, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -164,9 +179,9 @@ describe('Contributions and review component', () => { contextService = TestBed.inject(ContextService); skillBackendApiService = TestBed.inject(SkillBackendApiService); contributionOpportunitiesService = TestBed.inject( - ContributionOpportunitiesService); - formatRtePreviewPipe = TestBed.inject( - FormatRtePreviewPipe); + ContributionOpportunitiesService + ); + formatRtePreviewPipe = TestBed.inject(FormatRtePreviewPipe); htmlEscaperService = TestBed.inject(HtmlEscaperService); translationTopicService = TestBed.inject(TranslationTopicService); snackBar = TestBed.inject(MatSnackBar); @@ -175,24 +190,31 @@ describe('Contributions and review component', () => { spyOn( contributionOpportunitiesService.reloadOpportunitiesEventEmitter, - 'emit').and.callThrough(); + 'emit' + ).and.callThrough(); spyOn( contributionOpportunitiesService.reloadOpportunitiesEventEmitter, - 'subscribe').and.callThrough(); + 'subscribe' + ).and.callThrough(); spyOn(contextService, 'getExplorationId').and.returnValue('exp1'); - spyOn(userService, 'getUserInfoAsync') - .and.returnValue(Promise.resolve({ - isLoggedIn: () => true - } as UserInfo)); - getUserContributionRightsDataAsyncSpy = - spyOn(userService, 'getUserContributionRightsDataAsync'); - - getUserContributionRightsDataAsyncSpy.and.returnValue(Promise.resolve({ - can_review_translation_for_language_codes: ['hi'], - can_review_questions: true, - can_review_voiceover_for_language_codes: [], - can_suggest_questions: false, - })); + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve({ + isLoggedIn: () => true, + } as UserInfo) + ); + getUserContributionRightsDataAsyncSpy = spyOn( + userService, + 'getUserContributionRightsDataAsync' + ); + + getUserContributionRightsDataAsyncSpy.and.returnValue( + Promise.resolve({ + can_review_translation_for_language_codes: ['hi'], + can_review_questions: true, + can_review_voiceover_for_language_codes: [], + can_suggest_questions: false, + }) + ); spyOn( contributionOpportunitiesService, 'getReviewableTranslationOpportunitiesAsync' @@ -206,10 +228,10 @@ describe('Contributions and review component', () => { chapter_title: 'Chapter 1', content_count: 1, translation_counts: { - en: 2 + en: 2, }, translation_in_review_counts: { - en: 2 + en: 2, }, is_pinned: false, }), @@ -220,19 +242,21 @@ describe('Contributions and review component', () => { chapter_title: 'Chapter 2', content_count: 2, translation_counts: { - en: 4 + en: 4, }, translation_in_review_counts: { - en: 4 + en: 4, }, - is_pinned: false - }) + is_pinned: false, + }), ], - more: false - })); + more: false, + }) + ); getUserCreatedTranslationSuggestionsAsyncSpy = spyOn( contributionAndReviewService, - 'getUserCreatedTranslationSuggestionsAsync').and.returnValue( + 'getUserCreatedTranslationSuggestionsAsync' + ).and.returnValue( Promise.resolve({ suggestionIdToDetails: { suggestion_1: { @@ -249,21 +273,24 @@ describe('Contributions and review component', () => { old_value: null, content_html: 'Translation', translation_html: 'Tradução', - skill_id: 'skill_id' + skill_id: 'skill_id', }, - status: 'review' + status: 'review', }, details: { skill_id: 'skill_1', - skill_description: 'skill_1' - } - } + skill_description: 'skill_1', + }, + }, }, - more: false - })); + more: false, + }) + ); getReviewableQuestionSuggestionsAsyncSpy = spyOn( - contributionAndReviewService, 'getReviewableQuestionSuggestionsAsync') - .and.returnValue(Promise.resolve({ + contributionAndReviewService, + 'getReviewableQuestionSuggestionsAsync' + ).and.returnValue( + Promise.resolve({ suggestionIdToDetails: { suggestion_1: { suggestion: { @@ -283,82 +310,89 @@ describe('Contributions and review component', () => { question_state_data: { content: { html: 'Question 1', - content_id: 'content_1' + content_id: 'content_1', }, interaction: { - answer_groups: [{ - outcome: { - dest: 'outcome 1', - dest_if_really_stuck: null, - feedback: { - content_id: 'content_5', - html: '' + answer_groups: [ + { + outcome: { + dest: 'outcome 1', + dest_if_really_stuck: null, + feedback: { + content_id: 'content_5', + html: '', + }, + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, }, - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null + rule_specs: [], }, - rule_specs: [], - }], + ], confirmed_unclassified_answers: [], customization_args: { placeholder: { value: { content_id: 'ca_placeholder_0', - unicode_str: '' - } + unicode_str: '', + }, }, - rows: { value: 1 }, + rows: {value: 1}, catchMisspellings: { - value: false - } + value: false, + }, }, default_outcome: { dest: null, dest_if_really_stuck: null, feedback: { html: 'Correct Answer', - content_id: 'content_2' + content_id: 'content_2', }, param_changes: [], - labelled_as_correct: true + labelled_as_correct: true, }, - hints: [{ - hint_content: { - html: 'Hint 1', - content_id: 'content_3' - } - }], + hints: [ + { + hint_content: { + html: 'Hint 1', + content_id: 'content_3', + }, + }, + ], solution: { correct_answer: 'This is the correct answer', answer_is_exclusive: false, explanation: { html: 'Solution explanation', - content_id: 'content_4' - } + content_id: 'content_4', + }, }, - id: 'TextInput' + id: 'TextInput', }, param_changes: [], recorded_voiceovers: { - voiceovers_mapping: {} - } + voiceovers_mapping: {}, + }, }, - } + }, }, - status: 'review' + status: 'review', }, details: { skill_description: 'Skill description', skill_id: null, - } - } + }, + }, }, more: false, - })); + }) + ); getUserCreatedQuestionSuggestionsAsyncSpy = spyOn( - contributionAndReviewService, 'getUserCreatedQuestionSuggestionsAsync') - .and.returnValue(Promise.resolve({ + contributionAndReviewService, + 'getUserCreatedQuestionSuggestionsAsync' + ).and.returnValue( + Promise.resolve({ suggestionIdToDetails: { suggestion_1: { suggestion: { @@ -378,122 +412,134 @@ describe('Contributions and review component', () => { question_state_data: { content: { html: 'Question 1', - content_id: 'content_1' + content_id: 'content_1', }, interaction: { - answer_groups: [{ - outcome: { - dest: 'outcome 1', - dest_if_really_stuck: null, - feedback: { - content_id: 'content_5', - html: '' + answer_groups: [ + { + outcome: { + dest: 'outcome 1', + dest_if_really_stuck: null, + feedback: { + content_id: 'content_5', + html: '', + }, + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, }, - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null + rule_specs: [], }, - rule_specs: [], - }], + ], confirmed_unclassified_answers: [], customization_args: { placeholder: { value: { content_id: 'ca_placeholder_0', - unicode_str: '' - } + unicode_str: '', + }, }, - rows: { value: 1 }, + rows: {value: 1}, catchMisspellings: { - value: false - } + value: false, + }, }, default_outcome: { dest: null, dest_if_really_stuck: null, feedback: { html: 'Correct Answer', - content_id: 'content_2' + content_id: 'content_2', }, param_changes: [], - labelled_as_correct: true + labelled_as_correct: true, }, - hints: [{ - hint_content: { - html: 'Hint 1', - content_id: 'content_3' - } - }], + hints: [ + { + hint_content: { + html: 'Hint 1', + content_id: 'content_3', + }, + }, + ], solution: { correct_answer: 'This is the correct answer', answer_is_exclusive: false, explanation: { html: 'Solution explanation', - content_id: 'content_4' - } + content_id: 'content_4', + }, }, - id: 'TextInput' + id: 'TextInput', }, param_changes: [], recorded_voiceovers: { - voiceovers_mapping: {} - } + voiceovers_mapping: {}, + }, }, - } + }, }, - status: 'accepted' + status: 'accepted', }, details: { skill_id: 'skill_1', - skill_description: 'skill_1' - } - } + skill_description: 'skill_1', + }, + }, }, - more: false - })); - spyOnProperty(translationTopicService, 'onActiveTopicChanged') - .and.returnValue(mockActiveTopicEventEmitter); - spyOn(skillBackendApiService, 'fetchSkillAsync') - .and.returnValue( - Promise.resolve({ - skill: skillObjectFactory.createFromBackendDict({ - id: 'skill1', - description: 'test description 1', - misconceptions: [{ + more: false, + }) + ); + spyOnProperty( + translationTopicService, + 'onActiveTopicChanged' + ).and.returnValue(mockActiveTopicEventEmitter); + spyOn(skillBackendApiService, 'fetchSkillAsync').and.returnValue( + Promise.resolve({ + skill: skillObjectFactory.createFromBackendDict({ + id: 'skill1', + description: 'test description 1', + misconceptions: [ + { id: 2, name: 'test name', notes: 'test notes', feedback: 'test feedback', - must_be_addressed: true - }], - rubrics: [{ + must_be_addressed: true, + }, + ], + rubrics: [ + { difficulty: 'Easy', - explanations: ['explanation'] - }], - skill_contents: { - explanation: { - html: 'test explanation', - content_id: 'explanation', - }, - worked_examples: [], - recorded_voiceovers: { - voiceovers_mapping: {} - } + explanations: ['explanation'], }, - language_code: 'en', - version: 3, - prerequisite_skill_ids: ['skill_1'], - all_questions_merged: false, - next_misconception_id: 0, - superseding_skill_id: '' - }), - assignedSkillTopicData: null, - groupedSkillSummaries: null - })); + ], + skill_contents: { + explanation: { + html: 'test explanation', + content_id: 'explanation', + }, + worked_examples: [], + recorded_voiceovers: { + voiceovers_mapping: {}, + }, + }, + language_code: 'en', + version: 3, + prerequisite_skill_ids: ['skill_1'], + all_questions_merged: false, + next_misconception_id: 0, + superseding_skill_id: '', + }), + assignedSkillTopicData: null, + groupedSkillSummaries: null, + }) + ); getReviewableTranslationSuggestionsAsyncSpy = spyOn( contributionAndReviewService, - 'getReviewableTranslationSuggestionsAsync') - .and.returnValue(Promise.resolve({ + 'getReviewableTranslationSuggestionsAsync' + ).and.returnValue( + Promise.resolve({ suggestionIdToDetails: { suggestion_1: { suggestion: { @@ -508,20 +554,21 @@ describe('Contributions and review component', () => { new_value: null, old_value: null, content_html: 'Translation', - translation_html: 'Tradução' + translation_html: 'Tradução', }, - status: 'review' + status: 'review', }, details: { skill_id: 'skill_1', - skill_description: 'skill_1' - } - } + skill_description: 'skill_1', + }, + }, }, - more: false - })); - mockPlatformFeatureService. - status.ContributorDashboardAccomplishments.isEnabled = true; + more: false, + }) + ); + mockPlatformFeatureService.status.ContributorDashboardAccomplishments.isEnabled = + true; fixture.detectChanges(); })); @@ -559,7 +606,7 @@ describe('Contributions and review component', () => { action: null, reviewMessage: null, skillDifficulty: null, - }) + }), } as NgbModalRef); let suggestion = { @@ -567,7 +614,7 @@ describe('Contributions and review component', () => { skill_id: 'skill1', question_dict: null, skill_difficulty: null, - translation_html: ['suggestion_1', 'suggestion_2'] + translation_html: ['suggestion_1', 'suggestion_2'], }, target_id: 'string;,', suggestion_id: 'suggestion_id', @@ -575,40 +622,42 @@ describe('Contributions and review component', () => { }; let contributionDetails = { skill_description: 'string', - skill_rubrics: [] + skill_rubrics: [], }; - let question = questionObjectFactory.createFromBackendDict( - { - question_state_data_schema_version: null, - id: 'question_1', - question_state_data: { - classifier_model_id: null, - card_is_checkpoint: null, - linked_skill_id: null, - content: { - html: 'Question 1', - content_id: 'content_1' - }, - interaction: { - answer_groups: [{ + let question = questionObjectFactory.createFromBackendDict({ + question_state_data_schema_version: null, + id: 'question_1', + question_state_data: { + classifier_model_id: null, + card_is_checkpoint: null, + linked_skill_id: null, + content: { + html: 'Question 1', + content_id: 'content_1', + }, + interaction: { + answer_groups: [ + { outcome: { missing_prerequisite_skill_id: null, dest: 'outcome 1', dest_if_really_stuck: null, feedback: { content_id: 'content_5', - html: '' + html: '', }, labelled_as_correct: true, param_changes: [], - refresher_exploration_id: null + refresher_exploration_id: null, }, training_data: null, - rule_specs: [{ - rule_type: 'Equals', - inputs: {x: 10} - }], - tagged_skill_misconception_id: null + rule_specs: [ + { + rule_type: 'Equals', + inputs: {x: 10}, + }, + ], + tagged_skill_misconception_id: null, }, { training_data: null, @@ -618,91 +667,96 @@ describe('Contributions and review component', () => { dest_if_really_stuck: null, feedback: { content_id: 'content_5', - html: '' + html: '', }, labelled_as_correct: false, param_changes: [], - refresher_exploration_id: null + refresher_exploration_id: null, }, - rule_specs: [{ - rule_type: 'Equals', - inputs: {x: 10} - }], - tagged_skill_misconception_id: 'abc-1' - }], - confirmed_unclassified_answers: [], - customization_args: { - placeholder: { - value: { - content_id: 'ca_placeholder_0', - unicode_str: '' - } + rule_specs: [ + { + rule_type: 'Equals', + inputs: {x: 10}, + }, + ], + tagged_skill_misconception_id: 'abc-1', + }, + ], + confirmed_unclassified_answers: [], + customization_args: { + placeholder: { + value: { + content_id: 'ca_placeholder_0', + unicode_str: '', }, - rows: { value: 1 }, - catchMisspellings: { - value: false - } }, - default_outcome: { - dest: null, - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest_if_really_stuck: null, - feedback: { - html: 'Correct Answer', - content_id: 'content_2' + rows: {value: 1}, + catchMisspellings: { + value: false, + }, + }, + default_outcome: { + dest: null, + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest_if_really_stuck: null, + feedback: { + html: 'Correct Answer', + content_id: 'content_2', + }, + param_changes: [], + labelled_as_correct: false, + }, + hints: [ + { + hint_content: { + html: 'Hint 1', + content_id: 'content_3', }, - param_changes: [], - labelled_as_correct: false }, - hints: [ - { - hint_content: { - html: 'Hint 1', - content_id: 'content_3' - } - } - ], - solution: { - correct_answer: 'This is the correct answer', - answer_is_exclusive: false, - explanation: { - html: 'Solution explanation', - content_id: 'content_4' - } + ], + solution: { + correct_answer: 'This is the correct answer', + answer_is_exclusive: false, + explanation: { + html: 'Solution explanation', + content_id: 'content_4', }, - id: 'TextInput' }, - param_changes: [], - recorded_voiceovers: { - voiceovers_mapping: { - content_1: {}, - content_2: {}, - content_3: {}, - content_4: {}, - content_5: {} - } + id: 'TextInput', + }, + param_changes: [], + recorded_voiceovers: { + voiceovers_mapping: { + content_1: {}, + content_2: {}, + content_3: {}, + content_4: {}, + content_5: {}, }, - solicit_answer_details: false }, - language_code: 'en', - version: 1, - linked_skill_ids: ['abc'], - next_content_id_index: 1, - inapplicable_skill_misconception_ids: ['abc-2'] - }); + solicit_answer_details: false, + }, + language_code: 'en', + version: 1, + linked_skill_ids: ['abc'], + next_content_id_index: 1, + inapplicable_skill_misconception_ids: ['abc-2'], + }); spyOn(contextService, 'setCustomEntityContext').and.stub(); component.contributions = { suggestion_id: { details: contributionDetails as ContributionDetails, suggestion: null, - } + }, }; component.openQuestionSuggestionModal( - 'suggestion_id', suggestion as Suggestion, + 'suggestion_id', + suggestion as Suggestion, false, - question); + question + ); let value = { suggestionId: null, @@ -714,29 +768,31 @@ describe('Contributions and review component', () => { tick(); tick(); - expect(contributionAndReviewService.reviewSkillSuggestion) - .toHaveBeenCalled(); + expect( + contributionAndReviewService.reviewSkillSuggestion + ).toHaveBeenCalled(); expect(ngbModal.open).toHaveBeenCalled(); })); - it('should clear activeExplorationId when active topic changes', - fakeAsync(() => { - component.onClickReviewableTranslations('explorationId'); - expect(component.activeExplorationId).toBe('explorationId'); + it('should clear activeExplorationId when active topic changes', fakeAsync(() => { + component.onClickReviewableTranslations('explorationId'); + expect(component.activeExplorationId).toBe('explorationId'); - mockActiveTopicEventEmitter.emit(); - tick(); + mockActiveTopicEventEmitter.emit(); + tick(); - expect(component.activeExplorationId).toBeNull(); - })); + expect(component.activeExplorationId).toBeNull(); + })); it('should be able to change language', fakeAsync(() => { component.opportunitiesListRef = TestBed.inject( - OpportunitiesListComponent); - spyOn(component.opportunitiesListRef, 'onChangeLanguage') - .and.callFake(() => { + OpportunitiesListComponent + ); + spyOn(component.opportunitiesListRef, 'onChangeLanguage').and.callFake( + () => { return; - }); + } + ); expect(component.languageCode).toBeUndefined(); @@ -761,8 +817,9 @@ describe('Contributions and review component', () => { component.resolveSuggestionSuccess('suggestion_id'); tick(); - expect(alertsService.addSuccessMessage) - .toHaveBeenCalledWith('Submitted suggestion review.'); + expect(alertsService.addSuccessMessage).toHaveBeenCalledWith( + 'Submitted suggestion review.' + ); })); it('should return false on Review Questions tab', () => { @@ -789,15 +846,17 @@ describe('Contributions and review component', () => { translation_html: '', }, status: '', - } - } + }, + }, }; component.onClickViewSuggestion('SUGGESTION'); }); it('should return false on Translation Contributions tab', () => { component.switchToTab( - component.TAB_TYPE_CONTRIBUTIONS, 'translate_content'); + component.TAB_TYPE_CONTRIBUTIONS, + 'translate_content' + ); expect(component.isReviewTranslationsTab()).toBeFalse(); }); @@ -844,7 +903,7 @@ describe('Contributions and review component', () => { action: null, reviewMessage: null, skillDifficulty: null, - }) + }), } as NgbModalRef); let questionDict = { @@ -856,61 +915,67 @@ describe('Contributions and review component', () => { linked_skill_id: null, content: { html: 'Question 1', - content_id: 'content_1' + content_id: 'content_1', }, interaction: { - answer_groups: [{ - outcome: { - missing_prerequisite_skill_id: null, - dest: 'outcome 1', - dest_if_really_stuck: null, - feedback: { - content_id: 'content_5', - html: '' + answer_groups: [ + { + outcome: { + missing_prerequisite_skill_id: null, + dest: 'outcome 1', + dest_if_really_stuck: null, + feedback: { + content_id: 'content_5', + html: '', + }, + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, }, - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null + training_data: null, + rule_specs: [ + { + rule_type: 'Equals', + inputs: {x: 10}, + }, + ], + tagged_skill_misconception_id: null, }, - training_data: null, - rule_specs: [{ - rule_type: 'Equals', - inputs: {x: 10} - }], - tagged_skill_misconception_id: null - }, - { - training_data: null, - outcome: { - missing_prerequisite_skill_id: null, - dest: 'outcome 1', - dest_if_really_stuck: null, - feedback: { - content_id: 'content_5', - html: '' + { + training_data: null, + outcome: { + missing_prerequisite_skill_id: null, + dest: 'outcome 1', + dest_if_really_stuck: null, + feedback: { + content_id: 'content_5', + html: '', + }, + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, }, - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null + rule_specs: [ + { + rule_type: 'Equals', + inputs: {x: 10}, + }, + ], + tagged_skill_misconception_id: 'abc-1', }, - rule_specs: [{ - rule_type: 'Equals', - inputs: {x: 10} - }], - tagged_skill_misconception_id: 'abc-1' - }], + ], confirmed_unclassified_answers: [], customization_args: { placeholder: { value: { content_id: 'ca_placeholder_0', - unicode_str: '' - } + unicode_str: '', + }, }, - rows: { value: 1 }, + rows: {value: 1}, catchMisspellings: { - value: false - } + value: false, + }, }, default_outcome: { dest: null, @@ -919,28 +984,28 @@ describe('Contributions and review component', () => { dest_if_really_stuck: null, feedback: { html: 'Correct Answer', - content_id: 'content_2' + content_id: 'content_2', }, param_changes: [], - labelled_as_correct: false + labelled_as_correct: false, }, hints: [ { hint_content: { html: 'Hint 1', - content_id: 'content_3' - } - } + content_id: 'content_3', + }, + }, ], solution: { correct_answer: 'This is the correct answer', answer_is_exclusive: false, explanation: { html: 'Solution explanation', - content_id: 'content_4' - } + content_id: 'content_4', + }, }, - id: 'TextInput' + id: 'TextInput', }, param_changes: [], recorded_voiceovers: { @@ -949,16 +1014,16 @@ describe('Contributions and review component', () => { content_2: {}, content_3: {}, content_4: {}, - content_5: {} - } + content_5: {}, + }, }, - solicit_answer_details: false + solicit_answer_details: false, }, language_code: 'en', version: 1, linked_skill_ids: ['abc'], next_content_id_index: 6, - inapplicable_skill_misconception_ids: ['abc-2'] + inapplicable_skill_misconception_ids: ['abc-2'], }; let suggestion = { @@ -974,7 +1039,7 @@ describe('Contributions and review component', () => { suggestion_id: 'string;', author_name: 'string;', suggestion_type: 'question', - exploration_content_html: '' + exploration_content_html: '', }; let suggestionIdToContribution = { @@ -1004,87 +1069,92 @@ describe('Contributions and review component', () => { question_state_data: { content: { html: 'Question 1', - content_id: 'content_1' + content_id: 'content_1', }, interaction: { - answer_groups: [{ - outcome: { - dest: 'outcome 1', - dest_if_really_stuck: null, - feedback: { - content_id: 'content_5', - html: '' + answer_groups: [ + { + outcome: { + dest: 'outcome 1', + dest_if_really_stuck: null, + feedback: { + content_id: 'content_5', + html: '', + }, + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, }, - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null + rule_specs: [], }, - rule_specs: [], - }], + ], confirmed_unclassified_answers: [], customization_args: { placeholder: { value: { content_id: 'ca_placeholder_0', - unicode_str: '' - } + unicode_str: '', + }, }, - rows: { value: 1 }, + rows: {value: 1}, catchMisspellings: { - value: false - } + value: false, + }, }, default_outcome: { dest: null, dest_if_really_stuck: null, feedback: { html: 'Correct Answer', - content_id: 'content_2' + content_id: 'content_2', }, param_changes: [], - labelled_as_correct: true + labelled_as_correct: true, }, - hints: [{ - hint_content: { - html: 'Hint 1', - content_id: 'content_3' - } - }], + hints: [ + { + hint_content: { + html: 'Hint 1', + content_id: 'content_3', + }, + }, + ], solution: { correct_answer: 'This is the correct answer', answer_is_exclusive: false, explanation: { html: 'Solution explanation', - content_id: 'content_4' - } + content_id: 'content_4', + }, }, - id: 'TextInput' + id: 'TextInput', }, param_changes: [], recorded_voiceovers: { - voiceovers_mapping: {} - } + voiceovers_mapping: {}, + }, }, - } + }, }, - status: 'review' + status: 'review', }, details: { skill_description: 'Skill description', skill_id: null, chapter_title: null, story_title: null, - topic_name: null - } - } - }; + topic_name: null, + }, + }, + }; component._showQuestionSuggestionModal( suggestion, suggestionIdToContribution, false, null, - null); + null + ); let value = { suggestionId: null, @@ -1095,8 +1165,9 @@ describe('Contributions and review component', () => { eventEmitter.emit(value); tick(); - expect(contributionAndReviewService.reviewSkillSuggestion) - .toHaveBeenCalled(); + expect( + contributionAndReviewService.reviewSkillSuggestion + ).toHaveBeenCalled(); expect(component.openQuestionSuggestionModal).toHaveBeenCalled(); expect(ngbModal.open).toHaveBeenCalled(); })); @@ -1118,15 +1189,18 @@ describe('Contributions and review component', () => { it('should load reviewable questions', () => { component.loadContributions(null).then(({opportunitiesDicts, more}) => { expect(Object.keys(component.contributions)).toContain( - 'suggestion_1'); - expect(opportunitiesDicts).toEqual([{ - id: 'suggestion_1', - heading: 'Question 1', - subheading: 'Skill description', - labelText: 'Awaiting review', - labelColor: '#eeeeee', - actionButtonTitle: 'Review' - }]); + 'suggestion_1' + ); + expect(opportunitiesDicts).toEqual([ + { + id: 'suggestion_1', + heading: 'Question 1', + subheading: 'Skill description', + labelText: 'Awaiting review', + labelColor: '#eeeeee', + actionButtonTitle: 'Review', + }, + ]); expect(more).toEqual(false); }); }); @@ -1149,36 +1223,42 @@ describe('Contributions and review component', () => { old_value: null, content_html: 'Translation', translation_html: 'Tradução', - skill_id: 'skill_id' + skill_id: 'skill_id', }, status: 'rejected', - exploration_content_html: null + exploration_content_html: null, }, details: { topic_name: 'topic_name', story_title: 'story_title', - chapter_title: 'chapter_title' - } - } + chapter_title: 'chapter_title', + }, + }, }, - more: false - })); + more: false, + }) + ); component.switchToTab( - component.TAB_TYPE_CONTRIBUTIONS, 'translate_content'); + component.TAB_TYPE_CONTRIBUTIONS, + 'translate_content' + ); component.loadContributions(null).then(({opportunitiesDicts, more}) => { expect(Object.keys(component.contributions)).toContain( - 'suggestion_1'); - expect(opportunitiesDicts).toEqual([{ - id: 'suggestion_1', - heading: 'Tradução', - subheading: 'topic_name / story_title / chapter_title', - labelText: 'Obsolete', - labelColor: '#e76c8c', - actionButtonTitle: 'View', - translationWordCount: undefined - }]); + 'suggestion_1' + ); + expect(opportunitiesDicts).toEqual([ + { + id: 'suggestion_1', + heading: 'Tradução', + subheading: 'topic_name / story_title / chapter_title', + labelText: 'Obsolete', + labelColor: '#e76c8c', + actionButtonTitle: 'View', + translationWordCount: undefined, + }, + ]); expect(more).toEqual(false); }); }); @@ -1198,56 +1278,56 @@ describe('Contributions and review component', () => { new_value: 'new', old_value: 'old', content_html: 'Translation', - translation_html: 'Tradução' + translation_html: 'Tradução', }, status: 'rejected', - exploration_content_html: null + exploration_content_html: null, }, details: { topic_name: 'topic_name', story_title: 'story_title', - chapter_title: 'chapter_title' - } - } + chapter_title: 'chapter_title', + }, + }, }; const reviewableTranslation = Promise.resolve({ suggestionIdToDetails: suggestion1, - more: false + more: false, }); getReviewableTranslationSuggestionsAsyncSpy.and.returnValue( - reviewableTranslation); + reviewableTranslation + ); // Go to the review translations tab, to ensure that // getReviewableTranslationSuggestionsAsyncSpy is // called by loadContributions. - component.switchToTab( - component.TAB_TYPE_REVIEWS, 'translate_content'); + component.switchToTab(component.TAB_TYPE_REVIEWS, 'translate_content'); // Set up contributions with a translation to be reviewed. component.loadContributions(null).then(({opportunitiesDicts, more}) => { expect(Object.keys(component.contributions)).toContain( - 'suggestion_1'); - expect(opportunitiesDicts).toEqual([{ - id: 'suggestion_1', - heading: 'Tradução', - subheading: 'topic_name / story_title / chapter_title', - labelText: 'Obsolete', - labelColor: '#e76c8c', - actionButtonTitle: 'Review', - translationWordCount: undefined - }]); + 'suggestion_1' + ); + expect(opportunitiesDicts).toEqual([ + { + id: 'suggestion_1', + heading: 'Tradução', + subheading: 'topic_name / story_title / chapter_title', + labelText: 'Obsolete', + labelColor: '#e76c8c', + actionButtonTitle: 'Review', + translationWordCount: undefined, + }, + ]); expect(more).toEqual(false); // When opening the review modal for translations, // only translations should be shown. spyOn(component, '_showTranslationSuggestionModal'); component.onClickViewSuggestion('suggestion_1'); - expect(component._showTranslationSuggestionModal). - toHaveBeenCalledWith( - suggestion1, - 'suggestion_1', - true - ); + expect( + component._showTranslationSuggestionModal + ).toHaveBeenCalledWith(suggestion1, 'suggestion_1', true); }); // Wait for the first test to complete. tick(); @@ -1269,70 +1349,74 @@ describe('Contributions and review component', () => { question_state_data: { content: { html: 'Question 2', - content_id: 'content_2' + content_id: 'content_2', }, interaction: { - answer_groups: [{ - outcome: { - dest: 'outcome 1', - dest_if_really_stuck: null, - feedback: { - content_id: 'content_1', - html: '' + answer_groups: [ + { + outcome: { + dest: 'outcome 1', + dest_if_really_stuck: null, + feedback: { + content_id: 'content_1', + html: '', + }, + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, }, - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null + rule_specs: [], }, - rule_specs: [], - }], + ], confirmed_unclassified_answers: [], customization_args: { placeholder: { value: { content_id: 'ca_placeholder_0', - unicode_str: '' - } + unicode_str: '', + }, }, - rows: { value: 1 }, + rows: {value: 1}, catchMisspellings: { - value: false - } + value: false, + }, }, default_outcome: { dest: null, dest_if_really_stuck: null, feedback: { html: 'Correct Answer', - content_id: 'content_2' + content_id: 'content_2', }, param_changes: [], - labelled_as_correct: true + labelled_as_correct: true, }, - hints: [{ - hint_content: { - html: 'Hint 1', - content_id: 'content_3' - } - }], + hints: [ + { + hint_content: { + html: 'Hint 1', + content_id: 'content_3', + }, + }, + ], solution: { correct_answer: 'This is the correct answer', answer_is_exclusive: false, explanation: { html: 'Solution explanation', - content_id: 'content_4' - } + content_id: 'content_4', + }, }, - id: 'TextInput' + id: 'TextInput', }, param_changes: [], recorded_voiceovers: { - voiceovers_mapping: {} - } + voiceovers_mapping: {}, + }, }, - } + }, }, - status: 'accepted' + status: 'accepted', }; getUserCreatedQuestionSuggestionsAsyncSpy.and.returnValue( Promise.resolve({ @@ -1341,25 +1425,26 @@ describe('Contributions and review component', () => { suggestion: suggestion2, details: { skill_id: 'skill_1', - skill_description: 'skill_1' - } - } + skill_description: 'skill_1', + }, + }, }, - more: false - })); + more: false, + }) + ); // Go to the add questions tab, to ensure that // getUserCreatedQuestionSuggestionsAsyncSpy is // called by loadContributions. - component.switchToTab( - component.TAB_TYPE_CONTRIBUTIONS, 'add_question'); + component.switchToTab(component.TAB_TYPE_CONTRIBUTIONS, 'add_question'); // Load contributions object with a question. This should also remove // any data created in the previous call to loadContributions // from the component.contributions object. component.loadContributions(null).then(({opportunitiesDicts, more}) => { expect(Object.keys(component.contributions)).toContain( - 'suggestion_2'); + 'suggestion_2' + ); expect(opportunitiesDicts).toEqual([ { id: 'suggestion_2', @@ -1367,8 +1452,9 @@ describe('Contributions and review component', () => { subheading: 'skill_1', labelText: 'Accepted', labelColor: '#8ed274', - actionButtonTitle: 'View' - }]); + actionButtonTitle: 'View', + }, + ]); expect(more).toEqual(false); // When opening the contribution modal for questions, @@ -1376,12 +1462,11 @@ describe('Contributions and review component', () => { spyOn(component, 'openQuestionSuggestionModal'); component.onClickViewSuggestion('suggestion_2'); - expect(component.openQuestionSuggestionModal). - toHaveBeenCalledWith( - 'suggestion_2', - suggestion2, - false - ); + expect(component.openQuestionSuggestionModal).toHaveBeenCalledWith( + 'suggestion_2', + suggestion2, + false + ); }); })); @@ -1393,20 +1478,19 @@ describe('Contributions and review component', () => { }); }); - it('should return empty list if suggestion type is not initialized', - () => { - component.activeTabType = null; - component.loadContributions(null) - .then(({opportunitiesDicts, more}) => { - expect(opportunitiesDicts).toEqual([]); - expect(more).toEqual(false); - }); + it('should return empty list if suggestion type is not initialized', () => { + component.activeTabType = null; + component.loadContributions(null).then(({opportunitiesDicts, more}) => { + expect(opportunitiesDicts).toEqual([]); + expect(more).toEqual(false); }); + }); }); it('should load reviewable translation opportunities correctly', () => { - component.loadReviewableTranslationOpportunities().then( - ({opportunitiesDicts, more}) => { + component + .loadReviewableTranslationOpportunities() + .then(({opportunitiesDicts, more}) => { expect(opportunitiesDicts).toEqual([ { id: '1', @@ -1414,7 +1498,7 @@ describe('Contributions and review component', () => { subheading: 'Topic 1 - Story 1', actionButtonTitle: 'Translations', isPinned: false, - topicName: 'Topic 1' + topicName: 'Topic 1', } as unknown as Opportunity, { id: '2', @@ -1422,8 +1506,8 @@ describe('Contributions and review component', () => { subheading: 'Topic 2 - Story 2', actionButtonTitle: 'Translations', isPinned: false, - topicName: 'Topic 2' - } as unknown as Opportunity + topicName: 'Topic 2', + } as unknown as Opportunity, ]); expect(more).toEqual(false); }); @@ -1436,103 +1520,112 @@ describe('Contributions and review component', () => { topic_name: 'Topic 1', exploration_id: '1', }; - component.opportunities = [{ - id: '1', - heading: 'heading', - subheading: 'subheading', - actionButtonTitle: 'Translations', - isPinned: true, - topicName: 'Topic 1' - }, - { - id: '2', - heading: 'heading', - subheading: 'subheading', - actionButtonTitle: 'Translations', - isPinned: false, - topicName: 'Topic 1' - }, - { - id: '3', - heading: 'heading', - subheading: 'subheading', - actionButtonTitle: 'Translations', - isPinned: false, - topicName: 'Topic 1' - }]; + component.opportunities = [ + { + id: '1', + heading: 'heading', + subheading: 'subheading', + actionButtonTitle: 'Translations', + isPinned: true, + topicName: 'Topic 1', + }, + { + id: '2', + heading: 'heading', + subheading: 'subheading', + actionButtonTitle: 'Translations', + isPinned: false, + topicName: 'Topic 1', + }, + { + id: '3', + heading: 'heading', + subheading: 'subheading', + actionButtonTitle: 'Translations', + isPinned: false, + topicName: 'Topic 1', + }, + ]; component.languageCode = 'en'; component.pinReviewableTranslationOpportunity(dict); expect(openSnackbarSpy).toHaveBeenCalledWith( - 'Topic 1', '1', + 'Topic 1', + '1', 'A pinned opportunity already exists for this topic and language.', - 'Pin Anyway'); + 'Pin Anyway' + ); }); - it('should call pinReviewableTranslationOpportunityAsync if no pinned' + - ' opportunity exists', fakeAsync(() => { - const pinReviewableTranslationOpportunityAsyncSpy = spyOn( - contributionOpportunitiesService, - 'pinReviewableTranslationOpportunityAsync') - .and.returnValue(Promise.resolve({})); - - const dict = { - topic_name: 'Topic 3', - exploration_id: '8', - }; - component.opportunities = [{ - id: '1', - heading: 'heading', - subheading: 'subheading', - actionButtonTitle: 'Translations', - isPinned: true, - topicName: 'Topic 1' - }, - { - id: '2', - heading: 'heading', - subheading: 'subheading', - actionButtonTitle: 'Translations', - isPinned: false, - topicName: 'Topic 1' - }, - { - id: '3', - heading: 'heading', - subheading: 'subheading', - actionButtonTitle: 'Translations', - isPinned: false, - topicName: 'Topic 1' - }]; - component.languageCode = 'en'; - - component.pinReviewableTranslationOpportunity(dict); - tick(); - - expect(pinReviewableTranslationOpportunityAsyncSpy) - .toHaveBeenCalledWith('Topic 3', component.languageCode, '8'); - })); - - it('should call unpinReviewableTranslationOpportunityAsync', + it( + 'should call pinReviewableTranslationOpportunityAsync if no pinned' + + ' opportunity exists', fakeAsync(() => { - const unpinReviewableTranslationOpportunityAsyncSpy = spyOn( + const pinReviewableTranslationOpportunityAsyncSpy = spyOn( contributionOpportunitiesService, - 'unpinReviewableTranslationOpportunityAsync') - .and.returnValue(Promise.resolve({})); + 'pinReviewableTranslationOpportunityAsync' + ).and.returnValue(Promise.resolve({})); + const dict = { + topic_name: 'Topic 3', + exploration_id: '8', + }; + component.opportunities = [ + { + id: '1', + heading: 'heading', + subheading: 'subheading', + actionButtonTitle: 'Translations', + isPinned: true, + topicName: 'Topic 1', + }, + { + id: '2', + heading: 'heading', + subheading: 'subheading', + actionButtonTitle: 'Translations', + isPinned: false, + topicName: 'Topic 1', + }, + { + id: '3', + heading: 'heading', + subheading: 'subheading', + actionButtonTitle: 'Translations', + isPinned: false, + topicName: 'Topic 1', + }, + ]; component.languageCode = 'en'; - component.unpinReviewableTranslationOpportunity({ - topic_name: 'Dummy Topic 1', - exploration_id: '1' - }); + component.pinReviewableTranslationOpportunity(dict); tick(); expect( - unpinReviewableTranslationOpportunityAsyncSpy).toHaveBeenCalledWith( - 'Dummy Topic 1', component.languageCode, '1'); - })); + pinReviewableTranslationOpportunityAsyncSpy + ).toHaveBeenCalledWith('Topic 3', component.languageCode, '8'); + }) + ); + + it('should call unpinReviewableTranslationOpportunityAsync', fakeAsync(() => { + const unpinReviewableTranslationOpportunityAsyncSpy = spyOn( + contributionOpportunitiesService, + 'unpinReviewableTranslationOpportunityAsync' + ).and.returnValue(Promise.resolve({})); + + component.languageCode = 'en'; + + component.unpinReviewableTranslationOpportunity({ + topic_name: 'Dummy Topic 1', + exploration_id: '1', + }); + tick(); + + expect( + unpinReviewableTranslationOpportunityAsyncSpy + ).toHaveBeenCalledWith('Dummy Topic 1', component.languageCode, '1'); + })); it('should open snackbar and handle action', fakeAsync(() => { spyOn(snackBar, 'open').and.callFake((message, actionText, config) => { @@ -1540,19 +1633,20 @@ describe('Contributions and review component', () => { data.onAction = of(null); return { onAction: () => data.onAction, - dismiss: () => {} + dismiss: () => {}, }; }); spyOn( contributionOpportunitiesService, - 'pinReviewableTranslationOpportunityAsync').and.returnValue( - Promise.resolve()); + 'pinReviewableTranslationOpportunityAsync' + ).and.returnValue(Promise.resolve()); component.openSnackbarWithAction( 'testTopic', 'testExploration', 'Test message', - 'Action text'); + 'Action text' + ); tick(); fixture.detectChanges(); @@ -1562,16 +1656,19 @@ describe('Contributions and review component', () => { // TODO(#9749): Rename and actually assert on something. This test currently // only exists to satisfy code coverage. it('should cover other code too', fakeAsync(() => { - jasmine.createSpy('userReviewableSuggestionTypes.length') + jasmine + .createSpy('userReviewableSuggestionTypes.length') .and.returnValue(0); component.SUGGESTION_TYPE_TRANSLATE = null; component.SUGGESTION_TYPE_QUESTION = null; - getUserContributionRightsDataAsyncSpy.and.returnValue(Promise.resolve({ - can_review_translation_for_language_codes: ['something', 'cool'], - can_review_questions: false, - can_review_voiceover_for_language_codes: ['something', 'cool'], - can_suggest_questions: true, - })); + getUserContributionRightsDataAsyncSpy.and.returnValue( + Promise.resolve({ + can_review_translation_for_language_codes: ['something', 'cool'], + can_review_questions: false, + can_review_voiceover_for_language_codes: ['something', 'cool'], + can_suggest_questions: true, + }) + ); tick(); component.ngOnInit(); @@ -1583,16 +1680,19 @@ describe('Contributions and review component', () => { // TODO(#9749): Rename and actually assert on something. This test currently // only exists to satisfy code coverage. it('should cover other code too', fakeAsync(() => { - jasmine.createSpy('userReviewableSuggestionTypes.length') + jasmine + .createSpy('userReviewableSuggestionTypes.length') .and.returnValue(0); component.SUGGESTION_TYPE_TRANSLATE = null; component.SUGGESTION_TYPE_QUESTION = null; - getUserContributionRightsDataAsyncSpy.and.returnValue(Promise.resolve({ - can_review_translation_for_language_codes: [], - can_review_questions: false, - can_review_voiceover_for_language_codes: ['something', 'cool'], - can_suggest_questions: true, - })); + getUserContributionRightsDataAsyncSpy.and.returnValue( + Promise.resolve({ + can_review_translation_for_language_codes: [], + can_review_questions: false, + can_review_voiceover_for_language_codes: ['something', 'cool'], + can_suggest_questions: true, + }) + ); tick(); component.ngOnInit(); @@ -1605,81 +1705,89 @@ describe('Contributions and review component', () => { // to satisfy code coverage for ngOnInit() and // tabNameToOpportunityFetchFunction. it('should completely test onInIt', fakeAsync(() => { - jasmine.createSpy('userReviewableSuggestionTypes.length') + jasmine + .createSpy('userReviewableSuggestionTypes.length') .and.returnValue(0); component.SUGGESTION_TYPE_TRANSLATE = null; component.SUGGESTION_TYPE_QUESTION = null; - getUserContributionRightsDataAsyncSpy.and.returnValue(Promise.resolve({ - can_review_translation_for_language_codes: [], - can_review_questions: false, - can_review_voiceover_for_language_codes: ['something', 'cool'], - can_suggest_questions: false, - })); + getUserContributionRightsDataAsyncSpy.and.returnValue( + Promise.resolve({ + can_review_translation_for_language_codes: [], + can_review_questions: false, + can_review_voiceover_for_language_codes: ['something', 'cool'], + can_suggest_questions: false, + }) + ); tick(); component.ngOnInit(); tick(); - component - .tabNameToOpportunityFetchFunction[ - component.SUGGESTION_TYPE_QUESTION][ - component.TAB_TYPE_CONTRIBUTIONS](); + component.tabNameToOpportunityFetchFunction[ + component.SUGGESTION_TYPE_QUESTION + ][component.TAB_TYPE_CONTRIBUTIONS](); - component - .tabNameToOpportunityFetchFunction[ - component.SUGGESTION_TYPE_TRANSLATE][ - component.TAB_TYPE_REVIEWS](); + component.tabNameToOpportunityFetchFunction[ + component.SUGGESTION_TYPE_TRANSLATE + ][component.TAB_TYPE_REVIEWS](); expect( - contributionAndReviewService.getUserCreatedQuestionSuggestionsAsync) - .toHaveBeenCalled(); + contributionAndReviewService.getUserCreatedQuestionSuggestionsAsync + ).toHaveBeenCalled(); expect( - contributionAndReviewService.getReviewableTranslationSuggestionsAsync) - .toHaveBeenCalled(); + contributionAndReviewService.getReviewableTranslationSuggestionsAsync + ).toHaveBeenCalled(); })); it('should load opportunities correctly', () => { component.loadOpportunities().then(({opportunitiesDicts, more}) => { expect(Object.keys(component.contributions)).toContain('suggestion_1'); - expect(opportunitiesDicts).toEqual([{ - id: 'suggestion_1', - heading: 'Question 1', - subheading: 'Skill description', - labelText: 'Awaiting review', - labelColor: '#eeeeee', - actionButtonTitle: 'Review' - }]); + expect(opportunitiesDicts).toEqual([ + { + id: 'suggestion_1', + heading: 'Question 1', + subheading: 'Skill description', + labelText: 'Awaiting review', + labelColor: '#eeeeee', + actionButtonTitle: 'Review', + }, + ]); expect(more).toEqual(false); }); // Repeated calls should return the same results. component.loadOpportunities().then(({opportunitiesDicts, more}) => { expect(Object.keys(component.contributions)).toContain('suggestion_1'); - expect(opportunitiesDicts).toEqual([{ - id: 'suggestion_1', - heading: 'Question 1', - subheading: 'Skill description', - labelText: 'Awaiting review', - labelColor: '#eeeeee', - actionButtonTitle: 'Review' - }]); + expect(opportunitiesDicts).toEqual([ + { + id: 'suggestion_1', + heading: 'Question 1', + subheading: 'Skill description', + labelText: 'Awaiting review', + labelColor: '#eeeeee', + actionButtonTitle: 'Review', + }, + ]); expect(more).toEqual(false); }); }); it('should load more opportunities correctly', () => { - spyOn(translationTopicService, 'getActiveTopicName') - .and.returnValue('activeTopicName'); + spyOn(translationTopicService, 'getActiveTopicName').and.returnValue( + 'activeTopicName' + ); component.loadMoreOpportunities().then(({opportunitiesDicts, more}) => { expect(Object.keys(component.contributions)).toContain('suggestion_1'); - expect(opportunitiesDicts).toEqual([{ - id: 'suggestion_1', - heading: 'Question 1', - subheading: 'Skill description', - labelText: 'Awaiting review', - labelColor: '#eeeeee', - actionButtonTitle: 'Review' - }]); + expect(opportunitiesDicts).toEqual([ + { + id: 'suggestion_1', + heading: 'Question 1', + subheading: 'Skill description', + labelText: 'Awaiting review', + labelColor: '#eeeeee', + actionButtonTitle: 'Review', + }, + ]); expect(more).toEqual(false); }); expect(getReviewableQuestionSuggestionsAsyncSpy).toHaveBeenCalledWith( @@ -1688,8 +1796,9 @@ describe('Contributions and review component', () => { 'activeTopicName' ); - getReviewableQuestionSuggestionsAsyncSpy - .and.returnValue(Promise.resolve({})); + getReviewableQuestionSuggestionsAsyncSpy.and.returnValue( + Promise.resolve({}) + ); // Subsequent calls should return the next batch of results. component.loadMoreOpportunities().then(({opportunitiesDicts, more}) => { @@ -1712,9 +1821,9 @@ describe('Contributions and review component', () => { question_dict: { question_state_data: { content: { - html: 'html' - } - } + html: 'html', + }, + }, }, skill_difficulty: null, }, @@ -1722,10 +1831,10 @@ describe('Contributions and review component', () => { suggestion_id: 'suggestion_id', author_name: 'string;', status: 'review', - suggestion_type: 'string' + suggestion_type: 'string', } as Suggestion, details: null, - } + }, }; spyOn(formatRtePreviewPipe, 'transform').and.returnValue('heading'); @@ -1733,28 +1842,36 @@ describe('Contributions and review component', () => { component.getTranslationContributionsSummary(suggestion); }); - it('should open show translation suggestion modal when clicking on' + - ' suggestion', () => { - contributionOpportunitiesService - .reloadOpportunitiesEventEmitter.subscribe(() => { - component.loadContributions(null).then(() => { - spyOn(ngbModal, 'open').and.callThrough(); - component.onClickViewSuggestion('suggestion_1'); + it( + 'should open show translation suggestion modal when clicking on' + + ' suggestion', + () => { + contributionOpportunitiesService.reloadOpportunitiesEventEmitter.subscribe( + () => { + component.loadContributions(null).then(() => { + spyOn(ngbModal, 'open').and.callThrough(); + component.onClickViewSuggestion('suggestion_1'); - expect(ngbModal.open).toHaveBeenCalled(); - }); - }); + expect(ngbModal.open).toHaveBeenCalled(); + }); + } + ); - component.switchToTab( - component.TAB_TYPE_CONTRIBUTIONS, 'translate_content'); - }); + component.switchToTab( + component.TAB_TYPE_CONTRIBUTIONS, + 'translate_content' + ); + } + ); describe('when navigating to review tab', () => { it('should get in-review translation suggestions', fakeAsync(() => { - spyOn(formatRtePreviewPipe, 'transform') - .and.returnValue('Traducáú &'); - spyOn(htmlEscaperService, 'escapedStrToUnescapedStr') - .and.returnValue('Traducáú &'); + spyOn(formatRtePreviewPipe, 'transform').and.returnValue( + 'Traducáú &' + ); + spyOn(htmlEscaperService, 'escapedStrToUnescapedStr').and.returnValue( + 'Traducáú &' + ); let suggestionIdToSuggestions = { suggestion: { suggestion: { @@ -1765,16 +1882,16 @@ describe('Contributions and review component', () => { status: 'review', change_cmd: { content_html: ['

This is test para

'], - translation_html: '

Traducáú &

' - } + translation_html: '

Traducáú &

', + }, } as Suggestion, details: { skill_description: 'skill_description', topic_name: 'topic_name', story_title: 'story_title', - chapter_title: 'chapter_title' - } as ContributionDetails - } + chapter_title: 'chapter_title', + } as ContributionDetails, + }, } as Record; component.activeTabType = component.TAB_TYPE_REVIEWS; @@ -1782,128 +1899,150 @@ describe('Contributions and review component', () => { component.activeExplorationId = 'id'; tick(); - expect(component.getTranslationContributionsSummary( - suggestionIdToSuggestions)).toEqual([{ - id: 'id', - heading: 'Traducáú &', - subheading: 'topic_name / story_title / chapter_title', - labelText: 'Awaiting review', - labelColor: '#eeeeee', - actionButtonTitle: 'Review', - translationWordCount: 4 - }]); + expect( + component.getTranslationContributionsSummary( + suggestionIdToSuggestions + ) + ).toEqual([ + { + id: 'id', + heading: 'Traducáú &', + subheading: 'topic_name / story_title / chapter_title', + labelText: 'Awaiting review', + labelColor: '#eeeeee', + actionButtonTitle: 'Review', + translationWordCount: 4, + }, + ]); })); - it('should get in-review translation suggestions with' + - 'correct translation word count', fakeAsync(() => { - spyOn(formatRtePreviewPipe, 'transform') - .and.returnValue('Traducáú &'); - spyOn(htmlEscaperService, 'escapedStrToUnescapedStr') - .and.returnValue('Traducáú &'); - let suggestionIdToSuggestions = { - suggestion: { + it( + 'should get in-review translation suggestions with' + + 'correct translation word count', + fakeAsync(() => { + spyOn(formatRtePreviewPipe, 'transform').and.returnValue( + 'Traducáú &' + ); + spyOn(htmlEscaperService, 'escapedStrToUnescapedStr').and.returnValue( + 'Traducáú &' + ); + let suggestionIdToSuggestions = { suggestion: { - author_name: 'a', - target_id: '1', - suggestion_id: 'id', - suggestion_type: 'translate_content', - status: 'review', - change_cmd: { - content_html: '

This is test para

', - translation_html: '

Traducáú &

' - } - } as Suggestion, - details: { - skill_description: 'skill_description', - topic_name: 'topic_name', - story_title: 'story_title', - chapter_title: 'chapter_title' - } as ContributionDetails - } - } as Record; + suggestion: { + author_name: 'a', + target_id: '1', + suggestion_id: 'id', + suggestion_type: 'translate_content', + status: 'review', + change_cmd: { + content_html: '

This is test para

', + translation_html: '

Traducáú &

', + }, + } as Suggestion, + details: { + skill_description: 'skill_description', + topic_name: 'topic_name', + story_title: 'story_title', + chapter_title: 'chapter_title', + } as ContributionDetails, + }, + } as Record; - component.activeTabType = component.TAB_TYPE_REVIEWS; - component.activeTabSubtype = component.SUGGESTION_TYPE_TRANSLATE; - component.activeExplorationId = 'id'; - tick(); + component.activeTabType = component.TAB_TYPE_REVIEWS; + component.activeTabSubtype = component.SUGGESTION_TYPE_TRANSLATE; + component.activeExplorationId = 'id'; + tick(); - expect(component.getTranslationContributionsSummary( - suggestionIdToSuggestions)).toEqual([{ - id: 'id', - heading: 'Traducáú &', - subheading: 'topic_name / story_title / chapter_title', - labelText: 'Awaiting review', - labelColor: '#eeeeee', - actionButtonTitle: 'Review', - translationWordCount: 4 - }]); - - suggestionIdToSuggestions = { - suggestion: { + expect( + component.getTranslationContributionsSummary( + suggestionIdToSuggestions + ) + ).toEqual([ + { + id: 'id', + heading: 'Traducáú &', + subheading: 'topic_name / story_title / chapter_title', + labelText: 'Awaiting review', + labelColor: '#eeeeee', + actionButtonTitle: 'Review', + translationWordCount: 4, + }, + ]); + + suggestionIdToSuggestions = { suggestion: { - author_name: 'a', - target_id: '1', - suggestion_id: 'id', - suggestion_type: 'translate_content', - status: 'review', - change_cmd: { - content_html: [ - '

This is test para

', - '

This is test para 2

', - '

Test para 3

' - ], - translation_html: '

Traducáú &

' - } - } as Suggestion, - details: { - skill_description: 'skill_description', - topic_name: 'topic_name', - story_title: 'story_title', - chapter_title: 'chapter_title' - } as ContributionDetails - } - } as Record; + suggestion: { + author_name: 'a', + target_id: '1', + suggestion_id: 'id', + suggestion_type: 'translate_content', + status: 'review', + change_cmd: { + content_html: [ + '

This is test para

', + '

This is test para 2

', + '

Test para 3

', + ], + translation_html: '

Traducáú &

', + }, + } as Suggestion, + details: { + skill_description: 'skill_description', + topic_name: 'topic_name', + story_title: 'story_title', + chapter_title: 'chapter_title', + } as ContributionDetails, + }, + } as Record; - expect(component.getTranslationContributionsSummary( - suggestionIdToSuggestions)).toEqual([{ - id: 'id', - heading: 'Traducáú &', - subheading: 'topic_name / story_title / chapter_title', - labelText: 'Awaiting review', - labelColor: '#eeeeee', - actionButtonTitle: 'Review', - translationWordCount: 12 - }]); - - suggestionIdToSuggestions = { - suggestion: { + expect( + component.getTranslationContributionsSummary( + suggestionIdToSuggestions + ) + ).toEqual([ + { + id: 'id', + heading: 'Traducáú &', + subheading: 'topic_name / story_title / chapter_title', + labelText: 'Awaiting review', + labelColor: '#eeeeee', + actionButtonTitle: 'Review', + translationWordCount: 12, + }, + ]); + + suggestionIdToSuggestions = { suggestion: { - author_name: 'a', - target_id: '1', - suggestion_id: 'id', - suggestion_type: 'translate_content', - status: 'review', - change_cmd: { - content_html: 1 as unknown, - translation_html: '

Traducáú &

' - } - } as Suggestion, - details: { - skill_description: 'skill_description', - topic_name: 'topic_name', - story_title: 'story_title', - chapter_title: 'chapter_title' - } as ContributionDetails - } - } as Record; + suggestion: { + author_name: 'a', + target_id: '1', + suggestion_id: 'id', + suggestion_type: 'translate_content', + status: 'review', + change_cmd: { + content_html: 1 as unknown, + translation_html: '

Traducáú &

', + }, + } as Suggestion, + details: { + skill_description: 'skill_description', + topic_name: 'topic_name', + story_title: 'story_title', + chapter_title: 'chapter_title', + } as ContributionDetails, + }, + } as Record; - expect(() => { - component.getTranslationContributionsSummary( - suggestionIdToSuggestions); - }).toThrowError( - 'Invalid input: contentHtml must be a string or an array of ' + - 'strings.'); - })); + expect(() => { + component.getTranslationContributionsSummary( + suggestionIdToSuggestions + ); + }).toThrowError( + 'Invalid input: contentHtml must be a string or an array of ' + + 'strings.' + ); + }) + ); it('should get in-review question suggestions', fakeAsync(() => { spyOn(formatRtePreviewPipe, 'transform').and.returnValue('heading'); @@ -1918,190 +2057,219 @@ describe('Contributions and review component', () => { question_dict: { question_state_data: { content: { - html: 'html' - } - } - } - } + html: 'html', + }, + }, + }, + }, } as Suggestion, details: { skill_description: 'skill_description', topic_name: 'topic_name', story_title: 'story_title', - chapter_title: 'chapter_title' - } as ContributionDetails - } + chapter_title: 'chapter_title', + } as ContributionDetails, + }, }; component.activeTabType = component.TAB_TYPE_REVIEWS; tick(); - expect(component.getQuestionContributionsSummary( - suggestionIdToSuggestions as Record) - ).toEqual([{ - id: 'id', - heading: 'heading', - subheading: 'skill_description', - labelText: 'Awaiting review', - labelColor: '#eeeeee', - actionButtonTitle: 'Review' - }]); + expect( + component.getQuestionContributionsSummary( + suggestionIdToSuggestions as Record + ) + ).toEqual([ + { + id: 'id', + heading: 'heading', + subheading: 'skill_description', + labelText: 'Awaiting review', + labelColor: '#eeeeee', + actionButtonTitle: 'Review', + }, + ]); })); }); - it('should remove resolved suggestions when suggestion ' + - 'modal is opened and remove button is clicked', fakeAsync(() => { - spyOn(ngbModal, 'open').and.returnValue( - { + it( + 'should remove resolved suggestions when suggestion ' + + 'modal is opened and remove button is clicked', + fakeAsync(() => { + spyOn(ngbModal, 'open').and.returnValue({ componentInstance: MockNgbModalRef, - result: Promise.resolve(['id1', 'id2']) - } as NgbModalRef - ); - const removeSpy = spyOn( - contributionOpportunitiesService.removeOpportunitiesEventEmitter, - 'emit').and.returnValue(null); - component.contributions = { - suggestion_1: { - suggestion: { - suggestion_id: 'suggestion_1', - target_id: '1', - suggestion_type: 'translate_content', - change_cmd: { - content_html: 'Translation', - translation_html: 'Tradução' + result: Promise.resolve(['id1', 'id2']), + } as NgbModalRef); + const removeSpy = spyOn( + contributionOpportunitiesService.removeOpportunitiesEventEmitter, + 'emit' + ).and.returnValue(null); + component.contributions = { + suggestion_1: { + suggestion: { + suggestion_id: 'suggestion_1', + target_id: '1', + suggestion_type: 'translate_content', + change_cmd: { + content_html: 'Translation', + translation_html: 'Tradução', + }, + status: 'review', + }, + details: { + skill_description: 'skill_description', + skill_rubrics: [], + chapter_title: 'skill_1', + story_title: 'skill_1', + topic_name: 'skill_1', }, - status: 'review' }, - details: { - skill_description: 'skill_description', - skill_rubrics: [], - chapter_title: 'skill_1', - story_title: 'skill_1', - topic_name: 'skill_1', - } - } - }; - - component.onClickViewSuggestion('suggestion_1'); - tick(); - tick(); - - expect(removeSpy).toHaveBeenCalled(); - })); + }; - it('should resolve suggestion when closing show suggestion modal', - () => { - contributionOpportunitiesService - .reloadOpportunitiesEventEmitter.subscribe(() => { - component.loadContributions(null).then(() => { - spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.resolve({ - action: 'add', - reviewMessage: 'Review message', - skillDifficulty: 'Easy' - }) - } as NgbModalRef); - component.onClickViewSuggestion('suggestion_1'); + component.onClickViewSuggestion('suggestion_1'); + tick(); + tick(); - expect(ngbModal.open).toHaveBeenCalled(); - }); - }); - component.switchToTab( - component.TAB_TYPE_CONTRIBUTIONS, 'translate_content'); - }); + expect(removeSpy).toHaveBeenCalled(); + }) + ); - it('should not resolve suggestion when dismissing show suggestion modal', - () => { - contributionOpportunitiesService - .reloadOpportunitiesEventEmitter.subscribe(() => { - component.loadContributions(null).then(() => { - spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.reject() - } as NgbModalRef); - component.onClickViewSuggestion('suggestion_1'); + it('should resolve suggestion when closing show suggestion modal', () => { + contributionOpportunitiesService.reloadOpportunitiesEventEmitter.subscribe( + () => { + component.loadContributions(null).then(() => { + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.resolve({ + action: 'add', + reviewMessage: 'Review message', + skillDifficulty: 'Easy', + }), + } as NgbModalRef); + component.onClickViewSuggestion('suggestion_1'); - expect(ngbModal.open).toHaveBeenCalled(); - }); + expect(ngbModal.open).toHaveBeenCalled(); }); - component.switchToTab( - component.TAB_TYPE_CONTRIBUTIONS, 'translate_content'); - }); - }); + } + ); + component.switchToTab( + component.TAB_TYPE_CONTRIBUTIONS, + 'translate_content' + ); + }); - describe('when user is allowed to review questions and ' + - 'skill details are empty', () => { - it('should open suggestion modal when user clicks on ' + - 'view suggestion', () => { - contributionOpportunitiesService - .reloadOpportunitiesEventEmitter.subscribe(() => { + it('should not resolve suggestion when dismissing show suggestion modal', () => { + contributionOpportunitiesService.reloadOpportunitiesEventEmitter.subscribe( + () => { component.loadContributions(null).then(() => { spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.reject() + result: Promise.reject(), } as NgbModalRef); component.onClickViewSuggestion('suggestion_1'); expect(ngbModal.open).toHaveBeenCalled(); }); - }); + } + ); component.switchToTab( - component.TAB_TYPE_CONTRIBUTIONS, 'translate_content'); + component.TAB_TYPE_CONTRIBUTIONS, + 'translate_content' + ); }); }); + describe( + 'when user is allowed to review questions and ' + 'skill details are empty', + () => { + it( + 'should open suggestion modal when user clicks on ' + 'view suggestion', + () => { + contributionOpportunitiesService.reloadOpportunitiesEventEmitter.subscribe( + () => { + component.loadContributions(null).then(() => { + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.reject(), + } as NgbModalRef); + component.onClickViewSuggestion('suggestion_1'); + + expect(ngbModal.open).toHaveBeenCalled(); + }); + } + ); + component.switchToTab( + component.TAB_TYPE_CONTRIBUTIONS, + 'translate_content' + ); + } + ); + } + ); + // TODO(#9749): Refactor describe block, since the user *is* allowed to // review questions here. describe('when user is not allowed to review questions', () => { - it('should initialize $scope properties after controller is' + - ' initialized', () => { - expect(component.activeTabType).toBe('reviews'); - expect(component.activeTabSubtype).toBe('add_question'); - expect(component.activeDropdownTabChoice).toBe('Review Questions'); - expect(component.userIsLoggedIn).toBe(true); - expect(component.userDetailsLoading).toBe(false); - expect(component.reviewTabs.length).toEqual(2); - }); - - it('should open show view question modal when clicking on' + - ' question suggestion', () => { - spyOn(ngbModal, 'open').and.callThrough(); - component.switchToTab(component.TAB_TYPE_REVIEWS, 'add_question'); - component.loadContributions(null).then(() => { - component.onClickViewSuggestion('suggestion_1'); + it( + 'should initialize $scope properties after controller is' + + ' initialized', + () => { + expect(component.activeTabType).toBe('reviews'); + expect(component.activeTabSubtype).toBe('add_question'); + expect(component.activeDropdownTabChoice).toBe('Review Questions'); + expect(component.userIsLoggedIn).toBe(true); + expect(component.userDetailsLoading).toBe(false); + expect(component.reviewTabs.length).toEqual(2); + } + ); + + it( + 'should open show view question modal when clicking on' + + ' question suggestion', + () => { + spyOn(ngbModal, 'open').and.callThrough(); + component.switchToTab(component.TAB_TYPE_REVIEWS, 'add_question'); + component.loadContributions(null).then(() => { + component.onClickViewSuggestion('suggestion_1'); - expect(ngbModal.open).toHaveBeenCalled(); - }); - }); + expect(ngbModal.open).toHaveBeenCalled(); + }); + } + ); - it('should resolve suggestion to skill when closing show question' + - ' suggestion modal', () => { - spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.resolve({}) - } as NgbModalRef); + it( + 'should resolve suggestion to skill when closing show question' + + ' suggestion modal', + () => { + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.resolve({}), + } as NgbModalRef); - component.switchToTab(component.TAB_TYPE_REVIEWS, 'add_question'); - component.loadContributions(null).then(() => { - expect(Object.keys(component.contributions).length).toBe(1); - component.onClickViewSuggestion('suggestion_1'); - flush(); + component.switchToTab(component.TAB_TYPE_REVIEWS, 'add_question'); + component.loadContributions(null).then(() => { + expect(Object.keys(component.contributions).length).toBe(1); + component.onClickViewSuggestion('suggestion_1'); + flush(); - expect(ngbModal.open).toHaveBeenCalled(); - }); - }); + expect(ngbModal.open).toHaveBeenCalled(); + }); + } + ); - it('should not resolve suggestion to skill when dismissing show question' + - ' suggestion modal', () => { - component.switchToTab(component.TAB_TYPE_REVIEWS, 'add_question'); - spyOn(contributionAndReviewService, 'reviewSkillSuggestion'); - spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.reject({}) - } as NgbModalRef); + it( + 'should not resolve suggestion to skill when dismissing show question' + + ' suggestion modal', + () => { + component.switchToTab(component.TAB_TYPE_REVIEWS, 'add_question'); + spyOn(contributionAndReviewService, 'reviewSkillSuggestion'); + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.reject({}), + } as NgbModalRef); - component.loadContributions(null).then(() => { - component.onClickViewSuggestion('suggestion_1'); + component.loadContributions(null).then(() => { + component.onClickViewSuggestion('suggestion_1'); - expect(ngbModal.open).toHaveBeenCalled(); - }); - }); + expect(ngbModal.open).toHaveBeenCalled(); + }); + } + ); it('should return correctly check the active tab', () => { component.contributionTabs = [ @@ -2109,28 +2277,28 @@ describe('Contributions and review component', () => { tabType: 'contributions', tabSubType: 'translate_content', text: 'Questions', - enabled: false + enabled: false, }, { tabType: 'contributions', tabSubType: 'add_question', text: 'Translations', - enabled: true - } + enabled: true, + }, ]; component.reviewTabs = [ { tabType: 'reviews', tabSubType: 'add_question', text: 'Review Questions', - enabled: false + enabled: false, }, { tabType: 'reviews', tabSubType: 'translate_content', text: 'Review Translations', - enabled: false - } + enabled: false, + }, ]; component.switchToTab('reviews', 'translate_content'); @@ -2156,71 +2324,64 @@ describe('Contributions and review component', () => { tabType: 'contributions', tabSubType: 'translate_content', text: 'Translations', - enabled: false + enabled: false, }, { tabType: 'contributions', tabSubType: 'add_question', text: 'Questions', - enabled: true - } + enabled: true, + }, ]; component.accomplishmentsTabs = [ { tabSubType: 'stats', tabType: 'accomplishments', text: 'Contribution Stats', - enabled: true + enabled: true, }, { tabSubType: 'badges', tabType: 'accomplishments', text: 'Badges', - enabled: true - } + enabled: true, + }, ]; component.reviewTabs = [ { tabType: 'reviews', tabSubType: 'add_question', text: 'Review Questions', - enabled: false + enabled: false, }, { tabType: 'reviews', tabSubType: 'translate_content', text: 'Review Translations', - enabled: false - } + enabled: false, + }, ]; expect( - component.getActiveDropdownTabText( - 'reviews', - 'add_question')).toBe('Review Questions'); + component.getActiveDropdownTabText('reviews', 'add_question') + ).toBe('Review Questions'); expect( - component.getActiveDropdownTabText( - 'reviews', - 'translate_content')) - .toBe('Review Translations'); + component.getActiveDropdownTabText('reviews', 'translate_content') + ).toBe('Review Translations'); expect( - component.getActiveDropdownTabText( - 'contributions', - 'add_question')).toBe('Questions'); + component.getActiveDropdownTabText('contributions', 'add_question') + ).toBe('Questions'); expect( - component.getActiveDropdownTabText( - 'contributions', - 'translate_content')).toBe('Translations'); + component.getActiveDropdownTabText('contributions', 'translate_content') + ).toBe('Translations'); expect( - component.getActiveDropdownTabText( - 'accomplishments', - 'stats')).toBe('Contribution Stats'); + component.getActiveDropdownTabText('accomplishments', 'stats') + ).toBe('Contribution Stats'); expect( - component.getActiveDropdownTabText( - 'accomplishments', - 'badges')).toBe('Badges'); + component.getActiveDropdownTabText('accomplishments', 'badges') + ).toBe('Badges'); }); it('should throw an error when invalid tab names given', () => { @@ -2229,48 +2390,46 @@ describe('Contributions and review component', () => { tabType: 'contributions', tabSubType: 'translate_content', text: 'Translations', - enabled: false + enabled: false, }, { tabType: 'contributions', tabSubType: 'add_question', text: 'Questions', - enabled: true - } + enabled: true, + }, ]; component.accomplishmentsTabs = [ { tabSubType: 'stats', tabType: 'accomplishments', text: 'Contribution Stats', - enabled: true + enabled: true, }, { tabSubType: 'badges', tabType: 'accomplishments', text: 'Badges', - enabled: true - } + enabled: true, + }, ]; component.reviewTabs = [ { tabType: 'reviews', tabSubType: 'add_question', text: 'Review Questions', - enabled: false + enabled: false, }, { tabType: 'reviews', tabSubType: 'translate_content', text: 'Review Translations', - enabled: false - } + enabled: false, + }, ]; expect(() => { - component.getActiveDropdownTabText( - 'xxx', - 'xxx'); + component.getActiveDropdownTabText('xxx', 'xxx'); tick(); }).toThrowError(); }); @@ -2279,15 +2438,17 @@ describe('Contributions and review component', () => { const element = { contains: () => { return true; - } + }, }; const clickEvent = { - target: null + target: null, }; - const querySelectorSpy = spyOn(document, 'querySelector').and - .returnValue(null); - const elementContainsSpy = spyOn(element, 'contains').and - .returnValue(true); + const querySelectorSpy = spyOn(document, 'querySelector').and.returnValue( + null + ); + const elementContainsSpy = spyOn(element, 'contains').and.returnValue( + true + ); component.dropdownShown = true; component.closeDropdownWhenClickedOutside(null); @@ -2317,7 +2478,7 @@ describe('Contributions and review component', () => { it('should return back when user click is made outside', () => { const clickEvent = { - target: null + target: null, }; spyOn(document, 'querySelector').and.returnValue(null); diff --git a/core/templates/pages/contributor-dashboard-page/contributions-and-review/contributions-and-review.component.ts b/core/templates/pages/contributor-dashboard-page/contributions-and-review/contributions-and-review.component.ts index 1a5acae63b5d..4c7e7b4d0e52 100644 --- a/core/templates/pages/contributor-dashboard-page/contributions-and-review/contributions-and-review.component.ts +++ b/core/templates/pages/contributor-dashboard-page/contributions-and-review/contributions-and-review.component.ts @@ -16,33 +16,43 @@ * @fileoverview Component for showing and reviewing contributions. */ -import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbModalRef, NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { AppConstants } from 'app.constants'; +import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbModalRef, NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {AppConstants} from 'app.constants'; import cloneDeep from 'lodash/cloneDeep'; -import { Subscription, Observable } from 'rxjs'; -import { Rubric } from 'domain/skill/rubric.model'; -import { SkillBackendApiService } from 'domain/skill/skill-backend-api.service'; -import { MisconceptionSkillMap } from 'domain/skill/MisconceptionObjectFactory'; -import { Question, QuestionBackendDict, QuestionObjectFactory } from 'domain/question/QuestionObjectFactory'; -import { ActiveContributionDict, TranslationSuggestionReviewModalComponent } from '../modal-templates/translation-suggestion-review-modal.component'; -import { ContributorDashboardConstants } from 'pages/contributor-dashboard-page/contributor-dashboard-page.constants'; -import { QuestionSuggestionReviewModalComponent } from '../modal-templates/question-suggestion-review-modal.component'; -import { TranslationLanguageService } from 'pages/exploration-editor-page/translation-tab/services/translation-language.service'; -import { TranslationTopicService } from 'pages/exploration-editor-page/translation-tab/services/translation-topic.service'; -import { FormatRtePreviewPipe } from 'filters/format-rte-preview.pipe'; -import { UserService } from 'services/user.service'; -import { AlertsService } from 'services/alerts.service'; -import { ContextService } from 'services/context.service'; -import { ContributionAndReviewService } from '../services/contribution-and-review.service'; -import { ContributionOpportunitiesService } from '../services/contribution-opportunities.service'; -import { OpportunitiesListComponent } from '../opportunities-list/opportunities-list.component'; -import { PlatformFeatureService } from 'services/platform-feature.service'; -import { CALCULATION_TYPE_WORD, HtmlLengthService } from 'services/html-length.service'; -import { HtmlEscaperService } from 'services/html-escaper.service'; -import { MatSnackBar } from '@angular/material/snack-bar'; -import { ExplorationOpportunitySummary } from 'domain/opportunity/exploration-opportunity-summary.model'; +import {Subscription, Observable} from 'rxjs'; +import {Rubric} from 'domain/skill/rubric.model'; +import {SkillBackendApiService} from 'domain/skill/skill-backend-api.service'; +import {MisconceptionSkillMap} from 'domain/skill/MisconceptionObjectFactory'; +import { + Question, + QuestionBackendDict, + QuestionObjectFactory, +} from 'domain/question/QuestionObjectFactory'; +import { + ActiveContributionDict, + TranslationSuggestionReviewModalComponent, +} from '../modal-templates/translation-suggestion-review-modal.component'; +import {ContributorDashboardConstants} from 'pages/contributor-dashboard-page/contributor-dashboard-page.constants'; +import {QuestionSuggestionReviewModalComponent} from '../modal-templates/question-suggestion-review-modal.component'; +import {TranslationLanguageService} from 'pages/exploration-editor-page/translation-tab/services/translation-language.service'; +import {TranslationTopicService} from 'pages/exploration-editor-page/translation-tab/services/translation-topic.service'; +import {FormatRtePreviewPipe} from 'filters/format-rte-preview.pipe'; +import {UserService} from 'services/user.service'; +import {AlertsService} from 'services/alerts.service'; +import {ContextService} from 'services/context.service'; +import {ContributionAndReviewService} from '../services/contribution-and-review.service'; +import {ContributionOpportunitiesService} from '../services/contribution-opportunities.service'; +import {OpportunitiesListComponent} from '../opportunities-list/opportunities-list.component'; +import {PlatformFeatureService} from 'services/platform-feature.service'; +import { + CALCULATION_TYPE_WORD, + HtmlLengthService, +} from 'services/html-length.service'; +import {HtmlEscaperService} from 'services/html-escaper.service'; +import {MatSnackBar} from '@angular/material/snack-bar'; +import {ExplorationOpportunitySummary} from 'domain/opportunity/exploration-opportunity-summary.model'; export interface Suggestion { change_cmd: { @@ -112,12 +122,11 @@ export interface CustomMatSnackBarRef { @Component({ selector: 'oppia-contributions-and-review', - templateUrl: './contributions-and-review.component.html' + templateUrl: './contributions-and-review.component.html', }) -export class ContributionsAndReview - implements OnInit, OnDestroy { - @ViewChild('opportunitiesList') opportunitiesListRef!: - OpportunitiesListComponent; +export class ContributionsAndReview implements OnInit, OnDestroy { + @ViewChild('opportunitiesList') + opportunitiesListRef!: OpportunitiesListComponent; directiveSubscriptions = new Subscription(); @@ -164,16 +173,16 @@ export class ContributionsAndReview SUGGESTION_LABELS = { review: { text: 'Awaiting review', - color: '#eeeeee' + color: '#eeeeee', }, accepted: { text: 'Accepted', - color: '#8ed274' + color: '#8ed274', }, rejected: { text: 'Revisions Requested', - color: '#e76c8c' - } + color: '#e76c8c', + }, }; constructor( @@ -191,20 +200,20 @@ export class ContributionsAndReview private featureService: PlatformFeatureService, private htmlLengthService: HtmlLengthService, private htmlEscaperService: HtmlEscaperService, - private snackBar: MatSnackBar, + private snackBar: MatSnackBar ) {} getQuestionContributionsSummary( - suggestionIdToSuggestions: Record): - ContributionsSummary[] { + suggestionIdToSuggestions: Record + ): ContributionsSummary[] { const questionContributionsSummaryList = []; - Object.keys(suggestionIdToSuggestions).forEach((key) => { + Object.keys(suggestionIdToSuggestions).forEach(key => { const suggestion = suggestionIdToSuggestions[key].suggestion; const details = suggestionIdToSuggestions[key].details; let subheading = ''; if (details === null) { - subheading = ( - ContributorDashboardConstants.CORRESPONDING_DELETED_OPPORTUNITY_TEXT); + subheading = + ContributorDashboardConstants.CORRESPONDING_DELETED_OPPORTUNITY_TEXT; } else { subheading = details.skill_description; } @@ -212,12 +221,13 @@ export class ContributionsAndReview const requiredData = { id: suggestion.suggestion_id, heading: this.formatRtePreviewPipe.transform( - suggestion.change_cmd.question_dict.question_state_data.content.html), + suggestion.change_cmd.question_dict.question_state_data.content.html + ), subheading: subheading, labelText: this.SUGGESTION_LABELS[suggestion.status].text, labelColor: this.SUGGESTION_LABELS[suggestion.status].color, - actionButtonTitle: ( - this.activeTabType === this.TAB_TYPE_REVIEWS ? 'Review' : 'View') + actionButtonTitle: + this.activeTabType === this.TAB_TYPE_REVIEWS ? 'Review' : 'View', }; questionContributionsSummaryList.push(requiredData); @@ -227,40 +237,45 @@ export class ContributionsAndReview } getTranslationContributionsSummary( - suggestionIdToSuggestions: Record + suggestionIdToSuggestions: Record ): ContributionsSummary[] { const translationContributionsSummaryList = []; - Object.keys(suggestionIdToSuggestions).forEach((key) => { + Object.keys(suggestionIdToSuggestions).forEach(key => { const suggestion = suggestionIdToSuggestions[key].suggestion; const details = suggestionIdToSuggestions[key].details; let subheading = ''; if (details === null) { - subheading = ( - ContributorDashboardConstants.CORRESPONDING_DELETED_OPPORTUNITY_TEXT); + subheading = + ContributorDashboardConstants.CORRESPONDING_DELETED_OPPORTUNITY_TEXT; } else { - subheading = ( - details.topic_name + ' / ' + details.story_title + - ' / ' + details.chapter_title); + subheading = + details.topic_name + + ' / ' + + details.story_title + + ' / ' + + details.chapter_title; } const requiredData = { id: suggestion.suggestion_id, heading: this.getTranslationSuggestionHeading(suggestion), subheading: subheading, - labelText: ( + labelText: // Missing exploration content means the translation suggestion is // now obsolete. See issue #16022. - suggestion.exploration_content_html === null ? - 'Obsolete' : - this.SUGGESTION_LABELS[suggestion.status].text), + suggestion.exploration_content_html === null + ? 'Obsolete' + : this.SUGGESTION_LABELS[suggestion.status].text, labelColor: this.SUGGESTION_LABELS[suggestion.status].color, - actionButtonTitle: ( - this.activeTabType === this.TAB_TYPE_REVIEWS ? 'Review' : 'View'), - translationWordCount: ( - this.isReviewTranslationsTab() && this.activeExplorationId) ? ( - this.getTranslationContentLength( - suggestion.change_cmd.content_html)) : undefined + actionButtonTitle: + this.activeTabType === this.TAB_TYPE_REVIEWS ? 'Review' : 'View', + translationWordCount: + this.isReviewTranslationsTab() && this.activeExplorationId + ? this.getTranslationContentLength( + suggestion.change_cmd.content_html + ) + : undefined, }; translationContributionsSummaryList.push(requiredData); @@ -271,17 +286,22 @@ export class ContributionsAndReview getTranslationContentLength(contentHtml: string | string[]): number { if (typeof contentHtml === 'string') { return this.htmlLengthService.computeHtmlLength( - contentHtml, CALCULATION_TYPE_WORD); + contentHtml, + CALCULATION_TYPE_WORD + ); } else if (Array.isArray(contentHtml)) { let totalLength = 0; for (const str of contentHtml) { totalLength += this.htmlLengthService.computeHtmlLength( - str, CALCULATION_TYPE_WORD); + str, + CALCULATION_TYPE_WORD + ); } return totalLength; } else { throw new Error( - 'Invalid input: contentHtml must be a string or an array of strings.'); + 'Invalid input: contentHtml must be a string or an array of strings.' + ); } } @@ -297,113 +317,138 @@ export class ContributionsAndReview resolveSuggestionSuccess(suggestionId: string): void { this.alertsService.addSuccessMessage('Submitted suggestion review.'); - this.contributionOpportunitiesService.removeOpportunitiesEventEmitter.emit( - [suggestionId]); + this.contributionOpportunitiesService.removeOpportunitiesEventEmitter.emit([ + suggestionId, + ]); } _showQuestionSuggestionModal( - suggestion: Suggestion, - suggestionIdToContribution: Record, - reviewable: boolean, - question: Question, - misconceptionsBySkill: MisconceptionSkillMap): void { + suggestion: Suggestion, + suggestionIdToContribution: Record, + reviewable: boolean, + question: Question, + misconceptionsBySkill: MisconceptionSkillMap + ): void { const targetId = suggestion.target_id; const suggestionId = suggestion.suggestion_id; - const updatedQuestion = ( - question || this.questionObjectFactory.createFromBackendDict( - suggestion.change_cmd.question_dict)); + const updatedQuestion = + question || + this.questionObjectFactory.createFromBackendDict( + suggestion.change_cmd.question_dict + ); const modalRef = this.ngbModal.open( - QuestionSuggestionReviewModalComponent, { + QuestionSuggestionReviewModalComponent, + { backdrop: 'static', size: 'lg', - }); + } + ); modalRef.componentInstance.reviewable = reviewable; modalRef.componentInstance.question = updatedQuestion; modalRef.componentInstance.suggestionId = suggestionId; - modalRef.componentInstance.suggestionIdToContribution = ( - suggestionIdToContribution - ); - modalRef.componentInstance.misconceptionsBySkill = ( - misconceptionsBySkill); + modalRef.componentInstance.suggestionIdToContribution = + suggestionIdToContribution; + modalRef.componentInstance.misconceptionsBySkill = misconceptionsBySkill; - modalRef.componentInstance.editSuggestionEmitter.subscribe((value) => { + modalRef.componentInstance.editSuggestionEmitter.subscribe(value => { this.openQuestionSuggestionModal( value.suggestionId, value.suggestion, - value.reviewable); + value.reviewable + ); }); - modalRef.result.then((result) => { - this.contributionAndReviewService.reviewSkillSuggestion( - targetId, suggestionId, result.action, result.reviewMessage, - result.skillDifficulty, this.resolveSuggestionSuccess.bind(this), - () => { - this.alertsService.addInfoMessage('Failed to submit suggestion.'); - }); - }, () => {}); + modalRef.result.then( + result => { + this.contributionAndReviewService.reviewSkillSuggestion( + targetId, + suggestionId, + result.action, + result.reviewMessage, + result.skillDifficulty, + this.resolveSuggestionSuccess.bind(this), + () => { + this.alertsService.addInfoMessage('Failed to submit suggestion.'); + } + ); + }, + () => {} + ); } _showTranslationSuggestionModal( - suggestionIdToContribution: Record, - initialSuggestionId: string, reviewable: boolean): void { - const details = ( - this.contributions[initialSuggestionId].details as ContributionDetails); - const subheading = ( - details.topic_name + ' / ' + details.story_title + - ' / ' + details.chapter_title); + suggestionIdToContribution: Record, + initialSuggestionId: string, + reviewable: boolean + ): void { + const details = this.contributions[initialSuggestionId] + .details as ContributionDetails; + const subheading = + details.topic_name + + ' / ' + + details.story_title + + ' / ' + + details.chapter_title; const modalRef: NgbModalRef = this.ngbModal.open( - TranslationSuggestionReviewModalComponent, { + TranslationSuggestionReviewModalComponent, + { backdrop: 'static', windowClass: 'oppia-translation-suggestion-review-modal', size: 'lg', - }); + } + ); - modalRef.componentInstance.suggestionIdToContribution = ( - cloneDeep(suggestionIdToContribution)); + modalRef.componentInstance.suggestionIdToContribution = cloneDeep( + suggestionIdToContribution + ); modalRef.componentInstance.initialSuggestionId = initialSuggestionId; modalRef.componentInstance.reviewable = reviewable; modalRef.componentInstance.subheading = subheading; - modalRef.result.then((resolvedSuggestionIds) => { - this.contributionOpportunitiesService. - removeOpportunitiesEventEmitter.emit( - resolvedSuggestionIds); - resolvedSuggestionIds.forEach((suggestionId) => { - delete this.contributions[suggestionId]; - }); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + modalRef.result.then( + resolvedSuggestionIds => { + this.contributionOpportunitiesService.removeOpportunitiesEventEmitter.emit( + resolvedSuggestionIds + ); + resolvedSuggestionIds.forEach(suggestionId => { + delete this.contributions[suggestionId]; + }); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } isActiveTab(tabType: string, subType: string): boolean { - return ( - this.activeTabType === tabType && - this.activeTabSubtype === subType); + return this.activeTabType === tabType && this.activeTabSubtype === subType; } isReviewTranslationsTab(): boolean { return ( this.activeTabType === this.TAB_TYPE_REVIEWS && - this.activeTabSubtype === this.SUGGESTION_TYPE_TRANSLATE); + this.activeTabSubtype === this.SUGGESTION_TYPE_TRANSLATE + ); } isReviewQuestionsTab(): boolean { return ( this.activeTabType === this.TAB_TYPE_REVIEWS && - this.activeTabSubtype === this.SUGGESTION_TYPE_QUESTION); + this.activeTabSubtype === this.SUGGESTION_TYPE_QUESTION + ); } openQuestionSuggestionModal( - suggestionId: string, - suggestion: Suggestion, - reviewable: boolean, - question = undefined): void { + suggestionId: string, + suggestion: Suggestion, + reviewable: boolean, + question = undefined + ): void { const suggestionIdToContribution = {}; for (let suggestionId in this.contributions) { var contribution = this.contributions[suggestionId]; @@ -412,16 +457,21 @@ export class ContributionsAndReview const skillId = suggestion.change_cmd.skill_id; this.contextService.setCustomEntityContext( - AppConstants.IMAGE_CONTEXT.QUESTION_SUGGESTIONS, skillId); + AppConstants.IMAGE_CONTEXT.QUESTION_SUGGESTIONS, + skillId + ); - this.skillBackendApiService.fetchSkillAsync(skillId).then((skillDict) => { + this.skillBackendApiService.fetchSkillAsync(skillId).then(skillDict => { const misconceptionsBySkill = {}; const skill = skillDict.skill; misconceptionsBySkill[skill.getId()] = skill.getMisconceptions(); this._showQuestionSuggestionModal( - suggestion, suggestionIdToContribution, reviewable, + suggestion, + suggestionIdToContribution, + reviewable, question, - misconceptionsBySkill); + misconceptionsBySkill + ); }); } @@ -439,14 +489,18 @@ export class ContributionsAndReview } this.contextService.setCustomEntityContext( AppConstants.IMAGE_CONTEXT.EXPLORATION_SUGGESTIONS, - suggestion.target_id); + suggestion.target_id + ); this._showTranslationSuggestionModal( - suggestionIdToContribution, suggestionId, reviewable); + suggestionIdToContribution, + suggestionId, + reviewable + ); } } getContributionSummaries( - suggestionIdToSuggestions: Record + suggestionIdToSuggestions: Record ): ContributionsSummary[] { if (this.activeTabSubtype === this.SUGGESTION_TYPE_TRANSLATE) { return this.getTranslationContributionsSummary(suggestionIdToSuggestions); @@ -457,9 +511,12 @@ export class ContributionsAndReview getActiveDropdownTabText(tabType: string, subType: string): string { const tabs = this.contributionTabs.concat( - this.reviewTabs, this.accomplishmentsTabs); + this.reviewTabs, + this.accomplishmentsTabs + ); const tab = tabs.find( - (tab) => tab.tabType === tabType && tab.tabSubType === subType); + tab => tab.tabType === tabType && tab.tabSubType === subType + ); if (!tab) { throw new Error('Cannot find the tab'); @@ -472,15 +529,16 @@ export class ContributionsAndReview this.activeTabType = tabType; this.dropdownShown = false; this.activeDropdownTabChoice = this.getActiveDropdownTabText( - tabType, subType); + tabType, + subType + ); this.activeTabSubtype = subType; this.contributionAndReviewService.setActiveTabType(tabType); this.contributionAndReviewService.setActiveSuggestionType(subType); if (!this.isAccomplishmentsTabActive()) { this.activeExplorationId = null; - this.contributionOpportunitiesService - .reloadOpportunitiesEventEmitter.emit(); + this.contributionOpportunitiesService.reloadOpportunitiesEventEmitter.emit(); } } @@ -492,8 +550,9 @@ export class ContributionsAndReview return this.contributionOpportunitiesService .getReviewableTranslationOpportunitiesAsync( this.translationTopicService.getActiveTopicName(), - this.languageCode) - .then((response) => { + this.languageCode + ) + .then(response => { const opportunitiesDicts = []; response.opportunities.forEach(opportunity => { const opportunityDict = { @@ -502,50 +561,48 @@ export class ContributionsAndReview subheading: opportunity.getOpportunitySubheading(), actionButtonTitle: 'Translations', isPinned: opportunity.isPinned, - topicName: opportunity.topicName + topicName: opportunity.topicName, }; opportunitiesDicts.push(opportunityDict); }); this.opportunities = opportunitiesDicts; return { opportunitiesDicts: opportunitiesDicts, - more: response.more + more: response.more, }; }); } - pinReviewableTranslationOpportunity( - dict: Record - ): void { + pinReviewableTranslationOpportunity(dict: Record): void { const topicName = dict.topic_name; const explorationId = dict.exploration_id; const existingPinnedOpportunity = Object.values(this.opportunities).find( - (opportunity: { - topicName: string; - isPinned: boolean; - }) => ( - opportunity.topicName === topicName && opportunity.isPinned) + (opportunity: {topicName: string; isPinned: boolean}) => + opportunity.topicName === topicName && opportunity.isPinned ); if (existingPinnedOpportunity) { this.openSnackbarWithAction( - topicName, explorationId, + topicName, + explorationId, 'A pinned opportunity already exists for this topic and language.', 'Pin Anyway' ); } else { - this.contributionOpportunitiesService. - pinReviewableTranslationOpportunityAsync( - topicName, this.languageCode, explorationId); + this.contributionOpportunitiesService.pinReviewableTranslationOpportunityAsync( + topicName, + this.languageCode, + explorationId + ); } } - unpinReviewableTranslationOpportunity( - dict: Record - ): void { - this.contributionOpportunitiesService. - unpinReviewableTranslationOpportunityAsync( - dict.topic_name, this.languageCode, dict.exploration_id); + unpinReviewableTranslationOpportunity(dict: Record): void { + this.contributionOpportunitiesService.unpinReviewableTranslationOpportunityAsync( + dict.topic_name, + this.languageCode, + dict.exploration_id + ); } onClickReviewableTranslations(explorationId: string): void { @@ -556,25 +613,29 @@ export class ContributionsAndReview this.activeExplorationId = null; } - loadContributions(shouldResetOffset: boolean): - Promise { + loadContributions( + shouldResetOffset: boolean + ): Promise { this.contributions = {}; if (!this.activeTabType || !this.activeTabSubtype) { return new Promise((resolve, reject) => { resolve({opportunitiesDicts: [], more: false}); }); } - const fetchFunction = this.tabNameToOpportunityFetchFunction[ - this.activeTabSubtype][this.activeTabType]; + const fetchFunction = + this.tabNameToOpportunityFetchFunction[this.activeTabSubtype][ + this.activeTabType + ]; - return fetchFunction(shouldResetOffset).then((response) => { + return fetchFunction(shouldResetOffset).then(response => { Object.keys(response.suggestionIdToDetails).forEach(id => { this.contributions[id] = response.suggestionIdToDetails[id]; }); return { opportunitiesDicts: this.getContributionSummaries( - response.suggestionIdToDetails), - more: response.more + response.suggestionIdToDetails + ), + more: response.more, }; }); } @@ -588,14 +649,14 @@ export class ContributionsAndReview } closeDropdownWhenClickedOutside(clickEvent: {target: Node}): void { - const dropdown = document - .querySelector('.oppia-contributions-dropdown-container'); + const dropdown = document.querySelector( + '.oppia-contributions-dropdown-container' + ); if (!dropdown) { return; } - const clickOccurredWithinDropdown = - dropdown.contains(clickEvent.target); + const clickOccurredWithinDropdown = dropdown.contains(clickEvent.target); if (clickOccurredWithinDropdown) { return; } @@ -609,8 +670,7 @@ export class ContributionsAndReview setReviewableQuestionsSortKey(sortKey: string): void { this.reviewableQuestionsSortKey = sortKey; - this.contributionOpportunitiesService - .reloadOpportunitiesEventEmitter.emit(); + this.contributionOpportunitiesService.reloadOpportunitiesEventEmitter.emit(); } ngOnInit(): void { @@ -622,81 +682,82 @@ export class ContributionsAndReview this.TAB_TYPE_REVIEWS = 'reviews'; this.TAB_TYPE_ACCOMPLISHMENTS = 'accomplishments'; this.REVIEWABLE_QUESTIONS_SORT_KEYS = [ - AppConstants.SUGGESTIONS_SORT_KEY_DATE]; + AppConstants.SUGGESTIONS_SORT_KEY_DATE, + ]; this.userCreatedQuestionsSortKey = AppConstants.SUGGESTIONS_SORT_KEY_DATE; this.reviewableQuestionsSortKey = AppConstants.SUGGESTIONS_SORT_KEY_DATE; - this.userCreatedTranslationsSortKey = ( - AppConstants.SUGGESTIONS_SORT_KEY_DATE); + this.userCreatedTranslationsSortKey = + AppConstants.SUGGESTIONS_SORT_KEY_DATE; this.reviewableTranslationsSortKey = AppConstants.SUGGESTIONS_SORT_KEY_DATE; this.activeExplorationId = null; this.contributions = {}; this.userDetailsLoading = true; this.userIsLoggedIn = false; - this.languageCode = ( - this.translationLanguageService.getActiveLanguageCode()); + this.languageCode = this.translationLanguageService.getActiveLanguageCode(); this.activeTabType = ''; this.activeTabSubtype = ''; this.dropdownShown = false; this.activeDropdownTabChoice = ''; this.reviewTabs = []; - this.accomplishmentsTabIsEnabled = ( - this.featureService.status.ContributorDashboardAccomplishments.isEnabled); + this.accomplishmentsTabIsEnabled = + this.featureService.status.ContributorDashboardAccomplishments.isEnabled; this.contributionTabs = [ { tabType: this.TAB_TYPE_CONTRIBUTIONS, tabSubType: this.SUGGESTION_TYPE_QUESTION, text: 'Questions', - enabled: false + enabled: false, }, { tabType: this.TAB_TYPE_CONTRIBUTIONS, tabSubType: this.SUGGESTION_TYPE_TRANSLATE, text: 'Translations', - enabled: true - } + enabled: true, + }, ]; this.accomplishmentsTabs = [ { tabSubType: 'stats', tabType: this.TAB_TYPE_ACCOMPLISHMENTS, text: 'Contribution Stats', - enabled: true + enabled: true, }, { tabSubType: 'badges', tabType: this.TAB_TYPE_ACCOMPLISHMENTS, text: 'Badges', - enabled: true - } + enabled: true, + }, ]; // Reset active exploration when changing topics. this.directiveSubscriptions.add( - this.translationTopicService.onActiveTopicChanged.subscribe( - () => { - this.activeExplorationId = null; - this.loadOpportunities(); - })); + this.translationTopicService.onActiveTopicChanged.subscribe(() => { + this.activeExplorationId = null; + this.loadOpportunities(); + }) + ); - this.userService.getUserInfoAsync().then((userInfo) => { + this.userService.getUserInfoAsync().then(userInfo => { this.userIsLoggedIn = userInfo.isLoggedIn(); this.userDetailsLoading = false; if (this.userIsLoggedIn) { - this.userService.getUserContributionRightsDataAsync().then( - (userContributionRights) => { - const userCanReviewTranslationSuggestionsInLanguages = ( - userContributionRights - .can_review_translation_for_language_codes); - const userCanReviewQuestionSuggestions = ( - userContributionRights.can_review_questions); + this.userService + .getUserContributionRightsDataAsync() + .then(userContributionRights => { + const userCanReviewTranslationSuggestionsInLanguages = + userContributionRights.can_review_translation_for_language_codes; + const userCanReviewQuestionSuggestions = + userContributionRights.can_review_questions; const userReviewableSuggestionTypes = []; - const userCanSuggestQuestions = ( - userContributionRights.can_suggest_questions); + const userCanSuggestQuestions = + userContributionRights.can_suggest_questions; for (let index in this.contributionTabs) { - if (this.contributionTabs[index].tabSubType === ( - this.SUGGESTION_TYPE_QUESTION)) { - this.contributionTabs[index].enabled = ( - userCanSuggestQuestions); + if ( + this.contributionTabs[index].tabSubType === + this.SUGGESTION_TYPE_QUESTION + ) { + this.contributionTabs[index].enabled = userCanSuggestQuestions; } } if (userCanReviewQuestionSuggestions) { @@ -704,31 +765,36 @@ export class ContributionsAndReview tabType: this.TAB_TYPE_REVIEWS, tabSubType: this.SUGGESTION_TYPE_QUESTION, text: 'Review Questions', - enabled: false + enabled: false, }); userReviewableSuggestionTypes.push(this.SUGGESTION_TYPE_QUESTION); } - if ( - userCanReviewTranslationSuggestionsInLanguages - .length > 0) { + if (userCanReviewTranslationSuggestionsInLanguages.length > 0) { this.reviewTabs.push({ tabType: this.TAB_TYPE_REVIEWS, tabSubType: this.SUGGESTION_TYPE_TRANSLATE, text: 'Review Translations', - enabled: false + enabled: false, }); userReviewableSuggestionTypes.push( - this.SUGGESTION_TYPE_TRANSLATE); + this.SUGGESTION_TYPE_TRANSLATE + ); } if (userReviewableSuggestionTypes.length > 0) { this.switchToTab( - this.TAB_TYPE_REVIEWS, userReviewableSuggestionTypes[0]); + this.TAB_TYPE_REVIEWS, + userReviewableSuggestionTypes[0] + ); } else if (userCanSuggestQuestions) { this.switchToTab( - this.TAB_TYPE_CONTRIBUTIONS, this.SUGGESTION_TYPE_QUESTION); + this.TAB_TYPE_CONTRIBUTIONS, + this.SUGGESTION_TYPE_QUESTION + ); } else { this.switchToTab( - this.TAB_TYPE_CONTRIBUTIONS, this.SUGGESTION_TYPE_TRANSLATE); + this.TAB_TYPE_CONTRIBUTIONS, + this.SUGGESTION_TYPE_TRANSLATE + ); } }); } @@ -737,63 +803,67 @@ export class ContributionsAndReview this.tabNameToOpportunityFetchFunction = { [this.SUGGESTION_TYPE_QUESTION]: { [this.TAB_TYPE_CONTRIBUTIONS]: shouldResetOffset => { - return this.contributionAndReviewService - .getUserCreatedQuestionSuggestionsAsync( - shouldResetOffset, this.userCreatedQuestionsSortKey); + return this.contributionAndReviewService.getUserCreatedQuestionSuggestionsAsync( + shouldResetOffset, + this.userCreatedQuestionsSortKey + ); }, [this.TAB_TYPE_REVIEWS]: shouldResetOffset => { - return this.contributionAndReviewService - .getReviewableQuestionSuggestionsAsync( - shouldResetOffset, - this.reviewableQuestionsSortKey, - this.translationTopicService.getActiveTopicName()); - } + return this.contributionAndReviewService.getReviewableQuestionSuggestionsAsync( + shouldResetOffset, + this.reviewableQuestionsSortKey, + this.translationTopicService.getActiveTopicName() + ); + }, }, [this.SUGGESTION_TYPE_TRANSLATE]: { [this.TAB_TYPE_CONTRIBUTIONS]: shouldResetOffset => { - return this.contributionAndReviewService - .getUserCreatedTranslationSuggestionsAsync( - shouldResetOffset, this.userCreatedTranslationsSortKey); + return this.contributionAndReviewService.getUserCreatedTranslationSuggestionsAsync( + shouldResetOffset, + this.userCreatedTranslationsSortKey + ); }, [this.TAB_TYPE_REVIEWS]: shouldResetOffset => { - return this.contributionAndReviewService - .getReviewableTranslationSuggestionsAsync( - shouldResetOffset, - this.reviewableTranslationsSortKey, - this.activeExplorationId); - } - } + return this.contributionAndReviewService.getReviewableTranslationSuggestionsAsync( + shouldResetOffset, + this.reviewableTranslationsSortKey, + this.activeExplorationId + ); + }, + }, }; $(document).on('click', this.closeDropdownWhenClickedOutside); } openSnackbarWithAction( - topicName: string, - explorationId: string, - message: string, - actionText: string + topicName: string, + explorationId: string, + message: string, + actionText: string ): void { const snackBarRef: CustomMatSnackBarRef = this.snackBar.open( - message, actionText, { + message, + actionText, + { duration: 3000, - }); + } + ); this.handleSnackbarAction(snackBarRef, topicName, explorationId); } private handleSnackbarAction( - snackBarRef: CustomMatSnackBarRef, - topicName: string, - explorationId: string + snackBarRef: CustomMatSnackBarRef, + topicName: string, + explorationId: string ): void { snackBarRef.onAction().subscribe(() => { - this.contributionOpportunitiesService - .pinReviewableTranslationOpportunityAsync( - topicName, - this.languageCode, - explorationId - ); + this.contributionOpportunitiesService.pinReviewableTranslationOpportunityAsync( + topicName, + this.languageCode, + explorationId + ); }); } @@ -808,7 +878,9 @@ export class ContributionsAndReview } } -angular.module('oppia').directive('oppiaContributionsAndReview', +angular.module('oppia').directive( + 'oppiaContributionsAndReview', downgradeComponent({ - component: ContributionsAndReview - }) as angular.IDirectiveFactory); + component: ContributionsAndReview, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/contributor-dashboard-page/contributor-badges/contributor-badges.component.spec.ts b/core/templates/pages/contributor-dashboard-page/contributor-badges/contributor-badges.component.spec.ts index 1f8f76ee74b5..ea48efe62bce 100644 --- a/core/templates/pages/contributor-dashboard-page/contributor-badges/contributor-badges.component.spec.ts +++ b/core/templates/pages/contributor-dashboard-page/contributor-badges/contributor-badges.component.spec.ts @@ -16,15 +16,22 @@ * @fileoverview Unit tests for ContributorBadgesComponent. */ -import { ComponentFixture, fakeAsync, flush, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { UserService } from 'services/user.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { UserInfo } from 'domain/user/user-info.model'; -import { ContributionAndReviewStatsService } from '../services/contribution-and-review-stats.service'; -import { LanguageUtilService } from 'domain/utilities/language-util.service'; -import { ContributorBadgesComponent } from './contributor-badges.component'; -import { MobileBadgeType } from './contributor-badges.component'; +import { + ComponentFixture, + fakeAsync, + flush, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {UserService} from 'services/user.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {UserInfo} from 'domain/user/user-info.model'; +import {ContributionAndReviewStatsService} from '../services/contribution-and-review-stats.service'; +import {LanguageUtilService} from 'domain/utilities/language-util.service'; +import {ContributorBadgesComponent} from './contributor-badges.component'; +import {MobileBadgeType} from './contributor-badges.component'; describe('Contributor badge component', () => { let fetchAllContributionAndReviewStatsAsync: jasmine.Spy; @@ -39,7 +46,7 @@ describe('Contributor badge component', () => { rejected_translations_count: 0, rejected_translation_word_count: 0, first_contribution_date: 'Mar 2021', - last_contribution_date: 'Mar 2021' + last_contribution_date: 'Mar 2021', }; const akanSecondTopicTranslationContributionStat = { language_code: 'ak', @@ -52,7 +59,7 @@ describe('Contributor badge component', () => { rejected_translations_count: 0, rejected_translation_word_count: 0, first_contribution_date: 'Mar 2021', - last_contribution_date: 'Mar 2021' + last_contribution_date: 'Mar 2021', }; const spanishFirstTopicTranslationContributionStat = { language_code: 'es', @@ -65,7 +72,7 @@ describe('Contributor badge component', () => { rejected_translations_count: 0, rejected_translation_word_count: 0, first_contribution_date: 'Mar 2021', - last_contribution_date: 'Mar 2021' + last_contribution_date: 'Mar 2021', }; const spanishSecondTopicTranslationContributionStat = { language_code: 'es', @@ -78,7 +85,7 @@ describe('Contributor badge component', () => { rejected_translations_count: 0, rejected_translation_word_count: 0, first_contribution_date: 'Mar 2021', - last_contribution_date: 'Mar 2021' + last_contribution_date: 'Mar 2021', }; const akanFirstTopicTranslationReviewStat = { language_code: 'ak', @@ -89,7 +96,7 @@ describe('Contributor badge component', () => { accepted_translations_with_reviewer_edits_count: 10, accepted_translation_word_count: 70, first_contribution_date: 'Mar 2021', - last_contribution_date: 'Mar 2021' + last_contribution_date: 'Mar 2021', }; const hindiTopicTranslationReviewStat = { language_code: 'hi', @@ -100,7 +107,7 @@ describe('Contributor badge component', () => { accepted_translations_with_reviewer_edits_count: 10, accepted_translation_word_count: 70, first_contribution_date: 'Mar 2021', - last_contribution_date: 'Mar 2021' + last_contribution_date: 'Mar 2021', }; const spanishTranslationReviewStat = { language_code: 'es', @@ -111,7 +118,7 @@ describe('Contributor badge component', () => { accepted_translations_with_reviewer_edits_count: 1250, accepted_translation_word_count: 3500, first_contribution_date: 'Mar 2021', - last_contribution_date: 'Mar 2021' + last_contribution_date: 'Mar 2021', }; const questionContributionStat = { topic_name: 'published_topic_name', @@ -119,7 +126,7 @@ describe('Contributor badge component', () => { accepted_questions_count: 1, accepted_questions_without_reviewer_edits_count: 0, first_contribution_date: 'Mar 2021', - last_contribution_date: 'Mar 2021' + last_contribution_date: 'Mar 2021', }; const questionReviewStat = { topic_name: 'published_topic_name', @@ -127,7 +134,7 @@ describe('Contributor badge component', () => { accepted_questions_count: 1, accepted_questions_with_reviewer_edits_count: 1, first_contribution_date: 'Mar 2021', - last_contribution_date: 'Mar 2021' + last_contribution_date: 'Mar 2021', }; const fetchAllStatsResponse = { @@ -135,15 +142,15 @@ describe('Contributor badge component', () => { akanFirstTopicTranslationContributionStat, akanSecondTopicTranslationContributionStat, spanishFirstTopicTranslationContributionStat, - spanishSecondTopicTranslationContributionStat + spanishSecondTopicTranslationContributionStat, ], translation_review_stats: [ akanFirstTopicTranslationReviewStat, hindiTopicTranslationReviewStat, - spanishTranslationReviewStat + spanishTranslationReviewStat, ], question_contribution_stats: [questionContributionStat], - question_review_stats: [questionReviewStat] + question_review_stats: [questionReviewStat], }; let component: ContributorBadgesComponent; let fixture: ComponentFixture; @@ -154,15 +161,13 @@ describe('Contributor badge component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - ContributorBadgesComponent - ], + declarations: [ContributorBadgesComponent], providers: [ ContributionAndReviewStatsService, LanguageUtilService, - UserService + UserService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -171,7 +176,8 @@ describe('Contributor badge component', () => { component = fixture.componentInstance; contributionAndReviewStatsService = TestBed.inject( - ContributionAndReviewStatsService); + ContributionAndReviewStatsService + ); languageUtilService = TestBed.inject(LanguageUtilService); userService = TestBed.inject(UserService); @@ -194,25 +200,40 @@ describe('Contributor badge component', () => { beforeEach(waitForAsync(() => { fetchAllContributionAndReviewStatsAsync = spyOn( contributionAndReviewStatsService, - 'fetchAllStats'); + 'fetchAllStats' + ); fetchAllContributionAndReviewStatsAsync.and.returnValue( - Promise.resolve(fetchAllStatsResponse)); + Promise.resolve(fetchAllStatsResponse) + ); spyOn( - languageUtilService, 'getAudioLanguageDescription') - .and.returnValues( - 'Akan', 'Akan', 'Spanish', - 'Spanish', 'Akan', 'Hindi', - 'Spanish', 'Spanish', 'português', 'Hindi',); + languageUtilService, + 'getAudioLanguageDescription' + ).and.returnValues( + 'Akan', + 'Akan', + 'Spanish', + 'Spanish', + 'Akan', + 'Hindi', + 'Spanish', + 'Spanish', + 'português', + 'Hindi' + ); spyOn( - languageUtilService, 'getShortLanguageDescription') - .and.returnValue('Language'); - spyOn(userService, 'getUserInfoAsync') - .and.returnValue(Promise.resolve({ + languageUtilService, + 'getShortLanguageDescription' + ).and.returnValue('Language'); + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve({ isLoggedIn: () => true, - getUsername: () => 'user' - } as UserInfo)); - spyOn(userService, 'getUserContributionRightsDataAsync') - .and.returnValue(Promise.resolve(userContributionRights)); + getUsername: () => 'user', + } as UserInfo) + ); + spyOn( + userService, + 'getUserContributionRightsDataAsync' + ).and.returnValue(Promise.resolve(userContributionRights)); component.ngOnInit(); })); @@ -225,23 +246,21 @@ describe('Contributor badge component', () => { expect(component.questionReviewBadges.length).toBeGreaterThan(0); })); - it('should toggle language dropdown when user clicks on it', fakeAsync( - () => { - component.dropdownShown = false; + it('should toggle language dropdown when user clicks on it', fakeAsync(() => { + component.dropdownShown = false; - component.toggleLanguageDropdown(); + component.toggleLanguageDropdown(); - expect(component.dropdownShown).toBeTrue(); - })); + expect(component.dropdownShown).toBeTrue(); + })); - it('should toggle mobile language dropdown when user clicks on it', - fakeAsync(() => { - component.mobileDropdownShown = false; + it('should toggle mobile language dropdown when user clicks on it', fakeAsync(() => { + component.mobileDropdownShown = false; - component.toggleMobileLanguageDropdown(); + component.toggleMobileLanguageDropdown(); - expect(component.mobileDropdownShown).toBeTrue(); - })); + expect(component.mobileDropdownShown).toBeTrue(); + })); it('should toggle mobile badge type dropdown', fakeAsync(() => { component.mobileBadgeTypeDropdownShown = false; @@ -261,122 +280,136 @@ describe('Contributor badge component', () => { component.selectBadgeType(MobileBadgeType.Translation); expect(component.mobileBadgeTypeSelected).toBe( - MobileBadgeType.Translation); + MobileBadgeType.Translation + ); })); it('should show question badges type in mobile', fakeAsync(() => { component.selectBadgeType(MobileBadgeType.Question); expect(component.mobileBadgeTypeSelected).toBe( - MobileBadgeType.Question); + MobileBadgeType.Question + ); })); }); - describe( - 'when user has no translation badges and no question rights ', () => { - const userContributionRights = { - can_review_translation_for_language_codes: [], - can_review_voiceover_for_language_codes: [], - can_review_questions: false, - can_suggest_questions: false, - }; - - beforeEach(waitForAsync(() => { - fetchAllContributionAndReviewStatsAsync = spyOn( - contributionAndReviewStatsService, - 'fetchAllStats'); - fetchAllContributionAndReviewStatsAsync.and.returnValue( - Promise.resolve({ - translation_contribution_stats: [], - translation_review_stats: [], - question_contribution_stats: [], - question_review_stats: [] - })); - spyOn( - languageUtilService, 'getAudioLanguageDescription') - .and.returnValues('Spanish', 'português', 'Hindi', 'Akan'); - spyOn( - languageUtilService, 'getShortLanguageDescription') - .and.returnValue('Language'); - spyOn(userService, 'getUserInfoAsync') - .and.returnValue(Promise.resolve({ - isLoggedIn: () => true, - getUsername: () => 'user' - } as UserInfo)); - spyOn(userService, 'getUserContributionRightsDataAsync') - .and.returnValue(Promise.resolve(userContributionRights)); - component.ngOnInit(); - })); - - it('should not display any badge', fakeAsync(() => { - expect(component.translationBadges).toEqual({}); - expect(component.questionSubmissionBadges).toEqual([]); - })); - }); - }); + describe('when user has no translation badges and no question rights ', () => { + const userContributionRights = { + can_review_translation_for_language_codes: [], + can_review_voiceover_for_language_codes: [], + can_review_questions: false, + can_suggest_questions: false, + }; - describe('when user contribution rights can not be fetched', - () => { - it('should throw error to mention the error', - fakeAsync(() => { - spyOn(userService, 'getUserInfoAsync') - .and.returnValue(Promise.resolve({ - isLoggedIn: () => true, - getUsername: () => 'user' - } as UserInfo)); - spyOn(userService, 'getUserContributionRightsDataAsync') - .and.returnValue(Promise.resolve(null)); - - expect(() => { - component.ngOnInit(); - tick(); - }).toThrowError(); - flush(); - })); - }); + beforeEach(waitForAsync(() => { + fetchAllContributionAndReviewStatsAsync = spyOn( + contributionAndReviewStatsService, + 'fetchAllStats' + ); + fetchAllContributionAndReviewStatsAsync.and.returnValue( + Promise.resolve({ + translation_contribution_stats: [], + translation_review_stats: [], + question_contribution_stats: [], + question_review_stats: [], + }) + ); + spyOn( + languageUtilService, + 'getAudioLanguageDescription' + ).and.returnValues('Spanish', 'português', 'Hindi', 'Akan'); + spyOn( + languageUtilService, + 'getShortLanguageDescription' + ).and.returnValue('Language'); + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve({ + isLoggedIn: () => true, + getUsername: () => 'user', + } as UserInfo) + ); + spyOn( + userService, + 'getUserContributionRightsDataAsync' + ).and.returnValue(Promise.resolve(userContributionRights)); + component.ngOnInit(); + })); - describe('when user navigates to contributor stats page without login', - () => { - it('should throw error if username is invalid', - fakeAsync(() => { - const defaultUserInfo = new UserInfo( - ['GUEST'], false, false, false, false, false, - null, null, null, false); - spyOn(userService, 'getUserInfoAsync').and - .returnValue(Promise.resolve(defaultUserInfo)); - - expect(() => { - component.ngOnInit(); - tick(); - }).toThrowError(); - flush(); - })); + it('should not display any badge', fakeAsync(() => { + expect(component.translationBadges).toEqual({}); + expect(component.questionSubmissionBadges).toEqual([]); + })); }); + }); - describe('when user interacts with dropdown', - () => { - let getDropdownOptionsContainer: () => HTMLElement; - - beforeEach(() => { - getDropdownOptionsContainer = () => { - return fixture.debugElement.nativeElement.querySelector( - '.oppia-stats-type-selector-dropdown-container'); - }; - }); + describe('when user contribution rights can not be fetched', () => { + it('should throw error to mention the error', fakeAsync(() => { + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve({ + isLoggedIn: () => true, + getUsername: () => 'user', + } as UserInfo) + ); + spyOn(userService, 'getUserContributionRightsDataAsync').and.returnValue( + Promise.resolve(null) + ); + + expect(() => { + component.ngOnInit(); + tick(); + }).toThrowError(); + flush(); + })); + }); - it('should correctly show and hide when clicked away', - fakeAsync(() => { - let fakeClickAwayEvent = new MouseEvent('click'); - Object.defineProperty( - fakeClickAwayEvent, - 'target', - {value: document.createElement('div')}); + describe('when user navigates to contributor stats page without login', () => { + it('should throw error if username is invalid', fakeAsync(() => { + const defaultUserInfo = new UserInfo( + ['GUEST'], + false, + false, + false, + false, + false, + null, + null, + null, + false + ); + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve(defaultUserInfo) + ); + + expect(() => { + component.ngOnInit(); + tick(); + }).toThrowError(); + flush(); + })); + }); - component.onDocumentClick(fakeClickAwayEvent); - fixture.detectChanges(); + describe('when user interacts with dropdown', () => { + let getDropdownOptionsContainer: () => HTMLElement; - expect(component.dropdownShown).toBe(false); - expect(getDropdownOptionsContainer()).toBeFalsy(); - })); + beforeEach(() => { + getDropdownOptionsContainer = () => { + return fixture.debugElement.nativeElement.querySelector( + '.oppia-stats-type-selector-dropdown-container' + ); + }; }); + + it('should correctly show and hide when clicked away', fakeAsync(() => { + let fakeClickAwayEvent = new MouseEvent('click'); + Object.defineProperty(fakeClickAwayEvent, 'target', { + value: document.createElement('div'), + }); + + component.onDocumentClick(fakeClickAwayEvent); + fixture.detectChanges(); + + expect(component.dropdownShown).toBe(false); + expect(getDropdownOptionsContainer()).toBeFalsy(); + })); + }); }); diff --git a/core/templates/pages/contributor-dashboard-page/contributor-badges/contributor-badges.component.ts b/core/templates/pages/contributor-dashboard-page/contributor-badges/contributor-badges.component.ts index 9abd5b2b7605..6a65772eca04 100644 --- a/core/templates/pages/contributor-dashboard-page/contributor-badges/contributor-badges.component.ts +++ b/core/templates/pages/contributor-dashboard-page/contributor-badges/contributor-badges.component.ts @@ -16,11 +16,11 @@ * @fileoverview Component for the contributor badges. */ -import { Component, ElementRef, HostListener, ViewChild } from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { LanguageUtilService } from 'domain/utilities/language-util.service'; -import { UserService } from 'services/user.service'; -import { ContributionAndReviewStatsService } from '../services/contribution-and-review-stats.service'; +import {Component, ElementRef, HostListener, ViewChild} from '@angular/core'; +import {AppConstants} from 'app.constants'; +import {LanguageUtilService} from 'domain/utilities/language-util.service'; +import {UserService} from 'services/user.service'; +import {ContributionAndReviewStatsService} from '../services/contribution-and-review-stats.service'; interface ContributionCounts { language: string | null; @@ -44,18 +44,15 @@ export enum MobileBadgeType { @Component({ selector: 'contributor-badges', templateUrl: './contributor-badges.component.html', - styleUrls: [] + styleUrls: [], }) export class ContributorBadgesComponent { - @ViewChild('dropdown', {'static': false}) dropdownRef!: ElementRef; + @ViewChild('dropdown', {static: false}) dropdownRef!: ElementRef; - @ViewChild( - 'mobileDropdown', {'static': false} - ) mobileDropdownRef!: ElementRef; + @ViewChild('mobileDropdown', {static: false}) mobileDropdownRef!: ElementRef; - @ViewChild( - 'mobileBadgeTypeDropdown', {'static': false} - ) mobileBadgeTypeDropdownRef!: ElementRef; + @ViewChild('mobileBadgeTypeDropdown', {static: false}) + mobileBadgeTypeDropdownRef!: ElementRef; totalTranslationStats: {[key: string]: ContributionCounts} = {}; translationBadges: {[key: string]: {[key: string]: Badge[]}} = {}; @@ -68,7 +65,7 @@ export class ContributorBadgesComponent { language: null, submissions: 0, reviews: 0, - corrections: 0 + corrections: 0, }; dropdownShown = false; @@ -84,9 +81,9 @@ export class ContributorBadgesComponent { constructor( private readonly languageUtilService: LanguageUtilService, - private readonly contributionAndReviewStatsService: - ContributionAndReviewStatsService, - private readonly userService: UserService) {} + private readonly contributionAndReviewStatsService: ContributionAndReviewStatsService, + private readonly userService: UserService + ) {} async ngOnInit(): Promise { this.dataLoading = true; @@ -104,73 +101,77 @@ export class ContributorBadgesComponent { throw new Error('Cannot fetch user contribution rights.'); } - const allContributionStats = await this - .contributionAndReviewStatsService.fetchAllStats(username); + const allContributionStats = + await this.contributionAndReviewStatsService.fetchAllStats(username); - this.userCanReviewQuestionSuggestions = ( - userContributionRights.can_review_questions); - this.userCanSuggestQuestions = ( - userContributionRights.can_suggest_questions); - this.userHasQuestionRights = ( - this.userCanSuggestQuestions || - this.userCanReviewQuestionSuggestions); + this.userCanReviewQuestionSuggestions = + userContributionRights.can_review_questions; + this.userCanSuggestQuestions = userContributionRights.can_suggest_questions; + this.userHasQuestionRights = + this.userCanSuggestQuestions || this.userCanReviewQuestionSuggestions; if (allContributionStats.translation_contribution_stats.length > 0) { - allContributionStats.translation_contribution_stats.forEach((stat) => { + allContributionStats.translation_contribution_stats.forEach(stat => { const languageDescription = this.languageUtilService.getAudioLanguageDescription( - stat.language_code); + stat.language_code + ); // There can be languages that the contributor has translation // contribution stats but not translation review stats. Hence we need to // initialize TotalTranslationStats objects for those languages. if (!this.totalTranslationStats[languageDescription]) { this.totalTranslationStats[languageDescription] = { - language: this.languageUtilService.getShortLanguageDescription( - languageDescription), + language: + this.languageUtilService.getShortLanguageDescription( + languageDescription + ), submissions: stat.accepted_translations_count, reviews: 0, - corrections: 0 + corrections: 0, }; } else { - this.totalTranslationStats[languageDescription].submissions += ( - stat.accepted_translations_count); + this.totalTranslationStats[languageDescription].submissions += + stat.accepted_translations_count; } }); } if (allContributionStats.translation_review_stats.length > 0) { - allContributionStats.translation_review_stats.forEach((stat) => { + allContributionStats.translation_review_stats.forEach(stat => { const languageDescription = this.languageUtilService.getAudioLanguageDescription( - stat.language_code); + stat.language_code + ); if (!this.totalTranslationStats[languageDescription]) { this.totalTranslationStats[languageDescription] = { - language: this.languageUtilService.getShortLanguageDescription( - languageDescription), + language: + this.languageUtilService.getShortLanguageDescription( + languageDescription + ), submissions: 0, reviews: stat.reviewed_translations_count, - corrections: stat.accepted_translations_with_reviewer_edits_count + corrections: stat.accepted_translations_with_reviewer_edits_count, }; this.reviewableLanguages.push(languageDescription); } else { - this.totalTranslationStats[languageDescription].reviews += ( - stat.reviewed_translations_count); - this.totalTranslationStats[languageDescription].corrections += ( - stat.accepted_translations_with_reviewer_edits_count); + this.totalTranslationStats[languageDescription].reviews += + stat.reviewed_translations_count; + this.totalTranslationStats[languageDescription].corrections += + stat.accepted_translations_with_reviewer_edits_count; } }); } if (allContributionStats.question_contribution_stats.length > 0) { - allContributionStats.question_contribution_stats.map((stat) => { + allContributionStats.question_contribution_stats.map(stat => { this.totalQuestionStats.submissions += stat.accepted_questions_count; }); } if (allContributionStats.question_review_stats.length > 0) { - allContributionStats.question_review_stats.map((stat) => { + allContributionStats.question_review_stats.map(stat => { this.totalQuestionStats.reviews += stat.reviewed_questions_count; - this.totalQuestionStats.corrections += ( - stat.accepted_questions_with_reviewer_edits_count); + this.totalQuestionStats.corrections += + stat.accepted_questions_with_reviewer_edits_count; }); } @@ -178,53 +179,62 @@ export class ContributorBadgesComponent { this.translationBadges[language] = {}; this.translationBadges[language][ - AppConstants.CONTRIBUTION_STATS_SUBTYPE_SUBMISSION] = - this.getObtainedBadges( - this.totalTranslationStats[language].submissions, - AppConstants.CONTRIBUTION_STATS_SUBTYPE_SUBMISSION, - this.totalTranslationStats[language].language); + AppConstants.CONTRIBUTION_STATS_SUBTYPE_SUBMISSION + ] = this.getObtainedBadges( + this.totalTranslationStats[language].submissions, + AppConstants.CONTRIBUTION_STATS_SUBTYPE_SUBMISSION, + this.totalTranslationStats[language].language + ); this.translationBadges[language][ - AppConstants.CONTRIBUTION_STATS_SUBTYPE_REVIEW] = - this.getObtainedBadges( - this.totalTranslationStats[language].reviews, - AppConstants.CONTRIBUTION_STATS_SUBTYPE_REVIEW, - this.totalTranslationStats[language].language); + AppConstants.CONTRIBUTION_STATS_SUBTYPE_REVIEW + ] = this.getObtainedBadges( + this.totalTranslationStats[language].reviews, + AppConstants.CONTRIBUTION_STATS_SUBTYPE_REVIEW, + this.totalTranslationStats[language].language + ); this.translationBadges[language][ - AppConstants.CONTRIBUTION_STATS_SUBTYPE_CORRECTION] = - this.getObtainedBadges( - this.totalTranslationStats[language].corrections, - AppConstants.CONTRIBUTION_STATS_SUBTYPE_CORRECTION, - this.totalTranslationStats[language].language); + AppConstants.CONTRIBUTION_STATS_SUBTYPE_CORRECTION + ] = this.getObtainedBadges( + this.totalTranslationStats[language].corrections, + AppConstants.CONTRIBUTION_STATS_SUBTYPE_CORRECTION, + this.totalTranslationStats[language].language + ); } if (this.userCanSuggestQuestions) { this.questionSubmissionBadges = this.getObtainedBadges( this.totalQuestionStats.submissions, - AppConstants.CONTRIBUTION_STATS_SUBTYPE_SUBMISSION, null); + AppConstants.CONTRIBUTION_STATS_SUBTYPE_SUBMISSION, + null + ); } if (this.userCanReviewQuestionSuggestions) { this.questionReviewBadges = this.getObtainedBadges( this.totalQuestionStats.reviews, - AppConstants.CONTRIBUTION_STATS_SUBTYPE_REVIEW, null); + AppConstants.CONTRIBUTION_STATS_SUBTYPE_REVIEW, + null + ); this.questionCorrectionBadges = this.getObtainedBadges( this.totalQuestionStats.corrections, - AppConstants.CONTRIBUTION_STATS_SUBTYPE_CORRECTION, null); + AppConstants.CONTRIBUTION_STATS_SUBTYPE_CORRECTION, + null + ); } this.languages = Object.keys(this.totalTranslationStats); if (this.languages.length > 0) { this.selectedLanguage = this.languages[0]; - this.userCanReviewTranslationSuggestion = ( - this.reviewableLanguages.includes(this.selectedLanguage)); + this.userCanReviewTranslationSuggestion = + this.reviewableLanguages.includes(this.selectedLanguage); } this.dataLoading = false; } getObtainedBadges( - contributionCount: number, - contributionSubType: string, - language: string | null + contributionCount: number, + contributionSubType: string, + language: string | null ): Badge[] { const badges: Badge[] = []; let level = 0; @@ -238,24 +248,20 @@ export class ContributorBadgesComponent { } if (contributionCount >= level) { - badges.push( - { - contributionCount: level, - text: contributionSubType, - isUnlocked: true, - language - } - ); + badges.push({ + contributionCount: level, + text: contributionSubType, + isUnlocked: true, + language, + }); } else { // Add a locked badge for the next unachieved milestone. - badges.push( - { - contributionCount: level - contributionCount, - text: contributionSubType, - isUnlocked: false, - language - } - ); + badges.push({ + contributionCount: level - contributionCount, + text: contributionSubType, + isUnlocked: false, + language, + }); break; } } @@ -276,8 +282,9 @@ export class ContributorBadgesComponent { selectLanguageOption(language: string): void { this.selectedLanguage = language; - this.userCanReviewTranslationSuggestion = ( - this.reviewableLanguages.includes(this.selectedLanguage)); + this.userCanReviewTranslationSuggestion = this.reviewableLanguages.includes( + this.selectedLanguage + ); this.dropdownShown = false; this.mobileDropdownShown = false; } @@ -288,9 +295,9 @@ export class ContributorBadgesComponent { } /** - * Close dropdown when outside elements are clicked - * @param event mouse click event - */ + * Close dropdown when outside elements are clicked + * @param event mouse click event + */ @HostListener('document:click', ['$event']) onDocumentClick(event: MouseEvent): void { const targetElement = event.target as HTMLElement; diff --git a/core/templates/pages/contributor-dashboard-page/contributor-dashboard-page.component.spec.ts b/core/templates/pages/contributor-dashboard-page/contributor-dashboard-page.component.spec.ts index 38bee2d87277..c58f7e562b1f 100644 --- a/core/templates/pages/contributor-dashboard-page/contributor-dashboard-page.component.spec.ts +++ b/core/templates/pages/contributor-dashboard-page/contributor-dashboard-page.component.spec.ts @@ -16,20 +16,26 @@ * @fileoverview Unit tests for contributor dashboard page component. */ -import { ComponentFixture, fakeAsync, flush, TestBed, waitForAsync } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { ContributorDashboardPageComponent } from 'pages/contributor-dashboard-page/contributor-dashboard-page.component'; -import { ContributionAndReviewService } from './services/contribution-and-review.service'; -import { ContributionOpportunitiesService } from './services/contribution-opportunities.service'; -import { TranslationTopicService } from 'pages/exploration-editor-page/translation-tab/services/translation-topic.service'; -import { TranslationLanguageService } from 'pages/exploration-editor-page/translation-tab/services/translation-language.service'; -import { UserService } from 'services/user.service'; -import { LocalStorageService } from 'services/local-storage.service'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { UserInfo } from 'domain/user/user-info.model'; -import { AppConstants } from 'app.constants'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; +import { + ComponentFixture, + fakeAsync, + flush, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {ContributorDashboardPageComponent} from 'pages/contributor-dashboard-page/contributor-dashboard-page.component'; +import {ContributionAndReviewService} from './services/contribution-and-review.service'; +import {ContributionOpportunitiesService} from './services/contribution-opportunities.service'; +import {TranslationTopicService} from 'pages/exploration-editor-page/translation-tab/services/translation-topic.service'; +import {TranslationLanguageService} from 'pages/exploration-editor-page/translation-tab/services/translation-language.service'; +import {UserService} from 'services/user.service'; +import {LocalStorageService} from 'services/local-storage.service'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {UserInfo} from 'domain/user/user-info.model'; +import {AppConstants} from 'app.constants'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; describe('Contributor dashboard page', () => { let component: ContributorDashboardPageComponent; @@ -54,18 +60,16 @@ describe('Contributor dashboard page', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - ContributorDashboardPageComponent - ], + declarations: [ContributorDashboardPageComponent], providers: [ LocalStorageService, UserService, TranslationLanguageService, TranslationTopicService, ContributionOpportunitiesService, - ContributionAndReviewService + ContributionAndReviewService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -74,8 +78,9 @@ describe('Contributor dashboard page', () => { component = fixture.componentInstance; contributionAndReviewService = TestBed.inject(ContributionAndReviewService); - contributionOpportunitiesService = - TestBed.inject(ContributionOpportunitiesService); + contributionOpportunitiesService = TestBed.inject( + ContributionOpportunitiesService + ); localStorageService = TestBed.inject(LocalStorageService); translationLanguageService = TestBed.inject(TranslationLanguageService); translationTopicService = TestBed.inject(TranslationTopicService); @@ -84,34 +89,45 @@ describe('Contributor dashboard page', () => { urlInterpolationService = TestBed.inject(UrlInterpolationService); getTranslatableTopicNamesAsyncSpy = spyOn( - contributionOpportunitiesService, 'getTranslatableTopicNamesAsync'); + contributionOpportunitiesService, + 'getTranslatableTopicNamesAsync' + ); getTranslatableTopicNamesAsyncSpy.and.returnValue( - Promise.resolve(['Topic 1', 'Topic 2'])); - spyOn(localStorageService, 'getLastSelectedTranslationLanguageCode').and - .returnValue(''); - spyOn(localStorageService, 'getLastSelectedTranslationTopicName').and - .returnValue('Topic 1'); - spyOn(translationLanguageService, 'setActiveLanguageCode').and - .callThrough(); + Promise.resolve(['Topic 1', 'Topic 2']) + ); + spyOn( + localStorageService, + 'getLastSelectedTranslationLanguageCode' + ).and.returnValue(''); + spyOn( + localStorageService, + 'getLastSelectedTranslationTopicName' + ).and.returnValue('Topic 1'); + spyOn( + translationLanguageService, + 'setActiveLanguageCode' + ).and.callThrough(); spyOn(translationTopicService, 'setActiveTopicName').and.callThrough(); let userInfo = { isLoggedIn: () => true, - getUsername: () => 'username1' + getUsername: () => 'username1', }; getUserInfoAsyncSpy = spyOn(userService, 'getUserInfoAsync'); - getUserInfoAsyncSpy.and.returnValue( - Promise.resolve(userInfo as UserInfo)); - spyOn(userService, 'getProfileImageDataUrl').and.returnValue( - ['default-image-url-png', 'default-image-url-webp']); + getUserInfoAsyncSpy.and.returnValue(Promise.resolve(userInfo as UserInfo)); + spyOn(userService, 'getProfileImageDataUrl').and.returnValue([ + 'default-image-url-png', + 'default-image-url-webp', + ]); component.ngOnInit(); }); it('should set focus on select lang field', fakeAsync(() => { - spyOn(userService, 'getUserContributionRightsDataAsync') - .and.returnValue(Promise.resolve(userContributionRights)); + spyOn(userService, 'getUserContributionRightsDataAsync').and.returnValue( + Promise.resolve(userContributionRights) + ); let focusSpy = spyOn(focusManagerService, 'setFocusWithoutScroll'); component.onTabClick('translateTextTab'); @@ -121,47 +137,51 @@ describe('Contributor dashboard page', () => { })); it('should throw error if contribution rights is null', fakeAsync(() => { - spyOn(userService, 'getUserContributionRightsDataAsync') - .and.returnValue(Promise.resolve(null)); + spyOn(userService, 'getUserContributionRightsDataAsync').and.returnValue( + Promise.resolve(null) + ); expect(() => { component.ngOnInit(); flush(); }).toThrowError(); })); - it('should set default profile pictures when username is null', - fakeAsync(() => { - spyOn(userService, 'getUserContributionRightsDataAsync') - .and.returnValue(Promise.resolve(userContributionRights)); - let userInfo = { - isLoggedIn: () => true, - getUsername: () => null - }; + it('should set default profile pictures when username is null', fakeAsync(() => { + spyOn(userService, 'getUserContributionRightsDataAsync').and.returnValue( + Promise.resolve(userContributionRights) + ); + let userInfo = { + isLoggedIn: () => true, + getUsername: () => null, + }; - getUserInfoAsyncSpy.and.returnValue( - Promise.resolve(userInfo as UserInfo)); + getUserInfoAsyncSpy.and.returnValue(Promise.resolve(userInfo as UserInfo)); - component.ngOnInit(); - flush(); + component.ngOnInit(); + flush(); - expect(component.profilePicturePngDataUrl).toBe( - urlInterpolationService.getStaticImageUrl( - AppConstants.DEFAULT_PROFILE_IMAGE_PNG_PATH)); - expect(component.profilePictureWebpDataUrl).toBe( - urlInterpolationService.getStaticImageUrl( - AppConstants.DEFAULT_PROFILE_IMAGE_WEBP_PATH)); - })); + expect(component.profilePicturePngDataUrl).toBe( + urlInterpolationService.getStaticImageUrl( + AppConstants.DEFAULT_PROFILE_IMAGE_PNG_PATH + ) + ); + expect(component.profilePictureWebpDataUrl).toBe( + urlInterpolationService.getStaticImageUrl( + AppConstants.DEFAULT_PROFILE_IMAGE_WEBP_PATH + ) + ); + })); it('should username equal to "" when user is not loggedIn', fakeAsync(() => { - spyOn(userService, 'getUserContributionRightsDataAsync') - .and.returnValue(Promise.resolve(userContributionRights)); + spyOn(userService, 'getUserContributionRightsDataAsync').and.returnValue( + Promise.resolve(userContributionRights) + ); let userInfo = { isLoggedIn: () => false, - getUsername: () => 'username1' + getUsername: () => 'username1', }; - getUserInfoAsyncSpy.and.returnValue( - Promise.resolve(userInfo as UserInfo)); + getUserInfoAsyncSpy.and.returnValue(Promise.resolve(userInfo as UserInfo)); component.ngOnInit(); flush(); @@ -170,93 +190,110 @@ describe('Contributor dashboard page', () => { expect(component.userIsLoggedIn).toBeFalse(); expect(component.profilePicturePngDataUrl).toBe( urlInterpolationService.getStaticImageUrl( - AppConstants.DEFAULT_PROFILE_IMAGE_PNG_PATH)); + AppConstants.DEFAULT_PROFILE_IMAGE_PNG_PATH + ) + ); expect(component.profilePictureWebpDataUrl).toBe( urlInterpolationService.getStaticImageUrl( - AppConstants.DEFAULT_PROFILE_IMAGE_WEBP_PATH)); + AppConstants.DEFAULT_PROFILE_IMAGE_WEBP_PATH + ) + ); })); describe('when user is logged in', () => { - it('should set specific properties after $onInit is called', - fakeAsync(() => { - spyOn(userService, 'getUserContributionRightsDataAsync') - .and.returnValue(Promise.resolve(userContributionRights)); - component.ngOnInit(); - flush(); - - expect(component.topicName).toBe('Topic 1'); - expect(translationTopicService.setActiveTopicName) - .toHaveBeenCalled(); - expect(component.activeTabName).toBe('myContributionTab'); - expect(component.OPPIA_AVATAR_IMAGE_URL).toBe( - '/assets/images/avatar/oppia_avatar_100px.svg'); - expect(component.profilePicturePngDataUrl).toEqual( - 'default-image-url-png'); - expect(component.profilePictureWebpDataUrl).toEqual( - 'default-image-url-webp'); - })); - - it('should set active topic name as default when no topics are returned', - fakeAsync(() => { - spyOn(userService, 'getUserContributionRightsDataAsync') - .and.returnValue(Promise.resolve(userContributionRights)); - getTranslatableTopicNamesAsyncSpy.and.returnValue( - Promise.resolve([])); - - component.ngOnInit(); - flush(); - - expect(component.topicName).toBeUndefined(); - expect(translationTopicService.setActiveTopicName).toHaveBeenCalled(); - })); + it('should set specific properties after $onInit is called', fakeAsync(() => { + spyOn(userService, 'getUserContributionRightsDataAsync').and.returnValue( + Promise.resolve(userContributionRights) + ); + component.ngOnInit(); + flush(); + + expect(component.topicName).toBe('Topic 1'); + expect(translationTopicService.setActiveTopicName).toHaveBeenCalled(); + expect(component.activeTabName).toBe('myContributionTab'); + expect(component.OPPIA_AVATAR_IMAGE_URL).toBe( + '/assets/images/avatar/oppia_avatar_100px.svg' + ); + expect(component.profilePicturePngDataUrl).toEqual( + 'default-image-url-png' + ); + expect(component.profilePictureWebpDataUrl).toEqual( + 'default-image-url-webp' + ); + })); + + it('should set active topic name as default when no topics are returned', fakeAsync(() => { + spyOn(userService, 'getUserContributionRightsDataAsync').and.returnValue( + Promise.resolve(userContributionRights) + ); + getTranslatableTopicNamesAsyncSpy.and.returnValue(Promise.resolve([])); + + component.ngOnInit(); + flush(); + + expect(component.topicName).toBeUndefined(); + expect(translationTopicService.setActiveTopicName).toHaveBeenCalled(); + })); it('should return language description in kebab case format', () => { - spyOn(userService, 'getUserContributionRightsDataAsync') - .and.returnValue(Promise.resolve(userContributionRights)); + spyOn(userService, 'getUserContributionRightsDataAsync').and.returnValue( + Promise.resolve(userContributionRights) + ); let languageDescription = 'Deutsch (German)'; - expect(component.provideLanguageForProtractorClass( - languageDescription)).toEqual('deutsch-german'); + expect( + component.provideLanguageForProtractorClass(languageDescription) + ).toEqual('deutsch-german'); }); - it('should initialize $scope properties after controller is initialized' + - ' and get data from backend', () => { - spyOn(userService, 'getUserContributionRightsDataAsync') - .and.returnValue(Promise.resolve(userContributionRights)); - expect(component.userIsLoggedIn).toBe(false); - expect(component.username).toBe(''); - expect(component.userCanReviewQuestions).toBe(false); - expect(component.userIsReviewer).toBe(false); - }); - - it('should change active tab name when clicking on translate text tab', - () => { - spyOn(userService, 'getUserContributionRightsDataAsync') - .and.returnValue(Promise.resolve(userContributionRights)); - let changedTab = 'translateTextTab'; - expect(component.activeTabName).toBe('myContributionTab'); - component.onTabClick(changedTab); - expect(component.activeTabName).toBe(changedTab); - }); - - it('should change active language when clicking on language selector', + it( + 'should initialize $scope properties after controller is initialized' + + ' and get data from backend', () => { - spyOn(userService, 'getUserContributionRightsDataAsync') - .and.returnValue(Promise.resolve(userContributionRights)); - spyOn(localStorageService, 'updateLastSelectedTranslationLanguageCode') - .and.callThrough(); - - component.onChangeLanguage('hi'); + spyOn( + userService, + 'getUserContributionRightsDataAsync' + ).and.returnValue(Promise.resolve(userContributionRights)); + expect(component.userIsLoggedIn).toBe(false); + expect(component.username).toBe(''); + expect(component.userCanReviewQuestions).toBe(false); + expect(component.userIsReviewer).toBe(false); + } + ); + + it('should change active tab name when clicking on translate text tab', () => { + spyOn(userService, 'getUserContributionRightsDataAsync').and.returnValue( + Promise.resolve(userContributionRights) + ); + let changedTab = 'translateTextTab'; + expect(component.activeTabName).toBe('myContributionTab'); + component.onTabClick(changedTab); + expect(component.activeTabName).toBe(changedTab); + }); - expect(translationLanguageService.setActiveLanguageCode) - .toHaveBeenCalledWith('hi'); - expect(localStorageService.updateLastSelectedTranslationLanguageCode) - .toHaveBeenCalledWith('hi'); - }); + it('should change active language when clicking on language selector', () => { + spyOn(userService, 'getUserContributionRightsDataAsync').and.returnValue( + Promise.resolve(userContributionRights) + ); + spyOn( + localStorageService, + 'updateLastSelectedTranslationLanguageCode' + ).and.callThrough(); + + component.onChangeLanguage('hi'); + + expect( + translationLanguageService.setActiveLanguageCode + ).toHaveBeenCalledWith('hi'); + expect( + localStorageService.updateLastSelectedTranslationLanguageCode + ).toHaveBeenCalledWith('hi'); + }); it('should show language selector based on active tab', () => { - spyOn(userService, 'getUserContributionRightsDataAsync') - .and.returnValue(Promise.resolve(userContributionRights)); + spyOn(userService, 'getUserContributionRightsDataAsync').and.returnValue( + Promise.resolve(userContributionRights) + ); let changedTab = 'translateTextTab'; expect(component.activeTabName).toBe('myContributionTab'); @@ -267,24 +304,29 @@ describe('Contributor dashboard page', () => { expect(component.showLanguageSelector()).toBe(true); }); - it('should change active topic when clicking on topic selector', - () => { - spyOn(userService, 'getUserContributionRightsDataAsync') - .and.returnValue(Promise.resolve(userContributionRights)); - spyOn(localStorageService, 'updateLastSelectedTranslationTopicName') - .and.callThrough(); - - component.onChangeTopic('Topic 2'); - - expect(translationTopicService.setActiveTopicName) - .toHaveBeenCalledWith('Topic 2'); - expect(localStorageService.updateLastSelectedTranslationTopicName) - .toHaveBeenCalledWith('Topic 2'); - }); + it('should change active topic when clicking on topic selector', () => { + spyOn(userService, 'getUserContributionRightsDataAsync').and.returnValue( + Promise.resolve(userContributionRights) + ); + spyOn( + localStorageService, + 'updateLastSelectedTranslationTopicName' + ).and.callThrough(); + + component.onChangeTopic('Topic 2'); + + expect(translationTopicService.setActiveTopicName).toHaveBeenCalledWith( + 'Topic 2' + ); + expect( + localStorageService.updateLastSelectedTranslationTopicName + ).toHaveBeenCalledWith('Topic 2'); + }); it('should show topic selector based on active tab', () => { - spyOn(userService, 'getUserContributionRightsDataAsync') - .and.returnValue(Promise.resolve(userContributionRights)); + spyOn(userService, 'getUserContributionRightsDataAsync').and.returnValue( + Promise.resolve(userContributionRights) + ); let changedTab = 'translateTextTab'; expect(component.activeTabName).toBe('myContributionTab'); @@ -296,12 +338,16 @@ describe('Contributor dashboard page', () => { }); it('should show topic selector for questions reviews', () => { - spyOn(userService, 'getUserContributionRightsDataAsync') - .and.returnValue(Promise.resolve(userContributionRights)); - spyOn(contributionAndReviewService, 'getActiveSuggestionType') - .and.returnValue('add_question'); - spyOn(contributionAndReviewService, 'getActiveTabType') - .and.returnValue('reviews'); + spyOn(userService, 'getUserContributionRightsDataAsync').and.returnValue( + Promise.resolve(userContributionRights) + ); + spyOn( + contributionAndReviewService, + 'getActiveSuggestionType' + ).and.returnValue('add_question'); + spyOn(contributionAndReviewService, 'getActiveTabType').and.returnValue( + 'reviews' + ); let changedTab = 'myContributionTab'; component.onTabClick(changedTab); diff --git a/core/templates/pages/contributor-dashboard-page/contributor-dashboard-page.component.ts b/core/templates/pages/contributor-dashboard-page/contributor-dashboard-page.component.ts index 4e9c5b79296f..29b01a629457 100644 --- a/core/templates/pages/contributor-dashboard-page/contributor-dashboard-page.component.ts +++ b/core/templates/pages/contributor-dashboard-page/contributor-dashboard-page.component.ts @@ -16,26 +16,28 @@ * @fileoverview Component for the contributor dashboard page. */ -import { AppConstants } from 'app.constants'; -import { Component, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { LanguageUtilService } from 'domain/utilities/language-util.service'; -import { ContributorDashboardConstants, ContributorDashboardTabsDetails } from 'pages/contributor-dashboard-page/contributor-dashboard-page.constants'; -import { ContributionAndReviewService } from './services/contribution-and-review.service'; -import { ContributionOpportunitiesService } from './services/contribution-opportunities.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { LocalStorageService } from 'services/local-storage.service'; -import { TranslationLanguageService } from 'pages/exploration-editor-page/translation-tab/services/translation-language.service'; -import { TranslationTopicService } from 'pages/exploration-editor-page/translation-tab/services/translation-topic.service'; -import { UserService } from 'services/user.service'; +import {AppConstants} from 'app.constants'; +import {Component, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {LanguageUtilService} from 'domain/utilities/language-util.service'; +import { + ContributorDashboardConstants, + ContributorDashboardTabsDetails, +} from 'pages/contributor-dashboard-page/contributor-dashboard-page.constants'; +import {ContributionAndReviewService} from './services/contribution-and-review.service'; +import {ContributionOpportunitiesService} from './services/contribution-opportunities.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {LocalStorageService} from 'services/local-storage.service'; +import {TranslationLanguageService} from 'pages/exploration-editor-page/translation-tab/services/translation-language.service'; +import {TranslationTopicService} from 'pages/exploration-editor-page/translation-tab/services/translation-topic.service'; +import {UserService} from 'services/user.service'; @Component({ selector: 'contributor-dashboard-page', - templateUrl: './contributor-dashboard-page.component.html' + templateUrl: './contributor-dashboard-page.component.html', }) -export class ContributorDashboardPageComponent - implements OnInit { +export class ContributorDashboardPageComponent implements OnInit { // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -66,7 +68,7 @@ export class ContributorDashboardPageComponent private translationLanguageService: TranslationLanguageService, private translationTopicService: TranslationTopicService, private urlInterpolationService: UrlInterpolationService, - private userService: UserService, + private userService: UserService ) {} onTabClick(activeTabName: string): void { @@ -90,12 +92,15 @@ export class ContributorDashboardPageComponent this.languageCode = languageCode; this.translationLanguageService.setActiveLanguageCode(this.languageCode); this.localStorageService.updateLastSelectedTranslationLanguageCode( - this.languageCode); + this.languageCode + ); } showLanguageSelector(): boolean { - const activeTabDetail = this.tabsDetails[ - this.activeTabName as keyof ContributorDashboardTabsDetails]; + const activeTabDetail = + this.tabsDetails[ + this.activeTabName as keyof ContributorDashboardTabsDetails + ]; return activeTabDetail.customizationOptions.includes('language'); } @@ -103,38 +108,41 @@ export class ContributorDashboardPageComponent this.topicName = topicName; this.translationTopicService.setActiveTopicName(this.topicName); this.localStorageService.updateLastSelectedTranslationTopicName( - this.topicName); + this.topicName + ); } showTopicSelector(): boolean { - const activeTabDetail = this.tabsDetails[ - this.activeTabName as keyof ContributorDashboardTabsDetails]; + const activeTabDetail = + this.tabsDetails[ + this.activeTabName as keyof ContributorDashboardTabsDetails + ]; const activeSuggestionType = this.contributionAndReviewService.getActiveSuggestionType(); const activeTabType = this.contributionAndReviewService.getActiveTabType(); - const userIsReviewingQuestionSuggestions = ( + const userIsReviewingQuestionSuggestions = activeTabType === 'reviews' && activeSuggestionType === 'add_question' && - this.activeTabName !== 'submitQuestionTab' - ); - const userIsReviewingTranslationSuggestions = ( + this.activeTabName !== 'submitQuestionTab'; + const userIsReviewingTranslationSuggestions = activeTabType === 'reviews' && activeSuggestionType === 'translate_content' && - this.activeTabName !== 'submitQuestionTab' - ); + this.activeTabName !== 'submitQuestionTab'; - return activeTabDetail.customizationOptions.includes('topic') || + return ( + activeTabDetail.customizationOptions.includes('topic') || userIsReviewingQuestionSuggestions || - userIsReviewingTranslationSuggestions; + userIsReviewingTranslationSuggestions + ); } getLanguageDescriptions(languageCodes: string[]): string[] { const languageDescriptions: string[] = []; - languageCodes.forEach((languageCode) => { + languageCodes.forEach(languageCode => { languageDescriptions.push( - this.languageUtilService.getAudioLanguageDescription( - languageCode)); + this.languageUtilService.getAudioLanguageDescription(languageCode) + ); }); return languageDescriptions; } @@ -149,52 +157,53 @@ export class ContributorDashboardPageComponent this.userCanReviewQuestions = false; this.defaultHeaderVisible = true; - const prevSelectedTopicName = ( - this.localStorageService.getLastSelectedTranslationTopicName()); + const prevSelectedTopicName = + this.localStorageService.getLastSelectedTranslationTopicName(); - this.userService.getUserContributionRightsDataAsync().then( - (userContributionRights) => { + this.userService + .getUserContributionRightsDataAsync() + .then(userContributionRights => { if (userContributionRights === null) { throw new Error('User contribution rights not found.'); } - this.userCanReviewTranslationSuggestionsInLanguages = ( + this.userCanReviewTranslationSuggestionsInLanguages = this.getLanguageDescriptions( - userContributionRights - .can_review_translation_for_language_codes)); + userContributionRights.can_review_translation_for_language_codes + ); - this.userCanReviewVoiceoverSuggestionsInLanguages = ( + this.userCanReviewVoiceoverSuggestionsInLanguages = this.getLanguageDescriptions( - userContributionRights - .can_review_voiceover_for_language_codes)); + userContributionRights.can_review_voiceover_for_language_codes + ); - this.userCanReviewQuestions = ( - userContributionRights.can_review_questions); + this.userCanReviewQuestions = + userContributionRights.can_review_questions; - this.userIsReviewer = ( - this.userCanReviewTranslationSuggestionsInLanguages - .length > 0 || - this.userCanReviewVoiceoverSuggestionsInLanguages - .length > 0 || - this.userCanReviewQuestions); + this.userIsReviewer = + this.userCanReviewTranslationSuggestionsInLanguages.length > 0 || + this.userCanReviewVoiceoverSuggestionsInLanguages.length > 0 || + this.userCanReviewQuestions; - this.tabsDetails.submitQuestionTab.enabled = ( - userContributionRights.can_suggest_questions); + this.tabsDetails.submitQuestionTab.enabled = + userContributionRights.can_suggest_questions; }); - this.userService.getUserInfoAsync().then((userInfo) => { + this.userService.getUserInfoAsync().then(userInfo => { this.userInfoIsLoading = false; - this.profilePictureWebpDataUrl = ( + this.profilePictureWebpDataUrl = this.urlInterpolationService.getStaticImageUrl( - AppConstants.DEFAULT_PROFILE_IMAGE_WEBP_PATH)); - this.profilePicturePngDataUrl = ( + AppConstants.DEFAULT_PROFILE_IMAGE_WEBP_PATH + ); + this.profilePicturePngDataUrl = this.urlInterpolationService.getStaticImageUrl( - AppConstants.DEFAULT_PROFILE_IMAGE_PNG_PATH)); + AppConstants.DEFAULT_PROFILE_IMAGE_PNG_PATH + ); if (userInfo.isLoggedIn()) { this.userIsLoggedIn = true; this.username = userInfo.getUsername(); if (this.username !== null) { - [this.profilePicturePngDataUrl, this.profilePictureWebpDataUrl] = ( - this.userService.getProfileImageDataUrl(this.username)); + [this.profilePicturePngDataUrl, this.profilePictureWebpDataUrl] = + this.userService.getProfileImageDataUrl(this.username); } } else { this.userIsLoggedIn = false; @@ -202,12 +211,14 @@ export class ContributorDashboardPageComponent } }); - this.contributionOpportunitiesService.getTranslatableTopicNamesAsync() - .then((topicNames) => { + this.contributionOpportunitiesService + .getTranslatableTopicNamesAsync() + .then(topicNames => { // TODO(#15710): Set default active topic to 'All'. if (topicNames.length <= 0) { this.translationTopicService.setActiveTopicName( - ContributorDashboardConstants.DEFAULT_OPPORTUNITY_TOPIC_NAME); + ContributorDashboardConstants.DEFAULT_OPPORTUNITY_TOPIC_NAME + ); return; } this.topicName = topicNames[0]; @@ -223,17 +234,20 @@ export class ContributorDashboardPageComponent this.activeTabName = 'myContributionTab'; this.tabsDetails = { - ...ContributorDashboardConstants.CONTRIBUTOR_DASHBOARD_TABS_DETAILS - // TODO(#13015): Remove use of unknown as a type. + ...ContributorDashboardConstants.CONTRIBUTOR_DASHBOARD_TABS_DETAILS, + // TODO(#13015): Remove use of unknown as a type. } as unknown as ContributorDashboardTabsDetails; - this.OPPIA_AVATAR_IMAGE_URL = ( + this.OPPIA_AVATAR_IMAGE_URL = this.urlInterpolationService.getStaticImageUrl( - '/avatar/oppia_avatar_100px.svg')); + '/avatar/oppia_avatar_100px.svg' + ); this.languageCode = this.translationLanguageService.getActiveLanguageCode(); } } -angular.module('oppia').directive('contributorDashboardPage', +angular.module('oppia').directive( + 'contributorDashboardPage', downgradeComponent({ - component: ContributorDashboardPageComponent - }) as angular.IDirectiveFactory); + component: ContributorDashboardPageComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/contributor-dashboard-page/contributor-dashboard-page.constants.ajs.spec.ts b/core/templates/pages/contributor-dashboard-page/contributor-dashboard-page.constants.ajs.spec.ts index a238340e2519..93f68222c15f 100644 --- a/core/templates/pages/contributor-dashboard-page/contributor-dashboard-page.constants.ajs.spec.ts +++ b/core/templates/pages/contributor-dashboard-page/contributor-dashboard-page.constants.ajs.spec.ts @@ -18,35 +18,39 @@ // TODO(#7222): Remove the following block of unnnecessary imports once // the code corresponding to the spec is upgraded to Angular 8. -import { UpgradedServices } from 'services/UpgradedServices'; +import {UpgradedServices} from 'services/UpgradedServices'; // ^^^ This block is to be removed. -require( - // eslint-disable-next-line max-len - 'pages/contributor-dashboard-page/contributor-dashboard-page.constants.ajs.ts'); +require(// eslint-disable-next-line max-len +'pages/contributor-dashboard-page/contributor-dashboard-page.constants.ajs.ts'); -describe('Contributor dashboard page constants', function() { +describe('Contributor dashboard page constants', function () { var CONTRIBUTOR_DASHBOARD_TABS_DETAILS = null; var tabDetailsTemplate = { ariaLabel: 'string', tabName: 'string', description: 'string', - customizationOptions: 'array' + customizationOptions: 'array', }; beforeEach(angular.mock.module('oppia')); - beforeEach(angular.mock.module('oppia', function($provide) { - var ugs = new UpgradedServices(); - for (let [key, value] of Object.entries(ugs.getUpgradedServices())) { - $provide.value(key, value); - } - })); - beforeEach(angular.mock.inject(function($injector) { - CONTRIBUTOR_DASHBOARD_TABS_DETAILS = $injector.get( - 'CONTRIBUTOR_DASHBOARD_TABS_DETAILS'); - })); + beforeEach( + angular.mock.module('oppia', function ($provide) { + var ugs = new UpgradedServices(); + for (let [key, value] of Object.entries(ugs.getUpgradedServices())) { + $provide.value(key, value); + } + }) + ); + beforeEach( + angular.mock.inject(function ($injector) { + CONTRIBUTOR_DASHBOARD_TABS_DETAILS = $injector.get( + 'CONTRIBUTOR_DASHBOARD_TABS_DETAILS' + ); + }) + ); - it('should have expected template for tab details', function() { + it('should have expected template for tab details', function () { for (var tabName in CONTRIBUTOR_DASHBOARD_TABS_DETAILS) { var tabDetails = CONTRIBUTOR_DASHBOARD_TABS_DETAILS[tabName]; for (var infoKey in tabDetailsTemplate) { diff --git a/core/templates/pages/contributor-dashboard-page/contributor-dashboard-page.constants.ajs.ts b/core/templates/pages/contributor-dashboard-page/contributor-dashboard-page.constants.ajs.ts index 444a0e5ddedb..8bf0868ce26a 100644 --- a/core/templates/pages/contributor-dashboard-page/contributor-dashboard-page.constants.ajs.ts +++ b/core/templates/pages/contributor-dashboard-page/contributor-dashboard-page.constants.ajs.ts @@ -18,22 +18,32 @@ // TODO(#7092): Delete this file once migration is complete and these AngularJS // equivalents of the Angular constants are no longer needed. -import { ContributorDashboardConstants } from - 'pages/contributor-dashboard-page/contributor-dashboard-page.constants'; +import {ContributorDashboardConstants} from 'pages/contributor-dashboard-page/contributor-dashboard-page.constants'; -angular.module('oppia').constant( - 'CONTRIBUTOR_DASHBOARD_TABS_DETAILS', - ContributorDashboardConstants.CONTRIBUTOR_DASHBOARD_TABS_DETAILS -); +angular + .module('oppia') + .constant( + 'CONTRIBUTOR_DASHBOARD_TABS_DETAILS', + ContributorDashboardConstants.CONTRIBUTOR_DASHBOARD_TABS_DETAILS + ); -angular.module('oppia').constant( - 'CORRESPONDING_DELETED_OPPORTUNITY_TEXT', - ContributorDashboardConstants.CORRESPONDING_DELETED_OPPORTUNITY_TEXT); +angular + .module('oppia') + .constant( + 'CORRESPONDING_DELETED_OPPORTUNITY_TEXT', + ContributorDashboardConstants.CORRESPONDING_DELETED_OPPORTUNITY_TEXT + ); -angular.module('oppia').constant( - 'DEFAULT_OPPORTUNITY_LANGUAGE_CODE', - ContributorDashboardConstants.DEFAULT_OPPORTUNITY_LANGUAGE_CODE); +angular + .module('oppia') + .constant( + 'DEFAULT_OPPORTUNITY_LANGUAGE_CODE', + ContributorDashboardConstants.DEFAULT_OPPORTUNITY_LANGUAGE_CODE + ); -angular.module('oppia').constant( - 'DEFAULT_OPPORTUNITY_TOPIC_NAME', - ContributorDashboardConstants.DEFAULT_OPPORTUNITY_TOPIC_NAME); +angular + .module('oppia') + .constant( + 'DEFAULT_OPPORTUNITY_TOPIC_NAME', + ContributorDashboardConstants.DEFAULT_OPPORTUNITY_TOPIC_NAME + ); diff --git a/core/templates/pages/contributor-dashboard-page/contributor-dashboard-page.constants.ts b/core/templates/pages/contributor-dashboard-page/contributor-dashboard-page.constants.ts index 10867525e3a7..75d10b3a79bf 100644 --- a/core/templates/pages/contributor-dashboard-page/contributor-dashboard-page.constants.ts +++ b/core/templates/pages/contributor-dashboard-page/contributor-dashboard-page.constants.ts @@ -37,31 +37,33 @@ export const ContributorDashboardConstants = { tabName: 'My Contributions', description: '', customizationOptions: [], - enabled: true + enabled: true, }, submitQuestionTab: { ariaLabel: 'See opportunities for adding new questions.', tabName: 'Submit Question', - description: 'Submit a question for students to answer while ' + + description: + 'Submit a question for students to answer while ' + 'practicing that skill.', customizationOptions: ['sort'], - enabled: false + enabled: false, }, translateTextTab: { ariaLabel: 'See opportunities for translation.', tabName: 'Translate Text', - description: 'Translate the lesson text to help non-English speakers ' + + description: + 'Translate the lesson text to help non-English speakers ' + 'follow the lessons.', customizationOptions: ['language', 'topic', 'sort'], - enabled: true - } + enabled: true, + }, }, // The text to display for a submitted suggestion if its corresponding // opportunity was deleted. - CORRESPONDING_DELETED_OPPORTUNITY_TEXT: '[The corresponding opportunity ' + - 'has been deleted.]', + CORRESPONDING_DELETED_OPPORTUNITY_TEXT: + '[The corresponding opportunity ' + 'has been deleted.]', DEFAULT_OPPORTUNITY_LANGUAGE_CODE: 'hi', - DEFAULT_OPPORTUNITY_TOPIC_NAME: 'All' + DEFAULT_OPPORTUNITY_TOPIC_NAME: 'All', } as const; diff --git a/core/templates/pages/contributor-dashboard-page/contributor-dashboard-page.import.ts b/core/templates/pages/contributor-dashboard-page/contributor-dashboard-page.import.ts index d7f02ed8d505..6868150071d2 100644 --- a/core/templates/pages/contributor-dashboard-page/contributor-dashboard-page.import.ts +++ b/core/templates/pages/contributor-dashboard-page/contributor-dashboard-page.import.ts @@ -24,15 +24,20 @@ import ngInfiniteScroll from 'ng-infinite-scroll'; import 'third-party-imports/ui-tree.import'; angular.module('oppia', [ - require('angular-cookies'), 'ngAnimate', ngInfiniteScroll, - 'ngMaterial', 'ngSanitize', 'ngTouch', 'pascalprecht.translate', - 'ui.bootstrap', 'ui.tree', uiValidate + require('angular-cookies'), + 'ngAnimate', + ngInfiniteScroll, + 'ngMaterial', + 'ngSanitize', + 'ngTouch', + 'pascalprecht.translate', + 'ui.bootstrap', + 'ui.tree', + uiValidate, ]); -require( - 'pages/contributor-dashboard-page/contributor-dashboard-page.module.ts'); +require('pages/contributor-dashboard-page/contributor-dashboard-page.module.ts'); require('App.ts'); require('base-components/oppia-root.directive.ts'); -require( - 'pages/contributor-dashboard-page/contributor-dashboard-page.component.ts'); +require('pages/contributor-dashboard-page/contributor-dashboard-page.component.ts'); diff --git a/core/templates/pages/contributor-dashboard-page/contributor-dashboard-page.module.ts b/core/templates/pages/contributor-dashboard-page/contributor-dashboard-page.module.ts index a23d7d46333c..808edd207503 100644 --- a/core/templates/pages/contributor-dashboard-page/contributor-dashboard-page.module.ts +++ b/core/templates/pages/contributor-dashboard-page/contributor-dashboard-page.module.ts @@ -16,43 +16,42 @@ * @fileoverview Module for the contributor dashboard page. */ -import { APP_INITIALIZER, NgModule, StaticProvider } from '@angular/core'; -import { BrowserModule, HAMMER_GESTURE_CONFIG } from '@angular/platform-browser'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { HttpClientModule } from '@angular/common/http'; -import { HTTP_INTERCEPTORS } from '@angular/common/http'; -import { RequestInterceptor } from 'services/request-interceptor.service'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { OppiaAngularRootComponent } from - 'components/oppia-angular-root.component'; -import { RouterModule } from '@angular/router'; -import { APP_BASE_HREF } from '@angular/common'; +import {APP_INITIALIZER, NgModule, StaticProvider} from '@angular/core'; +import {BrowserModule, HAMMER_GESTURE_CONFIG} from '@angular/platform-browser'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {HttpClientModule} from '@angular/common/http'; +import {HTTP_INTERCEPTORS} from '@angular/common/http'; +import {RequestInterceptor} from 'services/request-interceptor.service'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {OppiaAngularRootComponent} from 'components/oppia-angular-root.component'; +import {RouterModule} from '@angular/router'; +import {APP_BASE_HREF} from '@angular/common'; -import { InteractionExtensionsModule } from 'interactions/interactions.module'; -import { TranslationLanguageSelectorComponent } from - './translation-language-selector/translation-language-selector.component'; -import { ReviewTranslationLanguageSelectorComponent } from './translation-language-selector/review-translation-language-selector.component'; -import { TranslationTopicSelectorComponent } from - './translation-topic-selector/translation-topic-selector.component'; -import { LoginRequiredMessageComponent } from './login-required-message/login-required-message.component'; -import { LoginRequiredModalContent } from './modal-templates/login-required-modal.component'; -import { SmartRouterModule } from 'hybrid-router-module-provider'; +import {InteractionExtensionsModule} from 'interactions/interactions.module'; +import {TranslationLanguageSelectorComponent} from './translation-language-selector/translation-language-selector.component'; +import {ReviewTranslationLanguageSelectorComponent} from './translation-language-selector/review-translation-language-selector.component'; +import {TranslationTopicSelectorComponent} from './translation-topic-selector/translation-topic-selector.component'; +import {LoginRequiredMessageComponent} from './login-required-message/login-required-message.component'; +import {LoginRequiredModalContent} from './modal-templates/login-required-modal.component'; +import {SmartRouterModule} from 'hybrid-router-module-provider'; -import { OpportunitiesListItemComponent } from './opportunities-list-item/opportunities-list-item.component'; -import { OpportunitiesListComponent } from './opportunities-list/opportunities-list.component'; -import { TranslationSuggestionReviewModalComponent } from './modal-templates/translation-suggestion-review-modal.component'; -import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap'; -import { platformFeatureInitFactory, PlatformFeatureService } from - 'services/platform-feature.service'; -import { TranslationModalComponent } from './modal-templates/translation-modal.component'; -import { TranslationOpportunitiesComponent } from './translation-opportunities/translation-opportunities.component'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { MyHammerConfig, toastrConfig } from 'pages/oppia-root/app.module'; -import { ContributionsAndReview } from './contributions-and-review/contributions-and-review.component'; -import { QuestionOpportunitiesComponent } from './question-opportunities/question-opportunities.component'; -import { ContributorDashboardPageComponent } from './contributor-dashboard-page.component'; -import { AppErrorHandlerProvider } from 'pages/oppia-root/app-error-handler'; -import { MatSnackBarModule } from '@angular/material/snack-bar'; +import {OpportunitiesListItemComponent} from './opportunities-list-item/opportunities-list-item.component'; +import {OpportunitiesListComponent} from './opportunities-list/opportunities-list.component'; +import {TranslationSuggestionReviewModalComponent} from './modal-templates/translation-suggestion-review-modal.component'; +import {NgbModalModule} from '@ng-bootstrap/ng-bootstrap'; +import { + platformFeatureInitFactory, + PlatformFeatureService, +} from 'services/platform-feature.service'; +import {TranslationModalComponent} from './modal-templates/translation-modal.component'; +import {TranslationOpportunitiesComponent} from './translation-opportunities/translation-opportunities.component'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {MyHammerConfig, toastrConfig} from 'pages/oppia-root/app.module'; +import {ContributionsAndReview} from './contributions-and-review/contributions-and-review.component'; +import {QuestionOpportunitiesComponent} from './question-opportunities/question-opportunities.component'; +import {ContributorDashboardPageComponent} from './contributor-dashboard-page.component'; +import {AppErrorHandlerProvider} from 'pages/oppia-root/app-error-handler'; +import {MatSnackBarModule} from '@angular/material/snack-bar'; @NgModule({ imports: [ @@ -89,7 +88,7 @@ import { MatSnackBarModule } from '@angular/material/snack-bar'; ContributionsAndReview, QuestionOpportunitiesComponent, ContributorDashboardPageComponent, - ContributorBadgesComponent + ContributorBadgesComponent, ], entryComponents: [ CertificateDownloadModalComponent, @@ -108,47 +107,47 @@ import { MatSnackBarModule } from '@angular/material/snack-bar'; TranslationModalComponent, ContributionsAndReview, QuestionOpportunitiesComponent, - ContributorDashboardPageComponent + ContributorDashboardPageComponent, ], providers: [ { provide: HTTP_INTERCEPTORS, useClass: RequestInterceptor, - multi: true + multi: true, }, { provide: APP_INITIALIZER, useFactory: platformFeatureInitFactory, deps: [PlatformFeatureService], - multi: true + multi: true, }, { provide: HAMMER_GESTURE_CONFIG, - useClass: MyHammerConfig + useClass: MyHammerConfig, }, AppErrorHandlerProvider, { provide: APP_BASE_HREF, - useValue: '/' - } - ] + useValue: '/', + }, + ], }) class ContributorDashboardPageModule { // Empty placeholder method to satisfy the `Compiler`. ngDoBootstrap() {} } -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { downgradeModule } from '@angular/upgrade/static'; -import { SharedFormsModule } from 'components/forms/shared-forms.module'; -import { ToastrModule } from 'ngx-toastr'; -import { OppiaCkEditorCopyToolBarModule } from 'components/ck-editor-helpers/ck-editor-copy-toolbar/ck-editor-copy-toolbar.module'; -import { ContributorStatsComponent } from './contributor-stats/contributor-stats.component'; -import { CertificateDownloadModalComponent } from './modal-templates/certificate-download-modal.component'; -import { ContributorBadgesComponent } from './contributor-badges/contributor-badges.component'; -import { BadgeComponent } from './badge/badge.component'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {downgradeModule} from '@angular/upgrade/static'; +import {SharedFormsModule} from 'components/forms/shared-forms.module'; +import {ToastrModule} from 'ngx-toastr'; +import {OppiaCkEditorCopyToolBarModule} from 'components/ck-editor-helpers/ck-editor-copy-toolbar/ck-editor-copy-toolbar.module'; +import {ContributorStatsComponent} from './contributor-stats/contributor-stats.component'; +import {CertificateDownloadModalComponent} from './modal-templates/certificate-download-modal.component'; +import {ContributorBadgesComponent} from './contributor-badges/contributor-badges.component'; +import {BadgeComponent} from './badge/badge.component'; -const bootstrapFnAsync = async(extraProviders: StaticProvider[]) => { +const bootstrapFnAsync = async (extraProviders: StaticProvider[]) => { const platformRef = platformBrowserDynamic(extraProviders); return platformRef.bootstrapModule(ContributorDashboardPageModule); }; @@ -163,5 +162,6 @@ angular.module('oppia').directive( // bootstrap the Angular 8. 'oppiaAngularRoot', downgradeComponent({ - component: OppiaAngularRootComponent - }) as angular.IDirectiveFactory); + component: OppiaAngularRootComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/contributor-dashboard-page/contributor-stats/contributor-stats.component.spec.ts b/core/templates/pages/contributor-dashboard-page/contributor-stats/contributor-stats.component.spec.ts index 0ae23f50ac7e..3792a06ffc4b 100644 --- a/core/templates/pages/contributor-dashboard-page/contributor-stats/contributor-stats.component.spec.ts +++ b/core/templates/pages/contributor-dashboard-page/contributor-stats/contributor-stats.component.spec.ts @@ -16,16 +16,27 @@ * @fileoverview Unit tests for ContributorStatsComponent. */ -import { ComponentFixture, fakeAsync, flush, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { UserService } from 'services/user.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { UserInfo } from 'domain/user/user-info.model'; -import { ContributorStatsComponent } from './contributor-stats.component'; -import { ContributionAndReviewStatsService } from '../services/contribution-and-review-stats.service'; -import { LanguageUtilService } from 'domain/utilities/language-util.service'; -import { NgbActiveModal, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { CertificateDownloadModalComponent } from '../modal-templates/certificate-download-modal.component'; +import { + ComponentFixture, + fakeAsync, + flush, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {UserService} from 'services/user.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {UserInfo} from 'domain/user/user-info.model'; +import {ContributorStatsComponent} from './contributor-stats.component'; +import {ContributionAndReviewStatsService} from '../services/contribution-and-review-stats.service'; +import {LanguageUtilService} from 'domain/utilities/language-util.service'; +import { + NgbActiveModal, + NgbModal, + NgbModalRef, +} from '@ng-bootstrap/ng-bootstrap'; +import {CertificateDownloadModalComponent} from '../modal-templates/certificate-download-modal.component'; describe('Contributor stats component', () => { let fetchAllContributionAndReviewStatsAsync: jasmine.Spy; @@ -46,7 +57,7 @@ describe('Contributor stats component', () => { rejected_translations_count: 0, rejected_translation_word_count: 0, first_contribution_date: 'Mar 2021', - last_contribution_date: 'Mar 2021' + last_contribution_date: 'Mar 2021', }; const translationContributionStatTopic2 = { language_code: 'es', @@ -59,7 +70,7 @@ describe('Contributor stats component', () => { rejected_translations_count: 0, rejected_translation_word_count: 0, first_contribution_date: 'Mar 2021', - last_contribution_date: 'Mar 2021' + last_contribution_date: 'Mar 2021', }; const translationReviewStatTopic1 = { language_code: 'es', @@ -70,7 +81,7 @@ describe('Contributor stats component', () => { accepted_translations_with_reviewer_edits_count: 0, accepted_translation_word_count: 1, first_contribution_date: 'Mar 2021', - last_contribution_date: 'Mar 2021' + last_contribution_date: 'Mar 2021', }; const translationReviewStatTopic2 = { language_code: 'es', @@ -81,7 +92,7 @@ describe('Contributor stats component', () => { accepted_translations_with_reviewer_edits_count: 0, accepted_translation_word_count: 1, first_contribution_date: 'Mar 2021', - last_contribution_date: 'Mar 2021' + last_contribution_date: 'Mar 2021', }; const questionContributionStat = { topic_name: 'published_topic_name', @@ -89,7 +100,7 @@ describe('Contributor stats component', () => { accepted_questions_count: 1, accepted_questions_without_reviewer_edits_count: 0, first_contribution_date: 'Mar 2021', - last_contribution_date: 'Mar 2021' + last_contribution_date: 'Mar 2021', }; const questionReviewStat = { topic_name: 'published_topic_name', @@ -97,16 +108,20 @@ describe('Contributor stats component', () => { accepted_questions_count: 1, accepted_questions_with_reviewer_edits_count: 1, first_contribution_date: 'Mar 2021', - last_contribution_date: 'Mar 2021' + last_contribution_date: 'Mar 2021', }; const fetchAllStatsResponse = { translation_contribution_stats: [ - translationContributionStatTopic1, translationContributionStatTopic2], + translationContributionStatTopic1, + translationContributionStatTopic2, + ], translation_review_stats: [ - translationReviewStatTopic1, translationReviewStatTopic2], + translationReviewStatTopic1, + translationReviewStatTopic2, + ], question_contribution_stats: [questionContributionStat], - question_review_stats: [questionReviewStat] + question_review_stats: [questionReviewStat], }; let component: ContributorStatsComponent; let fixture: ComponentFixture; @@ -121,16 +136,16 @@ describe('Contributor stats component', () => { imports: [HttpClientTestingModule], declarations: [ ContributorStatsComponent, - CertificateDownloadModalComponent + CertificateDownloadModalComponent, ], providers: [ ContributionAndReviewStatsService, LanguageUtilService, UserService, NgbModal, - NgbActiveModal + NgbActiveModal, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -139,9 +154,11 @@ describe('Contributor stats component', () => { component = fixture.componentInstance; contributionAndReviewStatsService = TestBed.inject( - ContributionAndReviewStatsService); + ContributionAndReviewStatsService + ); certificateModal = TestBed.createComponent( - CertificateDownloadModalComponent) as unknown as NgbModalRef; + CertificateDownloadModalComponent + ) as unknown as NgbModalRef; languageUtilService = TestBed.inject(LanguageUtilService); userService = TestBed.inject(UserService); modalService = TestBed.inject(NgbModal); @@ -149,12 +166,14 @@ describe('Contributor stats component', () => { fetchAllContributionAndReviewStatsAsync = spyOn( contributionAndReviewStatsService, - 'fetchAllStats'); + 'fetchAllStats' + ); fetchAllContributionAndReviewStatsAsync.and.returnValue( - Promise.resolve(fetchAllStatsResponse)); - spyOn( - languageUtilService, 'getAudioLanguageDescription') - .and.returnValue('audio_language_description'); + Promise.resolve(fetchAllStatsResponse) + ); + spyOn(languageUtilService, 'getAudioLanguageDescription').and.returnValue( + 'audio_language_description' + ); fixture.detectChanges(); @@ -167,13 +186,15 @@ describe('Contributor stats component', () => { describe('when user navigates to contributor stats page ', () => { beforeEach(waitForAsync(() => { - spyOn(userService, 'getUserInfoAsync') - .and.returnValue(Promise.resolve({ + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve({ isLoggedIn: () => true, - getUsername: () => 'user' - } as UserInfo)); - spyOn(userService, 'getUserContributionRightsDataAsync') - .and.returnValue(Promise.resolve(userContributionRights)); + getUsername: () => 'user', + } as UserInfo) + ); + spyOn(userService, 'getUserContributionRightsDataAsync').and.returnValue( + Promise.resolve(userContributionRights) + ); component.ngOnInit(); })); @@ -182,15 +203,15 @@ describe('Contributor stats component', () => { expect(component.dropdownShown).toBeFalse; expect(component.selectedContributionType).toEqual( - 'Translation Contributions'); + 'Translation Contributions' + ); })); it('should show translation review stats', fakeAsync(() => { component.selectOption('translationReview'); expect(component.dropdownShown).toBeFalse; - expect(component.selectedContributionType).toEqual( - 'Translation Reviews'); + expect(component.selectedContributionType).toEqual('Translation Reviews'); })); it('should show question contribution stats', fakeAsync(() => { @@ -198,24 +219,26 @@ describe('Contributor stats component', () => { expect(component.dropdownShown).toBeFalse; expect(component.selectedContributionType).toEqual( - 'Question Contributions'); + 'Question Contributions' + ); })); it('should show question review stats', fakeAsync(() => { component.selectOption('questionReview'); expect(component.dropdownShown).toBeFalse; - expect(component.selectedContributionType).toEqual( - 'Question Reviews'); + expect(component.selectedContributionType).toEqual('Question Reviews'); })); - it('should open date range selecting model to generate certificate for' + - ' contributors', - fakeAsync(() => { - component.openCertificateDownloadModal('add_question', ''); - tick(); - expect(modalService.open).toHaveBeenCalled(); - })); + it( + 'should open date range selecting model to generate certificate for' + + ' contributors', + fakeAsync(() => { + component.openCertificateDownloadModal('add_question', ''); + tick(); + expect(modalService.open).toHaveBeenCalled(); + }) + ); it('should be able to page stats', fakeAsync(() => { const pagedStats = { @@ -226,44 +249,44 @@ describe('Contributor stats component', () => { lastContributionDate: 'Mar 2022', topicName: 'Dummy Topic', acceptedCards: 1, - acceptedWordCount: 1 + acceptedWordCount: 1, }, { firstContributionDate: 'Mar 2020', lastContributionDate: 'Mar 2022', topicName: 'Dummy Topic', acceptedCards: 1, - acceptedWordCount: 1 + acceptedWordCount: 1, }, { firstContributionDate: 'Mar 2020', lastContributionDate: 'Mar 2022', topicName: 'Dummy Topic', acceptedCards: 1, - acceptedWordCount: 1 + acceptedWordCount: 1, }, { firstContributionDate: 'Mar 2020', lastContributionDate: 'Mar 2022', topicName: 'Dummy Topic', acceptedCards: 1, - acceptedWordCount: 1 + acceptedWordCount: 1, }, { firstContributionDate: 'Mar 2020', lastContributionDate: 'Mar 2022', topicName: 'Dummy Topic', acceptedCards: 1, - acceptedWordCount: 1 + acceptedWordCount: 1, }, { firstContributionDate: 'Mar 2020', lastContributionDate: 'Mar 2022', topicName: 'Dummy Topic', acceptedCards: 1, - acceptedWordCount: 1 + acceptedWordCount: 1, }, - ] + ], }; component.goToNextPage(pagedStats); @@ -282,9 +305,9 @@ describe('Contributor stats component', () => { lastContributionDate: 'Mar 2022', topicName: 'Dummy Topic', acceptedCards: 1, - acceptedWordCount: 1 - } - ] + acceptedWordCount: 1, + }, + ], }; expect(() => { @@ -300,86 +323,91 @@ describe('Contributor stats component', () => { })); }); - describe('when user navigates to contributor stats page without login', - () => { - it('should throw error if username is invalid', - fakeAsync(() => { - const defaultUserInfo = new UserInfo( - ['GUEST'], false, false, false, false, false, - null, null, null, false); - spyOn(userService, 'getUserInfoAsync').and - .returnValue(Promise.resolve(defaultUserInfo)); - - expect(() => { - component.ngOnInit(); - tick(); - }).toThrowError(); - flush(); - })); - }); + describe('when user navigates to contributor stats page without login', () => { + it('should throw error if username is invalid', fakeAsync(() => { + const defaultUserInfo = new UserInfo( + ['GUEST'], + false, + false, + false, + false, + false, + null, + null, + null, + false + ); + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve(defaultUserInfo) + ); - describe('when user contribution rights can not be fetched', - () => { - it('should throw error to mention the error', - fakeAsync(() => { - spyOn(userService, 'getUserInfoAsync') - .and.returnValue(Promise.resolve({ - isLoggedIn: () => true, - getUsername: () => 'user' - } as UserInfo)); - spyOn(userService, 'getUserContributionRightsDataAsync') - .and.returnValue(Promise.resolve(null)); - - expect(() => { - component.ngOnInit(); - tick(); - }).toThrowError(); - flush(); - })); - }); + expect(() => { + component.ngOnInit(); + tick(); + }).toThrowError(); + flush(); + })); + }); - describe('when user interacts with dropdown', - () => { - let getDropdownOptionsContainer: () => HTMLElement; + describe('when user contribution rights can not be fetched', () => { + it('should throw error to mention the error', fakeAsync(() => { + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve({ + isLoggedIn: () => true, + getUsername: () => 'user', + } as UserInfo) + ); + spyOn(userService, 'getUserContributionRightsDataAsync').and.returnValue( + Promise.resolve(null) + ); - beforeEach(() => { - getDropdownOptionsContainer = () => { - return fixture.debugElement.nativeElement.querySelector( - '.oppia-stats-type-selector-dropdown-container'); - }; - }); + expect(() => { + component.ngOnInit(); + tick(); + }).toThrowError(); + flush(); + })); + }); - it('should correctly show and hide when clicked away', - fakeAsync(() => { - let fakeClickAwayEvent = new MouseEvent('click'); - Object.defineProperty( - fakeClickAwayEvent, - 'target', - {value: document.createElement('div')}); + describe('when user interacts with dropdown', () => { + let getDropdownOptionsContainer: () => HTMLElement; - component.onDocumentClick(fakeClickAwayEvent); - fixture.detectChanges(); + beforeEach(() => { + getDropdownOptionsContainer = () => { + return fixture.debugElement.nativeElement.querySelector( + '.oppia-stats-type-selector-dropdown-container' + ); + }; + }); - expect(component.dropdownShown).toBe(false); - expect(getDropdownOptionsContainer()).toBeFalsy(); - })); + it('should correctly show and hide when clicked away', fakeAsync(() => { + let fakeClickAwayEvent = new MouseEvent('click'); + Object.defineProperty(fakeClickAwayEvent, 'target', { + value: document.createElement('div'), + }); - it('should correctly show and hide correctly', - fakeAsync(() => { - expect(component.dropdownShown).toBe(false); - expect(component.mobileDropdownShown).toBe(false); + component.onDocumentClick(fakeClickAwayEvent); + fixture.detectChanges(); - component.toggleDropdown(); - expect(component.dropdownShown).toBe(true); + expect(component.dropdownShown).toBe(false); + expect(getDropdownOptionsContainer()).toBeFalsy(); + })); - component.toggleDropdown(); - expect(component.dropdownShown).toBe(false); + it('should correctly show and hide correctly', fakeAsync(() => { + expect(component.dropdownShown).toBe(false); + expect(component.mobileDropdownShown).toBe(false); - component.toggleMobileDropdown(); - expect(component.mobileDropdownShown).toBe(true); + component.toggleDropdown(); + expect(component.dropdownShown).toBe(true); - component.toggleMobileDropdown(); - expect(component.mobileDropdownShown).toBe(false); - })); - }); + component.toggleDropdown(); + expect(component.dropdownShown).toBe(false); + + component.toggleMobileDropdown(); + expect(component.mobileDropdownShown).toBe(true); + + component.toggleMobileDropdown(); + expect(component.mobileDropdownShown).toBe(false); + })); + }); }); diff --git a/core/templates/pages/contributor-dashboard-page/contributor-stats/contributor-stats.component.ts b/core/templates/pages/contributor-dashboard-page/contributor-stats/contributor-stats.component.ts index f0b9ad74532c..9c4537b8dae3 100644 --- a/core/templates/pages/contributor-dashboard-page/contributor-stats/contributor-stats.component.ts +++ b/core/templates/pages/contributor-dashboard-page/contributor-stats/contributor-stats.component.ts @@ -16,15 +16,28 @@ * @fileoverview Component for the contribution stats view. */ -import { Component, ElementRef, HostListener, Injector, Input, ViewChild } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; - -import { ContributionAndReviewStatsService, QuestionContributionBackendDict, QuestionReviewBackendDict, TranslationContributionBackendDict, TranslationReviewBackendDict } from '../services/contribution-and-review-stats.service'; -import { UserService } from 'services/user.service'; -import { LanguageUtilService } from 'domain/utilities/language-util.service'; -import { AppConstants } from 'app.constants'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { CertificateDownloadModalComponent } from '../modal-templates/certificate-download-modal.component'; +import { + Component, + ElementRef, + HostListener, + Injector, + Input, + ViewChild, +} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; + +import { + ContributionAndReviewStatsService, + QuestionContributionBackendDict, + QuestionReviewBackendDict, + TranslationContributionBackendDict, + TranslationReviewBackendDict, +} from '../services/contribution-and-review-stats.service'; +import {UserService} from 'services/user.service'; +import {LanguageUtilService} from 'domain/utilities/language-util.service'; +import {AppConstants} from 'app.constants'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {CertificateDownloadModalComponent} from '../modal-templates/certificate-download-modal.component'; interface Option { contributionType: string; @@ -34,13 +47,20 @@ interface Option { class PageableStats { currentPageStartIndex: number; data: ( - TranslationContributionStats | TranslationReviewStats | - QuestionContributionStats | QuestionReviewStats)[]; + | TranslationContributionStats + | TranslationReviewStats + | QuestionContributionStats + | QuestionReviewStats + )[]; constructor( - data: ( - TranslationContributionStats | TranslationReviewStats | - QuestionContributionStats | QuestionReviewStats)[]) { + data: ( + | TranslationContributionStats + | TranslationReviewStats + | QuestionContributionStats + | QuestionReviewStats + )[] + ) { this.data = data; this.currentPageStartIndex = 0; } @@ -77,13 +97,12 @@ interface QuestionReviewStats extends Stat { @Component({ selector: 'contributor-stats', templateUrl: './contributor-stats.component.html', - styleUrls: [] + styleUrls: [], }) export class ContributorStatsComponent { @Input() type!: string; - @ViewChild('dropdown', {'static': false}) dropdownRef!: ElementRef; - @ViewChild('mobileDropdown', {'static': false}) mobileDropdownRef!: - ElementRef; + @ViewChild('dropdown', {static: false}) dropdownRef!: ElementRef; + @ViewChild('mobileDropdown', {static: false}) mobileDropdownRef!: ElementRef; dropdownShown: boolean = false; mobileDropdownShown: boolean = false; @@ -101,7 +120,7 @@ export class ContributorStatsComponent { months: 'Months', topicNames: 'Topic Names', acceptedCards: 'Accepted Cards', - acceptedWordCount: 'Accepted Word Count' + acceptedWordCount: 'Accepted Word Count', }, translationReview: { months: 'Months', @@ -109,54 +128,52 @@ export class ContributorStatsComponent { reviewedCards: 'Reviewed Cards', reviewedWordCount: 'Reviewed Word Count', acceptedCards: 'Accepted Cards', - acceptedWordCount: 'Accepted Word Count' + acceptedWordCount: 'Accepted Word Count', }, questionContribution: { months: 'Months', topicNames: 'Topic Names', acceptedQuestions: 'Accepted Questions', - acceptedQuestionsWithoutEdits: 'Accepted Questions Without Edits' + acceptedQuestionsWithoutEdits: 'Accepted Questions Without Edits', }, questionReview: { months: 'Months', topicNames: 'Topic Names', reviewedQuestions: 'Reviewed Questions', - acceptedQuestions: 'Accepted Questions' - } + acceptedQuestions: 'Accepted Questions', + }, }; translationContributionOption: Option = { displayName: - AppConstants.CONTRIBUTION_STATS_TYPES - .TRANSLATION_CONTRIBUTION.DISPLAY_NAME, + AppConstants.CONTRIBUTION_STATS_TYPES.TRANSLATION_CONTRIBUTION + .DISPLAY_NAME, contributionType: - AppConstants.CONTRIBUTION_STATS_TYPES.TRANSLATION_CONTRIBUTION.NAME + AppConstants.CONTRIBUTION_STATS_TYPES.TRANSLATION_CONTRIBUTION.NAME, }; translationReviewOption: Option = { displayName: AppConstants.CONTRIBUTION_STATS_TYPES.TRANSLATION_REVIEW.DISPLAY_NAME, contributionType: - AppConstants.CONTRIBUTION_STATS_TYPES.TRANSLATION_REVIEW.NAME + AppConstants.CONTRIBUTION_STATS_TYPES.TRANSLATION_REVIEW.NAME, }; questionContributionOption: Option = { displayName: AppConstants.CONTRIBUTION_STATS_TYPES.QUESTION_CONTRIBUTION.DISPLAY_NAME, contributionType: - AppConstants.CONTRIBUTION_STATS_TYPES.QUESTION_CONTRIBUTION.NAME + AppConstants.CONTRIBUTION_STATS_TYPES.QUESTION_CONTRIBUTION.NAME, }; questionReviewOption: Option = { displayName: AppConstants.CONTRIBUTION_STATS_TYPES.QUESTION_REVIEW.DISPLAY_NAME, contributionType: - AppConstants.CONTRIBUTION_STATS_TYPES.QUESTION_REVIEW.NAME + AppConstants.CONTRIBUTION_STATS_TYPES.QUESTION_REVIEW.NAME, }; - options: Option[] = [ - this.translationContributionOption - ]; + options: Option[] = [this.translationContributionOption]; statsData: { translationContribution: Map; @@ -164,18 +181,17 @@ export class ContributorStatsComponent { questionContribution?: PageableStats; questionReview?: PageableStats; } = { - translationContribution: new Map(), - translationReview: new Map() - }; + translationContribution: new Map(), + translationReview: new Map(), + }; constructor( public readonly languageUtilService: LanguageUtilService, - private readonly contributionAndReviewStatsService: - ContributionAndReviewStatsService, + private readonly contributionAndReviewStatsService: ContributionAndReviewStatsService, private readonly userService: UserService, private readonly modalService: NgbModal, - private readonly injector: Injector) { - } + private readonly injector: Injector + ) {} async ngOnInit(): Promise { const userInfo = await this.userService.getUserInfoAsync(); @@ -187,7 +203,8 @@ export class ContributorStatsComponent { } this.username = username; const currentOption = this.options.find( - (option) => option.contributionType === this.type); + option => option.contributionType === this.type + ); this.selectedContributionType = currentOption?.displayName; const userContributionRights = @@ -196,14 +213,13 @@ export class ContributorStatsComponent { if (userContributionRights === null) { throw new Error('Cannot fetch user contribution rights.'); } - const reviewableLanguageCodes = ( - userContributionRights.can_review_translation_for_language_codes); - this.userCanReviewTranslationSuggestions = ( - reviewableLanguageCodes.length > 0); - this.userCanReviewQuestionSuggestions = ( - userContributionRights.can_review_questions); - this.userCanSuggestQuestions = ( - userContributionRights.can_suggest_questions); + const reviewableLanguageCodes = + userContributionRights.can_review_translation_for_language_codes; + this.userCanReviewTranslationSuggestions = + reviewableLanguageCodes.length > 0; + this.userCanReviewQuestionSuggestions = + userContributionRights.can_review_questions; + this.userCanSuggestQuestions = userContributionRights.can_suggest_questions; if (this.userCanReviewTranslationSuggestions) { this.options.push(this.translationReviewOption); @@ -230,7 +246,8 @@ export class ContributorStatsComponent { async selectOption(contributionType: string): Promise { this.type = contributionType; const currentOption = this.options.find( - (option) => option.contributionType === contributionType); + option => option.contributionType === contributionType + ); this.selectedContributionType = currentOption?.displayName; this.dropdownShown = false; this.mobileDropdownShown = false; @@ -238,19 +255,22 @@ export class ContributorStatsComponent { async fetchStats(): Promise { const response = await this.contributionAndReviewStatsService.fetchAllStats( - this.username); + this.username + ); if (response.translation_contribution_stats.length > 0) { - response.translation_contribution_stats.map((stat) => { - const translationContributionStatsData = this - .statsData.translationContribution.get(stat.language_code); + response.translation_contribution_stats.map(stat => { + const translationContributionStatsData = + this.statsData.translationContribution.get(stat.language_code); if (translationContributionStatsData === undefined) { this.statsData?.translationContribution.set( stat.language_code, - new PageableStats([this.createTranslationContributionStat(stat)])); + new PageableStats([this.createTranslationContributionStat(stat)]) + ); } else { translationContributionStatsData.data?.push( - this.createTranslationContributionStat(stat)); + this.createTranslationContributionStat(stat) + ); this.statsData?.translationContribution.set( stat.language_code, translationContributionStatsData @@ -260,16 +280,19 @@ export class ContributorStatsComponent { } if (response.translation_review_stats.length > 0) { - response.translation_review_stats.map((stat) => { - const translationReviewStatsData = this - .statsData.translationReview.get(stat.language_code); + response.translation_review_stats.map(stat => { + const translationReviewStatsData = this.statsData.translationReview.get( + stat.language_code + ); if (translationReviewStatsData === undefined) { this.statsData.translationReview.set( stat.language_code, - new PageableStats([this.createTranslationReviewStat(stat)])); + new PageableStats([this.createTranslationReviewStat(stat)]) + ); } else { translationReviewStatsData.data?.push( - this.createTranslationReviewStat(stat)); + this.createTranslationReviewStat(stat) + ); this.statsData?.translationReview.set( stat.language_code, translationReviewStatsData @@ -280,7 +303,7 @@ export class ContributorStatsComponent { if (response.question_contribution_stats.length > 0) { this.statsData.questionContribution = new PageableStats( - response.question_contribution_stats.map((stat) => { + response.question_contribution_stats.map(stat => { return this.createQuestionContributionStat(stat); }) ); @@ -288,7 +311,7 @@ export class ContributorStatsComponent { if (response.question_review_stats.length > 0) { this.statsData.questionReview = new PageableStats( - response.question_review_stats.map((stat) => { + response.question_review_stats.map(stat => { return this.createQuestionReviewStat(stat); }) ); @@ -296,19 +319,19 @@ export class ContributorStatsComponent { } createTranslationContributionStat( - stat: TranslationContributionBackendDict + stat: TranslationContributionBackendDict ): TranslationContributionStats { return { firstContributionDate: stat.first_contribution_date, lastContributionDate: stat.last_contribution_date, topicName: stat.topic_name, acceptedCards: stat.accepted_translations_count, - acceptedWordCount: stat.accepted_translation_word_count + acceptedWordCount: stat.accepted_translation_word_count, }; } createTranslationReviewStat( - stat: TranslationReviewBackendDict + stat: TranslationReviewBackendDict ): TranslationReviewStats { return { firstContributionDate: stat.first_contribution_date, @@ -317,39 +340,37 @@ export class ContributorStatsComponent { acceptedCards: stat.accepted_translations_count, acceptedWordCount: stat.accepted_translation_word_count, reviewedCards: stat.reviewed_translations_count, - reviewedWordCount: stat.reviewed_translation_word_count + reviewedWordCount: stat.reviewed_translation_word_count, }; } createQuestionContributionStat( - stat: QuestionContributionBackendDict + stat: QuestionContributionBackendDict ): QuestionContributionStats { return { firstContributionDate: stat.first_contribution_date, lastContributionDate: stat.last_contribution_date, topicName: stat.topic_name, acceptedQuestions: stat.accepted_questions_count, - acceptedQuestionsWithoutEdits: ( - stat.accepted_questions_without_reviewer_edits_count) + acceptedQuestionsWithoutEdits: + stat.accepted_questions_without_reviewer_edits_count, }; } createQuestionReviewStat( - stat: QuestionReviewBackendDict + stat: QuestionReviewBackendDict ): QuestionReviewStats { return { firstContributionDate: stat.first_contribution_date, lastContributionDate: stat.last_contribution_date, topicName: stat.topic_name, reviewedQuestions: stat.reviewed_questions_count, - acceptedQuestions: stat.accepted_questions_count + acceptedQuestions: stat.accepted_questions_count, }; } goToNextPage(page: PageableStats): void { - if ( - page.currentPageStartIndex + this.ITEMS_PER_PAGE >= - page.data?.length) { + if (page.currentPageStartIndex + this.ITEMS_PER_PAGE >= page.data?.length) { throw new Error('There are no more pages after this one.'); } page.currentPageStartIndex += this.ITEMS_PER_PAGE; @@ -375,14 +396,14 @@ export class ContributorStatsComponent { } openCertificateDownloadModal( - suggestionType: string, languageCode: string | null + suggestionType: string, + languageCode: string | null ): void { - const modalRef = this.modalService.open( - CertificateDownloadModalComponent, { - size: 'lg', - backdrop: 'static', - injector: this.injector - }); + const modalRef = this.modalService.open(CertificateDownloadModalComponent, { + size: 'lg', + backdrop: 'static', + injector: this.injector, + }); modalRef.componentInstance.suggestionType = suggestionType; modalRef.componentInstance.username = this.username; modalRef.componentInstance.languageCode = languageCode; @@ -410,6 +431,9 @@ export class ContributorStatsComponent { } } -angular.module('oppia').directive( - 'oppiaOpportunitiesList', downgradeComponent( - {component: ContributorStatsComponent})); +angular + .module('oppia') + .directive( + 'oppiaOpportunitiesList', + downgradeComponent({component: ContributorStatsComponent}) + ); diff --git a/core/templates/pages/contributor-dashboard-page/login-required-message/login-required-message.component.spec.ts b/core/templates/pages/contributor-dashboard-page/login-required-message/login-required-message.component.spec.ts index aba0722542cb..8661d2855dda 100644 --- a/core/templates/pages/contributor-dashboard-page/login-required-message/login-required-message.component.spec.ts +++ b/core/templates/pages/contributor-dashboard-page/login-required-message/login-required-message.component.spec.ts @@ -16,14 +16,21 @@ * @fileoverview Unit tests for loginRequiredMessage. */ -import { ComponentFixture, flushMicrotasks, fakeAsync, TestBed, tick } from '@angular/core/testing'; - -import { LoginRequiredMessageComponent } from 'pages/contributor-dashboard-page/login-required-message/login-required-message.component'; -import { UserService } from 'services/user.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { HttpClientTestingModule, HttpTestingController } from - '@angular/common/http/testing'; +import { + ComponentFixture, + flushMicrotasks, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {LoginRequiredMessageComponent} from 'pages/contributor-dashboard-page/login-required-message/login-required-message.component'; +import {UserService} from 'services/user.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; describe('Login required message component', () => { let component: LoginRequiredMessageComponent; @@ -35,20 +42,19 @@ describe('Login required message component', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [LoginRequiredMessageComponent] + declarations: [LoginRequiredMessageComponent], }); httpTestingController = TestBed.inject(HttpTestingController); - fixture = TestBed.createComponent( - LoginRequiredMessageComponent); + fixture = TestBed.createComponent(LoginRequiredMessageComponent); component = fixture.componentInstance; userService = TestBed.inject(UserService); windowRef = TestBed.inject(WindowRef); spyOnProperty(windowRef, 'nativeWindow').and.returnValue({ location: { - reload: ()=>{}, - href: 'starting-url' + reload: () => {}, + href: 'starting-url', }, - gtag: () => {} + gtag: () => {}, } as unknown as Window); component.ngOnInit(); }); @@ -57,11 +63,11 @@ describe('Login required message component', () => { httpTestingController.verify(); }); - it('should initialize controller properties after its initialization', - () => { - expect(component.OPPIA_AVATAR_IMAGE_URL).toBe( - '/assets/images/avatar/oppia_avatar_100px.svg'); - }); + it('should initialize controller properties after its initialization', () => { + expect(component.OPPIA_AVATAR_IMAGE_URL).toBe( + '/assets/images/avatar/oppia_avatar_100px.svg' + ); + }); it('should go to login url when login button is clicked', fakeAsync(() => { spyOn(userService, 'getLoginUrlAsync').and.resolveTo('login-url'); @@ -73,13 +79,16 @@ describe('Login required message component', () => { expect(windowRef.nativeWindow.location.href).toBe('login-url'); })); - it('should refresh page if login url is not provided when login button is' + - ' clicked', fakeAsync(() => { - const reloadSpy = spyOn(windowRef.nativeWindow.location, 'reload'); - spyOn(userService, 'getLoginUrlAsync').and.resolveTo(''); - component.onLoginButtonClicked(); - flushMicrotasks(); + it( + 'should refresh page if login url is not provided when login button is' + + ' clicked', + fakeAsync(() => { + const reloadSpy = spyOn(windowRef.nativeWindow.location, 'reload'); + spyOn(userService, 'getLoginUrlAsync').and.resolveTo(''); + component.onLoginButtonClicked(); + flushMicrotasks(); - expect(reloadSpy).toHaveBeenCalled(); - })); + expect(reloadSpy).toHaveBeenCalled(); + }) + ); }); diff --git a/core/templates/pages/contributor-dashboard-page/login-required-message/login-required-message.component.ts b/core/templates/pages/contributor-dashboard-page/login-required-message/login-required-message.component.ts index a0744df26b87..51d2bb8ca4de 100644 --- a/core/templates/pages/contributor-dashboard-page/login-required-message/login-required-message.component.ts +++ b/core/templates/pages/contributor-dashboard-page/login-required-message/login-required-message.component.ts @@ -16,17 +16,17 @@ * @fileoverview Component for login required message. */ -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { UserService } from 'services/user.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { Component } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {UserService} from 'services/user.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {Component} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; @Component({ selector: 'login-required-message', templateUrl: './login-required-message.component.html', - styleUrls: [] + styleUrls: [], }) export class LoginRequiredMessageComponent { // These properties are initialized using Angular lifecycle hooks @@ -38,30 +38,33 @@ export class LoginRequiredMessageComponent { private readonly siteAnalyticsService: SiteAnalyticsService, private readonly urlInterpolationService: UrlInterpolationService, private readonly userService: UserService, - private readonly windowRef: WindowRef) {} + private readonly windowRef: WindowRef + ) {} ngOnInit(): void { - this.OPPIA_AVATAR_IMAGE_URL = ( + this.OPPIA_AVATAR_IMAGE_URL = this.urlInterpolationService.getStaticImageUrl( - '/avatar/oppia_avatar_100px.svg')); + '/avatar/oppia_avatar_100px.svg' + ); } onLoginButtonClicked(): void { - this.userService.getLoginUrlAsync().then( - (loginUrl) => { - if (loginUrl) { - this.siteAnalyticsService.registerStartLoginEvent('loginButton'); - setTimeout(() => { - this.windowRef.nativeWindow.location.href = loginUrl; - }, 150); - } else { - this.windowRef.nativeWindow.location.reload(); - } + this.userService.getLoginUrlAsync().then(loginUrl => { + if (loginUrl) { + this.siteAnalyticsService.registerStartLoginEvent('loginButton'); + setTimeout(() => { + this.windowRef.nativeWindow.location.href = loginUrl; + }, 150); + } else { + this.windowRef.nativeWindow.location.reload(); } - ); + }); } } -angular.module('oppia').directive( - 'loginRequiredMessage', downgradeComponent( - {component: LoginRequiredMessageComponent})); +angular + .module('oppia') + .directive( + 'loginRequiredMessage', + downgradeComponent({component: LoginRequiredMessageComponent}) + ); diff --git a/core/templates/pages/contributor-dashboard-page/modal-templates/certificate-download-modal.component.spec.ts b/core/templates/pages/contributor-dashboard-page/modal-templates/certificate-download-modal.component.spec.ts index 34762e85019e..a3283ff87645 100644 --- a/core/templates/pages/contributor-dashboard-page/modal-templates/certificate-download-modal.component.spec.ts +++ b/core/templates/pages/contributor-dashboard-page/modal-templates/certificate-download-modal.component.spec.ts @@ -14,21 +14,31 @@ /** * @fileoverview Unit tests for CertificateDownloadModalComponent. -*/ - -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { ChangeDetectorRef, NO_ERRORS_SCHEMA } from '@angular/core'; - -import { ComponentFixture, fakeAsync, flushMicrotasks, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { OppiaAngularRootComponent } from 'components/oppia-angular-root.component'; -import { ContextService } from 'services/context.service'; -import { WrapTextWithEllipsisPipe } from 'filters/string-utility-filters/wrap-text-with-ellipsis.pipe'; -import { CertificateDownloadModalComponent } from './certificate-download-modal.component'; -import { ContributionAndReviewService } from '../services/contribution-and-review.service'; -import { AlertsService } from 'services/alerts.service'; -import { ContributorCertificateResponse } from '../services/contribution-and-review-backend-api.service'; -import { HttpErrorResponse } from '@angular/common/http'; + */ + +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {ChangeDetectorRef, NO_ERRORS_SCHEMA} from '@angular/core'; + +import { + ComponentFixture, + fakeAsync, + flushMicrotasks, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {OppiaAngularRootComponent} from 'components/oppia-angular-root.component'; +import {ContextService} from 'services/context.service'; +import {WrapTextWithEllipsisPipe} from 'filters/string-utility-filters/wrap-text-with-ellipsis.pipe'; +import {CertificateDownloadModalComponent} from './certificate-download-modal.component'; +import {ContributionAndReviewService} from '../services/contribution-and-review.service'; +import {AlertsService} from 'services/alerts.service'; +import {ContributorCertificateResponse} from '../services/contribution-and-review-backend-api.service'; +import {HttpErrorResponse} from '@angular/common/http'; class MockChangeDetectorRef { detectChanges(): void {} @@ -47,27 +57,25 @@ describe('Contributor Certificate Download Modal Component', () => { to_date: '31 Oct 2022', team_lead: 'Test User', contribution_hours: 1.0, - language: 'Hindi' + language: 'Hindi', }; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], + imports: [HttpClientTestingModule], declarations: [ CertificateDownloadModalComponent, - WrapTextWithEllipsisPipe + WrapTextWithEllipsisPipe, ], providers: [ NgbActiveModal, AlertsService, { provide: ChangeDetectorRef, - useValue: changeDetectorRef - } + useValue: changeDetectorRef, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); OppiaAngularRootComponent.contextService = TestBed.inject(ContextService); })); @@ -93,8 +101,8 @@ describe('Contributor Certificate Download Modal Component', () => { component.toDate = '2022/10/31'; spyOn( contributionAndReviewService, - 'downloadContributorCertificateAsync') - .and.returnValue(Promise.resolve(certificateDataResponse)); + 'downloadContributorCertificateAsync' + ).and.returnValue(Promise.resolve(certificateDataResponse)); spyOn(alertsService, 'addInfoMessage').and.stub(); component.downloadCertificate(); @@ -111,8 +119,8 @@ describe('Contributor Certificate Download Modal Component', () => { component.suggestionType = 'add_question'; spyOn( contributionAndReviewService, - 'downloadContributorCertificateAsync') - .and.returnValue(Promise.resolve(certificateDataResponse)); + 'downloadContributorCertificateAsync' + ).and.returnValue(Promise.resolve(certificateDataResponse)); spyOn(alertsService, 'addInfoMessage').and.stub(); component.downloadCertificate(); @@ -123,20 +131,19 @@ describe('Contributor Certificate Download Modal Component', () => { ).toHaveBeenCalled(); }); - it( - 'should set errorsFound and errorMessage for To date in the future', () => { - const today = new Date(); - const tomorrow = new Date(today); - tomorrow.setDate(today.getDate() + 1); - - component.fromDate = '2023-10-01'; - component.toDate = tomorrow.toISOString().split('T')[0]; - component.validateDate(); - expect(component.errorsFound).toBe(true); - expect( - component.errorMessage - ).toBe("Please select a 'To' date that is earlier than today's date"); - }); + it('should set errorsFound and errorMessage for To date in the future', () => { + const today = new Date(); + const tomorrow = new Date(today); + tomorrow.setDate(today.getDate() + 1); + + component.fromDate = '2023-10-01'; + component.toDate = tomorrow.toISOString().split('T')[0]; + component.validateDate(); + expect(component.errorsFound).toBe(true); + expect(component.errorMessage).toBe( + "Please select a 'To' date that is earlier than today's date" + ); + }); it('should show error for invalid to date', () => { const today = new Date(); @@ -149,8 +156,7 @@ describe('Contributor Certificate Download Modal Component', () => { expect(component.errorsFound).toBeTrue(); expect(component.errorMessage).toEqual( - 'Please select a \'To\' date that is earlier than ' + - 'today\'s date' + "Please select a 'To' date that is earlier than " + "today's date" ); }); @@ -189,8 +195,7 @@ describe('Contributor Certificate Download Modal Component', () => { }); it('should handle errors properly', fakeAsync(() => { - const mockError = new HttpErrorResponse( - { error: { error: 'Error message' } }); + const mockError = new HttpErrorResponse({error: {error: 'Error message'}}); spyOn( contributionAndReviewService, 'downloadContributorCertificateAsync' @@ -207,15 +212,13 @@ describe('Contributor Certificate Download Modal Component', () => { it('should throw error when canvas context is null', () => { spyOn(document, 'createElement').and.callFake( - jasmine.createSpy('createElement').and.returnValue( - { - width: 0, - height: 0, - getContext: (txt: string) => { - return null; - }, - } - ) + jasmine.createSpy('createElement').and.returnValue({ + width: 0, + height: 0, + getContext: (txt: string) => { + return null; + }, + }) ); expect(() => { diff --git a/core/templates/pages/contributor-dashboard-page/modal-templates/certificate-download-modal.component.ts b/core/templates/pages/contributor-dashboard-page/modal-templates/certificate-download-modal.component.ts index 9eef06f8635d..3df2f42009ce 100644 --- a/core/templates/pages/contributor-dashboard-page/modal-templates/certificate-download-modal.component.ts +++ b/core/templates/pages/contributor-dashboard-page/modal-templates/certificate-download-modal.component.ts @@ -16,13 +16,13 @@ * @fileoverview Component for the certificate download modal. */ -import { Component, Input } from '@angular/core'; -import { HttpErrorResponse } from '@angular/common/http'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { AppConstants } from 'app.constants'; -import { ContributorCertificateResponse } from '../services/contribution-and-review-backend-api.service'; -import { ContributionAndReviewService } from '../services/contribution-and-review.service'; +import {Component, Input} from '@angular/core'; +import {HttpErrorResponse} from '@angular/common/http'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {AppConstants} from 'app.constants'; +import {ContributorCertificateResponse} from '../services/contribution-and-review-backend-api.service'; +import {ContributionAndReviewService} from '../services/contribution-and-review.service'; interface CertificateContentData { text: string; @@ -31,7 +31,7 @@ interface CertificateContentData { @Component({ selector: 'certificate-download-modal', - templateUrl: './certificate-download-modal.component.html' + templateUrl: './certificate-download-modal.component.html', }) export class CertificateDownloadModalComponent { @Input() suggestionType!: string; @@ -64,8 +64,8 @@ export class CertificateDownloadModalComponent { constructor( private readonly activeModal: NgbActiveModal, - private contributionAndReviewService: ContributionAndReviewService) { - } + private contributionAndReviewService: ContributionAndReviewService + ) {} close(): void { this.activeModal.close(); @@ -83,8 +83,8 @@ export class CertificateDownloadModalComponent { } if (new Date() < new Date(this.toDate)) { this.errorsFound = true; - this.errorMessage = 'Please select a \'To\' date that is earlier than ' + - 'today\'s date'; + this.errorMessage = + "Please select a 'To' date that is earlier than " + "today's date"; return; } this.errorsFound = false; @@ -95,21 +95,23 @@ export class CertificateDownloadModalComponent { this.errorsFound = false; this.errorMessage = ''; this.certificateDownloading = true; - this.contributionAndReviewService.downloadContributorCertificateAsync( - this.username, - this.suggestionType, - this.languageCode, - this.fromDate, - this.toDate - ).then((response: ContributorCertificateResponse) => { - this.createCertificate(response); - this.certificateDownloading = false; - }).catch((err: HttpErrorResponse) => { - this.errorsFound = true; - this.certificateDownloading = false; - this.errorMessage = ( - err.error.error); - }); + this.contributionAndReviewService + .downloadContributorCertificateAsync( + this.username, + this.suggestionType, + this.languageCode, + this.fromDate, + this.toDate + ) + .then((response: ContributorCertificateResponse) => { + this.createCertificate(response); + this.certificateDownloading = false; + }) + .catch((err: HttpErrorResponse) => { + this.errorsFound = true; + this.certificateDownloading = false; + this.errorMessage = err.error.error; + }); } disableDownloadButton(): boolean { @@ -125,7 +127,7 @@ export class CertificateDownloadModalComponent { const dateOptions: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'long', - day: 'numeric' + day: 'numeric', }; // Textual parts are starting when y coordinate is equals to 350. let linePosition = 350; @@ -194,59 +196,73 @@ export class CertificateDownloadModalComponent { if (this.suggestionType === 'translate_content') { const certificateContentData: CertificateContentData[] = [ { - text: 'for their dedication and time in translating Oppia\'s ' + - 'basic maths lessons to ' + response.language, - linePosition: linePosition + text: + "for their dedication and time in translating Oppia's " + + 'basic maths lessons to ' + + response.language, + linePosition: linePosition, }, { - text: 'which will help our ' + response.language + '-speaking ' + - 'learners better understand the lessons.', - linePosition: linePosition += 40 + text: + 'which will help our ' + + response.language + + '-speaking ' + + 'learners better understand the lessons.', + linePosition: (linePosition += 40), }, { - text: 'This certificate confirms that ' + this.username + - ' has contributed ' + response.contribution_hours + ' hours ' + - 'worth of', - linePosition: linePosition += 80 + text: + 'This certificate confirms that ' + + this.username + + ' has contributed ' + + response.contribution_hours + + ' hours ' + + 'worth of', + linePosition: (linePosition += 80), }, { - text: 'translations from ' + response.from_date + ' to ' + - response.to_date + '.', - linePosition: linePosition += 40 - } + text: + 'translations from ' + + response.from_date + + ' to ' + + response.to_date + + '.', + linePosition: (linePosition += 40), + }, ]; - this.fillCertificateContent( - ctx, certificateContentData - ); + this.fillCertificateContent(ctx, certificateContentData); linePosition += 100; } else { const certificateContentData: CertificateContentData[] = [ { - text: 'for their dedication and time in contributing practice ' + - 'questions to Oppia\'s', - linePosition: linePosition + text: + 'for their dedication and time in contributing practice ' + + "questions to Oppia's", + linePosition: linePosition, }, { text: 'Math Classroom, which supports our mission of improving', - linePosition: linePosition += 40 + linePosition: (linePosition += 40), }, { text: 'access to quality education.', - linePosition: linePosition += 40 + linePosition: (linePosition += 40), }, { - text: 'This certificate confirms that ' + this.username + - ' has contributed ' + response.contribution_hours + ' hours', - linePosition: linePosition += 80 + text: + 'This certificate confirms that ' + + this.username + + ' has contributed ' + + response.contribution_hours + + ' hours', + linePosition: (linePosition += 80), }, { text: `to Oppia from ${response.from_date} to ${response.to_date}.`, - linePosition: linePosition += 40 - } + linePosition: (linePosition += 40), + }, ]; - this.fillCertificateContent( - ctx, certificateContentData - ); + this.fillCertificateContent(ctx, certificateContentData); linePosition += 40; } @@ -288,19 +304,18 @@ export class CertificateDownloadModalComponent { } fillCertificateContent( - ctx: CanvasRenderingContext2D, - data: CertificateContentData[] + ctx: CanvasRenderingContext2D, + data: CertificateContentData[] ): void { data.forEach((data: CertificateContentData) => { - ctx.fillText( - data.text, - this.CERTIFICATE_MID_POINT, - data.linePosition - ); + ctx.fillText(data.text, this.CERTIFICATE_MID_POINT, data.linePosition); }); } } -angular.module('oppia').directive( - 'certificateDownloadModal', downgradeComponent( - {component: CertificateDownloadModalComponent})); +angular + .module('oppia') + .directive( + 'certificateDownloadModal', + downgradeComponent({component: CertificateDownloadModalComponent}) + ); diff --git a/core/templates/pages/contributor-dashboard-page/modal-templates/login-required-modal.component.spec.ts b/core/templates/pages/contributor-dashboard-page/modal-templates/login-required-modal.component.spec.ts index afee3a897136..d35a89a6f0ad 100644 --- a/core/templates/pages/contributor-dashboard-page/modal-templates/login-required-modal.component.spec.ts +++ b/core/templates/pages/contributor-dashboard-page/modal-templates/login-required-modal.component.spec.ts @@ -14,13 +14,13 @@ /** * @fileoverview Unit tests for LoginRequiredModalComponent. -*/ + */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { LoginRequiredModalContent } from 'pages/contributor-dashboard-page/modal-templates/login-required-modal.component'; -import { LoginRequiredMessageComponent } from '../login-required-message/login-required-message.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {LoginRequiredModalContent} from 'pages/contributor-dashboard-page/modal-templates/login-required-modal.component'; +import {LoginRequiredMessageComponent} from '../login-required-message/login-required-message.component'; describe('Login Required Modal Content', () => { let component: LoginRequiredModalContent; @@ -29,10 +29,8 @@ describe('Login Required Modal Content', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - LoginRequiredModalContent, - LoginRequiredMessageComponent], - providers: [NgbActiveModal] + declarations: [LoginRequiredModalContent, LoginRequiredMessageComponent], + providers: [NgbActiveModal], }).compileComponents(); fixture = TestBed.createComponent(LoginRequiredModalContent); component = fixture.componentInstance; diff --git a/core/templates/pages/contributor-dashboard-page/modal-templates/login-required-modal.component.ts b/core/templates/pages/contributor-dashboard-page/modal-templates/login-required-modal.component.ts index 227804da13f8..e1f0626843f8 100644 --- a/core/templates/pages/contributor-dashboard-page/modal-templates/login-required-modal.component.ts +++ b/core/templates/pages/contributor-dashboard-page/modal-templates/login-required-modal.component.ts @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import {Component} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; /** * @fileoverview Component for login required modal. @@ -22,12 +22,15 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; @Component({ selector: 'login-required-modal', - templateUrl: './login-required-modal.component.html' + templateUrl: './login-required-modal.component.html', }) export class LoginRequiredModalContent { constructor(public readonly activeModal: NgbActiveModal) {} } -angular.module('oppia').directive( - 'loginRequiredModalContent', downgradeComponent( - {component: LoginRequiredModalContent})); +angular + .module('oppia') + .directive( + 'loginRequiredModalContent', + downgradeComponent({component: LoginRequiredModalContent}) + ); diff --git a/core/templates/pages/contributor-dashboard-page/modal-templates/question-suggestion-editor-modal.component.spec.ts b/core/templates/pages/contributor-dashboard-page/modal-templates/question-suggestion-editor-modal.component.spec.ts index 8ed09222fd05..257152fe816e 100644 --- a/core/templates/pages/contributor-dashboard-page/modal-templates/question-suggestion-editor-modal.component.spec.ts +++ b/core/templates/pages/contributor-dashboard-page/modal-templates/question-suggestion-editor-modal.component.spec.ts @@ -16,25 +16,28 @@ * @fileoverview Unit tests for QuestionSuggestionEditorModalComponent. */ -import { fakeAsync, tick } from '@angular/core/testing'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { ContextService } from 'services/context.service'; -import { QuestionSuggestionEditorModalComponent } from './question-suggestion-editor-modal.component'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ContributionAndReviewService } from '../services/contribution-and-review.service'; -import { QuestionSuggestionBackendApiService } from '../services/question-suggestion-backend-api.service'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { QuestionUndoRedoService } from 'domain/editor/undo_redo/question-undo-redo.service'; -import { Question, QuestionObjectFactory } from 'domain/question/QuestionObjectFactory'; -import { Skill, SkillObjectFactory } from 'domain/skill/SkillObjectFactory'; -import { AlertsService } from 'services/alerts.service'; -import { CsrfTokenService } from 'services/csrf-token.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { State } from 'domain/state/StateObjectFactory'; +import {fakeAsync, tick} from '@angular/core/testing'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {ContextService} from 'services/context.service'; +import {QuestionSuggestionEditorModalComponent} from './question-suggestion-editor-modal.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ContributionAndReviewService} from '../services/contribution-and-review.service'; +import {QuestionSuggestionBackendApiService} from '../services/question-suggestion-backend-api.service'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {QuestionUndoRedoService} from 'domain/editor/undo_redo/question-undo-redo.service'; +import { + Question, + QuestionObjectFactory, +} from 'domain/question/QuestionObjectFactory'; +import {Skill, SkillObjectFactory} from 'domain/skill/SkillObjectFactory'; +import {AlertsService} from 'services/alerts.service'; +import {CsrfTokenService} from 'services/csrf-token.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {State} from 'domain/state/StateObjectFactory'; class MockNgbModalRef { componentInstance!: { @@ -55,43 +58,37 @@ class MockActiveModal { class MockNgbModal { open() { return { - result: Promise.resolve() + result: Promise.resolve(), }; } } class MockContributionAndReviewService { updateQuestionSuggestionAsync( - suggestionId: string, - skillDifficulty: string, - questionStateData: string, - imagesData: File[], + suggestionId: string, + skillDifficulty: string, + questionStateData: string, + imagesData: File[] ) { return { - then: ( - successCallback: () => void, - errorCallback: () => void - ) => { + then: (successCallback: () => void, errorCallback: () => void) => { successCallback(); - } + }, }; } } class MockQuestionSuggestionBackendApiService { submitSuggestionAsync( - question: Question, - associatedSkill: string, - skillDifficulty: string, - imagesData: File[], + question: Question, + associatedSkill: string, + skillDifficulty: string, + imagesData: File[] ) { return { - then: ( - successCallback: () => void, - errorCallback: () => void - ) => { + then: (successCallback: () => void, errorCallback: () => void) => { successCallback(); - } + }, }; } } @@ -126,40 +123,38 @@ describe('Question Suggestion Editor Modal Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - QuestionSuggestionEditorModalComponent - ], + declarations: [QuestionSuggestionEditorModalComponent], providers: [ ContextService, UrlInterpolationService, { provide: NgbActiveModal, - useClass: MockActiveModal + useClass: MockActiveModal, }, { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, { provide: ContributionAndReviewService, - useClass: MockContributionAndReviewService + useClass: MockContributionAndReviewService, }, { provide: QuestionSuggestionBackendApiService, - useClass: MockQuestionSuggestionBackendApiService + useClass: MockQuestionSuggestionBackendApiService, }, { provide: AlertsService, - useClass: MockAlertsService + useClass: MockAlertsService, }, CsrfTokenService, QuestionObjectFactory, QuestionUndoRedoService, SiteAnalyticsService, SkillObjectFactory, - StateEditorService + StateEditorService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -178,8 +173,9 @@ describe('Question Suggestion Editor Modal Component', () => { ngbModal = TestBed.inject(NgbModal); ngbActiveModal = TestBed.inject(NgbActiveModal); - spyOn(csrfTokenService, 'getTokenAsync') - .and.returnValue(Promise.resolve('sample-csrf-token')); + spyOn(csrfTokenService, 'getTokenAsync').and.returnValue( + Promise.resolve('sample-csrf-token') + ); const skillContentsDict = { explanation: { @@ -188,20 +184,22 @@ describe('Question Suggestion Editor Modal Component', () => { }, worked_examples: [], recorded_voiceovers: { - voiceovers_mapping: {} - } + voiceovers_mapping: {}, + }, }; const skillDict = { id: '1', description: 'test description', - misconceptions: [{ - id: 2, - name: 'test name', - notes: 'test notes', - feedback: 'test feedback', - must_be_addressed: false - }], + misconceptions: [ + { + id: 2, + name: 'test name', + notes: 'test notes', + feedback: 'test feedback', + must_be_addressed: false, + }, + ], next_misconception_id: 3, prerequisite_skill_ids: [], rubrics: [], @@ -219,70 +217,74 @@ describe('Question Suggestion Editor Modal Component', () => { classifier_model_id: null, content: { html: 'Question 1', - content_id: 'content_1' + content_id: 'content_1', }, interaction: { - answer_groups: [{ - outcome: { - dest: 'outcome 1', - dest_if_really_stuck: null, - feedback: { - content_id: 'content_5', - html: '' + answer_groups: [ + { + outcome: { + dest: 'outcome 1', + dest_if_really_stuck: null, + feedback: { + content_id: 'content_5', + html: '', + }, + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, }, - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, }, - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - }], + ], confirmed_unclassified_answers: [], customization_args: { placeholder: { value: { content_id: 'ca_placeholder_0', - unicode_str: '' - } + unicode_str: '', + }, }, - rows: { value: 1 }, + rows: {value: 1}, catchMisspellings: { - value: false - } + value: false, + }, }, default_outcome: { dest: 'dest', dest_if_really_stuck: null, feedback: { html: 'Correct Answer', - content_id: 'content_2' + content_id: 'content_2', }, param_changes: [], labelled_as_correct: true, refresher_exploration_id: null, missing_prerequisite_skill_id: null, }, - hints: [{ - hint_content: { - html: 'Hint 1', - content_id: 'content_3' - } - }], + hints: [ + { + hint_content: { + html: 'Hint 1', + content_id: 'content_3', + }, + }, + ], solution: { correct_answer: 'This is the correct answer', answer_is_exclusive: false, explanation: { html: 'Solution explanation', - content_id: 'content_4' - } + content_id: 'content_4', + }, }, - id: 'TextInput' + id: 'TextInput', }, param_changes: [], recorded_voiceovers: { - voiceovers_mapping: {} + voiceovers_mapping: {}, }, solicit_answer_details: false, card_is_checkpoint: false, @@ -293,7 +295,7 @@ describe('Question Suggestion Editor Modal Component', () => { language_code: 'en', version: 1, linked_skill_ids: [], - inapplicable_skill_misconception_ids: ['1-2'] + inapplicable_skill_misconception_ids: ['1-2'], }); component.question = question; questionId = question.getId() as string; @@ -309,21 +311,20 @@ describe('Question Suggestion Editor Modal Component', () => { fixture.destroy(); }); - it('should initialize component properties after component is initialized', - () => { - spyOn(component, 'isQuestionValid').and.returnValue(false); - spyOn(component, 'setDifficultyString').and.stub(); + it('should initialize component properties after component is initialized', () => { + spyOn(component, 'isQuestionValid').and.returnValue(false); + spyOn(component, 'setDifficultyString').and.stub(); - component.ngOnInit(); - component.done(); + component.ngOnInit(); + component.done(); - expect(component.canEditQuestion).toBe(true); - expect(component.newQuestionIsBeingCreated).toBe(true); - expect(component.question).toEqual(question); - expect(component.questionId).toEqual(questionId); - expect(component.questionStateData).toEqual(questionStateData); - expect(component.skill).toEqual(skill); - }); + expect(component.canEditQuestion).toBe(true); + expect(component.newQuestionIsBeingCreated).toBe(true); + expect(component.question).toEqual(question); + expect(component.questionId).toEqual(questionId); + expect(component.questionStateData).toEqual(questionStateData); + expect(component.skill).toEqual(skill); + }); it('should evaluate question validity', () => { expect(component.isQuestionValid()).toBe(true); @@ -331,15 +332,23 @@ describe('Question Suggestion Editor Modal Component', () => { }); it('should update the question', () => { - spyOn(contributionAndReviewService, 'updateQuestionSuggestionAsync') + spyOn( + contributionAndReviewService, + 'updateQuestionSuggestionAsync' + ).and.callFake( // This throws "Argument of type 'null' is not assignable to parameter of // type 'string'." We need to suppress this error // because of the need to test validations. This rule will be removed // when the codeowners file is updated. // @ts-ignore - .and.callFake(( - suggestionId, skillDifficulty, questionStateData, nextContentIdIndex, - imagesData, successCallback, errorCallback + ( + suggestionId, + skillDifficulty, + questionStateData, + nextContentIdIndex, + imagesData, + successCallback, + errorCallback ) => { // This throws "Argument of type 'null' is not assignable to parameter // of type 'string'." We need to suppress @@ -348,7 +357,8 @@ describe('Question Suggestion Editor Modal Component', () => { // @ts-ignore successCallback(null); return null; - }); + } + ); spyOn(questionUndoRedoService, 'hasChanges').and.returnValue(true); component.question = question; component.skillDifficulty = skillDifficulty; @@ -356,109 +366,135 @@ describe('Question Suggestion Editor Modal Component', () => { component.done(); - expect(contributionAndReviewService.updateQuestionSuggestionAsync) - .toHaveBeenCalled(); + expect( + contributionAndReviewService.updateQuestionSuggestionAsync + ).toHaveBeenCalled(); }); - it('should fail to update the question when no changes are made', - () => { - spyOn(contributionAndReviewService, 'updateQuestionSuggestionAsync') + it('should fail to update the question when no changes are made', () => { + spyOn( + contributionAndReviewService, + 'updateQuestionSuggestionAsync' + ).and.callFake( + // This throws "Argument of type 'null' is not assignable to parameter + // of type 'string'." We need to suppress this error + // because of the need to test validations. This rule will be removed + // when the codeowners file is updated. + // @ts-ignore + ( + suggestionId, + skillDifficulty, + questionStateData, + nextContentIdIndex, + imagesData, + successCallback, + errorCallback + ) => { // This throws "Argument of type 'null' is not assignable to parameter - // of type 'string'." We need to suppress this error - // because of the need to test validations. This rule will be removed - // when the codeowners file is updated. + // of type 'string'." We need to suppress + // this error because of the need to test validations. This rule will + // be removed when the codeowners file is updated. // @ts-ignore - .and.callFake(( - suggestionId, skillDifficulty, questionStateData, - nextContentIdIndex, imagesData, successCallback, errorCallback) => { - // This throws "Argument of type 'null' is not assignable to parameter - // of type 'string'." We need to suppress - // this error because of the need to test validations. This rule will - // be removed when the codeowners file is updated. - // @ts-ignore - successCallback(null); - return null; - }); - spyOn(questionUndoRedoService, 'hasChanges').and.returnValue(false); - spyOn(alertsService, 'addInfoMessage'); + successCallback(null); + return null; + } + ); + spyOn(questionUndoRedoService, 'hasChanges').and.returnValue(false); + spyOn(alertsService, 'addInfoMessage'); - component.done(); + component.done(); - expect(alertsService.addInfoMessage) - .toHaveBeenCalledWith('No changes detected.', 5000); - }); + expect(alertsService.addInfoMessage).toHaveBeenCalledWith( + 'No changes detected.', + 5000 + ); + }); it('should show alert when suggestion is submitted', () => { spyOn(questionUndoRedoService, 'hasChanges').and.returnValue(true); spyOn(alertsService, 'addSuccessMessage'); component.isEditing = false; component.done(); - expect(alertsService.addSuccessMessage) - .toHaveBeenCalledWith('Submitted question for review.'); + expect(alertsService.addSuccessMessage).toHaveBeenCalledWith( + 'Submitted question for review.' + ); }); - it('should register Contributor Dashboard submit suggestion event on' + - ' submit', () => { - spyOn( - siteAnalyticsService, - 'registerContributorDashboardSubmitSuggestionEvent'); - component.isEditing = false; - spyOn(questionUndoRedoService, 'hasChanges').and.returnValue(true); - component.done(); - expect( - siteAnalyticsService.registerContributorDashboardSubmitSuggestionEvent) - .toHaveBeenCalledWith('Question'); - }); + it( + 'should register Contributor Dashboard submit suggestion event on' + + ' submit', + () => { + spyOn( + siteAnalyticsService, + 'registerContributorDashboardSubmitSuggestionEvent' + ); + component.isEditing = false; + spyOn(questionUndoRedoService, 'hasChanges').and.returnValue(true); + component.done(); + expect( + siteAnalyticsService.registerContributorDashboardSubmitSuggestionEvent + ).toHaveBeenCalledWith('Question'); + } + ); it('should dismiss modal if there is no pending changes', () => { spyOn(questionUndoRedoService, 'hasChanges').and.returnValue(false); component.cancel(); }); - it('should dismiss modal if there is pending changes which won\'t be' + - ' saved', fakeAsync(() => { - let ngbSpy = spyOn(ngbActiveModal, 'dismiss').and.stub(); - spyOn(questionUndoRedoService, 'hasChanges').and.returnValue(true); - spyOn(ngbModal, 'open').and.returnValue({ - componentInstance: MockNgbModalRef, - result: Promise.resolve() - } as NgbModalRef); - - component.cancel(); - tick(); - - expect(ngbSpy).toHaveBeenCalledWith('cancel'); - })); - - it('should not dismiss modal if there is pending changes which will be' + - ' saved', () => { - let ngbSpy = spyOn(ngbActiveModal, 'dismiss').and.stub(); - spyOn(questionUndoRedoService, 'hasChanges').and.returnValue(true); - spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.reject() - } as NgbModalRef); - component.cancel(); - - expect(ngbSpy).not.toHaveBeenCalledWith('cancel'); - }); - - it('should change skill difficulty when skill difficulty' + - ' is edited via skill difficulty modal', fakeAsync(() => { - spyOn(component, 'setDifficultyString').and.stub(); - spyOn(ngbActiveModal, 'dismiss').and.stub(); - spyOn(questionUndoRedoService, 'hasChanges').and.returnValue(true); - spyOn(ngbModal, 'open').and.returnValue({ - componentInstance: MockNgbModalRef, - result: Promise.resolve({ - skillDifficulty: '0.6' - }) - } as NgbModalRef); - - component.onClickChangeDifficulty(); - tick(); - - expect(component.skillDifficulty).toBe('0.6'); - })); + it( + "should dismiss modal if there is pending changes which won't be" + + ' saved', + fakeAsync(() => { + let ngbSpy = spyOn(ngbActiveModal, 'dismiss').and.stub(); + spyOn(questionUndoRedoService, 'hasChanges').and.returnValue(true); + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: MockNgbModalRef, + result: Promise.resolve(), + } as NgbModalRef); + + component.cancel(); + tick(); + + expect(ngbSpy).toHaveBeenCalledWith('cancel'); + }) + ); + + it( + 'should not dismiss modal if there is pending changes which will be' + + ' saved', + () => { + let ngbSpy = spyOn(ngbActiveModal, 'dismiss').and.stub(); + spyOn(questionUndoRedoService, 'hasChanges').and.returnValue(true); + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.reject(), + } as NgbModalRef); + component.cancel(); + + expect(ngbSpy).not.toHaveBeenCalledWith('cancel'); + } + ); + + it( + 'should change skill difficulty when skill difficulty' + + ' is edited via skill difficulty modal', + fakeAsync(() => { + spyOn(component, 'setDifficultyString').and.stub(); + spyOn(ngbActiveModal, 'dismiss').and.stub(); + spyOn(questionUndoRedoService, 'hasChanges').and.returnValue(true); + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: MockNgbModalRef, + result: Promise.resolve({ + skillDifficulty: '0.6', + }), + } as NgbModalRef); + + component.onClickChangeDifficulty(); + tick(); + + expect(component.skillDifficulty).toBe('0.6'); + }) + ); it('should set the correct skill difficulty string', () => { component.setDifficultyString(0.6); @@ -473,7 +509,7 @@ describe('Question Suggestion Editor Modal Component', () => { let ngbSpy = spyOn(ngbActiveModal, 'dismiss').and.stub(); spyOn(ngbModal, 'open').and.returnValue({ componentInstance: MockNgbModalRef, - result: Promise.reject() + result: Promise.reject(), } as NgbModalRef); component.onClickChangeDifficulty(); diff --git a/core/templates/pages/contributor-dashboard-page/modal-templates/question-suggestion-editor-modal.component.ts b/core/templates/pages/contributor-dashboard-page/modal-templates/question-suggestion-editor-modal.component.ts index 1b18c472c35e..8cf08df905ed 100644 --- a/core/templates/pages/contributor-dashboard-page/modal-templates/question-suggestion-editor-modal.component.ts +++ b/core/templates/pages/contributor-dashboard-page/modal-templates/question-suggestion-editor-modal.component.ts @@ -16,31 +16,37 @@ * @fileoverview component for question suggestion editor modal. */ -import { Component, Input, OnInit } from '@angular/core'; -import { NgbActiveModal, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { AlertsService } from 'services/alerts.service'; -import { AppConstants } from 'app.constants'; -import { MisconceptionSkillMap } from 'domain/skill/MisconceptionObjectFactory'; -import { Question } from 'domain/question/QuestionObjectFactory'; -import { QuestionUndoRedoService } from 'domain/editor/undo_redo/question-undo-redo.service'; -import { Skill } from 'domain/skill/SkillObjectFactory'; -import { State } from 'domain/state/StateObjectFactory'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { ConfirmQuestionExitModalComponent } from 'components/question-directives/modal-templates/confirm-question-exit-modal.component'; -import { QuestionsOpportunitiesSelectDifficultyModalComponent } from 'pages/topic-editor-page/modal-templates/questions-opportunities-select-difficulty-modal.component'; -import { ContextService } from 'services/context.service'; -import { ContributionAndReviewService } from '../services/contribution-and-review.service'; -import { ImageLocalStorageService } from 'services/image-local-storage.service'; -import { QuestionSuggestionBackendApiService } from 'pages/contributor-dashboard-page/services/question-suggestion-backend-api.service'; -import { QuestionValidationService } from 'services/question-validation.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; +import {Component, Input, OnInit} from '@angular/core'; +import { + NgbActiveModal, + NgbModal, + NgbModalRef, +} from '@ng-bootstrap/ng-bootstrap'; +import {AlertsService} from 'services/alerts.service'; +import {AppConstants} from 'app.constants'; +import {MisconceptionSkillMap} from 'domain/skill/MisconceptionObjectFactory'; +import {Question} from 'domain/question/QuestionObjectFactory'; +import {QuestionUndoRedoService} from 'domain/editor/undo_redo/question-undo-redo.service'; +import {Skill} from 'domain/skill/SkillObjectFactory'; +import {State} from 'domain/state/StateObjectFactory'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {ConfirmQuestionExitModalComponent} from 'components/question-directives/modal-templates/confirm-question-exit-modal.component'; +import {QuestionsOpportunitiesSelectDifficultyModalComponent} from 'pages/topic-editor-page/modal-templates/questions-opportunities-select-difficulty-modal.component'; +import {ContextService} from 'services/context.service'; +import {ContributionAndReviewService} from '../services/contribution-and-review.service'; +import {ImageLocalStorageService} from 'services/image-local-storage.service'; +import {QuestionSuggestionBackendApiService} from 'pages/contributor-dashboard-page/services/question-suggestion-backend-api.service'; +import {QuestionValidationService} from 'services/question-validation.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; @Component({ selector: 'oppia-question-suggestion-editor-modal', - templateUrl: './question-suggestion-editor-modal.component.html' + templateUrl: './question-suggestion-editor-modal.component.html', }) export class QuestionSuggestionEditorModalComponent - extends ConfirmOrCancelModal implements OnInit { + extends ConfirmOrCancelModal + implements OnInit +{ // These properties below are initialized using Angular lifecycle hooks // where we need to do non-null assertion. For more information see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -60,8 +66,7 @@ export class QuestionSuggestionEditorModalComponent constructor( private questionUndoRedoService: QuestionUndoRedoService, - private questionSuggestionBackendApiService: - QuestionSuggestionBackendApiService, + private questionSuggestionBackendApiService: QuestionSuggestionBackendApiService, private alertsService: AlertsService, private contextService: ContextService, private imageLocalStorageService: ImageLocalStorageService, @@ -69,24 +74,29 @@ export class QuestionSuggestionEditorModalComponent private ngbModal: NgbModal, private ngbActiveModal: NgbActiveModal, private questionValidationService: QuestionValidationService, - private contributionAndReviewService: ContributionAndReviewService, + private contributionAndReviewService: ContributionAndReviewService ) { super(ngbActiveModal); } cancel(): void { if (this.questionUndoRedoService.hasChanges()) { - this.ngbModal.open(ConfirmQuestionExitModalComponent, { - backdrop: true, - }).result.then(() => { - this.ngbActiveModal.dismiss('cancel'); - this.imageLocalStorageService.flushStoredImagesData(); - this.contextService.resetImageSaveDestination(); - }, () => { - // Note to developers: - // This callback is triggered when the cancel button is clicked. - // No further action is needed. - }); + this.ngbModal + .open(ConfirmQuestionExitModalComponent, { + backdrop: true, + }) + .result.then( + () => { + this.ngbActiveModal.dismiss('cancel'); + this.imageLocalStorageService.flushStoredImagesData(); + this.contextService.resetImageSaveDestination(); + }, + () => { + // Note to developers: + // This callback is triggered when the cancel button is clicked. + // No further action is needed. + } + ); } else { this.imageLocalStorageService.flushStoredImagesData(); this.contextService.resetImageSaveDestination(); @@ -96,33 +106,41 @@ export class QuestionSuggestionEditorModalComponent onClickChangeDifficulty(): void { const modalRef: NgbModalRef = this.ngbModal.open( - QuestionsOpportunitiesSelectDifficultyModalComponent, { + QuestionsOpportunitiesSelectDifficultyModalComponent, + { backdrop: true, - }); + } + ); modalRef.componentInstance.skillId = this.skillId; - modalRef.result.then((result) => { - if (this.alertsService.warnings.length === 0) { - this.skillDifficulty = result.skillDifficulty; - this.setDifficultyString(this.skillDifficulty); + modalRef.result.then( + result => { + if (this.alertsService.warnings.length === 0) { + this.skillDifficulty = result.skillDifficulty; + this.setDifficultyString(this.skillDifficulty); + } + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. } - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + ); } // Checking if Question contains all requirements to enable // Save and Publish Question. isQuestionValid(): boolean { return this.questionValidationService.isQuestionValid( - this.question, this.misconceptionsBySkill); + this.question, + this.misconceptionsBySkill + ); } getQuestionValidationErrorMessage(): string | null { return this.questionValidationService.getValidationErrorMessage( - this.question); + this.question + ); } done(): void { @@ -130,12 +148,12 @@ export class QuestionSuggestionEditorModalComponent return; } if (!this.questionUndoRedoService.hasChanges()) { - this.alertsService.addInfoMessage( - 'No changes detected.', 5000); + this.alertsService.addInfoMessage('No changes detected.', 5000); return; } this.siteAnalyticsService.registerContributorDashboardSubmitSuggestionEvent( - 'Question'); + 'Question' + ); const imagesData = this.imageLocalStorageService.getStoredImagesData(); this.imageLocalStorageService.flushStoredImagesData(); this.contextService.resetImageSaveDestination(); @@ -150,18 +168,24 @@ export class QuestionSuggestionEditorModalComponent () => { this.alertsService.addSuccessMessage('Updated question.'); }, - () => {}); + () => {} + ); this.ngbActiveModal.close({ questionDict: questionDict, - skillDifficulty: this.skillDifficulty + skillDifficulty: this.skillDifficulty, }); } else { - this.questionSuggestionBackendApiService.submitSuggestionAsync( - this.question, this.skill, this.skillDifficulty, - imagesData).then( - () => { + this.questionSuggestionBackendApiService + .submitSuggestionAsync( + this.question, + this.skill, + this.skillDifficulty, + imagesData + ) + .then(() => { this.alertsService.addSuccessMessage( - 'Submitted question for review.'); + 'Submitted question for review.' + ); }); this.ngbActiveModal.close(); } @@ -173,18 +197,17 @@ export class QuestionSuggestionEditorModalComponent // error because of strict type checking. // @ts-ignore this.skillDifficultyString = Object.entries( - AppConstants.SKILL_DIFFICULTY_LABEL_TO_FLOAT).find( - entry => entry[1] === skillDifficulty)[0]; + AppConstants.SKILL_DIFFICULTY_LABEL_TO_FLOAT + ).find(entry => entry[1] === skillDifficulty)[0]; } ngOnInit(): void { this.canEditQuestion = true; this.newQuestionIsBeingCreated = true; - this.isEditing = ( - this.suggestionId !== '' ? true : false); + this.isEditing = this.suggestionId !== '' ? true : false; this.misconceptionsBySkill = {}; - this.misconceptionsBySkill[this.skill.getId()] = ( - this.skill.getMisconceptions()); + this.misconceptionsBySkill[this.skill.getId()] = + this.skill.getMisconceptions(); this.contextService.setCustomEntityContext( AppConstants.IMAGE_CONTEXT.QUESTION_SUGGESTIONS, this.skill.getId() diff --git a/core/templates/pages/contributor-dashboard-page/modal-templates/question-suggestion-review-modal.component.spec.ts b/core/templates/pages/contributor-dashboard-page/modal-templates/question-suggestion-review-modal.component.spec.ts index 11a0f50329be..d6928e405a3a 100644 --- a/core/templates/pages/contributor-dashboard-page/modal-templates/question-suggestion-review-modal.component.spec.ts +++ b/core/templates/pages/contributor-dashboard-page/modal-templates/question-suggestion-review-modal.component.spec.ts @@ -16,19 +16,35 @@ * @fileoverview Unit tests for QuestionSuggestionReviewModalcomponent. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed, fakeAsync, tick } from '@angular/core/testing'; -import { NgbActiveModal, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { FetchSkillResponse, SkillBackendApiService } from 'domain/skill/skill-backend-api.service'; -import { SkillObjectFactory } from 'domain/skill/SkillObjectFactory'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { SuggestionModalService } from 'services/suggestion-modal.service'; -import { QuestionSuggestionReviewModalComponent } from './question-suggestion-review-modal.component'; -import { ThreadDataBackendApiService, ThreadMessages } from 'pages/exploration-editor-page/feedback-tab/services/thread-data-backend-api.service'; -import { ContextService } from 'services/context.service'; -import { Question } from 'domain/question/QuestionObjectFactory'; -import { MisconceptionSkillMap } from 'domain/skill/MisconceptionObjectFactory'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + waitForAsync, + TestBed, + fakeAsync, + tick, +} from '@angular/core/testing'; +import { + NgbActiveModal, + NgbModal, + NgbModalRef, +} from '@ng-bootstrap/ng-bootstrap'; +import { + FetchSkillResponse, + SkillBackendApiService, +} from 'domain/skill/skill-backend-api.service'; +import {SkillObjectFactory} from 'domain/skill/SkillObjectFactory'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {SuggestionModalService} from 'services/suggestion-modal.service'; +import {QuestionSuggestionReviewModalComponent} from './question-suggestion-review-modal.component'; +import { + ThreadDataBackendApiService, + ThreadMessages, +} from 'pages/exploration-editor-page/feedback-tab/services/thread-data-backend-api.service'; +import {ContextService} from 'services/context.service'; +import {Question} from 'domain/question/QuestionObjectFactory'; +import {MisconceptionSkillMap} from 'domain/skill/MisconceptionObjectFactory'; import cloneDeep from 'lodash/cloneDeep'; class MockActiveModal { @@ -44,7 +60,7 @@ class MockActiveModal { class MockNgbModal { open() { return { - result: Promise.resolve() + result: Promise.resolve(), }; } } @@ -77,10 +93,12 @@ describe('Question Suggestion Review Modal component', () => { topic_name: null, question_count: 1, skill_description: questionHeader, - skill_rubrics: [{ - explanations: ['explanation'], - difficulty: 'Easy' - }] + skill_rubrics: [ + { + explanations: ['explanation'], + difficulty: 'Easy', + }, + ], }, suggestion: { suggestion_type: null, @@ -112,72 +130,76 @@ describe('Question Suggestion Review Modal component', () => { question_state_data: { content: { html: contentHtml, - content_id: 'content_1' + content_id: 'content_1', }, interaction: { - answer_groups: [{ - outcome: { - dest: 'outcome 1', - dest_if_really_stuck: null, - feedback: { - content_id: 'content_5', - html: '' + answer_groups: [ + { + outcome: { + dest: 'outcome 1', + dest_if_really_stuck: null, + feedback: { + content_id: 'content_5', + html: '', + }, + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, }, - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null + rule_specs: [], }, - rule_specs: [], - }], + ], confirmed_unclassified_answers: [], customization_args: { placeholder: { value: { content_id: 'ca_placeholder_0', - unicode_str: '' - } + unicode_str: '', + }, }, - rows: { value: 1 }, + rows: {value: 1}, catchMisspellings: { - value: false - } + value: false, + }, }, default_outcome: { dest: null, dest_if_really_stuck: null, feedback: { html: 'Correct Answer', - content_id: 'content_2' + content_id: 'content_2', }, param_changes: [], - labelled_as_correct: true + labelled_as_correct: true, }, - hints: [{ - hint_content: { - html: 'Hint 1', - content_id: 'content_3' - } - }], + hints: [ + { + hint_content: { + html: 'Hint 1', + content_id: 'content_3', + }, + }, + ], solution: { correct_answer: 'This is the correct answer', answer_is_exclusive: false, explanation: { html: 'Solution explanation', - content_id: 'content_4' - } + content_id: 'content_4', + }, }, - id: 'TextInput' + id: 'TextInput', }, param_changes: [], recorded_voiceovers: { - voiceovers_mapping: {} - } + voiceovers_mapping: {}, + }, }, }, skill_difficulty: skillDifficulty, - skill_id: 'skill_1' - } - } + skill_id: 'skill_1', + }, + }, }, 2: { details: { @@ -187,10 +209,12 @@ describe('Question Suggestion Review Modal component', () => { story_title: null, question_count: 1, skill_description: questionHeader, - skill_rubrics: [{ - explanations: ['explanation'], - difficulty: 'Easy' - }] + skill_rubrics: [ + { + explanations: ['explanation'], + difficulty: 'Easy', + }, + ], }, suggestion: { suggestion_id: null, @@ -222,98 +246,99 @@ describe('Question Suggestion Review Modal component', () => { question_state_data: { content: { html: 'Question 1', - content_id: 'content_1' + content_id: 'content_1', }, interaction: { - answer_groups: [{ - outcome: { - dest: 'outcome 1', - dest_if_really_stuck: null, - feedback: { - content_id: 'content_5', - html: '' + answer_groups: [ + { + outcome: { + dest: 'outcome 1', + dest_if_really_stuck: null, + feedback: { + content_id: 'content_5', + html: '', + }, + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, }, - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null + rule_specs: [], }, - rule_specs: [], - }], + ], confirmed_unclassified_answers: [], customization_args: { placeholder: { value: { content_id: 'ca_placeholder_0', - unicode_str: '' - } + unicode_str: '', + }, }, - rows: { value: 1 }, + rows: {value: 1}, catchMisspellings: { - value: false - } + value: false, + }, }, default_outcome: { dest: null, dest_if_really_stuck: null, feedback: { html: 'Correct Answer', - content_id: 'content_2' + content_id: 'content_2', }, param_changes: [], - labelled_as_correct: true + labelled_as_correct: true, }, - hints: [{ - hint_content: { - html: 'Hint 1', - content_id: 'content_3' - } - }], + hints: [ + { + hint_content: { + html: 'Hint 1', + content_id: 'content_3', + }, + }, + ], solution: { correct_answer: 'component is the correct answer', answer_is_exclusive: false, explanation: { html: 'Solution explanation', - content_id: 'content_4' - } + content_id: 'content_4', + }, }, - id: 'TextInput' + id: 'TextInput', }, param_changes: [], recorded_voiceovers: { - voiceovers_mapping: {} - } + voiceovers_mapping: {}, + }, }, }, skill_difficulty: skillDifficulty, - skill_id: 'skill_1' - } - } + skill_id: 'skill_1', + }, + }, }, }; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - QuestionSuggestionReviewModalComponent - ], + declarations: [QuestionSuggestionReviewModalComponent], providers: [ { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, ThreadDataBackendApiService, { provide: NgbActiveModal, - useClass: MockActiveModal + useClass: MockActiveModal, }, - ContextService + ContextService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); - beforeEach(() => { fixture = TestBed.createComponent(QuestionSuggestionReviewModalComponent); component = fixture.componentInstance; @@ -334,30 +359,33 @@ describe('Question Suggestion Review Modal component', () => { skillBackendApiService = TestBed.inject(SkillBackendApiService); skillObjectFactory = TestBed.inject(SkillObjectFactory); siteAnalyticsService = TestBed.inject(SiteAnalyticsService); - threadDataBackendApiService = TestBed.inject( - ThreadDataBackendApiService); - contextService = TestBed.inject( - ContextService); + threadDataBackendApiService = TestBed.inject(ThreadDataBackendApiService); + contextService = TestBed.inject(ContextService); spyOn( siteAnalyticsService, - 'registerContributorDashboardViewSuggestionForReview'); + 'registerContributorDashboardViewSuggestionForReview' + ); spyOn(skillBackendApiService, 'fetchSkillAsync').and.returnValue( Promise.resolve({ skill: skillObjectFactory.createFromBackendDict({ id: 'skill1', description: 'test description 1', - misconceptions: [{ - id: 2, - name: 'test name', - notes: 'test notes', - feedback: 'test feedback', - must_be_addressed: true - }], - rubrics: [{ - difficulty: 'Easy', - explanations: ['explanation'] - }], + misconceptions: [ + { + id: 2, + name: 'test name', + notes: 'test notes', + feedback: 'test feedback', + must_be_addressed: true, + }, + ], + rubrics: [ + { + difficulty: 'Easy', + explanations: ['explanation'], + }, + ], skill_contents: { explanation: { html: 'test explanation', @@ -365,61 +393,66 @@ describe('Question Suggestion Review Modal component', () => { }, worked_examples: [], recorded_voiceovers: { - voiceovers_mapping: {} - } + voiceovers_mapping: {}, + }, }, language_code: 'en', version: 3, prerequisite_skill_ids: ['skill_1'], all_questions_merged: false, next_misconception_id: 0, - superseding_skill_id: '' - }) - } as FetchSkillResponse)); - - component.skillRubrics = [{ - explanations: ['explanation'], - difficulty: 'Easy' - }]; + superseding_skill_id: '', + }), + } as FetchSkillResponse) + ); + + component.skillRubrics = [ + { + explanations: ['explanation'], + difficulty: 'Easy', + }, + ]; component.ngOnInit(); }); - describe('when skill rubrics is specified', () => { - it('should open edit question modal when clicking on' + - ' edit button', fakeAsync(() => { - class MockNgbModalRef { - componentInstance = { - suggestionId: suggestionId, - question: question, - questionId: '', - questionStateData: question.getStateData(), - skill: null, - skillDifficulty: 0.3 - }; - } - - const questionDict = cloneDeep( - component.suggestionIdToContribution[suggestionId] - .suggestion.change_cmd.question_dict - ); - - spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ( - { componentInstance: MockNgbModalRef, - result: Promise.resolve( - {questionDict: questionDict, skillDifficulty: 0.3} - ) - }) as NgbModalRef; - }); + it( + 'should open edit question modal when clicking on' + ' edit button', + fakeAsync(() => { + class MockNgbModalRef { + componentInstance = { + suggestionId: suggestionId, + question: question, + questionId: '', + questionStateData: question.getStateData(), + skill: null, + skillDifficulty: 0.3, + }; + } - component.suggestion.change_cmd.skill_id = 'skill_1'; - component.edit(); - tick(); + const questionDict = cloneDeep( + component.suggestionIdToContribution[suggestionId].suggestion + .change_cmd.question_dict + ); + + spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + componentInstance: MockNgbModalRef, + result: Promise.resolve({ + questionDict: questionDict, + skillDifficulty: 0.3, + }), + } as NgbModalRef; + }); + + component.suggestion.change_cmd.skill_id = 'skill_1'; + component.edit(); + tick(); - expect(ngbModal.open).toHaveBeenCalled(); - })); + expect(ngbModal.open).toHaveBeenCalled(); + }) + ); it('should throw error edit if skill id is null', fakeAsync(() => { class MockNgbModalRef { @@ -429,15 +462,15 @@ describe('Question Suggestion Review Modal component', () => { questionId: '', questionStateData: question.getStateData(), skill: null, - skillDifficulty: 0.3 + skillDifficulty: 0.3, }; } spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ( - { componentInstance: MockNgbModalRef, - result: Promise.resolve() - }) as NgbModalRef; + return { + componentInstance: MockNgbModalRef, + result: Promise.resolve(), + } as NgbModalRef; }); component.suggestion.change_cmd.skill_id = undefined; @@ -447,189 +480,210 @@ describe('Question Suggestion Review Modal component', () => { }).toThrowError(); })); - it('should open edit question modal when clicking on' + - ' edit button', fakeAsync(() => { - spyOn(contextService, 'resetImageSaveDestination').and.stub(); - class MockNgbModalRef { - componentInstance = { - suggestionId: suggestionId, - question: question, - questionId: '', - questionStateData: question.getStateData(), - skill: null, - skillDifficulty: 0.3 - }; - } - - spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ( - { componentInstance: MockNgbModalRef, - result: Promise.reject() - }) as NgbModalRef; - }); - - component.suggestion.change_cmd.skill_id = 'skill_1'; - component.edit(); - tick(); - - expect(contextService.resetImageSaveDestination).toHaveBeenCalled(); - expect(ngbModal.open).toHaveBeenCalled(); - })); - - it('should update review question modal when edit question modal is' + - ' resolved', fakeAsync(() => { - class MockNgbModalRef { - componentInstance = { - suggestionId: suggestionId, - question: question, - questionId: '', - questionStateData: question.getStateData(), - skill: null, - skillDifficulty: 0.3 - }; - } - - const newContentHtml = 'new html'; - const newSkillDifficulty = 1; - - const suggestionChange = ( - component.suggestionIdToContribution[suggestionId].suggestion. - change_cmd); - const newQuestionDict = cloneDeep(suggestionChange.question_dict); - newQuestionDict.question_state_data.content.html = newContentHtml; + it( + 'should open edit question modal when clicking on' + ' edit button', + fakeAsync(() => { + spyOn(contextService, 'resetImageSaveDestination').and.stub(); + class MockNgbModalRef { + componentInstance = { + suggestionId: suggestionId, + question: question, + questionId: '', + questionStateData: question.getStateData(), + skill: null, + skillDifficulty: 0.3, + }; + } - expect( - component.question.getStateData().content.html - ).toEqual(contentHtml); - expect( - suggestionChange.question_dict.question_state_data.content.html - ).toEqual(contentHtml); - expect(component.skillDifficulty).toBe(skillDifficulty); - expect(suggestionChange.skill_difficulty).toEqual(skillDifficulty); + spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + componentInstance: MockNgbModalRef, + result: Promise.reject(), + } as NgbModalRef; + }); - spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ( - { componentInstance: MockNgbModalRef, - result: Promise.resolve({ - questionDict: newQuestionDict, - skillDifficulty: newSkillDifficulty - }) - }) as NgbModalRef; - }); + component.suggestion.change_cmd.skill_id = 'skill_1'; + component.edit(); + tick(); - component.suggestion.change_cmd.skill_id = 'skill_1'; - component.edit(); - tick(); - - expect(ngbModal.open).toHaveBeenCalled(); - expect( - component.question.getStateData().content.html - ).toEqual(newContentHtml); - expect( - suggestionChange.question_dict.question_state_data.content.html - ).toEqual(newContentHtml); - expect(component.skillDifficulty).toBe(newSkillDifficulty); - expect(suggestionChange.skill_difficulty).toEqual(newSkillDifficulty); - - suggestionChange.question_dict.question_state_data.content.html = ( - contentHtml); - suggestionChange.skill_difficulty = skillDifficulty; - })); + expect(contextService.resetImageSaveDestination).toHaveBeenCalled(); + expect(ngbModal.open).toHaveBeenCalled(); + }) + ); + + it( + 'should update review question modal when edit question modal is' + + ' resolved', + fakeAsync(() => { + class MockNgbModalRef { + componentInstance = { + suggestionId: suggestionId, + question: question, + questionId: '', + questionStateData: question.getStateData(), + skill: null, + skillDifficulty: 0.3, + }; + } - it('should initialize properties after component is initialized', - () => { - expect(component.authorName).toBe(authorName); - expect(component.contentHtml).toBe(contentHtml); - expect(component.reviewable).toBe(reviewable); - expect(component.reviewMessage).toBe(''); - expect(component.questionHeader).toBe(questionHeader); - expect(component.canEditQuestion).toBe(false); - expect(component.skillDifficultyLabel).toBe('Easy'); - expect(component.skillRubricExplanations).toEqual(['explanation']); - }); + const newContentHtml = 'new html'; + const newSkillDifficulty = 1; + + const suggestionChange = + component.suggestionIdToContribution[suggestionId].suggestion + .change_cmd; + const newQuestionDict = cloneDeep(suggestionChange.question_dict); + newQuestionDict.question_state_data.content.html = newContentHtml; + + expect(component.question.getStateData().content.html).toEqual( + contentHtml + ); + expect( + suggestionChange.question_dict.question_state_data.content.html + ).toEqual(contentHtml); + expect(component.skillDifficulty).toBe(skillDifficulty); + expect(suggestionChange.skill_difficulty).toEqual(skillDifficulty); + + spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + componentInstance: MockNgbModalRef, + result: Promise.resolve({ + questionDict: newQuestionDict, + skillDifficulty: newSkillDifficulty, + }), + } as NgbModalRef; + }); + + component.suggestion.change_cmd.skill_id = 'skill_1'; + component.edit(); + tick(); - it('should register Contributor Dashboard view suggestion for review' + - ' event after component is initialized', () => { - expect( - siteAnalyticsService - .registerContributorDashboardViewSuggestionForReview) - .toHaveBeenCalledWith('Question'); + expect(ngbModal.open).toHaveBeenCalled(); + expect(component.question.getStateData().content.html).toEqual( + newContentHtml + ); + expect( + suggestionChange.question_dict.question_state_data.content.html + ).toEqual(newContentHtml); + expect(component.skillDifficulty).toBe(newSkillDifficulty); + expect(suggestionChange.skill_difficulty).toEqual(newSkillDifficulty); + + suggestionChange.question_dict.question_state_data.content.html = + contentHtml; + suggestionChange.skill_difficulty = skillDifficulty; + }) + ); + + it('should initialize properties after component is initialized', () => { + expect(component.authorName).toBe(authorName); + expect(component.contentHtml).toBe(contentHtml); + expect(component.reviewable).toBe(reviewable); + expect(component.reviewMessage).toBe(''); + expect(component.questionHeader).toBe(questionHeader); + expect(component.canEditQuestion).toBe(false); + expect(component.skillDifficultyLabel).toBe('Easy'); + expect(component.skillRubricExplanations).toEqual(['explanation']); }); - it('should reset validation error message when user updates question', + it( + 'should register Contributor Dashboard view suggestion for review' + + ' event after component is initialized', () => { - component.validationError = 'component is an error message'; - component.questionChanged(); - expect(component.validationError).toBeNull(); - }); - - it('should accept suggestion in suggestion modal when clicking accept' + - ' suggestion', fakeAsync(() => { - spyOn( - siteAnalyticsService, - 'registerContributorDashboardAcceptSuggestion'); - component.reviewMessage = 'Review message example'; - - component.accept(); - tick(); + expect( + siteAnalyticsService.registerContributorDashboardViewSuggestionForReview + ).toHaveBeenCalledWith('Question'); + } + ); - expect( - siteAnalyticsService.registerContributorDashboardAcceptSuggestion) - .toHaveBeenCalledWith('Question'); - })); + it('should reset validation error message when user updates question', () => { + component.validationError = 'component is an error message'; + component.questionChanged(); + expect(component.validationError).toBeNull(); + }); - it('should reject suggestion in suggestion modal when clicking reject' + - ' suggestion button', fakeAsync(() => { - spyOn( - siteAnalyticsService, - 'registerContributorDashboardRejectSuggestion'); - component.reviewMessage = 'Review message example'; + it( + 'should accept suggestion in suggestion modal when clicking accept' + + ' suggestion', + fakeAsync(() => { + spyOn( + siteAnalyticsService, + 'registerContributorDashboardAcceptSuggestion' + ); + component.reviewMessage = 'Review message example'; + + component.accept(); + tick(); - component.reject(); - tick(); + expect( + siteAnalyticsService.registerContributorDashboardAcceptSuggestion + ).toHaveBeenCalledWith('Question'); + }) + ); + + it( + 'should reject suggestion in suggestion modal when clicking reject' + + ' suggestion button', + fakeAsync(() => { + spyOn( + siteAnalyticsService, + 'registerContributorDashboardRejectSuggestion' + ); + component.reviewMessage = 'Review message example'; + + component.reject(); + tick(); - expect( - siteAnalyticsService.registerContributorDashboardRejectSuggestion) - .toHaveBeenCalledWith('Question'); - })); + expect( + siteAnalyticsService.registerContributorDashboardRejectSuggestion + ).toHaveBeenCalledWith('Question'); + }) + ); - it('should cancel suggestion in suggestion modal when clicking cancel' + - ' suggestion button', () => { - component.cancel(); + it( + 'should cancel suggestion in suggestion modal when clicking cancel' + + ' suggestion button', + () => { + component.cancel(); - expect(cancelSuggestionSpy).toHaveBeenCalled(); - }); + expect(cancelSuggestionSpy).toHaveBeenCalled(); + } + ); }); - it('should initialize properties after component is initialized', - () => { - component.skillRubrics = []; + it('should initialize properties after component is initialized', () => { + component.skillRubrics = []; - expect(component.getRubricExplanation('nothing')).toBe( - 'This rubric has not yet been specified.'); - }); + expect(component.getRubricExplanation('nothing')).toBe( + 'This rubric has not yet been specified.' + ); + }); it('should fetch the rejection message', fakeAsync(() => { component.currentSuggestionId = '2'; - component.skillRubrics = [{ - explanations: ['explanation'], - difficulty: 'Easy' - }]; + component.skillRubrics = [ + { + explanations: ['explanation'], + difficulty: 'Easy', + }, + ]; const messages = [ - { text: 'Question submitted.', author_username: 'Contributor' }, - { text: 'This is a rejection.', author_username: 'Reviewer' } + {text: 'Question submitted.', author_username: 'Contributor'}, + {text: 'This is a rejection.', author_username: 'Reviewer'}, ]; component.reviewable = false; component.suggestionIsRejected = true; spyOn(component, '_getThreadMessagesAsync').and.callThrough(); const fetchMessagesAsyncSpy = spyOn( - threadDataBackendApiService, 'fetchMessagesAsync') - .and.returnValue(Promise.resolve({ - messages: messages - } as ThreadMessages)); + threadDataBackendApiService, + 'fetchMessagesAsync' + ).and.returnValue( + Promise.resolve({ + messages: messages, + } as ThreadMessages) + ); component.refreshContributionState(); tick(1000); @@ -640,7 +694,7 @@ describe('Question Suggestion Review Modal component', () => { expect(component.reviewer).toBe('Reviewer'); })); - it('should allow users to navigate between suggestions', fakeAsync(()=>{ + it('should allow users to navigate between suggestions', fakeAsync(() => { spyOn(component, 'refreshActiveContributionState').and.callThrough(); expect(component.currentSuggestionId).toEqual('1'); @@ -703,33 +757,32 @@ describe('Question Suggestion Review Modal component', () => { }).toThrowError(); })); - it('should not navigate if the corresponding opportunity is deleted', - function() { - spyOn(component, 'cancel'); - let details1 = component.allContributions['1'].details; - let details2 = component.allContributions['2'].details; - // This throws "Type 'null' is not assignable to type - // 'ActiveContributionDetailsDict'." We need to suppress this error - // because of the need to test validations. This error is thrown - // because the details are null. - // @ts-ignore - component.allContributions['2'].details = null; + it('should not navigate if the corresponding opportunity is deleted', function () { + spyOn(component, 'cancel'); + let details1 = component.allContributions['1'].details; + let details2 = component.allContributions['2'].details; + // This throws "Type 'null' is not assignable to type + // 'ActiveContributionDetailsDict'." We need to suppress this error + // because of the need to test validations. This error is thrown + // because the details are null. + // @ts-ignore + component.allContributions['2'].details = null; - component.goToNextItem(); - expect(component.cancel).toHaveBeenCalled(); - component.allContributions['2'].details = details2; - component.goToNextItem(); - // This throws "Type 'null' is not assignable to type - // 'ActiveContributionDetailsDict'." We need to suppress this error - // because of the need to test validations. This error is thrown - // because the details are null. - // @ts-ignore - component.allContributions['1'].details = null; + component.goToNextItem(); + expect(component.cancel).toHaveBeenCalled(); + component.allContributions['2'].details = details2; + component.goToNextItem(); + // This throws "Type 'null' is not assignable to type + // 'ActiveContributionDetailsDict'." We need to suppress this error + // because of the need to test validations. This error is thrown + // because the details are null. + // @ts-ignore + component.allContributions['1'].details = null; - component.goToPreviousItem(); - expect(component.cancel).toHaveBeenCalledWith(); + component.goToPreviousItem(); + expect(component.cancel).toHaveBeenCalledWith(); - component.allContributions['1'].details = details1; - component.allContributions['2'].details = details2; - }); + component.allContributions['1'].details = details1; + component.allContributions['2'].details = details2; + }); }); diff --git a/core/templates/pages/contributor-dashboard-page/modal-templates/question-suggestion-review-modal.component.ts b/core/templates/pages/contributor-dashboard-page/modal-templates/question-suggestion-review-modal.component.ts index fe73a9dd8d9a..10a8db6f334e 100644 --- a/core/templates/pages/contributor-dashboard-page/modal-templates/question-suggestion-review-modal.component.ts +++ b/core/templates/pages/contributor-dashboard-page/modal-templates/question-suggestion-review-modal.component.ts @@ -16,23 +16,30 @@ * @fileoverview Component for question suggestion review modal. */ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { AppConstants } from 'app.constants'; -import { Misconception, MisconceptionSkillMap } from 'domain/skill/MisconceptionObjectFactory'; -import { Question, QuestionBackendDict, QuestionObjectFactory } from 'domain/question/QuestionObjectFactory'; -import { SkillBackendApiService } from 'domain/skill/skill-backend-api.service'; -import { State } from 'domain/state/StateObjectFactory'; -import { ThreadMessage } from 'domain/feedback_message/ThreadMessage.model'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { QuestionSuggestionEditorModalComponent } from './question-suggestion-editor-modal.component'; -import { ContextService } from 'services/context.service'; -import { ContributionOpportunitiesService } from 'pages/contributor-dashboard-page/services/contribution-opportunities.service'; -import { ParamDict } from 'services/suggestion-modal.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { SuggestionModalService } from 'services/suggestion-modal.service'; -import { ThreadDataBackendApiService } from 'pages/exploration-editor-page/feedback-tab/services/thread-data-backend-api.service'; +import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbActiveModal, NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {AppConstants} from 'app.constants'; +import { + Misconception, + MisconceptionSkillMap, +} from 'domain/skill/MisconceptionObjectFactory'; +import { + Question, + QuestionBackendDict, + QuestionObjectFactory, +} from 'domain/question/QuestionObjectFactory'; +import {SkillBackendApiService} from 'domain/skill/skill-backend-api.service'; +import {State} from 'domain/state/StateObjectFactory'; +import {ThreadMessage} from 'domain/feedback_message/ThreadMessage.model'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {QuestionSuggestionEditorModalComponent} from './question-suggestion-editor-modal.component'; +import {ContextService} from 'services/context.service'; +import {ContributionOpportunitiesService} from 'pages/contributor-dashboard-page/services/contribution-opportunities.service'; +import {ParamDict} from 'services/suggestion-modal.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {SuggestionModalService} from 'services/suggestion-modal.service'; +import {ThreadDataBackendApiService} from 'pages/exploration-editor-page/feedback-tab/services/thread-data-backend-api.service'; interface QuestionSuggestionModalValue { suggestionId: string; @@ -49,9 +56,9 @@ interface SkillRubrics { interface ActiveContributionDetailsDict { skill_description: string; skill_rubrics: SkillRubrics[]; - 'chapter_title': string; - 'story_title': string; - 'topic_name': string; + chapter_title: string; + story_title: string; + topic_name: string; } interface SuggestionChangeValue { @@ -59,46 +66,48 @@ interface SuggestionChangeValue { } interface SuggestionChangeDict { - 'skill_difficulty': number; - 'question_dict': QuestionBackendDict; - 'new_value': SuggestionChangeValue; - 'old_value': SuggestionChangeValue; - 'skill_id'?: string; - 'cmd': string; - 'content_html': string | string[]; - 'content_id': string; - 'data_format': string; - 'language_code': string; - 'state_name': string; - 'translation_html': string; + skill_difficulty: number; + question_dict: QuestionBackendDict; + new_value: SuggestionChangeValue; + old_value: SuggestionChangeValue; + skill_id?: string; + cmd: string; + content_html: string | string[]; + content_id: string; + data_format: string; + language_code: string; + state_name: string; + translation_html: string; } interface ActiveSuggestionDict { - 'author_name': string; - 'change_cmd': SuggestionChangeDict; - 'exploration_content_html': string | string[] | null; - 'language_code': string; - 'last_updated_msecs': number; - 'status': string; - 'suggestion_id': string; - 'suggestion_type': string; - 'target_id': string; - 'target_type': string; + author_name: string; + change_cmd: SuggestionChangeDict; + exploration_content_html: string | string[] | null; + language_code: string; + last_updated_msecs: number; + status: string; + suggestion_id: string; + suggestion_type: string; + target_id: string; + target_type: string; } interface ActiveContributionDict { - 'details': ActiveContributionDetailsDict; - 'suggestion': ActiveSuggestionDict; + details: ActiveContributionDetailsDict; + suggestion: ActiveSuggestionDict; } @Component({ selector: 'oppia-question-suggestion-review-modal', - templateUrl: './question-suggestion-review.component.html' + templateUrl: './question-suggestion-review.component.html', }) export class QuestionSuggestionReviewModalComponent - extends ConfirmOrCancelModal implements OnInit { - @Output() editSuggestionEmitter = ( - new EventEmitter()); + extends ConfirmOrCancelModal + implements OnInit +{ + @Output() editSuggestionEmitter = + new EventEmitter(); // These properties below are initialized using Angular lifecycle hooks // where we need to do non-null assertion. For more information see @@ -143,7 +152,7 @@ export class QuestionSuggestionReviewModalComponent private skillBackendApiService: SkillBackendApiService, private suggestionModalService: SuggestionModalService, private threadDataBackendApiService: ThreadDataBackendApiService, - private questionObjectFactory: QuestionObjectFactory, + private questionObjectFactory: QuestionObjectFactory ) { super(ngbActiveModal); } @@ -158,79 +167,86 @@ export class QuestionSuggestionReviewModalComponent if (!skillId) { throw new Error('Skill ID is null.'); } - this.skillBackendApiService.fetchSkillAsync(skillId).then((skillDict) => { + this.skillBackendApiService.fetchSkillAsync(skillId).then(skillDict => { const modalRef = this.ngbModal.open( - QuestionSuggestionEditorModalComponent, { + QuestionSuggestionEditorModalComponent, + { size: 'lg', backdrop: 'static', keyboard: false, - }); + } + ); modalRef.componentInstance.suggestionId = this.suggestionId; modalRef.componentInstance.question = this.question; modalRef.componentInstance.questionId = ''; - modalRef.componentInstance.questionStateData = ( - this.question.getStateData()); + modalRef.componentInstance.questionStateData = + this.question.getStateData(); modalRef.componentInstance.skill = skillDict.skill; modalRef.componentInstance.skillDifficulty = this.skillDifficulty; - modalRef.result.then((change) => { - // When the question suggestion editor modal is closed, the changes made - // in that modal should also be reflected in the question suggestion - // review modal. Then, the reviewers can see the changes they have made - // and know that their changes have been saved successfully. - this.allContributions[this.suggestionId].suggestion.change_cmd - .question_dict = change.questionDict; - this.allContributions[this.suggestionId].suggestion.change_cmd - .skill_difficulty = change.skillDifficulty; - this.refreshContributionState(); - this.editSuggestionEmitter.emit( - { + modalRef.result.then( + change => { + // When the question suggestion editor modal is closed, the changes made + // in that modal should also be reflected in the question suggestion + // review modal. Then, the reviewers can see the changes they have made + // and know that their changes have been saved successfully. + this.allContributions[ + this.suggestionId + ].suggestion.change_cmd.question_dict = change.questionDict; + this.allContributions[ + this.suggestionId + ].suggestion.change_cmd.skill_difficulty = change.skillDifficulty; + this.refreshContributionState(); + this.editSuggestionEmitter.emit({ suggestionId: this.suggestionId, suggestion: this.suggestion, reviewable: this.reviewable, - question: this.question + question: this.question, }); - }, () => { - this.contextService.resetImageSaveDestination(); - this.editSuggestionEmitter.emit({ - suggestionId: this.suggestionId, - suggestion: this.suggestion, - reviewable: this.reviewable, - question: undefined - }); - }); + }, + () => { + this.contextService.resetImageSaveDestination(); + this.editSuggestionEmitter.emit({ + suggestionId: this.suggestionId, + suggestion: this.suggestion, + reviewable: this.reviewable, + question: undefined, + }); + } + ); }); } reject(): void { - this.contributionOpportunitiesService.removeOpportunitiesEventEmitter.emit( - [this.suggestionId]); + this.contributionOpportunitiesService.removeOpportunitiesEventEmitter.emit([ + this.suggestionId, + ]); this.siteAnalyticsService.registerContributorDashboardRejectSuggestion( - 'Question'); - this.suggestionModalService.rejectSuggestion( - this.ngbActiveModal, { - action: AppConstants.ACTION_REJECT_SUGGESTION, - reviewMessage: this.reviewMessage - } as ParamDict); + 'Question' + ); + this.suggestionModalService.rejectSuggestion(this.ngbActiveModal, { + action: AppConstants.ACTION_REJECT_SUGGESTION, + reviewMessage: this.reviewMessage, + } as ParamDict); } accept(): void { - this.contributionOpportunitiesService.removeOpportunitiesEventEmitter.emit( - [this.suggestionId]); + this.contributionOpportunitiesService.removeOpportunitiesEventEmitter.emit([ + this.suggestionId, + ]); this.siteAnalyticsService.registerContributorDashboardAcceptSuggestion( - 'Question'); - this.suggestionModalService.acceptSuggestion( - this.ngbActiveModal, { - action: AppConstants.ACTION_ACCEPT_SUGGESTION, - reviewMessage: this.reviewMessage, - skillDifficulty: this.skillDifficulty - }); + 'Question' + ); + this.suggestionModalService.acceptSuggestion(this.ngbActiveModal, { + action: AppConstants.ACTION_ACCEPT_SUGGESTION, + reviewMessage: this.reviewMessage, + skillDifficulty: this.skillDifficulty, + }); } refreshActiveContributionState(): void { - const nextContribution = this.allContributions[ - this.currentSuggestionId]; + const nextContribution = this.allContributions[this.currentSuggestionId]; this.suggestion = nextContribution.suggestion; this.isLastItem = this.remainingContributionIdStack.length === 0; @@ -243,7 +259,7 @@ export class QuestionSuggestionReviewModalComponent const skillId = this.suggestion.change_cmd.skill_id; if (skillId) { - this.skillBackendApiService.fetchSkillAsync(skillId).then((skillDict) => { + this.skillBackendApiService.fetchSkillAsync(skillId).then(skillDict => { let misconceptionsBySkill: Record = {}; const skill = skillDict.skill; misconceptionsBySkill[skill.getId()] = skill.getMisconceptions(); @@ -298,7 +314,8 @@ export class QuestionSuggestionReviewModalComponent getSkillDifficultyLabel(): string { const skillDifficultyFloatToLabel = this.invertMap( - AppConstants.SKILL_DIFFICULTY_LABEL_TO_FLOAT); + AppConstants.SKILL_DIFFICULTY_LABEL_TO_FLOAT + ); return skillDifficultyFloatToLabel[this.skillDifficulty]; } @@ -313,15 +330,16 @@ export class QuestionSuggestionReviewModalComponent } _getThreadMessagesAsync(threadId: string): Promise { - return this.threadDataBackendApiService.fetchMessagesAsync( - threadId).then((response) => { - const threadMessageBackendDicts = response.messages; - const reviewThreadMessage = ThreadMessage.createFromBackendDict( - threadMessageBackendDicts[1] - ); - this.reviewMessage = reviewThreadMessage.text; - this.reviewer = reviewThreadMessage.authorUsername; - }); + return this.threadDataBackendApiService + .fetchMessagesAsync(threadId) + .then(response => { + const threadMessageBackendDicts = response.messages; + const reviewThreadMessage = ThreadMessage.createFromBackendDict( + threadMessageBackendDicts[1] + ); + this.reviewMessage = reviewThreadMessage.text; + this.reviewer = reviewThreadMessage.authorUsername; + }); } questionChanged(): void { @@ -329,29 +347,30 @@ export class QuestionSuggestionReviewModalComponent } refreshContributionState(): void { - this.suggestion = ( - this.allContributions[this.currentSuggestionId].suggestion); + this.suggestion = + this.allContributions[this.currentSuggestionId].suggestion; this.question = this.questionObjectFactory.createFromBackendDict( - this.suggestion.change_cmd.question_dict); + this.suggestion.change_cmd.question_dict + ); this.authorName = this.suggestion.author_name; this.contentHtml = this.question.getStateData().content.html; - this.questionHeader = ( - this.allContributions[ - this.currentSuggestionId].details.skill_description); - this.skillRubrics = ( - this.allContributions[ - this.currentSuggestionId].details.skill_rubrics); + this.questionHeader = + this.allContributions[this.currentSuggestionId].details.skill_description; + this.skillRubrics = + this.allContributions[this.currentSuggestionId].details.skill_rubrics; this.questionStateData = this.question.getStateData(); this.questionId = this.question.getId(); this.canEditQuestion = false; this.skillDifficulty = this.suggestion.change_cmd.skill_difficulty; this.skillDifficultyLabel = this.getSkillDifficultyLabel(); this.skillRubricExplanations = this.getRubricExplanation( - this.skillDifficultyLabel); + this.skillDifficultyLabel + ); this.suggestionIsRejected = this.suggestion.status === 'rejected'; if (this.reviewable) { - this.siteAnalyticsService - .registerContributorDashboardViewSuggestionForReview('Question'); + this.siteAnalyticsService.registerContributorDashboardViewSuggestionForReview( + 'Question' + ); } else { this.reviewMessage = ''; // Reset for next/prev. this.reviewer = ''; @@ -382,7 +401,9 @@ export class QuestionSuggestionReviewModalComponent } } -angular.module('oppia').directive('oppiaQuestionSuggestionReviewModal', +angular.module('oppia').directive( + 'oppiaQuestionSuggestionReviewModal', downgradeComponent({ - component: QuestionSuggestionReviewModalComponent - }) as angular.IDirectiveFactory); + component: QuestionSuggestionReviewModalComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/contributor-dashboard-page/modal-templates/translation-modal.component.spec.ts b/core/templates/pages/contributor-dashboard-page/modal-templates/translation-modal.component.spec.ts index 830af93e2965..12559b9e63d9 100644 --- a/core/templates/pages/contributor-dashboard-page/modal-templates/translation-modal.component.spec.ts +++ b/core/templates/pages/contributor-dashboard-page/modal-templates/translation-modal.component.spec.ts @@ -14,33 +14,49 @@ /** * @fileoverview Unit tests for TranslationModalComponent. -*/ - -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { ChangeDetectorRef, ElementRef, NO_ERRORS_SCHEMA } from '@angular/core'; - -import { ComponentFixture, fakeAsync, flushMicrotasks, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { AppConstants } from 'app.constants'; -import { CkEditorCopyContentService } from 'components/ck-editor-helpers/ck-editor-copy-content.service'; -import { OppiaAngularRootComponent } from 'components/oppia-angular-root.component'; -import { TranslationModalComponent, TranslationOpportunity } from 'pages/contributor-dashboard-page/modal-templates/translation-modal.component'; -import { TranslationLanguageService } from 'pages/exploration-editor-page/translation-tab/services/translation-language.service'; -import { ContextService } from 'services/context.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { ImageLocalStorageService, ImagesData } from 'services/image-local-storage.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { UserService } from 'services/user.service'; -import { TranslateTextService } from '../services/translate-text.service'; -import { WrapTextWithEllipsisPipe } from 'filters/string-utility-filters/wrap-text-with-ellipsis.pipe'; + */ + +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {ChangeDetectorRef, ElementRef, NO_ERRORS_SCHEMA} from '@angular/core'; + +import { + ComponentFixture, + fakeAsync, + flushMicrotasks, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {AppConstants} from 'app.constants'; +import {CkEditorCopyContentService} from 'components/ck-editor-helpers/ck-editor-copy-content.service'; +import {OppiaAngularRootComponent} from 'components/oppia-angular-root.component'; +import { + TranslationModalComponent, + TranslationOpportunity, +} from 'pages/contributor-dashboard-page/modal-templates/translation-modal.component'; +import {TranslationLanguageService} from 'pages/exploration-editor-page/translation-tab/services/translation-language.service'; +import {ContextService} from 'services/context.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import { + ImageLocalStorageService, + ImagesData, +} from 'services/image-local-storage.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {UserService} from 'services/user.service'; +import {TranslateTextService} from '../services/translate-text.service'; +import {WrapTextWithEllipsisPipe} from 'filters/string-utility-filters/wrap-text-with-ellipsis.pipe'; // This throws "TS2307". We need to // suppress this error because rte-text-components are not strictly typed yet. // @ts-ignore -import { RteOutputDisplayComponent } from 'rich_text_components/rte-output-display.component'; +import {RteOutputDisplayComponent} from 'rich_text_components/rte-output-display.component'; enum ExpansionTabType { CONTENT, - TRANSLATION + TRANSLATION, } class MockChangeDetectorRef { @@ -70,7 +86,7 @@ describe('Translation Modal Component', () => { actionButtonTitle: 'Action Button', inReviewCount: 12, totalCount: 50, - translationsCount: 20 + translationsCount: 20, }; const getContentTranslatableItemWithText = (text: string) => { return { @@ -78,27 +94,22 @@ describe('Translation Modal Component', () => { content_value: text, content_type: 'content', interaction_id: null, - rule_type: null + rule_type: null, }; }; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - TranslationModalComponent, - WrapTextWithEllipsisPipe - ], + imports: [HttpClientTestingModule], + declarations: [TranslationModalComponent, WrapTextWithEllipsisPipe], providers: [ NgbActiveModal, { provide: ChangeDetectorRef, - useValue: changeDetectorRef - } + useValue: changeDetectorRef, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); OppiaAngularRootComponent.contextService = TestBed.inject(ContextService); contextService = OppiaAngularRootComponent.contextService; @@ -126,17 +137,23 @@ describe('Translation Modal Component', () => { // the need to test validations. This is because the component is not // strictly typed yet. // @ts-ignore - null, null, new ElementRef({offsetHeight: 200}), null); + null, + null, + new ElementRef({offsetHeight: 200}), + null + ); getUserContributionRightsDataAsyncSpy = spyOn( - userService, 'getUserContributionRightsDataAsync'); - getUserContributionRightsDataAsyncSpy.and.returnValue(Promise.resolve( - { + userService, + 'getUserContributionRightsDataAsync' + ); + getUserContributionRightsDataAsyncSpy.and.returnValue( + Promise.resolve({ can_suggest_questions: false, can_review_translation_for_language_codes: ['ar'], can_review_voiceover_for_language_codes: [], - can_review_questions: false - } - )); + can_review_questions: false, + }) + ); }); it('should invoke change detection when html is updated', () => { @@ -154,7 +171,7 @@ describe('Translation Modal Component', () => { expect(changeDetectorRef.detectChanges).toHaveBeenCalledTimes(0); }); - it('should return the ExoansionTabType enum', ()=>{ + it('should return the ExoansionTabType enum', () => { let enumVariable = component.expansionTabType; expect(typeof enumVariable === typeof ExpansionTabType); }); @@ -205,59 +222,57 @@ describe('Translation Modal Component', () => { expect(component.isTranslationExpanded).toBeTrue(); }); - it('should correctly determine whether the content data is overflowing', - fakeAsync(() => { - // Pre-check. - // The default values for the overflow states are false. - expect(component.isContentOverflowing).toBeFalse(); + it('should correctly determine whether the content data is overflowing', fakeAsync(() => { + // Pre-check. + // The default values for the overflow states are false. + expect(component.isContentOverflowing).toBeFalse(); - // Setup. - component.contentPanel.elementRef.nativeElement.offsetHeight = 100; - component.contentContainer.nativeElement.offsetHeight = 150; + // Setup. + component.contentPanel.elementRef.nativeElement.offsetHeight = 100; + component.contentContainer.nativeElement.offsetHeight = 150; - // Action. - component.computePanelOverflowState(); - tick(501); + // Action. + component.computePanelOverflowState(); + tick(501); - // Expectations. - expect(component.isContentOverflowing).toBeFalse(); - // Change panel height to simulate changing of the modal data. - component.contentPanel.elementRef.nativeElement.offsetHeight = 300; + // Expectations. + expect(component.isContentOverflowing).toBeFalse(); + // Change panel height to simulate changing of the modal data. + component.contentPanel.elementRef.nativeElement.offsetHeight = 300; - // Action. - component.computePanelOverflowState(); - tick(501); + // Action. + component.computePanelOverflowState(); + tick(501); - // Expectations. - expect(component.isContentOverflowing).toBeTrue(); - })); + // Expectations. + expect(component.isContentOverflowing).toBeTrue(); + })); - it('should correctly determine whether the editor is overflowing', - fakeAsync(() => { - // Pre-check. - // The default values for the overflow states are false. - expect(component.isTranslationOverflowing).toBeFalse(); + it('should correctly determine whether the editor is overflowing', fakeAsync(() => { + // Pre-check. + // The default values for the overflow states are false. + expect(component.isTranslationOverflowing).toBeFalse(); - // Setup. - spyOn(wds, 'getHeight').and.returnValue(100); - component.translationContainer.nativeElement.offsetHeight = 25; + // Setup. + spyOn(wds, 'getHeight').and.returnValue(100); + component.translationContainer.nativeElement.offsetHeight = 25; - // Action. - component.computeTranslationEditorOverflowState(); - tick(501); + // Action. + component.computeTranslationEditorOverflowState(); + tick(501); - // Expectations. - expect(component.isTranslationOverflowing).toBeFalse(); - // Change panel height to simulate changing of the modal data. - component.translationContainer.nativeElement.offsetHeight = 300; + // Expectations. + expect(component.isTranslationOverflowing).toBeFalse(); + // Change panel height to simulate changing of the modal data. + component.translationContainer.nativeElement.offsetHeight = 300; - // Action. - component.computeTranslationEditorOverflowState(); - tick(501); + // Action. + component.computeTranslationEditorOverflowState(); + tick(501); - // Expectations. - expect(component.isTranslationOverflowing).toBeTrue(); - })); + // Expectations. + expect(component.isTranslationOverflowing).toBeTrue(); + })); afterEach(() => { httpTestingController.verify(); @@ -274,13 +289,15 @@ describe('Translation Modal Component', () => { beforeEach(fakeAsync(() => { translationLanguageService.setActiveLanguageCode('ar'); spyOn(translateTextService, 'init').and.callFake( - (expId, languageCode, successCallback) => successCallback()); + (expId, languageCode, successCallback) => successCallback() + ); component.ngOnInit(); })); it('should set the schema constant correctly', () => { - expect(component.getHtmlSchema().ui_config.languageDirection) - .toBe('rtl'); + expect(component.getHtmlSchema().ui_config.languageDirection).toBe( + 'rtl' + ); }); }); @@ -288,37 +305,42 @@ describe('Translation Modal Component', () => { beforeEach(fakeAsync(() => { translationLanguageService.setActiveLanguageCode('es'); spyOn(translateTextService, 'init').and.callFake( - (expId, languageCode, successCallback) => successCallback()); + (expId, languageCode, successCallback) => successCallback() + ); component.ngOnInit(); })); it('should set the schema constant correctly', () => { - expect(component.getHtmlSchema().ui_config.languageDirection) - .toBe('ltr'); + expect(component.getHtmlSchema().ui_config.languageDirection).toBe( + 'ltr' + ); }); - it('should throw error if contribution rights is null', fakeAsync( - () => { - getUserContributionRightsDataAsyncSpy.and.returnValue(Promise.resolve( - null)); - expect(() => { - component.ngOnInit(); - tick(); - }).toThrowError(); - })); + it('should throw error if contribution rights is null', fakeAsync(() => { + getUserContributionRightsDataAsyncSpy.and.returnValue( + Promise.resolve(null) + ); + expect(() => { + component.ngOnInit(); + tick(); + }).toThrowError(); + })); }); it('should set context correctly', fakeAsync(() => { contextService.removeCustomEntityContext(); contextService.resetImageSaveDestination(); spyOn(translateTextService, 'init').and.callFake( - (expId, languageCode, successCallback) => successCallback()); + (expId, languageCode, successCallback) => successCallback() + ); component.ngOnInit(); expect(contextService.getEntityType()).toBe( - AppConstants.ENTITY_TYPE.EXPLORATION); + AppConstants.ENTITY_TYPE.EXPLORATION + ); expect(contextService.getEntityId()).toBe('1'); expect(contextService.getImageSaveDestination()).toBe( - AppConstants.IMAGE_SAVE_DESTINATION_LOCAL_STORAGE); + AppConstants.IMAGE_SAVE_DESTINATION_LOCAL_STORAGE + ); })); it('should compute panel overflow after the view has initialized', () => { @@ -334,30 +356,34 @@ describe('Translation Modal Component', () => { component.ngAfterContentChecked(); - expect(component.computeTranslationEditorOverflowState) - .toHaveBeenCalled(); + expect( + component.computeTranslationEditorOverflowState + ).toHaveBeenCalled(); }); it('should initialize translateTextService', fakeAsync(() => { spyOn(translateTextService, 'init').and.callThrough(); spyOn(translateTextService, 'getTextToTranslate').and.callThrough(); - spyOn(translateTextService, 'getPreviousTextToTranslate') - .and.callThrough(); + spyOn( + translateTextService, + 'getPreviousTextToTranslate' + ).and.callThrough(); component.ngOnInit(); expect(component.loadingData).toBeTrue(); expect(translateTextService.init).toHaveBeenCalled(); const sampleStateWiseContentMapping = { stateName1: {contentId1: getContentTranslatableItemWithText('text1')}, - stateName2: {contentId2: getContentTranslatableItemWithText('text2')} + stateName2: {contentId2: getContentTranslatableItemWithText('text2')}, }; const req = httpTestingController.expectOne( - '/gettranslatabletexthandler?exp_id=1&language_code=es'); + '/gettranslatabletexthandler?exp_id=1&language_code=es' + ); expect(req.request.method).toEqual('GET'); req.flush({ state_names_to_content_id_mapping: sampleStateWiseContentMapping, - version: 1 + version: 1, }); flushMicrotasks(); expect(component.loadingData).toBeFalse(); @@ -367,8 +393,9 @@ describe('Translation Modal Component', () => { expect(component.moreAvailable).toBeTrue(); component.skipActiveTranslation(); component.returnToPreviousTranslation(); - expect(translateTextService.getPreviousTextToTranslate) - .toHaveBeenCalled(); + expect( + translateTextService.getPreviousTextToTranslate + ).toHaveBeenCalled(); expect(component.textToTranslate).toBe('text1'); // The value of moreAvailable will be set to true when the operation // is viewing a previous translation. If the value is false, the @@ -377,52 +404,53 @@ describe('Translation Modal Component', () => { expect(component.moreAvailable).toBeTrue(); })); - it('should set the schema constant based on the active language', fakeAsync( - () => { - translationLanguageService.setActiveLanguageCode('ar'); - spyOn(translateTextService, 'init').and.callFake( - (expId, languageCode, successCallback) => successCallback()); - component.ngOnInit(); - expect(component.getHtmlSchema().ui_config.language) - .toBe('ar'); - })); + it('should set the schema constant based on the active language', fakeAsync(() => { + translationLanguageService.setActiveLanguageCode('ar'); + spyOn(translateTextService, 'init').and.callFake( + (expId, languageCode, successCallback) => successCallback() + ); + component.ngOnInit(); + expect(component.getHtmlSchema().ui_config.language).toBe('ar'); + })); it('should get the unicode schema', () => { expect(component.getUnicodeSchema()).toEqual({type: 'unicode'}); }); it('should get the set of strings schema', () => { - expect(component.getSetOfStringsSchema()).toEqual( - { - type: 'list', - items: { - type: 'unicode' - } - } - ); + expect(component.getSetOfStringsSchema()).toEqual({ + type: 'list', + items: { + type: 'unicode', + }, + }); }); }); describe('when clicking on the translatable content', () => { const nonParagraphTarget: HTMLElement = document.createElement('div'); const mathTarget: HTMLElement = document.createElement( - 'oppia-noninteractive-math'); + 'oppia-noninteractive-math' + ); let paragraphTarget: HTMLElement; let broadcastSpy: jasmine.Spy<(target: HTMLElement) => void>; let propagationSpy: jasmine.Spy<() => void>; beforeEach(fakeAsync(() => { paragraphTarget = document.createElement('p'); spyOn(translateTextService, 'init').and.callFake( - (expId, languageCode, successCallback) => successCallback()); + (expId, languageCode, successCallback) => successCallback() + ); broadcastSpy = spyOn( - ckEditorCopyContentService, 'broadcastCopy').and.stub(); + ckEditorCopyContentService, + 'broadcastCopy' + ).and.stub(); component.ngOnInit(); - nonParagraphTarget.onclick = function(this, ev) { + nonParagraphTarget.onclick = function (this, ev) { propagationSpy = spyOn(ev, 'stopPropagation').and.stub(); component.onContentClick(ev); }; - paragraphTarget.onclick = function(this, ev) { + paragraphTarget.onclick = function (this, ev) { propagationSpy = spyOn(ev, 'stopPropagation').and.stub(); component.onContentClick(ev); }; @@ -470,21 +498,21 @@ describe('Translation Modal Component', () => { const sampleStateWiseContentMapping = { stateName1: {contentId1: getContentTranslatableItemWithText('text1')}, - stateName2: {contentId2: getContentTranslatableItemWithText('text2')} + stateName2: {contentId2: getContentTranslatableItemWithText('text2')}, }; const req = httpTestingController.expectOne( - '/gettranslatabletexthandler?exp_id=1&language_code=es'); + '/gettranslatabletexthandler?exp_id=1&language_code=es' + ); expect(req.request.method).toEqual('GET'); req.flush({ state_names_to_content_id_mapping: sampleStateWiseContentMapping, - version: 1 + version: 1, }); flushMicrotasks(); component.skipActiveTranslation(); })); - it('should retrieve remaining text and availability', () => { expect(component.textToTranslate).toBe('text2'); expect(component.moreAvailable).toBeFalse(); @@ -509,9 +537,9 @@ describe('Translation Modal Component', () => { language_code: 'es', content_html: 'text1', translation_html: 'texto1', - data_format: 'html' + data_format: 'html', }, - files: {} + files: {}, }; component.ngOnInit(); tick(); @@ -524,31 +552,32 @@ describe('Translation Modal Component', () => { content_value: 'input', content_type: 'interaction', interaction_id: 'TextInput', - rule_type: null + rule_type: null, }, contentId3: { content_format: 'unicode', content_value: 'Continue', content_type: 'ca', interaction_id: 'Continue', - rule_type: null + rule_type: null, }, contentId4: { content_format: 'set_of_normalized_string', content_value: ['answer1', 'answer2', 'answer3'], content_type: 'rule', interaction_id: 'TextInput', - rule_type: 'Contains' - } - } + rule_type: 'Contains', + }, + }, }; const req = httpTestingController.expectOne( - '/gettranslatabletexthandler?exp_id=1&language_code=es'); + '/gettranslatabletexthandler?exp_id=1&language_code=es' + ); expect(req.request.method).toEqual('GET'); req.flush({ state_names_to_content_id_mapping: sampleStateWiseContentMapping, - version: 1 + version: 1, }); flushMicrotasks(); component.activeWrittenTranslation = 'texto1'; @@ -560,12 +589,12 @@ describe('Translation Modal Component', () => { component.suggestTranslatedText(); flushMicrotasks(); - const req = httpTestingController.expectOne( - '/suggestionhandler/'); + const req = httpTestingController.expectOne('/suggestionhandler/'); expect(component.hadCopyParagraphError).toEqual(false); expect(req.request.method).toEqual('POST'); expect(req.request.body.getAll('payload')[0]).toEqual( - JSON.stringify(expectedPayload)); + JSON.stringify(expectedPayload) + ); req.flush({}); flushMicrotasks(); })); @@ -574,11 +603,11 @@ describe('Translation Modal Component', () => { component.suggestTranslatedText(); flushMicrotasks(); - const req = httpTestingController.expectOne( - '/suggestionhandler/'); + const req = httpTestingController.expectOne('/suggestionhandler/'); expect(req.request.method).toEqual('POST'); expect(req.request.body.getAll('payload')[0]).toEqual( - JSON.stringify(expectedPayload)); + JSON.stringify(expectedPayload) + ); req.flush({}); flushMicrotasks(); })); @@ -588,23 +617,24 @@ describe('Translation Modal Component', () => { spyOn(translateTextService, 'suggestTranslatedText').and.callThrough(); spyOn( imageLocalStorageService, - 'getFilenameToBase64MappingAsync').and.returnValue( - Promise.resolve({})); + 'getFilenameToBase64MappingAsync' + ).and.returnValue(Promise.resolve({})); component.suggestTranslatedText(); component.suggestTranslatedText(); tick(); - const req = httpTestingController.expectOne( - '/suggestionhandler/'); + const req = httpTestingController.expectOne('/suggestionhandler/'); expect(req.request.method).toEqual('POST'); expect(req.request.body.getAll('payload')[0]).toEqual( - JSON.stringify(expectedPayload)); + JSON.stringify(expectedPayload) + ); req.flush({}); flushMicrotasks(); // Prevention of concurrent suggestions also confirmed by "expectOne". - expect(translateTextService.suggestTranslatedText) - .toHaveBeenCalledTimes(1); + expect( + translateTextService.suggestTranslatedText + ).toHaveBeenCalledTimes(1); })); }); @@ -615,19 +645,22 @@ describe('Translation Modal Component', () => { component.suggestTranslatedText(); - expect(translateTextService.suggestTranslatedText) - .toHaveBeenCalledTimes(0); + expect( + translateTextService.suggestTranslatedText + ).toHaveBeenCalledTimes(0); }); }); describe('when alt text is not changed in copied images', () => { it('should not submit the translation', () => { - component.textToTranslate = ''; - component.activeWrittenTranslation = ''; - component.activeWrittenTranslation = ' { - const translatableItem = ( - this.translateTextService.getTextToTranslate()); + const translatableItem = this.translateTextService.getTextToTranslate(); this.updateActiveState(translatableItem); ({more: this.moreAvailable} = translatableItem); this.loadingData = false; - }); - this.userService.getUserContributionRightsDataAsync().then( - userContributionRights => { + } + ); + this.userService + .getUserContributionRightsDataAsync() + .then(userContributionRights => { if (!userContributionRights) { throw new Error('User contribution rights not found.'); } - const reviewableLanguageCodes = ( - userContributionRights.can_review_translation_for_language_codes); + const reviewableLanguageCodes = + userContributionRights.can_review_translation_for_language_codes; if (reviewableLanguageCodes.includes(this.activeLanguageCode)) { this.isActiveLanguageReviewer = true; } @@ -223,9 +237,9 @@ export class TranslationModalComponent { // complex extensions. hide_complex_extensions: false, language: this.translationLanguageService.getActiveLanguageCode(), - languageDirection: ( - this.translationLanguageService.getActiveLanguageDirection()) - } + languageDirection: + this.translationLanguageService.getActiveLanguageDirection(), + }, }; } @@ -239,12 +253,10 @@ export class TranslationModalComponent { computeTranslationEditorOverflowState(): void { const windowHeight = this.wds.getHeight(); - const heightLimit = windowHeight * (this.cutoff_height) / 100; + const heightLimit = (windowHeight * this.cutoff_height) / 100; - this.isTranslationOverflowing = ( - this.translationContainer?.nativeElement.offsetHeight >= - heightLimit - ); + this.isTranslationOverflowing = + this.translationContainer?.nativeElement.offsetHeight >= heightLimit; } computePanelOverflowState(): void { @@ -252,9 +264,9 @@ export class TranslationModalComponent { // before the overflow status is calculated. Values less than // 500ms also work but they sometimes lead to unexpected results. setTimeout(() => { - this.isContentOverflowing = ( + this.isContentOverflowing = this.contentPanel?.elementRef.nativeElement.offsetHeight > - this.contentContainer?.nativeElement.offsetHeight); + this.contentContainer?.nativeElement.offsetHeight; }, 500); } @@ -287,25 +299,21 @@ export class TranslationModalComponent { } updateActiveState(translatableItem: TranslatableItem): void { - ( - { - text: this.textToTranslate, - more: this.moreAvailable, - status: this.activeStatus, - translation: this.activeWrittenTranslation, - dataFormat: this.activeDataFormat - } = translatableItem - ); - const { + ({ + text: this.textToTranslate, + more: this.moreAvailable, + status: this.activeStatus, + translation: this.activeWrittenTranslation, + dataFormat: this.activeDataFormat, + } = translatableItem); + const {contentType, ruleType, interactionId} = translatableItem; + this.activeContentType = this.getFormattedContentType( contentType, - ruleType, interactionId - } = translatableItem; - this.activeContentType = this.getFormattedContentType( - contentType, interactionId ); this.activeRuleDescription = this.getRuleDescription( - ruleType, interactionId + ruleType, + interactionId ); } @@ -319,7 +327,7 @@ export class TranslationModalComponent { onContentClick(event: MouseEvent): boolean | void { if (this.triedToCopyParagraph(event)) { - return this.hadCopyParagraphError = true; + return (this.hadCopyParagraphError = true); } this.hadCopyParagraphError = false; if (this.isCopyModeActive()) { @@ -333,11 +341,11 @@ export class TranslationModalComponent { // Hence, math elements should be allowed to be copied. // See issue #11683. const target = $event.target as HTMLElement; - const paragraphChildrenElements: Element[] = ( - target.localName === 'p') ? Array.from( - target.children) : []; + const paragraphChildrenElements: Element[] = + target.localName === 'p' ? Array.from(target.children) : []; const mathElementsIncluded = paragraphChildrenElements.some( - child => child.localName === 'oppia-noninteractive-math'); + child => child.localName === 'oppia-noninteractive-math' + ); return target.localName === 'p' && !mathElementsIncluded; } @@ -357,8 +365,7 @@ export class TranslationModalComponent { } skipActiveTranslation(): void { - const translatableItem = ( - this.translateTextService.getTextToTranslate()); + const translatableItem = this.translateTextService.getTextToTranslate(); this.updateActiveState(translatableItem); ({more: this.moreAvailable} = translatableItem); this.resetEditor(); @@ -369,8 +376,8 @@ export class TranslationModalComponent { } returnToPreviousTranslation(): void { - const translatableItem = ( - this.translateTextService.getPreviousTextToTranslate()); + const translatableItem = + this.translateTextService.getPreviousTextToTranslate(); this.updateActiveState(translatableItem); this.moreAvailable = true; this.resetEditor(); @@ -385,7 +392,9 @@ export class TranslationModalComponent { } getFormattedContentType( - contentType: string, interactionId: string | undefined): string { + contentType: string, + interactionId: string | undefined + ): string { switch (contentType) { case 'interaction': return interactionId + ' interaction'; @@ -405,16 +414,20 @@ export class TranslationModalComponent { } // To match, e.g. "{{x|TranslatableSetOfNormalizedString}},". const descriptionPattern = /\{\{\s*(\w+)\s*(\|\s*\w+\s*)?\}\}/; - const ruleDescription = INTERACTION_SPECS[ - interactionId].rule_descriptions[ruleType]; - return 'Answer ' + ruleDescription.replace( - descriptionPattern, 'the following choices:'); + const ruleDescription = + INTERACTION_SPECS[interactionId].rule_descriptions[ruleType]; + return ( + 'Answer ' + + ruleDescription.replace(descriptionPattern, 'the following choices:') + ); } getElementAttributeTexts( - elements: HTMLCollectionOf, type: string): string[] { + elements: HTMLCollectionOf, + type: string + ): string[] { const textWrapperLength = 6; - const attributes = Array.from(elements, function(element: Element) { + const attributes = Array.from(elements, function (element: Element) { // A sample element would be as ; let component: TranslationSuggestionReviewModalComponent; let alertsService: AlertsService; @@ -52,9 +59,7 @@ describe('Translation Suggestion Review Modal Component', function() { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], + imports: [HttpClientTestingModule], declarations: [TranslationSuggestionReviewModalComponent], providers: [ NgbActiveModal, @@ -66,8 +71,8 @@ describe('Translation Suggestion Review Modal Component', function() { UserService, { provide: ChangeDetectorRef, - useValue: changeDetectorRef - } + useValue: changeDetectorRef, + }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); @@ -75,29 +80,38 @@ describe('Translation Suggestion Review Modal Component', function() { beforeEach(() => { fixture = TestBed.createComponent( - TranslationSuggestionReviewModalComponent); + TranslationSuggestionReviewModalComponent + ); component = fixture.componentInstance; activeModal = TestBed.inject(NgbActiveModal); alertsService = TestBed.inject(AlertsService); siteAnalyticsService = TestBed.inject(SiteAnalyticsService); threadDataBackendApiService = TestBed.inject(ThreadDataBackendApiService); userService = TestBed.inject(UserService); - contributionAndReviewService = TestBed.inject( - ContributionAndReviewService); + contributionAndReviewService = TestBed.inject(ContributionAndReviewService); languageUtilService = TestBed.inject(LanguageUtilService); spyOn( siteAnalyticsService, - 'registerContributorDashboardViewSuggestionForReview'); - spyOn( - languageUtilService, 'getAudioLanguageDescription') - .and.returnValue('audio_language_description'); + 'registerContributorDashboardViewSuggestionForReview' + ); + spyOn(languageUtilService, 'getAudioLanguageDescription').and.returnValue( + 'audio_language_description' + ); component.contentContainer = new ElementRef({offsetHeight: 150}); component.translationContainer = new ElementRef({offsetHeight: 150}); component.contentPanel = new RteOutputDisplayComponent( - null, null, new ElementRef({offsetHeight: 200}), null); + null, + null, + new ElementRef({offsetHeight: 200}), + null + ); component.translationPanel = new RteOutputDisplayComponent( - null, null, new ElementRef({offsetHeight: 200}), null); + null, + null, + new ElementRef({offsetHeight: 200}), + null + ); }); describe('when initializing the modal ', () => { @@ -122,7 +136,7 @@ describe('Translation Suggestion Review Modal Component', function() { data_format: 'html', language_code: 'language_code', }, - exploration_content_html: '

content

 

' + exploration_content_html: '

content

 

', }; const suggestion2 = { author_name: 'author_name', @@ -142,7 +156,7 @@ describe('Translation Suggestion Review Modal Component', function() { data_format: 'html', language_code: 'language_code', }, - exploration_content_html: '

content CHANGED

' + exploration_content_html: '

content CHANGED

', }; const suggestion3 = { author_name: 'author_name', @@ -162,7 +176,7 @@ describe('Translation Suggestion Review Modal Component', function() { data_format: 'html', language_code: 'language_code', }, - exploration_content_html: '

content CHANGED

' + exploration_content_html: '

content CHANGED

', }; const contribution1 = { @@ -170,77 +184,91 @@ describe('Translation Suggestion Review Modal Component', function() { details: { topic_name: 'topic_1', story_title: 'story_1', - chapter_title: 'chapter_1' - } + chapter_title: 'chapter_1', + }, }; const contribution2 = { suggestion: suggestion2, details: { topic_name: 'topic_2', story_title: 'story_2', - chapter_title: 'chapter_2' - } + chapter_title: 'chapter_2', + }, }; const contribution3 = { suggestion: suggestion3, details: { topic_name: 'topic_3', story_title: 'story_3', - chapter_title: 'chapter_3' - } + chapter_title: 'chapter_3', + }, }; const suggestionIdToContribution = { suggestion_1: contribution1, suggestion_2: contribution2, - suggestion_3: contribution3 + suggestion_3: contribution3, }; const editedContent = { - html: '

In Hindi

' + html: '

In Hindi

', }; beforeEach(() => { component.subheading = subheading; component.reviewable = reviewable; component.suggestionIdToContribution = angular.copy( - suggestionIdToContribution); + suggestionIdToContribution + ); component.editedContent = editedContent; }); - it('should be able to navigate to both previous suggestion and ' + - 'next suggestion if initial suggestion is in middle of list', () => { - component.initialSuggestionId = 'suggestion_2'; - component.ngOnInit(); + it( + 'should be able to navigate to both previous suggestion and ' + + 'next suggestion if initial suggestion is in middle of list', + () => { + component.initialSuggestionId = 'suggestion_2'; + component.ngOnInit(); - expect(component.activeSuggestionId).toBe('suggestion_2'); - expect(component.skippedContributionIds).toEqual(['suggestion_1']); - expect(component.remainingContributionIds).toEqual(['suggestion_3']); - }); + expect(component.activeSuggestionId).toBe('suggestion_2'); + expect(component.skippedContributionIds).toEqual(['suggestion_1']); + expect(component.remainingContributionIds).toEqual(['suggestion_3']); + } + ); - it('should be able to navigate to only previous suggestion ' + - 'if initial suggestion is the last suggestion of the list', () => { - component.initialSuggestionId = 'suggestion_3'; - component.ngOnInit(); + it( + 'should be able to navigate to only previous suggestion ' + + 'if initial suggestion is the last suggestion of the list', + () => { + component.initialSuggestionId = 'suggestion_3'; + component.ngOnInit(); - expect(component.activeSuggestionId).toBe('suggestion_3'); - expect(component.skippedContributionIds.sort()).toEqual( - ['suggestion_1', 'suggestion_2']); - expect(component.remainingContributionIds).toEqual([]); - }); + expect(component.activeSuggestionId).toBe('suggestion_3'); + expect(component.skippedContributionIds.sort()).toEqual([ + 'suggestion_1', + 'suggestion_2', + ]); + expect(component.remainingContributionIds).toEqual([]); + } + ); - it('should be able to navigate to only next suggestion ' + - 'if initial suggestion is in first suggestion of the list', () => { - component.initialSuggestionId = 'suggestion_1'; - component.ngOnInit(); + it( + 'should be able to navigate to only next suggestion ' + + 'if initial suggestion is in first suggestion of the list', + () => { + component.initialSuggestionId = 'suggestion_1'; + component.ngOnInit(); - expect(component.activeSuggestionId).toBe('suggestion_1'); - expect(component.skippedContributionIds).toEqual([]); - expect(component.remainingContributionIds.sort()).toEqual( - ['suggestion_2', 'suggestion_3']); - }); + expect(component.activeSuggestionId).toBe('suggestion_1'); + expect(component.skippedContributionIds).toEqual([]); + expect(component.remainingContributionIds.sort()).toEqual([ + 'suggestion_2', + 'suggestion_3', + ]); + } + ); }); - describe('when reviewing suggestion', function() { + describe('when reviewing suggestion', function () { const reviewable = true; const subheading = 'topic_1 / story_1 / chapter_1'; const suggestion1 = { @@ -261,7 +289,7 @@ describe('Translation Suggestion Review Modal Component', function() { data_format: 'html', language_code: 'language_code', }, - exploration_content_html: '

content

 

' + exploration_content_html: '

content

 

', }; const suggestion2 = { @@ -282,7 +310,7 @@ describe('Translation Suggestion Review Modal Component', function() { data_format: 'html', language_code: 'language_code', }, - exploration_content_html: '

content CHANGED

' + exploration_content_html: '

content CHANGED

', }; const contribution1 = { @@ -290,30 +318,38 @@ describe('Translation Suggestion Review Modal Component', function() { details: { topic_name: 'topic_1', story_title: 'story_1', - chapter_title: 'chapter_1' - } + chapter_title: 'chapter_1', + }, }; const contribution2 = { suggestion: suggestion2, details: { topic_name: 'topic_2', story_title: 'story_2', - chapter_title: 'chapter_2' - } + chapter_title: 'chapter_2', + }, }; const suggestionIdToContribution = { suggestion_1: contribution1, - suggestion_2: contribution2 + suggestion_2: contribution2, }; const editedContent = { - html: '

In Hindi

' + html: '

In Hindi

', }; const userInfo = new UserInfo( - ['USER_ROLE'], true, false, false, false, true, - 'en', 'username1', 'tester@example.com', true + ['USER_ROLE'], + true, + false, + false, + false, + true, + 'en', + 'username1', + 'tester@example.com', + true ); beforeEach(() => { @@ -321,66 +357,78 @@ describe('Translation Suggestion Review Modal Component', function() { component.subheading = subheading; component.reviewable = reviewable; component.suggestionIdToContribution = angular.copy( - suggestionIdToContribution); + suggestionIdToContribution + ); component.editedContent = editedContent; }); - it('should call user service at initialization.', - function() { - const userInfoSpy = spyOn( - userService, 'getUserInfoAsync') - .and.returnValue(Promise.resolve(userInfo)); - - const contributionRightsDataSpy = spyOn( - userService, - 'getUserContributionRightsDataAsync') - .and.returnValue(Promise.resolve( - { - can_review_translation_for_language_codes: ['ar'], - can_review_voiceover_for_language_codes: [], - can_review_questions: false, - can_suggest_questions: false - })); - component.ngOnInit(); - expect(userInfoSpy).toHaveBeenCalled(); - expect(contributionRightsDataSpy).toHaveBeenCalled(); - }); + it('should call user service at initialization.', function () { + const userInfoSpy = spyOn( + userService, + 'getUserInfoAsync' + ).and.returnValue(Promise.resolve(userInfo)); + + const contributionRightsDataSpy = spyOn( + userService, + 'getUserContributionRightsDataAsync' + ).and.returnValue( + Promise.resolve({ + can_review_translation_for_language_codes: ['ar'], + can_review_voiceover_for_language_codes: [], + can_review_questions: false, + can_suggest_questions: false, + }) + ); + component.ngOnInit(); + expect(userInfoSpy).toHaveBeenCalled(); + expect(contributionRightsDataSpy).toHaveBeenCalled(); + }); - it('should throw error if username is invalid', - fakeAsync(() => { - const defaultUserInfo = new UserInfo( - ['GUEST'], false, false, false, false, false, - null, null, null, false); - spyOn(userService, 'getUserInfoAsync').and - .returnValue(Promise.resolve(defaultUserInfo)); - - expect(() => { - component.ngOnInit(); - tick(); - }).toThrowError(); - flush(); - })); - - it('should initialize $scope properties after controller is initialized', - function() { + it('should throw error if username is invalid', fakeAsync(() => { + const defaultUserInfo = new UserInfo( + ['GUEST'], + false, + false, + false, + false, + false, + null, + null, + null, + false + ); + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve(defaultUserInfo) + ); + + expect(() => { component.ngOnInit(); - expect(component.subheading).toBe(subheading); - expect(component.reviewable).toBe(reviewable); - expect(component.activeSuggestionId).toBe('suggestion_1'); - expect(component.activeSuggestion).toEqual(suggestion1); - expect(component.reviewMessage).toBe(''); - }); + tick(); + }).toThrowError(); + flush(); + })); - it('should register Contributor Dashboard view suggestion for review ' + - 'event after controller is initialized', function() { + it('should initialize $scope properties after controller is initialized', function () { component.ngOnInit(); - expect( - siteAnalyticsService - .registerContributorDashboardViewSuggestionForReview) - .toHaveBeenCalledWith('Translation'); + expect(component.subheading).toBe(subheading); + expect(component.reviewable).toBe(reviewable); + expect(component.activeSuggestionId).toBe('suggestion_1'); + expect(component.activeSuggestion).toEqual(suggestion1); + expect(component.reviewMessage).toBe(''); }); - it('should notify user on failed suggestion update', function() { + it( + 'should register Contributor Dashboard view suggestion for review ' + + 'event after controller is initialized', + function () { + component.ngOnInit(); + expect( + siteAnalyticsService.registerContributorDashboardViewSuggestionForReview + ).toHaveBeenCalledWith('Translation'); + } + ); + + it('should notify user on failed suggestion update', function () { component.ngOnInit(); const error = new Error('Error'); expect(component.errorFound).toBeFalse(); @@ -392,234 +440,328 @@ describe('Translation Suggestion Review Modal Component', function() { expect(component.errorMessage).toBe('Invalid Suggestion: Error'); }); - it('should accept suggestion in suggestion modal service when clicking' + - ' on accept and review next suggestion button', function() { - component.ngOnInit(); - expect(component.activeSuggestionId).toBe('suggestion_1'); - expect(component.activeSuggestion).toEqual(suggestion1); - expect(component.reviewable).toBe(reviewable); - expect(component.reviewMessage).toBe(''); - // Suggestion 1's exploration_content_html matches its content_html. - expect(component.hasExplorationContentChanged()).toBe(false); - - spyOn( - siteAnalyticsService, - 'registerContributorDashboardAcceptSuggestion'); - spyOn(contributionAndReviewService, 'reviewExplorationSuggestion') - .and.callFake(( - targetId, suggestionId, action, reviewMessage, commitMessage, - successCallback, errorCallback) => { - return Promise.resolve(successCallback(suggestionId)); - }); - spyOn(activeModal, 'close'); - spyOn(alertsService, 'addSuccessMessage'); - - component.reviewMessage = 'Review message example'; - component.translationUpdated = true; - component.acceptAndReviewNext(); - - expect(component.activeSuggestionId).toBe('suggestion_2'); - expect(component.activeSuggestion).toEqual(suggestion2); - expect(component.reviewable).toBe(reviewable); - expect(component.reviewMessage).toBe(''); - // Suggestion 2's exploration_content_html does not match its - // content_html. - expect(component.hasExplorationContentChanged()).toBe(true); - expect( - siteAnalyticsService.registerContributorDashboardAcceptSuggestion) - .toHaveBeenCalledWith('Translation'); - expect(contributionAndReviewService.reviewExplorationSuggestion) - .toHaveBeenCalledWith( - '1', 'suggestion_1', 'accept', 'Review message example: ' + - '(Note: This suggestion was submitted with reviewer edits.)', - 'hint section of "StateName" card', - jasmine.any(Function), jasmine.any(Function)); - expect(alertsService.addSuccessMessage).toHaveBeenCalled(); - - component.reviewMessage = 'Review message example 2'; - component.translationUpdated = false; - component.acceptAndReviewNext(); + it( + 'should accept suggestion in suggestion modal service when clicking' + + ' on accept and review next suggestion button', + function () { + component.ngOnInit(); + expect(component.activeSuggestionId).toBe('suggestion_1'); + expect(component.activeSuggestion).toEqual(suggestion1); + expect(component.reviewable).toBe(reviewable); + expect(component.reviewMessage).toBe(''); + // Suggestion 1's exploration_content_html matches its content_html. + expect(component.hasExplorationContentChanged()).toBe(false); + + spyOn( + siteAnalyticsService, + 'registerContributorDashboardAcceptSuggestion' + ); + spyOn( + contributionAndReviewService, + 'reviewExplorationSuggestion' + ).and.callFake( + ( + targetId, + suggestionId, + action, + reviewMessage, + commitMessage, + successCallback, + errorCallback + ) => { + return Promise.resolve(successCallback(suggestionId)); + } + ); + spyOn(activeModal, 'close'); + spyOn(alertsService, 'addSuccessMessage'); - expect( - siteAnalyticsService.registerContributorDashboardAcceptSuggestion) - .toHaveBeenCalledWith('Translation'); - expect(contributionAndReviewService.reviewExplorationSuggestion) - .toHaveBeenCalledWith( - '2', 'suggestion_2', 'accept', 'Review message example 2', - 'hint section of "StateName" card', jasmine.any(Function), - jasmine.any(Function)); - expect(alertsService.addSuccessMessage).toHaveBeenCalled(); - expect(activeModal.close).toHaveBeenCalledWith([ - 'suggestion_1', 'suggestion_2']); - }); + component.reviewMessage = 'Review message example'; + component.translationUpdated = true; + component.acceptAndReviewNext(); - it('should set suggestion review message to auto-generated note when ' + - 'suggestion is accepted with edits and no user-supplied review message', - function() { - component.ngOnInit(); - expect(component.activeSuggestionId).toBe('suggestion_1'); - expect(component.activeSuggestion).toEqual(suggestion1); - expect(component.reviewable).toBe(reviewable); - expect(component.reviewMessage).toBe(''); + expect(component.activeSuggestionId).toBe('suggestion_2'); + expect(component.activeSuggestion).toEqual(suggestion2); + expect(component.reviewable).toBe(reviewable); + expect(component.reviewMessage).toBe(''); + // Suggestion 2's exploration_content_html does not match its + // content_html. + expect(component.hasExplorationContentChanged()).toBe(true); + expect( + siteAnalyticsService.registerContributorDashboardAcceptSuggestion + ).toHaveBeenCalledWith('Translation'); + expect( + contributionAndReviewService.reviewExplorationSuggestion + ).toHaveBeenCalledWith( + '1', + 'suggestion_1', + 'accept', + 'Review message example: ' + + '(Note: This suggestion was submitted with reviewer edits.)', + 'hint section of "StateName" card', + jasmine.any(Function), + jasmine.any(Function) + ); + expect(alertsService.addSuccessMessage).toHaveBeenCalled(); + + component.reviewMessage = 'Review message example 2'; + component.translationUpdated = false; + component.acceptAndReviewNext(); + + expect( + siteAnalyticsService.registerContributorDashboardAcceptSuggestion + ).toHaveBeenCalledWith('Translation'); + expect( + contributionAndReviewService.reviewExplorationSuggestion + ).toHaveBeenCalledWith( + '2', + 'suggestion_2', + 'accept', + 'Review message example 2', + 'hint section of "StateName" card', + jasmine.any(Function), + jasmine.any(Function) + ); + expect(alertsService.addSuccessMessage).toHaveBeenCalled(); + expect(activeModal.close).toHaveBeenCalledWith([ + 'suggestion_1', + 'suggestion_2', + ]); + } + ); - spyOn( - siteAnalyticsService, - 'registerContributorDashboardAcceptSuggestion'); - spyOn(contributionAndReviewService, 'reviewExplorationSuggestion') - .and.callFake(( - targetId, suggestionId, action, reviewMessage, commitMessage, - successCallback, errorCallback) => { - return Promise.resolve(successCallback(suggestionId)); - }); - spyOn(alertsService, 'addSuccessMessage'); - - component.translationUpdated = true; - component.acceptAndReviewNext(); + it( + 'should set suggestion review message to auto-generated note when ' + + 'suggestion is accepted with edits and no user-supplied review message', + function () { + component.ngOnInit(); + expect(component.activeSuggestionId).toBe('suggestion_1'); + expect(component.activeSuggestion).toEqual(suggestion1); + expect(component.reviewable).toBe(reviewable); + expect(component.reviewMessage).toBe(''); - expect( - siteAnalyticsService.registerContributorDashboardAcceptSuggestion) - .toHaveBeenCalledWith('Translation'); - expect(contributionAndReviewService.reviewExplorationSuggestion) - .toHaveBeenCalledWith( - '1', 'suggestion_1', 'accept', + spyOn( + siteAnalyticsService, + 'registerContributorDashboardAcceptSuggestion' + ); + spyOn( + contributionAndReviewService, + 'reviewExplorationSuggestion' + ).and.callFake( + ( + targetId, + suggestionId, + action, + reviewMessage, + commitMessage, + successCallback, + errorCallback + ) => { + return Promise.resolve(successCallback(suggestionId)); + } + ); + spyOn(alertsService, 'addSuccessMessage'); + + component.translationUpdated = true; + component.acceptAndReviewNext(); + + expect( + siteAnalyticsService.registerContributorDashboardAcceptSuggestion + ).toHaveBeenCalledWith('Translation'); + expect( + contributionAndReviewService.reviewExplorationSuggestion + ).toHaveBeenCalledWith( + '1', + 'suggestion_1', + 'accept', '(Note: This suggestion was submitted with reviewer edits.)', - 'hint section of "StateName" card', jasmine.any(Function), - jasmine.any(Function)); - expect(alertsService.addSuccessMessage).toHaveBeenCalled(); - }); - - it('should reject suggestion in suggestion modal service when clicking ' + - 'on reject and review next suggestion button', function() { - component.ngOnInit(); - expect(component.activeSuggestionId).toBe('suggestion_1'); - expect(component.activeSuggestion).toEqual(suggestion1); - expect(component.reviewable).toBe(reviewable); - expect(component.reviewMessage).toBe(''); + 'hint section of "StateName" card', + jasmine.any(Function), + jasmine.any(Function) + ); + expect(alertsService.addSuccessMessage).toHaveBeenCalled(); + } + ); - spyOn(contributionAndReviewService, 'reviewExplorationSuggestion') - .and.callFake(( - targetId, suggestionId, action, reviewMessage, commitMessage, - successCallback, errorCallback) => { - return Promise.resolve(successCallback(suggestionId)); - }); - spyOn( - siteAnalyticsService, - 'registerContributorDashboardRejectSuggestion'); - spyOn(activeModal, 'close'); - spyOn(alertsService, 'addSuccessMessage'); + it( + 'should reject suggestion in suggestion modal service when clicking ' + + 'on reject and review next suggestion button', + function () { + component.ngOnInit(); + expect(component.activeSuggestionId).toBe('suggestion_1'); + expect(component.activeSuggestion).toEqual(suggestion1); + expect(component.reviewable).toBe(reviewable); + expect(component.reviewMessage).toBe(''); - component.reviewMessage = 'Review message example'; - component.translationUpdated = true; - component.rejectAndReviewNext(component.reviewMessage); + spyOn( + contributionAndReviewService, + 'reviewExplorationSuggestion' + ).and.callFake( + ( + targetId, + suggestionId, + action, + reviewMessage, + commitMessage, + successCallback, + errorCallback + ) => { + return Promise.resolve(successCallback(suggestionId)); + } + ); + spyOn( + siteAnalyticsService, + 'registerContributorDashboardRejectSuggestion' + ); + spyOn(activeModal, 'close'); + spyOn(alertsService, 'addSuccessMessage'); - expect(component.activeSuggestionId).toBe('suggestion_2'); - expect(component.activeSuggestion).toEqual(suggestion2); - expect(component.reviewable).toBe(reviewable); - expect(component.reviewMessage).toBe(''); - expect( - siteAnalyticsService.registerContributorDashboardRejectSuggestion) - .toHaveBeenCalledWith('Translation'); - expect(contributionAndReviewService.reviewExplorationSuggestion) - .toHaveBeenCalledWith( - '1', 'suggestion_1', 'reject', 'Review message example', - null, jasmine.any(Function), - jasmine.any(Function)); - expect(alertsService.addSuccessMessage).toHaveBeenCalled(); - - component.reviewMessage = 'Review message example 2'; - component.translationUpdated = false; - component.rejectAndReviewNext(component.reviewMessage); + component.reviewMessage = 'Review message example'; + component.translationUpdated = true; + component.rejectAndReviewNext(component.reviewMessage); - expect( - siteAnalyticsService.registerContributorDashboardRejectSuggestion) - .toHaveBeenCalledWith('Translation'); - expect(alertsService.addSuccessMessage).toHaveBeenCalled(); - expect(activeModal.close).toHaveBeenCalledWith([ - 'suggestion_1', 'suggestion_2']); - }); + expect(component.activeSuggestionId).toBe('suggestion_2'); + expect(component.activeSuggestion).toEqual(suggestion2); + expect(component.reviewable).toBe(reviewable); + expect(component.reviewMessage).toBe(''); + expect( + siteAnalyticsService.registerContributorDashboardRejectSuggestion + ).toHaveBeenCalledWith('Translation'); + expect( + contributionAndReviewService.reviewExplorationSuggestion + ).toHaveBeenCalledWith( + '1', + 'suggestion_1', + 'reject', + 'Review message example', + null, + jasmine.any(Function), + jasmine.any(Function) + ); + expect(alertsService.addSuccessMessage).toHaveBeenCalled(); + + component.reviewMessage = 'Review message example 2'; + component.translationUpdated = false; + component.rejectAndReviewNext(component.reviewMessage); + + expect( + siteAnalyticsService.registerContributorDashboardRejectSuggestion + ).toHaveBeenCalledWith('Translation'); + expect(alertsService.addSuccessMessage).toHaveBeenCalled(); + expect(activeModal.close).toHaveBeenCalledWith([ + 'suggestion_1', + 'suggestion_2', + ]); + } + ); - it('should allow the reviewer to fix the suggestion if the backend pre' + - ' accept/reject validation failed', function() { - const responseMessage = 'Pre accept validation failed.'; + it( + 'should allow the reviewer to fix the suggestion if the backend pre' + + ' accept/reject validation failed', + function () { + const responseMessage = 'Pre accept validation failed.'; - component.ngOnInit(); - expect(component.activeSuggestionId).toBe('suggestion_1'); - expect(component.activeSuggestion).toEqual(suggestion1); - expect(component.reviewable).toBe(reviewable); - expect(component.reviewMessage).toBe(''); - spyOn( - siteAnalyticsService, - 'registerContributorDashboardAcceptSuggestion'); - spyOn( - siteAnalyticsService, - 'registerContributorDashboardRejectSuggestion'); - spyOn(contributionAndReviewService, 'reviewExplorationSuggestion') - .and.callFake(( - targetId, suggestionId, action, reviewMessage, commitMessage, - successCallback, errorCallback) => { - return Promise.reject( - errorCallback(responseMessage) - ); - }); - spyOn(alertsService, 'addWarning'); + component.ngOnInit(); + expect(component.activeSuggestionId).toBe('suggestion_1'); + expect(component.activeSuggestion).toEqual(suggestion1); + expect(component.reviewable).toBe(reviewable); + expect(component.reviewMessage).toBe(''); + spyOn( + siteAnalyticsService, + 'registerContributorDashboardAcceptSuggestion' + ); + spyOn( + siteAnalyticsService, + 'registerContributorDashboardRejectSuggestion' + ); + spyOn( + contributionAndReviewService, + 'reviewExplorationSuggestion' + ).and.callFake( + ( + targetId, + suggestionId, + action, + reviewMessage, + commitMessage, + successCallback, + errorCallback + ) => { + return Promise.reject(errorCallback(responseMessage)); + } + ); + spyOn(alertsService, 'addWarning'); + + component.reviewMessage = 'Review message example'; + component.acceptAndReviewNext(); - component.reviewMessage = 'Review message example'; - component.acceptAndReviewNext(); + expect(component.activeSuggestionId).toBe('suggestion_1'); + expect(component.activeSuggestion).toEqual(suggestion1); + expect(component.reviewable).toBe(reviewable); + expect(component.reviewMessage).toBe('Review message example'); + expect( + siteAnalyticsService.registerContributorDashboardAcceptSuggestion + ).toHaveBeenCalledWith('Translation'); + expect( + contributionAndReviewService.reviewExplorationSuggestion + ).toHaveBeenCalledWith( + '1', + 'suggestion_1', + 'accept', + 'Review message example', + 'hint section of "StateName" card', + jasmine.any(Function), + jasmine.any(Function) + ); + expect(alertsService.addWarning).toHaveBeenCalledWith( + jasmine.stringContaining(responseMessage) + ); - expect(component.activeSuggestionId).toBe('suggestion_1'); - expect(component.activeSuggestion).toEqual(suggestion1); - expect(component.reviewable).toBe(reviewable); - expect(component.reviewMessage).toBe('Review message example'); - expect( - siteAnalyticsService.registerContributorDashboardAcceptSuggestion) - .toHaveBeenCalledWith('Translation'); - expect(contributionAndReviewService.reviewExplorationSuggestion) - .toHaveBeenCalledWith( - '1', 'suggestion_1', 'accept', 'Review message example', - 'hint section of "StateName" card', jasmine.any(Function), - jasmine.any(Function)); - expect(alertsService.addWarning).toHaveBeenCalledWith( - jasmine.stringContaining(responseMessage)); - - component.reviewMessage = 'Edited review message example'; - component.rejectAndReviewNext(component.reviewMessage); + component.reviewMessage = 'Edited review message example'; + component.rejectAndReviewNext(component.reviewMessage); - expect(component.activeSuggestionId).toBe('suggestion_1'); - expect(component.activeSuggestion).toEqual(suggestion1); - expect(component.reviewable).toBe(reviewable); - expect(component.reviewMessage).toBe('Edited review message example'); - expect( - siteAnalyticsService.registerContributorDashboardRejectSuggestion) - .toHaveBeenCalledWith('Translation'); - expect(contributionAndReviewService.reviewExplorationSuggestion) - .toHaveBeenCalledWith( - '1', 'suggestion_1', 'reject', 'Edited review message example', null, - jasmine.any(Function), jasmine.any(Function)); - expect(alertsService.addWarning).toHaveBeenCalledWith( - jasmine.stringContaining(responseMessage)); - }); + expect(component.activeSuggestionId).toBe('suggestion_1'); + expect(component.activeSuggestion).toEqual(suggestion1); + expect(component.reviewable).toBe(reviewable); + expect(component.reviewMessage).toBe('Edited review message example'); + expect( + siteAnalyticsService.registerContributorDashboardRejectSuggestion + ).toHaveBeenCalledWith('Translation'); + expect( + contributionAndReviewService.reviewExplorationSuggestion + ).toHaveBeenCalledWith( + '1', + 'suggestion_1', + 'reject', + 'Edited review message example', + null, + jasmine.any(Function), + jasmine.any(Function) + ); + expect(alertsService.addWarning).toHaveBeenCalledWith( + jasmine.stringContaining(responseMessage) + ); + } + ); it( 'should cancel suggestion in suggestion modal service when clicking ' + - 'on cancel suggestion button', function() { + 'on cancel suggestion button', + function () { spyOn(activeModal, 'close'); component.cancel(); expect(activeModal.close).toHaveBeenCalledWith([]); - }); + } + ); - it( - 'should open the translation editor when the edit button is clicked', - function() { - component.editSuggestion(); - expect(component.startedEditing).toBe(true); - }); + it('should open the translation editor when the edit button is clicked', function () { + component.editSuggestion(); + expect(component.startedEditing).toBe(true); + }); - it( - 'should close the translation editor when the cancel button is clicked', - function() { - component.cancelEdit(); - expect(component.startedEditing).toBe(false); - }); + it('should close the translation editor when the cancel button is clicked', function () { + component.cancelEdit(); + expect(component.startedEditing).toBe(false); + }); it('should expand the content area', () => { spyOn(component, 'toggleExpansionState').and.callThrough(); @@ -663,70 +805,87 @@ describe('Translation Suggestion Review Modal Component', function() { expect(component.isTranslationExpanded).toBeFalse(); }); - it( - 'should update translation when the update button is clicked', - function() { - component.ngOnInit(); - spyOn(contributionAndReviewService, 'updateTranslationSuggestionAsync') - .and.callFake(( - suggestionId, translationHtml, - successCallback, errorCallback) => { - return Promise.resolve(successCallback()); - }); - - component.updateSuggestion(); - - expect(contributionAndReviewService.updateTranslationSuggestionAsync) - .toHaveBeenCalledWith( - 'suggestion_1', component.editedContent.html, - jasmine.any(Function), - jasmine.any(Function)); - }); + it('should update translation when the update button is clicked', function () { + component.ngOnInit(); + spyOn( + contributionAndReviewService, + 'updateTranslationSuggestionAsync' + ).and.callFake( + (suggestionId, translationHtml, successCallback, errorCallback) => { + return Promise.resolve(successCallback()); + } + ); + + component.updateSuggestion(); + + expect( + contributionAndReviewService.updateTranslationSuggestionAsync + ).toHaveBeenCalledWith( + 'suggestion_1', + component.editedContent.html, + jasmine.any(Function), + jasmine.any(Function) + ); + }); - describe('isHtmlContentEqual', function() { - it('should return true regardless of   differences', function() { - expect(component.isHtmlContentEqual( - '

content

  

', '

content

')) - .toBe(true); + describe('isHtmlContentEqual', function () { + it('should return true regardless of   differences', function () { + expect( + component.isHtmlContentEqual( + '

content

  

', + '

content

' + ) + ).toBe(true); }); - it('should return true regardless of new line differences', function() { - expect(component.isHtmlContentEqual( - '

content

\r\n\n

content2

', - '

content

content2

')) - .toBe(true); + it('should return true regardless of new line differences', function () { + expect( + component.isHtmlContentEqual( + '

content

\r\n\n

content2

', + '

content

content2

' + ) + ).toBe(true); }); - it('should return false if html content differ', function() { - expect(component.isHtmlContentEqual( - '

content

', '

content CHANGED

')) - .toBe(false); + it('should return false if html content differ', function () { + expect( + component.isHtmlContentEqual( + '

content

', + '

content CHANGED

' + ) + ).toBe(false); }); - it('should return false if array contents differ', function() { - expect(component.isHtmlContentEqual( - ['

content1

', '

content2

'], - ['

content1

', '

content2 CHANGED

'])) - .toBe(false); + it('should return false if array contents differ', function () { + expect( + component.isHtmlContentEqual( + ['

content1

', '

content2

'], + ['

content1

', '

content2 CHANGED

'] + ) + ).toBe(false); }); - it('should return true if array contents are equal', function() { - expect(component.isHtmlContentEqual( - ['

content1

', '

content2

'], - ['

content1

', '

content2

'])) - .toBe(true); + it('should return true if array contents are equal', function () { + expect( + component.isHtmlContentEqual( + ['

content1

', '

content2

'], + ['

content1

', '

content2

'] + ) + ).toBe(true); }); - it('should return false if type is different', function() { - expect(component.isHtmlContentEqual( - ['

content1

', '

content2

'], - '

content2

')) - .toBe(false); + it('should return false if type is different', function () { + expect( + component.isHtmlContentEqual( + ['

content1

', '

content2

'], + '

content2

' + ) + ).toBe(false); }); }); }); - describe('when viewing suggestion', function() { + describe('when viewing suggestion', function () { const reviewable = false; const subheading = 'topic_1 / story_1 / chapter_1'; @@ -797,30 +956,30 @@ describe('Translation Suggestion Review Modal Component', function() { details: { topic_name: 'topic_1', story_title: 'story_1', - chapter_title: 'chapter_1' - } + chapter_title: 'chapter_1', + }, }; const contribution2 = { suggestion: suggestion2, details: { topic_name: 'topic_2', story_title: 'story_2', - chapter_title: 'chapter_2' - } + chapter_title: 'chapter_2', + }, }; const contribution3 = { suggestion: suggestion3Obsolete, details: { topic_name: 'topic_3', story_title: 'story_3', - chapter_title: 'chapter_3' - } + chapter_title: 'chapter_3', + }, }; const suggestionIdToContribution = { suggestion_1: contribution1, suggestion_2: contribution2, - suggestion_3: contribution3 + suggestion_3: contribution3, }; beforeEach(() => { @@ -828,12 +987,13 @@ describe('Translation Suggestion Review Modal Component', function() { component.subheading = subheading; component.reviewable = reviewable; component.suggestionIdToContribution = angular.copy( - suggestionIdToContribution); + suggestionIdToContribution + ); }); - it('should initialize $scope properties after controller is initialized', - fakeAsync(function() { - const messages = [{ + it('should initialize $scope properties after controller is initialized', fakeAsync(function () { + const messages = [ + { author_username: '', created_on_msecs: 0, entity_type: '', @@ -852,54 +1012,55 @@ describe('Translation Suggestion Review Modal Component', function() { text: 'Review Message', updated_status: 'fixed', updated_subject: null, - }]; + }, + ]; - const fetchMessagesAsyncSpy = spyOn( - threadDataBackendApiService, 'fetchMessagesAsync') - .and.returnValue(Promise.resolve({messages: messages})); + const fetchMessagesAsyncSpy = spyOn( + threadDataBackendApiService, + 'fetchMessagesAsync' + ).and.returnValue(Promise.resolve({messages: messages})); - component.ngOnInit(); - component.refreshActiveContributionState(); - tick(); + component.ngOnInit(); + component.refreshActiveContributionState(); + tick(); - expect(component.activeSuggestionId).toBe('suggestion_1'); - expect(component.activeSuggestion).toEqual(suggestion1); - expect(component.reviewable).toBe(reviewable); - expect(component.subheading).toBe('topic_1 / story_1 / chapter_1'); - // Suggestion 1's exploration_content_html does not match its - // content_html. - expect(component.hasExplorationContentChanged()).toBe(true); - expect(fetchMessagesAsyncSpy).toHaveBeenCalledWith('suggestion_1'); - expect(component.reviewMessage).toBe('Review Message'); - expect(component.reviewer).toBe('Reviewer'); - })); - - it('should correctly determine whether the panel data is overflowing', - fakeAsync(() => { - // Pre-check. - // The default values for the overflow states are false. - expect(component.isContentOverflowing).toBeFalse(); - expect(component.isTranslationOverflowing).toBeFalse(); - // Setup. - component.contentPanel.elementRef.nativeElement.offsetHeight = 100; - component.translationPanel.elementRef.nativeElement.offsetHeight = 200; - component.contentContainer.nativeElement.offsetHeight = 150; - component.translationContainer.nativeElement.offsetHeight = 150; - // Action. - component.computePanelOverflowState(); - tick(0); - // Expectations. - expect(component.isContentOverflowing).toBeFalse(); - expect(component.isTranslationOverflowing).toBeTrue(); - // Change panel height to simulate changing of the modal data. - component.contentPanel.elementRef.nativeElement.offsetHeight = 300; - // Action. - component.computePanelOverflowState(); - tick(0); - // Expectations. - expect(component.isContentOverflowing).toBeTrue(); - expect(component.isTranslationOverflowing).toBeTrue(); - })); + expect(component.activeSuggestionId).toBe('suggestion_1'); + expect(component.activeSuggestion).toEqual(suggestion1); + expect(component.reviewable).toBe(reviewable); + expect(component.subheading).toBe('topic_1 / story_1 / chapter_1'); + // Suggestion 1's exploration_content_html does not match its + // content_html. + expect(component.hasExplorationContentChanged()).toBe(true); + expect(fetchMessagesAsyncSpy).toHaveBeenCalledWith('suggestion_1'); + expect(component.reviewMessage).toBe('Review Message'); + expect(component.reviewer).toBe('Reviewer'); + })); + + it('should correctly determine whether the panel data is overflowing', fakeAsync(() => { + // Pre-check. + // The default values for the overflow states are false. + expect(component.isContentOverflowing).toBeFalse(); + expect(component.isTranslationOverflowing).toBeFalse(); + // Setup. + component.contentPanel.elementRef.nativeElement.offsetHeight = 100; + component.translationPanel.elementRef.nativeElement.offsetHeight = 200; + component.contentContainer.nativeElement.offsetHeight = 150; + component.translationContainer.nativeElement.offsetHeight = 150; + // Action. + component.computePanelOverflowState(); + tick(0); + // Expectations. + expect(component.isContentOverflowing).toBeFalse(); + expect(component.isTranslationOverflowing).toBeTrue(); + // Change panel height to simulate changing of the modal data. + component.contentPanel.elementRef.nativeElement.offsetHeight = 300; + // Action. + component.computePanelOverflowState(); + tick(0); + // Expectations. + expect(component.isContentOverflowing).toBeTrue(); + expect(component.isTranslationOverflowing).toBeTrue(); + })); it('should determine panel height after view initialization', () => { spyOn(component, 'computePanelOverflowState').and.callFake(() => {}); @@ -909,142 +1070,166 @@ describe('Translation Suggestion Review Modal Component', function() { expect(component.computePanelOverflowState).toHaveBeenCalled(); }); - it('should set Obsolete review message for obsolete suggestions', - fakeAsync(function() { - const fetchMessagesAsyncSpy = spyOn( - threadDataBackendApiService, 'fetchMessagesAsync') - .and.returnValue(Promise.resolve({messages: []})); - component.initialSuggestionId = 'suggestion_3'; + it('should set Obsolete review message for obsolete suggestions', fakeAsync(function () { + const fetchMessagesAsyncSpy = spyOn( + threadDataBackendApiService, + 'fetchMessagesAsync' + ).and.returnValue(Promise.resolve({messages: []})); + component.initialSuggestionId = 'suggestion_3'; - component.ngOnInit(); - component.refreshActiveContributionState(); - tick(); + component.ngOnInit(); + component.refreshActiveContributionState(); + tick(); - expect(component.activeSuggestionId).toBe('suggestion_3'); - expect(component.activeSuggestion).toEqual(suggestion3Obsolete); - expect(component.reviewable).toBe(reviewable); - expect(component.subheading).toBe('topic_3 / story_3 / chapter_3'); - // Suggestion 3's exploration_content_html does not match its - // content_html. - expect(component.hasExplorationContentChanged()).toBe(true); - expect(fetchMessagesAsyncSpy).toHaveBeenCalledWith('suggestion_3'); - expect(component.reviewMessage).toBe( - AppConstants.OBSOLETE_TRANSLATION_SUGGESTION_REVIEW_MSG); - })); + expect(component.activeSuggestionId).toBe('suggestion_3'); + expect(component.activeSuggestion).toEqual(suggestion3Obsolete); + expect(component.reviewable).toBe(reviewable); + expect(component.subheading).toBe('topic_3 / story_3 / chapter_3'); + // Suggestion 3's exploration_content_html does not match its + // content_html. + expect(component.hasExplorationContentChanged()).toBe(true); + expect(fetchMessagesAsyncSpy).toHaveBeenCalledWith('suggestion_3'); + expect(component.reviewMessage).toBe( + AppConstants.OBSOLETE_TRANSLATION_SUGGESTION_REVIEW_MSG + ); + })); }); - describe('when reviewing suggestions' + - ' with deleted opportunites', function() { - const reviewable = true; - const subheading = 'topic_1 / story_1 / chapter_1'; - - const suggestion1 = { - suggestion_id: 'suggestion_1', - target_id: '1', - suggestion_type: 'translate_content', - change_cmd: { - content_id: 'hint_1', - content_html: ['Translation1', 'Translation2'], - translation_html: 'Tradução', - state_name: 'StateName', - cmd: 'edit_state_property', - data_format: 'html', + describe( + 'when reviewing suggestions' + ' with deleted opportunites', + function () { + const reviewable = true; + const subheading = 'topic_1 / story_1 / chapter_1'; + + const suggestion1 = { + suggestion_id: 'suggestion_1', + target_id: '1', + suggestion_type: 'translate_content', + change_cmd: { + content_id: 'hint_1', + content_html: ['Translation1', 'Translation2'], + translation_html: 'Tradução', + state_name: 'StateName', + cmd: 'edit_state_property', + data_format: 'html', + language_code: 'language_code', + }, + exploration_content_html: ['Translation1', 'Translation2 CHANGED'], + status: 'rejected', + author_name: 'author_name', language_code: 'language_code', - }, - exploration_content_html: ['Translation1', 'Translation2 CHANGED'], - status: 'rejected', - author_name: 'author_name', - language_code: 'language_code', - last_updated_msecs: 1559074000000, - target_type: 'target_type', - }; - const suggestion2 = { - suggestion_id: 'suggestion_2', - target_id: '2', - suggestion_type: 'translate_content', - change_cmd: { - content_id: 'hint_1', - content_html: 'Translation', - translation_html: 'Tradução', - state_name: 'StateName', - cmd: 'edit_state_property', - data_format: 'html', + last_updated_msecs: 1559074000000, + target_type: 'target_type', + }; + const suggestion2 = { + suggestion_id: 'suggestion_2', + target_id: '2', + suggestion_type: 'translate_content', + change_cmd: { + content_id: 'hint_1', + content_html: 'Translation', + translation_html: 'Tradução', + state_name: 'StateName', + cmd: 'edit_state_property', + data_format: 'html', + language_code: 'language_code', + }, + exploration_content_html: 'Translation', + status: 'rejected', + author_name: 'author_name', language_code: 'language_code', - }, - exploration_content_html: 'Translation', - status: 'rejected', - author_name: 'author_name', - language_code: 'language_code', - last_updated_msecs: 1559074000000, - target_type: 'target_type', - }; - - const contribution1 = { - suggestion: suggestion1, - details: { - topic_name: 'topic_1', - story_title: 'story_1', - chapter_title: 'chapter_1' - } - }; - - const deletedContribution = { - suggestion: suggestion2, - details: null - }; - - const suggestionIdToContribution = { - suggestion_1: contribution1, - suggestion_deleted: deletedContribution, - }; - - beforeEach(() => { - component.initialSuggestionId = 'suggestion_1'; - component.subheading = subheading; - component.reviewable = reviewable; - component.suggestionIdToContribution = angular.copy( - suggestionIdToContribution); - component.ngOnInit(); - }); - - it('should reject suggestion in suggestion modal service when clicking ' + - 'on reject and review next suggestion button', function() { - expect(component.activeSuggestionId).toBe('suggestion_1'); - expect(component.activeSuggestion).toEqual(suggestion1); - expect(component.reviewable).toBe(reviewable); - expect(component.reviewMessage).toBe(''); - - spyOn(contributionAndReviewService, 'reviewExplorationSuggestion') - .and.callFake(( - targetId, suggestionId, action, reviewMessage, commitMessage, - successCallback, errorCallback) => { - return Promise.resolve(successCallback(suggestionId)); - }); - spyOn( - siteAnalyticsService, - 'registerContributorDashboardRejectSuggestion'); - spyOn(activeModal, 'close'); - spyOn(alertsService, 'addSuccessMessage'); - - component.reviewMessage = 'Review message example'; - component.rejectAndReviewNext(component.reviewMessage); + last_updated_msecs: 1559074000000, + target_type: 'target_type', + }; + + const contribution1 = { + suggestion: suggestion1, + details: { + topic_name: 'topic_1', + story_title: 'story_1', + chapter_title: 'chapter_1', + }, + }; + + const deletedContribution = { + suggestion: suggestion2, + details: null, + }; + + const suggestionIdToContribution = { + suggestion_1: contribution1, + suggestion_deleted: deletedContribution, + }; + + beforeEach(() => { + component.initialSuggestionId = 'suggestion_1'; + component.subheading = subheading; + component.reviewable = reviewable; + component.suggestionIdToContribution = angular.copy( + suggestionIdToContribution + ); + component.ngOnInit(); + }); - expect( - siteAnalyticsService.registerContributorDashboardRejectSuggestion) - .toHaveBeenCalledWith('Translation'); - expect(contributionAndReviewService.reviewExplorationSuggestion) - .toHaveBeenCalledWith( - '1', 'suggestion_1', 'reject', 'Review message example', - null, jasmine.any(Function), - jasmine.any(Function)); - expect(alertsService.addSuccessMessage).toHaveBeenCalledWith( - 'Suggestion rejected.'); - expect(activeModal.close).toHaveBeenCalledWith([ - 'suggestion_1']); - }); - }); + it( + 'should reject suggestion in suggestion modal service when clicking ' + + 'on reject and review next suggestion button', + function () { + expect(component.activeSuggestionId).toBe('suggestion_1'); + expect(component.activeSuggestion).toEqual(suggestion1); + expect(component.reviewable).toBe(reviewable); + expect(component.reviewMessage).toBe(''); + + spyOn( + contributionAndReviewService, + 'reviewExplorationSuggestion' + ).and.callFake( + ( + targetId, + suggestionId, + action, + reviewMessage, + commitMessage, + successCallback, + errorCallback + ) => { + return Promise.resolve(successCallback(suggestionId)); + } + ); + spyOn( + siteAnalyticsService, + 'registerContributorDashboardRejectSuggestion' + ); + spyOn(activeModal, 'close'); + spyOn(alertsService, 'addSuccessMessage'); + + component.reviewMessage = 'Review message example'; + component.rejectAndReviewNext(component.reviewMessage); + + expect( + siteAnalyticsService.registerContributorDashboardRejectSuggestion + ).toHaveBeenCalledWith('Translation'); + expect( + contributionAndReviewService.reviewExplorationSuggestion + ).toHaveBeenCalledWith( + '1', + 'suggestion_1', + 'reject', + 'Review message example', + null, + jasmine.any(Function), + jasmine.any(Function) + ); + expect(alertsService.addSuccessMessage).toHaveBeenCalledWith( + 'Suggestion rejected.' + ); + expect(activeModal.close).toHaveBeenCalledWith(['suggestion_1']); + } + ); + } + ); - describe('when navigating through suggestions', function() { + describe('when navigating through suggestions', function () { const reviewable = false; const subheading = 'topic_1 / story_1 / chapter_1'; @@ -1094,16 +1279,16 @@ describe('Translation Suggestion Review Modal Component', function() { details: { topic_name: 'topic_1', story_title: 'story_1', - chapter_title: 'chapter_1' - } + chapter_title: 'chapter_1', + }, }; const contribution2 = { suggestion: suggestion2, details: { topic_name: 'topic_2', story_title: 'story_2', - chapter_title: 'chapter_2' - } + chapter_title: 'chapter_2', + }, }; const suggestionIdToContribution = { @@ -1112,7 +1297,7 @@ describe('Translation Suggestion Review Modal Component', function() { }; const suggestionIdToContributionOne = { - suggestion_1: contribution1 + suggestion_1: contribution1, }; beforeEach(() => { @@ -1123,7 +1308,8 @@ describe('Translation Suggestion Review Modal Component', function() { it('should correctly set variables if there is only one item', () => { component.suggestionIdToContribution = angular.copy( - suggestionIdToContributionOne); + suggestionIdToContributionOne + ); component.ngOnInit(); expect(component.isFirstItem).toBeTrue(); @@ -1134,7 +1320,8 @@ describe('Translation Suggestion Review Modal Component', function() { it('should correctly set variables if there are multiple items', () => { component.suggestionIdToContribution = angular.copy( - suggestionIdToContribution); + suggestionIdToContribution + ); component.ngOnInit(); expect(component.isFirstItem).toBeTrue(); @@ -1145,7 +1332,8 @@ describe('Translation Suggestion Review Modal Component', function() { it('should successfully navigate between items', () => { component.suggestionIdToContribution = angular.copy( - suggestionIdToContribution); + suggestionIdToContribution + ); component.ngOnInit(); spyOn(component, 'refreshActiveContributionState').and.callThrough(); @@ -1190,39 +1378,47 @@ describe('Translation Suggestion Review Modal Component', function() { expect(component.refreshActiveContributionState).toHaveBeenCalled(); }); - it('should close the modal if the opportunity is' + - ' deleted when navigating forward', () => { - component.suggestionIdToContribution = angular.copy( - suggestionIdToContribution); - component.ngOnInit(); - spyOn(activeModal, 'close'); - component.allContributions.suggestion_2.details = null; + it( + 'should close the modal if the opportunity is' + + ' deleted when navigating forward', + () => { + component.suggestionIdToContribution = angular.copy( + suggestionIdToContribution + ); + component.ngOnInit(); + spyOn(activeModal, 'close'); + component.allContributions.suggestion_2.details = null; - component.goToNextItem(); + component.goToNextItem(); - expect(activeModal.close).toHaveBeenCalledWith([]); - }); + expect(activeModal.close).toHaveBeenCalledWith([]); + } + ); - it('should close the modal if the opportunity is' + - ' deleted when navigating backward', () => { - component.suggestionIdToContribution = angular.copy( - suggestionIdToContribution); - component.ngOnInit(); - spyOn(activeModal, 'close'); + it( + 'should close the modal if the opportunity is' + + ' deleted when navigating backward', + () => { + component.suggestionIdToContribution = angular.copy( + suggestionIdToContribution + ); + component.ngOnInit(); + spyOn(activeModal, 'close'); - component.goToNextItem(); + component.goToNextItem(); - expect(component.activeSuggestionId).toEqual('suggestion_2'); - // Delete the opportunity of the previous item. - component.allContributions.suggestion_1.details = null; + expect(component.activeSuggestionId).toEqual('suggestion_2'); + // Delete the opportunity of the previous item. + component.allContributions.suggestion_1.details = null; - component.goToPreviousItem(); + component.goToPreviousItem(); - expect(activeModal.close).toHaveBeenCalledWith([]); - }); + expect(activeModal.close).toHaveBeenCalledWith([]); + } + ); }); - describe('when set the schema constant', function() { + describe('when set the schema constant', function () { const reviewable = true; const subheading = 'topic_1 / story_1 / chapter_1'; const suggestion1 = { @@ -1271,25 +1467,25 @@ describe('Translation Suggestion Review Modal Component', function() { details: { topic_name: 'topic_1', story_title: 'story_1', - chapter_title: 'chapter_1' - } + chapter_title: 'chapter_1', + }, }; const contribution2 = { suggestion: suggestion2, details: { topic_name: 'topic_2', story_title: 'story_2', - chapter_title: 'chapter_2' - } + chapter_title: 'chapter_2', + }, }; const suggestionIdToContribution = { suggestion_1: contribution1, - suggestion_2: contribution2 + suggestion_2: contribution2, }; const editedContent = { - html: '

In Hindi

' + html: '

In Hindi

', }; beforeEach(fakeAsync(() => { @@ -1297,7 +1493,8 @@ describe('Translation Suggestion Review Modal Component', function() { component.subheading = subheading; component.reviewable = reviewable; component.suggestionIdToContribution = angular.copy( - suggestionIdToContribution); + suggestionIdToContribution + ); component.editedContent = editedContent; component.ngOnInit(); tick(); @@ -1311,7 +1508,7 @@ describe('Translation Suggestion Review Modal Component', function() { it('should get unicode schema', () => { expect(component.getUnicodeSchema()).toEqual({ - type: 'unicode' + type: 'unicode', }); }); @@ -1319,8 +1516,8 @@ describe('Translation Suggestion Review Modal Component', function() { expect(component.getSetOfStringsSchema()).toEqual({ type: 'list', items: { - type: 'unicode' - } + type: 'unicode', + }, }); }); diff --git a/core/templates/pages/contributor-dashboard-page/modal-templates/translation-suggestion-review-modal.component.ts b/core/templates/pages/contributor-dashboard-page/modal-templates/translation-suggestion-review-modal.component.ts index 8767afdcbd26..51d5d64fd65d 100644 --- a/core/templates/pages/contributor-dashboard-page/modal-templates/translation-suggestion-review-modal.component.ts +++ b/core/templates/pages/contributor-dashboard-page/modal-templates/translation-suggestion-review-modal.component.ts @@ -12,85 +12,90 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Component for translation suggestion review modal. */ -import { Component, OnInit, ChangeDetectorRef, ViewChild, ElementRef, Input } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { AlertsService } from 'services/alerts.service'; -import { ContextService } from 'services/context.service'; -import { ContributionAndReviewService } from '../services/contribution-and-review.service'; -import { ContributionOpportunitiesService } from '../services/contribution-opportunities.service'; -import { LanguageUtilService } from 'domain/utilities/language-util.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { ThreadDataBackendApiService } from 'pages/exploration-editor-page/feedback-tab/services/thread-data-backend-api.service'; -import { UserService } from 'services/user.service'; -import { ValidatorsService } from 'services/validators.service'; -import { ThreadMessage } from 'domain/feedback_message/ThreadMessage.model'; -import { AppConstants } from 'app.constants'; -import { ListSchema, UnicodeSchema } from 'services/schema-default-value.service'; -import { UserContributionRightsDataBackendDict } from 'services/user-backend-api.service'; +import { + Component, + OnInit, + ChangeDetectorRef, + ViewChild, + ElementRef, + Input, +} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {AlertsService} from 'services/alerts.service'; +import {ContextService} from 'services/context.service'; +import {ContributionAndReviewService} from '../services/contribution-and-review.service'; +import {ContributionOpportunitiesService} from '../services/contribution-opportunities.service'; +import {LanguageUtilService} from 'domain/utilities/language-util.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {ThreadDataBackendApiService} from 'pages/exploration-editor-page/feedback-tab/services/thread-data-backend-api.service'; +import {UserService} from 'services/user.service'; +import {ValidatorsService} from 'services/validators.service'; +import {ThreadMessage} from 'domain/feedback_message/ThreadMessage.model'; +import {AppConstants} from 'app.constants'; +import {ListSchema, UnicodeSchema} from 'services/schema-default-value.service'; +import {UserContributionRightsDataBackendDict} from 'services/user-backend-api.service'; // This throws "TS2307". We need to // suppress this error because rte-output-display is not strictly typed yet. // @ts-ignore -import { RteOutputDisplayComponent } from 'rich_text_components/rte-output-display.component'; +import {RteOutputDisplayComponent} from 'rich_text_components/rte-output-display.component'; interface HTMLSchema { - 'type': string; + type: string; } interface EditedContentDict { - 'html': string; + html: string; } interface ActiveContributionDetailsDict { - 'chapter_title': string; - 'story_title': string; - 'topic_name': string; + chapter_title: string; + story_title: string; + topic_name: string; } interface SuggestionChangeDict { - 'cmd': string; - 'content_html': string | string[]; - 'content_id': string; - 'data_format': string; - 'language_code': string; - 'state_name': string; - 'translation_html': string; + cmd: string; + content_html: string | string[]; + content_id: string; + data_format: string; + language_code: string; + state_name: string; + translation_html: string; } interface ActiveSuggestionDict { - 'author_name': string; - 'change_cmd': SuggestionChangeDict; - 'exploration_content_html': string | string[] | null; - 'language_code': string; - 'last_updated_msecs': number; - 'status': string; - 'suggestion_id': string; - 'suggestion_type': string; - 'target_id': string; - 'target_type': string; + author_name: string; + change_cmd: SuggestionChangeDict; + exploration_content_html: string | string[] | null; + language_code: string; + last_updated_msecs: number; + status: string; + suggestion_id: string; + suggestion_type: string; + target_id: string; + target_type: string; } // Details are null if suggestion's corresponding opportunity is deleted. // See issue #14234. export interface ActiveContributionDict { - 'details': ActiveContributionDetailsDict | null; - 'suggestion': ActiveSuggestionDict; + details: ActiveContributionDetailsDict | null; + suggestion: ActiveSuggestionDict; } enum ExpansionTabType { CONTENT, - TRANSLATION + TRANSLATION, } @Component({ selector: 'oppia-translation-suggestion-review-modal', templateUrl: './translation-suggestion-review-modal.component.html', }) - export class TranslationSuggestionReviewModalComponent implements OnInit { // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see @@ -142,32 +147,32 @@ export class TranslationSuggestionReviewModalComponent implements OnInit { @Input() altTextIsDisplayed: boolean = false; @ViewChild('contentPanel') - contentPanel!: RteOutputDisplayComponent; + contentPanel!: RteOutputDisplayComponent; @ViewChild('translationPanel') - translationPanel!: RteOutputDisplayComponent; + translationPanel!: RteOutputDisplayComponent; @ViewChild('contentContainer') - contentContainer!: ElementRef; + contentContainer!: ElementRef; @ViewChild('translationContainer') - translationContainer!: ElementRef; + translationContainer!: ElementRef; @ViewChild('contentPanelWithAltText') - contentPanelWithAltText!: RteOutputDisplayComponent; + contentPanelWithAltText!: RteOutputDisplayComponent; - HTML_SCHEMA: HTMLSchema = { type: 'html' }; + HTML_SCHEMA: HTMLSchema = {type: 'html'}; MAX_REVIEW_MESSAGE_LENGTH = AppConstants.MAX_REVIEW_MESSAGE_LENGTH; SET_OF_STRINGS_SCHEMA: ListSchema = { type: 'list', items: { - type: 'unicode' - } + type: 'unicode', + }, }; startedEditing: boolean = false; translationUpdated: boolean = false; - UNICODE_SCHEMA: UnicodeSchema = { type: 'unicode' }; + UNICODE_SCHEMA: UnicodeSchema = {type: 'unicode'}; constructor( private readonly changeDetectorRef: ChangeDetectorRef, @@ -180,37 +185,43 @@ export class TranslationSuggestionReviewModalComponent implements OnInit { private siteAnalyticsService: SiteAnalyticsService, private threadDataBackendApiService: ThreadDataBackendApiService, private userService: UserService, - private validatorsService: ValidatorsService, + private validatorsService: ValidatorsService ) {} ngOnInit(): void { this.activeSuggestionId = this.initialSuggestionId; - this.activeContribution = this.suggestionIdToContribution[ - this.activeSuggestionId]; + this.activeContribution = + this.suggestionIdToContribution[this.activeSuggestionId]; this.activeSuggestion = this.activeContribution.suggestion; this.authorName = this.activeSuggestion.author_name; - this.languageDescription = ( + this.languageDescription = this.languageUtilService.getAudioLanguageDescription( - this.activeSuggestion.language_code)); + this.activeSuggestion.language_code + ); this.status = this.activeSuggestion.status; if (this.reviewable) { - this.siteAnalyticsService - .registerContributorDashboardViewSuggestionForReview('Translation'); + this.siteAnalyticsService.registerContributorDashboardViewSuggestionForReview( + 'Translation' + ); this.heading = 'Review Translation Contributions'; } const suggestionIds = Object.keys(this.suggestionIdToContribution); const clickedSuggestionIndex = suggestionIds.indexOf( - this.activeSuggestionId); - this.skippedContributionIds = ( - suggestionIds.slice(0, clickedSuggestionIndex)); + this.activeSuggestionId + ); + this.skippedContributionIds = suggestionIds.slice( + 0, + clickedSuggestionIndex + ); delete this.suggestionIdToContribution[this.initialSuggestionId]; this.remainingContributionIds = suggestionIds.slice( - clickedSuggestionIndex + 1, suggestionIds.length); + clickedSuggestionIndex + 1, + suggestionIds.length + ); this.remainingContributionIds.reverse(); this.isLastItem = this.remainingContributionIds.length === 0; this.allContributions = this.suggestionIdToContribution; - this.allContributions[this.activeSuggestionId] = ( - this.activeContribution); + this.allContributions[this.activeSuggestionId] = this.activeContribution; this.refreshActiveContributionState(); // The 'html' value is passed as an object as it is required for @@ -218,13 +229,12 @@ export class TranslationSuggestionReviewModalComponent implements OnInit { // the translation is not received from the editor when the translation // is edited by the reviewer. this.editedContent = { - html: this.translationHtml + html: this.translationHtml, }; } refreshActiveContributionState(): void { - this.activeContribution = this.allContributions[ - this.activeSuggestionId]; + this.activeContribution = this.allContributions[this.activeSuggestionId]; // Close modal instance if the suggestion's corresponding opportunity // is deleted. See issue #14234. @@ -235,18 +245,17 @@ export class TranslationSuggestionReviewModalComponent implements OnInit { this.activeSuggestion = this.activeContribution.suggestion; this.contextService.setCustomEntityContext( AppConstants.IMAGE_CONTEXT.EXPLORATION_SUGGESTIONS, - this.activeSuggestion.target_id); - this.subheading = ( - `${this.activeContribution.details.topic_name} / ` + - `${this.activeContribution.details.story_title} / ` + - `${this.activeContribution.details.chapter_title}` + this.activeSuggestion.target_id ); + this.subheading = + `${this.activeContribution.details.topic_name} / ` + + `${this.activeContribution.details.story_title} / ` + + `${this.activeContribution.details.chapter_title}`; this.isLastItem = this.remainingContributionIds.length === 0; this.isFirstItem = this.skippedContributionIds.length === 0; this.userCanReviewTranslationSuggestionsInLanguages = []; - this.languageCode = this.activeSuggestion.change_cmd. - language_code; + this.languageCode = this.activeSuggestion.change_cmd.language_code; this.userService.getUserInfoAsync().then(userInfo => { const username = userInfo.getUsername(); @@ -256,18 +265,17 @@ export class TranslationSuggestionReviewModalComponent implements OnInit { this.username = username; this.userIsCurriculumAdmin = userInfo.isCurriculumAdmin(); }); - this.userService.getUserContributionRightsDataAsync().then( - (userContributionRights) => { - let userContributionRightsData = ( - userContributionRights as UserContributionRightsDataBackendDict); - this.userCanReviewTranslationSuggestionsInLanguages = ( - userContributionRightsData - .can_review_translation_for_language_codes); - this.canEditTranslation = ( + this.userService + .getUserContributionRightsDataAsync() + .then(userContributionRights => { + let userContributionRightsData = + userContributionRights as UserContributionRightsDataBackendDict; + this.userCanReviewTranslationSuggestionsInLanguages = + userContributionRightsData.can_review_translation_for_language_codes; + this.canEditTranslation = this.userCanReviewTranslationSuggestionsInLanguages.includes( - this.languageCode) && this.username !== this.activeSuggestion. - author_name - ); + this.languageCode + ) && this.username !== this.activeSuggestion.author_name; }); this.isContentExpanded = false; this.isTranslationExpanded = false; @@ -275,27 +283,21 @@ export class TranslationSuggestionReviewModalComponent implements OnInit { this.errorFound = false; this.startedEditing = false; this.resolvingSuggestion = false; - this.lastSuggestionToReview = ( - Object.keys(this.allContributions).length <= 1); - this.translationHtml = ( - this.activeSuggestion.change_cmd.translation_html); + this.lastSuggestionToReview = + Object.keys(this.allContributions).length <= 1; + this.translationHtml = this.activeSuggestion.change_cmd.translation_html; this.status = this.activeSuggestion.status; - this.contentHtml = ( - this.activeSuggestion.change_cmd.content_html); - this.explorationContentHtml = ( - this.activeSuggestion.exploration_content_html); - this.contentTypeIsHtml = ( - this.activeSuggestion.change_cmd.data_format === 'html' - ); - this.contentTypeIsUnicode = ( - this.activeSuggestion.change_cmd.data_format === 'unicode' - ); - this.contentTypeIsSetOfStrings = ( + this.contentHtml = this.activeSuggestion.change_cmd.content_html; + this.explorationContentHtml = + this.activeSuggestion.exploration_content_html; + this.contentTypeIsHtml = + this.activeSuggestion.change_cmd.data_format === 'html'; + this.contentTypeIsUnicode = + this.activeSuggestion.change_cmd.data_format === 'unicode'; + this.contentTypeIsSetOfStrings = this.activeSuggestion.change_cmd.data_format === 'set_of_normalized_string' || - this.activeSuggestion.change_cmd.data_format === - 'set_of_unicode_string' - ); + this.activeSuggestion.change_cmd.data_format === 'set_of_unicode_string'; this.reviewMessage = ''; if (!this.reviewable) { this._getThreadMessagesAsync(this.activeSuggestionId).then(() => { @@ -303,15 +305,17 @@ export class TranslationSuggestionReviewModalComponent implements OnInit { // became obsolete and was auto-rejected in a batch job. See issue // #16022. if (!this.reviewMessage && !this.explorationContentHtml) { - this.reviewMessage = ( - AppConstants.OBSOLETE_TRANSLATION_SUGGESTION_REVIEW_MSG); + this.reviewMessage = + AppConstants.OBSOLETE_TRANSLATION_SUGGESTION_REVIEW_MSG; } }); } this.explorationImagesString = this.getImageInfoForSuggestion( - this.contentHtml); + this.contentHtml + ); this.suggestionImagesString = this.getImageInfoForSuggestion( - this.translationHtml); + this.translationHtml + ); setTimeout(() => { this.computePanelOverflowState(); }, 0); @@ -319,12 +323,12 @@ export class TranslationSuggestionReviewModalComponent implements OnInit { computePanelOverflowState(): void { setTimeout(() => { - this.isContentOverflowing = ( + this.isContentOverflowing = this.contentPanel.elementRef.nativeElement.offsetHeight > - this.contentContainer.nativeElement.offsetHeight); - this.isTranslationOverflowing = ( + this.contentContainer.nativeElement.offsetHeight; + this.isTranslationOverflowing = this.translationPanel.elementRef.nativeElement.offsetHeight > - this.translationContainer.nativeElement.offsetHeight); + this.translationContainer.nativeElement.offsetHeight; }, 0); } @@ -351,12 +355,13 @@ export class TranslationSuggestionReviewModalComponent implements OnInit { () => { this.translationUpdated = true; this.startedEditing = false; - this.contributionOpportunitiesService. - reloadOpportunitiesEventEmitter.emit(); + this.contributionOpportunitiesService.reloadOpportunitiesEventEmitter.emit(); }, - this.showTranslationSuggestionUpdateError); + this.showTranslationSuggestionUpdateError + ); this.suggestionImagesString = this.getImageInfoForSuggestion( - this.translationHtml); + this.translationHtml + ); } // The length of the commit message should not exceed 375 characters, @@ -371,11 +376,12 @@ export class TranslationSuggestionReviewModalComponent implements OnInit { } async _getThreadMessagesAsync(threadId: string): Promise { - const response = await this.threadDataBackendApiService.fetchMessagesAsync( - threadId); + const response = + await this.threadDataBackendApiService.fetchMessagesAsync(threadId); const threadMessageBackendDicts = response.messages; - let threadMessages = threadMessageBackendDicts.map( - m => ThreadMessage.createFromBackendDict(m)); + let threadMessages = threadMessageBackendDicts.map(m => + ThreadMessage.createFromBackendDict(m) + ); // This is to prevent a console error when a contribution // doesn't have a review message. When a contribution has // a review message the second element of the threadMessages @@ -436,50 +442,65 @@ export class TranslationSuggestionReviewModalComponent implements OnInit { acceptAndReviewNext(): void { this.finalCommitMessage = this.generateCommitMessage(); - const reviewMessageForSubmitter = this.reviewMessage + ( - this.translationUpdated ? ( - (this.reviewMessage.length > 0 ? ': ' : '') + - '(Note: This suggestion was submitted with reviewer edits.)') : - ''); + const reviewMessageForSubmitter = + this.reviewMessage + + (this.translationUpdated + ? (this.reviewMessage.length > 0 ? ': ' : '') + + '(Note: This suggestion was submitted with reviewer edits.)' + : ''); this.resolvingSuggestion = true; this.siteAnalyticsService.registerContributorDashboardAcceptSuggestion( - 'Translation'); + 'Translation' + ); this.contributionAndReviewService.reviewExplorationSuggestion( - this.activeSuggestion.target_id, this.activeSuggestionId, + this.activeSuggestion.target_id, + this.activeSuggestionId, AppConstants.ACTION_ACCEPT_SUGGESTION, - reviewMessageForSubmitter, this.finalCommitMessage, + reviewMessageForSubmitter, + this.finalCommitMessage, () => { this.alertsService.clearMessages(); this.alertsService.addSuccessMessage('Suggestion accepted.'); this.resolveSuggestionAndUpdateModal(); - }, (errorMessage) => { + }, + errorMessage => { this.alertsService.clearWarnings(); this.alertsService.addWarning(`Invalid Suggestion: ${errorMessage}`); - }); + } + ); } rejectAndReviewNext(reviewMessage: string): void { - if (this.validatorsService.isValidReviewMessage(reviewMessage, - /* ShowWarnings= */ true)) { + if ( + this.validatorsService.isValidReviewMessage( + reviewMessage, + /* ShowWarnings= */ true + ) + ) { this.resolvingSuggestion = true; this.siteAnalyticsService.registerContributorDashboardRejectSuggestion( - 'Translation'); + 'Translation' + ); // In case of rejection, the suggestion is not applied, so there is no // commit message. Because there is no commit to make. this.contributionAndReviewService.reviewExplorationSuggestion( - this.activeSuggestion.target_id, this.activeSuggestionId, + this.activeSuggestion.target_id, + this.activeSuggestionId, AppConstants.ACTION_REJECT_SUGGESTION, - reviewMessage || this.reviewMessage, null, + reviewMessage || this.reviewMessage, + null, () => { this.alertsService.clearMessages(); this.alertsService.addSuccessMessage('Suggestion rejected.'); this.resolveSuggestionAndUpdateModal(); - }, (errorMessage) => { + }, + errorMessage => { this.alertsService.clearWarnings(); this.alertsService.addWarning(`Invalid Suggestion: ${errorMessage}`); - }); + } + ); } } @@ -487,20 +508,23 @@ export class TranslationSuggestionReviewModalComponent implements OnInit { // differs from the content_html of the suggestion's change object. hasExplorationContentChanged(): boolean { return !this.isHtmlContentEqual( - this.contentHtml, this.explorationContentHtml); + this.contentHtml, + this.explorationContentHtml + ); } isHtmlContentEqual( - first: string | string[] | null, - second: string | string[] | null + first: string | string[] | null, + second: string | string[] | null ): boolean { if (Array.isArray(first) && Array.isArray(second)) { // Check equality of all array elements. return ( first.length === second.length && first.every( - (val, index) => this.stripWhitespace(val) === this.stripWhitespace( - second[index])) + (val, index) => + this.stripWhitespace(val) === this.stripWhitespace(second[index]) + ) ); } if (angular.isString(first) && angular.isString(second)) { @@ -583,7 +607,9 @@ export class TranslationSuggestionReviewModalComponent implements OnInit { this.altTextIsDisplayed = true; const doc = new DOMParser().parseFromString(content, 'text/html'); const imgElements = doc.querySelectorAll('oppia-noninteractive-image'); - htmlString = Array.from(imgElements).map((img) => img.outerHTML).join(''); + htmlString = Array.from(imgElements) + .map(img => img.outerHTML) + .join(''); } return htmlString; diff --git a/core/templates/pages/contributor-dashboard-page/opportunities-list-item/opportunities-list-item.component.spec.ts b/core/templates/pages/contributor-dashboard-page/opportunities-list-item/opportunities-list-item.component.spec.ts index 4f655b1555c2..22bbf0870b7f 100644 --- a/core/templates/pages/contributor-dashboard-page/opportunities-list-item/opportunities-list-item.component.spec.ts +++ b/core/templates/pages/contributor-dashboard-page/opportunities-list-item/opportunities-list-item.component.spec.ts @@ -16,16 +16,19 @@ * @fileoverview Unit tests for opportunitiesListItem. */ -import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing'; -import { LazyLoadingComponent } from 'components/common-layout-directives/common-elements/lazy-loading.component'; -import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; -import { WrapTextWithEllipsisPipe } from 'filters/string-utility-filters/wrap-text-with-ellipsis.pipe'; -import { of } from 'rxjs'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; +import {ComponentFixture, fakeAsync, TestBed} from '@angular/core/testing'; +import {LazyLoadingComponent} from 'components/common-layout-directives/common-elements/lazy-loading.component'; +import {NgbTooltipModule} from '@ng-bootstrap/ng-bootstrap'; +import {WrapTextWithEllipsisPipe} from 'filters/string-utility-filters/wrap-text-with-ellipsis.pipe'; +import {of} from 'rxjs'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; -import { ExplorationOpportunity, OpportunitiesListItemComponent } from './opportunities-list-item.component'; -import { ContributorDashboardConstants } from 'pages/contributor-dashboard-page/contributor-dashboard-page.constants'; -import { MatIconModule } from '@angular/material/icon'; +import { + ExplorationOpportunity, + OpportunitiesListItemComponent, +} from './opportunities-list-item.component'; +import {ContributorDashboardConstants} from 'pages/contributor-dashboard-page/contributor-dashboard-page.constants'; +import {MatIconModule} from '@angular/material/icon'; class MockWindowDimensionsService { getResizeEvent() { @@ -45,26 +48,24 @@ describe('Opportunities List Item Component', () => { beforeEach(fakeAsync(() => { TestBed.configureTestingModule({ - imports: [ - NgbTooltipModule, - MatIconModule - ], + imports: [NgbTooltipModule, MatIconModule], declarations: [ OpportunitiesListItemComponent, LazyLoadingComponent, - WrapTextWithEllipsisPipe + WrapTextWithEllipsisPipe, ], providers: [ { provide: WindowDimensionsService, - useClass: MockWindowDimensionsService - } + useClass: MockWindowDimensionsService, + }, ], - }).compileComponents().then(() => { - fixture = TestBed.createComponent( - OpportunitiesListItemComponent); - component = fixture.componentInstance; - }); + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(OpportunitiesListItemComponent); + component = fixture.componentInstance; + }); })); describe('when opportunity is provided', () => { @@ -77,10 +78,10 @@ describe('Opportunities List Item Component', () => { inReviewCount: 20, totalCount: 50, translationsCount: 0, - topicName: 'Topic 1' + topicName: 'Topic 1', }; - component.clickActionButton.emit = - () => jasmine.createSpy('click', () => {}); + component.clickActionButton.emit = () => + jasmine.createSpy('click', () => {}); component.labelRequired = true; component.progressBarRequired = true; component.opportunityHeadingTruncationLength = 35; @@ -89,46 +90,47 @@ describe('Opportunities List Item Component', () => { component.ngOnInit(); }); - it('should initialize $scope properties after controller is initialized', - () => { - const windowResizeSpy = spyOn( - windowDimensionsService, 'getResizeEvent').and.callThrough(); + it('should initialize $scope properties after controller is initialized', () => { + const windowResizeSpy = spyOn( + windowDimensionsService, + 'getResizeEvent' + ).and.callThrough(); - component.ngOnInit(); - fixture.detectChanges(); + component.ngOnInit(); + fixture.detectChanges(); - expect(component.opportunityDataIsLoading).toBe(false); - expect(component.labelText).toBe('Label text'); - expect(component.labelStyle).toEqual({ - 'background-color': '#fff' - }); - expect(component.opportunityHeadingTruncationLength).toBe(35); - expect(component.progressPercentage).toBe('50%'); - expect(component.progressBarStyle).toEqual({ - width: '50%' - }); - expect(component.correspondingOpportunityDeleted).toBe(false); - expect(windowResizeSpy).toHaveBeenCalled(); - expect(component.resizeSubscription).not.toBe(undefined); - expect(component.onMobile).toBeTrue(); + expect(component.opportunityDataIsLoading).toBe(false); + expect(component.labelText).toBe('Label text'); + expect(component.labelStyle).toEqual({ + 'background-color': '#fff', }); - - describe('when opportunity subheading corresponds to deleted ' + - 'opportunity', () => { - beforeEach(() => { - let opportunity = component.opportunity as ExplorationOpportunity; - opportunity.subheading = ( - ContributorDashboardConstants - .CORRESPONDING_DELETED_OPPORTUNITY_TEXT); - fixture.detectChanges(); - component.ngOnInit(); + expect(component.opportunityHeadingTruncationLength).toBe(35); + expect(component.progressPercentage).toBe('50%'); + expect(component.progressBarStyle).toEqual({ + width: '50%', }); + expect(component.correspondingOpportunityDeleted).toBe(false); + expect(windowResizeSpy).toHaveBeenCalled(); + expect(component.resizeSubscription).not.toBe(undefined); + expect(component.onMobile).toBeTrue(); + }); + + describe( + 'when opportunity subheading corresponds to deleted ' + 'opportunity', + () => { + beforeEach(() => { + let opportunity = component.opportunity as ExplorationOpportunity; + opportunity.subheading = + ContributorDashboardConstants.CORRESPONDING_DELETED_OPPORTUNITY_TEXT; + fixture.detectChanges(); + component.ngOnInit(); + }); - it('should initialize correspondingOpportunityDeleted to true', - () => { + it('should initialize correspondingOpportunityDeleted to true', () => { expect(component.correspondingOpportunityDeleted).toBe(true); }); - }); + } + ); }); describe('when a translation opportunity is provided', () => { @@ -141,11 +143,11 @@ describe('Opportunities List Item Component', () => { inReviewCount: 20, totalCount: 50, translationsCount: 25, - topicName: 'Topic 1' + topicName: 'Topic 1', }; component.opportunityType = 'translation'; - component.clickActionButton.emit = - () => jasmine.createSpy('click', () => {}); + component.clickActionButton.emit = () => + jasmine.createSpy('click', () => {}); component.labelRequired = true; component.progressBarRequired = true; component.opportunityHeadingTruncationLength = 35; @@ -153,43 +155,42 @@ describe('Opportunities List Item Component', () => { component.ngOnInit(); }); - it('should initialize $scope properties after controller is initialized', - () => { - expect(component.opportunityDataIsLoading).toBe(false); - expect(component.labelText).toBe('Label text'); - expect(component.labelStyle).toEqual({ - 'background-color': '#fff' - }); - expect(component.opportunityHeadingTruncationLength).toBe(35); - expect(component.progressPercentage).toBe('50%'); - expect(component.correspondingOpportunityDeleted).toBe(false); - expect(component.translationProgressBar).toBe(true); - expect(component.cardsAvailable).toEqual(5); + it('should initialize $scope properties after controller is initialized', () => { + expect(component.opportunityDataIsLoading).toBe(false); + expect(component.labelText).toBe('Label text'); + expect(component.labelStyle).toEqual({ + 'background-color': '#fff', }); + expect(component.opportunityHeadingTruncationLength).toBe(35); + expect(component.progressPercentage).toBe('50%'); + expect(component.correspondingOpportunityDeleted).toBe(false); + expect(component.translationProgressBar).toBe(true); + expect(component.cardsAvailable).toEqual(5); + }); - describe('when opportunity subheading corresponds to deleted ' + - 'opportunity', () => { - beforeEach(() => { - let opportunity = component.opportunity as ExplorationOpportunity; - opportunity.subheading = ( - ContributorDashboardConstants - .CORRESPONDING_DELETED_OPPORTUNITY_TEXT); - fixture.detectChanges(); - component.ngOnInit(); - }); + describe( + 'when opportunity subheading corresponds to deleted ' + 'opportunity', + () => { + beforeEach(() => { + let opportunity = component.opportunity as ExplorationOpportunity; + opportunity.subheading = + ContributorDashboardConstants.CORRESPONDING_DELETED_OPPORTUNITY_TEXT; + fixture.detectChanges(); + component.ngOnInit(); + }); - it('should initialize correspondingOpportunityDeleted to true', - () => { + it('should initialize correspondingOpportunityDeleted to true', () => { expect(component.correspondingOpportunityDeleted).toBe(true); }); - }); + } + ); }); describe('when opportunity is not provided', () => { beforeEach(() => { component.opportunityType = ''; - component.clickActionButton.emit = - () => jasmine.createSpy('click', () => {}); + component.clickActionButton.emit = () => + jasmine.createSpy('click', () => {}); component.labelRequired = true; component.progressBarRequired = true; component.opportunityHeadingTruncationLength = 0; @@ -198,14 +199,13 @@ describe('Opportunities List Item Component', () => { component.ngOnInit(); }); - it('should initialize $scope properties after controller is initialized', - () => { - expect(component.opportunityDataIsLoading).toBeTrue(); - expect(component.labelText).toBeUndefined(); - expect(component.labelStyle).toBeUndefined(); - expect(component.opportunityHeadingTruncationLength).toBe(40); - expect(component.correspondingOpportunityDeleted).toBeFalse(); - }); + it('should initialize $scope properties after controller is initialized', () => { + expect(component.opportunityDataIsLoading).toBeTrue(); + expect(component.labelText).toBeUndefined(); + expect(component.labelStyle).toBeUndefined(); + expect(component.opportunityHeadingTruncationLength).toBe(40); + expect(component.correspondingOpportunityDeleted).toBeFalse(); + }); }); describe('when reviewable translation suggestions are provided', () => { @@ -219,45 +219,54 @@ describe('Opportunities List Item Component', () => { totalCount: 50, translationsCount: 25, translationWordCount: 13, - topicName: 'Topic 1' + topicName: 'Topic 1', }; component.opportunityType = 'translation'; - component.clickActionButton.emit = - () => jasmine.createSpy('click', () => {}); + component.clickActionButton.emit = () => + jasmine.createSpy('click', () => {}); component.labelRequired = true; component.opportunityHeadingTruncationLength = 35; fixture.detectChanges(); component.ngOnInit(); }); - it('should show short label for translation suggestions with' + - ' word count less than 20', () => { - const bannerElement: HTMLElement = fixture.nativeElement; - const translationLengthLabel = bannerElement.querySelector( - '.oppia-translation-length-label'); + it( + 'should show short label for translation suggestions with' + + ' word count less than 20', + () => { + const bannerElement: HTMLElement = fixture.nativeElement; + const translationLengthLabel = bannerElement.querySelector( + '.oppia-translation-length-label' + ); - expect(translationLengthLabel).toBeTruthy(); - expect(translationLengthLabel?.textContent).toContain( - 'Short Translation'); - }); + expect(translationLengthLabel).toBeTruthy(); + expect(translationLengthLabel?.textContent).toContain( + 'Short Translation' + ); + } + ); - it('should not show length label for translation suggestions with word' + - ' count more than 20', () => { - component.opportunity.translationWordCount = 25; - fixture.detectChanges(); + it( + 'should not show length label for translation suggestions with word' + + ' count more than 20', + () => { + component.opportunity.translationWordCount = 25; + fixture.detectChanges(); - const bannerElement: HTMLElement = fixture.nativeElement; - const translationLengthLabel = bannerElement.querySelector( - '.oppia-translation-length-label'); + const bannerElement: HTMLElement = fixture.nativeElement; + const translationLengthLabel = bannerElement.querySelector( + '.oppia-translation-length-label' + ); - expect(translationLengthLabel).toBeNull(); - }); + expect(translationLengthLabel).toBeNull(); + } + ); it('should emit a pin event with the correct properties', () => { const spy = spyOn(component.clickPinButton, 'emit'); const expectedPayload = { topic_name: 'Topic 1', - exploration_id: '1' + exploration_id: '1', }; component.opportunity = { @@ -269,7 +278,7 @@ describe('Opportunities List Item Component', () => { totalCount: 50, translationsCount: 25, translationWordCount: 13, - topicName: 'Topic 1' + topicName: 'Topic 1', }; component.pinOpportunity(); @@ -289,13 +298,13 @@ describe('Opportunities List Item Component', () => { totalCount: 50, translationsCount: 25, translationWordCount: 13, - topicName: 'Topic 1' + topicName: 'Topic 1', }; component.unpinOpportunity(); expect(spy).toHaveBeenCalledWith({ topic_name: expectedTopicName, - exploration_id: '1' + exploration_id: '1', }); }); }); diff --git a/core/templates/pages/contributor-dashboard-page/opportunities-list-item/opportunities-list-item.component.ts b/core/templates/pages/contributor-dashboard-page/opportunities-list-item/opportunities-list-item.component.ts index 38cfa06850c9..44fa98c77f1a 100644 --- a/core/templates/pages/contributor-dashboard-page/opportunities-list-item/opportunities-list-item.component.ts +++ b/core/templates/pages/contributor-dashboard-page/opportunities-list-item/opportunities-list-item.component.ts @@ -16,14 +16,13 @@ * @fileoverview Component for the item view of an opportunity. */ -import { Component, Input, Output, EventEmitter } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; - -import { AppConstants } from 'app.constants'; -import { ContributorDashboardConstants } from 'pages/contributor-dashboard-page/contributor-dashboard-page.constants'; -import { Subscription } from 'rxjs'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; +import {Component, Input, Output, EventEmitter} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {AppConstants} from 'app.constants'; +import {ContributorDashboardConstants} from 'pages/contributor-dashboard-page/contributor-dashboard-page.constants'; +import {Subscription} from 'rxjs'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; export interface ExplorationOpportunity { id: string; @@ -44,12 +43,10 @@ export interface ExplorationOpportunity { @Component({ selector: 'oppia-opportunities-list-item', templateUrl: './opportunities-list-item.component.html', - styleUrls: [] + styleUrls: [], }) export class OpportunitiesListItemComponent { - constructor( - private windowDimensionsService: WindowDimensionsService - ) {} + constructor(private windowDimensionsService: WindowDimensionsService) {} // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see @@ -63,45 +60,42 @@ export class OpportunitiesListItemComponent { @Input() showPinUnpinButton: boolean = false; labelText!: string; - labelStyle!: { 'background-color': string }; + labelStyle!: {'background-color': string}; progressPercentage!: string; - progressBarStyle!: { width: string }; - translatedProgressStyle!: { width: string }; - inReviewProgressStyle!: { width: string }; - untranslatedProgressStyle!: { width: string }; + progressBarStyle!: {width: string}; + translatedProgressStyle!: {width: string}; + inReviewProgressStyle!: {width: string}; + untranslatedProgressStyle!: {width: string}; targetNumQuestionsPerSkill: number = AppConstants.MAX_QUESTIONS_PER_SKILL; cardsAvailable: number = 0; onMobile!: boolean; resizeSubscription!: Subscription; - mobileBreakpoint: number = ( - AppConstants.OPPORTUNITIES_LIST_ITEM_MOBILE_BREAKPOINT); + mobileBreakpoint: number = + AppConstants.OPPORTUNITIES_LIST_ITEM_MOBILE_BREAKPOINT; - @Output() clickActionButton: EventEmitter = ( - new EventEmitter()); + @Output() clickActionButton: EventEmitter = new EventEmitter(); @Output() clickPinButton: EventEmitter<{ - 'topic_name': string; - 'exploration_id': string; - }> = ( - new EventEmitter()); + topic_name: string; + exploration_id: string; + }> = new EventEmitter(); @Output() clickUnpinButton: EventEmitter<{ - 'topic_name': string; - 'exploration_id': string; - }> = ( - new EventEmitter()); + topic_name: string; + exploration_id: string; + }> = new EventEmitter(); pinOpportunity(): void { this.clickPinButton.emit({ topic_name: this.opportunity.topicName, - exploration_id: this.opportunity.id + exploration_id: this.opportunity.id, }); } unpinOpportunity(): void { this.clickUnpinButton.emit({ topic_name: this.opportunity.topicName, - exploration_id: this.opportunity.id + exploration_id: this.opportunity.id, }); } @@ -111,18 +105,19 @@ export class OpportunitiesListItemComponent { opportunityButtonDisabled: boolean = false; ngOnInit(): void { - this.onMobile = ( - this.windowDimensionsService.getWidth() <= this.mobileBreakpoint); - this.resizeSubscription = this.windowDimensionsService.getResizeEvent() + this.onMobile = + this.windowDimensionsService.getWidth() <= this.mobileBreakpoint; + this.resizeSubscription = this.windowDimensionsService + .getResizeEvent() .subscribe(event => { - this.onMobile = ( - this.windowDimensionsService.getWidth() <= this.mobileBreakpoint); + this.onMobile = + this.windowDimensionsService.getWidth() <= this.mobileBreakpoint; }); if (this.opportunity && this.labelRequired) { this.labelText = this.opportunity.labelText; this.labelStyle = { - 'background-color': this.opportunity.labelColor + 'background-color': this.opportunity.labelColor, }; } @@ -131,48 +126,45 @@ export class OpportunitiesListItemComponent { } if (this.opportunity) { if (this.opportunity.progressPercentage) { - this.progressPercentage = - `${Math.floor(this.opportunity.progressPercentage)}%`; + this.progressPercentage = `${Math.floor(this.opportunity.progressPercentage)}%`; if ( - this.opportunityType === - AppConstants.OPPORTUNITY_TYPE_TRANSLATION + this.opportunityType === AppConstants.OPPORTUNITY_TYPE_TRANSLATION ) { this.translationProgressBar = true; - const translatedPercentage = ( - this.opportunity.translationsCount / this.opportunity.totalCount - ) * 100; - const inReviewTranslationsPercentage = ( - this.opportunity.inReviewCount / this.opportunity.totalCount - ) * 100; - const untranslatedPercentage = ( - 100 - (translatedPercentage + inReviewTranslationsPercentage)); - - this.cardsAvailable = ( + const translatedPercentage = + (this.opportunity.translationsCount / this.opportunity.totalCount) * + 100; + const inReviewTranslationsPercentage = + (this.opportunity.inReviewCount / this.opportunity.totalCount) * + 100; + const untranslatedPercentage = + 100 - (translatedPercentage + inReviewTranslationsPercentage); + + this.cardsAvailable = this.opportunity.totalCount - - ( - this.opportunity.translationsCount + - this.opportunity.inReviewCount - ) - ); + (this.opportunity.translationsCount + + this.opportunity.inReviewCount); - this.translatedProgressStyle = { width: translatedPercentage + '%' }; + this.translatedProgressStyle = {width: translatedPercentage + '%'}; this.untranslatedProgressStyle = { - width: untranslatedPercentage + '%' + width: untranslatedPercentage + '%', }; this.inReviewProgressStyle = { - width: inReviewTranslationsPercentage + '%' + width: inReviewTranslationsPercentage + '%', }; - this.opportunityButtonDisabled = ( + this.opportunityButtonDisabled = this.opportunity.translationsCount + - this.opportunity.inReviewCount === this.opportunity.totalCount); + this.opportunity.inReviewCount === + this.opportunity.totalCount; } else { - this.progressBarStyle = { width: this.progressPercentage }; + this.progressBarStyle = {width: this.progressPercentage}; } } this.opportunityDataIsLoading = false; - if (this.opportunity.subheading === - ContributorDashboardConstants - .CORRESPONDING_DELETED_OPPORTUNITY_TEXT) { + if ( + this.opportunity.subheading === + ContributorDashboardConstants.CORRESPONDING_DELETED_OPPORTUNITY_TEXT + ) { this.correspondingOpportunityDeleted = true; } } else { @@ -187,6 +179,9 @@ export class OpportunitiesListItemComponent { } } -angular.module('oppia').directive( - 'oppiaOpportunitiesListItem', downgradeComponent( - { component: OpportunitiesListItemComponent })); +angular + .module('oppia') + .directive( + 'oppiaOpportunitiesListItem', + downgradeComponent({component: OpportunitiesListItemComponent}) + ); diff --git a/core/templates/pages/contributor-dashboard-page/opportunities-list/opportunities-list.component.spec.ts b/core/templates/pages/contributor-dashboard-page/opportunities-list/opportunities-list.component.spec.ts index 3b3436dea51f..3a3d8742e1bc 100644 --- a/core/templates/pages/contributor-dashboard-page/opportunities-list/opportunities-list.component.spec.ts +++ b/core/templates/pages/contributor-dashboard-page/opportunities-list/opportunities-list.component.spec.ts @@ -16,16 +16,21 @@ * @fileoverview Unit tests for the Opportunities List Component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { EventEmitter } from '@angular/core'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { TranslationLanguageService } from 'pages/exploration-editor-page/translation-tab/services/translation-language.service'; -import { TranslationTopicService } from 'pages/exploration-editor-page/translation-tab/services/translation-topic.service'; -import { ExplorationOpportunity } from '../opportunities-list-item/opportunities-list-item.component'; -import { ContributionOpportunitiesService } from '../services/contribution-opportunities.service'; -import { OpportunitiesListComponent } from './opportunities-list.component'; -import { MatIconModule } from '@angular/material/icon'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {EventEmitter} from '@angular/core'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {TranslationLanguageService} from 'pages/exploration-editor-page/translation-tab/services/translation-language.service'; +import {TranslationTopicService} from 'pages/exploration-editor-page/translation-tab/services/translation-topic.service'; +import {ExplorationOpportunity} from '../opportunities-list-item/opportunities-list-item.component'; +import {ContributionOpportunitiesService} from '../services/contribution-opportunities.service'; +import {OpportunitiesListComponent} from './opportunities-list.component'; +import {MatIconModule} from '@angular/material/icon'; describe('Opportunities List Component', () => { let component: OpportunitiesListComponent; @@ -37,17 +42,14 @@ describe('Opportunities List Component', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - MatIconModule - ], + imports: [HttpClientTestingModule, MatIconModule], declarations: [OpportunitiesListComponent], providers: [ ContributionOpportunitiesService, TranslationLanguageService, - TranslationTopicService + TranslationTopicService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); @@ -57,7 +59,8 @@ describe('Opportunities List Component', () => { translationLanguageService = TestBed.inject(TranslationLanguageService); translationTopicService = TestBed.inject(TranslationTopicService); contributionOpportunitiesService = TestBed.inject( - ContributionOpportunitiesService); + ContributionOpportunitiesService + ); }); afterEach(() => { @@ -69,303 +72,8 @@ describe('Opportunities List Component', () => { const mockActiveTopicEventEmitter = new EventEmitter(); const mockReloadOpportunitiesEventEmitter = new EventEmitter(); const mockRemoveOpportunitiesEventEmitter = new EventEmitter(); - const explorationOpportunitiesLoad1: ExplorationOpportunity[] = [{ - id: 'id1', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id2', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id3', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id4', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id5', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id6', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id7', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id8', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id9', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id10', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id11', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id12', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id13', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id14', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id15', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id16', - labelText: 'text', - labelColor: 'blue', - progressPercentage: 30, - inReviewCount: 20, - totalCount: 100, - translationsCount: 30, - topicName: 'Topic 1' - }]; - - const explorationOpportunitiesLoad2: ExplorationOpportunity[] = [{ - id: 'id17', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id18', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id19', - labelText: 'text', - labelColor: 'blue', - progressPercentage: 30, - inReviewCount: 20, - totalCount: 100, - translationsCount: 30, - topicName: 'Topic 1' - }, - { - id: 'id20', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id21', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id22', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id23', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id24', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id25', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id26', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }]; - - beforeEach(() => { - component.loadOpportunities = () => Promise.resolve({ - opportunitiesDicts: explorationOpportunitiesLoad1, - more: true - }); - component.loadMoreOpportunities = () => Promise.resolve({ - opportunitiesDicts: explorationOpportunitiesLoad2, - more: false - }); - - spyOnProperty(translationLanguageService, 'onActiveLanguageChanged') - .and.returnValue(mockActiveLanguageEventEmitter); - spyOnProperty(translationTopicService, 'onActiveTopicChanged') - .and.returnValue(mockActiveTopicEventEmitter); - spyOnProperty( - contributionOpportunitiesService, 'reloadOpportunitiesEventEmitter') - .and.returnValue(mockReloadOpportunitiesEventEmitter); - spyOnProperty( - contributionOpportunitiesService, 'removeOpportunitiesEventEmitter') - .and.returnValue(mockRemoveOpportunitiesEventEmitter); - }); - - it('should go to the new page when opportunities ' + - 'are greater then page length', fakeAsync(() => { - expect(component.activePageNumber).toBe(1); - - component.init(); - component.onChangeLanguage('en'); - tick(); - mockReloadOpportunitiesEventEmitter.emit(); - tick(); - component.gotoPage(1); - tick(); - expect(component.activePageNumber).toBe(1); - expect(component.visibleOpportunities).toEqual([{ + const explorationOpportunitiesLoad1: ExplorationOpportunity[] = [ + { id: 'id1', labelText: 'text', labelColor: 'red', @@ -373,7 +81,7 @@ describe('Opportunities List Component', () => { inReviewCount: 20, totalCount: 100, translationsCount: 50, - topicName: 'Topic 1' + topicName: 'Topic 1', }, { id: 'id2', @@ -383,7 +91,7 @@ describe('Opportunities List Component', () => { inReviewCount: 20, totalCount: 100, translationsCount: 50, - topicName: 'Topic 1' + topicName: 'Topic 1', }, { id: 'id3', @@ -393,7 +101,7 @@ describe('Opportunities List Component', () => { inReviewCount: 20, totalCount: 100, translationsCount: 50, - topicName: 'Topic 1' + topicName: 'Topic 1', }, { id: 'id4', @@ -403,7 +111,7 @@ describe('Opportunities List Component', () => { inReviewCount: 20, totalCount: 100, translationsCount: 50, - topicName: 'Topic 1' + topicName: 'Topic 1', }, { id: 'id5', @@ -413,7 +121,7 @@ describe('Opportunities List Component', () => { inReviewCount: 20, totalCount: 100, translationsCount: 50, - topicName: 'Topic 1' + topicName: 'Topic 1', }, { id: 'id6', @@ -423,7 +131,7 @@ describe('Opportunities List Component', () => { inReviewCount: 20, totalCount: 100, translationsCount: 50, - topicName: 'Topic 1' + topicName: 'Topic 1', }, { id: 'id7', @@ -433,7 +141,7 @@ describe('Opportunities List Component', () => { inReviewCount: 20, totalCount: 100, translationsCount: 50, - topicName: 'Topic 1' + topicName: 'Topic 1', }, { id: 'id8', @@ -443,7 +151,7 @@ describe('Opportunities List Component', () => { inReviewCount: 20, totalCount: 100, translationsCount: 50, - topicName: 'Topic 1' + topicName: 'Topic 1', }, { id: 'id9', @@ -453,7 +161,7 @@ describe('Opportunities List Component', () => { inReviewCount: 20, totalCount: 100, translationsCount: 50, - topicName: 'Topic 1' + topicName: 'Topic 1', }, { id: 'id10', @@ -463,45 +171,360 @@ describe('Opportunities List Component', () => { inReviewCount: 20, totalCount: 100, translationsCount: 50, - topicName: 'Topic 1' - }]); - component.gotoPage(2); - tick(); + topicName: 'Topic 1', + }, + { + id: 'id11', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id12', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id13', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id14', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id15', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id16', + labelText: 'text', + labelColor: 'blue', + progressPercentage: 30, + inReviewCount: 20, + totalCount: 100, + translationsCount: 30, + topicName: 'Topic 1', + }, + ]; - expect(component.activePageNumber).toBe(2); - })); + const explorationOpportunitiesLoad2: ExplorationOpportunity[] = [ + { + id: 'id17', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id18', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id19', + labelText: 'text', + labelColor: 'blue', + progressPercentage: 30, + inReviewCount: 20, + totalCount: 100, + translationsCount: 30, + topicName: 'Topic 1', + }, + { + id: 'id20', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id21', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id22', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id23', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id24', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id25', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id26', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + ]; - it('should not go to the new page when opportunities ' + - 'are less then page length', fakeAsync(() => { - // Setting more option to be false. - component.loadMoreOpportunities = () => Promise.resolve({ - opportunitiesDicts: explorationOpportunitiesLoad1, - more: false - }); - expect(component.activePageNumber).toBe(1); + beforeEach(() => { + component.loadOpportunities = () => + Promise.resolve({ + opportunitiesDicts: explorationOpportunitiesLoad1, + more: true, + }); + component.loadMoreOpportunities = () => + Promise.resolve({ + opportunitiesDicts: explorationOpportunitiesLoad2, + more: false, + }); - component.init(); - tick(); - mockReloadOpportunitiesEventEmitter.emit(); - tick(); - component.gotoPage(1); - tick(); + spyOnProperty( + translationLanguageService, + 'onActiveLanguageChanged' + ).and.returnValue(mockActiveLanguageEventEmitter); + spyOnProperty( + translationTopicService, + 'onActiveTopicChanged' + ).and.returnValue(mockActiveTopicEventEmitter); + spyOnProperty( + contributionOpportunitiesService, + 'reloadOpportunitiesEventEmitter' + ).and.returnValue(mockReloadOpportunitiesEventEmitter); + spyOnProperty( + contributionOpportunitiesService, + 'removeOpportunitiesEventEmitter' + ).and.returnValue(mockRemoveOpportunitiesEventEmitter); + }); - expect(component.activePageNumber).toBe(1); - })); + it( + 'should go to the new page when opportunities ' + + 'are greater then page length', + fakeAsync(() => { + expect(component.activePageNumber).toBe(1); - it('should show the first page when loadOpportunities is not set', + component.init(); + component.onChangeLanguage('en'); + tick(); + mockReloadOpportunitiesEventEmitter.emit(); + tick(); + component.gotoPage(1); + tick(); + expect(component.activePageNumber).toBe(1); + expect(component.visibleOpportunities).toEqual([ + { + id: 'id1', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id2', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id3', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id4', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id5', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id6', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id7', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id8', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id9', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id10', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + ]); + component.gotoPage(2); + tick(); + + expect(component.activePageNumber).toBe(2); + }) + ); + + it( + 'should not go to the new page when opportunities ' + + 'are less then page length', fakeAsync(() => { - component.loadOpportunities = undefined; + // Setting more option to be false. + component.loadMoreOpportunities = () => + Promise.resolve({ + opportunitiesDicts: explorationOpportunitiesLoad1, + more: false, + }); expect(component.activePageNumber).toBe(1); component.init(); tick(); - component.ngOnInit(); + mockReloadOpportunitiesEventEmitter.emit(); + tick(); + component.gotoPage(1); tick(); expect(component.activePageNumber).toBe(1); - })); + }) + ); + + it('should show the first page when loadOpportunities is not set', fakeAsync(() => { + component.loadOpportunities = undefined; + expect(component.activePageNumber).toBe(1); + + component.init(); + tick(); + component.ngOnInit(); + tick(); + + expect(component.activePageNumber).toBe(1); + })); it('should load opportunities when initialized', fakeAsync(() => { expect(component.opportunities).toEqual([]); @@ -537,36 +560,43 @@ describe('Opportunities List Component', () => { expect(component.opportunities.length).toEqual(15); })); - it('should navigate to updated last page when current last page is removed', - fakeAsync(() => { - component.init(); - component.onChangeLanguage('en'); - tick(); - component.ngOnInit(); - tick(); - expect(component.opportunities).toEqual(explorationOpportunitiesLoad1); - expect(component.opportunities.length).toEqual(16); - expect(component.activePageNumber).toBe(1); - // Navigate to the last page. - component.gotoPage(2); - tick(); - component.gotoPage(3); - tick(); - expect(component.activePageNumber).toBe(3); - // Reset the load method to return no more opportunities. - component.loadMoreOpportunities = () => Promise.resolve({ + it('should navigate to updated last page when current last page is removed', fakeAsync(() => { + component.init(); + component.onChangeLanguage('en'); + tick(); + component.ngOnInit(); + tick(); + expect(component.opportunities).toEqual(explorationOpportunitiesLoad1); + expect(component.opportunities.length).toEqual(16); + expect(component.activePageNumber).toBe(1); + // Navigate to the last page. + component.gotoPage(2); + tick(); + component.gotoPage(3); + tick(); + expect(component.activePageNumber).toBe(3); + // Reset the load method to return no more opportunities. + component.loadMoreOpportunities = () => + Promise.resolve({ opportunitiesDicts: [], - more: false + more: false, }); - // Remove all opportunities on the last page. - mockRemoveOpportunitiesEventEmitter.emit( - ['id20', 'id21', 'id22', 'id23', 'id24', 'id25', 'id26']); - tick(); + // Remove all opportunities on the last page. + mockRemoveOpportunitiesEventEmitter.emit([ + 'id20', + 'id21', + 'id22', + 'id23', + 'id24', + 'id25', + 'id26', + ]); + tick(); - expect(component.opportunities.length).toEqual(19); - expect(component.activePageNumber).toBe(2); - })); + expect(component.opportunities.length).toEqual(19); + expect(component.activePageNumber).toBe(2); + })); }); describe('when clicking on pin-unpin icon', () => { @@ -574,267 +604,271 @@ describe('Opportunities List Component', () => { const mockActiveTopicEventEmitter = new EventEmitter(); const mockReloadOpportunitiesEventEmitter = new EventEmitter(); const mockRemoveOpportunitiesEventEmitter = new EventEmitter(); - const explorationOpportunitiesLoad1: ExplorationOpportunity[] = [{ - id: 'id1', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id2', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id3', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id4', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id5', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id6', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id7', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id8', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id9', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id10', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id11', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id12', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id13', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id14', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id15', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id16', - labelText: 'text', - labelColor: 'blue', - progressPercentage: 30, - inReviewCount: 20, - totalCount: 100, - translationsCount: 30, - topicName: 'Topic 1' - }]; - - const explorationOpportunitiesLoad2: ExplorationOpportunity[] = [{ - id: 'id17', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id18', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id19', - labelText: 'text', - labelColor: 'blue', - progressPercentage: 30, - inReviewCount: 20, - totalCount: 100, - translationsCount: 30, - topicName: 'Topic 1' - }, - { - id: 'id20', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id21', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id22', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id23', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id24', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id25', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }, - { - id: 'id26', - labelText: 'text', - labelColor: 'red', - progressPercentage: 50, - inReviewCount: 20, - totalCount: 100, - translationsCount: 50, - topicName: 'Topic 1' - }]; + const explorationOpportunitiesLoad1: ExplorationOpportunity[] = [ + { + id: 'id1', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id2', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id3', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id4', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id5', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id6', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id7', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id8', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id9', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id10', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id11', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id12', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id13', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id14', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id15', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id16', + labelText: 'text', + labelColor: 'blue', + progressPercentage: 30, + inReviewCount: 20, + totalCount: 100, + translationsCount: 30, + topicName: 'Topic 1', + }, + ]; + + const explorationOpportunitiesLoad2: ExplorationOpportunity[] = [ + { + id: 'id17', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id18', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id19', + labelText: 'text', + labelColor: 'blue', + progressPercentage: 30, + inReviewCount: 20, + totalCount: 100, + translationsCount: 30, + topicName: 'Topic 1', + }, + { + id: 'id20', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id21', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id22', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id23', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id24', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id25', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + { + id: 'id26', + labelText: 'text', + labelColor: 'red', + progressPercentage: 50, + inReviewCount: 20, + totalCount: 100, + translationsCount: 50, + topicName: 'Topic 1', + }, + ]; let mockPinOpportunitiesEventEmitter = new EventEmitter(); let mockUnpinOpportunitiesEventEmitter = new EventEmitter(); @@ -844,31 +878,41 @@ describe('Opportunities List Component', () => { mockUnpinOpportunitiesEventEmitter = new EventEmitter(); spyOnProperty( - contributionOpportunitiesService, 'pinnedOpportunitiesChanged') - .and.returnValue(mockPinOpportunitiesEventEmitter); + contributionOpportunitiesService, + 'pinnedOpportunitiesChanged' + ).and.returnValue(mockPinOpportunitiesEventEmitter); spyOnProperty( - contributionOpportunitiesService, 'unpinnedOpportunitiesChanged') - .and.returnValue(mockUnpinOpportunitiesEventEmitter); - - component.loadOpportunities = () => Promise.resolve({ - opportunitiesDicts: explorationOpportunitiesLoad1, - more: true - }); - component.loadMoreOpportunities = () => Promise.resolve({ - opportunitiesDicts: explorationOpportunitiesLoad2, - more: false - }); - - spyOnProperty(translationLanguageService, 'onActiveLanguageChanged') - .and.returnValue(mockActiveLanguageEventEmitter); - spyOnProperty(translationTopicService, 'onActiveTopicChanged') - .and.returnValue(mockActiveTopicEventEmitter); + contributionOpportunitiesService, + 'unpinnedOpportunitiesChanged' + ).and.returnValue(mockUnpinOpportunitiesEventEmitter); + + component.loadOpportunities = () => + Promise.resolve({ + opportunitiesDicts: explorationOpportunitiesLoad1, + more: true, + }); + component.loadMoreOpportunities = () => + Promise.resolve({ + opportunitiesDicts: explorationOpportunitiesLoad2, + more: false, + }); + spyOnProperty( - contributionOpportunitiesService, 'reloadOpportunitiesEventEmitter') - .and.returnValue(mockReloadOpportunitiesEventEmitter); + translationLanguageService, + 'onActiveLanguageChanged' + ).and.returnValue(mockActiveLanguageEventEmitter); spyOnProperty( - contributionOpportunitiesService, 'removeOpportunitiesEventEmitter') - .and.returnValue(mockRemoveOpportunitiesEventEmitter); + translationTopicService, + 'onActiveTopicChanged' + ).and.returnValue(mockActiveTopicEventEmitter); + spyOnProperty( + contributionOpportunitiesService, + 'reloadOpportunitiesEventEmitter' + ).and.returnValue(mockReloadOpportunitiesEventEmitter); + spyOnProperty( + contributionOpportunitiesService, + 'removeOpportunitiesEventEmitter' + ).and.returnValue(mockRemoveOpportunitiesEventEmitter); }); it('should pin an opportunity', fakeAsync(() => { @@ -882,7 +926,7 @@ describe('Opportunities List Component', () => { tick(); mockReloadOpportunitiesEventEmitter.emit(); tick(); - const updatedData = { explorationId: 'id1', topicName: 'Topic 1' }; + const updatedData = {explorationId: 'id1', topicName: 'Topic 1'}; component.pinOpportunity(updatedData); expect(component.opportunities[0].isPinned).toBe(true); @@ -904,46 +948,56 @@ describe('Opportunities List Component', () => { tick(); mockReloadOpportunitiesEventEmitter.emit(); tick(); - const updatedData = { explorationId: 'id1', topicName: 'Topic 1' }; + const updatedData = {explorationId: 'id1', topicName: 'Topic 1'}; component.pinOpportunity(updatedData); expect(component.opportunities[0].isPinned).toBe(true); component.unpinOpportunity(updatedData); // Ensure the unpinned opportunity is at the end of the list. - expect(component.opportunities[ - component.opportunities.length - 1].id).toBe('id1'); - expect(component.opportunities[ - component.opportunities.length - 1].topicName).toBe('Topic 1'); + expect( + component.opportunities[component.opportunities.length - 1].id + ).toBe('id1'); + expect( + component.opportunities[component.opportunities.length - 1].topicName + ).toBe('Topic 1'); })); - it('should subscribe to pinnedOpportunitiesChanged and call' + - 'pinOpportunity', () => { - const updatedData = { - explorationId: 'id1', - topicName: 'topic' - }; - spyOn(component, 'pinOpportunity').and.callThrough(); - - component.subscribeToPinnedOpportunities(); - contributionOpportunitiesService.pinnedOpportunitiesChanged.emit( - updatedData); - - expect(component.pinOpportunity).toHaveBeenCalledWith(updatedData); - }); - - it('should subscribe to unpinnedOpportunitiesChanged and call' + - 'unpinOpportunity', () => { - const updatedData = { - explorationId: 'id1', - topicName: 'topic' - }; - spyOn(component, 'unpinOpportunity').and.callThrough(); - - component.subscribeToPinnedOpportunities(); - contributionOpportunitiesService.unpinnedOpportunitiesChanged.emit( - updatedData); - - expect(component.unpinOpportunity).toHaveBeenCalledWith(updatedData); - }); + it( + 'should subscribe to pinnedOpportunitiesChanged and call' + + 'pinOpportunity', + () => { + const updatedData = { + explorationId: 'id1', + topicName: 'topic', + }; + spyOn(component, 'pinOpportunity').and.callThrough(); + + component.subscribeToPinnedOpportunities(); + contributionOpportunitiesService.pinnedOpportunitiesChanged.emit( + updatedData + ); + + expect(component.pinOpportunity).toHaveBeenCalledWith(updatedData); + } + ); + + it( + 'should subscribe to unpinnedOpportunitiesChanged and call' + + 'unpinOpportunity', + () => { + const updatedData = { + explorationId: 'id1', + topicName: 'topic', + }; + spyOn(component, 'unpinOpportunity').and.callThrough(); + + component.subscribeToPinnedOpportunities(); + contributionOpportunitiesService.unpinnedOpportunitiesChanged.emit( + updatedData + ); + + expect(component.unpinOpportunity).toHaveBeenCalledWith(updatedData); + } + ); }); }); diff --git a/core/templates/pages/contributor-dashboard-page/opportunities-list/opportunities-list.component.ts b/core/templates/pages/contributor-dashboard-page/opportunities-list/opportunities-list.component.ts index 9063c5d8cde0..ab4403224878 100644 --- a/core/templates/pages/contributor-dashboard-page/opportunities-list/opportunities-list.component.ts +++ b/core/templates/pages/contributor-dashboard-page/opportunities-list/opportunities-list.component.ts @@ -16,15 +16,15 @@ * @fileoverview Component for the list view of opportunities. */ -import { Component, Input, Output, EventEmitter, NgZone } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component, Input, Output, EventEmitter, NgZone} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; -import { TranslationLanguageService } from 'pages/exploration-editor-page/translation-tab/services/translation-language.service'; -import { TranslationTopicService } from 'pages/exploration-editor-page/translation-tab/services/translation-topic.service'; -import { ContributionOpportunitiesService } from '../services/contribution-opportunities.service'; -import { ExplorationOpportunity } from '../opportunities-list-item/opportunities-list-item.component'; -import { AppConstants } from 'app.constants'; -import { Subscription } from 'rxjs'; +import {TranslationLanguageService} from 'pages/exploration-editor-page/translation-tab/services/translation-language.service'; +import {TranslationTopicService} from 'pages/exploration-editor-page/translation-tab/services/translation-topic.service'; +import {ContributionOpportunitiesService} from '../services/contribution-opportunities.service'; +import {ExplorationOpportunity} from '../opportunities-list-item/opportunities-list-item.component'; +import {AppConstants} from 'app.constants'; +import {Subscription} from 'rxjs'; type ExplorationOpportunitiesFetcherFunction = () => Promise<{ opportunitiesDicts: ExplorationOpportunity[]; @@ -34,7 +34,7 @@ type ExplorationOpportunitiesFetcherFunction = () => Promise<{ @Component({ selector: 'oppia-opportunities-list', templateUrl: './opportunities-list.component.html', - styleUrls: [] + styleUrls: [], }) export class OpportunitiesListComponent { // These properties are initialized using Angular lifecycle hooks @@ -51,21 +51,17 @@ export class OpportunitiesListComponent { @Input() showOpportunityButton: boolean = true; @Input() showPinUnpinButton: boolean = false; - @Output() clickActionButton: EventEmitter = ( - new EventEmitter() - ); + @Output() clickActionButton: EventEmitter = new EventEmitter(); @Output() clickPinButton: EventEmitter<{ - 'topic_name': string; - 'exploration_id': string; - }> = ( - new EventEmitter()); + topic_name: string; + exploration_id: string; + }> = new EventEmitter(); @Output() clickUnpinButton: EventEmitter<{ - 'topic_name': string; - 'exploration_id': string; - }> = ( - new EventEmitter()); + topic_name: string; + exploration_id: string; + }> = new EventEmitter(); loadingOpportunityData: boolean = true; opportunities: ExplorationOpportunity[] = []; @@ -79,49 +75,59 @@ export class OpportunitiesListComponent { constructor( private zone: NgZone, - private readonly contributionOpportunitiesService: - ContributionOpportunitiesService, + private readonly contributionOpportunitiesService: ContributionOpportunitiesService, private readonly translationLanguageService: TranslationLanguageService, - private readonly translationTopicService: TranslationTopicService) { + private readonly translationTopicService: TranslationTopicService + ) { this.init(); } init(): void { this.directiveSubscriptions.add( - this.translationLanguageService.onActiveLanguageChanged.subscribe( - () => this.ngOnInit())); + this.translationLanguageService.onActiveLanguageChanged.subscribe(() => + this.ngOnInit() + ) + ); this.directiveSubscriptions.add( - this.translationTopicService.onActiveTopicChanged.subscribe( - () => this.ngOnInit())); + this.translationTopicService.onActiveTopicChanged.subscribe(() => + this.ngOnInit() + ) + ); this.directiveSubscriptions.add( - this.contributionOpportunitiesService - .reloadOpportunitiesEventEmitter.subscribe(() => this.ngOnInit())); + this.contributionOpportunitiesService.reloadOpportunitiesEventEmitter.subscribe( + () => this.ngOnInit() + ) + ); this.directiveSubscriptions.add( - this.contributionOpportunitiesService - .removeOpportunitiesEventEmitter.subscribe((opportunityIds) => { + this.contributionOpportunitiesService.removeOpportunitiesEventEmitter.subscribe( + opportunityIds => { if (opportunityIds.length === 0) { return; } - this.opportunities = this.opportunities.filter((opportunity) => { + this.opportunities = this.opportunities.filter(opportunity => { return opportunityIds.indexOf(opportunity.id) < 0; }); - const currentIndex = ( - this.activePageNumber * this.OPPORTUNITIES_PAGE_SIZE); + const currentIndex = + this.activePageNumber * this.OPPORTUNITIES_PAGE_SIZE; if (currentIndex > this.opportunities.length) { // The active page number is no longer valid. Navigate to the // current last page. - const lastPage = Math.floor( - this.opportunities.length / this.OPPORTUNITIES_PAGE_SIZE) + 1; + const lastPage = + Math.floor( + this.opportunities.length / this.OPPORTUNITIES_PAGE_SIZE + ) + 1; this.gotoPage(lastPage); } else { // Navigate to the active page before opportunities were removed, // i.e. when reviewers accept/reject suggestions. this.gotoPage(this.activePageNumber); } - })); + } + ) + ); } ngOnDestroy(): void { @@ -139,29 +145,30 @@ export class OpportunitiesListComponent { this.contributionOpportunitiesService.pinnedOpportunitiesChanged.subscribe( updatedData => { this.pinOpportunity(updatedData); - }); - this.contributionOpportunitiesService. - unpinnedOpportunitiesChanged.subscribe( - updatedData => { - this.unpinOpportunity(updatedData); - }); + } + ); + this.contributionOpportunitiesService.unpinnedOpportunitiesChanged.subscribe( + updatedData => { + this.unpinOpportunity(updatedData); + } + ); } - pinOpportunity( - updatedData: Record - ): void { + pinOpportunity(updatedData: Record): void { const indexToModify = this.opportunities.findIndex( - opportunity => opportunity.id === updatedData. - explorationId && opportunity.topicName === updatedData.topicName + opportunity => + opportunity.id === updatedData.explorationId && + opportunity.topicName === updatedData.topicName ); if (indexToModify !== -1) { const opportunityToModify = this.opportunities[indexToModify]; const previouslyPinnedIndex = this.opportunities.findIndex( - opportunity => opportunity.isPinned && ( - opportunity.id !== updatedData. - explorationId || opportunity.topicName !== updatedData.topicName) + opportunity => + opportunity.isPinned && + (opportunity.id !== updatedData.explorationId || + opportunity.topicName !== updatedData.topicName) ); if (previouslyPinnedIndex !== -1) { @@ -176,8 +183,9 @@ export class OpportunitiesListComponent { // Update the visible opportunities. const indexInVisible = this.visibleOpportunities.findIndex( - opportunity => opportunity.id === updatedData. - explorationId && opportunity.topicName === updatedData.topicName + opportunity => + opportunity.id === updatedData.explorationId && + opportunity.topicName === updatedData.topicName ); if (indexInVisible !== -1) { @@ -187,12 +195,11 @@ export class OpportunitiesListComponent { } } - unpinOpportunity( - updatedData: Record - ): void { + unpinOpportunity(updatedData: Record): void { const indexToModify = this.opportunities.findIndex( - opportunity => opportunity.id === updatedData. - explorationId && opportunity.topicName === updatedData.topicName + opportunity => + opportunity.id === updatedData.explorationId && + opportunity.topicName === updatedData.topicName ); if (indexToModify !== -1) { @@ -205,8 +212,9 @@ export class OpportunitiesListComponent { // Update the visible opportunities. const indexInVisible = this.visibleOpportunities.findIndex( - opportunity => opportunity.id === updatedData. - explorationId && opportunity.topicName === updatedData.topicName + opportunity => + opportunity.id === updatedData.explorationId && + opportunity.topicName === updatedData.topicName ); if (indexInVisible !== -1) { @@ -227,12 +235,15 @@ export class OpportunitiesListComponent { this.opportunities = opportunitiesDicts; this.more = more; this.visibleOpportunities = this.opportunities.slice( - 0, this.OPPORTUNITIES_PAGE_SIZE); + 0, + this.OPPORTUNITIES_PAGE_SIZE + ); this.userIsOnLastPage = this.calculateUserIsOnLastPage( this.opportunities, this.OPPORTUNITIES_PAGE_SIZE, this.activePageNumber, - this.more); + this.more + ); this.loadingOpportunityData = false; }); }); @@ -246,36 +257,42 @@ export class OpportunitiesListComponent { if (endIndex >= this.opportunities.length && this.more) { this.visibleOpportunities = []; this.loadingOpportunityData = true; - this.loadMoreOpportunities().then( - ({opportunitiesDicts, more}) => { - this.more = more; - this.opportunities = this.opportunities.concat(opportunitiesDicts); - this.visibleOpportunities = this.opportunities.slice( - startIndex, endIndex); - this.loadingOpportunityData = false; - this.userIsOnLastPage = this.calculateUserIsOnLastPage( - this.opportunities, - this.OPPORTUNITIES_PAGE_SIZE, - pageNumber, - this.more); - }); + this.loadMoreOpportunities().then(({opportunitiesDicts, more}) => { + this.more = more; + this.opportunities = this.opportunities.concat(opportunitiesDicts); + this.visibleOpportunities = this.opportunities.slice( + startIndex, + endIndex + ); + this.loadingOpportunityData = false; + this.userIsOnLastPage = this.calculateUserIsOnLastPage( + this.opportunities, + this.OPPORTUNITIES_PAGE_SIZE, + pageNumber, + this.more + ); + }); } else { this.visibleOpportunities = this.opportunities.slice( - startIndex, endIndex); + startIndex, + endIndex + ); } this.userIsOnLastPage = this.calculateUserIsOnLastPage( this.opportunities, this.OPPORTUNITIES_PAGE_SIZE, pageNumber, - this.more); + this.more + ); this.activePageNumber = pageNumber; } calculateUserIsOnLastPage( - opportunities: ExplorationOpportunity[], - pageSize: number, - activePageNumber: number, - moreResults: boolean): boolean { + opportunities: ExplorationOpportunity[], + pageSize: number, + activePageNumber: number, + moreResults: boolean + ): boolean { const lastPageNumber = Math.ceil(opportunities.length / pageSize); return activePageNumber >= lastPageNumber && !moreResults; } @@ -286,6 +303,9 @@ export class OpportunitiesListComponent { } } -angular.module('oppia').directive( - 'oppiaOpportunitiesList', downgradeComponent( - {component: OpportunitiesListComponent})); +angular + .module('oppia') + .directive( + 'oppiaOpportunitiesList', + downgradeComponent({component: OpportunitiesListComponent}) + ); diff --git a/core/templates/pages/contributor-dashboard-page/question-opportunities/question-opportunities.component.spec.ts b/core/templates/pages/contributor-dashboard-page/question-opportunities/question-opportunities.component.spec.ts index 6e60f8e16f76..e77356ef5e93 100644 --- a/core/templates/pages/contributor-dashboard-page/question-opportunities/question-opportunities.component.spec.ts +++ b/core/templates/pages/contributor-dashboard-page/question-opportunities/question-opportunities.component.spec.ts @@ -16,20 +16,26 @@ * @fileoverview Unit tests for questionOpportunities. */ -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ContributionOpportunitiesBackendApiService } from 'pages/contributor-dashboard-page/services/contribution-opportunities-backend-api.service'; -import { SkillOpportunity } from 'domain/opportunity/skill-opportunity.model'; -import { AlertsService } from 'services/alerts.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { SkillObjectFactory } from 'domain/skill/SkillObjectFactory'; -import { UserService } from 'services/user.service'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { ContributionOpportunitiesService } from '../services/contribution-opportunities.service'; -import { QuestionOpportunitiesComponent } from './question-opportunities.component'; -import { QuestionUndoRedoService } from 'domain/editor/undo_redo/question-undo-redo.service'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { UserInfo } from 'domain/user/user-info.model'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ContributionOpportunitiesBackendApiService} from 'pages/contributor-dashboard-page/services/contribution-opportunities-backend-api.service'; +import {SkillOpportunity} from 'domain/opportunity/skill-opportunity.model'; +import {AlertsService} from 'services/alerts.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {SkillObjectFactory} from 'domain/skill/SkillObjectFactory'; +import {UserService} from 'services/user.service'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {ContributionOpportunitiesService} from '../services/contribution-opportunities.service'; +import {QuestionOpportunitiesComponent} from './question-opportunities.component'; +import {QuestionUndoRedoService} from 'domain/editor/undo_redo/question-undo-redo.service'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {UserInfo} from 'domain/user/user-info.model'; class MockNgbModalRef { componentInstance!: { @@ -40,7 +46,7 @@ class MockNgbModalRef { class MockNgbModal { open() { return { - result: Promise.resolve() + result: Promise.resolve(), }; } } @@ -60,13 +66,11 @@ describe('Question opportunities component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - QuestionOpportunitiesComponent - ], + declarations: [QuestionOpportunitiesComponent], providers: [ { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, AlertsService, SiteAnalyticsService, @@ -74,9 +78,9 @@ describe('Question opportunities component', () => { UserService, ContributionOpportunitiesBackendApiService, ContributionOpportunitiesService, - QuestionUndoRedoService + QuestionUndoRedoService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -90,7 +94,8 @@ describe('Question opportunities component', () => { skillObjectFactory = TestBed.inject(SkillObjectFactory); userService = TestBed.inject(UserService); contributionOpportunitiesService = TestBed.inject( - ContributionOpportunitiesService); + ContributionOpportunitiesService + ); questionUndoRedoService = TestBed.inject(QuestionUndoRedoService); opportunitiesArray = [ @@ -98,23 +103,27 @@ describe('Question opportunities component', () => { id: '1', skill_description: 'Skill description 1', topic_name: 'topic_1', - question_count: 5 + question_count: 5, }), SkillOpportunity.createFromBackendDict({ id: '2', skill_description: 'Skill description 2', topic_name: 'topic_1', - question_count: 2 - }) + question_count: 2, + }), ]; }); it('should load question opportunities', () => { - spyOn(contributionOpportunitiesService, 'getSkillOpportunitiesAsync').and - .returnValue(Promise.resolve({ + spyOn( + contributionOpportunitiesService, + 'getSkillOpportunitiesAsync' + ).and.returnValue( + Promise.resolve({ opportunities: opportunitiesArray, - more: false - })); + more: false, + }) + ); component.loadOpportunities().then(({opportunitiesDicts, more}) => { expect(opportunitiesDicts.length).toBe(2); @@ -123,11 +132,15 @@ describe('Question opportunities component', () => { }); it('should load more question opportunities', () => { - spyOn(contributionOpportunitiesService, 'getSkillOpportunitiesAsync').and - .returnValue(Promise.resolve({ + spyOn( + contributionOpportunitiesService, + 'getSkillOpportunitiesAsync' + ).and.returnValue( + Promise.resolve({ opportunities: opportunitiesArray, - more: true - })); + more: true, + }) + ); component.loadOpportunities().then(({opportunitiesDicts, more}) => { expect(opportunitiesDicts.length).toBe(2); @@ -135,11 +148,14 @@ describe('Question opportunities component', () => { }); spyOn( - contributionOpportunitiesService, 'getMoreSkillOpportunitiesAsync').and - .returnValue(Promise.resolve({ + contributionOpportunitiesService, + 'getMoreSkillOpportunitiesAsync' + ).and.returnValue( + Promise.resolve({ opportunities: opportunitiesArray, - more: false - })); + more: false, + }) + ); component.loadMoreOpportunities().then(({opportunitiesDicts, more}) => { expect(opportunitiesDicts.length).toBe(2); @@ -147,71 +163,101 @@ describe('Question opportunities component', () => { }); }); - it('should register Contributor Dashboard suggest event when clicking on' + - ' suggest question button', fakeAsync(() => { - spyOn(component, 'createQuestion').and.stub(); - spyOn(ngbModal, 'open').and.returnValue( - { + it( + 'should register Contributor Dashboard suggest event when clicking on' + + ' suggest question button', + fakeAsync(() => { + spyOn(component, 'createQuestion').and.stub(); + spyOn(ngbModal, 'open').and.returnValue({ componentInstance: MockNgbModalRef, result: Promise.resolve({ skill: null, - skillDifficulty: 'null' - }) - } as NgbModalRef - ); - spyOn(siteAnalyticsService, 'registerContributorDashboardSuggestEvent'); - let userInfo = new UserInfo( - ['USER_ROLE'], true, false, false, false, true, - 'en', 'username1', 'tester@example.com', true - ); - spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo); - component.ngOnInit(); - tick(); - - component.onClickSuggestQuestionButton('1'); - tick(); + skillDifficulty: 'null', + }), + } as NgbModalRef); + spyOn(siteAnalyticsService, 'registerContributorDashboardSuggestEvent'); + let userInfo = new UserInfo( + ['USER_ROLE'], + true, + false, + false, + false, + true, + 'en', + 'username1', + 'tester@example.com', + true + ); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo); + component.ngOnInit(); + tick(); - expect(ngbModal.open).toHaveBeenCalled(); - })); + component.onClickSuggestQuestionButton('1'); + tick(); - it('should open requires login modal when trying to select a question and' + - ' a skill difficulty and user is not logged', () => { - let userInfo = new UserInfo( - ['USER_ROLE'], true, false, false, false, true, - 'en', 'username1', 'tester@example.com', true - ); - spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo); - component.ngOnInit(); + expect(ngbModal.open).toHaveBeenCalled(); + }) + ); - spyOn(ngbModal, 'open'); - // The callFake is to avoid conflicts when testing modal calls. - spyOn(contributionOpportunitiesService, 'showRequiresLoginModal').and - .callFake(() => {}); - component.onClickSuggestQuestionButton('1'); + it( + 'should open requires login modal when trying to select a question and' + + ' a skill difficulty and user is not logged', + () => { + let userInfo = new UserInfo( + ['USER_ROLE'], + true, + false, + false, + false, + true, + 'en', + 'username1', + 'tester@example.com', + true + ); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo); + component.ngOnInit(); - expect(ngbModal.open).not.toHaveBeenCalled(); - }); + spyOn(ngbModal, 'open'); + // The callFake is to avoid conflicts when testing modal calls. + spyOn( + contributionOpportunitiesService, + 'showRequiresLoginModal' + ).and.callFake(() => {}); + component.onClickSuggestQuestionButton('1'); + expect(ngbModal.open).not.toHaveBeenCalled(); + } + ); - it('should open select skill and skill difficulty modal when clicking' + - ' on suggesting question button', () => { - spyOn(ngbModal, 'open').and.returnValue( - { + it( + 'should open select skill and skill difficulty modal when clicking' + + ' on suggesting question button', + () => { + spyOn(ngbModal, 'open').and.returnValue({ componentInstance: MockNgbModalRef, - result: Promise.resolve() - } as NgbModalRef - ); - let userInfo = new UserInfo( - ['USER_ROLE'], true, false, false, false, true, - 'en', 'username1', 'tester@example.com', true - ); - spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo); - component.ngOnInit(); + result: Promise.resolve(), + } as NgbModalRef); + let userInfo = new UserInfo( + ['USER_ROLE'], + true, + false, + false, + false, + true, + 'en', + 'username1', + 'tester@example.com', + true + ); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo); + component.ngOnInit(); - component.onClickSuggestQuestionButton('1'); + component.onClickSuggestQuestionButton('1'); - expect(ngbModal.open).toHaveBeenCalled(); - }); + expect(ngbModal.open).toHaveBeenCalled(); + } + ); it('should open create question modal when creating a question', () => { spyOn(ngbModal, 'open').and.returnValue({ @@ -221,9 +267,9 @@ describe('Question opportunities component', () => { questionId: 'questionId', questionStateData: null, skill: null, - skillDifficulty: 0.6 + skillDifficulty: 0.6, }, - result: Promise.reject() + result: Promise.reject(), } as NgbModalRef); component.createQuestion( @@ -239,137 +285,160 @@ describe('Question opportunities component', () => { }, worked_examples: [], recorded_voiceovers: { - voiceovers_mapping: {} - } + voiceovers_mapping: {}, + }, }, language_code: 'en', version: 3, all_questions_merged: false, next_misconception_id: 0, prerequisite_skill_ids: [], - superseding_skill_id: '' - }), 1); + superseding_skill_id: '', + }), + 1 + ); expect(ngbModal.open).toHaveBeenCalled(); }); - it('should create a question when closing create question modal', - fakeAsync(() => { - let userInfo = new UserInfo( - ['USER_ROLE'], true, false, false, false, true, - 'en', 'username1', 'tester@example.com', true - ); - spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo); - - component.ngOnInit(); - tick(); - alertsService.clearWarnings(); + it('should create a question when closing create question modal', fakeAsync(() => { + let userInfo = new UserInfo( + ['USER_ROLE'], + true, + false, + false, + false, + true, + 'en', + 'username1', + 'tester@example.com', + true + ); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo); - spyOn(questionUndoRedoService, 'clearChanges'); - let openSpy = spyOn(ngbModal, 'open').and.returnValue({ - componentInstance: MockNgbModalRef, - result: Promise.resolve({ - skill: skillObjectFactory.createFromBackendDict({ - id: '1', - description: 'test description', - misconceptions: [], - rubrics: [], - skill_contents: { - explanation: { - html: 'test explanation', - content_id: 'explanation', - }, - worked_examples: [], - recorded_voiceovers: { - voiceovers_mapping: {} - } + component.ngOnInit(); + tick(); + alertsService.clearWarnings(); + + spyOn(questionUndoRedoService, 'clearChanges'); + let openSpy = spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: MockNgbModalRef, + result: Promise.resolve({ + skill: skillObjectFactory.createFromBackendDict({ + id: '1', + description: 'test description', + misconceptions: [], + rubrics: [], + skill_contents: { + explanation: { + html: 'test explanation', + content_id: 'explanation', }, - language_code: 'en', - version: 3, - all_questions_merged: false, - next_misconception_id: 0, - prerequisite_skill_ids: [], - superseding_skill_id: '' - }), - skillDifficulty: 1 - }) - } as NgbModalRef); - - component.onClickSuggestQuestionButton('1'); - tick(); - - expect(openSpy).toHaveBeenCalled(); - expect(questionUndoRedoService.clearChanges).toHaveBeenCalled(); - })); - - it('should suggest a question when dismissing create question modal', - fakeAsync(() => { - let userInfo = new UserInfo( - ['USER_ROLE'], true, false, false, false, true, - 'en', 'username1', 'tester@example.com', true - ); - spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo); - - component.ngOnInit(); - tick(); - alertsService.clearWarnings(); - - spyOn(questionUndoRedoService, 'clearChanges'); - let openSpy = spyOn(ngbModal, 'open').and.returnValue({ - componentInstance: MockNgbModalRef, - result: Promise.resolve({ - skill: skillObjectFactory.createFromBackendDict({ - id: '1', - description: 'test description', - misconceptions: [], - rubrics: [], - skill_contents: { - explanation: { - html: 'test explanation', - content_id: 'explanation', - }, - worked_examples: [], - recorded_voiceovers: { - voiceovers_mapping: {} - } + worked_examples: [], + recorded_voiceovers: { + voiceovers_mapping: {}, }, - language_code: 'en', - version: 3, - all_questions_merged: false, - next_misconception_id: 0, - prerequisite_skill_ids: [], - superseding_skill_id: '' - }), - skillDifficulty: 1 - }) - } as NgbModalRef); + }, + language_code: 'en', + version: 3, + all_questions_merged: false, + next_misconception_id: 0, + prerequisite_skill_ids: [], + superseding_skill_id: '', + }), + skillDifficulty: 1, + }), + } as NgbModalRef); - component.onClickSuggestQuestionButton('1'); - tick(); + component.onClickSuggestQuestionButton('1'); + tick(); - expect(openSpy).toHaveBeenCalled(); - expect(questionUndoRedoService.clearChanges).toHaveBeenCalled(); - })); + expect(openSpy).toHaveBeenCalled(); + expect(questionUndoRedoService.clearChanges).toHaveBeenCalled(); + })); - it('should not create a question when dismissing select skill and skill' + - ' difficulty modal', () => { - spyOn(ngbModal, 'open').and.returnValue( - { - componentInstance: MockNgbModalRef, - result: Promise.reject() - } as NgbModalRef - ); + it('should suggest a question when dismissing create question modal', fakeAsync(() => { let userInfo = new UserInfo( - ['USER_ROLE'], true, false, false, false, true, - 'en', 'username1', 'tester@example.com', true + ['USER_ROLE'], + true, + false, + false, + false, + true, + 'en', + 'username1', + 'tester@example.com', + true ); spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo); - component.ngOnInit(); + component.ngOnInit(); + tick(); + alertsService.clearWarnings(); + + spyOn(questionUndoRedoService, 'clearChanges'); + let openSpy = spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: MockNgbModalRef, + result: Promise.resolve({ + skill: skillObjectFactory.createFromBackendDict({ + id: '1', + description: 'test description', + misconceptions: [], + rubrics: [], + skill_contents: { + explanation: { + html: 'test explanation', + content_id: 'explanation', + }, + worked_examples: [], + recorded_voiceovers: { + voiceovers_mapping: {}, + }, + }, + language_code: 'en', + version: 3, + all_questions_merged: false, + next_misconception_id: 0, + prerequisite_skill_ids: [], + superseding_skill_id: '', + }), + skillDifficulty: 1, + }), + } as NgbModalRef); component.onClickSuggestQuestionButton('1'); + tick(); + expect(openSpy).toHaveBeenCalled(); + expect(questionUndoRedoService.clearChanges).toHaveBeenCalled(); + })); - expect(ngbModal.open).toHaveBeenCalled(); - }); + it( + 'should not create a question when dismissing select skill and skill' + + ' difficulty modal', + () => { + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: MockNgbModalRef, + result: Promise.reject(), + } as NgbModalRef); + let userInfo = new UserInfo( + ['USER_ROLE'], + true, + false, + false, + false, + true, + 'en', + 'username1', + 'tester@example.com', + true + ); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo); + component.ngOnInit(); + + component.onClickSuggestQuestionButton('1'); + + expect(ngbModal.open).toHaveBeenCalled(); + } + ); }); diff --git a/core/templates/pages/contributor-dashboard-page/question-opportunities/question-opportunities.component.ts b/core/templates/pages/contributor-dashboard-page/question-opportunities/question-opportunities.component.ts index 73f4069f5e99..686cbeebd0bf 100644 --- a/core/templates/pages/contributor-dashboard-page/question-opportunities/question-opportunities.component.ts +++ b/core/templates/pages/contributor-dashboard-page/question-opportunities/question-opportunities.component.ts @@ -16,21 +16,21 @@ * @fileoverview Component for question opportunities. */ -import { Component, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { AppConstants } from 'app.constants'; -import { QuestionObjectFactory } from 'domain/question/QuestionObjectFactory'; -import { QuestionUndoRedoService } from 'domain/editor/undo_redo/question-undo-redo.service'; -import { Skill } from 'domain/skill/SkillObjectFactory'; -import { SkillOpportunity } from 'domain/opportunity/skill-opportunity.model'; -import { QuestionsOpportunitiesSelectDifficultyModalComponent } from 'pages/topic-editor-page/modal-templates/questions-opportunities-select-difficulty-modal.component'; -import { QuestionSuggestionEditorModalComponent } from '../modal-templates/question-suggestion-editor-modal.component'; -import { AlertsService } from 'services/alerts.service'; -import { ContextService } from 'services/context.service'; -import { ContributionOpportunitiesService } from '../services/contribution-opportunities.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { UserService } from 'services/user.service'; +import {Component, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {AppConstants} from 'app.constants'; +import {QuestionObjectFactory} from 'domain/question/QuestionObjectFactory'; +import {QuestionUndoRedoService} from 'domain/editor/undo_redo/question-undo-redo.service'; +import {Skill} from 'domain/skill/SkillObjectFactory'; +import {SkillOpportunity} from 'domain/opportunity/skill-opportunity.model'; +import {QuestionsOpportunitiesSelectDifficultyModalComponent} from 'pages/topic-editor-page/modal-templates/questions-opportunities-select-difficulty-modal.component'; +import {QuestionSuggestionEditorModalComponent} from '../modal-templates/question-suggestion-editor-modal.component'; +import {AlertsService} from 'services/alerts.service'; +import {ContextService} from 'services/context.service'; +import {ContributionOpportunitiesService} from '../services/contribution-opportunities.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {UserService} from 'services/user.service'; interface Opportunity { id: string; @@ -52,7 +52,7 @@ interface GetPresentableOpportunitiesResponse { @Component({ selector: 'oppia-question-opportunities', - templateUrl: './question-opportunities.component.html' + templateUrl: './question-opportunities.component.html', }) export class QuestionOpportunitiesComponent implements OnInit { userIsLoggedIn: boolean = false; @@ -66,11 +66,11 @@ export class QuestionOpportunitiesComponent implements OnInit { private questionObjectFactory: QuestionObjectFactory, private questionUndoRedoService: QuestionUndoRedoService, private siteAnalyticsService: SiteAnalyticsService, - private userService: UserService, + private userService: UserService ) {} getPresentableOpportunitiesData( - opportunitiesObject: GetSkillOpportunitiesResponse + opportunitiesObject: GetSkillOpportunitiesResponse ): GetPresentableOpportunitiesResponse { const opportunitiesDicts: Opportunity[] = []; const more = opportunitiesObject.more; @@ -81,7 +81,8 @@ export class QuestionOpportunitiesComponent implements OnInit { const subheading = opportunity.getOpportunitySubheading(); let maxQuestionsPerSkill = AppConstants.MAX_QUESTIONS_PER_SKILL; const progressPercentage = ( - (opportunity.getQuestionCount() / maxQuestionsPerSkill) * 100 + (opportunity.getQuestionCount() / maxQuestionsPerSkill) * + 100 ).toFixed(2); const opportunityDict: Opportunity = { id: opportunity.id, @@ -98,21 +99,23 @@ export class QuestionOpportunitiesComponent implements OnInit { return {opportunitiesDicts, more}; } - createQuestion( - skill: Skill, skillDifficulty: number): void { + createQuestion(skill: Skill, skillDifficulty: number): void { const skillId = skill.getId(); - const question = ( - this.questionObjectFactory.createDefaultQuestion([skillId])); + const question = this.questionObjectFactory.createDefaultQuestion([ + skillId, + ]); const questionId = question.getId(); const questionStateData = question.getStateData(); this.questionUndoRedoService.clearChanges(); const modalRef = this.ngbModal.open( - QuestionSuggestionEditorModalComponent, { + QuestionSuggestionEditorModalComponent, + { size: 'lg', backdrop: 'static', keyboard: false, - }); + } + ); modalRef.componentInstance.suggestionId = ''; modalRef.componentInstance.question = question; @@ -121,29 +124,30 @@ export class QuestionOpportunitiesComponent implements OnInit { modalRef.componentInstance.skill = skill; modalRef.componentInstance.skillDifficulty = skillDifficulty; - modalRef.result.then(() => {}, () => { - this.contextService.resetImageSaveDestination(); - }); + modalRef.result.then( + () => {}, + () => { + this.contextService.resetImageSaveDestination(); + } + ); } loadMoreOpportunities(): Promise<{ opportunitiesDicts: Opportunity[]; more: boolean; }> { - return ( - this.contributionOpportunitiesService - .getMoreSkillOpportunitiesAsync().then( - this.getPresentableOpportunitiesData.bind(this))); + return this.contributionOpportunitiesService + .getMoreSkillOpportunitiesAsync() + .then(this.getPresentableOpportunitiesData.bind(this)); } loadOpportunities(): Promise<{ opportunitiesDicts: Opportunity[]; more: boolean; }> { - return ( - this.contributionOpportunitiesService - .getSkillOpportunitiesAsync().then( - this.getPresentableOpportunitiesData.bind(this))); + return this.contributionOpportunitiesService + .getSkillOpportunitiesAsync() + .then(this.getPresentableOpportunitiesData.bind(this)); } onClickSuggestQuestionButton(skillId: string): void { @@ -153,34 +157,42 @@ export class QuestionOpportunitiesComponent implements OnInit { } this.siteAnalyticsService.registerContributorDashboardSuggestEvent( - 'Question'); + 'Question' + ); const modalRef: NgbModalRef = this.ngbModal.open( - QuestionsOpportunitiesSelectDifficultyModalComponent, { + QuestionsOpportunitiesSelectDifficultyModalComponent, + { backdrop: true, - }); + } + ); modalRef.componentInstance.skillId = skillId; - modalRef.result.then((result) => { - if (this.alertsService.warnings.length === 0) { - this.createQuestion(result.skill, result.skillDifficulty); + modalRef.result.then( + result => { + if (this.alertsService.warnings.length === 0) { + this.createQuestion(result.skill, result.skillDifficulty); + } + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. } - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + ); } ngOnInit(): void { - this.userService.getUserInfoAsync().then((userInfo) => { + this.userService.getUserInfoAsync().then(userInfo => { this.userIsLoggedIn = userInfo.isLoggedIn(); }); } } -angular.module('oppia').directive('oppiaQuestionOpportunities', +angular.module('oppia').directive( + 'oppiaQuestionOpportunities', downgradeComponent({ - component: QuestionOpportunitiesComponent - }) as angular.IDirectiveFactory); + component: QuestionOpportunitiesComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/contributor-dashboard-page/services/contribution-and-review-backend-api.service.spec.ts b/core/templates/pages/contributor-dashboard-page/services/contribution-and-review-backend-api.service.spec.ts index 1474eab844df..40a5b400f6aa 100644 --- a/core/templates/pages/contributor-dashboard-page/services/contribution-and-review-backend-api.service.spec.ts +++ b/core/templates/pages/contributor-dashboard-page/services/contribution-and-review-backend-api.service.spec.ts @@ -16,12 +16,13 @@ * @fileoverview Unit tests for contribution and review backend api service. */ -import { HttpClientTestingModule, HttpTestingController } - from '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; -import { AppConstants } from 'app.constants'; -import { ContributionAndReviewBackendApiService } - from './contribution-and-review-backend-api.service'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; +import {AppConstants} from 'app.constants'; +import {ContributionAndReviewBackendApiService} from './contribution-and-review-backend-api.service'; describe('Contribution and review backend API service', () => { let carbas: ContributionAndReviewBackendApiService; @@ -37,9 +38,7 @@ describe('Contribution and review backend API service', () => { skill_description: 'skill_description_1', }; const suggestionsBackendObject = { - suggestions: [ - suggestion1 - ], + suggestions: [suggestion1], target_id_to_opportunity_dict: { skill_id_1: opportunityDict1, }, @@ -47,7 +46,7 @@ describe('Contribution and review backend API service', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] + imports: [HttpClientTestingModule], }); carbas = TestBed.inject(ContributionAndReviewBackendApiService); http = TestBed.inject(HttpTestingController); @@ -68,131 +67,147 @@ describe('Contribution and review backend API service', () => { it('should fetch submitted question suggestions', fakeAsync(() => { spyOn(carbas, 'fetchSubmittedSuggestionsAsync').and.callThrough(); - const url = '/getsubmittedsuggestions/skill/add_question' + - '?limit=10&offset=0&sort_key=Date'; + const url = + '/getsubmittedsuggestions/skill/add_question' + + '?limit=10&offset=0&sort_key=Date'; - carbas.fetchSuggestionsAsync( - 'SUBMITTED_QUESTION_SUGGESTIONS', - AppConstants.OPPORTUNITIES_PAGE_SIZE, - 0, - AppConstants.SUGGESTIONS_SORT_KEY_DATE, - 'All', - null, - ).then(successHandler, failureHandler); + carbas + .fetchSuggestionsAsync( + 'SUBMITTED_QUESTION_SUGGESTIONS', + AppConstants.OPPORTUNITIES_PAGE_SIZE, + 0, + AppConstants.SUGGESTIONS_SORT_KEY_DATE, + 'All', + null + ) + .then(successHandler, failureHandler); const req = http.expectOne(url); expect(req.request.method).toEqual('GET'); req.flush(suggestionsBackendObject); flushMicrotasks(); - expect(carbas.fetchSubmittedSuggestionsAsync) - .toHaveBeenCalledWith( - 'skill', 'add_question', - AppConstants.OPPORTUNITIES_PAGE_SIZE, 0, - AppConstants.SUGGESTIONS_SORT_KEY_DATE); + expect(carbas.fetchSubmittedSuggestionsAsync).toHaveBeenCalledWith( + 'skill', + 'add_question', + AppConstants.OPPORTUNITIES_PAGE_SIZE, + 0, + AppConstants.SUGGESTIONS_SORT_KEY_DATE + ); expect(successHandler).toHaveBeenCalled(); expect(failureHandler).not.toHaveBeenCalled(); })); it('should fetch submitted translation suggestions', fakeAsync(() => { spyOn(carbas, 'fetchSubmittedSuggestionsAsync').and.callThrough(); - const url = '/getsubmittedsuggestions/exploration/translate_content' + - '?limit=10&offset=0&sort_key=Date'; + const url = + '/getsubmittedsuggestions/exploration/translate_content' + + '?limit=10&offset=0&sort_key=Date'; - carbas.fetchSuggestionsAsync( - 'SUBMITTED_TRANSLATION_SUGGESTIONS', - AppConstants.OPPORTUNITIES_PAGE_SIZE, - 0, - AppConstants.SUGGESTIONS_SORT_KEY_DATE, - 'All', - null, - ).then(successHandler, failureHandler); + carbas + .fetchSuggestionsAsync( + 'SUBMITTED_TRANSLATION_SUGGESTIONS', + AppConstants.OPPORTUNITIES_PAGE_SIZE, + 0, + AppConstants.SUGGESTIONS_SORT_KEY_DATE, + 'All', + null + ) + .then(successHandler, failureHandler); const req = http.expectOne(url); expect(req.request.method).toEqual('GET'); req.flush(suggestionsBackendObject); flushMicrotasks(); - expect(carbas.fetchSubmittedSuggestionsAsync) - .toHaveBeenCalledWith( - 'exploration', 'translate_content', - AppConstants.OPPORTUNITIES_PAGE_SIZE, 0, - AppConstants.SUGGESTIONS_SORT_KEY_DATE); + expect(carbas.fetchSubmittedSuggestionsAsync).toHaveBeenCalledWith( + 'exploration', + 'translate_content', + AppConstants.OPPORTUNITIES_PAGE_SIZE, + 0, + AppConstants.SUGGESTIONS_SORT_KEY_DATE + ); expect(successHandler).toHaveBeenCalled(); expect(failureHandler).not.toHaveBeenCalled(); })); it('should fetch reviewable question suggestions', fakeAsync(() => { spyOn(carbas, 'fetchReviewableSuggestionsAsync').and.callThrough(); - const url = '/getreviewablesuggestions/skill/add_question' + - '?offset=0&sort_key=Date&limit=10&topic_name=specifiedTopic'; + const url = + '/getreviewablesuggestions/skill/add_question' + + '?offset=0&sort_key=Date&limit=10&topic_name=specifiedTopic'; - carbas.fetchSuggestionsAsync( - 'REVIEWABLE_QUESTION_SUGGESTIONS', - AppConstants.OPPORTUNITIES_PAGE_SIZE, - 0, - AppConstants.SUGGESTIONS_SORT_KEY_DATE, - null, - 'specifiedTopic', - ).then(successHandler, failureHandler); + carbas + .fetchSuggestionsAsync( + 'REVIEWABLE_QUESTION_SUGGESTIONS', + AppConstants.OPPORTUNITIES_PAGE_SIZE, + 0, + AppConstants.SUGGESTIONS_SORT_KEY_DATE, + null, + 'specifiedTopic' + ) + .then(successHandler, failureHandler); const req = http.expectOne(url); expect(req.request.method).toEqual('GET'); req.flush(suggestionsBackendObject); flushMicrotasks(); - expect(carbas.fetchReviewableSuggestionsAsync) - .toHaveBeenCalledWith( - 'skill', - 'add_question', - AppConstants.OPPORTUNITIES_PAGE_SIZE, - 0, - 'Date', - null, - 'specifiedTopic', - ); + expect(carbas.fetchReviewableSuggestionsAsync).toHaveBeenCalledWith( + 'skill', + 'add_question', + AppConstants.OPPORTUNITIES_PAGE_SIZE, + 0, + 'Date', + null, + 'specifiedTopic' + ); expect(successHandler).toHaveBeenCalled(); expect(failureHandler).not.toHaveBeenCalled(); })); it('should fetch reviewable suggestions from exp1', fakeAsync(() => { spyOn(carbas, 'fetchReviewableSuggestionsAsync').and.callThrough(); - const url = '/getreviewablesuggestions/exploration/translate_content' + - '?offset=0&sort_key=Date&exploration_id=exp1'; + const url = + '/getreviewablesuggestions/exploration/translate_content' + + '?offset=0&sort_key=Date&exploration_id=exp1'; - carbas.fetchSuggestionsAsync( - 'REVIEWABLE_TRANSLATION_SUGGESTIONS', - null, - 0, - AppConstants.SUGGESTIONS_SORT_KEY_DATE, - explorationId, - null, - ).then(successHandler, failureHandler); + carbas + .fetchSuggestionsAsync( + 'REVIEWABLE_TRANSLATION_SUGGESTIONS', + null, + 0, + AppConstants.SUGGESTIONS_SORT_KEY_DATE, + explorationId, + null + ) + .then(successHandler, failureHandler); const req = http.expectOne(url); expect(req.request.method).toEqual('GET'); req.flush(suggestionsBackendObject); flushMicrotasks(); - expect(carbas.fetchReviewableSuggestionsAsync) - .toHaveBeenCalledWith( - 'exploration', - 'translate_content', - null, - 0, - 'Date', - explorationId, - null, - ); + expect(carbas.fetchReviewableSuggestionsAsync).toHaveBeenCalledWith( + 'exploration', + 'translate_content', + null, + 0, + 'Date', + explorationId, + null + ); expect(successHandler).toHaveBeenCalled(); expect(failureHandler).not.toHaveBeenCalled(); })); it('should throw error if fetch type is invalid', fakeAsync(() => { - carbas.fetchSuggestionsAsync( - 'INVALID_SUGGESTION_TYPE', - AppConstants.OPPORTUNITIES_PAGE_SIZE, - 0, - AppConstants.SUGGESTIONS_SORT_KEY_DATE, - 'All', - null, - ).then(successHandler, failureHandler); + carbas + .fetchSuggestionsAsync( + 'INVALID_SUGGESTION_TYPE', + AppConstants.OPPORTUNITIES_PAGE_SIZE, + 0, + AppConstants.SUGGESTIONS_SORT_KEY_DATE, + 'All', + null + ) + .then(successHandler, failureHandler); flushMicrotasks(); expect(successHandler).not.toHaveBeenCalled(); @@ -207,10 +222,11 @@ describe('Contribution and review backend API service', () => { const putBody = { action: 'accept', review_message: 'test review message', - commit_message: 'test commit message' + commit_message: 'test commit message', }; - carbas.reviewExplorationSuggestionAsync('abc', 'pqr', putBody) + carbas + .reviewExplorationSuggestionAsync('abc', 'pqr', putBody) .then(successHandler, failureHandler); const req = http.expectOne(url); expect(req.request.method).toEqual('PUT'); @@ -228,10 +244,11 @@ describe('Contribution and review backend API service', () => { const putBody = { action: 'accept', review_message: 'test review message', - skill_difficulty: 'easy' + skill_difficulty: 'easy', }; - carbas.reviewSkillSuggestionAsync('abc', 'pqr', putBody) + carbas + .reviewSkillSuggestionAsync('abc', 'pqr', putBody) .then(successHandler, failureHandler); const req = http.expectOne(url); expect(req.request.method).toEqual('PUT'); @@ -247,10 +264,11 @@ describe('Contribution and review backend API service', () => { const failureHandler = jasmine.createSpy('failure'); const url = '/updatetranslationsuggestionhandler/abc'; const putBody = { - translation_html: '

In English

' + translation_html: '

In English

', }; - carbas.updateTranslationSuggestionAsync('abc', putBody) + carbas + .updateTranslationSuggestionAsync('abc', putBody) .then(successHandler, failureHandler); const req = http.expectOne(url); expect(req.request.method).toEqual('PUT'); @@ -269,13 +287,13 @@ describe('Contribution and review backend API service', () => { classifier_model_id: null, content: { content_id: 'content', - html: '' + html: '', }, recorded_voiceovers: { voiceovers_mapping: { content: {}, - default_outcome: {} - } + default_outcome: {}, + }, }, interaction: { answer_groups: [], @@ -284,20 +302,20 @@ describe('Contribution and review backend API service', () => { placeholder: { value: { content_id: 'ca_placeholder_0', - unicode_str: '' - } + unicode_str: '', + }, }, - rows: { value: 1 }, + rows: {value: 1}, catchMisspellings: { - value: false - } + value: false, + }, }, default_outcome: { dest: 'new state', dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: '' + html: '', }, labelled_as_correct: false, param_changes: [], @@ -310,10 +328,10 @@ describe('Contribution and review backend API service', () => { correct_answer: 'answer', explanation: { content_id: 'solution', - html: '

This is an explanation.

' - } + html: '

This is an explanation.

', + }, }, - id: 'TextInput' + id: 'TextInput', }, linked_skill_id: null, param_changes: [], @@ -322,12 +340,13 @@ describe('Contribution and review backend API service', () => { }; const payload = { skill_difficulty: 'easy', - question_state_data: questionStateData + question_state_data: questionStateData, }; const postBody = new FormData(); postBody.append('payload', JSON.stringify(payload)); - carbas.updateQuestionSuggestionAsync('abc', postBody) + carbas + .updateQuestionSuggestionAsync('abc', postBody) .then(successHandler, failureHandler); const req = http.expectOne(url); expect(req.request.method).toEqual('POST'); @@ -342,29 +361,38 @@ describe('Contribution and review backend API service', () => { spyOn(carbas, 'downloadContributorCertificateAsync').and.callThrough(); const successHandler = jasmine.createSpy('success'); const failureHandler = jasmine.createSpy('failure'); - const url = ( + const url = '/contributorcertificate/user/translate_content?' + - 'from_date=2022-01-01&to_date=2022-01-02&language=hi' - ); + 'from_date=2022-01-01&to_date=2022-01-02&language=hi'; const response = { from_date: '1 Nov 2022', to_date: '1 Dec 2022', contribution_hours: 1.0, team_lead: 'Test User', - language: 'Hindi' + language: 'Hindi', }; - carbas.downloadContributorCertificateAsync( - 'user', 'translate_content', 'hi', '2022-01-01', '2022-01-02' - ).then(successHandler, failureHandler); + carbas + .downloadContributorCertificateAsync( + 'user', + 'translate_content', + 'hi', + '2022-01-01', + '2022-01-02' + ) + .then(successHandler, failureHandler); const req = http.expectOne(url); expect(req.request.method).toEqual('GET'); req.flush(response); flushMicrotasks(); - expect(carbas.downloadContributorCertificateAsync) - .toHaveBeenCalledWith( - 'user', 'translate_content', 'hi', '2022-01-01', '2022-01-02'); + expect(carbas.downloadContributorCertificateAsync).toHaveBeenCalledWith( + 'user', + 'translate_content', + 'hi', + '2022-01-01', + '2022-01-02' + ); expect(successHandler).toHaveBeenCalled(); expect(failureHandler).not.toHaveBeenCalled(); })); diff --git a/core/templates/pages/contributor-dashboard-page/services/contribution-and-review-backend-api.service.ts b/core/templates/pages/contributor-dashboard-page/services/contribution-and-review-backend-api.service.ts index a9f8bcc5be35..5df99d0b14e8 100644 --- a/core/templates/pages/contributor-dashboard-page/services/contribution-and-review-backend-api.service.ts +++ b/core/templates/pages/contributor-dashboard-page/services/contribution-and-review-backend-api.service.ts @@ -16,14 +16,14 @@ * @fileoverview Backend api service for fetching and resolving suggestions. */ -import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { OpportunityDict } from './contribution-and-review.service'; -import { SuggestionBackendDict } from 'domain/suggestion/suggestion.model'; +import {Injectable} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {OpportunityDict} from './contribution-and-review.service'; +import {SuggestionBackendDict} from 'domain/suggestion/suggestion.model'; interface FetchSuggestionsResponse { - 'target_id_to_opportunity_dict': { + target_id_to_opportunity_dict: { [targetId: string]: OpportunityDict; }; suggestions: SuggestionBackendDict[]; @@ -31,65 +31,63 @@ interface FetchSuggestionsResponse { } export interface ContributorCertificateResponse { - 'from_date': string; - 'to_date': string; - 'contribution_hours': number; - 'team_lead': string; - 'language': string | null; + from_date: string; + to_date: string; + contribution_hours: number; + team_lead: string; + language: string | null; } interface ReviewExplorationSuggestionRequestBody { action: string; - 'review_message': string; - 'commit_message': string | null; + review_message: string; + commit_message: string | null; } interface ReviewSkillSuggestionRequestBody { action: string; - 'review_message': string; - 'skill_difficulty': string; + review_message: string; + skill_difficulty: string; } interface UpdateTranslationRequestBody { - 'translation_html': string; + translation_html: string; } @Injectable({ providedIn: 'root', }) export class ContributionAndReviewBackendApiService { - private SUBMITTED_SUGGESTION_LIST_HANDLER_URL = ( - '/getsubmittedsuggestions//'); + private SUBMITTED_SUGGESTION_LIST_HANDLER_URL = + '/getsubmittedsuggestions//'; - private REVIEWABLE_SUGGESTIONS_HANDLER_URL = ( - '/getreviewablesuggestions//'); + private REVIEWABLE_SUGGESTIONS_HANDLER_URL = + '/getreviewablesuggestions//'; - private SUGGESTION_TO_EXPLORATION_ACTION_HANDLER_URL = ( - '/suggestionactionhandler/exploration//'); + private SUGGESTION_TO_EXPLORATION_ACTION_HANDLER_URL = + '/suggestionactionhandler/exploration//'; - private SUGGESTION_TO_SKILL_ACTION_HANDLER_URL = ( - '/suggestionactionhandler/skill//'); + private SUGGESTION_TO_SKILL_ACTION_HANDLER_URL = + '/suggestionactionhandler/skill//'; - private UPDATE_TRANSLATION_HANDLER_URL = ( - '/updatetranslationsuggestionhandler/'); + private UPDATE_TRANSLATION_HANDLER_URL = + '/updatetranslationsuggestionhandler/'; - private UPDATE_QUESTION_HANDLER_URL = ( - '/updatequestionsuggestionhandler/'); + private UPDATE_QUESTION_HANDLER_URL = + '/updatequestionsuggestionhandler/'; - private CONTRIBUTOR_CERTIFICATE_HANDLER_URL = ( - '/contributorcertificate//'); + private CONTRIBUTOR_CERTIFICATE_HANDLER_URL = + '/contributorcertificate//'; - private SUBMITTED_QUESTION_SUGGESTIONS = ( - 'SUBMITTED_QUESTION_SUGGESTIONS'); + private SUBMITTED_QUESTION_SUGGESTIONS = 'SUBMITTED_QUESTION_SUGGESTIONS'; - private REVIEWABLE_QUESTION_SUGGESTIONS = ( - 'REVIEWABLE_QUESTION_SUGGESTIONS'); + private REVIEWABLE_QUESTION_SUGGESTIONS = 'REVIEWABLE_QUESTION_SUGGESTIONS'; - private SUBMITTED_TRANSLATION_SUGGESTIONS = ( - 'SUBMITTED_TRANSLATION_SUGGESTIONS'); + private SUBMITTED_TRANSLATION_SUGGESTIONS = + 'SUBMITTED_TRANSLATION_SUGGESTIONS'; - private REVIEWABLE_TRANSLATION_SUGGESTIONS = ( - 'REVIEWABLE_TRANSLATION_SUGGESTIONS'); + private REVIEWABLE_TRANSLATION_SUGGESTIONS = + 'REVIEWABLE_TRANSLATION_SUGGESTIONS'; constructor( private http: HttpClient, @@ -97,24 +95,41 @@ export class ContributionAndReviewBackendApiService { ) {} async fetchSuggestionsAsync( - fetchType: string, - limit: number | null, - offset: number, - sortKey: string, - explorationId: string | null, - topicName: string | null, + fetchType: string, + limit: number | null, + offset: number, + sortKey: string, + explorationId: string | null, + topicName: string | null ): Promise { if (fetchType === this.SUBMITTED_QUESTION_SUGGESTIONS) { return this.fetchSubmittedSuggestionsAsync( - 'skill', 'add_question', limit || 0, offset, sortKey); + 'skill', + 'add_question', + limit || 0, + offset, + sortKey + ); } if (fetchType === this.SUBMITTED_TRANSLATION_SUGGESTIONS) { return this.fetchSubmittedSuggestionsAsync( - 'exploration', 'translate_content', limit || 0, offset, sortKey); + 'exploration', + 'translate_content', + limit || 0, + offset, + sortKey + ); } if (fetchType === this.REVIEWABLE_QUESTION_SUGGESTIONS) { return this.fetchReviewableSuggestionsAsync( - 'skill', 'add_question', limit || 0, offset, sortKey, null, topicName); + 'skill', + 'add_question', + limit || 0, + offset, + sortKey, + null, + topicName + ); } if (fetchType === this.REVIEWABLE_TRANSLATION_SUGGESTIONS) { return this.fetchReviewableSuggestionsAsync( @@ -124,45 +139,48 @@ export class ContributionAndReviewBackendApiService { offset, sortKey, explorationId, - null); + null + ); } throw new Error('Invalid fetch type'); } async fetchSubmittedSuggestionsAsync( - targetType: string, - suggestionType: string, - limit: number, - offset: number, - sortKey: string + targetType: string, + suggestionType: string, + limit: number, + offset: number, + sortKey: string ): Promise { const url = this.urlInterpolationService.interpolateUrl( - this.SUBMITTED_SUGGESTION_LIST_HANDLER_URL, { + this.SUBMITTED_SUGGESTION_LIST_HANDLER_URL, + { target_type: targetType, - suggestion_type: suggestionType + suggestion_type: suggestionType, } ); const params = { limit: limit.toString(), offset: offset.toString(), - sort_key: sortKey + sort_key: sortKey, }; - return this.http.get(url, { params }).toPromise(); + return this.http.get(url, {params}).toPromise(); } async fetchReviewableSuggestionsAsync( - targetType: string, - suggestionType: string, - limit: number | null, - offset: number, - sortKey: string, - explorationId: string | null, - topicName: string | null, + targetType: string, + suggestionType: string, + limit: number | null, + offset: number, + sortKey: string, + explorationId: string | null, + topicName: string | null ): Promise { const url = this.urlInterpolationService.interpolateUrl( - this.REVIEWABLE_SUGGESTIONS_HANDLER_URL, { + this.REVIEWABLE_SUGGESTIONS_HANDLER_URL, + { target_type: targetType, - suggestion_type: suggestionType + suggestion_type: suggestionType, } ); const params: { @@ -173,7 +191,7 @@ export class ContributionAndReviewBackendApiService { topic_name?: string; } = { offset: offset.toString(), - sort_key: sortKey + sort_key: sortKey, }; if (limit) { params.limit = limit.toString(); @@ -184,73 +202,79 @@ export class ContributionAndReviewBackendApiService { if (topicName) { params.topic_name = topicName; } - return this.http.get( - url, - { params } as Object - ).toPromise(); + return this.http + .get(url, {params} as Object) + .toPromise(); } async reviewExplorationSuggestionAsync( - expId: string, - suggestionId: string, - requestBody: ReviewExplorationSuggestionRequestBody + expId: string, + suggestionId: string, + requestBody: ReviewExplorationSuggestionRequestBody ): Promise { const url = this.urlInterpolationService.interpolateUrl( - this.SUGGESTION_TO_EXPLORATION_ACTION_HANDLER_URL, { + this.SUGGESTION_TO_EXPLORATION_ACTION_HANDLER_URL, + { exp_id: expId, - suggestion_id: suggestionId + suggestion_id: suggestionId, } ); return this.http.put(url, requestBody).toPromise(); } async reviewSkillSuggestionAsync( - skillId: string, - suggestionId: string, - requestBody: ReviewSkillSuggestionRequestBody + skillId: string, + suggestionId: string, + requestBody: ReviewSkillSuggestionRequestBody ): Promise { const url = this.urlInterpolationService.interpolateUrl( - this.SUGGESTION_TO_SKILL_ACTION_HANDLER_URL, { + this.SUGGESTION_TO_SKILL_ACTION_HANDLER_URL, + { skill_id: skillId, - suggestion_id: suggestionId + suggestion_id: suggestionId, } ); return this.http.put(url, requestBody).toPromise(); } async updateTranslationSuggestionAsync( - suggestionId: string, requestBody: UpdateTranslationRequestBody + suggestionId: string, + requestBody: UpdateTranslationRequestBody ): Promise { const url = this.urlInterpolationService.interpolateUrl( - this.UPDATE_TRANSLATION_HANDLER_URL, { - suggestion_id: suggestionId + this.UPDATE_TRANSLATION_HANDLER_URL, + { + suggestion_id: suggestionId, } ); return this.http.put(url, requestBody).toPromise(); } async updateQuestionSuggestionAsync( - suggestionId: string, requestBody: FormData + suggestionId: string, + requestBody: FormData ): Promise { const url = this.urlInterpolationService.interpolateUrl( - this.UPDATE_QUESTION_HANDLER_URL, { - suggestion_id: suggestionId + this.UPDATE_QUESTION_HANDLER_URL, + { + suggestion_id: suggestionId, } ); return this.http.post(url, requestBody).toPromise(); } async downloadContributorCertificateAsync( - username: string, - suggestionType: string, - language: string | null, - fromDate: string, - toDate: string + username: string, + suggestionType: string, + language: string | null, + fromDate: string, + toDate: string ): Promise { const url = this.urlInterpolationService.interpolateUrl( - this.CONTRIBUTOR_CERTIFICATE_HANDLER_URL, { + this.CONTRIBUTOR_CERTIFICATE_HANDLER_URL, + { username: username, - suggestion_type: suggestionType + suggestion_type: suggestionType, } ); let params: { @@ -259,13 +283,13 @@ export class ContributionAndReviewBackendApiService { language?: string; } = { from_date: fromDate, - to_date: toDate + to_date: toDate, }; if (language) { params.language = language; } - return this.http.get( - url, { params } as Object - ).toPromise(); + return this.http + .get(url, {params} as Object) + .toPromise(); } } diff --git a/core/templates/pages/contributor-dashboard-page/services/contribution-and-review-stats-backend-api.service.spec.ts b/core/templates/pages/contributor-dashboard-page/services/contribution-and-review-stats-backend-api.service.spec.ts index 8fd0b6634564..bc00140ce039 100644 --- a/core/templates/pages/contributor-dashboard-page/services/contribution-and-review-stats-backend-api.service.spec.ts +++ b/core/templates/pages/contributor-dashboard-page/services/contribution-and-review-stats-backend-api.service.spec.ts @@ -17,11 +17,12 @@ * service. */ -import { HttpClientTestingModule, HttpTestingController } - from '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; -import { ContributionAndReviewStatsBackendApiService } - from './contribution-and-review-stats-backend-api.service'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; +import {ContributionAndReviewStatsBackendApiService} from './contribution-and-review-stats-backend-api.service'; describe('Contribution and review stats backend API service', () => { let carbas: ContributionAndReviewStatsBackendApiService; @@ -38,7 +39,7 @@ describe('Contribution and review stats backend API service', () => { rejected_translations_count: 0, rejected_translation_word_count: 0, first_contribution_date: 'Mar 2021', - last_contribution_date: 'Mar 2021' + last_contribution_date: 'Mar 2021', }; const translationReviewStat = { language_code: 'es', @@ -49,7 +50,7 @@ describe('Contribution and review stats backend API service', () => { accepted_translations_with_reviewer_edits_count: 0, accepted_translation_word_count: 1, first_contribution_date: 'Mar 2021', - last_contribution_date: 'Mar 2021' + last_contribution_date: 'Mar 2021', }; const questionContributionStat = { topic_name: 'published_topic_name', @@ -57,7 +58,7 @@ describe('Contribution and review stats backend API service', () => { accepted_questions_count: 1, accepted_questions_without_reviewer_edits_count: 0, first_contribution_date: 'Mar 2021', - last_contribution_date: 'Mar 2021' + last_contribution_date: 'Mar 2021', }; const questionReviewStat = { topic_name: 'published_topic_name', @@ -65,31 +66,31 @@ describe('Contribution and review stats backend API service', () => { accepted_questions_count: 1, accepted_questions_with_reviewer_edits_count: 1, first_contribution_date: 'Mar 2021', - last_contribution_date: 'Mar 2021' + last_contribution_date: 'Mar 2021', }; const fetchTranslationContributionStatResponse = { - translation_contribution_stats: [translationContributionStat] + translation_contribution_stats: [translationContributionStat], }; const fetchTranslationReviewStatResponse = { - translation_review_stats: [translationReviewStat] + translation_review_stats: [translationReviewStat], }; const fetchQuestionContributionStatResponse = { - question_contribution_stats: [questionContributionStat] + question_contribution_stats: [questionContributionStat], }; const fetchQuestionReviewStatResponse = { - question_review_stats: [questionReviewStat] + question_review_stats: [questionReviewStat], }; const fetchAllStatsResponse = { translation_contribution_stats: [translationContributionStat], translation_review_stats: [translationReviewStat], question_contribution_stats: [questionContributionStat], - question_review_stats: [questionReviewStat] + question_review_stats: [questionReviewStat], }; beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] + imports: [HttpClientTestingModule], }); carbas = TestBed.inject(ContributionAndReviewStatsBackendApiService); http = TestBed.inject(HttpTestingController); @@ -110,112 +111,128 @@ describe('Contribution and review stats backend API service', () => { it('should fetch translation contribution stats', fakeAsync(() => { spyOn(carbas, 'fetchContributionAndReviewStatsAsync').and.callThrough(); - const url = ( - '/contributorstatssummaries/translation/' + - 'submission/translator'); - - carbas.fetchContributionAndReviewStatsAsync( - 'translation', - 'submission', - 'translator' - ).then(successHandler, failureHandler); + const url = + '/contributorstatssummaries/translation/' + 'submission/translator'; + + carbas + .fetchContributionAndReviewStatsAsync( + 'translation', + 'submission', + 'translator' + ) + .then(successHandler, failureHandler); const req = http.expectOne(url); expect(req.request.method).toEqual('GET'); req.flush(fetchTranslationContributionStatResponse); flushMicrotasks(); - expect(carbas.fetchContributionAndReviewStatsAsync) - .toHaveBeenCalledWith( - 'translation', 'submission', 'translator'); + expect(carbas.fetchContributionAndReviewStatsAsync).toHaveBeenCalledWith( + 'translation', + 'submission', + 'translator' + ); expect(successHandler).toHaveBeenCalled(); expect(failureHandler).not.toHaveBeenCalled(); })); it('should fetch translation review stats', fakeAsync(() => { spyOn(carbas, 'fetchContributionAndReviewStatsAsync').and.callThrough(); - const url = ( + const url = '/contributorstatssummaries/translation/' + - 'review/translation_reviewer'); - - carbas.fetchContributionAndReviewStatsAsync( - 'translation', - 'review', - 'translation_reviewer' - ).then(successHandler, failureHandler); + 'review/translation_reviewer'; + + carbas + .fetchContributionAndReviewStatsAsync( + 'translation', + 'review', + 'translation_reviewer' + ) + .then(successHandler, failureHandler); const req = http.expectOne(url); expect(req.request.method).toEqual('GET'); req.flush(fetchTranslationReviewStatResponse); flushMicrotasks(); - expect(carbas.fetchContributionAndReviewStatsAsync) - .toHaveBeenCalledWith( - 'translation', 'review', 'translation_reviewer'); + expect(carbas.fetchContributionAndReviewStatsAsync).toHaveBeenCalledWith( + 'translation', + 'review', + 'translation_reviewer' + ); expect(successHandler).toHaveBeenCalled(); expect(failureHandler).not.toHaveBeenCalled(); })); it('should fetch question contribution stats', fakeAsync(() => { spyOn(carbas, 'fetchContributionAndReviewStatsAsync').and.callThrough(); - const url = ( + const url = '/contributorstatssummaries/question/' + - 'submission/question_submitter'); - - carbas.fetchContributionAndReviewStatsAsync( - 'question', - 'submission', - 'question_submitter' - ).then(successHandler, failureHandler); + 'submission/question_submitter'; + + carbas + .fetchContributionAndReviewStatsAsync( + 'question', + 'submission', + 'question_submitter' + ) + .then(successHandler, failureHandler); const req = http.expectOne(url); expect(req.request.method).toEqual('GET'); req.flush(fetchQuestionContributionStatResponse); flushMicrotasks(); - expect(carbas.fetchContributionAndReviewStatsAsync) - .toHaveBeenCalledWith( - 'question', 'submission', 'question_submitter'); + expect(carbas.fetchContributionAndReviewStatsAsync).toHaveBeenCalledWith( + 'question', + 'submission', + 'question_submitter' + ); expect(successHandler).toHaveBeenCalled(); expect(failureHandler).not.toHaveBeenCalled(); })); it('should fetch question review stats', fakeAsync(() => { spyOn(carbas, 'fetchContributionAndReviewStatsAsync').and.callThrough(); - const url = ( - '/contributorstatssummaries/question/' + - 'review/question_reviewer'); - - carbas.fetchContributionAndReviewStatsAsync( - 'question', - 'review', - 'question_reviewer' - ).then(successHandler, failureHandler); + const url = + '/contributorstatssummaries/question/' + 'review/question_reviewer'; + + carbas + .fetchContributionAndReviewStatsAsync( + 'question', + 'review', + 'question_reviewer' + ) + .then(successHandler, failureHandler); const req = http.expectOne(url); expect(req.request.method).toEqual('GET'); req.flush(fetchQuestionReviewStatResponse); flushMicrotasks(); - expect(carbas.fetchContributionAndReviewStatsAsync) - .toHaveBeenCalledWith( - 'question', 'review', 'question_reviewer'); + expect(carbas.fetchContributionAndReviewStatsAsync).toHaveBeenCalledWith( + 'question', + 'review', + 'question_reviewer' + ); expect(successHandler).toHaveBeenCalled(); expect(failureHandler).not.toHaveBeenCalled(); })); it('should fetch all stats', fakeAsync(() => { spyOn( - carbas, 'fetchAllContributionAndReviewStatsAsync').and.callThrough(); - const url = ( - '/contributorallstatssummaries/user'); - - carbas.fetchAllContributionAndReviewStatsAsync( - 'user' - ).then(successHandler, failureHandler); + carbas, + 'fetchAllContributionAndReviewStatsAsync' + ).and.callThrough(); + const url = '/contributorallstatssummaries/user'; + + carbas + .fetchAllContributionAndReviewStatsAsync('user') + .then(successHandler, failureHandler); const req = http.expectOne(url); expect(req.request.method).toEqual('GET'); req.flush(fetchAllStatsResponse); flushMicrotasks(); - expect(carbas.fetchAllContributionAndReviewStatsAsync) - .toHaveBeenCalledWith('user'); + expect( + carbas.fetchAllContributionAndReviewStatsAsync + ).toHaveBeenCalledWith('user'); expect(successHandler).toHaveBeenCalled(); expect(failureHandler).not.toHaveBeenCalled(); })); diff --git a/core/templates/pages/contributor-dashboard-page/services/contribution-and-review-stats-backend-api.service.ts b/core/templates/pages/contributor-dashboard-page/services/contribution-and-review-stats-backend-api.service.ts index a0e259570aab..827618628472 100644 --- a/core/templates/pages/contributor-dashboard-page/services/contribution-and-review-stats-backend-api.service.ts +++ b/core/templates/pages/contributor-dashboard-page/services/contribution-and-review-stats-backend-api.service.ts @@ -17,21 +17,21 @@ * and review stats. */ -import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { ContributorStatsSummaryBackendDict } from './contribution-and-review-stats.service'; +import {Injectable} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {ContributorStatsSummaryBackendDict} from './contribution-and-review-stats.service'; @Injectable({ providedIn: 'root', }) export class ContributionAndReviewStatsBackendApiService { - private CONTRIBUTOR_STATS_SUMMARIES_URL = ( + private CONTRIBUTOR_STATS_SUMMARIES_URL = '/contributorstatssummaries//' + - '/'); + '/'; - private CONTRIBUTOR_ALL_STATS_SUMMARIES_URL = ( - '/contributorallstatssummaries/'); + private CONTRIBUTOR_ALL_STATS_SUMMARIES_URL = + '/contributorallstatssummaries/'; constructor( private http: HttpClient, @@ -39,24 +39,28 @@ export class ContributionAndReviewStatsBackendApiService { ) {} async fetchContributionAndReviewStatsAsync( - contributionType: string, - contributionSubtype: string, - username: string): Promise { + contributionType: string, + contributionSubtype: string, + username: string + ): Promise { const url = this.urlInterpolationService.interpolateUrl( - this.CONTRIBUTOR_STATS_SUMMARIES_URL, { + this.CONTRIBUTOR_STATS_SUMMARIES_URL, + { contribution_type: contributionType, contribution_subtype: contributionSubtype, - username: username + username: username, } ); return this.http.get(url).toPromise(); } async fetchAllContributionAndReviewStatsAsync( - username: string): Promise { + username: string + ): Promise { const url = this.urlInterpolationService.interpolateUrl( - this.CONTRIBUTOR_ALL_STATS_SUMMARIES_URL, { - username: username + this.CONTRIBUTOR_ALL_STATS_SUMMARIES_URL, + { + username: username, } ); return this.http.get(url).toPromise(); diff --git a/core/templates/pages/contributor-dashboard-page/services/contribution-and-review-stats.service.spec.ts b/core/templates/pages/contributor-dashboard-page/services/contribution-and-review-stats.service.spec.ts index 7e00b8db9cf1..4813e831460b 100644 --- a/core/templates/pages/contributor-dashboard-page/services/contribution-and-review-stats.service.spec.ts +++ b/core/templates/pages/contributor-dashboard-page/services/contribution-and-review-stats.service.spec.ts @@ -16,11 +16,11 @@ * @fileoverview Unit tests for contribution and review service */ -import { TestBed } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { ContributionAndReviewStatsService } from './contribution-and-review-stats.service'; -import { ContributionAndReviewStatsBackendApiService } from './contribution-and-review-stats-backend-api.service'; +import {TestBed} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {ContributionAndReviewStatsService} from './contribution-and-review-stats.service'; +import {ContributionAndReviewStatsBackendApiService} from './contribution-and-review-stats-backend-api.service'; describe('Contribution and review stats service', () => { let cars: ContributionAndReviewStatsService; @@ -39,7 +39,7 @@ describe('Contribution and review stats service', () => { rejected_translations_count: 0, rejected_translation_word_count: 0, first_contribution_date: 'Mar 2021', - last_contribution_date: 'Mar 2021' + last_contribution_date: 'Mar 2021', }; const translationReviewStat = { language_code: 'es', @@ -50,7 +50,7 @@ describe('Contribution and review stats service', () => { accepted_translations_with_reviewer_edits_count: 0, accepted_translation_word_count: 1, first_contribution_date: 'Mar 2021', - last_contribution_date: 'Mar 2021' + last_contribution_date: 'Mar 2021', }; const questionContributionStat = { topic_name: 'published_topic_name', @@ -58,7 +58,7 @@ describe('Contribution and review stats service', () => { accepted_questions_count: 1, accepted_questions_without_reviewer_edits_count: 0, first_contribution_date: 'Mar 2021', - last_contribution_date: 'Mar 2021' + last_contribution_date: 'Mar 2021', }; const questionReviewStat = { topic_name: 'published_topic_name', @@ -66,26 +66,26 @@ describe('Contribution and review stats service', () => { accepted_questions_count: 1, accepted_questions_with_reviewer_edits_count: 1, first_contribution_date: 'Mar 2021', - last_contribution_date: 'Mar 2021' + last_contribution_date: 'Mar 2021', }; const fetchTranslationContributionStatResponse = { - translation_contribution_stats: [translationContributionStat] + translation_contribution_stats: [translationContributionStat], }; const fetchTranslationReviewStatResponse = { - translation_review_stats: [translationReviewStat] + translation_review_stats: [translationReviewStat], }; const fetchQuestionContributionStatResponse = { - question_contribution_stats: [questionContributionStat] + question_contribution_stats: [questionContributionStat], }; const fetchQuestionReviewStatResponse = { - question_review_stats: [questionReviewStat] + question_review_stats: [questionReviewStat], }; const fetchAllStatsResponse = { translation_contribution_stats: [translationContributionStat], translation_review_stats: [translationReviewStat], question_contribution_stats: [questionContributionStat], - question_review_stats: [questionReviewStat] + question_review_stats: [questionReviewStat], }; beforeEach(() => { @@ -93,106 +93,119 @@ describe('Contribution and review stats service', () => { imports: [HttpClientTestingModule], providers: [ UrlInterpolationService, - ContributionAndReviewStatsBackendApiService - ] + ContributionAndReviewStatsBackendApiService, + ], }); cars = TestBed.inject(ContributionAndReviewStatsService); carbas = TestBed.inject(ContributionAndReviewStatsBackendApiService); }); describe('fetchTranslationContributionStats', () => { - it('should return available translation contribution stats', - () => { - fetchContributionAndReviewStatsAsyncSpy = spyOn( - carbas, 'fetchContributionAndReviewStatsAsync'); - fetchContributionAndReviewStatsAsyncSpy.and.returnValue( - Promise.resolve(fetchTranslationContributionStatResponse)); - - cars.fetchTranslationContributionStats('translator') - .then((response) => { - expect(response.translation_contribution_stats) - .toEqual([translationContributionStat]); - }); - - expect(fetchContributionAndReviewStatsAsyncSpy).toHaveBeenCalled(); + it('should return available translation contribution stats', () => { + fetchContributionAndReviewStatsAsyncSpy = spyOn( + carbas, + 'fetchContributionAndReviewStatsAsync' + ); + fetchContributionAndReviewStatsAsyncSpy.and.returnValue( + Promise.resolve(fetchTranslationContributionStatResponse) + ); + + cars.fetchTranslationContributionStats('translator').then(response => { + expect(response.translation_contribution_stats).toEqual([ + translationContributionStat, + ]); }); + + expect(fetchContributionAndReviewStatsAsyncSpy).toHaveBeenCalled(); + }); }); describe('fetchTranslationReviewStats', () => { - it('should return available translation review stats', - () => { - fetchContributionAndReviewStatsAsyncSpy = spyOn( - carbas, 'fetchContributionAndReviewStatsAsync'); - fetchContributionAndReviewStatsAsyncSpy.and.returnValue( - Promise.resolve(fetchTranslationReviewStatResponse)); - - cars.fetchTranslationReviewStats('translation_reviewer') - .then((response) => { - expect(response.translation_review_stats) - .toEqual([translationReviewStat]); - }); - - expect(fetchContributionAndReviewStatsAsyncSpy).toHaveBeenCalled(); - }); + it('should return available translation review stats', () => { + fetchContributionAndReviewStatsAsyncSpy = spyOn( + carbas, + 'fetchContributionAndReviewStatsAsync' + ); + fetchContributionAndReviewStatsAsyncSpy.and.returnValue( + Promise.resolve(fetchTranslationReviewStatResponse) + ); + + cars + .fetchTranslationReviewStats('translation_reviewer') + .then(response => { + expect(response.translation_review_stats).toEqual([ + translationReviewStat, + ]); + }); + + expect(fetchContributionAndReviewStatsAsyncSpy).toHaveBeenCalled(); + }); }); describe('fetchQuestionContributionStats', () => { - it('should return available question contribution stats', - () => { - fetchContributionAndReviewStatsAsyncSpy = spyOn( - carbas, 'fetchContributionAndReviewStatsAsync'); - fetchContributionAndReviewStatsAsyncSpy.and.returnValue( - Promise.resolve(fetchQuestionContributionStatResponse)); - - cars.fetchQuestionContributionStats('question_submitter') - .then((response) => { - expect(response.question_contribution_stats) - .toEqual([questionContributionStat]); - }); - - expect(fetchContributionAndReviewStatsAsyncSpy).toHaveBeenCalled(); - }); + it('should return available question contribution stats', () => { + fetchContributionAndReviewStatsAsyncSpy = spyOn( + carbas, + 'fetchContributionAndReviewStatsAsync' + ); + fetchContributionAndReviewStatsAsyncSpy.and.returnValue( + Promise.resolve(fetchQuestionContributionStatResponse) + ); + + cars + .fetchQuestionContributionStats('question_submitter') + .then(response => { + expect(response.question_contribution_stats).toEqual([ + questionContributionStat, + ]); + }); + + expect(fetchContributionAndReviewStatsAsyncSpy).toHaveBeenCalled(); + }); }); describe('fetchQuestionReviewStats', () => { - it('should return available question revieew stats', - () => { - fetchContributionAndReviewStatsAsyncSpy = spyOn( - carbas, 'fetchContributionAndReviewStatsAsync'); - fetchContributionAndReviewStatsAsyncSpy.and.returnValue( - Promise.resolve(fetchQuestionReviewStatResponse)); - - cars.fetchQuestionReviewStats('translator') - .then((response) => { - expect(response.question_review_stats) - .toEqual([questionReviewStat]); - }); - - expect(fetchContributionAndReviewStatsAsyncSpy).toHaveBeenCalled(); + it('should return available question revieew stats', () => { + fetchContributionAndReviewStatsAsyncSpy = spyOn( + carbas, + 'fetchContributionAndReviewStatsAsync' + ); + fetchContributionAndReviewStatsAsyncSpy.and.returnValue( + Promise.resolve(fetchQuestionReviewStatResponse) + ); + + cars.fetchQuestionReviewStats('translator').then(response => { + expect(response.question_review_stats).toEqual([questionReviewStat]); }); + + expect(fetchContributionAndReviewStatsAsyncSpy).toHaveBeenCalled(); + }); }); describe('fetchAllStats', () => { - it('should return available all stats', - () => { - fetchAllContributionAndReviewStatsAsync = spyOn( - carbas, 'fetchAllContributionAndReviewStatsAsync'); - fetchAllContributionAndReviewStatsAsync.and.returnValue( - Promise.resolve(fetchAllStatsResponse)); - - cars.fetchAllStats('user') - .then((response) => { - expect(response.translation_contribution_stats) - .toEqual([translationContributionStat]); - expect(response.translation_review_stats) - .toEqual([translationReviewStat]); - expect(response.question_contribution_stats) - .toEqual([questionContributionStat]); - expect(response.question_review_stats) - .toEqual([questionReviewStat]); - }); - - expect(fetchAllContributionAndReviewStatsAsync).toHaveBeenCalled(); + it('should return available all stats', () => { + fetchAllContributionAndReviewStatsAsync = spyOn( + carbas, + 'fetchAllContributionAndReviewStatsAsync' + ); + fetchAllContributionAndReviewStatsAsync.and.returnValue( + Promise.resolve(fetchAllStatsResponse) + ); + + cars.fetchAllStats('user').then(response => { + expect(response.translation_contribution_stats).toEqual([ + translationContributionStat, + ]); + expect(response.translation_review_stats).toEqual([ + translationReviewStat, + ]); + expect(response.question_contribution_stats).toEqual([ + questionContributionStat, + ]); + expect(response.question_review_stats).toEqual([questionReviewStat]); }); + + expect(fetchAllContributionAndReviewStatsAsync).toHaveBeenCalled(); + }); }); }); diff --git a/core/templates/pages/contributor-dashboard-page/services/contribution-and-review-stats.service.ts b/core/templates/pages/contributor-dashboard-page/services/contribution-and-review-stats.service.ts index fa6e83bb79d9..bc14c35b068e 100644 --- a/core/templates/pages/contributor-dashboard-page/services/contribution-and-review-stats.service.ts +++ b/core/templates/pages/contributor-dashboard-page/services/contribution-and-review-stats.service.ts @@ -17,61 +17,60 @@ * and review stats. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { ContributionAndReviewStatsBackendApiService } - from './contribution-and-review-stats-backend-api.service'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {AppConstants} from 'app.constants'; +import {ContributionAndReviewStatsBackendApiService} from './contribution-and-review-stats-backend-api.service'; export interface TranslationContributionBackendDict { - 'language_code': string; - 'topic_name': string; - 'submitted_translations_count': number; - 'submitted_translation_word_count': number; - 'accepted_translations_count': number; - 'accepted_translations_without_reviewer_edits_count': number; - 'accepted_translation_word_count': number; - 'rejected_translations_count': number; - 'rejected_translation_word_count': number; - 'first_contribution_date': string; - 'last_contribution_date': string; + language_code: string; + topic_name: string; + submitted_translations_count: number; + submitted_translation_word_count: number; + accepted_translations_count: number; + accepted_translations_without_reviewer_edits_count: number; + accepted_translation_word_count: number; + rejected_translations_count: number; + rejected_translation_word_count: number; + first_contribution_date: string; + last_contribution_date: string; } export interface TranslationReviewBackendDict { - 'language_code': string; - 'topic_name': string; - 'reviewed_translations_count': number; - 'reviewed_translation_word_count': number; - 'accepted_translations_count': number; - 'accepted_translations_with_reviewer_edits_count': number; - 'accepted_translation_word_count': number; - 'first_contribution_date': string; - 'last_contribution_date': string; + language_code: string; + topic_name: string; + reviewed_translations_count: number; + reviewed_translation_word_count: number; + accepted_translations_count: number; + accepted_translations_with_reviewer_edits_count: number; + accepted_translation_word_count: number; + first_contribution_date: string; + last_contribution_date: string; } export interface QuestionContributionBackendDict { - 'topic_name': string; - 'submitted_questions_count': number; - 'accepted_questions_count': number; - 'accepted_questions_without_reviewer_edits_count': number; - 'first_contribution_date': string; - 'last_contribution_date': string; + topic_name: string; + submitted_questions_count: number; + accepted_questions_count: number; + accepted_questions_without_reviewer_edits_count: number; + first_contribution_date: string; + last_contribution_date: string; } export interface QuestionReviewBackendDict { - 'topic_name': string; - 'reviewed_questions_count': number; - 'accepted_questions_count': number; - 'accepted_questions_with_reviewer_edits_count': number; - 'first_contribution_date': string; - 'last_contribution_date': string; + topic_name: string; + reviewed_questions_count: number; + accepted_questions_count: number; + accepted_questions_with_reviewer_edits_count: number; + first_contribution_date: string; + last_contribution_date: string; } export interface ContributorStatsSummaryBackendDict { - 'translation_contribution_stats': [TranslationContributionBackendDict]; - 'translation_review_stats': [TranslationReviewBackendDict]; - 'question_contribution_stats': [QuestionContributionBackendDict]; - 'question_review_stats': [QuestionReviewBackendDict]; + translation_contribution_stats: [TranslationContributionBackendDict]; + translation_review_stats: [TranslationReviewBackendDict]; + question_contribution_stats: [QuestionContributionBackendDict]; + question_review_stats: [QuestionReviewBackendDict]; } @Injectable({ @@ -79,58 +78,61 @@ export interface ContributorStatsSummaryBackendDict { }) export class ContributionAndReviewStatsService { constructor( - private contributionAndReviewStatsBackendApiService: - ContributionAndReviewStatsBackendApiService + private contributionAndReviewStatsBackendApiService: ContributionAndReviewStatsBackendApiService ) {} async fetchAllStats( - username: string + username: string ): Promise { - return ( - this.contributionAndReviewStatsBackendApiService - .fetchAllContributionAndReviewStatsAsync(username)); + return this.contributionAndReviewStatsBackendApiService.fetchAllContributionAndReviewStatsAsync( + username + ); } async fetchTranslationContributionStats( - username: string + username: string ): Promise { - return ( - this.contributionAndReviewStatsBackendApiService - .fetchContributionAndReviewStatsAsync( - AppConstants.CONTRIBUTION_STATS_TYPE_TRANSLATION, - AppConstants.CONTRIBUTION_STATS_SUBTYPE_SUBMISSION, username)); + return this.contributionAndReviewStatsBackendApiService.fetchContributionAndReviewStatsAsync( + AppConstants.CONTRIBUTION_STATS_TYPE_TRANSLATION, + AppConstants.CONTRIBUTION_STATS_SUBTYPE_SUBMISSION, + username + ); } async fetchTranslationReviewStats( - username: string + username: string ): Promise { - return ( - this.contributionAndReviewStatsBackendApiService - .fetchContributionAndReviewStatsAsync( - AppConstants.CONTRIBUTION_STATS_TYPE_TRANSLATION, - AppConstants.CONTRIBUTION_STATS_SUBTYPE_REVIEW, username)); + return this.contributionAndReviewStatsBackendApiService.fetchContributionAndReviewStatsAsync( + AppConstants.CONTRIBUTION_STATS_TYPE_TRANSLATION, + AppConstants.CONTRIBUTION_STATS_SUBTYPE_REVIEW, + username + ); } async fetchQuestionContributionStats( - username: string + username: string ): Promise { - return ( - this.contributionAndReviewStatsBackendApiService - .fetchContributionAndReviewStatsAsync( - AppConstants.CONTRIBUTION_STATS_TYPE_QUESTION, - AppConstants.CONTRIBUTION_STATS_SUBTYPE_SUBMISSION, username)); + return this.contributionAndReviewStatsBackendApiService.fetchContributionAndReviewStatsAsync( + AppConstants.CONTRIBUTION_STATS_TYPE_QUESTION, + AppConstants.CONTRIBUTION_STATS_SUBTYPE_SUBMISSION, + username + ); } async fetchQuestionReviewStats( - username: string + username: string ): Promise { - return ( - this.contributionAndReviewStatsBackendApiService - .fetchContributionAndReviewStatsAsync( - AppConstants.CONTRIBUTION_STATS_TYPE_QUESTION, - AppConstants.CONTRIBUTION_STATS_SUBTYPE_REVIEW, username)); + return this.contributionAndReviewStatsBackendApiService.fetchContributionAndReviewStatsAsync( + AppConstants.CONTRIBUTION_STATS_TYPE_QUESTION, + AppConstants.CONTRIBUTION_STATS_SUBTYPE_REVIEW, + username + ); } } -angular.module('oppia').factory('ContributionAndReviewStatsService', - downgradeInjectable(ContributionAndReviewStatsService)); +angular + .module('oppia') + .factory( + 'ContributionAndReviewStatsService', + downgradeInjectable(ContributionAndReviewStatsService) + ); diff --git a/core/templates/pages/contributor-dashboard-page/services/contribution-and-review.service.spec.ts b/core/templates/pages/contributor-dashboard-page/services/contribution-and-review.service.spec.ts index 7df74a73aa25..d41864ce7244 100644 --- a/core/templates/pages/contributor-dashboard-page/services/contribution-and-review.service.spec.ts +++ b/core/templates/pages/contributor-dashboard-page/services/contribution-and-review.service.spec.ts @@ -16,23 +16,29 @@ * @fileoverview Unit tests for contribution and review service */ -import { TestBed, fakeAsync, flushMicrotasks, tick } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { AppConstants } from 'app.constants'; -import { ContributionAndReviewService, FetchSuggestionsResponse } from './contribution-and-review.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { ContributionAndReviewBackendApiService } - from './contribution-and-review-backend-api.service'; -import { SuggestionBackendDict } from 'domain/suggestion/suggestion.model'; -import { ReadOnlyExplorationBackendApiService } - from 'domain/exploration/read-only-exploration-backend-api.service'; -import { ExplorationObjectFactory, Exploration} - from 'domain/exploration/ExplorationObjectFactory'; -import { StateObjectsBackendDict } from 'domain/exploration/StatesObjectFactory'; -import { StatesObjectFactory, States } from 'domain/exploration/StatesObjectFactory'; -import { FetchExplorationBackendResponse } from '../../../domain/exploration/read-only-exploration-backend-api.service'; -import { LoggerService } from 'services/contextual/logger.service'; -import { ParamSpecs } from '../../../domain/exploration/ParamSpecsObjectFactory'; +import {TestBed, fakeAsync, flushMicrotasks, tick} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {AppConstants} from 'app.constants'; +import { + ContributionAndReviewService, + FetchSuggestionsResponse, +} from './contribution-and-review.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {ContributionAndReviewBackendApiService} from './contribution-and-review-backend-api.service'; +import {SuggestionBackendDict} from 'domain/suggestion/suggestion.model'; +import {ReadOnlyExplorationBackendApiService} from 'domain/exploration/read-only-exploration-backend-api.service'; +import { + ExplorationObjectFactory, + Exploration, +} from 'domain/exploration/ExplorationObjectFactory'; +import {StateObjectsBackendDict} from 'domain/exploration/StatesObjectFactory'; +import { + StatesObjectFactory, + States, +} from 'domain/exploration/StatesObjectFactory'; +import {FetchExplorationBackendResponse} from '../../../domain/exploration/read-only-exploration-backend-api.service'; +import {LoggerService} from 'services/contextual/logger.service'; +import {ParamSpecs} from '../../../domain/exploration/ParamSpecsObjectFactory'; describe('Contribution and review service', () => { let cars: ContributionAndReviewService; @@ -40,8 +46,7 @@ describe('Contribution and review service', () => { let fetchSuggestionsAsyncSpy: jasmine.Spy; let downloadContributorCertificateAsyncSpy: jasmine.Spy; let statesObjectFactory: StatesObjectFactory; - let readOnlyExplorationBackendApiService: - ReadOnlyExplorationBackendApiService; + let readOnlyExplorationBackendApiService: ReadOnlyExplorationBackendApiService; let urlInterpolationService: UrlInterpolationService; let loggerService: LoggerService; @@ -72,42 +77,36 @@ describe('Contribution and review service', () => { }; const backendFetchResponse = { - suggestions: [ - suggestion1 - ], + suggestions: [suggestion1], target_id_to_opportunity_dict: { skill_id_1: opportunityDict1, }, - next_offset: 1 + next_offset: 1, }; const multiplePageBackendFetchResponse = { - suggestions: [ - suggestion1, - suggestion2, - suggestion3 - ], + suggestions: [suggestion1, suggestion2, suggestion3], target_id_to_opportunity_dict: { skill_id_1: opportunityDict1, skill_id_2: opportunityDict2, - skill_id_3: opportunityDict3 + skill_id_3: opportunityDict3, }, - next_offset: 3 + next_offset: 3, }; const expectedSuggestionDict = { suggestion: suggestion1, - details: backendFetchResponse.target_id_to_opportunity_dict.skill_id_1 + details: backendFetchResponse.target_id_to_opportunity_dict.skill_id_1, }; const expectedSuggestion2Dict = { suggestion: suggestion2, - details: multiplePageBackendFetchResponse - .target_id_to_opportunity_dict.skill_id_2 + details: + multiplePageBackendFetchResponse.target_id_to_opportunity_dict.skill_id_2, }; const expectedSuggestion3Dict = { suggestion: suggestion3, - details: multiplePageBackendFetchResponse - .target_id_to_opportunity_dict.skill_id_3 + details: + multiplePageBackendFetchResponse.target_id_to_opportunity_dict.skill_id_3, }; beforeEach(() => { @@ -119,20 +118,22 @@ describe('Contribution and review service', () => { ReadOnlyExplorationBackendApiService, ExplorationObjectFactory, StatesObjectFactory, - LoggerService - ] + LoggerService, + ], }); cars = TestBed.inject(ContributionAndReviewService); carbas = TestBed.inject(ContributionAndReviewBackendApiService); readOnlyExplorationBackendApiService = TestBed.inject( - ReadOnlyExplorationBackendApiService); + ReadOnlyExplorationBackendApiService + ); loggerService = TestBed.inject(LoggerService); - urlInterpolationService = TestBed.inject( - UrlInterpolationService); + urlInterpolationService = TestBed.inject(UrlInterpolationService); statesObjectFactory = TestBed.inject(StatesObjectFactory); fetchSuggestionsAsyncSpy = spyOn(carbas, 'fetchSuggestionsAsync'); downloadContributorCertificateAsyncSpy = spyOn( - carbas, 'downloadContributorCertificateAsync'); + carbas, + 'downloadContributorCertificateAsync' + ); }); describe('getUserCreatedQuestionSuggestionsAsync', () => { @@ -146,19 +147,21 @@ describe('Contribution and review service', () => { AppConstants.OPPORTUNITIES_PAGE_SIZE = defaultOpportunitiesPageSize; }); - it('should return available question suggestions and opportunity details', - () => { - fetchSuggestionsAsyncSpy.and.returnValue( - Promise.resolve(backendFetchResponse)); + it('should return available question suggestions and opportunity details', () => { + fetchSuggestionsAsyncSpy.and.returnValue( + Promise.resolve(backendFetchResponse) + ); - cars.getUserCreatedQuestionSuggestionsAsync(true, 'sort_key') - .then((response) => { - expect(response.suggestionIdToDetails.suggestion_id_1) - .toEqual(expectedSuggestionDict); - }); + cars + .getUserCreatedQuestionSuggestionsAsync(true, 'sort_key') + .then(response => { + expect(response.suggestionIdToDetails.suggestion_id_1).toEqual( + expectedSuggestionDict + ); + }); - expect(fetchSuggestionsAsyncSpy).toHaveBeenCalled(); - }); + expect(fetchSuggestionsAsyncSpy).toHaveBeenCalled(); + }); it('should fetch one page ahead and cache extra results', fakeAsync(() => { // This throws "Cannot assign to 'OPPORTUNITIES_PAGE_SIZE' because it @@ -171,18 +174,21 @@ describe('Contribution and review service', () => { // Return more than a page's worth of results (3 results for a page size // of 2). fetchSuggestionsAsyncSpy.and.returnValue( - Promise.resolve(multiplePageBackendFetchResponse)); + Promise.resolve(multiplePageBackendFetchResponse) + ); // Only the first 2 results should be returned and the extra result // should be cached. - cars.getUserCreatedQuestionSuggestionsAsync(true, 'sort_key') - .then((response) => { - expect(response.suggestionIdToDetails.suggestion_id_1) - .toEqual(expectedSuggestionDict); - expect(response.suggestionIdToDetails.suggestion_id_2) - .toEqual(expectedSuggestion2Dict); - expect(Object.keys(response.suggestionIdToDetails).length) - .toEqual(2); + cars + .getUserCreatedQuestionSuggestionsAsync(true, 'sort_key') + .then(response => { + expect(response.suggestionIdToDetails.suggestion_id_1).toEqual( + expectedSuggestionDict + ); + expect(response.suggestionIdToDetails.suggestion_id_2).toEqual( + expectedSuggestion2Dict + ); + expect(Object.keys(response.suggestionIdToDetails).length).toEqual(2); expect(response.more).toBeTrue(); }); @@ -197,35 +203,37 @@ describe('Contribution and review service', () => { skill_description: 'skill_description_4', }; const suggestion4BackendFetchResponse = { - suggestions: [ - suggestion4 - ], + suggestions: [suggestion4], target_id_to_opportunity_dict: { skill_id_4: opportunityDict4, }, - next_offset: 4 + next_offset: 4, }; const expectedSuggestion4Dict = { suggestion: suggestion4, - details: suggestion4BackendFetchResponse - .target_id_to_opportunity_dict.skill_id_4 + details: + suggestion4BackendFetchResponse.target_id_to_opportunity_dict + .skill_id_4, }; // Return a 4th suggestion from the backend that was not available in the // first fetch. fetchSuggestionsAsyncSpy.and.returnValue( - Promise.resolve(suggestion4BackendFetchResponse)); + Promise.resolve(suggestion4BackendFetchResponse) + ); // Return both the cached 3rd suggestion and the new 4th suggestion to the // caller. - cars.getUserCreatedQuestionSuggestionsAsync(false, 'sort_key') - .then((response) => { - expect(response.suggestionIdToDetails.suggestion_id_3) - .toEqual(expectedSuggestion3Dict); - expect(response.suggestionIdToDetails.suggestion_id_4) - .toEqual(expectedSuggestion4Dict); - expect(Object.keys(response.suggestionIdToDetails).length) - .toEqual(2); + cars + .getUserCreatedQuestionSuggestionsAsync(false, 'sort_key') + .then(response => { + expect(response.suggestionIdToDetails.suggestion_id_3).toEqual( + expectedSuggestion3Dict + ); + expect(response.suggestionIdToDetails.suggestion_id_4).toEqual( + expectedSuggestion4Dict + ); + expect(Object.keys(response.suggestionIdToDetails).length).toEqual(2); expect(response.more).toBeFalse(); }); })); @@ -241,18 +249,21 @@ describe('Contribution and review service', () => { // Return more than a page's worth of results (3 results for a page size // of 2). fetchSuggestionsAsyncSpy.and.returnValue( - Promise.resolve(multiplePageBackendFetchResponse)); + Promise.resolve(multiplePageBackendFetchResponse) + ); // Only the first 2 results should be returned and the extra result // should be cached. - cars.getUserCreatedQuestionSuggestionsAsync(true, 'sort_key') - .then((response) => { - expect(response.suggestionIdToDetails.suggestion_id_1) - .toEqual(expectedSuggestionDict); - expect(response.suggestionIdToDetails.suggestion_id_2) - .toEqual(expectedSuggestion2Dict); - expect(Object.keys(response.suggestionIdToDetails).length) - .toEqual(2); + cars + .getUserCreatedQuestionSuggestionsAsync(true, 'sort_key') + .then(response => { + expect(response.suggestionIdToDetails.suggestion_id_1).toEqual( + expectedSuggestionDict + ); + expect(response.suggestionIdToDetails.suggestion_id_2).toEqual( + expectedSuggestion2Dict + ); + expect(Object.keys(response.suggestionIdToDetails).length).toEqual(2); expect(response.more).toBeTrue(); }); @@ -260,37 +271,46 @@ describe('Contribution and review service', () => { // Fetch again from offset 0. fetchSuggestionsAsyncSpy.and.returnValue( - Promise.resolve(multiplePageBackendFetchResponse)); + Promise.resolve(multiplePageBackendFetchResponse) + ); // Return the first 2 results from offset 0 again. - cars.getUserCreatedQuestionSuggestionsAsync(true, 'sort_key') - .then((response) => { - expect(response.suggestionIdToDetails.suggestion_id_1) - .toEqual(expectedSuggestionDict); - expect(response.suggestionIdToDetails.suggestion_id_2) - .toEqual(expectedSuggestion2Dict); - expect(Object.keys(response.suggestionIdToDetails).length) - .toEqual(2); + cars + .getUserCreatedQuestionSuggestionsAsync(true, 'sort_key') + .then(response => { + expect(response.suggestionIdToDetails.suggestion_id_1).toEqual( + expectedSuggestionDict + ); + expect(response.suggestionIdToDetails.suggestion_id_2).toEqual( + expectedSuggestion2Dict + ); + expect(Object.keys(response.suggestionIdToDetails).length).toEqual(2); expect(response.more).toBeTrue(); }); })); }); describe('downloadContributorCertificateAsync', () => { - it('should download the contributor certificate', - () => { - downloadContributorCertificateAsyncSpy.and.returnValue( - Promise.resolve({ - from_date: '1 Nov 2022', - to_date: '1 Dec 2022', - contribution_hours: 1.0, - team_lead: 'Test User', - language: 'Hindi' - })); - - cars.downloadContributorCertificateAsync( - 'user', 'translate_content', 'hi', '2022-01-01', '2022-01-02' - ).then((response) => { + it('should download the contributor certificate', () => { + downloadContributorCertificateAsyncSpy.and.returnValue( + Promise.resolve({ + from_date: '1 Nov 2022', + to_date: '1 Dec 2022', + contribution_hours: 1.0, + team_lead: 'Test User', + language: 'Hindi', + }) + ); + + cars + .downloadContributorCertificateAsync( + 'user', + 'translate_content', + 'hi', + '2022-01-01', + '2022-01-02' + ) + .then(response => { expect(response.from_date).toEqual('1 Nov 2022'); expect(response.to_date).toEqual('1 Dec 2022'); expect(response.contribution_hours).toEqual(1.0); @@ -298,50 +318,51 @@ describe('Contribution and review service', () => { expect(response.language).toEqual('Hindi'); }); - expect(downloadContributorCertificateAsyncSpy).toHaveBeenCalled(); - }); + expect(downloadContributorCertificateAsyncSpy).toHaveBeenCalled(); + }); }); describe('getReviewableQuestionSuggestionsAsync', () => { - it('should return available question suggestions and opportunity details', - () => { - fetchSuggestionsAsyncSpy.and.returnValue( - Promise.resolve(backendFetchResponse)); - - cars.getReviewableQuestionSuggestionsAsync( - true, - 'sort_key', - 'topicName') - .then((response) => { - expect(response.suggestionIdToDetails.suggestion_id_1) - .toEqual(expectedSuggestionDict); - }); + it('should return available question suggestions and opportunity details', () => { + fetchSuggestionsAsyncSpy.and.returnValue( + Promise.resolve(backendFetchResponse) + ); - expect(fetchSuggestionsAsyncSpy).toHaveBeenCalledWith( - 'REVIEWABLE_QUESTION_SUGGESTIONS', - 20, - 0, - 'sort_key', - null, - 'topicName', - ); - }); + cars + .getReviewableQuestionSuggestionsAsync(true, 'sort_key', 'topicName') + .then(response => { + expect(response.suggestionIdToDetails.suggestion_id_1).toEqual( + expectedSuggestionDict + ); + }); + + expect(fetchSuggestionsAsyncSpy).toHaveBeenCalledWith( + 'REVIEWABLE_QUESTION_SUGGESTIONS', + 20, + 0, + 'sort_key', + null, + 'topicName' + ); + }); }); describe('getUserCreatedTranslationSuggestionsAsync', () => { - it('should return translation suggestions and opportunity details', - () => { - fetchSuggestionsAsyncSpy.and.returnValue( - Promise.resolve(backendFetchResponse)); + it('should return translation suggestions and opportunity details', () => { + fetchSuggestionsAsyncSpy.and.returnValue( + Promise.resolve(backendFetchResponse) + ); - cars.getUserCreatedTranslationSuggestionsAsync(true, 'sort_key') - .then((response) => { - expect(response.suggestionIdToDetails.suggestion_id_1) - .toEqual(expectedSuggestionDict); - }); + cars + .getUserCreatedTranslationSuggestionsAsync(true, 'sort_key') + .then(response => { + expect(response.suggestionIdToDetails.suggestion_id_1).toEqual( + expectedSuggestionDict + ); + }); - expect(fetchSuggestionsAsyncSpy).toHaveBeenCalled(); - }); + expect(fetchSuggestionsAsyncSpy).toHaveBeenCalled(); + }); }); describe('getReviewableTranslationSuggestionsAsync', () => { @@ -353,452 +374,533 @@ describe('Contribution and review service', () => { beforeEach(() => { explorationObjectFactory = TestBed.inject(ExplorationObjectFactory); explorationObjectFactorySpy = spyOn( - explorationObjectFactory, 'createFromBackendDict'); + explorationObjectFactory, + 'createFromBackendDict' + ); fetchExplorationSpy = spyOn( - readOnlyExplorationBackendApiService, 'fetchExplorationAsync'); - mockSortTranslationSpy = spyOn( - cars, 'sortTranslationSuggestionsByState'); + readOnlyExplorationBackendApiService, + 'fetchExplorationAsync' + ); + mockSortTranslationSpy = spyOn(cars, 'sortTranslationSuggestionsByState'); }); - it('should return translation suggestions and opportunity details', - () => { - fetchSuggestionsAsyncSpy.and.returnValue( - Promise.resolve(backendFetchResponse)); + it('should return translation suggestions and opportunity details', () => { + fetchSuggestionsAsyncSpy.and.returnValue( + Promise.resolve(backendFetchResponse) + ); - cars.getReviewableTranslationSuggestionsAsync( - /* ShouldResetOffset= */ true, 'skill_id_1') - .then((response) => { - expect(response.suggestionIdToDetails.suggestion_id_1) - .toEqual(expectedSuggestionDict); - }); + cars + .getReviewableTranslationSuggestionsAsync( + /* ShouldResetOffset= */ true, + 'skill_id_1' + ) + .then(response => { + expect(response.suggestionIdToDetails.suggestion_id_1).toEqual( + expectedSuggestionDict + ); + }); - expect(fetchSuggestionsAsyncSpy).toHaveBeenCalled(); - }); + expect(fetchSuggestionsAsyncSpy).toHaveBeenCalled(); + }); - it('should return translation suggestions for given ' + - 'exploration', async() => { - fetchSuggestionsAsyncSpy.and.returnValue( - Promise.resolve(backendFetchResponse)); - const fetchTranslationSuggestionsAsyncSpy = spyOn( - cars, 'fetchTranslationSuggestionsAsync').and.returnValue( - Promise.resolve({ - suggestionIdToDetails: { - skill_id_1: { - suggestions: suggestion1, - details: opportunityDict1 - } - }, - more: false - } as unknown as FetchSuggestionsResponse)); - - cars.getReviewableTranslationSuggestionsAsync( - true, 'skill_id_1', '1') - .then((response) => { - expect(response.suggestionIdToDetails.skill_id_1) - .toEqual({ + it( + 'should return translation suggestions for given ' + 'exploration', + async () => { + fetchSuggestionsAsyncSpy.and.returnValue( + Promise.resolve(backendFetchResponse) + ); + const fetchTranslationSuggestionsAsyncSpy = spyOn( + cars, + 'fetchTranslationSuggestionsAsync' + ).and.returnValue( + Promise.resolve({ + suggestionIdToDetails: { + skill_id_1: { + suggestions: suggestion1, + details: opportunityDict1, + }, + }, + more: false, + } as unknown as FetchSuggestionsResponse) + ); + + cars + .getReviewableTranslationSuggestionsAsync(true, 'skill_id_1', '1') + .then(response => { + expect(response.suggestionIdToDetails.skill_id_1).toEqual({ suggestions: suggestion1, - details: opportunityDict1 + details: opportunityDict1, }); - expect(fetchTranslationSuggestionsAsyncSpy).toHaveBeenCalled(); - }); - }); - - it('should correctly fetch translation suggestions and return ' + - 'the transformed result', async() => { - const mockStates = { - Introduction: { - classifier_model_id: null, - content: { - content_id: 'content', - html: '' - }, - recorded_voiceovers: { - voiceovers_mapping: { - content: {}, - default_outcome: {} - } - }, - interaction: { - answer_groups: [], - confirmed_unclassified_answers: [], - customization_args: { - buttonText: { - value: 'Continue' - } + expect(fetchTranslationSuggestionsAsyncSpy).toHaveBeenCalled(); + }); + } + ); + + it( + 'should correctly fetch translation suggestions and return ' + + 'the transformed result', + async () => { + const mockStates = { + Introduction: { + classifier_model_id: null, + content: { + content_id: 'content', + html: '', + }, + recorded_voiceovers: { + voiceovers_mapping: { + content: {}, + default_outcome: {}, + }, }, - default_outcome: { - dest: 'End State', - dest_if_really_stuck: null, - feedback: { - content_id: 'default_outcome', - html: '' + interaction: { + answer_groups: [], + confirmed_unclassified_answers: [], + customization_args: { + buttonText: { + value: 'Continue', + }, + }, + default_outcome: { + dest: 'End State', + dest_if_really_stuck: null, + feedback: { + content_id: 'default_outcome', + html: '', + }, + param_changes: [], + labelled_as_correct: true, + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, }, - param_changes: [], - labelled_as_correct: true, - refresher_exploration_id: null, - missing_prerequisite_skill_id: null + hints: [], + solution: null, + id: 'Continue', }, - hints: [], - solution: null, - id: 'Continue' + linked_skill_id: null, + param_changes: [], + solicit_answer_details: false, + card_is_checkpoint: true, }, - linked_skill_id: null, - param_changes: [], - solicit_answer_details: false, - card_is_checkpoint: true - }, - 'End State': { - classifier_model_id: null, - content: { - content_id: 'content', - html: '' + 'End State': { + classifier_model_id: null, + content: { + content_id: 'content', + html: '', + }, + recorded_voiceovers: { + voiceovers_mapping: { + content: {}, + default_outcome: {}, + }, + }, + interaction: { + answer_groups: [], + confirmed_unclassified_answers: [], + customization_args: { + recommendedExplorationIds: { + value: [], + }, + }, + default_outcome: null, + hints: [], + solution: null, + id: 'EndExploration', + }, + linked_skill_id: null, + param_changes: [], + solicit_answer_details: false, + card_is_checkpoint: false, }, - recorded_voiceovers: { - voiceovers_mapping: { - content: {}, - default_outcome: {} - } + } as StateObjectsBackendDict; + const states = statesObjectFactory.createFromBackendDict(mockStates); + const mockReadOnlyExplorationData: FetchExplorationBackendResponse = { + can_edit: true, + exploration: { + init_state_name: 'Introduction', + param_changes: [], + param_specs: {}, + states: mockStates, + title: 'Dummy Title', + language_code: 'en', + objective: 'Dummy Objective', + next_content_id_index: 4, }, - interaction: { - answer_groups: [], - confirmed_unclassified_answers: [], - customization_args: { - recommendedExplorationIds: { - value: [] - } - }, - default_outcome: null, - hints: [], - solution: null, - id: 'EndExploration' + exploration_metadata: { + title: 'Dummy Title', + category: 'Dummy Category', + objective: 'Dummy Objective', + language_code: 'en', + tags: [], + blurb: 'Dummy Blurb', + author_notes: 'Dummy Notes', + states_schema_version: 0, + init_state_name: 'Introduction', + param_specs: {}, + param_changes: [], + auto_tts_enabled: true, + edits_allowed: true, }, - linked_skill_id: null, - param_changes: [], - solicit_answer_details: false, - card_is_checkpoint: false - } - } as StateObjectsBackendDict; - const states = statesObjectFactory.createFromBackendDict( - mockStates); - const mockReadOnlyExplorationData: FetchExplorationBackendResponse = { - can_edit: true, - exploration: { - init_state_name: 'Introduction', - param_changes: [], - param_specs: {}, - states: mockStates, - title: 'Dummy Title', - language_code: 'en', - objective: 'Dummy Objective', - next_content_id_index: 4, - }, - exploration_metadata: { - title: 'Dummy Title', - category: 'Dummy Category', - objective: 'Dummy Objective', - language_code: 'en', - tags: [], - blurb: 'Dummy Blurb', - author_notes: 'Dummy Notes', - states_schema_version: 0, - init_state_name: 'Introduction', - param_specs: {}, - param_changes: [], + exploration_id: '1', + is_logged_in: true, + session_id: '0', + version: 0, + preferred_audio_language_code: 'en', + preferred_language_codes: [], auto_tts_enabled: true, - edits_allowed: true, - }, - exploration_id: '1', - is_logged_in: true, - session_id: '0', - version: 0, - preferred_audio_language_code: 'en', - preferred_language_codes: [], - auto_tts_enabled: true, - record_playthrough_probability: 1, - draft_change_list_id: 1, - has_viewed_lesson_info_modal_once: false, - furthest_reached_checkpoint_exp_version: 0, - furthest_reached_checkpoint_state_name: '', - most_recently_reached_checkpoint_state_name: '', - most_recently_reached_checkpoint_exp_version: 1, - displayable_language_codes: ['en'], - }; - const exploration: Exploration = new Exploration( - mockReadOnlyExplorationData.exploration.init_state_name, - [], - {} as unknown as ParamSpecs, - mockReadOnlyExplorationData.exploration.states as unknown as States, - mockReadOnlyExplorationData.exploration.title, - mockReadOnlyExplorationData.exploration.next_content_id_index, - mockReadOnlyExplorationData.exploration.language_code, - loggerService, - urlInterpolationService - ); - const getStatesSpy = spyOn(exploration, 'getStates'); + record_playthrough_probability: 1, + draft_change_list_id: 1, + has_viewed_lesson_info_modal_once: false, + furthest_reached_checkpoint_exp_version: 0, + furthest_reached_checkpoint_state_name: '', + most_recently_reached_checkpoint_state_name: '', + most_recently_reached_checkpoint_exp_version: 1, + displayable_language_codes: ['en'], + }; + const exploration: Exploration = new Exploration( + mockReadOnlyExplorationData.exploration.init_state_name, + [], + {} as unknown as ParamSpecs, + mockReadOnlyExplorationData.exploration.states as unknown as States, + mockReadOnlyExplorationData.exploration.title, + mockReadOnlyExplorationData.exploration.next_content_id_index, + mockReadOnlyExplorationData.exploration.language_code, + loggerService, + urlInterpolationService + ); + const getStatesSpy = spyOn(exploration, 'getStates'); - fetchExplorationSpy.and.returnValue( - Promise.resolve(mockReadOnlyExplorationData)); - fetchSuggestionsAsyncSpy.and.returnValue( - Promise.resolve(backendFetchResponse)); - explorationObjectFactorySpy.and.returnValue( - exploration); - mockSortTranslationSpy.and.returnValue([ - { - suggestion_type: 'suggestion', - suggestion_id: 'id', - target_type: 'exploration', - target_id: '1', - status: 'review', - author_name: 'author', - change_cmd: { - state_name: 'First State', - content_id: 'content_1' + fetchExplorationSpy.and.returnValue( + Promise.resolve(mockReadOnlyExplorationData) + ); + fetchSuggestionsAsyncSpy.and.returnValue( + Promise.resolve(backendFetchResponse) + ); + explorationObjectFactorySpy.and.returnValue(exploration); + mockSortTranslationSpy.and.returnValue([ + { + suggestion_type: 'suggestion', + suggestion_id: 'id', + target_type: 'exploration', + target_id: '1', + status: 'review', + author_name: 'author', + change_cmd: { + state_name: 'First State', + content_id: 'content_1', + }, + last_updated_msecs: 0, }, - last_updated_msecs: 0, - }, - { - suggestion_type: 'suggestion', - suggestion_id: 'id', - target_type: 'exploration', - target_id: '1', - status: 'review', - author_name: 'author', - change_cmd: { - state_name: 'First State', - content_id: 'content_3' + { + suggestion_type: 'suggestion', + suggestion_id: 'id', + target_type: 'exploration', + target_id: '1', + status: 'review', + author_name: 'author', + change_cmd: { + state_name: 'First State', + content_id: 'content_3', + }, + last_updated_msecs: 0, }, - last_updated_msecs: 0, - }, - { - suggestion_type: 'suggestion', - suggestion_id: 'id', - target_type: 'exploration', - target_id: '1', - status: 'review', - author_name: 'author', - change_cmd: { - state_name: 'First State', - content_id: 'feedback_2' + { + suggestion_type: 'suggestion', + suggestion_id: 'id', + target_type: 'exploration', + target_id: '1', + status: 'review', + author_name: 'author', + change_cmd: { + state_name: 'First State', + content_id: 'feedback_2', + }, + last_updated_msecs: 0, }, - last_updated_msecs: 0, - }, - { - suggestion_type: 'suggestion', - suggestion_id: 'id', - target_type: 'exploration', - target_id: '1', - status: 'review', - author_name: 'author', - change_cmd: { - state_name: 'End State', - content_id: 'content_2' + { + suggestion_type: 'suggestion', + suggestion_id: 'id', + target_type: 'exploration', + target_id: '1', + status: 'review', + author_name: 'author', + change_cmd: { + state_name: 'End State', + content_id: 'content_2', + }, + last_updated_msecs: 0, }, - last_updated_msecs: 0, - }, - { - suggestion_type: 'suggestion', - suggestion_id: 'id', - target_type: 'exploration', - target_id: '1', - status: 'review', - author_name: 'author', - change_cmd: { - state_name: 'End State', - content_id: 'interaction_1' + { + suggestion_type: 'suggestion', + suggestion_id: 'id', + target_type: 'exploration', + target_id: '1', + status: 'review', + author_name: 'author', + change_cmd: { + state_name: 'End State', + content_id: 'interaction_1', + }, + last_updated_msecs: 0, }, - last_updated_msecs: 0, - }, - { - suggestion_type: 'suggestion', - suggestion_id: 'id', - target_type: 'exploration', - target_id: '1', - status: 'review', - author_name: 'author', - change_cmd: { - state_name: 'End State', - content_id: 'hints_1' + { + suggestion_type: 'suggestion', + suggestion_id: 'id', + target_type: 'exploration', + target_id: '1', + status: 'review', + author_name: 'author', + change_cmd: { + state_name: 'End State', + content_id: 'hints_1', + }, + last_updated_msecs: 0, }, - last_updated_msecs: 0, - } - ]); - getStatesSpy.and.returnValue(states); - - await cars.fetchTranslationSuggestionsAsync( - '1').then((response)=>{ - expect(response).toEqual({ - suggestionIdToDetails: { - id: { - suggestion: { - suggestion_type: 'suggestion', - suggestion_id: 'id', - target_type: 'exploration', - target_id: '1', - status: 'review', - author_name: 'author', - change_cmd: { - state_name: 'End State', - content_id: 'hints_1', + ]); + getStatesSpy.and.returnValue(states); + + await cars.fetchTranslationSuggestionsAsync('1').then(response => { + expect(response).toEqual({ + suggestionIdToDetails: { + id: { + suggestion: { + suggestion_type: 'suggestion', + suggestion_id: 'id', + target_type: 'exploration', + target_id: '1', + status: 'review', + author_name: 'author', + change_cmd: { + state_name: 'End State', + content_id: 'hints_1', + }, + last_updated_msecs: 0, }, - last_updated_msecs: 0 + details: undefined, }, - details: undefined, }, - }, - more: false + more: false, + }); + expect(explorationObjectFactorySpy).toHaveBeenCalled(); + expect(getStatesSpy).toHaveBeenCalled(); + expect(fetchSuggestionsAsyncSpy).toHaveBeenCalled(); + expect(fetchExplorationSpy).toHaveBeenCalled(); }); - expect(explorationObjectFactorySpy).toHaveBeenCalled(); - expect(getStatesSpy).toHaveBeenCalled(); - expect(fetchSuggestionsAsyncSpy).toHaveBeenCalled(); - expect(fetchExplorationSpy).toHaveBeenCalled(); - }); - }); + } + ); }); describe('reviewExplorationSuggestion', () => { const requestBody = { action: 'accept', review_message: 'review message', - commit_message: 'commit message' + commit_message: 'commit message', }; let onSuccess: jasmine.Spy<(suggestionId: string) => void>; let onFailure: jasmine.Spy<(errorMessage: string) => void>; beforeEach(() => { - onSuccess = jasmine.createSpy( - 'onSuccess', (suggestionId: string) => {}); + onSuccess = jasmine.createSpy('onSuccess', (suggestionId: string) => {}); onFailure = jasmine.createSpy('onFailure', (errorMessage: string) => {}); }); - it('should call onSuccess function on' + - 'resolving suggestion to exploration correctly', fakeAsync(() => { - spyOn(carbas, 'reviewExplorationSuggestionAsync') - .and.returnValue(Promise.resolve()); - - cars.reviewExplorationSuggestion( - 'abc', 'pqr', 'accept', 'review message', 'commit message', - onSuccess, onFailure - ); - tick(); + it( + 'should call onSuccess function on' + + 'resolving suggestion to exploration correctly', + fakeAsync(() => { + spyOn(carbas, 'reviewExplorationSuggestionAsync').and.returnValue( + Promise.resolve() + ); - expect(carbas.reviewExplorationSuggestionAsync).toHaveBeenCalledWith( - 'abc', 'pqr', requestBody); - expect(onSuccess).toHaveBeenCalledWith('pqr'); - expect(onFailure).not.toHaveBeenCalled(); - })); + cars.reviewExplorationSuggestion( + 'abc', + 'pqr', + 'accept', + 'review message', + 'commit message', + onSuccess, + onFailure + ); + tick(); - it('should call onFailure function when' + - 'resolving suggestion to exploration fails', fakeAsync(() => { - spyOn(carbas, 'reviewExplorationSuggestionAsync').and - .returnValue(Promise.reject({ - error: {error: 'Backend error'} - })); + expect(carbas.reviewExplorationSuggestionAsync).toHaveBeenCalledWith( + 'abc', + 'pqr', + requestBody + ); + expect(onSuccess).toHaveBeenCalledWith('pqr'); + expect(onFailure).not.toHaveBeenCalled(); + }) + ); + + it( + 'should call onFailure function when' + + 'resolving suggestion to exploration fails', + fakeAsync(() => { + spyOn(carbas, 'reviewExplorationSuggestionAsync').and.returnValue( + Promise.reject({ + error: {error: 'Backend error'}, + }) + ); - cars.reviewExplorationSuggestion( - 'abc', 'pqr', 'accept', 'review message', 'commit message', - onSuccess, onFailure - ); - tick(); + cars.reviewExplorationSuggestion( + 'abc', + 'pqr', + 'accept', + 'review message', + 'commit message', + onSuccess, + onFailure + ); + tick(); - expect(carbas.reviewExplorationSuggestionAsync).toHaveBeenCalledWith( - 'abc', 'pqr', requestBody); - expect(onSuccess).not.toHaveBeenCalled(); - expect(onFailure).toHaveBeenCalled(); - })); + expect(carbas.reviewExplorationSuggestionAsync).toHaveBeenCalledWith( + 'abc', + 'pqr', + requestBody + ); + expect(onSuccess).not.toHaveBeenCalled(); + expect(onFailure).toHaveBeenCalled(); + }) + ); }); describe('reviewSkillSuggestion', () => { const requestBody = { action: 'accept', review_message: 'review message', - skill_difficulty: 'easy' + skill_difficulty: 'easy', }; let onSuccess: jasmine.Spy<(suggestionId: string) => void>; let onFailure: jasmine.Spy<() => void>; beforeEach(() => { - onSuccess = jasmine.createSpy( - 'onSuccess', (suggestionId: string) => {}); + onSuccess = jasmine.createSpy('onSuccess', (suggestionId: string) => {}); onFailure = jasmine.createSpy('onFailure', () => {}); }); - it('should call onSuccess function on' + - 'resolving suggestion to skill correctly', fakeAsync(() => { - spyOn( - carbas, 'reviewSkillSuggestionAsync' - ).and.returnValue(Promise.resolve()); - - cars.reviewSkillSuggestion( - 'abc', 'pqr', 'accept', 'review message', 'easy', onSuccess, onFailure); - tick(); + it( + 'should call onSuccess function on' + + 'resolving suggestion to skill correctly', + fakeAsync(() => { + spyOn(carbas, 'reviewSkillSuggestionAsync').and.returnValue( + Promise.resolve() + ); - expect(carbas.reviewSkillSuggestionAsync) - .toHaveBeenCalledWith('abc', 'pqr', requestBody); - expect(onSuccess).toHaveBeenCalledWith('pqr'); - expect(onFailure).not.toHaveBeenCalled(); - })); + cars.reviewSkillSuggestion( + 'abc', + 'pqr', + 'accept', + 'review message', + 'easy', + onSuccess, + onFailure + ); + tick(); - it('should call onFailure function when' + - 'resolving suggestion to skill fails', fakeAsync(() => { - spyOn( - carbas, 'reviewSkillSuggestionAsync' - ).and.returnValue(Promise.reject()); + expect(carbas.reviewSkillSuggestionAsync).toHaveBeenCalledWith( + 'abc', + 'pqr', + requestBody + ); + expect(onSuccess).toHaveBeenCalledWith('pqr'); + expect(onFailure).not.toHaveBeenCalled(); + }) + ); + + it( + 'should call onFailure function when' + + 'resolving suggestion to skill fails', + fakeAsync(() => { + spyOn(carbas, 'reviewSkillSuggestionAsync').and.returnValue( + Promise.reject() + ); - cars.reviewSkillSuggestion( - 'abc', 'pqr', 'accept', 'review message', 'easy', onSuccess, onFailure); - tick(); + cars.reviewSkillSuggestion( + 'abc', + 'pqr', + 'accept', + 'review message', + 'easy', + onSuccess, + onFailure + ); + tick(); - expect(carbas.reviewSkillSuggestionAsync) - .toHaveBeenCalledWith('abc', 'pqr', requestBody); - expect(onSuccess).not.toHaveBeenCalled(); - expect(onFailure).toHaveBeenCalled(); - })); + expect(carbas.reviewSkillSuggestionAsync).toHaveBeenCalledWith( + 'abc', + 'pqr', + requestBody + ); + expect(onSuccess).not.toHaveBeenCalled(); + expect(onFailure).toHaveBeenCalled(); + }) + ); }); describe('updateTranslationSuggestionAsync', () => { const requestBody = { - translation_html: '

In English

' + translation_html: '

In English

', }; let onSuccess: jasmine.Spy<() => void>; let onFailure: jasmine.Spy<(error: unknown) => void>; beforeEach(() => { - onSuccess = jasmine.createSpy( - 'onSuccess', () => {}); - onFailure = jasmine.createSpy('onFailure', (error) => {}); + onSuccess = jasmine.createSpy('onSuccess', () => {}); + onFailure = jasmine.createSpy('onFailure', error => {}); }); - it('should call onSuccess function when' + - 'updateTranslationSuggestionAsync succeeds', fakeAsync(() => { - spyOn(carbas, 'updateTranslationSuggestionAsync').and - .returnValue(Promise.resolve()); - - cars.updateTranslationSuggestionAsync( - 'pqr', '

In English

', onSuccess, onFailure); - tick(); + it( + 'should call onSuccess function when' + + 'updateTranslationSuggestionAsync succeeds', + fakeAsync(() => { + spyOn(carbas, 'updateTranslationSuggestionAsync').and.returnValue( + Promise.resolve() + ); - expect(carbas.updateTranslationSuggestionAsync) - .toHaveBeenCalledWith('pqr', requestBody); - expect(onSuccess).toHaveBeenCalled(); - expect(onFailure).not.toHaveBeenCalled(); - })); + cars.updateTranslationSuggestionAsync( + 'pqr', + '

In English

', + onSuccess, + onFailure + ); + tick(); - it('should call onFailure function when' + - 'updateTranslationSuggestionAsync fails', fakeAsync(() => { - spyOn(carbas, 'updateTranslationSuggestionAsync').and - .returnValue(Promise.reject()); + expect(carbas.updateTranslationSuggestionAsync).toHaveBeenCalledWith( + 'pqr', + requestBody + ); + expect(onSuccess).toHaveBeenCalled(); + expect(onFailure).not.toHaveBeenCalled(); + }) + ); + + it( + 'should call onFailure function when' + + 'updateTranslationSuggestionAsync fails', + fakeAsync(() => { + spyOn(carbas, 'updateTranslationSuggestionAsync').and.returnValue( + Promise.reject() + ); - cars.updateTranslationSuggestionAsync( - 'pqr', '

In English

', onSuccess, onFailure); - tick(); + cars.updateTranslationSuggestionAsync( + 'pqr', + '

In English

', + onSuccess, + onFailure + ); + tick(); - expect(carbas.updateTranslationSuggestionAsync) - .toHaveBeenCalledWith('pqr', requestBody); - expect(onSuccess).not.toHaveBeenCalled(); - expect(onFailure).toHaveBeenCalled(); - })); + expect(carbas.updateTranslationSuggestionAsync).toHaveBeenCalledWith( + 'pqr', + requestBody + ); + expect(onSuccess).not.toHaveBeenCalled(); + expect(onFailure).toHaveBeenCalled(); + }) + ); }); describe('updateQuestionSuggestionAsync', () => { @@ -806,13 +908,13 @@ describe('Contribution and review service', () => { classifier_model_id: null, content: { content_id: 'content', - html: '' + html: '', }, recorded_voiceovers: { voiceovers_mapping: { content: {}, - default_outcome: {} - } + default_outcome: {}, + }, }, interaction: { answer_groups: [], @@ -821,20 +923,20 @@ describe('Contribution and review service', () => { placeholder: { value: { content_id: 'ca_placeholder_0', - unicode_str: '' - } + unicode_str: '', + }, }, - rows: { value: 1 }, + rows: {value: 1}, catchMisspellings: { - value: false - } + value: false, + }, }, default_outcome: { dest: 'new state', dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: '' + html: '', }, labelled_as_correct: false, param_changes: [], @@ -847,10 +949,10 @@ describe('Contribution and review service', () => { correct_answer: 'answer', explanation: { content_id: 'solution', - html: '

This is an explanation.

' - } + html: '

This is an explanation.

', + }, }, - id: 'TextInput' + id: 'TextInput', }, linked_skill_id: null, param_changes: [], @@ -860,13 +962,15 @@ describe('Contribution and review service', () => { const payload = { skill_difficulty: 'easy', - question_state_data: questionStateData + question_state_data: questionStateData, }; - const imagesData = [{ - filename: 'image1.png', - imageBlob: new Blob() - }]; + const imagesData = [ + { + filename: 'image1.png', + imageBlob: new Blob(), + }, + ]; const requestBody = new FormData(); requestBody.append('payload', JSON.stringify(payload)); @@ -880,41 +984,65 @@ describe('Contribution and review service', () => { let onFailure: jasmine.Spy<(suggestionId: string) => void>; beforeEach(() => { - onSuccess = jasmine.createSpy( - 'onSuccess', (suggestionId: string) => {}); - onFailure = jasmine.createSpy( - 'onFailure', (suggestionId: string) => {}); + onSuccess = jasmine.createSpy('onSuccess', (suggestionId: string) => {}); + onFailure = jasmine.createSpy('onFailure', (suggestionId: string) => {}); }); - it('should call onSuccess function when' + - 'updateQuestionSuggestionAsync succeeds', fakeAsync(() =>{ - spyOn(carbas, 'updateQuestionSuggestionAsync').and - .returnValue(Promise.resolve()); - - cars.updateQuestionSuggestionAsync( - 'pqr', 2, questionStateData, 10, imagesData, onSuccess, onFailure); - tick(); + it( + 'should call onSuccess function when' + + 'updateQuestionSuggestionAsync succeeds', + fakeAsync(() => { + spyOn(carbas, 'updateQuestionSuggestionAsync').and.returnValue( + Promise.resolve() + ); - expect(carbas.updateQuestionSuggestionAsync) - .toHaveBeenCalledWith('pqr', requestBody); - expect(onSuccess).toHaveBeenCalledWith('pqr'); - expect(onFailure).not.toHaveBeenCalled(); - })); + cars.updateQuestionSuggestionAsync( + 'pqr', + 2, + questionStateData, + 10, + imagesData, + onSuccess, + onFailure + ); + tick(); - it('should call onFailure function when' + - 'updateQuestionSuggestionAsync fails', fakeAsync(() =>{ - spyOn(carbas, 'updateQuestionSuggestionAsync').and - .returnValue(Promise.reject()); + expect(carbas.updateQuestionSuggestionAsync).toHaveBeenCalledWith( + 'pqr', + requestBody + ); + expect(onSuccess).toHaveBeenCalledWith('pqr'); + expect(onFailure).not.toHaveBeenCalled(); + }) + ); + + it( + 'should call onFailure function when' + + 'updateQuestionSuggestionAsync fails', + fakeAsync(() => { + spyOn(carbas, 'updateQuestionSuggestionAsync').and.returnValue( + Promise.reject() + ); - cars.updateQuestionSuggestionAsync( - 'pqr', 2, questionStateData, 10, imagesData, onSuccess, onFailure); - tick(); + cars.updateQuestionSuggestionAsync( + 'pqr', + 2, + questionStateData, + 10, + imagesData, + onSuccess, + onFailure + ); + tick(); - expect(carbas.updateQuestionSuggestionAsync) - .toHaveBeenCalledWith('pqr', requestBody); - expect(onSuccess).not.toHaveBeenCalled(); - expect(onFailure).toHaveBeenCalledWith('pqr'); - })); + expect(carbas.updateQuestionSuggestionAsync).toHaveBeenCalledWith( + 'pqr', + requestBody + ); + expect(onSuccess).not.toHaveBeenCalled(); + expect(onFailure).toHaveBeenCalledWith('pqr'); + }) + ); }); describe('sortTranslationSuggestionsByState', () => { @@ -930,12 +1058,12 @@ describe('Contribution and review service', () => { state_name: 'First State', content_id: 'feedback_2', new_value: { - html: 'new_val' + html: 'new_val', }, old_value: { - html: 'old_val' + html: 'old_val', }, - skill_id: 'skill_id_1' + skill_id: 'skill_id_1', }, last_updated_msecs: 0, }, @@ -950,12 +1078,12 @@ describe('Contribution and review service', () => { state_name: 'End State', content_id: 'hints_1', new_value: { - html: 'new_val' + html: 'new_val', }, old_value: { - html: 'old_val' + html: 'old_val', }, - skill_id: 'skill_id_1' + skill_id: 'skill_id_1', }, last_updated_msecs: 0, }, @@ -970,12 +1098,12 @@ describe('Contribution and review service', () => { state_name: 'First State', content_id: 'content_3', new_value: { - html: 'new_val' + html: 'new_val', }, old_value: { - html: 'old_val' + html: 'old_val', }, - skill_id: 'skill_id_1' + skill_id: 'skill_id_1', }, last_updated_msecs: 0, }, @@ -990,12 +1118,12 @@ describe('Contribution and review service', () => { state_name: 'End State', content_id: 'interaction_1', new_value: { - html: 'new_val' + html: 'new_val', }, old_value: { - html: 'old_val' + html: 'old_val', }, - skill_id: 'skill_id_1' + skill_id: 'skill_id_1', }, last_updated_msecs: 0, }, @@ -1010,12 +1138,12 @@ describe('Contribution and review service', () => { state_name: 'First State', content_id: 'content_1', new_value: { - html: 'new_val' + html: 'new_val', }, old_value: { - html: 'old_val' + html: 'old_val', }, - skill_id: 'skill_id_1' + skill_id: 'skill_id_1', }, last_updated_msecs: 0, }, @@ -1030,231 +1158,238 @@ describe('Contribution and review service', () => { state_name: 'End State', content_id: 'content_2', new_value: { - html: 'new_val' + html: 'new_val', }, old_value: { - html: 'old_val' + html: 'old_val', }, - skill_id: 'skill_id_1' + skill_id: 'skill_id_1', }, last_updated_msecs: 0, - } + }, ] as unknown as SuggestionBackendDict[]; const statesBackendDict: StateObjectsBackendDict = { 'First State': { classifier_model_id: null, content: { content_id: 'content', - html: '' + html: '', }, recorded_voiceovers: { voiceovers_mapping: { content: {}, - default_outcome: {} - } + default_outcome: {}, + }, }, interaction: { answer_groups: [], confirmed_unclassified_answers: [], customization_args: { buttonText: { - value: 'Continue' - } + value: 'Continue', + }, }, default_outcome: { dest: 'End State', dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: '' + html: '', }, param_changes: [], labelled_as_correct: true, refresher_exploration_id: null, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, hints: [], solution: null, - id: 'Continue' + id: 'Continue', }, linked_skill_id: null, param_changes: [], solicit_answer_details: false, - card_is_checkpoint: true + card_is_checkpoint: true, }, 'End State': { classifier_model_id: null, content: { content_id: 'content', - html: '' + html: '', }, recorded_voiceovers: { voiceovers_mapping: { content: {}, - default_outcome: {} - } + default_outcome: {}, + }, }, interaction: { answer_groups: [], confirmed_unclassified_answers: [], customization_args: { recommendedExplorationIds: { - value: [] - } + value: [], + }, }, default_outcome: null, hints: [], solution: null, - id: 'EndExploration' + id: 'EndExploration', }, linked_skill_id: null, param_changes: [], solicit_answer_details: false, - card_is_checkpoint: false - } + card_is_checkpoint: false, + }, }; - it('should sort translation cards within each state based ' + - 'on type and index', () => { - const states = statesObjectFactory.createFromBackendDict( - statesBackendDict); - const sortedTranslationSuggestions = cars. - sortTranslationSuggestionsByState( - translationSuggestions, states, - 'First State') as unknown as SuggestionBackendDict[]; - - expect(sortedTranslationSuggestions).toEqual([ - { - suggestion_type: 'suggestion', - suggestion_id: 'id', - target_type: 'exploration', - target_id: '1', - status: 'review', - author_name: 'author', - change_cmd: { - state_name: 'First State', - content_id: 'content_1', - new_value: { - html: 'new_val' - }, - old_value: { - html: 'old_val' + it( + 'should sort translation cards within each state based ' + + 'on type and index', + () => { + const states = + statesObjectFactory.createFromBackendDict(statesBackendDict); + const sortedTranslationSuggestions = + cars.sortTranslationSuggestionsByState( + translationSuggestions, + states, + 'First State' + ) as unknown as SuggestionBackendDict[]; + + expect(sortedTranslationSuggestions).toEqual([ + { + suggestion_type: 'suggestion', + suggestion_id: 'id', + target_type: 'exploration', + target_id: '1', + status: 'review', + author_name: 'author', + change_cmd: { + state_name: 'First State', + content_id: 'content_1', + new_value: { + html: 'new_val', + }, + old_value: { + html: 'old_val', + }, + skill_id: 'skill_id_1', }, - skill_id: 'skill_id_1' + last_updated_msecs: 0, }, - last_updated_msecs: 0, - }, - { - suggestion_type: 'suggestion', - suggestion_id: 'id', - target_type: 'exploration', - target_id: '1', - status: 'review', - author_name: 'author', - change_cmd: { - state_name: 'First State', - content_id: 'content_3', - new_value: { - html: 'new_val' - }, - old_value: { - html: 'old_val' + { + suggestion_type: 'suggestion', + suggestion_id: 'id', + target_type: 'exploration', + target_id: '1', + status: 'review', + author_name: 'author', + change_cmd: { + state_name: 'First State', + content_id: 'content_3', + new_value: { + html: 'new_val', + }, + old_value: { + html: 'old_val', + }, + skill_id: 'skill_id_1', }, - skill_id: 'skill_id_1' + last_updated_msecs: 0, }, - last_updated_msecs: 0, - }, - { - suggestion_type: 'suggestion', - suggestion_id: 'id', - target_type: 'exploration', - target_id: '1', - status: 'review', - author_name: 'author', - change_cmd: { - state_name: 'First State', - content_id: 'feedback_2', - new_value: { - html: 'new_val' - }, - old_value: { - html: 'old_val' + { + suggestion_type: 'suggestion', + suggestion_id: 'id', + target_type: 'exploration', + target_id: '1', + status: 'review', + author_name: 'author', + change_cmd: { + state_name: 'First State', + content_id: 'feedback_2', + new_value: { + html: 'new_val', + }, + old_value: { + html: 'old_val', + }, + skill_id: 'skill_id_1', }, - skill_id: 'skill_id_1' + last_updated_msecs: 0, }, - last_updated_msecs: 0, - }, - { - suggestion_type: 'suggestion', - suggestion_id: 'id', - target_type: 'exploration', - target_id: '1', - status: 'review', - author_name: 'author', - change_cmd: { - state_name: 'End State', - content_id: 'content_2', - new_value: { - html: 'new_val' - }, - old_value: { - html: 'old_val' + { + suggestion_type: 'suggestion', + suggestion_id: 'id', + target_type: 'exploration', + target_id: '1', + status: 'review', + author_name: 'author', + change_cmd: { + state_name: 'End State', + content_id: 'content_2', + new_value: { + html: 'new_val', + }, + old_value: { + html: 'old_val', + }, + skill_id: 'skill_id_1', }, - skill_id: 'skill_id_1' + last_updated_msecs: 0, }, - last_updated_msecs: 0, - }, - { - suggestion_type: 'suggestion', - suggestion_id: 'id', - target_type: 'exploration', - target_id: '1', - status: 'review', - author_name: 'author', - change_cmd: { - state_name: 'End State', - content_id: 'interaction_1', - new_value: { - html: 'new_val' - }, - old_value: { - html: 'old_val' + { + suggestion_type: 'suggestion', + suggestion_id: 'id', + target_type: 'exploration', + target_id: '1', + status: 'review', + author_name: 'author', + change_cmd: { + state_name: 'End State', + content_id: 'interaction_1', + new_value: { + html: 'new_val', + }, + old_value: { + html: 'old_val', + }, + skill_id: 'skill_id_1', }, - skill_id: 'skill_id_1' + last_updated_msecs: 0, }, - last_updated_msecs: 0, - }, - { - suggestion_type: 'suggestion', - suggestion_id: 'id', - target_type: 'exploration', - target_id: '1', - status: 'review', - author_name: 'author', - change_cmd: { - state_name: 'End State', - content_id: 'hints_1', - new_value: { - html: 'new_val' - }, - old_value: { - html: 'old_val' + { + suggestion_type: 'suggestion', + suggestion_id: 'id', + target_type: 'exploration', + target_id: '1', + status: 'review', + author_name: 'author', + change_cmd: { + state_name: 'End State', + content_id: 'hints_1', + new_value: { + html: 'new_val', + }, + old_value: { + html: 'old_val', + }, + skill_id: 'skill_id_1', }, - skill_id: 'skill_id_1' + last_updated_msecs: 0, }, - last_updated_msecs: 0, - } - ]); - }); - - it('should return suggestions as it is when initStateName is not defined', - () => { - const states = statesObjectFactory.createFromBackendDict( - statesBackendDict); - const sortedTranslationCards = cars.sortTranslationSuggestionsByState( - translationSuggestions, states, null); + ]); + } + ); + + it('should return suggestions as it is when initStateName is not defined', () => { + const states = + statesObjectFactory.createFromBackendDict(statesBackendDict); + const sortedTranslationCards = cars.sortTranslationSuggestionsByState( + translationSuggestions, + states, + null + ); - expect(sortedTranslationCards).toEqual(translationSuggestions); - }); + expect(sortedTranslationCards).toEqual(translationSuggestions); + }); }); }); diff --git a/core/templates/pages/contributor-dashboard-page/services/contribution-and-review.service.ts b/core/templates/pages/contributor-dashboard-page/services/contribution-and-review.service.ts index 99f235e91295..37a1e11ff70c 100644 --- a/core/templates/pages/contributor-dashboard-page/services/contribution-and-review.service.ts +++ b/core/templates/pages/contributor-dashboard-page/services/contribution-and-review.service.ts @@ -16,24 +16,27 @@ * @fileoverview Service for fetching and resolving suggestions. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { ContributionAndReviewBackendApiService, ContributorCertificateResponse } - from './contribution-and-review-backend-api.service'; -import { SuggestionBackendDict } from 'domain/suggestion/suggestion.model'; -import { StateBackendDict } from 'domain/state/StateObjectFactory'; -import { ImagesData } from 'services/image-local-storage.service'; -import { ReadOnlyExplorationBackendApiService } - from 'domain/exploration/read-only-exploration-backend-api.service'; -import { ComputeGraphService } from 'services/compute-graph.service'; -import { States } from 'domain/exploration/StatesObjectFactory'; -import { ExplorationObjectFactory, Exploration} - from 'domain/exploration/ExplorationObjectFactory'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {AppConstants} from 'app.constants'; +import { + ContributionAndReviewBackendApiService, + ContributorCertificateResponse, +} from './contribution-and-review-backend-api.service'; +import {SuggestionBackendDict} from 'domain/suggestion/suggestion.model'; +import {StateBackendDict} from 'domain/state/StateObjectFactory'; +import {ImagesData} from 'services/image-local-storage.service'; +import {ReadOnlyExplorationBackendApiService} from 'domain/exploration/read-only-exploration-backend-api.service'; +import {ComputeGraphService} from 'services/compute-graph.service'; +import {States} from 'domain/exploration/StatesObjectFactory'; +import { + ExplorationObjectFactory, + Exploration, +} from 'domain/exploration/ExplorationObjectFactory'; export interface OpportunityDict { - 'skill_id': string; - 'skill_description': string; + skill_id: string; + skill_description: string; } // Encapsulates the state necessary to fetch a particular suggestion from the @@ -82,14 +85,10 @@ export class ContributionAndReviewService { private activeSuggestionType!: string; constructor( - private contributionAndReviewBackendApiService: - ContributionAndReviewBackendApiService, - private readOnlyExplorationBackendApiService: - ReadOnlyExplorationBackendApiService, - private computeGraphService: - ComputeGraphService, - private explorationObjectFactory: - ExplorationObjectFactory + private contributionAndReviewBackendApiService: ContributionAndReviewBackendApiService, + private readOnlyExplorationBackendApiService: ReadOnlyExplorationBackendApiService, + private computeGraphService: ComputeGraphService, + private explorationObjectFactory: ExplorationObjectFactory ) {} getActiveTabType(): string { @@ -108,17 +107,19 @@ export class ContributionAndReviewService { this.activeSuggestionType = activeSuggestionType; } - private userCreatedQuestionFetcher: SuggestionFetcher = ( - new SuggestionFetcher('SUBMITTED_QUESTION_SUGGESTIONS')); + private userCreatedQuestionFetcher: SuggestionFetcher = new SuggestionFetcher( + 'SUBMITTED_QUESTION_SUGGESTIONS' + ); - private reviewableQuestionFetcher: SuggestionFetcher = ( - new SuggestionFetcher('REVIEWABLE_QUESTION_SUGGESTIONS')); + private reviewableQuestionFetcher: SuggestionFetcher = new SuggestionFetcher( + 'REVIEWABLE_QUESTION_SUGGESTIONS' + ); - private userCreatedTranslationFetcher: SuggestionFetcher = ( - new SuggestionFetcher('SUBMITTED_TRANSLATION_SUGGESTIONS')); + private userCreatedTranslationFetcher: SuggestionFetcher = + new SuggestionFetcher('SUBMITTED_TRANSLATION_SUGGESTIONS'); - private reviewableTranslationFetcher: SuggestionFetcher = ( - new SuggestionFetcher('REVIEWABLE_TRANSLATION_SUGGESTIONS')); + private reviewableTranslationFetcher: SuggestionFetcher = + new SuggestionFetcher('REVIEWABLE_TRANSLATION_SUGGESTIONS'); /** * Fetches suggestions from the backend. @@ -131,10 +132,10 @@ export class ContributionAndReviewService { * @returns {Promise} */ private async fetchSuggestionsAsync( - fetcher: SuggestionFetcher, - shouldResetOffset: boolean, - explorationId: string | null, - topicName: string | null, + fetcher: SuggestionFetcher, + shouldResetOffset: boolean, + explorationId: string | null, + topicName: string | null ): Promise { if (shouldResetOffset) { // Handle the case where we need to fetch starting from the beginning. @@ -142,131 +143,140 @@ export class ContributionAndReviewService { fetcher.suggestionIdToDetails = {}; } const currentCacheSize: number = Object.keys( - fetcher.suggestionIdToDetails).length; - return ( - this.contributionAndReviewBackendApiService.fetchSuggestionsAsync( + fetcher.suggestionIdToDetails + ).length; + return this.contributionAndReviewBackendApiService + .fetchSuggestionsAsync( fetcher.type, // Fetch up to two pages at a time to compute if we have more results. // The first page of results is returned to the caller and the second // page is cached. - (AppConstants.OPPORTUNITIES_PAGE_SIZE * 2) - currentCacheSize, + AppConstants.OPPORTUNITIES_PAGE_SIZE * 2 - currentCacheSize, fetcher.offset, fetcher.sortKey, explorationId, - topicName, - ).then((responseBody) => { + topicName + ) + .then(responseBody => { const responseSuggestionIdToDetails = fetcher.suggestionIdToDetails; fetcher.suggestionIdToDetails = {}; const targetIdToDetails = responseBody.target_id_to_opportunity_dict; - responseBody.suggestions.forEach((suggestion) => { + responseBody.suggestions.forEach(suggestion => { const suggestionDetails = { suggestion: suggestion, - details: targetIdToDetails[suggestion.target_id] + details: targetIdToDetails[suggestion.target_id], }; const responseSize: number = Object.keys( - responseSuggestionIdToDetails).length; + responseSuggestionIdToDetails + ).length; if (responseSize < AppConstants.OPPORTUNITIES_PAGE_SIZE) { // Populate the response with up to a page's worth of results. - responseSuggestionIdToDetails[ - suggestion.suggestion_id] = suggestionDetails; + responseSuggestionIdToDetails[suggestion.suggestion_id] = + suggestionDetails; } else { // Cache the 2nd page. - fetcher.suggestionIdToDetails[ - suggestion.suggestion_id] = suggestionDetails; + fetcher.suggestionIdToDetails[suggestion.suggestion_id] = + suggestionDetails; } }); fetcher.offset = responseBody.next_offset; return { suggestionIdToDetails: responseSuggestionIdToDetails, - more: Object.keys(fetcher.suggestionIdToDetails).length > 0 + more: Object.keys(fetcher.suggestionIdToDetails).length > 0, }; - }) - ); + }); } async fetchTranslationSuggestionsAsync( - explorationId: string + explorationId: string ): Promise { - const explorationBackendResponse = await this. - readOnlyExplorationBackendApiService.fetchExplorationAsync( - explorationId, null); - return ( - this.contributionAndReviewBackendApiService. - fetchSuggestionsAsync( - 'REVIEWABLE_TRANSLATION_SUGGESTIONS', - null, - 0, - AppConstants.SUGGESTIONS_SORT_KEY_DATE, - explorationId, - null, - ).then((fetchSuggestionsResponse) => { - const exploration: Exploration = this.explorationObjectFactory. - createFromExplorationBackendResponse( - explorationBackendResponse); - const sortedTranslationSuggestions = ( - this.sortTranslationSuggestionsByState( - fetchSuggestionsResponse.suggestions, - exploration.getStates(), - exploration.initStateName)); - const responseSuggestionIdToDetails: SuggestionDetailsDict = {}; - sortedTranslationSuggestions.forEach((suggestion) => { - const suggestionDetails = { - suggestion: suggestion, - details: ( - fetchSuggestionsResponse.target_id_to_opportunity_dict[ - suggestion.target_id]) - }; - responseSuggestionIdToDetails[ - suggestion.suggestion_id] = suggestionDetails; - }); - return { - suggestionIdToDetails: responseSuggestionIdToDetails, - more: false + const explorationBackendResponse = + await this.readOnlyExplorationBackendApiService.fetchExplorationAsync( + explorationId, + null + ); + return this.contributionAndReviewBackendApiService + .fetchSuggestionsAsync( + 'REVIEWABLE_TRANSLATION_SUGGESTIONS', + null, + 0, + AppConstants.SUGGESTIONS_SORT_KEY_DATE, + explorationId, + null + ) + .then(fetchSuggestionsResponse => { + const exploration: Exploration = + this.explorationObjectFactory.createFromExplorationBackendResponse( + explorationBackendResponse + ); + const sortedTranslationSuggestions = + this.sortTranslationSuggestionsByState( + fetchSuggestionsResponse.suggestions, + exploration.getStates(), + exploration.initStateName + ); + const responseSuggestionIdToDetails: SuggestionDetailsDict = {}; + sortedTranslationSuggestions.forEach(suggestion => { + const suggestionDetails = { + suggestion: suggestion, + details: + fetchSuggestionsResponse.target_id_to_opportunity_dict[ + suggestion.target_id + ], }; - }) - ); + responseSuggestionIdToDetails[suggestion.suggestion_id] = + suggestionDetails; + }); + return { + suggestionIdToDetails: responseSuggestionIdToDetails, + more: false, + }; + }); } sortTranslationSuggestionsByState( - translationSuggestions: SuggestionBackendDict[], - states: States, - initStateName: string | null + translationSuggestions: SuggestionBackendDict[], + states: States, + initStateName: string | null ): SuggestionBackendDict[] { if (!initStateName) { return translationSuggestions; } - const stateNamesInOrder = this.computeGraphService. - computeBfsTraversalOfStates( + const stateNamesInOrder = + this.computeGraphService.computeBfsTraversalOfStates( initStateName, states, initStateName ); - const translationSuggestionsByState = ( - ContributionAndReviewService - .groupTranslationSuggestionsByState(translationSuggestions)); + const translationSuggestionsByState = + ContributionAndReviewService.groupTranslationSuggestionsByState( + translationSuggestions + ); const sortedTranslationCards: SuggestionBackendDict[] = []; for (const stateName of stateNamesInOrder) { - const cardsForState = ( - translationSuggestionsByState.get(stateName) || []); + const cardsForState = translationSuggestionsByState.get(stateName) || []; cardsForState.sort( - ContributionAndReviewService.compareTranslationSuggestions); + ContributionAndReviewService.compareTranslationSuggestions + ); sortedTranslationCards.push(...cardsForState); } return sortedTranslationCards; } private static groupTranslationSuggestionsByState( - translationSuggestions: SuggestionBackendDict[]): - Map { + translationSuggestions: SuggestionBackendDict[] + ): Map { const translationSuggestionsByState = new Map< - string, SuggestionBackendDict[]>(); + string, + SuggestionBackendDict[] + >(); for (const translationSuggestion of translationSuggestions) { const stateName = translationSuggestion.change_cmd.state_name; - const suggestionsForState = translationSuggestionsByState.get( - stateName) || []; + const suggestionsForState = + translationSuggestionsByState.get(stateName) || []; suggestionsForState.push(translationSuggestion); translationSuggestionsByState.set(stateName, suggestionsForState); } @@ -275,21 +285,29 @@ export class ContributionAndReviewService { // Compares translation suggestions based on type and index. private static compareTranslationSuggestions( - cardA: SuggestionBackendDict, - cardB: SuggestionBackendDict + cardA: SuggestionBackendDict, + cardB: SuggestionBackendDict ): number { - const cardATypeOrder = ContributionAndReviewService. - getTranslationContentTypeOrder(cardA.change_cmd.content_id); - const cardBTypeOrder = ContributionAndReviewService - .getTranslationContentTypeOrder(cardB.change_cmd.content_id); + const cardATypeOrder = + ContributionAndReviewService.getTranslationContentTypeOrder( + cardA.change_cmd.content_id + ); + const cardBTypeOrder = + ContributionAndReviewService.getTranslationContentTypeOrder( + cardB.change_cmd.content_id + ); if (cardATypeOrder !== cardBTypeOrder) { return cardATypeOrder - cardBTypeOrder; } else { - const cardAIndex = ContributionAndReviewService - .getTranslationContentIndex(cardA.change_cmd.content_id); - const cardBIndex = ContributionAndReviewService - .getTranslationContentIndex(cardB.change_cmd.content_id); + const cardAIndex = + ContributionAndReviewService.getTranslationContentIndex( + cardA.change_cmd.content_id + ); + const cardBIndex = + ContributionAndReviewService.getTranslationContentIndex( + cardB.change_cmd.content_id + ); return cardAIndex - cardBIndex; } } @@ -302,7 +320,7 @@ export class ContributionAndReviewService { ['feedback', 2], ['default', 3], ['hints', 4], - ['solution', 5] + ['solution', 5], ]); const type = contentId.split('_')[0]; return contentOrders.get(type) ?? Number.MAX_SAFE_INTEGER; @@ -315,124 +333,149 @@ export class ContributionAndReviewService { } async getUserCreatedQuestionSuggestionsAsync( - shouldResetOffset: boolean = true, - sortKey: string + shouldResetOffset: boolean = true, + sortKey: string ): Promise { this.userCreatedQuestionFetcher.sortKey = sortKey; return this.fetchSuggestionsAsync( this.userCreatedQuestionFetcher, - shouldResetOffset, null, null); + shouldResetOffset, + null, + null + ); } async getReviewableQuestionSuggestionsAsync( - shouldResetOffset: boolean = true, - sortKey: string, - topicName: string | null, + shouldResetOffset: boolean = true, + sortKey: string, + topicName: string | null ): Promise { this.reviewableQuestionFetcher.sortKey = sortKey; return this.fetchSuggestionsAsync( this.reviewableQuestionFetcher, - shouldResetOffset, null, - topicName); + shouldResetOffset, + null, + topicName + ); } async getUserCreatedTranslationSuggestionsAsync( - shouldResetOffset: boolean = true, - sortKey: string + shouldResetOffset: boolean = true, + sortKey: string ): Promise { this.userCreatedTranslationFetcher.sortKey = sortKey; return this.fetchSuggestionsAsync( this.userCreatedTranslationFetcher, - shouldResetOffset, null, null); + shouldResetOffset, + null, + null + ); } async getReviewableTranslationSuggestionsAsync( - shouldResetOffset: boolean = true, - sortKey: string, - explorationId?: string + shouldResetOffset: boolean = true, + sortKey: string, + explorationId?: string ): Promise { this.reviewableTranslationFetcher.sortKey = sortKey; if (explorationId) { - return this.fetchTranslationSuggestionsAsync( - explorationId); + return this.fetchTranslationSuggestionsAsync(explorationId); } return this.fetchSuggestionsAsync( this.reviewableTranslationFetcher, - shouldResetOffset, null, null); + shouldResetOffset, + null, + null + ); } reviewExplorationSuggestion( - targetId: string, suggestionId: string, action: string, - reviewMessage: string, commitMessage: string | null, - onSuccess: (suggestionId: string) => void, - onFailure: (errorMessage: string) => void + targetId: string, + suggestionId: string, + action: string, + reviewMessage: string, + commitMessage: string | null, + onSuccess: (suggestionId: string) => void, + onFailure: (errorMessage: string) => void ): Promise { const requestBody = { action: action, review_message: reviewMessage, - commit_message: commitMessage + commit_message: commitMessage, }; return this.contributionAndReviewBackendApiService - .reviewExplorationSuggestionAsync( - targetId, suggestionId, requestBody - ).then(() => { - onSuccess(suggestionId); - }, (errorResponse) => { - onFailure && onFailure(errorResponse.error.error); - }); + .reviewExplorationSuggestionAsync(targetId, suggestionId, requestBody) + .then( + () => { + onSuccess(suggestionId); + }, + errorResponse => { + onFailure && onFailure(errorResponse.error.error); + } + ); } reviewSkillSuggestion( - targetId: string, suggestionId: string, action: string, - reviewMessage: string, skillDifficulty: string, - onSuccess: (suggestionId: string) => void, - onFailure: () => void + targetId: string, + suggestionId: string, + action: string, + reviewMessage: string, + skillDifficulty: string, + onSuccess: (suggestionId: string) => void, + onFailure: () => void ): Promise { const requestBody = { action: action, review_message: reviewMessage, - skill_difficulty: skillDifficulty + skill_difficulty: skillDifficulty, }; return this.contributionAndReviewBackendApiService - .reviewSkillSuggestionAsync( - targetId, suggestionId, requestBody - ).then(() => { - onSuccess(suggestionId); - }, () => { - onFailure && onFailure(); - }); + .reviewSkillSuggestionAsync(targetId, suggestionId, requestBody) + .then( + () => { + onSuccess(suggestionId); + }, + () => { + onFailure && onFailure(); + } + ); } async updateTranslationSuggestionAsync( - suggestionId: string, translationHtml: string, - onSuccess: () => void, - onFailure: (error: Error) => void + suggestionId: string, + translationHtml: string, + onSuccess: () => void, + onFailure: (error: Error) => void ): Promise { const requestBody = { - translation_html: translationHtml + translation_html: translationHtml, }; return this.contributionAndReviewBackendApiService - .updateTranslationSuggestionAsync( - suggestionId, requestBody - ).then(() => { - onSuccess(); - }, (error) => onFailure && onFailure(error)); + .updateTranslationSuggestionAsync(suggestionId, requestBody) + .then( + () => { + onSuccess(); + }, + error => onFailure && onFailure(error) + ); } async updateQuestionSuggestionAsync( - suggestionId: string, skillDifficulty: number, - questionStateData: StateBackendDict, nextContentIdIndex: number, - imagesData: ImagesData[], - onSuccess: (suggestionId: string) => void, - onFailure: (suggestionId: string) => void + suggestionId: string, + skillDifficulty: number, + questionStateData: StateBackendDict, + nextContentIdIndex: number, + imagesData: ImagesData[], + onSuccess: (suggestionId: string) => void, + onFailure: (suggestionId: string) => void ): Promise { const payload = { skill_difficulty: skillDifficulty, question_state_data: questionStateData, - next_content_id_index: nextContentIdIndex + next_content_id_index: nextContentIdIndex, }; const requestBody = new FormData(); requestBody.append('payload', JSON.stringify(payload)); @@ -443,25 +486,35 @@ export class ContributionAndReviewService { }); return this.contributionAndReviewBackendApiService - .updateQuestionSuggestionAsync( - suggestionId, requestBody - ).then(() => { - onSuccess(suggestionId); - }, () => onFailure && onFailure(suggestionId)); + .updateQuestionSuggestionAsync(suggestionId, requestBody) + .then( + () => { + onSuccess(suggestionId); + }, + () => onFailure && onFailure(suggestionId) + ); } async downloadContributorCertificateAsync( - username: string, - suggestionType: string, - languageCode: string | null, - fromDate: string, - toDate: string + username: string, + suggestionType: string, + languageCode: string | null, + fromDate: string, + toDate: string ): Promise { - return this.contributionAndReviewBackendApiService - .downloadContributorCertificateAsync( - username, suggestionType, languageCode, fromDate, toDate); + return this.contributionAndReviewBackendApiService.downloadContributorCertificateAsync( + username, + suggestionType, + languageCode, + fromDate, + toDate + ); } } -angular.module('oppia').factory('ContributionAndReviewService', - downgradeInjectable(ContributionAndReviewService)); +angular + .module('oppia') + .factory( + 'ContributionAndReviewService', + downgradeInjectable(ContributionAndReviewService) + ); diff --git a/core/templates/pages/contributor-dashboard-page/services/contribution-opportunities-backend-api.service.spec.ts b/core/templates/pages/contributor-dashboard-page/services/contribution-opportunities-backend-api.service.spec.ts index 37d6b302fc00..5df1b5e618ee 100644 --- a/core/templates/pages/contributor-dashboard-page/services/contribution-opportunities-backend-api.service.spec.ts +++ b/core/templates/pages/contributor-dashboard-page/services/contribution-opportunities-backend-api.service.spec.ts @@ -16,57 +16,61 @@ * @fileoverview Unit tests for ContributionOpportunitiesBackendApiService. */ -import { HttpClientTestingModule, HttpTestingController } from - '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks, tick } from '@angular/core/testing'; - -import { ContributionOpportunitiesBackendApiService } from +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks, tick} from '@angular/core/testing'; + +import { + ContributionOpportunitiesBackendApiService, // eslint-disable-next-line max-len - 'pages/contributor-dashboard-page/services/contribution-opportunities-backend-api.service'; -import { SkillOpportunity } from - 'domain/opportunity/skill-opportunity.model'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; -import { UserInfo } from 'domain/user/user-info.model'; -import { UserService } from 'services/user.service'; -import { FeaturedTranslationLanguage } from 'domain/opportunity/featured-translation-language.model'; -import { ExplorationOpportunitySummary } from 'domain/opportunity/exploration-opportunity-summary.model'; - -describe('Contribution Opportunities backend API service', function() { - let contributionOpportunitiesBackendApiService: - ContributionOpportunitiesBackendApiService; +} from 'pages/contributor-dashboard-page/services/contribution-opportunities-backend-api.service'; +import {SkillOpportunity} from 'domain/opportunity/skill-opportunity.model'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {UserInfo} from 'domain/user/user-info.model'; +import {UserService} from 'services/user.service'; +import {FeaturedTranslationLanguage} from 'domain/opportunity/featured-translation-language.model'; +import {ExplorationOpportunitySummary} from 'domain/opportunity/exploration-opportunity-summary.model'; + +describe('Contribution Opportunities backend API service', function () { + let contributionOpportunitiesBackendApiService: ContributionOpportunitiesBackendApiService; let httpTestingController: HttpTestingController; let urlInterpolationService: UrlInterpolationService; let userService: UserService; const skillOpportunityResponse = { - opportunities: [{ - id: 'skill_id', - skill_description: 'A new skill for question', - topic_name: 'A new topic', - question_count: 30 - }], + opportunities: [ + { + id: 'skill_id', + skill_description: 'A new skill for question', + topic_name: 'A new topic', + question_count: 30, + }, + ], next_cursor: '6', - more: true + more: true, }; - const translationOpportunities = [{ - id: 'exp_id', - topic_name: 'Topic', - story_title: 'A new story', - chapter_title: 'Introduction', - content_count: 100, - translation_counts: { - hi: 15 - }, - translation_in_review_counts: { - hi: 15 + const translationOpportunities = [ + { + id: 'exp_id', + topic_name: 'Topic', + story_title: 'A new story', + chapter_title: 'Introduction', + content_count: 100, + translation_counts: { + hi: 15, + }, + translation_in_review_counts: { + hi: 15, + }, + language_code: 'hi', + is_pinned: true, }, - language_code: 'hi', - is_pinned: true - }]; + ]; const translationOpportunityResponse = { opportunities: translationOpportunities, next_cursor: '6', - more: true + more: true, }; const userInfoDict = [ { @@ -79,7 +83,7 @@ describe('Contribution Opportunities backend API service', function() { preferred_site_language_code: 'en', username: 'user', email: 'user@example.com', - user_is_logged_in: true + user_is_logged_in: true, }, { roles: ['USER_ROLE'], @@ -91,8 +95,8 @@ describe('Contribution Opportunities backend API service', function() { preferred_site_language_code: '', username: 'guest', email: '', - user_is_logged_in: false - } + user_is_logged_in: false, + }, ]; let userInfo: UserInfo[]; let sampleSkillOpportunitiesResponse: SkillOpportunity[]; @@ -100,25 +104,27 @@ describe('Contribution Opportunities backend API service', function() { beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] + imports: [HttpClientTestingModule], }); - contributionOpportunitiesBackendApiService = - TestBed.get(ContributionOpportunitiesBackendApiService); + contributionOpportunitiesBackendApiService = TestBed.get( + ContributionOpportunitiesBackendApiService + ); httpTestingController = TestBed.get(HttpTestingController); urlInterpolationService = TestBed.get(UrlInterpolationService); userService = TestBed.get(UserService); userInfo = [ UserInfo.createFromBackendDict(userInfoDict[0]), - UserInfo.createFromBackendDict(userInfoDict[1]) + UserInfo.createFromBackendDict(userInfoDict[1]), ]; sampleSkillOpportunitiesResponse = [ SkillOpportunity.createFromBackendDict( - skillOpportunityResponse.opportunities[0]) + skillOpportunityResponse.opportunities[0] + ), ]; sampleTranslationOpportunitiesResponse = [ ExplorationOpportunitySummary.createFromBackendDict( translationOpportunityResponse.opportunities[0] - ) + ), ]; }); @@ -126,249 +132,297 @@ describe('Contribution Opportunities backend API service', function() { httpTestingController.verify(); }); - it('should successfully fetch the skill opportunities data', + it('should successfully fetch the skill opportunities data', fakeAsync(() => { + const successHandler = jasmine.createSpy('success'); + const failHandler = jasmine.createSpy('fail'); + + contributionOpportunitiesBackendApiService + .fetchSkillOpportunitiesAsync('') + .then(successHandler, failHandler); + const req = httpTestingController.expectOne( + urlInterpolationService.interpolateUrl( + '/opportunitiessummaryhandler/', + {opportunityType: 'skill'} + ) + '?cursor=' + ); + expect(req.request.method).toEqual('GET'); + req.flush(skillOpportunityResponse); + + flushMicrotasks(); + + expect(successHandler).toHaveBeenCalledWith({ + opportunities: sampleSkillOpportunitiesResponse, + nextCursor: skillOpportunityResponse.next_cursor, + more: skillOpportunityResponse.more, + }); + expect(failHandler).not.toHaveBeenCalled(); + })); + + it( + 'should fail to fetch the skill opportunities data ' + + 'given invalid cursor ' + + "when calling 'fetchSkillOpportunitiesAsync'", fakeAsync(() => { const successHandler = jasmine.createSpy('success'); const failHandler = jasmine.createSpy('fail'); - contributionOpportunitiesBackendApiService.fetchSkillOpportunitiesAsync( - '').then( - successHandler, failHandler - ); + contributionOpportunitiesBackendApiService + .fetchSkillOpportunitiesAsync('invalidCursor') + .then(successHandler, failHandler); const req = httpTestingController.expectOne( urlInterpolationService.interpolateUrl( '/opportunitiessummaryhandler/', - { opportunityType: 'skill' } - ) + '?cursor=' + {opportunityType: 'skill'} + ) + '?cursor=invalidCursor' ); + expect(req.request.method).toEqual('GET'); - req.flush(skillOpportunityResponse); + req.flush( + { + error: 'Failed to fetch skill opportunities data.', + }, + { + status: 500, + statusText: 'Internal Server Error', + } + ); flushMicrotasks(); - expect(successHandler).toHaveBeenCalledWith({ - opportunities: sampleSkillOpportunitiesResponse, - nextCursor: skillOpportunityResponse.next_cursor, - more: skillOpportunityResponse.more - }); - expect(failHandler).not.toHaveBeenCalled(); + expect(successHandler).not.toHaveBeenCalled(); + expect(failHandler).toHaveBeenCalledWith( + new Error('Failed to fetch skill opportunities data.') + ); }) ); - it('should fail to fetch the skill opportunities data ' + - 'given invalid cursor ' + - 'when calling \'fetchSkillOpportunitiesAsync\'', fakeAsync(() => { + it('should successfully fetch the translation opportunities data', fakeAsync(() => { const successHandler = jasmine.createSpy('success'); const failHandler = jasmine.createSpy('fail'); - contributionOpportunitiesBackendApiService.fetchSkillOpportunitiesAsync( - 'invalidCursor').then(successHandler, failHandler); + contributionOpportunitiesBackendApiService + .fetchTranslationOpportunitiesAsync('hi', 'All', '') + .then(successHandler, failHandler); const req = httpTestingController.expectOne( urlInterpolationService.interpolateUrl( '/opportunitiessummaryhandler/', - { opportunityType: 'skill' } - ) + '?cursor=invalidCursor' + {opportunityType: 'translation'} + ) + '?language_code=hi&topic_name=&cursor=' ); - expect(req.request.method).toEqual('GET'); - req.flush({ - error: 'Failed to fetch skill opportunities data.' - }, { - status: 500, statusText: 'Internal Server Error' - }); + req.flush(translationOpportunityResponse); flushMicrotasks(); - expect(successHandler).not.toHaveBeenCalled(); - expect(failHandler).toHaveBeenCalledWith( - new Error('Failed to fetch skill opportunities data.')); + expect(successHandler).toHaveBeenCalledWith({ + opportunities: sampleTranslationOpportunitiesResponse, + nextCursor: translationOpportunityResponse.next_cursor, + more: translationOpportunityResponse.more, + }); + expect(failHandler).not.toHaveBeenCalled(); })); - it('should successfully fetch the translation opportunities data', + it( + 'should fail to fetch the translation opportunities data ' + + 'given invalid language code ' + + "when calling 'fetchTranslationOpportunitiesAsync'", fakeAsync(() => { const successHandler = jasmine.createSpy('success'); const failHandler = jasmine.createSpy('fail'); contributionOpportunitiesBackendApiService - .fetchTranslationOpportunitiesAsync('hi', 'All', '').then( - successHandler, failHandler - ); + .fetchTranslationOpportunitiesAsync( + 'invlaidCode', + 'Topic', + 'invalidCursor' + ) + .then(successHandler, failHandler); const req = httpTestingController.expectOne( urlInterpolationService.interpolateUrl( '/opportunitiessummaryhandler/', - { opportunityType: 'translation' } - ) + '?language_code=hi&topic_name=&cursor=' + {opportunityType: 'translation'} + ) + '?language_code=invlaidCode&topic_name=Topic&cursor=invalidCursor' ); - expect(req.request.method).toEqual('GET'); - req.flush(translationOpportunityResponse); + expect(req.request.method).toEqual('GET'); + req.flush( + { + error: 'Failed to fetch translation opportunities data.', + }, + { + status: 500, + statusText: 'Internal Server Error', + } + ); flushMicrotasks(); - expect(successHandler).toHaveBeenCalledWith({ - opportunities: sampleTranslationOpportunitiesResponse, - nextCursor: translationOpportunityResponse.next_cursor, - more: translationOpportunityResponse.more - }); - expect(failHandler).not.toHaveBeenCalled(); + expect(successHandler).not.toHaveBeenCalled(); + expect(failHandler).toHaveBeenCalledWith( + new Error('Failed to fetch translation opportunities data.') + ); }) ); - it('should fail to fetch the translation opportunities data ' + - 'given invalid language code ' + - 'when calling \'fetchTranslationOpportunitiesAsync\'', fakeAsync(() => { + it('should successfully fetch reviewable translation opportunities', fakeAsync(() => { const successHandler = jasmine.createSpy('success'); const failHandler = jasmine.createSpy('fail'); contributionOpportunitiesBackendApiService - .fetchTranslationOpportunitiesAsync( - 'invlaidCode', 'Topic', 'invalidCursor').then( - successHandler, failHandler - ); + .fetchReviewableTranslationOpportunitiesAsync('All') + .then(successHandler, failHandler); const req = httpTestingController.expectOne( urlInterpolationService.interpolateUrl( - '/opportunitiessummaryhandler/', - { opportunityType: 'translation' } - ) + '?language_code=invlaidCode&topic_name=Topic&cursor=invalidCursor' + '/getreviewableopportunitieshandler', + {} + ) ); - expect(req.request.method).toEqual('GET'); - req.flush({ - error: 'Failed to fetch translation opportunities data.' - }, { - status: 500, statusText: 'Internal Server Error' - }); + req.flush({opportunities: translationOpportunities}); + flushMicrotasks(); - expect(successHandler).not.toHaveBeenCalled(); - expect(failHandler).toHaveBeenCalledWith( - new Error('Failed to fetch translation opportunities data.')); + expect(successHandler).toHaveBeenCalledWith({ + opportunities: sampleTranslationOpportunitiesResponse, + }); + expect(failHandler).not.toHaveBeenCalled(); })); - it('should successfully fetch reviewable translation opportunities', + it( + 'should successfully pin reviewable pinned translation' + ' opportunities', fakeAsync(() => { const successHandler = jasmine.createSpy('success'); const failHandler = jasmine.createSpy('fail'); contributionOpportunitiesBackendApiService - .fetchReviewableTranslationOpportunitiesAsync('All').then( - successHandler, failHandler - ); - const req = httpTestingController.expectOne( - urlInterpolationService.interpolateUrl( - '/getreviewableopportunitieshandler', - {} - ) - ); - expect(req.request.method).toEqual('GET'); - req.flush({opportunities: translationOpportunities}); + .pinTranslationOpportunity('en', 'Topic 1', 'exp 1') + .then(successHandler, failHandler); + + const req = httpTestingController.expectOne('/pinned-opportunities'); + expect(req.request.method).toEqual('PUT'); + req.flush({}); flushMicrotasks(); - expect(successHandler).toHaveBeenCalledWith({ - opportunities: sampleTranslationOpportunitiesResponse - }); + expect(successHandler).toHaveBeenCalled(); expect(failHandler).not.toHaveBeenCalled(); }) ); - it('should successfully pin reviewable pinned translation' + - ' opportunities', fakeAsync(() => { - const successHandler = jasmine.createSpy('success'); - const failHandler = jasmine.createSpy('fail'); + it( + 'should successfully unpin reviewable pinned translation' + + ' opportunities', + fakeAsync(() => { + const successHandler = jasmine.createSpy('success'); + const failHandler = jasmine.createSpy('fail'); - contributionOpportunitiesBackendApiService - .pinTranslationOpportunity('en', 'Topic 1', 'exp 1').then( - successHandler, failHandler - ); + contributionOpportunitiesBackendApiService + .unpinTranslationOpportunity('en', 'Topic 1') + .then(successHandler, failHandler); - const req = httpTestingController.expectOne( - '/pinned-opportunities'); - expect(req.request.method).toEqual('PUT'); - req.flush({}); + const req = httpTestingController.expectOne('/pinned-opportunities'); + expect(req.request.method).toEqual('PUT'); + req.flush({}); - flushMicrotasks(); + flushMicrotasks(); - expect(successHandler).toHaveBeenCalled(); - expect(failHandler).not.toHaveBeenCalled(); - })); + expect(successHandler).toHaveBeenCalled(); + expect(failHandler).not.toHaveBeenCalled(); + }) + ); - it('should successfully unpin reviewable pinned translation' + - ' opportunities', fakeAsync(() => { + it('should fetch reviewable translation opportunities by language', fakeAsync(() => { const successHandler = jasmine.createSpy('success'); const failHandler = jasmine.createSpy('fail'); contributionOpportunitiesBackendApiService - .unpinTranslationOpportunity('en', 'Topic 1').then( - successHandler, failHandler - ); + .fetchReviewableTranslationOpportunitiesAsync('All', 'hi') + .then(successHandler, failHandler); const req = httpTestingController.expectOne( - '/pinned-opportunities'); - expect(req.request.method).toEqual('PUT'); - req.flush({}); - - flushMicrotasks(); - - expect(successHandler).toHaveBeenCalled(); - expect(failHandler).not.toHaveBeenCalled(); + urlInterpolationService.interpolateUrl( + '/getreviewableopportunitieshandler?language_code=', + { + language_code: 'hi', + } + ) + ); + expect(req.request.method).toEqual('GET'); + req.flush({opportunities: translationOpportunities}); })); - it('should fetch reviewable translation opportunities by language', + it( + 'should fail to fetch reviewable translation opportunities ' + + 'given invalid topic name when calling ' + + 'fetchReviewableTranslationOpportunitiesAsync', fakeAsync(() => { const successHandler = jasmine.createSpy('success'); const failHandler = jasmine.createSpy('fail'); contributionOpportunitiesBackendApiService - .fetchReviewableTranslationOpportunitiesAsync('All', 'hi').then( - successHandler, failHandler - ); - + .fetchReviewableTranslationOpportunitiesAsync('invalid') + .then(successHandler, failHandler); const req = httpTestingController.expectOne( urlInterpolationService.interpolateUrl( - '/getreviewableopportunitieshandler?language_code=', - { - language_code: 'hi' - } - ) + '/getreviewableopportunitieshandler', + {} + ) + '?topic_name=invalid' ); + expect(req.request.method).toEqual('GET'); - req.flush({opportunities: translationOpportunities}); - })); + req.flush( + { + error: 'Failed to fetch reviewable translation opportunities.', + }, + { + status: 500, + statusText: 'Internal Server Error', + } + ); + flushMicrotasks(); - it('should fail to fetch reviewable translation opportunities ' + - 'given invalid topic name when calling ' + - 'fetchReviewableTranslationOpportunitiesAsync', fakeAsync(() => { + expect(successHandler).not.toHaveBeenCalled(); + expect(failHandler).toHaveBeenCalledWith( + new Error('Failed to fetch reviewable translation opportunities.') + ); + }) + ); + + it('should successfully fetch the featured translation languages', fakeAsync(() => { const successHandler = jasmine.createSpy('success'); const failHandler = jasmine.createSpy('fail'); contributionOpportunitiesBackendApiService - .fetchReviewableTranslationOpportunitiesAsync('invalid').then( - successHandler, failHandler - ); + .fetchFeaturedTranslationLanguagesAsync() + .then(successHandler, failHandler); + const req = httpTestingController.expectOne( - urlInterpolationService.interpolateUrl( - '/getreviewableopportunitieshandler', - {} - ) + '?topic_name=invalid' + '/retrievefeaturedtranslationlanguages' ); - expect(req.request.method).toEqual('GET'); req.flush({ - error: 'Failed to fetch reviewable translation opportunities.' - }, { - status: 500, statusText: 'Internal Server Error' + featured_translation_languages: [ + {language_code: 'en', explanation: 'English'}, + ], }); + flushMicrotasks(); - expect(successHandler).not.toHaveBeenCalled(); - expect(failHandler).toHaveBeenCalledWith( - new Error('Failed to fetch reviewable translation opportunities.')); + expect(successHandler).toHaveBeenCalledWith([ + FeaturedTranslationLanguage.createFromBackendDict({ + language_code: 'en', + explanation: 'English', + }), + ]); + expect(failHandler).not.toHaveBeenCalled(); })); - it('should successfully fetch the featured translation languages', + it( + 'should fail to fetch the featured translation languages ' + + "when calling 'fetchFeaturedTranslationLanguagesAsync'", fakeAsync(() => { const successHandler = jasmine.createSpy('success'); const failHandler = jasmine.createSpy('fail'); + let emptyList: FeaturedTranslationLanguage[] = []; contributionOpportunitiesBackendApiService .fetchFeaturedTranslationLanguagesAsync() @@ -378,51 +432,45 @@ describe('Contribution Opportunities backend API service', function() { '/retrievefeaturedtranslationlanguages' ); expect(req.request.method).toEqual('GET'); - req.flush({ - featured_translation_languages: - [{ language_code: 'en', explanation: 'English' }] - }); + req.flush( + { + error: 'Failed to fetch featured translation languages.', + }, + { + status: 500, + statusText: 'Internal Server Error', + } + ); flushMicrotasks(); - expect(successHandler).toHaveBeenCalledWith([ - FeaturedTranslationLanguage.createFromBackendDict( - { language_code: 'en', explanation: 'English' } - ) - ]); + expect(successHandler).toHaveBeenCalledWith(emptyList); expect(failHandler).not.toHaveBeenCalled(); }) ); - it('should fail to fetch the featured translation languages ' + - 'when calling \'fetchFeaturedTranslationLanguagesAsync\'', fakeAsync(() => { + it('should successfully fetch translatable topic names', fakeAsync(() => { const successHandler = jasmine.createSpy('success'); const failHandler = jasmine.createSpy('fail'); - let emptyList: FeaturedTranslationLanguage[] = []; contributionOpportunitiesBackendApiService - .fetchFeaturedTranslationLanguagesAsync() + .fetchTranslatableTopicNamesAsync() .then(successHandler, failHandler); - const req = httpTestingController.expectOne( - '/retrievefeaturedtranslationlanguages' - ); + const req = httpTestingController.expectOne('/gettranslatabletopicnames'); expect(req.request.method).toEqual('GET'); - req.flush({ - error: 'Failed to fetch featured translation languages.' - }, { - status: 500, statusText: 'Internal Server Error' - }); + req.flush({topic_names: ['Topic 1', 'Topic 2']}); flushMicrotasks(); - expect(successHandler).toHaveBeenCalledWith(emptyList); + expect(successHandler).toHaveBeenCalledWith(['Topic 1', 'Topic 2']); expect(failHandler).not.toHaveBeenCalled(); })); - it('should successfully fetch translatable topic names', fakeAsync(() => { + it("should return empty response if 'gettranslatabletopicnames' call fails", fakeAsync(() => { const successHandler = jasmine.createSpy('success'); const failHandler = jasmine.createSpy('fail'); + const emptyResponse: string[] = []; contributionOpportunitiesBackendApiService .fetchTranslatableTopicNamesAsync() @@ -430,50 +478,59 @@ describe('Contribution Opportunities backend API service', function() { const req = httpTestingController.expectOne('/gettranslatabletopicnames'); expect(req.request.method).toEqual('GET'); - req.flush({ topic_names: ['Topic 1', 'Topic 2'] }); + req.flush( + { + error: 'Failed to fetch translatable topic names.', + }, + { + status: 500, + statusText: 'Internal Server Error', + } + ); flushMicrotasks(); - expect(successHandler).toHaveBeenCalledWith( - ['Topic 1', 'Topic 2'] - ); + expect(successHandler).toHaveBeenCalledWith(emptyResponse); expect(failHandler).not.toHaveBeenCalled(); })); - it('should return empty response if \'gettranslatabletopicnames\' call fails', - fakeAsync(() => { - const successHandler = jasmine.createSpy('success'); - const failHandler = jasmine.createSpy('fail'); - const emptyResponse: string [] = []; + it('should successfully save the preferred translation language.', fakeAsync(() => { + const successHandler = jasmine.createSpy('success'); + const failHandler = jasmine.createSpy('fail'); + const params = 'en'; - contributionOpportunitiesBackendApiService - .fetchTranslatableTopicNamesAsync() - .then(successHandler, failHandler); + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve(userInfo[0]) + ); - const req = httpTestingController.expectOne( - '/gettranslatabletopicnames' - ); - expect(req.request.method).toEqual('GET'); - req.flush({ - error: 'Failed to fetch translatable topic names.' - }, { - status: 500, statusText: 'Internal Server Error' - }); + contributionOpportunitiesBackendApiService + .savePreferredTranslationLanguageAsync(params) + .then(successHandler, failHandler); + tick(); - flushMicrotasks(); + const req = httpTestingController.expectOne( + '/preferredtranslationlanguage' + ); + expect(req.request.method).toEqual('POST'); + req.flush({}); + flushMicrotasks(); - expect(successHandler).toHaveBeenCalledWith(emptyResponse); - expect(failHandler).not.toHaveBeenCalled(); - })); + expect(successHandler).toHaveBeenCalledWith({}); + expect(failHandler).not.toHaveBeenCalled(); + })); - it('should successfully save the preferred translation language.', + it( + 'should fail to save the preferred translation language ' + + 'given invalid language code when calling ' + + "'savePreferredTranslationLanguageAsync'", fakeAsync(() => { const successHandler = jasmine.createSpy('success'); const failHandler = jasmine.createSpy('fail'); const params = 'en'; spyOn(userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(userInfo[0])); + Promise.resolve(userInfo[0]) + ); contributionOpportunitiesBackendApiService .savePreferredTranslationLanguageAsync(params) @@ -481,59 +538,66 @@ describe('Contribution Opportunities backend API service', function() { tick(); const req = httpTestingController.expectOne( - '/preferredtranslationlanguage' + urlInterpolationService.interpolateUrl( + '/preferredtranslationlanguage', + {language_code: 'invalidCode'} + ) ); + expect(req.request.method).toEqual('POST'); - req.flush({}); + req.flush( + { + error: 'Failed to save the preferred translation language.', + }, + { + status: 500, + statusText: 'Internal Server Error', + } + ); + flushMicrotasks(); - expect(successHandler).toHaveBeenCalledWith({}); - expect(failHandler).not.toHaveBeenCalled(); - })); + expect(successHandler).not.toHaveBeenCalled(); + expect(failHandler).toHaveBeenCalledWith( + new Error('Failed to save the preferred translation language.') + ); + }) + ); - it('should fail to save the preferred translation language ' + - 'given invalid language code when calling ' + - '\'savePreferredTranslationLanguageAsync\'', fakeAsync(() => { + it('should successfully fetch the preferred translation language', fakeAsync(() => { const successHandler = jasmine.createSpy('success'); const failHandler = jasmine.createSpy('fail'); - const params = 'en'; spyOn(userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(userInfo[0])); + Promise.resolve(userInfo[0]) + ); contributionOpportunitiesBackendApiService - .savePreferredTranslationLanguageAsync(params) + .getPreferredTranslationLanguageAsync() .then(successHandler, failHandler); tick(); const req = httpTestingController.expectOne( - urlInterpolationService.interpolateUrl( - '/preferredtranslationlanguage', - { language_code: 'invalidCode' } - ) + '/preferredtranslationlanguage' ); - - expect(req.request.method).toEqual('POST'); - req.flush({ - error: 'Failed to save the preferred translation language.' - }, { - status: 500, statusText: 'Internal Server Error' - }); + expect(req.request.method).toEqual('GET'); + req.flush({preferred_translation_language_code: 'en'}); flushMicrotasks(); - expect(successHandler).not.toHaveBeenCalled(); - expect(failHandler).toHaveBeenCalledWith( - new Error('Failed to save the preferred translation language.')); + expect(successHandler).toHaveBeenCalledWith('en'); + expect(failHandler).not.toHaveBeenCalled(); })); - it('should successfully fetch the preferred translation language', + it( + "should return null if 'preferredtranslationlanguage' " + 'call fails', fakeAsync(() => { const successHandler = jasmine.createSpy('success'); const failHandler = jasmine.createSpy('fail'); spyOn(userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(userInfo[0])); + Promise.resolve(userInfo[0]) + ); contributionOpportunitiesBackendApiService .getPreferredTranslationLanguageAsync() @@ -543,63 +607,46 @@ describe('Contribution Opportunities backend API service', function() { const req = httpTestingController.expectOne( '/preferredtranslationlanguage' ); + expect(req.request.method).toEqual('GET'); - req.flush({ preferred_translation_language_code: 'en' }); + req.flush( + { + error: '500 Internal Server Error', + }, + { + status: 500, + statusText: 'Internal Server Error', + } + ); flushMicrotasks(); - expect(successHandler).toHaveBeenCalledWith('en'); + expect(successHandler).toHaveBeenCalledWith(null); expect(failHandler).not.toHaveBeenCalled(); }) ); - it('should return null if \'preferredtranslationlanguage\' ' + - 'call fails', fakeAsync(() => { - const successHandler = jasmine.createSpy('success'); - const failHandler = jasmine.createSpy('fail'); - - spyOn(userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(userInfo[0])); - - contributionOpportunitiesBackendApiService - .getPreferredTranslationLanguageAsync() - .then(successHandler, failHandler); - tick(); - - const req = httpTestingController.expectOne( - '/preferredtranslationlanguage' - ); - - expect(req.request.method).toEqual('GET'); - req.flush({ - error: '500 Internal Server Error' - }, { - status: 500, statusText: 'Internal Server Error' - }); - - flushMicrotasks(); - - expect(successHandler).toHaveBeenCalledWith(null); - expect(failHandler).not.toHaveBeenCalled(); - })); - - it('should return null when calling ' + - '\'getPreferredTranslationLanguageAsync\' with guest ' + - 'user.', fakeAsync(() => { - const successHandler = jasmine.createSpy('success'); - const failHandler = jasmine.createSpy('fail'); + it( + 'should return null when calling ' + + "'getPreferredTranslationLanguageAsync' with guest " + + 'user.', + fakeAsync(() => { + const successHandler = jasmine.createSpy('success'); + const failHandler = jasmine.createSpy('fail'); - spyOn(userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(userInfo[1])); + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve(userInfo[1]) + ); - contributionOpportunitiesBackendApiService - .getPreferredTranslationLanguageAsync() - .then(successHandler, failHandler); - tick(); + contributionOpportunitiesBackendApiService + .getPreferredTranslationLanguageAsync() + .then(successHandler, failHandler); + tick(); - flushMicrotasks(); + flushMicrotasks(); - expect(successHandler).toHaveBeenCalledWith(null); - expect(failHandler).not.toHaveBeenCalled(); - })); + expect(successHandler).toHaveBeenCalledWith(null); + expect(failHandler).not.toHaveBeenCalled(); + }) + ); }); diff --git a/core/templates/pages/contributor-dashboard-page/services/contribution-opportunities-backend-api.service.ts b/core/templates/pages/contributor-dashboard-page/services/contribution-opportunities-backend-api.service.ts index ce7195b2c016..20f86c7cdeaa 100644 --- a/core/templates/pages/contributor-dashboard-page/services/contribution-opportunities-backend-api.service.ts +++ b/core/templates/pages/contributor-dashboard-page/services/contribution-opportunities-backend-api.service.ts @@ -17,40 +17,41 @@ * contributors to contribute. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; import { ExplorationOpportunitySummary, - ExplorationOpportunitySummaryBackendDict + ExplorationOpportunitySummaryBackendDict, } from 'domain/opportunity/exploration-opportunity-summary.model'; -import { SkillOpportunity, SkillOpportunityBackendDict } from - 'domain/opportunity/skill-opportunity.model'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; +import { + SkillOpportunity, + SkillOpportunityBackendDict, +} from 'domain/opportunity/skill-opportunity.model'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; import { FeaturedTranslationLanguage, FeaturedTranslationLanguageBackendDict, } from 'domain/opportunity/featured-translation-language.model'; -import { UserService } from 'services/user.service'; +import {UserService} from 'services/user.service'; -import { AppConstants } from 'app.constants'; +import {AppConstants} from 'app.constants'; interface SkillContributionOpportunitiesBackendDict { - 'opportunities': SkillOpportunityBackendDict[]; - 'next_cursor': string; - 'more': boolean; + opportunities: SkillOpportunityBackendDict[]; + next_cursor: string; + more: boolean; } interface TranslationContributionOpportunitiesBackendDict { - 'opportunities': ExplorationOpportunitySummaryBackendDict[]; - 'next_cursor': string; - 'more': boolean; + opportunities: ExplorationOpportunitySummaryBackendDict[]; + next_cursor: string; + more: boolean; } interface ReviewableTranslationOpportunitiesBackendDict { - 'opportunities': ExplorationOpportunitySummaryBackendDict[]; + opportunities: ExplorationOpportunitySummaryBackendDict[]; } interface SkillContributionOpportunities { @@ -70,92 +71,107 @@ interface FetchedReviewableTranslationOpportunitiesResponse { } interface FeaturedTranslationLanguagesBackendDict { - 'featured_translation_languages': FeaturedTranslationLanguageBackendDict[]; + featured_translation_languages: FeaturedTranslationLanguageBackendDict[]; } interface TopicNamesBackendDict { - 'topic_names': string[]; + topic_names: string[]; } interface PreferredTranslationLanguageBackendDict { - 'preferred_translation_language_code': string|null; + preferred_translation_language_code: string | null; } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ContributionOpportunitiesBackendApiService { urlTemplate = '/opportunitiessummaryhandler/'; constructor( private urlInterpolationService: UrlInterpolationService, private http: HttpClient, - private userService: UserService, + private userService: UserService ) {} - private UPDATE_PINNED_OPPORTUNITY_HANDLER_URL = ( - '/pinned-opportunities' - ); + private UPDATE_PINNED_OPPORTUNITY_HANDLER_URL = '/pinned-opportunities'; private _getExplorationOpportunityFromDict( - opportunityDict: ExplorationOpportunitySummaryBackendDict): - ExplorationOpportunitySummary { + opportunityDict: ExplorationOpportunitySummaryBackendDict + ): ExplorationOpportunitySummary { return new ExplorationOpportunitySummary( - opportunityDict.id, opportunityDict.topic_name, - opportunityDict.story_title, opportunityDict.chapter_title, - opportunityDict.content_count, opportunityDict.translation_counts, + opportunityDict.id, + opportunityDict.topic_name, + opportunityDict.story_title, + opportunityDict.chapter_title, + opportunityDict.content_count, + opportunityDict.translation_counts, opportunityDict.translation_in_review_counts, - opportunityDict.language_code, opportunityDict.is_pinned); + opportunityDict.language_code, + opportunityDict.is_pinned + ); } private _getSkillOpportunityFromDict( - opportunityDict: SkillOpportunityBackendDict): SkillOpportunity { + opportunityDict: SkillOpportunityBackendDict + ): SkillOpportunity { return new SkillOpportunity( - opportunityDict.id, opportunityDict.skill_description, - opportunityDict.topic_name, opportunityDict.question_count); + opportunityDict.id, + opportunityDict.skill_description, + opportunityDict.topic_name, + opportunityDict.question_count + ); } - async fetchSkillOpportunitiesAsync(cursor: string): - Promise { + async fetchSkillOpportunitiesAsync( + cursor: string + ): Promise { const params = { - cursor: cursor + cursor: cursor, }; - return this.http.get( - this.urlInterpolationService.interpolateUrl( - this.urlTemplate, { - opportunityType: AppConstants.OPPORTUNITY_TYPE_SKILL + return this.http + .get( + this.urlInterpolationService.interpolateUrl(this.urlTemplate, { + opportunityType: AppConstants.OPPORTUNITY_TYPE_SKILL, + }), + {params} + ) + .toPromise() + .then( + data => { + const opportunities = data.opportunities.map(dict => + this._getSkillOpportunityFromDict(dict) + ); + + return { + opportunities: opportunities, + nextCursor: data.next_cursor, + more: data.more, + }; + }, + errorResponse => { + throw new Error(errorResponse.error.error); } - ), { params }).toPromise().then(data => { - const opportunities = data.opportunities.map( - dict => this._getSkillOpportunityFromDict(dict)); - - return { - opportunities: opportunities, - nextCursor: data.next_cursor, - more: data.more - }; - }, errorResponse => { - throw new Error(errorResponse.error.error); - }); + ); } async pinTranslationOpportunity( - languageCode: string, - topicName: string, - explorationId: string + languageCode: string, + topicName: string, + explorationId: string ): Promise { return this.http .put(this.UPDATE_PINNED_OPPORTUNITY_HANDLER_URL, { language_code: languageCode, topic_id: topicName, - opportunity_id: explorationId + opportunity_id: explorationId, }) .toPromise(); } async unpinTranslationOpportunity( - languageCode: string, - topicName: string, + languageCode: string, + topicName: string ): Promise { return this.http .put(this.UPDATE_PINNED_OPPORTUNITY_HANDLER_URL, { @@ -166,39 +182,48 @@ export class ContributionOpportunitiesBackendApiService { } async fetchTranslationOpportunitiesAsync( - languageCode: string, topicName: string, cursor: string): - Promise { - topicName = ( - topicName === AppConstants.TOPIC_SENTINEL_NAME_ALL ? '' : topicName); + languageCode: string, + topicName: string, + cursor: string + ): Promise { + topicName = + topicName === AppConstants.TOPIC_SENTINEL_NAME_ALL ? '' : topicName; const params = { language_code: languageCode, topic_name: topicName, - cursor: cursor + cursor: cursor, }; - return this.http.get( - this.urlInterpolationService.interpolateUrl( - this.urlTemplate, { - opportunityType: AppConstants.OPPORTUNITY_TYPE_TRANSLATION + return this.http + .get( + this.urlInterpolationService.interpolateUrl(this.urlTemplate, { + opportunityType: AppConstants.OPPORTUNITY_TYPE_TRANSLATION, + }), + {params} + ) + .toPromise() + .then( + data => { + const opportunities = data.opportunities.map(dict => + this._getExplorationOpportunityFromDict(dict) + ); + + return { + opportunities: opportunities, + nextCursor: data.next_cursor, + more: data.more, + }; + }, + errorResponse => { + throw new Error(errorResponse.error.error); } - ), { params }).toPromise().then(data => { - const opportunities = data.opportunities.map( - dict => this._getExplorationOpportunityFromDict(dict)); - - return { - opportunities: opportunities, - nextCursor: data.next_cursor, - more: data.more - }; - }, errorResponse => { - throw new Error(errorResponse.error.error); - }); + ); } async fetchReviewableTranslationOpportunitiesAsync( - topicName: string, - languageCode?: string + topicName: string, + languageCode?: string ): Promise { const params: { topic_name?: string; @@ -213,40 +238,52 @@ export class ContributionOpportunitiesBackendApiService { if (languageCode && languageCode !== '') { params.language_code = languageCode; } - return this.http.get( - '/getreviewableopportunitieshandler', { - params - } as Object).toPromise().then(data => { - const opportunities = data.opportunities.map( - dict => this._getExplorationOpportunityFromDict(dict)); - return { - opportunities: opportunities - }; - }, errorResponse => { - throw new Error(errorResponse.error.error); - }); + return this.http + .get( + '/getreviewableopportunitieshandler', + { + params, + } as Object + ) + .toPromise() + .then( + data => { + const opportunities = data.opportunities.map(dict => + this._getExplorationOpportunityFromDict(dict) + ); + return { + opportunities: opportunities, + }; + }, + errorResponse => { + throw new Error(errorResponse.error.error); + } + ); } - async fetchFeaturedTranslationLanguagesAsync(): - Promise { + async fetchFeaturedTranslationLanguagesAsync(): Promise< + FeaturedTranslationLanguage[] + > { try { const response = await this.http .get( - '/retrievefeaturedtranslationlanguages').toPromise(); + '/retrievefeaturedtranslationlanguages' + ) + .toPromise(); - return response.featured_translation_languages.map( - backendDict => FeaturedTranslationLanguage - .createFromBackendDict(backendDict)); + return response.featured_translation_languages.map(backendDict => + FeaturedTranslationLanguage.createFromBackendDict(backendDict) + ); } catch { return []; } } - async fetchTranslatableTopicNamesAsync(): - Promise { + async fetchTranslatableTopicNamesAsync(): Promise { try { const response = await this.http - .get('/gettranslatabletopicnames').toPromise(); + .get('/gettranslatabletopicnames') + .toPromise(); // TODO(#15648): Re-enable "All Topics" after fetching latency is fixed. // response.topic_names.unshift('All'); @@ -257,44 +294,45 @@ export class ContributionOpportunitiesBackendApiService { } async savePreferredTranslationLanguageAsync( - languageCode: string + languageCode: string ): Promise { - return this.userService.getUserInfoAsync().then( - (userInfo) => { - if (userInfo.isLoggedIn()) { - return this.http.post( - '/preferredtranslationlanguage', - {language_code: languageCode} - ).toPromise().catch((errorResponse) => { + return this.userService.getUserInfoAsync().then(userInfo => { + if (userInfo.isLoggedIn()) { + return this.http + .post('/preferredtranslationlanguage', { + language_code: languageCode, + }) + .toPromise() + .catch(errorResponse => { throw new Error(errorResponse.error.error); }); - } } - ); + }); } - async getPreferredTranslationLanguageAsync( - ): Promise { + async getPreferredTranslationLanguageAsync(): Promise { const emptyResponse = { - preferred_translation_language_code: null + preferred_translation_language_code: null, }; - return this.userService.getUserInfoAsync().then( - async(userInfo) => { - if (userInfo.isLoggedIn()) { - const res = ( - await this.http.get( - '/preferredtranslationlanguage' - ).toPromise().catch(() => emptyResponse) - ); - return res.preferred_translation_language_code; - } else { - return null; - } + return this.userService.getUserInfoAsync().then(async userInfo => { + if (userInfo.isLoggedIn()) { + const res = await this.http + .get( + '/preferredtranslationlanguage' + ) + .toPromise() + .catch(() => emptyResponse); + return res.preferred_translation_language_code; + } else { + return null; } - ); + }); } } -angular.module('oppia').factory( - 'ContributionOpportunitiesBackendApiService', - downgradeInjectable(ContributionOpportunitiesBackendApiService)); +angular + .module('oppia') + .factory( + 'ContributionOpportunitiesBackendApiService', + downgradeInjectable(ContributionOpportunitiesBackendApiService) + ); diff --git a/core/templates/pages/contributor-dashboard-page/services/contribution-opportunities.service.spec.ts b/core/templates/pages/contributor-dashboard-page/services/contribution-opportunities.service.spec.ts index 5aa1a51ebdd7..175121278e55 100644 --- a/core/templates/pages/contributor-dashboard-page/services/contribution-opportunities.service.spec.ts +++ b/core/templates/pages/contributor-dashboard-page/services/contribution-opportunities.service.spec.ts @@ -15,88 +15,96 @@ * @fileoverview Unit tests for Contribution Opportunities Service. */ -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { ContributionOpportunitiesService, ExplorationOpportunitiesDict, SkillOpportunitiesDict } from '../services/contribution-opportunities.service'; -import { ContributionOpportunitiesBackendApiService } from 'pages/contributor-dashboard-page/services/contribution-opportunities-backend-api.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { LoginRequiredModalContent } from '../modal-templates/login-required-modal.component'; -import { SkillOpportunity } from 'domain/opportunity/skill-opportunity.model'; -import { ExplorationOpportunitySummary } from 'domain/opportunity/exploration-opportunity-summary.model'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import { + ContributionOpportunitiesService, + ExplorationOpportunitiesDict, + SkillOpportunitiesDict, +} from '../services/contribution-opportunities.service'; +import {ContributionOpportunitiesBackendApiService} from 'pages/contributor-dashboard-page/services/contribution-opportunities-backend-api.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {fakeAsync, TestBed, tick} from '@angular/core/testing'; +import {LoginRequiredModalContent} from '../modal-templates/login-required-modal.component'; +import {SkillOpportunity} from 'domain/opportunity/skill-opportunity.model'; +import {ExplorationOpportunitySummary} from 'domain/opportunity/exploration-opportunity-summary.model'; class MockNgbModalRef { componentInstance!: {}; } describe('Contribution Opportunities Service', () => { let ngbModal: NgbModal; - let contributionOpportunitiesBackendApiService: - ContributionOpportunitiesBackendApiService; + let contributionOpportunitiesBackendApiService: ContributionOpportunitiesBackendApiService; let contributionOpportunitiesService: ContributionOpportunitiesService; const skillOpportunityResponse = { - opportunities: [{ - id: 'skill_id', - skill_description: 'A new skill for question', - topic_name: 'A new topic', - question_count: 30 - }], + opportunities: [ + { + id: 'skill_id', + skill_description: 'A new skill for question', + topic_name: 'A new topic', + question_count: 30, + }, + ], next_cursor: '6', - more: true + more: true, }; const skillOpportunity = { - opportunities: [{ - id: 'exp_id', - topic_name: 'Topic', - story_title: 'A new story', - chapter_title: 'Introduction', - content_count: 100, - translation_counts: { - hi: 15 - }, - translation_in_review_counts: { - hi: 20 + opportunities: [ + { + id: 'exp_id', + topic_name: 'Topic', + story_title: 'A new story', + chapter_title: 'Introduction', + content_count: 100, + translation_counts: { + hi: 15, + }, + translation_in_review_counts: { + hi: 20, + }, + language_code: 'hi', + is_pinned: false, }, - language_code: 'hi', - is_pinned: false - }], + ], next_cursor: '6', - more: true + more: true, }; const sampleSkillOpportunitiesResponse = [ SkillOpportunity.createFromBackendDict( - skillOpportunityResponse.opportunities[0]) + skillOpportunityResponse.opportunities[0] + ), ]; const sampleTranslationOpportunitiesResponse = [ ExplorationOpportunitySummary.createFromBackendDict( - skillOpportunity.opportunities[0]) + skillOpportunity.opportunities[0] + ), ]; beforeEach(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ] + imports: [HttpClientTestingModule], }); }); beforeEach(() => { contributionOpportunitiesBackendApiService = TestBed.inject( - ContributionOpportunitiesBackendApiService); + ContributionOpportunitiesBackendApiService + ); ngbModal = TestBed.inject(NgbModal); contributionOpportunitiesService = TestBed.inject( - ContributionOpportunitiesService); + ContributionOpportunitiesService + ); }); it('should open login modal when user is not logged in', () => { const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ( - { componentInstance: MockNgbModalRef, - result: Promise.resolve('success') - } - ) as NgbModalRef; + return { + componentInstance: MockNgbModalRef, + result: Promise.resolve('success'), + } as NgbModalRef; }); contributionOpportunitiesService.showRequiresLoginModal(); @@ -104,272 +112,302 @@ describe('Contribution Opportunities Service', () => { expect(modalSpy).toHaveBeenCalledWith(LoginRequiredModalContent); }); - it('should return skill opportunities when calling ' + - '\'getSkillOpportunitiesAsync\'', fakeAsync(() => { - const successHandler = jasmine.createSpy('success'); - const failHandler = jasmine.createSpy('fail'); - - let skillOpportunitiesDict: SkillOpportunitiesDict = { - opportunities: sampleSkillOpportunitiesResponse, - more: skillOpportunityResponse.more - }; - - let getSkillOpportunitiesSpy = spyOn( - contributionOpportunitiesBackendApiService, - 'fetchSkillOpportunitiesAsync') - .and.returnValue(Promise.resolve( - { + it( + 'should return skill opportunities when calling ' + + "'getSkillOpportunitiesAsync'", + fakeAsync(() => { + const successHandler = jasmine.createSpy('success'); + const failHandler = jasmine.createSpy('fail'); + + let skillOpportunitiesDict: SkillOpportunitiesDict = { + opportunities: sampleSkillOpportunitiesResponse, + more: skillOpportunityResponse.more, + }; + + let getSkillOpportunitiesSpy = spyOn( + contributionOpportunitiesBackendApiService, + 'fetchSkillOpportunitiesAsync' + ).and.returnValue( + Promise.resolve({ opportunities: sampleSkillOpportunitiesResponse, nextCursor: skillOpportunityResponse.next_cursor, - more: skillOpportunityResponse.more - } - )); - - contributionOpportunitiesService.getSkillOpportunitiesAsync().then( - successHandler, failHandler); - tick(); - - expect(getSkillOpportunitiesSpy).toHaveBeenCalled(); - expect(successHandler).toHaveBeenCalledWith(skillOpportunitiesDict); - })); - - it('should return more skill opportunities if they are available ' + - 'when calling \'getMoreSkillOpportunitiesAsync\'', fakeAsync(() => { - const successHandler = jasmine.createSpy('success'); - const failHandler = jasmine.createSpy('fail'); - - let skillOpportunitiesDict: SkillOpportunitiesDict = { - opportunities: sampleSkillOpportunitiesResponse, - more: skillOpportunityResponse.more - }; - - let getSkillOpportunitiesSpy = spyOn( - contributionOpportunitiesBackendApiService, - 'fetchSkillOpportunitiesAsync') - .and.returnValue(Promise.resolve( - { + more: skillOpportunityResponse.more, + }) + ); + + contributionOpportunitiesService + .getSkillOpportunitiesAsync() + .then(successHandler, failHandler); + tick(); + + expect(getSkillOpportunitiesSpy).toHaveBeenCalled(); + expect(successHandler).toHaveBeenCalledWith(skillOpportunitiesDict); + }) + ); + + it( + 'should return more skill opportunities if they are available ' + + "when calling 'getMoreSkillOpportunitiesAsync'", + fakeAsync(() => { + const successHandler = jasmine.createSpy('success'); + const failHandler = jasmine.createSpy('fail'); + + let skillOpportunitiesDict: SkillOpportunitiesDict = { + opportunities: sampleSkillOpportunitiesResponse, + more: skillOpportunityResponse.more, + }; + + let getSkillOpportunitiesSpy = spyOn( + contributionOpportunitiesBackendApiService, + 'fetchSkillOpportunitiesAsync' + ).and.returnValue( + Promise.resolve({ opportunities: sampleSkillOpportunitiesResponse, nextCursor: skillOpportunityResponse.next_cursor, - more: skillOpportunityResponse.more - } - )); - - contributionOpportunitiesService.getMoreSkillOpportunitiesAsync().then( - successHandler, failHandler); - tick(); - - expect(getSkillOpportunitiesSpy).toHaveBeenCalled(); - expect(successHandler).toHaveBeenCalledWith(skillOpportunitiesDict); - })); - - it('should throw error if no more skill opportunity is available ' + - 'when calling \'getMoreSkillOpportunitiesAsync\'', fakeAsync(() => { - const successHandler = jasmine.createSpy('success'); - const failHandler = jasmine.createSpy('fail'); - - let getSkillOpportunitiesSpy = spyOn( - contributionOpportunitiesBackendApiService, - 'fetchSkillOpportunitiesAsync') - .and.returnValue(Promise.resolve( - { + more: skillOpportunityResponse.more, + }) + ); + + contributionOpportunitiesService + .getMoreSkillOpportunitiesAsync() + .then(successHandler, failHandler); + tick(); + + expect(getSkillOpportunitiesSpy).toHaveBeenCalled(); + expect(successHandler).toHaveBeenCalledWith(skillOpportunitiesDict); + }) + ); + + it( + 'should throw error if no more skill opportunity is available ' + + "when calling 'getMoreSkillOpportunitiesAsync'", + fakeAsync(() => { + const successHandler = jasmine.createSpy('success'); + const failHandler = jasmine.createSpy('fail'); + + let getSkillOpportunitiesSpy = spyOn( + contributionOpportunitiesBackendApiService, + 'fetchSkillOpportunitiesAsync' + ).and.returnValue( + Promise.resolve({ opportunities: sampleSkillOpportunitiesResponse, nextCursor: skillOpportunityResponse.next_cursor, - more: false - } - )); - - contributionOpportunitiesService.getMoreSkillOpportunitiesAsync().then( - successHandler, failHandler); - tick(); - - expect(getSkillOpportunitiesSpy).toHaveBeenCalled(); - expect(successHandler).toHaveBeenCalled(); - - contributionOpportunitiesService.getMoreSkillOpportunitiesAsync().then( - successHandler, failHandler); - tick(); - - expect(failHandler).toHaveBeenCalled(); - })); - - it('should return translation opportunities when calling ' + - '\'getTranslationOpportunitiesAsync\'', fakeAsync(() => { - const successHandler = jasmine.createSpy('success'); - const failHandler = jasmine.createSpy('fail'); - - let translationOpportunitiesDict: ExplorationOpportunitiesDict = { - opportunities: sampleTranslationOpportunitiesResponse, - more: true - }; - - let getTranslationOpportunitiesSpy = spyOn( - contributionOpportunitiesBackendApiService, - 'fetchTranslationOpportunitiesAsync') - .and.returnValue(Promise.resolve( - { + more: false, + }) + ); + + contributionOpportunitiesService + .getMoreSkillOpportunitiesAsync() + .then(successHandler, failHandler); + tick(); + + expect(getSkillOpportunitiesSpy).toHaveBeenCalled(); + expect(successHandler).toHaveBeenCalled(); + + contributionOpportunitiesService + .getMoreSkillOpportunitiesAsync() + .then(successHandler, failHandler); + tick(); + + expect(failHandler).toHaveBeenCalled(); + }) + ); + + it( + 'should return translation opportunities when calling ' + + "'getTranslationOpportunitiesAsync'", + fakeAsync(() => { + const successHandler = jasmine.createSpy('success'); + const failHandler = jasmine.createSpy('fail'); + + let translationOpportunitiesDict: ExplorationOpportunitiesDict = { + opportunities: sampleTranslationOpportunitiesResponse, + more: true, + }; + + let getTranslationOpportunitiesSpy = spyOn( + contributionOpportunitiesBackendApiService, + 'fetchTranslationOpportunitiesAsync' + ).and.returnValue( + Promise.resolve({ opportunities: sampleTranslationOpportunitiesResponse, nextCursor: '6', - more: true - } - )); - - contributionOpportunitiesService - .getTranslationOpportunitiesAsync('en', 'Topic') - .then(successHandler, failHandler); - tick(); - - expect(getTranslationOpportunitiesSpy).toHaveBeenCalled(); - expect(successHandler).toHaveBeenCalledWith(translationOpportunitiesDict); - })); - - it('should return more translation opportunities if they are available ' + - 'when calling \'getMoreTranslationOpportunitiesAsync\'', fakeAsync(() => { - const successHandler = jasmine.createSpy('success'); - const failHandler = jasmine.createSpy('fail'); - - let translationOpportunitiesDict: ExplorationOpportunitiesDict = { - opportunities: sampleTranslationOpportunitiesResponse, - more: true - }; - - let getTranslationOpportunitiesSpy = spyOn( - contributionOpportunitiesBackendApiService, - 'fetchTranslationOpportunitiesAsync') - .and.returnValue(Promise.resolve( - { + more: true, + }) + ); + + contributionOpportunitiesService + .getTranslationOpportunitiesAsync('en', 'Topic') + .then(successHandler, failHandler); + tick(); + + expect(getTranslationOpportunitiesSpy).toHaveBeenCalled(); + expect(successHandler).toHaveBeenCalledWith(translationOpportunitiesDict); + }) + ); + + it( + 'should return more translation opportunities if they are available ' + + "when calling 'getMoreTranslationOpportunitiesAsync'", + fakeAsync(() => { + const successHandler = jasmine.createSpy('success'); + const failHandler = jasmine.createSpy('fail'); + + let translationOpportunitiesDict: ExplorationOpportunitiesDict = { + opportunities: sampleTranslationOpportunitiesResponse, + more: true, + }; + + let getTranslationOpportunitiesSpy = spyOn( + contributionOpportunitiesBackendApiService, + 'fetchTranslationOpportunitiesAsync' + ).and.returnValue( + Promise.resolve({ opportunities: sampleTranslationOpportunitiesResponse, nextCursor: '6', - more: true - } - )); - - contributionOpportunitiesService - .getMoreTranslationOpportunitiesAsync('en', 'Topic') - .then(successHandler, failHandler); - tick(); - - expect(getTranslationOpportunitiesSpy).toHaveBeenCalled(); - expect(successHandler).toHaveBeenCalledWith(translationOpportunitiesDict); - })); - - it('should return reviewable translation opportunities when calling ' + - '\'getReviewableTranslationOpportunitiesAsync\'', fakeAsync(() => { - const successHandler = jasmine.createSpy('success'); - const failHandler = jasmine.createSpy('fail'); - const translationOpportunitiesDict: ExplorationOpportunitiesDict = { - opportunities: sampleTranslationOpportunitiesResponse, - more: false - }; - const getReviewableTranslationOpportunitiesSpy = spyOn( - contributionOpportunitiesBackendApiService, - 'fetchReviewableTranslationOpportunitiesAsync') - .and.returnValue(Promise.resolve( - { + more: true, + }) + ); + + contributionOpportunitiesService + .getMoreTranslationOpportunitiesAsync('en', 'Topic') + .then(successHandler, failHandler); + tick(); + + expect(getTranslationOpportunitiesSpy).toHaveBeenCalled(); + expect(successHandler).toHaveBeenCalledWith(translationOpportunitiesDict); + }) + ); + + it( + 'should return reviewable translation opportunities when calling ' + + "'getReviewableTranslationOpportunitiesAsync'", + fakeAsync(() => { + const successHandler = jasmine.createSpy('success'); + const failHandler = jasmine.createSpy('fail'); + const translationOpportunitiesDict: ExplorationOpportunitiesDict = { + opportunities: sampleTranslationOpportunitiesResponse, + more: false, + }; + const getReviewableTranslationOpportunitiesSpy = spyOn( + contributionOpportunitiesBackendApiService, + 'fetchReviewableTranslationOpportunitiesAsync' + ).and.returnValue( + Promise.resolve({ opportunities: sampleTranslationOpportunitiesResponse, - more: false - } - )); - - contributionOpportunitiesService - .getReviewableTranslationOpportunitiesAsync('Topic') - .then(successHandler, failHandler); - tick(); - - expect(getReviewableTranslationOpportunitiesSpy).toHaveBeenCalled(); - expect(successHandler).toHaveBeenCalledWith(translationOpportunitiesDict); - })); - - it('should throw error if no more translation opportunities is available ' + - 'when calling \'getMoreTranslationOpportunitiesAsync\'', fakeAsync(() => { - const successHandler = jasmine.createSpy('success'); - const failHandler = jasmine.createSpy('fail'); - - let getTranslationOpportunitiesSpy = spyOn( - contributionOpportunitiesBackendApiService, - 'fetchTranslationOpportunitiesAsync') - .and.returnValue(Promise.resolve( - { + more: false, + }) + ); + + contributionOpportunitiesService + .getReviewableTranslationOpportunitiesAsync('Topic') + .then(successHandler, failHandler); + tick(); + + expect(getReviewableTranslationOpportunitiesSpy).toHaveBeenCalled(); + expect(successHandler).toHaveBeenCalledWith(translationOpportunitiesDict); + }) + ); + + it( + 'should throw error if no more translation opportunities is available ' + + "when calling 'getMoreTranslationOpportunitiesAsync'", + fakeAsync(() => { + const successHandler = jasmine.createSpy('success'); + const failHandler = jasmine.createSpy('fail'); + + let getTranslationOpportunitiesSpy = spyOn( + contributionOpportunitiesBackendApiService, + 'fetchTranslationOpportunitiesAsync' + ).and.returnValue( + Promise.resolve({ opportunities: sampleTranslationOpportunitiesResponse, nextCursor: '6', - more: false - } - )); - - contributionOpportunitiesService - .getMoreTranslationOpportunitiesAsync('en', 'Topic') - .then(successHandler, failHandler); - tick(); - - expect(getTranslationOpportunitiesSpy).toHaveBeenCalled(); - expect(successHandler).toHaveBeenCalled(); - - contributionOpportunitiesService - .getMoreTranslationOpportunitiesAsync('en', 'Topic') - .then(successHandler, failHandler); - tick(); - - expect(failHandler).toHaveBeenCalled(); - })); - - it('should return topic names when calling ' + - '\'getTranslatableTopicNamesAsync\'', fakeAsync(() => { - const successHandler = jasmine.createSpy('success'); - const failHandler = jasmine.createSpy('fail'); - - let topicNamesDict = ['Topic 1', 'Topic 2']; - - let getTranslatableTopicNamesSpy = spyOn( - contributionOpportunitiesBackendApiService, - 'fetchTranslatableTopicNamesAsync') - .and.returnValue(Promise.resolve(topicNamesDict)); - - contributionOpportunitiesService.getTranslatableTopicNamesAsync() - .then(successHandler, failHandler); - tick(); - - expect(getTranslatableTopicNamesSpy).toHaveBeenCalled(); - expect(successHandler).toHaveBeenCalledWith(topicNamesDict); - })); - - it('should successfully pin reviewable pinned translation' + - ' opportunities', fakeAsync(() => { - const successHandler = jasmine.createSpy('success'); - const failHandler = jasmine.createSpy('fail'); - - let pinTranslationOpportunitySpy = spyOn( - contributionOpportunitiesBackendApiService, - 'pinTranslationOpportunity') - .and.returnValue(Promise.resolve(undefined)); - - contributionOpportunitiesService - .pinReviewableTranslationOpportunityAsync( - 'Topic 1', 'en', 'exp 1').then( - successHandler, failHandler - ); - tick(); - - expect(pinTranslationOpportunitySpy).toHaveBeenCalled(); - expect(successHandler).toHaveBeenCalled(); - })); - - it('should successfully unpin reviewable pinned translation' + - ' opportunities', fakeAsync(() => { - const successHandler = jasmine.createSpy('success'); - const failHandler = jasmine.createSpy('fail'); - - let unpinTranslationOpportunitySpy = spyOn( - contributionOpportunitiesBackendApiService, - 'unpinTranslationOpportunity') - .and.returnValue(Promise.resolve(undefined)); - - contributionOpportunitiesService - .unpinReviewableTranslationOpportunityAsync( - 'Topic 1', 'en', '1').then( - successHandler, failHandler + more: false, + }) ); - tick(); - expect(unpinTranslationOpportunitySpy).toHaveBeenCalled(); - expect(successHandler).toHaveBeenCalled(); - })); + contributionOpportunitiesService + .getMoreTranslationOpportunitiesAsync('en', 'Topic') + .then(successHandler, failHandler); + tick(); + + expect(getTranslationOpportunitiesSpy).toHaveBeenCalled(); + expect(successHandler).toHaveBeenCalled(); + + contributionOpportunitiesService + .getMoreTranslationOpportunitiesAsync('en', 'Topic') + .then(successHandler, failHandler); + tick(); + + expect(failHandler).toHaveBeenCalled(); + }) + ); + + it( + 'should return topic names when calling ' + + "'getTranslatableTopicNamesAsync'", + fakeAsync(() => { + const successHandler = jasmine.createSpy('success'); + const failHandler = jasmine.createSpy('fail'); + + let topicNamesDict = ['Topic 1', 'Topic 2']; + + let getTranslatableTopicNamesSpy = spyOn( + contributionOpportunitiesBackendApiService, + 'fetchTranslatableTopicNamesAsync' + ).and.returnValue(Promise.resolve(topicNamesDict)); + + contributionOpportunitiesService + .getTranslatableTopicNamesAsync() + .then(successHandler, failHandler); + tick(); + + expect(getTranslatableTopicNamesSpy).toHaveBeenCalled(); + expect(successHandler).toHaveBeenCalledWith(topicNamesDict); + }) + ); + + it( + 'should successfully pin reviewable pinned translation' + ' opportunities', + fakeAsync(() => { + const successHandler = jasmine.createSpy('success'); + const failHandler = jasmine.createSpy('fail'); + + let pinTranslationOpportunitySpy = spyOn( + contributionOpportunitiesBackendApiService, + 'pinTranslationOpportunity' + ).and.returnValue(Promise.resolve(undefined)); + + contributionOpportunitiesService + .pinReviewableTranslationOpportunityAsync('Topic 1', 'en', 'exp 1') + .then(successHandler, failHandler); + tick(); + + expect(pinTranslationOpportunitySpy).toHaveBeenCalled(); + expect(successHandler).toHaveBeenCalled(); + }) + ); + + it( + 'should successfully unpin reviewable pinned translation' + + ' opportunities', + fakeAsync(() => { + const successHandler = jasmine.createSpy('success'); + const failHandler = jasmine.createSpy('fail'); + + let unpinTranslationOpportunitySpy = spyOn( + contributionOpportunitiesBackendApiService, + 'unpinTranslationOpportunity' + ).and.returnValue(Promise.resolve(undefined)); + + contributionOpportunitiesService + .unpinReviewableTranslationOpportunityAsync('Topic 1', 'en', '1') + .then(successHandler, failHandler); + tick(); + + expect(unpinTranslationOpportunitySpy).toHaveBeenCalled(); + expect(successHandler).toHaveBeenCalled(); + }) + ); }); diff --git a/core/templates/pages/contributor-dashboard-page/services/contribution-opportunities.service.ts b/core/templates/pages/contributor-dashboard-page/services/contribution-opportunities.service.ts index fc1fe959cd26..7ed1c7aaff8b 100644 --- a/core/templates/pages/contributor-dashboard-page/services/contribution-opportunities.service.ts +++ b/core/templates/pages/contributor-dashboard-page/services/contribution-opportunities.service.ts @@ -16,15 +16,15 @@ * @fileoverview A service for handling contribution opportunities in different * fields. */ -import { EventEmitter } from '@angular/core'; -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {EventEmitter} from '@angular/core'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; -import { ContributionOpportunitiesBackendApiService } from 'pages/contributor-dashboard-page/services/contribution-opportunities-backend-api.service'; -import { SkillOpportunity } from 'domain/opportunity/skill-opportunity.model'; -import { ExplorationOpportunitySummary } from 'domain/opportunity/exploration-opportunity-summary.model'; -import { LoginRequiredModalContent } from 'pages/contributor-dashboard-page/modal-templates/login-required-modal.component'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import {ContributionOpportunitiesBackendApiService} from 'pages/contributor-dashboard-page/services/contribution-opportunities-backend-api.service'; +import {SkillOpportunity} from 'domain/opportunity/skill-opportunity.model'; +import {ExplorationOpportunitySummary} from 'domain/opportunity/exploration-opportunity-summary.model'; +import {LoginRequiredModalContent} from 'pages/contributor-dashboard-page/modal-templates/login-required-modal.component'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; export interface SkillOpportunitiesDict { opportunities: SkillOpportunity[]; @@ -37,21 +37,21 @@ export interface ExplorationOpportunitiesDict { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ContributionOpportunitiesService { constructor( - private readonly contributionOpportunitiesBackendApiService: - ContributionOpportunitiesBackendApiService, - private readonly modalService: NgbModal) {} + private readonly contributionOpportunitiesBackendApiService: ContributionOpportunitiesBackendApiService, + private readonly modalService: NgbModal + ) {} private _reloadOpportunitiesEventEmitter = new EventEmitter(); private _removeOpportunitiesEventEmitter = new EventEmitter(); - private _pinnedOpportunitiesChanged: EventEmitter< - Record> = new EventEmitter(); + private _pinnedOpportunitiesChanged: EventEmitter> = + new EventEmitter(); - private _unpinnedOpportunitiesChanged: EventEmitter< - Record> = new EventEmitter(); + private _unpinnedOpportunitiesChanged: EventEmitter> = + new EventEmitter(); // These properties are initialized using async methods // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -63,37 +63,40 @@ export class ContributionOpportunitiesService { private _moreTranslationOpportunitiesAvailable: boolean = true; private _moreVoiceoverOpportunitiesAvailable: boolean = true; - private async _getSkillOpportunitiesAsync(cursor: string): - Promise { + private async _getSkillOpportunitiesAsync( + cursor: string + ): Promise { return this.contributionOpportunitiesBackendApiService .fetchSkillOpportunitiesAsync(cursor) - .then(({ opportunities, nextCursor, more }) => { + .then(({opportunities, nextCursor, more}) => { this._skillOpportunitiesCursor = nextCursor; this._moreSkillOpportunitiesAvailable = more; return { opportunities: opportunities, - more: more + more: more, }; }); } private async _getTranslationOpportunitiesAsync( - languageCode: string, topicName: string, cursor: string) { + languageCode: string, + topicName: string, + cursor: string + ) { return this.contributionOpportunitiesBackendApiService .fetchTranslationOpportunitiesAsync(languageCode, topicName, cursor) - .then(({ opportunities, nextCursor, more }) => { + .then(({opportunities, nextCursor, more}) => { this._translationOpportunitiesCursor = nextCursor; this._moreTranslationOpportunitiesAvailable = more; return { opportunities: opportunities, - more: more + more: more, }; }); } private async _getTranslatableTopicNamesAsync() { - return this.contributionOpportunitiesBackendApiService - .fetchTranslatableTopicNamesAsync(); + return this.contributionOpportunitiesBackendApiService.fetchTranslatableTopicNamesAsync(); } showRequiresLoginModal(): void { @@ -105,14 +108,13 @@ export class ContributionOpportunitiesService { } async getTranslationOpportunitiesAsync( - languageCode: string, topicName: string): - Promise { - return this._getTranslationOpportunitiesAsync( - languageCode, topicName, ''); + languageCode: string, + topicName: string + ): Promise { + return this._getTranslationOpportunitiesAsync(languageCode, topicName, ''); } - async getMoreSkillOpportunitiesAsync(): - Promise { + async getMoreSkillOpportunitiesAsync(): Promise { if (this._moreSkillOpportunitiesAvailable) { return this._getSkillOpportunitiesAsync(this._skillOpportunitiesCursor); } @@ -120,25 +122,29 @@ export class ContributionOpportunitiesService { } async getMoreTranslationOpportunitiesAsync( - languageCode: string, topicName: string): - Promise { + languageCode: string, + topicName: string + ): Promise { if (this._moreTranslationOpportunitiesAvailable) { return this._getTranslationOpportunitiesAsync( - languageCode, topicName, this._translationOpportunitiesCursor); + languageCode, + topicName, + this._translationOpportunitiesCursor + ); } throw new Error('No more translation opportunities available.'); } async getReviewableTranslationOpportunitiesAsync( - topicName: string, - languageCode?: string): - Promise { + topicName: string, + languageCode?: string + ): Promise { return this.contributionOpportunitiesBackendApiService .fetchReviewableTranslationOpportunitiesAsync(topicName, languageCode) - .then(({ opportunities }) => { + .then(({opportunities}) => { return { opportunities: opportunities, - more: false + more: false, }; }); } @@ -152,47 +158,54 @@ export class ContributionOpportunitiesService { } async pinReviewableTranslationOpportunityAsync( - topicName: string, - languageCode: string, - explorationId: string): - Promise { - this.pinnedOpportunitiesChanged.emit( - {topicName, languageCode, explorationId}); - return this.contributionOpportunitiesBackendApiService - .pinTranslationOpportunity( - languageCode, - topicName, - explorationId); + topicName: string, + languageCode: string, + explorationId: string + ): Promise { + this.pinnedOpportunitiesChanged.emit({ + topicName, + languageCode, + explorationId, + }); + return this.contributionOpportunitiesBackendApiService.pinTranslationOpportunity( + languageCode, + topicName, + explorationId + ); } async unpinReviewableTranslationOpportunityAsync( - topicName: string, - languageCode: string, - explorationId: string): - Promise { - this.unpinnedOpportunitiesChanged.emit( - {topicName, languageCode, explorationId}); - return this.contributionOpportunitiesBackendApiService - .unpinTranslationOpportunity( - languageCode, - topicName); + topicName: string, + languageCode: string, + explorationId: string + ): Promise { + this.unpinnedOpportunitiesChanged.emit({ + topicName, + languageCode, + explorationId, + }); + return this.contributionOpportunitiesBackendApiService.unpinTranslationOpportunity( + languageCode, + topicName + ); } get removeOpportunitiesEventEmitter(): EventEmitter { return this._removeOpportunitiesEventEmitter; } - get pinnedOpportunitiesChanged(): EventEmitter< - Record> { + get pinnedOpportunitiesChanged(): EventEmitter> { return this._pinnedOpportunitiesChanged; } - get unpinnedOpportunitiesChanged(): EventEmitter< - Record> { + get unpinnedOpportunitiesChanged(): EventEmitter> { return this._unpinnedOpportunitiesChanged; } } -angular.module('oppia').factory( - 'ContributionOpportunitiesService', - downgradeInjectable(ContributionOpportunitiesService)); +angular + .module('oppia') + .factory( + 'ContributionOpportunitiesService', + downgradeInjectable(ContributionOpportunitiesService) + ); diff --git a/core/templates/pages/contributor-dashboard-page/services/question-suggestion-backend-api.service.spec.ts b/core/templates/pages/contributor-dashboard-page/services/question-suggestion-backend-api.service.spec.ts index 9d460e147c49..bcdf480875eb 100644 --- a/core/templates/pages/contributor-dashboard-page/services/question-suggestion-backend-api.service.spec.ts +++ b/core/templates/pages/contributor-dashboard-page/services/question-suggestion-backend-api.service.spec.ts @@ -1,4 +1,3 @@ - // Copyright 2021 The Oppia Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,19 +16,28 @@ * @fileoverview Unit tests for QuestionSuggestionBackendApiService. */ -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { fakeAsync, flushMicrotasks, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { AppConstants } from 'app.constants'; -import { RecordedVoiceovers } from 'domain/exploration/recorded-voiceovers.model'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { Question } from 'domain/question/QuestionObjectFactory'; -import { ConceptCard } from 'domain/skill/concept-card.model'; -import { Skill } from 'domain/skill/SkillObjectFactory'; -import { ImageLocalStorageService } from 'services/image-local-storage.service'; -import { QuestionSuggestionBackendApiService } from './question-suggestion-backend-api.service'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import { + fakeAsync, + flushMicrotasks, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {AppConstants} from 'app.constants'; +import {RecordedVoiceovers} from 'domain/exploration/recorded-voiceovers.model'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {Question} from 'domain/question/QuestionObjectFactory'; +import {ConceptCard} from 'domain/skill/concept-card.model'; +import {Skill} from 'domain/skill/SkillObjectFactory'; +import {ImageLocalStorageService} from 'services/image-local-storage.service'; +import {QuestionSuggestionBackendApiService} from './question-suggestion-backend-api.service'; const fakeImage = (): File => { - const blob = new Blob([''], { type: 'image/jpeg' }); + const blob = new Blob([''], {type: 'image/jpeg'}); return blob as File; }; @@ -40,12 +48,8 @@ describe('Question Suggestion Backend Api Service', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - providers: [ - QuestionSuggestionBackendApiService - ] + imports: [HttpClientTestingModule], + providers: [QuestionSuggestionBackendApiService], }).compileComponents(); })); @@ -58,37 +62,51 @@ describe('Question Suggestion Backend Api Service', () => { it('should suggest questions', fakeAsync(() => { spyOn( imageLocalStorageService, - 'getFilenameToBase64MappingAsync').and.returnValue( - Promise.resolve({})); + 'getFilenameToBase64MappingAsync' + ).and.returnValue(Promise.resolve({})); let question = { toBackendDict: (isNewQuestion: boolean) => { return {}; - } + }, }; const conceptCard = new ConceptCard( SubtitledHtml.createDefault( - 'review material', AppConstants.COMPONENT_NAME_EXPLANATION), + 'review material', + AppConstants.COMPONENT_NAME_EXPLANATION + ), [], RecordedVoiceovers.createFromBackendDict({ voiceovers_mapping: { - COMPONENT_NAME_EXPLANATION: {} - } + COMPONENT_NAME_EXPLANATION: {}, + }, }) ); let associatedSkill = new Skill( - 'test_skill', 'description', [], [], - conceptCard, 'en', 1, 0, 'test_id', false, []); + 'test_skill', + 'description', + [], + [], + conceptCard, + 'en', + 1, + 0, + 'test_id', + false, + [] + ); let successHandler = jasmine.createSpy('success'); let failHandler = jasmine.createSpy('fail'); - qsbas.submitSuggestionAsync( - question as Question, associatedSkill, 1, [{ - filename: 'image', - imageBlob: fakeImage() - }]) + qsbas + .submitSuggestionAsync(question as Question, associatedSkill, 1, [ + { + filename: 'image', + imageBlob: fakeImage(), + }, + ]) .then(successHandler, failHandler); tick(); diff --git a/core/templates/pages/contributor-dashboard-page/services/question-suggestion-backend-api.service.ts b/core/templates/pages/contributor-dashboard-page/services/question-suggestion-backend-api.service.ts index 2a12e010f097..a31089477de9 100644 --- a/core/templates/pages/contributor-dashboard-page/services/question-suggestion-backend-api.service.ts +++ b/core/templates/pages/contributor-dashboard-page/services/question-suggestion-backend-api.service.ts @@ -16,16 +16,16 @@ * @fileoverview A backend api service for handling question suggestions. */ -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { ImageData } from 'domain/skill/skill-creation-backend-api.service'; -import { Question } from 'domain/question/QuestionObjectFactory'; -import { Skill } from 'domain/skill/SkillObjectFactory'; -import { ImageLocalStorageService } from 'services/image-local-storage.service'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {ImageData} from 'domain/skill/skill-creation-backend-api.service'; +import {Question} from 'domain/question/QuestionObjectFactory'; +import {Skill} from 'domain/skill/SkillObjectFactory'; +import {ImageLocalStorageService} from 'services/image-local-storage.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class QuestionSuggestionBackendApiService { constructor( @@ -34,10 +34,10 @@ export class QuestionSuggestionBackendApiService { ) {} async submitSuggestionAsync( - question: Question, - associatedSkill: Skill, - skillDifficulty: number, - imagesData: ImageData[] + question: Question, + associatedSkill: Skill, + skillDifficulty: number, + imagesData: ImageData[] ): Promise { let url: string = '/suggestionhandler/'; let postData: Object = { @@ -52,9 +52,10 @@ export class QuestionSuggestionBackendApiService { skill_id: associatedSkill.getId(), skill_difficulty: skillDifficulty, }, - files: ( + files: await this.imageLocalStorageService.getFilenameToBase64MappingAsync( - imagesData)) + imagesData + ), }; let body: FormData = new FormData(); @@ -63,5 +64,9 @@ export class QuestionSuggestionBackendApiService { } } -angular.module('oppia').factory('QuestionSuggestionBackendApiService', - downgradeInjectable(QuestionSuggestionBackendApiService)); +angular + .module('oppia') + .factory( + 'QuestionSuggestionBackendApiService', + downgradeInjectable(QuestionSuggestionBackendApiService) + ); diff --git a/core/templates/pages/contributor-dashboard-page/services/translate-text-backend-api.service.spec.ts b/core/templates/pages/contributor-dashboard-page/services/translate-text-backend-api.service.spec.ts index 737bebfc1539..1c5b3a284cd6 100644 --- a/core/templates/pages/contributor-dashboard-page/services/translate-text-backend-api.service.spec.ts +++ b/core/templates/pages/contributor-dashboard-page/services/translate-text-backend-api.service.spec.ts @@ -16,13 +16,18 @@ * @fileoverview Tests that translatable text backend api works correctly. */ -import { HttpErrorResponse } from '@angular/common/http'; -import { HttpClientTestingModule, HttpTestingController } from - '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; -import { TranslatableTexts } from 'domain/opportunity/translatable-texts.model'; -import { ImageLocalStorageService, ImagesData } from 'services/image-local-storage.service'; -import { TranslateTextBackendApiService } from './translate-text-backend-api.service'; +import {HttpErrorResponse} from '@angular/common/http'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; +import {TranslatableTexts} from 'domain/opportunity/translatable-texts.model'; +import { + ImageLocalStorageService, + ImagesData, +} from 'services/image-local-storage.service'; +import {TranslateTextBackendApiService} from './translate-text-backend-api.service'; describe('TranslateTextBackendApiService', () => { let translateTextBackendApiService: TranslateTextBackendApiService; @@ -34,7 +39,7 @@ describe('TranslateTextBackendApiService', () => { content_value: text, content_type: 'content', interaction_id: null, - rule_type: null + rule_type: null, }; }; @@ -44,7 +49,8 @@ describe('TranslateTextBackendApiService', () => { }); httpTestingController = TestBed.inject(HttpTestingController); translateTextBackendApiService = TestBed.inject( - TranslateTextBackendApiService); + TranslateTextBackendApiService + ); imageLocalStorageService = TestBed.inject(ImageLocalStorageService); }); @@ -56,43 +62,49 @@ describe('TranslateTextBackendApiService', () => { let successHandler: jasmine.Spy; let failHandler: (error: HttpErrorResponse) => void; - it('should correctly request translatable texts for a given exploration ' + - 'id and language code', fakeAsync(() => { - successHandler = jasmine.createSpy('success'); - failHandler = jasmine.createSpy('error'); - const sampleDataResults = { - state_names_to_content_id_mapping: { - stateName1: { - contentId1: getTranslatableItem('text1'), - contentId2: getTranslatableItem('text2') + it( + 'should correctly request translatable texts for a given exploration ' + + 'id and language code', + fakeAsync(() => { + successHandler = jasmine.createSpy('success'); + failHandler = jasmine.createSpy('error'); + const sampleDataResults = { + state_names_to_content_id_mapping: { + stateName1: { + contentId1: getTranslatableItem('text1'), + contentId2: getTranslatableItem('text2'), + }, + stateName2: {contentId3: getTranslatableItem('text3')}, }, - stateName2: {contentId3: getTranslatableItem('text3')} - }, - version: '1' - }; - translateTextBackendApiService.getTranslatableTextsAsync('1', 'en').then( - successHandler, failHandler - ); - const req = httpTestingController.expectOne( - '/gettranslatabletexthandler?exp_id=1&language_code=en'); - expect(req.request.method).toEqual('GET'); - req.flush(sampleDataResults); - flushMicrotasks(); + version: '1', + }; + translateTextBackendApiService + .getTranslatableTextsAsync('1', 'en') + .then(successHandler, failHandler); + const req = httpTestingController.expectOne( + '/gettranslatabletexthandler?exp_id=1&language_code=en' + ); + expect(req.request.method).toEqual('GET'); + req.flush(sampleDataResults); + flushMicrotasks(); - expect(successHandler).toHaveBeenCalledWith( - TranslatableTexts.createFromBackendDict(sampleDataResults)); - })); + expect(successHandler).toHaveBeenCalledWith( + TranslatableTexts.createFromBackendDict(sampleDataResults) + ); + }) + ); it('should call the failHandler on error response', fakeAsync(() => { const errorEvent = new ErrorEvent('error'); failHandler = (error: HttpErrorResponse) => { expect(error.error).toBe(errorEvent); }; - translateTextBackendApiService.getTranslatableTextsAsync('1', 'en').then( - successHandler, failHandler - ); + translateTextBackendApiService + .getTranslatableTextsAsync('1', 'en') + .then(successHandler, failHandler); const req = httpTestingController.expectOne( - '/gettranslatabletexthandler?exp_id=1&language_code=en'); + '/gettranslatabletexthandler?exp_id=1&language_code=en' + ); expect(req.request.method).toEqual('GET'); req.error(errorEvent); flushMicrotasks(); @@ -121,18 +133,20 @@ describe('TranslateTextBackendApiService', () => { beforeEach(() => { successHandler = jasmine.createSpy('success'); failHandler = jasmine.createSpy('error'); - imagesData = [{ - filename: 'imageFilename', - imageBlob: new Blob(['imageBlob1'], {type: 'image'}) - }]; + imagesData = [ + { + filename: 'imageFilename', + imageBlob: new Blob(['imageBlob1'], {type: 'image'}), + }, + ]; }); it('should correctly submit a translation suggestion', fakeAsync(() => { - // This throws "Argument of type 'mockReaderObject' is not assignable to - // parameter of type 'HTMLImageElement'.". We need to suppress this - // error because 'HTMLImageElement' has around 250 more properties. - // We have only defined the properties we need in 'mockReaderObject'. - // @ts-expect-error + // This throws "Argument of type 'mockReaderObject' is not assignable to + // parameter of type 'HTMLImageElement'.". We need to suppress this + // error because 'HTMLImageElement' has around 250 more properties. + // We have only defined the properties we need in 'mockReaderObject'. + // @ts-expect-error spyOn(window, 'FileReader').and.returnValue(new MockReaderObject()); const expectedPayload = { suggestion_type: 'translate_content', @@ -147,30 +161,32 @@ describe('TranslateTextBackendApiService', () => { language_code: 'languageCode', content_html: 'contentHtml', translation_html: 'translationHtml', - data_format: 'html' + data_format: 'html', }, files: { - imageFilename: 'imageBlob1' - } + imageFilename: 'imageBlob1', + }, }; - translateTextBackendApiService.suggestTranslatedTextAsync( - 'activeExpId', - 'activeExpVersion', - 'activeContentId', - 'activeStateName', - 'languageCode', - 'contentHtml', - 'translationHtml', - imagesData, - 'html' - ).then(successHandler, failHandler); + translateTextBackendApiService + .suggestTranslatedTextAsync( + 'activeExpId', + 'activeExpVersion', + 'activeContentId', + 'activeStateName', + 'languageCode', + 'contentHtml', + 'translationHtml', + imagesData, + 'html' + ) + .then(successHandler, failHandler); flushMicrotasks(); - const req = httpTestingController.expectOne( - '/suggestionhandler/'); + const req = httpTestingController.expectOne('/suggestionhandler/'); expect(req.request.method).toEqual('POST'); expect(req.request.body.getAll('payload')[0]).toEqual( - JSON.stringify(expectedPayload)); + JSON.stringify(expectedPayload) + ); req.flush({}); flushMicrotasks(); @@ -180,24 +196,27 @@ describe('TranslateTextBackendApiService', () => { it('should append image data to form data', fakeAsync(() => { spyOn( imageLocalStorageService, - 'getFilenameToBase64MappingAsync').and.returnValue( + 'getFilenameToBase64MappingAsync' + ).and.returnValue( Promise.resolve({ - file1: 'imgBase64' - })); - translateTextBackendApiService.suggestTranslatedTextAsync( - 'activeExpId', - 'activeExpVersion', - 'activeContentId', - 'activeStateName', - 'languageCode', - 'contentHtml', - 'translationHtml', - imagesData, - 'html' - ).then(successHandler, failHandler); + file1: 'imgBase64', + }) + ); + translateTextBackendApiService + .suggestTranslatedTextAsync( + 'activeExpId', + 'activeExpVersion', + 'activeContentId', + 'activeStateName', + 'languageCode', + 'contentHtml', + 'translationHtml', + imagesData, + 'html' + ) + .then(successHandler, failHandler); flushMicrotasks(); - const req = httpTestingController.expectOne( - '/suggestionhandler/'); + const req = httpTestingController.expectOne('/suggestionhandler/'); const files = JSON.parse(req.request.body.getAll('payload')[0]).files; expect(req.request.method).toEqual('POST'); expect(files.file1).toContain('imgBase64'); @@ -208,40 +227,46 @@ describe('TranslateTextBackendApiService', () => { })); it('should handle multiple image blobs per filename', fakeAsync(() => { - imagesData = [{ - filename: 'imageFilename1', - imageBlob: { - size: 0, - type: 'imageBlob1' - } as Blob - }, { - filename: 'imageFilename2', - imageBlob: { - size: 0, - type: 'imageBlob2' - } as Blob - }]; + imagesData = [ + { + filename: 'imageFilename1', + imageBlob: { + size: 0, + type: 'imageBlob1', + } as Blob, + }, + { + filename: 'imageFilename2', + imageBlob: { + size: 0, + type: 'imageBlob2', + } as Blob, + }, + ]; spyOn( imageLocalStorageService, - 'getFilenameToBase64MappingAsync').and.returnValue( + 'getFilenameToBase64MappingAsync' + ).and.returnValue( Promise.resolve({ imageFilename1: 'img1Base64', - imageFilename2: 'img2Base64' - })); - translateTextBackendApiService.suggestTranslatedTextAsync( - 'activeExpId', - 'activeExpVersion', - 'activeContentId', - 'activeStateName', - 'languageCode', - 'contentHtml', - 'translationHtml', - imagesData, - 'html' - ).then(successHandler, failHandler); + imageFilename2: 'img2Base64', + }) + ); + translateTextBackendApiService + .suggestTranslatedTextAsync( + 'activeExpId', + 'activeExpVersion', + 'activeContentId', + 'activeStateName', + 'languageCode', + 'contentHtml', + 'translationHtml', + imagesData, + 'html' + ) + .then(successHandler, failHandler); flushMicrotasks(); - const req = httpTestingController.expectOne( - '/suggestionhandler/'); + const req = httpTestingController.expectOne('/suggestionhandler/'); expect(req.request.method).toEqual('POST'); const files = JSON.parse(req.request.body.getAll('payload')[0]).files; expect(files.imageFilename1).toContain('img1Base64'); @@ -252,7 +277,6 @@ describe('TranslateTextBackendApiService', () => { expect(successHandler).toHaveBeenCalled(); })); - it('should call the failhandler on error response', fakeAsync(() => { const errorEvent = new ErrorEvent('error'); failHandler = (error: HttpErrorResponse) => { @@ -260,36 +284,10 @@ describe('TranslateTextBackendApiService', () => { }; spyOn( imageLocalStorageService, - 'getFilenameToBase64MappingAsync').and.returnValue( - Promise.resolve({})); - translateTextBackendApiService.suggestTranslatedTextAsync( - 'activeExpId', - 'activeExpVersion', - 'activeContentId', - 'activeStateName', - 'languageCode', - 'contentHtml', - 'translationHtml', - imagesData, - 'html' - ).then(successHandler, failHandler); - flushMicrotasks(); - const req = httpTestingController.expectOne( - '/suggestionhandler/'); - expect(req.request.method).toEqual('POST'); - req.error(errorEvent); - flushMicrotasks(); - })); - - it('should throw error if Image Data is not present in' + - ' local Storage', async() => { - imagesData = [{ - filename: 'imageFilename1', - imageBlob: null - }]; - - await expectAsync( - translateTextBackendApiService.suggestTranslatedTextAsync( + 'getFilenameToBase64MappingAsync' + ).and.returnValue(Promise.resolve({})); + translateTextBackendApiService + .suggestTranslatedTextAsync( 'activeExpId', 'activeExpVersion', 'activeContentId', @@ -300,14 +298,49 @@ describe('TranslateTextBackendApiService', () => { imagesData, 'html' ) - ).toBeRejectedWithError('No image data found'); - }); + .then(successHandler, failHandler); + flushMicrotasks(); + const req = httpTestingController.expectOne('/suggestionhandler/'); + expect(req.request.method).toEqual('POST'); + req.error(errorEvent); + flushMicrotasks(); + })); - it('should throw error if prefix is invalid', async() => { - imagesData = [{ - filename: 'imageFilename1', - imageBlob: new Blob(['data:random/xyz;base64,Blob1'], {type: 'image'}) - }]; + it( + 'should throw error if Image Data is not present in' + ' local Storage', + async () => { + imagesData = [ + { + filename: 'imageFilename1', + imageBlob: null, + }, + ]; + + await expectAsync( + translateTextBackendApiService.suggestTranslatedTextAsync( + 'activeExpId', + 'activeExpVersion', + 'activeContentId', + 'activeStateName', + 'languageCode', + 'contentHtml', + 'translationHtml', + imagesData, + 'html' + ) + ).toBeRejectedWithError('No image data found'); + } + ); + + it('should throw error if prefix is invalid', async () => { + imagesData = [ + { + filename: 'imageFilename1', + imageBlob: new Blob(['data:random/xyz;base64,Blob1'], { + type: 'image', + }), + }, + ]; await expectAsync( translateTextBackendApiService.suggestTranslatedTextAsync( 'activeExpId', @@ -318,7 +351,8 @@ describe('TranslateTextBackendApiService', () => { 'contentHtml', 'translationHtml', imagesData, - 'html') + 'html' + ) ).toBeRejectedWithError('No valid prefix found in data url'); }); }); diff --git a/core/templates/pages/contributor-dashboard-page/services/translate-text-backend-api.service.ts b/core/templates/pages/contributor-dashboard-page/services/translate-text-backend-api.service.ts index d0b6d8e532b7..327053066e5f 100644 --- a/core/templates/pages/contributor-dashboard-page/services/translate-text-backend-api.service.ts +++ b/core/templates/pages/contributor-dashboard-page/services/translate-text-backend-api.service.ts @@ -16,23 +16,29 @@ * @fileoverview Service for handling user contributed translations. */ -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { TranslatableTexts, TranslatableTextsBackendDict } from 'domain/opportunity/translatable-texts.model'; -import { ImageLocalStorageService, ImagesData } from 'services/image-local-storage.service'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import { + TranslatableTexts, + TranslatableTextsBackendDict, +} from 'domain/opportunity/translatable-texts.model'; +import { + ImageLocalStorageService, + ImagesData, +} from 'services/image-local-storage.service'; interface Data { - 'suggestion_type': string; - 'target_type': string; + suggestion_type: string; + target_type: string; description: string; - 'target_id': string; - 'target_version_at_submission': string; + target_id: string; + target_version_at_submission: string; change_cmd: object; files?: Record; } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class TranslateTextBackendApiService { constructor( @@ -40,24 +46,33 @@ export class TranslateTextBackendApiService { private imageLocalStorageService: ImageLocalStorageService ) {} - async getTranslatableTextsAsync(expId: string, languageCode: string): - Promise { - return this.http.get( - '/gettranslatabletexthandler', { + async getTranslatableTextsAsync( + expId: string, + languageCode: string + ): Promise { + return this.http + .get('/gettranslatabletexthandler', { params: { exp_id: expId, - language_code: languageCode - } - }).toPromise().then((backendDict: TranslatableTextsBackendDict) => { - return TranslatableTexts.createFromBackendDict(backendDict); - }); + language_code: languageCode, + }, + }) + .toPromise() + .then((backendDict: TranslatableTextsBackendDict) => { + return TranslatableTexts.createFromBackendDict(backendDict); + }); } async suggestTranslatedTextAsync( - expId: string, expVersion: string, contentId: string, stateName: string, - languageCode: string, contentHtml: string | string[], - translationHtml: string | string[], imagesData: ImagesData[], - dataFormat: string + expId: string, + expVersion: string, + contentId: string, + stateName: string, + languageCode: string, + contentHtml: string | string[], + translationHtml: string | string[], + imagesData: ImagesData[], + dataFormat: string ): Promise { const postData: Data = { suggestion_type: 'translate_content', @@ -72,18 +87,22 @@ export class TranslateTextBackendApiService { language_code: languageCode, content_html: contentHtml, translation_html: translationHtml, - data_format: dataFormat + data_format: dataFormat, }, - files: ( + files: await this.imageLocalStorageService.getFilenameToBase64MappingAsync( - imagesData)) + imagesData + ), }; const body = new FormData(); body.append('payload', JSON.stringify(postData)); - return this.http.post( - '/suggestionhandler/', body).toPromise(); + return this.http.post('/suggestionhandler/', body).toPromise(); } } -angular.module('oppia').factory('TranslateTextBackendApiService', - downgradeInjectable(TranslateTextBackendApiService)); +angular + .module('oppia') + .factory( + 'TranslateTextBackendApiService', + downgradeInjectable(TranslateTextBackendApiService) + ); diff --git a/core/templates/pages/contributor-dashboard-page/services/translate-text.service.spec.ts b/core/templates/pages/contributor-dashboard-page/services/translate-text.service.spec.ts index 9086c3b93e1d..601d2053fd2b 100644 --- a/core/templates/pages/contributor-dashboard-page/services/translate-text.service.spec.ts +++ b/core/templates/pages/contributor-dashboard-page/services/translate-text.service.spec.ts @@ -16,10 +16,16 @@ * @fileoverview Tests for translate-text service. */ -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; -import { StateAndContent, TranslateTextService } from 'pages/contributor-dashboard-page/services/translate-text.service'; +import { + StateAndContent, + TranslateTextService, +} from 'pages/contributor-dashboard-page/services/translate-text.service'; describe('TranslateTextService', () => { let translateTextService: TranslateTextService; @@ -31,7 +37,7 @@ describe('TranslateTextService', () => { content_value: text, content_type: 'content', interaction_id: null, - rule_type: null + rule_type: null, }; }; @@ -42,8 +48,14 @@ describe('TranslateTextService', () => { httpTestingController = TestBed.inject(HttpTestingController); translateTextService = TestBed.inject(TranslateTextService); stateContent = new StateAndContent( - 'stateName', 'contentId', 'contentText', 'pending', 'translation', - 'html', 'content'); + 'stateName', + 'contentId', + 'contentText', + 'pending', + 'translation', + 'html', + 'content' + ); }); afterEach(() => { @@ -56,17 +68,18 @@ describe('TranslateTextService', () => { const sampleStateWiseContentMapping = { stateName1: { contentId1: getTranslatableItem('text1'), - contentId2: getTranslatableItem('text2') + contentId2: getTranslatableItem('text2'), }, - stateName2: {contentId3: getTranslatableItem('text3')} + stateName2: {contentId3: getTranslatableItem('text3')}, }; translateTextService.init('1', 'en', () => {}); const req = httpTestingController.expectOne( - '/gettranslatabletexthandler?exp_id=1&language_code=en'); + '/gettranslatabletexthandler?exp_id=1&language_code=en' + ); expect(req.request.method).toEqual('GET'); req.flush({ state_names_to_content_id_mapping: sampleStateWiseContentMapping, - version: 1 + version: 1, }); flushMicrotasks(); @@ -78,7 +91,7 @@ describe('TranslateTextService', () => { dataFormat: 'html', contentType: 'content', interactionId: null, - ruleType: null + ruleType: null, }; const expectedTextAndAvailability2 = { @@ -89,7 +102,7 @@ describe('TranslateTextService', () => { dataFormat: 'html', contentType: 'content', interactionId: null, - ruleType: null + ruleType: null, }; const expectedTextAndAvailability1 = { @@ -100,7 +113,7 @@ describe('TranslateTextService', () => { dataFormat: 'html', contentType: 'content', interactionId: null, - ruleType: null + ruleType: null, }; const expectedTextAndPreviousAvailability1 = { @@ -111,7 +124,7 @@ describe('TranslateTextService', () => { dataFormat: 'html', contentType: 'content', interactionId: null, - ruleType: null + ruleType: null, }; textAndAvailability = translateTextService.getTextToTranslate(); @@ -135,72 +148,72 @@ describe('TranslateTextService', () => { expect(textAndAvailability).toEqual(expectedTextAndAvailability3); })); - it('should return no more available for states with no texts', - fakeAsync(() => { - const expectedTextAndAvailability = { - text: 'text1', - more: false, - status: 'pending', - translation: '', - dataFormat: 'html', - contentType: 'content', - interactionId: null, - ruleType: null - }; - const sampleStateWiseContentMapping = { - stateName1: {contentId1: getTranslatableItem('text1')}, - stateName2: {contentId2: getTranslatableItem('')} - }; - translateTextService.init('1', 'en', () => {}); - const req = httpTestingController.expectOne( - '/gettranslatabletexthandler?exp_id=1&language_code=en'); - expect(req.request.method).toEqual('GET'); - req.flush({ - state_names_to_content_id_mapping: sampleStateWiseContentMapping, - version: 1 - }); - flushMicrotasks(); - - const textAndAvailability = translateTextService.getTextToTranslate(); - - expect(textAndAvailability).toEqual(expectedTextAndAvailability); - })); - - it('should return no text or metadata for completely empty states', - fakeAsync(() => { - const expectedTextAndAvailability = { - text: null, - more: false, - status: 'pending', - translation: '', - dataFormat: undefined, - contentType: undefined, - interactionId: undefined, - ruleType: undefined - }; - const sampleStateWiseContentMapping = { - stateName1: {contentId1: getTranslatableItem('')}, - stateName2: {contentId2: getTranslatableItem('')} - }; - translateTextService.init('1', 'en', () => {}); - const req = httpTestingController.expectOne( - '/gettranslatabletexthandler?exp_id=1&language_code=en'); - expect(req.request.method).toEqual('GET'); - req.flush({ - state_names_to_content_id_mapping: sampleStateWiseContentMapping, - version: 1 - }); - flushMicrotasks(); - - const textAndAvailability = translateTextService.getTextToTranslate(); - - expect(textAndAvailability).toEqual(expectedTextAndAvailability); - - const textAndPreviousAvailability = ( - translateTextService.getPreviousTextToTranslate()); - - expect(textAndAvailability).toEqual(textAndPreviousAvailability); - })); + it('should return no more available for states with no texts', fakeAsync(() => { + const expectedTextAndAvailability = { + text: 'text1', + more: false, + status: 'pending', + translation: '', + dataFormat: 'html', + contentType: 'content', + interactionId: null, + ruleType: null, + }; + const sampleStateWiseContentMapping = { + stateName1: {contentId1: getTranslatableItem('text1')}, + stateName2: {contentId2: getTranslatableItem('')}, + }; + translateTextService.init('1', 'en', () => {}); + const req = httpTestingController.expectOne( + '/gettranslatabletexthandler?exp_id=1&language_code=en' + ); + expect(req.request.method).toEqual('GET'); + req.flush({ + state_names_to_content_id_mapping: sampleStateWiseContentMapping, + version: 1, + }); + flushMicrotasks(); + + const textAndAvailability = translateTextService.getTextToTranslate(); + + expect(textAndAvailability).toEqual(expectedTextAndAvailability); + })); + + it('should return no text or metadata for completely empty states', fakeAsync(() => { + const expectedTextAndAvailability = { + text: null, + more: false, + status: 'pending', + translation: '', + dataFormat: undefined, + contentType: undefined, + interactionId: undefined, + ruleType: undefined, + }; + const sampleStateWiseContentMapping = { + stateName1: {contentId1: getTranslatableItem('')}, + stateName2: {contentId2: getTranslatableItem('')}, + }; + translateTextService.init('1', 'en', () => {}); + const req = httpTestingController.expectOne( + '/gettranslatabletexthandler?exp_id=1&language_code=en' + ); + expect(req.request.method).toEqual('GET'); + req.flush({ + state_names_to_content_id_mapping: sampleStateWiseContentMapping, + version: 1, + }); + flushMicrotasks(); + + const textAndAvailability = translateTextService.getTextToTranslate(); + + expect(textAndAvailability).toEqual(expectedTextAndAvailability); + + const textAndPreviousAvailability = + translateTextService.getPreviousTextToTranslate(); + + expect(textAndAvailability).toEqual(textAndPreviousAvailability); + })); }); // Testing setters and getters of StateAndContent class. diff --git a/core/templates/pages/contributor-dashboard-page/services/translate-text.service.ts b/core/templates/pages/contributor-dashboard-page/services/translate-text.service.ts index 6ef31bc9a031..177ea31e24e7 100644 --- a/core/templates/pages/contributor-dashboard-page/services/translate-text.service.ts +++ b/core/templates/pages/contributor-dashboard-page/services/translate-text.service.ts @@ -17,15 +17,15 @@ * fields. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; -import { ImagesData } from 'services/image-local-storage.service'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {ImagesData} from 'services/image-local-storage.service'; -import { TranslateTextBackendApiService } from './translate-text-backend-api.service'; -import { TranslatableTexts } from 'domain/opportunity/translatable-texts.model'; +import {TranslateTextBackendApiService} from './translate-text-backend-api.service'; +import {TranslatableTexts} from 'domain/opportunity/translatable-texts.model'; import { TRANSLATION_DATA_FORMAT_SET_OF_NORMALIZED_STRING, - TRANSLATION_DATA_FORMAT_SET_OF_UNICODE_STRING + TRANSLATION_DATA_FORMAT_SET_OF_UNICODE_STRING, } from 'domain/exploration/WrittenTranslationObjectFactory'; export interface TranslatableItem { @@ -56,7 +56,7 @@ export class StateAndContent { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class TranslateTextService { STARTING_INDEX = -1; @@ -75,9 +75,8 @@ export class TranslateTextService { activeContentStatus: Status; constructor( - private translateTextBackendApiService: - TranslateTextBackendApiService - ) { } + private translateTextBackendApiService: TranslateTextBackendApiService + ) {} private _getNextText(): string | string[] { if (this.stateAndContent.length === 0) { @@ -86,8 +85,7 @@ export class TranslateTextService { this.activeIndex += 1; this.activeStateName = this.stateAndContent[this.activeIndex].stateName; this.activeContentId = this.stateAndContent[this.activeIndex].contentID; - this.activeContentText = ( - this.stateAndContent[this.activeIndex].contentText); + this.activeContentText = this.stateAndContent[this.activeIndex].contentText; return this.activeContentText; } @@ -110,7 +108,7 @@ export class TranslateTextService { if (this.stateAndContent.length === 0) { return false; } - return (this.activeIndex + 1 < this.stateAndContent.length); + return this.activeIndex + 1 < this.stateAndContent.length; } private _isSetDataFormat(dataFormat: string): boolean { @@ -121,16 +119,16 @@ export class TranslateTextService { } private _getUpdatedTextToTranslate( - text: string | string[], - more: boolean, - status: Status, - translation: string + text: string | string[], + more: boolean, + status: Status, + translation: string ): TranslatableItem { const { dataFormat, contentType, interactionId, - ruleType + ruleType, }: { dataFormat?: string; contentType?: string; @@ -145,7 +143,7 @@ export class TranslateTextService { dataFormat: dataFormat, contentType: contentType, interactionId: interactionId, - ruleType: ruleType + ruleType: ruleType, }; } @@ -159,42 +157,44 @@ export class TranslateTextService { this.activeContentText = null; this.activeContentStatus = this.PENDING as Status; this.activeExpId = expId; - this.translateTextBackendApiService.getTranslatableTextsAsync( - expId, languageCode).then((translatableTexts: TranslatableTexts) => { - this.stateWiseContents = translatableTexts.stateWiseContents; - this.activeExpVersion = translatableTexts.explorationVersion; - for (const stateName in this.stateWiseContents) { - let stateHasText: boolean = false; - const contentIds = []; - const contentIdToContentMapping = this.stateWiseContents[stateName]; - for (const contentId in contentIdToContentMapping) { - const translatableItem = contentIdToContentMapping[contentId]; - if (translatableItem.content === '') { - continue; + this.translateTextBackendApiService + .getTranslatableTextsAsync(expId, languageCode) + .then((translatableTexts: TranslatableTexts) => { + this.stateWiseContents = translatableTexts.stateWiseContents; + this.activeExpVersion = translatableTexts.explorationVersion; + for (const stateName in this.stateWiseContents) { + let stateHasText: boolean = false; + const contentIds = []; + const contentIdToContentMapping = this.stateWiseContents[stateName]; + for (const contentId in contentIdToContentMapping) { + const translatableItem = contentIdToContentMapping[contentId]; + if (translatableItem.content === '') { + continue; + } + contentIds.push(contentId); + this.stateAndContent.push( + new StateAndContent( + stateName, + contentId, + translatableItem.content, + this.PENDING as Status, + this._isSetDataFormat(translatableItem.dataFormat) ? [] : '', + translatableItem.dataFormat, + translatableItem.contentType, + translatableItem.interactionId, + translatableItem.ruleType + ) + ); + stateHasText = true; } - contentIds.push(contentId); - this.stateAndContent.push( - new StateAndContent( - stateName, contentId, - translatableItem.content, - this.PENDING as Status, - this._isSetDataFormat(translatableItem.dataFormat) ? [] : '', - translatableItem.dataFormat, - translatableItem.contentType, - translatableItem.interactionId, - translatableItem.ruleType - ) - ); - stateHasText = true; - } - if (stateHasText) { - this.stateNamesList.push(stateName); - this.stateWiseContentIds[stateName] = contentIds; + if (stateHasText) { + this.stateNamesList.push(stateName); + this.stateWiseContentIds[stateName] = contentIds; + } } - } - successCallback(); - }); + successCallback(); + }); } getActiveIndex(): number { @@ -203,51 +203,66 @@ export class TranslateTextService { getTextToTranslate(): TranslatableItem { const text = this._getNextText(); - const { - status = this.PENDING, - translation = '' - } = { ...this.stateAndContent[this.activeIndex] }; + const {status = this.PENDING, translation = ''} = { + ...this.stateAndContent[this.activeIndex], + }; return this._getUpdatedTextToTranslate( - text, this._isMoreTextAvailableForTranslation(), status, translation); + text, + this._isMoreTextAvailableForTranslation(), + status, + translation + ); } getPreviousTextToTranslate(): TranslatableItem { const text = this._getPreviousText(); - const { - status = this.PENDING, - translation = '' - } = { ...this.stateAndContent[this.activeIndex] }; + const {status = this.PENDING, translation = ''} = { + ...this.stateAndContent[this.activeIndex], + }; return this._getUpdatedTextToTranslate( - text, this._isPreviousTextAvailableForTranslation(), status, translation); + text, + this._isPreviousTextAvailableForTranslation(), + status, + translation + ); } suggestTranslatedText( - translation: string | string[], languageCode: string, imagesData: - ImagesData[], dataFormat: string, successCallback: () => void, - errorCallback: (reason: string) => void): void { - this.translateTextBackendApiService.suggestTranslatedTextAsync( - this.activeExpId, - this.activeExpVersion, - this.activeContentId, - this.activeStateName, - languageCode, - this.stateWiseContents[ - this.activeStateName][this.activeContentId].content, - translation, - imagesData, - dataFormat - ).then(() => { - this.stateAndContent[this.activeIndex].status = this.SUBMITTED; - this.stateAndContent[this.activeIndex].translation = ( - translation); - successCallback(); - }, errorResponse => { - if (errorCallback) { - errorCallback(errorResponse.error.error); - } - }); + translation: string | string[], + languageCode: string, + imagesData: ImagesData[], + dataFormat: string, + successCallback: () => void, + errorCallback: (reason: string) => void + ): void { + this.translateTextBackendApiService + .suggestTranslatedTextAsync( + this.activeExpId, + this.activeExpVersion, + this.activeContentId, + this.activeStateName, + languageCode, + this.stateWiseContents[this.activeStateName][this.activeContentId] + .content, + translation, + imagesData, + dataFormat + ) + .then( + () => { + this.stateAndContent[this.activeIndex].status = this.SUBMITTED; + this.stateAndContent[this.activeIndex].translation = translation; + successCallback(); + }, + errorResponse => { + if (errorCallback) { + errorCallback(errorResponse.error.error); + } + } + ); } } -angular.module('oppia').factory( - 'TranslateTextService', downgradeInjectable(TranslateTextService)); +angular + .module('oppia') + .factory('TranslateTextService', downgradeInjectable(TranslateTextService)); diff --git a/core/templates/pages/contributor-dashboard-page/translation-language-selector/review-translation-language-selector.component.spec.ts b/core/templates/pages/contributor-dashboard-page/translation-language-selector/review-translation-language-selector.component.spec.ts index 053a90de8194..ff312ff89d17 100644 --- a/core/templates/pages/contributor-dashboard-page/translation-language-selector/review-translation-language-selector.component.spec.ts +++ b/core/templates/pages/contributor-dashboard-page/translation-language-selector/review-translation-language-selector.component.spec.ts @@ -16,20 +16,29 @@ * @fileoverview Unit tests for the translation language selector component. */ -import { ComponentFixture, fakeAsync, tick, flush, TestBed, waitForAsync } from '@angular/core/testing'; - -import { ReviewTranslationLanguageSelectorComponent } from +import { + ComponentFixture, + fakeAsync, + tick, + flush, + TestBed, + waitForAsync, +} from '@angular/core/testing'; + +import { + ReviewTranslationLanguageSelectorComponent, // eslint-disable-next-line max-len - 'pages/contributor-dashboard-page/translation-language-selector/review-translation-language-selector.component'; -import { ContributionOpportunitiesBackendApiService } from +} from 'pages/contributor-dashboard-page/translation-language-selector/review-translation-language-selector.component'; +import { + ContributionOpportunitiesBackendApiService, // eslint-disable-next-line max-len - 'pages/contributor-dashboard-page/services/contribution-opportunities-backend-api.service'; -import { TranslationLanguageService } from 'pages/exploration-editor-page/translation-tab/services/translation-language.service'; -import { UserService } from 'services/user.service'; -import { ElementRef, EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { FormsModule } from '@angular/forms'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; +} from 'pages/contributor-dashboard-page/services/contribution-opportunities-backend-api.service'; +import {TranslationLanguageService} from 'pages/exploration-editor-page/translation-tab/services/translation-language.service'; +import {UserService} from 'services/user.service'; +import {ElementRef, EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import {AppConstants} from 'app.constants'; +import {FormsModule} from '@angular/forms'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; describe('Review Translation language selector', () => { let component: ReviewTranslationLanguageSelectorComponent; @@ -40,16 +49,15 @@ describe('Review Translation language selector', () => { let preferredLanguageCode = 'en'; - const contributionOpportunitiesBackendApiServiceStub: - Partial = { - getPreferredTranslationLanguageAsync: async() => { + const contributionOpportunitiesBackendApiServiceStub: Partial = + { + getPreferredTranslationLanguageAsync: async () => { if (preferredLanguageCode) { component.populateLanguageSelection(preferredLanguageCode); } return Promise.resolve(preferredLanguageCode); }, - savePreferredTranslationLanguageAsync: async() => - Promise.resolve() + savePreferredTranslationLanguageAsync: async () => Promise.resolve(), }; let clickDropdown: () => void; @@ -57,32 +65,32 @@ describe('Review Translation language selector', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - FormsModule, - ], + imports: [HttpClientTestingModule, FormsModule], declarations: [ReviewTranslationLanguageSelectorComponent], providers: [ TranslationLanguageService, UserService, { provide: ContributionOpportunitiesBackendApiService, - useValue: contributionOpportunitiesBackendApiServiceStub - } + useValue: contributionOpportunitiesBackendApiServiceStub, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent( - ReviewTranslationLanguageSelectorComponent); + ReviewTranslationLanguageSelectorComponent + ); translationLanguageService = TestBed.inject(TranslationLanguageService); userService = TestBed.inject(UserService); component = fixture.componentInstance; component.activeLanguageCode = 'en'; - spyOnProperty(translationLanguageService, 'onActiveLanguageChanged').and - .returnValue(activeLanguageChangedEmitter); + spyOnProperty( + translationLanguageService, + 'onActiveLanguageChanged' + ).and.returnValue(activeLanguageChangedEmitter); fixture.detectChanges(); }); @@ -90,7 +98,8 @@ describe('Review Translation language selector', () => { clickDropdown = () => { fixture.debugElement.nativeElement .querySelector( - '.oppia-review-translation-language-selector-inner-container') + '.oppia-review-translation-language-selector-inner-container' + ) .click(); fixture.detectChanges(); flush(); @@ -98,7 +107,8 @@ describe('Review Translation language selector', () => { getDropdownOptionsContainer = () => { return fixture.debugElement.nativeElement.querySelector( - '.oppia-review-translation-language-selector-dropdown-container'); + '.oppia-review-translation-language-selector-dropdown-container' + ); }; }); @@ -113,43 +123,50 @@ describe('Review Translation language selector', () => { beforeEach(fakeAsync(() => { spyOn(userService, 'getUserContributionRightsDataAsync').and.resolveTo({ can_suggest_questions: false, - can_review_translation_for_language_codes: ( - translationReviewerLanguageCodes), + can_review_translation_for_language_codes: + translationReviewerLanguageCodes, can_review_voiceover_for_language_codes: [], - can_review_questions: false + can_review_questions: false, }); component.ngOnInit(); tick(); fixture.detectChanges(); })); - it('should initialize languageIdToDescription map to user\'s reviewable ' + - 'languages', () => { - const reviewableLanguageDescriptions = { - en: 'English', - es: 'español (Spanish)', - fr: 'français (French)' - }; - - expect(component.languageIdToDescription).toEqual( - reviewableLanguageDescriptions); + it( + "should initialize languageIdToDescription map to user's reviewable " + + 'languages', + () => { + const reviewableLanguageDescriptions = { + en: 'English', + es: 'español (Spanish)', + fr: 'français (French)', + }; + + expect(component.languageIdToDescription).toEqual( + reviewableLanguageDescriptions + ); + } + ); + + it('should initialize selected language to activeLanguageCode input', () => { + const dropdown = fixture.nativeElement.querySelector( + '.oppia-review-translation-language-selector-inner-container' + ); + + expect(dropdown.firstChild.textContent.trim()).toBe('English'); }); - it('should initialize selected language to activeLanguageCode input', + it( + 'should only show language options for which the user can review' + + ' translations', () => { - const dropdown = ( - fixture.nativeElement.querySelector( - '.oppia-review-translation-language-selector-inner-container')); - - expect(dropdown.firstChild.textContent.trim()).toBe('English'); - }); - - it('should only show language options for which the user can review' + - ' translations', () => { - expect(component.options.map(opt => opt.id)).toEqual( - translationReviewerLanguageCodes); - expect(component.filteredOptions).toEqual(component.options); - }); + expect(component.options.map(opt => opt.id)).toEqual( + translationReviewerLanguageCodes + ); + expect(component.filteredOptions).toEqual(component.options); + } + ); it('should correctly show and hide the dropdown', fakeAsync(() => { expect(component.dropdownShown).toBe(false); @@ -168,21 +185,23 @@ describe('Review Translation language selector', () => { expect(getDropdownOptionsContainer()).toBeTruthy(); })); - it('should hide the dropdown when open and the user clicks outside' + - ' the dropdown', () => { - const fakeClickAwayEvent = new MouseEvent('click'); - Object.defineProperty( - fakeClickAwayEvent, - 'target', - {value: document.createElement('div')}); - component.dropdownShown = true; + it( + 'should hide the dropdown when open and the user clicks outside' + + ' the dropdown', + () => { + const fakeClickAwayEvent = new MouseEvent('click'); + Object.defineProperty(fakeClickAwayEvent, 'target', { + value: document.createElement('div'), + }); + component.dropdownShown = true; - component.onDocumentClick(fakeClickAwayEvent); - fixture.detectChanges(); + component.onDocumentClick(fakeClickAwayEvent); + fixture.detectChanges(); - expect(component.dropdownShown).toBe(false); - expect(getDropdownOptionsContainer()).toBeFalsy(); - }); + expect(component.dropdownShown).toBe(false); + expect(getDropdownOptionsContainer()).toBeFalsy(); + } + ); it('should set active language code to selected option', () => { spyOn(component.setActiveLanguageCode, 'emit'); @@ -193,87 +212,103 @@ describe('Review Translation language selector', () => { expect(component.setActiveLanguageCode.emit).toHaveBeenCalledWith('fr'); }); - it('should display the selected language when the language is already' + - ' selected', () => { - component.activeLanguageCode = 'en'; - - component.ngOnInit(); - - expect(component.languageSelection).toBe('English'); - expect(component.activeLanguageCode).toBe('en'); - }); - - it('should display the preferred language when the preferred' + - ' language is defined', fakeAsync(() => { - component.activeLanguageCode = null; - component.languageSelection = ''; - preferredLanguageCode = 'en'; - const languageDescription = AppConstants.SUPPORTED_AUDIO_LANGUAGES.find( - e => e.id === 'en')?.description ?? ''; - - spyOn(component.setActiveLanguageCode, 'emit').and.callFake( - (languageCode: string) => { - component.activeLanguageCode = languageCode; - }); + it( + 'should display the selected language when the language is already' + + ' selected', + () => { + component.activeLanguageCode = 'en'; - component.ngOnInit(); + component.ngOnInit(); - fixture.whenStable().then(() => { - fixture.detectChanges(); - expect(component.setActiveLanguageCode.emit) - .toHaveBeenCalledWith(preferredLanguageCode); - expect(component.activeLanguageCode).toBe(preferredLanguageCode); - expect(component.languageSelection).toBe(languageDescription); - }); - })); + expect(component.languageSelection).toBe('English'); + expect(component.activeLanguageCode).toBe('en'); + } + ); + + it( + 'should display the preferred language when the preferred' + + ' language is defined', + fakeAsync(() => { + component.activeLanguageCode = null; + component.languageSelection = ''; + preferredLanguageCode = 'en'; + const languageDescription = + AppConstants.SUPPORTED_AUDIO_LANGUAGES.find(e => e.id === 'en') + ?.description ?? ''; + + spyOn(component.setActiveLanguageCode, 'emit').and.callFake( + (languageCode: string) => { + component.activeLanguageCode = languageCode; + } + ); - it('should ask user to select a language when the preferred' + - ' language is not defined', fakeAsync(() => { - preferredLanguageCode = ''; - component.activeLanguageCode = null; - component.languageSelection = 'Language'; + component.ngOnInit(); - component.ngOnInit(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(component.setActiveLanguageCode.emit).toHaveBeenCalledWith( + preferredLanguageCode + ); + expect(component.activeLanguageCode).toBe(preferredLanguageCode); + expect(component.languageSelection).toBe(languageDescription); + }); + }) + ); - fixture.detectChanges(); - expect(component.languageSelection).toBe('Language'); - expect(component.activeLanguageCode).toBe(null); - })); + it( + 'should ask user to select a language when the preferred' + + ' language is not defined', + fakeAsync(() => { + preferredLanguageCode = ''; + component.activeLanguageCode = null; + component.languageSelection = 'Language'; - it('should show the correct language when the language is changed' - , () => { - expect(component.languageSelection).toBe('English'); component.ngOnInit(); - spyOn( - translationLanguageService, 'getActiveLanguageCode').and.returnValue( - 'fr'); - activeLanguageChangedEmitter.emit(); - - expect(component.languageSelection).toBe('français (French)'); - }); + fixture.detectChanges(); + expect(component.languageSelection).toBe('Language'); + expect(component.activeLanguageCode).toBe(null); + }) + ); - it('should indicate selection and save the language' + - ' on selecting a new language', () => { - const selectedLanguage = 'fr'; - spyOn(component.setActiveLanguageCode, 'emit'); + it('should show the correct language when the language is changed', () => { + expect(component.languageSelection).toBe('English'); + component.ngOnInit(); spyOn( - contributionOpportunitiesBackendApiServiceStub, - 'savePreferredTranslationLanguageAsync' as never); + translationLanguageService, + 'getActiveLanguageCode' + ).and.returnValue('fr'); - component.selectOption(selectedLanguage); + activeLanguageChangedEmitter.emit(); - fixture.whenStable().then(() => { - fixture.detectChanges(); - expect(component.setActiveLanguageCode.emit).toHaveBeenCalledWith( - selectedLanguage); - expect( - contributionOpportunitiesBackendApiServiceStub - .savePreferredTranslationLanguageAsync).toHaveBeenCalledWith( - selectedLanguage); - }); + expect(component.languageSelection).toBe('français (French)'); }); + it( + 'should indicate selection and save the language' + + ' on selecting a new language', + () => { + const selectedLanguage = 'fr'; + spyOn(component.setActiveLanguageCode, 'emit'); + spyOn( + contributionOpportunitiesBackendApiServiceStub, + 'savePreferredTranslationLanguageAsync' as never + ); + + component.selectOption(selectedLanguage); + + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(component.setActiveLanguageCode.emit).toHaveBeenCalledWith( + selectedLanguage + ); + expect( + contributionOpportunitiesBackendApiServiceStub.savePreferredTranslationLanguageAsync + ).toHaveBeenCalledWith(selectedLanguage); + }); + } + ); + it('should toggle dropdown', fakeAsync(() => { component.filterDivRef = new ElementRef(document.createElement('div')); spyOn(component.filterDivRef.nativeElement, 'focus'); @@ -288,10 +323,14 @@ describe('Review Translation language selector', () => { expect(component.dropdownShown).toBe(true); expect(component.optionsFilter).toBe(''); expect(component.filteredOptions).toBe(component.options); - expect(component.filteredOptions).toContain( - {id: 'es', description: 'español (Spanish)'}); - expect(component.filteredOptions).toContain( - {id: 'fr', description: 'français (French)'}); + expect(component.filteredOptions).toContain({ + id: 'es', + description: 'español (Spanish)', + }); + expect(component.filteredOptions).toContain({ + id: 'fr', + description: 'français (French)', + }); expect(component.filterDivRef.nativeElement.focus).toHaveBeenCalled(); // Type a filter query. @@ -300,10 +339,14 @@ describe('Review Translation language selector', () => { fixture.detectChanges(); expect(component.filteredOptions).not.toBe(component.options); - expect(component.filteredOptions).toContain( - {id: 'es', description: 'español (Spanish)'}); - expect(component.filteredOptions).not.toContain( - {id: 'fr', description: 'français (French)'}); + expect(component.filteredOptions).toContain({ + id: 'es', + description: 'español (Spanish)', + }); + expect(component.filteredOptions).not.toContain({ + id: 'fr', + description: 'français (French)', + }); // Close the dropdown. component.toggleDropdown(); @@ -320,37 +363,49 @@ describe('Review Translation language selector', () => { expect(component.dropdownShown).toBe(true); expect(component.optionsFilter).toBe(''); expect(component.filteredOptions).toBe(component.options); - expect(component.filteredOptions).toContain( - {id: 'es', description: 'español (Spanish)'}); - expect(component.filteredOptions).toContain( - {id: 'fr', description: 'français (French)'}); + expect(component.filteredOptions).toContain({ + id: 'es', + description: 'español (Spanish)', + }); + expect(component.filteredOptions).toContain({ + id: 'fr', + description: 'français (French)', + }); expect(component.filterDivRef.nativeElement.focus).toHaveBeenCalled(); })); it('should filter language options based on the filter text', () => { // Expect the full list of languages to be contained. Adding just 3 here // as the list of languages may grow overtime. - expect(component.filteredOptions).toContain( - {id: 'en', description: 'English'}); - expect(component.filteredOptions).toContain( - {id: 'es', description: 'español (Spanish)'}); - expect(component.filteredOptions).toContain( - {id: 'fr', description: 'français (French)'}); + expect(component.filteredOptions).toContain({ + id: 'en', + description: 'English', + }); + expect(component.filteredOptions).toContain({ + id: 'es', + description: 'español (Spanish)', + }); + expect(component.filteredOptions).toContain({ + id: 'fr', + description: 'français (French)', + }); component.optionsFilter = 'sp'; component.filterOptions(); fixture.detectChanges(); // Expect it to contain Spanish but not any of the other languages. - expect(component.filteredOptions).toEqual( - [{id: 'es', description: 'español (Spanish)'}]); + expect(component.filteredOptions).toEqual([ + {id: 'es', description: 'español (Spanish)'}, + ]); }); }); describe('when the reviewer translation rights are not found', () => { it('should throw an error', fakeAsync(() => { - spyOn(userService, 'getUserContributionRightsDataAsync').and - .resolveTo(null); + spyOn(userService, 'getUserContributionRightsDataAsync').and.resolveTo( + null + ); expect(() => { component.ngOnInit(); diff --git a/core/templates/pages/contributor-dashboard-page/translation-language-selector/review-translation-language-selector.component.ts b/core/templates/pages/contributor-dashboard-page/translation-language-selector/review-translation-language-selector.component.ts index 4c8b7536049a..cbdcb5eafa2c 100644 --- a/core/templates/pages/contributor-dashboard-page/translation-language-selector/review-translation-language-selector.component.ts +++ b/core/templates/pages/contributor-dashboard-page/translation-language-selector/review-translation-language-selector.component.ts @@ -17,17 +17,24 @@ */ import { - Component, OnInit, Input, Output, EventEmitter, HostListener, ViewChild, - ElementRef + Component, + OnInit, + Input, + Output, + EventEmitter, + HostListener, + ViewChild, + ElementRef, } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {downgradeComponent} from '@angular/upgrade/static'; -import { ContributionOpportunitiesBackendApiService } from +import { + ContributionOpportunitiesBackendApiService, // eslint-disable-next-line max-len - 'pages/contributor-dashboard-page/services/contribution-opportunities-backend-api.service'; -import { LanguageUtilService } from 'domain/utilities/language-util.service'; -import { TranslationLanguageService } from 'pages/exploration-editor-page/translation-tab/services/translation-language.service'; -import { UserService } from 'services/user.service'; +} from 'pages/contributor-dashboard-page/services/contribution-opportunities-backend-api.service'; +import {LanguageUtilService} from 'domain/utilities/language-util.service'; +import {TranslationLanguageService} from 'pages/exploration-editor-page/translation-tab/services/translation-language.service'; +import {UserService} from 'services/user.service'; interface Options { id: string; @@ -36,7 +43,7 @@ interface Options { @Component({ selector: 'review-translation-language-selector', - templateUrl: './review-translation-language-selector.component.html' + templateUrl: './review-translation-language-selector.component.html', }) export class ReviewTranslationLanguageSelectorComponent implements OnInit { // These properties are initialized using Angular lifecycle hooks @@ -44,7 +51,7 @@ export class ReviewTranslationLanguageSelectorComponent implements OnInit { // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @Input() activeLanguageCode!: string | null; @Output() setActiveLanguageCode: EventEmitter = new EventEmitter(); - @ViewChild('dropdown', {'static': false}) dropdownRef!: ElementRef; + @ViewChild('dropdown', {static: false}) dropdownRef!: ElementRef; @ViewChild('filterDiv') filterDivRef!: ElementRef; options!: Options[]; @@ -56,48 +63,50 @@ export class ReviewTranslationLanguageSelectorComponent implements OnInit { dropdownShown = false; constructor( - private contributionOpportunitiesBackendApiService: - ContributionOpportunitiesBackendApiService, + private contributionOpportunitiesBackendApiService: ContributionOpportunitiesBackendApiService, private languageUtilService: LanguageUtilService, private readonly translationLanguageService: TranslationLanguageService, - private userService: UserService, + private userService: UserService ) {} ngOnInit(): void { - this.translationLanguageService.onActiveLanguageChanged.subscribe( - () => { - this.languageSelection = this.languageIdToDescription[ - this.translationLanguageService.getActiveLanguageCode()]; - }); - - this.userService.getUserContributionRightsDataAsync() + this.translationLanguageService.onActiveLanguageChanged.subscribe(() => { + this.languageSelection = + this.languageIdToDescription[ + this.translationLanguageService.getActiveLanguageCode() + ]; + }); + + this.userService + .getUserContributionRightsDataAsync() .then(userContributionRights => { if (!userContributionRights) { throw new Error('User contribution rights not found.'); } - this.filteredOptions = this.options = userContributionRights - .can_review_translation_for_language_codes.map(languageCode => { - const description = this.languageUtilService - .getAudioLanguageDescription(languageCode); - this.languageIdToDescription[languageCode] = description; - return { id: languageCode, description }; - }); + this.filteredOptions = this.options = + userContributionRights.can_review_translation_for_language_codes.map( + languageCode => { + const description = + this.languageUtilService.getAudioLanguageDescription( + languageCode + ); + this.languageIdToDescription[languageCode] = description; + return {id: languageCode, description}; + } + ); if (this.activeLanguageCode) { - this.languageSelection = this.languageIdToDescription[ - this.activeLanguageCode]; + this.languageSelection = + this.languageIdToDescription[this.activeLanguageCode]; } }); this.contributionOpportunitiesBackendApiService .getPreferredTranslationLanguageAsync() - .then((preferredLanguageCode: string|null) => { - if ( - preferredLanguageCode && this.languageSelection === 'Language' - ) { - this.populateLanguageSelection( - preferredLanguageCode); + .then((preferredLanguageCode: string | null) => { + if (preferredLanguageCode && this.languageSelection === 'Language') { + this.populateLanguageSelection(preferredLanguageCode); } }); } @@ -109,22 +118,21 @@ export class ReviewTranslationLanguageSelectorComponent implements OnInit { this.filteredOptions = this.options; setTimeout(() => { this.filterDivRef.nativeElement.focus(); - } - , 1); + }, 1); } } populateLanguageSelection(languageCode: string): void { this.setActiveLanguageCode.emit(languageCode); - this.languageSelection = ( - this.languageIdToDescription[languageCode]); + this.languageSelection = this.languageIdToDescription[languageCode]; } selectOption(activeLanguageCode: string): void { this.populateLanguageSelection(activeLanguageCode); this.dropdownShown = false; - this.contributionOpportunitiesBackendApiService - .savePreferredTranslationLanguageAsync(activeLanguageCode); + this.contributionOpportunitiesBackendApiService.savePreferredTranslationLanguageAsync( + activeLanguageCode + ); } /** @@ -143,9 +151,11 @@ export class ReviewTranslationLanguageSelectorComponent implements OnInit { } filterOptions(): void { - this.filteredOptions = this.options.filter( - option => option.description.toLowerCase().includes( - this.optionsFilter.toLowerCase())); + this.filteredOptions = this.options.filter(option => + option.description + .toLowerCase() + .includes(this.optionsFilter.toLowerCase()) + ); } } @@ -154,5 +164,6 @@ angular.module('oppia').directive( downgradeComponent({ component: ReviewTranslationLanguageSelectorComponent, inputs: ['activeLanguageCode'], - outputs: ['setActiveLanguageCode'] - })); + outputs: ['setActiveLanguageCode'], + }) +); diff --git a/core/templates/pages/contributor-dashboard-page/translation-language-selector/translation-language-selector.component.spec.ts b/core/templates/pages/contributor-dashboard-page/translation-language-selector/translation-language-selector.component.spec.ts index 48dcaa8afc2d..22cc5027cd33 100644 --- a/core/templates/pages/contributor-dashboard-page/translation-language-selector/translation-language-selector.component.spec.ts +++ b/core/templates/pages/contributor-dashboard-page/translation-language-selector/translation-language-selector.component.spec.ts @@ -16,19 +16,27 @@ * @fileoverview Unit tests for the translation language selector component. */ -import { ComponentFixture, fakeAsync, flush, TestBed, waitForAsync } from '@angular/core/testing'; - -import { TranslationLanguageSelectorComponent } from +import { + ComponentFixture, + fakeAsync, + flush, + TestBed, + waitForAsync, +} from '@angular/core/testing'; + +import { + TranslationLanguageSelectorComponent, // eslint-disable-next-line max-len - 'pages/contributor-dashboard-page/translation-language-selector/translation-language-selector.component'; -import { ContributionOpportunitiesBackendApiService } from +} from 'pages/contributor-dashboard-page/translation-language-selector/translation-language-selector.component'; +import { + ContributionOpportunitiesBackendApiService, // eslint-disable-next-line max-len - 'pages/contributor-dashboard-page/services/contribution-opportunities-backend-api.service'; -import { FeaturedTranslationLanguage } from 'domain/opportunity/featured-translation-language.model'; -import { TranslationLanguageService } from 'pages/exploration-editor-page/translation-tab/services/translation-language.service'; -import { ElementRef, EventEmitter } from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { FormsModule } from '@angular/forms'; +} from 'pages/contributor-dashboard-page/services/contribution-opportunities-backend-api.service'; +import {FeaturedTranslationLanguage} from 'domain/opportunity/featured-translation-language.model'; +import {TranslationLanguageService} from 'pages/exploration-editor-page/translation-tab/services/translation-language.service'; +import {ElementRef, EventEmitter} from '@angular/core'; +import {AppConstants} from 'app.constants'; +import {FormsModule} from '@angular/forms'; describe('Translation language selector', () => { let component: TranslationLanguageSelectorComponent; @@ -39,27 +47,26 @@ describe('Translation language selector', () => { let featuredLanguages = [ FeaturedTranslationLanguage.createFromBackendDict({ language_code: 'fr', - explanation: 'Partnership with ABC' + explanation: 'Partnership with ABC', }), FeaturedTranslationLanguage.createFromBackendDict({ language_code: 'de', - explanation: 'Partnership with CBA' - }) + explanation: 'Partnership with CBA', + }), ]; let preferredLanguageCode = 'en'; - let contributionOpportunitiesBackendApiServiceStub: - Partial = { - fetchFeaturedTranslationLanguagesAsync: async() => + let contributionOpportunitiesBackendApiServiceStub: Partial = + { + fetchFeaturedTranslationLanguagesAsync: async () => Promise.resolve(featuredLanguages), - getPreferredTranslationLanguageAsync: async() => { + getPreferredTranslationLanguageAsync: async () => { if (preferredLanguageCode) { component.populateLanguageSelection(preferredLanguageCode); } return Promise.resolve(preferredLanguageCode); }, - savePreferredTranslationLanguageAsync: async() => - Promise.resolve() + savePreferredTranslationLanguageAsync: async () => Promise.resolve(), }; let clickDropdown: () => void; @@ -67,14 +74,14 @@ describe('Translation language selector', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - FormsModule, - ], + imports: [FormsModule], declarations: [TranslationLanguageSelectorComponent], - providers: [{ - provide: ContributionOpportunitiesBackendApiService, - useValue: contributionOpportunitiesBackendApiServiceStub - }] + providers: [ + { + provide: ContributionOpportunitiesBackendApiService, + useValue: contributionOpportunitiesBackendApiServiceStub, + }, + ], }).compileComponents(); })); @@ -83,8 +90,10 @@ describe('Translation language selector', () => { translationLanguageService = TestBed.inject(TranslationLanguageService); component = fixture.componentInstance; component.activeLanguageCode = 'en'; - spyOnProperty(translationLanguageService, 'onActiveLanguageChanged').and - .returnValue(activeLanguageChangedEmitter); + spyOnProperty( + translationLanguageService, + 'onActiveLanguageChanged' + ).and.returnValue(activeLanguageChangedEmitter); fixture.detectChanges(); }); @@ -99,7 +108,8 @@ describe('Translation language selector', () => { getDropdownOptionsContainer = () => { return fixture.debugElement.nativeElement.querySelector( - '.oppia-translation-language-selector-dropdown-container'); + '.oppia-translation-language-selector-dropdown-container' + ); }; }); @@ -121,9 +131,9 @@ describe('Translation language selector', () => { })); it('should correctly initialize dropdown activeLanguageCode', () => { - const dropdown = ( - fixture.nativeElement.querySelector( - '.oppia-translation-language-selector-inner-container')); + const dropdown = fixture.nativeElement.querySelector( + '.oppia-translation-language-selector-inner-container' + ); expect(dropdown.firstChild.textContent.trim()).toBe('English'); }); @@ -145,10 +155,9 @@ describe('Translation language selector', () => { expect(getDropdownOptionsContainer()).toBeTruthy(); let fakeClickAwayEvent = new MouseEvent('click'); - Object.defineProperty( - fakeClickAwayEvent, - 'target', - {value: document.createElement('div')}); + Object.defineProperty(fakeClickAwayEvent, 'target', { + value: document.createElement('div'), + }); component.onDocumentClick(fakeClickAwayEvent); fixture.detectChanges(); expect(component.dropdownShown).toBe(false); @@ -173,8 +182,7 @@ describe('Translation language selector', () => { component.showExplanationPopup(0); fixture.detectChanges(); - expect(component.explanationPopupContent) - .toBe('Partnership with ABC'); + expect(component.explanationPopupContent).toBe('Partnership with ABC'); expect(component.explanationPopupShown).toBe(true); component.hideExplanationPopup(); @@ -183,87 +191,102 @@ describe('Translation language selector', () => { }); })); - it('should display the selected language when the language is already' + - ' selected', () => { - component.activeLanguageCode = 'en'; + it( + 'should display the selected language when the language is already' + + ' selected', + () => { + component.activeLanguageCode = 'en'; - component.ngOnInit(); + component.ngOnInit(); - expect(component.languageSelection).toBe('English'); - expect(component.activeLanguageCode).toBe('en'); - }); + expect(component.languageSelection).toBe('English'); + expect(component.activeLanguageCode).toBe('en'); + } + ); + + it( + 'should display the preferred language when the preferred' + + ' language is defined', + fakeAsync(() => { + component.activeLanguageCode = null; + component.languageSelection = ''; + preferredLanguageCode = 'en'; + const languageDescription = + AppConstants.SUPPORTED_AUDIO_LANGUAGES.find(e => e.id === 'en') + ?.description ?? ''; + + spyOn(component.setActiveLanguageCode, 'emit').and.callFake( + (languageCode: string) => { + component.activeLanguageCode = languageCode; + } + ); - it('should display the preferred language when the preferred' + - ' language is defined', fakeAsync(() => { - component.activeLanguageCode = null; - component.languageSelection = ''; - preferredLanguageCode = 'en'; - const languageDescription = AppConstants.SUPPORTED_AUDIO_LANGUAGES.find( - e => e.id === 'en')?.description ?? ''; + component.ngOnInit(); - spyOn(component.setActiveLanguageCode, 'emit').and.callFake( - (languageCode: string) => { - component.activeLanguageCode = languageCode; + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(component.setActiveLanguageCode.emit).toHaveBeenCalledWith( + preferredLanguageCode + ); + expect(component.activeLanguageCode).toBe(preferredLanguageCode); + expect(component.languageSelection).toBe(languageDescription); }); + }) + ); - component.ngOnInit(); - - fixture.whenStable().then(() => { - fixture.detectChanges(); - expect(component.setActiveLanguageCode.emit) - .toHaveBeenCalledWith(preferredLanguageCode); - expect(component.activeLanguageCode).toBe(preferredLanguageCode); - expect(component.languageSelection).toBe(languageDescription); - }); - })); - - it('should ask user to select a language when the preferred' + - ' language is not defined', fakeAsync(() => { - preferredLanguageCode = ''; - component.activeLanguageCode = null; - component.languageSelection = ''; - - component.ngOnInit(); + it( + 'should ask user to select a language when the preferred' + + ' language is not defined', + fakeAsync(() => { + preferredLanguageCode = ''; + component.activeLanguageCode = null; + component.languageSelection = ''; - fixture.detectChanges(); - expect(component.languageSelection).toBe('Language'); - expect(component.activeLanguageCode).toBe(null); - })); - - it('should show the correct language when the language is changed' - , () => { - expect(component.languageSelection).toBe('English'); component.ngOnInit(); - spyOn( - translationLanguageService, 'getActiveLanguageCode').and.returnValue( - 'fr'); - - activeLanguageChangedEmitter.emit(); - expect(component.languageSelection).toBe('français (French)'); - }); + fixture.detectChanges(); + expect(component.languageSelection).toBe('Language'); + expect(component.activeLanguageCode).toBe(null); + }) + ); - it('should indicate selection and save the language' + - ' on selecting a new language', () => { - const selectedLanguage = 'fr'; - spyOn(component.setActiveLanguageCode, 'emit'); - spyOn( - contributionOpportunitiesBackendApiServiceStub, - 'savePreferredTranslationLanguageAsync' as never); + it('should show the correct language when the language is changed', () => { + expect(component.languageSelection).toBe('English'); + component.ngOnInit(); + spyOn(translationLanguageService, 'getActiveLanguageCode').and.returnValue( + 'fr' + ); - component.selectOption(selectedLanguage); + activeLanguageChangedEmitter.emit(); - fixture.whenStable().then(() => { - fixture.detectChanges(); - expect(component.setActiveLanguageCode.emit).toHaveBeenCalledWith( - selectedLanguage); - expect( - contributionOpportunitiesBackendApiServiceStub - .savePreferredTranslationLanguageAsync).toHaveBeenCalledWith( - selectedLanguage); - }); + expect(component.languageSelection).toBe('français (French)'); }); + it( + 'should indicate selection and save the language' + + ' on selecting a new language', + () => { + const selectedLanguage = 'fr'; + spyOn(component.setActiveLanguageCode, 'emit'); + spyOn( + contributionOpportunitiesBackendApiServiceStub, + 'savePreferredTranslationLanguageAsync' as never + ); + + component.selectOption(selectedLanguage); + + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(component.setActiveLanguageCode.emit).toHaveBeenCalledWith( + selectedLanguage + ); + expect( + contributionOpportunitiesBackendApiServiceStub.savePreferredTranslationLanguageAsync + ).toHaveBeenCalledWith(selectedLanguage); + }); + } + ); + it('should toggle dropdown', fakeAsync(() => { component.ngOnInit(); component.filterDivRef = new ElementRef(document.createElement('div')); @@ -279,10 +302,14 @@ describe('Translation language selector', () => { expect(component.dropdownShown).toBe(true); expect(component.optionsFilter).toBe(''); expect(component.filteredOptions).toBe(component.options); - expect(component.filteredOptions).toContain( - {id: 'es', description: 'español (Spanish)'}); - expect(component.filteredOptions).toContain( - {id: 'fr', description: 'français (French)'}); + expect(component.filteredOptions).toContain({ + id: 'es', + description: 'español (Spanish)', + }); + expect(component.filteredOptions).toContain({ + id: 'fr', + description: 'français (French)', + }); expect(component.filterDivRef.nativeElement.focus).toHaveBeenCalled(); // Type a filter query. @@ -291,10 +318,14 @@ describe('Translation language selector', () => { fixture.detectChanges(); expect(component.filteredOptions).not.toBe(component.options); - expect(component.filteredOptions).toContain( - {id: 'es', description: 'español (Spanish)'}); - expect(component.filteredOptions).not.toContain( - {id: 'fr', description: 'français (French)'}); + expect(component.filteredOptions).toContain({ + id: 'es', + description: 'español (Spanish)', + }); + expect(component.filteredOptions).not.toContain({ + id: 'fr', + description: 'français (French)', + }); // Close the dropdown. component.toggleDropdown(); @@ -311,10 +342,14 @@ describe('Translation language selector', () => { expect(component.dropdownShown).toBe(true); expect(component.optionsFilter).toBe(''); expect(component.filteredOptions).toBe(component.options); - expect(component.filteredOptions).toContain( - {id: 'es', description: 'español (Spanish)'}); - expect(component.filteredOptions).toContain( - {id: 'fr', description: 'français (French)'}); + expect(component.filteredOptions).toContain({ + id: 'es', + description: 'español (Spanish)', + }); + expect(component.filteredOptions).toContain({ + id: 'fr', + description: 'français (French)', + }); expect(component.filterDivRef.nativeElement.focus).toHaveBeenCalled(); })); @@ -323,20 +358,26 @@ describe('Translation language selector', () => { // Expect the full list of languages to be contained. Adding just 3 here as // the list of languages may grow overtime. - expect(component.filteredOptions).toContain( - {id: 'en', description: 'English'}); - expect(component.filteredOptions).toContain( - {id: 'es', description: 'español (Spanish)'}); - expect(component.filteredOptions).toContain( - {id: 'fr', description: 'français (French)'}); - + expect(component.filteredOptions).toContain({ + id: 'en', + description: 'English', + }); + expect(component.filteredOptions).toContain({ + id: 'es', + description: 'español (Spanish)', + }); + expect(component.filteredOptions).toContain({ + id: 'fr', + description: 'français (French)', + }); component.optionsFilter = 'sp'; component.filterOptions(); fixture.detectChanges(); // Expect it to contain Spanish but not any of the other languages. - expect(component.filteredOptions).toEqual( - [{id: 'es', description: 'español (Spanish)'}]); + expect(component.filteredOptions).toEqual([ + {id: 'es', description: 'español (Spanish)'}, + ]); }); }); diff --git a/core/templates/pages/contributor-dashboard-page/translation-language-selector/translation-language-selector.component.ts b/core/templates/pages/contributor-dashboard-page/translation-language-selector/translation-language-selector.component.ts index a933831bcdbd..ecb4ea34b17d 100644 --- a/core/templates/pages/contributor-dashboard-page/translation-language-selector/translation-language-selector.component.ts +++ b/core/templates/pages/contributor-dashboard-page/translation-language-selector/translation-language-selector.component.ts @@ -17,18 +17,24 @@ */ import { - Component, OnInit, Input, Output, EventEmitter, HostListener, ViewChild, - ElementRef + Component, + OnInit, + Input, + Output, + EventEmitter, + HostListener, + ViewChild, + ElementRef, } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {downgradeComponent} from '@angular/upgrade/static'; -import { ContributionOpportunitiesBackendApiService } from +import { + ContributionOpportunitiesBackendApiService, // eslint-disable-next-line max-len - 'pages/contributor-dashboard-page/services/contribution-opportunities-backend-api.service'; -import { FeaturedTranslationLanguage } from - 'domain/opportunity/featured-translation-language.model'; -import { LanguageUtilService } from 'domain/utilities/language-util.service'; -import { TranslationLanguageService } from 'pages/exploration-editor-page/translation-tab/services/translation-language.service'; +} from 'pages/contributor-dashboard-page/services/contribution-opportunities-backend-api.service'; +import {FeaturedTranslationLanguage} from 'domain/opportunity/featured-translation-language.model'; +import {LanguageUtilService} from 'domain/utilities/language-util.service'; +import {TranslationLanguageService} from 'pages/exploration-editor-page/translation-tab/services/translation-language.service'; interface Options { id: string; @@ -37,7 +43,7 @@ interface Options { @Component({ selector: 'translation-language-selector', - templateUrl: './translation-language-selector.component.html' + templateUrl: './translation-language-selector.component.html', }) export class TranslationLanguageSelectorComponent implements OnInit { // These properties are initialized using Angular lifecycle hooks @@ -45,7 +51,7 @@ export class TranslationLanguageSelectorComponent implements OnInit { // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @Input() activeLanguageCode!: string | null; @Output() setActiveLanguageCode: EventEmitter = new EventEmitter(); - @ViewChild('dropdown', {'static': false}) dropdownRef!: ElementRef; + @ViewChild('dropdown', {static: false}) dropdownRef!: ElementRef; @ViewChild('filterDiv') filterDivRef!: ElementRef; options!: Options[]; @@ -61,24 +67,25 @@ export class TranslationLanguageSelectorComponent implements OnInit { explanationPopupContent = ''; constructor( - private contributionOpportunitiesBackendApiService: - ContributionOpportunitiesBackendApiService, + private contributionOpportunitiesBackendApiService: ContributionOpportunitiesBackendApiService, private languageUtilService: LanguageUtilService, - private readonly translationLanguageService: TranslationLanguageService, + private readonly translationLanguageService: TranslationLanguageService ) {} ngOnInit(): void { - this.translationLanguageService.onActiveLanguageChanged.subscribe( - () => { - this.languageSelection = this.languageIdToDescription[ - this.translationLanguageService.getActiveLanguageCode()]; - }); + this.translationLanguageService.onActiveLanguageChanged.subscribe(() => { + this.languageSelection = + this.languageIdToDescription[ + this.translationLanguageService.getActiveLanguageCode() + ]; + }); this.filteredOptions = this.options = this.languageUtilService - .getAllVoiceoverLanguageCodes().map(languageCode => { - const description = this.languageUtilService - .getAudioLanguageDescription(languageCode); + .getAllVoiceoverLanguageCodes() + .map(languageCode => { + const description = + this.languageUtilService.getAudioLanguageDescription(languageCode); this.languageIdToDescription[languageCode] = description; - return { id: languageCode, description }; + return {id: languageCode, description}; }); this.contributionOpportunitiesBackendApiService @@ -87,18 +94,15 @@ export class TranslationLanguageSelectorComponent implements OnInit { this.featuredLanguages = featuredLanguages; }); - this.languageSelection = ( - this.activeLanguageCode ? - this.languageIdToDescription[this.activeLanguageCode] : - 'Language' - ); + this.languageSelection = this.activeLanguageCode + ? this.languageIdToDescription[this.activeLanguageCode] + : 'Language'; this.contributionOpportunitiesBackendApiService .getPreferredTranslationLanguageAsync() - .then((preferredLanguageCode: string|null) => { + .then((preferredLanguageCode: string | null) => { if (preferredLanguageCode) { - this.populateLanguageSelection( - preferredLanguageCode); + this.populateLanguageSelection(preferredLanguageCode); } }); } @@ -110,22 +114,21 @@ export class TranslationLanguageSelectorComponent implements OnInit { this.filteredOptions = this.options; setTimeout(() => { this.filterDivRef.nativeElement.focus(); - } - , 1); + }, 1); } } populateLanguageSelection(languageCode: string): void { this.setActiveLanguageCode.emit(languageCode); - this.languageSelection = ( - this.languageIdToDescription[languageCode]); + this.languageSelection = this.languageIdToDescription[languageCode]; } selectOption(activeLanguageCode: string): void { this.populateLanguageSelection(activeLanguageCode); this.dropdownShown = false; - this.contributionOpportunitiesBackendApiService - .savePreferredTranslationLanguageAsync(activeLanguageCode); + this.contributionOpportunitiesBackendApiService.savePreferredTranslationLanguageAsync( + activeLanguageCode + ); } showExplanationPopup(index: number): void { @@ -135,8 +138,7 @@ export class TranslationLanguageSelectorComponent implements OnInit { * 30: approximate height of each dropdown element. */ this.explanationPopupPxOffsetY = 75 + 30 * index; - this.explanationPopupContent = ( - this.featuredLanguages[index].explanation); + this.explanationPopupContent = this.featuredLanguages[index].explanation; this.explanationPopupShown = true; } @@ -160,9 +162,11 @@ export class TranslationLanguageSelectorComponent implements OnInit { } filterOptions(): void { - this.filteredOptions = this.options.filter( - option => option.description.toLowerCase().includes( - this.optionsFilter.toLowerCase())); + this.filteredOptions = this.options.filter(option => + option.description + .toLowerCase() + .includes(this.optionsFilter.toLowerCase()) + ); } } @@ -171,5 +175,6 @@ angular.module('oppia').directive( downgradeComponent({ component: TranslationLanguageSelectorComponent, inputs: ['activeLanguageCode'], - outputs: ['setActiveLanguageCode'] - })); + outputs: ['setActiveLanguageCode'], + }) +); diff --git a/core/templates/pages/contributor-dashboard-page/translation-opportunities/translation-opportunities.component.spec.ts b/core/templates/pages/contributor-dashboard-page/translation-opportunities/translation-opportunities.component.spec.ts index ddcb7446c9d2..eca949e38ad6 100644 --- a/core/templates/pages/contributor-dashboard-page/translation-opportunities/translation-opportunities.component.spec.ts +++ b/core/templates/pages/contributor-dashboard-page/translation-opportunities/translation-opportunities.component.spec.ts @@ -16,24 +16,37 @@ * @fileoverview Unit tests for translationOpportunities. */ -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { NgbActiveModal, NgbModal, NgbModalRef, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; - -import { ContributionOpportunitiesService } from 'pages/contributor-dashboard-page/services/contribution-opportunities.service'; -import { ExplorationOpportunitySummary } from 'domain/opportunity/exploration-opportunity-summary.model'; -import { OpportunitiesListComponent } from 'pages/contributor-dashboard-page/opportunities-list/opportunities-list.component'; -import { OpportunitiesListItemComponent } from 'pages/contributor-dashboard-page/opportunities-list-item/opportunities-list-item.component'; -import { TranslationLanguageService } from 'pages/exploration-editor-page/translation-tab/services/translation-language.service'; -import { TranslationModalComponent } from 'pages/contributor-dashboard-page/modal-templates/translation-modal.component'; -import { TranslationOpportunitiesComponent } from './translation-opportunities.component'; -import { UserInfo } from 'domain/user/user-info.model'; -import { UserService } from 'services/user.service'; -import { WrapTextWithEllipsisPipe } from 'filters/string-utility-filters/wrap-text-with-ellipsis.pipe'; -import { LazyLoadingComponent } from 'components/common-layout-directives/common-elements/lazy-loading.component'; -import { CkEditorCopyToolbarComponent } from 'components/ck-editor-helpers/ck-editor-copy-toolbar/ck-editor-copy-toolbar.component'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { EventEmitter } from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import { + NgbActiveModal, + NgbModal, + NgbModalRef, + NgbTooltipModule, +} from '@ng-bootstrap/ng-bootstrap'; + +import {ContributionOpportunitiesService} from 'pages/contributor-dashboard-page/services/contribution-opportunities.service'; +import {ExplorationOpportunitySummary} from 'domain/opportunity/exploration-opportunity-summary.model'; +import {OpportunitiesListComponent} from 'pages/contributor-dashboard-page/opportunities-list/opportunities-list.component'; +import {OpportunitiesListItemComponent} from 'pages/contributor-dashboard-page/opportunities-list-item/opportunities-list-item.component'; +import {TranslationLanguageService} from 'pages/exploration-editor-page/translation-tab/services/translation-language.service'; +import {TranslationModalComponent} from 'pages/contributor-dashboard-page/modal-templates/translation-modal.component'; +import {TranslationOpportunitiesComponent} from './translation-opportunities.component'; +import {UserInfo} from 'domain/user/user-info.model'; +import {UserService} from 'services/user.service'; +import {WrapTextWithEllipsisPipe} from 'filters/string-utility-filters/wrap-text-with-ellipsis.pipe'; +import {LazyLoadingComponent} from 'components/common-layout-directives/common-elements/lazy-loading.component'; +import {CkEditorCopyToolbarComponent} from 'components/ck-editor-helpers/ck-editor-copy-toolbar/ck-editor-copy-toolbar.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {EventEmitter} from '@angular/core'; describe('Translation opportunities component', () => { let contributionOpportunitiesService: ContributionOpportunitiesService; @@ -45,12 +58,28 @@ describe('Translation opportunities component', () => { let translationModal: NgbModalRef; let httpTestingController: HttpTestingController; let loggedInUserInfo = new UserInfo( - ['EXPLORATION_EDITOR'], false, false, false, false, false, - 'en', 'username', 'test@example.com', true + ['EXPLORATION_EDITOR'], + false, + false, + false, + false, + false, + 'en', + 'username', + 'test@example.com', + true ); const notLoggedInUserInfo = new UserInfo( - ['GUEST'], false, false, false, false, false, - 'en', null, null, false + ['GUEST'], + false, + false, + false, + false, + false, + 'en', + null, + null, + false ); let opportunitiesArray: ExplorationOpportunitySummary[] = []; @@ -58,10 +87,7 @@ describe('Translation opportunities component', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - NgbTooltipModule - ], + imports: [HttpClientTestingModule, NgbTooltipModule], declarations: [ CkEditorCopyToolbarComponent, LazyLoadingComponent, @@ -71,23 +97,24 @@ describe('Translation opportunities component', () => { TranslationOpportunitiesComponent, WrapTextWithEllipsisPipe, ], - providers: [ - NgbModal, - NgbActiveModal - ], - schemas: [NO_ERRORS_SCHEMA] + providers: [NgbModal, NgbActiveModal], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); translationModal = TestBed.createComponent( - TranslationModalComponent) as unknown as NgbModalRef; + TranslationModalComponent + ) as unknown as NgbModalRef; httpTestingController = TestBed.inject(HttpTestingController); contributionOpportunitiesService = TestBed.inject( - ContributionOpportunitiesService); + ContributionOpportunitiesService + ); translationLanguageService = TestBed.inject(TranslationLanguageService); userService = TestBed.inject(UserService); modalService = TestBed.inject(NgbModal); spyOn(modalService, 'open').and.returnValue(translationModal); - spyOnProperty(translationLanguageService, 'onActiveLanguageChanged').and - .returnValue(activeLanguageChangedEmitter); + spyOnProperty( + translationLanguageService, + 'onActiveLanguageChanged' + ).and.returnValue(activeLanguageChangedEmitter); }); afterEach(() => { @@ -103,13 +130,13 @@ describe('Translation opportunities component', () => { chapter_title: 'Chapter title 1', content_count: 4, translation_counts: { - en: 2 + en: 2, }, translation_in_review_counts: { - en: 2 + en: 2, }, language_code: 'en', - is_pinned: false + is_pinned: false, }), ExplorationOpportunitySummary.createFromBackendDict({ id: '2', @@ -118,30 +145,31 @@ describe('Translation opportunities component', () => { chapter_title: 'Chapter title 2', content_count: 10, translation_counts: { - en: 4 + en: 4, }, translation_in_review_counts: { - en: 4 + en: 4, }, language_code: 'en', - is_pinned: false - }) + is_pinned: false, + }), ]; - fixture = TestBed.createComponent( - TranslationOpportunitiesComponent); + fixture = TestBed.createComponent(TranslationOpportunitiesComponent); component = fixture.componentInstance; }); it('should load translation opportunities', () => { spyOn(translationLanguageService, 'getActiveLanguageCode').and.returnValue( - 'en'); + 'en' + ); spyOn( - contributionOpportunitiesService, 'getTranslationOpportunitiesAsync').and - .resolveTo({ - opportunities: opportunitiesArray, - more: false - }); + contributionOpportunitiesService, + 'getTranslationOpportunitiesAsync' + ).and.resolveTo({ + opportunities: opportunitiesArray, + more: false, + }); component.loadOpportunitiesAsync().then(({opportunitiesDicts, more}) => { expect(opportunitiesDicts.length).toBe(2); @@ -151,13 +179,15 @@ describe('Translation opportunities component', () => { it('should load more translation opportunities', () => { spyOn(translationLanguageService, 'getActiveLanguageCode').and.returnValue( - 'en'); + 'en' + ); spyOn( - contributionOpportunitiesService, 'getTranslationOpportunitiesAsync').and - .resolveTo({ - opportunities: opportunitiesArray, - more: true - }); + contributionOpportunitiesService, + 'getTranslationOpportunitiesAsync' + ).and.resolveTo({ + opportunities: opportunitiesArray, + more: true, + }); component.loadOpportunitiesAsync().then(({opportunitiesDicts, more}) => { expect(opportunitiesDicts.length).toBe(2); expect(more).toBeTrue(); @@ -165,128 +195,145 @@ describe('Translation opportunities component', () => { spyOn( contributionOpportunitiesService, - 'getMoreTranslationOpportunitiesAsync').and.resolveTo({ + 'getMoreTranslationOpportunitiesAsync' + ).and.resolveTo({ opportunities: opportunitiesArray, - more: false + more: false, }); - component.loadMoreOpportunitiesAsync() + component + .loadMoreOpportunitiesAsync() .then(({opportunitiesDicts, more}) => { expect(opportunitiesDicts.length).toBe(2); expect(more).toBeFalse(); }); }); - it('should move opportunities with no translatable cards to the bottom ' + - 'of opportunity list', () => { - spyOn(translationLanguageService, 'getActiveLanguageCode').and.returnValue( - 'en'); + it( + 'should move opportunities with no translatable cards to the bottom ' + + 'of opportunity list', + () => { + spyOn( + translationLanguageService, + 'getActiveLanguageCode' + ).and.returnValue('en'); - const {opportunitiesDicts, } = ( - component.getPresentableOpportunitiesData({ + const {opportunitiesDicts} = component.getPresentableOpportunitiesData({ opportunities: opportunitiesArray, - more: false - })); - - expect(opportunitiesDicts.length).toBe(2); - expect( - opportunitiesDicts[opportunitiesDicts.length - 1].translationsCount + - opportunitiesDicts[opportunitiesDicts.length - 1].inReviewCount === - opportunitiesDicts[opportunitiesDicts.length - 1].totalCount).toBeTrue(); - expect(opportunitiesDicts).toEqual([{ - id: '2', - heading: 'Chapter title 2', - subheading: 'topic_2 - Story title 2', - progressPercentage: '40.00', - actionButtonTitle: 'Translate', - inReviewCount: 4, - totalCount: 10, - translationsCount: 4, - }, - { - id: '1', - heading: 'Chapter title 1', - subheading: 'topic_1 - Story title 1', - progressPercentage: '50.00', - actionButtonTitle: 'Translate', - inReviewCount: 2, - totalCount: 4, - translationsCount: 2, + more: false, + }); + + expect(opportunitiesDicts.length).toBe(2); + expect( + opportunitiesDicts[opportunitiesDicts.length - 1].translationsCount + + opportunitiesDicts[opportunitiesDicts.length - 1].inReviewCount === + opportunitiesDicts[opportunitiesDicts.length - 1].totalCount + ).toBeTrue(); + expect(opportunitiesDicts).toEqual([ + { + id: '2', + heading: 'Chapter title 2', + subheading: 'topic_2 - Story title 2', + progressPercentage: '40.00', + actionButtonTitle: 'Translate', + inReviewCount: 4, + totalCount: 10, + translationsCount: 4, + }, + { + id: '1', + heading: 'Chapter title 1', + subheading: 'topic_1 - Story title 1', + progressPercentage: '50.00', + actionButtonTitle: 'Translate', + inReviewCount: 2, + totalCount: 4, + translationsCount: 2, + }, + ]); } - ]); - }); + ); - it('should not chagne contents of each opportunity when get presentable ' + - 'opportunities', () => { - spyOn(translationLanguageService, 'getActiveLanguageCode').and.returnValue( - 'en'); + it( + 'should not chagne contents of each opportunity when get presentable ' + + 'opportunities', + () => { + spyOn( + translationLanguageService, + 'getActiveLanguageCode' + ).and.returnValue('en'); - expect(component.allOpportunities).toEqual({}); + expect(component.allOpportunities).toEqual({}); - const {opportunitiesDicts, } = ( - component.getPresentableOpportunitiesData({ + const {opportunitiesDicts} = component.getPresentableOpportunitiesData({ opportunities: opportunitiesArray, - more: false - })); - - expect(Object.keys(component.allOpportunities).length).toEqual(2); - expect(component.allOpportunities['1']).toEqual({ - id: '1', - heading: 'Chapter title 1', - subheading: 'topic_1 - Story title 1', - progressPercentage: '50.00', - actionButtonTitle: 'Translate', - inReviewCount: 2, - totalCount: 4, - translationsCount: 2, - }); - expect(component.allOpportunities['2']).toEqual({ - id: '2', - heading: 'Chapter title 2', - subheading: 'topic_2 - Story title 2', - progressPercentage: '40.00', - actionButtonTitle: 'Translate', - inReviewCount: 4, - totalCount: 10, - translationsCount: 4, - }); + more: false, + }); - expect(opportunitiesDicts.length).toBe(2); - expect(opportunitiesDicts.sort((a, b) => { - return parseInt(a.id) - parseInt(b.id); - })).toEqual([{ - id: '1', - heading: 'Chapter title 1', - subheading: 'topic_1 - Story title 1', - progressPercentage: '50.00', - actionButtonTitle: 'Translate', - inReviewCount: 2, - totalCount: 4, - translationsCount: 2, - }, - { - id: '2', - heading: 'Chapter title 2', - subheading: 'topic_2 - Story title 2', - progressPercentage: '40.00', - actionButtonTitle: 'Translate', - inReviewCount: 4, - totalCount: 10, - translationsCount: 4, + expect(Object.keys(component.allOpportunities).length).toEqual(2); + expect(component.allOpportunities['1']).toEqual({ + id: '1', + heading: 'Chapter title 1', + subheading: 'topic_1 - Story title 1', + progressPercentage: '50.00', + actionButtonTitle: 'Translate', + inReviewCount: 2, + totalCount: 4, + translationsCount: 2, + }); + expect(component.allOpportunities['2']).toEqual({ + id: '2', + heading: 'Chapter title 2', + subheading: 'topic_2 - Story title 2', + progressPercentage: '40.00', + actionButtonTitle: 'Translate', + inReviewCount: 4, + totalCount: 10, + translationsCount: 4, + }); + + expect(opportunitiesDicts.length).toBe(2); + expect( + opportunitiesDicts.sort((a, b) => { + return parseInt(a.id) - parseInt(b.id); + }) + ).toEqual([ + { + id: '1', + heading: 'Chapter title 1', + subheading: 'topic_1 - Story title 1', + progressPercentage: '50.00', + actionButtonTitle: 'Translate', + inReviewCount: 2, + totalCount: 4, + translationsCount: 2, + }, + { + id: '2', + heading: 'Chapter title 2', + subheading: 'topic_2 - Story title 2', + progressPercentage: '40.00', + actionButtonTitle: 'Translate', + inReviewCount: 4, + totalCount: 10, + translationsCount: 4, + }, + ]); } - ]); - }); + ); it('should open translation modal when clicking button', fakeAsync(() => { spyOn(translationLanguageService, 'getActiveLanguageCode').and.returnValue( - 'en'); + 'en' + ); spyOn(userService, 'getUserInfoAsync').and.resolveTo(loggedInUserInfo); spyOn( - contributionOpportunitiesService, 'getTranslationOpportunitiesAsync').and - .resolveTo({ - opportunities: opportunitiesArray, - more: false - }); + contributionOpportunitiesService, + 'getTranslationOpportunitiesAsync' + ).and.resolveTo({ + opportunities: opportunitiesArray, + more: false, + }); component.ngOnInit(); tick(); component.onClickButton('2'); @@ -294,51 +341,59 @@ describe('Translation opportunities component', () => { expect(modalService.open).toHaveBeenCalled(); })); - it('should not open translation modal when user is not logged', fakeAsync( - () => { - spyOn( - translationLanguageService, 'getActiveLanguageCode').and.returnValue( - 'en'); - spyOn(userService, 'getUserInfoAsync').and.resolveTo(notLoggedInUserInfo); - spyOn( - contributionOpportunitiesService, - 'getTranslationOpportunitiesAsync').and.resolveTo({ - opportunities: opportunitiesArray, - more: true - }); - spyOn(contributionOpportunitiesService, 'showRequiresLoginModal') - .and.stub(); - - component.ngOnInit(); - - component.onClickButton('2'); - tick(); - - expect(modalService.open).not.toHaveBeenCalled(); - })); - - it('should not show translation opportunities when language is not ' + - 'selected', fakeAsync(() => { + it('should not open translation modal when user is not logged', fakeAsync(() => { + spyOn(translationLanguageService, 'getActiveLanguageCode').and.returnValue( + 'en' + ); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(notLoggedInUserInfo); spyOn( - translationLanguageService, 'getActiveLanguageCode').and.callThrough(); - spyOn(userService, 'getUserInfoAsync').and.resolveTo(loggedInUserInfo); - expect(component.languageSelected).toBe(false); + contributionOpportunitiesService, + 'getTranslationOpportunitiesAsync' + ).and.resolveTo({ + opportunities: opportunitiesArray, + more: true, + }); + spyOn( + contributionOpportunitiesService, + 'showRequiresLoginModal' + ).and.stub(); component.ngOnInit(); - expect(component.languageSelected).toBe(false); + component.onClickButton('2'); + tick(); + + expect(modalService.open).not.toHaveBeenCalled(); })); - it('should show translation opportunities when language is changed' - , fakeAsync(() => { + it( + 'should not show translation opportunities when language is not ' + + 'selected', + fakeAsync(() => { spyOn( - translationLanguageService, 'getActiveLanguageCode').and.callThrough(); + translationLanguageService, + 'getActiveLanguageCode' + ).and.callThrough(); spyOn(userService, 'getUserInfoAsync').and.resolveTo(loggedInUserInfo); + expect(component.languageSelected).toBe(false); + component.ngOnInit(); + expect(component.languageSelected).toBe(false); + }) + ); - activeLanguageChangedEmitter.emit(); + it('should show translation opportunities when language is changed', fakeAsync(() => { + spyOn( + translationLanguageService, + 'getActiveLanguageCode' + ).and.callThrough(); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(loggedInUserInfo); + component.ngOnInit(); + expect(component.languageSelected).toBe(false); - expect(component.languageSelected).toBe(true); - })); + activeLanguageChangedEmitter.emit(); + + expect(component.languageSelected).toBe(true); + })); }); diff --git a/core/templates/pages/contributor-dashboard-page/translation-opportunities/translation-opportunities.component.ts b/core/templates/pages/contributor-dashboard-page/translation-opportunities/translation-opportunities.component.ts index 4ea086610d87..1887841083ea 100644 --- a/core/templates/pages/contributor-dashboard-page/translation-opportunities/translation-opportunities.component.ts +++ b/core/templates/pages/contributor-dashboard-page/translation-opportunities/translation-opportunities.component.ts @@ -16,18 +16,24 @@ * @fileoverview Component for the translation opportunities. */ -import { Component, Injector } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { TranslationLanguageService } from 'pages/exploration-editor-page/translation-tab/services/translation-language.service'; -import { TranslationTopicService } from 'pages/exploration-editor-page/translation-tab/services/translation-topic.service'; -import { ContextService } from 'services/context.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { UserService } from 'services/user.service'; -import { TranslationModalComponent, TranslationOpportunity } from '../modal-templates/translation-modal.component'; -import { ContributionOpportunitiesService, ExplorationOpportunitiesDict } from '../services/contribution-opportunities.service'; -import { TranslateTextService } from '../services/translate-text.service'; +import {Component, Injector} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {TranslationLanguageService} from 'pages/exploration-editor-page/translation-tab/services/translation-language.service'; +import {TranslationTopicService} from 'pages/exploration-editor-page/translation-tab/services/translation-topic.service'; +import {ContextService} from 'services/context.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {UserService} from 'services/user.service'; +import { + TranslationModalComponent, + TranslationOpportunity, +} from '../modal-templates/translation-modal.component'; +import { + ContributionOpportunitiesService, + ExplorationOpportunitiesDict, +} from '../services/contribution-opportunities.service'; +import {TranslateTextService} from '../services/translate-text.service'; @Component({ selector: 'oppia-translation-opportunities', @@ -45,8 +51,7 @@ export class TranslationOpportunitiesComponent { languageSelected = false; constructor( private readonly contextService: ContextService, - private readonly contributionOpportunitiesService: - ContributionOpportunitiesService, + private readonly contributionOpportunitiesService: ContributionOpportunitiesService, private readonly modalService: NgbModal, private readonly siteAnalyticsService: SiteAnalyticsService, private readonly translationLanguageService: TranslationLanguageService, @@ -61,8 +66,10 @@ export class TranslationOpportunitiesComponent { return this.allOpportunities[expId]; } - getPresentableOpportunitiesData( - {opportunities, more}: ExplorationOpportunitiesDict): { + getPresentableOpportunitiesData({ + opportunities, + more, + }: ExplorationOpportunitiesDict): { opportunitiesDicts: TranslationOpportunity[]; more: boolean; } { @@ -72,10 +79,10 @@ export class TranslationOpportunitiesComponent { const opportunity = opportunities[index]; const subheading = opportunity.getOpportunitySubheading(); const heading = opportunity.getOpportunityHeading(); - const languageCode = ( - this.translationLanguageService.getActiveLanguageCode()); - const progressPercentage = ( - opportunity.getTranslationProgressPercentage(languageCode)); + const languageCode = + this.translationLanguageService.getActiveLanguageCode(); + const progressPercentage = + opportunity.getTranslationProgressPercentage(languageCode); const opportunityDict: TranslationOpportunity = { id: opportunity.getExplorationId(), heading: heading, @@ -84,11 +91,13 @@ export class TranslationOpportunitiesComponent { actionButtonTitle: 'Translate', inReviewCount: opportunity.getTranslationsInReviewCount(languageCode), totalCount: opportunity.getContentCount(), - translationsCount: opportunity.getTranslationsCount(languageCode) + translationsCount: opportunity.getTranslationsCount(languageCode), }; this.allOpportunities[opportunityDict.id] = opportunityDict; - if (opportunityDict.translationsCount + - opportunityDict.inReviewCount === opportunityDict.totalCount) { + if ( + opportunityDict.translationsCount + opportunityDict.inReviewCount === + opportunityDict.totalCount + ) { untranslatableOpportunitiesDicts.push(opportunityDict); } else { opportunitiesDicts.push(opportunityDict); @@ -104,35 +113,37 @@ export class TranslationOpportunitiesComponent { return; } this.siteAnalyticsService.registerContributorDashboardSuggestEvent( - 'Translation'); + 'Translation' + ); const opportunity = this.getOpportunitySummary(expId); - const modalRef = this.modalService.open( - TranslationModalComponent, { - size: 'lg', - backdrop: 'static', - injector: this.injector, - // TODO(#12768): Remove the backdropClass & windowClass once the - // rte-component-modal is migrated to Angular. Currently, the custom - // class is used for correctly stacking AngularJS modal on top of - // Angular modal. - backdropClass: 'forced-modal-stack', - windowClass: 'forced-modal-stack' - }); + const modalRef = this.modalService.open(TranslationModalComponent, { + size: 'lg', + backdrop: 'static', + injector: this.injector, + // TODO(#12768): Remove the backdropClass & windowClass once the + // rte-component-modal is migrated to Angular. Currently, the custom + // class is used for correctly stacking AngularJS modal on top of + // Angular modal. + backdropClass: 'forced-modal-stack', + windowClass: 'forced-modal-stack', + }); modalRef.componentInstance.opportunity = opportunity; } ngOnInit(): void { - this.userService.getUserInfoAsync().then((userInfo) => { + this.userService.getUserInfoAsync().then(userInfo => { this.userIsLoggedIn = userInfo.isLoggedIn(); }); this.translationLanguageService.onActiveLanguageChanged.subscribe( - () => this.languageSelected = true); + () => (this.languageSelected = true) + ); if (this.translationLanguageService.getActiveLanguageCode()) { this.languageSelected = true; } else { - this.OPPIA_AVATAR_IMAGE_URL = ( + this.OPPIA_AVATAR_IMAGE_URL = this.urlInterpolationService.getStaticImageUrl( - '/avatar/oppia_avatar_100px.svg')); + '/avatar/oppia_avatar_100px.svg' + ); } } @@ -143,7 +154,8 @@ export class TranslationOpportunitiesComponent { return this.contributionOpportunitiesService .getMoreTranslationOpportunitiesAsync( this.translationLanguageService.getActiveLanguageCode(), - this.translationTopicService.getActiveTopicName()) + this.translationTopicService.getActiveTopicName() + ) .then(this.getPresentableOpportunitiesData.bind(this)); } @@ -154,11 +166,15 @@ export class TranslationOpportunitiesComponent { return this.contributionOpportunitiesService .getTranslationOpportunitiesAsync( this.translationLanguageService.getActiveLanguageCode(), - this.translationTopicService.getActiveTopicName()) + this.translationTopicService.getActiveTopicName() + ) .then(this.getPresentableOpportunitiesData.bind(this)); } } -angular.module('oppia').directive( - 'oppiaTranslationOpportunities', downgradeComponent( - {component: TranslationOpportunitiesComponent})); +angular + .module('oppia') + .directive( + 'oppiaTranslationOpportunities', + downgradeComponent({component: TranslationOpportunitiesComponent}) + ); diff --git a/core/templates/pages/contributor-dashboard-page/translation-topic-selector/translation-topic-selector.component.spec.ts b/core/templates/pages/contributor-dashboard-page/translation-topic-selector/translation-topic-selector.component.spec.ts index d2a78b45db88..d43e8028cd5e 100644 --- a/core/templates/pages/contributor-dashboard-page/translation-topic-selector/translation-topic-selector.component.spec.ts +++ b/core/templates/pages/contributor-dashboard-page/translation-topic-selector/translation-topic-selector.component.spec.ts @@ -16,14 +16,16 @@ * @fileoverview Unit tests for the translation topic selector component. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; -import { TranslationTopicSelectorComponent } from +import { + TranslationTopicSelectorComponent, // eslint-disable-next-line max-len - 'pages/contributor-dashboard-page/translation-topic-selector/translation-topic-selector.component'; -import { ContributionOpportunitiesBackendApiService } from +} from 'pages/contributor-dashboard-page/translation-topic-selector/translation-topic-selector.component'; +import { + ContributionOpportunitiesBackendApiService, // eslint-disable-next-line max-len - 'pages/contributor-dashboard-page/services/contribution-opportunities-backend-api.service'; +} from 'pages/contributor-dashboard-page/services/contribution-opportunities-backend-api.service'; describe('Translation language selector', () => { let component: TranslationTopicSelectorComponent; @@ -31,10 +33,9 @@ describe('Translation language selector', () => { let topicNames = ['All', 'Topic 1']; - let contributionOpportunitiesBackendApiServiceStub: - Partial = { - fetchTranslatableTopicNamesAsync: async() => - Promise.resolve(topicNames) + let contributionOpportunitiesBackendApiServiceStub: Partial = + { + fetchTranslatableTopicNamesAsync: async () => Promise.resolve(topicNames), }; let clickDropdown: () => void; @@ -43,10 +44,12 @@ describe('Translation language selector', () => { beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [TranslationTopicSelectorComponent], - providers: [{ - provide: ContributionOpportunitiesBackendApiService, - useValue: contributionOpportunitiesBackendApiServiceStub - }] + providers: [ + { + provide: ContributionOpportunitiesBackendApiService, + useValue: contributionOpportunitiesBackendApiServiceStub, + }, + ], }).compileComponents(); })); @@ -67,14 +70,15 @@ describe('Translation language selector', () => { getDropdownOptionsContainer = () => { return fixture.debugElement.nativeElement.querySelector( - '.oppia-translation-topic-selector-dropdown-container'); + '.oppia-translation-topic-selector-dropdown-container' + ); }; }); it('should correctly initialize dropdown activeTopicName', () => { - const dropdown = ( - fixture.nativeElement.querySelector( - '.oppia-translation-topic-selector-inner-container')); + const dropdown = fixture.nativeElement.querySelector( + '.oppia-translation-topic-selector-inner-container' + ); expect(dropdown.firstChild.textContent.trim()).toBe('All'); }); @@ -96,10 +100,9 @@ describe('Translation language selector', () => { expect(getDropdownOptionsContainer()).toBeTruthy(); let fakeClickAwayEvent = new MouseEvent('click'); - Object.defineProperty( - fakeClickAwayEvent, - 'target', - {value: document.createElement('div')}); + Object.defineProperty(fakeClickAwayEvent, 'target', { + value: document.createElement('div'), + }); component.onDocumentClick(fakeClickAwayEvent); fixture.detectChanges(); expect(component.dropdownShown).toBe(false); diff --git a/core/templates/pages/contributor-dashboard-page/translation-topic-selector/translation-topic-selector.component.ts b/core/templates/pages/contributor-dashboard-page/translation-topic-selector/translation-topic-selector.component.ts index d940e8b9e43b..e5e3daa58cda 100644 --- a/core/templates/pages/contributor-dashboard-page/translation-topic-selector/translation-topic-selector.component.ts +++ b/core/templates/pages/contributor-dashboard-page/translation-topic-selector/translation-topic-selector.component.ts @@ -17,18 +17,25 @@ */ import { - Component, OnInit, Input, Output, EventEmitter, HostListener, ViewChild, - ElementRef + Component, + OnInit, + Input, + Output, + EventEmitter, + HostListener, + ViewChild, + ElementRef, } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {downgradeComponent} from '@angular/upgrade/static'; -import { ContributionOpportunitiesBackendApiService } from +import { + ContributionOpportunitiesBackendApiService, // eslint-disable-next-line max-len - 'pages/contributor-dashboard-page/services/contribution-opportunities-backend-api.service'; +} from 'pages/contributor-dashboard-page/services/contribution-opportunities-backend-api.service'; @Component({ selector: 'translation-topic-selector', - templateUrl: './translation-topic-selector.component.html' + templateUrl: './translation-topic-selector.component.html', }) export class TranslationTopicSelectorComponent implements OnInit { // These properties are initialized using Angular lifecycle hooks @@ -36,20 +43,19 @@ export class TranslationTopicSelectorComponent implements OnInit { // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @Input() activeTopicName!: string; @Output() setActiveTopicName: EventEmitter = new EventEmitter(); - @ViewChild('dropdown', {'static': false}) dropdownRef!: ElementRef; + @ViewChild('dropdown', {static: false}) dropdownRef!: ElementRef; options!: string[]; dropdownShown = false; constructor( - private contributionOpportunitiesBackendApiService: - ContributionOpportunitiesBackendApiService + private contributionOpportunitiesBackendApiService: ContributionOpportunitiesBackendApiService ) {} ngOnInit(): void { this.contributionOpportunitiesBackendApiService .fetchTranslatableTopicNamesAsync() - .then((topicNames) => { + .then(topicNames => { this.options = topicNames; }); } @@ -84,5 +90,6 @@ angular.module('oppia').directive( downgradeComponent({ component: TranslationTopicSelectorComponent, inputs: ['activeTopicName'], - outputs: ['setActiveTopicName'] - })); + outputs: ['setActiveTopicName'], + }) +); diff --git a/core/templates/pages/creator-dashboard-page/creator-dashboard-page.component.spec.ts b/core/templates/pages/creator-dashboard-page/creator-dashboard-page.component.spec.ts index 91b1dc8a4bbd..0629cdf8a461 100644 --- a/core/templates/pages/creator-dashboard-page/creator-dashboard-page.component.spec.ts +++ b/core/templates/pages/creator-dashboard-page/creator-dashboard-page.component.spec.ts @@ -16,22 +16,33 @@ * @fileoverview Unit tests for creator dashboard page component. */ -import { CollectionSummary, CollectionSummaryBackendDict } from 'domain/collection/collection-summary.model'; -import { CreatorDashboardStats } from 'domain/creator_dashboard/creator-dashboard-stats.model'; -import { CreatorExplorationSummary } from 'domain/summary/creator-exploration-summary.model'; -import { ProfileSummary } from 'domain/user/profile-summary.model'; -import { CreatorDashboardPageComponent } from './creator-dashboard-page.component'; -import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing'; -import { CreatorDashboardBackendApiService, CreatorDashboardData } from 'domain/creator_dashboard/creator-dashboard-backend-api.service'; -import { CsrfTokenService } from 'services/csrf-token.service'; -import { UserService } from 'services/user.service'; -import { ExplorationCreationService } from 'components/entity-creation-services/exploration-creation.service'; -import { UserInfo } from 'domain/user/user-info.model'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { NO_ERRORS_SCHEMA, Pipe } from '@angular/core'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { SortByPipe } from 'filters/string-utility-filters/sort-by.pipe'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; +import { + CollectionSummary, + CollectionSummaryBackendDict, +} from 'domain/collection/collection-summary.model'; +import {CreatorDashboardStats} from 'domain/creator_dashboard/creator-dashboard-stats.model'; +import {CreatorExplorationSummary} from 'domain/summary/creator-exploration-summary.model'; +import {ProfileSummary} from 'domain/user/profile-summary.model'; +import {CreatorDashboardPageComponent} from './creator-dashboard-page.component'; +import { + ComponentFixture, + fakeAsync, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { + CreatorDashboardBackendApiService, + CreatorDashboardData, +} from 'domain/creator_dashboard/creator-dashboard-backend-api.service'; +import {CsrfTokenService} from 'services/csrf-token.service'; +import {UserService} from 'services/user.service'; +import {ExplorationCreationService} from 'components/entity-creation-services/exploration-creation.service'; +import {UserInfo} from 'domain/user/user-info.model'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {NO_ERRORS_SCHEMA, Pipe} from '@angular/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {SortByPipe} from 'filters/string-utility-filters/sort-by.pipe'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; @Pipe({name: 'truncate'}) class MockTruncatePipe { @@ -49,7 +60,7 @@ describe('Creator Dashboard Page Component', () => { let explorationCreationService: ExplorationCreationService; let windowDimensionsService: WindowDimensionsService; let userInfo = { - canCreateCollections: () => true + canCreateCollections: () => true, }; beforeEach(waitForAsync(() => { @@ -59,10 +70,10 @@ describe('Creator Dashboard Page Component', () => { CreatorDashboardPageComponent, MockTranslatePipe, MockTruncatePipe, - SortByPipe + SortByPipe, ], providers: [], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -70,16 +81,20 @@ describe('Creator Dashboard Page Component', () => { fixture = TestBed.createComponent(CreatorDashboardPageComponent); component = fixture.componentInstance; creatorDashboardBackendApiService = TestBed.inject( - CreatorDashboardBackendApiService); + CreatorDashboardBackendApiService + ); csrfService = TestBed.inject(CsrfTokenService); explorationCreationService = TestBed.inject(ExplorationCreationService); userService = TestBed.inject(UserService); windowDimensionsService = TestBed.inject(WindowDimensionsService); spyOn(csrfService, 'getTokenAsync').and.returnValue( - Promise.resolve('sample-csrf-token')); - spyOn(userService, 'getProfileImageDataUrl').and.returnValue( - ['default-image-url-png', 'default-image-url-webp']); + Promise.resolve('sample-csrf-token') + ); + spyOn(userService, 'getProfileImageDataUrl').and.returnValue([ + 'default-image-url-png', + 'default-image-url-webp', + ]); // This approach was choosen because spyOn() doesn't work on properties // that doesn't have a get access type. @@ -90,23 +105,31 @@ describe('Creator Dashboard Page Component', () => { // ref: https://github.com/jasmine/jasmine/issues/1415 Object.defineProperty(window, 'innerWidth', { get: () => undefined, - set: () => {} + set: () => {}, }); })); - it('should get the correct exploration editor page URL corresponding to a' + - ' given exploration ID', () => { - let explorationId = '1'; - expect(component.getExplorationUrl(explorationId)).toBe( - '/create/' + explorationId); - }); - - it('should get the correct collection editor page URL corresponding to a' + - ' given collection ID', () => { - let collectionId = '1'; - expect(component.getCollectionUrl(collectionId)).toBe( - '/collection_editor/create/' + collectionId); - }); + it( + 'should get the correct exploration editor page URL corresponding to a' + + ' given exploration ID', + () => { + let explorationId = '1'; + expect(component.getExplorationUrl(explorationId)).toBe( + '/create/' + explorationId + ); + } + ); + + it( + 'should get the correct collection editor page URL corresponding to a' + + ' given collection ID', + () => { + let collectionId = '1'; + expect(component.getCollectionUrl(collectionId)).toBe( + '/collection_editor/create/' + collectionId + ); + } + ); it('should check if the view is tablet or not', () => { let widthSpy = spyOn(windowDimensionsService, 'getWidth'); @@ -119,93 +142,102 @@ describe('Creator Dashboard Page Component', () => { expect(component.checkTabletView()).toBe(false); }); - it('should get username popover event type according to username length', - () => { - expect(component.showUsernamePopover('abcdefghijk')).toBe('mouseenter'); - expect(component.showUsernamePopover('abc')).toBe('none'); - }); - - it('should get complete thumbail icon path corresponding to a given' + - ' relative path', () => { - expect(component.getCompleteThumbnailIconUrl('/path/to/icon.png')).toBe( - '/assets/images/path/to/icon.png'); + it('should get username popover event type according to username length', () => { + expect(component.showUsernamePopover('abcdefghijk')).toBe('mouseenter'); + expect(component.showUsernamePopover('abc')).toBe('none'); }); + it( + 'should get complete thumbail icon path corresponding to a given' + + ' relative path', + () => { + expect(component.getCompleteThumbnailIconUrl('/path/to/icon.png')).toBe( + '/assets/images/path/to/icon.png' + ); + } + ); + it('should get Trusted Resource Url', () => { expect(component.getTrustedResourceUrl('%2Fimages%2Furl%2F1')).toBe( - '/images/url/1'); + '/images/url/1' + ); }); - it('should create new exploration when clicked on CREATE' + - ' EXPLORATION button', () => { - spyOn( - explorationCreationService, 'createNewExploration'); - component.createNewExploration(); - expect( - explorationCreationService.createNewExploration).toHaveBeenCalled(); - }); + it( + 'should create new exploration when clicked on CREATE' + + ' EXPLORATION button', + () => { + spyOn(explorationCreationService, 'createNewExploration'); + component.createNewExploration(); + expect( + explorationCreationService.createNewExploration + ).toHaveBeenCalled(); + } + ); it('should get user profile image png data url correctly', () => { expect(component.getProfileImagePngDataUrl('username')).toBe( - 'default-image-url-png'); + 'default-image-url-png' + ); }); it('should get user profile image webp data url correctly', () => { expect(component.getProfileImageWebpDataUrl('username')).toBe( - 'default-image-url-webp'); + 'default-image-url-webp' + ); }); - describe('when fetching dashboard successfully and on explorations tab', - () => { - let dashboardData = { - explorations_list: [ - { - human_readable_contributors_summary: { - username: { - num_commits: 3 - } + describe('when fetching dashboard successfully and on explorations tab', () => { + let dashboardData = { + explorations_list: [ + { + human_readable_contributors_summary: { + username: { + num_commits: 3, }, - category: 'Algebra', - community_owned: false, - tags: [], - title: 'Testing Exploration', - created_on_msec: 1593786508029.501, - num_total_threads: 0, - num_views: 1, - last_updated_msec: 1593786607552.753, - status: 'public', - num_open_threads: 0, - thumbnail_icon_url: '/subjects/Algebra.svg', - language_code: 'en', - objective: 'To test exploration recommendations', - id: 'hi27Jix1QGbT', - thumbnail_bg_color: '#cc4b00', - activity_type: 'exploration', - ratings: { - 1: 0, - 2: 0, - 3: 0, - 4: 0, - 5: 0 - } - } - ], - collections_list: [], - subscribers_list: [], - dashboard_stats: { - average_ratings: 0, - num_ratings: 0, - total_open_feedback: 0, - total_plays: 10 - }, - last_week_stats: { - average_ratings: 0, - num_ratings: 0, - total_open_feedback: 0, - total_plays: 5 + }, + category: 'Algebra', + community_owned: false, + tags: [], + title: 'Testing Exploration', + created_on_msec: 1593786508029.501, + num_total_threads: 0, + num_views: 1, + last_updated_msec: 1593786607552.753, + status: 'public', + num_open_threads: 0, + thumbnail_icon_url: '/subjects/Algebra.svg', + language_code: 'en', + objective: 'To test exploration recommendations', + id: 'hi27Jix1QGbT', + thumbnail_bg_color: '#cc4b00', + activity_type: 'exploration', + ratings: { + 1: 0, + 2: 0, + 3: 0, + 4: 0, + 5: 0, + }, }, - display_preference: 'card', - threads_for_created_suggestions_list: [{ + ], + collections_list: [], + subscribers_list: [], + dashboard_stats: { + average_ratings: 0, + num_ratings: 0, + total_open_feedback: 0, + total_plays: 10, + }, + last_week_stats: { + average_ratings: 0, + num_ratings: 0, + total_open_feedback: 0, + total_plays: 5, + }, + display_preference: 'card', + threads_for_created_suggestions_list: [ + { status: '', subject: '', summary: '', @@ -215,8 +247,10 @@ describe('Creator Dashboard Page Component', () => { thread_id: 'exp1', last_nonempty_message_author: '', last_nonempty_message_text: '', - }], - created_suggestions_list: [{ + }, + ], + created_suggestions_list: [ + { suggestion_type: 'edit_exploration_state_content', suggestion_id: 'exp1', target_type: '', @@ -225,11 +259,12 @@ describe('Creator Dashboard Page Component', () => { author_name: '', change: { state_name: '', - new_value: { html: ''}, - old_value: { html: ''}, + new_value: {html: ''}, + old_value: {html: ''}, }, - last_updated_msecs: 0 - }, { + last_updated_msecs: 0, + }, + { suggestion_type: 'edit_exploration_state_content', suggestion_id: 'exp2', target_type: '', @@ -238,12 +273,14 @@ describe('Creator Dashboard Page Component', () => { author_name: '', change: { state_name: '', - new_value: { html: ''}, - old_value: { html: ''}, + new_value: {html: ''}, + old_value: {html: ''}, }, - last_updated_msecs: 0 - }], - threads_for_suggestions_to_review_list: [{ + last_updated_msecs: 0, + }, + ], + threads_for_suggestions_to_review_list: [ + { status: '', subject: '', summary: '', @@ -253,8 +290,10 @@ describe('Creator Dashboard Page Component', () => { thread_id: 'exp2', last_nonempty_message_author: '', last_nonempty_message_text: '', - }], - suggestions_to_review_list: [{ + }, + ], + suggestions_to_review_list: [ + { suggestion_type: 'edit_exploration_state_content', suggestion_id: 'exp2', target_type: '', @@ -263,11 +302,12 @@ describe('Creator Dashboard Page Component', () => { author_name: '', change: { state_name: '', - new_value: { html: ''}, - old_value: { html: ''}, + new_value: {html: ''}, + old_value: {html: ''}, }, - last_updated_msecs: 0 - }, { + last_updated_msecs: 0, + }, + { suggestion_type: 'edit_exploration_state_content', suggestion_id: 'exp1', target_type: '', @@ -276,193 +316,236 @@ describe('Creator Dashboard Page Component', () => { author_name: '', change: { state_name: '', - new_value: { html: ''}, - old_value: { html: ''}, + new_value: {html: ''}, + old_value: {html: ''}, }, - last_updated_msecs: 0 - }] - }; - - beforeEach(waitForAsync(() => { - spyOn(creatorDashboardBackendApiService, 'fetchDashboardDataAsync') - .and.returnValue(Promise.resolve({ - dashboardStats: CreatorDashboardStats - .createFromBackendDict(dashboardData.dashboard_stats), - // Because lastWeekStats may be null. - lastWeekStats: dashboardData.last_week_stats ? ( - CreatorDashboardStats - .createFromBackendDict(dashboardData.last_week_stats)) : null, - displayPreference: dashboardData.display_preference, - subscribersList: dashboardData.subscribers_list.map( - subscriber => ProfileSummary - .createFromSubscriberBackendDict(subscriber)), - explorationsList: dashboardData.explorations_list.map( - expSummary => CreatorExplorationSummary - .createFromBackendDict(expSummary)), - collectionsList: dashboardData.collections_list.map( - collectionSummary => CollectionSummary - .createFromBackendDict(collectionSummary)) - } as CreatorDashboardData)); - spyOn(userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(userInfo as UserInfo)); - - component.ngOnInit(); - })); - - it('should save the exploration format view in the backend when creator' + - ' changes the format view', () => { + last_updated_msecs: 0, + }, + ], + }; + + beforeEach(waitForAsync(() => { + spyOn( + creatorDashboardBackendApiService, + 'fetchDashboardDataAsync' + ).and.returnValue( + Promise.resolve({ + dashboardStats: CreatorDashboardStats.createFromBackendDict( + dashboardData.dashboard_stats + ), + // Because lastWeekStats may be null. + lastWeekStats: dashboardData.last_week_stats + ? CreatorDashboardStats.createFromBackendDict( + dashboardData.last_week_stats + ) + : null, + displayPreference: dashboardData.display_preference, + subscribersList: dashboardData.subscribers_list.map(subscriber => + ProfileSummary.createFromSubscriberBackendDict(subscriber) + ), + explorationsList: dashboardData.explorations_list.map(expSummary => + CreatorExplorationSummary.createFromBackendDict(expSummary) + ), + collectionsList: dashboardData.collections_list.map( + collectionSummary => + CollectionSummary.createFromBackendDict(collectionSummary) + ), + } as CreatorDashboardData) + ); + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve(userInfo as UserInfo) + ); + + component.ngOnInit(); + })); + + it( + 'should save the exploration format view in the backend when creator' + + ' changes the format view', + () => { let spyObj = spyOn( - creatorDashboardBackendApiService, 'postExplorationViewAsync') - .and.returnValue(Promise.resolve()); + creatorDashboardBackendApiService, + 'postExplorationViewAsync' + ).and.returnValue(Promise.resolve()); component.setMyExplorationsView('a'); expect(spyObj).toHaveBeenCalled(); expect(component.myExplorationsView).toBe('a'); - }); + } + ); - it('should reverse the sort order of explorations when the creator' + - ' re-selects the current sorting type', () => { + it( + 'should reverse the sort order of explorations when the creator' + + ' re-selects the current sorting type', + () => { expect(component.isCurrentSortDescending).toBeTrue(); expect(component.currentSortType).toBe('numOpenThreads'); component.setExplorationsSortingOptions('numOpenThreads'); expect(component.isCurrentSortDescending).toBeFalse(); - }); + } + ); - it('should update the exploration sort order based on the' + - ' option chosen by the creator', () => { + it( + 'should update the exploration sort order based on the' + + ' option chosen by the creator', + () => { component.setExplorationsSortingOptions('new_open'); expect(component.currentSortType).toBe('new_open'); - }); + } + ); - it('should reverse the sort order of subscriptions when the creator' + - ' re-selects the current sorting type', () => { + it( + 'should reverse the sort order of subscriptions when the creator' + + ' re-selects the current sorting type', + () => { expect(component.isCurrentSubscriptionSortDescending).toBeTrue(); expect(component.currentSubscribersSortType).toBe('username'); component.setSubscriptionSortingOptions('username'); expect(component.isCurrentSubscriptionSortDescending).toBeFalse(); - }); + } + ); - it('should update the subscription sort order based on the' + - ' option chosen by the creator', () => { + it( + 'should update the subscription sort order based on the' + + ' option chosen by the creator', + () => { component.setSubscriptionSortingOptions('new_subscriber'); expect(component.currentSubscribersSortType).toBe('new_subscriber'); - }); + } + ); - it('should sort subscription list by username', () => { - expect(component.currentSubscribersSortType).toBe('username'); - expect(component.sortSubscriptionFunction()).toBe('username'); - }); - - it('should not sort subscription list by impact given empty object', - () => { - component.setSubscriptionSortingOptions('impact'); - expect(component.currentSubscribersSortType).toBe('impact'); - expect(component.sortSubscriptionFunction()).toBe('impact'); - }); - - it('should sort exploration list by untitled explorations when title' + - ' is not provided and exploration is private', () => { + it('should sort subscription list by username', () => { + expect(component.currentSubscribersSortType).toBe('username'); + expect(component.sortSubscriptionFunction()).toBe('username'); + }); + + it('should not sort subscription list by impact given empty object', () => { + component.setSubscriptionSortingOptions('impact'); + expect(component.currentSubscribersSortType).toBe('impact'); + expect(component.sortSubscriptionFunction()).toBe('impact'); + }); + + it( + 'should sort exploration list by untitled explorations when title' + + ' is not provided and exploration is private', + () => { expect(component.currentSortType).toBe('numOpenThreads'); component.setExplorationsSortingOptions('title'); expect(component.currentSortType).toBe('title'); expect(component.sortByFunction()).toBe('title'); - }); + } + ); - it('should sort exploration list by options that is not last update' + - ' when trying to sort by number of views', () => { + it( + 'should sort exploration list by options that is not last update' + + ' when trying to sort by number of views', + () => { component.setExplorationsSortingOptions('ratings'); expect(component.currentSortType).toBe('ratings'); expect(component.sortByFunction()).toBe('default'); - }); + } + ); - it('should sort exploration list by last updated when last updated' + - ' value is provided', () => { + it( + 'should sort exploration list by last updated when last updated' + + ' value is provided', + () => { component.setExplorationsSortingOptions('lastUpdatedMsec'); expect(component.currentSortType).toBe('lastUpdatedMsec'); expect(component.sortByFunction()).toBe('lastUpdatedMsec'); - }); + } + ); - it('should expect 0 to be returned', () => { - expect(component.returnZero()).toBe(0); - }); + it('should expect 0 to be returned', () => { + expect(component.returnZero()).toBe(0); + }); - it('should not sort exploration list by options that is not last update' + - ' when trying to sort by number of views', () => { + it( + 'should not sort exploration list by options that is not last update' + + ' when trying to sort by number of views', + () => { component.setExplorationsSortingOptions('numViews'); expect(component.currentSortType).toBe('numViews'); expect(component.sortByFunction()).toBe('numViews'); - }); - - it('should update exploration view and publish text on resizing page', - fakeAsync(() => { - let innerWidthSpy = spyOnProperty(window, 'innerWidth'); - let spyObj = spyOn( - creatorDashboardBackendApiService, 'postExplorationViewAsync') - .and.returnValue(Promise.resolve()); + } + ); - component.setMyExplorationsView('list'); + it('should update exploration view and publish text on resizing page', fakeAsync(() => { + let innerWidthSpy = spyOnProperty(window, 'innerWidth'); + let spyObj = spyOn( + creatorDashboardBackendApiService, + 'postExplorationViewAsync' + ).and.returnValue(Promise.resolve()); + component.setMyExplorationsView('list'); - expect(spyObj).toHaveBeenCalled(); - expect(component.myExplorationsView).toBe('list'); + expect(spyObj).toHaveBeenCalled(); + expect(component.myExplorationsView).toBe('list'); - innerWidthSpy.and.callFake(() => 480); + innerWidthSpy.and.callFake(() => 480); - angular.element(window).triggerHandler('resize'); + angular.element(window).triggerHandler('resize'); - expect(component.myExplorationsView).toBe('card'); - expect(component.publishText).toBe( - 'Publish the exploration to receive statistics.'); + expect(component.myExplorationsView).toBe('card'); + expect(component.publishText).toBe( + 'Publish the exploration to receive statistics.' + ); - innerWidthSpy.and.callFake(() => 768); + innerWidthSpy.and.callFake(() => 768); - angular.element(window).triggerHandler('resize'); + angular.element(window).triggerHandler('resize'); - expect(component.myExplorationsView).toBe('card'); - expect(component.publishText).toBe( - 'This exploration is private. Publish it to receive statistics.'); - })); - }); + expect(component.myExplorationsView).toBe('card'); + expect(component.publishText).toBe( + 'This exploration is private. Publish it to receive statistics.' + ); + })); + }); describe('when on collections tab', () => { let dashboardData = { explorations_list: [], - collections_list: [{ - last_updated: 1591296737470.528, - community_owned: false, - objective: 'Test Objective', - id: '44LKoKLlIbGe', - thumbnail_icon_url: '/subjects/Algebra.svg', - language_code: 'en', - thumbnail_bg_color: '#cc4b00', - created_on: 1591296635736.666, - status: 'public', - category: 'Algebra', - title: 'Test Title', - node_count: 0 - }], + collections_list: [ + { + last_updated: 1591296737470.528, + community_owned: false, + objective: 'Test Objective', + id: '44LKoKLlIbGe', + thumbnail_icon_url: '/subjects/Algebra.svg', + language_code: 'en', + thumbnail_bg_color: '#cc4b00', + created_on: 1591296635736.666, + status: 'public', + category: 'Algebra', + title: 'Test Title', + node_count: 0, + }, + ], subscribers_list: [], dashboard_stats: { average_ratings: 0, num_ratings: 0, total_open_feedback: 0, - total_plays: 10 + total_plays: 10, }, last_week_stats: null, display_preference: [], threads_for_created_suggestions_list: [], created_suggestions_list: [], threads_for_suggestions_to_review_list: [], - suggestions_to_review_list: [] + suggestions_to_review_list: [], }; beforeEach(waitForAsync(() => { - spyOn(creatorDashboardBackendApiService, 'fetchDashboardDataAsync') + spyOn( + creatorDashboardBackendApiService, + 'fetchDashboardDataAsync' + ).and.returnValue( // This throws "Type object is not assignable to type // 'CreatorDashboardData'." We need to suppress this error // because of the need to test validations. This throws an @@ -470,25 +553,27 @@ describe('Creator Dashboard Page Component', () => { // suppress this error because the case where the returned // data is an empty object is also important to test. // @ts-ignore - .and.returnValue(Promise.resolve({ - dashboardStats: CreatorDashboardStats - .createFromBackendDict(dashboardData.dashboard_stats), + Promise.resolve({ + dashboardStats: CreatorDashboardStats.createFromBackendDict( + dashboardData.dashboard_stats + ), // Because lastWeekStats may be null. - lastWeekStats: dashboardData.last_week_stats ? ( - CreatorDashboardStats.createFromBackendDict( - // This throws "Argument of type 'null' is not assignable to - // parameter of type 'object'." We need to suppress this error - // because of the need to test validations. - // @ts-ignore - dashboardData.last_week_stats) - ) : null, + lastWeekStats: dashboardData.last_week_stats + ? CreatorDashboardStats.createFromBackendDict( + // This throws "Argument of type 'null' is not assignable to + // parameter of type 'object'." We need to suppress this error + // because of the need to test validations. + // @ts-ignore + dashboardData.last_week_stats + ) + : null, displayPreference: dashboardData.display_preference, - subscribersList: dashboardData.subscribers_list.map( - subscriber => ProfileSummary - .createFromSubscriberBackendDict(subscriber)), - explorationsList: dashboardData.explorations_list.map( - expSummary => CreatorExplorationSummary - .createFromBackendDict(expSummary)), + subscribersList: dashboardData.subscribers_list.map(subscriber => + ProfileSummary.createFromSubscriberBackendDict(subscriber) + ), + explorationsList: dashboardData.explorations_list.map(expSummary => + CreatorExplorationSummary.createFromBackendDict(expSummary) + ), collectionsList: dashboardData.collections_list.map( // This throws "Type object is not assignable to type // 'CreatorDashboardData'." We need to suppress this error @@ -497,8 +582,8 @@ describe('Creator Dashboard Page Component', () => { // suppress this error because the case where the returned // data is an empty object is also important to test. // @ts-ignore - (collectionSummary: CollectionSummary) => CollectionSummary - .createFromBackendDict( + (collectionSummary: CollectionSummary) => + CollectionSummary.createFromBackendDict( // This throws "Type object is not assignable to type // 'CreatorDashboardData'." We need to suppress this error // because of the need to test validations. This throws an @@ -506,8 +591,11 @@ describe('Creator Dashboard Page Component', () => { // suppress this error because the case where the returned // data is an empty object is also important to test. // @ts-ignore - collectionSummary as CollectionSummaryBackendDict)) - } as CreatorDashboardData)); + collectionSummary as CollectionSummaryBackendDict + ) + ), + } as CreatorDashboardData) + ); component.ngOnInit(); })); diff --git a/core/templates/pages/creator-dashboard-page/creator-dashboard-page.component.ts b/core/templates/pages/creator-dashboard-page/creator-dashboard-page.component.ts index 27891cf4ec82..fee930faac95 100644 --- a/core/templates/pages/creator-dashboard-page/creator-dashboard-page.component.ts +++ b/core/templates/pages/creator-dashboard-page/creator-dashboard-page.component.ts @@ -16,32 +16,32 @@ * @fileoverview Component for the creator dashboard. */ -import { Component } from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { CreatorDashboardBackendApiService } from 'domain/creator_dashboard/creator-dashboard-backend-api.service'; -import { CreatorDashboardConstants } from './creator-dashboard-page.constants'; -import { RatingComputationService } from 'components/ratings/rating-computation/rating-computation.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { LoaderService } from 'services/loader.service'; -import { UserService } from 'services/user.service'; -import { AlertsService } from 'services/alerts.service'; -import { DateTimeFormatService } from 'services/date-time-format.service'; -import { ThreadStatusDisplayService } from 'pages/exploration-editor-page/feedback-tab/services/thread-status-display.service'; -import { ExplorationCreationService } from 'components/entity-creation-services/exploration-creation.service'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { forkJoin } from 'rxjs'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { CreatorDashboardData } from 'domain/creator_dashboard/creator-dashboard-backend-api.service'; -import { ProfileSummary } from 'domain/user/profile-summary.model'; -import { CreatorExplorationSummary } from 'domain/summary/creator-exploration-summary.model'; -import { CollectionSummary } from 'domain/collection/collection-summary.model'; -import { ExplorationRatings } from 'domain/summary/learner-exploration-summary.model'; -import { CreatorDashboardStats } from 'domain/creator_dashboard/creator-dashboard-stats.model'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; +import {Component} from '@angular/core'; +import {AppConstants} from 'app.constants'; +import {CreatorDashboardBackendApiService} from 'domain/creator_dashboard/creator-dashboard-backend-api.service'; +import {CreatorDashboardConstants} from './creator-dashboard-page.constants'; +import {RatingComputationService} from 'components/ratings/rating-computation/rating-computation.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {LoaderService} from 'services/loader.service'; +import {UserService} from 'services/user.service'; +import {AlertsService} from 'services/alerts.service'; +import {DateTimeFormatService} from 'services/date-time-format.service'; +import {ThreadStatusDisplayService} from 'pages/exploration-editor-page/feedback-tab/services/thread-status-display.service'; +import {ExplorationCreationService} from 'components/entity-creation-services/exploration-creation.service'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {forkJoin} from 'rxjs'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {CreatorDashboardData} from 'domain/creator_dashboard/creator-dashboard-backend-api.service'; +import {ProfileSummary} from 'domain/user/profile-summary.model'; +import {CreatorExplorationSummary} from 'domain/summary/creator-exploration-summary.model'; +import {CollectionSummary} from 'domain/collection/collection-summary.model'; +import {ExplorationRatings} from 'domain/summary/learner-exploration-summary.model'; +import {CreatorDashboardStats} from 'domain/creator_dashboard/creator-dashboard-stats.model'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; @Component({ selector: 'oppia-creator-dashboard-page', - templateUrl: './creator-dashboard-page.component.html' + templateUrl: './creator-dashboard-page.component.html', }) export class CreatorDashboardPageComponent { // These properties are initialized using Angular lifecycle hooks @@ -63,8 +63,7 @@ export class CreatorDashboardPageComponent { getLocaleAbbreviatedDatetimeString!: (millisSinceEpoch: number) => string; getHumanReadableStatus!: (status: string) => string; emptyDashboardImgUrl!: string; - getAverageRating!: ( - (ratingFrequencies: ExplorationRatings) => number | null); + getAverageRating!: (ratingFrequencies: ExplorationRatings) => number | null; isCurrentSortDescending: boolean = false; isCurrentSubscriptionSortDescending: boolean = false; @@ -88,8 +87,7 @@ export class CreatorDashboardPageComponent { AppConstants.DEFAULT_TWITTER_SHARE_MESSAGE_EDITOR; constructor( - private creatorDashboardBackendApiService: - CreatorDashboardBackendApiService, + private creatorDashboardBackendApiService: CreatorDashboardBackendApiService, private ratingComputationService: RatingComputationService, private urlInterpolationService: UrlInterpolationService, private loaderService: LoaderService, @@ -99,27 +97,25 @@ export class CreatorDashboardPageComponent { private dateTimeFormatService: DateTimeFormatService, private threadStatusDisplayService: ThreadStatusDisplayService, private explorationCreationService: ExplorationCreationService, - private windowRef: WindowRef, + private windowRef: WindowRef ) {} EXP_PUBLISH_TEXTS = { - defaultText: ( - 'This exploration is private. Publish it to receive statistics.'), - smText: 'Publish the exploration to receive statistics.' + defaultText: + 'This exploration is private. Publish it to receive statistics.', + smText: 'Publish the exploration to receive statistics.', }; - userDashboardDisplayPreference = ( - AppConstants.ALLOWED_CREATOR_DASHBOARD_DISPLAY_PREFS.CARD); + userDashboardDisplayPreference = + AppConstants.ALLOWED_CREATOR_DASHBOARD_DISPLAY_PREFS.CARD; getProfileImagePngDataUrl(username: string): string { - let [pngImageUrl, _] = this.userService.getProfileImageDataUrl( - username); + let [pngImageUrl, _] = this.userService.getProfileImageDataUrl(username); return pngImageUrl; } getProfileImageWebpDataUrl(username: string): string { - let [_, webpImageUrl] = this.userService.getProfileImageDataUrl( - username); + let [_, webpImageUrl] = this.userService.getProfileImageDataUrl(username); return webpImageUrl; } @@ -137,12 +133,13 @@ export class CreatorDashboardPageComponent { setMyExplorationsView(newViewType: string): void { this.myExplorationsView = newViewType; - this.creatorDashboardBackendApiService.postExplorationViewAsync( - newViewType).then(() => {}); + this.creatorDashboardBackendApiService + .postExplorationViewAsync(newViewType) + .then(() => {}); } checkMobileView(): boolean { - return (this.windowRef.nativeWindow.innerWidth < 500); + return this.windowRef.nativeWindow.innerWidth < 500; } showUsernamePopover(subscriberUsername: string | string[]): string { @@ -161,7 +158,7 @@ export class CreatorDashboardPageComponent { } checkTabletView(): boolean { - return (this.windowDimensionsService.getWidth() < 768); + return this.windowDimensionsService.getWidth() < 768; } updatesGivenScreenWidth(): void { @@ -169,8 +166,8 @@ export class CreatorDashboardPageComponent { // For mobile users, the view of the creators // exploration list is shown only in // the card view and can't be switched to list view. - this.myExplorationsView = ( - AppConstants.ALLOWED_CREATOR_DASHBOARD_DISPLAY_PREFS.CARD); + this.myExplorationsView = + AppConstants.ALLOWED_CREATOR_DASHBOARD_DISPLAY_PREFS.CARD; this.publishText = this.EXP_PUBLISH_TEXTS.smText; } else { // For computer users or users operating in larger screen size @@ -191,8 +188,8 @@ export class CreatorDashboardPageComponent { setSubscriptionSortingOptions(sortType: string): void { if (sortType === this.currentSubscribersSortType) { - this.isCurrentSubscriptionSortDescending = ( - !this.isCurrentSubscriptionSortDescending); + this.isCurrentSubscriptionSortDescending = + !this.isCurrentSubscriptionSortDescending; } else { this.currentSubscribersSortType = sortType; } @@ -205,7 +202,8 @@ export class CreatorDashboardPageComponent { sortByFunction(): string { if ( this.currentSortType === - CreatorDashboardConstants.EXPLORATIONS_SORT_BY_KEYS.RATING) { + CreatorDashboardConstants.EXPLORATIONS_SORT_BY_KEYS.RATING + ) { // TODO(sll): Find a better way to sort explorations according to // average ratings. Currently there is no parameter as such // average ratings in entities received by SortByPipe. @@ -222,67 +220,64 @@ export class CreatorDashboardPageComponent { ngOnInit(): void { this.loaderService.showLoadingScreen('Loading'); let userInfoPromise = this.userService.getUserInfoAsync(); - userInfoPromise.then((userInfo) => { + userInfoPromise.then(userInfo => { this.canCreateCollections = userInfo.canCreateCollections(); }); - let dashboardDataPromise = ( - this.creatorDashboardBackendApiService.fetchDashboardDataAsync()); - dashboardDataPromise.then( - (response: CreatorDashboardData) => { - // The following condition is required for Karma testing. The - // Angular HttpClient returns an Observable which when converted - // to a promise does not have the 'data' key but the AngularJS - // mocks of services using HttpClient use $http which return - // promise and the content is contained in the 'data' key. - // Therefore the following condition checks for presence of - // 'response.data' which would be the case in AngularJS testing - // but assigns 'response' if the former is not present which is - // the case with HttpClient. - let responseData = response; - this.currentSortType = ( - CreatorDashboardConstants. - EXPLORATIONS_SORT_BY_KEYS.OPEN_FEEDBACK); - this.currentSubscribersSortType = - CreatorDashboardConstants.SUBSCRIPTION_SORT_BY_KEYS.USERNAME; - this.isCurrentSortDescending = true; - this.isCurrentSubscriptionSortDescending = true; - this.explorationsList = responseData.explorationsList; - this.collectionsList = responseData.collectionsList; - this.subscribersList = responseData.subscribersList; - this.dashboardStats = responseData.dashboardStats; - this.lastWeekStats = responseData.lastWeekStats; - this.myExplorationsView = responseData.displayPreference; - - if (this.dashboardStats && this.lastWeekStats) { - this.relativeChangeInTotalPlays = ( - this.dashboardStats.totalPlays - ( - this.lastWeekStats.totalPlays) - ); - } - - if (this.explorationsList.length === 0 && - this.collectionsList.length > 0) { - this.activeTab = 'myCollections'; - } else { - this.activeTab = 'myExplorations'; - } + let dashboardDataPromise = + this.creatorDashboardBackendApiService.fetchDashboardDataAsync(); + dashboardDataPromise.then((response: CreatorDashboardData) => { + // The following condition is required for Karma testing. The + // Angular HttpClient returns an Observable which when converted + // to a promise does not have the 'data' key but the AngularJS + // mocks of services using HttpClient use $http which return + // promise and the content is contained in the 'data' key. + // Therefore the following condition checks for presence of + // 'response.data' which would be the case in AngularJS testing + // but assigns 'response' if the former is not present which is + // the case with HttpClient. + let responseData = response; + this.currentSortType = + CreatorDashboardConstants.EXPLORATIONS_SORT_BY_KEYS.OPEN_FEEDBACK; + this.currentSubscribersSortType = + CreatorDashboardConstants.SUBSCRIPTION_SORT_BY_KEYS.USERNAME; + this.isCurrentSortDescending = true; + this.isCurrentSubscriptionSortDescending = true; + this.explorationsList = responseData.explorationsList; + this.collectionsList = responseData.collectionsList; + this.subscribersList = responseData.subscribersList; + this.dashboardStats = responseData.dashboardStats; + this.lastWeekStats = responseData.lastWeekStats; + this.myExplorationsView = responseData.displayPreference; + + if (this.dashboardStats && this.lastWeekStats) { + this.relativeChangeInTotalPlays = + this.dashboardStats.totalPlays - this.lastWeekStats.totalPlays; } - ); + + if ( + this.explorationsList.length === 0 && + this.collectionsList.length > 0 + ) { + this.activeTab = 'myCollections'; + } else { + this.activeTab = 'myExplorations'; + } + }); forkJoin([userInfoPromise, dashboardDataPromise]).subscribe(() => { this.loaderService.hideLoadingScreen(); }); - this.getAverageRating = this.ratingComputationService - .computeAverageRating; - this.getLocaleAbbreviatedDatetimeString = ( - this.dateTimeFormatService.getLocaleAbbreviatedDatetimeString); - this.getHumanReadableStatus = ( - this.threadStatusDisplayService.getHumanReadableStatus); + this.getAverageRating = this.ratingComputationService.computeAverageRating; + this.getLocaleAbbreviatedDatetimeString = + this.dateTimeFormatService.getLocaleAbbreviatedDatetimeString; + this.getHumanReadableStatus = + this.threadStatusDisplayService.getHumanReadableStatus; - this.emptyDashboardImgUrl = this.urlInterpolationService - .getStaticImageUrl('/general/empty_dashboard.svg'); + this.emptyDashboardImgUrl = this.urlInterpolationService.getStaticImageUrl( + '/general/empty_dashboard.svg' + ); this.canReviewActiveThread = false; this.updatesGivenScreenWidth(); angular.element(this.windowRef.nativeWindow).on('resize', () => { @@ -303,7 +298,9 @@ export class CreatorDashboardPageComponent { } } -angular.module('oppia').directive('oppiaCreatorDashboardPage', +angular.module('oppia').directive( + 'oppiaCreatorDashboardPage', downgradeComponent({ - component: CreatorDashboardPageComponent - }) as angular.IDirectiveFactory); + component: CreatorDashboardPageComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/creator-dashboard-page/creator-dashboard-page.constants.ajs.ts b/core/templates/pages/creator-dashboard-page/creator-dashboard-page.constants.ajs.ts index 670871187106..1e53fdffe1e2 100644 --- a/core/templates/pages/creator-dashboard-page/creator-dashboard-page.constants.ajs.ts +++ b/core/templates/pages/creator-dashboard-page/creator-dashboard-page.constants.ajs.ts @@ -18,30 +18,39 @@ // TODO(#7092): Delete this file once migration is complete and these AngularJS // equivalents of the Angular constants are no longer needed. -import { CreatorDashboardConstants } from - 'pages/creator-dashboard-page/creator-dashboard-page.constants'; +import {CreatorDashboardConstants} from 'pages/creator-dashboard-page/creator-dashboard-page.constants'; -angular.module('oppia').constant( - 'EXPLORATION_DROPDOWN_STATS', - CreatorDashboardConstants.EXPLORATION_DROPDOWN_STATS -); +angular + .module('oppia') + .constant( + 'EXPLORATION_DROPDOWN_STATS', + CreatorDashboardConstants.EXPLORATION_DROPDOWN_STATS + ); -angular.module('oppia').constant( - 'EXPLORATIONS_SORT_BY_KEYS', - CreatorDashboardConstants.EXPLORATIONS_SORT_BY_KEYS -); +angular + .module('oppia') + .constant( + 'EXPLORATIONS_SORT_BY_KEYS', + CreatorDashboardConstants.EXPLORATIONS_SORT_BY_KEYS + ); -angular.module('oppia').constant( - 'HUMAN_READABLE_EXPLORATIONS_SORT_BY_KEYS', - CreatorDashboardConstants.HUMAN_READABLE_EXPLORATIONS_SORT_BY_KEYS -); +angular + .module('oppia') + .constant( + 'HUMAN_READABLE_EXPLORATIONS_SORT_BY_KEYS', + CreatorDashboardConstants.HUMAN_READABLE_EXPLORATIONS_SORT_BY_KEYS + ); -angular.module('oppia').constant( - 'SUBSCRIPTION_SORT_BY_KEYS', - CreatorDashboardConstants.SUBSCRIPTION_SORT_BY_KEYS -); +angular + .module('oppia') + .constant( + 'SUBSCRIPTION_SORT_BY_KEYS', + CreatorDashboardConstants.SUBSCRIPTION_SORT_BY_KEYS + ); -angular.module('oppia').constant( - 'HUMAN_READABLE_SUBSCRIPTION_SORT_BY_KEYS', - CreatorDashboardConstants.HUMAN_READABLE_SUBSCRIPTION_SORT_BY_KEYS -); +angular + .module('oppia') + .constant( + 'HUMAN_READABLE_SUBSCRIPTION_SORT_BY_KEYS', + CreatorDashboardConstants.HUMAN_READABLE_SUBSCRIPTION_SORT_BY_KEYS + ); diff --git a/core/templates/pages/creator-dashboard-page/creator-dashboard-page.constants.ts b/core/templates/pages/creator-dashboard-page/creator-dashboard-page.constants.ts index 87bf55c2b61f..85aafe54e7cf 100644 --- a/core/templates/pages/creator-dashboard-page/creator-dashboard-page.constants.ts +++ b/core/templates/pages/creator-dashboard-page/creator-dashboard-page.constants.ts @@ -18,7 +18,7 @@ export const CreatorDashboardConstants = { EXPLORATION_DROPDOWN_STATS: { - OPEN_FEEDBACK: 'open_feedback' + OPEN_FEEDBACK: 'open_feedback', }, EXPLORATIONS_SORT_BY_KEYS: { @@ -26,7 +26,7 @@ export const CreatorDashboardConstants = { RATING: 'ratings', NUM_VIEWS: 'numViews', OPEN_FEEDBACK: 'numOpenThreads', - LAST_UPDATED: 'lastUpdatedMsec' + LAST_UPDATED: 'lastUpdatedMsec', }, HUMAN_READABLE_EXPLORATIONS_SORT_BY_KEYS: { @@ -34,16 +34,16 @@ export const CreatorDashboardConstants = { RATING: 'I18N_DASHBOARD_EXPLORATIONS_SORT_BY_AVERAGE_RATING', NUM_VIEWS: 'I18N_DASHBOARD_EXPLORATIONS_SORT_BY_TOTAL_PLAYS', OPEN_FEEDBACK: 'I18N_DASHBOARD_EXPLORATIONS_SORT_BY_OPEN_FEEDBACK', - LAST_UPDATED: 'I18N_DASHBOARD_EXPLORATIONS_SORT_BY_LAST_UPDATED' + LAST_UPDATED: 'I18N_DASHBOARD_EXPLORATIONS_SORT_BY_LAST_UPDATED', }, SUBSCRIPTION_SORT_BY_KEYS: { USERNAME: 'username', - IMPACT: 'impact' + IMPACT: 'impact', }, HUMAN_READABLE_SUBSCRIPTION_SORT_BY_KEYS: { USERNAME: 'Username', - IMPACT: 'Impact' - } + IMPACT: 'Impact', + }, } as const; diff --git a/core/templates/pages/creator-dashboard-page/creator-dashboard-page.import.ts b/core/templates/pages/creator-dashboard-page/creator-dashboard-page.import.ts index 79e1958b5256..90c8782bc5e5 100644 --- a/core/templates/pages/creator-dashboard-page/creator-dashboard-page.import.ts +++ b/core/templates/pages/creator-dashboard-page/creator-dashboard-page.import.ts @@ -23,9 +23,15 @@ import 'third-party-imports/ui-tree.import'; import uiValidate from 'angular-ui-validate'; angular.module('oppia', [ - require('angular-cookies'), 'ngAnimate', - 'ngMaterial', 'ngSanitize', 'ngTouch', 'pascalprecht.translate', - 'ui.bootstrap', 'ui.tree', uiValidate + require('angular-cookies'), + 'ngAnimate', + 'ngMaterial', + 'ngSanitize', + 'ngTouch', + 'pascalprecht.translate', + 'ui.bootstrap', + 'ui.tree', + uiValidate, ]); require('Polyfills.ts'); diff --git a/core/templates/pages/creator-dashboard-page/creator-dashboard-page.module.ts b/core/templates/pages/creator-dashboard-page/creator-dashboard-page.module.ts index 745e24fb4142..765945f6e893 100644 --- a/core/templates/pages/creator-dashboard-page/creator-dashboard-page.module.ts +++ b/core/templates/pages/creator-dashboard-page/creator-dashboard-page.module.ts @@ -16,27 +16,27 @@ * @fileoverview Module for the collection player page. */ -import { APP_INITIALIZER, NgModule, StaticProvider } from '@angular/core'; -import { BrowserModule, HAMMER_GESTURE_CONFIG } from '@angular/platform-browser'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { HttpClientModule } from '@angular/common/http'; -import { HTTP_INTERCEPTORS } from '@angular/common/http'; -import { RouterModule } from '@angular/router'; -import { APP_BASE_HREF } from '@angular/common'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; - -import { RequestInterceptor } from 'services/request-interceptor.service'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { OppiaAngularRootComponent } from - 'components/oppia-angular-root.component'; -import { platformFeatureInitFactory, PlatformFeatureService } from - 'services/platform-feature.service'; -import { SmartRouterModule } from 'hybrid-router-module-provider'; -import { InteractionExtensionsModule } from 'interactions/interactions.module'; -import { MyHammerConfig, toastrConfig } from 'pages/oppia-root/app.module'; -import { CreatorDashboardPageComponent } from './creator-dashboard-page.component'; -import { AppErrorHandlerProvider } from 'pages/oppia-root/app-error-handler'; +import {APP_INITIALIZER, NgModule, StaticProvider} from '@angular/core'; +import {BrowserModule, HAMMER_GESTURE_CONFIG} from '@angular/platform-browser'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {HttpClientModule} from '@angular/common/http'; +import {HTTP_INTERCEPTORS} from '@angular/common/http'; +import {RouterModule} from '@angular/router'; +import {APP_BASE_HREF} from '@angular/common'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {RequestInterceptor} from 'services/request-interceptor.service'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {OppiaAngularRootComponent} from 'components/oppia-angular-root.component'; +import { + platformFeatureInitFactory, + PlatformFeatureService, +} from 'services/platform-feature.service'; +import {SmartRouterModule} from 'hybrid-router-module-provider'; +import {InteractionExtensionsModule} from 'interactions/interactions.module'; +import {MyHammerConfig, toastrConfig} from 'pages/oppia-root/app.module'; +import {CreatorDashboardPageComponent} from './creator-dashboard-page.component'; +import {AppErrorHandlerProvider} from 'pages/oppia-root/app-error-handler'; @NgModule({ imports: [ @@ -49,47 +49,43 @@ import { AppErrorHandlerProvider } from 'pages/oppia-root/app-error-handler'; RouterModule.forRoot([]), InteractionExtensionsModule, SharedComponentsModule, - ToastrModule.forRoot(toastrConfig) - ], - declarations: [ - CreatorDashboardPageComponent - ], - entryComponents: [ - CreatorDashboardPageComponent + ToastrModule.forRoot(toastrConfig), ], + declarations: [CreatorDashboardPageComponent], + entryComponents: [CreatorDashboardPageComponent], providers: [ { provide: HTTP_INTERCEPTORS, useClass: RequestInterceptor, - multi: true + multi: true, }, { provide: APP_INITIALIZER, useFactory: platformFeatureInitFactory, deps: [PlatformFeatureService], - multi: true + multi: true, }, { provide: HAMMER_GESTURE_CONFIG, - useClass: MyHammerConfig + useClass: MyHammerConfig, }, AppErrorHandlerProvider, { provide: APP_BASE_HREF, - useValue: '/' - } - ] + useValue: '/', + }, + ], }) class CreatorDashboardPageModule { // Empty placeholder method to satisfy the `Compiler`. ngDoBootstrap() {} } -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { downgradeModule } from '@angular/upgrade/static'; -import { ToastrModule } from 'ngx-toastr'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {downgradeModule} from '@angular/upgrade/static'; +import {ToastrModule} from 'ngx-toastr'; -const bootstrapFnAsync = async(extraProviders: StaticProvider[]) => { +const bootstrapFnAsync = async (extraProviders: StaticProvider[]) => { const platformRef = platformBrowserDynamic(extraProviders); return platformRef.bootstrapModule(CreatorDashboardPageModule); }; @@ -104,5 +100,6 @@ angular.module('oppia').directive( // bootstrap the Angular 8. 'oppiaAngularRoot', downgradeComponent({ - component: OppiaAngularRootComponent - }) as angular.IDirectiveFactory); + component: OppiaAngularRootComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/creator-dashboard-page/modal-templates/create-activity-modal.component.spec.ts b/core/templates/pages/creator-dashboard-page/modal-templates/create-activity-modal.component.spec.ts index 84fa8fd90d97..a02ebad942f5 100644 --- a/core/templates/pages/creator-dashboard-page/modal-templates/create-activity-modal.component.spec.ts +++ b/core/templates/pages/creator-dashboard-page/modal-templates/create-activity-modal.component.spec.ts @@ -16,16 +16,20 @@ * @fileoverview Unit tests for CreateActivityModalComponent. */ -import { ComponentFixture, TestBed, fakeAsync, flushMicrotasks } from - '@angular/core/testing'; -import { ExplorationCreationService } from 'components/entity-creation-services/exploration-creation.service'; -import { CollectionCreationService } from 'components/entity-creation-services/collection-creation.service'; -import { CreateActivityModalComponent } from './create-activity-modal.component'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { UserService } from 'services/user.service'; -import { UserInfo } from 'domain/user/user-info.model'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; +import { + ComponentFixture, + TestBed, + fakeAsync, + flushMicrotasks, +} from '@angular/core/testing'; +import {ExplorationCreationService} from 'components/entity-creation-services/exploration-creation.service'; +import {CollectionCreationService} from 'components/entity-creation-services/collection-creation.service'; +import {CreateActivityModalComponent} from './create-activity-modal.component'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {UserService} from 'services/user.service'; +import {UserInfo} from 'domain/user/user-info.model'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; class MockActiveModal { dismiss(): void {} @@ -39,7 +43,7 @@ class MockCollectionCreationService { createNewCollection(): void {} } -describe('Create Activity Modal Component', () =>{ +describe('Create Activity Modal Component', () => { let component: CreateActivityModalComponent; let fixture: ComponentFixture; let collectionCreationService: CollectionCreationService; @@ -54,20 +58,23 @@ describe('Create Activity Modal Component', () =>{ providers: [ { provide: NgbActiveModal, - useClass: MockActiveModal }, + useClass: MockActiveModal, + }, { provide: ExplorationCreationService, - useClass: MockExplorationCreationService + useClass: MockExplorationCreationService, + }, + { + provide: CollectionCreationService, + useClass: MockCollectionCreationService, }, - { provide: CollectionCreationService, - useClass: MockCollectionCreationService - } - ] - }).compileComponents().then(() => { - fixture = TestBed.createComponent( - CreateActivityModalComponent); - component = fixture.componentInstance; - }); + ], + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(CreateActivityModalComponent); + component = fixture.componentInstance; + }); ngbActiveModal = TestBed.get(NgbActiveModal); explorationCreationService = TestBed.get(ExplorationCreationService); collectionCreationService = TestBed.get(CollectionCreationService); @@ -81,47 +88,59 @@ describe('Create Activity Modal Component', () =>{ fixture.destroy(); }); - it('should evalute component properties after component is initialized', - fakeAsync(() => { - const UserInfoObject = { - roles: ['USER_ROLE'], - is_moderator: false, - is_curriculum_admin: false, - is_super_admin: false, - is_topic_manager: false, - can_create_collections: true, - preferred_site_language_code: null, - username: 'tester', - email: 'test@test.com', - user_is_logged_in: true - }; - spyOn(userService, 'getUserInfoAsync').and.returnValue(Promise.resolve( - UserInfo.createFromBackendDict(UserInfoObject)) - ); - component.ngOnInit(); - flushMicrotasks(); - expect(component.canCreateCollections).toBeTrue(); - expect(component.getStaticImageUrl('/activity/exploration.svg')) - .toBe('/assets/images/activity/exploration.svg'); - expect(component.getStaticImageUrl('/activity/collection.svg')) - .toBe('/assets/images/activity/collection.svg'); - })); + it('should evalute component properties after component is initialized', fakeAsync(() => { + const UserInfoObject = { + roles: ['USER_ROLE'], + is_moderator: false, + is_curriculum_admin: false, + is_super_admin: false, + is_topic_manager: false, + can_create_collections: true, + preferred_site_language_code: null, + username: 'tester', + email: 'test@test.com', + user_is_logged_in: true, + }; + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve(UserInfo.createFromBackendDict(UserInfoObject)) + ); + component.ngOnInit(); + flushMicrotasks(); + expect(component.canCreateCollections).toBeTrue(); + expect(component.getStaticImageUrl('/activity/exploration.svg')).toBe( + '/assets/images/activity/exploration.svg' + ); + expect(component.getStaticImageUrl('/activity/collection.svg')).toBe( + '/assets/images/activity/collection.svg' + ); + })); - it('should create new exploration when choosing exploration as the new' + - ' activity', () => { - const dismissSpy = spyOn(ngbActiveModal, 'dismiss').and.callThrough(); - spyOn(explorationCreationService, 'createNewExploration').and.callThrough(); - component.chooseExploration(); - expect(explorationCreationService.createNewExploration).toHaveBeenCalled(); - expect(dismissSpy).toHaveBeenCalled(); - }); + it( + 'should create new exploration when choosing exploration as the new' + + ' activity', + () => { + const dismissSpy = spyOn(ngbActiveModal, 'dismiss').and.callThrough(); + spyOn( + explorationCreationService, + 'createNewExploration' + ).and.callThrough(); + component.chooseExploration(); + expect( + explorationCreationService.createNewExploration + ).toHaveBeenCalled(); + expect(dismissSpy).toHaveBeenCalled(); + } + ); - it('should create new collection when choosing collection as the new' + - ' activity', () => { - const dismissSpy = spyOn(ngbActiveModal, 'dismiss').and.callThrough(); - spyOn(collectionCreationService, 'createNewCollection').and.callThrough(); - component.chooseCollection(); - expect(collectionCreationService.createNewCollection).toHaveBeenCalled(); - expect(dismissSpy).toHaveBeenCalled(); - }); + it( + 'should create new collection when choosing collection as the new' + + ' activity', + () => { + const dismissSpy = spyOn(ngbActiveModal, 'dismiss').and.callThrough(); + spyOn(collectionCreationService, 'createNewCollection').and.callThrough(); + component.chooseCollection(); + expect(collectionCreationService.createNewCollection).toHaveBeenCalled(); + expect(dismissSpy).toHaveBeenCalled(); + } + ); }); diff --git a/core/templates/pages/creator-dashboard-page/modal-templates/create-activity-modal.component.ts b/core/templates/pages/creator-dashboard-page/modal-templates/create-activity-modal.component.ts index 69b8ad3772a4..d0993ffb82c2 100644 --- a/core/templates/pages/creator-dashboard-page/modal-templates/create-activity-modal.component.ts +++ b/core/templates/pages/creator-dashboard-page/modal-templates/create-activity-modal.component.ts @@ -16,12 +16,12 @@ * @fileoverview Component for the Create Exploration/Collection modal. */ -import { Component, OnInit } from '@angular/core'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { UserService } from 'services/user.service'; -import { ExplorationCreationService } from 'components/entity-creation-services/exploration-creation.service'; -import { CollectionCreationService } from 'components/entity-creation-services/collection-creation.service'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import {Component, OnInit} from '@angular/core'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {UserService} from 'services/user.service'; +import {ExplorationCreationService} from 'components/entity-creation-services/exploration-creation.service'; +import {CollectionCreationService} from 'components/entity-creation-services/collection-creation.service'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; @Component({ selector: 'create-activity-modal', @@ -43,9 +43,8 @@ export class CreateActivityModalComponent implements OnInit { ) {} ngOnInit(): void { - this.userService.getUserInfoAsync().then((userInfo) => { - this.canCreateCollections = ( - userInfo.canCreateCollections()); + this.userService.getUserInfoAsync().then(userInfo => { + this.canCreateCollections = userInfo.canCreateCollections(); }); } diff --git a/core/templates/pages/creator-dashboard-page/modal-templates/upload-activity-modal.component.spec.ts b/core/templates/pages/creator-dashboard-page/modal-templates/upload-activity-modal.component.spec.ts index c44227412aeb..0f3f0a9cee84 100644 --- a/core/templates/pages/creator-dashboard-page/modal-templates/upload-activity-modal.component.spec.ts +++ b/core/templates/pages/creator-dashboard-page/modal-templates/upload-activity-modal.component.spec.ts @@ -16,11 +16,16 @@ * @fileoverview Unit tests for UploadActivityModalComponent. */ -import { AlertsService } from 'services/alerts.service'; -import { ComponentFixture, fakeAsync, flushMicrotasks, TestBed } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { UploadActivityModalComponent } from './upload-activity-modal.component'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; +import {AlertsService} from 'services/alerts.service'; +import { + ComponentFixture, + fakeAsync, + flushMicrotasks, + TestBed, +} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {UploadActivityModalComponent} from './upload-activity-modal.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; class MockActiveModal { dismiss(): void { @@ -51,18 +56,19 @@ describe('Upload Activity Modal Component', () => { providers: [ { provide: NgbActiveModal, - useClass: MockActiveModal + useClass: MockActiveModal, }, { provide: AlertsService, - useClass: MockAlertsService - } - ] - }).compileComponents().then(() => { - fixture = TestBed.createComponent( - UploadActivityModalComponent); - component = fixture.componentInstance; - }); + useClass: MockAlertsService, + }, + ], + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(UploadActivityModalComponent); + component = fixture.componentInstance; + }); ngbActiveModal = TestBed.inject(NgbActiveModal); alertsService = TestBed.inject(AlertsService); })); @@ -72,7 +78,7 @@ describe('Upload Activity Modal Component', () => { let file = { size: 100, - name: 'file.mp3' + name: 'file.mp3', }; // TODO(#10113): Refactor the code to not use the DOM methods. @@ -85,12 +91,12 @@ describe('Upload Activity Modal Component', () => { // @ts-expect-error spyOn(document, 'getElementById').and.callFake(() => { return { - files: [file] + files: [file], }; }); component.save(); expect(dismissSpy).toHaveBeenCalledWith({ - yamlFile: file + yamlFile: file, }); })); @@ -108,13 +114,14 @@ describe('Upload Activity Modal Component', () => { // @ts-expect-error spyOn(document, 'getElementById').and.callFake(() => { return { - files: [] + files: [], }; }); component.save(); flushMicrotasks(); expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Empty file detected.'); + 'Empty file detected.' + ); expect(dismissSpy).not.toHaveBeenCalled(); })); @@ -144,7 +151,7 @@ describe('Upload Activity Modal Component', () => { // @ts-expect-error spyOn(document, 'getElementById').and.callFake(() => { return { - files: null + files: null, }; }); expect(() => { diff --git a/core/templates/pages/creator-dashboard-page/modal-templates/upload-activity-modal.component.ts b/core/templates/pages/creator-dashboard-page/modal-templates/upload-activity-modal.component.ts index 5ea7347bef34..2fc4ab1d9913 100644 --- a/core/templates/pages/creator-dashboard-page/modal-templates/upload-activity-modal.component.ts +++ b/core/templates/pages/creator-dashboard-page/modal-templates/upload-activity-modal.component.ts @@ -16,9 +16,9 @@ * @fileoverview Controller for upload activity modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { AlertsService } from 'services/alerts.service'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {AlertsService} from 'services/alerts.service'; interface ExplorationObj { yamlFile: File | null; @@ -30,16 +30,15 @@ interface ExplorationObj { }) export class UploadActivityModalComponent { constructor( - private alertsService: AlertsService, - private activeModal: NgbActiveModal + private alertsService: AlertsService, + private activeModal: NgbActiveModal ) {} save(): void { let returnObj: ExplorationObj = { - yamlFile: null + yamlFile: null, }; - let label = - document.getElementById('newFileInput') as HTMLInputElement; + let label = document.getElementById('newFileInput') as HTMLInputElement; if (label === null) { throw new Error('No label found for uploading files.'); } diff --git a/core/templates/pages/delete-account-page/delete-account-page-root.component.spec.ts b/core/templates/pages/delete-account-page/delete-account-page-root.component.spec.ts index ebe28b6b8504..2aabb35265df 100644 --- a/core/templates/pages/delete-account-page/delete-account-page-root.component.spec.ts +++ b/core/templates/pages/delete-account-page/delete-account-page-root.component.spec.ts @@ -16,19 +16,25 @@ * @fileoverview Unit tests for the about page root component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; - -import { AppConstants } from 'app.constants'; -import { AccessValidationBackendApiService } from 'pages/oppia-root/routing/access-validation-backend-api.service'; -import { MetaTagCustomizationService } from 'services/contextual/meta-tag-customization.service'; -import { LoaderService } from 'services/loader.service'; -import { PageHeadService } from 'services/page-head.service'; - -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { DeleteAccountPageRootComponent } from './delete-account-page-root.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; + +import {AppConstants} from 'app.constants'; +import {AccessValidationBackendApiService} from 'pages/oppia-root/routing/access-validation-backend-api.service'; +import {MetaTagCustomizationService} from 'services/contextual/meta-tag-customization.service'; +import {LoaderService} from 'services/loader.service'; +import {PageHeadService} from 'services/page-head.service'; + +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {DeleteAccountPageRootComponent} from './delete-account-page-root.component'; class MockTranslateService { onLangChange: EventEmitter = new EventEmitter(); @@ -47,22 +53,17 @@ describe('Delete Account Page Root', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - DeleteAccountPageRootComponent, - MockTranslatePipe - ], + imports: [HttpClientTestingModule], + declarations: [DeleteAccountPageRootComponent, MockTranslatePipe], providers: [ PageHeadService, MetaTagCustomizationService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -72,18 +73,20 @@ describe('Delete Account Page Root', () => { pageHeadService = TestBed.inject(PageHeadService); loaderService = TestBed.inject(LoaderService); accessValidationBackendApiService = TestBed.inject( - AccessValidationBackendApiService); + AccessValidationBackendApiService + ); translateService = TestBed.inject(TranslateService); }); - it('should successfully instantiate the component', - () => { - expect(component).toBeDefined(); - }); + it('should successfully instantiate the component', () => { + expect(component).toBeDefined(); + }); it('should initialize and show page when access is valid', fakeAsync(() => { - spyOn(accessValidationBackendApiService, 'validateCanManageOwnAccount') - .and.returnValue(Promise.resolve()); + spyOn( + accessValidationBackendApiService, + 'validateCanManageOwnAccount' + ).and.returnValue(Promise.resolve()); spyOn(loaderService, 'showLoadingScreen'); spyOn(loaderService, 'hideLoadingScreen'); @@ -91,34 +94,39 @@ describe('Delete Account Page Root', () => { tick(); expect(loaderService.showLoadingScreen).toHaveBeenCalled(); - expect(accessValidationBackendApiService.validateCanManageOwnAccount) - .toHaveBeenCalled(); + expect( + accessValidationBackendApiService.validateCanManageOwnAccount + ).toHaveBeenCalled(); expect(component.pageIsShown).toBeTrue(); expect(component.errorPageIsShown).toBeFalse(); expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); })); - it('should initialize and show error page when server respond with error', - fakeAsync(() => { - spyOn(accessValidationBackendApiService, 'validateCanManageOwnAccount') - .and.returnValue(Promise.reject()); - spyOn(loaderService, 'showLoadingScreen'); - spyOn(loaderService, 'hideLoadingScreen'); + it('should initialize and show error page when server respond with error', fakeAsync(() => { + spyOn( + accessValidationBackendApiService, + 'validateCanManageOwnAccount' + ).and.returnValue(Promise.reject()); + spyOn(loaderService, 'showLoadingScreen'); + spyOn(loaderService, 'hideLoadingScreen'); - component.ngOnInit(); - tick(); + component.ngOnInit(); + tick(); - expect(loaderService.showLoadingScreen).toHaveBeenCalled(); - expect(accessValidationBackendApiService.validateCanManageOwnAccount) - .toHaveBeenCalled(); - expect(component.pageIsShown).toBeFalse(); - expect(component.errorPageIsShown).toBeTrue(); - expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); - })); + expect(loaderService.showLoadingScreen).toHaveBeenCalled(); + expect( + accessValidationBackendApiService.validateCanManageOwnAccount + ).toHaveBeenCalled(); + expect(component.pageIsShown).toBeFalse(); + expect(component.errorPageIsShown).toBeTrue(); + expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); + })); it('should initialize and subscribe to onLangChange', fakeAsync(() => { - spyOn(accessValidationBackendApiService, 'validateCanManageOwnAccount') - .and.returnValue(Promise.resolve()); + spyOn( + accessValidationBackendApiService, + 'validateCanManageOwnAccount' + ).and.returnValue(Promise.resolve()); spyOn(component.directiveSubscriptions, 'add'); spyOn(translateService.onLangChange, 'subscribe'); @@ -130,8 +138,10 @@ describe('Delete Account Page Root', () => { })); it('should update page title whenever the language changes', () => { - spyOn(accessValidationBackendApiService, 'validateCanManageOwnAccount') - .and.returnValue(Promise.resolve()); + spyOn( + accessValidationBackendApiService, + 'validateCanManageOwnAccount' + ).and.returnValue(Promise.resolve()); component.ngOnInit(); spyOn(component, 'setPageTitleAndMetaTags'); @@ -147,10 +157,12 @@ describe('Delete Account Page Root', () => { component.setPageTitleAndMetaTags(); expect(translateService.instant).toHaveBeenCalledWith( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.DELETE_ACCOUNT.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.DELETE_ACCOUNT.TITLE + ); expect(pageHeadService.updateTitleAndMetaTags).toHaveBeenCalledWith( AppConstants.PAGES_REGISTERED_WITH_FRONTEND.DELETE_ACCOUNT.TITLE, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.DELETE_ACCOUNT.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.DELETE_ACCOUNT.META + ); }); it('should unsubscribe on component destruction', () => { diff --git a/core/templates/pages/delete-account-page/delete-account-page-root.component.ts b/core/templates/pages/delete-account-page/delete-account-page-root.component.ts index 045dac5a0d8b..277685fb4fd3 100644 --- a/core/templates/pages/delete-account-page/delete-account-page-root.component.ts +++ b/core/templates/pages/delete-account-page/delete-account-page-root.component.ts @@ -16,18 +16,18 @@ * @fileoverview Root component for delete account page. */ -import { Component, OnDestroy } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; +import {Component, OnDestroy} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; -import { AppConstants } from 'app.constants'; -import { AccessValidationBackendApiService } from 'pages/oppia-root/routing/access-validation-backend-api.service'; -import { LoaderService } from 'services/loader.service'; -import { PageHeadService } from 'services/page-head.service'; +import {AppConstants} from 'app.constants'; +import {AccessValidationBackendApiService} from 'pages/oppia-root/routing/access-validation-backend-api.service'; +import {LoaderService} from 'services/loader.service'; +import {PageHeadService} from 'services/page-head.service'; @Component({ selector: 'oppia-delete-account-page-root', - templateUrl: './delete-account-page-root.component.html' + templateUrl: './delete-account-page-root.component.html', }) export class DeleteAccountPageRootComponent implements OnDestroy { directiveSubscriptions = new Subscription(); @@ -35,8 +35,7 @@ export class DeleteAccountPageRootComponent implements OnDestroy { errorPageIsShown: boolean = false; constructor( - private accessValidationBackendApiService: - AccessValidationBackendApiService, + private accessValidationBackendApiService: AccessValidationBackendApiService, private loaderService: LoaderService, private pageHeadService: PageHeadService, private translateService: TranslateService @@ -44,10 +43,12 @@ export class DeleteAccountPageRootComponent implements OnDestroy { setPageTitleAndMetaTags(): void { let translatedTitle = this.translateService.instant( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.DELETE_ACCOUNT.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.DELETE_ACCOUNT.TITLE + ); this.pageHeadService.updateTitleAndMetaTags( translatedTitle, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.DELETE_ACCOUNT.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.DELETE_ACCOUNT.META + ); } ngOnInit(): void { @@ -58,12 +59,17 @@ export class DeleteAccountPageRootComponent implements OnDestroy { ); this.loaderService.showLoadingScreen('Loading'); - this.accessValidationBackendApiService.validateCanManageOwnAccount() + this.accessValidationBackendApiService + .validateCanManageOwnAccount() + .then( + () => { + this.pageIsShown = true; + }, + err => { + this.errorPageIsShown = true; + } + ) .then(() => { - this.pageIsShown = true; - }, (err) => { - this.errorPageIsShown = true; - }).then(() => { this.loaderService.hideLoadingScreen(); }); } diff --git a/core/templates/pages/delete-account-page/delete-account-page-routing.module.ts b/core/templates/pages/delete-account-page/delete-account-page-routing.module.ts index f96757a665fe..440e13811614 100644 --- a/core/templates/pages/delete-account-page/delete-account-page-routing.module.ts +++ b/core/templates/pages/delete-account-page/delete-account-page-routing.module.ts @@ -16,24 +16,19 @@ * @fileoverview Routing module for delete page. */ -import { NgModule } from '@angular/core'; -import { Route, RouterModule } from '@angular/router'; -import { DeleteAccountPageRootComponent } from './delete-account-page-root.component'; +import {NgModule} from '@angular/core'; +import {Route, RouterModule} from '@angular/router'; +import {DeleteAccountPageRootComponent} from './delete-account-page-root.component'; const routes: Route[] = [ { path: '', - component: DeleteAccountPageRootComponent - } + component: DeleteAccountPageRootComponent, + }, ]; @NgModule({ - imports: [ - RouterModule.forChild(routes) - ], - exports: [ - RouterModule - ] + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], }) - export class DeleteAccountPageRoutingModule {} diff --git a/core/templates/pages/delete-account-page/delete-account-page.component.spec.ts b/core/templates/pages/delete-account-page/delete-account-page.component.spec.ts index 88bc4e5712a9..397b9c15762a 100644 --- a/core/templates/pages/delete-account-page/delete-account-page.component.spec.ts +++ b/core/templates/pages/delete-account-page/delete-account-page.component.spec.ts @@ -16,13 +16,13 @@ * @fileoverview Unit tests for delete account page. */ -import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { DeleteAccountPageComponent } from './delete-account-page.component'; -import { DeleteAccountBackendApiService } from './services/delete-account-backend-api.service'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; +import {ComponentFixture, fakeAsync, TestBed} from '@angular/core/testing'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {DeleteAccountPageComponent} from './delete-account-page.component'; +import {DeleteAccountBackendApiService} from './services/delete-account-backend-api.service'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; describe('Delete account page', () => { let component: DeleteAccountPageComponent; @@ -34,10 +34,8 @@ describe('Delete account page', () => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], declarations: [DeleteAccountPageComponent, MockTranslatePipe], - providers: [ - DeleteAccountBackendApiService, - ], - schemas: [NO_ERRORS_SCHEMA] + providers: [DeleteAccountBackendApiService], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); @@ -49,22 +47,21 @@ describe('Delete account page', () => { spyOn(deleteAccountService, 'deleteAccount').and.callThrough(); }); - it('should open a delete account modal', - fakeAsync(() => { - const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ - result: Promise.resolve('success') - }) as NgbModalRef; - }); - component.deleteAccount(); - expect(modalSpy).toHaveBeenCalled(); - })); + it('should open a delete account modal', fakeAsync(() => { + const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + result: Promise.resolve('success'), + } as NgbModalRef; + }); + component.deleteAccount(); + expect(modalSpy).toHaveBeenCalled(); + })); it('should do nothing when cancel button is clicked', () => { const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ - result: Promise.reject('cancel') - }) as NgbModalRef; + return { + result: Promise.reject('cancel'), + } as NgbModalRef; }); component.deleteAccount(); diff --git a/core/templates/pages/delete-account-page/delete-account-page.component.ts b/core/templates/pages/delete-account-page/delete-account-page.component.ts index 0ce2ab489188..c3fb3f24113d 100644 --- a/core/templates/pages/delete-account-page/delete-account-page.component.ts +++ b/core/templates/pages/delete-account-page/delete-account-page.component.ts @@ -16,15 +16,15 @@ * @fileoverview Component for the Oppia 'Delete Account' page. */ -import { Component } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { DeleteAccountBackendApiService } from './services/delete-account-backend-api.service'; -import { DeleteAccountModalComponent } from './templates/delete-account-modal.component'; +import {Component} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {DeleteAccountBackendApiService} from './services/delete-account-backend-api.service'; +import {DeleteAccountModalComponent} from './templates/delete-account-modal.component'; @Component({ selector: 'oppia-delete-account-page', - templateUrl: './delete-account-page.component.html' + templateUrl: './delete-account-page.component.html', }) export class DeleteAccountPageComponent { constructor( @@ -33,18 +33,25 @@ export class DeleteAccountPageComponent { ) {} deleteAccount(): void { - const modelRef = this.ngbModal.open( - DeleteAccountModalComponent, { backdrop: true }); - modelRef.result.then(() => { - this.deleteAccountBackendApiService.deleteAccount(); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. + const modelRef = this.ngbModal.open(DeleteAccountModalComponent, { + backdrop: true, }); + modelRef.result.then( + () => { + this.deleteAccountBackendApiService.deleteAccount(); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } } -angular.module('oppia').directive( - 'oppiaDeleteAccountPage', downgradeComponent( - {component: DeleteAccountPageComponent})); +angular + .module('oppia') + .directive( + 'oppiaDeleteAccountPage', + downgradeComponent({component: DeleteAccountPageComponent}) + ); diff --git a/core/templates/pages/delete-account-page/delete-account-page.module.ts b/core/templates/pages/delete-account-page/delete-account-page.module.ts index 7ca1a0ec1aab..d534b88db455 100644 --- a/core/templates/pages/delete-account-page/delete-account-page.module.ts +++ b/core/templates/pages/delete-account-page/delete-account-page.module.ts @@ -16,21 +16,21 @@ * @fileoverview Module for the delete account page. */ -import { NgModule } from '@angular/core'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { DeleteAccountPageComponent } from './delete-account-page.component'; -import { DeleteAccountModalComponent } from './templates/delete-account-modal.component'; -import { DeleteAccountPageRootComponent } from './delete-account-page-root.component'; -import { CommonModule } from '@angular/common'; -import { DeleteAccountPageRoutingModule } from './delete-account-page-routing.module'; -import { Error404PageModule } from 'pages/error-pages/error-404/error-404-page.module'; +import {NgModule} from '@angular/core'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {DeleteAccountPageComponent} from './delete-account-page.component'; +import {DeleteAccountModalComponent} from './templates/delete-account-modal.component'; +import {DeleteAccountPageRootComponent} from './delete-account-page-root.component'; +import {CommonModule} from '@angular/common'; +import {DeleteAccountPageRoutingModule} from './delete-account-page-routing.module'; +import {Error404PageModule} from 'pages/error-pages/error-404/error-404-page.module'; @NgModule({ imports: [ CommonModule, SharedComponentsModule, DeleteAccountPageRoutingModule, - Error404PageModule + Error404PageModule, ], declarations: [ DeleteAccountModalComponent, @@ -41,6 +41,6 @@ import { Error404PageModule } from 'pages/error-pages/error-404/error-404-page.m DeleteAccountModalComponent, DeleteAccountPageComponent, DeleteAccountPageRootComponent, - ] + ], }) export class DeleteAccountPageModule {} diff --git a/core/templates/pages/delete-account-page/services/delete-account-backend-api.service.spec.ts b/core/templates/pages/delete-account-page/services/delete-account-backend-api.service.spec.ts index b5ad15bbc849..f99637732d6d 100644 --- a/core/templates/pages/delete-account-page/services/delete-account-backend-api.service.spec.ts +++ b/core/templates/pages/delete-account-page/services/delete-account-backend-api.service.spec.ts @@ -16,17 +16,20 @@ * @fileoverview Unit Tests for delete account backend api service. */ -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { fakeAsync, flushMicrotasks, TestBed, tick } from '@angular/core/testing'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { DeleteAccountBackendApiService } from './delete-account-backend-api.service'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {fakeAsync, flushMicrotasks, TestBed, tick} from '@angular/core/testing'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {DeleteAccountBackendApiService} from './delete-account-backend-api.service'; class MockWindowRef { nativeWindow = { location: { - href: 'pending-account-deletion' + href: 'pending-account-deletion', }, - gtag: () => {} + gtag: () => {}, }; } @@ -42,15 +45,16 @@ describe('Delete Account Service', () => { providers: [ { provide: WindowRef, - useClass: MockWindowRef + useClass: MockWindowRef, }, - ] + ], }); }); beforeEach(() => { - deleteAccountBackendApiService = - TestBed.inject(DeleteAccountBackendApiService); + deleteAccountBackendApiService = TestBed.inject( + DeleteAccountBackendApiService + ); windowRef = TestBed.inject(WindowRef); http = TestBed.inject(HttpTestingController); httpTestingController = TestBed.inject(HttpTestingController); @@ -71,6 +75,7 @@ describe('Delete Account Service', () => { tick(150); expect(windowRef.nativeWindow.location.href).toBe( - '/logout?redirect_url=/pending-account-deletion'); + '/logout?redirect_url=/pending-account-deletion' + ); })); }); diff --git a/core/templates/pages/delete-account-page/services/delete-account-backend-api.service.ts b/core/templates/pages/delete-account-page/services/delete-account-backend-api.service.ts index 9b0ece6f9454..e274bfca4be0 100644 --- a/core/templates/pages/delete-account-page/services/delete-account-backend-api.service.ts +++ b/core/templates/pages/delete-account-page/services/delete-account-backend-api.service.ts @@ -16,34 +16,40 @@ * @fileoverview Backend Api Service for Delete Account Page. */ -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; import analyticsConstants from 'analytics-constants'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class DeleteAccountBackendApiService { constructor( private siteAnalyticsService: SiteAnalyticsService, private windowRef: WindowRef, - private http: HttpClient, + private http: HttpClient ) {} deleteAccount(): void { this.http.delete('/delete-account-handler').subscribe(() => { this.siteAnalyticsService.registerAccountDeletion(); - setTimeout(() => { - this.windowRef.nativeWindow.location.href = ( - '/logout?redirect_url=/pending-account-deletion'); - }, analyticsConstants.CAN_SEND_ANALYTICS_EVENTS ? 150 : 0); + setTimeout( + () => { + this.windowRef.nativeWindow.location.href = + '/logout?redirect_url=/pending-account-deletion'; + }, + analyticsConstants.CAN_SEND_ANALYTICS_EVENTS ? 150 : 0 + ); }); } } -angular.module('oppia').factory( - 'DeleteAccountBackendApiService', - downgradeInjectable(DeleteAccountBackendApiService)); +angular + .module('oppia') + .factory( + 'DeleteAccountBackendApiService', + downgradeInjectable(DeleteAccountBackendApiService) + ); diff --git a/core/templates/pages/delete-account-page/templates/delete-account-modal.component.spec.ts b/core/templates/pages/delete-account-page/templates/delete-account-modal.component.spec.ts index ccd13bd0d313..6b3fbc7a86ed 100644 --- a/core/templates/pages/delete-account-page/templates/delete-account-modal.component.spec.ts +++ b/core/templates/pages/delete-account-page/templates/delete-account-modal.component.spec.ts @@ -16,15 +16,20 @@ * @fileoverview Unit tests for the delete account modal. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { UserService } from 'services/user.service'; -import { DeleteAccountModalComponent } from './delete-account-modal.component'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { UserInfo } from 'domain/user/user-info.model'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {UserService} from 'services/user.service'; +import {DeleteAccountModalComponent} from './delete-account-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {FormsModule} from '@angular/forms'; +import {UserInfo} from 'domain/user/user-info.model'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; class MockActiveModal { dismiss(): void { @@ -50,10 +55,10 @@ describe('Delete account modal', () => { UserService, { provide: NgbActiveModal, - useClass: MockActiveModal - } + useClass: MockActiveModal, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); @@ -75,11 +80,12 @@ describe('Delete account modal', () => { preferred_site_language_code: null, username: 'username', email: 'test@test.com', - user_is_logged_in: true + user_is_logged_in: true, }; - spyOn(userService, 'getUserInfoAsync').and.returnValue(Promise.resolve( - UserInfo.createFromBackendDict(UserInfoObject))); + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve(UserInfo.createFromBackendDict(UserInfoObject)) + ); component.ngOnInit(); component.expectedUsername = 'username'; @@ -96,10 +102,9 @@ describe('Delete account modal', () => { let userInfo = { isModerator: () => true, getUsername: () => null, - isSuperAdmin: () => true + isSuperAdmin: () => true, }; - spyOn(userService, 'getUserInfoAsync') - .and.resolveTo(userInfo as UserInfo); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo as UserInfo); expect(() => { component.ngOnInit(); @@ -118,11 +123,12 @@ describe('Delete account modal', () => { preferred_site_language_code: null, username: 'username', email: 'test@test.com', - user_is_logged_in: true + user_is_logged_in: true, }; - spyOn(userService, 'getUserInfoAsync').and.returnValue(Promise.resolve( - UserInfo.createFromBackendDict(UserInfoObject))); + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve(UserInfo.createFromBackendDict(UserInfoObject)) + ); component.ngOnInit(); const closeSpy = spyOn(ngbActiveModal, 'close').and.callThrough(); @@ -141,11 +147,12 @@ describe('Delete account modal', () => { preferred_site_language_code: null, username: 'username', email: 'test@test.com', - user_is_logged_in: true + user_is_logged_in: true, }; - spyOn(userService, 'getUserInfoAsync').and.returnValue(Promise.resolve( - UserInfo.createFromBackendDict(UserInfoObject))); + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve(UserInfo.createFromBackendDict(UserInfoObject)) + ); component.ngOnInit(); const dismissSpy = spyOn(ngbActiveModal, 'dismiss').and.callThrough(); diff --git a/core/templates/pages/delete-account-page/templates/delete-account-modal.component.ts b/core/templates/pages/delete-account-page/templates/delete-account-modal.component.ts index c7e2e8e4d23e..1eb816c3f5e8 100644 --- a/core/templates/pages/delete-account-page/templates/delete-account-modal.component.ts +++ b/core/templates/pages/delete-account-page/templates/delete-account-modal.component.ts @@ -16,14 +16,14 @@ * @fileoverview Component for delete account modal. */ -import { Component } from '@angular/core'; -import { OnInit } from '@angular/core'; -import { UserService } from 'services/user.service'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import {Component} from '@angular/core'; +import {OnInit} from '@angular/core'; +import {UserService} from 'services/user.service'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; @Component({ selector: 'oppia-delete-account-modal', - templateUrl: './delete-account-modal.component.html' + templateUrl: './delete-account-modal.component.html', }) export class DeleteAccountModalComponent implements OnInit { // These properties are initialized using Angular lifecycle hooks @@ -33,11 +33,11 @@ export class DeleteAccountModalComponent implements OnInit { username!: string; constructor( private userService: UserService, - private ngbActiveModal: NgbActiveModal, + private ngbActiveModal: NgbActiveModal ) {} ngOnInit(): void { - this.userService.getUserInfoAsync().then((userInfo) => { + this.userService.getUserInfoAsync().then(userInfo => { const expectedUsername = userInfo.getUsername(); if (expectedUsername === null) { diff --git a/core/templates/pages/diagnostic-test-player-page/diagnostic-test-current-topic-status.model.spec.ts b/core/templates/pages/diagnostic-test-player-page/diagnostic-test-current-topic-status.model.spec.ts index bf3b42200e4c..0b9004b3bf69 100644 --- a/core/templates/pages/diagnostic-test-player-page/diagnostic-test-current-topic-status.model.spec.ts +++ b/core/templates/pages/diagnostic-test-player-page/diagnostic-test-current-topic-status.model.spec.ts @@ -16,13 +16,14 @@ * @fileoverview Test for the diagnostic test current topic status model. */ - -import { TestBed } from '@angular/core/testing'; -import { DiagnosticTestQuestionsModel } from 'domain/question/diagnostic-test-questions.model'; -import { Question } from 'domain/question/QuestionObjectFactory'; -import { StateObjectFactory } from 'domain/state/StateObjectFactory'; -import { DiagnosticTestCurrentTopicStatusModel, SkillIdToQuestionsDict } from './diagnostic-test-current-topic-status.model'; - +import {TestBed} from '@angular/core/testing'; +import {DiagnosticTestQuestionsModel} from 'domain/question/diagnostic-test-questions.model'; +import {Question} from 'domain/question/QuestionObjectFactory'; +import {StateObjectFactory} from 'domain/state/StateObjectFactory'; +import { + DiagnosticTestCurrentTopicStatusModel, + SkillIdToQuestionsDict, +} from './diagnostic-test-current-topic-status.model'; describe('Diagnostic test current topic status model', () => { let question1: Question, question2: Question, question3: Question; @@ -32,39 +33,63 @@ describe('Diagnostic test current topic status model', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [], - providers: [] + providers: [], }); stateObject = TestBed.inject(StateObjectFactory); question1 = new Question( 'question1', stateObject.createDefaultState('state', 'content_0', 'default_outcome_1'), - '', 1, ['skillID1'], [], 2 + '', + 1, + ['skillID1'], + [], + 2 ); question2 = new Question( 'question2', stateObject.createDefaultState('state', 'content_0', 'default_outcome_1'), - '', 1, ['skillID2'], [], 2 + '', + 1, + ['skillID2'], + [], + 2 ); question3 = new Question( 'question3', stateObject.createDefaultState('state', 'content_0', 'default_outcome_1'), - '', 1, ['skillID3'], [], 2 + '', + 1, + ['skillID3'], + [], + 2 ); question4 = new Question( 'question4', stateObject.createDefaultState('state', 'content_0', 'default_outcome_1'), - '', 1, ['skillID4'], [], 2 + '', + 1, + ['skillID4'], + [], + 2 ); question5 = new Question( 'question5', stateObject.createDefaultState('state', 'content_0', 'default_outcome_1'), - '', 1, ['skillID5'], [], 2 + '', + 1, + ['skillID5'], + [], + 2 ); question6 = new Question( 'question6', stateObject.createDefaultState('state', 'content_0', 'default_outcome_1'), - '', 1, ['skillID6'], [], 2 + '', + 1, + ['skillID6'], + [], + 2 ); }); @@ -72,21 +97,24 @@ describe('Diagnostic test current topic status model', () => { let skillIdToQuestionsDict: SkillIdToQuestionsDict = { skillID1: new DiagnosticTestQuestionsModel(question1, question2), skillID2: new DiagnosticTestQuestionsModel(question3, question4), - skillID3: new DiagnosticTestQuestionsModel(question5, question6) + skillID3: new DiagnosticTestQuestionsModel(question5, question6), }; - let diagnosticTestCurrentTopicStatusModel = ( - new DiagnosticTestCurrentTopicStatusModel(skillIdToQuestionsDict)); + let diagnosticTestCurrentTopicStatusModel = + new DiagnosticTestCurrentTopicStatusModel(skillIdToQuestionsDict); - expect(diagnosticTestCurrentTopicStatusModel.getPendingSkillIds()) - .toEqual(['skillID1', 'skillID2', 'skillID3']); + expect(diagnosticTestCurrentTopicStatusModel.getPendingSkillIds()).toEqual([ + 'skillID1', + 'skillID2', + 'skillID3', + ]); let currentSkillId = 'skillID1'; // Currently, none of the questions are answered incorrectly, so the // main question from current skill should be presented. - let question = diagnosticTestCurrentTopicStatusModel.getNextQuestion( - currentSkillId); + let question = + diagnosticTestCurrentTopicStatusModel.getNextQuestion(currentSkillId); expect(question).toEqual(question1); @@ -94,178 +122,207 @@ describe('Diagnostic test current topic status model', () => { diagnosticTestCurrentTopicStatusModel.recordCorrectAttempt(currentSkillId); // The current skill ID should be removed from the eligible skill IDs. - expect(diagnosticTestCurrentTopicStatusModel.getPendingSkillIds()) - .toEqual(['skillID2', 'skillID3']); + expect(diagnosticTestCurrentTopicStatusModel.getPendingSkillIds()).toEqual([ + 'skillID2', + 'skillID3', + ]); }); it( 'should be able to get the status of the current skill as true after ' + - 'passing', () => { + 'passing', + () => { let skillIdToQuestionsDict: SkillIdToQuestionsDict = { skillID1: new DiagnosticTestQuestionsModel(question1, question2), skillID2: new DiagnosticTestQuestionsModel(question3, question4), - skillID3: new DiagnosticTestQuestionsModel(question5, question6) + skillID3: new DiagnosticTestQuestionsModel(question5, question6), }; - let diagnosticTestCurrentTopicStatusModel = ( - new DiagnosticTestCurrentTopicStatusModel(skillIdToQuestionsDict)); + let diagnosticTestCurrentTopicStatusModel = + new DiagnosticTestCurrentTopicStatusModel(skillIdToQuestionsDict); - expect(diagnosticTestCurrentTopicStatusModel.getPendingSkillIds()) - .toEqual(['skillID1', 'skillID2', 'skillID3']); + expect( + diagnosticTestCurrentTopicStatusModel.getPendingSkillIds() + ).toEqual(['skillID1', 'skillID2', 'skillID3']); let currentSkillId = 'skillID1'; // Currently, none of the questions are answered incorrectly, so the // main question from current skill should be presented. - let question = diagnosticTestCurrentTopicStatusModel.getNextQuestion( - currentSkillId); + let question = + diagnosticTestCurrentTopicStatusModel.getNextQuestion(currentSkillId); expect(question).toEqual(question1); // Status of current skill should be false initially. - expect(diagnosticTestCurrentTopicStatusModel.getSkillIdToTestStatus()[ - currentSkillId]).toBeFalse(); + expect( + diagnosticTestCurrentTopicStatusModel.getSkillIdToTestStatus()[ + currentSkillId + ] + ).toBeFalse(); diagnosticTestCurrentTopicStatusModel.recordCorrectAttempt( - currentSkillId); + currentSkillId + ); - expect(diagnosticTestCurrentTopicStatusModel.getPendingSkillIds()) - .toEqual(['skillID2', 'skillID3']); + expect( + diagnosticTestCurrentTopicStatusModel.getPendingSkillIds() + ).toEqual(['skillID2', 'skillID3']); // Status of current skill should be true, since the answer is correct. - expect(diagnosticTestCurrentTopicStatusModel.getSkillIdToTestStatus()[ - currentSkillId]).toBeTrue(); + expect( + diagnosticTestCurrentTopicStatusModel.getSkillIdToTestStatus()[ + currentSkillId + ] + ).toBeTrue(); // Since the current question is answered correctly, the next skill // (skill 2) should be tested. currentSkillId = 'skillID2'; // Getting the main question from skill 2 i.e., question 3. - question = diagnosticTestCurrentTopicStatusModel.getNextQuestion( - currentSkillId); + question = + diagnosticTestCurrentTopicStatusModel.getNextQuestion(currentSkillId); expect(question).toEqual(question3); - }); + } + ); it( 'should be able to get the backup question of skill if the main question ' + - 'is marked as incorrect', () => { + 'is marked as incorrect', + () => { // The first wrong answer does not mark the topic as fail. The first // incorrect attempt for a topic is given another chance to try, so // the backup question from the same skill should be tested. let skillIdToQuestionsDict: SkillIdToQuestionsDict = { skillID1: new DiagnosticTestQuestionsModel(question1, question2), skillID2: new DiagnosticTestQuestionsModel(question3, question4), - skillID3: new DiagnosticTestQuestionsModel(question5, question6) + skillID3: new DiagnosticTestQuestionsModel(question5, question6), }; - let diagnosticTestCurrentTopicStatusModel = ( - new DiagnosticTestCurrentTopicStatusModel(skillIdToQuestionsDict)); + let diagnosticTestCurrentTopicStatusModel = + new DiagnosticTestCurrentTopicStatusModel(skillIdToQuestionsDict); - expect(diagnosticTestCurrentTopicStatusModel.getPendingSkillIds()) - .toEqual(['skillID1', 'skillID2', 'skillID3']); + expect( + diagnosticTestCurrentTopicStatusModel.getPendingSkillIds() + ).toEqual(['skillID1', 'skillID2', 'skillID3']); let currentSkillId = 'skillID1'; // Currently, none of the questions are answered incorrectly, so the // main question from skill 1 should be presented. - let question = diagnosticTestCurrentTopicStatusModel.getNextQuestion( - currentSkillId); + let question = + diagnosticTestCurrentTopicStatusModel.getNextQuestion(currentSkillId); expect(question).toEqual(question1); diagnosticTestCurrentTopicStatusModel.recordIncorrectAttempt( - currentSkillId); + currentSkillId + ); - expect(diagnosticTestCurrentTopicStatusModel.getPendingSkillIds()) - .toEqual(['skillID1', 'skillID2', 'skillID3']); + expect( + diagnosticTestCurrentTopicStatusModel.getPendingSkillIds() + ).toEqual(['skillID1', 'skillID2', 'skillID3']); - question = diagnosticTestCurrentTopicStatusModel.getNextQuestion( - currentSkillId); + question = + diagnosticTestCurrentTopicStatusModel.getNextQuestion(currentSkillId); // Currently, the question from skill 1 is answered incorrectly, so the // backup question from skill 1 should be presented. expect(question).toEqual(question2); - }); + } + ); it( 'should be able to mark the topic as failed if two incorrect attempts ' + - 'were made in the same or different skill', () => { + 'were made in the same or different skill', + () => { let skillIdToQuestionsDict: SkillIdToQuestionsDict = { skillID1: new DiagnosticTestQuestionsModel(question1, question2), skillID2: new DiagnosticTestQuestionsModel(question3, question4), - skillID3: new DiagnosticTestQuestionsModel(question5, question6) + skillID3: new DiagnosticTestQuestionsModel(question5, question6), }; - let diagnosticTestCurrentTopicStatusModel = ( - new DiagnosticTestCurrentTopicStatusModel(skillIdToQuestionsDict)); + let diagnosticTestCurrentTopicStatusModel = + new DiagnosticTestCurrentTopicStatusModel(skillIdToQuestionsDict); - expect(diagnosticTestCurrentTopicStatusModel.getPendingSkillIds()) - .toEqual(['skillID1', 'skillID2', 'skillID3']); + expect( + diagnosticTestCurrentTopicStatusModel.getPendingSkillIds() + ).toEqual(['skillID1', 'skillID2', 'skillID3']); let currentSkillId = 'skillID1'; // Currently, none of the questions are answered incorrectly, so the // main question from skill 1 should be presented. - let question = diagnosticTestCurrentTopicStatusModel.getNextQuestion( - currentSkillId); + let question = + diagnosticTestCurrentTopicStatusModel.getNextQuestion(currentSkillId); expect(question).toEqual(question1); diagnosticTestCurrentTopicStatusModel.recordIncorrectAttempt( - currentSkillId); + currentSkillId + ); - expect(diagnosticTestCurrentTopicStatusModel.getPendingSkillIds()) - .toEqual(['skillID1', 'skillID2', 'skillID3']); + expect( + diagnosticTestCurrentTopicStatusModel.getPendingSkillIds() + ).toEqual(['skillID1', 'skillID2', 'skillID3']); - expect(diagnosticTestCurrentTopicStatusModel.isTopicCompletelyTested()) - .toBeFalse(); + expect( + diagnosticTestCurrentTopicStatusModel.isTopicCompletelyTested() + ).toBeFalse(); // The earlier attempt was incorrect, so getting the backup question of // the current skill. - question = diagnosticTestCurrentTopicStatusModel.getNextQuestion( - currentSkillId); + question = + diagnosticTestCurrentTopicStatusModel.getNextQuestion(currentSkillId); expect(question).toEqual(question2); // Answering incorrectly twice in a topic marks the topic as fail. diagnosticTestCurrentTopicStatusModel.recordIncorrectAttempt( - currentSkillId); + currentSkillId + ); // Since two questions are attempted incorrectly, so the next skill from // the topic (if any) should not be tested and the topic should be marked // as failed. expect(diagnosticTestCurrentTopicStatusModel.isTopicPassed()).toBeFalse(); - expect(diagnosticTestCurrentTopicStatusModel.isTopicCompletelyTested()) - .toBeTrue(); - }); + expect( + diagnosticTestCurrentTopicStatusModel.isTopicCompletelyTested() + ).toBeTrue(); + } + ); it( 'should be able to mark the topic as passed if questions from all the ' + - 'skills were attempted correctly', () => { + 'skills were attempted correctly', + () => { // Attempting questions from all the skills mark the topic as passed. let skillIdToQuestionsDict: SkillIdToQuestionsDict = { skillID1: new DiagnosticTestQuestionsModel(question1, question2), - skillID2: new DiagnosticTestQuestionsModel(question3, question4) + skillID2: new DiagnosticTestQuestionsModel(question3, question4), }; - let diagnosticTestCurrentTopicStatusModel = ( - new DiagnosticTestCurrentTopicStatusModel(skillIdToQuestionsDict)); + let diagnosticTestCurrentTopicStatusModel = + new DiagnosticTestCurrentTopicStatusModel(skillIdToQuestionsDict); let currentSkillId = 'skillID1'; // Currently, none of the questions are answered incorrectly, so the // main question from skill 1 should be presented. - let question = diagnosticTestCurrentTopicStatusModel.getNextQuestion( - currentSkillId); + let question = + diagnosticTestCurrentTopicStatusModel.getNextQuestion(currentSkillId); expect(question).toEqual(question1); diagnosticTestCurrentTopicStatusModel.recordCorrectAttempt( - currentSkillId); + currentSkillId + ); - expect(diagnosticTestCurrentTopicStatusModel.isTopicCompletelyTested()) - .toBeFalse(); + expect( + diagnosticTestCurrentTopicStatusModel.isTopicCompletelyTested() + ).toBeFalse(); // Since the current question is answered correctly, the next skill // (skill 2) should be tested. @@ -273,16 +330,19 @@ describe('Diagnostic test current topic status model', () => { // Currently, none of the questions are answered incorrectly, so the // main question from skill 2 should be presented. - question = diagnosticTestCurrentTopicStatusModel.getNextQuestion( - currentSkillId); + question = + diagnosticTestCurrentTopicStatusModel.getNextQuestion(currentSkillId); expect(question).toEqual(question3); diagnosticTestCurrentTopicStatusModel.recordCorrectAttempt( - currentSkillId); + currentSkillId + ); expect(diagnosticTestCurrentTopicStatusModel.isTopicPassed()).toBeTrue(); - expect(diagnosticTestCurrentTopicStatusModel.isTopicCompletelyTested()) - .toBeTrue(); - }); + expect( + diagnosticTestCurrentTopicStatusModel.isTopicCompletelyTested() + ).toBeTrue(); + } + ); }); diff --git a/core/templates/pages/diagnostic-test-player-page/diagnostic-test-current-topic-status.model.ts b/core/templates/pages/diagnostic-test-player-page/diagnostic-test-current-topic-status.model.ts index dfae581c9a05..4ad6e616586c 100644 --- a/core/templates/pages/diagnostic-test-player-page/diagnostic-test-current-topic-status.model.ts +++ b/core/templates/pages/diagnostic-test-player-page/diagnostic-test-current-topic-status.model.ts @@ -19,8 +19,8 @@ * in the diagnostic test. */ -import { DiagnosticTestQuestionsModel } from 'domain/question/diagnostic-test-questions.model'; -import { Question } from 'domain/question/QuestionObjectFactory'; +import {DiagnosticTestQuestionsModel} from 'domain/question/diagnostic-test-questions.model'; +import {Question} from 'domain/question/QuestionObjectFactory'; export interface SkillIdToQuestionsDict { [skillId: string]: DiagnosticTestQuestionsModel; diff --git a/core/templates/pages/diagnostic-test-player-page/diagnostic-test-player-page.import.ts b/core/templates/pages/diagnostic-test-player-page/diagnostic-test-player-page.import.ts index 3c81f8da273c..512bfc98aa0c 100644 --- a/core/templates/pages/diagnostic-test-player-page/diagnostic-test-player-page.import.ts +++ b/core/templates/pages/diagnostic-test-player-page/diagnostic-test-player-page.import.ts @@ -22,16 +22,20 @@ import 'zone.js'; import 'angular-ui-validate'; angular.module('oppia', [ - require('angular-cookies'), 'ngAnimate', - 'ngMaterial', 'ngSanitize', 'ngTouch', 'pascalprecht.translate', - 'ui.bootstrap', 'ui.validate' + require('angular-cookies'), + 'ngAnimate', + 'ngMaterial', + 'ngSanitize', + 'ngTouch', + 'pascalprecht.translate', + 'ui.bootstrap', + 'ui.validate', ]); require('Polyfills.ts'); // The module needs to be loaded directly after jquery since it defines the // main module the elements are attached to. -require( - 'pages/diagnostic-test-player-page/diagnostic-test-player-page.module.ts'); +require('pages/diagnostic-test-player-page/diagnostic-test-player-page.module.ts'); require('App.ts'); require('base-components/oppia-root.directive.ts'); diff --git a/core/templates/pages/diagnostic-test-player-page/diagnostic-test-player-page.module.ts b/core/templates/pages/diagnostic-test-player-page/diagnostic-test-player-page.module.ts index d91697ee8554..a11bf1b08865 100644 --- a/core/templates/pages/diagnostic-test-player-page/diagnostic-test-player-page.module.ts +++ b/core/templates/pages/diagnostic-test-player-page/diagnostic-test-player-page.module.ts @@ -16,31 +16,32 @@ * @fileoverview Module for the diagnostic test player page. */ +import {HttpClientModule, HTTP_INTERCEPTORS} from '@angular/common/http'; +import {APP_INITIALIZER, DoBootstrap, NgModule} from '@angular/core'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {MatCardModule} from '@angular/material/card'; +import {MatTooltipModule} from '@angular/material/tooltip'; +import {BrowserModule, HAMMER_GESTURE_CONFIG} from '@angular/platform-browser'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {downgradeComponent, downgradeModule} from '@angular/upgrade/static'; +import {RouterModule} from '@angular/router'; +import {APP_BASE_HREF} from '@angular/common'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; -import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; -import { APP_INITIALIZER, DoBootstrap, NgModule } from '@angular/core'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { MatCardModule } from '@angular/material/card'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { BrowserModule, HAMMER_GESTURE_CONFIG } from '@angular/platform-browser'; -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { downgradeComponent, downgradeModule } from '@angular/upgrade/static'; -import { RouterModule } from '@angular/router'; -import { APP_BASE_HREF } from '@angular/common'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; - -import { OppiaAngularRootComponent } from 'components/oppia-angular-root.component'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { platformFeatureInitFactory, PlatformFeatureService } from 'services/platform-feature.service'; -import { RequestInterceptor } from 'services/request-interceptor.service'; -import { ToastrModule } from 'ngx-toastr'; -import { MyHammerConfig, toastrConfig } from 'pages/oppia-root/app.module'; -import { SmartRouterModule } from 'hybrid-router-module-provider'; -import { AppErrorHandlerProvider } from 'pages/oppia-root/app-error-handler'; -import { DiagnosticTestPlayerComponent } from './diagnostic-test-player.component'; -import { InteractionExtensionsModule } from 'interactions/interactions.module'; -import { SummaryTilesModule } from 'components/summary-tile/summary-tile.module'; - +import {OppiaAngularRootComponent} from 'components/oppia-angular-root.component'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import { + platformFeatureInitFactory, + PlatformFeatureService, +} from 'services/platform-feature.service'; +import {RequestInterceptor} from 'services/request-interceptor.service'; +import {ToastrModule} from 'ngx-toastr'; +import {MyHammerConfig, toastrConfig} from 'pages/oppia-root/app.module'; +import {SmartRouterModule} from 'hybrid-router-module-provider'; +import {AppErrorHandlerProvider} from 'pages/oppia-root/app-error-handler'; +import {DiagnosticTestPlayerComponent} from './diagnostic-test-player.component'; +import {InteractionExtensionsModule} from 'interactions/interactions.module'; +import {SummaryTilesModule} from 'components/summary-tile/summary-tile.module'; declare var angular: ng.IAngularStatic; @@ -60,14 +61,10 @@ declare var angular: ng.IAngularStatic; InteractionExtensionsModule, SharedComponentsModule, SummaryTilesModule, - ToastrModule.forRoot(toastrConfig) - ], - declarations: [ - DiagnosticTestPlayerComponent, - ], - entryComponents: [ - DiagnosticTestPlayerComponent, + ToastrModule.forRoot(toastrConfig), ], + declarations: [DiagnosticTestPlayerComponent], + entryComponents: [DiagnosticTestPlayerComponent], providers: [ { provide: HTTP_INTERCEPTORS, @@ -82,24 +79,29 @@ declare var angular: ng.IAngularStatic; }, { provide: HAMMER_GESTURE_CONFIG, - useClass: MyHammerConfig + useClass: MyHammerConfig, }, AppErrorHandlerProvider, { provide: APP_BASE_HREF, - useValue: '/' - } + useValue: '/', + }, ], }) class DiagnosticTestPlayerPageModule implements DoBootstrap { ngDoBootstrap() {} } -angular.module('oppia').requires.push(downgradeModule(extraProviders => { - const platformRef = platformBrowserDynamic(extraProviders); - return platformRef.bootstrapModule(DiagnosticTestPlayerPageModule); -})); +angular.module('oppia').requires.push( + downgradeModule(extraProviders => { + const platformRef = platformBrowserDynamic(extraProviders); + return platformRef.bootstrapModule(DiagnosticTestPlayerPageModule); + }) +); -angular.module('oppia').directive('oppiaAngularRoot', downgradeComponent({ - component: OppiaAngularRootComponent, -})); +angular.module('oppia').directive( + 'oppiaAngularRoot', + downgradeComponent({ + component: OppiaAngularRootComponent, + }) +); diff --git a/core/templates/pages/diagnostic-test-player-page/diagnostic-test-player-status.service.spec.ts b/core/templates/pages/diagnostic-test-player-page/diagnostic-test-player-status.service.spec.ts index cdc642f7d104..80bbc447a677 100644 --- a/core/templates/pages/diagnostic-test-player-page/diagnostic-test-player-status.service.spec.ts +++ b/core/templates/pages/diagnostic-test-player-page/diagnostic-test-player-status.service.spec.ts @@ -16,9 +16,8 @@ * @fileoverview Test for the diagnostic test player status service. */ - -import { DiagnosticTestPlayerStatusService } from './diagnostic-test-player-status.service'; -import { TestBed } from '@angular/core/testing'; +import {DiagnosticTestPlayerStatusService} from './diagnostic-test-player-status.service'; +import {TestBed} from '@angular/core/testing'; describe('Diagnostic test player status service', () => { let dtpss: DiagnosticTestPlayerStatusService; diff --git a/core/templates/pages/diagnostic-test-player-page/diagnostic-test-player-status.service.ts b/core/templates/pages/diagnostic-test-player-page/diagnostic-test-player-status.service.ts index b6f4bd5431a4..b7efd730fa81 100644 --- a/core/templates/pages/diagnostic-test-player-page/diagnostic-test-player-status.service.ts +++ b/core/templates/pages/diagnostic-test-player-page/diagnostic-test-player-status.service.ts @@ -17,22 +17,21 @@ * in the diagnostic test session. */ - -import { EventEmitter, Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {EventEmitter, Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class DiagnosticTestPlayerStatusService { - private _diagnosticTestPlayerCompletedEventEmitter = ( - new EventEmitter()); + private _diagnosticTestPlayerCompletedEventEmitter = new EventEmitter< + string[] + >(); - private _diagnosticTestPlayerProgressChangeEventEmitter = ( - new EventEmitter()); + private _diagnosticTestPlayerProgressChangeEventEmitter = + new EventEmitter(); - private _diagnosticTestSkipQuestionEventEmitter = ( - new EventEmitter()); + private _diagnosticTestSkipQuestionEventEmitter = new EventEmitter(); get onDiagnosticTestSessionCompleted(): EventEmitter { return this._diagnosticTestPlayerCompletedEventEmitter; @@ -47,5 +46,9 @@ export class DiagnosticTestPlayerStatusService { } } -angular.module('oppia').factory('DiagnosticTestPlayerStatusService', - downgradeInjectable(DiagnosticTestPlayerStatusService)); +angular + .module('oppia') + .factory( + 'DiagnosticTestPlayerStatusService', + downgradeInjectable(DiagnosticTestPlayerStatusService) + ); diff --git a/core/templates/pages/diagnostic-test-player-page/diagnostic-test-player.component.spec.ts b/core/templates/pages/diagnostic-test-player-page/diagnostic-test-player.component.spec.ts index 2e4dca14f194..f14911fb4186 100644 --- a/core/templates/pages/diagnostic-test-player-page/diagnostic-test-player.component.spec.ts +++ b/core/templates/pages/diagnostic-test-player-page/diagnostic-test-player.component.spec.ts @@ -16,20 +16,25 @@ * @fileoverview Tests for the diagnostic test player component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { DiagnosticTestPlayerComponent } from './diagnostic-test-player.component'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { PreventPageUnloadEventService } from 'services/prevent-page-unload-event.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { DiagnosticTestPlayerStatusService } from './diagnostic-test-player-status.service'; -import { ClassroomBackendApiService } from 'domain/classroom/classroom-backend-api.service'; -import { CreatorTopicSummary } from 'domain/topic/creator-topic-summary.model'; -import { ClassroomData } from 'domain/classroom/classroom-data.model'; -import { DiagnosticTestTopicTrackerModel } from './diagnostic-test-topic-tracker.model'; -import { TranslateService } from '@ngx-translate/core'; -import { EventEmitter } from '@angular/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {DiagnosticTestPlayerComponent} from './diagnostic-test-player.component'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {PreventPageUnloadEventService} from 'services/prevent-page-unload-event.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {DiagnosticTestPlayerStatusService} from './diagnostic-test-player-status.service'; +import {ClassroomBackendApiService} from 'domain/classroom/classroom-backend-api.service'; +import {CreatorTopicSummary} from 'domain/topic/creator-topic-summary.model'; +import {ClassroomData} from 'domain/classroom/classroom-data.model'; +import {DiagnosticTestTopicTrackerModel} from './diagnostic-test-topic-tracker.model'; +import {TranslateService} from '@ngx-translate/core'; +import {EventEmitter} from '@angular/core'; class MockTranslateService { instant(key: string, interpolateParams?: Object): string { @@ -41,7 +46,7 @@ class MockWindowRef { _window = { location: { href: '', - reload: (val: boolean) => val + reload: (val: boolean) => val, }, }; @@ -69,29 +74,24 @@ describe('Diagnostic test player component', () => { windowRef = new MockWindowRef(); TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - ], - declarations: [ - DiagnosticTestPlayerComponent, - MockTranslatePipe - ], + imports: [HttpClientTestingModule], + declarations: [DiagnosticTestPlayerComponent, MockTranslatePipe], providers: [ PreventPageUnloadEventService, { provide: WindowRef, - useValue: windowRef + useValue: windowRef, }, { provide: TranslateService, - useClass: MockTranslateService + useClass: MockTranslateService, }, { provide: DiagnosticTestPlayerStatusService, - useClass: MockDiagnosticTestPlayerStatusService + useClass: MockDiagnosticTestPlayerStatusService, }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); @@ -99,72 +99,67 @@ describe('Diagnostic test player component', () => { fixture = TestBed.createComponent(DiagnosticTestPlayerComponent); component = fixture.componentInstance; preventPageUnloadEventService = TestBed.inject( - PreventPageUnloadEventService); + PreventPageUnloadEventService + ); classroomBackendApiService = TestBed.inject(ClassroomBackendApiService); translateService = TestBed.inject(TranslateService); }); it('should listen to page unload events after initialization', () => { - spyOn(preventPageUnloadEventService, 'addListener').and - .callFake((callback: () => boolean) => callback()); + spyOn(preventPageUnloadEventService, 'addListener').and.callFake( + (callback: () => boolean) => callback() + ); component.ngOnInit(); - expect(preventPageUnloadEventService.addListener) - .toHaveBeenCalledWith(jasmine.any(Function)); + expect(preventPageUnloadEventService.addListener).toHaveBeenCalledWith( + jasmine.any(Function) + ); }); - it( - 'should be able to get Oppia\'s avatar image URL after initialization', - () => { - spyOn(preventPageUnloadEventService, 'addListener'); + it("should be able to get Oppia's avatar image URL after initialization", () => { + spyOn(preventPageUnloadEventService, 'addListener'); - expect(component.OPPIA_AVATAR_IMAGE_URL).toEqual(''); + expect(component.OPPIA_AVATAR_IMAGE_URL).toEqual(''); - const avatarImageLocation = ( - '/assets/images/avatar/oppia_avatar_100px.svg'); + const avatarImageLocation = '/assets/images/avatar/oppia_avatar_100px.svg'; - component.ngOnInit(); + component.ngOnInit(); - expect(component.OPPIA_AVATAR_IMAGE_URL).toEqual(avatarImageLocation); - }); + expect(component.OPPIA_AVATAR_IMAGE_URL).toEqual(avatarImageLocation); + }); - it( - 'should be able to subscribe event emitters after initialization', - fakeAsync(() => { - spyOn(preventPageUnloadEventService, 'addListener'); - spyOn(component, 'getRecommendedTopicSummaries'); - spyOn(component, 'getProgressText'); + it('should be able to subscribe event emitters after initialization', fakeAsync(() => { + spyOn(preventPageUnloadEventService, 'addListener'); + spyOn(component, 'getRecommendedTopicSummaries'); + spyOn(component, 'getProgressText'); - component.ngOnInit(); - sessionCompleteEmitter.emit(['recommendedTopicId']); - progressEmitter.emit(20); - tick(200); + component.ngOnInit(); + sessionCompleteEmitter.emit(['recommendedTopicId']); + progressEmitter.emit(20); + tick(200); - expect( - component.getRecommendedTopicSummaries - ).toHaveBeenCalledWith(['recommendedTopicId']); + expect(component.getRecommendedTopicSummaries).toHaveBeenCalledWith([ + 'recommendedTopicId', + ]); - expect( - component.getProgressText - ).toHaveBeenCalled(); - })); + expect(component.getProgressText).toHaveBeenCalled(); + })); - it( - 'should be able to get the math classroom ID after initialization', - fakeAsync(() => { - spyOn(preventPageUnloadEventService, 'addListener'); - spyOn(classroomBackendApiService, 'getClassroomIdAsync') - .and.returnValue(Promise.resolve('mathClassroomId')); - component.classroomUrlFragment = 'math'; + it('should be able to get the math classroom ID after initialization', fakeAsync(() => { + spyOn(preventPageUnloadEventService, 'addListener'); + spyOn(classroomBackendApiService, 'getClassroomIdAsync').and.returnValue( + Promise.resolve('mathClassroomId') + ); + component.classroomUrlFragment = 'math'; - expect(component.classroomId).toEqual(''); + expect(component.classroomId).toEqual(''); - component.ngOnInit(); - tick(); + component.ngOnInit(); + tick(); - expect(component.classroomId).toEqual('mathClassroomId'); - })); + expect(component.classroomId).toEqual('mathClassroomId'); + })); it('should be able to get the topic button text', () => { let topicName = 'Fraction'; @@ -173,31 +168,78 @@ describe('Diagnostic test player component', () => { component.getTopicButtonText(topicName); expect(translateService.instant).toHaveBeenCalledWith( - 'I18N_DIAGNOSTIC_TEST_RESULT_START_TOPIC', { topicName: 'Fraction' }); + 'I18N_DIAGNOSTIC_TEST_RESULT_START_TOPIC', + {topicName: 'Fraction'} + ); }); it('should be able to get the topic URL from the URL fragment', () => { let topicUrlFragment = 'subtraction'; expect(component.getTopicUrlFromUrlFragment(topicUrlFragment)).toEqual( - '/learn/math/' + topicUrlFragment); + '/learn/math/' + topicUrlFragment + ); }); it('should be able to get topic recommendations', fakeAsync(() => { let cData1: CreatorTopicSummary = new CreatorTopicSummary( - 'dummy', 'addition', 3, 3, 3, 3, 1, - 'en', 'dummy', 1, 1, 1, 1, true, - true, 'math', 'public/img.webp', 'red', 'add', 1, 1, [5, 4], [3, 4]); + 'dummy', + 'addition', + 3, + 3, + 3, + 3, + 1, + 'en', + 'dummy', + 1, + 1, + 1, + 1, + true, + true, + 'math', + 'public/img.webp', + 'red', + 'add', + 1, + 1, + [5, 4], + [3, 4] + ); let cData2: CreatorTopicSummary = new CreatorTopicSummary( - 'dummy2', 'division', 2, 2, 3, 3, 0, - 'es', 'dummy2', 1, 1, 1, 1, true, - true, 'math', 'public/img1.png', 'green', 'div', 1, 1, [5, 4], [3, 4]); + 'dummy2', + 'division', + 2, + 2, + 3, + 3, + 0, + 'es', + 'dummy2', + 1, + 1, + 1, + 1, + true, + true, + 'math', + 'public/img1.png', + 'green', + 'div', + 1, + 1, + [5, 4], + [3, 4] + ); let array: CreatorTopicSummary[] = [cData1, cData2]; let classroomData = new ClassroomData('test', array, 'dummy', 'dummy'); - spyOn(classroomBackendApiService, 'fetchClassroomDataAsync') - .and.returnValue(Promise.resolve(classroomData)); + spyOn( + classroomBackendApiService, + 'fetchClassroomDataAsync' + ).and.returnValue(Promise.resolve(classroomData)); expect(component.recommendedTopicSummaries).toEqual([]); @@ -207,39 +249,40 @@ describe('Diagnostic test player component', () => { expect(component.recommendedTopicSummaries).toEqual([cData1]); })); - it( - 'should be able to set topic tracker model after starting diagnostic test', - fakeAsync(() => { - // A linear graph with 3 nodes. - const topicIdToPrerequisiteTopicIds = { - topicId1: [], - topicId2: ['topicId1'], - topicId3: ['topicId2'] - }; - - const diagnosticTestTopicTrackerModel = ( - new DiagnosticTestTopicTrackerModel(topicIdToPrerequisiteTopicIds)); - - let response = { - classroomDict: { - classroomId: 'classroomId', - name: 'math', - urlFragment: 'math', - courseDetails: '', - topicListIntro: '', - topicIdToPrerequisiteTopicIds: topicIdToPrerequisiteTopicIds - } - }; - - spyOn(classroomBackendApiService, 'getClassroomDataAsync') - .and.returnValue(Promise.resolve(response)); - - expect(component.diagnosticTestTopicTrackerModel).toEqual(undefined); - - component.startDiagnosticTest(); - tick(); - - expect(component.diagnosticTestTopicTrackerModel).toEqual( - diagnosticTestTopicTrackerModel); - })); + it('should be able to set topic tracker model after starting diagnostic test', fakeAsync(() => { + // A linear graph with 3 nodes. + const topicIdToPrerequisiteTopicIds = { + topicId1: [], + topicId2: ['topicId1'], + topicId3: ['topicId2'], + }; + + const diagnosticTestTopicTrackerModel = new DiagnosticTestTopicTrackerModel( + topicIdToPrerequisiteTopicIds + ); + + let response = { + classroomDict: { + classroomId: 'classroomId', + name: 'math', + urlFragment: 'math', + courseDetails: '', + topicListIntro: '', + topicIdToPrerequisiteTopicIds: topicIdToPrerequisiteTopicIds, + }, + }; + + spyOn(classroomBackendApiService, 'getClassroomDataAsync').and.returnValue( + Promise.resolve(response) + ); + + expect(component.diagnosticTestTopicTrackerModel).toEqual(undefined); + + component.startDiagnosticTest(); + tick(); + + expect(component.diagnosticTestTopicTrackerModel).toEqual( + diagnosticTestTopicTrackerModel + ); + })); }); diff --git a/core/templates/pages/diagnostic-test-player-page/diagnostic-test-player.component.ts b/core/templates/pages/diagnostic-test-player-page/diagnostic-test-player.component.ts index 012d2e47056e..66c74b1e10da 100644 --- a/core/templates/pages/diagnostic-test-player-page/diagnostic-test-player.component.ts +++ b/core/templates/pages/diagnostic-test-player-page/diagnostic-test-player.component.ts @@ -16,21 +16,20 @@ * @fileoverview Diagnostic test player component. */ -import { Component, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { ClassroomBackendApiService } from 'domain/classroom/classroom-backend-api.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { PreventPageUnloadEventService } from 'services/prevent-page-unload-event.service'; -import { DiagnosticTestTopicTrackerModel } from './diagnostic-test-topic-tracker.model'; -import { Subscription } from 'rxjs'; -import { DiagnosticTestPlayerStatusService } from './diagnostic-test-player-status.service'; -import { CreatorTopicSummary } from 'domain/topic/creator-topic-summary.model'; -import { TranslateService } from '@ngx-translate/core'; - +import {Component, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {ClassroomBackendApiService} from 'domain/classroom/classroom-backend-api.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {PreventPageUnloadEventService} from 'services/prevent-page-unload-event.service'; +import {DiagnosticTestTopicTrackerModel} from './diagnostic-test-topic-tracker.model'; +import {Subscription} from 'rxjs'; +import {DiagnosticTestPlayerStatusService} from './diagnostic-test-player-status.service'; +import {CreatorTopicSummary} from 'domain/topic/creator-topic-summary.model'; +import {TranslateService} from '@ngx-translate/core'; @Component({ selector: 'oppia-diagnostic-test-player', - templateUrl: './diagnostic-test-player.component.html' + templateUrl: './diagnostic-test-player.component.html', }) export class DiagnosticTestPlayerComponent implements OnInit { OPPIA_AVATAR_IMAGE_URL: string = ''; @@ -54,82 +53,95 @@ export class DiagnosticTestPlayerComponent implements OnInit { ngOnInit(): void { this.preventPageUnloadEventService.addListener(() => { - return (this.diagnosticTestIsStarted && !this.diagnosticTestIsFinished); + return this.diagnosticTestIsStarted && !this.diagnosticTestIsFinished; }); - this.OPPIA_AVATAR_IMAGE_URL = ( + this.OPPIA_AVATAR_IMAGE_URL = this.urlInterpolationService.getStaticImageUrl( - '/avatar/oppia_avatar_100px.svg')); + '/avatar/oppia_avatar_100px.svg' + ); this.componentSubscription.add( - this.diagnosticTestPlayerStatusService - .onDiagnosticTestSessionCompleted.subscribe( - (recommendedTopicIds: string[]) => { - this.getRecommendedTopicSummaries(recommendedTopicIds); - } - )); + this.diagnosticTestPlayerStatusService.onDiagnosticTestSessionCompleted.subscribe( + (recommendedTopicIds: string[]) => { + this.getRecommendedTopicSummaries(recommendedTopicIds); + } + ) + ); this.componentSubscription.add( - this.diagnosticTestPlayerStatusService - .onDiagnosticTestSessionProgressChange.subscribe( - (progressPercentage: number) => { - this.progressPercentage = progressPercentage; - this.getProgressText(); - } - )); + this.diagnosticTestPlayerStatusService.onDiagnosticTestSessionProgressChange.subscribe( + (progressPercentage: number) => { + this.progressPercentage = progressPercentage; + this.getProgressText(); + } + ) + ); this.getProgressText(); - this.classroomBackendApiService.getClassroomIdAsync( - this.classroomUrlFragment).then(classroomId => { - this.classroomId = classroomId; - }); + this.classroomBackendApiService + .getClassroomIdAsync(this.classroomUrlFragment) + .then(classroomId => { + this.classroomId = classroomId; + }); } getProgressText(): string { return this.translateService.instant( - 'I18N_DIAGNOSTIC_TEST_CURRENT_PROGRESS', { - progressPercentage: this.progressPercentage - }); + 'I18N_DIAGNOSTIC_TEST_CURRENT_PROGRESS', + { + progressPercentage: this.progressPercentage, + } + ); } startDiagnosticTest(): void { - this.classroomBackendApiService.getClassroomDataAsync( - this.classroomId).then(response => { - this.diagnosticTestTopicTrackerModel = ( - new DiagnosticTestTopicTrackerModel( - response.classroomDict.topicIdToPrerequisiteTopicIds)); - this.diagnosticTestIsStarted = true; - }); + this.classroomBackendApiService + .getClassroomDataAsync(this.classroomId) + .then(response => { + this.diagnosticTestTopicTrackerModel = + new DiagnosticTestTopicTrackerModel( + response.classroomDict.topicIdToPrerequisiteTopicIds + ); + this.diagnosticTestIsStarted = true; + }); } getRecommendedTopicSummaries(recommendedTopicIds: string[]): void { - this.classroomBackendApiService.fetchClassroomDataAsync( - this.classroomUrlFragment).then((classroomData) => { - let topicSummaries: CreatorTopicSummary[] = ( - classroomData.getTopicSummaries()); - this.recommendedTopicSummaries = topicSummaries.filter( - (topicSummary) => { + this.classroomBackendApiService + .fetchClassroomDataAsync(this.classroomUrlFragment) + .then(classroomData => { + let topicSummaries: CreatorTopicSummary[] = + classroomData.getTopicSummaries(); + this.recommendedTopicSummaries = topicSummaries.filter(topicSummary => { return recommendedTopicIds.indexOf(topicSummary.getId()) !== -1; }); - this.diagnosticTestIsFinished = true; - }); + this.diagnosticTestIsFinished = true; + }); } getTopicButtonText(topicName: string): string { return this.translateService.instant( - 'I18N_DIAGNOSTIC_TEST_RESULT_START_TOPIC', { - topicName: topicName - }); + 'I18N_DIAGNOSTIC_TEST_RESULT_START_TOPIC', + { + topicName: topicName, + } + ); } getTopicUrlFromUrlFragment(urlFragment: string): string { return this.urlInterpolationService.interpolateUrl( - '/learn/math/', { - topicUrlFragment: urlFragment - }); + '/learn/math/', + { + topicUrlFragment: urlFragment, + } + ); } } -angular.module('oppia').directive( - 'oppiaDiagnosticTestPlayer', downgradeComponent( - {component: DiagnosticTestPlayerComponent})); +angular + .module('oppia') + .directive( + 'oppiaDiagnosticTestPlayer', + downgradeComponent({component: DiagnosticTestPlayerComponent}) + ); diff --git a/core/templates/pages/diagnostic-test-player-page/diagnostic-test-topic-tracker.model.spec.ts b/core/templates/pages/diagnostic-test-player-page/diagnostic-test-topic-tracker.model.spec.ts index cbf545904909..05ce51ee7b3a 100644 --- a/core/templates/pages/diagnostic-test-player-page/diagnostic-test-topic-tracker.model.spec.ts +++ b/core/templates/pages/diagnostic-test-player-page/diagnostic-test-topic-tracker.model.spec.ts @@ -16,15 +16,14 @@ * @fileoverview Test for the diagnostic test topic tracker model. */ -import { TestBed } from '@angular/core/testing'; -import { DiagnosticTestTopicTrackerModel } from './diagnostic-test-topic-tracker.model'; - +import {TestBed} from '@angular/core/testing'; +import {DiagnosticTestTopicTrackerModel} from './diagnostic-test-topic-tracker.model'; describe('Diagnostic test topic tracker model', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [], - providers: [] + providers: [], }); }); @@ -33,31 +32,37 @@ describe('Diagnostic test topic tracker model', () => { const topicIdToPrerequisiteTopicIds = { topicID1: [], topicID2: ['topicID1'], - topicID3: ['topicID2'] + topicID3: ['topicID2'], }; const expectedTopicIdToAncestorTopicIds = { topicID1: [], topicID2: ['topicID1'], - topicID3: ['topicID1', 'topicID2'] + topicID3: ['topicID1', 'topicID2'], }; const diagnosticTestTopicTrackerModel = new DiagnosticTestTopicTrackerModel( - topicIdToPrerequisiteTopicIds); + topicIdToPrerequisiteTopicIds + ); - expect(diagnosticTestTopicTrackerModel.getTopicIdToPrerequisiteTopicIds()) - .toEqual(topicIdToPrerequisiteTopicIds); + expect( + diagnosticTestTopicTrackerModel.getTopicIdToPrerequisiteTopicIds() + ).toEqual(topicIdToPrerequisiteTopicIds); - expect(diagnosticTestTopicTrackerModel.getTopicIdToAncestorTopicIds()) - .toEqual(expectedTopicIdToAncestorTopicIds); + expect( + diagnosticTestTopicTrackerModel.getTopicIdToAncestorTopicIds() + ).toEqual(expectedTopicIdToAncestorTopicIds); - expect(diagnosticTestTopicTrackerModel.getAncestorTopicIds('topicID1')) - .toEqual([]); + expect( + diagnosticTestTopicTrackerModel.getAncestorTopicIds('topicID1') + ).toEqual([]); - expect(diagnosticTestTopicTrackerModel.getAncestorTopicIds('topicID2')) - .toEqual(['topicID1']); + expect( + diagnosticTestTopicTrackerModel.getAncestorTopicIds('topicID2') + ).toEqual(['topicID1']); - expect(diagnosticTestTopicTrackerModel.getAncestorTopicIds('topicID3')) - .toEqual(['topicID1', 'topicID2']); + expect( + diagnosticTestTopicTrackerModel.getAncestorTopicIds('topicID3') + ).toEqual(['topicID1', 'topicID2']); }); it('should be able to able to get topic ID to successor topic IDs', () => { @@ -65,35 +70,40 @@ describe('Diagnostic test topic tracker model', () => { const topicIdToPrerequisiteTopicIds = { topicID1: [], topicID2: ['topicID1'], - topicID3: ['topicID1'] + topicID3: ['topicID1'], }; const diagnosticTestTopicTrackerModel = new DiagnosticTestTopicTrackerModel( - topicIdToPrerequisiteTopicIds); + topicIdToPrerequisiteTopicIds + ); const expectedTopicIdToSuccessorTopicIds = { topicID1: ['topicID2', 'topicID3'], topicID2: [], - topicID3: [] + topicID3: [], }; - expect(diagnosticTestTopicTrackerModel.getTopicIdToSuccessorTopicIds()) - .toEqual(expectedTopicIdToSuccessorTopicIds); + expect( + diagnosticTestTopicTrackerModel.getTopicIdToSuccessorTopicIds() + ).toEqual(expectedTopicIdToSuccessorTopicIds); - expect(diagnosticTestTopicTrackerModel.getSuccessorTopicIds('topicID1')) - .toEqual(['topicID2', 'topicID3']); + expect( + diagnosticTestTopicTrackerModel.getSuccessorTopicIds('topicID1') + ).toEqual(['topicID2', 'topicID3']); - expect(diagnosticTestTopicTrackerModel.getSuccessorTopicIds('topicID2')) - .toEqual([]); + expect( + diagnosticTestTopicTrackerModel.getSuccessorTopicIds('topicID2') + ).toEqual([]); - expect(diagnosticTestTopicTrackerModel.getSuccessorTopicIds('topicID3')) - .toEqual([]); + expect( + diagnosticTestTopicTrackerModel.getSuccessorTopicIds('topicID3') + ).toEqual([]); }); it('should be able to get initial eligible topic IDs', () => { const topicIdToPrerequisiteTopicIds = { topicID1: [], topicID2: ['topicID1'], - topicID3: ['topicID1'] + topicID3: ['topicID1'], }; // Initially, all the topics are eligible for testing, then eventually // topics were filtered from the eligible list based on the performance @@ -101,33 +111,36 @@ describe('Diagnostic test topic tracker model', () => { const expectedEligibleTopicIDs = ['topicID1', 'topicID2', 'topicID3']; const diagnosticTestTopicTrackerModel = new DiagnosticTestTopicTrackerModel( - topicIdToPrerequisiteTopicIds); + topicIdToPrerequisiteTopicIds + ); expect(diagnosticTestTopicTrackerModel.getPendingTopicIdsToTest()).toEqual( - expectedEligibleTopicIDs); + expectedEligibleTopicIDs + ); }); it( 'should be able to get eligible topic IDs after the initially selected' + - ' topic is marked as failed', () => { + ' topic is marked as failed', + () => { // A non-linear topics dependency graph with 5 nodes. const topicIdToPrerequisiteTopicIds = { topicID1: [], topicID2: ['topicID1'], topicID3: ['topicID1'], topicID4: ['topicID2', 'topicID3'], - topicID5: ['topicID3'] + topicID5: ['topicID3'], }; - const diagnosticTestTopicTrackerModel = ( - new DiagnosticTestTopicTrackerModel(topicIdToPrerequisiteTopicIds)); + const diagnosticTestTopicTrackerModel = + new DiagnosticTestTopicTrackerModel(topicIdToPrerequisiteTopicIds); let expectedTopicIdToAncestorTopicIds = { topicID1: [], topicID2: ['topicID1'], topicID3: ['topicID1'], topicID4: ['topicID1', 'topicID2', 'topicID3'], - topicID5: ['topicID1', 'topicID3'] + topicID5: ['topicID1', 'topicID3'], }; let expectedTopicIdToSuccessorTopicIds = { @@ -135,27 +148,31 @@ describe('Diagnostic test topic tracker model', () => { topicID2: ['topicID4'], topicID3: ['topicID4', 'topicID5'], topicID4: [], - topicID5: [] + topicID5: [], }; - expect(diagnosticTestTopicTrackerModel.getTopicIdToAncestorTopicIds()) - .toEqual(expectedTopicIdToAncestorTopicIds); + expect( + diagnosticTestTopicTrackerModel.getTopicIdToAncestorTopicIds() + ).toEqual(expectedTopicIdToAncestorTopicIds); - expect(diagnosticTestTopicTrackerModel.getTopicIdToSuccessorTopicIds()) - .toEqual(expectedTopicIdToSuccessorTopicIds); + expect( + diagnosticTestTopicTrackerModel.getTopicIdToSuccessorTopicIds() + ).toEqual(expectedTopicIdToSuccessorTopicIds); // Initially, all the topics are eligible for testing, then eventually // topics were filtered from the eligible list based on the performance // in any selected topic. - expect(diagnosticTestTopicTrackerModel.getPendingTopicIdsToTest()) - .toEqual(['topicID1', 'topicID2', 'topicID3', 'topicID4', 'topicID5']); + expect( + diagnosticTestTopicTrackerModel.getPendingTopicIdsToTest() + ).toEqual(['topicID1', 'topicID2', 'topicID3', 'topicID4', 'topicID5']); // Assuming L = min(length of ancestors, length of successors). Among all // the eligible topic IDs, topic2 and topic3 have the maximum value for L. // Since topic2 appears before topic3, thus topic2 should be selected as // the next eligible topic ID. - expect(diagnosticTestTopicTrackerModel.selectNextTopicIdToTest()) - .toEqual('topicID2'); + expect(diagnosticTestTopicTrackerModel.selectNextTopicIdToTest()).toEqual( + 'topicID2' + ); // None of the topics are currently failed. expect(diagnosticTestTopicTrackerModel.getFailedTopicIds()).toEqual([]); @@ -167,55 +184,62 @@ describe('Diagnostic test topic tracker model', () => { diagnosticTestTopicTrackerModel.recordTopicFailed('topicID2'); // Updated eligible topic IDs list. - expect(diagnosticTestTopicTrackerModel.getPendingTopicIdsToTest()) - .toEqual(['topicID1', 'topicID3', 'topicID5']); + expect( + diagnosticTestTopicTrackerModel.getPendingTopicIdsToTest() + ).toEqual(['topicID1', 'topicID3', 'topicID5']); // Updated topic ID to ancestor topic IDs dict. - expect(diagnosticTestTopicTrackerModel.getTopicIdToAncestorTopicIds()) - .toEqual({ - topicID1: [], - topicID3: ['topicID1'], - topicID5: ['topicID1', 'topicID3'] - }); + expect( + diagnosticTestTopicTrackerModel.getTopicIdToAncestorTopicIds() + ).toEqual({ + topicID1: [], + topicID3: ['topicID1'], + topicID5: ['topicID1', 'topicID3'], + }); // Updated topic ID to successor topic IDs dict. - expect(diagnosticTestTopicTrackerModel.getTopicIdToSuccessorTopicIds()) - .toEqual({ - topicID1: ['topicID3', 'topicID5'], - topicID3: ['topicID5'], - topicID5: [] - }); - - expect(diagnosticTestTopicTrackerModel.getFailedTopicIds()).toEqual( - ['topicID2']); + expect( + diagnosticTestTopicTrackerModel.getTopicIdToSuccessorTopicIds() + ).toEqual({ + topicID1: ['topicID3', 'topicID5'], + topicID3: ['topicID5'], + topicID5: [], + }); + + expect(diagnosticTestTopicTrackerModel.getFailedTopicIds()).toEqual([ + 'topicID2', + ]); // Assuming L = min(length of ancestors, length of successors). Among all // the eligible topic IDs, topic 3 has the maximum value for L. Thus // topic 3 should be selected as the next eligible topic ID. - expect(diagnosticTestTopicTrackerModel.selectNextTopicIdToTest()) - .toEqual('topicID3'); - }); + expect(diagnosticTestTopicTrackerModel.selectNextTopicIdToTest()).toEqual( + 'topicID3' + ); + } + ); it( 'should be able to get eligible topic IDs after the initially selected' + - ' topic is marked as passed', () => { + ' topic is marked as passed', + () => { const topicIdToPrerequisiteTopicIds = { topicID1: [], topicID2: ['topicID1'], topicID3: ['topicID1'], topicID4: ['topicID2', 'topicID3'], - topicID5: ['topicID3'] + topicID5: ['topicID3'], }; - const diagnosticTestTopicTrackerModel = ( - new DiagnosticTestTopicTrackerModel(topicIdToPrerequisiteTopicIds)); + const diagnosticTestTopicTrackerModel = + new DiagnosticTestTopicTrackerModel(topicIdToPrerequisiteTopicIds); const expectedTopicIdToAncestorTopicIds = { topicID1: [], topicID2: ['topicID1'], topicID3: ['topicID1'], topicID4: ['topicID1', 'topicID2', 'topicID3'], - topicID5: ['topicID1', 'topicID3'] + topicID5: ['topicID1', 'topicID3'], }; const expectedTopicIdToSuccessorTopicIds = { @@ -223,26 +247,30 @@ describe('Diagnostic test topic tracker model', () => { topicID2: ['topicID4'], topicID3: ['topicID4', 'topicID5'], topicID4: [], - topicID5: [] + topicID5: [], }; - expect(diagnosticTestTopicTrackerModel.getTopicIdToAncestorTopicIds()) - .toEqual(expectedTopicIdToAncestorTopicIds); + expect( + diagnosticTestTopicTrackerModel.getTopicIdToAncestorTopicIds() + ).toEqual(expectedTopicIdToAncestorTopicIds); - expect(diagnosticTestTopicTrackerModel.getTopicIdToSuccessorTopicIds()) - .toEqual(expectedTopicIdToSuccessorTopicIds); + expect( + diagnosticTestTopicTrackerModel.getTopicIdToSuccessorTopicIds() + ).toEqual(expectedTopicIdToSuccessorTopicIds); // Initially, all the topics are eligible for testing, then eventually // topics were filtered from the eligible list based on the performance // in any selected topic. - expect(diagnosticTestTopicTrackerModel.getPendingTopicIdsToTest()) - .toEqual(['topicID1', 'topicID2', 'topicID3', 'topicID4', 'topicID5']); + expect( + diagnosticTestTopicTrackerModel.getPendingTopicIdsToTest() + ).toEqual(['topicID1', 'topicID2', 'topicID3', 'topicID4', 'topicID5']); // Assuming L = min(length of ancestors, length of successors). Among all // the eligible topic IDs, topic 2 has the maximum value for L. Thus // topic 2 should be selected as the next eligible topic ID. - expect(diagnosticTestTopicTrackerModel.selectNextTopicIdToTest()) - .toEqual('topicID2'); + expect(diagnosticTestTopicTrackerModel.selectNextTopicIdToTest()).toEqual( + 'topicID2' + ); // Marking the current topic (topic2) as passed, will remove the current // topic and all of its ancestors (topic1) from the eligible topic IDs, @@ -251,63 +279,72 @@ describe('Diagnostic test topic tracker model', () => { diagnosticTestTopicTrackerModel.recordTopicPassed('topicID2'); // Updated eligible topic IDs list after removing the ancestors. - expect(diagnosticTestTopicTrackerModel.getPendingTopicIdsToTest()) - .toEqual(['topicID3', 'topicID4', 'topicID5']); + expect( + diagnosticTestTopicTrackerModel.getPendingTopicIdsToTest() + ).toEqual(['topicID3', 'topicID4', 'topicID5']); // Updated topic ID to ancestor topic IDs dict. - expect(diagnosticTestTopicTrackerModel.getTopicIdToAncestorTopicIds()) - .toEqual({ - topicID3: [], - topicID4: ['topicID3'], - topicID5: ['topicID3'] - }); + expect( + diagnosticTestTopicTrackerModel.getTopicIdToAncestorTopicIds() + ).toEqual({ + topicID3: [], + topicID4: ['topicID3'], + topicID5: ['topicID3'], + }); // Updated topic ID to successor topic IDs dict. - expect(diagnosticTestTopicTrackerModel.getTopicIdToSuccessorTopicIds()) - .toEqual({ - topicID3: ['topicID4', 'topicID5'], - topicID4: [], - topicID5: [] - }); + expect( + diagnosticTestTopicTrackerModel.getTopicIdToSuccessorTopicIds() + ).toEqual({ + topicID3: ['topicID4', 'topicID5'], + topicID4: [], + topicID5: [], + }); // Assuming L = min(length of ancestors, length of successors). Among all // the eligible topic IDs, topic 3 has the maximum value for L. Thus // topic 3 should be selected as the next eligible topic ID. - expect(diagnosticTestTopicTrackerModel.selectNextTopicIdToTest()) - .toEqual('topicID3'); - }); + expect(diagnosticTestTopicTrackerModel.selectNextTopicIdToTest()).toEqual( + 'topicID3' + ); + } + ); it( 'should be able to generate ancestors and successors of each topic for ' + - 'a dependency graph containing loops', () => { + 'a dependency graph containing loops', + () => { const topicIdToPrerequisiteTopicIds = { topicID1: [], topicID2: ['topicID1'], topicID3: ['topicID1'], topicID4: ['topicID3', 'topicID5'], - topicID5: ['topicID2'] + topicID5: ['topicID2'], }; const expectedTopicIdToAncestorTopicIds = { topicID1: [], topicID2: ['topicID1'], topicID3: ['topicID1'], topicID4: ['topicID1', 'topicID2', 'topicID3', 'topicID5'], - topicID5: ['topicID1', 'topicID2'] + topicID5: ['topicID1', 'topicID2'], }; const expectedTopicIdToSuccessorTopicIds = { topicID1: ['topicID2', 'topicID3', 'topicID4', 'topicID5'], topicID2: ['topicID4', 'topicID5'], topicID3: ['topicID4'], topicID4: [], - topicID5: ['topicID4'] + topicID5: ['topicID4'], }; - const diagnosticTestTopicTrackerModel = ( - new DiagnosticTestTopicTrackerModel(topicIdToPrerequisiteTopicIds)); + const diagnosticTestTopicTrackerModel = + new DiagnosticTestTopicTrackerModel(topicIdToPrerequisiteTopicIds); - expect(diagnosticTestTopicTrackerModel.getTopicIdToAncestorTopicIds()) - .toEqual(expectedTopicIdToAncestorTopicIds); + expect( + diagnosticTestTopicTrackerModel.getTopicIdToAncestorTopicIds() + ).toEqual(expectedTopicIdToAncestorTopicIds); - expect(diagnosticTestTopicTrackerModel.getTopicIdToSuccessorTopicIds()) - .toEqual(expectedTopicIdToSuccessorTopicIds); - }); + expect( + diagnosticTestTopicTrackerModel.getTopicIdToSuccessorTopicIds() + ).toEqual(expectedTopicIdToSuccessorTopicIds); + } + ); }); diff --git a/core/templates/pages/diagnostic-test-player-page/diagnostic-test-topic-tracker.model.ts b/core/templates/pages/diagnostic-test-player-page/diagnostic-test-topic-tracker.model.ts index e8c8c42423b9..0d6d1dfe00eb 100644 --- a/core/templates/pages/diagnostic-test-player-page/diagnostic-test-topic-tracker.model.ts +++ b/core/templates/pages/diagnostic-test-player-page/diagnostic-test-topic-tracker.model.ts @@ -21,12 +21,10 @@ import cloneDeep from 'lodash/cloneDeep'; - export interface TopicIdToRelatedTopicIds { [topicId: string]: string[]; } - export class DiagnosticTestTopicTrackerModel { // The list of pending topic IDs from which the next topic can be selected // and tested in the diagnostic test. @@ -59,7 +57,8 @@ export class DiagnosticTestTopicTrackerModel { this._topicIdToPrerequisiteTopicIds = topicIdToPrerequisiteTopicIds; this._pendingTopicIdsToTest = Object.keys( - this._topicIdToPrerequisiteTopicIds).sort(); + this._topicIdToPrerequisiteTopicIds + ).sort(); this.generateTopicIdToAncestorTopicIds(); this.generateTopicIdToSuccessorTopicIds(); @@ -102,7 +101,8 @@ export class DiagnosticTestTopicTrackerModel { for (let topicId in this._topicIdToPrerequisiteTopicIds) { let ancestorTopicIds: string[] = []; let unprocessedAncestorTopicIds: string[] = cloneDeep( - this._topicIdToPrerequisiteTopicIds[topicId]); + this._topicIdToPrerequisiteTopicIds[topicId] + ); let visitedTopicIdsForCurrentTopic: string[] = []; let lastTopicId: string; @@ -119,8 +119,9 @@ export class DiagnosticTestTopicTrackerModel { unprocessedAncestorTopicIds = unprocessedAncestorTopicIds.concat( this._topicIdToPrerequisiteTopicIds[lastTopicId].filter( - (topic) => visitedTopicIdsForCurrentTopic.indexOf(topic) === -1 - )); + topic => visitedTopicIdsForCurrentTopic.indexOf(topic) === -1 + ) + ); visitedTopicIdsForCurrentTopic.push(lastTopicId); } this._topicIdToAncestorTopicIds[topicId] = ancestorTopicIds.sort(); @@ -128,7 +129,7 @@ export class DiagnosticTestTopicTrackerModel { } _generateTopicIdToChildTopicId( - topicIdToPrerequisiteTopicIds: TopicIdToRelatedTopicIds + topicIdToPrerequisiteTopicIds: TopicIdToRelatedTopicIds ): TopicIdToRelatedTopicIds { // The method generates a dict with topic ID as the key and its immediate // successor topic IDs as value. The successor's list is generated using @@ -156,13 +157,14 @@ export class DiagnosticTestTopicTrackerModel { // the prerequisite dependency graph. // Example: A -> B -> C, here the topic ID to successor topic IDs dict will // look like: {A: [B, C], B: [C], C: []}. - let topicIdToChildTopicId: TopicIdToRelatedTopicIds = ( - this._generateTopicIdToChildTopicId(this._topicIdToPrerequisiteTopicIds)); + let topicIdToChildTopicId: TopicIdToRelatedTopicIds = + this._generateTopicIdToChildTopicId(this._topicIdToPrerequisiteTopicIds); for (let topicId in topicIdToChildTopicId) { let successorTopicIds: string[] = []; let unprocessedSuccessorTopicIds: string[] = cloneDeep( - topicIdToChildTopicId[topicId]); + topicIdToChildTopicId[topicId] + ); let visitedTopicIdsForCurrentTopic: string[] = []; let lastTopicId: string; @@ -179,8 +181,9 @@ export class DiagnosticTestTopicTrackerModel { unprocessedSuccessorTopicIds = unprocessedSuccessorTopicIds.concat( topicIdToChildTopicId[lastTopicId].filter( - (topicId) => visitedTopicIdsForCurrentTopic.indexOf(topicId) === -1 - )); + topicId => visitedTopicIdsForCurrentTopic.indexOf(topicId) === -1 + ) + ); visitedTopicIdsForCurrentTopic.push(lastTopicId); } this._topicIdToSuccessorTopicIds[topicId] = successorTopicIds.sort(); @@ -200,7 +203,9 @@ export class DiagnosticTestTopicTrackerModel { // removed from the eligible topic IDs after testing, is the minimum of // the counts of its ancestors and successors. topicIdToLengthOfRelatedTopicIds[topicId] = Math.min( - ancestorTopicIds.length, successorTopicIds.length); + ancestorTopicIds.length, + successorTopicIds.length + ); } // Among all the eligible topics, the topic with the maximum value for @@ -209,10 +214,12 @@ export class DiagnosticTestTopicTrackerModel { // recommendations. return Object.keys(topicIdToLengthOfRelatedTopicIds).reduce( (item1, item2) => { - return ( - topicIdToLengthOfRelatedTopicIds[item1] >= - topicIdToLengthOfRelatedTopicIds[item2] ? item1 : item2); - }); + return topicIdToLengthOfRelatedTopicIds[item1] >= + topicIdToLengthOfRelatedTopicIds[item2] + ? item1 + : item2; + } + ); } recordTopicPassed(passedTopicId: string): void { @@ -230,7 +237,7 @@ export class DiagnosticTestTopicTrackerModel { topicIdsToRemove.push(passedTopicId); this._pendingTopicIdsToTest = this._pendingTopicIdsToTest.filter( - (topicId) => (topicIdsToRemove.indexOf(topicId) === -1) + topicId => topicIdsToRemove.indexOf(topicId) === -1 ); this.removeTopicIdsFromTopicIdToAncestorsDict(topicIdsToRemove); this.removeTopicIdsFromTopicIdToSuccessorsDict(topicIdsToRemove); @@ -252,7 +259,7 @@ export class DiagnosticTestTopicTrackerModel { topicIdsToRemove.push(failedTopicId); this._pendingTopicIdsToTest = this._pendingTopicIdsToTest.filter( - (topicId) => (topicIdsToRemove.indexOf(topicId) === -1) + topicId => topicIdsToRemove.indexOf(topicId) === -1 ); this.removeTopicIdsFromTopicIdToAncestorsDict(topicIdsToRemove); this.removeTopicIdsFromTopicIdToSuccessorsDict(topicIdsToRemove); @@ -266,7 +273,7 @@ export class DiagnosticTestTopicTrackerModel { } for (let topicId in this._topicIdToAncestorTopicIds) { let ancestors = this._topicIdToAncestorTopicIds[topicId]; - this._topicIdToAncestorTopicIds[topicId] = ancestors.filter((topic) => { + this._topicIdToAncestorTopicIds[topicId] = ancestors.filter(topic => { return topicIdsToRemove.indexOf(topic) === -1; }); } @@ -280,7 +287,7 @@ export class DiagnosticTestTopicTrackerModel { } for (let topicId in this._topicIdToSuccessorTopicIds) { let successors = this._topicIdToSuccessorTopicIds[topicId]; - this._topicIdToSuccessorTopicIds[topicId] = successors.filter((topic) => { + this._topicIdToSuccessorTopicIds[topicId] = successors.filter(topic => { return topicIdsToRemove.indexOf(topic) === -1; }); } diff --git a/core/templates/pages/donate-page/donate-page-root.component.spec.ts b/core/templates/pages/donate-page/donate-page-root.component.spec.ts index f88f294d872c..51709ef3cb0a 100644 --- a/core/templates/pages/donate-page/donate-page-root.component.spec.ts +++ b/core/templates/pages/donate-page/donate-page-root.component.spec.ts @@ -16,12 +16,12 @@ * @fileoverview Unit tests for the donate page root component. */ -import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; -import { AppConstants } from 'app.constants'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { DonatePageRootComponent } from './donate-page-root.component'; +import {AppConstants} from 'app.constants'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {DonatePageRootComponent} from './donate-page-root.component'; describe('Donate Page Root', () => { let fixture: ComponentFixture; @@ -29,13 +29,9 @@ describe('Donate Page Root', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - DonatePageRootComponent, - MockTranslatePipe, - ], - providers: [ - ], - schemas: [CUSTOM_ELEMENTS_SCHEMA] + declarations: [DonatePageRootComponent, MockTranslatePipe], + providers: [], + schemas: [CUSTOM_ELEMENTS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(DonatePageRootComponent); component = fixture.componentInstance; @@ -44,8 +40,10 @@ describe('Donate Page Root', () => { it('should be defined', () => { expect(component).toBeDefined(); expect(component.title).toEqual( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.DONATE.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.DONATE.TITLE + ); expect(component.meta).toEqual( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.DONATE.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.DONATE.META + ); }); }); diff --git a/core/templates/pages/donate-page/donate-page-root.component.ts b/core/templates/pages/donate-page/donate-page-root.component.ts index 855601f5c08a..f07ced286b99 100644 --- a/core/templates/pages/donate-page/donate-page-root.component.ts +++ b/core/templates/pages/donate-page/donate-page-root.component.ts @@ -16,18 +16,17 @@ * @fileoverview Root Component for donate page. */ -import { Component } from '@angular/core'; +import {Component} from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { BaseRootComponent, MetaTagData } from 'pages/base-root.component'; +import {AppConstants} from 'app.constants'; +import {BaseRootComponent, MetaTagData} from 'pages/base-root.component'; @Component({ selector: 'oppia-donate-page-root', - templateUrl: './donate-page-root.component.html' + templateUrl: './donate-page-root.component.html', }) export class DonatePageRootComponent extends BaseRootComponent { title: string = AppConstants.PAGES_REGISTERED_WITH_FRONTEND.DONATE.TITLE; - meta: MetaTagData[] = - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.DONATE.META as - unknown as Readonly[]; + meta: MetaTagData[] = AppConstants.PAGES_REGISTERED_WITH_FRONTEND.DONATE + .META as unknown as Readonly[]; } diff --git a/core/templates/pages/donate-page/donate-page.component.spec.ts b/core/templates/pages/donate-page/donate-page.component.spec.ts index 11afcf1fcecb..bf8a7a634728 100644 --- a/core/templates/pages/donate-page/donate-page.component.spec.ts +++ b/core/templates/pages/donate-page/donate-page.component.spec.ts @@ -16,16 +16,16 @@ * @fileoverview Unit tests for donate page. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; -import { DonatePageComponent } from './donate-page.component'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { DonationBoxModalComponent } from './donation-box/donation-box-modal.component'; -import { ThanksForDonatingModalComponent } from './thanks-for-donating-modal.component'; +import {DonatePageComponent} from './donate-page.component'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {DonationBoxModalComponent} from './donation-box/donation-box-modal.component'; +import {ThanksForDonatingModalComponent} from './thanks-for-donating-modal.component'; class MockWindowRef { _window = { @@ -38,10 +38,10 @@ class MockWindowRef { set href(val) { this._href = val; }, - replace: (val: string) => {} + replace: (val: string) => {}, }, gtag: () => {}, - onhashchange: () => {} + onhashchange: () => {}, }; get nativeWindow() { @@ -59,15 +59,12 @@ describe('Donate page', () => { beforeEach(() => { windowRef = new MockWindowRef(); TestBed.configureTestingModule({ - declarations: [ - DonatePageComponent, - MockTranslatePipe - ], + declarations: [DonatePageComponent, MockTranslatePipe], providers: [ UrlInterpolationService, - { provide: WindowRef, useValue: windowRef }, + {provide: WindowRef, useValue: windowRef}, ], - schemas: [CUSTOM_ELEMENTS_SCHEMA] + schemas: [CUSTOM_ELEMENTS_SCHEMA], }).compileComponents(); }); @@ -85,7 +82,8 @@ describe('Donate page', () => { component.getStaticImageUrl('abc.webp'); expect(urlInterpolationService.getStaticImageUrl).toHaveBeenCalledWith( - 'abc.webp'); + 'abc.webp' + ); }); it('should show thank you modal on query parameters change', () => { @@ -111,13 +109,11 @@ describe('Donate page', () => { it('should open donation box modal', () => { component.openDonationBoxModal(); - expect(ngbModal.open).toHaveBeenCalledWith( - DonationBoxModalComponent, { - backdrop: 'static', - size: 'xl', - windowClass: 'donation-box-modal', - } - ); + expect(ngbModal.open).toHaveBeenCalledWith(DonationBoxModalComponent, { + backdrop: 'static', + size: 'xl', + windowClass: 'donation-box-modal', + }); }); it('should change learner tile in carousel', () => { diff --git a/core/templates/pages/donate-page/donate-page.component.ts b/core/templates/pages/donate-page/donate-page.component.ts index 3762459885fe..12e8ea5b547a 100644 --- a/core/templates/pages/donate-page/donate-page.component.ts +++ b/core/templates/pages/donate-page/donate-page.component.ts @@ -16,14 +16,20 @@ * @fileoverview Component for the donate page. */ -import { Component, ElementRef, OnInit, QueryList, ViewChildren } from '@angular/core'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import { + Component, + ElementRef, + OnInit, + QueryList, + ViewChildren, +} from '@angular/core'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; import 'popper.js'; import 'bootstrap'; -import { ThanksForDonatingModalComponent } from './thanks-for-donating-modal.component'; -import { DonationBoxModalComponent } from './donation-box/donation-box-modal.component'; +import {ThanksForDonatingModalComponent} from './thanks-for-donating-modal.component'; +import {DonationBoxModalComponent} from './donation-box/donation-box-modal.component'; interface ImpactStat { imageUrl: string | null; @@ -55,7 +61,6 @@ interface Learner { templateUrl: './donate-page.component.html', styleUrls: [], }) - export class DonatePageComponent implements OnInit { donationValues: DonationValue[] = [ { @@ -137,7 +142,7 @@ export class DonatePageComponent implements OnInit { country: 'I18N_DONATE_PAGE_CONTENT_LEARNER_COUNTRY_1', imageUrl: '/donate/learners-abasiekeme.png', webpUrl: '/donate/learners-abasiekeme.webp', - text: 'I18N_DONATE_PAGE_CONTENT_LEARNER_QUOTE_1' + text: 'I18N_DONATE_PAGE_CONTENT_LEARNER_QUOTE_1', }, { name: 'Sandra Bosso', @@ -180,7 +185,8 @@ export class DonatePageComponent implements OnInit { ngOnInit(): void { const searchParams = new URLSearchParams( - this.windowRef.nativeWindow.location.search); + this.windowRef.nativeWindow.location.search + ); const params = Object.fromEntries(searchParams.entries()); if (params.hasOwnProperty('thanks')) { this.ngbModal.open(ThanksForDonatingModalComponent, { @@ -208,7 +214,9 @@ export class DonatePageComponent implements OnInit { if (learnerTile !== null) { learnerTile.scrollIntoView({ - behavior: 'smooth', block: 'nearest', inline: 'center' + behavior: 'smooth', + block: 'nearest', + inline: 'center', }); } } diff --git a/core/templates/pages/donate-page/donate-page.module.ts b/core/templates/pages/donate-page/donate-page.module.ts index 15de02a7e86b..29ae308556f7 100644 --- a/core/templates/pages/donate-page/donate-page.module.ts +++ b/core/templates/pages/donate-page/donate-page.module.ts @@ -16,15 +16,15 @@ * @fileoverview Module for the donate page. */ -import { NgModule } from '@angular/core'; -import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { DonatePageComponent } from './donate-page.component'; -import { DonationBoxComponent } from './donation-box/donation-box.component'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { DonatePageRootComponent } from './donate-page-root.component'; -import { CommonModule } from '@angular/common'; -import { RouterModule } from '@angular/router'; -import { DonationBoxModalComponent } from './donation-box/donation-box-modal.component'; +import {NgModule} from '@angular/core'; +import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; +import {DonatePageComponent} from './donate-page.component'; +import {DonationBoxComponent} from './donation-box/donation-box.component'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {DonatePageRootComponent} from './donate-page-root.component'; +import {CommonModule} from '@angular/common'; +import {RouterModule} from '@angular/router'; +import {DonationBoxModalComponent} from './donation-box/donation-box-modal.component'; @NgModule({ imports: [ @@ -47,7 +47,7 @@ import { DonationBoxModalComponent } from './donation-box/donation-box-modal.com entryComponents: [ DonatePageComponent, DonatePageRootComponent, - DonationBoxModalComponent + DonationBoxModalComponent, ], }) export class DonatePageModule {} diff --git a/core/templates/pages/donate-page/donation-box/donation-box-modal.component.spec.ts b/core/templates/pages/donate-page/donation-box/donation-box-modal.component.spec.ts index e367cc37e147..5af73884a859 100644 --- a/core/templates/pages/donate-page/donation-box/donation-box-modal.component.spec.ts +++ b/core/templates/pages/donate-page/donation-box/donation-box-modal.component.spec.ts @@ -16,11 +16,11 @@ * @fileoverview Tests for the donation box modal component */ -import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; -import { DonationBoxModalComponent } from './donation-box-modal.component'; +import {DonationBoxModalComponent} from './donation-box-modal.component'; class MockActiveModal { dismiss(): void { @@ -43,10 +43,10 @@ describe('Donation box modal', () => { providers: [ { provide: NgbActiveModal, - useClass: MockActiveModal - } + useClass: MockActiveModal, + }, ], - schemas: [CUSTOM_ELEMENTS_SCHEMA] + schemas: [CUSTOM_ELEMENTS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(DonationBoxModalComponent); component = fixture.componentInstance; diff --git a/core/templates/pages/donate-page/donation-box/donation-box-modal.component.ts b/core/templates/pages/donate-page/donation-box/donation-box-modal.component.ts index a7eff641e1f7..7856b2c79ba2 100644 --- a/core/templates/pages/donate-page/donation-box/donation-box-modal.component.ts +++ b/core/templates/pages/donate-page/donation-box/donation-box-modal.component.ts @@ -16,8 +16,8 @@ * @fileoverview Controller for the donation box modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; @Component({ selector: 'donation-box-modal', diff --git a/core/templates/pages/donate-page/donation-box/donation-box.component.spec.ts b/core/templates/pages/donate-page/donation-box/donation-box.component.spec.ts index f54009c3fa53..2881c3efe709 100644 --- a/core/templates/pages/donate-page/donation-box/donation-box.component.spec.ts +++ b/core/templates/pages/donate-page/donation-box/donation-box.component.spec.ts @@ -16,9 +16,12 @@ * @fileoverview Tests for the donation box component */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { InsertScriptService, KNOWN_SCRIPTS } from 'services/insert-script.service'; -import { DonationBoxComponent } from './donation-box.component'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import { + InsertScriptService, + KNOWN_SCRIPTS, +} from 'services/insert-script.service'; +import {DonationBoxComponent} from './donation-box.component'; describe('Donation box', () => { let component: DonationBoxComponent; @@ -40,6 +43,7 @@ describe('Donation box', () => { component.ngOnInit(); expect(insertScriptService.loadScript).toHaveBeenCalledOnceWith( - KNOWN_SCRIPTS.DONORBOX); + KNOWN_SCRIPTS.DONORBOX + ); }); }); diff --git a/core/templates/pages/donate-page/donation-box/donation-box.component.ts b/core/templates/pages/donate-page/donation-box/donation-box.component.ts index 54f635d26f10..76310e4315a1 100644 --- a/core/templates/pages/donate-page/donation-box/donation-box.component.ts +++ b/core/templates/pages/donate-page/donation-box/donation-box.component.ts @@ -16,23 +16,27 @@ * @fileoverview Donation box component */ -import { Component, OnInit } from '@angular/core'; +import {Component, OnInit} from '@angular/core'; -import { InsertScriptService, KNOWN_SCRIPTS } from 'services/insert-script.service'; +import { + InsertScriptService, + KNOWN_SCRIPTS, +} from 'services/insert-script.service'; @Component({ selector: 'donation-box', template: ` `, }) diff --git a/core/templates/pages/donate-page/thanks-for-donating-modal.component.spec.ts b/core/templates/pages/donate-page/thanks-for-donating-modal.component.spec.ts index 631dadc7c841..e8f55eb1de14 100644 --- a/core/templates/pages/donate-page/thanks-for-donating-modal.component.spec.ts +++ b/core/templates/pages/donate-page/thanks-for-donating-modal.component.spec.ts @@ -16,11 +16,11 @@ * @fileoverview Unit tests for thanks for donating modal component. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { ThanksForDonatingModalComponent } from './thanks-for-donating-modal.component'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {ThanksForDonatingModalComponent} from './thanks-for-donating-modal.component'; class MockActiveModal { dismiss(): void { @@ -43,9 +43,9 @@ describe('Thanks For Donating Modal Component', () => { providers: [ { provide: NgbActiveModal, - useClass: MockActiveModal - } - ] + useClass: MockActiveModal, + }, + ], }).compileComponents(); })); diff --git a/core/templates/pages/donate-page/thanks-for-donating-modal.component.ts b/core/templates/pages/donate-page/thanks-for-donating-modal.component.ts index abb63b9df6df..3853ae37d03b 100644 --- a/core/templates/pages/donate-page/thanks-for-donating-modal.component.ts +++ b/core/templates/pages/donate-page/thanks-for-donating-modal.component.ts @@ -16,18 +16,15 @@ * @fileoverview Controller for the donation page thanks for donating modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; - +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; @Component({ selector: 'thanks-for-donating-modal', templateUrl: './thanks-for-donating-modal.component.html', }) export class ThanksForDonatingModalComponent { - constructor( - private activeModal: NgbActiveModal - ) {} + constructor(private activeModal: NgbActiveModal) {} dismiss(): void { this.activeModal.dismiss(); diff --git a/core/templates/pages/email-dashboard-pages/email-dashboard-auth.guard.spec.ts b/core/templates/pages/email-dashboard-pages/email-dashboard-auth.guard.spec.ts index b7e4351c8340..a62f84b425fb 100644 --- a/core/templates/pages/email-dashboard-pages/email-dashboard-auth.guard.spec.ts +++ b/core/templates/pages/email-dashboard-pages/email-dashboard-auth.guard.spec.ts @@ -16,15 +16,19 @@ * @fileoverview Tests for EmailDashboardAuthGuard */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TestBed } from '@angular/core/testing'; -import { ActivatedRouteSnapshot, Router, RouterStateSnapshot, NavigationExtras } from '@angular/router'; - -import { AppConstants } from '../../app.constants'; -import { UserInfo } from '../../domain/user/user-info.model'; -import { UserService } from '../../services/user.service'; -import { EmailDashboardAuthGuard } from './email-dashboard-auth.guard'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TestBed} from '@angular/core/testing'; +import { + ActivatedRouteSnapshot, + Router, + RouterStateSnapshot, + NavigationExtras, +} from '@angular/router'; +import {AppConstants} from '../../app.constants'; +import {UserInfo} from '../../domain/user/user-info.model'; +import {UserService} from '../../services/user.service'; +import {EmailDashboardAuthGuard} from './email-dashboard-auth.guard'; class MockRouter { navigate(commands: string[], extras?: NavigationExtras): Promise { @@ -40,7 +44,7 @@ describe('EmailDashboardAuthGuard', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [UserService, { provide: Router, useClass: MockRouter }], + providers: [UserService, {provide: Router, useClass: MockRouter}], }).compileComponents(); guard = TestBed.inject(EmailDashboardAuthGuard); @@ -48,35 +52,39 @@ describe('EmailDashboardAuthGuard', () => { router = TestBed.inject(Router); }); - it('should redirect to 401 page if user is not super admin', (done) => { + it('should redirect to 401 page if user is not super admin', done => { const getUserInfoAsyncSpy = spyOn( - userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(UserInfo.createDefault()) - ); + userService, + 'getUserInfoAsync' + ).and.returnValue(Promise.resolve(UserInfo.createDefault())); const navigateSpy = spyOn(router, 'navigate').and.callThrough(); - guard.canActivate( - new ActivatedRouteSnapshot(), {} as RouterStateSnapshot).then( - (canActivate) => { + guard + .canActivate(new ActivatedRouteSnapshot(), {} as RouterStateSnapshot) + .then(canActivate => { expect(canActivate).toBeFalse(); expect(getUserInfoAsyncSpy).toHaveBeenCalledTimes(1); expect(navigateSpy).toHaveBeenCalledWith([ - `${AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ERROR.ROUTE}/401`]); + `${AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ERROR.ROUTE}/401`, + ]); done(); }); }); - it('should access the email dashboard if the user is super admin', (done) => { + it('should access the email dashboard if the user is super admin', done => { const getUserInfoAsyncSpy = spyOn( - userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(new UserInfo( - [], false, false, true, false, false, '', '', '', true)) + userService, + 'getUserInfoAsync' + ).and.returnValue( + Promise.resolve( + new UserInfo([], false, false, true, false, false, '', '', '', true) + ) ); const navigateSpy = spyOn(router, 'navigate').and.callThrough(); - guard.canActivate( - new ActivatedRouteSnapshot(), {} as RouterStateSnapshot).then( - (canActivate) => { + guard + .canActivate(new ActivatedRouteSnapshot(), {} as RouterStateSnapshot) + .then(canActivate => { expect(canActivate).toBeTrue(); expect(getUserInfoAsyncSpy).toHaveBeenCalledTimes(1); expect(navigateSpy).not.toHaveBeenCalled(); diff --git a/core/templates/pages/email-dashboard-pages/email-dashboard-auth.guard.ts b/core/templates/pages/email-dashboard-pages/email-dashboard-auth.guard.ts index 5ffc8d8ad33a..51c3a7a22bc0 100644 --- a/core/templates/pages/email-dashboard-pages/email-dashboard-auth.guard.ts +++ b/core/templates/pages/email-dashboard-pages/email-dashboard-auth.guard.ts @@ -17,8 +17,8 @@ * if the user is not a super admin. */ -import { Location } from '@angular/common'; -import { Injectable } from '@angular/core'; +import {Location} from '@angular/common'; +import {Injectable} from '@angular/core'; import { ActivatedRouteSnapshot, CanActivate, @@ -26,11 +26,11 @@ import { RouterStateSnapshot, } from '@angular/router'; -import { AppConstants } from 'app.constants'; -import { UserService } from 'services/user.service'; +import {AppConstants} from 'app.constants'; +import {UserService} from 'services/user.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class EmailDashboardAuthGuard implements CanActivate { constructor( @@ -40,19 +40,21 @@ export class EmailDashboardAuthGuard implements CanActivate { ) {} async canActivate( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot ): Promise { const userInfo = await this.userService.getUserInfoAsync(); if (userInfo.isSuperAdmin()) { return true; } - this.router.navigate( - [`${AppConstants.PAGES_REGISTERED_WITH_FRONTEND - .ERROR.ROUTE}/401`]).then(() => { - this.location.replaceState(state.url); - }); + this.router + .navigate([ + `${AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ERROR.ROUTE}/401`, + ]) + .then(() => { + this.location.replaceState(state.url); + }); return false; } } diff --git a/core/templates/pages/email-dashboard-pages/email-dashboard-data.service.spec.ts b/core/templates/pages/email-dashboard-pages/email-dashboard-data.service.spec.ts index 03fb30659cf3..0e02f7d6b27b 100644 --- a/core/templates/pages/email-dashboard-pages/email-dashboard-data.service.spec.ts +++ b/core/templates/pages/email-dashboard-pages/email-dashboard-data.service.spec.ts @@ -16,17 +16,19 @@ * @fileoverview Unit tests for the email dashboard page. */ -import { HttpClientTestingModule, HttpTestingController } from - '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; - -import { CsrfTokenService } from - 'services/csrf-token.service'; -import { EmailDashboardDataService } from - 'pages/email-dashboard-pages/email-dashboard-data.service'; -import { EmailDashboardQuery, EmailDashboardQueryDict } from - 'domain/email-dashboard/email-dashboard-query.model'; -import { QueryData } from 'domain/email-dashboard/email-dashboard-backend-api.service'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; + +import {CsrfTokenService} from 'services/csrf-token.service'; +import {EmailDashboardDataService} from 'pages/email-dashboard-pages/email-dashboard-data.service'; +import { + EmailDashboardQuery, + EmailDashboardQueryDict, +} from 'domain/email-dashboard/email-dashboard-query.model'; +import {QueryData} from 'domain/email-dashboard/email-dashboard-backend-api.service'; describe('Email Dashboard Services', () => { describe('Email Dashboard Services', () => { @@ -39,307 +41,310 @@ describe('Email Dashboard Services', () => { created_at_least_n_exps: false, created_fewer_than_n_exps: false, edited_at_least_n_exps: false, - edited_fewer_than_n_exps: false + edited_fewer_than_n_exps: false, }; beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [EmailDashboardDataService] + providers: [EmailDashboardDataService], }); csrfService = TestBed.inject(CsrfTokenService); emailDashboardDataService = TestBed.inject(EmailDashboardDataService); httpTestingController = TestBed.inject(HttpTestingController); - spyOn(csrfService, 'getTokenAsync').and.callFake(async() => { - return new Promise((resolve) => { + spyOn(csrfService, 'getTokenAsync').and.callFake(async () => { + return new Promise(resolve => { resolve('sample-csrf-token'); }); }); }); - it('should fetch correct data from backend', - fakeAsync(() => { - var recentQueriesDict: EmailDashboardQueryDict[] = [ - { - id: 'q123', - status: 'processing', - num_qualified_users: 0, - submitter_username: '', - created_on: '' - } - , - { - id: 'q456', - status: 'processing', - num_qualified_users: 0, - submitter_username: '', - created_on: '' - }]; - var recentQueries = recentQueriesDict.map( - EmailDashboardQuery.createFromQueryDict); - - emailDashboardDataService.getNextQueriesAsync(); - - var req = httpTestingController.expectOne( - req => (/.*?emaildashboarddatahandler?.*/g).test(req.url)); - expect(req.request.method).toEqual('GET'); - req.flush({ - recent_queries: recentQueriesDict, - cursor: null - }); - - flushMicrotasks(); - - expect(emailDashboardDataService.getQueries().length).toEqual(2); - expect(emailDashboardDataService.getQueries()).toEqual(recentQueries); - expect(emailDashboardDataService.getCurrentPageIndex()).toEqual(0); - expect(emailDashboardDataService.getLatestCursor()).toBe(null); - }) - ); - - it('should post correct data to backend', - fakeAsync(() => { - var data = defaultData; - data.inactive_in_last_n_days = 10; - var queryDataDict = { - id: 'qnew', + it('should fetch correct data from backend', fakeAsync(() => { + var recentQueriesDict: EmailDashboardQueryDict[] = [ + { + id: 'q123', status: 'processing', num_qualified_users: 0, submitter_username: '', - created_on: '' - }; - var queryData = EmailDashboardQuery.createFromQueryDict( - queryDataDict); - var expectedQueries = [queryData]; - - emailDashboardDataService.submitQueryAsync(data); + created_on: '', + }, + { + id: 'q456', + status: 'processing', + num_qualified_users: 0, + submitter_username: '', + created_on: '', + }, + ]; + var recentQueries = recentQueriesDict.map( + EmailDashboardQuery.createFromQueryDict + ); + + emailDashboardDataService.getNextQueriesAsync(); + + var req = httpTestingController.expectOne(req => + /.*?emaildashboarddatahandler?.*/g.test(req.url) + ); + expect(req.request.method).toEqual('GET'); + req.flush({ + recent_queries: recentQueriesDict, + cursor: null, + }); - var req = httpTestingController.expectOne('/emaildashboarddatahandler'); - expect(req.request.method).toEqual('POST'); - req.flush({ - query: queryDataDict - }); + flushMicrotasks(); + + expect(emailDashboardDataService.getQueries().length).toEqual(2); + expect(emailDashboardDataService.getQueries()).toEqual(recentQueries); + expect(emailDashboardDataService.getCurrentPageIndex()).toEqual(0); + expect(emailDashboardDataService.getLatestCursor()).toBe(null); + })); + + it('should post correct data to backend', fakeAsync(() => { + var data = defaultData; + data.inactive_in_last_n_days = 10; + var queryDataDict = { + id: 'qnew', + status: 'processing', + num_qualified_users: 0, + submitter_username: '', + created_on: '', + }; + var queryData = EmailDashboardQuery.createFromQueryDict(queryDataDict); + var expectedQueries = [queryData]; + + emailDashboardDataService.submitQueryAsync(data); + + var req = httpTestingController.expectOne('/emaildashboarddatahandler'); + expect(req.request.method).toEqual('POST'); + req.flush({ + query: queryDataDict, + }); - flushMicrotasks(); + flushMicrotasks(); - expect(emailDashboardDataService.getQueries().length).toEqual(1); - expect(emailDashboardDataService.getQueries()).toEqual(expectedQueries); - }) - ); + expect(emailDashboardDataService.getQueries().length).toEqual(1); + expect(emailDashboardDataService.getQueries()).toEqual(expectedQueries); + })); - it('should replace correct query in queries list', - fakeAsync(() => { - var recentQueries = [{ + it('should replace correct query in queries list', fakeAsync(() => { + var recentQueries = [ + { id: 'q123', status: 'processing', num_qualified_users: 0, submitter_username: '', - created_on: '' + created_on: '', }, { id: 'q456', status: 'processing', num_qualified_users: 0, submitter_username: '', - created_on: '' - }].map(EmailDashboardQuery.createFromQueryDict); + created_on: '', + }, + ].map(EmailDashboardQuery.createFromQueryDict); - var expectedQueries = [{ + var expectedQueries = [ + { id: 'q123', status: 'completed', num_qualified_users: 0, submitter_username: '', - created_on: '' + created_on: '', }, { id: 'q456', status: 'processing', num_qualified_users: 0, submitter_username: '', - created_on: '' - }].map(EmailDashboardQuery.createFromQueryDict); + created_on: '', + }, + ].map(EmailDashboardQuery.createFromQueryDict); - emailDashboardDataService.getNextQueriesAsync(); + emailDashboardDataService.getNextQueriesAsync(); - var req = httpTestingController.expectOne( - req => (/.*?emaildashboarddatahandler?.*/g).test(req.url)); - expect(req.request.method).toEqual('GET'); - req.flush({ - recent_queries: [{ + var req = httpTestingController.expectOne(req => + /.*?emaildashboarddatahandler?.*/g.test(req.url) + ); + expect(req.request.method).toEqual('GET'); + req.flush({ + recent_queries: [ + { id: 'q123', status: 'processing', num_qualified_users: 0, submitter_username: '', - created_on: '' + created_on: '', }, { id: 'q456', status: 'processing', num_qualified_users: 0, submitter_username: '', - created_on: '' - }], - cursor: null - }); + created_on: '', + }, + ], + cursor: null, + }); - flushMicrotasks(); + flushMicrotasks(); - expect(emailDashboardDataService.getQueries().length).toEqual(2); - expect(emailDashboardDataService.getQueries()).toEqual(recentQueries); + expect(emailDashboardDataService.getQueries().length).toEqual(2); + expect(emailDashboardDataService.getQueries()).toEqual(recentQueries); - emailDashboardDataService.fetchQueryAsync('q123').then((query) => { - expect(query.id).toEqual('q123'); - expect(query.status).toEqual('completed'); - }); + emailDashboardDataService.fetchQueryAsync('q123').then(query => { + expect(query.id).toEqual('q123'); + expect(query.status).toEqual('completed'); + }); - var req = httpTestingController.expectOne( - req => (/.*?querystatuscheck?.*/g).test(req.url) - ); - expect(req.request.method).toEqual('GET'); - req.flush({ - query: { - id: 'q123', - status: 'completed', - num_qualified_users: 0, - submitter_username: '', - created_on: '' - } - }); + var req = httpTestingController.expectOne(req => + /.*?querystatuscheck?.*/g.test(req.url) + ); + expect(req.request.method).toEqual('GET'); + req.flush({ + query: { + id: 'q123', + status: 'completed', + num_qualified_users: 0, + submitter_username: '', + created_on: '', + }, + }); - flushMicrotasks(); + flushMicrotasks(); - expect(emailDashboardDataService.getQueries().length).toEqual(2); - expect(emailDashboardDataService.getQueries()).toEqual(expectedQueries); - }) - ); + expect(emailDashboardDataService.getQueries().length).toEqual(2); + expect(emailDashboardDataService.getQueries()).toEqual(expectedQueries); + })); - it('should check simulation', - fakeAsync(() => { - // Get next page of queries. - emailDashboardDataService.getNextQueriesAsync(); + it('should check simulation', fakeAsync(() => { + // Get next page of queries. + emailDashboardDataService.getNextQueriesAsync(); - var req = httpTestingController.expectOne( - req => (/.*?emaildashboarddatahandler?.*/g).test(req.url)); - expect(req.request.method).toEqual('GET'); - req.flush({ - recent_queries: [], - cursor: null - }); + var req = httpTestingController.expectOne(req => + /.*?emaildashboarddatahandler?.*/g.test(req.url) + ); + expect(req.request.method).toEqual('GET'); + req.flush({ + recent_queries: [], + cursor: null, + }); - flushMicrotasks(); + flushMicrotasks(); - expect(emailDashboardDataService.getQueries().length).toEqual(0); - expect(emailDashboardDataService.getQueries()).toEqual([]); - expect(emailDashboardDataService.getCurrentPageIndex()).toEqual(0); - - var data = defaultData; - data.inactive_in_last_n_days = 10; - // Maintain list of all submitted queries for cross checking. - var totalQueries = []; - // Submit 25 new queries. - for (var i = 0; i < 25; i++) { - var queryData = { - id: 'q' + i, - status: 'processing', - num_qualified_users: 0, - submitter_username: '', - created_on: '' - }; - - emailDashboardDataService.submitQueryAsync(data); - totalQueries.unshift(queryData); - - var req = httpTestingController.expectOne( - '/emaildashboarddatahandler'); - expect(req.request.method).toEqual('POST'); - req.flush({ - query: queryData - }); - - flushMicrotasks(); - } - - let totalQueriesResponse = totalQueries.map( - EmailDashboardQuery.createFromQueryDict); - expect(emailDashboardDataService.getQueries().length).toEqual(25); - expect(emailDashboardDataService.getCurrentPageIndex()).toEqual(0); - expect(emailDashboardDataService.getQueries()).toEqual( - totalQueriesResponse); - - // Check that queries on page 1 are correct. - emailDashboardDataService.getNextQueriesAsync().then( - (queries) => { - expect(queries.length).toEqual(10); - expect(queries).toEqual(totalQueriesResponse.slice(10, 20)); - }); - expect(emailDashboardDataService.getCurrentPageIndex()).toEqual(1); - - // Check that queries on page 2 are correct. - emailDashboardDataService.getNextQueriesAsync().then( - (queries) => { - expect(queries.length).toEqual(5); - expect(queries).toEqual(totalQueriesResponse.slice(20, 25)); - }); - expect(emailDashboardDataService.getCurrentPageIndex()).toEqual(2); - - // Go back to page 1 and check again. - expect(emailDashboardDataService.getPreviousQueries()).toEqual( - totalQueriesResponse.slice(10, 20)); - expect(emailDashboardDataService.getCurrentPageIndex()).toEqual(1); - - // Submit a new query. + expect(emailDashboardDataService.getQueries().length).toEqual(0); + expect(emailDashboardDataService.getQueries()).toEqual([]); + expect(emailDashboardDataService.getCurrentPageIndex()).toEqual(0); + + var data = defaultData; + data.inactive_in_last_n_days = 10; + // Maintain list of all submitted queries for cross checking. + var totalQueries = []; + // Submit 25 new queries. + for (var i = 0; i < 25; i++) { var queryData = { - id: 'q25', + id: 'q' + i, status: 'processing', num_qualified_users: 0, submitter_username: '', - created_on: '' + created_on: '', }; emailDashboardDataService.submitQueryAsync(data); + totalQueries.unshift(queryData); - var req = httpTestingController.expectOne( - '/emaildashboarddatahandler'); + var req = httpTestingController.expectOne('/emaildashboarddatahandler'); expect(req.request.method).toEqual('POST'); req.flush({ - query: queryData + query: queryData, }); flushMicrotasks(); + } + + let totalQueriesResponse = totalQueries.map( + EmailDashboardQuery.createFromQueryDict + ); + expect(emailDashboardDataService.getQueries().length).toEqual(25); + expect(emailDashboardDataService.getCurrentPageIndex()).toEqual(0); + expect(emailDashboardDataService.getQueries()).toEqual( + totalQueriesResponse + ); + + // Check that queries on page 1 are correct. + emailDashboardDataService.getNextQueriesAsync().then(queries => { + expect(queries.length).toEqual(10); + expect(queries).toEqual(totalQueriesResponse.slice(10, 20)); + }); + expect(emailDashboardDataService.getCurrentPageIndex()).toEqual(1); - totalQueries.unshift(queryData); - let queryDataResponse = ( - EmailDashboardQuery.createFromQueryDict(queryData)); - totalQueriesResponse.unshift(queryDataResponse); - - expect(emailDashboardDataService.getQueries().length).toEqual(26); - expect(emailDashboardDataService.getQueries()).toEqual( - totalQueriesResponse); - - // Check that new query is added on the top of fetched queries. - expect(emailDashboardDataService.getQueries()[0]).toEqual( - queryDataResponse); - - // Check queries on page 2. - emailDashboardDataService.getNextQueriesAsync().then( - (queries) => { - expect(queries.length).toEqual(6); - expect(queries).toEqual(totalQueriesResponse.slice(20, 26)); - }); - expect(emailDashboardDataService.getCurrentPageIndex()).toEqual(2); - - // Check queries on page 1. - expect(emailDashboardDataService.getPreviousQueries()).toEqual( - totalQueriesResponse.slice(10, 20)); - expect(emailDashboardDataService.getCurrentPageIndex()).toEqual(1); - - // Check queries on page 0. - expect(emailDashboardDataService.getPreviousQueries()).toEqual( - totalQueriesResponse.slice(0, 10)); - expect(emailDashboardDataService.getCurrentPageIndex()).toEqual(0); - }) - ); + // Check that queries on page 2 are correct. + emailDashboardDataService.getNextQueriesAsync().then(queries => { + expect(queries.length).toEqual(5); + expect(queries).toEqual(totalQueriesResponse.slice(20, 25)); + }); + expect(emailDashboardDataService.getCurrentPageIndex()).toEqual(2); + + // Go back to page 1 and check again. + expect(emailDashboardDataService.getPreviousQueries()).toEqual( + totalQueriesResponse.slice(10, 20) + ); + expect(emailDashboardDataService.getCurrentPageIndex()).toEqual(1); + + // Submit a new query. + var queryData = { + id: 'q25', + status: 'processing', + num_qualified_users: 0, + submitter_username: '', + created_on: '', + }; + + emailDashboardDataService.submitQueryAsync(data); + + var req = httpTestingController.expectOne('/emaildashboarddatahandler'); + expect(req.request.method).toEqual('POST'); + req.flush({ + query: queryData, + }); + + flushMicrotasks(); + + totalQueries.unshift(queryData); + let queryDataResponse = + EmailDashboardQuery.createFromQueryDict(queryData); + totalQueriesResponse.unshift(queryDataResponse); + + expect(emailDashboardDataService.getQueries().length).toEqual(26); + expect(emailDashboardDataService.getQueries()).toEqual( + totalQueriesResponse + ); + + // Check that new query is added on the top of fetched queries. + expect(emailDashboardDataService.getQueries()[0]).toEqual( + queryDataResponse + ); + + // Check queries on page 2. + emailDashboardDataService.getNextQueriesAsync().then(queries => { + expect(queries.length).toEqual(6); + expect(queries).toEqual(totalQueriesResponse.slice(20, 26)); + }); + expect(emailDashboardDataService.getCurrentPageIndex()).toEqual(2); + + // Check queries on page 1. + expect(emailDashboardDataService.getPreviousQueries()).toEqual( + totalQueriesResponse.slice(10, 20) + ); + expect(emailDashboardDataService.getCurrentPageIndex()).toEqual(1); + + // Check queries on page 0. + expect(emailDashboardDataService.getPreviousQueries()).toEqual( + totalQueriesResponse.slice(0, 10) + ); + expect(emailDashboardDataService.getCurrentPageIndex()).toEqual(0); + })); it('should return true if next page is available', () => { // This will return true if the number of queries diff --git a/core/templates/pages/email-dashboard-pages/email-dashboard-data.service.ts b/core/templates/pages/email-dashboard-pages/email-dashboard-data.service.ts index 520905a86047..8c4078ca6cb4 100644 --- a/core/templates/pages/email-dashboard-pages/email-dashboard-data.service.ts +++ b/core/templates/pages/email-dashboard-pages/email-dashboard-data.service.ts @@ -16,16 +16,17 @@ * @fileoverview Services for oppia email dashboard page. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { QueryData, EmailDashboardBackendApiService } from - 'domain/email-dashboard/email-dashboard-backend-api.service'; -import { EmailDashboardQuery } from - 'domain/email-dashboard/email-dashboard-query.model'; +import { + QueryData, + EmailDashboardBackendApiService, +} from 'domain/email-dashboard/email-dashboard-backend-api.service'; +import {EmailDashboardQuery} from 'domain/email-dashboard/email-dashboard-query.model'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class EmailDashboardDataService { // No. of query results to display on a single page. @@ -60,32 +61,36 @@ export class EmailDashboardDataService { var startQueryIndex = this.currentPageIndex * this.QUERIES_PER_PAGE; var endQueryIndex = (this.currentPageIndex + 1) * this.QUERIES_PER_PAGE; - return this.emailDashboardBackendApiService.submitQueryAsync( - data).then(query => { - var newQueries = [query]; - this.queries = newQueries.concat(this.queries); - return this.queries.slice(startQueryIndex, endQueryIndex); - }); + return this.emailDashboardBackendApiService + .submitQueryAsync(data) + .then(query => { + var newQueries = [query]; + this.queries = newQueries.concat(this.queries); + return this.queries.slice(startQueryIndex, endQueryIndex); + }); } async getNextQueriesAsync(): Promise { var startQueryIndex = (this.currentPageIndex + 1) * this.QUERIES_PER_PAGE; var endQueryIndex = (this.currentPageIndex + 2) * this.QUERIES_PER_PAGE; - if (this.queries.length >= endQueryIndex || - (this.latestCursor === null && this.currentPageIndex !== -1)) { + if ( + this.queries.length >= endQueryIndex || + (this.latestCursor === null && this.currentPageIndex !== -1) + ) { this.currentPageIndex = this.currentPageIndex + 1; - return new Promise((resolver) => { + return new Promise(resolver => { resolver(this.queries.slice(startQueryIndex, endQueryIndex)); }); } else { this.currentPageIndex = this.currentPageIndex + 1; - return this.emailDashboardBackendApiService.fetchQueriesPageAsync( - this.QUERIES_PER_PAGE, this.latestCursor).then(data => { - this.queries = this.queries.concat(data.recentQueries); - this.latestCursor = data.cursor; - return this.queries.slice(startQueryIndex, endQueryIndex); - }); + return this.emailDashboardBackendApiService + .fetchQueriesPageAsync(this.QUERIES_PER_PAGE, this.latestCursor) + .then(data => { + this.queries = this.queries.concat(data.recentQueries); + this.latestCursor = data.cursor; + return this.queries.slice(startQueryIndex, endQueryIndex); + }); } } @@ -98,17 +103,18 @@ export class EmailDashboardDataService { isNextPageAvailable(): boolean { var nextQueryIndex = (this.currentPageIndex + 1) * this.QUERIES_PER_PAGE; - return (this.queries.length > nextQueryIndex) || Boolean(this.latestCursor); + return this.queries.length > nextQueryIndex || Boolean(this.latestCursor); } isPreviousPageAvailable(): boolean { - return (this.currentPageIndex > 0); + return this.currentPageIndex > 0; } async fetchQueryAsync(queryId: string): Promise { - return this.emailDashboardBackendApiService.fetchQueryAsync(queryId) + return this.emailDashboardBackendApiService + .fetchQueryAsync(queryId) .then(newQuery => { - this.queries.forEach(function(query, index, queries) { + this.queries.forEach(function (query, index, queries) { if (query.id === queryId) { queries[index] = newQuery; } @@ -118,6 +124,9 @@ export class EmailDashboardDataService { } } -angular.module('oppia').factory( - 'EmailDashboardDataService', - downgradeInjectable(EmailDashboardDataService)); +angular + .module('oppia') + .factory( + 'EmailDashboardDataService', + downgradeInjectable(EmailDashboardDataService) + ); diff --git a/core/templates/pages/email-dashboard-pages/email-dashboard-page-root.component.spec.ts b/core/templates/pages/email-dashboard-pages/email-dashboard-page-root.component.spec.ts index 3a467b88920a..7eea301475c7 100644 --- a/core/templates/pages/email-dashboard-pages/email-dashboard-page-root.component.spec.ts +++ b/core/templates/pages/email-dashboard-pages/email-dashboard-page-root.component.spec.ts @@ -16,14 +16,14 @@ * @fileoverview Unit tests for Email Dashboard Page Root component. */ -import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { TranslateModule } from '@ngx-translate/core'; +import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {TranslateModule} from '@ngx-translate/core'; -import { AppConstants } from '../../app.constants'; -import { PageHeadService } from '../../services/page-head.service'; -import { EmailDashboardPageRootComponent } from './email-dashboard-page-root.component'; +import {AppConstants} from '../../app.constants'; +import {PageHeadService} from '../../services/page-head.service'; +import {EmailDashboardPageRootComponent} from './email-dashboard-page-root.component'; describe('EmailDashboardPageRootComponent', () => { let fixture: ComponentFixture; @@ -43,8 +43,10 @@ describe('EmailDashboardPageRootComponent', () => { it('should have the title and meta tags set', () => { expect(component.title).toEqual( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.EMAIL_DASHBOARD.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.EMAIL_DASHBOARD.TITLE + ); expect(component.meta).toEqual( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.EMAIL_DASHBOARD.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.EMAIL_DASHBOARD.META + ); }); }); diff --git a/core/templates/pages/email-dashboard-pages/email-dashboard-page-root.component.ts b/core/templates/pages/email-dashboard-pages/email-dashboard-page-root.component.ts index 9302f578f1ba..09853d30476b 100644 --- a/core/templates/pages/email-dashboard-pages/email-dashboard-page-root.component.ts +++ b/core/templates/pages/email-dashboard-pages/email-dashboard-page-root.component.ts @@ -16,9 +16,9 @@ * @fileoverview Email Dashboard page root component. */ -import { Component } from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { BaseRootComponent, MetaTagData } from 'pages/base-root.component'; +import {Component} from '@angular/core'; +import {AppConstants} from 'app.constants'; +import {BaseRootComponent, MetaTagData} from 'pages/base-root.component'; @Component({ selector: 'oppia-email-dashboard-page-root', @@ -28,7 +28,6 @@ export class EmailDashboardPageRootComponent extends BaseRootComponent { title: string = AppConstants.PAGES_REGISTERED_WITH_FRONTEND.EMAIL_DASHBOARD.TITLE; - meta: MetaTagData[] = - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.EMAIL_DASHBOARD.META as - unknown as Readonly[]; + meta: MetaTagData[] = AppConstants.PAGES_REGISTERED_WITH_FRONTEND + .EMAIL_DASHBOARD.META as unknown as Readonly[]; } diff --git a/core/templates/pages/email-dashboard-pages/email-dashboard-page.component.spec.ts b/core/templates/pages/email-dashboard-pages/email-dashboard-page.component.spec.ts index e428ec562b60..888254543709 100644 --- a/core/templates/pages/email-dashboard-pages/email-dashboard-page.component.spec.ts +++ b/core/templates/pages/email-dashboard-pages/email-dashboard-page.component.spec.ts @@ -16,16 +16,22 @@ * @fileoverview Unit tests for the email dashboard page. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ChangeDetectorRef, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { AppConstants } from 'app.constants'; -import { EmailDashboardQuery } from 'domain/email-dashboard/email-dashboard-query.model'; -import { UserInfo } from 'domain/user/user-info.model'; -import { LoaderService } from 'services/loader.service'; -import { UserService } from 'services/user.service'; -import { EmailDashboardDataService } from './email-dashboard-data.service'; -import { EmailDashboardPageComponent } from './email-dashboard-page.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ChangeDetectorRef, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {AppConstants} from 'app.constants'; +import {EmailDashboardQuery} from 'domain/email-dashboard/email-dashboard-query.model'; +import {UserInfo} from 'domain/user/user-info.model'; +import {LoaderService} from 'services/loader.service'; +import {UserService} from 'services/user.service'; +import {EmailDashboardDataService} from './email-dashboard-data.service'; +import {EmailDashboardPageComponent} from './email-dashboard-page.component'; describe('Email Dashboard Page Component', () => { let fixture: ComponentFixture; @@ -40,22 +46,18 @@ describe('Email Dashboard Page Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - EmailDashboardPageComponent - ], + imports: [HttpClientTestingModule], + declarations: [EmailDashboardPageComponent], providers: [ { provide: ChangeDetectorRef, - useClass: MockChangeDetectorRef + useClass: MockChangeDetectorRef, }, EmailDashboardDataService, LoaderService, - UserService + UserService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -78,13 +80,25 @@ describe('Email Dashboard Page Component', () => { let username = 'user'; let userInfo = new UserInfo( - [], true, true, true, true, true, '', username, '', true); + [], + true, + true, + true, + true, + true, + '', + username, + '', + true + ); let queries: EmailDashboardQuery[] = []; spyOn(userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(userInfo)); + Promise.resolve(userInfo) + ); spyOn(emailDashboardDataService, 'getNextQueriesAsync').and.returnValue( - Promise.resolve(queries)); + Promise.resolve(queries) + ); componentInstance.ngOnInit(); tick(); expect(componentInstance.username).toEqual(username); @@ -117,7 +131,8 @@ describe('Email Dashboard Page Component', () => { it('should submit query', fakeAsync(() => { let queries: EmailDashboardQuery[] = []; spyOn(emailDashboardDataService, 'submitQueryAsync').and.returnValue( - Promise.resolve(queries)); + Promise.resolve(queries) + ); spyOn(componentInstance, 'resetForm'); componentInstance.submitQueryAsync(); tick(); @@ -128,9 +143,11 @@ describe('Email Dashboard Page Component', () => { it('should get next page of queries', fakeAsync(() => { let queries: EmailDashboardQuery[] = []; spyOn(emailDashboardDataService, 'isNextPageAvailable').and.returnValue( - true); + true + ); spyOn(emailDashboardDataService, 'getNextQueriesAsync').and.returnValue( - Promise.resolve(queries)); + Promise.resolve(queries) + ); componentInstance.getNextPageOfQueries(); tick(); expect(componentInstance.currentPageOfQueries).toEqual(queries); @@ -138,34 +155,40 @@ describe('Email Dashboard Page Component', () => { it('should get previous page of queries', () => { spyOn(emailDashboardDataService, 'isPreviousPageAvailable').and.returnValue( - true); + true + ); spyOn(emailDashboardDataService, 'getPreviousQueries').and.returnValue([]); componentInstance.getPreviousPageOfQueries(); - expect(emailDashboardDataService.isPreviousPageAvailable) - .toHaveBeenCalled(); + expect( + emailDashboardDataService.isPreviousPageAvailable + ).toHaveBeenCalled(); expect(componentInstance.currentPageOfQueries).toEqual([]); }); it('should show next button', () => { let nextPageIsAvailable = true; spyOn(emailDashboardDataService, 'isNextPageAvailable').and.returnValue( - nextPageIsAvailable); + nextPageIsAvailable + ); expect(componentInstance.showNextButton()).toEqual(nextPageIsAvailable); }); it('should show previous button', () => { let previousPageIsAvailable = true; spyOn(emailDashboardDataService, 'isPreviousPageAvailable').and.returnValue( - previousPageIsAvailable); + previousPageIsAvailable + ); expect(componentInstance.showPreviousButton()).toEqual( - previousPageIsAvailable); + previousPageIsAvailable + ); }); it('should recheck status', fakeAsync(() => { let query = new EmailDashboardQuery('', '', 0, '', ''); componentInstance.currentPageOfQueries = [query]; spyOn(emailDashboardDataService, 'fetchQueryAsync').and.returnValue( - Promise.resolve(query)); + Promise.resolve(query) + ); componentInstance.recheckStatus(0); tick(); expect(componentInstance.currentPageOfQueries[0]).toEqual(query); @@ -174,7 +197,8 @@ describe('Email Dashboard Page Component', () => { it('should link to result page', () => { let submitter = 'submitter1'; componentInstance.username = submitter; - expect(componentInstance.showLinkToResultPage( - submitter, 'completed')).toBeTrue(); + expect( + componentInstance.showLinkToResultPage(submitter, 'completed') + ).toBeTrue(); }); }); diff --git a/core/templates/pages/email-dashboard-pages/email-dashboard-page.component.ts b/core/templates/pages/email-dashboard-pages/email-dashboard-page.component.ts index a84b08014964..a8221d1ad8b0 100644 --- a/core/templates/pages/email-dashboard-pages/email-dashboard-page.component.ts +++ b/core/templates/pages/email-dashboard-pages/email-dashboard-page.component.ts @@ -16,17 +16,17 @@ * @fileoverview Component for oppia email dashboard page. */ -import { ChangeDetectorRef, Component } from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { QueryData } from 'domain/email-dashboard/email-dashboard-backend-api.service'; -import { EmailDashboardQuery } from 'domain/email-dashboard/email-dashboard-query.model'; -import { LoaderService } from 'services/loader.service'; -import { UserService } from 'services/user.service'; -import { EmailDashboardDataService } from './email-dashboard-data.service'; +import {ChangeDetectorRef, Component} from '@angular/core'; +import {AppConstants} from 'app.constants'; +import {QueryData} from 'domain/email-dashboard/email-dashboard-backend-api.service'; +import {EmailDashboardQuery} from 'domain/email-dashboard/email-dashboard-query.model'; +import {LoaderService} from 'services/loader.service'; +import {UserService} from 'services/user.service'; +import {EmailDashboardDataService} from './email-dashboard-data.service'; @Component({ selector: 'oppia-email-dashboard-page', - templateUrl: './email-dashboard-page.component.html' + templateUrl: './email-dashboard-page.component.html', }) export class EmailDashboardPageComponent { data!: QueryData; @@ -40,7 +40,7 @@ export class EmailDashboardPageComponent { private changeDetectorRef: ChangeDetectorRef, private emailDashboardDataService: EmailDashboardDataService, private loaderService: LoaderService, - private userService: UserService, + private userService: UserService ) {} ngOnInit(): void { @@ -50,12 +50,12 @@ export class EmailDashboardPageComponent { this.loaderService.showLoadingScreen('Loading'); this.resetForm(); - this.userService.getUserInfoAsync().then((userInfo) => { + this.userService.getUserInfoAsync().then(userInfo => { this.username = userInfo.getUsername(); this.loaderService.hideLoadingScreen(); }); - this.emailDashboardDataService.getNextQueriesAsync().then((queries) => { + this.emailDashboardDataService.getNextQueriesAsync().then(queries => { this.currentPageOfQueries = queries; }); } @@ -76,32 +76,30 @@ export class EmailDashboardPageComponent { areAllInputsEmpty(): boolean { return Object.values(this.data).every( - value => value === null || value === false); + value => value === null || value === false + ); } submitQueryAsync(): void { - this.emailDashboardDataService.submitQueryAsync(this.data) - .then((queries) => { - this.currentPageOfQueries = queries; - }); + this.emailDashboardDataService.submitQueryAsync(this.data).then(queries => { + this.currentPageOfQueries = queries; + }); this.resetForm(); this.showSuccessMessage = true; } getNextPageOfQueries(): void { if (this.emailDashboardDataService.isNextPageAvailable()) { - this.emailDashboardDataService.getNextQueriesAsync().then( - (queries) => { - this.currentPageOfQueries = queries; - } - ); + this.emailDashboardDataService.getNextQueriesAsync().then(queries => { + this.currentPageOfQueries = queries; + }); } } getPreviousPageOfQueries(): void { if (this.emailDashboardDataService.isPreviousPageAvailable()) { - this.currentPageOfQueries = ( - this.emailDashboardDataService.getPreviousQueries()); + this.currentPageOfQueries = + this.emailDashboardDataService.getPreviousQueries(); } } @@ -113,20 +111,20 @@ export class EmailDashboardPageComponent { return this.emailDashboardDataService.isPreviousPageAvailable(); } - recheckStatus(index: number): void { - let query = this.currentPageOfQueries !== undefined ? - this.currentPageOfQueries[index] : null; + let query = + this.currentPageOfQueries !== undefined + ? this.currentPageOfQueries[index] + : null; if (query) { let queryId = query.id; - this.emailDashboardDataService.fetchQueryAsync(queryId).then( - (query) => { - this.currentPageOfQueries[index] = query; - }); + this.emailDashboardDataService.fetchQueryAsync(queryId).then(query => { + this.currentPageOfQueries[index] = query; + }); } } showLinkToResultPage(submitter: string, status: string): boolean { - return (submitter === this.username) && (status === 'completed'); + return submitter === this.username && status === 'completed'; } } diff --git a/core/templates/pages/email-dashboard-pages/email-dashboard-page.import.ts b/core/templates/pages/email-dashboard-pages/email-dashboard-page.import.ts index 4fb0eb5061f8..396cb17f4a96 100644 --- a/core/templates/pages/email-dashboard-pages/email-dashboard-page.import.ts +++ b/core/templates/pages/email-dashboard-pages/email-dashboard-page.import.ts @@ -20,8 +20,13 @@ import 'core-js/es7/reflect'; import 'zone.js'; angular.module('oppia', [ - require('angular-cookies'), 'ngAnimate', 'ngMaterial', - 'ngSanitize', 'ngTouch', 'pascalprecht.translate', 'ui.bootstrap' + require('angular-cookies'), + 'ngAnimate', + 'ngMaterial', + 'ngSanitize', + 'ngTouch', + 'pascalprecht.translate', + 'ui.bootstrap', ]); require('Polyfills.ts'); diff --git a/core/templates/pages/email-dashboard-pages/email-dashboard-page.module.ts b/core/templates/pages/email-dashboard-pages/email-dashboard-page.module.ts index ad2414e59e71..9655f1d15d0a 100644 --- a/core/templates/pages/email-dashboard-pages/email-dashboard-page.module.ts +++ b/core/templates/pages/email-dashboard-pages/email-dashboard-page.module.ts @@ -17,14 +17,14 @@ */ import {NgModule} from '@angular/core'; -import { RouterModule } from '@angular/router'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { EmailDashboardPageComponent } from './email-dashboard-page.component'; -import { ToastrModule } from 'ngx-toastr'; -import { toastrConfig } from 'pages/oppia-root/app.module'; -import { EmailDashboardAuthGuard } from './email-dashboard-auth.guard'; -import { EmailDashboardPageRootComponent } from './email-dashboard-page-root.component'; -import { CommonModule } from '@angular/common'; +import {RouterModule} from '@angular/router'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {EmailDashboardPageComponent} from './email-dashboard-page.component'; +import {ToastrModule} from 'ngx-toastr'; +import {toastrConfig} from 'pages/oppia-root/app.module'; +import {EmailDashboardAuthGuard} from './email-dashboard-auth.guard'; +import {EmailDashboardPageRootComponent} from './email-dashboard-page-root.component'; +import {CommonModule} from '@angular/common'; @NgModule({ imports: [ @@ -39,12 +39,7 @@ import { CommonModule } from '@angular/common'; }, ]), ], - declarations: [ - EmailDashboardPageRootComponent, - EmailDashboardPageComponent - ], - entryComponents: [ - EmailDashboardPageComponent - ], + declarations: [EmailDashboardPageRootComponent, EmailDashboardPageComponent], + entryComponents: [EmailDashboardPageComponent], }) export class EmailDashboardPageModule {} diff --git a/core/templates/pages/email-dashboard-pages/email-dashboard-result-backend-api.service.spec.ts b/core/templates/pages/email-dashboard-pages/email-dashboard-result-backend-api.service.spec.ts index ea3eb091fc5b..d25499287415 100644 --- a/core/templates/pages/email-dashboard-pages/email-dashboard-result-backend-api.service.spec.ts +++ b/core/templates/pages/email-dashboard-pages/email-dashboard-result-backend-api.service.spec.ts @@ -16,12 +16,20 @@ * @fileoverview Tests that the email dashboard result backend api service. */ -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { fakeAsync, flushMicrotasks, TestBed, waitForAsync } from '@angular/core/testing'; -import { EmailDashboardBackendApiService } from 'domain/email-dashboard/email-dashboard-backend-api.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { EmailDashboardResultBackendApiService } from './email-dashboard-result-backend-api.service'; -import { EmailData } from './email-dashboard-result.component'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import { + fakeAsync, + flushMicrotasks, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import {EmailDashboardBackendApiService} from 'domain/email-dashboard/email-dashboard-backend-api.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {EmailDashboardResultBackendApiService} from './email-dashboard-result-backend-api.service'; +import {EmailData} from './email-dashboard-result.component'; describe('Email dashboard result backend api service', () => { let edrbas: EmailDashboardResultBackendApiService; @@ -31,13 +39,8 @@ describe('Email dashboard result backend api service', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - providers: [ - EmailDashboardBackendApiService, - UrlInterpolationService - ] + imports: [HttpClientTestingModule], + providers: [EmailDashboardBackendApiService, UrlInterpolationService], }).compileComponents(); })); @@ -53,7 +56,7 @@ describe('Email dashboard result backend api service', () => { email_subject: '', email_body: '', email_intent: '', - max_recipients: 0 + max_recipients: 0, }; edrbas.submitEmailAsync(emailData, '').then(successSpy, failSpy); @@ -82,7 +85,8 @@ describe('Email dashboard result backend api service', () => { edrbas.sendTestEmailAsync('', '', '').then(successSpy, failSpy); const req = httpTestingController.expectOne( - '/emaildashboardtestbulkemailhandler/'); + '/emaildashboardtestbulkemailhandler/' + ); expect(req.request.method).toEqual('POST'); req.flush({}); diff --git a/core/templates/pages/email-dashboard-pages/email-dashboard-result-backend-api.service.ts b/core/templates/pages/email-dashboard-pages/email-dashboard-result-backend-api.service.ts index b7648f8bf156..6b5e8939bb55 100644 --- a/core/templates/pages/email-dashboard-pages/email-dashboard-result-backend-api.service.ts +++ b/core/templates/pages/email-dashboard-pages/email-dashboard-result-backend-api.service.ts @@ -16,13 +16,13 @@ * @fileoverview Backend api service for email dashboard page. */ -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { EmailData } from './email-dashboard-result.component'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {EmailData} from './email-dashboard-result.component'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class EmailDashboardResultBackendApiService { RESULT_HANDLER_URL = '/emaildashboardresult/'; @@ -36,33 +36,45 @@ export class EmailDashboardResultBackendApiService { async submitEmailAsync(data: EmailData, queryId: string): Promise { let resultHandlerUrl = this.urlInterpolationService.interpolateUrl( - this.RESULT_HANDLER_URL, { - query_id: queryId - }); - return this.http.post(resultHandlerUrl, { - data - }).toPromise(); + this.RESULT_HANDLER_URL, + { + query_id: queryId, + } + ); + return this.http + .post(resultHandlerUrl, { + data, + }) + .toPromise(); } async cancelEmailAsync(queryId: string): Promise { let cancelUrlHandler = this.urlInterpolationService.interpolateUrl( - this.CANCEL_EMAIL_HANDLER_URL, { - query_id: queryId - }); + this.CANCEL_EMAIL_HANDLER_URL, + { + query_id: queryId, + } + ); return this.http.post(cancelUrlHandler, {}).toPromise(); } async sendTestEmailAsync( - emailSubject: string, emailBody: string, queryId: string + emailSubject: string, + emailBody: string, + queryId: string ): Promise { let testEmailHandlerUrl = this.urlInterpolationService.interpolateUrl( - this.TEST_BULK_EMAIL_URL, { - query_id: queryId - }); + this.TEST_BULK_EMAIL_URL, + { + query_id: queryId, + } + ); - return this.http.post(testEmailHandlerUrl, { - email_subject: emailSubject, - email_body: emailBody - }).toPromise(); + return this.http + .post(testEmailHandlerUrl, { + email_subject: emailSubject, + email_body: emailBody, + }) + .toPromise(); } } diff --git a/core/templates/pages/email-dashboard-pages/email-dashboard-result-page-root.component.ts b/core/templates/pages/email-dashboard-pages/email-dashboard-result-page-root.component.ts index bf92cbcf2ebd..87b43f45f4af 100644 --- a/core/templates/pages/email-dashboard-pages/email-dashboard-result-page-root.component.ts +++ b/core/templates/pages/email-dashboard-pages/email-dashboard-result-page-root.component.ts @@ -16,10 +16,10 @@ * @fileoverview Root component for Email dashboard result Page. */ -import { Component } from '@angular/core'; +import {Component} from '@angular/core'; @Component({ selector: 'oppia-email-dashboard-result-page-root', - templateUrl: './email-dashboard-result-page-root.component.html' + templateUrl: './email-dashboard-result-page-root.component.html', }) export class EmailDashboardResultPageRootComponent {} diff --git a/core/templates/pages/email-dashboard-pages/email-dashboard-result.component.spec.ts b/core/templates/pages/email-dashboard-pages/email-dashboard-result.component.spec.ts index 0a0efc85f5d0..a7b87ee3da98 100644 --- a/core/templates/pages/email-dashboard-pages/email-dashboard-result.component.spec.ts +++ b/core/templates/pages/email-dashboard-pages/email-dashboard-result.component.spec.ts @@ -16,24 +16,29 @@ * @fileoverview Unit tests for emailDashboardResultPage. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { EmailDashboardResultBackendApiService } from './email-dashboard-result-backend-api.service'; -import { EmailDashboardResultComponent } from './email-dashboard-result.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {EmailDashboardResultBackendApiService} from './email-dashboard-result-backend-api.service'; +import {EmailDashboardResultComponent} from './email-dashboard-result.component'; describe('Email Dashboard Result Component', () => { let fixture: ComponentFixture; let componentInstance: EmailDashboardResultComponent; - let emailDashboardResultBackendApiService: - EmailDashboardResultBackendApiService; + let emailDashboardResultBackendApiService: EmailDashboardResultBackendApiService; class MockWindowRef { nativeWindow = { location: { - pathname: '' - } + pathname: '', + }, }; } @@ -41,20 +46,16 @@ describe('Email Dashboard Result Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - EmailDashboardResultComponent - ], + imports: [HttpClientTestingModule], + declarations: [EmailDashboardResultComponent], providers: [ EmailDashboardResultBackendApiService, { provide: WindowRef, - useValue: windowRef - } + useValue: windowRef, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -62,7 +63,8 @@ describe('Email Dashboard Result Component', () => { fixture = TestBed.createComponent(EmailDashboardResultComponent); componentInstance = fixture.componentInstance; emailDashboardResultBackendApiService = TestBed.inject( - EmailDashboardResultBackendApiService); + EmailDashboardResultBackendApiService + ); }); it('should create', () => { @@ -84,15 +86,19 @@ describe('Email Dashboard Result Component', () => { it('should submit email', fakeAsync(() => { spyOn(componentInstance, 'validateEmailSubjectAndBody').and.returnValue( - true); + true + ); componentInstance.emailOption = 'not_custom'; - spyOn(emailDashboardResultBackendApiService, 'submitEmailAsync') - .and.returnValue(Promise.resolve({})); + spyOn( + emailDashboardResultBackendApiService, + 'submitEmailAsync' + ).and.returnValue(Promise.resolve({})); componentInstance.submitEmail(); tick(); tick(4500); - expect(emailDashboardResultBackendApiService.submitEmailAsync) - .toHaveBeenCalled(); + expect( + emailDashboardResultBackendApiService.submitEmailAsync + ).toHaveBeenCalled(); expect(componentInstance.invalid.subject).toBeFalse(); expect(componentInstance.invalid.body).toBeFalse(); expect(componentInstance.invalid.maxRecipients).toBeFalse(); @@ -102,23 +108,26 @@ describe('Email Dashboard Result Component', () => { componentInstance.emailOption = 'custom'; componentInstance.maxRecipients = 0; spyOn(componentInstance, 'validateEmailSubjectAndBody').and.returnValue( - true); + true + ); componentInstance.submitEmail(); expect(componentInstance.invalid.maxRecipients).toBeTrue(); }); - it('should acknowledge error when request fails while submitting email', - fakeAsync(() => { - spyOn(componentInstance, 'validateEmailSubjectAndBody').and.returnValue( - true); - componentInstance.emailOption = 'not_custom'; - spyOn(emailDashboardResultBackendApiService, 'submitEmailAsync') - .and.returnValue(Promise.reject({})); - componentInstance.submitEmail(); - tick(); - expect(componentInstance.errorHasOccurred).toBeTrue(); - expect(componentInstance.submitIsInProgress).toBeFalse(); - })); + it('should acknowledge error when request fails while submitting email', fakeAsync(() => { + spyOn(componentInstance, 'validateEmailSubjectAndBody').and.returnValue( + true + ); + componentInstance.emailOption = 'not_custom'; + spyOn( + emailDashboardResultBackendApiService, + 'submitEmailAsync' + ).and.returnValue(Promise.reject({})); + componentInstance.submitEmail(); + tick(); + expect(componentInstance.errorHasOccurred).toBeTrue(); + expect(componentInstance.submitIsInProgress).toBeFalse(); + })); it('should reset form', () => { componentInstance.resetForm(); @@ -128,29 +137,35 @@ describe('Email Dashboard Result Component', () => { }); it('should cancel email', fakeAsync(() => { - spyOn(emailDashboardResultBackendApiService, 'cancelEmailAsync') - .and.returnValue(Promise.resolve({})); + spyOn( + emailDashboardResultBackendApiService, + 'cancelEmailAsync' + ).and.returnValue(Promise.resolve({})); componentInstance.cancelEmail(); tick(); tick(4500); expect(componentInstance.emailCancelled).toBeTrue(); })); - it('should handler error when http call to cancel email fails', - fakeAsync(() => { - spyOn(emailDashboardResultBackendApiService, 'cancelEmailAsync') - .and.returnValue(Promise.reject({})); - componentInstance.cancelEmail(); - tick(); - expect(componentInstance.errorHasOccurred).toBeTrue(); - expect(componentInstance.submitIsInProgress).toBeFalse(); - })); + it('should handler error when http call to cancel email fails', fakeAsync(() => { + spyOn( + emailDashboardResultBackendApiService, + 'cancelEmailAsync' + ).and.returnValue(Promise.reject({})); + componentInstance.cancelEmail(); + tick(); + expect(componentInstance.errorHasOccurred).toBeTrue(); + expect(componentInstance.submitIsInProgress).toBeFalse(); + })); it('should send test email', fakeAsync(() => { spyOn(componentInstance, 'validateEmailSubjectAndBody').and.returnValue( - true); - spyOn(emailDashboardResultBackendApiService, 'sendTestEmailAsync') - .and.returnValue(Promise.resolve({})); + true + ); + spyOn( + emailDashboardResultBackendApiService, + 'sendTestEmailAsync' + ).and.returnValue(Promise.resolve({})); componentInstance.sendTestEmail(); tick(); expect(componentInstance.testEmailSentSuccesfully).toBeTrue(); diff --git a/core/templates/pages/email-dashboard-pages/email-dashboard-result.component.ts b/core/templates/pages/email-dashboard-pages/email-dashboard-result.component.ts index cdb6c1dcfe9d..9fccceb0b276 100644 --- a/core/templates/pages/email-dashboard-pages/email-dashboard-result.component.ts +++ b/core/templates/pages/email-dashboard-pages/email-dashboard-result.component.ts @@ -16,21 +16,21 @@ * @fileoverview Component for oppia email dashboard page. */ -import { Component } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { EmailDashboardResultBackendApiService } from './email-dashboard-result-backend-api.service'; +import {Component} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {EmailDashboardResultBackendApiService} from './email-dashboard-result-backend-api.service'; export interface EmailData { - 'email_subject': string; - 'email_body': string; - 'email_intent': string; - 'max_recipients': number; + email_subject: string; + email_body: string; + email_intent: string; + max_recipients: number; } @Component({ selector: 'oppia-email-dashboard-result', - templateUrl: './email-dashboard-result.component.html' + templateUrl: './email-dashboard-result.component.html', }) export class EmailDashboardResultComponent { EMAIL_DASHBOARD_PAGE = '/emaildashboard'; @@ -38,7 +38,7 @@ export class EmailDashboardResultComponent { invalid = { subject: false, body: false, - maxRecipients: false + maxRecipients: false, }; emailSubject: string = ''; @@ -46,10 +46,12 @@ export class EmailDashboardResultComponent { emailOption: string = 'all'; submitIsInProgress: boolean = false; POSSIBLE_EMAIL_INTENTS = [ - 'bulk_email_marketing', 'bulk_email_improve_exploration', + 'bulk_email_marketing', + 'bulk_email_improve_exploration', 'bulk_email_create_exploration', 'bulk_email_creator_reengagement', - 'bulk_email_learner_reengagement']; + 'bulk_email_learner_reengagement', + ]; emailIntent = this.POSSIBLE_EMAIL_INTENTS[0]; emailSubmitted: boolean = false; @@ -58,14 +60,14 @@ export class EmailDashboardResultComponent { testEmailSentSuccesfully: boolean = false; constructor( - private emailDashboardResultBackendApiService: - EmailDashboardResultBackendApiService, - private windowRef: WindowRef, + private emailDashboardResultBackendApiService: EmailDashboardResultBackendApiService, + private windowRef: WindowRef ) {} getQueryId(): string { - return ( - this.windowRef.nativeWindow.location.pathname.split('/').slice(-1)[0]); + return this.windowRef.nativeWindow.location.pathname + .split('/') + .slice(-1)[0]; } validateEmailSubjectAndBody(): boolean { @@ -84,8 +86,7 @@ export class EmailDashboardResultComponent { submitEmail(): void { let dataIsValid = this.validateEmailSubjectAndBody(); - if (this.emailOption === 'custom' && - this.maxRecipients === 0) { + if (this.emailOption === 'custom' && this.maxRecipients === 0) { this.invalid.maxRecipients = true; dataIsValid = false; } @@ -96,20 +97,24 @@ export class EmailDashboardResultComponent { email_subject: this.emailSubject, email_body: this.emailBody, email_intent: this.emailIntent, - max_recipients: ( - this.emailOption !== 'all' ? this.maxRecipients : 0) + max_recipients: this.emailOption !== 'all' ? this.maxRecipients : 0, }; - this.emailDashboardResultBackendApiService.submitEmailAsync( - data, this.getQueryId()).then(() => { - this.emailSubmitted = true; - setTimeout(() => { - this.windowRef.nativeWindow.location.href = this.EMAIL_DASHBOARD_PAGE; - }, 4000); - }, () => { - this.errorHasOccurred = true; - this.submitIsInProgress = false; - }); + this.emailDashboardResultBackendApiService + .submitEmailAsync(data, this.getQueryId()) + .then( + () => { + this.emailSubmitted = true; + setTimeout(() => { + this.windowRef.nativeWindow.location.href = + this.EMAIL_DASHBOARD_PAGE; + }, 4000); + }, + () => { + this.errorHasOccurred = true; + this.submitIsInProgress = false; + } + ); this.invalid.subject = false; this.invalid.body = false; this.invalid.maxRecipients = false; @@ -125,26 +130,36 @@ export class EmailDashboardResultComponent { cancelEmail(): void { this.submitIsInProgress = true; - this.emailDashboardResultBackendApiService.cancelEmailAsync( - this.getQueryId()).then(() => { - this.emailCancelled = true; - setTimeout(() => { - this.windowRef.nativeWindow.location.href = this.EMAIL_DASHBOARD_PAGE; - }, 4000); - }, () => { - this.errorHasOccurred = true; - this.submitIsInProgress = false; - }); + this.emailDashboardResultBackendApiService + .cancelEmailAsync(this.getQueryId()) + .then( + () => { + this.emailCancelled = true; + setTimeout(() => { + this.windowRef.nativeWindow.location.href = + this.EMAIL_DASHBOARD_PAGE; + }, 4000); + }, + () => { + this.errorHasOccurred = true; + this.submitIsInProgress = false; + } + ); } sendTestEmail(): void { let dataIsValid = this.validateEmailSubjectAndBody(); if (dataIsValid) { - this.emailDashboardResultBackendApiService.sendTestEmailAsync( - this.emailSubject, this.emailBody, this.getQueryId()).then(() => { - this.testEmailSentSuccesfully = true; - }); + this.emailDashboardResultBackendApiService + .sendTestEmailAsync( + this.emailSubject, + this.emailBody, + this.getQueryId() + ) + .then(() => { + this.testEmailSentSuccesfully = true; + }); this.invalid.subject = false; this.invalid.body = false; this.invalid.maxRecipients = false; @@ -152,7 +167,9 @@ export class EmailDashboardResultComponent { } } -angular.module('oppia').directive('oppiaEmailDashboardResultPage', +angular.module('oppia').directive( + 'oppiaEmailDashboardResultPage', downgradeComponent({ - component: EmailDashboardResultComponent - })); + component: EmailDashboardResultComponent, + }) +); diff --git a/core/templates/pages/email-dashboard-pages/email-dashboard-result.import.ts b/core/templates/pages/email-dashboard-pages/email-dashboard-result.import.ts index 65e278083d68..5c3c5da3ab4f 100644 --- a/core/templates/pages/email-dashboard-pages/email-dashboard-result.import.ts +++ b/core/templates/pages/email-dashboard-pages/email-dashboard-result.import.ts @@ -22,12 +22,12 @@ import 'zone.js'; // TODO(#13080): Remove the mock-ajs.ts file after the migration is complete. import 'pages/mock-ajs'; import 'Polyfills.ts'; -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; -import { AppConstants } from 'app.constants'; -import { enableProdMode } from '@angular/core'; -import { LoggerService } from 'services/contextual/logger.service'; -import { EmailDashboardResultModule } from './email-dashboard-result.module'; +import {AppConstants} from 'app.constants'; +import {enableProdMode} from '@angular/core'; +import {LoggerService} from 'services/contextual/logger.service'; +import {EmailDashboardResultModule} from './email-dashboard-result.module'; if (!AppConstants.DEV_MODE) { enableProdMode(); @@ -35,9 +35,9 @@ if (!AppConstants.DEV_MODE) { const loggerService = new LoggerService(); -platformBrowserDynamic().bootstrapModule(EmailDashboardResultModule).catch( - (err) => loggerService.error(err) -); +platformBrowserDynamic() + .bootstrapModule(EmailDashboardResultModule) + .catch(err => loggerService.error(err)); // This prevents angular pages to cause side effects to hybrid pages. // TODO(#13080): Remove window.name statement from import.ts files diff --git a/core/templates/pages/email-dashboard-pages/email-dashboard-result.module.ts b/core/templates/pages/email-dashboard-pages/email-dashboard-result.module.ts index 96c3b80879e2..73b22160f6d3 100644 --- a/core/templates/pages/email-dashboard-pages/email-dashboard-result.module.ts +++ b/core/templates/pages/email-dashboard-pages/email-dashboard-result.module.ts @@ -16,22 +16,24 @@ * @fileoverview Module for the collection player page. */ -import { APP_INITIALIZER, NgModule } from '@angular/core'; -import { BrowserModule } from '@angular/platform-browser'; -import { APP_BASE_HREF } from '@angular/common'; -import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; -import { RouterModule } from '@angular/router'; +import {APP_INITIALIZER, NgModule} from '@angular/core'; +import {BrowserModule} from '@angular/platform-browser'; +import {APP_BASE_HREF} from '@angular/common'; +import {HttpClientModule, HTTP_INTERCEPTORS} from '@angular/common/http'; +import {RouterModule} from '@angular/router'; -import { RequestInterceptor } from 'services/request-interceptor.service'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { platformFeatureInitFactory, PlatformFeatureService } from - 'services/platform-feature.service'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { EmailDashboardResultComponent } from './email-dashboard-result.component'; -import { EmailDashboardResultPageRootComponent } from './email-dashboard-result-page-root.component'; -import { ToastrModule } from 'ngx-toastr'; -import { toastrConfig } from 'pages/oppia-root/app.module'; -import { SmartRouterModule } from 'hybrid-router-module-provider'; +import {RequestInterceptor} from 'services/request-interceptor.service'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import { + platformFeatureInitFactory, + PlatformFeatureService, +} from 'services/platform-feature.service'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {EmailDashboardResultComponent} from './email-dashboard-result.component'; +import {EmailDashboardResultPageRootComponent} from './email-dashboard-result-page-root.component'; +import {ToastrModule} from 'ngx-toastr'; +import {toastrConfig} from 'pages/oppia-root/app.module'; +import {SmartRouterModule} from 'hybrid-router-module-provider'; @NgModule({ imports: [ @@ -43,33 +45,33 @@ import { SmartRouterModule } from 'hybrid-router-module-provider'; SmartRouterModule, RouterModule.forRoot([]), SharedComponentsModule, - ToastrModule.forRoot(toastrConfig) + ToastrModule.forRoot(toastrConfig), ], declarations: [ EmailDashboardResultComponent, - EmailDashboardResultPageRootComponent + EmailDashboardResultPageRootComponent, ], entryComponents: [ EmailDashboardResultComponent, - EmailDashboardResultPageRootComponent + EmailDashboardResultPageRootComponent, ], providers: [ { provide: HTTP_INTERCEPTORS, useClass: RequestInterceptor, - multi: true + multi: true, }, { provide: APP_INITIALIZER, useFactory: platformFeatureInitFactory, deps: [PlatformFeatureService], - multi: true + multi: true, }, { provide: APP_BASE_HREF, - useValue: '/' - } + useValue: '/', + }, ], - bootstrap: [EmailDashboardResultPageRootComponent] + bootstrap: [EmailDashboardResultPageRootComponent], }) export class EmailDashboardResultModule {} diff --git a/core/templates/pages/error-pages/error-404/error-404-page-root.component.spec.ts b/core/templates/pages/error-pages/error-404/error-404-page-root.component.spec.ts index 50349ecfa110..188d51e907ac 100644 --- a/core/templates/pages/error-pages/error-404/error-404-page-root.component.spec.ts +++ b/core/templates/pages/error-pages/error-404/error-404-page-root.component.spec.ts @@ -16,11 +16,11 @@ * @fileoverview Unit tests for the error 404 page root component. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { LoggerService } from 'services/contextual/logger.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { Error404PageRootComponent } from './error-404-page-root.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {LoggerService} from 'services/contextual/logger.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {Error404PageRootComponent} from './error-404-page-root.component'; describe('Error 404 Page Root', () => { let fixture: ComponentFixture; @@ -31,24 +31,22 @@ describe('Error 404 Page Root', () => { class MockWindowRef { nativeWindow = { location: { - pathname - } + pathname, + }, }; } beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - Error404PageRootComponent - ], + declarations: [Error404PageRootComponent], providers: [ LoggerService, { provide: WindowRef, - useClass: MockWindowRef - } + useClass: MockWindowRef, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -66,6 +64,7 @@ describe('Error 404 Page Root', () => { spyOn(loggerService, 'error'); component.ngOnInit(); expect(loggerService.error).toHaveBeenCalledWith( - `The requested path ${pathname} is not found.`); + `The requested path ${pathname} is not found.` + ); }); }); diff --git a/core/templates/pages/error-pages/error-404/error-404-page-root.component.ts b/core/templates/pages/error-pages/error-404/error-404-page-root.component.ts index 7aeac36bd64a..f50e0770936b 100644 --- a/core/templates/pages/error-pages/error-404/error-404-page-root.component.ts +++ b/core/templates/pages/error-pages/error-404/error-404-page-root.component.ts @@ -18,13 +18,13 @@ // This page is used as error page for 404 status code. -import { Component } from '@angular/core'; -import { LoggerService } from 'services/contextual/logger.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import {Component} from '@angular/core'; +import {LoggerService} from 'services/contextual/logger.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; @Component({ selector: 'oppia-error-page-root', - templateUrl: './error-404-page-root.component.html' + templateUrl: './error-404-page-root.component.html', }) export class Error404PageRootComponent { constructor( diff --git a/core/templates/pages/error-pages/error-404/error-404-page-routing.module.ts b/core/templates/pages/error-pages/error-404/error-404-page-routing.module.ts index 254b29be4b90..482c598d1c6e 100644 --- a/core/templates/pages/error-pages/error-404/error-404-page-routing.module.ts +++ b/core/templates/pages/error-pages/error-404/error-404-page-routing.module.ts @@ -16,24 +16,19 @@ * @fileoverview Routing module for error page. */ -import { NgModule } from '@angular/core'; -import { Route, RouterModule } from '@angular/router'; -import { Error404PageRootComponent } from './error-404-page-root.component'; +import {NgModule} from '@angular/core'; +import {Route, RouterModule} from '@angular/router'; +import {Error404PageRootComponent} from './error-404-page-root.component'; const routes: Route[] = [ { path: '', - component: Error404PageRootComponent - } + component: Error404PageRootComponent, + }, ]; @NgModule({ - imports: [ - RouterModule.forChild(routes) - ], - exports: [ - RouterModule - ] + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], }) - export class Error404PageRoutingModule {} diff --git a/core/templates/pages/error-pages/error-404/error-404-page.component.spec.ts b/core/templates/pages/error-pages/error-404/error-404-page.component.spec.ts index 04138e5308b3..9feae9492cc8 100644 --- a/core/templates/pages/error-pages/error-404/error-404-page.component.spec.ts +++ b/core/templates/pages/error-pages/error-404/error-404-page.component.spec.ts @@ -15,16 +15,15 @@ /** * @fileoverview Unit tests for error page. */ -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { async, TestBed, ComponentFixture } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import {async, TestBed, ComponentFixture} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; -import { Error404PageComponent } from './error-404-page.component'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { PageTitleService } from 'services/page-title.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; +import {Error404PageComponent} from './error-404-page.component'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {PageTitleService} from 'services/page-title.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; class MockI18nLanguageCodeService { codeChangeEventEmiiter = new EventEmitter(); @@ -57,16 +56,16 @@ describe('Error page', () => { providers: [ { provide: I18nLanguageCodeService, - useClass: MockI18nLanguageCodeService + useClass: MockI18nLanguageCodeService, }, UrlInterpolationService, PageTitleService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -78,23 +77,26 @@ describe('Error page', () => { fixture.detectChanges(); }); - it('should set images and subscribe to onLangChange upon initialization', + it('should set images and subscribe to onLangChange upon initialization', () => { + spyOn(translateService.onLangChange, 'subscribe'); + component.ngOnInit(); + expect(component.getStaticImageUrl('/general/oops_mint.webp')).toBe( + '/assets/images/general/oops_mint.webp' + ); + expect(translateService.onLangChange.subscribe).toHaveBeenCalled(); + }); + + it( + 'should obtain translated page title whenever the selected' + + 'language changes', () => { - spyOn(translateService.onLangChange, 'subscribe'); component.ngOnInit(); - expect(component.getStaticImageUrl('/general/oops_mint.webp')) - .toBe('/assets/images/general/oops_mint.webp'); - expect(translateService.onLangChange.subscribe).toHaveBeenCalled(); - }); + spyOn(component, 'setPageTitle'); + translateService.onLangChange.emit(); - it('should obtain translated page title whenever the selected' + - 'language changes', () => { - component.ngOnInit(); - spyOn(component, 'setPageTitle'); - translateService.onLangChange.emit(); - - expect(component.setPageTitle).toHaveBeenCalled(); - }); + expect(component.setPageTitle).toHaveBeenCalled(); + } + ); it('should set new page title', () => { spyOn(translateService, 'instant').and.callThrough(); @@ -102,9 +104,11 @@ describe('Error page', () => { component.setPageTitle(); expect(translateService.instant).toHaveBeenCalledWith( - 'I18N_ERROR_PAGE_TITLE_404'); + 'I18N_ERROR_PAGE_TITLE_404' + ); expect(pageTitleService.setDocumentTitle).toHaveBeenCalledWith( - 'I18N_ERROR_PAGE_TITLE_404'); + 'I18N_ERROR_PAGE_TITLE_404' + ); }); it('should unsubscribe on component destruction', () => { diff --git a/core/templates/pages/error-pages/error-404/error-404-page.component.ts b/core/templates/pages/error-pages/error-404/error-404-page.component.ts index ced5f19f97f5..44a6cc281b96 100644 --- a/core/templates/pages/error-pages/error-404/error-404-page.component.ts +++ b/core/templates/pages/error-pages/error-404/error-404-page.component.ts @@ -16,18 +16,17 @@ * @fileoverview Component for the error 404 page. */ -import { Component, OnInit, OnDestroy } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; +import {Component, OnInit, OnDestroy} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; -import { PageTitleService } from 'services/page-title.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {PageTitleService} from 'services/page-title.service'; @Component({ selector: 'oppia-error-404-page', templateUrl: './error-404-page.component.html', - styleUrls: [] + styleUrls: [], }) export class Error404PageComponent implements OnInit, OnDestroy { directiveSubscriptions = new Subscription(); @@ -47,7 +46,8 @@ export class Error404PageComponent implements OnInit, OnDestroy { setPageTitle(): void { let translatedTitle = this.translateService.instant( - 'I18N_ERROR_PAGE_TITLE_404'); + 'I18N_ERROR_PAGE_TITLE_404' + ); this.pageTitleService.setDocumentTitle(translatedTitle); } diff --git a/core/templates/pages/error-pages/error-404/error-404-page.module.ts b/core/templates/pages/error-pages/error-404/error-404-page.module.ts index 231ea74acabb..e0994dc2b8b7 100644 --- a/core/templates/pages/error-pages/error-404/error-404-page.module.ts +++ b/core/templates/pages/error-pages/error-404/error-404-page.module.ts @@ -16,28 +16,16 @@ * @fileoverview Module for the error page. */ -import { NgModule } from '@angular/core'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { Error404PageRootComponent } from './error-404-page-root.component'; -import { Error404PageRoutingModule } from './error-404-page-routing.module'; -import { Error404PageComponent } from './error-404-page.component'; +import {NgModule} from '@angular/core'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {Error404PageRootComponent} from './error-404-page-root.component'; +import {Error404PageRoutingModule} from './error-404-page-routing.module'; +import {Error404PageComponent} from './error-404-page.component'; @NgModule({ - imports: [ - SharedComponentsModule, - Error404PageRoutingModule - ], - declarations: [ - Error404PageComponent, - Error404PageRootComponent, - ], - entryComponents: [ - Error404PageComponent, - Error404PageRootComponent, - ], - exports: [ - Error404PageComponent, - Error404PageRootComponent - ] + imports: [SharedComponentsModule, Error404PageRoutingModule], + declarations: [Error404PageComponent, Error404PageRootComponent], + entryComponents: [Error404PageComponent, Error404PageRootComponent], + exports: [Error404PageComponent, Error404PageRootComponent], }) export class Error404PageModule {} diff --git a/core/templates/pages/error-pages/error-iframed-page/error-iframed-page.import.ts b/core/templates/pages/error-pages/error-iframed-page/error-iframed-page.import.ts index bf7a18d1bd33..8e9b73a36669 100644 --- a/core/templates/pages/error-pages/error-iframed-page/error-iframed-page.import.ts +++ b/core/templates/pages/error-pages/error-iframed-page/error-iframed-page.import.ts @@ -16,11 +16,11 @@ * @fileoverview Scripts for the error iframed page. */ import 'pages/common-imports'; -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { AppConstants } from 'app.constants'; -import { enableProdMode } from '@angular/core'; -import { ErrorIframedPageModule } from './error-iframed-page.module'; -import { LoggerService } from 'services/contextual/logger.service'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {AppConstants} from 'app.constants'; +import {enableProdMode} from '@angular/core'; +import {ErrorIframedPageModule} from './error-iframed-page.module'; +import {LoggerService} from 'services/contextual/logger.service'; if (!AppConstants.DEV_MODE) { enableProdMode(); @@ -28,9 +28,9 @@ if (!AppConstants.DEV_MODE) { const loggerService = new LoggerService(); -platformBrowserDynamic().bootstrapModule(ErrorIframedPageModule).catch( - (err) => loggerService.error(err) -); +platformBrowserDynamic() + .bootstrapModule(ErrorIframedPageModule) + .catch(err => loggerService.error(err)); // This prevents angular pages to cause side effects to hybrid pages. // TODO(#13080): Remove window.name statement from import.ts files diff --git a/core/templates/pages/error-pages/error-iframed-page/error-iframed-page.module.ts b/core/templates/pages/error-pages/error-iframed-page/error-iframed-page.module.ts index 2b6dcb97a9da..5493ec92179a 100644 --- a/core/templates/pages/error-pages/error-iframed-page/error-iframed-page.module.ts +++ b/core/templates/pages/error-pages/error-iframed-page/error-iframed-page.module.ts @@ -16,21 +16,22 @@ * @fileoverview Module for the error iframed page. */ -import { APP_INITIALIZER, NgModule } from '@angular/core'; -import { BrowserModule, HAMMER_GESTURE_CONFIG } from '@angular/platform-browser'; -import { HttpClientModule } from '@angular/common/http'; -import { HTTP_INTERCEPTORS } from '@angular/common/http'; - -import { RequestInterceptor } from 'services/request-interceptor.service'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { platformFeatureInitFactory, PlatformFeatureService } from - 'services/platform-feature.service'; -import { ErrorIframedPageRootComponent } from './error-iframed-root.component'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { ToastrModule } from 'ngx-toastr'; -import { MyHammerConfig, toastrConfig } from 'pages/oppia-root/app.module'; -import { AppErrorHandlerProvider } from 'pages/oppia-root/app-error-handler'; +import {APP_INITIALIZER, NgModule} from '@angular/core'; +import {BrowserModule, HAMMER_GESTURE_CONFIG} from '@angular/platform-browser'; +import {HttpClientModule} from '@angular/common/http'; +import {HTTP_INTERCEPTORS} from '@angular/common/http'; +import {RequestInterceptor} from 'services/request-interceptor.service'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import { + platformFeatureInitFactory, + PlatformFeatureService, +} from 'services/platform-feature.service'; +import {ErrorIframedPageRootComponent} from './error-iframed-root.component'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {ToastrModule} from 'ngx-toastr'; +import {MyHammerConfig, toastrConfig} from 'pages/oppia-root/app.module'; +import {AppErrorHandlerProvider} from 'pages/oppia-root/app-error-handler'; @NgModule({ imports: [ @@ -38,32 +39,28 @@ import { AppErrorHandlerProvider } from 'pages/oppia-root/app-error-handler'; BrowserAnimationsModule, HttpClientModule, SharedComponentsModule, - ToastrModule.forRoot(toastrConfig) - ], - declarations: [ - ErrorIframedPageRootComponent, - ], - entryComponents: [ - ErrorIframedPageRootComponent, + ToastrModule.forRoot(toastrConfig), ], + declarations: [ErrorIframedPageRootComponent], + entryComponents: [ErrorIframedPageRootComponent], providers: [ { provide: HTTP_INTERCEPTORS, useClass: RequestInterceptor, - multi: true + multi: true, }, { provide: APP_INITIALIZER, useFactory: platformFeatureInitFactory, deps: [PlatformFeatureService], - multi: true + multi: true, }, { provide: HAMMER_GESTURE_CONFIG, - useClass: MyHammerConfig + useClass: MyHammerConfig, }, AppErrorHandlerProvider, ], - bootstrap: [ErrorIframedPageRootComponent] + bootstrap: [ErrorIframedPageRootComponent], }) export class ErrorIframedPageModule {} diff --git a/core/templates/pages/error-pages/error-iframed-page/error-iframed-root.component.ts b/core/templates/pages/error-pages/error-iframed-page/error-iframed-root.component.ts index 25a731712511..3c0fdba3275d 100644 --- a/core/templates/pages/error-pages/error-iframed-page/error-iframed-root.component.ts +++ b/core/templates/pages/error-pages/error-iframed-page/error-iframed-root.component.ts @@ -16,10 +16,10 @@ * @fileoverview Root component for error iframed page. */ -import { Component } from '@angular/core'; +import {Component} from '@angular/core'; @Component({ selector: 'oppia-error-iframed-page-root', - templateUrl: './error-iframed-root.component.html' + templateUrl: './error-iframed-root.component.html', }) export class ErrorIframedPageRootComponent {} diff --git a/core/templates/pages/error-pages/error-page-root.component.spec.ts b/core/templates/pages/error-pages/error-page-root.component.spec.ts index 084fa3f83e77..feb40f79e621 100644 --- a/core/templates/pages/error-pages/error-page-root.component.spec.ts +++ b/core/templates/pages/error-pages/error-page-root.component.spec.ts @@ -16,27 +16,29 @@ * @fileoverview Unit tests for error page root. */ -import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { TranslateModule, TranslateService } from '@ngx-translate/core'; -import { ActivatedRoute, UrlSegment } from '@angular/router'; +import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {TranslateModule, TranslateService} from '@ngx-translate/core'; +import {ActivatedRoute, UrlSegment} from '@angular/router'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { ErrorPageRootComponent } from './error-page-root.component'; -import { BaseRootComponent } from 'pages/base-root.component'; -import { PageHeadService } from 'services/page-head.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {ErrorPageRootComponent} from './error-page-root.component'; +import {BaseRootComponent} from 'pages/base-root.component'; +import {PageHeadService} from 'services/page-head.service'; class MockWindowRef { nativeWindow = { document: { getElementsByTagName(tagName: string) { - return [{ - getAttribute(attr: string) { - return '401'; - } - }]; - } - } + return [ + { + getAttribute(attr: string) { + return '401'; + }, + }, + ]; + }, + }, }; } @@ -45,9 +47,9 @@ class MockActivatedRoute { paramMap: { get: (key: string) => { return '500'; - } + }, }, - url: [] + url: [], }; } @@ -64,14 +66,14 @@ describe('ErrorPageRootComponent', () => { TranslateService, { provide: WindowRef, - useClass: MockWindowRef + useClass: MockWindowRef, }, { provide: ActivatedRoute, - useClass: MockActivatedRoute - } + useClass: MockActivatedRoute, + }, ], - schemas: [CUSTOM_ELEMENTS_SCHEMA] + schemas: [CUSTOM_ELEMENTS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(ErrorPageRootComponent); @@ -85,8 +87,9 @@ describe('ErrorPageRootComponent', () => { expect(component.statusCode).toEqual('500'); expect(parentNgOnInitSpy).toHaveBeenCalledTimes(1); - expect(component.titleInterpolationParams).toEqual( - {statusCode: component.statusCode}); + expect(component.titleInterpolationParams).toEqual({ + statusCode: component.statusCode, + }); }); it('should obtain status code from activated router', () => { @@ -105,7 +108,8 @@ describe('ErrorPageRootComponent', () => { ); const getElementsByTagNameSpy = spyOn( component.windowRef.nativeWindow.document, - 'getElementsByTagName').and.callThrough(); + 'getElementsByTagName' + ).and.callThrough(); component.ngOnInit(); @@ -121,7 +125,8 @@ describe('ErrorPageRootComponent', () => { ); const getElementsByTagNameSpy = spyOn( component.windowRef.nativeWindow.document, - 'getElementsByTagName'); + 'getElementsByTagName' + ); component.ngOnInit(); @@ -135,7 +140,8 @@ describe('ErrorPageRootComponent', () => { component.activatedRoute.snapshot.url = [new UrlSegment('nested_path', {})]; const getElementsByTagNameSpy = spyOn( component.windowRef.nativeWindow.document, - 'getElementsByTagName'); + 'getElementsByTagName' + ); component.ngOnInit(); diff --git a/core/templates/pages/error-pages/error-page-root.component.ts b/core/templates/pages/error-pages/error-page-root.component.ts index 800c0e748834..19758864cce5 100644 --- a/core/templates/pages/error-pages/error-page-root.component.ts +++ b/core/templates/pages/error-pages/error-page-root.component.ts @@ -16,24 +16,23 @@ * @fileoverview Root component for error page. */ -import { Component } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; -import { TranslateService } from '@ngx-translate/core'; -import { AppConstants } from 'app.constants'; +import {Component} from '@angular/core'; +import {ActivatedRoute} from '@angular/router'; +import {TranslateService} from '@ngx-translate/core'; +import {AppConstants} from 'app.constants'; -import { BaseRootComponent, MetaTagData } from 'pages/base-root.component'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { PageHeadService } from 'services/page-head.service'; +import {BaseRootComponent, MetaTagData} from 'pages/base-root.component'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {PageHeadService} from 'services/page-head.service'; @Component({ selector: 'oppia-error-page-root', - templateUrl: './error-page-root.component.html' + templateUrl: './error-page-root.component.html', }) export class ErrorPageRootComponent extends BaseRootComponent { title: string = AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ERROR.TITLE; - meta: MetaTagData[] = - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ERROR.META as - unknown as Readonly[]; + meta: MetaTagData[] = AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ERROR + .META as unknown as Readonly[]; statusCode: string | null = null; validStatusCodes: string[] = ['400', '401', '404', '500']; @@ -41,10 +40,10 @@ export class ErrorPageRootComponent extends BaseRootComponent { activatedRoute: ActivatedRoute; constructor( - pageHeadService: PageHeadService, - translateService: TranslateService, - windowRef: WindowRef, - activatedRoute: ActivatedRoute + pageHeadService: PageHeadService, + translateService: TranslateService, + windowRef: WindowRef, + activatedRoute: ActivatedRoute ) { super(pageHeadService, translateService); this.windowRef = windowRef; @@ -54,13 +53,15 @@ export class ErrorPageRootComponent extends BaseRootComponent { ngOnInit(): void { this.statusCode = this.activatedRoute.snapshot.paramMap.get('status_code'); if (this.statusCode === null) { - const bodyTag = ( - this.windowRef.nativeWindow.document.getElementsByTagName('body')); + const bodyTag = + this.windowRef.nativeWindow.document.getElementsByTagName('body'); this.statusCode = bodyTag[0].getAttribute('errorCode'); } - if (!this.validStatusCodes.includes(String(this.statusCode)) || - this.activatedRoute.snapshot.url.length > 0) { + if ( + !this.validStatusCodes.includes(String(this.statusCode)) || + this.activatedRoute.snapshot.url.length > 0 + ) { this.statusCode = '404'; } @@ -68,6 +69,6 @@ export class ErrorPageRootComponent extends BaseRootComponent { } get titleInterpolationParams(): Object { - return { statusCode: this.statusCode }; + return {statusCode: this.statusCode}; } } diff --git a/core/templates/pages/error-pages/error-page-shared.module.ts b/core/templates/pages/error-pages/error-page-shared.module.ts index 4edd388978c7..8e2f9e559e4c 100644 --- a/core/templates/pages/error-pages/error-page-shared.module.ts +++ b/core/templates/pages/error-pages/error-page-shared.module.ts @@ -18,14 +18,14 @@ * ErrorPageComponent and ErrorPageRootComponent. */ -import { NgModule } from '@angular/core'; -import { TranslateModule } from '@ngx-translate/core'; -import { ToastrModule } from 'ngx-toastr'; +import {NgModule} from '@angular/core'; +import {TranslateModule} from '@ngx-translate/core'; +import {ToastrModule} from 'ngx-toastr'; -import { ErrorPageComponent } from './error-page.component'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { ErrorPageRootComponent } from './error-page-root.component'; -import { toastrConfig } from 'pages/oppia-root/app.module'; +import {ErrorPageComponent} from './error-page.component'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {ErrorPageRootComponent} from './error-page-root.component'; +import {toastrConfig} from 'pages/oppia-root/app.module'; @NgModule({ imports: [ diff --git a/core/templates/pages/error-pages/error-page.component.spec.ts b/core/templates/pages/error-pages/error-page.component.spec.ts index 3f5bdd1ae46c..9ae247a6be79 100644 --- a/core/templates/pages/error-pages/error-page.component.spec.ts +++ b/core/templates/pages/error-pages/error-page.component.spec.ts @@ -15,12 +15,12 @@ /** * @fileoverview Unit tests for error page. */ -import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; -import { TestBed, ComponentFixture } from '@angular/core/testing'; -import { TranslateModule } from '@ngx-translate/core'; +import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; +import {TestBed, ComponentFixture} from '@angular/core/testing'; +import {TranslateModule} from '@ngx-translate/core'; -import { ErrorPageComponent } from './error-page.component'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; +import {ErrorPageComponent} from './error-page.component'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; describe('ErrorPageComponent', () => { let component: ErrorPageComponent; @@ -31,7 +31,7 @@ describe('ErrorPageComponent', () => { imports: [TranslateModule.forRoot()], declarations: [ErrorPageComponent], providers: [UrlInterpolationService], - schemas: [CUSTOM_ELEMENTS_SCHEMA] + schemas: [CUSTOM_ELEMENTS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(ErrorPageComponent); @@ -45,7 +45,8 @@ describe('ErrorPageComponent', () => { }); it('should get the static image url', () => { - expect(component.getStaticImageUrl('/general/oops_mint.webp')) - .toBe('/assets/images/general/oops_mint.webp'); + expect(component.getStaticImageUrl('/general/oops_mint.webp')).toBe( + '/assets/images/general/oops_mint.webp' + ); }); }); diff --git a/core/templates/pages/error-pages/error-page.component.ts b/core/templates/pages/error-pages/error-page.component.ts index 70264d591290..33a166d2a0f7 100644 --- a/core/templates/pages/error-pages/error-page.component.ts +++ b/core/templates/pages/error-pages/error-page.component.ts @@ -16,15 +16,14 @@ * @fileoverview Component for the error page. */ -import { Component, Input } from '@angular/core'; +import {Component, Input} from '@angular/core'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; @Component({ selector: 'error-page', templateUrl: './error-page.component.html', - styleUrls: [] + styleUrls: [], }) export class ErrorPageComponent { // This property is initialized using Angular lifecycle hooks @@ -32,9 +31,7 @@ export class ErrorPageComponent { // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @Input() statusCode!: string; - constructor( - private urlInterpolationService: UrlInterpolationService, - ) {} + constructor(private urlInterpolationService: UrlInterpolationService) {} getStaticImageUrl(imagePath: string): string { return this.urlInterpolationService.getStaticImageUrl(imagePath); diff --git a/core/templates/pages/error-pages/error-page.import.ts b/core/templates/pages/error-pages/error-page.import.ts index 26b281ba6de6..baba1b8e0ac0 100644 --- a/core/templates/pages/error-pages/error-page.import.ts +++ b/core/templates/pages/error-pages/error-page.import.ts @@ -17,11 +17,11 @@ */ import 'pages/common-imports'; -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { OldErrorPageModule } from './old-error-page.module'; -import { AppConstants } from 'app.constants'; -import { enableProdMode } from '@angular/core'; -import { LoggerService } from 'services/contextual/logger.service'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {OldErrorPageModule} from './old-error-page.module'; +import {AppConstants} from 'app.constants'; +import {enableProdMode} from '@angular/core'; +import {LoggerService} from 'services/contextual/logger.service'; if (!AppConstants.DEV_MODE) { enableProdMode(); @@ -29,9 +29,9 @@ if (!AppConstants.DEV_MODE) { const loggerService = new LoggerService(); -platformBrowserDynamic().bootstrapModule(OldErrorPageModule).catch( - (err) => loggerService.error(err) -); +platformBrowserDynamic() + .bootstrapModule(OldErrorPageModule) + .catch(err => loggerService.error(err)); // This prevents angular pages to cause side effects to hybrid pages. // TODO(#13080): Remove window.name statement from import.ts files diff --git a/core/templates/pages/error-pages/error-page.module.ts b/core/templates/pages/error-pages/error-page.module.ts index d42b4e0bd0e5..e134172c1ca7 100644 --- a/core/templates/pages/error-pages/error-page.module.ts +++ b/core/templates/pages/error-pages/error-page.module.ts @@ -16,14 +16,14 @@ * @fileoverview Module for the error page. */ -import { NgModule } from '@angular/core'; -import { HttpClientModule } from '@angular/common/http'; -import { CommonModule } from '@angular/common'; +import {NgModule} from '@angular/core'; +import {HttpClientModule} from '@angular/common/http'; +import {CommonModule} from '@angular/common'; -import { ErrorPageComponent } from './error-page.component'; -import { ErrorPageRootComponent } from './error-page-root.component'; -import { RouterModule } from '@angular/router'; -import { ErrorPageSharedModule } from './error-page-shared.module'; +import {ErrorPageComponent} from './error-page.component'; +import {ErrorPageRootComponent} from './error-page-root.component'; +import {RouterModule} from '@angular/router'; +import {ErrorPageSharedModule} from './error-page-shared.module'; @NgModule({ imports: [ @@ -32,7 +32,7 @@ import { ErrorPageSharedModule } from './error-page-shared.module'; RouterModule.forChild([ { path: '', - component: ErrorPageRootComponent + component: ErrorPageRootComponent, }, ]), ErrorPageSharedModule, diff --git a/core/templates/pages/error-pages/old-error-page-routing.module.ts b/core/templates/pages/error-pages/old-error-page-routing.module.ts index 96bbb642d295..64535c2cf5cd 100644 --- a/core/templates/pages/error-pages/old-error-page-routing.module.ts +++ b/core/templates/pages/error-pages/old-error-page-routing.module.ts @@ -16,17 +16,17 @@ * @fileoverview Routing module for error page. */ -import { NgModule } from '@angular/core'; -import { Route, RouterModule } from '@angular/router'; +import {NgModule} from '@angular/core'; +import {Route, RouterModule} from '@angular/router'; -import { ErrorPageRootComponent } from 'pages/error-pages/error-page-root.component'; -import { SmartRouterModule } from 'hybrid-router-module-provider'; +import {ErrorPageRootComponent} from 'pages/error-pages/error-page-root.component'; +import {SmartRouterModule} from 'hybrid-router-module-provider'; const routes: Route[] = [ { path: '**', - component: ErrorPageRootComponent - } + component: ErrorPageRootComponent, + }, ]; @NgModule({ @@ -34,11 +34,8 @@ const routes: Route[] = [ // TODO(#13443): Remove smart router module provider once all pages are // migrated to angular router. SmartRouterModule, - RouterModule.forRoot(routes) + RouterModule.forRoot(routes), ], - exports: [ - RouterModule - ] + exports: [RouterModule], }) - export class ErrorPageRoutingModule {} diff --git a/core/templates/pages/error-pages/old-error-page.module.ts b/core/templates/pages/error-pages/old-error-page.module.ts index f77686695189..2a50784f89b4 100644 --- a/core/templates/pages/error-pages/old-error-page.module.ts +++ b/core/templates/pages/error-pages/old-error-page.module.ts @@ -17,15 +17,15 @@ * been migrated to angular router. */ -import { NgModule } from '@angular/core'; -import { BrowserModule } from '@angular/platform-browser'; -import { HttpClientModule } from '@angular/common/http'; -import { APP_BASE_HREF } from '@angular/common'; +import {NgModule} from '@angular/core'; +import {BrowserModule} from '@angular/platform-browser'; +import {HttpClientModule} from '@angular/common/http'; +import {APP_BASE_HREF} from '@angular/common'; -import { ErrorPageComponent } from './error-page.component'; -import { ErrorPageRootComponent } from './error-page-root.component'; -import { ErrorPageRoutingModule } from './old-error-page-routing.module'; -import { ErrorPageSharedModule } from './error-page-shared.module'; +import {ErrorPageComponent} from './error-page.component'; +import {ErrorPageRootComponent} from './error-page-root.component'; +import {ErrorPageRoutingModule} from './old-error-page-routing.module'; +import {ErrorPageSharedModule} from './error-page-shared.module'; // TODO (#19154): Remove this module in favor of error-page-migrated.module.ts // once angular migration is complete. @@ -37,18 +37,13 @@ import { ErrorPageSharedModule } from './error-page-shared.module'; ErrorPageRoutingModule, ErrorPageSharedModule, ], - entryComponents: [ - ErrorPageComponent, - ErrorPageRootComponent, - ], - bootstrap: [ - ErrorPageRootComponent - ], + entryComponents: [ErrorPageComponent, ErrorPageRootComponent], + bootstrap: [ErrorPageRootComponent], providers: [ { provide: APP_BASE_HREF, - useValue: '/' - } - ] + useValue: '/', + }, + ], }) export class OldErrorPageModule {} diff --git a/core/templates/pages/exploration-editor-page/changes-in-human-readable-form/changes-in-human-readable-form.component.spec.ts b/core/templates/pages/exploration-editor-page/changes-in-human-readable-form/changes-in-human-readable-form.component.spec.ts index 4cb436b1cf6e..9888dff15ec2 100644 --- a/core/templates/pages/exploration-editor-page/changes-in-human-readable-form/changes-in-human-readable-form.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/changes-in-human-readable-form/changes-in-human-readable-form.component.spec.ts @@ -14,15 +14,23 @@ /** * @fileoverview Unit tests for ChangesInHumanReadableForm Component. -*/ - -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { FormsModule } from '@angular/forms'; - -import { LostChangeBackendDict, LostChangeObjectFactory, LostChangeValue } from 'domain/exploration/LostChangeObjectFactory'; -import { Outcome, OutcomeBackendDict, OutcomeObjectFactory } from 'domain/exploration/OutcomeObjectFactory'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { ChangesInHumanReadableFormComponent } from './changes-in-human-readable-form.component'; + */ + +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {FormsModule} from '@angular/forms'; + +import { + LostChangeBackendDict, + LostChangeObjectFactory, + LostChangeValue, +} from 'domain/exploration/LostChangeObjectFactory'; +import { + Outcome, + OutcomeBackendDict, + OutcomeObjectFactory, +} from 'domain/exploration/OutcomeObjectFactory'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {ChangesInHumanReadableFormComponent} from './changes-in-human-readable-form.component'; describe('Changes in Human Readable Form Component', () => { let component: ChangesInHumanReadableFormComponent; @@ -32,29 +40,23 @@ describe('Changes in Human Readable Form Component', () => { // This is a helper function to clean the compiled html // for each test, in order to make a cleaner assertion. - const removeComments = (HTML: { toString: () => string }) => { - return HTML - .toString() - // Removes Unecessary white spaces and new lines. - .replace(/^\s+|\r\n|\n|\r|(>)\s+(<)|\s+$/gm, '$1$2') - // Removes Comments. - .replace(/<\!--.*?-->/gm, '') - // Removes marker. - .replace(/::marker/, ''); + const removeComments = (HTML: {toString: () => string}) => { + return ( + HTML.toString() + // Removes Unecessary white spaces and new lines. + .replace(/^\s+|\r\n|\n|\r|(>)\s+(<)|\s+$/gm, '$1$2') + // Removes Comments. + .replace(/<\!--.*?-->/gm, '') + // Removes marker. + .replace(/::marker/, '') + ); }; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - FormsModule, - ], - declarations: [ - ChangesInHumanReadableFormComponent - ], - providers: [ - LostChangeObjectFactory, - OutcomeObjectFactory - ] + imports: [FormsModule], + declarations: [ChangesInHumanReadableFormComponent], + providers: [LostChangeObjectFactory, OutcomeObjectFactory], }).compileComponents(); })); @@ -68,123 +70,139 @@ describe('Changes in Human Readable Form Component', () => { })); it('should make human readable when adding a state', () => { - component.lostChanges = [lostChangeObjectFactory.createNew({ - cmd: 'add_state', - state_name: 'State name', - content_id_for_state_content: 'content_0', - content_id_for_default_outcome: 'default_outcome_1' - })]; + component.lostChanges = [ + lostChangeObjectFactory.createNew({ + cmd: 'add_state', + state_name: 'State name', + content_id_for_state_content: 'content_0', + content_id_for_default_outcome: 'default_outcome_1', + }), + ]; fixture.detectChanges(); - let html = fixture.debugElement.nativeElement - .querySelector('.oppia-lost-changes').outerHTML; + let html = fixture.debugElement.nativeElement.querySelector( + '.oppia-lost-changes' + ).outerHTML; let result = removeComments(html); expect(result).toBe( '
' + - '
    ' + - '
  • ' + - ' Added state: ' + component.lostChanges[0].stateName + ' ' + - '
  • ' + - '
' + - '
' + '
    ' + + '
  • ' + + ' Added state: ' + + component.lostChanges[0].stateName + + ' ' + + '
  • ' + + '
' + + '' ); }); it('should make human readable when renaming a state', () => { - component.lostChanges = [lostChangeObjectFactory.createNew({ - cmd: 'rename_state', - old_state_name: 'Old state name', - new_state_name: 'New state name' - })]; + component.lostChanges = [ + lostChangeObjectFactory.createNew({ + cmd: 'rename_state', + old_state_name: 'Old state name', + new_state_name: 'New state name', + }), + ]; fixture.detectChanges(); - let html = fixture.debugElement.nativeElement - .querySelector('.oppia-lost-changes').outerHTML; + let html = fixture.debugElement.nativeElement.querySelector( + '.oppia-lost-changes' + ).outerHTML; let result = removeComments(html); expect(result).toBe( '
' + - '
    ' + - '
  • ' + - ' Renamed state: ' + component.lostChanges[0].oldStateName + - ' to ' + component.lostChanges[0].newStateName + ' ' + - '
  • ' + - '
' + - '
' + '
    ' + + '
  • ' + + ' Renamed state: ' + + component.lostChanges[0].oldStateName + + ' to ' + + component.lostChanges[0].newStateName + + ' ' + + '
  • ' + + '
' + + '' ); }); it('should make human readable when deleting a state', () => { - component.lostChanges = [lostChangeObjectFactory.createNew({ - cmd: 'delete_state', - state_name: 'Deleted state name' - })]; + component.lostChanges = [ + lostChangeObjectFactory.createNew({ + cmd: 'delete_state', + state_name: 'Deleted state name', + }), + ]; fixture.detectChanges(); - let html = fixture.debugElement.nativeElement - .querySelector('.oppia-lost-changes').outerHTML; + let html = fixture.debugElement.nativeElement.querySelector( + '.oppia-lost-changes' + ).outerHTML; let result = removeComments(html); expect(result).toBe( '
' + - '
    ' + - '
  • ' + - ' Deleted state: ' + - component.lostChanges[0].stateName + - ' ' + - '
  • ' + - '
' + - '
' + '
    ' + + '
  • ' + + ' Deleted state: ' + + component.lostChanges[0].stateName + + ' ' + + '
  • ' + + '
' + + '' ); }); - it('should make human readable when editing a state with property content', - () => { - component.lostChanges = [lostChangeObjectFactory.createNew({ + it('should make human readable when editing a state with property content', () => { + component.lostChanges = [ + lostChangeObjectFactory.createNew({ cmd: 'edit_state_property', state_name: 'Edited state name', new_value: { - html: 'newValue' + html: 'newValue', } as LostChangeValue, old_value: { - html: 'oldValue' + html: 'oldValue', } as LostChangeValue, - property_name: 'content' - })]; + property_name: 'content', + }), + ]; - fixture.detectChanges(); + fixture.detectChanges(); - let html = fixture.debugElement.nativeElement - .querySelector('.oppia-lost-changes').outerHTML; + let html = fixture.debugElement.nativeElement.querySelector( + '.oppia-lost-changes' + ).outerHTML; - let result = removeComments(html); - let lostChangeValue = ( - component.lostChanges[0].newValue as LostChangeValue); - expect(result).toBe( - '
' + + let result = removeComments(html); + let lostChangeValue = component.lostChanges[0].newValue as LostChangeValue; + expect(result).toBe( + '
' + '
    ' + '
  • ' + '
    ' + '' + 'Edits to state:' + - ' ' + component.lostChanges[0].stateName + + ' ' + + component.lostChanges[0].stateName + '
    ' + '' + 'Edits to property:' + - ' ' + component.lostChanges[0].propertyName + + ' ' + + component.lostChanges[0].propertyName + '
    ' + '
    ' + '
    ' + 'Edited content: ' + // eslint-disable-next-line dot-notation - '
    ' + lostChangeValue[ - 'html' as keyof LostChangeValue - ] + + '
    ' + + lostChangeValue['html' as keyof LostChangeValue] + '
    ' + '
    ' + '
    ' + @@ -192,639 +210,741 @@ describe('Changes in Human Readable Form Component', () => { '
  • ' + '
' + '
' - ); - }); - - it('should make human readable when editing a state with property' + - ' widget_id and exploration ended', () => { - component.lostChanges = [lostChangeObjectFactory.createNew({ - cmd: 'edit_state_property', - state_name: 'Edited state name', - new_value: 'EndExploration', - old_value: null, - property_name: 'widget_id' - })]; - - fixture.detectChanges(); - - let html = fixture.debugElement.nativeElement - .querySelector('.oppia-lost-changes').outerHTML; - - let result = removeComments(html); - expect(result).toBe( - '
' + - '
    ' + - '
  • ' + - '
    ' + - '' + - 'Edits to state:' + - ' ' + component.lostChanges[0].stateName + - '
    ' + - '' + - 'Edits to property:' + - ' ' + component.lostChanges[0].propertyName + - '
    ' + - '
    ' + - ' Ended Exploration ' + - '
    ' + - '
    ' + - '
  • ' + - '
' + - '
' ); }); - it('should make human readable when editing a state with property' + - ' widget_id and an interaction is added', () => { - component.lostChanges = [lostChangeObjectFactory.createNew({ - cmd: 'edit_state_property', - state_name: 'Edited state name', - new_value: 'Exploration', - old_value: null, - property_name: 'widget_id' - })]; + it( + 'should make human readable when editing a state with property' + + ' widget_id and exploration ended', + () => { + component.lostChanges = [ + lostChangeObjectFactory.createNew({ + cmd: 'edit_state_property', + state_name: 'Edited state name', + new_value: 'EndExploration', + old_value: null, + property_name: 'widget_id', + }), + ]; - fixture.detectChanges(); + fixture.detectChanges(); - let html = fixture.debugElement.nativeElement - .querySelector('.oppia-lost-changes').outerHTML; + let html = fixture.debugElement.nativeElement.querySelector( + '.oppia-lost-changes' + ).outerHTML; - let result = removeComments(html); - expect(result).toBe( - '
' + - '
    ' + - '
  • ' + - '
    ' + - '' + - 'Edits to state:' + - ' ' + component.lostChanges[0].stateName + - '
    ' + - '' + - 'Edits to property:' + - ' ' + component.lostChanges[0].propertyName + - '
    ' + - '
    ' + - '' + - '' + - 'Added Interaction: ' + - component.lostChanges[0].newValue + - ' ' + - '
    ' + - '
    ' + - '
  • ' + - '
' + - '
' - ); - }); + let result = removeComments(html); + expect(result).toBe( + '
' + + '
    ' + + '
  • ' + + '
    ' + + '' + + 'Edits to state:' + + ' ' + + component.lostChanges[0].stateName + + '
    ' + + '' + + 'Edits to property:' + + ' ' + + component.lostChanges[0].propertyName + + '
    ' + + '
    ' + + ' Ended Exploration ' + + '
    ' + + '
    ' + + '
  • ' + + '
' + + '
' + ); + } + ); - it('should make human readable when editing a state with property' + - ' widget_id and an interaction is deleted', () => { - component.lostChanges = [lostChangeObjectFactory.createNew({ - cmd: 'edit_state_property', - state_name: 'Edited state name', - new_value: null, - old_value: 'EndExploration', - property_name: 'widget_id' - })]; + it( + 'should make human readable when editing a state with property' + + ' widget_id and an interaction is added', + () => { + component.lostChanges = [ + lostChangeObjectFactory.createNew({ + cmd: 'edit_state_property', + state_name: 'Edited state name', + new_value: 'Exploration', + old_value: null, + property_name: 'widget_id', + }), + ]; - fixture.detectChanges(); + fixture.detectChanges(); - let html = fixture.debugElement.nativeElement - .querySelector('.oppia-lost-changes').outerHTML; + let html = fixture.debugElement.nativeElement.querySelector( + '.oppia-lost-changes' + ).outerHTML; - let result = removeComments(html); - expect(result).toBe( - '
' + - '
    ' + - '
  • ' + - '
    ' + - '' + - 'Edits to state:' + - ' ' + component.lostChanges[0].stateName + - '
    ' + - '' + - 'Edits to property:' + - ' ' + component.lostChanges[0].propertyName + - '
    ' + - '
    ' + - '' + - 'Deleted Interaction: ' + - component.lostChanges[0].oldValue + - ' ' + - '
    ' + - '
    ' + - '
  • ' + - '
' + - '
' - ); - }); + let result = removeComments(html); + expect(result).toBe( + '
' + + '
    ' + + '
  • ' + + '
    ' + + '' + + 'Edits to state:' + + ' ' + + component.lostChanges[0].stateName + + '
    ' + + '' + + 'Edits to property:' + + ' ' + + component.lostChanges[0].propertyName + + '
    ' + + '
    ' + + '' + + '' + + 'Added Interaction: ' + + component.lostChanges[0].newValue + + ' ' + + '
    ' + + '
    ' + + '
  • ' + + '
' + + '
' + ); + } + ); - it('should make human readable when editing a state with property' + - ' widget_customization_args and an interaction is added', () => { - component.lostChanges = [lostChangeObjectFactory.createNew({ - cmd: 'edit_state_property', - state_name: 'Edited state name', - new_value: { - property1: true - }, - old_value: {}, - property_name: 'widget_customization_args' - } as LostChangeBackendDict)]; + it( + 'should make human readable when editing a state with property' + + ' widget_id and an interaction is deleted', + () => { + component.lostChanges = [ + lostChangeObjectFactory.createNew({ + cmd: 'edit_state_property', + state_name: 'Edited state name', + new_value: null, + old_value: 'EndExploration', + property_name: 'widget_id', + }), + ]; - fixture.detectChanges(); + fixture.detectChanges(); - let html = fixture.debugElement.nativeElement - .querySelector('.oppia-lost-changes').outerHTML; + let html = fixture.debugElement.nativeElement.querySelector( + '.oppia-lost-changes' + ).outerHTML; - let result = removeComments(html); - expect(result).toBe( - '
' + - '
    ' + - '
  • ' + - '
    ' + - '' + - 'Edits to state:' + - ' ' + component.lostChanges[0].stateName + - '
    ' + - '' + - 'Edits to property:' + - ' ' + component.lostChanges[0].propertyName + - '
    ' + - '
    ' + - ' ' + - 'Added Interaction Customizations ' + - '' + - '
    ' + - '
    ' + - '
  • ' + - '
' + - '
' - ); - }); + let result = removeComments(html); + expect(result).toBe( + '
' + + '
    ' + + '
  • ' + + '
    ' + + '' + + 'Edits to state:' + + ' ' + + component.lostChanges[0].stateName + + '
    ' + + '' + + 'Edits to property:' + + ' ' + + component.lostChanges[0].propertyName + + '
    ' + + '
    ' + + '' + + 'Deleted Interaction: ' + + component.lostChanges[0].oldValue + + ' ' + + '
    ' + + '
    ' + + '
  • ' + + '
' + + '
' + ); + } + ); - it('should make human readable when editing a state with property' + - ' widget_customization_args and an interaction is removed', () => { - component.lostChanges = [lostChangeObjectFactory.createNew({ - cmd: 'edit_state_property', - state_name: 'Edited state name', - new_value: {}, - old_value: { - property1: true - }, - property_name: 'widget_customization_args' - } as LostChangeBackendDict)]; + it( + 'should make human readable when editing a state with property' + + ' widget_customization_args and an interaction is added', + () => { + component.lostChanges = [ + lostChangeObjectFactory.createNew({ + cmd: 'edit_state_property', + state_name: 'Edited state name', + new_value: { + property1: true, + }, + old_value: {}, + property_name: 'widget_customization_args', + } as LostChangeBackendDict), + ]; - fixture.detectChanges(); + fixture.detectChanges(); - let html = fixture.debugElement.nativeElement - .querySelector('.oppia-lost-changes').outerHTML; + let html = fixture.debugElement.nativeElement.querySelector( + '.oppia-lost-changes' + ).outerHTML; - let result = removeComments(html); - expect(result).toBe( - '
' + - '
    ' + - '
  • ' + - '
    ' + - '' + - 'Edits to state:' + - ' ' + component.lostChanges[0].stateName + - '
    ' + - '' + - 'Edits to property:' + - ' ' + component.lostChanges[0].propertyName + - '
    ' + - '
    ' + - ' ' + - 'Removed Interaction Customizations ' + - '' + - '
    ' + - '
    ' + - '
  • ' + - '
' + - '
' - ); - }); + let result = removeComments(html); + expect(result).toBe( + '
' + + '
    ' + + '
  • ' + + '
    ' + + '' + + 'Edits to state:' + + ' ' + + component.lostChanges[0].stateName + + '
    ' + + '' + + 'Edits to property:' + + ' ' + + component.lostChanges[0].propertyName + + '
    ' + + '
    ' + + ' ' + + 'Added Interaction Customizations ' + + '' + + '
    ' + + '
    ' + + '
  • ' + + '
' + + '
' + ); + } + ); - it('should make human readable when editing a state with property' + - ' widget_customization_args and an interaction is edited', () => { - component.lostChanges = [lostChangeObjectFactory.createNew({ - cmd: 'edit_state_property', - state_name: 'Edited state name', - new_value: { - property1: true - }, - old_value: { - property1: true - }, - property_name: 'widget_customization_args' - } as LostChangeBackendDict)]; + it( + 'should make human readable when editing a state with property' + + ' widget_customization_args and an interaction is removed', + () => { + component.lostChanges = [ + lostChangeObjectFactory.createNew({ + cmd: 'edit_state_property', + state_name: 'Edited state name', + new_value: {}, + old_value: { + property1: true, + }, + property_name: 'widget_customization_args', + } as LostChangeBackendDict), + ]; - fixture.detectChanges(); + fixture.detectChanges(); - let html = fixture.debugElement.nativeElement - .querySelector('.oppia-lost-changes').outerHTML; + let html = fixture.debugElement.nativeElement.querySelector( + '.oppia-lost-changes' + ).outerHTML; - let result = removeComments(html); - expect(result).toBe( - '
' + - '
    ' + - '
  • ' + - '
    ' + - '' + - 'Edits to state:' + - ' ' + component.lostChanges[0].stateName + - '
    ' + - '' + - 'Edits to property:' + - ' ' + component.lostChanges[0].propertyName + - '
    ' + - '
    ' + - ' ' + - 'Edited Interaction Customizations ' + - '' + - '
    ' + - '
    ' + - '
  • ' + - '
' + - '
' - ); - }); + let result = removeComments(html); + expect(result).toBe( + '
' + + '
    ' + + '
  • ' + + '
    ' + + '' + + 'Edits to state:' + + ' ' + + component.lostChanges[0].stateName + + '
    ' + + '' + + 'Edits to property:' + + ' ' + + component.lostChanges[0].propertyName + + '
    ' + + '
    ' + + ' ' + + 'Removed Interaction Customizations ' + + '' + + '
    ' + + '
    ' + + '
  • ' + + '
' + + '
' + ); + } + ); - it('should make human readable when editing a state with property' + - ' answer_groups and a change is added', () => { - component.lostChanges = [lostChangeObjectFactory.createNew({ - cmd: 'edit_state_property', - state_name: 'Edited state name', - new_value: { - outcome: outcomeObjectFactory.createFromBackendDict({ - dest: 'outcome 2', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: 'Html' + it( + 'should make human readable when editing a state with property' + + ' widget_customization_args and an interaction is edited', + () => { + component.lostChanges = [ + lostChangeObjectFactory.createNew({ + cmd: 'edit_state_property', + state_name: 'Edited state name', + new_value: { + property1: true, + }, + old_value: { + property1: true, }, - } as OutcomeBackendDict), - rules: [{ - type: 'Type1', - inputs: { - input1: 'input1', - input2: 'input2' - } - }] - }, - property_name: 'answer_groups' - } as LostChangeBackendDict)]; + property_name: 'widget_customization_args', + } as LostChangeBackendDict), + ]; - fixture.detectChanges(); + fixture.detectChanges(); - let html = fixture.debugElement.nativeElement - .querySelector('.oppia-lost-changes').outerHTML; + let html = fixture.debugElement.nativeElement.querySelector( + '.oppia-lost-changes' + ).outerHTML; - let result = removeComments(html); - expect(result).toBe( - '
' + - '
    ' + - '
  • ' + - '
    ' + - '' + - 'Edits to state:' + - ' ' + component.lostChanges[0].stateName + - '
    ' + - '' + - 'Edits to property:' + - ' ' + component.lostChanges[0].propertyName + - '
    ' + - '
    ' + - ' ' + - 'Added answer group ' + - '' + - '
    ' + - '
    ' + - '
  • ' + - '
' + - '
' - ); - }); + let result = removeComments(html); + expect(result).toBe( + '
' + + '
    ' + + '
  • ' + + '
    ' + + '' + + 'Edits to state:' + + ' ' + + component.lostChanges[0].stateName + + '
    ' + + '' + + 'Edits to property:' + + ' ' + + component.lostChanges[0].propertyName + + '
    ' + + '
    ' + + ' ' + + 'Edited Interaction Customizations ' + + '' + + '
    ' + + '
    ' + + '
  • ' + + '
' + + '
' + ); + } + ); - it('should make human readable when editing a state with property' + - ' answer_groups and a change is edited', () => { - component.lostChanges = [lostChangeObjectFactory.createNew({ - cmd: 'edit_state_property', - state_name: 'Edited state name', - new_value: { - outcome: outcomeObjectFactory.createFromBackendDict({ - dest: 'outcome 2', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: 'Html' - }, - } as OutcomeBackendDict), - rules: [{ - type: 'Type1', - inputs: { - input1: 'input1', - input2: 'input2' - } - }] - }, - old_value: { - outcome: outcomeObjectFactory.createFromBackendDict({ - dest: 'outcome 1', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: 'Html' + it( + 'should make human readable when editing a state with property' + + ' answer_groups and a change is added', + () => { + component.lostChanges = [ + lostChangeObjectFactory.createNew({ + cmd: 'edit_state_property', + state_name: 'Edited state name', + new_value: { + outcome: outcomeObjectFactory.createFromBackendDict({ + dest: 'outcome 2', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: 'Html', + }, + } as OutcomeBackendDict), + rules: [ + { + type: 'Type1', + inputs: { + input1: 'input1', + input2: 'input2', + }, + }, + ], }, - } as OutcomeBackendDict), - rules: [{ - type: 'Type1', - inputs: { - input1: 'input1', - input2: 'input2' - } - }] - }, - property_name: 'answer_groups' - } as LostChangeBackendDict)]; + property_name: 'answer_groups', + } as LostChangeBackendDict), + ]; - fixture.detectChanges(); + fixture.detectChanges(); - let html = fixture.debugElement.nativeElement - .querySelector('.oppia-lost-changes').outerHTML; + let html = fixture.debugElement.nativeElement.querySelector( + '.oppia-lost-changes' + ).outerHTML; - let result = removeComments(html); - expect(result).toBe( - '
' + - '
    ' + - '
  • ' + - '
    ' + - '' + - 'Edits to state:' + - ' ' + component.lostChanges[0].stateName + - '
    ' + - '' + - 'Edits to property:' + - ' ' + component.lostChanges[0].propertyName + - '
    ' + - '
    ' + - ' ' + - 'Edited answer group ' + - '' + - '
    ' + - '
    ' + - '
  • ' + - '
' + - '
' - ); - }); + let result = removeComments(html); + expect(result).toBe( + '
' + + '
    ' + + '
  • ' + + '
    ' + + '' + + 'Edits to state:' + + ' ' + + component.lostChanges[0].stateName + + '
    ' + + '' + + 'Edits to property:' + + ' ' + + component.lostChanges[0].propertyName + + '
    ' + + '
    ' + + ' ' + + 'Added answer group ' + + '' + + '
    ' + + '
    ' + + '
  • ' + + '
' + + '
' + ); + } + ); - it('should make human readable when editing a state with property' + - ' answer_groups and a change is deleted', () => { - component.lostChanges = [lostChangeObjectFactory.createNew({ - cmd: 'edit_state_property', - state_name: 'Edited state name', - new_value: {} as LostChangeValue, - old_value: { - outcome: outcomeObjectFactory.createFromBackendDict({ - dest: 'outcome 1', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: 'Html' + it( + 'should make human readable when editing a state with property' + + ' answer_groups and a change is edited', + () => { + component.lostChanges = [ + lostChangeObjectFactory.createNew({ + cmd: 'edit_state_property', + state_name: 'Edited state name', + new_value: { + outcome: outcomeObjectFactory.createFromBackendDict({ + dest: 'outcome 2', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: 'Html', + }, + } as OutcomeBackendDict), + rules: [ + { + type: 'Type1', + inputs: { + input1: 'input1', + input2: 'input2', + }, + }, + ], + }, + old_value: { + outcome: outcomeObjectFactory.createFromBackendDict({ + dest: 'outcome 1', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: 'Html', + }, + } as OutcomeBackendDict), + rules: [ + { + type: 'Type1', + inputs: { + input1: 'input1', + input2: 'input2', + }, + }, + ], }, - } as OutcomeBackendDict), - rules: [{ - type: 'Type1', - inputs: { - input1: 'input1', - input2: 'input2' - } - }] - } as LostChangeValue, - property_name: 'answer_groups' - })]; + property_name: 'answer_groups', + } as LostChangeBackendDict), + ]; - fixture.detectChanges(); + fixture.detectChanges(); - let html = fixture.debugElement.nativeElement - .querySelector('.oppia-lost-changes').outerHTML; + let html = fixture.debugElement.nativeElement.querySelector( + '.oppia-lost-changes' + ).outerHTML; - let result = removeComments(html); - expect(result).toBe( - '
' + - '
    ' + - '
  • ' + - '
    ' + - '' + - 'Edits to state:' + - ' ' + component.lostChanges[0].stateName + - '
    ' + - '' + - 'Edits to property:' + - ' ' + component.lostChanges[0].propertyName + - '
    ' + - '
    ' + - ' ' + - 'Deleted answer group ' + - '' + - '
    ' + - '
    ' + - '
  • ' + - '
' + - '
' - ); - }); + let result = removeComments(html); + expect(result).toBe( + '
' + + '
    ' + + '
  • ' + + '
    ' + + '' + + 'Edits to state:' + + ' ' + + component.lostChanges[0].stateName + + '
    ' + + '' + + 'Edits to property:' + + ' ' + + component.lostChanges[0].propertyName + + '
    ' + + '
    ' + + ' ' + + 'Edited answer group ' + + '' + + '
    ' + + '
    ' + + '
  • ' + + '
' + + '
' + ); + } + ); - it('should make human readable when editing a state with property' + - ' default_outcome and a change is added', () => { - component.lostChanges = [lostChangeObjectFactory.createNew({ - cmd: 'edit_state_property', - state_name: 'Edited state name', - new_value: ( - outcomeObjectFactory.createFromBackendDict({ - dest: 'outcome 2', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: 'Html' + it( + 'should make human readable when editing a state with property' + + ' answer_groups and a change is deleted', + () => { + component.lostChanges = [ + lostChangeObjectFactory.createNew({ + cmd: 'edit_state_property', + state_name: 'Edited state name', + new_value: {} as LostChangeValue, + old_value: { + outcome: outcomeObjectFactory.createFromBackendDict({ + dest: 'outcome 1', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: 'Html', + }, + } as OutcomeBackendDict), + rules: [ + { + type: 'Type1', + inputs: { + input1: 'input1', + input2: 'input2', + }, + }, + ], } as LostChangeValue, - } as OutcomeBackendDict)), - old_value: {} as LostChangeValue, - property_name: 'default_outcome' - })]; + property_name: 'answer_groups', + }), + ]; - fixture.detectChanges(); + fixture.detectChanges(); - let html = fixture.debugElement.nativeElement - .querySelector('.oppia-lost-changes').outerHTML; + let html = fixture.debugElement.nativeElement.querySelector( + '.oppia-lost-changes' + ).outerHTML; - let result = removeComments(html); - let lostChangeValue = ( - component.lostChanges[0].newValue as LostChangeValue) as Outcome; - let feedbackValue = lostChangeValue.feedback as SubtitledHtml; - expect(result).toBe( - '
' + - '
    ' + - '
  • ' + - '
    ' + - '' + - 'Edits to state:' + - ' ' + component.lostChanges[0].stateName + - '
    ' + - '' + - 'Edits to property:' + - ' ' + 'default_outcome' + - '
    ' + - '
    ' + - '
    ' + - 'Added default outcome: ' + - '

    ' + - 'Destination: ' + - '' + - // eslint-disable-next-line dot-notation - lostChangeValue[ - 'dest' as keyof LostChangeValue - ] + '

    ' + - '
    ' + - 'Feedback: ' + - '' + - '' + - '
    ' + - '
    ' + - '
    ' + - '
    ' + - '
  • ' + - '
' + - '
' - ); - }); + let result = removeComments(html); + expect(result).toBe( + '
' + + '
    ' + + '
  • ' + + '
    ' + + '' + + 'Edits to state:' + + ' ' + + component.lostChanges[0].stateName + + '
    ' + + '' + + 'Edits to property:' + + ' ' + + component.lostChanges[0].propertyName + + '
    ' + + '
    ' + + ' ' + + 'Deleted answer group ' + + '' + + '
    ' + + '
    ' + + '
  • ' + + '
' + + '
' + ); + } + ); - it('should make human readable when editing a state with property' + - ' default_outcome and a change is edited', () => { - component.lostChanges = [lostChangeObjectFactory.createNew({ - cmd: 'edit_state_property', - state_name: 'Edited state name', - new_value: ( - outcomeObjectFactory.createFromBackendDict({ - dest: 'outcome 2', - feedback: { - content_id: 'feedback_2', - html: 'Html' - } as LostChangeValue, - } as OutcomeBackendDict)), - old_value: ( - outcomeObjectFactory.createFromBackendDict({ - dest: 'outcome 1', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: 'Html' - } as LostChangeValue, - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - } as OutcomeBackendDict)), - property_name: 'default_outcome' - })]; + it( + 'should make human readable when editing a state with property' + + ' default_outcome and a change is added', + () => { + component.lostChanges = [ + lostChangeObjectFactory.createNew({ + cmd: 'edit_state_property', + state_name: 'Edited state name', + new_value: outcomeObjectFactory.createFromBackendDict({ + dest: 'outcome 2', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: 'Html', + } as LostChangeValue, + } as OutcomeBackendDict), + old_value: {} as LostChangeValue, + property_name: 'default_outcome', + }), + ]; - fixture.detectChanges(); + fixture.detectChanges(); - let html = fixture.debugElement.nativeElement - .querySelector('.oppia-lost-changes').outerHTML; + let html = fixture.debugElement.nativeElement.querySelector( + '.oppia-lost-changes' + ).outerHTML; - let result = removeComments(html); - let lostChangeValue = ( - component.lostChanges[0].newValue as LostChangeValue); - expect(result).toBe( - '
' + - '
    ' + - '
  • ' + - '
    ' + - '' + - 'Edits to state:' + - ' ' + component.lostChanges[0].stateName + - '
    ' + - '' + - 'Edits to property:' + - ' ' + 'default_outcome' + - '
    ' + - '
    ' + - '
    ' + - 'Edited default outcome: ' + - '

    ' + - 'Destination: ' + - '' + - // eslint-disable-next-line dot-notation - lostChangeValue[ - 'dest' as keyof LostChangeValue - ] + '

    ' + - '
    ' + - '
    ' + - '
    ' + - '
  • ' + - '
' + - '
' - ); - }); + let result = removeComments(html); + let lostChangeValue = component.lostChanges[0] + .newValue as LostChangeValue as Outcome; + let feedbackValue = lostChangeValue.feedback as SubtitledHtml; + expect(result).toBe( + '
' + + '
    ' + + '
  • ' + + '
    ' + + '' + + 'Edits to state:' + + ' ' + + component.lostChanges[0].stateName + + '
    ' + + '' + + 'Edits to property:' + + ' ' + + 'default_outcome' + + '
    ' + + '
    ' + + '
    ' + + 'Added default outcome: ' + + '

    ' + + 'Destination: ' + + '' + + // eslint-disable-next-line dot-notation + lostChangeValue['dest' as keyof LostChangeValue] + + '

    ' + + '
    ' + + 'Feedback: ' + + '' + + '' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
  • ' + + '
' + + '
' + ); + } + ); - it('should make human readable when editing a state with property' + - ' default_outcome and a change is deleted', () => { - component.lostChanges = [lostChangeObjectFactory.createNew({ - cmd: 'edit_state_property', - state_name: 'Edited state name', - new_value: {} as LostChangeValue, - old_value: { - outcome: outcomeObjectFactory.createFromBackendDict({ - dest: 'outcome 1', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: 'Html' - } as LostChangeValue, - } as OutcomeBackendDict), - rules: [{ - type: 'Type1', - inputs: { - input1: 'input1', - input2: 'input2' - } - }] - }, - property_name: 'default_outcome' - })]; + it( + 'should make human readable when editing a state with property' + + ' default_outcome and a change is edited', + () => { + component.lostChanges = [ + lostChangeObjectFactory.createNew({ + cmd: 'edit_state_property', + state_name: 'Edited state name', + new_value: outcomeObjectFactory.createFromBackendDict({ + dest: 'outcome 2', + feedback: { + content_id: 'feedback_2', + html: 'Html', + } as LostChangeValue, + } as OutcomeBackendDict), + old_value: outcomeObjectFactory.createFromBackendDict({ + dest: 'outcome 1', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: 'Html', + } as LostChangeValue, + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + } as OutcomeBackendDict), + property_name: 'default_outcome', + }), + ]; - fixture.detectChanges(); + fixture.detectChanges(); - let html = fixture.debugElement.nativeElement - .querySelector('.oppia-lost-changes').outerHTML; + let html = fixture.debugElement.nativeElement.querySelector( + '.oppia-lost-changes' + ).outerHTML; - let result = removeComments(html); - expect(result).toBe( - '
' + - '
    ' + - '
  • ' + - '
    ' + - '' + - 'Edits to state:' + - ' ' + component.lostChanges[0].stateName + - '
    ' + - '' + - 'Edits to property:' + - ' ' + component.lostChanges[0].propertyName + - '
    ' + - '
    ' + - '
    ' + - ' Deleted default outcome ' + - '
    ' + - '
    ' + - '
    ' + - '
  • ' + - '
' + - '
' - ); - }); + let result = removeComments(html); + let lostChangeValue = component.lostChanges[0] + .newValue as LostChangeValue; + expect(result).toBe( + '
' + + '
    ' + + '
  • ' + + '
    ' + + '' + + 'Edits to state:' + + ' ' + + component.lostChanges[0].stateName + + '
    ' + + '' + + 'Edits to property:' + + ' ' + + 'default_outcome' + + '
    ' + + '
    ' + + '
    ' + + 'Edited default outcome: ' + + '

    ' + + 'Destination: ' + + '' + + // eslint-disable-next-line dot-notation + lostChangeValue['dest' as keyof LostChangeValue] + + '

    ' + + '
    ' + + '
    ' + + '
    ' + + '
  • ' + + '
' + + '
' + ); + } + ); + + it( + 'should make human readable when editing a state with property' + + ' default_outcome and a change is deleted', + () => { + component.lostChanges = [ + lostChangeObjectFactory.createNew({ + cmd: 'edit_state_property', + state_name: 'Edited state name', + new_value: {} as LostChangeValue, + old_value: { + outcome: outcomeObjectFactory.createFromBackendDict({ + dest: 'outcome 1', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: 'Html', + } as LostChangeValue, + } as OutcomeBackendDict), + rules: [ + { + type: 'Type1', + inputs: { + input1: 'input1', + input2: 'input2', + }, + }, + ], + }, + property_name: 'default_outcome', + }), + ]; + + fixture.detectChanges(); + + let html = fixture.debugElement.nativeElement.querySelector( + '.oppia-lost-changes' + ).outerHTML; + + let result = removeComments(html); + expect(result).toBe( + '
' + + '
    ' + + '
  • ' + + '
    ' + + '' + + 'Edits to state:' + + ' ' + + component.lostChanges[0].stateName + + '
    ' + + '' + + 'Edits to property:' + + ' ' + + component.lostChanges[0].propertyName + + '
    ' + + '
    ' + + '
    ' + + ' Deleted default outcome ' + + '
    ' + + '
    ' + + '
    ' + + '
  • ' + + '
' + + '
' + ); + } + ); }); diff --git a/core/templates/pages/exploration-editor-page/changes-in-human-readable-form/changes-in-human-readable-form.component.ts b/core/templates/pages/exploration-editor-page/changes-in-human-readable-form/changes-in-human-readable-form.component.ts index 93de8c8155b5..d9e78a99ae80 100644 --- a/core/templates/pages/exploration-editor-page/changes-in-human-readable-form/changes-in-human-readable-form.component.ts +++ b/core/templates/pages/exploration-editor-page/changes-in-human-readable-form/changes-in-human-readable-form.component.ts @@ -16,12 +16,12 @@ * @fileoverview Component to get changes in human readable form. */ -import { Component, Input } from '@angular/core'; -import { LostChange } from 'domain/exploration/LostChangeObjectFactory'; +import {Component, Input} from '@angular/core'; +import {LostChange} from 'domain/exploration/LostChangeObjectFactory'; @Component({ selector: 'oppia-changes-in-human-readable-form', - templateUrl: './changes-in-human-readable-form.component.html' + templateUrl: './changes-in-human-readable-form.component.html', }) export class ChangesInHumanReadableFormComponent { // This property is initialized using Angular lifecycle hooks diff --git a/core/templates/pages/exploration-editor-page/editor-navigation/editor-navbar-breadcrumb.component.spec.ts b/core/templates/pages/exploration-editor-page/editor-navigation/editor-navbar-breadcrumb.component.spec.ts index 7676a28dc724..352e59222bb3 100644 --- a/core/templates/pages/exploration-editor-page/editor-navigation/editor-navbar-breadcrumb.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/editor-navigation/editor-navbar-breadcrumb.component.spec.ts @@ -16,19 +16,25 @@ * @fileoverview Unit tests for editorNavbarBreadcrumb. */ -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed, fakeAsync, tick } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { ExplorationTitleService } from '../services/exploration-title.service'; -import { RouterService } from '../services/router.service'; -import { EditorNavbarBreadcrumbComponent } from './editor-navbar-breadcrumb.component'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + waitForAsync, + TestBed, + fakeAsync, + tick, +} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {ExplorationTitleService} from '../services/exploration-title.service'; +import {RouterService} from '../services/router.service'; +import {EditorNavbarBreadcrumbComponent} from './editor-navbar-breadcrumb.component'; class MockNgbModal { - open(): { result: Promise } { + open(): {result: Promise} { return { - result: Promise.resolve() + result: Promise.resolve(), }; } } @@ -44,19 +50,17 @@ describe('Editor Navbar Breadcrumb component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - EditorNavbarBreadcrumbComponent - ], + declarations: [EditorNavbarBreadcrumbComponent], providers: [ { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, ExplorationTitleService, FocusManagerService, - RouterService + RouterService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -72,8 +76,8 @@ describe('Editor Navbar Breadcrumb component', () => { spyOnProperty( explorationTitleService, - 'onExplorationPropertyChanged').and.returnValue( - mockExplorationPropertyChangedEventEmitter); + 'onExplorationPropertyChanged' + ).and.returnValue(mockExplorationPropertyChangedEventEmitter); component.ngOnInit(); }); @@ -82,39 +86,40 @@ describe('Editor Navbar Breadcrumb component', () => { component.ngOnDestroy(); }); - it('should initialize component properties after controller is initialized', + it('should initialize component properties after controller is initialized', () => { + expect(component.navbarTitle).toBeUndefined(); + }); + + it( + 'should go to settings tabs and focus on exploration title input' + + ' when editing title', () => { - expect(component.navbarTitle).toBeUndefined(); - }); + spyOn(routerService, 'navigateToSettingsTab'); + spyOn(focusManagerService, 'setFocus'); - it('should go to settings tabs and focus on exploration title input' + - ' when editing title', () => { - spyOn(routerService, 'navigateToSettingsTab'); - spyOn(focusManagerService, 'setFocus'); + component.editTitle(); - component.editTitle(); + expect(routerService.navigateToSettingsTab).toHaveBeenCalled(); + expect(focusManagerService.setFocus).toHaveBeenCalledWith( + 'explorationTitleInputFocusLabel' + ); + } + ); - expect(routerService.navigateToSettingsTab).toHaveBeenCalled(); - expect(focusManagerService.setFocus).toHaveBeenCalledWith( - 'explorationTitleInputFocusLabel'); + it('should get an empty current tab name when there is no active tab', () => { + spyOn(routerService, 'getActiveTabName').and.returnValue(''); + expect(component.getCurrentTabName()).toBe(''); }); - it('should get an empty current tab name when there is no active tab', - () => { - spyOn(routerService, 'getActiveTabName').and.returnValue(''); - expect(component.getCurrentTabName()).toBe(''); - }); - it('should get current tab name when there is an active tab', () => { spyOn(routerService, 'getActiveTabName').and.returnValue('settings'); expect(component.getCurrentTabName()).toBe('Settings'); }); - it('should update nav bar title when exploration property changes', - fakeAsync(() => { - mockExplorationPropertyChangedEventEmitter.emit('title'); - tick(); + it('should update nav bar title when exploration property changes', fakeAsync(() => { + mockExplorationPropertyChangedEventEmitter.emit('title'); + tick(); - expect(component.navbarTitle).toBe('Exploration Title...'); - })); + expect(component.navbarTitle).toBe('Exploration Title...'); + })); }); diff --git a/core/templates/pages/exploration-editor-page/editor-navigation/editor-navbar-breadcrumb.component.ts b/core/templates/pages/exploration-editor-page/editor-navigation/editor-navbar-breadcrumb.component.ts index f09c14c4955a..2a70bbb0793c 100644 --- a/core/templates/pages/exploration-editor-page/editor-navigation/editor-navbar-breadcrumb.component.ts +++ b/core/templates/pages/exploration-editor-page/editor-navigation/editor-navbar-breadcrumb.component.ts @@ -17,17 +17,17 @@ * in editor navbar. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { Subscription } from 'rxjs'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { ExplorationEditorPageConstants } from '../exploration-editor-page.constants'; -import { ExplorationTitleService } from '../services/exploration-title.service'; -import { RouterService } from '../services/router.service'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {Subscription} from 'rxjs'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {ExplorationEditorPageConstants} from '../exploration-editor-page.constants'; +import {ExplorationTitleService} from '../services/exploration-title.service'; +import {RouterService} from '../services/router.service'; @Component({ selector: 'oppia-editor-navbar-breadcrumb', - templateUrl: './editor-navbar-breadcrumb.component.html' + templateUrl: './editor-navbar-breadcrumb.component.html', }) export class EditorNavbarBreadcrumbComponent implements OnInit, OnDestroy { directiveSubscriptions = new Subscription(); @@ -49,25 +49,25 @@ export class EditorNavbarBreadcrumbComponent implements OnInit, OnDestroy { constructor( private explorationTitleService: ExplorationTitleService, private focusManagerService: FocusManagerService, - private routerService: RouterService, + private routerService: RouterService ) {} editTitle(): void { this.routerService.navigateToSettingsTab(); this.focusManagerService.setFocus( - ExplorationEditorPageConstants.EXPLORATION_TITLE_INPUT_FOCUS_LABEL); + ExplorationEditorPageConstants.EXPLORATION_TITLE_INPUT_FOCUS_LABEL + ); } getCurrentTabName(): string { const that = this; - type TabNamesToHumanReadableNamesKeys = ( - keyof typeof that._TAB_NAMES_TO_HUMAN_READABLE_NAMES); + type TabNamesToHumanReadableNamesKeys = + keyof typeof that._TAB_NAMES_TO_HUMAN_READABLE_NAMES; if (!this.routerService.getActiveTabName()) { return ''; } else { return this._TAB_NAMES_TO_HUMAN_READABLE_NAMES[ - this.routerService.getActiveTabName() as - TabNamesToHumanReadableNamesKeys + this.routerService.getActiveTabName() as TabNamesToHumanReadableNamesKeys ]; } } @@ -75,13 +75,12 @@ export class EditorNavbarBreadcrumbComponent implements OnInit, OnDestroy { ngOnInit(): void { this.directiveSubscriptions.add( this.explorationTitleService.onExplorationPropertyChanged.subscribe( - (propertyName) => { + propertyName => { const _MAX_TITLE_LENGTH = 20; this.navbarTitle = String(this.explorationTitleService.savedMemento); if (this.navbarTitle.length > _MAX_TITLE_LENGTH) { - this.navbarTitle = ( - this.navbarTitle.substring( - 0, _MAX_TITLE_LENGTH - 3) + '...'); + this.navbarTitle = + this.navbarTitle.substring(0, _MAX_TITLE_LENGTH - 3) + '...'; } } ) @@ -93,7 +92,9 @@ export class EditorNavbarBreadcrumbComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive('oppiaEditorNavbarBreadcrumb', +angular.module('oppia').directive( + 'oppiaEditorNavbarBreadcrumb', downgradeComponent({ - component: EditorNavbarBreadcrumbComponent - }) as angular.IDirectiveFactory); + component: EditorNavbarBreadcrumbComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-editor-page/editor-navigation/editor-navigation.component.spec.ts b/core/templates/pages/exploration-editor-page/editor-navigation/editor-navigation.component.spec.ts index f19130cf374f..18b53d7523d4 100644 --- a/core/templates/pages/exploration-editor-page/editor-navigation/editor-navigation.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/editor-navigation/editor-navigation.component.spec.ts @@ -16,29 +16,37 @@ * @fileoverview Unit tests for editorNavigation. */ -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, discardPeriodicTasks, fakeAsync, flush, flushMicrotasks, TestBed, tick } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { Subscription } from 'rxjs'; -import { NgbModal, NgbModalRef, NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { RouterService } from '../services/router.service'; -import { UserService } from 'services/user.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { ChangeListService } from '../services/change-list.service'; -import { HelpModalComponent } from '../modal-templates/help-modal.component'; -import { ContextService } from 'services/context.service'; -import { ExplorationFeaturesService } from 'services/exploration-features.service'; -import { ExplorationImprovementsService } from 'services/exploration-improvements.service'; -import { InternetConnectivityService } from 'services/internet-connectivity.service'; -import { ThreadDataBackendApiService } from '../feedback-tab/services/thread-data-backend-api.service'; -import { ExplorationWarningsService } from '../services/exploration-warnings.service'; -import { StateTutorialFirstTimeService } from '../services/state-tutorial-first-time.service'; -import { ExplorationRightsService } from '../services/exploration-rights.service'; -import { EditabilityService } from 'services/editability.service'; -import { ExplorationSaveService } from '../services/exploration-save.service'; -import { EditorNavigationComponent } from './editor-navigation.component'; -import { UserInfo } from 'domain/user/user-info.model'; -import { UserExplorationPermissionsService } from '../services/user-exploration-permissions.service'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + discardPeriodicTasks, + fakeAsync, + flush, + flushMicrotasks, + TestBed, + tick, +} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {Subscription} from 'rxjs'; +import {NgbModal, NgbModalRef, NgbModule} from '@ng-bootstrap/ng-bootstrap'; +import {RouterService} from '../services/router.service'; +import {UserService} from 'services/user.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {ChangeListService} from '../services/change-list.service'; +import {HelpModalComponent} from '../modal-templates/help-modal.component'; +import {ContextService} from 'services/context.service'; +import {ExplorationFeaturesService} from 'services/exploration-features.service'; +import {ExplorationImprovementsService} from 'services/exploration-improvements.service'; +import {InternetConnectivityService} from 'services/internet-connectivity.service'; +import {ThreadDataBackendApiService} from '../feedback-tab/services/thread-data-backend-api.service'; +import {ExplorationWarningsService} from '../services/exploration-warnings.service'; +import {StateTutorialFirstTimeService} from '../services/state-tutorial-first-time.service'; +import {ExplorationRightsService} from '../services/exploration-rights.service'; +import {EditabilityService} from 'services/editability.service'; +import {ExplorationSaveService} from '../services/exploration-save.service'; +import {EditorNavigationComponent} from './editor-navigation.component'; +import {UserInfo} from 'domain/user/user-info.model'; +import {UserExplorationPermissionsService} from '../services/user-exploration-permissions.service'; describe('Editor Navigation Component', () => { let component: EditorNavigationComponent; @@ -62,8 +70,9 @@ describe('Editor Navigation Component', () => { let testSubscriptions: Subscription; const openEditorTutorialSpy = jasmine.createSpy('openEditorTutorial'); - const openTranslationTutorialSpy = ( - jasmine.createSpy('openTranslationTutorial')); + const openTranslationTutorialSpy = jasmine.createSpy( + 'openTranslationTutorial' + ); let explorationId = 'exp1'; let isImprovementsTabEnabledAsyncSpy: jasmine.Spy; @@ -79,7 +88,7 @@ describe('Editor Navigation Component', () => { class MockUserService { getUserInfoAsync(): Promise { return Promise.resolve({ - isLoggedIn: () => true + isLoggedIn: () => true, } as UserInfo); } } @@ -107,7 +116,7 @@ describe('Editor Navigation Component', () => { class MockNgbModal { open() { return { - result: Promise.resolve() + result: Promise.resolve(), }; } } @@ -115,50 +124,44 @@ describe('Editor Navigation Component', () => { class MockUserExplorationPermissionsService { getPermissionsAsync() { return Promise.resolve({ - canPublish: true + canPublish: true, }); } } beforeEach(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - NgbModule - ], - declarations: [ - EditorNavigationComponent, - HelpModalComponent, - ], + imports: [HttpClientTestingModule, NgbModule], + declarations: [EditorNavigationComponent, HelpModalComponent], providers: [ ChangeListService, RouterService, { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, { provide: UserExplorationPermissionsService, - useClass: MockUserExplorationPermissionsService + useClass: MockUserExplorationPermissionsService, }, { provide: InternetConnectivityService, - useClass: MockInternetConnectivityService + useClass: MockInternetConnectivityService, }, { provide: UserService, - useClass: MockUserService + useClass: MockUserService, }, { provide: StateTutorialFirstTimeService, - useClass: MockStateTutorialFirstTimeService + useClass: MockStateTutorialFirstTimeService, }, { provide: WindowDimensionsService, - useClass: MockWindowDimensionsService - } + useClass: MockWindowDimensionsService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }); }); @@ -189,17 +192,20 @@ describe('Editor Navigation Component', () => { editabilityService = TestBed.inject(EditabilityService); explorationFeaturesService = TestBed.inject(ExplorationFeaturesService); explorationImprovementsService = TestBed.inject( - ExplorationImprovementsService); + ExplorationImprovementsService + ); explorationWarningsService = TestBed.inject(ExplorationWarningsService); - threadDataBackendApiService = ( - TestBed.inject(ThreadDataBackendApiService)); - stateTutorialFirstTimeService = ( - TestBed.inject(StateTutorialFirstTimeService)); + threadDataBackendApiService = TestBed.inject(ThreadDataBackendApiService); + stateTutorialFirstTimeService = TestBed.inject( + StateTutorialFirstTimeService + ); spyOn(contextService, 'getExplorationId').and.returnValue(explorationId); isImprovementsTabEnabledAsyncSpy = spyOn( - explorationImprovementsService, 'isImprovementsTabEnabledAsync'); + explorationImprovementsService, + 'isImprovementsTabEnabledAsync' + ); isImprovementsTabEnabledAsyncSpy.and.returnValue(Promise.resolve(false)); }); @@ -208,10 +214,14 @@ describe('Editor Navigation Component', () => { testSubscriptions = new Subscription(); testSubscriptions.add( stateTutorialFirstTimeService.onOpenTranslationTutorial.subscribe( - openTranslationTutorialSpy)); + openTranslationTutorialSpy + ) + ); testSubscriptions.add( stateTutorialFirstTimeService.onOpenEditorTutorial.subscribe( - openEditorTutorialSpy)); + openEditorTutorialSpy + ) + ); }); afterEach(() => { @@ -219,31 +229,34 @@ describe('Editor Navigation Component', () => { component.ngOnDestroy(); }); - it('should initialize component properties after controller is initialized', - () => { - spyOn(explorationRightsService, 'isPrivate').and.returnValue(true); - component.postTutorialHelpPopoverIsShown = false; - component.userIsLoggedIn = true; - component.improvementsTabIsEnabled = false; - component.isPublishButtonEnabled = true; - - expect(component.isPostTutorialHelpPopoverShown()) - .toBe(false); - expect(component.isUserLoggedIn()).toBe(true); - expect(component.isImprovementsTabEnabled()).toBe(false); - expect(component.showPublishButton()).toEqual(true); - }); + it('should initialize component properties after controller is initialized', () => { + spyOn(explorationRightsService, 'isPrivate').and.returnValue(true); + component.postTutorialHelpPopoverIsShown = false; + component.userIsLoggedIn = true; + component.improvementsTabIsEnabled = false; + component.isPublishButtonEnabled = true; + + expect(component.isPostTutorialHelpPopoverShown()).toBe(false); + expect(component.isUserLoggedIn()).toBe(true); + expect(component.isImprovementsTabEnabled()).toBe(false); + expect(component.showPublishButton()).toEqual(true); + }); it('should get warnings whenever has one', () => { - let warnings = [{ - type: 'ERROR' - }, { - type: 'CRITICAL' - }]; + let warnings = [ + { + type: 'ERROR', + }, + { + type: 'CRITICAL', + }, + ]; spyOn(explorationWarningsService, 'getWarnings').and.returnValue( - warnings); + warnings + ); spyOn(explorationWarningsService, 'countWarnings').and.returnValue( - warnings.length); + warnings.length + ); component.isScreenLarge(); expect(component.countWarnings()).toBe(2); @@ -251,33 +264,35 @@ describe('Editor Navigation Component', () => { expect(component.hasCriticalWarnings()).toBe(false); }); - it('should open editor tutorial after closing user help modal with mode' + - 'editor', () => { - spyOn(ngbModal, 'open').and.returnValue( - { + it( + 'should open editor tutorial after closing user help modal with mode' + + 'editor', + () => { + spyOn(ngbModal, 'open').and.returnValue({ componentInstance: new MockNgbModalRef(), - result: Promise.resolve('editor') - } as NgbModalRef - ); + result: Promise.resolve('editor'), + } as NgbModalRef); - component.showUserHelpModal(); + component.showUserHelpModal(); - expect(ngbModal.open).toHaveBeenCalled(); - }); + expect(ngbModal.open).toHaveBeenCalled(); + } + ); - it('should open editor tutorial after closing user help modal with mode' + - 'translation', () => { - spyOn(ngbModal, 'open').and.returnValue( - { - componentInstance: new MockNgbModalRef(), - result: Promise.resolve('translation') - } as NgbModalRef - ); + it( + 'should open editor tutorial after closing user help modal with mode' + + 'translation', + () => { + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: new MockNgbModalRef(), + result: Promise.resolve('translation'), + } as NgbModalRef); - component.showUserHelpModal(); + component.showUserHelpModal(); - expect(ngbModal.open).toHaveBeenCalled(); - }); + expect(ngbModal.open).toHaveBeenCalled(); + } + ); it('should return if exploration is private', () => { spyOn(explorationRightsService, 'isPrivate').and.returnValue(true); @@ -285,20 +300,20 @@ describe('Editor Navigation Component', () => { }); it('should return if exploration is locked for editing', () => { - spyOn( - changeListService, - 'isExplorationLockedForEditing').and.returnValue(true); + spyOn(changeListService, 'isExplorationLockedForEditing').and.returnValue( + true + ); expect(component.isExplorationLockedForEditing()).toEqual(true); }); - it('should return if exploration is editable outside tutorial mode', - () => { - spyOn( - editabilityService, - 'isEditableOutsideTutorialMode').and.returnValue(true); - spyOn(editabilityService, 'isTranslatable').and.returnValue(true); - expect(component.isEditableOutsideTutorialMode()).toEqual(true); - }); + it('should return if exploration is editable outside tutorial mode', () => { + spyOn( + editabilityService, + 'isEditableOutsideTutorialMode' + ).and.returnValue(true); + spyOn(editabilityService, 'isTranslatable').and.returnValue(true); + expect(component.isEditableOutsideTutorialMode()).toEqual(true); + }); it('should call exploration save service to discard changes', () => { let explorationSpy = spyOn(explorationSaveService, 'discardChanges'); @@ -310,7 +325,8 @@ describe('Editor Navigation Component', () => { let deferred = Promise.resolve(); let explorationSpy = spyOn( explorationSaveService, - 'saveChangesAsync').and.returnValue(deferred); + 'saveChangesAsync' + ).and.returnValue(deferred); component.saveChanges(); expect(explorationSpy).toHaveBeenCalled(); @@ -324,8 +340,9 @@ describe('Editor Navigation Component', () => { }); it('should return if exploration is saveable', () => { - spyOn( - explorationSaveService, 'isExplorationSaveable').and.returnValue(true); + spyOn(explorationSaveService, 'isExplorationSaveable').and.returnValue( + true + ); expect(component.isExplorationSaveable()).toEqual(true); }); @@ -338,28 +355,26 @@ describe('Editor Navigation Component', () => { }); it('should return the number of changes', () => { - spyOn( - changeListService, 'getChangeList').and.returnValue([]); + spyOn(changeListService, 'getChangeList').and.returnValue([]); expect(component.getChangeListLength()).toEqual(0); }); - it('should hide loading dots after publishing the exploration', fakeAsync( - () => { - component.loadingDotsAreShown = true; - let deferred = Promise.resolve(); - spyOn( - explorationSaveService, - 'showPublishExplorationModal').and.returnValue(deferred); + it('should hide loading dots after publishing the exploration', fakeAsync(() => { + component.loadingDotsAreShown = true; + let deferred = Promise.resolve(); + spyOn( + explorationSaveService, + 'showPublishExplorationModal' + ).and.returnValue(deferred); - component.showPublishExplorationModal(); - tick(); + component.showPublishExplorationModal(); + tick(); - expect(component.loadingDotsAreShown).toEqual(false); - })); + expect(component.loadingDotsAreShown).toEqual(false); + })); it('should navigate to main tab when clicking on tab', () => { - spyOn(routerService, 'getActiveTabName') - .and.returnValue('main'); + spyOn(routerService, 'getActiveTabName').and.returnValue('main'); component.selectMainTab(''); @@ -367,8 +382,7 @@ describe('Editor Navigation Component', () => { }); it('should navigate to translation tab when clicking on tab', () => { - spyOn(routerService, 'getActiveTabName') - .and.returnValue('translation'); + spyOn(routerService, 'getActiveTabName').and.returnValue('translation'); component.selectTranslationTab(); @@ -376,8 +390,7 @@ describe('Editor Navigation Component', () => { }); it('should navigate to preview tab when clicking on tab', fakeAsync(() => { - spyOn(routerService, 'getActiveTabName') - .and.returnValue('preview'); + spyOn(routerService, 'getActiveTabName').and.returnValue('preview'); component.selectPreviewTab(); @@ -389,8 +402,7 @@ describe('Editor Navigation Component', () => { })); it('should navigate to settings tab when clicking on tab', () => { - spyOn(routerService, 'getActiveTabName') - .and.returnValue('settings'); + spyOn(routerService, 'getActiveTabName').and.returnValue('settings'); component.selectSettingsTab(); @@ -398,8 +410,7 @@ describe('Editor Navigation Component', () => { }); it('should navigate to stats tab when clicking on tab', () => { - spyOn(routerService, 'getActiveTabName') - .and.returnValue('stats'); + spyOn(routerService, 'getActiveTabName').and.returnValue('stats'); component.selectStatsTab(); @@ -407,8 +418,7 @@ describe('Editor Navigation Component', () => { }); it('should navigate to improvements tab when clicking on tab', () => { - spyOn(routerService, 'getActiveTabName') - .and.returnValue('improvements'); + spyOn(routerService, 'getActiveTabName').and.returnValue('improvements'); spyOn(explorationFeaturesService, 'isInitialized').and.returnValue(true); isImprovementsTabEnabledAsyncSpy.and.returnValue(Promise.resolve(true)); @@ -418,8 +428,7 @@ describe('Editor Navigation Component', () => { }); it('should navigate to history tab when clicking on tab', () => { - spyOn(routerService, 'getActiveTabName') - .and.returnValue('history'); + spyOn(routerService, 'getActiveTabName').and.returnValue('history'); component.selectHistoryTab(); @@ -427,8 +436,7 @@ describe('Editor Navigation Component', () => { }); it('should navigate to feedback tab when clicking on tab', () => { - spyOn(routerService, 'getActiveTabName') - .and.returnValue('feedback'); + spyOn(routerService, 'getActiveTabName').and.returnValue('feedback'); component.selectFeedbackTab(); @@ -436,53 +444,50 @@ describe('Editor Navigation Component', () => { }); it('should get open thread count', () => { - spyOn( - threadDataBackendApiService, 'getOpenThreadsCount').and.returnValue(5); + spyOn(threadDataBackendApiService, 'getOpenThreadsCount').and.returnValue( + 5 + ); mockOpenPostTutorialHelpPopover.emit(); expect(component.getOpenThreadsCount()).toBe(5); }); - it('should toggle post tutorial help popover when resizing page', - fakeAsync(() => { - mockGetResizeEvent.emit(new Event('resize')); - mockOpenPostTutorialHelpPopover.emit(); + it('should toggle post tutorial help popover when resizing page', fakeAsync(() => { + mockGetResizeEvent.emit(new Event('resize')); + mockOpenPostTutorialHelpPopover.emit(); - expect(component.postTutorialHelpPopoverIsShown).toBe(true); + expect(component.postTutorialHelpPopoverIsShown).toBe(true); - tick(); - flush(); - flushMicrotasks(); + tick(); + flush(); + flushMicrotasks(); - expect(component.postTutorialHelpPopoverIsShown).toBe(false); - })); + expect(component.postTutorialHelpPopoverIsShown).toBe(false); + })); - it('should toggle post tutorial help popover when resizing page', - fakeAsync(() => { - spyOn(windowDimensionsService, 'getWidth').and.returnValue(10); + it('should toggle post tutorial help popover when resizing page', fakeAsync(() => { + spyOn(windowDimensionsService, 'getWidth').and.returnValue(10); - mockGetResizeEvent.emit(new Event('resize')); - mockOpenPostTutorialHelpPopover.emit(); + mockGetResizeEvent.emit(new Event('resize')); + mockOpenPostTutorialHelpPopover.emit(); - expect(component.postTutorialHelpPopoverIsShown).toBe(false); - })); + expect(component.postTutorialHelpPopoverIsShown).toBe(false); + })); - it('should change connnection status to ONLINE when internet is connected', - () => { - component.connectedToInternet = false; - mockConnectionServiceEmitter.emit(true); + it('should change connnection status to ONLINE when internet is connected', () => { + component.connectedToInternet = false; + mockConnectionServiceEmitter.emit(true); - expect(component.connectedToInternet).toBe(true); - }); + expect(component.connectedToInternet).toBe(true); + }); - it('should change connnection status to OFFLINE when internet disconnects', - fakeAsync(() => { - component.connectedToInternet = true; - mockConnectionServiceEmitter.emit(false); + it('should change connnection status to OFFLINE when internet disconnects', fakeAsync(() => { + component.connectedToInternet = true; + mockConnectionServiceEmitter.emit(false); - tick(); + tick(); - expect(component.connectedToInternet).toBe(false); - })); + expect(component.connectedToInternet).toBe(false); + })); }); }); diff --git a/core/templates/pages/exploration-editor-page/editor-navigation/editor-navigation.component.ts b/core/templates/pages/exploration-editor-page/editor-navigation/editor-navigation.component.ts index 98b20e5160a9..4cc7b9ba66c0 100644 --- a/core/templates/pages/exploration-editor-page/editor-navigation/editor-navigation.component.ts +++ b/core/templates/pages/exploration-editor-page/editor-navigation/editor-navigation.component.ts @@ -17,33 +17,32 @@ * in editor. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { Subscription } from 'rxjs'; -import { HelpModalComponent } from 'pages/exploration-editor-page/modal-templates/help-modal.component'; -import { ContextService } from 'services/context.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { EditabilityService } from 'services/editability.service'; -import { ExplorationImprovementsService } from 'services/exploration-improvements.service'; -import { InternetConnectivityService } from 'services/internet-connectivity.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { UserService } from 'services/user.service'; -import { ThreadDataBackendApiService } from '../feedback-tab/services/thread-data-backend-api.service'; -import { ChangeListService } from '../services/change-list.service'; -import { ExplorationRightsService } from '../services/exploration-rights.service'; -import { ExplorationSaveService } from '../services/exploration-save.service'; -import { ExplorationWarningsService } from '../services/exploration-warnings.service'; -import { RouterService } from '../services/router.service'; -import { StateTutorialFirstTimeService } from '../services/state-tutorial-first-time.service'; -import { UserExplorationPermissionsService } from '../services/user-exploration-permissions.service'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {Subscription} from 'rxjs'; +import {HelpModalComponent} from 'pages/exploration-editor-page/modal-templates/help-modal.component'; +import {ContextService} from 'services/context.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {EditabilityService} from 'services/editability.service'; +import {ExplorationImprovementsService} from 'services/exploration-improvements.service'; +import {InternetConnectivityService} from 'services/internet-connectivity.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {UserService} from 'services/user.service'; +import {ThreadDataBackendApiService} from '../feedback-tab/services/thread-data-backend-api.service'; +import {ChangeListService} from '../services/change-list.service'; +import {ExplorationRightsService} from '../services/exploration-rights.service'; +import {ExplorationSaveService} from '../services/exploration-save.service'; +import {ExplorationWarningsService} from '../services/exploration-warnings.service'; +import {RouterService} from '../services/router.service'; +import {StateTutorialFirstTimeService} from '../services/state-tutorial-first-time.service'; +import {UserExplorationPermissionsService} from '../services/user-exploration-permissions.service'; @Component({ selector: 'oppia-editor-navigation', - templateUrl: './editor-navigation.component.html' + templateUrl: './editor-navigation.component.html', }) -export class EditorNavigationComponent - implements OnInit, OnDestroy { +export class EditorNavigationComponent implements OnInit, OnDestroy { directiveSubscriptions = new Subscription(); screenIsLarge: boolean = false; isPublishButtonEnabled: boolean = false; @@ -70,10 +69,9 @@ export class EditorNavigationComponent private siteAnalyticsService: SiteAnalyticsService, private stateTutorialFirstTimeService: StateTutorialFirstTimeService, private threadDataBackendApiService: ThreadDataBackendApiService, - private userExplorationPermissionsService: - UserExplorationPermissionsService, + private userExplorationPermissionsService: UserExplorationPermissionsService, private userService: UserService, - private windowDimensionsService: WindowDimensionsService, + private windowDimensionsService: WindowDimensionsService ) {} getChangeListLength(): number { @@ -95,7 +93,8 @@ export class EditorNavigationComponent isEditableOutsideTutorialMode(): boolean { return ( this.editabilityService.isEditableOutsideTutorialMode() || - this.editabilityService.isTranslatable()); + this.editabilityService.isTranslatable() + ); } discardChanges(): void { @@ -106,8 +105,11 @@ export class EditorNavigationComponent this.publishIsInProcess = true; this.loadingDotsAreShown = true; - this.explorationSaveService.showPublishExplorationModal( - this.showLoadingDots.bind(this), this.hideLoadingDots.bind(this)) + this.explorationSaveService + .showPublishExplorationModal( + this.showLoadingDots.bind(this), + this.hideLoadingDots.bind(this) + ) .then(() => { this.publishIsInProcess = false; this.loadingDotsAreShown = false; @@ -126,12 +128,18 @@ export class EditorNavigationComponent this.saveIsInProcess = true; this.loadingDotsAreShown = true; - this.explorationSaveService.saveChangesAsync( - this.showLoadingDots.bind(this), this.hideLoadingDots.bind(this)) - .then(() => { - this.saveIsInProcess = false; - this.loadingDotsAreShown = false; - }, () => {}); + this.explorationSaveService + .saveChangesAsync( + this.showLoadingDots.bind(this), + this.hideLoadingDots.bind(this) + ) + .then( + () => { + this.saveIsInProcess = false; + this.loadingDotsAreShown = false; + }, + () => {} + ); } toggleMobileNavOptions(): void { @@ -197,20 +205,25 @@ export class EditorNavigationComponent const EDITOR_TUTORIAL_MODE = 'editor'; const TRANSLATION_TUTORIAL_MODE = 'translation'; - this.ngbModal.open(HelpModalComponent, { - backdrop: true, - windowClass: 'oppia-help-modal' - }).result.then(mode => { - if (mode === EDITOR_TUTORIAL_MODE) { - this.stateTutorialFirstTimeService.onOpenEditorTutorial.emit(); - } else if (mode === TRANSLATION_TUTORIAL_MODE) { - this.stateTutorialFirstTimeService.onOpenTranslationTutorial.emit(); - } - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + this.ngbModal + .open(HelpModalComponent, { + backdrop: true, + windowClass: 'oppia-help-modal', + }) + .result.then( + mode => { + if (mode === EDITOR_TUTORIAL_MODE) { + this.stateTutorialFirstTimeService.onOpenEditorTutorial.emit(); + } else if (mode === TRANSLATION_TUTORIAL_MODE) { + this.stateTutorialFirstTimeService.onOpenTranslationTutorial.emit(); + } + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } isScreenLarge(): boolean { @@ -231,14 +244,15 @@ export class EditorNavigationComponent showPublishButton(): boolean { return ( - this.isPublishButtonEnabled && ( - this.explorationRightsService.isPrivate())); + this.isPublishButtonEnabled && this.explorationRightsService.isPrivate() + ); } ngOnInit(): void { - this.userExplorationPermissionsService.getPermissionsAsync() - .then((permissions) => { - this.isPublishButtonEnabled = (permissions.canPublish); + this.userExplorationPermissionsService + .getPermissionsAsync() + .then(permissions => { + this.isPublishButtonEnabled = permissions.canPublish; }); this.screenIsLarge = this.windowDimensionsService.getWidth() >= 1024; @@ -252,43 +266,42 @@ export class EditorNavigationComponent this.postTutorialHelpPopoverIsShown = false; this.directiveSubscriptions.add( - this.stateTutorialFirstTimeService - .onOpenPostTutorialHelpPopover.subscribe( - () => { - if (this.screenIsLarge) { - this.postTutorialHelpPopoverIsShown = true; - setTimeout(() => { - this.postTutorialHelpPopoverIsShown = false; - }, 4000); - } else { + this.stateTutorialFirstTimeService.onOpenPostTutorialHelpPopover.subscribe( + () => { + if (this.screenIsLarge) { + this.postTutorialHelpPopoverIsShown = true; + setTimeout(() => { this.postTutorialHelpPopoverIsShown = false; - } + }, 4000); + } else { + this.postTutorialHelpPopoverIsShown = false; } - ) + } + ) ); this.directiveSubscriptions.add( this.internetConnectivityService.onInternetStateChange.subscribe( internetAccessible => { this.connectedToInternet = internetAccessible; - }) + } + ) ); this.connectedToInternet = this.internetConnectivityService.isOnline(); this.improvementsTabIsEnabled = false; Promise.resolve( - this.explorationImprovementsService.isImprovementsTabEnabledAsync()) - .then(improvementsTabIsEnabled => { - this.improvementsTabIsEnabled = improvementsTabIsEnabled; - }); + this.explorationImprovementsService.isImprovementsTabEnabledAsync() + ).then(improvementsTabIsEnabled => { + this.improvementsTabIsEnabled = improvementsTabIsEnabled; + }); this.userIsLoggedIn = false; - Promise.resolve(this.userService.getUserInfoAsync()) - .then(userInfo => { - this.userIsLoggedIn = userInfo.isLoggedIn(); - }); + Promise.resolve(this.userService.getUserInfoAsync()).then(userInfo => { + this.userIsLoggedIn = userInfo.isLoggedIn(); + }); } ngOnDestroy(): void { @@ -296,7 +309,9 @@ export class EditorNavigationComponent } } -angular.module('oppia').directive('oppiaEditorNavigation', +angular.module('oppia').directive( + 'oppiaEditorNavigation', downgradeComponent({ - component: EditorNavigationComponent - }) as angular.IDirectiveFactory); + component: EditorNavigationComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/exploration-editor-tab.component.spec.ts b/core/templates/pages/exploration-editor-page/editor-tab/exploration-editor-tab.component.spec.ts index b9d45d38a8ad..40145cf9502c 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/exploration-editor-tab.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/exploration-editor-tab.component.spec.ts @@ -16,41 +16,61 @@ * @fileoverview Unit tests for the component of the 'State Editor'. */ -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { fakeAsync, TestBed, tick, flush, ComponentFixture, waitForAsync } from '@angular/core/testing'; -import { AnswerGroupObjectFactory } from 'domain/exploration/AnswerGroupObjectFactory'; -import { ExplorationFeaturesService } from 'services/exploration-features.service'; -import { Hint } from 'domain/exploration/hint-object.model'; -import { OutcomeObjectFactory } from 'domain/exploration/OutcomeObjectFactory'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { StateCardIsCheckpointService } from 'components/state-editor/state-editor-properties-services/state-card-is-checkpoint.service'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { SolutionObjectFactory } from 'domain/exploration/SolutionObjectFactory'; -import { SubtitledUnicode } from 'domain/exploration/SubtitledUnicodeObjectFactory'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { ExplorationDataService } from '../services/exploration-data.service'; -import { GenerateContentIdService } from 'services/generate-content-id.service'; -import { EditabilityService } from 'services/editability.service'; -import { ExplorationInitStateNameService } from '../services/exploration-init-state-name.service'; -import { ExplorationStatesService } from '../services/exploration-states.service'; -import { ExplorationWarningsService } from '../services/exploration-warnings.service'; -import { RouterService } from '../services/router.service'; -import { StateEditorRefreshService } from '../services/state-editor-refresh.service'; -import { UserExplorationPermissionsService } from '../services/user-exploration-permissions.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { FormsModule } from '@angular/forms'; -import { ExplorationEditorTabComponent } from './exploration-editor-tab.component'; -import { DomRefService, JoyrideDirective, JoyrideOptionsService, JoyrideService, JoyrideStepsContainerService, JoyrideStepService, LoggerService, TemplatesService } from 'ngx-joyride'; -import { Router } from '@angular/router'; -import { ExplorationPermissions } from 'domain/exploration/exploration-permissions.model'; -import { State, StateBackendDict, StateObjectFactory } from 'domain/state/StateObjectFactory'; -import { Interaction } from 'domain/exploration/InteractionObjectFactory'; -import { ContextService } from 'services/context.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { ExplorationNextContentIdIndexService } from '../services/exploration-next-content-id-index.service'; -import { VersionHistoryService } from '../services/version-history.service'; -import { VersionHistoryBackendApiService } from '../services/version-history-backend-api.service'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + fakeAsync, + TestBed, + tick, + flush, + ComponentFixture, + waitForAsync, +} from '@angular/core/testing'; +import {AnswerGroupObjectFactory} from 'domain/exploration/AnswerGroupObjectFactory'; +import {ExplorationFeaturesService} from 'services/exploration-features.service'; +import {Hint} from 'domain/exploration/hint-object.model'; +import {OutcomeObjectFactory} from 'domain/exploration/OutcomeObjectFactory'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {StateCardIsCheckpointService} from 'components/state-editor/state-editor-properties-services/state-card-is-checkpoint.service'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {SolutionObjectFactory} from 'domain/exploration/SolutionObjectFactory'; +import {SubtitledUnicode} from 'domain/exploration/SubtitledUnicodeObjectFactory'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {ExplorationDataService} from '../services/exploration-data.service'; +import {GenerateContentIdService} from 'services/generate-content-id.service'; +import {EditabilityService} from 'services/editability.service'; +import {ExplorationInitStateNameService} from '../services/exploration-init-state-name.service'; +import {ExplorationStatesService} from '../services/exploration-states.service'; +import {ExplorationWarningsService} from '../services/exploration-warnings.service'; +import {RouterService} from '../services/router.service'; +import {StateEditorRefreshService} from '../services/state-editor-refresh.service'; +import {UserExplorationPermissionsService} from '../services/user-exploration-permissions.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {FormsModule} from '@angular/forms'; +import {ExplorationEditorTabComponent} from './exploration-editor-tab.component'; +import { + DomRefService, + JoyrideDirective, + JoyrideOptionsService, + JoyrideService, + JoyrideStepsContainerService, + JoyrideStepService, + LoggerService, + TemplatesService, +} from 'ngx-joyride'; +import {Router} from '@angular/router'; +import {ExplorationPermissions} from 'domain/exploration/exploration-permissions.model'; +import { + State, + StateBackendDict, + StateObjectFactory, +} from 'domain/state/StateObjectFactory'; +import {Interaction} from 'domain/exploration/InteractionObjectFactory'; +import {ContextService} from 'services/context.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {ExplorationNextContentIdIndexService} from '../services/exploration-next-content-id-index.service'; +import {VersionHistoryService} from '../services/version-history.service'; +import {VersionHistoryBackendApiService} from '../services/version-history-backend-api.service'; describe('Exploration editor tab component', () => { let component: ExplorationEditorTabComponent; @@ -72,8 +92,7 @@ describe('Exploration editor tab component', () => { let focusManagerService: FocusManagerService; let contextService: ContextService; var generateContentIdService: GenerateContentIdService; - var explorationNextContentIdIndexService: - ExplorationNextContentIdIndexService; + var explorationNextContentIdIndexService: ExplorationNextContentIdIndexService; let mockRefreshStateEditorEventEmitter = null; let versionHistoryService: VersionHistoryService; let stateObjectFactory: StateObjectFactory; @@ -90,7 +109,7 @@ describe('Exploration editor tab component', () => { value1({number: 8}); value2(); value3(); - } + }, }; } @@ -98,18 +117,18 @@ describe('Exploration editor tab component', () => { } class MockWindowRef { - location = { path: '/create/2234' }; + location = {path: '/create/2234'}; nativeWindow = { scrollTo: (value1, value2) => {}, sessionStorage: { promoIsDismissed: null, setItem: (testKey1, testKey2) => {}, - removeItem: (testKey) => {} + removeItem: testKey => {}, }, gtag: (value1, value2, value3) => {}, navigator: { onLine: true, - userAgent: null + userAgent: null, }, location: { path: '/create/2234', @@ -131,11 +150,11 @@ describe('Exploration editor tab component', () => { clientWidth: null, clientHeight: null, style: { - overflowY: '' - } - } + overflowY: '', + }, + }, }, - addEventListener: (value1, value2) => {} + addEventListener: (value1, value2) => {}, }; } @@ -143,14 +162,8 @@ describe('Exploration editor tab component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - FormsModule, - ], - declarations: [ - JoyrideDirective, - ExplorationEditorTabComponent, - ], + imports: [HttpClientTestingModule, FormsModule], + declarations: [JoyrideDirective, ExplorationEditorTabComponent], providers: [ JoyrideStepService, { @@ -160,7 +173,7 @@ describe('Exploration editor tab component', () => { TemplatesService, { provide: WindowRef, - useClass: MockWindowRef + useClass: MockWindowRef, }, JoyrideOptionsService, JoyrideStepsContainerService, @@ -176,12 +189,12 @@ describe('Exploration editor tab component', () => { explorationId: 0, autosaveChangeListAsync() { return; - } - } + }, + }, }, - VersionHistoryService + VersionHistoryService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -196,235 +209,251 @@ describe('Exploration editor tab component', () => { solutionObjectFactory = TestBed.inject(SolutionObjectFactory); focusManagerService = TestBed.inject(FocusManagerService); stateEditorService = TestBed.inject(StateEditorService); - stateCardIsCheckpointService = TestBed.inject( - StateCardIsCheckpointService); + stateCardIsCheckpointService = TestBed.inject(StateCardIsCheckpointService); editabilityService = TestBed.inject(EditabilityService); focusManagerService = TestBed.inject(FocusManagerService); explorationInitStateNameService = TestBed.inject( - ExplorationInitStateNameService); + ExplorationInitStateNameService + ); explorationStatesService = TestBed.inject(ExplorationStatesService); explorationWarningsService = TestBed.inject(ExplorationWarningsService); routerService = TestBed.inject(RouterService); siteAnalyticsService = TestBed.inject(SiteAnalyticsService); stateEditorRefreshService = TestBed.inject(StateEditorRefreshService); userExplorationPermissionsService = TestBed.inject( - UserExplorationPermissionsService); + UserExplorationPermissionsService + ); contextService = TestBed.inject(ContextService); explorationNextContentIdIndexService = TestBed.inject( - ExplorationNextContentIdIndexService); + ExplorationNextContentIdIndexService + ); versionHistoryService = TestBed.inject(VersionHistoryService); stateObjectFactory = TestBed.inject(StateObjectFactory); versionHistoryBackendApiService = TestBed.inject( - VersionHistoryBackendApiService); + VersionHistoryBackendApiService + ); mockRefreshStateEditorEventEmitter = new EventEmitter(); - spyOn(contextService, 'getExplorationId').and.returnValue( - 'explorationId'); - spyOn(stateEditorService, 'checkEventListenerRegistrationStatus') - .and.returnValue(true); + spyOn(contextService, 'getExplorationId').and.returnValue('explorationId'); + spyOn( + stateEditorService, + 'checkEventListenerRegistrationStatus' + ).and.returnValue(true); spyOn(document, 'getElementById').and.returnValue({ - offsetTop: 400 + offsetTop: 400, } as HTMLElement); spyOnProperty( - stateEditorRefreshService, 'onRefreshStateEditor').and.returnValue( - mockRefreshStateEditorEventEmitter); + stateEditorRefreshService, + 'onRefreshStateEditor' + ).and.returnValue(mockRefreshStateEditorEventEmitter); let element = document.createElement('div'); - spyOn(document, 'querySelector').and.returnValue(( - element as HTMLElement)); + spyOn(document, 'querySelector').and.returnValue(element as HTMLElement); spyOn( - versionHistoryService, 'getLatestVersionOfExploration' + versionHistoryService, + 'getLatestVersionOfExploration' ).and.returnValue(3); stateObject = { classifier_model_id: null, content: { content_id: 'content', - html: '' + html: '', }, recorded_voiceovers: { voiceovers_mapping: { content: {}, - default_outcome: {} - } + default_outcome: {}, + }, }, interaction: { answer_groups: [], confirmed_unclassified_answers: [], customization_args: { rows: { - value: 1 + value: 1, }, placeholder: { value: { unicode_str: 'Type your answer here.', - content_id: '' - } - } + content_id: '', + }, + }, }, default_outcome: { dest: '(untitled state)', dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: '' + html: '', }, param_changes: [], labelled_as_correct: false, refresher_exploration_id: null, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, hints: [], solution: null, - id: 'TextInput' + id: 'TextInput', }, linked_skill_id: null, param_changes: [], solicit_answer_details: false, - card_is_checkpoint: false + card_is_checkpoint: false, }; - explorationStatesService.init({ - 'First State': { - classifier_model_id: null, - card_is_checkpoint: true, - content: { - content_id: 'content', - html: 'First State Content' - }, - interaction: { - id: 'TextInput', - confirmed_unclassified_answers: null, - customization_args: { - placeholder: {value: { - content_id: 'ca_placeholder', - unicode_str: '' - }}, - rows: {value: 1}, - catchMisspellings: { - value: false - } + explorationStatesService.init( + { + 'First State': { + classifier_model_id: null, + card_is_checkpoint: true, + content: { + content_id: 'content', + html: 'First State Content', }, - answer_groups: [{ - rule_specs: [], - training_data: null, - tagged_skill_misconception_id: null, - outcome: { - dest: 'unused', - missing_prerequisite_skill_id: null, + interaction: { + id: 'TextInput', + confirmed_unclassified_answers: null, + customization_args: { + placeholder: { + value: { + content_id: 'ca_placeholder', + unicode_str: '', + }, + }, + rows: {value: 1}, + catchMisspellings: { + value: false, + }, + }, + answer_groups: [ + { + rule_specs: [], + training_data: null, + tagged_skill_misconception_id: null, + outcome: { + dest: 'unused', + missing_prerequisite_skill_id: null, + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: '', + }, + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, + }, + }, + ], + default_outcome: { + dest: 'default', dest_if_really_stuck: null, + missing_prerequisite_skill_id: null, feedback: { - content_id: 'feedback_1', - html: '' + content_id: 'default_outcome', + html: '', }, labelled_as_correct: false, param_changes: [], - refresher_exploration_id: null - } - }], - default_outcome: { - dest: 'default', - dest_if_really_stuck: null, - missing_prerequisite_skill_id: null, - feedback: { - content_id: 'default_outcome', - html: '' + refresher_exploration_id: null, }, - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null + solution: { + correct_answer: 'This is the correct answer', + answer_is_exclusive: false, + explanation: { + html: 'Solution explanation', + content_id: 'content_4', + }, + }, + hints: [], }, - solution: { - correct_answer: 'This is the correct answer', - answer_is_exclusive: false, - explanation: { - html: 'Solution explanation', - content_id: 'content_4' - } + linked_skill_id: null, + param_changes: [], + solicit_answer_details: false, + recorded_voiceovers: { + voiceovers_mapping: { + content: {}, + default_outcome: {}, + feedback_1: { + en: { + filename: 'myfile2.mp3', + file_size_bytes: 120000, + needs_update: false, + duration_secs: 1.2, + }, + }, + }, }, - hints: [] - }, - linked_skill_id: null, - param_changes: [], - solicit_answer_details: false, - recorded_voiceovers: { - voiceovers_mapping: { - content: {}, - default_outcome: {}, - feedback_1: { - en: { - filename: 'myfile2.mp3', - file_size_bytes: 120000, - needs_update: false, - duration_secs: 1.2 - } - } - } - } - }, - 'Second State': { - classifier_model_id: null, - card_is_checkpoint: false, - content: { - content_id: 'content', - html: 'Second State Content' }, - recorded_voiceovers: { - voiceovers_mapping: { - content: {}, - default_outcome: {}, - feedback_1: {} - } - }, - interaction: { - id: 'TextInput', - confirmed_unclassified_answers: null, - solution: null, - customization_args: { - placeholder: {value: { - content_id: 'ca_placeholder', - unicode_str: '' - }}, - rows: {value: 1}, - catchMisspellings: { - value: false - } + 'Second State': { + classifier_model_id: null, + card_is_checkpoint: false, + content: { + content_id: 'content', + html: 'Second State Content', }, - answer_groups: [{ - rule_specs: [], - training_data: null, - tagged_skill_misconception_id: null, - outcome: { - missing_prerequisite_skill_id: null, - dest: 'unused', + recorded_voiceovers: { + voiceovers_mapping: { + content: {}, + default_outcome: {}, + feedback_1: {}, + }, + }, + interaction: { + id: 'TextInput', + confirmed_unclassified_answers: null, + solution: null, + customization_args: { + placeholder: { + value: { + content_id: 'ca_placeholder', + unicode_str: '', + }, + }, + rows: {value: 1}, + catchMisspellings: { + value: false, + }, + }, + answer_groups: [ + { + rule_specs: [], + training_data: null, + tagged_skill_misconception_id: null, + outcome: { + missing_prerequisite_skill_id: null, + dest: 'unused', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: '', + }, + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, + }, + }, + ], + default_outcome: { + dest: 'default', dest_if_really_stuck: null, feedback: { - content_id: 'feedback_1', - html: '' + content_id: 'default_outcome', + html: '', }, + missing_prerequisite_skill_id: null, labelled_as_correct: false, param_changes: [], - refresher_exploration_id: null - } - }], - default_outcome: { - dest: 'default', - dest_if_really_stuck: null, - feedback: { - content_id: 'default_outcome', - html: '' + refresher_exploration_id: null, }, - missing_prerequisite_skill_id: null, - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null + hints: [], }, - hints: [] + linked_skill_id: null, + param_changes: [], + solicit_answer_details: false, }, - linked_skill_id: null, - param_changes: [], - solicit_answer_details: false - } - }, false); + }, + false + ); component.ngOnInit(); }); @@ -435,26 +464,32 @@ describe('Exploration editor tab component', () => { it('should apply autofocus to elements in active tab', () => { spyOn(routerService, 'getActiveTabName').and.returnValues( - 'main', 'feedback', 'history'); + 'main', + 'feedback', + 'history' + ); spyOn(focusManagerService, 'setFocus'); component.windowOnload(); expect(component.TabName).toBe('main'); expect(focusManagerService.setFocus).toHaveBeenCalledWith( - 'oppiaEditableSection'); + 'oppiaEditableSection' + ); component.windowOnload(); expect(component.TabName).toBe('feedback'); expect(focusManagerService.setFocus).toHaveBeenCalledWith( - 'newThreadButton'); + 'newThreadButton' + ); component.windowOnload(); expect(component.TabName).toBe('history'); expect(focusManagerService.setFocus).toHaveBeenCalledWith( - 'usernameInputField'); + 'usernameInputField' + ); }); it('should call focus method when window loads', fakeAsync(() => { @@ -468,10 +503,9 @@ describe('Exploration editor tab component', () => { expect(ctrlSpy).toHaveBeenCalled(); })); - it('should initialize controller properties after its initialization', - () => { - expect(component.interactionIsShown).toBe(false); - }); + it('should initialize controller properties after its initialization', () => { + expect(component.interactionIsShown).toBe(false); + }); it('should correctly initialize generateContentIdService', () => { explorationNextContentIdIndexService.init(5); @@ -497,29 +531,38 @@ describe('Exploration editor tab component', () => { expect(explorationNextContentIdIndexService.savedMemento).toBe(6); }); - it('should get state content placeholder text when init state name' + - ' is equal to active state name', () => { - stateEditorService.setActiveStateName('First State'); - explorationInitStateNameService.init('First State'); + it( + 'should get state content placeholder text when init state name' + + ' is equal to active state name', + () => { + stateEditorService.setActiveStateName('First State'); + explorationInitStateNameService.init('First State'); - expect(component.getStateContentPlaceholder()).toBe( - 'This is the first card of your exploration. Use this space ' + - 'to introduce your topic and engage the learner, then ask ' + - 'them a question.'); - }); + expect(component.getStateContentPlaceholder()).toBe( + 'This is the first card of your exploration. Use this space ' + + 'to introduce your topic and engage the learner, then ask ' + + 'them a question.' + ); + } + ); - it('should get state content placeholder text when init state name is' + - ' different from active state name', () => { - stateEditorService.setActiveStateName('First State'); - explorationInitStateNameService.init('Second State'); + it( + 'should get state content placeholder text when init state name is' + + ' different from active state name', + () => { + stateEditorService.setActiveStateName('First State'); + explorationInitStateNameService.init('Second State'); - expect(component.getStateContentPlaceholder()).toBe( - 'You can speak to the learner here, then ask them a question.'); - }); + expect(component.getStateContentPlaceholder()).toBe( + 'You can speak to the learner here, then ask them a question.' + ); + } + ); it('should get state content save button placeholder', () => { - expect( - component.getStateContentSaveButtonPlaceholder()).toBe('Save Content'); + expect(component.getStateContentSaveButtonPlaceholder()).toBe( + 'Save Content' + ); }); it('should add state in exploration states', () => { @@ -528,7 +571,9 @@ describe('Exploration editor tab component', () => { component.addState('Fourth State'); expect(explorationStatesService.addState).toHaveBeenCalledWith( - 'Fourth State', null); + 'Fourth State', + null + ); }); it('should refresh warnings', () => { @@ -544,56 +589,64 @@ describe('Exploration editor tab component', () => { expect(explorationStatesService.getState('First State').content).toEqual( SubtitledHtml.createFromBackendDict({ content_id: 'content', - html: 'First State Content' - })); + html: 'First State Content', + }) + ); let displayedValue = SubtitledHtml.createFromBackendDict({ content_id: 'content', - html: 'First State Content Changed' + html: 'First State Content Changed', }); component.saveStateContent(displayedValue); expect(explorationStatesService.getState('First State').content).toEqual( - displayedValue); + displayedValue + ); expect(component.interactionIsShown).toBe(true); }); - it('should save state interaction data when customization args' + - ' are changed', () => { - stateEditorService.setActiveStateName('First State'); - stateEditorService.setInteraction( - explorationStatesService.getState('First State').interaction); - - expect(stateEditorService.interaction.id).toBe('TextInput'); - expect(stateEditorService.interaction.customizationArgs).toEqual({ - rows: { value: 1 }, - placeholder: { value: new SubtitledUnicode('', 'ca_placeholder') }, - catchMisspellings: { - value: false - } - }); + it( + 'should save state interaction data when customization args' + + ' are changed', + () => { + stateEditorService.setActiveStateName('First State'); + stateEditorService.setInteraction( + explorationStatesService.getState('First State').interaction + ); - let newInteractionData = { - interactionId: 'TextInput', - customizationArgs: { - placeholder: { - value: new SubtitledUnicode('Placeholder value', 'ca_placeholder') + expect(stateEditorService.interaction.id).toBe('TextInput'); + expect(stateEditorService.interaction.customizationArgs).toEqual({ + rows: {value: 1}, + placeholder: {value: new SubtitledUnicode('', 'ca_placeholder')}, + catchMisspellings: { + value: false, }, - rows: { - value: 2 + }); + + let newInteractionData = { + interactionId: 'TextInput', + customizationArgs: { + placeholder: { + value: new SubtitledUnicode('Placeholder value', 'ca_placeholder'), + }, + rows: { + value: 2, + }, + catchMisspellings: { + value: false, + }, }, - catchMisspellings: { - value: false - } - } - }; - component.saveInteractionData(newInteractionData); + }; + component.saveInteractionData(newInteractionData); - expect(stateEditorService.interaction.id) - .toBe(newInteractionData.interactionId); - expect(stateEditorService.interaction.customizationArgs) - .toEqual(newInteractionData.customizationArgs); - }); + expect(stateEditorService.interaction.id).toBe( + newInteractionData.interactionId + ); + expect(stateEditorService.interaction.customizationArgs).toEqual( + newInteractionData.customizationArgs + ); + } + ); it('should save linked skill id', () => { stateEditorService.setActiveStateName('First State'); @@ -602,62 +655,72 @@ describe('Exploration editor tab component', () => { ).toEqual(null); component.saveLinkedSkillId('skill_id1'); - expect( - explorationStatesService.getState('First State').linkedSkillId - ).toBe('skill_id1'); + expect(explorationStatesService.getState('First State').linkedSkillId).toBe( + 'skill_id1' + ); }); it('should save interaction answer groups', () => { stateEditorService.setActiveStateName('First State'); stateEditorService.setInteraction( - explorationStatesService.getState('First State').interaction); + explorationStatesService.getState('First State').interaction + ); expect(stateEditorService.interaction.answerGroups).toEqual([ - answerGroupObjectFactory.createFromBackendDict({ - rule_specs: [], - training_data: null, - tagged_skill_misconception_id: null, - outcome: { - missing_prerequisite_skill_id: null, - dest: 'unused', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_1', - html: '' + answerGroupObjectFactory.createFromBackendDict( + { + rule_specs: [], + training_data: null, + tagged_skill_misconception_id: null, + outcome: { + missing_prerequisite_skill_id: null, + dest: 'unused', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: '', + }, + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, }, - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null - } - }, null)]); + }, + null + ), + ]); - let displayedValue = [answerGroupObjectFactory.createFromBackendDict({ - rule_specs: [], - outcome: { - missing_prerequisite_skill_id: null, - dest: 'Second State', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_1', - html: '' + let displayedValue = [ + answerGroupObjectFactory.createFromBackendDict( + { + rule_specs: [], + outcome: { + missing_prerequisite_skill_id: null, + dest: 'Second State', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: '', + }, + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, + }, + training_data: null, + tagged_skill_misconception_id: '', }, - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null - }, - training_data: null, - tagged_skill_misconception_id: '' - }, null)]; + null + ), + ]; component.saveInteractionAnswerGroups(displayedValue); - expect(stateEditorService.interaction.answerGroups) - .toEqual(displayedValue); + expect(stateEditorService.interaction.answerGroups).toEqual(displayedValue); }); it('should save interaction default outcome', () => { stateEditorService.setActiveStateName('First State'); stateEditorService.setInteraction( - explorationStatesService.getState('First State').interaction); + explorationStatesService.getState('First State').interaction + ); expect(stateEditorService.interaction.defaultOutcome).toEqual( outcomeObjectFactory.createFromBackendDict({ @@ -665,36 +728,39 @@ describe('Exploration editor tab component', () => { dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: '' + html: '', }, labelled_as_correct: false, param_changes: [], missing_prerequisite_skill_id: null, - refresher_exploration_id: null - })); + refresher_exploration_id: null, + }) + ); let displayedValue = outcomeObjectFactory.createFromBackendDict({ dest: 'Second State', dest_if_really_stuck: null, feedback: { content_id: 'default_outcome_changed', - html: 'This is the default outcome changed' + html: 'This is the default outcome changed', }, missing_prerequisite_skill_id: null, labelled_as_correct: false, param_changes: [], - refresher_exploration_id: null + refresher_exploration_id: null, }); component.saveInteractionDefaultOutcome(displayedValue); expect(stateEditorService.interaction.defaultOutcome).toEqual( - displayedValue); + displayedValue + ); }); it('should save interaction solution', () => { stateEditorService.setActiveStateName('First State'); stateEditorService.setInteraction( - explorationStatesService.getState('First State').interaction); + explorationStatesService.getState('First State').interaction + ); expect(stateEditorService.interaction.solution).toEqual( solutionObjectFactory.createFromBackendDict({ @@ -702,47 +768,50 @@ describe('Exploration editor tab component', () => { answer_is_exclusive: false, explanation: { html: 'Solution explanation', - content_id: 'content_4' - } - })); + content_id: 'content_4', + }, + }) + ); let displayedValue = solutionObjectFactory.createFromBackendDict({ correct_answer: 'This is the second correct answer', answer_is_exclusive: true, explanation: { html: 'Solution complete explanation', - content_id: 'content_4' - } + content_id: 'content_4', + }, }); component.saveSolution(displayedValue); - expect(stateEditorService.interaction.solution).toEqual( - displayedValue); + expect(stateEditorService.interaction.solution).toEqual(displayedValue); }); it('should save interaction hints', () => { stateEditorService.setActiveStateName('First State'); stateEditorService.setInteraction( - explorationStatesService.getState('First State').interaction); + explorationStatesService.getState('First State').interaction + ); expect(stateEditorService.interaction.hints).toEqual([]); - let displayedValue = [Hint.createFromBackendDict({ - hint_content: { - content_id: '', - html: 'This is a hint' - } - })]; + let displayedValue = [ + Hint.createFromBackendDict({ + hint_content: { + content_id: '', + html: 'This is a hint', + }, + }), + ]; component.saveHints(displayedValue); - expect(stateEditorService.interaction.hints).toEqual( - displayedValue); + expect(stateEditorService.interaction.hints).toEqual(displayedValue); }); it('should save solicit answer details', () => { stateEditorService.setActiveStateName('First State'); stateEditorService.setSolicitAnswerDetails( - explorationStatesService.getState('First State').solicitAnswerDetails); + explorationStatesService.getState('First State').solicitAnswerDetails + ); expect(stateEditorService.solicitAnswerDetails).toBe(false); @@ -754,7 +823,8 @@ describe('Exploration editor tab component', () => { it('should save card is checkpoint on change', () => { stateEditorService.setActiveStateName('Second State'); stateEditorService.setCardIsCheckpoint( - explorationStatesService.getState('Second State').cardIsCheckpoint); + explorationStatesService.getState('Second State').cardIsCheckpoint + ); expect(stateEditorService.cardIsCheckpoint).toBe(false); @@ -775,7 +845,9 @@ describe('Exploration editor tab component', () => { it('should evaluate if parameters are enabled', () => { let areParametersEnabledSpy = spyOn( - explorationFeaturesService, 'areParametersEnabled'); + explorationFeaturesService, + 'areParametersEnabled' + ); areParametersEnabledSpy.and.returnValue(true); expect(component.areParametersEnabled()).toBe(true); @@ -784,44 +856,55 @@ describe('Exploration editor tab component', () => { expect(component.areParametersEnabled()).toBe(false); }); - it('should correctly broadcast the stateEditorInitialized flag with ' + - 'the state data', fakeAsync(() => { - const state = new State( - 'stateName', 'id', 'some', null, - new Interaction([], [], null, null, [], 'id', null), - null, null, true, true); - component.stateName = 'stateName'; + it( + 'should correctly broadcast the stateEditorInitialized flag with ' + + 'the state data', + fakeAsync(() => { + const state = new State( + 'stateName', + 'id', + 'some', + null, + new Interaction([], [], null, null, [], 'id', null), + null, + null, + true, + true + ); + component.stateName = 'stateName'; - spyOn(explorationStatesService, 'getState').and.returnValues( - state - ); - spyOn(explorationStatesService, 'isInitialized') - .and.returnValue(true); - stateEditorService.setActiveStateName('Second State'); - stateEditorService.updateStateInteractionEditorInitialised(); - stateEditorService.updateStateResponsesInitialised(); - stateEditorService.updateStateEditorDirectiveInitialised(); - spyOn(component, 'initStateEditor').and.stub(); + spyOn(explorationStatesService, 'getState').and.returnValues(state); + spyOn(explorationStatesService, 'isInitialized').and.returnValue(true); + stateEditorService.setActiveStateName('Second State'); + stateEditorService.updateStateInteractionEditorInitialised(); + stateEditorService.updateStateResponsesInitialised(); + stateEditorService.updateStateEditorDirectiveInitialised(); + spyOn(component, 'initStateEditor').and.stub(); - mockRefreshStateEditorEventEmitter.emit(); - tick(); - component.initStateEditor(); - tick(); + mockRefreshStateEditorEventEmitter.emit(); + tick(); + component.initStateEditor(); + tick(); - expect(component.initStateEditor).toHaveBeenCalled(); - })); + expect(component.initStateEditor).toHaveBeenCalled(); + }) + ); it('should start tutorial if in tutorial mode on page load', () => { const state = new State( - 'stateName', 'id', 'some', null, + 'stateName', + 'id', + 'some', + null, new Interaction([], [], null, null, [], 'id', null), - null, null, true, true); - component.stateName = 'stateName'; - spyOn(explorationStatesService, 'getState').and.returnValues( - state + null, + null, + true, + true ); - spyOn(explorationStatesService, 'isInitialized') - .and.returnValue(true); + component.stateName = 'stateName'; + spyOn(explorationStatesService, 'getState').and.returnValues(state); + spyOn(explorationStatesService, 'isInitialized').and.returnValue(true); spyOn(component, 'startTutorial'); editabilityService.onStartTutorial(); @@ -845,33 +928,36 @@ describe('Exploration editor tab component', () => { expect(component.startTutorial).not.toHaveBeenCalled(); }); - it('should finish tutorial if finish tutorial button is clicked', - fakeAsync(() => { - let registerFinishTutorialEventSpy = ( - spyOn(siteAnalyticsService, 'registerFinishTutorialEvent')); - spyOn(editabilityService, 'onEndTutorial'); - editabilityService.onStartTutorial(); + it('should finish tutorial if finish tutorial button is clicked', fakeAsync(() => { + let registerFinishTutorialEventSpy = spyOn( + siteAnalyticsService, + 'registerFinishTutorialEvent' + ); + spyOn(editabilityService, 'onEndTutorial'); + editabilityService.onStartTutorial(); - component.initStateEditor(); - component.leaveTutorial(); - tick(); + component.initStateEditor(); + component.leaveTutorial(); + tick(); - expect(registerFinishTutorialEventSpy).toHaveBeenCalled(); - expect(editabilityService.onEndTutorial).toHaveBeenCalled(); - expect(component.tutorialInProgress).toBe(false); + expect(registerFinishTutorialEventSpy).toHaveBeenCalled(); + expect(editabilityService.onEndTutorial).toHaveBeenCalled(); + expect(component.tutorialInProgress).toBe(false); - flush(); - flush(); - })); + flush(); + flush(); + })); it('should skip tutorial if skip tutorial button is clicked', () => { spyOn(editabilityService, 'onEndTutorial'); - spyOn(userExplorationPermissionsService, 'getPermissionsAsync') - .and.returnValue( - Promise.resolve({ - canEdit: false - } as ExplorationPermissions) - ); + spyOn( + userExplorationPermissionsService, + 'getPermissionsAsync' + ).and.returnValue( + Promise.resolve({ + canEdit: false, + } as ExplorationPermissions) + ); editabilityService.onStartTutorial(); component.initStateEditor(); @@ -888,33 +974,36 @@ describe('Exploration editor tab component', () => { expect(component.getLastEditedVersionNumberInCaseOfError()).toEqual(4); }); - it('should fetch the version history data on initialization of state editor', - fakeAsync(() => { - stateEditorService.setActiveStateName('First State'); - let stateData = stateObjectFactory.createFromBackendDict( - 'State', stateObject); - spyOn( - versionHistoryBackendApiService, 'fetchStateVersionHistoryAsync' - ).and.resolveTo({ - lastEditedVersionNumber: 2, - stateNameInPreviousVersion: 'State', - stateInPreviousVersion: stateData, - lastEditedCommitterUsername: 'some' - }); + it('should fetch the version history data on initialization of state editor', fakeAsync(() => { + stateEditorService.setActiveStateName('First State'); + let stateData = stateObjectFactory.createFromBackendDict( + 'State', + stateObject + ); + spyOn( + versionHistoryBackendApiService, + 'fetchStateVersionHistoryAsync' + ).and.resolveTo({ + lastEditedVersionNumber: 2, + stateNameInPreviousVersion: 'State', + stateInPreviousVersion: stateData, + lastEditedCommitterUsername: 'some', + }); - component.initStateEditor(); - tick(); - flush(); + component.initStateEditor(); + tick(); + flush(); - expect( - versionHistoryBackendApiService.fetchStateVersionHistoryAsync - ).toHaveBeenCalled(); - })); + expect( + versionHistoryBackendApiService.fetchStateVersionHistoryAsync + ).toHaveBeenCalled(); + })); it('should show error message if the backend api fails', fakeAsync(() => { stateEditorService.setActiveStateName('First State'); spyOn( - versionHistoryBackendApiService, 'fetchStateVersionHistoryAsync' + versionHistoryBackendApiService, + 'fetchStateVersionHistoryAsync' ).and.resolveTo(null); expect(component.validationErrorIsShown).toBeFalse(); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/exploration-editor-tab.component.ts b/core/templates/pages/exploration-editor-page/editor-tab/exploration-editor-tab.component.ts index a46f00a1507f..f506d8aba7e5 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/exploration-editor-tab.component.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/exploration-editor-tab.component.ts @@ -16,356 +16,389 @@ * @fileoverview Component for the Editor tab in the exploration editor page. */ -import { Component, Input, OnDestroy, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { Subscription } from 'rxjs'; -import { JoyrideService } from 'ngx-joyride'; +import {Component, Input, OnDestroy, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {Subscription} from 'rxjs'; +import {JoyrideService} from 'ngx-joyride'; import cloneDeep from 'lodash/cloneDeep'; -import { StateTutorialFirstTimeService } from '../services/state-tutorial-first-time.service'; -import { EditabilityService } from 'services/editability.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { ExplorationStatesService } from '../services/exploration-states.service'; -import { UserExplorationPermissionsService } from '../services/user-exploration-permissions.service'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { RouterService } from '../services/router.service'; -import { ExplorationFeaturesService } from 'services/exploration-features.service'; -import { InteractionData } from 'interactions/customization-args-defs'; -import { Outcome } from 'domain/exploration/OutcomeObjectFactory'; -import { AnswerGroup } from 'domain/exploration/AnswerGroupObjectFactory'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { Hint } from 'domain/exploration/hint-object.model'; -import { Solution } from 'domain/exploration/SolutionObjectFactory'; -import { StateCardIsCheckpointService } from 'components/state-editor/state-editor-properties-services/state-card-is-checkpoint.service'; -import { ExplorationInitStateNameService } from '../services/exploration-init-state-name.service'; -import { ExplorationWarningsService } from '../services/exploration-warnings.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { StateEditorRefreshService } from '../services/state-editor-refresh.service'; -import { LoaderService } from 'services/loader.service'; -import { GraphDataService } from '../services/graph-data.service'; -import { ExplorationNextContentIdIndexService } from '../services/exploration-next-content-id-index.service'; -import { GenerateContentIdService } from 'services/generate-content-id.service'; -import { VersionHistoryService } from '../services/version-history.service'; -import { VersionHistoryBackendApiService } from '../services/version-history-backend-api.service'; -import { ContextService } from 'services/context.service'; +import {StateTutorialFirstTimeService} from '../services/state-tutorial-first-time.service'; +import {EditabilityService} from 'services/editability.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {ExplorationStatesService} from '../services/exploration-states.service'; +import {UserExplorationPermissionsService} from '../services/user-exploration-permissions.service'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {RouterService} from '../services/router.service'; +import {ExplorationFeaturesService} from 'services/exploration-features.service'; +import {InteractionData} from 'interactions/customization-args-defs'; +import {Outcome} from 'domain/exploration/OutcomeObjectFactory'; +import {AnswerGroup} from 'domain/exploration/AnswerGroupObjectFactory'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {Hint} from 'domain/exploration/hint-object.model'; +import {Solution} from 'domain/exploration/SolutionObjectFactory'; +import {StateCardIsCheckpointService} from 'components/state-editor/state-editor-properties-services/state-card-is-checkpoint.service'; +import {ExplorationInitStateNameService} from '../services/exploration-init-state-name.service'; +import {ExplorationWarningsService} from '../services/exploration-warnings.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {StateEditorRefreshService} from '../services/state-editor-refresh.service'; +import {LoaderService} from 'services/loader.service'; +import {GraphDataService} from '../services/graph-data.service'; +import {ExplorationNextContentIdIndexService} from '../services/exploration-next-content-id-index.service'; +import {GenerateContentIdService} from 'services/generate-content-id.service'; +import {VersionHistoryService} from '../services/version-history.service'; +import {VersionHistoryBackendApiService} from '../services/version-history-backend-api.service'; +import {ContextService} from 'services/context.service'; @Component({ selector: 'oppia-exploration-editor-tab', - templateUrl: './exploration-editor-tab.component.html' + templateUrl: './exploration-editor-tab.component.html', }) -export class ExplorationEditorTabComponent - implements OnInit, OnDestroy { - @Input() explorationIsLinkedToStory: boolean; - - directiveSubscriptions = new Subscription(); - TabName: string; - interactionIsShown: boolean; - _ID_TUTORIAL_STATE_INTERACTION = '#tutorialStateInteraction'; - _ID_TUTORIAL_PREVIEW_TAB = '#tutorialPreviewTab'; - tutorialInProgress: boolean; - explorationId: string; - stateName: string; - index: number = 0; - validationErrorIsShown: boolean = false; - joyRideSteps: string[] = [ - 'editorTabTourContainer', - 'editorTabTourContentEditorTab', - 'editorTabTourSlideStateInteractionEditorTab', - 'editorTabTourStateResponsesTab', - 'editorTabTourPreviewTab', - 'editorTabTourSaveDraft', - 'editorTabTourTutorialComplete' - ]; - - constructor( - private editabilityService: EditabilityService, - private explorationNextContentIdIndexService: - ExplorationNextContentIdIndexService, - private generateContentIdService: GenerateContentIdService, - private stateTutorialFirstTimeService: StateTutorialFirstTimeService, - private siteAnalyticsService: SiteAnalyticsService, - private explorationStatesService: ExplorationStatesService, - private userExplorationPermissionsService: - UserExplorationPermissionsService, - private stateEditorService: StateEditorService, - private explorationFeaturesService: ExplorationFeaturesService, - private routerService: RouterService, - public stateCardIsCheckpointService: StateCardIsCheckpointService, - private explorationInitStateNameService: ExplorationInitStateNameService, - private explorationWarningsService: ExplorationWarningsService, - private focusManagerService: FocusManagerService, - private stateEditorRefreshService: StateEditorRefreshService, - private loaderService: LoaderService, - private graphDataService: GraphDataService, - private joyride: JoyrideService, - private versionHistoryService: VersionHistoryService, - private versionHistoryBackendApiService: VersionHistoryBackendApiService, - private contextService: ContextService - ) { } - - startTutorial(): void { - this.tutorialInProgress = true; - this.joyride.startTour( - { steps: this.joyRideSteps, - stepDefaultPosition: 'top', - themeColor: '#212f23' - } - ).subscribe( - (value) => { +export class ExplorationEditorTabComponent implements OnInit, OnDestroy { + @Input() explorationIsLinkedToStory: boolean; + + directiveSubscriptions = new Subscription(); + TabName: string; + interactionIsShown: boolean; + _ID_TUTORIAL_STATE_INTERACTION = '#tutorialStateInteraction'; + _ID_TUTORIAL_PREVIEW_TAB = '#tutorialPreviewTab'; + tutorialInProgress: boolean; + explorationId: string; + stateName: string; + index: number = 0; + validationErrorIsShown: boolean = false; + joyRideSteps: string[] = [ + 'editorTabTourContainer', + 'editorTabTourContentEditorTab', + 'editorTabTourSlideStateInteractionEditorTab', + 'editorTabTourStateResponsesTab', + 'editorTabTourPreviewTab', + 'editorTabTourSaveDraft', + 'editorTabTourTutorialComplete', + ]; + + constructor( + private editabilityService: EditabilityService, + private explorationNextContentIdIndexService: ExplorationNextContentIdIndexService, + private generateContentIdService: GenerateContentIdService, + private stateTutorialFirstTimeService: StateTutorialFirstTimeService, + private siteAnalyticsService: SiteAnalyticsService, + private explorationStatesService: ExplorationStatesService, + private userExplorationPermissionsService: UserExplorationPermissionsService, + private stateEditorService: StateEditorService, + private explorationFeaturesService: ExplorationFeaturesService, + private routerService: RouterService, + public stateCardIsCheckpointService: StateCardIsCheckpointService, + private explorationInitStateNameService: ExplorationInitStateNameService, + private explorationWarningsService: ExplorationWarningsService, + private focusManagerService: FocusManagerService, + private stateEditorRefreshService: StateEditorRefreshService, + private loaderService: LoaderService, + private graphDataService: GraphDataService, + private joyride: JoyrideService, + private versionHistoryService: VersionHistoryService, + private versionHistoryBackendApiService: VersionHistoryBackendApiService, + private contextService: ContextService + ) {} + + startTutorial(): void { + this.tutorialInProgress = true; + this.joyride + .startTour({ + steps: this.joyRideSteps, + stepDefaultPosition: 'top', + themeColor: '#212f23', + }) + .subscribe( + value => { // This code make the joyride visible over navbar // by overriding the properties of joyride-step__holder class. document.querySelector( - '.joyride-step__holder').style.zIndex = '1020'; + '.joyride-step__holder' + ).style.zIndex = '1020'; document.querySelector( - '.joyride-step__counter').tabIndex = 0; + '.joyride-step__counter' + ).tabIndex = 0; - document.querySelector( - '.e2e-test-joyride-title').focus(); + document + .querySelector('.e2e-test-joyride-title') + .focus(); if (value.number === 2) { - $('html, body').animate({ - scrollTop: (true ? 0 : 20) - }, 1000); + $('html, body').animate( + { + scrollTop: true ? 0 : 20, + }, + 1000 + ); document.querySelector( - '.joyride-step__counter').tabIndex = 0; + '.joyride-step__counter' + ).tabIndex = 0; - document.querySelector( - '.e2e-test-joyride-title').focus(); + document + .querySelector('.e2e-test-joyride-title') + .focus(); } if (value.number === 4) { - let idToScrollTo = ( - true ? this._ID_TUTORIAL_PREVIEW_TAB : - this._ID_TUTORIAL_STATE_INTERACTION); - - $('html, body').animate({ - scrollTop: document.getElementById(idToScrollTo)?.offsetTop - 200 - }, 1000); + let idToScrollTo = true + ? this._ID_TUTORIAL_PREVIEW_TAB + : this._ID_TUTORIAL_STATE_INTERACTION; + + $('html, body').animate( + { + scrollTop: + document.getElementById(idToScrollTo)?.offsetTop - 200, + }, + 1000 + ); document.querySelector( - '.joyride-step__counter').tabIndex = 0; + '.joyride-step__counter' + ).tabIndex = 0; - document.querySelector( - '.e2e-test-joyride-title').focus(); + document + .querySelector('.e2e-test-joyride-title') + .focus(); } if (value.number === 6) { - let idToScrollTo = ( - true ? this._ID_TUTORIAL_PREVIEW_TAB : - this._ID_TUTORIAL_STATE_INTERACTION); - - $('html, body').animate({ - scrollTop: document.getElementById(idToScrollTo)?.offsetTop - 200 - }, 1000); + let idToScrollTo = true + ? this._ID_TUTORIAL_PREVIEW_TAB + : this._ID_TUTORIAL_STATE_INTERACTION; + + $('html, body').animate( + { + scrollTop: + document.getElementById(idToScrollTo)?.offsetTop - 200, + }, + 1000 + ); document.querySelector( - '.joyride-step__counter').tabIndex = 0; + '.joyride-step__counter' + ).tabIndex = 0; - document.querySelector( - '.e2e-test-joyride-title').focus(); + document + .querySelector('.e2e-test-joyride-title') + .focus(); } }, () => {}, () => { this.siteAnalyticsService.registerFinishTutorialEvent( - this.explorationId); + this.explorationId + ); this.leaveTutorial(); - }, + } ); + } + + // // Remove save from tutorial if user does not has edit rights for + // // exploration since in that case Save Draft button will not be + // // visible on the create page. + removeTutorialSaveButtonIfNoPermissions(): void { + this.userExplorationPermissionsService + .getPermissionsAsync() + .then(permissions => { + if (!permissions.canEdit) { + this.joyRideSteps = [ + 'editorTabTourContainer', + 'editorTabTourContentEditorTab', + 'editorTabTourSlideStateInteractionEditorTab', + 'editorTabTourStateResponsesTab', + 'editorTabTourPreviewTab', + 'editorTabTourTutorialComplete', + ]; + } + }); + } + + leaveTutorial(): void { + this.joyride.closeTour(); + this.editabilityService.onEndTutorial(); + this.stateTutorialFirstTimeService.markEditorTutorialFinished(); + this.tutorialInProgress = false; + } + + saveInteractionData(displayedValue: InteractionData): void { + this.explorationStatesService.saveInteractionId( + this.stateEditorService.getActiveStateName(), + cloneDeep(displayedValue.interactionId) + ); + this.stateEditorService.setInteractionId( + cloneDeep(displayedValue.interactionId) + ); + + this.explorationStatesService.saveInteractionCustomizationArgs( + this.stateEditorService.getActiveStateName(), + cloneDeep(displayedValue.customizationArgs) + ); + this.stateEditorService.setInteractionCustomizationArgs( + cloneDeep(displayedValue.customizationArgs) + ); + } + + saveInteractionAnswerGroups(newAnswerGroups: AnswerGroup[]): void { + this.explorationStatesService.saveInteractionAnswerGroups( + this.stateEditorService.getActiveStateName(), + cloneDeep(newAnswerGroups) + ); + + this.stateEditorService.setInteractionAnswerGroups( + cloneDeep(newAnswerGroups) + ); + this.recomputeGraph(); + } + + saveInteractionDefaultOutcome(newOutcome: Outcome): void { + this.explorationStatesService.saveInteractionDefaultOutcome( + this.stateEditorService.getActiveStateName(), + cloneDeep(newOutcome) + ); + + this.stateEditorService.setInteractionDefaultOutcome(cloneDeep(newOutcome)); + this.recomputeGraph(); + } + + saveNextContentIdIndex(): void { + this.explorationNextContentIdIndexService.saveDisplayedValue(); + } + + saveSolution(displayedValue: Solution | SubtitledHtml): void { + this.explorationStatesService.saveSolution( + this.stateEditorService.getActiveStateName(), + cloneDeep(displayedValue) as SubtitledHtml + ); + + this.stateEditorService.setInteractionSolution( + cloneDeep(displayedValue) as Solution + ); + } + + saveHints(displayedValue: Hint[]): void { + this.explorationStatesService.saveHints( + this.stateEditorService.getActiveStateName(), + cloneDeep(displayedValue) + ); + + this.stateEditorService.setInteractionHints(cloneDeep(displayedValue)); + } + + saveSolicitAnswerDetails(displayedValue: boolean): void { + this.explorationStatesService.saveSolicitAnswerDetails( + this.stateEditorService.getActiveStateName(), + cloneDeep(displayedValue) + ); + + this.stateEditorService.setSolicitAnswerDetails(cloneDeep(displayedValue)); + } + + navigateToState(stateName: string): void { + this.routerService.navigateToMainTab(stateName); + } + + areParametersEnabled(): boolean { + return this.explorationFeaturesService.areParametersEnabled(); + } + + onChangeCardIsCheckpoint(): void { + let displayedValue = this.stateCardIsCheckpointService.displayed; + this.explorationStatesService.saveCardIsCheckpoint( + this.stateEditorService.getActiveStateName(), + cloneDeep(displayedValue) + ); + this.stateEditorService.setCardIsCheckpoint(cloneDeep(displayedValue)); + this.stateCardIsCheckpointService.saveDisplayedValue(); + } + + isEditable(): boolean { + return this.editabilityService.isEditable(); + } + + getStateContentPlaceholder(): string { + if ( + this.stateEditorService.getActiveStateName() === + this.explorationInitStateNameService.savedMemento + ) { + return ( + 'This is the first card of your exploration. Use this space ' + + 'to introduce your topic and engage the learner, then ask ' + + 'them a question.' + ); + } else { + return 'You can speak to the learner here, then ask them a question.'; } + } - // // Remove save from tutorial if user does not has edit rights for - // // exploration since in that case Save Draft button will not be - // // visible on the create page. - removeTutorialSaveButtonIfNoPermissions(): void { - this.userExplorationPermissionsService.getPermissionsAsync() - .then((permissions) => { - if (!permissions.canEdit) { - this.joyRideSteps = [ - 'editorTabTourContainer', - 'editorTabTourContentEditorTab', - 'editorTabTourSlideStateInteractionEditorTab', - 'editorTabTourStateResponsesTab', - 'editorTabTourPreviewTab', - 'editorTabTourTutorialComplete' - ]; - } - }); - } - - leaveTutorial(): void { - this.joyride.closeTour(); - this.editabilityService.onEndTutorial(); - this.stateTutorialFirstTimeService.markEditorTutorialFinished(); - this.tutorialInProgress = false; - } - - saveInteractionData(displayedValue: InteractionData): void { - this.explorationStatesService.saveInteractionId( - this.stateEditorService.getActiveStateName(), - cloneDeep(displayedValue.interactionId)); - this.stateEditorService.setInteractionId( - cloneDeep(displayedValue.interactionId)); - - this.explorationStatesService.saveInteractionCustomizationArgs( - this.stateEditorService.getActiveStateName(), - cloneDeep(displayedValue.customizationArgs)); - this.stateEditorService.setInteractionCustomizationArgs( - cloneDeep(displayedValue.customizationArgs)); - } - - saveInteractionAnswerGroups(newAnswerGroups: AnswerGroup[]): void { - this.explorationStatesService.saveInteractionAnswerGroups( - this.stateEditorService.getActiveStateName(), - cloneDeep(newAnswerGroups)); - - this.stateEditorService.setInteractionAnswerGroups( - cloneDeep(newAnswerGroups)); - this.recomputeGraph(); - } - - saveInteractionDefaultOutcome(newOutcome: Outcome): void { - this.explorationStatesService.saveInteractionDefaultOutcome( - this.stateEditorService.getActiveStateName(), - cloneDeep(newOutcome)); - - this.stateEditorService.setInteractionDefaultOutcome( - cloneDeep(newOutcome)); - this.recomputeGraph(); - } - - saveNextContentIdIndex(): void { - this.explorationNextContentIdIndexService.saveDisplayedValue(); - } - - saveSolution(displayedValue: Solution | SubtitledHtml): void { - this.explorationStatesService.saveSolution( - this.stateEditorService.getActiveStateName(), - cloneDeep(displayedValue) as SubtitledHtml); - - this.stateEditorService.setInteractionSolution( - cloneDeep(displayedValue) as Solution); - } - - saveHints(displayedValue: Hint[]): void { - this.explorationStatesService.saveHints( - this.stateEditorService.getActiveStateName(), - cloneDeep(displayedValue)); - - this.stateEditorService.setInteractionHints( - cloneDeep(displayedValue)); - } - - saveSolicitAnswerDetails(displayedValue: boolean): void { - this.explorationStatesService.saveSolicitAnswerDetails( - this.stateEditorService.getActiveStateName(), - cloneDeep(displayedValue)); - - this.stateEditorService.setSolicitAnswerDetails( - cloneDeep(displayedValue)); - } - - navigateToState(stateName: string): void { - this.routerService.navigateToMainTab(stateName); - } + getStateContentSaveButtonPlaceholder(): string { + return 'Save Content'; + } - areParametersEnabled(): boolean { - return this.explorationFeaturesService.areParametersEnabled(); - } + addState(newStateName: string): void { + this.explorationStatesService.addState(newStateName, null); + } - onChangeCardIsCheckpoint(): void { - let displayedValue = this.stateCardIsCheckpointService.displayed; - this.explorationStatesService.saveCardIsCheckpoint( - this.stateEditorService.getActiveStateName(), - cloneDeep(displayedValue)); - this.stateEditorService.setCardIsCheckpoint( - cloneDeep(displayedValue)); - this.stateCardIsCheckpointService.saveDisplayedValue(); - } + refreshWarnings(): void { + this.explorationWarningsService.updateWarnings(); + } - isEditable(): boolean { - return this.editabilityService.isEditable(); - } + getLastEditedVersionNumberInCaseOfError(): number { + return this.versionHistoryService.fetchedStateVersionNumbers[ + this.versionHistoryService.getCurrentPositionInStateVersionHistoryList() + ]; + } + + initStateEditor(): void { + this.stateName = this.stateEditorService.getActiveStateName(); + this.stateEditorService.setStateNames( + this.explorationStatesService.getStateNames() + ); + this.stateEditorService.setInQuestionMode(false); + + let stateData = this.explorationStatesService.getState(this.stateName); + + if (this.stateName && stateData) { + // This.stateEditorService.checkEventListenerRegistrationStatus() + // returns true if the event listeners of the state editor child + // components have been registered. + // In this case 'stateEditorInitialized' is broadcasted so that: + // 1. state-editor directive can initialise the child + // components of the state editor. + // 2. state-interaction-editor directive can initialise the + // child components of the interaction editor. - getStateContentPlaceholder(): string { if ( - this.stateEditorService.getActiveStateName() === - this.explorationInitStateNameService.savedMemento) { - return ( - 'This is the first card of your exploration. Use this space ' + - 'to introduce your topic and engage the learner, then ask ' + - 'them a question.'); - } else { - return ( - 'You can speak to the learner here, then ask them a question.'); + this.stateEditorService.checkEventListenerRegistrationStatus() && + this.explorationStatesService.isInitialized() + ) { + let stateData = this.explorationStatesService.getState(this.stateName); + this.stateEditorService.onStateEditorInitialized.emit(stateData); } - } - - getStateContentSaveButtonPlaceholder(): string { - return 'Save Content'; - } - - addState(newStateName: string): void { - this.explorationStatesService.addState(newStateName, null); - } - refreshWarnings(): void { - this.explorationWarningsService.updateWarnings(); - } - - getLastEditedVersionNumberInCaseOfError(): number { - return ( - this.versionHistoryService.fetchedStateVersionNumbers[ - this.versionHistoryService - .getCurrentPositionInStateVersionHistoryList()]); - } - - initStateEditor(): void { - this.stateName = this.stateEditorService.getActiveStateName(); - this.stateEditorService.setStateNames( - this.explorationStatesService.getStateNames()); - this.stateEditorService.setInQuestionMode(false); - - let stateData = this.explorationStatesService.getState(this.stateName); - - if (this.stateName && stateData) { - // This.stateEditorService.checkEventListenerRegistrationStatus() - // returns true if the event listeners of the state editor child - // components have been registered. - // In this case 'stateEditorInitialized' is broadcasted so that: - // 1. state-editor directive can initialise the child - // components of the state editor. - // 2. state-interaction-editor directive can initialise the - // child components of the interaction editor. - - if ( - this.stateEditorService.checkEventListenerRegistrationStatus() && - this.explorationStatesService.isInitialized()) { - let stateData = ( - this.explorationStatesService.getState(this.stateName)); - this.stateEditorService.onStateEditorInitialized.emit(stateData); - } + let content = this.explorationStatesService.getStateContentMemento( + this.stateName + ); + if (content.html || stateData.interaction.id) { + this.interactionIsShown = true; + } - let content = this.explorationStatesService.getStateContentMemento( - this.stateName); - if (content.html || stateData.interaction.id) { - this.interactionIsShown = true; - } + this.versionHistoryService.resetStateVersionHistory(); + this.validationErrorIsShown = false; + this.versionHistoryService.insertStateVersionHistoryData( + this.versionHistoryService.getLatestVersionOfExploration(), + stateData, + '' + ); - this.versionHistoryService.resetStateVersionHistory(); - this.validationErrorIsShown = false; - this.versionHistoryService.insertStateVersionHistoryData( - this.versionHistoryService.getLatestVersionOfExploration(), - stateData, ''); - - if ( - this.versionHistoryService.getLatestVersionOfExploration() !== null - ) { - this.versionHistoryBackendApiService.fetchStateVersionHistoryAsync( - this.contextService.getExplorationId(), stateData.name, + if (this.versionHistoryService.getLatestVersionOfExploration() !== null) { + this.versionHistoryBackendApiService + .fetchStateVersionHistoryAsync( + this.contextService.getExplorationId(), + stateData.name, this.versionHistoryService.getLatestVersionOfExploration() - ).then((response) => { + ) + .then(response => { if (response !== null) { this.versionHistoryService.insertStateVersionHistoryData( response.lastEditedVersionNumber, @@ -376,84 +409,92 @@ export class ExplorationEditorTabComponent this.validationErrorIsShown = true; } }); - } - - this.loaderService.hideLoadingScreen(); - // $timeout is used to ensure that focus acts only after - // element is visible in DOM. - setTimeout(() => this.windowOnload(), 100); } - if (this.editabilityService.inTutorialMode()) { - this.startTutorial(); - } + this.loaderService.hideLoadingScreen(); + // $timeout is used to ensure that focus acts only after + // element is visible in DOM. + setTimeout(() => this.windowOnload(), 100); } - windowOnload(): void { - this.TabName = this.routerService.getActiveTabName(); - if (this.TabName === 'main') { - this.focusManagerService.setFocus('oppiaEditableSection'); - } - if (this.TabName === 'feedback') { - this.focusManagerService.setFocus('newThreadButton'); - } - if (this.TabName === 'history') { - this.focusManagerService.setFocus('usernameInputField'); - } + if (this.editabilityService.inTutorialMode()) { + this.startTutorial(); } + } - recomputeGraph(): void { - this.graphDataService.recompute(); + windowOnload(): void { + this.TabName = this.routerService.getActiveTabName(); + if (this.TabName === 'main') { + this.focusManagerService.setFocus('oppiaEditableSection'); } - - saveStateContent(displayedValue: SubtitledHtml): void { - this.explorationStatesService.saveStateContent( - this.stateEditorService.getActiveStateName(), - cloneDeep(displayedValue)); - // Show the interaction when the text content is saved, even if no - // content is entered. - this.interactionIsShown = true; + if (this.TabName === 'feedback') { + this.focusManagerService.setFocus('newThreadButton'); } - - saveLinkedSkillId(displayedValue: string): void { - this.explorationStatesService.saveLinkedSkillId( - this.stateEditorService.getActiveStateName(), - cloneDeep(displayedValue)); - - this.stateEditorService.setLinkedSkillId(cloneDeep(displayedValue)); + if (this.TabName === 'history') { + this.focusManagerService.setFocus('usernameInputField'); } + } + + recomputeGraph(): void { + this.graphDataService.recompute(); + } + + saveStateContent(displayedValue: SubtitledHtml): void { + this.explorationStatesService.saveStateContent( + this.stateEditorService.getActiveStateName(), + cloneDeep(displayedValue) + ); + // Show the interaction when the text content is saved, even if no + // content is entered. + this.interactionIsShown = true; + } + + saveLinkedSkillId(displayedValue: string): void { + this.explorationStatesService.saveLinkedSkillId( + this.stateEditorService.getActiveStateName(), + cloneDeep(displayedValue) + ); + + this.stateEditorService.setLinkedSkillId(cloneDeep(displayedValue)); + } + + ngOnInit(): void { + this.directiveSubscriptions.add( + this.stateEditorRefreshService.onRefreshStateEditor.subscribe(() => { + this.initStateEditor(); + }) + ); + + this.explorationStatesService.registerOnStatesChangedCallback(() => { + if (this.explorationStatesService.getStates()) { + this.stateEditorService.setStateNames( + this.explorationStatesService.getStateNames() + ); + } + }); - ngOnInit(): void { - this.directiveSubscriptions.add( - this.stateEditorRefreshService.onRefreshStateEditor.subscribe(() => { - this.initStateEditor(); - }) - ); - - this.explorationStatesService.registerOnStatesChangedCallback(() => { - if (this.explorationStatesService.getStates()) { - this.stateEditorService.setStateNames( - this.explorationStatesService.getStateNames()); - } - }); - - this.interactionIsShown = false; - this.removeTutorialSaveButtonIfNoPermissions(); - this.generateContentIdService.init(() => { + this.interactionIsShown = false; + this.removeTutorialSaveButtonIfNoPermissions(); + this.generateContentIdService.init( + () => { let indexToUse = this.explorationNextContentIdIndexService.displayed; this.explorationNextContentIdIndexService.displayed += 1; return indexToUse; - }, () => { + }, + () => { this.explorationNextContentIdIndexService.restoreFromMemento(); - }); - } + } + ); + } - ngOnDestroy(): void { - this.directiveSubscriptions.unsubscribe(); - } + ngOnDestroy(): void { + this.directiveSubscriptions.unsubscribe(); + } } -angular.module('oppia').directive('oppiaExplorationEditorTab', - downgradeComponent({ - component: ExplorationEditorTabComponent - }) as angular.IDirectiveFactory); +angular.module('oppia').directive( + 'oppiaExplorationEditorTab', + downgradeComponent({ + component: ExplorationEditorTabComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/graph-directives/exploration-graph.component.spec.ts b/core/templates/pages/exploration-editor-page/editor-tab/graph-directives/exploration-graph.component.spec.ts index b91c8a9d6e5f..9a21a15d2971 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/graph-directives/exploration-graph.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/graph-directives/exploration-graph.component.spec.ts @@ -16,19 +16,24 @@ * @fileoverview Unit tests for explorationGraph. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { NgbModule, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; -import { ExplorationWarningsService } from 'pages/exploration-editor-page/services/exploration-warnings.service'; -import { RouterService } from 'pages/exploration-editor-page/services/router.service'; -import { AlertsService } from 'services/alerts.service'; -import { LoggerService } from 'services/contextual/logger.service'; -import { EditabilityService } from 'services/editability.service'; -import { ExplorationGraphModalComponent } from '../templates/modal-templates/exploration-graph-modal.component'; -import { ExplorationGraphComponent } from './exploration-graph.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {NgbModule, NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; +import {ExplorationWarningsService} from 'pages/exploration-editor-page/services/exploration-warnings.service'; +import {RouterService} from 'pages/exploration-editor-page/services/router.service'; +import {AlertsService} from 'services/alerts.service'; +import {LoggerService} from 'services/contextual/logger.service'; +import {EditabilityService} from 'services/editability.service'; +import {ExplorationGraphModalComponent} from '../templates/modal-templates/exploration-graph-modal.component'; +import {ExplorationGraphComponent} from './exploration-graph.component'; describe('Exploration Graph Component', () => { let component: ExplorationGraphComponent; @@ -46,21 +51,15 @@ describe('Exploration Graph Component', () => { class MockNgbModal { open() { return { - result: Promise.resolve() + result: Promise.resolve(), }; } } beforeEach(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - NgbModule - ], - declarations: [ - ExplorationGraphComponent, - ExplorationGraphModalComponent, - ], + imports: [HttpClientTestingModule, NgbModule], + declarations: [ExplorationGraphComponent, ExplorationGraphModalComponent], providers: [ StateEditorService, RouterService, @@ -71,10 +70,10 @@ describe('Exploration Graph Component', () => { AlertsService, { provide: NgbModal, - useClass: MockNgbModal - } + useClass: MockNgbModal, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }); }); @@ -95,25 +94,27 @@ describe('Exploration Graph Component', () => { component = fixture.componentInstance; }); - - it('should show graph when exploration states service is initialized', - () => { - expect(component.isGraphShown()).toBe(false); - explorationStatesService.init({}, false); - expect(component.isGraphShown()).toBe(true); - }); + it('should show graph when exploration states service is initialized', () => { + expect(component.isGraphShown()).toBe(false); + explorationStatesService.init({}, false); + expect(component.isGraphShown()).toBe(true); + }); it('should get name from the active state', () => { spyOn(stateEditorService, 'getActiveStateName').and.returnValue( - 'Introduction'); + 'Introduction' + ); expect(component.getActiveStateName()).toBe('Introduction'); }); - it('should get null graph data from graph data service when it is not' + - ' recomputed', () => { - expect(component.isGraphShown()).toBe(false); - expect(component.getGraphData()).toBeUndefined(); - }); + it( + 'should get null graph data from graph data service when it is not' + + ' recomputed', + () => { + expect(component.isGraphShown()).toBe(false); + expect(component.getGraphData()).toBeUndefined(); + } + ); it('should evaluate if exploration graph is editable', () => { isEditableSpy.and.returnValue(true); @@ -125,7 +126,6 @@ describe('Exploration Graph Component', () => { expect(component.isEditable()).toBe(false); }); - it('should delete state when closing state graph modal', fakeAsync(() => { spyOn(ngbModal, 'open').and.returnValue({ componentInstance: { @@ -133,8 +133,8 @@ describe('Exploration Graph Component', () => { }, result: Promise.resolve({ action: 'delete', - stateName: 'Introduction' - }) + stateName: 'Introduction', + }), } as NgbModalRef); spyOn(explorationStatesService, 'deleteState'); @@ -142,55 +142,56 @@ describe('Exploration Graph Component', () => { tick(); expect(explorationStatesService.deleteState).toHaveBeenCalledWith( - 'Introduction'); + 'Introduction' + ); })); - it('should navigate to main tab when closing state graph modal', - fakeAsync(() => { - spyOn(ngbModal, 'open').and.returnValue({ - componentInstance: { - isEditable: true, - }, - result: Promise.resolve({ - action: 'navigate', - stateName: 'Introduction' - }) - } as NgbModalRef); - spyOn(routerService, 'navigateToMainTab'); - - component.openStateGraphModal(); - tick(); - - expect(routerService.navigateToMainTab).toHaveBeenCalledWith( - 'Introduction'); - })); - - it('should handle invalid actions when state graph modal is opened', - fakeAsync(() => { - spyOn(ngbModal, 'open').and.returnValue({ - componentInstance: { - isEditable: true, - }, - result: Promise.resolve({ - action: 'add', - stateName: 'Introduction' - }) - } as NgbModalRef); - spyOn(loggerService, 'error'); + it('should navigate to main tab when closing state graph modal', fakeAsync(() => { + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: { + isEditable: true, + }, + result: Promise.resolve({ + action: 'navigate', + stateName: 'Introduction', + }), + } as NgbModalRef); + spyOn(routerService, 'navigateToMainTab'); + + component.openStateGraphModal(); + tick(); - component.openStateGraphModal(); - tick(); + expect(routerService.navigateToMainTab).toHaveBeenCalledWith( + 'Introduction' + ); + })); - expect(loggerService.error).toHaveBeenCalledWith( - 'Invalid closeDict action: add'); - })); + it('should handle invalid actions when state graph modal is opened', fakeAsync(() => { + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: { + isEditable: true, + }, + result: Promise.resolve({ + action: 'add', + stateName: 'Introduction', + }), + } as NgbModalRef); + spyOn(loggerService, 'error'); + + component.openStateGraphModal(); + tick(); + + expect(loggerService.error).toHaveBeenCalledWith( + 'Invalid closeDict action: add' + ); + })); it('should dismiss state graph modal', fakeAsync(() => { spyOn(ngbModal, 'open').and.returnValue({ componentInstance: { isEditable: true, }, - result: Promise.reject() + result: Promise.reject(), } as NgbModalRef); spyOn(alertsService, 'clearWarnings'); @@ -207,12 +208,18 @@ describe('Exploration Graph Component', () => { }); it('should return checkpoint count warning', () => { - spyOn(explorationWarningsService, 'getCheckpointCountWarning').and - .returnValue('Only a maximum of 8 checkpoints are allowed per lesson.'); + spyOn( + explorationWarningsService, + 'getCheckpointCountWarning' + ).and.returnValue( + 'Only a maximum of 8 checkpoints are allowed per lesson.' + ); expect(component.showCheckpointCountWarningSign()).toEqual( - 'Only a maximum of 8 checkpoints are allowed per lesson.'); + 'Only a maximum of 8 checkpoints are allowed per lesson.' + ); expect(component.checkpointCountWarning).toEqual( - 'Only a maximum of 8 checkpoints are allowed per lesson.'); + 'Only a maximum of 8 checkpoints are allowed per lesson.' + ); }); }); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/graph-directives/exploration-graph.component.ts b/core/templates/pages/exploration-editor-page/editor-tab/graph-directives/exploration-graph.component.ts index 30f53fbba9ea..702ada81d1d1 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/graph-directives/exploration-graph.component.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/graph-directives/exploration-graph.component.ts @@ -16,23 +16,23 @@ * @fileoverview Component for the exploration graph. */ -import { Component } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; -import { ExplorationWarningsService } from 'pages/exploration-editor-page/services/exploration-warnings.service'; -import { GraphDataService } from 'pages/exploration-editor-page/services/graph-data.service'; -import { RouterService } from 'pages/exploration-editor-page/services/router.service'; -import { AlertsService } from 'services/alerts.service'; -import { GraphData } from 'services/compute-graph.service'; -import { LoggerService } from 'services/contextual/logger.service'; -import { EditabilityService } from 'services/editability.service'; -import { ExplorationGraphModalComponent } from '../templates/modal-templates/exploration-graph-modal.component'; +import {Component} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; +import {ExplorationWarningsService} from 'pages/exploration-editor-page/services/exploration-warnings.service'; +import {GraphDataService} from 'pages/exploration-editor-page/services/graph-data.service'; +import {RouterService} from 'pages/exploration-editor-page/services/router.service'; +import {AlertsService} from 'services/alerts.service'; +import {GraphData} from 'services/compute-graph.service'; +import {LoggerService} from 'services/contextual/logger.service'; +import {EditabilityService} from 'services/editability.service'; +import {ExplorationGraphModalComponent} from '../templates/modal-templates/exploration-graph-modal.component'; @Component({ selector: 'oppia-exploration-graph', - templateUrl: './exploration-graph.component.html' + templateUrl: './exploration-graph.component.html', }) export class ExplorationGraphComponent { // This property is initialized using Angular lifecycle hooks @@ -49,8 +49,8 @@ export class ExplorationGraphComponent { private loggerService: LoggerService, private ngbModal: NgbModal, private routerService: RouterService, - private stateEditorService: StateEditorService, - ) { } + private stateEditorService: StateEditorService + ) {} // We hide the graph at the outset in order not to confuse new // exploration creators. @@ -79,25 +79,31 @@ export class ExplorationGraphComponent { this.alertsService.clearWarnings(); const modalRef: NgbModalRef = this.ngbModal.open( - ExplorationGraphModalComponent, { + ExplorationGraphModalComponent, + { backdrop: true, windowClass: 'oppia-large-modal-window exploration-graph-modal', - }); + } + ); modalRef.componentInstance.isEditable = this.isEditable; - modalRef.result.then((closeDict) => { - if (closeDict.action === 'delete') { - this.deleteState(closeDict.stateName); - } else if (closeDict.action === 'navigate') { - this.onClickStateInMinimap(closeDict.stateName); - } else { - this.loggerService.error( - 'Invalid closeDict action: ' + closeDict.action); + modalRef.result.then( + closeDict => { + if (closeDict.action === 'delete') { + this.deleteState(closeDict.stateName); + } else if (closeDict.action === 'navigate') { + this.onClickStateInMinimap(closeDict.stateName); + } else { + this.loggerService.error( + 'Invalid closeDict action: ' + closeDict.action + ); + } + }, + () => { + this.alertsService.clearWarnings(); } - }, () => { - this.alertsService.clearWarnings(); - }); + ); } getGraphData(): GraphData { @@ -109,14 +115,16 @@ export class ExplorationGraphComponent { } showCheckpointCountWarningSign(): string { - this.checkpointCountWarning = ( - this.explorationWarningsService.getCheckpointCountWarning()); + this.checkpointCountWarning = + this.explorationWarningsService.getCheckpointCountWarning(); return this.checkpointCountWarning; } } -angular.module('oppia').directive('oppiaExplorationGraph', +angular.module('oppia').directive( + 'oppiaExplorationGraph', downgradeComponent({ - component: ExplorationGraphComponent - }) as angular.IDirectiveFactory); + component: ExplorationGraphComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/graph-directives/state-graph-visualization.component.spec.ts b/core/templates/pages/exploration-editor-page/editor-tab/graph-directives/state-graph-visualization.component.spec.ts index d86bff690db7..c70afe152cd1 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/graph-directives/state-graph-visualization.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/graph-directives/state-graph-visualization.component.spec.ts @@ -16,25 +16,35 @@ * @fileoverview Unit tests for State Graph Visualization directive. */ -import { EventEmitter, NO_ERRORS_SCHEMA, Pipe } from '@angular/core'; -import { ComponentFixture, fakeAsync, flush, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { StateGraphLayoutService } from 'components/graph-services/graph-layout.service'; +import {EventEmitter, NO_ERRORS_SCHEMA, Pipe} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + flush, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {StateGraphLayoutService} from 'components/graph-services/graph-layout.service'; import * as d3 from 'd3'; -import { of } from 'rxjs'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; -import { ExplorationWarningsService } from 'pages/exploration-editor-page/services/exploration-warnings.service'; -import { RouterService } from 'pages/exploration-editor-page/services/router.service'; -import { TranslationStatusService } from 'pages/exploration-editor-page/translation-tab/services/translation-status.service'; -import { NodeTitle, StateGraphVisualization } from './state-graph-visualization.component'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { GraphDataService } from 'pages/exploration-editor-page/services/graph-data.service'; +import {of} from 'rxjs'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; +import {ExplorationWarningsService} from 'pages/exploration-editor-page/services/exploration-warnings.service'; +import {RouterService} from 'pages/exploration-editor-page/services/router.service'; +import {TranslationStatusService} from 'pages/exploration-editor-page/translation-tab/services/translation-status.service'; +import { + NodeTitle, + StateGraphVisualization, +} from './state-graph-visualization.component'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {GraphDataService} from 'pages/exploration-editor-page/services/graph-data.service'; class MockNgbModal { open() { return { - result: Promise.resolve() + result: Promise.resolve(), }; } } @@ -49,7 +59,7 @@ class MockRouterService { onCenterGraph = of(new Event('resize')); } -@Pipe({ name: 'truncate' }) +@Pipe({name: 'truncate'}) class MockTruncatePipe { transform(value: string, params: number): string { return value; @@ -66,16 +76,18 @@ describe('State Graph Visualization Component when graph is redrawn', () => { var mockUpdateGraphDataEmitter = new EventEmitter(); var graphData = { nodes: { - State1: 'State 1 Node' + State1: 'State 1 Node', }, - links: [{ - connectsDestIfStuck: false, - linkProperty: 'added', - source: '', - target: '', - }], + links: [ + { + connectsDestIfStuck: false, + linkProperty: 'added', + source: '', + target: '', + }, + ], initStateId: 'state_1', - finalStateIds: [] + finalStateIds: [], }; var nodes = { state_1: { @@ -94,7 +106,7 @@ describe('State Graph Visualization Component when graph is redrawn', () => { reachableFromEnd: true, style: 'string', nodeClass: 'string', - canDelete: true + canDelete: true, }, state_3: { depth: 3, @@ -112,7 +124,7 @@ describe('State Graph Visualization Component when graph is redrawn', () => { secondaryLabel: '2nd', style: 'string', nodeClass: 'string', - canDelete: true + canDelete: true, }, state_4: { depth: 3, @@ -130,8 +142,8 @@ describe('State Graph Visualization Component when graph is redrawn', () => { secondaryLabel: '2nd', style: 'string', nodeClass: 'string', - canDelete: true - } + canDelete: true, + }, }; class MockGraphDataService { @@ -145,33 +157,30 @@ describe('State Graph Visualization Component when graph is redrawn', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - StateGraphVisualization, - MockTruncatePipe - ], + declarations: [StateGraphVisualization, MockTruncatePipe], providers: [ { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, ExplorationWarningsService, ExplorationStatesService, { provide: RouterService, - useClass: MockRouterService + useClass: MockRouterService, }, StateGraphLayoutService, TranslationStatusService, { provide: GraphDataService, - useClass: MockGraphDataService + useClass: MockGraphDataService, }, { provide: WindowDimensionsService, - useClass: MockWindowDimensionsService - } + useClass: MockWindowDimensionsService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -190,16 +199,16 @@ describe('State Graph Visualization Component when graph is redrawn', () => { component.initStateId2 = 'state_2'; component.linkPropertyMapping = { added: 'background-color: red; ', - deleted: 'background-color: red; ' + deleted: 'background-color: red; ', }; component.nodeColors = { state_1: '#000', state_2: '#ff0', - state_3: '#fff' + state_3: '#fff', }; component.nodeFill = '#fff'; component.nodeSecondaryLabels = { - state_3: 'This is a secondary label for state_3' + state_3: 'This is a secondary label for state_3', }; component.showTranslationWarnings = true; component.mainScreen = { @@ -208,17 +217,17 @@ describe('State Graph Visualization Component when graph is redrawn', () => { baseVal: { convertToSpecifiedUnits(value: number) { return 1000; - } - } + }, + }, }, width: { baseVal: { convertToSpecifiedUnits(value: number) { return 1000; - } - } - } - } + }, + }, + }, + }, }; // This throws "Type 'null' is not assignable to parameter of @@ -227,24 +236,41 @@ describe('State Graph Visualization Component when graph is redrawn', () => { // the state name is null. // @ts-ignore spyOn(explorationStatesService, 'getState').and.returnValue(null); - spyOn(stateGraphLayoutService, 'computeLayout') - .and.returnValue(nodes); - spyOn(translationStatusService, 'getAllStatesNeedUpdatewarning') - .and.returnValue({ - 'This is a label for node 1': ['red', 'green'] - }); - spyOn(stateGraphLayoutService, 'getAugmentedLinks').and.returnValue([{ - // This throws "Type 'null' is not assignable to parameter of - // type 'NodeData'." We need to suppress this error - // because of the need to test validations. This error is - // thrown because the source and target are null. - // @ts-ignore - source: null, target: null, d: null, style: '', connectsDestIfStuck: false - }]); + spyOn(stateGraphLayoutService, 'computeLayout').and.returnValue(nodes); + spyOn( + translationStatusService, + 'getAllStatesNeedUpdatewarning' + ).and.returnValue({ + 'This is a label for node 1': ['red', 'green'], + }); + spyOn(stateGraphLayoutService, 'getAugmentedLinks').and.returnValue([ + { + // This throws "Type 'null' is not assignable to parameter of + // type 'NodeData'." We need to suppress this error + // because of the need to test validations. This error is + // thrown because the source and target are null. + // @ts-ignore + source: null, + // This throws "Type 'null' is not assignable to parameter of + // type 'NodeData'." We need to suppress this error + // because of the need to test validations. This error is + // thrown because the source and target are null. + // @ts-ignore + target: null, + // This throws "Type 'null' is not assignable to parameter of + // type 'NodeData'." We need to suppress this error + // because of the need to test validations. This error is + // thrown because the source and target are null. + // @ts-ignore + d: null, + style: '', + connectsDestIfStuck: false, + }, + ]); component.linkPropertyMapping = { added: 'string', - deleted: 'string' + deleted: 'string', }; component.currentStateId = 'state_1'; component.ngOnInit(); @@ -252,7 +278,6 @@ describe('State Graph Visualization Component when graph is redrawn', () => { flush(); })); - afterEach(fakeAsync(() => { component.ngOnDestroy(); flush(); @@ -280,79 +305,90 @@ describe('State Graph Visualization Component when graph is redrawn', () => { flush(); })); - it('should initialize $scope properties after controller is initialized', - fakeAsync(() => { - component.versionGraphData = graphData; - component.ngOnInit(); - tick(); + it('should initialize $scope properties after controller is initialized', fakeAsync(() => { + component.versionGraphData = graphData; + component.ngOnInit(); + tick(); - expect(component.graphLoaded).toBeTrue(); - expect(component.GRAPH_WIDTH).toBe(630); - expect(component.GRAPH_HEIGHT).toBe(280); - expect(component.VIEWPORT_WIDTH).toBe('10000px'); - expect(component.VIEWPORT_HEIGHT).toBe('10000px'); - expect(component.VIEWPORT_X).toBe('-1260px'); - expect(component.VIEWPORT_Y).toBe('-1000px'); + expect(component.graphLoaded).toBeTrue(); + expect(component.GRAPH_WIDTH).toBe(630); + expect(component.GRAPH_HEIGHT).toBe(280); + expect(component.VIEWPORT_WIDTH).toBe('10000px'); + expect(component.VIEWPORT_HEIGHT).toBe('10000px'); + expect(component.VIEWPORT_X).toBe('-1260px'); + expect(component.VIEWPORT_Y).toBe('-1000px'); - expect(component.getGraphHeightInPixels()).toBe('300px'); + expect(component.getGraphHeightInPixels()).toBe('300px'); - expect(component.augmentedLinks[0].style).toBe( - 'string'); - expect(component.nodeList.length).toBe(3); + expect(component.augmentedLinks[0].style).toBe('string'); + expect(component.nodeList.length).toBe(3); - flush(); - })); - - it('should check if can navigate to node whenever node id is equal to' + - ' current state id', fakeAsync(() => { - spyOn(component, 'centerGraph'); - component.initStateId = 'nodeId'; - component.onNodeDeletionClick('nodeId2'); - component.getCenterGraph(); - tick(); - - expect(component.centerGraph).toHaveBeenCalled(); - expect(component.canNavigateToNode('state_1')).toBe(false); - expect(component.canNavigateToNode('state_3')).toBe(true); + flush(); })); - it('should get node complete title with its secondary label and' + - ' warnings', () => { - spyOn(component, 'getNodeErrorMessage').and.returnValue('warning'); - - expect(component.getNodeTitle(nodes.state_1)).toBe( - 'This is a label for node 1 Second label for node 1 ' + - '(Warning: this state is unreachable.)'); - - expect(component.getNodeTitle(nodes.state_3 as NodeTitle)).toBe( - 'This is a label for node 3 This is a secondary label for ' + - 'state_3 (Warning: there is no path from this state to the ' + - 'END state.)'); + it( + 'should check if can navigate to node whenever node id is equal to' + + ' current state id', + fakeAsync(() => { + spyOn(component, 'centerGraph'); + component.initStateId = 'nodeId'; + component.onNodeDeletionClick('nodeId2'); + component.getCenterGraph(); + tick(); - expect(component.getNodeTitle(nodes.state_4)).toBe( - 'This is a label for node 4 2nd (warning)'); - }); + expect(component.centerGraph).toHaveBeenCalled(); + expect(component.canNavigateToNode('state_1')).toBe(false); + expect(component.canNavigateToNode('state_3')).toBe(true); + }) + ); + + it( + 'should get node complete title with its secondary label and' + ' warnings', + () => { + spyOn(component, 'getNodeErrorMessage').and.returnValue('warning'); + + expect(component.getNodeTitle(nodes.state_1)).toBe( + 'This is a label for node 1 Second label for node 1 ' + + '(Warning: this state is unreachable.)' + ); + + expect(component.getNodeTitle(nodes.state_3 as NodeTitle)).toBe( + 'This is a label for node 3 This is a secondary label for ' + + 'state_3 (Warning: there is no path from this state to the ' + + 'END state.)' + ); + + expect(component.getNodeTitle(nodes.state_4)).toBe( + 'This is a label for node 4 2nd (warning)' + ); + } + ); it('should get truncated label with truncate filter', () => { component.sendOnMaximizeFunction(); component.sendOnClickFunctionData(''); expect(component.getTruncatedLabel('This is a label for node 3')).toBe( - 'This is a la...'); + 'This is a la...' + ); }); - - it('should get node error message from node label when' + - ' showTranslationWarnings is false', () => { - component.showTranslationWarnings = false; - var nodeErrorMessage = 'Node 1 error message from exploration warnings'; - spyOn(explorationWarningsService, 'getAllStateRelatedWarnings').and - .returnValue({ - 'This is a label for node 1': [nodeErrorMessage] + it( + 'should get node error message from node label when' + + ' showTranslationWarnings is false', + () => { + component.showTranslationWarnings = false; + var nodeErrorMessage = 'Node 1 error message from exploration warnings'; + spyOn( + explorationWarningsService, + 'getAllStateRelatedWarnings' + ).and.returnValue({ + 'This is a label for node 1': [nodeErrorMessage], }); - expect( - component.getNodeErrorMessage('This is a label for node 1')).toBe( - nodeErrorMessage); - }); + expect(component.getNodeErrorMessage('This is a label for node 1')).toBe( + nodeErrorMessage + ); + } + ); it('should center the graph', fakeAsync(() => { component.ngOnInit(); @@ -364,7 +400,7 @@ describe('State Graph Visualization Component when graph is redrawn', () => { right: 30, left: 40, bottom: 5, - top: 20 + top: 20, }; component.centerGraph(); @@ -397,8 +433,8 @@ describe('State Graph Visualization Component when graph is redrawn', () => { secondaryLabel: '2nd', style: 'string', nodeClass: 'string', - canDelete: true - } + canDelete: true, + }, }; component.centerGraph(); tick(); @@ -407,101 +443,105 @@ describe('State Graph Visualization Component when graph is redrawn', () => { flush(); })); - it('should center graph when centerGraph flag is broadcasted and transform' + - ' x and y axis to 0', fakeAsync(() => { - spyOn(component, 'getElementDimensions').and.returnValue({ - w: 1000, - h: 1000 - }); + it( + 'should center graph when centerGraph flag is broadcasted and transform' + + ' x and y axis to 0', + fakeAsync(() => { + spyOn(component, 'getElementDimensions').and.returnValue({ + w: 1000, + h: 1000, + }); - component.ngOnInit(); - tick(); + component.ngOnInit(); + tick(); - spyOn(stateGraphLayoutService, 'getGraphBoundaries').and.returnValue({ - bottom: 20, - left: 10, - top: 10, - right: 20 - }); - flush(); - // Spies for d3 library. - var zoomSpy = jasmine.createSpy('zoom').and.returnValue({ - scaleExtent: () => ({ - on: (evt: string, callback: () => void) => { - callback(); - return { - apply: () => {} - }; - } - }) - }); - spyOnProperty(d3, 'zoom').and.returnValue(zoomSpy); - spyOnProperty(d3, 'event').and.returnValue({ - transform: { - x: 10, - y: 20 - } - }); + spyOn(stateGraphLayoutService, 'getGraphBoundaries').and.returnValue({ + bottom: 20, + left: 10, + top: 10, + right: 20, + }); + flush(); + // Spies for d3 library. + var zoomSpy = jasmine.createSpy('zoom').and.returnValue({ + scaleExtent: () => ({ + on: (evt: string, callback: () => void) => { + callback(); + return { + apply: () => {}, + }; + }, + }), + }); + spyOnProperty(d3, 'zoom').and.returnValue(zoomSpy); + spyOnProperty(d3, 'event').and.returnValue({ + transform: { + x: 10, + y: 20, + }, + }); - component.makeGraphPannable(); - tick(); - flush(); + component.makeGraphPannable(); + tick(); + flush(); - expect(component.innerTransformStr).toBe( - 'translate(10,20)'); - })); + expect(component.innerTransformStr).toBe('translate(10,20)'); + }) + ); - it('should center graph when centerGraph flag is broadcasted and transform' + - ' x and y axis to 10, 20', fakeAsync(() => { - component.ngOnInit(); - tick(); + it( + 'should center graph when centerGraph flag is broadcasted and transform' + + ' x and y axis to 10, 20', + fakeAsync(() => { + component.ngOnInit(); + tick(); - spyOn(component, 'getElementDimensions').and.returnValue({ - w: 1000, - h: 1000 - }); + spyOn(component, 'getElementDimensions').and.returnValue({ + w: 1000, + h: 1000, + }); - jasmine.createSpy('apply').and.stub(); - spyOn(stateGraphLayoutService, 'getGraphBoundaries').and.returnValue({ - bottom: 20, - left: 10, - top: 10, - right: 20 - }); - component.graphBounds = { - right: 30, - left: 40, - bottom: 5, - top: 20 - }; + jasmine.createSpy('apply').and.stub(); + spyOn(stateGraphLayoutService, 'getGraphBoundaries').and.returnValue({ + bottom: 20, + left: 10, + top: 10, + right: 20, + }); + component.graphBounds = { + right: 30, + left: 40, + bottom: 5, + top: 20, + }; - flush(); - // Spies for d3 library. - var zoomSpy = jasmine.createSpy('zoom').and.returnValue({ - scaleExtent: () => ({ - on: (evt: string, callback: () => void) => { - callback(); - return { - apply: () => {} - }; - } - }) - }); - spyOnProperty(d3, 'zoom').and.returnValue(zoomSpy); - spyOnProperty(d3, 'event').and.returnValue({ - transform: { - x: 10, - y: 20 - } - }); + flush(); + // Spies for d3 library. + var zoomSpy = jasmine.createSpy('zoom').and.returnValue({ + scaleExtent: () => ({ + on: (evt: string, callback: () => void) => { + callback(); + return { + apply: () => {}, + }; + }, + }), + }); + spyOnProperty(d3, 'zoom').and.returnValue(zoomSpy); + spyOnProperty(d3, 'event').and.returnValue({ + transform: { + x: 10, + y: 20, + }, + }); - component.makeGraphPannable(); - tick(); - flush(); + component.makeGraphPannable(); + tick(); + flush(); - expect(d3.event.transform.x).toBe(0); - expect(d3.event.transform.y).toBe(0); - expect(component.overallTransformStr).toBe( - 'translate(465,487.5)'); - })); + expect(d3.event.transform.x).toBe(0); + expect(d3.event.transform.y).toBe(0); + expect(component.overallTransformStr).toBe('translate(465,487.5)'); + }) + ); }); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/graph-directives/state-graph-visualization.component.ts b/core/templates/pages/exploration-editor-page/editor-tab/graph-directives/state-graph-visualization.component.ts index fdfba10a642f..3c397e96d565 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/graph-directives/state-graph-visualization.component.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/graph-directives/state-graph-visualization.component.ts @@ -17,22 +17,35 @@ */ import * as d3 from 'd3'; -import { ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { AppConstants } from 'app.constants'; +import { + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + Input, + OnDestroy, + OnInit, + Output, + ViewChild, +} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {AppConstants} from 'app.constants'; import cloneDeep from 'lodash/cloneDeep'; -import { Subscription } from 'rxjs'; -import { TruncatePipe } from 'filters/string-utility-filters/truncate.pipe'; -import { AugmentedLink, NodeDataDict } from 'components/graph-services/graph-layout.service'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; -import { ExplorationWarningsService } from 'pages/exploration-editor-page/services/exploration-warnings.service'; -import { GraphDataService } from 'pages/exploration-editor-page/services/graph-data.service'; -import { GraphNodes, GraphLink, GraphData } from 'services/compute-graph.service'; -import { RouterService } from 'pages/exploration-editor-page/services/router.service'; -import { StateCardIsCheckpointService } from 'components/state-editor/state-editor-properties-services/state-card-is-checkpoint.service'; -import { StateGraphLayoutService } from 'components/graph-services/graph-layout.service'; -import { TranslationStatusService } from 'pages/exploration-editor-page/translation-tab/services/translation-status.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; +import {Subscription} from 'rxjs'; +import {TruncatePipe} from 'filters/string-utility-filters/truncate.pipe'; +import { + AugmentedLink, + NodeDataDict, +} from 'components/graph-services/graph-layout.service'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; +import {ExplorationWarningsService} from 'pages/exploration-editor-page/services/exploration-warnings.service'; +import {GraphDataService} from 'pages/exploration-editor-page/services/graph-data.service'; +import {GraphNodes, GraphLink, GraphData} from 'services/compute-graph.service'; +import {RouterService} from 'pages/exploration-editor-page/services/router.service'; +import {StateCardIsCheckpointService} from 'components/state-editor/state-editor-properties-services/state-card-is-checkpoint.service'; +import {StateGraphLayoutService} from 'components/graph-services/graph-layout.service'; +import {TranslationStatusService} from 'pages/exploration-editor-page/translation-tab/services/translation-status.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; interface ElementDimensions { h: number; @@ -78,10 +91,9 @@ interface OpacityMap { @Component({ selector: 'state-graph-visualization', - templateUrl: './state-graph-visualization.component.html' + templateUrl: './state-graph-visualization.component.html', }) -export class StateGraphVisualization - implements OnInit, OnDestroy { +export class StateGraphVisualization implements OnInit, OnDestroy { // Function called when node is clicked. Should take a parameter // node.id. @Output() onClickFunction = new EventEmitter(); @@ -141,7 +153,7 @@ export class StateGraphVisualization bottom: 0, left: 0, top: 0, - right: 0 + right: 0, }; // The translation applied when the graph is first loaded. @@ -159,8 +171,8 @@ export class StateGraphVisualization private stateGraphLayoutService: StateGraphLayoutService, private translationStatusService: TranslationStatusService, private truncate: TruncatePipe, - private windowDimensionsService: WindowDimensionsService, - ) { } + private windowDimensionsService: WindowDimensionsService + ) {} sendOnMaximizeFunction(): void { this.onMaximizeFunction.emit(); @@ -174,8 +186,10 @@ export class StateGraphVisualization if (this.graphData) { this.graphLoaded = false; this.drawGraph( - this.graphData.nodes, this.graphData.links, - this.graphData.initStateId, this.graphData.finalStateIds + this.graphData.nodes, + this.graphData.links, + this.graphData.initStateId, + this.graphData.finalStateIds ); // Wait for the graph to finish loading before showing it again. @@ -188,21 +202,25 @@ export class StateGraphVisualization makeGraphPannable(): void { setTimeout(() => { let dimensions = this.getElementDimensions(); - d3.selectAll('#pannableRect') - .call( - // This throws "Object is possibly undefined." The type undefined - // comes here from d3-zoom dependency. We need to suppress this - // error because of strict type checking. - // @ts-ignore - d3.zoom().scaleExtent([1, 1]).on('zoom', () => { + d3.selectAll('#pannableRect').call( + // This throws "Object is possibly undefined." The type undefined + // comes here from d3-zoom dependency. We need to suppress this + // error because of strict type checking. + // @ts-ignore + d3 + .zoom() + .scaleExtent([1, 1]) + .on('zoom', () => { if (this.graphBounds.right + this.graphBounds.left < dimensions.w) { - (d3.event).transform.x = 0; + d3.event.transform.x = 0; } else { d3.event.transform.x = this.clamp( d3.event.transform.x, - dimensions.w - this.graphBounds.right - - this.origTranslations[0], - -this.graphBounds.left - this.origTranslations[0]); + dimensions.w - + this.graphBounds.right - + this.origTranslations[0], + -this.graphBounds.left - this.origTranslations[0] + ); } if (this.graphBounds.bottom + this.graphBounds.top < dimensions.h) { @@ -210,19 +228,23 @@ export class StateGraphVisualization } else { d3.event.transform.y = this.clamp( d3.event.transform.y, - dimensions.h - this.graphBounds.bottom - - this.origTranslations[1], - -this.graphBounds.top - this.origTranslations[1]); + dimensions.h - + this.graphBounds.bottom - + this.origTranslations[1], + -this.graphBounds.top - this.origTranslations[1] + ); } // We need a separate layer here so that the translation // does not influence the panning event receivers. - this.innerTransformStr = ( - 'translate(' + d3.event.transform.x + - ',' + d3.event.transform.y + ')' - ); + this.innerTransformStr = + 'translate(' + + d3.event.transform.x + + ',' + + d3.event.transform.y + + ')'; }) - ); + ); }); } @@ -253,7 +275,7 @@ export class StateGraphVisualization return { h: Number(height.valueInSpecifiedUnits), - w: Number(width.valueInSpecifiedUnits) + w: Number(width.valueInSpecifiedUnits), }; } @@ -271,49 +293,52 @@ export class StateGraphVisualization return; } - this.origTranslations[0] = ( - dimensions.w / 2 - this.nodeData[this.currentStateId].x0 - - this.nodeData[this.currentStateId].width / 2); + this.origTranslations[0] = + dimensions.w / 2 - + this.nodeData[this.currentStateId].x0 - + this.nodeData[this.currentStateId].width / 2; - this.origTranslations[1] = ( - dimensions.h / 2 - this.nodeData[this.currentStateId].y0 - - this.nodeData[this.currentStateId].height / 2); + this.origTranslations[1] = + dimensions.h / 2 - + this.nodeData[this.currentStateId].y0 - + this.nodeData[this.currentStateId].height / 2; if (this.graphBounds.right - this.graphBounds.left < dimensions.w) { - this.origTranslations[0] = ( + this.origTranslations[0] = dimensions.w / 2 - - (this.graphBounds.right + this.graphBounds.left) / 2); + (this.graphBounds.right + this.graphBounds.left) / 2; } else { this.origTranslations[0] = this.clamp( this.origTranslations[0], dimensions.w - this.graphBounds.right, - -this.graphBounds.left); + -this.graphBounds.left + ); } if (this.graphBounds.bottom - this.graphBounds.top < dimensions.h) { - this.origTranslations[1] = ( + this.origTranslations[1] = dimensions.h / 2 - - (this.graphBounds.bottom + this.graphBounds.top) / 2); + (this.graphBounds.bottom + this.graphBounds.top) / 2; } else { this.origTranslations[1] = this.clamp( this.origTranslations[1], dimensions.h - this.graphBounds.bottom, - -this.graphBounds.top); + -this.graphBounds.top + ); } - this.overallTransformStr = ( - 'translate(' + this.origTranslations + ')'); + this.overallTransformStr = 'translate(' + this.origTranslations + ')'; }, 20); } } getNodeStrokeWidth(nodeId: string): string { - let currentNodeIsTerminal = ( - this.finalStateIds.indexOf(nodeId) !== -1); - return ( - nodeId === this.currentStateId ? '3' : - (nodeId === this.initStateId2 || currentNodeIsTerminal) ? - '2' : '1'); + let currentNodeIsTerminal = this.finalStateIds.indexOf(nodeId) !== -1; + return nodeId === this.currentStateId + ? '3' + : nodeId === this.initStateId2 || currentNodeIsTerminal + ? '2' + : '1'; } getNodeFillOpacity(nodeId: string): number { @@ -329,9 +354,7 @@ export class StateGraphVisualization if (node.reachable === false) { warning = 'Warning: this state is unreachable.'; } else if (node.reachableFromEnd === false) { - warning = ( - 'Warning: there is no path from this state to the END state.' - ); + warning = 'Warning: there is no path from this state to the END state.'; } else { warning = this.getNodeErrorMessage(node.label); } @@ -369,28 +392,42 @@ export class StateGraphVisualization getTruncatedLabel(nodeLabel: string): string { return this.truncate.transform( nodeLabel, - AppConstants.MAX_NODE_LABEL_LENGTH); + AppConstants.MAX_NODE_LABEL_LENGTH + ); } drawGraph( - nodes: GraphNodes, originalLinks: GraphLink[], - initStateId: string, finalStateIds: string[]): void { + nodes: GraphNodes, + originalLinks: GraphLink[], + initStateId: string, + finalStateIds: string[] + ): void { const that = this; this.initStateId = initStateId; this.finalStateIds = finalStateIds; let links = cloneDeep(originalLinks); this.nodeData = this.stateGraphLayoutService.computeLayout( - nodes, links, initStateId, cloneDeep(finalStateIds)); + nodes, + links, + initStateId, + cloneDeep(finalStateIds) + ); this.GRAPH_WIDTH = this.stateGraphLayoutService.getGraphWidth( - AppConstants.MAX_NODES_PER_ROW, AppConstants.MAX_NODE_LABEL_LENGTH); + AppConstants.MAX_NODES_PER_ROW, + AppConstants.MAX_NODE_LABEL_LENGTH + ); this.GRAPH_HEIGHT = this.stateGraphLayoutService.getGraphHeight( - this.nodeData); + this.nodeData + ); this.nodeData = this.stateGraphLayoutService.modifyPositionValues( - this.nodeData, this.GRAPH_WIDTH, this.GRAPH_HEIGHT); + this.nodeData, + this.GRAPH_WIDTH, + this.GRAPH_HEIGHT + ); // These constants correspond to the rectangle that, when clicked // and dragged, translates the graph. Its height, width, and x and @@ -402,21 +439,28 @@ export class StateGraphVisualization this.VIEWPORT_Y = -Math.max(1000, this.GRAPH_HEIGHT * 2) + 'px'; this.graphBounds = this.stateGraphLayoutService.getGraphBoundaries( - this.nodeData); + this.nodeData + ); this.augmentedLinks = this.stateGraphLayoutService.getAugmentedLinks( - this.nodeData, links); + this.nodeData, + links + ); for (let i = 0; i < this.augmentedLinks.length; i++) { - // Style links if link properties and style mappings are - // provided. + // Style links if link properties and style mappings are + // provided. let linkProperty = links[i].linkProperty; - if ('linkProperty' in links[i] && linkProperty && - this.linkPropertyMapping) { + if ( + 'linkProperty' in links[i] && + linkProperty && + this.linkPropertyMapping + ) { if (linkProperty in this.linkPropertyMapping) { - this.augmentedLinks[i].style = ( + this.augmentedLinks[i].style = this.linkPropertyMapping[ - linkProperty as keyof typeof that.linkPropertyMapping]); + linkProperty as keyof typeof that.linkPropertyMapping + ]; } } } @@ -424,42 +468,49 @@ export class StateGraphVisualization // Update the nodes. this.nodeList = []; for (let nodeId in this.nodeData) { - this.nodeData[nodeId].style = ( - 'stroke-width: ' + this.getNodeStrokeWidth(nodeId) + '; ' + - 'fill-opacity: ' + this.getNodeFillOpacity(nodeId) + ';'); + this.nodeData[nodeId].style = + 'stroke-width: ' + + this.getNodeStrokeWidth(nodeId) + + '; ' + + 'fill-opacity: ' + + this.getNodeFillOpacity(nodeId) + + ';'; if (this.nodeFill) { - this.nodeData[nodeId].style += ('fill: ' + this.nodeFill + '; '); + this.nodeData[nodeId].style += 'fill: ' + this.nodeFill + '; '; } // ---- Color nodes ---- let nodeColors = this.nodeColors; if (nodeColors) { - this.nodeData[nodeId].style += ( - 'fill: ' + nodeColors[nodeId] + '; '); + this.nodeData[nodeId].style += 'fill: ' + nodeColors[nodeId] + '; '; } // Add secondary label if it exists. if (this.nodeSecondaryLabels) { if (nodeId in this.nodeSecondaryLabels) { - this.nodeData[nodeId].secondaryLabel = ( - this.nodeSecondaryLabels[nodeId]); + this.nodeData[nodeId].secondaryLabel = + this.nodeSecondaryLabels[nodeId]; this.nodeData[nodeId].height *= 1.1; } } - let currentNodeIsTerminal = ( - this.finalStateIds.indexOf(nodeId) !== -1); - - this.nodeData[nodeId].nodeClass = ( - currentNodeIsTerminal ? 'terminal-node' : - nodeId === this.currentStateId ? 'current-node' : - nodeId === initStateId ? 'init-node' : !( - this.nodeData[nodeId].reachable && - this.nodeData[nodeId].reachableFromEnd) ? 'bad-node' : - 'normal-node'); - - this.nodeData[nodeId].canDelete = (nodeId !== initStateId); + let currentNodeIsTerminal = this.finalStateIds.indexOf(nodeId) !== -1; + + this.nodeData[nodeId].nodeClass = currentNodeIsTerminal + ? 'terminal-node' + : nodeId === this.currentStateId + ? 'current-node' + : nodeId === initStateId + ? 'init-node' + : !( + this.nodeData[nodeId].reachable && + this.nodeData[nodeId].reachableFromEnd + ) + ? 'bad-node' + : 'normal-node'; + + this.nodeData[nodeId].canDelete = nodeId !== initStateId; this.nodeList.push(this.nodeData[nodeId]); } @@ -479,11 +530,9 @@ export class StateGraphVisualization getNodeErrorMessage(nodeLabel: string): string | null { let warnings = null; if (this.showTranslationWarnings) { - warnings = - this.translationStatusService.getAllStatesNeedUpdatewarning(); + warnings = this.translationStatusService.getAllStatesNeedUpdatewarning(); } else { - warnings = - this.explorationWarningsService.getAllStateRelatedWarnings(); + warnings = this.explorationWarningsService.getAllStateRelatedWarnings(); } if (nodeLabel in warnings) { @@ -520,7 +569,8 @@ export class StateGraphVisualization if ( this.versionGraphData !== null && this.versionGraphData && - this.versionGraphData !== undefined) { + this.versionGraphData !== undefined + ) { return; } @@ -536,7 +586,8 @@ export class StateGraphVisualization if ( this.versionGraphData !== null && - this.versionGraphData !== undefined) { + this.versionGraphData !== undefined + ) { this.graphData = this.versionGraphData; } @@ -552,7 +603,9 @@ export class StateGraphVisualization } } -angular.module('oppia').directive('stateGraphVisualization', - downgradeComponent({ - component: StateGraphVisualization - }) as angular.IDirectiveFactory); +angular.module('oppia').directive( + 'stateGraphVisualization', + downgradeComponent({ + component: StateGraphVisualization, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/services/interaction-details-cache.service.spec.ts b/core/templates/pages/exploration-editor-page/editor-tab/services/interaction-details-cache.service.spec.ts index 2a552571227b..3048ff63fad2 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/services/interaction-details-cache.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/services/interaction-details-cache.service.spec.ts @@ -16,18 +16,18 @@ * @fileoverview Unit tests for Interaction Details Cache Service. */ -import { InteractionDetailsCacheService } from 'pages/exploration-editor-page/editor-tab/services/interaction-details-cache.service'; +import {InteractionDetailsCacheService} from 'pages/exploration-editor-page/editor-tab/services/interaction-details-cache.service'; describe('Interaction Details Cache Service', () => { describe('InteractionDetailsCache', () => { var interactionCustomizationArgs = { choices: { - value: 'SampleChoice' - } + value: 'SampleChoice', + }, }; var interaction = { - customization: interactionCustomizationArgs + customization: interactionCustomizationArgs, }; var idcs: InteractionDetailsCacheService; diff --git a/core/templates/pages/exploration-editor-page/editor-tab/services/interaction-details-cache.service.ts b/core/templates/pages/exploration-editor-page/editor-tab/services/interaction-details-cache.service.ts index bec47bf4f0ec..7cd0917f10a8 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/services/interaction-details-cache.service.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/services/interaction-details-cache.service.ts @@ -22,18 +22,17 @@ import cloneDeep from 'lodash/cloneDeep'; -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; -import { InteractionCustomizationArgs } from - 'interactions/customization-args-defs'; +import {InteractionCustomizationArgs} from 'interactions/customization-args-defs'; interface InteractionDetailsCache { [interactionId: string]: InteractionCustomizationArgs; } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class InteractionDetailsCacheService { static _cache: InteractionDetailsCache = {}; @@ -51,10 +50,11 @@ export class InteractionDetailsCacheService { } set( - interactionId: string, - interactionCustomizationArgs: InteractionCustomizationArgs): void { + interactionId: string, + interactionCustomizationArgs: InteractionCustomizationArgs + ): void { InteractionDetailsCacheService._cache[interactionId] = { - customization: cloneDeep(interactionCustomizationArgs) + customization: cloneDeep(interactionCustomizationArgs), }; } @@ -66,6 +66,9 @@ export class InteractionDetailsCacheService { } } -angular.module('oppia').factory( - 'InteractionDetailsCacheService', - downgradeInjectable(InteractionDetailsCacheService)); +angular + .module('oppia') + .factory( + 'InteractionDetailsCacheService', + downgradeInjectable(InteractionDetailsCacheService) + ); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/services/responses.service.spec.ts b/core/templates/pages/exploration-editor-page/editor-tab/services/responses.service.spec.ts index 6da418603f81..9f7c05e2ae20 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/services/responses.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/services/responses.service.spec.ts @@ -16,28 +16,35 @@ * @fileoverview Unit tests for ResponsesService. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { EventEmitter } from '@angular/core'; -import { fakeAsync, TestBed } from '@angular/core/testing'; - -import { AnswerGroup, AnswerGroupObjectFactory } from 'domain/exploration/AnswerGroupObjectFactory'; -import { AlertsService } from 'services/alerts.service'; -import { ExplorationHtmlFormatterService } from 'services/exploration-html-formatter.service'; -import { Interaction, InteractionObjectFactory } from 'domain/exploration/InteractionObjectFactory'; -import { LoggerService } from 'services/contextual/logger.service'; -import { Outcome, OutcomeObjectFactory } from 'domain/exploration/OutcomeObjectFactory'; -import { ResponsesService } from 'pages/exploration-editor-page/editor-tab/services/responses.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {EventEmitter} from '@angular/core'; +import {fakeAsync, TestBed} from '@angular/core/testing'; + +import { + AnswerGroup, + AnswerGroupObjectFactory, +} from 'domain/exploration/AnswerGroupObjectFactory'; +import {AlertsService} from 'services/alerts.service'; +import {ExplorationHtmlFormatterService} from 'services/exploration-html-formatter.service'; +import { + Interaction, + InteractionObjectFactory, +} from 'domain/exploration/InteractionObjectFactory'; +import {LoggerService} from 'services/contextual/logger.service'; +import { + Outcome, + OutcomeObjectFactory, +} from 'domain/exploration/OutcomeObjectFactory'; +import {ResponsesService} from 'pages/exploration-editor-page/editor-tab/services/responses.service'; import { StateEditorService, // eslint-disable-next-line max-len } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { StateInteractionIdService } from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; -import { StateSolutionService } from 'components/state-editor/state-editor-properties-services/state-solution.service'; -import { - SubtitledHtml, -} from 'domain/exploration/subtitled-html.model'; -import { Rule } from 'domain/exploration/rule.model'; -import { Solution } from 'domain/exploration/SolutionObjectFactory'; +import {StateInteractionIdService} from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; +import {StateSolutionService} from 'components/state-editor/state-editor-properties-services/state-solution.service'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {Rule} from 'domain/exploration/rule.model'; +import {Solution} from 'domain/exploration/SolutionObjectFactory'; describe('Responses Service', () => { let alertsService: AlertsService; @@ -56,7 +63,7 @@ describe('Responses Service', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] + imports: [HttpClientTestingModule], }); answerGroupObjectFactory = TestBed.get(AnswerGroupObjectFactory); alertsService = TestBed.get(AlertsService); @@ -72,7 +79,9 @@ describe('Responses Service', () => { stateSolutionService = TestBed.get(StateSolutionService); savedMemento = new Solution( - explorationHtmlFormatterService, true, 'This is the correct answer', + explorationHtmlFormatterService, + true, + 'This is the correct answer', new SubtitledHtml('', 'tesster') ); @@ -118,8 +127,8 @@ describe('Responses Service', () => { value: 1, }, catchMisspellings: { - value: false - } + value: false, + }, }, hints: [], solution: { @@ -182,8 +191,8 @@ describe('Responses Service', () => { value: 1, }, catchMisspellings: { - value: false - } + value: false, + }, }, hints: [], solution: { @@ -262,17 +271,26 @@ describe('Responses Service', () => { stateEditorService.setInteraction(interactionData); const updatedAnswerGroup = new AnswerGroup( - [new Rule( - 'Contains', { - x: { - contentId: 'rule_input_Contains', - normalizedStrSet: ['correct'] + [ + new Rule( + 'Contains', + { + x: { + contentId: 'rule_input_Contains', + normalizedStrSet: ['correct'], + }, }, - }, {})], + {} + ), + ], new Outcome( - 'State', null, + 'State', + null, new SubtitledHtml('', 'This is a new feedback text'), - true, [], 'test', 'test_skill_id' + true, + [], + 'test', + 'test_skill_id' ), ['This is training data text'], '' @@ -294,7 +312,7 @@ describe('Responses Service', () => { stateEditorService.setInteraction(interactionData); const updatedAnswerGroup = { - rules: [new Rule('Contains', { x: 'correct'}, {})], + rules: [new Rule('Contains', {x: 'correct'}, {})], outcome: { dest: 'State', feedback: new SubtitledHtml('', 'This is a new feedback text'), @@ -426,7 +444,7 @@ describe('Responses Service', () => { it( 'should update answer choices when savedMemento is ItemSelectionInput' + - ' and choices has its positions changed', + ' and choices has its positions changed', () => { responsesService.init(interactionDataWithRules); stateEditorService.setInteraction(interactionDataWithRules); @@ -481,7 +499,7 @@ describe('Responses Service', () => { it( 'should update answer choices when savedMemento is ItemSelectionInput' + - ' and choices has its values changed', + ' and choices has its values changed', () => { responsesService.init(interactionDataWithRules); stateEditorService.setInteraction(interactionDataWithRules); @@ -533,8 +551,8 @@ describe('Responses Service', () => { it( 'should update answer choices when savedMemento is' + - ' DragAndDropSortInput and rule type is' + - ' HasElementXAtPositionY', + ' DragAndDropSortInput and rule type is' + + ' HasElementXAtPositionY', () => { interactionDataWithRules.id = 'DragAndDropSortInput'; interactionDataWithRules.answerGroups[0].rules[0].type = @@ -585,8 +603,8 @@ describe('Responses Service', () => { it( 'should update answer choices when savedMemento is' + - ' DragAndDropSortInput and rule type is' + - ' HasElementXBeforeElementY', + ' DragAndDropSortInput and rule type is' + + ' HasElementXBeforeElementY', () => { interactionDataWithRules.id = 'DragAndDropSortInput'; interactionDataWithRules.answerGroups[0].rules[0].type = @@ -642,7 +660,7 @@ describe('Responses Service', () => { it( 'should update answer choices when savedMemento is' + - ' DragAndDropSortInput and choices had changed', + ' DragAndDropSortInput and choices had changed', () => { interactionDataWithRules.id = 'DragAndDropSortInput'; // Any other method from DragAndDropSortInputRulesService. @@ -699,7 +717,7 @@ describe('Responses Service', () => { it( 'should update answer choices when savedMemento is' + - ' DragAndDropSortInput and choices has its positions changed', + ' DragAndDropSortInput and choices has its positions changed', () => { responsesService.init(interactionDataWithRules); stateEditorService.setInteraction(interactionDataWithRules); @@ -771,20 +789,19 @@ describe('Responses Service', () => { ); }); - it('should change interaction when id does not exist in any answer group', - () => { - responsesService.init(interactionData); - stateEditorService.setInteraction(interactionData); + it('should change interaction when id does not exist in any answer group', () => { + responsesService.init(interactionData); + stateEditorService.setInteraction(interactionData); - const newInteractionId = 'Continue'; - const callbackSpy = jasmine.createSpy('callback'); - responsesService.onInteractionIdChanged(newInteractionId, callbackSpy); + const newInteractionId = 'Continue'; + const callbackSpy = jasmine.createSpy('callback'); + responsesService.onInteractionIdChanged(newInteractionId, callbackSpy); - expect(callbackSpy).toHaveBeenCalledWith( - [], - interactionData.defaultOutcome - ); - }); + expect(callbackSpy).toHaveBeenCalledWith( + [], + interactionData.defaultOutcome + ); + }); it('should change interaction', () => { stateInteractionIdService.init('stateName', 'TextInput'); @@ -819,7 +836,7 @@ describe('Responses Service', () => { it( "should change interaction id when interaction is terminal and it's" + - ' not cached', + ' not cached', () => { responsesService.init(interactionData); stateEditorService.setInteraction(interactionData); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/services/responses.service.ts b/core/templates/pages/exploration-editor-page/editor-tab/services/responses.service.ts index 18a3a491638d..05ed184d82b9 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/services/responses.service.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/services/responses.service.ts @@ -19,31 +19,37 @@ import cloneDeep from 'lodash/cloneDeep'; -import { EventEmitter } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; - -import { AlertsService } from 'services/alerts.service'; -import { AnswerChoice, StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { AnswerGroup } from 'domain/exploration/AnswerGroupObjectFactory'; -import { AppConstants } from 'app.constants'; -import { ExplorationEditorPageConstants } from 'pages/exploration-editor-page/exploration-editor-page.constants'; -import { Interaction } from 'domain/exploration/InteractionObjectFactory'; -import { InteractionAnswer } from 'interactions/answer-defs'; -import { ItemSelectionInputCustomizationArgs } from 'interactions/customization-args-defs'; -import { InteractionRuleInputs } from 'interactions/rule-input-defs'; -import { LoggerService } from 'services/contextual/logger.service'; -import { Outcome, OutcomeObjectFactory, } from 'domain/exploration/OutcomeObjectFactory'; -import { SolutionValidityService } from 'pages/exploration-editor-page/editor-tab/services/solution-validity.service'; -import { SolutionVerificationService } from 'pages/exploration-editor-page/editor-tab/services/solution-verification.service'; -import { StateCustomizationArgsService } from 'components/state-editor/state-editor-properties-services/state-customization-args.service'; -import { StateInteractionIdService } from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; -import { StateSolutionService } from 'components/state-editor/state-editor-properties-services/state-solution.service'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; +import {EventEmitter} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; + +import {AlertsService} from 'services/alerts.service'; +import { + AnswerChoice, + StateEditorService, +} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {AnswerGroup} from 'domain/exploration/AnswerGroupObjectFactory'; +import {AppConstants} from 'app.constants'; +import {ExplorationEditorPageConstants} from 'pages/exploration-editor-page/exploration-editor-page.constants'; +import {Interaction} from 'domain/exploration/InteractionObjectFactory'; +import {InteractionAnswer} from 'interactions/answer-defs'; +import {ItemSelectionInputCustomizationArgs} from 'interactions/customization-args-defs'; +import {InteractionRuleInputs} from 'interactions/rule-input-defs'; +import {LoggerService} from 'services/contextual/logger.service'; +import { + Outcome, + OutcomeObjectFactory, +} from 'domain/exploration/OutcomeObjectFactory'; +import {SolutionValidityService} from 'pages/exploration-editor-page/editor-tab/services/solution-validity.service'; +import {SolutionVerificationService} from 'pages/exploration-editor-page/editor-tab/services/solution-verification.service'; +import {StateCustomizationArgsService} from 'components/state-editor/state-editor-properties-services/state-customization-args.service'; +import {StateInteractionIdService} from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; +import {StateSolutionService} from 'components/state-editor/state-editor-properties-services/state-solution.service'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; import INTERACTION_SPECS from 'interactions/interaction_specs.json'; -import { InteractionSpecsKey } from 'pages/interaction-specs.constants'; -import { Rule } from 'domain/exploration/rule.model'; -import { InitializeAnswerGroups } from 'components/state-editor/state-interaction-editor/state-interaction-editor.component'; +import {InteractionSpecsKey} from 'pages/interaction-specs.constants'; +import {Rule} from 'domain/exploration/rule.model'; +import {InitializeAnswerGroups} from 'components/state-editor/state-interaction-editor/state-interaction-editor.component'; interface UpdateActiveAnswerGroupDest { dest: string; @@ -71,14 +77,13 @@ interface UpdateRule { rules: Rule[]; } -export type UpdateActiveAnswerGroup = ( - AnswerGroup | - DestIfReallyStuck | - UpdateAnswerGroupFeedback | - UpdateAnswerGroupCorrectnessLabel | - UpdateActiveAnswerGroupDest | - UpdateRule -); +export type UpdateActiveAnswerGroup = + | AnswerGroup + | DestIfReallyStuck + | UpdateAnswerGroupFeedback + | UpdateAnswerGroupCorrectnessLabel + | UpdateActiveAnswerGroupDest + | UpdateRule; @Injectable({ providedIn: 'root', @@ -121,16 +126,14 @@ export class ResponsesService { // This checks if the solution is valid once a rule has been changed or // added. const currentInteractionId = this.stateInteractionIdService.savedMemento; - const interactionCanHaveSolution = ( + const interactionCanHaveSolution = currentInteractionId && - INTERACTION_SPECS[ - currentInteractionId as InteractionSpecsKey - ].can_have_solution); - const stateSolutionSavedMemento = ( - this.stateSolutionService.savedMemento); - const solutionExists = ( + INTERACTION_SPECS[currentInteractionId as InteractionSpecsKey] + .can_have_solution; + const stateSolutionSavedMemento = this.stateSolutionService.savedMemento; + const solutionExists = stateSolutionSavedMemento && - stateSolutionSavedMemento.correctAnswer !== null); + stateSolutionSavedMemento.correctAnswer !== null; const activeStateName = this.stateEditorService.getActiveStateName(); if ( @@ -143,22 +146,25 @@ export class ResponsesService { interaction.answerGroups = cloneDeep(this._answerGroups); interaction.defaultOutcome = cloneDeep(this._defaultOutcome); const solutionIsValid = this.solutionVerificationService.verifySolution( - activeStateName, interaction, stateSolutionSavedMemento.correctAnswer + activeStateName, + interaction, + stateSolutionSavedMemento.correctAnswer ); - - const solutionWasPreviouslyValid = ( - this.solutionValidityService.isSolutionValid(activeStateName)); + const solutionWasPreviouslyValid = + this.solutionValidityService.isSolutionValid(activeStateName); this.solutionValidityService.updateValidity( - activeStateName, solutionIsValid); + activeStateName, + solutionIsValid + ); if (solutionIsValid && !solutionWasPreviouslyValid) { this.alertsService.addInfoMessage( - ExplorationEditorPageConstants.INFO_MESSAGE_SOLUTION_IS_VALID); + ExplorationEditorPageConstants.INFO_MESSAGE_SOLUTION_IS_VALID + ); } else if (!solutionIsValid && solutionWasPreviouslyValid) { this.alertsService.addInfoMessage( - ExplorationEditorPageConstants. - INFO_MESSAGE_SOLUTION_IS_INVALID_FOR_CURRENT_RULE + ExplorationEditorPageConstants.INFO_MESSAGE_SOLUTION_IS_INVALID_FOR_CURRENT_RULE ); } else if (!solutionIsValid && !solutionWasPreviouslyValid) { this.alertsService.addInfoMessage( @@ -183,26 +189,26 @@ export class ResponsesService { }; private _updateAnswerGroup = ( - index: number, - updates: UpdateActiveAnswerGroup, - callback: (value: AnswerGroup[]) => void + index: number, + updates: UpdateActiveAnswerGroup, + callback: (value: AnswerGroup[]) => void ) => { const answerGroup = this._answerGroups[index]; if (answerGroup) { if (updates.hasOwnProperty('rules')) { - let ruleUpdates = updates as { rules: Rule[] }; + let ruleUpdates = updates as {rules: Rule[]}; answerGroup.rules = ruleUpdates.rules; } if (updates.hasOwnProperty('taggedSkillMisconceptionId')) { let taggedSkillMisconceptionIdUpdates = updates as { taggedSkillMisconceptionId: string; }; - answerGroup.taggedSkillMisconceptionId = ( - taggedSkillMisconceptionIdUpdates.taggedSkillMisconceptionId); + answerGroup.taggedSkillMisconceptionId = + taggedSkillMisconceptionIdUpdates.taggedSkillMisconceptionId; } if (updates.hasOwnProperty('feedback')) { - let feedbackUpdates = updates as { feedback: SubtitledHtml }; + let feedbackUpdates = updates as {feedback: SubtitledHtml}; answerGroup.outcome.feedback = feedbackUpdates.feedback; } if (updates.hasOwnProperty('dest')) { @@ -213,29 +219,29 @@ export class ResponsesService { let refresherExplorationIdUpdates = updates as { refresherExplorationId: string; }; - answerGroup.outcome.refresherExplorationId = ( - refresherExplorationIdUpdates.refresherExplorationId); + answerGroup.outcome.refresherExplorationId = + refresherExplorationIdUpdates.refresherExplorationId; } if (updates.hasOwnProperty('missingPrerequisiteSkillId')) { let missingPrerequisiteSkillIdUpdates = updates as { missingPrerequisiteSkillId: string; }; - answerGroup.outcome.missingPrerequisiteSkillId = ( - missingPrerequisiteSkillIdUpdates.missingPrerequisiteSkillId); + answerGroup.outcome.missingPrerequisiteSkillId = + missingPrerequisiteSkillIdUpdates.missingPrerequisiteSkillId; } if (updates.hasOwnProperty('labelledAsCorrect')) { let labelledAsCorrectUpdates = updates as { labelledAsCorrect: boolean; }; - answerGroup.outcome.labelledAsCorrect = ( - labelledAsCorrectUpdates.labelledAsCorrect); + answerGroup.outcome.labelledAsCorrect = + labelledAsCorrectUpdates.labelledAsCorrect; } if (updates.hasOwnProperty('destIfReallyStuck')) { let destIfReallyStuckUpdates = updates as { destIfReallyStuck: string | null; }; - answerGroup.outcome.destIfReallyStuck = ( - destIfReallyStuckUpdates.destIfReallyStuck); + answerGroup.outcome.destIfReallyStuck = + destIfReallyStuckUpdates.destIfReallyStuck; } if (updates.hasOwnProperty('trainingData')) { let trainingDataUpdates = updates as AnswerGroup; @@ -262,10 +268,10 @@ export class ResponsesService { }; private _saveConfirmedUnclassifiedAnswers = ( - newConfirmedUnclassifiedAnswers: readonly InteractionAnswer[] + newConfirmedUnclassifiedAnswers: readonly InteractionAnswer[] ) => { - const oldConfirmedUnclassifiedAnswers = this - ._confirmedUnclassifiedAnswersMemento; + const oldConfirmedUnclassifiedAnswers = + this._confirmedUnclassifiedAnswersMemento; if ( !angular.equals( newConfirmedUnclassifiedAnswers, @@ -274,8 +280,8 @@ export class ResponsesService { ) { this._confirmedUnclassifiedAnswers = newConfirmedUnclassifiedAnswers; - this._confirmedUnclassifiedAnswersMemento = ( - cloneDeep(newConfirmedUnclassifiedAnswers) + this._confirmedUnclassifiedAnswersMemento = cloneDeep( + newConfirmedUnclassifiedAnswers ); } }; @@ -338,8 +344,8 @@ export class ResponsesService { } onInteractionIdChanged( - newInteractionId: string, - callback: (value: AnswerGroup[], value2: Outcome | null) => void + newInteractionId: string, + callback: (value: AnswerGroup[], value2: Outcome | null) => void ): void { this._answerGroups = []; @@ -347,9 +353,9 @@ export class ResponsesService { // Recreate the default outcome if switching away from a terminal // interaction. if (newInteractionId) { - if (INTERACTION_SPECS[ - newInteractionId as InteractionSpecsKey - ].is_terminal) { + if ( + INTERACTION_SPECS[newInteractionId as InteractionSpecsKey].is_terminal + ) { this._defaultOutcome = null; } else if (!this._defaultOutcome) { const stateName = this.stateEditorService.getActiveStateName(); @@ -404,8 +410,7 @@ export class ResponsesService { // This array contains the text of each of the possible answers // for the interaction. let answerChoices = []; - let customizationArgs = ( - this.stateCustomizationArgsService.savedMemento); + let customizationArgs = this.stateCustomizationArgsService.savedMemento; let handledAnswersArray: InteractionRuleInputs[] = []; if (interactionId === 'MultipleChoiceInput') { @@ -423,13 +428,13 @@ export class ResponsesService { } // We only suppress the default warning if each choice index has // been handled by at least one answer group. - return choiceIndices.every((choiceIndex) => { + return choiceIndices.every(choiceIndex => { return handledAnswersArray.indexOf(choiceIndex) !== -1; }); } else if (interactionId === 'ItemSelectionInput') { let maxSelectionCount = ( - (customizationArgs as ItemSelectionInputCustomizationArgs) - .maxAllowableSelectionCount.value); + customizationArgs as ItemSelectionInputCustomizationArgs + ).maxAllowableSelectionCount.value; if (maxSelectionCount === 1) { let numChoices = this.getAnswerChoices().length; // This array contains a list of booleans, one for each answer @@ -441,20 +446,21 @@ export class ResponsesService { answerChoices.push(this.getAnswerChoices()[i].val); } - let answerChoiceToIndex: - Record = {}; + let answerChoiceToIndex: Record = {}; answerChoices.forEach((answerChoice, choiceIndex) => { answerChoiceToIndex[answerChoice as string] = choiceIndex; }); - answerGroups.forEach((answerGroup) => { + answerGroups.forEach(answerGroup => { let rules = answerGroup.rules; - rules.forEach((rule) => { + rules.forEach(rule => { let ruleInputs = rule.inputs.x; - Object.keys(ruleInputs).forEach((ruleInput) => { + Object.keys(ruleInputs).forEach(ruleInput => { let choiceIndex = answerChoiceToIndex[ruleInput]; - if (rule.type === 'Equals' || - rule.type === 'ContainsAtLeastOneOf') { + if ( + rule.type === 'Equals' || + rule.type === 'ContainsAtLeastOneOf' + ) { handledAnswersArray[choiceIndex] = true; } else if (rule.type === 'DoesNotContainAtLeastOneOf') { for (let i = 0; i < handledAnswersArray.length; i++) { @@ -467,10 +473,9 @@ export class ResponsesService { }); }); - let areAllChoicesCovered = handledAnswersArray.every( - (handledAnswer) => { - return handledAnswer; - }); + let areAllChoicesCovered = handledAnswersArray.every(handledAnswer => { + return handledAnswer; + }); // We only suppress the default warning if each choice text has // been handled by at least one answer group, based on rule // type. @@ -481,16 +486,16 @@ export class ResponsesService { } updateAnswerGroup( - index: number, - updates: AnswerGroup, - callback: (value: AnswerGroup[]) => void + index: number, + updates: AnswerGroup, + callback: (value: AnswerGroup[]) => void ): void { this._updateAnswerGroup(index, updates, callback); } deleteAnswerGroup( - index: number, - callback: (value: AnswerGroup[]) => void + index: number, + callback: (value: AnswerGroup[]) => void ): void { this._answerGroupsMemento = cloneDeep(this._answerGroups); this._answerGroups.splice(index, 1); @@ -500,15 +505,15 @@ export class ResponsesService { } updateActiveAnswerGroup( - updates: UpdateActiveAnswerGroup, - callback: (value: AnswerGroup[]) => void + updates: UpdateActiveAnswerGroup, + callback: (value: AnswerGroup[]) => void ): void { this._updateAnswerGroup(this._activeAnswerGroupIndex, updates, callback); } updateDefaultOutcome( - updates: Outcome, - callback: (value: Outcome | null) => void + updates: Outcome, + callback: (value: Outcome | null) => void ): void { const outcome = this._defaultOutcome; if (!outcome) { @@ -537,7 +542,7 @@ export class ResponsesService { } updateConfirmedUnclassifiedAnswers( - confirmedUnclassifiedAnswers: InteractionAnswer[] + confirmedUnclassifiedAnswers: InteractionAnswer[] ): void { this._saveConfirmedUnclassifiedAnswers(confirmedUnclassifiedAnswers); } @@ -552,8 +557,8 @@ export class ResponsesService { // Handles changes to custom args by updating the answer choices // accordingly. handleCustomArgsUpdate( - newAnswerChoices: AnswerChoice[], - callback: (value: AnswerGroup[]) => void + newAnswerChoices: AnswerChoice[], + callback: (value: AnswerGroup[]) => void ): void { const oldAnswerChoices = this._updateAnswerChoices(newAnswerChoices); // If the interaction is ItemSelectionInput, update the answer groups @@ -592,17 +597,17 @@ export class ResponsesService { } } - const oldChoiceStrings = oldAnswerChoices.map((choice) => { + const oldChoiceStrings = oldAnswerChoices.map(choice => { return choice.val; }); - const newChoiceStrings = newAnswerChoices.map((choice) => { + const newChoiceStrings = newAnswerChoices.map(choice => { return choice.val; }); let key: string, newInputValue: (string | number | SubtitledHtml)[]; this._answerGroups.forEach((answerGroup, answerGroupIndex) => { const newRules = cloneDeep(answerGroup.rules); - newRules.forEach((rule) => { + newRules.forEach(rule => { for (key in rule.inputs) { newInputValue = []; let inputValue = rule.inputs[key] as string[]; @@ -668,7 +673,7 @@ export class ResponsesService { if (anyChangesHappened) { this._answerGroups.forEach((answerGroup, answerGroupIndex) => { const newRules = cloneDeep(answerGroup.rules); - newRules.forEach((rule) => { + newRules.forEach(rule => { if (rule.type === 'HasElementXAtPositionY') { rule.inputs.x = newAnswerChoices[0].val; rule.inputs.y = 1; @@ -676,7 +681,7 @@ export class ResponsesService { rule.inputs.x = newAnswerChoices[0].val; rule.inputs.y = newAnswerChoices[1].val; } else { - rule.inputs.x = newAnswerChoices.map(({ val }) => [val]); + rule.inputs.x = newAnswerChoices.map(({val}) => [val]); } }); @@ -694,9 +699,9 @@ export class ResponsesService { // This registers the change to the handlers in the list of changes. save( - newAnswerGroups: AnswerGroup[], - defaultOutcome: Outcome | null, - callback: (value: AnswerGroup[], value2: Outcome | null) => void + newAnswerGroups: AnswerGroup[], + defaultOutcome: Outcome | null, + callback: (value: AnswerGroup[], value2: Outcome | null) => void ): void { this._saveAnswerGroups(newAnswerGroups); this._saveDefaultOutcome(defaultOutcome); @@ -707,12 +712,13 @@ export class ResponsesService { return this._answerGroupsChangedEventEmitter; } - get onInitializeAnswerGroups(): ( - EventEmitter - ) { + get onInitializeAnswerGroups(): EventEmitter< + string | Interaction | InitializeAnswerGroups + > { return this._initializeAnswerGroupsEventEmitter; } } -angular.module('oppia').factory('ResponsesService', - downgradeInjectable(ResponsesService)); +angular + .module('oppia') + .factory('ResponsesService', downgradeInjectable(ResponsesService)); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/services/solution-validity.service.spec.ts b/core/templates/pages/exploration-editor-page/editor-tab/services/solution-validity.service.spec.ts index c14b6c0e50bc..81c0e5253ee6 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/services/solution-validity.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/services/solution-validity.service.spec.ts @@ -16,16 +16,16 @@ * @fileoverview Unit tests for Solution Validity Service. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TestBed } from '@angular/core/testing'; -import { SolutionValidityService } from 'pages/exploration-editor-page/editor-tab/services/solution-validity.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TestBed} from '@angular/core/testing'; +import {SolutionValidityService} from 'pages/exploration-editor-page/editor-tab/services/solution-validity.service'; describe('Solution Validity Service', () => { let svs: SolutionValidityService; beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] + imports: [HttpClientTestingModule], }); svs = TestBed.inject(SolutionValidityService); }); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/services/solution-validity.service.ts b/core/templates/pages/exploration-editor-page/editor-tab/services/solution-validity.service.ts index bc041b34d9f9..55ccdee4d1a5 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/services/solution-validity.service.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/services/solution-validity.service.ts @@ -16,11 +16,11 @@ * @fileoverview Service for keeping track of solution validity. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class SolutionValidityService { solutionValidities: Record = {}; @@ -57,5 +57,9 @@ export class SolutionValidityService { } } -angular.module('oppia').factory( - 'SolutionValidityService', downgradeInjectable(SolutionValidityService)); +angular + .module('oppia') + .factory( + 'SolutionValidityService', + downgradeInjectable(SolutionValidityService) + ); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/services/solution-verification.service.spec.ts b/core/templates/pages/exploration-editor-page/editor-tab/services/solution-verification.service.spec.ts index eb10680634dd..a7daca62b121 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/services/solution-verification.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/services/solution-verification.service.spec.ts @@ -16,18 +16,18 @@ * @fileoverview Unit tests for Solution Verification Service. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TestBed } from '@angular/core/testing'; -import { StateCustomizationArgsService } from 'components/state-editor/state-editor-properties-services/state-customization-args.service'; -import { StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { StateInteractionIdService } from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; -import { Interaction } from 'domain/exploration/InteractionObjectFactory'; -import { SolutionObjectFactory } from 'domain/exploration/SolutionObjectFactory'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TestBed} from '@angular/core/testing'; +import {StateCustomizationArgsService} from 'components/state-editor/state-editor-properties-services/state-customization-args.service'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {StateInteractionIdService} from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; +import {Interaction} from 'domain/exploration/InteractionObjectFactory'; +import {SolutionObjectFactory} from 'domain/exploration/SolutionObjectFactory'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; import INTERACTION_SPECS from 'interactions/interaction_specs.json'; -import { SolutionVerificationService } from 'pages/exploration-editor-page/editor-tab/services/solution-verification.service'; -import { ExplorationDataService } from 'pages/exploration-editor-page/services/exploration-data.service'; -import { importAllAngularServices } from 'tests/unit-test-utils.ajs'; +import {SolutionVerificationService} from 'pages/exploration-editor-page/editor-tab/services/solution-verification.service'; +import {ExplorationDataService} from 'pages/exploration-editor-page/services/exploration-data.service'; +import {importAllAngularServices} from 'tests/unit-test-utils.ajs'; require('pages/exploration-editor-page/services/exploration-states.service.ts'); @@ -36,25 +36,27 @@ describe('Solution Verification Service', () => { let mockInteractionState; importAllAngularServices(); - beforeEach(angular.mock.module('oppia', function($provide) { - $provide.value('NgbModal', { - open: () => { - return { - result: Promise.resolve() - }; - } - }); - })); + beforeEach( + angular.mock.module('oppia', function ($provide) { + $provide.value('NgbModal', { + open: () => { + return { + result: Promise.resolve(), + }; + }, + }); + }) + ); beforeEach(() => { mockInteractionState = { TextInput: { display_mode: 'inline', - is_terminal: false + is_terminal: false, }, TerminalInteraction: { display_mode: 'inline', - is_terminal: true - } + is_terminal: true, + }, }; TestBed.configureTestingModule({ @@ -62,7 +64,7 @@ describe('Solution Verification Service', () => { providers: [ { provide: INTERACTION_SPECS, - useValue: mockInteractionState + useValue: mockInteractionState, }, { provide: ExplorationDataService, @@ -70,10 +72,10 @@ describe('Solution Verification Service', () => { explorationId: 0, autosaveChangeListAsync() { return; - } - } - } - ] + }, + }, + }, + ], }); siis = TestBed.get(StateInteractionIdService); @@ -83,191 +85,234 @@ describe('Solution Verification Service', () => { svs = TestBed.get(SolutionVerificationService); }); - // TODO(#11149): Replace $injector.get(...) to TestBed.get in following // block when ExplorationStateService has been migrated to Angular 8. - beforeEach(angular.mock.inject(function($injector) { - ess = $injector.get('ExplorationStatesService'); - ess.init({ - 'First State': { - content: { - content_id: 'content', - html: 'First State Content' - }, - recorded_voiceovers: { - voiceovers_mapping: { - content: {}, - default_outcome: {}, - feedback_1: {}, - hint_1: {}, - hint_2: {} - } - }, - interaction: { - id: 'TextInput', - answer_groups: [{ - outcome: { - dest: 'End State', + beforeEach( + angular.mock.inject(function ($injector) { + ess = $injector.get('ExplorationStatesService'); + ess.init({ + 'First State': { + content: { + content_id: 'content', + html: 'First State Content', + }, + recorded_voiceovers: { + voiceovers_mapping: { + content: {}, + default_outcome: {}, + feedback_1: {}, + hint_1: {}, + hint_2: {}, + }, + }, + interaction: { + id: 'TextInput', + answer_groups: [ + { + outcome: { + dest: 'End State', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: '', + }, + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, + }, + rule_specs: [ + { + rule_type: 'Contains', + inputs: { + x: { + contentId: 'rule_input', + normalizedStrSet: ['abc'], + }, + }, + }, + ], + }, + ], + customization_args: { + placeholder: { + value: { + content_id: 'ca_placeholder_0', + unicode_str: '', + }, + }, + rows: {value: 1}, + catchMisspellings: { + value: false, + }, + }, + default_outcome: { + dest: 'First State', dest_if_really_stuck: null, feedback: { - content_id: 'feedback_1', - html: '' + content_id: 'default_outcome', + html: '', }, labelled_as_correct: false, param_changes: [], - refresher_exploration_id: null + refresher_exploration_id: null, }, - rule_specs: [{ - rule_type: 'Contains', - inputs: {x: { - contentId: 'rule_input', - normalizedStrSet: ['abc'] - }} - }], - }], - customization_args: { - placeholder: { - value: { - content_id: 'ca_placeholder_0', - unicode_str: '' - } - }, - rows: { value: 1 }, - catchMisspellings: { - value: false - } + hints: [ + { + hint_content: { + content_id: 'hint_1', + html: 'one', + }, + }, + { + hint_content: { + content_id: 'hint_2', + html: 'two', + }, + }, + ], + }, + param_changes: [], + solicit_answer_details: false, + }, + 'End State': { + content: { + content_id: 'content', + html: '', }, - default_outcome: { - dest: 'First State', - dest_if_really_stuck: null, - feedback: { - content_id: 'default_outcome', - html: '' + recorded_voiceovers: { + voiceovers_mapping: { + content: {}, + default_outcome: {}, + feedback_1: {}, }, - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null }, - hints: [{ - hint_content: { - content_id: 'hint_1', - html: 'one' - } - }, { - hint_content: { - content_id: 'hint_2', - html: 'two' - } - }] - }, - param_changes: [], - solicit_answer_details: false - }, - 'End State': { - content: { - content_id: 'content', - html: '' - }, - recorded_voiceovers: { - voiceovers_mapping: { - content: {}, - default_outcome: {}, - feedback_1: {} - } - }, - interaction: { - id: 'TextInput', - answer_groups: [{ - rule_specs: [], - outcome: { + interaction: { + id: 'TextInput', + answer_groups: [ + { + rule_specs: [], + outcome: { + dest: 'default', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: '', + }, + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, + }, + }, + ], + customization_args: { + placeholder: { + value: { + content_id: 'ca_placeholder_0', + unicode_str: '', + }, + }, + rows: {value: 1}, + catchMisspellings: { + value: false, + }, + }, + default_outcome: { dest: 'default', dest_if_really_stuck: null, feedback: { - content_id: 'feedback_1', - html: '' + content_id: 'default_outcome', + html: '', }, - labelled_as_correct: false, param_changes: [], - refresher_exploration_id: null - } - }], - customization_args: { - placeholder: { - value: { - content_id: 'ca_placeholder_0', - unicode_str: '' - } - }, - rows: { value: 1 }, - catchMisspellings: { - value: false - } - }, - default_outcome: { - dest: 'default', - dest_if_really_stuck: null, - feedback: { - content_id: 'default_outcome', - html: '' }, - param_changes: [] + hints: [], }, - hints: [] + param_changes: [], + solicit_answer_details: false, }, - param_changes: [], - solicit_answer_details: false - } - }); - })); + }); + }) + ); it('should verify a correct solution', () => { var state = ess.getState('First State'); siis.init( - 'First State', state.interaction.id, state.interaction, 'widget_id'); + 'First State', + state.interaction.id, + state.interaction, + 'widget_id' + ); scas.init( - 'First State', state.interaction.customizationArgs, - state.interaction, 'widget_customization_args'); + 'First State', + state.interaction.customizationArgs, + state.interaction, + 'widget_customization_args' + ); siis.savedMemento = 'TextInput'; ess.saveSolution('First State', sof.createNew(false, 'abc', 'nothing')); - expect(svs.verifySolution( - 'First State', state.interaction, - ess.getState('First State').interaction.solution.correctAnswer) + expect( + svs.verifySolution( + 'First State', + state.interaction, + ess.getState('First State').interaction.solution.correctAnswer + ) ).toBe(true); see.setInQuestionMode(true); state.interaction.answerGroups[0].outcome.dest = 'First State'; state.interaction.answerGroups[0].outcome.labelledAsCorrect = true; - expect(svs.verifySolution( - 'First State', state.interaction, - ess.getState('First State').interaction.solution.correctAnswer) + expect( + svs.verifySolution( + 'First State', + state.interaction, + ess.getState('First State').interaction.solution.correctAnswer + ) ).toBe(true); }); it('should verify an incorrect solution', () => { var state = ess.getState('First State'); siis.init( - 'First State', state.interaction.id, state.interaction, 'widget_id'); + 'First State', + state.interaction.id, + state.interaction, + 'widget_id' + ); scas.init( - 'First State', state.interaction.customizationArgs, - state.interaction, 'widget_customization_args'); + 'First State', + state.interaction.customizationArgs, + state.interaction, + 'widget_customization_args' + ); siis.savedMemento = 'TextInput'; ess.saveSolution('First State', sof.createNew(false, 'xyz', 'nothing')); - expect(svs.verifySolution( - 'First State', state.interaction, - ess.getState('First State').interaction.solution.correctAnswer) + expect( + svs.verifySolution( + 'First State', + state.interaction, + ess.getState('First State').interaction.solution.correctAnswer + ) ).toBe(false); }); - it('should throw an error if Interaction\'s id is null', () => { - const interaction = new Interaction([], [], { - choices: { - value: [new SubtitledHtml('This is a choice', '')] - } - }, null, [], null, null); + it("should throw an error if Interaction's id is null", () => { + const interaction = new Interaction( + [], + [], + { + choices: { + value: [new SubtitledHtml('This is a choice', '')], + }, + }, + null, + [], + null, + null + ); expect(() => { svs.verifySolution('State 1', interaction, 'Answer'); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/services/solution-verification.service.ts b/core/templates/pages/exploration-editor-page/editor-tab/services/solution-verification.service.ts index aa05dae434a0..483d44154a33 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/services/solution-verification.service.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/services/solution-verification.service.ts @@ -16,40 +16,46 @@ * @fileoverview Service for solution verification. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; -import { StateEditorService } from +import { + StateEditorService, // eslint-disable-next-line max-len - 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { Interaction } from 'domain/exploration/InteractionObjectFactory'; -import { InteractionAnswer } from 'interactions/answer-defs'; -import { AnswerClassificationService } from - 'pages/exploration-player-page/services/answer-classification.service'; -import { InteractionRulesRegistryService } from - 'services/interaction-rules-registry.service'; +} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {Interaction} from 'domain/exploration/InteractionObjectFactory'; +import {InteractionAnswer} from 'interactions/answer-defs'; +import {AnswerClassificationService} from 'pages/exploration-player-page/services/answer-classification.service'; +import {InteractionRulesRegistryService} from 'services/interaction-rules-registry.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class SolutionVerificationService { constructor( - private interactionRulesRegistryService: InteractionRulesRegistryService, - private answerClassificationService: AnswerClassificationService, - private stateEditorService: StateEditorService) {} + private interactionRulesRegistryService: InteractionRulesRegistryService, + private answerClassificationService: AnswerClassificationService, + private stateEditorService: StateEditorService + ) {} verifySolution( - stateName: string, - interaction: Interaction, - correctAnswer: InteractionAnswer): boolean { + stateName: string, + interaction: Interaction, + correctAnswer: InteractionAnswer + ): boolean { if (interaction.id === null) { throw new Error('Interaction ID must not be null'); } - let rulesService = this.interactionRulesRegistryService. - getRulesServiceByInteractionId(interaction.id); + let rulesService = + this.interactionRulesRegistryService.getRulesServiceByInteractionId( + interaction.id + ); let result = this.answerClassificationService.getMatchingClassificationResult( - stateName, interaction, correctAnswer, rulesService + stateName, + interaction, + correctAnswer, + rulesService ); if (this.stateEditorService.isInQuestionMode()) { return result.outcome.labelledAsCorrect; @@ -57,5 +63,9 @@ export class SolutionVerificationService { return stateName !== result.outcome.dest; } } -angular.module('oppia').factory('SolutionVerificationService', - downgradeInjectable(SolutionVerificationService)); +angular + .module('oppia') + .factory( + 'SolutionVerificationService', + downgradeInjectable(SolutionVerificationService) + ); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/state-name-editor/state-name-editor.component.spec.ts b/core/templates/pages/exploration-editor-page/editor-tab/state-name-editor/state-name-editor.component.spec.ts index 498f3dc32af6..489fb18fdd88 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/state-name-editor/state-name-editor.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/state-name-editor/state-name-editor.component.spec.ts @@ -16,19 +16,25 @@ * @fileoverview Unit tests for the component of the 'State Editor'. */ -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { TestBed, fakeAsync, tick, ComponentFixture, flush } from '@angular/core/testing'; -import { EditabilityService } from 'services/editability.service'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { StateNameService } from 'components/state-editor/state-editor-properties-services/state-name.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ExplorationDataService } from 'pages/exploration-editor-page/services/exploration-data.service'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; -import { RouterService } from 'pages/exploration-editor-page/services/router.service'; -import { ExternalSaveService } from 'services/external-save.service'; -import { StateNameEditorComponent } from './state-name-editor.component'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + TestBed, + fakeAsync, + tick, + ComponentFixture, + flush, +} from '@angular/core/testing'; +import {EditabilityService} from 'services/editability.service'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {StateNameService} from 'components/state-editor/state-editor-properties-services/state-name.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ExplorationDataService} from 'pages/exploration-editor-page/services/exploration-data.service'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; +import {RouterService} from 'pages/exploration-editor-page/services/router.service'; +import {ExternalSaveService} from 'services/external-save.service'; +import {StateNameEditorComponent} from './state-name-editor.component'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; class MockExplorationDataService { explorationId!: 0; @@ -57,7 +63,7 @@ describe('State Name Editor component', () => { class MockNgbModal { open() { return { - result: Promise.resolve() + result: Promise.resolve(), }; } } @@ -70,14 +76,8 @@ describe('State Name Editor component', () => { explorationDataService = new MockExplorationDataService(); TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - FormsModule, - ReactiveFormsModule - ], - declarations: [ - StateNameEditorComponent - ], + imports: [HttpClientTestingModule, FormsModule, ReactiveFormsModule], + declarations: [StateNameEditorComponent], providers: [ EditabilityService, ExplorationStatesService, @@ -86,22 +86,21 @@ describe('State Name Editor component', () => { StateNameService, { provide: ExplorationDataService, - useValue: explorationDataService + useValue: explorationDataService, }, { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, { provide: ExternalSaveService, - useClass: MockExternalSaveService - } + useClass: MockExternalSaveService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); - beforeEach(() => { fixture = TestBed.createComponent(StateNameEditorComponent); component = fixture.componentInstance; @@ -113,128 +112,135 @@ describe('State Name Editor component', () => { spyOn(stateNameService, 'isStateNameEditorShown').and.returnValue(true); autosaveChangeListSpy = spyOn( - explorationDataService, 'autosaveChangeListAsync'); + explorationDataService, + 'autosaveChangeListAsync' + ); - explorationStatesService.init({ - 'First State': { - classifier_model_id: null, - card_is_checkpoint: false, - linked_skill_id: null, - content: { - content_id: 'content', - html: 'First State Content' - }, - recorded_voiceovers: { - voiceovers_mapping: { - content: {}, - default_outcome: {} - } - }, - interaction: { - confirmed_unclassified_answers: [], - customization_args: {}, - solution: null, - id: null, - answer_groups: [], - default_outcome: { - dest: 'Second State', - dest_if_really_stuck: null, - feedback: { - content_id: 'default_outcome', - html: '' + explorationStatesService.init( + { + 'First State': { + classifier_model_id: null, + card_is_checkpoint: false, + linked_skill_id: null, + content: { + content_id: 'content', + html: 'First State Content', + }, + recorded_voiceovers: { + voiceovers_mapping: { + content: {}, + default_outcome: {}, }, - labelled_as_correct: true, - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - param_changes: [] }, - hints: [] - }, - param_changes: [], - solicit_answer_details: false - }, - 'Second State': { - classifier_model_id: null, - card_is_checkpoint: false, - linked_skill_id: null, - content: { - content_id: 'content', - html: 'Second State Content' - }, - recorded_voiceovers: { - voiceovers_mapping: { - content: {}, - default_outcome: {} - } - }, - interaction: { - confirmed_unclassified_answers: [], - customization_args: {}, - solution: null, - id: null, - answer_groups: [], - default_outcome: { - dest: 'Second State', - dest_if_really_stuck: null, - feedback: { - content_id: 'default_outcome', - html: '' + interaction: { + confirmed_unclassified_answers: [], + customization_args: {}, + solution: null, + id: null, + answer_groups: [], + default_outcome: { + dest: 'Second State', + dest_if_really_stuck: null, + feedback: { + content_id: 'default_outcome', + html: '', + }, + labelled_as_correct: true, + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + param_changes: [], }, - labelled_as_correct: true, - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - param_changes: [] + hints: [], }, - hints: [] + param_changes: [], + solicit_answer_details: false, }, - param_changes: [], - solicit_answer_details: false, - }, - 'Third State': { - classifier_model_id: null, - card_is_checkpoint: false, - linked_skill_id: null, - content: { - content_id: 'content', - html: 'This is some content.' - }, - recorded_voiceovers: { - voiceovers_mapping: { - content: {}, - default_outcome: {} - } + 'Second State': { + classifier_model_id: null, + card_is_checkpoint: false, + linked_skill_id: null, + content: { + content_id: 'content', + html: 'Second State Content', + }, + recorded_voiceovers: { + voiceovers_mapping: { + content: {}, + default_outcome: {}, + }, + }, + interaction: { + confirmed_unclassified_answers: [], + customization_args: {}, + solution: null, + id: null, + answer_groups: [], + default_outcome: { + dest: 'Second State', + dest_if_really_stuck: null, + feedback: { + content_id: 'default_outcome', + html: '', + }, + labelled_as_correct: true, + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + param_changes: [], + }, + hints: [], + }, + param_changes: [], + solicit_answer_details: false, }, - interaction: { - confirmed_unclassified_answers: [], - customization_args: {}, - solution: null, - id: null, - answer_groups: [], - default_outcome: { - dest: 'Second State', - dest_if_really_stuck: null, - feedback: { - content_id: 'default_outcome', - html: '' + 'Third State': { + classifier_model_id: null, + card_is_checkpoint: false, + linked_skill_id: null, + content: { + content_id: 'content', + html: 'This is some content.', + }, + recorded_voiceovers: { + voiceovers_mapping: { + content: {}, + default_outcome: {}, + }, + }, + interaction: { + confirmed_unclassified_answers: [], + customization_args: {}, + solution: null, + id: null, + answer_groups: [], + default_outcome: { + dest: 'Second State', + dest_if_really_stuck: null, + feedback: { + content_id: 'default_outcome', + html: '', + }, + labelled_as_correct: true, + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + param_changes: [], }, - labelled_as_correct: true, - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - param_changes: [] + hints: [], }, - hints: [] + param_changes: [ + { + name: 'comparison', + generator_id: 'Copier', + customization_args: { + value: 'something clever', + parse_with_jinja: false, + }, + }, + ], + solicit_answer_details: false, }, - param_changes: [{ - name: 'comparison', - generator_id: 'Copier', - customization_args: { - value: 'something clever', - parse_with_jinja: false - } - }], - solicit_answer_details: false, - } - }, false); + }, + false + ); component.ngOnInit(); }); @@ -246,51 +252,57 @@ describe('State Name Editor component', () => { fixture.destroy(); }); - it('should not save state name when it is longer than 50 characters', - () => { - expect(component.saveStateName( - 'babababababababababababababababababababababababababab')).toBe(false); - }); - - it('should not save state names when it contains invalid characteres', - () => { - stateEditorService.setActiveStateName('Third State'); - component.initStateNameEditor(); - expect(component.saveStateName('#')).toBe(false); - }); - - it('should not save duplicate state names when trying to save a state' + - ' that already exists', () => { - expect(component.saveStateName('Second State')).toBe(false); + it('should not save state name when it is longer than 50 characters', () => { + expect( + component.saveStateName( + 'babababababababababababababababababababababababababab' + ) + ).toBe(false); }); - it('should save state name and refresh to main tab when submitting' + - ' state name form', () => { - spyOn(routerService, 'navigateToMainTab'); - stateEditorService.setActiveStateName('First State'); + it('should not save state names when it contains invalid characteres', () => { + stateEditorService.setActiveStateName('Third State'); component.initStateNameEditor(); - - component.saveStateNameAndRefresh('Fourth State'); - expect(routerService.navigateToMainTab).toHaveBeenCalled(); + expect(component.saveStateName('#')).toBe(false); }); - it('should save state names independently when editting more than one state', - fakeAsync(() => { - stateEditorService.setActiveStateName('Third State'); - component.saveStateName('Fourth State'); - tick(200); - expect(explorationStatesService.getState('Fourth State')).toBeTruthy(); - expect(explorationStatesService.getState('Third State')).toBeFalsy(); + it( + 'should not save duplicate state names when trying to save a state' + + ' that already exists', + () => { + expect(component.saveStateName('Second State')).toBe(false); + } + ); + it( + 'should save state name and refresh to main tab when submitting' + + ' state name form', + () => { + spyOn(routerService, 'navigateToMainTab'); stateEditorService.setActiveStateName('First State'); - component.saveStateName('Fifth State'); - tick(200); - expect(explorationStatesService.getState('Fifth State')).toBeTruthy(); - expect(explorationStatesService.getState('First State')).toBeFalsy(); - expect(autosaveChangeListSpy).toHaveBeenCalled(); + component.initStateNameEditor(); + + component.saveStateNameAndRefresh('Fourth State'); + expect(routerService.navigateToMainTab).toHaveBeenCalled(); + } + ); - flush(); - })); + it('should save state names independently when editting more than one state', fakeAsync(() => { + stateEditorService.setActiveStateName('Third State'); + component.saveStateName('Fourth State'); + tick(200); + expect(explorationStatesService.getState('Fourth State')).toBeTruthy(); + expect(explorationStatesService.getState('Third State')).toBeFalsy(); + + stateEditorService.setActiveStateName('First State'); + component.saveStateName('Fifth State'); + tick(200); + expect(explorationStatesService.getState('Fifth State')).toBeTruthy(); + expect(explorationStatesService.getState('First State')).toBeFalsy(); + expect(autosaveChangeListSpy).toHaveBeenCalled(); + + flush(); + })); it('should not re-save state names when it did not changed', () => { stateEditorService.setActiveStateName('Second State'); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/state-name-editor/state-name-editor.component.ts b/core/templates/pages/exploration-editor-page/editor-tab/state-name-editor/state-name-editor.component.ts index d2919acacaa7..64260e008e52 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/state-name-editor/state-name-editor.component.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/state-name-editor/state-name-editor.component.ts @@ -17,25 +17,24 @@ * editor. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { Subscription } from 'rxjs'; -import { AppConstants } from 'app.constants'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { StateNameService } from 'components/state-editor/state-editor-properties-services/state-name.service'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; -import { ExternalSaveService } from 'services/external-save.service'; -import { RouterService } from 'pages/exploration-editor-page/services/router.service'; -import { NormalizeWhitespacePipe } from 'filters/string-utility-filters/normalize-whitespace.pipe'; -import { EditabilityService } from 'services/editability.service'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {Subscription} from 'rxjs'; +import {AppConstants} from 'app.constants'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {StateNameService} from 'components/state-editor/state-editor-properties-services/state-name.service'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; +import {ExternalSaveService} from 'services/external-save.service'; +import {RouterService} from 'pages/exploration-editor-page/services/router.service'; +import {NormalizeWhitespacePipe} from 'filters/string-utility-filters/normalize-whitespace.pipe'; +import {EditabilityService} from 'services/editability.service'; @Component({ selector: 'oppia-state-name-editor', - templateUrl: './state-name-editor.component.html' + templateUrl: './state-name-editor.component.html', }) -export class StateNameEditorComponent - implements OnInit, OnDestroy { +export class StateNameEditorComponent implements OnInit, OnDestroy { directiveSubscriptions = new Subscription(); // These properties below are initialized using Angular lifecycle hooks // where we need to do non-null assertion. For more information see @@ -52,7 +51,7 @@ export class StateNameEditorComponent private normalizeWhitespacePipe: NormalizeWhitespacePipe, private routerService: RouterService, private stateEditorService: StateEditorService, - private stateNameService: StateNameService, + private stateNameService: StateNameService ) {} openStateNameEditor(): void { @@ -68,8 +67,7 @@ export class StateNameEditorComponent } saveStateName(newStateName: string): boolean { - let normalizedNewName = - this._getNormalizedStateName(newStateName); + let normalizedNewName = this._getNormalizedStateName(newStateName); let savedMemento = this.stateNameService.getStateNameSavedMemento(); if (!this._isNewStateNameValid(normalizedNewName)) { return false; @@ -80,8 +78,7 @@ export class StateNameEditorComponent } else { let stateName = this.stateEditorService.getActiveStateName(); if (stateName) { - this.explorationStatesService.renameState( - stateName, normalizedNewName); + this.explorationStatesService.renameState(stateName, normalizedNewName); } this.stateNameService.setStateNameEditorVisibility(false); // Save the contents of other open fields. @@ -92,8 +89,7 @@ export class StateNameEditorComponent } saveStateNameAndRefresh(newStateName: string): void { - const normalizedStateName = ( - this._getNormalizedStateName(newStateName)); + const normalizedStateName = this._getNormalizedStateName(newStateName); const valid = this.saveStateName(normalizedStateName); if (valid) { @@ -106,8 +102,7 @@ export class StateNameEditorComponent return true; } - return this.explorationStatesService.isNewStateNameValid( - stateName, true); + return this.explorationStatesService.isNewStateNameValid(stateName, true); } initStateNameEditor(): void { @@ -120,13 +115,11 @@ export class StateNameEditorComponent ngOnInit(): void { this.directiveSubscriptions.add( - this.externalSaveService.onExternalSave.subscribe( - () => { - if (this.stateNameService.isStateNameEditorShown()) { - this.saveStateName(this.tmpStateName); - } + this.externalSaveService.onExternalSave.subscribe(() => { + if (this.stateNameService.isStateNameEditorShown()) { + this.saveStateName(this.tmpStateName); } - ) + }) ); this.stateNameService.init(); @@ -138,7 +131,9 @@ export class StateNameEditorComponent } } -angular.module('oppia').directive('oppiaStateNameEditor', +angular.module('oppia').directive( + 'oppiaStateNameEditor', downgradeComponent({ - component: StateNameEditorComponent - }) as angular.IDirectiveFactory); + component: StateNameEditorComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/state-param-changes-editor/state-param-changes-editor.component.spec.ts b/core/templates/pages/exploration-editor-page/editor-tab/state-param-changes-editor/state-param-changes-editor.component.spec.ts index 63a7ca8c6aa5..1dcc954f8274 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/state-param-changes-editor/state-param-changes-editor.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/state-param-changes-editor/state-param-changes-editor.component.spec.ts @@ -16,15 +16,16 @@ * @fileoverview Unit tests for stateParamChangesEditor component. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, async, TestBed } from '@angular/core/testing'; -import { StateParamChangesService } from +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, async, TestBed} from '@angular/core/testing'; +import { + StateParamChangesService, // eslint-disable-next-line max-len - 'components/state-editor/state-editor-properties-services/state-param-changes.service'; -import { StateParamChangesEditorComponent } from +} from 'components/state-editor/state-editor-properties-services/state-param-changes.service'; +import { + StateParamChangesEditorComponent, // eslint-disable-next-line max-len - 'pages/exploration-editor-page/editor-tab/state-param-changes-editor/state-param-changes-editor.component'; - +} from 'pages/exploration-editor-page/editor-tab/state-param-changes-editor/state-param-changes-editor.component'; let component: StateParamChangesEditorComponent; let fixture: ComponentFixture; @@ -34,10 +35,9 @@ describe('State Param Changes Editor directive', () => { TestBed.configureTestingModule({ declarations: [StateParamChangesEditorComponent], providers: [], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - stateParamChangesService = - TestBed.get(StateParamChangesService); + stateParamChangesService = TestBed.get(StateParamChangesService); })); beforeEach(() => { diff --git a/core/templates/pages/exploration-editor-page/editor-tab/state-param-changes-editor/state-param-changes-editor.component.ts b/core/templates/pages/exploration-editor-page/editor-tab/state-param-changes-editor/state-param-changes-editor.component.ts index dd3d3e0fc85b..78af1488c699 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/state-param-changes-editor/state-param-changes-editor.component.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/state-param-changes-editor/state-param-changes-editor.component.ts @@ -17,16 +17,17 @@ * state editor. */ -import { Component, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { StateParamChangesService } from +import {Component, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import { + StateParamChangesService, // eslint-disable-next-line max-len - 'components/state-editor/state-editor-properties-services/state-param-changes.service'; +} from 'components/state-editor/state-editor-properties-services/state-param-changes.service'; @Component({ selector: 'state-param-changes-editor', templateUrl: './state-param-changes-editor.component.html', - styleUrls: [] + styleUrls: [], }) export class StateParamChangesEditorComponent implements OnInit { // This property below is initialized using Angular lifecycle hooks @@ -39,6 +40,9 @@ export class StateParamChangesEditorComponent implements OnInit { this.spcs = this.stateParamChangesService; } } -angular.module('oppia').directive( - 'stateParamChangesEditor', downgradeComponent( - {component: StateParamChangesEditorComponent})); +angular + .module('oppia') + .directive( + 'stateParamChangesEditor', + downgradeComponent({component: StateParamChangesEditorComponent}) + ); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/state-version-history/state-version-history.component.spec.ts b/core/templates/pages/exploration-editor-page/editor-tab/state-version-history/state-version-history.component.spec.ts index 22bd81016f3f..4421b8cf1132 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/state-version-history/state-version-history.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/state-version-history/state-version-history.component.spec.ts @@ -16,14 +16,20 @@ * @fileoverview Unit tests for state version history component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { StateBackendDict, StateObjectFactory } from 'domain/state/StateObjectFactory'; -import { VersionHistoryBackendApiService } from 'pages/exploration-editor-page/services/version-history-backend-api.service'; -import { StateDiffData, VersionHistoryService } from 'pages/exploration-editor-page/services/version-history.service'; -import { StateVersionHistoryComponent } from './state-version-history.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import { + StateBackendDict, + StateObjectFactory, +} from 'domain/state/StateObjectFactory'; +import {VersionHistoryBackendApiService} from 'pages/exploration-editor-page/services/version-history-backend-api.service'; +import { + StateDiffData, + VersionHistoryService, +} from 'pages/exploration-editor-page/services/version-history.service'; +import {StateVersionHistoryComponent} from './state-version-history.component'; describe('State version history component', () => { let component: StateVersionHistoryComponent; @@ -37,7 +43,7 @@ describe('State version history component', () => { class MockNgbModal { open() { return { - result: Promise.resolve() + result: Promise.resolve(), }; } } @@ -51,10 +57,10 @@ describe('State version history component', () => { VersionHistoryService, { provide: NgbModal, - useClass: MockNgbModal - } + useClass: MockNgbModal, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -63,7 +69,8 @@ describe('State version history component', () => { component = fixture.componentInstance; stateObjectFactory = TestBed.inject(StateObjectFactory); versionHistoryBackendApiService = TestBed.inject( - VersionHistoryBackendApiService); + VersionHistoryBackendApiService + ); versionHistoryService = TestBed.inject(VersionHistoryService); ngbModal = TestBed.inject(NgbModal); @@ -71,171 +78,185 @@ describe('State version history component', () => { classifier_model_id: null, content: { content_id: 'content', - html: '' + html: '', }, recorded_voiceovers: { voiceovers_mapping: { content: {}, - default_outcome: {} - } + default_outcome: {}, + }, }, interaction: { answer_groups: [], confirmed_unclassified_answers: [], customization_args: { rows: { - value: 1 + value: 1, }, placeholder: { value: { unicode_str: 'Type your answer here.', - content_id: '' - } - } + content_id: '', + }, + }, }, default_outcome: { dest: '(untitled state)', dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: '' + html: '', }, param_changes: [], labelled_as_correct: false, refresher_exploration_id: null, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, hints: [], solution: null, - id: 'TextInput' + id: 'TextInput', }, linked_skill_id: null, param_changes: [], solicit_answer_details: false, - card_is_checkpoint: false + card_is_checkpoint: false, }; let stateData = stateObjectFactory.createFromBackendDict( - 'State', stateObject); + 'State', + stateObject + ); spyOn( - versionHistoryBackendApiService, 'fetchStateVersionHistoryAsync' + versionHistoryBackendApiService, + 'fetchStateVersionHistoryAsync' ).and.resolveTo({ lastEditedVersionNumber: 2, stateNameInPreviousVersion: 'State', stateInPreviousVersion: stateData, - lastEditedCommitterUsername: 'some' + lastEditedCommitterUsername: 'some', }); }); it('should get the last edited version number for the active state', () => { spyOn(versionHistoryService, 'getBackwardStateDiffData').and.returnValue({ - oldVersionNumber: 3 + oldVersionNumber: 3, } as StateDiffData); expect(component.getLastEditedVersionNumber()).toEqual(3); }); - it('should get the last edited committer username for the active state', - () => { - spyOn(versionHistoryService, 'getBackwardStateDiffData').and.returnValue({ - committerUsername: 'some' - } as StateDiffData); + it('should get the last edited committer username for the active state', () => { + spyOn(versionHistoryService, 'getBackwardStateDiffData').and.returnValue({ + committerUsername: 'some', + } as StateDiffData); - expect(component.getLastEditedCommitterUsername()).toEqual('some'); - }); + expect(component.getLastEditedCommitterUsername()).toEqual('some'); + }); it('should throw error when last edited version number is null', () => { - spyOn( - versionHistoryService, 'getBackwardStateDiffData' - ).and.returnValue({ - oldVersionNumber: null + spyOn(versionHistoryService, 'getBackwardStateDiffData').and.returnValue({ + oldVersionNumber: null, } as StateDiffData); - expect( - () =>component.getLastEditedVersionNumber() - ).toThrowError('The value of last edited version number cannot be null'); + expect(() => component.getLastEditedVersionNumber()).toThrowError( + 'The value of last edited version number cannot be null' + ); }); it('should get whether version history can be explored', () => { spyOn( - versionHistoryService, 'canShowBackwardStateDiffData' + versionHistoryService, + 'canShowBackwardStateDiffData' ).and.returnValue(true); expect(component.canShowExploreVersionHistoryButton()).toBeTrue(); }); - it('should open the state version history modal on clicking the explore ' + - 'version history button', () => { - class MockComponentInstance { - componentInstance = { - newState: null, - newStateName: 'A', - oldState: null, - oldStateName: 'B', - headers: { - leftPane: '', - rightPane: '', - } - }; - } - spyOn(ngbModal, 'open').and.returnValues({ - componentInstance: MockComponentInstance, - result: Promise.resolve() - } as NgbModalRef, { - componentInstance: MockComponentInstance, - result: Promise.reject() - } as NgbModalRef); - let stateData = stateObjectFactory - .createFromBackendDict('State', stateObject); - spyOn(versionHistoryService, 'getBackwardStateDiffData').and.returnValue({ - oldState: stateData, - newState: stateData, - oldVersionNumber: 3 - } as StateDiffData); + it( + 'should open the state version history modal on clicking the explore ' + + 'version history button', + () => { + class MockComponentInstance { + componentInstance = { + newState: null, + newStateName: 'A', + oldState: null, + oldStateName: 'B', + headers: { + leftPane: '', + rightPane: '', + }, + }; + } + spyOn(ngbModal, 'open').and.returnValues( + { + componentInstance: MockComponentInstance, + result: Promise.resolve(), + } as NgbModalRef, + { + componentInstance: MockComponentInstance, + result: Promise.reject(), + } as NgbModalRef + ); + let stateData = stateObjectFactory.createFromBackendDict( + 'State', + stateObject + ); + spyOn(versionHistoryService, 'getBackwardStateDiffData').and.returnValue({ + oldState: stateData, + newState: stateData, + oldVersionNumber: 3, + } as StateDiffData); - component.onClickExploreVersionHistoryButton(); + component.onClickExploreVersionHistoryButton(); - expect(ngbModal.open).toHaveBeenCalled(); + expect(ngbModal.open).toHaveBeenCalled(); - component.onClickExploreVersionHistoryButton(); - }); + component.onClickExploreVersionHistoryButton(); + } + ); - it('should throw error on exploring version history when state' + - ' names from version history data are not defined', () => { - class MockComponentInstance { - componentInstance = { - newState: null, - newStateName: 'A', - oldState: null, - oldStateName: 'B', - headers: { - leftPane: '', - rightPane: '', - } - }; + it( + 'should throw error on exploring version history when state' + + ' names from version history data are not defined', + () => { + class MockComponentInstance { + componentInstance = { + newState: null, + newStateName: 'A', + oldState: null, + oldStateName: 'B', + headers: { + leftPane: '', + rightPane: '', + }, + }; + } + spyOn(versionHistoryService, 'getBackwardStateDiffData').and.returnValues( + { + oldState: stateObjectFactory.createFromBackendDict(null, stateObject), + newState: stateObjectFactory.createFromBackendDict(null, stateObject), + oldVersionNumber: 3, + } as StateDiffData, + { + oldState: stateObjectFactory.createFromBackendDict(null, stateObject), + newState: stateObjectFactory.createFromBackendDict( + 'State', + stateObject + ), + oldVersionNumber: 3, + } as StateDiffData + ); + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: MockComponentInstance, + result: Promise.resolve(), + } as NgbModalRef); + + expect(() => component.onClickExploreVersionHistoryButton()).toThrowError( + 'State name cannot be null' + ); + expect(() => component.onClickExploreVersionHistoryButton()).toThrowError( + 'State name cannot be null' + ); } - spyOn(versionHistoryService, 'getBackwardStateDiffData').and.returnValues({ - oldState: stateObjectFactory.createFromBackendDict( - null, stateObject), - newState: stateObjectFactory.createFromBackendDict( - null, stateObject), - oldVersionNumber: 3 - } as StateDiffData, { - oldState: stateObjectFactory.createFromBackendDict( - null, stateObject), - newState: stateObjectFactory.createFromBackendDict( - 'State', stateObject), - oldVersionNumber: 3 - } as StateDiffData); - spyOn(ngbModal, 'open').and.returnValue({ - componentInstance: MockComponentInstance, - result: Promise.resolve() - } as NgbModalRef); - - expect( - () => component.onClickExploreVersionHistoryButton() - ).toThrowError('State name cannot be null'); - expect( - () => component.onClickExploreVersionHistoryButton() - ).toThrowError('State name cannot be null'); - }); + ); }); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/state-version-history/state-version-history.component.ts b/core/templates/pages/exploration-editor-page/editor-tab/state-version-history/state-version-history.component.ts index a3858ba43fc2..83245b645b65 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/state-version-history/state-version-history.component.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/state-version-history/state-version-history.component.ts @@ -16,14 +16,17 @@ * @fileoverview Component for the state version history button. */ -import { Component, Input } from '@angular/core'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { StateVersionHistoryModalComponent } from 'pages/exploration-editor-page/modal-templates/state-version-history-modal.component'; -import { StateDiffData, VersionHistoryService } from 'pages/exploration-editor-page/services/version-history.service'; +import {Component, Input} from '@angular/core'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {StateVersionHistoryModalComponent} from 'pages/exploration-editor-page/modal-templates/state-version-history-modal.component'; +import { + StateDiffData, + VersionHistoryService, +} from 'pages/exploration-editor-page/services/version-history.service'; @Component({ selector: 'oppia-state-version-history', - templateUrl: './state-version-history.component.html' + templateUrl: './state-version-history.component.html', }) export class StateVersionHistoryComponent { @Input() validationErrorIsShown!: boolean; @@ -38,9 +41,8 @@ export class StateVersionHistoryComponent { } getLastEditedCommitterUsername(): string { - return ( - this.versionHistoryService.getBackwardStateDiffData().committerUsername - ); + return this.versionHistoryService.getBackwardStateDiffData() + .committerUsername; } getLastEditedVersionNumber(): number { @@ -59,14 +61,16 @@ export class StateVersionHistoryComponent { onClickExploreVersionHistoryButton(): void { const modalRef: NgbModalRef = this.ngbModal.open( - StateVersionHistoryModalComponent, { + StateVersionHistoryModalComponent, + { backdrop: true, windowClass: 'metadata-diff-modal', - size: 'xl' - }); + size: 'xl', + } + ); - const stateDiffData: StateDiffData = ( - this.versionHistoryService.getBackwardStateDiffData()); + const stateDiffData: StateDiffData = + this.versionHistoryService.getBackwardStateDiffData(); // Explanation for why diffData.newState can be null: // It is explained in VersionHistoryService as to why the values of @@ -97,16 +101,21 @@ export class StateVersionHistoryComponent { } modalRef.componentInstance.oldStateName = stateDiffData.oldState.name; } - modalRef.componentInstance.committerUsername = ( - stateDiffData.committerUsername); + modalRef.componentInstance.committerUsername = + stateDiffData.committerUsername; modalRef.componentInstance.oldVersion = stateDiffData.oldVersionNumber; - modalRef.result.then(() => { - this.versionHistoryService - .setCurrentPositionInStateVersionHistoryList(0); - }, () => { - this.versionHistoryService - .setCurrentPositionInStateVersionHistoryList(0); - }); + modalRef.result.then( + () => { + this.versionHistoryService.setCurrentPositionInStateVersionHistoryList( + 0 + ); + }, + () => { + this.versionHistoryService.setCurrentPositionInStateVersionHistoryList( + 0 + ); + } + ); } } diff --git a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/add-answer-group-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/add-answer-group-modal.component.spec.ts index 43136dcc311d..aecf218f2c1f 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/add-answer-group-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/add-answer-group-modal.component.spec.ts @@ -16,18 +16,27 @@ * @fileoverview Unit tests for AddAnswerGroupModalController. */ -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { EditorFirstTimeEventsService } from 'pages/exploration-editor-page/services/editor-first-time-events.service'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { GenerateContentIdService } from 'services/generate-content-id.service'; -import { Outcome, OutcomeObjectFactory } from 'domain/exploration/OutcomeObjectFactory'; -import { Subscription } from 'rxjs'; -import { EventBusGroup, EventBusService } from 'app-events/event-bus.service'; -import { ObjectFormValidityChangeEvent } from 'app-events/app-events'; -import { AddAnswerGroupModalComponent } from './add-answer-group-modal.component'; -import { NO_ERRORS_SCHEMA, ElementRef } from '@angular/core'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {EditorFirstTimeEventsService} from 'pages/exploration-editor-page/services/editor-first-time-events.service'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {GenerateContentIdService} from 'services/generate-content-id.service'; +import { + Outcome, + OutcomeObjectFactory, +} from 'domain/exploration/OutcomeObjectFactory'; +import {Subscription} from 'rxjs'; +import {EventBusGroup, EventBusService} from 'app-events/event-bus.service'; +import {ObjectFormValidityChangeEvent} from 'app-events/app-events'; +import {AddAnswerGroupModalComponent} from './add-answer-group-modal.component'; +import {NO_ERRORS_SCHEMA, ElementRef} from '@angular/core'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; class MockActiveModal { close(): void { @@ -51,9 +60,7 @@ describe('Add Answer Group Modal Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - AddAnswerGroupModalComponent - ], + declarations: [AddAnswerGroupModalComponent], providers: [ EditorFirstTimeEventsService, GenerateContentIdService, @@ -61,10 +68,10 @@ describe('Add Answer Group Modal Component', () => { StateEditorService, { provide: NgbActiveModal, - useClass: MockActiveModal - } + useClass: MockActiveModal, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -75,11 +82,17 @@ describe('Add Answer Group Modal Component', () => { outcomeObjectFactory = TestBed.inject(OutcomeObjectFactory); stateEditorService = TestBed.inject(StateEditorService); generateContentIdService = TestBed.inject(GenerateContentIdService); - generateContentIdService.init(() => 0, () => {}); + generateContentIdService.init( + () => 0, + () => {} + ); spyOn(stateEditorService, 'isInQuestionMode').and.returnValue(true); testSubscriptions = new Subscription(); - testSubscriptions.add(stateEditorService.onSaveOutcomeDestDetails.subscribe( - saveOutcomeDestDetailsSpy)); + testSubscriptions.add( + stateEditorService.onSaveOutcomeDestDetails.subscribe( + saveOutcomeDestDetailsSpy + ) + ); fixture.detectChanges(); }); @@ -88,42 +101,45 @@ describe('Add Answer Group Modal Component', () => { testSubscriptions.unsubscribe(); }); - it('should initialize component properties after controller is initialized', - fakeAsync(() => { - expect(component.feedbackEditorIsOpen).toBe(false); - expect(component.questionModeEnabled).toBe(true); - expect(component.tmpTaggedSkillMisconceptionId).toBe(null); - expect(component.addAnswerGroupForm).toEqual({}); - expect(component.validation).toBe(false); + it('should initialize component properties after controller is initialized', fakeAsync(() => { + expect(component.feedbackEditorIsOpen).toBe(false); + expect(component.questionModeEnabled).toBe(true); + expect(component.tmpTaggedSkillMisconceptionId).toBe(null); + expect(component.addAnswerGroupForm).toEqual({}); + expect(component.validation).toBe(false); - component.validateChanges({ - isCreatingNewState: true, - value: '' - }); - tick(); + component.validateChanges({ + isCreatingNewState: true, + value: '', + }); + tick(); - expect(component.validation).toBe(true); + expect(component.validation).toBe(true); - component.validateChanges({ - isCreatingNewState: true, - value: 'newState' - }); - tick(); + component.validateChanges({ + isCreatingNewState: true, + value: 'newState', + }); + tick(); - expect(component.validation).toBe(false); - })); + expect(component.validation).toBe(false); + })); it('should update answer group feedback', () => { expect(component.feedbackEditorIsOpen).toBe(false); var feedback = new SubtitledHtml('New feedback', null); component.updateAnswerGroupFeedback({ - feedback: feedback + feedback: feedback, } as Outcome); component.modalId = Symbol(); const eventBusGroup = new EventBusGroup(TestBed.inject(EventBusService)); - eventBusGroup.emit(new ObjectFormValidityChangeEvent({ - value: true, modalId: component.modalId})); + eventBusGroup.emit( + new ObjectFormValidityChangeEvent({ + value: true, + modalId: component.modalId, + }) + ); expect(component.feedbackEditorIsOpen).toBe(true); expect(component.tmpOutcome.feedback).toBe(feedback); }); @@ -133,7 +149,7 @@ describe('Add Answer Group Modal Component', () => { var taggedMisconception = { misconceptionId: 1, - skillId: 'skill_1' + skillId: 'skill_1', }; component.updateTaggedMisconception(taggedMisconception); @@ -147,32 +163,42 @@ describe('Add Answer Group Modal Component', () => { }); it('should check if outcome has no feedback with self loop', fakeAsync(() => { - var outcome = outcomeObjectFactory.createNew( - 'State Name', '1', '', []); + var outcome = outcomeObjectFactory.createNew('State Name', '1', '', []); component.stateName = 'State Name'; tick(); expect(component.isSelfLoopWithNoFeedback(outcome)).toBe(true); var outcome2 = outcomeObjectFactory.createNew( - 'State Name', '1', 'Feedback Text', []); + 'State Name', + '1', + 'Feedback Text', + [] + ); tick(); expect(component.isSelfLoopWithNoFeedback(outcome2)).toBe(false); })); it('should check if outcome feedback exceeds 10000 characters', () => { var outcome1 = outcomeObjectFactory.createNew( - 'State Name', '1', 'a'.repeat(10000), []); + 'State Name', + '1', + 'a'.repeat(10000), + [] + ); expect(component.isFeedbackLengthExceeded(outcome1)).toBe(false); var outcome2 = outcomeObjectFactory.createNew( - 'State Name', '1', 'a'.repeat(10001), []); + 'State Name', + '1', + 'a'.repeat(10001), + [] + ); expect(component.isFeedbackLengthExceeded(outcome2)).toBe(true); }); it('should focus on the header after loading', () => { - const addResponseRef = new ElementRef( - document.createElement('h4')); + const addResponseRef = new ElementRef(document.createElement('h4')); component.addResponseRef = addResponseRef; spyOn(addResponseRef.nativeElement, 'focus'); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/add-answer-group-modal.component.ts b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/add-answer-group-modal.component.ts index f02b7ef80a01..b36bf4f7ef58 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/add-answer-group-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/add-answer-group-modal.component.ts @@ -16,190 +16,214 @@ * @fileoverview Component for add answer group modal. */ -import { EventBusGroup, EventBusService, Newable } from 'app-events/event-bus.service'; -import { ObjectFormValidityChangeEvent } from 'app-events/app-events'; -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, ElementRef, ViewChild } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { ContextService } from 'services/context.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { EditorFirstTimeEventsService } from 'pages/exploration-editor-page/services/editor-first-time-events.service'; -import { PopulateRuleContentIdsService } from 'pages/exploration-editor-page/services/populate-rule-content-ids.service'; +import { + EventBusGroup, + EventBusService, + Newable, +} from 'app-events/event-bus.service'; +import {ObjectFormValidityChangeEvent} from 'app-events/app-events'; +import { + Component, + EventEmitter, + Input, + OnDestroy, + OnInit, + Output, + ElementRef, + ViewChild, +} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {ContextService} from 'services/context.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {EditorFirstTimeEventsService} from 'pages/exploration-editor-page/services/editor-first-time-events.service'; +import {PopulateRuleContentIdsService} from 'pages/exploration-editor-page/services/populate-rule-content-ids.service'; import INTERACTION_SPECS from 'interactions/interaction_specs.json'; -import { Rule } from 'domain/exploration/rule.model'; -import { GenerateContentIdService } from 'services/generate-content-id.service'; -import { Outcome, OutcomeObjectFactory } from 'domain/exploration/OutcomeObjectFactory'; -import { AppConstants } from 'app.constants'; -import { EditabilityService } from 'services/editability.service'; +import {Rule} from 'domain/exploration/rule.model'; +import {GenerateContentIdService} from 'services/generate-content-id.service'; +import { + Outcome, + OutcomeObjectFactory, +} from 'domain/exploration/OutcomeObjectFactory'; +import {AppConstants} from 'app.constants'; +import {EditabilityService} from 'services/editability.service'; import cloneDeep from 'lodash/cloneDeep'; -import { InteractionSpecsKey } from 'pages/interaction-specs.constants'; - - interface TaggedMisconception { - skillId: string; - misconceptionId: number; - } - - interface DestValidation { - isCreatingNewState: boolean; - value: string; - } - - @Component({ - selector: 'oppia-add-answer-group-modal-component', - templateUrl: './add-answer-group-modal.component.html' - }) +import {InteractionSpecsKey} from 'pages/interaction-specs.constants'; + +interface TaggedMisconception { + skillId: string; + misconceptionId: number; +} + +interface DestValidation { + isCreatingNewState: boolean; + value: string; +} + +@Component({ + selector: 'oppia-add-answer-group-modal-component', + templateUrl: './add-answer-group-modal.component.html', +}) export class AddAnswerGroupModalComponent - extends ConfirmOrCancelModal implements OnInit, OnDestroy { - @Output() addState = new EventEmitter(); - // These properties are initialized using Angular lifecycle hooks - // and we need to do non-null assertion. For more information, see - // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 - @Input() currentInteractionId!: string; - @Input() stateName!: string; - @ViewChild('addResponse',) addResponseRef!: ElementRef; - - eventBusGroup!: EventBusGroup; - tmpRule!: Rule; - tmpOutcome!: Outcome; - // Below property(temporary value) is null until the user clicks on the 'Add' - // button in the answer group modal to add a new answer group. This is to - // prevent the user from adding an answer group without having selected an - // answer group type. - tmpTaggedSkillMisconceptionId!: string | null; - addAnswerGroupForm!: object; - modalId = Symbol(); - isEditable: boolean = false; - feedbackEditorIsOpen: boolean = false; - questionModeEnabled: boolean = false; - isInvalid: boolean = false; - validation: boolean = false; - - constructor( - private ngbActiveModal: NgbActiveModal, - private urlInterpolationService: UrlInterpolationService, - private contextService: ContextService, - private windowRef: WindowRef, - private eventBusService: EventBusService, - private populateRuleContentIdsService: PopulateRuleContentIdsService, - private stateEditorService: StateEditorService, - private editorFirstTimeEventsService: EditorFirstTimeEventsService, - private generateContentIdService: GenerateContentIdService, - private outcomeObjectFactory: OutcomeObjectFactory, - private editabilityService: EditabilityService, - ) { - super(ngbActiveModal); - this.eventBusGroup = new EventBusGroup(this.eventBusService); - } - - updateState(event: string): void { - this.addState.emit(event); - } - - updateTaggedMisconception( - taggedMisconception: TaggedMisconception): void { - this.tmpTaggedSkillMisconceptionId = ( - `${taggedMisconception.skillId}-${ - taggedMisconception.misconceptionId}`); - } - - isSelfLoopWithNoFeedback(tmpOutcome: Outcome): boolean { - return ( - tmpOutcome.dest === - this.stateName && !tmpOutcome.hasNonemptyFeedback()); - } - - openFeedbackEditor(): void { - this.feedbackEditorIsOpen = true; - } - - // This returns false if the current interaction ID is null. - isCurrentInteractionLinear(): boolean { - return ( - Boolean(this.currentInteractionId) && - INTERACTION_SPECS[ - this.currentInteractionId as InteractionSpecsKey].is_linear); - } - - isFeedbackLengthExceeded(tmpOutcome: Outcome): boolean { - // TODO(#13764): Edit this check after appropriate limits are found. - return (tmpOutcome.feedback._html.length > 10000); - } - - validateChanges(value: DestValidation): void { - if (value.isCreatingNewState === true) { - if (value.value === '' || - value.value === undefined || value.value === null) { - this.validation = true; - return; - } - } - - this.validation = false; - } - - saveResponse(reopen: boolean): void { - this.populateRuleContentIdsService - .populateNullRuleContentIds(this.tmpRule); - this.stateEditorService.onSaveOutcomeDestDetails.emit(); - this.stateEditorService.onSaveOutcomeDestIfStuckDetails.emit(); - - this.editorFirstTimeEventsService.registerFirstSaveRuleEvent(); - - // Close the modal and save it afterwards. - this.ngbActiveModal.close({ - tmpRule: cloneDeep(this.tmpRule), - tmpOutcome: cloneDeep(this.tmpOutcome), - tmpTaggedSkillMisconceptionId: ( - this.tmpOutcome.labelledAsCorrect ? null : ( - this.tmpTaggedSkillMisconceptionId)), - reopen: reopen - }); - } - - ngOnInit(): void { - this.eventBusGroup.on( - ObjectFormValidityChangeEvent as Newable, - event => { - if (event.message.modalId === this.modalId) { - this.isInvalid = event.message.value; - } - }); - - this.tmpTaggedSkillMisconceptionId = null; - this.addAnswerGroupForm = {}; - this.modalId = Symbol(); - this.isInvalid = false; - this.feedbackEditorIsOpen = false; - this.isEditable = this.editabilityService.isEditable(); - this.questionModeEnabled = ( - this.stateEditorService.isInQuestionMode()); - - this.tmpRule = Rule.createNew(null, {}, {}); - var feedbackContentId = this.generateContentIdService.getNextStateId( - AppConstants.COMPONENT_NAME_FEEDBACK); - this.tmpOutcome = this.outcomeObjectFactory.createNew( - this.questionModeEnabled ? null : this.stateName, - feedbackContentId, '', []); - } - - updateAnswerGroupFeedback(outcome: Outcome): void { - this.openFeedbackEditor(); - this.tmpOutcome.feedback = outcome.feedback; - } - - ngAfterViewInit(): void { - this.addResponseRef.nativeElement.focus(); - } - - ngOnDestroy(): void { - this.eventBusGroup.unsubscribe(); - } + extends ConfirmOrCancelModal + implements OnInit, OnDestroy +{ + @Output() addState = new EventEmitter(); + // These properties are initialized using Angular lifecycle hooks + // and we need to do non-null assertion. For more information, see + // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 + @Input() currentInteractionId!: string; + @Input() stateName!: string; + @ViewChild('addResponse') addResponseRef!: ElementRef; + + eventBusGroup!: EventBusGroup; + tmpRule!: Rule; + tmpOutcome!: Outcome; + // Below property(temporary value) is null until the user clicks on the 'Add' + // button in the answer group modal to add a new answer group. This is to + // prevent the user from adding an answer group without having selected an + // answer group type. + tmpTaggedSkillMisconceptionId!: string | null; + addAnswerGroupForm!: object; + modalId = Symbol(); + isEditable: boolean = false; + feedbackEditorIsOpen: boolean = false; + questionModeEnabled: boolean = false; + isInvalid: boolean = false; + validation: boolean = false; + + constructor( + private ngbActiveModal: NgbActiveModal, + private urlInterpolationService: UrlInterpolationService, + private contextService: ContextService, + private windowRef: WindowRef, + private eventBusService: EventBusService, + private populateRuleContentIdsService: PopulateRuleContentIdsService, + private stateEditorService: StateEditorService, + private editorFirstTimeEventsService: EditorFirstTimeEventsService, + private generateContentIdService: GenerateContentIdService, + private outcomeObjectFactory: OutcomeObjectFactory, + private editabilityService: EditabilityService + ) { + super(ngbActiveModal); + this.eventBusGroup = new EventBusGroup(this.eventBusService); + } + + updateState(event: string): void { + this.addState.emit(event); + } + + updateTaggedMisconception(taggedMisconception: TaggedMisconception): void { + this.tmpTaggedSkillMisconceptionId = `${taggedMisconception.skillId}-${taggedMisconception.misconceptionId}`; + } + + isSelfLoopWithNoFeedback(tmpOutcome: Outcome): boolean { + return ( + tmpOutcome.dest === this.stateName && !tmpOutcome.hasNonemptyFeedback() + ); + } + + openFeedbackEditor(): void { + this.feedbackEditorIsOpen = true; + } + + // This returns false if the current interaction ID is null. + isCurrentInteractionLinear(): boolean { + return ( + Boolean(this.currentInteractionId) && + INTERACTION_SPECS[this.currentInteractionId as InteractionSpecsKey] + .is_linear + ); + } + + isFeedbackLengthExceeded(tmpOutcome: Outcome): boolean { + // TODO(#13764): Edit this check after appropriate limits are found. + return tmpOutcome.feedback._html.length > 10000; + } + + validateChanges(value: DestValidation): void { + if (value.isCreatingNewState === true) { + if ( + value.value === '' || + value.value === undefined || + value.value === null + ) { + this.validation = true; + return; + } + } + + this.validation = false; + } + + saveResponse(reopen: boolean): void { + this.populateRuleContentIdsService.populateNullRuleContentIds(this.tmpRule); + this.stateEditorService.onSaveOutcomeDestDetails.emit(); + this.stateEditorService.onSaveOutcomeDestIfStuckDetails.emit(); + + this.editorFirstTimeEventsService.registerFirstSaveRuleEvent(); + + // Close the modal and save it afterwards. + this.ngbActiveModal.close({ + tmpRule: cloneDeep(this.tmpRule), + tmpOutcome: cloneDeep(this.tmpOutcome), + tmpTaggedSkillMisconceptionId: this.tmpOutcome.labelledAsCorrect + ? null + : this.tmpTaggedSkillMisconceptionId, + reopen: reopen, + }); + } + + ngOnInit(): void { + this.eventBusGroup.on( + ObjectFormValidityChangeEvent as Newable, + event => { + if (event.message.modalId === this.modalId) { + this.isInvalid = event.message.value; + } + } + ); + + this.tmpTaggedSkillMisconceptionId = null; + this.addAnswerGroupForm = {}; + this.modalId = Symbol(); + this.isInvalid = false; + this.feedbackEditorIsOpen = false; + this.isEditable = this.editabilityService.isEditable(); + this.questionModeEnabled = this.stateEditorService.isInQuestionMode(); + + this.tmpRule = Rule.createNew(null, {}, {}); + var feedbackContentId = this.generateContentIdService.getNextStateId( + AppConstants.COMPONENT_NAME_FEEDBACK + ); + this.tmpOutcome = this.outcomeObjectFactory.createNew( + this.questionModeEnabled ? null : this.stateName, + feedbackContentId, + '', + [] + ); + } + + updateAnswerGroupFeedback(outcome: Outcome): void { + this.openFeedbackEditor(); + this.tmpOutcome.feedback = outcome.feedback; + } + + ngAfterViewInit(): void { + this.addResponseRef.nativeElement.focus(); + } + + ngOnDestroy(): void { + this.eventBusGroup.unsubscribe(); + } } -angular.module('oppia').directive('oppiaAddAnswerGroupModalComponent', - downgradeComponent({ - component: AddAnswerGroupModalComponent - }) as angular.IDirectiveFactory); +angular.module('oppia').directive( + 'oppiaAddAnswerGroupModalComponent', + downgradeComponent({ + component: AddAnswerGroupModalComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/add-hint-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/add-hint-modal.component.spec.ts index 503aa631c055..e64e7849db66 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/add-hint-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/add-hint-modal.component.spec.ts @@ -16,14 +16,14 @@ * @fileoverview Unit tests for AddHintModalComponent. */ -import { ChangeDetectorRef, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { StateHintsService } from 'components/state-editor/state-editor-properties-services/state-hints.service'; -import { Hint } from 'domain/exploration/hint-object.model'; -import { ContextService } from 'services/context.service'; -import { GenerateContentIdService } from 'services/generate-content-id.service'; -import { AddHintModalComponent } from './add-hint-modal.component'; +import {ChangeDetectorRef, NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {StateHintsService} from 'components/state-editor/state-editor-properties-services/state-hints.service'; +import {Hint} from 'domain/exploration/hint-object.model'; +import {ContextService} from 'services/context.service'; +import {GenerateContentIdService} from 'services/generate-content-id.service'; +import {AddHintModalComponent} from './add-hint-modal.component'; class MockActiveModal { close(): void { @@ -45,19 +45,18 @@ describe('Add Hint Modal Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - AddHintModalComponent - ], + declarations: [AddHintModalComponent], providers: [ ContextService, GenerateContentIdService, ChangeDetectorRef, - StateHintsService, { + StateHintsService, + { provide: NgbActiveModal, - useClass: MockActiveModal + useClass: MockActiveModal, }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -75,30 +74,28 @@ describe('Add Hint Modal Component', () => { fixture.detectChanges(); }); - it('should initialize the properties after component is initialized', - () => { - expect(component.tmpHint).toBe(''); - expect(component.hintIndex).toBe(5); - }); + it('should initialize the properties after component is initialized', () => { + expect(component.tmpHint).toBe(''); + expect(component.hintIndex).toBe(5); + }); it('should get schema', () => { - expect(component.getSchema()) - .toEqual(component.HINT_FORM_SCHEMA); + expect(component.getSchema()).toEqual(component.HINT_FORM_SCHEMA); }); it('should save hint when closing the modal', () => { let contentId = 'cont_1'; let hintExpected = Hint.createNew(contentId, ''); spyOn(ngbActiveModal, 'close'); - spyOn( - generateContentIdService, 'getNextStateId' - ).and.returnValue(contentId); + spyOn(generateContentIdService, 'getNextStateId').and.returnValue( + contentId + ); component.saveHint(); expect(ngbActiveModal.close).toHaveBeenCalledWith({ hint: hintExpected, - contentId: contentId + contentId: contentId, }); }); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/add-hint-modal.component.ts b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/add-hint-modal.component.ts index 1c653ef78a7d..ee9b712b422d 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/add-hint-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/add-hint-modal.component.ts @@ -16,29 +16,34 @@ * @fileoverview Component for add hint modal. */ -import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import {ChangeDetectorRef, Component, OnInit} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; import cloneDeep from 'lodash/cloneDeep'; -import { AppConstants } from 'app.constants'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { StateHintsService } from 'components/state-editor/state-editor-properties-services/state-hints.service'; -import { Hint } from 'domain/exploration/hint-object.model'; -import { ContextService } from 'services/context.service'; -import { GenerateContentIdService } from 'services/generate-content-id.service'; -import { ExplorationEditorPageConstants } from 'pages/exploration-editor-page/exploration-editor-page.constants'; -import { CALCULATION_TYPE_CHARACTER, HtmlLengthService } from 'services/html-length.service'; +import {AppConstants} from 'app.constants'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {StateHintsService} from 'components/state-editor/state-editor-properties-services/state-hints.service'; +import {Hint} from 'domain/exploration/hint-object.model'; +import {ContextService} from 'services/context.service'; +import {GenerateContentIdService} from 'services/generate-content-id.service'; +import {ExplorationEditorPageConstants} from 'pages/exploration-editor-page/exploration-editor-page.constants'; +import { + CALCULATION_TYPE_CHARACTER, + HtmlLengthService, +} from 'services/html-length.service'; interface HintFormSchema { type: string; - 'ui_config': object; + ui_config: object; } @Component({ selector: 'oppia-add-hint-modal', - templateUrl: './add-hint-modal.component.html' + templateUrl: './add-hint-modal.component.html', }) export class AddHintModalComponent - extends ConfirmOrCancelModal implements OnInit { + extends ConfirmOrCancelModal + implements OnInit +{ // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -49,9 +54,10 @@ export class AddHintModalComponent HINT_FORM_SCHEMA: HintFormSchema = { type: 'html', ui_config: { - hide_complex_extensions: ( - this.contextService.getEntityType() === 'question') - }}; + hide_complex_extensions: + this.contextService.getEntityType() === 'question', + }, + }; constructor( private changeDetectorRef: ChangeDetectorRef, @@ -59,7 +65,7 @@ export class AddHintModalComponent private generateContentIdService: GenerateContentIdService, private ngbActiveModal: NgbActiveModal, private stateHintsService: StateHintsService, - private htmlLengthService: HtmlLengthService, + private htmlLengthService: HtmlLengthService ) { super(ngbActiveModal); } @@ -77,8 +83,10 @@ export class AddHintModalComponent isHintLengthExceeded(tmpHint: string): boolean { return Boolean( this.htmlLengthService.computeHtmlLength( - tmpHint, CALCULATION_TYPE_CHARACTER) > - ExplorationEditorPageConstants.HINT_CHARACTER_LIMIT); + tmpHint, + CALCULATION_TYPE_CHARACTER + ) > ExplorationEditorPageConstants.HINT_CHARACTER_LIMIT + ); } updateLocalHint($event: string): void { @@ -90,12 +98,12 @@ export class AddHintModalComponent saveHint(): void { let contentId = this.generateContentIdService.getNextStateId( - this.COMPONENT_NAME_HINT); + this.COMPONENT_NAME_HINT + ); // Close the modal and save it afterwards. this.ngbActiveModal.close({ - hint: cloneDeep( - Hint.createNew(contentId, this.tmpHint)), - contentId: contentId + hint: cloneDeep(Hint.createNew(contentId, this.tmpHint)), + contentId: contentId, }); } } diff --git a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/add-or-update-solution-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/add-or-update-solution-modal.component.spec.ts index 8dc71e858d9b..71ba643561a2 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/add-or-update-solution-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/add-or-update-solution-modal.component.spec.ts @@ -16,20 +16,23 @@ * @fileoverview Unit tests for add or update solution modal component. */ -import { ChangeDetectorRef, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { StateCustomizationArgsService } from 'components/state-editor/state-editor-properties-services/state-customization-args.service'; -import { StateInteractionIdService } from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; -import { StateSolutionService } from 'components/state-editor/state-editor-properties-services/state-solution.service'; -import { Solution, SolutionObjectFactory } from 'domain/exploration/SolutionObjectFactory'; -import { CurrentInteractionService } from 'pages/exploration-player-page/services/current-interaction.service'; -import { ContextService } from 'services/context.service'; -import { ExplorationHtmlFormatterService } from 'services/exploration-html-formatter.service'; -import { AddOrUpdateSolutionModalComponent } from './add-or-update-solution-modal.component'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { InteractionRulesService } from 'pages/exploration-player-page/services/answer-classification.service'; -import { GenerateContentIdService } from 'services/generate-content-id.service'; +import {ChangeDetectorRef, NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {StateCustomizationArgsService} from 'components/state-editor/state-editor-properties-services/state-customization-args.service'; +import {StateInteractionIdService} from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; +import {StateSolutionService} from 'components/state-editor/state-editor-properties-services/state-solution.service'; +import { + Solution, + SolutionObjectFactory, +} from 'domain/exploration/SolutionObjectFactory'; +import {CurrentInteractionService} from 'pages/exploration-player-page/services/current-interaction.service'; +import {ContextService} from 'services/context.service'; +import {ExplorationHtmlFormatterService} from 'services/exploration-html-formatter.service'; +import {AddOrUpdateSolutionModalComponent} from './add-or-update-solution-modal.component'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {InteractionRulesService} from 'pages/exploration-player-page/services/answer-classification.service'; +import {GenerateContentIdService} from 'services/generate-content-id.service'; class MockActiveModal { close(): void { @@ -58,9 +61,7 @@ describe('Add Or Update Solution Modal Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - AddOrUpdateSolutionModalComponent - ], + declarations: [AddOrUpdateSolutionModalComponent], providers: [ ContextService, CurrentInteractionService, @@ -68,13 +69,13 @@ describe('Add Or Update Solution Modal Component', () => { ExplorationHtmlFormatterService, { provide: NgbActiveModal, - useClass: MockActiveModal + useClass: MockActiveModal, }, StateCustomizationArgsService, StateInteractionIdService, - StateSolutionService + StateSolutionService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -83,12 +84,16 @@ describe('Add Or Update Solution Modal Component', () => { component = fixture.componentInstance; currentInteractionService = TestBed.inject(CurrentInteractionService); explorationHtmlFormatterService = TestBed.inject( - ExplorationHtmlFormatterService); + ExplorationHtmlFormatterService + ); solutionObjectFactory = TestBed.inject(SolutionObjectFactory); stateInteractionIdService = TestBed.inject(StateInteractionIdService); stateSolutionService = TestBed.inject(StateSolutionService); generateContentIdService = TestBed.inject(GenerateContentIdService); - generateContentIdService.init(() => 0, () => {}); + generateContentIdService.init( + () => 0, + () => {} + ); }); describe('when solution is valid', () => { @@ -97,12 +102,17 @@ describe('Add Or Update Solution Modal Component', () => { contextService = TestBed.inject(ContextService); spyOn(contextService, 'getEntityType').and.returnValue('question'); - spyOn(explorationHtmlFormatterService, 'getInteractionHtml') - .and.returnValue('

Interaction Html

'); + spyOn( + explorationHtmlFormatterService, + 'getInteractionHtml' + ).and.returnValue('

Interaction Html

'); answerEditorHtml = new Solution( - explorationHtmlFormatterService, true, 'solution', - SubtitledHtml.createDefault('Explanation html', 'cont_1')); + explorationHtmlFormatterService, + true, + 'solution', + SubtitledHtml.createDefault('Explanation html', 'cont_1') + ); stateSolutionService.init('', answerEditorHtml); stateInteractionIdService.init('', 'TextInput'); @@ -114,12 +124,13 @@ describe('Add Or Update Solution Modal Component', () => { stateSolutionService.init('', answerEditorHtml); expect(component.correctAnswerEditorHtml).toEqual( - '

Interaction Html

'); + '

Interaction Html

' + ); expect(component.data).toEqual({ answerIsExclusive: true, correctAnswer: undefined, explanationHtml: 'Explanation html', - explanationContentId: 'cont_1' + explanationContentId: 'cont_1', }); expect(component.answerIsValid).toBeFalse(); expect(component.ansOptions).toEqual(['The only', 'One']); @@ -140,12 +151,11 @@ describe('Add Or Update Solution Modal Component', () => { expect(component.data.answerIsExclusive).toBeFalse(); }); - it('should update correct answer when submitting current interaction', - () => { - currentInteractionService.onSubmit('answer', mockInteractionRule); + it('should update correct answer when submitting current interaction', () => { + currentInteractionService.onSubmit('answer', mockInteractionRule); - expect(component.data.correctAnswer).toEqual('answer'); - }); + expect(component.data.correctAnswer).toEqual('answer'); + }); it('should submit answer when clicking on submit button', () => { spyOn(currentInteractionService, 'submitAnswer'); @@ -168,8 +178,10 @@ describe('Add Or Update Solution Modal Component', () => { }); it('should tell if submit button is disabled', () => { - spyOn(currentInteractionService, 'isSubmitButtonDisabled') - .and.returnValue(true); + spyOn( + currentInteractionService, + 'isSubmitButtonDisabled' + ).and.returnValue(true); expect(component.isSubmitButtonDisabled()).toBeTrue(); }); @@ -183,15 +195,20 @@ describe('Add Or Update Solution Modal Component', () => { expect(ngbActiveModal.close).toHaveBeenCalledWith({ solution: solutionObjectFactory.createNew( - true, 'answer', 'Explanation html', 'cont_1') + true, + 'answer', + 'Explanation html', + 'cont_1' + ), }); }); it('should not show solution explanation length validation error', () => { let solutionExplanation = '

Explanation html

'; - expect(component.isSolutionExplanationLengthExceeded( - solutionExplanation)).toBeFalse(); + expect( + component.isSolutionExplanationLengthExceeded(solutionExplanation) + ).toBeFalse(); }); }); @@ -201,8 +218,10 @@ describe('Add Or Update Solution Modal Component', () => { contextService = TestBed.inject(ContextService); spyOn(contextService, 'getEntityType').and.returnValue('question'); - spyOn(explorationHtmlFormatterService, 'getInteractionHtml').and - .returnValue('answerEditorHtml'); + spyOn( + explorationHtmlFormatterService, + 'getInteractionHtml' + ).and.returnValue('answerEditorHtml'); stateInteractionIdService.init('', 'TextInput'); fixture.detectChanges(); @@ -217,8 +236,9 @@ describe('Add Or Update Solution Modal Component', () => { it('should show solution explanation length validation error', () => { let solutionExplanation = '

Solution explanation

'.repeat(180); - expect(component.isSolutionExplanationLengthExceeded( - solutionExplanation)).toBeTrue(); + expect( + component.isSolutionExplanationLengthExceeded(solutionExplanation) + ).toBeTrue(); }); }); }); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/add-or-update-solution-modal.component.ts b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/add-or-update-solution-modal.component.ts index fd83b184caae..af3a47d2bd2d 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/add-or-update-solution-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/add-or-update-solution-modal.component.ts @@ -16,21 +16,30 @@ * @fileoverview Component for add or update solution modal. */ -import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { AppConstants } from 'app.constants'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { ContextService } from 'services/context.service'; -import { CurrentInteractionService } from 'pages/exploration-player-page/services/current-interaction.service'; -import { ExplorationHtmlFormatterService } from 'services/exploration-html-formatter.service'; -import { InteractionAnswer } from 'interactions/answer-defs'; -import { StateCustomizationArgsService } from 'components/state-editor/state-editor-properties-services/state-customization-args.service'; -import { StateInteractionIdService } from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; -import { StateSolutionService } from 'components/state-editor/state-editor-properties-services/state-solution.service'; -import { Solution, SolutionObjectFactory } from 'domain/exploration/SolutionObjectFactory'; -import { InteractionSpecsConstants, InteractionSpecsKey } from 'pages/interaction-specs.constants'; -import { GenerateContentIdService } from 'services/generate-content-id.service'; -import { CALCULATION_TYPE_CHARACTER, HtmlLengthService } from 'services/html-length.service'; +import {ChangeDetectorRef, Component, OnInit} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {AppConstants} from 'app.constants'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {ContextService} from 'services/context.service'; +import {CurrentInteractionService} from 'pages/exploration-player-page/services/current-interaction.service'; +import {ExplorationHtmlFormatterService} from 'services/exploration-html-formatter.service'; +import {InteractionAnswer} from 'interactions/answer-defs'; +import {StateCustomizationArgsService} from 'components/state-editor/state-editor-properties-services/state-customization-args.service'; +import {StateInteractionIdService} from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; +import {StateSolutionService} from 'components/state-editor/state-editor-properties-services/state-solution.service'; +import { + Solution, + SolutionObjectFactory, +} from 'domain/exploration/SolutionObjectFactory'; +import { + InteractionSpecsConstants, + InteractionSpecsKey, +} from 'pages/interaction-specs.constants'; +import {GenerateContentIdService} from 'services/generate-content-id.service'; +import { + CALCULATION_TYPE_CHARACTER, + HtmlLengthService, +} from 'services/html-length.service'; interface HtmlFormSchema { type: 'html'; @@ -52,10 +61,12 @@ interface SolutionInterface { @Component({ selector: 'oppia-add-or-update-solution-modal', - templateUrl: './add-or-update-solution-modal.component.html' + templateUrl: './add-or-update-solution-modal.component.html', }) export class AddOrUpdateSolutionModalComponent - extends ConfirmOrCancelModal implements OnInit { + extends ConfirmOrCancelModal + implements OnInit +{ // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -69,15 +80,15 @@ export class AddOrUpdateSolutionModalComponent tempAnsOption!: string; COMPONENT_NAME_SOLUTION: string = AppConstants.COMPONENT_NAME_SOLUTION; - SOLUTION_EDITOR_FOCUS_LABEL: string = ( - 'currentCorrectAnswerEditorHtmlForSolutionEditor'); + SOLUTION_EDITOR_FOCUS_LABEL: string = + 'currentCorrectAnswerEditorHtmlForSolutionEditor'; EXPLANATION_FORM_SCHEMA: HtmlFormSchema = { type: 'html', ui_config: { - hide_complex_extensions: ( - this.contextService.getEntityType() === 'question') - } + hide_complex_extensions: + this.contextService.getEntityType() === 'question', + }, }; constructor( @@ -91,7 +102,7 @@ export class AddOrUpdateSolutionModalComponent private stateCustomizationArgsService: StateCustomizationArgsService, private stateInteractionIdService: StateInteractionIdService, private stateSolutionService: StateSolutionService, - private htmlLengthService: HtmlLengthService, + private htmlLengthService: HtmlLengthService ) { super(ngbActiveModal); } @@ -101,22 +112,24 @@ export class AddOrUpdateSolutionModalComponent } shouldAdditionalSubmitButtonBeShown(): boolean { - let interactionId = ( - this.stateInteractionIdService.savedMemento as InteractionSpecsKey); - const interactionSpec = ( - InteractionSpecsConstants.INTERACTION_SPECS[interactionId]); + let interactionId = this.stateInteractionIdService + .savedMemento as InteractionSpecsKey; + const interactionSpec = + InteractionSpecsConstants.INTERACTION_SPECS[interactionId]; return interactionSpec.show_generic_submit_button; } - isSolutionExplanationLengthExceeded( - solExplanation: string): boolean { + isSolutionExplanationLengthExceeded(solExplanation: string): boolean { return Boolean( this.htmlLengthService.computeHtmlLength( - solExplanation, CALCULATION_TYPE_CHARACTER) > 3000); + solExplanation, + CALCULATION_TYPE_CHARACTER + ) > 3000 + ); } onAnswerChange(): void { - this.data.answerIsExclusive = (this.tempAnsOption === this.ansOptions[0]); + this.data.answerIsExclusive = this.tempAnsOption === this.ansOptions[0]; } isSubmitButtonDisabled(): boolean { @@ -135,7 +148,8 @@ export class AddOrUpdateSolutionModalComponent this.data.answerIsExclusive, this.data.correctAnswer, this.data.explanationHtml, - this.data.explanationContentId) + this.data.explanationContentId + ), }); } else { throw new Error('Cannot save invalid solution'); @@ -145,26 +159,25 @@ export class AddOrUpdateSolutionModalComponent ngOnInit(): void { this.solutionType = this.stateSolutionService.savedMemento; if (this.solutionType) { - this.savedSolution = ( - this.solutionType.correctAnswer); + this.savedSolution = this.solutionType.correctAnswer; } else { this.savedSolution = null; } - this.correctAnswerEditorHtml = ( + this.correctAnswerEditorHtml = this.explorationHtmlFormatterService.getInteractionHtml( this.stateInteractionIdService.savedMemento, this.stateCustomizationArgsService.savedMemento, false, this.SOLUTION_EDITOR_FOCUS_LABEL, - this.savedSolution ? 'savedSolution' : null) - ); + this.savedSolution ? 'savedSolution' : null + ); this.answerIsValid = false; if (this.solutionType) { this.data = { answerIsExclusive: this.solutionType.answerIsExclusive, correctAnswer: undefined, explanationHtml: this.solutionType.explanation.html, - explanationContentId: this.solutionType.explanation.contentId + explanationContentId: this.solutionType.explanation.contentId, }; } else { this.data = { @@ -172,13 +185,15 @@ export class AddOrUpdateSolutionModalComponent correctAnswer: undefined, explanationHtml: '', explanationContentId: this.generateContentIdService.getNextStateId( - this.COMPONENT_NAME_SOLUTION) + this.COMPONENT_NAME_SOLUTION + ), }; } this.currentInteractionService.setOnSubmitFn( (answer: InteractionAnswer) => { this.data.correctAnswer = answer; - }); + } + ); this.ansOptions = ['The only', 'One']; this.tempAnsOption = this.ansOptions[1]; } diff --git a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/add-outcome-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/add-outcome-modal.component.spec.ts index f570c1027c89..350673afd7c0 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/add-outcome-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/add-outcome-modal.component.spec.ts @@ -16,12 +16,12 @@ * @fileoverview Unit tests for AddOutcomeModalComponent. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { Outcome } from 'domain/exploration/OutcomeObjectFactory'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { AddOutcomeModalComponent } from './add-outcome-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {Outcome} from 'domain/exploration/OutcomeObjectFactory'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {AddOutcomeModalComponent} from './add-outcome-modal.component'; class MockActiveModal { close(): void { @@ -40,13 +40,11 @@ describe('Add Outcome Modal Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - AddOutcomeModalComponent - ], + declarations: [AddOutcomeModalComponent], providers: [ { provide: NgbActiveModal, - useClass: MockActiveModal + useClass: MockActiveModal, }, ], schemas: [NO_ERRORS_SCHEMA], @@ -59,7 +57,6 @@ describe('Add Outcome Modal Component', () => { ngbActiveModal = TestBed.inject(NgbActiveModal); }); - it('should save outcome when closing the modal', () => { component.outcome = new Outcome( 'Dest', @@ -68,14 +65,14 @@ describe('Add Outcome Modal Component', () => { true, [], 'OutcomeExpId', - 'SkillId', + 'SkillId' ); spyOn(ngbActiveModal, 'close'); component.save(); expect(ngbActiveModal.close).toHaveBeenCalledWith({ - outcome: component.outcome + outcome: component.outcome, }); }); @@ -87,7 +84,7 @@ describe('Add Outcome Modal Component', () => { true, [], 'OutcomeExpId', - 'SkillId', + 'SkillId' ); expect(component.isFeedbackLengthExceeded()).toBe(false); @@ -101,7 +98,7 @@ describe('Add Outcome Modal Component', () => { true, [], 'OutcomeExpId', - 'SkillId', + 'SkillId' ); expect(component.isFeedbackLengthExceeded()).toBe(true); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/add-outcome-modal.component.ts b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/add-outcome-modal.component.ts index 0465da59a999..ddce914a566d 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/add-outcome-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/add-outcome-modal.component.ts @@ -16,37 +16,37 @@ * @fileoverview Component for add outcome modal. */ -import { Component, Input, OnInit } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import {Component, Input, OnInit} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; import cloneDeep from 'lodash/cloneDeep'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { Outcome } from 'domain/exploration/OutcomeObjectFactory'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Outcome} from 'domain/exploration/OutcomeObjectFactory'; @Component({ selector: 'oppia-add-outcome-modal', - templateUrl: './add-outcome-modal.component.html' + templateUrl: './add-outcome-modal.component.html', }) -export class AddOutcomeModalComponent extends ConfirmOrCancelModal -implements OnInit { +export class AddOutcomeModalComponent + extends ConfirmOrCancelModal + implements OnInit +{ @Input() outcome!: Outcome; - constructor( - private ngbActiveModal: NgbActiveModal, - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } - ngOnInit(): void { } + ngOnInit(): void {} isFeedbackLengthExceeded(): boolean { // TODO(#13764): Edit this check after appropriate limits are found. - return (this.outcome.feedback._html.length > 10000); + return this.outcome.feedback._html.length > 10000; } save(): void { // Close the modal and save it afterwards. this.ngbActiveModal.close({ - outcome: cloneDeep(this.outcome) + outcome: cloneDeep(this.outcome), }); } } diff --git a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/confirm-delete-state-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/confirm-delete-state-modal.component.spec.ts index c987a838f80d..1260f4591c16 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/confirm-delete-state-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/confirm-delete-state-modal.component.spec.ts @@ -16,10 +16,10 @@ * @fileoverview Unit tests for ConfirmDeleteStateModalComponent. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmDeleteStateModalComponent } from './confirm-delete-state-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmDeleteStateModalComponent} from './confirm-delete-state-modal.component'; class MockActiveModal { close(): void { @@ -31,21 +31,21 @@ class MockActiveModal { } } -describe('Confirm Delete State Modal Component', function() { +describe('Confirm Delete State Modal Component', function () { let component: ConfirmDeleteStateModalComponent; let fixture: ComponentFixture; let deleteStateName = 'Introduction'; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - ConfirmDeleteStateModalComponent + declarations: [ConfirmDeleteStateModalComponent], + providers: [ + { + provide: NgbActiveModal, + useClass: MockActiveModal, + }, ], - providers: [{ - provide: NgbActiveModal, - useClass: MockActiveModal - }], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -59,6 +59,7 @@ describe('Confirm Delete State Modal Component', function() { it('should initialize properties after component is initialized', () => { expect(component.deleteStateWarningText).toBe( - 'Are you sure you want to delete the card "Introduction"?'); + 'Are you sure you want to delete the card "Introduction"?' + ); }); }); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/confirm-delete-state-modal.component.ts b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/confirm-delete-state-modal.component.ts index 9a9972f727df..042eed428755 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/confirm-delete-state-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/confirm-delete-state-modal.component.ts @@ -16,31 +16,32 @@ * @fileoverview Component for confirm delete state modal. */ -import { Component, Input, OnInit } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component, Input, OnInit} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-confirm-delete-state-modal', - templateUrl: './confirm-delete-state-modal.component.html' + templateUrl: './confirm-delete-state-modal.component.html', }) -export class ConfirmDeleteStateModalComponent extends ConfirmOrCancelModal - implements OnInit { +export class ConfirmDeleteStateModalComponent + extends ConfirmOrCancelModal + implements OnInit +{ // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @Input() deleteStateName!: string; deleteStateWarningText!: string; - constructor( - private ngbActiveModal: NgbActiveModal, - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } ngOnInit(): void { - this.deleteStateWarningText = ( + this.deleteStateWarningText = 'Are you sure you want to delete the card "' + - this.deleteStateName + '"?'); + this.deleteStateName + + '"?'; } } diff --git a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/customize-interaction-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/customize-interaction-modal.component.spec.ts index 3ff6b2ad0bba..877f5032cd63 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/customize-interaction-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/customize-interaction-modal.component.spec.ts @@ -16,22 +16,39 @@ * @fileoverview Unit tests for Customize Interaction Modal. */ -import { ChangeDetectorRef, EventEmitter, NO_ERRORS_SCHEMA, ElementRef } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal, NgbModal, NgbModalModule, NgbModalRef, NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { StateInteractionIdService } from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; -import { CustomizeInteractionModalComponent } from './customize-interaction-modal.component'; -import { InteractionDetailsCacheService } from 'pages/exploration-editor-page/editor-tab/services/interaction-details-cache.service'; -import { StateCustomizationArgsService } from 'components/state-editor/state-editor-properties-services/state-customization-args.service'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { EditorFirstTimeEventsService } from 'pages/exploration-editor-page/services/editor-first-time-events.service'; -import { InteractionObjectFactory } from 'domain/exploration/InteractionObjectFactory'; -import { SubtitledUnicodeObjectFactory } from 'domain/exploration/SubtitledUnicodeObjectFactory'; -import { ContextService } from 'services/context.service'; -import { AppConstants } from 'app.constants'; -import { RatioExpressionInputValidationService } from 'interactions/RatioExpressionInput/directives/ratio-expression-input-validation.service'; +import { + ChangeDetectorRef, + EventEmitter, + NO_ERRORS_SCHEMA, + ElementRef, +} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import { + NgbActiveModal, + NgbModal, + NgbModalModule, + NgbModalRef, + NgbModule, +} from '@ng-bootstrap/ng-bootstrap'; +import {StateInteractionIdService} from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; +import {CustomizeInteractionModalComponent} from './customize-interaction-modal.component'; +import {InteractionDetailsCacheService} from 'pages/exploration-editor-page/editor-tab/services/interaction-details-cache.service'; +import {StateCustomizationArgsService} from 'components/state-editor/state-editor-properties-services/state-customization-args.service'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {EditorFirstTimeEventsService} from 'pages/exploration-editor-page/services/editor-first-time-events.service'; +import {InteractionObjectFactory} from 'domain/exploration/InteractionObjectFactory'; +import {SubtitledUnicodeObjectFactory} from 'domain/exploration/SubtitledUnicodeObjectFactory'; +import {ContextService} from 'services/context.service'; +import {AppConstants} from 'app.constants'; +import {RatioExpressionInputValidationService} from 'interactions/RatioExpressionInput/directives/ratio-expression-input-validation.service'; import INTERACTION_SPECS from 'interactions/interaction_specs.json'; -import { GenerateContentIdService } from 'services/generate-content-id.service'; +import {GenerateContentIdService} from 'services/generate-content-id.service'; class MockStateCustomizationArgsService { displayed = { @@ -41,14 +58,14 @@ class MockStateCustomizationArgsService { _unicode: '2:3', contentId: 'ca_placeholder_0', unicode: '2:3', - } + }, }, numberOfTerms: { - value: 0 + value: 0, }, hasOwnProperty(argName) { return true; - } + }, }; savedMemento = { @@ -58,14 +75,14 @@ class MockStateCustomizationArgsService { _unicode: '2:3', contentId: 'ca_placeholder_0', unicode: '2:3', - } + }, }, numberOfTerms: { - value: 0 + value: 0, }, hasOwnProperty(argName) { return true; - } + }, }; get onSchemaBasedFormsShown(): EventEmitter { @@ -83,14 +100,14 @@ const MockInteractionState = { name: 'placeholder', schema: { type: 'custom', - obj_type: 'SubtitledUnicode' + obj_type: 'SubtitledUnicode', }, default_value: { content_id: null, - unicode_str: '' - } - } - ] + unicode_str: '', + }, + }, + ], }, }; @@ -114,8 +131,7 @@ describe('Customize Interaction Modal Component', () => { let interactionObjectFactory: InteractionObjectFactory; let ngbActiveModal: NgbActiveModal; let ngbModal: NgbModal; - let ratioExpressionInputValidationService: - RatioExpressionInputValidationService; + let ratioExpressionInputValidationService: RatioExpressionInputValidationService; let stateEditorService: StateEditorService; let stateInteractionIdService: StateInteractionIdService; let subtitledUnicodeObjectFactory: SubtitledUnicodeObjectFactory; @@ -123,13 +139,8 @@ describe('Customize Interaction Modal Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - NgbModalModule, - NgbModule - ], - declarations: [ - CustomizeInteractionModalComponent, - ], + imports: [NgbModalModule, NgbModule], + declarations: [CustomizeInteractionModalComponent], providers: [ NgbActiveModal, StateInteractionIdService, @@ -142,22 +153,22 @@ describe('Customize Interaction Modal Component', () => { ContextService, { provide: INTERACTION_SPECS, - useValue: MockInteractionState + useValue: MockInteractionState, }, { provide: ChangeDetectorRef, - useClass: MockChangeDetectorRef + useClass: MockChangeDetectorRef, }, { provide: StateCustomizationArgsService, - useClass: MockStateCustomizationArgsService + useClass: MockStateCustomizationArgsService, }, { provide: StateEditorService, - useClass: MockStateEditorService - } + useClass: MockStateEditorService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -168,20 +179,27 @@ describe('Customize Interaction Modal Component', () => { changeDetectorRef = TestBed.inject(ChangeDetectorRef); contextService = TestBed.inject(ContextService); interactionDetailsCacheService = TestBed.inject( - InteractionDetailsCacheService); + InteractionDetailsCacheService + ); ngbModal = TestBed.inject(NgbModal); ngbActiveModal = TestBed.inject(NgbActiveModal); interactionObjectFactory = TestBed.inject(InteractionObjectFactory); stateCustomizationArgsService = TestBed.inject( - StateCustomizationArgsService); + StateCustomizationArgsService + ); stateEditorService = TestBed.inject(StateEditorService); stateInteractionIdService = TestBed.inject(StateInteractionIdService); subtitledUnicodeObjectFactory = TestBed.inject( - SubtitledUnicodeObjectFactory); + SubtitledUnicodeObjectFactory + ); ratioExpressionInputValidationService = TestBed.inject( - RatioExpressionInputValidationService); + RatioExpressionInputValidationService + ); generateContentIdService = TestBed.inject(GenerateContentIdService); - generateContentIdService.init(() => 0, () => {}); + generateContentIdService.init( + () => 0, + () => {} + ); stateInteractionIdService.displayed = 'RatioExpressionInput'; fixture.detectChanges(); @@ -190,41 +208,50 @@ describe('Customize Interaction Modal Component', () => { it('should return the hyphenated category name as expected', () => { const categoryName = 'Camel Case CATEGORY Name With Spaces'; expect(component.getHyphenatedLowercaseCategoryName(categoryName)).toBe( - 'camel-case-category-name-with-spaces'); + 'camel-case-category-name-with-spaces' + ); }); - it('should get complete interaction thumbnail icon path corresponding to' + - ' a given relative path', () => { - const interactionId = 'i1'; - expect(component.getInteractionThumbnailImageUrl(interactionId)).toBe( - '/extensions/interactions/i1/static/i1.png'); - }); + it( + 'should get complete interaction thumbnail icon path corresponding to' + + ' a given relative path', + () => { + const interactionId = 'i1'; + expect(component.getInteractionThumbnailImageUrl(interactionId)).toBe( + '/extensions/interactions/i1/static/i1.png' + ); + } + ); it('should be defined', () => { - const warningListData = [{ - type: 'error', - message: ( - 'The number of terms should be a non-negative integer other than 1.' - ) - }]; + const warningListData = [ + { + type: 'error', + message: + 'The number of terms should be a non-negative integer other than 1.', + }, + ]; stateInteractionIdService.displayed = 'RatioExpressionInput'; - spyOn(ratioExpressionInputValidationService, 'getCustomizationArgsWarnings') - .and.returnValue(warningListData); + spyOn( + ratioExpressionInputValidationService, + 'getCustomizationArgsWarnings' + ).and.returnValue(warningListData); expect(component).toBeDefined(); - expect(component.getTitle('NumberWithUnits')) - .toBe('Number With Units'); - expect(component.getDescription('NumericInput')) - .toBe( - 'Allows learners to enter integers and floating point numbers.'); + expect(component.getTitle('NumberWithUnits')).toBe('Number With Units'); + expect(component.getDescription('NumericInput')).toBe( + 'Allows learners to enter integers and floating point numbers.' + ); let result = component.getSchemaCallback({type: 'bool'}); expect(result()).toEqual({type: 'bool'}); - expect(component.getCustomizationArgsWarningsList()) - .toEqual(warningListData); + expect(component.getCustomizationArgsWarningsList()).toEqual( + warningListData + ); expect(component.getCustomizationArgsWarningMessage()).toBe( - 'The number of terms should be a non-negative integer other than 1.'); + 'The number of terms should be a non-negative integer other than 1.' + ); }); it('should update view after chagnes', () => { @@ -238,21 +265,20 @@ describe('Customize Interaction Modal Component', () => { it('should update Save interaction Button when userinputs data', () => { component.hasCustomizationArgs = true; - spyOn(component, 'getCustomizationArgsWarningsList').and - .returnValue([]); + spyOn(component, 'getCustomizationArgsWarningsList').and.returnValue([]); expect(component.isSaveInteractionButtonEnabled()).toBe(true); }); it('should open intreaction when user click on it', () => { - spyOn(interactionDetailsCacheService, 'contains').and - .returnValue(true); - spyOn(interactionDetailsCacheService, 'get').and - .returnValue('RatioExpressionInput'); + spyOn(interactionDetailsCacheService, 'contains').and.returnValue(true); + spyOn(interactionDetailsCacheService, 'get').and.returnValue( + 'RatioExpressionInput' + ); const mockCustomizeInteractionHeaderRef = new ElementRef( - document.createElement('h3')); - component.customizeInteractionHeader = - mockCustomizeInteractionHeaderRef; + document.createElement('h3') + ); + component.customizeInteractionHeader = mockCustomizeInteractionHeaderRef; component.onChangeInteractionId('RatioExpressionInput'); @@ -261,17 +287,15 @@ describe('Customize Interaction Modal Component', () => { }); it('should open save intreaction when user click on it', () => { - spyOn(interactionDetailsCacheService, 'contains').and - .returnValue(true); - spyOn(interactionDetailsCacheService, 'get').and - .returnValue({}); + spyOn(interactionDetailsCacheService, 'contains').and.returnValue(true); + spyOn(interactionDetailsCacheService, 'get').and.returnValue({}); component.originalContentIdToContent = subtitledUnicodeObjectFactory.createDefault('unicode', 'contentId'); const mockCustomizeInteractionHeaderRef = new ElementRef( - document.createElement('h3')); - component.customizeInteractionHeader = - mockCustomizeInteractionHeaderRef; + document.createElement('h3') + ); + component.customizeInteractionHeader = mockCustomizeInteractionHeaderRef; component.onChangeInteractionId('RatioExpressionInput'); expect(component.hasCustomizationArgs).toBe(false); @@ -280,9 +304,9 @@ describe('Customize Interaction Modal Component', () => { it('should close modal when user click close', fakeAsync(() => { spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ - result: Promise.resolve() - } as NgbModalRef); + return { + result: Promise.resolve(), + } as NgbModalRef; }); spyOn(ngbActiveModal, 'dismiss'); @@ -294,9 +318,9 @@ describe('Customize Interaction Modal Component', () => { it('should stay in modal if user click cancel', fakeAsync(() => { spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ - result: Promise.reject() - } as NgbModalRef); + return { + result: Promise.reject(), + } as NgbModalRef; }); spyOn(ngbActiveModal, 'dismiss'); @@ -317,18 +341,18 @@ describe('Customize Interaction Modal Component', () => { }); it('should open save intreaction when user click on it', () => { - spyOn(interactionDetailsCacheService, 'contains').and - .returnValue(false); + spyOn(interactionDetailsCacheService, 'contains').and.returnValue(false); spyOn( - interactionObjectFactory, 'convertFromCustomizationArgsBackendDict').and - .returnValue(false); + interactionObjectFactory, + 'convertFromCustomizationArgsBackendDict' + ).and.returnValue(false); component.originalContentIdToContent = subtitledUnicodeObjectFactory.createDefault('unicode', 'contentId'); const mockCustomizeInteractionHeaderRef = new ElementRef( - document.createElement('h3')); - component.customizeInteractionHeader = - mockCustomizeInteractionHeaderRef; + document.createElement('h3') + ); + component.customizeInteractionHeader = mockCustomizeInteractionHeaderRef; component.onChangeInteractionId('RatioExpressionInput'); expect(component.hasCustomizationArgs).toBeFalse(); @@ -336,116 +360,140 @@ describe('Customize Interaction Modal Component', () => { }); it('should show proper warning message on popover', fakeAsync(() => { - spyOn(ratioExpressionInputValidationService, 'getCustomizationArgsWarnings') - .and.returnValue( - [{ - type: 'string', - message: 'warning 1' - }, - { - type: 'string', - message: 'warning 2' - }] - ); + spyOn( + ratioExpressionInputValidationService, + 'getCustomizationArgsWarnings' + ).and.returnValue([ + { + type: 'string', + message: 'warning 1', + }, + { + type: 'string', + message: 'warning 2', + }, + ]); component.hasCustomizationArgs = false; tick(); expect(component.getSaveInteractionButtonTooltip()).toBe( - 'No customization arguments'); + 'No customization arguments' + ); component.hasCustomizationArgs = true; stateInteractionIdService.displayed = undefined; tick(); expect(component.getSaveInteractionButtonTooltip()).toBe( - 'No interaction being displayed'); + 'No interaction being displayed' + ); component.hasCustomizationArgs = true; stateInteractionIdService.displayed = 'RatioExpressionInput'; tick(); expect(component.getSaveInteractionButtonTooltip()).toBe( - 'warning 1 warning 2'); + 'warning 1 warning 2' + ); })); - it('should show proper popover if warningMessages array is empty', - fakeAsync(() => { - spyOn( - ratioExpressionInputValidationService, 'getCustomizationArgsWarnings') - .and.returnValue([]); - - component.hasCustomizationArgs = true; - stateInteractionIdService.displayed = 'RatioExpressionInput'; - tick(); - - expect(component.getSaveInteractionButtonTooltip()).toBe( - 'Some of the form entries are invalid.'); - })); + it('should show proper popover if warningMessages array is empty', fakeAsync(() => { + spyOn( + ratioExpressionInputValidationService, + 'getCustomizationArgsWarnings' + ).and.returnValue([]); - it('should properly open modal if editor is in' + - ' question mode and have intreaction', fakeAsync(() => { + component.hasCustomizationArgs = true; stateInteractionIdService.displayed = 'RatioExpressionInput'; - stateInteractionIdService.savedMemento = 'RatioExpressionInput'; - - component.ngOnInit(); tick(); - expect(component.allowedInteractionCategories).toEqual( - Array.prototype.concat.apply( - [], AppConstants.ALLOWED_QUESTION_INTERACTION_CATEGORIES)); - expect(component.customizationModalReopened).toBeTrue(); + expect(component.getSaveInteractionButtonTooltip()).toBe( + 'Some of the form entries are invalid.' + ); })); - it('should properly open modal if editor is not in' + - ' question mode and linked to story', fakeAsync(() => { - spyOn(stateEditorService, 'isInQuestionMode') - .and.returnValue(false); - spyOn(contextService, 'isExplorationLinkedToStory').and.returnValue(true); - jasmine.createSpy( - 'stateCustomizationArgsService.savedMemento.hasOwnProperty') - .and.returnValue(false); - - stateInteractionIdService.displayed = 'RatioExpressionInput'; - stateInteractionIdService.savedMemento = 'RatioExpressionInput'; + it( + 'should properly open modal if editor is in' + + ' question mode and have intreaction', + fakeAsync(() => { + stateInteractionIdService.displayed = 'RatioExpressionInput'; + stateInteractionIdService.savedMemento = 'RatioExpressionInput'; - component.ngOnInit(); - tick(); + component.ngOnInit(); + tick(); - expect(component.allowedInteractionCategories).toEqual( - Array.prototype.concat.apply( - [], AppConstants.ALLOWED_EXPLORATION_IN_STORY_INTERACTION_CATEGORIES)); - expect(component.customizationModalReopened).toBeTrue(); - })); + expect(component.allowedInteractionCategories).toEqual( + Array.prototype.concat.apply( + [], + AppConstants.ALLOWED_QUESTION_INTERACTION_CATEGORIES + ) + ); + expect(component.customizationModalReopened).toBeTrue(); + }) + ); - it('should properly open modal if editor is not in' + - ' question mode and not linked to story', fakeAsync(() => { - spyOn(stateEditorService, 'isInQuestionMode') - .and.returnValue(false); - spyOn(contextService, 'isExplorationLinkedToStory').and.returnValue(false); + it( + 'should properly open modal if editor is not in' + + ' question mode and linked to story', + fakeAsync(() => { + spyOn(stateEditorService, 'isInQuestionMode').and.returnValue(false); + spyOn(contextService, 'isExplorationLinkedToStory').and.returnValue(true); + jasmine + .createSpy('stateCustomizationArgsService.savedMemento.hasOwnProperty') + .and.returnValue(false); - stateInteractionIdService.displayed = ''; - stateInteractionIdService.savedMemento = ''; + stateInteractionIdService.displayed = 'RatioExpressionInput'; + stateInteractionIdService.savedMemento = 'RatioExpressionInput'; - component.ngOnInit(); - tick(); + component.ngOnInit(); + tick(); - expect(component.isinteractionOpen).toBeTrue(); - expect(component.allowedInteractionCategories).toEqual( - Array.prototype.concat.apply( - [], AppConstants.ALLOWED_INTERACTION_CATEGORIES)); - })); + expect(component.allowedInteractionCategories).toEqual( + Array.prototype.concat.apply( + [], + AppConstants.ALLOWED_EXPLORATION_IN_STORY_INTERACTION_CATEGORIES + ) + ); + expect(component.customizationModalReopened).toBeTrue(); + }) + ); - it('should get proper contentId of DragAndDropSortInput intreaction', + it( + 'should properly open modal if editor is not in' + + ' question mode and not linked to story', fakeAsync(() => { - jasmine.createSpy( - 'stateCustomizationArgsService.displayed.hasOwnProperty') - .and.returnValue(true); + spyOn(stateEditorService, 'isInQuestionMode').and.returnValue(false); + spyOn(contextService, 'isExplorationLinkedToStory').and.returnValue( + false + ); - stateInteractionIdService.displayed = 'DragAndDropSortInput'; - stateCustomizationArgsService.displayed = { - choices: { - value: [{ + stateInteractionIdService.displayed = ''; + stateInteractionIdService.savedMemento = ''; + + component.ngOnInit(); + tick(); + + expect(component.isinteractionOpen).toBeTrue(); + expect(component.allowedInteractionCategories).toEqual( + Array.prototype.concat.apply( + [], + AppConstants.ALLOWED_INTERACTION_CATEGORIES + ) + ); + }) + ); + + it('should get proper contentId of DragAndDropSortInput intreaction', fakeAsync(() => { + jasmine + .createSpy('stateCustomizationArgsService.displayed.hasOwnProperty') + .and.returnValue(true); + + stateInteractionIdService.displayed = 'DragAndDropSortInput'; + stateCustomizationArgsService.displayed = { + choices: { + value: [ + { _html: 'html', _contentId: 'contentId', isEmpty(): boolean { @@ -462,64 +510,71 @@ describe('Customize Interaction Modal Component', () => { }, set html(html: string) { this._html = html; - } - }] - }, - allowMultipleItemsInSamePosition: { - value: false - } - }; - - expect(component.getContentIdToContent()).toEqual({contentId: 'html'}); - })); - - it('should save and populate null for ContentIds' + - ' for DragAndDropSortInput intreaction', fakeAsync(() => { - spyOn(component, 'getContentIdToContent').and.returnValue( - subtitledUnicodeObjectFactory.createDefault('unicode', 'contentId 1') - ); - - stateInteractionIdService.displayed = 'DragAndDropSortInput'; - component.originalContentIdToContent = subtitledUnicodeObjectFactory - .createDefault('unicode', 'contentId 2'); - stateCustomizationArgsService.displayed = { - choices: { - value: [{ - _html: 'html', - _contentId: null, - isEmpty(): boolean { - return !this._html; - }, - get contentId(): string | null { - return this._contentId; - }, - set contentId(contentId: string | null) { - this._contentId = contentId; - }, - get html(): string { - return this._html; + }, }, - set html(html: string) { - this._html = html; - } - }] + ], }, allowMultipleItemsInSamePosition: { - value: false - } + value: false, + }, }; - component.save(); + expect(component.getContentIdToContent()).toEqual({contentId: 'html'}); })); + it( + 'should save and populate null for ContentIds' + + ' for DragAndDropSortInput intreaction', + fakeAsync(() => { + spyOn(component, 'getContentIdToContent').and.returnValue( + subtitledUnicodeObjectFactory.createDefault('unicode', 'contentId 1') + ); + + stateInteractionIdService.displayed = 'DragAndDropSortInput'; + component.originalContentIdToContent = + subtitledUnicodeObjectFactory.createDefault('unicode', 'contentId 2'); + stateCustomizationArgsService.displayed = { + choices: { + value: [ + { + _html: 'html', + _contentId: null, + isEmpty(): boolean { + return !this._html; + }, + get contentId(): string | null { + return this._contentId; + }, + set contentId(contentId: string | null) { + this._contentId = contentId; + }, + get html(): string { + return this._html; + }, + set html(html: string) { + this._html = html; + }, + }, + ], + }, + allowMultipleItemsInSamePosition: { + value: false, + }, + }; + + component.save(); + }) + ); + it('should show error when a saved customization arg is missing', () => { stateInteractionIdService.displayed = 'RatioExpressionInput'; stateInteractionIdService.savedMemento = 'RatioExpressionInput'; stateCustomizationArgsService.savedMemento = {}; - expect(()=>{ + expect(() => { component.ngOnInit(); }).toThrowError( - 'Interaction is missing customization argument placeholder'); + 'Interaction is missing customization argument placeholder' + ); }); }); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/customize-interaction-modal.component.ts b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/customize-interaction-modal.component.ts index cff79b09a21a..08aff8496f5e 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/customize-interaction-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/customize-interaction-modal.component.ts @@ -16,62 +16,69 @@ * @fileoverview Component for the Customize Interaction Modal Component. */ -import { AfterContentChecked, ChangeDetectorRef, Component, Injector, OnInit, ElementRef, ViewChild } from '@angular/core'; -import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { SchemaConstants } from 'components/forms/schema-based-editors/schema.constants'; -import { StateCustomizationArgsService } from 'components/state-editor/state-editor-properties-services/state-customization-args.service'; -import { StateInteractionIdService } from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { SubtitledUnicode } from 'domain/exploration/SubtitledUnicodeObjectFactory'; -import { EditorFirstTimeEventsService } from 'pages/exploration-editor-page/services/editor-first-time-events.service'; -import { ContextService } from 'services/context.service'; -import { Schema } from 'services/schema-default-value.service'; +import { + AfterContentChecked, + ChangeDetectorRef, + Component, + Injector, + OnInit, + ElementRef, + ViewChild, +} from '@angular/core'; +import {NgbActiveModal, NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {SchemaConstants} from 'components/forms/schema-based-editors/schema.constants'; +import {StateCustomizationArgsService} from 'components/state-editor/state-editor-properties-services/state-customization-args.service'; +import {StateInteractionIdService} from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {SubtitledUnicode} from 'domain/exploration/SubtitledUnicodeObjectFactory'; +import {EditorFirstTimeEventsService} from 'pages/exploration-editor-page/services/editor-first-time-events.service'; +import {ContextService} from 'services/context.service'; +import {Schema} from 'services/schema-default-value.service'; import INTERACTION_SPECS from 'interactions/interaction_specs.json'; -import { ConfirmLeaveModalComponent } from 'pages/exploration-editor-page/modal-templates/confirm-leave-modal.component'; -import { InteractionDetailsCacheService } from '../../services/interaction-details-cache.service'; -import { InteractionObjectFactory } from 'domain/exploration/InteractionObjectFactory'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { AppConstants } from 'app.constants'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { ContinueValidationService } from 'interactions/Continue/directives/continue-validation.service'; -import { EndExplorationValidationService } from 'interactions/EndExploration/directives/end-exploration-validation.service'; -import { AlgebraicExpressionInputValidationService } from 'interactions/AlgebraicExpressionInput/directives/algebraic-expression-input-validation.service'; -import { ImageClickInputValidationService } from 'interactions/ImageClickInput/directives/image-click-input-validation.service'; -import { ItemSelectionInputValidationService } from 'interactions/ItemSelectionInput/directives/item-selection-input-validation.service'; -import { NumberWithUnitsValidationService } from 'interactions/NumberWithUnits/directives/number-with-units-validation.service'; -import { NumericExpressionInputValidationService } from 'interactions/NumericExpressionInput/directives/numeric-expression-input-validation.service'; -import { NumericInputValidationService } from 'interactions/NumericInput/directives/numeric-input-validation.service'; -import { DragAndDropSortInputValidationService } from 'interactions/DragAndDropSortInput/directives/drag-and-drop-sort-input-validation.service'; -import { GraphInputValidationService } from 'interactions/GraphInput/directives/graph-input-validation.service'; -import { SetInputValidationService } from 'interactions/SetInput/directives/set-input-validation.service'; -import { CodeReplValidationService } from 'interactions/CodeRepl/directives/code-repl-validation.service'; -import { MathEquationInputValidationService } from 'interactions/MathEquationInput/directives/math-equation-input-validation.service'; -import { MultipleChoiceInputValidationService } from 'interactions/MultipleChoiceInput/directives/multiple-choice-input-validation.service'; -import { PencilCodeEditorValidationService } from 'interactions/PencilCodeEditor/directives/pencil-code-editor-validation.service'; -import { TextInputValidationService } from 'interactions/TextInput/directives/text-input-validation.service'; -import { InteractiveMapValidationService } from 'interactions/InteractiveMap/directives/interactive-map-validation.service'; -import { MusicNotesInputValidationService } from 'interactions/MusicNotesInput/directives/music-notes-input-validation.service'; -import { FractionInputValidationService } from 'interactions/FractionInput/directives/fraction-input-validation.service'; -import { RatioExpressionInputValidationService } from 'interactions/RatioExpressionInput/directives/ratio-expression-input-validation.service'; -import { Warning } from 'interactions/base-interaction-validation.service'; +import {ConfirmLeaveModalComponent} from 'pages/exploration-editor-page/modal-templates/confirm-leave-modal.component'; +import {InteractionDetailsCacheService} from '../../services/interaction-details-cache.service'; +import {InteractionObjectFactory} from 'domain/exploration/InteractionObjectFactory'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {AppConstants} from 'app.constants'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {ContinueValidationService} from 'interactions/Continue/directives/continue-validation.service'; +import {EndExplorationValidationService} from 'interactions/EndExploration/directives/end-exploration-validation.service'; +import {AlgebraicExpressionInputValidationService} from 'interactions/AlgebraicExpressionInput/directives/algebraic-expression-input-validation.service'; +import {ImageClickInputValidationService} from 'interactions/ImageClickInput/directives/image-click-input-validation.service'; +import {ItemSelectionInputValidationService} from 'interactions/ItemSelectionInput/directives/item-selection-input-validation.service'; +import {NumberWithUnitsValidationService} from 'interactions/NumberWithUnits/directives/number-with-units-validation.service'; +import {NumericExpressionInputValidationService} from 'interactions/NumericExpressionInput/directives/numeric-expression-input-validation.service'; +import {NumericInputValidationService} from 'interactions/NumericInput/directives/numeric-input-validation.service'; +import {DragAndDropSortInputValidationService} from 'interactions/DragAndDropSortInput/directives/drag-and-drop-sort-input-validation.service'; +import {GraphInputValidationService} from 'interactions/GraphInput/directives/graph-input-validation.service'; +import {SetInputValidationService} from 'interactions/SetInput/directives/set-input-validation.service'; +import {CodeReplValidationService} from 'interactions/CodeRepl/directives/code-repl-validation.service'; +import {MathEquationInputValidationService} from 'interactions/MathEquationInput/directives/math-equation-input-validation.service'; +import {MultipleChoiceInputValidationService} from 'interactions/MultipleChoiceInput/directives/multiple-choice-input-validation.service'; +import {PencilCodeEditorValidationService} from 'interactions/PencilCodeEditor/directives/pencil-code-editor-validation.service'; +import {TextInputValidationService} from 'interactions/TextInput/directives/text-input-validation.service'; +import {InteractiveMapValidationService} from 'interactions/InteractiveMap/directives/interactive-map-validation.service'; +import {MusicNotesInputValidationService} from 'interactions/MusicNotesInput/directives/music-notes-input-validation.service'; +import {FractionInputValidationService} from 'interactions/FractionInput/directives/fraction-input-validation.service'; +import {RatioExpressionInputValidationService} from 'interactions/RatioExpressionInput/directives/ratio-expression-input-validation.service'; +import {Warning} from 'interactions/base-interaction-validation.service'; import cloneDeep from 'lodash/cloneDeep'; -import { ImageWithRegions } from 'interactions/customization-args-defs'; -import { GenerateContentIdService } from 'services/generate-content-id.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; - -type DefaultCustomizationArg = ( - DefaultValueHtml[] | - DefaultValueHtml | - DefaultValueUnicode[] | - DefaultValueUnicode | - DefaultValueGraph | - ImageWithRegions | - [] | - number | - string | - boolean -); +import {ImageWithRegions} from 'interactions/customization-args-defs'; +import {GenerateContentIdService} from 'services/generate-content-id.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; + +type DefaultCustomizationArg = + | DefaultValueHtml[] + | DefaultValueHtml + | DefaultValueUnicode[] + | DefaultValueUnicode + | DefaultValueGraph + | ImageWithRegions + | [] + | number + | string + | boolean; interface DefaultValueHtml { content_id: string; @@ -140,10 +147,12 @@ const INTERACTION_SERVICE_MAPPING = { @Component({ selector: 'oppia-customize-interaction', - templateUrl: './customize-interaction-modal.component.html' + templateUrl: './customize-interaction-modal.component.html', }) export class CustomizeInteractionModalComponent - extends ConfirmOrCancelModal implements OnInit, AfterContentChecked { + extends ConfirmOrCancelModal + implements OnInit, AfterContentChecked +{ customizationArgSpecs: CustomizationArgSpecsInterface[]; originalContentIdToContent: object; hasCustomizationArgs: boolean; @@ -153,7 +162,7 @@ export class CustomizeInteractionModalComponent isinteractionOpen: boolean; @ViewChild('customizeInteractionHeader') - customizeInteractionHeader!: ElementRef; + customizeInteractionHeader!: ElementRef; constructor( private changeDetectorRef: ChangeDetectorRef, @@ -169,7 +178,7 @@ export class CustomizeInteractionModalComponent private stateEditorService: StateEditorService, public stateInteractionIdService: StateInteractionIdService, private generateContentIdService: GenerateContentIdService, - private urlInterpolationService: UrlInterpolationService, + private urlInterpolationService: UrlInterpolationService ) { super(ngbActiveModal); } @@ -189,21 +198,21 @@ export class CustomizeInteractionModalComponent } getCustomizationArgsWarningsList(): Warning[] { - const validationServiceName: string = ( + const validationServiceName: string = INTERACTION_SPECS[this.stateInteractionIdService.displayed].id + - 'ValidationService'); + 'ValidationService'; - let validationService = - this.injector.get( - INTERACTION_SERVICE_MAPPING[validationServiceName]); + let validationService = this.injector.get( + INTERACTION_SERVICE_MAPPING[validationServiceName] + ); let warningsList = validationService.getCustomizationArgsWarnings( - this.stateCustomizationArgsService.displayed); + this.stateCustomizationArgsService.displayed + ); return warningsList; } getCustomizationArgsWarningMessage(): string { - let warningsList = ( - this.getCustomizationArgsWarningsList()); + let warningsList = this.getCustomizationArgsWarningsList(); let warningMessage = ''; if (warningsList.length !== 0) { warningMessage = warningsList[0].message; @@ -213,42 +222,38 @@ export class CustomizeInteractionModalComponent onChangeInteractionId(newInteractionId: string): void { this.isinteractionOpen = false; - this.editorFirstTimeEventsService - .registerFirstSelectInteractionTypeEvent(); + this.editorFirstTimeEventsService.registerFirstSelectInteractionTypeEvent(); let interactionSpec = INTERACTION_SPECS[newInteractionId]; - this.customizationArgSpecs = ( - interactionSpec.customization_arg_specs); + this.customizationArgSpecs = interactionSpec.customization_arg_specs; this.stateInteractionIdService.displayed = newInteractionId; this.stateCustomizationArgsService.displayed = {}; - if ( - this.interactionDetailsCacheService.contains( - newInteractionId)) { - this.stateCustomizationArgsService.displayed = ( - this.interactionDetailsCacheService.get( - newInteractionId)); + if (this.interactionDetailsCacheService.contains(newInteractionId)) { + this.stateCustomizationArgsService.displayed = + this.interactionDetailsCacheService.get(newInteractionId); } else { const customizationArgsBackendDict = {}; - this.customizationArgSpecs.forEach(( - caSpec: { - name: string | number; - default_value: DefaultCustomizationArg; - }) => { - customizationArgsBackendDict[caSpec.name] = { - value: caSpec.default_value - }; - }); - - this.stateCustomizationArgsService.displayed = ( + this.customizationArgSpecs.forEach( + (caSpec: { + name: string | number; + default_value: DefaultCustomizationArg; + }) => { + customizationArgsBackendDict[caSpec.name] = { + value: caSpec.default_value, + }; + } + ); + + this.stateCustomizationArgsService.displayed = this.interactionObjectFactory.convertFromCustomizationArgsBackendDict( newInteractionId, customizationArgsBackendDict - ) - ); + ); } - if (Object.keys( - this.stateCustomizationArgsService.displayed).length === 0) { + if ( + Object.keys(this.stateCustomizationArgsService.displayed).length === 0 + ) { this.save(); this.hasCustomizationArgs = false; } else { @@ -269,7 +274,8 @@ export class CustomizeInteractionModalComponent return !!( this.hasCustomizationArgs && this.stateInteractionIdService.displayed && - (this.getCustomizationArgsWarningsList().length === 0)); + this.getCustomizationArgsWarningsList().length === 0 + ); } getSaveInteractionButtonTooltip(): string { @@ -280,9 +286,8 @@ export class CustomizeInteractionModalComponent return 'No interaction being displayed'; } - let warningsList = - this.getCustomizationArgsWarningsList(); - let warningMessages = warningsList.map((warning) => { + let warningsList = this.getCustomizationArgsWarningsList(); + let warningMessages = warningsList.map(warning => { return warning.message; }); @@ -294,55 +299,56 @@ export class CustomizeInteractionModalComponent } cancelWithConfirm(): void { - this.ngbModal.open(ConfirmLeaveModalComponent, { - backdrop: 'static', - keyboard: false, - }).result.then(() => { - this.ngbActiveModal.dismiss(); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + this.ngbModal + .open(ConfirmLeaveModalComponent, { + backdrop: 'static', + keyboard: false, + }) + .result.then( + () => { + this.ngbActiveModal.dismiss(); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } /** - * The default values of SubtitledHtml and SubtitledUnicode objects in the - * customization arguments have a null content_id. This function populates - * these null content_id's with a content_id generated from traversing the - * schema and with next content id index, ensuring a unique content_id. - */ + * The default values of SubtitledHtml and SubtitledUnicode objects in the + * customization arguments have a null content_id. This function populates + * these null content_id's with a content_id generated from traversing the + * schema and with next content id index, ensuring a unique content_id. + */ populateNullContentIds(): void { const interactionId = this.stateInteractionIdService.displayed; let traverseSchemaAndAssignContentIds = ( - value: Object | Object[], - schema: Schema, - contentIdPrefix: string, + value: Object | Object[], + schema: Schema, + contentIdPrefix: string ): void => { - const schemaIsSubtitledHtml = ( + const schemaIsSubtitledHtml = schema.type === SchemaConstants.SCHEMA_TYPE_CUSTOM && - schema.obj_type === SchemaConstants.SCHEMA_OBJ_TYPE_SUBTITLED_HTML); - const schemaIsSubtitledUnicode = ( + schema.obj_type === SchemaConstants.SCHEMA_OBJ_TYPE_SUBTITLED_HTML; + const schemaIsSubtitledUnicode = schema.type === SchemaConstants.SCHEMA_TYPE_CUSTOM && - schema.obj_type === SchemaConstants.SCHEMA_OBJ_TYPE_SUBTITLED_UNICODE - ); + schema.obj_type === SchemaConstants.SCHEMA_OBJ_TYPE_SUBTITLED_UNICODE; if (schemaIsSubtitledHtml || schemaIsSubtitledUnicode) { - if ((value as SubtitledHtml|SubtitledUnicode).contentId === null) { - (value as SubtitledHtml|SubtitledUnicode).contentId = ( - this.generateContentIdService.getNextStateId(contentIdPrefix)); + if ((value as SubtitledHtml | SubtitledUnicode).contentId === null) { + (value as SubtitledHtml | SubtitledUnicode).contentId = + this.generateContentIdService.getNextStateId(contentIdPrefix); } } else if (schema.type === SchemaConstants.SCHEMA_KEY_LIST) { - for ( - let i = 0; - i < (value as Object[]).length; - i++ - ) { + for (let i = 0; i < (value as Object[]).length; i++) { traverseSchemaAndAssignContentIds( value[i], schema.items as Schema, - `${contentIdPrefix}`); + `${contentIdPrefix}` + ); } } }; @@ -355,44 +361,41 @@ export class CustomizeInteractionModalComponent traverseSchemaAndAssignContentIds( caValues[name].value, caSpec.schema, - `${AppConstants.COMPONENT_NAME_INTERACTION_CUSTOMIZATION_ARGS}_${ - name}`); + `${AppConstants.COMPONENT_NAME_INTERACTION_CUSTOMIZATION_ARGS}_${name}` + ); } } } /** - * Extracts a mapping of content ids to the html or unicode content found - * in the customization arguments. - * @returns {Object} A Mapping of content ids (string) to content (string). - */ + * Extracts a mapping of content ids to the html or unicode content found + * in the customization arguments. + * @returns {Object} A Mapping of content ids (string) to content (string). + */ getContentIdToContent(): object { const interactionId = this.stateInteractionIdService.displayed; const contentIdToContent = {}; let traverseSchemaAndCollectContent = ( - value: Object | Object[], - schema: Schema + value: Object | Object[], + schema: Schema ): void => { - const schemaIsSubtitledHtml = ( + const schemaIsSubtitledHtml = schema.type === SchemaConstants.SCHEMA_TYPE_CUSTOM && - schema.obj_type === SchemaConstants.SCHEMA_OBJ_TYPE_SUBTITLED_HTML); - const schemaIsSubtitledUnicode = ( + schema.obj_type === SchemaConstants.SCHEMA_OBJ_TYPE_SUBTITLED_HTML; + const schemaIsSubtitledUnicode = schema.type === SchemaConstants.SCHEMA_TYPE_CUSTOM && - schema.obj_type === SchemaConstants.SCHEMA_OBJ_TYPE_SUBTITLED_UNICODE - ); + schema.obj_type === SchemaConstants.SCHEMA_OBJ_TYPE_SUBTITLED_UNICODE; if (schemaIsSubtitledHtml) { const subtitledHtmlValue = value as SubtitledHtml; - contentIdToContent[ - subtitledHtmlValue.contentId - ] = subtitledHtmlValue.html; + contentIdToContent[subtitledHtmlValue.contentId] = + subtitledHtmlValue.html; } else if (schemaIsSubtitledUnicode) { const subtitledUnicodeValue = value as SubtitledUnicode; - contentIdToContent[ - subtitledUnicodeValue.contentId - ] = subtitledUnicodeValue.unicode; + contentIdToContent[subtitledUnicodeValue.contentId] = + subtitledUnicodeValue.unicode; } else if (schema.type === SchemaConstants.SCHEMA_KEY_LIST) { for (let i = 0; i < (value as Object[]).length; i++) { traverseSchemaAndCollectContent(value[i], schema.items as Schema); @@ -424,7 +427,8 @@ export class CustomizeInteractionModalComponent getInteractionThumbnailImageUrl(interactionId: string): string { return this.urlInterpolationService.getInteractionThumbnailImageUrl( - interactionId); + interactionId + ); } ngAfterContentChecked(): void { @@ -446,61 +450,76 @@ export class CustomizeInteractionModalComponent this.originalContentIdToContent = this.getContentIdToContent(); // Above called with this.stateCustomizationArgsService.displayed. } - this.explorationIsLinkedToStory = ( - this.contextService.isExplorationLinkedToStory()); + this.explorationIsLinkedToStory = + this.contextService.isExplorationLinkedToStory(); this.editorFirstTimeEventsService.registerFirstClickAddInteractionEvent(); if (this.stateEditorService.isInQuestionMode()) { this.allowedInteractionCategories = Array.prototype.concat.apply( - [], AppConstants.ALLOWED_QUESTION_INTERACTION_CATEGORIES); + [], + AppConstants.ALLOWED_QUESTION_INTERACTION_CATEGORIES + ); } else if (this.contextService.isExplorationLinkedToStory()) { this.allowedInteractionCategories = Array.prototype.concat.apply( - [], AppConstants.ALLOWED_EXPLORATION_IN_STORY_INTERACTION_CATEGORIES); + [], + AppConstants.ALLOWED_EXPLORATION_IN_STORY_INTERACTION_CATEGORIES + ); } else { this.allowedInteractionCategories = Array.prototype.concat.apply( - [], AppConstants.ALLOWED_INTERACTION_CATEGORIES); + [], + AppConstants.ALLOWED_INTERACTION_CATEGORIES + ); } if (this.stateEditorService.isInQuestionMode()) { this.allowedInteractionCategories = Array.prototype.concat.apply( - [], AppConstants.ALLOWED_QUESTION_INTERACTION_CATEGORIES); + [], + AppConstants.ALLOWED_QUESTION_INTERACTION_CATEGORIES + ); } else if (this.contextService.isExplorationLinkedToStory()) { this.allowedInteractionCategories = Array.prototype.concat.apply( - [], AppConstants.ALLOWED_EXPLORATION_IN_STORY_INTERACTION_CATEGORIES); + [], + AppConstants.ALLOWED_EXPLORATION_IN_STORY_INTERACTION_CATEGORIES + ); } else { this.allowedInteractionCategories = Array.prototype.concat.apply( - [], AppConstants.ALLOWED_INTERACTION_CATEGORIES); + [], + AppConstants.ALLOWED_INTERACTION_CATEGORIES + ); } if (this.stateInteractionIdService.savedMemento) { this.customizationModalReopened = true; - let interactionSpec = INTERACTION_SPECS[ - this.stateInteractionIdService.savedMemento]; + let interactionSpec = + INTERACTION_SPECS[this.stateInteractionIdService.savedMemento]; this.customizationArgSpecs = interactionSpec.customization_arg_specs; this.stateInteractionIdService.displayed = cloneDeep( - this.stateInteractionIdService.savedMemento); + this.stateInteractionIdService.savedMemento + ); this.stateCustomizationArgsService.displayed = cloneDeep( - this.stateCustomizationArgsService.savedMemento); + this.stateCustomizationArgsService.savedMemento + ); // Ensure that StateCustomizationArgsService.displayed is // fully populated. for (let i = 0; i < this.customizationArgSpecs.length; i++) { let argName = this.customizationArgSpecs[i].name; if ( - !this.stateCustomizationArgsService - .savedMemento.hasOwnProperty(argName) + !this.stateCustomizationArgsService.savedMemento.hasOwnProperty( + argName + ) ) { throw new Error( - `Interaction is missing customization argument ${argName}`); + `Interaction is missing customization argument ${argName}` + ); } } this.stateCustomizationArgsService.onSchemaBasedFormsShown.emit(); - this.hasCustomizationArgs = ( + this.hasCustomizationArgs = this.stateCustomizationArgsService.displayed && - Object.keys(this.stateCustomizationArgsService.displayed).length > 0 - ); + Object.keys(this.stateCustomizationArgsService.displayed).length > 0; } } } diff --git a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-answer-group-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-answer-group-modal.component.spec.ts index 33c0aa8071c0..0c610d4e7f9a 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-answer-group-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-answer-group-modal.component.spec.ts @@ -16,9 +16,9 @@ * @fileoverview Unit tests for Delete Answer Group Modal. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { DeleteAnswerGroupModalComponent } from './delete-answer-group-modal.component'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {DeleteAnswerGroupModalComponent} from './delete-answer-group-modal.component'; describe('Delete Answer Group Modal Component', () => { let fixture: ComponentFixture; @@ -26,12 +26,8 @@ describe('Delete Answer Group Modal Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - DeleteAnswerGroupModalComponent - ], - providers: [ - NgbActiveModal - ] + declarations: [DeleteAnswerGroupModalComponent], + providers: [NgbActiveModal], }).compileComponents(); })); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-answer-group-modal.component.ts b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-answer-group-modal.component.ts index cbd862f9620a..4e6ff5872531 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-answer-group-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-answer-group-modal.component.ts @@ -16,16 +16,15 @@ * @fileoverview Component for Delete Answer Group Modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'delete-answer-group-modal', - templateUrl: './delete-answer-group-modal.component.html' + templateUrl: './delete-answer-group-modal.component.html', }) -export class DeleteAnswerGroupModalComponent - extends ConfirmOrCancelModal { +export class DeleteAnswerGroupModalComponent extends ConfirmOrCancelModal { constructor(ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } diff --git a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-hint-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-hint-modal.component.spec.ts index e88fc5695d48..251ec4f475c2 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-hint-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-hint-modal.component.spec.ts @@ -16,9 +16,9 @@ * @fileoverview Unit tests for Delete Hint Modal. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { DeleteHintModalComponent } from './delete-hint-modal.component'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {DeleteHintModalComponent} from './delete-hint-modal.component'; describe('Delete Hint Modal Component', () => { let fixture: ComponentFixture; @@ -26,12 +26,8 @@ describe('Delete Hint Modal Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - DeleteHintModalComponent - ], - providers: [ - NgbActiveModal - ] + declarations: [DeleteHintModalComponent], + providers: [NgbActiveModal], }).compileComponents(); })); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-hint-modal.component.ts b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-hint-modal.component.ts index 9b92ef8258b9..2b043cae82ac 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-hint-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-hint-modal.component.ts @@ -16,16 +16,15 @@ * @fileoverview Component for Delete Hint Modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'delete-hint-modal', - templateUrl: './delete-hint-modal.component.html' + templateUrl: './delete-hint-modal.component.html', }) -export class DeleteHintModalComponent - extends ConfirmOrCancelModal { +export class DeleteHintModalComponent extends ConfirmOrCancelModal { constructor(ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } diff --git a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-interaction-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-interaction-modal.component.spec.ts index a0540b7bd528..8f634d2dd396 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-interaction-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-interaction-modal.component.spec.ts @@ -16,9 +16,9 @@ * @fileoverview Unit tests for Delete Interaction Modal. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { DeleteInteractionModalComponent } from './delete-interaction-modal.component'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {DeleteInteractionModalComponent} from './delete-interaction-modal.component'; describe('Delete Hint Modal Component', () => { let fixture: ComponentFixture; @@ -26,12 +26,8 @@ describe('Delete Hint Modal Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - DeleteInteractionModalComponent - ], - providers: [ - NgbActiveModal - ] + declarations: [DeleteInteractionModalComponent], + providers: [NgbActiveModal], }).compileComponents(); })); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-interaction-modal.component.ts b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-interaction-modal.component.ts index a0a63f35ffda..e010b545e956 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-interaction-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-interaction-modal.component.ts @@ -16,16 +16,15 @@ * @fileoverview Component for Delete Interaction Modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'delete-interaction-modal', - templateUrl: './delete-interaction-modal.component.html' + templateUrl: './delete-interaction-modal.component.html', }) -export class DeleteInteractionModalComponent - extends ConfirmOrCancelModal { +export class DeleteInteractionModalComponent extends ConfirmOrCancelModal { constructor(ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } diff --git a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-last-hint-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-last-hint-modal.component.spec.ts index 4b33cc953a16..4e1e5ad62075 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-last-hint-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-last-hint-modal.component.spec.ts @@ -16,9 +16,9 @@ * @fileoverview Unit tests for Delete Last Hint Modal. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { DeleteLastHintModalComponent } from './delete-last-hint-modal.component'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {DeleteLastHintModalComponent} from './delete-last-hint-modal.component'; describe('Delete Hint Modal Component', () => { let fixture: ComponentFixture; @@ -26,12 +26,8 @@ describe('Delete Hint Modal Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - DeleteLastHintModalComponent - ], - providers: [ - NgbActiveModal - ] + declarations: [DeleteLastHintModalComponent], + providers: [NgbActiveModal], }).compileComponents(); })); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-last-hint-modal.component.ts b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-last-hint-modal.component.ts index ce5deb9b601d..154dd868fc4f 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-last-hint-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-last-hint-modal.component.ts @@ -16,16 +16,15 @@ * @fileoverview Component for Delete Last Hint Modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'delete-last-hint-modal', - templateUrl: './delete-last-hint-modal.component.html' + templateUrl: './delete-last-hint-modal.component.html', }) -export class DeleteLastHintModalComponent - extends ConfirmOrCancelModal { +export class DeleteLastHintModalComponent extends ConfirmOrCancelModal { constructor(ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } diff --git a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-solution-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-solution-modal.component.spec.ts index 89bd4eab67cc..0df9bbd8e286 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-solution-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-solution-modal.component.spec.ts @@ -16,9 +16,9 @@ * @fileoverview Unit tests for Delete Solution Modal. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { DeleteSolutionModalComponent } from './delete-solution-modal.component'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {DeleteSolutionModalComponent} from './delete-solution-modal.component'; describe('Delete Hint Modal Component', () => { let fixture: ComponentFixture; @@ -26,12 +26,8 @@ describe('Delete Hint Modal Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - DeleteSolutionModalComponent - ], - providers: [ - NgbActiveModal - ] + declarations: [DeleteSolutionModalComponent], + providers: [NgbActiveModal], }).compileComponents(); })); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-solution-modal.component.ts b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-solution-modal.component.ts index 6e4307deb02e..0e1bceffffd4 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-solution-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-solution-modal.component.ts @@ -16,16 +16,15 @@ * @fileoverview Component for Delete Solution Modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'delete-solution-modal', - templateUrl: './delete-solution-modal.component.html' + templateUrl: './delete-solution-modal.component.html', }) -export class DeleteSolutionModalComponent - extends ConfirmOrCancelModal { +export class DeleteSolutionModalComponent extends ConfirmOrCancelModal { constructor(ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } diff --git a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-state-skill-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-state-skill-modal.component.spec.ts index 24a3fed859a9..52c8cfb0ca85 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-state-skill-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-state-skill-modal.component.spec.ts @@ -16,9 +16,9 @@ * @fileoverview Unit tests for Delete State Skill Modal. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { DeleteStateSkillModalComponent } from './delete-state-skill-modal.component'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {DeleteStateSkillModalComponent} from './delete-state-skill-modal.component'; describe('Delete Topic Modal Component', () => { let fixture: ComponentFixture; @@ -26,12 +26,8 @@ describe('Delete Topic Modal Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - DeleteStateSkillModalComponent - ], - providers: [ - NgbActiveModal - ] + declarations: [DeleteStateSkillModalComponent], + providers: [NgbActiveModal], }).compileComponents(); })); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-state-skill-modal.component.ts b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-state-skill-modal.component.ts index 353d7ed2c8ad..905cd08eacc0 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-state-skill-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/delete-state-skill-modal.component.ts @@ -16,16 +16,15 @@ * @fileoverview Component for Delete State Skill Modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'delete-state-skill-modal', - templateUrl: './delete-state-skill-modal.component.html' + templateUrl: './delete-state-skill-modal.component.html', }) -export class DeleteStateSkillModalComponent - extends ConfirmOrCancelModal { +export class DeleteStateSkillModalComponent extends ConfirmOrCancelModal { constructor(ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } diff --git a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/exploration-graph-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/exploration-graph-modal.component.spec.ts index 79974bc4ad5c..3ebbd8a9ddda 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/exploration-graph-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/exploration-graph-modal.component.spec.ts @@ -16,13 +16,13 @@ * @fileoverview Unit tests for ExplorationGraphModalComponent. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { ExplorationGraphModalComponent } from './exploration-graph-modal.component'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { GraphDataService } from 'pages/exploration-editor-page/services/graph-data.service'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {NgbActiveModal, NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {ExplorationGraphModalComponent} from './exploration-graph-modal.component'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {GraphDataService} from 'pages/exploration-editor-page/services/graph-data.service'; class MockActiveModal { close(): void { @@ -37,7 +37,7 @@ class MockActiveModal { class MockNgbModal { open() { return { - result: Promise.resolve() + result: Promise.resolve(), }; } } @@ -54,22 +54,20 @@ describe('Exploration Graph Modal Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - ExplorationGraphModalComponent - ], + declarations: [ExplorationGraphModalComponent], providers: [ GraphDataService, StateEditorService, { provide: NgbActiveModal, - useClass: MockActiveModal + useClass: MockActiveModal, }, { provide: NgbModal, - useClass: MockNgbModal - } + useClass: MockNgbModal, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -89,13 +87,11 @@ describe('Exploration Graph Modal Component', () => { component.ngOnInit(); }); - - it('should initialize component properties after Component is initialized', - () => { - expect(component.currentStateName).toBe(stateName); - expect(component.graphData).toBeUndefined(); - expect(component.isEditable).toBe(isEditable); - }); + it('should initialize component properties after Component is initialized', () => { + expect(component.currentStateName).toBe(stateName); + expect(component.graphData).toBeUndefined(); + expect(component.isEditable).toBe(isEditable); + }); it('should delete state when closing the modal', () => { let stateName = 'State Name'; diff --git a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/exploration-graph-modal.component.ts b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/exploration-graph-modal.component.ts index ea1aa59e21e8..a70f88deb935 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/exploration-graph-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/exploration-graph-modal.component.ts @@ -16,20 +16,22 @@ * @fileoverview Component for exploration graph modal. */ -import { Component, Input, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { GraphDataService } from 'pages/exploration-editor-page/services/graph-data.service'; -import { GraphData } from 'services/compute-graph.service'; +import {Component, Input, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {GraphDataService} from 'pages/exploration-editor-page/services/graph-data.service'; +import {GraphData} from 'services/compute-graph.service'; @Component({ selector: 'oppia-exploration-graph-modal', - templateUrl: './exploration-graph-modal.component.html' + templateUrl: './exploration-graph-modal.component.html', }) export class ExplorationGraphModalComponent - extends ConfirmOrCancelModal implements OnInit { + extends ConfirmOrCancelModal + implements OnInit +{ // These properties below are initialized using Angular lifecycle hooks // where we need to do non-null assertion. For more information see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -42,7 +44,7 @@ export class ExplorationGraphModalComponent constructor( private graphDataService: GraphDataService, private ngbActiveModal: NgbActiveModal, - private stateEditorService: StateEditorService, + private stateEditorService: StateEditorService ) { super(ngbActiveModal); } @@ -50,25 +52,26 @@ export class ExplorationGraphModalComponent deleteState(stateName: string): void { this.ngbActiveModal.close({ action: 'delete', - stateName: stateName + stateName: stateName, }); } selectState(stateName: string): void { this.ngbActiveModal.close({ action: 'navigate', - stateName: stateName + stateName: stateName, }); } ngOnInit(): void { - this.currentStateName = this.stateEditorService - .getActiveStateName(); + this.currentStateName = this.stateEditorService.getActiveStateName(); this.graphData = this.graphDataService.getGraphData(); } } -angular.module('oppia').directive('oppiaPostPublishModal', +angular.module('oppia').directive( + 'oppiaPostPublishModal', downgradeComponent({ - component: ExplorationGraphModalComponent - }) as angular.IDirectiveFactory); + component: ExplorationGraphModalComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/teach-oppia-modal-backend-api.service.ts b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/teach-oppia-modal-backend-api.service.ts index 253ae33694a5..692f8499204b 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/teach-oppia-modal-backend-api.service.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/teach-oppia-modal-backend-api.service.ts @@ -17,10 +17,10 @@ * backend. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { InteractionAnswer } from 'interactions/answer-defs'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {InteractionAnswer} from 'interactions/answer-defs'; export interface TeachOppiaModalData { data: { @@ -29,23 +29,22 @@ export interface TeachOppiaModalData { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class TeachOppiaModalBackendApiService { - constructor( - private http: HttpClient, - ) {} + constructor(private http: HttpClient) {} async fetchTeachOppiaModalDataAsync( - urlFragment: string, params: object): - Promise { - return this.http.get( - urlFragment, - params - ).toPromise(); + urlFragment: string, + params: object + ): Promise { + return this.http.get(urlFragment, params).toPromise(); } } -angular.module('oppia').factory( - 'TeachOppiaModalBackendApiService', - downgradeInjectable(TeachOppiaModalBackendApiService)); +angular + .module('oppia') + .factory( + 'TeachOppiaModalBackendApiService', + downgradeInjectable(TeachOppiaModalBackendApiService) + ); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/teach-oppia-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/teach-oppia-modal.component.spec.ts index 6dbd0358f10e..7a8045d1b22c 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/teach-oppia-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/teach-oppia-modal.component.spec.ts @@ -16,29 +16,40 @@ * @fileoverview Unit tests for TeachOppiaModalComponent. */ -import { EventEmitter, Injector, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { StateInteractionIdService } from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; -import { StateBackendDict, StateObjectFactory } from 'domain/state/StateObjectFactory'; -import { TrainingDataService } from '../../training-panel/training-data.service'; -import { TrainingModalService } from '../../training-panel/training-modal.service'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; -import { AlertsService } from 'services/alerts.service'; -import { ContextService } from 'services/context.service'; -import { ExplorationHtmlFormatterService } from 'services/exploration-html-formatter.service'; -import { ResponsesService } from '../../services/responses.service'; -import { TeachOppiaModalComponent, UnresolvedAnswer } from './teach-oppia-modal.component'; -import { TruncateInputBasedOnInteractionAnswerTypePipe } from 'filters/truncate-input-based-on-interaction-answer-type.pipe'; -import { AnswerClassificationResult } from 'domain/classifier/answer-classification-result.model'; -import { AnswerClassificationService } from 'pages/exploration-player-page/services/answer-classification.service'; -import { Outcome } from 'domain/exploration/OutcomeObjectFactory'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { TeachOppiaModalBackendApiService } from './teach-oppia-modal-backend-api.service'; -import { AnswerGroup } from 'domain/exploration/AnswerGroupObjectFactory'; -import { InteractionAnswer } from 'interactions/answer-defs'; +import {EventEmitter, Injector, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {StateInteractionIdService} from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; +import { + StateBackendDict, + StateObjectFactory, +} from 'domain/state/StateObjectFactory'; +import {TrainingDataService} from '../../training-panel/training-data.service'; +import {TrainingModalService} from '../../training-panel/training-modal.service'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; +import {AlertsService} from 'services/alerts.service'; +import {ContextService} from 'services/context.service'; +import {ExplorationHtmlFormatterService} from 'services/exploration-html-formatter.service'; +import {ResponsesService} from '../../services/responses.service'; +import { + TeachOppiaModalComponent, + UnresolvedAnswer, +} from './teach-oppia-modal.component'; +import {TruncateInputBasedOnInteractionAnswerTypePipe} from 'filters/truncate-input-based-on-interaction-answer-type.pipe'; +import {AnswerClassificationResult} from 'domain/classifier/answer-classification-result.model'; +import {AnswerClassificationService} from 'pages/exploration-player-page/services/answer-classification.service'; +import {Outcome} from 'domain/exploration/OutcomeObjectFactory'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {TeachOppiaModalBackendApiService} from './teach-oppia-modal-backend-api.service'; +import {AnswerGroup} from 'domain/exploration/AnswerGroupObjectFactory'; +import {InteractionAnswer} from 'interactions/answer-defs'; describe('Teach Oppia Modal Component', () => { let component: TeachOppiaModalComponent; @@ -63,63 +74,67 @@ describe('Teach Oppia Modal Component', () => { classifier_model_id: null, content: { html: '', - content_id: 'content' + content_id: 'content', }, interaction: { id: 'TextInput', customization_args: { rows: { - value: 1 + value: 1, }, placeholder: { - value: 'Type your answer here.' - } + value: 'Type your answer here.', + }, }, - answer_groups: [{ - rule_specs: [{ - rule_type: 'Equals', - inputs: { - x: { - contentId: 'rule_input', - normalizedStrSet: ['Correct Answer'] - } - } - }], - outcome: { - dest: 'outcome 1', - dest_if_really_stuck: null, - feedback: { - content_id: 'content_5', - html: '' + answer_groups: [ + { + rule_specs: [ + { + rule_type: 'Equals', + inputs: { + x: { + contentId: 'rule_input', + normalizedStrSet: ['Correct Answer'], + }, + }, + }, + ], + outcome: { + dest: 'outcome 1', + dest_if_really_stuck: null, + feedback: { + content_id: 'content_5', + html: '', + }, + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, }, - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null + training_data: [], + tagged_skill_misconception_id: null, }, - training_data: [], - tagged_skill_misconception_id: null - }], + ], default_outcome: { dest: 'Introduction', dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: 'This is a html feedback' + html: 'This is a html feedback', }, labelled_as_correct: false, param_changes: [], refresher_exploration_id: null, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, confirmed_unclassified_answers: [], hints: [], - solution: null + solution: null, }, linked_skill_id: null, param_changes: [], recorded_voiceovers: { - voiceovers_mapping: {} + voiceovers_mapping: {}, }, solicit_answer_details: false, card_is_checkpoint: false, @@ -151,12 +166,8 @@ describe('Teach Oppia Modal Component', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - TeachOppiaModalComponent, - ], + imports: [HttpClientTestingModule], + declarations: [TeachOppiaModalComponent], providers: [ Injector, TruncateInputBasedOnInteractionAnswerTypePipe, @@ -164,18 +175,18 @@ describe('Teach Oppia Modal Component', () => { AnswerClassificationService, { provide: NgbActiveModal, - useClass: MockActiveModal + useClass: MockActiveModal, }, { provide: TrainingDataService, - useClass: MockTrainingDataService + useClass: MockTrainingDataService, }, { provide: TrainingModalService, - useClass: MockTrainingModalService + useClass: MockTrainingModalService, }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); @@ -188,7 +199,8 @@ describe('Teach Oppia Modal Component', () => { trainingDataService = TestBed.inject(TrainingDataService); trainingModalService = TestBed.inject(TrainingModalService); teachOppiaModalBackendApiService = TestBed.inject( - TeachOppiaModalBackendApiService); + TeachOppiaModalBackendApiService + ); answerClassificationService = TestBed.inject(AnswerClassificationService); }); @@ -199,21 +211,30 @@ describe('Teach Oppia Modal Component', () => { answer: 'Answer Text', answerTemplate: '', classificationResult: new AnswerClassificationResult( - {} as Outcome, 0, 0, ''), - feedbackHtml: 'This is a html feedback' + {} as Outcome, + 0, + 0, + '' + ), + feedbackHtml: 'This is a html feedback', }, { answer: 'Answer Text', answerTemplate: '', classificationResult: new AnswerClassificationResult( - {} as Outcome, 0, 0, ''), - feedbackHtml: 'This is a html feedback' - } + {} as Outcome, + 0, + 0, + '' + ), + feedbackHtml: 'This is a html feedback', + }, ]; alertsService = TestBed.inject(AlertsService); contextService = TestBed.inject(ContextService); explorationHtmlFormatterService = TestBed.inject( - ExplorationHtmlFormatterService); + ExplorationHtmlFormatterService + ); explorationStatesService = TestBed.inject(ExplorationStatesService); stateEditorService = TestBed.inject(StateEditorService); responsesService = TestBed.inject(ResponsesService); @@ -223,17 +244,23 @@ describe('Teach Oppia Modal Component', () => { spyOn(injector, 'get').and.stub(); spyOn(contextService, 'getExplorationId').and.returnValue(explorationId); spyOn(stateEditorService, 'getActiveStateName').and.returnValue( - stateName); + stateName + ); spyOn(explorationStatesService, 'getState').and.returnValue( - stateObjectFactory.createFromBackendDict(stateName, state)); + stateObjectFactory.createFromBackendDict(stateName, state) + ); stateInteractionIdService.init(stateName, 'TextInput'); - spyOn(responsesService, 'getConfirmedUnclassifiedAnswers').and - .returnValue([]); - spyOn(responsesService, 'getAnswerGroups').and - .returnValue([new AnswerGroup([], {} as Outcome, [], '')]); - - spyOn(explorationHtmlFormatterService, 'getAnswerHtml').and - .returnValue(''); + spyOn( + responsesService, + 'getConfirmedUnclassifiedAnswers' + ).and.returnValue([]); + spyOn(responsesService, 'getAnswerGroups').and.returnValue([ + new AnswerGroup([], {} as Outcome, [], ''), + ]); + + spyOn(explorationHtmlFormatterService, 'getAnswerHtml').and.returnValue( + '' + ); component.ngOnInit(); }); @@ -242,135 +269,168 @@ describe('Teach Oppia Modal Component', () => { component.ngOnDestroy(); }); - it('should initialize unresolved answer properties after controller is' + - ' initialized', fakeAsync(() => { - let unresolvedAnswers = component.unresolvedAnswers[0]; - - let finishTrainingResult = { - answerIndex: 0, - answer: 'answer Data for truncateInputBasedOnInteractionAnswerType' - }; - component.interactionId = 'TextInput'; + it( + 'should initialize unresolved answer properties after controller is' + + ' initialized', + fakeAsync(() => { + let unresolvedAnswers = component.unresolvedAnswers[0]; - onchange.emit(finishTrainingResult); - tick(); + let finishTrainingResult = { + answerIndex: 0, + answer: 'answer Data for truncateInputBasedOnInteractionAnswerType', + }; + component.interactionId = 'TextInput'; - expect(unresolvedAnswers.answer).toBe('Answer Text'); - expect(unresolvedAnswers.answerTemplate).toBe(''); - expect(unresolvedAnswers.feedbackHtml).toBe('This is a html feedback'); - })); + onchange.emit(finishTrainingResult); + tick(); - it('should confirm answer assignment when its type is default_outcome', - () => { - spyOn(alertsService, 'addSuccessMessage'); - spyOn(trainingDataService, 'associateWithDefaultResponse').and - .callFake(() => {}); - component.confirmAnswerAssignment(0); + expect(unresolvedAnswers.answer).toBe('Answer Text'); + expect(unresolvedAnswers.answerTemplate).toBe(''); + expect(unresolvedAnswers.feedbackHtml).toBe('This is a html feedback'); + }) + ); + + it('should confirm answer assignment when its type is default_outcome', () => { + spyOn(alertsService, 'addSuccessMessage'); + spyOn(trainingDataService, 'associateWithDefaultResponse').and.callFake( + () => {} + ); + component.confirmAnswerAssignment(0); + + expect(alertsService.addSuccessMessage).toHaveBeenCalledWith( + 'The answer Answer Text has been successfully trained.', + 2000 + ); + }); - expect(alertsService.addSuccessMessage).toHaveBeenCalledWith( - 'The answer Answer Text has been successfully trained.', 2000); - }); + it('should return when its type is not default_outcome', () => { + component.unresolvedAnswers = [ + { + answer: 'Answer Text', + answerTemplate: '', + classificationResult: new AnswerClassificationResult( + {} as Outcome, + 0, + 0, + 'default_outcome' + ), + feedbackHtml: 'This is a html feedback', + }, + { + answer: 'Answer Text', + answerTemplate: '', + classificationResult: new AnswerClassificationResult( + {} as Outcome, + 0, + 0, + 'default_outcome' + ), + feedbackHtml: 'This is a html feedback', + }, + ]; - it('should return when its type is not default_outcome', - () => { - component.unresolvedAnswers = [ - { - answer: 'Answer Text', - answerTemplate: '', - classificationResult: new AnswerClassificationResult( - {} as Outcome, 0, 0, 'default_outcome'), - feedbackHtml: 'This is a html feedback' - }, - { - answer: 'Answer Text', - answerTemplate: '', - classificationResult: new AnswerClassificationResult( - {} as Outcome, 0, 0, 'default_outcome'), - feedbackHtml: 'This is a html feedback' - } - ]; - - spyOn(alertsService, 'addSuccessMessage'); - spyOn(trainingDataService, 'associateWithDefaultResponse').and - .callFake(() => {}); - component.confirmAnswerAssignment(0); - - expect(alertsService.addSuccessMessage).toHaveBeenCalledWith( - 'The answer Answer Text has been successfully trained.', 2000); - }); + spyOn(alertsService, 'addSuccessMessage'); + spyOn(trainingDataService, 'associateWithDefaultResponse').and.callFake( + () => {} + ); + component.confirmAnswerAssignment(0); - it('should confirm answer assignment when its type is not default_outcome', - () => { - spyOn(alertsService, 'addSuccessMessage'); - spyOn(trainingDataService, 'associateWithAnswerGroup').and - .callFake(() => {}); - - // Mocking the answer object to change its type manually because - // the controller has a lot of dependencies and can make it - // hard to understand. - Object.defineProperty(component, 'unresolvedAnswers', { - get: () => undefined - }); - spyOnProperty(component, 'unresolvedAnswers').and.returnValue([ - {} as UnresolvedAnswer, - { - answer: 'Correct answer', - classificationResult: { - classificationCategorization: 'explicit', - answerGroupIndex: 0 - }, - } as UnresolvedAnswer - ]); - component.confirmAnswerAssignment(1); + expect(alertsService.addSuccessMessage).toHaveBeenCalledWith( + 'The answer Answer Text has been successfully trained.', + 2000 + ); + }); - expect(alertsService.addSuccessMessage).toHaveBeenCalledWith( - 'The answer Correct a... has been successfully trained.', 2000); + it('should confirm answer assignment when its type is not default_outcome', () => { + spyOn(alertsService, 'addSuccessMessage'); + spyOn(trainingDataService, 'associateWithAnswerGroup').and.callFake( + () => {} + ); + + // Mocking the answer object to change its type manually because + // the controller has a lot of dependencies and can make it + // hard to understand. + Object.defineProperty(component, 'unresolvedAnswers', { + get: () => undefined, }); + spyOnProperty(component, 'unresolvedAnswers').and.returnValue([ + {} as UnresolvedAnswer, + { + answer: 'Correct answer', + classificationResult: { + classificationCategorization: 'explicit', + answerGroupIndex: 0, + }, + } as UnresolvedAnswer, + ]); + component.confirmAnswerAssignment(1); + + expect(alertsService.addSuccessMessage).toHaveBeenCalledWith( + 'The answer Correct a... has been successfully trained.', + 2000 + ); + }); it('should open train unresolved answer modal', () => { - spyOn(trainingModalService, 'openTrainUnresolvedAnswerModal').and - .callFake(function(InteractionAnswer, interactionId, answerIndex) { - }); + spyOn( + trainingModalService, + 'openTrainUnresolvedAnswerModal' + ).and.callFake( + function (InteractionAnswer, interactionId, answerIndex) {} + ); component.openTrainUnresolvedAnswerModal(0); - expect(trainingModalService.openTrainUnresolvedAnswerModal) - .toHaveBeenCalled(); + expect( + trainingModalService.openTrainUnresolvedAnswerModal + ).toHaveBeenCalled(); }); it('should show Unresolved Answers', () => { let outcome = new Outcome( - '', '', new SubtitledHtml('html', 'html'), - false, [], '', ''); - spyOn(answerClassificationService, 'getMatchingClassificationResult') - .and.returnValue( - new AnswerClassificationResult(outcome, 0, 0, 'Answers')); - spyOn(trainingDataService, 'isConfirmedUnclassifiedAnswer') - .and.returnValue(false); - let unresolvedAnswers = [{ - answer: {} as InteractionAnswer, - }]; + '', + '', + new SubtitledHtml('html', 'html'), + false, + [], + '', + '' + ); + spyOn( + answerClassificationService, + 'getMatchingClassificationResult' + ).and.returnValue( + new AnswerClassificationResult(outcome, 0, 0, 'Answers') + ); + spyOn( + trainingDataService, + 'isConfirmedUnclassifiedAnswer' + ).and.returnValue(false); + let unresolvedAnswers = [ + { + answer: {} as InteractionAnswer, + }, + ]; component.showUnresolvedAnswers(unresolvedAnswers); }); - it('should call teachOppiaModalBackendApiService to fetch data', - fakeAsync(() => { - let response = { - data: { - unresolved_answers: [], - } - }; + it('should call teachOppiaModalBackendApiService to fetch data', fakeAsync(() => { + let response = { + data: { + unresolved_answers: [], + }, + }; - spyOn(component, 'showUnresolvedAnswers').and.stub(); - spyOn(teachOppiaModalBackendApiService, 'fetchTeachOppiaModalDataAsync') - .and.returnValue( - Promise.resolve(response) - ); + spyOn(component, 'showUnresolvedAnswers').and.stub(); + spyOn( + teachOppiaModalBackendApiService, + 'fetchTeachOppiaModalDataAsync' + ).and.returnValue(Promise.resolve(response)); - component.ngOnInit(); - tick(); + component.ngOnInit(); + tick(); - expect(component.showUnresolvedAnswers).toHaveBeenCalled(); - })); + expect(component.showUnresolvedAnswers).toHaveBeenCalled(); + })); }); }); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/teach-oppia-modal.component.ts b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/teach-oppia-modal.component.ts index e817057b5300..6c0f95eb8631 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/teach-oppia-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/templates/modal-templates/teach-oppia-modal.component.ts @@ -16,30 +16,33 @@ * @fileoverview Component for teach oppia modal. */ -import { Component, Injector, OnDestroy, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { Subscription } from 'rxjs'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { StateCustomizationArgsService } from 'components/state-editor/state-editor-properties-services/state-customization-args.service'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { StateInteractionIdService } from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { AngularNameService } from 'pages/exploration-editor-page/services/angular-name.service'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; -import { AnswerClassificationService, InteractionRulesService } from 'pages/exploration-player-page/services/answer-classification.service'; -import { AlertsService } from 'services/alerts.service'; -import { ContextService } from 'services/context.service'; -import { ExplorationHtmlFormatterService } from 'services/exploration-html-formatter.service'; -import { TrainingDataService } from '../../training-panel/training-data.service'; -import { TrainingModalService } from '../../training-panel/training-modal.service'; -import { TruncateInputBasedOnInteractionAnswerTypePipe } from 'filters/truncate-input-based-on-interaction-answer-type.pipe'; -import { ExplorationPlayerConstants } from 'pages/exploration-player-page/exploration-player-page.constants'; -import { LoggerService } from 'services/contextual/logger.service'; -import { State } from 'domain/state/StateObjectFactory'; -import { InteractionAnswer } from 'interactions/answer-defs'; -import { TeachOppiaModalBackendApiService } from './teach-oppia-modal-backend-api.service'; -import { AnswerClassificationResult } from 'domain/classifier/answer-classification-result.model'; +import {Component, Injector, OnDestroy, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {Subscription} from 'rxjs'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {StateCustomizationArgsService} from 'components/state-editor/state-editor-properties-services/state-customization-args.service'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {StateInteractionIdService} from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {AngularNameService} from 'pages/exploration-editor-page/services/angular-name.service'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; +import { + AnswerClassificationService, + InteractionRulesService, +} from 'pages/exploration-player-page/services/answer-classification.service'; +import {AlertsService} from 'services/alerts.service'; +import {ContextService} from 'services/context.service'; +import {ExplorationHtmlFormatterService} from 'services/exploration-html-formatter.service'; +import {TrainingDataService} from '../../training-panel/training-data.service'; +import {TrainingModalService} from '../../training-panel/training-modal.service'; +import {TruncateInputBasedOnInteractionAnswerTypePipe} from 'filters/truncate-input-based-on-interaction-answer-type.pipe'; +import {ExplorationPlayerConstants} from 'pages/exploration-player-page/exploration-player-page.constants'; +import {LoggerService} from 'services/contextual/logger.service'; +import {State} from 'domain/state/StateObjectFactory'; +import {InteractionAnswer} from 'interactions/answer-defs'; +import {TeachOppiaModalBackendApiService} from './teach-oppia-modal-backend-api.service'; +import {AnswerClassificationResult} from 'domain/classifier/answer-classification-result.model'; export interface UnresolvedAnswer { answer: InteractionAnswer; @@ -50,10 +53,12 @@ export interface UnresolvedAnswer { @Component({ selector: 'oppia-teach-oppia-modal', - templateUrl: './teach-oppia-modal.component.html' + templateUrl: './teach-oppia-modal.component.html', }) export class TeachOppiaModalComponent - extends ConfirmOrCancelModal implements OnInit, OnDestroy { + extends ConfirmOrCancelModal + implements OnInit, OnDestroy +{ directiveSubscriptions = new Subscription(); // These properties below are initialized using Angular lifecycle hooks // where we need to do non-null assertion. For more information see @@ -85,62 +90,75 @@ export class TeachOppiaModalComponent private teachOppiaModalBackendApiService: TeachOppiaModalBackendApiService, private trainingDataService: TrainingDataService, private trainingModalService: TrainingModalService, - private truncateInputBasedOnInteractionAnswerTypePipe: - TruncateInputBasedOnInteractionAnswerTypePipe, - private urlInterpolationService: UrlInterpolationService, + private truncateInputBasedOnInteractionAnswerTypePipe: TruncateInputBasedOnInteractionAnswerTypePipe, + private urlInterpolationService: UrlInterpolationService ) { super(ngbActiveModal); } - fetchAndShowUnresolvedAnswers( - expId: string, stateName: string): void { - let unresolvedAnswersUrl = ( - this.urlInterpolationService.interpolateUrl( - '/createhandler/get_top_unresolved_answers/' + - '', { - exploration_id: expId - })); + fetchAndShowUnresolvedAnswers(expId: string, stateName: string): void { + let unresolvedAnswersUrl = this.urlInterpolationService.interpolateUrl( + '/createhandler/get_top_unresolved_answers/' + '', + { + exploration_id: expId, + } + ); this.teachOppiaModalBackendApiService .fetchTeachOppiaModalDataAsync(unresolvedAnswersUrl, { params: { - state_name: stateName + state_name: stateName, + }, + }) + .then( + response => { + this.showUnresolvedAnswers(response.data.unresolved_answers); + }, + response => { + this.loggerService.error( + 'Error occurred while fetching unresolved answers ' + + 'for exploration ' + + this._explorationId + + ' state ' + + this._stateName + + ': ' + + response.data + ); + this.showUnresolvedAnswers([]); } - }).then((response) => { - this.showUnresolvedAnswers(response.data.unresolved_answers); - }, (response) => { - this.loggerService.error( - 'Error occurred while fetching unresolved answers ' + - 'for exploration ' + this._explorationId + ' state ' + - this._stateName + ': ' + response.data); - this.showUnresolvedAnswers([]); - }); + ); } showUnresolvedAnswers( - unresolvedAnswers: {answer: InteractionAnswer}[]): void { + unresolvedAnswers: {answer: InteractionAnswer}[] + ): void { this.loadingDotsAreShown = false; this.unresolvedAnswers = []; unresolvedAnswers.forEach((item: {answer: InteractionAnswer}) => { let answer = item.answer; - let classificationResult = ( + let classificationResult = this.answerClassificationService.getMatchingClassificationResult( - this._stateName, this._state.interaction, answer, this.rulesService)); - let classificationType = ( - classificationResult.classificationCategorization); - if (classificationType !== ( - ExplorationPlayerConstants.EXPLICIT_CLASSIFICATION) && - classificationType !== ( - ExplorationPlayerConstants.TRAINING_DATA_CLASSIFICATION) && - !this.trainingDataService.isConfirmedUnclassifiedAnswer( - answer)) { - let answerTemplate = ( - this.explorationHtmlFormatterService.getAnswerHtml( - answer, this.stateInteractionIdService.savedMemento, - this.stateCustomizationArgsService.savedMemento)); - let feedbackHtml = ( - classificationResult.outcome.feedback.html); + this._stateName, + this._state.interaction, + answer, + this.rulesService + ); + let classificationType = + classificationResult.classificationCategorization; + if ( + classificationType !== + ExplorationPlayerConstants.EXPLICIT_CLASSIFICATION && + classificationType !== + ExplorationPlayerConstants.TRAINING_DATA_CLASSIFICATION && + !this.trainingDataService.isConfirmedUnclassifiedAnswer(answer) + ) { + let answerTemplate = this.explorationHtmlFormatterService.getAnswerHtml( + answer, + this.stateInteractionIdService.savedMemento, + this.stateCustomizationArgsService.savedMemento + ); + let feedbackHtml = classificationResult.outcome.feedback.html; this.unresolvedAnswers.push({ answer: answer, @@ -156,60 +174,70 @@ export class TeachOppiaModalComponent const unresolvedAnswer = this.unresolvedAnswers[answerIndex]; this.unresolvedAnswers.splice(answerIndex, 1); - const classificationType = ( - unresolvedAnswer.classificationResult.classificationCategorization); - const truncatedAnswer = ( + const classificationType = + unresolvedAnswer.classificationResult.classificationCategorization; + const truncatedAnswer = this.truncateInputBasedOnInteractionAnswerTypePipe.transform( - unresolvedAnswer.answer, this.interactionId, 12)); - const successToast = ( - 'The answer ' + truncatedAnswer + - ' has been successfully trained.'); + unresolvedAnswer.answer, + this.interactionId, + 12 + ); + const successToast = + 'The answer ' + truncatedAnswer + ' has been successfully trained.'; - if (classificationType === ( - ExplorationPlayerConstants.DEFAULT_OUTCOME_CLASSIFICATION)) { + if ( + classificationType === + ExplorationPlayerConstants.DEFAULT_OUTCOME_CLASSIFICATION + ) { this.trainingDataService.associateWithDefaultResponse( - unresolvedAnswer.answer); - this.alertsService.addSuccessMessage( - successToast, this.TOAST_TIMEOUT); + unresolvedAnswer.answer + ); + this.alertsService.addSuccessMessage(successToast, this.TOAST_TIMEOUT); return; } this.trainingDataService.associateWithAnswerGroup( unresolvedAnswer.classificationResult.answerGroupIndex, - unresolvedAnswer.answer); - this.alertsService.addSuccessMessage( - successToast, this.TOAST_TIMEOUT); + unresolvedAnswer.answer + ); + this.alertsService.addSuccessMessage(successToast, this.TOAST_TIMEOUT); } openTrainUnresolvedAnswerModal(answerIndex: number): void { - const unresolvedAnswer = ( - this.unresolvedAnswers[answerIndex]); + const unresolvedAnswer = this.unresolvedAnswers[answerIndex]; const answer = unresolvedAnswer.answer; let interactionId = this.stateInteractionIdService.savedMemento; this.trainingModalService.openTrainUnresolvedAnswerModal( - answer, interactionId, answerIndex); + answer, + interactionId, + answerIndex + ); } ngOnInit(): void { this.directiveSubscriptions.add( this.trainingModalService.onFinishTrainingCallback.subscribe( - (finishTrainingResult) => { - this.unresolvedAnswers.splice( - finishTrainingResult.answerIndex, 1); - const truncatedAnswer = ( + finishTrainingResult => { + this.unresolvedAnswers.splice(finishTrainingResult.answerIndex, 1); + const truncatedAnswer = this.truncateInputBasedOnInteractionAnswerTypePipe.transform( - finishTrainingResult.answer, this.interactionId, 12)); - const successToast = ( - 'The response for ' + truncatedAnswer + - ' has been fixed.'); + finishTrainingResult.answer, + this.interactionId, + 12 + ); + const successToast = + 'The response for ' + truncatedAnswer + ' has been fixed.'; this.alertsService.addSuccessMessage( - successToast, this.TOAST_TIMEOUT); - })); + successToast, + this.TOAST_TIMEOUT + ); + } + ) + ); - this._explorationId = ( - this.contextService.getExplorationId()); + this._explorationId = this.contextService.getExplorationId(); let stateName = this.stateEditorService.getActiveStateName(); if (stateName) { this._stateName = stateName; @@ -217,9 +245,10 @@ export class TeachOppiaModalComponent this.interactionId = this.stateInteractionIdService.savedMemento; } - const rulesServiceName = ( + const rulesServiceName = this.angularNameService.getNameOfInteractionRulesService( - this.interactionId)); + this.interactionId + ); // Inject RulesService dynamically. this.rulesService = this.injector.get(rulesServiceName); @@ -232,7 +261,9 @@ export class TeachOppiaModalComponent this.directiveSubscriptions.unsubscribe(); } } -angular.module('oppia').factory('oppiaTeachOppiaModal', +angular.module('oppia').factory( + 'oppiaTeachOppiaModal', downgradeComponent({ - component: TeachOppiaModalComponent - }) as angular.IDirectiveFactory); + component: TeachOppiaModalComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/test-interaction-panel/test-interaction-panel.component.spec.ts b/core/templates/pages/exploration-editor-page/editor-tab/test-interaction-panel/test-interaction-panel.component.spec.ts index 123c8c713437..36ec22c7d0cc 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/test-interaction-panel/test-interaction-panel.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/test-interaction-panel/test-interaction-panel.component.spec.ts @@ -16,12 +16,12 @@ * @fileoverview Unit tests for testInteractionPanel. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; -import { CurrentInteractionService } from 'pages/exploration-player-page/services/current-interaction.service'; -import { TestInteractionPanel } from './test-interaction-panel.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; +import {CurrentInteractionService} from 'pages/exploration-player-page/services/current-interaction.service'; +import {TestInteractionPanel} from './test-interaction-panel.component'; class MockNgbModal { close() { @@ -33,8 +33,8 @@ class MockExplorationStatesService { getState(item1: string) { return { interaction: { - id: 'TextInput' - } + id: 'TextInput', + }, }; } } @@ -46,21 +46,19 @@ describe('Test Interaction Panel Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - TestInteractionPanel - ], + declarations: [TestInteractionPanel], providers: [ { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, { provide: ExplorationStatesService, - useClass: MockExplorationStatesService + useClass: MockExplorationStatesService, }, - CurrentInteractionService + CurrentInteractionService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -73,22 +71,21 @@ describe('Test Interaction Panel Component', () => { fixture.detectChanges(); }); - it('should initialize controller properties after its initialization', - () => { - spyOn(currentInteractionService, 'isSubmitButtonDisabled') - .and.returnValue(false); + it('should initialize controller properties after its initialization', () => { + spyOn(currentInteractionService, 'isSubmitButtonDisabled').and.returnValue( + false + ); - component.stateName = 'TextInput'; - component.ngOnInit(); - let isSubmitButtonDisabled = component.isSubmitButtonDisabled(); + component.stateName = 'TextInput'; + component.ngOnInit(); + let isSubmitButtonDisabled = component.isSubmitButtonDisabled(); - expect(component.interactionIsInline).toEqual(true); - expect(isSubmitButtonDisabled).toEqual(false); - }); + expect(component.interactionIsInline).toEqual(true); + expect(isSubmitButtonDisabled).toEqual(false); + }); it('should submit answer when clicking on button', () => { - spyOn(currentInteractionService, 'submitAnswer') - .and.stub(); + spyOn(currentInteractionService, 'submitAnswer').and.stub(); component.stateName = 'TextInput'; component.ngOnInit(); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/test-interaction-panel/test-interaction-panel.component.ts b/core/templates/pages/exploration-editor-page/editor-tab/test-interaction-panel/test-interaction-panel.component.ts index 8c4abd44f4c2..a01746043d8f 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/test-interaction-panel/test-interaction-panel.component.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/test-interaction-panel/test-interaction-panel.component.ts @@ -16,17 +16,17 @@ * @fileoverview Component for the test interaction panel in the state editor. */ -import { Component, Input, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; -import { CurrentInteractionService } from 'pages/exploration-player-page/services/current-interaction.service'; +import {Component, Input, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; +import {CurrentInteractionService} from 'pages/exploration-player-page/services/current-interaction.service'; import INTERACTION_SPECS from 'interactions/interaction_specs.json'; -import { AppConstants } from 'app.constants'; -import { InteractionSpecsKey } from 'pages/interaction-specs.constants'; +import {AppConstants} from 'app.constants'; +import {InteractionSpecsKey} from 'pages/interaction-specs.constants'; @Component({ selector: 'oppia-test-interaction-panel', - templateUrl: './test-interaction-panel.component.html' + templateUrl: './test-interaction-panel.component.html', }) export class TestInteractionPanel implements OnInit { // These properties below are initialized using Angular lifecycle hooks @@ -38,8 +38,8 @@ export class TestInteractionPanel implements OnInit { constructor( private currentInteractionService: CurrentInteractionService, - private explorationStatesService: ExplorationStatesService, - ) { } + private explorationStatesService: ExplorationStatesService + ) {} isSubmitButtonDisabled(): boolean { return this.currentInteractionService.isSubmitButtonDisabled(); @@ -52,15 +52,15 @@ export class TestInteractionPanel implements OnInit { ngOnInit(): void { let _stateName = this.stateName; let _state = this.explorationStatesService.getState(_stateName); - this.interactionIsInline = ( - INTERACTION_SPECS[ - _state.interaction.id as InteractionSpecsKey - ].display_mode === - AppConstants.INTERACTION_DISPLAY_MODE_INLINE); + this.interactionIsInline = + INTERACTION_SPECS[_state.interaction.id as InteractionSpecsKey] + .display_mode === AppConstants.INTERACTION_DISPLAY_MODE_INLINE; } } -angular.module('oppia').directive('oppiaTestInteractionPanel', +angular.module('oppia').directive( + 'oppiaTestInteractionPanel', downgradeComponent({ - component: TestInteractionPanel - }) as angular.IDirectiveFactory); + component: TestInteractionPanel, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-data-editor-panel-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-data-editor-panel-modal.component.spec.ts index 2f93d8c5be90..b978575575f3 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-data-editor-panel-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-data-editor-panel-modal.component.spec.ts @@ -16,101 +16,119 @@ * @fileoverview Unit tests for TrainingDataEditorPanelServiceModalcomponent. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { StateInteractionIdService } from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; -import { AnswerGroup } from 'domain/exploration/AnswerGroupObjectFactory'; -import { Outcome } from 'domain/exploration/OutcomeObjectFactory'; -import { Rule } from 'domain/exploration/rule.model'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { TruncateInputBasedOnInteractionAnswerTypePipe } from 'filters/truncate-input-based-on-interaction-answer-type.pipe'; -import { ExplorationDataService } from 'pages/exploration-editor-page/services/exploration-data.service'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; -import { AnswerClassificationService } from 'pages/exploration-player-page/services/answer-classification.service'; -import { AlertsService } from 'services/alerts.service'; -import { ExplorationHtmlFormatterService } from 'services/exploration-html-formatter.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { ResponsesService } from '../services/responses.service'; -import { TrainingDataEditorPanelComponent } from './training-data-editor-panel-modal.component'; -import { TrainingDataService } from './training-data.service'; -import { TrainingModalService } from './training-modal.service'; - - - class MockStateEditorService { - getActiveStateName() { - return 'Hola'; - } - } - - class MockExplorationStatesService { - getState(item1: string) { - return { - content: { - html: 'This is Hola State' - } - }; - } - } - - class MockResponsesService { - getActiveAnswerGroupIndex() { - return 1; - } - - getAnswerGroup() { - return new AnswerGroup([ - new Rule('TextInput', { - x: [], - }, { - x: 'ListOfSetsOfTranslatableHtmlContentIds' - }), - new Rule('TextInput', { - x: [], - }, { - x: 'ListOfSetsOfTranslatableHtmlContentIds' - }), - ], {} as Outcome, ['Answer1', 'Answer2'], null); - } - } - - class MockExplorationHtmlFormatterService { - getInteractionHtml() { - return 'MockExplorationHtmlFormattered string'; - } - - getAnswerHtml() { - return 'answer'; - } - } - - class MockActiveModal { - close(): void { - return; - } - - dismiss(): void { - return; - } - } - - class MockStateInteractionIdService { - savedMemento = 'TextInput'; - } - - class MockAnswerClassificationService { - getMatchingClassificationResult() { - return { - outcome: { - dest: 'dest', - feedback: 'feedback' - }, - classificationCategorization: 'explicit Type' - }; - } - } +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {StateInteractionIdService} from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; +import {AnswerGroup} from 'domain/exploration/AnswerGroupObjectFactory'; +import {Outcome} from 'domain/exploration/OutcomeObjectFactory'; +import {Rule} from 'domain/exploration/rule.model'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {TruncateInputBasedOnInteractionAnswerTypePipe} from 'filters/truncate-input-based-on-interaction-answer-type.pipe'; +import {ExplorationDataService} from 'pages/exploration-editor-page/services/exploration-data.service'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; +import {AnswerClassificationService} from 'pages/exploration-player-page/services/answer-classification.service'; +import {AlertsService} from 'services/alerts.service'; +import {ExplorationHtmlFormatterService} from 'services/exploration-html-formatter.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {ResponsesService} from '../services/responses.service'; +import {TrainingDataEditorPanelComponent} from './training-data-editor-panel-modal.component'; +import {TrainingDataService} from './training-data.service'; +import {TrainingModalService} from './training-modal.service'; + +class MockStateEditorService { + getActiveStateName() { + return 'Hola'; + } +} + +class MockExplorationStatesService { + getState(item1: string) { + return { + content: { + html: 'This is Hola State', + }, + }; + } +} + +class MockResponsesService { + getActiveAnswerGroupIndex() { + return 1; + } + + getAnswerGroup() { + return new AnswerGroup( + [ + new Rule( + 'TextInput', + { + x: [], + }, + { + x: 'ListOfSetsOfTranslatableHtmlContentIds', + } + ), + new Rule( + 'TextInput', + { + x: [], + }, + { + x: 'ListOfSetsOfTranslatableHtmlContentIds', + } + ), + ], + {} as Outcome, + ['Answer1', 'Answer2'], + null + ); + } +} + +class MockExplorationHtmlFormatterService { + getInteractionHtml() { + return 'MockExplorationHtmlFormattered string'; + } + + getAnswerHtml() { + return 'answer'; + } +} + +class MockActiveModal { + close(): void { + return; + } + + dismiss(): void { + return; + } +} + +class MockStateInteractionIdService { + savedMemento = 'TextInput'; +} + +class MockAnswerClassificationService { + getMatchingClassificationResult() { + return { + outcome: { + dest: 'dest', + feedback: 'feedback', + }, + classificationCategorization: 'explicit Type', + }; + } +} describe('Training Data Editor Panel Component', () => { let component: TrainingDataEditorPanelComponent; @@ -119,227 +137,234 @@ describe('Training Data Editor Panel Component', () => { let ngbActiveModal: NgbActiveModal; let trainingModalService: TrainingModalService; let trainingDataService: TrainingDataService; - let truncateInputBasedOnInteractionAnswerTypePipe: - TruncateInputBasedOnInteractionAnswerTypePipe; + let truncateInputBasedOnInteractionAnswerTypePipe: TruncateInputBasedOnInteractionAnswerTypePipe; let answerClassificationService: AnswerClassificationService; let stateEditorService: StateEditorService; let trainingModalServiceeventEmitter = new EventEmitter(); - class MockTrainingModalService { - get onFinishTrainingCallback() { - return trainingModalServiceeventEmitter; - } - - getTrainingDataOfAnswerGroup(index1: string) { - return ['name', 'class']; - } - - openTrainUnresolvedAnswerModal( - item1: string, item2: string, item3: string) { - } - } - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - ], - declarations: [ - TrainingDataEditorPanelComponent - ], - providers: [ - FocusManagerService, - AlertsService, - TruncateInputBasedOnInteractionAnswerTypePipe, - { - provide: ExplorationDataService, - useValue: { - explorationId: 0, - autosaveChangeListAsync() { - return; - } - } - }, - { - provide: StateInteractionIdService, - useClass: MockStateInteractionIdService - }, - { - provide: NgbActiveModal, - useClass: MockActiveModal - }, - { - provide: StateEditorService, - useClass: MockStateEditorService - }, - { - provide: ExplorationStatesService, - useClass: MockExplorationStatesService - }, - { - provide: ResponsesService, - useClass: MockResponsesService - }, - { - provide: ExplorationHtmlFormatterService, - useClass: MockExplorationHtmlFormatterService - }, - { - provide: TrainingModalService, - useClass: MockTrainingModalService - }, - { - provide: AnswerClassificationService, - useClass: MockAnswerClassificationService - }, - TrainingDataService - ], - schemas: [NO_ERRORS_SCHEMA] - }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(TrainingDataEditorPanelComponent); - component = fixture.componentInstance; - - trainingDataService = TestBed.inject(TrainingDataService); - trainingModalService = TestBed.inject(TrainingModalService); - answerClassificationService = TestBed.inject(AnswerClassificationService); - focusManagerService = TestBed.inject(FocusManagerService); - stateEditorService = TestBed.inject(StateEditorService); - ngbActiveModal = TestBed.inject(NgbActiveModal); - truncateInputBasedOnInteractionAnswerTypePipe = - TestBed.inject(TruncateInputBasedOnInteractionAnswerTypePipe); - - spyOn(focusManagerService, 'setFocus').and.stub(); - spyOn(truncateInputBasedOnInteractionAnswerTypePipe, 'transform') - .and.returnValue('of question'); - spyOn(trainingDataService, 'associateWithAnswerGroup') - .and.stub(); - - fixture.detectChanges(); - }); - - it('should initialize component properties after component is initialized', - fakeAsync(() => { - component.ngOnInit(); - - trainingModalServiceeventEmitter.emit({ - answer: 'answer', - interactionId: 'interactionId', - }); - tick(); - - component.ngOnDestroy(); - - expect(truncateInputBasedOnInteractionAnswerTypePipe.transform) - .toHaveBeenCalled(); - expect(component.stateName).toBe('Hola'); - expect(component.stateContent).toBe('This is Hola State'); - expect(component.answerGroupHasNonEmptyRules).toBe(true); - expect(component.inputTemplate).toBe( - 'MockExplorationHtmlFormattered string'); - })); - - it('should call init when component is initialized', () => { - expect(component.trainingData).toEqual([{ - answer: 'Answer1', - answerTemplate: 'answer' - }, { - answer: 'Answer2', - answerTemplate: 'answer' - }]); - expect(component.newAnswerIsAlreadyResolved).toBe(false); - expect(component.answerSuccessfullyAdded).toBe(false); - }); - - it('should remove answer from training data', () => { - spyOn(trainingDataService, 'removeAnswerFromAnswerGroupTrainingData') - .and.stub(); - - component.removeAnswerFromTrainingData(0); - expect(component.trainingData).toEqual([{ - answer: 'Answer2', - answerTemplate: 'answer' - }]); - }); - - it('should submit answer that is not explicity classified', () => { - component.submitAnswer('answer'); - - expect(component.newAnswerTemplate).toBe( - 'answer'); - expect(component.newAnswerFeedback).toEqual( - 'feedback'); - expect(component.newAnswerOutcomeDest).toBe('dest'); - expect(component.newAnswerIsAlreadyResolved).toBe(false); - }); - - it('should submit answer that is explicity classified', () => { - spyOn(answerClassificationService, 'getMatchingClassificationResult') - .and.returnValue({ - outcome: new Outcome( - 'Hola', - '', - new SubtitledHtml('

Saved Outcome

', 'Id'), - false, - [], - '', - '', - ), - answerGroupIndex: 0, - ruleIndex: null, - classificationCategorization: 'explicit', - }); - - component.submitAnswer('answer'); - - expect(component.newAnswerTemplate).toBe( - 'answer'); - expect(component.newAnswerFeedback).toEqual( - new SubtitledHtml('

Saved Outcome

', 'Id')); - expect(component.newAnswerOutcomeDest).toBe('(try again)'); - expect(component.newAnswerIsAlreadyResolved).toBe(true); - }); - - it('should open train unresolved answer modal', () => { - component.answerGroupHasNonEmptyRules = true; - - spyOn(trainingModalService, 'openTrainUnresolvedAnswerModal').and - .stub(); - - component.openTrainUnresolvedAnswerModal(1); - expect(trainingModalService.openTrainUnresolvedAnswerModal) - .toHaveBeenCalled(); - }); - - it('should exit modal', () => { - spyOn(ngbActiveModal, 'close').and.stub(); - - component.exit(); - - expect(ngbActiveModal.close).toHaveBeenCalled(); - }); - - it('should dismiss modal', () => { - spyOn(ngbActiveModal, 'dismiss').and.stub(); - - component.cancel(); - - expect(ngbActiveModal.dismiss).toHaveBeenCalled(); - }); - - it('should throw error if state name is null', fakeAsync(() => { - spyOn(stateEditorService, 'getActiveStateName').and.returnValue(null); - expect(() => { - component.ngOnInit(); - }).toThrowError('State name cannot be empty.'); - })); - - it('should throw error if state name is null', fakeAsync(() => { - component._stateName = null; - expect(() => { - component.submitAnswer('answer'); - }).toThrowError('State name cannot be empty.'); - })); + class MockTrainingModalService { + get onFinishTrainingCallback() { + return trainingModalServiceeventEmitter; + } + + getTrainingDataOfAnswerGroup(index1: string) { + return ['name', 'class']; + } + + openTrainUnresolvedAnswerModal( + item1: string, + item2: string, + item3: string + ) {} + } + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + declarations: [TrainingDataEditorPanelComponent], + providers: [ + FocusManagerService, + AlertsService, + TruncateInputBasedOnInteractionAnswerTypePipe, + { + provide: ExplorationDataService, + useValue: { + explorationId: 0, + autosaveChangeListAsync() { + return; + }, + }, + }, + { + provide: StateInteractionIdService, + useClass: MockStateInteractionIdService, + }, + { + provide: NgbActiveModal, + useClass: MockActiveModal, + }, + { + provide: StateEditorService, + useClass: MockStateEditorService, + }, + { + provide: ExplorationStatesService, + useClass: MockExplorationStatesService, + }, + { + provide: ResponsesService, + useClass: MockResponsesService, + }, + { + provide: ExplorationHtmlFormatterService, + useClass: MockExplorationHtmlFormatterService, + }, + { + provide: TrainingModalService, + useClass: MockTrainingModalService, + }, + { + provide: AnswerClassificationService, + useClass: MockAnswerClassificationService, + }, + TrainingDataService, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TrainingDataEditorPanelComponent); + component = fixture.componentInstance; + + trainingDataService = TestBed.inject(TrainingDataService); + trainingModalService = TestBed.inject(TrainingModalService); + answerClassificationService = TestBed.inject(AnswerClassificationService); + focusManagerService = TestBed.inject(FocusManagerService); + stateEditorService = TestBed.inject(StateEditorService); + ngbActiveModal = TestBed.inject(NgbActiveModal); + truncateInputBasedOnInteractionAnswerTypePipe = TestBed.inject( + TruncateInputBasedOnInteractionAnswerTypePipe + ); + + spyOn(focusManagerService, 'setFocus').and.stub(); + spyOn( + truncateInputBasedOnInteractionAnswerTypePipe, + 'transform' + ).and.returnValue('of question'); + spyOn(trainingDataService, 'associateWithAnswerGroup').and.stub(); + + fixture.detectChanges(); + }); + + it('should initialize component properties after component is initialized', fakeAsync(() => { + component.ngOnInit(); + + trainingModalServiceeventEmitter.emit({ + answer: 'answer', + interactionId: 'interactionId', + }); + tick(); + + component.ngOnDestroy(); + + expect( + truncateInputBasedOnInteractionAnswerTypePipe.transform + ).toHaveBeenCalled(); + expect(component.stateName).toBe('Hola'); + expect(component.stateContent).toBe('This is Hola State'); + expect(component.answerGroupHasNonEmptyRules).toBe(true); + expect(component.inputTemplate).toBe( + 'MockExplorationHtmlFormattered string' + ); + })); + + it('should call init when component is initialized', () => { + expect(component.trainingData).toEqual([ + { + answer: 'Answer1', + answerTemplate: 'answer', + }, + { + answer: 'Answer2', + answerTemplate: 'answer', + }, + ]); + expect(component.newAnswerIsAlreadyResolved).toBe(false); + expect(component.answerSuccessfullyAdded).toBe(false); + }); + + it('should remove answer from training data', () => { + spyOn( + trainingDataService, + 'removeAnswerFromAnswerGroupTrainingData' + ).and.stub(); + + component.removeAnswerFromTrainingData(0); + expect(component.trainingData).toEqual([ + { + answer: 'Answer2', + answerTemplate: 'answer', + }, + ]); + }); + + it('should submit answer that is not explicity classified', () => { + component.submitAnswer('answer'); + + expect(component.newAnswerTemplate).toBe('answer'); + expect(component.newAnswerFeedback).toEqual('feedback'); + expect(component.newAnswerOutcomeDest).toBe('dest'); + expect(component.newAnswerIsAlreadyResolved).toBe(false); + }); + + it('should submit answer that is explicity classified', () => { + spyOn( + answerClassificationService, + 'getMatchingClassificationResult' + ).and.returnValue({ + outcome: new Outcome( + 'Hola', + '', + new SubtitledHtml('

Saved Outcome

', 'Id'), + false, + [], + '', + '' + ), + answerGroupIndex: 0, + ruleIndex: null, + classificationCategorization: 'explicit', + }); + + component.submitAnswer('answer'); + + expect(component.newAnswerTemplate).toBe('answer'); + expect(component.newAnswerFeedback).toEqual( + new SubtitledHtml('

Saved Outcome

', 'Id') + ); + expect(component.newAnswerOutcomeDest).toBe('(try again)'); + expect(component.newAnswerIsAlreadyResolved).toBe(true); + }); + + it('should open train unresolved answer modal', () => { + component.answerGroupHasNonEmptyRules = true; + + spyOn(trainingModalService, 'openTrainUnresolvedAnswerModal').and.stub(); + + component.openTrainUnresolvedAnswerModal(1); + expect( + trainingModalService.openTrainUnresolvedAnswerModal + ).toHaveBeenCalled(); + }); + + it('should exit modal', () => { + spyOn(ngbActiveModal, 'close').and.stub(); + + component.exit(); + + expect(ngbActiveModal.close).toHaveBeenCalled(); + }); + + it('should dismiss modal', () => { + spyOn(ngbActiveModal, 'dismiss').and.stub(); + + component.cancel(); + + expect(ngbActiveModal.dismiss).toHaveBeenCalled(); + }); + + it('should throw error if state name is null', fakeAsync(() => { + spyOn(stateEditorService, 'getActiveStateName').and.returnValue(null); + expect(() => { + component.ngOnInit(); + }).toThrowError('State name cannot be empty.'); + })); + + it('should throw error if state name is null', fakeAsync(() => { + component._stateName = null; + expect(() => { + component.submitAnswer('answer'); + }).toThrowError('State name cannot be empty.'); + })); }); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-data-editor-panel-modal.component.ts b/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-data-editor-panel-modal.component.ts index 264c1ada4004..79e299543f5e 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-data-editor-panel-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-data-editor-panel-modal.component.ts @@ -15,42 +15,45 @@ /** * @fileoverview Component for TrainingDataEditorPanelComponent modal. */ -import { Subscription } from 'rxjs'; -import { Component, Injector, OnDestroy, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { StateInteractionIdService } from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; -import { StateCustomizationArgsService } from 'components/state-editor/state-editor-properties-services/state-customization-args.service'; -import { ExplorationHtmlFormatterService } from 'services/exploration-html-formatter.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { ExplorationPlayerConstants } from 'pages/exploration-player-page/exploration-player-page.constants'; -import { AngularNameService } from 'pages/exploration-editor-page/services/angular-name.service'; -import { AlgebraicExpressionInputRulesService } from 'interactions/AlgebraicExpressionInput/directives/algebraic-expression-input-rules.service'; -import { CodeReplRulesService } from 'interactions/CodeRepl/directives/code-repl-rules.service'; -import { ContinueRulesService } from 'interactions/Continue/directives/continue-rules.service'; -import { FractionInputRulesService } from 'interactions/FractionInput/directives/fraction-input-rules.service'; -import { GraphInputRulesService } from 'interactions/GraphInput/directives/graph-input-rules.service'; -import { ImageClickInputRulesService } from 'interactions/ImageClickInput/directives/image-click-input-rules.service'; -import { InteractiveMapRulesService } from 'interactions/InteractiveMap/directives/interactive-map-rules.service'; -import { MathEquationInputRulesService } from 'interactions/MathEquationInput/directives/math-equation-input-rules.service'; -import { NumericExpressionInputRulesService } from 'interactions/NumericExpressionInput/directives/numeric-expression-input-rules.service'; -import { NumericInputRulesService } from 'interactions/NumericInput/directives/numeric-input-rules.service'; -import { PencilCodeEditorRulesService } from 'interactions/PencilCodeEditor/directives/pencil-code-editor-rules.service'; -import { SetInputRulesService } from 'interactions/SetInput/directives/set-input-rules.service'; -import { TextInputRulesService } from 'interactions/TextInput/directives/text-input-rules.service'; -import { AnswerClassificationService, InteractionRulesService } from 'pages/exploration-player-page/services/answer-classification.service'; -import { AlertsService } from 'services/alerts.service'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; -import { State } from 'domain/state/StateObjectFactory'; -import { ResponsesService } from '../services/responses.service'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { CurrentInteractionService } from 'pages/exploration-player-page/services/current-interaction.service'; -import { TrainingDataService } from './training-data.service'; -import { TrainingModalService } from './training-modal.service'; -import { TruncateInputBasedOnInteractionAnswerTypePipe } from 'filters/truncate-input-based-on-interaction-answer-type.pipe'; -import { InteractionAnswer } from 'interactions/answer-defs'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; +import {Subscription} from 'rxjs'; +import {Component, Injector, OnDestroy, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {StateInteractionIdService} from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; +import {StateCustomizationArgsService} from 'components/state-editor/state-editor-properties-services/state-customization-args.service'; +import {ExplorationHtmlFormatterService} from 'services/exploration-html-formatter.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {ExplorationPlayerConstants} from 'pages/exploration-player-page/exploration-player-page.constants'; +import {AngularNameService} from 'pages/exploration-editor-page/services/angular-name.service'; +import {AlgebraicExpressionInputRulesService} from 'interactions/AlgebraicExpressionInput/directives/algebraic-expression-input-rules.service'; +import {CodeReplRulesService} from 'interactions/CodeRepl/directives/code-repl-rules.service'; +import {ContinueRulesService} from 'interactions/Continue/directives/continue-rules.service'; +import {FractionInputRulesService} from 'interactions/FractionInput/directives/fraction-input-rules.service'; +import {GraphInputRulesService} from 'interactions/GraphInput/directives/graph-input-rules.service'; +import {ImageClickInputRulesService} from 'interactions/ImageClickInput/directives/image-click-input-rules.service'; +import {InteractiveMapRulesService} from 'interactions/InteractiveMap/directives/interactive-map-rules.service'; +import {MathEquationInputRulesService} from 'interactions/MathEquationInput/directives/math-equation-input-rules.service'; +import {NumericExpressionInputRulesService} from 'interactions/NumericExpressionInput/directives/numeric-expression-input-rules.service'; +import {NumericInputRulesService} from 'interactions/NumericInput/directives/numeric-input-rules.service'; +import {PencilCodeEditorRulesService} from 'interactions/PencilCodeEditor/directives/pencil-code-editor-rules.service'; +import {SetInputRulesService} from 'interactions/SetInput/directives/set-input-rules.service'; +import {TextInputRulesService} from 'interactions/TextInput/directives/text-input-rules.service'; +import { + AnswerClassificationService, + InteractionRulesService, +} from 'pages/exploration-player-page/services/answer-classification.service'; +import {AlertsService} from 'services/alerts.service'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; +import {State} from 'domain/state/StateObjectFactory'; +import {ResponsesService} from '../services/responses.service'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {CurrentInteractionService} from 'pages/exploration-player-page/services/current-interaction.service'; +import {TrainingDataService} from './training-data.service'; +import {TrainingModalService} from './training-modal.service'; +import {TruncateInputBasedOnInteractionAnswerTypePipe} from 'filters/truncate-input-based-on-interaction-answer-type.pipe'; +import {InteractionAnswer} from 'interactions/answer-defs'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; export const RULES_SERVICE_MAPPING = { AlgebraicExpressionInputRulesService: AlgebraicExpressionInputRulesService, @@ -68,18 +71,19 @@ export const RULES_SERVICE_MAPPING = { TextInputRulesService: TextInputRulesService, }; - interface TrainingData { - answer: InteractionAnswer; - answerTemplate: string; - } - - @Component({ - selector: 'training-data-editor-panel', - templateUrl: './training-data-editor-panel-modal.component.html' - }) +interface TrainingData { + answer: InteractionAnswer; + answerTemplate: string; +} +@Component({ + selector: 'training-data-editor-panel', + templateUrl: './training-data-editor-panel-modal.component.html', +}) export class TrainingDataEditorPanelComponent - extends ConfirmOrCancelModal implements OnInit, OnDestroy { + extends ConfirmOrCancelModal + implements OnInit, OnDestroy +{ directiveSubscriptions = new Subscription(); // These properties below are initialized using Angular lifecycle hooks @@ -103,23 +107,22 @@ export class TrainingDataEditorPanelComponent newAnswerOutcomeDest!: string; constructor( - private ngbActiveModal: NgbActiveModal, - private injector: Injector, - private alertsService: AlertsService, - private trainingModalService: TrainingModalService, - private stateInteractionIdService: StateInteractionIdService, - private explorationHtmlFormatterService: ExplorationHtmlFormatterService, - private stateCustomizationArgsService: StateCustomizationArgsService, - private focusManagerService: FocusManagerService, - private trainingDataService: TrainingDataService, - private angularNameService: AngularNameService, - private answerClassificationService: AnswerClassificationService, - private explorationStatesService: ExplorationStatesService, - private responsesService: ResponsesService, - private stateEditorService: StateEditorService, - private currentInteractionService: CurrentInteractionService, - private truncateInputBasedOnInteractionAnswerTypePipe: - TruncateInputBasedOnInteractionAnswerTypePipe, + private ngbActiveModal: NgbActiveModal, + private injector: Injector, + private alertsService: AlertsService, + private trainingModalService: TrainingModalService, + private stateInteractionIdService: StateInteractionIdService, + private explorationHtmlFormatterService: ExplorationHtmlFormatterService, + private stateCustomizationArgsService: StateCustomizationArgsService, + private focusManagerService: FocusManagerService, + private trainingDataService: TrainingDataService, + private angularNameService: AngularNameService, + private answerClassificationService: AnswerClassificationService, + private explorationStatesService: ExplorationStatesService, + private responsesService: ResponsesService, + private stateEditorService: StateEditorService, + private currentInteractionService: CurrentInteractionService, + private truncateInputBasedOnInteractionAnswerTypePipe: TruncateInputBasedOnInteractionAnswerTypePipe ) { super(ngbActiveModal); } @@ -131,34 +134,38 @@ export class TrainingDataEditorPanelComponent throw new Error('State name cannot be empty.'); } this._state = this.explorationStatesService.getState(this._stateName); - this.answerGroupIndex = ( - this.responsesService.getActiveAnswerGroupIndex()); + this.answerGroupIndex = this.responsesService.getActiveAnswerGroupIndex(); this.FOCUS_LABEL_TEST_INTERACTION_INPUT = 'testInteractionInput'; this.stateContent = this._state.content.html; this.trainingData = []; - this.answerGroupHasNonEmptyRules = ( - this.responsesService.getAnswerGroup( - this.answerGroupIndex).rules.length > 0); - this.inputTemplate = ( + this.answerGroupHasNonEmptyRules = + this.responsesService.getAnswerGroup(this.answerGroupIndex).rules.length > + 0; + this.inputTemplate = this.explorationHtmlFormatterService.getInteractionHtml( this.stateInteractionIdService.savedMemento, this.stateCustomizationArgsService.savedMemento, - false, this.FOCUS_LABEL_TEST_INTERACTION_INPUT, null)); + false, + this.FOCUS_LABEL_TEST_INTERACTION_INPUT, + null + ); this.directiveSubscriptions.add( this.trainingModalService.onFinishTrainingCallback.subscribe( - (finishTrainingResult) => { - let truncatedAnswer = ( + finishTrainingResult => { + let truncatedAnswer = this.truncateInputBasedOnInteractionAnswerTypePipe.transform( finishTrainingResult.answer, - finishTrainingResult.interactionId, 12)); - let successToast = ( - 'The answer ' + truncatedAnswer + - ' has been successfully trained.'); - this.alertsService.addSuccessMessage( - successToast, 1000); + finishTrainingResult.interactionId, + 12 + ); + let successToast = + 'The answer ' + truncatedAnswer + ' has been successfully trained.'; + this.alertsService.addSuccessMessage(successToast, 1000); this._rebuildTrainingData(); - })); + } + ) + ); this.currentInteractionService.setOnSubmitFn(this.submitAnswer); this.init(); @@ -178,42 +185,51 @@ export class TrainingDataEditorPanelComponent // data answers if there are no rules and only one training data // answer is present. - if ((this.answerGroupHasNonEmptyRules && - this.trainingData.length > 0) && this.trainingData.length > 1) { + if ( + this.answerGroupHasNonEmptyRules && + this.trainingData.length > 0 && + this.trainingData.length > 1 + ) { let answer = this.trainingData[answerIndex].answer; let interactionId = this.stateInteractionIdService.savedMemento; this.trainingModalService.openTrainUnresolvedAnswerModal( - answer, interactionId, answerIndex); + answer, + interactionId, + answerIndex + ); } } _rebuildTrainingData(): void { this.trainingData = []; - this.trainingDataService.getTrainingDataOfAnswerGroup( - this.answerGroupIndex).forEach((answer) => { - let answerTemplate = ( - this.explorationHtmlFormatterService.getAnswerHtml( - answer, this.stateInteractionIdService.savedMemento, - this.stateCustomizationArgsService.savedMemento)); - this.trainingData.push({ - answer: answer, - answerTemplate: answerTemplate + this.trainingDataService + .getTrainingDataOfAnswerGroup(this.answerGroupIndex) + .forEach(answer => { + let answerTemplate = this.explorationHtmlFormatterService.getAnswerHtml( + answer, + this.stateInteractionIdService.savedMemento, + this.stateCustomizationArgsService.savedMemento + ); + this.trainingData.push({ + answer: answer, + answerTemplate: answerTemplate, + }); }); - }); } init(): void { this._rebuildTrainingData(); this.newAnswerIsAlreadyResolved = false; this.answerSuccessfullyAdded = false; - this.focusManagerService.setFocus( - this.FOCUS_LABEL_TEST_INTERACTION_INPUT); + this.focusManagerService.setFocus(this.FOCUS_LABEL_TEST_INTERACTION_INPUT); } removeAnswerFromTrainingData(answerIndex: number): void { let answer = this.trainingData[answerIndex].answer; this.trainingDataService.removeAnswerFromAnswerGroupTrainingData( - answer, this.answerGroupIndex); + answer, + this.answerGroupIndex + ); this.trainingData.splice(answerIndex, 1); } @@ -227,26 +243,31 @@ export class TrainingDataEditorPanelComponent let interactionId = this.stateInteractionIdService.savedMemento; let rulesServiceName = - this.angularNameService.getNameOfInteractionRulesService( - interactionId); + this.angularNameService.getNameOfInteractionRulesService(interactionId); // Inject RulesService dynamically. - let rulesService = ( - this.injector.get(RULES_SERVICE_MAPPING[ + let rulesService = this.injector.get( + RULES_SERVICE_MAPPING[ rulesServiceName as keyof typeof RULES_SERVICE_MAPPING - ]) as InteractionRulesService); + ] + ) as InteractionRulesService; - let newAnswerTemplate = ( - this.explorationHtmlFormatterService.getAnswerHtml( - newAnswer, this.stateInteractionIdService.savedMemento, - this.stateCustomizationArgsService.savedMemento)); + let newAnswerTemplate = this.explorationHtmlFormatterService.getAnswerHtml( + newAnswer, + this.stateInteractionIdService.savedMemento, + this.stateCustomizationArgsService.savedMemento + ); if (!this._stateName) { throw new Error('State name cannot be empty.'); } - let classificationResult = ( + let classificationResult = this.answerClassificationService.getMatchingClassificationResult( - this._stateName, this._state.interaction, newAnswer, rulesService)); + this._stateName, + this._state.interaction, + newAnswer, + rulesService + ); let newAnswerOutcomeDest = classificationResult.outcome.dest; let newAnswerFeedback = classificationResult.outcome.feedback; if (newAnswerOutcomeDest === this._stateName) { @@ -257,32 +278,37 @@ export class TrainingDataEditorPanelComponent this.newAnswerFeedback = newAnswerFeedback; this.newAnswerOutcomeDest = newAnswerOutcomeDest; - let classificationType = ( - classificationResult.classificationCategorization); + let classificationType = classificationResult.classificationCategorization; - if (( + if ( + classificationType === + ExplorationPlayerConstants.EXPLICIT_CLASSIFICATION || classificationType === - ExplorationPlayerConstants.EXPLICIT_CLASSIFICATION) || - (classificationType === - ExplorationPlayerConstants.TRAINING_DATA_CLASSIFICATION)) { + ExplorationPlayerConstants.TRAINING_DATA_CLASSIFICATION + ) { this.newAnswerIsAlreadyResolved = true; } else { this.trainingDataService.associateWithAnswerGroup( - this.answerGroupIndex, newAnswer); - let truncatedAnswer = ( + this.answerGroupIndex, + newAnswer + ); + let truncatedAnswer = this.truncateInputBasedOnInteractionAnswerTypePipe.transform( - newAnswer, interactionId, 12)); - let successToast = ( - 'The answer ' + truncatedAnswer + - ' has been successfully trained.'); - this.alertsService.addSuccessMessage( - successToast, 1000); + newAnswer, + interactionId, + 12 + ); + let successToast = + 'The answer ' + truncatedAnswer + ' has been successfully trained.'; + this.alertsService.addSuccessMessage(successToast, 1000); this._rebuildTrainingData(); } } } -angular.module('oppia').directive('trainingDataEditorPanel', - downgradeComponent({ - component: TrainingDataEditorPanelComponent - }) as angular.IDirectiveFactory); +angular.module('oppia').directive( + 'trainingDataEditorPanel', + downgradeComponent({ + component: TrainingDataEditorPanelComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-data-editor-panel.service.spec.ts b/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-data-editor-panel.service.spec.ts index df0a4dc7a11e..4e617631abaf 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-data-editor-panel.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-data-editor-panel.service.spec.ts @@ -16,12 +16,12 @@ * @fileoverview Unit tests for TrainingDataEditorPanelService. */ -import { TestBed } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { AlertsService } from 'services/alerts.service'; -import { ExternalSaveService } from 'services/external-save.service'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { TrainingDataEditorPanelService } from './training-data-editor-panel.service'; +import {TestBed} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {AlertsService} from 'services/alerts.service'; +import {ExternalSaveService} from 'services/external-save.service'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {TrainingDataEditorPanelService} from './training-data-editor-panel.service'; describe('Training Modal Service', () => { let trainingDataEditorPanelService: TrainingDataEditorPanelService; @@ -36,32 +36,31 @@ describe('Training Modal Service', () => { TrainingDataEditorPanelService, AlertsService, ExternalSaveService, - NgbModal - ] + NgbModal, + ], }); trainingDataEditorPanelService = TestBed.inject( - TrainingDataEditorPanelService); + TrainingDataEditorPanelService + ); alertsService = TestBed.inject(AlertsService); ngbModal = TestBed.inject(NgbModal); externalSaveService = TestBed.inject(ExternalSaveService); }); it('should open NgbModal', () => { - spyOn(alertsService, 'clearWarnings') - .and.stub(); + spyOn(alertsService, 'clearWarnings').and.stub(); spyOn(ngbModal, 'open').and.callFake(() => { - return ({ - result: Promise.resolve() - } as NgbModalRef); + return { + result: Promise.resolve(), + } as NgbModalRef; }); spyOn(externalSaveService.onExternalSave, 'emit').and.stub(); trainingDataEditorPanelService.openTrainingDataEditor(); expect(alertsService.clearWarnings).toHaveBeenCalled(); - expect( - externalSaveService.onExternalSave.emit).toHaveBeenCalled(); + expect(externalSaveService.onExternalSave.emit).toHaveBeenCalled(); expect(ngbModal.open).toHaveBeenCalled(); }); }); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-data-editor-panel.service.ts b/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-data-editor-panel.service.ts index 6bebd34083c9..2574c5ffa496 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-data-editor-panel.service.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-data-editor-panel.service.ts @@ -17,15 +17,15 @@ * the training data editor of an answer group. */ -import { TrainingDataEditorPanelComponent } from './training-data-editor-panel-modal.component'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { AlertsService } from 'services/alerts.service'; -import { ExternalSaveService } from 'services/external-save.service'; +import {TrainingDataEditorPanelComponent} from './training-data-editor-panel-modal.component'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {AlertsService} from 'services/alerts.service'; +import {ExternalSaveService} from 'services/external-save.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class TrainingDataEditorPanelService { constructor( @@ -40,14 +40,23 @@ export class TrainingDataEditorPanelService { openTrainingDataEditor(): void { this.alertsService.clearWarnings(); - this.ngbModal.open(TrainingDataEditorPanelComponent, { - backdrop: 'static', - }).result.then(() => {}, () => {}); + this.ngbModal + .open(TrainingDataEditorPanelComponent, { + backdrop: 'static', + }) + .result.then( + () => {}, + () => {} + ); // Save the modified training data externally in state content. this.externalSaveService.onExternalSave.emit(); } } -angular.module('oppia').factory('TrainingDataEditorPanelService', - downgradeInjectable(TrainingDataEditorPanelService)); +angular + .module('oppia') + .factory( + 'TrainingDataEditorPanelService', + downgradeInjectable(TrainingDataEditorPanelService) + ); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-data.service.spec.ts b/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-data.service.spec.ts index cee254082696..a2887e83087a 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-data.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-data.service.spec.ts @@ -16,39 +16,43 @@ * @fileoverview Unit tests for the training data service. */ -import { fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { ExplorationDataService } from 'pages/exploration-editor-page/services/exploration-data.service'; -import { TrainingDataService } from './training-data.service'; -import { ResponsesService } from '../services/responses.service'; -import { AnswerGroup } from 'domain/exploration/AnswerGroupObjectFactory'; -import { Rule } from 'domain/exploration/rule.model'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { State } from 'domain/state/StateObjectFactory'; -import { Interaction } from 'domain/exploration/InteractionObjectFactory'; -import { Outcome } from 'domain/exploration/OutcomeObjectFactory'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; +import {fakeAsync, TestBed, tick} from '@angular/core/testing'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {ExplorationDataService} from 'pages/exploration-editor-page/services/exploration-data.service'; +import {TrainingDataService} from './training-data.service'; +import {ResponsesService} from '../services/responses.service'; +import {AnswerGroup} from 'domain/exploration/AnswerGroupObjectFactory'; +import {Rule} from 'domain/exploration/rule.model'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {State} from 'domain/state/StateObjectFactory'; +import {Interaction} from 'domain/exploration/InteractionObjectFactory'; +import {Outcome} from 'domain/exploration/OutcomeObjectFactory'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; class MockNgbModal { open() { return { - result: Promise.resolve() + result: Promise.resolve(), }; } } class MockResponsesService { AnswerGroupArray = [ - new AnswerGroup([ - new Rule('TextInput', null, null), - new Rule('TextInput', null, null) - ], null, ['trainingData 1'], null), - new AnswerGroup([ - new Rule('TextInput', null, null), - new Rule('TextInput', null, null) - ], null, ['trainingData 1', 'trainingData 2'], null) + new AnswerGroup( + [new Rule('TextInput', null, null), new Rule('TextInput', null, null)], + null, + ['trainingData 1'], + null + ), + new AnswerGroup( + [new Rule('TextInput', null, null), new Rule('TextInput', null, null)], + null, + ['trainingData 1', 'trainingData 2'], + null + ), ]; getAnswerGroup(index: number) { @@ -60,22 +64,18 @@ class MockResponsesService { } updateAnswerGroup( - item1: string, - item2: string, - item3: (arg0: string) => void + item1: string, + item2: string, + item3: (arg0: string) => void ) { item3(null); } - save( - item1: string, - item2: string, - item3: (arg0: string) => void - ) { + save(item1: string, item2: string, item3: (arg0: string) => void) { item3(null); } - updateConfirmedUnclassifiedAnswers(item1: string) { } + updateConfirmedUnclassifiedAnswers(item1: string) {} getConfirmedUnclassifiedAnswers() { return ['answer1', 'answer2']; } @@ -86,17 +86,11 @@ class MockResponsesService { } class MockExplorationStatesService { - saveInteractionAnswerGroups( - item1: string, item2: string - ) { } + saveInteractionAnswerGroups(item1: string, item2: string) {} - saveInteractionDefaultOutcome( - item1: string, item2: string - ) { } + saveInteractionDefaultOutcome(item1: string, item2: string) {} - saveConfirmedUnclassifiedAnswers( - item1: string, item2: string - ) { } + saveConfirmedUnclassifiedAnswers(item1: string, item2: string) {} } class MockStateEditorService { @@ -115,7 +109,7 @@ describe('Training Data Service', () => { providers: [ { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, { provide: ExplorationDataService, @@ -123,69 +117,82 @@ describe('Training Data Service', () => { explorationId: 0, autosaveChangeListAsync() { return; - } - } + }, + }, }, TrainingDataService, { provide: ExplorationStatesService, - useClass: MockExplorationStatesService + useClass: MockExplorationStatesService, }, { provide: StateEditorService, - useClass: MockStateEditorService + useClass: MockStateEditorService, }, { provide: ResponsesService, - useClass: MockResponsesService - } - ] + useClass: MockResponsesService, + }, + ], }); responsesService = TestBed.inject(ResponsesService); trainingDataService = TestBed.inject(TrainingDataService); }); - it('should be able to train answer groups and the default response', - () => { - spyOn(responsesService, 'updateAnswerGroup'); + it('should be able to train answer groups and the default response', () => { + spyOn(responsesService, 'updateAnswerGroup'); + + // Training the first answer of a group should add a new classifier. + trainingDataService.associateWithAnswerGroup(0, 'answer1'); - // Training the first answer of a group should add a new classifier. - trainingDataService.associateWithAnswerGroup(0, 'answer1'); + expect(responsesService.updateAnswerGroup).toHaveBeenCalled(); + }); - expect(responsesService.updateAnswerGroup).toHaveBeenCalled(); + it( + 'should be able to retrain answers between answer groups and the ' + + 'default outcome', + () => { + // Retraining an answer from the answer group to the default outcome + // should remove it from the first, then add it to the second. + trainingDataService.associateWithAnswerGroup(0, 'text answer'); + trainingDataService.associateWithAnswerGroup(0, 'second answer'); + trainingDataService.associateWithDefaultResponse('third answer'); + + expect(trainingDataService.getTrainingDataOfAnswerGroup(0)).toEqual([ + 'trainingData 1', + 'text answer', + 'second answer', + ]); + + // Try to retrain the second answer (answer group -> default response). + trainingDataService.associateWithDefaultResponse('second answer'); + expect(trainingDataService.getTrainingDataOfAnswerGroup(0)).toEqual([ + 'trainingData 1', + 'text answer', + 'second answer', + ]); + + // Try to retrain the third answer (default response -> answer group). + trainingDataService.associateWithAnswerGroup(0, 'third answer'); + + expect(trainingDataService.getTrainingDataOfAnswerGroup(0)).toEqual([ + 'trainingData 1', + 'text answer', + 'second answer', + 'third answer', + ]); } ); - it('should be able to retrain answers between answer groups and the ' + - 'default outcome', () => { - // Retraining an answer from the answer group to the default outcome - // should remove it from the first, then add it to the second. - trainingDataService.associateWithAnswerGroup(0, 'text answer'); - trainingDataService.associateWithAnswerGroup(0, 'second answer'); - trainingDataService.associateWithDefaultResponse('third answer'); - - expect(trainingDataService.getTrainingDataOfAnswerGroup(0)).toEqual( - ['trainingData 1', 'text answer', 'second answer']); - - // Try to retrain the second answer (answer group -> default response). - trainingDataService.associateWithDefaultResponse('second answer'); - expect(trainingDataService.getTrainingDataOfAnswerGroup(0)).toEqual( - ['trainingData 1', 'text answer', 'second answer']); - - // Try to retrain the third answer (default response -> answer group). - trainingDataService.associateWithAnswerGroup(0, 'third answer'); - - expect(trainingDataService.getTrainingDataOfAnswerGroup(0)).toEqual( - ['trainingData 1', 'text answer', 'second answer', 'third answer']); - }); - it('should not be able to train duplicated answers', () => { trainingDataService.associateWithAnswerGroup(0, 'trainingData 2'); trainingDataService.associateWithDefaultResponse('second answer'); - expect(trainingDataService.getTrainingDataOfAnswerGroup(0)).toEqual( - ['trainingData 1', 'trainingData 2']); + expect(trainingDataService.getTrainingDataOfAnswerGroup(0)).toEqual([ + 'trainingData 1', + 'trainingData 2', + ]); // Training a duplicate answer for the answer group should change nothing. trainingDataService.associateWithAnswerGroup(0, 'trainingData 3'); @@ -194,8 +201,11 @@ describe('Training Data Service', () => { // nothing. trainingDataService.associateWithDefaultResponse('second answer'); - expect(trainingDataService.getTrainingDataOfAnswerGroup(0)).toEqual( - ['trainingData 1', 'trainingData 2', 'trainingData 3']); + expect(trainingDataService.getTrainingDataOfAnswerGroup(0)).toEqual([ + 'trainingData 1', + 'trainingData 2', + 'trainingData 3', + ]); }); it('should get all potential outcomes of an interaction', () => { @@ -205,79 +215,131 @@ describe('Training Data Service', () => { // type 'Outcome'." We need to suppress this error because of the need to // test validations. This throws an error because the outcome is null. // @ts-ignore - expect(trainingDataService.getAllPotentialOutcomes( - new State( - 'State', 'id', 'some', null, - new Interaction([ - new AnswerGroup([ - new Rule('TextInput', null, null), - new Rule('TextInput', null, null) - ], null, ['trainingData 1'], null), - new AnswerGroup([ - new Rule('TextInput', null, null), - new Rule('TextInput', null, null) - ], null, ['trainingData 1'], null) - ], [], null, new Outcome( - 'Hola', + expect( + trainingDataService.getAllPotentialOutcomes( + new State( + 'State', + 'id', + 'some', null, - new SubtitledHtml('

HTML string

', 'Id'), - false, - [], + new Interaction( + [ + new AnswerGroup( + [ + new Rule('TextInput', null, null), + new Rule('TextInput', null, null), + ], + null, + ['trainingData 1'], + null + ), + new AnswerGroup( + [ + new Rule('TextInput', null, null), + new Rule('TextInput', null, null), + ], + null, + ['trainingData 1'], + null + ), + ], + [], + null, + new Outcome( + 'Hola', + null, + new SubtitledHtml('

HTML string

', 'Id'), + false, + [], + null, + null + ), + [], + 'id', + null + ), null, null, - ), [], 'id', null), - null, null, true, null) - )).toEqual([null, null, new Outcome( - 'Hola', - null, - new SubtitledHtml('

HTML string

', 'Id'), - false, - [], + true, + null + ) + ) + ).toEqual([ null, null, - )]); - }); - - it('should remove answer from training data associated with given answer ' + - 'group', () => { - trainingDataService.associateWithAnswerGroup(0, 'text answer'); - trainingDataService.associateWithAnswerGroup(0, 'second answer'); - trainingDataService.associateWithAnswerGroup(0, 'second answer'); - - trainingDataService - .removeAnswerFromAnswerGroupTrainingData('second answer', 0); - - expect(trainingDataService.getTrainingDataOfAnswerGroup(0)).toEqual( - ['trainingData 1', 'text answer', 'second answer', 'second answer']); + new Outcome( + 'Hola', + null, + new SubtitledHtml('

HTML string

', 'Id'), + false, + [], + null, + null + ), + ]); }); - it('should correctly check whether answer is in confirmed unclassified ' + - 'answers', fakeAsync(() => { - trainingDataService.associateWithAnswerGroup(0, 'text answer'); - trainingDataService.associateWithAnswerGroup(0, 'second answer'); - trainingDataService.associateWithDefaultResponse('second answer'); - tick(); + it( + 'should remove answer from training data associated with given answer ' + + 'group', + () => { + trainingDataService.associateWithAnswerGroup(0, 'text answer'); + trainingDataService.associateWithAnswerGroup(0, 'second answer'); + trainingDataService.associateWithAnswerGroup(0, 'second answer'); + + trainingDataService.removeAnswerFromAnswerGroupTrainingData( + 'second answer', + 0 + ); + + expect(trainingDataService.getTrainingDataOfAnswerGroup(0)).toEqual([ + 'trainingData 1', + 'text answer', + 'second answer', + 'second answer', + ]); + } + ); - expect(trainingDataService.isConfirmedUnclassifiedAnswer('text answer')) - .toBe(false); - expect(trainingDataService.getTrainingDataOfAnswerGroup(0)).toEqual( - ['trainingData 1', 'text answer', 'second answer']); - })); + it( + 'should correctly check whether answer is in confirmed unclassified ' + + 'answers', + fakeAsync(() => { + trainingDataService.associateWithAnswerGroup(0, 'text answer'); + trainingDataService.associateWithAnswerGroup(0, 'second answer'); + trainingDataService.associateWithDefaultResponse('second answer'); + tick(); + + expect( + trainingDataService.isConfirmedUnclassifiedAnswer('text answer') + ).toBe(false); + expect(trainingDataService.getTrainingDataOfAnswerGroup(0)).toEqual([ + 'trainingData 1', + 'text answer', + 'second answer', + ]); + }) + ); it('should get all the training data answers', () => { trainingDataService.associateWithAnswerGroup(0, 'text answer'); trainingDataService.associateWithAnswerGroup(0, 'second answer'); trainingDataService.associateWithDefaultResponse('second answer'); - expect(trainingDataService.getTrainingDataAnswers()).toEqual([{ - answerGroupIndex: 0, - answers: ['trainingData 1', 'text answer', 'second answer'] - }, - { - answerGroupIndex: 1, - answers: ['trainingData 1', 'trainingData 2'] - }]); - expect(trainingDataService.getTrainingDataOfAnswerGroup(0)).toEqual( - ['trainingData 1', 'text answer', 'second answer']); + expect(trainingDataService.getTrainingDataAnswers()).toEqual([ + { + answerGroupIndex: 0, + answers: ['trainingData 1', 'text answer', 'second answer'], + }, + { + answerGroupIndex: 1, + answers: ['trainingData 1', 'trainingData 2'], + }, + ]); + expect(trainingDataService.getTrainingDataOfAnswerGroup(0)).toEqual([ + 'trainingData 1', + 'text answer', + 'second answer', + ]); }); }); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-data.service.ts b/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-data.service.ts index 5b7dad7b01df..35cb81297c8b 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-data.service.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-data.service.ts @@ -18,17 +18,17 @@ * across all answer groups. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; -import { GraphDataService } from 'pages/exploration-editor-page/services/graph-data.service'; -import { ResponsesService } from 'pages/exploration-editor-page/editor-tab/services/responses.service'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; +import {GraphDataService} from 'pages/exploration-editor-page/services/graph-data.service'; +import {ResponsesService} from 'pages/exploration-editor-page/editor-tab/services/responses.service'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; import cloneDeep from 'lodash/cloneDeep'; -import { AnswerGroup } from 'domain/exploration/AnswerGroupObjectFactory'; -import { InteractionAnswer } from 'interactions/answer-defs'; -import { State } from 'domain/state/StateObjectFactory'; -import { Outcome } from 'domain/exploration/OutcomeObjectFactory'; +import {AnswerGroup} from 'domain/exploration/AnswerGroupObjectFactory'; +import {InteractionAnswer} from 'interactions/answer-defs'; +import {State} from 'domain/state/StateObjectFactory'; +import {Outcome} from 'domain/exploration/OutcomeObjectFactory'; interface AnswerGroupData { answerGroupIndex: number; @@ -36,18 +36,20 @@ interface AnswerGroupData { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class TrainingDataService { constructor( private explorationStatesService: ExplorationStatesService, private graphDataService: GraphDataService, private responsesService: ResponsesService, - private stateEditorService: StateEditorService, - ) { } + private stateEditorService: StateEditorService + ) {} _getIndexOfTrainingData( - answer: InteractionAnswer, trainingData: InteractionAnswer[]): number { + answer: InteractionAnswer, + trainingData: InteractionAnswer[] + ): number { let index = -1; for (let i = 0; i < trainingData.length; i++) { if (trainingData[i] === answer) { @@ -62,7 +64,9 @@ export class TrainingDataService { // function returns the index of the answer that was removed if it was // successfully removed from the training data, or -1 otherwise. _removeAnswerFromTrainingData( - answer: InteractionAnswer, trainingData: InteractionAnswer[]): number { + answer: InteractionAnswer, + trainingData: InteractionAnswer[] + ): number { let index = this._getIndexOfTrainingData(answer, trainingData); if (index !== -1) { trainingData.slice(index, 1); @@ -76,8 +80,9 @@ export class TrainingDataService { // show up again. _removeAnswer(answer: InteractionAnswer): void { let answerGroups = this.responsesService.getAnswerGroups(); - let confirmedUnclassifiedAnswers = [...( - this.responsesService.getConfirmedUnclassifiedAnswers())]; + let confirmedUnclassifiedAnswers = [ + ...this.responsesService.getConfirmedUnclassifiedAnswers(), + ]; let updatedAnswerGroups = false; let updatedConfirmedUnclassifiedAnswers = false; @@ -85,41 +90,54 @@ export class TrainingDataService { for (let i = 0; i < answerGroups.length; i++) { let answerGroup = answerGroups[i]; let trainingData = answerGroup.trainingData; - if (trainingData && - this._removeAnswerFromTrainingData( - answer, [...trainingData]) !== -1) { + if ( + trainingData && + this._removeAnswerFromTrainingData(answer, [...trainingData]) !== -1 + ) { updatedAnswerGroups = true; } } // Remove the answer from the confirmed unclassified answers. - updatedConfirmedUnclassifiedAnswers = (this._removeAnswerFromTrainingData( - answer, confirmedUnclassifiedAnswers) !== -1); + updatedConfirmedUnclassifiedAnswers = + this._removeAnswerFromTrainingData( + answer, + confirmedUnclassifiedAnswers + ) !== -1; if (updatedAnswerGroups) { this.responsesService.save( - answerGroups, this.responsesService.getDefaultOutcome(), + answerGroups, + this.responsesService.getDefaultOutcome(), (newAnswerGroups, newDefaultOutcome) => { let stateName = this.stateEditorService.getActiveStateName(); if (stateName) { this.explorationStatesService.saveInteractionAnswerGroups( - stateName, cloneDeep(newAnswerGroups)); + stateName, + cloneDeep(newAnswerGroups) + ); this.explorationStatesService.saveInteractionDefaultOutcome( - stateName, cloneDeep(newDefaultOutcome) as Outcome); + stateName, + cloneDeep(newDefaultOutcome) as Outcome + ); } this.graphDataService.recompute(); - }); + } + ); } if (updatedConfirmedUnclassifiedAnswers) { this.responsesService.updateConfirmedUnclassifiedAnswers( - confirmedUnclassifiedAnswers); + confirmedUnclassifiedAnswers + ); let stateName = this.stateEditorService.getActiveStateName(); if (stateName) { this.explorationStatesService.saveConfirmedUnclassifiedAnswers( - stateName, confirmedUnclassifiedAnswers); + stateName, + confirmedUnclassifiedAnswers + ); } } } @@ -132,7 +150,7 @@ export class TrainingDataService { let answerGroup = answerGroups[i]; trainingDataAnswers.push({ answerGroupIndex: i, - answers: answerGroup.trainingData + answers: answerGroup.trainingData, }); } return trainingDataAnswers; @@ -140,7 +158,8 @@ export class TrainingDataService { getTrainingDataOfAnswerGroup(answerGroupIndex: number): InteractionAnswer[] { return [ - ...this.responsesService.getAnswerGroup(answerGroupIndex).trainingData]; + ...this.responsesService.getAnswerGroup(answerGroupIndex).trainingData, + ]; } getAllPotentialOutcomes(state: State): Outcome[] { @@ -159,7 +178,9 @@ export class TrainingDataService { } associateWithAnswerGroup( - answerGroupIndex: number, answer: InteractionAnswer): void { + answerGroupIndex: number, + answer: InteractionAnswer + ): void { // Remove answer from traning data of any answer group or // confirmed unclassified answers. this._removeAnswer(answer); @@ -168,21 +189,25 @@ export class TrainingDataService { let answerGroup: AnswerGroup = answerGroups[answerGroupIndex]; // Train the rule to include this answer. - answerGroup.trainingData = [ - ...answerGroup.trainingData, - answer]; + answerGroup.trainingData = [...answerGroup.trainingData, answer]; - this.responsesService.updateAnswerGroup(answerGroupIndex, { - trainingData: answerGroup.trainingData - } as AnswerGroup, (newAnswerGroups) => { - let stateName = this.stateEditorService.getActiveStateName(); - if (stateName) { - this.explorationStatesService.saveInteractionAnswerGroups( - stateName, newAnswerGroups); - } + this.responsesService.updateAnswerGroup( + answerGroupIndex, + { + trainingData: answerGroup.trainingData, + } as AnswerGroup, + newAnswerGroups => { + let stateName = this.stateEditorService.getActiveStateName(); + if (stateName) { + this.explorationStatesService.saveInteractionAnswerGroups( + stateName, + newAnswerGroups + ); + } - this.graphDataService.recompute(); - }); + this.graphDataService.recompute(); + } + ); } associateWithDefaultResponse(answer: InteractionAnswer): void { @@ -190,47 +215,62 @@ export class TrainingDataService { // confirmed unclassified answers. this._removeAnswer(answer); - let confirmedUnclassifiedAnswers = [...( - this.responsesService.getConfirmedUnclassifiedAnswers())]; + let confirmedUnclassifiedAnswers = [ + ...this.responsesService.getConfirmedUnclassifiedAnswers(), + ]; confirmedUnclassifiedAnswers.push(answer); this.responsesService.updateConfirmedUnclassifiedAnswers( - confirmedUnclassifiedAnswers); + confirmedUnclassifiedAnswers + ); let stateName = this.stateEditorService.getActiveStateName(); if (stateName) { this.explorationStatesService.saveConfirmedUnclassifiedAnswers( - stateName, confirmedUnclassifiedAnswers); + stateName, + confirmedUnclassifiedAnswers + ); } } isConfirmedUnclassifiedAnswer(answer: InteractionAnswer): boolean { - return (this._getIndexOfTrainingData( - answer, - [...this.responsesService.getConfirmedUnclassifiedAnswers()]) !== -1); + return ( + this._getIndexOfTrainingData(answer, [ + ...this.responsesService.getConfirmedUnclassifiedAnswers(), + ]) !== -1 + ); } removeAnswerFromAnswerGroupTrainingData( - answer: InteractionAnswer, answerGroupIndex: number): void { - let trainingData = [...this.responsesService.getAnswerGroup( - answerGroupIndex).trainingData]; + answer: InteractionAnswer, + answerGroupIndex: number + ): void { + let trainingData = [ + ...this.responsesService.getAnswerGroup(answerGroupIndex).trainingData, + ]; this._removeAnswerFromTrainingData(answer, trainingData); let answerGroups = this.responsesService.getAnswerGroups(); answerGroups[answerGroupIndex].trainingData = trainingData; this.responsesService.updateAnswerGroup( - answerGroupIndex, {} as AnswerGroup, (newAnswerGroups) => { + answerGroupIndex, + {} as AnswerGroup, + newAnswerGroups => { let stateName = this.stateEditorService.getActiveStateName(); if (stateName) { this.explorationStatesService.saveInteractionAnswerGroups( - stateName, newAnswerGroups); + stateName, + newAnswerGroups + ); } this.graphDataService.recompute(); - }); + } + ); } } -angular.module('oppia').factory( - 'TrainingDataService', downgradeInjectable(TrainingDataService)); +angular + .module('oppia') + .factory('TrainingDataService', downgradeInjectable(TrainingDataService)); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-modal.component.spec.ts index f1e6f86fb3e4..2e83ff9360db 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-modal.component.spec.ts @@ -16,23 +16,25 @@ * @fileoverview Unit tests for TrainingModalController. */ -import { ExplorationDataService } from 'pages/exploration-editor-page/services/exploration-data.service'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { TrainingModalComponent } from './training-modal.component'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { StateInteractionIdService } from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; -import { ResponsesService } from '../services/responses.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { Outcome } from 'domain/exploration/OutcomeObjectFactory'; -import { TrainingDataService } from './training-data.service'; -import { AnswerGroup, AnswerGroupObjectFactory } from 'domain/exploration/AnswerGroupObjectFactory'; -import { AnswerClassificationService } from 'pages/exploration-player-page/services/answer-classification.service'; -import { GraphDataService } from 'pages/exploration-editor-page/services/graph-data.service'; -import { ExplorationWarningsService } from 'pages/exploration-editor-page/services/exploration-warnings.service'; - +import {ExplorationDataService} from 'pages/exploration-editor-page/services/exploration-data.service'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {TrainingModalComponent} from './training-modal.component'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {StateInteractionIdService} from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; +import {ResponsesService} from '../services/responses.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {Outcome} from 'domain/exploration/OutcomeObjectFactory'; +import {TrainingDataService} from './training-data.service'; +import { + AnswerGroup, + AnswerGroupObjectFactory, +} from 'domain/exploration/AnswerGroupObjectFactory'; +import {AnswerClassificationService} from 'pages/exploration-player-page/services/answer-classification.service'; +import {GraphDataService} from 'pages/exploration-editor-page/services/graph-data.service'; +import {ExplorationWarningsService} from 'pages/exploration-editor-page/services/exploration-warnings.service'; class MockActiveModal { close(): void { @@ -49,15 +51,13 @@ class MockStateInteractionIdService { } class MockExplorationStatesService { - saveInteractionAnswerGroups(item1, item2) { - } + saveInteractionAnswerGroups(item1, item2) {} - saveInteractionDefaultOutcome(item1, item2) { - } + saveInteractionDefaultOutcome(item1, item2) {} getState() { return { - interaction: 'TextInput' + interaction: 'TextInput', }; } } @@ -72,7 +72,7 @@ class MockAnswerClassificationService { getMatchingClassificationResult() { return { answerGroupIndex: 2, - outcome: null + outcome: null, }; } } @@ -89,12 +89,8 @@ describe('Training Modal Component', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - ], - declarations: [ - TrainingModalComponent - ], + imports: [HttpClientTestingModule], + declarations: [TrainingModalComponent], providers: [ { provide: ExplorationDataService, @@ -102,36 +98,36 @@ describe('Training Modal Component', () => { explorationId: 0, autosaveChangeListAsync() { return; - } - } + }, + }, }, { provide: NgbActiveModal, - useClass: MockActiveModal + useClass: MockActiveModal, }, { provide: StateInteractionIdService, - useClass: MockStateInteractionIdService + useClass: MockStateInteractionIdService, }, { provide: StateEditorService, - useClass: MockStateEditorService + useClass: MockStateEditorService, }, { provide: ExplorationStatesService, - useClass: MockExplorationStatesService + useClass: MockExplorationStatesService, }, { provide: AnswerClassificationService, - useClass: MockAnswerClassificationService + useClass: MockAnswerClassificationService, }, AnswerGroupObjectFactory, TrainingDataService, ResponsesService, ExplorationWarningsService, - GraphDataService + GraphDataService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }); }); @@ -157,73 +153,69 @@ describe('Training Modal Component', () => { expect(ngbActiveModal.close).toHaveBeenCalled(); }); - it('should click on confirm button when ' + - 'answerGroupIndex is greater than Response', () => { - component.classification = { - answerGroupIndex: 2, - newOutcome: new Outcome( - 'dest', null, null, true, - [], - null, null - ) - }; - component.unhandledAnswer = 'string'; - - spyOn(answerGroupObjectFactory, 'createNew').and.returnValue(null); - spyOn(trainingDataService, 'associateWithAnswerGroup').and.stub(); - spyOn(responsesService, 'getAnswerGroupCount') - .and.returnValue(1); - spyOn(responsesService, 'getAnswerGroups') - .and.returnValue([{}] as AnswerGroup[]); - spyOn(responsesService, 'save') - .and.callFake((answerGroups, getDefaultOutcome, save) => { - save(null, null); - }); - - component.ngOnInit(); - component.onConfirm(); - - expect(responsesService.save).toHaveBeenCalled(); - expect(ngbActiveModal.close).toHaveBeenCalled(); - }); - - it('should click on confirm button when ' + - 'answerGroupIndex is greater than Response', () => { - component.classification = { - answerGroupIndex: 1, - newOutcome: new Outcome( - 'dest', null, null, true, - [], - null, null - ) - }; - component.unhandledAnswer = 'string'; - - spyOn(trainingDataService, 'associateWithDefaultResponse').and.stub(); - spyOn(responsesService, 'getAnswerGroupCount') - .and.returnValue(1); - - component.onConfirm(); - expect(ngbActiveModal.close).toHaveBeenCalled(); - }); - - it('should click on confirm button when ' + - 'answerGroupIndex is less than Response', () => { - component.classification = { - answerGroupIndex: 1, - newOutcome: new Outcome( - 'dest', null, null, true, - [], - null, null - ) - }; - component.unhandledAnswer = 'string'; - - spyOn(trainingDataService, 'associateWithAnswerGroup').and.stub(); - spyOn(responsesService, 'getAnswerGroupCount') - .and.returnValue(3); - - component.onConfirm(); - expect(ngbActiveModal.close).toHaveBeenCalled(); - }); + it( + 'should click on confirm button when ' + + 'answerGroupIndex is greater than Response', + () => { + component.classification = { + answerGroupIndex: 2, + newOutcome: new Outcome('dest', null, null, true, [], null, null), + }; + component.unhandledAnswer = 'string'; + + spyOn(answerGroupObjectFactory, 'createNew').and.returnValue(null); + spyOn(trainingDataService, 'associateWithAnswerGroup').and.stub(); + spyOn(responsesService, 'getAnswerGroupCount').and.returnValue(1); + spyOn(responsesService, 'getAnswerGroups').and.returnValue([ + {}, + ] as AnswerGroup[]); + spyOn(responsesService, 'save').and.callFake( + (answerGroups, getDefaultOutcome, save) => { + save(null, null); + } + ); + + component.ngOnInit(); + component.onConfirm(); + + expect(responsesService.save).toHaveBeenCalled(); + expect(ngbActiveModal.close).toHaveBeenCalled(); + } + ); + + it( + 'should click on confirm button when ' + + 'answerGroupIndex is greater than Response', + () => { + component.classification = { + answerGroupIndex: 1, + newOutcome: new Outcome('dest', null, null, true, [], null, null), + }; + component.unhandledAnswer = 'string'; + + spyOn(trainingDataService, 'associateWithDefaultResponse').and.stub(); + spyOn(responsesService, 'getAnswerGroupCount').and.returnValue(1); + + component.onConfirm(); + expect(ngbActiveModal.close).toHaveBeenCalled(); + } + ); + + it( + 'should click on confirm button when ' + + 'answerGroupIndex is less than Response', + () => { + component.classification = { + answerGroupIndex: 1, + newOutcome: new Outcome('dest', null, null, true, [], null, null), + }; + component.unhandledAnswer = 'string'; + + spyOn(trainingDataService, 'associateWithAnswerGroup').and.stub(); + spyOn(responsesService, 'getAnswerGroupCount').and.returnValue(3); + + component.onConfirm(); + expect(ngbActiveModal.close).toHaveBeenCalled(); + } + ); }); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-modal.component.ts b/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-modal.component.ts index 5e79753a745d..c543d049f728 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-modal.component.ts @@ -16,37 +16,46 @@ * @fileoverview Component for Training Modal. */ - -import { Component, EventEmitter, Injector, Input, OnInit, Output } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { ResponsesService } from '../services/responses.service'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { StateInteractionIdService } from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; -import { AnswerGroup, AnswerGroupObjectFactory } from 'domain/exploration/AnswerGroupObjectFactory'; -import { AngularNameService } from 'pages/exploration-editor-page/services/angular-name.service'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; -import { ExplorationWarningsService } from 'pages/exploration-editor-page/services/exploration-warnings.service'; -import { GraphDataService } from 'pages/exploration-editor-page/services/graph-data.service'; -import { AnswerClassificationService } from 'pages/exploration-player-page/services/answer-classification.service'; -import { TrainingDataService } from './training-data.service'; +import { + Component, + EventEmitter, + Injector, + Input, + OnInit, + Output, +} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {ResponsesService} from '../services/responses.service'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {StateInteractionIdService} from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; +import { + AnswerGroup, + AnswerGroupObjectFactory, +} from 'domain/exploration/AnswerGroupObjectFactory'; +import {AngularNameService} from 'pages/exploration-editor-page/services/angular-name.service'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; +import {ExplorationWarningsService} from 'pages/exploration-editor-page/services/exploration-warnings.service'; +import {GraphDataService} from 'pages/exploration-editor-page/services/graph-data.service'; +import {AnswerClassificationService} from 'pages/exploration-player-page/services/answer-classification.service'; +import {TrainingDataService} from './training-data.service'; import cloneDeep from 'lodash/cloneDeep'; -import { InteractionAnswer } from 'interactions/answer-defs'; -import { Outcome } from 'domain/exploration/OutcomeObjectFactory'; -import { AlgebraicExpressionInputRulesService } from 'interactions/AlgebraicExpressionInput/directives/algebraic-expression-input-rules.service'; -import { CodeReplRulesService } from 'interactions/CodeRepl/directives/code-repl-rules.service'; -import { ContinueRulesService } from 'interactions/Continue/directives/continue-rules.service'; -import { FractionInputRulesService } from 'interactions/FractionInput/directives/fraction-input-rules.service'; -import { GraphInputRulesService } from 'interactions/GraphInput/directives/graph-input-rules.service'; -import { ImageClickInputRulesService } from 'interactions/ImageClickInput/directives/image-click-input-rules.service'; -import { InteractiveMapRulesService } from 'interactions/InteractiveMap/directives/interactive-map-rules.service'; -import { MathEquationInputRulesService } from 'interactions/MathEquationInput/directives/math-equation-input-rules.service'; -import { NumericExpressionInputRulesService } from 'interactions/NumericExpressionInput/directives/numeric-expression-input-rules.service'; -import { NumericInputRulesService } from 'interactions/NumericInput/directives/numeric-input-rules.service'; -import { PencilCodeEditorRulesService } from 'interactions/PencilCodeEditor/directives/pencil-code-editor-rules.service'; -import { SetInputRulesService } from 'interactions/SetInput/directives/set-input-rules.service'; -import { TextInputRulesService } from 'interactions/TextInput/directives/text-input-rules.service'; +import {InteractionAnswer} from 'interactions/answer-defs'; +import {Outcome} from 'domain/exploration/OutcomeObjectFactory'; +import {AlgebraicExpressionInputRulesService} from 'interactions/AlgebraicExpressionInput/directives/algebraic-expression-input-rules.service'; +import {CodeReplRulesService} from 'interactions/CodeRepl/directives/code-repl-rules.service'; +import {ContinueRulesService} from 'interactions/Continue/directives/continue-rules.service'; +import {FractionInputRulesService} from 'interactions/FractionInput/directives/fraction-input-rules.service'; +import {GraphInputRulesService} from 'interactions/GraphInput/directives/graph-input-rules.service'; +import {ImageClickInputRulesService} from 'interactions/ImageClickInput/directives/image-click-input-rules.service'; +import {InteractiveMapRulesService} from 'interactions/InteractiveMap/directives/interactive-map-rules.service'; +import {MathEquationInputRulesService} from 'interactions/MathEquationInput/directives/math-equation-input-rules.service'; +import {NumericExpressionInputRulesService} from 'interactions/NumericExpressionInput/directives/numeric-expression-input-rules.service'; +import {NumericInputRulesService} from 'interactions/NumericInput/directives/numeric-input-rules.service'; +import {PencilCodeEditorRulesService} from 'interactions/PencilCodeEditor/directives/pencil-code-editor-rules.service'; +import {SetInputRulesService} from 'interactions/SetInput/directives/set-input-rules.service'; +import {TextInputRulesService} from 'interactions/TextInput/directives/text-input-rules.service'; export const RULES_SERVICE_MAPPING = { AlgebraicExpressionInputRulesService: AlgebraicExpressionInputRulesService, @@ -71,13 +80,14 @@ interface classification { @Component({ selector: 'oppia-training-modal', - templateUrl: './training-modal.component.html' + templateUrl: './training-modal.component.html', }) export class TrainingModalComponent - extends ConfirmOrCancelModal implements OnInit { + extends ConfirmOrCancelModal + implements OnInit +{ @Input() unhandledAnswer: InteractionAnswer; - @Output() finishTrainingCallback: EventEmitter = - new EventEmitter(); + @Output() finishTrainingCallback: EventEmitter = new EventEmitter(); trainingDataAnswer: InteractionAnswer | string = ''; // See the training panel directive in ExplorationEditorTab for an @@ -97,7 +107,7 @@ export class TrainingModalComponent private trainingDataService: TrainingDataService, private angularNameService: AngularNameService, private answerClassificationService: AnswerClassificationService, - private stateInteractionIdService: StateInteractionIdService, + private stateInteractionIdService: StateInteractionIdService ) { super(ngbActiveModal); } @@ -105,7 +115,7 @@ export class TrainingModalComponent ngOnInit(): void { this.classification = { answerGroupIndex: 0, - newOutcome: null + newOutcome: null, }; this.addingNewResponse = false; @@ -117,19 +127,23 @@ export class TrainingModalComponent answerGroups.push(newAnswerGroup); this.responsesService.save( - answerGroups, this.responsesService.getDefaultOutcome(), + answerGroups, + this.responsesService.getDefaultOutcome(), (newAnswerGroups, newDefaultOutcome) => { this.explorationStatesService.saveInteractionAnswerGroups( this.stateEditorService.getActiveStateName(), - cloneDeep(newAnswerGroups)); + cloneDeep(newAnswerGroups) + ); this.explorationStatesService.saveInteractionDefaultOutcome( this.stateEditorService.getActiveStateName(), - cloneDeep(newDefaultOutcome)); + cloneDeep(newDefaultOutcome) + ); this.graphDataService.recompute(); this.explorationWarningsService.updateWarnings(); - }); + } + ); } exitTrainer(): void { @@ -141,17 +155,25 @@ export class TrainingModalComponent if (index > this.responsesService.getAnswerGroupCount()) { let newOutcome = this.classification.newOutcome; let newAnswerGroup = this.answerGroupObjectFactory.createNew( - [], cloneDeep(newOutcome), [this.unhandledAnswer], null); + [], + cloneDeep(newOutcome), + [this.unhandledAnswer], + null + ); this._saveNewAnswerGroup(newAnswerGroup); this.trainingDataService.associateWithAnswerGroup( this.responsesService.getAnswerGroupCount() - 1, - this.unhandledAnswer); + this.unhandledAnswer + ); } else if (index === this.responsesService.getAnswerGroupCount()) { this.trainingDataService.associateWithDefaultResponse( - this.unhandledAnswer); + this.unhandledAnswer + ); } else { this.trainingDataService.associateWithAnswerGroup( - index, this.unhandledAnswer); + index, + this.unhandledAnswer + ); } this.finishTrainingCallback.emit(); @@ -159,25 +181,27 @@ export class TrainingModalComponent } init(): void { - let currentStateName = - this.stateEditorService.getActiveStateName(); + let currentStateName = this.stateEditorService.getActiveStateName(); let state = this.explorationStatesService.getState(currentStateName); // Retrieve the interaction ID. let interactionId = this.stateInteractionIdService.savedMemento; let rulesServiceName = - this.angularNameService.getNameOfInteractionRulesService( - interactionId); + this.angularNameService.getNameOfInteractionRulesService(interactionId); // Inject RulesService dynamically. - let rulesService = ( - this.injector.get(RULES_SERVICE_MAPPING[rulesServiceName])); + let rulesService = this.injector.get( + RULES_SERVICE_MAPPING[rulesServiceName] + ); - let classificationResult = ( + let classificationResult = this.answerClassificationService.getMatchingClassificationResult( - currentStateName, state.interaction, this.unhandledAnswer, - rulesService)); + currentStateName, + state.interaction, + this.unhandledAnswer, + rulesService + ); // This.trainingDataAnswer, this.trainingDataFeedback // this.trainingDataOutcomeDest are intended to be local @@ -188,13 +212,15 @@ export class TrainingModalComponent // specific feedback of the outcome (for instance, it // includes the destination state within the feedback). this.trainingDataAnswer = this.unhandledAnswer; - this.classification.answerGroupIndex = ( - classificationResult.answerGroupIndex); + this.classification.answerGroupIndex = + classificationResult.answerGroupIndex; this.classification.newOutcome = classificationResult.outcome; } } -angular.module('oppia').directive('oppiaTrainingModal', +angular.module('oppia').directive( + 'oppiaTrainingModal', downgradeComponent({ - component: TrainingModalComponent - }) as angular.IDirectiveFactory); + component: TrainingModalComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-modal.service.spec.ts b/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-modal.service.spec.ts index b17e4fa80e36..45f6b4afdf82 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-modal.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-modal.service.spec.ts @@ -16,13 +16,13 @@ * @fileoverview Unit tests for TrainingModalService. */ -import { fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { AlertsService } from 'services/alerts.service'; -import { TrainingModalService } from './training-modal.service'; -import { ExternalSaveService } from 'services/external-save.service'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { EventEmitter } from '@angular/core'; +import {fakeAsync, TestBed, tick} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {AlertsService} from 'services/alerts.service'; +import {TrainingModalService} from './training-modal.service'; +import {ExternalSaveService} from 'services/external-save.service'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {EventEmitter} from '@angular/core'; describe('Training Modal Service', () => { let trainingModalService: TrainingModalService; @@ -36,12 +36,11 @@ describe('Training Modal Service', () => { TrainingModalService, AlertsService, ExternalSaveService, - NgbModal - ] + NgbModal, + ], }); - trainingModalService = TestBed.inject( - TrainingModalService); + trainingModalService = TestBed.inject(TrainingModalService); alertsService = TestBed.inject(AlertsService); ngbModal = TestBed.inject(NgbModal); }); @@ -50,28 +49,27 @@ describe('Training Modal Service', () => { let emitter = new EventEmitter(); let MockComponentInstance = { unhandledAnswer: 'unhandledAnswer', - finishTrainingCallback: emitter + finishTrainingCallback: emitter, }; spyOn(trainingModalService.onFinishTrainingCallback, 'emit'); - spyOn(alertsService, 'clearWarnings') - .and.stub(); + spyOn(alertsService, 'clearWarnings').and.stub(); spyOn(ngbModal, 'open').and.callFake(() => { - return ({ + return { componentInstance: MockComponentInstance, - result: Promise.resolve() - } as NgbModalRef); + result: Promise.resolve(), + } as NgbModalRef; }); - trainingModalService.openTrainUnresolvedAnswerModal( - 'Test', 'textInput', 2); + trainingModalService.openTrainUnresolvedAnswerModal('Test', 'textInput', 2); emitter.emit(); tick(); expect(alertsService.clearWarnings).toHaveBeenCalled(); expect( - trainingModalService.onFinishTrainingCallback.emit).toHaveBeenCalled(); + trainingModalService.onFinishTrainingCallback.emit + ).toHaveBeenCalled(); expect(ngbModal.open).toHaveBeenCalled(); })); }); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-modal.service.ts b/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-modal.service.ts index 8e5fd53226a4..b7bd5c599296 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-modal.service.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-modal.service.ts @@ -17,13 +17,13 @@ * the training modal used for unresolved answers. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { EventEmitter, Injectable } from '@angular/core'; -import { AlertsService } from 'services/alerts.service'; -import { ExternalSaveService } from 'services/external-save.service'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { TrainingModalComponent } from './training-modal.component'; -import { InteractionAnswer } from 'interactions/answer-defs'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {EventEmitter, Injectable} from '@angular/core'; +import {AlertsService} from 'services/alerts.service'; +import {ExternalSaveService} from 'services/external-save.service'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {TrainingModalComponent} from './training-modal.component'; +import {InteractionAnswer} from 'interactions/answer-defs'; interface FinishTrainingResult { answer: InteractionAnswer; @@ -32,14 +32,14 @@ interface FinishTrainingResult { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class TrainingModalService { constructor( private alertsService: AlertsService, private externalSaveService: ExternalSaveService, - private ngbModal: NgbModal, - ) { } + private ngbModal: NgbModal + ) {} private _finishTrainingCallbackEmitter = new EventEmitter(); @@ -54,36 +54,39 @@ export class TrainingModalService { answer has been trained. */ openTrainUnresolvedAnswerModal( - unhandledAnswer: InteractionAnswer, - interactionId: string, - answerIndex: number): void { + unhandledAnswer: InteractionAnswer, + interactionId: string, + answerIndex: number + ): void { this.alertsService.clearWarnings(); let modalRef: NgbModalRef = this.ngbModal.open(TrainingModalComponent, { backdrop: 'static', windowClass: 'skill-select-modal', - size: 'xl' + size: 'xl', }); modalRef.componentInstance.unhandledAnswer = unhandledAnswer; - modalRef.componentInstance.finishTrainingCallback.subscribe( - () => { - let finishTrainingResult: FinishTrainingResult = { - answer: unhandledAnswer, - interactionId: interactionId, - answerIndex: answerIndex - }; + modalRef.componentInstance.finishTrainingCallback.subscribe(() => { + let finishTrainingResult: FinishTrainingResult = { + answer: unhandledAnswer, + interactionId: interactionId, + answerIndex: answerIndex, + }; - this.onFinishTrainingCallback.emit(finishTrainingResult); - }); - - modalRef.result.then(() => { }, () => { }); + this.onFinishTrainingCallback.emit(finishTrainingResult); + }); + modalRef.result.then( + () => {}, + () => {} + ); // Save the modified training data externally in state content. this.externalSaveService.onExternalSave.emit(); } } -angular.module('oppia').factory('TrainingModalService', - downgradeInjectable(TrainingModalService)); +angular + .module('oppia') + .factory('TrainingModalService', downgradeInjectable(TrainingModalService)); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-panel.component.spec.ts b/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-panel.component.spec.ts index c848e112d1ab..14df2c133935 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-panel.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-panel.component.spec.ts @@ -16,24 +16,24 @@ * @fileoverview Unit tests for trainingPanel. */ -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { Outcome } from 'domain/exploration/OutcomeObjectFactory'; -import { TrainingPanelComponent } from './training-panel.component'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; -import { TrainingDataService } from './training-data.service'; -import { ExplorationHtmlFormatterService } from 'services/exploration-html-formatter.service'; -import { ResponsesService } from '../services/responses.service'; -import { GenerateContentIdService } from 'services/generate-content-id.service'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {Outcome} from 'domain/exploration/OutcomeObjectFactory'; +import {TrainingPanelComponent} from './training-panel.component'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; +import {TrainingDataService} from './training-data.service'; +import {ExplorationHtmlFormatterService} from 'services/exploration-html-formatter.service'; +import {ResponsesService} from '../services/responses.service'; +import {GenerateContentIdService} from 'services/generate-content-id.service'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; class MockNgbModal { open() { return { - result: Promise.resolve() + result: Promise.resolve(), }; } } @@ -66,30 +66,28 @@ describe('Training Panel Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - TrainingPanelComponent - ], + declarations: [TrainingPanelComponent], providers: [ { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, { provide: StateEditorService, - useClass: MockStateEditorService + useClass: MockStateEditorService, }, { provide: ExplorationStatesService, - useClass: MockExplorationStatesService + useClass: MockExplorationStatesService, }, { provide: TrainingDataService, - useClass: MockTrainingDataService + useClass: MockTrainingDataService, }, ExplorationHtmlFormatterService, - ResponsesService + ResponsesService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -97,19 +95,30 @@ describe('Training Panel Component', () => { fixture = TestBed.createComponent(TrainingPanelComponent); component = fixture.componentInstance; - explorationHtmlFormatterService = - TestBed.inject(ExplorationHtmlFormatterService); + explorationHtmlFormatterService = TestBed.inject( + ExplorationHtmlFormatterService + ); responsesService = TestBed.inject(ResponsesService); generateContentIdService = TestBed.inject(GenerateContentIdService); - generateContentIdService.init(() => 0, () => { }); - spyOn(explorationHtmlFormatterService, 'getAnswerHtml') - .and.returnValue('answerTemplate'); + generateContentIdService.init( + () => 0, + () => {} + ); + spyOn(explorationHtmlFormatterService, 'getAnswerHtml').and.returnValue( + 'answerTemplate' + ); component.classification = { answerGroupIndex: 0, newOutcome: new Outcome( - 'dest', '', new SubtitledHtml('

Saved Outcome

', 'Id'), - true, [], '', '') + 'dest', + '', + new SubtitledHtml('

Saved Outcome

', 'Id'), + true, + [], + '', + '' + ), }; component.addingNewResponse = false; // This throws "Argument of type 'null' is not assignable to parameter of @@ -123,14 +132,12 @@ describe('Training Panel Component', () => { fixture.detectChanges(); }); - - it('should initialize $scope properties after controller is initialized', - () => { - expect(component.addingNewResponse).toBe(false); - expect(component.allOutcomes.length).toBe(0); - expect(component.selectedAnswerGroupIndex).toBe(0); - expect(component.answerTemplate).toBe('answerTemplate'); - }); + it('should initialize $scope properties after controller is initialized', () => { + expect(component.addingNewResponse).toBe(false); + expect(component.allOutcomes.length).toBe(0); + expect(component.selectedAnswerGroupIndex).toBe(0); + expect(component.answerTemplate).toBe('answerTemplate'); + }); it('should get name from current state', () => { expect(component.getCurrentStateName()).toBe('activeState'); @@ -139,11 +146,23 @@ describe('Training Panel Component', () => { it('should add new feedback and select it', () => { component.allOutcomes = [ new Outcome( - 'dest', '', new SubtitledHtml('

Saved Outcome

', 'Id'), true, - [], '', ''), + 'dest', + '', + new SubtitledHtml('

Saved Outcome

', 'Id'), + true, + [], + '', + '' + ), new Outcome( - 'dest', '', new SubtitledHtml('

Saved Outcome

', 'Id'), true, - [], '', '') + 'dest', + '', + new SubtitledHtml('

Saved Outcome

', 'Id'), + true, + [], + '', + '' + ), ]; spyOn(responsesService, 'getAnswerGroupCount').and.returnValue(0); expect(component.allOutcomes.length).toBe(2); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-panel.component.ts b/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-panel.component.ts index a8ee7200ffaf..68cadb4ec8fd 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-panel.component.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/training-panel/training-panel.component.ts @@ -16,19 +16,22 @@ * @fileoverview Component for the training panel in the state editor. */ -import { Component, Input, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { StateCustomizationArgsService } from 'components/state-editor/state-editor-properties-services/state-customization-args.service'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { StateInteractionIdService } from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; -import { Outcome, OutcomeObjectFactory } from 'domain/exploration/OutcomeObjectFactory'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; -import { ExplorationHtmlFormatterService } from 'services/exploration-html-formatter.service'; -import { GenerateContentIdService } from 'services/generate-content-id.service'; -import { ResponsesService } from '../services/responses.service'; -import { TrainingDataService } from './training-data.service'; -import { AppConstants } from 'app.constants'; -import { InteractionAnswer } from 'interactions/answer-defs'; +import {Component, Input, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {StateCustomizationArgsService} from 'components/state-editor/state-editor-properties-services/state-customization-args.service'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {StateInteractionIdService} from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; +import { + Outcome, + OutcomeObjectFactory, +} from 'domain/exploration/OutcomeObjectFactory'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; +import {ExplorationHtmlFormatterService} from 'services/exploration-html-formatter.service'; +import {GenerateContentIdService} from 'services/generate-content-id.service'; +import {ResponsesService} from '../services/responses.service'; +import {TrainingDataService} from './training-data.service'; +import {AppConstants} from 'app.constants'; +import {InteractionAnswer} from 'interactions/answer-defs'; interface ClassificationInterface { answerGroupIndex: number; @@ -37,10 +40,9 @@ interface ClassificationInterface { @Component({ selector: 'oppia-training-panel', - templateUrl: './training-panel.component.html' + templateUrl: './training-panel.component.html', }) -export class TrainingPanelComponent - implements OnInit { +export class TrainingPanelComponent implements OnInit { // These properties below are initialized using Angular lifecycle hooks // where we need to do non-null assertion. For more information see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -71,14 +73,15 @@ export class TrainingPanelComponent private explorationStatesService: ExplorationStatesService, private explorationHtmlFormatterService: ExplorationHtmlFormatterService, private generateContentIdService: GenerateContentIdService, - private outcomeObjectFactory: OutcomeObjectFactory, - ) { } + private outcomeObjectFactory: OutcomeObjectFactory + ) {} _updateAnswerTemplate(): void { - this.answerTemplate = ( - this.explorationHtmlFormatterService.getAnswerHtml( - this.answer, this.stateInteractionIdService.savedMemento, - this.stateCustomizationArgsService.savedMemento)); + this.answerTemplate = this.explorationHtmlFormatterService.getAnswerHtml( + this.answer, + this.stateInteractionIdService.savedMemento, + this.stateCustomizationArgsService.savedMemento + ); } getCurrentStateName(): string | null { @@ -87,11 +90,16 @@ export class TrainingPanelComponent beginAddingNewResponse(): void { let contentId = this.generateContentIdService.getNextStateId( - AppConstants.COMPONENT_NAME_FEEDBACK); + AppConstants.COMPONENT_NAME_FEEDBACK + ); let currentStateName = this.stateEditorService.getActiveStateName(); if (currentStateName) { this.classification.newOutcome = this.outcomeObjectFactory.createNew( - currentStateName, contentId, '', []); + currentStateName, + contentId, + '', + [] + ); } this.addingNewResponse = true; } @@ -124,17 +132,18 @@ export class TrainingPanelComponent let _stateName = this.stateEditorService.getActiveStateName(); if (_stateName) { let _state = this.explorationStatesService.getState(_stateName); - this.allOutcomes = this.trainingDataService.getAllPotentialOutcomes( - _state); + this.allOutcomes = + this.trainingDataService.getAllPotentialOutcomes(_state); } this._updateAnswerTemplate(); - this.selectedAnswerGroupIndex = ( - this.classification.answerGroupIndex); + this.selectedAnswerGroupIndex = this.classification.answerGroupIndex; } } -angular.module('oppia').directive('oppiaTrainingPanel', +angular.module('oppia').directive( + 'oppiaTrainingPanel', downgradeComponent({ - component: TrainingPanelComponent - }) as angular.IDirectiveFactory); + component: TrainingPanelComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/unresolved-answers-overview/unresolved-answers-overview.component.spec.ts b/core/templates/pages/exploration-editor-page/editor-tab/unresolved-answers-overview/unresolved-answers-overview.component.spec.ts index 445f2c8ba41b..0eba57eeb355 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/unresolved-answers-overview/unresolved-answers-overview.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/unresolved-answers-overview/unresolved-answers-overview.component.spec.ts @@ -16,19 +16,24 @@ * @fileoverview Unit tests for unresolvedAnswersOverview. */ -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { EditabilityService } from 'services/editability.service'; -import { StateInteractionIdService } from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; -import { ImprovementsService } from 'services/improvements.service'; -import { StateTopAnswersStatsService } from 'services/state-top-answers-stats.service'; -import { UnresolvedAnswersOverviewComponent } from './unresolved-answers-overview.component'; -import { ExternalSaveService } from 'services/external-save.service'; -import { State } from 'domain/state/StateObjectFactory'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {EditabilityService} from 'services/editability.service'; +import {StateInteractionIdService} from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; +import {ImprovementsService} from 'services/improvements.service'; +import {StateTopAnswersStatsService} from 'services/state-top-answers-stats.service'; +import {UnresolvedAnswersOverviewComponent} from './unresolved-answers-overview.component'; +import {ExternalSaveService} from 'services/external-save.service'; +import {State} from 'domain/state/StateObjectFactory'; describe('Unresolved Answers Overview Component', () => { let component: UnresolvedAnswersOverviewComponent; @@ -48,7 +53,7 @@ describe('Unresolved Answers Overview Component', () => { class MockNgbModal { open() { return { - result: Promise.resolve() + result: Promise.resolve(), }; } } @@ -60,9 +65,7 @@ describe('Unresolved Answers Overview Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - UnresolvedAnswersOverviewComponent - ], + declarations: [UnresolvedAnswersOverviewComponent], providers: [ EditabilityService, ExplorationStatesService, @@ -72,18 +75,17 @@ describe('Unresolved Answers Overview Component', () => { StateTopAnswersStatsService, { provide: ExternalSaveService, - useClass: MockExternalSaveService + useClass: MockExternalSaveService, }, { provide: NgbModal, - useClass: MockNgbModal - } + useClass: MockNgbModal, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); - beforeEach(() => { fixture = TestBed.createComponent(UnresolvedAnswersOverviewComponent); component = fixture.componentInstance; @@ -99,102 +101,119 @@ describe('Unresolved Answers Overview Component', () => { component.ngOnInit(); }); - it('should initialize component properties after controller is initialized', + it('should initialize component properties after controller is initialized', () => { + spyOn(stateEditorService, 'getActiveStateName').and.returnValue(stateName); + spyOn(explorationStatesService, 'getState').and.returnValue({} as State); + expect(component.unresolvedAnswersOverviewIsShown).toBe(false); + expect(component.SHOW_TRAINABLE_UNRESOLVED_ANSWERS).toBe(false); + }); + + it( + 'should check unresolved answers overview are shown when it has' + + ' state stats', + () => { + spyOn(stateEditorService, 'getActiveStateName').and.returnValue(null); + spyOn(explorationStatesService, 'getState').and.returnValue({} as State); + spyOn(stateTopAnswersStatsService, 'hasStateStats').and.returnValue(true); + spyOn( + improvementsService, + 'isStateForcedToResolveOutstandingUnaddressedAnswers' + ).and.returnValue(true); + + expect(component.isUnresolvedAnswersOverviewShown()).toBe(false); + } + ); + + it( + 'should check unresolved answers overview are shown when it has' + + ' state stats', () => { spyOn(stateEditorService, 'getActiveStateName').and.returnValue( - stateName); + stateName + ); spyOn(explorationStatesService, 'getState').and.returnValue({} as State); - expect(component.unresolvedAnswersOverviewIsShown).toBe(false); - expect(component.SHOW_TRAINABLE_UNRESOLVED_ANSWERS).toBe(false); - }); + spyOn(stateTopAnswersStatsService, 'hasStateStats').and.returnValue(true); + spyOn( + improvementsService, + 'isStateForcedToResolveOutstandingUnaddressedAnswers' + ).and.returnValue(true); - it('should check unresolved answers overview are shown when it has' + - ' state stats', () => { - spyOn(stateEditorService, 'getActiveStateName').and.returnValue(null); - spyOn(explorationStatesService, 'getState').and.returnValue({} as State); - spyOn(stateTopAnswersStatsService, 'hasStateStats').and.returnValue(true); - spyOn( - improvementsService, - 'isStateForcedToResolveOutstandingUnaddressedAnswers') - .and.returnValue(true); + expect(component.isUnresolvedAnswersOverviewShown()).toBe(true); + } + ); - expect(component.isUnresolvedAnswersOverviewShown()).toBe(false); - }); + it( + 'should check unresolved answers overview are not shown when it' + + ' has no state stats', + () => { + spyOn(stateEditorService, 'getActiveStateName').and.returnValue( + stateName + ); + spyOn(explorationStatesService, 'getState').and.returnValue({} as State); + spyOn(stateTopAnswersStatsService, 'hasStateStats').and.returnValue( + false + ); + spyOn( + improvementsService, + 'isStateForcedToResolveOutstandingUnaddressedAnswers' + ); + + expect(component.isUnresolvedAnswersOverviewShown()).toBe(false); + expect( + improvementsService.isStateForcedToResolveOutstandingUnaddressedAnswers + ).not.toHaveBeenCalled(); + } + ); - it('should check unresolved answers overview are shown when it has' + - ' state stats', () => { - spyOn(stateEditorService, 'getActiveStateName').and.returnValue(stateName); - spyOn(explorationStatesService, 'getState').and.returnValue({} as State); - spyOn(stateTopAnswersStatsService, 'hasStateStats').and.returnValue(true); - spyOn( - improvementsService, - 'isStateForcedToResolveOutstandingUnaddressedAnswers') - .and.returnValue(true); + it( + 'should check unresolved answers overview are not shown when' + + ' the state is not forced to resolved unaddressed answers', + () => { + spyOn(stateEditorService, 'getActiveStateName').and.returnValue( + stateName + ); + spyOn(explorationStatesService, 'getState').and.returnValue({} as State); + spyOn(stateTopAnswersStatsService, 'hasStateStats').and.returnValue(true); + spyOn( + improvementsService, + 'isStateForcedToResolveOutstandingUnaddressedAnswers' + ).and.returnValue(false); - expect(component.isUnresolvedAnswersOverviewShown()).toBe(true); - }); + expect(component.isUnresolvedAnswersOverviewShown()).toBe(false); + } + ); - it('should check unresolved answers overview are not shown when it' + - ' has no state stats', () => { + it('should check whenever the current interaction is trainable or not', () => { spyOn(stateEditorService, 'getActiveStateName').and.returnValue(stateName); spyOn(explorationStatesService, 'getState').and.returnValue({} as State); - spyOn(stateTopAnswersStatsService, 'hasStateStats').and.returnValue(false); - spyOn( - improvementsService, - 'isStateForcedToResolveOutstandingUnaddressedAnswers'); + stateInteractionIdService.init(stateName, 'CodeRepl'); + expect(component.getCurrentInteractionId()).toBe('CodeRepl'); + expect(component.isCurrentInteractionTrainable()).toBe(true); - expect(component.isUnresolvedAnswersOverviewShown()).toBe(false); - expect( - improvementsService.isStateForcedToResolveOutstandingUnaddressedAnswers) - .not.toHaveBeenCalled(); + stateInteractionIdService.init(stateName, 'Continue'); + expect(component.getCurrentInteractionId()).toBe('Continue'); + expect(component.isCurrentInteractionTrainable()).toBe(false); }); - it('should check unresolved answers overview are not shown when' + - ' the state is not forced to resolved unaddressed answers', () => { + it('should check whenever the current interaction is linear or not', () => { spyOn(stateEditorService, 'getActiveStateName').and.returnValue(stateName); spyOn(explorationStatesService, 'getState').and.returnValue({} as State); - spyOn(stateTopAnswersStatsService, 'hasStateStats').and.returnValue(true); - spyOn( - improvementsService, - 'isStateForcedToResolveOutstandingUnaddressedAnswers') - .and.returnValue(false); + stateInteractionIdService.init(stateName, 'Continue'); + expect(component.getCurrentInteractionId()).toBe('Continue'); + expect(component.isCurrentInteractionLinear()).toBe(true); - expect(component.isUnresolvedAnswersOverviewShown()).toBe(false); + stateInteractionIdService.init(stateName, 'PencilCodeEditor'); + expect(component.getCurrentInteractionId()).toBe('PencilCodeEditor'); + expect(component.isCurrentInteractionLinear()).toBe(false); }); - it('should check whenever the current interaction is trainable or not', - () => { - spyOn(stateEditorService, 'getActiveStateName').and.returnValue( - stateName); - spyOn(explorationStatesService, 'getState').and.returnValue({} as State); - stateInteractionIdService.init(stateName, 'CodeRepl'); - expect(component.getCurrentInteractionId()).toBe('CodeRepl'); - expect(component.isCurrentInteractionTrainable()).toBe(true); - - stateInteractionIdService.init(stateName, 'Continue'); - expect(component.getCurrentInteractionId()).toBe('Continue'); - expect(component.isCurrentInteractionTrainable()).toBe(false); - }); - - it('should check whenever the current interaction is linear or not', - () => { - spyOn(stateEditorService, 'getActiveStateName').and.returnValue( - stateName); - spyOn(explorationStatesService, 'getState').and.returnValue({} as State); - stateInteractionIdService.init(stateName, 'Continue'); - expect(component.getCurrentInteractionId()).toBe('Continue'); - expect(component.isCurrentInteractionLinear()).toBe(true); - - stateInteractionIdService.init(stateName, 'PencilCodeEditor'); - expect(component.getCurrentInteractionId()).toBe('PencilCodeEditor'); - expect(component.isCurrentInteractionLinear()).toBe(false); - }); - it('should throw error if state name is null', fakeAsync(() => { spyOn(stateEditorService, 'getActiveStateName').and.returnValue(null); spyOn(explorationStatesService, 'getState').and.returnValue({} as State); - spyOn(stateTopAnswersStatsService, 'getUnresolvedStateStats').and - .returnValue([]); + spyOn( + stateTopAnswersStatsService, + 'getUnresolvedStateStats' + ).and.returnValue([]); expect(() => { component.getUnresolvedStateStats(); }).toThrowError('State name should not be null.'); @@ -204,7 +223,9 @@ describe('Unresolved Answers Overview Component', () => { spyOn(stateEditorService, 'getActiveStateName').and.returnValue(stateName); spyOn(explorationStatesService, 'getState').and.returnValue({} as State); let editabilitySpy = spyOn( - editabilityService, 'isEditableOutsideTutorialMode'); + editabilityService, + 'isEditableOutsideTutorialMode' + ); editabilitySpy.and.returnValue(true); expect(component.isEditableOutsideTutorialMode()).toBe(true); @@ -228,7 +249,7 @@ describe('Unresolved Answers Overview Component', () => { spyOn(explorationStatesService, 'getState').and.returnValue({} as State); spyOn(mockExternalSaveEventEmitter, 'emit').and.callThrough(); spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.resolve() + result: Promise.resolve(), } as NgbModalRef); component.openTeachOppiaModal(); @@ -236,26 +257,26 @@ describe('Unresolved Answers Overview Component', () => { expect(mockExternalSaveEventEmitter.emit).toHaveBeenCalled(); }); - it('should broadcast externalSave flag when dismissing the modal', - () => { - spyOn(stateEditorService, 'getActiveStateName').and.returnValue( - stateName); - spyOn(explorationStatesService, 'getState').and.returnValue({} as State); - spyOn(mockExternalSaveEventEmitter, 'emit').and.callThrough(); - spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.reject() - } as NgbModalRef); + it('should broadcast externalSave flag when dismissing the modal', () => { + spyOn(stateEditorService, 'getActiveStateName').and.returnValue(stateName); + spyOn(explorationStatesService, 'getState').and.returnValue({} as State); + spyOn(mockExternalSaveEventEmitter, 'emit').and.callThrough(); + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.reject(), + } as NgbModalRef); - component.openTeachOppiaModal(); + component.openTeachOppiaModal(); - expect(mockExternalSaveEventEmitter.emit).toHaveBeenCalled(); - }); + expect(mockExternalSaveEventEmitter.emit).toHaveBeenCalled(); + }); it('should fetch unresolved state stats from backend', () => { spyOn(stateEditorService, 'getActiveStateName').and.returnValue(stateName); spyOn(explorationStatesService, 'getState').and.returnValue({} as State); - spyOn(stateTopAnswersStatsService, 'getUnresolvedStateStats').and - .returnValue([]); + spyOn( + stateTopAnswersStatsService, + 'getUnresolvedStateStats' + ).and.returnValue([]); expect(component.getUnresolvedStateStats()).toEqual([]); }); }); diff --git a/core/templates/pages/exploration-editor-page/editor-tab/unresolved-answers-overview/unresolved-answers-overview.component.ts b/core/templates/pages/exploration-editor-page/editor-tab/unresolved-answers-overview/unresolved-answers-overview.component.ts index e4538e031576..f2c05c0ff579 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/unresolved-answers-overview/unresolved-answers-overview.component.ts +++ b/core/templates/pages/exploration-editor-page/editor-tab/unresolved-answers-overview/unresolved-answers-overview.component.ts @@ -16,29 +16,27 @@ * @fileoverview Component for the state graph visualization. */ -import { Component, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import {Component, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; import INTERACTION_SPECS from 'interactions/interaction_specs.json'; -import { AppConstants } from 'app.constants'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { StateInteractionIdService } from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; -import { EditabilityService } from 'services/editability.service'; -import { ImprovementsService } from 'services/improvements.service'; -import { StateTopAnswersStatsService } from 'services/state-top-answers-stats.service'; -import { TeachOppiaModalComponent } from '../templates/modal-templates/teach-oppia-modal.component'; -import { AnswerStats } from 'domain/exploration/answer-stats.model'; -import { ExternalSaveService } from 'services/external-save.service'; -import { InteractionSpecsKey } from 'pages/interaction-specs.constants'; +import {AppConstants} from 'app.constants'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {StateInteractionIdService} from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; +import {EditabilityService} from 'services/editability.service'; +import {ImprovementsService} from 'services/improvements.service'; +import {StateTopAnswersStatsService} from 'services/state-top-answers-stats.service'; +import {TeachOppiaModalComponent} from '../templates/modal-templates/teach-oppia-modal.component'; +import {AnswerStats} from 'domain/exploration/answer-stats.model'; +import {ExternalSaveService} from 'services/external-save.service'; +import {InteractionSpecsKey} from 'pages/interaction-specs.constants'; @Component({ selector: 'oppia-unresolved-answers-overview', - templateUrl: './unresolved-answers-overview.component.html' + templateUrl: './unresolved-answers-overview.component.html', }) - -export class UnresolvedAnswersOverviewComponent - implements OnInit { +export class UnresolvedAnswersOverviewComponent implements OnInit { // These properties below are initialized using Angular lifecycle hooks // where we need to do non-null assertion. For more information see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -53,13 +51,13 @@ export class UnresolvedAnswersOverviewComponent private ngbModal: NgbModal, private stateEditorService: StateEditorService, private stateInteractionIdService: StateInteractionIdService, - private stateTopAnswersStatsService: StateTopAnswersStatsService, - ) { } + private stateTopAnswersStatsService: StateTopAnswersStatsService + ) {} isStateRequiredToBeResolved(stateName: string): boolean { - return this.improvementsService - .isStateForcedToResolveOutstandingUnaddressedAnswers( - this.explorationStatesService.getState(stateName)); + return this.improvementsService.isStateForcedToResolveOutstandingUnaddressedAnswers( + this.explorationStatesService.getState(stateName) + ); } isUnresolvedAnswersOverviewShown(): boolean { @@ -67,8 +65,10 @@ export class UnresolvedAnswersOverviewComponent if (activeStateName === null) { return false; } - return this.stateTopAnswersStatsService.hasStateStats(activeStateName) && - this.isStateRequiredToBeResolved(activeStateName); + return ( + this.stateTopAnswersStatsService.hasStateStats(activeStateName) && + this.isStateRequiredToBeResolved(activeStateName) + ); } getCurrentInteractionId(): string { @@ -77,18 +77,18 @@ export class UnresolvedAnswersOverviewComponent isCurrentInteractionLinear(): boolean { let interactionId = this.getCurrentInteractionId(); - return Boolean(interactionId) && INTERACTION_SPECS[ - interactionId as InteractionSpecsKey - ].is_linear; + return ( + Boolean(interactionId) && + INTERACTION_SPECS[interactionId as InteractionSpecsKey].is_linear + ); } isCurrentInteractionTrainable(): boolean { let interactionId = this.getCurrentInteractionId(); return ( Boolean(interactionId) && - INTERACTION_SPECS[ - interactionId as InteractionSpecsKey - ].is_trainable); + INTERACTION_SPECS[interactionId as InteractionSpecsKey].is_trainable + ); } isEditableOutsideTutorialMode(): boolean { @@ -98,13 +98,18 @@ export class UnresolvedAnswersOverviewComponent openTeachOppiaModal(): void { this.externalSaveService.onExternalSave.emit(); - this.ngbModal.open(TeachOppiaModalComponent, { - backdrop: 'static' - }).result.then(() => {}, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + this.ngbModal + .open(TeachOppiaModalComponent, { + backdrop: 'static', + }) + .result.then( + () => {}, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } getUnresolvedStateStats(): AnswerStats[] { @@ -117,12 +122,14 @@ export class UnresolvedAnswersOverviewComponent ngOnInit(): void { this.unresolvedAnswersOverviewIsShown = false; - this.SHOW_TRAINABLE_UNRESOLVED_ANSWERS = ( - AppConstants.SHOW_TRAINABLE_UNRESOLVED_ANSWERS); + this.SHOW_TRAINABLE_UNRESOLVED_ANSWERS = + AppConstants.SHOW_TRAINABLE_UNRESOLVED_ANSWERS; } } -angular.module('oppia').directive('oppiaUnresolvedAnswersOverview', +angular.module('oppia').directive( + 'oppiaUnresolvedAnswersOverview', downgradeComponent({ - component: UnresolvedAnswersOverviewComponent - }) as angular.IDirectiveFactory); + component: UnresolvedAnswersOverviewComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-editor-page/exploration-editor-page.component.spec.ts b/core/templates/pages/exploration-editor-page/exploration-editor-page.component.spec.ts index a95b06c460c1..77075c6e24e5 100644 --- a/core/templates/pages/exploration-editor-page/exploration-editor-page.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/exploration-editor-page.component.spec.ts @@ -16,64 +16,75 @@ * @fileoverview Unit tests for exploration editor page component. */ -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { TestBed, fakeAsync, flushMicrotasks, discardPeriodicTasks, tick, flush, ComponentFixture } from '@angular/core/testing'; -import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { ParamChangesObjectFactory } from 'domain/exploration/ParamChangesObjectFactory'; -import { ParamSpecsObjectFactory } from 'domain/exploration/ParamSpecsObjectFactory'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { StateEditorRefreshService } from 'pages/exploration-editor-page/services/state-editor-refresh.service'; -import { UserExplorationPermissionsService } from 'pages/exploration-editor-page/services/user-exploration-permissions.service'; -import { StateClassifierMappingService } from 'pages/exploration-player-page/services/state-classifier-mapping.service'; -import { AlertsService } from 'services/alerts.service'; -import { InternetConnectivityService } from 'services/internet-connectivity.service'; -import { ContextService } from 'services/context.service'; -import { EditabilityService } from 'services/editability.service'; -import { ExplorationFeatures, ExplorationFeaturesBackendApiService } from 'services/exploration-features-backend-api.service'; -import { ExplorationFeaturesService } from 'services/exploration-features.service'; -import { LoaderService } from 'services/loader.service'; -import { PageTitleService } from 'services/page-title.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { StateTopAnswersStatsBackendApiService } from 'services/state-top-answers-stats-backend-api.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { LostChangesModalComponent } from './modal-templates/lost-changes-modal.component'; -import { AutosaveInfoModalsService } from './services/autosave-info-modals.service'; -import { ChangeListService } from './services/change-list.service'; -import { ExplorationDataService } from './services/exploration-data.service'; -import { UserInfo } from 'domain/user/user-info.model'; -import { WelcomeModalComponent } from './modal-templates/welcome-modal.component'; -import { HelpModalComponent } from './modal-templates/help-modal.component'; -import { ExplorationImprovementsService } from 'services/exploration-improvements.service'; -import { UserService } from 'services/user.service'; -import { ExplorationEditorPageComponent } from './exploration-editor-page.component'; -import { ThreadDataBackendApiService } from './feedback-tab/services/thread-data-backend-api.service'; -import { ExplorationPropertyService } from './services/exploration-property.service'; -import { ExplorationRightsService } from './services/exploration-rights.service'; -import { ExplorationSaveService } from './services/exploration-save.service'; -import { ExplorationStatesService } from './services/exploration-states.service'; -import { ExplorationTitleService } from './services/exploration-title.service'; -import { ExplorationWarningsService } from './services/exploration-warnings.service'; -import { GraphDataService } from './services/graph-data.service'; -import { RouterService } from './services/router.service'; -import { StateTutorialFirstTimeService } from './services/state-tutorial-first-time.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ExplorationPermissions } from 'domain/exploration/exploration-permissions.model'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { ExplorationPermissionsBackendApiService } from 'domain/exploration/exploration-permissions-backend-api.service'; - - class MockNgbModalRef { - componentInstance = {}; - } - - class MockNgbModal { - open() { - return { - result: Promise.resolve() - }; - } - } +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + TestBed, + fakeAsync, + flushMicrotasks, + discardPeriodicTasks, + tick, + flush, + ComponentFixture, +} from '@angular/core/testing'; +import {BrowserDynamicTestingModule} from '@angular/platform-browser-dynamic/testing'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {ParamChangesObjectFactory} from 'domain/exploration/ParamChangesObjectFactory'; +import {ParamSpecsObjectFactory} from 'domain/exploration/ParamSpecsObjectFactory'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {StateEditorRefreshService} from 'pages/exploration-editor-page/services/state-editor-refresh.service'; +import {UserExplorationPermissionsService} from 'pages/exploration-editor-page/services/user-exploration-permissions.service'; +import {StateClassifierMappingService} from 'pages/exploration-player-page/services/state-classifier-mapping.service'; +import {AlertsService} from 'services/alerts.service'; +import {InternetConnectivityService} from 'services/internet-connectivity.service'; +import {ContextService} from 'services/context.service'; +import {EditabilityService} from 'services/editability.service'; +import { + ExplorationFeatures, + ExplorationFeaturesBackendApiService, +} from 'services/exploration-features-backend-api.service'; +import {ExplorationFeaturesService} from 'services/exploration-features.service'; +import {LoaderService} from 'services/loader.service'; +import {PageTitleService} from 'services/page-title.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {StateTopAnswersStatsBackendApiService} from 'services/state-top-answers-stats-backend-api.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {LostChangesModalComponent} from './modal-templates/lost-changes-modal.component'; +import {AutosaveInfoModalsService} from './services/autosave-info-modals.service'; +import {ChangeListService} from './services/change-list.service'; +import {ExplorationDataService} from './services/exploration-data.service'; +import {UserInfo} from 'domain/user/user-info.model'; +import {WelcomeModalComponent} from './modal-templates/welcome-modal.component'; +import {HelpModalComponent} from './modal-templates/help-modal.component'; +import {ExplorationImprovementsService} from 'services/exploration-improvements.service'; +import {UserService} from 'services/user.service'; +import {ExplorationEditorPageComponent} from './exploration-editor-page.component'; +import {ThreadDataBackendApiService} from './feedback-tab/services/thread-data-backend-api.service'; +import {ExplorationPropertyService} from './services/exploration-property.service'; +import {ExplorationRightsService} from './services/exploration-rights.service'; +import {ExplorationSaveService} from './services/exploration-save.service'; +import {ExplorationStatesService} from './services/exploration-states.service'; +import {ExplorationTitleService} from './services/exploration-title.service'; +import {ExplorationWarningsService} from './services/exploration-warnings.service'; +import {GraphDataService} from './services/graph-data.service'; +import {RouterService} from './services/router.service'; +import {StateTutorialFirstTimeService} from './services/state-tutorial-first-time.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ExplorationPermissions} from 'domain/exploration/exploration-permissions.model'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {ExplorationPermissionsBackendApiService} from 'domain/exploration/exploration-permissions-backend-api.service'; + +class MockNgbModalRef { + componentInstance = {}; +} + +class MockNgbModal { + open() { + return { + result: Promise.resolve(), + }; + } +} describe('Exploration editor page component', () => { let component: ExplorationEditorPageComponent; @@ -104,8 +115,7 @@ describe('Exploration editor page component', () => { let registerAcceptTutorialModalEventSpy; let registerDeclineTutorialModalEventSpy; let focusManagerService: FocusManagerService; - let explorationPermissionsBackendApiService: - ExplorationPermissionsBackendApiService; + let explorationPermissionsBackendApiService: ExplorationPermissionsBackendApiService; let ngbModal: NgbModal; let refreshGraphEmitter = new EventEmitter(); let mockRefreshTranslationTabEventEmitter = new EventEmitter(); @@ -135,7 +145,7 @@ describe('Exploration editor page component', () => { dest_if_really_stuck: null, feedback: { content_id: 'content_1', - html: '' + html: '', }, }, confirmed_unclassified_answers: [], @@ -143,14 +153,14 @@ describe('Exploration editor page component', () => { hints: [], }, recorded_voiceovers: { - voiceovers_mapping: {} - } + voiceovers_mapping: {}, + }, }, Final: { param_changes: [], content: { html: '', - audio_translations: {} + audio_translations: {}, }, unresolved_answers: {}, interaction: { @@ -162,17 +172,17 @@ describe('Exploration editor page component', () => { dest_if_really_stuck: null, feedback: { html: '', - audio_translations: {} - } + audio_translations: {}, + }, }, confirmed_unclassified_answers: [], id: null, - hints: [] + hints: [], }, recorded_voiceovers: { - voiceovers_mapping: {} - } - } + voiceovers_mapping: {}, + }, + }, }, title: 'Exploration Title', category: 'Exploration Category', @@ -192,22 +202,22 @@ describe('Exploration editor page component', () => { draft_changes: [{}, {}, {}], is_version_of_draft_valid: false, show_state_editor_tutorial_on_load: true, - show_state_translation_tutorial_on_load: true + show_state_translation_tutorial_on_load: true, }; class MockWindowRef { - location = { path: '/create/2234' }; + location = {path: '/create/2234'}; nativeWindow = { scrollTo: (value1, value2) => {}, sessionStorage: { promoIsDismissed: null, setItem: (testKey1, testKey2) => {}, - removeItem: (testKey) => {} + removeItem: testKey => {}, }, gtag: (value1, value2, value3) => {}, navigator: { onLine: true, - userAgent: null + userAgent: null, }, location: { path: '/create/2234', @@ -229,19 +239,17 @@ describe('Exploration editor page component', () => { clientWidth: null, clientHeight: null, style: { - overflowY: '' - } - } + overflowY: '', + }, + }, }, - addEventListener: (value1, value2) => {} + addEventListener: (value1, value2) => {}, }; } beforeEach(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], + imports: [HttpClientTestingModule], declarations: [ ExplorationEditorPageComponent, LostChangesModalComponent, @@ -259,8 +267,8 @@ describe('Exploration editor page component', () => { getExplorationId: () => { return explorationId; }, - setExplorationIsLinkedToStory: () => {} - } + setExplorationIsLinkedToStory: () => {}, + }, }, EditabilityService, ExplorationFeaturesBackendApiService, @@ -286,29 +294,30 @@ describe('Exploration editor page component', () => { }, { provide: WindowRef, - useClass: MockWindowRef + useClass: MockWindowRef, }, { provide: ExplorationDataService, useValue: { - getDataAsync: (callback) => { + getDataAsync: callback => { callback(); return Promise.resolve(explorationData); }, autosaveChangeListAsync: () => { return; - } - } - } + }, + }, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).overrideModule(BrowserDynamicTestingModule, { set: { entryComponents: [ LostChangesModalComponent, WelcomeModalComponent, - HelpModalComponent] - } + HelpModalComponent, + ], + }, }); }); @@ -341,19 +350,35 @@ describe('Exploration editor page component', () => { ueps = TestBed.inject(UserExplorationPermissionsService); focusManagerService = TestBed.inject(FocusManagerService); explorationPermissionsBackendApiService = TestBed.inject( - ExplorationPermissionsBackendApiService); + ExplorationPermissionsBackendApiService + ); isLocationSetToNonStateEditorTabSpy = spyOn( - rs, 'isLocationSetToNonStateEditorTab'); + rs, + 'isLocationSetToNonStateEditorTab' + ); isLocationSetToNonStateEditorTabSpy.and.returnValue(null); - spyOn(explorationPermissionsBackendApiService, 'getPermissionsAsync') - .and.returnValue(Promise.resolve( + spyOn( + explorationPermissionsBackendApiService, + 'getPermissionsAsync' + ).and.returnValue( + Promise.resolve( new ExplorationPermissions( - null, null, null, null, null, null, true, null) - )); - spyOn(autosaveInfoModalsService, 'showVersionMismatchModal') - .and.callFake((value) => {}); + null, + null, + null, + null, + null, + null, + true, + null + ) + ) + ); + spyOn(autosaveInfoModalsService, 'showVersionMismatchModal').and.callFake( + value => {} + ); spyOn(autosaveInfoModalsService, 'showLostChangesModal').and.stub(); spyOn(autosaveInfoModalsService, 'isModalOpen').and.returnValue(false); @@ -376,39 +401,61 @@ describe('Exploration editor page component', () => { ews = TestBed.inject(ExplorationWarningsService); ueps = TestBed.inject(UserExplorationPermissionsService); - registerAcceptTutorialModalEventSpy = ( - spyOn(sas, 'registerAcceptTutorialModalEvent')); - registerDeclineTutorialModalEventSpy = ( - spyOn(sas, 'registerDeclineTutorialModalEvent')); - spyOn(efbas, 'fetchExplorationFeaturesAsync') - .and.returnValue(Promise.resolve({ + registerAcceptTutorialModalEventSpy = spyOn( + sas, + 'registerAcceptTutorialModalEvent' + ); + registerDeclineTutorialModalEventSpy = spyOn( + sas, + 'registerDeclineTutorialModalEvent' + ); + spyOn(efbas, 'fetchExplorationFeaturesAsync').and.returnValue( + Promise.resolve({ explorationIsCurated: null, - } as ExplorationFeatures)); + } as ExplorationFeatures) + ); spyOn(eis, 'initAsync').and.returnValue(Promise.resolve()); - spyOn(eis, 'flushUpdatedTasksToBackend') - .and.returnValue(Promise.resolve()); + spyOn(eis, 'flushUpdatedTasksToBackend').and.returnValue( + Promise.resolve() + ); spyOn(ews, 'updateWarnings').and.callThrough(); spyOn(gds, 'recompute').and.callThrough(); spyOn(pts, 'setDocumentTitle').and.callThrough(); - spyOn(tds, 'getFeedbackThreadsAsync') - .and.returnValue(Promise.resolve([])); - spyOn(ueps, 'getPermissionsAsync') - .and.returnValue(Promise.resolve( - { - canEdit: true, - canVoiceover: true - } as ExplorationPermissions)); - spyOnProperty(rs, 'onRefreshTranslationTab') - .and.returnValue(mockRefreshTranslationTabEventEmitter); + spyOn(tds, 'getFeedbackThreadsAsync').and.returnValue( + Promise.resolve([]) + ); + spyOn(ueps, 'getPermissionsAsync').and.returnValue( + Promise.resolve({ + canEdit: true, + canVoiceover: true, + } as ExplorationPermissions) + ); + spyOnProperty(rs, 'onRefreshTranslationTab').and.returnValue( + mockRefreshTranslationTabEventEmitter + ); spyOn(cls, 'getChangeList').and.returnValue(null); - spyOn(userService, 'getUserInfoAsync') - .and.returnValue(Promise.resolve(new UserInfo( - ['USER_ROLE'], true, true, false, false, false, null, null, null, - false))); + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve( + new UserInfo( + ['USER_ROLE'], + true, + true, + false, + false, + false, + null, + null, + null, + false + ) + ) + ); spyOnProperty(stfts, 'onOpenEditorTutorial').and.returnValue( - mockOpenEditorTutorialEmitter); + mockOpenEditorTutorialEmitter + ); spyOnProperty(stfts, 'onOpenTranslationTutorial').and.returnValue( - mockOpenTranslationTutorialEmitter); + mockOpenTranslationTutorialEmitter + ); explorationData.is_version_of_draft_valid = false; explorationData.draft_changes = ['data1', 'data2']; @@ -467,53 +514,50 @@ describe('Exploration editor page component', () => { expect(rs.navigateToMainTab).toHaveBeenCalled(); })); - it('should start translation tutorial when on translation page', - fakeAsync(() => { - tds.countOfOpenFeedbackThreads = 2; - spyOn(tds, 'getOpenThreadsCount').and.returnValue(2); - spyOn(component, 'startTranslationTutorial').and.callThrough(); - rs.navigateToTranslationTab(); - mockRefreshTranslationTabEventEmitter.emit(); + it('should start translation tutorial when on translation page', fakeAsync(() => { + tds.countOfOpenFeedbackThreads = 2; + spyOn(tds, 'getOpenThreadsCount').and.returnValue(2); + spyOn(component, 'startTranslationTutorial').and.callThrough(); + rs.navigateToTranslationTab(); + mockRefreshTranslationTabEventEmitter.emit(); - tick(); + tick(); - mockOpenTranslationTutorialEmitter.emit(); - expect(component.startTranslationTutorial).toHaveBeenCalled(); + mockOpenTranslationTutorialEmitter.emit(); + expect(component.startTranslationTutorial).toHaveBeenCalled(); - flush(); - discardPeriodicTasks(); - })); + flush(); + discardPeriodicTasks(); + })); - it('should start translation tutorial when not on translation page', - fakeAsync(() => { - tds.countOfOpenFeedbackThreads = 2; - spyOn(tds, 'getOpenThreadsCount').and.returnValue(2); - spyOn(component, 'startTranslationTutorial').and.callThrough(); - spyOn(rs, 'navigateToTranslationTab'); + it('should start translation tutorial when not on translation page', fakeAsync(() => { + tds.countOfOpenFeedbackThreads = 2; + spyOn(tds, 'getOpenThreadsCount').and.returnValue(2); + spyOn(component, 'startTranslationTutorial').and.callThrough(); + spyOn(rs, 'navigateToTranslationTab'); - rs.navigateToSettingsTab(); - tick(); - mockOpenTranslationTutorialEmitter.emit(); - tick(); + rs.navigateToSettingsTab(); + tick(); + mockOpenTranslationTutorialEmitter.emit(); + tick(); - expect(component.startTranslationTutorial).toHaveBeenCalled(); - expect(rs.navigateToTranslationTab).toHaveBeenCalled(); + expect(component.startTranslationTutorial).toHaveBeenCalled(); + expect(rs.navigateToTranslationTab).toHaveBeenCalled(); - flush(); - discardPeriodicTasks(); - })); + flush(); + discardPeriodicTasks(); + })); it('should return navbar text', () => { expect(component.getNavbarText()).toEqual('Exploration Editor'); }); - it('should return warning count, warnings list & critical warning', - () => { - spyOn(ews, 'countWarnings').and.returnValue(1); - expect(component.countWarnings()).toEqual(1); - spyOn(ews, 'getWarnings').and.returnValue([]); - expect(component.getWarnings()).toEqual([]); - }); + it('should return warning count, warnings list & critical warning', () => { + spyOn(ews, 'countWarnings').and.returnValue(1); + expect(component.countWarnings()).toEqual(1); + spyOn(ews, 'getWarnings').and.returnValue([]); + expect(component.getWarnings()).toEqual([]); + }); it('should return the thread count', () => { tds.countOfOpenFeedbackThreads = 2; @@ -596,9 +640,9 @@ describe('Exploration editor page component', () => { it('should generate the aria label correctly', () => { const mockWarnings = [ - { message: 'Warning 1' }, - { message: 'Warning 2' }, - { message: 'Warning 3' }, + {message: 'Warning 1'}, + {message: 'Warning 2'}, + {message: 'Warning 3'}, ]; spyOn(component, 'getWarnings').and.returnValue(mockWarnings); @@ -608,15 +652,14 @@ describe('Exploration editor page component', () => { expect(ariaLabel).toBe( 'Total warnings: 3. Warning 1: Warning 1. Warning 2: ' + - 'Warning 2. Warning 3: Warning 3'); + 'Warning 2. Warning 3: Warning 3' + ); }); it('should show the user help modal for editor tutorial', fakeAsync(() => { - spyOn(ngbModal, 'open').and.returnValue( - { - result: Promise.resolve('editor') - } as NgbModalRef - ); + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.resolve('editor'), + } as NgbModalRef); component.showUserHelpModal(); tick(); @@ -627,11 +670,9 @@ describe('Exploration editor page component', () => { })); it('should show the user help modal for editor tutorial', fakeAsync(() => { - spyOn(ngbModal, 'open').and.returnValue( - { - result: Promise.resolve('translation') - } as NgbModalRef - ); + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.resolve('translation'), + } as NgbModalRef); component.showUserHelpModal(); tick(); @@ -645,33 +686,43 @@ describe('Exploration editor page component', () => { describe('Checking internet Connection', () => { beforeEach(() => { ueps = TestBed.inject(UserExplorationPermissionsService); - registerAcceptTutorialModalEventSpy = ( - spyOn(sas, 'registerAcceptTutorialModalEvent')); - registerDeclineTutorialModalEventSpy = ( - spyOn(sas, 'registerDeclineTutorialModalEvent')); - spyOn(efbas, 'fetchExplorationFeaturesAsync') - .and.returnValue(Promise.resolve(null)); + registerAcceptTutorialModalEventSpy = spyOn( + sas, + 'registerAcceptTutorialModalEvent' + ); + registerDeclineTutorialModalEventSpy = spyOn( + sas, + 'registerDeclineTutorialModalEvent' + ); + spyOn(efbas, 'fetchExplorationFeaturesAsync').and.returnValue( + Promise.resolve(null) + ); spyOn(eis, 'initAsync').and.returnValue(Promise.resolve()); - spyOn(eis, 'flushUpdatedTasksToBackend') - .and.returnValue(Promise.resolve()); + spyOn(eis, 'flushUpdatedTasksToBackend').and.returnValue( + Promise.resolve() + ); spyOn(ews, 'updateWarnings').and.callThrough(); spyOn(gds, 'recompute').and.callThrough(); spyOn(pts, 'setDocumentTitle').and.callThrough(); spyOn(tds, 'getOpenThreadsCount').and.returnValue(0); - spyOn(tds, 'getFeedbackThreadsAsync') - .and.returnValue(Promise.resolve([])); - spyOn(ueps, 'getPermissionsAsync') - .and.returnValue(Promise.resolve( - { - canEdit: true, - canVoiceover: true - } as ExplorationPermissions)); + spyOn(tds, 'getFeedbackThreadsAsync').and.returnValue( + Promise.resolve([]) + ); + spyOn(ueps, 'getPermissionsAsync').and.returnValue( + Promise.resolve({ + canEdit: true, + canVoiceover: true, + } as ExplorationPermissions) + ); spyOnProperty(stfts, 'onOpenEditorTutorial').and.returnValue( - mockOpenEditorTutorialEmitter); + mockOpenEditorTutorialEmitter + ); spyOnProperty(ics, 'onInternetStateChange').and.returnValue( - mockConnectionServiceEmitter); + mockConnectionServiceEmitter + ); spyOnProperty(stfts, 'onOpenTranslationTutorial').and.returnValue( - mockOpenTranslationTutorialEmitter); + mockOpenTranslationTutorialEmitter + ); spyOn(as, 'addInfoMessage'); spyOn(as, 'addSuccessMessage'); explorationData.is_version_of_draft_valid = false; @@ -713,40 +764,62 @@ describe('Exploration editor page component', () => { beforeEach(() => { ueps = TestBed.inject(UserExplorationPermissionsService); tds = TestBed.inject(ThreadDataBackendApiService); - registerAcceptTutorialModalEventSpy = ( - spyOn(sas, 'registerAcceptTutorialModalEvent')); - registerDeclineTutorialModalEventSpy = ( - spyOn(sas, 'registerDeclineTutorialModalEvent')); - spyOn(efbas, 'fetchExplorationFeaturesAsync') - .and.returnValue(Promise.resolve({ + registerAcceptTutorialModalEventSpy = spyOn( + sas, + 'registerAcceptTutorialModalEvent' + ); + registerDeclineTutorialModalEventSpy = spyOn( + sas, + 'registerDeclineTutorialModalEvent' + ); + spyOn(efbas, 'fetchExplorationFeaturesAsync').and.returnValue( + Promise.resolve({ explorationIsCurated: null, - } as ExplorationFeatures)); + } as ExplorationFeatures) + ); spyOn(eis, 'initAsync').and.returnValue(Promise.resolve()); - spyOn(eis, 'flushUpdatedTasksToBackend') - .and.returnValue(Promise.resolve()); + spyOn(eis, 'flushUpdatedTasksToBackend').and.returnValue( + Promise.resolve() + ); spyOnProperty(eps, 'onExplorationPropertyChanged').and.returnValue( - mockExplorationPropertyChangedEventEmitter); + mockExplorationPropertyChangedEventEmitter + ); spyOn(ews, 'updateWarnings'); spyOn(gds, 'recompute'); spyOn(pts, 'setDocumentTitle').and.callThrough(); spyOn(tds, 'getOpenThreadsCount').and.returnValue(1); - spyOn(tds, 'getFeedbackThreadsAsync') - .and.returnValue(Promise.resolve([])); - spyOn(ueps, 'getPermissionsAsync') - .and.returnValue(Promise.resolve( - { - canEdit: true, - canVoiceover: true - } as ExplorationPermissions)); - spyOn(userService, 'getUserInfoAsync') - .and.returnValue(Promise.resolve(new UserInfo( - ['USER_ROLE'], true, true, false, false, false, null, null, null, - false))); + spyOn(tds, 'getFeedbackThreadsAsync').and.returnValue( + Promise.resolve([]) + ); + spyOn(ueps, 'getPermissionsAsync').and.returnValue( + Promise.resolve({ + canEdit: true, + canVoiceover: true, + } as ExplorationPermissions) + ); + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve( + new UserInfo( + ['USER_ROLE'], + true, + true, + false, + false, + false, + null, + null, + null, + false + ) + ) + ); spyOnProperty(ess, 'onRefreshGraph').and.returnValue(refreshGraphEmitter); spyOnProperty(cls, 'autosaveIsInProgress$').and.returnValue( - autosaveIsInProgress); + autosaveIsInProgress + ); spyOnProperty(esaves, 'onInitExplorationPage').and.returnValue( - mockInitExplorationPageEmitter); + mockInitExplorationPageEmitter + ); explorationData.is_version_of_draft_valid = false; explorationData.draft_changes = ['data1', 'data2']; @@ -767,9 +840,11 @@ describe('Exploration editor page component', () => { it('should have component properties correspond to backend data', () => { expect(component.explorationUrl).toBe('/create/' + explorationId); expect(component.explorationDownloadUrl).toBe( - '/createhandler/download/' + explorationId); + '/createhandler/download/' + explorationId + ); expect(component.revertExplorationUrl).toBe( - '/createhandler/revert/' + explorationId); + '/createhandler/revert/' + explorationId + ); expect(component.areExplorationWarningsVisible).toBeFalse(); }); @@ -789,7 +864,8 @@ describe('Exploration editor page component', () => { mockExplorationPropertyChangedEventEmitter.emit(); expect(pts.setDocumentTitle).toHaveBeenCalledWith( - 'Exploration Title - Oppia Editor'); + 'Exploration Title - Oppia Editor' + ); }); it('should react when untitled exploration property changes', () => { @@ -797,7 +873,8 @@ describe('Exploration editor page component', () => { mockExplorationPropertyChangedEventEmitter.emit(); expect(pts.setDocumentTitle).toHaveBeenCalledWith( - 'Untitled Exploration - Oppia Editor'); + 'Untitled Exploration - Oppia Editor' + ); }); it('should react when refreshing graph', () => { @@ -823,46 +900,47 @@ describe('Exploration editor page component', () => { discardPeriodicTasks(); })); - it('should start editor tutorial when closing welcome exploration' + - ' modal', fakeAsync(() => { - spyOn(component, 'startEditorTutorial').and.callThrough(); - spyOn(ngbModal, 'open').and.returnValue( - { - componentInstance: new MockNgbModalRef(), - result: Promise.resolve(explorationId) - } as NgbModalRef - ); - - component.isModalOpenable = true; - component.showWelcomeExplorationModal(); - tick(); - tick(); + it( + 'should start editor tutorial when closing welcome exploration' + + ' modal', + fakeAsync(() => { + spyOn(component, 'startEditorTutorial').and.callThrough(); + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: new MockNgbModalRef(), + result: Promise.resolve(explorationId), + } as NgbModalRef); + + component.isModalOpenable = true; + component.showWelcomeExplorationModal(); + tick(); + tick(); - expect(registerAcceptTutorialModalEventSpy) - .toHaveBeenCalledWith(explorationId); - expect(component.startEditorTutorial).toHaveBeenCalled(); + expect(registerAcceptTutorialModalEventSpy).toHaveBeenCalledWith( + explorationId + ); + expect(component.startEditorTutorial).toHaveBeenCalled(); - flush(); - discardPeriodicTasks(); - })); + flush(); + discardPeriodicTasks(); + }) + ); - it('should dismiss tutorial when dismissing welcome exploration' + - ' modal', fakeAsync(() => { - spyOn(component, 'startEditorTutorial').and.callThrough(); - spyOn(ngbModal, 'open').and.returnValue( - { - componentInstance: new MockNgbModalRef(), - result: Promise.reject(explorationId) - } as NgbModalRef - ); + it( + 'should dismiss tutorial when dismissing welcome exploration' + ' modal', + fakeAsync(() => { + spyOn(component, 'startEditorTutorial').and.callThrough(); + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: new MockNgbModalRef(), + result: Promise.reject(explorationId), + } as NgbModalRef); - component.showWelcomeExplorationModal(); - tick(); + component.showWelcomeExplorationModal(); + tick(); - expect(registerDeclineTutorialModalEventSpy) - .toHaveBeenCalled(); - expect(component.startEditorTutorial).not.toHaveBeenCalled(); - })); + expect(registerDeclineTutorialModalEventSpy).toHaveBeenCalled(); + expect(component.startEditorTutorial).not.toHaveBeenCalled(); + }) + ); it('should toggle exploration warning visibility', () => { expect(component.areExplorationWarningsVisible).toBeFalse(); @@ -895,30 +973,38 @@ describe('Exploration editor page component', () => { tds = TestBed.inject(ThreadDataBackendApiService); ueps = TestBed.inject(UserExplorationPermissionsService); - registerAcceptTutorialModalEventSpy = ( - spyOn(sas, 'registerAcceptTutorialModalEvent')); - registerDeclineTutorialModalEventSpy = ( - spyOn(sas, 'registerDeclineTutorialModalEvent')); + registerAcceptTutorialModalEventSpy = spyOn( + sas, + 'registerAcceptTutorialModalEvent' + ); + registerDeclineTutorialModalEventSpy = spyOn( + sas, + 'registerDeclineTutorialModalEvent' + ); mockEnterEditorForTheFirstTime = new EventEmitter(); - spyOn(efbas, 'fetchExplorationFeaturesAsync') - .and.returnValue(Promise.resolve(null)); + spyOn(efbas, 'fetchExplorationFeaturesAsync').and.returnValue( + Promise.resolve(null) + ); spyOn(eis, 'initAsync').and.returnValue(Promise.resolve()); - spyOn(eis, 'flushUpdatedTasksToBackend') - .and.returnValue(Promise.resolve()); + spyOn(eis, 'flushUpdatedTasksToBackend').and.returnValue( + Promise.resolve() + ); spyOn(ers, 'isPublic').and.returnValue(true); spyOn(ews, 'updateWarnings').and.callThrough(); spyOn(gds, 'recompute').and.callThrough(); spyOn(pts, 'setDocumentTitle').and.callThrough(); spyOn(tds, 'getOpenThreadsCount').and.returnValue(5); - spyOn(tds, 'getFeedbackThreadsAsync') - .and.returnValue(Promise.resolve([])); - spyOn(ueps, 'getPermissionsAsync') - .and.returnValue(Promise.resolve( - { - canEdit: true - } as ExplorationPermissions)); + spyOn(tds, 'getFeedbackThreadsAsync').and.returnValue( + Promise.resolve([]) + ); + spyOn(ueps, 'getPermissionsAsync').and.returnValue( + Promise.resolve({ + canEdit: true, + } as ExplorationPermissions) + ); spyOnProperty(sts, 'onEnterEditorForTheFirstTime').and.returnValue( - mockEnterEditorForTheFirstTime); + mockEnterEditorForTheFirstTime + ); explorationData.is_version_of_draft_valid = false; explorationData.draft_changes = ['data1', 'data2']; @@ -931,20 +1017,21 @@ describe('Exploration editor page component', () => { it('should recognize when improvements tab is enabled', fakeAsync(() => { spyOn(ics, 'startCheckingConnection'); spyOn(eis, 'isImprovementsTabEnabledAsync').and.returnValue( - Promise.resolve(true)); + Promise.resolve(true) + ); component.ngOnInit(); tick(); flushMicrotasks(); - expect(component.isImprovementsTabEnabled()).toBeTrue(); })); it('should recognize when improvements tab is disabled', fakeAsync(() => { spyOn(ics, 'startCheckingConnection'); spyOn(eis, 'isImprovementsTabEnabledAsync').and.returnValue( - Promise.resolve(false)); + Promise.resolve(false) + ); component.ngOnInit(); tick(); diff --git a/core/templates/pages/exploration-editor-page/exploration-editor-page.component.ts b/core/templates/pages/exploration-editor-page/exploration-editor-page.component.ts index 79eaf2e7f607..bf5ab93982c3 100644 --- a/core/templates/pages/exploration-editor-page/exploration-editor-page.component.ts +++ b/core/templates/pages/exploration-editor-page/exploration-editor-page.component.ts @@ -17,59 +17,62 @@ * help tab in the navbar. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { Subscription } from 'rxjs'; -import { WelcomeModalComponent } from './modal-templates/welcome-modal.component'; -import { HelpModalComponent } from './modal-templates/help-modal.component'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { ParamChangesObjectFactory } from 'domain/exploration/ParamChangesObjectFactory'; -import { ParamSpecsBackendDict, ParamSpecsObjectFactory } from 'domain/exploration/ParamSpecsObjectFactory'; -import { StateClassifierMappingService } from 'pages/exploration-player-page/services/state-classifier-mapping.service'; -import { AlertsService } from 'services/alerts.service'; -import { BottomNavbarStatusService } from 'services/bottom-navbar-status.service'; -import { ContextService } from 'services/context.service'; -import { EditabilityService } from 'services/editability.service'; -import { ExplorationFeaturesBackendApiService } from 'services/exploration-features-backend-api.service'; -import { ExplorationFeaturesService } from 'services/exploration-features.service'; -import { ExplorationImprovementsService } from 'services/exploration-improvements.service'; -import { InternetConnectivityService } from 'services/internet-connectivity.service'; -import { LoaderService } from 'services/loader.service'; -import { PageTitleService } from 'services/page-title.service'; -import { PreventPageUnloadEventService } from 'services/prevent-page-unload-event.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { UserService } from 'services/user.service'; -import { ThreadDataBackendApiService } from './feedback-tab/services/thread-data-backend-api.service'; -import { AutosaveInfoModalsService } from './services/autosave-info-modals.service'; -import { ChangeListService } from './services/change-list.service'; -import { ExplorationAutomaticTextToSpeechService } from './services/exploration-automatic-text-to-speech.service'; -import { ExplorationCategoryService } from './services/exploration-category.service'; -import { ExplorationDataService } from './services/exploration-data.service'; -import { ExplorationInitStateNameService } from './services/exploration-init-state-name.service'; -import { ExplorationLanguageCodeService } from './services/exploration-language-code.service'; -import { ExplorationObjectiveService } from './services/exploration-objective.service'; -import { ExplorationParamChangesService } from './services/exploration-param-changes.service'; -import { ExplorationParamSpecsService } from './services/exploration-param-specs.service'; -import { ExplorationPropertyService } from './services/exploration-property.service'; -import { ExplorationRightsService } from './services/exploration-rights.service'; -import { ExplorationSaveService } from './services/exploration-save.service'; -import { ExplorationStatesService } from './services/exploration-states.service'; -import { ExplorationTagsService } from './services/exploration-tags.service'; -import { ExplorationTitleService } from './services/exploration-title.service'; -import { ExplorationWarningsService } from './services/exploration-warnings.service'; -import { GraphDataService } from './services/graph-data.service'; -import { RouterService } from './services/router.service'; -import { StateEditorRefreshService } from './services/state-editor-refresh.service'; -import { StateTutorialFirstTimeService } from './services/state-tutorial-first-time.service'; -import { UserEmailPreferencesService } from './services/user-email-preferences.service'; -import { UserExplorationPermissionsService } from './services/user-exploration-permissions.service'; -import { EntityTranslationsService } from 'services/entity-translations.services'; -import { ExplorationNextContentIdIndexService } from './services/exploration-next-content-id-index.service'; -import { VersionHistoryService } from './services/version-history.service'; -import { ExplorationBackendDict } from 'domain/exploration/ExplorationObjectFactory'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {Subscription} from 'rxjs'; +import {WelcomeModalComponent} from './modal-templates/welcome-modal.component'; +import {HelpModalComponent} from './modal-templates/help-modal.component'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {ParamChangesObjectFactory} from 'domain/exploration/ParamChangesObjectFactory'; +import { + ParamSpecsBackendDict, + ParamSpecsObjectFactory, +} from 'domain/exploration/ParamSpecsObjectFactory'; +import {StateClassifierMappingService} from 'pages/exploration-player-page/services/state-classifier-mapping.service'; +import {AlertsService} from 'services/alerts.service'; +import {BottomNavbarStatusService} from 'services/bottom-navbar-status.service'; +import {ContextService} from 'services/context.service'; +import {EditabilityService} from 'services/editability.service'; +import {ExplorationFeaturesBackendApiService} from 'services/exploration-features-backend-api.service'; +import {ExplorationFeaturesService} from 'services/exploration-features.service'; +import {ExplorationImprovementsService} from 'services/exploration-improvements.service'; +import {InternetConnectivityService} from 'services/internet-connectivity.service'; +import {LoaderService} from 'services/loader.service'; +import {PageTitleService} from 'services/page-title.service'; +import {PreventPageUnloadEventService} from 'services/prevent-page-unload-event.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {UserService} from 'services/user.service'; +import {ThreadDataBackendApiService} from './feedback-tab/services/thread-data-backend-api.service'; +import {AutosaveInfoModalsService} from './services/autosave-info-modals.service'; +import {ChangeListService} from './services/change-list.service'; +import {ExplorationAutomaticTextToSpeechService} from './services/exploration-automatic-text-to-speech.service'; +import {ExplorationCategoryService} from './services/exploration-category.service'; +import {ExplorationDataService} from './services/exploration-data.service'; +import {ExplorationInitStateNameService} from './services/exploration-init-state-name.service'; +import {ExplorationLanguageCodeService} from './services/exploration-language-code.service'; +import {ExplorationObjectiveService} from './services/exploration-objective.service'; +import {ExplorationParamChangesService} from './services/exploration-param-changes.service'; +import {ExplorationParamSpecsService} from './services/exploration-param-specs.service'; +import {ExplorationPropertyService} from './services/exploration-property.service'; +import {ExplorationRightsService} from './services/exploration-rights.service'; +import {ExplorationSaveService} from './services/exploration-save.service'; +import {ExplorationStatesService} from './services/exploration-states.service'; +import {ExplorationTagsService} from './services/exploration-tags.service'; +import {ExplorationTitleService} from './services/exploration-title.service'; +import {ExplorationWarningsService} from './services/exploration-warnings.service'; +import {GraphDataService} from './services/graph-data.service'; +import {RouterService} from './services/router.service'; +import {StateEditorRefreshService} from './services/state-editor-refresh.service'; +import {StateTutorialFirstTimeService} from './services/state-tutorial-first-time.service'; +import {UserEmailPreferencesService} from './services/user-email-preferences.service'; +import {UserExplorationPermissionsService} from './services/user-exploration-permissions.service'; +import {EntityTranslationsService} from 'services/entity-translations.services'; +import {ExplorationNextContentIdIndexService} from './services/exploration-next-content-id-index.service'; +import {VersionHistoryService} from './services/version-history.service'; +import {ExplorationBackendDict} from 'domain/exploration/ExplorationObjectFactory'; interface ExplorationData extends ExplorationBackendDict { exploration_is_linked_to_story: boolean; @@ -98,7 +101,7 @@ interface ExplorationData extends ExplorationBackendDict { @Component({ selector: 'exploration-editor-page', - templateUrl: './exploration-editor-page.component.html' + templateUrl: './exploration-editor-page.component.html', }) export class ExplorationEditorPageComponent implements OnInit, OnDestroy { directiveSubscriptions = new Subscription(); @@ -133,18 +136,15 @@ export class ExplorationEditorPageComponent implements OnInit, OnDestroy { private contextService: ContextService, public editabilityService: EditabilityService, private entityTranslationsService: EntityTranslationsService, - private explorationAutomaticTextToSpeechService: - ExplorationAutomaticTextToSpeechService, + private explorationAutomaticTextToSpeechService: ExplorationAutomaticTextToSpeechService, private explorationCategoryService: ExplorationCategoryService, private explorationDataService: ExplorationDataService, - private explorationFeaturesBackendApiService: - ExplorationFeaturesBackendApiService, + private explorationFeaturesBackendApiService: ExplorationFeaturesBackendApiService, private explorationFeaturesService: ExplorationFeaturesService, private explorationImprovementsService: ExplorationImprovementsService, private explorationInitStateNameService: ExplorationInitStateNameService, private explorationLanguageCodeService: ExplorationLanguageCodeService, - private explorationNextContentIdIndexService: - ExplorationNextContentIdIndexService, + private explorationNextContentIdIndexService: ExplorationNextContentIdIndexService, private explorationObjectiveService: ExplorationObjectiveService, private explorationParamChangesService: ExplorationParamChangesService, private explorationParamSpecsService: ExplorationParamSpecsService, @@ -172,33 +172,33 @@ export class ExplorationEditorPageComponent implements OnInit, OnDestroy { private stateTutorialFirstTimeService: StateTutorialFirstTimeService, private threadDataBackendApiService: ThreadDataBackendApiService, private userEmailPreferencesService: UserEmailPreferencesService, - private userExplorationPermissionsService: - UserExplorationPermissionsService, + private userExplorationPermissionsService: UserExplorationPermissionsService, private userService: UserService, private windowDimensionsService: WindowDimensionsService, private versionHistoryService: VersionHistoryService - ) { } + ) {} setDocumentTitle(): void { if (this.explorationTitleService.savedMemento) { this.pageTitleService.setDocumentTitle( - this.explorationTitleService.savedMemento + ' - Oppia Editor'); + this.explorationTitleService.savedMemento + ' - Oppia Editor' + ); } else { this.pageTitleService.setDocumentTitle( - 'Untitled Exploration - Oppia Editor'); + 'Untitled Exploration - Oppia Editor' + ); } } /** ****************************************** - * Methods affecting the graph visualization. - ********************************************/ + * Methods affecting the graph visualization. + ********************************************/ toggleExplorationWarningVisibility(): void { - this.areExplorationWarningsVisible = ( - !this.areExplorationWarningsVisible); + this.areExplorationWarningsVisible = !this.areExplorationWarningsVisible; } getExplorationUrl(explorationId: string): string { - return explorationId ? ('/explore/' + explorationId) : ''; + return explorationId ? '/explore/' + explorationId : ''; } // Initializes the exploration page using data from the backend. @@ -209,14 +209,17 @@ export class ExplorationEditorPageComponent implements OnInit, OnDestroy { this.explorationDataService.getDataAsync((explorationId, lostChanges) => { if (!this.autosaveInfoModalsService.isModalOpen()) { this.autosaveInfoModalsService.showLostChangesModal( - lostChanges, explorationId); + lostChanges, + explorationId + ); } }), this.explorationFeaturesBackendApiService.fetchExplorationFeaturesAsync( - this.contextService.getExplorationId()), + this.contextService.getExplorationId() + ), this.threadDataBackendApiService.getFeedbackThreadsAsync(), - this.userService.getUserInfoAsync() - ]).then(async([explorationData, featuresData, _, userInfo]) => { + this.userService.getUserInfoAsync(), + ]).then(async ([explorationData, featuresData, _, userInfo]) => { if ((explorationData as ExplorationData).exploration_is_linked_to_story) { this.explorationIsLinkedToStory = true; this.contextService.setExplorationIsLinkedToStory(); @@ -225,35 +228,49 @@ export class ExplorationEditorPageComponent implements OnInit, OnDestroy { this.explorationFeaturesService.init(explorationData, featuresData); this.stateClassifierMappingService.init( - this.contextService.getExplorationId(), explorationData.version); + this.contextService.getExplorationId(), + explorationData.version + ); this.explorationStatesService.init( explorationData.states, - (explorationData as ExplorationData).exploration_is_linked_to_story); + (explorationData as ExplorationData).exploration_is_linked_to_story + ); this.entityTranslationsService.init( - this.explorationId, 'exploration', explorationData.version); + this.explorationId, + 'exploration', + explorationData.version + ); this.explorationTitleService.init(explorationData.title); this.explorationCategoryService.init( - (explorationData as ExplorationData).category); - this.explorationObjectiveService.init(( - explorationData as ExplorationData).objective); - this.explorationLanguageCodeService.init( - explorationData.language_code); + (explorationData as ExplorationData).category + ); + this.explorationObjectiveService.init( + (explorationData as ExplorationData).objective + ); + this.explorationLanguageCodeService.init(explorationData.language_code); this.explorationInitStateNameService.init( - explorationData.init_state_name); + explorationData.init_state_name + ); this.explorationTagsService.init( - (explorationData as ExplorationData).tags); + (explorationData as ExplorationData).tags + ); this.explorationParamSpecsService.init( this.paramSpecsObjectFactory.createFromBackendDict( explorationData.param_specs as ParamSpecsBackendDict - )); + ) + ); this.explorationParamChangesService.init( this.paramChangesObjectFactory.createFromBackendList( - explorationData.param_changes)); + explorationData.param_changes + ) + ); this.explorationAutomaticTextToSpeechService.init( - explorationData.auto_tts_enabled); + explorationData.auto_tts_enabled + ); this.explorationNextContentIdIndexService.init( - explorationData.next_content_id_index); + explorationData.next_content_id_index + ); if (explorationData.edits_allowed) { this.editabilityService.lockExploration(false); } @@ -271,15 +288,17 @@ export class ExplorationEditorPageComponent implements OnInit, OnDestroy { (explorationData as ExplorationData).rights.status, (explorationData as ExplorationData).rights.cloned_from, (explorationData as ExplorationData).rights.community_owned, - (explorationData as ExplorationData).rights.viewable_if_private); + (explorationData as ExplorationData).rights.viewable_if_private + ); this.userEmailPreferencesService.init( - ( - explorationData as ExplorationData - ).email_preferences.mute_feedback_notifications, (explorationData as ExplorationData).email_preferences - .mute_suggestion_notifications); + .mute_feedback_notifications, + (explorationData as ExplorationData).email_preferences + .mute_suggestion_notifications + ); - this.userExplorationPermissionsService.getPermissionsAsync() + this.userExplorationPermissionsService + .getPermissionsAsync() .then(permissions => { if (permissions.canEdit) { this.editabilityService.markEditable(); @@ -293,16 +312,23 @@ export class ExplorationEditorPageComponent implements OnInit, OnDestroy { this.graphDataService.recompute(); - if (!this.stateEditorService.getActiveStateName() || + if ( + !this.stateEditorService.getActiveStateName() || !this.explorationStatesService.getState( - this.stateEditorService.getActiveStateName())) { + this.stateEditorService.getActiveStateName() + ) + ) { this.stateEditorService.setActiveStateName( - this.explorationInitStateNameService.displayed as string); + this.explorationInitStateNameService.displayed as string + ); } - if (!this.routerService.isLocationSetToNonStateEditorTab() && + if ( + !this.routerService.isLocationSetToNonStateEditorTab() && !explorationData.states.hasOwnProperty( - this.routerService.getCurrentStateFromLocationPath())) { + this.routerService.getCurrentStateFromLocationPath() + ) + ) { if (this.threadDataBackendApiService.getOpenThreadsCount() > 0) { this.routerService.navigateToFeedbackTab(); } else { @@ -313,37 +339,45 @@ export class ExplorationEditorPageComponent implements OnInit, OnDestroy { // Initialize changeList by draft changes if they exist. if (explorationData.draft_changes !== null) { this.changeListService.loadAutosavedChangeList( - explorationData.draft_changes); + explorationData.draft_changes + ); } - if (explorationData.is_version_of_draft_valid === false && + if ( + explorationData.is_version_of_draft_valid === false && explorationData.draft_changes !== null && - explorationData.draft_changes.length > 0) { + explorationData.draft_changes.length > 0 + ) { // Show modal displaying lost changes if the version of draft // changes is invalid, and draft_changes is not `null`. this.autosaveInfoModalsService.showVersionMismatchModal( - this.changeListService.getChangeList()); + this.changeListService.getChangeList() + ); } this.routerService.onRefreshStatisticsTab.emit(); this.routerService.onRefreshVersionHistory.emit({ - forceRefresh: true + forceRefresh: true, }); - if (this.explorationStatesService.getState( - this.stateEditorService.getActiveStateName())) { + if ( + this.explorationStatesService.getState( + this.stateEditorService.getActiveStateName() + ) + ) { this.stateEditorRefreshService.onRefreshStateEditor.emit(); } this.stateTutorialFirstTimeService.initEditor( (explorationData as ExplorationData).show_state_editor_tutorial_on_load, - this.explorationId); - - if (( - explorationData as ExplorationData - ).show_state_translation_tutorial_on_load) { - this.stateTutorialFirstTimeService - .markTranslationTutorialNotSeenBefore(); + this.explorationId + ); + + if ( + (explorationData as ExplorationData) + .show_state_translation_tutorial_on_load + ) { + this.stateTutorialFirstTimeService.markTranslationTutorialNotSeenBefore(); } // TODO(#13352): Initialize StateTopAnswersStatsService and register @@ -377,7 +411,8 @@ export class ExplorationEditorPageComponent implements OnInit, OnDestroy { skipEditorNavbar(): void { let mainContentElement: HTMLElement | null = document.querySelector( - '.exploration-editor-content'); + '.exploration-editor-content' + ); mainContentElement.tabIndex = -1; mainContentElement.scrollIntoView(); @@ -407,27 +442,36 @@ export class ExplorationEditorPageComponent implements OnInit, OnDestroy { showWelcomeExplorationModal(): void { if (this.isModalOpenable) { this.isModalOpenable = false; - this.ngbModal.open(WelcomeModalComponent, { - backdrop: true, - windowClass: 'oppia-welcome-modal' - }).result.then((explorationId) => { - this.siteAnalyticsService.registerAcceptTutorialModalEvent( - explorationId); - this.startEditorTutorial(); - this.isModalOpenable = true; - }, (explorationId) => { - this.siteAnalyticsService.registerDeclineTutorialModalEvent( - explorationId); - this.stateTutorialFirstTimeService.markEditorTutorialFinished(); - this.isModalOpenable = true; - }); + this.ngbModal + .open(WelcomeModalComponent, { + backdrop: true, + windowClass: 'oppia-welcome-modal', + }) + .result.then( + explorationId => { + this.siteAnalyticsService.registerAcceptTutorialModalEvent( + explorationId + ); + this.startEditorTutorial(); + this.isModalOpenable = true; + }, + explorationId => { + this.siteAnalyticsService.registerDeclineTutorialModalEvent( + explorationId + ); + this.stateTutorialFirstTimeService.markEditorTutorialFinished(); + this.isModalOpenable = true; + } + ); } } generateAriaLabelForWarnings(): string { - const warnings = this.getWarnings() as { message: string }[]; - const warningLabels = warnings.map( - (warning, index) => 'Warning ' + (index + 1) + ': ' + warning.message) + const warnings = this.getWarnings() as {message: string}[]; + const warningLabels = warnings + .map( + (warning, index) => 'Warning ' + (index + 1) + ': ' + warning.message + ) .join('. '); return 'Total warnings: ' + this.countWarnings() + '. ' + warningLabels; @@ -492,20 +536,25 @@ export class ExplorationEditorPageComponent implements OnInit, OnDestroy { this.siteAnalyticsService.registerClickHelpButtonEvent(explorationId); let EDITOR_TUTORIAL_MODE = 'editor'; let TRANSLATION_TUTORIAL_MODE = 'translation'; - this.ngbModal.open(HelpModalComponent, { - backdrop: true, - windowClass: 'oppia-help-modal' - }).result.then(mode => { - if (mode === EDITOR_TUTORIAL_MODE) { - this.stateTutorialFirstTimeService.onOpenEditorTutorial.emit(); - } else if (mode === TRANSLATION_TUTORIAL_MODE) { - this.stateTutorialFirstTimeService.onOpenTranslationTutorial.emit(); - } - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + this.ngbModal + .open(HelpModalComponent, { + backdrop: true, + windowClass: 'oppia-help-modal', + }) + .result.then( + mode => { + if (mode === EDITOR_TUTORIAL_MODE) { + this.stateTutorialFirstTimeService.onOpenEditorTutorial.emit(); + } else if (mode === TRANSLATION_TUTORIAL_MODE) { + this.stateTutorialFirstTimeService.onOpenTranslationTutorial.emit(); + } + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } isWarningsAreShown(value: boolean): void { @@ -530,20 +579,23 @@ export class ExplorationEditorPageComponent implements OnInit, OnDestroy { if (internetAccessible) { this.alertsService.addSuccessMessage( 'Reconnected. Checking whether your changes are mergeable.', - this.reconnectedMessageTimeoutMilliseconds); + this.reconnectedMessageTimeoutMilliseconds + ); this.preventPageUnloadEventService.removeListener(); } else { this.alertsService.addInfoMessage( 'Looks like you are offline. ' + - 'You can continue working, and can save ' + - 'your changes once reconnected.', - this.disconnectedMessageTimeoutMilliseconds); + 'You can continue working, and can save ' + + 'your changes once reconnected.', + this.disconnectedMessageTimeoutMilliseconds + ); this.preventPageUnloadEventService.addListener(); if (this.routerService.getActiveTabName() !== 'main') { this.selectMainTab(); } } - }) + } + ) ); this.directiveSubscriptions.add( @@ -554,62 +606,61 @@ export class ExplorationEditorPageComponent implements OnInit, OnDestroy { ) ); - this.screenIsLarge = (this.windowDimensionsService.getWidth() >= 1024); + this.screenIsLarge = this.windowDimensionsService.getWidth() >= 1024; this.bottomNavbarStatusService.markBottomNavbarStatus(true); this.directiveSubscriptions.add( - this.explorationSaveService.onInitExplorationPage.subscribe( - () => { - this.initExplorationPage(); - } - ) + this.explorationSaveService.onInitExplorationPage.subscribe(() => { + this.initExplorationPage(); + }) ); this.directiveSubscriptions.add( this.explorationStatesService.onRefreshGraph.subscribe(() => { this.graphDataService.recompute(); this.explorationWarningsService.updateWarnings(); - })); - - this.directiveSubscriptions.add( - // eslint-disable-next-line max-len - this.stateTutorialFirstTimeService.onEnterEditorForTheFirstTime.subscribe(() => { - this.showWelcomeExplorationModal(); }) ); this.directiveSubscriptions.add( - this.stateTutorialFirstTimeService.onOpenEditorTutorial.subscribe( + // eslint-disable-next-line max-len + this.stateTutorialFirstTimeService.onEnterEditorForTheFirstTime.subscribe( () => { - this.startEditorTutorial(); - }) + this.showWelcomeExplorationModal(); + } + ) ); this.directiveSubscriptions.add( - this.routerService.onRefreshTranslationTab.subscribe(() => { + this.stateTutorialFirstTimeService.onOpenEditorTutorial.subscribe(() => { + this.startEditorTutorial(); }) ); + this.directiveSubscriptions.add( + this.routerService.onRefreshTranslationTab.subscribe(() => {}) + ); + this.directiveSubscriptions.add( this.stateTutorialFirstTimeService.onOpenTranslationTutorial.subscribe( () => { this.startTranslationTutorial(); - }) + } + ) ); /** ******************************************************** - * Called on initial load of the exploration editor page. - *********************************************************/ + * Called on initial load of the exploration editor page. + *********************************************************/ this.loaderService.showLoadingScreen('Loading'); this.explorationId = this.contextService.getExplorationId(); this.explorationUrl = '/create/' + this.explorationId; - this.explorationDownloadUrl = ( - '/createhandler/download/' + this.explorationId); - this.checkRevertExplorationValidUrl = ( - '/createhandler/check_revert_valid/' + this.explorationId); - this.revertExplorationUrl = ( - '/createhandler/revert/' + this.explorationId); + this.explorationDownloadUrl = + '/createhandler/download/' + this.explorationId; + this.checkRevertExplorationValidUrl = + '/createhandler/check_revert_valid/' + this.explorationId; + this.revertExplorationUrl = '/createhandler/revert/' + this.explorationId; this.areExplorationWarningsVisible = false; // The initExplorationPage function is written separately since it @@ -619,10 +670,10 @@ export class ExplorationEditorPageComponent implements OnInit, OnDestroy { this.improvementsTabIsEnabled = false; Promise.resolve( - this.explorationImprovementsService.isImprovementsTabEnabledAsync()) - .then(improvementsTabIsEnabledResponse => { - this.improvementsTabIsEnabled = improvementsTabIsEnabledResponse; - }); + this.explorationImprovementsService.isImprovementsTabEnabledAsync() + ).then(improvementsTabIsEnabledResponse => { + this.improvementsTabIsEnabled = improvementsTabIsEnabledResponse; + }); this.initExplorationPage(); } @@ -636,7 +687,9 @@ export class ExplorationEditorPageComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive('explorationEditorPage', +angular.module('oppia').directive( + 'explorationEditorPage', downgradeComponent({ - component: ExplorationEditorPageComponent - }) as angular.IDirectiveFactory); + component: ExplorationEditorPageComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-editor-page/exploration-editor-page.constants.ajs.ts b/core/templates/pages/exploration-editor-page/exploration-editor-page.constants.ajs.ts index ae440af034bd..2ac1009d05ee 100644 --- a/core/templates/pages/exploration-editor-page/exploration-editor-page.constants.ajs.ts +++ b/core/templates/pages/exploration-editor-page/exploration-editor-page.constants.ajs.ts @@ -19,64 +19,110 @@ // TODO(#7092): Delete this file once migration is complete and these AngularJS // equivalents of the Angular constants are no longer needed. -import { ExplorationEditorPageConstants } from - 'pages/exploration-editor-page/exploration-editor-page.constants'; +import {ExplorationEditorPageConstants} from 'pages/exploration-editor-page/exploration-editor-page.constants'; -angular.module('oppia').constant( - 'EXPLORATION_TITLE_INPUT_FOCUS_LABEL', - ExplorationEditorPageConstants.EXPLORATION_TITLE_INPUT_FOCUS_LABEL); +angular + .module('oppia') + .constant( + 'EXPLORATION_TITLE_INPUT_FOCUS_LABEL', + ExplorationEditorPageConstants.EXPLORATION_TITLE_INPUT_FOCUS_LABEL + ); -angular.module('oppia').constant( - 'PARAM_ACTION_GET', ExplorationEditorPageConstants.PARAM_ACTION_GET); +angular + .module('oppia') + .constant( + 'PARAM_ACTION_GET', + ExplorationEditorPageConstants.PARAM_ACTION_GET + ); -angular.module('oppia').constant( - 'PARAM_ACTION_SET', ExplorationEditorPageConstants.PARAM_ACTION_SET); +angular + .module('oppia') + .constant( + 'PARAM_ACTION_SET', + ExplorationEditorPageConstants.PARAM_ACTION_SET + ); -angular.module('oppia').constant( - 'VOICEOVER_MODE', ExplorationEditorPageConstants.VOICEOVER_MODE); +angular + .module('oppia') + .constant('VOICEOVER_MODE', ExplorationEditorPageConstants.VOICEOVER_MODE); -angular.module('oppia').constant( - 'TRANSLATION_MODE', ExplorationEditorPageConstants.TRANSLATION_MODE); +angular + .module('oppia') + .constant( + 'TRANSLATION_MODE', + ExplorationEditorPageConstants.TRANSLATION_MODE + ); // When an unresolved answer's frequency exceeds this threshold, an exploration // will be blocked from being published until the answer is resolved. -angular.module('oppia').constant( - 'UNRESOLVED_ANSWER_FREQUENCY_THRESHOLD', - ExplorationEditorPageConstants.UNRESOLVED_ANSWER_FREQUENCY_THRESHOLD); +angular + .module('oppia') + .constant( + 'UNRESOLVED_ANSWER_FREQUENCY_THRESHOLD', + ExplorationEditorPageConstants.UNRESOLVED_ANSWER_FREQUENCY_THRESHOLD + ); // Constant for audio recording time limit. -angular.module('oppia').constant( - 'RECORDING_TIME_LIMIT', ExplorationEditorPageConstants.RECORDING_TIME_LIMIT); +angular + .module('oppia') + .constant( + 'RECORDING_TIME_LIMIT', + ExplorationEditorPageConstants.RECORDING_TIME_LIMIT + ); -angular.module('oppia').constant( - 'IMPROVE_TYPE_INCOMPLETE', - ExplorationEditorPageConstants.IMPROVE_TYPE_INCOMPLETE); +angular + .module('oppia') + .constant( + 'IMPROVE_TYPE_INCOMPLETE', + ExplorationEditorPageConstants.IMPROVE_TYPE_INCOMPLETE + ); -angular.module('oppia').constant( - 'DEFAULT_AUDIO_LANGUAGE', - ExplorationEditorPageConstants.DEFAULT_AUDIO_LANGUAGE); +angular + .module('oppia') + .constant( + 'DEFAULT_AUDIO_LANGUAGE', + ExplorationEditorPageConstants.DEFAULT_AUDIO_LANGUAGE + ); -angular.module('oppia').constant( - 'INFO_MESSAGE_SOLUTION_IS_VALID', - ExplorationEditorPageConstants.INFO_MESSAGE_SOLUTION_IS_VALID); +angular + .module('oppia') + .constant( + 'INFO_MESSAGE_SOLUTION_IS_VALID', + ExplorationEditorPageConstants.INFO_MESSAGE_SOLUTION_IS_VALID + ); -angular.module('oppia').constant( - 'INFO_MESSAGE_SOLUTION_IS_INVALID_FOR_CURRENT_RULE', - ExplorationEditorPageConstants - .INFO_MESSAGE_SOLUTION_IS_INVALID_FOR_CURRENT_RULE); +angular + .module('oppia') + .constant( + 'INFO_MESSAGE_SOLUTION_IS_INVALID_FOR_CURRENT_RULE', + ExplorationEditorPageConstants.INFO_MESSAGE_SOLUTION_IS_INVALID_FOR_CURRENT_RULE + ); -angular.module('oppia').constant( - 'STATUS_COMPLIMENT', ExplorationEditorPageConstants.STATUS_COMPLIMENT); -angular.module('oppia').constant( - 'STATUS_FIXED', ExplorationEditorPageConstants.STATUS_FIXED); -angular.module('oppia').constant( - 'STATUS_IGNORED', ExplorationEditorPageConstants.STATUS_IGNORED); -angular.module('oppia').constant( - 'STATUS_NOT_ACTIONABLE', - ExplorationEditorPageConstants.STATUS_NOT_ACTIONABLE); -angular.module('oppia').constant( - 'STATUS_OPEN', ExplorationEditorPageConstants.STATUS_OPEN); +angular + .module('oppia') + .constant( + 'STATUS_COMPLIMENT', + ExplorationEditorPageConstants.STATUS_COMPLIMENT + ); +angular + .module('oppia') + .constant('STATUS_FIXED', ExplorationEditorPageConstants.STATUS_FIXED); +angular + .module('oppia') + .constant('STATUS_IGNORED', ExplorationEditorPageConstants.STATUS_IGNORED); +angular + .module('oppia') + .constant( + 'STATUS_NOT_ACTIONABLE', + ExplorationEditorPageConstants.STATUS_NOT_ACTIONABLE + ); +angular + .module('oppia') + .constant('STATUS_OPEN', ExplorationEditorPageConstants.STATUS_OPEN); -angular.module('oppia').constant( - 'COMPONENT_NAME_DEFAULT_OUTCOME', - ExplorationEditorPageConstants.COMPONENT_NAME_DEFAULT_OUTCOME); +angular + .module('oppia') + .constant( + 'COMPONENT_NAME_DEFAULT_OUTCOME', + ExplorationEditorPageConstants.COMPONENT_NAME_DEFAULT_OUTCOME + ); diff --git a/core/templates/pages/exploration-editor-page/exploration-editor-page.constants.ts b/core/templates/pages/exploration-editor-page/exploration-editor-page.constants.ts index dbe72482b1ed..801f5846edba 100644 --- a/core/templates/pages/exploration-editor-page/exploration-editor-page.constants.ts +++ b/core/templates/pages/exploration-editor-page/exploration-editor-page.constants.ts @@ -18,8 +18,7 @@ */ export const ExplorationEditorPageConstants = { - EXPLORATION_TITLE_INPUT_FOCUS_LABEL: - 'explorationTitleInputFocusLabel', + EXPLORATION_TITLE_INPUT_FOCUS_LABEL: 'explorationTitleInputFocusLabel', PARAM_ACTION_GET: 'get', @@ -45,8 +44,7 @@ export const ExplorationEditorPageConstants = { DEFAULT_AUDIO_LANGUAGE: 'en', - INFO_MESSAGE_SOLUTION_IS_VALID: - 'The solution is now valid!', + INFO_MESSAGE_SOLUTION_IS_VALID: 'The solution is now valid!', INFO_MESSAGE_SOLUTION_IS_INVALID_FOR_CURRENT_RULE: 'The current solution is no longer valid.', @@ -57,5 +55,5 @@ export const ExplorationEditorPageConstants = { STATUS_FIXED: 'fixed', STATUS_IGNORED: 'ignored', STATUS_NOT_ACTIONABLE: 'not_actionable', - STATUS_OPEN: 'open' + STATUS_OPEN: 'open', } as const; diff --git a/core/templates/pages/exploration-editor-page/exploration-editor-page.import.ts b/core/templates/pages/exploration-editor-page/exploration-editor-page.import.ts index a5b97cb6a07f..804085ab0cf1 100644 --- a/core/templates/pages/exploration-editor-page/exploration-editor-page.import.ts +++ b/core/templates/pages/exploration-editor-page/exploration-editor-page.import.ts @@ -27,10 +27,16 @@ import 'third-party-imports/skulpt.import'; import 'third-party-imports/ui-tree.import'; angular.module('oppia', [ - require('angular-cookies'), 'ngAnimate', + require('angular-cookies'), + 'ngAnimate', 'ngMaterial', - 'ngSanitize', 'ngTouch', 'pascalprecht.translate', - 'ui.bootstrap', 'ui-leaflet', 'ui.tree', uiValidate, + 'ngSanitize', + 'ngTouch', + 'pascalprecht.translate', + 'ui.bootstrap', + 'ui-leaflet', + 'ui.tree', + uiValidate, ]); require('Polyfills.ts'); @@ -43,12 +49,15 @@ require('base-components/oppia-root.directive.ts'); require( 'pages/exploration-editor-page/editor-navigation/' + - 'editor-navbar-breadcrumb.component.ts'); + 'editor-navbar-breadcrumb.component.ts' +); require( 'pages/exploration-editor-page/editor-navigation/' + - 'editor-navigation.component.ts'); + 'editor-navigation.component.ts' +); require( 'pages/exploration-editor-page/exploration-save-and-publish-buttons/' + - 'exploration-save-and-publish-buttons.component.ts'); + 'exploration-save-and-publish-buttons.component.ts' +); require('pages/exploration-editor-page/exploration-editor-page.component.ts'); require('base-components/base-content.component.ts'); diff --git a/core/templates/pages/exploration-editor-page/exploration-editor-page.module.ts b/core/templates/pages/exploration-editor-page/exploration-editor-page.module.ts index 591472c43e93..684f95ac0cb7 100644 --- a/core/templates/pages/exploration-editor-page/exploration-editor-page.module.ts +++ b/core/templates/pages/exploration-editor-page/exploration-editor-page.module.ts @@ -16,89 +16,90 @@ * @fileoverview Module for the exploration editor page. */ -import { APP_INITIALIZER, NgModule, StaticProvider } from '@angular/core'; -import { BrowserModule, HAMMER_GESTURE_CONFIG } from '@angular/platform-browser'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { HttpClientModule } from '@angular/common/http'; -import { HTTP_INTERCEPTORS } from '@angular/common/http'; -import { RouterModule } from '@angular/router'; -import { APP_BASE_HREF } from '@angular/common'; +import {APP_INITIALIZER, NgModule, StaticProvider} from '@angular/core'; +import {BrowserModule, HAMMER_GESTURE_CONFIG} from '@angular/platform-browser'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {HttpClientModule} from '@angular/common/http'; +import {HTTP_INTERCEPTORS} from '@angular/common/http'; +import {RouterModule} from '@angular/router'; +import {APP_BASE_HREF} from '@angular/common'; -import { JoyrideModule } from 'ngx-joyride'; -import { MatPaginatorModule } from '@angular/material/paginator'; -import { MatMenuModule } from '@angular/material/menu'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { OppiaAngularRootComponent } from - 'components/oppia-angular-root.component'; -import { platformFeatureInitFactory, PlatformFeatureService } from - 'services/platform-feature.service'; -import { RequestInterceptor } from 'services/request-interceptor.service'; -import { StateParamChangesEditorComponent } from './editor-tab/state-param-changes-editor/state-param-changes-editor.component'; -import { DeleteStateSkillModalComponent } from './editor-tab/templates/modal-templates/delete-state-skill-modal.component'; -import { InteractionExtensionsModule } from 'interactions/interactions.module'; -import { SaveVersionMismatchModalComponent } from './modal-templates/save-version-mismatch-modal.component'; -import { SaveValidationFailModalComponent } from './modal-templates/save-validation-fail-modal.component'; -import { ChangesInHumanReadableFormComponent } from './changes-in-human-readable-form/changes-in-human-readable-form.component'; -import { LostChangesModalComponent } from './modal-templates/lost-changes-modal.component'; -import { WelcomeModalComponent } from './modal-templates/welcome-modal.component'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { StateDiffModalComponent } from './modal-templates/state-diff-modal.component'; -import { PostPublishModalComponent } from './modal-templates/post-publish-modal.component'; -import { ExplorationPublishModalComponent } from 'pages/exploration-editor-page/modal-templates/exploration-publish-modal.component'; -import { EditorReloadingModalComponent } from './modal-templates/editor-reloading-modal.component'; -import { ConfirmDiscardChangesModalComponent } from './modal-templates/confirm-discard-changes-modal.component'; -import { CreateFeedbackThreadModalComponent } from './feedback-tab/templates/create-feedback-thread-modal.component'; -import { PreviewSummaryTileModalComponent } from './settings-tab/templates/preview-summary-tile-modal.component'; -import { WelcomeTranslationModalComponent } from './translation-tab/modal-templates/welcome-translation-modal.component'; -import { DeleteExplorationModalComponent } from './settings-tab/templates/delete-exploration-modal.component'; -import { RemoveRoleConfirmationModalComponent } from './settings-tab/templates/remove-role-confirmation-modal.component'; -import { ReassignRoleConfirmationModalComponent } from './settings-tab/templates/reassign-role-confirmation-modal.component'; -import { ModeratorUnpublishExplorationModalComponent } from './settings-tab/templates/moderator-unpublish-exploration-modal.component'; -import { TransferExplorationOwnershipModalComponent } from './settings-tab/templates/transfer-exploration-ownership-modal.component'; -import { HelpModalComponent } from './modal-templates/help-modal.component'; -import { DeleteAudioTranslationModalComponent } from './translation-tab/modal-templates/delete-audio-translation-modal.component'; -import { TranslationTabBusyModalComponent } from './translation-tab/modal-templates/translation-tab-busy-modal.component'; -import { ConfirmDeleteStateModalComponent } from './editor-tab/templates/modal-templates/confirm-delete-state-modal.component'; -import { PreviewSetParametersModalComponent } from './preview-tab/templates/preview-set-parameters-modal.component'; -import { CheckRevertExplorationModalComponent } from './history-tab/modal-templates/check-revert-exploration-modal.component'; -import { RevertExplorationModalComponent } from './history-tab/modal-templates/revert-exploration-modal.component'; -import { ExplorationMetadataDiffModalComponent } from './modal-templates/exploration-metadata-diff-modal.component'; -import { SmartRouterModule } from 'hybrid-router-module-provider'; -import { AppErrorHandlerProvider } from 'pages/oppia-root/app-error-handler'; -import { ExplorationTitleEditorComponent } from './exploration-title-editor/exploration-title-editor.component'; -import { ExplorationObjectiveEditorComponent } from './exploration-objective-editor/exploration-objective-editor.component'; -import { ExplorationMetadataModalComponent } from 'pages/exploration-editor-page/modal-templates/exploration-metadata-modal.component'; -import { MatAutocompleteModule } from '@angular/material/autocomplete'; -import { ExplorationSaveModalComponent } from './modal-templates/exploration-save-modal.component'; -import { EditorNavbarBreadcrumbComponent } from './editor-navigation/editor-navbar-breadcrumb.component'; -import { ExplorationGraphModalComponent } from './editor-tab/templates/modal-templates/exploration-graph-modal.component'; -import { ExplorationGraphComponent } from './editor-tab/graph-directives/exploration-graph.component'; -import { StateNameEditorComponent } from './editor-tab/state-name-editor/state-name-editor.component'; -import { EditorNavigationComponent } from './editor-navigation/editor-navigation.component'; -import { TeachOppiaModalComponent } from './editor-tab/templates/modal-templates/teach-oppia-modal.component'; -import { SettingsTabComponent } from './settings-tab/settings-tab.component'; -import { UnresolvedAnswersOverviewComponent } from './editor-tab/unresolved-answers-overview/unresolved-answers-overview.component'; -import { PreviewTabComponent } from './preview-tab/preview-tab.component'; -import { HistoryTabComponent } from './history-tab/history-tab.component'; -import { FeedbackTabComponent } from './feedback-tab/feedback-tab.component'; -import { ImprovementsTabComponent } from './improvements-tab/improvements-tab.component'; -import { NeedsGuidingResponsesTaskComponent } from './improvements-tab/needs-guiding-responses-task.component'; -import { StatisticsTabComponent } from './statistics-tab/statistics-tab.component'; -import { StateStatsModalComponent } from './statistics-tab/templates/state-stats-modal.component'; -import { PieChartComponent } from './statistics-tab/charts/pie-chart.component'; -import { ExplorationEditorTabComponent } from './editor-tab/exploration-editor-tab.component'; -import { ExplorationSaveAndPublishButtonsComponent } from './exploration-save-and-publish-buttons/exploration-save-and-publish-buttons.component'; -import { ExplorationSavePromptModalComponent } from './modal-templates/exploration-save-prompt-modal.component'; -import { AddAudioTranslationModalComponent } from './translation-tab/modal-templates/add-audio-translation-modal.component'; -import { AudioTranslationBarComponent } from './translation-tab/audio-translation-bar/audio-translation-bar.component'; -import { StateTranslationEditorComponent } from './translation-tab/state-translation-editor/state-translation-editor.component'; -import { StateTranslationComponent } from './translation-tab/state-translation/state-translation.component'; -import { TranslatorOverviewComponent } from './translation-tab/translator-overview/translator-overview.component'; -import { StateTranslationStatusGraphComponent } from './translation-tab/state-translation-status-graph/state-translation-status-graph.component'; -import { TranslationTabComponent } from './translation-tab/translation-tab.component'; -import { ValueGeneratorEditorComponent } from './param-changes-editor/value-generator-editor.component'; -import { ParamChangesEditorComponent } from './param-changes-editor/param-changes-editor.component'; -import { ExplorationEditorPageComponent } from './exploration-editor-page.component'; +import {JoyrideModule} from 'ngx-joyride'; +import {MatPaginatorModule} from '@angular/material/paginator'; +import {MatMenuModule} from '@angular/material/menu'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {OppiaAngularRootComponent} from 'components/oppia-angular-root.component'; +import { + platformFeatureInitFactory, + PlatformFeatureService, +} from 'services/platform-feature.service'; +import {RequestInterceptor} from 'services/request-interceptor.service'; +import {StateParamChangesEditorComponent} from './editor-tab/state-param-changes-editor/state-param-changes-editor.component'; +import {DeleteStateSkillModalComponent} from './editor-tab/templates/modal-templates/delete-state-skill-modal.component'; +import {InteractionExtensionsModule} from 'interactions/interactions.module'; +import {SaveVersionMismatchModalComponent} from './modal-templates/save-version-mismatch-modal.component'; +import {SaveValidationFailModalComponent} from './modal-templates/save-validation-fail-modal.component'; +import {ChangesInHumanReadableFormComponent} from './changes-in-human-readable-form/changes-in-human-readable-form.component'; +import {LostChangesModalComponent} from './modal-templates/lost-changes-modal.component'; +import {WelcomeModalComponent} from './modal-templates/welcome-modal.component'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {StateDiffModalComponent} from './modal-templates/state-diff-modal.component'; +import {PostPublishModalComponent} from './modal-templates/post-publish-modal.component'; +import {ExplorationPublishModalComponent} from 'pages/exploration-editor-page/modal-templates/exploration-publish-modal.component'; +import {EditorReloadingModalComponent} from './modal-templates/editor-reloading-modal.component'; +import {ConfirmDiscardChangesModalComponent} from './modal-templates/confirm-discard-changes-modal.component'; +import {CreateFeedbackThreadModalComponent} from './feedback-tab/templates/create-feedback-thread-modal.component'; +import {PreviewSummaryTileModalComponent} from './settings-tab/templates/preview-summary-tile-modal.component'; +import {WelcomeTranslationModalComponent} from './translation-tab/modal-templates/welcome-translation-modal.component'; +import {DeleteExplorationModalComponent} from './settings-tab/templates/delete-exploration-modal.component'; +import {RemoveRoleConfirmationModalComponent} from './settings-tab/templates/remove-role-confirmation-modal.component'; +import {ReassignRoleConfirmationModalComponent} from './settings-tab/templates/reassign-role-confirmation-modal.component'; +import {ModeratorUnpublishExplorationModalComponent} from './settings-tab/templates/moderator-unpublish-exploration-modal.component'; +import {TransferExplorationOwnershipModalComponent} from './settings-tab/templates/transfer-exploration-ownership-modal.component'; +import {HelpModalComponent} from './modal-templates/help-modal.component'; +import {DeleteAudioTranslationModalComponent} from './translation-tab/modal-templates/delete-audio-translation-modal.component'; +import {TranslationTabBusyModalComponent} from './translation-tab/modal-templates/translation-tab-busy-modal.component'; +import {ConfirmDeleteStateModalComponent} from './editor-tab/templates/modal-templates/confirm-delete-state-modal.component'; +import {PreviewSetParametersModalComponent} from './preview-tab/templates/preview-set-parameters-modal.component'; +import {CheckRevertExplorationModalComponent} from './history-tab/modal-templates/check-revert-exploration-modal.component'; +import {RevertExplorationModalComponent} from './history-tab/modal-templates/revert-exploration-modal.component'; +import {ExplorationMetadataDiffModalComponent} from './modal-templates/exploration-metadata-diff-modal.component'; +import {SmartRouterModule} from 'hybrid-router-module-provider'; +import {AppErrorHandlerProvider} from 'pages/oppia-root/app-error-handler'; +import {ExplorationTitleEditorComponent} from './exploration-title-editor/exploration-title-editor.component'; +import {ExplorationObjectiveEditorComponent} from './exploration-objective-editor/exploration-objective-editor.component'; +import {ExplorationMetadataModalComponent} from 'pages/exploration-editor-page/modal-templates/exploration-metadata-modal.component'; +import {MatAutocompleteModule} from '@angular/material/autocomplete'; +import {ExplorationSaveModalComponent} from './modal-templates/exploration-save-modal.component'; +import {EditorNavbarBreadcrumbComponent} from './editor-navigation/editor-navbar-breadcrumb.component'; +import {ExplorationGraphModalComponent} from './editor-tab/templates/modal-templates/exploration-graph-modal.component'; +import {ExplorationGraphComponent} from './editor-tab/graph-directives/exploration-graph.component'; +import {StateNameEditorComponent} from './editor-tab/state-name-editor/state-name-editor.component'; +import {EditorNavigationComponent} from './editor-navigation/editor-navigation.component'; +import {TeachOppiaModalComponent} from './editor-tab/templates/modal-templates/teach-oppia-modal.component'; +import {SettingsTabComponent} from './settings-tab/settings-tab.component'; +import {UnresolvedAnswersOverviewComponent} from './editor-tab/unresolved-answers-overview/unresolved-answers-overview.component'; +import {PreviewTabComponent} from './preview-tab/preview-tab.component'; +import {HistoryTabComponent} from './history-tab/history-tab.component'; +import {FeedbackTabComponent} from './feedback-tab/feedback-tab.component'; +import {ImprovementsTabComponent} from './improvements-tab/improvements-tab.component'; +import {NeedsGuidingResponsesTaskComponent} from './improvements-tab/needs-guiding-responses-task.component'; +import {StatisticsTabComponent} from './statistics-tab/statistics-tab.component'; +import {StateStatsModalComponent} from './statistics-tab/templates/state-stats-modal.component'; +import {PieChartComponent} from './statistics-tab/charts/pie-chart.component'; +import {ExplorationEditorTabComponent} from './editor-tab/exploration-editor-tab.component'; +import {ExplorationSaveAndPublishButtonsComponent} from './exploration-save-and-publish-buttons/exploration-save-and-publish-buttons.component'; +import {ExplorationSavePromptModalComponent} from './modal-templates/exploration-save-prompt-modal.component'; +import {AddAudioTranslationModalComponent} from './translation-tab/modal-templates/add-audio-translation-modal.component'; +import {AudioTranslationBarComponent} from './translation-tab/audio-translation-bar/audio-translation-bar.component'; +import {StateTranslationEditorComponent} from './translation-tab/state-translation-editor/state-translation-editor.component'; +import {StateTranslationComponent} from './translation-tab/state-translation/state-translation.component'; +import {TranslatorOverviewComponent} from './translation-tab/translator-overview/translator-overview.component'; +import {StateTranslationStatusGraphComponent} from './translation-tab/state-translation-status-graph/state-translation-status-graph.component'; +import {TranslationTabComponent} from './translation-tab/translation-tab.component'; +import {ValueGeneratorEditorComponent} from './param-changes-editor/value-generator-editor.component'; +import {ParamChangesEditorComponent} from './param-changes-editor/param-changes-editor.component'; +import {ExplorationEditorPageComponent} from './exploration-editor-page.component'; @NgModule({ imports: [ @@ -185,7 +186,7 @@ import { ExplorationEditorPageComponent } from './exploration-editor-page.compon StateTranslationStatusGraphComponent, TranslationTabComponent, ExplorationEditorPageComponent, - StateVersionHistoryComponent + StateVersionHistoryComponent, ], entryComponents: [ DeleteStateSkillModalComponent, @@ -251,48 +252,48 @@ import { ExplorationEditorPageComponent } from './exploration-editor-page.compon StateTranslationStatusGraphComponent, TranslationTabComponent, ExplorationEditorPageComponent, - StateVersionHistoryComponent + StateVersionHistoryComponent, ], providers: [ { provide: HTTP_INTERCEPTORS, useClass: RequestInterceptor, - multi: true + multi: true, }, { provide: APP_INITIALIZER, useFactory: platformFeatureInitFactory, deps: [PlatformFeatureService], - multi: true + multi: true, }, { provide: HAMMER_GESTURE_CONFIG, - useClass: MyHammerConfig + useClass: MyHammerConfig, }, AppErrorHandlerProvider, { provide: APP_BASE_HREF, - useValue: '/' - } - ] + useValue: '/', + }, + ], }) class ExplorationEditorPageModule { // Empty placeholder method to satisfy the `Compiler`. ngDoBootstrap() {} } -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { downgradeModule } from '@angular/upgrade/static'; -import { ToastrModule } from 'ngx-toastr'; -import { MyHammerConfig, toastrConfig } from 'pages/oppia-root/app.module'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { OppiaCkEditorCopyToolBarModule } from 'components/ck-editor-helpers/ck-editor-copy-toolbar/ck-editor-copy-toolbar.module'; -import { ExplorationPlayerViewerCommonModule } from 'pages/exploration-player-page/exploration-player-viewer-common.module'; -import { StateVersionHistoryModalComponent } from './modal-templates/state-version-history-modal.component'; -import { MetadataVersionHistoryModalComponent } from './modal-templates/metadata-version-history-modal.component'; -import { StateVersionHistoryComponent } from './editor-tab/state-version-history/state-version-history.component'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {downgradeModule} from '@angular/upgrade/static'; +import {ToastrModule} from 'ngx-toastr'; +import {MyHammerConfig, toastrConfig} from 'pages/oppia-root/app.module'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {OppiaCkEditorCopyToolBarModule} from 'components/ck-editor-helpers/ck-editor-copy-toolbar/ck-editor-copy-toolbar.module'; +import {ExplorationPlayerViewerCommonModule} from 'pages/exploration-player-page/exploration-player-viewer-common.module'; +import {StateVersionHistoryModalComponent} from './modal-templates/state-version-history-modal.component'; +import {MetadataVersionHistoryModalComponent} from './modal-templates/metadata-version-history-modal.component'; +import {StateVersionHistoryComponent} from './editor-tab/state-version-history/state-version-history.component'; -const bootstrapFnAsync = async(extraProviders: StaticProvider[]) => { +const bootstrapFnAsync = async (extraProviders: StaticProvider[]) => { const platformRef = platformBrowserDynamic(extraProviders); return platformRef.bootstrapModule(ExplorationEditorPageModule); }; @@ -307,5 +308,6 @@ angular.module('oppia').directive( // bootstrap the Angular 8. 'oppiaAngularRoot', downgradeComponent({ - component: OppiaAngularRootComponent - }) as angular.IDirectiveFactory); + component: OppiaAngularRootComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-editor-page/exploration-objective-editor/exploration-objective-editor.component.spec.ts b/core/templates/pages/exploration-editor-page/exploration-objective-editor/exploration-objective-editor.component.spec.ts index a01478bfc6d4..0d37d9362c2a 100644 --- a/core/templates/pages/exploration-editor-page/exploration-objective-editor/exploration-objective-editor.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/exploration-objective-editor/exploration-objective-editor.component.spec.ts @@ -16,12 +16,18 @@ * @fileoverview Unit tests for explorationObjectiveEditor component. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { ExplorationObjectiveEditorComponent } from './exploration-objective-editor.component'; -import { ExplorationObjectiveService } from '../services/exploration-objective.service'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {ExplorationObjectiveEditorComponent} from './exploration-objective-editor.component'; +import {ExplorationObjectiveService} from '../services/exploration-objective.service'; describe('Exploration Objective Editor Component', () => { let component: ExplorationObjectiveEditorComponent; @@ -29,18 +35,10 @@ describe('Exploration Objective Editor Component', () => { let explorationObjectiveService: ExplorationObjectiveService; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - FormsModule, - ReactiveFormsModule - ], - declarations: [ - ExplorationObjectiveEditorComponent - ], - providers: [ - ExplorationObjectiveService - ], - schemas: [NO_ERRORS_SCHEMA] + imports: [HttpClientTestingModule, FormsModule, ReactiveFormsModule], + declarations: [ExplorationObjectiveEditorComponent], + providers: [ExplorationObjectiveService], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -53,15 +51,13 @@ describe('Exploration Objective Editor Component', () => { fixture.detectChanges(); }); - it('should initialize controller properties after its initialization', - fakeAsync(() => { - spyOn( - component.onInputFieldBlur, 'emit').and.stub(); + it('should initialize controller properties after its initialization', fakeAsync(() => { + spyOn(component.onInputFieldBlur, 'emit').and.stub(); - component.inputFieldBlur(); - tick(); + component.inputFieldBlur(); + tick(); - expect(component.onInputFieldBlur.emit).toHaveBeenCalled(); - expect(component).toBeDefined(); - })); + expect(component.onInputFieldBlur.emit).toHaveBeenCalled(); + expect(component).toBeDefined(); + })); }); diff --git a/core/templates/pages/exploration-editor-page/exploration-objective-editor/exploration-objective-editor.component.ts b/core/templates/pages/exploration-editor-page/exploration-objective-editor/exploration-objective-editor.component.ts index 971f5ebc0e37..3aa61a871e82 100644 --- a/core/templates/pages/exploration-editor-page/exploration-objective-editor/exploration-objective-editor.component.ts +++ b/core/templates/pages/exploration-editor-page/exploration-objective-editor/exploration-objective-editor.component.ts @@ -16,13 +16,13 @@ * @fileoverview Component for the exploration objective/goal field in forms. */ -import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { ExplorationObjectiveService } from 'pages/exploration-editor-page/services/exploration-objective.service'; +import {Component, EventEmitter, Input, Output} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {ExplorationObjectiveService} from 'pages/exploration-editor-page/services/exploration-objective.service'; @Component({ selector: 'oppia-exploration-objective-editor', - templateUrl: './exploration-objective-editor.component.html' + templateUrl: './exploration-objective-editor.component.html', }) export class ExplorationObjectiveEditorComponent { // These properties below are initialized using Angular lifecycle hooks @@ -43,6 +43,8 @@ export class ExplorationObjectiveEditorComponent { } angular.module('oppia').directive( - 'oppiaExplorationObjectiveEditor', downgradeComponent({ - component: ExplorationObjectiveEditorComponent - })); + 'oppiaExplorationObjectiveEditor', + downgradeComponent({ + component: ExplorationObjectiveEditorComponent, + }) +); diff --git a/core/templates/pages/exploration-editor-page/exploration-save-and-publish-buttons/exploration-save-and-publish-buttons.component.spec.ts b/core/templates/pages/exploration-editor-page/exploration-save-and-publish-buttons/exploration-save-and-publish-buttons.component.spec.ts index 1c77be86dc12..9af6a0dd3620 100644 --- a/core/templates/pages/exploration-editor-page/exploration-save-and-publish-buttons/exploration-save-and-publish-buttons.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/exploration-save-and-publish-buttons/exploration-save-and-publish-buttons.component.spec.ts @@ -16,28 +16,34 @@ * @fileoverview Unit tests for explorationSaveAndPublishButtons. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed, fakeAsync, tick } from '@angular/core/testing'; -import { EventEmitter } from '@angular/core'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { FormsModule } from '@angular/forms'; -import { MatCardModule } from '@angular/material/card'; -import { MatMenuModule } from '@angular/material/menu'; -import { UserExplorationPermissionsService } from 'pages/exploration-editor-page/services/user-exploration-permissions.service'; -import { EditabilityService } from 'services/editability.service'; -import { ChangeListService } from '../services/change-list.service'; -import { ExplorationChange } from 'domain/exploration/exploration-draft.model'; -import { ExplorationSaveAndPublishButtonsComponent } from './exploration-save-and-publish-buttons.component'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { InternetConnectivityService } from 'services/internet-connectivity.service'; -import { ExplorationRightsService } from '../services/exploration-rights.service'; -import { ExplorationSaveService } from '../services/exploration-save.service'; -import { ExplorationWarningsService } from '../services/exploration-warnings.service'; -import { ExternalSaveService } from 'services/external-save.service'; -import { ExplorationSavePromptModalComponent } from '../modal-templates/exploration-save-prompt-modal.component'; -import { ExplorationPermissions } from 'domain/exploration/exploration-permissions.model'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { ContextService } from 'services/context.service'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + waitForAsync, + TestBed, + fakeAsync, + tick, +} from '@angular/core/testing'; +import {EventEmitter} from '@angular/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {FormsModule} from '@angular/forms'; +import {MatCardModule} from '@angular/material/card'; +import {MatMenuModule} from '@angular/material/menu'; +import {UserExplorationPermissionsService} from 'pages/exploration-editor-page/services/user-exploration-permissions.service'; +import {EditabilityService} from 'services/editability.service'; +import {ChangeListService} from '../services/change-list.service'; +import {ExplorationChange} from 'domain/exploration/exploration-draft.model'; +import {ExplorationSaveAndPublishButtonsComponent} from './exploration-save-and-publish-buttons.component'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {InternetConnectivityService} from 'services/internet-connectivity.service'; +import {ExplorationRightsService} from '../services/exploration-rights.service'; +import {ExplorationSaveService} from '../services/exploration-save.service'; +import {ExplorationWarningsService} from '../services/exploration-warnings.service'; +import {ExternalSaveService} from 'services/external-save.service'; +import {ExplorationSavePromptModalComponent} from '../modal-templates/exploration-save-prompt-modal.component'; +import {ExplorationPermissions} from 'domain/exploration/exploration-permissions.model'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {ContextService} from 'services/context.service'; describe('Exploration save and publish buttons component', () => { let component: ExplorationSaveAndPublishButtonsComponent; @@ -56,364 +62,419 @@ describe('Exploration save and publish buttons component', () => { let mockExternalSaveEventEmitter = new EventEmitter(); let mockConnectionServiceEmitter = new EventEmitter(); - class MockExternalSaveService { - onExternalSave = mockExternalSaveEventEmitter; - } - - class MockNgbModal { - open() { - return { - result: Promise.resolve() - }; - } - } - - class MockWindowRef { - location = { path: '/create/2234' }; - nativeWindow = { - scrollTo: (value1, value2) => {}, - sessionStorage: { - promoIsDismissed: null, - setItem: (testKey1, testKey2) => {}, - removeItem: (testKey) => {} - }, - gtag: (value1, value2, value3) => {}, - navigator: { - onLine: true, - userAgent: null - }, - location: { - path: '/create/2234', - pathname: '/', - hostname: 'oppiaserver.appspot.com', - search: '', - protocol: '', - reload: () => {}, - hash: '', - href: '', - }, - document: { - documentElement: { - setAttribute: (value1, value2) => {}, - clientWidth: null, - clientHeight: null, - }, - body: { - clientWidth: null, - clientHeight: null, - style: { - overflowY: '' - } - } - }, - addEventListener: (value1, value2) => {} - }; - } - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - FormsModule, - MatCardModule, - MatMenuModule, - ], - declarations: [ - ExplorationSaveAndPublishButtonsComponent, - ExplorationSavePromptModalComponent - ], - providers: [ - ChangeListService, - { - provide: ExternalSaveService, - useClass: MockExternalSaveService - }, - { - provide: WindowRef, - useClass: MockWindowRef - }, - { - provide: NgbModal, - useClass: MockNgbModal - } - ], - schemas: [NO_ERRORS_SCHEMA] - }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent( - ExplorationSaveAndPublishButtonsComponent); - component = fixture.componentInstance; - - changeListService = TestBed.inject(ChangeListService); - contextService = TestBed.inject(ContextService); - ngbModal = TestBed.inject(NgbModal); - ics = TestBed.inject(InternetConnectivityService); - explorationRightsService = TestBed.inject(ExplorationRightsService); - explorationSaveService = TestBed.inject(ExplorationSaveService); - explorationWarningsService = TestBed.inject(ExplorationWarningsService); - spyOn(contextService, 'getExplorationId').and.returnValue('exp1'); - editabilityService = TestBed.inject(EditabilityService); - userExplorationPermissionsService = TestBed.inject( - UserExplorationPermissionsService); - - let userPermissions = { - canPublish: true - }; - fetchPermissionsAsyncSpy = spyOn( - userExplorationPermissionsService, 'fetchPermissionsAsync'); - fetchPermissionsAsyncSpy.and - .returnValue(Promise.resolve(userPermissions as ExplorationPermissions)); - - spyOn(userExplorationPermissionsService, 'getPermissionsAsync').and - .returnValue(Promise.resolve({ - canPublish: true - } as ExplorationPermissions)); - spyOnProperty(ics, 'onInternetStateChange').and.returnValue( - mockConnectionServiceEmitter); - spyOn(explorationSaveService, 'saveChangesAsync').and - .callFake((showCallback, hideCallback) => { - showCallback(); - hideCallback(); - return Promise.resolve(); - }); - spyOn(explorationSaveService, 'showPublishExplorationModal').and - .callFake((showCallback, hideCallback) => { - showCallback(); - hideCallback(); - return Promise.resolve(); - }); - - component.ngOnInit(); - }); - - afterEach(() => { - component.ngOnDestroy(); - }); - - it('should initialize component properties after controller initialization', - () => { - expect(component.saveIsInProcess).toBe(false); - expect(component.publishIsInProcess).toBe(false); - expect(component.loadingDotsAreShown).toBe(false); - }); - - it('should save exploration when saving changes', fakeAsync(() => { - component.saveChanges(); - tick(); - - expect(component.saveIsInProcess).toBe(false); - expect(component.loadingDotsAreShown).toBe(false); - })); - - it('should check if exploration is editable', () => { - spyOn(editabilityService, 'isLockedByAdmin').and.returnValue(true); - expect(component.isLockedByAdmin()).toBe(true); - }); - - it('should publish exploration when show publish exploration is shown', - fakeAsync(() => { - component.showPublishExplorationModal(); - tick(); - - expect(component.publishIsInProcess).toBe(false); - expect(component.loadingDotsAreShown).toBe(false); - })); - - it('should resolve the warnings before saving exploration when exploration' + - ' has critical warnings', () => { - spyOn(explorationWarningsService, 'hasCriticalWarnings').and.returnValue( - true); - - expect(component.getSaveButtonTooltip()) - .toBe('Please resolve the warnings.'); - }); - - it('should save exploration draft when it has no warnings and exploration' + - ' is private', () => { - spyOn(explorationWarningsService, 'hasCriticalWarnings') - .and.returnValue(false); - spyOn(explorationRightsService, 'isPrivate').and.returnValue(true); - - expect(component.getSaveButtonTooltip()).toBe('Save Draft'); - }); - - it('should publish exploration changes when it has no warnings and it is' + - ' public', () => { - spyOn(explorationWarningsService, 'hasCriticalWarnings').and - .returnValue(false); - spyOn(explorationRightsService, 'isPrivate').and.returnValue(false); - expect(component.getSaveButtonTooltip()).toBe('Publish Changes'); - }); - - it('should ask user to resolve the warnings before publishing' + - ' exploration when exploration has warnings', () => { - spyOn(explorationWarningsService, 'countWarnings').and.returnValue(1); - expect(component.getPublishExplorationButtonTooltip()).toBe( - 'Please resolve the warnings before publishing.'); - }); - - it('should save exploration changes before publishing it when trying to' + - ' publish a changed exploration without saving it first', () => { - spyOn(explorationWarningsService, 'countWarnings').and.returnValue(0); - spyOn(changeListService, 'isExplorationLockedForEditing').and - .returnValue(true); - expect(component.getPublishExplorationButtonTooltip()).toBe( - 'Please save your changes before publishing.'); - }); - - it('should publish exploration when it is already saved', () => { - spyOn(explorationWarningsService, 'countWarnings').and.returnValue(0); - spyOn(changeListService, 'isExplorationLockedForEditing') - .and.returnValue(false); - expect(component.getPublishExplorationButtonTooltip()).toBe( - 'Publish to Oppia Library'); - }); - - it('should discard changes when exploration is changed', () => { - spyOn(explorationSaveService, 'discardChanges'); - component.discardChanges(); - expect(explorationSaveService.discardChanges).toHaveBeenCalled(); - }); - - it('should get whether exploration is saveable', () => { - spyOn(explorationSaveService, 'isExplorationSaveable') - .and.returnValue(true); - expect(component.isExplorationSaveable()).toBe(true); - }); - - it('should count changes made in an exploration', () => { - spyOn(changeListService, 'getChangeList').and.returnValue( - [{}, {}] as ExplorationChange[]); - expect(component.getChangeListLength()).toBe(2); - }); - - it('should save or publish exploration when editing outside tutorial mode' + - ' and exploration is translatable', () => { - spyOn(editabilityService, 'isEditableOutsideTutorialMode').and - .returnValue(false); - spyOn(editabilityService, 'isTranslatable').and.returnValue(true); - expect(component.isEditableOutsideTutorialMode()).toBe(true); - }); - - it('should save or publish exploration when editing outside tutorial mode' + - ' and exploration is not translatable', () => { - spyOn(editabilityService, 'isEditableOutsideTutorialMode').and - .returnValue(true); - spyOn(editabilityService, 'isTranslatable').and.returnValue(false); - expect(component.isEditableOutsideTutorialMode()).toBe(true); - }); - - it('should not save and publish exploration when editing inside tutorial' + - ' mode and exploration is not translatable', () => { - spyOn(editabilityService, 'isEditableOutsideTutorialMode').and - .returnValue(false); - spyOn(editabilityService, 'isTranslatable').and.returnValue(false); - expect(component.isEditableOutsideTutorialMode()).toBe(false); - }); - - it('should display publish button when the exploration is unpublished', - fakeAsync(() => { - component.explorationCanBePublished = false; - - userExplorationPermissionsService. - onUserExplorationPermissionsFetched.emit(); - tick(); - - expect(userExplorationPermissionsService.getPermissionsAsync) - .toHaveBeenCalled(); - expect(component.explorationCanBePublished).toBe(true); - })); - - it('should fetch userExplorationPermissions when ' + - 'showPublishExplorationModal is called', fakeAsync(() => { - let userPermissions = { - canPublish: true - }; - component.explorationCanBePublished = false; - fetchPermissionsAsyncSpy.and - .returnValue(Promise.resolve(userPermissions as ExplorationPermissions)); - - component.showPublishExplorationModal(); - tick(); - - expect(component.publishIsInProcess).toBe(false); - expect(component.loadingDotsAreShown).toBe(false); - expect(userExplorationPermissionsService.fetchPermissionsAsync) - .toHaveBeenCalled(); - expect(component.explorationCanBePublished).toBe(true); - })); - - it('should open a exploration save prompt modal', fakeAsync(() => { - spyOn(changeListService, 'getChangeList').and.returnValue(new Array(51)); - spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.resolve() - } as NgbModalRef); - spyOn(component, 'saveChanges'); - - component.saveIsInProcess = false; - component.getChangeListLength(); - tick(); - - expect(ngbModal.open).toHaveBeenCalled(); - expect(component.saveChanges).toHaveBeenCalled(); - })); - - it('should open a exploration save prompt modal only once', - fakeAsync(() => { - spyOn(changeListService, 'getChangeList').and.returnValue(new Array(51)); - spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.reject() - } as NgbModalRef); - spyOn(component, 'saveChanges'); - - component.saveIsInProcess = false; - component.getChangeListLength(); - tick(); - - expect(ngbModal.open).toHaveBeenCalledTimes(1); - expect(component.saveChanges).not.toHaveBeenCalled(); - - component.getChangeListLength(); - tick(); - - expect(ngbModal.open).toHaveBeenCalledTimes(1); - expect(component.saveChanges).not.toHaveBeenCalled(); - })); - - it('should open a confirmation modal with rejection', fakeAsync(() => { - spyOn(changeListService, 'getChangeList').and.returnValue(new Array(51)); - spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.reject() - } as NgbModalRef); - spyOn(component, 'saveChanges'); - component.saveIsInProcess = false; - - component.getChangeListLength(); - tick(); - - expect(ngbModal.open).toHaveBeenCalled(); - expect(component.saveChanges).not.toHaveBeenCalled(); - })); - - it('should change connnection status to ONLINE when internet is connected', - () => { - component.connectedToInternet = false; - mockConnectionServiceEmitter.emit(true); - - expect(component.connectedToInternet).toBe(true); - }); - - it('should change connnection status to OFFLINE when internet disconnects', - () => { - component.connectedToInternet = true; - mockConnectionServiceEmitter.emit(false); - - expect(component.connectedToInternet).toBe(false); - expect(component.getSaveButtonTooltip()).toBe( - 'You can not save the exploration when offline.'); - expect(component.getPublishExplorationButtonTooltip()).toBe( - 'You can not publish the exploration when offline.'); - }); + class MockExternalSaveService { + onExternalSave = mockExternalSaveEventEmitter; + } + + class MockNgbModal { + open() { + return { + result: Promise.resolve(), + }; + } + } + + class MockWindowRef { + location = {path: '/create/2234'}; + nativeWindow = { + scrollTo: (value1, value2) => {}, + sessionStorage: { + promoIsDismissed: null, + setItem: (testKey1, testKey2) => {}, + removeItem: testKey => {}, + }, + gtag: (value1, value2, value3) => {}, + navigator: { + onLine: true, + userAgent: null, + }, + location: { + path: '/create/2234', + pathname: '/', + hostname: 'oppiaserver.appspot.com', + search: '', + protocol: '', + reload: () => {}, + hash: '', + href: '', + }, + document: { + documentElement: { + setAttribute: (value1, value2) => {}, + clientWidth: null, + clientHeight: null, + }, + body: { + clientWidth: null, + clientHeight: null, + style: { + overflowY: '', + }, + }, + }, + addEventListener: (value1, value2) => {}, + }; + } + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule, + FormsModule, + MatCardModule, + MatMenuModule, + ], + declarations: [ + ExplorationSaveAndPublishButtonsComponent, + ExplorationSavePromptModalComponent, + ], + providers: [ + ChangeListService, + { + provide: ExternalSaveService, + useClass: MockExternalSaveService, + }, + { + provide: WindowRef, + useClass: MockWindowRef, + }, + { + provide: NgbModal, + useClass: MockNgbModal, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent( + ExplorationSaveAndPublishButtonsComponent + ); + component = fixture.componentInstance; + + changeListService = TestBed.inject(ChangeListService); + contextService = TestBed.inject(ContextService); + ngbModal = TestBed.inject(NgbModal); + ics = TestBed.inject(InternetConnectivityService); + explorationRightsService = TestBed.inject(ExplorationRightsService); + explorationSaveService = TestBed.inject(ExplorationSaveService); + explorationWarningsService = TestBed.inject(ExplorationWarningsService); + spyOn(contextService, 'getExplorationId').and.returnValue('exp1'); + editabilityService = TestBed.inject(EditabilityService); + userExplorationPermissionsService = TestBed.inject( + UserExplorationPermissionsService + ); + + let userPermissions = { + canPublish: true, + }; + fetchPermissionsAsyncSpy = spyOn( + userExplorationPermissionsService, + 'fetchPermissionsAsync' + ); + fetchPermissionsAsyncSpy.and.returnValue( + Promise.resolve(userPermissions as ExplorationPermissions) + ); + + spyOn( + userExplorationPermissionsService, + 'getPermissionsAsync' + ).and.returnValue( + Promise.resolve({ + canPublish: true, + } as ExplorationPermissions) + ); + spyOnProperty(ics, 'onInternetStateChange').and.returnValue( + mockConnectionServiceEmitter + ); + spyOn(explorationSaveService, 'saveChangesAsync').and.callFake( + (showCallback, hideCallback) => { + showCallback(); + hideCallback(); + return Promise.resolve(); + } + ); + spyOn(explorationSaveService, 'showPublishExplorationModal').and.callFake( + (showCallback, hideCallback) => { + showCallback(); + hideCallback(); + return Promise.resolve(); + } + ); + + component.ngOnInit(); + }); + + afterEach(() => { + component.ngOnDestroy(); + }); + + it('should initialize component properties after controller initialization', () => { + expect(component.saveIsInProcess).toBe(false); + expect(component.publishIsInProcess).toBe(false); + expect(component.loadingDotsAreShown).toBe(false); + }); + + it('should save exploration when saving changes', fakeAsync(() => { + component.saveChanges(); + tick(); + + expect(component.saveIsInProcess).toBe(false); + expect(component.loadingDotsAreShown).toBe(false); + })); + + it('should check if exploration is editable', () => { + spyOn(editabilityService, 'isLockedByAdmin').and.returnValue(true); + expect(component.isLockedByAdmin()).toBe(true); + }); + + it('should publish exploration when show publish exploration is shown', fakeAsync(() => { + component.showPublishExplorationModal(); + tick(); + + expect(component.publishIsInProcess).toBe(false); + expect(component.loadingDotsAreShown).toBe(false); + })); + + it( + 'should resolve the warnings before saving exploration when exploration' + + ' has critical warnings', + () => { + spyOn(explorationWarningsService, 'hasCriticalWarnings').and.returnValue( + true + ); + + expect(component.getSaveButtonTooltip()).toBe( + 'Please resolve the warnings.' + ); + } + ); + + it( + 'should save exploration draft when it has no warnings and exploration' + + ' is private', + () => { + spyOn(explorationWarningsService, 'hasCriticalWarnings').and.returnValue( + false + ); + spyOn(explorationRightsService, 'isPrivate').and.returnValue(true); + + expect(component.getSaveButtonTooltip()).toBe('Save Draft'); + } + ); + + it( + 'should publish exploration changes when it has no warnings and it is' + + ' public', + () => { + spyOn(explorationWarningsService, 'hasCriticalWarnings').and.returnValue( + false + ); + spyOn(explorationRightsService, 'isPrivate').and.returnValue(false); + expect(component.getSaveButtonTooltip()).toBe('Publish Changes'); + } + ); + + it( + 'should ask user to resolve the warnings before publishing' + + ' exploration when exploration has warnings', + () => { + spyOn(explorationWarningsService, 'countWarnings').and.returnValue(1); + expect(component.getPublishExplorationButtonTooltip()).toBe( + 'Please resolve the warnings before publishing.' + ); + } + ); + + it( + 'should save exploration changes before publishing it when trying to' + + ' publish a changed exploration without saving it first', + () => { + spyOn(explorationWarningsService, 'countWarnings').and.returnValue(0); + spyOn(changeListService, 'isExplorationLockedForEditing').and.returnValue( + true + ); + expect(component.getPublishExplorationButtonTooltip()).toBe( + 'Please save your changes before publishing.' + ); + } + ); + + it('should publish exploration when it is already saved', () => { + spyOn(explorationWarningsService, 'countWarnings').and.returnValue(0); + spyOn(changeListService, 'isExplorationLockedForEditing').and.returnValue( + false + ); + expect(component.getPublishExplorationButtonTooltip()).toBe( + 'Publish to Oppia Library' + ); + }); + + it('should discard changes when exploration is changed', () => { + spyOn(explorationSaveService, 'discardChanges'); + component.discardChanges(); + expect(explorationSaveService.discardChanges).toHaveBeenCalled(); + }); + + it('should get whether exploration is saveable', () => { + spyOn(explorationSaveService, 'isExplorationSaveable').and.returnValue( + true + ); + expect(component.isExplorationSaveable()).toBe(true); + }); + + it('should count changes made in an exploration', () => { + spyOn(changeListService, 'getChangeList').and.returnValue([ + {}, + {}, + ] as ExplorationChange[]); + expect(component.getChangeListLength()).toBe(2); + }); + + it( + 'should save or publish exploration when editing outside tutorial mode' + + ' and exploration is translatable', + () => { + spyOn( + editabilityService, + 'isEditableOutsideTutorialMode' + ).and.returnValue(false); + spyOn(editabilityService, 'isTranslatable').and.returnValue(true); + expect(component.isEditableOutsideTutorialMode()).toBe(true); + } + ); + + it( + 'should save or publish exploration when editing outside tutorial mode' + + ' and exploration is not translatable', + () => { + spyOn( + editabilityService, + 'isEditableOutsideTutorialMode' + ).and.returnValue(true); + spyOn(editabilityService, 'isTranslatable').and.returnValue(false); + expect(component.isEditableOutsideTutorialMode()).toBe(true); + } + ); + + it( + 'should not save and publish exploration when editing inside tutorial' + + ' mode and exploration is not translatable', + () => { + spyOn( + editabilityService, + 'isEditableOutsideTutorialMode' + ).and.returnValue(false); + spyOn(editabilityService, 'isTranslatable').and.returnValue(false); + expect(component.isEditableOutsideTutorialMode()).toBe(false); + } + ); + + it('should display publish button when the exploration is unpublished', fakeAsync(() => { + component.explorationCanBePublished = false; + + userExplorationPermissionsService.onUserExplorationPermissionsFetched.emit(); + tick(); + + expect( + userExplorationPermissionsService.getPermissionsAsync + ).toHaveBeenCalled(); + expect(component.explorationCanBePublished).toBe(true); + })); + + it( + 'should fetch userExplorationPermissions when ' + + 'showPublishExplorationModal is called', + fakeAsync(() => { + let userPermissions = { + canPublish: true, + }; + component.explorationCanBePublished = false; + fetchPermissionsAsyncSpy.and.returnValue( + Promise.resolve(userPermissions as ExplorationPermissions) + ); + + component.showPublishExplorationModal(); + tick(); + + expect(component.publishIsInProcess).toBe(false); + expect(component.loadingDotsAreShown).toBe(false); + expect( + userExplorationPermissionsService.fetchPermissionsAsync + ).toHaveBeenCalled(); + expect(component.explorationCanBePublished).toBe(true); + }) + ); + + it('should open a exploration save prompt modal', fakeAsync(() => { + spyOn(changeListService, 'getChangeList').and.returnValue(new Array(51)); + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.resolve(), + } as NgbModalRef); + spyOn(component, 'saveChanges'); + + component.saveIsInProcess = false; + component.getChangeListLength(); + tick(); + + expect(ngbModal.open).toHaveBeenCalled(); + expect(component.saveChanges).toHaveBeenCalled(); + })); + + it('should open a exploration save prompt modal only once', fakeAsync(() => { + spyOn(changeListService, 'getChangeList').and.returnValue(new Array(51)); + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.reject(), + } as NgbModalRef); + spyOn(component, 'saveChanges'); + + component.saveIsInProcess = false; + component.getChangeListLength(); + tick(); + + expect(ngbModal.open).toHaveBeenCalledTimes(1); + expect(component.saveChanges).not.toHaveBeenCalled(); + + component.getChangeListLength(); + tick(); + + expect(ngbModal.open).toHaveBeenCalledTimes(1); + expect(component.saveChanges).not.toHaveBeenCalled(); + })); + + it('should open a confirmation modal with rejection', fakeAsync(() => { + spyOn(changeListService, 'getChangeList').and.returnValue(new Array(51)); + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.reject(), + } as NgbModalRef); + spyOn(component, 'saveChanges'); + component.saveIsInProcess = false; + + component.getChangeListLength(); + tick(); + + expect(ngbModal.open).toHaveBeenCalled(); + expect(component.saveChanges).not.toHaveBeenCalled(); + })); + + it('should change connnection status to ONLINE when internet is connected', () => { + component.connectedToInternet = false; + mockConnectionServiceEmitter.emit(true); + + expect(component.connectedToInternet).toBe(true); + }); + + it('should change connnection status to OFFLINE when internet disconnects', () => { + component.connectedToInternet = true; + mockConnectionServiceEmitter.emit(false); + + expect(component.connectedToInternet).toBe(false); + expect(component.getSaveButtonTooltip()).toBe( + 'You can not save the exploration when offline.' + ); + expect(component.getPublishExplorationButtonTooltip()).toBe( + 'You can not publish the exploration when offline.' + ); + }); }); diff --git a/core/templates/pages/exploration-editor-page/exploration-save-and-publish-buttons/exploration-save-and-publish-buttons.component.ts b/core/templates/pages/exploration-editor-page/exploration-save-and-publish-buttons/exploration-save-and-publish-buttons.component.ts index 38c09deb0dbf..e0cf8c2295e8 100644 --- a/core/templates/pages/exploration-editor-page/exploration-save-and-publish-buttons/exploration-save-and-publish-buttons.component.ts +++ b/core/templates/pages/exploration-editor-page/exploration-save-and-publish-buttons/exploration-save-and-publish-buttons.component.ts @@ -16,26 +16,27 @@ * @fileoverview Component for the exploration save & publish buttons. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { Subscription } from 'rxjs'; -import { EditabilityService } from 'services/editability.service'; -import { EntityTranslationsService } from 'services/entity-translations.services'; -import { InternetConnectivityService } from 'services/internet-connectivity.service'; -import { ExplorationSavePromptModalComponent } from '../modal-templates/exploration-save-prompt-modal.component'; -import { ChangeListService } from '../services/change-list.service'; -import { ExplorationRightsService } from '../services/exploration-rights.service'; -import { ExplorationSaveService } from '../services/exploration-save.service'; -import { ExplorationWarningsService } from '../services/exploration-warnings.service'; -import { UserExplorationPermissionsService } from '../services/user-exploration-permissions.service'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {Subscription} from 'rxjs'; +import {EditabilityService} from 'services/editability.service'; +import {EntityTranslationsService} from 'services/entity-translations.services'; +import {InternetConnectivityService} from 'services/internet-connectivity.service'; +import {ExplorationSavePromptModalComponent} from '../modal-templates/exploration-save-prompt-modal.component'; +import {ChangeListService} from '../services/change-list.service'; +import {ExplorationRightsService} from '../services/exploration-rights.service'; +import {ExplorationSaveService} from '../services/exploration-save.service'; +import {ExplorationWarningsService} from '../services/exploration-warnings.service'; +import {UserExplorationPermissionsService} from '../services/user-exploration-permissions.service'; @Component({ selector: 'exploration-save-and-publish-buttons', - templateUrl: './exploration-save-and-publish-buttons.component.html' + templateUrl: './exploration-save-and-publish-buttons.component.html', }) export class ExplorationSaveAndPublishButtonsComponent - implements OnInit, OnDestroy { + implements OnInit, OnDestroy +{ directiveSubscriptions = new Subscription(); isModalDisplayed: boolean = false; @@ -46,17 +47,16 @@ export class ExplorationSaveAndPublishButtonsComponent connectedToInternet: boolean; constructor( - private explorationRightsService: ExplorationRightsService, - private editabilityService: EditabilityService, - private entityTranslationsService: EntityTranslationsService, - private changeListService: ChangeListService, - private explorationWarningsService: ExplorationWarningsService, - private explorationSaveService: ExplorationSaveService, - private userExplorationPermissionsService: - UserExplorationPermissionsService, - private internetConnectivityService: InternetConnectivityService, - private ngbModal: NgbModal, - ) { } + private explorationRightsService: ExplorationRightsService, + private editabilityService: EditabilityService, + private entityTranslationsService: EntityTranslationsService, + private changeListService: ChangeListService, + private explorationWarningsService: ExplorationWarningsService, + private explorationSaveService: ExplorationSaveService, + private userExplorationPermissionsService: UserExplorationPermissionsService, + private internetConnectivityService: InternetConnectivityService, + private ngbModal: NgbModal + ) {} isPrivate(): boolean { return this.explorationRightsService.isPrivate(); @@ -71,8 +71,10 @@ export class ExplorationSaveAndPublishButtonsComponent } isEditableOutsideTutorialMode(): boolean { - return this.editabilityService.isEditableOutsideTutorialMode() || - this.editabilityService.isTranslatable(); + return ( + this.editabilityService.isEditableOutsideTutorialMode() || + this.editabilityService.isTranslatable() + ); } countWarnings(): number { @@ -88,19 +90,27 @@ export class ExplorationSaveAndPublishButtonsComponent const MIN_CHANGES_DISPLAY_PROMPT = 50; - if (countChanges >= MIN_CHANGES_DISPLAY_PROMPT && !this.isModalDisplayed && - !this.saveIsInProcess) { + if ( + countChanges >= MIN_CHANGES_DISPLAY_PROMPT && + !this.isModalDisplayed && + !this.saveIsInProcess + ) { this.isModalDisplayed = true; - this.ngbModal.open(ExplorationSavePromptModalComponent, { - backdrop: 'static', - }).result.then(() => { - this.saveChanges(); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + this.ngbModal + .open(ExplorationSavePromptModalComponent, { + backdrop: 'static', + }) + .result.then( + () => { + this.saveChanges(); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } return this.changeListService.getChangeList().length; } @@ -139,8 +149,9 @@ export class ExplorationSaveAndPublishButtonsComponent hideLoadingAndUpdatePermission(): void { this.loadingDotsAreShown = false; - this.userExplorationPermissionsService.fetchPermissionsAsync() - .then((permissions) => { + this.userExplorationPermissionsService + .fetchPermissionsAsync() + .then(permissions => { this.explorationCanBePublished = permissions.canPublish; }); } @@ -149,9 +160,11 @@ export class ExplorationSaveAndPublishButtonsComponent this.publishIsInProcess = true; this.loadingDotsAreShown = true; - this.explorationSaveService.showPublishExplorationModal( - this.showLoadingDots.bind(this), - this.hideLoadingAndUpdatePermission.bind(this)) + this.explorationSaveService + .showPublishExplorationModal( + this.showLoadingDots.bind(this), + this.hideLoadingAndUpdatePermission.bind(this) + ) .finally(() => { this.publishIsInProcess = false; this.loadingDotsAreShown = false; @@ -163,14 +176,19 @@ export class ExplorationSaveAndPublishButtonsComponent this.saveIsInProcess = true; this.loadingDotsAreShown = true; - this.explorationSaveService.saveChangesAsync( - this.showLoadingDots.bind(this), - this.hideLoadingAndUpdatePermission.bind(this)) - .then(() => { - this.saveIsInProcess = false; - this.loadingDotsAreShown = false; - this.entityTranslationsService.reset(); - }, () => {}); + this.explorationSaveService + .saveChangesAsync( + this.showLoadingDots.bind(this), + this.hideLoadingAndUpdatePermission.bind(this) + ) + .then( + () => { + this.saveIsInProcess = false; + this.loadingDotsAreShown = false; + this.entityTranslationsService.reset(); + }, + () => {} + ); } ngOnInit(): void { @@ -180,28 +198,30 @@ export class ExplorationSaveAndPublishButtonsComponent this.explorationCanBePublished = false; this.connectedToInternet = true; - this.userExplorationPermissionsService.getPermissionsAsync() - .then((permissions) => { + this.userExplorationPermissionsService + .getPermissionsAsync() + .then(permissions => { this.explorationCanBePublished = permissions.canPublish; }); this.directiveSubscriptions.add( - this.userExplorationPermissionsService.onUserExplorationPermissionsFetched - .subscribe( - () => { - this.userExplorationPermissionsService.getPermissionsAsync() - .then((permissions) => { - this.explorationCanBePublished = permissions.canPublish; - }); - } - ) + this.userExplorationPermissionsService.onUserExplorationPermissionsFetched.subscribe( + () => { + this.userExplorationPermissionsService + .getPermissionsAsync() + .then(permissions => { + this.explorationCanBePublished = permissions.canPublish; + }); + } + ) ); this.directiveSubscriptions.add( this.internetConnectivityService.onInternetStateChange.subscribe( internetAccessible => { this.connectedToInternet = internetAccessible; - }) + } + ) ); } @@ -213,5 +233,6 @@ export class ExplorationSaveAndPublishButtonsComponent angular.module('oppia').directive( 'explorationSaveAndPublishButtons', downgradeComponent({ - component: ExplorationSaveAndPublishButtonsComponent - })); + component: ExplorationSaveAndPublishButtonsComponent, + }) +); diff --git a/core/templates/pages/exploration-editor-page/exploration-title-editor/exploration-title-editor.component.spec.ts b/core/templates/pages/exploration-editor-page/exploration-title-editor/exploration-title-editor.component.spec.ts index d5e02fb2047a..10187f3a0ccb 100644 --- a/core/templates/pages/exploration-editor-page/exploration-title-editor/exploration-title-editor.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/exploration-title-editor/exploration-title-editor.component.spec.ts @@ -16,14 +16,21 @@ * @fileoverview Unit tests for explorationTitleEditor component. */ -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { ComponentFixture, fakeAsync, flush, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { ExplorationTitleEditorComponent } from './exploration-title-editor.component'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { RouterService } from '../services/router.service'; -import { ExplorationTitleService } from '../services/exploration-title.service'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + flush, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {ExplorationTitleEditorComponent} from './exploration-title-editor.component'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {RouterService} from '../services/router.service'; +import {ExplorationTitleService} from '../services/exploration-title.service'; describe('Exploration Title Editor Component', () => { let component: ExplorationTitleEditorComponent; @@ -38,27 +45,20 @@ describe('Exploration Title Editor Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - FormsModule, - ReactiveFormsModule - ], - declarations: [ - ExplorationTitleEditorComponent - ], + imports: [HttpClientTestingModule, FormsModule, ReactiveFormsModule], + declarations: [ExplorationTitleEditorComponent], providers: [ { provide: RouterService, - useClass: MockRouterService + useClass: MockRouterService, }, ExplorationTitleService, FocusManagerService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); - beforeEach(() => { fixture = TestBed.createComponent(ExplorationTitleEditorComponent); component = fixture.componentInstance; @@ -76,25 +76,29 @@ describe('Exploration Title Editor Component', () => { component.ngOnDestroy(); }); - it('should set focus on settings tab when refreshSettingsTab flag is ' + - 'emit', fakeAsync(() => { - spyOn(focusManagerService, 'setFocus').and.stub(); + it( + 'should set focus on settings tab when refreshSettingsTab flag is ' + + 'emit', + fakeAsync(() => { + spyOn(focusManagerService, 'setFocus').and.stub(); - component.focusLabel = 'xyzz'; + component.focusLabel = 'xyzz'; - mockEventEmitter.emit(); - component.inputFieldBlur(); - tick(); + mockEventEmitter.emit(); + component.inputFieldBlur(); + tick(); - flush(); + flush(); - expect(focusManagerService.setFocus).toHaveBeenCalledWith( - 'xyzz'); - })); + expect(focusManagerService.setFocus).toHaveBeenCalledWith('xyzz'); + }) + ); it('should unsubscribe when component is destroyed', () => { - const unsubscribeSpy = - spyOn(component.directiveSubscriptions, 'unsubscribe'); + const unsubscribeSpy = spyOn( + component.directiveSubscriptions, + 'unsubscribe' + ); component.ngOnDestroy(); diff --git a/core/templates/pages/exploration-editor-page/exploration-title-editor/exploration-title-editor.component.ts b/core/templates/pages/exploration-editor-page/exploration-title-editor/exploration-title-editor.component.ts index 4d24d9c47803..a1113b4d5c97 100644 --- a/core/templates/pages/exploration-editor-page/exploration-title-editor/exploration-title-editor.component.ts +++ b/core/templates/pages/exploration-editor-page/exploration-title-editor/exploration-title-editor.component.ts @@ -16,17 +16,24 @@ * @fileoverview Component for the exploration title field in forms. */ -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { Subscription } from 'rxjs'; -import { RouterService } from 'pages/exploration-editor-page/services/router.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { AppConstants } from 'app.constants'; -import { ExplorationTitleService } from '../services/exploration-title.service'; +import { + Component, + EventEmitter, + Input, + OnDestroy, + OnInit, + Output, +} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {Subscription} from 'rxjs'; +import {RouterService} from 'pages/exploration-editor-page/services/router.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {AppConstants} from 'app.constants'; +import {ExplorationTitleService} from '../services/exploration-title.service'; @Component({ selector: 'oppia-exploration-title-editor', - templateUrl: './exploration-title-editor.component.html' + templateUrl: './exploration-title-editor.component.html', }) export class ExplorationTitleEditorComponent implements OnInit, OnDestroy { directiveSubscriptions = new Subscription(); @@ -45,23 +52,21 @@ export class ExplorationTitleEditorComponent implements OnInit, OnDestroy { constructor( public explorationTitleService: ExplorationTitleService, private focusManagerService: FocusManagerService, - private routerService: RouterService, - ) { } + private routerService: RouterService + ) {} inputFieldBlur(): void { this.onInputFieldBlur.emit(); } ngOnInit(): void { - this.MAX_CHARS_IN_EXPLORATION_TITLE = ( - AppConstants.MAX_CHARS_IN_EXPLORATION_TITLE); + this.MAX_CHARS_IN_EXPLORATION_TITLE = + AppConstants.MAX_CHARS_IN_EXPLORATION_TITLE; this.directiveSubscriptions.add( - this.routerService.onRefreshSettingsTab.subscribe( - () => { - this.focusManagerService.setFocus(this.focusLabel); - } - ) + this.routerService.onRefreshSettingsTab.subscribe(() => { + this.focusManagerService.setFocus(this.focusLabel); + }) ); } @@ -71,6 +76,8 @@ export class ExplorationTitleEditorComponent implements OnInit, OnDestroy { } angular.module('oppia').directive( - 'oppiaExplorationTitleEditor', downgradeComponent({ - component: ExplorationTitleEditorComponent - })); + 'oppiaExplorationTitleEditor', + downgradeComponent({ + component: ExplorationTitleEditorComponent, + }) +); diff --git a/core/templates/pages/exploration-editor-page/feedback-tab/feedback-tab.component.spec.ts b/core/templates/pages/exploration-editor-page/feedback-tab/feedback-tab.component.spec.ts index cd55a160ff2b..dc4e49023154 100644 --- a/core/templates/pages/exploration-editor-page/feedback-tab/feedback-tab.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/feedback-tab/feedback-tab.component.spec.ts @@ -16,23 +16,28 @@ * @fileoverview Unit tests for feedbackTab. */ -import { ComponentFixture, fakeAsync, tick, waitForAsync } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TestBed } from '@angular/core/testing'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { FormsModule } from '@angular/forms'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { AlertsService } from 'services/alerts.service'; -import { SuggestionThread } from 'domain/suggestion/suggestion-thread-object.model'; -import { DateTimeFormatService } from 'services/date-time-format.service'; -import { UserService } from 'services/user.service'; -import { ChangeListService } from '../services/change-list.service'; -import { EditabilityService } from 'services/editability.service'; -import { ExplorationStatesService } from '../services/exploration-states.service'; -import { ThreadDataBackendApiService } from './services/thread-data-backend-api.service'; -import { FeedbackTabComponent } from './feedback-tab.component'; -import { UserInfo } from 'domain/user/user-info.model'; -import { FeedbackThread } from 'domain/feedback_thread/FeedbackThreadObjectFactory'; +import { + ComponentFixture, + fakeAsync, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TestBed} from '@angular/core/testing'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {FormsModule} from '@angular/forms'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import {AlertsService} from 'services/alerts.service'; +import {SuggestionThread} from 'domain/suggestion/suggestion-thread-object.model'; +import {DateTimeFormatService} from 'services/date-time-format.service'; +import {UserService} from 'services/user.service'; +import {ChangeListService} from '../services/change-list.service'; +import {EditabilityService} from 'services/editability.service'; +import {ExplorationStatesService} from '../services/exploration-states.service'; +import {ThreadDataBackendApiService} from './services/thread-data-backend-api.service'; +import {FeedbackTabComponent} from './feedback-tab.component'; +import {UserInfo} from 'domain/user/user-info.model'; +import {FeedbackThread} from 'domain/feedback_thread/FeedbackThreadObjectFactory'; describe('Feedback Tab Component', () => { let component: FeedbackTabComponent; @@ -49,34 +54,28 @@ describe('Feedback Tab Component', () => { class MockNgbModal { open() { return { - result: Promise.resolve() + result: Promise.resolve(), }; } } beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - FormsModule, - ], - declarations: [ - FeedbackTabComponent - ], + imports: [HttpClientTestingModule, FormsModule], + declarations: [FeedbackTabComponent], providers: [ ChangeListService, { provide: NgbModal, - useClass: MockNgbModal - } + useClass: MockNgbModal, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent( - FeedbackTabComponent); + fixture = TestBed.createComponent(FeedbackTabComponent); component = fixture.componentInstance; alertsService = TestBed.inject(AlertsService); @@ -85,17 +84,18 @@ describe('Feedback Tab Component', () => { ngbModal = TestBed.inject(NgbModal); editabilityService = TestBed.inject(EditabilityService); explorationStatesService = TestBed.inject(ExplorationStatesService); - threadDataBackendApiService = ( - TestBed.inject(ThreadDataBackendApiService)); + threadDataBackendApiService = TestBed.inject(ThreadDataBackendApiService); userService = TestBed.inject(UserService); - spyOn(userService, 'getUserInfoAsync').and.returnValue(Promise.resolve({ - isLoggedIn: () => true - } as UserInfo)); + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve({ + isLoggedIn: () => true, + } as UserInfo) + ); spyOn( threadDataBackendApiService, - 'getFeedbackThreadsAsync').and.returnValue(Promise.resolve( - {} as FeedbackThread[])); + 'getFeedbackThreadsAsync' + ).and.returnValue(Promise.resolve({} as FeedbackThread[])); component.ngOnInit(); }); @@ -104,98 +104,33 @@ describe('Feedback Tab Component', () => { component.ngOnDestroy(); }); - it('should get threads after feedback threads are available', - fakeAsync(() => { - let onFeedbackThreadsInitializedEmitter = new EventEmitter(); - spyOnProperty( - threadDataBackendApiService, 'onFeedbackThreadsInitialized') - .and.returnValue(onFeedbackThreadsInitializedEmitter); - spyOn(threadDataBackendApiService, 'getThread').and.stub(); - spyOn(component, 'fetchUpdatedThreads'); - - component.ngOnInit(); - tick(); - - onFeedbackThreadsInitializedEmitter.emit(); - - expect(component.fetchUpdatedThreads).toHaveBeenCalled(); - })); - - it('should throw an error when trying to active a non-existent thread', - () => { - expect(() => { - component.setActiveThread('0'); - }).toThrowError('Trying to display a non-existent thread'); - }); - - it('should set active thread when it exists', fakeAsync(() => { - let thread = SuggestionThread.createFromBackendDicts({ - status: 'review', - subject: '', - summary: '', - original_author_username: 'Username1', - last_updated_msecs: 0, - message_count: 1, - thread_id: '1', - state_name: '', - last_nonempty_message_author: '', - last_nonempty_message_text: '' - }, { - suggestion_type: 'edit_exploration_state_content', - suggestion_id: '1', - target_type: '', - target_id: '', - status: '', - author_name: '', - change_cmd: { - state_name: '', - new_value: {html: ''}, - old_value: {html: ''}, - skill_id: '', - }, - last_updated_msecs: 0 - }); - spyOn(threadDataBackendApiService, 'getThread').and.returnValue(thread); - spyOn(threadDataBackendApiService, 'getMessagesAsync').and.returnValue( - Promise.resolve(null)); + it('should get threads after feedback threads are available', fakeAsync(() => { + let onFeedbackThreadsInitializedEmitter = new EventEmitter(); + spyOnProperty( + threadDataBackendApiService, + 'onFeedbackThreadsInitialized' + ).and.returnValue(onFeedbackThreadsInitializedEmitter); + spyOn(threadDataBackendApiService, 'getThread').and.stub(); + spyOn(component, 'fetchUpdatedThreads'); - component.setActiveThread('1'); + component.ngOnInit(); tick(); - expect(component.activeThread).toEqual(thread); - expect(component.feedbackMessage.status).toBe('review'); + onFeedbackThreadsInitializedEmitter.emit(); + + expect(component.fetchUpdatedThreads).toHaveBeenCalled(); })); - it('should add warning when trying to add a message in a thread with id' + - ' null', () => { - let addWarningSpy = spyOn(alertsService, 'addWarning').and.callThrough(); - component.addNewMessage(null, 'Text', 'Open'); - expect(addWarningSpy).toHaveBeenCalledWith( - 'Cannot add message to thread with ID: null.'); + it('should throw an error when trying to active a non-existent thread', () => { + expect(() => { + component.setActiveThread('0'); + }).toThrowError('Trying to display a non-existent thread'); }); - it('should add warning when trying to add a invalid message in a thread', - () => { - let addWarningSpy = spyOn(alertsService, 'addWarning').and.callThrough(); - component.addNewMessage('0', 'Text', null); - expect(addWarningSpy).toHaveBeenCalledWith( - 'Invalid message status: null'); - }); - - it('should throw error when trying to add a message in an invalid thread', - () => { - expect(() => { - component.addNewMessage('0', 'Text', 'Open'); - }).toThrowError('Trying to add message to a non-existent thread.'); - expect(component.threadIsStale).toBe(true); - expect(component.messageSendingInProgress).toBe(true); - }); - - it('should add new message to a thread and then go back to feedback' + - ' threads list', fakeAsync(() => { - spyOn(threadDataBackendApiService, 'getThread').and.returnValue( - SuggestionThread.createFromBackendDicts({ - status: 'Open', + it('should set active thread when it exists', fakeAsync(() => { + let thread = SuggestionThread.createFromBackendDicts( + { + status: 'review', subject: '', summary: '', original_author_username: 'Username1', @@ -204,8 +139,9 @@ describe('Feedback Tab Component', () => { thread_id: '1', state_name: '', last_nonempty_message_author: '', - last_nonempty_message_text: '' - }, { + last_nonempty_message_text: '', + }, + { suggestion_type: 'edit_exploration_state_content', suggestion_id: '1', target_type: '', @@ -218,35 +154,112 @@ describe('Feedback Tab Component', () => { old_value: {html: ''}, skill_id: '', }, - last_updated_msecs: 0 - })); + last_updated_msecs: 0, + } + ); + spyOn(threadDataBackendApiService, 'getThread').and.returnValue(thread); spyOn(threadDataBackendApiService, 'getMessagesAsync').and.returnValue( - Promise.resolve(null)); + Promise.resolve(null) + ); component.setActiveThread('1'); tick(); - spyOn(threadDataBackendApiService, 'addNewMessageAsync').and.returnValue( - Promise.resolve(null)); - - component.addNewMessage('1', 'Text', 'Open'); - tick(); + expect(component.activeThread).toEqual(thread); + expect(component.feedbackMessage.status).toBe('review'); + })); - expect(component.messageSendingInProgress).toBe(false); - expect(component.messageSendingInProgress).toBe(false); - expect(component.feedbackMessage.status).toBe('Open'); - expect(component.feedbackMessage.text).toBe(''); + it( + 'should add warning when trying to add a message in a thread with id' + + ' null', + () => { + let addWarningSpy = spyOn(alertsService, 'addWarning').and.callThrough(); + component.addNewMessage(null, 'Text', 'Open'); + expect(addWarningSpy).toHaveBeenCalledWith( + 'Cannot add message to thread with ID: null.' + ); + } + ); - component.onBackButtonClicked(); - tick(); + it('should add warning when trying to add a invalid message in a thread', () => { + let addWarningSpy = spyOn(alertsService, 'addWarning').and.callThrough(); + component.addNewMessage('0', 'Text', null); + expect(addWarningSpy).toHaveBeenCalledWith('Invalid message status: null'); + }); - expect(threadDataBackendApiService.getThread).toHaveBeenCalledWith('1'); - })); + it('should throw error when trying to add a message in an invalid thread', () => { + expect(() => { + component.addNewMessage('0', 'Text', 'Open'); + }).toThrowError('Trying to add message to a non-existent thread.'); + expect(component.threadIsStale).toBe(true); + expect(component.messageSendingInProgress).toBe(true); + }); - it('should use reject handler when trying to add a message in a thread fails', + it( + 'should add new message to a thread and then go back to feedback' + + ' threads list', fakeAsync(() => { spyOn(threadDataBackendApiService, 'getThread').and.returnValue( - SuggestionThread.createFromBackendDicts({ + SuggestionThread.createFromBackendDicts( + { + status: 'Open', + subject: '', + summary: '', + original_author_username: 'Username1', + last_updated_msecs: 0, + message_count: 1, + thread_id: '1', + state_name: '', + last_nonempty_message_author: '', + last_nonempty_message_text: '', + }, + { + suggestion_type: 'edit_exploration_state_content', + suggestion_id: '1', + target_type: '', + target_id: '', + status: '', + author_name: '', + change_cmd: { + state_name: '', + new_value: {html: ''}, + old_value: {html: ''}, + skill_id: '', + }, + last_updated_msecs: 0, + } + ) + ); + spyOn(threadDataBackendApiService, 'getMessagesAsync').and.returnValue( + Promise.resolve(null) + ); + + component.setActiveThread('1'); + tick(); + + spyOn(threadDataBackendApiService, 'addNewMessageAsync').and.returnValue( + Promise.resolve(null) + ); + + component.addNewMessage('1', 'Text', 'Open'); + tick(); + + expect(component.messageSendingInProgress).toBe(false); + expect(component.messageSendingInProgress).toBe(false); + expect(component.feedbackMessage.status).toBe('Open'); + expect(component.feedbackMessage.text).toBe(''); + + component.onBackButtonClicked(); + tick(); + + expect(threadDataBackendApiService.getThread).toHaveBeenCalledWith('1'); + }) + ); + + it('should use reject handler when trying to add a message in a thread fails', fakeAsync(() => { + spyOn(threadDataBackendApiService, 'getThread').and.returnValue( + SuggestionThread.createFromBackendDicts( + { status: 'Open', subject: '', summary: '', @@ -256,8 +269,9 @@ describe('Feedback Tab Component', () => { thread_id: '1', state_name: '', last_nonempty_message_author: '', - last_nonempty_message_text: '' - }, { + last_nonempty_message_text: '', + }, + { suggestion_type: 'edit_exploration_state_content', suggestion_id: '1', target_type: '', @@ -270,177 +284,198 @@ describe('Feedback Tab Component', () => { old_value: {html: ''}, skill_id: '', }, - last_updated_msecs: 0 - })); + last_updated_msecs: 0, + } + ) + ); + spyOn(threadDataBackendApiService, 'getMessagesAsync').and.returnValue( + Promise.resolve(null) + ); + + component.setActiveThread('1'); + tick(); + + spyOn(threadDataBackendApiService, 'addNewMessageAsync').and.returnValue( + Promise.reject() + ); + + component.addNewMessage('1', 'Text', 'Open'); + tick(); + + expect(component.messageSendingInProgress).toBe(false); + })); + + it( + 'should evaluate suggestion button type to be default when a feedback' + + ' thread is selected', + () => { + let thread = SuggestionThread.createFromBackendDicts( + { + status: 'open', + subject: '', + summary: '', + original_author_username: 'Username1', + last_updated_msecs: 0, + message_count: 1, + thread_id: '1', + state_name: '', + last_nonempty_message_author: '', + last_nonempty_message_text: '', + }, + { + suggestion_type: 'edit_exploration_state_content', + suggestion_id: '1', + target_type: '', + target_id: '', + status: 'open', + author_name: '', + change_cmd: { + state_name: '', + new_value: {html: ''}, + old_value: {html: ''}, + skill_id: '', + }, + last_updated_msecs: 0, + } + ); + spyOn(threadDataBackendApiService, 'getThread').and.returnValue(thread); spyOn(threadDataBackendApiService, 'getMessagesAsync').and.returnValue( - Promise.resolve(null)); + Promise.resolve(null) + ); component.setActiveThread('1'); - tick(); - spyOn(threadDataBackendApiService, 'addNewMessageAsync').and.returnValue( - Promise.reject()); + expect(component.getSuggestionButtonType()).toBe('default'); + } + ); - component.addNewMessage('1', 'Text', 'Open'); - tick(); + it( + 'should evaluate suggestion button type to be primary when a feedback' + + ' thread is selected', + fakeAsync(() => { + let thread = SuggestionThread.createFromBackendDicts( + { + status: 'review', + subject: '', + summary: '', + original_author_username: 'Username1', + last_updated_msecs: 0, + message_count: 1, + thread_id: '1', + state_name: '', + last_nonempty_message_author: '', + last_nonempty_message_text: '', + }, + { + suggestion_type: 'edit_exploration_state_content', + suggestion_id: '1', + target_type: '', + target_id: '', + status: 'review', + author_name: '', + change_cmd: { + state_name: '', + new_value: {html: ''}, + old_value: {html: ''}, + skill_id: '', + }, + last_updated_msecs: 0, + } + ); + spyOn(threadDataBackendApiService, 'getThread').and.returnValue(thread); + spyOn(threadDataBackendApiService, 'getMessagesAsync').and.returnValue( + Promise.resolve(null) + ); - expect(component.messageSendingInProgress).toBe(false); - })); - - it('should evaluate suggestion button type to be default when a feedback' + - ' thread is selected', () => { - let thread = SuggestionThread.createFromBackendDicts({ - status: 'open', - subject: '', - summary: '', - original_author_username: 'Username1', - last_updated_msecs: 0, - message_count: 1, - thread_id: '1', - state_name: '', - last_nonempty_message_author: '', - last_nonempty_message_text: '' - }, { - suggestion_type: 'edit_exploration_state_content', - suggestion_id: '1', - target_type: '', - target_id: '', - status: 'open', - author_name: '', - change_cmd: { - state_name: '', - new_value: {html: ''}, - old_value: {html: ''}, - skill_id: '', - }, - last_updated_msecs: 0 - }); - spyOn(threadDataBackendApiService, 'getThread').and.returnValue(thread); - spyOn(threadDataBackendApiService, 'getMessagesAsync').and.returnValue( - Promise.resolve(null)); + component.setActiveThread('1'); + tick(); - component.setActiveThread('1'); + spyOn(explorationStatesService, 'hasState').and.returnValue(true); + spyOn(changeListService, 'getChangeList').and.returnValue([]); - expect(component.getSuggestionButtonType()).toBe('default'); - }); + expect(component.getSuggestionButtonType()).toBe('primary'); + }) + ); - it('should evaluate suggestion button type to be primary when a feedback' + - ' thread is selected', fakeAsync(() => { - let thread = SuggestionThread.createFromBackendDicts({ - status: 'review', - subject: '', - summary: '', - original_author_username: 'Username1', - last_updated_msecs: 0, - message_count: 1, - thread_id: '1', - state_name: '', - last_nonempty_message_author: '', - last_nonempty_message_text: '' - }, { - suggestion_type: 'edit_exploration_state_content', - suggestion_id: '1', - target_type: '', - target_id: '', - status: 'review', - author_name: '', - change_cmd: { + it('should call fetchUpdatedThreads', fakeAsync(() => { + component.activeThread = SuggestionThread.createFromBackendDicts( + { + status: 'review', + subject: '', + summary: '', + original_author_username: 'Username1', + last_updated_msecs: 0, + message_count: 1, + thread_id: '1', state_name: '', - new_value: {html: ''}, - old_value: {html: ''}, - skill_id: '', + last_nonempty_message_author: '', + last_nonempty_message_text: '', }, - last_updated_msecs: 0 - }); - spyOn(threadDataBackendApiService, 'getThread').and.returnValue(thread); - spyOn(threadDataBackendApiService, 'getMessagesAsync').and.returnValue( - Promise.resolve(null)); + { + suggestion_type: 'edit_exploration_state_content', + suggestion_id: '1', + target_type: '', + target_id: '2', + status: '', + author_name: '', + change_cmd: { + state_name: '', + new_value: {html: ''}, + old_value: {html: ''}, + skill_id: '', + }, + last_updated_msecs: 0, + } + ); - component.setActiveThread('1'); + spyOn(threadDataBackendApiService, 'getThread').and.returnValue(null); + component.fetchUpdatedThreads().then(() => {}); tick(); - spyOn(explorationStatesService, 'hasState').and.returnValue(true); - spyOn(changeListService, 'getChangeList').and.returnValue([]); - - expect(component.getSuggestionButtonType()).toBe('primary'); + expect(threadDataBackendApiService.getThread).toHaveBeenCalled(); })); - it('should call fetchUpdatedThreads', fakeAsync(() => { - component.activeThread = SuggestionThread.createFromBackendDicts({ - status: 'review', - subject: '', - summary: '', - original_author_username: 'Username1', - last_updated_msecs: 0, - message_count: 1, - thread_id: '1', - state_name: '', - last_nonempty_message_author: '', - last_nonempty_message_text: '' - }, { - suggestion_type: 'edit_exploration_state_content', - suggestion_id: '1', - target_type: '', - target_id: '2', - status: '', - author_name: '', - change_cmd: { - state_name: '', - new_value: {html: ''}, - old_value: {html: ''}, - skill_id: '', - }, - last_updated_msecs: 0 + it('should create a new thread when closing create new thread modal', fakeAsync(() => { + spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + result: Promise.resolve({ + newThreadSubject: 'New subject', + newThreadText: 'New text', + }), + } as NgbModalRef; }); + spyOn(alertsService, 'addSuccessMessage').and.callThrough(); + spyOn(threadDataBackendApiService, 'createNewThreadAsync').and.returnValue( + Promise.resolve() + ); - spyOn(threadDataBackendApiService, 'getThread') - .and.returnValue(null); - component.fetchUpdatedThreads().then(()=> {}); + component.showCreateThreadModal(); + tick(); tick(); - expect(threadDataBackendApiService.getThread) - .toHaveBeenCalled(); + expect( + threadDataBackendApiService.createNewThreadAsync + ).toHaveBeenCalledWith('New subject', 'New text'); + expect(alertsService.addSuccessMessage).toHaveBeenCalledWith( + 'Feedback thread created.' + ); + expect(component.feedbackMessage.status).toBe(null); + expect(component.feedbackMessage.text).toBe(''); })); - it('should create a new thread when closing create new thread modal', - fakeAsync(() => { - spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ - result: Promise.resolve({ - newThreadSubject: 'New subject', - newThreadText: 'New text' - }) - } as NgbModalRef); - }); - spyOn(alertsService, 'addSuccessMessage').and.callThrough(); - spyOn(threadDataBackendApiService, 'createNewThreadAsync').and. - returnValue(Promise.resolve()); - - component.showCreateThreadModal(); - tick(); - tick(); - - expect(threadDataBackendApiService.createNewThreadAsync) - .toHaveBeenCalledWith('New subject', 'New text'); - expect(alertsService.addSuccessMessage).toHaveBeenCalledWith( - 'Feedback thread created.'); - expect(component.feedbackMessage.status).toBe(null); - expect(component.feedbackMessage.text).toBe(''); - })); - - it('should not create a new thread when dismissing create new thread modal', - () => { - spyOn(threadDataBackendApiService, 'createNewThreadAsync'); - spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ - result: Promise.reject() - } as NgbModalRef); - }); - component.showCreateThreadModal(); - - expect(threadDataBackendApiService.createNewThreadAsync).not - .toHaveBeenCalled(); + it('should not create a new thread when dismissing create new thread modal', () => { + spyOn(threadDataBackendApiService, 'createNewThreadAsync'); + spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + result: Promise.reject(), + } as NgbModalRef; }); + component.showCreateThreadModal(); + + expect( + threadDataBackendApiService.createNewThreadAsync + ).not.toHaveBeenCalled(); + }); it('should get css classes based on status', () => { expect(component.getLabelClass('open')).toBe('badge badge-info'); @@ -452,19 +487,22 @@ describe('Feedback Tab Component', () => { expect(component.getHumanReadableStatus('open')).toBe('Open'); expect(component.getHumanReadableStatus('compliment')).toBe('Compliment'); expect(component.getHumanReadableStatus('not_actionable')).toBe( - 'Not Actionable'); + 'Not Actionable' + ); }); - it('should get formatted date string from the timestamp in milliseconds', - () => { - // This method is being spied to avoid any timezone issues. - spyOn(dateTimeFormatService, 'getLocaleAbbreviatedDatetimeString').and - .returnValue('11/21/14'); - // This corresponds to Fri, 21 Nov 2014 09:45:00 GMT. - let NOW_MILLIS = 1416563100000; - expect(component.getLocaleAbbreviatedDatetimeString(NOW_MILLIS)).toBe( - '11/21/14'); - }); + it('should get formatted date string from the timestamp in milliseconds', () => { + // This method is being spied to avoid any timezone issues. + spyOn( + dateTimeFormatService, + 'getLocaleAbbreviatedDatetimeString' + ).and.returnValue('11/21/14'); + // This corresponds to Fri, 21 Nov 2014 09:45:00 GMT. + let NOW_MILLIS = 1416563100000; + expect(component.getLocaleAbbreviatedDatetimeString(NOW_MILLIS)).toBe( + '11/21/14' + ); + }); it('should evaluate if exploration is editable', () => { let isEditableSpy = spyOn(editabilityService, 'isEditable'); diff --git a/core/templates/pages/exploration-editor-page/feedback-tab/feedback-tab.component.ts b/core/templates/pages/exploration-editor-page/feedback-tab/feedback-tab.component.ts index b6fb55fc42dc..8bf8c5ecb533 100644 --- a/core/templates/pages/exploration-editor-page/feedback-tab/feedback-tab.component.ts +++ b/core/templates/pages/exploration-editor-page/feedback-tab/feedback-tab.component.ts @@ -16,27 +16,27 @@ * @fileoverview Component for the exploration editor feedback tab. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { Subscription } from 'rxjs'; -import { CreateFeedbackThreadModalComponent } from 'pages/exploration-editor-page/feedback-tab/templates/create-feedback-thread-modal.component'; -import { AlertsService } from 'services/alerts.service'; -import { DateTimeFormatService } from 'services/date-time-format.service'; -import { EditabilityService } from 'services/editability.service'; -import { LoaderService } from 'services/loader.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { UserService } from 'services/user.service'; -import { ChangeListService } from '../services/change-list.service'; -import { ExplorationStatesService } from '../services/exploration-states.service'; -import { ThreadDataBackendApiService } from './services/thread-data-backend-api.service'; -import { ThreadStatusDisplayService } from './services/thread-status-display.service'; -import { FeedbackThread } from 'domain/feedback_thread/FeedbackThreadObjectFactory'; -import { SuggestionThread } from 'domain/suggestion/suggestion-thread-object.model'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {Subscription} from 'rxjs'; +import {CreateFeedbackThreadModalComponent} from 'pages/exploration-editor-page/feedback-tab/templates/create-feedback-thread-modal.component'; +import {AlertsService} from 'services/alerts.service'; +import {DateTimeFormatService} from 'services/date-time-format.service'; +import {EditabilityService} from 'services/editability.service'; +import {LoaderService} from 'services/loader.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {UserService} from 'services/user.service'; +import {ChangeListService} from '../services/change-list.service'; +import {ExplorationStatesService} from '../services/exploration-states.service'; +import {ThreadDataBackendApiService} from './services/thread-data-backend-api.service'; +import {ThreadStatusDisplayService} from './services/thread-status-display.service'; +import {FeedbackThread} from 'domain/feedback_thread/FeedbackThreadObjectFactory'; +import {SuggestionThread} from 'domain/suggestion/suggestion-thread-object.model'; @Component({ selector: 'oppia-feedback-tab', - templateUrl: './feedback-tab.component.html' + templateUrl: './feedback-tab.component.html', }) export class FeedbackTabComponent implements OnInit, OnDestroy { directiveSubscriptions = new Subscription(); @@ -62,12 +62,11 @@ export class FeedbackTabComponent implements OnInit, OnDestroy { private ngbModal: NgbModal, private threadDataBackendApiService: ThreadDataBackendApiService, private threadStatusDisplayService: ThreadStatusDisplayService, - private userService: UserService, - ) { } + private userService: UserService + ) {} _resetFeedbackMessageFields(): void { - this.feedbackMessage.status = - this.activeThread && this.activeThread.status; + this.feedbackMessage.status = this.activeThread && this.activeThread.status; this.feedbackMessage.text = ''; } @@ -78,17 +77,18 @@ export class FeedbackTabComponent implements OnInit, OnDestroy { // Fetches the threads again if any thread is updated. fetchUpdatedThreads(): Promise { - let activeThreadId = - this.activeThread && this.activeThread.threadId; - return this.threadDataBackendApiService.getFeedbackThreadsAsync().then( - data => { + let activeThreadId = this.activeThread && this.activeThread.threadId; + return this.threadDataBackendApiService + .getFeedbackThreadsAsync() + .then(data => { this.threadData = data; this.threadIsStale = false; if (activeThreadId !== null) { // Fetching threads invalidates old thread domain objects, so we // need to update our reference to the active thread afterwards. this.activeThread = this.threadDataBackendApiService.getThread( - activeThreadId) as SuggestionThread; + activeThreadId + ) as SuggestionThread; } this.loaderService.hideLoadingScreen(); }); @@ -103,15 +103,17 @@ export class FeedbackTabComponent implements OnInit, OnDestroy { _isSuggestionHandled(): boolean { return ( - this.activeThread !== null && - this.activeThread.isSuggestionHandled()); + this.activeThread !== null && this.activeThread.isSuggestionHandled() + ); } _isSuggestionValid(): boolean { return ( this.activeThread !== null && - this.explorationStatesService.hasState( - this.activeThread.getSuggestionStateName())); + this.explorationStatesService.hasState( + this.activeThread.getSuggestionStateName() + ) + ); } _hasUnsavedChanges(): boolean { @@ -119,33 +121,44 @@ export class FeedbackTabComponent implements OnInit, OnDestroy { } showCreateThreadModal(): void { - this.ngbModal.open(CreateFeedbackThreadModalComponent, { - backdrop: 'static' - }).result.then( - (result) => this.threadDataBackendApiService.createNewThreadAsync( - result.newThreadSubject, result.newThreadText).then(() => { - this.clearActiveThread(); - this.alertsService.addSuccessMessage('Feedback thread created.'); - }, - () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }), - () => {} - ); + this.ngbModal + .open(CreateFeedbackThreadModalComponent, { + backdrop: 'static', + }) + .result.then( + result => + this.threadDataBackendApiService + .createNewThreadAsync(result.newThreadSubject, result.newThreadText) + .then( + () => { + this.clearActiveThread(); + this.alertsService.addSuccessMessage( + 'Feedback thread created.' + ); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ), + () => {} + ); } getSuggestionButtonType(): string { - return ( - !this._isSuggestionHandled() && this._isSuggestionValid() && - !this._hasUnsavedChanges()) ? 'primary' : 'default'; + return !this._isSuggestionHandled() && + this._isSuggestionValid() && + !this._hasUnsavedChanges() + ? 'primary' + : 'default'; } addNewMessage(threadId: string, tmpText: string, tmpStatus: string): void { if (threadId === null) { this.alertsService.addWarning( - 'Cannot add message to thread with ID: null.'); + 'Cannot add message to thread with ID: null.' + ); return; } @@ -160,19 +173,21 @@ export class FeedbackTabComponent implements OnInit, OnDestroy { let thread = this.threadDataBackendApiService.getThread(threadId); if (thread === null) { - throw new Error( - 'Trying to add message to a non-existent thread.'); + throw new Error('Trying to add message to a non-existent thread.'); } - this.threadDataBackendApiService.addNewMessageAsync( - thread, tmpText, tmpStatus).then((messages) => { - this._resetFeedbackMessageFields(); - this.activeThread.messages = messages; - this.messageSendingInProgress = false; - }, - () => { - this.messageSendingInProgress = false; - }); + this.threadDataBackendApiService + .addNewMessageAsync(thread, tmpText, tmpStatus) + .then( + messages => { + this._resetFeedbackMessageFields(); + this.activeThread.messages = messages; + this.messageSendingInProgress = false; + }, + () => { + this.messageSendingInProgress = false; + } + ); } setActiveThread(threadId: string): void { @@ -199,7 +214,8 @@ export class FeedbackTabComponent implements OnInit, OnDestroy { getLocaleAbbreviatedDatetimeString(millisSinceEpoch: number): string { return this.dateTimeFormatService.getLocaleAbbreviatedDatetimeString( - millisSinceEpoch); + millisSinceEpoch + ); } isExplorationEditable(): boolean { @@ -215,7 +231,7 @@ export class FeedbackTabComponent implements OnInit, OnDestroy { // Initial load of the thread list on page load. this.feedbackMessage = { status: null, - text: '' + text: '', }; this.clearActiveThread(); @@ -224,15 +240,16 @@ export class FeedbackTabComponent implements OnInit, OnDestroy { () => { this.fetchUpdatedThreads(); } - )); + ) + ); Promise.all([ - this.userService.getUserInfoAsync().then( - userInfo => this.userIsLoggedIn = userInfo.isLoggedIn()), - ]).then( - () => { - this.loaderService.hideLoadingScreen(); - }); + this.userService + .getUserInfoAsync() + .then(userInfo => (this.userIsLoggedIn = userInfo.isLoggedIn())), + ]).then(() => { + this.loaderService.hideLoadingScreen(); + }); } ngOnDestroy(): void { @@ -240,7 +257,9 @@ export class FeedbackTabComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive('oppiaFeedbackTab', - downgradeComponent({ - component: FeedbackTabComponent - }) as angular.IDirectiveFactory); +angular.module('oppia').directive( + 'oppiaFeedbackTab', + downgradeComponent({ + component: FeedbackTabComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-editor-page/feedback-tab/services/thread-data-backend-api.service.spec.ts b/core/templates/pages/exploration-editor-page/feedback-tab/services/thread-data-backend-api.service.spec.ts index 4c67ade1e637..5cd997107682 100644 --- a/core/templates/pages/exploration-editor-page/feedback-tab/services/thread-data-backend-api.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/feedback-tab/services/thread-data-backend-api.service.spec.ts @@ -17,16 +17,23 @@ * which retrieves thread data for the feedback tab of the exploration editor. */ -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; -import { ThreadMessageBackendDict } from 'domain/feedback_message/ThreadMessage.model'; - -import { FeedbackThread, FeedbackThreadBackendDict, FeedbackThreadObjectFactory } from 'domain/feedback_thread/FeedbackThreadObjectFactory'; -import { SuggestionBackendDict } from 'domain/suggestion/suggestion.model'; -import { SuggestionThread } from 'domain/suggestion/suggestion-thread-object.model'; -import { ThreadDataBackendApiService } from 'pages/exploration-editor-page/feedback-tab/services/thread-data-backend-api.service'; -import { ContextService } from 'services/context.service'; -import { CsrfTokenService } from 'services/csrf-token.service'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; +import {ThreadMessageBackendDict} from 'domain/feedback_message/ThreadMessage.model'; + +import { + FeedbackThread, + FeedbackThreadBackendDict, + FeedbackThreadObjectFactory, +} from 'domain/feedback_thread/FeedbackThreadObjectFactory'; +import {SuggestionBackendDict} from 'domain/suggestion/suggestion.model'; +import {SuggestionThread} from 'domain/suggestion/suggestion-thread-object.model'; +import {ThreadDataBackendApiService} from 'pages/exploration-editor-page/feedback-tab/services/thread-data-backend-api.service'; +import {ContextService} from 'services/context.service'; +import {CsrfTokenService} from 'services/csrf-token.service'; describe('retrieving threads service', () => { let httpTestingController: HttpTestingController; @@ -42,7 +49,7 @@ describe('retrieving threads service', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] + imports: [HttpClientTestingModule], }); httpTestingController = TestBed.get(HttpTestingController); }); @@ -63,7 +70,7 @@ describe('retrieving threads service', () => { summary: 'Summary', thread_id: 'exploration.exp1.abc1', last_nonempty_message_author: '', - last_nonempty_message_text: '' + last_nonempty_message_text: '', }, { last_updated_msecs: 1441870501231.642, @@ -75,8 +82,8 @@ describe('retrieving threads service', () => { summary: 'Summary', thread_id: 'exploration.exp1.def2', last_nonempty_message_author: '', - last_nonempty_message_text: '' - } + last_nonempty_message_text: '', + }, ]; mockSuggestionThreads = [ { @@ -89,8 +96,8 @@ describe('retrieving threads service', () => { summary: '', thread_id: 'exploration.exp1.ghi3', last_nonempty_message_author: '', - last_nonempty_message_text: '' - } + last_nonempty_message_text: '', + }, ]; mockSuggestions = [ { @@ -98,10 +105,10 @@ describe('retrieving threads service', () => { change_cmd: { skill_id: 'skill_id', new_value: { - html: 'new content html' + html: 'new content html', }, old_value: { - html: '' + html: '', }, state_name: 'state_1', }, @@ -111,7 +118,7 @@ describe('retrieving threads service', () => { suggestion_type: 'edit_exploration_state_content', target_id: 'exp1', target_type: 'exploration', - } as unknown as SuggestionBackendDict + } as unknown as SuggestionBackendDict, ]; mockMessages = [ { @@ -122,7 +129,7 @@ describe('retrieving threads service', () => { message_id: 0, text: '1st message', updated_status: null, - updated_subject: null + updated_subject: null, }, { author_username: 'author', @@ -132,8 +139,8 @@ describe('retrieving threads service', () => { message_id: 1, text: '2nd message', updated_status: null, - updated_subject: null - } + updated_subject: null, + }, ]; }); @@ -144,32 +151,33 @@ describe('retrieving threads service', () => { threadDataBackendApiService = TestBed.inject(ThreadDataBackendApiService); spyOn(contextService, 'getExplorationId').and.returnValue('exp1'); - spyOn(csrfTokenService, 'getTokenAsync') - .and.returnValue(Promise.resolve('sample-csrf-token')); + spyOn(csrfTokenService, 'getTokenAsync').and.returnValue( + Promise.resolve('sample-csrf-token') + ); }); it('should retrieve feedback threads', fakeAsync(() => { - threadDataBackendApiService.getFeedbackThreadsAsync().then( - threadData => { - for (let mockFeedbackThread of mockFeedbackThreads) { - expect(threadDataBackendApiService.getThread( - mockFeedbackThread.thread_id)).not.toBeNull(); - } - }); + threadDataBackendApiService.getFeedbackThreadsAsync().then(threadData => { + for (let mockFeedbackThread of mockFeedbackThreads) { + expect( + threadDataBackendApiService.getThread(mockFeedbackThread.thread_id) + ).not.toBeNull(); + } + }); let req = httpTestingController.expectOne('/threadlisthandler/exp1'); expect(req.request.method).toEqual('GET'); req.flush({ - feedback_thread_dicts: mockFeedbackThreads + feedback_thread_dicts: mockFeedbackThreads, }); flushMicrotasks(); })); it('should call reject handler if feedback thread is null', fakeAsync(() => { - threadDataBackendApiService.getFeedbackThreadsAsync().then( - Promise.reject, - error => { + threadDataBackendApiService + .getFeedbackThreadsAsync() + .then(Promise.reject, error => { expect(error).toMatch('Missing input backend dict'); Promise.resolve(); }); @@ -177,15 +185,15 @@ describe('retrieving threads service', () => { let req = httpTestingController.expectOne('/threadlisthandler/exp1'); expect(req.request.method).toEqual('GET'); req.flush({ - feedback_thread_dicts: [null] + feedback_thread_dicts: [null], }); flushMicrotasks(); })); it( - 'should set open feedbacks to 0 when fetching feedback threads ' + - 'fails', fakeAsync(async() => { + 'should set open feedbacks to 0 when fetching feedback threads ' + 'fails', + fakeAsync(async () => { expect(threadDataBackendApiService.getOpenThreadsCount()).toEqual(0); threadDataBackendApiService.getFeedbackThreadsAsync(); @@ -193,12 +201,13 @@ describe('retrieving threads service', () => { expect(req.request.method).toEqual('GET'); req.flush('Error on retrieving feedback threads.', { status: 500, - statusText: 'Error on retrieving feedback threads.' + statusText: 'Error on retrieving feedback threads.', }); flushMicrotasks(); expect(threadDataBackendApiService.getOpenThreadsCount()).toEqual(0); - })); + }) + ); it('should successfully fetch the messages of a thread', fakeAsync(() => { let mockThread = mockFeedbackThreads[0]; @@ -206,77 +215,74 @@ describe('retrieving threads service', () => { let setMessagesSpy = spyOn(thread, 'setMessages').and.callThrough(); - threadDataBackendApiService.getMessagesAsync(thread).then( - () => { - expect(setMessagesSpy).toHaveBeenCalled(); - expect(thread.lastNonemptyMessageSummary.text).toEqual('2nd message'); - Promise.resolve(); - }, - Promise.reject); + threadDataBackendApiService.getMessagesAsync(thread).then(() => { + expect(setMessagesSpy).toHaveBeenCalled(); + expect(thread.lastNonemptyMessageSummary.text).toEqual('2nd message'); + Promise.resolve(); + }, Promise.reject); let req = httpTestingController.expectOne( - '/threadhandler/exploration.exp1.abc1'); + '/threadhandler/exploration.exp1.abc1' + ); expect(req.request.method).toEqual('GET'); - req.flush({ messages: mockMessages }); + req.flush({messages: mockMessages}); flushMicrotasks(); })); - it('should throw error if trying to fetch messages of' + - 'null thread', async() => { - // This throws "Argument of type 'null' is not assignable to parameter of - // type 'SuggestionAndFeedbackThread'". We need to suppress this - // error because we are testing validations here. We can't remove - // null here because the function actually accepts null. - // @ts-ignore - await expectAsync(threadDataBackendApiService.getMessagesAsync(null)) - .toBeRejectedWithError('Trying to update a non-existent thread'); - }); - it( - 'should call reject handler when fetching messages fails', - fakeAsync(() => { - let mockThread = mockFeedbackThreads[0]; - let thread = feedbackThreadObjectFactory.createFromBackendDict( - mockThread); - - let setMessagesSpy = spyOn(thread, 'setMessages').and.callThrough(); - - threadDataBackendApiService.getMessagesAsync(thread).then( - Promise.reject, - error => { - expect(error.error).toEqual( - 'Error on fetching messages from a thread.'); - expect(error.status).toEqual(500); - expect(setMessagesSpy).not.toHaveBeenCalled(); - Promise.resolve(); - }); + 'should throw error if trying to fetch messages of' + 'null thread', + async () => { + await expectAsync( + // This throws "Argument of type 'null' is not assignable to parameter of + // type 'SuggestionAndFeedbackThread'". We need to suppress this + // error because we are testing validations here. We can't remove + // null here because the function actually accepts null. + // @ts-ignore + threadDataBackendApiService.getMessagesAsync(null) + ).toBeRejectedWithError('Trying to update a non-existent thread'); + } + ); - let req = httpTestingController.expectOne( - '/threadhandler/exploration.exp1.abc1'); - expect(req.request.method).toEqual('GET'); - req.flush('Error on fetching messages from a thread.', { - status: 500, - statusText: 'Error on fetching messages from a thread.' + it('should call reject handler when fetching messages fails', fakeAsync(() => { + let mockThread = mockFeedbackThreads[0]; + let thread = feedbackThreadObjectFactory.createFromBackendDict(mockThread); + + let setMessagesSpy = spyOn(thread, 'setMessages').and.callThrough(); + + threadDataBackendApiService + .getMessagesAsync(thread) + .then(Promise.reject, error => { + expect(error.error).toEqual( + 'Error on fetching messages from a thread.' + ); + expect(error.status).toEqual(500); + expect(setMessagesSpy).not.toHaveBeenCalled(); + Promise.resolve(); }); - flushMicrotasks(); - })); + let req = httpTestingController.expectOne( + '/threadhandler/exploration.exp1.abc1' + ); + expect(req.request.method).toEqual('GET'); + req.flush('Error on fetching messages from a thread.', { + status: 500, + statusText: 'Error on fetching messages from a thread.', + }); + + flushMicrotasks(); + })); it('should successfully fetch feedback stats', fakeAsync(() => { - threadDataBackendApiService.getFeedbackThreadsAsync().then( - () => { - expect(threadDataBackendApiService.getOpenThreadsCount()).toEqual(2); - Promise.resolve(); - }, - Promise.reject); + threadDataBackendApiService.getFeedbackThreadsAsync().then(() => { + expect(threadDataBackendApiService.getOpenThreadsCount()).toEqual(2); + Promise.resolve(); + }, Promise.reject); let req = httpTestingController.expectOne('/threadlisthandler/exp1'); expect(req.request.method).toEqual('GET'); req.flush({ - feedback_thread_dicts: [ - mockFeedbackThreads[0], - mockFeedbackThreads[0]], + feedback_thread_dicts: [mockFeedbackThreads[0], mockFeedbackThreads[0]], }); flushMicrotasks(); @@ -300,49 +306,46 @@ describe('retrieving threads service', () => { status: 'open', subject: subject, summary: null, - thread_id: 'exploration.exp1.jkl1' + thread_id: 'exploration.exp1.jkl1', }; expect(threadDataBackendApiService.getOpenThreadsCount()).toEqual(0); - threadDataBackendApiService.createNewThreadAsync(subject, 'Text').then( - threadData => { + threadDataBackendApiService + .createNewThreadAsync(subject, 'Text') + .then(threadData => { const data = threadData as FeedbackThread[]; expect(data.length).toEqual(1); - expect(data[0].threadId) - .toEqual('exploration.exp1.jkl1'); + expect(data[0].threadId).toEqual('exploration.exp1.jkl1'); expect(threadDataBackendApiService.getOpenThreadsCount()).toEqual(1); Promise.resolve(); - }, - Promise.reject); + }, Promise.reject); let req = httpTestingController.expectOne('/threadlisthandler/exp1'); expect(req.request.method).toEqual('POST'); - req.flush(null, { status: 200, statusText: '' }); + req.flush(null, {status: 200, statusText: ''}); flushMicrotasks(); req = httpTestingController.expectOne('/threadlisthandler/exp1'); expect(req.request.method).toEqual('GET'); req.flush({ - feedback_thread_dicts: [mockCreatedFeedbackThread] + feedback_thread_dicts: [mockCreatedFeedbackThread], }); flushMicrotasks(); })); - it( - 'should use reject handler when creating a new thread fails', - fakeAsync(() => { - expect(threadDataBackendApiService.getOpenThreadsCount()).toEqual(0); - threadDataBackendApiService.createNewThreadAsync('Subject', 'Text'); + it('should use reject handler when creating a new thread fails', fakeAsync(() => { + expect(threadDataBackendApiService.getOpenThreadsCount()).toEqual(0); + threadDataBackendApiService.createNewThreadAsync('Subject', 'Text'); - let req = httpTestingController.expectOne('/threadlisthandler/exp1'); - expect(req.request.method).toEqual('POST'); - req.flush(null, { status: 500, statusText: '' }); + let req = httpTestingController.expectOne('/threadlisthandler/exp1'); + expect(req.request.method).toEqual('POST'); + req.flush(null, {status: 500, statusText: ''}); - flushMicrotasks(); - expect(threadDataBackendApiService.getOpenThreadsCount()).toEqual(0); - })); + flushMicrotasks(); + expect(threadDataBackendApiService.getOpenThreadsCount()).toEqual(0); + })); it('should successfully mark thread as seen', fakeAsync(() => { let mockThread = mockFeedbackThreads[0]; @@ -350,209 +353,224 @@ describe('retrieving threads service', () => { threadDataBackendApiService.markThreadAsSeenAsync(thread); let req = httpTestingController.expectOne( - '/feedbackhandler/thread_view_event/exploration.exp1.abc1'); + '/feedbackhandler/thread_view_event/exploration.exp1.abc1' + ); expect(req.request.method).toEqual('POST'); - req.flush(null, { status: 200, statusText: '' }); + req.flush(null, {status: 200, statusText: ''}); flushMicrotasks(); })); - it('should throw error if trying to mark null thread as seen', async() => { - // This throws "Argument of type 'null' is not assignable to parameter of - // type 'SuggestionAndFeedbackThread'". We need to suppress this - // error because we are testing validations here. We can't remove - // null here because the function actually accepts null. - // @ts-ignore - await expectAsync(threadDataBackendApiService.markThreadAsSeenAsync(null)) - .toBeRejectedWithError('Trying to update a non-existent thread'); + it('should throw error if trying to mark null thread as seen', async () => { + await expectAsync( + // This throws "Argument of type 'null' is not assignable to parameter of + // type 'SuggestionAndFeedbackThread'". We need to suppress this + // error because we are testing validations here. We can't remove + // null here because the function actually accepts null. + // @ts-ignore + threadDataBackendApiService.markThreadAsSeenAsync(null) + ).toBeRejectedWithError('Trying to update a non-existent thread'); }); - it( - 'should use reject handler when marking thread as seen fails', - fakeAsync(() => { - let mockThread = mockFeedbackThreads[0]; - let thread = feedbackThreadObjectFactory.createFromBackendDict( - mockThread); + it('should use reject handler when marking thread as seen fails', fakeAsync(() => { + let mockThread = mockFeedbackThreads[0]; + let thread = feedbackThreadObjectFactory.createFromBackendDict(mockThread); - threadDataBackendApiService.markThreadAsSeenAsync(thread).then( - Promise.reject, - error => { - expect(error.status).toEqual(500); - Promise.resolve(); - }); + threadDataBackendApiService + .markThreadAsSeenAsync(thread) + .then(Promise.reject, error => { + expect(error.status).toEqual(500); + Promise.resolve(); + }); - let req = httpTestingController.expectOne( - '/feedbackhandler/thread_view_event/exploration.exp1.abc1'); - expect(req.request.method).toEqual('POST'); - req.flush(null, { status: 500, statusText: '' }); + let req = httpTestingController.expectOne( + '/feedbackhandler/thread_view_event/exploration.exp1.abc1' + ); + expect(req.request.method).toEqual('POST'); + req.flush(null, {status: 500, statusText: ''}); - flushMicrotasks(); - })); + flushMicrotasks(); + })); - it('should use reject handler when passing a null thread', async() => { - await expectAsync(threadDataBackendApiService.addNewMessageAsync( - // This throws "Argument of type 'null' is not assignable to parameter of - // type 'SuggestionAndFeedbackThread'". We need to suppress this - // error because we are testing validations here. We can't remove - // null here because the function actually accepts null. - // @ts-ignore - null, 'Message', 'open')).toBeRejectedWithError( - 'Trying to update a non-existent thread'); + it('should use reject handler when passing a null thread', async () => { + await expectAsync( + threadDataBackendApiService.addNewMessageAsync( + // This throws "Argument of type 'null' is not assignable to parameter of + // type 'SuggestionAndFeedbackThread'". We need to suppress this + // error because we are testing validations here. We can't remove + // null here because the function actually accepts null. + // @ts-ignore + null, + 'Message', + 'open' + ) + ).toBeRejectedWithError('Trying to update a non-existent thread'); }); it( 'should successfully add a new message in a thread when its status ' + - 'is different than old status and its status is close', fakeAsync(() => { + 'is different than old status and its status is close', + fakeAsync(() => { let mockThread = mockFeedbackThreads[0]; - let thread = feedbackThreadObjectFactory.createFromBackendDict( - mockThread); + let thread = + feedbackThreadObjectFactory.createFromBackendDict(mockThread); // Fetch feedback stats. threadDataBackendApiService.getFeedbackThreadsAsync(); let req = httpTestingController.expectOne('/threadlisthandler/exp1'); expect(req.request.method).toEqual('GET'); - req.flush({ feedback_thread_dicts: [mockFeedbackThreads[0]] }); + req.flush({feedback_thread_dicts: [mockFeedbackThreads[0]]}); flushMicrotasks(); expect(threadDataBackendApiService.getOpenThreadsCount()).toEqual(1); - threadDataBackendApiService.addNewMessageAsync( - thread, 'Message', 'close').then(() => { - expect(threadDataBackendApiService.getOpenThreadsCount()).toEqual(0); - Promise.resolve(); - }, - Promise.reject); + threadDataBackendApiService + .addNewMessageAsync(thread, 'Message', 'close') + .then(() => { + expect(threadDataBackendApiService.getOpenThreadsCount()).toEqual(0); + Promise.resolve(); + }, Promise.reject); req = httpTestingController.expectOne( - '/threadhandler/exploration.exp1.abc1'); + '/threadhandler/exploration.exp1.abc1' + ); expect(req.request.method).toEqual('POST'); req.flush({messages: []}); flushMicrotasks(); - })); + }) + ); it( 'should successfully add a new message in a thread when its status ' + - 'is different of old status and its status is open', fakeAsync(() => { + 'is different of old status and its status is open', + fakeAsync(() => { let mockThread = mockFeedbackThreads[0]; mockThread.status = 'close'; - let thread = feedbackThreadObjectFactory.createFromBackendDict( - mockThread); + let thread = + feedbackThreadObjectFactory.createFromBackendDict(mockThread); // Fetch feedback stats. threadDataBackendApiService.getFeedbackThreadsAsync(); let req = httpTestingController.expectOne('/threadlisthandler/exp1'); expect(req.request.method).toEqual('GET'); - req.flush({ feedback_thread_dicts: [mockFeedbackThreads[0]] }); + req.flush({feedback_thread_dicts: [mockFeedbackThreads[0]]}); flushMicrotasks(); expect(threadDataBackendApiService.getOpenThreadsCount()).toEqual(0); - threadDataBackendApiService.addNewMessageAsync( - thread, 'Message', 'open').then(() => { - expect(threadDataBackendApiService.getOpenThreadsCount()).toEqual(1); - Promise.resolve(); - }, - Promise.reject); + threadDataBackendApiService + .addNewMessageAsync(thread, 'Message', 'open') + .then(() => { + expect(threadDataBackendApiService.getOpenThreadsCount()).toEqual(1); + Promise.resolve(); + }, Promise.reject); req = httpTestingController.expectOne( - '/threadhandler/exploration.exp1.abc1'); + '/threadhandler/exploration.exp1.abc1' + ); expect(req.request.method).toEqual('POST'); req.flush({messages: []}); flushMicrotasks(); - })); + }) + ); it( 'should successfully add a new message in a thread when its status ' + - 'is equal old status', fakeAsync(() => { + 'is equal old status', + fakeAsync(() => { let mockThread = mockFeedbackThreads[0]; - let thread = feedbackThreadObjectFactory.createFromBackendDict( - mockThread); + let thread = + feedbackThreadObjectFactory.createFromBackendDict(mockThread); // Fetch feedback stats. threadDataBackendApiService.getFeedbackThreadsAsync(); let req = httpTestingController.expectOne('/threadlisthandler/exp1'); expect(req.request.method).toEqual('GET'); - req.flush({ feedback_thread_dicts: [mockFeedbackThreads[0]] }); + req.flush({feedback_thread_dicts: [mockFeedbackThreads[0]]}); flushMicrotasks(); expect(threadDataBackendApiService.getOpenThreadsCount()).toEqual(1); - threadDataBackendApiService.addNewMessageAsync( - thread, 'Message', 'open').then(() => { - expect(threadDataBackendApiService.getOpenThreadsCount()).toEqual(1); - Promise.resolve(); - }, - Promise.reject); + threadDataBackendApiService + .addNewMessageAsync(thread, 'Message', 'open') + .then(() => { + expect(threadDataBackendApiService.getOpenThreadsCount()).toEqual(1); + Promise.resolve(); + }, Promise.reject); req = httpTestingController.expectOne( - '/threadhandler/exploration.exp1.abc1'); + '/threadhandler/exploration.exp1.abc1' + ); expect(req.request.method).toEqual('POST'); req.flush({messages: mockMessages}); flushMicrotasks(); - })); + }) + ); it('should successfully resolve a suggestion', fakeAsync(() => { let thread = SuggestionThread.createFromBackendDicts( - mockSuggestionThreads[0], mockSuggestions[0]); + mockSuggestionThreads[0], + mockSuggestions[0] + ); - threadDataBackendApiService.resolveSuggestionAsync( - thread, 'Message', 'status', 'a') + threadDataBackendApiService + .resolveSuggestionAsync(thread, 'Message', 'status', 'a') .then(() => { expect(threadDataBackendApiService.getOpenThreadsCount()).toEqual(0); Promise.resolve(); - }, - Promise.reject); + }, Promise.reject); let req = httpTestingController.expectOne( - '/suggestionactionhandler/exploration/exp1/exploration.exp1.ghi3'); + '/suggestionactionhandler/exploration/exp1/exploration.exp1.ghi3' + ); expect(req.request.method).toEqual('PUT'); - req.flush(null, { status: 200, statusText: '' }); + req.flush(null, {status: 200, statusText: ''}); flushMicrotasks(); req = httpTestingController.expectOne( - '/threadhandler/exploration.exp1.ghi3'); + '/threadhandler/exploration.exp1.ghi3' + ); expect(req.request.method).toEqual('GET'); req.flush({messages: []}); flushMicrotasks(); })); - it('should throw an error if trying to resolve a null thread', async() => { + it('should throw an error if trying to resolve a null thread', async () => { await expectAsync( // This throws "Argument of type 'null' is not assignable to parameter of // type 'SuggestionAndFeedbackThread'". We need to suppress this // error because we are testing validations here. We can't remove // null here because the function actually accepts null. // @ts-ignore - threadDataBackendApiService.resolveSuggestionAsync(null, '', '', '')) - .toBeRejectedWithError('Trying to update a non-existent thread'); + threadDataBackendApiService.resolveSuggestionAsync(null, '', '', '') + ).toBeRejectedWithError('Trying to update a non-existent thread'); }); - it('should fetch messages from a given threadId', - fakeAsync(() => { - var successHandler = jasmine.createSpy('success'); - var failHandler = jasmine.createSpy('fail'); + it('should fetch messages from a given threadId', fakeAsync(() => { + var successHandler = jasmine.createSpy('success'); + var failHandler = jasmine.createSpy('fail'); - let mockThread = mockFeedbackThreads[0]; - let thread = feedbackThreadObjectFactory - .createFromBackendDict(mockThread); + let mockThread = mockFeedbackThreads[0]; + let thread = feedbackThreadObjectFactory.createFromBackendDict(mockThread); - threadDataBackendApiService.fetchMessagesAsync(thread.threadId) - .then(successHandler, failHandler); + threadDataBackendApiService + .fetchMessagesAsync(thread.threadId) + .then(successHandler, failHandler); - var req = httpTestingController.expectOne( - '/threadhandler/exploration.exp1.abc1'); - expect(req.request.method).toEqual('GET'); - req.flush('Success'); + var req = httpTestingController.expectOne( + '/threadhandler/exploration.exp1.abc1' + ); + expect(req.request.method).toEqual('GET'); + req.flush('Success'); - flushMicrotasks(); + flushMicrotasks(); - expect(successHandler).toHaveBeenCalled(); - expect(failHandler).not.toHaveBeenCalled(); - }) - ); + expect(successHandler).toHaveBeenCalled(); + expect(failHandler).not.toHaveBeenCalled(); + })); }); diff --git a/core/templates/pages/exploration-editor-page/feedback-tab/services/thread-data-backend-api.service.ts b/core/templates/pages/exploration-editor-page/feedback-tab/services/thread-data-backend-api.service.ts index c2613260f841..b2a22a4350cc 100644 --- a/core/templates/pages/exploration-editor-page/feedback-tab/services/thread-data-backend-api.service.ts +++ b/core/templates/pages/exploration-editor-page/feedback-tab/services/thread-data-backend-api.service.ts @@ -17,18 +17,25 @@ * feedback tab of the exploration editor. */ -import { EventEmitter, Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { downgradeInjectable } from '@angular/upgrade/static'; - -import { AppConstants } from 'app.constants'; -import { FeedbackThread, FeedbackThreadBackendDict, FeedbackThreadObjectFactory } from 'domain/feedback_thread/FeedbackThreadObjectFactory'; -import { ThreadMessage, ThreadMessageBackendDict } from 'domain/feedback_message/ThreadMessage.model'; -import { SuggestionThread } from 'domain/suggestion/suggestion-thread-object.model'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { ExplorationEditorPageConstants } from 'pages/exploration-editor-page/exploration-editor-page.constants'; -import { AlertsService } from 'services/alerts.service'; -import { ContextService } from 'services/context.service'; +import {EventEmitter, Injectable} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; +import {downgradeInjectable} from '@angular/upgrade/static'; + +import {AppConstants} from 'app.constants'; +import { + FeedbackThread, + FeedbackThreadBackendDict, + FeedbackThreadObjectFactory, +} from 'domain/feedback_thread/FeedbackThreadObjectFactory'; +import { + ThreadMessage, + ThreadMessageBackendDict, +} from 'domain/feedback_message/ThreadMessage.model'; +import {SuggestionThread} from 'domain/suggestion/suggestion-thread-object.model'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {ExplorationEditorPageConstants} from 'pages/exploration-editor-page/exploration-editor-page.constants'; +import {AlertsService} from 'services/alerts.service'; +import {ContextService} from 'services/context.service'; export type SuggestionAndFeedbackThread = FeedbackThread | SuggestionThread; @@ -37,27 +44,28 @@ export interface FeedbackThreads { } interface FeedbackThreadData { - 'feedback_thread_dicts': FeedbackThreadBackendDict[]; + feedback_thread_dicts: FeedbackThreadBackendDict[]; } export interface ThreadMessages { - 'messages': ThreadMessageBackendDict[]; + messages: ThreadMessageBackendDict[]; } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ThreadDataBackendApiService { // Container for all of the threads related to this exploration. - threadsById: Map = ( - new Map()); + threadsById: Map = new Map< + string, + SuggestionAndFeedbackThread + >(); // Cached number of open threads requiring action. feedbackThreads: FeedbackThread[] | undefined; countOfOpenFeedbackThreads = 0; _feedbackThreadsInitializedEventEmitter = new EventEmitter(); - constructor( private alertsService: AlertsService, private contextService: ContextService, @@ -68,40 +76,49 @@ export class ThreadDataBackendApiService { getFeedbackThreadViewEventUrl(threadId: string): string { return this.urlInterpolationService.interpolateUrl( - '/feedbackhandler/thread_view_event/', { - thread_id: threadId - }); + '/feedbackhandler/thread_view_event/', + { + thread_id: threadId, + } + ); } getSuggestionActionHandlerUrl(threadId: string): string { return this.urlInterpolationService.interpolateUrl( - '/suggestionactionhandler/exploration//', { + '/suggestionactionhandler/exploration//', + { exploration_id: this.contextService.getExplorationId(), - thread_id: threadId - }); + thread_id: threadId, + } + ); } getThreadHandlerUrl(threadId: string): string { return this.urlInterpolationService.interpolateUrl( - '/threadhandler/', { - thread_id: threadId - }); + '/threadhandler/', + { + thread_id: threadId, + } + ); } getThreadListHandlerUrl(): string { return this.urlInterpolationService.interpolateUrl( - '/threadlisthandler/', { - exploration_id: this.contextService.getExplorationId() - }); + '/threadlisthandler/', + { + exploration_id: this.contextService.getExplorationId(), + } + ); } setFeedbackThreadFromBackendDict( - threadBackendDict: FeedbackThreadBackendDict): FeedbackThread { + threadBackendDict: FeedbackThreadBackendDict + ): FeedbackThread { if (!threadBackendDict) { throw new Error('Missing input backend dict'); } - let thread = this.feedbackThreadObjectFactory.createFromBackendDict( - threadBackendDict); + let thread = + this.feedbackThreadObjectFactory.createFromBackendDict(threadBackendDict); this.threadsById.set(thread.threadId, thread); return thread; } @@ -118,17 +135,19 @@ export class ThreadDataBackendApiService { async getFeedbackThreadsAsync(useCache = true): Promise { if (!this.feedbackThreads || !useCache) { const threads = this.http.get( - this.getThreadListHandlerUrl()); + this.getThreadListHandlerUrl() + ); - return threads - .toPromise() - .then((response: FeedbackThreadData) => { + return threads.toPromise().then( + (response: FeedbackThreadData) => { const feedbackThreadBackendDicts = response.feedback_thread_dicts; this.countOfOpenFeedbackThreads = feedbackThreadBackendDicts.filter( - thread => thread?.status === 'open').length; + thread => thread?.status === 'open' + ).length; - this.feedbackThreads = feedbackThreadBackendDicts.map( - dict => this.setFeedbackThreadFromBackendDict(dict)); + this.feedbackThreads = feedbackThreadBackendDicts.map(dict => + this.setFeedbackThreadFromBackendDict(dict) + ); // Open Threads should be displayed first and older threads should be // Given priority over newer threads, // While addressed threads are sorted by last updated time. @@ -152,130 +171,154 @@ export class ThreadDataBackendApiService { this.countOfOpenFeedbackThreads = 0; this._feedbackThreadsInitializedEventEmitter.emit(); return this.feedbackThreads; - }); + } + ); } else { return this.feedbackThreads; } } async getMessagesAsync( - thread: SuggestionAndFeedbackThread + thread: SuggestionAndFeedbackThread ): Promise { if (!thread) { throw new Error('Trying to update a non-existent thread'); } let threadId = thread.threadId; - return this.http.get( - this.getThreadHandlerUrl(threadId) - ).toPromise().then((response: ThreadMessages) => { - let threadMessageBackendDicts = response.messages; - thread.setMessages(threadMessageBackendDicts.map( - m => ThreadMessage.createFromBackendDict(m))); - return thread.getMessages(); - }); + return this.http + .get(this.getThreadHandlerUrl(threadId)) + .toPromise() + .then((response: ThreadMessages) => { + let threadMessageBackendDicts = response.messages; + thread.setMessages( + threadMessageBackendDicts.map(m => + ThreadMessage.createFromBackendDict(m) + ) + ); + return thread.getMessages(); + }); } - async fetchMessagesAsync( - threadId: string - ): Promise { - return this.http.get( - this.getThreadHandlerUrl(threadId) - ).toPromise(); + async fetchMessagesAsync(threadId: string): Promise { + return this.http + .get(this.getThreadHandlerUrl(threadId)) + .toPromise(); } getOpenThreadsCount(): number { return this.countOfOpenFeedbackThreads || 0; } - async createNewThreadAsync(newSubject: string, newText: string): - Promise { - return this.http.post( - this.getThreadListHandlerUrl(), { + async createNewThreadAsync( + newSubject: string, + newText: string + ): Promise { + return this.http + .post(this.getThreadListHandlerUrl(), { subject: newSubject, - text: newText - } - ).toPromise().then(async() => { - this.countOfOpenFeedbackThreads += 1; - return this.getFeedbackThreadsAsync(false); - }, - error => { - this.alertsService.addWarning( - 'Error creating new thread: ' + error + '.'); - }); + text: newText, + }) + .toPromise() + .then( + async () => { + this.countOfOpenFeedbackThreads += 1; + return this.getFeedbackThreadsAsync(false); + }, + error => { + this.alertsService.addWarning( + 'Error creating new thread: ' + error + '.' + ); + } + ); } async markThreadAsSeenAsync( - thread: SuggestionAndFeedbackThread): Promise { + thread: SuggestionAndFeedbackThread + ): Promise { if (!thread) { throw new Error('Trying to update a non-existent thread'); } let threadId = thread.threadId; - return this.http.post( - this.getFeedbackThreadViewEventUrl(threadId), {}).toPromise().then(); + return this.http + .post(this.getFeedbackThreadViewEventUrl(threadId), {}) + .toPromise() + .then(); } async addNewMessageAsync( - thread: SuggestionAndFeedbackThread, newMessage: string, - newStatus: string): Promise { + thread: SuggestionAndFeedbackThread, + newMessage: string, + newStatus: string + ): Promise { if (!thread) { throw new Error('Trying to update a non-existent thread'); } let threadId = thread.threadId; let oldStatus = thread.status; - let updatedStatus = (oldStatus === newStatus) ? null : newStatus; - - return this.http.post(this.getThreadHandlerUrl(threadId), { - updated_status: updatedStatus, - updated_subject: null, - text: newMessage - }).toPromise().then((response: ThreadMessages) => { - if (updatedStatus) { - if (newStatus === ExplorationEditorPageConstants.STATUS_OPEN) { - this.countOfOpenFeedbackThreads += 1; - } else { - this.countOfOpenFeedbackThreads += ( - oldStatus === ExplorationEditorPageConstants.STATUS_OPEN ? -1 : 0); + let updatedStatus = oldStatus === newStatus ? null : newStatus; + + return this.http + .post(this.getThreadHandlerUrl(threadId), { + updated_status: updatedStatus, + updated_subject: null, + text: newMessage, + }) + .toPromise() + .then((response: ThreadMessages) => { + if (updatedStatus) { + if (newStatus === ExplorationEditorPageConstants.STATUS_OPEN) { + this.countOfOpenFeedbackThreads += 1; + } else { + this.countOfOpenFeedbackThreads += + oldStatus === ExplorationEditorPageConstants.STATUS_OPEN ? -1 : 0; + } } - } - thread.status = newStatus; - let threadMessageBackendDicts = response.messages; - thread.setMessages(threadMessageBackendDicts.map( - m => ThreadMessage.createFromBackendDict(m))); - return thread.messages; - }); + thread.status = newStatus; + let threadMessageBackendDicts = response.messages; + thread.setMessages( + threadMessageBackendDicts.map(m => + ThreadMessage.createFromBackendDict(m) + ) + ); + return thread.messages; + }); } async resolveSuggestionAsync( - thread: SuggestionAndFeedbackThread, - action: string, - commitMsg: string, - reviewMsg: string + thread: SuggestionAndFeedbackThread, + action: string, + commitMsg: string, + reviewMsg: string ): Promise { if (!thread) { throw new Error('Trying to update a non-existent thread'); } let threadId = thread.threadId; - return this.http.put(this.getSuggestionActionHandlerUrl(threadId), { - action: action, - review_message: reviewMsg, - commit_message: ( - action === AppConstants.ACTION_ACCEPT_SUGGESTION ? - commitMsg : null) - }).toPromise().then(async() => { - thread.status = ( - action === AppConstants.ACTION_ACCEPT_SUGGESTION ? - ExplorationEditorPageConstants.STATUS_FIXED : - ExplorationEditorPageConstants.STATUS_IGNORED - ); - - return this.getMessagesAsync(thread); - }); + return this.http + .put(this.getSuggestionActionHandlerUrl(threadId), { + action: action, + review_message: reviewMsg, + commit_message: + action === AppConstants.ACTION_ACCEPT_SUGGESTION ? commitMsg : null, + }) + .toPromise() + .then(async () => { + thread.status = + action === AppConstants.ACTION_ACCEPT_SUGGESTION + ? ExplorationEditorPageConstants.STATUS_FIXED + : ExplorationEditorPageConstants.STATUS_IGNORED; + + return this.getMessagesAsync(thread); + }); } } -angular.module('oppia').factory( - 'ThreadDataBackendApiService', - downgradeInjectable(ThreadDataBackendApiService)); +angular + .module('oppia') + .factory( + 'ThreadDataBackendApiService', + downgradeInjectable(ThreadDataBackendApiService) + ); diff --git a/core/templates/pages/exploration-editor-page/feedback-tab/services/thread-status-display.service.spec.ts b/core/templates/pages/exploration-editor-page/feedback-tab/services/thread-status-display.service.spec.ts index 41f70643ae03..f95ac29ccf03 100644 --- a/core/templates/pages/exploration-editor-page/feedback-tab/services/thread-status-display.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/feedback-tab/services/thread-status-display.service.spec.ts @@ -18,7 +18,7 @@ * feedback tab of the exploration editor. */ -import { ThreadStatusDisplayService } from 'pages/exploration-editor-page/feedback-tab/services/thread-status-display.service'; +import {ThreadStatusDisplayService} from 'pages/exploration-editor-page/feedback-tab/services/thread-status-display.service'; describe('Thread Status Display Service', () => { let threadStatusDisplayService: ThreadStatusDisplayService; @@ -32,35 +32,40 @@ describe('Thread Status Display Service', () => { for (let i = 0; i < mockStatusChoices.length; i++) { let mockStatusID = mockStatusChoices[i].id; expect( - threadStatusDisplayService.getHumanReadableStatus( - mockStatusID)).toBe(mockStatusChoices[i].text); + threadStatusDisplayService.getHumanReadableStatus(mockStatusID) + ).toBe(mockStatusChoices[i].text); } let mockStatusID = 'INVALID_STATUS'; expect( - threadStatusDisplayService.getHumanReadableStatus( - mockStatusID)).toBe(''); + threadStatusDisplayService.getHumanReadableStatus(mockStatusID) + ).toBe(''); }); it('should give appropriate label class for status id', () => { let mockStatusID = 'open'; expect(threadStatusDisplayService.getLabelClass(mockStatusID)).toBe( - 'badge badge-info'); + 'badge badge-info' + ); mockStatusID = 'fixed'; expect(threadStatusDisplayService.getLabelClass(mockStatusID)).toBe( - 'badge badge-secondary'); + 'badge badge-secondary' + ); mockStatusID = 'ignored'; expect(threadStatusDisplayService.getLabelClass(mockStatusID)).toBe( - 'badge badge-secondary'); + 'badge badge-secondary' + ); mockStatusID = 'not_actionable'; expect(threadStatusDisplayService.getLabelClass(mockStatusID)).toBe( - 'badge badge-secondary'); + 'badge badge-secondary' + ); mockStatusID = 'compliment'; expect(threadStatusDisplayService.getLabelClass(mockStatusID)).toBe( - 'badge badge-success'); + 'badge badge-success' + ); }); }); diff --git a/core/templates/pages/exploration-editor-page/feedback-tab/services/thread-status-display.service.ts b/core/templates/pages/exploration-editor-page/feedback-tab/services/thread-status-display.service.ts index e19a725863bb..b0d11663072a 100644 --- a/core/templates/pages/exploration-editor-page/feedback-tab/services/thread-status-display.service.ts +++ b/core/templates/pages/exploration-editor-page/feedback-tab/services/thread-status-display.service.ts @@ -19,29 +19,35 @@ import cloneDeep from 'lodash/cloneDeep'; -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ThreadStatusDisplayService { - static _STATUS_CHOICES = [{ - id: 'open', - text: 'Open' - }, { - id: 'fixed', - text: 'Fixed' - }, { - id: 'ignored', - text: 'Ignored' - }, { - id: 'compliment', - text: 'Compliment' - }, { - id: 'not_actionable', - text: 'Not Actionable' - }]; + static _STATUS_CHOICES = [ + { + id: 'open', + text: 'Open', + }, + { + id: 'fixed', + text: 'Fixed', + }, + { + id: 'ignored', + text: 'Ignored', + }, + { + id: 'compliment', + text: 'Compliment', + }, + { + id: 'not_actionable', + text: 'Not Actionable', + }, + ]; STATUS_CHOICES = cloneDeep(ThreadStatusDisplayService._STATUS_CHOICES); @@ -57,7 +63,10 @@ export class ThreadStatusDisplayService { getHumanReadableStatus(status: string): string { for ( - var i = 0; i < ThreadStatusDisplayService._STATUS_CHOICES.length; i++) { + var i = 0; + i < ThreadStatusDisplayService._STATUS_CHOICES.length; + i++ + ) { if (ThreadStatusDisplayService._STATUS_CHOICES[i].id === status) { return ThreadStatusDisplayService._STATUS_CHOICES[i].text; } @@ -66,6 +75,9 @@ export class ThreadStatusDisplayService { } } -angular.module('oppia').factory( - 'ThreadStatusDisplayService', - downgradeInjectable(ThreadStatusDisplayService)); +angular + .module('oppia') + .factory( + 'ThreadStatusDisplayService', + downgradeInjectable(ThreadStatusDisplayService) + ); diff --git a/core/templates/pages/exploration-editor-page/feedback-tab/templates/create-feedback-thread-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/feedback-tab/templates/create-feedback-thread-modal.component.spec.ts index a11bb718777c..f85d3fee1704 100644 --- a/core/templates/pages/exploration-editor-page/feedback-tab/templates/create-feedback-thread-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/feedback-tab/templates/create-feedback-thread-modal.component.spec.ts @@ -16,11 +16,11 @@ * @fileoverview Unit tests for CreateFeedbackThreadModalComponent. */ -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { AlertsService } from 'services/alerts.service'; -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { CreateFeedbackThreadModalComponent } from './create-feedback-thread-modal.component'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {AlertsService} from 'services/alerts.service'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {CreateFeedbackThreadModalComponent} from './create-feedback-thread-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; class MockActiveModal { close(): void { @@ -32,7 +32,7 @@ class MockActiveModal { } } -describe('Create Feedback Thread Modal Controller', function() { +describe('Create Feedback Thread Modal Controller', function () { let component: CreateFeedbackThreadModalComponent; let fixture: ComponentFixture; let alertsService: AlertsService; @@ -40,16 +40,14 @@ describe('Create Feedback Thread Modal Controller', function() { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - CreateFeedbackThreadModalComponent - ], + declarations: [CreateFeedbackThreadModalComponent], providers: [ { provide: NgbActiveModal, - useClass: MockActiveModal - } + useClass: MockActiveModal, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -63,13 +61,12 @@ describe('Create Feedback Thread Modal Controller', function() { fixture.detectChanges(); }); - it('should initialize properties after component is initialized', - function() { - expect(component.newThreadSubject).toEqual(''); - expect(component.newThreadText).toEqual(''); - }); + it('should initialize properties after component is initialized', function () { + expect(component.newThreadSubject).toEqual(''); + expect(component.newThreadText).toEqual(''); + }); - it('should not close modal when new thread subject is empty', function() { + it('should not close modal when new thread subject is empty', function () { spyOn(alertsService, 'addWarning').and.callThrough(); spyOn(ngbActiveModal, 'close').and.callThrough(); @@ -78,11 +75,12 @@ describe('Create Feedback Thread Modal Controller', function() { component.create(newThreadSubject, newThreadText); expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Please specify a thread subject.'); + 'Please specify a thread subject.' + ); expect(ngbActiveModal.close).not.toHaveBeenCalled(); }); - it('should not close modal when new thread text is empty', function() { + it('should not close modal when new thread text is empty', function () { spyOn(alertsService, 'addWarning').and.callThrough(); spyOn(ngbActiveModal, 'close').and.callThrough(); @@ -91,21 +89,25 @@ describe('Create Feedback Thread Modal Controller', function() { component.create(newThreadSubject, newThreadText); expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Please specify a message.'); + 'Please specify a message.' + ); expect(ngbActiveModal.close).not.toHaveBeenCalled(); }); - it('should close modal when both new thread subject and new thread text are' + - ' valid', function() { - spyOn(ngbActiveModal, 'close').and.callThrough(); - - let newThreadSubject = 'subject'; - let newThreadText = 'text'; - component.create(newThreadSubject, newThreadText); - - expect(ngbActiveModal.close).toHaveBeenCalledWith({ - newThreadSubject: 'subject', - newThreadText: 'text' - }); - }); + it( + 'should close modal when both new thread subject and new thread text are' + + ' valid', + function () { + spyOn(ngbActiveModal, 'close').and.callThrough(); + + let newThreadSubject = 'subject'; + let newThreadText = 'text'; + component.create(newThreadSubject, newThreadText); + + expect(ngbActiveModal.close).toHaveBeenCalledWith({ + newThreadSubject: 'subject', + newThreadText: 'text', + }); + } + ); }); diff --git a/core/templates/pages/exploration-editor-page/feedback-tab/templates/create-feedback-thread-modal.component.ts b/core/templates/pages/exploration-editor-page/feedback-tab/templates/create-feedback-thread-modal.component.ts index 7cbd4689bdef..6f3f337a8a32 100644 --- a/core/templates/pages/exploration-editor-page/feedback-tab/templates/create-feedback-thread-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/feedback-tab/templates/create-feedback-thread-modal.component.ts @@ -16,35 +16,34 @@ * @fileoverview Component for create feedback thread modal. */ -import { Component, OnInit } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { AlertsService } from 'services/alerts.service'; +import {Component, OnInit} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {AlertsService} from 'services/alerts.service'; @Component({ selector: 'oppia-create-feedback-thread-modal', - templateUrl: './create-feedback-thread-modal.component.html' + templateUrl: './create-feedback-thread-modal.component.html', }) - export class CreateFeedbackThreadModalComponent - extends ConfirmOrCancelModal implements OnInit { + extends ConfirmOrCancelModal + implements OnInit +{ constructor( - private ngbActiveModal: NgbActiveModal, - private alertsService: AlertsService + private ngbActiveModal: NgbActiveModal, + private alertsService: AlertsService ) { super(ngbActiveModal); } - ngOnInit(): void { - } + ngOnInit(): void {} newThreadSubject = ''; newThreadText = ''; create(newThreadSubject: string, newThreadText: string): void { if (!newThreadSubject) { - this.alertsService.addWarning( - 'Please specify a thread subject.'); + this.alertsService.addWarning('Please specify a thread subject.'); return; } if (!newThreadText) { @@ -53,7 +52,7 @@ export class CreateFeedbackThreadModalComponent } this.ngbActiveModal.close({ newThreadSubject: newThreadSubject, - newThreadText: newThreadText + newThreadText: newThreadText, }); } } diff --git a/core/templates/pages/exploration-editor-page/feedback-tab/thread-table/thread-table.component.spec.ts b/core/templates/pages/exploration-editor-page/feedback-tab/thread-table/thread-table.component.spec.ts index 0976b2d08b34..005ccd8ccf5f 100644 --- a/core/templates/pages/exploration-editor-page/feedback-tab/thread-table/thread-table.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/feedback-tab/thread-table/thread-table.component.spec.ts @@ -16,11 +16,11 @@ * @fileoverview Unit tests for threadTableComponent. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { TruncatePipe } from 'filters/string-utility-filters/truncate.pipe'; -import { DateTimeFormatService } from 'services/date-time-format.service'; -import { ThreadStatusDisplayService } from '../services/thread-status-display.service'; -import { ThreadTableComponent } from './thread-table.component'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {TruncatePipe} from 'filters/string-utility-filters/truncate.pipe'; +import {DateTimeFormatService} from 'services/date-time-format.service'; +import {ThreadStatusDisplayService} from '../services/thread-status-display.service'; +import {ThreadTableComponent} from './thread-table.component'; export class MockDateTimeFormatService { getLocaleAbbreviatedDatetimeString(): string { @@ -38,15 +38,15 @@ describe('Thread table component', () => { providers: [ { provide: DateTimeFormatService, - useClass: MockDateTimeFormatService + useClass: MockDateTimeFormatService, }, - ThreadStatusDisplayService - ]}).compileComponents(); + ThreadStatusDisplayService, + ], + }).compileComponents(); })); beforeEach(() => { - fixture = - TestBed.createComponent(ThreadTableComponent); + fixture = TestBed.createComponent(ThreadTableComponent); component = fixture.componentInstance; fixture.detectChanges(); }); @@ -65,21 +65,23 @@ describe('Thread table component', () => { expect(component.getHumanReadableStatus('open')).toBe('Open'); expect(component.getHumanReadableStatus('compliment')).toBe('Compliment'); expect(component.getHumanReadableStatus('not_actionable')).toBe( - 'Not Actionable'); + 'Not Actionable' + ); }); - it('should get formatted date string from the timestamp in milliseconds', - () => { - let NOW_MILLIS = 1416563100000; - expect(component.getLocaleAbbreviatedDateTimeString(NOW_MILLIS)).toBe( - '11/21/2014'); - }); + it('should get formatted date string from the timestamp in milliseconds', () => { + let NOW_MILLIS = 1416563100000; + expect(component.getLocaleAbbreviatedDateTimeString(NOW_MILLIS)).toBe( + '11/21/2014' + ); + }); it('should emit rowClick event when onRowClick is called', () => { spyOn(fixture.componentInstance.rowClick, 'emit'); let threadId = 'testId'; component.onRowClick(threadId); - expect(fixture.componentInstance.rowClick.emit) - .toHaveBeenCalledWith(threadId); + expect(fixture.componentInstance.rowClick.emit).toHaveBeenCalledWith( + threadId + ); }); }); diff --git a/core/templates/pages/exploration-editor-page/feedback-tab/thread-table/thread-table.component.ts b/core/templates/pages/exploration-editor-page/feedback-tab/thread-table/thread-table.component.ts index f507c89f7337..4ef12089e8e5 100644 --- a/core/templates/pages/exploration-editor-page/feedback-tab/thread-table/thread-table.component.ts +++ b/core/templates/pages/exploration-editor-page/feedback-tab/thread-table/thread-table.component.ts @@ -17,25 +17,24 @@ * tab of the exploration editor. */ -import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { SuggestionThread } from 'domain/suggestion/suggestion-thread-object.model'; -import { DateTimeFormatService } from 'services/date-time-format.service'; -import { ThreadStatusDisplayService } from '../services/thread-status-display.service'; +import {Component, EventEmitter, Input, Output} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {SuggestionThread} from 'domain/suggestion/suggestion-thread-object.model'; +import {DateTimeFormatService} from 'services/date-time-format.service'; +import {ThreadStatusDisplayService} from '../services/thread-status-display.service'; @Component({ selector: 'oppia-thread-table', - templateUrl: './thread-table.component.html' + templateUrl: './thread-table.component.html', }) export class ThreadTableComponent { - @Output() rowClick: EventEmitter = ( - new EventEmitter()); + @Output() rowClick: EventEmitter = new EventEmitter(); @Input() threads: SuggestionThread[] = []; constructor( private dateTimeFormatService: DateTimeFormatService, private threadStatusDisplayService: ThreadStatusDisplayService - ) { } + ) {} onRowClick(threadId: string): void { this.rowClick.emit(threadId); @@ -50,11 +49,15 @@ export class ThreadTableComponent { } getLocaleAbbreviatedDateTimeString(millisSinceEpoch: number): string { - return this.dateTimeFormatService - .getLocaleAbbreviatedDatetimeString(millisSinceEpoch); + return this.dateTimeFormatService.getLocaleAbbreviatedDatetimeString( + millisSinceEpoch + ); } } -angular.module('oppia').directive( - 'oppiaThreadTable', downgradeComponent( - {component: ThreadTableComponent})); +angular + .module('oppia') + .directive( + 'oppiaThreadTable', + downgradeComponent({component: ThreadTableComponent}) + ); diff --git a/core/templates/pages/exploration-editor-page/history-tab/history-tab.component.spec.ts b/core/templates/pages/exploration-editor-page/history-tab/history-tab.component.spec.ts index 5a0bdac41446..8bae86e4bd3e 100644 --- a/core/templates/pages/exploration-editor-page/history-tab/history-tab.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/history-tab/history-tab.component.spec.ts @@ -16,18 +16,24 @@ * @fileoverview Unit tests for the exploration history tab. */ -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { EditabilityService } from 'services/editability.service'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { DateTimeFormatService } from 'services/date-time-format.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { HistoryTabComponent } from './history-tab.component'; -import { HistoryTabBackendApiService } from '../services/history-tab-backend-api.service'; -import { CompareVersionsService } from './services/compare-versions.service'; -import { ExplorationDataService } from '../services/exploration-data.service'; -import { RouterService } from '../services/router.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import {EditabilityService} from 'services/editability.service'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {DateTimeFormatService} from 'services/date-time-format.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {HistoryTabComponent} from './history-tab.component'; +import {HistoryTabBackendApiService} from '../services/history-tab-backend-api.service'; +import {CompareVersionsService} from './services/compare-versions.service'; +import {ExplorationDataService} from '../services/exploration-data.service'; +import {RouterService} from '../services/router.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; class MockNgbModalRef { componentInstance: { @@ -53,56 +59,58 @@ describe('History tab component', () => { let mockRefreshVersionHistoryEmitter = new EventEmitter(); let explorationId = 'exp1'; - let snapshots = [{ - commit_message: 'This is the commit message', - committer_id: 'committer_3', - commit_type: '', - version_number: 1, - created_on_ms: 1416563100000, - commit_cmds: [] - }, { - commit_message: 'This is the commit message 2', - committer_id: 'committer_3', - commit_type: '', - version_number: 2, - created_on_ms: 1416563100000, - commit_cmds: [] - }]; + let snapshots = [ + { + commit_message: 'This is the commit message', + committer_id: 'committer_3', + commit_type: '', + version_number: 1, + created_on_ms: 1416563100000, + commit_cmds: [], + }, + { + commit_message: 'This is the commit message 2', + committer_id: 'committer_3', + commit_type: '', + version_number: 2, + created_on_ms: 1416563100000, + commit_cmds: [], + }, + ]; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - HistoryTabComponent - ], + declarations: [HistoryTabComponent], providers: [ { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, { provide: ExplorationDataService, useValue: { explorationId: explorationId, data: { - version: 2 - }, - getDataAsync: () => Promise.resolve({ version: 2, - }) - } + }, + getDataAsync: () => + Promise.resolve({ + version: 2, + }), + }, }, { provide: RouterService, useValue: { onRefreshVersionHistory: mockRefreshVersionHistoryEmitter, getActiveTabName() { - return ('main'); - } - } - } + return 'main'; + }, + }, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -117,8 +125,9 @@ describe('History tab component', () => { compareVersionsService = TestBed.inject(CompareVersionsService); historyTabBackendApiService = TestBed.inject(HistoryTabBackendApiService); - spyOn(dateTimeFormatService, 'getLocaleDateTimeHourString') - .and.returnValue('11/21/2014'); + spyOn(dateTimeFormatService, 'getLocaleDateTimeHourString').and.returnValue( + '11/21/2014' + ); fixture.detectChanges(); component.diffData = { @@ -127,7 +136,7 @@ describe('History tab component', () => { }; component.compareVersionMetadata = { earlierVersion: 2, - laterVersion: 3 + laterVersion: 3, }; component.selectedVersionsArray = [1, 4]; component.ngOnInit(); @@ -137,43 +146,47 @@ describe('History tab component', () => { component.ngOnDestroy(); }); - it('should initialize controller properties after its initialization', - () => { - expect(component.explorationId).toBe(explorationId); - expect(component.explorationAllSnapshotsUrl).toBe( - '/createhandler/snapshots/exp1'); - expect(component.revertExplorationUrl).toBe('/createhandler/revert/exp1'); - expect(component.explorationDownloadUrl) - .toBe('/createhandler/download/exp1'); - - expect(component.explorationVersionMetadata).toBe(null); - expect(component.versionCheckboxArray).toEqual([]); - expect(component.displayedCurrentPageNumber).toBe(1); - expect(component.versionNumbersToDisplay).toEqual([]); - }); + it('should initialize controller properties after its initialization', () => { + expect(component.explorationId).toBe(explorationId); + expect(component.explorationAllSnapshotsUrl).toBe( + '/createhandler/snapshots/exp1' + ); + expect(component.revertExplorationUrl).toBe('/createhandler/revert/exp1'); + expect(component.explorationDownloadUrl).toBe( + '/createhandler/download/exp1' + ); - it('should refresh version history when refreshVersionHistory flag is' + - ' broadcasted and force refresh is true', fakeAsync(() => { - spyOn( - historyTabBackendApiService, 'getData') - .and.returnValue(Promise.resolve({ - summaries: [], - snapshots: snapshots - })); + expect(component.explorationVersionMetadata).toBe(null); + expect(component.versionCheckboxArray).toEqual([]); + expect(component.displayedCurrentPageNumber).toBe(1); + expect(component.versionNumbersToDisplay).toEqual([]); + }); - component.refreshVersionHistory(); + it( + 'should refresh version history when refreshVersionHistory flag is' + + ' broadcasted and force refresh is true', + fakeAsync(() => { + spyOn(historyTabBackendApiService, 'getData').and.returnValue( + Promise.resolve({ + summaries: [], + snapshots: snapshots, + }) + ); - let data = { - forceRefresh: true - }; + component.refreshVersionHistory(); - mockRefreshVersionHistoryEmitter.emit(data); - tick(); + let data = { + forceRefresh: true, + }; - expect(component.currentVersion).toBe(2); - expect(component.hideHistoryGraph).toBe(true); - expect(component.comparisonsAreDisabled).toBe(false); - })); + mockRefreshVersionHistoryEmitter.emit(data); + tick(); + + expect(component.currentVersion).toBe(2); + expect(component.hideHistoryGraph).toBe(true); + expect(component.comparisonsAreDisabled).toBe(false); + }) + ); it('should compare selected versions successfully', fakeAsync(() => { component.selectedVersionsArray = [3, 4, 5, 6]; @@ -203,34 +216,41 @@ describe('History tab component', () => { commit_cmds: null, commit_type: null, commit_message: 'message', - } + }, ]; - spyOn( - historyTabBackendApiService, 'getData') - .and.returnValue(Promise.resolve({ + spyOn(historyTabBackendApiService, 'getData').and.returnValue( + Promise.resolve({ summaries: [], - snapshots: snapshots - })); + snapshots: snapshots, + }) + ); component.refreshVersionHistory(); - component.changeSelectedVersions({ - versionNumber: 1, - committerId: 'committer_3', - createdOnMsecsStr: '11/21/2014', - commitMessage: 'This is the commit message', - }, 1); + component.changeSelectedVersions( + { + versionNumber: 1, + committerId: 'committer_3', + createdOnMsecsStr: '11/21/2014', + commitMessage: 'This is the commit message', + }, + 1 + ); - component.changeSelectedVersions({ - versionNumber: 2, - committerId: 'committer_3', - createdOnMsecsStr: '11/21/2014', - commitMessage: 'This is the commit message', - }, 2); + component.changeSelectedVersions( + { + versionNumber: 2, + committerId: 'committer_3', + createdOnMsecsStr: '11/21/2014', + commitMessage: 'This is the commit message', + }, + 2 + ); spyOn(compareVersionsService, 'getDiffGraphData').and.returnValue( - Promise.resolve(null)); + Promise.resolve(null) + ); component.compareSelectedVersions(); component.changeCompareVersion(); @@ -238,22 +258,20 @@ describe('History tab component', () => { tick(); expect(component.hideHistoryGraph).toBe(true); - expect(component.diffData).toEqual({ v1Metadata: null, v2Metadata: null }); + expect(component.diffData).toEqual({v1Metadata: null, v2Metadata: null}); - expect(component.earlierVersionHeader).toBe( - undefined); - expect(component.laterVersionHeader).toBe( - undefined); + expect(component.earlierVersionHeader).toBe(undefined); + expect(component.laterVersionHeader).toBe(undefined); })); it('should show exploration metadata diff modal', () => { spyOn(component, 'changeItemsPerPage').and.stub(); - spyOn( - historyTabBackendApiService, 'getData') - .and.returnValue(Promise.resolve({ + spyOn(historyTabBackendApiService, 'getData').and.returnValue( + Promise.resolve({ summaries: [], - snapshots: snapshots - })); + snapshots: snapshots, + }) + ); component.VERSIONS_PER_PAGE = 2; component.paginator({ @@ -264,22 +282,29 @@ describe('History tab component', () => { }); component.refreshVersionHistory(); - component.changeSelectedVersions({ - committerId: 'committer_3', - createdOnMsecsStr: '11/21/2014', - commitMessage: 'This is the commit message', - versionNumber: 1 - }, 1); + component.changeSelectedVersions( + { + committerId: 'committer_3', + createdOnMsecsStr: '11/21/2014', + commitMessage: 'This is the commit message', + versionNumber: 1, + }, + 1 + ); - component.changeSelectedVersions({ - committerId: 'committer_3', - createdOnMsecsStr: '11/21/2014', - commitMessage: 'This is the commit message', - versionNumber: 2 - }, 2); + component.changeSelectedVersions( + { + committerId: 'committer_3', + createdOnMsecsStr: '11/21/2014', + commitMessage: 'This is the commit message', + versionNumber: 2, + }, + 2 + ); spyOn(compareVersionsService, 'getDiffGraphData').and.returnValue( - Promise.resolve(null)); + Promise.resolve(null) + ); component.compareSelectedVersions(); component.changeCompareVersion(); @@ -290,7 +315,7 @@ describe('History tab component', () => { newMetadata: null, headers: null, }, - result: Promise.resolve() + result: Promise.resolve(), } as NgbModalRef; }); @@ -305,114 +330,102 @@ describe('History tab component', () => { it('should open a new tab for download exploration with version', () => { spyOnProperty(windowRef, 'nativeWindow').and.returnValue({ - open: jasmine.createSpy('open', () => {}) + open: jasmine.createSpy('open', () => {}), }); component.downloadExplorationWithVersion(1); expect(windowRef.nativeWindow.open).toHaveBeenCalledWith( - '/createhandler/download/exp1?v=1', '&output_format=zip'); + '/createhandler/download/exp1?v=1', + '&output_format=zip' + ); }); it('should open check revert exploration modal', () => { - spyOn(ngbModal, 'open').and.returnValue( - { - componentInstance: new MockNgbModalRef(), - result: Promise.resolve() - } as NgbModalRef - ); + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: new MockNgbModalRef(), + result: Promise.resolve(), + } as NgbModalRef); component.showCheckRevertExplorationModal(1); expect(ngbModal.open).toHaveBeenCalled(); }); - it('should not open revert exploration model when exploration is invalid', - fakeAsync(() => { - spyOn(ngbModal, 'open').and.returnValue( - { - componentInstance: new MockNgbModalRef(), - result: Promise.resolve(1), - close: () => {} - } as NgbModalRef - ); - spyOn(component, 'showRevertExplorationModal'); - const historyBackendCall = spyOn( - historyTabBackendApiService, 'getCheckRevertValidData' - ).and.returnValue(Promise.resolve({valid: false, details: 'details'})); - - component.showCheckRevertExplorationModal(1); - tick(); + it('should not open revert exploration model when exploration is invalid', fakeAsync(() => { + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: new MockNgbModalRef(), + result: Promise.resolve(1), + close: () => {}, + } as NgbModalRef); + spyOn(component, 'showRevertExplorationModal'); + const historyBackendCall = spyOn( + historyTabBackendApiService, + 'getCheckRevertValidData' + ).and.returnValue(Promise.resolve({valid: false, details: 'details'})); + component.showCheckRevertExplorationModal(1); + tick(); - expect(historyBackendCall).toHaveBeenCalled(); - expect(component.showRevertExplorationModal).not.toHaveBeenCalled(); - })); + expect(historyBackendCall).toHaveBeenCalled(); + expect(component.showRevertExplorationModal).not.toHaveBeenCalled(); + })); - it('should open revert exploration modal when exploration is valid', - fakeAsync(() => { - spyOn(ngbModal, 'open').and.returnValue( - { - componentInstance: new MockNgbModalRef(), - result: Promise.resolve(1), - close: () => {} - } as NgbModalRef - ); - spyOn(component, 'showRevertExplorationModal'); - const historyBackendCall = spyOn( - historyTabBackendApiService, 'getCheckRevertValidData' - ).and.returnValue(Promise.resolve({valid: true, details: null})); + it('should open revert exploration modal when exploration is valid', fakeAsync(() => { + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: new MockNgbModalRef(), + result: Promise.resolve(1), + close: () => {}, + } as NgbModalRef); + spyOn(component, 'showRevertExplorationModal'); + const historyBackendCall = spyOn( + historyTabBackendApiService, + 'getCheckRevertValidData' + ).and.returnValue(Promise.resolve({valid: true, details: null})); - component.showCheckRevertExplorationModal(1); - tick(); + component.showCheckRevertExplorationModal(1); + tick(); + expect(historyBackendCall).toHaveBeenCalled(); + expect(component.showRevertExplorationModal).toHaveBeenCalled(); + })); - expect(historyBackendCall).toHaveBeenCalled(); - expect(component.showRevertExplorationModal).toHaveBeenCalled(); - })); + it('should reload page when closing revert exploration modal', fakeAsync(() => { + spyOnProperty(windowRef, 'nativeWindow').and.returnValue({ + location: { + reload: jasmine.createSpy('reload', () => {}), + }, + }); + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: new MockNgbModalRef(), + result: Promise.resolve(1), + } as NgbModalRef); - it('should reload page when closing revert exploration modal', - fakeAsync(() => { - spyOnProperty(windowRef, 'nativeWindow').and.returnValue({ - location: { - reload: jasmine.createSpy('reload', () => {}) - } - }); - spyOn(ngbModal, 'open').and.returnValue( - { - componentInstance: new MockNgbModalRef(), - result: Promise.resolve(1) - } as NgbModalRef - ); + let spyObj = spyOn(historyTabBackendApiService, 'postData').and.returnValue( + Promise.resolve(null) + ); - let spyObj = spyOn( - historyTabBackendApiService, 'postData' - ).and.returnValue(Promise.resolve(null)); + component.showRevertExplorationModal(1); + tick(); - component.showRevertExplorationModal(1); - tick(); + expect(spyObj).toHaveBeenCalled(); + expect(windowRef.nativeWindow.location.reload).toHaveBeenCalled(); + })); - expect(spyObj).toHaveBeenCalled(); - expect(windowRef.nativeWindow.location.reload).toHaveBeenCalled(); - })); - - it('should not reload page when dismissing revert exploration modal', - () => { - spyOnProperty(windowRef, 'nativeWindow').and.returnValue({ - location: { - reload: jasmine.createSpy('reload', () => {}) - } - }); - spyOn(ngbModal, 'open').and.returnValue( - { - componentInstance: new MockNgbModalRef(), - result: Promise.reject() - } as NgbModalRef - ); + it('should not reload page when dismissing revert exploration modal', () => { + spyOnProperty(windowRef, 'nativeWindow').and.returnValue({ + location: { + reload: jasmine.createSpy('reload', () => {}), + }, + }); + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: new MockNgbModalRef(), + result: Promise.reject(), + } as NgbModalRef); - component.showRevertExplorationModal(1); + component.showRevertExplorationModal(1); - expect(windowRef.nativeWindow.location.reload).not.toHaveBeenCalled(); - }); + expect(windowRef.nativeWindow.location.reload).not.toHaveBeenCalled(); + }); it('should return if the content is editable', () => { spyOn(editabilityService, 'isEditable').and.returnValue(false); @@ -420,28 +433,32 @@ describe('History tab component', () => { }); it('should filter the history by username', () => { - let snapshots = [{ - commit_message: 'This is the commit message', - committerId: 'committer_3', - commit_type: '', - version_number: 1, - created_on_ms: 1416563100000, - commit_cmds: [] - }, { - commit_message: 'This is the commit message 2', - committerId: 'committer_3', - commit_type: '', - version_number: 2, - created_on_ms: 1416563100000, - commit_cmds: [] - }, { - commit_message: 'This is the commit message 2', - committerId: 'committer_1', - commit_type: '', - version_number: 2, - created_on_ms: 1416563100000, - commit_cmds: [] - }]; + let snapshots = [ + { + commit_message: 'This is the commit message', + committerId: 'committer_3', + commit_type: '', + version_number: 1, + created_on_ms: 1416563100000, + commit_cmds: [], + }, + { + commit_message: 'This is the commit message 2', + committerId: 'committer_3', + commit_type: '', + version_number: 2, + created_on_ms: 1416563100000, + commit_cmds: [], + }, + { + commit_message: 'This is the commit message 2', + committerId: 'committer_1', + commit_type: '', + version_number: 2, + created_on_ms: 1416563100000, + commit_cmds: [], + }, + ]; component.totalExplorationVersionMetadata = snapshots; component.username = ''; component.filterByUsername(); @@ -449,8 +466,10 @@ describe('History tab component', () => { component.username = 'committer_3'; component.filterByUsername(); - expect(component.explorationVersionMetadata).toEqual( - [snapshots[0], snapshots[1]]); + expect(component.explorationVersionMetadata).toEqual([ + snapshots[0], + snapshots[1], + ]); component.username = 'committer_1'; component.filterByUsername(); @@ -464,28 +483,32 @@ describe('History tab component', () => { }); it('should reverse the array when the date filter is applied', () => { - let snapshots = [{ - commit_message: 'This is the commit message', - committerId: 'committer_3', - commit_type: '', - version_number: 1, - created_on_ms: 1416563100000, - commit_cmds: [] - }, { - commit_message: 'This is the commit message 2', - committerId: 'committer_3', - commit_type: '', - version_number: 2, - created_on_ms: 1416563100000, - commit_cmds: [] - }, { - commit_message: 'This is the commit message 2', - committerId: 'committer_1', - commit_type: '', - version_number: 3, - created_on_ms: 1416563100000, - commit_cmds: [] - }]; + let snapshots = [ + { + commit_message: 'This is the commit message', + committerId: 'committer_3', + commit_type: '', + version_number: 1, + created_on_ms: 1416563100000, + commit_cmds: [], + }, + { + commit_message: 'This is the commit message 2', + committerId: 'committer_3', + commit_type: '', + version_number: 2, + created_on_ms: 1416563100000, + commit_cmds: [], + }, + { + commit_message: 'This is the commit message 2', + committerId: 'committer_1', + commit_type: '', + version_number: 3, + created_on_ms: 1416563100000, + commit_cmds: [], + }, + ]; component.explorationVersionMetadata = snapshots; component.reverseDateOrder(); expect(component.explorationVersionMetadata[0].version_number).toEqual(3); @@ -499,7 +522,8 @@ describe('History tab component', () => { it('should find the versions to compare', fakeAsync(() => { spyOn(component, 'getVersionHeader').and.stub(); spyOn(compareVersionsService, 'getDiffGraphData').and.returnValue( - Promise.resolve(null)); + Promise.resolve(null) + ); component.selectedVersionsArray = [1, 4]; component.compareVersionMetadata = {}; @@ -508,46 +532,56 @@ describe('History tab component', () => { committerId: '1', createdOnMsecsStr: 10, commitMessage: 'commit message 1', - versionNumber: 1 - }, { + versionNumber: 1, + }, + { committerId: '2', createdOnMsecsStr: 10, commitMessage: 'commit message 2', - versionNumber: 2 - }, { + versionNumber: 2, + }, + { committerId: '3', createdOnMsecsStr: 10, commitMessage: 'commit message 3', - versionNumber: 3 - }, { + versionNumber: 3, + }, + { committerId: '4', createdOnMsecsStr: 10, commitMessage: 'commit message 4', - versionNumber: 4 - }]; + versionNumber: 4, + }, + ]; component.changeCompareVersion(); tick(); expect(component.compareVersionMetadata.earlierVersion).toEqual( - component.totalExplorationVersionMetadata[0]); + component.totalExplorationVersionMetadata[0] + ); expect(component.compareVersionMetadata.laterVersion).toEqual( - component.totalExplorationVersionMetadata[3]); + component.totalExplorationVersionMetadata[3] + ); component.selectedVersionsArray = [2, 4]; component.changeCompareVersion(); tick(); expect(component.compareVersionMetadata.earlierVersion).toEqual( - component.totalExplorationVersionMetadata[1]); + component.totalExplorationVersionMetadata[1] + ); expect(component.compareVersionMetadata.laterVersion).toEqual( - component.totalExplorationVersionMetadata[3]); + component.totalExplorationVersionMetadata[3] + ); component.selectedVersionsArray = [2, 3]; component.changeCompareVersion(); tick(); expect(component.compareVersionMetadata.earlierVersion).toEqual( - component.totalExplorationVersionMetadata[1]); + component.totalExplorationVersionMetadata[1] + ); expect(component.compareVersionMetadata.laterVersion).toEqual( - component.totalExplorationVersionMetadata[2]); + component.totalExplorationVersionMetadata[2] + ); })); }); diff --git a/core/templates/pages/exploration-editor-page/history-tab/history-tab.component.ts b/core/templates/pages/exploration-editor-page/history-tab/history-tab.component.ts index 42fbf2985eeb..73deec086135 100644 --- a/core/templates/pages/exploration-editor-page/history-tab/history-tab.component.ts +++ b/core/templates/pages/exploration-editor-page/history-tab/history-tab.component.ts @@ -16,27 +16,30 @@ * @fileoverview Component for the exploration history tab. */ -import { Component, OnInit, OnDestroy, ChangeDetectorRef } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { Subscription } from 'rxjs'; +import {Component, OnInit, OnDestroy, ChangeDetectorRef} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {Subscription} from 'rxjs'; import cloneDeep from 'lodash/cloneDeep'; -import { CheckRevertExplorationModalComponent } from './modal-templates/check-revert-exploration-modal.component'; -import { RevertExplorationModalComponent } from './modal-templates/revert-exploration-modal.component'; -import { ExplorationMetadataDiffModalComponent } from '../modal-templates/exploration-metadata-diff-modal.component'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { DateTimeFormatService } from 'services/date-time-format.service'; -import { EditabilityService } from 'services/editability.service'; -import { LoaderService } from 'services/loader.service'; -import { ExplorationDataService } from '../services/exploration-data.service'; -import { HistoryTabBackendApiService } from '../services/history-tab-backend-api.service'; -import { RouterService } from '../services/router.service'; -import { CheckRevertService } from './services/check-revert.service'; -import { ExplorationSnapshot, VersionTreeService } from './services/version-tree.service'; -import { CompareVersionsService } from './services/compare-versions.service'; -import { ExplorationMetadata } from 'domain/exploration/ExplorationMetadataObjectFactory'; -import { LoggerService } from 'services/contextual/logger.service'; +import {CheckRevertExplorationModalComponent} from './modal-templates/check-revert-exploration-modal.component'; +import {RevertExplorationModalComponent} from './modal-templates/revert-exploration-modal.component'; +import {ExplorationMetadataDiffModalComponent} from '../modal-templates/exploration-metadata-diff-modal.component'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {DateTimeFormatService} from 'services/date-time-format.service'; +import {EditabilityService} from 'services/editability.service'; +import {LoaderService} from 'services/loader.service'; +import {ExplorationDataService} from '../services/exploration-data.service'; +import {HistoryTabBackendApiService} from '../services/history-tab-backend-api.service'; +import {RouterService} from '../services/router.service'; +import {CheckRevertService} from './services/check-revert.service'; +import { + ExplorationSnapshot, + VersionTreeService, +} from './services/version-tree.service'; +import {CompareVersionsService} from './services/compare-versions.service'; +import {ExplorationMetadata} from 'domain/exploration/ExplorationMetadataObjectFactory'; +import {LoggerService} from 'services/contextual/logger.service'; interface VersionMetadata { versionNumber: number; @@ -54,8 +57,7 @@ interface Metadata { selector: 'oppia-history-tab', templateUrl: './history-tab.component.html', }) -export class HistoryTabComponent - implements OnInit, OnDestroy { +export class HistoryTabComponent implements OnInit, OnDestroy { directiveSubscriptions = new Subscription(); firstVersion: string; @@ -68,10 +70,12 @@ export class HistoryTabComponent explorationSnapshots: ExplorationSnapshot[]; currentPage: number = 0; explorationVersionMetadata; - versionCheckboxArray: { - vnum: number; - selected: boolean; - }[] | null = []; + versionCheckboxArray: + | { + vnum: number; + selected: boolean; + }[] + | null = []; username: string; displayedCurrentPageNumber: number; @@ -109,7 +113,7 @@ export class HistoryTabComponent private versionTreeService: VersionTreeService, private windowRef: WindowRef, private loggerService: LoggerService, - private changeDetectorRef: ChangeDetectorRef, + private changeDetectorRef: ChangeDetectorRef ) {} // Compares the two selected versions and displays the comparison @@ -131,20 +135,26 @@ export class HistoryTabComponent // Changes the checkbox selection and provides an appropriate user // prompt. changeSelectedVersions( - snapshot: { - versionNumber: number; - committerId: string; - createdOnMsecsStr: string; - commitMessage: string; - }, - item: number + snapshot: { + versionNumber: number; + committerId: string; + createdOnMsecsStr: string; + commitMessage: string; + }, + item: number ): void { - if (item === 1 && snapshot && !this.selectedVersionsArray.includes( - snapshot.versionNumber)) { + if ( + item === 1 && + snapshot && + !this.selectedVersionsArray.includes(snapshot.versionNumber) + ) { this.selectedVersionsArray[0] = snapshot.versionNumber; } - if (item === 2 && snapshot && !this.selectedVersionsArray.includes( - snapshot.versionNumber)) { + if ( + item === 2 && + snapshot && + !this.selectedVersionsArray.includes(snapshot.versionNumber) + ) { this.selectedVersionsArray[1] = snapshot.versionNumber; } if (this.selectedVersionsArray && this.selectedVersionsArray.length === 2) { @@ -160,30 +170,32 @@ export class HistoryTabComponent // Refreshes the displayed version history log. refreshVersionHistory(): void { this.loaderService.showLoadingScreen('Loading'); - this.explorationDataService.getDataAsync(() => {}).then((data) => { - let currentVersion = data.version; - this.currentVersion = currentVersion; - // The this.compareVersionMetadata is an object with keys - // 'earlierVersion' and 'laterVersion' whose values are the - // metadata of the compared versions, containing 'committerId', - // 'createdOnMsecs', 'commitMessage', and 'versionNumber'. - this.compareVersions = {}; - this.compareVersionMetadata = {}; - - // Contains the IDs of the versions selected for comparison. - // Should contain a maximum of two elements. - this.selectedVersionsArray = []; - - this.hideHistoryGraph = true; - - // Disable all comparisons if there are less than two revisions in - // total. - this.comparisonsAreDisabled = (currentVersion < 2); - this.compareVersionsButtonIsHidden = this.comparisonsAreDisabled; - - this.historyTabBackendApiService - .getData(this.explorationAllSnapshotsUrl).then( - (response) => { + this.explorationDataService + .getDataAsync(() => {}) + .then(data => { + let currentVersion = data.version; + this.currentVersion = currentVersion; + // The this.compareVersionMetadata is an object with keys + // 'earlierVersion' and 'laterVersion' whose values are the + // metadata of the compared versions, containing 'committerId', + // 'createdOnMsecs', 'commitMessage', and 'versionNumber'. + this.compareVersions = {}; + this.compareVersionMetadata = {}; + + // Contains the IDs of the versions selected for comparison. + // Should contain a maximum of two elements. + this.selectedVersionsArray = []; + + this.hideHistoryGraph = true; + + // Disable all comparisons if there are less than two revisions in + // total. + this.comparisonsAreDisabled = currentVersion < 2; + this.compareVersionsButtonIsHidden = this.comparisonsAreDisabled; + + this.historyTabBackendApiService + .getData(this.explorationAllSnapshotsUrl) + .then(response => { this.explorationSnapshots = response.snapshots; this.versionTreeService.init(this.explorationSnapshots); // Re-populate versionCheckboxArray and @@ -191,59 +203,69 @@ export class HistoryTabComponent this.versionCheckboxArray = []; this.explorationVersionMetadata = []; let lowestVersionIndex = 0; - for ( - let i = currentVersion - 1; i >= lowestVersionIndex; i--) { + for (let i = currentVersion - 1; i >= lowestVersionIndex; i--) { let versionNumber = this.explorationSnapshots[i].version_number; this.explorationVersionMetadata[versionNumber - 1] = { committerId: this.explorationSnapshots[i].committer_id, - createdOnMsecsStr: ( - this.dateTimeFormatService - .getLocaleDateTimeHourString( - this.explorationSnapshots[i].created_on_ms)), + createdOnMsecsStr: + this.dateTimeFormatService.getLocaleDateTimeHourString( + this.explorationSnapshots[i].created_on_ms + ), tooltipText: this.dateTimeFormatService.getDateTimeInWords( - this.explorationSnapshots[i].created_on_ms), + this.explorationSnapshots[i].created_on_ms + ), commitMessage: this.explorationSnapshots[i].commit_message, - versionNumber: this.explorationSnapshots[i].version_number + versionNumber: this.explorationSnapshots[i].version_number, }; this.versionCheckboxArray.push({ vnum: this.explorationSnapshots[i].version_number, - selected: false + selected: false, }); } this.totalExplorationVersionMetadata = cloneDeep( - this.explorationVersionMetadata); + this.explorationVersionMetadata + ); this.totalExplorationVersionMetadata.reverse(); this.loaderService.hideLoadingScreen(); this.changeItemsPerPage(); - } - ); - }); + }); + }); } getVersionHeader(versionMetadata: VersionMetadata): string { return ( - 'Revision #' + versionMetadata.versionNumber + - ' by ' + versionMetadata.committerId + - ' (' + versionMetadata.createdOnMsecsStr + - ')' + ( - versionMetadata.commitMessage ? - ': ' + versionMetadata.commitMessage : '')); + 'Revision #' + + versionMetadata.versionNumber + + ' by ' + + versionMetadata.committerId + + ' (' + + versionMetadata.createdOnMsecsStr + + ')' + + (versionMetadata.commitMessage + ? ': ' + versionMetadata.commitMessage + : '') + ); } filterByUsername(): void { if (!this.username && this.totalExplorationVersionMetadata) { this.explorationVersionMetadata = cloneDeep( - this.totalExplorationVersionMetadata); + this.totalExplorationVersionMetadata + ); this.versionNumbersToDisplay = this.explorationVersionMetadata.length; return; } - this.explorationVersionMetadata = ( - this.totalExplorationVersionMetadata.filter((metadata) => { + this.explorationVersionMetadata = + this.totalExplorationVersionMetadata.filter(metadata => { return ( - metadata && metadata.committerId.trim().toLowerCase().includes( - this.username.trim().toLowerCase())); - })); + metadata && + metadata.committerId + .trim() + .toLowerCase() + .includes(this.username.trim().toLowerCase()) + ); + }); if (this.explorationVersionMetadata) { this.versionNumbersToDisplay = this.explorationVersionMetadata.length; } @@ -255,17 +277,26 @@ export class HistoryTabComponent this.diffData = null; let earlierComparedVersion = Math.min( - this.selectedVersionsArray[0], this.selectedVersionsArray[1]); + this.selectedVersionsArray[0], + this.selectedVersionsArray[1] + ); let laterComparedVersion = Math.max( - this.selectedVersionsArray[0], this.selectedVersionsArray[1]); - let earlierIndex = null, laterIndex = null; + this.selectedVersionsArray[0], + this.selectedVersionsArray[1] + ); + let earlierIndex = null, + laterIndex = null; for (let i = 0; i < this.totalExplorationVersionMetadata.length; i++) { - if (this.totalExplorationVersionMetadata[i].versionNumber === - earlierComparedVersion) { + if ( + this.totalExplorationVersionMetadata[i].versionNumber === + earlierComparedVersion + ) { earlierIndex = i; - } else if (this.totalExplorationVersionMetadata[i].versionNumber === - laterComparedVersion) { + } else if ( + this.totalExplorationVersionMetadata[i].versionNumber === + laterComparedVersion + ) { laterIndex = i; } if (earlierIndex !== null && laterIndex !== null) { @@ -273,24 +304,28 @@ export class HistoryTabComponent } } - this.compareVersionMetadata.earlierVersion = ( - this.totalExplorationVersionMetadata[earlierIndex]); - this.compareVersionMetadata.laterVersion = ( - this.totalExplorationVersionMetadata[laterIndex]); - - Promise.resolve(this.compareVersionsService.getDiffGraphData( - earlierComparedVersion, laterComparedVersion)).then( - (response) => { - this.loggerService.info('Retrieved version comparison data'); - this.loggerService.info(String(response)); - - this.diffData = response; - this.earlierVersionHeader = this.getVersionHeader( - this.compareVersionMetadata.earlierVersion); - this.laterVersionHeader = this.getVersionHeader( - this.compareVersionMetadata.laterVersion); - } - ); + this.compareVersionMetadata.earlierVersion = + this.totalExplorationVersionMetadata[earlierIndex]; + this.compareVersionMetadata.laterVersion = + this.totalExplorationVersionMetadata[laterIndex]; + + Promise.resolve( + this.compareVersionsService.getDiffGraphData( + earlierComparedVersion, + laterComparedVersion + ) + ).then(response => { + this.loggerService.info('Retrieved version comparison data'); + this.loggerService.info(String(response)); + + this.diffData = response; + this.earlierVersionHeader = this.getVersionHeader( + this.compareVersionMetadata.earlierVersion + ); + this.laterVersionHeader = this.getVersionHeader( + this.compareVersionMetadata.laterVersion + ); + }); } // Downloads the zip file for an exploration. @@ -300,7 +335,8 @@ export class HistoryTabComponent // triggered. this.windowRef.nativeWindow.open( this.explorationDownloadUrl + '?v=' + versionNumber, - '&output_format=zip'); + '&output_format=zip' + ); } showCheckRevertExplorationModal(version: number): void { @@ -309,13 +345,15 @@ export class HistoryTabComponent }); modalRef.componentInstance.version = version; const url = this.urlInterpolationService.interpolateUrl( - this.checkRevertExplorationValidUrl, { + this.checkRevertExplorationValidUrl, + { exploration_id: this.explorationId, - version: String(version) + version: String(version), } ); - this.historyTabBackendApiService.getCheckRevertValidData(url).then( - (revertData) => { + this.historyTabBackendApiService + .getCheckRevertValidData(url) + .then(revertData => { if (revertData.valid) { modalRef.close(); this.showRevertExplorationModal(version); @@ -330,26 +368,29 @@ export class HistoryTabComponent backdrop: true, }); modalRef.componentInstance.version = version; - modalRef.result.then((version) => { - let data = { - revertExplorationUrl: this.revertExplorationUrl, - currentVersion: this.explorationDataService.data.version, - revertToVersion: version - }; - this.historyTabBackendApiService.postData(data).then( - () => { - this.windowRef.nativeWindow.location.reload(); - }, - () => { - // Note to developers: - // This callback is triggered when the Post Request is failed. - } - ); - }, (error) => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + modalRef.result.then( + version => { + let data = { + revertExplorationUrl: this.revertExplorationUrl, + currentVersion: this.explorationDataService.data.version, + revertToVersion: version, + }; + this.historyTabBackendApiService.postData(data).then( + () => { + this.windowRef.nativeWindow.location.reload(); + }, + () => { + // Note to developers: + // This callback is triggered when the Post Request is failed. + } + ); + }, + error => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } paginator(value: { @@ -371,13 +412,13 @@ export class HistoryTabComponent changeItemsPerPage(): void { this.explorationVersionMetadata = - this.totalExplorationVersionMetadata.slice( - (this.displayedCurrentPageNumber - 1) * this.VERSIONS_PER_PAGE, - (this.displayedCurrentPageNumber) * this.VERSIONS_PER_PAGE); - this.startingIndex = ( - (this.displayedCurrentPageNumber - 1) * this.VERSIONS_PER_PAGE + 1); - this.endIndex = ( - (this.displayedCurrentPageNumber) * this.VERSIONS_PER_PAGE); + this.totalExplorationVersionMetadata.slice( + (this.displayedCurrentPageNumber - 1) * this.VERSIONS_PER_PAGE, + this.displayedCurrentPageNumber * this.VERSIONS_PER_PAGE + ); + this.startingIndex = + (this.displayedCurrentPageNumber - 1) * this.VERSIONS_PER_PAGE + 1; + this.endIndex = this.displayedCurrentPageNumber * this.VERSIONS_PER_PAGE; this.versionNumbersToDisplay = this.explorationVersionMetadata.length; } @@ -399,58 +440,65 @@ export class HistoryTabComponent showExplorationMetadataDiffModal(): void { let modalRef: NgbModalRef = this.ngbModal.open( - ExplorationMetadataDiffModalComponent, { + ExplorationMetadataDiffModalComponent, + { backdrop: true, windowClass: 'exploration-metadata-diff-modal', - size: 'xl' - }); + size: 'xl', + } + ); modalRef.componentInstance.oldMetadata = ( - this.diffData as Metadata).v1Metadata; + this.diffData as Metadata + ).v1Metadata; modalRef.componentInstance.newMetadata = ( - this.diffData as Metadata).v2Metadata; + this.diffData as Metadata + ).v2Metadata; modalRef.componentInstance.headers = { leftPane: this.earlierVersionHeader, - rightPane: this.laterVersionHeader + rightPane: this.laterVersionHeader, }; - modalRef.result.then(() => {}, () => {}); + modalRef.result.then( + () => {}, + () => {} + ); } ngOnInit(): void { this.directiveSubscriptions.add( - this.routerService.onRefreshVersionHistory.subscribe((data) => { + this.routerService.onRefreshVersionHistory.subscribe(data => { if ( (data && data.forceRefresh) || - this.explorationVersionMetadata === null) { + this.explorationVersionMetadata === null + ) { this.refreshVersionHistory(); } }) ); this.explorationId = this.explorationDataService.explorationId; - this.explorationAllSnapshotsUrl = ( - '/createhandler/snapshots/' + this.explorationId); - this.checkRevertExplorationValidUrl = ( - '/createhandler/check_revert_valid//'); - this.revertExplorationUrl = ( - '/createhandler/revert/' + this.explorationId); - this.explorationDownloadUrl = ( - '/createhandler/download/' + this.explorationId); + this.explorationAllSnapshotsUrl = + '/createhandler/snapshots/' + this.explorationId; + this.checkRevertExplorationValidUrl = + '/createhandler/check_revert_valid//'; + this.revertExplorationUrl = '/createhandler/revert/' + this.explorationId; + this.explorationDownloadUrl = + '/createhandler/download/' + this.explorationId; /* Letiable definitions: - * - * explorationVersionMetadata is an object whose keys are version - * numbers and whose values are objects containing data of that - * revision (that is to be displayed) with the keys 'committerId', - * 'createdOnMsecs', 'commitMessage', and 'versionNumber'. It - * contains a maximum of 30 versions. - * - * versionCheckboxArray is an array of the version numbers of the - * revisions to be displayed on the page, in the order they are - * displayed in. - * - */ + * + * explorationVersionMetadata is an object whose keys are version + * numbers and whose values are objects containing data of that + * revision (that is to be displayed) with the keys 'committerId', + * 'createdOnMsecs', 'commitMessage', and 'versionNumber'. It + * contains a maximum of 30 versions. + * + * versionCheckboxArray is an array of the version numbers of the + * revisions to be displayed on the page, in the order they are + * displayed in. + * + */ this.explorationVersionMetadata = null; this.versionCheckboxArray = []; this.username = ''; @@ -471,7 +519,9 @@ export class HistoryTabComponent } } -angular.module('oppia').directive('oppiaHistoryTab', +angular.module('oppia').directive( + 'oppiaHistoryTab', downgradeComponent({ - component: HistoryTabComponent - }) as angular.IDirectiveFactory); + component: HistoryTabComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-editor-page/history-tab/modal-templates/check-revert-exploration-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/history-tab/modal-templates/check-revert-exploration-modal.component.spec.ts index 1046aad3b62a..35a92913b5a3 100644 --- a/core/templates/pages/exploration-editor-page/history-tab/modal-templates/check-revert-exploration-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/history-tab/modal-templates/check-revert-exploration-modal.component.spec.ts @@ -12,18 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Unit tests for the CheckRevertExplorationModalComponent. */ -import { EventEmitter } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import {EventEmitter} from '@angular/core'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; -import { CheckRevertService } from '../services/check-revert.service'; -import { CheckRevertExplorationModalComponent } from './check-revert-exploration-modal.component'; -import { LoadingDotsComponent } from 'components/common-layout-directives/common-elements/loading-dots.component'; +import {CheckRevertService} from '../services/check-revert.service'; +import {CheckRevertExplorationModalComponent} from './check-revert-exploration-modal.component'; +import {LoadingDotsComponent} from 'components/common-layout-directives/common-elements/loading-dots.component'; class MockActiveModal { close(): void { @@ -31,7 +30,7 @@ class MockActiveModal { } } -describe('Check Revert Exploration Modal Component', function() { +describe('Check Revert Exploration Modal Component', function () { let component: CheckRevertExplorationModalComponent; let fixture: ComponentFixture; let ngbActiveModal: NgbActiveModal; @@ -42,20 +41,20 @@ describe('Check Revert Exploration Modal Component', function() { TestBed.configureTestingModule({ declarations: [ CheckRevertExplorationModalComponent, - LoadingDotsComponent + LoadingDotsComponent, ], providers: [ { provide: NgbActiveModal, - useClass: MockActiveModal + useClass: MockActiveModal, }, { provide: CheckRevertService, useValue: { - detailsEventEmitter: mockDetailsEventEmitter - } - } - ] + detailsEventEmitter: mockDetailsEventEmitter, + }, + }, + ], }).compileComponents(); })); diff --git a/core/templates/pages/exploration-editor-page/history-tab/modal-templates/check-revert-exploration-modal.component.ts b/core/templates/pages/exploration-editor-page/history-tab/modal-templates/check-revert-exploration-modal.component.ts index 94066f957825..e444d050bbae 100644 --- a/core/templates/pages/exploration-editor-page/history-tab/modal-templates/check-revert-exploration-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/history-tab/modal-templates/check-revert-exploration-modal.component.ts @@ -16,14 +16,14 @@ * @fileoverview Component for revert exploration modal. */ -import { Component, Input, OnInit } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import {Component, Input, OnInit} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; -import { CheckRevertService } from 'pages/exploration-editor-page/history-tab/services/check-revert.service'; +import {CheckRevertService} from 'pages/exploration-editor-page/history-tab/services/check-revert.service'; @Component({ selector: 'oppia-check-revert-exploration-modal', - templateUrl: './check-revert-exploration-modal.component.html' + templateUrl: './check-revert-exploration-modal.component.html', }) export class CheckRevertExplorationModalComponent implements OnInit { // These properties are initialized using Angular lifecycle hooks @@ -39,7 +39,7 @@ export class CheckRevertExplorationModalComponent implements OnInit { ) {} ngOnInit(): void { - this.checkRevertService.detailsEventEmitter.subscribe((details) => { + this.checkRevertService.detailsEventEmitter.subscribe(details => { this.details = details; }); } diff --git a/core/templates/pages/exploration-editor-page/history-tab/modal-templates/revert-exploration-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/history-tab/modal-templates/revert-exploration-modal.component.spec.ts index 1e312c456eba..93145d3000b5 100644 --- a/core/templates/pages/exploration-editor-page/history-tab/modal-templates/revert-exploration-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/history-tab/modal-templates/revert-exploration-modal.component.spec.ts @@ -12,17 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Unit tests for the RevertExplorationModalComponent. */ -import { NO_ERRORS_SCHEMA, ElementRef } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import {NO_ERRORS_SCHEMA, ElementRef} from '@angular/core'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; -import { ExplorationDataService } from 'pages/exploration-editor-page/services/exploration-data.service'; -import { RevertExplorationModalComponent } from './revert-exploration-modal.component'; +import {ExplorationDataService} from 'pages/exploration-editor-page/services/exploration-data.service'; +import {RevertExplorationModalComponent} from './revert-exploration-modal.component'; class MockActiveModal { close(): void { @@ -38,24 +37,24 @@ class MockExplorationDataService { explorationId: string = 'exp1'; } -describe('Revert Exploration Modal Component', function() { +describe('Revert Exploration Modal Component', function () { let component: RevertExplorationModalComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - RevertExplorationModalComponent + declarations: [RevertExplorationModalComponent], + providers: [ + { + provide: NgbActiveModal, + useClass: MockActiveModal, + }, + { + provide: ExplorationDataService, + useClass: MockExplorationDataService, + }, ], - providers: [{ - provide: NgbActiveModal, - useClass: MockActiveModal - }, - { - provide: ExplorationDataService, - useClass: MockExplorationDataService - }], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); diff --git a/core/templates/pages/exploration-editor-page/history-tab/modal-templates/revert-exploration-modal.component.ts b/core/templates/pages/exploration-editor-page/history-tab/modal-templates/revert-exploration-modal.component.ts index d43280059220..331b79279443 100644 --- a/core/templates/pages/exploration-editor-page/history-tab/modal-templates/revert-exploration-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/history-tab/modal-templates/revert-exploration-modal.component.ts @@ -16,17 +16,16 @@ * @fileoverview Component for revert exploration modal. */ -import { Component, Input, ViewChild, ElementRef } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import {Component, Input, ViewChild, ElementRef} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { ExplorationDataService } from 'pages/exploration-editor-page/services/exploration-data.service'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {ExplorationDataService} from 'pages/exploration-editor-page/services/exploration-data.service'; @Component({ selector: 'oppia-revert-exploration-modal', - templateUrl: './revert-exploration-modal.component.html' + templateUrl: './revert-exploration-modal.component.html', }) - export class RevertExplorationModalComponent extends ConfirmOrCancelModal { // This property is initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see @@ -43,8 +42,7 @@ export class RevertExplorationModalComponent extends ConfirmOrCancelModal { getExplorationUrl(version: string): string { return ( - '/explore/' + this.explorationDataService.explorationId + - '?v=' + version + '/explore/' + this.explorationDataService.explorationId + '?v=' + version ); } diff --git a/core/templates/pages/exploration-editor-page/history-tab/services/check-revert.service.spec.ts b/core/templates/pages/exploration-editor-page/history-tab/services/check-revert.service.spec.ts index 360f682353f8..1e7b79fcc0ef 100644 --- a/core/templates/pages/exploration-editor-page/history-tab/services/check-revert.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/history-tab/services/check-revert.service.spec.ts @@ -16,7 +16,7 @@ * @fileoverview Unit tests for the Check Revert Service. */ -import { CheckRevertService } from 'pages/exploration-editor-page/history-tab/services/check-revert.service'; +import {CheckRevertService} from 'pages/exploration-editor-page/history-tab/services/check-revert.service'; describe('Check revert service', () => { describe('check revert service', () => { diff --git a/core/templates/pages/exploration-editor-page/history-tab/services/check-revert.service.ts b/core/templates/pages/exploration-editor-page/history-tab/services/check-revert.service.ts index dd3ef9641f25..4a707587f3a1 100644 --- a/core/templates/pages/exploration-editor-page/history-tab/services/check-revert.service.ts +++ b/core/templates/pages/exploration-editor-page/history-tab/services/check-revert.service.ts @@ -16,15 +16,16 @@ * @fileoverview Service for passing exploration revert details. */ -import { EventEmitter, Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {EventEmitter, Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class CheckRevertService { detailsEventEmitter: EventEmitter = new EventEmitter(); } -angular.module('oppia').factory( - 'CheckRevertService', downgradeInjectable(CheckRevertService)); +angular + .module('oppia') + .factory('CheckRevertService', downgradeInjectable(CheckRevertService)); diff --git a/core/templates/pages/exploration-editor-page/history-tab/services/compare-versions.service.spec.ts b/core/templates/pages/exploration-editor-page/history-tab/services/compare-versions.service.spec.ts index d459585c2686..90b12112c5e7 100644 --- a/core/templates/pages/exploration-editor-page/history-tab/services/compare-versions.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/history-tab/services/compare-versions.service.spec.ts @@ -16,12 +16,18 @@ * @fileoverview Unit tests for the Compare versions Service. */ -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; -import { ExplorationSnapshot, VersionTreeService } from 'pages/exploration-editor-page/history-tab/services/version-tree.service'; -import { ExplorationDataService } from 'pages/exploration-editor-page/services/exploration-data.service'; -import { CompareVersionsService } from './compare-versions.service'; -import { StateBackendDict } from 'domain/state/StateObjectFactory'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; +import { + ExplorationSnapshot, + VersionTreeService, +} from 'pages/exploration-editor-page/history-tab/services/version-tree.service'; +import {ExplorationDataService} from 'pages/exploration-editor-page/services/exploration-data.service'; +import {CompareVersionsService} from './compare-versions.service'; +import {StateBackendDict} from 'domain/state/StateObjectFactory'; interface stateDetails { contentStr: string; @@ -30,7 +36,8 @@ interface stateDetails { interface StatesData { A?: stateDetails; -}[]; +} +[]; describe('Compare versions service', () => { let cvs: CompareVersionsService; @@ -48,11 +55,11 @@ describe('Compare versions service', () => { useValue: { explorationId: '0', data: { - version: 1 - } - } - } - ] + version: 1, + }, + }, + }, + ], }); cvs = TestBed.inject(CompareVersionsService); @@ -72,22 +79,23 @@ describe('Compare versions service', () => { // links // Only information accessed by getDiffGraphData is included in the return // value. - let _getStatesAndMetadata = function(statesDetails: StatesData) { + let _getStatesAndMetadata = function (statesDetails: StatesData) { let statesData: Record = {}; for (let stateName in statesDetails) { let stateDetail = statesDetails[ - stateName as keyof StatesData] as stateDetails; + stateName as keyof StatesData + ] as stateDetails; let newStateData = { classifier_model_id: null, content: { content_id: 'content', - html: stateDetail.contentStr + html: stateDetail.contentStr, }, recorded_voiceovers: { voiceovers_mapping: { content: {}, default_outcome: {}, - } + }, }, interaction: { id: 'EndExploration', @@ -95,20 +103,20 @@ describe('Compare versions service', () => { confirmed_unclassified_answers: [], customization_args: { buttonText: { - value: 'Continue' - } + value: 'Continue', + }, }, default_outcome: { dest: 'default', dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: '' + html: '', }, labelled_as_correct: false, param_changes: [], refresher_exploration_id: null, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, hints: [], solution: null, @@ -117,30 +125,31 @@ describe('Compare versions service', () => { next_content_id_index: 0, param_changes: [], solicit_answer_details: false, - card_is_checkpoint: true + card_is_checkpoint: true, }; // This throws "Argument of type 'null' is not assignable to parameter of // type 'AnswerGroup[]'." We need to suppress this error // because of the need to test validations. This throws an // error only in the frontend tests and not in the linter. // @ts-ignore - newStateData.interaction.answer_groups = - stateDetail.ruleDests.map(function(ruleDestName) { - return { - outcome: { - dest: ruleDestName, - dest_if_really_stuck: null, - feedback: [], - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null - }, - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - }; - }); + newStateData.interaction.answer_groups = stateDetail.ruleDests.map( + function (ruleDestName) { + return { + outcome: { + dest: ruleDestName, + dest_if_really_stuck: null, + feedback: [], + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + }, + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + }; + } + ); statesData[stateName as keyof StatesData] = newStateData; } let explorationMetadataDict = { @@ -156,320 +165,372 @@ describe('Compare versions service', () => { param_specs: {}, param_changes: [], auto_tts_enabled: false, - edits_allowed: true + edits_allowed: true, }; return { exploration: { - states: statesData + states: statesData, }, - exploration_metadata: explorationMetadataDict + exploration_metadata: explorationMetadataDict, }; }; - const testSnapshots1: ExplorationSnapshot[] = [{ - commit_type: 'create', - version_number: 1, - committer_id: 'admin', - commit_message: 'Commit message', - created_on_ms: 1592229964515.148, - commit_cmds: [] - }, { - commit_type: 'edit', - commit_cmds: [{ - cmd: 'edit_state_property', - state_name: 'A', - new_value: { - content_id: 'content', - html: 'Some text' - }, - old_value: { - content_id: 'content', - html: '' - }, - property_name: 'property' - }], - version_number: 2, - committer_id: 'admin', - commit_message: 'Commit message', - created_on_ms: 1592229964515.148, - }, { - commit_type: 'edit', - commit_cmds: [{ - cmd: 'rename_state', - new_state_name: 'B', - old_state_name: 'A' - }], - version_number: 3, - committer_id: 'admin', - commit_message: 'Commit message', - created_on_ms: 1592229964515.148, - }, { - commit_type: 'edit', - commit_cmds: [{ - cmd: 'rename_state', - new_state_name: 'A', - old_state_name: 'B' - }], - version_number: 4, - committer_id: 'admin', - commit_message: 'Commit message', - created_on_ms: 1592229964515.148, - }, { - commit_type: 'edit', - commit_cmds: [{ - cmd: 'add_state', - state_name: 'B', - content_id_for_state_content: 'content_0', - content_id_for_default_outcome: 'default_outcome_1' - }], - version_number: 5, - committer_id: 'admin', - commit_message: 'Commit message', - created_on_ms: 1592229964515.148, - }, { - commit_type: 'edit', - commit_cmds: [{ - cmd: 'rename_state', - new_state_name: 'C', - old_state_name: 'B' - }], - version_number: 6, - committer_id: 'admin', - commit_message: 'Commit message', - created_on_ms: 1592229964515.148, - }, { - commit_type: 'edit', - commit_cmds: [{ - cmd: 'edit_state_property', - state_name: 'C', - new_value: { - content_id: 'content', - html: 'More text' - }, - old_value: { - content_id: 'content', - html: '' - }, - property_name: 'property' - }], - version_number: 7, - committer_id: 'admin', - commit_message: 'Commit message', - created_on_ms: 1592229964515.148, - }, { - commit_type: 'edit', - commit_cmds: [{ - cmd: 'add_state', - state_name: 'B', - content_id_for_state_content: 'content_0', - content_id_for_default_outcome: 'default_outcome_1' - }], - version_number: 8, - committer_id: 'admin', - commit_message: 'Commit message', - created_on_ms: 1592229964515.148, - }, { - commit_type: 'edit', - commit_cmds: [{ - cmd: 'delete_state', - state_name: 'B' - }], - version_number: 9, - committer_id: 'admin', - commit_message: 'Commit message', - created_on_ms: 1592229964515.148, - }, { - commit_type: 'edit', - commit_cmds: [{ - cmd: 'add_state', - state_name: 'B', - content_id_for_state_content: 'content_0', - content_id_for_default_outcome: 'default_outcome_1' - }], - version_number: 10, - committer_id: 'admin', - commit_message: 'Commit message', - created_on_ms: 1592229964515.148, - }, { - commit_type: 'edit', - commit_cmds: [{ - cmd: 'edit_state_property', - state_name: 'A', - new_value: { - content_id: 'content', - html: '' - }, - old_value: { - content_id: 'content', - html: 'Some text' - }, - property_name: 'property' - }], - version_number: 11, - committer_id: 'admin', - commit_message: 'Commit message', - created_on_ms: 1592229964515.148, - }, { - commit_type: 'edit', - commit_cmds: [{ - cmd: 'rename_state', - new_state_name: 'D', - old_state_name: 'A' - }], - version_number: 12, - committer_id: 'admin', - commit_message: 'Commit message', - created_on_ms: 1592229964515.148, - }, { - commit_type: 'edit', - commit_cmds: [{ - cmd: 'delete_state', - state_name: 'D' - }], - version_number: 13, - committer_id: 'admin', - commit_message: 'Commit message', - created_on_ms: 1592229964515.148, - }]; + const testSnapshots1: ExplorationSnapshot[] = [ + { + commit_type: 'create', + version_number: 1, + committer_id: 'admin', + commit_message: 'Commit message', + created_on_ms: 1592229964515.148, + commit_cmds: [], + }, + { + commit_type: 'edit', + commit_cmds: [ + { + cmd: 'edit_state_property', + state_name: 'A', + new_value: { + content_id: 'content', + html: 'Some text', + }, + old_value: { + content_id: 'content', + html: '', + }, + property_name: 'property', + }, + ], + version_number: 2, + committer_id: 'admin', + commit_message: 'Commit message', + created_on_ms: 1592229964515.148, + }, + { + commit_type: 'edit', + commit_cmds: [ + { + cmd: 'rename_state', + new_state_name: 'B', + old_state_name: 'A', + }, + ], + version_number: 3, + committer_id: 'admin', + commit_message: 'Commit message', + created_on_ms: 1592229964515.148, + }, + { + commit_type: 'edit', + commit_cmds: [ + { + cmd: 'rename_state', + new_state_name: 'A', + old_state_name: 'B', + }, + ], + version_number: 4, + committer_id: 'admin', + commit_message: 'Commit message', + created_on_ms: 1592229964515.148, + }, + { + commit_type: 'edit', + commit_cmds: [ + { + cmd: 'add_state', + state_name: 'B', + content_id_for_state_content: 'content_0', + content_id_for_default_outcome: 'default_outcome_1', + }, + ], + version_number: 5, + committer_id: 'admin', + commit_message: 'Commit message', + created_on_ms: 1592229964515.148, + }, + { + commit_type: 'edit', + commit_cmds: [ + { + cmd: 'rename_state', + new_state_name: 'C', + old_state_name: 'B', + }, + ], + version_number: 6, + committer_id: 'admin', + commit_message: 'Commit message', + created_on_ms: 1592229964515.148, + }, + { + commit_type: 'edit', + commit_cmds: [ + { + cmd: 'edit_state_property', + state_name: 'C', + new_value: { + content_id: 'content', + html: 'More text', + }, + old_value: { + content_id: 'content', + html: '', + }, + property_name: 'property', + }, + ], + version_number: 7, + committer_id: 'admin', + commit_message: 'Commit message', + created_on_ms: 1592229964515.148, + }, + { + commit_type: 'edit', + commit_cmds: [ + { + cmd: 'add_state', + state_name: 'B', + content_id_for_state_content: 'content_0', + content_id_for_default_outcome: 'default_outcome_1', + }, + ], + version_number: 8, + committer_id: 'admin', + commit_message: 'Commit message', + created_on_ms: 1592229964515.148, + }, + { + commit_type: 'edit', + commit_cmds: [ + { + cmd: 'delete_state', + state_name: 'B', + }, + ], + version_number: 9, + committer_id: 'admin', + commit_message: 'Commit message', + created_on_ms: 1592229964515.148, + }, + { + commit_type: 'edit', + commit_cmds: [ + { + cmd: 'add_state', + state_name: 'B', + content_id_for_state_content: 'content_0', + content_id_for_default_outcome: 'default_outcome_1', + }, + ], + version_number: 10, + committer_id: 'admin', + commit_message: 'Commit message', + created_on_ms: 1592229964515.148, + }, + { + commit_type: 'edit', + commit_cmds: [ + { + cmd: 'edit_state_property', + state_name: 'A', + new_value: { + content_id: 'content', + html: '', + }, + old_value: { + content_id: 'content', + html: 'Some text', + }, + property_name: 'property', + }, + ], + version_number: 11, + committer_id: 'admin', + commit_message: 'Commit message', + created_on_ms: 1592229964515.148, + }, + { + commit_type: 'edit', + commit_cmds: [ + { + cmd: 'rename_state', + new_state_name: 'D', + old_state_name: 'A', + }, + ], + version_number: 12, + committer_id: 'admin', + commit_message: 'Commit message', + created_on_ms: 1592229964515.148, + }, + { + commit_type: 'edit', + commit_cmds: [ + { + cmd: 'delete_state', + state_name: 'D', + }, + ], + version_number: 13, + committer_id: 'admin', + commit_message: 'Commit message', + created_on_ms: 1592229964515.148, + }, + ]; // Information for mock state data for getDiffGraphData() to be passed to // _getStatesAndMetadata. - let testExplorationData1 = [{ - A: { - contentStr: '', - ruleDests: ['A'] - } - }, { - A: { - contentStr: 'Some text', - ruleDests: ['A'] - } - }, { - B: { - contentStr: 'Some text', - ruleDests: ['B'] - } - }, { - A: { - contentStr: 'Some text', - ruleDests: ['A'] - } - }, { - A: { - contentStr: 'Some text', - ruleDests: ['A'] + let testExplorationData1 = [ + { + A: { + contentStr: '', + ruleDests: ['A'], + }, }, - B: { - contentStr: '', - ruleDests: ['B'] - } - }, { - A: { - contentStr: 'Some text', - ruleDests: ['A'] + { + A: { + contentStr: 'Some text', + ruleDests: ['A'], + }, }, - C: { - contentStr: '', - ruleDests: ['C'] - } - }, { - A: { - contentStr: 'Some text', - ruleDests: ['A'] + { + B: { + contentStr: 'Some text', + ruleDests: ['B'], + }, }, - C: { - contentStr: 'More text', - ruleDests: ['C'] - } - }, { - A: { - contentStr: 'Some text', - ruleDests: ['A'] + { + A: { + contentStr: 'Some text', + ruleDests: ['A'], + }, }, - B: { - contentStr: '', - ruleDests: ['B'] + { + A: { + contentStr: 'Some text', + ruleDests: ['A'], + }, + B: { + contentStr: '', + ruleDests: ['B'], + }, }, - C: { - contentStr: 'More text', - ruleDests: ['C'] - } - }, { - A: { - contentStr: 'Some text', - ruleDests: ['A'] + { + A: { + contentStr: 'Some text', + ruleDests: ['A'], + }, + C: { + contentStr: '', + ruleDests: ['C'], + }, }, - C: { - contentStr: 'More text', - ruleDests: ['C'] - } - }, { - A: { - contentStr: 'Some text', - ruleDests: ['A'] + { + A: { + contentStr: 'Some text', + ruleDests: ['A'], + }, + C: { + contentStr: 'More text', + ruleDests: ['C'], + }, }, - B: { - contentStr: 'Added text', - ruleDests: ['B'] + { + A: { + contentStr: 'Some text', + ruleDests: ['A'], + }, + B: { + contentStr: '', + ruleDests: ['B'], + }, + C: { + contentStr: 'More text', + ruleDests: ['C'], + }, }, - C: { - contentStr: 'More text', - ruleDests: ['C'] - } - }, { - A: { - contentStr: '', - ruleDests: ['A'] + { + A: { + contentStr: 'Some text', + ruleDests: ['A'], + }, + C: { + contentStr: 'More text', + ruleDests: ['C'], + }, }, - B: { - contentStr: 'Added text', - ruleDests: ['B'] + { + A: { + contentStr: 'Some text', + ruleDests: ['A'], + }, + B: { + contentStr: 'Added text', + ruleDests: ['B'], + }, + C: { + contentStr: 'More text', + ruleDests: ['C'], + }, }, - C: { - contentStr: 'More text', - ruleDests: ['C'] - } - }, { - B: { - contentStr: 'Added text', - ruleDests: ['B'] + { + A: { + contentStr: '', + ruleDests: ['A'], + }, + B: { + contentStr: 'Added text', + ruleDests: ['B'], + }, + C: { + contentStr: 'More text', + ruleDests: ['C'], + }, }, - C: { - contentStr: 'More text', - ruleDests: ['C'] + { + B: { + contentStr: 'Added text', + ruleDests: ['B'], + }, + C: { + contentStr: 'More text', + ruleDests: ['C'], + }, + D: { + contentStr: '', + ruleDests: ['D'], + }, }, - D: { - contentStr: '', - ruleDests: ['D'] - } - }, { - B: { - contentStr: 'Added text', - ruleDests: ['B'] + { + B: { + contentStr: 'Added text', + ruleDests: ['B'], + }, + C: { + contentStr: 'More text', + ruleDests: ['C'], + }, }, - C: { - contentStr: 'More text', - ruleDests: ['C'] - } - }]; + ]; // Tests for getDiffGraphData on linear commits. it('should detect changed, renamed and added states', fakeAsync(() => { vts.init(testSnapshots1); let nodeData = null; - cvs.getDiffGraphData(1, 7).then((data) => { + cvs.getDiffGraphData(1, 7).then(data => { nodeData = data.nodes; expect(nodeData).toEqual({ 1: { newestStateName: 'A', stateProperty: 'changed', - originalStateName: 'A' + originalStateName: 'A', }, 2: { newestStateName: 'C', stateProperty: 'added', - originalStateName: 'B' - } + originalStateName: 'B', + }, }); }); @@ -477,68 +538,64 @@ describe('Compare versions service', () => { expect(req.request.method).toEqual('GET'); req.flush(_getStatesAndMetadata(testExplorationData1[0])); - const req2 = httpTestingController.expectOne( - '/explorehandler/init/0?v=7'); + const req2 = httpTestingController.expectOne('/explorehandler/init/0?v=7'); expect(req2.request.method).toEqual('GET'); req2.flush(_getStatesAndMetadata(testExplorationData1[6])); flushMicrotasks(); })); - it('should add new state with same name as old name of renamed state', - fakeAsync(() => { - vts.init(testSnapshots1); - let nodeData = null; - cvs.getDiffGraphData(5, 8).then((data) => { - nodeData = data.nodes; - expect(nodeData).toEqual({ - 1: { - newestStateName: 'A', - stateProperty: 'unchanged', - originalStateName: 'A' - }, - 2: { - newestStateName: 'C', - stateProperty: 'changed', - originalStateName: 'B' - }, - 3: { - newestStateName: 'B', - stateProperty: 'added', - originalStateName: 'B' - } - }); + it('should add new state with same name as old name of renamed state', fakeAsync(() => { + vts.init(testSnapshots1); + let nodeData = null; + cvs.getDiffGraphData(5, 8).then(data => { + nodeData = data.nodes; + expect(nodeData).toEqual({ + 1: { + newestStateName: 'A', + stateProperty: 'unchanged', + originalStateName: 'A', + }, + 2: { + newestStateName: 'C', + stateProperty: 'changed', + originalStateName: 'B', + }, + 3: { + newestStateName: 'B', + stateProperty: 'added', + originalStateName: 'B', + }, }); + }); - const req = httpTestingController.expectOne( - '/explorehandler/init/0?v=5'); - expect(req.request.method).toEqual('GET'); - req.flush(_getStatesAndMetadata(testExplorationData1[4])); + const req = httpTestingController.expectOne('/explorehandler/init/0?v=5'); + expect(req.request.method).toEqual('GET'); + req.flush(_getStatesAndMetadata(testExplorationData1[4])); - const req2 = httpTestingController.expectOne( - '/explorehandler/init/0?v=8'); - expect(req2.request.method).toEqual('GET'); - req2.flush(_getStatesAndMetadata(testExplorationData1[7])); + const req2 = httpTestingController.expectOne('/explorehandler/init/0?v=8'); + expect(req2.request.method).toEqual('GET'); + req2.flush(_getStatesAndMetadata(testExplorationData1[7])); - flushMicrotasks(); - })); + flushMicrotasks(); + })); it('should not include added, then deleted state', fakeAsync(() => { vts.init(testSnapshots1); let nodeData = null; - cvs.getDiffGraphData(7, 9).then((data) => { + cvs.getDiffGraphData(7, 9).then(data => { nodeData = data.nodes; expect(nodeData).toEqual({ 1: { newestStateName: 'A', stateProperty: 'unchanged', - originalStateName: 'A' + originalStateName: 'A', }, 2: { newestStateName: 'C', stateProperty: 'unchanged', - originalStateName: 'C' - } + originalStateName: 'C', + }, }); }); @@ -546,8 +603,7 @@ describe('Compare versions service', () => { expect(req.request.method).toEqual('GET'); req.flush(_getStatesAndMetadata(testExplorationData1[6])); - const req2 = httpTestingController.expectOne( - '/explorehandler/init/0?v=9'); + const req2 = httpTestingController.expectOne('/explorehandler/init/0?v=9'); expect(req2.request.method).toEqual('GET'); req2.flush(_getStatesAndMetadata(testExplorationData1[8])); @@ -557,24 +613,24 @@ describe('Compare versions service', () => { it('should mark deleted then added states as changed', fakeAsync(() => { vts.init(testSnapshots1); let nodeData = null; - cvs.getDiffGraphData(8, 10).then((data) => { + cvs.getDiffGraphData(8, 10).then(data => { nodeData = data.nodes; expect(nodeData).toEqual({ 1: { newestStateName: 'A', stateProperty: 'unchanged', - originalStateName: 'A' + originalStateName: 'A', }, 2: { newestStateName: 'B', stateProperty: 'changed', - originalStateName: 'B' + originalStateName: 'B', }, 3: { newestStateName: 'C', stateProperty: 'unchanged', - originalStateName: 'C' - } + originalStateName: 'C', + }, }); }); @@ -582,8 +638,7 @@ describe('Compare versions service', () => { expect(req.request.method).toEqual('GET'); req.flush(_getStatesAndMetadata(testExplorationData1[7])); - const req2 = httpTestingController.expectOne( - '/explorehandler/init/0?v=10'); + const req2 = httpTestingController.expectOne('/explorehandler/init/0?v=10'); expect(req2.request.method).toEqual('GET'); req2.flush(_getStatesAndMetadata(testExplorationData1[9])); @@ -593,62 +648,131 @@ describe('Compare versions service', () => { it('should mark renamed then deleted states as deleted', fakeAsync(() => { vts.init(testSnapshots1); let nodeData = null; - cvs.getDiffGraphData(11, 13).then((data) => { + cvs.getDiffGraphData(11, 13).then(data => { nodeData = data.nodes; expect(nodeData).toEqual({ 1: { newestStateName: 'D', stateProperty: 'deleted', - originalStateName: 'A' + originalStateName: 'A', }, 2: { newestStateName: 'B', stateProperty: 'unchanged', - originalStateName: 'B' + originalStateName: 'B', }, 3: { newestStateName: 'C', stateProperty: 'unchanged', - originalStateName: 'C' - } + originalStateName: 'C', + }, }); }); - const req = httpTestingController.expectOne( - '/explorehandler/init/0?v=11'); + const req = httpTestingController.expectOne('/explorehandler/init/0?v=11'); expect(req.request.method).toEqual('GET'); req.flush(_getStatesAndMetadata(testExplorationData1[10])); - const req2 = httpTestingController.expectOne( - '/explorehandler/init/0?v=13'); + const req2 = httpTestingController.expectOne('/explorehandler/init/0?v=13'); expect(req2.request.method).toEqual('GET'); req2.flush(_getStatesAndMetadata(testExplorationData1[12])); flushMicrotasks(); })); - it('should mark changed state as unchanged when name and content is same' + - 'on both versions', fakeAsync(() => { + it( + 'should mark changed state as unchanged when name and content is same' + + 'on both versions', + fakeAsync(() => { + vts.init(testSnapshots1); + let nodeData = null; + cvs.getDiffGraphData(1, 11).then(data => { + nodeData = data.nodes; + expect(nodeData).toEqual({ + 1: { + newestStateName: 'A', + stateProperty: 'unchanged', + originalStateName: 'A', + }, + 2: { + newestStateName: 'C', + stateProperty: 'added', + originalStateName: 'B', + }, + 3: { + newestStateName: 'B', + stateProperty: 'added', + originalStateName: 'B', + }, + }); + }); + + const req = httpTestingController.expectOne('/explorehandler/init/0?v=1'); + expect(req.request.method).toEqual('GET'); + req.flush(_getStatesAndMetadata(testExplorationData1[0])); + + const req2 = httpTestingController.expectOne( + '/explorehandler/init/0?v=11' + ); + expect(req2.request.method).toEqual('GET'); + req2.flush(_getStatesAndMetadata(testExplorationData1[10])); + + flushMicrotasks(); + }) + ); + + it( + 'should mark renamed state as not renamed when name is same on both ' + + 'versions', + fakeAsync(() => { + vts.init(testSnapshots1); + let nodeData = null; + cvs.getDiffGraphData(2, 4).then(data => { + nodeData = data.nodes; + expect(nodeData).toEqual({ + 1: { + newestStateName: 'A', + stateProperty: 'unchanged', + originalStateName: 'A', + }, + }); + }); + + const req = httpTestingController.expectOne('/explorehandler/init/0?v=2'); + expect(req.request.method).toEqual('GET'); + req.flush(_getStatesAndMetadata(testExplorationData1[1])); + + const req2 = httpTestingController.expectOne( + '/explorehandler/init/0?v=4' + ); + expect(req2.request.method).toEqual('GET'); + req2.flush(_getStatesAndMetadata(testExplorationData1[3])); + + flushMicrotasks(); + }) + ); + + it('should mark states correctly when a series of changes are applied', fakeAsync(() => { vts.init(testSnapshots1); let nodeData = null; - cvs.getDiffGraphData(1, 11).then((data) => { + cvs.getDiffGraphData(1, 13).then(data => { nodeData = data.nodes; expect(nodeData).toEqual({ 1: { - newestStateName: 'A', - stateProperty: 'unchanged', - originalStateName: 'A' + newestStateName: 'D', + stateProperty: 'deleted', + originalStateName: 'A', }, 2: { newestStateName: 'C', stateProperty: 'added', - originalStateName: 'B' + originalStateName: 'B', }, 3: { newestStateName: 'B', stateProperty: 'added', - originalStateName: 'B' - } + originalStateName: 'B', + }, }); }); @@ -656,631 +780,617 @@ describe('Compare versions service', () => { expect(req.request.method).toEqual('GET'); req.flush(_getStatesAndMetadata(testExplorationData1[0])); - const req2 = httpTestingController.expectOne( - '/explorehandler/init/0?v=11'); - expect(req2.request.method).toEqual('GET'); - req2.flush(_getStatesAndMetadata(testExplorationData1[10])); - - flushMicrotasks(); - })); - - it('should mark renamed state as not renamed when name is same on both ' + - 'versions', fakeAsync(() => { - vts.init(testSnapshots1); - let nodeData = null; - cvs.getDiffGraphData(2, 4).then((data) => { - nodeData = data.nodes; - expect(nodeData).toEqual({ - 1: { - newestStateName: 'A', - stateProperty: 'unchanged', - originalStateName: 'A' - } - }); - }); - - const req = httpTestingController.expectOne('/explorehandler/init/0?v=2'); - expect(req.request.method).toEqual('GET'); - req.flush(_getStatesAndMetadata(testExplorationData1[1])); - - const req2 = httpTestingController.expectOne( - '/explorehandler/init/0?v=4'); + const req2 = httpTestingController.expectOne('/explorehandler/init/0?v=13'); expect(req2.request.method).toEqual('GET'); - req2.flush(_getStatesAndMetadata(testExplorationData1[3])); + req2.flush(_getStatesAndMetadata(testExplorationData1[12])); flushMicrotasks(); })); - it('should mark states correctly when a series of changes are applied', - fakeAsync(() => { - vts.init(testSnapshots1); - let nodeData = null; - cvs.getDiffGraphData(1, 13).then((data) => { - nodeData = data.nodes; - expect(nodeData).toEqual({ - 1: { - newestStateName: 'D', - stateProperty: 'deleted', - originalStateName: 'A' + let testSnapshots2: ExplorationSnapshot[] = [ + { + commit_type: 'create', + version_number: 1, + committer_id: 'admin', + commit_message: 'Commit message', + created_on_ms: 1592229964515.148, + commit_cmds: [], + }, + { + commit_type: 'edit', + commit_cmds: [ + { + cmd: 'add_state', + state_name: 'B', + content_id_for_state_content: 'content_0', + content_id_for_default_outcome: 'default_outcome_1', + }, + ], + version_number: 2, + committer_id: 'admin', + commit_message: 'Commit message', + created_on_ms: 1592229964515.148, + }, + { + commit_type: 'edit', + commit_cmds: [ + { + cmd: 'rename_state', + new_state_name: 'C', + old_state_name: 'B', + }, + ], + version_number: 3, + committer_id: 'admin', + commit_message: 'Commit message', + created_on_ms: 1592229964515.148, + }, + { + commit_type: 'revert', + commit_cmds: [ + { + cmd: 'AUTO_revert_version_number', + version_number: 2, + }, + ], + version_number: 4, + committer_id: 'admin', + commit_message: 'Commit message', + created_on_ms: 1592229964515.148, + }, + { + commit_type: 'edit', + commit_cmds: [ + { + cmd: 'delete_state', + state_name: 'B', + }, + ], + version_number: 5, + committer_id: 'admin', + commit_message: 'Commit message', + created_on_ms: 1592229964515.148, + }, + { + commit_type: 'revert', + commit_cmds: [ + { + cmd: 'AUTO_revert_version_number', + version_number: 3, + }, + ], + version_number: 6, + committer_id: 'admin', + commit_message: 'Commit message', + created_on_ms: 1592229964515.148, + }, + { + commit_type: 'edit', + commit_cmds: [ + { + cmd: 'add_state', + state_name: 'D', + content_id_for_state_content: 'content_0', + content_id_for_default_outcome: 'default_outcome_1', + }, + ], + version_number: 7, + committer_id: 'admin', + commit_message: 'Commit message', + created_on_ms: 1592229964515.148, + }, + { + commit_type: 'edit', + commit_cmds: [ + { + cmd: 'edit_state_property', + state_name: 'D', + new_value: { + content_id: 'content', + html: 'Some text', }, - 2: { - newestStateName: 'C', - stateProperty: 'added', - originalStateName: 'B' + old_value: { + content_id: 'content', + html: '', }, - 3: { - newestStateName: 'B', - stateProperty: 'added', - originalStateName: 'B' - } - }); - }); - - const req = httpTestingController.expectOne( - '/explorehandler/init/0?v=1'); - expect(req.request.method).toEqual('GET'); - req.flush(_getStatesAndMetadata(testExplorationData1[0])); - - const req2 = httpTestingController.expectOne( - '/explorehandler/init/0?v=13'); - expect(req2.request.method).toEqual('GET'); - req2.flush(_getStatesAndMetadata(testExplorationData1[12])); - - flushMicrotasks(); - })); - - let testSnapshots2: ExplorationSnapshot[] = [{ - commit_type: 'create', - version_number: 1, - committer_id: 'admin', - commit_message: 'Commit message', - created_on_ms: 1592229964515.148, - commit_cmds: [] - }, { - commit_type: 'edit', - commit_cmds: [{ - cmd: 'add_state', - state_name: 'B', - content_id_for_state_content: 'content_0', - content_id_for_default_outcome: 'default_outcome_1' - }], - version_number: 2, - committer_id: 'admin', - commit_message: 'Commit message', - created_on_ms: 1592229964515.148, - }, { - commit_type: 'edit', - commit_cmds: [{ - cmd: 'rename_state', - new_state_name: 'C', - old_state_name: 'B' - }], - version_number: 3, - committer_id: 'admin', - commit_message: 'Commit message', - created_on_ms: 1592229964515.148, - }, { - commit_type: 'revert', - commit_cmds: [{ - cmd: 'AUTO_revert_version_number', - version_number: 2 - }], - version_number: 4, - committer_id: 'admin', - commit_message: 'Commit message', - created_on_ms: 1592229964515.148, - }, { - commit_type: 'edit', - commit_cmds: [{ - cmd: 'delete_state', - state_name: 'B' - }], - version_number: 5, - committer_id: 'admin', - commit_message: 'Commit message', - created_on_ms: 1592229964515.148, - }, { - commit_type: 'revert', - commit_cmds: [{ - cmd: 'AUTO_revert_version_number', - version_number: 3 - }], - version_number: 6, - committer_id: 'admin', - commit_message: 'Commit message', - created_on_ms: 1592229964515.148, - }, { - commit_type: 'edit', - commit_cmds: [{ - cmd: 'add_state', - state_name: 'D', - content_id_for_state_content: 'content_0', - content_id_for_default_outcome: 'default_outcome_1' - }], - version_number: 7, - committer_id: 'admin', - commit_message: 'Commit message', - created_on_ms: 1592229964515.148, - }, { - commit_type: 'edit', - commit_cmds: [{ - cmd: 'edit_state_property', - state_name: 'D', - new_value: { - content_id: 'content', - html: 'Some text' - }, - old_value: { - content_id: 'content', - html: '' - }, - property_name: 'property' - }], - version_number: 8, - committer_id: 'admin', - commit_message: 'Commit message', - created_on_ms: 1592229964515.148, - }]; + property_name: 'property', + }, + ], + version_number: 8, + committer_id: 'admin', + commit_message: 'Commit message', + created_on_ms: 1592229964515.148, + }, + ]; // Information for mock state data for getDiffGraphData() to be passed to // _getStatesAndMetadata. - let testExplorationData2 = [{ - A: { - contentStr: '', - ruleDests: ['A'] - } - }, { - A: { - contentStr: '', - ruleDests: ['A'] + let testExplorationData2 = [ + { + A: { + contentStr: '', + ruleDests: ['A'], + }, }, - B: { - contentStr: '', - ruleDests: ['B'] - } - }, { - A: { - contentStr: '', - ruleDests: ['A'] + { + A: { + contentStr: '', + ruleDests: ['A'], + }, + B: { + contentStr: '', + ruleDests: ['B'], + }, }, - C: { - contentStr: '', - ruleDests: ['C'] - } - }, { - A: { - contentStr: '', - ruleDests: ['A'] + { + A: { + contentStr: '', + ruleDests: ['A'], + }, + C: { + contentStr: '', + ruleDests: ['C'], + }, }, - B: { - contentStr: '', - ruleDests: ['B'] - } - }, { - A: { - contentStr: '', - ruleDests: ['A'] - } - }, { - A: { - contentStr: '', - ruleDests: ['A'] + { + A: { + contentStr: '', + ruleDests: ['A'], + }, + B: { + contentStr: '', + ruleDests: ['B'], + }, }, - C: { - contentStr: '', - ruleDests: ['C'] - } - }, { - A: { - contentStr: '', - ruleDests: ['A'] + { + A: { + contentStr: '', + ruleDests: ['A'], + }, }, - C: { - contentStr: '', - ruleDests: ['C'] + { + A: { + contentStr: '', + ruleDests: ['A'], + }, + C: { + contentStr: '', + ruleDests: ['C'], + }, }, - D: { - contentStr: '', - ruleDests: ['D'] - } - }, { - A: { - contentStr: '', - ruleDests: ['A'] + { + A: { + contentStr: '', + ruleDests: ['A'], + }, + C: { + contentStr: '', + ruleDests: ['C'], + }, + D: { + contentStr: '', + ruleDests: ['D'], + }, }, - C: { - contentStr: '', - ruleDests: ['C'] + { + A: { + contentStr: '', + ruleDests: ['A'], + }, + C: { + contentStr: '', + ruleDests: ['C'], + }, + D: { + contentStr: 'Some text', + ruleDests: ['D'], + }, }, - D: { - contentStr: 'Some text', - ruleDests: ['D'] - } - }]; + ]; // Tests for getDiffGraphData with reversions. - it( - 'should mark states correctly when there is 1 reversion', - fakeAsync(() => { - vts.init(testSnapshots2); - let nodeData = null; - cvs.getDiffGraphData(1, 5).then((data) => { - nodeData = data.nodes; - expect(nodeData).toEqual({ - 1: { - newestStateName: 'A', - stateProperty: 'unchanged', - originalStateName: 'A' - } - }); + it('should mark states correctly when there is 1 reversion', fakeAsync(() => { + vts.init(testSnapshots2); + let nodeData = null; + cvs.getDiffGraphData(1, 5).then(data => { + nodeData = data.nodes; + expect(nodeData).toEqual({ + 1: { + newestStateName: 'A', + stateProperty: 'unchanged', + originalStateName: 'A', + }, }); + }); - const req = httpTestingController.expectOne( - '/explorehandler/init/0?v=1'); - expect(req.request.method).toEqual('GET'); - req.flush(_getStatesAndMetadata(testExplorationData2[0])); + const req = httpTestingController.expectOne('/explorehandler/init/0?v=1'); + expect(req.request.method).toEqual('GET'); + req.flush(_getStatesAndMetadata(testExplorationData2[0])); - const req2 = httpTestingController.expectOne( - '/explorehandler/init/0?v=5'); - expect(req2.request.method).toEqual('GET'); - req2.flush(_getStatesAndMetadata(testExplorationData2[4])); + const req2 = httpTestingController.expectOne('/explorehandler/init/0?v=5'); + expect(req2.request.method).toEqual('GET'); + req2.flush(_getStatesAndMetadata(testExplorationData2[4])); - flushMicrotasks(); - })); + flushMicrotasks(); + })); - it('should mark states correctly when there is 1 reversion to before v1', - fakeAsync(() => { - vts.init(testSnapshots2); - let nodeData = null; - cvs.getDiffGraphData(3, 5).then((data) => { - nodeData = data.nodes; - expect(nodeData).toEqual({ - 1: { - newestStateName: 'A', - stateProperty: 'unchanged', - originalStateName: 'A' - }, - 2: { - newestStateName: 'B', - stateProperty: 'deleted', - originalStateName: 'C' - } - }); + it('should mark states correctly when there is 1 reversion to before v1', fakeAsync(() => { + vts.init(testSnapshots2); + let nodeData = null; + cvs.getDiffGraphData(3, 5).then(data => { + nodeData = data.nodes; + expect(nodeData).toEqual({ + 1: { + newestStateName: 'A', + stateProperty: 'unchanged', + originalStateName: 'A', + }, + 2: { + newestStateName: 'B', + stateProperty: 'deleted', + originalStateName: 'C', + }, }); + }); - const req = httpTestingController.expectOne( - '/explorehandler/init/0?v=3'); - expect(req.request.method).toEqual('GET'); - req.flush(_getStatesAndMetadata(testExplorationData2[2])); + const req = httpTestingController.expectOne('/explorehandler/init/0?v=3'); + expect(req.request.method).toEqual('GET'); + req.flush(_getStatesAndMetadata(testExplorationData2[2])); - const req2 = httpTestingController.expectOne( - '/explorehandler/init/0?v=5'); - expect(req2.request.method).toEqual('GET'); - req2.flush(_getStatesAndMetadata(testExplorationData2[4])); + const req2 = httpTestingController.expectOne('/explorehandler/init/0?v=5'); + expect(req2.request.method).toEqual('GET'); + req2.flush(_getStatesAndMetadata(testExplorationData2[4])); - flushMicrotasks(); - })); + flushMicrotasks(); + })); - it('should mark states correctly when compared version is a reversion', - fakeAsync(() => { - vts.init(testSnapshots2); - let nodeData = null; - cvs.getDiffGraphData(4, 5).then((data) => { - nodeData = data.nodes; - expect(nodeData).toEqual({ - 1: { - newestStateName: 'A', - stateProperty: 'unchanged', - originalStateName: 'A' - }, - 2: { - newestStateName: 'B', - stateProperty: 'deleted', - originalStateName: 'B' - } - }); + it('should mark states correctly when compared version is a reversion', fakeAsync(() => { + vts.init(testSnapshots2); + let nodeData = null; + cvs.getDiffGraphData(4, 5).then(data => { + nodeData = data.nodes; + expect(nodeData).toEqual({ + 1: { + newestStateName: 'A', + stateProperty: 'unchanged', + originalStateName: 'A', + }, + 2: { + newestStateName: 'B', + stateProperty: 'deleted', + originalStateName: 'B', + }, }); + }); - const req = httpTestingController.expectOne( - '/explorehandler/init/0?v=4'); - expect(req.request.method).toEqual('GET'); - req.flush(_getStatesAndMetadata(testExplorationData2[3])); + const req = httpTestingController.expectOne('/explorehandler/init/0?v=4'); + expect(req.request.method).toEqual('GET'); + req.flush(_getStatesAndMetadata(testExplorationData2[3])); - const req2 = httpTestingController.expectOne( - '/explorehandler/init/0?v=5'); - expect(req2.request.method).toEqual('GET'); - req2.flush(_getStatesAndMetadata(testExplorationData2[4])); + const req2 = httpTestingController.expectOne('/explorehandler/init/0?v=5'); + expect(req2.request.method).toEqual('GET'); + req2.flush(_getStatesAndMetadata(testExplorationData2[4])); - flushMicrotasks(); - })); + flushMicrotasks(); + })); - it( - 'should mark states correctly when there are 2 reversions', - fakeAsync(() => { - vts.init(testSnapshots2); - cvs.getDiffGraphData(5, 8).then((data) => { - expect(data.nodes).toEqual({ - 1: { - newestStateName: 'A', - stateProperty: 'unchanged', - originalStateName: 'A' - }, - 2: { - newestStateName: 'C', - stateProperty: 'added', - originalStateName: 'B' - }, - 3: { - newestStateName: 'D', - stateProperty: 'added', - originalStateName: 'D' - } - }); + it('should mark states correctly when there are 2 reversions', fakeAsync(() => { + vts.init(testSnapshots2); + cvs.getDiffGraphData(5, 8).then(data => { + expect(data.nodes).toEqual({ + 1: { + newestStateName: 'A', + stateProperty: 'unchanged', + originalStateName: 'A', + }, + 2: { + newestStateName: 'C', + stateProperty: 'added', + originalStateName: 'B', + }, + 3: { + newestStateName: 'D', + stateProperty: 'added', + originalStateName: 'D', + }, }); + }); - const req = httpTestingController.expectOne( - '/explorehandler/init/0?v=5'); - expect(req.request.method).toEqual('GET'); - req.flush(_getStatesAndMetadata(testExplorationData2[4])); + const req = httpTestingController.expectOne('/explorehandler/init/0?v=5'); + expect(req.request.method).toEqual('GET'); + req.flush(_getStatesAndMetadata(testExplorationData2[4])); - const req2 = httpTestingController.expectOne( - '/explorehandler/init/0?v=8'); - expect(req2.request.method).toEqual('GET'); - req2.flush(_getStatesAndMetadata(testExplorationData2[7])); + const req2 = httpTestingController.expectOne('/explorehandler/init/0?v=8'); + expect(req2.request.method).toEqual('GET'); + req2.flush(_getStatesAndMetadata(testExplorationData2[7])); - flushMicrotasks(); - })); + flushMicrotasks(); + })); // Represents snapshots and exploration data for tests for links // Only includes information accessed by getDiffGraphData(). - let testSnapshots3: ExplorationSnapshot[] = [{ - commit_type: 'create', - version_number: 1, - committer_id: 'admin', - commit_message: 'Commit message', - created_on_ms: 1592229964515.148, - commit_cmds: [] - }, { - commit_type: 'edit', - commit_cmds: [{ - cmd: 'add_state', - state_name: 'B', - content_id_for_state_content: 'content_5', - content_id_for_default_outcome: 'default_outcome_6' - }], - version_number: 2, - committer_id: 'admin', - commit_message: 'Commit message', - created_on_ms: 1592229964515.148, - }, { - commit_type: 'edit', - commit_cmds: [{ - cmd: 'add_state', - state_name: 'C', - content_id_for_state_content: 'content_7', - content_id_for_default_outcome: 'default_outcome_8' - }], - version_number: 3, - committer_id: 'admin', - commit_message: 'Commit message', - created_on_ms: 1592229964515.148, - }, { - commit_type: 'edit', - commit_cmds: [{ - cmd: 'rename_state', - old_state_name: 'C', - new_state_name: 'D' - }], - version_number: 4, - committer_id: 'admin', - commit_message: 'Commit message', - created_on_ms: 1592229964515.148, - }, { - commit_type: 'edit', - commit_cmds: [{ - cmd: 'edit_state_property', - state_name: 'D', - new_value: { - content_id: 'content', - html: 'Some text' - }, - old_value: { - content_id: 'content', - html: '' - }, - property_name: 'property' - }], - version_number: 5, - committer_id: 'admin', - commit_message: 'Commit message', - created_on_ms: 1592229964515.148, - }, { - commit_type: 'edit', - commit_cmds: [{ - cmd: 'edit_state_property', - state_name: 'D', - new_value: { - content_id: 'content', - html: 'Some text' - }, - old_value: { - content_id: 'content', - html: '' - }, - property_name: 'property' - }], - version_number: 6, - committer_id: 'admin', - commit_message: 'Commit message', - created_on_ms: 1592229964515.148, - }, { - commit_type: 'edit', - commit_cmds: [{ - cmd: 'delete_state', - state_name: 'D' - }], - version_number: 7, - committer_id: 'admin', - commit_message: 'Commit message', - created_on_ms: 1592229964515.148, - }, { - commit_type: 'edit', - commit_cmds: [{ - cmd: 'add_state', - state_name: 'D', - content_id_for_state_content: 'content_3', - content_id_for_default_outcome: 'default_outcome_9' - }], - version_number: 8, - committer_id: 'admin', - commit_message: 'Commit message', - created_on_ms: 1592229964515.148, - }]; - - let testExplorationData3 = [{ - A: { - contentStr: '', - ruleDests: ['A'] - }, - END: { - contentStr: '', - ruleDests: ['END'] - } - }, { - A: { - contentStr: '', - ruleDests: ['B'] - }, - B: { - contentStr: '', - ruleDests: ['END'] + let testSnapshots3: ExplorationSnapshot[] = [ + { + commit_type: 'create', + version_number: 1, + committer_id: 'admin', + commit_message: 'Commit message', + created_on_ms: 1592229964515.148, + commit_cmds: [], }, - END: { - contentStr: '', - ruleDests: ['END'] - } - }, { - A: { - contentStr: '', - ruleDests: ['B', 'C'] - }, - B: { - contentStr: '', - ruleDests: ['END'] - }, - C: { - contentStr: '', - ruleDests: ['A'] + { + commit_type: 'edit', + commit_cmds: [ + { + cmd: 'add_state', + state_name: 'B', + content_id_for_state_content: 'content_5', + content_id_for_default_outcome: 'default_outcome_6', + }, + ], + version_number: 2, + committer_id: 'admin', + commit_message: 'Commit message', + created_on_ms: 1592229964515.148, }, - END: { - contentStr: '', - ruleDests: ['END'] - } - }, { - A: { - contentStr: '', - ruleDests: ['B', 'D'] + { + commit_type: 'edit', + commit_cmds: [ + { + cmd: 'add_state', + state_name: 'C', + content_id_for_state_content: 'content_7', + content_id_for_default_outcome: 'default_outcome_8', + }, + ], + version_number: 3, + committer_id: 'admin', + commit_message: 'Commit message', + created_on_ms: 1592229964515.148, }, - B: { - contentStr: '', - ruleDests: ['END'] + { + commit_type: 'edit', + commit_cmds: [ + { + cmd: 'rename_state', + old_state_name: 'C', + new_state_name: 'D', + }, + ], + version_number: 4, + committer_id: 'admin', + commit_message: 'Commit message', + created_on_ms: 1592229964515.148, }, - D: { - contentStr: '', - ruleDests: ['A'] + { + commit_type: 'edit', + commit_cmds: [ + { + cmd: 'edit_state_property', + state_name: 'D', + new_value: { + content_id: 'content', + html: 'Some text', + }, + old_value: { + content_id: 'content', + html: '', + }, + property_name: 'property', + }, + ], + version_number: 5, + committer_id: 'admin', + commit_message: 'Commit message', + created_on_ms: 1592229964515.148, }, - END: { - contentStr: '', - ruleDests: ['END'] - } - }, { - A: { - contentStr: '', - ruleDests: ['B', 'D'] + { + commit_type: 'edit', + commit_cmds: [ + { + cmd: 'edit_state_property', + state_name: 'D', + new_value: { + content_id: 'content', + html: 'Some text', + }, + old_value: { + content_id: 'content', + html: '', + }, + property_name: 'property', + }, + ], + version_number: 6, + committer_id: 'admin', + commit_message: 'Commit message', + created_on_ms: 1592229964515.148, }, - B: { - contentStr: '', - ruleDests: ['D', 'END'] + { + commit_type: 'edit', + commit_cmds: [ + { + cmd: 'delete_state', + state_name: 'D', + }, + ], + version_number: 7, + committer_id: 'admin', + commit_message: 'Commit message', + created_on_ms: 1592229964515.148, }, - D: { - contentStr: '', - ruleDests: ['A'] + { + commit_type: 'edit', + commit_cmds: [ + { + cmd: 'add_state', + state_name: 'D', + content_id_for_state_content: 'content_3', + content_id_for_default_outcome: 'default_outcome_9', + }, + ], + version_number: 8, + committer_id: 'admin', + commit_message: 'Commit message', + created_on_ms: 1592229964515.148, }, - END: { - contentStr: '', - ruleDests: ['END'] - } - }, { - A: { - contentStr: '', - ruleDests: ['B'] + ]; + + let testExplorationData3 = [ + { + A: { + contentStr: '', + ruleDests: ['A'], + }, + END: { + contentStr: '', + ruleDests: ['END'], + }, }, - B: { - contentStr: '', - ruleDests: ['D', 'END'] + { + A: { + contentStr: '', + ruleDests: ['B'], + }, + B: { + contentStr: '', + ruleDests: ['END'], + }, + END: { + contentStr: '', + ruleDests: ['END'], + }, }, - D: { - contentStr: '', - ruleDests: ['A'] + { + A: { + contentStr: '', + ruleDests: ['B', 'C'], + }, + B: { + contentStr: '', + ruleDests: ['END'], + }, + C: { + contentStr: '', + ruleDests: ['A'], + }, + END: { + contentStr: '', + ruleDests: ['END'], + }, }, - END: { - contentStr: '', - ruleDests: ['END'] - } - }, { - A: { - contentStr: '', - ruleDests: ['B'] + { + A: { + contentStr: '', + ruleDests: ['B', 'D'], + }, + B: { + contentStr: '', + ruleDests: ['END'], + }, + D: { + contentStr: '', + ruleDests: ['A'], + }, + END: { + contentStr: '', + ruleDests: ['END'], + }, }, - B: { - contentStr: '', - ruleDests: ['END'] + { + A: { + contentStr: '', + ruleDests: ['B', 'D'], + }, + B: { + contentStr: '', + ruleDests: ['D', 'END'], + }, + D: { + contentStr: '', + ruleDests: ['A'], + }, + END: { + contentStr: '', + ruleDests: ['END'], + }, }, - END: { - contentStr: '', - ruleDests: ['END'] - } - }, { - A: { - contentStr: '', - ruleDests: ['B'] + { + A: { + contentStr: '', + ruleDests: ['B'], + }, + B: { + contentStr: '', + ruleDests: ['D', 'END'], + }, + D: { + contentStr: '', + ruleDests: ['A'], + }, + END: { + contentStr: '', + ruleDests: ['END'], + }, }, - B: { - contentStr: '', - ruleDests: ['D', 'END'] + { + A: { + contentStr: '', + ruleDests: ['B'], + }, + B: { + contentStr: '', + ruleDests: ['END'], + }, + END: { + contentStr: '', + ruleDests: ['END'], + }, }, - D: { - contentStr: '', - ruleDests: ['B'] + { + A: { + contentStr: '', + ruleDests: ['B'], + }, + B: { + contentStr: '', + ruleDests: ['D', 'END'], + }, + D: { + contentStr: '', + ruleDests: ['B'], + }, + END: { + contentStr: '', + ruleDests: ['END'], + }, }, - END: { - contentStr: '', - ruleDests: ['END'] - } - }]; + ]; it('should correctly display added links', fakeAsync(() => { vts.init(testSnapshots3); let linkData = null; - cvs.getDiffGraphData(1, 2).then((data) => { + cvs.getDiffGraphData(1, 2).then(data => { linkData = data.links; - expect(linkData).toEqual([{ - source: 1, - target: 3, - linkProperty: 'added' - }, { - source: 3, - target: 2, - linkProperty: 'added' - }]); + expect(linkData).toEqual([ + { + source: 1, + target: 3, + linkProperty: 'added', + }, + { + source: 3, + target: 2, + linkProperty: 'added', + }, + ]); }); const req = httpTestingController.expectOne('/explorehandler/init/0?v=1'); expect(req.request.method).toEqual('GET'); req.flush(_getStatesAndMetadata(testExplorationData3[0])); - const req2 = httpTestingController.expectOne( - '/explorehandler/init/0?v=2'); + const req2 = httpTestingController.expectOne('/explorehandler/init/0?v=2'); expect(req2.request.method).toEqual('GET'); req2.flush(_getStatesAndMetadata(testExplorationData3[1])); @@ -1290,37 +1400,42 @@ describe('Compare versions service', () => { it('should correctly display deleted links', fakeAsync(() => { vts.init(testSnapshots3); let linkData = null; - cvs.getDiffGraphData(5, 6).then((data) => { + cvs.getDiffGraphData(5, 6).then(data => { linkData = data.links; - expect(linkData).toEqual([{ - source: 1, - target: 2, - linkProperty: 'unchanged' - }, { - source: 1, - target: 3, - linkProperty: 'deleted' - }, { - source: 2, - target: 3, - linkProperty: 'unchanged' - }, { - source: 2, - target: 4, - linkProperty: 'unchanged' - }, { - source: 3, - target: 1, - linkProperty: 'unchanged' - }]); + expect(linkData).toEqual([ + { + source: 1, + target: 2, + linkProperty: 'unchanged', + }, + { + source: 1, + target: 3, + linkProperty: 'deleted', + }, + { + source: 2, + target: 3, + linkProperty: 'unchanged', + }, + { + source: 2, + target: 4, + linkProperty: 'unchanged', + }, + { + source: 3, + target: 1, + linkProperty: 'unchanged', + }, + ]); }); const req = httpTestingController.expectOne('/explorehandler/init/0?v=5'); expect(req.request.method).toEqual('GET'); req.flush(_getStatesAndMetadata(testExplorationData3[4])); - const req2 = httpTestingController.expectOne( - '/explorehandler/init/0?v=6'); + const req2 = httpTestingController.expectOne('/explorehandler/init/0?v=6'); expect(req2.request.method).toEqual('GET'); req2.flush(_getStatesAndMetadata(testExplorationData3[5])); @@ -1330,37 +1445,42 @@ describe('Compare versions service', () => { it('should correctly display links on renamed states', fakeAsync(() => { vts.init(testSnapshots3); let linkData = null; - cvs.getDiffGraphData(3, 5).then((data) => { + cvs.getDiffGraphData(3, 5).then(data => { linkData = data.links; - expect(linkData).toEqual([{ - source: 1, - target: 2, - linkProperty: 'unchanged' - }, { - source: 1, - target: 3, - linkProperty: 'unchanged' - }, { - source: 2, - target: 3, - linkProperty: 'added' - }, { - source: 2, - target: 4, - linkProperty: 'unchanged' - }, { - source: 3, - target: 1, - linkProperty: 'unchanged' - }]); + expect(linkData).toEqual([ + { + source: 1, + target: 2, + linkProperty: 'unchanged', + }, + { + source: 1, + target: 3, + linkProperty: 'unchanged', + }, + { + source: 2, + target: 3, + linkProperty: 'added', + }, + { + source: 2, + target: 4, + linkProperty: 'unchanged', + }, + { + source: 3, + target: 1, + linkProperty: 'unchanged', + }, + ]); }); const req = httpTestingController.expectOne('/explorehandler/init/0?v=3'); expect(req.request.method).toEqual('GET'); req.flush(_getStatesAndMetadata(testExplorationData3[2])); - const req2 = httpTestingController.expectOne( - '/explorehandler/init/0?v=5'); + const req2 = httpTestingController.expectOne('/explorehandler/init/0?v=5'); expect(req2.request.method).toEqual('GET'); req2.flush(_getStatesAndMetadata(testExplorationData3[4])); @@ -1370,25 +1490,27 @@ describe('Compare versions service', () => { it('should correctly display added, then deleted links', fakeAsync(() => { vts.init(testSnapshots3); let linkData = null; - cvs.getDiffGraphData(2, 7).then((data) => { + cvs.getDiffGraphData(2, 7).then(data => { linkData = data.links; - expect(linkData).toEqual([{ - source: 1, - target: 2, - linkProperty: 'unchanged' - }, { - source: 2, - target: 3, - linkProperty: 'unchanged' - }]); + expect(linkData).toEqual([ + { + source: 1, + target: 2, + linkProperty: 'unchanged', + }, + { + source: 2, + target: 3, + linkProperty: 'unchanged', + }, + ]); }); const req = httpTestingController.expectOne('/explorehandler/init/0?v=2'); expect(req.request.method).toEqual('GET'); req.flush(_getStatesAndMetadata(testExplorationData3[1])); - const req2 = httpTestingController.expectOne( - '/explorehandler/init/0?v=7'); + const req2 = httpTestingController.expectOne('/explorehandler/init/0?v=7'); expect(req2.request.method).toEqual('GET'); req2.flush(_getStatesAndMetadata(testExplorationData3[6])); @@ -1398,37 +1520,42 @@ describe('Compare versions service', () => { it('should correctly display deleted, then added links', fakeAsync(() => { vts.init(testSnapshots3); let linkData = null; - cvs.getDiffGraphData(6, 8).then((data) => { + cvs.getDiffGraphData(6, 8).then(data => { linkData = data.links; - expect(linkData).toEqual([{ - source: 1, - target: 2, - linkProperty: 'unchanged' - }, { - source: 2, - target: 3, - linkProperty: 'unchanged' - }, { - source: 2, - target: 4, - linkProperty: 'unchanged' - }, { - source: 3, - target: 1, - linkProperty: 'deleted' - }, { - source: 3, - target: 2, - linkProperty: 'added' - }]); + expect(linkData).toEqual([ + { + source: 1, + target: 2, + linkProperty: 'unchanged', + }, + { + source: 2, + target: 3, + linkProperty: 'unchanged', + }, + { + source: 2, + target: 4, + linkProperty: 'unchanged', + }, + { + source: 3, + target: 1, + linkProperty: 'deleted', + }, + { + source: 3, + target: 2, + linkProperty: 'added', + }, + ]); }); const req = httpTestingController.expectOne('/explorehandler/init/0?v=6'); expect(req.request.method).toEqual('GET'); req.flush(_getStatesAndMetadata(testExplorationData3[5])); - const req2 = httpTestingController.expectOne( - '/explorehandler/init/0?v=8'); + const req2 = httpTestingController.expectOne('/explorehandler/init/0?v=8'); expect(req2.request.method).toEqual('GET'); req2.flush(_getStatesAndMetadata(testExplorationData3[7])); diff --git a/core/templates/pages/exploration-editor-page/history-tab/services/compare-versions.service.ts b/core/templates/pages/exploration-editor-page/history-tab/services/compare-versions.service.ts index 07a236a73b24..696c041e9cfc 100644 --- a/core/templates/pages/exploration-editor-page/history-tab/services/compare-versions.service.ts +++ b/core/templates/pages/exploration-editor-page/history-tab/services/compare-versions.service.ts @@ -16,15 +16,25 @@ * @fileoverview Service to compare versions of explorations. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { ExplorationChange } from 'domain/exploration/exploration-draft.model'; -import { ExplorationMetadata, ExplorationMetadataObjectFactory } from 'domain/exploration/ExplorationMetadataObjectFactory'; -import { ReadOnlyExplorationBackendApiService } from 'domain/exploration/read-only-exploration-backend-api.service'; -import { StateObjectsDict, StatesObjectFactory } from 'domain/exploration/StatesObjectFactory'; -import { ExplorationDataService } from 'pages/exploration-editor-page/services/exploration-data.service'; -import { ExplorationDiffService, StateData, StateLink } from 'pages/exploration-editor-page/services/exploration-diff.service'; -import { VersionTreeService } from './version-tree.service'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {ExplorationChange} from 'domain/exploration/exploration-draft.model'; +import { + ExplorationMetadata, + ExplorationMetadataObjectFactory, +} from 'domain/exploration/ExplorationMetadataObjectFactory'; +import {ReadOnlyExplorationBackendApiService} from 'domain/exploration/read-only-exploration-backend-api.service'; +import { + StateObjectsDict, + StatesObjectFactory, +} from 'domain/exploration/StatesObjectFactory'; +import {ExplorationDataService} from 'pages/exploration-editor-page/services/exploration-data.service'; +import { + ExplorationDiffService, + StateData, + StateLink, +} from 'pages/exploration-editor-page/services/exploration-diff.service'; +import {VersionTreeService} from './version-tree.service'; export interface CompareVersionData { nodes: StateData; @@ -39,30 +49,29 @@ export interface CompareVersionData { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class CompareVersionsService { constructor( private explorationDataService: ExplorationDataService, private explorationDiffService: ExplorationDiffService, private explorationMetadataObjectFactory: ExplorationMetadataObjectFactory, - private readOnlyExplorationBackendApiService: - ReadOnlyExplorationBackendApiService, + private readOnlyExplorationBackendApiService: ReadOnlyExplorationBackendApiService, private statesObjectFactory: StatesObjectFactory, - private versionTreeService: VersionTreeService, - ) { } + private versionTreeService: VersionTreeService + ) {} /** - * Constructs the combined list of changes needed to get from v1 to v2. - * - * v1, v2 are version numbers. v1 must be an ancestor of v2. - * directionForwards is true if changes are compared in increasing version - * number, and false if changes are compared in decreasing version number. - */ + * Constructs the combined list of changes needed to get from v1 to v2. + * + * v1, v2 are version numbers. v1 must be an ancestor of v2. + * directionForwards is true if changes are compared in increasing version + * number, and false if changes are compared in decreasing version number. + */ _getCombinedChangeList( - v1: number, - v2: number, - directionForwards: boolean + v1: number, + v2: number, + directionForwards: boolean ): ExplorationChange[] { let _treeParents = this.versionTreeService.getVersionTree(); @@ -78,7 +87,7 @@ export class CompareVersionsService { // The full changelist that is applied to go from v1 to v2. let combinedChangeList: ExplorationChange[] = []; - versionPath.forEach((version) => { + versionPath.forEach(version => { let changeListForVersion = this.versionTreeService.getChangeList(version); if (!directionForwards) { changeListForVersion.reverse(); @@ -112,19 +121,22 @@ export class CompareVersionsService { * Should be called after this.versionTreeService.init() is called. * Should satisfy v1 < v2. */ - getDiffGraphData( - v1: number, - v2: number - ): Promise { + getDiffGraphData(v1: number, v2: number): Promise { if (v1 > v2) { throw new Error('Tried to compare v1 > v2.'); } - return Promise.all([{ - v1Data: this.readOnlyExplorationBackendApiService.loadExplorationAsync( - this.explorationDataService.explorationId, v1), - v2Data: this.readOnlyExplorationBackendApiService.loadExplorationAsync( - this.explorationDataService.explorationId, v2) - }]).then(async(response) => { + return Promise.all([ + { + v1Data: this.readOnlyExplorationBackendApiService.loadExplorationAsync( + this.explorationDataService.explorationId, + v1 + ), + v2Data: this.readOnlyExplorationBackendApiService.loadExplorationAsync( + this.explorationDataService.explorationId, + v2 + ), + }, + ]).then(async response => { let v1StatesDict = (await response[0].v1Data).exploration.states; let v2StatesDict = (await response[0].v2Data).exploration.states; let v1MetadataDict = (await response[0].v1Data).exploration_metadata; @@ -133,49 +145,61 @@ export class CompareVersionsService { // Track changes from v1 to LCA, and then from LCA to v2. let lca = this.versionTreeService.findLCA(v1, v2); - let v1States = this.statesObjectFactory.createFromBackendDict( - v1StatesDict).getStateObjects(); - let v2States = this.statesObjectFactory.createFromBackendDict( - v2StatesDict).getStateObjects(); + let v1States = this.statesObjectFactory + .createFromBackendDict(v1StatesDict) + .getStateObjects(); + let v2States = this.statesObjectFactory + .createFromBackendDict(v2StatesDict) + .getStateObjects(); let diffGraphData = this.explorationDiffService.getDiffGraphData( - v1States, v2States, [{ - changeList: this._getCombinedChangeList(lca, v1, false), - directionForwards: false - }, { - changeList: this._getCombinedChangeList(lca, v2, true), - directionForwards: true - }] + v1States, + v2States, + [ + { + changeList: this._getCombinedChangeList(lca, v1, false), + directionForwards: false, + }, + { + changeList: this._getCombinedChangeList(lca, v2, true), + directionForwards: true, + }, + ] ); - let v1Metadata = ( + let v1Metadata = this.explorationMetadataObjectFactory.createFromBackendDict( v1MetadataDict - ) - ); - let v2Metadata = ( + ); + let v2Metadata = this.explorationMetadataObjectFactory.createFromBackendDict( v2MetadataDict - ) - ); + ); return { nodes: diffGraphData.nodes, links: diffGraphData.links, finalStateIds: diffGraphData.finalStateIds, - v1InitStateId: diffGraphData.originalStateIds[ - (await response[0].v1Data).exploration.init_state_name], - v2InitStateId: diffGraphData.stateIds[ - (await response[0].v2Data).exploration.init_state_name], + v1InitStateId: + diffGraphData.originalStateIds[ + (await response[0].v1Data).exploration.init_state_name + ], + v2InitStateId: + diffGraphData.stateIds[ + (await response[0].v2Data).exploration.init_state_name + ], v1States: v1States, v2States: v2States, v1Metadata: v1Metadata, - v2Metadata: v2Metadata + v2Metadata: v2Metadata, }; }); } } -angular.module('oppia').factory( - 'CompareVersionsService', - downgradeInjectable(CompareVersionsService)); +angular + .module('oppia') + .factory( + 'CompareVersionsService', + downgradeInjectable(CompareVersionsService) + ); diff --git a/core/templates/pages/exploration-editor-page/history-tab/services/version-tree.service.spec.ts b/core/templates/pages/exploration-editor-page/history-tab/services/version-tree.service.spec.ts index 30abe0f2af1e..04ba4be05158 100644 --- a/core/templates/pages/exploration-editor-page/history-tab/services/version-tree.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/history-tab/services/version-tree.service.spec.ts @@ -16,111 +16,139 @@ * @fileoverview Unit tests for the Versions Tree Service. */ -import { ExplorationSnapshot, VersionTreeService } from 'pages/exploration-editor-page/history-tab/services/version-tree.service'; +import { + ExplorationSnapshot, + VersionTreeService, +} from 'pages/exploration-editor-page/history-tab/services/version-tree.service'; describe('Versions tree service', () => { describe('versions tree service', () => { let vts: VersionTreeService; - var snapshots: ExplorationSnapshot[] = [{ - commit_type: 'create', - version_number: 1, - committer_id: 'admin', - commit_message: 'Commit message', - created_on_ms: 1592229964515.148, - commit_cmds: [] - }, { - commit_type: 'edit', - commit_cmds: [{ - cmd: 'add_state', - state_name: 'B', - content_id_for_state_content: 'content_0', - content_id_for_default_outcome: 'default_outcome_1' - }, { - cmd: 'rename_state', - new_state_name: 'A', - old_state_name: 'First State' - }], - version_number: 2, - committer_id: 'admin', - commit_message: 'Commit message', - created_on_ms: 1592229964515.148, - }, { - commit_type: 'edit', - commit_cmds: [{ - cmd: 'rename_state', - new_state_name: 'C', - old_state_name: 'B' - }], - version_number: 3, - committer_id: 'admin', - commit_message: 'Commit message', - created_on_ms: 1592229964515.148, - }, { - commit_type: 'revert', - commit_cmds: [{ + var snapshots: ExplorationSnapshot[] = [ + { + commit_type: 'create', + version_number: 1, + committer_id: 'admin', + commit_message: 'Commit message', + created_on_ms: 1592229964515.148, + commit_cmds: [], + }, + { + commit_type: 'edit', + commit_cmds: [ + { + cmd: 'add_state', + state_name: 'B', + content_id_for_state_content: 'content_0', + content_id_for_default_outcome: 'default_outcome_1', + }, + { + cmd: 'rename_state', + new_state_name: 'A', + old_state_name: 'First State', + }, + ], version_number: 2, - cmd: 'AUTO_revert_version_number' - }], - version_number: 4, - committer_id: 'admin', - commit_message: 'Commit message', - created_on_ms: 1592229964515.148, - }, { - commit_type: 'edit', - commit_cmds: [{ - cmd: 'delete_state', - state_name: 'B' - }, { - cmd: 'rename_state', - new_state_name: 'D', - old_state_name: 'A' - }], - version_number: 5, - committer_id: 'admin', - commit_message: 'Commit message', - created_on_ms: 1592229964515.148, - }, { - commit_type: 'revert', - commit_cmds: [{ + committer_id: 'admin', + commit_message: 'Commit message', + created_on_ms: 1592229964515.148, + }, + { + commit_type: 'edit', + commit_cmds: [ + { + cmd: 'rename_state', + new_state_name: 'C', + old_state_name: 'B', + }, + ], version_number: 3, - cmd: 'AUTO_revert_version_number' - }], - version_number: 6, - committer_id: 'admin', - commit_message: 'Commit message', - created_on_ms: 1592229964515.148, - }, { - commit_type: 'edit', - commit_cmds: [{ - cmd: 'add_state', - state_name: 'D', - content_id_for_state_content: 'content_5', - content_id_for_default_outcome: 'default_outcome_6' - }], - version_number: 7, - committer_id: 'admin', - commit_message: 'Commit message', - created_on_ms: 1592229964515.148, - }, { - commit_type: 'edit', - commit_cmds: [{ - cmd: 'edit_state_property', - state_name: 'D', - new_value: { - html: 'Some text', - content_id: '2' - }, - old_value: { - html: '', - content_id: '1' - }, - property_name: 'property' - }], - version_number: 8, - committer_id: 'admin', - commit_message: 'Commit message', - created_on_ms: 1592229964515.148, - }]; + committer_id: 'admin', + commit_message: 'Commit message', + created_on_ms: 1592229964515.148, + }, + { + commit_type: 'revert', + commit_cmds: [ + { + version_number: 2, + cmd: 'AUTO_revert_version_number', + }, + ], + version_number: 4, + committer_id: 'admin', + commit_message: 'Commit message', + created_on_ms: 1592229964515.148, + }, + { + commit_type: 'edit', + commit_cmds: [ + { + cmd: 'delete_state', + state_name: 'B', + }, + { + cmd: 'rename_state', + new_state_name: 'D', + old_state_name: 'A', + }, + ], + version_number: 5, + committer_id: 'admin', + commit_message: 'Commit message', + created_on_ms: 1592229964515.148, + }, + { + commit_type: 'revert', + commit_cmds: [ + { + version_number: 3, + cmd: 'AUTO_revert_version_number', + }, + ], + version_number: 6, + committer_id: 'admin', + commit_message: 'Commit message', + created_on_ms: 1592229964515.148, + }, + { + commit_type: 'edit', + commit_cmds: [ + { + cmd: 'add_state', + state_name: 'D', + content_id_for_state_content: 'content_5', + content_id_for_default_outcome: 'default_outcome_6', + }, + ], + version_number: 7, + committer_id: 'admin', + commit_message: 'Commit message', + created_on_ms: 1592229964515.148, + }, + { + commit_type: 'edit', + commit_cmds: [ + { + cmd: 'edit_state_property', + state_name: 'D', + new_value: { + html: 'Some text', + content_id: '2', + }, + old_value: { + html: '', + content_id: '1', + }, + property_name: 'property', + }, + ], + version_number: 8, + committer_id: 'admin', + commit_message: 'Commit message', + created_on_ms: 1592229964515.148, + }, + ]; beforeEach(() => { vts = new VersionTreeService(); @@ -140,7 +168,7 @@ describe('Versions tree service', () => { 5: 4, 6: 3, 7: 6, - 8: 7 + 8: 7, }; expect(vts.getVersionTree()).toEqual(expectedParents); }); @@ -155,20 +183,23 @@ describe('Versions tree service', () => { expect(vts.findLCA(2, 4)).toBe(2); }); - it('should throw error if we try access elements which ' + - 'are not in version tree when finding lowes common ancestor', () => { - vts.init(snapshots); + it( + 'should throw error if we try access elements which ' + + 'are not in version tree when finding lowes common ancestor', + () => { + vts.init(snapshots); - // Checking path 1, Here 10 is not in the list. - expect(() => { - vts.findLCA(10, 1); - }).toThrowError('Could not find parent of 10'); + // Checking path 1, Here 10 is not in the list. + expect(() => { + vts.findLCA(10, 1); + }).toThrowError('Could not find parent of 10'); - // Checking path 2, Here 11 is not in the list. - expect(() => { - vts.findLCA(1, 11); - }).toThrowError('Could not find parent of 11'); - }); + // Checking path 2, Here 11 is not in the list. + expect(() => { + vts.findLCA(1, 11); + }).toThrowError('Could not find parent of 11'); + } + ); it('should get correct change list', () => { // Prechecks: If we try to access snapshots without initializing them. @@ -179,41 +210,51 @@ describe('Versions tree service', () => { expect(() => { vts.getChangeList(1); }).toThrowError('Tried to retrieve change list of version 1'); - expect(vts.getChangeList(2)).toEqual([{ - cmd: 'add_state', - state_name: 'B', - content_id_for_state_content: 'content_0', - content_id_for_default_outcome: 'default_outcome_1' - }, { - cmd: 'rename_state', - new_state_name: 'A', - old_state_name: 'First State' - }]); - expect(vts.getChangeList(4)).toEqual([{ - cmd: 'AUTO_revert_version_number', - version_number: 2 - }]); - expect(vts.getChangeList(5)).toEqual([{ - cmd: 'delete_state', - state_name: 'B' - }, { - cmd: 'rename_state', - new_state_name: 'D', - old_state_name: 'A' - }]); - expect(vts.getChangeList(8)).toEqual([{ - cmd: 'edit_state_property', - state_name: 'D', - new_value: { - html: 'Some text', - content_id: '2' + expect(vts.getChangeList(2)).toEqual([ + { + cmd: 'add_state', + state_name: 'B', + content_id_for_state_content: 'content_0', + content_id_for_default_outcome: 'default_outcome_1', + }, + { + cmd: 'rename_state', + new_state_name: 'A', + old_state_name: 'First State', + }, + ]); + expect(vts.getChangeList(4)).toEqual([ + { + cmd: 'AUTO_revert_version_number', + version_number: 2, + }, + ]); + expect(vts.getChangeList(5)).toEqual([ + { + cmd: 'delete_state', + state_name: 'B', + }, + { + cmd: 'rename_state', + new_state_name: 'D', + old_state_name: 'A', }, - old_value: { - html: '', - content_id: '1' + ]); + expect(vts.getChangeList(8)).toEqual([ + { + cmd: 'edit_state_property', + state_name: 'D', + new_value: { + html: 'Some text', + content_id: '2', + }, + old_value: { + html: '', + content_id: '1', + }, + property_name: 'property', }, - property_name: 'property' - }]); + ]); }); }); }); diff --git a/core/templates/pages/exploration-editor-page/history-tab/services/version-tree.service.ts b/core/templates/pages/exploration-editor-page/history-tab/services/version-tree.service.ts index 030c2f229f04..67d2e855fdf9 100644 --- a/core/templates/pages/exploration-editor-page/history-tab/services/version-tree.service.ts +++ b/core/templates/pages/exploration-editor-page/history-tab/services/version-tree.service.ts @@ -19,18 +19,21 @@ import cloneDeep from 'lodash/cloneDeep'; -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; -import { RevertChangeList, ExplorationChange } from 'domain/exploration/exploration-draft.model'; +import { + RevertChangeList, + ExplorationChange, +} from 'domain/exploration/exploration-draft.model'; export interface ExplorationSnapshot { - 'commit_message': string; - 'committer_id': string; - 'commit_type': string; - 'version_number': number; - 'created_on_ms': number; - 'commit_cmds': ExplorationChange[]; + commit_message: string; + committer_id: string; + commit_type: string; + version_number: number; + created_on_ms: number; + commit_cmds: ExplorationChange[]; } interface ExplorationSnapshots { @@ -38,7 +41,7 @@ interface ExplorationSnapshots { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class VersionTreeService { // These properties are initialized using init method @@ -61,12 +64,17 @@ export class VersionTreeService { for (var versionNum = 2; versionNum <= numberOfVersions; versionNum++) { if (this._snapshots[versionNum].commit_type === 'revert') { for ( - var i = 0; i < this._snapshots[versionNum].commit_cmds.length; i++) { - if (this._snapshots[versionNum].commit_cmds[i].cmd === - 'AUTO_revert_version_number') { - this._treeParents[versionNum] = - (this._snapshots[versionNum].commit_cmds[i] as RevertChangeList) - .version_number; + var i = 0; + i < this._snapshots[versionNum].commit_cmds.length; + i++ + ) { + if ( + this._snapshots[versionNum].commit_cmds[i].cmd === + 'AUTO_revert_version_number' + ) { + this._treeParents[versionNum] = ( + this._snapshots[versionNum].commit_cmds[i] as RevertChangeList + ).version_number; } } } else { @@ -158,5 +166,6 @@ export class VersionTreeService { } } -angular.module('oppia').factory( - 'VersionTreeService', downgradeInjectable(VersionTreeService)); +angular + .module('oppia') + .factory('VersionTreeService', downgradeInjectable(VersionTreeService)); diff --git a/core/templates/pages/exploration-editor-page/improvements-tab/improvements-tab.component.spec.ts b/core/templates/pages/exploration-editor-page/improvements-tab/improvements-tab.component.spec.ts index 76f8ac288120..72f442c124c3 100644 --- a/core/templates/pages/exploration-editor-page/improvements-tab/improvements-tab.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/improvements-tab/improvements-tab.component.spec.ts @@ -17,23 +17,23 @@ * the improvements tab. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { fakeAsync, flushMicrotasks } from '@angular/core/testing'; -import { ImprovementsTabComponent } from './improvements-tab.component'; -import { ExplorationTaskType } from 'domain/improvements/exploration-task.model'; -import { HighBounceRateTask } from 'domain/improvements/high-bounce-rate-task.model'; -import { IneffectiveFeedbackLoopTask } from 'domain/improvements/ineffective-feedback-loop-task.model'; -import { NeedsGuidingResponsesTask } from 'domain/improvements/needs-guiding-response-task.model'; -import { SuccessiveIncorrectAnswersTask } from 'domain/improvements/successive-incorrect-answers-task.model'; -import { StateStats } from 'domain/statistics/state-stats-model'; -import { ExplorationStats } from 'domain/statistics/exploration-stats.model'; -import { ExplorationImprovementsTaskRegistryService } from 'services/exploration-improvements-task-registry.service'; -import { ExplorationImprovementsService } from 'services/exploration-improvements.service'; -import { RouterService } from '../services/router.service'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { FormsModule } from '@angular/forms'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {fakeAsync, flushMicrotasks} from '@angular/core/testing'; +import {ImprovementsTabComponent} from './improvements-tab.component'; +import {ExplorationTaskType} from 'domain/improvements/exploration-task.model'; +import {HighBounceRateTask} from 'domain/improvements/high-bounce-rate-task.model'; +import {IneffectiveFeedbackLoopTask} from 'domain/improvements/ineffective-feedback-loop-task.model'; +import {NeedsGuidingResponsesTask} from 'domain/improvements/needs-guiding-response-task.model'; +import {SuccessiveIncorrectAnswersTask} from 'domain/improvements/successive-incorrect-answers-task.model'; +import {StateStats} from 'domain/statistics/state-stats-model'; +import {ExplorationStats} from 'domain/statistics/exploration-stats.model'; +import {ExplorationImprovementsTaskRegistryService} from 'services/exploration-improvements-task-registry.service'; +import {ExplorationImprovementsService} from 'services/exploration-improvements.service'; +import {RouterService} from '../services/router.service'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {FormsModule} from '@angular/forms'; describe('Improvements tab', () => { let component: ImprovementsTabComponent; @@ -52,34 +52,34 @@ describe('Improvements tab', () => { class MockNgbModal { open() { return { - result: Promise.resolve() + result: Promise.resolve(), }; } } beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - FormsModule, - ], - declarations: [ - ImprovementsTabComponent - ], + imports: [HttpClientTestingModule, FormsModule], + declarations: [ImprovementsTabComponent], providers: [ { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); const emptyStateStats = new StateStats(0, 0, 0, 0, 0, 0); - const emptyExpStats = new ExplorationStats('id', 1, 0, 0, 0, new Map([ - ['Introduction', emptyStateStats], - ])); + const emptyExpStats = new ExplorationStats( + 'id', + 1, + 0, + 0, + 0, + new Map([['Introduction', emptyStateStats]]) + ); beforeEach(() => { fixture = TestBed.createComponent(ImprovementsTabComponent); @@ -87,20 +87,26 @@ describe('Improvements tab', () => { routerService = TestBed.inject(RouterService); taskRegistryService = TestBed.inject( - ExplorationImprovementsTaskRegistryService); + ExplorationImprovementsTaskRegistryService + ); explorationImprovementsService = TestBed.inject( ExplorationImprovementsService ); expStatsSpy = spyOn(taskRegistryService, 'getExplorationStats'); - hbrTasksSpy = ( - spyOn(taskRegistryService, 'getOpenHighBounceRateTasks')); - iflTasksSpy = ( - spyOn(taskRegistryService, 'getOpenIneffectiveFeedbackLoopTasks')); - ngrTasksSpy = ( - spyOn(taskRegistryService, 'getOpenNeedsGuidingResponsesTasks')); - siaTasksSpy = ( - spyOn(taskRegistryService, 'getOpenSuccessiveIncorrectAnswersTasks')); + hbrTasksSpy = spyOn(taskRegistryService, 'getOpenHighBounceRateTasks'); + iflTasksSpy = spyOn( + taskRegistryService, + 'getOpenIneffectiveFeedbackLoopTasks' + ); + ngrTasksSpy = spyOn( + taskRegistryService, + 'getOpenNeedsGuidingResponsesTasks' + ); + siaTasksSpy = spyOn( + taskRegistryService, + 'getOpenSuccessiveIncorrectAnswersTasks' + ); stateTasksSpy = spyOn(taskRegistryService, 'getStateTasks'); allStateTasksSpy = spyOn(taskRegistryService, 'getAllStateTasks'); @@ -128,32 +134,44 @@ describe('Improvements tab', () => { }); describe('Post-initialization', () => { - const newTaskEntryBackendDict = ( - (taskType: T, isOpen: boolean) => ({ - entity_type: 'exploration', - entity_id: 'eid', - entity_version: 1, - task_type: taskType, - target_type: 'state', - target_id: 'Introduction', - status: (isOpen ? 'open' : 'obsolete'), - issue_description: null, - resolver_username: null, - resolved_on_msecs: null, - })); + const newTaskEntryBackendDict = ( + taskType: T, + isOpen: boolean + ) => ({ + entity_type: 'exploration', + entity_id: 'eid', + entity_version: 1, + task_type: taskType, + target_type: 'state', + target_id: 'Introduction', + status: isOpen ? 'open' : 'obsolete', + issue_description: null, + resolver_username: null, + resolved_on_msecs: null, + }); - const newHbrTask = (isOpen = true) => new HighBounceRateTask( - newTaskEntryBackendDict('high_bounce_rate', isOpen)); - const newIflTask = (isOpen = true) => new IneffectiveFeedbackLoopTask( - newTaskEntryBackendDict('ineffective_feedback_loop', isOpen)); - const newNgrTask = (isOpen = true) => new NeedsGuidingResponsesTask( - newTaskEntryBackendDict('needs_guiding_responses', isOpen)); - const newSiaTask = (isOpen = true) => new SuccessiveIncorrectAnswersTask( - newTaskEntryBackendDict('successive_incorrect_answers', isOpen)); + const newHbrTask = (isOpen = true) => + new HighBounceRateTask( + newTaskEntryBackendDict('high_bounce_rate', isOpen) + ); + const newIflTask = (isOpen = true) => + new IneffectiveFeedbackLoopTask( + newTaskEntryBackendDict('ineffective_feedback_loop', isOpen) + ); + const newNgrTask = (isOpen = true) => + new NeedsGuidingResponsesTask( + newTaskEntryBackendDict('needs_guiding_responses', isOpen) + ); + const newSiaTask = (isOpen = true) => + new SuccessiveIncorrectAnswersTask( + newTaskEntryBackendDict('successive_incorrect_answers', isOpen) + ); beforeEach(() => { - spyOn(explorationImprovementsService, 'isImprovementsTabEnabledAsync') - .and.returnValue(Promise.resolve(true)); + spyOn( + explorationImprovementsService, + 'isImprovementsTabEnabledAsync' + ).and.returnValue(Promise.resolve(true)); }); it('should report the number of exp-level tasks', fakeAsync(() => { @@ -209,7 +227,8 @@ describe('Improvements tab', () => { const numStarts = 100; const numCompletions = 60; expStatsSpy.and.returnValue( - new ExplorationStats('id', 1, numStarts, 0, numCompletions, new Map())); + new ExplorationStats('id', 1, numStarts, 0, numCompletions, new Map()) + ); component.ngOnInit(); flushMicrotasks(); @@ -221,17 +240,31 @@ describe('Improvements tab', () => { it('should provide the correct state retention', fakeAsync(() => { const totalHitCount = 100; const numCompletions = 60; - const stateStats = ( - new StateStats(0, 0, totalHitCount, 0, 0, numCompletions)); - expStatsSpy.and.returnValue(new ExplorationStats( - 'id', 1, 0, 0, 0, new Map([['Introduction', stateStats]]))); + const stateStats = new StateStats( + 0, + 0, + totalHitCount, + 0, + 0, + numCompletions + ); + expStatsSpy.and.returnValue( + new ExplorationStats( + 'id', + 1, + 0, + 0, + 0, + new Map([['Introduction', stateStats]]) + ) + ); allStateTasksSpy.and.returnValue([ { stateName: 'Introduction', ngrTask: newNgrTask(), siaTask: newSiaTask(), supportingStats: {stateStats}, - } + }, ]); component.ngOnInit(); @@ -242,10 +275,18 @@ describe('Improvements tab', () => { it('should provide the number of open cards in a state', fakeAsync(() => { expStatsSpy.and.returnValue( - new ExplorationStats('id', 1, 0, 0, 0, new Map([ - ['Introduction', emptyStateStats], - ['End', emptyStateStats], - ]))); + new ExplorationStats( + 'id', + 1, + 0, + 0, + 0, + new Map([ + ['Introduction', emptyStateStats], + ['End', emptyStateStats], + ]) + ) + ); const stateTasks = { Introduction: { stateName: 'Introduction', @@ -271,10 +312,18 @@ describe('Improvements tab', () => { it('should toggle the visibility of state tasks', fakeAsync(() => { expStatsSpy.and.returnValue( - new ExplorationStats('id', 1, 0, 0, 0, new Map([ - ['Introduction', emptyStateStats], - ['End', emptyStateStats], - ]))); + new ExplorationStats( + 'id', + 1, + 0, + 0, + 0, + new Map([ + ['Introduction', emptyStateStats], + ['End', emptyStateStats], + ]) + ) + ); allStateTasksSpy.and.returnValue([ { stateName: 'Introduction', @@ -312,43 +361,39 @@ describe('Improvements tab', () => { expect(component.getExplorationHealth()).toEqual('warning'); })); - it('should report heath as warning if only IFL task exists', - fakeAsync(() => { - iflTasksSpy.and.returnValue([newIflTask()]); + it('should report heath as warning if only IFL task exists', fakeAsync(() => { + iflTasksSpy.and.returnValue([newIflTask()]); - component.ngOnInit(); - flushMicrotasks(); + component.ngOnInit(); + flushMicrotasks(); - expect(component.getExplorationHealth()).toEqual('warning'); - })); + expect(component.getExplorationHealth()).toEqual('warning'); + })); - it('should report heath as critical if NGR task exists', - fakeAsync(() => { - ngrTasksSpy.and.returnValue([newNgrTask()]); + it('should report heath as critical if NGR task exists', fakeAsync(() => { + ngrTasksSpy.and.returnValue([newNgrTask()]); - component.ngOnInit(); - flushMicrotasks(); + component.ngOnInit(); + flushMicrotasks(); - expect(component.getExplorationHealth()).toEqual('critical'); - })); + expect(component.getExplorationHealth()).toEqual('critical'); + })); - it('should report heath as warning if only SIA task exists', - fakeAsync(() => { - siaTasksSpy.and.returnValue([newSiaTask()]); + it('should report heath as warning if only SIA task exists', fakeAsync(() => { + siaTasksSpy.and.returnValue([newSiaTask()]); - component.ngOnInit(); - flushMicrotasks(); + component.ngOnInit(); + flushMicrotasks(); - expect(component.getExplorationHealth()).toEqual('warning'); - })); + expect(component.getExplorationHealth()).toEqual('warning'); + })); - it('should report health as healthy if zero tasks exist', - fakeAsync(() => { - component.ngOnInit(); - flushMicrotasks(); + it('should report health as healthy if zero tasks exist', fakeAsync(() => { + component.ngOnInit(); + flushMicrotasks(); - expect(component.getExplorationHealth()).toEqual('healthy'); - })); + expect(component.getExplorationHealth()).toEqual('healthy'); + })); }); }); }); diff --git a/core/templates/pages/exploration-editor-page/improvements-tab/improvements-tab.component.ts b/core/templates/pages/exploration-editor-page/improvements-tab/improvements-tab.component.ts index 9dcc12815d41..1e8c37e06bc9 100644 --- a/core/templates/pages/exploration-editor-page/improvements-tab/improvements-tab.component.ts +++ b/core/templates/pages/exploration-editor-page/improvements-tab/improvements-tab.component.ts @@ -18,20 +18,23 @@ * have returned true. The component should be disabled by an ngIf until then. */ -import { Component, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { HighBounceRateTask } from 'domain/improvements/high-bounce-rate-task.model'; -import { ImprovementsConstants } from 'domain/improvements/improvements.constants'; -import { IneffectiveFeedbackLoopTask } from 'domain/improvements/ineffective-feedback-loop-task.model'; -import { NeedsGuidingResponsesTask } from 'domain/improvements/needs-guiding-response-task.model'; -import { SuccessiveIncorrectAnswersTask } from 'domain/improvements/successive-incorrect-answers-task.model'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { ExplorationImprovementsTaskRegistryService, StateTasks } from 'services/exploration-improvements-task-registry.service'; -import { RouterService } from '../services/router.service'; +import {Component, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {HighBounceRateTask} from 'domain/improvements/high-bounce-rate-task.model'; +import {ImprovementsConstants} from 'domain/improvements/improvements.constants'; +import {IneffectiveFeedbackLoopTask} from 'domain/improvements/ineffective-feedback-loop-task.model'; +import {NeedsGuidingResponsesTask} from 'domain/improvements/needs-guiding-response-task.model'; +import {SuccessiveIncorrectAnswersTask} from 'domain/improvements/successive-incorrect-answers-task.model'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import { + ExplorationImprovementsTaskRegistryService, + StateTasks, +} from 'services/exploration-improvements-task-registry.service'; +import {RouterService} from '../services/router.service'; @Component({ selector: 'oppia-improvements-tab', - templateUrl: './improvements-tab.component.html' + templateUrl: './improvements-tab.component.html', }) export class ImprovementsTabComponent implements OnInit { stateRetentions: Map; @@ -46,11 +49,10 @@ export class ImprovementsTabComponent implements OnInit { allStateTasks: StateTasks[]; constructor( - private explorationImprovementsTaskRegistryService: - ExplorationImprovementsTaskRegistryService, - private routerService: RouterService, - private urlInterpolationService: UrlInterpolationService, - ) { } + private explorationImprovementsTaskRegistryService: ExplorationImprovementsTaskRegistryService, + private routerService: RouterService, + private urlInterpolationService: UrlInterpolationService + ) {} navigateToStateEditor(stateName: string): void { this.routerService.navigateToMainTab(stateName); @@ -58,8 +60,11 @@ export class ImprovementsTabComponent implements OnInit { getNumTasks(): number { return ( - this.hbrTasks.length + this.iflTasks.length + this.ngrTasks.length + - this.siaTasks.length); + this.hbrTasks.length + + this.iflTasks.length + + this.ngrTasks.length + + this.siaTasks.length + ); } getNumExpLevelTasks(): number { @@ -71,11 +76,11 @@ export class ImprovementsTabComponent implements OnInit { } getNumCardLevelTasks(): number { - return (this.ngrTasks.length + this.siaTasks.length); + return this.ngrTasks.length + this.siaTasks.length; } hasCriticalTasks(): boolean { - return (this.ngrTasks.length > 0); + return this.ngrTasks.length > 0; } getExplorationHealth(): string { @@ -89,9 +94,8 @@ export class ImprovementsTabComponent implements OnInit { } getNumCardLevelTasksForState(stateName: string): number { - const stateTasks = ( - this.explorationImprovementsTaskRegistryService.getStateTasks( - stateName)); + const stateTasks = + this.explorationImprovementsTaskRegistryService.getStateTasks(stateName); const ngrTask = stateTasks.ngrTask; const siaTask = stateTasks.siaTask; return (ngrTask.isOpen() ? 1 : 0) + (siaTask.isOpen() ? 1 : 0); @@ -102,9 +106,10 @@ export class ImprovementsTabComponent implements OnInit { } toggleStateTasks(stateName: string): Map { - return ( - this.stateVisibility.set( - stateName, !this.stateVisibility.get(stateName))); + return this.stateVisibility.set( + stateName, + !this.stateVisibility.get(stateName) + ); } getStateRetentionPercent(stateName: string): string { @@ -112,55 +117,58 @@ export class ImprovementsTabComponent implements OnInit { } ngOnInit(): void { - this.timeMachineImageUrl = ( - this.urlInterpolationService - .getStaticImageUrl('/icons/time_machine.svg')); - - const expStats = ( - this.explorationImprovementsTaskRegistryService.getExplorationStats()); - - this.completionRate = ( - (expStats && expStats.numStarts > 0) ? - (expStats.numCompletions / expStats.numStarts) : 0); - - this.completionRateAsPercent = ( - Math.round(100.0 * this.completionRate) + '%'); - - this.hbrTasks = ( - this.explorationImprovementsTaskRegistryService - .getOpenHighBounceRateTasks()); - this.iflTasks = ( - this.explorationImprovementsTaskRegistryService - .getOpenIneffectiveFeedbackLoopTasks()); - this.ngrTasks = ( - this.explorationImprovementsTaskRegistryService - .getOpenNeedsGuidingResponsesTasks()); - this.siaTasks = ( - this.explorationImprovementsTaskRegistryService - .getOpenSuccessiveIncorrectAnswersTasks()); - - this.allStateTasks = ( - this.explorationImprovementsTaskRegistryService.getAllStateTasks()); - - const namesOfStatesWithTasks = ( - this.allStateTasks.map(stateTasks => stateTasks.stateName)); - - this.stateRetentions = ( - new Map(namesOfStatesWithTasks.map(stateName => { + this.timeMachineImageUrl = this.urlInterpolationService.getStaticImageUrl( + '/icons/time_machine.svg' + ); + + const expStats = + this.explorationImprovementsTaskRegistryService.getExplorationStats(); + + this.completionRate = + expStats && expStats.numStarts > 0 + ? expStats.numCompletions / expStats.numStarts + : 0; + + this.completionRateAsPercent = + Math.round(100.0 * this.completionRate) + '%'; + + this.hbrTasks = + this.explorationImprovementsTaskRegistryService.getOpenHighBounceRateTasks(); + this.iflTasks = + this.explorationImprovementsTaskRegistryService.getOpenIneffectiveFeedbackLoopTasks(); + this.ngrTasks = + this.explorationImprovementsTaskRegistryService.getOpenNeedsGuidingResponsesTasks(); + this.siaTasks = + this.explorationImprovementsTaskRegistryService.getOpenSuccessiveIncorrectAnswersTasks(); + + this.allStateTasks = + this.explorationImprovementsTaskRegistryService.getAllStateTasks(); + + const namesOfStatesWithTasks = this.allStateTasks.map( + stateTasks => stateTasks.stateName + ); + + this.stateRetentions = new Map( + namesOfStatesWithTasks.map(stateName => { const stateStats = expStats.getStateStats(stateName); const numCompletions = stateStats.numCompletions; const totalHitCount = stateStats.totalHitCount; const retentionRate = Math.round( - totalHitCount ? (100.0 * numCompletions / totalHitCount) : 0); + totalHitCount ? (100.0 * numCompletions) / totalHitCount : 0 + ); return [stateName, retentionRate]; - }))); + }) + ); - this.stateVisibility = ( - new Map(namesOfStatesWithTasks.map(stateName => [stateName, true]))); + this.stateVisibility = new Map( + namesOfStatesWithTasks.map(stateName => [stateName, true]) + ); } } -angular.module('oppia').directive('oppiaImprovementsTab', - downgradeComponent({ - component: ImprovementsTabComponent - }) as angular.IDirectiveFactory); +angular.module('oppia').directive( + 'oppiaImprovementsTab', + downgradeComponent({ + component: ImprovementsTabComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-editor-page/improvements-tab/needs-guiding-responses-task.component.spec.ts b/core/templates/pages/exploration-editor-page/improvements-tab/needs-guiding-responses-task.component.spec.ts index 41452645fd33..d642a3a080ac 100644 --- a/core/templates/pages/exploration-editor-page/improvements-tab/needs-guiding-responses-task.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/improvements-tab/needs-guiding-responses-task.component.spec.ts @@ -17,17 +17,17 @@ * the improvements tab. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { FormsModule } from '@angular/forms'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { NeedsGuidingResponsesTask } from 'domain/improvements/needs-guiding-response-task.model'; -import { SupportingStateStats } from 'services/exploration-improvements-task-registry.service'; -import { RouterService } from '../services/router.service'; -import { NeedsGuidingResponsesTaskComponent } from './needs-guiding-responses-task.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {FormsModule} from '@angular/forms'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {NeedsGuidingResponsesTask} from 'domain/improvements/needs-guiding-response-task.model'; +import {SupportingStateStats} from 'services/exploration-improvements-task-registry.service'; +import {RouterService} from '../services/router.service'; +import {NeedsGuidingResponsesTaskComponent} from './needs-guiding-responses-task.component'; -describe('NeedsGuidingResponsesTask component', function() { +describe('NeedsGuidingResponsesTask component', function () { let component: NeedsGuidingResponsesTaskComponent; let fixture: ComponentFixture; let routerService: RouterService; @@ -42,43 +42,37 @@ describe('NeedsGuidingResponsesTask component', function() { totalHitCount: 0, firstHitCount: 0, numTimesSolutionViewed: 0, - numCompletions: 0 + numCompletions: 0, }, cstPlaythroughIssues: [], eqPlaythroughIssues: [], - misPlaythroughIssues: [] + misPlaythroughIssues: [], } as SupportingStateStats; class MockNgbModal { open() { return { - result: Promise.resolve() + result: Promise.resolve(), }; } } beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - FormsModule, - ], - declarations: [ - NeedsGuidingResponsesTaskComponent - ], + imports: [HttpClientTestingModule, FormsModule], + declarations: [NeedsGuidingResponsesTaskComponent], providers: [ { provide: NgbModal, - useClass: MockNgbModal - } + useClass: MockNgbModal, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent( - NeedsGuidingResponsesTaskComponent); + fixture = TestBed.createComponent(NeedsGuidingResponsesTaskComponent); component = fixture.componentInstance; routerService = TestBed.inject(RouterService); @@ -90,8 +84,10 @@ describe('NeedsGuidingResponsesTask component', function() { it('should configure sorted tiles viz based on input task and stats', () => { expect(component.sortedTilesData).toEqual(stats.answerStats); - expect(component.sortedTilesOptions) - .toEqual({header: '', use_percentages: true}); + expect(component.sortedTilesOptions).toEqual({ + header: '', + use_percentages: true, + }); expect(component.sortedTilesTotalFrequency).toEqual(totalAnswersCount); }); diff --git a/core/templates/pages/exploration-editor-page/improvements-tab/needs-guiding-responses-task.component.ts b/core/templates/pages/exploration-editor-page/improvements-tab/needs-guiding-responses-task.component.ts index 9cb1ea772405..b5f896b0cc53 100644 --- a/core/templates/pages/exploration-editor-page/improvements-tab/needs-guiding-responses-task.component.ts +++ b/core/templates/pages/exploration-editor-page/improvements-tab/needs-guiding-responses-task.component.ts @@ -16,47 +16,46 @@ * @fileoverview Component for the improvements tab of the exploration editor. */ -import { Component, Input, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { AnswerStats } from 'domain/exploration/answer-stats.model'; -import { NeedsGuidingResponsesTask } from 'domain/improvements/needs-guiding-response-task.model'; -import { SupportingStateStats } from 'services/exploration-improvements-task-registry.service'; -import { RouterService } from '../services/router.service'; +import {Component, Input, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {AnswerStats} from 'domain/exploration/answer-stats.model'; +import {NeedsGuidingResponsesTask} from 'domain/improvements/needs-guiding-response-task.model'; +import {SupportingStateStats} from 'services/exploration-improvements-task-registry.service'; +import {RouterService} from '../services/router.service'; @Component({ selector: 'oppia-needs-guiding-responses-task', - templateUrl: './needs-guiding-responses-task.component.html' + templateUrl: './needs-guiding-responses-task.component.html', }) export class NeedsGuidingResponsesTaskComponent implements OnInit { - // These properties below are initialized using Angular lifecycle hooks - // where we need to do non-null assertion. For more information see - // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 - @Input() stats!: SupportingStateStats; - @Input() task!: NeedsGuidingResponsesTask; - sortedTilesData!: AnswerStats[]; - sortedTilesTotalFrequency!: number; - sortedTilesOptions!: { - header: string; - use_percentages: boolean; - }; + // These properties below are initialized using Angular lifecycle hooks + // where we need to do non-null assertion. For more information see + // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 + @Input() stats!: SupportingStateStats; + @Input() task!: NeedsGuidingResponsesTask; + sortedTilesData!: AnswerStats[]; + sortedTilesTotalFrequency!: number; + sortedTilesOptions!: { + header: string; + use_percentages: boolean; + }; - constructor( - private routerService: RouterService, - ) { } + constructor(private routerService: RouterService) {} - navigateToStateEditor(): void { - this.routerService.navigateToMainTab(this.task.targetId); - } + navigateToStateEditor(): void { + this.routerService.navigateToMainTab(this.task.targetId); + } - ngOnInit(): void { - this.sortedTilesData = [...this.stats.answerStats]; - this.sortedTilesOptions = {header: '', use_percentages: true}; - this.sortedTilesTotalFrequency = ( - this.stats.stateStats.totalAnswersCount); - } + ngOnInit(): void { + this.sortedTilesData = [...this.stats.answerStats]; + this.sortedTilesOptions = {header: '', use_percentages: true}; + this.sortedTilesTotalFrequency = this.stats.stateStats.totalAnswersCount; + } } -angular.module('oppia').directive('oppiaNeedsGuidingResponsesTask', - downgradeComponent({ - component: NeedsGuidingResponsesTaskComponent - }) as angular.IDirectiveFactory); +angular.module('oppia').directive( + 'oppiaNeedsGuidingResponsesTask', + downgradeComponent({ + component: NeedsGuidingResponsesTaskComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-editor-page/modal-templates/confirm-discard-changes-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/modal-templates/confirm-discard-changes-modal.component.spec.ts index 2a9f967212ea..cd4279ed5f22 100644 --- a/core/templates/pages/exploration-editor-page/modal-templates/confirm-discard-changes-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/modal-templates/confirm-discard-changes-modal.component.spec.ts @@ -16,10 +16,10 @@ * @fileoverview Unit tests for confirm discard changes modal. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmDiscardChangesModalComponent } from './confirm-discard-changes-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmDiscardChangesModalComponent} from './confirm-discard-changes-modal.component'; describe('Collection editor save modal component', () => { let fixture: ComponentFixture; @@ -27,13 +27,9 @@ describe('Collection editor save modal component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - ConfirmDiscardChangesModalComponent - ], - providers: [ - NgbActiveModal - ], - schemas: [NO_ERRORS_SCHEMA] + declarations: [ConfirmDiscardChangesModalComponent], + providers: [NgbActiveModal], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); diff --git a/core/templates/pages/exploration-editor-page/modal-templates/confirm-discard-changes-modal.component.ts b/core/templates/pages/exploration-editor-page/modal-templates/confirm-discard-changes-modal.component.ts index f1a108d709ac..2b9833c6055b 100644 --- a/core/templates/pages/exploration-editor-page/modal-templates/confirm-discard-changes-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/modal-templates/confirm-discard-changes-modal.component.ts @@ -16,20 +16,16 @@ * @fileoverview Component for discarding exploration changes. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-confirm-discard-changes-modal', - templateUrl: './confirm-discard-changes-modal.component.html' + templateUrl: './confirm-discard-changes-modal.component.html', }) - -export class ConfirmDiscardChangesModalComponent - extends ConfirmOrCancelModal { - constructor( - private ngbActiveModal: NgbActiveModal - ) { +export class ConfirmDiscardChangesModalComponent extends ConfirmOrCancelModal { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } } diff --git a/core/templates/pages/exploration-editor-page/modal-templates/confirm-leave-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/modal-templates/confirm-leave-modal.component.spec.ts index b157dbdbeba9..c5dbac502f17 100644 --- a/core/templates/pages/exploration-editor-page/modal-templates/confirm-leave-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/modal-templates/confirm-leave-modal.component.spec.ts @@ -16,24 +16,20 @@ * @fileoverview Unit tests for confirm leave modal component. */ -import { NO_ERRORS_SCHEMA, ElementRef } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmLeaveModalComponent } from './confirm-leave-modal.component'; +import {NO_ERRORS_SCHEMA, ElementRef} from '@angular/core'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmLeaveModalComponent} from './confirm-leave-modal.component'; -describe('Confirm Leave Modal Component', function() { +describe('Confirm Leave Modal Component', function () { let component: ConfirmLeaveModalComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - ConfirmLeaveModalComponent - ], - providers: [ - NgbActiveModal - ], - schemas: [NO_ERRORS_SCHEMA] + declarations: [ConfirmLeaveModalComponent], + providers: [NgbActiveModal], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -47,8 +43,7 @@ describe('Confirm Leave Modal Component', function() { }); it('should focus on the header after loading', () => { - const confirmHeaderRef = new ElementRef( - document.createElement('h4')); + const confirmHeaderRef = new ElementRef(document.createElement('h4')); component.confirmHeaderRef = confirmHeaderRef; spyOn(confirmHeaderRef.nativeElement, 'focus'); diff --git a/core/templates/pages/exploration-editor-page/modal-templates/confirm-leave-modal.component.ts b/core/templates/pages/exploration-editor-page/modal-templates/confirm-leave-modal.component.ts index c1c4f121badb..4dc9042d36dc 100644 --- a/core/templates/pages/exploration-editor-page/modal-templates/confirm-leave-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/modal-templates/confirm-leave-modal.component.ts @@ -16,21 +16,17 @@ * @fileoverview Component for confirm leave modal. */ -import { Component, ViewChild, ElementRef } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component, ViewChild, ElementRef} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-confirm-leave-modal', - templateUrl: './confirm-leave-modal.component.html' + templateUrl: './confirm-leave-modal.component.html', }) - -export class ConfirmLeaveModalComponent - extends ConfirmOrCancelModal { - @ViewChild('confirmHeader',) confirmHeaderRef!: ElementRef; - constructor( - private ngbActiveModal: NgbActiveModal - ) { +export class ConfirmLeaveModalComponent extends ConfirmOrCancelModal { + @ViewChild('confirmHeader') confirmHeaderRef!: ElementRef; + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } diff --git a/core/templates/pages/exploration-editor-page/modal-templates/editor-reloading-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/modal-templates/editor-reloading-modal.component.spec.ts index 0a82da494a4b..09c9638b0322 100644 --- a/core/templates/pages/exploration-editor-page/modal-templates/editor-reloading-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/modal-templates/editor-reloading-modal.component.spec.ts @@ -16,11 +16,16 @@ * @fileoverview Unit tests for EditorReloadingModalController. */ -import { ComponentFixture, waitForAsync, TestBed, fakeAsync, tick } from - '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { EditorReloadingModalComponent } from './editor-reloading-modal.component'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { + ComponentFixture, + waitForAsync, + TestBed, + fakeAsync, + tick, +} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {EditorReloadingModalComponent} from './editor-reloading-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; class MockActiveModal { close(): void { @@ -39,16 +44,14 @@ describe('Editor Reloading Modal Controller', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - EditorReloadingModalComponent, - ], + declarations: [EditorReloadingModalComponent], providers: [ { provide: NgbActiveModal, - useClass: MockActiveModal - } + useClass: MockActiveModal, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); diff --git a/core/templates/pages/exploration-editor-page/modal-templates/editor-reloading-modal.component.ts b/core/templates/pages/exploration-editor-page/modal-templates/editor-reloading-modal.component.ts index 11f07bb06b76..b51ba646917d 100644 --- a/core/templates/pages/exploration-editor-page/modal-templates/editor-reloading-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/modal-templates/editor-reloading-modal.component.ts @@ -16,20 +16,19 @@ * @fileoverview Component for editor reloading modal. */ -import { Component, OnInit } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component, OnInit} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-editor-reloading-modal', - templateUrl: './editor-reloading-modal.component.html' + templateUrl: './editor-reloading-modal.component.html', }) - export class EditorReloadingModalComponent - extends ConfirmOrCancelModal implements OnInit { - constructor( - private ngbActiveModal: NgbActiveModal - ) { + extends ConfirmOrCancelModal + implements OnInit +{ + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } diff --git a/core/templates/pages/exploration-editor-page/modal-templates/exploration-metadata-diff-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/modal-templates/exploration-metadata-diff-modal.component.spec.ts index c6938608a5f3..f5da5d567028 100644 --- a/core/templates/pages/exploration-editor-page/modal-templates/exploration-metadata-diff-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/modal-templates/exploration-metadata-diff-modal.component.spec.ts @@ -16,13 +16,23 @@ * @fileoverview Unit tests for ExplorationMetadataDiffModalComponent. */ -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { HistoryTabYamlConversionService } from '../services/history-tab-yaml-conversion.service'; -import { ExplorationMetadataDiffModalComponent } from './exploration-metadata-diff-modal.component'; -import { ExplorationMetadata, ExplorationMetadataBackendDict, ExplorationMetadataObjectFactory } from 'domain/exploration/ExplorationMetadataObjectFactory'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {HistoryTabYamlConversionService} from '../services/history-tab-yaml-conversion.service'; +import {ExplorationMetadataDiffModalComponent} from './exploration-metadata-diff-modal.component'; +import { + ExplorationMetadata, + ExplorationMetadataBackendDict, + ExplorationMetadataObjectFactory, +} from 'domain/exploration/ExplorationMetadataObjectFactory'; describe('Exploration Metadata Diff Modal Component', () => { let explorationMetadataObjectFactory: ExplorationMetadataObjectFactory; @@ -34,26 +44,17 @@ describe('Exploration Metadata Diff Modal Component', () => { let headers = { leftPane: 'header 1', - rightPane: 'header 2' + rightPane: 'header 2', }; let newMetadata: ExplorationMetadata | null; let oldMetadata: ExplorationMetadata | null; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - ExplorationMetadataDiffModalComponent - ], - providers: [ - NgbActiveModal, - HistoryTabYamlConversionService - ], - schemas: [ - NO_ERRORS_SCHEMA - ], + imports: [HttpClientTestingModule], + declarations: [ExplorationMetadataDiffModalComponent], + providers: [NgbActiveModal, HistoryTabYamlConversionService], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -61,9 +62,11 @@ describe('Exploration Metadata Diff Modal Component', () => { fixture = TestBed.createComponent(ExplorationMetadataDiffModalComponent); component = fixture.componentInstance; explorationMetadataObjectFactory = TestBed.inject( - ExplorationMetadataObjectFactory); + ExplorationMetadataObjectFactory + ); historyTabYamlConversionService = TestBed.inject( - HistoryTabYamlConversionService); + HistoryTabYamlConversionService + ); }); beforeEach(() => { @@ -80,7 +83,7 @@ describe('Exploration Metadata Diff Modal Component', () => { param_specs: {}, param_changes: [], auto_tts_enabled: false, - edits_allowed: true + edits_allowed: true, }; newExplorationMetadataBackendDict = { title: 'Exploration', @@ -95,13 +98,15 @@ describe('Exploration Metadata Diff Modal Component', () => { param_specs: {}, param_changes: [], auto_tts_enabled: false, - edits_allowed: true + edits_allowed: true, }; - newMetadata = explorationMetadataObjectFactory - .createFromBackendDict(newExplorationMetadataBackendDict); - oldMetadata = explorationMetadataObjectFactory - .createFromBackendDict(oldExplorationMetadataBackendDict); + newMetadata = explorationMetadataObjectFactory.createFromBackendDict( + newExplorationMetadataBackendDict + ); + oldMetadata = explorationMetadataObjectFactory.createFromBackendDict( + oldExplorationMetadataBackendDict + ); component.headers = headers; component.newMetadata = newMetadata; @@ -110,7 +115,8 @@ describe('Exploration Metadata Diff Modal Component', () => { it('should evaluate yaml strings object', fakeAsync(() => { spyOn( - historyTabYamlConversionService, 'getYamlStringFromStateOrMetadata' + historyTabYamlConversionService, + 'getYamlStringFromStateOrMetadata' ).and.resolveTo('Yaml data'); expect(component.yamlStrs.leftPane).toBe(''); diff --git a/core/templates/pages/exploration-editor-page/modal-templates/exploration-metadata-diff-modal.component.ts b/core/templates/pages/exploration-editor-page/modal-templates/exploration-metadata-diff-modal.component.ts index 73c625e324b8..9e711f663002 100644 --- a/core/templates/pages/exploration-editor-page/modal-templates/exploration-metadata-diff-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/modal-templates/exploration-metadata-diff-modal.component.ts @@ -17,12 +17,12 @@ * exploration versions. */ -import { Input, OnInit } from '@angular/core'; -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { ExplorationMetadata } from 'domain/exploration/ExplorationMetadataObjectFactory'; -import { HistoryTabYamlConversionService } from '../services/history-tab-yaml-conversion.service'; +import {Input, OnInit} from '@angular/core'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {ExplorationMetadata} from 'domain/exploration/ExplorationMetadataObjectFactory'; +import {HistoryTabYamlConversionService} from '../services/history-tab-yaml-conversion.service'; interface headersAndYamlStrs { leftPane: string; @@ -41,7 +41,9 @@ interface mergeviewOptions { templateUrl: './exploration-metadata-diff-modal.component.html', }) export class ExplorationMetadataDiffModalComponent - extends ConfirmOrCancelModal implements OnInit { + extends ConfirmOrCancelModal + implements OnInit +{ // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -53,14 +55,14 @@ export class ExplorationMetadataDiffModalComponent @Input() headers!: headersAndYamlStrs; yamlStrs: headersAndYamlStrs = { leftPane: '', - rightPane: '' + rightPane: '', }; CODEMIRROR_MERGEVIEW_OPTIONS: mergeviewOptions = { lineNumbers: true, readOnly: true, mode: 'yaml', - viewportMargin: 100 + viewportMargin: 100, }; constructor( @@ -73,13 +75,13 @@ export class ExplorationMetadataDiffModalComponent ngOnInit(): void { this.historyTabYamlConversionService .getYamlStringFromStateOrMetadata(this.oldMetadata) - .then((result) => { + .then(result => { this.yamlStrs.leftPane = result; }); this.historyTabYamlConversionService .getYamlStringFromStateOrMetadata(this.newMetadata) - .then((result) => { + .then(result => { this.yamlStrs.rightPane = result; }); } diff --git a/core/templates/pages/exploration-editor-page/modal-templates/exploration-metadata-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/modal-templates/exploration-metadata-modal.component.spec.ts index 44ef1345b178..a440bb559a9b 100644 --- a/core/templates/pages/exploration-editor-page/modal-templates/exploration-metadata-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/modal-templates/exploration-metadata-modal.component.spec.ts @@ -16,21 +16,28 @@ * @fileoverview Unit tests for ExplorationMetadataModalComponent. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, flush, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { MatChipInputEvent } from '@angular/material/chips'; -import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { StateObjectsBackendDict } from 'domain/exploration/StatesObjectFactory'; -import { AlertsService } from 'services/alerts.service'; -import { ExplorationCategoryService } from '../services/exploration-category.service'; -import { ExplorationDataService } from '../services/exploration-data.service'; -import { ExplorationLanguageCodeService } from '../services/exploration-language-code.service'; -import { ExplorationObjectiveService } from '../services/exploration-objective.service'; -import { ExplorationStatesService } from '../services/exploration-states.service'; -import { ExplorationTagsService } from '../services/exploration-tags.service'; -import { ExplorationTitleService } from '../services/exploration-title.service'; -import { ExplorationMetadataModalComponent } from './exploration-metadata-modal.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + flush, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {MatChipInputEvent} from '@angular/material/chips'; +import {NgbActiveModal, NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {StateObjectsBackendDict} from 'domain/exploration/StatesObjectFactory'; +import {AlertsService} from 'services/alerts.service'; +import {ExplorationCategoryService} from '../services/exploration-category.service'; +import {ExplorationDataService} from '../services/exploration-data.service'; +import {ExplorationLanguageCodeService} from '../services/exploration-language-code.service'; +import {ExplorationObjectiveService} from '../services/exploration-objective.service'; +import {ExplorationStatesService} from '../services/exploration-states.service'; +import {ExplorationTagsService} from '../services/exploration-tags.service'; +import {ExplorationTitleService} from '../services/exploration-title.service'; +import {ExplorationMetadataModalComponent} from './exploration-metadata-modal.component'; class MockActiveModal { close(): void { @@ -63,9 +70,7 @@ describe('Exploration Metadata Modal Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - ExplorationMetadataModalComponent - ], + declarations: [ExplorationMetadataModalComponent], providers: [ { provide: ExplorationDataService, @@ -73,16 +78,16 @@ describe('Exploration Metadata Modal Component', () => { explorationId: 0, autosaveChangeListAsync() { return; - } - } + }, + }, }, { provide: NgbActiveModal, - useClass: MockActiveModal + useClass: MockActiveModal, }, { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, AlertsService, ExplorationCategoryService, @@ -92,7 +97,7 @@ describe('Exploration Metadata Modal Component', () => { ExplorationTagsService, ExplorationTitleService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -103,9 +108,9 @@ describe('Exploration Metadata Modal Component', () => { explorationCategoryService = TestBed.inject(ExplorationCategoryService); explorationLanguageCodeService = TestBed.inject( - ExplorationLanguageCodeService); - explorationObjectiveService = TestBed.inject( - ExplorationObjectiveService); + ExplorationLanguageCodeService + ); + explorationObjectiveService = TestBed.inject(ExplorationObjectiveService); explorationStatesService = TestBed.inject(ExplorationStatesService); explorationTagsService = TestBed.inject(ExplorationTagsService); explorationTitleService = TestBed.inject(ExplorationTitleService); @@ -127,87 +132,89 @@ describe('Exploration Metadata Modal Component', () => { component.add({ value: 'name', input: { - value: '' - } - } as MatChipInputEvent); - tick(); - - expect(explorationTagsService.displayed).toEqual(['name']); - })); - - it('should not add same exploration editor tags' + - 'when user enter same tag again', fakeAsync(() => { - component.explorationTags = []; - explorationTagsService.displayed = []; - component.add({ - value: 'name', - input: { - value: '' - } + value: '', + }, } as MatChipInputEvent); tick(); expect(explorationTagsService.displayed).toEqual(['name']); - - // When user try to enter same tag again. - component.add({ - value: 'name', - input: { - value: '' - } - } as MatChipInputEvent); - tick(); - expect(explorationTagsService.displayed).toEqual(['name']); })); - it('should be able to add multiple exploration editor tags', + it( + 'should not add same exploration editor tags' + + 'when user enter same tag again', fakeAsync(() => { component.explorationTags = []; explorationTagsService.displayed = []; - component.add({ - value: 'tag-one', + value: 'name', input: { - value: '' - } + value: '', + }, } as MatChipInputEvent); tick(); - component.add({ - value: 'tag-two', - input: { - value: '' - } - } as MatChipInputEvent); - tick(); + expect(explorationTagsService.displayed).toEqual(['name']); + // When user try to enter same tag again. component.add({ - value: 'tag-three', + value: 'name', input: { - value: '' - } + value: '', + }, } as MatChipInputEvent); tick(); + expect(explorationTagsService.displayed).toEqual(['name']); + }) + ); - expect(explorationTagsService.displayed).toEqual( - ['tag-one', 'tag-two', 'tag-three']); - })); + it('should be able to add multiple exploration editor tags', fakeAsync(() => { + component.explorationTags = []; + explorationTagsService.displayed = []; - it('should be able to remove multiple exploration editor tags', - fakeAsync(() => { - component.explorationTags = ['tag-one', 'tag-two', 'tag-three']; - explorationTagsService.displayed = ['tag-one', 'tag-two', 'tag-three']; + component.add({ + value: 'tag-one', + input: { + value: '', + }, + } as MatChipInputEvent); + tick(); - component.remove('tag-two'); - tick(); + component.add({ + value: 'tag-two', + input: { + value: '', + }, + } as MatChipInputEvent); + tick(); - component.remove('tag-three'); - tick(); + component.add({ + value: 'tag-three', + input: { + value: '', + }, + } as MatChipInputEvent); + tick(); + + expect(explorationTagsService.displayed).toEqual([ + 'tag-one', + 'tag-two', + 'tag-three', + ]); + })); - expect(explorationTagsService.displayed).toEqual( - ['tag-one']); - })); + it('should be able to remove multiple exploration editor tags', fakeAsync(() => { + component.explorationTags = ['tag-one', 'tag-two', 'tag-three']; + explorationTagsService.displayed = ['tag-one', 'tag-two', 'tag-three']; + component.remove('tag-two'); + tick(); + + component.remove('tag-three'); + tick(); + + expect(explorationTagsService.displayed).toEqual(['tag-one']); + })); it('should be able to remove exploration editor tags', fakeAsync(() => { component.explorationTags = []; @@ -216,14 +223,14 @@ describe('Exploration Metadata Modal Component', () => { component.add({ value: 'first', input: { - value: '' - } + value: '', + }, } as MatChipInputEvent); component.add({ value: 'second', input: { - value: '' - } + value: '', + }, } as MatChipInputEvent); component.remove('second'); @@ -231,57 +238,63 @@ describe('Exploration Metadata Modal Component', () => { expect(explorationTagsService.displayed).toEqual(['first']); })); - it('should initialize component properties after Component is initialized', - fakeAsync(() => { - let TOTAL_CATEGORIES = 42; - expect(component.objectiveHasBeenPreviouslyEdited).toBe(false); - expect(component.requireTitleToBeSpecified).toBe(true); - expect(component.requireObjectiveToBeSpecified).toBe(true); - expect(component.requireCategoryToBeSpecified).toBe(true); - expect(component.askForLanguageCheck).toBe(true); - expect(component.askForTags).toBe(true); - expect(component.CATEGORY_LIST_FOR_SELECT2.length) - .toBe(TOTAL_CATEGORIES); - - component.filterChoices(''); + it('should initialize component properties after Component is initialized', fakeAsync(() => { + let TOTAL_CATEGORIES = 42; + expect(component.objectiveHasBeenPreviouslyEdited).toBe(false); + expect(component.requireTitleToBeSpecified).toBe(true); + expect(component.requireObjectiveToBeSpecified).toBe(true); + expect(component.requireCategoryToBeSpecified).toBe(true); + expect(component.askForLanguageCheck).toBe(true); + expect(component.askForTags).toBe(true); + expect(component.CATEGORY_LIST_FOR_SELECT2.length).toBe(TOTAL_CATEGORIES); - component.explorationTags = []; - component.add({ - value: 'shivam', - input: { - value: '' - } - } as MatChipInputEvent); - component.remove('shivam'); - tick(); + component.filterChoices(''); - component.filterChoices('filterChoices'); - component.updateCategoryListWithUserData(); - expect(component.newCategory).toEqual({ - id: 'filterChoices', - text: 'filterChoices', - }); - })); - - it('should save all exploration metadata values when it contains title,' + - ' category and objective', fakeAsync(() => { - spyOn(ngbActiveModal, 'close').and.stub(); - - explorationCategoryService.displayed = 'New Category'; - explorationLanguageCodeService.displayed = 'es'; - explorationObjectiveService.displayed = ( - 'Exp Objective is ready to be saved'); - explorationTagsService.displayed = ['h1']; - explorationTitleService.displayed = 'New Title'; - expect(component.isSavingAllowed()).toBe(true); - component.save(); - - tick(500); - flush(); - - expect(ngbActiveModal.close).toHaveBeenCalledWith([ - 'title', 'objective', 'category', 'language', 'tags']); + component.explorationTags = []; + component.add({ + value: 'shivam', + input: { + value: '', + }, + } as MatChipInputEvent); + component.remove('shivam'); + tick(); + + component.filterChoices('filterChoices'); + component.updateCategoryListWithUserData(); + expect(component.newCategory).toEqual({ + id: 'filterChoices', + text: 'filterChoices', + }); })); + + it( + 'should save all exploration metadata values when it contains title,' + + ' category and objective', + fakeAsync(() => { + spyOn(ngbActiveModal, 'close').and.stub(); + + explorationCategoryService.displayed = 'New Category'; + explorationLanguageCodeService.displayed = 'es'; + explorationObjectiveService.displayed = + 'Exp Objective is ready to be saved'; + explorationTagsService.displayed = ['h1']; + explorationTitleService.displayed = 'New Title'; + expect(component.isSavingAllowed()).toBe(true); + component.save(); + + tick(500); + flush(); + + expect(ngbActiveModal.close).toHaveBeenCalledWith([ + 'title', + 'objective', + 'category', + 'language', + 'tags', + ]); + }) + ); }); describe('when all metadata are not filled', () => { @@ -292,9 +305,9 @@ describe('Exploration Metadata Modal Component', () => { alertsService = TestBed.inject(AlertsService); explorationCategoryService = TestBed.inject(ExplorationCategoryService); explorationLanguageCodeService = TestBed.inject( - ExplorationLanguageCodeService); - explorationObjectiveService = TestBed.inject( - ExplorationObjectiveService); + ExplorationLanguageCodeService + ); + explorationObjectiveService = TestBed.inject(ExplorationObjectiveService); explorationStatesService = TestBed.inject(ExplorationStatesService); explorationTagsService = TestBed.inject(ExplorationTagsService); explorationTitleService = TestBed.inject(ExplorationTitleService); @@ -309,53 +322,65 @@ describe('Exploration Metadata Modal Component', () => { fixture.detectChanges(); }); - it('should not save exploration metadata values when title is not' + - ' provided', fakeAsync(() => { - spyOn(ngbActiveModal, 'close').and.stub(); - spyOn(alertsService, 'addWarning'); - expect(component.isSavingAllowed()).toBe(false); - - component.save(); - tick(500); - - expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Please specify a title'); - expect(ngbActiveModal.close).not.toHaveBeenCalled(); - })); - - it('should not save exploration metadata values when objective is not' + - ' provided', fakeAsync(() => { - spyOn(ngbActiveModal, 'close').and.stub(); - spyOn(alertsService, 'addWarning'); + it( + 'should not save exploration metadata values when title is not' + + ' provided', + fakeAsync(() => { + spyOn(ngbActiveModal, 'close').and.stub(); + spyOn(alertsService, 'addWarning'); + expect(component.isSavingAllowed()).toBe(false); + + component.save(); + tick(500); + + expect(alertsService.addWarning).toHaveBeenCalledWith( + 'Please specify a title' + ); + expect(ngbActiveModal.close).not.toHaveBeenCalled(); + }) + ); + + it( + 'should not save exploration metadata values when objective is not' + + ' provided', + fakeAsync(() => { + spyOn(ngbActiveModal, 'close').and.stub(); + spyOn(alertsService, 'addWarning'); - explorationTitleService.displayed = 'New Title'; + explorationTitleService.displayed = 'New Title'; - expect(component.isSavingAllowed()).toBe(false); + expect(component.isSavingAllowed()).toBe(false); - component.save(); - tick(); + component.save(); + tick(); - expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Please specify an objective'); - expect(ngbActiveModal.close).not.toHaveBeenCalled(); - })); + expect(alertsService.addWarning).toHaveBeenCalledWith( + 'Please specify an objective' + ); + expect(ngbActiveModal.close).not.toHaveBeenCalled(); + }) + ); - it('should not save exploration metadata values when category is not' + - ' provided', fakeAsync(() => { - spyOn(ngbActiveModal, 'close').and.stub(); + it( + 'should not save exploration metadata values when category is not' + + ' provided', + fakeAsync(() => { + spyOn(ngbActiveModal, 'close').and.stub(); - explorationTitleService.displayed = 'New Title'; - explorationObjectiveService.displayed = 'Exp Objective'; - explorationCategoryService.displayed = ''; + explorationTitleService.displayed = 'New Title'; + explorationObjectiveService.displayed = 'Exp Objective'; + explorationCategoryService.displayed = ''; - spyOn(alertsService, 'addWarning'); - expect(component.isSavingAllowed()).toBe(false); + spyOn(alertsService, 'addWarning'); + expect(component.isSavingAllowed()).toBe(false); - component.save(); - tick(); + component.save(); + tick(); - expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Please specify a category'); - })); + expect(alertsService.addWarning).toHaveBeenCalledWith( + 'Please specify a category' + ); + }) + ); }); }); diff --git a/core/templates/pages/exploration-editor-page/modal-templates/exploration-metadata-modal.component.ts b/core/templates/pages/exploration-editor-page/modal-templates/exploration-metadata-modal.component.ts index 569d44cea000..717a742b4d02 100644 --- a/core/templates/pages/exploration-editor-page/modal-templates/exploration-metadata-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/modal-templates/exploration-metadata-modal.component.ts @@ -16,22 +16,21 @@ * @fileoverview Component for exploration metadata modal. */ -import { Component, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { MatChipInputEvent } from '@angular/material/chips'; -import { COMMA, ENTER } from '@angular/cdk/keycodes'; -import { AppConstants } from 'app.constants'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { ExplorationCategoryService } from '../services/exploration-category.service'; -import { ExplorationLanguageCodeService } from '../services/exploration-language-code.service'; -import { ExplorationObjectiveService } from '../services/exploration-objective.service'; -import { ExplorationTagsService } from '../services/exploration-tags.service'; -import { ExplorationTitleService } from '../services/exploration-title.service'; -import { AlertsService } from 'services/alerts.service'; -import { ExplorationStatesService } from '../services/exploration-states.service'; -import { ParamChange } from 'domain/exploration/ParamChangeObjectFactory'; - +import {Component, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {MatChipInputEvent} from '@angular/material/chips'; +import {COMMA, ENTER} from '@angular/cdk/keycodes'; +import {AppConstants} from 'app.constants'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {ExplorationCategoryService} from '../services/exploration-category.service'; +import {ExplorationLanguageCodeService} from '../services/exploration-language-code.service'; +import {ExplorationObjectiveService} from '../services/exploration-objective.service'; +import {ExplorationTagsService} from '../services/exploration-tags.service'; +import {ExplorationTitleService} from '../services/exploration-title.service'; +import {AlertsService} from 'services/alerts.service'; +import {ExplorationStatesService} from '../services/exploration-states.service'; +import {ParamChange} from 'domain/exploration/ParamChangeObjectFactory'; interface CategoryChoices { id: string; @@ -40,10 +39,12 @@ interface CategoryChoices { @Component({ selector: 'oppia-exploration-metadata-modal', - templateUrl: './exploration-metadata-modal.component.html' + templateUrl: './exploration-metadata-modal.component.html', }) export class ExplorationMetadataModalComponent - extends ConfirmOrCancelModal implements OnInit { + extends ConfirmOrCancelModal + implements OnInit +{ // These properties below are initialized using Angular lifecycle hooks // where we need to do non-null assertion. For more information see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -70,7 +71,7 @@ export class ExplorationMetadataModalComponent private explorationStatesService: ExplorationStatesService, private explorationTagsService: ExplorationTagsService, private explorationTitleService: ExplorationTitleService, - private ngbActiveModal: NgbActiveModal, + private ngbActiveModal: NgbActiveModal ) { super(ngbActiveModal); } @@ -84,11 +85,12 @@ export class ExplorationMetadataModalComponent filterChoices(searchTerm: string): void { this.newCategory = { id: searchTerm, - text: searchTerm + text: searchTerm, }; this.filteredChoices = this.CATEGORY_LIST_FOR_SELECT2.filter( - value => value.text.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1); + value => value.text.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1 + ); this.filteredChoices.push(this.newCategory); @@ -102,10 +104,13 @@ export class ExplorationMetadataModalComponent // Add our explorationTags. if (value) { - if (!(this.explorationTagsService.displayed) || - (this.explorationTagsService.displayed as []).length < 10) { + if ( + !this.explorationTagsService.displayed || + (this.explorationTagsService.displayed as []).length < 10 + ) { if ( - (this.explorationTagsService.displayed as string[]).includes(value)) { + (this.explorationTagsService.displayed as string[]).includes(value) + ) { // Clear the input value. event.input.value = ''; return; @@ -192,64 +197,62 @@ export class ExplorationMetadataModalComponent isSavingAllowed(): boolean { return Boolean( this.explorationTitleService.displayed && - this.explorationObjectiveService.displayed && - this.explorationObjectiveService.displayed.length >= 15 && - this.explorationCategoryService.displayed && - this.explorationLanguageCodeService.displayed); + this.explorationObjectiveService.displayed && + this.explorationObjectiveService.displayed.length >= 15 && + this.explorationCategoryService.displayed && + this.explorationLanguageCodeService.displayed + ); } ngOnInit(): void { this.CATEGORY_LIST_FOR_SELECT2 = []; - this.objectiveHasBeenPreviouslyEdited = ( - ( - this.explorationObjectiveService.savedMemento as ParamChange[] - ).length > 0); - - this.requireTitleToBeSpecified = ( - !this.explorationTitleService.savedMemento); - this.requireObjectiveToBeSpecified = ( - ( - this.explorationObjectiveService.savedMemento as ParamChange[] - ).length < 15); - this.requireCategoryToBeSpecified = ( - !this.explorationCategoryService.savedMemento); - this.askForLanguageCheck = ( + this.objectiveHasBeenPreviouslyEdited = + (this.explorationObjectiveService.savedMemento as ParamChange[]).length > + 0; + + this.requireTitleToBeSpecified = !this.explorationTitleService.savedMemento; + this.requireObjectiveToBeSpecified = + (this.explorationObjectiveService.savedMemento as ParamChange[]).length < + 15; + this.requireCategoryToBeSpecified = + !this.explorationCategoryService.savedMemento; + this.askForLanguageCheck = this.explorationLanguageCodeService.savedMemento === - AppConstants.DEFAULT_LANGUAGE_CODE); - this.askForTags = ( - ( - this.explorationTagsService.savedMemento as ParamChange[] - ).length === 0); + AppConstants.DEFAULT_LANGUAGE_CODE; + this.askForTags = + (this.explorationTagsService.savedMemento as ParamChange[]).length === 0; for (let i = 0; i < AppConstants.ALL_CATEGORIES.length; i++) { this.CATEGORY_LIST_FOR_SELECT2.push({ id: AppConstants.ALL_CATEGORIES[i], - text: AppConstants.ALL_CATEGORIES[i] + text: AppConstants.ALL_CATEGORIES[i], }); } if (this.explorationStatesService.isInitialized()) { - let categoryIsInSelect2 = this.CATEGORY_LIST_FOR_SELECT2 - .some( - (categoryItem) => { - return categoryItem.id === - this.explorationCategoryService.savedMemento; - } - ); + let categoryIsInSelect2 = this.CATEGORY_LIST_FOR_SELECT2.some( + categoryItem => { + return ( + categoryItem.id === this.explorationCategoryService.savedMemento + ); + } + ); // If the current category is not in the dropdown, add it // as the first option. - if (!categoryIsInSelect2 && - this.explorationCategoryService.savedMemento) { + if ( + !categoryIsInSelect2 && + this.explorationCategoryService.savedMemento + ) { this.CATEGORY_LIST_FOR_SELECT2.unshift({ id: this.explorationCategoryService.savedMemento as string, - text: this.explorationCategoryService.savedMemento as string + text: this.explorationCategoryService.savedMemento as string, }); } } this.filteredChoices = this.CATEGORY_LIST_FOR_SELECT2; - this.explorationTags = (this.explorationTagsService.displayed) as string[]; + this.explorationTags = this.explorationTagsService.displayed as string[]; // This logic has been used here to // solve ExpressionChangedAfterItHasBeenCheckedError error. @@ -259,7 +262,9 @@ export class ExplorationMetadataModalComponent } } -angular.module('oppia').directive('oppiaExplorationMetadataModal', +angular.module('oppia').directive( + 'oppiaExplorationMetadataModal', downgradeComponent({ - component: ExplorationMetadataModalComponent - }) as angular.IDirectiveFactory); + component: ExplorationMetadataModalComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-editor-page/modal-templates/exploration-publish-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/modal-templates/exploration-publish-modal.component.spec.ts index b6f31ead27d7..20cbdaa96b79 100644 --- a/core/templates/pages/exploration-editor-page/modal-templates/exploration-publish-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/modal-templates/exploration-publish-modal.component.spec.ts @@ -16,9 +16,9 @@ * @fileoverview Unit tests for ExplorationPublishModalComponent. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap'; -import { ExplorationPublishModalComponent } from './exploration-publish-modal.component'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal, NgbModalModule} from '@ng-bootstrap/ng-bootstrap'; +import {ExplorationPublishModalComponent} from './exploration-publish-modal.component'; describe('Editor Reloading Modal', () => { let fixture: ComponentFixture; @@ -26,15 +26,9 @@ describe('Editor Reloading Modal', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - NgbModalModule - ], - declarations: [ - ExplorationPublishModalComponent - ], - providers: [ - NgbActiveModal - ] + imports: [NgbModalModule], + declarations: [ExplorationPublishModalComponent], + providers: [NgbActiveModal], }).compileComponents(); })); diff --git a/core/templates/pages/exploration-editor-page/modal-templates/exploration-publish-modal.component.ts b/core/templates/pages/exploration-editor-page/modal-templates/exploration-publish-modal.component.ts index d68d3ee6dabb..a856d235d4d9 100644 --- a/core/templates/pages/exploration-editor-page/modal-templates/exploration-publish-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/modal-templates/exploration-publish-modal.component.ts @@ -17,20 +17,16 @@ * dismiss. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-exploration-publish-modal', - templateUrl: './exploration-publish-modal.component.html' + templateUrl: './exploration-publish-modal.component.html', }) - -export class ExplorationPublishModalComponent - extends ConfirmOrCancelModal { - constructor( - private ngbActiveModal: NgbActiveModal - ) { +export class ExplorationPublishModalComponent extends ConfirmOrCancelModal { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } } diff --git a/core/templates/pages/exploration-editor-page/modal-templates/exploration-save-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/modal-templates/exploration-save-modal.component.spec.ts index 8b0a4be4ac13..41862c6c06fe 100644 --- a/core/templates/pages/exploration-editor-page/modal-templates/exploration-save-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/modal-templates/exploration-save-modal.component.spec.ts @@ -16,10 +16,10 @@ * @fileoverview Unit tests for ExplorationSaveModalcomponent. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ExplorationSaveModalComponent } from './exploration-save-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ExplorationSaveModalComponent} from './exploration-save-modal.component'; class MockActiveModal { close(): void { @@ -38,16 +38,14 @@ describe('Exploration Save Modal component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - ExplorationSaveModalComponent - ], + declarations: [ExplorationSaveModalComponent], providers: [ { provide: NgbActiveModal, - useClass: MockActiveModal + useClass: MockActiveModal, }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -66,21 +64,23 @@ describe('Exploration Save Modal component', () => { fixture.detectChanges(); }); - it('should initialize component properties after component is initialized', - () => { - expect(component.showDiff).toBe(false); - expect(component.diffData).toBe(null); - expect(component.isExplorationPrivate).toBe(isExplorationPrivate); - expect(component.earlierVersionHeader).toBe('Last saved'); - expect(component.laterVersionHeader).toBe('New changes'); - }); - - it('should toggle exploration diff visibility when clicking on toggle diff' + - ' button', () => { - expect(component.showDiff).toBe(false); - component.onClickToggleDiffButton(); - expect(component.showDiff).toBe(true); - component.onClickToggleDiffButton(); + it('should initialize component properties after component is initialized', () => { expect(component.showDiff).toBe(false); + expect(component.diffData).toBe(null); + expect(component.isExplorationPrivate).toBe(isExplorationPrivate); + expect(component.earlierVersionHeader).toBe('Last saved'); + expect(component.laterVersionHeader).toBe('New changes'); }); + + it( + 'should toggle exploration diff visibility when clicking on toggle diff' + + ' button', + () => { + expect(component.showDiff).toBe(false); + component.onClickToggleDiffButton(); + expect(component.showDiff).toBe(true); + component.onClickToggleDiffButton(); + expect(component.showDiff).toBe(false); + } + ); }); diff --git a/core/templates/pages/exploration-editor-page/modal-templates/exploration-save-modal.component.ts b/core/templates/pages/exploration-editor-page/modal-templates/exploration-save-modal.component.ts index 96af7987e75f..a760e75d3d4a 100644 --- a/core/templates/pages/exploration-editor-page/modal-templates/exploration-save-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/modal-templates/exploration-save-modal.component.ts @@ -16,19 +16,18 @@ * @fileoverview Component for exploration save modal. */ -import { Component, Input } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { AppConstants } from 'app.constants'; -import { DiffNodeData } from 'components/version-diff-visualization/version-diff-visualization.component'; +import {Component, Input} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {AppConstants} from 'app.constants'; +import {DiffNodeData} from 'components/version-diff-visualization/version-diff-visualization.component'; @Component({ selector: 'oppia-exploration-save-modal', - templateUrl: './exploration-save-modal.component.html' + templateUrl: './exploration-save-modal.component.html', }) -export class ExplorationSaveModalComponent - extends ConfirmOrCancelModal { +export class ExplorationSaveModalComponent extends ConfirmOrCancelModal { earlierVersionHeader: string = 'Last saved'; laterVersionHeader: string = 'New changes'; commitMessage: string = ''; @@ -41,9 +40,7 @@ export class ExplorationSaveModalComponent @Input() isExplorationPrivate!: boolean; @Input() diffData!: DiffNodeData; - constructor( - private ngbActiveModal: NgbActiveModal - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } @@ -52,7 +49,9 @@ export class ExplorationSaveModalComponent } } -angular.module('oppia').directive('oppiaExplorationSaveModal', +angular.module('oppia').directive( + 'oppiaExplorationSaveModal', downgradeComponent({ - component: ExplorationSaveModalComponent - }) as angular.IDirectiveFactory); + component: ExplorationSaveModalComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-editor-page/modal-templates/exploration-save-prompt-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/modal-templates/exploration-save-prompt-modal.component.spec.ts index 4c46ad69df0d..fce0be3b7905 100644 --- a/core/templates/pages/exploration-editor-page/modal-templates/exploration-save-prompt-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/modal-templates/exploration-save-prompt-modal.component.spec.ts @@ -16,12 +16,12 @@ * @fileoverview Unit tests for the Exploration save prompt modal. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ContextService } from 'services/context.service'; -import { ExplorationSavePromptModalComponent } from './exploration-save-prompt-modal.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ContextService} from 'services/context.service'; +import {ExplorationSavePromptModalComponent} from './exploration-save-prompt-modal.component'; class MockActiveModal { close(): void { @@ -42,17 +42,15 @@ describe('Exploration Save Prompt Modal Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - ExplorationSavePromptModalComponent - ], + declarations: [ExplorationSavePromptModalComponent], providers: [ { provide: NgbActiveModal, - useClass: MockActiveModal + useClass: MockActiveModal, }, ContextService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -64,8 +62,7 @@ describe('Exploration Save Prompt Modal Component', () => { ngbActiveModal = TestBed.inject(NgbActiveModal); contextService = TestBed.inject(ContextService); - spyOn(contextService, 'getExplorationId').and.returnValue( - 'explorationId'); + spyOn(contextService, 'getExplorationId').and.returnValue('explorationId'); fixture.detectChanges(); }); diff --git a/core/templates/pages/exploration-editor-page/modal-templates/exploration-save-prompt-modal.component.ts b/core/templates/pages/exploration-editor-page/modal-templates/exploration-save-prompt-modal.component.ts index 654c02cbcdc1..200cfeadada8 100644 --- a/core/templates/pages/exploration-editor-page/modal-templates/exploration-save-prompt-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/modal-templates/exploration-save-prompt-modal.component.ts @@ -16,19 +16,17 @@ * @fileoverview Component for the Exploration save prompt modal. */ -import { Component } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-exploration-save-prompt-modal', - templateUrl: './exploration-save-prompt-modal.component.html' + templateUrl: './exploration-save-prompt-modal.component.html', }) export class ExplorationSavePromptModalComponent extends ConfirmOrCancelModal { - constructor( - private ngbActiveModal: NgbActiveModal, - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } @@ -37,7 +35,9 @@ export class ExplorationSavePromptModalComponent extends ConfirmOrCancelModal { } } -angular.module('oppia').directive('oppiaExplorationSavePromptModal', - downgradeComponent({ - component: ExplorationSavePromptModalComponent - }) as angular.IDirectiveFactory); +angular.module('oppia').directive( + 'oppiaExplorationSavePromptModal', + downgradeComponent({ + component: ExplorationSavePromptModalComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-editor-page/modal-templates/help-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/modal-templates/help-modal.component.spec.ts index 09bf6de6aa35..5930560cea1d 100644 --- a/core/templates/pages/exploration-editor-page/modal-templates/help-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/modal-templates/help-modal.component.spec.ts @@ -16,15 +16,14 @@ * @fileoverview Unit tests for HelpModalComponent. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { HelpModalComponent } from './help-modal.component'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { ContextService } from 'services/context.service'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; - - -describe('Exploration Player Suggestion Modal Controller', function() { +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {HelpModalComponent} from './help-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {ContextService} from 'services/context.service'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; + +describe('Exploration Player Suggestion Modal Controller', function () { let component: HelpModalComponent; let fixture: ComponentFixture; let siteAnalyticsService: SiteAnalyticsService; @@ -32,20 +31,15 @@ describe('Exploration Player Suggestion Modal Controller', function() { let ngbActiveModal: NgbActiveModal; let explorationId = 'exp1'; - beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [HelpModalComponent], - providers: [ - SiteAnalyticsService, - ContextService, - NgbActiveModal, - ], + providers: [SiteAnalyticsService, ContextService, NgbActiveModal], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); - beforeEach(function() { + beforeEach(function () { fixture = TestBed.createComponent(HelpModalComponent); component = fixture.componentInstance; siteAnalyticsService = TestBed.inject(SiteAnalyticsService); @@ -55,36 +49,44 @@ describe('Exploration Player Suggestion Modal Controller', function() { spyOn(contextService, 'getExplorationId').and.returnValue(explorationId); }); - it('should begin editor tutorial when closing the modal', function() { + it('should begin editor tutorial when closing the modal', function () { const closeSpy = spyOn(ngbActiveModal, 'close').and.callThrough(); const registerOpenTutorialFromHelpCenterEventSpy = spyOn( - siteAnalyticsService, 'registerOpenTutorialFromHelpCenterEvent'); + siteAnalyticsService, + 'registerOpenTutorialFromHelpCenterEvent' + ); component.ngOnInit(); component.beginEditorTutorial(); - expect(registerOpenTutorialFromHelpCenterEventSpy) - .toHaveBeenCalledWith(explorationId); + expect(registerOpenTutorialFromHelpCenterEventSpy).toHaveBeenCalledWith( + explorationId + ); expect(closeSpy).toHaveBeenCalledWith('editor'); }); - it('should begin translation tutorial when closing the modal', function() { + it('should begin translation tutorial when closing the modal', function () { const closeSpy = spyOn(ngbActiveModal, 'close').and.callThrough(); let registerOpenTutorialFromHelpCenterEventSpy = spyOn( - siteAnalyticsService, 'registerOpenTutorialFromHelpCenterEvent'); + siteAnalyticsService, + 'registerOpenTutorialFromHelpCenterEvent' + ); component.ngOnInit(); component.beginTranslationTutorial(); - expect(registerOpenTutorialFromHelpCenterEventSpy) - .toHaveBeenCalledWith(explorationId); + expect(registerOpenTutorialFromHelpCenterEventSpy).toHaveBeenCalledWith( + explorationId + ); expect(closeSpy).toHaveBeenCalledWith('translation'); }); - it('should dismiss modal when changing to help center', function() { + it('should dismiss modal when changing to help center', function () { const dismissSpy = spyOn(ngbActiveModal, 'dismiss').and.callThrough(); let registerVisitHelpCenterEventSpy = spyOn( - siteAnalyticsService, 'registerVisitHelpCenterEvent'); + siteAnalyticsService, + 'registerVisitHelpCenterEvent' + ); component.ngOnInit(); component.goToHelpCenter(); diff --git a/core/templates/pages/exploration-editor-page/modal-templates/help-modal.component.ts b/core/templates/pages/exploration-editor-page/modal-templates/help-modal.component.ts index 65d689ece09c..aa031ff9e8c7 100644 --- a/core/templates/pages/exploration-editor-page/modal-templates/help-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/modal-templates/help-modal.component.ts @@ -16,10 +16,10 @@ * @fileoverview Component for help modal. */ -import { Component, OnInit } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ContextService } from 'services/context.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; +import {Component, OnInit} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ContextService} from 'services/context.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; @Component({ selector: 'oppia-help-modal', @@ -40,27 +40,25 @@ export class HelpModalComponent implements OnInit { ) {} ngOnInit(): void { - this.explorationId = ( - this.contextService.getExplorationId()); + this.explorationId = this.contextService.getExplorationId(); } beginEditorTutorial(): void { - this.siteAnalyticsService - .registerOpenTutorialFromHelpCenterEvent( - this.explorationId); + this.siteAnalyticsService.registerOpenTutorialFromHelpCenterEvent( + this.explorationId + ); this.ngbActiveModal.close(this.EDITOR_TUTORIAL_MODE); } beginTranslationTutorial(): void { - this.siteAnalyticsService - .registerOpenTutorialFromHelpCenterEvent( - this.explorationId); + this.siteAnalyticsService.registerOpenTutorialFromHelpCenterEvent( + this.explorationId + ); this.ngbActiveModal.close(this.TRANSLATION_TUTORIAL_MODE); } goToHelpCenter(): void { - this.siteAnalyticsService.registerVisitHelpCenterEvent( - this.explorationId); + this.siteAnalyticsService.registerVisitHelpCenterEvent(this.explorationId); this.ngbActiveModal.dismiss('cancel'); } } diff --git a/core/templates/pages/exploration-editor-page/modal-templates/lost-changes-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/modal-templates/lost-changes-modal.component.spec.ts index 2cc63ebe86f8..fb4fab0a5542 100644 --- a/core/templates/pages/exploration-editor-page/modal-templates/lost-changes-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/modal-templates/lost-changes-modal.component.spec.ts @@ -16,23 +16,24 @@ * @fileoverview Unit tests for LostChangesModalComponent. */ -import { Component, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import {Component, NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; -import { LostChange, LostChangeObjectFactory } from - 'domain/exploration/LostChangeObjectFactory'; +import { + LostChange, + LostChangeObjectFactory, +} from 'domain/exploration/LostChangeObjectFactory'; -import { LostChangesModalComponent } from './lost-changes-modal.component'; -import { LoggerService } from 'services/contextual/logger.service'; -import { UtilsService } from 'services/utils.service'; +import {LostChangesModalComponent} from './lost-changes-modal.component'; +import {LoggerService} from 'services/contextual/logger.service'; +import {UtilsService} from 'services/utils.service'; @Component({ selector: 'oppia-changes-in-human-readable-form', - template: '' + template: '', }) -class ChangesInHumanReadableFormComponentStub { -} +class ChangesInHumanReadableFormComponentStub {} class MockActiveModal { close(): void { @@ -44,47 +45,48 @@ class MockActiveModal { } } - describe('Lost Changes Modal Component', () => { let component: LostChangesModalComponent; let fixture: ComponentFixture; let ngbActiveModal: NgbActiveModal; - const lostChanges = [{ - cmd: 'add_state', - state_name: 'State name', - content_id_for_state_content: 'content_0', - content_id_for_default_outcome: 'default_outcome_1', - utilsService: new UtilsService, - isEndingExploration: () => false, - isAddingInteraction: () => false, - isOldValueEmpty: () => false, - isNewValueEmpty: () => false, - isOutcomeFeedbackEqual: () => false, - isOutcomeDestEqual: () => false, - isDestEqual: () => false, - isFeedbackEqual: () => false, - isRulesEqual: () => false, - getRelativeChangeToGroups: () => 'string', - getLanguage: () => 'en', - getStatePropertyValue: (value1) => 'string' - } as LostChange]; + const lostChanges = [ + { + cmd: 'add_state', + state_name: 'State name', + content_id_for_state_content: 'content_0', + content_id_for_default_outcome: 'default_outcome_1', + utilsService: new UtilsService(), + isEndingExploration: () => false, + isAddingInteraction: () => false, + isOldValueEmpty: () => false, + isNewValueEmpty: () => false, + isOutcomeFeedbackEqual: () => false, + isOutcomeDestEqual: () => false, + isDestEqual: () => false, + isFeedbackEqual: () => false, + isRulesEqual: () => false, + getRelativeChangeToGroups: () => 'string', + getLanguage: () => 'en', + getStatePropertyValue: value1 => 'string', + } as LostChange, + ]; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ LostChangesModalComponent, - ChangesInHumanReadableFormComponentStub + ChangesInHumanReadableFormComponentStub, ], providers: [ LostChangeObjectFactory, LoggerService, { provide: NgbActiveModal, - useClass: MockActiveModal + useClass: MockActiveModal, }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -107,44 +109,50 @@ describe('Lost Changes Modal Component', () => { it('should contain correct modal header', () => { const modalHeader = - fixture.debugElement.nativeElement - .querySelector('.modal-header').innerText; + fixture.debugElement.nativeElement.querySelector( + '.modal-header' + ).innerText; expect(modalHeader).toBe('Error Loading Exploration'); }); it('should contain correct modal body', () => { const modalBody = - fixture.debugElement.nativeElement - .querySelector('.modal-body').children[0].innerText; + fixture.debugElement.nativeElement.querySelector('.modal-body') + .children[0].innerText; expect(modalBody).toBe( 'Sorry! The following changes will be lost. It appears that your ' + - 'draft was overwritten by changes on another machine.'); + 'draft was overwritten by changes on another machine.' + ); }); - it('should contain description on lost changes' + - 'only if they exists in modal body', () => { - const modalBody = - fixture.debugElement.nativeElement - .querySelector('.modal-body').children[1].innerText; + it( + 'should contain description on lost changes' + + 'only if they exists in modal body', + () => { + const modalBody = + fixture.debugElement.nativeElement.querySelector('.modal-body') + .children[1].innerText; - component.hasLostChanges = true; - fixture.detectChanges(); + component.hasLostChanges = true; + fixture.detectChanges(); - expect(modalBody).toBe( - 'The lost changes are displayed below. You may want to export or ' + - 'copy and paste these changes before discarding them.'); - }); + expect(modalBody).toBe( + 'The lost changes are displayed below. You may want to export or ' + + 'copy and paste these changes before discarding them.' + ); + } + ); it('should export the lost changes and close the modal', () => { - spyOn( - fixture.elementRef.nativeElement, 'getElementsByClassName' - ).withArgs('oppia-lost-changes').and.returnValue([ - { - innerText: 'Dummy Inner Text' - } - ]); + spyOn(fixture.elementRef.nativeElement, 'getElementsByClassName') + .withArgs('oppia-lost-changes') + .and.returnValue([ + { + innerText: 'Dummy Inner Text', + }, + ]); const dismissSpy = spyOn(ngbActiveModal, 'dismiss').and.callThrough(); const spyObj = jasmine.createSpyObj('a', ['click']); spyOn(document, 'createElement').and.returnValue(spyObj); diff --git a/core/templates/pages/exploration-editor-page/modal-templates/lost-changes-modal.component.ts b/core/templates/pages/exploration-editor-page/modal-templates/lost-changes-modal.component.ts index a72d2959d305..85900690262b 100644 --- a/core/templates/pages/exploration-editor-page/modal-templates/lost-changes-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/modal-templates/lost-changes-modal.component.ts @@ -16,20 +16,25 @@ * @fileoverview Component for lost changes modal. */ -import { Component, ElementRef, Input, OnInit } from '@angular/core'; +import {Component, ElementRef, Input, OnInit} from '@angular/core'; -import { LoggerService } from 'services/contextual/logger.service'; -import { LostChange, LostChangeObjectFactory } from 'domain/exploration/LostChangeObjectFactory'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import {LoggerService} from 'services/contextual/logger.service'; +import { + LostChange, + LostChangeObjectFactory, +} from 'domain/exploration/LostChangeObjectFactory'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {WindowRef} from 'services/contextual/window-ref.service'; @Component({ selector: 'oppia-lost-changes-modal', - templateUrl: './lost-changes-modal.component.html' + templateUrl: './lost-changes-modal.component.html', }) export class LostChangesModalComponent - extends ConfirmOrCancelModal implements OnInit { + extends ConfirmOrCancelModal + implements OnInit +{ // The property is initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -41,15 +46,16 @@ export class LostChangesModalComponent private windowRef: WindowRef, private loggerService: LoggerService, private lostChangeObjectFactory: LostChangeObjectFactory, - private ngbActiveModal: NgbActiveModal, + private ngbActiveModal: NgbActiveModal ) { super(ngbActiveModal); } ngOnInit(): void { - this.hasLostChanges = (this.lostChanges && this.lostChanges.length > 0); + this.hasLostChanges = this.lostChanges && this.lostChanges.length > 0; this.lostChanges = this.lostChanges.map( - this.lostChangeObjectFactory.createNew); + this.lostChangeObjectFactory.createNew + ); } cancel(): void { @@ -60,9 +66,9 @@ export class LostChangesModalComponent // 'getElementsByClassName' returns null if the class name is not // found, here we know that the class name is available, so we // are explicitly typecasting it to remove type error. - let lostChangesData = ( - this.elRef.nativeElement.getElementsByClassName( - 'oppia-lost-changes'))[0] as HTMLInputElement; + let lostChangesData = this.elRef.nativeElement.getElementsByClassName( + 'oppia-lost-changes' + )[0] as HTMLInputElement; let blob = new Blob([lostChangesData.innerText], {type: 'text/plain'}); let elem = this.windowRef.nativeWindow.document.createElement('a'); elem.href = URL.createObjectURL(blob); diff --git a/core/templates/pages/exploration-editor-page/modal-templates/metadata-version-history-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/modal-templates/metadata-version-history-modal.component.spec.ts index 2951b5b35b7b..eb7f1d77a33e 100644 --- a/core/templates/pages/exploration-editor-page/modal-templates/metadata-version-history-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/modal-templates/metadata-version-history-modal.component.spec.ts @@ -16,18 +16,27 @@ * @fileoverview Unit tests for metadata version history modal. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ContextService } from 'services/context.service'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { HistoryTabYamlConversionService } from '../services/history-tab-yaml-conversion.service'; -import { VersionHistoryBackendApiService } from '../services/version-history-backend-api.service'; -import { MetadataDiffData, VersionHistoryService } from '../services/version-history.service'; -import { MetadataVersionHistoryModalComponent } from './metadata-version-history-modal.component'; -import { ExplorationMetadata } from 'domain/exploration/ExplorationMetadataObjectFactory'; -import { ParamSpecs } from 'domain/exploration/ParamSpecsObjectFactory'; -import { ParamSpecObjectFactory } from 'domain/exploration/ParamSpecObjectFactory'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ContextService} from 'services/context.service'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {HistoryTabYamlConversionService} from '../services/history-tab-yaml-conversion.service'; +import {VersionHistoryBackendApiService} from '../services/version-history-backend-api.service'; +import { + MetadataDiffData, + VersionHistoryService, +} from '../services/version-history.service'; +import {MetadataVersionHistoryModalComponent} from './metadata-version-history-modal.component'; +import {ExplorationMetadata} from 'domain/exploration/ExplorationMetadataObjectFactory'; +import {ParamSpecs} from 'domain/exploration/ParamSpecsObjectFactory'; +import {ParamSpecObjectFactory} from 'domain/exploration/ParamSpecObjectFactory'; describe('Metadata version history modal', () => { let component: MetadataVersionHistoryModalComponent; @@ -48,9 +57,9 @@ describe('Metadata version history modal', () => { ContextService, VersionHistoryService, VersionHistoryBackendApiService, - HistoryTabYamlConversionService + HistoryTabYamlConversionService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -58,7 +67,8 @@ describe('Metadata version history modal', () => { fixture = TestBed.createComponent(MetadataVersionHistoryModalComponent); component = fixture.componentInstance; historyTabYamlConversionService = TestBed.inject( - HistoryTabYamlConversionService); + HistoryTabYamlConversionService + ); versionHistoryService = TestBed.inject(VersionHistoryService); versionHistoryBackendApiService = TestBed.inject( VersionHistoryBackendApiService @@ -67,15 +77,26 @@ describe('Metadata version history modal', () => { paramSpecObjectFactory = TestBed.inject(ParamSpecObjectFactory); explorationMetadata = new ExplorationMetadata( - 'title', 'category', 'objective', 'en', - [], '', '', 55, 'Introduction', - new ParamSpecs({}, paramSpecObjectFactory), [], false, true + 'title', + 'category', + 'objective', + 'en', + [], + '', + '', + 55, + 'Introduction', + new ParamSpecs({}, paramSpecObjectFactory), + [], + false, + true ); }); it('should get whether we can explore backward version history', () => { spyOn( - versionHistoryService, 'canShowBackwardMetadataDiffData' + versionHistoryService, + 'canShowBackwardMetadataDiffData' ).and.returnValue(true); expect(component.canExploreBackwardVersionHistory()).toBeTrue(); @@ -83,48 +104,49 @@ describe('Metadata version history modal', () => { it('should get whether we can explore forward version history', () => { spyOn( - versionHistoryService, 'canShowForwardMetadataDiffData' + versionHistoryService, + 'canShowForwardMetadataDiffData' ).and.returnValue(true); expect(component.canExploreForwardVersionHistory()).toBeTrue(); }); it('should get the last edited version number', () => { - spyOn( - versionHistoryService, 'getBackwardMetadataDiffData' - ).and.returnValue({ - oldMetadata: explorationMetadata, - newMetadata: explorationMetadata, - oldVersionNumber: 2, - newVersionNumber: 3, - committerUsername: '' - }); + spyOn(versionHistoryService, 'getBackwardMetadataDiffData').and.returnValue( + { + oldMetadata: explorationMetadata, + newMetadata: explorationMetadata, + oldVersionNumber: 2, + newVersionNumber: 3, + committerUsername: '', + } + ); expect(component.getLastEditedVersionNumber()).toEqual(2); }); it('should throw error when last edited version number is null', () => { - spyOn( - versionHistoryService, 'getBackwardMetadataDiffData' - ).and.returnValue({ - oldVersionNumber: null - } as MetadataDiffData); + spyOn(versionHistoryService, 'getBackwardMetadataDiffData').and.returnValue( + { + oldVersionNumber: null, + } as MetadataDiffData + ); - expect( - () =>component.getLastEditedVersionNumber() - ).toThrowError('Last edited version number cannot be null'); + expect(() => component.getLastEditedVersionNumber()).toThrowError( + 'Last edited version number cannot be null' + ); }); it('should get the last edited committer username', () => { - spyOn( - versionHistoryService, 'getBackwardMetadataDiffData' - ).and.returnValue({ - oldMetadata: explorationMetadata, - newMetadata: explorationMetadata, - oldVersionNumber: 2, - newVersionNumber: 3, - committerUsername: 'some' - }); + spyOn(versionHistoryService, 'getBackwardMetadataDiffData').and.returnValue( + { + oldMetadata: explorationMetadata, + newMetadata: explorationMetadata, + oldVersionNumber: 2, + newVersionNumber: 3, + committerUsername: 'some', + } + ); expect(component.getLastEditedCommitterUsername()).toEqual('some'); }); @@ -135,22 +157,20 @@ describe('Metadata version history modal', () => { newMetadata: explorationMetadata, oldVersionNumber: 3, newVersionNumber: 2, - committerUsername: 'some' + committerUsername: 'some', }); expect(component.getNextEditedVersionNumber()).toEqual(3); }); it('should throw error when next edited version number is null', () => { - spyOn( - versionHistoryService, 'getForwardMetadataDiffData' - ).and.returnValue({ - oldVersionNumber: null + spyOn(versionHistoryService, 'getForwardMetadataDiffData').and.returnValue({ + oldVersionNumber: null, } as MetadataDiffData); - expect( - () =>component.getNextEditedVersionNumber() - ).toThrowError('Next edited version number cannot be null'); + expect(() => component.getNextEditedVersionNumber()).toThrowError( + 'Next edited version number cannot be null' + ); }); it('should get the next edited committer username', () => { @@ -159,92 +179,107 @@ describe('Metadata version history modal', () => { newMetadata: explorationMetadata, oldVersionNumber: 3, newVersionNumber: 2, - committerUsername: 'some' + committerUsername: 'some', }); expect(component.getNextEditedCommitterUsername()).toEqual('some'); }); - it('should update the left and the right side yaml strings on exploring' + - ' forward version history', fakeAsync(() => { - spyOn(versionHistoryService, 'getForwardMetadataDiffData').and.returnValue({ - oldMetadata: explorationMetadata, - newMetadata: explorationMetadata, - oldVersionNumber: 3, - newVersionNumber: 2, - committerUsername: 'some' - }); - spyOn( - historyTabYamlConversionService, 'getYamlStringFromStateOrMetadata' - ).and.resolveTo('YAML STRING'); - - expect(component.yamlStrs.previousVersionMetadataYaml).toEqual(''); - expect(component.yamlStrs.currentVersionMetadataYaml).toEqual(''); + it( + 'should update the left and the right side yaml strings on exploring' + + ' forward version history', + fakeAsync(() => { + spyOn( + versionHistoryService, + 'getForwardMetadataDiffData' + ).and.returnValue({ + oldMetadata: explorationMetadata, + newMetadata: explorationMetadata, + oldVersionNumber: 3, + newVersionNumber: 2, + committerUsername: 'some', + }); + spyOn( + historyTabYamlConversionService, + 'getYamlStringFromStateOrMetadata' + ).and.resolveTo('YAML STRING'); - component.onClickExploreForwardVersionHistory(); - tick(); + expect(component.yamlStrs.previousVersionMetadataYaml).toEqual(''); + expect(component.yamlStrs.currentVersionMetadataYaml).toEqual(''); - expect( - component.yamlStrs.previousVersionMetadataYaml - ).toEqual('YAML STRING'); - expect( - component.yamlStrs.currentVersionMetadataYaml - ).toEqual('YAML STRING'); - })); + component.onClickExploreForwardVersionHistory(); + tick(); - it('should update the left and the right side yaml strings on exploring' + - ' backward version history', fakeAsync(() => { - spyOn( - versionHistoryService, 'getBackwardMetadataDiffData' - ).and.returnValue({ - oldMetadata: explorationMetadata, - newMetadata: explorationMetadata, - oldVersionNumber: 2, - newVersionNumber: 3, - committerUsername: 'some' - }); - spyOn( - historyTabYamlConversionService, 'getYamlStringFromStateOrMetadata' - ).and.resolveTo('YAML STRING'); - spyOn(component, 'fetchPreviousVersionHistory').and.callFake(() => {}); + expect(component.yamlStrs.previousVersionMetadataYaml).toEqual( + 'YAML STRING' + ); + expect(component.yamlStrs.currentVersionMetadataYaml).toEqual( + 'YAML STRING' + ); + }) + ); + + it( + 'should update the left and the right side yaml strings on exploring' + + ' backward version history', + fakeAsync(() => { + spyOn( + versionHistoryService, + 'getBackwardMetadataDiffData' + ).and.returnValue({ + oldMetadata: explorationMetadata, + newMetadata: explorationMetadata, + oldVersionNumber: 2, + newVersionNumber: 3, + committerUsername: 'some', + }); + spyOn( + historyTabYamlConversionService, + 'getYamlStringFromStateOrMetadata' + ).and.resolveTo('YAML STRING'); + spyOn(component, 'fetchPreviousVersionHistory').and.callFake(() => {}); - expect(component.yamlStrs.previousVersionMetadataYaml).toEqual(''); - expect(component.yamlStrs.currentVersionMetadataYaml).toEqual(''); + expect(component.yamlStrs.previousVersionMetadataYaml).toEqual(''); + expect(component.yamlStrs.currentVersionMetadataYaml).toEqual(''); - component.onClickExploreBackwardVersionHistory(); - tick(); + component.onClickExploreBackwardVersionHistory(); + tick(); - expect( - component.yamlStrs.previousVersionMetadataYaml - ).toEqual('YAML STRING'); - expect( - component.yamlStrs.currentVersionMetadataYaml - ).toEqual('YAML STRING'); - })); + expect(component.yamlStrs.previousVersionMetadataYaml).toEqual( + 'YAML STRING' + ); + expect(component.yamlStrs.currentVersionMetadataYaml).toEqual( + 'YAML STRING' + ); + }) + ); it('should be able to fetch the backward version history', fakeAsync(() => { spyOn( - versionHistoryService, 'shouldFetchNewMetadataVersionHistory' + versionHistoryService, + 'shouldFetchNewMetadataVersionHistory' ).and.returnValues(false, true); spyOn( - versionHistoryService, 'insertMetadataVersionHistoryData' + versionHistoryService, + 'insertMetadataVersionHistoryData' ).and.callThrough(); spyOn(contextService, 'getExplorationId').and.returnValue('exp_1'); + spyOn(versionHistoryService, 'getBackwardMetadataDiffData').and.returnValue( + { + oldMetadata: explorationMetadata, + newMetadata: explorationMetadata, + oldVersionNumber: 2, + newVersionNumber: 3, + committerUsername: 'some', + } + ); spyOn( - versionHistoryService, 'getBackwardMetadataDiffData' - ).and.returnValue({ - oldMetadata: explorationMetadata, - newMetadata: explorationMetadata, - oldVersionNumber: 2, - newVersionNumber: 3, - committerUsername: 'some' - }); - spyOn( - versionHistoryBackendApiService, 'fetchMetadataVersionHistoryAsync' + versionHistoryBackendApiService, + 'fetchMetadataVersionHistoryAsync' ).and.resolveTo({ lastEditedVersionNumber: 3, lastEditedCommitterUsername: '', - metadataInPreviousVersion: explorationMetadata + metadataInPreviousVersion: explorationMetadata, }); versionHistoryService.setCurrentPositionInMetadataVersionHistoryList(0); @@ -265,20 +300,22 @@ describe('Metadata version history modal', () => { it('should be show error message if the backend api fails', fakeAsync(() => { spyOn( - versionHistoryService, 'shouldFetchNewMetadataVersionHistory' + versionHistoryService, + 'shouldFetchNewMetadataVersionHistory' ).and.returnValue(true); spyOn(contextService, 'getExplorationId').and.returnValue('exp_1'); + spyOn(versionHistoryService, 'getBackwardMetadataDiffData').and.returnValue( + { + oldMetadata: explorationMetadata, + newMetadata: explorationMetadata, + oldVersionNumber: 2, + newVersionNumber: 3, + committerUsername: 'some', + } + ); spyOn( - versionHistoryService, 'getBackwardMetadataDiffData' - ).and.returnValue({ - oldMetadata: explorationMetadata, - newMetadata: explorationMetadata, - oldVersionNumber: 2, - newVersionNumber: 3, - committerUsername: 'some' - }); - spyOn( - versionHistoryBackendApiService, 'fetchMetadataVersionHistoryAsync' + versionHistoryBackendApiService, + 'fetchMetadataVersionHistoryAsync' ).and.resolveTo(null); expect(component.validationErrorIsShown).toBeFalse(); @@ -289,24 +326,24 @@ describe('Metadata version history modal', () => { expect(component.validationErrorIsShown).toBeTrue(); })); - it('should update the left and right side yaml strings on initialization', - fakeAsync(() => { - spyOn( - historyTabYamlConversionService, 'getYamlStringFromStateOrMetadata' - ).and.resolveTo('YAML STRING'); - spyOn(component, 'fetchPreviousVersionHistory').and.callFake(() => {}); + it('should update the left and right side yaml strings on initialization', fakeAsync(() => { + spyOn( + historyTabYamlConversionService, + 'getYamlStringFromStateOrMetadata' + ).and.resolveTo('YAML STRING'); + spyOn(component, 'fetchPreviousVersionHistory').and.callFake(() => {}); - component.ngOnInit(); - tick(); + component.ngOnInit(); + tick(); - expect( - component.yamlStrs.previousVersionMetadataYaml - ).toEqual('YAML STRING'); - expect( - component.yamlStrs.currentVersionMetadataYaml - ).toEqual('YAML STRING'); - expect(component.fetchPreviousVersionHistory).toHaveBeenCalled(); - })); + expect(component.yamlStrs.previousVersionMetadataYaml).toEqual( + 'YAML STRING' + ); + expect(component.yamlStrs.currentVersionMetadataYaml).toEqual( + 'YAML STRING' + ); + expect(component.fetchPreviousVersionHistory).toHaveBeenCalled(); + })); it('should get the last edited version number in case of error', () => { versionHistoryService.insertMetadataVersionHistoryData(4, null, ''); diff --git a/core/templates/pages/exploration-editor-page/modal-templates/metadata-version-history-modal.component.ts b/core/templates/pages/exploration-editor-page/modal-templates/metadata-version-history-modal.component.ts index a3f358d5871f..ceb9041217d4 100644 --- a/core/templates/pages/exploration-editor-page/modal-templates/metadata-version-history-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/modal-templates/metadata-version-history-modal.component.ts @@ -16,15 +16,15 @@ * @fileoverview Component for state changes modal. */ -import { Input, OnInit } from '@angular/core'; -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { ExplorationMetadata } from 'domain/exploration/ExplorationMetadataObjectFactory'; -import { ContextService } from 'services/context.service'; -import { HistoryTabYamlConversionService } from '../services/history-tab-yaml-conversion.service'; -import { VersionHistoryBackendApiService } from '../services/version-history-backend-api.service'; -import { VersionHistoryService } from '../services/version-history.service'; +import {Input, OnInit} from '@angular/core'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {ExplorationMetadata} from 'domain/exploration/ExplorationMetadataObjectFactory'; +import {ContextService} from 'services/context.service'; +import {HistoryTabYamlConversionService} from '../services/history-tab-yaml-conversion.service'; +import {VersionHistoryBackendApiService} from '../services/version-history-backend-api.service'; +import {VersionHistoryService} from '../services/version-history.service'; interface HeadersAndYamlStrs { previousVersionMetadataYaml: string; @@ -43,7 +43,9 @@ interface MergeviewOptions { templateUrl: './metadata-version-history-modal.component.html', }) export class MetadataVersionHistoryModalComponent - extends ConfirmOrCancelModal implements OnInit { + extends ConfirmOrCancelModal + implements OnInit +{ @Input() committerUsername!: string; @Input() oldVersion: number | null = null; @Input() newMetadata!: ExplorationMetadata; @@ -58,15 +60,15 @@ export class MetadataVersionHistoryModalComponent lineNumbers: true, readOnly: true, mode: 'yaml', - viewportMargin: 100 + viewportMargin: 100, }; constructor( - private ngbActiveModal: NgbActiveModal, - private contextService: ContextService, - private versionHistoryService: VersionHistoryService, - private versionHistoryBackendApiService: VersionHistoryBackendApiService, - private historyTabYamlConversionService: HistoryTabYamlConversionService + private ngbActiveModal: NgbActiveModal, + private contextService: ContextService, + private versionHistoryService: VersionHistoryService, + private versionHistoryBackendApiService: VersionHistoryBackendApiService, + private historyTabYamlConversionService: HistoryTabYamlConversionService ) { super(ngbActiveModal); } @@ -80,10 +82,8 @@ export class MetadataVersionHistoryModalComponent } getLastEditedVersionNumber(): number { - const lastEditedVersionNumber = this - .versionHistoryService - .getBackwardMetadataDiffData() - .oldVersionNumber; + const lastEditedVersionNumber = + this.versionHistoryService.getBackwardMetadataDiffData().oldVersionNumber; if (lastEditedVersionNumber === null) { // A null value for lastEditedVersionNumber marks the end of the version // history for the exploration metadata. This is impossible here because @@ -96,26 +96,19 @@ export class MetadataVersionHistoryModalComponent } getLastEditedCommitterUsername(): string { - return ( - this - .versionHistoryService - .getBackwardMetadataDiffData() - .committerUsername - ); + return this.versionHistoryService.getBackwardMetadataDiffData() + .committerUsername; } getLastEditedVersionNumberInCaseOfError(): number { - return ( - this.versionHistoryService.fetchedMetadataVersionNumbers[ - this.versionHistoryService - .getCurrentPositionInMetadataVersionHistoryList()] as number); + return this.versionHistoryService.fetchedMetadataVersionNumbers[ + this.versionHistoryService.getCurrentPositionInMetadataVersionHistoryList() + ] as number; } getNextEditedVersionNumber(): number { - const nextEditedVersionNumber = this - .versionHistoryService - .getForwardMetadataDiffData() - .oldVersionNumber; + const nextEditedVersionNumber = + this.versionHistoryService.getForwardMetadataDiffData().oldVersionNumber; if (nextEditedVersionNumber === null) { // A null value for nextEditedVersionNumber marks the end of the version // history for the exploration metadata. This is impossible here because @@ -128,12 +121,8 @@ export class MetadataVersionHistoryModalComponent } getNextEditedCommitterUsername(): string { - return ( - this - .versionHistoryService - .getForwardMetadataDiffData() - .committerUsername - ); + return this.versionHistoryService.getForwardMetadataDiffData() + .committerUsername; } onClickExploreForwardVersionHistory(): void { @@ -157,8 +146,7 @@ export class MetadataVersionHistoryModalComponent this.validationErrorIsShown = false; - this.versionHistoryService - .decrementCurrentPositionInMetadataVersionHistoryList(); + this.versionHistoryService.decrementCurrentPositionInMetadataVersionHistoryList(); } onClickExploreBackwardVersionHistory(): void { @@ -191,36 +179,36 @@ export class MetadataVersionHistoryModalComponent fetchPreviousVersionHistory(): void { if (!this.versionHistoryService.shouldFetchNewMetadataVersionHistory()) { - this.versionHistoryService - .incrementCurrentPositionInMetadataVersionHistoryList(); + this.versionHistoryService.incrementCurrentPositionInMetadataVersionHistoryList(); return; } const diffData = this.versionHistoryService.getBackwardMetadataDiffData(); if (diffData.oldVersionNumber !== null) { - this.versionHistoryBackendApiService.fetchMetadataVersionHistoryAsync( - this.contextService.getExplorationId(), diffData.oldVersionNumber - ).then((response) => { - if (response !== null) { - this.versionHistoryService.insertMetadataVersionHistoryData( - response.lastEditedVersionNumber, - response.metadataInPreviousVersion, - response.lastEditedCommitterUsername - ); - this.versionHistoryService - .incrementCurrentPositionInMetadataVersionHistoryList(); - } else { - this.validationErrorIsShown = true; - this.versionHistoryService - .incrementCurrentPositionInMetadataVersionHistoryList(); - } - }); + this.versionHistoryBackendApiService + .fetchMetadataVersionHistoryAsync( + this.contextService.getExplorationId(), + diffData.oldVersionNumber + ) + .then(response => { + if (response !== null) { + this.versionHistoryService.insertMetadataVersionHistoryData( + response.lastEditedVersionNumber, + response.metadataInPreviousVersion, + response.lastEditedCommitterUsername + ); + this.versionHistoryService.incrementCurrentPositionInMetadataVersionHistoryList(); + } else { + this.validationErrorIsShown = true; + this.versionHistoryService.incrementCurrentPositionInMetadataVersionHistoryList(); + } + }); } } updateLeftPane(): void { this.historyTabYamlConversionService .getYamlStringFromStateOrMetadata(this.oldMetadata) - .then((result) => { + .then(result => { this.yamlStrs.previousVersionMetadataYaml = result; }); } @@ -228,7 +216,7 @@ export class MetadataVersionHistoryModalComponent updateRightPane(): void { this.historyTabYamlConversionService .getYamlStringFromStateOrMetadata(this.newMetadata) - .then((result) => { + .then(result => { this.yamlStrs.currentVersionMetadataYaml = result; }); } diff --git a/core/templates/pages/exploration-editor-page/modal-templates/post-publish-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/modal-templates/post-publish-modal.component.spec.ts index 5bcb55b924f7..915e9743db90 100644 --- a/core/templates/pages/exploration-editor-page/modal-templates/post-publish-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/modal-templates/post-publish-modal.component.spec.ts @@ -12,27 +12,23 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Unit tests for the Post Publish Modal. */ -import { Component, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; -import { ContextService } from 'services/context.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { PostPublishModalComponent } from 'pages/exploration-editor-page/modal-templates/post-publish-modal.component'; +import {Component, NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {ContextService} from 'services/context.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {PostPublishModalComponent} from 'pages/exploration-editor-page/modal-templates/post-publish-modal.component'; @Component({ selector: 'oppia-changes-in-human-readable-form', - template: '' + template: '', }) - -class ChangesInHumanReadableFormComponentStub { -} +class ChangesInHumanReadableFormComponentStub {} class MockActiveModal { close(): void { @@ -44,7 +40,7 @@ class MockActiveModal { } } -describe('Post Publish Modal Controller', function() { +describe('Post Publish Modal Controller', function () { let component: PostPublishModalComponent; let fixture: ComponentFixture; let ngbActiveModal: NgbActiveModal; @@ -57,8 +53,8 @@ describe('Post Publish Modal Controller', function() { nativeWindow = { location: { protocol: 'https:', - host: 'www.oppia.org' - } + host: 'www.oppia.org', + }, }; } @@ -66,20 +62,21 @@ describe('Post Publish Modal Controller', function() { TestBed.configureTestingModule({ declarations: [ PostPublishModalComponent, - ChangesInHumanReadableFormComponentStub + ChangesInHumanReadableFormComponentStub, ], providers: [ ContextService, { provide: WindowRef, - useClass: MockWindowRef + useClass: MockWindowRef, }, - UrlInterpolationService, { + UrlInterpolationService, + { provide: NgbActiveModal, - useClass: MockActiveModal + useClass: MockActiveModal, }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -91,8 +88,9 @@ describe('Post Publish Modal Controller', function() { contextService = TestBed.inject(ContextService); urlInterpolationService = TestBed.inject(UrlInterpolationService); spyOn(contextService, 'getExplorationId').and.returnValue(explorationId); - spyOn(urlInterpolationService, 'getStaticImageUrl') - .and.returnValue(address); + spyOn(urlInterpolationService, 'getStaticImageUrl').and.returnValue( + address + ); fixture.detectChanges(); }); @@ -100,7 +98,9 @@ describe('Post Publish Modal Controller', function() { expect(component.congratsImgUrl).toBe(address); expect(component.explorationId).toBe(explorationId); expect(component.explorationLinkCopied).toBe(false); - expect(component.explorationLink).toBe('https://www.oppia.org/explore/exp1'); + expect(component.explorationLink).toBe( + 'https://www.oppia.org/explore/exp1' + ); }); it('should close the modal', () => { diff --git a/core/templates/pages/exploration-editor-page/modal-templates/post-publish-modal.component.ts b/core/templates/pages/exploration-editor-page/modal-templates/post-publish-modal.component.ts index cde247710082..676a23984aab 100644 --- a/core/templates/pages/exploration-editor-page/modal-templates/post-publish-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/modal-templates/post-publish-modal.component.ts @@ -12,26 +12,26 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Component for the Post Publish Modal. */ -import { Component, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { ContextService } from 'services/context.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {ContextService} from 'services/context.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-post-publish-modal', - templateUrl: './post-publish-modal.component.html' + templateUrl: './post-publish-modal.component.html', }) - export class PostPublishModalComponent - extends ConfirmOrCancelModal implements OnInit { + extends ConfirmOrCancelModal + implements OnInit +{ number = '1'; // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see @@ -44,20 +44,23 @@ export class PostPublishModalComponent private ngbActiveModal: NgbActiveModal, private urlInterpolationService: UrlInterpolationService, private contextService: ContextService, - private windowRef: WindowRef, + private windowRef: WindowRef ) { super(ngbActiveModal); } ngOnInit(): void { this.congratsImgUrl = this.urlInterpolationService.getStaticImageUrl( - '/general/congrats.svg'); - this.explorationId = (this.contextService.getExplorationId()); + '/general/congrats.svg' + ); + this.explorationId = this.contextService.getExplorationId(); this.explorationLinkCopied = false; this.explorationLink = - this.windowRef.nativeWindow.location.protocol + '//' + + this.windowRef.nativeWindow.location.protocol + + '//' + this.windowRef.nativeWindow.location.host + - '/explore/' + this.explorationId; + '/explore/' + + this.explorationId; } cancel(): void { @@ -65,7 +68,9 @@ export class PostPublishModalComponent } } -angular.module('oppia').factory('oppiaPostPublishModal', +angular.module('oppia').factory( + 'oppiaPostPublishModal', downgradeComponent({ - component: PostPublishModalComponent - }) as angular.IDirectiveFactory); + component: PostPublishModalComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-editor-page/modal-templates/save-validation-fail-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/modal-templates/save-validation-fail-modal.component.spec.ts index 53173e19e153..e7e2f58273cd 100644 --- a/core/templates/pages/exploration-editor-page/modal-templates/save-validation-fail-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/modal-templates/save-validation-fail-modal.component.spec.ts @@ -16,18 +16,23 @@ * @fileoverview Unit tests for SaveValidationFailModalComponent. */ -import { Component, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { SaveValidationFailModalComponent } from './save-validation-fail-modal.component'; +import {Component, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {SaveValidationFailModalComponent} from './save-validation-fail-modal.component'; @Component({ selector: 'oppia-changes-in-human-readable-form', - template: '' + template: '', }) -class ChangesInHumanReadableFormComponentStub { -} +class ChangesInHumanReadableFormComponentStub {} class MockActiveModal { close(): void { @@ -55,7 +60,7 @@ class MockWindowRef { return; } }, - reload: (val: number) => val + reload: (val: number) => val, }, get onhashchange() { return this.location._hashChange; @@ -63,7 +68,7 @@ class MockWindowRef { set onhashchange(val) { this.location._hashChange = val; - } + }, }; get nativeWindow() { @@ -83,18 +88,16 @@ describe('Save Validation Fail Modal Component', () => { TestBed.configureTestingModule({ declarations: [ SaveValidationFailModalComponent, - ChangesInHumanReadableFormComponentStub + ChangesInHumanReadableFormComponentStub, ], providers: [ { provide: NgbActiveModal, - useClass: MockActiveModal + useClass: MockActiveModal, }, - { provide: WindowRef, - useValue: windowRef - } + {provide: WindowRef, useValue: windowRef}, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -128,21 +131,19 @@ describe('Save Validation Fail Modal Component', () => { expect(reloadSpy).toHaveBeenCalled(); })); - it('should contain correct modal header', () => { const modalHeader = - fixture.debugElement.nativeElement - .querySelector('.modal-header').innerText; + fixture.debugElement.nativeElement.querySelector( + '.modal-header' + ).innerText; expect(modalHeader).toBe('Error Saving Exploration'); }); it('should contain correct modal body', () => { const modalBody = - fixture.debugElement.nativeElement - .querySelector('.modal-body').innerText; + fixture.debugElement.nativeElement.querySelector('.modal-body').innerText; - expect(modalBody).toContain( - 'Sorry, an unexpected error occurred.'); + expect(modalBody).toContain('Sorry, an unexpected error occurred.'); }); }); diff --git a/core/templates/pages/exploration-editor-page/modal-templates/save-validation-fail-modal.component.ts b/core/templates/pages/exploration-editor-page/modal-templates/save-validation-fail-modal.component.ts index d55abf1590b8..1caa0ac1b843 100644 --- a/core/templates/pages/exploration-editor-page/modal-templates/save-validation-fail-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/modal-templates/save-validation-fail-modal.component.ts @@ -16,22 +16,22 @@ * @fileoverview Component for non strict validation fail modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-save-validation-fail-modal', - templateUrl: './save-validation-fail-modal.component.html' + templateUrl: './save-validation-fail-modal.component.html', }) export class SaveValidationFailModalComponent extends ConfirmOrCancelModal { MSECS_TO_REFRESH: number = 20; constructor( private windowRef: WindowRef, - private ngbActiveModal: NgbActiveModal, + private ngbActiveModal: NgbActiveModal ) { super(ngbActiveModal); } diff --git a/core/templates/pages/exploration-editor-page/modal-templates/save-version-mismatch-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/modal-templates/save-version-mismatch-modal.component.spec.ts index 573e2e3211dd..a631b2c877c7 100644 --- a/core/templates/pages/exploration-editor-page/modal-templates/save-version-mismatch-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/modal-templates/save-version-mismatch-modal.component.spec.ts @@ -16,21 +16,29 @@ * @fileoverview Unit tests for SaveVersionMismatchModalComponent. */ -import { Component, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; - -import { WindowRef } from 'services/contextual/window-ref.service'; -import { SaveVersionMismatchModalComponent } from './save-version-mismatch-modal.component'; -import { LostChange, LostChangeObjectFactory } from 'domain/exploration/LostChangeObjectFactory'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ExplorationDataService } from '../services/exploration-data.service'; +import {Component, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; + +import {WindowRef} from 'services/contextual/window-ref.service'; +import {SaveVersionMismatchModalComponent} from './save-version-mismatch-modal.component'; +import { + LostChange, + LostChangeObjectFactory, +} from 'domain/exploration/LostChangeObjectFactory'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ExplorationDataService} from '../services/exploration-data.service'; @Component({ selector: 'oppia-changes-in-human-readable-form', - template: '' + template: '', }) -class ChangesInHumanReadableFormComponentStub { -} +class ChangesInHumanReadableFormComponentStub {} class MockActiveModal { close(): void { @@ -58,7 +66,7 @@ class MockWindowRef { return; } }, - reload: (val: string) => val + reload: (val: string) => val, }, get onhashchange() { return this.location._hashChange; @@ -66,7 +74,7 @@ class MockWindowRef { set onhashchange(val) { this.location._hashChange = val; - } + }, }; get nativeWindow() { @@ -83,12 +91,14 @@ class MockExplorationDataService { } describe('Save Version Mismatch Modal Component', () => { - const lostChanges = [{ - cmd: 'add_state', - state_name: 'State name', - content_id_for_state_content: 'content_0', - content_id_for_default_outcome: 'default_outcome_1' - } as unknown as LostChange]; + const lostChanges = [ + { + cmd: 'add_state', + state_name: 'State name', + content_id_for_state_content: 'content_0', + content_id_for_default_outcome: 'default_outcome_1', + } as unknown as LostChange, + ]; let component: SaveVersionMismatchModalComponent; let fixture: ComponentFixture; @@ -102,23 +112,21 @@ describe('Save Version Mismatch Modal Component', () => { TestBed.configureTestingModule({ declarations: [ SaveVersionMismatchModalComponent, - ChangesInHumanReadableFormComponentStub + ChangesInHumanReadableFormComponentStub, ], providers: [ LostChangeObjectFactory, { provide: ExplorationDataService, - useValue: explorationDataService + useValue: explorationDataService, }, { provide: NgbActiveModal, - useClass: MockActiveModal + useClass: MockActiveModal, }, - { provide: WindowRef, - useValue: windowRef - } + {provide: WindowRef, useValue: windowRef}, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -130,69 +138,74 @@ describe('Save Version Mismatch Modal Component', () => { fixture.detectChanges(); }); - it('should remove exploration draft from local storage when modal is closed', - fakeAsync(() => { - const reloadSpy = jasmine.createSpy('reload'); - spyOnProperty(windowRef, 'nativeWindow').and.returnValue({ - location: { - _hash: '', - _hashChange: null, - hash: '', - reload: reloadSpy, - }, - onhashchange: null, - }); + it('should remove exploration draft from local storage when modal is closed', fakeAsync(() => { + const reloadSpy = jasmine.createSpy('reload'); + spyOnProperty(windowRef, 'nativeWindow').and.returnValue({ + location: { + _hash: '', + _hashChange: null, + hash: '', + reload: reloadSpy, + }, + onhashchange: null, + }); - component.discardChanges(); - tick(component.MSECS_TO_REFRESH); - fixture.detectChanges(); + component.discardChanges(); + tick(component.MSECS_TO_REFRESH); + fixture.detectChanges(); - waitForAsync(() => { - expect(explorationDataService.discardDraftAsync).toHaveBeenCalled(); - expect(reloadSpy).toHaveBeenCalled(); - }); - })); + waitForAsync(() => { + expect(explorationDataService.discardDraftAsync).toHaveBeenCalled(); + expect(reloadSpy).toHaveBeenCalled(); + }); + })); it('should contain correct modal header', () => { const modalHeader = - fixture.debugElement.nativeElement - .querySelector('.modal-header').innerText; + fixture.debugElement.nativeElement.querySelector( + '.modal-header' + ).innerText; expect(modalHeader).toBe('Error Saving Exploration'); }); it('should contain correct modal body', () => { const modalBody = - fixture.debugElement.nativeElement - .querySelector('.modal-body').children[0].innerText; + fixture.debugElement.nativeElement.querySelector('.modal-body') + .children[0].innerText; expect(modalBody).toBe( 'Sorry! Someone else has saved a new version of this exploration, so ' + - 'your pending changes cannot be saved.'); + 'your pending changes cannot be saved.' + ); }); - it('should contain description on lost changes' + - 'only if they exists in modal body', () => { - const modalBody = - fixture.debugElement.nativeElement - .querySelector('.modal-body').children[1].innerText; + it( + 'should contain description on lost changes' + + 'only if they exists in modal body', + () => { + const modalBody = + fixture.debugElement.nativeElement.querySelector('.modal-body') + .children[1].innerText; - component.hasLostChanges = true; - fixture.detectChanges(); + component.hasLostChanges = true; + fixture.detectChanges(); - expect(modalBody).toBe( - 'The lost changes are displayed below. You may want to export or ' + - 'copy and paste these changes before discarding them.'); - }); + expect(modalBody).toBe( + 'The lost changes are displayed below. You may want to export or ' + + 'copy and paste these changes before discarding them.' + ); + } + ); it('should export the lost changes and close the modal', () => { - spyOn( - fixture.elementRef.nativeElement, 'getElementsByClassName' - ).withArgs('oppia-lost-changes').and.returnValue([ - { - innerText: 'Dummy Inner Text' - } - ]); + spyOn(fixture.elementRef.nativeElement, 'getElementsByClassName') + .withArgs('oppia-lost-changes') + .and.returnValue([ + { + innerText: 'Dummy Inner Text', + }, + ]); const spyObj = jasmine.createSpyObj('a', ['click']); const reloadSpy = jasmine.createSpy('reload'); spyOnProperty(windowRef, 'nativeWindow').and.returnValue({ diff --git a/core/templates/pages/exploration-editor-page/modal-templates/save-version-mismatch-modal.component.ts b/core/templates/pages/exploration-editor-page/modal-templates/save-version-mismatch-modal.component.ts index 00b4342f6df4..b31bf153ed31 100644 --- a/core/templates/pages/exploration-editor-page/modal-templates/save-version-mismatch-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/modal-templates/save-version-mismatch-modal.component.ts @@ -16,20 +16,25 @@ * @fileoverview Component for version mismatch modal. */ -import { Component, ElementRef, Input, OnInit } from '@angular/core'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { LoggerService } from 'services/contextual/logger.service'; -import { ExplorationDataService } from 'pages/exploration-editor-page/services/exploration-data.service'; -import { LostChange, LostChangeObjectFactory } from 'domain/exploration/LostChangeObjectFactory'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component, ElementRef, Input, OnInit} from '@angular/core'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {LoggerService} from 'services/contextual/logger.service'; +import {ExplorationDataService} from 'pages/exploration-editor-page/services/exploration-data.service'; +import { + LostChange, + LostChangeObjectFactory, +} from 'domain/exploration/LostChangeObjectFactory'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-save-version-mismatch-modal', - templateUrl: './save-version-mismatch-modal.component.html' + templateUrl: './save-version-mismatch-modal.component.html', }) export class SaveVersionMismatchModalComponent - extends ConfirmOrCancelModal implements OnInit { + extends ConfirmOrCancelModal + implements OnInit +{ MSECS_TO_REFRESH: number = 20; hasLostChanges: boolean = false; // The property is initialized using Angular lifecycle hooks @@ -43,16 +48,17 @@ export class SaveVersionMismatchModalComponent private loggerService: LoggerService, private explorationDataService: ExplorationDataService, private lostChangeObjectFactory: LostChangeObjectFactory, - private ngbActiveModal: NgbActiveModal, + private ngbActiveModal: NgbActiveModal ) { super(ngbActiveModal); } ngOnInit(): void { - this.hasLostChanges = (this.lostChanges && this.lostChanges.length > 0); + this.hasLostChanges = this.lostChanges && this.lostChanges.length > 0; if (this.hasLostChanges) { this.lostChanges = this.lostChanges.map( - this.lostChangeObjectFactory.createNew); + this.lostChangeObjectFactory.createNew + ); } } @@ -72,9 +78,9 @@ export class SaveVersionMismatchModalComponent // 'getElementsByClassName' returns null if the class name is not // found, here we know that the class name is available, so we // are explicitly typecasting it to remove type error. - let lostChangesData = ( - this.elRef.nativeElement.getElementsByClassName( - 'oppia-lost-changes')[0]) as HTMLInputElement; + let lostChangesData = this.elRef.nativeElement.getElementsByClassName( + 'oppia-lost-changes' + )[0] as HTMLInputElement; let blob = new Blob([lostChangesData.innerText], {type: 'text/plain'}); var elem = document.createElement('a'); elem.href = URL.createObjectURL(blob); diff --git a/core/templates/pages/exploration-editor-page/modal-templates/state-diff-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/modal-templates/state-diff-modal.component.spec.ts index c8ce3f86f2c1..57d8871afee6 100644 --- a/core/templates/pages/exploration-editor-page/modal-templates/state-diff-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/modal-templates/state-diff-modal.component.spec.ts @@ -16,13 +16,22 @@ * @fileoverview Unit tests for StateDiffModalComponent. */ -import { State, StateObjectFactory } from 'domain/state/StateObjectFactory'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { headersAndYamlStrs, StateDiffModalComponent } from './state-diff-modal.component'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { HistoryTabYamlConversionService } from '../services/history-tab-yaml-conversion.service'; +import {State, StateObjectFactory} from 'domain/state/StateObjectFactory'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import { + headersAndYamlStrs, + StateDiffModalComponent, +} from './state-diff-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {HistoryTabYamlConversionService} from '../services/history-tab-yaml-conversion.service'; describe('State Diff Modal Component', () => { let stateObjectFactory: StateObjectFactory; @@ -40,10 +49,7 @@ describe('State Diff Modal Component', () => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], declarations: [StateDiffModalComponent], - providers: [ - NgbActiveModal, - HistoryTabYamlConversionService - ], + providers: [NgbActiveModal, HistoryTabYamlConversionService], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -53,14 +59,21 @@ describe('State Diff Modal Component', () => { component = fixture.componentInstance; stateObjectFactory = TestBed.inject(StateObjectFactory); historyTabYamlConversionService = TestBed.inject( - HistoryTabYamlConversionService); + HistoryTabYamlConversionService + ); }); beforeEach(() => { newState = stateObjectFactory.createDefaultState( - newStateName, 'content_0', 'default_outcome_1'); + newStateName, + 'content_0', + 'default_outcome_1' + ); oldState = stateObjectFactory.createDefaultState( - oldStateName, 'content_0', 'default_outcome_1'); + oldStateName, + 'content_0', + 'default_outcome_1' + ); component.headers = headers; component.newState = newState; @@ -69,35 +82,34 @@ describe('State Diff Modal Component', () => { component.oldStateName = oldStateName; }); - it('should initialize component properties after component is initialized', - fakeAsync(() => { - spyOn( - historyTabYamlConversionService, 'getYamlStringFromStateOrMetadata' - ).and.resolveTo('Yaml data'); + it('should initialize component properties after component is initialized', fakeAsync(() => { + spyOn( + historyTabYamlConversionService, + 'getYamlStringFromStateOrMetadata' + ).and.resolveTo('Yaml data'); - component.ngOnInit(); - tick(201); + component.ngOnInit(); + tick(201); - fixture.whenStable() - .then(() => { - expect(component.headers).toBe(headers); - expect(component.newStateName).toBe(newStateName); - expect(component.oldStateName).toBe(oldStateName); - }); - })); + fixture.whenStable().then(() => { + expect(component.headers).toBe(headers); + expect(component.newStateName).toBe(newStateName); + expect(component.oldStateName).toBe(oldStateName); + }); + })); it('should evaluate yaml strings object', fakeAsync(() => { spyOn( - historyTabYamlConversionService, 'getYamlStringFromStateOrMetadata' + historyTabYamlConversionService, + 'getYamlStringFromStateOrMetadata' ).and.resolveTo('Yaml data'); component.ngOnInit(); tick(201); - fixture.whenStable() - .then(() => { - expect(component.yamlStrs.leftPane).toBe('Yaml data'); - expect(component.yamlStrs.rightPane).toBe('Yaml data'); - }); + fixture.whenStable().then(() => { + expect(component.yamlStrs.leftPane).toBe('Yaml data'); + expect(component.yamlStrs.rightPane).toBe('Yaml data'); + }); })); }); diff --git a/core/templates/pages/exploration-editor-page/modal-templates/state-diff-modal.component.ts b/core/templates/pages/exploration-editor-page/modal-templates/state-diff-modal.component.ts index aaa6a47fa5b4..729d10712e4d 100644 --- a/core/templates/pages/exploration-editor-page/modal-templates/state-diff-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/modal-templates/state-diff-modal.component.ts @@ -16,12 +16,12 @@ * @fileoverview Component for state diff modal. */ -import { Input, OnInit } from '@angular/core'; -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { State } from 'domain/state/StateObjectFactory'; -import { HistoryTabYamlConversionService } from '../services/history-tab-yaml-conversion.service'; +import {Input, OnInit} from '@angular/core'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {State} from 'domain/state/StateObjectFactory'; +import {HistoryTabYamlConversionService} from '../services/history-tab-yaml-conversion.service'; export interface headersAndYamlStrs { leftPane: string; @@ -40,7 +40,9 @@ interface mergeviewOptions { templateUrl: './state-diff-modal.component.html', }) export class StateDiffModalComponent - extends ConfirmOrCancelModal implements OnInit { + extends ConfirmOrCancelModal + implements OnInit +{ // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -61,12 +63,12 @@ export class StateDiffModalComponent lineNumbers: true, readOnly: true, mode: 'yaml', - viewportMargin: 100 + viewportMargin: 100, }; constructor( - private ngbActiveModal: NgbActiveModal, - private historyTabYamlConversionService: HistoryTabYamlConversionService + private ngbActiveModal: NgbActiveModal, + private historyTabYamlConversionService: HistoryTabYamlConversionService ) { super(ngbActiveModal); } @@ -74,13 +76,13 @@ export class StateDiffModalComponent ngOnInit(): void { this.historyTabYamlConversionService .getYamlStringFromStateOrMetadata(this.oldState) - .then((result) => { + .then(result => { this.yamlStrs.leftPane = result; }); this.historyTabYamlConversionService .getYamlStringFromStateOrMetadata(this.newState) - .then((result) => { + .then(result => { this.yamlStrs.rightPane = result; }); } diff --git a/core/templates/pages/exploration-editor-page/modal-templates/state-version-history-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/modal-templates/state-version-history-modal.component.spec.ts index bb5e8b5dc9e2..763ef9573ed3 100644 --- a/core/templates/pages/exploration-editor-page/modal-templates/state-version-history-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/modal-templates/state-version-history-modal.component.spec.ts @@ -16,16 +16,28 @@ * @fileoverview Unit tests for state version history modal. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ContextService } from 'services/context.service'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { HistoryTabYamlConversionService } from '../services/history-tab-yaml-conversion.service'; -import { VersionHistoryBackendApiService } from '../services/version-history-backend-api.service'; -import { StateDiffData, VersionHistoryService } from '../services/version-history.service'; -import { StateVersionHistoryModalComponent } from './state-version-history-modal.component'; -import { StateBackendDict, StateObjectFactory } from 'domain/state/StateObjectFactory'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ContextService} from 'services/context.service'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {HistoryTabYamlConversionService} from '../services/history-tab-yaml-conversion.service'; +import {VersionHistoryBackendApiService} from '../services/version-history-backend-api.service'; +import { + StateDiffData, + VersionHistoryService, +} from '../services/version-history.service'; +import {StateVersionHistoryModalComponent} from './state-version-history-modal.component'; +import { + StateBackendDict, + StateObjectFactory, +} from 'domain/state/StateObjectFactory'; describe('State version history modal', () => { let component: StateVersionHistoryModalComponent; @@ -46,9 +58,9 @@ describe('State version history modal', () => { ContextService, VersionHistoryService, VersionHistoryBackendApiService, - HistoryTabYamlConversionService + HistoryTabYamlConversionService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -57,7 +69,8 @@ describe('State version history modal', () => { component = fixture.componentInstance; stateObjectFactory = TestBed.inject(StateObjectFactory); historyTabYamlConversionService = TestBed.inject( - HistoryTabYamlConversionService); + HistoryTabYamlConversionService + ); versionHistoryService = TestBed.inject(VersionHistoryService); versionHistoryBackendApiService = TestBed.inject( VersionHistoryBackendApiService @@ -68,102 +81,105 @@ describe('State version history modal', () => { classifier_model_id: null, content: { content_id: 'content', - html: '' + html: '', }, recorded_voiceovers: { voiceovers_mapping: { content: {}, - default_outcome: {} - } + default_outcome: {}, + }, }, interaction: { answer_groups: [], confirmed_unclassified_answers: [], customization_args: { rows: { - value: 1 + value: 1, }, placeholder: { value: { unicode_str: 'Type your answer here.', - content_id: '' - } - } + content_id: '', + }, + }, }, default_outcome: { dest: '(untitled state)', dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: '' + html: '', }, param_changes: [], labelled_as_correct: false, refresher_exploration_id: null, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, hints: [], solution: null, - id: 'TextInput' + id: 'TextInput', }, linked_skill_id: null, param_changes: [], solicit_answer_details: false, - card_is_checkpoint: false + card_is_checkpoint: false, }; }); it('should get whether we can explore backward version history', () => { spyOn( - versionHistoryService, 'canShowBackwardStateDiffData' + versionHistoryService, + 'canShowBackwardStateDiffData' ).and.returnValue(true); expect(component.canExploreBackwardVersionHistory()).toBeTrue(); }); it('should get whether we can explore forward version history', () => { - spyOn( - versionHistoryService, 'canShowForwardStateDiffData' - ).and.returnValue(true); + spyOn(versionHistoryService, 'canShowForwardStateDiffData').and.returnValue( + true + ); expect(component.canExploreForwardVersionHistory()).toBeTrue(); }); it('should get the last edited version number', () => { const stateData = stateObjectFactory.createFromBackendDict( - 'State', stateObject); + 'State', + stateObject + ); spyOn(versionHistoryService, 'getBackwardStateDiffData').and.returnValue({ oldState: stateData, newState: stateData, oldVersionNumber: 2, newVersionNumber: 3, - committerUsername: '' + committerUsername: '', }); expect(component.getLastEditedVersionNumber()).toEqual(2); }); it('should throw error when last edited version number is null', () => { - spyOn( - versionHistoryService, 'getBackwardStateDiffData' - ).and.returnValue({ - oldVersionNumber: null + spyOn(versionHistoryService, 'getBackwardStateDiffData').and.returnValue({ + oldVersionNumber: null, } as StateDiffData); - expect( - () =>component.getLastEditedVersionNumber() - ).toThrowError('Last edited version number cannot be null'); + expect(() => component.getLastEditedVersionNumber()).toThrowError( + 'Last edited version number cannot be null' + ); }); it('should get the last edited committer username', () => { const stateData = stateObjectFactory.createFromBackendDict( - 'State', stateObject); + 'State', + stateObject + ); spyOn(versionHistoryService, 'getBackwardStateDiffData').and.returnValue({ oldState: stateData, newState: stateData, oldVersionNumber: 2, newVersionNumber: 3, - committerUsername: 'some' + committerUsername: 'some', }); expect(component.getLastEditedCommitterUsername()).toEqual('some'); @@ -171,167 +187,206 @@ describe('State version history modal', () => { it('should get the next edited version number', () => { const stateData = stateObjectFactory.createFromBackendDict( - 'State', stateObject); + 'State', + stateObject + ); spyOn(versionHistoryService, 'getForwardStateDiffData').and.returnValue({ oldState: stateData, newState: stateData, oldVersionNumber: 3, newVersionNumber: 2, - committerUsername: 'some' + committerUsername: 'some', }); expect(component.getNextEditedVersionNumber()).toEqual(3); }); it('should throw error when next edited version number is null', () => { - spyOn( - versionHistoryService, 'getForwardStateDiffData' - ).and.returnValue({ - oldVersionNumber: null + spyOn(versionHistoryService, 'getForwardStateDiffData').and.returnValue({ + oldVersionNumber: null, } as StateDiffData); - expect( - () =>component.getNextEditedVersionNumber() - ).toThrowError('Next edited version number cannot be null'); + expect(() => component.getNextEditedVersionNumber()).toThrowError( + 'Next edited version number cannot be null' + ); }); it('should get the next edited committer username', () => { const stateData = stateObjectFactory.createFromBackendDict( - 'State', stateObject); + 'State', + stateObject + ); spyOn(versionHistoryService, 'getForwardStateDiffData').and.returnValue({ oldState: stateData, newState: stateData, oldVersionNumber: 3, newVersionNumber: 2, - committerUsername: 'some' + committerUsername: 'some', }); expect(component.getNextEditedCommitterUsername()).toEqual('some'); }); - it('should update the left and the right side yaml strings on exploring' + - ' forward version history', fakeAsync(() => { - const stateData = stateObjectFactory.createFromBackendDict( - 'State', stateObject); - spyOn(versionHistoryService, 'getForwardStateDiffData').and.returnValue({ - oldState: stateData, - newState: stateData, - oldVersionNumber: 3, - newVersionNumber: 2, - committerUsername: 'some' - }); - spyOn( - historyTabYamlConversionService, 'getYamlStringFromStateOrMetadata' - ).and.resolveTo('YAML STRING'); - - expect(component.yamlStrs.previousVersionStateYaml).toEqual(''); - expect(component.yamlStrs.currentVersionStateYaml).toEqual(''); - - component.onClickExploreForwardVersionHistory(); - tick(); - - expect(component.yamlStrs.previousVersionStateYaml).toEqual('YAML STRING'); - expect(component.yamlStrs.currentVersionStateYaml).toEqual('YAML STRING'); - })); - - it('should throw error on exploring forward version history when state' + - ' names from version history data are not defined', () => { - spyOn(versionHistoryService, 'getForwardStateDiffData').and.returnValues({ - oldState: stateObjectFactory.createFromBackendDict( - 'State', stateObject), - newState: stateObjectFactory.createFromBackendDict( - null, stateObject), - oldVersionNumber: 3 - } as StateDiffData, { - oldState: stateObjectFactory.createFromBackendDict( - null, stateObject), - newState: stateObjectFactory.createFromBackendDict( - 'State', stateObject), - oldVersionNumber: 3 - } as StateDiffData); + it( + 'should update the left and the right side yaml strings on exploring' + + ' forward version history', + fakeAsync(() => { + const stateData = stateObjectFactory.createFromBackendDict( + 'State', + stateObject + ); + spyOn(versionHistoryService, 'getForwardStateDiffData').and.returnValue({ + oldState: stateData, + newState: stateData, + oldVersionNumber: 3, + newVersionNumber: 2, + committerUsername: 'some', + }); + spyOn( + historyTabYamlConversionService, + 'getYamlStringFromStateOrMetadata' + ).and.resolveTo('YAML STRING'); - expect( - () => component.onClickExploreForwardVersionHistory() - ).toThrowError('State name cannot be null'); - expect( - () => component.onClickExploreForwardVersionHistory() - ).toThrowError('State name cannot be null'); - }); + expect(component.yamlStrs.previousVersionStateYaml).toEqual(''); + expect(component.yamlStrs.currentVersionStateYaml).toEqual(''); - it('should update the left and the right side yaml strings on exploring' + - ' backward version history', fakeAsync(() => { - const stateData = stateObjectFactory.createFromBackendDict( - 'State', stateObject); - spyOn(versionHistoryService, 'getBackwardStateDiffData').and.returnValue({ - oldState: stateData, - newState: stateData, - oldVersionNumber: 2, - newVersionNumber: 3, - committerUsername: 'some' - }); - spyOn( - historyTabYamlConversionService, 'getYamlStringFromStateOrMetadata' - ).and.resolveTo('YAML STRING'); - spyOn(component, 'fetchPreviousVersionHistory').and.callFake(() => {}); - - expect(component.yamlStrs.previousVersionStateYaml).toEqual(''); - expect(component.yamlStrs.currentVersionStateYaml).toEqual(''); + component.onClickExploreForwardVersionHistory(); + tick(); - component.onClickExploreBackwardVersionHistory(); - tick(); + expect(component.yamlStrs.previousVersionStateYaml).toEqual( + 'YAML STRING' + ); + expect(component.yamlStrs.currentVersionStateYaml).toEqual('YAML STRING'); + }) + ); + + it( + 'should throw error on exploring forward version history when state' + + ' names from version history data are not defined', + () => { + spyOn(versionHistoryService, 'getForwardStateDiffData').and.returnValues( + { + oldState: stateObjectFactory.createFromBackendDict( + 'State', + stateObject + ), + newState: stateObjectFactory.createFromBackendDict(null, stateObject), + oldVersionNumber: 3, + } as StateDiffData, + { + oldState: stateObjectFactory.createFromBackendDict(null, stateObject), + newState: stateObjectFactory.createFromBackendDict( + 'State', + stateObject + ), + oldVersionNumber: 3, + } as StateDiffData + ); + + expect(() => + component.onClickExploreForwardVersionHistory() + ).toThrowError('State name cannot be null'); + expect(() => + component.onClickExploreForwardVersionHistory() + ).toThrowError('State name cannot be null'); + } + ); + + it( + 'should update the left and the right side yaml strings on exploring' + + ' backward version history', + fakeAsync(() => { + const stateData = stateObjectFactory.createFromBackendDict( + 'State', + stateObject + ); + spyOn(versionHistoryService, 'getBackwardStateDiffData').and.returnValue({ + oldState: stateData, + newState: stateData, + oldVersionNumber: 2, + newVersionNumber: 3, + committerUsername: 'some', + }); + spyOn( + historyTabYamlConversionService, + 'getYamlStringFromStateOrMetadata' + ).and.resolveTo('YAML STRING'); + spyOn(component, 'fetchPreviousVersionHistory').and.callFake(() => {}); - expect(component.yamlStrs.previousVersionStateYaml).toEqual('YAML STRING'); - expect(component.yamlStrs.currentVersionStateYaml).toEqual('YAML STRING'); - })); + expect(component.yamlStrs.previousVersionStateYaml).toEqual(''); + expect(component.yamlStrs.currentVersionStateYaml).toEqual(''); - it('should throw error on exploring backward version history when state' + - ' names from version history data are not defined', () => { - spyOn(versionHistoryService, 'getBackwardStateDiffData').and.returnValues({ - oldState: stateObjectFactory.createFromBackendDict( - 'State', stateObject), - newState: stateObjectFactory.createFromBackendDict( - null, stateObject), - oldVersionNumber: 3 - } as StateDiffData, { - oldState: stateObjectFactory.createFromBackendDict( - null, stateObject), - newState: stateObjectFactory.createFromBackendDict( - 'State', stateObject), - oldVersionNumber: 3 - } as StateDiffData); + component.onClickExploreBackwardVersionHistory(); + tick(); - expect( - () => component.onClickExploreBackwardVersionHistory() - ).toThrowError('State name cannot be null'); - expect( - () => component.onClickExploreBackwardVersionHistory() - ).toThrowError('State name cannot be null'); - }); + expect(component.yamlStrs.previousVersionStateYaml).toEqual( + 'YAML STRING' + ); + expect(component.yamlStrs.currentVersionStateYaml).toEqual('YAML STRING'); + }) + ); + + it( + 'should throw error on exploring backward version history when state' + + ' names from version history data are not defined', + () => { + spyOn(versionHistoryService, 'getBackwardStateDiffData').and.returnValues( + { + oldState: stateObjectFactory.createFromBackendDict( + 'State', + stateObject + ), + newState: stateObjectFactory.createFromBackendDict(null, stateObject), + oldVersionNumber: 3, + } as StateDiffData, + { + oldState: stateObjectFactory.createFromBackendDict(null, stateObject), + newState: stateObjectFactory.createFromBackendDict( + 'State', + stateObject + ), + oldVersionNumber: 3, + } as StateDiffData + ); + + expect(() => + component.onClickExploreBackwardVersionHistory() + ).toThrowError('State name cannot be null'); + expect(() => + component.onClickExploreBackwardVersionHistory() + ).toThrowError('State name cannot be null'); + } + ); it('should be able to fetch the backward version history', fakeAsync(() => { spyOn( - versionHistoryService, 'shouldFetchNewStateVersionHistory' + versionHistoryService, + 'shouldFetchNewStateVersionHistory' ).and.returnValues(false, true); spyOn( - versionHistoryService, 'insertStateVersionHistoryData' + versionHistoryService, + 'insertStateVersionHistoryData' ).and.callThrough(); spyOn(contextService, 'getExplorationId').and.returnValue('exp_1'); const stateData = stateObjectFactory.createFromBackendDict( - 'State', stateObject); + 'State', + stateObject + ); spyOn(versionHistoryService, 'getBackwardStateDiffData').and.returnValue({ oldState: stateData, newState: stateData, oldVersionNumber: 2, newVersionNumber: 3, - committerUsername: 'some' + committerUsername: 'some', }); spyOn( - versionHistoryBackendApiService, 'fetchStateVersionHistoryAsync' + versionHistoryBackendApiService, + 'fetchStateVersionHistoryAsync' ).and.resolveTo({ lastEditedVersionNumber: 3, stateNameInPreviousVersion: 'State', stateInPreviousVersion: stateData, - lastEditedCommitterUsername: '' + lastEditedCommitterUsername: '', }); versionHistoryService.setCurrentPositionInStateVersionHistoryList(0); @@ -350,51 +405,64 @@ describe('State version history modal', () => { ).toEqual(1); })); - it('should throw error while fetching previous version history data if the ' + - 'state data for previous version is not available', () => { - spyOn( - versionHistoryService, 'shouldFetchNewStateVersionHistory' - ).and.returnValue(true); - const stateData = stateObjectFactory.createFromBackendDict( - null, stateObject); - spyOn(versionHistoryService, 'getBackwardStateDiffData').and.returnValues({ - oldState: null, - newState: null, - oldVersionNumber: 2, - newVersionNumber: 3, - committerUsername: 'some' - }, { - oldState: stateData, - newState: stateData, - oldVersionNumber: 2, - newVersionNumber: 3, - committerUsername: 'some' - }); + it( + 'should throw error while fetching previous version history data if the ' + + 'state data for previous version is not available', + () => { + spyOn( + versionHistoryService, + 'shouldFetchNewStateVersionHistory' + ).and.returnValue(true); + const stateData = stateObjectFactory.createFromBackendDict( + null, + stateObject + ); + spyOn(versionHistoryService, 'getBackwardStateDiffData').and.returnValues( + { + oldState: null, + newState: null, + oldVersionNumber: 2, + newVersionNumber: 3, + committerUsername: 'some', + }, + { + oldState: stateData, + newState: stateData, + oldVersionNumber: 2, + newVersionNumber: 3, + committerUsername: 'some', + } + ); - expect(() => component.fetchPreviousVersionHistory()).toThrowError( - 'The state data for the previous version is not available.' - ); - expect(() => component.fetchPreviousVersionHistory()).toThrowError( - 'The name of the state in the previous version was not specified.' - ); - }); + expect(() => component.fetchPreviousVersionHistory()).toThrowError( + 'The state data for the previous version is not available.' + ); + expect(() => component.fetchPreviousVersionHistory()).toThrowError( + 'The name of the state in the previous version was not specified.' + ); + } + ); it('should be show error message if the backend api fails', fakeAsync(() => { spyOn( - versionHistoryService, 'shouldFetchNewStateVersionHistory' + versionHistoryService, + 'shouldFetchNewStateVersionHistory' ).and.returnValue(true); spyOn(contextService, 'getExplorationId').and.returnValue('exp_1'); const stateData = stateObjectFactory.createFromBackendDict( - 'State', stateObject); + 'State', + stateObject + ); spyOn(versionHistoryService, 'getBackwardStateDiffData').and.returnValue({ oldState: stateData, newState: stateData, oldVersionNumber: 2, newVersionNumber: 3, - committerUsername: 'some' + committerUsername: 'some', }); spyOn( - versionHistoryBackendApiService, 'fetchStateVersionHistoryAsync' + versionHistoryBackendApiService, + 'fetchStateVersionHistoryAsync' ).and.resolveTo(null); expect(component.validationErrorIsShown).toBeFalse(); @@ -405,22 +473,20 @@ describe('State version history modal', () => { expect(component.validationErrorIsShown).toBeTrue(); })); - it('should update the left and right side yaml strings on initialization', - fakeAsync(() => { - spyOn( - historyTabYamlConversionService, 'getYamlStringFromStateOrMetadata' - ).and.resolveTo('YAML STRING'); - spyOn(component, 'fetchPreviousVersionHistory').and.callFake(() => {}); + it('should update the left and right side yaml strings on initialization', fakeAsync(() => { + spyOn( + historyTabYamlConversionService, + 'getYamlStringFromStateOrMetadata' + ).and.resolveTo('YAML STRING'); + spyOn(component, 'fetchPreviousVersionHistory').and.callFake(() => {}); - component.ngOnInit(); - tick(); + component.ngOnInit(); + tick(); - expect( - component.yamlStrs.previousVersionStateYaml - ).toEqual('YAML STRING'); - expect(component.yamlStrs.currentVersionStateYaml).toEqual('YAML STRING'); - expect(component.fetchPreviousVersionHistory).toHaveBeenCalled(); - })); + expect(component.yamlStrs.previousVersionStateYaml).toEqual('YAML STRING'); + expect(component.yamlStrs.currentVersionStateYaml).toEqual('YAML STRING'); + expect(component.fetchPreviousVersionHistory).toHaveBeenCalled(); + })); it('should get the last edited version number in case of error', () => { versionHistoryService.insertStateVersionHistoryData(4, null, ''); diff --git a/core/templates/pages/exploration-editor-page/modal-templates/state-version-history-modal.component.ts b/core/templates/pages/exploration-editor-page/modal-templates/state-version-history-modal.component.ts index 6604e821ceee..0b634a772fd1 100644 --- a/core/templates/pages/exploration-editor-page/modal-templates/state-version-history-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/modal-templates/state-version-history-modal.component.ts @@ -16,15 +16,15 @@ * @fileoverview Component for state changes modal. */ -import { Input, OnInit } from '@angular/core'; -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { State } from 'domain/state/StateObjectFactory'; -import { ContextService } from 'services/context.service'; -import { HistoryTabYamlConversionService } from '../services/history-tab-yaml-conversion.service'; -import { VersionHistoryBackendApiService } from '../services/version-history-backend-api.service'; -import { VersionHistoryService } from '../services/version-history.service'; +import {Input, OnInit} from '@angular/core'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {State} from 'domain/state/StateObjectFactory'; +import {ContextService} from 'services/context.service'; +import {HistoryTabYamlConversionService} from '../services/history-tab-yaml-conversion.service'; +import {VersionHistoryBackendApiService} from '../services/version-history-backend-api.service'; +import {VersionHistoryService} from '../services/version-history.service'; interface HeadersAndYamlStrs { previousVersionStateYaml: string; @@ -43,7 +43,9 @@ interface MergeviewOptions { templateUrl: './state-version-history-modal.component.html', }) export class StateVersionHistoryModalComponent - extends ConfirmOrCancelModal implements OnInit { + extends ConfirmOrCancelModal + implements OnInit +{ @Input() committerUsername!: string; @Input() oldVersion: number | null = null; @Input() newState!: State; @@ -60,15 +62,15 @@ export class StateVersionHistoryModalComponent lineNumbers: true, readOnly: true, mode: 'yaml', - viewportMargin: 100 + viewportMargin: 100, }; constructor( - private ngbActiveModal: NgbActiveModal, - private contextService: ContextService, - private versionHistoryService: VersionHistoryService, - private versionHistoryBackendApiService: VersionHistoryBackendApiService, - private historyTabYamlConversionService: HistoryTabYamlConversionService + private ngbActiveModal: NgbActiveModal, + private contextService: ContextService, + private versionHistoryService: VersionHistoryService, + private versionHistoryBackendApiService: VersionHistoryBackendApiService, + private historyTabYamlConversionService: HistoryTabYamlConversionService ) { super(ngbActiveModal); } @@ -96,15 +98,14 @@ export class StateVersionHistoryModalComponent } getLastEditedVersionNumberInCaseOfError(): number { - return ( - this.versionHistoryService.fetchedStateVersionNumbers[ - this.versionHistoryService - .getCurrentPositionInStateVersionHistoryList()] as number); + return this.versionHistoryService.fetchedStateVersionNumbers[ + this.versionHistoryService.getCurrentPositionInStateVersionHistoryList() + ] as number; } getLastEditedCommitterUsername(): string { - return ( - this.versionHistoryService.getBackwardStateDiffData().committerUsername); + return this.versionHistoryService.getBackwardStateDiffData() + .committerUsername; } getNextEditedVersionNumber(): number { @@ -122,8 +123,8 @@ export class StateVersionHistoryModalComponent } getNextEditedCommitterUsername(): string { - return ( - this.versionHistoryService.getForwardStateDiffData().committerUsername); + return this.versionHistoryService.getForwardStateDiffData() + .committerUsername; } onClickExploreForwardVersionHistory(): void { @@ -165,9 +166,7 @@ export class StateVersionHistoryModalComponent this.validationErrorIsShown = false; - this - .versionHistoryService - .decrementCurrentPositionInStateVersionHistoryList(); + this.versionHistoryService.decrementCurrentPositionInStateVersionHistoryList(); } onClickExploreBackwardVersionHistory(): void { @@ -218,8 +217,7 @@ export class StateVersionHistoryModalComponent fetchPreviousVersionHistory(): void { if (!this.versionHistoryService.shouldFetchNewStateVersionHistory()) { - this.versionHistoryService - .incrementCurrentPositionInStateVersionHistoryList(); + this.versionHistoryService.incrementCurrentPositionInStateVersionHistoryList(); return; } const diffData = this.versionHistoryService.getBackwardStateDiffData(); @@ -244,31 +242,32 @@ export class StateVersionHistoryModalComponent ); } if (diffData.oldVersionNumber !== null) { - this.versionHistoryBackendApiService.fetchStateVersionHistoryAsync( - this.contextService.getExplorationId(), - diffData.oldState.name, diffData.oldVersionNumber - ).then((response) => { - if (response !== null) { - this.versionHistoryService.insertStateVersionHistoryData( - response.lastEditedVersionNumber, - response.stateInPreviousVersion, - response.lastEditedCommitterUsername - ); - this.versionHistoryService - .incrementCurrentPositionInStateVersionHistoryList(); - } else { - this.validationErrorIsShown = true; - this.versionHistoryService - .incrementCurrentPositionInStateVersionHistoryList(); - } - }); + this.versionHistoryBackendApiService + .fetchStateVersionHistoryAsync( + this.contextService.getExplorationId(), + diffData.oldState.name, + diffData.oldVersionNumber + ) + .then(response => { + if (response !== null) { + this.versionHistoryService.insertStateVersionHistoryData( + response.lastEditedVersionNumber, + response.stateInPreviousVersion, + response.lastEditedCommitterUsername + ); + this.versionHistoryService.incrementCurrentPositionInStateVersionHistoryList(); + } else { + this.validationErrorIsShown = true; + this.versionHistoryService.incrementCurrentPositionInStateVersionHistoryList(); + } + }); } } updateLeftPane(): void { this.historyTabYamlConversionService .getYamlStringFromStateOrMetadata(this.oldState) - .then((result) => { + .then(result => { this.yamlStrs.previousVersionStateYaml = result; }); } @@ -276,7 +275,7 @@ export class StateVersionHistoryModalComponent updateRightPane(): void { this.historyTabYamlConversionService .getYamlStringFromStateOrMetadata(this.newState) - .then((result) => { + .then(result => { this.yamlStrs.currentVersionStateYaml = result; }); } diff --git a/core/templates/pages/exploration-editor-page/modal-templates/welcome-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/modal-templates/welcome-modal.component.spec.ts index 8e049743666e..73b6ff0c8bd9 100644 --- a/core/templates/pages/exploration-editor-page/modal-templates/welcome-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/modal-templates/welcome-modal.component.spec.ts @@ -16,21 +16,19 @@ * @fileoverview Unit tests for WelcomeModalComponent. */ -import { Component, NO_ERRORS_SCHEMA, ElementRef } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; -import { ContextService } from 'services/context.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { WelcomeModalComponent } from './welcome-modal.component'; +import {Component, NO_ERRORS_SCHEMA, ElementRef} from '@angular/core'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {ContextService} from 'services/context.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {WelcomeModalComponent} from './welcome-modal.component'; @Component({ selector: 'oppia-changes-in-human-readable-form', - template: '' + template: '', }) -class ChangesInHumanReadableFormComponentStub { -} +class ChangesInHumanReadableFormComponentStub {} class MockActiveModal { close(): void { @@ -55,17 +53,18 @@ describe('Welcome Modal Component', () => { TestBed.configureTestingModule({ declarations: [ WelcomeModalComponent, - ChangesInHumanReadableFormComponentStub + ChangesInHumanReadableFormComponentStub, ], providers: [ ContextService, SiteAnalyticsService, - UrlInterpolationService, { + UrlInterpolationService, + { provide: NgbActiveModal, - useClass: MockActiveModal + useClass: MockActiveModal, }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -77,22 +76,24 @@ describe('Welcome Modal Component', () => { contextService = TestBed.inject(ContextService); siteAnalyticsService = TestBed.inject(SiteAnalyticsService); siteAnalyticsServiceSpy = spyOn( - siteAnalyticsService, 'registerTutorialModalOpenEvent'); + siteAnalyticsService, + 'registerTutorialModalOpenEvent' + ); spyOn(contextService, 'getExplorationId').and.returnValue(explorationId); fixture.detectChanges(); }); - it ('should evaluate exploration id when component is initialized', () => { + it('should evaluate exploration id when component is initialized', () => { const welcomeModalRef = new ElementRef(document.createElement('h1')); component.welcomeHeading = welcomeModalRef; expect(component.explorationId).toBe(explorationId); expect(component.editorWelcomeImgUrl).toBe( - '/assets/images/general/editor_welcome.svg'); - expect(siteAnalyticsServiceSpy) - .toHaveBeenCalled(); + '/assets/images/general/editor_welcome.svg' + ); + expect(siteAnalyticsServiceSpy).toHaveBeenCalled(); }); - it ('should close the modal', () => { + it('should close the modal', () => { const welcomeModalRef = new ElementRef(document.createElement('h1')); component.welcomeHeading = welcomeModalRef; const dismissSpy = spyOn(ngbActiveModal, 'dismiss').and.callThrough(); diff --git a/core/templates/pages/exploration-editor-page/modal-templates/welcome-modal.component.ts b/core/templates/pages/exploration-editor-page/modal-templates/welcome-modal.component.ts index fb3454934824..234c573667ba 100644 --- a/core/templates/pages/exploration-editor-page/modal-templates/welcome-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/modal-templates/welcome-modal.component.ts @@ -15,19 +15,21 @@ /** * @fileoverview Component for welcome modal. */ -import { Component, Input, OnInit, ViewChild, ElementRef } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { ContextService } from 'services/context.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; +import {Component, Input, OnInit, ViewChild, ElementRef} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {ContextService} from 'services/context.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; @Component({ selector: 'oppia-welcome-modal', - templateUrl: './welcome-modal.component.html' + templateUrl: './welcome-modal.component.html', }) export class WelcomeModalComponent - extends ConfirmOrCancelModal implements OnInit { + extends ConfirmOrCancelModal + implements OnInit +{ // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -39,7 +41,7 @@ export class WelcomeModalComponent private ngbActiveModal: NgbActiveModal, private contextService: ContextService, private siteAnalyticsService: SiteAnalyticsService, - private urlInterpolationService: UrlInterpolationService, + private urlInterpolationService: UrlInterpolationService ) { super(ngbActiveModal); } @@ -47,9 +49,11 @@ export class WelcomeModalComponent ngOnInit(): void { this.explorationId = this.contextService.getExplorationId(); this.siteAnalyticsService.registerTutorialModalOpenEvent( - this.explorationId); + this.explorationId + ); this.editorWelcomeImgUrl = this.urlInterpolationService.getStaticImageUrl( - '/general/editor_welcome.svg'); + '/general/editor_welcome.svg' + ); this.welcomeHeading?.nativeElement.focus(); } diff --git a/core/templates/pages/exploration-editor-page/param-changes-editor/param-changes-editor.component.spec.ts b/core/templates/pages/exploration-editor-page/param-changes-editor/param-changes-editor.component.spec.ts index 3f4924e08540..92d432605a5d 100644 --- a/core/templates/pages/exploration-editor-page/param-changes-editor/param-changes-editor.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/param-changes-editor/param-changes-editor.component.spec.ts @@ -16,26 +16,38 @@ * @fileoverview Unit tests for paramChangesEditor. */ -import { CdkDragSortEvent } from '@angular/cdk/drag-drop'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { StateParamChangesService } from 'components/state-editor/state-editor-properties-services/state-param-changes.service'; -import { ParamChange, ParamChangeObjectFactory } from 'domain/exploration/ParamChangeObjectFactory'; -import { ParamSpecs, ParamSpecsObjectFactory } from 'domain/exploration/ParamSpecsObjectFactory'; -import { AlertsService } from 'services/alerts.service'; -import { EditabilityService } from 'services/editability.service'; -import { ExternalSaveService } from 'services/external-save.service'; -import { ExplorationDataService } from '../services/exploration-data.service'; -import { ExplorationParamSpecsService } from '../services/exploration-param-specs.service'; -import { ExplorationStatesService } from '../services/exploration-states.service'; -import { ParamChangesEditorComponent } from './param-changes-editor.component'; +import {CdkDragSortEvent} from '@angular/cdk/drag-drop'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {StateParamChangesService} from 'components/state-editor/state-editor-properties-services/state-param-changes.service'; +import { + ParamChange, + ParamChangeObjectFactory, +} from 'domain/exploration/ParamChangeObjectFactory'; +import { + ParamSpecs, + ParamSpecsObjectFactory, +} from 'domain/exploration/ParamSpecsObjectFactory'; +import {AlertsService} from 'services/alerts.service'; +import {EditabilityService} from 'services/editability.service'; +import {ExternalSaveService} from 'services/external-save.service'; +import {ExplorationDataService} from '../services/exploration-data.service'; +import {ExplorationParamSpecsService} from '../services/exploration-param-specs.service'; +import {ExplorationStatesService} from '../services/exploration-states.service'; +import {ParamChangesEditorComponent} from './param-changes-editor.component'; class MockNgbModal { open() { return { - result: Promise.resolve() + result: Promise.resolve(), }; } } @@ -56,9 +68,7 @@ describe('Param Changes Editor Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - ParamChangesEditorComponent - ], + declarations: [ParamChangesEditorComponent], providers: [ { provide: ExplorationDataService, @@ -66,25 +76,24 @@ describe('Param Changes Editor Component', () => { explorationId: 0, autosaveChangeListAsync() { return; - } - } + }, + }, }, { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, { provide: ExternalSaveService, useValue: { - onExternalSave: mockExternalSaveEventEmitter - } - } + onExternalSave: mockExternalSaveEventEmitter, + }, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); - beforeEach(() => { fixture = TestBed.createComponent(ParamChangesEditorComponent); component = fixture.componentInstance; @@ -94,18 +103,18 @@ describe('Param Changes Editor Component', () => { paramSpecsObjectFactory = TestBed.inject(ParamSpecsObjectFactory); stateParamChangesService = TestBed.inject(StateParamChangesService); editabilityService = TestBed.inject(EditabilityService); - explorationParamSpecsService = TestBed.inject( - ExplorationParamSpecsService); + explorationParamSpecsService = TestBed.inject(ExplorationParamSpecsService); explorationStatesService = TestBed.inject(ExplorationStatesService); explorationParamSpecsService.init( paramSpecsObjectFactory.createFromBackendDict({ y: { - obj_type: 'UnicodeString' + obj_type: 'UnicodeString', }, a: { - obj_type: 'UnicodeString' - } - }) as ParamSpecs); + obj_type: 'UnicodeString', + }, + }) as ParamSpecs + ); stateParamChangesService.init('', []); @@ -123,92 +132,96 @@ describe('Param Changes Editor Component', () => { component.ngOnDestroy(); }); - it('should initialize component properties after controller is initialized', + it('should initialize component properties after controller is initialized', () => { + expect(component.isParamChangesEditorOpen).toBe(false); + expect(component.warningText).toBe(''); + expect(component.paramNameChoices).toEqual([]); + }); + + it( + 'should reset customization args from param change when changing' + + ' generator type', () => { - expect(component.isParamChangesEditorOpen).toBe(false); - expect(component.warningText).toBe(''); - expect(component.paramNameChoices).toEqual([]); - }); - - it('should reset customization args from param change when changing' + - ' generator type', () => { - let paramChange = paramChangeObjectFactory.createFromBackendDict({ - customization_args: { - list_of_values: ['first value', 'second value'] - }, - generator_id: 'RandomSelector', - name: 'a' - }); + let paramChange = paramChangeObjectFactory.createFromBackendDict({ + customization_args: { + list_of_values: ['first value', 'second value'], + }, + generator_id: 'RandomSelector', + name: 'a', + }); + + component.onChangeGeneratorType(paramChange); - component.onChangeGeneratorType(paramChange); + expect(paramChange.customizationArgs).toEqual({ + list_of_values: ['sample value'], + }); + } + ); - expect(paramChange.customizationArgs).toEqual({ - list_of_values: ['sample value'] - }); + it('should get complete image path corresponding to a given relative path', () => { + expect(component.getStaticImageUrl('/path/to/image.png')).toBe( + '/assets/images/path/to/image.png' + ); }); - it('should get complete image path corresponding to a given relative path', - () => { - expect(component.getStaticImageUrl('/path/to/image.png')).toBe( - '/assets/images/path/to/image.png'); - }); - - it('should save param changes when externalSave is broadcasted', - fakeAsync(() => { - component.paramChangesService.displayed = []; - spyOn(component, 'generateParamNameChoices').and.stub(); - spyOn(component.paramChangesService, 'saveDisplayedValue').and.stub(); - spyOn(explorationParamSpecsService, 'saveDisplayedValue').and.stub(); - spyOn(editabilityService, 'isEditable').and.returnValue(true); - let saveParamChangesSpy = spyOn( - explorationStatesService, 'saveStateParamChanges') - .and.callFake(() => {}); - component.addParamChange(); - component.openParamChangesEditor(); + it('should save param changes when externalSave is broadcasted', fakeAsync(() => { + component.paramChangesService.displayed = []; + spyOn(component, 'generateParamNameChoices').and.stub(); + spyOn(component.paramChangesService, 'saveDisplayedValue').and.stub(); + spyOn(explorationParamSpecsService, 'saveDisplayedValue').and.stub(); + spyOn(editabilityService, 'isEditable').and.returnValue(true); + let saveParamChangesSpy = spyOn( + explorationStatesService, + 'saveStateParamChanges' + ).and.callFake(() => {}); + component.addParamChange(); + component.openParamChangesEditor(); - mockExternalSaveEventEmitter.emit(); - tick(); + mockExternalSaveEventEmitter.emit(); + tick(); - expect(saveParamChangesSpy).toHaveBeenCalled(); - expect(postSaveHookSpy).toHaveBeenCalled(); - })); + expect(saveParamChangesSpy).toHaveBeenCalled(); + expect(postSaveHookSpy).toHaveBeenCalled(); + })); - it('should add a new param change when there are no param changes displayed', - () => { - expect(( - component.paramChangesService.displayed as ParamChange[] - ).length).toBe(0); - component.addParamChange(); + it('should add a new param change when there are no param changes displayed', () => { + expect( + (component.paramChangesService.displayed as ParamChange[]).length + ).toBe(0); + component.addParamChange(); - expect(component.paramNameChoices).toEqual([{ + expect(component.paramNameChoices).toEqual([ + { id: 'a', - text: 'a' - }, { + text: 'a', + }, + { id: 'x', - text: 'x' - }, { + text: 'x', + }, + { id: 'y', - text: 'y' - }]); - expect(( - component.paramChangesService.displayed as ParamChange[] - ).length).toBe(1); - }); - - it('should not open param changes editor when it is not editable', - () => { - spyOn(editabilityService, 'isEditable').and.returnValue(false); + text: 'y', + }, + ]); + expect( + (component.paramChangesService.displayed as ParamChange[]).length + ).toBe(1); + }); + + it('should not open param changes editor when it is not editable', () => { + spyOn(editabilityService, 'isEditable').and.returnValue(false); - expect(( - component.paramChangesService.displayed as ParamChange[] - ).length).toBe(0); - component.openParamChangesEditor(); + expect( + (component.paramChangesService.displayed as ParamChange[]).length + ).toBe(0); + component.openParamChangesEditor(); - expect(component.isParamChangesEditorOpen).toBe(false); - expect(( - component.paramChangesService.displayed as ParamChange[] - ).length).toBe(0); - }); + expect(component.isParamChangesEditorOpen).toBe(false); + expect( + (component.paramChangesService.displayed as ParamChange[]).length + ).toBe(0); + }); it('should open param changes editor and cancel edit', fakeAsync(() => { component.paramChangesService.displayed = []; @@ -222,15 +235,17 @@ describe('Param Changes Editor Component', () => { tick(); expect(component.isParamChangesEditorOpen).toBe(true); - expect(( - component.paramChangesService.displayed as ParamChange[]).length).toBe(1); + expect( + (component.paramChangesService.displayed as ParamChange[]).length + ).toBe(1); component.cancelEdit(); tick(); expect(component.isParamChangesEditorOpen).toBe(false); - expect(( - component.paramChangesService.displayed as ParamChange[]).length).toBe(1); + expect( + (component.paramChangesService.displayed as ParamChange[]).length + ).toBe(1); })); it('should open param changes editor and add a param change', () => { @@ -239,15 +254,19 @@ describe('Param Changes Editor Component', () => { component.openParamChangesEditor(); expect(component.isParamChangesEditorOpen).toBe(true); - expect(component.paramNameChoices).toEqual([{ - id: 'a', - text: 'a' - }, { - id: 'y', - text: 'y' - }]); - expect(( - component.paramChangesService.displayed as ParamChange[]).length).toBe(1); + expect(component.paramNameChoices).toEqual([ + { + id: 'a', + text: 'a', + }, + { + id: 'y', + text: 'y', + }, + ]); + expect( + (component.paramChangesService.displayed as ParamChange[]).length + ).toBe(1); }); it('should check whenever param changes are valid', () => { @@ -257,72 +276,98 @@ describe('Param Changes Editor Component', () => { expect(component.warningText).toBe(''); }); - it('should check param changes as invalid when it has an empty parameter' + - ' name', () => { - component.paramChangesService.displayed = [ - paramChangeObjectFactory.createDefault('')]; - - expect(component.areDisplayedParamChangesValid()).toBe(false); - expect(component.warningText).toBe( - 'Please pick a non-empty parameter name.'); - }); - - it('should check param changes as invalid when it has a reserved parameter' + - ' name', () => { - component.paramChangesService.displayed = [ - paramChangeObjectFactory.createDefault('answer')]; - - expect(component.areDisplayedParamChangesValid()).toBe(false); - expect(component.warningText).toBe( - 'The parameter name \'answer\' is reserved.'); - }); - - it('should check param changes as invalid when it has non alphabetic' + - ' characters in parameter name', () => { - component.paramChangesService.displayed = [ - paramChangeObjectFactory.createDefault('123')]; - - expect(component.areDisplayedParamChangesValid()).toBe(false); - expect(component.warningText).toBe( - 'Parameter names should use only alphabetic characters.'); - }); - - it('should check param changes as invalid when it has no default' + - ' generator id', () => { - component.paramChangesService.displayed = [ - paramChangeObjectFactory.createFromBackendDict({ - customization_args: {}, - generator_id: '', - name: 'a' - })]; - - component.areDisplayedParamChangesValid(); - expect(component.areDisplayedParamChangesValid()).toBe(false); - expect(component.warningText).toBe( - 'Each parameter should have a generator id.'); - }); - - it('should check param changes as invalid when it has no values and its' + - ' generator id is RandomSelector', () => { - component.paramChangesService.displayed = [ - paramChangeObjectFactory.createFromBackendDict({ - customization_args: { - list_of_values: [] - }, - generator_id: 'RandomSelector', - name: 'a' - })]; - - component.areDisplayedParamChangesValid(); - expect(component.areDisplayedParamChangesValid()).toBe(false); - expect(component.warningText).toBe( - 'Each parameter should have at least one possible value.'); - }); + it( + 'should check param changes as invalid when it has an empty parameter' + + ' name', + () => { + component.paramChangesService.displayed = [ + paramChangeObjectFactory.createDefault(''), + ]; + + expect(component.areDisplayedParamChangesValid()).toBe(false); + expect(component.warningText).toBe( + 'Please pick a non-empty parameter name.' + ); + } + ); + + it( + 'should check param changes as invalid when it has a reserved parameter' + + ' name', + () => { + component.paramChangesService.displayed = [ + paramChangeObjectFactory.createDefault('answer'), + ]; + + expect(component.areDisplayedParamChangesValid()).toBe(false); + expect(component.warningText).toBe( + "The parameter name 'answer' is reserved." + ); + } + ); + + it( + 'should check param changes as invalid when it has non alphabetic' + + ' characters in parameter name', + () => { + component.paramChangesService.displayed = [ + paramChangeObjectFactory.createDefault('123'), + ]; + + expect(component.areDisplayedParamChangesValid()).toBe(false); + expect(component.warningText).toBe( + 'Parameter names should use only alphabetic characters.' + ); + } + ); + + it( + 'should check param changes as invalid when it has no default' + + ' generator id', + () => { + component.paramChangesService.displayed = [ + paramChangeObjectFactory.createFromBackendDict({ + customization_args: {}, + generator_id: '', + name: 'a', + }), + ]; + + component.areDisplayedParamChangesValid(); + expect(component.areDisplayedParamChangesValid()).toBe(false); + expect(component.warningText).toBe( + 'Each parameter should have a generator id.' + ); + } + ); + + it( + 'should check param changes as invalid when it has no values and its' + + ' generator id is RandomSelector', + () => { + component.paramChangesService.displayed = [ + paramChangeObjectFactory.createFromBackendDict({ + customization_args: { + list_of_values: [], + }, + generator_id: 'RandomSelector', + name: 'a', + }), + ]; + + component.areDisplayedParamChangesValid(); + expect(component.areDisplayedParamChangesValid()).toBe(false); + expect(component.warningText).toBe( + 'Each parameter should have at least one possible value.' + ); + } + ); it('should not save param changes when it is invalid', fakeAsync(() => { spyOn(alertsService, 'addWarning'); component.paramChangesService.displayed = [ - paramChangeObjectFactory.createDefault('123')]; + paramChangeObjectFactory.createDefault('123'), + ]; component.postSaveHook = () => { let value = 'value'; @@ -334,7 +379,8 @@ describe('Param Changes Editor Component', () => { tick(); expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Invalid parameter changes.'); + 'Invalid parameter changes.' + ); })); it('should save param changes when it is valid', fakeAsync(() => { @@ -343,7 +389,9 @@ describe('Param Changes Editor Component', () => { spyOn(explorationParamSpecsService, 'saveDisplayedValue').and.stub(); let saveParamChangesSpy = spyOn( - explorationStatesService, 'saveStateParamChanges').and.callFake(() => {}); + explorationStatesService, + 'saveStateParamChanges' + ).and.callFake(() => {}); component.currentlyInSettingsTab = false; component.addParamChange(); @@ -356,61 +404,71 @@ describe('Param Changes Editor Component', () => { it('should not delete a param change when index is less than 0', () => { component.addParamChange(); - expect(( - component.paramChangesService.displayed as ParamChange[]).length).toBe(1); + expect( + (component.paramChangesService.displayed as ParamChange[]).length + ).toBe(1); spyOn(alertsService, 'addWarning'); component.deleteParamChange(-1); expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Cannot delete parameter change at position -1: index out of range'); + 'Cannot delete parameter change at position -1: index out of range' + ); }); - it('should not delete a param change when index is greather than param' + - ' changes length', () => { - component.addParamChange(); - expect(( - component.paramChangesService.displayed as ParamChange[]).length).toBe(1); - - spyOn(alertsService, 'addWarning'); - component.deleteParamChange(5); - expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Cannot delete parameter change at position 5: index out of range'); - }); + it( + 'should not delete a param change when index is greather than param' + + ' changes length', + () => { + component.addParamChange(); + expect( + (component.paramChangesService.displayed as ParamChange[]).length + ).toBe(1); + + spyOn(alertsService, 'addWarning'); + component.deleteParamChange(5); + expect(alertsService.addWarning).toHaveBeenCalledWith( + 'Cannot delete parameter change at position 5: index out of range' + ); + } + ); it('should delete a param change', () => { component.addParamChange(); - expect(( - component.paramChangesService.displayed as ParamChange[] - ).length).toBe(1); + expect( + (component.paramChangesService.displayed as ParamChange[]).length + ).toBe(1); component.deleteParamChange(0); - expect(( - component.paramChangesService.displayed as ParamChange[] - ).length).toBe(0); + expect( + (component.paramChangesService.displayed as ParamChange[]).length + ).toBe(0); }); - it('should change customization args values to be human readable', - () => { - expect(component.HUMAN_READABLE_ARGS_RENDERERS.Copier({ - value: 'Copier value' - })).toBe('to Copier value'); - - expect(component.HUMAN_READABLE_ARGS_RENDERERS.RandomSelector({ - list_of_values: ['first value', 'second value'] - })).toBe('to one of [first value, second value] at random'); - }); + it('should change customization args values to be human readable', () => { + expect( + component.HUMAN_READABLE_ARGS_RENDERERS.Copier({ + value: 'Copier value', + }) + ).toBe('to Copier value'); + + expect( + component.HUMAN_READABLE_ARGS_RENDERERS.RandomSelector({ + list_of_values: ['first value', 'second value'], + }) + ).toBe('to one of [first value, second value] at random'); + }); it('should change list order properly', () => { jasmine.createSpy('moveItemInArray').and.stub(); component.paramChangesService.displayed = [ paramChangeObjectFactory.createDefault(''), - paramChangeObjectFactory.createDefault('') + paramChangeObjectFactory.createDefault(''), ]; component.drop({ previousIndex: 1, - currentIndex: 2 + currentIndex: 2, } as CdkDragSortEvent); }); }); diff --git a/core/templates/pages/exploration-editor-page/param-changes-editor/param-changes-editor.component.ts b/core/templates/pages/exploration-editor-page/param-changes-editor/param-changes-editor.component.ts index 408d141a7c0a..5eb4ed1d720b 100644 --- a/core/templates/pages/exploration-editor-page/param-changes-editor/param-changes-editor.component.ts +++ b/core/templates/pages/exploration-editor-page/param-changes-editor/param-changes-editor.component.ts @@ -17,26 +17,29 @@ * both the exploration settings tab and the state editor page). */ -import { Component, Injector, Input, OnDestroy, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { Subscription } from 'rxjs'; -import { ExplorationParamSpecsService } from '../services/exploration-param-specs.service'; -import { ParamChange, ParamChangeObjectFactory } from 'domain/exploration/ParamChangeObjectFactory'; -import { EditabilityService } from 'services/editability.service'; -import { AlertsService } from 'services/alerts.service'; -import { ExplorationStatesService } from '../services/exploration-states.service'; -import { ExternalSaveService } from 'services/external-save.service'; -import { AppConstants } from 'app.constants'; -import { StateParamChangesService } from 'components/state-editor/state-editor-properties-services/state-param-changes.service'; -import { ExplorationParamChangesService } from '../services/exploration-param-changes.service'; +import {Component, Injector, Input, OnDestroy, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {Subscription} from 'rxjs'; +import {ExplorationParamSpecsService} from '../services/exploration-param-specs.service'; +import { + ParamChange, + ParamChangeObjectFactory, +} from 'domain/exploration/ParamChangeObjectFactory'; +import {EditabilityService} from 'services/editability.service'; +import {AlertsService} from 'services/alerts.service'; +import {ExplorationStatesService} from '../services/exploration-states.service'; +import {ExternalSaveService} from 'services/external-save.service'; +import {AppConstants} from 'app.constants'; +import {StateParamChangesService} from 'components/state-editor/state-editor-properties-services/state-param-changes.service'; +import {ExplorationParamChangesService} from '../services/exploration-param-changes.service'; import cloneDeep from 'lodash/cloneDeep'; -import { ParamSpecs } from 'domain/exploration/ParamSpecsObjectFactory'; -import { CdkDragSortEvent, moveItemInArray} from '@angular/cdk/drag-drop'; +import {ParamSpecs} from 'domain/exploration/ParamSpecsObjectFactory'; +import {CdkDragSortEvent, moveItemInArray} from '@angular/cdk/drag-drop'; @Component({ selector: 'param-changes-editor', - templateUrl: './param-changes-editor.component.html' + templateUrl: './param-changes-editor.component.html', }) export class ParamChangesEditorComponent implements OnInit, OnDestroy { @Input() paramChangesServiceName: string; @@ -50,7 +53,7 @@ export class ParamChangesEditorComponent implements OnInit, OnDestroy { directiveSubscriptions = new Subscription(); isParamChangesEditorOpen: boolean; - paramNameChoices: { id: string; text: string }[]; + paramNameChoices: {id: string; text: string}[]; warningText: string; HUMAN_READABLE_ARGS_RENDERERS: { Copier: (value) => void; @@ -59,27 +62,30 @@ export class ParamChangesEditorComponent implements OnInit, OnDestroy { PREAMBLE_TEXT = { Copier: 'to', - RandomSelector: 'to one of' + RandomSelector: 'to one of', }; - paramChangesService: ( - ExplorationParamChangesService | StateParamChangesService); + paramChangesService: + | ExplorationParamChangesService + | StateParamChangesService; constructor( - private alertsService: AlertsService, - private externalSaveService: ExternalSaveService, - private explorationStatesService: ExplorationStatesService, - private explorationParamSpecsService: ExplorationParamSpecsService, - private paramChangeObjectFactory: ParamChangeObjectFactory, - public editabilityService: EditabilityService, - private urlInterpolationService: UrlInterpolationService, - private injector: Injector, + private alertsService: AlertsService, + private externalSaveService: ExternalSaveService, + private explorationStatesService: ExplorationStatesService, + private explorationParamSpecsService: ExplorationParamSpecsService, + private paramChangeObjectFactory: ParamChangeObjectFactory, + public editabilityService: EditabilityService, + private urlInterpolationService: UrlInterpolationService, + private injector: Injector ) {} drop(event: CdkDragSortEvent): void { moveItemInArray( - this.paramChangesService.displayed as ParamChange[], event.previousIndex, - event.currentIndex); + this.paramChangesService.displayed as ParamChange[], + event.previousIndex, + event.currentIndex + ); } openParamChangesEditor(): void { @@ -96,29 +102,31 @@ export class ParamChangesEditorComponent implements OnInit, OnDestroy { } addParamChange(): void { - let newParamName = ( - this.paramNameChoices.length > 0 ? - this.paramNameChoices[0].id : 'x'); - let newParamChange = this.paramChangeObjectFactory.createDefault( - newParamName); + let newParamName = + this.paramNameChoices.length > 0 ? this.paramNameChoices[0].id : 'x'; + let newParamChange = + this.paramChangeObjectFactory.createDefault(newParamName); // Add the new param name to this.paramNameChoices, if necessary, // so that it shows up in the dropdown. - if (( - this.explorationParamSpecsService.displayed as ParamSpecs).addParamIfNew( - newParamChange.name, null)) { + if ( + (this.explorationParamSpecsService.displayed as ParamSpecs).addParamIfNew( + newParamChange.name, + null + ) + ) { this.paramNameChoices = this.generateParamNameChoices(); } (this.paramChangesService.displayed as ParamChange[]).push(newParamChange); } generateParamNameChoices(): {id: string; text: string}[] { - return ( - this.explorationParamSpecsService.displayed as ParamSpecs - ).getParamNames().sort() - .map((paramName) => { + return (this.explorationParamSpecsService.displayed as ParamSpecs) + .getParamNames() + .sort() + .map(paramName => { return { id: paramName, - text: paramName + text: paramName, }; }); } @@ -139,15 +147,15 @@ export class ParamChangesEditorComponent implements OnInit, OnDestroy { } if (AppConstants.INVALID_PARAMETER_NAMES.indexOf(paramName) !== -1) { - this.warningText = ( - 'The parameter name \'' + paramName + '\' is reserved.'); + this.warningText = + "The parameter name '" + paramName + "' is reserved."; return false; } let ALPHA_CHARS_REGEX = /^[A-Za-z]+$/; if (!ALPHA_CHARS_REGEX.test(paramName)) { - this.warningText = ( - 'Parameter names should use only alphabetic characters.'); + this.warningText = + 'Parameter names should use only alphabetic characters.'; return false; } @@ -155,15 +163,16 @@ export class ParamChangesEditorComponent implements OnInit, OnDestroy { let customizationArgs = paramChanges[i].customizationArgs; if (!this.PREAMBLE_TEXT.hasOwnProperty(generatorId)) { - this.warningText = - 'Each parameter should have a generator id.'; + this.warningText = 'Each parameter should have a generator id.'; return false; } - if (generatorId === 'RandomSelector' && - customizationArgs.list_of_values.length === 0) { - this.warningText = ( - 'Each parameter should have at least one possible value.'); + if ( + generatorId === 'RandomSelector' && + customizationArgs.list_of_values.length === 0 + ) { + this.warningText = + 'Each parameter should have at least one possible value.'; return false; } } @@ -184,11 +193,13 @@ export class ParamChangesEditorComponent implements OnInit, OnDestroy { // Update paramSpecs manually with newly-added param names. this.explorationParamSpecsService.restoreFromMemento(); - (this.paramChangesService.displayed as ParamChange[]).forEach(( - paramChange) => { - (this.explorationParamSpecsService.displayed as ParamSpecs).addParamIfNew( - paramChange.name, null); - }); + (this.paramChangesService.displayed as ParamChange[]).forEach( + paramChange => { + ( + this.explorationParamSpecsService.displayed as ParamSpecs + ).addParamIfNew(paramChange.name, null); + } + ); this.explorationParamSpecsService.saveDisplayedValue(); @@ -196,7 +207,8 @@ export class ParamChangesEditorComponent implements OnInit, OnDestroy { if (!this.currentlyInSettingsTab) { this.explorationStatesService.saveStateParamChanges( (this.paramChangesService as StateParamChangesService).stateName, - cloneDeep(this.paramChangesService.displayed as ParamChange[])); + cloneDeep(this.paramChangesService.displayed as ParamChange[]) + ); } if (this.postSaveHook) { this.postSaveHook(); @@ -204,11 +216,15 @@ export class ParamChangesEditorComponent implements OnInit, OnDestroy { } deleteParamChange(index: number): void { - if (index < 0 || - index >= (this.paramChangesService.displayed as []).length) { + if ( + index < 0 || + index >= (this.paramChangesService.displayed as []).length + ) { this.alertsService.addWarning( - 'Cannot delete parameter change at position ' + index + - ': index out of range'); + 'Cannot delete parameter change at position ' + + index + + ': index out of range' + ); } // This ensures that any new parameter names that have been added @@ -216,11 +232,12 @@ export class ParamChangesEditorComponent implements OnInit, OnDestroy { // the select2 dropdowns. Otherwise, after the deletion, the // dropdowns may turn blank. (this.paramChangesService.displayed as ParamChange[]).forEach( - (paramChange) => { + paramChange => { ( this.explorationParamSpecsService.displayed as ParamSpecs ).addParamIfNew(paramChange.name, null); - }); + } + ); this.paramNameChoices = this.generateParamNameChoices(); (this.paramChangesService.displayed as []).splice(index, 1); @@ -236,18 +253,19 @@ export class ParamChangesEditorComponent implements OnInit, OnDestroy { } ngOnInit(): void { - this.paramChangesService = ( - this.injector.get(this.SERVICE_MAPPING[this.paramChangesServiceName])); + this.paramChangesService = this.injector.get( + this.SERVICE_MAPPING[this.paramChangesServiceName] + ); this.isParamChangesEditorOpen = false; this.warningText = ''; this.directiveSubscriptions.add( - this.externalSaveService.onExternalSave.subscribe( - () => { - if (this.isParamChangesEditorOpen) { - this.saveParamChanges(); - } - })); + this.externalSaveService.onExternalSave.subscribe(() => { + if (this.isParamChangesEditorOpen) { + this.saveParamChanges(); + } + }) + ); // This is a local letiable that is used by the select2 dropdowns // for choosing parameter names. It may not accurately reflect the @@ -256,13 +274,12 @@ export class ParamChangesEditorComponent implements OnInit, OnDestroy { // the course of a single "parameter changes" edit. this.paramNameChoices = []; this.HUMAN_READABLE_ARGS_RENDERERS = { - Copier: (customizationArgs) => { + Copier: customizationArgs => { return 'to ' + customizationArgs.value; }, - RandomSelector: (customizationArgs) => { + RandomSelector: customizationArgs => { let result = 'to one of ['; - for ( - let i = 0; i < customizationArgs.list_of_values.length; i++) { + for (let i = 0; i < customizationArgs.list_of_values.length; i++) { if (i !== 0) { result += ', '; } @@ -270,7 +287,7 @@ export class ParamChangesEditorComponent implements OnInit, OnDestroy { } result += '] at random'; return result; - } + }, }; } @@ -279,7 +296,9 @@ export class ParamChangesEditorComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive('paramChangesEditor', - downgradeComponent({ - component: ParamChangesEditorComponent - }) as angular.IDirectiveFactory); +angular.module('oppia').directive( + 'paramChangesEditor', + downgradeComponent({ + component: ParamChangesEditorComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-editor-page/param-changes-editor/value-generator-editor.component.spec.ts b/core/templates/pages/exploration-editor-page/param-changes-editor/value-generator-editor.component.spec.ts index e6aa4d85e7c4..48cc581da3d9 100644 --- a/core/templates/pages/exploration-editor-page/param-changes-editor/value-generator-editor.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/param-changes-editor/value-generator-editor.component.spec.ts @@ -16,14 +16,14 @@ * @fileoverview Unit tests for valueGeneratorEditor. */ -import { NO_ERRORS_SCHEMA, SimpleChange } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; -import { CopierComponent } from 'value_generators/templates/copier.component'; -import { RandomSelectorComponent } from 'value_generators/templates/random-selector.component'; -import { ValueGeneratorEditorComponent } from './value-generator-editor.component'; +import {NO_ERRORS_SCHEMA, SimpleChange} from '@angular/core'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {BrowserDynamicTestingModule} from '@angular/platform-browser-dynamic/testing'; +import {CopierComponent} from 'value_generators/templates/copier.component'; +import {RandomSelectorComponent} from 'value_generators/templates/random-selector.component'; +import {ValueGeneratorEditorComponent} from './value-generator-editor.component'; -describe('Value Generator Editor Component', function() { +describe('Value Generator Editor Component', function () { let component: ValueGeneratorEditorComponent; let fixture: ComponentFixture; @@ -32,16 +32,16 @@ describe('Value Generator Editor Component', function() { declarations: [ ValueGeneratorEditorComponent, RandomSelectorComponent, - CopierComponent + CopierComponent, ], - schemas: [NO_ERRORS_SCHEMA] - }).overrideModule(BrowserDynamicTestingModule, { - set: { - entryComponents: [ - CopierComponent, - RandomSelectorComponent], - } - }).compileComponents(); + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideModule(BrowserDynamicTestingModule, { + set: { + entryComponents: [CopierComponent, RandomSelectorComponent], + }, + }) + .compileComponents(); })); beforeEach(() => { @@ -53,7 +53,7 @@ describe('Value Generator Editor Component', function() { component.objType = 'objType'; component.customizationArgs = { value: 'value', - list_of_values: ['list_of_values'] + list_of_values: ['list_of_values'], }; fixture.detectChanges(); @@ -65,9 +65,9 @@ describe('Value Generator Editor Component', function() { component.ngOnChanges({ generatorId: { currentValue: 'currentValue', - previousValue: 'previousValue' - } - } as { generatorId: SimpleChange}); + previousValue: 'previousValue', + }, + } as {generatorId: SimpleChange}); expect(component).toBeDefined(); }); diff --git a/core/templates/pages/exploration-editor-page/param-changes-editor/value-generator-editor.component.ts b/core/templates/pages/exploration-editor-page/param-changes-editor/value-generator-editor.component.ts index e1fff48ac5dc..c2038f719f3e 100644 --- a/core/templates/pages/exploration-editor-page/param-changes-editor/value-generator-editor.component.ts +++ b/core/templates/pages/exploration-editor-page/param-changes-editor/value-generator-editor.component.ts @@ -25,15 +25,15 @@ import { OnChanges, SimpleChange, ViewChild, - ViewContainerRef + ViewContainerRef, } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { CopierComponent } from 'value_generators/templates/copier.component'; -import { RandomSelectorComponent } from 'value_generators/templates/random-selector.component'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {CopierComponent} from 'value_generators/templates/copier.component'; +import {RandomSelectorComponent} from 'value_generators/templates/random-selector.component'; @Component({ selector: 'oppia-value-generator-editor', - templateUrl: './value-generator-editor.component.html' + templateUrl: './value-generator-editor.component.html', }) export class ValueGeneratorEditorComponent implements OnChanges, AfterViewInit { @Input() generatorId: string; @@ -45,29 +45,33 @@ export class ValueGeneratorEditorComponent implements OnChanges, AfterViewInit { }; @ViewChild('interactionContainer', { - read: ViewContainerRef}) viewContainerRef!: ViewContainerRef; + read: ViewContainerRef, + }) + viewContainerRef!: ViewContainerRef; TAG_TO_INTERACTION_MAPPING = { copier: CopierComponent, - 'random-selector': RandomSelectorComponent + 'random-selector': RandomSelectorComponent, }; constructor( - private componentFactoryResolver: ComponentFactoryResolver, - private changeDetectorRef: ChangeDetectorRef + private componentFactoryResolver: ComponentFactoryResolver, + private changeDetectorRef: ChangeDetectorRef ) {} ngAfterViewInit(): void { - let componentName = this.generatorId.replace( - /([a-z])([A-Z])/g, '$1-$2').toLowerCase(); + let componentName = this.generatorId + .replace(/([a-z])([A-Z])/g, '$1-$2') + .toLowerCase(); - const componentFactory = this.componentFactoryResolver - .resolveComponentFactory( - this.TAG_TO_INTERACTION_MAPPING[componentName]); + const componentFactory = + this.componentFactoryResolver.resolveComponentFactory< + CopierComponent | RandomSelectorComponent + >(this.TAG_TO_INTERACTION_MAPPING[componentName]); const componentRef = this.viewContainerRef.createComponent< - CopierComponent | RandomSelectorComponent>( - componentFactory); + CopierComponent | RandomSelectorComponent + >(componentFactory); componentRef.instance.customizationArgs = this.customizationArgs; componentRef.instance.generatorId = this.generatorId; @@ -78,17 +82,20 @@ export class ValueGeneratorEditorComponent implements OnChanges, AfterViewInit { this.changeDetectorRef.detectChanges(); } - ngOnChanges(changes: { generatorId: SimpleChange }): void { - if ((changes.generatorId.currentValue !== - changes.generatorId.previousValue) && - this.viewContainerRef) { + ngOnChanges(changes: {generatorId: SimpleChange}): void { + if ( + changes.generatorId.currentValue !== changes.generatorId.previousValue && + this.viewContainerRef + ) { this.viewContainerRef.clear(); this.ngAfterViewInit(); } } } -angular.module('oppia').directive('oppiaValueGeneratorEditor', - downgradeComponent({ - component: ValueGeneratorEditorComponent - }) as angular.IDirectiveFactory); +angular.module('oppia').directive( + 'oppiaValueGeneratorEditor', + downgradeComponent({ + component: ValueGeneratorEditorComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-editor-page/preview-tab/preview-tab.component.spec.ts b/core/templates/pages/exploration-editor-page/preview-tab/preview-tab.component.spec.ts index 1e74862a6964..1f7e5cdeecec 100644 --- a/core/templates/pages/exploration-editor-page/preview-tab/preview-tab.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/preview-tab/preview-tab.component.spec.ts @@ -16,30 +16,37 @@ * @fileoverview Unit tests for previewTab. */ -import { ComponentFixture, fakeAsync, flush, flushMicrotasks, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { ParamChangeObjectFactory } from 'domain/exploration/ParamChangeObjectFactory'; -import { StateObjectFactory } from 'domain/state/StateObjectFactory'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { TranslateService } from '@ngx-translate/core'; -import { MockTranslateService } from 'components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { EditableExplorationBackendApiService } from 'domain/exploration/editable-exploration-backend-api.service'; -import { ExplorationEngineService } from 'pages/exploration-player-page/services/exploration-engine.service'; -import { ExplorationPlayerStateService } from 'pages/exploration-player-page/services/exploration-player-state.service'; -import { ContextService } from 'services/context.service'; -import { ExplorationFeaturesService } from 'services/exploration-features.service'; -import { ExplorationInitStateNameService } from '../services/exploration-init-state-name.service'; -import { ExplorationParamChangesService } from '../services/exploration-param-changes.service'; -import { ExplorationStatesService } from '../services/exploration-states.service'; -import { GraphDataService } from '../services/graph-data.service'; -import { ParameterMetadataService } from '../services/parameter-metadata.service'; -import { PreviewTabComponent } from './preview-tab.component'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ExplorationDataService } from '../services/exploration-data.service'; -import { NumberAttemptsService } from 'pages/exploration-player-page/services/number-attempts.service'; -import { RouterService } from '../services/router.service'; - +import { + ComponentFixture, + fakeAsync, + flush, + flushMicrotasks, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {ParamChangeObjectFactory} from 'domain/exploration/ParamChangeObjectFactory'; +import {StateObjectFactory} from 'domain/state/StateObjectFactory'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {TranslateService} from '@ngx-translate/core'; +import {MockTranslateService} from 'components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {EditableExplorationBackendApiService} from 'domain/exploration/editable-exploration-backend-api.service'; +import {ExplorationEngineService} from 'pages/exploration-player-page/services/exploration-engine.service'; +import {ExplorationPlayerStateService} from 'pages/exploration-player-page/services/exploration-player-state.service'; +import {ContextService} from 'services/context.service'; +import {ExplorationFeaturesService} from 'services/exploration-features.service'; +import {ExplorationInitStateNameService} from '../services/exploration-init-state-name.service'; +import {ExplorationParamChangesService} from '../services/exploration-param-changes.service'; +import {ExplorationStatesService} from '../services/exploration-states.service'; +import {GraphDataService} from '../services/graph-data.service'; +import {ParameterMetadataService} from '../services/parameter-metadata.service'; +import {PreviewTabComponent} from './preview-tab.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ExplorationDataService} from '../services/exploration-data.service'; +import {NumberAttemptsService} from 'pages/exploration-player-page/services/number-attempts.service'; +import {RouterService} from '../services/router.service'; class MockNgbModalRef { componentInstance!: { @@ -50,7 +57,7 @@ class MockNgbModalRef { class MockNgbModal { open(): object { return { - result: Promise.resolve() + result: Promise.resolve(), }; } } @@ -60,8 +67,7 @@ describe('Preview Tab Component', () => { let fixture: ComponentFixture; let ngbModal: NgbModal; let contextService: ContextService; - let editableExplorationBackendApiService: - EditableExplorationBackendApiService; + let editableExplorationBackendApiService: EditableExplorationBackendApiService; let explorationEngineService: ExplorationEngineService; let explorationInitStateNameService: ExplorationInitStateNameService; let explorationFeaturesService: ExplorationFeaturesService; @@ -106,49 +112,57 @@ describe('Preview Tab Component', () => { param_specs: {}, param_changes: [], auto_tts_enabled: false, - edits_allowed: true + edits_allowed: true, }, next_content_id_index: 5, }; - let parameters = [{ - paramName: 'paramName1', - stateName: null, - }, { - paramName: 'paramName2', - stateName: null, - }]; + let parameters = [ + { + paramName: 'paramName1', + stateName: null, + }, + { + paramName: 'paramName2', + stateName: null, + }, + ]; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - PreviewTabComponent - ], + declarations: [PreviewTabComponent], providers: [ { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, { provide: ExplorationDataService, useValue: { - getDataAsync: () => Promise.resolve({ - param_changes: [ - paramChangeObjectFactory - .createEmpty(changeObjectName).toBackendDict() - ], - states: [stateObjectFactory.createDefaultState( - stateName, 'content_0', 'default_outcome_1')], - init_state_name: stateName - }) - } + getDataAsync: () => + Promise.resolve({ + param_changes: [ + paramChangeObjectFactory + .createEmpty(changeObjectName) + .toBackendDict(), + ], + states: [ + stateObjectFactory.createDefaultState( + stateName, + 'content_0', + 'default_outcome_1' + ), + ], + init_state_name: stateName, + }), + }, }, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -161,14 +175,18 @@ describe('Preview Tab Component', () => { stateObjectFactory = TestBed.inject(StateObjectFactory); explorationEngineService = TestBed.inject(ExplorationEngineService); editableExplorationBackendApiService = TestBed.inject( - EditableExplorationBackendApiService); + EditableExplorationBackendApiService + ); explorationFeaturesService = TestBed.inject(ExplorationFeaturesService); explorationInitStateNameService = TestBed.inject( - ExplorationInitStateNameService); + ExplorationInitStateNameService + ); explorationPlayerStateService = TestBed.inject( - ExplorationPlayerStateService); + ExplorationPlayerStateService + ); explorationParamChangesService = TestBed.inject( - ExplorationParamChangesService); + ExplorationParamChangesService + ); explorationStatesService = TestBed.inject(ExplorationStatesService); graphDataService = TestBed.inject(GraphDataService); parameterMetadataService = TestBed.inject(ParameterMetadataService); @@ -178,126 +196,127 @@ describe('Preview Tab Component', () => { contextService = TestBed.inject(ContextService); spyOn(contextService, 'getExplorationId').and.returnValue(explorationId); getUnsetParametersInfo = spyOn( - parameterMetadataService, 'getUnsetParametersInfo'); - getUnsetParametersInfo.and.returnValue( - parameters); + parameterMetadataService, + 'getUnsetParametersInfo' + ); + getUnsetParametersInfo.and.returnValue(parameters); spyOn( - editableExplorationBackendApiService, 'fetchApplyDraftExplorationAsync') - .and.returnValue(Promise.resolve(exploration)); + editableExplorationBackendApiService, + 'fetchApplyDraftExplorationAsync' + ).and.returnValue(Promise.resolve(exploration)); explorationParamChangesService.savedMemento = [ - paramChangeObjectFactory.createEmpty(changeObjectName).toBackendDict() + paramChangeObjectFactory.createEmpty(changeObjectName).toBackendDict(), ]; spyOnProperty( explorationEngineService, - 'onUpdateActiveStateIfInEditor').and.returnValue( - mockUpdateActiveStateIfInEditorEventEmitter); + 'onUpdateActiveStateIfInEditor' + ).and.returnValue(mockUpdateActiveStateIfInEditorEventEmitter); spyOnProperty( explorationPlayerStateService, - 'onPlayerStateChange').and.returnValue( - mockPlayerStateChangeEventEmitter); + 'onPlayerStateChange' + ).and.returnValue(mockPlayerStateChangeEventEmitter); spyOn(explorationEngineService, 'initSettingsFromEditor').and.stub(); explorationInitStateNameService.savedMemento = 'state'; explorationParamChangesService.savedMemento = null; }); - it('should initialize controller properties after its initialization', - fakeAsync(() => { - spyOn(stateEditorService, 'getActiveStateName').and.returnValue('state'); - spyOn(explorationParamChangesService, 'init').and.stub(); - spyOn(explorationStatesService, 'init').and.stub(); - spyOn(explorationInitStateNameService, 'init').and.stub(); - spyOn(graphDataService, 'recompute').and.stub(); - // This throws "Type 'null' is not assignable to type 'State'." - // We need to suppress this error because of the need to test validations. - // @ts-ignore - spyOn(explorationStatesService, 'getState').and.returnValue(null); - spyOn(component, 'getManualParamChanges').and.returnValue( - Promise.resolve([])); - spyOn(component, 'loadPreviewState').and.stub(); - spyOn(ngbModal, 'open').and.returnValue({ - componentInstance: new MockNgbModalRef(), - result: Promise.resolve() - } as NgbModalRef); + it('should initialize controller properties after its initialization', fakeAsync(() => { + spyOn(stateEditorService, 'getActiveStateName').and.returnValue('state'); + spyOn(explorationParamChangesService, 'init').and.stub(); + spyOn(explorationStatesService, 'init').and.stub(); + spyOn(explorationInitStateNameService, 'init').and.stub(); + spyOn(graphDataService, 'recompute').and.stub(); + // This throws "Type 'null' is not assignable to type 'State'." + // We need to suppress this error because of the need to test validations. + // @ts-ignore + spyOn(explorationStatesService, 'getState').and.returnValue(null); + spyOn(component, 'getManualParamChanges').and.returnValue( + Promise.resolve([]) + ); + spyOn(component, 'loadPreviewState').and.stub(); + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: new MockNgbModalRef(), + result: Promise.resolve(), + } as NgbModalRef); - component.ngOnInit(); - tick(); - flush(); + component.ngOnInit(); + tick(); + flush(); - // Get data from exploration data service. - expect(component.isExplorationPopulated).toBe(false); - expect(component.previewWarning).toBe(''); + // Get data from exploration data service. + expect(component.isExplorationPopulated).toBe(false); + expect(component.previewWarning).toBe(''); - component.ngOnDestroy(); - })); + component.ngOnDestroy(); + })); - it('should initialize controller properties after its initialization', - fakeAsync(() => { - explorationInitStateNameService.savedMemento = 'state2'; - explorationParamChangesService.savedMemento = null; - spyOn(stateEditorService, 'getActiveStateName').and.returnValue('state'); - spyOn(explorationParamChangesService, 'init').and.stub(); - spyOn(explorationStatesService, 'init').and.stub(); - spyOn(explorationInitStateNameService, 'init').and.stub(); - spyOn(graphDataService, 'recompute').and.stub(); - // This throws "Type 'null' is not assignable to type 'State'." - // We need to suppress this error because of the need to test validations. - // @ts-ignore - spyOn(explorationStatesService, 'getState').and.returnValue(null); - spyOn(component, 'getManualParamChanges').and.returnValue( - Promise.resolve([])); - spyOn(component, 'loadPreviewState').and.stub(); - spyOn(ngbModal, 'open').and.returnValue({ - componentInstance: new MockNgbModalRef(), - result: Promise.resolve() - } as NgbModalRef); + it('should initialize controller properties after its initialization', fakeAsync(() => { + explorationInitStateNameService.savedMemento = 'state2'; + explorationParamChangesService.savedMemento = null; + spyOn(stateEditorService, 'getActiveStateName').and.returnValue('state'); + spyOn(explorationParamChangesService, 'init').and.stub(); + spyOn(explorationStatesService, 'init').and.stub(); + spyOn(explorationInitStateNameService, 'init').and.stub(); + spyOn(graphDataService, 'recompute').and.stub(); + // This throws "Type 'null' is not assignable to type 'State'." + // We need to suppress this error because of the need to test validations. + // @ts-ignore + spyOn(explorationStatesService, 'getState').and.returnValue(null); + spyOn(component, 'getManualParamChanges').and.returnValue( + Promise.resolve([]) + ); + spyOn(component, 'loadPreviewState').and.stub(); + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: new MockNgbModalRef(), + result: Promise.resolve(), + } as NgbModalRef); - component.ngOnInit(); - tick(); - mockUpdateActiveStateIfInEditorEventEmitter.emit('stateName'); - mockPlayerStateChangeEventEmitter.emit(); - tick(); - flush(); + component.ngOnInit(); + tick(); + mockUpdateActiveStateIfInEditorEventEmitter.emit('stateName'); + mockPlayerStateChangeEventEmitter.emit(); + tick(); + flush(); - // Get data from exploration data service. - expect(component.isExplorationPopulated).toBe(false); - expect(component.previewWarning).toBe( - 'Preview started from "state"'); - })); + // Get data from exploration data service. + expect(component.isExplorationPopulated).toBe(false); + expect(component.previewWarning).toBe('Preview started from "state"'); + })); - it('should initialize open ngbModal and navigate to mainTab', - fakeAsync(() => { - spyOn(explorationFeaturesService, 'areParametersEnabled') - .and.returnValue(false); - spyOn(routerService, 'navigateToMainTab'); - component.allParams = {}; + it('should initialize open ngbModal and navigate to mainTab', fakeAsync(() => { + spyOn(explorationFeaturesService, 'areParametersEnabled').and.returnValue( + false + ); + spyOn(routerService, 'navigateToMainTab'); + component.allParams = {}; - expect(component.showParameterSummary()).toBe(false); + expect(component.showParameterSummary()).toBe(false); - spyOn(ngbModal, 'open').and.returnValue({ - componentInstance: { - manualParamChanges: null, - }, - result: Promise.reject() - } as NgbModalRef); + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: { + manualParamChanges: null, + }, + result: Promise.reject(), + } as NgbModalRef); - component.loadPreviewState('', ''); - component.showSetParamsModal([], () => {}); - tick(); - tick(); - flush(); - flushMicrotasks(); + component.loadPreviewState('', ''); + component.showSetParamsModal([], () => {}); + tick(); + tick(); + flush(); + flushMicrotasks(); - expect(ngbModal.open).toHaveBeenCalled(); - expect(routerService.navigateToMainTab).toHaveBeenCalled(); - })); + expect(ngbModal.open).toHaveBeenCalled(); + expect(routerService.navigateToMainTab).toHaveBeenCalled(); + })); it('should getManualParamChanges', fakeAsync(() => { spyOn(ngbModal, 'open').and.returnValue({ componentInstance: { manualParamChanges: null, }, - result: Promise.resolve() + result: Promise.resolve(), } as NgbModalRef); component.getManualParamChanges('state'); tick(); @@ -311,11 +330,11 @@ describe('Preview Tab Component', () => { componentInstance: { manualParamChanges: null, }, - result: Promise.resolve() + result: Promise.resolve(), } as NgbModalRef); getUnsetParametersInfo.and.returnValue([]); - component.getManualParamChanges('state').then((value) => { + component.getManualParamChanges('state').then(value => { expect(value).toEqual([]); }); @@ -333,7 +352,8 @@ describe('Preview Tab Component', () => { // validations. // @ts-ignore callback(null, null); - }); + } + ); // Get data from exploration data service and resolve promise in open // modal. diff --git a/core/templates/pages/exploration-editor-page/preview-tab/preview-tab.component.ts b/core/templates/pages/exploration-editor-page/preview-tab/preview-tab.component.ts index 8e24ef540e73..71c1e22ad5ae 100644 --- a/core/templates/pages/exploration-editor-page/preview-tab/preview-tab.component.ts +++ b/core/templates/pages/exploration-editor-page/preview-tab/preview-tab.component.ts @@ -17,37 +17,41 @@ * editor page. */ - -import { Component, OnInit, OnDestroy } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import {Component, OnInit, OnDestroy} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; import isEqual from 'lodash/isEqual'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { EditableExplorationBackendApiService } from 'domain/exploration/editable-exploration-backend-api.service'; -import { ParamChange, ParamChangeObjectFactory } from 'domain/exploration/ParamChangeObjectFactory'; -import { ParamChangesObjectFactory } from 'domain/exploration/ParamChangesObjectFactory'; -import { ExplorationEngineService } from 'pages/exploration-player-page/services/exploration-engine.service'; -import { ExplorationPlayerStateService } from 'pages/exploration-player-page/services/exploration-player-state.service'; -import { ExplorationParams, LearnerParamsService } from 'pages/exploration-player-page/services/learner-params.service'; -import { NumberAttemptsService } from 'pages/exploration-player-page/services/number-attempts.service'; -import { Subscription } from 'rxjs'; -import { ContextService } from 'services/context.service'; -import { ExplorationFeaturesService } from 'services/exploration-features.service'; -import { ExplorationDataService } from '../services/exploration-data.service'; -import { ExplorationInitStateNameService } from '../services/exploration-init-state-name.service'; -import { ExplorationParamChangesService } from '../services/exploration-param-changes.service'; -import { ExplorationStatesService } from '../services/exploration-states.service'; -import { GraphDataService } from '../services/graph-data.service'; -import { ParameterMetadataService } from '../services/parameter-metadata.service'; -import { RouterService } from '../services/router.service'; -import { PreviewSetParametersModalComponent } from './templates/preview-set-parameters-modal.component'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {EditableExplorationBackendApiService} from 'domain/exploration/editable-exploration-backend-api.service'; +import { + ParamChange, + ParamChangeObjectFactory, +} from 'domain/exploration/ParamChangeObjectFactory'; +import {ParamChangesObjectFactory} from 'domain/exploration/ParamChangesObjectFactory'; +import {ExplorationEngineService} from 'pages/exploration-player-page/services/exploration-engine.service'; +import {ExplorationPlayerStateService} from 'pages/exploration-player-page/services/exploration-player-state.service'; +import { + ExplorationParams, + LearnerParamsService, +} from 'pages/exploration-player-page/services/learner-params.service'; +import {NumberAttemptsService} from 'pages/exploration-player-page/services/number-attempts.service'; +import {Subscription} from 'rxjs'; +import {ContextService} from 'services/context.service'; +import {ExplorationFeaturesService} from 'services/exploration-features.service'; +import {ExplorationDataService} from '../services/exploration-data.service'; +import {ExplorationInitStateNameService} from '../services/exploration-init-state-name.service'; +import {ExplorationParamChangesService} from '../services/exploration-param-changes.service'; +import {ExplorationStatesService} from '../services/exploration-states.service'; +import {GraphDataService} from '../services/graph-data.service'; +import {ParameterMetadataService} from '../services/parameter-metadata.service'; +import {RouterService} from '../services/router.service'; +import {PreviewSetParametersModalComponent} from './templates/preview-set-parameters-modal.component'; @Component({ selector: 'oppia-preview-tab', - templateUrl: './preview-tab.component.html' + templateUrl: './preview-tab.component.html', }) -export class PreviewTabComponent - implements OnInit, OnDestroy { +export class PreviewTabComponent implements OnInit, OnDestroy { directiveSubscriptions = new Subscription(); // These properties below are initialized using Angular lifecycle hooks @@ -59,8 +63,7 @@ export class PreviewTabComponent constructor( private contextService: ContextService, - private editableExplorationBackendApiService: - EditableExplorationBackendApiService, + private editableExplorationBackendApiService: EditableExplorationBackendApiService, private explorationDataService: ExplorationDataService, private explorationEngineService: ExplorationEngineService, private explorationFeaturesService: ExplorationFeaturesService, @@ -77,18 +80,22 @@ export class PreviewTabComponent private routerService: RouterService, private stateEditorService: StateEditorService, private paramChangesObjectFactory: ParamChangesObjectFactory - ) { } + ) {} getManualParamChanges( - initStateNameForPreview: string): Promise { - let unsetParametersInfo = this.parameterMetadataService - .getUnsetParametersInfo([initStateNameForPreview]); + initStateNameForPreview: string + ): Promise { + let unsetParametersInfo = + this.parameterMetadataService.getUnsetParametersInfo([ + initStateNameForPreview, + ]); // Construct array to hold required parameter changes. let manualParamChanges: ParamChange[] = []; for (let i = 0; i < unsetParametersInfo.length; i++) { let newParamChange = this.paramChangeObjectFactory.createEmpty( - unsetParametersInfo[i].paramName); + unsetParametersInfo[i].paramName + ); manualParamChanges.push(newParamChange); } @@ -105,52 +112,66 @@ export class PreviewTabComponent showParameterSummary(): boolean { return ( this.explorationFeaturesService.areParametersEnabled() && - !isEqual({}, this.allParams)); + !isEqual({}, this.allParams) + ); } showSetParamsModal( - manualParamChanges: ParamChange[], - callback: Function + manualParamChanges: ParamChange[], + callback: Function ): void { const modalRef = this.ngbModal.open(PreviewSetParametersModalComponent, { backdrop: 'static', windowClass: 'oppia-preview-set-params-modal', }); modalRef.componentInstance.manualParamChanges = manualParamChanges; - modalRef.result.then(() => { - if (callback) { - callback(); + modalRef.result.then( + () => { + if (callback) { + callback(); + } + }, + () => { + this.routerService.navigateToMainTab(null); } - }, () => { - this.routerService.navigateToMainTab(null); - }); + ); } loadPreviewState( - stateName: string[] | string, - manualParamChanges: ParamChange[] | string[] | string): void { + stateName: string[] | string, + manualParamChanges: ParamChange[] | string[] | string + ): void { this.explorationEngineService.initSettingsFromEditor( - stateName as string, manualParamChanges as ParamChange[]); + stateName as string, + manualParamChanges as ParamChange[] + ); this.isExplorationPopulated = true; } resetPreview(): void { this.previewWarning = ''; this.isExplorationPopulated = false; - const initStateNameForPreview = ( - this.explorationInitStateNameService.savedMemento); + const initStateNameForPreview = + this.explorationInitStateNameService.savedMemento; setTimeout(() => { const explorationId = this.contextService.getExplorationId(); - this.editableExplorationBackendApiService.fetchApplyDraftExplorationAsync( - explorationId).then((returnDict) => { - this.explorationEngineService.init( - returnDict, 0, null, false, [], [], - () => { - this.loadPreviewState(initStateNameForPreview, []); - }); - this.numberAttemptsService.reset(); - }); + this.editableExplorationBackendApiService + .fetchApplyDraftExplorationAsync(explorationId) + .then(returnDict => { + this.explorationEngineService.init( + returnDict, + 0, + null, + false, + [], + [], + () => { + this.loadPreviewState(initStateNameForPreview, []); + } + ); + this.numberAttemptsService.reset(); + }); }, 200); } @@ -160,9 +181,10 @@ export class PreviewTabComponent // change when toggling between editor and preview. this.directiveSubscriptions.add( this.explorationEngineService.onUpdateActiveStateIfInEditor.subscribe( - (stateName) => { + stateName => { this.stateEditorService.setActiveStateName(stateName); - }) + } + ) ); this.directiveSubscriptions.add( @@ -174,37 +196,44 @@ export class PreviewTabComponent this.isExplorationPopulated = false; this.explorationDataService - .getDataAsync( - () => {} - ).then(async(explorationData) => { - // TODO(#13564): Remove this part of code and make sure that this - // function is executed only after the Promise in initExplorationPage - // is fully finished. + .getDataAsync(() => {}) + .then(async explorationData => { + // TODO(#13564): Remove this part of code and make sure that this + // function is executed only after the Promise in initExplorationPage + // is fully finished. if (!this.explorationParamChangesService.savedMemento) { this.explorationParamChangesService.init( this.paramChangesObjectFactory.createFromBackendList( - explorationData.param_changes)); + explorationData.param_changes + ) + ); this.explorationStatesService.init(explorationData.states, false); this.explorationInitStateNameService.init( - explorationData.init_state_name); + explorationData.init_state_name + ); this.graphDataService.recompute(); let stateName = this.stateEditorService.getActiveStateName(); if ( - !stateName || !this.explorationStatesService.getState(stateName) + !stateName || + !this.explorationStatesService.getState(stateName) ) { this.stateEditorService.setActiveStateName( - this.explorationInitStateNameService.displayed as string); + this.explorationInitStateNameService.displayed as string + ); } } - let initStateNameForPreview = ( - this.stateEditorService.getActiveStateName()); + let initStateNameForPreview = + this.stateEditorService.getActiveStateName(); // Show a warning message if preview doesn't start from the first // state. - if (initStateNameForPreview && initStateNameForPreview !== - this.explorationInitStateNameService.savedMemento) { + if ( + initStateNameForPreview && + initStateNameForPreview !== + this.explorationInitStateNameService.savedMemento + ) { this.previewWarning = - 'Preview started from \"' + initStateNameForPreview + '\"'; + 'Preview started from "' + initStateNameForPreview + '"'; } else { this.previewWarning = ''; } @@ -213,12 +242,15 @@ export class PreviewTabComponent // Prompt user to enter any unset parameters, then populate // exploration. this.getManualParamChanges(initStateNameForPreview).then( - (manualParamChanges) => { + manualParamChanges => { if (initStateNameForPreview) { this.loadPreviewState( - initStateNameForPreview, manualParamChanges); + initStateNameForPreview, + manualParamChanges + ); } - }); + } + ); } }); } @@ -228,7 +260,9 @@ export class PreviewTabComponent } } -angular.module('oppia').directive('oppiaPreviewTab', +angular.module('oppia').directive( + 'oppiaPreviewTab', downgradeComponent({ - component: PreviewTabComponent - }) as angular.IDirectiveFactory); + component: PreviewTabComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-editor-page/preview-tab/templates/preview-set-parameters-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/preview-tab/templates/preview-set-parameters-modal.component.spec.ts index b807b228fac9..273ceced1a8c 100644 --- a/core/templates/pages/exploration-editor-page/preview-tab/templates/preview-set-parameters-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/preview-tab/templates/preview-set-parameters-modal.component.spec.ts @@ -12,16 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Unit tests for the PreviewSetParametersModalComponent. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; -import { PreviewSetParametersModalComponent } from './preview-set-parameters-modal.component'; +import {PreviewSetParametersModalComponent} from './preview-set-parameters-modal.component'; class MockActiveModal { close(): void { @@ -33,20 +32,20 @@ class MockActiveModal { } } -describe('Preview Set Parameters Modal Component', function() { +describe('Preview Set Parameters Modal Component', function () { let component: PreviewSetParametersModalComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - PreviewSetParametersModalComponent, + declarations: [PreviewSetParametersModalComponent], + providers: [ + { + provide: NgbActiveModal, + useClass: MockActiveModal, + }, ], - providers: [{ - provide: NgbActiveModal, - useClass: MockActiveModal - }], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); diff --git a/core/templates/pages/exploration-editor-page/preview-tab/templates/preview-set-parameters-modal.component.ts b/core/templates/pages/exploration-editor-page/preview-tab/templates/preview-set-parameters-modal.component.ts index 370779c14cfb..718d5d9d748b 100644 --- a/core/templates/pages/exploration-editor-page/preview-tab/templates/preview-set-parameters-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/preview-tab/templates/preview-set-parameters-modal.component.ts @@ -16,25 +16,22 @@ * @fileoverview Component for preview set parameters modal. */ -import { Component, Input } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import {Component, Input} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-preview-set-parameters-modal', - templateUrl: './preview-set-parameters-modal.component.html' + templateUrl: './preview-set-parameters-modal.component.html', }) - export class PreviewSetParametersModalComponent extends ConfirmOrCancelModal { // This property is initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @Input() manualParamChanges!: string[]; - constructor( - private ngbActiveModal: NgbActiveModal, - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } } diff --git a/core/templates/pages/exploration-editor-page/services/angular-name.service.spec.ts b/core/templates/pages/exploration-editor-page/services/angular-name.service.spec.ts index 80f61e8f01e0..43f9e1ba8831 100644 --- a/core/templates/pages/exploration-editor-page/services/angular-name.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/angular-name.service.spec.ts @@ -16,8 +16,7 @@ * @fileoverview Unit test for the Angular names service. */ -import { AngularNameService } from - 'pages/exploration-editor-page/services/angular-name.service'; +import {AngularNameService} from 'pages/exploration-editor-page/services/angular-name.service'; describe('Angular names service', () => { describe('angular name service', () => { @@ -29,7 +28,8 @@ describe('Angular names service', () => { it('should map interaction ID to correct RulesService', () => { expect(ans.getNameOfInteractionRulesService('TextInput')).toEqual( - 'TextInputRulesService'); + 'TextInputRulesService' + ); }); }); }); diff --git a/core/templates/pages/exploration-editor-page/services/angular-name.service.ts b/core/templates/pages/exploration-editor-page/services/angular-name.service.ts index 993a1c21a5a2..3af4c70c2f62 100644 --- a/core/templates/pages/exploration-editor-page/services/angular-name.service.ts +++ b/core/templates/pages/exploration-editor-page/services/angular-name.service.ts @@ -16,11 +16,11 @@ * @fileoverview A service that maps IDs to Angular names. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class AngularNameService { /** @@ -35,14 +35,12 @@ export class AngularNameService { * @returns - The angular name after mapping with ID is done. */ getNameOfInteractionRulesService(interactionId: string): string { - AngularNameService.angularName = ( - interactionId.charAt(0) + - interactionId.slice(1) + - 'RulesService' - ); + AngularNameService.angularName = + interactionId.charAt(0) + interactionId.slice(1) + 'RulesService'; return AngularNameService.angularName; } } -angular.module('oppia').factory( - 'AngularNameService', downgradeInjectable(AngularNameService)); +angular + .module('oppia') + .factory('AngularNameService', downgradeInjectable(AngularNameService)); diff --git a/core/templates/pages/exploration-editor-page/services/autosave-info-modals.service.spec.ts b/core/templates/pages/exploration-editor-page/services/autosave-info-modals.service.spec.ts index 5fd949f39c40..c02c64364d1d 100644 --- a/core/templates/pages/exploration-editor-page/services/autosave-info-modals.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/autosave-info-modals.service.spec.ts @@ -16,18 +16,17 @@ * @fileoverview Unit tests for autosaveInfoModalsService. */ -import { fakeAsync, flushMicrotasks, TestBed } from '@angular/core/testing'; -import { HttpClientModule } from '@angular/common/http'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import {fakeAsync, flushMicrotasks, TestBed} from '@angular/core/testing'; +import {HttpClientModule} from '@angular/common/http'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; -import { AutosaveInfoModalsService } from './autosave-info-modals.service'; -import { CsrfTokenService } from 'services/csrf-token.service'; -import { LostChange } from 'domain/exploration/LostChangeObjectFactory'; -import { LocalStorageService } from 'services/local-storage.service'; +import {AutosaveInfoModalsService} from './autosave-info-modals.service'; +import {CsrfTokenService} from 'services/csrf-token.service'; +import {LostChange} from 'domain/exploration/LostChangeObjectFactory'; +import {LocalStorageService} from 'services/local-storage.service'; class showNonStrictValidationFailModalRef { - componentInstance!: { - }; + componentInstance!: {}; } class showVersionMismatchModalRef { @@ -53,11 +52,7 @@ describe('AutosaveInfoModalsService', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientModule], - providers: [ - AutosaveInfoModalsService, - LocalStorageService, - NgbModal - ] + providers: [AutosaveInfoModalsService, LocalStorageService, NgbModal], }); }); @@ -68,88 +63,57 @@ describe('AutosaveInfoModalsService', () => { csrfService = TestBed.inject(CsrfTokenService); spyOn(csrfService, 'getTokenAsync').and.callFake(() => { - return new Promise((resolve) => { + return new Promise(resolve => { resolve('sample-csrf-token'); }); }); }); - it('should call ngbModal open when opening non strict validation fail' + - ' modal', fakeAsync(() => { - const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ( - { componentInstance: showNonStrictValidationFailModalRef, - result: Promise.resolve('success') - } as NgbModalRef); - }); - - autosaveInfoModalsService.showNonStrictValidationFailModal(); - - expect(modalSpy).toHaveBeenCalled(); - })); - - it('should close non strict validation fail modal successfully', - fakeAsync(() => { - expect(autosaveInfoModalsService.isModalOpen()).toBe(false); - const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ( - { componentInstance: showNonStrictValidationFailModalRef, - result: Promise.resolve('success') - } as NgbModalRef); - }); - - autosaveInfoModalsService.showNonStrictValidationFailModal(); - expect(autosaveInfoModalsService.isModalOpen()).toBe(true); - - flushMicrotasks(); - - expect(autosaveInfoModalsService.isModalOpen()).toBe(false); - expect(modalSpy).toHaveBeenCalled(); - })); - - it('should handle rejects when closing non strict validation fail modal', + it( + 'should call ngbModal open when opening non strict validation fail' + + ' modal', fakeAsync(() => { - expect(autosaveInfoModalsService.isModalOpen()).toBe(false); const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ( - { componentInstance: showNonStrictValidationFailModalRef, - result: Promise.reject('fail') - } as NgbModalRef); + return { + componentInstance: showNonStrictValidationFailModalRef, + result: Promise.resolve('success'), + } as NgbModalRef; }); autosaveInfoModalsService.showNonStrictValidationFailModal(); - expect(autosaveInfoModalsService.isModalOpen()).toBe(true); - flushMicrotasks(); - - expect(autosaveInfoModalsService.isModalOpen()).toBe(false); expect(modalSpy).toHaveBeenCalled(); - })); + }) + ); - it('should call ngbModal open when opening version mismatch' + - ' modal', fakeAsync(() => { + it('should close non strict validation fail modal successfully', fakeAsync(() => { + expect(autosaveInfoModalsService.isModalOpen()).toBe(false); const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ( - { componentInstance: showLostChangesModalRef, - result: Promise.resolve('success') - } as NgbModalRef); + return { + componentInstance: showNonStrictValidationFailModalRef, + result: Promise.resolve('success'), + } as NgbModalRef; }); - autosaveInfoModalsService.showVersionMismatchModal(lostChanges); + autosaveInfoModalsService.showNonStrictValidationFailModal(); + expect(autosaveInfoModalsService.isModalOpen()).toBe(true); + + flushMicrotasks(); + expect(autosaveInfoModalsService.isModalOpen()).toBe(false); expect(modalSpy).toHaveBeenCalled(); })); - it('should close version mismatch modal successfully', fakeAsync(() => { + it('should handle rejects when closing non strict validation fail modal', fakeAsync(() => { expect(autosaveInfoModalsService.isModalOpen()).toBe(false); const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ( - { componentInstance: showLostChangesModalRef, - result: Promise.resolve('success') - } as NgbModalRef); + return { + componentInstance: showNonStrictValidationFailModalRef, + result: Promise.reject('fail'), + } as NgbModalRef; }); - autosaveInfoModalsService.showVersionMismatchModal(lostChanges); + autosaveInfoModalsService.showNonStrictValidationFailModal(); expect(autosaveInfoModalsService.isModalOpen()).toBe(true); flushMicrotasks(); @@ -158,49 +122,50 @@ describe('AutosaveInfoModalsService', () => { expect(modalSpy).toHaveBeenCalled(); })); - it('should handle rejects when dismissing save version mismatch modal', + it( + 'should call ngbModal open when opening version mismatch' + ' modal', fakeAsync(() => { - expect(autosaveInfoModalsService.isModalOpen()).toBe(false); const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ( - { componentInstance: showVersionMismatchModalRef, - result: Promise.reject('fail') - } as NgbModalRef); + return { + componentInstance: showLostChangesModalRef, + result: Promise.resolve('success'), + } as NgbModalRef; }); autosaveInfoModalsService.showVersionMismatchModal(lostChanges); - expect(autosaveInfoModalsService.isModalOpen()).toBe(true); - - flushMicrotasks(); - expect(autosaveInfoModalsService.isModalOpen()).toBe(false); expect(modalSpy).toHaveBeenCalled(); - })); + }) + ); - it('should call ngbModal open when opening show lost changes modal', - fakeAsync(() => { - const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ( - { componentInstance: showLostChangesModalRef, - result: Promise.resolve('success') - } as NgbModalRef); - }); + it('should close version mismatch modal successfully', fakeAsync(() => { + expect(autosaveInfoModalsService.isModalOpen()).toBe(false); + const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + componentInstance: showLostChangesModalRef, + result: Promise.resolve('success'), + } as NgbModalRef; + }); - autosaveInfoModalsService.showLostChangesModal( - lostChanges, explorationId); + autosaveInfoModalsService.showVersionMismatchModal(lostChanges); + expect(autosaveInfoModalsService.isModalOpen()).toBe(true); - expect(modalSpy).toHaveBeenCalled(); - })); + flushMicrotasks(); - it('should close show lost changes modal successfully', fakeAsync(() => { + expect(autosaveInfoModalsService.isModalOpen()).toBe(false); + expect(modalSpy).toHaveBeenCalled(); + })); + + it('should handle rejects when dismissing save version mismatch modal', fakeAsync(() => { expect(autosaveInfoModalsService.isModalOpen()).toBe(false); const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ( - { componentInstance: showLostChangesModalRef, - result: Promise.resolve('success') - } as NgbModalRef); + return { + componentInstance: showVersionMismatchModalRef, + result: Promise.reject('fail'), + } as NgbModalRef; }); - autosaveInfoModalsService.showLostChangesModal(lostChanges, explorationId); + + autosaveInfoModalsService.showVersionMismatchModal(lostChanges); expect(autosaveInfoModalsService.isModalOpen()).toBe(true); flushMicrotasks(); @@ -209,26 +174,63 @@ describe('AutosaveInfoModalsService', () => { expect(modalSpy).toHaveBeenCalled(); })); - it('should handle reject when dismissing show' + - 'lost changes modal', fakeAsync(() => { - expect(autosaveInfoModalsService.isModalOpen()).toBe(false); - - const localStorageSpy = spyOn(localStorageService, 'removeExplorationDraft') - .and.callThrough(); + it('should call ngbModal open when opening show lost changes modal', fakeAsync(() => { const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ( - { componentInstance: showLostChangesModalRef, - result: Promise.reject('fail') - } as NgbModalRef); + return { + componentInstance: showLostChangesModalRef, + result: Promise.resolve('success'), + } as NgbModalRef; }); + autosaveInfoModalsService.showLostChangesModal(lostChanges, explorationId); + + expect(modalSpy).toHaveBeenCalled(); + })); + + it('should close show lost changes modal successfully', fakeAsync(() => { + expect(autosaveInfoModalsService.isModalOpen()).toBe(false); + const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + componentInstance: showLostChangesModalRef, + result: Promise.resolve('success'), + } as NgbModalRef; + }); autosaveInfoModalsService.showLostChangesModal(lostChanges, explorationId); expect(autosaveInfoModalsService.isModalOpen()).toBe(true); flushMicrotasks(); expect(autosaveInfoModalsService.isModalOpen()).toBe(false); - expect(localStorageSpy).toHaveBeenCalled(); expect(modalSpy).toHaveBeenCalled(); })); + + it( + 'should handle reject when dismissing show' + 'lost changes modal', + fakeAsync(() => { + expect(autosaveInfoModalsService.isModalOpen()).toBe(false); + + const localStorageSpy = spyOn( + localStorageService, + 'removeExplorationDraft' + ).and.callThrough(); + const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + componentInstance: showLostChangesModalRef, + result: Promise.reject('fail'), + } as NgbModalRef; + }); + + autosaveInfoModalsService.showLostChangesModal( + lostChanges, + explorationId + ); + expect(autosaveInfoModalsService.isModalOpen()).toBe(true); + + flushMicrotasks(); + + expect(autosaveInfoModalsService.isModalOpen()).toBe(false); + expect(localStorageSpy).toHaveBeenCalled(); + expect(modalSpy).toHaveBeenCalled(); + }) + ); }); diff --git a/core/templates/pages/exploration-editor-page/services/autosave-info-modals.service.ts b/core/templates/pages/exploration-editor-page/services/autosave-info-modals.service.ts index be00f9e06e3f..f4187523179a 100644 --- a/core/templates/pages/exploration-editor-page/services/autosave-info-modals.service.ts +++ b/core/templates/pages/exploration-editor-page/services/autosave-info-modals.service.ts @@ -17,19 +17,19 @@ * on the type of response received as a result of the autosaving request. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { LocalStorageService } from 'services/local-storage.service'; -import { SaveVersionMismatchModalComponent } from '../modal-templates/save-version-mismatch-modal.component'; -import { SaveValidationFailModalComponent } from '../modal-templates/save-validation-fail-modal.component'; -import { LostChangesModalComponent } from '../modal-templates/lost-changes-modal.component'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { LostChange } from 'domain/exploration/LostChangeObjectFactory'; -import { ExplorationChange } from 'domain/exploration/exploration-draft.model'; +import {LocalStorageService} from 'services/local-storage.service'; +import {SaveVersionMismatchModalComponent} from '../modal-templates/save-version-mismatch-modal.component'; +import {SaveValidationFailModalComponent} from '../modal-templates/save-validation-fail-modal.component'; +import {LostChangesModalComponent} from '../modal-templates/lost-changes-modal.component'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {LostChange} from 'domain/exploration/LostChangeObjectFactory'; +import {ExplorationChange} from 'domain/exploration/exploration-draft.model'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class AutosaveInfoModalsService { private _isModalOpen: boolean = false; @@ -37,17 +37,21 @@ export class AutosaveInfoModalsService { constructor( private localStorageService: LocalStorageService, - private ngbModal: NgbModal, + private ngbModal: NgbModal ) {} showNonStrictValidationFailModal(): void { - const modelRef = this.ngbModal.open( - SaveValidationFailModalComponent, {backdrop: true}); - modelRef.result.then(() => { - this._isModalOpen = false; - }, () => { - this._isModalOpen = false; + const modelRef = this.ngbModal.open(SaveValidationFailModalComponent, { + backdrop: true, }); + modelRef.result.then( + () => { + this._isModalOpen = false; + }, + () => { + this._isModalOpen = false; + } + ); this._isModalOpen = true; } @@ -57,51 +61,59 @@ export class AutosaveInfoModalsService { } showVersionMismatchModal( - lostChanges: LostChange[] | ExplorationChange[]): void { + lostChanges: LostChange[] | ExplorationChange[] + ): void { if (!this.isLostChangesModalOpen) { this.isLostChangesModalOpen = true; - const modelRef = this.ngbModal.open( - SaveVersionMismatchModalComponent, { - backdrop: 'static', - keyboard: false - }); - modelRef.componentInstance.lostChanges = lostChanges; - modelRef.result.then(() => { - this._isModalOpen = false; - this.isLostChangesModalOpen = false; - }, () => { - this._isModalOpen = false; - this.isLostChangesModalOpen = false; + const modelRef = this.ngbModal.open(SaveVersionMismatchModalComponent, { + backdrop: 'static', + keyboard: false, }); + modelRef.componentInstance.lostChanges = lostChanges; + modelRef.result.then( + () => { + this._isModalOpen = false; + this.isLostChangesModalOpen = false; + }, + () => { + this._isModalOpen = false; + this.isLostChangesModalOpen = false; + } + ); } this._isModalOpen = true; } showLostChangesModal( - lostChanges: LostChange[] | ExplorationChange[], - explorationId: string): void { - const modelRef = this.ngbModal.open( - LostChangesModalComponent, { - backdrop: 'static', - keyboard: false - }); - modelRef.componentInstance.lostChanges = lostChanges; - modelRef.result.then(() => { - this._isModalOpen = false; - }, () => { - // When the user clicks on discard changes button, signal backend - // to discard the draft and reload the page thereafter. - this.localStorageService.removeExplorationDraft(explorationId); - this._isModalOpen = false; + lostChanges: LostChange[] | ExplorationChange[], + explorationId: string + ): void { + const modelRef = this.ngbModal.open(LostChangesModalComponent, { + backdrop: 'static', + keyboard: false, }); + modelRef.componentInstance.lostChanges = lostChanges; + modelRef.result.then( + () => { + this._isModalOpen = false; + }, + () => { + // When the user clicks on discard changes button, signal backend + // to discard the draft and reload the page thereafter. + this.localStorageService.removeExplorationDraft(explorationId); + this._isModalOpen = false; + } + ); this._isModalOpen = true; } } -angular.module('oppia').factory( - 'AutosaveInfoModalsService', - downgradeInjectable(AutosaveInfoModalsService) -); +angular + .module('oppia') + .factory( + 'AutosaveInfoModalsService', + downgradeInjectable(AutosaveInfoModalsService) + ); diff --git a/core/templates/pages/exploration-editor-page/services/change-list.service.spec.ts b/core/templates/pages/exploration-editor-page/services/change-list.service.spec.ts index 9c8fe45e2344..0d7c575ebe0f 100644 --- a/core/templates/pages/exploration-editor-page/services/change-list.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/change-list.service.spec.ts @@ -16,17 +16,17 @@ * @fileoverview Tests for Change List Service. */ -import { async, fakeAsync, flush, TestBed } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ChangeListService } from './change-list.service'; -import { LoaderService } from 'services/loader.service'; -import { EventEmitter } from '@angular/core'; -import { InternetConnectivityService } from 'services/internet-connectivity.service'; -import { ExplorationDataService } from './exploration-data.service'; -import { AutosaveInfoModalsService } from './autosave-info-modals.service'; -import { AlertsService } from 'services/alerts.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { TranslatedContent } from 'domain/exploration/TranslatedContentObjectFactory'; +import {async, fakeAsync, flush, TestBed} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ChangeListService} from './change-list.service'; +import {LoaderService} from 'services/loader.service'; +import {EventEmitter} from '@angular/core'; +import {InternetConnectivityService} from 'services/internet-connectivity.service'; +import {ExplorationDataService} from './exploration-data.service'; +import {AutosaveInfoModalsService} from './autosave-info-modals.service'; +import {AlertsService} from 'services/alerts.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {TranslatedContent} from 'domain/exploration/TranslatedContentObjectFactory'; class MockWindowRef { _window = { @@ -45,16 +45,16 @@ class MockWindowRef { set pathname(val) { this._pathname = val; }, - reload: () => {} + reload: () => {}, }, localStorage: { last_uploaded_audio_lang: 'en', - removeItem: (name: string) => {} + removeItem: (name: string) => {}, }, navigator: { // Internet Connection. - onLine: true - } + onLine: true, + }, }; get nativeWindow() { @@ -65,13 +65,12 @@ class MockWindowRef { class MockExplorationDataService1 { explorationId!: 0; autosaveChangeListAsync( - changeList: string[], - successCb: ( - arg0: { - changes_are_mergeable: boolean; - is_version_of_draft_valid: boolean; - }) => void, - errorCb: () => void + changeList: string[], + successCb: (arg0: { + changes_are_mergeable: boolean; + is_version_of_draft_valid: boolean; + }) => void, + errorCb: () => void ) { successCb({ changes_are_mergeable: true, @@ -87,13 +86,12 @@ class MockExplorationDataService1 { class MockExplorationDataService2 { explorationId!: 0; autosaveChangeListAsync( - changeList: string[], - successCb: ( - arg0: { - changes_are_mergeable: boolean; - is_version_of_draft_valid: boolean; - }) => void, - errorCb: () => void + changeList: string[], + successCb: (arg0: { + changes_are_mergeable: boolean; + is_version_of_draft_valid: boolean; + }) => void, + errorCb: () => void ) { successCb({ changes_are_mergeable: false, @@ -109,9 +107,9 @@ class MockExplorationDataService2 { class MockExplorationDataService3 { explorationId!: 0; autosaveChangeListAsync( - changeList: string[], - successCb: () => void, - errorCb: () => void + changeList: string[], + successCb: () => void, + errorCb: () => void ) { errorCb(); } @@ -150,19 +148,19 @@ describe('Change List Service when changes are mergable', () => { providers: [ { provide: ExplorationDataService, - useValue: mockExplorationDataService + useValue: mockExplorationDataService, }, { provide: WindowRef, - useValue: mockWindowRef + useValue: mockWindowRef, }, { provide: LoaderService, useValue: { - onLoadingMessageChange: mockEventEmitter - } - } - ] + onLoadingMessageChange: mockEventEmitter, + }, + }, + ], }); })); @@ -172,12 +170,15 @@ describe('Change List Service when changes are mergable', () => { autosaveInfoModalsService = TestBed.inject(AutosaveInfoModalsService); alertsService = TestBed.inject(AlertsService); - spyOn(autosaveInfoModalsService, 'showVersionMismatchModal') - .and.returnValue(); - spyOn(autosaveInfoModalsService, 'showNonStrictValidationFailModal') - .and.returnValue(); - alertsSpy = spyOn(alertsService, 'addWarning') - .and.returnValue(); + spyOn( + autosaveInfoModalsService, + 'showVersionMismatchModal' + ).and.returnValue(); + spyOn( + autosaveInfoModalsService, + 'showNonStrictValidationFailModal' + ).and.returnValue(); + alertsSpy = spyOn(alertsService, 'addWarning').and.returnValue(); }); it('should set loading message when initialized', () => { @@ -186,45 +187,57 @@ describe('Change List Service when changes are mergable', () => { expect(changeListService.loadingMessage).toBe('loadingMessage'); }); - it('should save changes after deleting a state ' + - 'when calling \'deleteState\'', fakeAsync(() => { - changeListService.changeListAddedTimeoutId = setTimeout(() => {}, 10); - changeListService.explorationChangeList.length = 0; - let saveSpy = spyOn( - changeListService.autosaveInProgressEventEmitter, 'emit') - .and.callThrough(); - - changeListService.deleteState('state'); - flush(); - - expect(saveSpy).toHaveBeenCalled(); - })); - - it('should save changes after adding a state ' + - 'when calling \'addState\'', fakeAsync(() => { - changeListService.changeListAddedTimeoutId = setTimeout(() => {}, 10); - changeListService.explorationChangeList.length = 0; - let saveSpy = spyOn( - changeListService.autosaveInProgressEventEmitter, 'emit') - .and.callThrough(); - - changeListService.addState('state', 'content_1', 'dafault_outcome_4'); - flush(); - - expect(saveSpy).toHaveBeenCalled(); - })); + it( + 'should save changes after deleting a state ' + + "when calling 'deleteState'", + fakeAsync(() => { + changeListService.changeListAddedTimeoutId = setTimeout(() => {}, 10); + changeListService.explorationChangeList.length = 0; + let saveSpy = spyOn( + changeListService.autosaveInProgressEventEmitter, + 'emit' + ).and.callThrough(); + + changeListService.deleteState('state'); + flush(); + + expect(saveSpy).toHaveBeenCalled(); + }) + ); + + it( + 'should save changes after adding a state ' + "when calling 'addState'", + fakeAsync(() => { + changeListService.changeListAddedTimeoutId = setTimeout(() => {}, 10); + changeListService.explorationChangeList.length = 0; + let saveSpy = spyOn( + changeListService.autosaveInProgressEventEmitter, + 'emit' + ).and.callThrough(); + + changeListService.addState('state', 'content_1', 'dafault_outcome_4'); + flush(); + + expect(saveSpy).toHaveBeenCalled(); + }) + ); it('should add Written Translation', fakeAsync(() => { changeListService.changeListAddedTimeoutId = setTimeout(() => {}, 10); changeListService.explorationChangeList.length = 0; changeListService.loadingMessage = ''; changeListService.addWrittenTranslation( - 'contentId', 'dataFormat', - 'languageCode', 'stateName', 'translationHtml'); + 'contentId', + 'dataFormat', + 'languageCode', + 'stateName', + 'translationHtml' + ); let saveSpy = spyOn( - changeListService.autosaveInProgressEventEmitter, 'emit') - .and.callThrough(); + changeListService.autosaveInProgressEventEmitter, + 'emit' + ).and.callThrough(); changeListService.addState('state', 'content_1', 'dafault_outcome_4'); flush(); @@ -233,12 +246,13 @@ describe('Change List Service when changes are mergable', () => { })); it('should add change for markTranslationsAsNeedingUpdate', fakeAsync(() => { - changeListService.changeListAddedTimeoutId = setTimeout(() => { }, 10); + changeListService.changeListAddedTimeoutId = setTimeout(() => {}, 10); changeListService.explorationChangeList.length = 0; changeListService.loadingMessage = ''; let saveSpy = spyOn( - changeListService.autosaveInProgressEventEmitter, 'emit') - .and.callThrough(); + changeListService.autosaveInProgressEventEmitter, + 'emit' + ).and.callThrough(); changeListService.markTranslationsAsNeedingUpdate('content_id_1'); flush(); @@ -246,12 +260,13 @@ describe('Change List Service when changes are mergable', () => { })); it('should add change for removeTranslations', fakeAsync(() => { - changeListService.changeListAddedTimeoutId = setTimeout(() => { }, 10); + changeListService.changeListAddedTimeoutId = setTimeout(() => {}, 10); changeListService.explorationChangeList.length = 0; changeListService.loadingMessage = ''; let saveSpy = spyOn( - changeListService.autosaveInProgressEventEmitter, 'emit') - .and.callThrough(); + changeListService.autosaveInProgressEventEmitter, + 'emit' + ).and.callThrough(); changeListService.removeTranslations('content_id_1'); flush(); @@ -259,34 +274,39 @@ describe('Change List Service when changes are mergable', () => { })); it('should add change for edit translations', fakeAsync(() => { - changeListService.changeListAddedTimeoutId = setTimeout(() => { }, 10); + changeListService.changeListAddedTimeoutId = setTimeout(() => {}, 10); changeListService.explorationChangeList.length = 0; changeListService.loadingMessage = ''; let saveSpy = spyOn( - changeListService.autosaveInProgressEventEmitter, 'emit') - .and.callThrough(); + changeListService.autosaveInProgressEventEmitter, + 'emit' + ).and.callThrough(); changeListService.editTranslation( - 'content_id_1', 'en', new TranslatedContent( - 'Translated content', 'unicode', false)); + 'content_id_1', + 'en', + new TranslatedContent('Translated content', 'unicode', false) + ); flush(); expect(saveSpy).toHaveBeenCalled(); expect(changeListService.explorationChangeList.length).toEqual(1); })); it('should get all translation changelist', fakeAsync(() => { - changeListService.changeListAddedTimeoutId = setTimeout(() => { }, 10); + changeListService.changeListAddedTimeoutId = setTimeout(() => {}, 10); changeListService.explorationChangeList.length = 0; changeListService.loadingMessage = ''; spyOn( - changeListService.autosaveInProgressEventEmitter, 'emit') - .and.callThrough(); + changeListService.autosaveInProgressEventEmitter, + 'emit' + ).and.callThrough(); changeListService.editTranslation( - 'content_id_1', 'en', new TranslatedContent( - 'Translated content', 'unicode', false)); - changeListService.addState( - 'state_2', 'content_id_2', 'content_id_default'); + 'content_id_1', + 'en', + new TranslatedContent('Translated content', 'unicode', false) + ); + changeListService.addState('state_2', 'content_id_2', 'content_id_default'); changeListService.removeTranslations('content_id_3'); changeListService.markTranslationsAsNeedingUpdate('content_id_4'); flush(); @@ -294,130 +314,171 @@ describe('Change List Service when changes are mergable', () => { expect(changeListService.explorationChangeList.length).toEqual(4); const translationChangeList = changeListService.getTranslationChangeList(); expect(translationChangeList.length).toEqual(3); - expect( - translationChangeList.map(change => change.cmd)).toEqual( + expect(translationChangeList.map(change => change.cmd)).toEqual( jasmine.arrayWithExactContents([ 'remove_translations', 'edit_translation', - 'mark_translations_needs_update' - ])); + 'mark_translations_needs_update', + ]) + ); })); - it('should save changes after renaming a state ' + - 'when calling \'renameState\'', fakeAsync(() => { - changeListService.changeListAddedTimeoutId = setTimeout(() => {}, 10); - changeListService.explorationChangeList.length = 0; - let saveSpy = spyOn( - changeListService.autosaveInProgressEventEmitter, 'emit') - .and.callThrough(); - - changeListService.renameState('oldState', 'newState'); - flush(); - - expect(saveSpy).toHaveBeenCalled(); - })); - - it('should save changes after editing exploration property ' + - 'when calling \'editExplorationProperty\'', fakeAsync(() => { - changeListService.changeListAddedTimeoutId = setTimeout(() => {}, 10); - changeListService.explorationChangeList.length = 0; - let saveSpy = spyOn( - changeListService.autosaveInProgressEventEmitter, 'emit') - .and.callThrough(); - - changeListService.editExplorationProperty( - 'language_code', 'oldValue', 'newValue'); - flush(); - - expect(saveSpy).toHaveBeenCalled(); - })); - - it('should save changes after editing state property ' + - 'when calling \'editExplorationProperty\'', fakeAsync(() => { - changeListService.changeListAddedTimeoutId = setTimeout(() => {}, 10); - changeListService.explorationChangeList.length = 0; - let saveSpy = spyOn( - changeListService.autosaveInProgressEventEmitter, 'emit') - .and.callThrough(); - - changeListService.editStateProperty( - 'state', 'solution', 'oldValue', 'newValue'); - flush(); - - expect(saveSpy).toHaveBeenCalled(); - })); - - it('should not save changes after deleting a state ' + - 'if loading message is being shown', () => { - changeListService.explorationChangeList.length = 0; - let saveSpy = spyOn( - changeListService.autosaveInProgressEventEmitter, 'emit') - .and.callThrough(); - - // Setting loading message. - changeListService.loadingMessage = 'loadingMessage'; - changeListService.deleteState('state'); - - expect(saveSpy).not.toHaveBeenCalled(); - }); - - it('should not save changes after deleting a state ' + - 'if internet is offline', () => { - changeListService.explorationChangeList.length = 0; - let saveSpy = spyOn( - changeListService.autosaveInProgressEventEmitter, 'emit') - .and.callThrough(); - // Setting internet to offline. - spyOn(internetConnectivityService, 'isOnline') - .and.returnValue(false); - - changeListService.deleteState('state'); - - expect(saveSpy).not.toHaveBeenCalled(); - }); + it( + 'should save changes after renaming a state ' + + "when calling 'renameState'", + fakeAsync(() => { + changeListService.changeListAddedTimeoutId = setTimeout(() => {}, 10); + changeListService.explorationChangeList.length = 0; + let saveSpy = spyOn( + changeListService.autosaveInProgressEventEmitter, + 'emit' + ).and.callThrough(); + + changeListService.renameState('oldState', 'newState'); + flush(); + + expect(saveSpy).toHaveBeenCalled(); + }) + ); + + it( + 'should save changes after editing exploration property ' + + "when calling 'editExplorationProperty'", + fakeAsync(() => { + changeListService.changeListAddedTimeoutId = setTimeout(() => {}, 10); + changeListService.explorationChangeList.length = 0; + let saveSpy = spyOn( + changeListService.autosaveInProgressEventEmitter, + 'emit' + ).and.callThrough(); + + changeListService.editExplorationProperty( + 'language_code', + 'oldValue', + 'newValue' + ); + flush(); + + expect(saveSpy).toHaveBeenCalled(); + }) + ); + + it( + 'should save changes after editing state property ' + + "when calling 'editExplorationProperty'", + fakeAsync(() => { + changeListService.changeListAddedTimeoutId = setTimeout(() => {}, 10); + changeListService.explorationChangeList.length = 0; + let saveSpy = spyOn( + changeListService.autosaveInProgressEventEmitter, + 'emit' + ).and.callThrough(); + + changeListService.editStateProperty( + 'state', + 'solution', + 'oldValue', + 'newValue' + ); + flush(); + + expect(saveSpy).toHaveBeenCalled(); + }) + ); + + it( + 'should not save changes after deleting a state ' + + 'if loading message is being shown', + () => { + changeListService.explorationChangeList.length = 0; + let saveSpy = spyOn( + changeListService.autosaveInProgressEventEmitter, + 'emit' + ).and.callThrough(); + + // Setting loading message. + changeListService.loadingMessage = 'loadingMessage'; + changeListService.deleteState('state'); + + expect(saveSpy).not.toHaveBeenCalled(); + } + ); + + it( + 'should not save changes after deleting a state ' + + 'if internet is offline', + () => { + changeListService.explorationChangeList.length = 0; + let saveSpy = spyOn( + changeListService.autosaveInProgressEventEmitter, + 'emit' + ).and.callThrough(); + // Setting internet to offline. + spyOn(internetConnectivityService, 'isOnline').and.returnValue(false); + + changeListService.deleteState('state'); + + expect(saveSpy).not.toHaveBeenCalled(); + } + ); - it('should discard all changes ' + - 'when calling \'discardAllChanges\'', () => { - let discardSpy = spyOn(mockExplorationDataService, 'discardDraftAsync') - .and.callThrough(); + it('should discard all changes ' + "when calling 'discardAllChanges'", () => { + let discardSpy = spyOn( + mockExplorationDataService, + 'discardDraftAsync' + ).and.callThrough(); changeListService.discardAllChanges(); expect(discardSpy).toHaveBeenCalled(); }); - it('should show alert message if we try to edit ' + - 'an exploration with invalid property', () => { - changeListService.editExplorationProperty( - 'prop1', 'oldValue', 'newValue'); - - expect(alertsSpy).toHaveBeenCalledWith( - 'Invalid exploration property: prop1'); - }); - - it('should show alert message if we try to edit ' + - 'an state with invalid property', () => { - changeListService.editStateProperty( - // This throws "Argument of type 'prop1' is not assignable to parameter - // of type 'StatePropertyNames'.". We need to suppress this error because - // we want to test passing wrong values. - // @ts-expect-error - 'stateName', 'prop1', 'oldValue', 'newValue'); - - expect(alertsSpy).toHaveBeenCalledWith( - 'Invalid state property: prop1'); - }); + it( + 'should show alert message if we try to edit ' + + 'an exploration with invalid property', + () => { + changeListService.editExplorationProperty( + 'prop1', + 'oldValue', + 'newValue' + ); + + expect(alertsSpy).toHaveBeenCalledWith( + 'Invalid exploration property: prop1' + ); + } + ); + + it( + 'should show alert message if we try to edit ' + + 'an state with invalid property', + () => { + changeListService.editStateProperty( + 'stateName', + // This throws "Argument of type 'prop1' is not assignable to parameter + // of type 'StatePropertyNames'.". We need to suppress this error because + // we want to test passing wrong values. + // @ts-expect-error + 'prop1', + 'oldValue', + 'newValue' + ); + + expect(alertsSpy).toHaveBeenCalledWith('Invalid state property: prop1'); + } + ); - it('should check whether exploration locked for editing ' + - 'when calling \'isExplorationLockedForEditing\'', () => { - changeListService.explorationChangeList.length = 2; - expect(changeListService.isExplorationLockedForEditing()) - .toBe(true); + it( + 'should check whether exploration locked for editing ' + + "when calling 'isExplorationLockedForEditing'", + () => { + changeListService.explorationChangeList.length = 2; + expect(changeListService.isExplorationLockedForEditing()).toBe(true); - changeListService.explorationChangeList.length = 0; - expect(changeListService.isExplorationLockedForEditing()) - .toBe(false); - }); + changeListService.explorationChangeList.length = 0; + expect(changeListService.isExplorationLockedForEditing()).toBe(false); + } + ); }); describe('Change List Service when changes are not mergable', () => { @@ -437,13 +498,13 @@ describe('Change List Service when changes are not mergable', () => { providers: [ { provide: ExplorationDataService, - useValue: mockExplorationDataService + useValue: mockExplorationDataService, }, { provide: WindowRef, - useValue: mockWindowRef - } - ] + useValue: mockWindowRef, + }, + ], }); })); @@ -452,18 +513,22 @@ describe('Change List Service when changes are not mergable', () => { autosaveInfoModalsService = TestBed.inject(AutosaveInfoModalsService); alertsService = TestBed.inject(AlertsService); - spyOn(autosaveInfoModalsService, 'showVersionMismatchModal') - .and.returnValue(); - spyOn(autosaveInfoModalsService, 'showNonStrictValidationFailModal') - .and.returnValue(); - alertsSpy = spyOn(alertsService, 'addWarning') - .and.returnValue(); + spyOn( + autosaveInfoModalsService, + 'showVersionMismatchModal' + ).and.returnValue(); + spyOn( + autosaveInfoModalsService, + 'showNonStrictValidationFailModal' + ).and.returnValue(); + alertsSpy = spyOn(alertsService, 'addWarning').and.returnValue(); }); - it('should undo and save changes when calling \'undoLastChange\'', () => { + it("should undo and save changes when calling 'undoLastChange'", () => { let saveSpy = spyOn( - changeListService.autosaveInProgressEventEmitter, 'emit') - .and.returnValue(); + changeListService.autosaveInProgressEventEmitter, + 'emit' + ).and.returnValue(); changeListService.explorationChangeList.length = 2; changeListService.undoLastChange(); @@ -476,8 +541,7 @@ describe('Change List Service when changes are not mergable', () => { changeListService.undoLastChange(); - expect(alertsSpy).toHaveBeenCalledWith( - 'There are no changes to undo.'); + expect(alertsSpy).toHaveBeenCalledWith('There are no changes to undo.'); }); }); @@ -500,15 +564,15 @@ describe('Change List Service when internet is available', () => { providers: [ { provide: ExplorationDataService, - useValue: mockExplorationDataService + useValue: mockExplorationDataService, }, { provide: WindowRef, - useValue: mockWindowRef + useValue: mockWindowRef, }, { provide: AutosaveInfoModalsService, - useValue: mockAutosaveInfoModalsService + useValue: mockAutosaveInfoModalsService, }, { provide: InternetConnectivityService, @@ -516,30 +580,32 @@ describe('Change List Service when internet is available', () => { onInternetStateChange: onInternetStateChangeEventEmitter, isOnline() { return true; - } - } - } - ] + }, + }, + }, + ], }); })); beforeEach(() => { changeListService = TestBed.inject(ChangeListService); alertsService = TestBed.inject(AlertsService); - alertsSpy = spyOn(alertsService, 'addWarning') - .and.returnValue(); + alertsSpy = spyOn(alertsService, 'addWarning').and.returnValue(); }); - it('should undo and save changes when calling \'undoLastChange\'', () => { + it("should undo and save changes when calling 'undoLastChange'", () => { let saveSpy = spyOn( - changeListService.autosaveInProgressEventEmitter, 'emit') - .and.returnValue(); - changeListService.temporaryListOfChanges = [{ - cmd: 'add_state', - state_name: 'stateName', - content_id_for_state_content: 'content_0', - content_id_for_default_outcome: 'default_outcome_1' - }]; + changeListService.autosaveInProgressEventEmitter, + 'emit' + ).and.returnValue(); + changeListService.temporaryListOfChanges = [ + { + cmd: 'add_state', + state_name: 'stateName', + content_id_for_state_content: 'content_0', + content_id_for_default_outcome: 'default_outcome_1', + }, + ]; changeListService.explorationChangeList.length = 2; onInternetStateChangeEventEmitter.emit(true); @@ -549,16 +615,17 @@ describe('Change List Service when internet is available', () => { }); it('should not undo changes when there are no changes', () => { - changeListService.temporaryListOfChanges = [{ - cmd: 'add_state', - state_name: 'stateName', - content_id_for_state_content: 'content_0', - content_id_for_default_outcome: 'default_outcome_1' - }]; + changeListService.temporaryListOfChanges = [ + { + cmd: 'add_state', + state_name: 'stateName', + content_id_for_state_content: 'content_0', + content_id_for_default_outcome: 'default_outcome_1', + }, + ]; changeListService.undoLastChange(); - expect(alertsSpy).toHaveBeenCalledWith( - 'There are no changes to undo.'); + expect(alertsSpy).toHaveBeenCalledWith('There are no changes to undo.'); }); }); diff --git a/core/templates/pages/exploration-editor-page/services/change-list.service.ts b/core/templates/pages/exploration-editor-page/services/change-list.service.ts index 5746865f32ea..00d28bf61045 100644 --- a/core/templates/pages/exploration-editor-page/services/change-list.service.ts +++ b/core/templates/pages/exploration-editor-page/services/change-list.service.ts @@ -17,73 +17,91 @@ * committed to the server. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { EventEmitter, Output } from '@angular/core'; -import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {EventEmitter, Output} from '@angular/core'; +import {Injectable} from '@angular/core'; +import {Observable} from 'rxjs'; -import { AutosaveInfoModalsService } from 'pages/exploration-editor-page/services/autosave-info-modals.service'; -import { ExplorationDataService } from 'pages/exploration-editor-page/services/exploration-data.service'; -import { AlertsService } from 'services/alerts.service'; -import { LoaderService } from 'services/loader.service'; -import { LoggerService } from 'services/contextual/logger.service'; -import { ExplorationChange, ExplorationChangeEditExplorationProperty } from 'domain/exploration/exploration-draft.model'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { InternetConnectivityService } from 'services/internet-connectivity.service'; -import { SubtitledHtml, SubtitledHtmlBackendDict } from 'domain/exploration/subtitled-html.model'; -import { ParamChange, ParamChangeBackendDict } from 'domain/exploration/ParamChangeObjectFactory'; -import { InteractionCustomizationArgs, InteractionCustomizationArgsBackendDict } from 'interactions/customization-args-defs'; -import { AnswerGroup, AnswerGroupBackendDict } from 'domain/exploration/AnswerGroupObjectFactory'; -import { Hint, HintBackendDict } from 'domain/exploration/hint-object.model'; -import { Outcome, OutcomeBackendDict } from 'domain/exploration/OutcomeObjectFactory'; -import { RecordedVoiceOverBackendDict, RecordedVoiceovers } from 'domain/exploration/recorded-voiceovers.model'; -import { LostChange } from 'domain/exploration/LostChangeObjectFactory'; -import { BaseTranslatableObject } from 'domain/objects/BaseTranslatableObject.model'; -import { TranslatedContent } from 'domain/exploration/TranslatedContentObjectFactory'; +import {AutosaveInfoModalsService} from 'pages/exploration-editor-page/services/autosave-info-modals.service'; +import {ExplorationDataService} from 'pages/exploration-editor-page/services/exploration-data.service'; +import {AlertsService} from 'services/alerts.service'; +import {LoaderService} from 'services/loader.service'; +import {LoggerService} from 'services/contextual/logger.service'; +import { + ExplorationChange, + ExplorationChangeEditExplorationProperty, +} from 'domain/exploration/exploration-draft.model'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {InternetConnectivityService} from 'services/internet-connectivity.service'; +import { + SubtitledHtml, + SubtitledHtmlBackendDict, +} from 'domain/exploration/subtitled-html.model'; +import { + ParamChange, + ParamChangeBackendDict, +} from 'domain/exploration/ParamChangeObjectFactory'; +import { + InteractionCustomizationArgs, + InteractionCustomizationArgsBackendDict, +} from 'interactions/customization-args-defs'; +import { + AnswerGroup, + AnswerGroupBackendDict, +} from 'domain/exploration/AnswerGroupObjectFactory'; +import {Hint, HintBackendDict} from 'domain/exploration/hint-object.model'; +import { + Outcome, + OutcomeBackendDict, +} from 'domain/exploration/OutcomeObjectFactory'; +import { + RecordedVoiceOverBackendDict, + RecordedVoiceovers, +} from 'domain/exploration/recorded-voiceovers.model'; +import {LostChange} from 'domain/exploration/LostChangeObjectFactory'; +import {BaseTranslatableObject} from 'domain/objects/BaseTranslatableObject.model'; +import {TranslatedContent} from 'domain/exploration/TranslatedContentObjectFactory'; -export type StatePropertyValues = ( - AnswerGroup[] | - boolean | - Hint[] | - InteractionCustomizationArgs | - Outcome | - ParamChange[] | - RecordedVoiceovers | - string | - SubtitledHtml | - BaseTranslatableObject -); -export type StatePropertyDictValues = ( - AnswerGroupBackendDict[] | - boolean | - HintBackendDict[] | - InteractionCustomizationArgsBackendDict | - OutcomeBackendDict | - ParamChangeBackendDict[] | - RecordedVoiceOverBackendDict | - string | - SubtitledHtmlBackendDict -); -export type StatePropertyNames = ( - 'answer_groups' | - 'card_is_checkpoint' | - 'confirmed_unclassified_answers' | - 'content' | - 'default_outcome' | - 'hints' | - 'linked_skill_id' | - 'param_changes' | - 'param_specs' | - 'recorded_voiceovers' | - 'solicit_answer_details' | - 'solution' | - 'state_name' | - 'widget_customization_args' | - 'widget_id' -); +export type StatePropertyValues = + | AnswerGroup[] + | boolean + | Hint[] + | InteractionCustomizationArgs + | Outcome + | ParamChange[] + | RecordedVoiceovers + | string + | SubtitledHtml + | BaseTranslatableObject; +export type StatePropertyDictValues = + | AnswerGroupBackendDict[] + | boolean + | HintBackendDict[] + | InteractionCustomizationArgsBackendDict + | OutcomeBackendDict + | ParamChangeBackendDict[] + | RecordedVoiceOverBackendDict + | string + | SubtitledHtmlBackendDict; +export type StatePropertyNames = + | 'answer_groups' + | 'card_is_checkpoint' + | 'confirmed_unclassified_answers' + | 'content' + | 'default_outcome' + | 'hints' + | 'linked_skill_id' + | 'param_changes' + | 'param_specs' + | 'recorded_voiceovers' + | 'solicit_answer_details' + | 'solution' + | 'state_name' + | 'widget_customization_args' + | 'widget_id'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ChangeListService { // Temporary buffer for changes made to the exploration. @@ -96,8 +114,8 @@ export class ChangeListService { // Temporary list of the changes made to the exploration when offline. temporaryListOfChanges: ExplorationChange[] = []; - @Output() autosaveInProgressEventEmitter: EventEmitter = ( - new EventEmitter()); + @Output() autosaveInProgressEventEmitter: EventEmitter = + new EventEmitter(); ALLOWED_EXPLORATION_BACKEND_NAMES = { category: true, @@ -127,7 +145,7 @@ export class ChangeListService { solution: true, state_name: true, widget_customization_args: true, - widget_id: true + widget_id: true, }; // This property is initialized using private methods and we need to do @@ -143,13 +161,13 @@ export class ChangeListService { private explorationDataService: ExplorationDataService, private loaderService: LoaderService, private loggerService: LoggerService, - private internetConnectivityService: InternetConnectivityService, + private internetConnectivityService: InternetConnectivityService ) { // We have added subscriptions in the constructor. // Since, ngOnInit does not work in angular services. // Ref: https://github.com/angular/angular/issues/23235. this.loaderService.onLoadingMessageChange.subscribe( - (message: string) => this.loadingMessage = message + (message: string) => (this.loadingMessage = message) ); this.internetConnectivityService.onInternetStateChange.subscribe( internetAccessible => { @@ -159,11 +177,13 @@ export class ChangeListService { } this.temporaryListOfChanges = []; } - }); + } + ); } private autosaveChangeListOnChange( - explorationChangeList: ExplorationChange[] | LostChange[]) { + explorationChangeList: ExplorationChange[] | LostChange[] + ) { // Asynchronously send an autosave request, and check for errors in the // response: // If error is present -> Check for the type of error occurred @@ -177,20 +197,23 @@ export class ChangeListService { if (!response.changes_are_mergeable) { if (!this.autosaveInfoModalsService.isModalOpen()) { this.autosaveInfoModalsService.showVersionMismatchModal( - explorationChangeList as LostChange[]); + explorationChangeList as LostChange[] + ); } } this.autosaveInProgressEventEmitter.emit(false); - if (!response.is_version_of_draft_valid && - response.changes_are_mergeable) { + if ( + !response.is_version_of_draft_valid && + response.changes_are_mergeable + ) { this.windowRef.nativeWindow.location.reload(); } }, () => { this.alertsService.clearWarnings(); this.loggerService.error( - 'nonStrictValidationFailure: ' + - JSON.stringify(explorationChangeList)); + 'nonStrictValidationFailure: ' + JSON.stringify(explorationChangeList) + ); if (!this.autosaveInfoModalsService.isModalOpen()) { this.autosaveInfoModalsService.showNonStrictValidationFailModal(); } @@ -225,14 +248,15 @@ export class ChangeListService { * @param {string} stateName - The name of the newly-added state */ addState( - stateName: string, - contentIdForContent: string, - contentIdForDefaultOutcome: string): void { + stateName: string, + contentIdForContent: string, + contentIdForDefaultOutcome: string + ): void { this.addChange({ cmd: 'add_state', state_name: stateName, content_id_for_state_content: contentIdForContent, - content_id_for_default_outcome: contentIdForDefaultOutcome + content_id_for_default_outcome: contentIdForDefaultOutcome, }); } @@ -246,7 +270,7 @@ export class ChangeListService { deleteState(stateName: string): void { this.addChange({ cmd: 'delete_state', - state_name: stateName + state_name: stateName, }); } @@ -267,20 +291,21 @@ export class ChangeListService { * @param {string} oldValue - The previous value of the property */ editExplorationProperty( - backendName: string, - newValue: string, - oldValue: string + backendName: string, + newValue: string, + oldValue: string ): void { if (!this.ALLOWED_EXPLORATION_BACKEND_NAMES.hasOwnProperty(backendName)) { this.alertsService.addWarning( - 'Invalid exploration property: ' + backendName); + 'Invalid exploration property: ' + backendName + ); return; } this.addChange({ cmd: 'edit_exploration_property', new_value: angular.copy(newValue), old_value: angular.copy(oldValue), - property_name: backendName + property_name: backendName, } as ExplorationChangeEditExplorationProperty); } @@ -295,8 +320,10 @@ export class ChangeListService { * @param {string} oldValue - The previous value of the property */ editStateProperty( - stateName: string, backendName: StatePropertyNames, - newValue: StatePropertyDictValues, oldValue: StatePropertyDictValues + stateName: string, + backendName: StatePropertyNames, + newValue: StatePropertyDictValues, + oldValue: StatePropertyDictValues ): void { if (!this.ALLOWED_STATE_BACKEND_NAMES.hasOwnProperty(backendName)) { this.alertsService.addWarning('Invalid state property: ' + backendName); @@ -307,7 +334,7 @@ export class ChangeListService { new_value: angular.copy(newValue), old_value: angular.copy(oldValue), property_name: backendName, - state_name: stateName + state_name: stateName, }); } @@ -316,13 +343,15 @@ export class ChangeListService { } getTranslationChangeList(): ExplorationChange[] { - return angular.copy(this.explorationChangeList.filter((change) => { - return [ - 'edit_translation', - 'remove_translations', - 'mark_translations_needs_update' - ].includes(change.cmd); - })); + return angular.copy( + this.explorationChangeList.filter(change => { + return [ + 'edit_translation', + 'remove_translations', + 'mark_translations_needs_update', + ].includes(change.cmd); + }) + ); } isExplorationLockedForEditing(): boolean { @@ -353,20 +382,24 @@ export class ChangeListService { this.addChange({ cmd: 'rename_state', new_state_name: newStateName, - old_state_name: oldStateName + old_state_name: oldStateName, }); } addWrittenTranslation( - contentId: string, dataFormat: string, languageCode: string, - stateName: string, translationHtml: string): void { - // Written translations submitted via the translation tab in the - // exploration editor need not pass content_html because - // translations submitted via this method do not undergo a review. The - // content_html is only required when submitting translations via - // the contributor dashboard because such translation suggestions - // undergo a manual review process where the reviewer will need to look - // at the corresponding original content at the time of submission. + contentId: string, + dataFormat: string, + languageCode: string, + stateName: string, + translationHtml: string + ): void { + // Written translations submitted via the translation tab in the + // exploration editor need not pass content_html because + // translations submitted via this method do not undergo a review. The + // content_html is only required when submitting translations via + // the contributor dashboard because such translation suggestions + // undergo a manual review process where the reviewer will need to look + // at the corresponding original content at the time of submission. this.addChange({ cmd: 'add_written_translation', content_id: contentId, @@ -374,7 +407,7 @@ export class ChangeListService { language_code: languageCode, state_name: stateName, content_html: 'N/A', - translation_html: translationHtml + translation_html: translationHtml, }); } @@ -387,7 +420,7 @@ export class ChangeListService { markTranslationsAsNeedingUpdate(contentId: string): void { this.addChange({ cmd: 'mark_translations_needs_update', - content_id: contentId + content_id: contentId, }); } @@ -395,15 +428,15 @@ export class ChangeListService { * Saves a change dict that represents editing translations. */ editTranslation( - contentId: string, - languageCode: string, - translatedContent: TranslatedContent, + contentId: string, + languageCode: string, + translatedContent: TranslatedContent ): void { this.addChange({ cmd: 'edit_translation', language_code: languageCode, content_id: contentId, - translation: translatedContent.toBackendDict() + translation: translatedContent.toBackendDict(), }); } @@ -416,7 +449,7 @@ export class ChangeListService { removeTranslations(contentId: string): void { this.addChange({ cmd: 'remove_translations', - content_id: contentId + content_id: contentId, }); } @@ -435,6 +468,6 @@ export class ChangeListService { } } -angular.module('oppia').factory( - 'ChangeListService', downgradeInjectable( - ChangeListService)); +angular + .module('oppia') + .factory('ChangeListService', downgradeInjectable(ChangeListService)); diff --git a/core/templates/pages/exploration-editor-page/services/editor-first-time-events.service.spec.ts b/core/templates/pages/exploration-editor-page/services/editor-first-time-events.service.spec.ts index a0fdf6d5ed2e..4bd0bae542a6 100644 --- a/core/templates/pages/exploration-editor-page/services/editor-first-time-events.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/editor-first-time-events.service.spec.ts @@ -15,11 +15,9 @@ /** * @fileoverview Unit tests for EditorFirstTimeEventsService. */ -import { TestBed } from '@angular/core/testing'; -import { EditorFirstTimeEventsService } from - 'pages/exploration-editor-page/services/editor-first-time-events.service'; -import { SiteAnalyticsService } from - 'services/site-analytics.service'; +import {TestBed} from '@angular/core/testing'; +import {EditorFirstTimeEventsService} from 'pages/exploration-editor-page/services/editor-first-time-events.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; describe('Editor First Time Events Service', () => { let eftes: EditorFirstTimeEventsService; @@ -36,240 +34,299 @@ describe('Editor First Time Events Service', () => { describe('registerEditorFirstEntryEvent', () => { it('should not call site analytics service if init is not called', () => { - const sasSpy = spyOn(sas, 'registerEditorFirstEntryEvent').and - .callThrough(); + const sasSpy = spyOn( + sas, + 'registerEditorFirstEntryEvent' + ).and.callThrough(); eftes.registerEditorFirstEntryEvent(); expect(sasSpy).not.toHaveBeenCalled(); }); - it('should call site analytics service when init was called before', + it('should call site analytics service when init was called before', () => { + const sasSpy = spyOn( + sas, + 'registerEditorFirstEntryEvent' + ).and.callThrough(); + eftes.initRegisterEvents('0'); + eftes.registerEditorFirstEntryEvent(); + expect(sasSpy).toHaveBeenCalledWith('0'); + }); + + it( + "should not call site analytics service if it's was already" + + ' called before', () => { - const sasSpy = spyOn(sas, 'registerEditorFirstEntryEvent').and - .callThrough(); eftes.initRegisterEvents('0'); eftes.registerEditorFirstEntryEvent(); - expect(sasSpy).toHaveBeenCalledWith('0'); - }); - it('should not call site analytics service if it\'s was already' + - ' called before', () => { - eftes.initRegisterEvents('0'); - eftes.registerEditorFirstEntryEvent(); - - const sasSpy = spyOn(sas, 'registerEditorFirstEntryEvent').and - .callThrough(); - eftes.registerEditorFirstEntryEvent(); - expect(sasSpy).not.toHaveBeenCalled(); - }); + const sasSpy = spyOn( + sas, + 'registerEditorFirstEntryEvent' + ).and.callThrough(); + eftes.registerEditorFirstEntryEvent(); + expect(sasSpy).not.toHaveBeenCalled(); + } + ); }); describe('registerFirstOpenContentBoxEvent', () => { it('should not call site analytics service if init is not called', () => { - const sasSpy = spyOn(sas, 'registerFirstOpenContentBoxEvent').and - .callThrough(); + const sasSpy = spyOn( + sas, + 'registerFirstOpenContentBoxEvent' + ).and.callThrough(); eftes.registerFirstOpenContentBoxEvent(); expect(sasSpy).not.toHaveBeenCalled(); }); - it('should call site analytics service when init was called before', + it('should call site analytics service when init was called before', () => { + const sasSpy = spyOn( + sas, + 'registerFirstOpenContentBoxEvent' + ).and.callThrough(); + eftes.initRegisterEvents('0'); + eftes.registerFirstOpenContentBoxEvent(); + expect(sasSpy).toHaveBeenCalledWith('0'); + }); + + it( + "should not call site analytics service if it's was already" + + ' called before', () => { - const sasSpy = spyOn(sas, 'registerFirstOpenContentBoxEvent').and - .callThrough(); eftes.initRegisterEvents('0'); eftes.registerFirstOpenContentBoxEvent(); - expect(sasSpy).toHaveBeenCalledWith('0'); - }); - it('should not call site analytics service if it\'s was already' + - ' called before', () => { - eftes.initRegisterEvents('0'); - eftes.registerFirstOpenContentBoxEvent(); - - const sasSpy = spyOn(sas, 'registerFirstOpenContentBoxEvent').and - .callThrough(); - eftes.registerFirstOpenContentBoxEvent(); - expect(sasSpy).not.toHaveBeenCalled(); - }); + const sasSpy = spyOn( + sas, + 'registerFirstOpenContentBoxEvent' + ).and.callThrough(); + eftes.registerFirstOpenContentBoxEvent(); + expect(sasSpy).not.toHaveBeenCalled(); + } + ); }); describe('registerFirstSaveContentEvent', () => { it('should not call site analytics service if init is not called', () => { - const sasSpy = spyOn(sas, 'registerFirstSaveContentEvent').and - .callThrough(); + const sasSpy = spyOn( + sas, + 'registerFirstSaveContentEvent' + ).and.callThrough(); eftes.registerFirstSaveContentEvent(); expect(sasSpy).not.toHaveBeenCalled(); }); - it('should call site analytics service when init was called before', + it('should call site analytics service when init was called before', () => { + const sasSpy = spyOn( + sas, + 'registerFirstSaveContentEvent' + ).and.callThrough(); + eftes.initRegisterEvents('0'); + eftes.registerFirstSaveContentEvent(); + expect(sasSpy).toHaveBeenCalledWith('0'); + }); + + it( + "should not call site analytics service if it's was already" + + ' called before', () => { - const sasSpy = spyOn(sas, 'registerFirstSaveContentEvent').and - .callThrough(); eftes.initRegisterEvents('0'); eftes.registerFirstSaveContentEvent(); - expect(sasSpy).toHaveBeenCalledWith('0'); - }); - it('should not call site analytics service if it\'s was already' + - ' called before', () => { - eftes.initRegisterEvents('0'); - eftes.registerFirstSaveContentEvent(); - - const sasSpy = spyOn(sas, 'registerFirstSaveContentEvent').and - .callThrough(); - eftes.registerFirstSaveContentEvent(); - expect(sasSpy).not.toHaveBeenCalled(); - }); + const sasSpy = spyOn( + sas, + 'registerFirstSaveContentEvent' + ).and.callThrough(); + eftes.registerFirstSaveContentEvent(); + expect(sasSpy).not.toHaveBeenCalled(); + } + ); }); describe('registerFirstClickAddInteractionEvent', () => { it('should not call site analytics service if init is not called', () => { - const sasSpy = spyOn(sas, 'registerFirstClickAddInteractionEvent').and - .callThrough(); + const sasSpy = spyOn( + sas, + 'registerFirstClickAddInteractionEvent' + ).and.callThrough(); eftes.registerFirstClickAddInteractionEvent(); expect(sasSpy).not.toHaveBeenCalled(); }); - it('should call site analytics service when init was called before', + it('should call site analytics service when init was called before', () => { + const sasSpy = spyOn( + sas, + 'registerFirstClickAddInteractionEvent' + ).and.callThrough(); + eftes.initRegisterEvents('0'); + eftes.registerFirstClickAddInteractionEvent(); + expect(sasSpy).toHaveBeenCalledWith('0'); + }); + + it( + "should not call site analytics service if it's was already" + + ' called before', () => { - const sasSpy = spyOn(sas, 'registerFirstClickAddInteractionEvent').and - .callThrough(); eftes.initRegisterEvents('0'); eftes.registerFirstClickAddInteractionEvent(); - expect(sasSpy).toHaveBeenCalledWith('0'); - }); - - it('should not call site analytics service if it\'s was already' + - ' called before', () => { - eftes.initRegisterEvents('0'); - eftes.registerFirstClickAddInteractionEvent(); - const sasSpy = spyOn(sas, 'registerFirstClickAddInteractionEvent').and - .callThrough(); - eftes.registerFirstClickAddInteractionEvent(); - expect(sasSpy).not.toHaveBeenCalled(); - }); + const sasSpy = spyOn( + sas, + 'registerFirstClickAddInteractionEvent' + ).and.callThrough(); + eftes.registerFirstClickAddInteractionEvent(); + expect(sasSpy).not.toHaveBeenCalled(); + } + ); }); describe('registerFirstSelectInteractionTypeEvent', () => { it('should not call site analytics service if init is not called', () => { - const sasSpy = spyOn(sas, 'registerFirstSelectInteractionTypeEvent').and - .callThrough(); + const sasSpy = spyOn( + sas, + 'registerFirstSelectInteractionTypeEvent' + ).and.callThrough(); eftes.registerFirstSelectInteractionTypeEvent(); expect(sasSpy).not.toHaveBeenCalled(); }); it('should call site analytics service when init was called before', () => { - const sasSpy = spyOn(sas, 'registerFirstSelectInteractionTypeEvent').and - .callThrough(); + const sasSpy = spyOn( + sas, + 'registerFirstSelectInteractionTypeEvent' + ).and.callThrough(); eftes.initRegisterEvents('0'); eftes.registerFirstSelectInteractionTypeEvent(); expect(sasSpy).toHaveBeenCalledWith('0'); }); - it('should not call site analytics service if it\'s was already' + - ' called before', () => { - eftes.initRegisterEvents('0'); - eftes.registerFirstSelectInteractionTypeEvent(); - - const sasSpy = spyOn(sas, 'registerFirstSelectInteractionTypeEvent').and - .callThrough(); - eftes.registerFirstSelectInteractionTypeEvent(); - expect(sasSpy).not.toHaveBeenCalled(); - }); + it( + "should not call site analytics service if it's was already" + + ' called before', + () => { + eftes.initRegisterEvents('0'); + eftes.registerFirstSelectInteractionTypeEvent(); + + const sasSpy = spyOn( + sas, + 'registerFirstSelectInteractionTypeEvent' + ).and.callThrough(); + eftes.registerFirstSelectInteractionTypeEvent(); + expect(sasSpy).not.toHaveBeenCalled(); + } + ); }); describe('registerFirstSaveInteractionEvent', () => { it('should not call site analytics service if init is not called', () => { - const sasSpy = spyOn(sas, 'registerFirstSaveInteractionEvent').and - .callThrough(); + const sasSpy = spyOn( + sas, + 'registerFirstSaveInteractionEvent' + ).and.callThrough(); eftes.registerFirstSaveInteractionEvent(); expect(sasSpy).not.toHaveBeenCalled(); }); - it('should call site analytics service when init was called before', + it('should call site analytics service when init was called before', () => { + const sasSpy = spyOn( + sas, + 'registerFirstSaveInteractionEvent' + ).and.callThrough(); + eftes.initRegisterEvents('0'); + eftes.registerFirstSaveInteractionEvent(); + expect(sasSpy).toHaveBeenCalledWith('0'); + }); + + it( + "should not call site analytics service if it's was already" + + ' called before', () => { - const sasSpy = spyOn(sas, 'registerFirstSaveInteractionEvent').and - .callThrough(); eftes.initRegisterEvents('0'); eftes.registerFirstSaveInteractionEvent(); - expect(sasSpy).toHaveBeenCalledWith('0'); - }); - - it('should not call site analytics service if it\'s was already' + - ' called before', () => { - eftes.initRegisterEvents('0'); - eftes.registerFirstSaveInteractionEvent(); - const sasSpy = spyOn(sas, 'registerFirstSaveInteractionEvent').and - .callThrough(); - eftes.registerFirstSaveInteractionEvent(); - expect(sasSpy).not.toHaveBeenCalled(); - }); + const sasSpy = spyOn( + sas, + 'registerFirstSaveInteractionEvent' + ).and.callThrough(); + eftes.registerFirstSaveInteractionEvent(); + expect(sasSpy).not.toHaveBeenCalled(); + } + ); }); describe('registerFirstSaveRuleEvent', () => { it('should not call site analytics service if init is not called', () => { - const sasSpy = spyOn(sas, 'registerFirstSaveRuleEvent').and - .callThrough(); + const sasSpy = spyOn(sas, 'registerFirstSaveRuleEvent').and.callThrough(); eftes.registerFirstSaveRuleEvent(); expect(sasSpy).not.toHaveBeenCalled(); }); - it('should call site analytics service when init was called before', + it('should call site analytics service when init was called before', () => { + const sasSpy = spyOn(sas, 'registerFirstSaveRuleEvent').and.callThrough(); + eftes.initRegisterEvents('0'); + eftes.registerFirstSaveRuleEvent(); + expect(sasSpy).toHaveBeenCalledWith('0'); + }); + + it( + "should not call site analytics service if it's was already" + + ' called before', () => { - const sasSpy = spyOn(sas, 'registerFirstSaveRuleEvent').and - .callThrough(); eftes.initRegisterEvents('0'); eftes.registerFirstSaveRuleEvent(); - expect(sasSpy).toHaveBeenCalledWith('0'); - }); - - it('should not call site analytics service if it\'s was already' + - ' called before', () => { - eftes.initRegisterEvents('0'); - eftes.registerFirstSaveRuleEvent(); - const sasSpy = spyOn(sas, 'registerFirstSaveRuleEvent').and - .callThrough(); - eftes.registerFirstSaveRuleEvent(); - expect(sasSpy).not.toHaveBeenCalled(); - }); + const sasSpy = spyOn( + sas, + 'registerFirstSaveRuleEvent' + ).and.callThrough(); + eftes.registerFirstSaveRuleEvent(); + expect(sasSpy).not.toHaveBeenCalled(); + } + ); }); describe('registerFirstCreateSecondStateEvent', () => { it('should not call site analytics service if init is not called', () => { - const sasSpy = spyOn(sas, 'registerFirstCreateSecondStateEvent').and - .callThrough(); + const sasSpy = spyOn( + sas, + 'registerFirstCreateSecondStateEvent' + ).and.callThrough(); eftes.registerFirstCreateSecondStateEvent(); expect(sasSpy).not.toHaveBeenCalled(); }); - it('should call site analytics service when init was called before', + it('should call site analytics service when init was called before', () => { + const sasSpy = spyOn( + sas, + 'registerFirstCreateSecondStateEvent' + ).and.callThrough(); + eftes.initRegisterEvents('0'); + eftes.registerFirstCreateSecondStateEvent(); + expect(sasSpy).toHaveBeenCalledWith('0'); + }); + + it( + "should not call site analytics service if it's was already" + + ' called before', () => { - const sasSpy = spyOn(sas, 'registerFirstCreateSecondStateEvent').and - .callThrough(); eftes.initRegisterEvents('0'); eftes.registerFirstCreateSecondStateEvent(); - expect(sasSpy).toHaveBeenCalledWith('0'); - }); - - it('should not call site analytics service if it\'s was already' + - ' called before', () => { - eftes.initRegisterEvents('0'); - eftes.registerFirstCreateSecondStateEvent(); - const sasSpy = spyOn(sas, 'registerFirstCreateSecondStateEvent').and - .callThrough(); - eftes.registerFirstCreateSecondStateEvent(); - expect(sasSpy).not.toHaveBeenCalled(); - }); + const sasSpy = spyOn( + sas, + 'registerFirstCreateSecondStateEvent' + ).and.callThrough(); + eftes.registerFirstCreateSecondStateEvent(); + expect(sasSpy).not.toHaveBeenCalled(); + } + ); }); }); diff --git a/core/templates/pages/exploration-editor-page/services/editor-first-time-events.service.ts b/core/templates/pages/exploration-editor-page/services/editor-first-time-events.service.ts index 14f78c3ce502..ffdf161ae260 100644 --- a/core/templates/pages/exploration-editor-page/services/editor-first-time-events.service.ts +++ b/core/templates/pages/exploration-editor-page/services/editor-first-time-events.service.ts @@ -18,13 +18,13 @@ * is opened for the first time for an exploration. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class EditorFirstTimeEventsService { constructor(private siteAnalyticsService: SiteAnalyticsService) {} @@ -40,7 +40,7 @@ export class EditorFirstTimeEventsService { FirstSelectInteractionTypeEvent: false, FirstSaveInteractionEvent: false, FirstSaveRuleEvent: false, - FirstCreateSecondStateEvent: false + FirstCreateSecondStateEvent: false, }; initRegisterEvents(expId: string): void { @@ -49,77 +49,103 @@ export class EditorFirstTimeEventsService { } registerEditorFirstEntryEvent(): void { - if (this.shouldRegisterEvents && - !this.alreadyRegisteredEvents.EditorFirstEntryEvent) { + if ( + this.shouldRegisterEvents && + !this.alreadyRegisteredEvents.EditorFirstEntryEvent + ) { this.siteAnalyticsService.registerEditorFirstEntryEvent( - this.explorationId); + this.explorationId + ); this.alreadyRegisteredEvents.EditorFirstEntryEvent = true; } } registerFirstOpenContentBoxEvent(): void { - if (this.shouldRegisterEvents && - !this.alreadyRegisteredEvents.FirstOpenContentBoxEvent) { + if ( + this.shouldRegisterEvents && + !this.alreadyRegisteredEvents.FirstOpenContentBoxEvent + ) { this.siteAnalyticsService.registerFirstOpenContentBoxEvent( - this.explorationId); + this.explorationId + ); this.alreadyRegisteredEvents.FirstOpenContentBoxEvent = true; } } registerFirstSaveContentEvent(): void { - if (this.shouldRegisterEvents && - !this.alreadyRegisteredEvents.FirstSaveContentEvent) { + if ( + this.shouldRegisterEvents && + !this.alreadyRegisteredEvents.FirstSaveContentEvent + ) { this.siteAnalyticsService.registerFirstSaveContentEvent( - this.explorationId); + this.explorationId + ); this.alreadyRegisteredEvents.FirstSaveContentEvent = true; } } registerFirstClickAddInteractionEvent(): void { - if (this.shouldRegisterEvents && - !this.alreadyRegisteredEvents.FirstClickAddInteractionEvent) { + if ( + this.shouldRegisterEvents && + !this.alreadyRegisteredEvents.FirstClickAddInteractionEvent + ) { this.siteAnalyticsService.registerFirstClickAddInteractionEvent( - this.explorationId); + this.explorationId + ); this.alreadyRegisteredEvents.FirstClickAddInteractionEvent = true; } } registerFirstSelectInteractionTypeEvent(): void { - if (this.shouldRegisterEvents && - !this.alreadyRegisteredEvents.FirstSelectInteractionTypeEvent) { + if ( + this.shouldRegisterEvents && + !this.alreadyRegisteredEvents.FirstSelectInteractionTypeEvent + ) { this.siteAnalyticsService.registerFirstSelectInteractionTypeEvent( - this.explorationId); + this.explorationId + ); this.alreadyRegisteredEvents.FirstSelectInteractionTypeEvent = true; } } registerFirstSaveInteractionEvent(): void { - if (this.shouldRegisterEvents && - !this.alreadyRegisteredEvents.FirstSaveInteractionEvent) { + if ( + this.shouldRegisterEvents && + !this.alreadyRegisteredEvents.FirstSaveInteractionEvent + ) { this.siteAnalyticsService.registerFirstSaveInteractionEvent( - this.explorationId); + this.explorationId + ); this.alreadyRegisteredEvents.FirstSaveInteractionEvent = true; } } registerFirstSaveRuleEvent(): void { - if (this.shouldRegisterEvents && - !this.alreadyRegisteredEvents.FirstSaveRuleEvent) { + if ( + this.shouldRegisterEvents && + !this.alreadyRegisteredEvents.FirstSaveRuleEvent + ) { this.siteAnalyticsService.registerFirstSaveRuleEvent(this.explorationId); this.alreadyRegisteredEvents.FirstSaveRuleEvent = true; } } registerFirstCreateSecondStateEvent(): void { - if (this.shouldRegisterEvents && - !this.alreadyRegisteredEvents.FirstCreateSecondStateEvent) { + if ( + this.shouldRegisterEvents && + !this.alreadyRegisteredEvents.FirstCreateSecondStateEvent + ) { this.siteAnalyticsService.registerFirstCreateSecondStateEvent( - this.explorationId); + this.explorationId + ); this.alreadyRegisteredEvents.FirstCreateSecondStateEvent = true; } } } -angular.module('oppia').factory( - 'EditorFirstTimeEventsService', - downgradeInjectable(EditorFirstTimeEventsService)); +angular + .module('oppia') + .factory( + 'EditorFirstTimeEventsService', + downgradeInjectable(EditorFirstTimeEventsService) + ); diff --git a/core/templates/pages/exploration-editor-page/services/entity-translation-backend-api.service.spec.ts b/core/templates/pages/exploration-editor-page/services/entity-translation-backend-api.service.spec.ts index 1d586c86c8c6..80756a5ff937 100644 --- a/core/templates/pages/exploration-editor-page/services/entity-translation-backend-api.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/entity-translation-backend-api.service.spec.ts @@ -16,10 +16,12 @@ * @fileoverview Unit tests for EntityTranslationBackendApiService. */ -import { HttpClientTestingModule, HttpTestingController } from - '@angular/common/http/testing'; -import { fakeAsync, flushMicrotasks, TestBed, tick } from '@angular/core/testing'; -import { EntityTranslationBackendApiService } from './entity-translation-backend-api.service'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {fakeAsync, flushMicrotasks, TestBed, tick} from '@angular/core/testing'; +import {EntityTranslationBackendApiService} from './entity-translation-backend-api.service'; describe('Entity Translation Backend Api Service', () => { let translationApiService: EntityTranslationBackendApiService; @@ -29,7 +31,7 @@ describe('Entity Translation Backend Api Service', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] + imports: [HttpClientTestingModule], }); httpTestingController = TestBed.inject(HttpTestingController); translationApiService = TestBed.inject(EntityTranslationBackendApiService); @@ -44,27 +46,37 @@ describe('Entity Translation Backend Api Service', () => { let entityType: string = 'exploration'; let entityVersion: number = 5; let languageCode: string = 'hi'; - translationApiService.fetchEntityTranslationAsync( - entityId, entityType, entityVersion, languageCode - ).then(successHandler, failHandler); + translationApiService + .fetchEntityTranslationAsync( + entityId, + entityType, + entityVersion, + languageCode + ) + .then(successHandler, failHandler); let req = httpTestingController.expectOne( - '/entity_translations_handler/exploration/entity1/5/hi'); + '/entity_translations_handler/exploration/entity1/5/hi' + ); expect(req.request.method).toEqual('GET'); - req.flush({ - entity_id: 'entity1', - entity_type: 'exploration', - entity_version: 5, - language_code: 'hi', - translations: { - feedback_3: { - content_format: 'html', - content_value: '

This is feedback 1.

', - needs_update: false - } + req.flush( + { + entity_id: 'entity1', + entity_type: 'exploration', + entity_version: 5, + language_code: 'hi', + translations: { + feedback_3: { + content_format: 'html', + content_value: '

This is feedback 1.

', + needs_update: false, + }, + }, + }, + { + status: 200, + statusText: 'Success.', } - }, { - status: 200, statusText: 'Success.' - }); + ); tick(); flushMicrotasks(); @@ -76,17 +88,27 @@ describe('Entity Translation Backend Api Service', () => { let entityType: string = 'exploration'; let entityVersion: number = 5; let languageCode: string = 'hi'; - translationApiService.fetchEntityTranslationAsync( - entityId, entityType, entityVersion, languageCode - ).then(successHandler, failHandler); + translationApiService + .fetchEntityTranslationAsync( + entityId, + entityType, + entityVersion, + languageCode + ) + .then(successHandler, failHandler); let req = httpTestingController.expectOne( - '/entity_translations_handler/exploration/entity1/5/hi'); + '/entity_translations_handler/exploration/entity1/5/hi' + ); expect(req.request.method).toEqual('GET'); - req.flush({ - error: 'Some error in the backend.' - }, { - status: 500, statusText: 'Internal Server Error' - }); + req.flush( + { + error: 'Some error in the backend.', + }, + { + status: 500, + statusText: 'Internal Server Error', + } + ); flushMicrotasks(); diff --git a/core/templates/pages/exploration-editor-page/services/entity-translation-backend-api.service.ts b/core/templates/pages/exploration-editor-page/services/entity-translation-backend-api.service.ts index 7a272b4c7299..72c0dd65b4f3 100644 --- a/core/templates/pages/exploration-editor-page/services/entity-translation-backend-api.service.ts +++ b/core/templates/pages/exploration-editor-page/services/entity-translation-backend-api.service.ts @@ -16,55 +16,69 @@ * @fileoverview Service to fetch entity-translation from the backend. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { EntityTranslation, EntityTranslationBackendDict } from 'domain/translation/EntityTranslationObjectFactory'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { AppConstants } from 'app.constants'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import { + EntityTranslation, + EntityTranslationBackendDict, +} from 'domain/translation/EntityTranslationObjectFactory'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {AppConstants} from 'app.constants'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class EntityTranslationBackendApiService { constructor( - private httpClient: HttpClient, - private urlInterpolationService: UrlInterpolationService, + private httpClient: HttpClient, + private urlInterpolationService: UrlInterpolationService ) {} private _getUrl( - entityId: string, entityType: string, entityVersion: number, - languageCode: string + entityId: string, + entityType: string, + entityVersion: number, + languageCode: string ) { return this.urlInterpolationService.interpolateUrl( - AppConstants.ENTITY_TRANSLATIONS_HANDLER_URL_TEMPLATE, { + AppConstants.ENTITY_TRANSLATIONS_HANDLER_URL_TEMPLATE, + { entity_id: entityId, entity_type: entityType, entity_version: String(entityVersion), - language_code: languageCode + language_code: languageCode, } ); } async fetchEntityTranslationAsync( - entityId: string, - entityType: string, - entityVersion: number, - languageCode: string): Promise { + entityId: string, + entityType: string, + entityVersion: number, + languageCode: string + ): Promise { return new Promise((resolve, reject) => { - this.httpClient.get( - this._getUrl( - entityId, entityType, entityVersion, languageCode - )).toPromise().then(response => { - resolve(EntityTranslation.createFromBackendDict(response)); - }, errorResponse => { - reject(errorResponse.error.error); - }); + this.httpClient + .get( + this._getUrl(entityId, entityType, entityVersion, languageCode) + ) + .toPromise() + .then( + response => { + resolve(EntityTranslation.createFromBackendDict(response)); + }, + errorResponse => { + reject(errorResponse.error.error); + } + ); }); } } - -angular.module('oppia').factory( - 'EntityTranslationBackendApiService', - downgradeInjectable(EntityTranslationBackendApiService)); +angular + .module('oppia') + .factory( + 'EntityTranslationBackendApiService', + downgradeInjectable(EntityTranslationBackendApiService) + ); diff --git a/core/templates/pages/exploration-editor-page/services/exploration-automatic-text-to-speech.service.ts b/core/templates/pages/exploration-editor-page/services/exploration-automatic-text-to-speech.service.ts index 666b42c78404..b7e25df68629 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-automatic-text-to-speech.service.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-automatic-text-to-speech.service.ts @@ -17,35 +17,34 @@ * text to speech data. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { ExplorationPropertyService } from 'pages/exploration-editor-page/services/exploration-property.service'; -import { AlertsService } from 'services/alerts.service'; -import { LoggerService } from 'services/contextual/logger.service'; -import { ChangeListService } from './change-list.service'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {ExplorationPropertyService} from 'pages/exploration-editor-page/services/exploration-property.service'; +import {AlertsService} from 'services/alerts.service'; +import {LoggerService} from 'services/contextual/logger.service'; +import {ChangeListService} from './change-list.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) -export class ExplorationAutomaticTextToSpeechService - extends ExplorationPropertyService { +export class ExplorationAutomaticTextToSpeechService extends ExplorationPropertyService { propertyName: string = 'auto_tts_enabled'; constructor( protected alertsService: AlertsService, protected changeListService: ChangeListService, - protected loggerService: LoggerService, + protected loggerService: LoggerService ) { super(alertsService, changeListService, loggerService); } // Type unknown is used here to check validity of the input. _isValid(value: unknown): boolean { - return (typeof value === 'boolean'); + return typeof value === 'boolean'; } isAutomaticTextToSpeechEnabled(): boolean { - return (this.savedMemento as boolean); + return this.savedMemento as boolean; } toggleAutomaticTextToSpeech(): void { @@ -54,7 +53,9 @@ export class ExplorationAutomaticTextToSpeechService } } -angular.module('oppia').factory( - 'ExplorationAutomaticTextToSpeechService', - downgradeInjectable( - ExplorationAutomaticTextToSpeechService)); +angular + .module('oppia') + .factory( + 'ExplorationAutomaticTextToSpeechService', + downgradeInjectable(ExplorationAutomaticTextToSpeechService) + ); diff --git a/core/templates/pages/exploration-editor-page/services/exploration-category.service.spec.ts b/core/templates/pages/exploration-editor-page/services/exploration-category.service.spec.ts index ffd3cc6c189e..133a702fd667 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-category.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-category.service.spec.ts @@ -16,13 +16,13 @@ * @fileoverview Unit tests for the ExplorationCategoryService. */ -import { TestBed } from '@angular/core/testing'; -import { ExplorationCategoryService } from './exploration-category.service'; -import { ExplorationPropertyService } from './exploration-property.service'; -import { ExplorationRightsService } from './exploration-rights.service'; -import { ValidatorsService } from 'services/validators.service'; -import { NormalizeWhitespacePipe } from 'filters/string-utility-filters/normalize-whitespace.pipe'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; +import {TestBed} from '@angular/core/testing'; +import {ExplorationCategoryService} from './exploration-category.service'; +import {ExplorationPropertyService} from './exploration-property.service'; +import {ExplorationRightsService} from './exploration-rights.service'; +import {ValidatorsService} from 'services/validators.service'; +import {NormalizeWhitespacePipe} from 'filters/string-utility-filters/normalize-whitespace.pipe'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; describe('Exploration Category Service', () => { let service: ExplorationCategoryService; @@ -34,8 +34,8 @@ describe('Exploration Category Service', () => { ExplorationRightsService, ValidatorsService, NormalizeWhitespacePipe, - ExplorationPropertyService - ] + ExplorationPropertyService, + ], }); service = TestBed.inject(ExplorationCategoryService); diff --git a/core/templates/pages/exploration-editor-page/services/exploration-category.service.ts b/core/templates/pages/exploration-editor-page/services/exploration-category.service.ts index 4996d4646f30..a4386e91a6a2 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-category.service.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-category.service.ts @@ -17,18 +17,18 @@ * that it can be displayed and edited in multiple places in the UI. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { ExplorationPropertyService } from './exploration-property.service'; -import { ExplorationRightsService } from './exploration-rights.service'; -import { ValidatorsService } from 'services/validators.service'; -import { NormalizeWhitespacePipe } from 'filters/string-utility-filters/normalize-whitespace.pipe'; -import { AlertsService } from 'services/alerts.service'; -import { ChangeListService } from './change-list.service'; -import { LoggerService } from 'services/contextual/logger.service'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {ExplorationPropertyService} from './exploration-property.service'; +import {ExplorationRightsService} from './exploration-rights.service'; +import {ValidatorsService} from 'services/validators.service'; +import {NormalizeWhitespacePipe} from 'filters/string-utility-filters/normalize-whitespace.pipe'; +import {AlertsService} from 'services/alerts.service'; +import {ChangeListService} from './change-list.service'; +import {LoggerService} from 'services/contextual/logger.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ExplorationCategoryService extends ExplorationPropertyService { constructor( @@ -37,7 +37,7 @@ export class ExplorationCategoryService extends ExplorationPropertyService { private whitespaceNormalize: NormalizeWhitespacePipe, protected alertsService: AlertsService, protected changeListService: ChangeListService, - protected loggerService: LoggerService, + protected loggerService: LoggerService ) { super(alertsService, changeListService, loggerService); } @@ -50,9 +50,16 @@ export class ExplorationCategoryService extends ExplorationPropertyService { _isValid(value: string): boolean { return this.validatorsService.isValidEntityName( - value, true, this.explorationRightsService.isPrivate()); + value, + true, + this.explorationRightsService.isPrivate() + ); } } -angular.module('oppia').factory('ExplorationCategoryService', - downgradeInjectable(ExplorationCategoryService)); +angular + .module('oppia') + .factory( + 'ExplorationCategoryService', + downgradeInjectable(ExplorationCategoryService) + ); diff --git a/core/templates/pages/exploration-editor-page/services/exploration-data-backend-api.service.spec.ts b/core/templates/pages/exploration-editor-page/services/exploration-data-backend-api.service.spec.ts index 7f077ae15480..e5e3eeb90bf6 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-data-backend-api.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-data-backend-api.service.spec.ts @@ -16,9 +16,12 @@ * @fileoverview Unit tests for the Exploration data backend api service. */ -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { fakeAsync, flushMicrotasks, TestBed } from '@angular/core/testing'; -import { ExplorationDataBackendApiService } from './exploration-data-backend-api.service'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {fakeAsync, flushMicrotasks, TestBed} from '@angular/core/testing'; +import {ExplorationDataBackendApiService} from './exploration-data-backend-api.service'; describe('Exploration data backend api service', () => { let edbas: ExplorationDataBackendApiService; @@ -26,33 +29,31 @@ describe('Exploration data backend api service', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] + imports: [HttpClientTestingModule], }); edbas = TestBed.inject(ExplorationDataBackendApiService); - httpTestingController = TestBed.inject( - HttpTestingController - ); + httpTestingController = TestBed.inject(HttpTestingController); }); it('should send a post request to discardDraft', fakeAsync(() => { - edbas.discardDraft( - '/createhandler/autosave_draft/0').subscribe( - async(res) => expect(res).toBe(1) - ); + edbas + .discardDraft('/createhandler/autosave_draft/0') + .subscribe(async res => expect(res).toBe(1)); const req = httpTestingController.expectOne( - '/createhandler/autosave_draft/0'); + '/createhandler/autosave_draft/0' + ); expect(req.request.method).toBe('POST'); req.flush(1); flushMicrotasks(); })); it('should send a put update exploration', fakeAsync(() => { - edbas.saveChangeList( - '/createhandler/autosave_draft/0', [], 1).subscribe( - async(res) => expect(res).toBe(1) - ); + edbas + .saveChangeList('/createhandler/autosave_draft/0', [], 1) + .subscribe(async res => expect(res).toBe(1)); const req = httpTestingController.expectOne( - '/createhandler/autosave_draft/0'); + '/createhandler/autosave_draft/0' + ); expect(req.request.method).toBe('PUT'); req.flush(1); flushMicrotasks(); diff --git a/core/templates/pages/exploration-editor-page/services/exploration-data-backend-api.service.ts b/core/templates/pages/exploration-editor-page/services/exploration-data-backend-api.service.ts index e064cb183c66..386e0ca1795c 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-data-backend-api.service.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-data-backend-api.service.ts @@ -12,40 +12,42 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Service for handling all http calls * with the exploration editor backend. */ -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { ExplorationChange } from 'domain/exploration/exploration-draft.model'; -import { Observable } from 'rxjs'; -import { DraftAutoSaveResponse } from './exploration-data.service'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {ExplorationChange} from 'domain/exploration/exploration-draft.model'; +import {Observable} from 'rxjs'; +import {DraftAutoSaveResponse} from './exploration-data.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ExplorationDataBackendApiService { constructor(private httpClient: HttpClient) {} discardDraft(url: string): Observable { - return this.httpClient.post( - url, {}); + return this.httpClient.post(url, {}); } saveChangeList( - url: string, - changeList: ExplorationChange[], - version: number): Observable { - return this.httpClient.put( - url, { - change_list: changeList, - version: version - }); + url: string, + changeList: ExplorationChange[], + version: number + ): Observable { + return this.httpClient.put(url, { + change_list: changeList, + version: version, + }); } } -angular.module('oppia').factory('ExplorationDataBackendApiService', - downgradeInjectable(ExplorationDataBackendApiService)); +angular + .module('oppia') + .factory( + 'ExplorationDataBackendApiService', + downgradeInjectable(ExplorationDataBackendApiService) + ); diff --git a/core/templates/pages/exploration-editor-page/services/exploration-data.service.spec.ts b/core/templates/pages/exploration-editor-page/services/exploration-data.service.spec.ts index 75a33e8a7d0c..2ea74186cc98 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-data.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-data.service.spec.ts @@ -16,20 +16,26 @@ * @fileoverview Unit tests for the Exploration data service. */ -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; -import { EditableExplorationBackendApiService } from 'domain/exploration/editable-exploration-backend-api.service'; -import { ExplorationDataService } from './exploration-data.service'; -import { LocalStorageService } from 'services/local-storage.service'; -import { LoggerService } from 'services/contextual/logger.service'; -import { CsrfTokenService } from 'services/csrf-token.service'; -import { UrlService } from 'services/contextual/url.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { ExplorationChange, ExplorationDraft } from 'domain/exploration/exploration-draft.model'; -import { ExplorationBackendDict } from 'domain/exploration/ExplorationObjectFactory'; -import { FetchExplorationBackendResponse } from 'domain/exploration/read-only-exploration-backend-api.service'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; +import {EditableExplorationBackendApiService} from 'domain/exploration/editable-exploration-backend-api.service'; +import {ExplorationDataService} from './exploration-data.service'; +import {LocalStorageService} from 'services/local-storage.service'; +import {LoggerService} from 'services/contextual/logger.service'; +import {CsrfTokenService} from 'services/csrf-token.service'; +import {UrlService} from 'services/contextual/url.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import { + ExplorationChange, + ExplorationDraft, +} from 'domain/exploration/exploration-draft.model'; +import {ExplorationBackendDict} from 'domain/exploration/ExplorationObjectFactory'; +import {FetchExplorationBackendResponse} from 'domain/exploration/read-only-exploration-backend-api.service'; -describe('Exploration data service', function() { +describe('Exploration data service', function () { let eds: ExplorationDataService; let eebas: EditableExplorationBackendApiService; let lss: LocalStorageService; @@ -62,8 +68,8 @@ describe('Exploration data service', function() { param_specs: {}, param_changes: [], auto_tts_enabled: false, - edits_allowed: true - } + edits_allowed: true, + }, }; let sampleExploration: FetchExplorationBackendResponse = { can_edit: true, @@ -74,7 +80,7 @@ describe('Exploration data service', function() { param_changes: [], content: { html: '', - audio_translations: {} + audio_translations: {}, }, unresolved_answers: {}, interaction: { @@ -82,10 +88,10 @@ describe('Exploration data service', function() { answer_groups: [], default_outcome: {}, confirmed_unclassified_answers: [], - id: null - } - } - } + id: null, + }, + }, + }, }, exploration_id: '1', is_logged_in: true, @@ -120,9 +126,9 @@ describe('Exploration data service', function() { const windowMock = { nativeWindow: { location: { - reload: function() {} - } - } + reload: function () {}, + }, + }, }; beforeEach(() => { TestBed.configureTestingModule({ @@ -130,14 +136,14 @@ describe('Exploration data service', function() { providers: [ { provide: UrlService, - useValue: {getPathname: () => '/create/0'} + useValue: {getPathname: () => '/create/0'}, }, { provide: EditableExplorationBackendApiService, - useClass: MockEditableExplorationBackendApiService + useClass: MockEditableExplorationBackendApiService, }, - {provide: WindowRef, useValue: windowMock } - ] + {provide: WindowRef, useValue: windowMock}, + ], }); }); beforeEach(() => { @@ -147,87 +153,84 @@ describe('Exploration data service', function() { eebas = TestBed.inject(EditableExplorationBackendApiService); csrfService = TestBed.inject(CsrfTokenService); httpTestingController = TestBed.inject(HttpTestingController); - spyOn(csrfService, 'getTokenAsync').and.callFake(async() => { + spyOn(csrfService, 'getTokenAsync').and.callFake(async () => { return Promise.resolve('sample-csrf-token'); }); }); - afterEach(function() { + afterEach(function () { httpTestingController.verify(); }); - it('should trigger success handler when auto saved successfully', fakeAsync( - () => { - eds.data = sampleDataResults; - const errorCallback = jasmine.createSpy('error'); - const successCallback = jasmine.createSpy('success'); - eds.autosaveChangeListAsync([], successCallback, errorCallback); - const req = httpTestingController.expectOne( - '/createhandler/autosave_draft/0'); - expect(req.request.method).toBe('PUT'); - req.flush(sampleDataResults); - flushMicrotasks(); - expect(successCallback).toHaveBeenCalledWith(sampleDataResults); - expect(errorCallback).not.toHaveBeenCalled(); - } - )); + it('should trigger success handler when auto saved successfully', fakeAsync(() => { + eds.data = sampleDataResults; + const errorCallback = jasmine.createSpy('error'); + const successCallback = jasmine.createSpy('success'); + eds.autosaveChangeListAsync([], successCallback, errorCallback); + const req = httpTestingController.expectOne( + '/createhandler/autosave_draft/0' + ); + expect(req.request.method).toBe('PUT'); + req.flush(sampleDataResults); + flushMicrotasks(); + expect(successCallback).toHaveBeenCalledWith(sampleDataResults); + expect(errorCallback).not.toHaveBeenCalled(); + })); - it('should trigger errorcallback handler when auto save fails', fakeAsync( - () => { - eds.data = sampleDataResults; - const errorCallback = jasmine.createSpy('error'); - const successCallback = jasmine.createSpy('success'); - eds.autosaveChangeListAsync([], successCallback, errorCallback); - const req = httpTestingController.expectOne( - '/createhandler/autosave_draft/0'); - expect(req.request.method).toBe('PUT'); - req.error(new ErrorEvent('Server error')); - flushMicrotasks(); - expect(successCallback).not.toHaveBeenCalled(); - expect(errorCallback).toHaveBeenCalled(); - } - )); + it('should trigger errorcallback handler when auto save fails', fakeAsync(() => { + eds.data = sampleDataResults; + const errorCallback = jasmine.createSpy('error'); + const successCallback = jasmine.createSpy('success'); + eds.autosaveChangeListAsync([], successCallback, errorCallback); + const req = httpTestingController.expectOne( + '/createhandler/autosave_draft/0' + ); + expect(req.request.method).toBe('PUT'); + req.error(new ErrorEvent('Server error')); + flushMicrotasks(); + expect(successCallback).not.toHaveBeenCalled(); + expect(errorCallback).toHaveBeenCalled(); + })); - it('should trigger errorcallback handler when version number is undefined', - fakeAsync(() => { - let dataResults: ExplorationBackendDict = { - draft_change_list_id: 3, - version: undefined, - auto_tts_enabled: false, - draft_changes: [], - is_version_of_draft_valid: true, - init_state_name: 'init', - param_changes: [], - param_specs: {randomProp: {obj_type: 'randomVal'}}, - states: {}, - title: 'Test Exploration', + it('should trigger errorcallback handler when version number is undefined', fakeAsync(() => { + let dataResults: ExplorationBackendDict = { + draft_change_list_id: 3, + version: undefined, + auto_tts_enabled: false, + draft_changes: [], + is_version_of_draft_valid: true, + init_state_name: 'init', + param_changes: [], + param_specs: {randomProp: {obj_type: 'randomVal'}}, + states: {}, + title: 'Test Exploration', + language_code: 'en', + next_content_id_index: 5, + exploration_metadata: { + title: 'Exploration', + category: 'Algebra', + objective: 'To learn', language_code: 'en', - next_content_id_index: 5, - exploration_metadata: { - title: 'Exploration', - category: 'Algebra', - objective: 'To learn', - language_code: 'en', - tags: [], - blurb: '', - author_notes: '', - states_schema_version: 50, - init_state_name: 'Introduction', - param_specs: {}, - param_changes: [], - auto_tts_enabled: false, - edits_allowed: true - } - }; - eds.data = dataResults; - const errorCallback = jasmine.createSpy('error'); - const successCallback = jasmine.createSpy('success'); - eds.autosaveChangeListAsync([], successCallback, errorCallback); + tags: [], + blurb: '', + author_notes: '', + states_schema_version: 50, + init_state_name: 'Introduction', + param_specs: {}, + param_changes: [], + auto_tts_enabled: false, + edits_allowed: true, + }, + }; + eds.data = dataResults; + const errorCallback = jasmine.createSpy('error'); + const successCallback = jasmine.createSpy('success'); + eds.autosaveChangeListAsync([], successCallback, errorCallback); - flushMicrotasks(); - expect(successCallback).not.toHaveBeenCalled(); - expect(errorCallback).toHaveBeenCalled(); - })); + flushMicrotasks(); + expect(successCallback).not.toHaveBeenCalled(); + expect(errorCallback).toHaveBeenCalled(); + })); it('should autosave draft changes when draft ids match', fakeAsync(() => { const errorCallback = jasmine.createSpy('error'); @@ -238,50 +241,54 @@ describe('Exploration data service', function() { eds.getDataAsync(errorCallback).then(successCallback); flushMicrotasks(); const req = httpTestingController.expectOne( - '/createhandler/autosave_draft/0'); + '/createhandler/autosave_draft/0' + ); expect(req.request.method).toBe('PUT'); req.flush(sampleDataResults); flushMicrotasks(); expect(successCallback).toHaveBeenCalledWith(sampleDataResults); })); - it('should not autosave draft changes when draft is already cached', - fakeAsync(() => { - const errorCallback = jasmine.createSpy('error'); - const explorationDraft: ExplorationDraft = new ExplorationDraft([], 1); - spyOn(explorationDraft, 'isValid').and.callFake(() => true); - spyOn(lss, 'getExplorationDraft').and.returnValue(explorationDraft); - // Save draft. - eds.getDataAsync(errorCallback).then(data => { - expect(data).toEqual(sampleDataResults); - expect(errorCallback).not.toHaveBeenCalled(); - }); - flushMicrotasks(); - const req = httpTestingController.expectOne( - '/createhandler/autosave_draft/0'); - expect(req.request.method).toBe('PUT'); - req.flush(sampleDataResults); - flushMicrotasks(); - httpTestingController.verify(); + it('should not autosave draft changes when draft is already cached', fakeAsync(() => { + const errorCallback = jasmine.createSpy('error'); + const explorationDraft: ExplorationDraft = new ExplorationDraft([], 1); + spyOn(explorationDraft, 'isValid').and.callFake(() => true); + spyOn(lss, 'getExplorationDraft').and.returnValue(explorationDraft); + // Save draft. + eds.getDataAsync(errorCallback).then(data => { + expect(data).toEqual(sampleDataResults); + expect(errorCallback).not.toHaveBeenCalled(); + }); + flushMicrotasks(); + const req = httpTestingController.expectOne( + '/createhandler/autosave_draft/0' + ); + expect(req.request.method).toBe('PUT'); + req.flush(sampleDataResults); + flushMicrotasks(); + httpTestingController.verify(); - const logInfoSpy = spyOn(ls, 'info').and.callThrough(); - // Draft is already saved and it's in cache. - eds.getDataAsync(errorCallback).then(data => { - expect(logInfoSpy).toHaveBeenCalledWith( - 'Found exploration data in cache.'); - expect(data).toEqual(sampleDataResults); - expect(errorCallback).not.toHaveBeenCalled(); - }); - flushMicrotasks(); - })); + const logInfoSpy = spyOn(ls, 'info').and.callThrough(); + // Draft is already saved and it's in cache. + eds.getDataAsync(errorCallback).then(data => { + expect(logInfoSpy).toHaveBeenCalledWith( + 'Found exploration data in cache.' + ); + expect(data).toEqual(sampleDataResults); + expect(errorCallback).not.toHaveBeenCalled(); + }); + flushMicrotasks(); + })); it('should autosave draft changes when draft ids match', fakeAsync(() => { const errorCallback = jasmine.createSpy('error'); const explorationDraft: ExplorationDraft = new ExplorationDraft([], 1); spyOn(explorationDraft, 'isValid').and.callFake(() => true); spyOn(lss, 'getExplorationDraft').and.returnValue(explorationDraft); - const windowRefSpy = spyOn(windowMock.nativeWindow.location, 'reload') - .and.callThrough(); + const windowRefSpy = spyOn( + windowMock.nativeWindow.location, + 'reload' + ).and.callThrough(); eds.getDataAsync(errorCallback).then(data => { expect(data).toEqual(sampleDataResults); expect(errorCallback).not.toHaveBeenCalled(); @@ -289,9 +296,10 @@ describe('Exploration data service', function() { }); flushMicrotasks(); const req = httpTestingController.expectOne( - '/createhandler/autosave_draft/0'); + '/createhandler/autosave_draft/0' + ); expect(req.request.method).toBe('PUT'); - req.flush('Whoops!', { status: 500, statusText: 'Internal Server error' }); + req.flush('Whoops!', {status: 500, statusText: 'Internal Server error'}); flushMicrotasks(); })); @@ -312,7 +320,8 @@ describe('Exploration data service', function() { const failHandler = jasmine.createSpy('fail'); eds.discardDraftAsync().then(successHandler, failHandler); const req = httpTestingController.expectOne( - '/createhandler/autosave_draft/0'); + '/createhandler/autosave_draft/0' + ); req.flush({}); flushMicrotasks(); expect(successHandler).toHaveBeenCalled(); @@ -324,7 +333,8 @@ describe('Exploration data service', function() { const failHandler = jasmine.createSpy('fail'); eds.discardDraftAsync().then(successHandler, failHandler); const req = httpTestingController.expectOne( - '/createhandler/autosave_draft/0'); + '/createhandler/autosave_draft/0' + ); req.error(new ErrorEvent('Internal server error')); flushMicrotasks(); expect(successHandler).not.toHaveBeenCalled(); @@ -342,8 +352,7 @@ describe('Exploration data service', function() { req.flush(sampleExploration); flushMicrotasks(); - expect(successHandler).toHaveBeenCalledWith( - sampleExploration.exploration); + expect(successHandler).toHaveBeenCalledWith(sampleExploration.exploration); })); it('should save an exploration to the backend', fakeAsync(() => { @@ -361,86 +370,88 @@ describe('Exploration data service', function() { }); flushMicrotasks(); const req = httpTestingController.expectOne( - '/createhandler/autosave_draft/0'); + '/createhandler/autosave_draft/0' + ); req.flush(sampleDataResults); flushMicrotasks(); eds.save(changeList, 'Commit Message', successHandler, failHandler); flushMicrotasks(); expect(successHandler).toHaveBeenCalledWith( sampleDataResults.is_version_of_draft_valid, - sampleDataResults.draft_changes); - expect(failHandler).not.toHaveBeenCalled(); - })); - - it('should save an exploration to the backend even when ' + - 'data.exploration is not defined', fakeAsync(() => { - const successHandler = jasmine.createSpy('success'); - const failHandler = jasmine.createSpy('fail'); - - const errorCallback = jasmine.createSpy('error'); - const explorationDraft: ExplorationDraft = new ExplorationDraft([], 1); - spyOn(explorationDraft, 'isValid').and.callFake(() => false); - spyOn(lss, 'getExplorationDraft').and.returnValue(explorationDraft); - const changeList: ExplorationChange[] = []; - let toBeResolved = false; - // The data.exploration won't receive a value. - spyOn(eebas, 'updateExplorationAsync').and.callFake( - async() => { - return new Promise((resolve, reject) => { - if (toBeResolved) { - resolve(sampleDataResults); - } else { - reject(); - } - }); - } + sampleDataResults.draft_changes ); - eds.getDataAsync(errorCallback); - flushMicrotasks(); - expect(errorCallback).toHaveBeenCalled(); - toBeResolved = true; - eds.save(changeList, 'Commit Message', successHandler, failHandler); - flushMicrotasks(); - expect(successHandler).toHaveBeenCalledWith( - sampleDataResults.is_version_of_draft_valid, - sampleDataResults.draft_changes); expect(failHandler).not.toHaveBeenCalled(); })); - it('should use reject handler when save an exploration to the backend fails', + it( + 'should save an exploration to the backend even when ' + + 'data.exploration is not defined', fakeAsync(() => { const successHandler = jasmine.createSpy('success'); const failHandler = jasmine.createSpy('fail'); const errorCallback = jasmine.createSpy('error'); const explorationDraft: ExplorationDraft = new ExplorationDraft([], 1); - spyOn(explorationDraft, 'isValid').and.callFake(() => true); + spyOn(explorationDraft, 'isValid').and.callFake(() => false); spyOn(lss, 'getExplorationDraft').and.returnValue(explorationDraft); const changeList: ExplorationChange[] = []; - eds.getDataAsync(errorCallback).then(function(data) { - expect(data).toEqual(sampleDataResults); - expect(errorCallback).not.toHaveBeenCalled(); - }); - flushMicrotasks(); - const req = httpTestingController.expectOne( - '/createhandler/autosave_draft/0'); - req.flush(sampleDataResults); - spyOn(eebas, 'updateExplorationAsync').and.callFake( - async() => { - return new Promise((resolve, reject) => { + let toBeResolved = false; + // The data.exploration won't receive a value. + spyOn(eebas, 'updateExplorationAsync').and.callFake(async () => { + return new Promise((resolve, reject) => { + if (toBeResolved) { + resolve(sampleDataResults); + } else { reject(); - }); - } - ); + } + }); + }); + eds.getDataAsync(errorCallback); flushMicrotasks(); + expect(errorCallback).toHaveBeenCalled(); + toBeResolved = true; eds.save(changeList, 'Commit Message', successHandler, failHandler); flushMicrotasks(); - expect(successHandler).not.toHaveBeenCalled(); - expect(failHandler).toHaveBeenCalled(); - })); + expect(successHandler).toHaveBeenCalledWith( + sampleDataResults.is_version_of_draft_valid, + sampleDataResults.draft_changes + ); + expect(failHandler).not.toHaveBeenCalled(); + }) + ); + + it('should use reject handler when save an exploration to the backend fails', fakeAsync(() => { + const successHandler = jasmine.createSpy('success'); + const failHandler = jasmine.createSpy('fail'); + + const errorCallback = jasmine.createSpy('error'); + const explorationDraft: ExplorationDraft = new ExplorationDraft([], 1); + spyOn(explorationDraft, 'isValid').and.callFake(() => true); + spyOn(lss, 'getExplorationDraft').and.returnValue(explorationDraft); + const changeList: ExplorationChange[] = []; + eds.getDataAsync(errorCallback).then(function (data) { + expect(data).toEqual(sampleDataResults); + expect(errorCallback).not.toHaveBeenCalled(); + }); + flushMicrotasks(); + const req = httpTestingController.expectOne( + '/createhandler/autosave_draft/0' + ); + req.flush(sampleDataResults); + spyOn(eebas, 'updateExplorationAsync').and.callFake(async () => { + return new Promise((resolve, reject) => { + reject(); + }); + }); + flushMicrotasks(); + eds.save(changeList, 'Commit Message', successHandler, failHandler); + flushMicrotasks(); + expect(successHandler).not.toHaveBeenCalled(); + expect(failHandler).toHaveBeenCalled(); + })); }); -describe('Exploration data service', function() { +describe('Exploration data service', function () { var eds: ExplorationDataService; var ls = null; var logErrorSpy: jasmine.Spy; @@ -452,9 +463,9 @@ describe('Exploration data service', function() { providers: [ { provide: UrlService, - useValue: {getPathname: () => '/exploration/0'} - } - ] + useValue: {getPathname: () => '/exploration/0'}, + }, + ], }); }); @@ -469,6 +480,7 @@ describe('Exploration data service', function() { eds.getDataAsync(errorCallback); expect(logErrorSpy).toHaveBeenCalledWith( - 'Unexpected call to ExplorationDataService for pathname: ' + pathname); + 'Unexpected call to ExplorationDataService for pathname: ' + pathname + ); }); }); diff --git a/core/templates/pages/exploration-editor-page/services/exploration-data.service.ts b/core/templates/pages/exploration-editor-page/services/exploration-data.service.ts index df9f905396bb..103bbf54c832 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-data.service.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-data.service.ts @@ -17,28 +17,31 @@ * with the exploration editor backend. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { EditableExplorationBackendApiService } from 'domain/exploration/editable-exploration-backend-api.service'; -import { ExplorationChange } from 'domain/exploration/exploration-draft.model'; -import { ExplorationBackendDict } from 'domain/exploration/ExplorationObjectFactory'; -import { ReadOnlyExplorationBackendApiService, ReadOnlyExplorationBackendDict } from 'domain/exploration/read-only-exploration-backend-api.service'; -import { tap } from 'rxjs/operators'; -import { AlertsService } from 'services/alerts.service'; -import { LoggerService } from 'services/contextual/logger.service'; -import { UrlService } from 'services/contextual/url.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { LocalStorageService } from 'services/local-storage.service'; -import { ExplorationDataBackendApiService } from './exploration-data-backend-api.service'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {EditableExplorationBackendApiService} from 'domain/exploration/editable-exploration-backend-api.service'; +import {ExplorationChange} from 'domain/exploration/exploration-draft.model'; +import {ExplorationBackendDict} from 'domain/exploration/ExplorationObjectFactory'; +import { + ReadOnlyExplorationBackendApiService, + ReadOnlyExplorationBackendDict, +} from 'domain/exploration/read-only-exploration-backend-api.service'; +import {tap} from 'rxjs/operators'; +import {AlertsService} from 'services/alerts.service'; +import {LoggerService} from 'services/contextual/logger.service'; +import {UrlService} from 'services/contextual/url.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {LocalStorageService} from 'services/local-storage.service'; +import {ExplorationDataBackendApiService} from './exploration-data-backend-api.service'; export interface DraftAutoSaveResponse { - 'draft_change_list_id': number; - 'is_version_of_draft_valid': boolean; - 'changes_are_mergeable': boolean; + draft_change_list_id: number; + is_version_of_draft_valid: boolean; + changes_are_mergeable: boolean; } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ExplorationDataService { // These properties are initialized using Angular lifecycle hooks @@ -52,13 +55,11 @@ export class ExplorationDataService { constructor( private alertsService: AlertsService, - private editableExplorationBackendApiService: - EditableExplorationBackendApiService, + private editableExplorationBackendApiService: EditableExplorationBackendApiService, private explorationDataBackendApiService: ExplorationDataBackendApiService, private localStorageService: LocalStorageService, private loggerService: LoggerService, - private readOnlyExplorationBackendApiService: - ReadOnlyExplorationBackendApiService, + private readOnlyExplorationBackendApiService: ReadOnlyExplorationBackendApiService, private urlService: UrlService, private windowRef: WindowRef ) { @@ -75,45 +76,54 @@ export class ExplorationDataService { if (!explorationId) { this.loggerService.error( - 'Unexpected call to ExplorationDataService for pathname: ' + pathname); + 'Unexpected call to ExplorationDataService for pathname: ' + pathname + ); } else { this.explorationId = explorationId; - this.resolvedAnswersUrlPrefix = ( - '/createhandler/resolved_answers/' + explorationId); - this.explorationDraftAutosaveUrl = ( - '/createhandler/autosave_draft/' + explorationId); + this.resolvedAnswersUrlPrefix = + '/createhandler/resolved_answers/' + explorationId; + this.explorationDraftAutosaveUrl = + '/createhandler/autosave_draft/' + explorationId; } } private async _autosaveChangeListAsync( - changeList: ExplorationChange[]): Promise { + changeList: ExplorationChange[] + ): Promise { this.localStorageService.saveExplorationDraft( - this.explorationId, changeList, this.draftChangeListId); + this.explorationId, + changeList, + this.draftChangeListId + ); if (!this.data.version) { throw new Error('Version cannot be undefined'); } - return this.explorationDataBackendApiService.saveChangeList( - this.explorationDraftAutosaveUrl, - changeList, - this.data.version, - ).pipe( - tap(response => { - this.draftChangeListId = response.draft_change_list_id; - // We can safely remove the locally saved draft copy if it was saved - // to the backend. - this.localStorageService.removeExplorationDraft(this.explorationId); - })).toPromise(); + return this.explorationDataBackendApiService + .saveChangeList( + this.explorationDraftAutosaveUrl, + changeList, + this.data.version + ) + .pipe( + tap(response => { + this.draftChangeListId = response.draft_change_list_id; + // We can safely remove the locally saved draft copy if it was saved + // to the backend. + this.localStorageService.removeExplorationDraft(this.explorationId); + }) + ) + .toPromise(); } // Note that the changeList is the full changeList since the last // committed version (as opposed to the most recent autosave). async autosaveChangeListAsync( - changeList: ExplorationChange[], - successCallback: (response: DraftAutoSaveResponse) => void, - errorCallback: () => void + changeList: ExplorationChange[], + successCallback: (response: DraftAutoSaveResponse) => void, + errorCallback: () => void ): Promise { return this._autosaveChangeListAsync(changeList).then( - (response) => { + response => { if (successCallback) { successCallback(response); } @@ -127,13 +137,16 @@ export class ExplorationDataService { } async discardDraftAsync(): Promise { - return this.explorationDataBackendApiService.discardDraft( - this.explorationDraftAutosaveUrl).toPromise(); + return this.explorationDataBackendApiService + .discardDraft(this.explorationDraftAutosaveUrl) + .toPromise(); } - async getDataAsync(errorCallback: ( - explorationId: string, - lostChanges: ExplorationChange[]) => void | undefined + async getDataAsync( + errorCallback: ( + explorationId: string, + lostChanges: ExplorationChange[] + ) => void | undefined ): Promise { if (this.data) { this.loggerService.info('Found exploration data in cache.'); @@ -146,13 +159,14 @@ export class ExplorationDataService { // (which is cached here) will be reused. return new Promise((resolve, reject) => { this.editableExplorationBackendApiService - .fetchApplyDraftExplorationAsync( - this.explorationId).then((response) => { + .fetchApplyDraftExplorationAsync(this.explorationId) + .then(response => { this.loggerService.info('Retrieved exploration data.'); this.draftChangeListId = response.draft_change_list_id; this.data = response; const draft = this.localStorageService.getExplorationDraft( - this.explorationId); + this.explorationId + ); if (draft) { if (draft.isValid(this.draftChangeListId)) { var changeList = draft.getChanges(); @@ -181,11 +195,12 @@ export class ExplorationDataService { // Returns a promise supplying the last saved version for the current // exploration. async getLastSavedDataAsync(): Promise { - return this.readOnlyExplorationBackendApiService.loadLatestExplorationAsync( - this.explorationId).then(response => { - this.loggerService.info('Retrieved saved exploration data.'); - return response.exploration; - }); + return this.readOnlyExplorationBackendApiService + .loadLatestExplorationAsync(this.explorationId) + .then(response => { + this.loggerService.info('Retrieved saved exploration data.'); + return response.exploration; + }); } /** @@ -199,35 +214,48 @@ export class ExplorationDataService { * this save operation. */ save( - changeList: ExplorationChange[], - commitMessage: string, - successCallback: ( - isDraftVersionvalid: boolean, - draftChanges: ExplorationChange[]) => void, - errorCallback: (errorResponse?: object) => void): void { + changeList: ExplorationChange[], + commitMessage: string, + successCallback: ( + isDraftVersionvalid: boolean, + draftChanges: ExplorationChange[] + ) => void, + errorCallback: (errorResponse?: object) => void + ): void { let dataVersion = 1; if (this.data && this.data.version !== undefined) { dataVersion = this.data.version; } - this.editableExplorationBackendApiService.updateExplorationAsync( - this.explorationId, - dataVersion, commitMessage, changeList - ).then(response => { - this.alertsService.clearWarnings(); - this.data = response; - if (successCallback) { - successCallback( - response.is_version_of_draft_valid, - response.draft_changes); - } - }, (response) => { - if (errorCallback) { - errorCallback(response); - } - } - ); + this.editableExplorationBackendApiService + .updateExplorationAsync( + this.explorationId, + dataVersion, + commitMessage, + changeList + ) + .then( + response => { + this.alertsService.clearWarnings(); + this.data = response; + if (successCallback) { + successCallback( + response.is_version_of_draft_valid, + response.draft_changes + ); + } + }, + response => { + if (errorCallback) { + errorCallback(response); + } + } + ); } } -angular.module('oppia').factory( - 'ExplorationDataService', downgradeInjectable(ExplorationDataService)); +angular + .module('oppia') + .factory( + 'ExplorationDataService', + downgradeInjectable(ExplorationDataService) + ); diff --git a/core/templates/pages/exploration-editor-page/services/exploration-diff.service.spec.ts b/core/templates/pages/exploration-editor-page/services/exploration-diff.service.spec.ts index 1ccb47975623..3eca4a86bdd1 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-diff.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-diff.service.spec.ts @@ -12,10 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { TestBed } from '@angular/core/testing'; -import { StateObjectFactory } from 'domain/state/StateObjectFactory'; -import { ExplorationDiffService, ExplorationGraphChangeList } from './exploration-diff.service'; -import { ExplorationChange } from 'domain/exploration/exploration-draft.model'; +import {TestBed} from '@angular/core/testing'; +import {StateObjectFactory} from 'domain/state/StateObjectFactory'; +import { + ExplorationDiffService, + ExplorationGraphChangeList, +} from './exploration-diff.service'; +import {ExplorationChange} from 'domain/exploration/exploration-draft.model'; /** * @fileoverview Unit tests for the Exploration Diff Service. @@ -31,23 +34,39 @@ describe('Exploration Diff Service', () => { stateObjectFactory = TestBed.inject(StateObjectFactory); }); - it('should throw error if try to access graph ' + - 'diff data with invalid command', () => { - let newState = stateObjectFactory.createDefaultState( - 'newState', 'content_0', 'default_outcome_1'); - let oldState = stateObjectFactory.createDefaultState( - 'oldState', 'content_0', 'default_outcome_1'); - explorationGraphChangeList = [{ - changeList: [{ - cmd: 'invalidCommand', - state_name: 'newState' - } as unknown as ExplorationChange], - directionForwards: true - }]; + it( + 'should throw error if try to access graph ' + + 'diff data with invalid command', + () => { + let newState = stateObjectFactory.createDefaultState( + 'newState', + 'content_0', + 'default_outcome_1' + ); + let oldState = stateObjectFactory.createDefaultState( + 'oldState', + 'content_0', + 'default_outcome_1' + ); + explorationGraphChangeList = [ + { + changeList: [ + { + cmd: 'invalidCommand', + state_name: 'newState', + } as unknown as ExplorationChange, + ], + directionForwards: true, + }, + ]; - expect(() => { - explorationDiffService.getDiffGraphData( - {newState: newState}, {oldState: oldState}, explorationGraphChangeList); - }).toThrowError('Invalid change command: invalidCommand'); - }); + expect(() => { + explorationDiffService.getDiffGraphData( + {newState: newState}, + {oldState: oldState}, + explorationGraphChangeList + ); + }).toThrowError('Invalid change command: invalidCommand'); + } + ); }); diff --git a/core/templates/pages/exploration-editor-page/services/exploration-diff.service.ts b/core/templates/pages/exploration-editor-page/services/exploration-diff.service.ts index 1a8ffbd71664..2e64cc9e3008 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-diff.service.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-diff.service.ts @@ -16,21 +16,24 @@ * @fileoverview Service for computing diffs of explorations. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; import cloneDeep from 'lodash/cloneDeep'; import isEqual from 'lodash/isEqual'; -import { InteractionSpecsConstants, InteractionSpecsKey } from 'pages/interaction-specs.constants'; +import { + InteractionSpecsConstants, + InteractionSpecsKey, +} from 'pages/interaction-specs.constants'; import { ExplorationChangeAddState, ExplorationChange, ExplorationChangeRenameState, - ExplorationChangeEditStateProperty + ExplorationChangeEditStateProperty, } from 'domain/exploration/exploration-draft.model'; -import { StateObjectsDict } from 'domain/exploration/StatesObjectFactory'; +import {StateObjectsDict} from 'domain/exploration/StatesObjectFactory'; export interface ExplorationGraphChangeList { changeList: ExplorationChange[]; @@ -80,7 +83,7 @@ export interface StateLink { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ExplorationDiffService { STATE_PROPERTY_ADDED = 'added'; @@ -100,10 +103,11 @@ export class ExplorationDiffService { } _generateInitialStateIdsAndData( - statesDict: StateObjectsDict): StateIdsAndData { + statesDict: StateObjectsDict + ): StateIdsAndData { let result: ResultInterface = { stateIds: {}, - stateData: {} + stateData: {}, }; this._resetMaxId(); @@ -113,7 +117,7 @@ export class ExplorationDiffService { result.stateData[stateId] = { newestStateName: stateName, originalStateName: stateName, - stateProperty: this.STATE_PROPERTY_UNCHANGED + stateProperty: this.STATE_PROPERTY_UNCHANGED, }; result.stateIds[stateName] = stateId; } @@ -121,27 +125,33 @@ export class ExplorationDiffService { } _postprocessStateIdsAndData( - originalStateIds: StateIds, - stateIds: StateIds, - stateData: StateData, - v1States: StateObjectsDict, - v2States: StateObjectsDict): ProcessedStateIdsAndData { + originalStateIds: StateIds, + stateIds: StateIds, + stateData: StateData, + v1States: StateObjectsDict, + v2States: StateObjectsDict + ): ProcessedStateIdsAndData { // Ignore changes that were canceled out by later changes. for (let stateId in stateData) { - if (stateData[stateId].stateProperty === this.STATE_PROPERTY_CHANGED && - v1States.hasOwnProperty(stateData[stateId].originalStateName) && - v2States.hasOwnProperty(stateData[stateId].newestStateName) && - isEqual(v1States[stateData[stateId].originalStateName], - v2States[stateData[stateId].newestStateName])) { + if ( + stateData[stateId].stateProperty === this.STATE_PROPERTY_CHANGED && + v1States.hasOwnProperty(stateData[stateId].originalStateName) && + v2States.hasOwnProperty(stateData[stateId].newestStateName) && + isEqual( + v1States[stateData[stateId].originalStateName], + v2States[stateData[stateId].newestStateName] + ) + ) { stateData[stateId].stateProperty = this.STATE_PROPERTY_UNCHANGED; } } // Delete states not present in both v1 and v2. for (let stateId in stateData) { - if (!v1States.hasOwnProperty( - stateData[stateId].originalStateName) && - !v2States.hasOwnProperty(stateData[stateId].newestStateName)) { + if ( + !v1States.hasOwnProperty(stateData[stateId].originalStateName) && + !v2States.hasOwnProperty(stateData[stateId].newestStateName) + ) { delete stateData[stateId]; } } @@ -156,17 +166,19 @@ export class ExplorationDiffService { let newStateIsTerminal = false; if (oldState) { let interactionId = oldState.interaction.id as InteractionSpecsKey; - const interactionSpec = ( - InteractionSpecsConstants.INTERACTION_SPECS[interactionId]); + const interactionSpec = + InteractionSpecsConstants.INTERACTION_SPECS[interactionId]; oldStateIsTerminal = Boolean( - oldState.interaction.id && interactionSpec.is_terminal); + oldState.interaction.id && interactionSpec.is_terminal + ); } if (newState) { let interactionId = newState.interaction.id as InteractionSpecsKey; - const interactionSpec = ( - InteractionSpecsConstants.INTERACTION_SPECS[interactionId]); + const interactionSpec = + InteractionSpecsConstants.INTERACTION_SPECS[interactionId]; newStateIsTerminal = Boolean( - newState.interaction.id && interactionSpec.is_terminal); + newState.interaction.id && interactionSpec.is_terminal + ); } if (oldStateIsTerminal || newStateIsTerminal) { finalStateIds.push(stateId); @@ -174,14 +186,18 @@ export class ExplorationDiffService { } let links = this._compareLinks( - v1States, originalStateIds, v2States, stateIds); + v1States, + originalStateIds, + v2States, + stateIds + ); return { nodes: stateData, links: links, originalStateIds: originalStateIds, stateIds: stateIds, - finalStateIds: finalStateIds + finalStateIds: finalStateIds, }; } @@ -202,10 +218,10 @@ export class ExplorationDiffService { * number, and false if changes are compared in decreasing version number. */ _getDiffGraphData( - v1States: StateObjectsDict, - v2States: StateObjectsDict, - changeListData: ExplorationGraphChangeList[]): - ProcessedStateIdsAndData { + v1States: StateObjectsDict, + v2States: StateObjectsDict, + changeListData: ExplorationGraphChangeList[] + ): ProcessedStateIdsAndData { let v1Info = this._generateInitialStateIdsAndData(v1States); let stateData = v1Info.stateData; let stateIds = v1Info.stateIds; @@ -217,68 +233,77 @@ export class ExplorationDiffService { changeList.forEach(change => { let explorationChangeAddState = change as ExplorationChangeAddState; - if ((directionForwards && change.cmd === 'add_state') || - (!directionForwards && change.cmd === 'delete_state')) { + if ( + (directionForwards && change.cmd === 'add_state') || + (!directionForwards && change.cmd === 'delete_state') + ) { if (!stateIds.hasOwnProperty(explorationChangeAddState.state_name)) { let newId = this._generateNewId(); stateIds[(change as ExplorationChangeAddState).state_name] = newId; } - let currentStateId = ( - stateIds[explorationChangeAddState.state_name]); - if (stateData.hasOwnProperty(currentStateId) && - stateData[currentStateId].stateProperty === - this.STATE_PROPERTY_DELETED) { + let currentStateId = stateIds[explorationChangeAddState.state_name]; + if ( + stateData.hasOwnProperty(currentStateId) && + stateData[currentStateId].stateProperty === + this.STATE_PROPERTY_DELETED + ) { stateData[currentStateId].stateProperty = - this.STATE_PROPERTY_CHANGED; - stateData[currentStateId].newestStateName = ( - explorationChangeAddState).state_name; + this.STATE_PROPERTY_CHANGED; + stateData[currentStateId].newestStateName = + explorationChangeAddState.state_name; } else { stateData[currentStateId] = { newestStateName: (change as ExplorationChangeAddState).state_name, - originalStateName: ( - explorationChangeAddState).state_name, - stateProperty: this.STATE_PROPERTY_ADDED + originalStateName: explorationChangeAddState.state_name, + stateProperty: this.STATE_PROPERTY_ADDED, }; } - } else if ((directionForwards && change.cmd === 'delete_state') || - (!directionForwards && change.cmd === 'add_state')) { - if (stateData[stateIds[( - explorationChangeAddState).state_name]].stateProperty === - this.STATE_PROPERTY_ADDED) { - stateData[stateIds[( - explorationChangeAddState).state_name]].stateProperty = ( - this.STATE_PROPERTY_CHANGED); + } else if ( + (directionForwards && change.cmd === 'delete_state') || + (!directionForwards && change.cmd === 'add_state') + ) { + if ( + stateData[stateIds[explorationChangeAddState.state_name]] + .stateProperty === this.STATE_PROPERTY_ADDED + ) { + stateData[ + stateIds[explorationChangeAddState.state_name] + ].stateProperty = this.STATE_PROPERTY_CHANGED; } else { - stateData[stateIds[( - explorationChangeAddState).state_name]].stateProperty = ( - this.STATE_PROPERTY_DELETED); + stateData[ + stateIds[explorationChangeAddState.state_name] + ].stateProperty = this.STATE_PROPERTY_DELETED; } } else if (change.cmd === 'rename_state') { let newStateName = null; let oldStateName = null; if (directionForwards) { - newStateName = ( - change as ExplorationChangeRenameState).new_state_name; - oldStateName = ( - change as ExplorationChangeRenameState).old_state_name; + newStateName = (change as ExplorationChangeRenameState) + .new_state_name; + oldStateName = (change as ExplorationChangeRenameState) + .old_state_name; } else { - newStateName = ( - change as ExplorationChangeRenameState).old_state_name; - oldStateName = ( - change as ExplorationChangeRenameState).new_state_name; + newStateName = (change as ExplorationChangeRenameState) + .old_state_name; + oldStateName = (change as ExplorationChangeRenameState) + .new_state_name; } stateIds[newStateName] = stateIds[oldStateName]; delete stateIds[oldStateName]; stateData[stateIds[newStateName]].newestStateName = newStateName; } else if (change.cmd === 'edit_state_property') { - if (stateData[stateIds[( - change as ExplorationChangeEditStateProperty).state_name]] - .stateProperty === - this.STATE_PROPERTY_UNCHANGED) { - stateData[stateIds[( - change as ExplorationChangeEditStateProperty).state_name]] - .stateProperty = ( - this.STATE_PROPERTY_CHANGED); + if ( + stateData[ + stateIds[ + (change as ExplorationChangeEditStateProperty).state_name + ] + ].stateProperty === this.STATE_PROPERTY_UNCHANGED + ) { + stateData[ + stateIds[ + (change as ExplorationChangeEditStateProperty).state_name + ] + ].stateProperty = this.STATE_PROPERTY_CHANGED; } } else if ( change.cmd !== 'migrate_states_schema_to_latest_version' && @@ -288,7 +313,6 @@ export class ExplorationDiffService { change.cmd !== 'mark_translations_needs_update' && change.cmd !== 'remove_translations' && change.cmd !== 'edit_translation' - ) { throw new Error('Invalid change command: ' + change.cmd); } @@ -296,7 +320,12 @@ export class ExplorationDiffService { }); return this._postprocessStateIdsAndData( - originalStateIds, stateIds, stateData, v1States, v2States); + originalStateIds, + stateIds, + stateData, + v1States, + v2States + ); } /** @@ -312,8 +341,10 @@ export class ExplorationDiffService { * - maxId: the maximum id in states and stateIds. */ _getAdjMatrix( - states: StateObjectsDict, - stateIds: StateIds, maxId: number): AdjMatrix { + states: StateObjectsDict, + stateIds: StateIds, + maxId: number + ): AdjMatrix { let adjMatrix: AdjMatrix = {}; for (let stateId = 1; stateId <= maxId; stateId++) { adjMatrix[stateId] = {}; @@ -346,13 +377,17 @@ export class ExplorationDiffService { * - 'linkProperty': 'added', 'deleted' or 'unchanged' */ _compareLinks( - v1States: StateObjectsDict, - originalStateIds: StateIds, - v2States: StateObjectsDict, - newestStateIds: StateIds): StateLink[] { + v1States: StateObjectsDict, + originalStateIds: StateIds, + v2States: StateObjectsDict, + newestStateIds: StateIds + ): StateLink[] { let links = []; let adjMatrixV1 = this._getAdjMatrix( - v1States, originalStateIds, this._maxId); + v1States, + originalStateIds, + this._maxId + ); let adjMatrixV2 = this._getAdjMatrix(v2States, newestStateIds, this._maxId); for (let i = 1; i <= this._maxId; i++) { @@ -361,10 +396,12 @@ export class ExplorationDiffService { links.push({ source: i, target: j, - linkProperty: ( - adjMatrixV1[i][j] && adjMatrixV2[i][j] ? 'unchanged' : - !adjMatrixV1[i][j] && adjMatrixV2[i][j] ? 'added' : - 'deleted') + linkProperty: + adjMatrixV1[i][j] && adjMatrixV2[i][j] + ? 'unchanged' + : !adjMatrixV1[i][j] && adjMatrixV2[i][j] + ? 'added' + : 'deleted', }); } } @@ -374,16 +411,17 @@ export class ExplorationDiffService { } getDiffGraphData( - oldStates: StateObjectsDict, newStates: StateObjectsDict, - changeListData: ExplorationGraphChangeList[]): - ProcessedStateIdsAndData { - return this._getDiffGraphData( - oldStates, - newStates, - changeListData); + oldStates: StateObjectsDict, + newStates: StateObjectsDict, + changeListData: ExplorationGraphChangeList[] + ): ProcessedStateIdsAndData { + return this._getDiffGraphData(oldStates, newStates, changeListData); } } -angular.module('oppia').factory( - 'ExplorationDiffService', - downgradeInjectable(ExplorationDiffService)); +angular + .module('oppia') + .factory( + 'ExplorationDiffService', + downgradeInjectable(ExplorationDiffService) + ); diff --git a/core/templates/pages/exploration-editor-page/services/exploration-edits-allowed-backend-api.service.spec.ts b/core/templates/pages/exploration-editor-page/services/exploration-edits-allowed-backend-api.service.spec.ts index e9b62aff1e15..8cb46cf674c7 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-edits-allowed-backend-api.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-edits-allowed-backend-api.service.spec.ts @@ -15,10 +15,12 @@ * @fileoverview Unit tests for ExplorationEditsAllowedBackendApiService. */ -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; -import { ExplorationEditsAllowedBackendApiService } from './exploration-edits-allowed-backend-api.service'; - +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; +import {ExplorationEditsAllowedBackendApiService} from './exploration-edits-allowed-backend-api.service'; describe('Exploration edits allowed backend API service', () => { let eeabas: ExplorationEditsAllowedBackendApiService; @@ -26,7 +28,7 @@ describe('Exploration edits allowed backend API service', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] + imports: [HttpClientTestingModule], }); eeabas = TestBed.get(ExplorationEditsAllowedBackendApiService); @@ -42,8 +44,9 @@ describe('Exploration edits allowed backend API service', () => { let failHandler = jasmine.createSpy('fail'); let handlerUrl = '/editsallowedhandler/123'; - eeabas.setEditsAllowed(true, '123', () => {}).then( - successHandler, failHandler); + eeabas + .setEditsAllowed(true, '123', () => {}) + .then(successHandler, failHandler); let req = httpTestingController.expectOne(handlerUrl); expect(req.request.method).toEqual('PUT'); @@ -61,8 +64,9 @@ describe('Exploration edits allowed backend API service', () => { let failHandler = jasmine.createSpy('fail'); let handlerUrl = '/editsallowedhandler/123'; - eeabas.setEditsAllowed(false, '123', () => {}).then( - successHandler, failHandler); + eeabas + .setEditsAllowed(false, '123', () => {}) + .then(successHandler, failHandler); let req = httpTestingController.expectOne(handlerUrl); expect(req.request.method).toEqual('PUT'); diff --git a/core/templates/pages/exploration-editor-page/services/exploration-edits-allowed-backend-api.service.ts b/core/templates/pages/exploration-editor-page/services/exploration-edits-allowed-backend-api.service.ts index a36dc7991dfa..c027b3e41560 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-edits-allowed-backend-api.service.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-edits-allowed-backend-api.service.ts @@ -17,28 +17,33 @@ * exploration. */ -import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; +import {downgradeInjectable} from '@angular/upgrade/static'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ExplorationEditsAllowedBackendApiService { - constructor( - private http: HttpClient - ) {} + constructor(private http: HttpClient) {} async setEditsAllowed( - editsAreAllowed: boolean, - explorationId: string, - callback: () => void + editsAreAllowed: boolean, + explorationId: string, + callback: () => void ): Promise { - return this.http.put(`/editsallowedhandler/${explorationId}`, { - edits_are_allowed: editsAreAllowed - }).toPromise().then(callback, () => {}); + return this.http + .put(`/editsallowedhandler/${explorationId}`, { + edits_are_allowed: editsAreAllowed, + }) + .toPromise() + .then(callback, () => {}); } } -angular.module('oppia').factory('ExplorationEditsAllowedBackendApiService', - downgradeInjectable(ExplorationEditsAllowedBackendApiService)); +angular + .module('oppia') + .factory( + 'ExplorationEditsAllowedBackendApiService', + downgradeInjectable(ExplorationEditsAllowedBackendApiService) + ); diff --git a/core/templates/pages/exploration-editor-page/services/exploration-init-state-name.service.spec.ts b/core/templates/pages/exploration-editor-page/services/exploration-init-state-name.service.spec.ts index 7c8944ea80f0..65d811158551 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-init-state-name.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-init-state-name.service.spec.ts @@ -16,10 +16,10 @@ * @fileoverview Unit tests for the ExplorationInitSateNameService. */ -import { TestBed } from '@angular/core/testing'; -import { ExplorationPropertyService } from './exploration-property.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ExplorationInitStateNameService } from './exploration-init-state-name.service'; +import {TestBed} from '@angular/core/testing'; +import {ExplorationPropertyService} from './exploration-property.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ExplorationInitStateNameService} from './exploration-init-state-name.service'; describe('Exploration Init State Name Service', () => { let eisns: ExplorationInitStateNameService; @@ -27,9 +27,7 @@ describe('Exploration Init State Name Service', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [ - ExplorationPropertyService - ] + providers: [ExplorationPropertyService], }); eisns = TestBed.inject(ExplorationInitStateNameService); diff --git a/core/templates/pages/exploration-editor-page/services/exploration-init-state-name.service.ts b/core/templates/pages/exploration-editor-page/services/exploration-init-state-name.service.ts index 935ee0da2892..e5a2cd9bdabe 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-init-state-name.service.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-init-state-name.service.ts @@ -19,18 +19,17 @@ * valid. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { ExplorationPropertyService } from './exploration-property.service'; -import { AlertsService } from 'services/alerts.service'; -import { ChangeListService } from './change-list.service'; -import { LoggerService } from 'services/contextual/logger.service'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {ExplorationPropertyService} from './exploration-property.service'; +import {AlertsService} from 'services/alerts.service'; +import {ChangeListService} from './change-list.service'; +import {LoggerService} from 'services/contextual/logger.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) -export class ExplorationInitStateNameService - extends ExplorationPropertyService { +export class ExplorationInitStateNameService extends ExplorationPropertyService { // This property is initialized using init method and we need to do // non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -45,6 +44,9 @@ export class ExplorationInitStateNameService } } -angular.module('oppia').factory( - 'ExplorationInitStateNameService', downgradeInjectable( - ExplorationInitStateNameService)); +angular + .module('oppia') + .factory( + 'ExplorationInitStateNameService', + downgradeInjectable(ExplorationInitStateNameService) + ); diff --git a/core/templates/pages/exploration-editor-page/services/exploration-language-code.service.spec.ts b/core/templates/pages/exploration-editor-page/services/exploration-language-code.service.spec.ts index 9d94dc765816..99a61aba5627 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-language-code.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-language-code.service.spec.ts @@ -16,11 +16,11 @@ * @fileoverview Unit tests for the ExplorationLanguageCodeService. */ -import { TestBed } from '@angular/core/testing'; -import { ContextService } from 'services/context.service'; -import { ExplorationPropertyService } from './exploration-property.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ExplorationLanguageCodeService } from './exploration-language-code.service'; +import {TestBed} from '@angular/core/testing'; +import {ContextService} from 'services/context.service'; +import {ExplorationPropertyService} from './exploration-property.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ExplorationLanguageCodeService} from './exploration-language-code.service'; describe('Exploration Language Code Service', () => { let elcs: ExplorationLanguageCodeService; @@ -29,9 +29,7 @@ describe('Exploration Language Code Service', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [ - ExplorationPropertyService - ] + providers: [ExplorationPropertyService], }); elcs = TestBed.inject(ExplorationLanguageCodeService); diff --git a/core/templates/pages/exploration-editor-page/services/exploration-language-code.service.ts b/core/templates/pages/exploration-editor-page/services/exploration-language-code.service.ts index e70809ded295..06b77ce480fb 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-language-code.service.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-language-code.service.ts @@ -16,18 +16,18 @@ * @fileoverview A data service that stores the exploration language code. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { ExplorationPropertyService } from './exploration-property.service'; -import { AlertsService } from 'services/alerts.service'; -import { ChangeListService } from './change-list.service'; -import { LoggerService } from 'services/contextual/logger.service'; -import { ContextService } from 'services/context.service'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {ExplorationPropertyService} from './exploration-property.service'; +import {AlertsService} from 'services/alerts.service'; +import {ChangeListService} from './change-list.service'; +import {LoggerService} from 'services/contextual/logger.service'; +import {ContextService} from 'services/context.service'; -import { AppConstants } from 'app.constants'; +import {AppConstants} from 'app.constants'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ExplorationLanguageCodeService extends ExplorationPropertyService { propertyName: string = 'language_code'; @@ -59,12 +59,15 @@ export class ExplorationLanguageCodeService extends ExplorationPropertyService { // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types _isValid(value: string) { - return AppConstants.SUPPORTED_CONTENT_LANGUAGES.some((elt) => { + return AppConstants.SUPPORTED_CONTENT_LANGUAGES.some(elt => { return elt.code === value; }); } } -angular.module('oppia').factory( - 'ExplorationLanguageCodeService', downgradeInjectable( - ExplorationLanguageCodeService)); +angular + .module('oppia') + .factory( + 'ExplorationLanguageCodeService', + downgradeInjectable(ExplorationLanguageCodeService) + ); diff --git a/core/templates/pages/exploration-editor-page/services/exploration-next-content-id-index.service.ts b/core/templates/pages/exploration-editor-page/services/exploration-next-content-id-index.service.ts index d6e59e99ed01..4151eb61231d 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-next-content-id-index.service.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-next-content-id-index.service.ts @@ -17,19 +17,17 @@ * next_content_id_index value. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { ExplorationPropertyService } from './exploration-property.service'; -import { AlertsService } from 'services/alerts.service'; -import { ChangeListService } from './change-list.service'; -import { LoggerService } from 'services/contextual/logger.service'; - +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {ExplorationPropertyService} from './exploration-property.service'; +import {AlertsService} from 'services/alerts.service'; +import {ChangeListService} from './change-list.service'; +import {LoggerService} from 'services/contextual/logger.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) -export class ExplorationNextContentIdIndexService - extends ExplorationPropertyService { +export class ExplorationNextContentIdIndexService extends ExplorationPropertyService { propertyName: string = 'next_content_id_index'; displayed!: number; savedMemento!: number; @@ -37,11 +35,15 @@ export class ExplorationNextContentIdIndexService constructor( protected alertsService: AlertsService, protected changeListService: ChangeListService, - protected loggerService: LoggerService, + protected loggerService: LoggerService ) { super(alertsService, changeListService, loggerService); } } -angular.module('oppia').factory('ExplorationNextContentIdIndexService', - downgradeInjectable(ExplorationNextContentIdIndexService)); +angular + .module('oppia') + .factory( + 'ExplorationNextContentIdIndexService', + downgradeInjectable(ExplorationNextContentIdIndexService) + ); diff --git a/core/templates/pages/exploration-editor-page/services/exploration-objective.service.spec.ts b/core/templates/pages/exploration-editor-page/services/exploration-objective.service.spec.ts index 7f8b1b9efac5..dc339e71479c 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-objective.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-objective.service.spec.ts @@ -16,13 +16,13 @@ * @fileoverview Unit tests for the ExplorationObjectiveService. */ -import { TestBed } from '@angular/core/testing'; -import { ExplorationObjectiveService } from './exploration-objective.service'; -import { ExplorationPropertyService } from './exploration-property.service'; -import { ExplorationRightsService } from './exploration-rights.service'; -import { ValidatorsService } from 'services/validators.service'; -import { NormalizeWhitespacePipe } from 'filters/string-utility-filters/normalize-whitespace.pipe'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; +import {TestBed} from '@angular/core/testing'; +import {ExplorationObjectiveService} from './exploration-objective.service'; +import {ExplorationPropertyService} from './exploration-property.service'; +import {ExplorationRightsService} from './exploration-rights.service'; +import {ValidatorsService} from 'services/validators.service'; +import {NormalizeWhitespacePipe} from 'filters/string-utility-filters/normalize-whitespace.pipe'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; describe('Exploration Objective Service', () => { let eos: ExplorationObjectiveService; @@ -34,14 +34,14 @@ describe('Exploration Objective Service', () => { ExplorationRightsService, ValidatorsService, NormalizeWhitespacePipe, - ExplorationPropertyService - ] + ExplorationPropertyService, + ], }); eos = TestBed.inject(ExplorationObjectiveService); }); - it('should test the child object properties', function() { + it('should test the child object properties', function () { expect(eos.propertyName).toBe('objective'); let NotNormalize = ' Exploration Objective Service '; let Normalize = 'Exploration Objective Service'; diff --git a/core/templates/pages/exploration-editor-page/services/exploration-objective.service.ts b/core/templates/pages/exploration-editor-page/services/exploration-objective.service.ts index 70540001bc57..4eeffe751282 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-objective.service.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-objective.service.ts @@ -17,18 +17,18 @@ * that it can be displayed and edited in multiple places in the UI. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { ExplorationPropertyService } from './exploration-property.service'; -import { AlertsService } from 'services/alerts.service'; -import { ChangeListService } from './change-list.service'; -import { LoggerService } from 'services/contextual/logger.service'; -import { ExplorationRightsService } from './exploration-rights.service'; -import { ValidatorsService } from 'services/validators.service'; -import { NormalizeWhitespacePipe } from 'filters/string-utility-filters/normalize-whitespace.pipe'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {ExplorationPropertyService} from './exploration-property.service'; +import {AlertsService} from 'services/alerts.service'; +import {ChangeListService} from './change-list.service'; +import {LoggerService} from 'services/contextual/logger.service'; +import {ExplorationRightsService} from './exploration-rights.service'; +import {ValidatorsService} from 'services/validators.service'; +import {NormalizeWhitespacePipe} from 'filters/string-utility-filters/normalize-whitespace.pipe'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ExplorationObjectiveService extends ExplorationPropertyService { propertyName: string = 'objective'; @@ -51,9 +51,14 @@ export class ExplorationObjectiveService extends ExplorationPropertyService { _isValid(value: string): boolean { return ( this.explorationRightsService.isPrivate() || - this.validatorsService.isNonempty(value, false)); + this.validatorsService.isNonempty(value, false) + ); } } -angular.module('oppia').factory('ExplorationObjectiveService', - downgradeInjectable(ExplorationObjectiveService)); +angular + .module('oppia') + .factory( + 'ExplorationObjectiveService', + downgradeInjectable(ExplorationObjectiveService) + ); diff --git a/core/templates/pages/exploration-editor-page/services/exploration-param-changes.service.spec.ts b/core/templates/pages/exploration-editor-page/services/exploration-param-changes.service.spec.ts index 4d69ea1cafba..2d6841972701 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-param-changes.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-param-changes.service.spec.ts @@ -16,10 +16,10 @@ * @fileoverview Unit tests for the ExplorationParamChangesService. */ -import { TestBed } from '@angular/core/testing'; -import { ExplorationParamChangesService } from './exploration-param-changes.service'; -import { ExplorationPropertyService } from './exploration-property.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; +import {TestBed} from '@angular/core/testing'; +import {ExplorationParamChangesService} from './exploration-param-changes.service'; +import {ExplorationPropertyService} from './exploration-property.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; describe('Exploration Param Specs Service', () => { let epcs: ExplorationParamChangesService; @@ -27,15 +27,13 @@ describe('Exploration Param Specs Service', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [ - ExplorationPropertyService - ] + providers: [ExplorationPropertyService], }); epcs = TestBed.inject(ExplorationParamChangesService); }); - it('should test the child object properties', function() { + it('should test the child object properties', function () { expect(epcs.propertyName).toBe('param_changes'); }); }); diff --git a/core/templates/pages/exploration-editor-page/services/exploration-param-changes.service.ts b/core/templates/pages/exploration-editor-page/services/exploration-param-changes.service.ts index e536b4a14033..902fb9aa85be 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-param-changes.service.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-param-changes.service.ts @@ -17,18 +17,17 @@ * changes to parameters. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { ExplorationPropertyService } from 'pages/exploration-editor-page/services/exploration-property.service'; -import { AlertsService } from 'services/alerts.service'; -import { ChangeListService } from './change-list.service'; -import { LoggerService } from 'services/contextual/logger.service'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {ExplorationPropertyService} from 'pages/exploration-editor-page/services/exploration-property.service'; +import {AlertsService} from 'services/alerts.service'; +import {ChangeListService} from './change-list.service'; +import {LoggerService} from 'services/contextual/logger.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) -export class ExplorationParamChangesService extends - ExplorationPropertyService { +export class ExplorationParamChangesService extends ExplorationPropertyService { propertyName: string = 'param_changes'; constructor( @@ -40,5 +39,9 @@ export class ExplorationParamChangesService extends } } -angular.module('oppia').factory('ExplorationParamChangesService', - downgradeInjectable(ExplorationParamChangesService)); +angular + .module('oppia') + .factory( + 'ExplorationParamChangesService', + downgradeInjectable(ExplorationParamChangesService) + ); diff --git a/core/templates/pages/exploration-editor-page/services/exploration-param-specs.service.spec.ts b/core/templates/pages/exploration-editor-page/services/exploration-param-specs.service.spec.ts index 363ff2971228..ad6964732bfb 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-param-specs.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-param-specs.service.spec.ts @@ -16,10 +16,10 @@ * @fileoverview Unit tests for the ExplorationParamSpecsService. */ -import { TestBed } from '@angular/core/testing'; -import { ExplorationParamSpecsService } from './exploration-param-specs.service'; -import { ExplorationPropertyService } from './exploration-property.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; +import {TestBed} from '@angular/core/testing'; +import {ExplorationParamSpecsService} from './exploration-param-specs.service'; +import {ExplorationPropertyService} from './exploration-property.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; describe('Exploration Param Specs Service', () => { let epcs: ExplorationParamSpecsService; @@ -27,15 +27,13 @@ describe('Exploration Param Specs Service', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [ - ExplorationPropertyService - ] + providers: [ExplorationPropertyService], }); epcs = TestBed.inject(ExplorationParamSpecsService); }); - it('should test the child object properties', function() { + it('should test the child object properties', function () { expect(epcs.propertyName).toBe('param_specs'); }); }); diff --git a/core/templates/pages/exploration-editor-page/services/exploration-param-specs.service.ts b/core/templates/pages/exploration-editor-page/services/exploration-param-specs.service.ts index 2ccdd1c6b611..5ebe612712ef 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-param-specs.service.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-param-specs.service.ts @@ -17,19 +17,18 @@ * the specification of the parameters. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { ExplorationPropertyService } from 'pages/exploration-editor-page/services/exploration-property.service'; -import { AlertsService } from 'services/alerts.service'; -import { ChangeListService } from './change-list.service'; -import { LoggerService } from 'services/contextual/logger.service'; -import { ParamSpecs } from 'domain/exploration/ParamSpecsObjectFactory'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {ExplorationPropertyService} from 'pages/exploration-editor-page/services/exploration-property.service'; +import {AlertsService} from 'services/alerts.service'; +import {ChangeListService} from './change-list.service'; +import {LoggerService} from 'services/contextual/logger.service'; +import {ParamSpecs} from 'domain/exploration/ParamSpecsObjectFactory'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) -export class ExplorationParamSpecsService extends - ExplorationPropertyService { +export class ExplorationParamSpecsService extends ExplorationPropertyService { propertyName: string = 'param_specs'; // This property is initialized using init method and we need to do // non-null assertion. For more information, see @@ -44,5 +43,9 @@ export class ExplorationParamSpecsService extends } } -angular.module('oppia').factory('ExplorationParamSpecsService', - downgradeInjectable(ExplorationParamSpecsService)); +angular + .module('oppia') + .factory( + 'ExplorationParamSpecsService', + downgradeInjectable(ExplorationParamSpecsService) + ); diff --git a/core/templates/pages/exploration-editor-page/services/exploration-property.service.spec.ts b/core/templates/pages/exploration-editor-page/services/exploration-property.service.spec.ts index 5a572daebfbe..05b0ff54b683 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-property.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-property.service.spec.ts @@ -16,16 +16,19 @@ * @fileoverview Unit tests for ExplorationPropertyService. */ -import { TestBed, waitForAsync } from '@angular/core/testing'; +import {TestBed, waitForAsync} from '@angular/core/testing'; -import { ExplorationPropertyService } from 'pages/exploration-editor-page/services/exploration-property.service'; -import { ChangeListService } from 'pages/exploration-editor-page/services/change-list.service'; +import {ExplorationPropertyService} from 'pages/exploration-editor-page/services/exploration-property.service'; +import {ChangeListService} from 'pages/exploration-editor-page/services/change-list.service'; -import { ParamChangesObjectFactory } from 'domain/exploration/ParamChangesObjectFactory'; -import { ParamSpecs, ParamSpecsObjectFactory } from 'domain/exploration/ParamSpecsObjectFactory'; -import { ParamSpecObjectFactory } from 'domain/exploration/ParamSpecObjectFactory'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ParamChange } from 'domain/exploration/ParamChangeObjectFactory'; +import {ParamChangesObjectFactory} from 'domain/exploration/ParamChangesObjectFactory'; +import { + ParamSpecs, + ParamSpecsObjectFactory, +} from 'domain/exploration/ParamSpecsObjectFactory'; +import {ParamSpecObjectFactory} from 'domain/exploration/ParamSpecObjectFactory'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ParamChange} from 'domain/exploration/ParamChangeObjectFactory'; describe('Exploration Property Service', () => { let explorationPropertyService: ExplorationPropertyService; @@ -37,7 +40,7 @@ describe('Exploration Property Service', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] + imports: [HttpClientTestingModule], }); explorationPropertyService = TestBed.inject(ExplorationPropertyService); @@ -47,11 +50,13 @@ describe('Exploration Property Service', () => { paramSpecObjectFactory = TestBed.inject(ParamSpecObjectFactory); editExplorationPropertySpy = spyOn( - changeListService, 'editExplorationProperty').and.returnValue(); + changeListService, + 'editExplorationProperty' + ).and.returnValue(); }); - it('should create a new exploration properties object', function() { - expect(function() { + it('should create a new exploration properties object', function () { + expect(function () { explorationPropertyService.init('initial value'); }).toThrowError('Exploration property name cannot be null.'); @@ -61,21 +66,25 @@ describe('Exploration Property Service', () => { expect(explorationPropertyService.hasChanged()).toBe(false); }); - it('should overrides _normalize and _isValid methods', function() { + it('should overrides _normalize and _isValid methods', function () { let childToOverride = Object.create(explorationPropertyService); - childToOverride._isValid = function(value: string) { + childToOverride._isValid = function (value: string) { return !!value; }; - childToOverride._normalize = function(value: string) { + childToOverride._normalize = function (value: string) { return value; }; - let overrideIsValidSpy = spyOn(childToOverride, '_isValid').and - .callThrough(); - let overrideNormalizeSpy = spyOn(childToOverride, '_normalize').and - .callThrough(); + let overrideIsValidSpy = spyOn( + childToOverride, + '_isValid' + ).and.callThrough(); + let overrideNormalizeSpy = spyOn( + childToOverride, + '_normalize' + ).and.callThrough(); childToOverride.propertyName = 'property_1'; childToOverride.saveDisplayedValue(); @@ -97,11 +106,11 @@ describe('Exploration Property Service', () => { explorationPropertyService.hasChanged(); }); - it('should not save the displayed value when it\'s empty', function() { + it("should not save the displayed value when it's empty", function () { let child = Object.create(explorationPropertyService); child.propertyName = 'property_1'; - child._isValid = function(value: string) { + child._isValid = function (value: string) { if (!value) { throw new Error('this.displayed should have a valid value.'); } @@ -109,7 +118,7 @@ describe('Exploration Property Service', () => { }; child.init(); - expect(function() { + expect(function () { child.saveDisplayedValue(); }).toThrowError('this.displayed should have a valid value.'); @@ -119,35 +128,39 @@ describe('Exploration Property Service', () => { expect(child.hasChanged()).toBe(false); }); - it('should save displayed value when is ParamChanges object', function() { + it('should save displayed value when is ParamChanges object', function () { let child = Object.create(explorationPropertyService); child.propertyName = 'param_changes'; - child._normalize = function(paramChanges: ParamChange[]) { + child._normalize = function (paramChanges: ParamChange[]) { // Changing paramChanges so hasChanged() turns to be true on line 87. - paramChanges.forEach(function(paramChange: ParamChange) { + paramChanges.forEach(function (paramChange: ParamChange) { paramChange.resetCustomizationArgs(); }); return paramChanges; }; - let normalizeSpy = spyOn(child, '_normalize').and - .callThrough(); - - child.init(paramChangesObjectFactory.createFromBackendList([{ - customization_args: { - parse_with_jinja: true, - value: '' - }, - generator_id: 'Copier', - name: 'Param change 1' - }, { - customization_args: { - parse_with_jinja: true, - value: '' - }, - generator_id: 'RandomSelector', - name: 'Param change 2' - }])); + let normalizeSpy = spyOn(child, '_normalize').and.callThrough(); + + child.init( + paramChangesObjectFactory.createFromBackendList([ + { + customization_args: { + parse_with_jinja: true, + value: '', + }, + generator_id: 'Copier', + name: 'Param change 1', + }, + { + customization_args: { + parse_with_jinja: true, + value: '', + }, + generator_id: 'RandomSelector', + name: 'Param change 2', + }, + ]) + ); child.saveDisplayedValue(); expect(normalizeSpy).toHaveBeenCalled(); @@ -155,27 +168,28 @@ describe('Exploration Property Service', () => { expect(child.hasChanged()).toBe(false); }); - it('should save displayed value when is ParamSpecs object', function() { + it('should save displayed value when is ParamSpecs object', function () { let child = Object.create(explorationPropertyService); child.propertyName = 'param_specs'; - child._normalize = function(paramSpecs: ParamSpecs) { + child._normalize = function (paramSpecs: ParamSpecs) { // Changing paramSpecs so hasChanged() turns to be true on line 87. let paramSpec = paramSpecObjectFactory.createDefault(); paramSpecs.addParamIfNew('z', paramSpec); return paramSpecs; }; - let normalizeSpy = spyOn(child, '_normalize').and - .callThrough(); - - child.init(paramSpecsObjectFactory.createFromBackendDict({ - x: { - obj_type: 'UnicodeString' - }, - y: { - obj_type: 'UnicodeString' - } - })); + let normalizeSpy = spyOn(child, '_normalize').and.callThrough(); + + child.init( + paramSpecsObjectFactory.createFromBackendDict({ + x: { + obj_type: 'UnicodeString', + }, + y: { + obj_type: 'UnicodeString', + }, + }) + ); child.saveDisplayedValue(); expect(normalizeSpy).toHaveBeenCalled(); @@ -183,25 +197,31 @@ describe('Exploration Property Service', () => { expect(child.hasChanged()).toBe(false); }); - it('should return stream of observables when exploration' + - ' property is changed', () => { - let count = 0; - let subscription = explorationPropertyService.onExplorationPropertyChanged; - subscription - .subscribe((event) => { + it( + 'should return stream of observables when exploration' + + ' property is changed', + () => { + let count = 0; + let subscription = + explorationPropertyService.onExplorationPropertyChanged; + subscription.subscribe(event => { expect(event).toBe(null); count++; }); - explorationPropertyService._explorationPropertyChangedEventEmitter - .emit(null); - explorationPropertyService._explorationPropertyChangedEventEmitter - .emit(null); - explorationPropertyService._explorationPropertyChangedEventEmitter - .emit(null); - - waitForAsync(() => { - expect(count).toBe(3); - }); - }); + explorationPropertyService._explorationPropertyChangedEventEmitter.emit( + null + ); + explorationPropertyService._explorationPropertyChangedEventEmitter.emit( + null + ); + explorationPropertyService._explorationPropertyChangedEventEmitter.emit( + null + ); + + waitForAsync(() => { + expect(count).toBe(3); + }); + } + ); }); diff --git a/core/templates/pages/exploration-editor-page/services/exploration-property.service.ts b/core/templates/pages/exploration-editor-page/services/exploration-property.service.ts index 38ab48acd708..9b011b3f1dc9 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-property.service.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-property.service.ts @@ -18,32 +18,34 @@ * with base class as ExplorationPropertyService. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { EventEmitter } from '@angular/core'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {EventEmitter} from '@angular/core'; +import {Injectable} from '@angular/core'; import cloneDeep from 'lodash/cloneDeep'; -import { ChangeListService } from 'pages/exploration-editor-page/services/change-list.service'; -import { AlertsService } from 'services/alerts.service'; -import { LoggerService } from 'services/contextual/logger.service'; -import { ParamChange, ParamChangeBackendDict } from 'domain/exploration/ParamChangeObjectFactory'; -import { ParamSpecs } from 'domain/exploration/ParamSpecsObjectFactory'; - -export type ExplorationPropertyValues = ( - null | - number | - string | - string[] | - boolean | - ParamChange | - ParamChange[] | - ParamSpecs | - ParamChangeBackendDict | - ParamChangeBackendDict[] -); +import {ChangeListService} from 'pages/exploration-editor-page/services/change-list.service'; +import {AlertsService} from 'services/alerts.service'; +import {LoggerService} from 'services/contextual/logger.service'; +import { + ParamChange, + ParamChangeBackendDict, +} from 'domain/exploration/ParamChangeObjectFactory'; +import {ParamSpecs} from 'domain/exploration/ParamSpecsObjectFactory'; + +export type ExplorationPropertyValues = + | null + | number + | string + | string[] + | boolean + | ParamChange + | ParamChange[] + | ParamSpecs + | ParamChangeBackendDict + | ParamChangeBackendDict[]; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ExplorationPropertyService { // These properties are initialized using private methods and we need to do @@ -61,7 +63,7 @@ export class ExplorationPropertyService { constructor( protected alertsService: AlertsService, protected changeListService: ChangeListService, - protected loggerService: LoggerService, + protected loggerService: LoggerService ) {} private BACKEND_CONVERSIONS = { @@ -81,7 +83,8 @@ export class ExplorationPropertyService { } this.loggerService.info( - 'Initializing exploration ' + this.propertyName + ': ' + value); + 'Initializing exploration ' + this.propertyName + ': ' + value + ); // The current value of the property (which may not have been saved to // the frontend yet). In general, this will be bound directly to the UI. @@ -134,18 +137,19 @@ export class ExplorationPropertyService { let oldBackendValue = cloneDeep(this.savedMemento); const that = this; if (this.BACKEND_CONVERSIONS.hasOwnProperty(this.propertyName)) { - newBackendValue = - this.BACKEND_CONVERSIONS[ - this.propertyName as keyof typeof that.BACKEND_CONVERSIONS - ](this.displayed as ParamChange[] & ParamChange); - oldBackendValue = - this.BACKEND_CONVERSIONS[ - this.propertyName as keyof typeof that.BACKEND_CONVERSIONS - ](this.savedMemento as ParamChange[] & ParamChange); + newBackendValue = this.BACKEND_CONVERSIONS[ + this.propertyName as keyof typeof that.BACKEND_CONVERSIONS + ](this.displayed as ParamChange[] & ParamChange); + oldBackendValue = this.BACKEND_CONVERSIONS[ + this.propertyName as keyof typeof that.BACKEND_CONVERSIONS + ](this.savedMemento as ParamChange[] & ParamChange); } this.changeListService.editExplorationProperty( - this.propertyName, newBackendValue as string, oldBackendValue as string); + this.propertyName, + newBackendValue as string, + oldBackendValue as string + ); this.savedMemento = cloneDeep(this.displayed); this._explorationPropertyChangedEventEmitter.emit(); @@ -161,6 +165,9 @@ export class ExplorationPropertyService { } } -angular.module('oppia').factory( - 'ExplorationPropertyService', downgradeInjectable( - ExplorationPropertyService)); +angular + .module('oppia') + .factory( + 'ExplorationPropertyService', + downgradeInjectable(ExplorationPropertyService) + ); diff --git a/core/templates/pages/exploration-editor-page/services/exploration-rights-backend-api.service.spec.ts b/core/templates/pages/exploration-editor-page/services/exploration-rights-backend-api.service.spec.ts index 2eb6c08f8450..e94a076da63e 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-rights-backend-api.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-rights-backend-api.service.spec.ts @@ -16,10 +16,17 @@ * @fileoverview Unit tests for Exploration Rights Backend Api Service */ -import { HttpClientTestingModule, HttpTestingController } from - '@angular/common/http/testing'; -import { fakeAsync, flushMicrotasks, TestBed, waitForAsync} from '@angular/core/testing'; -import { ExplorationRightsBackendApiService } from './exploration-rights-backend-api.service'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import { + fakeAsync, + flushMicrotasks, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import {ExplorationRightsBackendApiService} from './exploration-rights-backend-api.service'; describe('Exploration Rights Backend Api Service', () => { let service: ExplorationRightsBackendApiService; @@ -30,7 +37,7 @@ describe('Exploration Rights Backend Api Service', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [ExplorationRightsBackendApiService] + providers: [ExplorationRightsBackendApiService], }); httpTestingController = TestBed.inject(HttpTestingController); service = TestBed.inject(ExplorationRightsBackendApiService); @@ -40,20 +47,26 @@ describe('Exploration Rights Backend Api Service', () => { httpTestingController.verify(); }); - it('should successfully send put http request when' + - ' makeCommunityOwnedPutData called', fakeAsync( - () => { + it( + 'should successfully send put http request when' + + ' makeCommunityOwnedPutData called', + fakeAsync(() => { let requestData = { version: 3, - make_community_owned: true + make_community_owned: true, }; - service.makeCommunityOwnedPutData( - 'oppia12345', requestData.version, requestData.make_community_owned - ).then(successHandler, failHandler); + service + .makeCommunityOwnedPutData( + 'oppia12345', + requestData.version, + requestData.make_community_owned + ) + .then(successHandler, failHandler); let req = httpTestingController.expectOne( - '/createhandler/rights/oppia12345'); + '/createhandler/rights/oppia12345' + ); expect(req.request.method).toEqual('PUT'); expect(req.request.body).toEqual(requestData); req.flush([]); @@ -65,22 +78,28 @@ describe('Exploration Rights Backend Api Service', () => { }) ); - it('should successfully send put http request when' + - ' when saveRoleChangesPutData called', fakeAsync( - () => { + it( + 'should successfully send put http request when' + + ' when saveRoleChangesPutData called', + fakeAsync(() => { let requestData = { version: 3, new_member_role: 'editor', - new_member_username: 'usernameForEditorRole' + new_member_username: 'usernameForEditorRole', }; - service.saveRoleChangesPutData( - 'oppia12345', requestData.version, requestData.new_member_role, - requestData.new_member_username - ).then(successHandler, failHandler); + service + .saveRoleChangesPutData( + 'oppia12345', + requestData.version, + requestData.new_member_role, + requestData.new_member_username + ) + .then(successHandler, failHandler); let req = httpTestingController.expectOne( - '/createhandler/rights/oppia12345'); + '/createhandler/rights/oppia12345' + ); expect(req.request.method).toEqual('PUT'); expect(req.request.body).toEqual(requestData); req.flush([]); @@ -92,20 +111,26 @@ describe('Exploration Rights Backend Api Service', () => { }) ); - it('should successfully send put http request' + - ' when setViewabilityPutData called', fakeAsync( - () => { + it( + 'should successfully send put http request' + + ' when setViewabilityPutData called', + fakeAsync(() => { let requestData = { version: 3, - viewableIfPrivate: true + viewableIfPrivate: true, }; - service.setViewabilityPutData( - 'oppia12345', requestData.version, requestData.viewableIfPrivate - ).then(successHandler, failHandler); + service + .setViewabilityPutData( + 'oppia12345', + requestData.version, + requestData.viewableIfPrivate + ) + .then(successHandler, failHandler); let req = httpTestingController.expectOne( - '/createhandler/rights/oppia12345'); + '/createhandler/rights/oppia12345' + ); expect(req.request.method).toEqual('PUT'); expect(req.request.body).toEqual(requestData); req.flush([]); @@ -117,62 +142,73 @@ describe('Exploration Rights Backend Api Service', () => { }) ); - it('should successfully send put http request' + - 'when publishPutData called', fakeAsync(() => { - let requestData = { - make_public: true - }; + it( + 'should successfully send put http request' + 'when publishPutData called', + fakeAsync(() => { + let requestData = { + make_public: true, + }; - service.publishPutData( - 'oppia12345', requestData.make_public - ).then(successHandler, failHandler); + service + .publishPutData('oppia12345', requestData.make_public) + .then(successHandler, failHandler); - let req = httpTestingController.expectOne( - '/createhandler/status/oppia12345'); - expect(req.request.method).toEqual('PUT'); - expect(req.request.body).toEqual(requestData); - req.flush([]); + let req = httpTestingController.expectOne( + '/createhandler/status/oppia12345' + ); + expect(req.request.method).toEqual('PUT'); + expect(req.request.body).toEqual(requestData); + req.flush([]); - flushMicrotasks(); + flushMicrotasks(); - expect(successHandler).toHaveBeenCalled(); - expect(failHandler).not.toHaveBeenCalled(); - }) + expect(successHandler).toHaveBeenCalled(); + expect(failHandler).not.toHaveBeenCalled(); + }) ); - it('should successfully send put http request' + - ' when saveModeratorChangeToBackendAsyncPutData called', fakeAsync(() => { - let requestData = { - version: 3, - email_body: '' - }; + it( + 'should successfully send put http request' + + ' when saveModeratorChangeToBackendAsyncPutData called', + fakeAsync(() => { + let requestData = { + version: 3, + email_body: '', + }; - service.saveModeratorChangeToBackendAsyncPutData( - 'oppia12345', requestData.version, requestData.email_body - ).then(successHandler, failHandler); + service + .saveModeratorChangeToBackendAsyncPutData( + 'oppia12345', + requestData.version, + requestData.email_body + ) + .then(successHandler, failHandler); - let req = httpTestingController.expectOne( - '/createhandler/moderatorrights/oppia12345'); - expect(req.request.method).toEqual('PUT'); - expect(req.request.body).toEqual(requestData); - req.flush([]); + let req = httpTestingController.expectOne( + '/createhandler/moderatorrights/oppia12345' + ); + expect(req.request.method).toEqual('PUT'); + expect(req.request.body).toEqual(requestData); + req.flush([]); - flushMicrotasks(); + flushMicrotasks(); - expect(successHandler).toHaveBeenCalled(); - expect(failHandler).not.toHaveBeenCalled(); - }) + expect(successHandler).toHaveBeenCalled(); + expect(failHandler).not.toHaveBeenCalled(); + }) ); - it('should successfully send Delete http request' + - ' when removeRoleAsyncDeleteData called', fakeAsync( - () => { - service.removeRoleAsyncDeleteData( - 'oppia12345', 'userNameToDeleteTheUser' - ).then(successHandler, failHandler); + it( + 'should successfully send Delete http request' + + ' when removeRoleAsyncDeleteData called', + fakeAsync(() => { + service + .removeRoleAsyncDeleteData('oppia12345', 'userNameToDeleteTheUser') + .then(successHandler, failHandler); let req = httpTestingController.expectOne( - '/createhandler/rights/oppia12345?username=userNameToDeleteTheUser'); + '/createhandler/rights/oppia12345?username=userNameToDeleteTheUser' + ); expect(req.request.method).toEqual('DELETE'); req.flush([]); @@ -183,44 +219,53 @@ describe('Exploration Rights Backend Api Service', () => { }) ); - it('should successfully send http Post request' + - ' when assignVoiceArtistRoleAsyncPostData called', fakeAsync(() => { - let requestData = { - username: 'usernameForAssignVoiceArtistRole' - }; - - service.assignVoiceArtistRoleAsyncPostData( - 'oppia12345', requestData.username - ).then(successHandler, failHandler); - - let req = httpTestingController.expectOne( - '/voice_artist_management_handler/exploration/oppia12345'); - - expect(req.request.method).toEqual('POST'); - expect(req.request.body).toEqual(requestData); - req.flush([]); - flushMicrotasks(); - expect(successHandler).toHaveBeenCalled(); - expect(failHandler).not.toHaveBeenCalled(); - }) + it( + 'should successfully send http Post request' + + ' when assignVoiceArtistRoleAsyncPostData called', + fakeAsync(() => { + let requestData = { + username: 'usernameForAssignVoiceArtistRole', + }; + + service + .assignVoiceArtistRoleAsyncPostData('oppia12345', requestData.username) + .then(successHandler, failHandler); + + let req = httpTestingController.expectOne( + '/voice_artist_management_handler/exploration/oppia12345' + ); + + expect(req.request.method).toEqual('POST'); + expect(req.request.body).toEqual(requestData); + req.flush([]); + flushMicrotasks(); + expect(successHandler).toHaveBeenCalled(); + expect(failHandler).not.toHaveBeenCalled(); + }) ); - it('should successfully send Delete http request' + - ' when removeVoiceArtistRoleAsyncDeleteData called', fakeAsync(() => { - service.removeVoiceArtistRoleAsyncDeleteData( - 'oppia12345', 'usernameForRemoveVoiceArtistRole' - ).then(successHandler, failHandler); + it( + 'should successfully send Delete http request' + + ' when removeVoiceArtistRoleAsyncDeleteData called', + fakeAsync(() => { + service + .removeVoiceArtistRoleAsyncDeleteData( + 'oppia12345', + 'usernameForRemoveVoiceArtistRole' + ) + .then(successHandler, failHandler); - let req = httpTestingController.expectOne( - '/voice_artist_management_handler/' + - 'exploration/oppia12345?voice_artist=usernameForRemoveVoiceArtistRole'); - expect(req.request.method).toEqual('DELETE'); - req.flush([]); + let req = httpTestingController.expectOne( + '/voice_artist_management_handler/' + + 'exploration/oppia12345?voice_artist=usernameForRemoveVoiceArtistRole' + ); + expect(req.request.method).toEqual('DELETE'); + req.flush([]); - flushMicrotasks(); + flushMicrotasks(); - expect(successHandler).toHaveBeenCalled(); - expect(failHandler).not.toHaveBeenCalled(); - }) + expect(successHandler).toHaveBeenCalled(); + expect(failHandler).not.toHaveBeenCalled(); + }) ); }); diff --git a/core/templates/pages/exploration-editor-page/services/exploration-rights-backend-api.service.ts b/core/templates/pages/exploration-editor-page/services/exploration-rights-backend-api.service.ts index a28825e4b27a..e39f6dbab8e6 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-rights-backend-api.service.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-rights-backend-api.service.ts @@ -16,126 +16,153 @@ * @fileoverview Backend api service for Exploration Rights Service; */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; export interface ExplorationRightsBackendData { rights: { - 'cloned_from': string; - 'status': string; - 'community_owned': boolean; - 'owner_names': string[]; - 'editor_names': string[]; - 'voice_artist_names': string[]; - 'viewer_names': string[]; - 'viewable_if_private': boolean; + cloned_from: string; + status: string; + community_owned: boolean; + owner_names: string[]; + editor_names: string[]; + voice_artist_names: string[]; + viewer_names: string[]; + viewable_if_private: boolean; }; } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ExplorationRightsBackendApiService { - constructor( - private http: HttpClient, - ) {} + constructor(private http: HttpClient) {} async makeCommunityOwnedPutData( - explorationId: string, version: number, makeCommunityOwned: boolean): - Promise { + explorationId: string, + version: number, + makeCommunityOwned: boolean + ): Promise { const requestUrl = '/createhandler/rights/' + explorationId; - return this.http.put(requestUrl, { - version: version, - make_community_owned: makeCommunityOwned - }).toPromise(); + return this.http + .put(requestUrl, { + version: version, + make_community_owned: makeCommunityOwned, + }) + .toPromise(); } async saveRoleChangesPutData( - explorationId: string, - version: number, - newMemberRole: string, - newMemberUsername: string + explorationId: string, + version: number, + newMemberRole: string, + newMemberUsername: string ): Promise { const requestUrl = '/createhandler/rights/' + explorationId; - return this.http.put(requestUrl, { - version: version, - new_member_role: newMemberRole, - new_member_username: newMemberUsername - }).toPromise(); + return this.http + .put(requestUrl, { + version: version, + new_member_role: newMemberRole, + new_member_username: newMemberUsername, + }) + .toPromise(); } async setViewabilityPutData( - explorationId: string, version: number, viewableIfPrivate: boolean + explorationId: string, + version: number, + viewableIfPrivate: boolean ): Promise { const requestUrl = '/createhandler/rights/' + explorationId; - return this.http.put(requestUrl, { - version: version, - viewableIfPrivate: viewableIfPrivate - }).toPromise(); + return this.http + .put(requestUrl, { + version: version, + viewableIfPrivate: viewableIfPrivate, + }) + .toPromise(); } async publishPutData( - explorationId: string, makePublic: boolean + explorationId: string, + makePublic: boolean ): Promise { const requestUrl = '/createhandler/status/' + explorationId; - return this.http.put(requestUrl, { - make_public: makePublic - }).toPromise(); + return this.http + .put(requestUrl, { + make_public: makePublic, + }) + .toPromise(); } async saveModeratorChangeToBackendAsyncPutData( - explorationId: string, version: number, emailBody: string + explorationId: string, + version: number, + emailBody: string ): Promise { const requestUrl = '/createhandler/moderatorrights/' + explorationId; - return this.http.put(requestUrl, { - email_body: emailBody, - version: version - }).toPromise(); + return this.http + .put(requestUrl, { + email_body: emailBody, + version: version, + }) + .toPromise(); } async removeRoleAsyncDeleteData( - explorationId: string, memberUsername: string + explorationId: string, + memberUsername: string ): Promise { const requestUrl = '/createhandler/rights/' + explorationId; - return this.http.delete(requestUrl, { - params: { - username: memberUsername - } - }).toPromise(); + return this.http + .delete(requestUrl, { + params: { + username: memberUsername, + }, + }) + .toPromise(); } async assignVoiceArtistRoleAsyncPostData( - explorationId: string, newVoiceArtistUsername: string + explorationId: string, + newVoiceArtistUsername: string ): Promise { const requestUrl = '/voice_artist_management_handler/' + 'exploration/' + explorationId; - return this.http.post(requestUrl, { - username: newVoiceArtistUsername - }).toPromise(); + return this.http + .post(requestUrl, { + username: newVoiceArtistUsername, + }) + .toPromise(); } async removeVoiceArtistRoleAsyncDeleteData( - explorationId: string, voiceArtistUsername: string + explorationId: string, + voiceArtistUsername: string ): Promise { const requestUrl = '/voice_artist_management_handler/' + 'exploration/' + explorationId; - return this.http.delete(requestUrl, { - params: { - voice_artist: voiceArtistUsername - } - }).toPromise(); + return this.http + .delete(requestUrl, { + params: { + voice_artist: voiceArtistUsername, + }, + }) + .toPromise(); } } -angular.module('oppia').factory( - 'ExplorationRightsBackendApiService', - downgradeInjectable(ExplorationRightsBackendApiService)); +angular + .module('oppia') + .factory( + 'ExplorationRightsBackendApiService', + downgradeInjectable(ExplorationRightsBackendApiService) + ); diff --git a/core/templates/pages/exploration-editor-page/services/exploration-rights.service.spec.ts b/core/templates/pages/exploration-editor-page/services/exploration-rights.service.spec.ts index 2930bf4474c4..ab21abb30a7a 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-rights.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-rights.service.spec.ts @@ -17,15 +17,21 @@ * of the exploration editor page. */ -import { TestBed, fakeAsync, tick } from '@angular/core/testing'; -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { AlertsService } from 'services/alerts.service'; -import { CsrfTokenService } from 'services/csrf-token.service'; -import { ExplorationDataService } from 'pages/exploration-editor-page/services/exploration-data.service'; -import { ExplorationRightsService } from './exploration-rights.service'; -import { ExplorationRightsBackendApiService, ExplorationRightsBackendData } from './exploration-rights-backend-api.service'; +import {TestBed, fakeAsync, tick} from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {AlertsService} from 'services/alerts.service'; +import {CsrfTokenService} from 'services/csrf-token.service'; +import {ExplorationDataService} from 'pages/exploration-editor-page/services/exploration-data.service'; +import {ExplorationRightsService} from './exploration-rights.service'; +import { + ExplorationRightsBackendApiService, + ExplorationRightsBackendData, +} from './exploration-rights-backend-api.service'; import cloneDeep from 'lodash/cloneDeep'; -import { HttpErrorResponse } from '@angular/common/http'; +import {HttpErrorResponse} from '@angular/common/http'; describe('Exploration rights service', () => { let ers: ExplorationRightsService; @@ -46,8 +52,8 @@ describe('Exploration rights service', () => { status: 'private', cloned_from: 'e1234', community_owned: true, - viewable_if_private: true - } + viewable_if_private: true, + }, }; beforeEach(() => { @@ -59,12 +65,12 @@ describe('Exploration rights service', () => { useValue: { explorationId: '12345', data: { - version: 1 - } - } + version: 1, + }, + }, }, - ExplorationRightsBackendApiService - ] + ExplorationRightsBackendApiService, + ], }); als = TestBed.inject(AlertsService); @@ -72,8 +78,9 @@ describe('Exploration rights service', () => { ers = TestBed.inject(ExplorationRightsService); explorationDataService = TestBed.inject(ExplorationDataService); httpTestingController = TestBed.inject(HttpTestingController); - explorationRightsBackendApiService = - TestBed.inject(ExplorationRightsBackendApiService); + explorationRightsBackendApiService = TestBed.inject( + ExplorationRightsBackendApiService + ); }); beforeEach(() => { @@ -113,15 +120,14 @@ describe('Exploration rights service', () => { expect(ers.ownerNames).toEqual(serviceData.rights.owner_names); expect(ers.editorNames).toEqual(serviceData.rights.editor_names); - expect(ers.voiceArtistNames).toEqual( - serviceData.rights.voice_artist_names); + expect(ers.voiceArtistNames).toEqual(serviceData.rights.voice_artist_names); expect(ers.viewerNames).toEqual(serviceData.rights.viewer_names); expect(ers.isPrivate()).toEqual(true); expect(ers.clonedFrom()).toEqual(serviceData.rights.cloned_from); - expect(ers.isCommunityOwned()).toBe( - serviceData.rights.community_owned); + expect(ers.isCommunityOwned()).toBe(serviceData.rights.community_owned); expect(ers.viewableIfPrivate()).toBe( - serviceData.rights.viewable_if_private); + serviceData.rights.viewable_if_private + ); }); it('should throw error if version of data is null', fakeAsync(() => { @@ -176,55 +182,52 @@ describe('Exploration rights service', () => { expect(ers.isPublic()).toBe(true); }); - it('should reports correcty if exploration rights is viewable when private', - () => { - ers.init(['abc'], [], [], [], 'private', 'e1234', true, true); - expect(ers.viewableIfPrivate()).toBe(true); + it('should reports correcty if exploration rights is viewable when private', () => { + ers.init(['abc'], [], [], [], 'private', 'e1234', true, true); + expect(ers.viewableIfPrivate()).toBe(true); - ers.init(['abc'], [], [], [], 'private', 'e1234', false, false); - expect(ers.viewableIfPrivate()).toBe(false); - }); + ers.init(['abc'], [], [], [], 'private', 'e1234', false, false); + expect(ers.viewableIfPrivate()).toBe(false); + }); it('should change community owned to true', fakeAsync(() => { serviceData.rights.community_owned = true; spyOn( explorationRightsBackendApiService, - 'makeCommunityOwnedPutData').and.returnValue( - Promise.resolve(serviceData)); + 'makeCommunityOwnedPutData' + ).and.returnValue(Promise.resolve(serviceData)); ers.init(['abc'], [], [], [], 'private', 'e1234', false, true); ers.makeCommunityOwned(); tick(); - expect(explorationRightsBackendApiService.makeCommunityOwnedPutData) - .toHaveBeenCalled(); + expect( + explorationRightsBackendApiService.makeCommunityOwnedPutData + ).toHaveBeenCalled(); expect(ers.isCommunityOwned()).toBe(true); })); - it('should use reject handler when changing community owned to true fails', - fakeAsync(() => { - spyOn( - explorationRightsBackendApiService, - 'makeCommunityOwnedPutData').and.returnValue( - Promise.reject()); - - ers.init( - ['abc'], [], [], [], 'private', 'e1234', false, true); - ers.makeCommunityOwned().then( - successHandler, failHandler); - tick(); + it('should use reject handler when changing community owned to true fails', fakeAsync(() => { + spyOn( + explorationRightsBackendApiService, + 'makeCommunityOwnedPutData' + ).and.returnValue(Promise.reject()); + + ers.init(['abc'], [], [], [], 'private', 'e1234', false, true); + ers.makeCommunityOwned().then(successHandler, failHandler); + tick(); - expect(ers.isCommunityOwned()).toBe(false); - expect(clearWarningsSpy).not.toHaveBeenCalled(); - expect(successHandler).not.toHaveBeenCalled(); - expect(failHandler).toHaveBeenCalled(); - })); + expect(ers.isCommunityOwned()).toBe(false); + expect(clearWarningsSpy).not.toHaveBeenCalled(); + expect(successHandler).not.toHaveBeenCalled(); + expect(failHandler).toHaveBeenCalled(); + })); it('should change exploration right viewability', fakeAsync(() => { serviceData.rights.viewable_if_private = true; spyOn( explorationRightsBackendApiService, - 'setViewabilityPutData').and.returnValue( - Promise.resolve(serviceData)); + 'setViewabilityPutData' + ).and.returnValue(Promise.resolve(serviceData)); ers.setViewability(true); tick(); @@ -232,45 +235,41 @@ describe('Exploration rights service', () => { expect(ers.viewableIfPrivate()).toBe(true); })); - it('should use reject when changing exploration right viewability fails', - fakeAsync(() => { - spyOn( - explorationRightsBackendApiService, - 'setViewabilityPutData').and.returnValue( - Promise.reject()); - - ers.init( - ['abc'], [], [], [], 'private', 'e1234', false, false); - ers.setViewability(true).then( - successHandler, failHandler); - tick(); + it('should use reject when changing exploration right viewability fails', fakeAsync(() => { + spyOn( + explorationRightsBackendApiService, + 'setViewabilityPutData' + ).and.returnValue(Promise.reject()); + + ers.init(['abc'], [], [], [], 'private', 'e1234', false, false); + ers.setViewability(true).then(successHandler, failHandler); + tick(); - expect(ers.viewableIfPrivate()).toBe(false); - expect(clearWarningsSpy).not.toHaveBeenCalled(); - expect(successHandler).not.toHaveBeenCalled(); - expect(failHandler).toHaveBeenCalled(); - })); + expect(ers.viewableIfPrivate()).toBe(false); + expect(clearWarningsSpy).not.toHaveBeenCalled(); + expect(successHandler).not.toHaveBeenCalled(); + expect(failHandler).toHaveBeenCalled(); + })); it('should save a new member', fakeAsync(() => { serviceData.rights.viewer_names = ['viewerName']; spyOn( explorationRightsBackendApiService, - 'saveRoleChangesPutData').and.returnValue( - Promise.resolve(serviceData)); + 'saveRoleChangesPutData' + ).and.returnValue(Promise.resolve(serviceData)); ers.saveRoleChanges('newUser', 'viewer'); tick(); - expect(ers.viewerNames).toEqual( - ['viewerName']); + expect(ers.viewerNames).toEqual(['viewerName']); })); it('should remove existing user', fakeAsync(() => { serviceData.rights.viewer_names = ['newUser']; spyOn( explorationRightsBackendApiService, - 'removeRoleAsyncDeleteData').and.returnValue( - Promise.resolve(serviceData)); + 'removeRoleAsyncDeleteData' + ).and.returnValue(Promise.resolve(serviceData)); ers.removeRoleAsync('newUser').then(successHandler, failHandler); tick(); @@ -278,22 +277,22 @@ describe('Exploration rights service', () => { expect(successHandler).toHaveBeenCalled(); expect(failHandler).not.toHaveBeenCalled(); - expect(ers.viewerNames).toEqual( - serviceData.rights.viewer_names); + expect(ers.viewerNames).toEqual(serviceData.rights.viewer_names); })); it('should save a new voice artist', fakeAsync(() => { serviceData.rights.voice_artist_names = ['voiceArtist']; spyOn( explorationRightsBackendApiService, - 'assignVoiceArtistRoleAsyncPostData').and.returnValue( - Promise.resolve(serviceData)); + 'assignVoiceArtistRoleAsyncPostData' + ).and.returnValue(Promise.resolve(serviceData)); ers.init(['abc'], [], [], [], 'public', '1234', true, false); expect(ers.voiceArtistNames).toEqual([]); - ers.assignVoiceArtistRoleAsync('voiceArtist').then( - successHandler, failHandler); + ers + .assignVoiceArtistRoleAsync('voiceArtist') + .then(successHandler, failHandler); tick(); expect(ers.voiceArtistNames).toEqual(['voiceArtist']); @@ -304,15 +303,16 @@ describe('Exploration rights service', () => { spyOn( explorationRightsBackendApiService, - 'removeVoiceArtistRoleAsyncDeleteData').and.returnValue( - Promise.resolve(serviceData)); + 'removeVoiceArtistRoleAsyncDeleteData' + ).and.returnValue(Promise.resolve(serviceData)); ers.init(['abc'], [], ['voiceArtist'], [], 'public', '1234', true, false); tick(); expect(ers.voiceArtistNames).toEqual(['voiceArtist']); - ers.removeVoiceArtistRoleAsync('voiceArtist').then( - successHandler, failHandler); + ers + .removeVoiceArtistRoleAsync('voiceArtist') + .then(successHandler, failHandler); tick(); expect(successHandler).toHaveBeenCalled(); @@ -323,25 +323,23 @@ describe('Exploration rights service', () => { it('should reject handler when saving a voice artist fails', fakeAsync(() => { const errorMessage = 'An error occurred while assigning voice artist role.'; - const responseError = { error: { error: errorMessage } }; + const responseError = {error: {error: errorMessage}}; spyOn( explorationRightsBackendApiService, - 'assignVoiceArtistRoleAsyncPostData').and.returnValue( - Promise.reject(responseError)); + 'assignVoiceArtistRoleAsyncPostData' + ).and.returnValue(Promise.reject(responseError)); spyOn(als, 'addWarning'); - ers.assignVoiceArtistRoleAsync('voiceArtist').then( - successHandler, failHandler); + ers + .assignVoiceArtistRoleAsync('voiceArtist') + .then(successHandler, failHandler); tick(); expect( - explorationRightsBackendApiService - .assignVoiceArtistRoleAsyncPostData + explorationRightsBackendApiService.assignVoiceArtistRoleAsyncPostData ).toHaveBeenCalled(); - expect(als.addWarning).toHaveBeenCalledWith( - errorMessage - ); + expect(als.addWarning).toHaveBeenCalledWith(errorMessage); })); it('should check user already has roles', () => { @@ -395,12 +393,10 @@ describe('Exploration rights service', () => { it('should reject handler when saving a new member fails', fakeAsync(() => { spyOn( explorationRightsBackendApiService, - 'saveRoleChangesPutData').and.returnValue( - Promise.reject()); + 'saveRoleChangesPutData' + ).and.returnValue(Promise.reject()); - ers.saveRoleChanges( - 'newUser', 'viewer').then( - successHandler, failHandler); + ers.saveRoleChanges('newUser', 'viewer').then(successHandler, failHandler); tick(); @@ -413,10 +409,9 @@ describe('Exploration rights service', () => { let sampleDataResultsCopy = angular.copy(serviceData); sampleDataResultsCopy.rights.status = 'public'; - spyOn( - explorationRightsBackendApiService, - 'publishPutData').and.returnValue( - Promise.resolve(sampleDataResultsCopy)); + spyOn(explorationRightsBackendApiService, 'publishPutData').and.returnValue( + Promise.resolve(sampleDataResultsCopy) + ); ers.publish(); tick(); @@ -424,75 +419,69 @@ describe('Exploration rights service', () => { expect(ers.isPublic()).toBe(true); })); - it('should call reject handler when making exploration rights public fails', - fakeAsync(() => { - spyOn( - explorationRightsBackendApiService, - 'publishPutData').and.returnValue( - Promise.reject(new HttpErrorResponse({ + it('should call reject handler when making exploration rights public fails', fakeAsync(() => { + spyOn(explorationRightsBackendApiService, 'publishPutData').and.returnValue( + Promise.reject( + new HttpErrorResponse({ error: { - error: 'error_message' - } - }))); - spyOn(als, 'addWarning'); + error: 'error_message', + }, + }) + ) + ); + spyOn(als, 'addWarning'); - ers.publish().then(successHandler, failHandler); - tick(); + ers.publish().then(successHandler, failHandler); + tick(); - expect(clearWarningsSpy).not.toHaveBeenCalled(); - expect(successHandler).not.toHaveBeenCalled(); - expect(failHandler).toHaveBeenCalled(); - expect(als.addWarning).toHaveBeenCalledWith( - 'Failed to publish an exploration: error_message' - ); - })); + expect(clearWarningsSpy).not.toHaveBeenCalled(); + expect(successHandler).not.toHaveBeenCalled(); + expect(failHandler).toHaveBeenCalled(); + expect(als.addWarning).toHaveBeenCalledWith( + 'Failed to publish an exploration: error_message' + ); + })); it('should save moderator change to backend', fakeAsync(() => { spyOn( explorationRightsBackendApiService, - 'saveModeratorChangeToBackendAsyncPutData').and.returnValue( - Promise.resolve(serviceData)); + 'saveModeratorChangeToBackendAsyncPutData' + ).and.returnValue(Promise.resolve(serviceData)); ers.saveModeratorChangeToBackendAsync(''); tick(); expect(clearWarningsSpy).toHaveBeenCalled(); - expect(ers.ownerNames).toEqual( - serviceData.rights.owner_names); - expect(ers.editorNames).toEqual( - serviceData.rights.editor_names); - expect(ers.voiceArtistNames).toEqual( - serviceData.rights.voice_artist_names); - expect(ers.viewerNames).toEqual( - serviceData.rights.viewer_names); + expect(ers.ownerNames).toEqual(serviceData.rights.owner_names); + expect(ers.editorNames).toEqual(serviceData.rights.editor_names); + expect(ers.voiceArtistNames).toEqual(serviceData.rights.voice_artist_names); + expect(ers.viewerNames).toEqual(serviceData.rights.viewer_names); expect(ers.isPrivate()).toEqual(true); - expect(ers.clonedFrom()).toEqual( - serviceData.rights.cloned_from); - expect(ers.isCommunityOwned()).toBe( - serviceData.rights.community_owned); + expect(ers.clonedFrom()).toEqual(serviceData.rights.cloned_from); + expect(ers.isCommunityOwned()).toBe(serviceData.rights.community_owned); expect(ers.viewableIfPrivate()).toBe( - serviceData.rights.viewable_if_private); + serviceData.rights.viewable_if_private + ); })); - it('should reject handler when saving moderator change to backend fails', - fakeAsync(() => { - spyOn( - explorationRightsBackendApiService, - 'saveModeratorChangeToBackendAsyncPutData').and.returnValue( - Promise.reject()); + it('should reject handler when saving moderator change to backend fails', fakeAsync(() => { + spyOn( + explorationRightsBackendApiService, + 'saveModeratorChangeToBackendAsyncPutData' + ).and.returnValue(Promise.reject()); - ers.saveModeratorChangeToBackendAsync(''); - tick(); + ers.saveModeratorChangeToBackendAsync(''); + tick(); - expect(clearWarningsSpy).not.toHaveBeenCalled(); - expect(ers.ownerNames).toBeUndefined(); - expect(ers.editorNames).toBeUndefined(); - expect(ers.voiceArtistNames).toBeUndefined(); - expect(ers.viewerNames).toBeUndefined(); - expect(ers.isPrivate()).toBeFalsy(); - expect(ers.isPublic()).toBeFalsy(); - expect(ers.clonedFrom()).toBeUndefined(); - expect(ers.isCommunityOwned()).toBeUndefined(); - expect(ers.viewableIfPrivate()).toBeUndefined(); - })); + expect(clearWarningsSpy).not.toHaveBeenCalled(); + expect(ers.ownerNames).toBeUndefined(); + expect(ers.editorNames).toBeUndefined(); + expect(ers.voiceArtistNames).toBeUndefined(); + expect(ers.viewerNames).toBeUndefined(); + expect(ers.isPrivate()).toBeFalsy(); + expect(ers.isPublic()).toBeFalsy(); + expect(ers.clonedFrom()).toBeUndefined(); + expect(ers.isCommunityOwned()).toBeUndefined(); + expect(ers.viewableIfPrivate()).toBeUndefined(); + })); }); diff --git a/core/templates/pages/exploration-editor-page/services/exploration-rights.service.ts b/core/templates/pages/exploration-editor-page/services/exploration-rights.service.ts index caff46d218da..b357e9611e72 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-rights.service.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-rights.service.ts @@ -17,18 +17,17 @@ * about the rights for this exploration. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { AppConstants } from 'app.constants'; -import { ExplorationDataService } from - 'pages/exploration-editor-page/services/exploration-data.service'; -import { AlertsService } from 'services/alerts.service'; -import { ExplorationRightsBackendApiService } from './exploration-rights-backend-api.service'; -import { ExplorationRightsBackendData } from './exploration-rights-backend-api.service'; -import { HttpErrorResponse } from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {AppConstants} from 'app.constants'; +import {ExplorationDataService} from 'pages/exploration-editor-page/services/exploration-data.service'; +import {AlertsService} from 'services/alerts.service'; +import {ExplorationRightsBackendApiService} from './exploration-rights-backend-api.service'; +import {ExplorationRightsBackendData} from './exploration-rights-backend-api.service'; +import {HttpErrorResponse} from '@angular/common/http'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ExplorationRightsService { // These properties are initialized using init method and we need to do @@ -46,14 +45,19 @@ export class ExplorationRightsService { constructor( private alertsService: AlertsService, private explorationDataService: ExplorationDataService, - private explorationRightsBackendApiService: - ExplorationRightsBackendApiService + private explorationRightsBackendApiService: ExplorationRightsBackendApiService ) {} init( - ownerNames: string[], editorNames: string[], voiceArtistNames: string[], - viewerNames: string[], status: string, clonedFrom: string, - isCommunityOwned: boolean, viewableIfPrivate: boolean): void { + ownerNames: string[], + editorNames: string[], + voiceArtistNames: string[], + viewerNames: string[], + status: string, + clonedFrom: string, + isCommunityOwned: boolean, + viewableIfPrivate: boolean + ): void { this.ownerNames = ownerNames; this.editorNames = editorNames; this.voiceArtistNames = voiceArtistNames; @@ -95,71 +99,106 @@ export class ExplorationRightsService { } return this.explorationRightsBackendApiService .makeCommunityOwnedPutData( - this.explorationDataService.explorationId, version, true) + this.explorationDataService.explorationId, + version, + true + ) .then((response: ExplorationRightsBackendData) => { this.alertsService.clearWarnings(); this.init( - response.rights.owner_names, response.rights.editor_names, - response.rights.voice_artist_names, response.rights.viewer_names, - response.rights.status, response.rights.cloned_from, - response.rights.community_owned, response.rights.viewable_if_private); + response.rights.owner_names, + response.rights.editor_names, + response.rights.voice_artist_names, + response.rights.viewer_names, + response.rights.status, + response.rights.cloned_from, + response.rights.community_owned, + response.rights.viewable_if_private + ); }); } saveRoleChanges( - newMemberUsername: string, newMemberRole: string): Promise { + newMemberUsername: string, + newMemberRole: string + ): Promise { const version = this.explorationDataService.data.version; if (version === undefined) { throw new Error('Exploration version is undefined'); } - return this.explorationRightsBackendApiService.saveRoleChangesPutData( - this.explorationDataService.explorationId, version, newMemberRole, - newMemberUsername) - .then((response: ExplorationRightsBackendData) => { - this.alertsService.clearWarnings(); - this.init( - response.rights.owner_names, response.rights.editor_names, - response.rights.voice_artist_names, response.rights.viewer_names, - response.rights.status, response.rights.cloned_from, - response.rights.community_owned, response.rights.viewable_if_private); - }, (response) => { - this.alertsService.addWarning(response.error.error); - }); + return this.explorationRightsBackendApiService + .saveRoleChangesPutData( + this.explorationDataService.explorationId, + version, + newMemberRole, + newMemberUsername + ) + .then( + (response: ExplorationRightsBackendData) => { + this.alertsService.clearWarnings(); + this.init( + response.rights.owner_names, + response.rights.editor_names, + response.rights.voice_artist_names, + response.rights.viewer_names, + response.rights.status, + response.rights.cloned_from, + response.rights.community_owned, + response.rights.viewable_if_private + ); + }, + response => { + this.alertsService.addWarning(response.error.error); + } + ); } - setViewability( - viewableIfPrivate: boolean): Promise { + setViewability(viewableIfPrivate: boolean): Promise { const version = this.explorationDataService.data.version; if (version === undefined) { throw new Error('Exploration version is undefined'); } - return this.explorationRightsBackendApiService.setViewabilityPutData( - this.explorationDataService.explorationId, version, viewableIfPrivate - ).then( - (response: ExplorationRightsBackendData) => { + return this.explorationRightsBackendApiService + .setViewabilityPutData( + this.explorationDataService.explorationId, + version, + viewableIfPrivate + ) + .then((response: ExplorationRightsBackendData) => { this.alertsService.clearWarnings(); this.init( - response.rights.owner_names, response.rights.editor_names, - response.rights.voice_artist_names, response.rights.viewer_names, - response.rights.status, response.rights.cloned_from, - response.rights.community_owned, response.rights.viewable_if_private); + response.rights.owner_names, + response.rights.editor_names, + response.rights.voice_artist_names, + response.rights.viewer_names, + response.rights.status, + response.rights.cloned_from, + response.rights.community_owned, + response.rights.viewable_if_private + ); }); } publish(): Promise { - return this.explorationRightsBackendApiService.publishPutData( - this.explorationDataService.explorationId, true).then( - (response: ExplorationRightsBackendData) => { + return this.explorationRightsBackendApiService + .publishPutData(this.explorationDataService.explorationId, true) + .then((response: ExplorationRightsBackendData) => { this.alertsService.clearWarnings(); this.init( - response.rights.owner_names, response.rights.editor_names, - response.rights.voice_artist_names, response.rights.viewer_names, - response.rights.status, response.rights.cloned_from, - response.rights.community_owned, response.rights.viewable_if_private); - }).catch( - (response: HttpErrorResponse) => { + response.rights.owner_names, + response.rights.editor_names, + response.rights.voice_artist_names, + response.rights.viewer_names, + response.rights.status, + response.rights.cloned_from, + response.rights.community_owned, + response.rights.viewable_if_private + ); + }) + .catch((response: HttpErrorResponse) => { this.alertsService.addWarning( - 'Failed to publish an exploration: ' + response.error.error); + 'Failed to publish an exploration: ' + response.error.error + ); throw response; }); } @@ -169,30 +208,45 @@ export class ExplorationRightsService { return this.explorationRightsBackendApiService .saveModeratorChangeToBackendAsyncPutData( this.explorationDataService.explorationId, - version as number, emailBody).then( - (response: ExplorationRightsBackendData) => { - this.alertsService.clearWarnings(); - this.init( - response.rights.owner_names, response.rights.editor_names, - response.rights.voice_artist_names, response.rights.viewer_names, - response.rights.status, response.rights.cloned_from, - response.rights.community_owned, response.rights.viewable_if_private - ); - }).catch((response) => { + version as number, + emailBody + ) + .then((response: ExplorationRightsBackendData) => { + this.alertsService.clearWarnings(); + this.init( + response.rights.owner_names, + response.rights.editor_names, + response.rights.voice_artist_names, + response.rights.viewer_names, + response.rights.status, + response.rights.cloned_from, + response.rights.community_owned, + response.rights.viewable_if_private + ); + }) + .catch(response => { this.alertsService.addWarning('Failed to send email: ' + response); }); } removeRoleAsync(memberUsername: string): Promise { - return this.explorationRightsBackendApiService.removeRoleAsyncDeleteData( - this.explorationDataService.explorationId, memberUsername).then( - (response: ExplorationRightsBackendData) => { + return this.explorationRightsBackendApiService + .removeRoleAsyncDeleteData( + this.explorationDataService.explorationId, + memberUsername + ) + .then((response: ExplorationRightsBackendData) => { this.alertsService.clearWarnings(); this.init( - response.rights.owner_names, response.rights.editor_names, - response.rights.voice_artist_names, response.rights.viewer_names, - response.rights.status, response.rights.cloned_from, - response.rights.community_owned, response.rights.viewable_if_private); + response.rights.owner_names, + response.rights.editor_names, + response.rights.voice_artist_names, + response.rights.viewer_names, + response.rights.status, + response.rights.cloned_from, + response.rights.community_owned, + response.rights.viewable_if_private + ); }); } @@ -200,32 +254,42 @@ export class ExplorationRightsService { return this.explorationRightsBackendApiService .assignVoiceArtistRoleAsyncPostData( this.explorationDataService.explorationId, - newVoiceArtistUsername).then((response) => { - this.alertsService.clearWarnings(); - this.voiceArtistNames.push(newVoiceArtistUsername); - }, (response) => { - this.alertsService.addWarning( - response.error.error); - }); + newVoiceArtistUsername + ) + .then( + response => { + this.alertsService.clearWarnings(); + this.voiceArtistNames.push(newVoiceArtistUsername); + }, + response => { + this.alertsService.addWarning(response.error.error); + } + ); } removeVoiceArtistRoleAsync(voiceArtistUsername: string): Promise { return this.explorationRightsBackendApiService .removeVoiceArtistRoleAsyncDeleteData( - this.explorationDataService.explorationId, voiceArtistUsername).then( - (response) => { - this.alertsService.clearWarnings(); - this.voiceArtistNames.forEach((username, index) => { - if (username === voiceArtistUsername) { - this.voiceArtistNames.splice(index, 1); - } - }); + this.explorationDataService.explorationId, + voiceArtistUsername + ) + .then(response => { + this.alertsService.clearWarnings(); + this.voiceArtistNames.forEach((username, index) => { + if (username === voiceArtistUsername) { + this.voiceArtistNames.splice(index, 1); + } }); + }); } checkUserAlreadyHasRoles(username: string): boolean { - return [...this.ownerNames, ...this.editorNames, ...this.viewerNames, - ...this.voiceArtistNames].includes(username); + return [ + ...this.ownerNames, + ...this.editorNames, + ...this.viewerNames, + ...this.voiceArtistNames, + ].includes(username); } getOldRole(username: string): string { @@ -240,5 +304,9 @@ export class ExplorationRightsService { } } } -angular.module('oppia').factory( - 'ExplorationRightsService', downgradeInjectable(ExplorationRightsService)); +angular + .module('oppia') + .factory( + 'ExplorationRightsService', + downgradeInjectable(ExplorationRightsService) + ); diff --git a/core/templates/pages/exploration-editor-page/services/exploration-save.service.spec.ts b/core/templates/pages/exploration-editor-page/services/exploration-save.service.spec.ts index b5e07decbffc..35c1b6d4dc6c 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-save.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-save.service.spec.ts @@ -12,37 +12,42 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Unit tests for the Exploration save service. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { EventEmitter } from '@angular/core'; -import { fakeAsync, flush, TestBed, tick } from '@angular/core/testing'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { ExplorationChangeAddState } from 'domain/exploration/exploration-draft.model'; -import { StateObjectsBackendDict, StatesObjectFactory } from 'domain/exploration/StatesObjectFactory'; -import { AlertsService } from 'services/alerts.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { EditabilityService } from 'services/editability.service'; -import { InternetConnectivityService } from 'services/internet-connectivity.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { AutosaveInfoModalsService } from './autosave-info-modals.service'; -import { ChangeListService } from './change-list.service'; -import { ExplorationCategoryService } from './exploration-category.service'; -import { ExplorationDataService } from './exploration-data.service'; -import { ExplorationDiffService, ProcessedStateIdsAndData } from './exploration-diff.service'; -import { ExplorationLanguageCodeService } from './exploration-language-code.service'; -import { ExplorationObjectiveService } from './exploration-objective.service'; -import { ExplorationRightsService } from './exploration-rights.service'; -import { ExplorationSaveService } from './exploration-save.service'; -import { ExplorationStatesService } from './exploration-states.service'; -import { ExplorationTagsService } from './exploration-tags.service'; -import { ExplorationTitleService } from './exploration-title.service'; -import { ExplorationWarningsService } from './exploration-warnings.service'; -import { RouterService } from './router.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {EventEmitter} from '@angular/core'; +import {fakeAsync, flush, TestBed, tick} from '@angular/core/testing'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {ExplorationChangeAddState} from 'domain/exploration/exploration-draft.model'; +import { + StateObjectsBackendDict, + StatesObjectFactory, +} from 'domain/exploration/StatesObjectFactory'; +import {AlertsService} from 'services/alerts.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {EditabilityService} from 'services/editability.service'; +import {InternetConnectivityService} from 'services/internet-connectivity.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {AutosaveInfoModalsService} from './autosave-info-modals.service'; +import {ChangeListService} from './change-list.service'; +import {ExplorationCategoryService} from './exploration-category.service'; +import {ExplorationDataService} from './exploration-data.service'; +import { + ExplorationDiffService, + ProcessedStateIdsAndData, +} from './exploration-diff.service'; +import {ExplorationLanguageCodeService} from './exploration-language-code.service'; +import {ExplorationObjectiveService} from './exploration-objective.service'; +import {ExplorationRightsService} from './exploration-rights.service'; +import {ExplorationSaveService} from './exploration-save.service'; +import {ExplorationStatesService} from './exploration-states.service'; +import {ExplorationTagsService} from './exploration-tags.service'; +import {ExplorationTitleService} from './exploration-title.service'; +import {ExplorationWarningsService} from './exploration-warnings.service'; +import {RouterService} from './router.service'; class MockNgbModal { open() { @@ -55,381 +60,420 @@ class MockrouterService { onRefreshVersionHistory = new EventEmitter(); } -describe('Exploration save service ' + - 'when draft changes are present and there ' + - 'is version mismatch it', () => { - let explorationSaveService: ExplorationSaveService; - let autosaveInfoModalsService: AutosaveInfoModalsService; - let changeListService: ChangeListService; - let explorationRightsService: ExplorationRightsService; - let explorationTitleService: ExplorationTitleService; - let ngbModal: NgbModal; - let mockConnectionServiceEmitter = new EventEmitter(); - let siteAnalyticsService: SiteAnalyticsService; +describe( + 'Exploration save service ' + + 'when draft changes are present and there ' + + 'is version mismatch it', + () => { + let explorationSaveService: ExplorationSaveService; + let autosaveInfoModalsService: AutosaveInfoModalsService; + let changeListService: ChangeListService; + let explorationRightsService: ExplorationRightsService; + let explorationTitleService: ExplorationTitleService; + let ngbModal: NgbModal; + let mockConnectionServiceEmitter = new EventEmitter(); + let siteAnalyticsService: SiteAnalyticsService; - class MockInternetConnectivityService { - onInternetStateChange = mockConnectionServiceEmitter; + class MockInternetConnectivityService { + onInternetStateChange = mockConnectionServiceEmitter; - isOnline() { - return true; + isOnline() { + return true; + } } - } - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [HttpClientTestingModule], - providers: [ - { - provide: ExplorationDataService, - useValue: { - save( + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { + provide: ExplorationDataService, + useValue: { + save( changeList: string[], message: string, successCb: ( arg0: boolean, - arg1: { cmd: string; state_name: string }[] + arg1: {cmd: string; state_name: string}[] ) => void, errorCb: () => void - ) { - successCb(false, [ - { - cmd: 'add_state', - state_name: 'StateName', - content_id_for_state_content: 'content_0', - content_id_for_default_outcome: 'default_outcome_1' - } as ExplorationChangeAddState]); - } - } - }, - ExplorationSaveService, - AutosaveInfoModalsService, - ChangeListService, - ExplorationRightsService, - ExplorationTitleService, - { - provide: InternetConnectivityService, - useClass: MockInternetConnectivityService - }, - { - provide: NgbModal, - useClass: MockNgbModal - }, - { - provide: RouterService, - useClass: MockrouterService - }, - { - provide: WindowRef, - useValue: { - nativeWindow: { - location: { - reload() {} + ) { + successCb(false, [ + { + cmd: 'add_state', + state_name: 'StateName', + content_id_for_state_content: 'content_0', + content_id_for_default_outcome: 'default_outcome_1', + } as ExplorationChangeAddState, + ]); + }, + }, + }, + ExplorationSaveService, + AutosaveInfoModalsService, + ChangeListService, + ExplorationRightsService, + ExplorationTitleService, + { + provide: InternetConnectivityService, + useClass: MockInternetConnectivityService, + }, + { + provide: NgbModal, + useClass: MockNgbModal, + }, + { + provide: RouterService, + useClass: MockrouterService, + }, + { + provide: WindowRef, + useValue: { + nativeWindow: { + location: { + reload() {}, + }, + gtag: () => {}, }, - gtag: () => {} - } - } - } - ] + }, + }, + ], + }); }); - }); - - beforeEach(() => { - explorationSaveService = TestBed.inject(ExplorationSaveService); - autosaveInfoModalsService = TestBed.inject(AutosaveInfoModalsService); - changeListService = TestBed.inject(ChangeListService); - explorationRightsService = TestBed.inject(ExplorationRightsService); - explorationTitleService = TestBed.inject(ExplorationTitleService); - ngbModal = TestBed.inject(NgbModal); - siteAnalyticsService = TestBed.inject(SiteAnalyticsService); - - spyOn(siteAnalyticsService, 'registerOpenPublishExplorationModalEvent') - .and.stub(); - spyOn(siteAnalyticsService, 'registerPublishExplorationEvent') - .and.stub(); - spyOn(siteAnalyticsService, 'registerCommitChangesToPublicExplorationEvent') - .and.stub(); - spyOn(siteAnalyticsService, 'registerSavePlayableExplorationEvent') - .and.stub(); - spyOn(siteAnalyticsService, '_sendEventToGoogleAnalytics') - .and.stub(); - }); - - it('should open version mismatch modal', fakeAsync(() => { - let modalSpy = spyOn( - autosaveInfoModalsService, 'showVersionMismatchModal') - .and.returnValue(); - let startLoadingCb = jasmine.createSpy('startLoadingCb'); - let endLoadingCb = jasmine.createSpy('endLoadingCb'); - spyOn(ngbModal, 'open').and.returnValue( - { - result: Promise.resolve(['1']) - } as NgbModalRef); - spyOn(explorationRightsService, 'isPrivate') - .and.returnValue(true); - - explorationSaveService.showPublishExplorationModal( - startLoadingCb, endLoadingCb); - tick(); - tick(); - - expect(modalSpy).toHaveBeenCalled(); - })); - - it('should restore all memento\'s after modal was ' + - 'closed', fakeAsync(() => { - let startLoadingCb = jasmine.createSpy('startLoadingCb'); - let endLoadingCb = jasmine.createSpy('endLoadingCb'); - let restoreSpy = spyOn(explorationTitleService, 'restoreFromMemento') - .and.returnValue(); - spyOn(ngbModal, 'open').and.returnValue( - { - result: Promise.reject() - } as NgbModalRef); - explorationSaveService.showPublishExplorationModal( - startLoadingCb, endLoadingCb).catch(() => {}); - - tick(); - - expect(restoreSpy).toHaveBeenCalled(); - })); + beforeEach(() => { + explorationSaveService = TestBed.inject(ExplorationSaveService); + autosaveInfoModalsService = TestBed.inject(AutosaveInfoModalsService); + changeListService = TestBed.inject(ChangeListService); + explorationRightsService = TestBed.inject(ExplorationRightsService); + explorationTitleService = TestBed.inject(ExplorationTitleService); + ngbModal = TestBed.inject(NgbModal); + siteAnalyticsService = TestBed.inject(SiteAnalyticsService); - it('should open confirm discard changes modal when clicked ' + - 'on discard changes button', fakeAsync(() => { - const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ - result: Promise.resolve() - } as NgbModalRef); + spyOn( + siteAnalyticsService, + 'registerOpenPublishExplorationModalEvent' + ).and.stub(); + spyOn(siteAnalyticsService, 'registerPublishExplorationEvent').and.stub(); + spyOn( + siteAnalyticsService, + 'registerCommitChangesToPublicExplorationEvent' + ).and.stub(); + spyOn( + siteAnalyticsService, + 'registerSavePlayableExplorationEvent' + ).and.stub(); + spyOn(siteAnalyticsService, '_sendEventToGoogleAnalytics').and.stub(); }); - spyOn(changeListService, 'discardAllChanges') - .and.returnValue(Promise.resolve()); - explorationSaveService.discardChanges(); - tick(); - tick(); + it('should open version mismatch modal', fakeAsync(() => { + let modalSpy = spyOn( + autosaveInfoModalsService, + 'showVersionMismatchModal' + ).and.returnValue(); + let startLoadingCb = jasmine.createSpy('startLoadingCb'); + let endLoadingCb = jasmine.createSpy('endLoadingCb'); + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.resolve(['1']), + } as NgbModalRef); + spyOn(explorationRightsService, 'isPrivate').and.returnValue(true); - expect(modalSpy).toHaveBeenCalled(); - })); + explorationSaveService.showPublishExplorationModal( + startLoadingCb, + endLoadingCb + ); + tick(); + tick(); - it('should return \'initExplorationPageEventEmitter\' ' + - 'when calling \'onInitExplorationPage\'', fakeAsync(() => { - let mockEventEmitter = new EventEmitter(); + expect(modalSpy).toHaveBeenCalled(); + })); - expect(explorationSaveService.onInitExplorationPage) - .toEqual(mockEventEmitter); - })); -}); + it( + "should restore all memento's after modal was " + 'closed', + fakeAsync(() => { + let startLoadingCb = jasmine.createSpy('startLoadingCb'); + let endLoadingCb = jasmine.createSpy('endLoadingCb'); + let restoreSpy = spyOn( + explorationTitleService, + 'restoreFromMemento' + ).and.returnValue(); + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.reject(), + } as NgbModalRef); -describe('Exploration save service ' + - 'when there are no pending draft changes it', () => { - let explorationSaveService: ExplorationSaveService; - let autosaveInfoModalsService: AutosaveInfoModalsService; - let changeListService: ChangeListService; - let editabilityService: EditabilityService; - let explorationCategoryService: ExplorationCategoryService; - let explorationLanguageCodeService: ExplorationLanguageCodeService; - let explorationObjectiveService: ExplorationObjectiveService; - let explorationRightsService: ExplorationRightsService; - let explorationTagsService: ExplorationTagsService; - let explorationTitleService: ExplorationTitleService; - let ngbModal: NgbModal; - let mockConnectionServiceEmitter = new EventEmitter(); - let siteAnalyticsService: SiteAnalyticsService; + explorationSaveService + .showPublishExplorationModal(startLoadingCb, endLoadingCb) + .catch(() => {}); + + tick(); + + expect(restoreSpy).toHaveBeenCalled(); + }) + ); + + it( + 'should open confirm discard changes modal when clicked ' + + 'on discard changes button', + fakeAsync(() => { + const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + result: Promise.resolve(), + } as NgbModalRef; + }); + spyOn(changeListService, 'discardAllChanges').and.returnValue( + Promise.resolve() + ); + + explorationSaveService.discardChanges(); + tick(); + tick(); + + expect(modalSpy).toHaveBeenCalled(); + }) + ); + + it( + "should return 'initExplorationPageEventEmitter' " + + "when calling 'onInitExplorationPage'", + fakeAsync(() => { + let mockEventEmitter = new EventEmitter(); + + expect(explorationSaveService.onInitExplorationPage).toEqual( + mockEventEmitter + ); + }) + ); + } +); + +describe( + 'Exploration save service ' + 'when there are no pending draft changes it', + () => { + let explorationSaveService: ExplorationSaveService; + let autosaveInfoModalsService: AutosaveInfoModalsService; + let changeListService: ChangeListService; + let editabilityService: EditabilityService; + let explorationCategoryService: ExplorationCategoryService; + let explorationLanguageCodeService: ExplorationLanguageCodeService; + let explorationObjectiveService: ExplorationObjectiveService; + let explorationRightsService: ExplorationRightsService; + let explorationTagsService: ExplorationTagsService; + let explorationTitleService: ExplorationTitleService; + let ngbModal: NgbModal; + let mockConnectionServiceEmitter = new EventEmitter(); + let siteAnalyticsService: SiteAnalyticsService; - class MockInternetConnectivityService { - onInternetStateChange = mockConnectionServiceEmitter; + class MockInternetConnectivityService { + onInternetStateChange = mockConnectionServiceEmitter; - isOnline() { - return true; + isOnline() { + return true; + } } - } - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [HttpClientTestingModule], - providers: [ - { - provide: ExplorationDataService, - useValue: { - save( + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { + provide: ExplorationDataService, + useValue: { + save( changeList: string[], message: string, successCb: (arg0: boolean, arg1: string[]) => void, errorCb: () => void - ) { - successCb(true, []); - } - } - }, - ExplorationSaveService, - AutosaveInfoModalsService, - ChangeListService, - ExplorationRightsService, - ExplorationTitleService, - { - provide: InternetConnectivityService, - useClass: MockInternetConnectivityService - }, - { - provide: NgbModal, - useClass: MockNgbModal - }, - { - provide: RouterService, - useClass: MockrouterService - }, - { - provide: WindowRef, - useValue: { - nativeWindow: { - location: { - reload() {} - } - } - } - } - ] + ) { + successCb(true, []); + }, + }, + }, + ExplorationSaveService, + AutosaveInfoModalsService, + ChangeListService, + ExplorationRightsService, + ExplorationTitleService, + { + provide: InternetConnectivityService, + useClass: MockInternetConnectivityService, + }, + { + provide: NgbModal, + useClass: MockNgbModal, + }, + { + provide: RouterService, + useClass: MockrouterService, + }, + { + provide: WindowRef, + useValue: { + nativeWindow: { + location: { + reload() {}, + }, + }, + }, + }, + ], + }); }); - }); - beforeEach(() => { - explorationSaveService = TestBed.inject(ExplorationSaveService); - autosaveInfoModalsService = TestBed.inject(AutosaveInfoModalsService); - changeListService = TestBed.inject(ChangeListService); - editabilityService = TestBed.inject(EditabilityService); - explorationCategoryService = TestBed.inject(ExplorationCategoryService); - explorationLanguageCodeService = TestBed.inject( - ExplorationLanguageCodeService); - explorationObjectiveService = TestBed.inject(ExplorationObjectiveService); - explorationRightsService = TestBed.inject(ExplorationRightsService); - explorationTagsService = TestBed.inject(ExplorationTagsService); - explorationTitleService = TestBed.inject(ExplorationTitleService); - ngbModal = TestBed.inject(NgbModal); - siteAnalyticsService = TestBed.inject(SiteAnalyticsService); + beforeEach(() => { + explorationSaveService = TestBed.inject(ExplorationSaveService); + autosaveInfoModalsService = TestBed.inject(AutosaveInfoModalsService); + changeListService = TestBed.inject(ChangeListService); + editabilityService = TestBed.inject(EditabilityService); + explorationCategoryService = TestBed.inject(ExplorationCategoryService); + explorationLanguageCodeService = TestBed.inject( + ExplorationLanguageCodeService + ); + explorationObjectiveService = TestBed.inject(ExplorationObjectiveService); + explorationRightsService = TestBed.inject(ExplorationRightsService); + explorationTagsService = TestBed.inject(ExplorationTagsService); + explorationTitleService = TestBed.inject(ExplorationTitleService); + ngbModal = TestBed.inject(NgbModal); + siteAnalyticsService = TestBed.inject(SiteAnalyticsService); - spyOn(siteAnalyticsService, 'registerOpenPublishExplorationModalEvent') - .and.stub(); - spyOn(siteAnalyticsService, 'registerPublishExplorationEvent') - .and.stub(); - spyOn(siteAnalyticsService, 'registerCommitChangesToPublicExplorationEvent') - .and.stub(); - spyOn(siteAnalyticsService, 'registerSavePlayableExplorationEvent') - .and.stub(); - spyOn(siteAnalyticsService, '_sendEventToGoogleAnalytics') - .and.stub(); - }); + spyOn( + siteAnalyticsService, + 'registerOpenPublishExplorationModalEvent' + ).and.stub(); + spyOn(siteAnalyticsService, 'registerPublishExplorationEvent').and.stub(); + spyOn( + siteAnalyticsService, + 'registerCommitChangesToPublicExplorationEvent' + ).and.stub(); + spyOn( + siteAnalyticsService, + 'registerSavePlayableExplorationEvent' + ).and.stub(); + spyOn(siteAnalyticsService, '_sendEventToGoogleAnalytics').and.stub(); + }); - it('should not open version mismatch modal', fakeAsync(() => { - let modalSpy = spyOn( - autosaveInfoModalsService, 'showVersionMismatchModal') - .and.returnValue(); - let startLoadingCb = jasmine.createSpy('startLoadingCb'); - let endLoadingCb = jasmine.createSpy('endLoadingCb'); - spyOn(explorationRightsService, 'publish') - .and.resolveTo(); - spyOn(ngbModal, 'open').and.returnValue( - { - result: Promise.resolve([]) + it('should not open version mismatch modal', fakeAsync(() => { + let modalSpy = spyOn( + autosaveInfoModalsService, + 'showVersionMismatchModal' + ).and.returnValue(); + let startLoadingCb = jasmine.createSpy('startLoadingCb'); + let endLoadingCb = jasmine.createSpy('endLoadingCb'); + spyOn(explorationRightsService, 'publish').and.resolveTo(); + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.resolve([]), } as NgbModalRef); - spyOn(explorationRightsService, 'isPrivate') - .and.returnValue(true); + spyOn(explorationRightsService, 'isPrivate').and.returnValue(true); - explorationSaveService.showPublishExplorationModal( - startLoadingCb, endLoadingCb); - tick(); - tick(); + explorationSaveService.showPublishExplorationModal( + startLoadingCb, + endLoadingCb + ); + tick(); + tick(); - expect(modalSpy).not.toHaveBeenCalled(); - })); + expect(modalSpy).not.toHaveBeenCalled(); + })); - it('should show congratulatory sharing modal', fakeAsync(() => { - let startLoadingCb = jasmine.createSpy('startLoadingCb'); - let endLoadingCb = jasmine.createSpy('endLoadingCb'); - spyOn(changeListService, 'discardAllChanges') - .and.returnValue(Promise.reject(null)); - let modalSpy = spyOn(ngbModal, 'open').and.returnValue( - { - result: Promise.resolve(['1']) + it('should show congratulatory sharing modal', fakeAsync(() => { + let startLoadingCb = jasmine.createSpy('startLoadingCb'); + let endLoadingCb = jasmine.createSpy('endLoadingCb'); + spyOn(changeListService, 'discardAllChanges').and.returnValue( + Promise.reject(null) + ); + let modalSpy = spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.resolve(['1']), } as NgbModalRef); - spyOn(explorationRightsService, 'publish') - .and.resolveTo(); - - explorationSaveService.showPublishExplorationModal( - startLoadingCb, endLoadingCb); - tick(); - tick(); - - expect(modalSpy).toHaveBeenCalled(); - })); + spyOn(explorationRightsService, 'publish').and.resolveTo(); - it('should not publish exploration in case of backend ' + - 'error', fakeAsync(() => { - spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.reject('failure') - } as NgbModalRef); - let failHandler = jasmine.createSpy('fail'); - let publishSpy = spyOn(explorationRightsService, 'publish') - .and.resolveTo(); - - explorationTitleService.savedMemento = true; - explorationObjectiveService.savedMemento = true; - explorationCategoryService.savedMemento = true; - explorationLanguageCodeService.savedMemento = 'afk'; - explorationTagsService.savedMemento = 'invalid'; - - // This throws "Argument of type 'null' is not assignable.". We need - // to suppress this error because of strict type checking. This is - // because the function is called with null as an argument. - // @ts-ignore - explorationSaveService.showPublishExplorationModal(null, null) - .catch(failHandler); - tick(); - tick(); + explorationSaveService.showPublishExplorationModal( + startLoadingCb, + endLoadingCb + ); + tick(); + tick(); - expect(publishSpy).not.toHaveBeenCalled(); - expect(failHandler).toHaveBeenCalled(); - })); + expect(modalSpy).toHaveBeenCalled(); + })); - it('should mark exploaration as editable', fakeAsync(() => { - let editableSpy = spyOn(editabilityService, 'markNotEditable') - .and.returnValue(); - let startLoadingCb = jasmine.createSpy('startLoadingCb'); - let endLoadingCb = jasmine.createSpy('endLoadingCb'); - spyOn(changeListService, 'discardAllChanges') - .and.returnValue(Promise.resolve()); - spyOn(ngbModal, 'open').and.returnValue( - { - result: Promise.resolve(['1']) + it( + 'should not publish exploration in case of backend ' + 'error', + fakeAsync(() => { + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.reject('failure'), + } as NgbModalRef); + let failHandler = jasmine.createSpy('fail'); + let publishSpy = spyOn( + explorationRightsService, + 'publish' + ).and.resolveTo(); + + explorationTitleService.savedMemento = true; + explorationObjectiveService.savedMemento = true; + explorationCategoryService.savedMemento = true; + explorationLanguageCodeService.savedMemento = 'afk'; + explorationTagsService.savedMemento = 'invalid'; + + explorationSaveService + // This throws "Argument of type 'null' is not assignable.". We need + // to suppress this error because of strict type checking. This is + // because the function is called with null as an argument. + // @ts-ignore + .showPublishExplorationModal(null, null) + .catch(failHandler); + tick(); + tick(); + + expect(publishSpy).not.toHaveBeenCalled(); + expect(failHandler).toHaveBeenCalled(); + }) + ); + + it('should mark exploaration as editable', fakeAsync(() => { + let editableSpy = spyOn( + editabilityService, + 'markNotEditable' + ).and.returnValue(); + let startLoadingCb = jasmine.createSpy('startLoadingCb'); + let endLoadingCb = jasmine.createSpy('endLoadingCb'); + spyOn(changeListService, 'discardAllChanges').and.returnValue( + Promise.resolve() + ); + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.resolve(['1']), } as NgbModalRef); - explorationSaveService.showPublishExplorationModal( - startLoadingCb, endLoadingCb); - tick(); - tick(); - - expect(editableSpy).toHaveBeenCalled(); - })); - - it('should check whether the exploration is saveable', () => { - spyOn(changeListService, 'isExplorationLockedForEditing') - .and.returnValue(false); + explorationSaveService.showPublishExplorationModal( + startLoadingCb, + endLoadingCb + ); + tick(); + tick(); - let result = explorationSaveService.isExplorationSaveable(); + expect(editableSpy).toHaveBeenCalled(); + })); - expect(result).toBe(false); - }); -}); + it('should check whether the exploration is saveable', () => { + spyOn(changeListService, 'isExplorationLockedForEditing').and.returnValue( + false + ); + let result = explorationSaveService.isExplorationSaveable(); -describe('Exploration save service ' + - 'in case of backend error while saving ' + - 'exploration data it', () => { - let explorationSaveService: ExplorationSaveService; - let ngbModal: NgbModal; - let changeListService: ChangeListService; - let mockConnectionServiceEmitter = new EventEmitter(); - let siteAnalyticsService: SiteAnalyticsService; + expect(result).toBe(false); + }); + } +); + +describe( + 'Exploration save service ' + + 'in case of backend error while saving ' + + 'exploration data it', + () => { + let explorationSaveService: ExplorationSaveService; + let ngbModal: NgbModal; + let changeListService: ChangeListService; + let mockConnectionServiceEmitter = new EventEmitter(); + let siteAnalyticsService: SiteAnalyticsService; class MockInternetConnectivityService { onInternetStateChange = mockConnectionServiceEmitter; @@ -447,16 +491,16 @@ describe('Exploration save service ' + provide: ExplorationDataService, useValue: { save( - changeList: string[], - message: string, - successCb: (arg0: boolean, arg1: string[]) => void, - errorCb: (arg0: { error: { error: string }}) => void + changeList: string[], + message: string, + successCb: (arg0: boolean, arg1: string[]) => void, + errorCb: (arg0: {error: {error: string}}) => void ) { successCb(true, []); errorCb({error: {error: 'errorMessage'}}); }, - discardDraftAsync() {} - } + discardDraftAsync() {}, + }, }, ExplorationSaveService, AutosaveInfoModalsService, @@ -465,27 +509,27 @@ describe('Exploration save service ' + ExplorationTitleService, { provide: InternetConnectivityService, - useClass: MockInternetConnectivityService + useClass: MockInternetConnectivityService, }, { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, { provide: RouterService, - useClass: MockrouterService + useClass: MockrouterService, }, { provide: WindowRef, useValue: { nativeWindow: { location: { - reload() {} - } - } - } - } - ] + reload() {}, + }, + }, + }, + }, + ], }); }); @@ -495,45 +539,46 @@ describe('Exploration save service ' + siteAnalyticsService = TestBed.inject(SiteAnalyticsService); changeListService = TestBed.inject(ChangeListService); - spyOn(changeListService, 'discardAllChanges') - .and.returnValue(Promise.resolve()); - spyOn(siteAnalyticsService, 'registerOpenPublishExplorationModalEvent') - .and.stub(); - spyOn(siteAnalyticsService, 'registerPublishExplorationEvent') - .and.stub(); + spyOn(changeListService, 'discardAllChanges').and.returnValue( + Promise.resolve() + ); + spyOn( + siteAnalyticsService, + 'registerOpenPublishExplorationModalEvent' + ).and.stub(); + spyOn(siteAnalyticsService, 'registerPublishExplorationEvent').and.stub(); spyOn( siteAnalyticsService, - 'registerCommitChangesToPublicExplorationEvent') - .and.stub(); - spyOn(siteAnalyticsService, 'registerSavePlayableExplorationEvent') - .and.stub(); - spyOn(siteAnalyticsService, '_sendEventToGoogleAnalytics') - .and.stub(); + 'registerCommitChangesToPublicExplorationEvent' + ).and.stub(); + spyOn( + siteAnalyticsService, + 'registerSavePlayableExplorationEvent' + ).and.stub(); + spyOn(siteAnalyticsService, '_sendEventToGoogleAnalytics').and.stub(); }); it('should call error callback', fakeAsync(() => { let successCb = jasmine.createSpy('success'); let errorCb = jasmine.createSpy('error'); - let modalSpy = spyOn(ngbModal, 'open').and.returnValue( - { + let modalSpy = spyOn(ngbModal, 'open').and.returnValue({ componentInstance: { isExplorationPrivate: true, - diffData: null + diffData: null, }, - result: Promise.resolve(['1']) + result: Promise.resolve(['1']), } as NgbModalRef); - explorationSaveService.showPublishExplorationModal( - successCb, errorCb); + explorationSaveService.showPublishExplorationModal(successCb, errorCb); tick(); tick(); expect(modalSpy).toHaveBeenCalled(); })); -}); + } +); -describe('Exploration save service ' + - 'while saving changes', () => { +describe('Exploration save service ' + 'while saving changes', () => { let explorationSaveService: ExplorationSaveService; let changeListService: ChangeListService; let explorationRightsService: ExplorationRightsService; @@ -563,7 +608,7 @@ describe('Exploration save service ' + linked_skill_id: '', content: { content_id: 'content', - html: '{{HtmlValue}}' + html: '{{HtmlValue}}', }, recorded_voiceovers: { voiceovers_mapping: { @@ -577,23 +622,25 @@ describe('Exploration save service ' + customization_args: {}, solution: null, id: null, - answer_groups: [{ - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: '', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_1', - html: '{{FeedbackValue}}' + answer_groups: [ + { + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: '', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: '{{FeedbackValue}}', + }, }, }, - }], + ], default_outcome: { labelled_as_correct: false, param_changes: [], @@ -607,7 +654,7 @@ describe('Exploration save service ' + }, }, hints: [], - } + }, }, State: { classifier_model_id: '', @@ -616,13 +663,13 @@ describe('Exploration save service ' + linked_skill_id: '', content: { content_id: 'content', - html: 'content' + html: 'content', }, recorded_voiceovers: { voiceovers_mapping: { content: {}, default_outcome: {}, - } + }, }, param_changes: [], interaction: { @@ -630,23 +677,25 @@ describe('Exploration save service ' + customization_args: {}, solution: null, id: null, - answer_groups: [{ - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: '', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_1', - html: '{{StateFeedbackValue}}' + answer_groups: [ + { + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: '', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: '{{StateFeedbackValue}}', + }, }, }, - }], + ], default_outcome: { labelled_as_correct: false, param_changes: [], @@ -656,11 +705,11 @@ describe('Exploration save service ' + dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: '' + html: '', }, }, - hints: [] - } + hints: [], + }, }, State2: { classifier_model_id: '', @@ -669,13 +718,13 @@ describe('Exploration save service ' + linked_skill_id: '', content: { content_id: 'content', - html: 'content' + html: 'content', }, recorded_voiceovers: { voiceovers_mapping: { content: {}, default_outcome: {}, - } + }, }, param_changes: [], interaction: { @@ -683,23 +732,25 @@ describe('Exploration save service ' + customization_args: {}, solution: null, id: null, - answer_groups: [{ - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: '', - dest_if_really_stuck: null, - feedback: { - content_id: '', - html: '' - } - } - }], + answer_groups: [ + { + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: '', + dest_if_really_stuck: null, + feedback: { + content_id: '', + html: '', + }, + }, + }, + ], default_outcome: { labelled_as_correct: false, param_changes: [], @@ -709,11 +760,11 @@ describe('Exploration save service ' + dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: '' + html: '', }, }, - hints: [] - } + hints: [], + }, }, State3: { classifier_model_id: '', @@ -722,13 +773,13 @@ describe('Exploration save service ' + linked_skill_id: '', content: { content_id: 'content', - html: 'content' + html: 'content', }, recorded_voiceovers: { voiceovers_mapping: { content: {}, default_outcome: {}, - } + }, }, param_changes: [], interaction: { @@ -736,23 +787,25 @@ describe('Exploration save service ' + customization_args: {}, solution: null, id: null, - answer_groups: [{ - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: '', - dest_if_really_stuck: null, - feedback: { - content_id: '', - html: '' - } - } - }], + answer_groups: [ + { + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: '', + dest_if_really_stuck: null, + feedback: { + content_id: '', + html: '', + }, + }, + }, + ], default_outcome: { labelled_as_correct: false, param_changes: [], @@ -762,12 +815,12 @@ describe('Exploration save service ' + dest_if_really_stuck: null, feedback: { content_id: '', - html: '' + html: '', }, }, - hints: [] - } - } + hints: [], + }, + }, }; beforeEach(() => { @@ -780,23 +833,20 @@ describe('Exploration save service ' + getLastSavedDataAsync() { return Promise.resolve({ states: statesBackendDict, - init_state_name: 'Hola' + init_state_name: 'Hola', }); }, save( - changeList: string[], - message: string, - successCb: ( - arg0: boolean, arg1: string[] - ) => void, - errorCb: ( - arg0: { error: { error: string } } - ) => void) { + changeList: string[], + message: string, + successCb: (arg0: boolean, arg1: string[]) => void, + errorCb: (arg0: {error: {error: string}}) => void + ) { successCb(true, []); errorCb({error: {error: 'errorMessage'}}); }, - discardDraftAsync() {} - } + discardDraftAsync() {}, + }, }, FocusManagerService, ExplorationSaveService, @@ -806,31 +856,31 @@ describe('Exploration save service ' + ExplorationTitleService, { provide: InternetConnectivityService, - useClass: MockInternetConnectivityService + useClass: MockInternetConnectivityService, }, { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, { provide: RouterService, - useClass: MockrouterService + useClass: MockrouterService, }, { provide: WindowRef, useValue: { nativeWindow: { location: { - reload() {} - } - } - } + reload() {}, + }, + }, + }, }, ExplorationDiffService, ExplorationStatesService, FocusManagerService, ExplorationWarningsService, - ] + ], }); }); @@ -849,56 +899,57 @@ describe('Exploration save service ' + changeListServiceSpy = spyOn(changeListService, 'discardAllChanges'); changeListServiceSpy.and.returnValue(Promise.resolve(null)); - spyOn(siteAnalyticsService, 'registerOpenPublishExplorationModalEvent') - .and.stub(); - spyOn(siteAnalyticsService, 'registerPublishExplorationEvent') - .and.stub(); - spyOn(siteAnalyticsService, 'registerCommitChangesToPublicExplorationEvent') - .and.stub(); - spyOn(siteAnalyticsService, 'registerSavePlayableExplorationEvent') - .and.stub(); - spyOn(siteAnalyticsService, '_sendEventToGoogleAnalytics') - .and.stub(); + spyOn( + siteAnalyticsService, + 'registerOpenPublishExplorationModalEvent' + ).and.stub(); + spyOn(siteAnalyticsService, 'registerPublishExplorationEvent').and.stub(); + spyOn( + siteAnalyticsService, + 'registerCommitChangesToPublicExplorationEvent' + ).and.stub(); + spyOn( + siteAnalyticsService, + 'registerSavePlayableExplorationEvent' + ).and.stub(); + spyOn(siteAnalyticsService, '_sendEventToGoogleAnalytics').and.stub(); }); it('should open exploration save modal', fakeAsync(() => { let startLoadingCb = jasmine.createSpy('startLoadingCb'); let endLoadingCb = jasmine.createSpy('endLoadingCb'); - let sampleStates = statesObjectFactory.createFromBackendDict( - statesBackendDict); - spyOn(routerService, 'savePendingChanges') - .and.returnValue(); - spyOn(explorationStatesService, 'getStates') - .and.returnValue(sampleStates); - spyOn(explorationDiffService, 'getDiffGraphData') - .and.returnValue({ + let sampleStates = + statesObjectFactory.createFromBackendDict(statesBackendDict); + spyOn(routerService, 'savePendingChanges').and.returnValue(); + spyOn(explorationStatesService, 'getStates').and.returnValue(sampleStates); + spyOn(explorationDiffService, 'getDiffGraphData').and.returnValue({ + nodes: { nodes: { - nodes: { - newestStateName: 'newestStateName', - originalStateName: 'originalStateName', - stateProperty: 'stateProperty', - } + newestStateName: 'newestStateName', + originalStateName: 'originalStateName', + stateProperty: 'stateProperty', }, - links: [{ + }, + links: [ + { source: 0, target: 0, - linkProperty: 'links' - }], - finalStateIds: ['finalStaeIds'], - originalStateIds: {Hola: 0}, - stateIds: {Hola: 0}, - } as ProcessedStateIdsAndData); - let modalSpy = spyOn(ngbModal, 'open').and.returnValue( - { - componentInstance: { - isExplorationPrivate: true, - diffData: null, - }, - result: Promise.resolve('commitMessage') - } as NgbModalRef); + linkProperty: 'links', + }, + ], + finalStateIds: ['finalStaeIds'], + originalStateIds: {Hola: 0}, + stateIds: {Hola: 0}, + } as ProcessedStateIdsAndData); + let modalSpy = spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: { + isExplorationPrivate: true, + diffData: null, + }, + result: Promise.resolve('commitMessage'), + } as NgbModalRef); - explorationSaveService.saveChangesAsync( - startLoadingCb, endLoadingCb); + explorationSaveService.saveChangesAsync(startLoadingCb, endLoadingCb); flush(); tick(); @@ -907,168 +958,173 @@ describe('Exploration save service ' + expect(modalSpy).toHaveBeenCalled(); })); - it('should not open exploration save modal in case of ' + - 'backend error', fakeAsync(() => { - let startLoadingCb = jasmine.createSpy('startLoadingCb'); - let endLoadingCb = jasmine.createSpy('endLoadingCb'); - - spyOn(routerService, 'savePendingChanges').and.stub(); - spyOn(alertsService, 'addWarning').and.stub(); - spyOn(explorationRightsService, 'isPrivate').and.returnValue(false); - spyOn(explorationWarningsService, 'countWarnings').and.returnValue(1); - spyOn(explorationWarningsService, 'getWarnings') - .and.returnValue(['something']); - - explorationSaveService.saveChangesAsync(startLoadingCb, endLoadingCb); - flush(); - tick(); - flush(); - - expect(explorationWarningsService.countWarnings).toHaveBeenCalled(); - })); - - it('should not open exploration save modal if ' + - 'it is already opened', fakeAsync(() => { - let startLoadingCb = jasmine.createSpy('startLoadingCb'); - let endLoadingCb = jasmine.createSpy('endLoadingCb'); - let sampleStates = statesObjectFactory.createFromBackendDict( - statesBackendDict); - spyOn(routerService, 'savePendingChanges') - .and.returnValue(); - spyOn(explorationStatesService, 'getStates') - .and.returnValue(sampleStates); - spyOn(explorationDiffService, 'getDiffGraphData') - .and.returnValue({ + it( + 'should not open exploration save modal in case of ' + 'backend error', + fakeAsync(() => { + let startLoadingCb = jasmine.createSpy('startLoadingCb'); + let endLoadingCb = jasmine.createSpy('endLoadingCb'); + + spyOn(routerService, 'savePendingChanges').and.stub(); + spyOn(alertsService, 'addWarning').and.stub(); + spyOn(explorationRightsService, 'isPrivate').and.returnValue(false); + spyOn(explorationWarningsService, 'countWarnings').and.returnValue(1); + spyOn(explorationWarningsService, 'getWarnings').and.returnValue([ + 'something', + ]); + + explorationSaveService.saveChangesAsync(startLoadingCb, endLoadingCb); + flush(); + tick(); + flush(); + + expect(explorationWarningsService.countWarnings).toHaveBeenCalled(); + }) + ); + + it( + 'should not open exploration save modal if ' + 'it is already opened', + fakeAsync(() => { + let startLoadingCb = jasmine.createSpy('startLoadingCb'); + let endLoadingCb = jasmine.createSpy('endLoadingCb'); + let sampleStates = + statesObjectFactory.createFromBackendDict(statesBackendDict); + spyOn(routerService, 'savePendingChanges').and.returnValue(); + spyOn(explorationStatesService, 'getStates').and.returnValue( + sampleStates + ); + spyOn(explorationDiffService, 'getDiffGraphData').and.returnValue({ nodes: { nodes: { newestStateName: 'nodes', originalStateName: 'originalStateName', stateProperty: 'stateProperty', - } + }, }, - links: [{ - source: 0, - target: 0, - linkProperty: 'links' - }], + links: [ + { + source: 0, + target: 0, + linkProperty: 'links', + }, + ], finalStateIds: ['finalStaeIds'], originalStateIds: {Hola: 0}, stateIds: {Hola: 0}, } as ProcessedStateIdsAndData); - let modalSpy = spyOn(ngbModal, 'open').and.returnValue( - { - componentInstance: { - isExplorationPrivate: true, - diffData: null, - }, - result: Promise.resolve('commitMessage') - } as NgbModalRef); - - // Opening modal first time. - explorationSaveService.saveChangesAsync( - startLoadingCb, endLoadingCb); - // Opening modal second time. - explorationSaveService.saveChangesAsync( - startLoadingCb, endLoadingCb); - // We need multiple '$rootScope.$apply()' here since, the source code - // consists of nested promises. - flush(); - flush(); - - expect(modalSpy).toHaveBeenCalledTimes(1); - })); + let modalSpy = spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: { + isExplorationPrivate: true, + diffData: null, + }, + result: Promise.resolve('commitMessage'), + } as NgbModalRef); - it('should focus on the exploration save modal ' + - 'when modal is opened', fakeAsync(() => { - let startLoadingCb = jasmine.createSpy('startLoadingCb'); - let endLoadingCb = jasmine.createSpy('endLoadingCb'); - let sampleStates = statesObjectFactory.createFromBackendDict( - statesBackendDict); - spyOn(routerService, 'savePendingChanges') - .and.returnValue(); - spyOn(explorationStatesService, 'getStates') - .and.returnValue(sampleStates); - spyOn(explorationDiffService, 'getDiffGraphData') - .and.returnValue({ + // Opening modal first time. + explorationSaveService.saveChangesAsync(startLoadingCb, endLoadingCb); + // Opening modal second time. + explorationSaveService.saveChangesAsync(startLoadingCb, endLoadingCb); + // We need multiple '$rootScope.$apply()' here since, the source code + // consists of nested promises. + flush(); + flush(); + + expect(modalSpy).toHaveBeenCalledTimes(1); + }) + ); + + it( + 'should focus on the exploration save modal ' + 'when modal is opened', + fakeAsync(() => { + let startLoadingCb = jasmine.createSpy('startLoadingCb'); + let endLoadingCb = jasmine.createSpy('endLoadingCb'); + let sampleStates = + statesObjectFactory.createFromBackendDict(statesBackendDict); + spyOn(routerService, 'savePendingChanges').and.returnValue(); + spyOn(explorationStatesService, 'getStates').and.returnValue( + sampleStates + ); + spyOn(explorationDiffService, 'getDiffGraphData').and.returnValue({ nodes: { nodes: { newestStateName: 'newestStateName', originalStateName: 'originalStateName', stateProperty: 'stateProperty', - } + }, }, - links: [{ - source: 0, - target: 0, - linkProperty: 'links' - }], + links: [ + { + source: 0, + target: 0, + linkProperty: 'links', + }, + ], finalStateIds: ['finalStaeIds'], originalStateIds: {Hola: 0}, stateIds: {Hola: 0}, } as ProcessedStateIdsAndData); - changeListServiceSpy.and.returnValue(Promise.resolve(null)); - let modalSpy = spyOn(ngbModal, 'open').and.returnValue( - { + changeListServiceSpy.and.returnValue(Promise.resolve(null)); + let modalSpy = spyOn(ngbModal, 'open').and.returnValue({ componentInstance: { isExplorationPrivate: true, diffData: null, }, - result: Promise.resolve('commitMessage') + result: Promise.resolve('commitMessage'), } as NgbModalRef); - explorationSaveService.saveChangesAsync( - startLoadingCb, endLoadingCb); - // We need multiple '$rootScope.$apply()' here since, the source code - // consists of nested promises. - flush(); - tick(); - flush(); - - expect(modalSpy).toHaveBeenCalled(); - })); + explorationSaveService.saveChangesAsync(startLoadingCb, endLoadingCb); + // We need multiple '$rootScope.$apply()' here since, the source code + // consists of nested promises. + flush(); + tick(); + flush(); - it('should not focus on exploration save modal in case ' + - 'of backend error', fakeAsync(() => { - let startLoadingCb = jasmine.createSpy('startLoadingCb'); - let endLoadingCb = jasmine.createSpy('endLoadingCb'); - let sampleStates = statesObjectFactory.createFromBackendDict( - statesBackendDict); - spyOn(routerService, 'savePendingChanges') - .and.returnValue(); - spyOn(explorationStatesService, 'getStates') - .and.returnValue(sampleStates); - spyOn(explorationDiffService, 'getDiffGraphData') - .and.returnValue({ + expect(modalSpy).toHaveBeenCalled(); + }) + ); + + it( + 'should not focus on exploration save modal in case ' + 'of backend error', + fakeAsync(() => { + let startLoadingCb = jasmine.createSpy('startLoadingCb'); + let endLoadingCb = jasmine.createSpy('endLoadingCb'); + let sampleStates = + statesObjectFactory.createFromBackendDict(statesBackendDict); + spyOn(routerService, 'savePendingChanges').and.returnValue(); + spyOn(explorationStatesService, 'getStates').and.returnValue( + sampleStates + ); + spyOn(explorationDiffService, 'getDiffGraphData').and.returnValue({ nodes: { nodes: { newestStateName: 'newestStateName', originalStateName: 'originalStateName', stateProperty: 'stateProperty', - } + }, }, - links: [{ - source: 0, - target: 0, - linkProperty: 'links' - }], + links: [ + { + source: 0, + target: 0, + linkProperty: 'links', + }, + ], finalStateIds: ['finalStaeIds'], originalStateIds: {Hola: 0}, stateIds: {Hola: 0}, } as ProcessedStateIdsAndData); - let modalSpy = spyOn(ngbModal, 'open').and.returnValue( - { + let modalSpy = spyOn(ngbModal, 'open').and.returnValue({ componentInstance: { isExplorationPrivate: true, diffData: null, }, - result: Promise.reject() + result: Promise.reject(), } as NgbModalRef); - explorationSaveService.saveChangesAsync( - startLoadingCb, endLoadingCb); - tick(); + explorationSaveService.saveChangesAsync(startLoadingCb, endLoadingCb); + tick(); - expect(modalSpy).toHaveBeenCalled(); - })); + expect(modalSpy).toHaveBeenCalled(); + }) + ); }); diff --git a/core/templates/pages/exploration-editor-page/services/exploration-save.service.ts b/core/templates/pages/exploration-editor-page/services/exploration-save.service.ts index 6c0e917744a1..1ac4822c7de2 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-save.service.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-save.service.ts @@ -16,43 +16,43 @@ * @fileoverview Service for exploration saving & publication functionality. */ -import { Injectable, EventEmitter } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { AppConstants } from 'app.constants'; -import { PostPublishModalComponent } from 'pages/exploration-editor-page/modal-templates/post-publish-modal.component'; -import { ExplorationPublishModalComponent } from 'pages/exploration-editor-page/modal-templates/exploration-publish-modal.component'; -import { EditorReloadingModalComponent } from 'pages/exploration-editor-page/modal-templates/editor-reloading-modal.component'; -import { ConfirmDiscardChangesModalComponent } from 'pages/exploration-editor-page/modal-templates/confirm-discard-changes-modal.component'; -import { ExplorationMetadataModalComponent } from '../modal-templates/exploration-metadata-modal.component'; -import { ExplorationSaveModalComponent } from '../modal-templates/exploration-save-modal.component'; -import { AlertsService } from 'services/alerts.service'; -import { EditabilityService } from 'services/editability.service'; -import { ExternalSaveService } from 'services/external-save.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { AutosaveInfoModalsService } from './autosave-info-modals.service'; -import { ChangeListService } from './change-list.service'; -import { ExplorationCategoryService } from './exploration-category.service'; -import { ExplorationDataService } from './exploration-data.service'; -import { ExplorationDiffService } from './exploration-diff.service'; -import { ExplorationInitStateNameService } from './exploration-init-state-name.service'; -import { ExplorationLanguageCodeService } from './exploration-language-code.service'; -import { ExplorationObjectiveService } from './exploration-objective.service'; -import { ExplorationRightsService } from './exploration-rights.service'; -import { ExplorationStatesService } from './exploration-states.service'; -import { ExplorationTagsService } from './exploration-tags.service'; -import { ExplorationTitleService } from './exploration-title.service'; -import { ExplorationWarningsService } from './exploration-warnings.service'; -import { RouterService } from './router.service'; -import { StatesObjectFactory } from 'domain/exploration/StatesObjectFactory'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { LoggerService } from 'services/contextual/logger.service'; -import { HttpErrorResponse } from '@angular/common/http'; -import { ParamChange } from 'domain/exploration/ParamChangeObjectFactory'; -import { DiffNodeData } from 'components/version-diff-visualization/version-diff-visualization.component'; +import {Injectable, EventEmitter} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {AppConstants} from 'app.constants'; +import {PostPublishModalComponent} from 'pages/exploration-editor-page/modal-templates/post-publish-modal.component'; +import {ExplorationPublishModalComponent} from 'pages/exploration-editor-page/modal-templates/exploration-publish-modal.component'; +import {EditorReloadingModalComponent} from 'pages/exploration-editor-page/modal-templates/editor-reloading-modal.component'; +import {ConfirmDiscardChangesModalComponent} from 'pages/exploration-editor-page/modal-templates/confirm-discard-changes-modal.component'; +import {ExplorationMetadataModalComponent} from '../modal-templates/exploration-metadata-modal.component'; +import {ExplorationSaveModalComponent} from '../modal-templates/exploration-save-modal.component'; +import {AlertsService} from 'services/alerts.service'; +import {EditabilityService} from 'services/editability.service'; +import {ExternalSaveService} from 'services/external-save.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {AutosaveInfoModalsService} from './autosave-info-modals.service'; +import {ChangeListService} from './change-list.service'; +import {ExplorationCategoryService} from './exploration-category.service'; +import {ExplorationDataService} from './exploration-data.service'; +import {ExplorationDiffService} from './exploration-diff.service'; +import {ExplorationInitStateNameService} from './exploration-init-state-name.service'; +import {ExplorationLanguageCodeService} from './exploration-language-code.service'; +import {ExplorationObjectiveService} from './exploration-objective.service'; +import {ExplorationRightsService} from './exploration-rights.service'; +import {ExplorationStatesService} from './exploration-states.service'; +import {ExplorationTagsService} from './exploration-tags.service'; +import {ExplorationTitleService} from './exploration-title.service'; +import {ExplorationWarningsService} from './exploration-warnings.service'; +import {RouterService} from './router.service'; +import {StatesObjectFactory} from 'domain/exploration/StatesObjectFactory'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {LoggerService} from 'services/contextual/logger.service'; +import {HttpErrorResponse} from '@angular/common/http'; +import {ParamChange} from 'domain/exploration/ParamChangeObjectFactory'; +import {DiffNodeData} from 'components/version-diff-visualization/version-diff-visualization.component'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ExplorationSaveService { // Whether or not a save action is currently in progress @@ -91,40 +91,48 @@ export class ExplorationSaveService { private routerService: RouterService, private siteAnalyticsService: SiteAnalyticsService, private statesObjectFactory: StatesObjectFactory, - private windowRef: WindowRef, - ) { } + private windowRef: WindowRef + ) {} showCongratulatorySharingModal(): void { - this.ngbModal.open(PostPublishModalComponent, { - backdrop: true - }).result.then(() => { }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + this.ngbModal + .open(PostPublishModalComponent, { + backdrop: true, + }) + .result.then( + () => {}, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } openPublishExplorationModal( - onStartSaveCallback: Function, - onSaveDoneCallback: Function): Promise { + onStartSaveCallback: Function, + onSaveDoneCallback: Function + ): Promise { // This is resolved when modal is closed. - return this.ngbModal.open(ExplorationPublishModalComponent, { - backdrop: 'static', - }).result.then(() => { - if (onStartSaveCallback) { - onStartSaveCallback(); - } - return this.explorationRightsService.publish().then( - () => { + return this.ngbModal + .open(ExplorationPublishModalComponent, { + backdrop: 'static', + }) + .result.then(() => { + if (onStartSaveCallback) { + onStartSaveCallback(); + } + return this.explorationRightsService.publish().then(() => { if (onSaveDoneCallback) { onSaveDoneCallback(); } this.showCongratulatorySharingModal(); this.siteAnalyticsService.registerPublishExplorationEvent( - this.explorationDataService.explorationId); + this.explorationDataService.explorationId + ); }); - }); + }); } saveDraftToBackend(commitMessage: string): Promise { @@ -134,58 +142,68 @@ export class ExplorationSaveService { const changeList = this.changeListService.getChangeList(); if (this.explorationRightsService.isPrivate()) { - this.siteAnalyticsService - .registerCommitChangesToPrivateExplorationEvent( - this.explorationDataService.explorationId); + this.siteAnalyticsService.registerCommitChangesToPrivateExplorationEvent( + this.explorationDataService.explorationId + ); } else { this.siteAnalyticsService.registerCommitChangesToPublicExplorationEvent( - this.explorationDataService.explorationId); + this.explorationDataService.explorationId + ); } if (this.explorationWarningsService.countWarnings() === 0) { this.siteAnalyticsService.registerSavePlayableExplorationEvent( - this.explorationDataService.explorationId); + this.explorationDataService.explorationId + ); } this.saveIsInProgress = true; this.editabilityService.markNotEditable(); this.explorationDataService.save( - changeList, commitMessage, + changeList, + commitMessage, (isDraftVersionValid, draftChanges) => { - if (isDraftVersionValid === false && - draftChanges !== null && - draftChanges.length > 0) { - this.autosaveInfoModalsService.showVersionMismatchModal( - changeList); + if ( + isDraftVersionValid === false && + draftChanges !== null && + draftChanges.length > 0 + ) { + this.autosaveInfoModalsService.showVersionMismatchModal(changeList); return; } this.logger.info( - 'Changes to this exploration were saved successfully.'); - - this.changeListService.discardAllChanges().then(() => { - this._initExplorationPageEventEmitter.emit(); - this.routerService.onRefreshVersionHistory.emit({ - forceRefresh: true - }); - this.alertsService.addSuccessMessage('Changes saved.', 5000); - this.saveIsInProgress = false; - this.editabilityService.markEditable(); - resolve(); - }, () => { - this.editabilityService.markEditable(); - resolve(); - }); - // The type of error 'e' is unknown because anything can be throw - // in TypeScript. We need to make sure to check the type of 'e'. - }, (errorResponse: unknown) => { + 'Changes to this exploration were saved successfully.' + ); + + this.changeListService.discardAllChanges().then( + () => { + this._initExplorationPageEventEmitter.emit(); + this.routerService.onRefreshVersionHistory.emit({ + forceRefresh: true, + }); + this.alertsService.addSuccessMessage('Changes saved.', 5000); + this.saveIsInProgress = false; + this.editabilityService.markEditable(); + resolve(); + }, + () => { + this.editabilityService.markEditable(); + resolve(); + } + ); + // The type of error 'e' is unknown because anything can be throw + // in TypeScript. We need to make sure to check the type of 'e'. + }, + (errorResponse: unknown) => { this.saveIsInProgress = false; resolve(); this.editabilityService.markEditable(); let httpErrorResponse = errorResponse as HttpErrorResponse; const errorMessage = httpErrorResponse.error.error; this.alertsService.addWarning( - 'Error! Changes could not be saved - ' + errorMessage); + 'Error! Changes could not be saved - ' + errorMessage + ); } ); }); @@ -197,47 +215,60 @@ export class ExplorationSaveService { !this.explorationObjectiveService.savedMemento || !this.explorationCategoryService.savedMemento || this.explorationLanguageCodeService.savedMemento === - AppConstants.DEFAULT_LANGUAGE_CODE || - (this.explorationTagsService.savedMemento as ParamChange[]).length === 0); + AppConstants.DEFAULT_LANGUAGE_CODE || + (this.explorationTagsService.savedMemento as ParamChange[]).length === 0 + ); } async saveChangesAsync( - onStartLoadingCallback: Function, - onEndLoadingCallback: Function + onStartLoadingCallback: Function, + onEndLoadingCallback: Function ): Promise { // This is marked as resolved after modal is closed, so we can change // controller 'saveIsInProgress' back to false. return new Promise((resolve, reject) => { this.routerService.savePendingChanges(); - if (!this.explorationRightsService.isPrivate() && - this.explorationWarningsService.countWarnings() > 0) { - // If the exploration is not private, warnings should be fixed before - // it can be saved. + if ( + !this.explorationRightsService.isPrivate() && + this.explorationWarningsService.countWarnings() > 0 + ) { + // If the exploration is not private, warnings should be fixed before + // it can be saved. this.alertsService.addWarning( - this.explorationWarningsService.getWarnings()[0] as string); + this.explorationWarningsService.getWarnings()[0] as string + ); return; } - this.explorationDataService.getLastSavedDataAsync().then((data) => { - const oldStates = this.statesObjectFactory.createFromBackendDict( - data.states).getStateObjects(); - const newStates = this.explorationStatesService.getStates() + this.explorationDataService.getLastSavedDataAsync().then(data => { + const oldStates = this.statesObjectFactory + .createFromBackendDict(data.states) + .getStateObjects(); + const newStates = this.explorationStatesService + .getStates() .getStateObjects(); const diffGraphData = this.explorationDiffService.getDiffGraphData( - oldStates, newStates, [{ - changeList: this.changeListService.getChangeList(), - directionForwards: true - }]); + oldStates, + newStates, + [ + { + changeList: this.changeListService.getChangeList(), + directionForwards: true, + }, + ] + ); this.diffData = { nodes: diffGraphData.nodes, links: diffGraphData.links, finalStateIds: diffGraphData.finalStateIds, v1InitStateId: diffGraphData.originalStateIds[data.init_state_name], - v2InitStateId: diffGraphData.stateIds[ - this.explorationInitStateNameService.displayed as string], + v2InitStateId: + diffGraphData.stateIds[ + this.explorationInitStateNameService.displayed as string + ], v1States: oldStates, - v2States: newStates + v2States: newStates, }; // TODO(wxy): After diff supports exploration metadata, add a check @@ -255,29 +286,32 @@ export class ExplorationSaveService { windowClass: 'oppia-save-exploration-modal', }); - modalInstance.componentInstance.isExplorationPrivate = ( - this.explorationRightsService.isPrivate()); + modalInstance.componentInstance.isExplorationPrivate = + this.explorationRightsService.isPrivate(); modalInstance.componentInstance.diffData = this.diffData; // Modal is Opened. this.modalIsOpen = true; - modalInstance.result.then((commitMessage) => { - this.modalIsOpen = false; + modalInstance.result.then( + commitMessage => { + this.modalIsOpen = false; - // Toggle loading dots back on for loading from backend. - if (onStartLoadingCallback) { - onStartLoadingCallback(); - } + // Toggle loading dots back on for loading from backend. + if (onStartLoadingCallback) { + onStartLoadingCallback(); + } - this.saveDraftToBackend(commitMessage).then(() => { + this.saveDraftToBackend(commitMessage).then(() => { + resolve(); + }); + }, + () => { + this.alertsService.clearWarnings(); + this.modalIsOpen = false; resolve(); - }); - }, () => { - this.alertsService.clearWarnings(); - this.modalIsOpen = false; - resolve(); - }); + } + ); }); }); } @@ -287,10 +321,12 @@ export class ExplorationSaveService { } showPublishExplorationModal( - onStartLoadingCallback: Function, - onEndLoadingCallback: Function): Promise { + onStartLoadingCallback: Function, + onEndLoadingCallback: Function + ): Promise { this.siteAnalyticsService.registerOpenPublishExplorationModalEvent( - this.explorationDataService.explorationId); + this.explorationDataService.explorationId + ); this.alertsService.clearWarnings(); // If the metadata has not yet been specified, open the pre-publication @@ -298,90 +334,111 @@ export class ExplorationSaveService { if (!this.isAdditionalMetadataNeeded()) { // No further metadata is needed. Open the publish modal immediately. return this.openPublishExplorationModal( - onStartLoadingCallback, onEndLoadingCallback); + onStartLoadingCallback, + onEndLoadingCallback + ); } const modalInstance = this.ngbModal.open( - ExplorationMetadataModalComponent, { + ExplorationMetadataModalComponent, + { backdrop: 'static', - }); - - return modalInstance.result.then((metadataList) => { - if (metadataList.length > 0) { - const commitMessage = ( - 'Add metadata: ' + metadataList.join(', ') + '.'); + } + ); - if (onStartLoadingCallback) { - onStartLoadingCallback(); - } + return modalInstance.result + .then(metadataList => { + if (metadataList.length > 0) { + const commitMessage = + 'Add metadata: ' + metadataList.join(', ') + '.'; - return this.saveDraftToBackend(commitMessage).then(() => { - if (onEndLoadingCallback) { - onEndLoadingCallback(); + if (onStartLoadingCallback) { + onStartLoadingCallback(); } + + return this.saveDraftToBackend(commitMessage).then(() => { + if (onEndLoadingCallback) { + onEndLoadingCallback(); + } + return this.openPublishExplorationModal( + onStartLoadingCallback, + onEndLoadingCallback + ); + }); + } else { return this.openPublishExplorationModal( - onStartLoadingCallback, onEndLoadingCallback); - }); - } else { - return this.openPublishExplorationModal( - onStartLoadingCallback, onEndLoadingCallback); - } - }).catch((error) => { - this.explorationTitleService.restoreFromMemento(); - this.explorationObjectiveService.restoreFromMemento(); - this.explorationCategoryService.restoreFromMemento(); - this.explorationLanguageCodeService.restoreFromMemento(); - this.explorationTagsService.restoreFromMemento(); - throw error; - }); + onStartLoadingCallback, + onEndLoadingCallback + ); + } + }) + .catch(error => { + this.explorationTitleService.restoreFromMemento(); + this.explorationObjectiveService.restoreFromMemento(); + this.explorationCategoryService.restoreFromMemento(); + this.explorationLanguageCodeService.restoreFromMemento(); + this.explorationTagsService.restoreFromMemento(); + throw error; + }); } isExplorationSaveable(): boolean { return ( this.changeListService.isExplorationLockedForEditing() && - !this.saveIsInProgress && ( - ( - this.explorationRightsService.isPrivate() && - !this.explorationWarningsService.hasCriticalWarnings()) || - ( - !this.explorationRightsService.isPrivate() && - this.explorationWarningsService.countWarnings() === 0) - ) + !this.saveIsInProgress && + ((this.explorationRightsService.isPrivate() && + !this.explorationWarningsService.hasCriticalWarnings()) || + (!this.explorationRightsService.isPrivate() && + this.explorationWarningsService.countWarnings() === 0)) ); } discardChanges(): void { - this.ngbModal.open(ConfirmDiscardChangesModalComponent, { - backdrop: 'static', - }).result.then(() => { - this.alertsService.clearWarnings(); - this.externalSaveService.onExternalSave.emit(); - - this.ngbModal.open(EditorReloadingModalComponent, { + this.ngbModal + .open(ConfirmDiscardChangesModalComponent, { backdrop: 'static', - keyboard: false, - windowClass: 'oppia-loading-modal' - }).result.then(() => { }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + }) + .result.then( + () => { + this.alertsService.clearWarnings(); + this.externalSaveService.onExternalSave.emit(); + + this.ngbModal + .open(EditorReloadingModalComponent, { + backdrop: 'static', + keyboard: false, + windowClass: 'oppia-loading-modal', + }) + .result.then( + () => {}, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); - this.changeListService.discardAllChanges().then(() => { - this.alertsService.addSuccessMessage('Changes discarded.'); - this._initExplorationPageEventEmitter.emit(); + this.changeListService.discardAllChanges().then(() => { + this.alertsService.addSuccessMessage('Changes discarded.'); + this._initExplorationPageEventEmitter.emit(); - // The reload is necessary because, otherwise, the - // exploration-with-draft-changes will be reloaded - // (since it is already cached in ExplorationDataService). - this.windowRef.nativeWindow.location.reload(); - }); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + // The reload is necessary because, otherwise, the + // exploration-with-draft-changes will be reloaded + // (since it is already cached in ExplorationDataService). + this.windowRef.nativeWindow.location.reload(); + }); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } } -angular.module('oppia').factory( - 'ExplorationSaveService', downgradeInjectable(ExplorationSaveService)); +angular + .module('oppia') + .factory( + 'ExplorationSaveService', + downgradeInjectable(ExplorationSaveService) + ); diff --git a/core/templates/pages/exploration-editor-page/services/exploration-states.service.spec.ts b/core/templates/pages/exploration-editor-page/services/exploration-states.service.spec.ts index 497ae363bf51..826d58d6df9a 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-states.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-states.service.spec.ts @@ -16,18 +16,21 @@ * @fileoverview Tests for ExplorationStatesService. */ -import { ChangeListService } from './change-list.service'; -import { fakeAsync, flushMicrotasks, TestBed } from '@angular/core/testing'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { ContextService } from 'services/context.service'; -import { ExplorationStatesService } from './exploration-states.service'; -import { AnswerGroup, AnswerGroupObjectFactory } from 'domain/exploration/AnswerGroupObjectFactory'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { GenerateContentIdService } from 'services/generate-content-id.service'; +import {ChangeListService} from './change-list.service'; +import {fakeAsync, flushMicrotasks, TestBed} from '@angular/core/testing'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {ContextService} from 'services/context.service'; +import {ExplorationStatesService} from './exploration-states.service'; +import { + AnswerGroup, + AnswerGroupObjectFactory, +} from 'domain/exploration/AnswerGroupObjectFactory'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {GenerateContentIdService} from 'services/generate-content-id.service'; class MockNgbModalRef { componentInstance = { - deleteStateName: null + deleteStateName: null, }; } @@ -35,7 +38,7 @@ class MockNgbModal { open() { return { componentInstance: MockNgbModalRef, - result: Promise.resolve('Hola') + result: Promise.resolve('Hola'), }; } } @@ -56,9 +59,9 @@ describe('ExplorationStatesService', () => { ChangeListService, { provide: NgbModal, - useClass: MockNgbModal - } - ] + useClass: MockNgbModal, + }, + ], }); }); @@ -69,98 +72,109 @@ describe('ExplorationStatesService', () => { explorationStatesService = TestBed.inject(ExplorationStatesService); answerGroupObjectFactory = TestBed.inject(AnswerGroupObjectFactory); generateContentIdService = TestBed.inject(GenerateContentIdService); - generateContentIdService.init(() => 0, () => {}); + generateContentIdService.init( + () => 0, + () => {} + ); }); beforeEach(() => { let EXP_ID = '7'; spyOn(contextService, 'getExplorationId').and.returnValue(EXP_ID); - answerGroup = answerGroupObjectFactory.createFromBackendDict({ - rule_specs: [{ - rule_type: 'Contains', - inputs: { - x: { - contentId: 'rule_input', - normalizedStrSet: ['hola'] - } - } - }], - outcome: { - dest: 'Me Llamo', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_1', - html: 'buen trabajo!', - }, - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null - }, - training_data: [], - tagged_skill_misconception_id: null - }, 'TextInput'); - - explorationStatesService.init({ - Hola: { - content: {content_id: 'content', html: ''}, - recorded_voiceovers: { - voiceovers_mapping: { - content: {}, - default_outcome: {}, - feedback_1: {}, - rule_input: {} + answerGroup = answerGroupObjectFactory.createFromBackendDict( + { + rule_specs: [ + { + rule_type: 'Contains', + inputs: { + x: { + contentId: 'rule_input', + normalizedStrSet: ['hola'], + }, + }, + }, + ], + outcome: { + dest: 'Me Llamo', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: 'buen trabajo!', }, + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, }, - param_changes: [], - interaction: { - confirmed_unclassified_answers: [], - answer_groups: [answerGroup.toBackendDict()], - customization_args: { - placeholder: { - value: { - content_id: 'ca_placeholder_0', - unicode_str: '' - } + training_data: [], + tagged_skill_misconception_id: null, + }, + 'TextInput' + ); + + explorationStatesService.init( + { + Hola: { + content: {content_id: 'content', html: ''}, + recorded_voiceovers: { + voiceovers_mapping: { + content: {}, + default_outcome: {}, + feedback_1: {}, + rule_input: {}, }, - rows: { value: 1 }, - catchMisspellings: { - value: false - } }, - default_outcome: { - dest: 'Me Llamo', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_1', - html: 'buen trabajo!', + param_changes: [], + interaction: { + confirmed_unclassified_answers: [], + answer_groups: [answerGroup.toBackendDict()], + customization_args: { + placeholder: { + value: { + content_id: 'ca_placeholder_0', + unicode_str: '', + }, + }, + rows: {value: 1}, + catchMisspellings: { + value: false, + }, }, - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null + default_outcome: { + dest: 'Me Llamo', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: 'buen trabajo!', + }, + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + }, + hints: [], + id: 'TextInput', + solution: null, }, - hints: [], - id: 'TextInput', - solution: null, + linked_skill_id: null, + solicit_answer_details: false, + classifier_model_id: '0', + card_is_checkpoint: false, }, - linked_skill_id: null, - solicit_answer_details: false, - classifier_model_id: '0', - card_is_checkpoint: false, }, - }, false); + false + ); }); describe('Callback Registration', () => { describe('.registerOnStateAddedCallback', () => { it('should callback when a new state is added', fakeAsync(() => { spyOn(ngbModal, 'open').and.callFake(() => { - return ({ + return { componentInstance: NgbModalRef, - result: Promise.resolve() - } as NgbModalRef); + result: Promise.resolve(), + } as NgbModalRef; }); let spy = jasmine.createSpy('callback'); spyOn(changeListService, 'addState'); @@ -175,10 +189,10 @@ describe('ExplorationStatesService', () => { describe('.registerOnStateDeletedCallback', () => { it('should callback when a state is deleted', fakeAsync(() => { spyOn(ngbModal, 'open').and.callFake(() => { - return ({ + return { componentInstance: MockNgbModalRef, - result: Promise.resolve('Hola') - } as NgbModalRef); + result: Promise.resolve('Hola'), + } as NgbModalRef; }); spyOn(changeListService, 'deleteState'); @@ -205,29 +219,34 @@ describe('ExplorationStatesService', () => { }); describe('.registerOnStateInteractionSaved', () => { - it('should callback when answer groups of a state are saved', - () => { - let spy = jasmine.createSpy('callback'); - spyOn(changeListService, 'editStateProperty'); + it('should callback when answer groups of a state are saved', () => { + let spy = jasmine.createSpy('callback'); + spyOn(changeListService, 'editStateProperty'); - explorationStatesService.registerOnStateInteractionSavedCallback(spy); - explorationStatesService.saveInteractionAnswerGroups( - 'Hola', [answerGroup]); + explorationStatesService.registerOnStateInteractionSavedCallback(spy); + explorationStatesService.saveInteractionAnswerGroups('Hola', [ + answerGroup, + ]); - expect(spy).toHaveBeenCalledWith( - explorationStatesService.getState('Hola')); - }); + expect(spy).toHaveBeenCalledWith( + explorationStatesService.getState('Hola') + ); + }); }); }); it('should save the solicitAnswerDetails correctly', () => { expect( - explorationStatesService.getSolicitAnswerDetailsMemento( - 'Hola')).toEqual(false); + explorationStatesService.getSolicitAnswerDetailsMemento('Hola') + ).toEqual(false); const changeListSpy = spyOn(changeListService, 'editStateProperty'); explorationStatesService.saveSolicitAnswerDetails('Hola', true); expect(changeListSpy).toHaveBeenCalledWith( - 'Hola', 'solicit_answer_details', true, false); + 'Hola', + 'solicit_answer_details', + true, + false + ); expect( explorationStatesService.getSolicitAnswerDetailsMemento('Hola') ).toBeTrue(); diff --git a/core/templates/pages/exploration-editor-page/services/exploration-states.service.ts b/core/templates/pages/exploration-editor-page/services/exploration-states.service.ts index 6df9a2f9a3cb..681c14077cd0 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-states.service.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-states.service.ts @@ -18,45 +18,59 @@ * keeps no mementos. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { EventEmitter, Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {EventEmitter, Injectable} from '@angular/core'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; import cloneDeep from 'lodash/cloneDeep'; import isEqual from 'lodash/isEqual'; -import { Interaction } from 'domain/exploration/InteractionObjectFactory'; -import { ConfirmDeleteStateModalComponent } from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/confirm-delete-state-modal.component'; -import { ContextService } from 'services/context.service'; -import { ChangeListService, StatePropertyNames, StatePropertyValues } from 'pages/exploration-editor-page/services/change-list.service'; -import { StateObjectsBackendDict, States, StatesObjectFactory } from 'domain/exploration/StatesObjectFactory'; -import { SolutionValidityService } from 'pages/exploration-editor-page/editor-tab/services/solution-validity.service'; -import { AnswerClassificationService } from 'pages/exploration-player-page/services/answer-classification.service'; -import { AngularNameService } from 'pages/exploration-editor-page/services/angular-name.service'; -import { AlertsService } from 'services/alerts.service'; -import { ValidatorsService } from 'services/validators.service'; -import { ExplorationInitStateNameService } from 'pages/exploration-editor-page/services/exploration-init-state-name.service'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { StateEditorRefreshService } from 'pages/exploration-editor-page/services/state-editor-refresh.service'; -import { State } from 'domain/state/StateObjectFactory'; -import { NormalizeWhitespacePipe } from 'filters/string-utility-filters/normalize-whitespace.pipe'; -import { WrittenTranslations } from 'domain/exploration/WrittenTranslationsObjectFactory'; -import { AnswerGroup } from 'domain/exploration/AnswerGroupObjectFactory'; -import { RecordedVoiceovers } from 'domain/exploration/recorded-voiceovers.model'; -import { Outcome } from 'domain/exploration/OutcomeObjectFactory'; -import { Hint } from 'domain/exploration/hint-object.model'; -import { Solution } from 'domain/exploration/SolutionObjectFactory'; -import { InteractionCustomizationArgs } from 'interactions/customization-args-defs'; -import { ParamSpecs } from 'domain/exploration/ParamSpecsObjectFactory'; -import { ParamChange } from 'domain/exploration/ParamChangeObjectFactory'; -import { SubtitledHtml, SubtitledHtmlBackendDict } from 'domain/exploration/subtitled-html.model'; -import { InteractionRulesRegistryService } from 'services/interaction-rules-registry.service'; -import { GenerateContentIdService } from 'services/generate-content-id.service'; -import { ExplorationNextContentIdIndexService } from 'pages/exploration-editor-page/services/exploration-next-content-id-index.service'; -import { MarkTranslationsAsNeedingUpdateModalComponent } from 'components/forms/forms-templates/mark-translations-as-needing-update-modal.component'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { BaseTranslatableObject, TranslatableField } from 'domain/objects/BaseTranslatableObject.model'; -import { InteractionAnswer } from 'interactions/answer-defs'; +import {Interaction} from 'domain/exploration/InteractionObjectFactory'; +import {ConfirmDeleteStateModalComponent} from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/confirm-delete-state-modal.component'; +import {ContextService} from 'services/context.service'; +import { + ChangeListService, + StatePropertyNames, + StatePropertyValues, +} from 'pages/exploration-editor-page/services/change-list.service'; +import { + StateObjectsBackendDict, + States, + StatesObjectFactory, +} from 'domain/exploration/StatesObjectFactory'; +import {SolutionValidityService} from 'pages/exploration-editor-page/editor-tab/services/solution-validity.service'; +import {AnswerClassificationService} from 'pages/exploration-player-page/services/answer-classification.service'; +import {AngularNameService} from 'pages/exploration-editor-page/services/angular-name.service'; +import {AlertsService} from 'services/alerts.service'; +import {ValidatorsService} from 'services/validators.service'; +import {ExplorationInitStateNameService} from 'pages/exploration-editor-page/services/exploration-init-state-name.service'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {StateEditorRefreshService} from 'pages/exploration-editor-page/services/state-editor-refresh.service'; +import {State} from 'domain/state/StateObjectFactory'; +import {NormalizeWhitespacePipe} from 'filters/string-utility-filters/normalize-whitespace.pipe'; +import {WrittenTranslations} from 'domain/exploration/WrittenTranslationsObjectFactory'; +import {AnswerGroup} from 'domain/exploration/AnswerGroupObjectFactory'; +import {RecordedVoiceovers} from 'domain/exploration/recorded-voiceovers.model'; +import {Outcome} from 'domain/exploration/OutcomeObjectFactory'; +import {Hint} from 'domain/exploration/hint-object.model'; +import {Solution} from 'domain/exploration/SolutionObjectFactory'; +import {InteractionCustomizationArgs} from 'interactions/customization-args-defs'; +import {ParamSpecs} from 'domain/exploration/ParamSpecsObjectFactory'; +import {ParamChange} from 'domain/exploration/ParamChangeObjectFactory'; +import { + SubtitledHtml, + SubtitledHtmlBackendDict, +} from 'domain/exploration/subtitled-html.model'; +import {InteractionRulesRegistryService} from 'services/interaction-rules-registry.service'; +import {GenerateContentIdService} from 'services/generate-content-id.service'; +import {ExplorationNextContentIdIndexService} from 'pages/exploration-editor-page/services/exploration-next-content-id-index.service'; +import {MarkTranslationsAsNeedingUpdateModalComponent} from 'components/forms/forms-templates/mark-translations-as-needing-update-modal.component'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import { + BaseTranslatableObject, + TranslatableField, +} from 'domain/objects/BaseTranslatableObject.model'; +import {InteractionAnswer} from 'interactions/answer-defs'; interface ContentsMapping { [contentId: string]: TranslatableField; @@ -69,14 +83,15 @@ interface ContentExtractors { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ExplorationStatesService { stateAddedCallbacks: ((addedStateName: string) => void)[] = []; stateDeletedCallbacks: ((deletedStateName: string) => void)[] = []; - stateRenamedCallbacks: ( - (oldStateName: string, newStateName: string) => void - )[] = []; + stateRenamedCallbacks: (( + oldStateName: string, + newStateName: string + ) => void)[] = []; initalContentsMapping: ContentsMapping = {}; contentChangesCanAffectTranslations: boolean = true; @@ -102,15 +117,14 @@ export class ExplorationStatesService { private statesObjectFactory: StatesObjectFactory, private validatorsService: ValidatorsService, private generateContentIdService: GenerateContentIdService, - private explorationNextContentIdIndexService: ( - ExplorationNextContentIdIndexService) + private explorationNextContentIdIndexService: ExplorationNextContentIdIndexService ) {} // Properties that have a different backend representation from the // frontend and must be converted. private _BACKEND_CONVERSIONS = { answer_groups: (answerGroups: AnswerGroup[]) => { - return answerGroups.map((answerGroup) => { + return answerGroups.map(answerGroup => { return answerGroup.toBackendDict(); }); }, @@ -128,12 +142,12 @@ export class ExplorationStatesService { } }, hints: (hints: Hint[]) => { - return hints.map((hint) => { + return hints.map(hint => { return hint.toBackendDict(); }); }, param_changes: (paramChanges: ParamChange[]) => { - return paramChanges.map((paramChange) => { + return paramChanges.map(paramChange => { return paramChange.toBackendDict(); }); }, @@ -151,18 +165,21 @@ export class ExplorationStatesService { return writtenTranslations.toBackendDict(); }, widget_customization_args: ( - customizationArgs: InteractionCustomizationArgs + customizationArgs: InteractionCustomizationArgs ) => { return Interaction.convertCustomizationArgsToBackendDict( - customizationArgs); - } + customizationArgs + ); + }, }; // Maps backend names to the corresponding frontend dict accessor lists. PROPERTY_REF_DATA = { answer_groups: ['interaction', 'answerGroups'], confirmed_unclassified_answers: [ - 'interaction', 'confirmedUnclassifiedAnswers'], + 'interaction', + 'confirmedUnclassifiedAnswers', + ], content: ['content'], recorded_voiceovers: ['recordedVoiceovers'], linked_skill_id: ['linkedSkillId'], @@ -174,16 +191,15 @@ export class ExplorationStatesService { card_is_checkpoint: ['cardIsCheckpoint'], solution: ['interaction', 'solution'], widget_id: ['interaction', 'id'], - widget_customization_args: ['interaction', 'customizationArgs'] + widget_customization_args: ['interaction', 'customizationArgs'], }; private _CONTENT_EXTRACTORS = { answer_groups: (answerGroups: BaseTranslatableObject[]) => { let contents: TranslatableField[] = []; - answerGroups.forEach( - answerGroup => { - contents = contents.concat(answerGroup.getAllContents()); - }); + answerGroups.forEach(answerGroup => { + contents = contents.concat(answerGroup.getAllContents()); + }); return contents; }, default_outcome: (defaultOutcome: BaseTranslatableObject) => { @@ -200,29 +216,36 @@ export class ExplorationStatesService { return solution ? solution.getAllContents() : []; }, widget_customization_args: ( - customizationArgs: InteractionCustomizationArgs) => { - return customizationArgs ? Interaction.getCustomizationArgContents( - customizationArgs) : []; - } + customizationArgs: InteractionCustomizationArgs + ) => { + return customizationArgs + ? Interaction.getCustomizationArgContents(customizationArgs) + : []; + }, } as ContentExtractors; _extractContentIds( - backendName: string, value: StatePropertyValues + backendName: string, + value: StatePropertyValues ): Set { let contents: TranslatableField[] = this._CONTENT_EXTRACTORS[backendName]( - value as BaseTranslatableObject | BaseTranslatableObject[]); - return new Set(contents.map(content => (content.contentId as string))); + value as BaseTranslatableObject | BaseTranslatableObject[] + ); + return new Set(contents.map(content => content.contentId as string)); } _verifyChangesInitialContents( - backendName: string, value: StatePropertyValues): void { + backendName: string, + value: StatePropertyValues + ): void { let contents: TranslatableField[]; if (backendName === 'content') { contents = [value as SubtitledHtml]; } else if (this._CONTENT_EXTRACTORS.hasOwnProperty(backendName)) { contents = this._CONTENT_EXTRACTORS[backendName]( - value as BaseTranslatableObject | BaseTranslatableObject[]); + value as BaseTranslatableObject | BaseTranslatableObject[] + ); } else { return; } @@ -236,12 +259,14 @@ export class ExplorationStatesService { let intialContent = this.initalContentsMapping[contentId]; if ( JSON.stringify(BaseTranslatableObject.getContentValue(content)) === - JSON.stringify(BaseTranslatableObject.getContentValue(intialContent))) { + JSON.stringify(BaseTranslatableObject.getContentValue(intialContent)) + ) { continue; } const modalRef = this.ngbModal.open( - MarkTranslationsAsNeedingUpdateModalComponent, { + MarkTranslationsAsNeedingUpdateModalComponent, + { size: 'lg', backdrop: 'static', // TODO(#12768): Remove the backdropClass & windowClass once the @@ -249,13 +274,14 @@ export class ExplorationStatesService { // class is used for correctly stacking AngularJS modal on top of // Angular modal. backdropClass: 'forced-modal-stack', - windowClass: 'forced-modal-stack' - }); + windowClass: 'forced-modal-stack', + } + ); modalRef.componentInstance.contentId = contentId; - modalRef.componentInstance.markNeedsUpdateHandler = ( - this.markTranslationAndVoiceoverNeedsUpdate.bind(this)); - modalRef.componentInstance.removeHandler = ( - this.removeTranslationAndVoiceover.bind(this)); + modalRef.componentInstance.markNeedsUpdateHandler = + this.markTranslationAndVoiceoverNeedsUpdate.bind(this); + modalRef.componentInstance.removeHandler = + this.removeTranslationAndVoiceover.bind(this); this.initalContentsMapping[contentId] = content; } } @@ -283,15 +309,20 @@ export class ExplorationStatesService { } private _getElementsInFirstSetButNotInSecond( - setA: Set, setB: Set): string[] { - let diffList = Array.from(setA).filter((element) => { + setA: Set, + setB: Set + ): string[] { + let diffList = Array.from(setA).filter(element => { return !setB.has(element); }); return diffList as string[]; } private _setState( - stateName: string, stateData: State, refreshGraph: boolean): void { + stateName: string, + stateData: State, + refreshGraph: boolean + ): void { (this._states as States).setState(stateName, cloneDeep(stateData)); if (refreshGraph) { this._refreshGraphEventEmitter.emit(); @@ -299,42 +330,54 @@ export class ExplorationStatesService { } getStatePropertyMemento( - stateName: string, backendName: 'content' + stateName: string, + backendName: 'content' ): SubtitledHtml; getStatePropertyMemento( - stateName: string, backendName: 'param_changes' + stateName: string, + backendName: 'param_changes' ): ParamChange[]; getStatePropertyMemento(stateName: string, backendName: 'widget_id'): string; getStatePropertyMemento( - stateName: string, backendName: 'widget_customization_args' + stateName: string, + backendName: 'widget_customization_args' ): InteractionCustomizationArgs; getStatePropertyMemento( - stateName: string, backendName: 'answer_groups' + stateName: string, + backendName: 'answer_groups' ): AnswerGroup[]; getStatePropertyMemento( - stateName: string, backendName: 'confirmed_unclassified_answers' + stateName: string, + backendName: 'confirmed_unclassified_answers' ): AnswerGroup[]; getStatePropertyMemento( - stateName: string, backendName: 'default_outcome' + stateName: string, + backendName: 'default_outcome' ): Outcome; getStatePropertyMemento(stateName: string, backendName: 'hints'): Hint[]; getStatePropertyMemento( - stateName: string, backendName: 'solution' + stateName: string, + backendName: 'solution' ): SubtitledHtml; getStatePropertyMemento( - stateName: string, backendName: 'recorded_voiceovers' + stateName: string, + backendName: 'recorded_voiceovers' ): RecordedVoiceovers; getStatePropertyMemento( - stateName: string, backendName: 'solicit_answer_details' + stateName: string, + backendName: 'solicit_answer_details' ): boolean; getStatePropertyMemento( - stateName: string, backendName: 'card_is_checkpoint' + stateName: string, + backendName: 'card_is_checkpoint' ): boolean; getStatePropertyMemento( - stateName: string, backendName: StatePropertyNames + stateName: string, + backendName: StatePropertyNames ): StatePropertyValues; getStatePropertyMemento( - stateName: string, backendName: StatePropertyNames + stateName: string, + backendName: StatePropertyNames ): StatePropertyValues { let accessorList: string[] = this.PROPERTY_REF_DATA[backendName]; let propertyRef = (this._states as States).getState(stateName); @@ -343,13 +386,16 @@ export class ExplorationStatesService { propertyRef = propertyRef[key]; }); } catch (e) { - let additionalInfo = ( + let additionalInfo = '\nUndefined states error debug logs:' + - '\nRequested state name: ' + stateName + - '\nExploration ID: ' + this.contextService.getExplorationId() + - '\nChange list: ' + JSON.stringify( - this.changeListService.getChangeList()) + - '\nAll states names: ' + this._states.getStateNames()); + '\nRequested state name: ' + + stateName + + '\nExploration ID: ' + + this.contextService.getExplorationId() + + '\nChange list: ' + + JSON.stringify(this.changeListService.getChangeList()) + + '\nAll states names: ' + + this._states.getStateNames(); e.message += additionalInfo; throw e; } @@ -358,71 +404,96 @@ export class ExplorationStatesService { } saveStateProperty( - stateName: string, backendName: 'content', newValue: SubtitledHtml + stateName: string, + backendName: 'content', + newValue: SubtitledHtml ): void; saveStateProperty( - stateName: string, backendName: 'param_changes', newValue: ParamChange[] + stateName: string, + backendName: 'param_changes', + newValue: ParamChange[] ): void; saveStateProperty( - stateName: string, backendName: 'widget_id', newValue: string + stateName: string, + backendName: 'widget_id', + newValue: string ): void; saveStateProperty( - stateName: string, - backendName: 'widget_customization_args', - newValue: InteractionCustomizationArgs + stateName: string, + backendName: 'widget_customization_args', + newValue: InteractionCustomizationArgs ): void; saveStateProperty( - stateName: string, backendName: 'answer_groups', newValue: AnswerGroup[] + stateName: string, + backendName: 'answer_groups', + newValue: AnswerGroup[] ): void; saveStateProperty( - stateName: string, - backendName: 'confirmed_unclassified_answers', - newValue: AnswerGroup[] + stateName: string, + backendName: 'confirmed_unclassified_answers', + newValue: AnswerGroup[] ): void; saveStateProperty( - stateName: string, backendName: 'default_outcome', newValue: Outcome + stateName: string, + backendName: 'default_outcome', + newValue: Outcome ): void; saveStateProperty( - stateName: string, backendName: 'hints', newValue: Hint[] + stateName: string, + backendName: 'hints', + newValue: Hint[] ): void; saveStateProperty( - stateName: string, backendName: 'solution', newValue: SubtitledHtml + stateName: string, + backendName: 'solution', + newValue: SubtitledHtml ): void; saveStateProperty( - stateName: string, - backendName: 'recorded_voiceovers', - newValue: RecordedVoiceovers + stateName: string, + backendName: 'recorded_voiceovers', + newValue: RecordedVoiceovers ): void; saveStateProperty( - stateName: string, - backendName: 'solicit_answer_details', - newValue: boolean + stateName: string, + backendName: 'solicit_answer_details', + newValue: boolean ): void; saveStateProperty( - stateName: string, backendName: 'card_is_checkpoint', newValue: boolean + stateName: string, + backendName: 'card_is_checkpoint', + newValue: boolean ): void; saveStateProperty( - stateName: string, backendName: 'linked_skill_id', newValue: string + stateName: string, + backendName: 'linked_skill_id', + newValue: string ): void; saveStateProperty( - stateName: string, - backendName: StatePropertyNames, - newValue: StatePropertyValues + stateName: string, + backendName: StatePropertyNames, + newValue: StatePropertyValues ): void { - let oldValue = ( - this.getStatePropertyMemento(stateName, backendName)); + let oldValue = this.getStatePropertyMemento(stateName, backendName); let newBackendValue = cloneDeep(newValue); let oldBackendValue = cloneDeep(oldValue); if (this._BACKEND_CONVERSIONS.hasOwnProperty(backendName)) { - newBackendValue = ( - this.convertToBackendRepresentation(newValue, backendName)); - oldBackendValue = ( - this.convertToBackendRepresentation(oldValue, backendName)); + newBackendValue = this.convertToBackendRepresentation( + newValue, + backendName + ); + oldBackendValue = this.convertToBackendRepresentation( + oldValue, + backendName + ); } if (!isEqual(oldValue, newValue)) { this.changeListService.editStateProperty( - stateName, backendName, newBackendValue, oldBackendValue); + stateName, + backendName, + newBackendValue, + oldBackendValue + ); let newStateData = this._states.getState(stateName); let accessorList = this.PROPERTY_REF_DATA[backendName]; @@ -434,13 +505,17 @@ export class ExplorationStatesService { let oldContentIds = this._extractContentIds(backendName, oldValue); let newContentIds = this._extractContentIds(backendName, newValue); let contentIdsToDelete = this._getElementsInFirstSetButNotInSecond( - oldContentIds, newContentIds); + oldContentIds, + newContentIds + ); let contentIdsToAdd = this._getElementsInFirstSetButNotInSecond( - newContentIds, oldContentIds); - contentIdsToDelete.forEach((contentId) => { + newContentIds, + oldContentIds + ); + contentIdsToDelete.forEach(contentId => { newStateData.recordedVoiceovers.deleteContentId(contentId); }); - contentIdsToAdd.forEach((contentId) => { + contentIdsToAdd.forEach(contentId => { newStateData.recordedVoiceovers.addContentId(contentId); }); } @@ -449,33 +524,34 @@ export class ExplorationStatesService { propertyRef = propertyRef[accessorList[i]]; } - propertyRef[accessorList[accessorList.length - 1]] = cloneDeep( - newValue); + propertyRef[accessorList[accessorList.length - 1]] = cloneDeep(newValue); // We do not refresh the state editor immediately after the interaction // id alone is saved, because the customization args dict will be // temporarily invalid. A change in interaction id will always entail // a change in the customization args dict anyway, so the graph will // get refreshed after both properties have been updated. - let refreshGraph = (backendName !== 'widget_id'); + let refreshGraph = backendName !== 'widget_id'; this._setState(stateName, newStateData, refreshGraph); } } convertToBackendRepresentation( - frontendValue: StatePropertyValues, backendName: string + frontendValue: StatePropertyValues, + backendName: string ): string { let conversionFunction = this._BACKEND_CONVERSIONS[backendName]; return conversionFunction(frontendValue); } init( - statesBackendDict: StateObjectsBackendDict, - contentChangesCanAffectTranslations: boolean): void { - this._states = ( - this.statesObjectFactory.createFromBackendDict(statesBackendDict)); - this.contentChangesCanAffectTranslations = ( - contentChangesCanAffectTranslations); + statesBackendDict: StateObjectsBackendDict, + contentChangesCanAffectTranslations: boolean + ): void { + this._states = + this.statesObjectFactory.createFromBackendDict(statesBackendDict); + this.contentChangesCanAffectTranslations = + contentChangesCanAffectTranslations; // Initialize the solutionValidityService. this.solutionValidityService.init(this._states.getStateNames()); this._states.getStateNames().forEach((stateName: string) => { @@ -483,7 +559,7 @@ export class ExplorationStatesService { let solution = state.interaction.solution; if (solution) { let interactionId = state.interaction.id; - let result = ( + let result = this.answerClassificationService.getMatchingClassificationResult( stateName, state.interaction, @@ -491,15 +567,16 @@ export class ExplorationStatesService { this.interactionRulesRegistryService.getRulesServiceByInteractionId( interactionId ) - ) - ); + ); let solutionIsValid = stateName !== result.outcome.dest; - this.solutionValidityService.updateValidity( - stateName, solutionIsValid); + this.solutionValidityService.updateValidity(stateName, solutionIsValid); } - state.getAllContents().forEach( - content => this.initalContentsMapping[content.contentId] = content); + state + .getAllContents() + .forEach( + content => (this.initalContentsMapping[content.contentId] = content) + ); }); } @@ -526,7 +603,7 @@ export class ExplorationStatesService { getCheckpointCount(): number { let count: number = 0; if (this._states) { - this._states.getStateNames().forEach((stateName) => { + this._states.getStateNames().forEach(stateName => { if (this._states.getState(stateName).cardIsCheckpoint) { count++; } @@ -542,8 +619,7 @@ export class ExplorationStatesService { } return false; } - return ( - this.validatorsService.isValidStateName(newStateName, showWarnings)); + return this.validatorsService.isValidStateName(newStateName, showWarnings); } getStateContentMemento(stateName: string): SubtitledHtml { @@ -559,7 +635,8 @@ export class ExplorationStatesService { } saveStateParamChanges( - stateName: string, newParamChanges: ParamChange[] + stateName: string, + newParamChanges: ParamChange[] ): void { this.saveStateProperty(stateName, 'param_changes', newParamChanges); } @@ -570,7 +647,7 @@ export class ExplorationStatesService { saveInteractionId(stateName: string, newInteractionId: string): void { this.saveStateProperty(stateName, 'widget_id', newInteractionId); - this.stateInteractionSavedCallbacks.forEach((callback) => { + this.stateInteractionSavedCallbacks.forEach(callback => { callback(this._states.getState(stateName)); }); } @@ -580,17 +657,21 @@ export class ExplorationStatesService { } getInteractionCustomizationArgsMemento( - stateName: string + stateName: string ): InteractionCustomizationArgs { return this.getStatePropertyMemento(stateName, 'widget_customization_args'); } saveInteractionCustomizationArgs( - stateName: string, newCustomizationArgs: InteractionCustomizationArgs + stateName: string, + newCustomizationArgs: InteractionCustomizationArgs ): void { this.saveStateProperty( - stateName, 'widget_customization_args', newCustomizationArgs); - this.stateInteractionSavedCallbacks.forEach((callback) => { + stateName, + 'widget_customization_args', + newCustomizationArgs + ); + this.stateInteractionSavedCallbacks.forEach(callback => { callback(this._states.getState(stateName)); }); } @@ -600,25 +681,32 @@ export class ExplorationStatesService { } saveInteractionAnswerGroups( - stateName: string, newAnswerGroups: AnswerGroup[] + stateName: string, + newAnswerGroups: AnswerGroup[] ): void { this.saveStateProperty(stateName, 'answer_groups', newAnswerGroups); - this.stateInteractionSavedCallbacks.forEach((callback) => { + this.stateInteractionSavedCallbacks.forEach(callback => { callback(this._states.getState(stateName)); }); } getConfirmedUnclassifiedAnswersMemento(stateName: string): AnswerGroup[] { return this.getStatePropertyMemento( - stateName, 'confirmed_unclassified_answers'); + stateName, + 'confirmed_unclassified_answers' + ); } saveConfirmedUnclassifiedAnswers( - stateName: string, newAnswers: AnswerGroup[] | InteractionAnswer[] + stateName: string, + newAnswers: AnswerGroup[] | InteractionAnswer[] ): void { this.saveStateProperty( - stateName, 'confirmed_unclassified_answers', newAnswers as AnswerGroup[]); - this.stateInteractionSavedCallbacks.forEach((callback) => { + stateName, + 'confirmed_unclassified_answers', + newAnswers as AnswerGroup[] + ); + this.stateInteractionSavedCallbacks.forEach(callback => { callback(this._states.getState(stateName)); }); } @@ -628,7 +716,8 @@ export class ExplorationStatesService { } saveInteractionDefaultOutcome( - stateName: string, newDefaultOutcome: Outcome + stateName: string, + newDefaultOutcome: Outcome ): void { this.saveStateProperty(stateName, 'default_outcome', newDefaultOutcome); } @@ -654,9 +743,14 @@ export class ExplorationStatesService { } saveRecordedVoiceovers( - stateName: string, newRecordedVoiceovers: RecordedVoiceovers): void { + stateName: string, + newRecordedVoiceovers: RecordedVoiceovers + ): void { this.saveStateProperty( - stateName, 'recorded_voiceovers', newRecordedVoiceovers); + stateName, + 'recorded_voiceovers', + newRecordedVoiceovers + ); } getSolicitAnswerDetailsMemento(stateName: string): boolean { @@ -664,9 +758,14 @@ export class ExplorationStatesService { } saveSolicitAnswerDetails( - stateName: string, newSolicitAnswerDetails: boolean): void { + stateName: string, + newSolicitAnswerDetails: boolean + ): void { this.saveStateProperty( - stateName, 'solicit_answer_details', newSolicitAnswerDetails); + stateName, + 'solicit_answer_details', + newSolicitAnswerDetails + ); } getCardIsCheckpointMemento(stateName: string): boolean { @@ -675,7 +774,10 @@ export class ExplorationStatesService { saveCardIsCheckpoint(stateName: string, newCardIsCheckpoint: boolean): void { this.saveStateProperty( - stateName, 'card_is_checkpoint', newCardIsCheckpoint); + stateName, + 'card_is_checkpoint', + newCardIsCheckpoint + ); } isInitialized(): boolean { @@ -683,7 +785,8 @@ export class ExplorationStatesService { } addState( - newStateName: string, successCallback: (arg0: string) => void + newStateName: string, + successCallback: (arg0: string) => void ): void { newStateName = this.normalizeWhitespacePipe.transform(newStateName); if (!this.validatorsService.isValidStateName(newStateName, true)) { @@ -695,18 +798,24 @@ export class ExplorationStatesService { } this.alertsService.clearWarnings(); - let contentIdForContent = this.generateContentIdService - .getNextStateId('content'); - let contentIdForDefaultOutcome = this.generateContentIdService - .getNextStateId('default_outcome'); + let contentIdForContent = + this.generateContentIdService.getNextStateId('content'); + let contentIdForDefaultOutcome = + this.generateContentIdService.getNextStateId('default_outcome'); this._states.addState( - newStateName, contentIdForContent, contentIdForDefaultOutcome); + newStateName, + contentIdForContent, + contentIdForDefaultOutcome + ); this.changeListService.addState( - newStateName, contentIdForContent, contentIdForDefaultOutcome); + newStateName, + contentIdForContent, + contentIdForDefaultOutcome + ); this.explorationNextContentIdIndexService.saveDisplayedValue(); - this.stateAddedCallbacks.forEach((callback) => { + this.stateAddedCallbacks.forEach(callback => { callback(newStateName); }); this._refreshGraphEventEmitter.emit(); @@ -732,28 +841,32 @@ export class ExplorationStatesService { backdrop: true, }); modalRef.componentInstance.deleteStateName = deleteStateName; - modalRef.result.then(() => { - this._states.deleteState(deleteStateName); + modalRef.result.then( + () => { + this._states.deleteState(deleteStateName); - this.changeListService.deleteState(deleteStateName); + this.changeListService.deleteState(deleteStateName); - if (this.stateEditorService.getActiveStateName() === deleteStateName) { - this.stateEditorService.setActiveStateName( - this.explorationInitStateNameService.savedMemento); - } + if (this.stateEditorService.getActiveStateName() === deleteStateName) { + this.stateEditorService.setActiveStateName( + this.explorationInitStateNameService.savedMemento + ); + } - this.stateDeletedCallbacks.forEach((callback) => { - callback(deleteStateName); - }); - this.windowRef.nativeWindow.location.hash = ( - '/gui/' + this.stateEditorService.getActiveStateName()); - this._refreshGraphEventEmitter.emit(); - // This ensures that if the deletion changes rules in the current - // state, they get updated in the view. - this.stateEditorRefreshService.onRefreshStateEditor.emit(); - }, () => { - this.alertsService.clearWarnings(); - }); + this.stateDeletedCallbacks.forEach(callback => { + callback(deleteStateName); + }); + this.windowRef.nativeWindow.location.hash = + '/gui/' + this.stateEditorService.getActiveStateName(); + this._refreshGraphEventEmitter.emit(); + // This ensures that if the deletion changes rules in the current + // state, they get updated in the view. + this.stateEditorRefreshService.onRefreshStateEditor.emit(); + }, + () => { + this.alertsService.clearWarnings(); + } + ); } renameState(oldStateName: string, newStateName: string): void { @@ -784,24 +897,27 @@ export class ExplorationStatesService { this.explorationInitStateNameService.displayed = newStateName; this.explorationInitStateNameService.saveDisplayedValue(); } - this.stateRenamedCallbacks.forEach((callback) => { + this.stateRenamedCallbacks.forEach(callback => { callback(oldStateName, newStateName); }); this._refreshGraphEventEmitter.emit(); } registerOnStateAddedCallback( - callback: (addedStateName: string) => void): void { + callback: (addedStateName: string) => void + ): void { this.stateAddedCallbacks.push(callback); } registerOnStateDeletedCallback( - callback: (deletedStateName: string) => void): void { + callback: (deletedStateName: string) => void + ): void { this.stateDeletedCallbacks.push(callback); } registerOnStateRenamedCallback( - callback: (oldStateName: string, newStateName: string) => void): void { + callback: (oldStateName: string, newStateName: string) => void + ): void { this.stateRenamedCallbacks.push(callback); } @@ -812,7 +928,8 @@ export class ExplorationStatesService { } registerOnStateInteractionSavedCallback( - callback: (state: State) => void): void { + callback: (state: State) => void + ): void { this.stateInteractionSavedCallbacks.push(callback); } @@ -821,6 +938,9 @@ export class ExplorationStatesService { } } -angular.module('oppia').factory( - 'ExplorationStatesService', - downgradeInjectable(ExplorationStatesService)); +angular + .module('oppia') + .factory( + 'ExplorationStatesService', + downgradeInjectable(ExplorationStatesService) + ); diff --git a/core/templates/pages/exploration-editor-page/services/exploration-tags.service.spec.ts b/core/templates/pages/exploration-editor-page/services/exploration-tags.service.spec.ts index febcef63396d..784fdf2a9ca1 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-tags.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-tags.service.spec.ts @@ -16,10 +16,10 @@ * @fileoverview Unit tests for the ExplorationTagsService. */ -import { TestBed } from '@angular/core/testing'; -import { ExplorationPropertyService } from './exploration-property.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ExplorationTagsService } from './exploration-tags.service'; +import {TestBed} from '@angular/core/testing'; +import {ExplorationPropertyService} from './exploration-property.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ExplorationTagsService} from './exploration-tags.service'; describe('Exploration Tags Service', () => { let explorationTagsService: ExplorationTagsService; @@ -27,9 +27,7 @@ describe('Exploration Tags Service', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [ - ExplorationPropertyService - ] + providers: [ExplorationPropertyService], }); explorationTagsService = TestBed.inject(ExplorationTagsService); @@ -42,11 +40,9 @@ describe('Exploration Tags Service', () => { let UpperCaseNotValid = ['Angularjs', 'google cloud storage', 'Python']; let NumberNotValid = ['angularjs', 'google cloud storage', 'Python123']; let SpecialNotValid = ['@ngularjs', 'google cloud storage', 'Python']; - expect( - explorationTagsService._normalize(NotNormalize)).toEqual(Normalize); + expect(explorationTagsService._normalize(NotNormalize)).toEqual(Normalize); expect(explorationTagsService._isValid(Normalize)).toBe(true); - expect( - explorationTagsService._isValid(UpperCaseNotValid)).toBe(false); + expect(explorationTagsService._isValid(UpperCaseNotValid)).toBe(false); expect(explorationTagsService._isValid(NumberNotValid)).toBe(false); expect(explorationTagsService._isValid(SpecialNotValid)).toBe(false); }); diff --git a/core/templates/pages/exploration-editor-page/services/exploration-tags.service.ts b/core/templates/pages/exploration-editor-page/services/exploration-tags.service.ts index a44182bd6eb9..4166004f25e5 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-tags.service.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-tags.service.ts @@ -16,16 +16,16 @@ * @fileoverview A data service that stores tags for the exploration. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { ExplorationPropertyService } from './exploration-property.service'; -import { AppConstants } from 'app.constants'; -import { AlertsService } from 'services/alerts.service'; -import { ChangeListService } from './change-list.service'; -import { LoggerService } from 'services/contextual/logger.service'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {ExplorationPropertyService} from './exploration-property.service'; +import {AppConstants} from 'app.constants'; +import {AlertsService} from 'services/alerts.service'; +import {ChangeListService} from './change-list.service'; +import {LoggerService} from 'services/contextual/logger.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ExplorationTagsService extends ExplorationPropertyService { propertyName: string = 'tags'; @@ -38,10 +38,10 @@ export class ExplorationTagsService extends ExplorationPropertyService { } /** - *@param {string[]} value - tag array to be normalized - *(white spaces removed and '+' replaced with ' ') - *@return {string} -normalized array - */ + *@param {string[]} value - tag array to be normalized + *(white spaces removed and '+' replaced with ' ') + *@return {string} -normalized array + */ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types _normalize(value: string[]) { for (let i = 0; i < value.length; i++) { @@ -52,9 +52,9 @@ export class ExplorationTagsService extends ExplorationPropertyService { } /** - *@param {string[]} value -tag array to be matched with TAG_REGEX - *@return {boolean} -whether or not all tags match TAG_REGEX - */ + *@param {string[]} value -tag array to be matched with TAG_REGEX + *@return {boolean} -whether or not all tags match TAG_REGEX + */ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types _isValid(value: string[]) { // Every tag should match the TAG_REGEX. @@ -69,6 +69,9 @@ export class ExplorationTagsService extends ExplorationPropertyService { } } -angular.module('oppia').factory( - 'ExplorationTagsService', downgradeInjectable( - ExplorationTagsService)); +angular + .module('oppia') + .factory( + 'ExplorationTagsService', + downgradeInjectable(ExplorationTagsService) + ); diff --git a/core/templates/pages/exploration-editor-page/services/exploration-title.service.spec.ts b/core/templates/pages/exploration-editor-page/services/exploration-title.service.spec.ts index c6ad1a7fe353..be429e701255 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-title.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-title.service.spec.ts @@ -16,15 +16,15 @@ * @fileoverview Unit tests for the ExplorationTitleService. */ -import { TestBed } from '@angular/core/testing'; -import { ExplorationTitleService } from './exploration-title.service'; -import { ExplorationPropertyService } from './exploration-property.service'; -import { ExplorationRightsService } from './exploration-rights.service'; -import { ValidatorsService } from 'services/validators.service'; -import { NormalizeWhitespacePipe } from 'filters/string-utility-filters/normalize-whitespace.pipe'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; +import {TestBed} from '@angular/core/testing'; +import {ExplorationTitleService} from './exploration-title.service'; +import {ExplorationPropertyService} from './exploration-property.service'; +import {ExplorationRightsService} from './exploration-rights.service'; +import {ValidatorsService} from 'services/validators.service'; +import {NormalizeWhitespacePipe} from 'filters/string-utility-filters/normalize-whitespace.pipe'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; -describe('Exploration Title Service', function() { +describe('Exploration Title Service', function () { let ets: ExplorationTitleService; beforeEach(() => { @@ -34,14 +34,14 @@ describe('Exploration Title Service', function() { ExplorationRightsService, ValidatorsService, NormalizeWhitespacePipe, - ExplorationPropertyService - ] + ExplorationPropertyService, + ], }); ets = TestBed.inject(ExplorationTitleService); }); - it('should test the child object properties', function() { + it('should test the child object properties', function () { expect(ets.propertyName).toBe('title'); let NotNormalize = ' Exploration Title Service '; let Normalize = 'Exploration Title Service'; diff --git a/core/templates/pages/exploration-editor-page/services/exploration-title.service.ts b/core/templates/pages/exploration-editor-page/services/exploration-title.service.ts index ede781f98270..55b7cbfc8b68 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-title.service.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-title.service.ts @@ -17,18 +17,18 @@ * that it can be displayed and edited in multiple places in the UI. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { ExplorationPropertyService } from './exploration-property.service'; -import { AlertsService } from 'services/alerts.service'; -import { ChangeListService } from './change-list.service'; -import { LoggerService } from 'services/contextual/logger.service'; -import { ExplorationRightsService } from './exploration-rights.service'; -import { ValidatorsService } from 'services/validators.service'; -import { NormalizeWhitespacePipe } from 'filters/string-utility-filters/normalize-whitespace.pipe'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {ExplorationPropertyService} from './exploration-property.service'; +import {AlertsService} from 'services/alerts.service'; +import {ChangeListService} from './change-list.service'; +import {LoggerService} from 'services/contextual/logger.service'; +import {ExplorationRightsService} from './exploration-rights.service'; +import {ValidatorsService} from 'services/validators.service'; +import {NormalizeWhitespacePipe} from 'filters/string-utility-filters/normalize-whitespace.pipe'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ExplorationTitleService extends ExplorationPropertyService { propertyName: string = 'title'; @@ -39,7 +39,7 @@ export class ExplorationTitleService extends ExplorationPropertyService { private whitespaceNormalizePipe: NormalizeWhitespacePipe, protected alertsService: AlertsService, protected changeListService: ChangeListService, - protected loggerService: LoggerService, + protected loggerService: LoggerService ) { super(alertsService, changeListService, loggerService); } @@ -50,9 +50,16 @@ export class ExplorationTitleService extends ExplorationPropertyService { _isValid(value: string): boolean { return this.validatorService.isValidEntityName( - value, true, this.explorationRightsService.isPrivate()); + value, + true, + this.explorationRightsService.isPrivate() + ); } } -angular.module('oppia').factory('ExplorationTitleService', - downgradeInjectable(ExplorationTitleService)); +angular + .module('oppia') + .factory( + 'ExplorationTitleService', + downgradeInjectable(ExplorationTitleService) + ); diff --git a/core/templates/pages/exploration-editor-page/services/exploration-warnings.service.spec.ts b/core/templates/pages/exploration-editor-page/services/exploration-warnings.service.spec.ts index 3968bd1e4ed1..70e54682cc72 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-warnings.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-warnings.service.spec.ts @@ -16,56 +16,59 @@ * @fileoverview Unit tests for explorationWarningsService. */ -import { fakeAsync, tick } from '@angular/core/testing'; -import { AnswerStats } from 'domain/exploration/answer-stats.model'; -import { StateTopAnswersStats } from 'domain/statistics/state-top-answers-stats-object.factory'; -import { TestBed } from '@angular/core/testing'; -import { ExplorationParamChangesService } from 'pages/exploration-editor-page/services/exploration-param-changes.service'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ExplorationInitStateNameService } from 'pages/exploration-editor-page/services/exploration-init-state-name.service'; -import { ExplorationWarningsService } from './exploration-warnings.service'; -import { StateTopAnswersStatsService } from 'services/state-top-answers-stats.service'; -import { StateTopAnswersStatsBackendApiService } from 'services/state-top-answers-stats-backend-api.service'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import {fakeAsync, tick} from '@angular/core/testing'; +import {AnswerStats} from 'domain/exploration/answer-stats.model'; +import {StateTopAnswersStats} from 'domain/statistics/state-top-answers-stats-object.factory'; +import {TestBed} from '@angular/core/testing'; +import {ExplorationParamChangesService} from 'pages/exploration-editor-page/services/exploration-param-changes.service'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ExplorationInitStateNameService} from 'pages/exploration-editor-page/services/exploration-init-state-name.service'; +import {ExplorationWarningsService} from './exploration-warnings.service'; +import {StateTopAnswersStatsService} from 'services/state-top-answers-stats.service'; +import {StateTopAnswersStatsBackendApiService} from 'services/state-top-answers-stats-backend-api.service'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; - class MockNgbModal { - open() { - return { - result: Promise.resolve() - }; - } - } - class MockExplorationParamChangesService { - savedMemento = [{ - customizationArgs: { - parse_with_jinja: false, - value: '5' - }, - generatorId: 'Copier', - name: 'ParamChange1' - }, { - customizationArgs: { - parse_with_jinja: true, - value: '{{ParamChange2}}' - }, - generatorId: 'Copier', - }, { - customizationArgs: { - parse_with_jinja: true, - value: '5' - }, - generatorId: 'RandomSelector', - name: 'ParamChange3' - }]; - } +class MockNgbModal { + open() { + return { + result: Promise.resolve(), + }; + } +} +class MockExplorationParamChangesService { + savedMemento = [ + { + customizationArgs: { + parse_with_jinja: false, + value: '5', + }, + generatorId: 'Copier', + name: 'ParamChange1', + }, + { + customizationArgs: { + parse_with_jinja: true, + value: '{{ParamChange2}}', + }, + generatorId: 'Copier', + }, + { + customizationArgs: { + parse_with_jinja: true, + value: '5', + }, + generatorId: 'RandomSelector', + name: 'ParamChange3', + }, + ]; +} describe('Exploration Warnings Service', () => { let explorationInitStateNameService: ExplorationInitStateNameService; let explorationWarningsService: ExplorationWarningsService; let explorationStatesService: ExplorationStatesService; let stateTopAnswersStatsService: StateTopAnswersStatsService; - let stateTopAnswersStatsBackendApiService: - StateTopAnswersStatsBackendApiService; + let stateTopAnswersStatsBackendApiService: StateTopAnswersStatsBackendApiService; beforeEach(() => { TestBed.configureTestingModule({ @@ -73,309 +76,343 @@ describe('Exploration Warnings Service', () => { providers: [ { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, { provide: ExplorationParamChangesService, - useClass: MockExplorationParamChangesService + useClass: MockExplorationParamChangesService, }, ExplorationInitStateNameService, ExplorationStatesService, StateTopAnswersStatsBackendApiService, StateTopAnswersStatsService, - ExplorationWarningsService - ] + ExplorationWarningsService, + ], }); explorationInitStateNameService = TestBed.inject( - ExplorationInitStateNameService); + ExplorationInitStateNameService + ); explorationStatesService = TestBed.inject(ExplorationStatesService); stateTopAnswersStatsBackendApiService = TestBed.inject( - StateTopAnswersStatsBackendApiService); - stateTopAnswersStatsService = TestBed.inject( - StateTopAnswersStatsService); + StateTopAnswersStatsBackendApiService + ); + stateTopAnswersStatsService = TestBed.inject(StateTopAnswersStatsService); explorationWarningsService = TestBed.inject(ExplorationWarningsService); explorationInitStateNameService.init('Hola'); }); it('should update warnings with TextInput as interaction id', () => { - explorationStatesService.init({ - Hola: { - classifier_model_id: null, - solicit_answer_details: false, - card_is_checkpoint: true, - linked_skill_id: null, - content: { - content_id: 'content', - html: '{{HtmlValue}}' - }, - recorded_voiceovers: { - voiceovers_mapping: {}, - }, - param_changes: [], - interaction: { - confirmed_unclassified_answers: [], - customization_args: { - rows: { - value: true - }, - placeholder: { - value: 1 - }, - catchMisspellings: { - value: false - } + explorationStatesService.init( + { + Hola: { + classifier_model_id: null, + solicit_answer_details: false, + card_is_checkpoint: true, + linked_skill_id: null, + content: { + content_id: 'content', + html: '{{HtmlValue}}', }, - solution: null, - id: 'TextInput', - answer_groups: [{ - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: true, + recorded_voiceovers: { + voiceovers_mapping: {}, + }, + param_changes: [], + interaction: { + confirmed_unclassified_answers: [], + customization_args: { + rows: { + value: true, + }, + placeholder: { + value: 1, + }, + catchMisspellings: { + value: false, + }, + }, + solution: null, + id: 'TextInput', + answer_groups: [ + { + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: '', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: '', + }, + }, + }, + ], + default_outcome: { + labelled_as_correct: false, param_changes: [], refresher_exploration_id: null, missing_prerequisite_skill_id: null, - dest: '', + dest: 'Hola', dest_if_really_stuck: null, feedback: { - content_id: 'feedback_1', - html: '' + content_id: 'feedback', + html: 'feedback', }, }, - }], - default_outcome: { - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: 'Hola', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback', - html: 'feedback', - }, + hints: [], }, - hints: [], }, - } - }, false); + }, + false + ); explorationWarningsService.updateWarnings(); - expect(explorationWarningsService.getWarnings()).toEqual([{ - type: 'critical', - message: 'Please ensure the value of parameter "ParamChange2" is' + - ' set before it is referred to in the initial list of parameter' + - ' changes.' - }, { - type: 'critical', - message: 'Please ensure the value of parameter "HtmlValue" is set' + - ' before using it in "Hola".' - }, { - type: 'error', - message: 'The following card has errors: Hola.' - }, { - type: 'error', - message: 'In \'Hola\', the following answer group has a classifier' + - ' with no training data: 0' - }]); + expect(explorationWarningsService.getWarnings()).toEqual([ + { + type: 'critical', + message: + 'Please ensure the value of parameter "ParamChange2" is' + + ' set before it is referred to in the initial list of parameter' + + ' changes.', + }, + { + type: 'critical', + message: + 'Please ensure the value of parameter "HtmlValue" is set' + + ' before using it in "Hola".', + }, + { + type: 'error', + message: 'The following card has errors: Hola.', + }, + { + type: 'error', + message: + "In 'Hola', the following answer group has a classifier" + + ' with no training data: 0', + }, + ]); expect(explorationWarningsService.hasCriticalWarnings()).toBe(true); expect(explorationWarningsService.countWarnings()).toBe(4); expect(explorationWarningsService.getAllStateRelatedWarnings()).toEqual({ Hola: [ 'Placeholder text must be a string.', 'Number of rows must be integral.', - 'There\'s no way to complete the exploration starting from this' + - ' card. To fix this, make sure that the last card in the chain' + - ' starting from this one has an \'End Exploration\' question type.' - ] + "There's no way to complete the exploration starting from this" + + ' card. To fix this, make sure that the last card in the chain' + + " starting from this one has an 'End Exploration' question type.", + ], }); }); it('should update warnings with Continue as interaction id', () => { - explorationStatesService.init({ - Hola: { - classifier_model_id: null, - solicit_answer_details: false, - card_is_checkpoint: true, - linked_skill_id: null, - content: { - content_id: 'content', - html: '{{HtmlValue}}' - }, - recorded_voiceovers: { - voiceovers_mapping: {}, - }, - param_changes: [], - interaction: { - id: 'Continue', - confirmed_unclassified_answers: [], - solution: null, - answer_groups: [ - { - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: '', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_1', - html: '' + explorationStatesService.init( + { + Hola: { + classifier_model_id: null, + solicit_answer_details: false, + card_is_checkpoint: true, + linked_skill_id: null, + content: { + content_id: 'content', + html: '{{HtmlValue}}', + }, + recorded_voiceovers: { + voiceovers_mapping: {}, + }, + param_changes: [], + interaction: { + id: 'Continue', + confirmed_unclassified_answers: [], + solution: null, + answer_groups: [ + { + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: '', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: '', + }, + }, + }, + { + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: '', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: '', + }, }, }, + ], + default_outcome: { + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: 'Hola', + dest_if_really_stuck: null, + feedback: { + content_id: '', + html: 'feedback', + }, }, - { - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: '', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_1', - html: '' + customization_args: { + rows: { + value: true, + }, + placeholder: { + value: 1, + }, + buttonText: { + value: { + unicode_str: '', + content_id: '', }, }, - }], - default_outcome: { - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: 'Hola', - dest_if_really_stuck: null, - feedback: { - content_id: '', - html: 'feedback', }, + hints: [], }, - customization_args: { - rows: { - value: true - }, - placeholder: { - value: 1 - }, - buttonText: { - value: { - unicode_str: '', - content_id: '' - } - } - }, - hints: [], }, - } - }, false); + }, + false + ); explorationWarningsService.updateWarnings(); - expect(explorationWarningsService.getWarnings()).toEqual([{ - type: 'critical', - message: 'Please ensure the value of parameter "ParamChange2" is set' + - ' before it is referred to in the initial list of parameter changes.' - }, { - type: 'critical', - message: 'Please ensure the value of parameter "HtmlValue" is set' + - ' before using it in "Hola".' - }, { - type: 'error', - message: 'The following card has errors: Hola.' - }, { - type: 'error', - message: 'In \'Hola\', the following answer groups have classifiers' + - ' with no training data: 0, 1' - }]); + expect(explorationWarningsService.getWarnings()).toEqual([ + { + type: 'critical', + message: + 'Please ensure the value of parameter "ParamChange2" is set' + + ' before it is referred to in the initial list of parameter changes.', + }, + { + type: 'critical', + message: + 'Please ensure the value of parameter "HtmlValue" is set' + + ' before using it in "Hola".', + }, + { + type: 'error', + message: 'The following card has errors: Hola.', + }, + { + type: 'error', + message: + "In 'Hola', the following answer groups have classifiers" + + ' with no training data: 0, 1', + }, + ]); expect(explorationWarningsService.countWarnings()).toBe(4); - expect(explorationWarningsService.hasCriticalWarnings()) - .toBe(true); + expect(explorationWarningsService.hasCriticalWarnings()).toBe(true); expect(explorationWarningsService.getAllStateRelatedWarnings()).toEqual({ Hola: [ 'The button text should not be empty.', 'Only the default outcome is necessary for a continue interaction.', - 'There\'s no way to complete the exploration starting from this' + - ' card. To fix this, make sure that the last card in the chain' + - ' starting from this one has an \'End Exploration\' question type.' - ] + "There's no way to complete the exploration starting from this" + + ' card. To fix this, make sure that the last card in the chain' + + " starting from this one has an 'End Exploration' question type.", + ], }); }); it('should update warnings when no interaction id is provided', () => { - explorationStatesService.init({ - Hola: { - classifier_model_id: null, - solicit_answer_details: false, - card_is_checkpoint: true, - linked_skill_id: null, - content: { - content_id: 'content', - html: '{{HtmlValue}}' - }, - recorded_voiceovers: { - voiceovers_mapping: {}, - }, - param_changes: [], - interaction: { - confirmed_unclassified_answers: [], - solution: null, - id: null, - answer_groups: [], - default_outcome: { - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: 'Hola', - dest_if_really_stuck: null, - feedback: { - content_id: '', - html: '', + explorationStatesService.init( + { + Hola: { + classifier_model_id: null, + solicit_answer_details: false, + card_is_checkpoint: true, + linked_skill_id: null, + content: { + content_id: 'content', + html: '{{HtmlValue}}', + }, + recorded_voiceovers: { + voiceovers_mapping: {}, + }, + param_changes: [], + interaction: { + confirmed_unclassified_answers: [], + solution: null, + id: null, + answer_groups: [], + default_outcome: { + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: 'Hola', + dest_if_really_stuck: null, + feedback: { + content_id: '', + html: '', + }, }, + customization_args: {}, + hints: [], }, - customization_args: {}, - hints: [], }, - } - }, false); + }, + false + ); explorationWarningsService.updateWarnings(); - expect(explorationWarningsService.getWarnings()).toEqual([{ - type: 'critical', - message: 'Please ensure the value of parameter "ParamChange2" is set' + - ' before it is referred to in the initial list of parameter changes.' - }, { - type: 'critical', - message: 'Please ensure the value of parameter "HtmlValue" is set' + - ' before using it in "Hola".' - }, { - type: 'error', - message: 'The following card has errors: Hola.' - }]); + expect(explorationWarningsService.getWarnings()).toEqual([ + { + type: 'critical', + message: + 'Please ensure the value of parameter "ParamChange2" is set' + + ' before it is referred to in the initial list of parameter changes.', + }, + { + type: 'critical', + message: + 'Please ensure the value of parameter "HtmlValue" is set' + + ' before using it in "Hola".', + }, + { + type: 'error', + message: 'The following card has errors: Hola.', + }, + ]); expect(explorationWarningsService.countWarnings()).toBe(3); expect(explorationWarningsService.hasCriticalWarnings()).toBe(true); expect(explorationWarningsService.getAllStateRelatedWarnings()).toEqual({ Hola: [ 'Please add an interaction to this card.', - 'There\'s no way to complete the exploration starting from this' + - ' card. To fix this, make sure that the last card in the chain' + - ' starting from this one has an \'End Exploration\' question type.' - ] + "There's no way to complete the exploration starting from this" + + ' card. To fix this, make sure that the last card in the chain' + + " starting from this one has an 'End Exploration' question type.", + ], }); }); - it('should update warnings when there is a solution in the interaction', - fakeAsync(() => { - explorationStatesService.init({ + it('should update warnings when there is a solution in the interaction', fakeAsync(() => { + explorationStatesService.init( + { Hola: { classifier_model_id: null, solicit_answer_details: false, @@ -383,7 +420,7 @@ describe('Exploration Warnings Service', () => { card_is_checkpoint: true, content: { content_id: 'content', - html: '{{HtmlValue}}' + html: '{{HtmlValue}}', }, recorded_voiceovers: { voiceovers_mapping: {}, @@ -397,26 +434,28 @@ describe('Exploration Warnings Service', () => { answer_is_exclusive: false, explanation: { content_id: 'content_id', - html: 'Solution explanation' - } + html: 'Solution explanation', + }, }, - answer_groups: [{ - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: '', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_1', - html: 'something' + answer_groups: [ + { + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: '', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: 'something', + }, }, + rule_specs: [], + training_data: [], }, - rule_specs: [], - training_data: [] - }], + ], default_outcome: { labelled_as_correct: false, param_changes: [], @@ -431,58 +470,67 @@ describe('Exploration Warnings Service', () => { }, customization_args: { rows: { - value: true + value: true, }, placeholder: { - value: 1 + value: 1, }, catchMisspellings: { - value: false - } + value: false, + }, }, hints: [], }, - } - }, false); - explorationWarningsService.updateWarnings(); - tick(); + }, + }, + false + ); + explorationWarningsService.updateWarnings(); + tick(); - expect(explorationWarningsService.getWarnings()).toEqual([{ + expect(explorationWarningsService.getWarnings()).toEqual([ + { type: 'critical', - message: 'Please ensure the value of parameter "ParamChange2"' + - ' is set before it is referred to in the initial list of' + - ' parameter changes.' - }, { + message: + 'Please ensure the value of parameter "ParamChange2"' + + ' is set before it is referred to in the initial list of' + + ' parameter changes.', + }, + { type: 'critical', - message: 'Please ensure the value of parameter "HtmlValue" is set' + - ' before using it in "Hola".' - }, { + message: + 'Please ensure the value of parameter "HtmlValue" is set' + + ' before using it in "Hola".', + }, + { type: 'error', - message: 'The following card has errors: Hola.' - }, { + message: 'The following card has errors: Hola.', + }, + { type: 'error', - message: 'In \'Hola\', the following answer group has a classifier' + - ' with no training data: 0' - }]); - expect(explorationWarningsService.countWarnings()).toBe(4); - expect(explorationWarningsService.hasCriticalWarnings()).toBe(true); - expect(explorationWarningsService.getAllStateRelatedWarnings()) - .toEqual({ - Hola: [ - 'Placeholder text must be a string.', - 'Number of rows must be integral.', - 'The current solution does not lead to another card.', - 'There\'s no way to complete the exploration starting from' + - ' this card. To fix this, make sure that the last card in' + - ' the chain starting from this one has an \'End Exploration\'' + - ' question type.' - ] - }); - })); + message: + "In 'Hola', the following answer group has a classifier" + + ' with no training data: 0', + }, + ]); + expect(explorationWarningsService.countWarnings()).toBe(4); + expect(explorationWarningsService.hasCriticalWarnings()).toBe(true); + expect(explorationWarningsService.getAllStateRelatedWarnings()).toEqual({ + Hola: [ + 'Placeholder text must be a string.', + 'Number of rows must be integral.', + 'The current solution does not lead to another card.', + "There's no way to complete the exploration starting from" + + ' this card. To fix this, make sure that the last card in' + + " the chain starting from this one has an 'End Exploration'" + + ' question type.', + ], + }); + })); - it('should update warnings when state top answers stats is initiated', - fakeAsync(async() => { - explorationStatesService.init({ + it('should update warnings when state top answers stats is initiated', fakeAsync(async () => { + explorationStatesService.init( + { Hola: { classifier_model_id: null, solicit_answer_details: false, @@ -490,7 +538,7 @@ describe('Exploration Warnings Service', () => { linked_skill_id: null, content: { content_id: 'content', - html: '{{HtmlValue}}' + html: '{{HtmlValue}}', }, recorded_voiceovers: { voiceovers_mapping: {}, @@ -501,40 +549,42 @@ describe('Exploration Warnings Service', () => { confirmed_unclassified_answers: [], customization_args: { rows: { - value: true + value: true, }, placeholder: { - value: 1 + value: 1, }, catchMisspellings: { - value: false - } + value: false, + }, }, solution: { correct_answer: 'This is the correct answer', answer_is_exclusive: false, explanation: { content_id: 'null', - html: 'Solution explanation' - } - }, - answer_groups: [{ - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: '', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_1', - html: '' + html: 'Solution explanation', + }, + }, + answer_groups: [ + { + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: '', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: '', + }, }, }, - }], + ], default_outcome: { labelled_as_correct: false, param_changes: [], @@ -549,398 +599,456 @@ describe('Exploration Warnings Service', () => { }, hints: [], }, - } - }, false); - spyOn(stateTopAnswersStatsBackendApiService, 'fetchStatsAsync') - .and.returnValue(Promise.resolve( - new StateTopAnswersStats( - { Hola: [new AnswerStats('hola', 'hola', 7, false)] }, - { Hola: 'TextInput' }))); - await stateTopAnswersStatsService.initAsync( - 'expId', explorationStatesService.getStates()); + }, + }, + false + ); + spyOn( + stateTopAnswersStatsBackendApiService, + 'fetchStatsAsync' + ).and.returnValue( + Promise.resolve( + new StateTopAnswersStats( + {Hola: [new AnswerStats('hola', 'hola', 7, false)]}, + {Hola: 'TextInput'} + ) + ) + ); + await stateTopAnswersStatsService.initAsync( + 'expId', + explorationStatesService.getStates() + ); - explorationWarningsService.updateWarnings(); + explorationWarningsService.updateWarnings(); - expect(explorationWarningsService.getWarnings()).toEqual([{ + expect(explorationWarningsService.getWarnings()).toEqual([ + { type: 'critical', - message: 'Please ensure the value of parameter "ParamChange2" is' + - ' set before it is referred to in the initial list of parameter' + - ' changes.' - }, { + message: + 'Please ensure the value of parameter "ParamChange2" is' + + ' set before it is referred to in the initial list of parameter' + + ' changes.', + }, + { type: 'critical', - message: 'Please ensure the value of parameter "HtmlValue" is set' + - ' before using it in "Hola".' - }, { + message: + 'Please ensure the value of parameter "HtmlValue" is set' + + ' before using it in "Hola".', + }, + { type: 'error', - message: 'The following card has errors: Hola.' - }, { + message: 'The following card has errors: Hola.', + }, + { type: 'error', - message: 'In \'Hola\', the following answer group has a classifier' + - ' with no training data: 0' - }]); - expect(explorationWarningsService.countWarnings()).toBe(4); - expect(explorationWarningsService.hasCriticalWarnings()).toBe(true); - expect(explorationWarningsService.getAllStateRelatedWarnings()) - .toEqual({ - Hola: [ - 'Placeholder text must be a string.', - 'Number of rows must be integral.', - 'There is an answer among the top 10 which has no explicit' + - ' feedback.', - 'The current solution does not lead to another card.', - 'There\'s no way to complete the exploration starting from' + - ' this card. To fix this, make sure that the last card in' + - ' the chain starting from this one has an \'End Exploration\'' + - ' question type.' - ] - }); - })); - - it('should update warnings when state name is not equal to the default' + - ' outcome destination', () => { - explorationStatesService.init({ - Hola: { - classifier_model_id: null, - solicit_answer_details: false, - card_is_checkpoint: true, - linked_skill_id: null, - content: { - content_id: 'content', - html: '{{HtmlValue}}' - }, - recorded_voiceovers: { - voiceovers_mapping: { - content: {}, - default_outcome: {}, - }, - }, - param_changes: [], - interaction: { - confirmed_unclassified_answers: [], - id: 'TextInput', - solution: { - correct_answer: 'This is the correct answer', - answer_is_exclusive: false, - explanation: { - content_id: 'null', - html: 'Solution explanation' - } - }, - answer_groups: [{ - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: '', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_1', - html: '' - }, - }, - rule_specs: [], - training_data: [] - }], - default_outcome: { - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: 'State', - dest_if_really_stuck: null, - feedback: { - content_id: '', - html: '', - }, - }, - customization_args: { - rows: { - value: true - }, - placeholder: { - value: 1 - }, - catchMisspellings: { - value: false - } - }, - hints: [], - }, - } - }, false); - explorationWarningsService.updateWarnings(); - - expect(explorationWarningsService.getWarnings()).toEqual([{ - type: 'critical', - message: 'Please ensure the value of parameter "ParamChange2" is set' + - ' before it is referred to in the initial list of parameter changes.' - }, { - type: 'critical', - message: 'Please ensure the value of parameter "HtmlValue" is set' + - ' before using it in "Hola".' - }, { - type: 'error', - message: 'The following card has errors: Hola.' - }, { - type: 'error', - message: 'In \'Hola\', the following answer group has a classifier' + - ' with no training data: 0' - }]); + message: + "In 'Hola', the following answer group has a classifier" + + ' with no training data: 0', + }, + ]); expect(explorationWarningsService.countWarnings()).toBe(4); expect(explorationWarningsService.hasCriticalWarnings()).toBe(true); expect(explorationWarningsService.getAllStateRelatedWarnings()).toEqual({ Hola: [ 'Placeholder text must be a string.', 'Number of rows must be integral.', - 'There\'s no way to complete the exploration starting from this' + - ' card. To fix this, make sure that the last card in the chain' + - ' starting from this one has an \'End Exploration\' question type.' - ] + 'There is an answer among the top 10 which has no explicit' + + ' feedback.', + 'The current solution does not lead to another card.', + "There's no way to complete the exploration starting from" + + ' this card. To fix this, make sure that the last card in' + + " the chain starting from this one has an 'End Exploration'" + + ' question type.', + ], }); - }); + })); - it('should update warnings when there are two states but only one saved' + - ' memento value', () => { - explorationStatesService.init({ - Hola: { - classifier_model_id: null, - solicit_answer_details: false, - card_is_checkpoint: true, - linked_skill_id: null, - content: { - content_id: 'content', - html: '{{HtmlValue}}' - }, - recorded_voiceovers: { - voiceovers_mapping: {}, - }, - param_changes: [], - interaction: { - confirmed_unclassified_answers: [], - customization_args: { - rows: { - value: true - }, - placeholder: { - value: 1 - }, - catchMisspellings: { - value: false - } - }, - solution: { - correct_answer: 'This is the correct answer', - answer_is_exclusive: false, - explanation: { - content_id: 'id', - html: 'Solution explanation' - } - }, - id: 'TextInput', - answer_groups: [{ - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: '', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_1', - html: '' + it( + 'should update warnings when state name is not equal to the default' + + ' outcome destination', + () => { + explorationStatesService.init( + { + Hola: { + classifier_model_id: null, + solicit_answer_details: false, + card_is_checkpoint: true, + linked_skill_id: null, + content: { + content_id: 'content', + html: '{{HtmlValue}}', + }, + recorded_voiceovers: { + voiceovers_mapping: { + content: {}, + default_outcome: {}, }, }, - }], - default_outcome: { - labelled_as_correct: false, param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: 'Hola', - dest_if_really_stuck: null, - feedback: { - content_id: '', - html: 'feedback', + interaction: { + confirmed_unclassified_answers: [], + id: 'TextInput', + solution: { + correct_answer: 'This is the correct answer', + answer_is_exclusive: false, + explanation: { + content_id: 'null', + html: 'Solution explanation', + }, + }, + answer_groups: [ + { + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: '', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: '', + }, + }, + rule_specs: [], + training_data: [], + }, + ], + default_outcome: { + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: 'State', + dest_if_really_stuck: null, + feedback: { + content_id: '', + html: '', + }, + }, + customization_args: { + rows: { + value: true, + }, + placeholder: { + value: 1, + }, + catchMisspellings: { + value: false, + }, + }, + hints: [], }, }, - hints: [], }, - }, - State: { - classifier_model_id: null, - solicit_answer_details: false, - card_is_checkpoint: false, - linked_skill_id: null, - content: { - content_id: 'content', - html: 'content' + false + ); + explorationWarningsService.updateWarnings(); + + expect(explorationWarningsService.getWarnings()).toEqual([ + { + type: 'critical', + message: + 'Please ensure the value of parameter "ParamChange2" is set' + + ' before it is referred to in the initial list of parameter changes.', }, - recorded_voiceovers: { - voiceovers_mapping: {}, + { + type: 'critical', + message: + 'Please ensure the value of parameter "HtmlValue" is set' + + ' before using it in "Hola".', }, - param_changes: [], - interaction: { - id: 'TextInput', - confirmed_unclassified_answers: [], - customization_args: { - rows: { - value: true - }, - placeholder: { - value: 1 - }, - catchMisspellings: { - value: false - } - }, - solution: { - correct_answer: 'This is the correct answer', - answer_is_exclusive: false, - explanation: { - content_id: 'id', - html: 'Solution explanation' - } - }, - answer_groups: [{ - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: '', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_1', - html: '' - }, - }, - }], - default_outcome: { - labelled_as_correct: false, + { + type: 'error', + message: 'The following card has errors: Hola.', + }, + { + type: 'error', + message: + "In 'Hola', the following answer group has a classifier" + + ' with no training data: 0', + }, + ]); + expect(explorationWarningsService.countWarnings()).toBe(4); + expect(explorationWarningsService.hasCriticalWarnings()).toBe(true); + expect(explorationWarningsService.getAllStateRelatedWarnings()).toEqual({ + Hola: [ + 'Placeholder text must be a string.', + 'Number of rows must be integral.', + "There's no way to complete the exploration starting from this" + + ' card. To fix this, make sure that the last card in the chain' + + " starting from this one has an 'End Exploration' question type.", + ], + }); + } + ); + + it( + 'should update warnings when there are two states but only one saved' + + ' memento value', + () => { + explorationStatesService.init( + { + Hola: { + classifier_model_id: null, + solicit_answer_details: false, + card_is_checkpoint: true, + linked_skill_id: null, + content: { + content_id: 'content', + html: '{{HtmlValue}}', + }, + recorded_voiceovers: { + voiceovers_mapping: {}, + }, param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: 'State', - dest_if_really_stuck: null, - feedback: { - content_id: 'default_outcome', - html: 'feedback' + interaction: { + confirmed_unclassified_answers: [], + customization_args: { + rows: { + value: true, + }, + placeholder: { + value: 1, + }, + catchMisspellings: { + value: false, + }, + }, + solution: { + correct_answer: 'This is the correct answer', + answer_is_exclusive: false, + explanation: { + content_id: 'id', + html: 'Solution explanation', + }, + }, + id: 'TextInput', + answer_groups: [ + { + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: '', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: '', + }, + }, + }, + ], + default_outcome: { + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: 'Hola', + dest_if_really_stuck: null, + feedback: { + content_id: '', + html: 'feedback', + }, + }, + hints: [], }, }, - hints: [] - } - } - }, false); - explorationWarningsService.updateWarnings(); - - expect(explorationWarningsService.getWarnings()).toEqual([{ - type: 'critical', - message: 'Please ensure the value of parameter "ParamChange2" is set' + - ' before it is referred to in the initial list of parameter changes.' - }, { - type: 'critical', - message: 'Please ensure the value of parameter "HtmlValue" is set' + - ' before using it in "Hola".' - }, { - type: 'error', - message: 'The following cards have errors: Hola, State.' - }, { - type: 'error', - message: 'In \'Hola\', the following answer group has a classifier' + - ' with no training data: 0' - }, { - type: 'error', - message: 'In \'State\', the following answer group has a classifier' + - ' with no training data: 0' - }]); - expect(explorationWarningsService.countWarnings()).toBe(5); - expect(explorationWarningsService.hasCriticalWarnings()).toBe(true); - expect(explorationWarningsService.getAllStateRelatedWarnings()).toEqual({ - Hola: [ - 'Placeholder text must be a string.', - 'Number of rows must be integral.', - 'The current solution does not lead to another card.', - ], - State: [ - 'Placeholder text must be a string.', - 'Number of rows must be integral.', - 'The current solution does not lead to another card.', - 'This card is unreachable.' - ] - }); - }); + State: { + classifier_model_id: null, + solicit_answer_details: false, + card_is_checkpoint: false, + linked_skill_id: null, + content: { + content_id: 'content', + html: 'content', + }, + recorded_voiceovers: { + voiceovers_mapping: {}, + }, + param_changes: [], + interaction: { + id: 'TextInput', + confirmed_unclassified_answers: [], + customization_args: { + rows: { + value: true, + }, + placeholder: { + value: 1, + }, + catchMisspellings: { + value: false, + }, + }, + solution: { + correct_answer: 'This is the correct answer', + answer_is_exclusive: false, + explanation: { + content_id: 'id', + html: 'Solution explanation', + }, + }, + answer_groups: [ + { + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: '', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: '', + }, + }, + }, + ], + default_outcome: { + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: 'State', + dest_if_really_stuck: null, + feedback: { + content_id: 'default_outcome', + html: 'feedback', + }, + }, + hints: [], + }, + }, + }, + false + ); + explorationWarningsService.updateWarnings(); - it('should update warning when first card is not a checkpoint', () => { - explorationStatesService.init({ - Hola: { - classifier_model_id: null, - solicit_answer_details: false, - card_is_checkpoint: false, - linked_skill_id: null, - content: { - content_id: 'content', - html: '{{HtmlValue}}' + expect(explorationWarningsService.getWarnings()).toEqual([ + { + type: 'critical', + message: + 'Please ensure the value of parameter "ParamChange2" is set' + + ' before it is referred to in the initial list of parameter changes.', + }, + { + type: 'critical', + message: + 'Please ensure the value of parameter "HtmlValue" is set' + + ' before using it in "Hola".', }, - recorded_voiceovers: { - voiceovers_mapping: {}, + { + type: 'error', + message: 'The following cards have errors: Hola, State.', + }, + { + type: 'error', + message: + "In 'Hola', the following answer group has a classifier" + + ' with no training data: 0', + }, + { + type: 'error', + message: + "In 'State', the following answer group has a classifier" + + ' with no training data: 0', }, - param_changes: [], - interaction: { - confirmed_unclassified_answers: [], - customization_args: { - rows: { - value: true - }, - placeholder: { - value: 1 - }, - catchMisspellings: { - value: false - } + ]); + expect(explorationWarningsService.countWarnings()).toBe(5); + expect(explorationWarningsService.hasCriticalWarnings()).toBe(true); + expect(explorationWarningsService.getAllStateRelatedWarnings()).toEqual({ + Hola: [ + 'Placeholder text must be a string.', + 'Number of rows must be integral.', + 'The current solution does not lead to another card.', + ], + State: [ + 'Placeholder text must be a string.', + 'Number of rows must be integral.', + 'The current solution does not lead to another card.', + 'This card is unreachable.', + ], + }); + } + ); + + it('should update warning when first card is not a checkpoint', () => { + explorationStatesService.init( + { + Hola: { + classifier_model_id: null, + solicit_answer_details: false, + card_is_checkpoint: false, + linked_skill_id: null, + content: { + content_id: 'content', + html: '{{HtmlValue}}', }, - solution: null, - id: 'TextInput', - answer_groups: [{ - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: true, + recorded_voiceovers: { + voiceovers_mapping: {}, + }, + param_changes: [], + interaction: { + confirmed_unclassified_answers: [], + customization_args: { + rows: { + value: true, + }, + placeholder: { + value: 1, + }, + catchMisspellings: { + value: false, + }, + }, + solution: null, + id: 'TextInput', + answer_groups: [ + { + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: '', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: '', + }, + }, + }, + ], + default_outcome: { + labelled_as_correct: false, param_changes: [], refresher_exploration_id: null, missing_prerequisite_skill_id: null, - dest: '', + dest: 'Hola', dest_if_really_stuck: null, feedback: { - content_id: 'feedback_1', - html: '' + content_id: 'feedback', + html: 'feedback', }, }, - }], - default_outcome: { - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: 'Hola', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback', - html: 'feedback', - }, + hints: [], }, - hints: [], }, - } - }, false); + }, + false + ); explorationWarningsService.updateWarnings(); expect(explorationWarningsService.countWarnings()).toBe(4); @@ -949,123 +1057,130 @@ describe('Exploration Warnings Service', () => { Hola: [ 'Placeholder text must be a string.', 'Number of rows must be integral.', - 'There\'s no way to complete the exploration starting from this' + - ' card. To fix this, make sure that the last card in the chain' + - ' starting from this one has an \'End Exploration\' question type.', - 'The first card of the lesson must be a checkpoint.' - ] + "There's no way to complete the exploration starting from this" + + ' card. To fix this, make sure that the last card in the chain' + + " starting from this one has an 'End Exploration' question type.", + 'The first card of the lesson must be a checkpoint.', + ], }); }); it('should show warning if terminal card is a checkpoint', () => { - explorationStatesService.init({ - Hola: { - classifier_model_id: null, - solicit_answer_details: false, - card_is_checkpoint: true, - linked_skill_id: null, - content: { - content_id: 'content', - html: '{{HtmlValue}}' - }, - recorded_voiceovers: { - voiceovers_mapping: { - content: {}, - default_outcome: {}, + explorationStatesService.init( + { + Hola: { + classifier_model_id: null, + solicit_answer_details: false, + card_is_checkpoint: true, + linked_skill_id: null, + content: { + content_id: 'content', + html: '{{HtmlValue}}', }, - }, - param_changes: [], - interaction: { - confirmed_unclassified_answers: [], - solution: null, - id: 'TextInput', - answer_groups: [{ - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: true, + recorded_voiceovers: { + voiceovers_mapping: { + content: {}, + default_outcome: {}, + }, + }, + param_changes: [], + interaction: { + confirmed_unclassified_answers: [], + solution: null, + id: 'TextInput', + answer_groups: [ + { + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: '', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: '', + }, + }, + }, + ], + default_outcome: { + labelled_as_correct: false, param_changes: [], refresher_exploration_id: null, missing_prerequisite_skill_id: null, - dest: '', + dest: 'End', dest_if_really_stuck: null, feedback: { - content_id: 'feedback_1', - html: '' + content_id: '', + html: 'feedback', }, }, - }], - default_outcome: { - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: 'End', - dest_if_really_stuck: null, - feedback: { - content_id: '', - html: 'feedback', - }, - }, - customization_args: { - rows: { - value: true - }, - placeholder: { - value: 1 + customization_args: { + rows: { + value: true, + }, + placeholder: { + value: 1, + }, + catchMisspellings: { + value: false, + }, }, - catchMisspellings: { - value: false - } + hints: [], }, - hints: [], - }, - }, - End: { - classifier_model_id: null, - solicit_answer_details: false, - card_is_checkpoint: true, - linked_skill_id: null, - content: { - content_id: 'content', - html: '{{HtmlValue}}' - }, - recorded_voiceovers: { - voiceovers_mapping: {}, }, - param_changes: [], - interaction: { - confirmed_unclassified_answers: [], - customization_args: { - recommendedExplorationIds: { - value: [] - } + End: { + classifier_model_id: null, + solicit_answer_details: false, + card_is_checkpoint: true, + linked_skill_id: null, + content: { + content_id: 'content', + html: '{{HtmlValue}}', }, - solution: null, - id: 'EndExploration', - answer_groups: [{ - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: '', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: '' + recorded_voiceovers: { + voiceovers_mapping: {}, + }, + param_changes: [], + interaction: { + confirmed_unclassified_answers: [], + customization_args: { + recommendedExplorationIds: { + value: [], }, }, - }], - default_outcome: null, - hints: [], + solution: null, + id: 'EndExploration', + answer_groups: [ + { + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: '', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: '', + }, + }, + }, + ], + default_outcome: null, + hints: [], + }, }, - } - }, false); + }, + false + ); explorationWarningsService.updateWarnings(); expect(explorationWarningsService.countWarnings()).toBe(5); @@ -1077,1075 +1192,1121 @@ describe('Exploration Warnings Service', () => { ], End: [ 'Please make sure end exploration interactions do not ' + - 'have any Oppia responses.', + 'have any Oppia responses.', 'Checkpoints are not allowed on the last card of the lesson.', - 'Checkpoints must not be assigned to cards that can be bypassed.' - ] + 'Checkpoints must not be assigned to cards that can be bypassed.', + ], }); }); - it('should show warnings if checkpoint count is more than 8 and' + - ' bypassable state is made a checkpoint', () => { - explorationStatesService.init({ - Hola: { - classifier_model_id: null, - solicit_answer_details: false, - linked_skill_id: null, - card_is_checkpoint: true, - content: { - content_id: 'content', - html: '{{HtmlValue}}' - }, - recorded_voiceovers: { - voiceovers_mapping: {}, - }, - param_changes: [], - interaction: { - confirmed_unclassified_answers: [], - solution: null, - default_outcome: null, - id: 'TextInput', - answer_groups: [ - { - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: 'State1', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_1', - html: '' + it( + 'should show warnings if checkpoint count is more than 8 and' + + ' bypassable state is made a checkpoint', + () => { + explorationStatesService.init( + { + Hola: { + classifier_model_id: null, + solicit_answer_details: false, + linked_skill_id: null, + card_is_checkpoint: true, + content: { + content_id: 'content', + html: '{{HtmlValue}}', + }, + recorded_voiceovers: { + voiceovers_mapping: {}, + }, + param_changes: [], + interaction: { + confirmed_unclassified_answers: [], + solution: null, + default_outcome: null, + id: 'TextInput', + answer_groups: [ + { + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: 'State1', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: '', + }, + }, + }, + { + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: 'State2', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: '', + }, + }, + }, + { + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: 'State3', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_3', + html: '', + }, + }, + }, + ], + customization_args: { + rows: { + value: true, + }, + placeholder: { + value: 1, + }, + catchMisspellings: { + value: false, }, }, + hints: [], }, - { - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: 'State2', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: '' + }, + State1: { + classifier_model_id: null, + solicit_answer_details: false, + linked_skill_id: null, + card_is_checkpoint: true, + content: { + content_id: 'content', + html: '{{HtmlValue}}', + }, + recorded_voiceovers: { + voiceovers_mapping: {}, + }, + param_changes: [], + interaction: { + confirmed_unclassified_answers: [], + solution: null, + id: 'TextInput', + answer_groups: [ + { + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: 'State4', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: '', + }, + }, + }, + ], + default_outcome: null, + customization_args: { + rows: { + value: true, + }, + placeholder: { + value: 1, + }, + catchMisspellings: { + value: false, }, }, + hints: [], }, - { - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: 'State3', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_3', - html: '' + }, + State2: { + classifier_model_id: null, + solicit_answer_details: false, + linked_skill_id: null, + card_is_checkpoint: true, + content: { + content_id: 'content', + html: '{{HtmlValue}}', + }, + recorded_voiceovers: { + voiceovers_mapping: {}, + }, + param_changes: [], + interaction: { + confirmed_unclassified_answers: [], + solution: null, + id: 'TextInput', + answer_groups: [ + { + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: 'State4', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: '', + }, + }, + }, + ], + default_outcome: null, + customization_args: { + rows: { + value: true, + }, + placeholder: { + value: 1, + }, + catchMisspellings: { + value: false, }, }, - } - ], - customization_args: { - rows: { - value: true + hints: [], + }, + }, + State3: { + classifier_model_id: null, + solicit_answer_details: false, + linked_skill_id: null, + card_is_checkpoint: true, + content: { + content_id: 'content', + html: '{{HtmlValue}}', + }, + recorded_voiceovers: { + voiceovers_mapping: {}, }, - placeholder: { - value: 1 + param_changes: [], + interaction: { + confirmed_unclassified_answers: [], + solution: null, + id: 'TextInput', + answer_groups: [ + { + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: 'State4', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: '', + }, + }, + }, + ], + default_outcome: null, + customization_args: { + rows: { + value: true, + }, + placeholder: { + value: 1, + }, + catchMisspellings: { + value: false, + }, + }, + hints: [], }, - catchMisspellings: { - value: false - } }, - hints: [], - }, - }, - State1: { - classifier_model_id: null, - solicit_answer_details: false, - linked_skill_id: null, - card_is_checkpoint: true, - content: { - content_id: 'content', - html: '{{HtmlValue}}' - }, - recorded_voiceovers: { - voiceovers_mapping: {}, - }, - param_changes: [], - interaction: { - confirmed_unclassified_answers: [], - solution: null, - id: 'TextInput', - answer_groups: [{ - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: 'State4', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: '' - }, - } - }], - default_outcome: null, - customization_args: { - rows: { - value: true - }, - placeholder: { - value: 1 - }, - catchMisspellings: { - value: false - } + State4: { + classifier_model_id: null, + solicit_answer_details: false, + linked_skill_id: null, + card_is_checkpoint: true, + content: { + content_id: 'content', + html: '{{HtmlValue}}', + }, + recorded_voiceovers: { + voiceovers_mapping: {}, + }, + param_changes: [], + interaction: { + confirmed_unclassified_answers: [], + solution: null, + id: 'TextInput', + answer_groups: [ + { + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: 'State5', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: '', + }, + }, + }, + ], + default_outcome: null, + customization_args: { + rows: { + value: true, + }, + placeholder: { + value: 1, + }, + catchMisspellings: { + value: false, + }, + }, + hints: [], + }, }, - hints: [], - }, - }, - State2: { - classifier_model_id: null, - solicit_answer_details: false, - linked_skill_id: null, - card_is_checkpoint: true, - content: { - content_id: 'content', - html: '{{HtmlValue}}' - }, - recorded_voiceovers: { - voiceovers_mapping: {}, - }, - param_changes: [], - interaction: { - confirmed_unclassified_answers: [], - solution: null, - id: 'TextInput', - answer_groups: [{ - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: 'State4', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: '' - }, - } - }], - default_outcome: null, - customization_args: { - rows: { - value: true - }, - placeholder: { - value: 1 - }, - catchMisspellings: { - value: false - } + State5: { + classifier_model_id: null, + solicit_answer_details: false, + linked_skill_id: null, + card_is_checkpoint: true, + content: { + content_id: 'content', + html: '{{HtmlValue}}', + }, + recorded_voiceovers: { + voiceovers_mapping: {}, + }, + param_changes: [], + interaction: { + confirmed_unclassified_answers: [], + solution: null, + id: 'TextInput', + answer_groups: [ + { + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: 'State6', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: '', + }, + }, + }, + ], + default_outcome: null, + customization_args: { + rows: { + value: true, + }, + placeholder: { + value: 1, + }, + catchMisspellings: { + value: false, + }, + }, + hints: [], + }, }, - hints: [], - }, - }, - State3: { - classifier_model_id: null, - solicit_answer_details: false, - linked_skill_id: null, - card_is_checkpoint: true, - content: { - content_id: 'content', - html: '{{HtmlValue}}' - }, - recorded_voiceovers: { - voiceovers_mapping: {}, - }, - param_changes: [], - interaction: { - confirmed_unclassified_answers: [], - solution: null, - id: 'TextInput', - answer_groups: [{ - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: 'State4', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: '' - }, - } - }], - default_outcome: null, - customization_args: { - rows: { - value: true - }, - placeholder: { - value: 1 - }, - catchMisspellings: { - value: false - } + State6: { + classifier_model_id: null, + solicit_answer_details: false, + linked_skill_id: null, + card_is_checkpoint: true, + content: { + content_id: 'content', + html: '{{HtmlValue}}', + }, + recorded_voiceovers: { + voiceovers_mapping: {}, + }, + param_changes: [], + interaction: { + confirmed_unclassified_answers: [], + solution: null, + id: 'TextInput', + answer_groups: [ + { + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: 'State7', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: '', + }, + }, + }, + ], + default_outcome: null, + customization_args: { + rows: { + value: true, + }, + placeholder: { + value: 1, + }, + catchMisspellings: { + value: false, + }, + }, + hints: [], + }, }, - hints: [], - }, - }, - State4: { - classifier_model_id: null, - solicit_answer_details: false, - linked_skill_id: null, - card_is_checkpoint: true, - content: { - content_id: 'content', - html: '{{HtmlValue}}' - }, - recorded_voiceovers: { - voiceovers_mapping: {}, - }, - param_changes: [], - interaction: { - confirmed_unclassified_answers: [], - solution: null, - id: 'TextInput', - answer_groups: [{ - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: 'State5', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: '' - }, - } - }], - default_outcome: null, - customization_args: { - rows: { - value: true - }, - placeholder: { - value: 1 - }, - catchMisspellings: { - value: false - } + State7: { + classifier_model_id: null, + solicit_answer_details: false, + linked_skill_id: null, + card_is_checkpoint: true, + content: { + content_id: 'content', + html: '{{HtmlValue}}', + }, + recorded_voiceovers: { + voiceovers_mapping: {}, + }, + param_changes: [], + interaction: { + confirmed_unclassified_answers: [], + solution: null, + id: 'TextInput', + answer_groups: [ + { + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: 'End', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: '', + }, + }, + }, + ], + default_outcome: null, + customization_args: { + rows: { + value: true, + }, + placeholder: { + value: 1, + }, + catchMisspellings: { + value: false, + }, + }, + hints: [], + }, }, - hints: [], - }, - }, - State5: { - classifier_model_id: null, - solicit_answer_details: false, - linked_skill_id: null, - card_is_checkpoint: true, - content: { - content_id: 'content', - html: '{{HtmlValue}}' - }, - recorded_voiceovers: { - voiceovers_mapping: {}, - }, - param_changes: [], - interaction: { - confirmed_unclassified_answers: [], - solution: null, - id: 'TextInput', - answer_groups: [{ - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: 'State6', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: '' - }, - } - }], - default_outcome: null, - customization_args: { - rows: { - value: true - }, - placeholder: { - value: 1 - }, - catchMisspellings: { - value: false - } + End: { + classifier_model_id: null, + solicit_answer_details: false, + linked_skill_id: null, + card_is_checkpoint: true, + content: { + content_id: 'content', + html: '{{HtmlValue}}', + }, + recorded_voiceovers: { + voiceovers_mapping: {}, + }, + param_changes: [], + interaction: { + id: 'EndExploration', + confirmed_unclassified_answers: [], + solution: null, + answer_groups: [ + { + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: '', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: '', + }, + }, + rule_specs: [], + training_data: [], + }, + ], + default_outcome: null, + customization_args: { + recommendedExplorationIds: { + value: [], + }, + }, + hints: [], + }, }, - hints: [], - }, - }, - State6: { - classifier_model_id: null, - solicit_answer_details: false, - linked_skill_id: null, - card_is_checkpoint: true, - content: { - content_id: 'content', - html: '{{HtmlValue}}' }, - recorded_voiceovers: { - voiceovers_mapping: {}, - }, - param_changes: [], - interaction: { - confirmed_unclassified_answers: [], - solution: null, - id: 'TextInput', - answer_groups: [{ - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: 'State7', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: '' - }, - } - }], - default_outcome: null, - customization_args: { - rows: { - value: true - }, - placeholder: { - value: 1 - }, - catchMisspellings: { - value: false - } + false + ); + + explorationWarningsService.updateWarnings(); + expect(explorationWarningsService.countWarnings()).toBe(13); + expect(explorationWarningsService.hasCriticalWarnings()).toBe(true); + expect(explorationWarningsService.getCheckpointCountWarning()).toEqual( + 'Only a maximum of 8 checkpoints are allowed per lesson.' + ); + expect(explorationWarningsService.getAllStateRelatedWarnings()).toEqual({ + Hola: [ + 'Placeholder text must be a string.', + 'Number of rows must be integral.', + ], + State1: [ + 'Placeholder text must be a string.', + 'Number of rows must be integral.', + 'Checkpoints must not be assigned to cards that can be bypassed.', + ], + State2: [ + 'Placeholder text must be a string.', + 'Number of rows must be integral.', + 'Checkpoints must not be assigned to cards that can be bypassed.', + ], + State3: [ + 'Placeholder text must be a string.', + 'Number of rows must be integral.', + 'Checkpoints must not be assigned to cards that can be bypassed.', + ], + State4: [ + 'Placeholder text must be a string.', + 'Number of rows must be integral.', + ], + State5: [ + 'Placeholder text must be a string.', + 'Number of rows must be integral.', + ], + State6: [ + 'Placeholder text must be a string.', + 'Number of rows must be integral.', + ], + State7: [ + 'Placeholder text must be a string.', + 'Number of rows must be integral.', + ], + End: [ + 'Please make sure end exploration interactions do not ' + + 'have any Oppia responses.', + 'Checkpoints are not allowed on the last card of the lesson.', + 'Checkpoints must not be assigned to cards that can be bypassed.', + ], + }); + } + ); + + it( + 'should show warnings if learner is directed more than 3 cards' + + ' back in the exploration', + () => { + explorationStatesService.init( + { + Hola: { + classifier_model_id: null, + solicit_answer_details: false, + linked_skill_id: null, + card_is_checkpoint: true, + content: { + content_id: 'content', + html: '{{HtmlValue}}', + }, + recorded_voiceovers: { + voiceovers_mapping: {}, + }, + param_changes: [], + interaction: { + confirmed_unclassified_answers: [], + solution: null, + default_outcome: null, + id: 'TextInput', + answer_groups: [ + { + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: 'State1', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: '', + }, + }, + }, + { + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: 'State2', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: '', + }, + }, + }, + ], + customization_args: { + rows: { + value: true, + }, + placeholder: { + value: 1, + }, + catchMisspellings: { + value: false, + }, + }, + hints: [], + }, }, - hints: [], - }, - }, - State7: { - classifier_model_id: null, - solicit_answer_details: false, - linked_skill_id: null, - card_is_checkpoint: true, - content: { - content_id: 'content', - html: '{{HtmlValue}}' - }, - recorded_voiceovers: { - voiceovers_mapping: {}, - }, - param_changes: [], - interaction: { - confirmed_unclassified_answers: [], - solution: null, - id: 'TextInput', - answer_groups: [{ - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: 'End', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: '' - }, - } - }], - default_outcome: null, - customization_args: { - rows: { - value: true - }, - placeholder: { - value: 1 - }, - catchMisspellings: { - value: false - } + State1: { + classifier_model_id: null, + solicit_answer_details: false, + linked_skill_id: null, + card_is_checkpoint: true, + content: { + content_id: 'content', + html: '{{HtmlValue}}', + }, + recorded_voiceovers: { + voiceovers_mapping: {}, + }, + param_changes: [], + interaction: { + confirmed_unclassified_answers: [], + solution: null, + id: 'TextInput', + answer_groups: [ + { + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: 'State3', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: '', + }, + }, + }, + ], + default_outcome: null, + customization_args: { + rows: { + value: true, + }, + placeholder: { + value: 1, + }, + catchMisspellings: { + value: false, + }, + }, + hints: [], + }, }, - hints: [], - }, - }, - End: { - classifier_model_id: null, - solicit_answer_details: false, - linked_skill_id: null, - card_is_checkpoint: true, - content: { - content_id: 'content', - html: '{{HtmlValue}}' - }, - recorded_voiceovers: { - voiceovers_mapping: {}, - }, - param_changes: [], - interaction: { - id: 'EndExploration', - confirmed_unclassified_answers: [], - solution: null, - answer_groups: [{ - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: '', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: '' - }, - }, - rule_specs: [], - training_data: [] - }], - default_outcome: null, - customization_args: { - recommendedExplorationIds: { - value: [] - } + State2: { + classifier_model_id: null, + solicit_answer_details: false, + linked_skill_id: null, + card_is_checkpoint: true, + content: { + content_id: 'content', + html: '{{HtmlValue}}', + }, + recorded_voiceovers: { + voiceovers_mapping: {}, + }, + param_changes: [], + interaction: { + confirmed_unclassified_answers: [], + solution: null, + id: 'TextInput', + answer_groups: [ + { + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: 'State5', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: '', + }, + }, + }, + ], + default_outcome: null, + customization_args: { + rows: { + value: true, + }, + placeholder: { + value: 1, + }, + catchMisspellings: { + value: false, + }, + }, + hints: [], + }, }, - hints: [], - }, - } - }, false); - - explorationWarningsService.updateWarnings(); - expect(explorationWarningsService.countWarnings()).toBe(13); - expect(explorationWarningsService.hasCriticalWarnings()).toBe(true); - expect(explorationWarningsService.getCheckpointCountWarning()).toEqual( - 'Only a maximum of 8 checkpoints are allowed per lesson.'); - expect(explorationWarningsService.getAllStateRelatedWarnings()).toEqual({ - Hola: [ - 'Placeholder text must be a string.', - 'Number of rows must be integral.', - ], - State1: [ - 'Placeholder text must be a string.', - 'Number of rows must be integral.', - 'Checkpoints must not be assigned to cards that can be bypassed.' - ], - State2: [ - 'Placeholder text must be a string.', - 'Number of rows must be integral.', - 'Checkpoints must not be assigned to cards that can be bypassed.' - ], - State3: [ - 'Placeholder text must be a string.', - 'Number of rows must be integral.', - 'Checkpoints must not be assigned to cards that can be bypassed.' - ], - State4: [ - 'Placeholder text must be a string.', - 'Number of rows must be integral.', - ], - State5: [ - 'Placeholder text must be a string.', - 'Number of rows must be integral.', - ], - State6: [ - 'Placeholder text must be a string.', - 'Number of rows must be integral.', - ], - State7: [ - 'Placeholder text must be a string.', - 'Number of rows must be integral.', - ], - End: [ - 'Please make sure end exploration interactions do not ' + - 'have any Oppia responses.', - 'Checkpoints are not allowed on the last card of the lesson.', - 'Checkpoints must not be assigned to cards that can be bypassed.' - ] - }); - }); - - it('should show warnings if learner is directed more than 3 cards' + - ' back in the exploration', () => { - explorationStatesService.init({ - Hola: { - classifier_model_id: null, - solicit_answer_details: false, - linked_skill_id: null, - card_is_checkpoint: true, - content: { - content_id: 'content', - html: '{{HtmlValue}}' - }, - recorded_voiceovers: { - voiceovers_mapping: {}, - }, - param_changes: [], - interaction: { - confirmed_unclassified_answers: [], - solution: null, - default_outcome: null, - id: 'TextInput', - answer_groups: [ - { - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: 'State1', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_1', - html: '' + State3: { + classifier_model_id: null, + solicit_answer_details: false, + linked_skill_id: null, + card_is_checkpoint: true, + content: { + content_id: 'content', + html: '{{HtmlValue}}', + }, + recorded_voiceovers: { + voiceovers_mapping: {}, + }, + param_changes: [], + interaction: { + confirmed_unclassified_answers: [], + solution: null, + id: 'TextInput', + answer_groups: [ + { + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: 'State4', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: '', + }, + }, + }, + ], + default_outcome: null, + customization_args: { + rows: { + value: true, + }, + placeholder: { + value: 1, + }, + catchMisspellings: { + value: false, }, }, + hints: [], }, - { - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: true, + }, + State4: { + classifier_model_id: null, + solicit_answer_details: false, + linked_skill_id: null, + card_is_checkpoint: true, + content: { + content_id: 'content', + html: '{{HtmlValue}}', + }, + recorded_voiceovers: { + voiceovers_mapping: {}, + }, + param_changes: [], + interaction: { + confirmed_unclassified_answers: [], + solution: null, + id: 'TextInput', + answer_groups: [ + { + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: 'State6', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: '', + }, + }, + }, + ], + default_outcome: null, + customization_args: { + rows: { + value: true, + }, + placeholder: { + value: 1, + }, + catchMisspellings: { + value: false, + }, + }, + hints: [], + }, + }, + State5: { + classifier_model_id: null, + solicit_answer_details: false, + linked_skill_id: null, + card_is_checkpoint: true, + content: { + content_id: 'content', + html: '{{HtmlValue}}', + }, + recorded_voiceovers: { + voiceovers_mapping: {}, + }, + param_changes: [], + interaction: { + confirmed_unclassified_answers: [], + solution: null, + id: 'TextInput', + answer_groups: [ + { + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: 'End', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: '', + }, + }, + }, + ], + default_outcome: null, + customization_args: { + rows: { + value: true, + }, + placeholder: { + value: 1, + }, + catchMisspellings: { + value: false, + }, + }, + hints: [], + }, + }, + State6: { + classifier_model_id: null, + solicit_answer_details: false, + linked_skill_id: null, + card_is_checkpoint: true, + content: { + content_id: 'content', + html: '{{HtmlValue}}', + }, + recorded_voiceovers: { + voiceovers_mapping: {}, + }, + param_changes: [], + interaction: { + confirmed_unclassified_answers: [], + solution: null, + id: 'TextInput', + answer_groups: [ + { + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: 'State7', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: '', + }, + }, + }, + ], + default_outcome: { + labelled_as_correct: false, param_changes: [], refresher_exploration_id: null, missing_prerequisite_skill_id: null, - dest: 'State2', + dest: 'Hola', dest_if_really_stuck: null, feedback: { - content_id: 'feedback_2', - html: '' + content_id: 'feedback', + html: 'feedback', }, }, - } - ], - customization_args: { - rows: { - value: true - }, - placeholder: { - value: 1 + customization_args: { + rows: { + value: true, + }, + placeholder: { + value: 1, + }, + catchMisspellings: { + value: false, + }, + }, + hints: [], }, - catchMisspellings: { - value: false - } - }, - hints: [], - }, - }, - State1: { - classifier_model_id: null, - solicit_answer_details: false, - linked_skill_id: null, - card_is_checkpoint: true, - content: { - content_id: 'content', - html: '{{HtmlValue}}' - }, - recorded_voiceovers: { - voiceovers_mapping: {}, - }, - param_changes: [], - interaction: { - confirmed_unclassified_answers: [], - solution: null, - id: 'TextInput', - answer_groups: [{ - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: 'State3', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: '' - }, - } - }], - default_outcome: null, - customization_args: { - rows: { - value: true - }, - placeholder: { - value: 1 - }, - catchMisspellings: { - value: false - } - }, - hints: [], - }, - }, - State2: { - classifier_model_id: null, - solicit_answer_details: false, - linked_skill_id: null, - card_is_checkpoint: true, - content: { - content_id: 'content', - html: '{{HtmlValue}}' - }, - recorded_voiceovers: { - voiceovers_mapping: {}, - }, - param_changes: [], - interaction: { - confirmed_unclassified_answers: [], - solution: null, - id: 'TextInput', - answer_groups: [{ - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: 'State5', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: '' - }, - } - }], - default_outcome: null, - customization_args: { - rows: { - value: true - }, - placeholder: { - value: 1 - }, - catchMisspellings: { - value: false - } }, - hints: [], - }, - }, - State3: { - classifier_model_id: null, - solicit_answer_details: false, - linked_skill_id: null, - card_is_checkpoint: true, - content: { - content_id: 'content', - html: '{{HtmlValue}}' - }, - recorded_voiceovers: { - voiceovers_mapping: {}, - }, - param_changes: [], - interaction: { - confirmed_unclassified_answers: [], - solution: null, - id: 'TextInput', - answer_groups: [{ - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: 'State4', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: '' - }, - } - }], - default_outcome: null, - customization_args: { - rows: { - value: true - }, - placeholder: { - value: 1 - }, - catchMisspellings: { - value: false - } - }, - hints: [], - }, - }, - State4: { - classifier_model_id: null, - solicit_answer_details: false, - linked_skill_id: null, - card_is_checkpoint: true, - content: { - content_id: 'content', - html: '{{HtmlValue}}' - }, - recorded_voiceovers: { - voiceovers_mapping: {}, - }, - param_changes: [], - interaction: { - confirmed_unclassified_answers: [], - solution: null, - id: 'TextInput', - answer_groups: [{ - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: 'State6', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: '' - }, - } - }], - default_outcome: null, - customization_args: { - rows: { - value: true - }, - placeholder: { - value: 1 - }, - catchMisspellings: { - value: false - } - }, - hints: [], - }, - }, - State5: { - classifier_model_id: null, - solicit_answer_details: false, - linked_skill_id: null, - card_is_checkpoint: true, - content: { - content_id: 'content', - html: '{{HtmlValue}}' - }, - recorded_voiceovers: { - voiceovers_mapping: {}, - }, - param_changes: [], - interaction: { - confirmed_unclassified_answers: [], - solution: null, - id: 'TextInput', - answer_groups: [{ - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: 'End', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: '' - }, - } - }], - default_outcome: null, - customization_args: { - rows: { - value: true - }, - placeholder: { - value: 1 - }, - catchMisspellings: { - value: false - } - }, - hints: [], - }, - }, - State6: { - classifier_model_id: null, - solicit_answer_details: false, - linked_skill_id: null, - card_is_checkpoint: true, - content: { - content_id: 'content', - html: '{{HtmlValue}}' - }, - recorded_voiceovers: { - voiceovers_mapping: {}, - }, - param_changes: [], - interaction: { - confirmed_unclassified_answers: [], - solution: null, - id: 'TextInput', - answer_groups: [{ - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: 'State7', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: '' - }, - } - }], - default_outcome: { - labelled_as_correct: false, + State7: { + classifier_model_id: null, + solicit_answer_details: false, + linked_skill_id: null, + card_is_checkpoint: false, + content: { + content_id: 'content', + html: '{{HtmlValue}}', + }, + recorded_voiceovers: { + voiceovers_mapping: {}, + }, param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: 'Hola', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback', - html: 'feedback', + interaction: { + confirmed_unclassified_answers: [], + solution: null, + id: 'TextInput', + answer_groups: [ + { + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: 'End', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: '', + }, + }, + }, + { + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: 'Hola', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: '', + }, + }, + }, + ], + default_outcome: null, + customization_args: { + rows: { + value: true, + }, + placeholder: { + value: 1, + }, + catchMisspellings: { + value: false, + }, + }, + hints: [], }, }, - customization_args: { - rows: { - value: true + End: { + classifier_model_id: null, + solicit_answer_details: false, + linked_skill_id: null, + card_is_checkpoint: false, + content: { + content_id: 'content', + html: '{{HtmlValue}}', + }, + recorded_voiceovers: { + voiceovers_mapping: {}, }, - placeholder: { - value: 1 + param_changes: [], + interaction: { + id: 'EndExploration', + confirmed_unclassified_answers: [], + solution: null, + answer_groups: [ + { + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: '', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: '', + }, + }, + rule_specs: [], + training_data: [], + }, + ], + default_outcome: null, + customization_args: { + recommendedExplorationIds: { + value: [], + }, + }, + hints: [], }, - catchMisspellings: { - value: false - } - }, - hints: [], - }, - }, - State7: { - classifier_model_id: null, - solicit_answer_details: false, - linked_skill_id: null, - card_is_checkpoint: false, - content: { - content_id: 'content', - html: '{{HtmlValue}}' - }, - recorded_voiceovers: { - voiceovers_mapping: {}, - }, - param_changes: [], - interaction: { - confirmed_unclassified_answers: [], - solution: null, - id: 'TextInput', - answer_groups: [{ - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: 'End', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: '' - }, - } - }, { - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: 'Hola', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: '' - }, - } - }], - default_outcome: null, - customization_args: { - rows: { - value: true - }, - placeholder: { - value: 1 - }, - catchMisspellings: { - value: false - } - }, - hints: [], - }, - }, - End: { - classifier_model_id: null, - solicit_answer_details: false, - linked_skill_id: null, - card_is_checkpoint: false, - content: { - content_id: 'content', - html: '{{HtmlValue}}' - }, - recorded_voiceovers: { - voiceovers_mapping: {}, - }, - param_changes: [], - interaction: { - id: 'EndExploration', - confirmed_unclassified_answers: [], - solution: null, - answer_groups: [{ - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: '', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: '' - }, - }, - rule_specs: [], - training_data: [] - }], - default_outcome: null, - customization_args: { - recommendedExplorationIds: { - value: [] - } }, - hints: [], }, - } - }, false); + false + ); - explorationWarningsService.updateWarnings(); - expect(explorationWarningsService.countWarnings()).toBe(12); - expect(explorationWarningsService.hasCriticalWarnings()).toBe(true); - expect(explorationWarningsService.getAllStateRelatedWarnings()).toEqual({ - Hola: [ - 'Placeholder text must be a string.', - 'Number of rows must be integral.', - ], - State1: [ - 'Placeholder text must be a string.', - 'Number of rows must be integral.', - 'Checkpoints must not be assigned to cards that can be bypassed.' - ], - State2: [ - 'Placeholder text must be a string.', - 'Number of rows must be integral.', - 'Checkpoints must not be assigned to cards that can be bypassed.' - ], - State3: [ - 'Placeholder text must be a string.', - 'Number of rows must be integral.', - 'Checkpoints must not be assigned to cards that can be bypassed.' - ], - State4: [ - 'Placeholder text must be a string.', - 'Number of rows must be integral.', - 'Checkpoints must not be assigned to cards that can be bypassed.' - ], - State5: [ - 'Placeholder text must be a string.', - 'Number of rows must be integral.', - 'Checkpoints must not be assigned to cards that can be bypassed.' - ], - State6: [ - 'Placeholder text must be a string.', - 'Number of rows must be integral.', - 'Learner should not be directed back by more than' + - ' 3 cards in the lesson.', - 'Checkpoints must not be assigned to cards that can be bypassed.' - ], - State7: [ - 'Placeholder text must be a string.', - 'Number of rows must be integral.', - 'Learner should not be directed back by more than' + - ' 3 cards in the lesson.' - ], - End: [ - 'Please make sure end exploration interactions do not ' + - 'have any Oppia responses.', - ] - }); - }); + explorationWarningsService.updateWarnings(); + expect(explorationWarningsService.countWarnings()).toBe(12); + expect(explorationWarningsService.hasCriticalWarnings()).toBe(true); + expect(explorationWarningsService.getAllStateRelatedWarnings()).toEqual({ + Hola: [ + 'Placeholder text must be a string.', + 'Number of rows must be integral.', + ], + State1: [ + 'Placeholder text must be a string.', + 'Number of rows must be integral.', + 'Checkpoints must not be assigned to cards that can be bypassed.', + ], + State2: [ + 'Placeholder text must be a string.', + 'Number of rows must be integral.', + 'Checkpoints must not be assigned to cards that can be bypassed.', + ], + State3: [ + 'Placeholder text must be a string.', + 'Number of rows must be integral.', + 'Checkpoints must not be assigned to cards that can be bypassed.', + ], + State4: [ + 'Placeholder text must be a string.', + 'Number of rows must be integral.', + 'Checkpoints must not be assigned to cards that can be bypassed.', + ], + State5: [ + 'Placeholder text must be a string.', + 'Number of rows must be integral.', + 'Checkpoints must not be assigned to cards that can be bypassed.', + ], + State6: [ + 'Placeholder text must be a string.', + 'Number of rows must be integral.', + 'Learner should not be directed back by more than' + + ' 3 cards in the lesson.', + 'Checkpoints must not be assigned to cards that can be bypassed.', + ], + State7: [ + 'Placeholder text must be a string.', + 'Number of rows must be integral.', + 'Learner should not be directed back by more than' + + ' 3 cards in the lesson.', + ], + End: [ + 'Please make sure end exploration interactions do not ' + + 'have any Oppia responses.', + ], + }); + } + ); }); diff --git a/core/templates/pages/exploration-editor-page/services/exploration-warnings.service.ts b/core/templates/pages/exploration-editor-page/services/exploration-warnings.service.ts index 5fc018e67cd6..4fa97d03d25b 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-warnings.service.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-warnings.service.ts @@ -16,42 +16,46 @@ * @fileoverview A service that lists all the exploration warnings. */ -import { Injectable, Injector } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; -import { GraphDataService } from 'pages/exploration-editor-page/services/graph-data.service'; -import { SolutionValidityService } from 'pages/exploration-editor-page/editor-tab/services/solution-validity.service'; -import { ImprovementsService } from 'services/improvements.service'; -import { StateTopAnswersStatsService } from 'services/state-top-answers-stats.service'; -import { ExplorationEditorPageConstants } from 'pages/exploration-editor-page/exploration-editor-page.constants'; -import { ExplorationInitStateNameService } from './exploration-init-state-name.service'; -import { ParameterMetadataService } from 'pages/exploration-editor-page/services/parameter-metadata.service'; +import {Injectable, Injector} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; +import {GraphDataService} from 'pages/exploration-editor-page/services/graph-data.service'; +import {SolutionValidityService} from 'pages/exploration-editor-page/editor-tab/services/solution-validity.service'; +import {ImprovementsService} from 'services/improvements.service'; +import {StateTopAnswersStatsService} from 'services/state-top-answers-stats.service'; +import {ExplorationEditorPageConstants} from 'pages/exploration-editor-page/exploration-editor-page.constants'; +import {ExplorationInitStateNameService} from './exploration-init-state-name.service'; +import {ParameterMetadataService} from 'pages/exploration-editor-page/services/parameter-metadata.service'; import INTERACTION_SPECS from 'interactions/interaction_specs.json'; -import { AppConstants } from 'app.constants'; -import { State } from 'domain/state/StateObjectFactory'; -import { ComputeGraphService, GraphLink, GraphNodes } from 'services/compute-graph.service'; -import { AlgebraicExpressionInputValidationService } from 'interactions/AlgebraicExpressionInput/directives/algebraic-expression-input-validation.service'; -import { CodeReplValidationService } from 'interactions/CodeRepl/directives/code-repl-validation.service'; -import { ContinueValidationService } from 'interactions/Continue/directives/continue-validation.service'; -import { DragAndDropSortInputValidationService } from 'interactions/DragAndDropSortInput/directives/drag-and-drop-sort-input-validation.service'; -import { EndExplorationValidationService } from 'interactions/EndExploration/directives/end-exploration-validation.service'; -import { FractionInputValidationService } from 'interactions/FractionInput/directives/fraction-input-validation.service'; -import { GraphInputValidationService } from 'interactions/GraphInput/directives/graph-input-validation.service'; -import { InteractiveMapValidationService } from 'interactions/InteractiveMap/directives/interactive-map-validation.service'; -import { ImageClickInputValidationService } from 'interactions/ImageClickInput/directives/image-click-input-validation.service'; -import { MathEquationInputValidationService } from 'interactions/MathEquationInput/directives/math-equation-input-validation.service'; -import { ItemSelectionInputValidationService } from 'interactions/ItemSelectionInput/directives/item-selection-input-validation.service'; -import { MultipleChoiceInputValidationService } from 'interactions/MultipleChoiceInput/directives/multiple-choice-input-validation.service'; -import { NumberWithUnitsValidationService } from 'interactions/NumberWithUnits/directives/number-with-units-validation.service'; -import { MusicNotesInputValidationService } from 'interactions/MusicNotesInput/directives/music-notes-input-validation.service'; -import { NumericExpressionInputValidationService } from 'interactions/NumericExpressionInput/directives/numeric-expression-input-validation.service'; -import { NumericInputValidationService } from 'interactions/NumericInput/directives/numeric-input-validation.service'; -import { PencilCodeEditorValidationService } from 'interactions/PencilCodeEditor/directives/pencil-code-editor-validation.service'; -import { RatioExpressionInputValidationService } from 'interactions/RatioExpressionInput/directives/ratio-expression-input-validation.service'; -import { SetInputValidationService } from 'interactions/SetInput/directives/set-input-validation.service'; -import { TextInputValidationService } from 'interactions/TextInput/directives/text-input-validation.service'; -import { States } from 'domain/exploration/StatesObjectFactory'; -import { InteractionSpecsKey } from 'pages/interaction-specs.constants'; +import {AppConstants} from 'app.constants'; +import {State} from 'domain/state/StateObjectFactory'; +import { + ComputeGraphService, + GraphLink, + GraphNodes, +} from 'services/compute-graph.service'; +import {AlgebraicExpressionInputValidationService} from 'interactions/AlgebraicExpressionInput/directives/algebraic-expression-input-validation.service'; +import {CodeReplValidationService} from 'interactions/CodeRepl/directives/code-repl-validation.service'; +import {ContinueValidationService} from 'interactions/Continue/directives/continue-validation.service'; +import {DragAndDropSortInputValidationService} from 'interactions/DragAndDropSortInput/directives/drag-and-drop-sort-input-validation.service'; +import {EndExplorationValidationService} from 'interactions/EndExploration/directives/end-exploration-validation.service'; +import {FractionInputValidationService} from 'interactions/FractionInput/directives/fraction-input-validation.service'; +import {GraphInputValidationService} from 'interactions/GraphInput/directives/graph-input-validation.service'; +import {InteractiveMapValidationService} from 'interactions/InteractiveMap/directives/interactive-map-validation.service'; +import {ImageClickInputValidationService} from 'interactions/ImageClickInput/directives/image-click-input-validation.service'; +import {MathEquationInputValidationService} from 'interactions/MathEquationInput/directives/math-equation-input-validation.service'; +import {ItemSelectionInputValidationService} from 'interactions/ItemSelectionInput/directives/item-selection-input-validation.service'; +import {MultipleChoiceInputValidationService} from 'interactions/MultipleChoiceInput/directives/multiple-choice-input-validation.service'; +import {NumberWithUnitsValidationService} from 'interactions/NumberWithUnits/directives/number-with-units-validation.service'; +import {MusicNotesInputValidationService} from 'interactions/MusicNotesInput/directives/music-notes-input-validation.service'; +import {NumericExpressionInputValidationService} from 'interactions/NumericExpressionInput/directives/numeric-expression-input-validation.service'; +import {NumericInputValidationService} from 'interactions/NumericInput/directives/numeric-input-validation.service'; +import {PencilCodeEditorValidationService} from 'interactions/PencilCodeEditor/directives/pencil-code-editor-validation.service'; +import {RatioExpressionInputValidationService} from 'interactions/RatioExpressionInput/directives/ratio-expression-input-validation.service'; +import {SetInputValidationService} from 'interactions/SetInput/directives/set-input-validation.service'; +import {TextInputValidationService} from 'interactions/TextInput/directives/text-input-validation.service'; +import {States} from 'domain/exploration/StatesObjectFactory'; +import {InteractionSpecsKey} from 'pages/interaction-specs.constants'; var Dequeue = require('dequeue'); interface _getStatesAndAnswerGroupsWithEmptyClassifiersResult { @@ -95,7 +99,7 @@ const INTERACTION_SERVICE_MAPPING = { }; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ExplorationWarningsService { constructor( @@ -107,7 +111,7 @@ export class ExplorationWarningsService { private solutionValidityService: SolutionValidityService, private stateTopAnswersStatsService: StateTopAnswersStatsService, private parameterMetadataService: ParameterMetadataService, - private computeGraphService: ComputeGraphService, + private computeGraphService: ComputeGraphService ) { this.stateWarnings = {}; } @@ -123,7 +127,7 @@ export class ExplorationWarningsService { let states = this.explorationStatesService.getStates(); - states.getStateNames().forEach((stateName) => { + states.getStateNames().forEach(stateName => { if (!states.getState(stateName).interaction.id) { statesWithoutInteractionIds.push(stateName); } @@ -136,9 +140,11 @@ export class ExplorationWarningsService { let statesWithIncorrectSolution: string[] = []; let states = this.explorationStatesService.getStates(); - states.getStateNames().forEach((stateName) => { - if (states.getState(stateName).interaction.solution && - !this.solutionValidityService.isSolutionValid(stateName)) { + states.getStateNames().forEach(stateName => { + if ( + states.getState(stateName).interaction.solution && + !this.solutionValidityService.isSolutionValid(stateName) + ) { statesWithIncorrectSolution.push(stateName); } }); @@ -155,8 +161,10 @@ export class ExplorationWarningsService { // - edges: a list of edges, each of which is an object with keys 'source', // and 'target'. _getUnreachableNodeNames( - initNodeIds: string[], - nodes: GraphNodes, edges: GraphLink[]): string[] { + initNodeIds: string[], + nodes: GraphNodes, + edges: GraphLink[] + ): string[] { let queue = initNodeIds; let seen: Record = {}; for (let i = 0; i < initNodeIds.length; i++) { @@ -164,7 +172,7 @@ export class ExplorationWarningsService { } while (queue.length > 0) { let currNodeId = queue.shift(); - edges.forEach((edge) => { + edges.forEach(edge => { if (edge.source === currNodeId && !seen.hasOwnProperty(edge.target)) { seen[edge.target] = true; queue.push(edge.target); @@ -174,7 +182,7 @@ export class ExplorationWarningsService { let unreachableNodeNames = []; for (let nodeId in nodes) { - if (!(seen.hasOwnProperty(nodes[nodeId]))) { + if (!seen.hasOwnProperty(nodes[nodeId])) { unreachableNodeNames.push(nodes[nodeId]); } } @@ -186,12 +194,12 @@ export class ExplorationWarningsService { // 'target' switched. (The objects represent edges in a graph, and this // operation amounts to reversing all the edges.) _getReversedLinks(links: GraphLink[]): _getReversedLinksResult[] { - return links.map((link) => { + return links.map(link => { return { source: link.target, target: link.source, linkProperty: null, - connectsDestIfStuck: false + connectsDestIfStuck: false, }; }); } @@ -199,31 +207,32 @@ export class ExplorationWarningsService { // Verify that all parameters referred to in a state are guaranteed to // have been set beforehand. _verifyParameters(initNodeIds: string[]): _verifyParametersResult[] { - let unsetParametersInfo = ( - this.parameterMetadataService.getUnsetParametersInfo(initNodeIds)); + let unsetParametersInfo = + this.parameterMetadataService.getUnsetParametersInfo(initNodeIds); let paramWarningsList: _verifyParametersResult[] = []; - unsetParametersInfo.forEach((unsetParameterData) => { + unsetParametersInfo.forEach(unsetParameterData => { if (!unsetParameterData.stateName) { // The parameter value is required in the initial list of parameter // changes. paramWarningsList.push({ type: AppConstants.WARNING_TYPES.CRITICAL, - message: ( + message: 'Please ensure the value of parameter "' + unsetParameterData.paramName + '" is set before it is referred to in the initial list of ' + - 'parameter changes.') + 'parameter changes.', }); } else { // The parameter value is required in a subsequent state. paramWarningsList.push({ type: AppConstants.WARNING_TYPES.CRITICAL, - message: ( + message: 'Please ensure the value of parameter "' + unsetParameterData.paramName + - '" is set before using it in "' + unsetParameterData.stateName + - '".') + '" is set before using it in "' + + unsetParameterData.stateName + + '".', }); } }); @@ -236,8 +245,7 @@ export class ExplorationWarningsService { let answerGroups = state.interaction.answerGroups; for (let i = 0; i < answerGroups.length; i++) { let group = answerGroups[i]; - if (group.rules.length === 0 && - group.trainingData.length === 0) { + if (group.rules.length === 0 && group.trainingData.length === 0) { indexes.push(i); } } @@ -245,14 +253,18 @@ export class ExplorationWarningsService { } _getStatesWithInvalidRedirection( - initStateId: string, links: GraphLink[] + initStateId: string, + links: GraphLink[] ): string[] { let results: string[] = []; let states = this.explorationStatesService.getStates(); let bfsStateList = this.computeGraphService.computeBfsTraversalOfStates( - initStateId, states, initStateId); + initStateId, + states, + initStateId + ); let visited: Record = {}; - bfsStateList.forEach((stateName) => { + bfsStateList.forEach(stateName => { // Go through all states, taking in the dest and source // calculate distance for each, if invalid push in results. visited[stateName] = true; @@ -260,16 +272,21 @@ export class ExplorationWarningsService { if (state && state.interaction) { if (state.interaction.defaultOutcome) { let defaultDest = state.interaction.defaultOutcome.dest; - if (defaultDest !== stateName && visited[defaultDest] && - !this.redirectionIsValid(defaultDest, stateName, links)) { + if ( + defaultDest !== stateName && + visited[defaultDest] && + !this.redirectionIsValid(defaultDest, stateName, links) + ) { results.push(stateName); } } let answerGroups = state.interaction.answerGroups; for (let i = 0; i < answerGroups.length; i++) { let dest = answerGroups[i].outcome.dest; - if (visited[dest] && - !this.redirectionIsValid(dest, stateName, links)) { + if ( + visited[dest] && + !this.redirectionIsValid(dest, stateName, links) + ) { results.push(stateName); } } @@ -278,18 +295,18 @@ export class ExplorationWarningsService { return results; } - _getStatesAndAnswerGroupsWithEmptyClassifiers(): - _getStatesAndAnswerGroupsWithEmptyClassifiersResult[] { + _getStatesAndAnswerGroupsWithEmptyClassifiers(): _getStatesAndAnswerGroupsWithEmptyClassifiersResult[] { let results: {groupIndexes: number[]; stateName: string}[] = []; let states: States = this.explorationStatesService.getStates(); - states.getStateNames().forEach((stateName) => { + states.getStateNames().forEach(stateName => { let groupIndexes = this._getAnswerGroupIndexesWithEmptyClassifiers( - states.getState(stateName)); + states.getState(stateName) + ); if (groupIndexes.length > 0) { results.push({ groupIndexes: groupIndexes, - stateName: stateName + stateName: stateName, }); } }); @@ -299,21 +316,25 @@ export class ExplorationWarningsService { _getStatesWithAnswersThatMustBeResolved(): string[] { let states: States = this.explorationStatesService.getStates(); - return this.stateTopAnswersStatsService.getStateNamesWithStats() - .filter((stateName) => { - let mustResolveState = ( - this.improvementsService - .isStateForcedToResolveOutstandingUnaddressedAnswers( - states.getState(stateName))); - - return mustResolveState && - this.stateTopAnswersStatsService.getUnresolvedStateStats(stateName) - .some((answer) => { + return this.stateTopAnswersStatsService + .getStateNamesWithStats() + .filter(stateName => { + let mustResolveState = + this.improvementsService.isStateForcedToResolveOutstandingUnaddressedAnswers( + states.getState(stateName) + ); + + return ( + mustResolveState && + this.stateTopAnswersStatsService + .getUnresolvedStateStats(stateName) + .some(answer => { return ( answer.frequency >= - ExplorationEditorPageConstants - .UNRESOLVED_ANSWER_FREQUENCY_THRESHOLD); - }); + ExplorationEditorPageConstants.UNRESOLVED_ANSWER_FREQUENCY_THRESHOLD + ); + }) + ); }); } @@ -336,7 +357,7 @@ export class ExplorationWarningsService { let _graphData = this.graphDataService.getGraphData(); let _states = this.explorationStatesService.getStates(); - _states.getStateNames().forEach((stateName) => { + _states.getStateNames().forEach(stateName => { let interaction = _states.getState(stateName).interaction; if (interaction.id) { let validatorServiceName = @@ -344,17 +365,23 @@ export class ExplorationWarningsService { let validatorService = this.injector.get( INTERACTION_SERVICE_MAPPING[ - validatorServiceName as keyof typeof INTERACTION_SERVICE_MAPPING]); + validatorServiceName as keyof typeof INTERACTION_SERVICE_MAPPING + ] + ); let interactionWarnings = validatorService.getAllWarnings( - stateName, interaction.customizationArgs, - interaction.answerGroups, interaction.defaultOutcome); + stateName, + interaction.customizationArgs, + interaction.answerGroups, + interaction.defaultOutcome + ); for (let j = 0; j < interactionWarnings.length; j++) { _extendStateWarnings(stateName, interactionWarnings[j].message); - if (interactionWarnings[j].type === - AppConstants.WARNING_TYPES.CRITICAL) { + if ( + interactionWarnings[j].type === AppConstants.WARNING_TYPES.CRITICAL + ) { this.hasCriticalStateWarning = true; } } @@ -362,59 +389,75 @@ export class ExplorationWarningsService { }); let statesWithoutInteractionIds = this._getStatesWithoutInteractionIds(); - statesWithoutInteractionIds.forEach((stateWithoutInteractionIds) => { + statesWithoutInteractionIds.forEach(stateWithoutInteractionIds => { _extendStateWarnings( stateWithoutInteractionIds, - AppConstants.STATE_ERROR_MESSAGES.ADD_INTERACTION); + AppConstants.STATE_ERROR_MESSAGES.ADD_INTERACTION + ); }); let statesWithInvalidRedirection = this._getStatesWithInvalidRedirection( - _graphData.initStateId, _graphData.links); - statesWithInvalidRedirection.forEach((stateName) => { + _graphData.initStateId, + _graphData.links + ); + statesWithInvalidRedirection.forEach(stateName => { _extendStateWarnings( - stateName, AppConstants.STATE_ERROR_MESSAGES.INVALID_REDIRECTION); + stateName, + AppConstants.STATE_ERROR_MESSAGES.INVALID_REDIRECTION + ); }); let statesWithAnswersThatMustBeResolved = this._getStatesWithAnswersThatMustBeResolved(); - statesWithAnswersThatMustBeResolved.forEach((stateName) => { + statesWithAnswersThatMustBeResolved.forEach(stateName => { _extendStateWarnings( - stateName, AppConstants.STATE_ERROR_MESSAGES.UNRESOLVED_ANSWER); + stateName, + AppConstants.STATE_ERROR_MESSAGES.UNRESOLVED_ANSWER + ); }); let statesWithIncorrectSolution = this._getStatesWithIncorrectSolution(); - statesWithIncorrectSolution.forEach((state) => { + statesWithIncorrectSolution.forEach(state => { _extendStateWarnings( - state, AppConstants.STATE_ERROR_MESSAGES.INCORRECT_SOLUTION); + state, + AppConstants.STATE_ERROR_MESSAGES.INCORRECT_SOLUTION + ); }); - if (_graphData) { let unreachableStateNames = this._getUnreachableNodeNames( - [_graphData.initStateId], _graphData.nodes, _graphData.links); + [_graphData.initStateId], + _graphData.nodes, + _graphData.links + ); if (unreachableStateNames.length) { - unreachableStateNames.forEach((unreachableStateName) => { + unreachableStateNames.forEach(unreachableStateName => { _extendStateWarnings( unreachableStateName, - AppConstants.STATE_ERROR_MESSAGES.STATE_UNREACHABLE); + AppConstants.STATE_ERROR_MESSAGES.STATE_UNREACHABLE + ); }); } else { // Only perform this check if all states are reachable. let deadEndStates = this._getUnreachableNodeNames( - _graphData.finalStateIds, _graphData.nodes, - this._getReversedLinks(_graphData.links)); + _graphData.finalStateIds, + _graphData.nodes, + this._getReversedLinks(_graphData.links) + ); if (deadEndStates.length) { - deadEndStates.forEach((deadEndState) => { + deadEndStates.forEach(deadEndState => { _extendStateWarnings( deadEndState, - AppConstants.STATE_ERROR_MESSAGES.UNABLE_TO_END_EXPLORATION); + AppConstants.STATE_ERROR_MESSAGES.UNABLE_TO_END_EXPLORATION + ); }); } } - this._warningsList = this._warningsList.concat(this._verifyParameters([ - _graphData.initStateId])); + this._warningsList = this._warningsList.concat( + this._verifyParameters([_graphData.initStateId]) + ); } let initStateName = this.explorationInitStateNameService.displayed; @@ -422,7 +465,8 @@ export class ExplorationWarningsService { if (initState && !initState.cardIsCheckpoint) { _extendStateWarnings( initStateName as string, - AppConstants.CHECKPOINT_ERROR_MESSAGES.INIT_CARD); + AppConstants.CHECKPOINT_ERROR_MESSAGES.INIT_CARD + ); } let nonInitialCheckpointStateNames: string[] = []; @@ -430,12 +474,15 @@ export class ExplorationWarningsService { _states.getStateNames().forEach(stateName => { let state = _states.getState(stateName); let interactionId = state.interaction.id; - if (Boolean(interactionId) && INTERACTION_SPECS[ - interactionId as InteractionSpecsKey - ].is_terminal) { + if ( + Boolean(interactionId) && + INTERACTION_SPECS[interactionId as InteractionSpecsKey].is_terminal + ) { if (state.cardIsCheckpoint && stateName !== initStateName) { _extendStateWarnings( - stateName, AppConstants.CHECKPOINT_ERROR_MESSAGES.TERMINAL_CARD); + stateName, + AppConstants.CHECKPOINT_ERROR_MESSAGES.TERMINAL_CARD + ); } terminalStateCount++; } @@ -446,11 +493,11 @@ export class ExplorationWarningsService { let checkpointCount = this.explorationStatesService.getCheckpointCount(); if (checkpointCount >= 9) { - this.checkpointCountWarning = ( - AppConstants.CHECKPOINT_ERROR_MESSAGES.CHECKPOINT_COUNT); + this.checkpointCountWarning = + AppConstants.CHECKPOINT_ERROR_MESSAGES.CHECKPOINT_COUNT; this._warningsList.push({ type: AppConstants.WARNING_TYPES.ERROR, - message: ('Checkpoint count has an error.') + message: 'Checkpoint count has an error.', }); } @@ -467,14 +514,18 @@ export class ExplorationWarningsService { }); let unreachableNodeNames = this._getUnreachableNodeNames( - [_graphData.initStateId], newNodes, newLinks); + [_graphData.initStateId], + newNodes, + newLinks + ); let terminalUnreachableStateCount = 0; - unreachableNodeNames.forEach((stateName) => { + unreachableNodeNames.forEach(stateName => { let interactionId = _states.getState(stateName).interaction.id; - if (Boolean(interactionId) && INTERACTION_SPECS[ - interactionId as InteractionSpecsKey - ].is_terminal) { + if ( + Boolean(interactionId) && + INTERACTION_SPECS[interactionId as InteractionSpecsKey].is_terminal + ) { terminalUnreachableStateCount++; } }); @@ -482,25 +533,29 @@ export class ExplorationWarningsService { if (terminalStateCount !== terminalUnreachableStateCount) { _extendStateWarnings( nonInitialCheckpointStateNames[i], - AppConstants.CHECKPOINT_ERROR_MESSAGES.BYPASSABLE_CARD); + AppConstants.CHECKPOINT_ERROR_MESSAGES.BYPASSABLE_CARD + ); } } if (Object.keys(this.stateWarnings).length) { - let errorString = ( - Object.keys(this.stateWarnings).length > 1 ? 'cards have' : 'card has'); + let errorString = + Object.keys(this.stateWarnings).length > 1 ? 'cards have' : 'card has'; this._warningsList.push({ type: AppConstants.WARNING_TYPES.ERROR, - message: ( - 'The following ' + errorString + ' errors: ' + - Object.keys(this.stateWarnings).join(', ') + '.') + message: + 'The following ' + + errorString + + ' errors: ' + + Object.keys(this.stateWarnings).join(', ') + + '.', }); } - let statesWithAnswerGroupsWithEmptyClassifiers = ( - this._getStatesAndAnswerGroupsWithEmptyClassifiers()); - statesWithAnswerGroupsWithEmptyClassifiers.forEach((result) => { - let warningMessage = 'In \'' + result.stateName + '\''; + let statesWithAnswerGroupsWithEmptyClassifiers = + this._getStatesAndAnswerGroupsWithEmptyClassifiers(); + statesWithAnswerGroupsWithEmptyClassifiers.forEach(result => { + let warningMessage = "In '" + result.stateName + "'"; if (result.groupIndexes.length !== 1) { warningMessage += ', the following answer groups have classifiers '; warningMessage += 'with no training data: '; @@ -512,7 +567,7 @@ export class ExplorationWarningsService { this._warningsList.push({ message: warningMessage, - type: AppConstants.WARNING_TYPES.ERROR + type: AppConstants.WARNING_TYPES.ERROR, }); }); } @@ -535,7 +590,8 @@ export class ExplorationWarningsService { hasCriticalWarnings(): boolean { return ( - this.hasCriticalStateWarning || this._warningsList.some((warning) => { + this.hasCriticalStateWarning || + this._warningsList.some(warning => { return warning.type === AppConstants.WARNING_TYPES.CRITICAL; }) ); @@ -546,17 +602,24 @@ export class ExplorationWarningsService { } redirectionIsValid( - sourceStateName: string, destStateName: string, links: GraphLink[] + sourceStateName: string, + destStateName: string, + links: GraphLink[] ): boolean { let distance = this.getDistanceToDestState( - sourceStateName, destStateName, links); + sourceStateName, + destStateName, + links + ); // Raise validation error if the creator redirects the learner // back by more than MAX_CARD_COUNT_FOR_VALID_REDIRECTION cards. return distance <= AppConstants.MAX_CARD_COUNT_FOR_VALID_REDIRECTION; } getDistanceToDestState( - sourceStateName: string, destStateName: string, links: GraphLink[] + sourceStateName: string, + destStateName: string, + links: GraphLink[] ): number { let distanceToDestState = -1; let stateFound = false; @@ -593,6 +656,9 @@ export class ExplorationWarningsService { } } -angular.module('oppia').factory( - 'ExplorationWarningsService', downgradeInjectable( - ExplorationWarningsService)); +angular + .module('oppia') + .factory( + 'ExplorationWarningsService', + downgradeInjectable(ExplorationWarningsService) + ); diff --git a/core/templates/pages/exploration-editor-page/services/graph-data.service.spec.ts b/core/templates/pages/exploration-editor-page/services/graph-data.service.spec.ts index e8cf28653415..ffab50b0103b 100644 --- a/core/templates/pages/exploration-editor-page/services/graph-data.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/graph-data.service.spec.ts @@ -15,13 +15,12 @@ /** * @fileoverview Unit tests for GraphDataService. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TestBed } from '@angular/core/testing'; - -import { GraphDataService } from 'pages/exploration-editor-page/services/graph-data.service'; -import { ExplorationInitStateNameService } from 'pages/exploration-editor-page/services/exploration-init-state-name.service'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TestBed} from '@angular/core/testing'; +import {GraphDataService} from 'pages/exploration-editor-page/services/graph-data.service'; +import {ExplorationInitStateNameService} from 'pages/exploration-editor-page/services/exploration-init-state-name.service'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; describe('Graph Data Service', () => { let graphDataService: GraphDataService; @@ -30,115 +29,128 @@ describe('Graph Data Service', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] + imports: [HttpClientTestingModule], }); graphDataService = TestBed.get(GraphDataService); explorationInitStateNameService = TestBed.get( - ExplorationInitStateNameService); + ExplorationInitStateNameService + ); explorationStatesService = TestBed.get(ExplorationStatesService); - explorationStatesService.init({ - Hola: { - content: {content_id: 'content', html: ''}, - recorded_voiceovers: { - voiceovers_mapping: { - content: {}, - default_outcome: {}, - feedback_1: {}, - rule_input: {} + explorationStatesService.init( + { + Hola: { + content: {content_id: 'content', html: ''}, + recorded_voiceovers: { + voiceovers_mapping: { + content: {}, + default_outcome: {}, + feedback_1: {}, + rule_input: {}, + }, }, - }, - param_changes: [], - interaction: { - confirmed_unclassified_answers: [], - answer_groups: [{ - rule_specs: [{ - rule_type: 'Contains', - inputs: { - x: { - contentId: 'rule_input', - normalizedStrSet: ['hola'] - } - } - }], - outcome: { - dest: 'Me Llamo', - dest_if_really_stuck: 'Me Llamo', + param_changes: [], + interaction: { + confirmed_unclassified_answers: [], + answer_groups: [ + { + rule_specs: [ + { + rule_type: 'Contains', + inputs: { + x: { + contentId: 'rule_input', + normalizedStrSet: ['hola'], + }, + }, + }, + ], + outcome: { + dest: 'Me Llamo', + dest_if_really_stuck: 'Me Llamo', + feedback: { + content_id: 'feedback_1', + html: 'buen trabajo!', + }, + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + }, + training_data: [], + tagged_skill_misconception_id: null, + }, + ], + customization_args: { + placeholder: { + value: { + content_id: 'ca_placeholder_0', + unicode_str: '', + }, + }, + rows: {value: 1}, + catchMisspellings: { + value: false, + }, + }, + default_outcome: { + dest: 'Hola', + dest_if_really_stuck: 'Hola', feedback: { - content_id: 'feedback_1', - html: 'buen trabajo!', + content_id: 'default_outcome', + html: 'try again!', }, labelled_as_correct: true, param_changes: [], refresher_exploration_id: null, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, - training_data: [], - tagged_skill_misconception_id: null - }], - customization_args: { - placeholder: { - value: { - content_id: 'ca_placeholder_0', - unicode_str: '' - } - }, - rows: { value: 1 }, - catchMisspellings: { - value: false - } - }, - default_outcome: { - dest: 'Hola', - dest_if_really_stuck: 'Hola', - feedback: { - content_id: 'default_outcome', - html: 'try again!', - }, - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null + hints: [], + id: 'TextInput', + solution: null, }, - hints: [], - id: 'TextInput', - solution: null, + linked_skill_id: null, + solicit_answer_details: false, + classifier_model_id: '0', + card_is_checkpoint: false, }, - linked_skill_id: null, - solicit_answer_details: false, - classifier_model_id: '0', - card_is_checkpoint: false, }, - }, false); + false + ); }); it('should recompute graph data', () => { var graphData = { finalStateIds: [], initStateId: 'property_1', - links: [{ - source: 'Hola', - target: 'Me Llamo', - linkProperty: null, - connectsDestIfStuck: false - }, { - source: 'Hola', - target: 'Me Llamo', - linkProperty: null, - connectsDestIfStuck: true - }, { - source: 'Hola', - target: 'Hola', - linkProperty: null, - connectsDestIfStuck: false - }, { - source: 'Hola', - target: 'Hola', - linkProperty: null, - connectsDestIfStuck: true - }], - nodes: { Hola: 'Hola' } + links: [ + { + source: 'Hola', + target: 'Me Llamo', + linkProperty: null, + connectsDestIfStuck: false, + }, + { + source: 'Hola', + target: 'Me Llamo', + linkProperty: null, + connectsDestIfStuck: true, + }, + { + source: 'Hola', + target: 'Hola', + linkProperty: null, + connectsDestIfStuck: false, + }, + { + source: 'Hola', + target: 'Hola', + linkProperty: null, + connectsDestIfStuck: true, + }, + ], + nodes: {Hola: 'Hola'}, }; graphDataService.recompute(); diff --git a/core/templates/pages/exploration-editor-page/services/graph-data.service.ts b/core/templates/pages/exploration-editor-page/services/graph-data.service.ts index 8c44866ed7a7..01af29091390 100644 --- a/core/templates/pages/exploration-editor-page/services/graph-data.service.ts +++ b/core/templates/pages/exploration-editor-page/services/graph-data.service.ts @@ -16,15 +16,15 @@ * @fileoverview Service for computing graph data. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { EventEmitter, Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {EventEmitter, Injectable} from '@angular/core'; import cloneDeep from 'lodash/cloneDeep'; -import { ComputeGraphService, GraphData } from 'services/compute-graph.service'; -import { ExplorationInitStateNameService } from 'pages/exploration-editor-page/services/exploration-init-state-name.service'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; +import {ComputeGraphService, GraphData} from 'services/compute-graph.service'; +import {ExplorationInitStateNameService} from 'pages/exploration-editor-page/services/exploration-init-state-name.service'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class GraphDataService { // This property is initialized using int method and we need to do @@ -66,5 +66,6 @@ export class GraphDataService { } } -angular.module('oppia').factory( - 'GraphDataService', downgradeInjectable(GraphDataService)); +angular + .module('oppia') + .factory('GraphDataService', downgradeInjectable(GraphDataService)); diff --git a/core/templates/pages/exploration-editor-page/services/history-tab-backend-api.service.spec.ts b/core/templates/pages/exploration-editor-page/services/history-tab-backend-api.service.spec.ts index 13cff02def08..0816fe34c724 100644 --- a/core/templates/pages/exploration-editor-page/services/history-tab-backend-api.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/history-tab-backend-api.service.spec.ts @@ -16,10 +16,12 @@ * @fileoverview Unit tests for HistoryTabBackendApiService. */ -import { HttpClientTestingModule, HttpTestingController } from - '@angular/common/http/testing'; -import { fakeAsync, flushMicrotasks, TestBed } from '@angular/core/testing'; -import { HistoryTabBackendApiService } from './history-tab-backend-api.service'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {fakeAsync, flushMicrotasks, TestBed} from '@angular/core/testing'; +import {HistoryTabBackendApiService} from './history-tab-backend-api.service'; describe('History Tab Backend Api Service', () => { let service: HistoryTabBackendApiService; @@ -30,7 +32,7 @@ describe('History Tab Backend Api Service', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [HistoryTabBackendApiService] + providers: [HistoryTabBackendApiService], }); httpTestingController = TestBed.get(HttpTestingController); service = TestBed.get(HistoryTabBackendApiService); @@ -40,65 +42,55 @@ describe('History Tab Backend Api Service', () => { httpTestingController.verify(); }); - it('should get history data when getData called', - fakeAsync(() => { - let stringifiedExpIds = 'check'; - service.getData( - stringifiedExpIds - ).then(successHandler, failHandler); - - let req = httpTestingController.expectOne('check'); - expect(req.request.method).toEqual('GET'); - req.flush([]); - - flushMicrotasks(); - - expect(successHandler).toHaveBeenCalled(); - expect(failHandler).not.toHaveBeenCalled(); - }) - ); - - it('should get check revert valid data when getCheckRevertValidData called', - fakeAsync(() => { - const url = 'url'; - service.getCheckRevertValidData(url).then(successHandler, failHandler); - - let req = httpTestingController.expectOne(url); - expect(req.request.method).toEqual('GET'); - req.flush([]); - - flushMicrotasks(); - - expect(successHandler).toHaveBeenCalled(); - expect(failHandler).not.toHaveBeenCalled(); - }) - ); - - it('should post history data when postData called', - fakeAsync(() => { - let data = { - revertExplorationUrl: 'check', - currentVersion: 5, - revertToVersion: 3 - }; - let recivedData = { - current_version: 5, - revert_to_version: 3 - }; - - service.postData( - data - ).then(successHandler, failHandler); - - let req = httpTestingController.expectOne('check'); - expect(req.request.method).toEqual('POST'); - expect(req.request.body).toEqual(recivedData); - req.flush([]); - - flushMicrotasks(); - - expect(successHandler).toHaveBeenCalled(); - expect(failHandler).not.toHaveBeenCalled(); - }) - ); + it('should get history data when getData called', fakeAsync(() => { + let stringifiedExpIds = 'check'; + service.getData(stringifiedExpIds).then(successHandler, failHandler); + + let req = httpTestingController.expectOne('check'); + expect(req.request.method).toEqual('GET'); + req.flush([]); + + flushMicrotasks(); + + expect(successHandler).toHaveBeenCalled(); + expect(failHandler).not.toHaveBeenCalled(); + })); + + it('should get check revert valid data when getCheckRevertValidData called', fakeAsync(() => { + const url = 'url'; + service.getCheckRevertValidData(url).then(successHandler, failHandler); + + let req = httpTestingController.expectOne(url); + expect(req.request.method).toEqual('GET'); + req.flush([]); + + flushMicrotasks(); + + expect(successHandler).toHaveBeenCalled(); + expect(failHandler).not.toHaveBeenCalled(); + })); + + it('should post history data when postData called', fakeAsync(() => { + let data = { + revertExplorationUrl: 'check', + currentVersion: 5, + revertToVersion: 3, + }; + let recivedData = { + current_version: 5, + revert_to_version: 3, + }; + + service.postData(data).then(successHandler, failHandler); + + let req = httpTestingController.expectOne('check'); + expect(req.request.method).toEqual('POST'); + expect(req.request.body).toEqual(recivedData); + req.flush([]); + + flushMicrotasks(); + + expect(successHandler).toHaveBeenCalled(); + expect(failHandler).not.toHaveBeenCalled(); + })); }); diff --git a/core/templates/pages/exploration-editor-page/services/history-tab-backend-api.service.ts b/core/templates/pages/exploration-editor-page/services/history-tab-backend-api.service.ts index 62afa0d744a0..ab7f5e20aa76 100644 --- a/core/templates/pages/exploration-editor-page/services/history-tab-backend-api.service.ts +++ b/core/templates/pages/exploration-editor-page/services/history-tab-backend-api.service.ts @@ -16,10 +16,10 @@ * @fileoverview Backend api service for history tab component; */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { ExplorationSnapshot } from '../history-tab/services/version-tree.service'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {ExplorationSnapshot} from '../history-tab/services/version-tree.service'; export interface HistoryTabDict { summaries: string[]; @@ -31,44 +31,45 @@ interface HistoryTabCheckRevertValidDict { details: string; } -interface HistoryTabData{ +interface HistoryTabData { revertExplorationUrl: string; currentVersion: number; revertToVersion: number; } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class HistoryTabBackendApiService { - constructor( - private http: HttpClient - ) {} + constructor(private http: HttpClient) {} getData(explorationAllSnapshotsUrl: string): Promise { - return this.http.get( - explorationAllSnapshotsUrl - ).toPromise(); + return this.http + .get(explorationAllSnapshotsUrl) + .toPromise(); } getCheckRevertValidData( - revertExplorationUrl: string + revertExplorationUrl: string ): Promise { - return this.http.get( - revertExplorationUrl).toPromise(); + return this.http + .get(revertExplorationUrl) + .toPromise(); } postData(data: HistoryTabData): Promise { - return this.http.post( - data.revertExplorationUrl, - { + return this.http + .post(data.revertExplorationUrl, { current_version: data.currentVersion, - revert_to_version: data.revertToVersion - } - ).toPromise(); + revert_to_version: data.revertToVersion, + }) + .toPromise(); } } -angular.module('oppia').factory( - 'HistoryTabBackendApiService', - downgradeInjectable(HistoryTabBackendApiService)); +angular + .module('oppia') + .factory( + 'HistoryTabBackendApiService', + downgradeInjectable(HistoryTabBackendApiService) + ); diff --git a/core/templates/pages/exploration-editor-page/services/history-tab-yaml-conversion.service.spec.ts b/core/templates/pages/exploration-editor-page/services/history-tab-yaml-conversion.service.spec.ts index 832b61a00315..93932ff77906 100644 --- a/core/templates/pages/exploration-editor-page/services/history-tab-yaml-conversion.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/history-tab-yaml-conversion.service.spec.ts @@ -16,10 +16,10 @@ * @fileoverview Unit tests for history tab yaml conversion service. */ -import { fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { State, StateObjectFactory } from 'domain/state/StateObjectFactory'; -import { YamlService } from 'services/yaml.service'; -import { HistoryTabYamlConversionService } from './history-tab-yaml-conversion.service'; +import {fakeAsync, TestBed, tick} from '@angular/core/testing'; +import {State, StateObjectFactory} from 'domain/state/StateObjectFactory'; +import {YamlService} from 'services/yaml.service'; +import {HistoryTabYamlConversionService} from './history-tab-yaml-conversion.service'; describe('History tab yaml conversion service', () => { let historyTabYamlConversionService: HistoryTabYamlConversionService; @@ -30,44 +30,46 @@ describe('History tab yaml conversion service', () => { beforeEach(() => { TestBed.configureTestingModule({ - providers: [YamlService] + providers: [YamlService], }); historyTabYamlConversionService = TestBed.inject( - HistoryTabYamlConversionService); + HistoryTabYamlConversionService + ); yamlService = TestBed.inject(YamlService); stateObjectFactory = TestBed.inject(StateObjectFactory); testState = stateObjectFactory.createDefaultState( - 'state_1', 'content_0', 'default_outcome_1'); + 'state_1', + 'content_0', + 'default_outcome_1' + ); testStateYamlString = yamlService.stringify(testState.toBackendDict()); }); - it('should get the yaml representation of the given entity when it is truthy', - fakeAsync(() => { - const successHandler = jasmine.createSpy('success'); - const failHandler = jasmine.createSpy('fail'); + it('should get the yaml representation of the given entity when it is truthy', fakeAsync(() => { + const successHandler = jasmine.createSpy('success'); + const failHandler = jasmine.createSpy('fail'); - historyTabYamlConversionService - .getYamlStringFromStateOrMetadata(testState) - .then(successHandler, failHandler); - tick(201); + historyTabYamlConversionService + .getYamlStringFromStateOrMetadata(testState) + .then(successHandler, failHandler); + tick(201); - expect(successHandler).toHaveBeenCalledWith(testStateYamlString); - expect(failHandler).not.toHaveBeenCalled(); - })); + expect(successHandler).toHaveBeenCalledWith(testStateYamlString); + expect(failHandler).not.toHaveBeenCalled(); + })); - it('should return an empty string when the given entity is falsy', - fakeAsync(() => { - const successHandler = jasmine.createSpy('success'); - const failHandler = jasmine.createSpy('fail'); + it('should return an empty string when the given entity is falsy', fakeAsync(() => { + const successHandler = jasmine.createSpy('success'); + const failHandler = jasmine.createSpy('fail'); - historyTabYamlConversionService - .getYamlStringFromStateOrMetadata(null) - .then(successHandler, failHandler); - tick(201); + historyTabYamlConversionService + .getYamlStringFromStateOrMetadata(null) + .then(successHandler, failHandler); + tick(201); - expect(successHandler).toHaveBeenCalledWith(''); - expect(failHandler).not.toHaveBeenCalled(); - })); + expect(successHandler).toHaveBeenCalledWith(''); + expect(failHandler).not.toHaveBeenCalled(); + })); }); diff --git a/core/templates/pages/exploration-editor-page/services/history-tab-yaml-conversion.service.ts b/core/templates/pages/exploration-editor-page/services/history-tab-yaml-conversion.service.ts index 6ae13501b2d3..f7dd3f367ddf 100644 --- a/core/templates/pages/exploration-editor-page/services/history-tab-yaml-conversion.service.ts +++ b/core/templates/pages/exploration-editor-page/services/history-tab-yaml-conversion.service.ts @@ -12,28 +12,25 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Service for converting state or metadata dicts into yaml during * version comparison in history tab. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { ExplorationMetadata } from 'domain/exploration/ExplorationMetadataObjectFactory'; -import { State } from 'domain/state/StateObjectFactory'; -import { YamlService } from 'services/yaml.service'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {ExplorationMetadata} from 'domain/exploration/ExplorationMetadataObjectFactory'; +import {State} from 'domain/state/StateObjectFactory'; +import {YamlService} from 'services/yaml.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class HistoryTabYamlConversionService { - constructor( - private yamlService: YamlService - ) {} + constructor(private yamlService: YamlService) {} getYamlStringFromStateOrMetadata( - entity: (State | ExplorationMetadata) | null + entity: (State | ExplorationMetadata) | null ): Promise { return new Promise((resolve, reject) => { // Note: the timeout is needed or the string will be sent @@ -50,6 +47,9 @@ export class HistoryTabYamlConversionService { } } -angular.module('oppia').factory( - 'HistoryTabYamlConversionService', - downgradeInjectable(HistoryTabYamlConversionService)); +angular + .module('oppia') + .factory( + 'HistoryTabYamlConversionService', + downgradeInjectable(HistoryTabYamlConversionService) + ); diff --git a/core/templates/pages/exploration-editor-page/services/parameter-metadata.service.spec.ts b/core/templates/pages/exploration-editor-page/services/parameter-metadata.service.spec.ts index 1e8a7f872302..68d00bbe8642 100644 --- a/core/templates/pages/exploration-editor-page/services/parameter-metadata.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/parameter-metadata.service.spec.ts @@ -16,53 +16,62 @@ * @fileoverview Unit tests for ParameterMetadataService. */ -import { TestBed } from '@angular/core/testing'; -import { ParameterMetadataService } from 'pages/exploration-editor-page/services/parameter-metadata.service'; -import { ExplorationParamChangesService } from 'pages/exploration-editor-page/services/exploration-param-changes.service'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; -import { GraphDataService } from 'pages/exploration-editor-page/services/graph-data.service'; -import { StatesObjectFactory } from 'domain/exploration/StatesObjectFactory'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; +import {TestBed} from '@angular/core/testing'; +import {ParameterMetadataService} from 'pages/exploration-editor-page/services/parameter-metadata.service'; +import {ExplorationParamChangesService} from 'pages/exploration-editor-page/services/exploration-param-changes.service'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; +import {GraphDataService} from 'pages/exploration-editor-page/services/graph-data.service'; +import {StatesObjectFactory} from 'domain/exploration/StatesObjectFactory'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; class MockExplorationParamChangesService { - savedMemento = [{ - customizationArgs: { - parse_with_jinja: false, - value: '5' + savedMemento = [ + { + customizationArgs: { + parse_with_jinja: false, + value: '5', + }, + generatorId: 'Copier', + name: 'ParamChange1', }, - generatorId: 'Copier', - name: 'ParamChange1' - }, { - customizationArgs: { - parse_with_jinja: true, - value: '{{ParamChange2}}' + { + customizationArgs: { + parse_with_jinja: true, + value: '{{ParamChange2}}', + }, + generatorId: 'Copier', }, - generatorId: 'Copier', - }, { - customizationArgs: { - parse_with_jinja: true, - value: '5' + { + customizationArgs: { + parse_with_jinja: true, + value: '5', + }, + generatorId: 'RandomSelector', + name: 'ParamChange3', }, - generatorId: 'RandomSelector', - name: 'ParamChange3' - }]; + ]; } class MockGraphDataService { getGraphData() { return { - links: [{ - source: 'Hola', - target: 'Hola' - }, { - source: 'State2', - target: 'State3' - }, { - source: 'State', - target: 'State' - }, { - source: 'State3', - target: 'State' - }] + links: [ + { + source: 'Hola', + target: 'Hola', + }, + { + source: 'State2', + target: 'State3', + }, + { + source: 'State', + target: 'State', + }, + { + source: 'State3', + target: 'State', + }, + ], }; } } @@ -82,7 +91,7 @@ describe('Parameter Metadata Service', () => { linked_skill_id: null, content: { content_id: 'content', - html: '{{HtmlValue}}' + html: '{{HtmlValue}}', }, recorded_voiceovers: { voiceovers_mapping: { @@ -97,42 +106,44 @@ describe('Parameter Metadata Service', () => { placeholder: { value: { content_id: 'ca_placeholder_2', - unicode_str: '' - } + unicode_str: '', + }, }, rows: { - value: 1 + value: 1, }, catchMisspellings: { - value: false - } + value: false, + }, }, solution: { answer_is_exclusive: true, correct_answer: '1', explanation: { content_id: 'solution_5', - html: '

1

' - } + html: '

1

', + }, }, id: 'TextInput', - answer_groups: [{ - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: '', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_1', - html: '{{FeedbackValue}}' + answer_groups: [ + { + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: '', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: '{{FeedbackValue}}', + }, }, }, - }], + ], default_outcome: { labelled_as_correct: true, param_changes: [], @@ -155,13 +166,13 @@ describe('Parameter Metadata Service', () => { linked_skill_id: null, content: { content_id: 'content', - html: 'content' + html: 'content', }, recorded_voiceovers: { voiceovers_mapping: { content: {}, default_outcome: {}, - } + }, }, param_changes: [], interaction: { @@ -170,42 +181,44 @@ describe('Parameter Metadata Service', () => { placeholder: { value: { content_id: 'ca_placeholder_2', - unicode_str: '' - } + unicode_str: '', + }, }, rows: { - value: 1 + value: 1, }, catchMisspellings: { - value: false - } + value: false, + }, }, solution: { answer_is_exclusive: true, correct_answer: '1', explanation: { content_id: 'solution_5', - html: '

1

' - } + html: '

1

', + }, }, id: 'TextInput', - answer_groups: [{ - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: '', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_1', - html: '{{StateFeedbackValue}}' + answer_groups: [ + { + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: '', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: '{{StateFeedbackValue}}', + }, }, }, - }], + ], default_outcome: { labelled_as_correct: true, param_changes: [], @@ -215,10 +228,10 @@ describe('Parameter Metadata Service', () => { dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: '' + html: '', }, }, - hints: [] + hints: [], }, }, State2: { @@ -228,13 +241,13 @@ describe('Parameter Metadata Service', () => { linked_skill_id: null, content: { content_id: 'content', - html: 'content' + html: 'content', }, recorded_voiceovers: { voiceovers_mapping: { content: {}, default_outcome: {}, - } + }, }, param_changes: [], interaction: { @@ -243,42 +256,44 @@ describe('Parameter Metadata Service', () => { placeholder: { value: { content_id: 'ca_placeholder_2', - unicode_str: '' - } + unicode_str: '', + }, }, rows: { - value: 1 + value: 1, }, catchMisspellings: { - value: false - } + value: false, + }, }, solution: { answer_is_exclusive: true, correct_answer: '1', explanation: { content_id: 'solution_5', - html: '

1

' - } + html: '

1

', + }, }, id: 'TextInput', - answer_groups: [{ - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: '', - dest_if_really_stuck: null, - feedback: { - content_id: '', - html: '' - } - } - }], + answer_groups: [ + { + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: '', + dest_if_really_stuck: null, + feedback: { + content_id: '', + html: '', + }, + }, + }, + ], default_outcome: { labelled_as_correct: true, param_changes: [], @@ -288,10 +303,10 @@ describe('Parameter Metadata Service', () => { dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: '' + html: '', }, }, - hints: [] + hints: [], }, }, State3: { @@ -301,13 +316,13 @@ describe('Parameter Metadata Service', () => { linked_skill_id: null, content: { content_id: 'content', - html: 'content' + html: 'content', }, recorded_voiceovers: { voiceovers_mapping: { content: {}, default_outcome: {}, - } + }, }, param_changes: [], interaction: { @@ -316,42 +331,44 @@ describe('Parameter Metadata Service', () => { placeholder: { value: { content_id: 'ca_placeholder_2', - unicode_str: '' - } + unicode_str: '', + }, }, rows: { - value: 1 + value: 1, }, catchMisspellings: { - value: false - } + value: false, + }, }, solution: { answer_is_exclusive: true, correct_answer: '1', explanation: { content_id: 'solution_5', - html: '

1

' - } + html: '

1

', + }, }, id: 'TextInput', - answer_groups: [{ - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - outcome: { - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: '', - dest_if_really_stuck: null, - feedback: { - content_id: '', - html: '' - } - } - }], + answer_groups: [ + { + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + outcome: { + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: '', + dest_if_really_stuck: null, + feedback: { + content_id: '', + html: '', + }, + }, + }, + ], default_outcome: { labelled_as_correct: true, param_changes: [], @@ -361,12 +378,12 @@ describe('Parameter Metadata Service', () => { dest_if_really_stuck: null, feedback: { content_id: '', - html: '' + html: '', }, }, - hints: [] - } - } + hints: [], + }, + }, }); } } @@ -377,49 +394,56 @@ describe('Parameter Metadata Service', () => { ParameterMetadataService, { provide: ExplorationParamChangesService, - useClass: MockExplorationParamChangesService + useClass: MockExplorationParamChangesService, }, { provide: ExplorationStatesService, - useClass: MockExplorationStatesService + useClass: MockExplorationStatesService, }, { provide: GraphDataService, - useClass: MockGraphDataService - } - ] + useClass: MockGraphDataService, + }, + ], }); parameterMetadataService = TestBed.inject(ParameterMetadataService); statesObjectFactory = TestBed.inject(StatesObjectFactory); }); - it('should get unset parameters info', () => { - expect(parameterMetadataService.getUnsetParametersInfo( - ['Hola', 'State2'])) - .toEqual([{ + expect( + parameterMetadataService.getUnsetParametersInfo(['Hola', 'State2']) + ).toEqual([ + { paramName: 'ParamChange2', - stateName: null - }, { + stateName: null, + }, + { paramName: 'HtmlValue', stateName: 'Hola', - }, { + }, + { paramName: 'FeedbackValue', - stateName: 'Hola' - }, { + stateName: 'Hola', + }, + { paramName: 'StateFeedbackValue', - stateName: 'State' - }]); + stateName: 'State', + }, + ]); - expect(parameterMetadataService.getUnsetParametersInfo( - ['State', 'State3'])) - .toEqual([{ + expect( + parameterMetadataService.getUnsetParametersInfo(['State', 'State3']) + ).toEqual([ + { paramName: 'ParamChange2', - stateName: null - }, { + stateName: null, + }, + { paramName: 'StateFeedbackValue', - stateName: 'State' - }]); + stateName: 'State', + }, + ]); }); }); diff --git a/core/templates/pages/exploration-editor-page/services/parameter-metadata.service.ts b/core/templates/pages/exploration-editor-page/services/parameter-metadata.service.ts index e2715802ae93..9d21255935aa 100644 --- a/core/templates/pages/exploration-editor-page/services/parameter-metadata.service.ts +++ b/core/templates/pages/exploration-editor-page/services/parameter-metadata.service.ts @@ -16,16 +16,16 @@ * @fileoverview Service for computing parameter metadata. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { ParamMetadata } from 'domain/exploration/param-metadata.model'; -import { ExpressionInterpolationService } from 'expressions/expression-interpolation.service'; -import { ExplorationParamChangesService } from 'pages/exploration-editor-page/services/exploration-param-changes.service'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; -import { GraphDataService } from 'pages/exploration-editor-page/services/graph-data.service'; -import { ExplorationEditorPageConstants } from 'pages/exploration-editor-page/exploration-editor-page.constants'; -import { State } from 'domain/state/StateObjectFactory'; -import { ParamChange } from 'domain/exploration/ParamChangeObjectFactory'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {ParamMetadata} from 'domain/exploration/param-metadata.model'; +import {ExpressionInterpolationService} from 'expressions/expression-interpolation.service'; +import {ExplorationParamChangesService} from 'pages/exploration-editor-page/services/exploration-param-changes.service'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; +import {GraphDataService} from 'pages/exploration-editor-page/services/graph-data.service'; +import {ExplorationEditorPageConstants} from 'pages/exploration-editor-page/exploration-editor-page.constants'; +import {State} from 'domain/state/StateObjectFactory'; +import {ParamChange} from 'domain/exploration/ParamChangeObjectFactory'; import cloneDeep from 'lodash/cloneDeep'; interface GetUnsetParametersInfoResult { @@ -34,15 +34,15 @@ interface GetUnsetParametersInfoResult { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ParameterMetadataService { constructor( private expressionInterpolationService: ExpressionInterpolationService, private explorationParamChangesService: ExplorationParamChangesService, private explorationStatesService: ExplorationStatesService, - private graphDataService: GraphDataService, - ) { } + private graphDataService: GraphDataService + ) {} PARAM_SOURCE_ANSWER = 'answer'; PARAM_SOURCE_CONTENT = 'content'; @@ -55,28 +55,48 @@ export class ParameterMetadataService { let pc = paramChanges[i]; if (pc.generatorId === 'Copier') { if (!pc.customizationArgs.parse_with_jinja) { - result.push(ParamMetadata.createWithSetAction( - pc.name, this.PARAM_SOURCE_PARAM_CHANGES, String(i))); + result.push( + ParamMetadata.createWithSetAction( + pc.name, + this.PARAM_SOURCE_PARAM_CHANGES, + String(i) + ) + ); } else { const customizationArgsValue = pc.customizationArgs.value; if (customizationArgsValue) { - let paramsReferenced = ( + let paramsReferenced = this.expressionInterpolationService.getParamsFromString( - customizationArgsValue)); + customizationArgsValue + ); for (let j = 0; j < paramsReferenced.length; j++) { - result.push(ParamMetadata.createWithGetAction( - paramsReferenced[j], - this.PARAM_SOURCE_PARAM_CHANGES, String(i))); + result.push( + ParamMetadata.createWithGetAction( + paramsReferenced[j], + this.PARAM_SOURCE_PARAM_CHANGES, + String(i) + ) + ); } - result.push(ParamMetadata.createWithSetAction( - pc.name, this.PARAM_SOURCE_PARAM_CHANGES, String(i))); + result.push( + ParamMetadata.createWithSetAction( + pc.name, + this.PARAM_SOURCE_PARAM_CHANGES, + String(i) + ) + ); } } } else { // RandomSelector. Elements in the list of possibilities are treated // as raw unicode strings, not expressions. - result.push(ParamMetadata.createWithSetAction( - pc.name, this.PARAM_SOURCE_PARAM_CHANGES, String(i))); + result.push( + ParamMetadata.createWithSetAction( + pc.name, + this.PARAM_SOURCE_PARAM_CHANGES, + String(i) + ) + ); } } @@ -93,23 +113,36 @@ export class ParameterMetadataService { let result = this.getMetadataFromParamChanges(state.paramChanges); // Next, the content is evaluated. - this.expressionInterpolationService.getParamsFromString( - state.content.html).forEach((paramName, index) => { - result.push(ParamMetadata.createWithGetAction( - paramName, this.PARAM_SOURCE_CONTENT, '0')); - }); + this.expressionInterpolationService + .getParamsFromString(state.content.html) + .forEach((paramName, index) => { + result.push( + ParamMetadata.createWithGetAction( + paramName, + this.PARAM_SOURCE_CONTENT, + '0' + ) + ); + }); // Next, the answer is received. - result.push(ParamMetadata.createWithSetAction( - 'answer', this.PARAM_SOURCE_ANSWER, '0')); + result.push( + ParamMetadata.createWithSetAction('answer', this.PARAM_SOURCE_ANSWER, '0') + ); // Finally, the rule feedback strings are evaluated. - state.interaction.answerGroups.forEach((group) => { - this.expressionInterpolationService.getParamsFromString( - group.outcome.feedback.html).forEach((paramName, index) => { - result.push(ParamMetadata.createWithGetAction( - paramName, this.PARAM_SOURCE_FEEDBACK, String(index))); - }); + state.interaction.answerGroups.forEach(group => { + this.expressionInterpolationService + .getParamsFromString(group.outcome.feedback.html) + .forEach((paramName, index) => { + result.push( + ParamMetadata.createWithGetAction( + paramName, + this.PARAM_SOURCE_FEEDBACK, + String(index) + ) + ); + }); }); return result; @@ -119,7 +152,9 @@ export class ParameterMetadataService { // whether this parameter is not used at all in this state, or // whether its first occurrence is a 'set' or 'get'. getParamStatus( - stateParamMetadata: ParamMetadata[], paramName: string): null | string { + stateParamMetadata: ParamMetadata[], + paramName: string + ): null | string { for (let i = 0; i < stateParamMetadata.length; i++) { if (stateParamMetadata[i].paramName === paramName) { return stateParamMetadata[i].action; @@ -138,7 +173,8 @@ export class ParameterMetadataService { // (e.g. one parameter may be set based on the value assigned to // another parameter). getUnsetParametersInfo( - initNodeIds: string[]): GetUnsetParametersInfoResult[] { + initNodeIds: string[] + ): GetUnsetParametersInfoResult[] { let graphData = this.graphDataService.getGraphData(); let states = this.explorationStatesService.getStates(); @@ -146,18 +182,20 @@ export class ParameterMetadataService { // Determine all parameter names that are used within this exploration. let allParamNames: string[] = []; let expParamChangesMetadata = this.getMetadataFromParamChanges( - this.explorationParamChangesService.savedMemento as ParamChange[]); + this.explorationParamChangesService.savedMemento as ParamChange[] + ); let stateParamMetadatas: Record = {}; - expParamChangesMetadata.forEach((expParamMetadataItem) => { + expParamChangesMetadata.forEach(expParamMetadataItem => { if (allParamNames.indexOf(expParamMetadataItem.paramName) === -1) { allParamNames.push(expParamMetadataItem.paramName); } }); - states.getStateNames().forEach((stateName) => { + states.getStateNames().forEach(stateName => { stateParamMetadatas[stateName] = this.getStateParamMetadata( - states.getState(stateName)); + states.getState(stateName) + ); for (let i = 0; i < stateParamMetadatas[stateName].length; i++) { let pName = stateParamMetadatas[stateName][i].paramName; if (allParamNames.indexOf(pName) === -1) { @@ -176,16 +214,20 @@ export class ParameterMetadataService { let tmpUnsetParameter = null; let paramStatusAtOutset = this.getParamStatus( - expParamChangesMetadata, paramName); - if (paramStatusAtOutset === - ExplorationEditorPageConstants.PARAM_ACTION_GET) { + expParamChangesMetadata, + paramName + ); + if ( + paramStatusAtOutset === ExplorationEditorPageConstants.PARAM_ACTION_GET + ) { unsetParametersInfo.push({ paramName: paramName, - stateName: null + stateName: null, }); continue; - } else if (paramStatusAtOutset === - ExplorationEditorPageConstants.PARAM_ACTION_SET) { + } else if ( + paramStatusAtOutset === ExplorationEditorPageConstants.PARAM_ACTION_SET + ) { // This parameter will remain set for the entirety of the // exploration. continue; @@ -196,12 +238,13 @@ export class ParameterMetadataService { for (let i = 0; i < initNodeIds.length; i++) { seen[initNodeIds[i]] = true; let paramStatus = this.getParamStatus( - stateParamMetadatas[initNodeIds[i]], paramName); - if (paramStatus === - ExplorationEditorPageConstants.PARAM_ACTION_GET) { + stateParamMetadatas[initNodeIds[i]], + paramName + ); + if (paramStatus === ExplorationEditorPageConstants.PARAM_ACTION_GET) { tmpUnsetParameter = { paramName: paramName, - stateName: initNodeIds[i] + stateName: initNodeIds[i], }; break; } else if (!paramStatus) { @@ -218,16 +261,18 @@ export class ParameterMetadataService { let currNodeId = queue.shift(); for (let edgeInd = 0; edgeInd < graphData.links.length; edgeInd++) { let edge = graphData.links[edgeInd]; - if (edge.source === currNodeId && - !seen.hasOwnProperty(edge.target)) { + if (edge.source === currNodeId && !seen.hasOwnProperty(edge.target)) { seen[edge.target] = true; let paramStatus = this.getParamStatus( - stateParamMetadatas[edge.target], paramName); - if (paramStatus === - ExplorationEditorPageConstants.PARAM_ACTION_GET) { + stateParamMetadatas[edge.target], + paramName + ); + if ( + paramStatus === ExplorationEditorPageConstants.PARAM_ACTION_GET + ) { tmpUnsetParameter = { paramName: paramName, - stateName: edge.target + stateName: edge.target, }; break; } else if (!paramStatus) { @@ -246,6 +291,9 @@ export class ParameterMetadataService { } } -angular.module('oppia').factory( - 'ParameterMetadataService', - downgradeInjectable(ParameterMetadataService)); +angular + .module('oppia') + .factory( + 'ParameterMetadataService', + downgradeInjectable(ParameterMetadataService) + ); diff --git a/core/templates/pages/exploration-editor-page/services/populate-rule-content-ids.service.spec.ts b/core/templates/pages/exploration-editor-page/services/populate-rule-content-ids.service.spec.ts index 5d34433a15ab..c5d5246d6163 100644 --- a/core/templates/pages/exploration-editor-page/services/populate-rule-content-ids.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/populate-rule-content-ids.service.spec.ts @@ -14,15 +14,14 @@ /** * @fileoverview Unit tests for PopulateRuleContentIdsService. -*/ + */ -import { TestBed } from '@angular/core/testing'; -import { Rule } from 'domain/exploration/rule.model'; -import { TranslatableSetOfNormalizedString } from 'interactions/rule-input-defs'; +import {TestBed} from '@angular/core/testing'; +import {Rule} from 'domain/exploration/rule.model'; +import {TranslatableSetOfNormalizedString} from 'interactions/rule-input-defs'; -import { PopulateRuleContentIdsService } from - 'pages/exploration-editor-page/services/populate-rule-content-ids.service'; -import { GenerateContentIdService } from 'services/generate-content-id.service'; +import {PopulateRuleContentIdsService} from 'pages/exploration-editor-page/services/populate-rule-content-ids.service'; +import {GenerateContentIdService} from 'services/generate-content-id.service'; describe('Populate Rule Content Ids Service', () => { let populateRuleContentIdsService: PopulateRuleContentIdsService; @@ -31,17 +30,22 @@ describe('Populate Rule Content Ids Service', () => { beforeEach(() => { populateRuleContentIdsService = TestBed.get(PopulateRuleContentIdsService); generateContentIdService = TestBed.inject(GenerateContentIdService); - generateContentIdService.init(() => 0, () => {}); + generateContentIdService.init( + () => 0, + () => {} + ); }); it('should populate null content ids on save', () => { let rule = new Rule( - 'Equals', { + 'Equals', + { x: { contentId: null, - normalizedStrSet: [] - } - }, { x: 'TranslatableSetOfNormalizedString' }, + normalizedStrSet: [], + }, + }, + {x: 'TranslatableSetOfNormalizedString'} ); let content = rule.inputs.x as TranslatableSetOfNormalizedString; expect(content.contentId).toBeNull(); @@ -52,10 +56,13 @@ describe('Populate Rule Content Ids Service', () => { it('should not populate non-null content ids on save', () => { const ruleInput = { contentId: 'rule_input', - normalizedStrSet: [] + normalizedStrSet: [], }; let rule = new Rule( - 'Equals', { x: ruleInput }, { x: 'TranslatableSetOfNormalizedString' }); + 'Equals', + {x: ruleInput}, + {x: 'TranslatableSetOfNormalizedString'} + ); let content = rule.inputs.x as TranslatableSetOfNormalizedString; populateRuleContentIdsService.populateNullRuleContentIds(rule); @@ -64,7 +71,10 @@ describe('Populate Rule Content Ids Service', () => { it('should not populate content ids if input does not need one', () => { let rule = new Rule( - 'HasNumberOfTermsEqualTo', { y: 1 }, { y: 'NonnegativeInt' }); + 'HasNumberOfTermsEqualTo', + {y: 1}, + {y: 'NonnegativeInt'} + ); populateRuleContentIdsService.populateNullRuleContentIds(rule); expect(rule.inputs).toEqual({y: 1}); diff --git a/core/templates/pages/exploration-editor-page/services/populate-rule-content-ids.service.ts b/core/templates/pages/exploration-editor-page/services/populate-rule-content-ids.service.ts index 29e0685ce4d8..d2f43a8a60f8 100644 --- a/core/templates/pages/exploration-editor-page/services/populate-rule-content-ids.service.ts +++ b/core/templates/pages/exploration-editor-page/services/populate-rule-content-ids.service.ts @@ -16,16 +16,16 @@ * @fileoverview Service that populates rule content ids. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { BaseTranslatableObject } from 'interactions/rule-input-defs'; -import { GenerateContentIdService } from 'services/generate-content-id.service'; -import { Rule } from 'domain/exploration/rule.model'; +import {AppConstants} from 'app.constants'; +import {BaseTranslatableObject} from 'interactions/rule-input-defs'; +import {GenerateContentIdService} from 'services/generate-content-id.service'; +import {Rule} from 'domain/exploration/rule.model'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class PopulateRuleContentIdsService { constructor(private generateContentIdService: GenerateContentIdService) {} @@ -50,22 +50,24 @@ export class PopulateRuleContentIdsService { // All rules input types which are translatable are subclasses of // BaseTranslatableObject having dict structure with contentId // as a key. - const hasContentId = ( - ruleInput && ruleInput.hasOwnProperty('contentId')); + const hasContentId = ruleInput && ruleInput.hasOwnProperty('contentId'); if (!hasContentId) { return; } const needsContentId = ruleInput.contentId === null; if (needsContentId) { - ruleInput.contentId = ( - this.generateContentIdService.getNextStateId( - `${AppConstants.COMPONENT_NAME_RULE_INPUT}`)); + ruleInput.contentId = this.generateContentIdService.getNextStateId( + `${AppConstants.COMPONENT_NAME_RULE_INPUT}` + ); } }); } } -angular.module('oppia').factory( - 'PopulateRuleContentIdsService', - downgradeInjectable(PopulateRuleContentIdsService)); +angular + .module('oppia') + .factory( + 'PopulateRuleContentIdsService', + downgradeInjectable(PopulateRuleContentIdsService) + ); diff --git a/core/templates/pages/exploration-editor-page/services/router.service.spec.ts b/core/templates/pages/exploration-editor-page/services/router.service.spec.ts index 7920e8d7b13c..242fedaef8ee 100644 --- a/core/templates/pages/exploration-editor-page/services/router.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/router.service.spec.ts @@ -16,19 +16,25 @@ * @fileoverview Unit tests for RouterService. */ -import { RouterService } from './router.service'; -import { Subscription } from 'rxjs'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { discardPeriodicTasks, fakeAsync, flush, TestBed, tick } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; +import {RouterService} from './router.service'; +import {Subscription} from 'rxjs'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import { + discardPeriodicTasks, + fakeAsync, + flush, + TestBed, + tick, +} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; import $ from 'jquery'; -import { ContextService } from 'services/context.service'; -import { ExplorationImprovementsService } from 'services/exploration-improvements.service'; -import { ExplorationStatesService } from './exploration-states.service'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { ExplorationInitStateNameService } from './exploration-init-state-name.service'; -import { TranslationLanguageService } from 'pages/exploration-editor-page/translation-tab/services/translation-language.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import {ContextService} from 'services/context.service'; +import {ExplorationImprovementsService} from 'services/exploration-improvements.service'; +import {ExplorationStatesService} from './exploration-states.service'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {ExplorationInitStateNameService} from './exploration-init-state-name.service'; +import {TranslationLanguageService} from 'pages/exploration-editor-page/translation-tab/services/translation-language.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; class MockContextService { getExplorationId() { @@ -57,7 +63,7 @@ describe('Router Service', () => { let hasStateSpy: jasmine.Spy; let isInitializedSpy: jasmine.Spy; - beforeEach((() => { + beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [ @@ -65,7 +71,7 @@ describe('Router Service', () => { WindowRef, { provide: NgbActiveModal, - useClass: MockActiveModal + useClass: MockActiveModal, }, ExplorationImprovementsService, ExplorationStatesService, @@ -73,32 +79,31 @@ describe('Router Service', () => { TranslationLanguageService, { provide: ContextService, - useClass: MockContextService + useClass: MockContextService, }, { provide: ExplorationInitStateNameService, - useClass: MockExplorationInitStateNameService - } - ] + useClass: MockExplorationInitStateNameService, + }, + ], }); routerService = TestBed.inject(RouterService); explorationImprovementsService = TestBed.inject( - ExplorationImprovementsService); - explorationStatesService = TestBed.inject( - ExplorationStatesService); + ExplorationImprovementsService + ); + explorationStatesService = TestBed.inject(ExplorationStatesService); windowRef = TestBed.inject(WindowRef); stateEditorService = TestBed.inject(StateEditorService); translationLanguageService = TestBed.inject(TranslationLanguageService); - })); + }); beforeEach(() => { isInitializedSpy = spyOn(explorationStatesService, 'isInitialized'); isInitializedSpy.and.returnValue(true); hasStateSpy = spyOn(explorationStatesService, 'hasState'); hasStateSpy.and.returnValue(true); - spyOn(stateEditorService, 'getActiveStateName') - .and.returnValue(null); + spyOn(stateEditorService, 'getActiveStateName').and.returnValue(null); testSubscriptions = new Subscription(); routerService.navigateToMainTab('first card'); @@ -109,13 +114,14 @@ describe('Router Service', () => { it('should not navigate to main tab when already there', fakeAsync(() => { let jQuerySpy = spyOn(window, '$'); - jQuerySpy.withArgs('.oppia-editor-cards-container').and.returnValue( - $(document.createElement('div'))); + jQuerySpy + .withArgs('.oppia-editor-cards-container') + .and.returnValue($(document.createElement('div'))); jQuerySpy.and.callThrough(); - spyOn($.fn, 'fadeOut').and.callFake((cb) => { + spyOn($.fn, 'fadeOut').and.callFake(cb => { cb(); - setTimeout(() => {},); + setTimeout(() => {}); return null; }); @@ -127,14 +133,12 @@ describe('Router Service', () => { tick(300); - expect(routerService.getActiveTabName()).toBe('main'); routerService.navigateToMainTab('first card'); tick(300); - expect(routerService.getActiveTabName()).toBe('main'); flush(); @@ -180,7 +184,7 @@ describe('Router Service', () => { discardPeriodicTasks(); })); - it('should navigate to translation tab', fakeAsync(()=>{ + it('should navigate to translation tab', fakeAsync(() => { window.location.hash = '/translation/Start/ca_buttonText_6'; routerService._changeTab('/translation/Start/ca_buttonText_6'); @@ -189,16 +193,14 @@ describe('Router Service', () => { expect(stateEditorService.getInitActiveContentId()).toBe('ca_buttonText_6'); })); - it('should navigate to translation tab with correct voiceover language', - fakeAsync(()=>{ - window.location.hash = '/translation/Start/ca_buttonText_6/ak'; - routerService._changeTab('/translation/Start/ca_buttonText_6/ak'); + it('should navigate to translation tab with correct voiceover language', fakeAsync(() => { + window.location.hash = '/translation/Start/ca_buttonText_6/ak'; + routerService._changeTab('/translation/Start/ca_buttonText_6/ak'); - tick(300); + tick(300); - expect( - translationLanguageService.getActiveLanguageCode()).toBe('ak'); - })); + expect(translationLanguageService.getActiveLanguageCode()).toBe('ak'); + })); it('should navigate to preview tab', fakeAsync(() => { expect(routerService.getActiveTabName()).toBe('main'); @@ -240,37 +242,39 @@ describe('Router Service', () => { discardPeriodicTasks(); })); - it('should navigate to main tab if improvements tab is not enabled', - fakeAsync(() => { - spyOn(explorationImprovementsService, 'isImprovementsTabEnabledAsync') - .and.returnValue(Promise.resolve(false)); + it('should navigate to main tab if improvements tab is not enabled', fakeAsync(() => { + spyOn( + explorationImprovementsService, + 'isImprovementsTabEnabledAsync' + ).and.returnValue(Promise.resolve(false)); - expect(routerService.getActiveTabName()).toBe('main'); + expect(routerService.getActiveTabName()).toBe('main'); - routerService.navigateToImprovementsTab(); - tick(300); + routerService.navigateToImprovementsTab(); + tick(300); - expect(routerService.getActiveTabName()).toBe('main'); + expect(routerService.getActiveTabName()).toBe('main'); - flush(); - discardPeriodicTasks(); - })); + flush(); + discardPeriodicTasks(); + })); - it('should navigate to improvements tab is not enabled', - fakeAsync(() => { - spyOn(explorationImprovementsService, 'isImprovementsTabEnabledAsync') - .and.returnValue(Promise.resolve(true)); + it('should navigate to improvements tab is not enabled', fakeAsync(() => { + spyOn( + explorationImprovementsService, + 'isImprovementsTabEnabledAsync' + ).and.returnValue(Promise.resolve(true)); - expect(routerService.getActiveTabName()).toBe('main'); - routerService.navigateToImprovementsTab(); + expect(routerService.getActiveTabName()).toBe('main'); + routerService.navigateToImprovementsTab(); - tick(300); + tick(300); - expect(routerService.getActiveTabName()).toBe('improvements'); + expect(routerService.getActiveTabName()).toBe('improvements'); - flush(); - discardPeriodicTasks(); - })); + flush(); + discardPeriodicTasks(); + })); it('should navigate to history tab', fakeAsync(() => { expect(routerService.getActiveTabName()).toBe('main'); @@ -310,45 +314,42 @@ describe('Router Service', () => { })); it('should tell isLocationSetToNonStateEditorTab', fakeAsync(() => { - spyOnProperty(windowRef, 'nativeWindow') - .and.returnValue({ - location: { - hash: '#/settings' - } - }); + spyOnProperty(windowRef, 'nativeWindow').and.returnValue({ + location: { + hash: '#/settings', + }, + }); - expect(routerService.isLocationSetToNonStateEditorTab()) - .toBeTrue(); + expect(routerService.isLocationSetToNonStateEditorTab()).toBeTrue(); flush(); discardPeriodicTasks(); })); - it('should tell current State From Location Path to be null', - fakeAsync(() => { - spyOnProperty(windowRef, 'nativeWindow') - .and.returnValue({ - location: { - hash: '#/gui/Introduction' - } - }); + it('should tell current State From Location Path to be null', fakeAsync(() => { + spyOnProperty(windowRef, 'nativeWindow').and.returnValue({ + location: { + hash: '#/gui/Introduction', + }, + }); - routerService.savePendingChanges(); - expect(routerService.getCurrentStateFromLocationPath()) - .toBe('/Introduction'); + routerService.savePendingChanges(); + expect(routerService.getCurrentStateFromLocationPath()).toBe( + '/Introduction' + ); - routerService.navigateToMainTab('/Introduction'); - flush(); - discardPeriodicTasks(); - })); + routerService.navigateToMainTab('/Introduction'); + flush(); + discardPeriodicTasks(); + })); it('should not navigate to main tab', () => { - spyOn(routerService, '_getCurrentStateFromLocationPath') - .and.returnValue('/main'); + spyOn(routerService, '_getCurrentStateFromLocationPath').and.returnValue( + '/main' + ); routerService.navigateToMainTab('main'); - expect(routerService._getCurrentStateFromLocationPath) - .toHaveBeenCalled(); + expect(routerService._getCurrentStateFromLocationPath).toHaveBeenCalled(); }); }); diff --git a/core/templates/pages/exploration-editor-page/services/router.service.ts b/core/templates/pages/exploration-editor-page/services/router.service.ts index 404b153e1051..3002969afb54 100644 --- a/core/templates/pages/exploration-editor-page/services/router.service.ts +++ b/core/templates/pages/exploration-editor-page/services/router.service.ts @@ -16,31 +16,31 @@ * @fileoverview Service that handles routing for the exploration editor page. */ -import { PlatformLocation } from '@angular/common'; -import { Injectable, EventEmitter, NgZone } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { ExplorationInitStateNameService } from 'pages/exploration-editor-page/services/exploration-init-state-name.service'; -import { StateEditorRefreshService } from 'pages/exploration-editor-page/services/state-editor-refresh.service'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; -import { ExplorationImprovementsService } from 'services/exploration-improvements.service'; -import { ExternalSaveService } from 'services/external-save.service'; -import { TranslationLanguageService } from 'pages/exploration-editor-page/translation-tab/services/translation-language.service'; +import {PlatformLocation} from '@angular/common'; +import {Injectable, EventEmitter, NgZone} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {ExplorationInitStateNameService} from 'pages/exploration-editor-page/services/exploration-init-state-name.service'; +import {StateEditorRefreshService} from 'pages/exploration-editor-page/services/state-editor-refresh.service'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; +import {ExplorationImprovementsService} from 'services/exploration-improvements.service'; +import {ExternalSaveService} from 'services/external-save.service'; +import {TranslationLanguageService} from 'pages/exploration-editor-page/translation-tab/services/translation-language.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class RouterService { TABS = { - MAIN: { name: 'main', path: '/main' }, - TRANSLATION: { name: 'translation', path: '/translation' }, - PREVIEW: { name: 'preview', path: '/preview' }, - SETTINGS: { name: 'settings', path: '/settings' }, - STATS: { name: 'stats', path: '/stats' }, - IMPROVEMENTS: { name: 'improvements', path: '/improvements' }, - HISTORY: { name: 'history', path: '/history' }, - FEEDBACK: { name: 'feedback', path: '/feedback' }, + MAIN: {name: 'main', path: '/main'}, + TRANSLATION: {name: 'translation', path: '/translation'}, + PREVIEW: {name: 'preview', path: '/preview'}, + SETTINGS: {name: 'settings', path: '/settings'}, + STATS: {name: 'stats', path: '/stats'}, + IMPROVEMENTS: {name: 'improvements', path: '/improvements'}, + HISTORY: {name: 'history', path: '/history'}, + FEEDBACK: {name: 'feedback', path: '/feedback'}, }; /** @private */ @@ -91,8 +91,9 @@ export class RouterService { if (newPath.indexOf(this.TABS.TRANSLATION.path) === 0) { this._activeTabName = this.TABS.TRANSLATION.name; - const [stateName, contentId, languageCode] = newPath.substring( - this.TABS.TRANSLATION.path.length + 1).split('/'); + const [stateName, contentId, languageCode] = newPath + .substring(this.TABS.TRANSLATION.path.length + 1) + .split('/'); if (stateName) { this.stateEditorService.setActiveStateName(stateName); } @@ -100,11 +101,10 @@ export class RouterService { this.stateEditorService.setInitActiveContentId(contentId); } if (languageCode) { - this.translationLanguagesService.setActiveLanguageCode( - languageCode); + this.translationLanguagesService.setActiveLanguageCode(languageCode); } - this.windowRef.nativeWindow.location.hash = ( - this.TABS.TRANSLATION.path + '/' + stateName); + this.windowRef.nativeWindow.location.hash = + this.TABS.TRANSLATION.path + '/' + stateName; this.refreshTranslationTabEventEmitter.emit(); this.ngZone.runOutsideAngular(() => { let waitForStatesToLoad = setInterval(() => { @@ -113,7 +113,8 @@ export class RouterService { clearInterval(waitForStatesToLoad); if (!this.stateEditorService.getActiveStateName()) { this.stateEditorService.setActiveStateName( - this.explorationInitStateNameService.savedMemento); + this.explorationInitStateNameService.savedMemento + ); } this.refreshTranslationTabEventEmitter.emit(); } @@ -135,8 +136,10 @@ export class RouterService { Promise.resolve( this.explorationImprovementsService.isImprovementsTabEnabledAsync() ).then(improvementsTabIsEnabled => { - if (this._activeTabName === this.TABS.IMPROVEMENTS.name && - !improvementsTabIsEnabled) { + if ( + this._activeTabName === this.TABS.IMPROVEMENTS.name && + !improvementsTabIsEnabled + ) { // Redirect to the main tab. this._actuallyNavigate(this.SLUG_GUI, null); } @@ -144,7 +147,7 @@ export class RouterService { } else if (newPath === this.TABS.HISTORY.path) { // TODO(sll): Do this on-hover rather than on-click. this.refreshVersionHistoryEventEmitter.emit({ - forceRefresh: false + forceRefresh: false, }); this._activeTabName = this.TABS.HISTORY.name; } else if (newPath === this.TABS.FEEDBACK.path) { @@ -155,7 +158,8 @@ export class RouterService { } else { if (this.explorationInitStateNameService.savedMemento) { this._changeTab( - '/gui/' + this.explorationInitStateNameService.savedMemento); + '/gui/' + this.explorationInitStateNameService.savedMemento + ); } } @@ -179,14 +183,17 @@ export class RouterService { // navigated to a different tab before the states finish loading. // In such a case, we should not switch back to the editor main // tab. - if (pathType === this.SLUG_GUI && - this._activeTabName === this.TABS.MAIN.name) { + if ( + pathType === this.SLUG_GUI && + this._activeTabName === this.TABS.MAIN.name + ) { this.windowRef.nativeWindow.location.hash = path; this.stateEditorRefreshService.onRefreshStateEditor.emit(); } } else { this._changeTab( - pathBase + this.explorationInitStateNameService.savedMemento); + pathBase + this.explorationInitStateNameService.savedMemento + ); } } }); @@ -214,7 +221,8 @@ export class RouterService { } this._changeTab( - '/' + pathType + '/' + this.stateEditorService.getActiveStateName()); + '/' + pathType + '/' + this.stateEditorService.getActiveStateName() + ); this.windowRef.nativeWindow.scrollTo(0, 0); } @@ -227,12 +235,11 @@ export class RouterService { } isLocationSetToNonStateEditorTab(): boolean { - let currentPath = ( + let currentPath = '/' + - ( - this.windowRef.nativeWindow.location.hash?. - split('#')[1]?.split('/')[1]) ?? - ''); + this.windowRef.nativeWindow.location.hash + ?.split('#')[1] + ?.split('/')[1] ?? ''; return ( currentPath === this.TABS.MAIN.path || @@ -242,7 +249,8 @@ export class RouterService { currentPath === this.TABS.IMPROVEMENTS.path || currentPath === this.TABS.SETTINGS.path || currentPath === this.TABS.HISTORY.path || - currentPath === this.TABS.FEEDBACK.path); + currentPath === this.TABS.FEEDBACK.path + ); } getCurrentStateFromLocationPath(): string | null { @@ -251,10 +259,9 @@ export class RouterService { navigateToMainTab(stateName: string | null): void { this._savePendingChanges(); - let oldState = decodeURI( - this._getCurrentStateFromLocationPath()); + let oldState = decodeURI(this._getCurrentStateFromLocationPath()); - if (oldState === ('/' + stateName)) { + if (oldState === '/' + stateName) { return; } @@ -339,5 +346,6 @@ export class RouterService { } } -angular.module('oppia').factory('RouterService', - downgradeInjectable(RouterService)); +angular + .module('oppia') + .factory('RouterService', downgradeInjectable(RouterService)); diff --git a/core/templates/pages/exploration-editor-page/services/setting-tab-backend-api.service.spec.ts b/core/templates/pages/exploration-editor-page/services/setting-tab-backend-api.service.spec.ts index 664b9839845c..da03d5243874 100644 --- a/core/templates/pages/exploration-editor-page/services/setting-tab-backend-api.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/setting-tab-backend-api.service.spec.ts @@ -16,10 +16,17 @@ * @fileoverview Unit tests for Backend api service for Setting tab. */ -import { HttpClientTestingModule, HttpTestingController } from - '@angular/common/http/testing'; -import { fakeAsync, flushMicrotasks, TestBed, waitForAsync} from '@angular/core/testing'; -import { SettingTabBackendApiService } from './setting-tab-backend-api.service'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import { + fakeAsync, + flushMicrotasks, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import {SettingTabBackendApiService} from './setting-tab-backend-api.service'; describe('History Tab Backend Api Service', () => { let service: SettingTabBackendApiService; @@ -30,7 +37,7 @@ describe('History Tab Backend Api Service', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [SettingTabBackendApiService] + providers: [SettingTabBackendApiService], }); httpTestingController = TestBed.inject(HttpTestingController); service = TestBed.inject(SettingTabBackendApiService); @@ -40,21 +47,17 @@ describe('History Tab Backend Api Service', () => { httpTestingController.verify(); }); - it('should get setting tab data when getData is called', - fakeAsync(() => { - let moderatorEmailDraftUrl = 'check'; - service.getData( - moderatorEmailDraftUrl - ).then(successHandler, failHandler); + it('should get setting tab data when getData is called', fakeAsync(() => { + let moderatorEmailDraftUrl = 'check'; + service.getData(moderatorEmailDraftUrl).then(successHandler, failHandler); - let req = httpTestingController.expectOne('check'); - expect(req.request.method).toEqual('GET'); - req.flush([]); + let req = httpTestingController.expectOne('check'); + expect(req.request.method).toEqual('GET'); + req.flush([]); - flushMicrotasks(); + flushMicrotasks(); - expect(successHandler).toHaveBeenCalled(); - expect(failHandler).not.toHaveBeenCalled(); - }) - ); + expect(successHandler).toHaveBeenCalled(); + expect(failHandler).not.toHaveBeenCalled(); + })); }); diff --git a/core/templates/pages/exploration-editor-page/services/setting-tab-backend-api.service.ts b/core/templates/pages/exploration-editor-page/services/setting-tab-backend-api.service.ts index 0f634495cfa5..82667a535c7e 100644 --- a/core/templates/pages/exploration-editor-page/services/setting-tab-backend-api.service.ts +++ b/core/templates/pages/exploration-editor-page/services/setting-tab-backend-api.service.ts @@ -16,12 +16,12 @@ * @fileoverview Backend api service for Setting tab; */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; interface SettingTabBackendDict { - 'draft_email_body': string; + draft_email_body: string; } export interface SettingTabResponse { @@ -29,7 +29,7 @@ export interface SettingTabResponse { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class SettingTabBackendApiService { constructor(private http: HttpClient) {} @@ -39,6 +39,9 @@ export class SettingTabBackendApiService { } } -angular.module('oppia').factory( - 'SettingTabBackendApiService', - downgradeInjectable(SettingTabBackendApiService)); +angular + .module('oppia') + .factory( + 'SettingTabBackendApiService', + downgradeInjectable(SettingTabBackendApiService) + ); diff --git a/core/templates/pages/exploration-editor-page/services/state-editor-refresh.service.spec.ts b/core/templates/pages/exploration-editor-page/services/state-editor-refresh.service.spec.ts index 3162a3970562..1026cc18fc7f 100644 --- a/core/templates/pages/exploration-editor-page/services/state-editor-refresh.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/state-editor-refresh.service.spec.ts @@ -14,13 +14,12 @@ /** * @fileoverview Unit tests for StateEditorRefreshService -*/ + */ -import { EventEmitter } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; +import {EventEmitter} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; -import { StateEditorRefreshService } from - 'pages/exploration-editor-page/services/state-editor-refresh.service'; +import {StateEditorRefreshService} from 'pages/exploration-editor-page/services/state-editor-refresh.service'; describe('State Editor Refresh Service', () => { let stateEditorRefreshService: StateEditorRefreshService; @@ -32,6 +31,7 @@ describe('State Editor Refresh Service', () => { it('should fetch refreshStateEditor event emitter', () => { let sampleRefreshStateEditorEventEmitter = new EventEmitter(); expect(stateEditorRefreshService.onRefreshStateEditor).toEqual( - sampleRefreshStateEditorEventEmitter); + sampleRefreshStateEditorEventEmitter + ); }); }); diff --git a/core/templates/pages/exploration-editor-page/services/state-editor-refresh.service.ts b/core/templates/pages/exploration-editor-page/services/state-editor-refresh.service.ts index 373c36ef0c73..f1b2b4898415 100644 --- a/core/templates/pages/exploration-editor-page/services/state-editor-refresh.service.ts +++ b/core/templates/pages/exploration-editor-page/services/state-editor-refresh.service.ts @@ -17,11 +17,11 @@ * refreshing state editor in exploration editor page */ -import { Injectable, EventEmitter } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {Injectable, EventEmitter} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class StateEditorRefreshService { private _refreshStateEditorEventEmitter = new EventEmitter(); @@ -31,5 +31,9 @@ export class StateEditorRefreshService { } } -angular.module('oppia').factory('StateEditorRefreshService', - downgradeInjectable(StateEditorRefreshService)); +angular + .module('oppia') + .factory( + 'StateEditorRefreshService', + downgradeInjectable(StateEditorRefreshService) + ); diff --git a/core/templates/pages/exploration-editor-page/services/state-tutorial-first-time.service.spec.ts b/core/templates/pages/exploration-editor-page/services/state-tutorial-first-time.service.spec.ts index 39d0448dd88d..db54769b6923 100644 --- a/core/templates/pages/exploration-editor-page/services/state-tutorial-first-time.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/state-tutorial-first-time.service.spec.ts @@ -14,15 +14,15 @@ /** * @fileoverview Unit tests for StateTutorialFirstTimeService -*/ + */ -import { EventEmitter } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; +import {EventEmitter} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; -import { EditorFirstTimeEventsService } from 'pages/exploration-editor-page/services/editor-first-time-events.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { StateTutorialFirstTimeService } from 'pages/exploration-editor-page/services/state-tutorial-first-time.service'; -import { TutorialEventsBackendApiService } from 'pages/exploration-editor-page/services/tutorial-events-backend-api.service'; +import {EditorFirstTimeEventsService} from 'pages/exploration-editor-page/services/editor-first-time-events.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {StateTutorialFirstTimeService} from 'pages/exploration-editor-page/services/state-tutorial-first-time.service'; +import {TutorialEventsBackendApiService} from 'pages/exploration-editor-page/services/tutorial-events-backend-api.service'; describe('State Tutorial First Time Service', () => { let stft: StateTutorialFirstTimeService; @@ -41,66 +41,62 @@ describe('State Tutorial First Time Service', () => { }); it('should check the initialisation of the EventEmitters', () => { - expect(stft.onEnterEditorForTheFirstTime).toEqual( - mockEmitter); - expect(stft.onEnterTranslationForTheFirstTime).toEqual( - mockEmitter); + expect(stft.onEnterEditorForTheFirstTime).toEqual(mockEmitter); + expect(stft.onEnterTranslationForTheFirstTime).toEqual(mockEmitter); }); it('should fetch enterEditorForTheFirstTime EventEmitter', () => { - expect(stft.onEnterEditorForTheFirstTime).toEqual( - mockEmitter); + expect(stft.onEnterEditorForTheFirstTime).toEqual(mockEmitter); }); it('should fetch enterTranslationForTheFirstTime EventEmitter', () => { - expect(stft.onEnterTranslationForTheFirstTime).toEqual( - mockEmitter); + expect(stft.onEnterTranslationForTheFirstTime).toEqual(mockEmitter); }); it('should fetch openEditorTutorial EventEmitter', () => { - expect(stft.onOpenEditorTutorial).toEqual( - mockEmitter); + expect(stft.onOpenEditorTutorial).toEqual(mockEmitter); }); it('should fetch openPostTutorialHelpPopover EventEmitter', () => { - expect(stft.onOpenPostTutorialHelpPopover).toEqual( - mockEmitter); + expect(stft.onOpenPostTutorialHelpPopover).toEqual(mockEmitter); }); it('should fetch openTranslationTutorial EventEmitter', () => { - expect(stft.onOpenTranslationTutorial).toEqual( - mockEmitter); + expect(stft.onOpenTranslationTutorial).toEqual(mockEmitter); }); - it('should initialise the Editor', ()=> { + it('should initialise the Editor', () => { spyOn(eftes, 'initRegisterEvents'); spyOn(stft.onEnterEditorForTheFirstTime, 'emit'); - spyOn(tebas, 'recordStartedEditorTutorialEventAsync').and - .returnValue(Promise.resolve({})); + spyOn(tebas, 'recordStartedEditorTutorialEventAsync').and.returnValue( + Promise.resolve({}) + ); const errorLog = spyOn(console, 'error').and.callThrough(); const expId = 'abc'; stft.initEditor(true, expId); expect(eftes.initRegisterEvents).toHaveBeenCalled(); expect(stft.onEnterEditorForTheFirstTime.emit).toHaveBeenCalled(); expect(errorLog).not.toHaveBeenCalledWith( - 'Warning: could not record editor tutorial start event.'); + 'Warning: could not record editor tutorial start event.' + ); expect(tebas.recordStartedEditorTutorialEventAsync).toHaveBeenCalled(); }); it('should not initialise the Editor', () => { spyOn(eftes, 'initRegisterEvents'); spyOn(stft.onEnterEditorForTheFirstTime, 'emit'); - spyOn(tebas, 'recordStartedEditorTutorialEventAsync').and - .returnValue(Promise.resolve({})); + spyOn(tebas, 'recordStartedEditorTutorialEventAsync').and.returnValue( + Promise.resolve({}) + ); const errorLog = spyOn(console, 'error').and.callThrough(); const expId = 'abc'; stft.markEditorTutorialFinished(); stft.initEditor(false, expId); expect(eftes.initRegisterEvents).not.toHaveBeenCalled(); - expect(stft.onEnterEditorForTheFirstTime.emit).not - .toHaveBeenCalled(); + expect(stft.onEnterEditorForTheFirstTime.emit).not.toHaveBeenCalled(); expect(errorLog).not.toHaveBeenCalledWith( - 'Warning: could not record editor tutorial start event.'); + 'Warning: could not record editor tutorial start event.' + ); expect(tebas.recordStartedEditorTutorialEventAsync).not.toHaveBeenCalled(); }); @@ -111,49 +107,52 @@ describe('State Tutorial First Time Service', () => { }); it('should test the promise rejection for Editor', () => { - spyOn(tebas, 'recordStartedEditorTutorialEventAsync').and. - returnValue(Promise.reject()); + spyOn(tebas, 'recordStartedEditorTutorialEventAsync').and.returnValue( + Promise.reject() + ); const errorLog = spyOn(console, 'error').and.callThrough(); const expId = 'abc'; stft.initEditor(true, expId); expect(errorLog).not.toHaveBeenCalledWith( - 'Warning: could not record translation tutorial start event.'); - expect(tebas.recordStartedEditorTutorialEventAsync) - .toHaveBeenCalled(); + 'Warning: could not record translation tutorial start event.' + ); + expect(tebas.recordStartedEditorTutorialEventAsync).toHaveBeenCalled(); }); it('should initialise the translation', () => { spyOn(eftes, 'initRegisterEvents'); spyOn(stft.onEnterTranslationForTheFirstTime, 'emit'); - spyOn(tebas, 'recordStartedTranslationTutorialEventAsync').and - .returnValue(Promise.resolve({})); + spyOn(tebas, 'recordStartedTranslationTutorialEventAsync').and.returnValue( + Promise.resolve({}) + ); const errorLog = spyOn(console, 'error').and.callThrough(); const expId = 'abc'; stft.initTranslation(expId); - expect(eftes.initRegisterEvents).not - .toHaveBeenCalled(); - expect(stft.onEnterTranslationForTheFirstTime.emit).not - .toHaveBeenCalled(); + expect(eftes.initRegisterEvents).not.toHaveBeenCalled(); + expect(stft.onEnterTranslationForTheFirstTime.emit).not.toHaveBeenCalled(); expect(errorLog).not.toHaveBeenCalledWith( - 'Warning: could not record translation tutorial start event.'); - expect(tebas.recordStartedTranslationTutorialEventAsync).not - .toHaveBeenCalled(); + 'Warning: could not record translation tutorial start event.' + ); + expect( + tebas.recordStartedTranslationTutorialEventAsync + ).not.toHaveBeenCalled(); }); it('should not initialise the translation', () => { spyOn(eftes, 'initRegisterEvents'); spyOn(stft.onEnterTranslationForTheFirstTime, 'emit'); - spyOn(tebas, 'recordStartedTranslationTutorialEventAsync').and - .returnValue(Promise.resolve({})); + spyOn(tebas, 'recordStartedTranslationTutorialEventAsync').and.returnValue( + Promise.resolve({}) + ); const errorLog = spyOn(console, 'error').and.callThrough(); const expId = 'abc'; stft.markTranslationTutorialNotSeenBefore(); stft.initTranslation(expId); expect(eftes.initRegisterEvents).toHaveBeenCalled(); - expect(stft.onEnterTranslationForTheFirstTime.emit) - .toHaveBeenCalled(); + expect(stft.onEnterTranslationForTheFirstTime.emit).toHaveBeenCalled(); expect(errorLog).not.toHaveBeenCalledWith( - 'Warning: could not record translation tutorial start event.'); + 'Warning: could not record translation tutorial start event.' + ); expect(tebas.recordStartedTranslationTutorialEventAsync).toHaveBeenCalled(); }); @@ -164,14 +163,16 @@ describe('State Tutorial First Time Service', () => { }); it('should test the promise rejection for Translator', () => { - spyOn(tebas, 'recordStartedTranslationTutorialEventAsync').and - .returnValue(Promise.reject()); + spyOn(tebas, 'recordStartedTranslationTutorialEventAsync').and.returnValue( + Promise.reject() + ); const errorLog = spyOn(console, 'error').and.callThrough(); const expId = 'abc'; stft.markTranslationTutorialNotSeenBefore(); stft.initTranslation(expId); expect(errorLog).not.toHaveBeenCalledWith( - 'Warning: could not record translation tutorial start event.'); + 'Warning: could not record translation tutorial start event.' + ); expect(tebas.recordStartedTranslationTutorialEventAsync).toHaveBeenCalled(); }); }); diff --git a/core/templates/pages/exploration-editor-page/services/state-tutorial-first-time.service.ts b/core/templates/pages/exploration-editor-page/services/state-tutorial-first-time.service.ts index b481e799055f..9a8b65fdf82d 100644 --- a/core/templates/pages/exploration-editor-page/services/state-tutorial-first-time.service.ts +++ b/core/templates/pages/exploration-editor-page/services/state-tutorial-first-time.service.ts @@ -16,14 +16,14 @@ * @fileoverview Service for all tutorials to be run only for the first time. */ -import { EventEmitter, Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {EventEmitter, Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; -import { EditorFirstTimeEventsService } from 'pages/exploration-editor-page/services/editor-first-time-events.service'; -import { TutorialEventsBackendApiService } from 'pages/exploration-editor-page/services/tutorial-events-backend-api.service'; +import {EditorFirstTimeEventsService} from 'pages/exploration-editor-page/services/editor-first-time-events.service'; +import {TutorialEventsBackendApiService} from 'pages/exploration-editor-page/services/tutorial-events-backend-api.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class StateTutorialFirstTimeService { // Whether this is the first time the tutorial has been seen by this user. @@ -40,7 +40,8 @@ export class StateTutorialFirstTimeService { constructor( private editorFirstTimeEventsService: EditorFirstTimeEventsService, - private tutorialEventsBackendApiService: TutorialEventsBackendApiService) {} + private tutorialEventsBackendApiService: TutorialEventsBackendApiService + ) {} initEditor(firstTime: boolean, expId: string): void { // After the first call to it in a client session, this does nothing. @@ -52,7 +53,8 @@ export class StateTutorialFirstTimeService { this.enterEditorForTheFirstTimeEventEmitter.emit(); this.editorFirstTimeEventsService.initRegisterEvents(expId); this.tutorialEventsBackendApiService - .recordStartedEditorTutorialEventAsync(expId).then(null, () => { + .recordStartedEditorTutorialEventAsync(expId) + .then(null, () => { console.error( 'Warning: could not record editor tutorial start event.' ); @@ -75,8 +77,10 @@ export class StateTutorialFirstTimeService { initTranslation(expId: string): void { // After the first call to it in a client session, this does nothing. - if (!this._translationTutorialNotSeenBefore || - !this._currentlyInTranslationFirstVisit) { + if ( + !this._translationTutorialNotSeenBefore || + !this._currentlyInTranslationFirstVisit + ) { this._currentlyInTranslationFirstVisit = false; } @@ -85,9 +89,11 @@ export class StateTutorialFirstTimeService { this._currentlyInTranslationFirstVisit = false; this.editorFirstTimeEventsService.initRegisterEvents(expId); this.tutorialEventsBackendApiService - .recordStartedTranslationTutorialEventAsync(expId).then(null, () => { + .recordStartedTranslationTutorialEventAsync(expId) + .then(null, () => { console.error( - 'Warning: could not record translation tutorial start event.'); + 'Warning: could not record translation tutorial start event.' + ); }); } } @@ -122,6 +128,9 @@ export class StateTutorialFirstTimeService { } } -angular.module('oppia').factory( - 'StateTutorialFirstTimeService', - downgradeInjectable(StateTutorialFirstTimeService)); +angular + .module('oppia') + .factory( + 'StateTutorialFirstTimeService', + downgradeInjectable(StateTutorialFirstTimeService) + ); diff --git a/core/templates/pages/exploration-editor-page/services/tutorial-events-backend-api.service.spec.ts b/core/templates/pages/exploration-editor-page/services/tutorial-events-backend-api.service.spec.ts index f238a2b1a5f0..84b7f7784137 100644 --- a/core/templates/pages/exploration-editor-page/services/tutorial-events-backend-api.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/tutorial-events-backend-api.service.spec.ts @@ -16,10 +16,13 @@ * @fileoverview Unit tests for StateTutorialFirstTimeBackendApiService. */ -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; -import { TutorialEventsBackendApiService } from 'pages/exploration-editor-page/services/tutorial-events-backend-api.service'; +import {TutorialEventsBackendApiService} from 'pages/exploration-editor-page/services/tutorial-events-backend-api.service'; describe('Tutorial events backend service', () => { let tutorialEventsBackendApiService: TutorialEventsBackendApiService; @@ -27,11 +30,12 @@ describe('Tutorial events backend service', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] + imports: [HttpClientTestingModule], }); tutorialEventsBackendApiService = TestBed.inject( - TutorialEventsBackendApiService); + TutorialEventsBackendApiService + ); httpTestingController = TestBed.inject(HttpTestingController); }); @@ -45,11 +49,13 @@ describe('Tutorial events backend service', () => { let expId = 'abc'; - tutorialEventsBackendApiService.recordStartedEditorTutorialEventAsync(expId) + tutorialEventsBackendApiService + .recordStartedEditorTutorialEventAsync(expId) .then(successHandler, failHandler); let req = httpTestingController.expectOne( - '/createhandler/started_tutorial_event/' + expId); + '/createhandler/started_tutorial_event/' + expId + ); expect(req.request.method).toEqual('POST'); req.flush('Success'); @@ -59,28 +65,30 @@ describe('Tutorial events backend service', () => { expect(failHandler).not.toHaveBeenCalled(); })); - it('should use rejection handler if startEditorTutorial fails', - fakeAsync(() => { - let successHandler = jasmine.createSpy('success'); - let failHandler = jasmine.createSpy('fail'); + it('should use rejection handler if startEditorTutorial fails', fakeAsync(() => { + let successHandler = jasmine.createSpy('success'); + let failHandler = jasmine.createSpy('fail'); - let expId = 'abc'; + let expId = 'abc'; - tutorialEventsBackendApiService.recordStartedEditorTutorialEventAsync( - expId).then(successHandler, failHandler); + tutorialEventsBackendApiService + .recordStartedEditorTutorialEventAsync(expId) + .then(successHandler, failHandler); - let req = httpTestingController.expectOne( - '/createhandler/started_tutorial_event/' + expId); - expect(req.request.method).toEqual('POST'); - req.flush('Error', { - status: 500, statusText: 'Invalid Request' - }); + let req = httpTestingController.expectOne( + '/createhandler/started_tutorial_event/' + expId + ); + expect(req.request.method).toEqual('POST'); + req.flush('Error', { + status: 500, + statusText: 'Invalid Request', + }); - flushMicrotasks(); + flushMicrotasks(); - expect(successHandler).not.toHaveBeenCalled(); - expect(failHandler).toHaveBeenCalled(); - })); + expect(successHandler).not.toHaveBeenCalled(); + expect(failHandler).toHaveBeenCalled(); + })); it('should successfully call startTranslationTutorial', fakeAsync(() => { let successHandler = jasmine.createSpy('success'); @@ -88,11 +96,13 @@ describe('Tutorial events backend service', () => { let expId = 'abc'; - tutorialEventsBackendApiService.recordStartedTranslationTutorialEventAsync( - expId).then(successHandler, failHandler); + tutorialEventsBackendApiService + .recordStartedTranslationTutorialEventAsync(expId) + .then(successHandler, failHandler); let req = httpTestingController.expectOne( - '/createhandler/started_translation_tutorial_event/' + expId); + '/createhandler/started_translation_tutorial_event/' + expId + ); expect(req.request.method).toEqual('POST'); req.flush('Success'); @@ -102,27 +112,28 @@ describe('Tutorial events backend service', () => { expect(failHandler).not.toHaveBeenCalled(); })); - it('should use rejection handler for startTranslationTutorial', - fakeAsync(() => { - let successHandler = jasmine.createSpy('success'); - let failHandler = jasmine.createSpy('fail'); + it('should use rejection handler for startTranslationTutorial', fakeAsync(() => { + let successHandler = jasmine.createSpy('success'); + let failHandler = jasmine.createSpy('fail'); - let expId = 'abc'; + let expId = 'abc'; - tutorialEventsBackendApiService - .recordStartedTranslationTutorialEventAsync(expId) - .then(successHandler, failHandler); + tutorialEventsBackendApiService + .recordStartedTranslationTutorialEventAsync(expId) + .then(successHandler, failHandler); - let req = httpTestingController.expectOne( - '/createhandler/started_translation_tutorial_event/' + expId); - expect(req.request.method).toEqual('POST'); - req.flush('Error', { - status: 500, statusText: 'Invalid Request' - }); + let req = httpTestingController.expectOne( + '/createhandler/started_translation_tutorial_event/' + expId + ); + expect(req.request.method).toEqual('POST'); + req.flush('Error', { + status: 500, + statusText: 'Invalid Request', + }); - flushMicrotasks(); + flushMicrotasks(); - expect(successHandler).not.toHaveBeenCalled(); - expect(failHandler).toHaveBeenCalled(); - })); + expect(successHandler).not.toHaveBeenCalled(); + expect(failHandler).toHaveBeenCalled(); + })); }); diff --git a/core/templates/pages/exploration-editor-page/services/tutorial-events-backend-api.service.ts b/core/templates/pages/exploration-editor-page/services/tutorial-events-backend-api.service.ts index 81fb37c5e2f3..0c2f8d1ce529 100644 --- a/core/templates/pages/exploration-editor-page/services/tutorial-events-backend-api.service.ts +++ b/core/templates/pages/exploration-editor-page/services/tutorial-events-backend-api.service.ts @@ -16,35 +16,51 @@ * @fileoverview Backend api service for all tutorials events. */ -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class TutorialEventsBackendApiService { constructor( private http: HttpClient, - private urlInterpolationService: UrlInterpolationService) {} + private urlInterpolationService: UrlInterpolationService + ) {} async recordStartedEditorTutorialEventAsync(expId: string): Promise { - return this.http.post( - this.urlInterpolationService.interpolateUrl( - '/createhandler/started_tutorial_event/', { expId: expId }), {} - ).toPromise(); + return this.http + .post( + this.urlInterpolationService.interpolateUrl( + '/createhandler/started_tutorial_event/', + {expId: expId} + ), + {} + ) + .toPromise(); } async recordStartedTranslationTutorialEventAsync( - expId: string): Promise { - return this.http.post(this.urlInterpolationService.interpolateUrl( - '/createhandler/started_translation_tutorial_event/', - { expId: expId }), {}).toPromise(); + expId: string + ): Promise { + return this.http + .post( + this.urlInterpolationService.interpolateUrl( + '/createhandler/started_translation_tutorial_event/', + {expId: expId} + ), + {} + ) + .toPromise(); } } -angular.module('oppia').factory( - 'TutorialEventsBackendApiService', - downgradeInjectable(TutorialEventsBackendApiService)); +angular + .module('oppia') + .factory( + 'TutorialEventsBackendApiService', + downgradeInjectable(TutorialEventsBackendApiService) + ); diff --git a/core/templates/pages/exploration-editor-page/services/user-email-preferences-backend-api.service.spec.ts b/core/templates/pages/exploration-editor-page/services/user-email-preferences-backend-api.service.spec.ts index e9a6efbcbc2d..a7181c08c8b6 100644 --- a/core/templates/pages/exploration-editor-page/services/user-email-preferences-backend-api.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/user-email-preferences-backend-api.service.spec.ts @@ -16,37 +16,41 @@ * @fileoverview Unit tests for the UserEmailPreferencesBackendApiService. */ -import { UserEmailPreferencesBackendApiService } from './user-email-preferences-backend-api.service'; -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { fakeAsync, TestBed, flushMicrotasks } from '@angular/core/testing'; -import { EmailPreferencesData } from './user-email-preferences.service'; -import { ExplorationDataService } from './exploration-data.service'; +import {UserEmailPreferencesBackendApiService} from './user-email-preferences-backend-api.service'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {fakeAsync, TestBed, flushMicrotasks} from '@angular/core/testing'; +import {EmailPreferencesData} from './user-email-preferences.service'; +import {ExplorationDataService} from './exploration-data.service'; describe('User Email Preferences Backend Api Service', () => { const expId = '12345'; let sampleResponse = { email_preferences: { mute_feedback_notifications: false, - mute_suggestion_notifications: false - } + mute_suggestion_notifications: false, + }, }; class MockExplorationDataService { explorationId: string = expId; } - let userEmailPreferencesBackendApiService: - UserEmailPreferencesBackendApiService; + let userEmailPreferencesBackendApiService: UserEmailPreferencesBackendApiService; let httpTestingController: HttpTestingController; beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [{ provide: ExplorationDataService, - useClass: MockExplorationDataService }] + providers: [ + {provide: ExplorationDataService, useClass: MockExplorationDataService}, + ], }); - userEmailPreferencesBackendApiService = - TestBed.inject(UserEmailPreferencesBackendApiService); + userEmailPreferencesBackendApiService = TestBed.inject( + UserEmailPreferencesBackendApiService + ); httpTestingController = TestBed.inject(HttpTestingController); }); @@ -54,22 +58,20 @@ describe('User Email Preferences Backend Api Service', () => { httpTestingController.verify(); }); - it('should successfully send http request and get a valid response', - fakeAsync(() => { - let result: Promise = - userEmailPreferencesBackendApiService - .saveChangeToBackendAsync({ - message_type: 'feedback', - mute: false - }); - let req = httpTestingController.expectOne( - '/createhandler/notificationpreferences/' + expId - ); - expect(req.request.method).toEqual('PUT'); - req.flush(sampleResponse); - flushMicrotasks(); - result.then((data: EmailPreferencesData) => { - expect(data).toEqual(sampleResponse); + it('should successfully send http request and get a valid response', fakeAsync(() => { + let result: Promise = + userEmailPreferencesBackendApiService.saveChangeToBackendAsync({ + message_type: 'feedback', + mute: false, }); - })); + let req = httpTestingController.expectOne( + '/createhandler/notificationpreferences/' + expId + ); + expect(req.request.method).toEqual('PUT'); + req.flush(sampleResponse); + flushMicrotasks(); + result.then((data: EmailPreferencesData) => { + expect(data).toEqual(sampleResponse); + }); + })); }); diff --git a/core/templates/pages/exploration-editor-page/services/user-email-preferences-backend-api.service.ts b/core/templates/pages/exploration-editor-page/services/user-email-preferences-backend-api.service.ts index 009ebb8de257..9b549cbfc490 100644 --- a/core/templates/pages/exploration-editor-page/services/user-email-preferences-backend-api.service.ts +++ b/core/templates/pages/exploration-editor-page/services/user-email-preferences-backend-api.service.ts @@ -17,37 +17,44 @@ * for the exploration settings. */ -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { ExplorationDataService } from './exploration-data.service'; -import { EmailPreferencesData, RequestParams } from './user-email-preferences.service'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {ExplorationDataService} from './exploration-data.service'; +import { + EmailPreferencesData, + RequestParams, +} from './user-email-preferences.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class UserEmailPreferencesBackendApiService { constructor( private http: HttpClient, private urlInterpolationService: UrlInterpolationService, private explorationDataService: ExplorationDataService - ) { } + ) {} async saveChangeToBackendAsync( - requestParams: RequestParams + requestParams: RequestParams ): Promise { let emailPreferencesUrl = this.urlInterpolationService.interpolateUrl( - '/createhandler/notificationpreferences/', { - exploration_id: this.explorationDataService.explorationId + '/createhandler/notificationpreferences/', + { + exploration_id: this.explorationDataService.explorationId, } ); - return this.http.put( - emailPreferencesUrl, requestParams).toPromise(); + return this.http + .put(emailPreferencesUrl, requestParams) + .toPromise(); } } -angular.module('oppia').factory( - 'UserEmailPreferencesBackendApiService', - downgradeInjectable(UserEmailPreferencesBackendApiService) -); +angular + .module('oppia') + .factory( + 'UserEmailPreferencesBackendApiService', + downgradeInjectable(UserEmailPreferencesBackendApiService) + ); diff --git a/core/templates/pages/exploration-editor-page/services/user-email-preferences.service.spec.ts b/core/templates/pages/exploration-editor-page/services/user-email-preferences.service.spec.ts index d09bb1d3d99a..5218b8238236 100644 --- a/core/templates/pages/exploration-editor-page/services/user-email-preferences.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/user-email-preferences.service.spec.ts @@ -16,20 +16,22 @@ * @fileoverview Unit tests for the UserEmailPreferencesService. */ -import { UserEmailPreferencesService } from './user-email-preferences.service'; -import { HttpClientTestingModule, HttpTestingController } from - '@angular/common/http/testing'; -import { fakeAsync, flushMicrotasks, TestBed } from '@angular/core/testing'; -import { CsrfTokenService } from 'services/csrf-token.service'; -import { ExplorationDataService } from './exploration-data.service'; +import {UserEmailPreferencesService} from './user-email-preferences.service'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {fakeAsync, flushMicrotasks, TestBed} from '@angular/core/testing'; +import {CsrfTokenService} from 'services/csrf-token.service'; +import {ExplorationDataService} from './exploration-data.service'; describe('User Email Preferences Service', () => { let expId = '12345'; let sampleResponse = { email_preferences: { mute_feedback_notifications: false, - mute_suggestion_notifications: false - } + mute_suggestion_notifications: false, + }, }; class MockExplorationDataService { explorationId: string = expId; @@ -42,15 +44,16 @@ describe('User Email Preferences Service', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [{ provide: ExplorationDataService, - useClass: MockExplorationDataService }] + providers: [ + {provide: ExplorationDataService, useClass: MockExplorationDataService}, + ], }); userEmailPreferencesService = TestBed.inject(UserEmailPreferencesService); httpTestingController = TestBed.inject(HttpTestingController); csrfTokenService = TestBed.inject(CsrfTokenService); - spyOn(csrfTokenService, 'getTokenAsync').and.callFake(async() => { - return new Promise((resolve) => { + spyOn(csrfTokenService, 'getTokenAsync').and.callFake(async () => { + return new Promise(resolve => { resolve('sample-csrf-token'); }); }); @@ -61,10 +64,10 @@ describe('User Email Preferences Service', () => { }); it('should successfully intialise the service', () => { - expect(userEmailPreferencesService.feedbackNotificationsMuted) - .toBeFalse(); - expect(userEmailPreferencesService.suggestionNotificationsMuted) - .toBeFalse(); + expect(userEmailPreferencesService.feedbackNotificationsMuted).toBeFalse(); + expect( + userEmailPreferencesService.suggestionNotificationsMuted + ).toBeFalse(); userEmailPreferencesService.init(true, true); @@ -72,45 +75,50 @@ describe('User Email Preferences Service', () => { expect(userEmailPreferencesService.suggestionNotificationsMuted).toBe(true); }); - it('should successfully return the feedbackNotificationsMuted value', - () => { - userEmailPreferencesService.init(true, true); - expect(userEmailPreferencesService.areFeedbackNotificationsMuted()) - .toBe(true); - }); + it('should successfully return the feedbackNotificationsMuted value', () => { + userEmailPreferencesService.init(true, true); + expect(userEmailPreferencesService.areFeedbackNotificationsMuted()).toBe( + true + ); + }); - it('should successfully return the suggestionNotificationsMuted value', - () => { - userEmailPreferencesService.init(true, true); - expect(userEmailPreferencesService.areSuggestionNotificationsMuted()) - .toBe(true); - }); + it('should successfully return the suggestionNotificationsMuted value', () => { + userEmailPreferencesService.init(true, true); + expect(userEmailPreferencesService.areSuggestionNotificationsMuted()).toBe( + true + ); + }); - it('should successfully set the feedback notification preferences', - fakeAsync(() => { - userEmailPreferencesService - .setFeedbackNotificationPreferences(false, () => {}); - let req = httpTestingController.expectOne( - '/createhandler/notificationpreferences/' + expId); - expect(req.request.method).toEqual('PUT'); - req.flush(sampleResponse); - flushMicrotasks(); - expect(userEmailPreferencesService.areFeedbackNotificationsMuted()) - .toBe(false); - })); + it('should successfully set the feedback notification preferences', fakeAsync(() => { + userEmailPreferencesService.setFeedbackNotificationPreferences( + false, + () => {} + ); + let req = httpTestingController.expectOne( + '/createhandler/notificationpreferences/' + expId + ); + expect(req.request.method).toEqual('PUT'); + req.flush(sampleResponse); + flushMicrotasks(); + expect(userEmailPreferencesService.areFeedbackNotificationsMuted()).toBe( + false + ); + })); - it('should successfully set the suggestion notification preferences', - fakeAsync(() => { - userEmailPreferencesService - .setSuggestionNotificationPreferences(false, () => {}); + it('should successfully set the suggestion notification preferences', fakeAsync(() => { + userEmailPreferencesService.setSuggestionNotificationPreferences( + false, + () => {} + ); - let req = httpTestingController.expectOne( - '/createhandler/notificationpreferences/' + expId - ); - expect(req.request.method).toEqual('PUT'); - req.flush(sampleResponse); - flushMicrotasks(); - expect(userEmailPreferencesService.areSuggestionNotificationsMuted()) - .toBe(false); - })); + let req = httpTestingController.expectOne( + '/createhandler/notificationpreferences/' + expId + ); + expect(req.request.method).toEqual('PUT'); + req.flush(sampleResponse); + flushMicrotasks(); + expect(userEmailPreferencesService.areSuggestionNotificationsMuted()).toBe( + false + ); + })); }); diff --git a/core/templates/pages/exploration-editor-page/services/user-email-preferences.service.ts b/core/templates/pages/exploration-editor-page/services/user-email-preferences.service.ts index 4bc594526a66..65c66cddc2da 100644 --- a/core/templates/pages/exploration-editor-page/services/user-email-preferences.service.ts +++ b/core/templates/pages/exploration-editor-page/services/user-email-preferences.service.ts @@ -16,25 +16,25 @@ * @fileoverview User exploration emails service for the exploration settings. */ -import { AlertsService } from 'services/alerts.service'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; -import { UserEmailPreferencesBackendApiService } from './user-email-preferences-backend-api.service'; +import {AlertsService} from 'services/alerts.service'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {UserEmailPreferencesBackendApiService} from './user-email-preferences-backend-api.service'; export interface EmailPreferencesData { - 'email_preferences': { - 'mute_feedback_notifications': boolean; - 'mute_suggestion_notifications': boolean; + email_preferences: { + mute_feedback_notifications: boolean; + mute_suggestion_notifications: boolean; }; } export interface RequestParams { - 'message_type': string; + message_type: string; mute: boolean; } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class UserEmailPreferencesService { MESSAGE_TYPE_SUGGESTION = 'suggestion'; @@ -43,13 +43,13 @@ export class UserEmailPreferencesService { suggestionNotificationsMuted: boolean = false; constructor( private alertsService: AlertsService, - private userEmailPreferencesBackendApiService: - UserEmailPreferencesBackendApiService - ) { } + private userEmailPreferencesBackendApiService: UserEmailPreferencesBackendApiService + ) {} init( - feedbackNotificationsMuted: boolean, - suggestionNotificationsMuted: boolean): void { + feedbackNotificationsMuted: boolean, + suggestionNotificationsMuted: boolean + ): void { this.feedbackNotificationsMuted = feedbackNotificationsMuted; this.suggestionNotificationsMuted = suggestionNotificationsMuted; } @@ -73,10 +73,12 @@ export class UserEmailPreferencesService { * @param {boolean} mute - Whether the feedback notification is muted. */ setFeedbackNotificationPreferences( - mute: boolean, successCallback: () => void): void { + mute: boolean, + successCallback: () => void + ): void { this.saveChangeToBackendAsync({ message_type: this.MESSAGE_TYPE_FEEDBACK, - mute: mute + mute: mute, }).then(() => { successCallback(); }); @@ -87,11 +89,13 @@ export class UserEmailPreferencesService { * @param {boolean} mute - Whether the suggestion notification is muted. */ setSuggestionNotificationPreferences( - mute: boolean, successCallback: () => void): void { + mute: boolean, + successCallback: () => void + ): void { this.saveChangeToBackendAsync({ message_type: this.MESSAGE_TYPE_SUGGESTION, - mute: mute - }).then(()=> { + mute: mute, + }).then(() => { successCallback(); }); } @@ -100,20 +104,23 @@ export class UserEmailPreferencesService { * Save the change of message_type and mute to backend. * @param {RequestParam} requestParams - Info about message_type and mute. */ - async saveChangeToBackendAsync(requestParams: RequestParams): - Promise { + async saveChangeToBackendAsync(requestParams: RequestParams): Promise { return this.userEmailPreferencesBackendApiService - .saveChangeToBackendAsync(requestParams).then( - (response: EmailPreferencesData) => { - let data = response; - this.alertsService.clearWarnings(), + .saveChangeToBackendAsync(requestParams) + .then((response: EmailPreferencesData) => { + let data = response; + this.alertsService.clearWarnings(), this.init( data.email_preferences.mute_feedback_notifications, - data.email_preferences.mute_suggestion_notifications); - }); + data.email_preferences.mute_suggestion_notifications + ); + }); } } -angular.module('oppia').factory( - 'UserEmailPreferencesService', - downgradeInjectable(UserEmailPreferencesService)); +angular + .module('oppia') + .factory( + 'UserEmailPreferencesService', + downgradeInjectable(UserEmailPreferencesService) + ); diff --git a/core/templates/pages/exploration-editor-page/services/user-exploration-permissions.service.spec.ts b/core/templates/pages/exploration-editor-page/services/user-exploration-permissions.service.spec.ts index 434b1d1234a4..bc13d17de797 100644 --- a/core/templates/pages/exploration-editor-page/services/user-exploration-permissions.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/user-exploration-permissions.service.spec.ts @@ -15,18 +15,17 @@ /** * @fileoverview Unit tests for the UserExplorationPermissionsService. */ -import { EventEmitter } from '@angular/core'; +import {EventEmitter} from '@angular/core'; -import { HttpClientTestingModule, HttpTestingController } - from '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; -import { ContextService } from - 'services/context.service'; -import { UserExplorationPermissionsService } from - 'pages/exploration-editor-page/services/user-exploration-permissions.service'; -import { ExplorationPermissions } from - 'domain/exploration/exploration-permissions.model'; +import {ContextService} from 'services/context.service'; +import {UserExplorationPermissionsService} from 'pages/exploration-editor-page/services/user-exploration-permissions.service'; +import {ExplorationPermissions} from 'domain/exploration/exploration-permissions.model'; describe('User Exploration Permissions Service', () => { let ueps: UserExplorationPermissionsService; @@ -42,12 +41,11 @@ describe('User Exploration Permissions Service', () => { can_publish: false, can_delete: false, can_modify_roles: false, - can_manage_voice_artist: false + can_manage_voice_artist: false, }; let permissionsResponse: ExplorationPermissions; - - beforeEach(()=> { + beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], }); @@ -55,24 +53,27 @@ describe('User Exploration Permissions Service', () => { httpTestingController = TestBed.inject(HttpTestingController); ueps = TestBed.inject(UserExplorationPermissionsService); contextService = TestBed.inject(ContextService); - permissionsResponse = - ExplorationPermissions.createFromBackendDict(samplePermissionsData); + permissionsResponse = ExplorationPermissions.createFromBackendDict( + samplePermissionsData + ); spyOn(contextService, 'getExplorationId').and.returnValue( - sampleExplorationId); + sampleExplorationId + ); UserExplorationPermissionsService.permissionsPromise = null; }); - afterEach(()=> { + afterEach(() => { httpTestingController.verify(); }); it('should fetch the correct data', fakeAsync(() => { - ueps.getPermissionsAsync().then((response) => { + ueps.getPermissionsAsync().then(response => { expect(response).toEqual(permissionsResponse); }); let req = httpTestingController.expectOne( - '/createhandler/permissions/' + sampleExplorationId); + '/createhandler/permissions/' + sampleExplorationId + ); expect(req.request.method).toEqual('GET'); req.flush(samplePermissionsData); flushMicrotasks(); @@ -81,45 +82,55 @@ describe('User Exploration Permissions Service', () => { it('should cache rights data', fakeAsync(() => { ueps.getPermissionsAsync(); let req = httpTestingController.expectOne( - '/createhandler/permissions/' + sampleExplorationId); + '/createhandler/permissions/' + sampleExplorationId + ); expect(req.request.method).toEqual('GET'); req.flush(samplePermissionsData); flushMicrotasks(); ueps.getPermissionsAsync(); httpTestingController.expectNone( - '/createhandler/permissions/' + sampleExplorationId); + '/createhandler/permissions/' + sampleExplorationId + ); })); - it('should fetch rights data irrespective' + - 'whether it is cached or not', fakeAsync(() => { - ueps.getPermissionsAsync(); - let req = httpTestingController.expectOne( - '/createhandler/permissions/' + sampleExplorationId); - expect(req.request.method).toEqual('GET'); - req.flush(samplePermissionsData); - flushMicrotasks(); - - ueps.fetchPermissionsAsync(); - let req2 = httpTestingController.expectOne( - '/createhandler/permissions/' + sampleExplorationId); - - expect(req2.request.method).toEqual('GET'); - req2.flush(samplePermissionsData); - flushMicrotasks(); - })); - - it('should emit when the user exploration' + - 'permissions are fetched', fakeAsync(() => { - let mockuserExplorationPermissionsFetched = new EventEmitter(); - ueps.fetchPermissionsAsync(); - let req = httpTestingController.expectOne( - '/createhandler/permissions/' + sampleExplorationId); - - expect(req.request.method).toEqual('GET'); - req.flush(samplePermissionsData); - flushMicrotasks(); - expect(ueps.onUserExplorationPermissionsFetched).toEqual( - mockuserExplorationPermissionsFetched); - })); + it( + 'should fetch rights data irrespective' + 'whether it is cached or not', + fakeAsync(() => { + ueps.getPermissionsAsync(); + let req = httpTestingController.expectOne( + '/createhandler/permissions/' + sampleExplorationId + ); + expect(req.request.method).toEqual('GET'); + req.flush(samplePermissionsData); + flushMicrotasks(); + + ueps.fetchPermissionsAsync(); + let req2 = httpTestingController.expectOne( + '/createhandler/permissions/' + sampleExplorationId + ); + + expect(req2.request.method).toEqual('GET'); + req2.flush(samplePermissionsData); + flushMicrotasks(); + }) + ); + + it( + 'should emit when the user exploration' + 'permissions are fetched', + fakeAsync(() => { + let mockuserExplorationPermissionsFetched = new EventEmitter(); + ueps.fetchPermissionsAsync(); + let req = httpTestingController.expectOne( + '/createhandler/permissions/' + sampleExplorationId + ); + + expect(req.request.method).toEqual('GET'); + req.flush(samplePermissionsData); + flushMicrotasks(); + expect(ueps.onUserExplorationPermissionsFetched).toEqual( + mockuserExplorationPermissionsFetched + ); + }) + ); }); diff --git a/core/templates/pages/exploration-editor-page/services/user-exploration-permissions.service.ts b/core/templates/pages/exploration-editor-page/services/user-exploration-permissions.service.ts index 937bea4e59f9..fceac354577a 100644 --- a/core/templates/pages/exploration-editor-page/services/user-exploration-permissions.service.ts +++ b/core/templates/pages/exploration-editor-page/services/user-exploration-permissions.service.ts @@ -17,25 +17,22 @@ * of a user for a particular exploration. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { EventEmitter } from '@angular/core'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {EventEmitter} from '@angular/core'; +import {Injectable} from '@angular/core'; -import { ExplorationPermissionsBackendApiService } from - 'domain/exploration/exploration-permissions-backend-api.service'; -import { ExplorationPermissions } from - 'domain/exploration/exploration-permissions.model'; +import {ExplorationPermissionsBackendApiService} from 'domain/exploration/exploration-permissions-backend-api.service'; +import {ExplorationPermissions} from 'domain/exploration/exploration-permissions.model'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class UserExplorationPermissionsService { private userExplorationPermissionsFetched = new EventEmitter(); constructor( - private explorationPermissionsBackendApiService: - ExplorationPermissionsBackendApiService) { - } + private explorationPermissionsBackendApiService: ExplorationPermissionsBackendApiService + ) {} // 'permissionsPromise' will be null until populated by async function // getPermissionsAsync(). @@ -43,24 +40,21 @@ export class UserExplorationPermissionsService { async getPermissionsAsync(): Promise { if (!UserExplorationPermissionsService.permissionsPromise) { - UserExplorationPermissionsService.permissionsPromise = ( - this.fetchPermissionsAsync()); + UserExplorationPermissionsService.permissionsPromise = + this.fetchPermissionsAsync(); } return UserExplorationPermissionsService.permissionsPromise; } async fetchPermissionsAsync(): Promise { - let permissionPromise = ( - this.explorationPermissionsBackendApiService.getPermissionsAsync()); + let permissionPromise = + this.explorationPermissionsBackendApiService.getPermissionsAsync(); UserExplorationPermissionsService.permissionsPromise = permissionPromise; return new Promise((resolve, reject) => { - permissionPromise.then( - (response) => { - this.userExplorationPermissionsFetched.emit(); - resolve(response); - }, - reject, - ); + permissionPromise.then(response => { + this.userExplorationPermissionsFetched.emit(); + resolve(response); + }, reject); }); } @@ -69,5 +63,9 @@ export class UserExplorationPermissionsService { } } -angular.module('oppia').factory('UserExplorationPermissionsService', - downgradeInjectable(UserExplorationPermissionsService)); +angular + .module('oppia') + .factory( + 'UserExplorationPermissionsService', + downgradeInjectable(UserExplorationPermissionsService) + ); diff --git a/core/templates/pages/exploration-editor-page/services/version-history-backend-api.service.spec.ts b/core/templates/pages/exploration-editor-page/services/version-history-backend-api.service.spec.ts index 901845b46404..07f21f2bdf48 100644 --- a/core/templates/pages/exploration-editor-page/services/version-history-backend-api.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/version-history-backend-api.service.spec.ts @@ -16,12 +16,15 @@ * @fileoverview Unit tests for version history backend api service. */ -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { fakeAsync, flushMicrotasks, TestBed } from '@angular/core/testing'; -import { ExplorationMetadataObjectFactory } from 'domain/exploration/ExplorationMetadataObjectFactory'; -import { StateObjectFactory } from 'domain/state/StateObjectFactory'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { VersionHistoryBackendApiService } from './version-history-backend-api.service'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {fakeAsync, flushMicrotasks, TestBed} from '@angular/core/testing'; +import {ExplorationMetadataObjectFactory} from 'domain/exploration/ExplorationMetadataObjectFactory'; +import {StateObjectFactory} from 'domain/state/StateObjectFactory'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {VersionHistoryBackendApiService} from './version-history-backend-api.service'; describe('Version history backend api service', () => { let versionHistoryBackendApiService: VersionHistoryBackendApiService; @@ -31,22 +34,22 @@ describe('Version history backend api service', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], + imports: [HttpClientTestingModule], providers: [ ExplorationMetadataObjectFactory, StateObjectFactory, - UrlInterpolationService - ] + UrlInterpolationService, + ], }); versionHistoryBackendApiService = TestBed.inject( - VersionHistoryBackendApiService); + VersionHistoryBackendApiService + ); http = TestBed.inject(HttpTestingController); stateObjectFactory = TestBed.inject(StateObjectFactory); explorationMetadataObjectFactory = TestBed.inject( - ExplorationMetadataObjectFactory); + ExplorationMetadataObjectFactory + ); }); it('should correctly fetch the version history of a state', fakeAsync(() => { @@ -55,24 +58,28 @@ describe('Version history backend api service', () => { const sampleStateVersionHistoryDict = { last_edited_version_number: 1, state_name_in_previous_version: 'Introduction', - state_dict_in_previous_version: stateObjectFactory.createDefaultState( - 'Introduction', 'content_0', 'default_outcome_1' - ).toBackendDict(), - last_edited_committer_username: 'user1' + state_dict_in_previous_version: stateObjectFactory + .createDefaultState('Introduction', 'content_0', 'default_outcome_1') + .toBackendDict(), + last_edited_committer_username: 'user1', }; const sampleStateVersionHistory = { lastEditedVersionNumber: 1, stateNameInPreviousVersion: 'Introduction', stateInPreviousVersion: stateObjectFactory.createDefaultState( - 'Introduction', 'content_0', 'default_outcome_1'), - lastEditedCommitterUsername: 'user1' + 'Introduction', + 'content_0', + 'default_outcome_1' + ), + lastEditedCommitterUsername: 'user1', }; - versionHistoryBackendApiService.fetchStateVersionHistoryAsync( - '1', 'Introduction', 2 - ).then(successHandler, failureHandler); + versionHistoryBackendApiService + .fetchStateVersionHistoryAsync('1', 'Introduction', 2) + .then(successHandler, failureHandler); const req = http.expectOne( - '/version_history_handler/state/1/Introduction/2'); + '/version_history_handler/state/1/Introduction/2' + ); expect(req.request.method).toEqual('GET'); req.flush(sampleStateVersionHistoryDict); flushMicrotasks(); @@ -84,88 +91,93 @@ describe('Version history backend api service', () => { it('should fail to fetch the version history of a state', fakeAsync(() => { const successHandler = jasmine.createSpy('success'); const failureHandler = jasmine.createSpy('failure'); - versionHistoryBackendApiService.fetchStateVersionHistoryAsync( - '1', 'Introduction', 2 - ).then(successHandler, failureHandler); + versionHistoryBackendApiService + .fetchStateVersionHistoryAsync('1', 'Introduction', 2) + .then(successHandler, failureHandler); const req = http.expectOne( - '/version_history_handler/state/1/Introduction/2'); + '/version_history_handler/state/1/Introduction/2' + ); expect(req.request.method).toEqual('GET'); - req.flush({ - error: 'could not find the version history data' - }, { - status: 404, - statusText: 'Page not found' - }); + req.flush( + { + error: 'could not find the version history data', + }, + { + status: 404, + statusText: 'Page not found', + } + ); flushMicrotasks(); expect(successHandler).toHaveBeenCalledWith(null); expect(failureHandler).not.toHaveBeenCalled(); })); - it('should correctly fetch the version history of the exploration metadata', - fakeAsync(() => { - const successHandler = jasmine.createSpy('success'); - const failureHandler = jasmine.createSpy('failure'); - const sampleMetadataVersionHistoryDict = { - last_edited_version_number: 1, - last_edited_committer_username: 'user1', - metadata_dict_in_previous_version: { - title: 'Exploration', - category: 'Algebra', - objective: 'To learn', - language_code: 'en', - tags: [], - blurb: '', - author_notes: '', - states_schema_version: 50, - init_state_name: 'Introduction', - param_changes: [], - param_specs: {}, - auto_tts_enabled: false, - edits_allowed: true - } - }; - const sampleMetadataVersionHistory = { - lastEditedVersionNumber: 1, - lastEditedCommitterUsername: 'user1', - metadataInPreviousVersion: ( - explorationMetadataObjectFactory.createFromBackendDict( - sampleMetadataVersionHistoryDict.metadata_dict_in_previous_version)) - }; - versionHistoryBackendApiService - .fetchMetadataVersionHistoryAsync('1', 2) - .then(successHandler, failureHandler); - - const req = http.expectOne('/version_history_handler/metadata/1/2'); - expect(req.request.method).toEqual('GET'); - req.flush(sampleMetadataVersionHistoryDict); - flushMicrotasks(); - - expect(successHandler).toHaveBeenCalledWith(sampleMetadataVersionHistory); - expect(failureHandler).not.toHaveBeenCalled(); - })); - - it('should fail to fetch version history for exploration metadata', - fakeAsync(() => { - const successHandler = jasmine.createSpy('success'); - const failureHandler = jasmine.createSpy('failure'); - versionHistoryBackendApiService - .fetchMetadataVersionHistoryAsync('1', 2) - .then(successHandler, failureHandler); - - const req = http.expectOne('/version_history_handler/metadata/1/2'); - expect(req.request.method).toEqual('GET'); - req.flush({ - error: 'could not find the version history data' - }, { + it('should correctly fetch the version history of the exploration metadata', fakeAsync(() => { + const successHandler = jasmine.createSpy('success'); + const failureHandler = jasmine.createSpy('failure'); + const sampleMetadataVersionHistoryDict = { + last_edited_version_number: 1, + last_edited_committer_username: 'user1', + metadata_dict_in_previous_version: { + title: 'Exploration', + category: 'Algebra', + objective: 'To learn', + language_code: 'en', + tags: [], + blurb: '', + author_notes: '', + states_schema_version: 50, + init_state_name: 'Introduction', + param_changes: [], + param_specs: {}, + auto_tts_enabled: false, + edits_allowed: true, + }, + }; + const sampleMetadataVersionHistory = { + lastEditedVersionNumber: 1, + lastEditedCommitterUsername: 'user1', + metadataInPreviousVersion: + explorationMetadataObjectFactory.createFromBackendDict( + sampleMetadataVersionHistoryDict.metadata_dict_in_previous_version + ), + }; + versionHistoryBackendApiService + .fetchMetadataVersionHistoryAsync('1', 2) + .then(successHandler, failureHandler); + + const req = http.expectOne('/version_history_handler/metadata/1/2'); + expect(req.request.method).toEqual('GET'); + req.flush(sampleMetadataVersionHistoryDict); + flushMicrotasks(); + + expect(successHandler).toHaveBeenCalledWith(sampleMetadataVersionHistory); + expect(failureHandler).not.toHaveBeenCalled(); + })); + + it('should fail to fetch version history for exploration metadata', fakeAsync(() => { + const successHandler = jasmine.createSpy('success'); + const failureHandler = jasmine.createSpy('failure'); + versionHistoryBackendApiService + .fetchMetadataVersionHistoryAsync('1', 2) + .then(successHandler, failureHandler); + + const req = http.expectOne('/version_history_handler/metadata/1/2'); + expect(req.request.method).toEqual('GET'); + req.flush( + { + error: 'could not find the version history data', + }, + { status: 404, - statusText: 'Page not found' - }); - flushMicrotasks(); - - expect(successHandler).toHaveBeenCalledWith(null); - expect(failureHandler).not.toHaveBeenCalled(); - }) - ); + statusText: 'Page not found', + } + ); + flushMicrotasks(); + + expect(successHandler).toHaveBeenCalledWith(null); + expect(failureHandler).not.toHaveBeenCalled(); + })); }); diff --git a/core/templates/pages/exploration-editor-page/services/version-history-backend-api.service.ts b/core/templates/pages/exploration-editor-page/services/version-history-backend-api.service.ts index 7b517ca13da9..4812ca7cf5bb 100644 --- a/core/templates/pages/exploration-editor-page/services/version-history-backend-api.service.ts +++ b/core/templates/pages/exploration-editor-page/services/version-history-backend-api.service.ts @@ -17,12 +17,20 @@ * or the exploration metadata at a particular version of the exploration. */ -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { ExplorationMetadata, ExplorationMetadataBackendDict, ExplorationMetadataObjectFactory } from 'domain/exploration/ExplorationMetadataObjectFactory'; -import { State, StateBackendDict, StateObjectFactory } from 'domain/state/StateObjectFactory'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import { + ExplorationMetadata, + ExplorationMetadataBackendDict, + ExplorationMetadataObjectFactory, +} from 'domain/exploration/ExplorationMetadataObjectFactory'; +import { + State, + StateBackendDict, + StateObjectFactory, +} from 'domain/state/StateObjectFactory'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; interface StateVersionHistoryBackendResponse { last_edited_version_number: number | null; @@ -51,14 +59,14 @@ export interface MetadataVersionHistoryResponse { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class VersionHistoryBackendApiService { - STATE_VERSION_HISTORY_URL_TEMPLATE = ( - '/version_history_handler/state///'); + STATE_VERSION_HISTORY_URL_TEMPLATE = + '/version_history_handler/state///'; - METADATA_VERSION_HISTORY_URL_TEMPLATE = ( - '/version_history_handler/metadata//'); + METADATA_VERSION_HISTORY_URL_TEMPLATE = + '/version_history_handler/metadata//'; constructor( private explorationMetadataObjectFactory: ExplorationMetadataObjectFactory, @@ -68,54 +76,64 @@ export class VersionHistoryBackendApiService { ) {} async fetchStateVersionHistoryAsync( - explorationId: string, stateName: string, version: number + explorationId: string, + stateName: string, + version: number ): Promise { const url = this.urlInterpolationService.interpolateUrl( - this.STATE_VERSION_HISTORY_URL_TEMPLATE, { + this.STATE_VERSION_HISTORY_URL_TEMPLATE, + { exploration_id: explorationId, state_name: stateName, - version: version.toString() - }); + version: version.toString(), + } + ); - return this.http.get(url) + return this.http + .get(url) .toPromise() - .then((response) => { + .then(response => { let stateInPreviousVersion: State | null = null; if (response.state_dict_in_previous_version) { - stateInPreviousVersion = ( + stateInPreviousVersion = this.stateObjectFactory.createFromBackendDict( response.state_name_in_previous_version, response.state_dict_in_previous_version - ) - ); + ); } return { lastEditedVersionNumber: response.last_edited_version_number, stateNameInPreviousVersion: response.state_name_in_previous_version, stateInPreviousVersion: stateInPreviousVersion, - lastEditedCommitterUsername: response.last_edited_committer_username + lastEditedCommitterUsername: response.last_edited_committer_username, }; }) - .catch((error) => { + .catch(error => { return null; }); } async fetchMetadataVersionHistoryAsync( - explorationId: string, version: number + explorationId: string, + version: number ): Promise { const url = this.urlInterpolationService.interpolateUrl( - this.METADATA_VERSION_HISTORY_URL_TEMPLATE, { + this.METADATA_VERSION_HISTORY_URL_TEMPLATE, + { exploration_id: explorationId, - version: version.toString() - }); - return this.http.get(url) + version: version.toString(), + } + ); + return this.http + .get(url) .toPromise() - .then((response) => { + .then(response => { let metadataInPreviousVersion: ExplorationMetadata | null = null; if (response.metadata_dict_in_previous_version) { - metadataInPreviousVersion = this.explorationMetadataObjectFactory - .createFromBackendDict(response.metadata_dict_in_previous_version); + metadataInPreviousVersion = + this.explorationMetadataObjectFactory.createFromBackendDict( + response.metadata_dict_in_previous_version + ); } return { lastEditedVersionNumber: response.last_edited_version_number, @@ -123,12 +141,15 @@ export class VersionHistoryBackendApiService { metadataInPreviousVersion: metadataInPreviousVersion, }; }) - .catch((error) => { + .catch(error => { return null; }); } } -angular.module('oppia').factory( - 'VersionHistoryBackendApiService', - downgradeInjectable(VersionHistoryBackendApiService)); +angular + .module('oppia') + .factory( + 'VersionHistoryBackendApiService', + downgradeInjectable(VersionHistoryBackendApiService) + ); diff --git a/core/templates/pages/exploration-editor-page/services/version-history.service.spec.ts b/core/templates/pages/exploration-editor-page/services/version-history.service.spec.ts index 1c2e8c6e5faa..880c182b7f96 100644 --- a/core/templates/pages/exploration-editor-page/services/version-history.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/version-history.service.spec.ts @@ -16,12 +16,12 @@ * @fileoverview Service for the exploration states version history. */ -import { TestBed } from '@angular/core/testing'; -import { ExplorationMetadata } from 'domain/exploration/ExplorationMetadataObjectFactory'; -import { ParamSpecObjectFactory } from 'domain/exploration/ParamSpecObjectFactory'; -import { ParamSpecs } from 'domain/exploration/ParamSpecsObjectFactory'; -import { StateObjectFactory } from 'domain/state/StateObjectFactory'; -import { VersionHistoryService } from './version-history.service'; +import {TestBed} from '@angular/core/testing'; +import {ExplorationMetadata} from 'domain/exploration/ExplorationMetadataObjectFactory'; +import {ParamSpecObjectFactory} from 'domain/exploration/ParamSpecObjectFactory'; +import {ParamSpecs} from 'domain/exploration/ParamSpecsObjectFactory'; +import {StateObjectFactory} from 'domain/state/StateObjectFactory'; +import {VersionHistoryService} from './version-history.service'; describe('Version history service', () => { let versionHistoryService: VersionHistoryService; @@ -39,28 +39,57 @@ describe('Version history service', () => { expect(versionHistoryService.fetchedMetadata.length).toEqual(0); const explorationMetadata = new ExplorationMetadata( - 'title', 'category', 'objective', 'en', - [], '', '', 55, 'Introduction', - new ParamSpecs({}, paramSpecObjectFactory), [], false, true + 'title', + 'category', + 'objective', + 'en', + [], + '', + '', + 55, + 'Introduction', + new ParamSpecs({}, paramSpecObjectFactory), + [], + false, + true ); versionHistoryService.insertMetadataVersionHistoryData( - 3, explorationMetadata, ''); + 3, + explorationMetadata, + '' + ); expect(versionHistoryService.fetchedMetadata.length).toEqual(1); versionHistoryService.insertMetadataVersionHistoryData( - 3, explorationMetadata, ''); + 3, + explorationMetadata, + '' + ); expect(versionHistoryService.fetchedMetadata.length).toEqual(1); }); it('should reset metadata version history', () => { const explorationMetadata = new ExplorationMetadata( - 'title', 'category', 'objective', 'en', - [], '', '', 55, 'Introduction', - new ParamSpecs({}, paramSpecObjectFactory), [], false, true + 'title', + 'category', + 'objective', + 'en', + [], + '', + '', + 55, + 'Introduction', + new ParamSpecs({}, paramSpecObjectFactory), + [], + false, + true ); versionHistoryService.insertMetadataVersionHistoryData( - 3, explorationMetadata, ''); + 3, + explorationMetadata, + '' + ); expect(versionHistoryService.fetchedMetadata.length).toEqual(1); @@ -76,43 +105,43 @@ describe('Version history service', () => { classifier_model_id: null, content: { content_id: 'content', - html: '' + html: '', }, recorded_voiceovers: { voiceovers_mapping: { content: {}, - default_outcome: {} - } + default_outcome: {}, + }, }, interaction: { answer_groups: [], confirmed_unclassified_answers: [], customization_args: { rows: { - value: 1 + value: 1, }, placeholder: { value: { unicode_str: 'Type your answer here.', - content_id: '' - } - } + content_id: '', + }, + }, }, default_outcome: { dest: '(untitled state)', dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: '' + html: '', }, param_changes: [], labelled_as_correct: false, refresher_exploration_id: null, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, hints: [], solution: null, - id: 'TextInput' + id: 'TextInput', }, linked_skill_id: null, next_content_id_index: 0, @@ -124,12 +153,14 @@ describe('Version history service', () => { content: {}, default_outcome: {}, hint_1: {}, - rule_input_2: {} - } - } + rule_input_2: {}, + }, + }, }; const stateData = stateObjectFactory.createFromBackendDict( - 'State', stateObject); + 'State', + stateObject + ); versionHistoryService.insertStateVersionHistoryData(3, stateData, ''); expect(versionHistoryService.fetchedStateData.length).toEqual(1); @@ -144,43 +175,43 @@ describe('Version history service', () => { classifier_model_id: null, content: { content_id: 'content', - html: '' + html: '', }, recorded_voiceovers: { voiceovers_mapping: { content: {}, - default_outcome: {} - } + default_outcome: {}, + }, }, interaction: { answer_groups: [], confirmed_unclassified_answers: [], customization_args: { rows: { - value: 1 + value: 1, }, placeholder: { value: { unicode_str: 'Type your answer here.', - content_id: '' - } - } + content_id: '', + }, + }, }, default_outcome: { dest: '(untitled state)', dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: '' + html: '', }, param_changes: [], labelled_as_correct: false, refresher_exploration_id: null, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, hints: [], solution: null, - id: 'TextInput' + id: 'TextInput', }, linked_skill_id: null, next_content_id_index: 0, @@ -192,12 +223,14 @@ describe('Version history service', () => { content: {}, default_outcome: {}, hint_1: {}, - rule_input_2: {} - } - } + rule_input_2: {}, + }, + }, }; const stateData = stateObjectFactory.createFromBackendDict( - 'State', stateObject); + 'State', + stateObject + ); versionHistoryService.insertStateVersionHistoryData(3, stateData, ''); expect(versionHistoryService.fetchedStateData.length).toEqual(1); @@ -207,106 +240,122 @@ describe('Version history service', () => { expect(versionHistoryService.fetchedStateData.length).toEqual(0); }); - it('should find whether new metadata version history data should be fetched', - () => { - const explorationMetadata = new ExplorationMetadata( - 'title', 'category', 'objective', 'en', - [], '', '', 55, 'Introduction', - new ParamSpecs({}, paramSpecObjectFactory), [], false, true - ); - versionHistoryService.insertMetadataVersionHistoryData( - 3, explorationMetadata, ''); - - - expect( - versionHistoryService.shouldFetchNewMetadataVersionHistory() - ).toBeTrue(); - - versionHistoryService.insertMetadataVersionHistoryData( - 4, explorationMetadata, ''); - versionHistoryService.insertMetadataVersionHistoryData( - 5, explorationMetadata, ''); - - expect( - versionHistoryService.shouldFetchNewMetadataVersionHistory() - ).toBeFalse(); - }); - - it('should find whether new state version history data should be fetched', - () => { - const stateObject = { - classifier_model_id: null, - content: { - content_id: 'content', - html: '' - }, - recorded_voiceovers: { - voiceovers_mapping: { - content: {}, - default_outcome: {} - } + it('should find whether new metadata version history data should be fetched', () => { + const explorationMetadata = new ExplorationMetadata( + 'title', + 'category', + 'objective', + 'en', + [], + '', + '', + 55, + 'Introduction', + new ParamSpecs({}, paramSpecObjectFactory), + [], + false, + true + ); + versionHistoryService.insertMetadataVersionHistoryData( + 3, + explorationMetadata, + '' + ); + + expect( + versionHistoryService.shouldFetchNewMetadataVersionHistory() + ).toBeTrue(); + + versionHistoryService.insertMetadataVersionHistoryData( + 4, + explorationMetadata, + '' + ); + versionHistoryService.insertMetadataVersionHistoryData( + 5, + explorationMetadata, + '' + ); + + expect( + versionHistoryService.shouldFetchNewMetadataVersionHistory() + ).toBeFalse(); + }); + + it('should find whether new state version history data should be fetched', () => { + const stateObject = { + classifier_model_id: null, + content: { + content_id: 'content', + html: '', + }, + recorded_voiceovers: { + voiceovers_mapping: { + content: {}, + default_outcome: {}, }, - interaction: { - answer_groups: [], - confirmed_unclassified_answers: [], - customization_args: { - rows: { - value: 1 - }, - placeholder: { - value: { - unicode_str: 'Type your answer here.', - content_id: '' - } - } + }, + interaction: { + answer_groups: [], + confirmed_unclassified_answers: [], + customization_args: { + rows: { + value: 1, }, - default_outcome: { - dest: '(untitled state)', - dest_if_really_stuck: null, - feedback: { - content_id: 'default_outcome', - html: '' + placeholder: { + value: { + unicode_str: 'Type your answer here.', + content_id: '', }, - param_changes: [], - labelled_as_correct: false, - refresher_exploration_id: null, - missing_prerequisite_skill_id: null }, - hints: [], - solution: null, - id: 'TextInput' }, - linked_skill_id: null, - next_content_id_index: 0, - param_changes: [], - solicit_answer_details: false, - card_is_checkpoint: false, - written_translations: { - translations_mapping: { - content: {}, - default_outcome: {}, - hint_1: {}, - rule_input_2: {} - } - } - }; - const stateData = stateObjectFactory.createFromBackendDict( - 'State', stateObject); - versionHistoryService.insertStateVersionHistoryData(3, stateData, ''); - - expect( - versionHistoryService.shouldFetchNewStateVersionHistory() - ).toBeTrue(); - - versionHistoryService.insertStateVersionHistoryData( - 4, stateData, ''); - versionHistoryService.insertStateVersionHistoryData( - 5, stateData, ''); - - expect( - versionHistoryService.shouldFetchNewStateVersionHistory() - ).toBeFalse(); - }); + default_outcome: { + dest: '(untitled state)', + dest_if_really_stuck: null, + feedback: { + content_id: 'default_outcome', + html: '', + }, + param_changes: [], + labelled_as_correct: false, + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + }, + hints: [], + solution: null, + id: 'TextInput', + }, + linked_skill_id: null, + next_content_id_index: 0, + param_changes: [], + solicit_answer_details: false, + card_is_checkpoint: false, + written_translations: { + translations_mapping: { + content: {}, + default_outcome: {}, + hint_1: {}, + rule_input_2: {}, + }, + }, + }; + const stateData = stateObjectFactory.createFromBackendDict( + 'State', + stateObject + ); + versionHistoryService.insertStateVersionHistoryData(3, stateData, ''); + + expect( + versionHistoryService.shouldFetchNewStateVersionHistory() + ).toBeTrue(); + + versionHistoryService.insertStateVersionHistoryData(4, stateData, ''); + versionHistoryService.insertStateVersionHistoryData(5, stateData, ''); + + expect( + versionHistoryService.shouldFetchNewStateVersionHistory() + ).toBeFalse(); + }); it('should get whether we should show backward state diff data', () => { expect(versionHistoryService.canShowBackwardStateDiffData()).toBeFalse(); @@ -315,43 +364,43 @@ describe('Version history service', () => { classifier_model_id: null, content: { content_id: 'content', - html: '' + html: '', }, recorded_voiceovers: { voiceovers_mapping: { content: {}, - default_outcome: {} - } + default_outcome: {}, + }, }, interaction: { answer_groups: [], confirmed_unclassified_answers: [], customization_args: { rows: { - value: 1 + value: 1, }, placeholder: { value: { unicode_str: 'Type your answer here.', - content_id: '' - } - } + content_id: '', + }, + }, }, default_outcome: { dest: '(untitled state)', dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: '' + html: '', }, param_changes: [], labelled_as_correct: false, refresher_exploration_id: null, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, hints: [], solution: null, - id: 'TextInput' + id: 'TextInput', }, linked_skill_id: null, next_content_id_index: 0, @@ -363,15 +412,16 @@ describe('Version history service', () => { content: {}, default_outcome: {}, hint_1: {}, - rule_input_2: {} - } - } + rule_input_2: {}, + }, + }, }; const stateData = stateObjectFactory.createFromBackendDict( - 'State', stateObject); + 'State', + stateObject + ); versionHistoryService.insertStateVersionHistoryData(3, stateData, ''); - versionHistoryService.insertStateVersionHistoryData( - 4, stateData, ''); + versionHistoryService.insertStateVersionHistoryData(4, stateData, ''); expect(versionHistoryService.canShowBackwardStateDiffData()).toBeTrue(); }); @@ -380,14 +430,30 @@ describe('Version history service', () => { expect(versionHistoryService.canShowBackwardMetadataDiffData()).toBeFalse(); const explorationMetadata = new ExplorationMetadata( - 'title', 'category', 'objective', 'en', - [], '', '', 55, 'Introduction', - new ParamSpecs({}, paramSpecObjectFactory), [], false, true + 'title', + 'category', + 'objective', + 'en', + [], + '', + '', + 55, + 'Introduction', + new ParamSpecs({}, paramSpecObjectFactory), + [], + false, + true ); versionHistoryService.insertMetadataVersionHistoryData( - 3, explorationMetadata, ''); + 3, + explorationMetadata, + '' + ); versionHistoryService.insertMetadataVersionHistoryData( - 4, explorationMetadata, ''); + 4, + explorationMetadata, + '' + ); expect(versionHistoryService.canShowBackwardMetadataDiffData()).toBeTrue(); }); @@ -399,43 +465,43 @@ describe('Version history service', () => { classifier_model_id: null, content: { content_id: 'content', - html: '' + html: '', }, recorded_voiceovers: { voiceovers_mapping: { content: {}, - default_outcome: {} - } + default_outcome: {}, + }, }, interaction: { answer_groups: [], confirmed_unclassified_answers: [], customization_args: { rows: { - value: 1 + value: 1, }, placeholder: { value: { unicode_str: 'Type your answer here.', - content_id: '' - } - } + content_id: '', + }, + }, }, default_outcome: { dest: '(untitled state)', dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: '' + html: '', }, param_changes: [], labelled_as_correct: false, refresher_exploration_id: null, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, hints: [], solution: null, - id: 'TextInput' + id: 'TextInput', }, linked_skill_id: null, next_content_id_index: 0, @@ -447,18 +513,17 @@ describe('Version history service', () => { content: {}, default_outcome: {}, hint_1: {}, - rule_input_2: {} - } - } + rule_input_2: {}, + }, + }, }; const stateData = stateObjectFactory.createFromBackendDict( - 'State', stateObject); - versionHistoryService.insertStateVersionHistoryData( - 3, stateData, ''); - versionHistoryService.insertStateVersionHistoryData( - 4, stateData, ''); - versionHistoryService.insertStateVersionHistoryData( - 5, stateData, ''); + 'State', + stateObject + ); + versionHistoryService.insertStateVersionHistoryData(3, stateData, ''); + versionHistoryService.insertStateVersionHistoryData(4, stateData, ''); + versionHistoryService.insertStateVersionHistoryData(5, stateData, ''); versionHistoryService.incrementCurrentPositionInStateVersionHistoryList(); versionHistoryService.incrementCurrentPositionInStateVersionHistoryList(); @@ -469,20 +534,37 @@ describe('Version history service', () => { expect(versionHistoryService.canShowForwardMetadataDiffData()).toBeFalse(); const explorationMetadata = new ExplorationMetadata( - 'title', 'category', 'objective', 'en', - [], '', '', 55, 'Introduction', - new ParamSpecs({}, paramSpecObjectFactory), [], false, true + 'title', + 'category', + 'objective', + 'en', + [], + '', + '', + 55, + 'Introduction', + new ParamSpecs({}, paramSpecObjectFactory), + [], + false, + true ); versionHistoryService.insertMetadataVersionHistoryData( - 3, explorationMetadata, ''); + 3, + explorationMetadata, + '' + ); versionHistoryService.insertMetadataVersionHistoryData( - 4, explorationMetadata, ''); + 4, + explorationMetadata, + '' + ); versionHistoryService.insertMetadataVersionHistoryData( - 5, explorationMetadata, ''); - versionHistoryService - .incrementCurrentPositionInMetadataVersionHistoryList(); - versionHistoryService - .incrementCurrentPositionInMetadataVersionHistoryList(); + 5, + explorationMetadata, + '' + ); + versionHistoryService.incrementCurrentPositionInMetadataVersionHistoryList(); + versionHistoryService.incrementCurrentPositionInMetadataVersionHistoryList(); expect(versionHistoryService.canShowForwardMetadataDiffData()).toBeTrue(); }); @@ -492,43 +574,43 @@ describe('Version history service', () => { classifier_model_id: null, content: { content_id: 'content', - html: '' + html: '', }, recorded_voiceovers: { voiceovers_mapping: { content: {}, - default_outcome: {} - } + default_outcome: {}, + }, }, interaction: { answer_groups: [], confirmed_unclassified_answers: [], customization_args: { rows: { - value: 1 + value: 1, }, placeholder: { value: { unicode_str: 'Type your answer here.', - content_id: '' - } - } + content_id: '', + }, + }, }, default_outcome: { dest: '(untitled state)', dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: '' + html: '', }, param_changes: [], labelled_as_correct: false, refresher_exploration_id: null, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, hints: [], solution: null, - id: 'TextInput' + id: 'TextInput', }, linked_skill_id: null, next_content_id_index: 0, @@ -540,16 +622,16 @@ describe('Version history service', () => { content: {}, default_outcome: {}, hint_1: {}, - rule_input_2: {} - } - } + rule_input_2: {}, + }, + }, }; const stateData = stateObjectFactory.createFromBackendDict( - 'State', stateObject); - versionHistoryService.insertStateVersionHistoryData( - 3, stateData, ''); - versionHistoryService.insertStateVersionHistoryData( - 2, stateData, ''); + 'State', + stateObject + ); + versionHistoryService.insertStateVersionHistoryData(3, stateData, ''); + versionHistoryService.insertStateVersionHistoryData(2, stateData, ''); const diffData = versionHistoryService.getBackwardStateDiffData(); expect(diffData.oldVersionNumber).toEqual(2); @@ -561,43 +643,43 @@ describe('Version history service', () => { classifier_model_id: null, content: { content_id: 'content', - html: '' + html: '', }, recorded_voiceovers: { voiceovers_mapping: { content: {}, - default_outcome: {} - } + default_outcome: {}, + }, }, interaction: { answer_groups: [], confirmed_unclassified_answers: [], customization_args: { rows: { - value: 1 + value: 1, }, placeholder: { value: { unicode_str: 'Type your answer here.', - content_id: '' - } - } + content_id: '', + }, + }, }, default_outcome: { dest: '(untitled state)', dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: '' + html: '', }, param_changes: [], labelled_as_correct: false, refresher_exploration_id: null, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, hints: [], solution: null, - id: 'TextInput' + id: 'TextInput', }, linked_skill_id: null, next_content_id_index: 0, @@ -609,18 +691,17 @@ describe('Version history service', () => { content: {}, default_outcome: {}, hint_1: {}, - rule_input_2: {} - } - } + rule_input_2: {}, + }, + }, }; const stateData = stateObjectFactory.createFromBackendDict( - 'State', stateObject); - versionHistoryService.insertStateVersionHistoryData( - 3, stateData, ''); - versionHistoryService.insertStateVersionHistoryData( - 2, stateData, ''); - versionHistoryService.insertStateVersionHistoryData( - 1, stateData, ''); + 'State', + stateObject + ); + versionHistoryService.insertStateVersionHistoryData(3, stateData, ''); + versionHistoryService.insertStateVersionHistoryData(2, stateData, ''); + versionHistoryService.insertStateVersionHistoryData(1, stateData, ''); versionHistoryService.incrementCurrentPositionInStateVersionHistoryList(); versionHistoryService.incrementCurrentPositionInStateVersionHistoryList(); const diffData = versionHistoryService.getForwardStateDiffData(); @@ -631,16 +712,35 @@ describe('Version history service', () => { it('should get backward metadata diff data', () => { const explorationMetadata = new ExplorationMetadata( - 'title', 'category', 'objective', 'en', - [], '', '', 55, 'Introduction', - new ParamSpecs({}, paramSpecObjectFactory), [], false, true + 'title', + 'category', + 'objective', + 'en', + [], + '', + '', + 55, + 'Introduction', + new ParamSpecs({}, paramSpecObjectFactory), + [], + false, + true ); versionHistoryService.insertMetadataVersionHistoryData( - 3, explorationMetadata, ''); + 3, + explorationMetadata, + '' + ); versionHistoryService.insertMetadataVersionHistoryData( - 2, explorationMetadata, ''); + 2, + explorationMetadata, + '' + ); versionHistoryService.insertMetadataVersionHistoryData( - 1, explorationMetadata, ''); + 1, + explorationMetadata, + '' + ); const diffData = versionHistoryService.getBackwardMetadataDiffData(); expect(diffData.oldVersionNumber).toEqual(2); @@ -649,20 +749,37 @@ describe('Version history service', () => { it('should get forward metadata diff data', () => { const explorationMetadata = new ExplorationMetadata( - 'title', 'category', 'objective', 'en', - [], '', '', 55, 'Introduction', - new ParamSpecs({}, paramSpecObjectFactory), [], false, true + 'title', + 'category', + 'objective', + 'en', + [], + '', + '', + 55, + 'Introduction', + new ParamSpecs({}, paramSpecObjectFactory), + [], + false, + true ); versionHistoryService.insertMetadataVersionHistoryData( - 3, explorationMetadata, ''); + 3, + explorationMetadata, + '' + ); versionHistoryService.insertMetadataVersionHistoryData( - 2, explorationMetadata, ''); + 2, + explorationMetadata, + '' + ); versionHistoryService.insertMetadataVersionHistoryData( - 1, explorationMetadata, ''); - versionHistoryService - .incrementCurrentPositionInMetadataVersionHistoryList(); - versionHistoryService - .incrementCurrentPositionInMetadataVersionHistoryList(); + 1, + explorationMetadata, + '' + ); + versionHistoryService.incrementCurrentPositionInMetadataVersionHistoryList(); + versionHistoryService.incrementCurrentPositionInMetadataVersionHistoryList(); const diffData = versionHistoryService.getForwardMetadataDiffData(); expect(diffData.oldVersionNumber).toEqual(2); @@ -677,60 +794,55 @@ describe('Version history service', () => { expect(versionHistoryService.getLatestVersionOfExploration()).toEqual(5); }); - it('should get and set current position in state version history list', - () => { - expect( - versionHistoryService - .getCurrentPositionInStateVersionHistoryList()).toEqual(0); + it('should get and set current position in state version history list', () => { + expect( + versionHistoryService.getCurrentPositionInStateVersionHistoryList() + ).toEqual(0); - versionHistoryService.setCurrentPositionInStateVersionHistoryList(2); + versionHistoryService.setCurrentPositionInStateVersionHistoryList(2); - expect( - versionHistoryService - .getCurrentPositionInStateVersionHistoryList()).toEqual(2); - }); + expect( + versionHistoryService.getCurrentPositionInStateVersionHistoryList() + ).toEqual(2); + }); - it('should get and set current position in metadata version history list', - () => { - expect( - versionHistoryService - .getCurrentPositionInMetadataVersionHistoryList()).toEqual(0); + it('should get and set current position in metadata version history list', () => { + expect( + versionHistoryService.getCurrentPositionInMetadataVersionHistoryList() + ).toEqual(0); - versionHistoryService.setCurrentPositionInMetadataVersionHistoryList(2); + versionHistoryService.setCurrentPositionInMetadataVersionHistoryList(2); - expect( - versionHistoryService - .getCurrentPositionInMetadataVersionHistoryList()).toEqual(2); - }); + expect( + versionHistoryService.getCurrentPositionInMetadataVersionHistoryList() + ).toEqual(2); + }); - it('should decrement current position in state version history list', - () => { - versionHistoryService.setCurrentPositionInStateVersionHistoryList(2); + it('should decrement current position in state version history list', () => { + versionHistoryService.setCurrentPositionInStateVersionHistoryList(2); - expect( - versionHistoryService - .getCurrentPositionInStateVersionHistoryList()).toEqual(2); + expect( + versionHistoryService.getCurrentPositionInStateVersionHistoryList() + ).toEqual(2); - versionHistoryService.decrementCurrentPositionInStateVersionHistoryList(); + versionHistoryService.decrementCurrentPositionInStateVersionHistoryList(); - expect( - versionHistoryService - .getCurrentPositionInStateVersionHistoryList()).toEqual(1); - }); + expect( + versionHistoryService.getCurrentPositionInStateVersionHistoryList() + ).toEqual(1); + }); - it('should decrement current position in metadata version history list', - () => { - versionHistoryService.setCurrentPositionInMetadataVersionHistoryList(2); + it('should decrement current position in metadata version history list', () => { + versionHistoryService.setCurrentPositionInMetadataVersionHistoryList(2); - expect( - versionHistoryService - .getCurrentPositionInMetadataVersionHistoryList()).toEqual(2); + expect( + versionHistoryService.getCurrentPositionInMetadataVersionHistoryList() + ).toEqual(2); - versionHistoryService - .decrementCurrentPositionInMetadataVersionHistoryList(); + versionHistoryService.decrementCurrentPositionInMetadataVersionHistoryList(); - expect( - versionHistoryService - .getCurrentPositionInMetadataVersionHistoryList()).toEqual(1); - }); + expect( + versionHistoryService.getCurrentPositionInMetadataVersionHistoryList() + ).toEqual(1); + }); }); diff --git a/core/templates/pages/exploration-editor-page/services/version-history.service.ts b/core/templates/pages/exploration-editor-page/services/version-history.service.ts index ba22b3b92169..4ce1ea4a37a8 100644 --- a/core/templates/pages/exploration-editor-page/services/version-history.service.ts +++ b/core/templates/pages/exploration-editor-page/services/version-history.service.ts @@ -16,10 +16,10 @@ * @fileoverview Service for the exploration states version history. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { ExplorationMetadata } from 'domain/exploration/ExplorationMetadataObjectFactory'; -import { State } from 'domain/state/StateObjectFactory'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {ExplorationMetadata} from 'domain/exploration/ExplorationMetadataObjectFactory'; +import {State} from 'domain/state/StateObjectFactory'; export interface StateDiffData { oldState: State | null; @@ -38,7 +38,7 @@ export interface MetadataDiffData { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class VersionHistoryService { // The following value is null when the service is not initialized i.e. @@ -53,10 +53,10 @@ export class VersionHistoryService { // fetching the 'previously edited versions' of a state or exploration // metadata, the backend returns the version number as 'None' when we // reach the end of the version history. - fetchedStateVersionNumbers: (number | null) [] = []; - fetchedMetadataVersionNumbers: (number | null) [] = []; - fetchedStateData: (State | null) [] = []; - fetchedMetadata: (ExplorationMetadata | null) [] = []; + fetchedStateVersionNumbers: (number | null)[] = []; + fetchedMetadataVersionNumbers: (number | null)[] = []; + fetchedStateData: (State | null)[] = []; + fetchedMetadata: (ExplorationMetadata | null)[] = []; fetchedCommitterUsernames: string[] = []; @@ -94,7 +94,7 @@ export class VersionHistoryService { shouldFetchNewStateVersionHistory(): boolean { if ( this.currentPositionInStateVersionHistoryList < - this.fetchedStateVersionNumbers.length - 2 + this.fetchedStateVersionNumbers.length - 2 ) { return false; } @@ -104,7 +104,7 @@ export class VersionHistoryService { shouldFetchNewMetadataVersionHistory(): boolean { if ( this.currentPositionInMetadataVersionHistoryList < - this.fetchedMetadataVersionNumbers.length - 2 + this.fetchedMetadataVersionNumbers.length - 2 ) { return false; } @@ -112,9 +112,9 @@ export class VersionHistoryService { } insertMetadataVersionHistoryData( - versionNumber: number | null, - metadata: ExplorationMetadata | null, - committerUsername: string + versionNumber: number | null, + metadata: ExplorationMetadata | null, + committerUsername: string ): void { // If the version number already exists, then don't update the list. if (this.fetchedMetadataVersionNumbers.at(-1) === versionNumber) { @@ -126,9 +126,9 @@ export class VersionHistoryService { } insertStateVersionHistoryData( - versionNumber: number | null, - stateData: State | null, - committerUsername: string + versionNumber: number | null, + stateData: State | null, + committerUsername: string ): void { // If the version number already exists, then don't update the list. if (this.fetchedStateVersionNumbers.at(-1) === versionNumber) { @@ -145,9 +145,11 @@ export class VersionHistoryService { this.currentPositionInStateVersionHistoryList < this.fetchedStateVersionNumbers.length - 1 && this.fetchedStateVersionNumbers[ - this.currentPositionInStateVersionHistoryList + 1] !== null && + this.currentPositionInStateVersionHistoryList + 1 + ] !== null && this.fetchedStateData[ - this.currentPositionInStateVersionHistoryList + 1] !== null + this.currentPositionInStateVersionHistoryList + 1 + ] !== null ); } @@ -157,49 +159,65 @@ export class VersionHistoryService { this.currentPositionInStateVersionHistoryList < this.fetchedStateVersionNumbers.length && this.fetchedStateVersionNumbers[ - this.currentPositionInStateVersionHistoryList - 1] !== null && + this.currentPositionInStateVersionHistoryList - 1 + ] !== null && this.fetchedStateVersionNumbers[ - this.currentPositionInStateVersionHistoryList - 2] !== null && + this.currentPositionInStateVersionHistoryList - 2 + ] !== null && this.fetchedStateData[ - this.currentPositionInStateVersionHistoryList - 1] !== null && + this.currentPositionInStateVersionHistoryList - 1 + ] !== null && this.fetchedStateData[ - this.currentPositionInStateVersionHistoryList - 2] !== null + this.currentPositionInStateVersionHistoryList - 2 + ] !== null ); } getBackwardStateDiffData(): StateDiffData { return { - oldState: this.fetchedStateData[ - this.currentPositionInStateVersionHistoryList + 1], - newState: this.fetchedStateData[ - this.currentPositionInStateVersionHistoryList], - oldVersionNumber: ( + oldState: + this.fetchedStateData[ + this.currentPositionInStateVersionHistoryList + 1 + ], + newState: + this.fetchedStateData[this.currentPositionInStateVersionHistoryList], + oldVersionNumber: this.fetchedStateVersionNumbers[ - this.currentPositionInStateVersionHistoryList + 1]), - newVersionNumber: ( + this.currentPositionInStateVersionHistoryList + 1 + ], + newVersionNumber: this.fetchedStateVersionNumbers[ - this.currentPositionInStateVersionHistoryList]), - committerUsername: ( + this.currentPositionInStateVersionHistoryList + ], + committerUsername: this.fetchedCommitterUsernames[ - this.currentPositionInStateVersionHistoryList + 1]) + this.currentPositionInStateVersionHistoryList + 1 + ], }; } getForwardStateDiffData(): StateDiffData { return { - oldState: this.fetchedStateData[ - this.currentPositionInStateVersionHistoryList - 1], - newState: this.fetchedStateData[ - this.currentPositionInStateVersionHistoryList - 2], - oldVersionNumber: ( + oldState: + this.fetchedStateData[ + this.currentPositionInStateVersionHistoryList - 1 + ], + newState: + this.fetchedStateData[ + this.currentPositionInStateVersionHistoryList - 2 + ], + oldVersionNumber: this.fetchedStateVersionNumbers[ - this.currentPositionInStateVersionHistoryList - 1]), - newVersionNumber: ( + this.currentPositionInStateVersionHistoryList - 1 + ], + newVersionNumber: this.fetchedStateVersionNumbers[ - this.currentPositionInStateVersionHistoryList - 2]), - committerUsername: ( + this.currentPositionInStateVersionHistoryList - 2 + ], + committerUsername: this.fetchedCommitterUsernames[ - this.currentPositionInStateVersionHistoryList - 1]) + this.currentPositionInStateVersionHistoryList - 1 + ], }; } @@ -209,9 +227,11 @@ export class VersionHistoryService { this.currentPositionInMetadataVersionHistoryList < this.fetchedMetadataVersionNumbers.length - 1 && this.fetchedMetadataVersionNumbers[ - this.currentPositionInMetadataVersionHistoryList + 1] !== null && + this.currentPositionInMetadataVersionHistoryList + 1 + ] !== null && this.fetchedMetadata[ - this.currentPositionInMetadataVersionHistoryList + 1] !== null + this.currentPositionInMetadataVersionHistoryList + 1 + ] !== null ); } @@ -221,49 +241,65 @@ export class VersionHistoryService { this.currentPositionInMetadataVersionHistoryList < this.fetchedMetadataVersionNumbers.length && this.fetchedMetadataVersionNumbers[ - this.currentPositionInMetadataVersionHistoryList - 1] !== null && + this.currentPositionInMetadataVersionHistoryList - 1 + ] !== null && this.fetchedMetadataVersionNumbers[ - this.currentPositionInMetadataVersionHistoryList - 2] !== null && + this.currentPositionInMetadataVersionHistoryList - 2 + ] !== null && this.fetchedMetadata[ - this.currentPositionInMetadataVersionHistoryList - 1] !== null && + this.currentPositionInMetadataVersionHistoryList - 1 + ] !== null && this.fetchedMetadata[ - this.currentPositionInMetadataVersionHistoryList - 2] !== null + this.currentPositionInMetadataVersionHistoryList - 2 + ] !== null ); } getBackwardMetadataDiffData(): MetadataDiffData { return { - oldMetadata: this.fetchedMetadata[ - this.currentPositionInMetadataVersionHistoryList + 1], - newMetadata: this.fetchedMetadata[ - this.currentPositionInMetadataVersionHistoryList], - oldVersionNumber: ( + oldMetadata: + this.fetchedMetadata[ + this.currentPositionInMetadataVersionHistoryList + 1 + ], + newMetadata: + this.fetchedMetadata[this.currentPositionInMetadataVersionHistoryList], + oldVersionNumber: this.fetchedMetadataVersionNumbers[ - this.currentPositionInMetadataVersionHistoryList + 1]), - newVersionNumber: ( + this.currentPositionInMetadataVersionHistoryList + 1 + ], + newVersionNumber: this.fetchedMetadataVersionNumbers[ - this.currentPositionInMetadataVersionHistoryList]), - committerUsername: ( + this.currentPositionInMetadataVersionHistoryList + ], + committerUsername: this.fetchedCommitterUsernames[ - this.currentPositionInMetadataVersionHistoryList + 1]) + this.currentPositionInMetadataVersionHistoryList + 1 + ], }; } getForwardMetadataDiffData(): MetadataDiffData { return { - oldMetadata: this.fetchedMetadata[ - this.currentPositionInMetadataVersionHistoryList - 1], - newMetadata: this.fetchedMetadata[ - this.currentPositionInMetadataVersionHistoryList - 2], - oldVersionNumber: ( + oldMetadata: + this.fetchedMetadata[ + this.currentPositionInMetadataVersionHistoryList - 1 + ], + newMetadata: + this.fetchedMetadata[ + this.currentPositionInMetadataVersionHistoryList - 2 + ], + oldVersionNumber: this.fetchedMetadataVersionNumbers[ - this.currentPositionInMetadataVersionHistoryList - 1]), - newVersionNumber: ( + this.currentPositionInMetadataVersionHistoryList - 1 + ], + newVersionNumber: this.fetchedMetadataVersionNumbers[ - this.currentPositionInMetadataVersionHistoryList - 2]), - committerUsername: ( + this.currentPositionInMetadataVersionHistoryList - 2 + ], + committerUsername: this.fetchedCommitterUsernames[ - this.currentPositionInMetadataVersionHistoryList - 1]) + this.currentPositionInMetadataVersionHistoryList - 1 + ], }; } @@ -280,20 +316,20 @@ export class VersionHistoryService { } setCurrentPositionInStateVersionHistoryList( - currentPositionInVersionHistoryList: number + currentPositionInVersionHistoryList: number ): void { - this.currentPositionInStateVersionHistoryList = ( - currentPositionInVersionHistoryList); + this.currentPositionInStateVersionHistoryList = + currentPositionInVersionHistoryList; } decrementCurrentPositionInStateVersionHistoryList(): void { - this.currentPositionInStateVersionHistoryList = ( - this.currentPositionInStateVersionHistoryList - 1); + this.currentPositionInStateVersionHistoryList = + this.currentPositionInStateVersionHistoryList - 1; } incrementCurrentPositionInStateVersionHistoryList(): void { - this.currentPositionInStateVersionHistoryList = ( - this.currentPositionInStateVersionHistoryList + 1); + this.currentPositionInStateVersionHistoryList = + this.currentPositionInStateVersionHistoryList + 1; } getCurrentPositionInMetadataVersionHistoryList(): number { @@ -301,23 +337,23 @@ export class VersionHistoryService { } setCurrentPositionInMetadataVersionHistoryList( - currentPositionInVersionHistoryList: number + currentPositionInVersionHistoryList: number ): void { - this.currentPositionInMetadataVersionHistoryList = ( - currentPositionInVersionHistoryList); + this.currentPositionInMetadataVersionHistoryList = + currentPositionInVersionHistoryList; } decrementCurrentPositionInMetadataVersionHistoryList(): void { - this.currentPositionInMetadataVersionHistoryList = ( - this.currentPositionInMetadataVersionHistoryList - 1); + this.currentPositionInMetadataVersionHistoryList = + this.currentPositionInMetadataVersionHistoryList - 1; } incrementCurrentPositionInMetadataVersionHistoryList(): void { - this.currentPositionInMetadataVersionHistoryList = ( - this.currentPositionInMetadataVersionHistoryList + 1); + this.currentPositionInMetadataVersionHistoryList = + this.currentPositionInMetadataVersionHistoryList + 1; } } -angular.module('oppia').factory( - 'VersionHistoryService', - downgradeInjectable(VersionHistoryService)); +angular + .module('oppia') + .factory('VersionHistoryService', downgradeInjectable(VersionHistoryService)); diff --git a/core/templates/pages/exploration-editor-page/services/versioned-exploration-caching.service.spec.ts b/core/templates/pages/exploration-editor-page/services/versioned-exploration-caching.service.spec.ts index 1423a2a0a46a..b569e9ca8eaa 100644 --- a/core/templates/pages/exploration-editor-page/services/versioned-exploration-caching.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/versioned-exploration-caching.service.spec.ts @@ -16,9 +16,9 @@ * @fileoverview Unit tests for versioned exploration caching service. */ -import { TestBed } from '@angular/core/testing'; -import { FetchExplorationBackendResponse } from 'domain/exploration/read-only-exploration-backend-api.service'; -import { VersionedExplorationCachingService } from './versioned-exploration-caching.service'; +import {TestBed} from '@angular/core/testing'; +import {FetchExplorationBackendResponse} from 'domain/exploration/read-only-exploration-backend-api.service'; +import {VersionedExplorationCachingService} from './versioned-exploration-caching.service'; describe('Versioned exploration caching service', () => { let versionedExplorationCachingService: VersionedExplorationCachingService; @@ -41,14 +41,14 @@ describe('Versioned exploration caching service', () => { param_changes: [], classifier_model_id: null, recorded_voiceovers: { - voiceovers_mapping: {} + voiceovers_mapping: {}, }, solicit_answer_details: true, card_is_checkpoint: true, linked_skill_id: null, content: { html: '', - content_id: 'content' + content_id: 'content', }, interaction: { customization_args: {}, @@ -61,17 +61,17 @@ describe('Versioned exploration caching service', () => { dest_if_really_stuck: null, feedback: { html: '', - content_id: 'content' + content_id: 'content', }, labelled_as_correct: true, refresher_exploration_id: 'exp', - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, confirmed_unclassified_answers: [], - id: null - } - } - } + id: null, + }, + }, + }, }, exploration_metadata: { title: 'Exploration', @@ -86,7 +86,7 @@ describe('Versioned exploration caching service', () => { param_specs: {}, param_changes: [], auto_tts_enabled: false, - edits_allowed: true + edits_allowed: true, }, version: 1, can_edit: true, @@ -98,15 +98,19 @@ describe('Versioned exploration caching service', () => { furthest_reached_checkpoint_exp_version: 1, furthest_reached_checkpoint_state_name: 'State A', most_recently_reached_checkpoint_state_name: 'State A', - most_recently_reached_checkpoint_exp_version: 1 + most_recently_reached_checkpoint_exp_version: 1, }; beforeEach(() => { versionedExplorationCachingService = TestBed.inject( - VersionedExplorationCachingService); + VersionedExplorationCachingService + ); versionedExplorationCachingService.cacheVersionedExplorationData( - 'exp_1', 1, testVersionedExplorationData); + 'exp_1', + 1, + testVersionedExplorationData + ); }); afterEach(() => { @@ -122,7 +126,10 @@ describe('Versioned exploration caching service', () => { expect(versionedExplorationCachingService.isCached('exp_1', 2)).toBeFalse(); versionedExplorationCachingService.cacheVersionedExplorationData( - 'exp_1', 2, testVersionedExplorationData); + 'exp_1', + 2, + testVersionedExplorationData + ); expect(versionedExplorationCachingService.isCached('exp_1', 2)).toBeTrue(); }); @@ -131,12 +138,17 @@ describe('Versioned exploration caching service', () => { expect(versionedExplorationCachingService.isCached('exp_1', 2)).toBeFalse(); versionedExplorationCachingService.cacheVersionedExplorationData( - 'exp_1', 2, testVersionedExplorationData); + 'exp_1', + 2, + testVersionedExplorationData + ); expect(versionedExplorationCachingService.isCached('exp_1', 2)).toBeTrue(); expect( - versionedExplorationCachingService - .retrieveCachedVersionedExplorationData('exp_1', 2) + versionedExplorationCachingService.retrieveCachedVersionedExplorationData( + 'exp_1', + 2 + ) ).toEqual(testVersionedExplorationData); }); @@ -144,12 +156,17 @@ describe('Versioned exploration caching service', () => { expect(versionedExplorationCachingService.isCached('exp_1', 2)).toBeFalse(); versionedExplorationCachingService.cacheVersionedExplorationData( - 'exp_1', 2, testVersionedExplorationData); + 'exp_1', + 2, + testVersionedExplorationData + ); expect(versionedExplorationCachingService.isCached('exp_1', 2)).toBeTrue(); - versionedExplorationCachingService - .removeCachedVersionedExplorationData('exp_1', 2); + versionedExplorationCachingService.removeCachedVersionedExplorationData( + 'exp_1', + 2 + ); expect(versionedExplorationCachingService.isCached('exp_1', 2)).toBeFalse(); }); diff --git a/core/templates/pages/exploration-editor-page/services/versioned-exploration-caching.service.ts b/core/templates/pages/exploration-editor-page/services/versioned-exploration-caching.service.ts index cdb72bc5c671..e3e2eb101ce9 100644 --- a/core/templates/pages/exploration-editor-page/services/versioned-exploration-caching.service.ts +++ b/core/templates/pages/exploration-editor-page/services/versioned-exploration-caching.service.ts @@ -19,9 +19,9 @@ * exploration versions. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { FetchExplorationBackendResponse } from 'domain/exploration/read-only-exploration-backend-api.service'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {FetchExplorationBackendResponse} from 'domain/exploration/read-only-exploration-backend-api.service'; export interface VersionedExplorationData { [explorationId: string]: { @@ -30,7 +30,7 @@ export interface VersionedExplorationData { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class VersionedExplorationCachingService { _versionedExplorationDataCache: VersionedExplorationData = {}; @@ -44,15 +44,16 @@ export class VersionedExplorationCachingService { isCached(explorationId: string, version: number): boolean { return ( this._versionedExplorationDataCache.hasOwnProperty(explorationId) && - this._versionedExplorationDataCache[explorationId].hasOwnProperty( - version)); + this._versionedExplorationDataCache[explorationId].hasOwnProperty(version) + ); } /** * Retrieves the cached exploration data for the given id and version. */ retrieveCachedVersionedExplorationData( - explorationId: string, version: number + explorationId: string, + version: number ): FetchExplorationBackendResponse { return this._versionedExplorationDataCache[explorationId][version]; } @@ -62,15 +63,16 @@ export class VersionedExplorationCachingService { * version. */ cacheVersionedExplorationData( - explorationId: string, version: number, - explorationData: FetchExplorationBackendResponse + explorationId: string, + version: number, + explorationData: FetchExplorationBackendResponse ): void { if (this._versionedExplorationDataCache.hasOwnProperty(explorationId)) { - this._versionedExplorationDataCache[explorationId][ - version] = explorationData; + this._versionedExplorationDataCache[explorationId][version] = + explorationData; } else { this._versionedExplorationDataCache[explorationId] = { - [version]: explorationData + [version]: explorationData, }; } } @@ -79,7 +81,8 @@ export class VersionedExplorationCachingService { * Removes the cached exploration data for the given id and version. */ removeCachedVersionedExplorationData( - explorationId: string, version: number + explorationId: string, + version: number ): void { if (this.isCached(explorationId, version)) { delete this._versionedExplorationDataCache[explorationId][version]; @@ -94,6 +97,9 @@ export class VersionedExplorationCachingService { } } -angular.module('oppia').factory( - 'VersionedExplorationCachingService', - downgradeInjectable(VersionedExplorationCachingService)); +angular + .module('oppia') + .factory( + 'VersionedExplorationCachingService', + downgradeInjectable(VersionedExplorationCachingService) + ); diff --git a/core/templates/pages/exploration-editor-page/settings-tab/settings-tab.component.spec.ts b/core/templates/pages/exploration-editor-page/settings-tab/settings-tab.component.spec.ts index d93f75429196..fb6a6e66cd56 100644 --- a/core/templates/pages/exploration-editor-page/settings-tab/settings-tab.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/settings-tab/settings-tab.component.spec.ts @@ -16,47 +16,58 @@ * @fileoverview Unit tests for settingsTab. */ -import { ChangeDetectorRef, EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, flush, flushMicrotasks, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { AlertsService } from 'services/alerts.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { UserExplorationPermissionsService } from 'pages/exploration-editor-page/services/user-exploration-permissions.service'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { ChangeListService } from '../services/change-list.service'; -import { ExplorationDataService } from '../services/exploration-data.service'; -import { ExplorationEditsAllowedBackendApiService } from '../services/exploration-edits-allowed-backend-api.service'; -import { EditabilityService } from 'services/editability.service'; -import { EditableExplorationBackendApiService } from 'domain/exploration/editable-exploration-backend-api.service'; -import { ContextService } from 'services/context.service'; -import { UserService } from 'services/user.service'; -import { ExplorationCategoryService } from '../services/exploration-category.service'; -import { ExplorationInitStateNameService } from '../services/exploration-init-state-name.service'; -import { ExplorationLanguageCodeService } from '../services/exploration-language-code.service'; -import { ExplorationObjectiveService } from '../services/exploration-objective.service'; -import { ExplorationRightsService } from '../services/exploration-rights.service'; -import { ExplorationStatesService } from '../services/exploration-states.service'; -import { ExplorationTagsService } from '../services/exploration-tags.service'; -import { ExplorationTitleService } from '../services/exploration-title.service'; -import { ExplorationWarningsService } from '../services/exploration-warnings.service'; -import { RouterService } from '../services/router.service'; -import { SettingTabBackendApiService } from '../services/setting-tab-backend-api.service'; -import { UserEmailPreferencesService } from '../services/user-email-preferences.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { SettingsTabComponent } from './settings-tab.component'; -import { UserInfo } from 'domain/user/user-info.model'; -import { ExplorationPermissions } from 'domain/exploration/exploration-permissions.model'; -import { State } from 'domain/state/StateObjectFactory'; -import { MatChipInputEvent } from '@angular/material/chips'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { MetadataDiffData, VersionHistoryService } from '../services/version-history.service'; -import { ExplorationMetadata } from 'domain/exploration/ExplorationMetadataObjectFactory'; -import { ParamSpecs } from 'domain/exploration/ParamSpecsObjectFactory'; -import { ParamSpecObjectFactory } from 'domain/exploration/ParamSpecObjectFactory'; -import { VersionHistoryBackendApiService } from '../services/version-history-backend-api.service'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { Interaction } from 'domain/exploration/InteractionObjectFactory'; -import { RecordedVoiceovers } from 'domain/exploration/recorded-voiceovers.model'; +import {ChangeDetectorRef, EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + flush, + flushMicrotasks, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {AlertsService} from 'services/alerts.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {UserExplorationPermissionsService} from 'pages/exploration-editor-page/services/user-exploration-permissions.service'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {ChangeListService} from '../services/change-list.service'; +import {ExplorationDataService} from '../services/exploration-data.service'; +import {ExplorationEditsAllowedBackendApiService} from '../services/exploration-edits-allowed-backend-api.service'; +import {EditabilityService} from 'services/editability.service'; +import {EditableExplorationBackendApiService} from 'domain/exploration/editable-exploration-backend-api.service'; +import {ContextService} from 'services/context.service'; +import {UserService} from 'services/user.service'; +import {ExplorationCategoryService} from '../services/exploration-category.service'; +import {ExplorationInitStateNameService} from '../services/exploration-init-state-name.service'; +import {ExplorationLanguageCodeService} from '../services/exploration-language-code.service'; +import {ExplorationObjectiveService} from '../services/exploration-objective.service'; +import {ExplorationRightsService} from '../services/exploration-rights.service'; +import {ExplorationStatesService} from '../services/exploration-states.service'; +import {ExplorationTagsService} from '../services/exploration-tags.service'; +import {ExplorationTitleService} from '../services/exploration-title.service'; +import {ExplorationWarningsService} from '../services/exploration-warnings.service'; +import {RouterService} from '../services/router.service'; +import {SettingTabBackendApiService} from '../services/setting-tab-backend-api.service'; +import {UserEmailPreferencesService} from '../services/user-email-preferences.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {SettingsTabComponent} from './settings-tab.component'; +import {UserInfo} from 'domain/user/user-info.model'; +import {ExplorationPermissions} from 'domain/exploration/exploration-permissions.model'; +import {State} from 'domain/state/StateObjectFactory'; +import {MatChipInputEvent} from '@angular/material/chips'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import { + MetadataDiffData, + VersionHistoryService, +} from '../services/version-history.service'; +import {ExplorationMetadata} from 'domain/exploration/ExplorationMetadataObjectFactory'; +import {ParamSpecs} from 'domain/exploration/ParamSpecsObjectFactory'; +import {ParamSpecObjectFactory} from 'domain/exploration/ParamSpecObjectFactory'; +import {VersionHistoryBackendApiService} from '../services/version-history-backend-api.service'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {Interaction} from 'domain/exploration/InteractionObjectFactory'; +import {RecordedVoiceovers} from 'domain/exploration/recorded-voiceovers.model'; describe('Settings Tab Component', () => { let component: SettingsTabComponent; @@ -65,8 +76,7 @@ describe('Settings Tab Component', () => { let changeListService: ChangeListService; let explorationDataService: ExplorationDataService; let contextService: ContextService; - let editableExplorationBackendApiService: - EditableExplorationBackendApiService; + let editableExplorationBackendApiService: EditableExplorationBackendApiService; let explorationCategoryService: ExplorationCategoryService; let explorationInitStateNameService: ExplorationInitStateNameService; let explorationLanguageCodeService: ExplorationLanguageCodeService; @@ -91,7 +101,7 @@ describe('Settings Tab Component', () => { canModifyRoles: true, canReleaseOwnership: true, canUnpublish: true, - canManageVoiceArtist: true + canManageVoiceArtist: true, }; let mockExplorationTagsServiceonPropertyChanged = new EventEmitter(); let mockEventEmitterRouterService = new EventEmitter(); @@ -107,7 +117,7 @@ describe('Settings Tab Component', () => { class MockNgbModal { open() { return { - result: Promise.resolve() + result: Promise.resolve(), }; } } @@ -119,78 +129,77 @@ describe('Settings Tab Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - SettingsTabComponent - ], + declarations: [SettingsTabComponent], providers: [ { provide: ChangeDetectorRef, - useClass: MockChangeDetectorRef + useClass: MockChangeDetectorRef, }, { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, { provide: ExplorationDataService, useValue: { explorationId: explorationId, data: { - param_changes: [] - }, - getDataAsync: () => Promise.resolve({ - draft_change_list_id: 3, - version: undefined, - draft_changes: [], - is_version_of_draft_valid: true, - init_state_name: 'init', param_changes: [], - param_specs: {randomProp: {obj_type: 'randomVal'}}, - states: {}, - title: 'Test Exploration', - language_code: 'en', - exploration_metadata: { - title: 'Exploration', - category: 'Algebra', - objective: 'To learn', - language_code: 'en', - tags: [], - blurb: '', - author_notes: '', - states_schema_version: 50, - init_state_name: 'Introduction', - param_specs: {}, + }, + getDataAsync: () => + Promise.resolve({ + draft_change_list_id: 3, + version: undefined, + draft_changes: [], + is_version_of_draft_valid: true, + init_state_name: 'init', param_changes: [], - auto_tts_enabled: false, - edits_allowed: true - } - }), + param_specs: {randomProp: {obj_type: 'randomVal'}}, + states: {}, + title: 'Test Exploration', + language_code: 'en', + exploration_metadata: { + title: 'Exploration', + category: 'Algebra', + objective: 'To learn', + language_code: 'en', + tags: [], + blurb: '', + author_notes: '', + states_schema_version: 50, + init_state_name: 'Introduction', + param_specs: {}, + param_changes: [], + auto_tts_enabled: false, + edits_allowed: true, + }, + }), autosaveChangeListAsync() { return; - } - } + }, + }, }, WindowDimensionsService, ExplorationTitleService, FocusManagerService, { provide: RouterService, - useClass: MockRouterService - } + useClass: MockRouterService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); - beforeEach(() => { fixture = TestBed.createComponent(SettingsTabComponent); component = fixture.componentInstance; alertsService = TestBed.inject(AlertsService); changeListService = TestBed.inject(ChangeListService); - userExplorationPermissionsService = ( - TestBed.inject(UserExplorationPermissionsService)); + userExplorationPermissionsService = TestBed.inject( + UserExplorationPermissionsService + ); windowRef = TestBed.inject(WindowRef); ngbModal = TestBed.inject(NgbModal); eeabas = TestBed.inject(ExplorationEditsAllowedBackendApiService); @@ -199,228 +208,242 @@ describe('Settings Tab Component', () => { windowDimensionsService = TestBed.inject(WindowDimensionsService); explorationDataService = TestBed.inject(ExplorationDataService); contextService = TestBed.inject(ContextService); - settingTabBackendApiService = TestBed.inject( - SettingTabBackendApiService); + settingTabBackendApiService = TestBed.inject(SettingTabBackendApiService); editableExplorationBackendApiService = TestBed.inject( - EditableExplorationBackendApiService); + EditableExplorationBackendApiService + ); explorationCategoryService = TestBed.inject(ExplorationCategoryService); explorationInitStateNameService = TestBed.inject( - ExplorationInitStateNameService); + ExplorationInitStateNameService + ); explorationLanguageCodeService = TestBed.inject( - ExplorationLanguageCodeService); - explorationObjectiveService = TestBed.inject( - ExplorationObjectiveService); + ExplorationLanguageCodeService + ); + explorationObjectiveService = TestBed.inject(ExplorationObjectiveService); explorationRightsService = TestBed.inject(ExplorationRightsService); explorationStatesService = TestBed.inject(ExplorationStatesService); explorationTagsService = TestBed.inject(ExplorationTagsService); explorationTitleService = TestBed.inject(ExplorationTitleService); explorationWarningsService = TestBed.inject(ExplorationWarningsService); - userEmailPreferencesService = TestBed.inject( - UserEmailPreferencesService); + userEmailPreferencesService = TestBed.inject(UserEmailPreferencesService); userService = TestBed.inject(UserService); versionHistoryService = TestBed.inject(VersionHistoryService); paramSpecObjectFactory = TestBed.inject(ParamSpecObjectFactory); versionHistoryBackendApiService = TestBed.inject( - VersionHistoryBackendApiService); + VersionHistoryBackendApiService + ); - spyOn(explorationTagsService, 'onExplorationPropertyChanged') - .and.returnValue(mockExplorationTagsServiceonPropertyChanged); + spyOn( + explorationTagsService, + 'onExplorationPropertyChanged' + ).and.returnValue(mockExplorationTagsServiceonPropertyChanged); spyOn(contextService, 'getExplorationId').and.returnValue(explorationId); - spyOn(userExplorationPermissionsService, 'getPermissionsAsync').and - .returnValue(Promise.resolve(userPermissions as ExplorationPermissions)); - spyOn(userExplorationPermissionsService, 'fetchPermissionsAsync').and - .returnValue(Promise.resolve(userPermissions as ExplorationPermissions)); + spyOn( + userExplorationPermissionsService, + 'getPermissionsAsync' + ).and.returnValue( + Promise.resolve(userPermissions as ExplorationPermissions) + ); + spyOn( + userExplorationPermissionsService, + 'fetchPermissionsAsync' + ).and.returnValue( + Promise.resolve(userPermissions as ExplorationPermissions) + ); spyOn(explorationStatesService, 'isInitialized').and.returnValue(true); spyOn(explorationStatesService, 'getStateNames').and.returnValue([ - 'Introduction']); - spyOn(userService, 'getUserInfoAsync').and.returnValue(Promise.resolve({ - getUsername: () => 'username1', - isSuperAdmin: () => true - } as UserInfo)); + 'Introduction', + ]); + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve({ + getUsername: () => 'username1', + isSuperAdmin: () => true, + } as UserInfo) + ); explorationCategoryService.init('Astrology'); spyOnProperty( - userExplorationPermissionsService, 'onUserExplorationPermissionsFetched') - .and.returnValue(mockEventEmitteruserExplorationPermissionsService); + userExplorationPermissionsService, + 'onUserExplorationPermissionsFetched' + ).and.returnValue(mockEventEmitteruserExplorationPermissionsService); spyOn( - versionHistoryService, 'getLatestVersionOfExploration' + versionHistoryService, + 'getLatestVersionOfExploration' ).and.returnValue(3); fixture.detectChanges(); }); - it('should initialize controller properties after its initialization', - fakeAsync(() => { - component.ngOnInit(); - mockExplorationTagsServiceonPropertyChanged.emit(); - tick(600); - jasmine.createSpy('hasState').and.stub(); - - expect(component.isRolesFormOpen).toBe(false); - expect(component.canDelete).toBe(true); - expect(component.canModifyRoles).toBe(true); - expect(component.canReleaseOwnership).toBe(true); - expect(component.canUnpublish).toBe(true); - expect(component.explorationId).toBe(explorationId); - expect(component.canManageVoiceArtist).toBe(true); - expect(component.loggedInUser).toBe('username1'); - - mockEventEmitterRouterService.emit(); - tick(600); - - expect(component.CATEGORY_LIST_FOR_SELECT2[0]).toEqual({ - id: 'Astrology', - text: 'Astrology' - }); + it('should initialize controller properties after its initialization', fakeAsync(() => { + component.ngOnInit(); + mockExplorationTagsServiceonPropertyChanged.emit(); + tick(600); + jasmine.createSpy('hasState').and.stub(); - component.filterChoices('English'); - component.updateCategoryListWithUserData(); - expect(component.newCategory).toEqual({ - id: 'English', - text: 'English', - }); + expect(component.isRolesFormOpen).toBe(false); + expect(component.canDelete).toBe(true); + expect(component.canModifyRoles).toBe(true); + expect(component.canReleaseOwnership).toBe(true); + expect(component.canUnpublish).toBe(true); + expect(component.explorationId).toBe(explorationId); + expect(component.canManageVoiceArtist).toBe(true); + expect(component.loggedInUser).toBe('username1'); - component.filterChoices(''); + mockEventEmitterRouterService.emit(); + tick(600); - explorationTagsService.displayed = ['name']; - component.add({ - value: 'shivam', - input: { - value: '' - } - } as MatChipInputEvent); - component.remove('shivam'); - expect(component.stateNames).toEqual(['Introduction']); - expect(component.hasPageLoaded).toBe(true); + expect(component.CATEGORY_LIST_FOR_SELECT2[0]).toEqual({ + id: 'Astrology', + text: 'Astrology', + }); - flush(); - })); + component.filterChoices('English'); + component.updateCategoryListWithUserData(); + expect(component.newCategory).toEqual({ + id: 'English', + text: 'English', + }); - it('should not create new category if it is not selected', - fakeAsync(() => { - spyOn(explorationCategoryService, 'saveDisplayedValue').and.stub(); + component.filterChoices(''); - explorationCategoryService.displayed = 'old'; - component.newCategory = { - id: 'new', - text: 'new' - }; + explorationTagsService.displayed = ['name']; + component.add({ + value: 'shivam', + input: { + value: '', + }, + } as MatChipInputEvent); + component.remove('shivam'); + expect(component.stateNames).toEqual(['Introduction']); + expect(component.hasPageLoaded).toBe(true); - component.updateCategoryListWithUserData(); - tick(); + flush(); + })); - expect(explorationCategoryService.displayed).toBe('old'); - expect(explorationCategoryService.saveDisplayedValue).toHaveBeenCalled(); - })); + it('should not create new category if it is not selected', fakeAsync(() => { + spyOn(explorationCategoryService, 'saveDisplayedValue').and.stub(); - it('should be able to create new category if it is selected', - fakeAsync(() => { - spyOn(explorationCategoryService, 'saveDisplayedValue').and.stub(); + explorationCategoryService.displayed = 'old'; + component.newCategory = { + id: 'new', + text: 'new', + }; - explorationCategoryService.displayed = 'new'; - component.newCategory = { - id: 'new', - text: 'new' - }; + component.updateCategoryListWithUserData(); + tick(); - component.updateCategoryListWithUserData(); - tick(); + expect(explorationCategoryService.displayed).toBe('old'); + expect(explorationCategoryService.saveDisplayedValue).toHaveBeenCalled(); + })); - expect(explorationCategoryService.displayed).toBe('new'); - expect(explorationCategoryService.saveDisplayedValue).toHaveBeenCalled(); - })); + it('should be able to create new category if it is selected', fakeAsync(() => { + spyOn(explorationCategoryService, 'saveDisplayedValue').and.stub(); - it('should be able to add exploration editor tags', - fakeAsync(() => { - spyOn(component, 'saveExplorationTags').and.stub(); - explorationTagsService.displayed = []; + explorationCategoryService.displayed = 'new'; + component.newCategory = { + id: 'new', + text: 'new', + }; - component.add({ - value: 'name', - input: { - value: '' - } - } as MatChipInputEvent); - tick(); + component.updateCategoryListWithUserData(); + tick(); - expect(explorationTagsService.displayed).toEqual(['name']); - })); + expect(explorationCategoryService.displayed).toBe('new'); + expect(explorationCategoryService.saveDisplayedValue).toHaveBeenCalled(); + })); - it('should not add same exploration editor tags' + - 'when user enter same tag again', fakeAsync(() => { + it('should be able to add exploration editor tags', fakeAsync(() => { spyOn(component, 'saveExplorationTags').and.stub(); explorationTagsService.displayed = []; component.add({ value: 'name', input: { - value: '' - } - } as MatChipInputEvent); - tick(); - - expect(explorationTagsService.displayed).toEqual(['name']); - - // When user try to enter same tag again. - component.add({ - value: 'name', - input: { - value: '' - } + value: '', + }, } as MatChipInputEvent); tick(); expect(explorationTagsService.displayed).toEqual(['name']); })); - it('should be able to add multiple exploration editor tags', + it( + 'should not add same exploration editor tags' + + 'when user enter same tag again', fakeAsync(() => { spyOn(component, 'saveExplorationTags').and.stub(); explorationTagsService.displayed = []; component.add({ - value: 'tag-one', + value: 'name', input: { - value: '' - } + value: '', + }, } as MatChipInputEvent); tick(); - component.add({ - value: 'tag-two', - input: { - value: '' - } - } as MatChipInputEvent); - tick(); + expect(explorationTagsService.displayed).toEqual(['name']); + // When user try to enter same tag again. component.add({ - value: 'tag-three', + value: 'name', input: { - value: '' - } + value: '', + }, } as MatChipInputEvent); tick(); - expect(explorationTagsService.displayed).toEqual( - ['tag-one', 'tag-two', 'tag-three'] - ); - })); + expect(explorationTagsService.displayed).toEqual(['name']); + }) + ); - it('should be able to remove multiple exploration editor tags', - fakeAsync(() => { - spyOn(component, 'saveExplorationTags').and.stub(); - component.explorationTags = ['tag-one', 'tag-two', 'tag-three']; - explorationTagsService.displayed = ['tag-one', 'tag-two', 'tag-three']; + it('should be able to add multiple exploration editor tags', fakeAsync(() => { + spyOn(component, 'saveExplorationTags').and.stub(); + explorationTagsService.displayed = []; - component.remove('tag-two'); - tick(); + component.add({ + value: 'tag-one', + input: { + value: '', + }, + } as MatChipInputEvent); + tick(); - component.remove('tag-three'); - tick(); + component.add({ + value: 'tag-two', + input: { + value: '', + }, + } as MatChipInputEvent); + tick(); + + component.add({ + value: 'tag-three', + input: { + value: '', + }, + } as MatChipInputEvent); + tick(); + + expect(explorationTagsService.displayed).toEqual([ + 'tag-one', + 'tag-two', + 'tag-three', + ]); + })); + + it('should be able to remove multiple exploration editor tags', fakeAsync(() => { + spyOn(component, 'saveExplorationTags').and.stub(); + component.explorationTags = ['tag-one', 'tag-two', 'tag-three']; + explorationTagsService.displayed = ['tag-one', 'tag-two', 'tag-three']; + + component.remove('tag-two'); + tick(); - expect(explorationTagsService.displayed).toEqual( - ['tag-one']); - })); + component.remove('tag-three'); + tick(); + + expect(explorationTagsService.displayed).toEqual(['tag-one']); + })); it('should be able to remove exploration editor tags', fakeAsync(() => { spyOn(component, 'saveExplorationTags').and.stub(); @@ -429,14 +452,14 @@ describe('Settings Tab Component', () => { component.add({ value: 'first', input: { - value: '' - } + value: '', + }, } as MatChipInputEvent); component.add({ value: 'second', input: { - value: '' - } + value: '', + }, } as MatChipInputEvent); component.remove('second'); @@ -448,10 +471,12 @@ describe('Settings Tab Component', () => { spyOnProperty(windowRef, 'nativeWindow').and.returnValue({ location: { protocol: 'https:', - host: 'oppia.org' - } + host: 'oppia.org', + }, } as Window); - expect(component.getExplorePageUrl()).toBe('https://oppia.org/explore/exp1'); + expect(component.getExplorePageUrl()).toBe( + 'https://oppia.org/explore/exp1' + ); }); it('should save exploration title', () => { @@ -464,15 +489,15 @@ describe('Settings Tab Component', () => { expect(component.isTitlePresent()).toBe(true); }); - it('should save exploration language code', () => { spyOn(explorationLanguageCodeService, 'saveDisplayedValue'); explorationLanguageCodeService.init('hi-en'); component.saveExplorationLanguageCode(); - expect(explorationLanguageCodeService.saveDisplayedValue) - .toHaveBeenCalled(); + expect( + explorationLanguageCodeService.saveDisplayedValue + ).toHaveBeenCalled(); }); it('should save exploration objective', () => { @@ -494,78 +519,87 @@ describe('Settings Tab Component', () => { expect(explorationTagsService.saveDisplayedValue).toHaveBeenCalled(); })); - it('should not save exploration init state name if it\'s invalid', - () => { - explorationInitStateNameService.init('First State'); - // This throws "Argument of type 'null' is not assignable to - // parameter of type 'state'." We need to suppress this error - // because of the need to test validations. This throws an - // error because the state name is invalid. - // @ts-ignore - spyOn(explorationStatesService, 'getState').and.returnValue(null); - spyOn(alertsService, 'addWarning'); - - component.saveExplorationInitStateName(); - expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Invalid initial state name: First State'); - }); + it("should not save exploration init state name if it's invalid", () => { + explorationInitStateNameService.init('First State'); + // This throws "Argument of type 'null' is not assignable to + // parameter of type 'state'." We need to suppress this error + // because of the need to test validations. This throws an + // error because the state name is invalid. + // @ts-ignore + spyOn(explorationStatesService, 'getState').and.returnValue(null); + spyOn(alertsService, 'addWarning'); + + component.saveExplorationInitStateName(); + expect(alertsService.addWarning).toHaveBeenCalledWith( + 'Invalid initial state name: First State' + ); + }); - it('should save exploration init state name successfully and refresh graph', - () => { - explorationInitStateNameService.init('Introduction'); - spyOn(explorationStatesService, 'getState').and.returnValue( - new State( - null, null, null, {} as SubtitledHtml, {} as Interaction, - [], {} as RecordedVoiceovers, false, false)); - spyOn(explorationInitStateNameService, 'saveDisplayedValue'); + it('should save exploration init state name successfully and refresh graph', () => { + explorationInitStateNameService.init('Introduction'); + spyOn(explorationStatesService, 'getState').and.returnValue( + new State( + null, + null, + null, + {} as SubtitledHtml, + {} as Interaction, + [], + {} as RecordedVoiceovers, + false, + false + ) + ); + spyOn(explorationInitStateNameService, 'saveDisplayedValue'); - component.saveExplorationInitStateName(); + component.saveExplorationInitStateName(); - expect(explorationInitStateNameService.saveDisplayedValue) - .toHaveBeenCalled(); - }); + expect( + explorationInitStateNameService.saveDisplayedValue + ).toHaveBeenCalled(); + }); - it('should delete exploration when closing delete exploration modal', - fakeAsync(() => { - spyOn(editableExplorationBackendApiService, 'deleteExplorationAsync') - .and.returnValue(Promise.resolve()); - spyOnProperty(windowRef, 'nativeWindow').and.returnValue({ - location: { - reload: () => {} - } - } as Window); - - component.deleteExploration(); - tick(); + it('should delete exploration when closing delete exploration modal', fakeAsync(() => { + spyOn( + editableExplorationBackendApiService, + 'deleteExplorationAsync' + ).and.returnValue(Promise.resolve()); + spyOnProperty(windowRef, 'nativeWindow').and.returnValue({ + location: { + reload: () => {}, + }, + } as Window); - expect(windowRef.nativeWindow.location).toBe('/creator-dashboard'); - })); + component.deleteExploration(); + tick(); - it('should not delete exploration when dismissing delete exploration modal', - fakeAsync(() => { - spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ - result: Promise.reject() - } as NgbModalRef); - }); - spyOn(alertsService, 'clearWarnings'); - spyOnProperty(windowRef, 'nativeWindow').and.returnValue({ - location: '' - } as unknown as Window); + expect(windowRef.nativeWindow.location).toBe('/creator-dashboard'); + })); - component.deleteExploration(); - tick(); + it('should not delete exploration when dismissing delete exploration modal', fakeAsync(() => { + spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + result: Promise.reject(), + } as NgbModalRef; + }); + spyOn(alertsService, 'clearWarnings'); + spyOnProperty(windowRef, 'nativeWindow').and.returnValue({ + location: '', + } as unknown as Window); - expect(alertsService.clearWarnings).toHaveBeenCalled(); - expect(windowRef.nativeWindow.location).toBe(''); - })); + component.deleteExploration(); + tick(); + + expect(alertsService.clearWarnings).toHaveBeenCalled(); + expect(windowRef.nativeWindow.location).toBe(''); + })); it('should open a modal when removeRole is called', fakeAsync(() => { spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ + return { componentInstance: NgbModalRef, - result: Promise.resolve() - } as NgbModalRef); + result: Promise.resolve(), + } as NgbModalRef; }); component.removeRole('username', 'editor'); @@ -576,126 +610,127 @@ describe('Settings Tab Component', () => { it('should remove role when resolving remove-role-modal', fakeAsync(() => { spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ + return { componentInstance: NgbModalRef, - result: Promise.resolve() - } as NgbModalRef); + result: Promise.resolve(), + } as NgbModalRef; }); - spyOn(explorationRightsService, 'removeRoleAsync').and - .returnValue(Promise.resolve()); + spyOn(explorationRightsService, 'removeRoleAsync').and.returnValue( + Promise.resolve() + ); component.removeRole('username', 'editor'); tick(); - expect( - explorationRightsService.removeRoleAsync).toHaveBeenCalled(); + expect(explorationRightsService.removeRoleAsync).toHaveBeenCalled(); })); - it('should not remove role when rejecting remove-role-modal', - fakeAsync(() => { - spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ - componentInstance: NgbModalRef, - result: Promise.reject() - } as NgbModalRef); - }); - spyOn(explorationRightsService, 'removeRoleAsync'); + it('should not remove role when rejecting remove-role-modal', fakeAsync(() => { + spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + componentInstance: NgbModalRef, + result: Promise.reject(), + } as NgbModalRef; + }); + spyOn(explorationRightsService, 'removeRoleAsync'); - component.removeRole('username1', 'editor'); - tick(); + component.removeRole('username1', 'editor'); + tick(); - expect( - explorationRightsService.removeRoleAsync).not.toHaveBeenCalled(); - })); + expect(explorationRightsService.removeRoleAsync).not.toHaveBeenCalled(); + })); - it('should remove role when user accept remove-role-modal', - fakeAsync(() => { - spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ - componentInstance: NgbModalRef, - result: Promise.resolve() - } as NgbModalRef); - }); - spyOn(explorationRightsService, 'removeRoleAsync') - .and.returnValue(Promise.resolve()); + it('should remove role when user accept remove-role-modal', fakeAsync(() => { + spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + componentInstance: NgbModalRef, + result: Promise.resolve(), + } as NgbModalRef; + }); + spyOn(explorationRightsService, 'removeRoleAsync').and.returnValue( + Promise.resolve() + ); - component.removeRole('username1', 'editor'); - tick(); + component.removeRole('username1', 'editor'); + tick(); - expect( - explorationRightsService.removeRoleAsync).toHaveBeenCalled(); - })); + expect(explorationRightsService.removeRoleAsync).toHaveBeenCalled(); + })); it('should open a modal when removeVoiceArtist is called', fakeAsync(() => { spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ + return { componentInstance: NgbModalRef, - result: Promise.resolve() - } as NgbModalRef); + result: Promise.resolve(), + } as NgbModalRef; }); - spyOn(explorationRightsService, 'removeVoiceArtistRoleAsync') - .and.returnValue(Promise.resolve()); + spyOn( + explorationRightsService, + 'removeVoiceArtistRoleAsync' + ).and.returnValue(Promise.resolve()); component.removeVoiceArtist('username'); tick(); expect(ngbModal.open).toHaveBeenCalled(); - expect(explorationRightsService.removeVoiceArtistRoleAsync) - .toHaveBeenCalled(); + expect( + explorationRightsService.removeVoiceArtistRoleAsync + ).toHaveBeenCalled(); })); - it('should remove voice artist when resolving remove-role-modal', - fakeAsync(() => { - spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ - componentInstance: NgbModalRef, - result: Promise.resolve() - } as NgbModalRef); - }); - spyOn(explorationRightsService, 'removeVoiceArtistRoleAsync').and - .returnValue(Promise.resolve()); + it('should remove voice artist when resolving remove-role-modal', fakeAsync(() => { + spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + componentInstance: NgbModalRef, + result: Promise.resolve(), + } as NgbModalRef; + }); + spyOn( + explorationRightsService, + 'removeVoiceArtistRoleAsync' + ).and.returnValue(Promise.resolve()); - component.removeVoiceArtist('username'); - tick(); + component.removeVoiceArtist('username'); + tick(); - expect( - explorationRightsService.removeVoiceArtistRoleAsync) - .toHaveBeenCalledWith('username'); - })); + expect( + explorationRightsService.removeVoiceArtistRoleAsync + ).toHaveBeenCalledWith('username'); + })); - it('should not remove voice artist when rejecting remove-role-modal', - fakeAsync(() => { - spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ - componentInstance: NgbModalRef, - result: Promise.reject() - } as NgbModalRef); - }); - spyOn(explorationRightsService, 'removeVoiceArtistRoleAsync'); + it('should not remove voice artist when rejecting remove-role-modal', fakeAsync(() => { + spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + componentInstance: NgbModalRef, + result: Promise.reject(), + } as NgbModalRef; + }); + spyOn(explorationRightsService, 'removeVoiceArtistRoleAsync'); - component.removeVoiceArtist('username'); - tick(); + component.removeVoiceArtist('username'); + tick(); - expect( - explorationRightsService.removeVoiceArtistRoleAsync) - .not.toHaveBeenCalled(); - })); + expect( + explorationRightsService.removeVoiceArtistRoleAsync + ).not.toHaveBeenCalled(); + })); it('should open a modal when reassignRole is called', fakeAsync(() => { spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ + return { componentInstance: NgbModalRef, - result: Promise.resolve() - } as NgbModalRef); + result: Promise.resolve(), + } as NgbModalRef; }); component.openEditRolesForm(); - explorationRightsService.init( - ['owner'], [], [], [], '', '', false, true); + explorationRightsService.init(['owner'], [], [], [], '', '', false, true); - spyOn(explorationRightsService, 'checkUserAlreadyHasRoles') - .and.returnValue(true); + spyOn(explorationRightsService, 'checkUserAlreadyHasRoles').and.returnValue( + true + ); spyOn(explorationRightsService, 'saveRoleChanges').and.returnValue( - Promise.resolve()); + Promise.resolve() + ); component.editRole('Username1', 'editor'); tick(); component.editRole('Username1', 'owner'); @@ -704,223 +739,234 @@ describe('Settings Tab Component', () => { expect(ngbModal.open).toHaveBeenCalled(); })); - it('should reassign role when resolving reassign-role-modal', - fakeAsync(() => { - spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ - componentInstance: NgbModalRef, - result: Promise.resolve() - } as NgbModalRef); - }); - - component.openEditRolesForm(); - explorationRightsService.init( - ['owner'], [], [], [], '', '', false, true); - - spyOn(explorationRightsService, 'checkUserAlreadyHasRoles') - .and.returnValue(false); - spyOn(explorationRightsService, 'saveRoleChanges').and.returnValue( - Promise.resolve()); - - component.editRole('Username1', 'editor'); - tick(); - component.editRole('Username1', 'owner'); - tick(); - - expect(explorationRightsService.saveRoleChanges).toHaveBeenCalledWith( - 'Username1', 'owner'); - })); - - it('should not reassign role when rejecting remove-role-modal', fakeAsync( - () => { - spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ - componentInstance: NgbModalRef, - result: Promise.reject() - } as NgbModalRef); - }); - - explorationRightsService.init( - ['owner'], [], [], [], '', '', false, true); - - spyOn(explorationRightsService, 'checkUserAlreadyHasRoles') - .and.returnValue(true); - spyOn(explorationRightsService, 'saveRoleChanges').and.returnValue( - Promise.resolve()); - component.editRole('Username1', 'editor'); - tick(); - component.editRole('Username1', 'owner'); - tick(); + it('should reassign role when resolving reassign-role-modal', fakeAsync(() => { + spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + componentInstance: NgbModalRef, + result: Promise.resolve(), + } as NgbModalRef; + }); - expect( - explorationRightsService.saveRoleChanges).not.toHaveBeenCalled(); - })); + component.openEditRolesForm(); + explorationRightsService.init(['owner'], [], [], [], '', '', false, true); - it('should transfer exploration ownership when closing transfer ownership' + - ' modal', fakeAsync(() => { - spyOn(explorationRightsService, 'makeCommunityOwned').and.returnValue( + spyOn(explorationRightsService, 'checkUserAlreadyHasRoles').and.returnValue( + false + ); + spyOn(explorationRightsService, 'saveRoleChanges').and.returnValue( Promise.resolve() ); - component.showTransferExplorationOwnershipModal(); + component.editRole('Username1', 'editor'); + tick(); + component.editRole('Username1', 'owner'); tick(); - expect(explorationRightsService.makeCommunityOwned).toHaveBeenCalled(); + expect(explorationRightsService.saveRoleChanges).toHaveBeenCalledWith( + 'Username1', + 'owner' + ); })); - it('should not transfer exploration ownership when dismissing transfer' + - ' ownership modal', fakeAsync(() => { + it('should not reassign role when rejecting remove-role-modal', fakeAsync(() => { spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ + return { componentInstance: NgbModalRef, - result: Promise.reject() - } as NgbModalRef); + result: Promise.reject(), + } as NgbModalRef; }); - spyOn(alertsService, 'clearWarnings'); - component.showTransferExplorationOwnershipModal(); + explorationRightsService.init(['owner'], [], [], [], '', '', false, true); + + spyOn(explorationRightsService, 'checkUserAlreadyHasRoles').and.returnValue( + true + ); + spyOn(explorationRightsService, 'saveRoleChanges').and.returnValue( + Promise.resolve() + ); + component.editRole('Username1', 'editor'); + tick(); + component.editRole('Username1', 'owner'); tick(); - expect(alertsService.clearWarnings).toHaveBeenCalled(); + expect(explorationRightsService.saveRoleChanges).not.toHaveBeenCalled(); })); - it('should open preview summary tile modal with ngbModal', + it( + 'should transfer exploration ownership when closing transfer ownership' + + ' modal', fakeAsync(() => { - spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.resolve() - } as NgbModalRef); - component.previewSummaryTile(); - tick(); - - expect(ngbModal.open).toHaveBeenCalled(); - })); - - it('should clear alerts warning when dismissing preview summary tile modal', - () => { - spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.reject() - } as NgbModalRef); - spyOn(alertsService, 'clearWarnings'); + spyOn(explorationRightsService, 'makeCommunityOwned').and.returnValue( + Promise.resolve() + ); - component.previewSummaryTile(); + component.showTransferExplorationOwnershipModal(); + tick(); - expect(alertsService.clearWarnings).toHaveBeenCalled(); - }); + expect(explorationRightsService.makeCommunityOwned).toHaveBeenCalled(); + }) + ); - it('should open preview summary tile modal with ngbModal', + it( + 'should not transfer exploration ownership when dismissing transfer' + + ' ownership modal', fakeAsync(() => { spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ + return { componentInstance: NgbModalRef, - result: Promise.resolve() - } as NgbModalRef); + result: Promise.reject(), + } as NgbModalRef; }); - spyOn(settingTabBackendApiService, 'getData') - .and.returnValue(Promise.resolve({ - draft_email_body: 'Draf message' - })); + spyOn(alertsService, 'clearWarnings'); - component.unpublishExplorationAsModerator(); + component.showTransferExplorationOwnershipModal(); tick(); - expect(ngbModal.open).toHaveBeenCalled(); - })); + expect(alertsService.clearWarnings).toHaveBeenCalled(); + }) + ); - it('should save moderator changes to backend when closing preview summary' + - ' tile modal', fakeAsync(() => { + it('should open preview summary tile modal with ngbModal', fakeAsync(() => { + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.resolve(), + } as NgbModalRef); + component.previewSummaryTile(); + tick(); + + expect(ngbModal.open).toHaveBeenCalled(); + })); + + it('should clear alerts warning when dismissing preview summary tile modal', () => { + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.reject(), + } as NgbModalRef); + spyOn(alertsService, 'clearWarnings'); + + component.previewSummaryTile(); + + expect(alertsService.clearWarnings).toHaveBeenCalled(); + }); + + it('should open preview summary tile modal with ngbModal', fakeAsync(() => { spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ + return { componentInstance: NgbModalRef, - result: Promise.resolve() - } as NgbModalRef); + result: Promise.resolve(), + } as NgbModalRef; }); - spyOn(explorationRightsService, 'saveModeratorChangeToBackendAsync').and - .callFake((emailBody) => { - return Promise.resolve(); - }); - spyOn( - settingTabBackendApiService, 'getData') - .and.returnValue(Promise.resolve({ - draft_email_body: 'Draf message' - })); + spyOn(settingTabBackendApiService, 'getData').and.returnValue( + Promise.resolve({ + draft_email_body: 'Draf message', + }) + ); - component.canUnpublish = false; - component.canReleaseOwnership = false; component.unpublishExplorationAsModerator(); tick(); - expect(explorationRightsService.saveModeratorChangeToBackendAsync) - .toHaveBeenCalled(); - expect(userExplorationPermissionsService.fetchPermissionsAsync) - .toHaveBeenCalled(); - expect(component.canUnpublish).toBe(true); - expect(component.canReleaseOwnership).toBe(true); + expect(ngbModal.open).toHaveBeenCalled(); })); - it('should clear alerts warning when dismissing preview summary tile modal', + it( + 'should save moderator changes to backend when closing preview summary' + + ' tile modal', fakeAsync(() => { spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ + return { componentInstance: NgbModalRef, - result: Promise.reject() - } as NgbModalRef); + result: Promise.resolve(), + } as NgbModalRef; }); - spyOn(alertsService, 'clearWarnings'); spyOn( - settingTabBackendApiService, 'getData') - .and.returnValue(Promise.resolve({ - draft_email_body: 'Draf message' - })); + explorationRightsService, + 'saveModeratorChangeToBackendAsync' + ).and.callFake(emailBody => { + return Promise.resolve(); + }); + spyOn(settingTabBackendApiService, 'getData').and.returnValue( + Promise.resolve({ + draft_email_body: 'Draf message', + }) + ); + component.canUnpublish = false; + component.canReleaseOwnership = false; component.unpublishExplorationAsModerator(); tick(); - expect(alertsService.clearWarnings).toHaveBeenCalled(); - })); + expect( + explorationRightsService.saveModeratorChangeToBackendAsync + ).toHaveBeenCalled(); + expect( + userExplorationPermissionsService.fetchPermissionsAsync + ).toHaveBeenCalled(); + expect(component.canUnpublish).toBe(true); + expect(component.canReleaseOwnership).toBe(true); + }) + ); + + it('should clear alerts warning when dismissing preview summary tile modal', fakeAsync(() => { + spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + componentInstance: NgbModalRef, + result: Promise.reject(), + } as NgbModalRef; + }); + spyOn(alertsService, 'clearWarnings'); + spyOn(settingTabBackendApiService, 'getData').and.returnValue( + Promise.resolve({ + draft_email_body: 'Draf message', + }) + ); + + component.unpublishExplorationAsModerator(); + tick(); + + expect(alertsService.clearWarnings).toHaveBeenCalled(); + })); it('should toggle notifications', () => { let feedbackNotificationsSpy = spyOn( - userEmailPreferencesService, 'setFeedbackNotificationPreferences') - .and.callFake((mute: boolean, callb: () => void) => { - callb(); - }); + userEmailPreferencesService, + 'setFeedbackNotificationPreferences' + ).and.callFake((mute: boolean, callb: () => void) => { + callb(); + }); let suggestionNotificationsSpy = spyOn( - userEmailPreferencesService, 'setSuggestionNotificationPreferences') - .and.callFake((mute: boolean, callb: () => void) => { - callb(); - }); + userEmailPreferencesService, + 'setSuggestionNotificationPreferences' + ).and.callFake((mute: boolean, callb: () => void) => { + callb(); + }); component.muteFeedbackNotifications(); - expect(feedbackNotificationsSpy) - .toHaveBeenCalled(); + expect(feedbackNotificationsSpy).toHaveBeenCalled(); component.unmuteFeedbackNotifications(); - expect(feedbackNotificationsSpy) - .toHaveBeenCalled(); + expect(feedbackNotificationsSpy).toHaveBeenCalled(); component.muteSuggestionNotifications(); - expect(suggestionNotificationsSpy) - .toHaveBeenCalled(); + expect(suggestionNotificationsSpy).toHaveBeenCalled(); component.unmuteSuggestionNotifications(); - expect(suggestionNotificationsSpy) - .toHaveBeenCalled(); + expect(suggestionNotificationsSpy).toHaveBeenCalled(); }); it('should open edit roles form and edit username and role', () => { component.openEditRolesForm(); - explorationRightsService.init( - ['owner'], [], [], [], '', '', false, true); + explorationRightsService.init(['owner'], [], [], [], '', '', false, true); expect(component.isRolesFormOpen).toBe(true); expect(component.newMemberUsername).toBe(''); expect(component.newMemberRole.value).toBe('owner'); spyOn(explorationRightsService, 'saveRoleChanges').and.returnValue( - Promise.resolve()); + Promise.resolve() + ); component.editRole('Username1', 'editor'); expect(explorationRightsService.saveRoleChanges).toHaveBeenCalledWith( - 'Username1', 'editor'); + 'Username1', + 'editor' + ); expect(component.isRolesFormOpen).toBe(false); }); @@ -940,18 +986,20 @@ describe('Settings Tab Component', () => { it('should open voice artist edit roles form and edit username', () => { component.openVoiceoverRolesForm(); - explorationRightsService.init( - ['owner'], [], [], [], '', '', false, true); + explorationRightsService.init(['owner'], [], [], [], '', '', false, true); expect(component.isVoiceoverFormOpen).toBe(true); expect(component.newVoiceArtistUsername).toBe(''); - spyOn(explorationRightsService, 'assignVoiceArtistRoleAsync') - .and.returnValue(Promise.resolve()); + spyOn( + explorationRightsService, + 'assignVoiceArtistRoleAsync' + ).and.returnValue(Promise.resolve()); component.editVoiceArtist('Username1'); - expect(explorationRightsService.assignVoiceArtistRoleAsync) - .toHaveBeenCalledWith('Username1'); + expect( + explorationRightsService.assignVoiceArtistRoleAsync + ).toHaveBeenCalledWith('Username1'); expect(component.isVoiceoverFormOpen).toBe(false); }); @@ -981,7 +1029,8 @@ describe('Settings Tab Component', () => { it('should evaluate when edits are allowed', fakeAsync(() => { spyOn(eeabas, 'setEditsAllowed').and.callFake( - async(unusedValue, unusedId, cb) => cb()); + async (unusedValue, unusedId, cb) => cb() + ); spyOn(editabilityService, 'lockExploration'); component.enableEdits(); tick(); @@ -992,7 +1041,8 @@ describe('Settings Tab Component', () => { it('should evaluate when edits are not allowed', fakeAsync(() => { spyOn(eeabas, 'setEditsAllowed').and.callFake( - async(unusedValue, unusedId, cb) => cb()); + async (unusedValue, unusedId, cb) => cb() + ); spyOn(editabilityService, 'lockExploration'); component.disableEdits(); tick(); @@ -1003,7 +1053,9 @@ describe('Settings Tab Component', () => { it('should check if exploration is locked for editing', () => { let changeListSpy = spyOn( - changeListService, 'isExplorationLockedForEditing'); + changeListService, + 'isExplorationLockedForEditing' + ); changeListSpy.and.returnValue(true); expect(component.isExplorationLockedForEditing()).toBe(true); @@ -1012,33 +1064,37 @@ describe('Settings Tab Component', () => { expect(component.isExplorationLockedForEditing()).toBe(false); }); - it('should check edit-modal has been open ' + - 'when editRole function has been called', fakeAsync(() => { - spyOn(ngbModal, 'open').and.returnValue({ - componentInstance: NgbModalRef, - result: Promise.resolve() - } as NgbModalRef); + it( + 'should check edit-modal has been open ' + + 'when editRole function has been called', + fakeAsync(() => { + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: NgbModalRef, + result: Promise.resolve(), + } as NgbModalRef); - component.openEditRolesForm(); - expect(component.isRolesFormOpen).toEqual(true); - tick(); - explorationRightsService.init( - ['owner'], [], [], [], '', '', false, true); - tick(); - spyOn(explorationRightsService, 'checkUserAlreadyHasRoles') - .and.returnValue(true); - spyOn(explorationRightsService, 'saveRoleChanges').and.returnValue( - Promise.resolve() - ); - component.editRole('Username1', 'editor'); - tick(); - component.editRole('Username1', 'owner'); - tick(); + component.openEditRolesForm(); + expect(component.isRolesFormOpen).toEqual(true); + tick(); + explorationRightsService.init(['owner'], [], [], [], '', '', false, true); + tick(); + spyOn( + explorationRightsService, + 'checkUserAlreadyHasRoles' + ).and.returnValue(true); + spyOn(explorationRightsService, 'saveRoleChanges').and.returnValue( + Promise.resolve() + ); + component.editRole('Username1', 'editor'); + tick(); + component.editRole('Username1', 'owner'); + tick(); - expect(ngbModal.open).toHaveBeenCalled(); - expect(explorationRightsService.saveRoleChanges).toHaveBeenCalled(); - expect(component.isRolesFormOpen).toEqual(false); - })); + expect(ngbModal.open).toHaveBeenCalled(); + expect(explorationRightsService.saveRoleChanges).toHaveBeenCalled(); + expect(component.isRolesFormOpen).toEqual(false); + }) + ); it('should update warnings when save param changes hook', () => { spyOn(explorationWarningsService, 'updateWarnings'); @@ -1051,7 +1107,7 @@ describe('Settings Tab Component', () => { let paramChangeBackendDict = { customization_args: { parse_with_jinja: false, - value: 'test value' + value: 'test value', }, generator_id: '123', name: 'test', @@ -1069,72 +1125,78 @@ describe('Settings Tab Component', () => { it('should disable save button when exploration title is empty', () => { component.newMemberUsername = 'newUser'; component.rolesSaveButtonEnabled = true; - spyOn(explorationRightsService, 'checkUserAlreadyHasRoles') - .and.returnValue(false); + spyOn(explorationRightsService, 'checkUserAlreadyHasRoles').and.returnValue( + false + ); explorationTitleService.init(''); component.saveExplorationTitle(); expect(component.rolesSaveButtonEnabled).toBe(false); expect(component.errorMessage).toBe( - 'Please provide a title before inviting.'); + 'Please provide a title before inviting.' + ); }); - it('should disable save button when adding same role to existing users.', - () => { - component.openEditRolesForm(); - component.rolesSaveButtonEnabled = true; - explorationRightsService.init( - ['Username1'], [], [], [], '', 'false', false, true); - component.newMemberUsername = 'Username1'; - explorationTitleService.init('Exploration title'); - component.saveExplorationTitle(); - spyOn(explorationRightsService, 'getOldRole') - .and.returnValue('owner'); - - component.onRolesFormUsernameBlur(); - - expect(component.rolesSaveButtonEnabled).toBe(false); - expect(component.errorMessage).toBe('User is already owner.'); - }); + it('should disable save button when adding same role to existing users.', () => { + component.openEditRolesForm(); + component.rolesSaveButtonEnabled = true; + explorationRightsService.init( + ['Username1'], + [], + [], + [], + '', + 'false', + false, + true + ); + component.newMemberUsername = 'Username1'; + explorationTitleService.init('Exploration title'); + component.saveExplorationTitle(); + spyOn(explorationRightsService, 'getOldRole').and.returnValue('owner'); - it('should disable save button when adding another role to itself', - () => { - component.newMemberUsername = component.loggedInUser as string; - component.rolesSaveButtonEnabled = true; - explorationTitleService.init('Exploration title'); - component.saveExplorationTitle(); + component.onRolesFormUsernameBlur(); + + expect(component.rolesSaveButtonEnabled).toBe(false); + expect(component.errorMessage).toBe('User is already owner.'); + }); - spyOn(explorationRightsService, 'checkUserAlreadyHasRoles') - .and.returnValue(true); + it('should disable save button when adding another role to itself', () => { + component.newMemberUsername = component.loggedInUser as string; + component.rolesSaveButtonEnabled = true; + explorationTitleService.init('Exploration title'); + component.saveExplorationTitle(); - component.onRolesFormUsernameBlur(); + spyOn(explorationRightsService, 'checkUserAlreadyHasRoles').and.returnValue( + true + ); - expect(component.rolesSaveButtonEnabled).toBe(false); - expect(component.errorMessage).toBe( - 'Users are not allowed to assign other roles to themselves.'); - }); + component.onRolesFormUsernameBlur(); + + expect(component.rolesSaveButtonEnabled).toBe(false); + expect(component.errorMessage).toBe( + 'Users are not allowed to assign other roles to themselves.' + ); + }); it('should enable save button when tile and username are valid', () => { component.newMemberUsername = 'newUser'; - component.newMemberRole = { name: 'owner', value: 'owner'}; + component.newMemberRole = {name: 'owner', value: 'owner'}; component.rolesSaveButtonEnabled = true; explorationTitleService.init('Exploration title'); component.saveExplorationTitle(); - spyOn(explorationRightsService, 'getOldRole') - .and.returnValue('editor'); + spyOn(explorationRightsService, 'getOldRole').and.returnValue('editor'); expect(component.rolesSaveButtonEnabled).toBe(true); expect(component.errorMessage).toBe(''); }); it('should toggle exploration visibility', () => { spyOn(explorationRightsService, 'setViewability'); - spyOn(explorationRightsService, 'viewableIfPrivate').and.returnValue( - false); + spyOn(explorationRightsService, 'viewableIfPrivate').and.returnValue(false); component.toggleViewabilityIfPrivate(); - expect(explorationRightsService.setViewability).toHaveBeenCalledWith( - true); + expect(explorationRightsService.setViewability).toHaveBeenCalledWith(true); }); it('should toggle the preview cards', fakeAsync(() => { @@ -1174,81 +1236,96 @@ describe('Settings Tab Component', () => { expect(component.basicSettingIsShown).toEqual(true); })); - it('should get the last edited version number for the ' + - 'exploration metadata', () => { - spyOn( - versionHistoryService, 'getBackwardMetadataDiffData' - ).and.returnValue({ - oldVersionNumber: 3 - } as MetadataDiffData); + it( + 'should get the last edited version number for the ' + + 'exploration metadata', + () => { + spyOn( + versionHistoryService, + 'getBackwardMetadataDiffData' + ).and.returnValue({ + oldVersionNumber: 3, + } as MetadataDiffData); - expect(component.getLastEditedVersionNumber()).toEqual(3); - }); + expect(component.getLastEditedVersionNumber()).toEqual(3); + } + ); it('should throw error when last edited version number is null', () => { - spyOn( - versionHistoryService, 'getBackwardMetadataDiffData' - ).and.returnValue({ - oldVersionNumber: null - } as MetadataDiffData); + spyOn(versionHistoryService, 'getBackwardMetadataDiffData').and.returnValue( + { + oldVersionNumber: null, + } as MetadataDiffData + ); - expect( - () =>component.getLastEditedVersionNumber() - ).toThrowError('Last edited version number cannot be null'); + expect(() => component.getLastEditedVersionNumber()).toThrowError( + 'Last edited version number cannot be null' + ); }); - it('should get the last edited committer username for exploration metadata', - () => { - spyOn( - versionHistoryService, 'getBackwardMetadataDiffData' - ).and.returnValue({ - committerUsername: 'some' - } as MetadataDiffData); + it('should get the last edited committer username for exploration metadata', () => { + spyOn(versionHistoryService, 'getBackwardMetadataDiffData').and.returnValue( + { + committerUsername: 'some', + } as MetadataDiffData + ); - expect(component.getLastEditedCommitterUsername()).toEqual('some'); - }); + expect(component.getLastEditedCommitterUsername()).toEqual('some'); + }); - it('should fetch the version history data from the backend', - fakeAsync(() => { - const explorationMetadata = new ExplorationMetadata( - 'title', 'category', 'objective', 'en', - [], '', '', 55, 'Introduction', - new ParamSpecs({}, paramSpecObjectFactory), [], false, true - ); - spyOn( - versionHistoryBackendApiService, 'fetchMetadataVersionHistoryAsync' - ).and.resolveTo({ - lastEditedVersionNumber: 3, - lastEditedCommitterUsername: '', - metadataInPreviousVersion: explorationMetadata - }); + it('should fetch the version history data from the backend', fakeAsync(() => { + const explorationMetadata = new ExplorationMetadata( + 'title', + 'category', + 'objective', + 'en', + [], + '', + '', + 55, + 'Introduction', + new ParamSpecs({}, paramSpecObjectFactory), + [], + false, + true + ); + spyOn( + versionHistoryBackendApiService, + 'fetchMetadataVersionHistoryAsync' + ).and.resolveTo({ + lastEditedVersionNumber: 3, + lastEditedCommitterUsername: '', + metadataInPreviousVersion: explorationMetadata, + }); - component.updateMetadataVersionHistory(); - tick(); - flushMicrotasks(); + component.updateMetadataVersionHistory(); + tick(); + flushMicrotasks(); - expect( - versionHistoryBackendApiService.fetchMetadataVersionHistoryAsync - ).toHaveBeenCalled(); - })); + expect( + versionHistoryBackendApiService.fetchMetadataVersionHistoryAsync + ).toHaveBeenCalled(); + })); - it('should fetch show validation error if the backend api fails', - fakeAsync(() => { - spyOn( - versionHistoryBackendApiService, 'fetchMetadataVersionHistoryAsync' - ).and.resolveTo(null); + it('should fetch show validation error if the backend api fails', fakeAsync(() => { + spyOn( + versionHistoryBackendApiService, + 'fetchMetadataVersionHistoryAsync' + ).and.resolveTo(null); - expect(component.validationErrorIsShown).toBeFalse(); + expect(component.validationErrorIsShown).toBeFalse(); - component.updateMetadataVersionHistory(); - tick(); - flushMicrotasks(); + component.updateMetadataVersionHistory(); + tick(); + flushMicrotasks(); - expect(component.validationErrorIsShown).toBeTrue(); - })); + expect(component.validationErrorIsShown).toBeTrue(); + })); - it('should open the metadata version history modal on clicking the explore ' + - 'version history button', () => { + it( + 'should open the metadata version history modal on clicking the explore ' + + 'version history button', + () => { class MockComponentInstance { compoenentInstance = { newMetadata: null, @@ -1259,24 +1336,38 @@ describe('Settings Tab Component', () => { }, }; } - spyOn(ngbModal, 'open').and.returnValues({ - componentInstance: MockComponentInstance, - result: Promise.resolve() - } as NgbModalRef, { - componentInstance: MockComponentInstance, - result: Promise.reject() - } as NgbModalRef); + spyOn(ngbModal, 'open').and.returnValues( + { + componentInstance: MockComponentInstance, + result: Promise.resolve(), + } as NgbModalRef, + { + componentInstance: MockComponentInstance, + result: Promise.reject(), + } as NgbModalRef + ); const explorationMetadata = new ExplorationMetadata( - 'title', 'category', 'objective', 'en', - [], '', '', 55, 'Introduction', - new ParamSpecs({}, paramSpecObjectFactory), [], false, true + 'title', + 'category', + 'objective', + 'en', + [], + '', + '', + 55, + 'Introduction', + new ParamSpecs({}, paramSpecObjectFactory), + [], + false, + true ); spyOn( - versionHistoryService, 'getBackwardMetadataDiffData' + versionHistoryService, + 'getBackwardMetadataDiffData' ).and.returnValue({ oldMetadata: explorationMetadata, newMetadata: explorationMetadata, - oldVersionNumber: 3 + oldVersionNumber: 3, } as MetadataDiffData); component.onClickExploreVersionHistoryButton(); @@ -1284,5 +1375,6 @@ describe('Settings Tab Component', () => { expect(ngbModal.open).toHaveBeenCalled(); component.onClickExploreVersionHistoryButton(); - }); + } + ); }); diff --git a/core/templates/pages/exploration-editor-page/settings-tab/settings-tab.component.ts b/core/templates/pages/exploration-editor-page/settings-tab/settings-tab.component.ts index 1105d2146212..9ffafbb183d5 100644 --- a/core/templates/pages/exploration-editor-page/settings-tab/settings-tab.component.ts +++ b/core/templates/pages/exploration-editor-page/settings-tab/settings-tab.component.ts @@ -16,58 +16,66 @@ * @fileoverview Component for the exploration settings tab. */ -import { Component, Input, OnDestroy, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { MatChipInputEvent } from '@angular/material/chips'; -import { COMMA, ENTER } from '@angular/cdk/keycodes'; -import { NgbModalRef, NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { Subscription } from 'rxjs'; -import { DeleteExplorationModalComponent } from './templates/delete-exploration-modal.component'; -import { RemoveRoleConfirmationModalComponent } from './templates/remove-role-confirmation-modal.component'; -import { ModeratorUnpublishExplorationModalComponent } from './templates/moderator-unpublish-exploration-modal.component'; -import { ReassignRoleConfirmationModalComponent } from './templates/reassign-role-confirmation-modal.component'; -import { TransferExplorationOwnershipModalComponent } from './templates/transfer-exploration-ownership-modal.component'; -import { PreviewSummaryTileModalComponent } from './templates/preview-summary-tile-modal.component'; -import { EditableExplorationBackendApiService } from 'domain/exploration/editable-exploration-backend-api.service'; -import { AlertsService } from 'services/alerts.service'; -import { ContextService } from 'services/context.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { EditabilityService } from 'services/editability.service'; -import { ExplorationFeaturesService } from 'services/exploration-features.service'; -import { UserService } from 'services/user.service'; -import { ChangeListService } from '../services/change-list.service'; -import { ExplorationAutomaticTextToSpeechService } from '../services/exploration-automatic-text-to-speech.service'; -import { ExplorationCategoryService } from '../services/exploration-category.service'; -import { ExplorationDataService } from '../services/exploration-data.service'; -import { ExplorationEditsAllowedBackendApiService } from '../services/exploration-edits-allowed-backend-api.service'; -import { ExplorationInitStateNameService } from '../services/exploration-init-state-name.service'; -import { ExplorationLanguageCodeService } from '../services/exploration-language-code.service'; -import { ExplorationObjectiveService } from '../services/exploration-objective.service'; -import { ExplorationParamChangesService } from '../services/exploration-param-changes.service'; -import { ExplorationParamSpecsService } from '../services/exploration-param-specs.service'; -import { ExplorationRightsService } from '../services/exploration-rights.service'; -import { ExplorationStatesService } from '../services/exploration-states.service'; -import { ExplorationTagsService } from '../services/exploration-tags.service'; -import { ExplorationTitleService } from '../services/exploration-title.service'; -import { ExplorationWarningsService } from '../services/exploration-warnings.service'; -import { RouterService } from '../services/router.service'; -import { SettingTabBackendApiService, SettingTabResponse } from '../services/setting-tab-backend-api.service'; -import { UserEmailPreferencesService } from '../services/user-email-preferences.service'; -import { UserExplorationPermissionsService } from '../services/user-exploration-permissions.service'; -import { ExplorationEditorPageConstants } from '../exploration-editor-page.constants'; -import { AppConstants } from 'app.constants'; -import { ExplorationMetadataObjectFactory } from 'domain/exploration/ExplorationMetadataObjectFactory'; -import { MetadataDiffData, VersionHistoryService } from '../services/version-history.service'; -import { MetadataVersionHistoryResponse, VersionHistoryBackendApiService } from '../services/version-history-backend-api.service'; -import { MetadataVersionHistoryModalComponent } from '../modal-templates/metadata-version-history-modal.component'; +import {Component, Input, OnDestroy, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {MatChipInputEvent} from '@angular/material/chips'; +import {COMMA, ENTER} from '@angular/cdk/keycodes'; +import {NgbModalRef, NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {Subscription} from 'rxjs'; +import {DeleteExplorationModalComponent} from './templates/delete-exploration-modal.component'; +import {RemoveRoleConfirmationModalComponent} from './templates/remove-role-confirmation-modal.component'; +import {ModeratorUnpublishExplorationModalComponent} from './templates/moderator-unpublish-exploration-modal.component'; +import {ReassignRoleConfirmationModalComponent} from './templates/reassign-role-confirmation-modal.component'; +import {TransferExplorationOwnershipModalComponent} from './templates/transfer-exploration-ownership-modal.component'; +import {PreviewSummaryTileModalComponent} from './templates/preview-summary-tile-modal.component'; +import {EditableExplorationBackendApiService} from 'domain/exploration/editable-exploration-backend-api.service'; +import {AlertsService} from 'services/alerts.service'; +import {ContextService} from 'services/context.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {EditabilityService} from 'services/editability.service'; +import {ExplorationFeaturesService} from 'services/exploration-features.service'; +import {UserService} from 'services/user.service'; +import {ChangeListService} from '../services/change-list.service'; +import {ExplorationAutomaticTextToSpeechService} from '../services/exploration-automatic-text-to-speech.service'; +import {ExplorationCategoryService} from '../services/exploration-category.service'; +import {ExplorationDataService} from '../services/exploration-data.service'; +import {ExplorationEditsAllowedBackendApiService} from '../services/exploration-edits-allowed-backend-api.service'; +import {ExplorationInitStateNameService} from '../services/exploration-init-state-name.service'; +import {ExplorationLanguageCodeService} from '../services/exploration-language-code.service'; +import {ExplorationObjectiveService} from '../services/exploration-objective.service'; +import {ExplorationParamChangesService} from '../services/exploration-param-changes.service'; +import {ExplorationParamSpecsService} from '../services/exploration-param-specs.service'; +import {ExplorationRightsService} from '../services/exploration-rights.service'; +import {ExplorationStatesService} from '../services/exploration-states.service'; +import {ExplorationTagsService} from '../services/exploration-tags.service'; +import {ExplorationTitleService} from '../services/exploration-title.service'; +import {ExplorationWarningsService} from '../services/exploration-warnings.service'; +import {RouterService} from '../services/router.service'; +import { + SettingTabBackendApiService, + SettingTabResponse, +} from '../services/setting-tab-backend-api.service'; +import {UserEmailPreferencesService} from '../services/user-email-preferences.service'; +import {UserExplorationPermissionsService} from '../services/user-exploration-permissions.service'; +import {ExplorationEditorPageConstants} from '../exploration-editor-page.constants'; +import {AppConstants} from 'app.constants'; +import {ExplorationMetadataObjectFactory} from 'domain/exploration/ExplorationMetadataObjectFactory'; +import { + MetadataDiffData, + VersionHistoryService, +} from '../services/version-history.service'; +import { + MetadataVersionHistoryResponse, + VersionHistoryBackendApiService, +} from '../services/version-history-backend-api.service'; +import {MetadataVersionHistoryModalComponent} from '../modal-templates/metadata-version-history-modal.component'; @Component({ selector: 'oppia-settings-tab', - templateUrl: './settings-tab.component.html' + templateUrl: './settings-tab.component.html', }) -export class SettingsTabComponent - implements OnInit, OnDestroy { +export class SettingsTabComponent implements OnInit, OnDestroy { // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -79,7 +87,7 @@ export class SettingsTabComponent errorMessage!: string; isRolesFormOpen!: boolean; newMemberUsername!: string; - newMemberRole!: { name: string; value: string}; + newMemberRole!: {name: string; value: string}; isVoiceoverFormOpen!: boolean; newVoiceArtistUsername!: string; hasPageLoaded!: boolean; @@ -106,21 +114,25 @@ export class SettingsTabComponent directiveSubscriptions = new Subscription(); explorationIsLinkedToStory: boolean = false; isSuperAdmin: boolean = false; - ROLES = [{ - name: 'Manager (can edit permissions)', - value: 'owner' - }, { - name: 'Collaborator (can make changes)', - value: 'editor' - }, { - name: 'Playtester (can give feedback)', - value: 'viewer' - }]; + ROLES = [ + { + name: 'Manager (can edit permissions)', + value: 'owner', + }, + { + name: 'Collaborator (can make changes)', + value: 'editor', + }, + { + name: 'Playtester (can give feedback)', + value: 'viewer', + }, + ]; CATEGORY_LIST_FOR_SELECT2!: { id: string; text: string; - } []; + }[]; addOnBlur: boolean = true; @@ -131,14 +143,11 @@ export class SettingsTabComponent private changeListService: ChangeListService, private contextService: ContextService, public editabilityService: EditabilityService, - private editableExplorationBackendApiService: - EditableExplorationBackendApiService, - private explorationAutomaticTextToSpeechService: - ExplorationAutomaticTextToSpeechService, + private editableExplorationBackendApiService: EditableExplorationBackendApiService, + private explorationAutomaticTextToSpeechService: ExplorationAutomaticTextToSpeechService, private explorationCategoryService: ExplorationCategoryService, private explorationDataService: ExplorationDataService, - private explorationEditsAllowedBackendApiService: - ExplorationEditsAllowedBackendApiService, + private explorationEditsAllowedBackendApiService: ExplorationEditsAllowedBackendApiService, private explorationFeaturesService: ExplorationFeaturesService, private explorationInitStateNameService: ExplorationInitStateNameService, private explorationLanguageCodeService: ExplorationLanguageCodeService, @@ -155,13 +164,12 @@ export class SettingsTabComponent private routerService: RouterService, private settingTabBackendApiService: SettingTabBackendApiService, private userEmailPreferencesService: UserEmailPreferencesService, - private userExplorationPermissionsService: - UserExplorationPermissionsService, + private userExplorationPermissionsService: UserExplorationPermissionsService, private userService: UserService, private versionHistoryBackendApiService: VersionHistoryBackendApiService, private versionHistoryService: VersionHistoryService, private windowDimensionsService: WindowDimensionsService, - private windowRef: WindowRef, + private windowRef: WindowRef ) {} filteredChoices: { @@ -181,10 +189,13 @@ export class SettingsTabComponent // Add our explorationTags. if (value) { - if (!(this.explorationTagsService.displayed) || - (this.explorationTagsService.displayed as []).length < 10) { + if ( + !this.explorationTagsService.displayed || + (this.explorationTagsService.displayed as []).length < 10 + ) { if ( - (this.explorationTagsService.displayed as string[]).includes(value)) { + (this.explorationTagsService.displayed as string[]).includes(value) + ) { // Clear the input value. event.input.value = ''; return; @@ -215,8 +226,9 @@ export class SettingsTabComponent } updateCategoryListWithUserData(): void { - if (this.newCategory && - (this.explorationCategoryService.displayed === this.newCategory.id) + if ( + this.newCategory && + this.explorationCategoryService.displayed === this.newCategory.id ) { this.CATEGORY_LIST_FOR_SELECT2.push(this.newCategory); this.explorationCategoryService.displayed = this.newCategory.id; @@ -228,11 +240,12 @@ export class SettingsTabComponent filterChoices(searchTerm: string): void { this.newCategory = { id: searchTerm, - text: searchTerm + text: searchTerm, }; this.filteredChoices = this.CATEGORY_LIST_FOR_SELECT2.filter( - value => value.text.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1); + value => value.text.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1 + ); this.filteredChoices.push(this.newCategory); @@ -248,38 +261,40 @@ export class SettingsTabComponent // /settings) results in a console error. this.hasPageLoaded = false; - this.explorationDataService.getDataAsync(() => {}).then(() => { - if (this.explorationStatesService.isInitialized()) { - var categoryIsInSelect2 = this.CATEGORY_LIST_FOR_SELECT2.some( - (categoryItem) => { - return ( - categoryItem.id === - this.explorationCategoryService.savedMemento); + this.explorationDataService + .getDataAsync(() => {}) + .then(() => { + if (this.explorationStatesService.isInitialized()) { + var categoryIsInSelect2 = this.CATEGORY_LIST_FOR_SELECT2.some( + categoryItem => { + return ( + categoryItem.id === this.explorationCategoryService.savedMemento + ); + } + ); + // If the current category is not in the dropdown, add it + // as the first option. + if ( + !categoryIsInSelect2 && + this.explorationCategoryService.savedMemento + ) { + this.CATEGORY_LIST_FOR_SELECT2.unshift({ + id: this.explorationCategoryService.savedMemento as string, + text: this.explorationCategoryService.savedMemento as string, + }); } - ); - // If the current category is not in the dropdown, add it - // as the first option. - if (!categoryIsInSelect2 && - this.explorationCategoryService.savedMemento) { - this.CATEGORY_LIST_FOR_SELECT2.unshift({ - id: this.explorationCategoryService.savedMemento as string, - text: this.explorationCategoryService.savedMemento as string - }); - } - this.stateNames = this.explorationStatesService.getStateNames(); - this.explorationIsLinkedToStory = ( - this.contextService.isExplorationLinkedToStory()); - } - this.hasPageLoaded = true; - }); + this.stateNames = this.explorationStatesService.getStateNames(); + this.explorationIsLinkedToStory = + this.contextService.isExplorationLinkedToStory(); + } + this.hasPageLoaded = true; + }); } getLastEditedVersionNumber(): number { - const lastEditedVersionNumber = this - .versionHistoryService - .getBackwardMetadataDiffData() - .oldVersionNumber; + const lastEditedVersionNumber = + this.versionHistoryService.getBackwardMetadataDiffData().oldVersionNumber; if (lastEditedVersionNumber === null) { // A null value for lastEditedVersionNumber marks the end of the version // history for the exploration metadata. This is impossible here because @@ -292,12 +307,8 @@ export class SettingsTabComponent } getLastEditedCommitterUsername(): string { - return ( - this - .versionHistoryService - .getBackwardMetadataDiffData() - .committerUsername - ); + return this.versionHistoryService.getBackwardMetadataDiffData() + .committerUsername; } canShowExploreVersionHistoryButton(): boolean { @@ -306,52 +317,59 @@ export class SettingsTabComponent onClickExploreVersionHistoryButton(): void { const modalRef: NgbModalRef = this.ngbModal.open( - MetadataVersionHistoryModalComponent, { + MetadataVersionHistoryModalComponent, + { backdrop: true, windowClass: 'metadata-diff-modal', - size: 'xl' - }); + size: 'xl', + } + ); - const metadataDiffData: MetadataDiffData = this - .versionHistoryService - .getBackwardMetadataDiffData(); + const metadataDiffData: MetadataDiffData = + this.versionHistoryService.getBackwardMetadataDiffData(); modalRef.componentInstance.newMetadata = metadataDiffData.newMetadata; modalRef.componentInstance.oldMetadata = metadataDiffData.oldMetadata; - modalRef.componentInstance.committerUsername = ( - metadataDiffData.committerUsername); + modalRef.componentInstance.committerUsername = + metadataDiffData.committerUsername; modalRef.componentInstance.oldVersion = metadataDiffData.oldVersionNumber; - modalRef.result.then(() => { - this.versionHistoryService - .setCurrentPositionInMetadataVersionHistoryList(0); - }, () => { - this.versionHistoryService - .setCurrentPositionInMetadataVersionHistoryList(0); - }); + modalRef.result.then( + () => { + this.versionHistoryService.setCurrentPositionInMetadataVersionHistoryList( + 0 + ); + }, + () => { + this.versionHistoryService.setCurrentPositionInMetadataVersionHistoryList( + 0 + ); + } + ); } async updateMetadataVersionHistory(): Promise { this.versionHistoryService.resetMetadataVersionHistory(); this.validationErrorIsShown = false; - const explorationData = ( - await this.explorationDataService.getDataAsync(() => {})); + const explorationData = await this.explorationDataService.getDataAsync( + () => {} + ); - const explorationMetadata = this - .explorationMetadataObjectFactory - .createFromBackendDict(explorationData.exploration_metadata); + const explorationMetadata = + this.explorationMetadataObjectFactory.createFromBackendDict( + explorationData.exploration_metadata + ); this.versionHistoryService.insertMetadataVersionHistoryData( this.versionHistoryService.getLatestVersionOfExploration(), - explorationMetadata, ''); + explorationMetadata, + '' + ); - if ( - this.versionHistoryService.getLatestVersionOfExploration() !== null - ) { - const metadataVersionHistory = await this - .versionHistoryBackendApiService - .fetchMetadataVersionHistoryAsync( + if (this.versionHistoryService.getLatestVersionOfExploration() !== null) { + const metadataVersionHistory = + await this.versionHistoryBackendApiService.fetchMetadataVersionHistoryAsync( this.contextService.getExplorationId(), this.versionHistoryService.getLatestVersionOfExploration() as number ); @@ -374,12 +392,11 @@ export class SettingsTabComponent this.explorationTitleService.saveDisplayedValue(); if (!this.isTitlePresent()) { this.rolesSaveButtonEnabled = false; - this.errorMessage = ( - 'Please provide a title before inviting.'); + this.errorMessage = 'Please provide a title before inviting.'; return; } else { this.rolesSaveButtonEnabled = true; - this.errorMessage = (''); + this.errorMessage = ''; return; } } @@ -403,7 +420,8 @@ export class SettingsTabComponent if (!this.explorationStatesService.getState(newInitStateName as string)) { this.alertsService.addWarning( - 'Invalid initial state name: ' + newInitStateName); + 'Invalid initial state name: ' + newInitStateName + ); this.explorationInitStateNameService.restoreFromMemento(); return; } @@ -424,7 +442,7 @@ export class SettingsTabComponent areParametersUsed(): boolean { if (this.hasPageLoaded) { - return (this.explorationDataService.data.param_changes.length > 0); + return this.explorationDataService.data.param_changes.length > 0; } return false; } @@ -434,37 +452,41 @@ export class SettingsTabComponent } isAutomaticTextToSpeechEnabled(): boolean { - return this.explorationAutomaticTextToSpeechService - .isAutomaticTextToSpeechEnabled(); + return this.explorationAutomaticTextToSpeechService.isAutomaticTextToSpeechEnabled(); } toggleAutomaticTextToSpeech(): void { - return this.explorationAutomaticTextToSpeechService - .toggleAutomaticTextToSpeech(); + return this.explorationAutomaticTextToSpeechService.toggleAutomaticTextToSpeech(); } isExplorationEditable(): boolean { return ( - this.explorationDataService.data && - this.explorationDataService.data.edits_allowed) || false; + (this.explorationDataService.data && + this.explorationDataService.data.edits_allowed) || + false + ); } enableEdits(): void { this.explorationEditsAllowedBackendApiService.setEditsAllowed( - true, this.explorationDataService.explorationId, + true, + this.explorationDataService.explorationId, () => { this.editabilityService.lockExploration(false); this.explorationDataService.data.edits_allowed = true; - }); + } + ); } disableEdits(): void { this.explorationEditsAllowedBackendApiService.setEditsAllowed( - false, this.explorationDataService.explorationId, + false, + this.explorationDataService.explorationId, () => { this.editabilityService.lockExploration(true); this.explorationDataService.data.edits_allowed = false; - }); + } + ); } // Methods for rights management. @@ -491,17 +513,19 @@ export class SettingsTabComponent } editRole(newMemberUsername: string, newMemberRole: string): void { - if (!this.explorationRightsService.checkUserAlreadyHasRoles( - newMemberUsername)) { + if ( + !this.explorationRightsService.checkUserAlreadyHasRoles(newMemberUsername) + ) { this.explorationRightsService.saveRoleChanges( - newMemberUsername, newMemberRole); + newMemberUsername, + newMemberRole + ); this.closeRolesForm(); return; } - let oldRole = this.explorationRightsService.getOldRole( - newMemberUsername); + let oldRole = this.explorationRightsService.getOldRole(newMemberUsername); this.reassignRole(newMemberUsername, newMemberRole, oldRole); } @@ -531,28 +555,30 @@ export class SettingsTabComponent removeRole(memberUsername: string, memberRole: string): void { this.alertsService.clearWarnings(); - const modalRef = this.ngbModal - .open(RemoveRoleConfirmationModalComponent, { - backdrop: true, - }); + const modalRef = this.ngbModal.open(RemoveRoleConfirmationModalComponent, { + backdrop: true, + }); modalRef.componentInstance.username = memberUsername; modalRef.componentInstance.role = memberRole; - modalRef.result.then(() => { - this.explorationRightsService.removeRoleAsync( - memberUsername); - this.closeRolesForm(); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is - // clicked. No further action is needed. - }); + modalRef.result.then( + () => { + this.explorationRightsService.removeRoleAsync(memberUsername); + this.closeRolesForm(); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is + // clicked. No further action is needed. + } + ); } editVoiceArtist(newVoiceArtistUsername: string): void { this.explorationRightsService.assignVoiceArtistRoleAsync( - newVoiceArtistUsername); + newVoiceArtistUsername + ); this.closeVoiceoverForm(); return; } @@ -560,32 +586,38 @@ export class SettingsTabComponent removeVoiceArtist(voiceArtistUsername: string): void { this.alertsService.clearWarnings(); - const modalRef = this.ngbModal - .open(RemoveRoleConfirmationModalComponent, { - backdrop: true, - }); + const modalRef = this.ngbModal.open(RemoveRoleConfirmationModalComponent, { + backdrop: true, + }); modalRef.componentInstance.username = voiceArtistUsername; modalRef.componentInstance.role = 'voice artist'; - modalRef.result.then(() => { - this.explorationRightsService.removeVoiceArtistRoleAsync( - voiceArtistUsername); - this.closeVoiceoverForm(); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is - // clicked. No further action is needed. - }); + modalRef.result.then( + () => { + this.explorationRightsService.removeVoiceArtistRoleAsync( + voiceArtistUsername + ); + this.closeVoiceoverForm(); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is + // clicked. No further action is needed. + } + ); } toggleViewabilityIfPrivate(): void { this.explorationRightsService.setViewability( - !this.explorationRightsService.viewableIfPrivate()); + !this.explorationRightsService.viewableIfPrivate() + ); } // Methods for muting notifications. muteFeedbackNotifications(): void { this.userEmailPreferencesService.setFeedbackNotificationPreferences( - true, () => {}); + true, + () => {} + ); } muteSuggestionNotifications(): void { @@ -604,45 +636,63 @@ export class SettingsTabComponent unmuteSuggestionNotifications(): void { this.userEmailPreferencesService.setSuggestionNotificationPreferences( - false, () => {}); + false, + () => {} + ); } // Methods relating to control buttons. previewSummaryTile(): void { this.alertsService.clearWarnings(); - this.ngbModal.open(PreviewSummaryTileModalComponent, { - backdrop: true, - }).result.then(() => {}, () => { - this.alertsService.clearWarnings(); - }); + this.ngbModal + .open(PreviewSummaryTileModalComponent, { + backdrop: true, + }) + .result.then( + () => {}, + () => { + this.alertsService.clearWarnings(); + } + ); } showTransferExplorationOwnershipModal(): void { this.alertsService.clearWarnings(); - this.ngbModal.open(TransferExplorationOwnershipModalComponent, { - backdrop: true, - }).result.then(() => { - this.explorationRightsService.makeCommunityOwned(); - }, () => { - this.alertsService.clearWarnings(); - }); + this.ngbModal + .open(TransferExplorationOwnershipModalComponent, { + backdrop: true, + }) + .result.then( + () => { + this.explorationRightsService.makeCommunityOwned(); + }, + () => { + this.alertsService.clearWarnings(); + } + ); } deleteExploration(): void { this.alertsService.clearWarnings(); - this.ngbModal.open(DeleteExplorationModalComponent, { - backdrop: true, - }).result.then(() => { - this.editableExplorationBackendApiService.deleteExplorationAsync( - this.explorationId).then(() => { - this.windowRef.nativeWindow.location = ( - // TODO(#13015): Remove use of unknown as a type. - this.CREATOR_DASHBOARD_PAGE_URL as unknown as Location); - }); - }, () => { - this.alertsService.clearWarnings(); - }); + this.ngbModal + .open(DeleteExplorationModalComponent, { + backdrop: true, + }) + .result.then( + () => { + this.editableExplorationBackendApiService + .deleteExplorationAsync(this.explorationId) + .then(() => { + this.windowRef.nativeWindow.location = + // TODO(#13015): Remove use of unknown as a type. + this.CREATOR_DASHBOARD_PAGE_URL as unknown as Location; + }); + }, + () => { + this.alertsService.clearWarnings(); + } + ); } unpublishExplorationAsModerator(): void { @@ -651,32 +701,39 @@ export class SettingsTabComponent var moderatorEmailDraftUrl = '/moderatorhandler/email_draft'; this.settingTabBackendApiService - .getData(moderatorEmailDraftUrl).then( - (response: SettingTabResponse) => { - // If the draft email body is empty, email functionality will not - // be exposed to the mdoerator. - const draftEmailBody = response.draft_email_body; - - const modalRef = this.ngbModal - .open(ModeratorUnpublishExplorationModalComponent, { - backdrop: true, - }); - - modalRef.componentInstance.draftEmailBody = draftEmailBody; + .getData(moderatorEmailDraftUrl) + .then((response: SettingTabResponse) => { + // If the draft email body is empty, email functionality will not + // be exposed to the mdoerator. + const draftEmailBody = response.draft_email_body; + + const modalRef = this.ngbModal.open( + ModeratorUnpublishExplorationModalComponent, + { + backdrop: true, + } + ); - modalRef.result.then((emailBody) => { - this.explorationRightsService.saveModeratorChangeToBackendAsync( - emailBody).then(() => { - this.userExplorationPermissionsService.fetchPermissionsAsync() - .then((permissions) => { - this.canUnpublish = permissions.canUnpublish; - this.canReleaseOwnership = permissions.canReleaseOwnership; - }); - }); - }, () => { + modalRef.componentInstance.draftEmailBody = draftEmailBody; + + modalRef.result.then( + emailBody => { + this.explorationRightsService + .saveModeratorChangeToBackendAsync(emailBody) + .then(() => { + this.userExplorationPermissionsService + .fetchPermissionsAsync() + .then(permissions => { + this.canUnpublish = permissions.canUnpublish; + this.canReleaseOwnership = permissions.canReleaseOwnership; + }); + }); + }, + () => { this.alertsService.clearWarnings(); - }); - }); + } + ); + }); } isExplorationLockedForEditing(): boolean { @@ -699,14 +756,18 @@ export class SettingsTabComponent if (this.newMemberUsername === this.loggedInUser) { this.rolesSaveButtonEnabled = false; - this.errorMessage = ( - 'Users are not allowed to assign other roles to themselves.'); + this.errorMessage = + 'Users are not allowed to assign other roles to themselves.'; return; } - if (this.explorationRightsService.checkUserAlreadyHasRoles( - this.newMemberUsername)) { + if ( + this.explorationRightsService.checkUserAlreadyHasRoles( + this.newMemberUsername + ) + ) { var oldRole = this.explorationRightsService.getOldRole( - this.newMemberUsername); + this.newMemberUsername + ); if (oldRole === this.newMemberRole.value) { this.rolesSaveButtonEnabled = false; this.errorMessage = `User is already ${oldRole}.`; @@ -717,82 +778,80 @@ export class SettingsTabComponent getExplorePageUrl(): string { return ( - this.windowRef.nativeWindow.location.protocol + '//' + + this.windowRef.nativeWindow.location.protocol + + '//' + this.windowRef.nativeWindow.location.host + - this.EXPLORE_PAGE_PREFIX + this.explorationId); + this.EXPLORE_PAGE_PREFIX + + this.explorationId + ); } - reassignRole( - username: string, - newRole: string, - oldRole: string - ): void { + reassignRole(username: string, newRole: string, oldRole: string): void { this.alertsService.clearWarnings(); - const modalRef: NgbModalRef = this.ngbModal - .open(ReassignRoleConfirmationModalComponent, { + const modalRef: NgbModalRef = this.ngbModal.open( + ReassignRoleConfirmationModalComponent, + { backdrop: true, - }); + } + ); modalRef.componentInstance.username = username; modalRef.componentInstance.newRole = newRole; modalRef.componentInstance.oldRole = oldRole; - modalRef.result.then(() => { - this.explorationRightsService.saveRoleChanges( - username, newRole); - this.closeRolesForm(); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is - // clicked. No further action is needed. - }); + modalRef.result.then( + () => { + this.explorationRightsService.saveRoleChanges(username, newRole); + this.closeRolesForm(); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is + // clicked. No further action is needed. + } + ); } ngOnInit(): void { this.directiveSubscriptions.add( - this.explorationTagsService.onExplorationPropertyChanged - .subscribe( - () => { - this.explorationTags = ( - this.explorationTagsService.displayed as string[]); - this.updateMetadataVersionHistory(); - } - ) + this.explorationTagsService.onExplorationPropertyChanged.subscribe(() => { + this.explorationTags = this.explorationTagsService + .displayed as string[]; + this.updateMetadataVersionHistory(); + }) ); this.directiveSubscriptions.add( - this.routerService.onRefreshSettingsTab.subscribe( - () => { - // TODO(#15473): Remove this delay after this has been - // migrated to Angular 2+. - setTimeout(()=>{ - this.refreshSettingsTab(); - }, 500); - } - ) + this.routerService.onRefreshSettingsTab.subscribe(() => { + // TODO(#15473): Remove this delay after this has been + // migrated to Angular 2+. + setTimeout(() => { + this.refreshSettingsTab(); + }, 500); + }) ); this.directiveSubscriptions.add( - this.userExplorationPermissionsService.onUserExplorationPermissionsFetched - .subscribe( - () => { - this.userExplorationPermissionsService.getPermissionsAsync() - .then((permissions) => { - this.canUnpublish = permissions.canUnpublish; - this.canReleaseOwnership = permissions.canReleaseOwnership; - }); - } - ) + this.userExplorationPermissionsService.onUserExplorationPermissionsFetched.subscribe( + () => { + this.userExplorationPermissionsService + .getPermissionsAsync() + .then(permissions => { + this.canUnpublish = permissions.canUnpublish; + this.canReleaseOwnership = permissions.canReleaseOwnership; + }); + } + ) ); - this.EXPLORATION_TITLE_INPUT_FOCUS_LABEL = ( - ExplorationEditorPageConstants.EXPLORATION_TITLE_INPUT_FOCUS_LABEL); + this.EXPLORATION_TITLE_INPUT_FOCUS_LABEL = + ExplorationEditorPageConstants.EXPLORATION_TITLE_INPUT_FOCUS_LABEL; this.CATEGORY_LIST_FOR_SELECT2 = []; for (var i = 0; i < AppConstants.ALL_CATEGORIES.length; i++) { this.CATEGORY_LIST_FOR_SELECT2.push({ id: AppConstants.ALL_CATEGORIES[i], - text: AppConstants.ALL_CATEGORIES[i] + text: AppConstants.ALL_CATEGORIES[i], }); } this.isRolesFormOpen = false; @@ -800,15 +859,15 @@ export class SettingsTabComponent this.rolesSaveButtonEnabled = false; this.errorMessage = ''; this.basicSettingIsShown = !this.windowDimensionsService.isWindowNarrow(); - this.advancedFeaturesIsShown = ( - !this.windowDimensionsService.isWindowNarrow()); + this.advancedFeaturesIsShown = + !this.windowDimensionsService.isWindowNarrow(); this.rolesCardIsShown = !this.windowDimensionsService.isWindowNarrow(); - this.permissionsCardIsShown = ( - !this.windowDimensionsService.isWindowNarrow()); + this.permissionsCardIsShown = + !this.windowDimensionsService.isWindowNarrow(); this.feedbackCardIsShown = !this.windowDimensionsService.isWindowNarrow(); this.controlsCardIsShown = !this.windowDimensionsService.isWindowNarrow(); - this.voiceArtistsCardIsShown = ( - !this.windowDimensionsService.isWindowNarrow()); + this.voiceArtistsCardIsShown = + !this.windowDimensionsService.isWindowNarrow(); this.TAG_REGEX = AppConstants.TAG_REGEX; this.canDelete = false; @@ -819,13 +878,14 @@ export class SettingsTabComponent this.explorationId = this.explorationDataService.explorationId; this.loggedInUser = null; - this.userService.getUserInfoAsync().then((userInfo) => { + this.userService.getUserInfoAsync().then(userInfo => { this.loggedInUser = userInfo.getUsername(); this.isSuperAdmin = userInfo.isSuperAdmin(); }); - this.userExplorationPermissionsService.getPermissionsAsync() - .then((permissions) => { + this.userExplorationPermissionsService + .getPermissionsAsync() + .then(permissions => { this.canDelete = permissions.canDelete; this.canModifyRoles = permissions.canModifyRoles; this.canReleaseOwnership = permissions.canReleaseOwnership; @@ -836,7 +896,7 @@ export class SettingsTabComponent this.formStyle = JSON.stringify({ display: 'table-cell', width: '16.66666667%', - 'vertical-align': 'top' + 'vertical-align': 'top', }); this.filteredChoices = this.CATEGORY_LIST_FOR_SELECT2; @@ -847,7 +907,9 @@ export class SettingsTabComponent } } -angular.module('oppia').directive('oppiaSettingsTab', +angular.module('oppia').directive( + 'oppiaSettingsTab', downgradeComponent({ - component: SettingsTabComponent - }) as angular.IDirectiveFactory); + component: SettingsTabComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-editor-page/settings-tab/templates/delete-exploration-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/settings-tab/templates/delete-exploration-modal.component.spec.ts index ca1982f336e4..09681b72db23 100644 --- a/core/templates/pages/exploration-editor-page/settings-tab/templates/delete-exploration-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/settings-tab/templates/delete-exploration-modal.component.spec.ts @@ -12,15 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Unit tests for the delete exploration modal component. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { DeleteExplorationModalComponent } from './delete-exploration-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {DeleteExplorationModalComponent} from './delete-exploration-modal.component'; class MockActiveModal { close(): void { @@ -32,20 +31,20 @@ class MockActiveModal { } } -describe('Delete Exploration Modal Component', function() { +describe('Delete Exploration Modal Component', function () { let component: DeleteExplorationModalComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - DeleteExplorationModalComponent + declarations: [DeleteExplorationModalComponent], + providers: [ + { + provide: NgbActiveModal, + useClass: MockActiveModal, + }, ], - providers: [{ - provide: NgbActiveModal, - useClass: MockActiveModal - }], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); diff --git a/core/templates/pages/exploration-editor-page/settings-tab/templates/delete-exploration-modal.component.ts b/core/templates/pages/exploration-editor-page/settings-tab/templates/delete-exploration-modal.component.ts index f4b46748e71e..525cf68df76e 100644 --- a/core/templates/pages/exploration-editor-page/settings-tab/templates/delete-exploration-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/settings-tab/templates/delete-exploration-modal.component.ts @@ -16,18 +16,16 @@ * @fileoverview Component for Delete exploration modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-delete-exploration-modal', - templateUrl: './delete-exploration-modal.component.html' + templateUrl: './delete-exploration-modal.component.html', }) export class DeleteExplorationModalComponent extends ConfirmOrCancelModal { - constructor( - private ngbActiveModal: NgbActiveModal, - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } } diff --git a/core/templates/pages/exploration-editor-page/settings-tab/templates/moderator-unpublish-exploration-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/settings-tab/templates/moderator-unpublish-exploration-modal.component.spec.ts index 71f256998fab..6dff7dd0bc72 100644 --- a/core/templates/pages/exploration-editor-page/settings-tab/templates/moderator-unpublish-exploration-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/settings-tab/templates/moderator-unpublish-exploration-modal.component.spec.ts @@ -16,10 +16,10 @@ * @fileoverview Unit tests for ModeratorUnpublishExplorationModalComponent. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ModeratorUnpublishExplorationModalComponent } from './moderator-unpublish-exploration-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ModeratorUnpublishExplorationModalComponent} from './moderator-unpublish-exploration-modal.component'; class MockActiveModal { close(): void { @@ -31,7 +31,7 @@ class MockActiveModal { } } -describe('Moderator Unpublish Exploration Modal', function() { +describe('Moderator Unpublish Exploration Modal', function () { let component: ModeratorUnpublishExplorationModalComponent; let fixture: ComponentFixture; let ngbActiveModal: NgbActiveModal; @@ -39,20 +39,21 @@ describe('Moderator Unpublish Exploration Modal', function() { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - ModeratorUnpublishExplorationModalComponent + declarations: [ModeratorUnpublishExplorationModalComponent], + providers: [ + { + provide: NgbActiveModal, + useClass: MockActiveModal, + }, ], - providers: [{ - provide: NgbActiveModal, - useClass: MockActiveModal - }], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent( - ModeratorUnpublishExplorationModalComponent); + ModeratorUnpublishExplorationModalComponent + ); component = fixture.componentInstance; component.draftEmailBody = draftEmailBody; @@ -62,28 +63,26 @@ describe('Moderator Unpublish Exploration Modal', function() { spyOn(ngbActiveModal, 'close'); }); - it('should initialize properties when component is initialized', - () => { - expect(component).toBeDefined(); - expect(component.willEmailBeSent).toBe(true); - expect(component.emailBody).toBe(draftEmailBody); - expect(component.getSchema()).toEqual({ - type: 'unicode', - ui_config: { - rows: 20 - } - }); - - let newValue = 'update this value in emailbody'; - component.updateValue(newValue); - expect(component.emailBody).toBe(newValue); + it('should initialize properties when component is initialized', () => { + expect(component).toBeDefined(); + expect(component.willEmailBeSent).toBe(true); + expect(component.emailBody).toBe(draftEmailBody); + expect(component.getSchema()).toEqual({ + type: 'unicode', + ui_config: { + rows: 20, + }, }); - it('should close modal when \"Unpublish Exploration\" button is clicked', - () => { - component.emailBody = 'nothing'; - component.confirm(component.emailBody); + let newValue = 'update this value in emailbody'; + component.updateValue(newValue); + expect(component.emailBody).toBe(newValue); + }); - expect(ngbActiveModal.close).toHaveBeenCalledWith(component.emailBody); - }); + it('should close modal when "Unpublish Exploration" button is clicked', () => { + component.emailBody = 'nothing'; + component.confirm(component.emailBody); + + expect(ngbActiveModal.close).toHaveBeenCalledWith(component.emailBody); + }); }); diff --git a/core/templates/pages/exploration-editor-page/settings-tab/templates/moderator-unpublish-exploration-modal.component.ts b/core/templates/pages/exploration-editor-page/settings-tab/templates/moderator-unpublish-exploration-modal.component.ts index 46d43ad1ef53..3cbbae1c2539 100644 --- a/core/templates/pages/exploration-editor-page/settings-tab/templates/moderator-unpublish-exploration-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/settings-tab/templates/moderator-unpublish-exploration-modal.component.ts @@ -16,16 +16,18 @@ * @fileoverview Component for moderator unpublish exploration modal. */ -import { Component, Input, OnInit } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component, Input, OnInit} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-delete-exploration-modal', - templateUrl: './moderator-unpublish-exploration-modal.component.html' + templateUrl: './moderator-unpublish-exploration-modal.component.html', }) export class ModeratorUnpublishExplorationModalComponent - extends ConfirmOrCancelModal implements OnInit { + extends ConfirmOrCancelModal + implements OnInit +{ // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -34,9 +36,7 @@ export class ModeratorUnpublishExplorationModalComponent emailBody!: string; willEmailBeSent: boolean = false; - constructor( - private ngbActiveModal: NgbActiveModal, - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } @@ -48,8 +48,8 @@ export class ModeratorUnpublishExplorationModalComponent this.EMAIL_BODY_SCHEMA = { type: 'unicode', ui_config: { - rows: 20 - } + rows: 20, + }, }; } } diff --git a/core/templates/pages/exploration-editor-page/settings-tab/templates/preview-summary-tile-modal.component.ts b/core/templates/pages/exploration-editor-page/settings-tab/templates/preview-summary-tile-modal.component.ts index 25add15a8dd4..3895c48dc434 100644 --- a/core/templates/pages/exploration-editor-page/settings-tab/templates/preview-summary-tile-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/settings-tab/templates/preview-summary-tile-modal.component.ts @@ -16,13 +16,13 @@ * @fileoverview Component for preview summary tile modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { AppConstants } from 'app.constants'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { ExplorationCategoryService } from 'pages/exploration-editor-page/services/exploration-category.service'; -import { ExplorationObjectiveService } from 'pages/exploration-editor-page/services/exploration-objective.service'; -import { ExplorationTitleService } from 'pages/exploration-editor-page/services/exploration-title.service'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {AppConstants} from 'app.constants'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {ExplorationCategoryService} from 'pages/exploration-editor-page/services/exploration-category.service'; +import {ExplorationObjectiveService} from 'pages/exploration-editor-page/services/exploration-objective.service'; +import {ExplorationTitleService} from 'pages/exploration-editor-page/services/exploration-title.service'; @Component({ selector: 'oppia-preview-summary-tile-modal', @@ -65,8 +65,10 @@ export class PreviewSummaryTileModalComponent extends ConfirmOrCancelModal { if (!AppConstants.CATEGORIES_TO_COLORS.hasOwnProperty(category)) { color = AppConstants.DEFAULT_COLOR; } else { - color = AppConstants.CATEGORIES_TO_COLORS[ - category as keyof typeof AppConstants.CATEGORIES_TO_COLORS]; + color = + AppConstants.CATEGORIES_TO_COLORS[ + category as keyof typeof AppConstants.CATEGORIES_TO_COLORS + ]; } return color; } diff --git a/core/templates/pages/exploration-editor-page/settings-tab/templates/preview-summary-tile-modal.controller.spec.ts b/core/templates/pages/exploration-editor-page/settings-tab/templates/preview-summary-tile-modal.controller.spec.ts index e1f2f04e4a28..dd2df746ca6a 100644 --- a/core/templates/pages/exploration-editor-page/settings-tab/templates/preview-summary-tile-modal.controller.spec.ts +++ b/core/templates/pages/exploration-editor-page/settings-tab/templates/preview-summary-tile-modal.controller.spec.ts @@ -16,14 +16,14 @@ * @fileoverview Unit tests for PreviewSummaryTileModalController. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ExplorationCategoryService } from 'pages/exploration-editor-page/services/exploration-category.service'; -import { ExplorationObjectiveService } from 'pages/exploration-editor-page/services/exploration-objective.service'; -import { ExplorationTitleService } from 'pages/exploration-editor-page/services/exploration-title.service'; -import { PreviewSummaryTileModalComponent } from './preview-summary-tile-modal.component'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ExplorationCategoryService} from 'pages/exploration-editor-page/services/exploration-category.service'; +import {ExplorationObjectiveService} from 'pages/exploration-editor-page/services/exploration-objective.service'; +import {ExplorationTitleService} from 'pages/exploration-editor-page/services/exploration-title.service'; +import {PreviewSummaryTileModalComponent} from './preview-summary-tile-modal.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; class MockActiveModal { close(): void { @@ -35,7 +35,7 @@ class MockActiveModal { } } -describe('Preview Summary Tile Modal Controller', function() { +describe('Preview Summary Tile Modal Controller', function () { let component: PreviewSummaryTileModalComponent; let fixture: ComponentFixture; let explorationCategoryService: ExplorationCategoryService; @@ -44,20 +44,18 @@ describe('Preview Summary Tile Modal Controller', function() { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - PreviewSummaryTileModalComponent - ], + declarations: [PreviewSummaryTileModalComponent], imports: [HttpClientTestingModule], providers: [ { provide: NgbActiveModal, - useClass: MockActiveModal + useClass: MockActiveModal, }, ExplorationCategoryService, ExplorationObjectiveService, - ExplorationTitleService + ExplorationTitleService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); beforeEach(() => { @@ -70,32 +68,32 @@ describe('Preview Summary Tile Modal Controller', function() { fixture.detectChanges(); }); - it('should get exploration title', function() { + it('should get exploration title', function () { explorationTitleService.init('Exploration Title'); expect(component.getExplorationTitle()).toBe('Exploration Title'); }); - it('should get exploration objective', function() { + it('should get exploration objective', function () { explorationObjectiveService.init('Exploration Objective'); expect(component.getExplorationObjective()).toBe('Exploration Objective'); }); - it('should get exploration category', function() { + it('should get exploration category', function () { explorationCategoryService.init('Exploration Category'); expect(component.getExplorationCategory()).toBe('Exploration Category'); }); - it('should get thumbnail icon url', function() { + it('should get thumbnail icon url', function () { explorationCategoryService.init('Astrology'); expect(component.getThumbnailIconUrl()).toBe('/subjects/Lightbulb.svg'); }); - it('should get thumbnail bg color if category is listed', function() { + it('should get thumbnail bg color if category is listed', function () { explorationCategoryService.init('Algebra'); expect(component.getThumbnailBgColor()).toBe('#cc4b00'); }); - it('should get thumbnail bg color if category is not listed', function() { + it('should get thumbnail bg color if category is not listed', function () { explorationCategoryService.init('Astrology'); expect(component.getThumbnailBgColor()).toBe('#a33f40'); }); diff --git a/core/templates/pages/exploration-editor-page/settings-tab/templates/reassign-role-confirmation-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/settings-tab/templates/reassign-role-confirmation-modal.component.spec.ts index f2fa3ffc03e5..a0528967fc5d 100644 --- a/core/templates/pages/exploration-editor-page/settings-tab/templates/reassign-role-confirmation-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/settings-tab/templates/reassign-role-confirmation-modal.component.spec.ts @@ -16,10 +16,10 @@ * @fileoverview Unit tests for ReassignRoleConfirmationModalComponent. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ReassignRoleConfirmationModalComponent } from './reassign-role-confirmation-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ReassignRoleConfirmationModalComponent} from './reassign-role-confirmation-modal.component'; class MockActiveModal { close(): void { @@ -31,20 +31,20 @@ class MockActiveModal { } } -describe('Remove Role confirmation modal ', function() { +describe('Remove Role confirmation modal ', function () { let component: ReassignRoleConfirmationModalComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - ReassignRoleConfirmationModalComponent + declarations: [ReassignRoleConfirmationModalComponent], + providers: [ + { + provide: NgbActiveModal, + useClass: MockActiveModal, + }, ], - providers: [{ - provide: NgbActiveModal, - useClass: MockActiveModal - }], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); diff --git a/core/templates/pages/exploration-editor-page/settings-tab/templates/reassign-role-confirmation-modal.component.ts b/core/templates/pages/exploration-editor-page/settings-tab/templates/reassign-role-confirmation-modal.component.ts index 77224d3cb0e8..f4866bdfb818 100644 --- a/core/templates/pages/exploration-editor-page/settings-tab/templates/reassign-role-confirmation-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/settings-tab/templates/reassign-role-confirmation-modal.component.ts @@ -16,24 +16,20 @@ * @fileoverview Component for reassign role confirmation modal. */ -import { Component, Input } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component, Input} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-remove-role-confirmation-modal', - templateUrl: './reassign-role-confirmation-modal.component.html' + templateUrl: './reassign-role-confirmation-modal.component.html', }) - -export class ReassignRoleConfirmationModalComponent - extends ConfirmOrCancelModal { +export class ReassignRoleConfirmationModalComponent extends ConfirmOrCancelModal { @Input() username!: string; @Input() newRole!: string; @Input() oldRole!: string; - constructor( - private ngbActiveModal: NgbActiveModal, - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } } diff --git a/core/templates/pages/exploration-editor-page/settings-tab/templates/remove-role-confirmation-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/settings-tab/templates/remove-role-confirmation-modal.component.spec.ts index 1900a1bca918..8e35abfa9654 100644 --- a/core/templates/pages/exploration-editor-page/settings-tab/templates/remove-role-confirmation-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/settings-tab/templates/remove-role-confirmation-modal.component.spec.ts @@ -16,10 +16,10 @@ * @fileoverview Unit tests for RemoveRoleConfirmationModalComponent. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { RemoveRoleConfirmationModalComponent } from './remove-role-confirmation-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {RemoveRoleConfirmationModalComponent} from './remove-role-confirmation-modal.component'; class MockActiveModal { close(): void { @@ -31,20 +31,20 @@ class MockActiveModal { } } -describe('Remove role confirmation modal component', function() { +describe('Remove role confirmation modal component', function () { let component: RemoveRoleConfirmationModalComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - RemoveRoleConfirmationModalComponent + declarations: [RemoveRoleConfirmationModalComponent], + providers: [ + { + provide: NgbActiveModal, + useClass: MockActiveModal, + }, ], - providers: [{ - provide: NgbActiveModal, - useClass: MockActiveModal - }], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); diff --git a/core/templates/pages/exploration-editor-page/settings-tab/templates/remove-role-confirmation-modal.component.ts b/core/templates/pages/exploration-editor-page/settings-tab/templates/remove-role-confirmation-modal.component.ts index b1620b5ca503..67160ccde625 100644 --- a/core/templates/pages/exploration-editor-page/settings-tab/templates/remove-role-confirmation-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/settings-tab/templates/remove-role-confirmation-modal.component.ts @@ -16,22 +16,19 @@ * @fileoverview Component for remove role confirmation modal. */ -import { Component, Input } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component, Input} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-remove-role-confirmation-modal', - templateUrl: './remove-role-confirmation-modal.component.html' + templateUrl: './remove-role-confirmation-modal.component.html', }) - export class RemoveRoleConfirmationModalComponent extends ConfirmOrCancelModal { @Input() username!: string; @Input() role!: string; - constructor( - private ngbActiveModal: NgbActiveModal, - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } } diff --git a/core/templates/pages/exploration-editor-page/settings-tab/templates/transfer-exploration-ownership-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/settings-tab/templates/transfer-exploration-ownership-modal.component.spec.ts index b5c825c39d7c..e3f7cac80e9a 100644 --- a/core/templates/pages/exploration-editor-page/settings-tab/templates/transfer-exploration-ownership-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/settings-tab/templates/transfer-exploration-ownership-modal.component.spec.ts @@ -12,15 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Unit tests for the TransferExplorationOwnershipModalComponent. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { TransferExplorationOwnershipModalComponent } from './transfer-exploration-ownership-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {TransferExplorationOwnershipModalComponent} from './transfer-exploration-ownership-modal.component'; class MockActiveModal { close(): void { @@ -32,26 +31,27 @@ class MockActiveModal { } } -describe('Transfer Exploration Ownership Modal', function() { +describe('Transfer Exploration Ownership Modal', function () { let component: TransferExplorationOwnershipModalComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - TransferExplorationOwnershipModalComponent + declarations: [TransferExplorationOwnershipModalComponent], + providers: [ + { + provide: NgbActiveModal, + useClass: MockActiveModal, + }, ], - providers: [{ - provide: NgbActiveModal, - useClass: MockActiveModal - }], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed - .createComponent(TransferExplorationOwnershipModalComponent); + fixture = TestBed.createComponent( + TransferExplorationOwnershipModalComponent + ); component = fixture.componentInstance; TestBed.inject(NgbActiveModal); diff --git a/core/templates/pages/exploration-editor-page/settings-tab/templates/transfer-exploration-ownership-modal.component.ts b/core/templates/pages/exploration-editor-page/settings-tab/templates/transfer-exploration-ownership-modal.component.ts index ae33754b0b4b..b11f2226856b 100644 --- a/core/templates/pages/exploration-editor-page/settings-tab/templates/transfer-exploration-ownership-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/settings-tab/templates/transfer-exploration-ownership-modal.component.ts @@ -16,19 +16,16 @@ * @fileoverview Component for transfer exploration ownership modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-transfer-exploration-ownership-modal', - templateUrl: './transfer-exploration-ownership-modal.component.html' + templateUrl: './transfer-exploration-ownership-modal.component.html', }) -export class TransferExplorationOwnershipModalComponent - extends ConfirmOrCancelModal { - constructor( - private ngbActiveModal: NgbActiveModal, - ) { +export class TransferExplorationOwnershipModalComponent extends ConfirmOrCancelModal { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } } diff --git a/core/templates/pages/exploration-editor-page/statistics-tab/charts/pie-chart.component.spec.ts b/core/templates/pages/exploration-editor-page/statistics-tab/charts/pie-chart.component.spec.ts index cda2b2857f79..a0dc4c7d4291 100644 --- a/core/templates/pages/exploration-editor-page/statistics-tab/charts/pie-chart.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/statistics-tab/charts/pie-chart.component.spec.ts @@ -16,12 +16,18 @@ * @fileoverview Unit tests for pieChart. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed, fakeAsync, tick } from '@angular/core/testing'; -import { FormsModule } from '@angular/forms'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { PieChartComponent } from './pie-chart.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + waitForAsync, + TestBed, + fakeAsync, + tick, +} from '@angular/core/testing'; +import {FormsModule} from '@angular/forms'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {PieChartComponent} from './pie-chart.component'; describe('Pie Chart component', () => { let component: PieChartComponent; @@ -37,31 +43,25 @@ describe('Pie Chart component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - FormsModule, - ], - declarations: [ - PieChartComponent - ], + imports: [HttpClientTestingModule, FormsModule], + declarations: [PieChartComponent], providers: [ { provide: WindowDimensionsService, - useClass: MockWindowDimensionsService - } + useClass: MockWindowDimensionsService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent( - PieChartComponent); + fixture = TestBed.createComponent(PieChartComponent); component = fixture.componentInstance; mockedChart = { - draw: () => { }, - data: 1 + draw: () => {}, + data: 1, } as unknown as google.visualization.PieChart; // This approach was choosen because spyOnProperty() doesn't work on @@ -72,7 +72,7 @@ describe('Pie Chart component', () => { // ref: https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty // ref: https://github.com/jasmine/jasmine/issues/1415 Object.defineProperty(window, 'google', { - get: () => ({}) + get: () => ({}), }); component.data = []; component.chart = mockedChart; @@ -86,7 +86,7 @@ describe('Pie Chart component', () => { chartAreaWidth: 0, colors: [], height: 0, - width: 0 + width: 0, }; }); @@ -100,13 +100,13 @@ describe('Pie Chart component', () => { arrayToDataTable: () => {}, PieChart: () => { return mockedChart; - } + }, }, charts: { setOnLoadCallback: (callback: () => void) => { callback(); - } - } + }, + }, } as unknown as typeof google); component.redrawChart(); tick(); @@ -123,16 +123,16 @@ describe('Pie Chart component', () => { PieChart: class Mockdraw { constructor(value: string) {} draw() {} - } + }, }, charts: { setOnLoadCallback: (callback: () => void) => { callback(); - } - } + }, + }, } as unknown as typeof google); component.pieChart = { - nativeElement: null + nativeElement: null, }; // This throws "Type 'null' is not assignable to // parameter of type 'Piechart'." We need to suppress this error @@ -152,13 +152,13 @@ describe('Pie Chart component', () => { PieChart: class Mockdraw { constructor(value: string) {} draw() {} - } + }, }, charts: { setOnLoadCallback: (callback: () => void) => { callback(); - } - } + }, + }, } as unknown as typeof google); spyOn(component, 'redrawChart').and.stub(); component.chart = mockedChart; @@ -175,13 +175,13 @@ describe('Pie Chart component', () => { PieChart: class Mockdraw { constructor(value: string) {} draw() {} - } + }, }, charts: { setOnLoadCallback: (callback: () => void) => { callback(); - } - } + }, + }, } as unknown as typeof google); spyOn(component, 'redrawChart').and.stub(); diff --git a/core/templates/pages/exploration-editor-page/statistics-tab/charts/pie-chart.component.ts b/core/templates/pages/exploration-editor-page/statistics-tab/charts/pie-chart.component.ts index c712cc71c991..80ea2db4b42b 100644 --- a/core/templates/pages/exploration-editor-page/statistics-tab/charts/pie-chart.component.ts +++ b/core/templates/pages/exploration-editor-page/statistics-tab/charts/pie-chart.component.ts @@ -16,107 +16,118 @@ * @fileoverview Directive for pie chart visualization. */ -import { AfterViewInit, Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { Subscription } from 'rxjs'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; +import { + AfterViewInit, + Component, + ElementRef, + Input, + OnDestroy, + OnInit, + ViewChild, +} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {Subscription} from 'rxjs'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; -export type ChartLegendPosition = ( - 'bottom' | 'left' | 'in' | 'none' | 'right' | 'top'); +export type ChartLegendPosition = + | 'bottom' + | 'left' + | 'in' + | 'none' + | 'right' + | 'top'; @Component({ selector: 'oppia-pie-chart', - templateUrl: './pie-chart.component.html' + templateUrl: './pie-chart.component.html', }) export class PieChartComponent implements OnInit, OnDestroy, AfterViewInit { - // These properties are initialized using Angular lifecycle hooks - // and we need to do non-null assertion. For more information, see - // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 - @ViewChild('pieChart') pieChart!: ElementRef; + // These properties are initialized using Angular lifecycle hooks + // and we need to do non-null assertion. For more information, see + // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 + @ViewChild('pieChart') pieChart!: ElementRef; - // A read-only array representing the table of chart data. - @Input() data!: string[]; - // A read-only object containing several chart options. This object - // should have the following keys: pieHole, pieSliceTextStyleColor, - // chartAreaWidth, colors, height, legendPosition, width. - @Input() options!: { - chartAreaWidth: number; - colors: string[]; - height: number; - left: number; - legendPosition: string; - pieHole: number; - pieSliceBorderColor: string; - pieSliceTextStyleColor: string; - title: string; - width: number; - }; + // A read-only array representing the table of chart data. + @Input() data!: string[]; + // A read-only object containing several chart options. This object + // should have the following keys: pieHole, pieSliceTextStyleColor, + // chartAreaWidth, colors, height, legendPosition, width. + @Input() options!: { + chartAreaWidth: number; + colors: string[]; + height: number; + left: number; + legendPosition: string; + pieHole: number; + pieSliceBorderColor: string; + pieSliceTextStyleColor: string; + title: string; + width: number; + }; - chart!: google.visualization.PieChart; - directiveSubscriptions = new Subscription(); + chart!: google.visualization.PieChart; + directiveSubscriptions = new Subscription(); - constructor( - private windowDimensionsService: WindowDimensionsService, - ) { } + constructor(private windowDimensionsService: WindowDimensionsService) {} - redrawChart(): void { - if (this.chart !== null) { - (this.chart) - .draw(google.visualization.arrayToDataTable(this.data), { - title: this.options.title, - pieHole: this.options.pieHole, - pieSliceTextStyle: { - color: this.options.pieSliceTextStyleColor, - }, - pieSliceBorderColor: this.options.pieSliceBorderColor, - pieSliceText: 'none', - chartArea: { - left: this.options.left, - width: this.options.chartAreaWidth - }, - colors: this.options.colors, - height: this.options.height, - legend: { - position: ( - this.options.legendPosition || 'none') as ChartLegendPosition - }, - width: this.options.width - }); - } - } + redrawChart(): void { + if (this.chart !== null) { + this.chart.draw(google.visualization.arrayToDataTable(this.data), { + title: this.options.title, + pieHole: this.options.pieHole, + pieSliceTextStyle: { + color: this.options.pieSliceTextStyleColor, + }, + pieSliceBorderColor: this.options.pieSliceBorderColor, + pieSliceText: 'none', + chartArea: { + left: this.options.left, + width: this.options.chartAreaWidth, + }, + colors: this.options.colors, + height: this.options.height, + legend: { + position: (this.options.legendPosition || + 'none') as ChartLegendPosition, + }, + width: this.options.width, + }); + } + } - ngAfterViewInit(): void { - // Need to wait for load statement in editor template to finish. - // https://stackoverflow.com/questions/42714876/ - google.charts.setOnLoadCallback(() => { - if (!this.chart) { - setTimeout(() => { - this.chart = new google.visualization.PieChart( - this.pieChart.nativeElement); - this.redrawChart(); - }); - } - }); - } + ngAfterViewInit(): void { + // Need to wait for load statement in editor template to finish. + // https://stackoverflow.com/questions/42714876/ + google.charts.setOnLoadCallback(() => { + if (!this.chart) { + setTimeout(() => { + this.chart = new google.visualization.PieChart( + this.pieChart.nativeElement + ); + this.redrawChart(); + }); + } + }); + } - ngOnInit(): void { - this.directiveSubscriptions.add( - this.windowDimensionsService.getResizeEvent().subscribe( - () => { - setTimeout(() => { - this.redrawChart(); - }); - } - ) - ); - } + ngOnInit(): void { + this.directiveSubscriptions.add( + this.windowDimensionsService.getResizeEvent().subscribe(() => { + setTimeout(() => { + this.redrawChart(); + }); + }) + ); + } - ngOnDestroy(): void { - this.directiveSubscriptions.unsubscribe(); - } + ngOnDestroy(): void { + this.directiveSubscriptions.unsubscribe(); + } } -angular.module('oppia').directive('oppiaPieChart', - downgradeComponent({ - component: PieChartComponent - }) as angular.IDirectiveFactory); +angular.module('oppia').directive( + 'oppiaPieChart', + downgradeComponent({ + component: PieChartComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-editor-page/statistics-tab/statistics-tab.component.spec.ts b/core/templates/pages/exploration-editor-page/statistics-tab/statistics-tab.component.spec.ts index dc2c18b9da04..76609445c8aa 100644 --- a/core/templates/pages/exploration-editor-page/statistics-tab/statistics-tab.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/statistics-tab/statistics-tab.component.spec.ts @@ -16,23 +16,36 @@ * @fileoverview Unit tests for statisticsTab. */ -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { ExplorationStatsService } from 'services/exploration-stats.service'; -import { StateInteractionStats, StateInteractionStatsService } from - 'services/state-interaction-stats.service'; -import { States, StatesObjectFactory } from 'domain/exploration/StatesObjectFactory'; -import { AlertsService } from 'services/alerts.service'; -import { ComputeGraphService } from 'services/compute-graph.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { FetchExplorationBackendResponse, ReadOnlyExplorationBackendApiService } from 'domain/exploration/read-only-exploration-backend-api.service'; -import { RouterService } from '../services/router.service'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { FormsModule } from '@angular/forms'; -import { StatisticsTabComponent } from './statistics-tab.component'; -import { ExplorationDataService } from '../services/exploration-data.service'; -import { ExplorationStats } from 'domain/statistics/exploration-stats.model'; -import { StateStatsModalComponent } from './templates/state-stats-modal.component'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {ExplorationStatsService} from 'services/exploration-stats.service'; +import { + StateInteractionStats, + StateInteractionStatsService, +} from 'services/state-interaction-stats.service'; +import { + States, + StatesObjectFactory, +} from 'domain/exploration/StatesObjectFactory'; +import {AlertsService} from 'services/alerts.service'; +import {ComputeGraphService} from 'services/compute-graph.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + FetchExplorationBackendResponse, + ReadOnlyExplorationBackendApiService, +} from 'domain/exploration/read-only-exploration-backend-api.service'; +import {RouterService} from '../services/router.service'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {FormsModule} from '@angular/forms'; +import {StatisticsTabComponent} from './statistics-tab.component'; +import {ExplorationDataService} from '../services/exploration-data.service'; +import {ExplorationStats} from 'domain/statistics/exploration-stats.model'; +import {StateStatsModalComponent} from './templates/state-stats-modal.component'; describe('Statistics Tab Component', () => { let component: StatisticsTabComponent; @@ -41,8 +54,7 @@ describe('Statistics Tab Component', () => { let alertsService: AlertsService; let computeGraphService: ComputeGraphService; let explorationStatsService: ExplorationStatsService; - let readOnlyExplorationBackendApiService: - ReadOnlyExplorationBackendApiService; + let readOnlyExplorationBackendApiService: ReadOnlyExplorationBackendApiService; let stateInteractionStatsService: StateInteractionStatsService; let statesObjectFactory: StatesObjectFactory; let refreshStatisticsTabEventEmitter = new EventEmitter(); @@ -55,33 +67,26 @@ describe('Statistics Tab Component', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - FormsModule, - ], - declarations: [ - StateStatsModalComponent, - StatisticsTabComponent - ], + imports: [HttpClientTestingModule, FormsModule], + declarations: [StateStatsModalComponent, StatisticsTabComponent], providers: [ { provide: ExplorationDataService, useValue: { - explorationId: explorationId - } + explorationId: explorationId, + }, }, { provide: RouterService, - useClass: MockRouterService - } + useClass: MockRouterService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); beforeEach(() => { - fixture = TestBed.createComponent( - StatisticsTabComponent); + fixture = TestBed.createComponent(StatisticsTabComponent); component = fixture.componentInstance; alertsService = TestBed.inject(AlertsService); @@ -89,11 +94,10 @@ describe('Statistics Tab Component', () => { stateInteractionStatsService = TestBed.inject(StateInteractionStatsService); ngbModal = TestBed.inject(NgbModal); readOnlyExplorationBackendApiService = TestBed.inject( - ReadOnlyExplorationBackendApiService); - statesObjectFactory = TestBed.inject( - StatesObjectFactory); - computeGraphService = TestBed.inject( - ComputeGraphService); + ReadOnlyExplorationBackendApiService + ); + statesObjectFactory = TestBed.inject(StatesObjectFactory); + computeGraphService = TestBed.inject(ComputeGraphService); // This throws "Argument of type 'null' is not assignable to // parameter of type 'State'." We need to suppress this error @@ -113,8 +117,8 @@ describe('Statistics Tab Component', () => { feedback_1: {}, rule_input_2: {}, content: {}, - default_outcome: {} - } + default_outcome: {}, + }, }, solicit_answer_details: false, interaction: { @@ -124,14 +128,14 @@ describe('Statistics Tab Component', () => { hints: [], customization_args: { rows: { - value: 1 + value: 1, }, placeholder: { value: { unicode_str: '', - content_id: 'ca_placeholder_0' - } - } + content_id: 'ca_placeholder_0', + }, + }, }, answer_groups: [ { @@ -141,28 +145,26 @@ describe('Statistics Tab Component', () => { labelled_as_correct: false, feedback: { content_id: 'feedback_1', - html: '

Good Job

' + html: '

Good Job

', }, param_changes: [], dest_if_really_stuck: null, - dest: 'Mid' + dest: 'Mid', }, training_data: [], rule_specs: [ { inputs: { x: { - normalizedStrSet: [ - 'answer' - ], - contentId: 'rule_input_2' - } + normalizedStrSet: ['answer'], + contentId: 'rule_input_2', + }, }, - rule_type: 'FuzzyEquals' - } + rule_type: 'FuzzyEquals', + }, ], - tagged_skill_misconception_id: null - } + tagged_skill_misconception_id: null, + }, ], default_outcome: { missing_prerequisite_skill_id: null, @@ -170,27 +172,27 @@ describe('Statistics Tab Component', () => { labelled_as_correct: false, feedback: { content_id: 'default_outcome', - html: '

Try again.

' + html: '

Try again.

', }, param_changes: [], dest_if_really_stuck: null, - dest: 'Start' - } + dest: 'Start', + }, }, param_changes: [], card_is_checkpoint: true, linked_skill_id: null, content: { content_id: 'content', - html: '

First Question

' - } + html: '

First Question

', + }, }, End: { classifier_model_id: null, recorded_voiceovers: { voiceovers_mapping: { - content: {} - } + content: {}, + }, }, solicit_answer_details: false, interaction: { @@ -200,19 +202,19 @@ describe('Statistics Tab Component', () => { hints: [], customization_args: { recommendedExplorationIds: { - value: ['recommnendedExplorationId'] - } + value: ['recommnendedExplorationId'], + }, }, answer_groups: [], - default_outcome: null + default_outcome: null, }, param_changes: [], card_is_checkpoint: false, linked_skill_id: null, content: { content_id: 'content', - html: 'Congratulations, you have finished!' - } + html: 'Congratulations, you have finished!', + }, }, Mid: { classifier_model_id: null, @@ -222,8 +224,8 @@ describe('Statistics Tab Component', () => { feedback_1: {}, rule_input_2: {}, content: {}, - default_outcome: {} - } + default_outcome: {}, + }, }, solicit_answer_details: false, interaction: { @@ -233,14 +235,14 @@ describe('Statistics Tab Component', () => { hints: [], customization_args: { rows: { - value: 1 + value: 1, }, placeholder: { value: { unicode_str: '', - content_id: 'ca_placeholder_0' - } - } + content_id: 'ca_placeholder_0', + }, + }, }, answer_groups: [ { @@ -250,28 +252,26 @@ describe('Statistics Tab Component', () => { labelled_as_correct: false, feedback: { content_id: 'feedback_1', - html: '

Good Job

' + html: '

Good Job

', }, param_changes: [], dest_if_really_stuck: null, - dest: 'End' + dest: 'End', }, training_data: [], rule_specs: [ { inputs: { x: { - normalizedStrSet: [ - 'answer' - ], - contentId: 'rule_input_2' - } + normalizedStrSet: ['answer'], + contentId: 'rule_input_2', + }, }, - rule_type: 'FuzzyEquals' - } + rule_type: 'FuzzyEquals', + }, ], - tagged_skill_misconception_id: null - } + tagged_skill_misconception_id: null, + }, ], default_outcome: { missing_prerequisite_skill_id: null, @@ -279,21 +279,21 @@ describe('Statistics Tab Component', () => { labelled_as_correct: false, feedback: { content_id: 'default_outcome', - html: '

try again.

' + html: '

try again.

', }, param_changes: [], dest_if_really_stuck: null, - dest: 'Mid' - } + dest: 'Mid', + }, }, param_changes: [], card_is_checkpoint: false, linked_skill_id: null, content: { content_id: 'content', - html: '

Second Question

' - } - } + html: '

Second Question

', + }, + }, }, auto_tts_enabled: true, version: 2, @@ -319,7 +319,7 @@ describe('Statistics Tab Component', () => { language_code: 'en', objective: 'To learn', states: explorationDict.states, - next_content_id_index: 6 + next_content_id_index: 6, }, exploration_metadata: { title: 'Exploration', @@ -334,7 +334,7 @@ describe('Statistics Tab Component', () => { param_specs: {}, param_changes: [], auto_tts_enabled: false, - edits_allowed: true + edits_allowed: true, }, version: 2, can_edit: true, @@ -348,36 +348,40 @@ describe('Statistics Tab Component', () => { furthest_reached_checkpoint_state_name: 'End', most_recently_reached_checkpoint_state_name: 'Mid', most_recently_reached_checkpoint_exp_version: 2, - displayable_language_codes: [] + displayable_language_codes: [], }; - spyOn(readOnlyExplorationBackendApiService, 'loadLatestExplorationAsync') - .and.returnValue(Promise.resolve(explorationResponse)); + spyOn( + readOnlyExplorationBackendApiService, + 'loadLatestExplorationAsync' + ).and.returnValue(Promise.resolve(explorationResponse)); spyOn(explorationStatsService, 'getExplorationStatsAsync').and.returnValue( Promise.resolve({ numStarts: 20, numActualStarts: 10, numCompletions: 5, - } as ExplorationStats)); + } as ExplorationStats) + ); spyOn(stateInteractionStatsService, 'computeStatsAsync').and.returnValue( Promise.resolve({ - visualizationsInfo: {} - } as StateInteractionStats)); + visualizationsInfo: {}, + } as StateInteractionStats) + ); - spyOn (computeGraphService, 'compute').and.stub(); + spyOn(computeGraphService, 'compute').and.stub(); component.states = { getState: (name: string) => { return { interaction: { customizationArgs: null, - } + }, }; - } + }, } as unknown as States; component.expStats = { - getStateStats: (name: string) => null + getStateStats: (name: string) => null, } as unknown as ExplorationStats; component.ngOnInit(); @@ -387,65 +391,73 @@ describe('Statistics Tab Component', () => { component.ngOnDestroy(); }); - it('should initialize controller properties after its initialization', - () => { - expect(component.stateStatsModalIsOpen).toBe(false); - expect(component.explorationHasBeenVisited).toBe(false); - }); + it('should initialize controller properties after its initialization', () => { + expect(component.stateStatsModalIsOpen).toBe(false); + expect(component.explorationHasBeenVisited).toBe(false); + }); - it('should refresh exploration statistics when broadcasting' + - ' refreshStatisticsTab', fakeAsync(() => { - refreshStatisticsTabEventEmitter.emit(); - tick(); + it( + 'should refresh exploration statistics when broadcasting' + + ' refreshStatisticsTab', + fakeAsync(() => { + refreshStatisticsTabEventEmitter.emit(); + tick(); - expect(component.statsGraphData).toEqual(undefined); - expect(component.pieChartData).toEqual([ - ['Type', 'Number'], - ['Completions', 5], - ['Non-Completions', 5] - ]); - expect(component.numPassersby).toBe(10); - expect(component.explorationHasBeenVisited).toBe(true); - })); + expect(component.statsGraphData).toEqual(undefined); + expect(component.pieChartData).toEqual([ + ['Type', 'Number'], + ['Completions', 5], + ['Non-Completions', 5], + ]); + expect(component.numPassersby).toBe(10); + expect(component.explorationHasBeenVisited).toBe(true); + }) + ); - it('should open state stats modal and close it when clicking in stats' + - ' graph', fakeAsync(() => { - tick(); + it( + 'should open state stats modal and close it when clicking in stats' + + ' graph', + fakeAsync(() => { + tick(); - spyOn(ngbModal, 'open').and.returnValue({ - componentInstance: { - interactionArgs: '', - stateName: 'stateName', - visualizationsInfo: '', - stateStats: false - }, - result: Promise.resolve() - } as NgbModalRef); - tick(); + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: { + interactionArgs: '', + stateName: 'stateName', + visualizationsInfo: '', + stateStats: false, + }, + result: Promise.resolve(), + } as NgbModalRef); + tick(); - component.onClickStateInStatsGraph('id'); - tick(); + component.onClickStateInStatsGraph('id'); + tick(); - expect(component.stateStatsModalIsOpen).toBe(false); - })); + expect(component.stateStatsModalIsOpen).toBe(false); + }) + ); - it('should open state stats modal and dismiss it when clicking in' + - ' stats graph', fakeAsync(() => { - spyOn(ngbModal, 'open').and.returnValue({ - componentInstance: { - interactionArgs: '', - stateName: 'stateName', - visualizationsInfo: '', - stateStats: false - }, - result: Promise.reject() - } as NgbModalRef); - spyOn(alertsService, 'clearWarnings'); + it( + 'should open state stats modal and dismiss it when clicking in' + + ' stats graph', + fakeAsync(() => { + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: { + interactionArgs: '', + stateName: 'stateName', + visualizationsInfo: '', + stateStats: false, + }, + result: Promise.reject(), + } as NgbModalRef); + spyOn(alertsService, 'clearWarnings'); - component.onClickStateInStatsGraph('State1'); - tick(); + component.onClickStateInStatsGraph('State1'); + tick(); - expect(component.stateStatsModalIsOpen).toBe(false); - expect(alertsService.clearWarnings).toHaveBeenCalled(); - })); + expect(component.stateStatsModalIsOpen).toBe(false); + expect(alertsService.clearWarnings).toHaveBeenCalled(); + }) + ); }); diff --git a/core/templates/pages/exploration-editor-page/statistics-tab/statistics-tab.component.ts b/core/templates/pages/exploration-editor-page/statistics-tab/statistics-tab.component.ts index 3b8266f77516..0893a8cd81de 100644 --- a/core/templates/pages/exploration-editor-page/statistics-tab/statistics-tab.component.ts +++ b/core/templates/pages/exploration-editor-page/statistics-tab/statistics-tab.component.ts @@ -17,21 +17,24 @@ * exploration editor. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { ReadOnlyExplorationBackendApiService } from 'domain/exploration/read-only-exploration-backend-api.service'; -import { States, StatesObjectFactory } from 'domain/exploration/StatesObjectFactory'; -import { ExplorationStats } from 'domain/statistics/exploration-stats.model'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { Subscription } from 'rxjs'; -import { AlertsService } from 'services/alerts.service'; -import { ComputeGraphService, GraphData } from 'services/compute-graph.service'; -import { ExplorationStatsService } from 'services/exploration-stats.service'; -import { StateInteractionStatsService } from 'services/state-interaction-stats.service'; -import { ExplorationDataService } from '../services/exploration-data.service'; -import { RouterService } from '../services/router.service'; -import { StateStatsModalComponent } from './templates/state-stats-modal.component'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {ReadOnlyExplorationBackendApiService} from 'domain/exploration/read-only-exploration-backend-api.service'; +import { + States, + StatesObjectFactory, +} from 'domain/exploration/StatesObjectFactory'; +import {ExplorationStats} from 'domain/statistics/exploration-stats.model'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {Subscription} from 'rxjs'; +import {AlertsService} from 'services/alerts.service'; +import {ComputeGraphService, GraphData} from 'services/compute-graph.service'; +import {ExplorationStatsService} from 'services/exploration-stats.service'; +import {StateInteractionStatsService} from 'services/state-interaction-stats.service'; +import {ExplorationDataService} from '../services/exploration-data.service'; +import {RouterService} from '../services/router.service'; +import {StateStatsModalComponent} from './templates/state-stats-modal.component'; interface PieChartOptions { chartAreaWidth: number; @@ -48,10 +51,9 @@ interface PieChartOptions { @Component({ selector: 'oppia-statistics-tab', - templateUrl: './statistics-tab.component.html' + templateUrl: './statistics-tab.component.html', }) -export class StatisticsTabComponent implements - OnInit, OnDestroy { +export class StatisticsTabComponent implements OnInit, OnDestroy { directiveSubscriptions = new Subscription(); // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see @@ -59,7 +61,7 @@ export class StatisticsTabComponent implements stateStatsModalIsOpen!: boolean; explorationHasBeenVisited!: boolean; pieChartOptions!: PieChartOptions; - pieChartData!: ((string | number)[] | string[]) []; + pieChartData!: ((string | number)[] | string[])[]; statsGraphData!: GraphData; numPassersby!: number; states!: States; @@ -72,38 +74,40 @@ export class StatisticsTabComponent implements private explorationDataService: ExplorationDataService, private explorationStatsService: ExplorationStatsService, private ngbModal: NgbModal, - private readOnlyExplorationBackendApiService: - ReadOnlyExplorationBackendApiService, + private readOnlyExplorationBackendApiService: ReadOnlyExplorationBackendApiService, private routerService: RouterService, private stateInteractionStatsService: StateInteractionStatsService, private statesObjectFactory: StatesObjectFactory, - private urlInterpolationService: UrlInterpolationService, + private urlInterpolationService: UrlInterpolationService ) {} refreshExplorationStatistics(): void { Promise.all([ - this.readOnlyExplorationBackendApiService - .loadLatestExplorationAsync(this.expId), - this.explorationStatsService.getExplorationStatsAsync(this.expId) - ]).then((responses) => { + this.readOnlyExplorationBackendApiService.loadLatestExplorationAsync( + this.expId + ), + this.explorationStatsService.getExplorationStatsAsync(this.expId), + ]).then(responses => { const [expResponse, expStats] = responses; const initStateName = expResponse.exploration.init_state_name; - const numNonCompletions = ( - expStats.numActualStarts - expStats.numCompletions); + const numNonCompletions = + expStats.numActualStarts - expStats.numCompletions; this.states = this.statesObjectFactory.createFromBackendDict( - expResponse.exploration.states); + expResponse.exploration.states + ); this.expStats = expStats; - this.statsGraphData = ( - this.computeGraphService.compute(initStateName, this.states)); - this.numPassersby = ( - expStats.numStarts - expStats.numActualStarts); + this.statsGraphData = this.computeGraphService.compute( + initStateName, + this.states + ); + this.numPassersby = expStats.numStarts - expStats.numActualStarts; this.pieChartData = [ ['Type', 'Number'], ['Completions', expStats.numCompletions], - ['Non-Completions', numNonCompletions] + ['Non-Completions', numNonCompletions], ]; this.pieChartOptions = { @@ -132,27 +136,28 @@ export class StatisticsTabComponent implements const state = this.states.getState(stateName); this.alertsService.clearWarnings(); - this.stateInteractionStatsService.computeStatsAsync( - this.expId, state) - .then((stats) => { + this.stateInteractionStatsService + .computeStatsAsync(this.expId, state) + .then(stats => { const modalRef = this.ngbModal.open(StateStatsModalComponent, { backdrop: false, }); - modalRef.componentInstance.interactionArgs = ( - state.interaction.customizationArgs); + modalRef.componentInstance.interactionArgs = + state.interaction.customizationArgs; modalRef.componentInstance.stateName = stateName; - modalRef.componentInstance.visualizationsInfo = ( - stats.visualizationsInfo); - modalRef.componentInstance.stateStats = ( - this.expStats.getStateStats(stateName)); + modalRef.componentInstance.visualizationsInfo = + stats.visualizationsInfo; + modalRef.componentInstance.stateStats = + this.expStats.getStateStats(stateName); modalRef.result.then( - () => this.stateStatsModalIsOpen = false, + () => (this.stateStatsModalIsOpen = false), () => { this.alertsService.clearWarnings(); this.stateStatsModalIsOpen = false; - }); + } + ); }); } } @@ -164,8 +169,9 @@ export class StatisticsTabComponent implements this.explorationHasBeenVisited = false; this.directiveSubscriptions.add( - this.routerService.onRefreshStatisticsTab.subscribe( - () => this.refreshExplorationStatistics()) + this.routerService.onRefreshStatisticsTab.subscribe(() => + this.refreshExplorationStatistics() + ) ); this.refreshExplorationStatistics(); @@ -176,7 +182,9 @@ export class StatisticsTabComponent implements } } -angular.module('oppia').directive('oppiaStatisticsTab', - downgradeComponent({ - component: StatisticsTabComponent - }) as angular.IDirectiveFactory); +angular.module('oppia').directive( + 'oppiaStatisticsTab', + downgradeComponent({ + component: StatisticsTabComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-editor-page/statistics-tab/templates/state-stats-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/statistics-tab/templates/state-stats-modal.component.spec.ts index 211d555bf201..69882b37c688 100644 --- a/core/templates/pages/exploration-editor-page/statistics-tab/templates/state-stats-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/statistics-tab/templates/state-stats-modal.component.spec.ts @@ -16,12 +16,12 @@ * @fileoverview Unit tests for StateStatsModalComponent. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { RouterService } from 'pages/exploration-editor-page/services/router.service'; -import { StateStatsModalComponent } from './state-stats-modal.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {RouterService} from 'pages/exploration-editor-page/services/router.service'; +import {StateStatsModalComponent} from './state-stats-modal.component'; class MockActiveModal { close(): void { @@ -45,30 +45,29 @@ describe('State Stats Modal Component', () => { totalAnswersCount: 10, numTimesSolutionViewed: 4, totalHitCount: 13, - numCompletions: 8 - }; - let visualizationsInfo = [{ - data: 'Hola', - options: 'Options', - id: '1', - addressed_info_is_supported: true - }]; - let interactionArgs = { + numCompletions: 8, }; + let visualizationsInfo = [ + { + data: 'Hola', + options: 'Options', + id: '1', + addressed_info_is_supported: true, + }, + ]; + let interactionArgs = {}; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - StateStatsModalComponent - ], + declarations: [StateStatsModalComponent], providers: [ { provide: NgbActiveModal, - useClass: MockActiveModal + useClass: MockActiveModal, }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -90,15 +89,15 @@ describe('State Stats Modal Component', () => { fixture.detectChanges(); }); - it('should initialize component properties after component is initialized', - () => { - expect(component.stateName).toBe(stateName); - expect(component.numEnters).toEqual(stateStats.totalHitCount); - expect(component.numQuits) - .toEqual(stateStats.totalHitCount - stateStats.numCompletions); - expect(component.interactionArgs).toBe(interactionArgs); - expect(component.visualizationsInfo).toEqual(visualizationsInfo); - }); + it('should initialize component properties after component is initialized', () => { + expect(component.stateName).toBe(stateName); + expect(component.numEnters).toEqual(stateStats.totalHitCount); + expect(component.numQuits).toEqual( + stateStats.totalHitCount - stateStats.numCompletions + ); + expect(component.interactionArgs).toBe(interactionArgs); + expect(component.visualizationsInfo).toEqual(visualizationsInfo); + }); it('should navigate to state editor', () => { spyOn(ngbActiveModal, 'dismiss').and.stub(); @@ -117,7 +116,7 @@ describe('State Stats Modal Component', () => { height: 270, legendPosition: 'right', title: 'title', - width: 240 + width: 240, }); expect(ngbActiveModal.dismiss).toHaveBeenCalledWith('cancel'); expect(routerService.navigateToMainTab).toHaveBeenCalledWith(stateName); diff --git a/core/templates/pages/exploration-editor-page/statistics-tab/templates/state-stats-modal.component.ts b/core/templates/pages/exploration-editor-page/statistics-tab/templates/state-stats-modal.component.ts index 1355851bc3cf..0e1ab4c6d716 100644 --- a/core/templates/pages/exploration-editor-page/statistics-tab/templates/state-stats-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/statistics-tab/templates/state-stats-modal.component.ts @@ -16,11 +16,11 @@ * @fileoverview Component for state stats modal. */ -import { Component, Input, OnInit } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { InteractionCustomizationArgs } from 'interactions/customization-args-defs'; -import { RouterService } from 'pages/exploration-editor-page/services/router.service'; +import {Component, Input, OnInit} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {InteractionCustomizationArgs} from 'interactions/customization-args-defs'; +import {RouterService} from 'pages/exploration-editor-page/services/router.service'; import './state-stats-modal.component.css'; @@ -39,10 +39,12 @@ interface PieChartOpitons { @Component({ selector: 'oppia-state-stats-modal', - templateUrl: './state-stats-modal.component.html' + templateUrl: './state-stats-modal.component.html', }) export class StateStatsModalComponent - extends ConfirmOrCancelModal implements OnInit { + extends ConfirmOrCancelModal + implements OnInit +{ // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -51,7 +53,7 @@ export class StateStatsModalComponent options: string; id: string; addressed_info_is_supported: boolean; - } []; + }[]; @Input() interactionArgs!: InteractionCustomizationArgs; @Input() stateName!: string; @@ -75,8 +77,8 @@ export class StateStatsModalComponent hasExplorationBeenAnswered!: boolean; constructor( - private ngbActiveModal: NgbActiveModal, - private routerService: RouterService, + private ngbActiveModal: NgbActiveModal, + private routerService: RouterService ) { super(ngbActiveModal); } @@ -97,7 +99,7 @@ export class StateStatsModalComponent height: 270, legendPosition: 'right', title: title, - width: 240 + width: 240, }; } @@ -108,13 +110,15 @@ export class StateStatsModalComponent this.hasExplorationBeenAnswered = this.totalAnswersCount > 0; this.numEnters = this.stateStats.totalHitCount; - this.numQuits = ( - this.stateStats.totalHitCount - this.stateStats.numCompletions); + this.numQuits = + this.stateStats.totalHitCount - this.stateStats.numCompletions; - this.answerFeedbackPieChartOptions = ( - this.makeCompletionRatePieChartOptions('Answer feedback statistics')); - this.solutionUsagePieChartOptions = ( - this.makeCompletionRatePieChartOptions('Solution usage statistics')); + this.answerFeedbackPieChartOptions = this.makeCompletionRatePieChartOptions( + 'Answer feedback statistics' + ); + this.solutionUsagePieChartOptions = this.makeCompletionRatePieChartOptions( + 'Solution usage statistics' + ); this.answerFeedbackPieChartData = [ ['Type', 'Number'], @@ -125,8 +129,10 @@ export class StateStatsModalComponent this.solutionUsagePieChartData = [ ['Type', 'Number'], ['Solutions used to answer', this.numTimesSolutionViewed], - ['Solutions not used', - this.totalAnswersCount - this.numTimesSolutionViewed] + [ + 'Solutions not used', + this.totalAnswersCount - this.numTimesSolutionViewed, + ], ]; } } diff --git a/core/templates/pages/exploration-editor-page/translation-tab/audio-translation-bar/audio-translation-bar.component.spec.ts b/core/templates/pages/exploration-editor-page/translation-tab/audio-translation-bar/audio-translation-bar.component.spec.ts index 6f6f2cfc1068..6a48bcb358ed 100644 --- a/core/templates/pages/exploration-editor-page/translation-tab/audio-translation-bar/audio-translation-bar.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/translation-tab/audio-translation-bar/audio-translation-bar.component.spec.ts @@ -16,35 +16,48 @@ * @fileoverview Unit tests for Audio Translation Bar component. */ -import { ElementRef, EventEmitter, NgZone, NO_ERRORS_SCHEMA, Pipe } from '@angular/core'; -import { ComponentFixture, fakeAsync, flush, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { StateRecordedVoiceoversService } from 'components/state-editor/state-editor-properties-services/state-recorded-voiceovers.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { RecordedVoiceovers } from 'domain/exploration/recorded-voiceovers.model'; -import { EditabilityService } from 'services/editability.service'; -import { AlertsService } from 'services/alerts.service'; -import { AudioPlayerService } from 'services/audio-player.service'; +import { + ElementRef, + EventEmitter, + NgZone, + NO_ERRORS_SCHEMA, + Pipe, +} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + flush, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {StateRecordedVoiceoversService} from 'components/state-editor/state-editor-properties-services/state-recorded-voiceovers.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {RecordedVoiceovers} from 'domain/exploration/recorded-voiceovers.model'; +import {EditabilityService} from 'services/editability.service'; +import {AlertsService} from 'services/alerts.service'; +import {AudioPlayerService} from 'services/audio-player.service'; import WaveSurfer from 'wavesurfer.js'; -import { OppiaAngularRootComponent } from 'components/oppia-angular-root.component'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; -import { UserExplorationPermissionsService } from 'pages/exploration-editor-page/services/user-exploration-permissions.service'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; -import { ContextService } from 'services/context.service'; -import { UserService } from 'services/user.service'; -import { TranslationLanguageService } from '../services/translation-language.service'; -import { TranslationTabActiveContentIdService } from '../services/translation-tab-active-content-id.service'; -import { VoiceoverRecordingService } from '../services/voiceover-recording.service'; -import { AudioTranslationBarComponent } from './audio-translation-bar.component'; -import { ExternalSaveService } from 'services/external-save.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ExplorationPermissions } from 'domain/exploration/exploration-permissions.model'; -import { UserInfo } from 'domain/user/user-info.model'; -import { Voiceover } from 'domain/exploration/voiceover.model'; -import { FormatTimePipe } from 'filters/format-timer.pipe'; - -@Pipe({ name: 'formatTime' }) +import {OppiaAngularRootComponent} from 'components/oppia-angular-root.component'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; +import {UserExplorationPermissionsService} from 'pages/exploration-editor-page/services/user-exploration-permissions.service'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; +import {ContextService} from 'services/context.service'; +import {UserService} from 'services/user.service'; +import {TranslationLanguageService} from '../services/translation-language.service'; +import {TranslationTabActiveContentIdService} from '../services/translation-tab-active-content-id.service'; +import {VoiceoverRecordingService} from '../services/voiceover-recording.service'; +import {AudioTranslationBarComponent} from './audio-translation-bar.component'; +import {ExternalSaveService} from 'services/external-save.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ExplorationPermissions} from 'domain/exploration/exploration-permissions.model'; +import {UserInfo} from 'domain/user/user-info.model'; +import {Voiceover} from 'domain/exploration/voiceover.model'; +import {FormatTimePipe} from 'filters/format-timer.pipe'; + +@Pipe({name: 'formatTime'}) class MockFormatTimePipe { transform(value: number): string { return String(value); @@ -64,8 +77,7 @@ describe('Audio translation bar Component', () => { let stateEditorService: StateEditorService; let stateRecordedVoiceoversService: StateRecordedVoiceoversService; let translationLanguageService: TranslationLanguageService; - let translationTabActiveContentIdService: - TranslationTabActiveContentIdService; + let translationTabActiveContentIdService: TranslationTabActiveContentIdService; let userExplorationPermissionsService: UserExplorationPermissionsService; let userService: UserService; let voiceoverRecordingService: VoiceoverRecordingService; @@ -82,707 +94,744 @@ describe('Audio translation bar Component', () => { var translationTabDivMock = null; var dropAreaMessageDivMock = null; - class MockExternalSaveService { - onExternalSave = mockExternalSaveEventEmitter; - } - - class MockNgbModal { - open() { - return { - result: Promise.resolve() - }; - } - } - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - imports: [HttpClientTestingModule], - declarations: [ - AudioTranslationBarComponent, - MockFormatTimePipe - ], - providers: [ - ContextService, - { - provide: NgbModal, - useClass: MockNgbModal - }, - { - provide: FormatTimePipe, - useClass: MockFormatTimePipe - }, - { - provide: ExternalSaveService, - useClass: MockExternalSaveService - } - ], - schemas: [NO_ERRORS_SCHEMA] - }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(AudioTranslationBarComponent); - component = fixture.componentInstance; - - alertsService = TestBed.inject(AlertsService); - editabilityService = TestBed.inject(EditabilityService); - siteAnalyticsService = TestBed.inject(SiteAnalyticsService); - stateRecordedVoiceoversService = TestBed.inject( - StateRecordedVoiceoversService); - zone = TestBed.inject(NgZone); - ngbModal = TestBed.inject(NgbModal); - assetsBackendApiService = TestBed.inject(AssetsBackendApiService); - audioPlayerService = TestBed.inject(AudioPlayerService); - contextService = TestBed.inject(ContextService); - userService = TestBed.inject(UserService); - userExplorationPermissionsService = TestBed.inject( - UserExplorationPermissionsService); - - spyOn(userExplorationPermissionsService, 'getPermissionsAsync').and - .returnValue(Promise.resolve({ - canVoiceover: true - } as ExplorationPermissions)); - - spyOn(userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve({ - isLoggedIn: () => true - } as UserInfo)); - - spyOn(contextService, 'getExplorationId').and.returnValue(explorationId); - explorationStatesService = TestBed.inject(ExplorationStatesService); - stateEditorService = TestBed.inject(StateEditorService); - translationLanguageService = TestBed.inject(TranslationLanguageService); - translationTabActiveContentIdService = TestBed.inject( - TranslationTabActiveContentIdService); - voiceoverRecordingService = TestBed.inject(VoiceoverRecordingService); - - OppiaAngularRootComponent.ngZone = zone; - - isTranslatableSpy = spyOn(editabilityService, 'isTranslatable'); - isTranslatableSpy.and.returnValue(false); - - spyOn(translationLanguageService, 'getActiveLanguageCode').and - .returnValue('en'); - spyOn(translationTabActiveContentIdService, 'getActiveContentId').and - .returnValue('content'); - spyOn(stateEditorService, 'getActiveStateName').and.returnValue( - stateName); - // This method is being mocked because this spec only handles with - // recordedvoiceovers and not all the exploration. - spyOn(explorationStatesService, 'saveRecordedVoiceovers').and - .callFake(() => {}); - modalSpy = spyOn(ngbModal, 'open'); - modalSpy.and.returnValue({ - componentInstance: { - busyMessage: '' - }, - result: Promise.resolve() - } as NgbModalRef); - - spyOnProperty( - translationTabActiveContentIdService, - 'onActiveContentIdChanged').and.returnValue( - mockActiveContentIdChangedEventEmitter); - - spyOnProperty( - translationLanguageService, 'onActiveLanguageChanged').and.returnValue( - mockActiveLanguageChangedEventEmitter); - - spyOnProperty( - stateEditorService, - 'onShowTranslationTabBusyModal').and.returnValue( - mockShowTranslationTabBusyModalEventEmitter); - - stateRecordedVoiceoversService.init( - stateName, RecordedVoiceovers.createFromBackendDict({ - voiceovers_mapping: { - content: { - en: { - duration_secs: 0, - filename: '', - file_size_bytes: 0, - needs_update: false - } - } - } - })); - - spyOn(zone, 'runOutsideAngular').and.callFake((fn: Function) => fn()); - spyOn(zone, 'run').and.callFake((fn: Function) => fn()); - - - dropAreaMessageDivMock = document.createElement('div'); - dropAreaMessageDivMock.classList.add('oppia-drop-area-message'); - - translationTabDivMock = $(document.createElement('div')); - mainBodyDivMock = $(document.createElement('div')); - - var jQuerySpy = spyOn(window, '$'); - - jQuerySpy - .withArgs('.oppia-translation-tab').and.returnValue( - translationTabDivMock) - .withArgs('.oppia-main-body').and.returnValue(mainBodyDivMock); - jQuerySpy.and.callThrough(); - - - fixture.detectChanges(); - component.showRecorderWarning = true; - component.visualized = { - nativeElement: { - innerHTML: '' - } - } as ElementRef; - - component.ngOnInit(); - }); - - it('should trigger dragover event in translation tab element', - fakeAsync(() => { - component.ngOnInit(); - tick(); - - translationTabDivMock.triggerHandler('dragover'); - tick(); - - component.waveSurferOnFinishCb(); - tick(); - - expect(component.dropAreaIsAccessible).toBe(true); - expect(component.userIsGuest).toBe(false); - })); - - it('should load and play audio', () => { - spyOn(component, 'getAvailableAudio') - .and.returnValue(new Voiceover('filename', 1, false, 12)); - spyOn(audioPlayerService, 'loadAsync').and.returnValue( - Promise.resolve() - ); - spyOn(audioPlayerService, 'play').and.stub(); - - component.loadAndPlayAudioTranslation(); - - expect(audioPlayerService.play).not.toHaveBeenCalled(); - }); - - it('should stop setInterval after timeout', fakeAsync(() => { - spyOn(voiceoverRecordingService, 'startRecordingAsync').and.returnValue( - Promise.resolve(null) - ); - spyOn(component, 'stopRecording').and.callThrough(); - - component.recordingTimeLimit = 2; - component.showPermissionAndStartRecording(); - tick(); - tick(5000); - - expect(component.stopRecording).toHaveBeenCalled(); - flush(); - })); - - it('should play, pause and upload audio', () => { - spyOn(component, 'getAvailableAudio') - .and.returnValue(new Voiceover('filename', 1, false, 12)); - spyOn(audioPlayerService, 'isPlaying').and.returnValue(false); - spyOn(audioPlayerService, 'isTrackLoaded').and.returnValue(false); - spyOn(audioPlayerService, 'play').and.stub(); - - component.playPauseUploadedAudioTranslation('en'); - - expect(audioPlayerService.play).not.toHaveBeenCalled(); - }); - - it('should trigger dragleave event in main body element', fakeAsync(() => { - spyOn(component, 'openAddAudioTranslationModal').and.stub(); - - component.ngOnInit(); - tick(); - - mainBodyDivMock.triggerHandler({ - pageX: 0, - pageY: 0, - preventDefault: () => {}, - type: 'dragleave' - }); - tick(); - - translationTabDivMock.triggerHandler('dragover'); - translationTabDivMock.triggerHandler({ - originalEvent: { - dataTransfer: { - files: [] - } - }, - preventDefault: () => {}, - stopPropagation: () => {}, - target: dropAreaMessageDivMock, - type: 'drop', - }); - tick(); - - expect(component.dropAreaIsAccessible) - .toBe(false); - expect(component.openAddAudioTranslationModal).toHaveBeenCalled(); - })); - it('should evaluate component properties after audio bar initialization', - () => { - expect(component.languageCode).toBe('en'); - expect(component.contentId).toBe('content'); - - component.ngOnDestroy(); - }); - - it('should clear interval', fakeAsync(() => { - let waveSurferObjSpy = { - load: () => {}, - on: (evt, callback) => {}, - empty: () => {}, - pause: () => {}, - play: () => {}, - destroy: () => {} - }; - spyOn(waveSurferObjSpy, 'play'); - // This throws "Argument of type '{ load: () => void; ... }' - // is not assignable to parameter of type 'WaveSurfer'." - // This is because the actual 'WaveSurfer.create` function returns a - // object with around 50 more properties than `waveSurferObjSpy`. - // We need to suppress this error because we have defined the properties - // we need for this test in 'waveSurferObjSpy' object. - // @ts-expect-error - spyOn(WaveSurfer, 'create').and.returnValue(waveSurferObjSpy); - component.waveSurfer = WaveSurfer.create({ - container: '#visualized', - waveColor: '#009688', - progressColor: '#cccccc', - height: 38 - }); - component.showRecorderWarning = true; - spyOn(component, 'checkAndStartRecording').and.stub(); - component.audioBlob = null; - spyOn(voiceoverRecordingService, 'status').and.returnValue({ - isRecording: false, - isAvailable: false - }); - component.timerInterval = setInterval(() => {}, 500); - component.cancelTimer(); - tick(); - flush(); - component.unsavedAudioIsPlaying = true; - component.playAndPauseUnsavedAudio(); - tick(); - - component.toggleStartAndStopRecording(); - tick(); - expect(component.checkAndStartRecording).toHaveBeenCalled(); - - - expect(component.getTranslationTabBusyMessage()).toBe( - 'You haven\'t saved your recording. Please save or ' + - 'cancel the recording.' - ); - })); - - it('should not check and start recording when user deny access', - () => { - component.cannotRecord = true; - spyOn(voiceoverRecordingService, 'status').and.returnValue({ - isAvailable: true, - isRecording: true - }); - spyOn(voiceoverRecordingService, 'startRecordingAsync').and.returnValue( - Promise.reject()); - - component.checkAndStartRecording(); - mockActiveContentIdChangedEventEmitter.emit(); - expect(component.unsupportedBrowser).toBe(false); - expect(component.cannotRecord).toBe(true); - - expect(component.getTranslationTabBusyMessage()).toBe( - 'You haven\'t finished recording. Please stop ' + - 'recording and either save or cancel the recording.' - ); - }); - - it('should not check and start recording when voiceover recorder is' + - ' not available', () => { - spyOn(voiceoverRecordingService, 'status').and.returnValue({ - isAvailable: false, - isRecording: false - }); - spyOn(voiceoverRecordingService, 'startRecordingAsync').and.returnValue( - Promise.resolve(null)); - component.checkAndStartRecording(); - - expect(component.unsupportedBrowser).toBe(true); - expect(component.cannotRecord).toBe(true); - }); - - it('should stop record when language changes', () => { - component.showRecorderWarning = (true); - spyOn(voiceoverRecordingService, 'status').and.returnValue({ - isAvailable: true, - isRecording: true - }); - spyOn(voiceoverRecordingService, 'startRecordingAsync').and.returnValue( - Promise.resolve(null)); - spyOn(voiceoverRecordingService, 'stopRecord'); - spyOn(voiceoverRecordingService, 'closeRecorder'); - - component.checkAndStartRecording(); - mockActiveLanguageChangedEventEmitter.emit(); - - expect(voiceoverRecordingService.stopRecord).toHaveBeenCalled(); - expect(voiceoverRecordingService.closeRecorder).toHaveBeenCalled(); - }); - - it('should open translation busy modal on event', () => { - mockShowTranslationTabBusyModalEventEmitter.emit(); - expect(ngbModal.open).toHaveBeenCalled(); - }); - - it('should stop record when externalSave flag is broadcasted', () => { - spyOn(voiceoverRecordingService, 'status').and.returnValue({ - isAvailable: true, - isRecording: true - }); - spyOn(voiceoverRecordingService, 'startRecordingAsync').and.returnValue( - Promise.resolve(null)); - spyOn(voiceoverRecordingService, 'stopRecord'); - spyOn(voiceoverRecordingService, 'closeRecorder'); - spyOn(audioPlayerService, 'stop'); - spyOn(audioPlayerService, 'clear'); - - component.checkAndStartRecording(); - - mockExternalSaveEventEmitter.emit(); - - expect(voiceoverRecordingService.stopRecord).toHaveBeenCalled(); - expect(voiceoverRecordingService.closeRecorder).toHaveBeenCalled(); - expect(audioPlayerService.stop).toHaveBeenCalled(); - expect(audioPlayerService.clear).toHaveBeenCalled(); - expect(component.audioBlob).toEqual(undefined); - }); - - it('should toggle audio needs update', () => { - spyOn( - stateRecordedVoiceoversService.displayed, 'toggleNeedsUpdateAttribute'); - - component.toggleAudioNeedsUpdate(); - expect( - stateRecordedVoiceoversService.displayed.toggleNeedsUpdateAttribute) - .toHaveBeenCalled(); - expect(component.audioNeedsUpdate).toBe(true); - - component.toggleAudioNeedsUpdate(); - expect( - stateRecordedVoiceoversService.displayed.toggleNeedsUpdateAttribute) - .toHaveBeenCalled(); - expect(component.audioNeedsUpdate).toBe(false); - }); - - it('should play and pause unsaved audio when wave surfer calls on method' + - ' callback', fakeAsync(() => { - component.ngOnInit(); - tick(); - let mockVoiceoverRecorderEventEmitter = new EventEmitter(); - spyOn(component.voiceoverRecorder, 'getMp3Data').and.returnValue( - mockVoiceoverRecorderEventEmitter); - let waveSurferObjSpy = { - load: () => {}, - on: (evt, callback) => {}, - empty: () => {}, - pause: () => {}, - play: () => {}, - destroy: () => {} - }; - spyOn(waveSurferObjSpy, 'play'); - // This throws "Argument of type '{ load: () => void; ... }' - // is not assignable to parameter of type 'WaveSurfer'." - // This is because the actual 'WaveSurfer.create` function returns a - // object with around 50 more properties than `waveSurferObjSpy`. - // We need to suppress this error because we have defined the properties - // we need for this test in 'waveSurferObjSpy' object. - // @ts-expect-error - spyOn(WaveSurfer, 'create').and.returnValue(waveSurferObjSpy); - component.stopRecording(); - component.stopRecording(); - mockVoiceoverRecorderEventEmitter.emit(); - tick(); - flush(); - - component.playAndPauseUnsavedAudio(); - expect(waveSurferObjSpy.play).toHaveBeenCalled(); - })); - - it('should play and pause unsaved audio when wave surfer on method does' + - ' not call the callback', fakeAsync(() => { - let mockVoiceoverRecorderEventEmitter = new EventEmitter(); - spyOn(component.voiceoverRecorder, 'getMp3Data').and.returnValue( - mockVoiceoverRecorderEventEmitter); - let waveSurferObjSpy = { - load: () => {}, - on: () => {}, - empty: () => {}, - pause: () => {}, - play: () => {}, - destroy: () => {} - }; - spyOn(waveSurferObjSpy, 'play'); - spyOn(waveSurferObjSpy, 'pause'); - component.unsavedAudioIsPlaying = false; - // This throws "Argument of type '{ load: () => void; ... }' - // is not assignable to parameter of type 'WaveSurfer'." - // This is because the actual 'WaveSurfer.create` function returns a - // object with around 50 more properties than `waveSurferObjSpy`. - // We need to suppress this error because we have defined the properties - // we need for this test in 'waveSurferObjSpy' object. - // @ts-expect-error - spyOn(WaveSurfer, 'create').and.returnValue(waveSurferObjSpy); - component.stopRecording(); - mockVoiceoverRecorderEventEmitter.emit(); - tick(); - - component.playAndPauseUnsavedAudio(); - tick(); - expect(component.unsavedAudioIsPlaying).toBe(true); - - component.playAndPauseUnsavedAudio(); - tick(); - expect(component.unsavedAudioIsPlaying).toBe(false); - })); - - it('should toggle start and stop recording on keyup event', fakeAsync(() => { - let mockVoiceoverRecorderEventEmitter = new EventEmitter(); - component.canVoiceover = true; - component.isAudioAvailable = false; - spyOn(component, 'cancelTimer').and.stub(); - let waveSurferObjSpy = { - load: () => {}, - on: () => {}, - empty: () => {}, - pause: () => {}, - play: () => {}, - destroy: () => {} - }; - // This throws "Argument of type '{ load: () => void; ... }' - // is not assignable to parameter of type 'WaveSurfer'." - // This is because the actual 'WaveSurfer.create` function returns a - // object with around 50 more properties than `waveSurferObjSpy`. - // We need to suppress this error because we have defined the properties - // we need for this test in 'waveSurferObjSpy' object. - // @ts-expect-error - spyOn(WaveSurfer, 'create').and.returnValue(waveSurferObjSpy); - spyOn(voiceoverRecordingService, 'status').and.returnValue({ - isAvailable: true, - isRecording: true - }); - spyOn(component.voiceoverRecorder, 'getMp3Data').and.returnValue( - mockVoiceoverRecorderEventEmitter); - spyOn(siteAnalyticsService, 'registerStartAudioRecordingEvent'); - - let keyEvent = new KeyboardEvent('keyup', { code: 'KeyR' }); - document.body.dispatchEvent(keyEvent); - tick(); - - expect(siteAnalyticsService.registerStartAudioRecordingEvent) - .not.toHaveBeenCalled(); - - document.body.dispatchEvent(keyEvent); - mockVoiceoverRecorderEventEmitter.emit(); - tick(); - expect(component.recordingComplete).toBe(true); - - // Reset value to not affect other specs. - component.canVoiceover = false; - flush(); - })); - - it('should not toggle start and stop recording on keyup event', () => { - component.canVoiceover = false; - - spyOn(siteAnalyticsService, 'registerStartAudioRecordingEvent'); - - let keyEvent = new KeyboardEvent('keyup', { code: 'KeyR' }); - document.body.dispatchEvent(keyEvent); - - expect(siteAnalyticsService.registerStartAudioRecordingEvent) - .not.toHaveBeenCalled(); - }); - - it('should rerecord successfully', () => { - component.reRecord(); - - - expect(component.selectedRecording).toBe(false); - }); - - it('should cancel recording successfully', () => { - component.cancelRecording(); - - expect(component.selectedRecording).toBe(false); - expect(component.audioIsUpdating).toBe(false); - expect(component.audioBlob).toBe(null); - expect(component.showRecorderWarning).toBe(false); - }); - - it('should save recorded audio successfully', fakeAsync(() => { - spyOn(component, 'saveRecordedVoiceoversChanges').and.stub(); - spyOn(siteAnalyticsService, 'registerSaveRecordedAudioEvent'); - spyOn(alertsService, 'addSuccessMessage'); - spyOn(stateRecordedVoiceoversService.displayed, 'addVoiceover'); - spyOn(assetsBackendApiService, 'saveAudio') - .and.returnValue(Promise.resolve({ - filename: 'filename', - duration_secs: 90 - })); - - component.updateAudio(); - tick(); - - component.saveRecordedAudio(); - tick(); - - expect(siteAnalyticsService.registerSaveRecordedAudioEvent) - .toHaveBeenCalled(); - expect(stateRecordedVoiceoversService.displayed.addVoiceover) - .toHaveBeenCalled(); - expect(component.durationSecs).toBe(90); - expect(component.audioIsCurrentlyBeingSaved).toBe(false); - expect(alertsService.addSuccessMessage).toHaveBeenCalledWith( - 'Succesfuly uploaded recorded audio.'); - })); - - it('should use reject handler when saving recorded audio fails', - () => { - spyOn(siteAnalyticsService, 'registerSaveRecordedAudioEvent'); - spyOn(alertsService, 'addWarning'); - spyOn(assetsBackendApiService, 'saveAudio') - .and.returnValue(Promise.reject({ - error: 'It was not possible to save the recorded audio' - })); - component.saveRecordedAudio(); - - expect(siteAnalyticsService.registerSaveRecordedAudioEvent) - .toHaveBeenCalled(); - - expect(component.audioIsCurrentlyBeingSaved).toBe(true); - expect(alertsService.addWarning).not.toHaveBeenCalledWith( - 'It was not possible to save the recorded audio'); - }); - - it('should open translation tab busy modal with NgbModal', - fakeAsync(() => { - component.openTranslationTabBusyModal(); - tick(); - - expect(ngbModal.open).toHaveBeenCalled(); - })); - - it('should play a loaded audio translation', fakeAsync(() => { - spyOn(audioPlayerService, 'isPlaying').and.returnValue(false); - spyOn(audioPlayerService, 'isTrackLoaded').and.returnValue(true); - spyOn(audioPlayerService, 'play'); - spyOn(audioPlayerService, 'loadAsync').and.returnValue(Promise.resolve()); - expect(component.isPlayingUploadedAudio()).toBe(false); - component.playPauseUploadedAudioTranslation('en'); - tick(); - - expect(component.audioTimerIsShown).toBe(true); - expect(audioPlayerService.play).toHaveBeenCalled(); - })); - - it('should pause ongoing audio translation', () => { - spyOn(audioPlayerService, 'isPlaying').and.returnValue(true); - spyOn(audioPlayerService, 'pause'); - - expect(component.isPlayingUploadedAudio()).toBe(true); - component.playPauseUploadedAudioTranslation('en'); - - expect(audioPlayerService.pause).toHaveBeenCalled(); - }); - - it('should get set progress audio timer', () => { - const setCurrentTimeSpy = spyOn(audioPlayerService, 'setCurrentTime'); - setCurrentTimeSpy.and.returnValue(undefined); - component.setProgress({value: 100}); - expect(setCurrentTimeSpy).toHaveBeenCalledWith(100); - }); - - it('should delete audio when closing delete audio translation modal', - fakeAsync(() => { - spyOn(stateRecordedVoiceoversService.displayed, 'deleteVoiceover'); - - component.openDeleteAudioTranslationModal(); - tick(); - - expect(stateRecordedVoiceoversService.displayed.deleteVoiceover) - .toHaveBeenCalled(); - expect(explorationStatesService.saveRecordedVoiceovers) - .toHaveBeenCalled(); - })); - - it('should not delete audio when dismissing delete audio translation' + - ' modal', fakeAsync(() => { - spyOn(stateRecordedVoiceoversService.displayed, 'deleteVoiceover'); - ngbModal.open = jasmine.createSpy().and.returnValue({ - result: Promise.reject() - } as NgbModalRef); - - component.openDeleteAudioTranslationModal(); - tick(); - - - expect(stateRecordedVoiceoversService.displayed.deleteVoiceover) - .not.toHaveBeenCalled(); - })); - - it('should add audio translation when closing add audio translation modal', - fakeAsync(() => { - spyOn(stateRecordedVoiceoversService.displayed, 'deleteVoiceover'); - spyOn(stateRecordedVoiceoversService.displayed, 'addVoiceover').and - .callFake(() => {}); - modalSpy.and.returnValue({ - componentInstance: { - busyMessage: '' - }, - result: Promise.resolve({ - durationSecs: 100 - }) - } as NgbModalRef); - - component.openAddAudioTranslationModal(null); - tick(); - - expect(stateRecordedVoiceoversService.displayed.deleteVoiceover) - .toHaveBeenCalled(); - expect(stateRecordedVoiceoversService.displayed.addVoiceover) - .toHaveBeenCalled(); - expect(component.durationSecs).toBe(0); - })); - - it('should not add audio translation when dismissing add audio' + - ' translation modal', fakeAsync(() => { - spyOn(alertsService, 'clearWarnings'); - modalSpy.and.returnValue({ - componentInstance: { - audioFile: 'null', - generatedFilename: 'generatedFilename', - languageCode: '', - isAudioAvailable: false, - busyMessage: '' - }, - result: Promise.reject() - } as NgbModalRef); - - component.openAddAudioTranslationModal(null); - tick(); - - expect(alertsService.clearWarnings).toHaveBeenCalled(); - })); - - it('should apply changed when view update emits', fakeAsync(() => { - audioPlayerService.viewUpdate.emit(); - audioPlayerService.onAudioStop.next(); - tick(); - })); + class MockExternalSaveService { + onExternalSave = mockExternalSaveEventEmitter; + } + + class MockNgbModal { + open() { + return { + result: Promise.resolve(), + }; + } + } + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + declarations: [AudioTranslationBarComponent, MockFormatTimePipe], + providers: [ + ContextService, + { + provide: NgbModal, + useClass: MockNgbModal, + }, + { + provide: FormatTimePipe, + useClass: MockFormatTimePipe, + }, + { + provide: ExternalSaveService, + useClass: MockExternalSaveService, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AudioTranslationBarComponent); + component = fixture.componentInstance; + + alertsService = TestBed.inject(AlertsService); + editabilityService = TestBed.inject(EditabilityService); + siteAnalyticsService = TestBed.inject(SiteAnalyticsService); + stateRecordedVoiceoversService = TestBed.inject( + StateRecordedVoiceoversService + ); + zone = TestBed.inject(NgZone); + ngbModal = TestBed.inject(NgbModal); + assetsBackendApiService = TestBed.inject(AssetsBackendApiService); + audioPlayerService = TestBed.inject(AudioPlayerService); + contextService = TestBed.inject(ContextService); + userService = TestBed.inject(UserService); + userExplorationPermissionsService = TestBed.inject( + UserExplorationPermissionsService + ); + + spyOn( + userExplorationPermissionsService, + 'getPermissionsAsync' + ).and.returnValue( + Promise.resolve({ + canVoiceover: true, + } as ExplorationPermissions) + ); + + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve({ + isLoggedIn: () => true, + } as UserInfo) + ); + + spyOn(contextService, 'getExplorationId').and.returnValue(explorationId); + explorationStatesService = TestBed.inject(ExplorationStatesService); + stateEditorService = TestBed.inject(StateEditorService); + translationLanguageService = TestBed.inject(TranslationLanguageService); + translationTabActiveContentIdService = TestBed.inject( + TranslationTabActiveContentIdService + ); + voiceoverRecordingService = TestBed.inject(VoiceoverRecordingService); + + OppiaAngularRootComponent.ngZone = zone; + + isTranslatableSpy = spyOn(editabilityService, 'isTranslatable'); + isTranslatableSpy.and.returnValue(false); + + spyOn(translationLanguageService, 'getActiveLanguageCode').and.returnValue( + 'en' + ); + spyOn( + translationTabActiveContentIdService, + 'getActiveContentId' + ).and.returnValue('content'); + spyOn(stateEditorService, 'getActiveStateName').and.returnValue(stateName); + // This method is being mocked because this spec only handles with + // recordedvoiceovers and not all the exploration. + spyOn(explorationStatesService, 'saveRecordedVoiceovers').and.callFake( + () => {} + ); + modalSpy = spyOn(ngbModal, 'open'); + modalSpy.and.returnValue({ + componentInstance: { + busyMessage: '', + }, + result: Promise.resolve(), + } as NgbModalRef); + + spyOnProperty( + translationTabActiveContentIdService, + 'onActiveContentIdChanged' + ).and.returnValue(mockActiveContentIdChangedEventEmitter); + + spyOnProperty( + translationLanguageService, + 'onActiveLanguageChanged' + ).and.returnValue(mockActiveLanguageChangedEventEmitter); + + spyOnProperty( + stateEditorService, + 'onShowTranslationTabBusyModal' + ).and.returnValue(mockShowTranslationTabBusyModalEventEmitter); + + stateRecordedVoiceoversService.init( + stateName, + RecordedVoiceovers.createFromBackendDict({ + voiceovers_mapping: { + content: { + en: { + duration_secs: 0, + filename: '', + file_size_bytes: 0, + needs_update: false, + }, + }, + }, + }) + ); + + spyOn(zone, 'runOutsideAngular').and.callFake((fn: Function) => fn()); + spyOn(zone, 'run').and.callFake((fn: Function) => fn()); + + dropAreaMessageDivMock = document.createElement('div'); + dropAreaMessageDivMock.classList.add('oppia-drop-area-message'); + + translationTabDivMock = $(document.createElement('div')); + mainBodyDivMock = $(document.createElement('div')); + + var jQuerySpy = spyOn(window, '$'); + + jQuerySpy + .withArgs('.oppia-translation-tab') + .and.returnValue(translationTabDivMock) + .withArgs('.oppia-main-body') + .and.returnValue(mainBodyDivMock); + jQuerySpy.and.callThrough(); + + fixture.detectChanges(); + component.showRecorderWarning = true; + component.visualized = { + nativeElement: { + innerHTML: '', + }, + } as ElementRef; + + component.ngOnInit(); + }); + + it('should trigger dragover event in translation tab element', fakeAsync(() => { + component.ngOnInit(); + tick(); + + translationTabDivMock.triggerHandler('dragover'); + tick(); + + component.waveSurferOnFinishCb(); + tick(); + + expect(component.dropAreaIsAccessible).toBe(true); + expect(component.userIsGuest).toBe(false); + })); + + it('should load and play audio', () => { + spyOn(component, 'getAvailableAudio').and.returnValue( + new Voiceover('filename', 1, false, 12) + ); + spyOn(audioPlayerService, 'loadAsync').and.returnValue(Promise.resolve()); + spyOn(audioPlayerService, 'play').and.stub(); + + component.loadAndPlayAudioTranslation(); + + expect(audioPlayerService.play).not.toHaveBeenCalled(); + }); + + it('should stop setInterval after timeout', fakeAsync(() => { + spyOn(voiceoverRecordingService, 'startRecordingAsync').and.returnValue( + Promise.resolve(null) + ); + spyOn(component, 'stopRecording').and.callThrough(); + + component.recordingTimeLimit = 2; + component.showPermissionAndStartRecording(); + tick(); + tick(5000); + + expect(component.stopRecording).toHaveBeenCalled(); + flush(); + })); + + it('should play, pause and upload audio', () => { + spyOn(component, 'getAvailableAudio').and.returnValue( + new Voiceover('filename', 1, false, 12) + ); + spyOn(audioPlayerService, 'isPlaying').and.returnValue(false); + spyOn(audioPlayerService, 'isTrackLoaded').and.returnValue(false); + spyOn(audioPlayerService, 'play').and.stub(); + + component.playPauseUploadedAudioTranslation('en'); + + expect(audioPlayerService.play).not.toHaveBeenCalled(); + }); + + it('should trigger dragleave event in main body element', fakeAsync(() => { + spyOn(component, 'openAddAudioTranslationModal').and.stub(); + + component.ngOnInit(); + tick(); + + mainBodyDivMock.triggerHandler({ + pageX: 0, + pageY: 0, + preventDefault: () => {}, + type: 'dragleave', + }); + tick(); + + translationTabDivMock.triggerHandler('dragover'); + translationTabDivMock.triggerHandler({ + originalEvent: { + dataTransfer: { + files: [], + }, + }, + preventDefault: () => {}, + stopPropagation: () => {}, + target: dropAreaMessageDivMock, + type: 'drop', + }); + tick(); + + expect(component.dropAreaIsAccessible).toBe(false); + expect(component.openAddAudioTranslationModal).toHaveBeenCalled(); + })); + it('should evaluate component properties after audio bar initialization', () => { + expect(component.languageCode).toBe('en'); + expect(component.contentId).toBe('content'); + + component.ngOnDestroy(); + }); + + it('should clear interval', fakeAsync(() => { + let waveSurferObjSpy = { + load: () => {}, + on: (evt, callback) => {}, + empty: () => {}, + pause: () => {}, + play: () => {}, + destroy: () => {}, + }; + spyOn(waveSurferObjSpy, 'play'); + // This throws "Argument of type '{ load: () => void; ... }' + // is not assignable to parameter of type 'WaveSurfer'." + // This is because the actual 'WaveSurfer.create` function returns a + // object with around 50 more properties than `waveSurferObjSpy`. + // We need to suppress this error because we have defined the properties + // we need for this test in 'waveSurferObjSpy' object. + // @ts-expect-error + spyOn(WaveSurfer, 'create').and.returnValue(waveSurferObjSpy); + component.waveSurfer = WaveSurfer.create({ + container: '#visualized', + waveColor: '#009688', + progressColor: '#cccccc', + height: 38, + }); + component.showRecorderWarning = true; + spyOn(component, 'checkAndStartRecording').and.stub(); + component.audioBlob = null; + spyOn(voiceoverRecordingService, 'status').and.returnValue({ + isRecording: false, + isAvailable: false, + }); + component.timerInterval = setInterval(() => {}, 500); + component.cancelTimer(); + tick(); + flush(); + component.unsavedAudioIsPlaying = true; + component.playAndPauseUnsavedAudio(); + tick(); + + component.toggleStartAndStopRecording(); + tick(); + expect(component.checkAndStartRecording).toHaveBeenCalled(); + + expect(component.getTranslationTabBusyMessage()).toBe( + "You haven't saved your recording. Please save or " + + 'cancel the recording.' + ); + })); + + it('should not check and start recording when user deny access', () => { + component.cannotRecord = true; + spyOn(voiceoverRecordingService, 'status').and.returnValue({ + isAvailable: true, + isRecording: true, + }); + spyOn(voiceoverRecordingService, 'startRecordingAsync').and.returnValue( + Promise.reject() + ); + + component.checkAndStartRecording(); + mockActiveContentIdChangedEventEmitter.emit(); + expect(component.unsupportedBrowser).toBe(false); + expect(component.cannotRecord).toBe(true); + + expect(component.getTranslationTabBusyMessage()).toBe( + "You haven't finished recording. Please stop " + + 'recording and either save or cancel the recording.' + ); + }); + + it( + 'should not check and start recording when voiceover recorder is' + + ' not available', + () => { + spyOn(voiceoverRecordingService, 'status').and.returnValue({ + isAvailable: false, + isRecording: false, + }); + spyOn(voiceoverRecordingService, 'startRecordingAsync').and.returnValue( + Promise.resolve(null) + ); + component.checkAndStartRecording(); + + expect(component.unsupportedBrowser).toBe(true); + expect(component.cannotRecord).toBe(true); + } + ); + + it('should stop record when language changes', () => { + component.showRecorderWarning = true; + spyOn(voiceoverRecordingService, 'status').and.returnValue({ + isAvailable: true, + isRecording: true, + }); + spyOn(voiceoverRecordingService, 'startRecordingAsync').and.returnValue( + Promise.resolve(null) + ); + spyOn(voiceoverRecordingService, 'stopRecord'); + spyOn(voiceoverRecordingService, 'closeRecorder'); + + component.checkAndStartRecording(); + mockActiveLanguageChangedEventEmitter.emit(); + + expect(voiceoverRecordingService.stopRecord).toHaveBeenCalled(); + expect(voiceoverRecordingService.closeRecorder).toHaveBeenCalled(); + }); + + it('should open translation busy modal on event', () => { + mockShowTranslationTabBusyModalEventEmitter.emit(); + expect(ngbModal.open).toHaveBeenCalled(); + }); + + it('should stop record when externalSave flag is broadcasted', () => { + spyOn(voiceoverRecordingService, 'status').and.returnValue({ + isAvailable: true, + isRecording: true, + }); + spyOn(voiceoverRecordingService, 'startRecordingAsync').and.returnValue( + Promise.resolve(null) + ); + spyOn(voiceoverRecordingService, 'stopRecord'); + spyOn(voiceoverRecordingService, 'closeRecorder'); + spyOn(audioPlayerService, 'stop'); + spyOn(audioPlayerService, 'clear'); + + component.checkAndStartRecording(); + + mockExternalSaveEventEmitter.emit(); + + expect(voiceoverRecordingService.stopRecord).toHaveBeenCalled(); + expect(voiceoverRecordingService.closeRecorder).toHaveBeenCalled(); + expect(audioPlayerService.stop).toHaveBeenCalled(); + expect(audioPlayerService.clear).toHaveBeenCalled(); + expect(component.audioBlob).toEqual(undefined); + }); + + it('should toggle audio needs update', () => { + spyOn( + stateRecordedVoiceoversService.displayed, + 'toggleNeedsUpdateAttribute' + ); + + component.toggleAudioNeedsUpdate(); + expect( + stateRecordedVoiceoversService.displayed.toggleNeedsUpdateAttribute + ).toHaveBeenCalled(); + expect(component.audioNeedsUpdate).toBe(true); + + component.toggleAudioNeedsUpdate(); + expect( + stateRecordedVoiceoversService.displayed.toggleNeedsUpdateAttribute + ).toHaveBeenCalled(); + expect(component.audioNeedsUpdate).toBe(false); + }); + + it( + 'should play and pause unsaved audio when wave surfer calls on method' + + ' callback', + fakeAsync(() => { + component.ngOnInit(); + tick(); + let mockVoiceoverRecorderEventEmitter = new EventEmitter(); + spyOn(component.voiceoverRecorder, 'getMp3Data').and.returnValue( + mockVoiceoverRecorderEventEmitter + ); + let waveSurferObjSpy = { + load: () => {}, + on: (evt, callback) => {}, + empty: () => {}, + pause: () => {}, + play: () => {}, + destroy: () => {}, + }; + spyOn(waveSurferObjSpy, 'play'); + // This throws "Argument of type '{ load: () => void; ... }' + // is not assignable to parameter of type 'WaveSurfer'." + // This is because the actual 'WaveSurfer.create` function returns a + // object with around 50 more properties than `waveSurferObjSpy`. + // We need to suppress this error because we have defined the properties + // we need for this test in 'waveSurferObjSpy' object. + // @ts-expect-error + spyOn(WaveSurfer, 'create').and.returnValue(waveSurferObjSpy); + component.stopRecording(); + component.stopRecording(); + mockVoiceoverRecorderEventEmitter.emit(); + tick(); + flush(); + + component.playAndPauseUnsavedAudio(); + expect(waveSurferObjSpy.play).toHaveBeenCalled(); + }) + ); + + it( + 'should play and pause unsaved audio when wave surfer on method does' + + ' not call the callback', + fakeAsync(() => { + let mockVoiceoverRecorderEventEmitter = new EventEmitter(); + spyOn(component.voiceoverRecorder, 'getMp3Data').and.returnValue( + mockVoiceoverRecorderEventEmitter + ); + let waveSurferObjSpy = { + load: () => {}, + on: () => {}, + empty: () => {}, + pause: () => {}, + play: () => {}, + destroy: () => {}, + }; + spyOn(waveSurferObjSpy, 'play'); + spyOn(waveSurferObjSpy, 'pause'); + component.unsavedAudioIsPlaying = false; + // This throws "Argument of type '{ load: () => void; ... }' + // is not assignable to parameter of type 'WaveSurfer'." + // This is because the actual 'WaveSurfer.create` function returns a + // object with around 50 more properties than `waveSurferObjSpy`. + // We need to suppress this error because we have defined the properties + // we need for this test in 'waveSurferObjSpy' object. + // @ts-expect-error + spyOn(WaveSurfer, 'create').and.returnValue(waveSurferObjSpy); + component.stopRecording(); + mockVoiceoverRecorderEventEmitter.emit(); + tick(); + + component.playAndPauseUnsavedAudio(); + tick(); + expect(component.unsavedAudioIsPlaying).toBe(true); + + component.playAndPauseUnsavedAudio(); + tick(); + expect(component.unsavedAudioIsPlaying).toBe(false); + }) + ); + + it('should toggle start and stop recording on keyup event', fakeAsync(() => { + let mockVoiceoverRecorderEventEmitter = new EventEmitter(); + component.canVoiceover = true; + component.isAudioAvailable = false; + spyOn(component, 'cancelTimer').and.stub(); + let waveSurferObjSpy = { + load: () => {}, + on: () => {}, + empty: () => {}, + pause: () => {}, + play: () => {}, + destroy: () => {}, + }; + // This throws "Argument of type '{ load: () => void; ... }' + // is not assignable to parameter of type 'WaveSurfer'." + // This is because the actual 'WaveSurfer.create` function returns a + // object with around 50 more properties than `waveSurferObjSpy`. + // We need to suppress this error because we have defined the properties + // we need for this test in 'waveSurferObjSpy' object. + // @ts-expect-error + spyOn(WaveSurfer, 'create').and.returnValue(waveSurferObjSpy); + spyOn(voiceoverRecordingService, 'status').and.returnValue({ + isAvailable: true, + isRecording: true, + }); + spyOn(component.voiceoverRecorder, 'getMp3Data').and.returnValue( + mockVoiceoverRecorderEventEmitter + ); + spyOn(siteAnalyticsService, 'registerStartAudioRecordingEvent'); + + let keyEvent = new KeyboardEvent('keyup', {code: 'KeyR'}); + document.body.dispatchEvent(keyEvent); + tick(); + + expect( + siteAnalyticsService.registerStartAudioRecordingEvent + ).not.toHaveBeenCalled(); + + document.body.dispatchEvent(keyEvent); + mockVoiceoverRecorderEventEmitter.emit(); + tick(); + expect(component.recordingComplete).toBe(true); + + // Reset value to not affect other specs. + component.canVoiceover = false; + flush(); + })); + + it('should not toggle start and stop recording on keyup event', () => { + component.canVoiceover = false; + + spyOn(siteAnalyticsService, 'registerStartAudioRecordingEvent'); + + let keyEvent = new KeyboardEvent('keyup', {code: 'KeyR'}); + document.body.dispatchEvent(keyEvent); + + expect( + siteAnalyticsService.registerStartAudioRecordingEvent + ).not.toHaveBeenCalled(); + }); + + it('should rerecord successfully', () => { + component.reRecord(); + + expect(component.selectedRecording).toBe(false); + }); + + it('should cancel recording successfully', () => { + component.cancelRecording(); + + expect(component.selectedRecording).toBe(false); + expect(component.audioIsUpdating).toBe(false); + expect(component.audioBlob).toBe(null); + expect(component.showRecorderWarning).toBe(false); + }); + + it('should save recorded audio successfully', fakeAsync(() => { + spyOn(component, 'saveRecordedVoiceoversChanges').and.stub(); + spyOn(siteAnalyticsService, 'registerSaveRecordedAudioEvent'); + spyOn(alertsService, 'addSuccessMessage'); + spyOn(stateRecordedVoiceoversService.displayed, 'addVoiceover'); + spyOn(assetsBackendApiService, 'saveAudio').and.returnValue( + Promise.resolve({ + filename: 'filename', + duration_secs: 90, + }) + ); + + component.updateAudio(); + tick(); + + component.saveRecordedAudio(); + tick(); + + expect( + siteAnalyticsService.registerSaveRecordedAudioEvent + ).toHaveBeenCalled(); + expect( + stateRecordedVoiceoversService.displayed.addVoiceover + ).toHaveBeenCalled(); + expect(component.durationSecs).toBe(90); + expect(component.audioIsCurrentlyBeingSaved).toBe(false); + expect(alertsService.addSuccessMessage).toHaveBeenCalledWith( + 'Succesfuly uploaded recorded audio.' + ); + })); + + it('should use reject handler when saving recorded audio fails', () => { + spyOn(siteAnalyticsService, 'registerSaveRecordedAudioEvent'); + spyOn(alertsService, 'addWarning'); + spyOn(assetsBackendApiService, 'saveAudio').and.returnValue( + Promise.reject({ + error: 'It was not possible to save the recorded audio', + }) + ); + component.saveRecordedAudio(); + + expect( + siteAnalyticsService.registerSaveRecordedAudioEvent + ).toHaveBeenCalled(); + + expect(component.audioIsCurrentlyBeingSaved).toBe(true); + expect(alertsService.addWarning).not.toHaveBeenCalledWith( + 'It was not possible to save the recorded audio' + ); + }); + + it('should open translation tab busy modal with NgbModal', fakeAsync(() => { + component.openTranslationTabBusyModal(); + tick(); + + expect(ngbModal.open).toHaveBeenCalled(); + })); + + it('should play a loaded audio translation', fakeAsync(() => { + spyOn(audioPlayerService, 'isPlaying').and.returnValue(false); + spyOn(audioPlayerService, 'isTrackLoaded').and.returnValue(true); + spyOn(audioPlayerService, 'play'); + spyOn(audioPlayerService, 'loadAsync').and.returnValue(Promise.resolve()); + expect(component.isPlayingUploadedAudio()).toBe(false); + component.playPauseUploadedAudioTranslation('en'); + tick(); + + expect(component.audioTimerIsShown).toBe(true); + expect(audioPlayerService.play).toHaveBeenCalled(); + })); + + it('should pause ongoing audio translation', () => { + spyOn(audioPlayerService, 'isPlaying').and.returnValue(true); + spyOn(audioPlayerService, 'pause'); + + expect(component.isPlayingUploadedAudio()).toBe(true); + component.playPauseUploadedAudioTranslation('en'); + + expect(audioPlayerService.pause).toHaveBeenCalled(); + }); + + it('should get set progress audio timer', () => { + const setCurrentTimeSpy = spyOn(audioPlayerService, 'setCurrentTime'); + setCurrentTimeSpy.and.returnValue(undefined); + component.setProgress({value: 100}); + expect(setCurrentTimeSpy).toHaveBeenCalledWith(100); + }); + + it('should delete audio when closing delete audio translation modal', fakeAsync(() => { + spyOn(stateRecordedVoiceoversService.displayed, 'deleteVoiceover'); + + component.openDeleteAudioTranslationModal(); + tick(); + + expect( + stateRecordedVoiceoversService.displayed.deleteVoiceover + ).toHaveBeenCalled(); + expect(explorationStatesService.saveRecordedVoiceovers).toHaveBeenCalled(); + })); + + it( + 'should not delete audio when dismissing delete audio translation' + + ' modal', + fakeAsync(() => { + spyOn(stateRecordedVoiceoversService.displayed, 'deleteVoiceover'); + ngbModal.open = jasmine.createSpy().and.returnValue({ + result: Promise.reject(), + } as NgbModalRef); + + component.openDeleteAudioTranslationModal(); + tick(); + + expect( + stateRecordedVoiceoversService.displayed.deleteVoiceover + ).not.toHaveBeenCalled(); + }) + ); + + it('should add audio translation when closing add audio translation modal', fakeAsync(() => { + spyOn(stateRecordedVoiceoversService.displayed, 'deleteVoiceover'); + spyOn( + stateRecordedVoiceoversService.displayed, + 'addVoiceover' + ).and.callFake(() => {}); + modalSpy.and.returnValue({ + componentInstance: { + busyMessage: '', + }, + result: Promise.resolve({ + durationSecs: 100, + }), + } as NgbModalRef); + + component.openAddAudioTranslationModal(null); + tick(); + + expect( + stateRecordedVoiceoversService.displayed.deleteVoiceover + ).toHaveBeenCalled(); + expect( + stateRecordedVoiceoversService.displayed.addVoiceover + ).toHaveBeenCalled(); + expect(component.durationSecs).toBe(0); + })); + + it( + 'should not add audio translation when dismissing add audio' + + ' translation modal', + fakeAsync(() => { + spyOn(alertsService, 'clearWarnings'); + modalSpy.and.returnValue({ + componentInstance: { + audioFile: 'null', + generatedFilename: 'generatedFilename', + languageCode: '', + isAudioAvailable: false, + busyMessage: '', + }, + result: Promise.reject(), + } as NgbModalRef); + + component.openAddAudioTranslationModal(null); + tick(); + + expect(alertsService.clearWarnings).toHaveBeenCalled(); + }) + ); + + it('should apply changed when view update emits', fakeAsync(() => { + audioPlayerService.viewUpdate.emit(); + audioPlayerService.onAudioStop.next(); + tick(); + })); }); diff --git a/core/templates/pages/exploration-editor-page/translation-tab/audio-translation-bar/audio-translation-bar.component.ts b/core/templates/pages/exploration-editor-page/translation-tab/audio-translation-bar/audio-translation-bar.component.ts index 7d4389118e87..2375f6865e2e 100644 --- a/core/templates/pages/exploration-editor-page/translation-tab/audio-translation-bar/audio-translation-bar.component.ts +++ b/core/templates/pages/exploration-editor-page/translation-tab/audio-translation-bar/audio-translation-bar.component.ts @@ -16,39 +16,46 @@ * @fileoverview Component for the audio translation bar. */ -import { Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { downgradeComponent } from '@angular/upgrade/static'; +import { + Component, + ElementRef, + Input, + OnDestroy, + OnInit, + ViewChild, +} from '@angular/core'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {downgradeComponent} from '@angular/upgrade/static'; import WaveSurfer from 'wavesurfer.js'; -import { Subscription } from 'rxjs'; -import { OppiaAngularRootComponent } from 'components/oppia-angular-root.component'; -import { DeleteAudioTranslationModalComponent } from 'pages/exploration-editor-page/translation-tab/modal-templates/delete-audio-translation-modal.component'; -import { TranslationTabBusyModalComponent } from 'pages/exploration-editor-page/translation-tab/modal-templates/translation-tab-busy-modal.component'; -import { AddAudioTranslationModalComponent } from '../modal-templates/add-audio-translation-modal.component'; -import { UserExplorationPermissionsService } from 'pages/exploration-editor-page/services/user-exploration-permissions.service'; -import { UserService } from 'services/user.service'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { StateRecordedVoiceoversService } from 'components/state-editor/state-editor-properties-services/state-recorded-voiceovers.service'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; -import { GraphDataService } from 'pages/exploration-editor-page/services/graph-data.service'; -import { AlertsService } from 'services/alerts.service'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; -import { AudioPlayerService } from 'services/audio-player.service'; -import { ContextService } from 'services/context.service'; -import { EditabilityService } from 'services/editability.service'; -import { ExternalSaveService } from 'services/external-save.service'; -import { IdGenerationService } from 'services/id-generation.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { TranslationLanguageService } from '../services/translation-language.service'; -import { TranslationStatusService } from '../services/translation-status.service'; -import { TranslationTabActiveContentIdService } from '../services/translation-tab-active-content-id.service'; -import { Voiceover } from 'domain/exploration/voiceover.model'; -import { ExplorationEditorPageConstants } from 'pages/exploration-editor-page/exploration-editor-page.constants'; -import { VoiceoverRecordingService } from '../services/voiceover-recording.service'; +import {Subscription} from 'rxjs'; +import {OppiaAngularRootComponent} from 'components/oppia-angular-root.component'; +import {DeleteAudioTranslationModalComponent} from 'pages/exploration-editor-page/translation-tab/modal-templates/delete-audio-translation-modal.component'; +import {TranslationTabBusyModalComponent} from 'pages/exploration-editor-page/translation-tab/modal-templates/translation-tab-busy-modal.component'; +import {AddAudioTranslationModalComponent} from '../modal-templates/add-audio-translation-modal.component'; +import {UserExplorationPermissionsService} from 'pages/exploration-editor-page/services/user-exploration-permissions.service'; +import {UserService} from 'services/user.service'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {StateRecordedVoiceoversService} from 'components/state-editor/state-editor-properties-services/state-recorded-voiceovers.service'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; +import {GraphDataService} from 'pages/exploration-editor-page/services/graph-data.service'; +import {AlertsService} from 'services/alerts.service'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; +import {AudioPlayerService} from 'services/audio-player.service'; +import {ContextService} from 'services/context.service'; +import {EditabilityService} from 'services/editability.service'; +import {ExternalSaveService} from 'services/external-save.service'; +import {IdGenerationService} from 'services/id-generation.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {TranslationLanguageService} from '../services/translation-language.service'; +import {TranslationStatusService} from '../services/translation-status.service'; +import {TranslationTabActiveContentIdService} from '../services/translation-tab-active-content-id.service'; +import {Voiceover} from 'domain/exploration/voiceover.model'; +import {ExplorationEditorPageConstants} from 'pages/exploration-editor-page/exploration-editor-page.constants'; +import {VoiceoverRecordingService} from '../services/voiceover-recording.service'; @Component({ selector: 'oppia-audio-translation-bar', - templateUrl: './audio-translation-bar.component.html' + templateUrl: './audio-translation-bar.component.html', }) export class AudioTranslationBarComponent implements OnInit, OnDestroy { @Input() isTranslationTabBusy: boolean; @@ -101,13 +108,11 @@ export class AudioTranslationBarComponent implements OnInit, OnDestroy { private stateRecordedVoiceoversService: StateRecordedVoiceoversService, private translationLanguageService: TranslationLanguageService, private translationStatusService: TranslationStatusService, - private translationTabActiveContentIdService: - TranslationTabActiveContentIdService, - private userExplorationPermissionsService: - UserExplorationPermissionsService, + private translationTabActiveContentIdService: TranslationTabActiveContentIdService, + private userExplorationPermissionsService: UserExplorationPermissionsService, private userService: UserService, - public voiceoverRecorder: VoiceoverRecordingService, - ) { } + public voiceoverRecorder: VoiceoverRecordingService + ) {} setProgress(val: {value: number}): void { this.audioPlayerService.setCurrentTime(val.value); @@ -123,9 +128,10 @@ export class AudioTranslationBarComponent implements OnInit, OnDestroy { getAvailableAudio(contentId: string, languageCode: string): Voiceover { if (this.contentId) { - return ( - this.stateRecordedVoiceoversService.displayed.getVoiceover( - contentId, languageCode)); + return this.stateRecordedVoiceoversService.displayed.getVoiceover( + contentId, + languageCode + ); } } @@ -137,46 +143,53 @@ export class AudioTranslationBarComponent implements OnInit, OnDestroy { generateNewFilename(): string { return ( - this.contentId + '-' + - this.languageCode + '-' + - this.idGenerationService.generateNewId() + '.mp3'); + this.contentId + + '-' + + this.languageCode + + '-' + + this.idGenerationService.generateNewId() + + '.mp3' + ); } showPermissionAndStartRecording(): void { this.checkingMicrophonePermission = true; - this.voiceoverRecorder.startRecordingAsync().then(() => { - // When the user accepts the microphone access. - this.showRecorderWarning = true; - this.isTranslationTabBusy = true; - - this.recordingPermissionDenied = false; - this.cannotRecord = false; - this.selectedRecording = true; - this.checkingMicrophonePermission = false; - - this.recordingDate = 0; - this.elapsedTime = 0; - OppiaAngularRootComponent.ngZone.runOutsideAngular(() => { - this.timerInterval = setInterval(() => { - OppiaAngularRootComponent.ngZone.run(() => { - this.elapsedTime++; - this.recordingDate = this.elapsedTime; - - // This.recordingTimeLimit is decremented to - // compensate for the audio recording timing inconsistency, - // so it allows the server to accept the recording. - if (this.elapsedTime === this.recordingTimeLimit - 1) { - this.stopRecording(); - } - }); - }, 1000); - }); - }, () => { - // When the user denies microphone access. - this.recordingPermissionDenied = true; - this.cannotRecord = true; - this.checkingMicrophonePermission = false; - }); + this.voiceoverRecorder.startRecordingAsync().then( + () => { + // When the user accepts the microphone access. + this.showRecorderWarning = true; + this.isTranslationTabBusy = true; + + this.recordingPermissionDenied = false; + this.cannotRecord = false; + this.selectedRecording = true; + this.checkingMicrophonePermission = false; + + this.recordingDate = 0; + this.elapsedTime = 0; + OppiaAngularRootComponent.ngZone.runOutsideAngular(() => { + this.timerInterval = setInterval(() => { + OppiaAngularRootComponent.ngZone.run(() => { + this.elapsedTime++; + this.recordingDate = this.elapsedTime; + + // This.recordingTimeLimit is decremented to + // compensate for the audio recording timing inconsistency, + // so it allows the server to accept the recording. + if (this.elapsedTime === this.recordingTimeLimit - 1) { + this.stopRecording(); + } + }); + }, 1000); + }); + }, + () => { + // When the user denies microphone access. + this.recordingPermissionDenied = true; + this.cannotRecord = true; + this.checkingMicrophonePermission = false; + } + ); } checkAndStartRecording(): void { @@ -194,7 +207,9 @@ export class AudioTranslationBarComponent implements OnInit, OnDestroy { toggleAudioNeedsUpdate(): void { this.stateRecordedVoiceoversService.displayed.toggleNeedsUpdateAttribute( - this.contentId, this.languageCode); + this.contentId, + this.languageCode + ); this.saveRecordedVoiceoversChanges(); this.audioNeedsUpdate = !this.audioNeedsUpdate; } @@ -236,12 +251,11 @@ export class AudioTranslationBarComponent implements OnInit, OnDestroy { container: '#visualized', waveColor: '#009688', progressColor: '#cccccc', - height: 38 + height: 38, }); this.waveSurfer.empty(); this.waveSurfer.load(url); - } - ); + }); } waveSurferOnFinishCb(): void { @@ -260,8 +274,7 @@ export class AudioTranslationBarComponent implements OnInit, OnDestroy { } toggleStartAndStopRecording(): void { - if (!this.voiceoverRecorder.status().isRecording && - !this.audioBlob) { + if (!this.voiceoverRecorder.status().isRecording && !this.audioBlob) { this.checkAndStartRecording(); } else { this.stopRecording(); @@ -285,47 +298,63 @@ export class AudioTranslationBarComponent implements OnInit, OnDestroy { let fileType = 'audio/mp3'; let contentId = this.contentId; let languageCode = this.languageCode; - let recordedAudioFile = new File( - [this.audioBlob as BlobPart], filename, {type: fileType}); + let recordedAudioFile = new File([this.audioBlob as BlobPart], filename, { + type: fileType, + }); this.showRecorderWarning = false; Promise.resolve( this.assetsBackendApiService.saveAudio( - this.contextService.getExplorationId(), filename, recordedAudioFile) - ).then((response) => { - if (this.audioIsUpdating) { - this.stateRecordedVoiceoversService.displayed.deleteVoiceover( - contentId, languageCode); - this.audioIsUpdating = false; + this.contextService.getExplorationId(), + filename, + recordedAudioFile + ) + ).then( + response => { + if (this.audioIsUpdating) { + this.stateRecordedVoiceoversService.displayed.deleteVoiceover( + contentId, + languageCode + ); + this.audioIsUpdating = false; + } + this.stateRecordedVoiceoversService.displayed.addVoiceover( + contentId, + languageCode, + filename, + recordedAudioFile.size, + response.duration_secs + ); + this.durationSecs = Math.round(response.duration_secs); + this.saveRecordedVoiceoversChanges(); + this.alertsService.addSuccessMessage( + 'Succesfuly uploaded recorded audio.' + ); + this.audioIsCurrentlyBeingSaved = false; + this.initAudioBar(); + + setTimeout(() => { + this.graphDataService.recompute(); + }); + }, + errorResponse => { + this.audioIsCurrentlyBeingSaved = false; + this.alertsService.addWarning(errorResponse.error); + this.initAudioBar(); } - this.stateRecordedVoiceoversService.displayed.addVoiceover( - contentId, languageCode, filename, recordedAudioFile.size, - response.duration_secs); - this.durationSecs = Math.round(response.duration_secs); - this.saveRecordedVoiceoversChanges(); - this.alertsService.addSuccessMessage( - 'Succesfuly uploaded recorded audio.'); - this.audioIsCurrentlyBeingSaved = false; - this.initAudioBar(); - - setTimeout(() => { - this.graphDataService.recompute(); - }); - }, (errorResponse) => { - this.audioIsCurrentlyBeingSaved = false; - this.alertsService.addWarning(errorResponse.error); - this.initAudioBar(); - }); + ); } getTranslationTabBusyMessage(): string { let message = ''; if (this.voiceoverRecorder.status().isRecording) { - message = 'You haven\'t finished recording. Please stop ' + - 'recording and either save or cancel the recording.'; + message = + "You haven't finished recording. Please stop " + + 'recording and either save or cancel the recording.'; } else if (this.showRecorderWarning) { - message = 'You haven\'t saved your recording. Please save or ' + - 'cancel the recording.'; + message = + "You haven't saved your recording. Please save or " + + 'cancel the recording.'; } return message; } @@ -336,13 +365,16 @@ export class AudioTranslationBarComponent implements OnInit, OnDestroy { }); modalRef.componentInstance.busyMessage = - this.getTranslationTabBusyMessage(); - - modalRef.result.then(() => {}, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + this.getTranslationTabBusyMessage(); + + modalRef.result.then( + () => {}, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } playPauseUploadedAudioTranslation(languageCode: string): void { @@ -367,16 +399,17 @@ export class AudioTranslationBarComponent implements OnInit, OnDestroy { this.audioLoadingIndicatorIsShown = true; let audioTranslation = this.getAvailableAudio( - this.contentId, this.languageCode); + this.contentId, + this.languageCode + ); if (audioTranslation) { - this.audioPlayerService.loadAsync(audioTranslation.filename) - .then(() => { - this.audioLoadingIndicatorIsShown = false; - this.audioIsLoading = false; - this.audioTimerIsShown = true; - this.audioPlayerService.play(); - }); + this.audioPlayerService.loadAsync(audioTranslation.filename).then(() => { + this.audioLoadingIndicatorIsShown = false; + this.audioIsLoading = false; + this.audioTimerIsShown = true; + this.audioPlayerService.play(); + }); } } @@ -384,8 +417,10 @@ export class AudioTranslationBarComponent implements OnInit, OnDestroy { // This stops the voiceoverRecorder when user navigates // while recording. if (this.voiceoverRecorder) { - if (this.voiceoverRecorder.status().isRecording && - this.showRecorderWarning) { + if ( + this.voiceoverRecorder.status().isRecording && + this.showRecorderWarning + ) { this.voiceoverRecorder.stopRecord(); this.cancelTimer(); this.voiceoverRecorder.closeRecorder(); @@ -398,20 +433,20 @@ export class AudioTranslationBarComponent implements OnInit, OnDestroy { // Re-initialize for unsaved recording. this.unsavedAudioIsPlaying = false; this.waveSurfer = null; - this.languageCode = this.translationLanguageService - .getActiveLanguageCode(); + this.languageCode = this.translationLanguageService.getActiveLanguageCode(); this.canVoiceover = this.editabilityService.isTranslatable(); - this.contentId = ( - this.translationTabActiveContentIdService.getActiveContentId()); + this.contentId = + this.translationTabActiveContentIdService.getActiveContentId(); let audioTranslationObject = this.getAvailableAudio( - this.contentId, this.languageCode); + this.contentId, + this.languageCode + ); if (audioTranslationObject) { this.isAudioAvailable = true; this.audioIsLoading = true; this.selectedRecording = false; this.audioNeedsUpdate = audioTranslationObject.needsUpdate; - this.durationSecs = - Math.round(audioTranslationObject.durationSecs); + this.durationSecs = Math.round(audioTranslationObject.durationSecs); } else { this.isAudioAvailable = false; this.audioBlob = null; @@ -420,18 +455,25 @@ export class AudioTranslationBarComponent implements OnInit, OnDestroy { } openDeleteAudioTranslationModal(): void { - this.ngbModal.open(DeleteAudioTranslationModalComponent, { - backdrop: true - }).result.then(() => { - this.stateRecordedVoiceoversService.displayed.deleteVoiceover( - this.contentId, this.languageCode); - this.saveRecordedVoiceoversChanges(); - this.initAudioBar(); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + this.ngbModal + .open(DeleteAudioTranslationModalComponent, { + backdrop: true, + }) + .result.then( + () => { + this.stateRecordedVoiceoversService.displayed.deleteVoiceover( + this.contentId, + this.languageCode + ); + this.saveRecordedVoiceoversChanges(); + this.initAudioBar(); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } openAddAudioTranslationModal(audioFile: FileList): void { @@ -441,31 +483,38 @@ export class AudioTranslationBarComponent implements OnInit, OnDestroy { }); modalRef.componentInstance.audioFile = audioFile; - modalRef.componentInstance.generatedFilename = ( - this.generateNewFilename()); + modalRef.componentInstance.generatedFilename = this.generateNewFilename(); modalRef.componentInstance.languageCode = this.languageCode; - modalRef.componentInstance.isAudioAvailable = ( - this.isAudioAvailable); - - modalRef.result.then((result) => { - if (this.isAudioAvailable) { - this.stateRecordedVoiceoversService.displayed.deleteVoiceover( - this.contentId, this.languageCode); + modalRef.componentInstance.isAudioAvailable = this.isAudioAvailable; + + modalRef.result.then( + result => { + if (this.isAudioAvailable) { + this.stateRecordedVoiceoversService.displayed.deleteVoiceover( + this.contentId, + this.languageCode + ); + } + this.stateRecordedVoiceoversService.displayed.addVoiceover( + this.contentId, + this.languageCode, + result.filename, + result.fileSizeBytes, + result.durationSecs + ); + this.durationSecs = Math.round(result.durationSecs); + this.saveRecordedVoiceoversChanges(); + this.initAudioBar(); + }, + () => { + this.alertsService.clearWarnings(); } - this.stateRecordedVoiceoversService.displayed.addVoiceover( - this.contentId, this.languageCode, result.filename, - result.fileSizeBytes, result.durationSecs); - this.durationSecs = Math.round(result.durationSecs); - this.saveRecordedVoiceoversChanges(); - this.initAudioBar(); - }, () => { - this.alertsService.clearWarnings(); - }); + ); } ngOnInit(): void { - this.recordingTimeLimit = ( - ExplorationEditorPageConstants.RECORDING_TIME_LIMIT); + this.recordingTimeLimit = + ExplorationEditorPageConstants.RECORDING_TIME_LIMIT; this.startingDuration = 0; this.unsupportedBrowser = false; this.selectedRecording = false; @@ -482,7 +531,7 @@ export class AudioTranslationBarComponent implements OnInit, OnDestroy { this.elapsedTime = 0; this.unsavedAudioIsPlaying = false; - document.body.onkeyup = (e) => { + document.body.onkeyup = e => { if (!this.canVoiceover) { return; } @@ -504,47 +553,47 @@ export class AudioTranslationBarComponent implements OnInit, OnDestroy { ); this.directiveSubscriptions.add( - this.translationTabActiveContentIdService.onActiveContentIdChanged. - subscribe( - () => this.initAudioBar() - ) + this.translationTabActiveContentIdService.onActiveContentIdChanged.subscribe( + () => this.initAudioBar() + ) ); this.directiveSubscriptions.add( - this.translationLanguageService.onActiveLanguageChanged.subscribe( - () => this.initAudioBar() + this.translationLanguageService.onActiveLanguageChanged.subscribe(() => + this.initAudioBar() ) ); this.directiveSubscriptions.add( - this.stateEditorService.onShowTranslationTabBusyModal.subscribe( - () => this.openTranslationTabBusyModal() + this.stateEditorService.onShowTranslationTabBusyModal.subscribe(() => + this.openTranslationTabBusyModal() ) ); this.directiveSubscriptions.add( - this.audioPlayerService.viewUpdate.subscribe(() => { - }) + this.audioPlayerService.viewUpdate.subscribe(() => {}) ); this.directiveSubscriptions.add( - this.audioPlayerService.onAudioStop.subscribe(() => { - }) + this.audioPlayerService.onAudioStop.subscribe(() => {}) ); let userIsLoggedIn; - this.userService.getUserInfoAsync().then((userInfo) => { - userIsLoggedIn = userInfo.isLoggedIn(); - return this.userExplorationPermissionsService.getPermissionsAsync(); - }).then((permissions) => { - $('.oppia-translation-tab').on('dragover', (evt) => { - evt.preventDefault(); - this.dropAreaIsAccessible = permissions.canVoiceover; - this.userIsGuest = !userIsLoggedIn; - return false; + this.userService + .getUserInfoAsync() + .then(userInfo => { + userIsLoggedIn = userInfo.isLoggedIn(); + return this.userExplorationPermissionsService.getPermissionsAsync(); + }) + .then(permissions => { + $('.oppia-translation-tab').on('dragover', evt => { + evt.preventDefault(); + this.dropAreaIsAccessible = permissions.canVoiceover; + this.userIsGuest = !userIsLoggedIn; + return false; + }); }); - }); - $('.oppia-main-body').on('dragleave', (evt) => { + $('.oppia-main-body').on('dragleave', evt => { evt.preventDefault(); if (evt.pageX === 0 || evt.pageY === 0) { this.dropAreaIsAccessible = false; @@ -553,7 +602,7 @@ export class AudioTranslationBarComponent implements OnInit, OnDestroy { return false; }); - $('.oppia-translation-tab').on('drop', (evt) => { + $('.oppia-translation-tab').on('drop', evt => { evt.preventDefault(); if ( // TODO(#13015): Remove use of unknown as a type. @@ -561,7 +610,8 @@ export class AudioTranslationBarComponent implements OnInit, OnDestroy { // So probably #12882 also. (evt.target as unknown as Element).classList.contains( 'oppia-drop-area-message' - ) && this.dropAreaIsAccessible + ) && + this.dropAreaIsAccessible ) { let files = (evt.originalEvent as DragEvent).dataTransfer.files; this.openAddAudioTranslationModal(files); @@ -594,7 +644,9 @@ export class AudioTranslationBarComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive('oppiaAudioTranslationBar', - downgradeComponent({ - component: AudioTranslationBarComponent - }) as angular.IDirectiveFactory); +angular.module('oppia').directive( + 'oppiaAudioTranslationBar', + downgradeComponent({ + component: AudioTranslationBarComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-editor-page/translation-tab/modal-templates/add-audio-translation-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/translation-tab/modal-templates/add-audio-translation-modal.component.spec.ts index 67f07f451ebf..7edf4ca17c77 100644 --- a/core/templates/pages/exploration-editor-page/translation-tab/modal-templates/add-audio-translation-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/translation-tab/modal-templates/add-audio-translation-modal.component.spec.ts @@ -16,13 +16,19 @@ * @fileoverview Unit tests for AddAudioTranslationModalComponent. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed, fakeAsync, tick } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; -import { ContextService } from 'services/context.service'; -import { AddAudioTranslationModalComponent } from './add-audio-translation-modal.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + waitForAsync, + TestBed, + fakeAsync, + tick, +} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; +import {ContextService} from 'services/context.service'; +import {AddAudioTranslationModalComponent} from './add-audio-translation-modal.component'; class MockActiveModal { close(): void { @@ -49,16 +55,14 @@ describe('Add Audio Translation Modal component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - AddAudioTranslationModalComponent - ], + declarations: [AddAudioTranslationModalComponent], providers: [ { provide: NgbActiveModal, - useClass: MockActiveModal + useClass: MockActiveModal, }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -81,17 +85,16 @@ describe('Add Audio Translation Modal component', () => { fixture.detectChanges(); }); - it('should initialize component properties after component is initialized', - () => { - expect(component.saveButtonText).toBe('Save'); - expect(component.saveInProgress).toBe(false); - expect(component.isAudioAvailable).toBe(isAudioAvailable); - expect(component.droppedFile).toBe(audioFile); - }); + it('should initialize component properties after component is initialized', () => { + expect(component.saveButtonText).toBe('Save'); + expect(component.saveInProgress).toBe(false); + expect(component.isAudioAvailable).toBe(isAudioAvailable); + expect(component.droppedFile).toBe(audioFile); + }); it('should save audio successfully then close the modal', fakeAsync(() => { let file = { - size: 1000 + size: 1000, }; component.updateUploadedFile(file as Blob); @@ -99,11 +102,12 @@ describe('Add Audio Translation Modal component', () => { let response = { filename: 'filename', - duration_secs: 10 + duration_secs: 10, }; spyOn(assetsBackendApiService, 'saveAudio').and.returnValue( - Promise.resolve(response)); + Promise.resolve(response) + ); component.confirm(); tick(); @@ -114,35 +118,36 @@ describe('Add Audio Translation Modal component', () => { languageCode: languageCode, filename: generatedFilename, fileSizeBytes: file.size, - durationSecs: response.duration_secs + durationSecs: response.duration_secs, }); })); - it('should use reject handler when trying to save audio fails', - fakeAsync(() => { - let file = { - size: 1000 - }; - component.updateUploadedFile(file as Blob); + it('should use reject handler when trying to save audio fails', fakeAsync(() => { + let file = { + size: 1000, + }; + component.updateUploadedFile(file as Blob); - spyOn(contextService, 'getExplorationId').and.returnValue('exp1'); - spyOn(assetsBackendApiService, 'saveAudio') - .and.returnValue(Promise.reject({})); + spyOn(contextService, 'getExplorationId').and.returnValue('exp1'); + spyOn(assetsBackendApiService, 'saveAudio').and.returnValue( + Promise.reject({}) + ); - component.confirm(); + component.confirm(); - expect(component.saveButtonText).toBe('Saving...'); - expect(component.saveInProgress).toBe(true); + expect(component.saveButtonText).toBe('Saving...'); + expect(component.saveInProgress).toBe(true); - tick(); + tick(); - expect(component.errorMessage).toBe( - 'There was an error uploading the audio file.'); - expect(component.saveButtonText).toBe('Save'); - expect(component.saveInProgress).toBe(false); - expect(ngbActiveModal.close).not.toHaveBeenCalled(); + expect(component.errorMessage).toBe( + 'There was an error uploading the audio file.' + ); + expect(component.saveButtonText).toBe('Save'); + expect(component.saveInProgress).toBe(false); + expect(ngbActiveModal.close).not.toHaveBeenCalled(); - component.clearUploadedFile(); - expect(component.errorMessage).toBeNull(); - })); + component.clearUploadedFile(); + expect(component.errorMessage).toBeNull(); + })); }); diff --git a/core/templates/pages/exploration-editor-page/translation-tab/modal-templates/add-audio-translation-modal.component.ts b/core/templates/pages/exploration-editor-page/translation-tab/modal-templates/add-audio-translation-modal.component.ts index 2d417d7e42ce..92c91cd6c908 100644 --- a/core/templates/pages/exploration-editor-page/translation-tab/modal-templates/add-audio-translation-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/translation-tab/modal-templates/add-audio-translation-modal.component.ts @@ -16,103 +16,113 @@ * @fileoverview Component for add audio translation modal. */ -import { Component, Input, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; -import { ContextService } from 'services/context.service'; +import {Component, Input, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; +import {ContextService} from 'services/context.service'; @Component({ selector: 'oppia-add-audio-translation-modal', - templateUrl: './add-audio-translation-modal.component.html' + templateUrl: './add-audio-translation-modal.component.html', }) export class AddAudioTranslationModalComponent - extends ConfirmOrCancelModal implements OnInit { - // These properties are initialized using Angular lifecycle hooks - // and we need to do non-null assertion. For more information, see - // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 - @Input() audioFile!: File; - @Input() generatedFilename!: string; - @Input() isAudioAvailable!: boolean; - @Input() languageCode!: string; + extends ConfirmOrCancelModal + implements OnInit +{ + // These properties are initialized using Angular lifecycle hooks + // and we need to do non-null assertion. For more information, see + // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 + @Input() audioFile!: File; + @Input() generatedFilename!: string; + @Input() isAudioAvailable!: boolean; + @Input() languageCode!: string; - uploadedFile!: Blob | null; - droppedFile!: File; - saveButtonText!: string; - saveInProgress!: boolean; - errorMessage!: string | null; - BUTTON_TEXT_SAVE: string = 'Save'; - BUTTON_TEXT_SAVING: string = 'Saving...'; - ERROR_MESSAGE_BAD_FILE_UPLOAD: string = ( - 'There was an error uploading the audio file.'); + uploadedFile!: Blob | null; + droppedFile!: File; + saveButtonText!: string; + saveInProgress!: boolean; + errorMessage!: string | null; + BUTTON_TEXT_SAVE: string = 'Save'; + BUTTON_TEXT_SAVING: string = 'Saving...'; + ERROR_MESSAGE_BAD_FILE_UPLOAD: string = + 'There was an error uploading the audio file.'; - constructor( + constructor( private assetsBackendApiService: AssetsBackendApiService, private contextService: ContextService, - private ngbActiveModal: NgbActiveModal, - ) { - super(ngbActiveModal); - } + private ngbActiveModal: NgbActiveModal + ) { + super(ngbActiveModal); + } - isAudioTranslationValid(): boolean { - return ( - Boolean(this.uploadedFile) && - this.uploadedFile !== null && - this.uploadedFile.size !== null && - this.uploadedFile.size > 0); - } + isAudioTranslationValid(): boolean { + return ( + Boolean(this.uploadedFile) && + this.uploadedFile !== null && + this.uploadedFile.size !== null && + this.uploadedFile.size > 0 + ); + } - updateUploadedFile(file: Blob): void { - this.errorMessage = null; - this.uploadedFile = file; - } + updateUploadedFile(file: Blob): void { + this.errorMessage = null; + this.uploadedFile = file; + } - clearUploadedFile(): void { - this.errorMessage = null; - this.uploadedFile = null; - } + clearUploadedFile(): void { + this.errorMessage = null; + this.uploadedFile = null; + } - confirm(): void { - if (this.isAudioTranslationValid()) { - this.saveButtonText = this.BUTTON_TEXT_SAVING; - this.saveInProgress = true; - let explorationId = ( - this.contextService.getExplorationId()); - let file = this.uploadedFile; - if (file) { - Promise.resolve( - this.assetsBackendApiService.saveAudio( - explorationId, this.generatedFilename, file) - ).then((response) => { - if (file) { - this.ngbActiveModal.close({ - languageCode: this.languageCode, - filename: this.generatedFilename, - fileSizeBytes: file.size, - durationSecs: response.duration_secs - }); - } - }, (errorResponse) => { - this.errorMessage = ( - errorResponse.error || this.ERROR_MESSAGE_BAD_FILE_UPLOAD); - this.uploadedFile = null; - this.saveButtonText = this.BUTTON_TEXT_SAVE; - this.saveInProgress = false; - }); - } - } - } + confirm(): void { + if (this.isAudioTranslationValid()) { + this.saveButtonText = this.BUTTON_TEXT_SAVING; + this.saveInProgress = true; + let explorationId = this.contextService.getExplorationId(); + let file = this.uploadedFile; + if (file) { + Promise.resolve( + this.assetsBackendApiService.saveAudio( + explorationId, + this.generatedFilename, + file + ) + ).then( + response => { + if (file) { + this.ngbActiveModal.close({ + languageCode: this.languageCode, + filename: this.generatedFilename, + fileSizeBytes: file.size, + durationSecs: response.duration_secs, + }); + } + }, + errorResponse => { + this.errorMessage = + errorResponse.error || this.ERROR_MESSAGE_BAD_FILE_UPLOAD; + this.uploadedFile = null; + this.saveButtonText = this.BUTTON_TEXT_SAVE; + this.saveInProgress = false; + } + ); + } + } + } - ngOnInit(): void { - // Whether there was an error uploading the audio file. - this.saveButtonText = this.BUTTON_TEXT_SAVE; - this.saveInProgress = false; - this.droppedFile = this.audioFile; - } + ngOnInit(): void { + // Whether there was an error uploading the audio file. + this.saveButtonText = this.BUTTON_TEXT_SAVE; + this.saveInProgress = false; + this.droppedFile = this.audioFile; + } } -angular.module('oppia').directive('oppiaAddAudioTranslationModal', - downgradeComponent({ - component: AddAudioTranslationModalComponent - }) as angular.IDirectiveFactory); +angular.module('oppia').directive( + 'oppiaAddAudioTranslationModal', + downgradeComponent({ + component: AddAudioTranslationModalComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-editor-page/translation-tab/modal-templates/delete-audio-translation-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/translation-tab/modal-templates/delete-audio-translation-modal.component.spec.ts index 76be3d2b5284..bb2f28e9f3b5 100644 --- a/core/templates/pages/exploration-editor-page/translation-tab/modal-templates/delete-audio-translation-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/translation-tab/modal-templates/delete-audio-translation-modal.component.spec.ts @@ -12,15 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Unit tests for the DeleteAudioTranslationModalComponent. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { DeleteAudioTranslationModalComponent } from './delete-audio-translation-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {DeleteAudioTranslationModalComponent} from './delete-audio-translation-modal.component'; class MockActiveModal { close(): void { @@ -32,20 +31,20 @@ class MockActiveModal { } } -describe('Delete Exploration Modal Component', function() { +describe('Delete Exploration Modal Component', function () { let component: DeleteAudioTranslationModalComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - DeleteAudioTranslationModalComponent + declarations: [DeleteAudioTranslationModalComponent], + providers: [ + { + provide: NgbActiveModal, + useClass: MockActiveModal, + }, ], - providers: [{ - provide: NgbActiveModal, - useClass: MockActiveModal - }], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); diff --git a/core/templates/pages/exploration-editor-page/translation-tab/modal-templates/delete-audio-translation-modal.component.ts b/core/templates/pages/exploration-editor-page/translation-tab/modal-templates/delete-audio-translation-modal.component.ts index 611821541299..208f9696a6c4 100644 --- a/core/templates/pages/exploration-editor-page/translation-tab/modal-templates/delete-audio-translation-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/translation-tab/modal-templates/delete-audio-translation-modal.component.ts @@ -16,19 +16,16 @@ * @fileoverview Component for Delete Audio Translation Modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-delete-audio-translation-modal', - templateUrl: './delete-audio-translation-modal.component.html' + templateUrl: './delete-audio-translation-modal.component.html', }) - export class DeleteAudioTranslationModalComponent extends ConfirmOrCancelModal { - constructor( - private ngbActiveModal: NgbActiveModal, - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } } diff --git a/core/templates/pages/exploration-editor-page/translation-tab/modal-templates/translation-tab-busy-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/translation-tab/modal-templates/translation-tab-busy-modal.component.spec.ts index 63a03479c21e..fda38c49bb59 100644 --- a/core/templates/pages/exploration-editor-page/translation-tab/modal-templates/translation-tab-busy-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/translation-tab/modal-templates/translation-tab-busy-modal.component.spec.ts @@ -16,10 +16,10 @@ * @fileoverview Unit tests for TranslationTabBusyModalController. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { TranslationTabBusyModalComponent } from './translation-tab-busy-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {TranslationTabBusyModalComponent} from './translation-tab-busy-modal.component'; class MockActiveModal { close(): void { @@ -36,14 +36,14 @@ describe('Translation Tab Busy Modal Component ', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - TranslationTabBusyModalComponent + declarations: [TranslationTabBusyModalComponent], + providers: [ + { + provide: NgbActiveModal, + useClass: MockActiveModal, + }, ], - providers: [{ - provide: NgbActiveModal, - useClass: MockActiveModal - }], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -58,8 +58,7 @@ describe('Translation Tab Busy Modal Component ', () => { // Confirm and cancel functions tested in ConfirmOrCancelModal spec file. // So only Component is defined need to be tested in this file. - it('should initialize component properties when component is initialized', - function() { - expect(component).toBeDefined(); - }); + it('should initialize component properties when component is initialized', function () { + expect(component).toBeDefined(); + }); }); diff --git a/core/templates/pages/exploration-editor-page/translation-tab/modal-templates/translation-tab-busy-modal.component.ts b/core/templates/pages/exploration-editor-page/translation-tab/modal-templates/translation-tab-busy-modal.component.ts index 761286c8fadf..9222865d8e9d 100644 --- a/core/templates/pages/exploration-editor-page/translation-tab/modal-templates/translation-tab-busy-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/translation-tab/modal-templates/translation-tab-busy-modal.component.ts @@ -16,24 +16,21 @@ * @fileoverview Component for translation tab busy modal. */ -import { Component, Input } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component, Input} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-translation-tab-busy-modal', - templateUrl: './translation-tab-busy-modal.component.html' + templateUrl: './translation-tab-busy-modal.component.html', }) -export class TranslationTabBusyModalComponent - extends ConfirmOrCancelModal { +export class TranslationTabBusyModalComponent extends ConfirmOrCancelModal { // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @Input() busyMessage!: string; - constructor( - private ngbActiveModal: NgbActiveModal, - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } } diff --git a/core/templates/pages/exploration-editor-page/translation-tab/modal-templates/welcome-translation-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/translation-tab/modal-templates/welcome-translation-modal.component.spec.ts index 68f6f1616e07..b456eb2d7169 100644 --- a/core/templates/pages/exploration-editor-page/translation-tab/modal-templates/welcome-translation-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/translation-tab/modal-templates/welcome-translation-modal.component.spec.ts @@ -16,14 +16,13 @@ * @fileoverview Unit tests for welcome translation tab. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; -import { ContextService } from 'services/context.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { WelcomeTranslationModalComponent } from './welcome-translation-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {ContextService} from 'services/context.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {WelcomeTranslationModalComponent} from './welcome-translation-modal.component'; class MockActiveModal { close(): void { @@ -35,7 +34,7 @@ class MockActiveModal { } } -describe('Welcome Translation Modal Component', function() { +describe('Welcome Translation Modal Component', function () { let component: WelcomeTranslationModalComponent; let fixture: ComponentFixture; let contextService: ContextService; @@ -45,19 +44,17 @@ describe('Welcome Translation Modal Component', function() { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - WelcomeTranslationModalComponent - ], + declarations: [WelcomeTranslationModalComponent], providers: [ ContextService, UrlInterpolationService, { provide: NgbActiveModal, - useClass: MockActiveModal + useClass: MockActiveModal, }, - SiteAnalyticsService + SiteAnalyticsService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); beforeEach(() => { @@ -70,17 +67,20 @@ describe('Welcome Translation Modal Component', function() { siteAnalyticsService = TestBed.inject(SiteAnalyticsService); spyOn(contextService, 'getExplorationId').and.returnValue(explorationId); - spyOn(siteAnalyticsService, 'registerTutorialModalOpenEvent') - .and.callThrough(); + spyOn( + siteAnalyticsService, + 'registerTutorialModalOpenEvent' + ).and.callThrough(); fixture.detectChanges(); }); - it('should initialize component properties after it is initialized', - function() { - expect(component.explorationId).toBe(explorationId); - expect(siteAnalyticsService.registerTutorialModalOpenEvent) - .toHaveBeenCalledWith(explorationId); - expect(component.translationWelcomeImgUrl).toBe( - '/assets/images/general/editor_welcome.svg'); - }); + it('should initialize component properties after it is initialized', function () { + expect(component.explorationId).toBe(explorationId); + expect( + siteAnalyticsService.registerTutorialModalOpenEvent + ).toHaveBeenCalledWith(explorationId); + expect(component.translationWelcomeImgUrl).toBe( + '/assets/images/general/editor_welcome.svg' + ); + }); }); diff --git a/core/templates/pages/exploration-editor-page/translation-tab/modal-templates/welcome-translation-modal.component.ts b/core/templates/pages/exploration-editor-page/translation-tab/modal-templates/welcome-translation-modal.component.ts index bb7c0d41afc3..a32c369fce88 100644 --- a/core/templates/pages/exploration-editor-page/translation-tab/modal-templates/welcome-translation-modal.component.ts +++ b/core/templates/pages/exploration-editor-page/translation-tab/modal-templates/welcome-translation-modal.component.ts @@ -16,20 +16,21 @@ * @fileoverview Component for the welcome translation modal. */ -import { Component, OnInit, ViewChild, ElementRef } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { ContextService } from 'services/context.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component, OnInit, ViewChild, ElementRef} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {ContextService} from 'services/context.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-welcome-translation-modal', - templateUrl: './welcome-translation-modal.component.html' + templateUrl: './welcome-translation-modal.component.html', }) - export class WelcomeTranslationModalComponent - extends ConfirmOrCancelModal implements OnInit { + extends ConfirmOrCancelModal + implements OnInit +{ // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -41,7 +42,7 @@ export class WelcomeTranslationModalComponent private ngbActiveModal: NgbActiveModal, private urlInterpolationService: UrlInterpolationService, private contextService: ContextService, - private siteAnalyticsService: SiteAnalyticsService, + private siteAnalyticsService: SiteAnalyticsService ) { super(ngbActiveModal); } @@ -49,9 +50,12 @@ export class WelcomeTranslationModalComponent ngOnInit(): void { this.explorationId = this.contextService.getExplorationId(); this.siteAnalyticsService.registerTutorialModalOpenEvent( - this.explorationId); - this.translationWelcomeImgUrl = this.urlInterpolationService - .getStaticImageUrl('/general/editor_welcome.svg'); + this.explorationId + ); + this.translationWelcomeImgUrl = + this.urlInterpolationService.getStaticImageUrl( + '/general/editor_welcome.svg' + ); this.welcomeHeading?.nativeElement.focus(); } } diff --git a/core/templates/pages/exploration-editor-page/translation-tab/services/translation-language.service.spec.ts b/core/templates/pages/exploration-editor-page/translation-tab/services/translation-language.service.spec.ts index d913a6eff9cc..db8b82637d53 100644 --- a/core/templates/pages/exploration-editor-page/translation-tab/services/translation-language.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/translation-tab/services/translation-language.service.spec.ts @@ -16,9 +16,9 @@ * @fileoverview Unit test for the Translation language service. */ -import { TranslationLanguageService } from 'pages/exploration-editor-page/translation-tab/services/translation-language.service'; -import { LanguageUtilService } from 'domain/utilities/language-util.service'; -import { TestBed } from '@angular/core/testing'; +import {TranslationLanguageService} from 'pages/exploration-editor-page/translation-tab/services/translation-language.service'; +import {LanguageUtilService} from 'domain/utilities/language-util.service'; +import {TestBed} from '@angular/core/testing'; describe('Translation language service', () => { let translationLanguageService: TranslationLanguageService; @@ -27,57 +27,61 @@ describe('Translation language service', () => { beforeEach(() => { translationLanguageService = TestBed.get(TranslationLanguageService); languageUtilService = TestBed.get(LanguageUtilService); - spyOn(languageUtilService, 'getAllVoiceoverLanguageCodes').and.returnValue( - ['en', 'hi']); + spyOn(languageUtilService, 'getAllVoiceoverLanguageCodes').and.returnValue([ + 'en', + 'hi', + ]); spyOn(languageUtilService, 'getAudioLanguageDescription').and.callFake( (activeLanguageCode: string) => { let descriptions: Record = { - en: 'English' + en: 'English', }; return descriptions[activeLanguageCode]; - }); + } + ); }); - describe('Translation language service', () => { it('should correctly set and get state names', () => { translationLanguageService.setActiveLanguageCode('en'); - expect(translationLanguageService.getActiveLanguageCode()).toBe( - 'en'); + expect(translationLanguageService.getActiveLanguageCode()).toBe('en'); }); it('should not allow invalid state names to be set', () => { translationLanguageService.setActiveLanguageCode('eng'); expect( - translationLanguageService?.getActiveLanguageCode()).toBeUndefined(); + translationLanguageService?.getActiveLanguageCode() + ).toBeUndefined(); translationLanguageService.setActiveLanguageCode('Invalid language code'); expect( - translationLanguageService.getActiveLanguageCode()).toBeUndefined(); + translationLanguageService.getActiveLanguageCode() + ).toBeUndefined(); }); it('should show the language direction', () => { translationLanguageService.setActiveLanguageCode('ar'); - expect( - translationLanguageService.getActiveLanguageDirection()).toBe('rtl'); + expect(translationLanguageService.getActiveLanguageDirection()).toBe( + 'rtl' + ); translationLanguageService.setActiveLanguageCode('en'); - expect( - translationLanguageService.getActiveLanguageDirection()).toBe('ltr'); + expect(translationLanguageService.getActiveLanguageDirection()).toBe( + 'ltr' + ); }); it('should show the language description', () => { translationLanguageService.setActiveLanguageCode('en'); - expect( - translationLanguageService.getActiveLanguageDescription()).toBe( - 'English'); + expect(translationLanguageService.getActiveLanguageDescription()).toBe( + 'English' + ); }); - it('should not show the language description of invalid state name', - () => { - translationLanguageService.setActiveLanguageCode('eng'); - expect( - translationLanguageService.getActiveLanguageDescription()) - .toBeNull(); - }); + it('should not show the language description of invalid state name', () => { + translationLanguageService.setActiveLanguageCode('eng'); + expect( + translationLanguageService.getActiveLanguageDescription() + ).toBeNull(); + }); }); }); diff --git a/core/templates/pages/exploration-editor-page/translation-tab/services/translation-language.service.ts b/core/templates/pages/exploration-editor-page/translation-tab/services/translation-language.service.ts index d76b7115af16..cab9b864d6c2 100644 --- a/core/templates/pages/exploration-editor-page/translation-tab/services/translation-language.service.ts +++ b/core/templates/pages/exploration-editor-page/translation-tab/services/translation-language.service.ts @@ -17,29 +17,29 @@ * in the translation tab is currently active. */ -import { EventEmitter, Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; - -import { LanguageUtilService } from 'domain/utilities/language-util.service'; -import { LoggerService } from 'services/contextual/logger.service'; +import {EventEmitter, Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {LanguageUtilService} from 'domain/utilities/language-util.service'; +import {LoggerService} from 'services/contextual/logger.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class TranslationLanguageService { // This property is initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 private activeLanguageCode!: string; - private allAudioLanguageCodes: string[] = ( - this.languageUtilService.getAllVoiceoverLanguageCodes()); + private allAudioLanguageCodes: string[] = + this.languageUtilService.getAllVoiceoverLanguageCodes(); private _activeLanguageChangedEventEmitter = new EventEmitter(); constructor( private languageUtilService: LanguageUtilService, - private loggerService: LoggerService) {} + private loggerService: LoggerService + ) {} getActiveLanguageCode(): string { return this.activeLanguageCode; @@ -47,15 +47,19 @@ export class TranslationLanguageService { getActiveLanguageDirection(): string { return this.languageUtilService.getLanguageDirection( - this.getActiveLanguageCode()); + this.getActiveLanguageCode() + ); } // This function throws an error if 'newActiveLanguageCode' is invalid. setActiveLanguageCode(newActiveLanguageCode: string): void { - if (newActiveLanguageCode && - this.allAudioLanguageCodes.indexOf(newActiveLanguageCode) < 0) { + if ( + newActiveLanguageCode && + this.allAudioLanguageCodes.indexOf(newActiveLanguageCode) < 0 + ) { this.loggerService.error( - 'Invalid active language code: ' + newActiveLanguageCode); + 'Invalid active language code: ' + newActiveLanguageCode + ); return; } this.activeLanguageCode = newActiveLanguageCode; @@ -68,7 +72,8 @@ export class TranslationLanguageService { return null; } return this.languageUtilService.getAudioLanguageDescription( - this.activeLanguageCode); + this.activeLanguageCode + ); } get onActiveLanguageChanged(): EventEmitter { @@ -76,6 +81,9 @@ export class TranslationLanguageService { } } -angular.module('oppia').service( - 'TranslationLanguageService', - downgradeInjectable(TranslationLanguageService)); +angular + .module('oppia') + .service( + 'TranslationLanguageService', + downgradeInjectable(TranslationLanguageService) + ); diff --git a/core/templates/pages/exploration-editor-page/translation-tab/services/translation-status.service.spec.ts b/core/templates/pages/exploration-editor-page/translation-tab/services/translation-status.service.spec.ts index 89a2b8c307ed..f9b1b4911ad5 100644 --- a/core/templates/pages/exploration-editor-page/translation-tab/services/translation-status.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/translation-tab/services/translation-status.service.spec.ts @@ -16,25 +16,24 @@ * @fileoverview Unit test for the Translation status service. */ -import { TestBed } from '@angular/core/testing'; -import { ExplorationDataService } from 'pages/exploration-editor-page/services/exploration-data.service'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; -import { TranslationLanguageService } from 'pages/exploration-editor-page/translation-tab/services/translation-language.service'; -import { TranslationStatusService } from 'pages/exploration-editor-page/translation-tab/services/translation-status.service'; -import { TranslationTabActiveModeService } from 'pages/exploration-editor-page/translation-tab/services/translation-tab-active-mode.service'; -import { StateWrittenTranslationsService } from 'components/state-editor/state-editor-properties-services/state-written-translations.service'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { StateRecordedVoiceoversService } from 'components/state-editor/state-editor-properties-services/state-recorded-voiceovers.service'; -import { GenerateContentIdService } from 'services/generate-content-id.service'; -import { EntityTranslationsService } from 'services/entity-translations.services'; -import { EntityTranslation } from 'domain/translation/EntityTranslationObjectFactory'; - +import {TestBed} from '@angular/core/testing'; +import {ExplorationDataService} from 'pages/exploration-editor-page/services/exploration-data.service'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; +import {TranslationLanguageService} from 'pages/exploration-editor-page/translation-tab/services/translation-language.service'; +import {TranslationStatusService} from 'pages/exploration-editor-page/translation-tab/services/translation-status.service'; +import {TranslationTabActiveModeService} from 'pages/exploration-editor-page/translation-tab/services/translation-tab-active-mode.service'; +import {StateWrittenTranslationsService} from 'components/state-editor/state-editor-properties-services/state-written-translations.service'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {StateRecordedVoiceoversService} from 'components/state-editor/state-editor-properties-services/state-recorded-voiceovers.service'; +import {GenerateContentIdService} from 'services/generate-content-id.service'; +import {EntityTranslationsService} from 'services/entity-translations.services'; +import {EntityTranslation} from 'domain/translation/EntityTranslationObjectFactory'; class MockNgbModal { open() { return { - result: Promise.resolve() + result: Promise.resolve(), }; } } @@ -53,7 +52,6 @@ describe('Translation status service', () => { let NO_ASSETS_AVAILABLE_COLOR = '#D14836'; let statesWithAudioDict = null; - beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], @@ -67,7 +65,7 @@ describe('Translation status service', () => { GenerateContentIdService, { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, { provide: ExplorationDataService, @@ -75,10 +73,10 @@ describe('Translation status service', () => { explorationId: 0, autosaveChangeListAsync() { return; - } - } - } - ] + }, + }, + }, + ], }); tss = TestBed.inject(TranslationStatusService); @@ -89,7 +87,10 @@ describe('Translation status service', () => { entityTranslationsService = TestBed.inject(EntityTranslationsService); generateContentIdService = TestBed.inject(GenerateContentIdService); let currentIndex = 9; - generateContentIdService.init(() => currentIndex++, () => { }); + generateContentIdService.init( + () => currentIndex++, + () => {} + ); }); beforeEach(() => { @@ -97,7 +98,7 @@ describe('Translation status service', () => { First: { content: { html: '

This is first card.

', - content_id: 'content_0' + content_id: 'content_0', }, recorded_voiceovers: { voiceovers_mapping: { @@ -108,61 +109,67 @@ describe('Translation status service', () => { needs_update: false, filename: 'filename1.mp3', file_size_bytes: 43467, - duration_secs: 4.3 - } + duration_secs: 4.3, + }, }, feedback_2: {}, - } + }, }, interaction: { - answer_groups: [{ - tagged_skill_misconception_id: null, - outcome: { - refresher_exploration_id: null, - param_changes: [], - labelled_as_correct: false, - feedback: { - html: '

This is feedback1

', - content_id: 'feedback_2' + answer_groups: [ + { + tagged_skill_misconception_id: null, + outcome: { + refresher_exploration_id: null, + param_changes: [], + labelled_as_correct: false, + feedback: { + html: '

This is feedback1

', + content_id: 'feedback_2', + }, + missing_prerequisite_skill_id: null, + dest_if_really_stuck: null, + dest: 'Second', }, - missing_prerequisite_skill_id: null, - dest_if_really_stuck: null, - dest: 'Second' + rule_specs: [ + { + rule_type: 'Equals', + inputs: {x: 0}, + }, + ], + training_data: [], }, - rule_specs: [{ - rule_type: 'Equals', - inputs: {x: 0} - }], - training_data: [] - }, - { - tagged_skill_misconception_id: null, - outcome: { - refresher_exploration_id: null, - param_changes: [], - labelled_as_correct: false, - feedback: { - html: '

This is feedback2

', - content_id: 'feedback_3' + { + tagged_skill_misconception_id: null, + outcome: { + refresher_exploration_id: null, + param_changes: [], + labelled_as_correct: false, + feedback: { + html: '

This is feedback2

', + content_id: 'feedback_3', + }, + missing_prerequisite_skill_id: null, + dest_if_really_stuck: null, + dest: 'First', }, - missing_prerequisite_skill_id: null, - dest_if_really_stuck: null, - dest: 'First' + rule_specs: [ + { + rule_type: 'Equals', + inputs: {x: 1}, + }, + ], + training_data: [], }, - rule_specs: [{ - rule_type: 'Equals', - inputs: {x: 1} - }], - training_data: [] - }], + ], solution: null, hints: [], id: 'MultipleChoiceInput', customization_args: { choices: { - value: ['

1

', '

2

'] + value: ['

1

', '

2

'], }, - showChoicesInShuffledOrder: {value: false} + showChoicesInShuffledOrder: {value: false}, }, default_outcome: { refresher_exploration_id: null, @@ -170,13 +177,13 @@ describe('Translation status service', () => { labelled_as_correct: false, feedback: { html: '', - content_id: 'default_outcome_1' + content_id: 'default_outcome_1', }, missing_prerequisite_skill_id: null, dest_if_really_stuck: null, - dest: 'First' + dest: 'First', }, - confirmed_unclassified_answers: [] + confirmed_unclassified_answers: [], }, linked_skill_id: null, solicit_answer_details: false, @@ -187,36 +194,40 @@ describe('Translation status service', () => { Second: { content: { html: '

This is second card

', - content_id: 'content_5' + content_id: 'content_5', }, recorded_voiceovers: { voiceovers_mapping: { content_5: {}, default_outcome_6: {}, - feedback_7: {} - } + feedback_7: {}, + }, }, interaction: { - answer_groups: [{ - tagged_skill_misconception_id: null, - outcome: { - refresher_exploration_id: null, - param_changes: [], - labelled_as_correct: false, - feedback: { - html: '', - content_id: 'feedback_7' + answer_groups: [ + { + tagged_skill_misconception_id: null, + outcome: { + refresher_exploration_id: null, + param_changes: [], + labelled_as_correct: false, + feedback: { + html: '', + content_id: 'feedback_7', + }, + missing_prerequisite_skill_id: null, + dest_if_really_stuck: null, + dest: 'Third', }, - missing_prerequisite_skill_id: null, - dest_if_really_stuck: null, - dest: 'Third' + rule_specs: [ + { + rule_type: 'Equals', + inputs: {x: 0}, + }, + ], + training_data: [], }, - rule_specs: [{ - rule_type: 'Equals', - inputs: {x: 0} - }], - training_data: [] - }], + ], solution: null, hints: [], id: 'MultipleChoiceInput', @@ -224,7 +235,7 @@ describe('Translation status service', () => { choices: { value: ['

1

'], }, - showChoicesInShuffledOrder: {value: false} + showChoicesInShuffledOrder: {value: false}, }, default_outcome: { refresher_exploration_id: null, @@ -232,13 +243,13 @@ describe('Translation status service', () => { labelled_as_correct: false, feedback: { html: '', - content_id: 'default_outcome_6' + content_id: 'default_outcome_6', }, missing_prerequisite_skill_id: null, dest_if_really_stuck: null, - dest: 'Second' + dest: 'Second', }, - confirmed_unclassified_answers: [] + confirmed_unclassified_answers: [], }, linked_skill_id: null, solicit_answer_details: false, @@ -249,7 +260,7 @@ describe('Translation status service', () => { Third: { content: { html: 'Congratulations, you have finished!', - content_id: 'content_8' + content_id: 'content_8', }, recorded_voiceovers: { voiceovers_mapping: { @@ -258,10 +269,10 @@ describe('Translation status service', () => { needs_update: false, filename: 'content-en-s86jb5zajs.mp3', file_size_bytes: 38870, - duration_secs: 38.8 - } - } - } + duration_secs: 38.8, + }, + }, + }, }, interaction: { answer_groups: [], @@ -270,23 +281,23 @@ describe('Translation status service', () => { id: 'EndExploration', customization_args: { recommendedExplorationIds: { - value: [] - } + value: [], + }, }, default_outcome: null, - confirmed_unclassified_answers: [] + confirmed_unclassified_answers: [], }, linked_skill_id: null, solicit_answer_details: false, classifier_model_id: null, param_changes: [], card_is_checkpoint: false, - } + }, }; ess.init(statesWithAudioDict, false); ttams.activateVoiceoverMode(); tls.setActiveLanguageCode('en'); - entityTranslationsService.languageCodeToEntityTranslations.hi = ( + entityTranslationsService.languageCodeToEntityTranslations.hi = EntityTranslation.createFromBackendDict({ entity_id: 'exp_id', entity_type: 'exploration', @@ -296,11 +307,10 @@ describe('Translation status service', () => { feedback_3: { content_format: 'html', content_value: '

This is feedback 1.

', - needs_update: false - } - } - }) - ); + needs_update: false, + }, + }, + }); tss.refresh(); }); @@ -313,327 +323,359 @@ describe('Translation status service', () => { expect(tss.explorationVoiceoverContentRequiredCount).toEqual(0); }); - it('should return a correct list of state names for which audio needs ' + - 'update', () => { - ttams.activateVoiceoverMode(); - var statesNeedingAudioUpdate = tss.getAllStatesNeedUpdatewarning(); - // To check that initially no state contains audio that needs update. - expect(Object.keys(statesNeedingAudioUpdate).length).toBe(0); - srvs.init('First', ess.getRecordedVoiceoversMemento('First')); - var value = srvs.displayed; - value.toggleNeedsUpdateAttribute('feedback_3', 'en'); - srvs.saveDisplayedValue(); - ess.saveRecordedVoiceovers('First', value); - tss.refresh(); - tss.getAllStateStatusColors(); - - statesNeedingAudioUpdate = tss.getAllStatesNeedUpdatewarning(); - // To check that "First" state contains audio that needs update. - expect(Object.keys(statesNeedingAudioUpdate).length).toBe(1); - expect(Object.keys(statesNeedingAudioUpdate)[0]).toBe('First'); - }); - - it('should return a correct list of state names for which translation ' + - 'needs update', () => { - ttams.activateTranslationMode(); - tls.setActiveLanguageCode('hi'); - var statesNeedingTranslationUpdate = tss.getAllStatesNeedUpdatewarning(); - expect(Object.keys(statesNeedingTranslationUpdate).length).toBe(0); - - entityTranslationsService.languageCodeToEntityTranslations.hi = ( - EntityTranslation.createFromBackendDict({ - entity_id: 'exp_id', - entity_type: 'exploration', - entity_version: 5, - language_code: 'hi', - translations: { - feedback_3: { - content_format: 'html', - content_value: '

This is feedback 1.

', - needs_update: true + it( + 'should return a correct list of state names for which audio needs ' + + 'update', + () => { + ttams.activateVoiceoverMode(); + var statesNeedingAudioUpdate = tss.getAllStatesNeedUpdatewarning(); + // To check that initially no state contains audio that needs update. + expect(Object.keys(statesNeedingAudioUpdate).length).toBe(0); + srvs.init('First', ess.getRecordedVoiceoversMemento('First')); + var value = srvs.displayed; + value.toggleNeedsUpdateAttribute('feedback_3', 'en'); + srvs.saveDisplayedValue(); + ess.saveRecordedVoiceovers('First', value); + tss.refresh(); + tss.getAllStateStatusColors(); + + statesNeedingAudioUpdate = tss.getAllStatesNeedUpdatewarning(); + // To check that "First" state contains audio that needs update. + expect(Object.keys(statesNeedingAudioUpdate).length).toBe(1); + expect(Object.keys(statesNeedingAudioUpdate)[0]).toBe('First'); + } + ); + + it( + 'should return a correct list of state names for which translation ' + + 'needs update', + () => { + ttams.activateTranslationMode(); + tls.setActiveLanguageCode('hi'); + var statesNeedingTranslationUpdate = tss.getAllStatesNeedUpdatewarning(); + expect(Object.keys(statesNeedingTranslationUpdate).length).toBe(0); + + entityTranslationsService.languageCodeToEntityTranslations.hi = + EntityTranslation.createFromBackendDict({ + entity_id: 'exp_id', + entity_type: 'exploration', + entity_version: 5, + language_code: 'hi', + translations: { + feedback_3: { + content_format: 'html', + content_value: '

This is feedback 1.

', + needs_update: true, + }, + feedback_2: { + content_format: 'html', + content_value: '

This is first card.

', + needs_update: true, + }, }, - feedback_2: { - content_format: 'html', - content_value: '

This is first card.

', - needs_update: true - } - } - }) - ); - - tss.refresh(); - statesNeedingTranslationUpdate = tss.getAllStatesNeedUpdatewarning(); - expect(Object.keys(statesNeedingTranslationUpdate).length).toBe(1); - expect(Object.keys(statesNeedingTranslationUpdate)[0]).toBe('First'); - }); + }); - it('should return a correct count of audio and translations required in ' + - 'an exploration', () => { - ttams.activateVoiceoverMode(); - var explorationAudioRequiredCount = ( - tss.getExplorationContentRequiredCount()); - expect(explorationAudioRequiredCount).toBe(8); - - ttams.activateTranslationMode(); - tls.setActiveLanguageCode('hi'); - var explorationTranslationsRequiredCount = ( - tss.getExplorationContentRequiredCount()); - expect(explorationTranslationsRequiredCount).toBe(8); - - // To test changes after adding a new state. - ess.addState('Fourth', () => {}); - ess.saveInteractionId('Third', 'MultipleChoiceInput'); - ess.saveInteractionId('Fourth', 'EndExploration'); - - tss.refresh(); - ttams.activateVoiceoverMode(); - tls.setActiveLanguageCode('en'); - var explorationAudioRequiredCount = ( - tss.getExplorationContentRequiredCount()); - expect(explorationAudioRequiredCount).toBe(9); - - ttams.activateTranslationMode(); - tls.setActiveLanguageCode('hi'); - - var explorationTranslationsRequiredCount = ( - tss.getExplorationContentRequiredCount()); - expect(explorationTranslationsRequiredCount).toBe(9); - }); - - it('should return a correct count of audio not available in an exploration', + tss.refresh(); + statesNeedingTranslationUpdate = tss.getAllStatesNeedUpdatewarning(); + expect(Object.keys(statesNeedingTranslationUpdate).length).toBe(1); + expect(Object.keys(statesNeedingTranslationUpdate)[0]).toBe('First'); + } + ); + + it( + 'should return a correct count of audio and translations required in ' + + 'an exploration', () => { ttams.activateVoiceoverMode(); - var explorationAudioNotAvailableCount = tss - .getExplorationContentNotAvailableCount(); - expect(explorationAudioNotAvailableCount).toBe(6); + var explorationAudioRequiredCount = + tss.getExplorationContentRequiredCount(); + expect(explorationAudioRequiredCount).toBe(8); + ttams.activateTranslationMode(); + tls.setActiveLanguageCode('hi'); + var explorationTranslationsRequiredCount = + tss.getExplorationContentRequiredCount(); + expect(explorationTranslationsRequiredCount).toBe(8); + + // To test changes after adding a new state. ess.addState('Fourth', () => {}); ess.saveInteractionId('Third', 'MultipleChoiceInput'); ess.saveInteractionId('Fourth', 'EndExploration'); + tss.refresh(); + ttams.activateVoiceoverMode(); + tls.setActiveLanguageCode('en'); + var explorationAudioRequiredCount = + tss.getExplorationContentRequiredCount(); + expect(explorationAudioRequiredCount).toBe(9); - explorationAudioNotAvailableCount = ( - tss.getExplorationContentNotAvailableCount()); - expect(explorationAudioNotAvailableCount).toBe(7); - }); + ttams.activateTranslationMode(); + tls.setActiveLanguageCode('hi'); - it('should return a correct count of translations not available in an ' + - 'exploration', () => { - ttams.activateTranslationMode(); - tls.setActiveLanguageCode('hi'); - var explorationTranslationNotAvailableCount = ( - tss.getExplorationContentNotAvailableCount()); - expect(explorationTranslationNotAvailableCount).toBe(6); + var explorationTranslationsRequiredCount = + tss.getExplorationContentRequiredCount(); + expect(explorationTranslationsRequiredCount).toBe(9); + } + ); + + it('should return a correct count of audio not available in an exploration', () => { + ttams.activateVoiceoverMode(); + var explorationAudioNotAvailableCount = + tss.getExplorationContentNotAvailableCount(); + expect(explorationAudioNotAvailableCount).toBe(6); ess.addState('Fourth', () => {}); ess.saveInteractionId('Third', 'MultipleChoiceInput'); ess.saveInteractionId('Fourth', 'EndExploration'); - - ttams.activateTranslationMode(); tss.refresh(); - explorationTranslationNotAvailableCount = ( - tss.getExplorationContentNotAvailableCount()); - expect(explorationTranslationNotAvailableCount).toBe(8); - }); - it('should return correct status color for audio availability in the ' + - 'active state components', () => { - ttams.activateVoiceoverMode(); - srvs.init('First', ess.getRecordedVoiceoversMemento('First')); - var activeStateComponentStatus = tss - .getActiveStateComponentStatusColor('content'); - expect(activeStateComponentStatus).toBe(NO_ASSETS_AVAILABLE_COLOR); - activeStateComponentStatus = tss - .getActiveStateComponentStatusColor('feedback'); - expect(activeStateComponentStatus).toBe(FEW_ASSETS_AVAILABLE_COLOR); - // To test changes after adding an audio translation to "content" - // in the first state. - srvs.displayed.addVoiceover('content_0', 'en', 'file.mp3', 1000, 1000); - srvs.saveDisplayedValue(); - var value = srvs.displayed; - ess.saveRecordedVoiceovers('First', value); - activeStateComponentStatus = tss - .getActiveStateComponentStatusColor('content'); - expect(activeStateComponentStatus).toBe(ALL_ASSETS_AVAILABLE_COLOR); - srvs.init('Second', ess.getRecordedVoiceoversMemento('Second')); - activeStateComponentStatus = tss - .getActiveStateComponentStatusColor('content'); - expect(activeStateComponentStatus).toBe(NO_ASSETS_AVAILABLE_COLOR); - activeStateComponentStatus = tss - .getActiveStateComponentStatusColor('feedback'); - expect(activeStateComponentStatus).toBe(NO_ASSETS_AVAILABLE_COLOR); - srvs.init('Third', ess.getRecordedVoiceoversMemento('Third')); - activeStateComponentStatus = tss - .getActiveStateComponentStatusColor('content'); - expect(activeStateComponentStatus).toBe(ALL_ASSETS_AVAILABLE_COLOR); + explorationAudioNotAvailableCount = + tss.getExplorationContentNotAvailableCount(); + expect(explorationAudioNotAvailableCount).toBe(7); }); - it('should return correct status color for translations availability in ' + - 'the active state components', () => { - ttams.activateTranslationMode(); - tls.setActiveLanguageCode('hi'); - srvs.init('First', ess.getRecordedVoiceoversMemento('First')); - tss.refresh(); - - var activeStateComponentStatus = ( - tss.getActiveStateComponentStatusColor('content')); - expect(activeStateComponentStatus).toBe(NO_ASSETS_AVAILABLE_COLOR); - activeStateComponentStatus = ( - tss.getActiveStateComponentStatusColor('feedback')); - expect(activeStateComponentStatus).toBe(FEW_ASSETS_AVAILABLE_COLOR); - - tss.entityTranslation = EntityTranslation.createFromBackendDict({ - entity_id: 'exp_id', - entity_type: 'exploration', - entity_version: 5, - language_code: 'hi', - translations: { - content_0: { - content_format: 'html', - content_value: '

This is content.

', - needs_update: false - }, - feedback_2: { - content_format: 'html', - content_value: '

This is first card.

', - needs_update: false - } - } - }); - - activeStateComponentStatus = ( - tss.getActiveStateComponentStatusColor('content')); - expect(activeStateComponentStatus).toBe(ALL_ASSETS_AVAILABLE_COLOR); - activeStateComponentStatus = ( - tss.getActiveStateComponentStatusColor('feedback')); - expect(activeStateComponentStatus).toBe(FEW_ASSETS_AVAILABLE_COLOR); - }); + it( + 'should return a correct count of translations not available in an ' + + 'exploration', + () => { + ttams.activateTranslationMode(); + tls.setActiveLanguageCode('hi'); + var explorationTranslationNotAvailableCount = + tss.getExplorationContentNotAvailableCount(); + expect(explorationTranslationNotAvailableCount).toBe(6); - it('should correctly return whether active state component audio needs ' + - 'update', () => { - ttams.activateVoiceoverMode(); - srvs.init('First', ess.getRecordedVoiceoversMemento('First')); - var activeStateComponentNeedsUpdateStatus = tss - .getActiveStateComponentNeedsUpdateStatus('feedback'); - // To check that initially the state component "feedback" does not - // contain audio that needs update. - expect(activeStateComponentNeedsUpdateStatus).toBe(false); - var value = srvs.displayed; - // To test changes after changing "needs update" status of an audio. - value.toggleNeedsUpdateAttribute('feedback_3', 'en'); - srvs.saveDisplayedValue(); - ess.saveRecordedVoiceovers('First', value); - activeStateComponentNeedsUpdateStatus = tss - .getActiveStateComponentNeedsUpdateStatus('feedback'); - // To check that the state component "feedback" contains audio that - // needs update. - expect(activeStateComponentNeedsUpdateStatus).toBe(true); - }); + ess.addState('Fourth', () => {}); + ess.saveInteractionId('Third', 'MultipleChoiceInput'); + ess.saveInteractionId('Fourth', 'EndExploration'); - it('should correctly return whether active state component translation ' + - 'needs update', (() => { - ttams.activateTranslationMode(); - tls.setActiveLanguageCode('hi'); - srvs.init('First', ess.getRecordedVoiceoversMemento('First')); + ttams.activateTranslationMode(); + tss.refresh(); + explorationTranslationNotAvailableCount = + tss.getExplorationContentNotAvailableCount(); + expect(explorationTranslationNotAvailableCount).toBe(8); + } + ); + + it( + 'should return correct status color for audio availability in the ' + + 'active state components', + () => { + ttams.activateVoiceoverMode(); + srvs.init('First', ess.getRecordedVoiceoversMemento('First')); + var activeStateComponentStatus = + tss.getActiveStateComponentStatusColor('content'); + expect(activeStateComponentStatus).toBe(NO_ASSETS_AVAILABLE_COLOR); + activeStateComponentStatus = + tss.getActiveStateComponentStatusColor('feedback'); + expect(activeStateComponentStatus).toBe(FEW_ASSETS_AVAILABLE_COLOR); + // To test changes after adding an audio translation to "content" + // in the first state. + srvs.displayed.addVoiceover('content_0', 'en', 'file.mp3', 1000, 1000); + srvs.saveDisplayedValue(); + var value = srvs.displayed; + ess.saveRecordedVoiceovers('First', value); + activeStateComponentStatus = + tss.getActiveStateComponentStatusColor('content'); + expect(activeStateComponentStatus).toBe(ALL_ASSETS_AVAILABLE_COLOR); + srvs.init('Second', ess.getRecordedVoiceoversMemento('Second')); + activeStateComponentStatus = + tss.getActiveStateComponentStatusColor('content'); + expect(activeStateComponentStatus).toBe(NO_ASSETS_AVAILABLE_COLOR); + activeStateComponentStatus = + tss.getActiveStateComponentStatusColor('feedback'); + expect(activeStateComponentStatus).toBe(NO_ASSETS_AVAILABLE_COLOR); + srvs.init('Third', ess.getRecordedVoiceoversMemento('Third')); + activeStateComponentStatus = + tss.getActiveStateComponentStatusColor('content'); + expect(activeStateComponentStatus).toBe(ALL_ASSETS_AVAILABLE_COLOR); + } + ); + + it( + 'should return correct status color for translations availability in ' + + 'the active state components', + () => { + ttams.activateTranslationMode(); + tls.setActiveLanguageCode('hi'); + srvs.init('First', ess.getRecordedVoiceoversMemento('First')); + tss.refresh(); - var activeStateComponentNeedsUpdateStatus = ( - tss.getActiveStateComponentNeedsUpdateStatus('feedback')); - expect(activeStateComponentNeedsUpdateStatus).toBe(false); + var activeStateComponentStatus = + tss.getActiveStateComponentStatusColor('content'); + expect(activeStateComponentStatus).toBe(NO_ASSETS_AVAILABLE_COLOR); + activeStateComponentStatus = + tss.getActiveStateComponentStatusColor('feedback'); + expect(activeStateComponentStatus).toBe(FEW_ASSETS_AVAILABLE_COLOR); - tss.refresh(); - tss.entityTranslation.markTranslationAsNeedingUpdate('feedback_3'); - activeStateComponentNeedsUpdateStatus = ( - tss.getActiveStateComponentNeedsUpdateStatus('feedback')); - expect(activeStateComponentNeedsUpdateStatus).toBe(true); - })); - - it('should return correct audio availability status color of a contentId ' + - 'of active state', () => { - ttams.activateVoiceoverMode(); - srvs.init('First', ess.getRecordedVoiceoversMemento('First')); - var activeStateContentIdStatusColor = tss - .getActiveStateContentIdStatusColor('content_0'); - expect(activeStateContentIdStatusColor).toBe(NO_ASSETS_AVAILABLE_COLOR); - activeStateContentIdStatusColor = tss - .getActiveStateContentIdStatusColor('feedback_2'); - expect(activeStateContentIdStatusColor).toBe(NO_ASSETS_AVAILABLE_COLOR); - activeStateContentIdStatusColor = tss - .getActiveStateContentIdStatusColor('feedback_3'); - expect(activeStateContentIdStatusColor).toBe(ALL_ASSETS_AVAILABLE_COLOR); - var value = srvs.displayed; - // To test changes after adding an audio translation to "content" - // in the first state. - value.addVoiceover('content_0', 'en', 'file.mp3', 1000, 1000); - srvs.saveDisplayedValue(); - ess.saveRecordedVoiceovers('First', value); - activeStateContentIdStatusColor = tss - .getActiveStateContentIdStatusColor('content_0'); - expect(activeStateContentIdStatusColor).toBe(ALL_ASSETS_AVAILABLE_COLOR); - srvs.init('Second', ess.getRecordedVoiceoversMemento('Second')); - activeStateContentIdStatusColor = tss - .getActiveStateContentIdStatusColor('content_5'); - expect(activeStateContentIdStatusColor).toBe(NO_ASSETS_AVAILABLE_COLOR); - activeStateContentIdStatusColor = tss - .getActiveStateContentIdStatusColor('feedback_7'); - expect(activeStateContentIdStatusColor).toBe(NO_ASSETS_AVAILABLE_COLOR); - srvs.init('Third', ess.getRecordedVoiceoversMemento('Third')); - activeStateContentIdStatusColor = tss - .getActiveStateContentIdStatusColor('content_8'); - expect(activeStateContentIdStatusColor).toBe(ALL_ASSETS_AVAILABLE_COLOR); - }); + tss.entityTranslation = EntityTranslation.createFromBackendDict({ + entity_id: 'exp_id', + entity_type: 'exploration', + entity_version: 5, + language_code: 'hi', + translations: { + content_0: { + content_format: 'html', + content_value: '

This is content.

', + needs_update: false, + }, + feedback_2: { + content_format: 'html', + content_value: '

This is first card.

', + needs_update: false, + }, + }, + }); + + activeStateComponentStatus = + tss.getActiveStateComponentStatusColor('content'); + expect(activeStateComponentStatus).toBe(ALL_ASSETS_AVAILABLE_COLOR); + activeStateComponentStatus = + tss.getActiveStateComponentStatusColor('feedback'); + expect(activeStateComponentStatus).toBe(FEW_ASSETS_AVAILABLE_COLOR); + } + ); + + it( + 'should correctly return whether active state component audio needs ' + + 'update', + () => { + ttams.activateVoiceoverMode(); + srvs.init('First', ess.getRecordedVoiceoversMemento('First')); + var activeStateComponentNeedsUpdateStatus = + tss.getActiveStateComponentNeedsUpdateStatus('feedback'); + // To check that initially the state component "feedback" does not + // contain audio that needs update. + expect(activeStateComponentNeedsUpdateStatus).toBe(false); + var value = srvs.displayed; + // To test changes after changing "needs update" status of an audio. + value.toggleNeedsUpdateAttribute('feedback_3', 'en'); + srvs.saveDisplayedValue(); + ess.saveRecordedVoiceovers('First', value); + activeStateComponentNeedsUpdateStatus = + tss.getActiveStateComponentNeedsUpdateStatus('feedback'); + // To check that the state component "feedback" contains audio that + // needs update. + expect(activeStateComponentNeedsUpdateStatus).toBe(true); + } + ); + + it( + 'should correctly return whether active state component translation ' + + 'needs update', + () => { + ttams.activateTranslationMode(); + tls.setActiveLanguageCode('hi'); + srvs.init('First', ess.getRecordedVoiceoversMemento('First')); - it('should return correct translation availability status color of a ' + - 'contentId of active state', () => { - ttams.activateTranslationMode(); - tls.setActiveLanguageCode('hi'); - tss.refresh(); + var activeStateComponentNeedsUpdateStatus = + tss.getActiveStateComponentNeedsUpdateStatus('feedback'); + expect(activeStateComponentNeedsUpdateStatus).toBe(false); - var activeStateContentIdStatusColor = ( - tss.getActiveStateContentIdStatusColor('content_0')); - expect(activeStateContentIdStatusColor).toBe(NO_ASSETS_AVAILABLE_COLOR); - activeStateContentIdStatusColor = ( - tss.getActiveStateContentIdStatusColor('feedback_2')); - expect(activeStateContentIdStatusColor).toBe( - NO_ASSETS_AVAILABLE_COLOR); - activeStateContentIdStatusColor = ( - tss.getActiveStateContentIdStatusColor('feedback_3')); - expect(activeStateContentIdStatusColor).toBe( - ALL_ASSETS_AVAILABLE_COLOR); - }); + tss.refresh(); + tss.entityTranslation.markTranslationAsNeedingUpdate('feedback_3'); + activeStateComponentNeedsUpdateStatus = + tss.getActiveStateComponentNeedsUpdateStatus('feedback'); + expect(activeStateComponentNeedsUpdateStatus).toBe(true); + } + ); + + it( + 'should return correct audio availability status color of a contentId ' + + 'of active state', + () => { + ttams.activateVoiceoverMode(); + srvs.init('First', ess.getRecordedVoiceoversMemento('First')); + var activeStateContentIdStatusColor = + tss.getActiveStateContentIdStatusColor('content_0'); + expect(activeStateContentIdStatusColor).toBe(NO_ASSETS_AVAILABLE_COLOR); + activeStateContentIdStatusColor = + tss.getActiveStateContentIdStatusColor('feedback_2'); + expect(activeStateContentIdStatusColor).toBe(NO_ASSETS_AVAILABLE_COLOR); + activeStateContentIdStatusColor = + tss.getActiveStateContentIdStatusColor('feedback_3'); + expect(activeStateContentIdStatusColor).toBe(ALL_ASSETS_AVAILABLE_COLOR); + var value = srvs.displayed; + // To test changes after adding an audio translation to "content" + // in the first state. + value.addVoiceover('content_0', 'en', 'file.mp3', 1000, 1000); + srvs.saveDisplayedValue(); + ess.saveRecordedVoiceovers('First', value); + activeStateContentIdStatusColor = + tss.getActiveStateContentIdStatusColor('content_0'); + expect(activeStateContentIdStatusColor).toBe(ALL_ASSETS_AVAILABLE_COLOR); + srvs.init('Second', ess.getRecordedVoiceoversMemento('Second')); + activeStateContentIdStatusColor = + tss.getActiveStateContentIdStatusColor('content_5'); + expect(activeStateContentIdStatusColor).toBe(NO_ASSETS_AVAILABLE_COLOR); + activeStateContentIdStatusColor = + tss.getActiveStateContentIdStatusColor('feedback_7'); + expect(activeStateContentIdStatusColor).toBe(NO_ASSETS_AVAILABLE_COLOR); + srvs.init('Third', ess.getRecordedVoiceoversMemento('Third')); + activeStateContentIdStatusColor = + tss.getActiveStateContentIdStatusColor('content_8'); + expect(activeStateContentIdStatusColor).toBe(ALL_ASSETS_AVAILABLE_COLOR); + } + ); + + it( + 'should return correct translation availability status color of a ' + + 'contentId of active state', + () => { + ttams.activateTranslationMode(); + tls.setActiveLanguageCode('hi'); + tss.refresh(); - it('should return correct needs update status of voice-over of active ' + - 'state contentId', () => { - ttams.activateVoiceoverMode(); - srvs.init('First', ess.getRecordedVoiceoversMemento('First')); - var activeStateContentIdNeedsUpdateStatus = tss - .getActiveStateContentIdNeedsUpdateStatus('feedback_3'); + var activeStateContentIdStatusColor = + tss.getActiveStateContentIdStatusColor('content_0'); + expect(activeStateContentIdStatusColor).toBe(NO_ASSETS_AVAILABLE_COLOR); + activeStateContentIdStatusColor = + tss.getActiveStateContentIdStatusColor('feedback_2'); + expect(activeStateContentIdStatusColor).toBe(NO_ASSETS_AVAILABLE_COLOR); + activeStateContentIdStatusColor = + tss.getActiveStateContentIdStatusColor('feedback_3'); + expect(activeStateContentIdStatusColor).toBe(ALL_ASSETS_AVAILABLE_COLOR); + } + ); + + it( + 'should return correct needs update status of voice-over of active ' + + 'state contentId', + () => { + ttams.activateVoiceoverMode(); + srvs.init('First', ess.getRecordedVoiceoversMemento('First')); + var activeStateContentIdNeedsUpdateStatus = + tss.getActiveStateContentIdNeedsUpdateStatus('feedback_3'); // To check that initially the state content id "feedback" does not // contain audio that needs update. - expect(activeStateContentIdNeedsUpdateStatus).toBe(false); - - var value = srvs.displayed; - value.toggleNeedsUpdateAttribute('feedback_3', 'en'); - srvs.saveDisplayedValue(); - activeStateContentIdNeedsUpdateStatus = ( - tss.getActiveStateContentIdNeedsUpdateStatus('feedback_3')); - // To check that the state content id "feedback" contains audio that - // needs update. - expect(activeStateContentIdNeedsUpdateStatus).toBe(true); - }); - - it('should return correct needs update status of translation of active ' + - 'state contentId', () => { - ttams.activateTranslationMode(); - tls.setActiveLanguageCode('hi'); - var activeStateContentIdNeedsUpdateStatus = ( - tss.getActiveStateContentIdNeedsUpdateStatus('feedback_3')); - expect(activeStateContentIdNeedsUpdateStatus).toBe(false); + expect(activeStateContentIdNeedsUpdateStatus).toBe(false); + + var value = srvs.displayed; + value.toggleNeedsUpdateAttribute('feedback_3', 'en'); + srvs.saveDisplayedValue(); + activeStateContentIdNeedsUpdateStatus = + tss.getActiveStateContentIdNeedsUpdateStatus('feedback_3'); + // To check that the state content id "feedback" contains audio that + // needs update. + expect(activeStateContentIdNeedsUpdateStatus).toBe(true); + } + ); + + it( + 'should return correct needs update status of translation of active ' + + 'state contentId', + () => { + ttams.activateTranslationMode(); + tls.setActiveLanguageCode('hi'); + var activeStateContentIdNeedsUpdateStatus = + tss.getActiveStateContentIdNeedsUpdateStatus('feedback_3'); + expect(activeStateContentIdNeedsUpdateStatus).toBe(false); - tss.refresh(); + tss.refresh(); - tss.entityTranslation.markTranslationAsNeedingUpdate('feedback_3'); - activeStateContentIdNeedsUpdateStatus = ( - tss.getActiveStateContentIdNeedsUpdateStatus('feedback_3')); - expect(activeStateContentIdNeedsUpdateStatus).toBe(true); - }); + tss.entityTranslation.markTranslationAsNeedingUpdate('feedback_3'); + activeStateContentIdNeedsUpdateStatus = + tss.getActiveStateContentIdNeedsUpdateStatus('feedback_3'); + expect(activeStateContentIdNeedsUpdateStatus).toBe(true); + } + ); }); diff --git a/core/templates/pages/exploration-editor-page/translation-tab/services/translation-status.service.ts b/core/templates/pages/exploration-editor-page/translation-tab/services/translation-status.service.ts index c8fa779788c1..d54801e6601f 100644 --- a/core/templates/pages/exploration-editor-page/translation-tab/services/translation-status.service.ts +++ b/core/templates/pages/exploration-editor-page/translation-tab/services/translation-status.service.ts @@ -17,27 +17,27 @@ * its components. */ -import { Injectable, OnInit } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; -import { TranslationLanguageService } from 'pages/exploration-editor-page/translation-tab/services/translation-language.service'; -import { TranslationTabActiveModeService } from 'pages/exploration-editor-page/translation-tab/services/translation-tab-active-mode.service'; -import { StateRecordedVoiceoversService } from 'components/state-editor/state-editor-properties-services/state-recorded-voiceovers.service'; +import {Injectable, OnInit} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; +import {TranslationLanguageService} from 'pages/exploration-editor-page/translation-tab/services/translation-language.service'; +import {TranslationTabActiveModeService} from 'pages/exploration-editor-page/translation-tab/services/translation-tab-active-mode.service'; +import {StateRecordedVoiceoversService} from 'components/state-editor/state-editor-properties-services/state-recorded-voiceovers.service'; import INTERACTION_SPECS from 'interactions/interaction_specs.json'; -import { AppConstants } from 'app.constants'; -import { RecordedVoiceovers } from 'domain/exploration/recorded-voiceovers.model'; -import { EntityTranslation } from 'domain/translation/EntityTranslationObjectFactory'; -import { EntityTranslationsService } from 'services/entity-translations.services'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { InteractionSpecsKey } from 'pages/interaction-specs.constants'; -import { TranslatedContent } from 'domain/exploration/TranslatedContentObjectFactory'; +import {AppConstants} from 'app.constants'; +import {RecordedVoiceovers} from 'domain/exploration/recorded-voiceovers.model'; +import {EntityTranslation} from 'domain/translation/EntityTranslationObjectFactory'; +import {EntityTranslationsService} from 'services/entity-translations.services'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {InteractionSpecsKey} from 'pages/interaction-specs.constants'; +import {TranslatedContent} from 'domain/exploration/TranslatedContentObjectFactory'; interface AvailabilityStatus { available: boolean; needsUpdate: boolean; } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class TranslationStatusService implements OnInit { AUDIO_NEEDS_UPDATE_MESSAGE: string[] = ['Audio needs update!']; @@ -57,7 +57,6 @@ export class TranslationStatusService implements OnInit { explorationVoiceoverContentNotAvailableCount!: number; entityTranslation!: EntityTranslation; - constructor( private explorationStatesService: ExplorationStatesService, private translationLanguageService: TranslationLanguageService, @@ -65,7 +64,7 @@ export class TranslationStatusService implements OnInit { private stateRecordedVoiceoversService: StateRecordedVoiceoversService, private entityTranslationsService: EntityTranslationsService, private stateEditorService: StateEditorService - ) { } + ) {} ngOnInit(): void { this.langCode = this.translationLanguageService.getActiveLanguageCode(); @@ -78,19 +77,21 @@ export class TranslationStatusService implements OnInit { } _getVoiceOverStatus( - recordedVoiceovers: RecordedVoiceovers, - contentId: string): AvailabilityStatus { + recordedVoiceovers: RecordedVoiceovers, + contentId: string + ): AvailabilityStatus { let availabilityStatus = { available: false, needsUpdate: false, }; - let availableLanguages = recordedVoiceovers.getLanguageCodes( - contentId); + let availableLanguages = recordedVoiceovers.getLanguageCodes(contentId); if (availableLanguages.indexOf(this.langCode) !== -1) { availabilityStatus.available = true; let audioTranslation = recordedVoiceovers.getVoiceover( - contentId, this.langCode); + contentId, + this.langCode + ); availabilityStatus.needsUpdate = audioTranslation.needsUpdate; } return availabilityStatus; @@ -101,10 +102,13 @@ export class TranslationStatusService implements OnInit { available: false, needsUpdate: false, }; - if (this.entityTranslation && this.entityTranslation.hasWrittenTranslation( - contentId)) { + if ( + this.entityTranslation && + this.entityTranslation.hasWrittenTranslation(contentId) + ) { let translatedContent = this.entityTranslation.getWrittenTranslation( - contentId) as TranslatedContent; + contentId + ) as TranslatedContent; if (translatedContent.translation !== '') { availabilityStatus.available = true; availabilityStatus.needsUpdate = translatedContent.needsUpdate; @@ -114,19 +118,22 @@ export class TranslationStatusService implements OnInit { } _getContentAvailabilityStatus( - stateName: string, contentId: string): AvailabilityStatus { + stateName: string, + contentId: string + ): AvailabilityStatus { if (this.translationTabActiveModeService.isTranslationModeActive()) { return this._getTranslationStatus(contentId); } else { this.langCode = this.translationLanguageService.getActiveLanguageCode(); - let recordedVoiceovers = ( - this.explorationStatesService.getRecordedVoiceoversMemento(stateName)); + let recordedVoiceovers = + this.explorationStatesService.getRecordedVoiceoversMemento(stateName); return this._getVoiceOverStatus(recordedVoiceovers, contentId); } } _getActiveStateContentAvailabilityStatus( - contentId: string): AvailabilityStatus { + contentId: string + ): AvailabilityStatus { if (this.translationTabActiveModeService.isTranslationModeActive()) { return this._getTranslationStatus(contentId); } else { @@ -144,104 +151,113 @@ export class TranslationStatusService implements OnInit { this.explorationVoiceoverContentNotAvailableCount = 0; if (this.explorationStatesService.isInitialized()) { - this.explorationStatesService.getStateNames().forEach( - (stateName) => { - let stateNeedsUpdate = false; - let noTranslationCount = 0; - let noVoiceoverCount = 0; - let recordedVoiceovers = ( - this.explorationStatesService - .getRecordedVoiceoversMemento(stateName)); - let allContentIds = recordedVoiceovers.getAllContentIds(); - let interactionId = ( - this.explorationStatesService.getInteractionIdMemento( - stateName)); - // This is used to prevent users from adding unwanted hints audio, as - // of now we do not delete interaction.hints when a user deletes - // interaction, so these hints audio are not counted in checking - // status of a state. - if (!interactionId || + this.explorationStatesService.getStateNames().forEach(stateName => { + let stateNeedsUpdate = false; + let noTranslationCount = 0; + let noVoiceoverCount = 0; + let recordedVoiceovers = + this.explorationStatesService.getRecordedVoiceoversMemento(stateName); + let allContentIds = recordedVoiceovers.getAllContentIds(); + let interactionId = + this.explorationStatesService.getInteractionIdMemento(stateName); + // This is used to prevent users from adding unwanted hints audio, as + // of now we do not delete interaction.hints when a user deletes + // interaction, so these hints audio are not counted in checking + // status of a state. + if ( + !interactionId || INTERACTION_SPECS[interactionId as InteractionSpecsKey].is_linear || - INTERACTION_SPECS[interactionId as InteractionSpecsKey].is_terminal) { - let contentIdToRemove = this._getContentIdListRelatedToComponent( - AppConstants.COMPONENT_NAME_HINT, - allContentIds); - allContentIds = allContentIds.filter(function(contentId) { - return !( - // Excluding default_outcome content status as default outcome's - // content is left empty so the translation or voiceover is not - // required. + INTERACTION_SPECS[interactionId as InteractionSpecsKey].is_terminal + ) { + let contentIdToRemove = this._getContentIdListRelatedToComponent( + AppConstants.COMPONENT_NAME_HINT, + allContentIds + ); + allContentIds = allContentIds.filter(function (contentId) { + return !( + // Excluding default_outcome content status as default outcome's + // content is left empty so the translation or voiceover is not + // required. + ( contentId.startsWith('default_outcome_') || contentIdToRemove.indexOf(contentId) > 0 - ); - }); - } + ) + ); + }); + } - this.explorationTranslationContentRequiredCount += ( - allContentIds.length); - - // Rule inputs do not need voiceovers. To have an accurate - // representation of the progress bar for voiceovers, we remove rule - // input content ids. - const ruleInputContentIds = this._getContentIdListRelatedToComponent( - AppConstants.COMPONENT_NAME_RULE_INPUT, allContentIds); - this.explorationVoiceoverContentRequiredCount += ( - allContentIds.length - ruleInputContentIds.length); - if (this.translationTabActiveModeService.isVoiceoverModeActive()) { - allContentIds = allContentIds.filter(function(contentId) { - return ruleInputContentIds.indexOf(contentId) < 0; - }); - } + this.explorationTranslationContentRequiredCount += allContentIds.length; + + // Rule inputs do not need voiceovers. To have an accurate + // representation of the progress bar for voiceovers, we remove rule + // input content ids. + const ruleInputContentIds = this._getContentIdListRelatedToComponent( + AppConstants.COMPONENT_NAME_RULE_INPUT, + allContentIds + ); + this.explorationVoiceoverContentRequiredCount += + allContentIds.length - ruleInputContentIds.length; + if (this.translationTabActiveModeService.isVoiceoverModeActive()) { + allContentIds = allContentIds.filter(function (contentId) { + return ruleInputContentIds.indexOf(contentId) < 0; + }); + } - allContentIds.forEach((contentId) => { - let availabilityStatus = this._getContentAvailabilityStatus( - stateName, contentId); - if (!availabilityStatus.available) { - noTranslationCount++; - if (contentId.indexOf( - AppConstants.COMPONENT_NAME_RULE_INPUT) !== 0) { - noVoiceoverCount++; - } + allContentIds.forEach(contentId => { + let availabilityStatus = this._getContentAvailabilityStatus( + stateName, + contentId + ); + if (!availabilityStatus.available) { + noTranslationCount++; + if ( + contentId.indexOf(AppConstants.COMPONENT_NAME_RULE_INPUT) !== 0 + ) { + noVoiceoverCount++; } - if (availabilityStatus.needsUpdate) { - if (this.translationTabActiveModeService - .isTranslationModeActive()) { - this.stateNeedsUpdateWarnings[stateName] = ( - this.TRANSLATION_NEEDS_UPDATE_MESSAGE); - stateNeedsUpdate = true; - } else { - this.stateNeedsUpdateWarnings[stateName] = ( - this.AUDIO_NEEDS_UPDATE_MESSAGE); - stateNeedsUpdate = true; - } + } + if (availabilityStatus.needsUpdate) { + if ( + this.translationTabActiveModeService.isTranslationModeActive() + ) { + this.stateNeedsUpdateWarnings[stateName] = + this.TRANSLATION_NEEDS_UPDATE_MESSAGE; + stateNeedsUpdate = true; + } else { + this.stateNeedsUpdateWarnings[stateName] = + this.AUDIO_NEEDS_UPDATE_MESSAGE; + stateNeedsUpdate = true; } - }); - this.explorationTranslationContentNotAvailableCount += ( - noTranslationCount); - this.explorationVoiceoverContentNotAvailableCount += ( - noVoiceoverCount); - if (noTranslationCount === 0 && !stateNeedsUpdate) { - this.stateWiseStatusColor[stateName] = ( - this.ALL_ASSETS_AVAILABLE_COLOR); - } else if ( - noTranslationCount === allContentIds.length && !stateNeedsUpdate) { - this.stateWiseStatusColor[stateName] = ( - this.NO_ASSETS_AVAILABLE_COLOR); - } else { - this.stateWiseStatusColor[stateName] = ( - this.FEW_ASSETS_AVAILABLE_COLOR); } }); + this.explorationTranslationContentNotAvailableCount += + noTranslationCount; + this.explorationVoiceoverContentNotAvailableCount += noVoiceoverCount; + if (noTranslationCount === 0 && !stateNeedsUpdate) { + this.stateWiseStatusColor[stateName] = + this.ALL_ASSETS_AVAILABLE_COLOR; + } else if ( + noTranslationCount === allContentIds.length && + !stateNeedsUpdate + ) { + this.stateWiseStatusColor[stateName] = this.NO_ASSETS_AVAILABLE_COLOR; + } else { + this.stateWiseStatusColor[stateName] = + this.FEW_ASSETS_AVAILABLE_COLOR; + } + }); } } _getContentIdListRelatedToComponent( - componentName: string, availableContentIds: string[]): string[] { + componentName: string, + availableContentIds: string[] + ): string[] { let contentIdList: string[] = []; if (availableContentIds.length > 0) { var searchKey = componentName + '_'; - availableContentIds.forEach(function(contentId) { + availableContentIds.forEach(function (contentId) { if (contentId.indexOf(searchKey) > -1) { contentIdList.push(contentId); } @@ -252,12 +268,14 @@ export class TranslationStatusService implements OnInit { _getActiveStateComponentStatus(componentName: string): string { let contentIdList = this._getContentIdListRelatedToComponent( - componentName, this._getAvailableContentIds()); + componentName, + this._getAvailableContentIds() + ); let availableAudioCount = 0; - contentIdList.forEach((contentId) => { - let availabilityStatus = this._getActiveStateContentAvailabilityStatus( - contentId); + contentIdList.forEach(contentId => { + let availabilityStatus = + this._getActiveStateContentAvailabilityStatus(contentId); if (availabilityStatus.available) { availableAudioCount++; } @@ -278,13 +296,15 @@ export class TranslationStatusService implements OnInit { _getActiveStateComponentNeedsUpdateStatus(componentName: string): boolean { let contentIdList = this._getContentIdListRelatedToComponent( - componentName, this._getAvailableContentIds()); + componentName, + this._getAvailableContentIds() + ); let contentId = null; if (contentIdList) { for (let index in contentIdList) { contentId = contentIdList[index]; - let availabilityStatus = this._getActiveStateContentAvailabilityStatus( - contentId); + let availabilityStatus = + this._getActiveStateContentAvailabilityStatus(contentId); if (availabilityStatus.needsUpdate) { return true; } @@ -294,8 +314,8 @@ export class TranslationStatusService implements OnInit { } _getActiveStateContentIdStatusColor(contentId: string): string { - let availabilityStatus = this._getActiveStateContentAvailabilityStatus( - contentId); + let availabilityStatus = + this._getActiveStateContentAvailabilityStatus(contentId); if (availabilityStatus.available) { return this.ALL_ASSETS_AVAILABLE_COLOR; } else { @@ -304,8 +324,8 @@ export class TranslationStatusService implements OnInit { } _getActiveStateContentIdNeedsUpdateStatus(contentId: string): boolean { - let availabilityStatus = this._getActiveStateContentAvailabilityStatus( - contentId); + let availabilityStatus = + this._getActiveStateContentAvailabilityStatus(contentId); return availabilityStatus.needsUpdate; } @@ -327,10 +347,10 @@ export class TranslationStatusService implements OnInit { refresh(): void { this.langCode = this.translationLanguageService.getActiveLanguageCode(); - this.entityTranslation = ( + this.entityTranslation = this.entityTranslationsService.languageCodeToEntityTranslations[ - this.langCode] - ); + this.langCode + ]; this._computeAllStatesStatus(); this.stateEditorService.onRefreshStateTranslation.emit(); } @@ -368,5 +388,9 @@ export class TranslationStatusService implements OnInit { } } -angular.module('oppia').factory('TranslationStatusService', - downgradeInjectable(TranslationStatusService)); +angular + .module('oppia') + .factory( + 'TranslationStatusService', + downgradeInjectable(TranslationStatusService) + ); diff --git a/core/templates/pages/exploration-editor-page/translation-tab/services/translation-tab-active-content-id.service.spec.ts b/core/templates/pages/exploration-editor-page/translation-tab/services/translation-tab-active-content-id.service.spec.ts index c3199e1e8792..228cfaec5da3 100644 --- a/core/templates/pages/exploration-editor-page/translation-tab/services/translation-tab-active-content-id.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/translation-tab/services/translation-tab-active-content-id.service.spec.ts @@ -16,26 +16,28 @@ * @fileoverview Unit test for the Translation tab active content id service. */ -import { EventEmitter } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; -import { TranslationTabActiveContentIdService } from 'pages/exploration-editor-page/translation-tab/services/translation-tab-active-content-id.service'; -import { StateRecordedVoiceoversService } from 'components/state-editor/state-editor-properties-services/state-recorded-voiceovers.service'; +import {EventEmitter} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; +import {TranslationTabActiveContentIdService} from 'pages/exploration-editor-page/translation-tab/services/translation-tab-active-content-id.service'; +import {StateRecordedVoiceoversService} from 'components/state-editor/state-editor-properties-services/state-recorded-voiceovers.service'; describe('Translation tab active content id service', () => { let ttacis: TranslationTabActiveContentIdService; beforeEach(() => { - TestBed.configureTestingModule({ providers: [{ - provide: StateRecordedVoiceoversService, - useValue: { - displayed: { - getAllContentIds: () => { - return ['content', 'feedback_1']; - } - } - } - } - ] + TestBed.configureTestingModule({ + providers: [ + { + provide: StateRecordedVoiceoversService, + useValue: { + displayed: { + getAllContentIds: () => { + return ['content', 'feedback_1']; + }, + }, + }, + }, + ], }); ttacis = TestBed.inject(TranslationTabActiveContentIdService); @@ -50,8 +52,7 @@ describe('Translation tab active content id service', () => { it('should throw error on setting invalid content id', () => { expect(() => { ttacis.setActiveContent('feedback_2', 'html'); - }).toThrowError( - 'Invalid active content id: feedback_2'); + }).toThrowError('Invalid active content id: feedback_2'); }); it('should return data format correctly', () => { @@ -63,6 +64,7 @@ describe('Translation tab active content id service', () => { it('should emit data format', () => { let mockquestionSessionEventEmitter = new EventEmitter(); expect(ttacis.onActiveContentIdChanged).toEqual( - mockquestionSessionEventEmitter); + mockquestionSessionEventEmitter + ); }); }); diff --git a/core/templates/pages/exploration-editor-page/translation-tab/services/translation-tab-active-content-id.service.ts b/core/templates/pages/exploration-editor-page/translation-tab/services/translation-tab-active-content-id.service.ts index 0a432ce6d691..57ea1530ff99 100644 --- a/core/templates/pages/exploration-editor-page/translation-tab/services/translation-tab-active-content-id.service.ts +++ b/core/templates/pages/exploration-editor-page/translation-tab/services/translation-tab-active-content-id.service.ts @@ -16,19 +16,19 @@ * @fileoverview Service to get and set active content id in translation tab. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { EventEmitter } from '@angular/core'; -import { Injectable } from '@angular/core'; - -import { StateRecordedVoiceoversService } from 'components/state-editor/state-editor-properties-services/state-recorded-voiceovers.service'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {EventEmitter} from '@angular/core'; +import {Injectable} from '@angular/core'; +import {StateRecordedVoiceoversService} from 'components/state-editor/state-editor-properties-services/state-recorded-voiceovers.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class TranslationTabActiveContentIdService { constructor( - private _stateRecordedVoiceoversService: StateRecordedVoiceoversService) {} + private _stateRecordedVoiceoversService: StateRecordedVoiceoversService + ) {} // 'activeContentId' and 'activeDataFormat' will be 'null' if active content // has not been set. @@ -45,8 +45,8 @@ export class TranslationTabActiveContentIdService { } setActiveContent(contentId: string, dataFormat: string): void { - const displayStateRecordedVoiceovers = ( - this._stateRecordedVoiceoversService.displayed); + const displayStateRecordedVoiceovers = + this._stateRecordedVoiceoversService.displayed; let allContentIds = displayStateRecordedVoiceovers.getAllContentIds(); if (allContentIds.indexOf(contentId) === -1) { throw new Error('Invalid active content id: ' + contentId); @@ -61,6 +61,9 @@ export class TranslationTabActiveContentIdService { } } -angular.module('oppia').factory( - 'TranslationTabActiveContentIdService', downgradeInjectable( - TranslationTabActiveContentIdService)); +angular + .module('oppia') + .factory( + 'TranslationTabActiveContentIdService', + downgradeInjectable(TranslationTabActiveContentIdService) + ); diff --git a/core/templates/pages/exploration-editor-page/translation-tab/services/translation-tab-active-mode.service.spec.ts b/core/templates/pages/exploration-editor-page/translation-tab/services/translation-tab-active-mode.service.spec.ts index a459705afbdc..4e88b38ebef1 100644 --- a/core/templates/pages/exploration-editor-page/translation-tab/services/translation-tab-active-mode.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/translation-tab/services/translation-tab-active-mode.service.spec.ts @@ -16,51 +16,56 @@ * @fileoverview Unit test for the Translation tab active mode service. */ -import { TestBed } from '@angular/core/testing'; -import { TranslationTabActiveModeService } from 'pages/exploration-editor-page/translation-tab/services/translation-tab-active-mode.service'; +import {TestBed} from '@angular/core/testing'; +import {TranslationTabActiveModeService} from 'pages/exploration-editor-page/translation-tab/services/translation-tab-active-mode.service'; describe('Translation tab active mode service', () => { let translationTabActiveModeService: TranslationTabActiveModeService; - beforeEach(()=> { + beforeEach(() => { translationTabActiveModeService = TestBed.get( - TranslationTabActiveModeService); + TranslationTabActiveModeService + ); }); it('should correctly activate translation mode', () => { expect( - translationTabActiveModeService.isTranslationModeActive()).toBeFalsy(); + translationTabActiveModeService.isTranslationModeActive() + ).toBeFalsy(); translationTabActiveModeService.activateTranslationMode(); expect( - translationTabActiveModeService.isTranslationModeActive()).toBeTruthy(); + translationTabActiveModeService.isTranslationModeActive() + ).toBeTruthy(); }); it('should correctly activate voiceover mode', () => { - expect( - translationTabActiveModeService.isVoiceoverModeActive()).toBeFalsy(); + expect(translationTabActiveModeService.isVoiceoverModeActive()).toBeFalsy(); translationTabActiveModeService.activateVoiceoverMode(); expect( - translationTabActiveModeService.isVoiceoverModeActive()).toBeTruthy(); + translationTabActiveModeService.isVoiceoverModeActive() + ).toBeTruthy(); }); it('should correctly report the active mode', () => { + expect(translationTabActiveModeService.isVoiceoverModeActive()).toBeFalsy(); expect( - translationTabActiveModeService.isVoiceoverModeActive()).toBeFalsy(); - expect( - translationTabActiveModeService.isTranslationModeActive()).toBeFalsy(); + translationTabActiveModeService.isTranslationModeActive() + ).toBeFalsy(); translationTabActiveModeService.activateVoiceoverMode(); expect( - translationTabActiveModeService.isVoiceoverModeActive()).toBeTruthy(); + translationTabActiveModeService.isVoiceoverModeActive() + ).toBeTruthy(); expect( - translationTabActiveModeService.isTranslationModeActive()).toBeFalsy(); + translationTabActiveModeService.isTranslationModeActive() + ).toBeFalsy(); translationTabActiveModeService.activateTranslationMode(); + expect(translationTabActiveModeService.isVoiceoverModeActive()).toBeFalsy(); expect( - translationTabActiveModeService.isVoiceoverModeActive()).toBeFalsy(); - expect( - translationTabActiveModeService.isTranslationModeActive()).toBeTruthy(); + translationTabActiveModeService.isTranslationModeActive() + ).toBeTruthy(); }); }); diff --git a/core/templates/pages/exploration-editor-page/translation-tab/services/translation-tab-active-mode.service.ts b/core/templates/pages/exploration-editor-page/translation-tab/services/translation-tab-active-mode.service.ts index 804b740b0967..31e03cd3b035 100644 --- a/core/templates/pages/exploration-editor-page/translation-tab/services/translation-tab-active-mode.service.ts +++ b/core/templates/pages/exploration-editor-page/translation-tab/services/translation-tab-active-mode.service.ts @@ -17,14 +17,13 @@ * tab. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; -import { ExplorationEditorPageConstants } from - 'pages/exploration-editor-page/exploration-editor-page.constants'; +import {ExplorationEditorPageConstants} from 'pages/exploration-editor-page/exploration-editor-page.constants'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class TranslationTabActiveModeService { private activeMode!: string; @@ -46,6 +45,9 @@ export class TranslationTabActiveModeService { } } -angular.module('oppia').factory( - 'TranslationTabActiveModeService', - downgradeInjectable(TranslationTabActiveModeService)); +angular + .module('oppia') + .factory( + 'TranslationTabActiveModeService', + downgradeInjectable(TranslationTabActiveModeService) + ); diff --git a/core/templates/pages/exploration-editor-page/translation-tab/services/translation-topic.service.spec.ts b/core/templates/pages/exploration-editor-page/translation-tab/services/translation-topic.service.spec.ts index 34c1945bff3f..41cb1e514b6f 100644 --- a/core/templates/pages/exploration-editor-page/translation-tab/services/translation-topic.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/translation-tab/services/translation-topic.service.spec.ts @@ -16,17 +16,18 @@ * @fileoverview Unit test for the Translation topic service. */ -import { ContributionOpportunitiesService } from +import { + ContributionOpportunitiesService, // eslint-disable-next-line max-len - 'pages/contributor-dashboard-page/services/contribution-opportunities.service'; -import { EventEmitter } from '@angular/core'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { LoggerService } from 'services/contextual/logger.service'; -import { TranslationTopicService } from +} from 'pages/contributor-dashboard-page/services/contribution-opportunities.service'; +import {EventEmitter} from '@angular/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {LoggerService} from 'services/contextual/logger.service'; +import { + TranslationTopicService, // eslint-disable-next-line max-len - 'pages/exploration-editor-page/translation-tab/services/translation-topic.service'; -import { fakeAsync, TestBed } from '@angular/core/testing'; - +} from 'pages/exploration-editor-page/translation-tab/services/translation-topic.service'; +import {fakeAsync, TestBed} from '@angular/core/testing'; describe('Translation topic service', () => { let $flushPendingTasks: () => void; @@ -36,28 +37,32 @@ describe('Translation topic service', () => { let translationTopicService: TranslationTopicService; let contributionOpportunitiesService: ContributionOpportunitiesService; - beforeEach(angular.mock.inject(function($injector) { - $flushPendingTasks = $injector.get('$flushPendingTasks'); - $q = $injector.get('$q'); + beforeEach( + angular.mock.inject(function ($injector) { + $flushPendingTasks = $injector.get('$flushPendingTasks'); + $q = $injector.get('$q'); - TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] - }); + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); - loggerService = TestBed.get(LoggerService); - translationTopicService = TestBed.get(TranslationTopicService); - contributionOpportunitiesService = TestBed.get( - ContributionOpportunitiesService); - spyOn(contributionOpportunitiesService, 'getTranslatableTopicNamesAsync') - .and.returnValue($q.resolve(['Topic 1', 'Topic 2'])); - })); + loggerService = TestBed.get(LoggerService); + translationTopicService = TestBed.get(TranslationTopicService); + contributionOpportunitiesService = TestBed.get( + ContributionOpportunitiesService + ); + spyOn( + contributionOpportunitiesService, + 'getTranslatableTopicNamesAsync' + ).and.returnValue($q.resolve(['Topic 1', 'Topic 2'])); + }) + ); describe('Translation topic service', () => { - it('should correctly set and get topic names', fakeAsync(async() => { + it('should correctly set and get topic names', fakeAsync(async () => { translationTopicService.setActiveTopicName('Topic 1'); $flushPendingTasks(); - expect(translationTopicService.getActiveTopicName()).toBe( - 'Topic 1'); + expect(translationTopicService.getActiveTopicName()).toBe('Topic 1'); })); it('should not allow invalid topic names to be set', () => { @@ -65,8 +70,7 @@ describe('Translation topic service', () => { translationTopicService.setActiveTopicName('Topic 3'); $flushPendingTasks(); - expect( - translationTopicService.getActiveTopicName()).toBeUndefined(); + expect(translationTopicService.getActiveTopicName()).toBeUndefined(); expect(logErrorSpy).toHaveBeenCalledWith( 'Invalid active topic name: Topic 3' ); @@ -78,14 +82,14 @@ describe('Translation topic service', () => { // @ts-ignore translationTopicService.setActiveTopicName(null); $flushPendingTasks(); - expect( - translationTopicService.getActiveTopicName()).toBeUndefined(); + expect(translationTopicService.getActiveTopicName()).toBeUndefined(); }); it('should emit the new topic name', () => { let newTopicEventEmitter = new EventEmitter(); expect(translationTopicService.onActiveTopicChanged).toEqual( - newTopicEventEmitter); + newTopicEventEmitter + ); }); }); }); diff --git a/core/templates/pages/exploration-editor-page/translation-tab/services/translation-topic.service.ts b/core/templates/pages/exploration-editor-page/translation-tab/services/translation-topic.service.ts index 2347cbd641b4..c799c0b404bf 100644 --- a/core/templates/pages/exploration-editor-page/translation-tab/services/translation-topic.service.ts +++ b/core/templates/pages/exploration-editor-page/translation-tab/services/translation-topic.service.ts @@ -17,18 +17,17 @@ * in the translation tab is currently active. */ -import { EventEmitter, Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {EventEmitter, Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; - -import { ContributionOpportunitiesService } from +import { + ContributionOpportunitiesService, // eslint-disable-next-line max-len - 'pages/contributor-dashboard-page/services/contribution-opportunities.service'; -import { LoggerService } from 'services/contextual/logger.service'; - +} from 'pages/contributor-dashboard-page/services/contribution-opportunities.service'; +import {LoggerService} from 'services/contextual/logger.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class TranslationTopicService { // This property is initialized using async methods @@ -39,24 +38,29 @@ export class TranslationTopicService { constructor( private ContributionOpportunitiesService: ContributionOpportunitiesService, - private loggerService: LoggerService) {} + private loggerService: LoggerService + ) {} getActiveTopicName(): string { return this.activeTopicName; } setActiveTopicName(newActiveTopicName: string): void { - this.ContributionOpportunitiesService.getTranslatableTopicNamesAsync() - .then((data) => { - if (newActiveTopicName !== 'All' && - data.indexOf(newActiveTopicName) < 0) { + this.ContributionOpportunitiesService.getTranslatableTopicNamesAsync().then( + data => { + if ( + newActiveTopicName !== 'All' && + data.indexOf(newActiveTopicName) < 0 + ) { this.loggerService.error( - 'Invalid active topic name: ' + newActiveTopicName); + 'Invalid active topic name: ' + newActiveTopicName + ); return; } this.activeTopicName = newActiveTopicName; this._activeTopicChangedEventEmitter.emit(); - }); + } + ); } get onActiveTopicChanged(): EventEmitter { @@ -64,6 +68,9 @@ export class TranslationTopicService { } } -angular.module('oppia').service( - 'TranslationTopicService', - downgradeInjectable(TranslationTopicService)); +angular + .module('oppia') + .service( + 'TranslationTopicService', + downgradeInjectable(TranslationTopicService) + ); diff --git a/core/templates/pages/exploration-editor-page/translation-tab/services/voiceover-recording.service.ts b/core/templates/pages/exploration-editor-page/translation-tab/services/voiceover-recording.service.ts index 54fa4681e781..a9fd1f7b2bf7 100644 --- a/core/templates/pages/exploration-editor-page/translation-tab/services/voiceover-recording.service.ts +++ b/core/templates/pages/exploration-editor-page/translation-tab/services/voiceover-recording.service.ts @@ -16,9 +16,9 @@ * @fileoverview Service for handling microphone data and mp3 audio processing. */ -import { EventEmitter, Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { LoggerService } from 'services/contextual/logger.service'; +import {EventEmitter, Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {LoggerService} from 'services/contextual/logger.service'; declare global { interface Window { @@ -27,7 +27,7 @@ declare global { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class VoiceoverRecordingService { // These properties are initialized using init method and we need to do @@ -43,9 +43,7 @@ export class VoiceoverRecordingService { mp3Worker: Worker | null = null; defer: EventEmitter = new EventEmitter(); - constructor( - private loggerService: LoggerService - ) { } + constructor(private loggerService: LoggerService) {} _stopRecord(): void { if (this.microphone && this.processor && this.mp3Worker) { @@ -56,7 +54,7 @@ export class VoiceoverRecordingService { // Issue command to retrieve converted audio. this.mp3Worker.postMessage({cmd: 'finish'}); // Stop microphone stream. - this.microphoneStream.getTracks().forEach(function(track) { + this.microphoneStream.getTracks().forEach(function (track) { track.stop(); }); @@ -73,12 +71,12 @@ export class VoiceoverRecordingService { } status(): { - isAvailable: boolean; - isRecording: boolean; - } { + isAvailable: boolean; + isRecording: boolean; + } { return { isAvailable: this.isAvailable, - isRecording: this.isRecording + isRecording: this.isRecording, }; } @@ -90,17 +88,21 @@ export class VoiceoverRecordingService { let navigator = this._startMicrophoneAsync(); - navigator.then((stream) => { - this.isRecording = true; - // Set microphone stream will be used for stopping track - // stream in another function. - this.microphoneStream = stream; - this._processMicAudio(stream); - }, () => { - this.loggerService.warn( - 'Microphone was not started because ofuser denied permission.'); - this.isRecording = false; - }); + navigator.then( + stream => { + this.isRecording = true; + // Set microphone stream will be used for stopping track + // stream in another function. + this.microphoneStream = stream; + this._processMicAudio(stream); + }, + () => { + this.loggerService.warn( + 'Microphone was not started because ofuser denied permission.' + ); + this.isRecording = false; + } + ); return navigator; } @@ -125,12 +127,13 @@ export class VoiceoverRecordingService { return; } if (this.mp3Worker === null) { - let lameWorkerFileUrl = '/third_party/static/lamejs-1.2.0/' + - 'worker-example/worker-realtime.js'; + let lameWorkerFileUrl = + '/third_party/static/lamejs-1.2.0/' + + 'worker-example/worker-realtime.js'; // Config the mp3 encoding worker. let config = {sampleRate: 44100, bitRate: 128}; this.mp3Worker = new Worker(lameWorkerFileUrl); - this.mp3Worker.onmessage = (e) => { + this.mp3Worker.onmessage = e => { // Async data flow. this.defer.emit(e.data.buf); return; @@ -141,12 +144,11 @@ export class VoiceoverRecordingService { } // Convert directly from mic input to mp3. - _onAudioProcess( - event: { - inputBuffer: { - getChannelData: (value: number) => Transferable[]; - }; - }): void { + _onAudioProcess(event: { + inputBuffer: { + getChannelData: (value: number) => Transferable[]; + }; + }): void { let array = event.inputBuffer.getChannelData(0); this._postMessage(array); } @@ -172,8 +174,8 @@ export class VoiceoverRecordingService { _initRecorder(): void { // Browser agnostic AudioContext API check. - this.audioContextAvailable = window.AudioContext || - (window as Window).webkitAudioContext; + this.audioContextAvailable = + window.AudioContext || (window as Window).webkitAudioContext; if (this.audioContextAvailable) { // Promise required because angular is async with worker. @@ -206,5 +208,9 @@ export class VoiceoverRecordingService { } } -angular.module('oppia').factory( - 'VoiceoverRecordingService', downgradeInjectable(VoiceoverRecordingService)); +angular + .module('oppia') + .factory( + 'VoiceoverRecordingService', + downgradeInjectable(VoiceoverRecordingService) + ); diff --git a/core/templates/pages/exploration-editor-page/translation-tab/state-translation-editor/state-translation-editor.component.spec.ts b/core/templates/pages/exploration-editor-page/translation-tab/state-translation-editor/state-translation-editor.component.spec.ts index 3f8a19da57dc..40a40b5aaa48 100644 --- a/core/templates/pages/exploration-editor-page/translation-tab/state-translation-editor/state-translation-editor.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/translation-tab/state-translation-editor/state-translation-editor.component.spec.ts @@ -16,25 +16,24 @@ * @fileoverview Unit tests for stateTranslationEditor. */ -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { State, StateObjectFactory } from 'domain/state/StateObjectFactory'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; -import { EditabilityService } from 'services/editability.service'; -import { ExternalSaveService } from 'services/external-save.service'; -import { TranslationLanguageService } from '../services/translation-language.service'; -import { TranslationTabActiveContentIdService } from '../services/translation-tab-active-content-id.service'; -import { StateTranslationEditorComponent } from './state-translation-editor.component'; -import { MarkAudioAsNeedingUpdateModalComponent } from 'components/forms/forms-templates/mark-audio-as-needing-update-modal.component'; -import { RecordedVoiceovers } from 'domain/exploration/recorded-voiceovers.model'; -import { ChangeListService } from 'pages/exploration-editor-page/services/change-list.service'; -import { TranslatedContent } from 'domain/exploration/TranslatedContentObjectFactory'; -import { EntityTranslationsService } from 'services/entity-translations.services'; -import { EntityTranslation } from 'domain/translation/EntityTranslationObjectFactory'; -import { TranslationStatusService } from '../services/translation-status.service'; - +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {State, StateObjectFactory} from 'domain/state/StateObjectFactory'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; +import {EditabilityService} from 'services/editability.service'; +import {ExternalSaveService} from 'services/external-save.service'; +import {TranslationLanguageService} from '../services/translation-language.service'; +import {TranslationTabActiveContentIdService} from '../services/translation-tab-active-content-id.service'; +import {StateTranslationEditorComponent} from './state-translation-editor.component'; +import {MarkAudioAsNeedingUpdateModalComponent} from 'components/forms/forms-templates/mark-audio-as-needing-update-modal.component'; +import {RecordedVoiceovers} from 'domain/exploration/recorded-voiceovers.model'; +import {ChangeListService} from 'pages/exploration-editor-page/services/change-list.service'; +import {TranslatedContent} from 'domain/exploration/TranslatedContentObjectFactory'; +import {EntityTranslationsService} from 'services/entity-translations.services'; +import {EntityTranslation} from 'domain/translation/EntityTranslationObjectFactory'; +import {TranslationStatusService} from '../services/translation-status.service'; class MockNgbModalRef { result: Promise = Promise.resolve(); @@ -57,8 +56,7 @@ describe('State Translation Editor Component', () => { let stateObjectFactory: StateObjectFactory; let translationLanguageService: TranslationLanguageService; let externalSaveService: ExternalSaveService; - let translationTabActiveContentIdService: - TranslationTabActiveContentIdService; + let translationTabActiveContentIdService: TranslationTabActiveContentIdService; let translationStatusService: TranslationStatusService; let state: State; @@ -70,15 +68,15 @@ describe('State Translation Editor Component', () => { imports: [HttpClientTestingModule], declarations: [ StateTranslationEditorComponent, - MarkAudioAsNeedingUpdateModalComponent + MarkAudioAsNeedingUpdateModalComponent, ], providers: [ { provide: NgbModal, - useClass: MockNgbModal - } + useClass: MockNgbModal, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -92,28 +90,40 @@ describe('State Translation Editor Component', () => { translationLanguageService = TestBed.inject(TranslationLanguageService); externalSaveService = TestBed.inject(ExternalSaveService); translationTabActiveContentIdService = TestBed.inject( - TranslationTabActiveContentIdService); + TranslationTabActiveContentIdService + ); editabilityService = TestBed.inject(EditabilityService); explorationStatesService = TestBed.inject(ExplorationStatesService); stateObjectFactory = TestBed.inject(StateObjectFactory); translationStatusService = TestBed.inject(TranslationStatusService); state = stateObjectFactory.createDefaultState( - '', 'content1', 'default_outcome'); + '', + 'content1', + 'default_outcome' + ); state.content.html = 'This is a html text1'; spyOn(explorationStatesService, 'getState').and.returnValue(state); - spyOn(translationTabActiveContentIdService, 'getActiveContentId').and - .returnValue('content1'); + spyOn( + translationTabActiveContentIdService, + 'getActiveContentId' + ).and.returnValue('content1'); // SpyOn(editabilityService, 'isEditable').and.returnValue(true); - spyOn(translationLanguageService, 'getActiveLanguageDirection') - .and.returnValue('left'); - spyOnProperty(translationLanguageService, 'onActiveLanguageChanged') - .and.returnValue(mockActiveLanguageChangedEventEmitter); - spyOn(translationLanguageService, 'getActiveLanguageCode') - .and.returnValue('hi'); + spyOn( + translationLanguageService, + 'getActiveLanguageDirection' + ).and.returnValue('left'); + spyOnProperty( + translationLanguageService, + 'onActiveLanguageChanged' + ).and.returnValue(mockActiveLanguageChangedEventEmitter); + spyOn(translationLanguageService, 'getActiveLanguageCode').and.returnValue( + 'hi' + ); spyOnProperty( - translationTabActiveContentIdService, 'onActiveContentIdChanged' + translationTabActiveContentIdService, + 'onActiveContentIdChanged' ).and.returnValue(mockActiveLanguageIdChangedEventEmitter); entityTranslationsService.languageCodeToEntityTranslations = { hi: EntityTranslation.createFromBackendDict({ @@ -125,20 +135,20 @@ describe('State Translation Editor Component', () => { content1: { content_value: 'This is a html text1 in hindi', needs_update: false, - content_format: 'html' + content_format: 'html', }, content2: { content_value: 'This is a html text2 in hindi', needs_update: false, - content_format: 'html' + content_format: 'html', }, content3: { content_value: 'This is a html text3 in hindi', needs_update: false, - content_format: 'html' + content_format: 'html', }, - } - }) + }, + }), }; component.ngOnInit(); @@ -159,19 +169,21 @@ describe('State Translation Editor Component', () => { filename: 'filename1.mp3', file_size_bytes: 100, needs_update: false, - duration_secs: 10 - } - } - } + duration_secs: 10, + }, + }, + }, }); spyOn(ngbModal, 'open').and.callThrough(); component.onSaveTranslationButtonClicked(); expect(ngbModal.open).toHaveBeenCalledWith( - MarkAudioAsNeedingUpdateModalComponent, { - backdrop: 'static' - }); + MarkAudioAsNeedingUpdateModalComponent, + { + backdrop: 'static', + } + ); }); it('should not open the modal if voiceover already needs update', () => { @@ -182,10 +194,10 @@ describe('State Translation Editor Component', () => { filename: 'filename1.mp3', file_size_bytes: 100, needs_update: true, - duration_secs: 10 - } - } - } + duration_secs: 10, + }, + }, + }, }); spyOn(ngbModal, 'open'); @@ -202,10 +214,10 @@ describe('State Translation Editor Component', () => { filename: 'filename1.mp3', file_size_bytes: 100, needs_update: false, - duration_secs: 10 - } - } - } + duration_secs: 10, + }, + }, + }, }); const mockNgbModalRef = new MockNgbModalRef(); mockNgbModalRef.result = Promise.reject(); @@ -214,7 +226,9 @@ describe('State Translation Editor Component', () => { component.onSaveTranslationButtonClicked(); expect(ngbModal.open).toHaveBeenCalledWith( - MarkAudioAsNeedingUpdateModalComponent, {backdrop: 'static'}); + MarkAudioAsNeedingUpdateModalComponent, + {backdrop: 'static'} + ); }); it('should add editTranslation changes to draft change list', () => { @@ -223,9 +237,7 @@ describe('State Translation Editor Component', () => { expect(changeListService.editTranslation).toHaveBeenCalled(); }); - it('should update the translation with edited translation', () => { - - }); + it('should update the translation with edited translation', () => {}); it('should refresh the translation status', () => { spyOn(translationStatusService, 'refresh'); @@ -251,7 +263,8 @@ describe('State Translation Editor Component', () => { component.dataFormat = 'html'; component.openTranslationEditor(); expect(component.activeWrittenTranslation).toEqual( - TranslatedContent.createNew('html')); + TranslatedContent.createNew('html') + ); }); }); @@ -306,18 +319,22 @@ describe('State Translation Editor Component', () => { expect(component.initEditor).toHaveBeenCalled(); }); - it('should init editor and update data ' + - 'format on active content id change', () => { - component.dataFormat = 'html'; - spyOn(component, 'initEditor'); - - translationTabActiveContentIdService - .onActiveContentIdChanged.emit('unicode'); - fixture.detectChanges(); - - expect(component.dataFormat).toEqual('unicode'); - expect(component.initEditor).toHaveBeenCalled(); - }); + it( + 'should init editor and update data ' + + 'format on active content id change', + () => { + component.dataFormat = 'html'; + spyOn(component, 'initEditor'); + + translationTabActiveContentIdService.onActiveContentIdChanged.emit( + 'unicode' + ); + fixture.detectChanges(); + + expect(component.dataFormat).toEqual('unicode'); + expect(component.initEditor).toHaveBeenCalled(); + } + ); it('should save translation if translation editor is open', () => { component.translationEditorIsOpen = true; diff --git a/core/templates/pages/exploration-editor-page/translation-tab/state-translation-editor/state-translation-editor.component.ts b/core/templates/pages/exploration-editor-page/translation-tab/state-translation-editor/state-translation-editor.component.ts index 86a26b760c9f..0ac698ca4ef2 100644 --- a/core/templates/pages/exploration-editor-page/translation-tab/state-translation-editor/state-translation-editor.component.ts +++ b/core/templates/pages/exploration-editor-page/translation-tab/state-translation-editor/state-translation-editor.component.ts @@ -16,22 +16,25 @@ * @fileoverview Component for the state translation editor. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { Subscription } from 'rxjs'; -import { MarkAudioAsNeedingUpdateModalComponent } from 'components/forms/forms-templates/mark-audio-as-needing-update-modal.component'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; -import { GraphDataService } from 'pages/exploration-editor-page/services/graph-data.service'; -import { EditabilityService } from 'services/editability.service'; -import { ExternalSaveService } from 'services/external-save.service'; -import { TranslationLanguageService } from '../services/translation-language.service'; -import { TranslationStatusService } from '../services/translation-status.service'; -import { TranslationTabActiveContentIdService } from '../services/translation-tab-active-content-id.service'; -import { EntityTranslationsService } from 'services/entity-translations.services'; -import { DataFormatToDefaultValuesKey, TranslatedContent } from 'domain/exploration/TranslatedContentObjectFactory'; -import { ChangeListService } from 'pages/exploration-editor-page/services/change-list.service'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {Subscription} from 'rxjs'; +import {MarkAudioAsNeedingUpdateModalComponent} from 'components/forms/forms-templates/mark-audio-as-needing-update-modal.component'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; +import {GraphDataService} from 'pages/exploration-editor-page/services/graph-data.service'; +import {EditabilityService} from 'services/editability.service'; +import {ExternalSaveService} from 'services/external-save.service'; +import {TranslationLanguageService} from '../services/translation-language.service'; +import {TranslationStatusService} from '../services/translation-status.service'; +import {TranslationTabActiveContentIdService} from '../services/translation-tab-active-content-id.service'; +import {EntityTranslationsService} from 'services/entity-translations.services'; +import { + DataFormatToDefaultValuesKey, + TranslatedContent, +} from 'domain/exploration/TranslatedContentObjectFactory'; +import {ChangeListService} from 'pages/exploration-editor-page/services/change-list.service'; interface HTMLSchema { type: string; @@ -43,16 +46,15 @@ interface HTMLSchema { interface ListSchema { type: 'list'; - items: { type: string }; - validators: { id: string }[]; + items: {type: string}; + validators: {id: string}[]; } @Component({ selector: 'oppia-state-translation-editor', - templateUrl: './state-translation-editor.component.html' + templateUrl: './state-translation-editor.component.html', }) -export class StateTranslationEditorComponent - implements OnInit, OnDestroy { +export class StateTranslationEditorComponent implements OnInit, OnDestroy { directiveSubscriptions = new Subscription(); contentId: string = ''; @@ -60,18 +62,20 @@ export class StateTranslationEditorComponent activeWrittenTranslation: TranslatedContent | null = null; translationEditorIsOpen: boolean = false; dataFormat: DataFormatToDefaultValuesKey | string = ''; - UNICODE_SCHEMA: { type: string } = { - type: 'unicode' + UNICODE_SCHEMA: {type: string} = { + type: 'unicode', }; SET_OF_STRINGS_SCHEMA: ListSchema = { type: 'list', items: { - type: 'unicode' + type: 'unicode', }, - validators: [{ - id: 'is_uniquified' - }] + validators: [ + { + id: 'is_uniquified', + }, + ], }; HTML_SCHEMA: HTMLSchema | null = null; @@ -87,36 +91,45 @@ export class StateTranslationEditorComponent private stateEditorService: StateEditorService, private translationLanguageService: TranslationLanguageService, private translationStatusService: TranslationStatusService, - private translationTabActiveContentIdService: - TranslationTabActiveContentIdService, - ) { } + private translationTabActiveContentIdService: TranslationTabActiveContentIdService + ) {} showMarkAudioAsNeedingUpdateModalIfRequired( - contentId: string, languageCode: string): void { + contentId: string, + languageCode: string + ): void { let stateName = this.stateEditorService.getActiveStateName() as string; let state = this.explorationStatesService.getState(stateName); let recordedVoiceovers = state.recordedVoiceovers; - let availableAudioLanguages = ( - recordedVoiceovers.getLanguageCodes(contentId)); + let availableAudioLanguages = + recordedVoiceovers.getLanguageCodes(contentId); if (availableAudioLanguages.indexOf(languageCode) !== -1) { - let voiceover = recordedVoiceovers.getVoiceover( - contentId, languageCode); + let voiceover = recordedVoiceovers.getVoiceover(contentId, languageCode); if (voiceover.needsUpdate) { return; } - this.ngbModal.open(MarkAudioAsNeedingUpdateModalComponent, { - backdrop: 'static' - }).result.then(() => { - recordedVoiceovers.toggleNeedsUpdateAttribute( - contentId, languageCode); - this.explorationStatesService.saveRecordedVoiceovers( - stateName, recordedVoiceovers); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + this.ngbModal + .open(MarkAudioAsNeedingUpdateModalComponent, { + backdrop: 'static', + }) + .result.then( + () => { + recordedVoiceovers.toggleNeedsUpdateAttribute( + contentId, + languageCode + ); + this.explorationStatesService.saveRecordedVoiceovers( + stateName, + recordedVoiceovers + ); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } } @@ -126,23 +139,23 @@ export class StateTranslationEditorComponent initEditor(): void { this.translationEditorIsOpen = false; - this.contentId = ( - this.translationTabActiveContentIdService.getActiveContentId() as string); + this.contentId = + this.translationTabActiveContentIdService.getActiveContentId() as string; this.languageCode = this.translationLanguageService.getActiveLanguageCode(); this.HTML_SCHEMA = { type: 'html', ui_config: { language: this.languageCode, - languageDirection: ( - this.translationLanguageService.getActiveLanguageDirection()) - } + languageDirection: + this.translationLanguageService.getActiveLanguageDirection(), + }, }; - const entityTranslations = ( + const entityTranslations = this.entityTranslationsService.languageCodeToEntityTranslations[ - this.languageCode] - ); + this.languageCode + ]; if (entityTranslations) { this.activeWrittenTranslation = entityTranslations.getWrittenTranslation( this.translationTabActiveContentIdService.getActiveContentId() as string @@ -151,16 +164,21 @@ export class StateTranslationEditorComponent } saveTranslation(): void { - this.contentId = ( - this.translationTabActiveContentIdService.getActiveContentId() as string); + this.contentId = + this.translationTabActiveContentIdService.getActiveContentId() as string; this.languageCode = this.translationLanguageService.getActiveLanguageCode(); this.showMarkAudioAsNeedingUpdateModalIfRequired( - this.contentId, this.languageCode); - this.activeWrittenTranslation = ( - this.activeWrittenTranslation as TranslatedContent); + this.contentId, + this.languageCode + ); + this.activeWrittenTranslation = this + .activeWrittenTranslation as TranslatedContent; this.changeListService.editTranslation( - this.contentId, this.languageCode, this.activeWrittenTranslation); + this.contentId, + this.languageCode, + this.activeWrittenTranslation + ); this.entityTranslationsService.languageCodeToEntityTranslations[ this.languageCode ].updateTranslation(this.contentId, this.activeWrittenTranslation); @@ -177,15 +195,16 @@ export class StateTranslationEditorComponent if (this.isEditable()) { this.translationEditorIsOpen = true; if (!this.activeWrittenTranslation) { - this.activeWrittenTranslation = ( - TranslatedContent.createNew(this.dataFormat)); + this.activeWrittenTranslation = TranslatedContent.createNew( + this.dataFormat + ); } } } onSaveTranslationButtonClicked(): void { - this.activeWrittenTranslation = ( - this.activeWrittenTranslation as TranslatedContent); + this.activeWrittenTranslation = this + .activeWrittenTranslation as TranslatedContent; this.activeWrittenTranslation.needsUpdate = false; this.saveTranslation(); } @@ -195,35 +214,36 @@ export class StateTranslationEditorComponent } markAsNeedingUpdate(): void { - let contentId = ( - this.translationTabActiveContentIdService.getActiveContentId() as string); + let contentId = + this.translationTabActiveContentIdService.getActiveContentId() as string; let languageCode = this.translationLanguageService.getActiveLanguageCode(); - this.activeWrittenTranslation = ( - this.activeWrittenTranslation as TranslatedContent); + this.activeWrittenTranslation = this + .activeWrittenTranslation as TranslatedContent; this.activeWrittenTranslation.markAsNeedingUpdate(); this.changeListService.editTranslation( - contentId, languageCode, this.activeWrittenTranslation); + contentId, + languageCode, + this.activeWrittenTranslation + ); this.translationStatusService.refresh(); } ngOnInit(): void { - this.dataFormat = ( - this.translationTabActiveContentIdService.getActiveDataFormat() as string - ); + this.dataFormat = + this.translationTabActiveContentIdService.getActiveDataFormat() as string; this.directiveSubscriptions.add( - this.translationTabActiveContentIdService.onActiveContentIdChanged. - subscribe( - (dataFormat) => { - this.dataFormat = dataFormat; - this.initEditor(); - } - ) + this.translationTabActiveContentIdService.onActiveContentIdChanged.subscribe( + dataFormat => { + this.dataFormat = dataFormat; + this.initEditor(); + } + ) ); this.directiveSubscriptions.add( - this.translationLanguageService.onActiveLanguageChanged.subscribe( - () => this.initEditor() + this.translationLanguageService.onActiveLanguageChanged.subscribe(() => + this.initEditor() ) ); @@ -234,7 +254,8 @@ export class StateTranslationEditorComponent if (this.translationEditorIsOpen) { this.saveTranslation(); } - })); + }) + ); } ngOnDestroy(): void { @@ -242,7 +263,9 @@ export class StateTranslationEditorComponent } } -angular.module('oppia').directive('oppiaStateTranslationEditor', +angular.module('oppia').directive( + 'oppiaStateTranslationEditor', downgradeComponent({ - component: StateTranslationEditorComponent - }) as angular.IDirectiveFactory); + component: StateTranslationEditorComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-editor-page/translation-tab/state-translation-status-graph/state-translation-status-graph.component.spec.ts b/core/templates/pages/exploration-editor-page/translation-tab/state-translation-status-graph/state-translation-status-graph.component.spec.ts index f912c6c7c9ad..781b0137cba7 100644 --- a/core/templates/pages/exploration-editor-page/translation-tab/state-translation-status-graph/state-translation-status-graph.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/translation-tab/state-translation-status-graph/state-translation-status-graph.component.spec.ts @@ -16,21 +16,27 @@ * @fileoverview Unit tests for stateTranslationStatusGraph. */ -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { Subscription } from 'rxjs'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { StateTranslationStatusGraphComponent } from './state-translation-status-graph.component'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; -import { TranslationStatusService } from '../services/translation-status.service'; -import { State } from 'domain/state/StateObjectFactory'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {Subscription} from 'rxjs'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {StateTranslationStatusGraphComponent} from './state-translation-status-graph.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; +import {TranslationStatusService} from '../services/translation-status.service'; +import {State} from 'domain/state/StateObjectFactory'; class MockNgbModal { open() { return { - result: Promise.resolve() + result: Promise.resolve(), }; } } @@ -43,26 +49,25 @@ describe('State Translation Status Graph Component', () => { let translationStatusService: TranslationStatusService; let testSubscriptions: Subscription; const refreshStateTranslationSpy = jasmine.createSpy( - 'refreshStateTranslationSpy'); + 'refreshStateTranslationSpy' + ); let stateName: string = 'State1'; let state = { - recordedVoiceovers: {} + recordedVoiceovers: {}, } as State; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - StateTranslationStatusGraphComponent - ], + declarations: [StateTranslationStatusGraphComponent], providers: [ { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -77,8 +82,9 @@ describe('State Translation Status Graph Component', () => { testSubscriptions = new Subscription(); testSubscriptions.add( stateEditorService.onRefreshStateTranslation.subscribe( - refreshStateTranslationSpy)); - + refreshStateTranslationSpy + ) + ); fixture.detectChanges(); }); @@ -90,34 +96,42 @@ describe('State Translation Status Graph Component', () => { describe('when translation tab is not busy', () => { beforeEach(() => { spyOn(stateEditorService, 'getActiveStateName').and.returnValue( - stateName); + stateName + ); spyOn(explorationStatesService, 'getState').and.returnValue(state); component.isTranslationTabBusy = false; }); it('should get node colors from translation status', () => { let nodeColors = {}; - spyOn(translationStatusService, 'getAllStateStatusColors').and - .returnValue(nodeColors); + spyOn( + translationStatusService, + 'getAllStateStatusColors' + ).and.returnValue(nodeColors); expect(component.nodeColors()).toEqual(nodeColors); - expect(translationStatusService.getAllStateStatusColors) - .toHaveBeenCalled(); + expect( + translationStatusService.getAllStateStatusColors + ).toHaveBeenCalled(); }); it('should get active state name from state editor', () => { expect(component.getActiveStateName()).toBe(stateName); }); - it('should set new active state name and refresh state when clicking' + - ' on state in map', () => { - spyOn(stateEditorService, 'setActiveStateName'); - component.onClickStateInMap('State2'); + it( + 'should set new active state name and refresh state when clicking' + + ' on state in map', + () => { + spyOn(stateEditorService, 'setActiveStateName'); + component.onClickStateInMap('State2'); - expect(stateEditorService.setActiveStateName).toHaveBeenCalledWith( - 'State2'); - expect(refreshStateTranslationSpy).toHaveBeenCalled(); - }); + expect(stateEditorService.setActiveStateName).toHaveBeenCalledWith( + 'State2' + ); + expect(refreshStateTranslationSpy).toHaveBeenCalled(); + } + ); }); describe('when translation tab is busy', () => { @@ -128,25 +142,27 @@ describe('State Translation Status Graph Component', () => { component.isTranslationTabBusy = true; showTranslationTabBusyModalspy = jasmine.createSpy( - 'showTranslationTabBusyModal'); + 'showTranslationTabBusyModal' + ); testSubscriptions = new Subscription(); testSubscriptions.add( stateEditorService.onShowTranslationTabBusyModal.subscribe( - showTranslationTabBusyModalspy)); + showTranslationTabBusyModalspy + ) + ); }); afterEach(() => { testSubscriptions.unsubscribe(); }); - it('should show translation tab busy modal when clicking on state in map', - () => { - spyOn(stateEditorService, 'setActiveStateName'); - component.onClickStateInMap('State2'); + it('should show translation tab busy modal when clicking on state in map', () => { + spyOn(stateEditorService, 'setActiveStateName'); + component.onClickStateInMap('State2'); - expect(stateEditorService.setActiveStateName).not.toHaveBeenCalled(); - expect(showTranslationTabBusyModalspy).toHaveBeenCalled(); - }); + expect(stateEditorService.setActiveStateName).not.toHaveBeenCalled(); + expect(showTranslationTabBusyModalspy).toHaveBeenCalled(); + }); it('should throw error if state name is null', fakeAsync(() => { spyOn(stateEditorService, 'getActiveStateName').and.returnValue(null); diff --git a/core/templates/pages/exploration-editor-page/translation-tab/state-translation-status-graph/state-translation-status-graph.component.ts b/core/templates/pages/exploration-editor-page/translation-tab/state-translation-status-graph/state-translation-status-graph.component.ts index 529cc1a4ec87..e5101cac8184 100644 --- a/core/templates/pages/exploration-editor-page/translation-tab/state-translation-status-graph/state-translation-status-graph.component.ts +++ b/core/templates/pages/exploration-editor-page/translation-tab/state-translation-status-graph/state-translation-status-graph.component.ts @@ -16,18 +16,18 @@ * @fileoverview Compoennt for the state translation status graph. */ -import { Component, Input } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { StateRecordedVoiceoversService } from 'components/state-editor/state-editor-properties-services/state-recorded-voiceovers.service'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; -import { GraphDataService } from 'pages/exploration-editor-page/services/graph-data.service'; -import { RouterService } from 'pages/exploration-editor-page/services/router.service'; -import { TranslationStatusService } from '../services/translation-status.service'; +import {Component, Input} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {StateRecordedVoiceoversService} from 'components/state-editor/state-editor-properties-services/state-recorded-voiceovers.service'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; +import {GraphDataService} from 'pages/exploration-editor-page/services/graph-data.service'; +import {RouterService} from 'pages/exploration-editor-page/services/router.service'; +import {TranslationStatusService} from '../services/translation-status.service'; @Component({ selector: 'oppia-state-translation-status-graph', - templateUrl: './state-translation-status-graph.component.html' + templateUrl: './state-translation-status-graph.component.html', }) export class StateTranslationStatusGraphComponent { // This property is initialized using Angular lifecycle hooks @@ -42,7 +42,7 @@ export class StateTranslationStatusGraphComponent { private routerService: RouterService, private stateRecordedVoiceoversService: StateRecordedVoiceoversService, private translationStatusService: TranslationStatusService - ) { } + ) {} nodeColors(): object { return this.translationStatusService.getAllStateStatusColors(); @@ -66,14 +66,18 @@ export class StateTranslationStatusGraphComponent { if (stateName && stateData) { this.stateRecordedVoiceoversService.init( - stateName, stateData.recordedVoiceovers); + stateName, + stateData.recordedVoiceovers + ); this.stateEditorService.onRefreshStateTranslation.emit(); } this.routerService.onCenterGraph.emit(); } } -angular.module('oppia').directive('oppiaStateTranslationStatusGraph', +angular.module('oppia').directive( + 'oppiaStateTranslationStatusGraph', downgradeComponent({ - component: StateTranslationStatusGraphComponent - }) as angular.IDirectiveFactory); + component: StateTranslationStatusGraphComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-editor-page/translation-tab/state-translation/state-translation.component.spec.ts b/core/templates/pages/exploration-editor-page/translation-tab/state-translation/state-translation.component.spec.ts index c2b3315f9c9e..9c77fcf6999f 100644 --- a/core/templates/pages/exploration-editor-page/translation-tab/state-translation/state-translation.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/translation-tab/state-translation/state-translation.component.spec.ts @@ -16,84 +16,88 @@ * @fileoverview Unit tests for stateTranslation. */ - -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { EventEmitter, NO_ERRORS_SCHEMA, Pipe } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { CkEditorCopyContentService } from 'components/ck-editor-helpers/ck-editor-copy-content.service'; -import { StateCustomizationArgsService } from 'components/state-editor/state-editor-properties-services/state-customization-args.service'; -import { AnswerChoice, StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { StateInteractionIdService } from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; -import { StateRecordedVoiceoversService } from 'components/state-editor/state-editor-properties-services/state-recorded-voiceovers.service'; -import { StateSolutionService } from 'components/state-editor/state-editor-properties-services/state-solution.service'; -import { StateWrittenTranslationsService } from 'components/state-editor/state-editor-properties-services/state-written-translations.service'; -import { AnswerGroupObjectFactory } from 'domain/exploration/AnswerGroupObjectFactory'; -import { OutcomeObjectFactory } from 'domain/exploration/OutcomeObjectFactory'; -import { ReadOnlyExplorationBackendApiService } from 'domain/exploration/read-only-exploration-backend-api.service'; -import { RecordedVoiceovers } from 'domain/exploration/recorded-voiceovers.model'; -import { Rule } from 'domain/exploration/rule.model'; -import { StateObjectsBackendDict } from 'domain/exploration/StatesObjectFactory'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { SubtitledUnicodeObjectFactory } from 'domain/exploration/SubtitledUnicodeObjectFactory'; -import { NumberWithUnitsObjectFactory } from 'domain/objects/NumberWithUnitsObjectFactory'; -import { EntityTranslation } from 'domain/translation/EntityTranslationObjectFactory'; -import { ParameterizeRuleDescriptionPipe } from 'filters/parameterize-rule-description.pipe'; -import { ConvertToPlainTextPipe } from 'filters/string-utility-filters/convert-to-plain-text.pipe'; -import { WrapTextWithEllipsisPipe } from 'filters/string-utility-filters/wrap-text-with-ellipsis.pipe'; -import { ContinueRulesService } from 'interactions/Continue/directives/continue-rules.service'; -import { ContinueValidationService } from 'interactions/Continue/directives/continue-validation.service'; -import { TextInputRulesService } from 'interactions/TextInput/directives/text-input-rules.service'; -import { AngularNameService } from 'pages/exploration-editor-page/services/angular-name.service'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; -import { StateEditorRefreshService } from 'pages/exploration-editor-page/services/state-editor-refresh.service'; -import { ContextService } from 'services/context.service'; -import { EntityTranslationsService } from 'services/entity-translations.services'; -import { ExplorationHtmlFormatterService } from 'services/exploration-html-formatter.service'; -import { ExplorationImprovementsTaskRegistryService } from 'services/exploration-improvements-task-registry.service'; -import { ExternalSaveService } from 'services/external-save.service'; -import { TranslationLanguageService } from '../services/translation-language.service'; -import { TranslationTabActiveContentIdService } from '../services/translation-tab-active-content-id.service'; -import { TranslationTabActiveModeService } from '../services/translation-tab-active-mode.service'; -import { StateTranslationComponent } from './state-translation.component'; -import { RouterService } from 'pages/exploration-editor-page/services/router.service'; -import { TranslatedContent } from 'domain/exploration/TranslatedContentObjectFactory'; -import { Hint } from 'domain/exploration/hint-object.model'; -import { AnswerGroup } from 'domain/exploration/AnswerGroupObjectFactory'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {EventEmitter, NO_ERRORS_SCHEMA, Pipe} from '@angular/core'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {CkEditorCopyContentService} from 'components/ck-editor-helpers/ck-editor-copy-content.service'; +import {StateCustomizationArgsService} from 'components/state-editor/state-editor-properties-services/state-customization-args.service'; +import { + AnswerChoice, + StateEditorService, +} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {StateInteractionIdService} from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; +import {StateRecordedVoiceoversService} from 'components/state-editor/state-editor-properties-services/state-recorded-voiceovers.service'; +import {StateSolutionService} from 'components/state-editor/state-editor-properties-services/state-solution.service'; +import {StateWrittenTranslationsService} from 'components/state-editor/state-editor-properties-services/state-written-translations.service'; +import {AnswerGroupObjectFactory} from 'domain/exploration/AnswerGroupObjectFactory'; +import {OutcomeObjectFactory} from 'domain/exploration/OutcomeObjectFactory'; +import {ReadOnlyExplorationBackendApiService} from 'domain/exploration/read-only-exploration-backend-api.service'; +import {RecordedVoiceovers} from 'domain/exploration/recorded-voiceovers.model'; +import {Rule} from 'domain/exploration/rule.model'; +import {StateObjectsBackendDict} from 'domain/exploration/StatesObjectFactory'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {SubtitledUnicodeObjectFactory} from 'domain/exploration/SubtitledUnicodeObjectFactory'; +import {NumberWithUnitsObjectFactory} from 'domain/objects/NumberWithUnitsObjectFactory'; +import {EntityTranslation} from 'domain/translation/EntityTranslationObjectFactory'; +import {ParameterizeRuleDescriptionPipe} from 'filters/parameterize-rule-description.pipe'; +import {ConvertToPlainTextPipe} from 'filters/string-utility-filters/convert-to-plain-text.pipe'; +import {WrapTextWithEllipsisPipe} from 'filters/string-utility-filters/wrap-text-with-ellipsis.pipe'; +import {ContinueRulesService} from 'interactions/Continue/directives/continue-rules.service'; +import {ContinueValidationService} from 'interactions/Continue/directives/continue-validation.service'; +import {TextInputRulesService} from 'interactions/TextInput/directives/text-input-rules.service'; +import {AngularNameService} from 'pages/exploration-editor-page/services/angular-name.service'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; +import {StateEditorRefreshService} from 'pages/exploration-editor-page/services/state-editor-refresh.service'; +import {ContextService} from 'services/context.service'; +import {EntityTranslationsService} from 'services/entity-translations.services'; +import {ExplorationHtmlFormatterService} from 'services/exploration-html-formatter.service'; +import {ExplorationImprovementsTaskRegistryService} from 'services/exploration-improvements-task-registry.service'; +import {ExternalSaveService} from 'services/external-save.service'; +import {TranslationLanguageService} from '../services/translation-language.service'; +import {TranslationTabActiveContentIdService} from '../services/translation-tab-active-content-id.service'; +import {TranslationTabActiveModeService} from '../services/translation-tab-active-mode.service'; +import {StateTranslationComponent} from './state-translation.component'; +import {RouterService} from 'pages/exploration-editor-page/services/router.service'; +import {TranslatedContent} from 'domain/exploration/TranslatedContentObjectFactory'; +import {Hint} from 'domain/exploration/hint-object.model'; +import {AnswerGroup} from 'domain/exploration/AnswerGroupObjectFactory'; const DEFAULT_OBJECT_VALUES = require('objects/object_defaults.json'); class MockNgbModal { open() { return { - result: Promise.resolve() + result: Promise.resolve(), }; } } -@Pipe({ name: 'parameterizeRuleDescriptionPipe' }) +@Pipe({name: 'parameterizeRuleDescriptionPipe'}) class MockParameterizeRuleDescriptionPipe { transform( - rule: Rule | null, interactionId: string | null, - choices: AnswerChoice[] | null): string { + rule: Rule | null, + interactionId: string | null, + choices: AnswerChoice[] | null + ): string { return ''; } } -@Pipe({ name: 'wrapTextWithEllipsis' }) +@Pipe({name: 'wrapTextWithEllipsis'}) class MockWrapTextWithEllipsisPipe { transform(input: string, characterCount: number): string { return ''; } } -@Pipe({ name: 'truncate' }) +@Pipe({name: 'truncate'}) class MockTruncatePipe { transform(value: string, params: number): string { return value; } } -@Pipe({ name: 'convertToPlainText' }) +@Pipe({name: 'convertToPlainText'}) class MockConvertToPlainTextPipe { transform(value: string): string { return value; @@ -112,15 +116,14 @@ describe('State translation component', () => { let stateRecordedVoiceoversService: StateRecordedVoiceoversService; let subtitledUnicodeObjectFactory: SubtitledUnicodeObjectFactory; let translationLanguageService: TranslationLanguageService; - let translationTabActiveContentIdService: - TranslationTabActiveContentIdService; + let translationTabActiveContentIdService: TranslationTabActiveContentIdService; let translationTabActiveModeService: TranslationTabActiveModeService; let explorationState1 = { Introduction: { content: { content_id: 'content_1', - html: 'Introduction Content' + html: 'Introduction Content', }, classifier_model_id: 'null', card_is_checkpoint: false, @@ -131,63 +134,69 @@ describe('State translation component', () => { placeholder: { value: { content_id: 'ca_placeholder', - unicode_str: '' - } + unicode_str: '', + }, }, rows: { - value: 1 + value: 1, }, catchMisspellings: { - value: false - } + value: false, + }, }, - answer_groups: [{ - training_data: null, - tagged_skill_misconception_id: null, - rule_specs: [{ - rule_type: 'Equals', - inputs: { - x: { - contentId: 'rule_input_4', - normalizedStrSet: ['input1'] - } - } - }, { - rule_type: 'Equals', - inputs: { - x: { - contentId: 'rule_input_5', - normalizedStrSet: ['input2'] - } - } - }], - outcome: { - labelled_as_correct: null, - param_changes: null, - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: 'unused', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_1', - html: '' + answer_groups: [ + { + training_data: null, + tagged_skill_misconception_id: null, + rule_specs: [ + { + rule_type: 'Equals', + inputs: { + x: { + contentId: 'rule_input_4', + normalizedStrSet: ['input1'], + }, + }, + }, + { + rule_type: 'Equals', + inputs: { + x: { + contentId: 'rule_input_5', + normalizedStrSet: ['input2'], + }, + }, + }, + ], + outcome: { + labelled_as_correct: null, + param_changes: null, + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: 'unused', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: '', + }, }, - } - }, { - rule_specs: [], - outcome: { - labelled_as_correct: null, - param_changes: null, - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: 'unused', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: '' + }, + { + rule_specs: [], + outcome: { + labelled_as_correct: null, + param_changes: null, + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: 'unused', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: '', + }, }, - } - }], + }, + ], default_outcome: { dest: 'default', labelled_as_correct: null, @@ -197,7 +206,7 @@ describe('State translation component', () => { dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: 'Default Outcome' + html: 'Default Outcome', }, }, solution: { @@ -205,28 +214,31 @@ describe('State translation component', () => { answer_is_exclusive: false, explanation: { html: 'Solution explanation', - content_id: 'solution_1' - } + content_id: 'solution_1', + }, }, - hints: [{ - hint_content: { - html: 'Hint 1', - content_id: 'hint_1' - } - }, { - hint_content: { - html: 'Hint 2', - content_id: 'hint_2' - } - }] + hints: [ + { + hint_content: { + html: 'Hint 1', + content_id: 'hint_1', + }, + }, + { + hint_content: { + html: 'Hint 2', + content_id: 'hint_2', + }, + }, + ], }, linked_skill_id: null, param_changes: [], solicit_answer_details: false, recorded_voiceovers: { - voiceovers_mapping: {} - } - } + voiceovers_mapping: {}, + }, + }, } as StateObjectsBackendDict; let recordedVoiceovers = { @@ -241,8 +253,8 @@ describe('State translation component', () => { ca_placeholder: {}, ca_fakePlaceholder: {}, rule_input_4: {}, - rule_input_5: {} - } + rule_input_5: {}, + }, }; let refreshStateTranslationEmitter = new EventEmitter(); @@ -261,13 +273,13 @@ describe('State translation component', () => { MockParameterizeRuleDescriptionPipe, MockTruncatePipe, MockConvertToPlainTextPipe, - MockWrapTextWithEllipsisPipe + MockWrapTextWithEllipsisPipe, ], providers: [ WrapTextWithEllipsisPipe, ConvertToPlainTextPipe, AngularNameService, - { provide: ContextService, useClass: MockContextService }, + {provide: ContextService, useClass: MockContextService}, ContinueValidationService, ContinueRulesService, ExplorationImprovementsTaskRegistryService, @@ -289,18 +301,18 @@ describe('State translation component', () => { TranslationTabActiveModeService, { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, { provide: ParameterizeRuleDescriptionPipe, - useClass: MockParameterizeRuleDescriptionPipe + useClass: MockParameterizeRuleDescriptionPipe, }, { provide: WrapTextWithEllipsisPipe, - useClass: MockWrapTextWithEllipsisPipe - } + useClass: MockWrapTextWithEllipsisPipe, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -314,44 +326,55 @@ describe('State translation component', () => { stateEditorService = TestBed.inject(StateEditorService); explorationStatesService = TestBed.inject(ExplorationStatesService); stateRecordedVoiceoversService = TestBed.inject( - StateRecordedVoiceoversService); + StateRecordedVoiceoversService + ); subtitledUnicodeObjectFactory = TestBed.inject( - SubtitledUnicodeObjectFactory); + SubtitledUnicodeObjectFactory + ); translationLanguageService = TestBed.inject(TranslationLanguageService); translationTabActiveContentIdService = TestBed.inject( - TranslationTabActiveContentIdService); + TranslationTabActiveContentIdService + ); translationTabActiveModeService = TestBed.inject( - TranslationTabActiveModeService); + TranslationTabActiveModeService + ); explorationStatesService.init(explorationState1, false); stateRecordedVoiceoversService.init( - 'Introduction', RecordedVoiceovers.createFromBackendDict( - recordedVoiceovers)); + 'Introduction', + RecordedVoiceovers.createFromBackendDict(recordedVoiceovers) + ); entityTranslationsService = TestBed.inject(EntityTranslationsService); entityTranslationsService.init('exp1', 'exploration', 5); - entityTranslationsService.entityTranslation = ( + entityTranslationsService.entityTranslation = EntityTranslation.createFromBackendDict({ entity_id: 'exp1', entity_type: 'exploration', entity_version: 5, language_code: 'hi', - translations: {} - }) - ); + translations: {}, + }); - spyOnProperty(stateEditorService, 'onRefreshStateTranslation').and - .returnValue(refreshStateTranslationEmitter); + spyOnProperty( + stateEditorService, + 'onRefreshStateTranslation' + ).and.returnValue(refreshStateTranslationEmitter); spyOn(stateEditorService, 'getActiveStateName').and.returnValue( - 'Introduction'); + 'Introduction' + ); ckEditorCopyContentService.copyModeActive = true; - spyOn(translationLanguageService, 'getActiveLanguageCode').and - .returnValue('en'); - spyOn(translationTabActiveModeService, 'isVoiceoverModeActive').and - .returnValue(true); + spyOn(translationLanguageService, 'getActiveLanguageCode').and.returnValue( + 'en' + ); + spyOn( + translationTabActiveModeService, + 'isVoiceoverModeActive' + ).and.returnValue(true); explorationStatesService.init(explorationState1, false); stateRecordedVoiceoversService.init( - 'Introduction', RecordedVoiceovers.createFromBackendDict( - recordedVoiceovers)); + 'Introduction', + RecordedVoiceovers.createFromBackendDict(recordedVoiceovers) + ); component.isTranslationTabBusy = false; component.stateName = 'Introduction'; @@ -364,308 +387,388 @@ describe('State translation component', () => { component.ngOnDestroy(); }); - describe('when translation tab is not busy and voiceover mode is' + - ' active', () => { - it('should init state translation when refreshing page', () => { - spyOn(translationTabActiveContentIdService, 'setActiveContent'); - refreshStateTranslationEmitter.emit(); - - expect(component.isActive('content')).toBe(true); - expect(component.isVoiceoverModeActive()).toBe(true); - expect(component.isDisabled('content')).toBe(false); - expect(translationTabActiveContentIdService.setActiveContent) - .toHaveBeenCalledWith('content_1', 'html'); - }); + describe( + 'when translation tab is not busy and voiceover mode is' + ' active', + () => { + it('should init state translation when refreshing page', () => { + spyOn(translationTabActiveContentIdService, 'setActiveContent'); + refreshStateTranslationEmitter.emit(); + + expect(component.isActive('content')).toBe(true); + expect(component.isVoiceoverModeActive()).toBe(true); + expect(component.isDisabled('content')).toBe(false); + expect( + translationTabActiveContentIdService.setActiveContent + ).toHaveBeenCalledWith('content_1', 'html'); + }); - it('should get customization argument translatable customization' + - ' arguments', () => { - let content = SubtitledHtml.createDefault('', ''); - let translatableCa = ( - component.getInteractionCustomizationArgTranslatableContents({ - testingCustArgs: { - value: { - innerValue: content - } - } - }) + it( + 'should get customization argument translatable customization' + + ' arguments', + () => { + let content = SubtitledHtml.createDefault('', ''); + let translatableCa = + component.getInteractionCustomizationArgTranslatableContents({ + testingCustArgs: { + value: { + innerValue: content, + }, + }, + }); + expect(translatableCa).toEqual([ + { + name: 'Testing Cust Args > Inner Value', + content, + }, + ]); + } ); - expect(translatableCa).toEqual([{ - name: 'Testing Cust Args > Inner Value', - content - }]); - }); - it('should broadcast copy to ck editor when clicking on content', - () => { - spyOn(ckEditorCopyContentService, 'broadcastCopy').and - .callFake(() => { }); + it('should broadcast copy to ck editor when clicking on content', () => { + spyOn(ckEditorCopyContentService, 'broadcastCopy').and.callFake( + () => {} + ); let mockEvent = { - stopPropagation: () => { }, - target: {} + stopPropagation: () => {}, + target: {}, } as Event; component.onContentClick(mockEvent); expect(ckEditorCopyContentService.broadcastCopy).toHaveBeenCalledWith( - mockEvent.target); + mockEvent.target + ); }); - it('should activate content tab when clicking on tab', () => { - spyOn(translationTabActiveContentIdService, 'setActiveContent'); - component.onTabClick('content'); - - expect(component.isActive('content')).toBe(true); - expect(component.isDisabled('content')).toBe(false); - expect(translationTabActiveContentIdService.setActiveContent) - .toHaveBeenCalledWith('content_1', 'html'); - expect(component.tabStatusColorStyle('content')).toEqual({ - 'border-top-color': '#D14836' - }); - expect(component.tabNeedUpdatesStatus('content')).toBe(false); - expect(component.contentIdNeedUpdates('content_1')).toBe(false); - expect(component.contentIdStatusColorStyle('content_1')).toEqual({ - 'border-left': '3px solid #D14836' + it('should activate content tab when clicking on tab', () => { + spyOn(translationTabActiveContentIdService, 'setActiveContent'); + component.onTabClick('content'); + + expect(component.isActive('content')).toBe(true); + expect(component.isDisabled('content')).toBe(false); + expect( + translationTabActiveContentIdService.setActiveContent + ).toHaveBeenCalledWith('content_1', 'html'); + expect(component.tabStatusColorStyle('content')).toEqual({ + 'border-top-color': '#D14836', + }); + expect(component.tabNeedUpdatesStatus('content')).toBe(false); + expect(component.contentIdNeedUpdates('content_1')).toBe(false); + expect(component.contentIdStatusColorStyle('content_1')).toEqual({ + 'border-left': '3px solid #D14836', + }); }); - }); - it('should activate interaction custimization arguments tab when ' + - 'clicking on tab', () => { - spyOn(translationTabActiveContentIdService, 'setActiveContent'); - component.onTabClick('ca'); - - expect(component.isActive('ca')).toBe(true); - expect(component.isDisabled('ca')).toBe(false); - expect(translationTabActiveContentIdService.setActiveContent) - .toHaveBeenCalledWith('ca_placeholder', 'unicode'); - expect(component.tabStatusColorStyle('ca')).toEqual({ - 'border-top-color': '#D14836' - }); - expect(component.tabNeedUpdatesStatus('ca')).toBe(false); - expect(component.contentIdNeedUpdates('ca_placeholder')).toBe(false); - expect(component.contentIdStatusColorStyle('ca_placeholder')).toEqual({ - 'border-left': '3px solid #D14836' - }); - }); + it( + 'should activate interaction custimization arguments tab when ' + + 'clicking on tab', + () => { + spyOn(translationTabActiveContentIdService, 'setActiveContent'); + component.onTabClick('ca'); + + expect(component.isActive('ca')).toBe(true); + expect(component.isDisabled('ca')).toBe(false); + expect( + translationTabActiveContentIdService.setActiveContent + ).toHaveBeenCalledWith('ca_placeholder', 'unicode'); + expect(component.tabStatusColorStyle('ca')).toEqual({ + 'border-top-color': '#D14836', + }); + expect(component.tabNeedUpdatesStatus('ca')).toBe(false); + expect(component.contentIdNeedUpdates('ca_placeholder')).toBe(false); + expect(component.contentIdStatusColorStyle('ca_placeholder')).toEqual( + { + 'border-left': '3px solid #D14836', + } + ); + } + ); - it('should activate feedback tab when clicking on tab', () => { - spyOn(translationTabActiveContentIdService, 'setActiveContent'); - component.onTabClick('feedback'); + it('should activate feedback tab when clicking on tab', () => { + spyOn(translationTabActiveContentIdService, 'setActiveContent'); + component.onTabClick('feedback'); - expect(component.isActive('feedback')).toBe(true); - expect(component.isDisabled('feedback')).toBe(false); - expect(translationTabActiveContentIdService.setActiveContent) - .toHaveBeenCalledWith('feedback_1', 'html'); - expect(component.tabStatusColorStyle('feedback')).toEqual({ - 'border-top-color': '#D14836' - }); - expect(component.tabNeedUpdatesStatus('feedback')).toBe(false); - expect(component.contentIdNeedUpdates('feedback_1')).toBe(false); - expect(component.contentIdStatusColorStyle('feedback_1')).toEqual({ - 'border-left': '3px solid #D14836' + expect(component.isActive('feedback')).toBe(true); + expect(component.isDisabled('feedback')).toBe(false); + expect( + translationTabActiveContentIdService.setActiveContent + ).toHaveBeenCalledWith('feedback_1', 'html'); + expect(component.tabStatusColorStyle('feedback')).toEqual({ + 'border-top-color': '#D14836', + }); + expect(component.tabNeedUpdatesStatus('feedback')).toBe(false); + expect(component.contentIdNeedUpdates('feedback_1')).toBe(false); + expect(component.contentIdStatusColorStyle('feedback_1')).toEqual({ + 'border-left': '3px solid #D14836', + }); }); - }); - it('should activate hint tab when clicking on tab', () => { - spyOn(translationTabActiveContentIdService, 'setActiveContent'); - component.onTabClick('hint'); + it('should activate hint tab when clicking on tab', () => { + spyOn(translationTabActiveContentIdService, 'setActiveContent'); + component.onTabClick('hint'); - expect(component.isActive('hint')).toBe(true); - expect(component.isDisabled('hint')).toBe(false); - expect(translationTabActiveContentIdService.setActiveContent) - .toHaveBeenCalledWith('hint_1', 'html'); - expect(component.tabStatusColorStyle('hint')).toEqual({ - 'border-top-color': '#D14836' - }); - expect(component.tabNeedUpdatesStatus('hint')).toBe(false); - expect(component.contentIdNeedUpdates('hint_1')).toBe(false); - expect(component.contentIdStatusColorStyle('hint_1')).toEqual({ - 'border-left': '3px solid #D14836' + expect(component.isActive('hint')).toBe(true); + expect(component.isDisabled('hint')).toBe(false); + expect( + translationTabActiveContentIdService.setActiveContent + ).toHaveBeenCalledWith('hint_1', 'html'); + expect(component.tabStatusColorStyle('hint')).toEqual({ + 'border-top-color': '#D14836', + }); + expect(component.tabNeedUpdatesStatus('hint')).toBe(false); + expect(component.contentIdNeedUpdates('hint_1')).toBe(false); + expect(component.contentIdStatusColorStyle('hint_1')).toEqual({ + 'border-left': '3px solid #D14836', + }); }); - }); - it('should activate solution tab when clicking on tab', () => { - spyOn(translationTabActiveContentIdService, 'setActiveContent'); - component.onTabClick('solution'); - - expect(component.isActive('solution')).toBe(true); - expect(component.isDisabled('solution')).toBe(false); - expect(translationTabActiveContentIdService.setActiveContent) - .toHaveBeenCalledWith('solution_1', 'html'); - expect(component.tabStatusColorStyle('solution')).toEqual({ - 'border-top-color': '#D14836' - }); - expect(component.tabNeedUpdatesStatus('solution')).toBe(false); - expect(component.contentIdNeedUpdates('solution')).toBe(false); - expect(component.contentIdStatusColorStyle('solution_1')).toEqual({ - 'border-left': '3px solid #D14836' + it('should activate solution tab when clicking on tab', () => { + spyOn(translationTabActiveContentIdService, 'setActiveContent'); + component.onTabClick('solution'); + + expect(component.isActive('solution')).toBe(true); + expect(component.isDisabled('solution')).toBe(false); + expect( + translationTabActiveContentIdService.setActiveContent + ).toHaveBeenCalledWith('solution_1', 'html'); + expect(component.tabStatusColorStyle('solution')).toEqual({ + 'border-top-color': '#D14836', + }); + expect(component.tabNeedUpdatesStatus('solution')).toBe(false); + expect(component.contentIdNeedUpdates('solution')).toBe(false); + expect(component.contentIdStatusColorStyle('solution_1')).toEqual({ + 'border-left': '3px solid #D14836', + }); }); - }); - it('should activate rule inputs tab when clicking on tab', () => { - spyOn(translationTabActiveContentIdService, 'setActiveContent'); - component.onTabClick('rule_input'); + it('should activate rule inputs tab when clicking on tab', () => { + spyOn(translationTabActiveContentIdService, 'setActiveContent'); + component.onTabClick('rule_input'); - expect(component.isActive('rule_input')).toBe(true); - expect(component.isDisabled('rule_input')).toBe(false); - expect(translationTabActiveContentIdService.setActiveContent) - .toHaveBeenCalledWith('rule_input_4', 'set_of_normalized_string'); - }); + expect(component.isActive('rule_input')).toBe(true); + expect(component.isDisabled('rule_input')).toBe(false); + expect( + translationTabActiveContentIdService.setActiveContent + ).toHaveBeenCalledWith('rule_input_4', 'set_of_normalized_string'); + }); - it('should change active rule content index', () => { - component.onTabClick('rule_input'); + it('should change active rule content index', () => { + component.onTabClick('rule_input'); - spyOn(translationTabActiveContentIdService, 'setActiveContent'); - component.changeActiveRuleContentIndex(1); + spyOn(translationTabActiveContentIdService, 'setActiveContent'); + component.changeActiveRuleContentIndex(1); - expect(translationTabActiveContentIdService.setActiveContent) - .toHaveBeenCalledWith('rule_input_5', 'set_of_normalized_string'); - }); + expect( + translationTabActiveContentIdService.setActiveContent + ).toHaveBeenCalledWith('rule_input_5', 'set_of_normalized_string'); + }); - it('should not change active rule content index if it is equal to the ' + - 'current one', () => { - component.onTabClick('rule_input'); + it( + 'should not change active rule content index if it is equal to the ' + + 'current one', + () => { + component.onTabClick('rule_input'); - spyOn(translationTabActiveContentIdService, 'setActiveContent'); - component.changeActiveRuleContentIndex(0); + spyOn(translationTabActiveContentIdService, 'setActiveContent'); + component.changeActiveRuleContentIndex(0); - expect(translationTabActiveContentIdService.setActiveContent).not - .toHaveBeenCalled(); - }); + expect( + translationTabActiveContentIdService.setActiveContent + ).not.toHaveBeenCalled(); + } + ); - it('should change active hint index', () => { - component.onTabClick('hint'); + it('should change active hint index', () => { + component.onTabClick('hint'); - spyOn(translationTabActiveContentIdService, 'setActiveContent'); - component.changeActiveHintIndex(1); + spyOn(translationTabActiveContentIdService, 'setActiveContent'); + component.changeActiveHintIndex(1); - expect(translationTabActiveContentIdService.setActiveContent) - .toHaveBeenCalledWith('hint_2', 'html'); - }); + expect( + translationTabActiveContentIdService.setActiveContent + ).toHaveBeenCalledWith('hint_2', 'html'); + }); - it('should not change active hint index if it is equal to the current one', - () => { + it('should not change active hint index if it is equal to the current one', () => { component.onTabClick('hint'); spyOn(translationTabActiveContentIdService, 'setActiveContent'); component.changeActiveHintIndex(0); - expect(translationTabActiveContentIdService.setActiveContent).not - .toHaveBeenCalled(); + expect( + translationTabActiveContentIdService.setActiveContent + ).not.toHaveBeenCalled(); }); - it('should change active answer group index', () => { - component.onTabClick('feedback'); + it('should change active answer group index', () => { + component.onTabClick('feedback'); - spyOn(translationTabActiveContentIdService, 'setActiveContent'); - component.changeActiveAnswerGroupIndex(1); + spyOn(translationTabActiveContentIdService, 'setActiveContent'); + component.changeActiveAnswerGroupIndex(1); - expect(translationTabActiveContentIdService.setActiveContent) - .toHaveBeenCalledWith('feedback_2', 'html'); - }); + expect( + translationTabActiveContentIdService.setActiveContent + ).toHaveBeenCalledWith('feedback_2', 'html'); + }); - it('should not change active customization argument index if it is equal' + - ' to the current one', - () => { - component.onTabClick('ca'); + it( + 'should not change active customization argument index if it is equal' + + ' to the current one', + () => { + component.onTabClick('ca'); - spyOn(translationTabActiveContentIdService, 'setActiveContent'); - component.changeActiveCustomizationArgContentIndex(0); + spyOn(translationTabActiveContentIdService, 'setActiveContent'); + component.changeActiveCustomizationArgContentIndex(0); - expect(translationTabActiveContentIdService.setActiveContent).not - .toHaveBeenCalled(); - }); + expect( + translationTabActiveContentIdService.setActiveContent + ).not.toHaveBeenCalled(); + } + ); - it('should change active answer group index to default outcome when' + - ' index provided is equal to answer groups length', () => { - component.onTabClick('feedback'); + it( + 'should change active answer group index to default outcome when' + + ' index provided is equal to answer groups length', + () => { + component.onTabClick('feedback'); - spyOn(translationTabActiveContentIdService, 'setActiveContent'); - component.changeActiveAnswerGroupIndex(2); + spyOn(translationTabActiveContentIdService, 'setActiveContent'); + component.changeActiveAnswerGroupIndex(2); - expect(translationTabActiveContentIdService.setActiveContent) - .toHaveBeenCalledWith('default_outcome', 'html'); - }); + expect( + translationTabActiveContentIdService.setActiveContent + ).toHaveBeenCalledWith('default_outcome', 'html'); + } + ); - it('should not change active hint index if it is equal to the current one', - () => { + it('should not change active hint index if it is equal to the current one', () => { component.onTabClick('feedback'); spyOn(translationTabActiveContentIdService, 'setActiveContent'); component.changeActiveAnswerGroupIndex(0); - expect(translationTabActiveContentIdService.setActiveContent).not - .toHaveBeenCalled(); - }); - - it('should get subtitled html data translation', () => { - let subtitledObject = SubtitledHtml.createFromBackendDict({ - content_id: 'content_1', - html: 'This is the html' + expect( + translationTabActiveContentIdService.setActiveContent + ).not.toHaveBeenCalled(); }); - expect(component.getRequiredHtml(subtitledObject)).toBe( - 'This is the html'); - expect(component.getSubtitledContentSummary(subtitledObject)).toBe( - 'This is the html'); - }); - it('should get subtitled Unicode data translation', () => { - let subtitledObject = subtitledUnicodeObjectFactory.createFromBackendDict( - { + it('should get subtitled html data translation', () => { + let subtitledObject = SubtitledHtml.createFromBackendDict({ content_id: 'content_1', - unicode_str: 'This is the unicode' + html: 'This is the html', }); - expect(component.getRequiredUnicode(subtitledObject)).toBe( - 'This is the unicode'); - expect(component.getSubtitledContentSummary(subtitledObject)).toBe( - 'This is the unicode'); - }); + expect(component.getRequiredHtml(subtitledObject)).toBe( + 'This is the html' + ); + expect(component.getSubtitledContentSummary(subtitledObject)).toBe( + 'This is the html' + ); + }); - it('should get empty content message when text translations haven\'t' + - ' been added yet', () => { - expect(component.getEmptyContentMessage()).toBe( - 'The translation for this section has not been created yet.' + - ' Switch to translation mode to add a text translation.'); - }); + it('should get subtitled Unicode data translation', () => { + let subtitledObject = + subtitledUnicodeObjectFactory.createFromBackendDict({ + content_id: 'content_1', + unicode_str: 'This is the unicode', + }); + expect(component.getRequiredUnicode(subtitledObject)).toBe( + 'This is the unicode' + ); + expect(component.getSubtitledContentSummary(subtitledObject)).toBe( + 'This is the unicode' + ); + }); - it('should get summary default outcome when outcome is linear', - () => { - expect(component.summarizeDefaultOutcome( - outcomeObjectFactory.createNew( - 'unused', '1', 'Feedback Text', []), 'Continue', 0, 'true')) - .toBe('[] Feedback Text'); + it( + "should get empty content message when text translations haven't" + + ' been added yet', + () => { + expect(component.getEmptyContentMessage()).toBe( + 'The translation for this section has not been created yet.' + + ' Switch to translation mode to add a text translation.' + ); + } + ); + + it('should get summary default outcome when outcome is linear', () => { + expect( + component.summarizeDefaultOutcome( + outcomeObjectFactory.createNew('unused', '1', 'Feedback Text', []), + 'Continue', + 0, + 'true' + ) + ).toBe('[] Feedback Text'); }); - it('should get summary default outcome when answer group count' + - ' is greater than 0', () => { - expect(component.summarizeDefaultOutcome( - outcomeObjectFactory.createNew( - 'unused', '1', 'Feedback Text', []), 'TextInput', 1, 'true')) - .toBe('[] Feedback Text'); - }); + it( + 'should get summary default outcome when answer group count' + + ' is greater than 0', + () => { + expect( + component.summarizeDefaultOutcome( + outcomeObjectFactory.createNew( + 'unused', + '1', + 'Feedback Text', + [] + ), + 'TextInput', + 1, + 'true' + ) + ).toBe('[] Feedback Text'); + } + ); - it('should get summary default outcome when answer group count' + - ' is equal to 0', () => { - expect(component.summarizeDefaultOutcome( - outcomeObjectFactory.createNew( - 'unused', '1', 'Feedback Text', []), 'TextInput', 0, 'true')) - .toBe('[] Feedback Text'); - }); + it( + 'should get summary default outcome when answer group count' + + ' is equal to 0', + () => { + expect( + component.summarizeDefaultOutcome( + outcomeObjectFactory.createNew( + 'unused', + '1', + 'Feedback Text', + [] + ), + 'TextInput', + 0, + 'true' + ) + ).toBe('[] Feedback Text'); + } + ); - it('should get an empty summary when default outcome is a falsy value', - () => { - expect(component.summarizeDefaultOutcome(null, 'Continue', 0, 'true')) - .toBe(''); + it('should get an empty summary when default outcome is a falsy value', () => { + expect( + component.summarizeDefaultOutcome(null, 'Continue', 0, 'true') + ).toBe(''); }); - it('should get summary answer group', () => { - expect(component.summarizeAnswerGroup( - answerGroupObjectFactory.createNew( - [], - outcomeObjectFactory.createNew('unused', '1', 'Feedback text', []), - null, '0'), '1', null, true)) - .toBe('[] Feedback text'); - }); - }); + it('should get summary answer group', () => { + expect( + component.summarizeAnswerGroup( + answerGroupObjectFactory.createNew( + [], + outcomeObjectFactory.createNew( + 'unused', + '1', + 'Feedback text', + [] + ), + null, + '0' + ), + '1', + null, + true + ) + ).toBe('[] Feedback text'); + }); + } + ); }); describe('State translation component', () => { @@ -678,15 +781,14 @@ describe('State translation component', () => { let stateRecordedVoiceoversService: StateRecordedVoiceoversService; let subtitledUnicodeObjectFactory: SubtitledUnicodeObjectFactory; let translationLanguageService: TranslationLanguageService; - let translationTabActiveContentIdService: - TranslationTabActiveContentIdService; + let translationTabActiveContentIdService: TranslationTabActiveContentIdService; let translationTabActiveModeService: TranslationTabActiveModeService; let explorationState1 = { Introduction: { content: { content_id: 'content_1', - html: 'Introduction Content' + html: 'Introduction Content', }, classifier_model_id: 'null', card_is_checkpoint: false, @@ -697,60 +799,66 @@ describe('State translation component', () => { placeholder: { value: { content_id: 'ca_placeholder', - unicode_str: '' - } + unicode_str: '', + }, }, rows: { - value: 1 - } + value: 1, + }, }, - answer_groups: [{ - training_data: null, - tagged_skill_misconception_id: null, - rule_specs: [{ - rule_type: 'Equals', - inputs: { - x: { - contentId: 'rule_input_4', - normalizedStrSet: ['input1'] - } - } - }, { - rule_type: 'Equals', - inputs: { - x: { - contentId: 'rule_input_5', - normalizedStrSet: ['input2'] - } - } - }], - outcome: { - labelled_as_correct: null, - param_changes: null, - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: 'unused', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_1', - html: '' + answer_groups: [ + { + training_data: null, + tagged_skill_misconception_id: null, + rule_specs: [ + { + rule_type: 'Equals', + inputs: { + x: { + contentId: 'rule_input_4', + normalizedStrSet: ['input1'], + }, + }, + }, + { + rule_type: 'Equals', + inputs: { + x: { + contentId: 'rule_input_5', + normalizedStrSet: ['input2'], + }, + }, + }, + ], + outcome: { + labelled_as_correct: null, + param_changes: null, + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: 'unused', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: '', + }, }, - } - }, { - rule_specs: [], - outcome: { - labelled_as_correct: null, - param_changes: null, - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: 'unused', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: '' + }, + { + rule_specs: [], + outcome: { + labelled_as_correct: null, + param_changes: null, + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: 'unused', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: '', + }, }, - } - }], + }, + ], default_outcome: { dest: 'default', labelled_as_correct: null, @@ -760,7 +868,7 @@ describe('State translation component', () => { dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: 'Default Outcome' + html: 'Default Outcome', }, }, solution: { @@ -768,28 +876,31 @@ describe('State translation component', () => { answer_is_exclusive: false, explanation: { html: 'Solution explanation', - content_id: 'solution_1' - } + content_id: 'solution_1', + }, }, - hints: [{ - hint_content: { - html: 'Hint 1', - content_id: 'hint_1' - } - }, { - hint_content: { - html: 'Hint 2', - content_id: 'hint_2' - } - }] + hints: [ + { + hint_content: { + html: 'Hint 1', + content_id: 'hint_1', + }, + }, + { + hint_content: { + html: 'Hint 2', + content_id: 'hint_2', + }, + }, + ], }, linked_skill_id: null, param_changes: [], solicit_answer_details: false, recorded_voiceovers: { - voiceovers_mapping: {} - } - } + voiceovers_mapping: {}, + }, + }, } as StateObjectsBackendDict; let recordedVoiceovers = { @@ -804,8 +915,8 @@ describe('State translation component', () => { ca_placeholder: {}, ca_fakePlaceholder: {}, rule_input_4: {}, - rule_input_5: {} - } + rule_input_5: {}, + }, }; let refreshStateTranslationEmitter = new EventEmitter(); @@ -825,13 +936,13 @@ describe('State translation component', () => { MockParameterizeRuleDescriptionPipe, MockTruncatePipe, MockConvertToPlainTextPipe, - MockWrapTextWithEllipsisPipe + MockWrapTextWithEllipsisPipe, ], providers: [ WrapTextWithEllipsisPipe, ConvertToPlainTextPipe, AngularNameService, - { provide: ContextService, useClass: MockContextService }, + {provide: ContextService, useClass: MockContextService}, ContinueValidationService, ContinueRulesService, ExplorationImprovementsTaskRegistryService, @@ -853,18 +964,18 @@ describe('State translation component', () => { TranslationTabActiveModeService, { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, { provide: ParameterizeRuleDescriptionPipe, - useClass: MockParameterizeRuleDescriptionPipe + useClass: MockParameterizeRuleDescriptionPipe, }, { provide: WrapTextWithEllipsisPipe, - useClass: MockWrapTextWithEllipsisPipe - } + useClass: MockWrapTextWithEllipsisPipe, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -876,46 +987,59 @@ describe('State translation component', () => { stateEditorService = TestBed.inject(StateEditorService); explorationStatesService = TestBed.inject(ExplorationStatesService); stateRecordedVoiceoversService = TestBed.inject( - StateRecordedVoiceoversService); + StateRecordedVoiceoversService + ); subtitledUnicodeObjectFactory = TestBed.inject( - SubtitledUnicodeObjectFactory); + SubtitledUnicodeObjectFactory + ); translationLanguageService = TestBed.inject(TranslationLanguageService); translationTabActiveContentIdService = TestBed.inject( - TranslationTabActiveContentIdService); + TranslationTabActiveContentIdService + ); translationTabActiveModeService = TestBed.inject( - TranslationTabActiveModeService); + TranslationTabActiveModeService + ); explorationStatesService.init(explorationState1, false); stateRecordedVoiceoversService.init( - 'Introduction', RecordedVoiceovers.createFromBackendDict( - recordedVoiceovers)); + 'Introduction', + RecordedVoiceovers.createFromBackendDict(recordedVoiceovers) + ); entityTranslationsService = TestBed.inject(EntityTranslationsService); entityTranslationsService.init('exp1', 'exploration', 5); - entityTranslationsService.entityTranslation = ( + entityTranslationsService.entityTranslation = EntityTranslation.createFromBackendDict({ entity_id: 'exp1', entity_type: 'exploration', entity_version: 5, language_code: 'hi', - translations: {} - }) - ); - spyOnProperty(stateEditorService, 'onRefreshStateTranslation').and - .returnValue(refreshStateTranslationEmitter); + translations: {}, + }); + spyOnProperty( + stateEditorService, + 'onRefreshStateTranslation' + ).and.returnValue(refreshStateTranslationEmitter); spyOn(stateEditorService, 'getActiveStateName').and.returnValue( - 'Introduction'); + 'Introduction' + ); ckEditorCopyContentService.copyModeActive = true; - spyOn(translationLanguageService, 'getActiveLanguageCode').and - .returnValue('en'); - spyOn(translationTabActiveModeService, 'isVoiceoverModeActive').and - .returnValue(false); + spyOn(translationLanguageService, 'getActiveLanguageCode').and.returnValue( + 'en' + ); + spyOn( + translationTabActiveModeService, + 'isVoiceoverModeActive' + ).and.returnValue(false); explorationStatesService.init(explorationState1, false); stateRecordedVoiceoversService.init( - 'Introduction', RecordedVoiceovers.createFromBackendDict( - recordedVoiceovers)); - spyOnProperty(stateEditorService, 'onShowTranslationTabBusyModal').and - .returnValue(showTranslationTabBusyModalEmitter); + 'Introduction', + RecordedVoiceovers.createFromBackendDict(recordedVoiceovers) + ); + spyOnProperty( + stateEditorService, + 'onShowTranslationTabBusyModal' + ).and.returnValue(showTranslationTabBusyModalEmitter); component.isTranslationTabBusy = true; component.stateName = 'Introduction'; @@ -926,133 +1050,177 @@ describe('State translation component', () => { component.ngOnDestroy(); }); - describe('when translation tab is busy and voiceover mode is not' + - ' activate', () => { - it('should open translation tab busy modal when clicking on content' + - ' tab', () => { - spyOn(showTranslationTabBusyModalEmitter, 'emit'); - spyOn(translationTabActiveContentIdService, 'setActiveContent'); - component.onTabClick('content'); - - expect(showTranslationTabBusyModalEmitter.emit).toHaveBeenCalled(); - expect(component.isVoiceoverModeActive()).toBe(false); - expect(translationTabActiveContentIdService.setActiveContent).not - .toHaveBeenCalled(); - }); - - it('should open translation tab busy modal when clicking on interaction' + - 'customization arguments tab', () => { - spyOn(showTranslationTabBusyModalEmitter, 'emit'); - spyOn(translationTabActiveContentIdService, 'setActiveContent'); - component.onTabClick('ca'); - - expect(showTranslationTabBusyModalEmitter.emit).toHaveBeenCalled(); - expect(translationTabActiveContentIdService.setActiveContent).not - .toHaveBeenCalled(); - }); - - it('should open translation tab busy modal when clicking on feedback' + - ' tab', () => { - spyOn(showTranslationTabBusyModalEmitter, 'emit'); - spyOn(translationTabActiveContentIdService, 'setActiveContent'); - component.onTabClick('feedback'); - - expect(showTranslationTabBusyModalEmitter.emit).toHaveBeenCalled(); - expect(translationTabActiveContentIdService.setActiveContent).not - .toHaveBeenCalled(); - }); - - it('should open translation tab busy modal when clicking on hint' + - ' tab', () => { - spyOn(showTranslationTabBusyModalEmitter, 'emit'); - spyOn(translationTabActiveContentIdService, 'setActiveContent'); - component.onTabClick('hint'); - - expect(showTranslationTabBusyModalEmitter.emit).toHaveBeenCalled(); - expect(translationTabActiveContentIdService.setActiveContent).not - .toHaveBeenCalled(); - }); - - it('should open translation tab busy modal when clicking on solution' + - ' tab', () => { - spyOn(showTranslationTabBusyModalEmitter, 'emit'); - spyOn(translationTabActiveContentIdService, 'setActiveContent'); - component.onTabClick('solution'); - - expect(showTranslationTabBusyModalEmitter.emit).toHaveBeenCalled(); - expect(translationTabActiveContentIdService.setActiveContent).not - .toHaveBeenCalled(); - }); - - it('should open translation tab busy modal when trying to change' + - ' active rule content index', () => { - spyOn(showTranslationTabBusyModalEmitter, 'emit'); - spyOn(translationTabActiveContentIdService, 'setActiveContent'); - component.changeActiveRuleContentIndex(1); + describe( + 'when translation tab is busy and voiceover mode is not' + ' activate', + () => { + it( + 'should open translation tab busy modal when clicking on content' + + ' tab', + () => { + spyOn(showTranslationTabBusyModalEmitter, 'emit'); + spyOn(translationTabActiveContentIdService, 'setActiveContent'); + component.onTabClick('content'); + + expect(showTranslationTabBusyModalEmitter.emit).toHaveBeenCalled(); + expect(component.isVoiceoverModeActive()).toBe(false); + expect( + translationTabActiveContentIdService.setActiveContent + ).not.toHaveBeenCalled(); + } + ); - expect(showTranslationTabBusyModalEmitter.emit).toHaveBeenCalled(); - expect(translationTabActiveContentIdService.setActiveContent).not - .toHaveBeenCalled(); - }); + it( + 'should open translation tab busy modal when clicking on interaction' + + 'customization arguments tab', + () => { + spyOn(showTranslationTabBusyModalEmitter, 'emit'); + spyOn(translationTabActiveContentIdService, 'setActiveContent'); + component.onTabClick('ca'); + + expect(showTranslationTabBusyModalEmitter.emit).toHaveBeenCalled(); + expect( + translationTabActiveContentIdService.setActiveContent + ).not.toHaveBeenCalled(); + } + ); - it('should open translation tab busy modal when trying to change' + - ' active hint index', () => { - spyOn(showTranslationTabBusyModalEmitter, 'emit'); - spyOn(translationTabActiveContentIdService, 'setActiveContent'); - component.changeActiveHintIndex(1); + it( + 'should open translation tab busy modal when clicking on feedback' + + ' tab', + () => { + spyOn(showTranslationTabBusyModalEmitter, 'emit'); + spyOn(translationTabActiveContentIdService, 'setActiveContent'); + component.onTabClick('feedback'); + + expect(showTranslationTabBusyModalEmitter.emit).toHaveBeenCalled(); + expect( + translationTabActiveContentIdService.setActiveContent + ).not.toHaveBeenCalled(); + } + ); - expect(showTranslationTabBusyModalEmitter.emit).toHaveBeenCalled(); - expect(translationTabActiveContentIdService.setActiveContent).not - .toHaveBeenCalled(); - }); + it( + 'should open translation tab busy modal when clicking on hint' + ' tab', + () => { + spyOn(showTranslationTabBusyModalEmitter, 'emit'); + spyOn(translationTabActiveContentIdService, 'setActiveContent'); + component.onTabClick('hint'); + + expect(showTranslationTabBusyModalEmitter.emit).toHaveBeenCalled(); + expect( + translationTabActiveContentIdService.setActiveContent + ).not.toHaveBeenCalled(); + } + ); - it('should open translation tab busy modal when trying to change' + - ' active answer group index', () => { - spyOn(showTranslationTabBusyModalEmitter, 'emit'); - spyOn(translationTabActiveContentIdService, 'setActiveContent'); - component.changeActiveAnswerGroupIndex(1); + it( + 'should open translation tab busy modal when clicking on solution' + + ' tab', + () => { + spyOn(showTranslationTabBusyModalEmitter, 'emit'); + spyOn(translationTabActiveContentIdService, 'setActiveContent'); + component.onTabClick('solution'); + + expect(showTranslationTabBusyModalEmitter.emit).toHaveBeenCalled(); + expect( + translationTabActiveContentIdService.setActiveContent + ).not.toHaveBeenCalled(); + } + ); - expect(showTranslationTabBusyModalEmitter.emit).toHaveBeenCalled(); - expect(translationTabActiveContentIdService.setActiveContent).not - .toHaveBeenCalled(); - }); + it( + 'should open translation tab busy modal when trying to change' + + ' active rule content index', + () => { + spyOn(showTranslationTabBusyModalEmitter, 'emit'); + spyOn(translationTabActiveContentIdService, 'setActiveContent'); + component.changeActiveRuleContentIndex(1); + + expect(showTranslationTabBusyModalEmitter.emit).toHaveBeenCalled(); + expect( + translationTabActiveContentIdService.setActiveContent + ).not.toHaveBeenCalled(); + } + ); - it('should open translation tab busy modal when trying to change' + - ' interaction customization argument index', () => { - spyOn(showTranslationTabBusyModalEmitter, 'emit'); - spyOn(translationTabActiveContentIdService, 'setActiveContent'); - component.changeActiveCustomizationArgContentIndex(0); + it( + 'should open translation tab busy modal when trying to change' + + ' active hint index', + () => { + spyOn(showTranslationTabBusyModalEmitter, 'emit'); + spyOn(translationTabActiveContentIdService, 'setActiveContent'); + component.changeActiveHintIndex(1); + + expect(showTranslationTabBusyModalEmitter.emit).toHaveBeenCalled(); + expect( + translationTabActiveContentIdService.setActiveContent + ).not.toHaveBeenCalled(); + } + ); - expect(showTranslationTabBusyModalEmitter.emit).toHaveBeenCalled(); - expect(translationTabActiveContentIdService.setActiveContent).not - .toHaveBeenCalled(); - }); + it( + 'should open translation tab busy modal when trying to change' + + ' active answer group index', + () => { + spyOn(showTranslationTabBusyModalEmitter, 'emit'); + spyOn(translationTabActiveContentIdService, 'setActiveContent'); + component.changeActiveAnswerGroupIndex(1); + + expect(showTranslationTabBusyModalEmitter.emit).toHaveBeenCalled(); + expect( + translationTabActiveContentIdService.setActiveContent + ).not.toHaveBeenCalled(); + } + ); - it('should get subtitled data', () => { - let subtitledObject = SubtitledHtml.createFromBackendDict({ - content_id: 'content_1', - html: 'This is the html' - }); - expect(component.getRequiredHtml(subtitledObject)) - .toBe('This is the html'); - expect(component.getSubtitledContentSummary(subtitledObject)).toBe( - 'This is the html'); + it( + 'should open translation tab busy modal when trying to change' + + ' interaction customization argument index', + () => { + spyOn(showTranslationTabBusyModalEmitter, 'emit'); + spyOn(translationTabActiveContentIdService, 'setActiveContent'); + component.changeActiveCustomizationArgContentIndex(0); + + expect(showTranslationTabBusyModalEmitter.emit).toHaveBeenCalled(); + expect( + translationTabActiveContentIdService.setActiveContent + ).not.toHaveBeenCalled(); + } + ); - let subtitledObjectBack = subtitledUnicodeObjectFactory - .createFromBackendDict({ + it('should get subtitled data', () => { + let subtitledObject = SubtitledHtml.createFromBackendDict({ content_id: 'content_1', - unicode_str: 'This is the unicode' + html: 'This is the html', }); - expect(component.getSubtitledContentSummary(subtitledObjectBack)).toBe( - 'This is the unicode'); - }); + expect(component.getRequiredHtml(subtitledObject)).toBe( + 'This is the html' + ); + expect(component.getSubtitledContentSummary(subtitledObject)).toBe( + 'This is the html' + ); + + let subtitledObjectBack = + subtitledUnicodeObjectFactory.createFromBackendDict({ + content_id: 'content_1', + unicode_str: 'This is the unicode', + }); + expect(component.getSubtitledContentSummary(subtitledObjectBack)).toBe( + 'This is the unicode' + ); + }); - it('should get content message warning that there is not text available' + - ' to translate', () => { - expect(component.getEmptyContentMessage()).toBe( - 'There is no text available to translate.'); - }); - }); + it( + 'should get content message warning that there is not text available' + + ' to translate', + () => { + expect(component.getEmptyContentMessage()).toBe( + 'There is no text available to translate.' + ); + } + ); + } + ); }); describe('State translation component', () => { @@ -1065,8 +1233,7 @@ describe('State translation component', () => { let stateRecordedVoiceoversService: StateRecordedVoiceoversService; let subtitledUnicodeObjectFactory: SubtitledUnicodeObjectFactory; let translationLanguageService: TranslationLanguageService; - let translationTabActiveContentIdService: - TranslationTabActiveContentIdService; + let translationTabActiveContentIdService: TranslationTabActiveContentIdService; let translationTabActiveModeService: TranslationTabActiveModeService; let routerService: RouterService; @@ -1074,7 +1241,7 @@ describe('State translation component', () => { Introduction: { content: { content_id: 'content_1', - html: 'Introduction Content' + html: 'Introduction Content', }, classifier_model_id: 'null', card_is_checkpoint: false, @@ -1085,60 +1252,66 @@ describe('State translation component', () => { placeholder: { value: { content_id: 'ca_placeholder', - unicode_str: '' - } + unicode_str: '', + }, }, rows: { - value: 1 - } + value: 1, + }, }, - answer_groups: [{ - training_data: null, - tagged_skill_misconception_id: null, - rule_specs: [{ - rule_type: 'Equals', - inputs: { - x: { - contentId: 'rule_input_4', - normalizedStrSet: ['input1'] - } - } - }, { - rule_type: 'Equals', - inputs: { - x: { - contentId: 'rule_input_5', - normalizedStrSet: ['input2'] - } - } - }], - outcome: { - labelled_as_correct: null, - param_changes: null, - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: 'unused', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_1', - html: '' + answer_groups: [ + { + training_data: null, + tagged_skill_misconception_id: null, + rule_specs: [ + { + rule_type: 'Equals', + inputs: { + x: { + contentId: 'rule_input_4', + normalizedStrSet: ['input1'], + }, + }, + }, + { + rule_type: 'Equals', + inputs: { + x: { + contentId: 'rule_input_5', + normalizedStrSet: ['input2'], + }, + }, + }, + ], + outcome: { + labelled_as_correct: null, + param_changes: null, + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: 'unused', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: '', + }, }, - } - }, { - rule_specs: [], - outcome: { - labelled_as_correct: null, - param_changes: null, - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: 'unused', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: '' + }, + { + rule_specs: [], + outcome: { + labelled_as_correct: null, + param_changes: null, + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: 'unused', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: '', + }, }, - } - }], + }, + ], default_outcome: { dest: 'default', labelled_as_correct: null, @@ -1148,7 +1321,7 @@ describe('State translation component', () => { dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: 'Default Outcome' + html: 'Default Outcome', }, }, solution: { @@ -1156,35 +1329,38 @@ describe('State translation component', () => { answer_is_exclusive: false, explanation: { html: 'Solution explanation', - content_id: 'solution_1' - } + content_id: 'solution_1', + }, }, - hints: [{ - hint_content: { - html: 'Hint 1', - content_id: 'hint_1' - } - }, { - hint_content: { - html: 'Hint 2', - content_id: 'hint_2' - } - }] + hints: [ + { + hint_content: { + html: 'Hint 1', + content_id: 'hint_1', + }, + }, + { + hint_content: { + html: 'Hint 2', + content_id: 'hint_2', + }, + }, + ], }, linked_skill_id: null, param_changes: [], solicit_answer_details: false, recorded_voiceovers: { - voiceovers_mapping: {} - } - } + voiceovers_mapping: {}, + }, + }, } as StateObjectsBackendDict; let explorationState2 = { Introduction: { content: { content_id: 'content_1', - html: 'Introduction Content' + html: 'Introduction Content', }, classifier_model_id: 'null', card_is_checkpoint: false, @@ -1195,12 +1371,12 @@ describe('State translation component', () => { placeholder: { value: { content_id: 'ca_placeholder', - unicode_str: '' - } + unicode_str: '', + }, }, rows: { - value: 1 - } + value: 1, + }, }, answer_groups: [], default_outcome: { @@ -1212,7 +1388,7 @@ describe('State translation component', () => { dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: 'Default Outcome' + html: 'Default Outcome', }, }, solution: { @@ -1220,28 +1396,31 @@ describe('State translation component', () => { answer_is_exclusive: false, explanation: { html: 'Solution explanation', - content_id: 'solution_1' - } + content_id: 'solution_1', + }, }, - hints: [{ - hint_content: { - html: 'Hint 1', - content_id: 'hint_1' - } - }, { - hint_content: { - html: 'Hint 2', - content_id: 'hint_2' - } - }] + hints: [ + { + hint_content: { + html: 'Hint 1', + content_id: 'hint_1', + }, + }, + { + hint_content: { + html: 'Hint 2', + content_id: 'hint_2', + }, + }, + ], }, linked_skill_id: null, param_changes: [], solicit_answer_details: false, recorded_voiceovers: { - voiceovers_mapping: {} - } - } + voiceovers_mapping: {}, + }, + }, } as StateObjectsBackendDict; let recordedVoiceovers = { @@ -1256,8 +1435,8 @@ describe('State translation component', () => { ca_placeholder: {}, ca_fakePlaceholder: {}, rule_input_4: {}, - rule_input_5: {} - } + rule_input_5: {}, + }, }; let refreshStateTranslationEmitter = new EventEmitter(); @@ -1276,13 +1455,13 @@ describe('State translation component', () => { MockParameterizeRuleDescriptionPipe, MockTruncatePipe, MockConvertToPlainTextPipe, - MockWrapTextWithEllipsisPipe + MockWrapTextWithEllipsisPipe, ], providers: [ WrapTextWithEllipsisPipe, ConvertToPlainTextPipe, AngularNameService, - { provide: ContextService, useClass: MockContextService }, + {provide: ContextService, useClass: MockContextService}, ContinueValidationService, ContinueRulesService, ExplorationImprovementsTaskRegistryService, @@ -1304,18 +1483,18 @@ describe('State translation component', () => { TranslationTabActiveModeService, { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, { provide: ParameterizeRuleDescriptionPipe, - useClass: MockParameterizeRuleDescriptionPipe + useClass: MockParameterizeRuleDescriptionPipe, }, { provide: WrapTextWithEllipsisPipe, - useClass: MockWrapTextWithEllipsisPipe - } + useClass: MockWrapTextWithEllipsisPipe, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -1327,46 +1506,57 @@ describe('State translation component', () => { stateEditorService = TestBed.inject(StateEditorService); explorationStatesService = TestBed.inject(ExplorationStatesService); stateRecordedVoiceoversService = TestBed.inject( - StateRecordedVoiceoversService); + StateRecordedVoiceoversService + ); subtitledUnicodeObjectFactory = TestBed.inject( - SubtitledUnicodeObjectFactory); + SubtitledUnicodeObjectFactory + ); translationLanguageService = TestBed.inject(TranslationLanguageService); translationTabActiveContentIdService = TestBed.inject( - TranslationTabActiveContentIdService); + TranslationTabActiveContentIdService + ); translationTabActiveModeService = TestBed.inject( - TranslationTabActiveModeService); + TranslationTabActiveModeService + ); routerService = TestBed.inject(RouterService); explorationStatesService.init(explorationState1, false); stateRecordedVoiceoversService.init( - 'Introduction', RecordedVoiceovers.createFromBackendDict( - recordedVoiceovers)); + 'Introduction', + RecordedVoiceovers.createFromBackendDict(recordedVoiceovers) + ); entityTranslationsService = TestBed.inject(EntityTranslationsService); entityTranslationsService.init('exp1', 'exploration', 5); - entityTranslationsService.entityTranslation = ( + entityTranslationsService.entityTranslation = EntityTranslation.createFromBackendDict({ entity_id: 'exp1', entity_type: 'exploration', entity_version: 5, language_code: 'hi', - translations: {} - }) - ); + translations: {}, + }); - spyOnProperty(stateEditorService, 'onRefreshStateTranslation').and - .returnValue(refreshStateTranslationEmitter); + spyOnProperty( + stateEditorService, + 'onRefreshStateTranslation' + ).and.returnValue(refreshStateTranslationEmitter); spyOn(stateEditorService, 'getActiveStateName').and.returnValue( - 'Introduction'); + 'Introduction' + ); ckEditorCopyContentService.copyModeActive = true; - spyOn(translationLanguageService, 'getActiveLanguageCode').and - .returnValue('en'); - spyOn(translationTabActiveModeService, 'isVoiceoverModeActive').and - .returnValue(true); + spyOn(translationLanguageService, 'getActiveLanguageCode').and.returnValue( + 'en' + ); + spyOn( + translationTabActiveModeService, + 'isVoiceoverModeActive' + ).and.returnValue(true); explorationStatesService.init(explorationState2, false); stateRecordedVoiceoversService.init( - 'Introduction', RecordedVoiceovers.createFromBackendDict( - recordedVoiceovers)); + 'Introduction', + RecordedVoiceovers.createFromBackendDict(recordedVoiceovers) + ); component.isTranslationTabBusy = false; component.stateName = 'Introduction'; @@ -1381,64 +1571,69 @@ describe('State translation component', () => { it('should cover all translatable objects', () => { Object.keys(DEFAULT_OBJECT_VALUES).forEach(objName => { - if (objName.indexOf('Translatable') !== 0 || - objName.indexOf('ContentId') !== -1) { + if ( + objName.indexOf('Translatable') !== 0 || + objName.indexOf('ContentId') !== -1 + ) { return; } expect(() => { component.getHumanReadableRuleInputValues( DEFAULT_OBJECT_VALUES[objName], - objName); + objName + ); }).not.toThrowError(); }); }); it('should update correct translation with updateTranslatedContent', () => { component.activeTranslatedContent = new TranslatedContent(); - entityTranslationsService.languageCodeToEntityTranslations.en = ( - new EntityTranslation( - 'entityId', 'entityType', 'entityVersion', 'hi', { - content_0: new TranslatedContent('Translated HTML', 'html', true) - }) - ); + entityTranslationsService.languageCodeToEntityTranslations.en = + new EntityTranslation('entityId', 'entityType', 'entityVersion', 'hi', { + content_0: new TranslatedContent('Translated HTML', 'html', true), + }); - translationTabActiveModeService.isVoiceoverModeActive = ( - jasmine.createSpy().and.returnValue(false) - ); + translationTabActiveModeService.isVoiceoverModeActive = jasmine + .createSpy() + .and.returnValue(false); spyOn( - translationTabActiveContentIdService, 'getActiveContentId' + translationTabActiveContentIdService, + 'getActiveContentId' ).and.returnValue('content_0'); component.updateTranslatedContent(); - expect( - component.activeTranslatedContent.translation).toBe('Translated HTML'); + expect(component.activeTranslatedContent.translation).toBe( + 'Translated HTML' + ); }); it('should format TranslatableSetOfNormalizedString values', () => { - expect(component.getHumanReadableRuleInputValues( - { normalizedStrSet: ['input1', 'input2'], unicodeStrSet: null }, - 'TranslatableSetOfNormalizedString' - )).toEqual('[input1, input2]'); + expect( + component.getHumanReadableRuleInputValues( + {normalizedStrSet: ['input1', 'input2'], unicodeStrSet: null}, + 'TranslatableSetOfNormalizedString' + ) + ).toEqual('[input1, input2]'); }); it('should format TranslatableSetOfUnicodeString values', () => { - expect(component.getHumanReadableRuleInputValues( - { normalizedStrSet: null, unicodeStrSet: ['input1', 'input2'] }, - 'TranslatableSetOfUnicodeString' - )).toEqual('[input1, input2]'); + expect( + component.getHumanReadableRuleInputValues( + {normalizedStrSet: null, unicodeStrSet: ['input1', 'input2']}, + 'TranslatableSetOfUnicodeString' + ) + ).toEqual('[input1, input2]'); }); it('should throw an error on invalid type', () => { expect(() => { - component.getHumanReadableRuleInputValues( - null, - 'InvalidType'); + component.getHumanReadableRuleInputValues(null, 'InvalidType'); }).toThrowError('The InvalidType type is not implemented.'); }); it('should correctly navigate to the given state', () => { - spyOn(routerService, 'navigateToMainTab').and.callFake(() => { }); + spyOn(routerService, 'navigateToMainTab').and.callFake(() => {}); component.navigateToState('new_state'); @@ -1446,102 +1641,90 @@ describe('State translation component', () => { }); it('should return original html when translation tab is active', () => { - spyOn(translationTabActiveModeService, 'isTranslationModeActive').and - .returnValue(true); + spyOn( + translationTabActiveModeService, + 'isTranslationModeActive' + ).and.returnValue(true); const htmlData = component.getRequiredHtml( - new SubtitledHtml('

HTML data

', 'content_0')); + new SubtitledHtml('

HTML data

', 'content_0') + ); expect(htmlData).toBe('

HTML data

'); }); it('should return original html when translation not available', () => { const htmlData = component.getRequiredHtml( - new SubtitledHtml('

HTML data

', 'content_0')); + new SubtitledHtml('

HTML data

', 'content_0') + ); expect(htmlData).toBe('

HTML data

'); }); it('should return unicode when translation tab is active', () => { - spyOn(translationTabActiveModeService, 'isTranslationModeActive').and - .returnValue(true); + spyOn( + translationTabActiveModeService, + 'isTranslationModeActive' + ).and.returnValue(true); let subtitledObject = subtitledUnicodeObjectFactory.createFromBackendDict({ content_id: 'content_1', - unicode_str: 'This is the unicode' + unicode_str: 'This is the unicode', }); const unicodeData = component.getRequiredUnicode(subtitledObject); expect(unicodeData).toBe('This is the unicode'); }); it('should return translation html when translation available', () => { - entityTranslationsService.languageCodeToEntityTranslations.en = ( - new EntityTranslation( - 'entityId', 'entityType', 'entityVersion', 'hi', { - content_0: new TranslatedContent('Translated HTML', 'html', true) - }) - ); + entityTranslationsService.languageCodeToEntityTranslations.en = + new EntityTranslation('entityId', 'entityType', 'entityVersion', 'hi', { + content_0: new TranslatedContent('Translated HTML', 'html', true), + }); const htmlData = component.getRequiredHtml( - new SubtitledHtml('

HTML data

', 'content_0')); + new SubtitledHtml('

HTML data

', 'content_0') + ); expect(htmlData).toBe('Translated HTML'); }); - it('should return unicode when translation is empty in voiceover mode', () =>{ - entityTranslationsService.languageCodeToEntityTranslations.en = ( - new EntityTranslation( - 'entityId', 'entityType', 'entityVersion', 'hi', { - content_0: new TranslatedContent - ( - 'Translated unicode', 'unicode', true - ) - }) - ); + it('should return unicode when translation is empty in voiceover mode', () => { + entityTranslationsService.languageCodeToEntityTranslations.en = + new EntityTranslation('entityId', 'entityType', 'entityVersion', 'hi', { + content_0: new TranslatedContent('Translated unicode', 'unicode', true), + }); let subtitledObject = subtitledUnicodeObjectFactory.createFromBackendDict({ content_id: 'content_1', - unicode_str: 'This is the unicode' + unicode_str: 'This is the unicode', }); const unicodeData = component.getRequiredUnicode(subtitledObject); expect(unicodeData).toBe('This is the unicode'); }); it('should return translation html when translation no available', () => { - entityTranslationsService.languageCodeToEntityTranslations.en = ( - new EntityTranslation( - 'entityId', 'entityType', 'entityVersion', 'hi', { - content_1: new TranslatedContent('Translated HTML', 'html', true) - }) - ); + entityTranslationsService.languageCodeToEntityTranslations.en = + new EntityTranslation('entityId', 'entityType', 'entityVersion', 'hi', { + content_1: new TranslatedContent('Translated HTML', 'html', true), + }); const htmlData = component.getRequiredHtml( - new SubtitledHtml('

HTML data

', 'content_0')); + new SubtitledHtml('

HTML data

', 'content_0') + ); expect(htmlData).toBe('

HTML data

'); }); - it('should return translated unicode in voiceover mode when translation exist' - , () => { - entityTranslationsService.languageCodeToEntityTranslations.en = ( - new EntityTranslation( - 'entityId', - 'entityType', - 'entityVersion', - 'hi', - { - content_1: new TranslatedContent - ( - 'Translated UNICODE', 'unicode', true - ) - }) - ); - let subtitledObject = subtitledUnicodeObjectFactory.createFromBackendDict( - { - content_id: 'content_1', - unicode_str: 'This is the unicode' - }); - const unicodeData = component.getRequiredUnicode(subtitledObject); - expect(unicodeData).toBe('Translated UNICODE'); + it('should return translated unicode in voiceover mode when translation exist', () => { + entityTranslationsService.languageCodeToEntityTranslations.en = + new EntityTranslation('entityId', 'entityType', 'entityVersion', 'hi', { + content_1: new TranslatedContent('Translated UNICODE', 'unicode', true), + }); + let subtitledObject = subtitledUnicodeObjectFactory.createFromBackendDict({ + content_id: 'content_1', + unicode_str: 'This is the unicode', }); + const unicodeData = component.getRequiredUnicode(subtitledObject); + expect(unicodeData).toBe('Translated UNICODE'); + }); describe('when rules input tab is accessed but with no rules', () => { it('should throw an error when there are no rules', () => { @@ -1551,7 +1734,8 @@ describe('State translation component', () => { expect(() => { component.onTabClick('rule_input'); }).toThrowError( - 'Accessed rule input translation tab when there are no rules'); + 'Accessed rule input translation tab when there are no rules' + ); }); it('should throw an error when there are no rules', () => { @@ -1559,21 +1743,26 @@ describe('State translation component', () => { expect(() => { component.onTabClick('rule_input'); }).not.toThrowError( - 'Accessed rule input translation tab when there are no rules'); + 'Accessed rule input translation tab when there are no rules' + ); }); }); describe('when state has default outcome and no answer groups', () => { - it('should activate feedback tab with default outcome when' + - ' clicking on tab', () => { - spyOn(translationTabActiveContentIdService, 'setActiveContent'); - component.onTabClick('feedback'); - - expect(component.isActive('feedback')).toBe(true); - expect(component.isDisabled('feedback')).toBe(false); - expect(translationTabActiveContentIdService.setActiveContent) - .toHaveBeenCalledWith('default_outcome', 'html'); - }); + it( + 'should activate feedback tab with default outcome when' + + ' clicking on tab', + () => { + spyOn(translationTabActiveContentIdService, 'setActiveContent'); + component.onTabClick('feedback'); + + expect(component.isActive('feedback')).toBe(true); + expect(component.isDisabled('feedback')).toBe(false); + expect( + translationTabActiveContentIdService.setActiveContent + ).toHaveBeenCalledWith('default_outcome', 'html'); + } + ); }); describe('when initContentId and initTabName are provided', () => { @@ -1586,16 +1775,16 @@ describe('State translation component', () => { _contentId: 'feedback_27', _html: 'html', contentId: 'feedback_27', - html: 'html' + html: 'html', }, labelledAsCorrect: false, missingPrerequisiteSkillId: null, paramChanges: [], - refresherExplorationId: null + refresherExplorationId: null, }, rules: [], taggedSkillMisconceptionId: null, - trainingData: [] + trainingData: [], }, { outcome: { @@ -1605,16 +1794,16 @@ describe('State translation component', () => { _contentId: 'feedback_28', _html: 'html', contentId: 'feedback_28', - html: 'html' + html: 'html', }, labelledAsCorrect: false, missingPrerequisiteSkillId: null, paramChanges: [], - refresherExplorationId: null + refresherExplorationId: null, }, rules: [], taggedSkillMisconceptionId: null, - trainingData: [] + trainingData: [], }, { outcome: { @@ -1624,66 +1813,67 @@ describe('State translation component', () => { _contentId: 'feedback_29', _html: 'html', contentId: 'feedback_29', - html: 'html' + html: 'html', }, labelledAsCorrect: false, missingPrerequisiteSkillId: null, paramChanges: [], - refresherExplorationId: null + refresherExplorationId: null, }, rules: [], taggedSkillMisconceptionId: null, - trainingData: [] - } + trainingData: [], + }, ]; const mockStateHints = [ { hintContent: { - contentId: 'hint_1' - } + contentId: 'hint_1', + }, }, { hintContent: { - contentId: 'hint_2' - } + contentId: 'hint_2', + }, }, { hintContent: { - contentId: 'hint_3' - } - } + contentId: 'hint_3', + }, + }, ]; const mockinteractionCustomizationArgTranslatableContent = [ { name: 'demo', content: { - contentId: 'ca_1' - } + contentId: 'ca_1', + }, }, { name: 'demo', content: { - contentId: 'ca_2' - } + contentId: 'ca_2', + }, }, { name: 'demo', content: { - contentId: 'ca_3' - } - } + contentId: 'ca_3', + }, + }, ]; it('should return correct index for card of type feedback', () => { - component.stateAnswerGroups = mockStateAnswerGroups as unknown as - AnswerGroup[]; + component.stateAnswerGroups = + mockStateAnswerGroups as unknown as AnswerGroup[]; component.activeTab = 'feedback'; component.initActiveContentId = 'feedback_29'; - spyOn(stateEditorService, 'getInitActiveContentId').and. - returnValue('feedback_29'); + spyOn(stateEditorService, 'getInitActiveContentId').and.returnValue( + 'feedback_29' + ); const index = component.getIndexOfActiveCard(); expect(index).toEqual(2); @@ -1694,21 +1884,23 @@ describe('State translation component', () => { component.activeTab = 'hint'; component.initActiveContentId = 'hint_2'; - spyOn(stateEditorService, 'getInitActiveContentId').and. - returnValue('hint_2'); + spyOn(stateEditorService, 'getInitActiveContentId').and.returnValue( + 'hint_2' + ); const index = component.getIndexOfActiveCard(); expect(index).toEqual(1); }); it('should return correct index for card of type custom args', () => { - component.interactionCustomizationArgTranslatableContent = ( - mockinteractionCustomizationArgTranslatableContent); + component.interactionCustomizationArgTranslatableContent = + mockinteractionCustomizationArgTranslatableContent; component.activeTab = 'ca'; component.initActiveContentId = 'ca_1'; - spyOn(stateEditorService, 'getInitActiveContentId').and. - returnValue('ca_1'); + spyOn(stateEditorService, 'getInitActiveContentId').and.returnValue( + 'ca_1' + ); const index = component.getIndexOfActiveCard(); expect(index).toEqual(0); @@ -1718,23 +1910,24 @@ describe('State translation component', () => { component.activeTab = 'unknown'; component.initActiveContentId = 'unknown_1'; - spyOn(stateEditorService, 'getInitActiveContentId').and. - returnValue('ca_1'); + spyOn(stateEditorService, 'getInitActiveContentId').and.returnValue( + 'ca_1' + ); const index = component.getIndexOfActiveCard(); expect(index).toEqual(0); }); it('should return correct active tab name', () => { - spyOn(stateEditorService, 'getInitActiveContentId').and. - returnValue('content_29'); + spyOn(stateEditorService, 'getInitActiveContentId').and.returnValue( + 'content_29' + ); expect(component.getActiveTab()).toBe('content'); }); it('should return active tab name as null when contentId is null', () => { - spyOn(stateEditorService, 'getInitActiveContentId').and. - returnValue(null); + spyOn(stateEditorService, 'getInitActiveContentId').and.returnValue(null); expect(component.getActiveTab()).toBe(null); }); @@ -1749,8 +1942,7 @@ describe('State translation component', () => { let stateEditorService: StateEditorService; let stateRecordedVoiceoversService: StateRecordedVoiceoversService; let translationLanguageService: TranslationLanguageService; - let translationTabActiveContentIdService: - TranslationTabActiveContentIdService; + let translationTabActiveContentIdService: TranslationTabActiveContentIdService; let translationTabActiveModeService: TranslationTabActiveModeService; let subtitledUnicodeObjectFactory: SubtitledUnicodeObjectFactory; let explorationHtmlFormatterService: ExplorationHtmlFormatterService; @@ -1758,7 +1950,7 @@ describe('State translation component', () => { Introduction: { content: { content_id: 'content_1', - html: 'Introduction Content' + html: 'Introduction Content', }, classifier_model_id: 'null', card_is_checkpoint: false, @@ -1769,63 +1961,69 @@ describe('State translation component', () => { placeholder: { value: { content_id: 'ca_placeholder', - unicode_str: '' - } + unicode_str: '', + }, }, rows: { - value: 1 + value: 1, }, catchMisspellings: { - value: false - } + value: false, + }, }, - answer_groups: [{ - training_data: null, - tagged_skill_misconception_id: null, - rule_specs: [{ - rule_type: 'Equals', - inputs: { - x: { - contentId: 'rule_input_4', - normalizedStrSet: ['input1'] - } - } - }, { - rule_type: 'Equals', - inputs: { - x: { - contentId: 'rule_input_5', - normalizedStrSet: ['input2'] - } - } - }], - outcome: { - labelled_as_correct: null, - param_changes: null, - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: 'unused', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_1', - html: '' + answer_groups: [ + { + training_data: null, + tagged_skill_misconception_id: null, + rule_specs: [ + { + rule_type: 'Equals', + inputs: { + x: { + contentId: 'rule_input_4', + normalizedStrSet: ['input1'], + }, + }, + }, + { + rule_type: 'Equals', + inputs: { + x: { + contentId: 'rule_input_5', + normalizedStrSet: ['input2'], + }, + }, + }, + ], + outcome: { + labelled_as_correct: null, + param_changes: null, + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: 'unused', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: '', + }, }, - } - }, { - rule_specs: [], - outcome: { - labelled_as_correct: null, - param_changes: null, - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - dest: 'unused', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: '' + }, + { + rule_specs: [], + outcome: { + labelled_as_correct: null, + param_changes: null, + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + dest: 'unused', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: '', + }, }, - } - }], + }, + ], default_outcome: { dest: 'default', labelled_as_correct: null, @@ -1835,7 +2033,7 @@ describe('State translation component', () => { dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: 'Default Outcome' + html: 'Default Outcome', }, }, solution: { @@ -1843,28 +2041,31 @@ describe('State translation component', () => { answer_is_exclusive: false, explanation: { html: 'Solution explanation', - content_id: 'solution_1' - } + content_id: 'solution_1', + }, }, - hints: [{ - hint_content: { - html: 'Hint 1', - content_id: 'hint_1' - } - }, { - hint_content: { - html: 'Hint 2', - content_id: 'hint_2' - } - }] + hints: [ + { + hint_content: { + html: 'Hint 1', + content_id: 'hint_1', + }, + }, + { + hint_content: { + html: 'Hint 2', + content_id: 'hint_2', + }, + }, + ], }, linked_skill_id: null, param_changes: [], solicit_answer_details: false, recorded_voiceovers: { - voiceovers_mapping: {} - } - } + voiceovers_mapping: {}, + }, + }, } as StateObjectsBackendDict; let explorationState4 = { @@ -1873,7 +2074,7 @@ describe('State translation component', () => { card_is_checkpoint: null, content: { content_id: 'content_1', - html: 'Introduction Content' + html: 'Introduction Content', }, interaction: { default_outcome: null, @@ -1884,23 +2085,23 @@ describe('State translation component', () => { placeholder: { value: { content_id: '', - unicode_str: '' - } + unicode_str: '', + }, }, rows: { - value: 1 - } + value: 1, + }, }, answer_groups: [], - hints: [] + hints: [], }, linked_skill_id: null, param_changes: [], solicit_answer_details: false, recorded_voiceovers: { - voiceovers_mapping: {} - } - } + voiceovers_mapping: {}, + }, + }, } as StateObjectsBackendDict; let recordedVoiceovers = { @@ -1915,8 +2116,8 @@ describe('State translation component', () => { ca_placeholder: {}, ca_fakePlaceholder: {}, rule_input_4: {}, - rule_input_5: {} - } + rule_input_5: {}, + }, }; let refreshStateTranslationEmitter = new EventEmitter(); @@ -1935,14 +2136,14 @@ describe('State translation component', () => { MockParameterizeRuleDescriptionPipe, MockTruncatePipe, MockConvertToPlainTextPipe, - MockWrapTextWithEllipsisPipe + MockWrapTextWithEllipsisPipe, ], providers: [ WrapTextWithEllipsisPipe, ExplorationHtmlFormatterService, ConvertToPlainTextPipe, AngularNameService, - { provide: ContextService, useClass: MockContextService }, + {provide: ContextService, useClass: MockContextService}, ContinueValidationService, ContinueRulesService, ExplorationImprovementsTaskRegistryService, @@ -1964,18 +2165,18 @@ describe('State translation component', () => { TranslationTabActiveModeService, { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, { provide: ParameterizeRuleDescriptionPipe, - useClass: MockParameterizeRuleDescriptionPipe + useClass: MockParameterizeRuleDescriptionPipe, }, { provide: WrapTextWithEllipsisPipe, - useClass: MockWrapTextWithEllipsisPipe - } + useClass: MockWrapTextWithEllipsisPipe, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -1987,64 +2188,79 @@ describe('State translation component', () => { stateEditorService = TestBed.inject(StateEditorService); explorationStatesService = TestBed.inject(ExplorationStatesService); stateRecordedVoiceoversService = TestBed.inject( - StateRecordedVoiceoversService); + StateRecordedVoiceoversService + ); translationLanguageService = TestBed.inject(TranslationLanguageService); translationTabActiveContentIdService = TestBed.inject( - TranslationTabActiveContentIdService); + TranslationTabActiveContentIdService + ); translationTabActiveModeService = TestBed.inject( - TranslationTabActiveModeService); + TranslationTabActiveModeService + ); explorationStatesService.init(explorationState1, false); stateRecordedVoiceoversService.init( - 'Introduction', RecordedVoiceovers.createFromBackendDict( - recordedVoiceovers)); + 'Introduction', + RecordedVoiceovers.createFromBackendDict(recordedVoiceovers) + ); explorationHtmlFormatterService = TestBed.inject( - ExplorationHtmlFormatterService); + ExplorationHtmlFormatterService + ); subtitledUnicodeObjectFactory = TestBed.inject( - SubtitledUnicodeObjectFactory); - spyOnProperty(stateEditorService, 'onRefreshStateTranslation').and - .returnValue(refreshStateTranslationEmitter); + SubtitledUnicodeObjectFactory + ); + spyOnProperty( + stateEditorService, + 'onRefreshStateTranslation' + ).and.returnValue(refreshStateTranslationEmitter); spyOn(stateEditorService, 'getActiveStateName').and.returnValue( - 'Introduction'); + 'Introduction' + ); ckEditorCopyContentService.copyModeActive = true; - spyOn(translationLanguageService, 'getActiveLanguageCode').and - .returnValue('en'); - spyOn(translationTabActiveModeService, 'isVoiceoverModeActive').and - .returnValue(true); + spyOn(translationLanguageService, 'getActiveLanguageCode').and.returnValue( + 'en' + ); + spyOn( + translationTabActiveModeService, + 'isVoiceoverModeActive' + ).and.returnValue(true); explorationStatesService.init(explorationState4, false); stateRecordedVoiceoversService.init( - 'Introduction', RecordedVoiceovers.createFromBackendDict( - { - voiceovers_mapping: { - content: {}, - default_outcome: {}, - content_1: {}, - feedback_1: {}, - hint_1: {}, - solution: {}, - solution_1: {}, - ca_0: {}, - ca_1: {} - } - })); + 'Introduction', + RecordedVoiceovers.createFromBackendDict({ + voiceovers_mapping: { + content: {}, + default_outcome: {}, + content_1: {}, + feedback_1: {}, + hint_1: {}, + solution: {}, + solution_1: {}, + ca_0: {}, + ca_1: {}, + }, + }) + ); // Because the customization arguments we are passing for testing are // invalid, we will skip getInteractionHtml(), which would error // otherwise. spyOn( - explorationHtmlFormatterService, 'getInteractionHtml' + explorationHtmlFormatterService, + 'getInteractionHtml' ).and.returnValue(''); // These customization arguments are invalid. However, it is required to // test an edge case that could occur in the future (customization // argument value being a dictionary). spyOn( - explorationStatesService, 'getInteractionCustomizationArgsMemento' + explorationStatesService, + 'getInteractionCustomizationArgsMemento' ).and.returnValue({ testCa: { value: { unicode: subtitledUnicodeObjectFactory.createDefault('', 'ca_0'), - html: [SubtitledHtml.createDefault('', 'ca_1')] - } - } + html: [SubtitledHtml.createDefault('', 'ca_1')], + }, + }, }); component.isTranslationTabBusy = false; component.stateName = 'Introduction'; @@ -2057,38 +2273,43 @@ describe('State translation component', () => { component.ngOnDestroy(); }); - describe('when state has a multiple choice interaction with no hints, ' + - 'solution or outcome', () => { - it('should evaluate feedback tab as disabled', () => { - expect(component.isDisabled('feedback')).toBe(true); - }); + describe( + 'when state has a multiple choice interaction with no hints, ' + + 'solution or outcome', + () => { + it('should evaluate feedback tab as disabled', () => { + expect(component.isDisabled('feedback')).toBe(true); + }); - it('should evaluate hint tab as disabled', () => { - expect(component.isDisabled('hint')).toBe(true); - }); + it('should evaluate hint tab as disabled', () => { + expect(component.isDisabled('hint')).toBe(true); + }); - it('should evaluate solution tab as disabled', () => { - expect(component.isDisabled('solution')).toBe(true); - }); + it('should evaluate solution tab as disabled', () => { + expect(component.isDisabled('solution')).toBe(true); + }); - it('should change active customization argument index', () => { - component.onTabClick('ca'); - spyOn(translationTabActiveContentIdService, 'setActiveContent'); + it('should change active customization argument index', () => { + component.onTabClick('ca'); + spyOn(translationTabActiveContentIdService, 'setActiveContent'); - component.changeActiveCustomizationArgContentIndex(1); - expect(translationTabActiveContentIdService.setActiveContent) - .toHaveBeenCalledWith('ca_1', 'html'); + component.changeActiveCustomizationArgContentIndex(1); + expect( + translationTabActiveContentIdService.setActiveContent + ).toHaveBeenCalledWith('ca_1', 'html'); - component.changeActiveCustomizationArgContentIndex(0); - expect(translationTabActiveContentIdService.setActiveContent) - .toHaveBeenCalledWith('ca_0', 'unicode'); - }); + component.changeActiveCustomizationArgContentIndex(0); + expect( + translationTabActiveContentIdService.setActiveContent + ).toHaveBeenCalledWith('ca_0', 'unicode'); + }); - it('should isDisabled return true when stateinteractionId is null', () => { - component.TAB_ID_CONTENT = 'some_id'; - component.stateInteractionId = null; + it('should isDisabled return true when stateinteractionId is null', () => { + component.TAB_ID_CONTENT = 'some_id'; + component.stateInteractionId = null; - expect(component.isDisabled('any')).toBeTrue(); - }); - }); + expect(component.isDisabled('any')).toBeTrue(); + }); + } + ); }); diff --git a/core/templates/pages/exploration-editor-page/translation-tab/state-translation/state-translation.component.ts b/core/templates/pages/exploration-editor-page/translation-tab/state-translation/state-translation.component.ts index f6c796809af6..6240725221cb 100644 --- a/core/templates/pages/exploration-editor-page/translation-tab/state-translation/state-translation.component.ts +++ b/core/templates/pages/exploration-editor-page/translation-tab/state-translation/state-translation.component.ts @@ -16,44 +16,51 @@ * @fileoverview Component containing the exploration material to be translated. */ -import { Component, Input, OnDestroy, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { Subscription } from 'rxjs'; -import { AppConstants } from 'app.constants'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { SubtitledUnicode } from 'domain/exploration/SubtitledUnicodeObjectFactory'; -import { TRANSLATION_DATA_FORMAT_HTML, TRANSLATION_DATA_FORMAT_UNICODE, TRANSLATION_DATA_FORMAT_SET_OF_NORMALIZED_STRING, TRANSLATION_DATA_FORMAT_SET_OF_UNICODE_STRING } from 'domain/exploration/WrittenTranslationObjectFactory'; -import { InteractionCustomizationArgs } from 'interactions/customization-args-defs'; -import { Rule } from 'domain/exploration/rule.model'; -import { CkEditorCopyContentService } from 'components/ck-editor-helpers/ck-editor-copy-content.service'; -import { AnswerChoice, StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; -import { RouterService } from 'pages/exploration-editor-page/services/router.service'; -import { ExplorationHtmlFormatterService } from 'services/exploration-html-formatter.service'; -import { TranslationStatusService } from '../services/translation-status.service'; -import { TranslationTabActiveContentIdService } from '../services/translation-tab-active-content-id.service'; -import { TranslationTabActiveModeService } from '../services/translation-tab-active-mode.service'; -import { FormatRtePreviewPipe } from 'filters/format-rte-preview.pipe'; +import {Component, Input, OnDestroy, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {Subscription} from 'rxjs'; +import {AppConstants} from 'app.constants'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {SubtitledUnicode} from 'domain/exploration/SubtitledUnicodeObjectFactory'; +import { + TRANSLATION_DATA_FORMAT_HTML, + TRANSLATION_DATA_FORMAT_UNICODE, + TRANSLATION_DATA_FORMAT_SET_OF_NORMALIZED_STRING, + TRANSLATION_DATA_FORMAT_SET_OF_UNICODE_STRING, +} from 'domain/exploration/WrittenTranslationObjectFactory'; +import {InteractionCustomizationArgs} from 'interactions/customization-args-defs'; +import {Rule} from 'domain/exploration/rule.model'; +import {CkEditorCopyContentService} from 'components/ck-editor-helpers/ck-editor-copy-content.service'; +import { + AnswerChoice, + StateEditorService, +} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; +import {RouterService} from 'pages/exploration-editor-page/services/router.service'; +import {ExplorationHtmlFormatterService} from 'services/exploration-html-formatter.service'; +import {TranslationStatusService} from '../services/translation-status.service'; +import {TranslationTabActiveContentIdService} from '../services/translation-tab-active-content-id.service'; +import {TranslationTabActiveModeService} from '../services/translation-tab-active-mode.service'; +import {FormatRtePreviewPipe} from 'filters/format-rte-preview.pipe'; import INTERACTION_SPECS from 'interactions/interaction_specs.json'; -import { Outcome } from 'domain/exploration/OutcomeObjectFactory'; -import { ConvertToPlainTextPipe } from 'filters/string-utility-filters/convert-to-plain-text.pipe'; -import { TruncatePipe } from 'filters/string-utility-filters/truncate.pipe'; -import { WrapTextWithEllipsisPipe } from 'filters/string-utility-filters/wrap-text-with-ellipsis.pipe'; -import { ParameterizeRuleDescriptionPipe } from 'filters/parameterize-rule-description.pipe'; -import { AnswerGroup } from 'domain/exploration/AnswerGroupObjectFactory'; -import { BaseTranslatableObject } from 'interactions/rule-input-defs'; -import { Hint } from 'domain/exploration/hint-object.model'; -import { Solution } from 'domain/exploration/SolutionObjectFactory'; -import { EntityTranslationsService } from 'services/entity-translations.services'; -import { TranslatedContent } from 'domain/exploration/TranslatedContentObjectFactory'; -import { TranslationLanguageService } from '../services/translation-language.service'; +import {Outcome} from 'domain/exploration/OutcomeObjectFactory'; +import {ConvertToPlainTextPipe} from 'filters/string-utility-filters/convert-to-plain-text.pipe'; +import {TruncatePipe} from 'filters/string-utility-filters/truncate.pipe'; +import {WrapTextWithEllipsisPipe} from 'filters/string-utility-filters/wrap-text-with-ellipsis.pipe'; +import {ParameterizeRuleDescriptionPipe} from 'filters/parameterize-rule-description.pipe'; +import {AnswerGroup} from 'domain/exploration/AnswerGroupObjectFactory'; +import {BaseTranslatableObject} from 'interactions/rule-input-defs'; +import {Hint} from 'domain/exploration/hint-object.model'; +import {Solution} from 'domain/exploration/SolutionObjectFactory'; +import {EntityTranslationsService} from 'services/entity-translations.services'; +import {TranslatedContent} from 'domain/exploration/TranslatedContentObjectFactory'; +import {TranslationLanguageService} from '../services/translation-language.service'; @Component({ selector: 'oppia-state-translation', - templateUrl: './state-translation.component.html' + templateUrl: './state-translation.component.html', }) -export class StateTranslationComponent - implements OnInit, OnDestroy { +export class StateTranslationComponent implements OnInit, OnDestroy { @Input() isTranslationTabBusy: boolean; directiveSubscriptions = new Subscription(); @@ -87,12 +94,14 @@ export class StateTranslationComponent initActiveContentId: string | null; initActiveIndex: number; interactionRuleTranslatableContents: { - rule: Rule; inputName: string; contentId: string; + rule: Rule; + inputName: string; + contentId: string; }[]; interactionCustomizationArgTranslatableContent: { name: string; - content: (SubtitledUnicode | SubtitledHtml); + content: SubtitledUnicode | SubtitledHtml; }[]; constructor( @@ -104,18 +113,17 @@ export class StateTranslationComponent private entityTranslationsService: EntityTranslationsService, private translationLanguageService: TranslationLanguageService, private translationStatusService: TranslationStatusService, - private translationTabActiveContentIdService: - TranslationTabActiveContentIdService, + private translationTabActiveContentIdService: TranslationTabActiveContentIdService, private translationTabActiveModeService: TranslationTabActiveModeService, private formatRtePreviewPipe: FormatRtePreviewPipe, private ConvertToPlainTextPipe: ConvertToPlainTextPipe, private truncatePipe: TruncatePipe, private wrapTextWithEllipsisPipe: WrapTextWithEllipsisPipe, - private parameterizeRuleDescriptionPipe: ParameterizeRuleDescriptionPipe, - ) { } + private parameterizeRuleDescriptionPipe: ParameterizeRuleDescriptionPipe + ) {} isVoiceoverModeActive(): boolean { - return (this.translationTabActiveModeService.isVoiceoverModeActive()); + return this.translationTabActiveModeService.isVoiceoverModeActive(); } getRequiredHtml(subtitledHtml: SubtitledHtml): string { @@ -125,18 +133,17 @@ export class StateTranslationComponent let langCode = this.translationLanguageService.getActiveLanguageCode(); if ( - !this.entityTranslationsService - .languageCodeToEntityTranslations.hasOwnProperty(langCode) + !this.entityTranslationsService.languageCodeToEntityTranslations.hasOwnProperty( + langCode + ) ) { return subtitledHtml.html; } - let translationContent = ( - this.entityTranslationsService - .languageCodeToEntityTranslations[langCode].getWrittenTranslation( - subtitledHtml.contentId - ) - ); + let translationContent = + this.entityTranslationsService.languageCodeToEntityTranslations[ + langCode + ].getWrittenTranslation(subtitledHtml.contentId); if (!translationContent) { return subtitledHtml.html; } @@ -151,18 +158,17 @@ export class StateTranslationComponent let langCode = this.translationLanguageService.getActiveLanguageCode(); if ( - !this.entityTranslationsService - .languageCodeToEntityTranslations.hasOwnProperty(langCode) + !this.entityTranslationsService.languageCodeToEntityTranslations.hasOwnProperty( + langCode + ) ) { return SubtitledUnicode.unicode; } - let translationContent = ( - this.entityTranslationsService - .languageCodeToEntityTranslations[langCode].getWrittenTranslation( - SubtitledUnicode.contentId - ) - ); + let translationContent = + this.entityTranslationsService.languageCodeToEntityTranslations[ + langCode + ].getWrittenTranslation(SubtitledUnicode.contentId); if (!translationContent) { return SubtitledUnicode.unicode; } @@ -174,14 +180,15 @@ export class StateTranslationComponent if (this.translationTabActiveModeService.isVoiceoverModeActive()) { return ( 'The translation for this section has not been created yet. ' + - 'Switch to translation mode to add a text translation.'); + 'Switch to translation mode to add a text translation.' + ); } else { return 'There is no text available to translate.'; } } isActive(tabId: string): boolean { - return (this.activatedTabId === tabId); + return this.activatedTabId === tabId; } navigateToState(stateName: string): void { @@ -218,23 +225,19 @@ export class StateTranslationComponent } else if (tabId === this.TAB_ID_FEEDBACK) { this.activeAnswerGroupIndex = this.initActiveIndex; if (this.stateAnswerGroups.length > 0) { - activeContentId = ( - this.stateAnswerGroups[0].outcome.feedback.contentId); + activeContentId = this.stateAnswerGroups[0].outcome.feedback.contentId; } else { - activeContentId = ( - this.stateDefaultOutcome.feedback.contentId); + activeContentId = this.stateDefaultOutcome.feedback.contentId; } } else if (tabId === this.TAB_ID_HINTS) { this.activeHintIndex = this.initActiveIndex; - activeContentId = ( - this.stateHints[0].hintContent.contentId); + activeContentId = this.stateHints[0].hintContent.contentId; } else if (tabId === this.TAB_ID_SOLUTION) { activeContentId = (this.stateSolution as Solution).explanation.contentId; } else if (tabId === this.TAB_ID_CUSTOMIZATION_ARGS) { this.activeCustomizationArgContentIndex = this.initActiveIndex; - const activeContent = ( - this.interactionCustomizationArgTranslatableContent[0].content - ); + const activeContent = + this.interactionCustomizationArgTranslatableContent[0].content; activeContentId = activeContent.contentId; if (activeContent instanceof SubtitledUnicode) { activeDataFormat = TRANSLATION_DATA_FORMAT_UNICODE; @@ -242,21 +245,23 @@ export class StateTranslationComponent } else if (tabId === this.TAB_ID_RULE_INPUTS) { if (this.interactionRuleTranslatableContents.length === 0) { throw new Error( - 'Accessed rule input translation tab when there are no rules'); + 'Accessed rule input translation tab when there are no rules' + ); } // Note that only 'TextInput' and 'SetInput' have translatable rule // types. The rules tab is disabled for other interactions. - const { - rule, inputName, contentId - } = this.interactionRuleTranslatableContents[0]; + const {rule, inputName, contentId} = + this.interactionRuleTranslatableContents[0]; activeContentId = contentId; const inputType = rule.inputTypes[inputName]; activeDataFormat = this.RULE_INPUT_TYPES_TO_DATA_FORMATS[inputType]; this.activeRuleContentIndex = 0; } this.translationTabActiveContentIdService.setActiveContent( - activeContentId, activeDataFormat); + activeContentId, + activeDataFormat + ); this.activatedTabId = tabId; this.updateTranslatedContent(); } @@ -264,34 +269,36 @@ export class StateTranslationComponent updateTranslatedContent(): void { if (!this.translationTabActiveModeService.isVoiceoverModeActive()) { let langCode = this.translationLanguageService.getActiveLanguageCode(); - const entityTranslations = ( + const entityTranslations = this.entityTranslationsService.languageCodeToEntityTranslations[ - langCode] - ); + langCode + ]; if (entityTranslations) { this.activeTranslatedContent = entityTranslations.getWrittenTranslation( - this.translationTabActiveContentIdService.getActiveContentId()); + this.translationTabActiveContentIdService.getActiveContentId() + ); } } } getHumanReadableRuleInputValues( - inputValue: { normalizedStrSet: string[]; unicodeStrSet: string[] }, - inputType: string): string { + inputValue: {normalizedStrSet: string[]; unicodeStrSet: string[]}, + inputType: string + ): string { if (inputType === 'TranslatableSetOfNormalizedString') { - return ('[' + inputValue.normalizedStrSet.join(', ') + ']'); + return '[' + inputValue.normalizedStrSet.join(', ') + ']'; } else if (inputType === 'TranslatableSetOfUnicodeString') { - return ('[' + inputValue.unicodeStrSet.join(', ') + ']'); + return '[' + inputValue.unicodeStrSet.join(', ') + ']'; } else { throw new Error(`The ${inputType} type is not implemented.`); } } summarizeDefaultOutcome( - defaultOutcome: Outcome, - interactionId: string, - answerGroupCount: number, - shortenRule: string + defaultOutcome: Outcome, + interactionId: string, + answerGroupCount: number, + shortenRule: string ): string { if (!defaultOutcome) { return ''; @@ -301,8 +308,7 @@ export class StateTranslationComponent let hasFeedback = defaultOutcome.hasNonemptyFeedback(); if (interactionId && INTERACTION_SPECS[interactionId].is_linear) { - summary = - INTERACTION_SPECS[interactionId].default_outcome_heading; + summary = INTERACTION_SPECS[interactionId].default_outcome_heading; } else if (answerGroupCount > 0) { summary = 'All other answers'; } else { @@ -311,23 +317,26 @@ export class StateTranslationComponent if (hasFeedback && shortenRule) { summary = this.wrapTextWithEllipsisPipe.transform( - summary, AppConstants.RULE_SUMMARY_WRAP_CHARACTER_COUNT); + summary, + AppConstants.RULE_SUMMARY_WRAP_CHARACTER_COUNT + ); } summary = '[' + summary + '] '; if (hasFeedback) { summary += this.ConvertToPlainTextPipe.transform( - defaultOutcome.feedback.html); + defaultOutcome.feedback.html + ); } return summary; } summarizeAnswerGroup( - answerGroup: AnswerGroup, - interactionId: string, - answerChoices: AnswerChoice[], - shortenRule: boolean + answerGroup: AnswerGroup, + interactionId: string, + answerChoices: AnswerChoice[], + shortenRule: boolean ): string { let summary = ''; let outcome = answerGroup.outcome; @@ -336,21 +345,26 @@ export class StateTranslationComponent if (answerGroup.rules) { let firstRule = this.ConvertToPlainTextPipe.transform( this.parameterizeRuleDescriptionPipe.transform( - answerGroup.rules[0], interactionId, answerChoices)); + answerGroup.rules[0], + interactionId, + answerChoices + ) + ); summary = 'Answer ' + firstRule; if (hasFeedback && shortenRule) { summary = this.wrapTextWithEllipsisPipe.transform( - summary, AppConstants.RULE_SUMMARY_WRAP_CHARACTER_COUNT); + summary, + AppConstants.RULE_SUMMARY_WRAP_CHARACTER_COUNT + ); } summary = '[' + summary + '] '; } if (hasFeedback) { - summary += ( - shortenRule ? - this.truncatePipe.transform(outcome.feedback.html, 30) : - this.ConvertToPlainTextPipe.transform(outcome.feedback.html)); + summary += shortenRule + ? this.truncatePipe.transform(outcome.feedback.html, 30) + : this.ConvertToPlainTextPipe.transform(outcome.feedback.html); } return summary; } @@ -365,11 +379,10 @@ export class StateTranslationComponent // translatable customization arguments -- e.g. Continue // interaction's placeholder. if ( - tabId !== this.TAB_ID_CUSTOMIZATION_ARGS && ( - !this.stateInteractionId || + tabId !== this.TAB_ID_CUSTOMIZATION_ARGS && + (!this.stateInteractionId || INTERACTION_SPECS[this.stateInteractionId].is_linear || - INTERACTION_SPECS[this.stateInteractionId].is_terminal - ) + INTERACTION_SPECS[this.stateInteractionId].is_terminal) ) { return true; } else if (tabId === this.TAB_ID_FEEDBACK) { @@ -391,8 +404,7 @@ export class StateTranslationComponent return false; } } else if (tabId === this.TAB_ID_CUSTOMIZATION_ARGS) { - return ( - this.interactionCustomizationArgTranslatableContent.length === 0); + return this.interactionCustomizationArgTranslatableContent.length === 0; } else if (tabId === this.TAB_ID_RULE_INPUTS) { return this.interactionRuleTranslatableContents.length === 0; } @@ -409,10 +421,11 @@ export class StateTranslationComponent } this.activeHintIndex = newIndex; - let activeContentId = ( - this.stateHints[newIndex].hintContent.contentId); + let activeContentId = this.stateHints[newIndex].hintContent.contentId; this.translationTabActiveContentIdService.setActiveContent( - activeContentId, TRANSLATION_DATA_FORMAT_HTML); + activeContentId, + TRANSLATION_DATA_FORMAT_HTML + ); this.updateTranslatedContent(); } @@ -424,15 +437,16 @@ export class StateTranslationComponent if (this.activeRuleContentIndex === newIndex) { return; } - const { - rule, inputName, contentId - } = this.interactionRuleTranslatableContents[newIndex]; + const {rule, inputName, contentId} = + this.interactionRuleTranslatableContents[newIndex]; const activeContentId = contentId; const inputType = rule.inputTypes[inputName]; const activeDataFormat = this.RULE_INPUT_TYPES_TO_DATA_FORMATS[inputType]; this.translationTabActiveContentIdService.setActiveContent( - activeContentId, activeDataFormat); + activeContentId, + activeDataFormat + ); this.activeRuleContentIndex = newIndex; this.updateTranslatedContent(); } @@ -447,10 +461,8 @@ export class StateTranslationComponent return; } - const activeContent = ( - this.interactionCustomizationArgTranslatableContent[ - newIndex].content - ); + const activeContent = + this.interactionCustomizationArgTranslatableContent[newIndex].content; const activeContentId = activeContent.contentId; let activeDataFormat = null; @@ -461,7 +473,9 @@ export class StateTranslationComponent } this.translationTabActiveContentIdService.setActiveContent( - activeContentId, activeDataFormat); + activeContentId, + activeDataFormat + ); this.activeCustomizationArgContentIndex = newIndex; this.updateTranslatedContent(); } @@ -476,49 +490,54 @@ export class StateTranslationComponent let activeContentId = null; this.activeAnswerGroupIndex = newIndex; if (newIndex === this.stateAnswerGroups.length) { - activeContentId = ( - this.stateDefaultOutcome.feedback.contentId); + activeContentId = this.stateDefaultOutcome.feedback.contentId; } else { - activeContentId = ( - this.stateAnswerGroups[newIndex] - .outcome.feedback.contentId); + activeContentId = + this.stateAnswerGroups[newIndex].outcome.feedback.contentId; } this.translationTabActiveContentIdService.setActiveContent( - activeContentId, TRANSLATION_DATA_FORMAT_HTML); + activeContentId, + TRANSLATION_DATA_FORMAT_HTML + ); } this.updateTranslatedContent(); } tabStatusColorStyle(tabId: string): object { if (!this.isDisabled(tabId)) { - let color = this.translationStatusService - .getActiveStateComponentStatusColor(tabId); - return { 'border-top-color': color }; + let color = + this.translationStatusService.getActiveStateComponentStatusColor(tabId); + return {'border-top-color': color}; } } tabNeedUpdatesStatus(tabId: string): boolean { if (!this.isDisabled(tabId)) { - return this.translationStatusService - .getActiveStateComponentNeedsUpdateStatus(tabId); + return this.translationStatusService.getActiveStateComponentNeedsUpdateStatus( + tabId + ); } } contentIdNeedUpdates(contentId: string): boolean { - return this.translationStatusService - .getActiveStateContentIdNeedsUpdateStatus(contentId); + return this.translationStatusService.getActiveStateContentIdNeedsUpdateStatus( + contentId + ); } contentIdStatusColorStyle(contentId: string): object { - let color = this.translationStatusService - .getActiveStateContentIdStatusColor(contentId); + let color = + this.translationStatusService.getActiveStateContentIdStatusColor( + contentId + ); - return { 'border-left': '3px solid ' + color }; + return {'border-left': '3px solid ' + color}; } getSubtitledContentSummary( - subtitledContent: SubtitledHtml | SubtitledUnicode): string { + subtitledContent: SubtitledHtml | SubtitledUnicode + ): string { if (subtitledContent instanceof SubtitledHtml) { return this.formatRtePreviewPipe.transform(subtitledContent.html); } else if (subtitledContent instanceof SubtitledUnicode) { @@ -527,10 +546,13 @@ export class StateTranslationComponent } getInteractionRuleTranslatableContents(): { - rule: Rule; inputName: string; contentId: string; + rule: Rule; + inputName: string; + contentId: string; }[] { - const allRules = this.stateAnswerGroups.map( - answerGroup => answerGroup.rules).flat(); + const allRules = this.stateAnswerGroups + .map(answerGroup => answerGroup.rules) + .flat(); const interactionRuleTranslatableContent = []; allRules.forEach(rule => { @@ -542,7 +564,9 @@ export class StateTranslationComponent if (ruleInput && ruleInput.hasOwnProperty('contentId')) { const contentId = (ruleInput as BaseTranslatableObject).contentId; interactionRuleTranslatableContent.push({ - rule, inputName, contentId + rule, + inputName, + contentId, }); } }); @@ -552,11 +576,11 @@ export class StateTranslationComponent } getInteractionCustomizationArgTranslatableContents( - customizationArgs: InteractionCustomizationArgs - ): { name: string; content: SubtitledUnicode | SubtitledHtml }[] { + customizationArgs: InteractionCustomizationArgs + ): {name: string; content: SubtitledUnicode | SubtitledHtml}[] { const translatableContents = []; - const camelCaseToSentenceCase = (s) => { + const camelCaseToSentenceCase = s => { // Lowercase the first letter (edge case for UpperCamelCase). s = s.charAt(0).toLowerCase() + s.slice(1); // Add a space in front of capital letters. @@ -567,24 +591,24 @@ export class StateTranslationComponent }; const traverseValueAndRetrieveSubtitledContent = ( - name: string, - value: Object[] | Object, + name: string, + value: Object[] | Object ): void => { - if (value instanceof SubtitledUnicode || - value instanceof SubtitledHtml - ) { + if (value instanceof SubtitledUnicode || value instanceof SubtitledHtml) { translatableContents.push({ - name, content: value + name, + content: value, }); } else if (value instanceof Array) { - value.forEach( - (element, index) => traverseValueAndRetrieveSubtitledContent( + value.forEach((element, index) => + traverseValueAndRetrieveSubtitledContent( `${name} (${index})`, - element) + element + ) ); } else if (value instanceof Object) { - Object.keys(value).forEach( - key => traverseValueAndRetrieveSubtitledContent( + Object.keys(value).forEach(key => + traverseValueAndRetrieveSubtitledContent( `${name} > ${camelCaseToSentenceCase(key)}`, value[key] ) @@ -592,22 +616,24 @@ export class StateTranslationComponent } }; - Object.keys(customizationArgs).forEach( - caName => traverseValueAndRetrieveSubtitledContent( + Object.keys(customizationArgs).forEach(caName => + traverseValueAndRetrieveSubtitledContent( camelCaseToSentenceCase(caName), - customizationArgs[caName].value)); + customizationArgs[caName].value + ) + ); return translatableContents; } getActiveTab(): string { - this.initActiveContentId = this.stateEditorService. - getInitActiveContentId(); + this.initActiveContentId = this.stateEditorService.getInitActiveContentId(); if (!this.initActiveContentId) { return null; } - const tabName = this.stateEditorService.getInitActiveContentId(). - split('_')[0]; + const tabName = this.stateEditorService + .getInitActiveContentId() + .split('_')[0]; return tabName === 'default' ? this.TAB_ID_FEEDBACK : tabName; } @@ -619,69 +645,84 @@ export class StateTranslationComponent const tabName = this.activeTab; switch (tabName) { case this.TAB_ID_FEEDBACK: - return this.stateAnswerGroups.findIndex ( - card => card.outcome.feedback.contentId === this. - initActiveContentId); + return this.stateAnswerGroups.findIndex( + card => card.outcome.feedback.contentId === this.initActiveContentId + ); case this.TAB_ID_HINTS: return this.stateHints.findIndex( - card => card.hintContent.contentId === this. - initActiveContentId); + card => card.hintContent.contentId === this.initActiveContentId + ); case this.TAB_ID_CUSTOMIZATION_ARGS: - return ( - this.interactionCustomizationArgTranslatableContent - .findIndex(card => card.content.contentId === ( - this.initActiveContentId))); + return this.interactionCustomizationArgTranslatableContent.findIndex( + card => card.content.contentId === this.initActiveContentId + ); default: return 0; } } - initStateTranslation(): void { this.stateName = this.stateEditorService.getActiveStateName(); - this.stateContent = this.explorationStatesService - .getStateContentMemento(this.stateName); - this.stateSolution = this.explorationStatesService - .getSolutionMemento(this.stateName); - this.stateHints = this.explorationStatesService - .getHintsMemento(this.stateName); - this.stateAnswerGroups = this.explorationStatesService - .getInteractionAnswerGroupsMemento(this.stateName); - this.stateDefaultOutcome = this.explorationStatesService - .getInteractionDefaultOutcomeMemento(this.stateName); - this.stateInteractionId = this.explorationStatesService - .getInteractionIdMemento(this.stateName); - this.stateInteractionCustomizationArgs = this.explorationStatesService - .getInteractionCustomizationArgsMemento(this.stateName); + this.stateContent = this.explorationStatesService.getStateContentMemento( + this.stateName + ); + this.stateSolution = this.explorationStatesService.getSolutionMemento( + this.stateName + ); + this.stateHints = this.explorationStatesService.getHintsMemento( + this.stateName + ); + this.stateAnswerGroups = + this.explorationStatesService.getInteractionAnswerGroupsMemento( + this.stateName + ); + this.stateDefaultOutcome = + this.explorationStatesService.getInteractionDefaultOutcomeMemento( + this.stateName + ); + this.stateInteractionId = + this.explorationStatesService.getInteractionIdMemento(this.stateName); + this.stateInteractionCustomizationArgs = + this.explorationStatesService.getInteractionCustomizationArgsMemento( + this.stateName + ); this.activeHintIndex = null; this.activeAnswerGroupIndex = null; - let currentCustomizationArgs = this.explorationStatesService - .getInteractionCustomizationArgsMemento(this.stateName); + let currentCustomizationArgs = + this.explorationStatesService.getInteractionCustomizationArgsMemento( + this.stateName + ); this.answerChoices = this.stateEditorService.getAnswerChoices( - this.stateInteractionId, currentCustomizationArgs); - this.interactionPreviewHtml = ( - this.stateInteractionId ? ( - this.explorationHtmlFormatterService.getInteractionHtml( + this.stateInteractionId, + currentCustomizationArgs + ); + this.interactionPreviewHtml = this.stateInteractionId + ? this.explorationHtmlFormatterService.getInteractionHtml( this.stateInteractionId, - this.stateInteractionCustomizationArgs, false, null, null) - ) : ''); - this.interactionCustomizationArgTranslatableContent = ( + this.stateInteractionCustomizationArgs, + false, + null, + null + ) + : ''; + this.interactionCustomizationArgTranslatableContent = this.getInteractionCustomizationArgTranslatableContents( - this.stateInteractionCustomizationArgs) - ); - this.interactionRuleTranslatableContents = ( - this.getInteractionRuleTranslatableContents()); + this.stateInteractionCustomizationArgs + ); + this.interactionRuleTranslatableContents = + this.getInteractionRuleTranslatableContents(); if (this.translationTabActiveModeService.isVoiceoverModeActive()) { - this.needsUpdateTooltipMessage = 'Audio needs update to ' + - 'match text. Please record new audio.'; + this.needsUpdateTooltipMessage = + 'Audio needs update to ' + 'match text. Please record new audio.'; } else { - this.needsUpdateTooltipMessage = 'Translation needs update ' + + this.needsUpdateTooltipMessage = + 'Translation needs update ' + 'to match text. Please re-translate the content.'; } - this.isDisabled(this.activeTab) || !this.activeTab ? - this.onTabClick(this.TAB_ID_CONTENT) : this.onTabClick(this.activeTab); - + this.isDisabled(this.activeTab) || !this.activeTab + ? this.onTabClick(this.TAB_ID_CONTENT) + : this.onTabClick(this.activeTab); this.updateTranslatedContent(); } @@ -690,10 +731,10 @@ export class StateTranslationComponent // A map from translatable rule input types to their corresponding data // formats. this.RULE_INPUT_TYPES_TO_DATA_FORMATS = { - TranslatableSetOfNormalizedString: ( - TRANSLATION_DATA_FORMAT_SET_OF_NORMALIZED_STRING), - TranslatableSetOfUnicodeString: ( - TRANSLATION_DATA_FORMAT_SET_OF_UNICODE_STRING), + TranslatableSetOfNormalizedString: + TRANSLATION_DATA_FORMAT_SET_OF_NORMALIZED_STRING, + TranslatableSetOfUnicodeString: + TRANSLATION_DATA_FORMAT_SET_OF_UNICODE_STRING, }; // Define tab constants. @@ -702,8 +743,8 @@ export class StateTranslationComponent this.TAB_ID_HINTS = AppConstants.COMPONENT_NAME_HINT; this.TAB_ID_RULE_INPUTS = AppConstants.COMPONENT_NAME_RULE_INPUT; this.TAB_ID_SOLUTION = AppConstants.COMPONENT_NAME_SOLUTION; - this.TAB_ID_CUSTOMIZATION_ARGS = ( - AppConstants.COMPONENT_NAME_INTERACTION_CUSTOMIZATION_ARGS); + this.TAB_ID_CUSTOMIZATION_ARGS = + AppConstants.COMPONENT_NAME_INTERACTION_CUSTOMIZATION_ARGS; // Activates Content tab by default. this.activatedTabId = this.TAB_ID_CONTENT; @@ -713,8 +754,9 @@ export class StateTranslationComponent this.activeTab = this.getActiveTab(); this.directiveSubscriptions.add( - this.stateEditorService.onRefreshStateTranslation.subscribe( - () => this.initStateTranslation()) + this.stateEditorService.onRefreshStateTranslation.subscribe(() => + this.initStateTranslation() + ) ); this.initStateTranslation(); @@ -727,7 +769,9 @@ export class StateTranslationComponent } } -angular.module('oppia').directive('oppiaStateTranslation', +angular.module('oppia').directive( + 'oppiaStateTranslation', downgradeComponent({ - component: StateTranslationComponent - }) as angular.IDirectiveFactory); + component: StateTranslationComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-editor-page/translation-tab/translation-tab.component.spec.ts b/core/templates/pages/exploration-editor-page/translation-tab/translation-tab.component.spec.ts index 1741d121f3f4..ae1024dd2a2a 100644 --- a/core/templates/pages/exploration-editor-page/translation-tab/translation-tab.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/translation-tab/translation-tab.component.spec.ts @@ -16,27 +16,33 @@ * @fileoverview Unit tests for translationTab. */ -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { LoaderService } from 'services/loader.service'; -import { ContextService } from 'services/context.service'; -import { UserExplorationPermissionsService } from 'pages/exploration-editor-page/services/user-exploration-permissions.service'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { EditabilityService } from 'services/editability.service'; -import { ExplorationStatesService } from '../services/exploration-states.service'; -import { RouterService } from '../services/router.service'; -import { StateTutorialFirstTimeService } from '../services/state-tutorial-first-time.service'; -import { TranslationTabComponent } from './translation-tab.component'; -import { ExplorationPermissions } from 'domain/exploration/exploration-permissions.model'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {LoaderService} from 'services/loader.service'; +import {ContextService} from 'services/context.service'; +import {UserExplorationPermissionsService} from 'pages/exploration-editor-page/services/user-exploration-permissions.service'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {EditabilityService} from 'services/editability.service'; +import {ExplorationStatesService} from '../services/exploration-states.service'; +import {RouterService} from '../services/router.service'; +import {StateTutorialFirstTimeService} from '../services/state-tutorial-first-time.service'; +import {TranslationTabComponent} from './translation-tab.component'; +import {ExplorationPermissions} from 'domain/exploration/exploration-permissions.model'; import { JoyrideDirective, JoyrideOptionsService, JoyrideService, JoyrideStepsContainerService, - JoyrideStepService + JoyrideStepService, // This throws "Object is possibly undefined." The type undefined // comes here from ngx joyride dependency. We need to suppress this // error because of strict type checking. This error is thrown because @@ -47,7 +53,7 @@ import { class MockNgbModal { open() { return { - result: Promise.resolve() + result: Promise.resolve(), }; } } @@ -78,9 +84,9 @@ describe('Translation tab component', () => { startTour() { return { subscribe: ( - value1: (arg0: { number: number }) => void, - value2: () => void, - value3: () => void + value1: (arg0: {number: number}) => void, + value2: () => void, + value3: () => void ) => { value1({number: 2}); value1({number: 4}); @@ -88,7 +94,7 @@ describe('Translation tab component', () => { value1({number: 8}); value2(); value3(); - } + }, }; } @@ -98,26 +104,21 @@ describe('Translation tab component', () => { class MockUserExplorationPermissionsService { getPermissionsAsync() { return Promise.resolve({ - canVoiceover: true + canVoiceover: true, } as ExplorationPermissions); } fetchPermissionsAsync() { return Promise.resolve({ - canVoiceover: true + canVoiceover: true, } as ExplorationPermissions); } } beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - TranslationTabComponent, - JoyrideDirective, - ], + imports: [HttpClientTestingModule], + declarations: [TranslationTabComponent, JoyrideDirective], providers: [ JoyrideStepService, JoyrideOptionsService, @@ -137,14 +138,14 @@ describe('Translation tab component', () => { }, { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, { provide: ContextService, - useClass: MockContextservice - } + useClass: MockContextservice, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -156,106 +157,117 @@ describe('Translation tab component', () => { loaderService = TestBed.inject(LoaderService); siteAnalyticsService = TestBed.inject(SiteAnalyticsService); userExplorationPermissionsService = TestBed.inject( - UserExplorationPermissionsService); + UserExplorationPermissionsService + ); editabilityService = TestBed.inject(EditabilityService); explorationStatesService = TestBed.inject(ExplorationStatesService); routerService = TestBed.inject(RouterService); stateEditorService = TestBed.inject(StateEditorService); ngbModal = TestBed.inject(NgbModal); stateTutorialFirstTimeService = TestBed.inject( - StateTutorialFirstTimeService); + StateTutorialFirstTimeService + ); spyOn(contextService, 'getExplorationId').and.returnValue('exp1'); spyOn(stateEditorService, 'getActiveStateName').and.returnValue( - 'Introduction'); + 'Introduction' + ); spyOnProperty( - stateTutorialFirstTimeService, 'onEnterTranslationForTheFirstTime') - .and.returnValue(enterTranslationForTheFirstTimeEmitter); - spyOnProperty(routerService, 'onRefreshTranslationTab') - .and.returnValue(refreshTranslationTabEmitter); + stateTutorialFirstTimeService, + 'onEnterTranslationForTheFirstTime' + ).and.returnValue(enterTranslationForTheFirstTimeEmitter); + spyOnProperty(routerService, 'onRefreshTranslationTab').and.returnValue( + refreshTranslationTabEmitter + ); let element = document.createElement('div'); - spyOn(document, 'querySelector').and.returnValue(( - element as HTMLElement)); - - explorationStatesService.init({ - Introduction: { - classifier_model_id: null, - card_is_checkpoint: false, - content: { - content_id: 'content', - html: 'Introduction Content' - }, - interaction: { - confirmed_unclassified_answers: [], - id: 'TextInput', - customization_args: { - placeholder: {value: { - content_id: 'ca_placeholder', - unicode_str: '' - }}, - rows: {value: 1}, - catchMisspellings: { - value: false - } + spyOn(document, 'querySelector').and.returnValue(element as HTMLElement); + + explorationStatesService.init( + { + Introduction: { + classifier_model_id: null, + card_is_checkpoint: false, + content: { + content_id: 'content', + html: 'Introduction Content', }, - answer_groups: [{ - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: null, - outcome: { + interaction: { + confirmed_unclassified_answers: [], + id: 'TextInput', + customization_args: { + placeholder: { + value: { + content_id: 'ca_placeholder', + unicode_str: '', + }, + }, + rows: {value: 1}, + catchMisspellings: { + value: false, + }, + }, + answer_groups: [ + { + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: null, + outcome: { + missing_prerequisite_skill_id: null, + dest: 'unused', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: '', + }, + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, + }, + }, + ], + default_outcome: { missing_prerequisite_skill_id: null, - dest: 'unused', + dest: 'default', dest_if_really_stuck: null, feedback: { - content_id: 'feedback_1', - html: '' + content_id: 'default_outcome', + html: '', }, labelled_as_correct: false, param_changes: [], - refresher_exploration_id: null - } - }], - default_outcome: { - missing_prerequisite_skill_id: null, - dest: 'default', - dest_if_really_stuck: null, - feedback: { - content_id: 'default_outcome', - html: '' + refresher_exploration_id: null, + }, + solution: { + correct_answer: 'This is the correct answer', + answer_is_exclusive: false, + explanation: { + html: 'Solution explanation', + content_id: 'content_4', + }, }, - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null + hints: [], }, - solution: { - correct_answer: 'This is the correct answer', - answer_is_exclusive: false, - explanation: { - html: 'Solution explanation', - content_id: 'content_4' - } + linked_skill_id: null, + param_changes: [], + solicit_answer_details: false, + recorded_voiceovers: { + voiceovers_mapping: { + content: {}, + default_outcome: {}, + feedback_1: { + en: { + filename: 'myfile2.mp3', + file_size_bytes: 120000, + needs_update: false, + duration_secs: 1.2, + }, + }, + }, }, - hints: [] }, - linked_skill_id: null, - param_changes: [], - solicit_answer_details: false, - recorded_voiceovers: { - voiceovers_mapping: { - content: {}, - default_outcome: {}, - feedback_1: { - en: { - filename: 'myfile2.mp3', - file_size_bytes: 120000, - needs_update: false, - duration_secs: 1.2 - } - } - } - } - } - }, false); + }, + false + ); fixture.detectChanges(); }); @@ -263,84 +275,109 @@ describe('Translation tab component', () => { component.ngOnDestroy(); }); - it('should initialize component properties after controller is initialized', - () => { - spyOn(userExplorationPermissionsService, 'getPermissionsAsync').and - .returnValue(Promise.resolve({ - canVoiceover: true - } as ExplorationPermissions)); - - component.ngOnInit(); + it('should initialize component properties after controller is initialized', () => { + spyOn( + userExplorationPermissionsService, + 'getPermissionsAsync' + ).and.returnValue( + Promise.resolve({ + canVoiceover: true, + } as ExplorationPermissions) + ); - expect(component.isTranslationTabBusy).toBe(false); - expect(component.showTranslationTabSubDirectives).toBe(false); - expect(component.tutorialInProgress).toBe(false); - }); + component.ngOnInit(); - it('should load translation tab data when translation tab page is' + - ' refreshed', fakeAsync(() => { - spyOn(loaderService, 'hideLoadingScreen'); - spyOn(userExplorationPermissionsService, 'getPermissionsAsync').and - .returnValue(Promise.resolve({ - canVoiceover: true - } as ExplorationPermissions)); + expect(component.isTranslationTabBusy).toBe(false); + expect(component.showTranslationTabSubDirectives).toBe(false); + expect(component.tutorialInProgress).toBe(false); + }); - component.ngOnInit(); - tick(); + it( + 'should load translation tab data when translation tab page is' + + ' refreshed', + fakeAsync(() => { + spyOn(loaderService, 'hideLoadingScreen'); + spyOn( + userExplorationPermissionsService, + 'getPermissionsAsync' + ).and.returnValue( + Promise.resolve({ + canVoiceover: true, + } as ExplorationPermissions) + ); - refreshTranslationTabEmitter.emit(); - tick(); + component.ngOnInit(); + tick(); - expect(component.showTranslationTabSubDirectives).toBe(true); - expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); - })); + refreshTranslationTabEmitter.emit(); + tick(); - it('should start tutorial if in tutorial mode on page load with' + - ' permissions', fakeAsync(() => { - component.permissions = { - canVoiceover: true - }; - spyOn(userExplorationPermissionsService, 'getPermissionsAsync').and - .returnValue(Promise.resolve({ - canVoiceover: true - } as ExplorationPermissions)); + expect(component.showTranslationTabSubDirectives).toBe(true); + expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); + }) + ); - spyOn(component, 'startTutorial').and.callThrough(); + it( + 'should start tutorial if in tutorial mode on page load with' + + ' permissions', + fakeAsync(() => { + component.permissions = { + canVoiceover: true, + }; + spyOn( + userExplorationPermissionsService, + 'getPermissionsAsync' + ).and.returnValue( + Promise.resolve({ + canVoiceover: true, + } as ExplorationPermissions) + ); - editabilityService.onStartTutorial(); - component.ngOnInit(); - component.initTranslationTab(); - component.startTutorial(); + spyOn(component, 'startTutorial').and.callThrough(); + editabilityService.onStartTutorial(); + component.ngOnInit(); + component.initTranslationTab(); + component.startTutorial(); - expect(editabilityService.inTutorialMode()).toBe(false); - expect(component.startTutorial).toHaveBeenCalled(); - expect(component.tutorialInProgress).toBe(false); - })); + expect(editabilityService.inTutorialMode()).toBe(false); + expect(component.startTutorial).toHaveBeenCalled(); + expect(component.tutorialInProgress).toBe(false); + }) + ); - it('should not start tutorial if in tutorial mode on page load but' + - ' no permissions', () => { - component.permissions = { - canVoiceover: true - }; + it( + 'should not start tutorial if in tutorial mode on page load but' + + ' no permissions', + () => { + component.permissions = { + canVoiceover: true, + }; - spyOn(userExplorationPermissionsService, 'getPermissionsAsync').and - .returnValue(Promise.resolve({} as ExplorationPermissions)); + spyOn( + userExplorationPermissionsService, + 'getPermissionsAsync' + ).and.returnValue(Promise.resolve({} as ExplorationPermissions)); - editabilityService.onStartTutorial(); - component.ngOnInit(); + editabilityService.onStartTutorial(); + component.ngOnInit(); - component.initTranslationTab(); + component.initTranslationTab(); - expect(editabilityService.inTutorialMode()).toBe(false); - expect(component.tutorialInProgress).toBe(false); - }); + expect(editabilityService.inTutorialMode()).toBe(false); + expect(component.tutorialInProgress).toBe(false); + } + ); it('should not start tutorial if not in tutorial mode on page load', () => { - spyOn(userExplorationPermissionsService, 'getPermissionsAsync').and - .returnValue(Promise.resolve({ - canVoiceover: true - } as ExplorationPermissions)); + spyOn( + userExplorationPermissionsService, + 'getPermissionsAsync' + ).and.returnValue( + Promise.resolve({ + canVoiceover: true, + } as ExplorationPermissions) + ); editabilityService.onEndTutorial(); component.ngOnInit(); @@ -351,33 +388,20 @@ describe('Translation tab component', () => { expect(component.tutorialInProgress).toBe(false); }); - it('should finish tutorial on clicking the end tutorial button when' + - ' it has already started', fakeAsync(() => { - spyOn(editabilityService, 'onEndTutorial'); - spyOn(stateTutorialFirstTimeService, 'markTranslationTutorialFinished'); - spyOn(userExplorationPermissionsService, 'getPermissionsAsync').and - .returnValue(Promise.resolve({ - canVoiceover: true - } as ExplorationPermissions)); - - component.ngOnInit(); - - editabilityService.onStartTutorial(); - component.leaveTutorial(); - - expect(component.tutorialInProgress).toBe(false); - expect(stateTutorialFirstTimeService.markTranslationTutorialFinished) - .toHaveBeenCalled(); - })); - - it('should skip tutorial when the skip tutorial button is clicked', + it( + 'should finish tutorial on clicking the end tutorial button when' + + ' it has already started', fakeAsync(() => { spyOn(editabilityService, 'onEndTutorial'); spyOn(stateTutorialFirstTimeService, 'markTranslationTutorialFinished'); - spyOn(userExplorationPermissionsService, 'getPermissionsAsync').and - .returnValue(Promise.resolve({ - canVoiceover: true - } as ExplorationPermissions)); + spyOn( + userExplorationPermissionsService, + 'getPermissionsAsync' + ).and.returnValue( + Promise.resolve({ + canVoiceover: true, + } as ExplorationPermissions) + ); component.ngOnInit(); @@ -385,54 +409,96 @@ describe('Translation tab component', () => { component.leaveTutorial(); expect(component.tutorialInProgress).toBe(false); - expect(stateTutorialFirstTimeService.markTranslationTutorialFinished) - .toHaveBeenCalled(); - })); + expect( + stateTutorialFirstTimeService.markTranslationTutorialFinished + ).toHaveBeenCalled(); + }) + ); - it('should start tutorial when welcome translation modal is closed', - fakeAsync(() => { - spyOn(userExplorationPermissionsService, 'getPermissionsAsync').and - .returnValue(Promise.resolve({ - canVoiceover: true - } as ExplorationPermissions)); + it('should skip tutorial when the skip tutorial button is clicked', fakeAsync(() => { + spyOn(editabilityService, 'onEndTutorial'); + spyOn(stateTutorialFirstTimeService, 'markTranslationTutorialFinished'); + spyOn( + userExplorationPermissionsService, + 'getPermissionsAsync' + ).and.returnValue( + Promise.resolve({ + canVoiceover: true, + } as ExplorationPermissions) + ); - component.ngOnInit(); + component.ngOnInit(); - spyOn(siteAnalyticsService, 'registerAcceptTutorialModalEvent'); - spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.resolve('exp1') - } as NgbModalRef); - enterTranslationForTheFirstTimeEmitter.emit(); - tick(); + editabilityService.onStartTutorial(); + component.leaveTutorial(); - expect(siteAnalyticsService.registerAcceptTutorialModalEvent) - .toHaveBeenCalled(); - })); + expect(component.tutorialInProgress).toBe(false); + expect( + stateTutorialFirstTimeService.markTranslationTutorialFinished + ).toHaveBeenCalled(); + })); + + it('should start tutorial when welcome translation modal is closed', fakeAsync(() => { + spyOn( + userExplorationPermissionsService, + 'getPermissionsAsync' + ).and.returnValue( + Promise.resolve({ + canVoiceover: true, + } as ExplorationPermissions) + ); - it('should finish translation tutorial when welcome translation modal is' + - ' dismissed', fakeAsync(() => { - spyOn(userExplorationPermissionsService, 'getPermissionsAsync').and - .returnValue(Promise.resolve({ - canVoiceover: true - } as ExplorationPermissions)); component.ngOnInit(); - spyOn(stateTutorialFirstTimeService, 'markTranslationTutorialFinished') - .and.stub(); - spyOn(siteAnalyticsService, 'registerDeclineTutorialModalEvent').and.stub(); + spyOn(siteAnalyticsService, 'registerAcceptTutorialModalEvent'); spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.reject('exp1') + result: Promise.resolve('exp1'), } as NgbModalRef); enterTranslationForTheFirstTimeEmitter.emit(); tick(); - - expect(siteAnalyticsService.registerDeclineTutorialModalEvent) - .toHaveBeenCalledWith('exp1'); - expect(stateTutorialFirstTimeService.markTranslationTutorialFinished) - .toHaveBeenCalled(); + expect( + siteAnalyticsService.registerAcceptTutorialModalEvent + ).toHaveBeenCalled(); })); + it( + 'should finish translation tutorial when welcome translation modal is' + + ' dismissed', + fakeAsync(() => { + spyOn( + userExplorationPermissionsService, + 'getPermissionsAsync' + ).and.returnValue( + Promise.resolve({ + canVoiceover: true, + } as ExplorationPermissions) + ); + component.ngOnInit(); + + spyOn( + stateTutorialFirstTimeService, + 'markTranslationTutorialFinished' + ).and.stub(); + spyOn( + siteAnalyticsService, + 'registerDeclineTutorialModalEvent' + ).and.stub(); + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.reject('exp1'), + } as NgbModalRef); + enterTranslationForTheFirstTimeEmitter.emit(); + tick(); + + expect( + siteAnalyticsService.registerDeclineTutorialModalEvent + ).toHaveBeenCalledWith('exp1'); + expect( + stateTutorialFirstTimeService.markTranslationTutorialFinished + ).toHaveBeenCalled(); + }) + ); + it('should not start tutorial', () => { component.tutorialInProgress = false; // This throws "Type 'null' is not assignable to parameter of diff --git a/core/templates/pages/exploration-editor-page/translation-tab/translation-tab.component.ts b/core/templates/pages/exploration-editor-page/translation-tab/translation-tab.component.ts index 7b562fa26ef1..5b39597febac 100644 --- a/core/templates/pages/exploration-editor-page/translation-tab/translation-tab.component.ts +++ b/core/templates/pages/exploration-editor-page/translation-tab/translation-tab.component.ts @@ -16,9 +16,9 @@ * @fileoverview Component for the translation tab. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; // This throws "Object is possibly undefined." The type undefined // comes here from ngx joyride dependency. We need to suppress this // error because of strict type checking. This error is thrown because @@ -26,36 +26,33 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; // the variable is undefined. This is because the type of the variable // is undefined. This is because the type of the variable is undefined. // @ts-ignore -import { JoyrideService } from 'ngx-joyride'; -import { Subscription } from 'rxjs'; -import { WelcomeTranslationModalComponent } from 'pages/exploration-editor-page/translation-tab/modal-templates/welcome-translation-modal.component'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { StateRecordedVoiceoversService } from 'components/state-editor/state-editor-properties-services/state-recorded-voiceovers.service'; -import { ContextService } from 'services/context.service'; -import { EditabilityService } from 'services/editability.service'; -import { LoaderService } from 'services/loader.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { ExplorationStatesService } from '../services/exploration-states.service'; -import { RouterService } from '../services/router.service'; -import { StateTutorialFirstTimeService } from '../services/state-tutorial-first-time.service'; -import { UserExplorationPermissionsService } from '../services/user-exploration-permissions.service'; -import { TranslationTabActiveModeService } from './services/translation-tab-active-mode.service'; +import {JoyrideService} from 'ngx-joyride'; +import {Subscription} from 'rxjs'; +import {WelcomeTranslationModalComponent} from 'pages/exploration-editor-page/translation-tab/modal-templates/welcome-translation-modal.component'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {StateRecordedVoiceoversService} from 'components/state-editor/state-editor-properties-services/state-recorded-voiceovers.service'; +import {ContextService} from 'services/context.service'; +import {EditabilityService} from 'services/editability.service'; +import {LoaderService} from 'services/loader.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {ExplorationStatesService} from '../services/exploration-states.service'; +import {RouterService} from '../services/router.service'; +import {StateTutorialFirstTimeService} from '../services/state-tutorial-first-time.service'; +import {UserExplorationPermissionsService} from '../services/user-exploration-permissions.service'; +import {TranslationTabActiveModeService} from './services/translation-tab-active-mode.service'; @Component({ selector: 'oppia-translation-tab', - templateUrl: './translation-tab.component.html' + templateUrl: './translation-tab.component.html', }) export class TranslationTabComponent implements OnInit, OnDestroy { directiveSubscriptions = new Subscription(); - _ID_TUTORIAL_TRANSLATION_LANGUAGE: string = ( - '#tutorialTranslationLanguage'); + _ID_TUTORIAL_TRANSLATION_LANGUAGE: string = '#tutorialTranslationLanguage'; - _ID_TUTORIAL_TRANSLATION_STATE: string = ( - '#tutorialTranslationState'); + _ID_TUTORIAL_TRANSLATION_STATE: string = '#tutorialTranslationState'; - _ID_TUTORIAL_TRANSLATION_OVERVIEW: string = ( - '#tutorialTranslationOverview'); + _ID_TUTORIAL_TRANSLATION_OVERVIEW: string = '#tutorialTranslationOverview'; // This property is initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see @@ -79,19 +76,20 @@ export class TranslationTabComponent implements OnInit, OnDestroy { private stateRecordedVoiceoversService: StateRecordedVoiceoversService, private stateTutorialFirstTimeService: StateTutorialFirstTimeService, private translationTabActiveModeService: TranslationTabActiveModeService, - private userExplorationPermissionsService: - UserExplorationPermissionsService, - private joyride: JoyrideService, - ) { } + private userExplorationPermissionsService: UserExplorationPermissionsService, + private joyride: JoyrideService + ) {} initTranslationTab(): void { this.stateTutorialFirstTimeService.initTranslation( - this.contextService.getExplorationId()); + this.contextService.getExplorationId() + ); let stateName = this.stateEditorService.getActiveStateName(); if (stateName) { this.stateRecordedVoiceoversService.init( - stateName, this.explorationStatesService.getRecordedVoiceoversMemento( - stateName)); + stateName, + this.explorationStatesService.getRecordedVoiceoversMemento(stateName) + ); } this.showTranslationTabSubDirectives = true; this.translationTabActiveModeService.activateVoiceoverMode(); @@ -114,48 +112,57 @@ export class TranslationTabComponent implements OnInit, OnDestroy { } if (this.permissions.canVoiceover) { this.tutorialInProgress = true; - this.joyride.startTour( - { steps: [ - 'translationTabTourContainer', - 'translationTabOverview', - 'translationTabStatusGraph', - 'translationTabCardOptions', - 'translationTabRecordingOverview', - 'translationTabReRecordingOverview', - 'translationTabTutorialComplete' - ], - stepDefaultPosition: 'bottom', - themeColor: '#212f23', - } - ).subscribe( - () => { - let element = document.querySelector( - '.joyride-step__holder') as HTMLElement; - // This code make the joyride visible over navbar - // by overriding the properties of joyride-step__holder class. - element.style.zIndex = '1020'; - }, - () => {}, - () => { - this.leaveTutorial(); - } - ); + this.joyride + .startTour({ + steps: [ + 'translationTabTourContainer', + 'translationTabOverview', + 'translationTabStatusGraph', + 'translationTabCardOptions', + 'translationTabRecordingOverview', + 'translationTabReRecordingOverview', + 'translationTabTutorialComplete', + ], + stepDefaultPosition: 'bottom', + themeColor: '#212f23', + }) + .subscribe( + () => { + let element = document.querySelector( + '.joyride-step__holder' + ) as HTMLElement; + // This code make the joyride visible over navbar + // by overriding the properties of joyride-step__holder class. + element.style.zIndex = '1020'; + }, + () => {}, + () => { + this.leaveTutorial(); + } + ); } } showWelcomeTranslationModal(): void { - this.ngbModal.open(WelcomeTranslationModalComponent, { - backdrop: true, - windowClass: 'oppia-welcome-modal' - }).result.then((explorationId) => { - this.siteAnalyticsService.registerAcceptTutorialModalEvent( - explorationId); - this.startTutorial(); - }, (explorationId) => { - this.siteAnalyticsService.registerDeclineTutorialModalEvent( - explorationId); - this.stateTutorialFirstTimeService.markTranslationTutorialFinished(); - }); + this.ngbModal + .open(WelcomeTranslationModalComponent, { + backdrop: true, + windowClass: 'oppia-welcome-modal', + }) + .result.then( + explorationId => { + this.siteAnalyticsService.registerAcceptTutorialModalEvent( + explorationId + ); + this.startTutorial(); + }, + explorationId => { + this.siteAnalyticsService.registerDeclineTutorialModalEvent( + explorationId + ); + this.stateTutorialFirstTimeService.markTranslationTutorialFinished(); + } + ); } ngOnInit(): void { @@ -165,15 +172,14 @@ export class TranslationTabComponent implements OnInit, OnDestroy { this.tutorialInProgress = false; this.directiveSubscriptions.add( - this.routerService.onRefreshTranslationTab.subscribe( - () => { - this.initTranslationTab(); - } - ) + this.routerService.onRefreshTranslationTab.subscribe(() => { + this.initTranslationTab(); + }) ); - this.userExplorationPermissionsService.getPermissionsAsync() - .then((explorationPermissions) => { + this.userExplorationPermissionsService + .getPermissionsAsync() + .then(explorationPermissions => { this.permissions = explorationPermissions; }); @@ -190,7 +196,9 @@ export class TranslationTabComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive('oppiaTranslationTab', +angular.module('oppia').directive( + 'oppiaTranslationTab', downgradeComponent({ - component: TranslationTabComponent - }) as angular.IDirectiveFactory); + component: TranslationTabComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-editor-page/translation-tab/translator-overview/translator-overview.component.spec.ts b/core/templates/pages/exploration-editor-page/translation-tab/translator-overview/translator-overview.component.spec.ts index 7c55b44dd92c..88be4e815195 100644 --- a/core/templates/pages/exploration-editor-page/translation-tab/translator-overview/translator-overview.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/translation-tab/translator-overview/translator-overview.component.spec.ts @@ -16,33 +16,40 @@ * @fileoverview Unit tests for translatorOverview. */ -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, discardPeriodicTasks, fakeAsync, flush, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { LanguageUtilService } from 'domain/utilities/language-util.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { ExplorationLanguageCodeService } from 'pages/exploration-editor-page/services/exploration-language-code.service'; -import { RouterService } from 'pages/exploration-editor-page/services/router.service'; -import { GraphDataService } from 'pages/exploration-editor-page/services/graph-data.service'; -import { TranslationLanguageService } from '../services/translation-language.service'; -import { TranslationStatusService } from '../services/translation-status.service'; -import { TranslationTabActiveModeService } from '../services/translation-tab-active-mode.service'; -import { TranslatorOverviewComponent } from './translator-overview.component'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { ContextService } from 'services/context.service'; -import { EntityTranslationsService } from 'services/entity-translations.services'; -import { UserExplorationPermissionsService } from '../../services/user-exploration-permissions.service'; -import { ChangeListService } from '../../services/change-list.service'; -import { EntityTranslation } from 'domain/translation/EntityTranslationObjectFactory'; -import { TranslatedContent } from 'domain/exploration/TranslatedContentObjectFactory'; - +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import { + ComponentFixture, + discardPeriodicTasks, + fakeAsync, + flush, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {LanguageUtilService} from 'domain/utilities/language-util.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {ExplorationLanguageCodeService} from 'pages/exploration-editor-page/services/exploration-language-code.service'; +import {RouterService} from 'pages/exploration-editor-page/services/router.service'; +import {GraphDataService} from 'pages/exploration-editor-page/services/graph-data.service'; +import {TranslationLanguageService} from '../services/translation-language.service'; +import {TranslationStatusService} from '../services/translation-status.service'; +import {TranslationTabActiveModeService} from '../services/translation-tab-active-mode.service'; +import {TranslatorOverviewComponent} from './translator-overview.component'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {ContextService} from 'services/context.service'; +import {EntityTranslationsService} from 'services/entity-translations.services'; +import {UserExplorationPermissionsService} from '../../services/user-exploration-permissions.service'; +import {ChangeListService} from '../../services/change-list.service'; +import {EntityTranslation} from 'domain/translation/EntityTranslationObjectFactory'; +import {TranslatedContent} from 'domain/exploration/TranslatedContentObjectFactory'; class MockNgbModal { open() { return { - result: Promise.resolve() + result: Promise.resolve(), }; } } @@ -70,18 +77,16 @@ describe('Translator Overview component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - TranslatorOverviewComponent - ], + declarations: [TranslatorOverviewComponent], providers: [ ExplorationLanguageCodeService, { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, - WindowRef + WindowRef, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -93,27 +98,36 @@ describe('Translator Overview component', () => { languageUtilService = TestBed.inject(LanguageUtilService); focusManagerService = TestBed.inject(FocusManagerService); explorationLanguageCodeService = TestBed.inject( - ExplorationLanguageCodeService); + ExplorationLanguageCodeService + ); stateEditorService = TestBed.inject(StateEditorService); translationLanguageService = TestBed.inject(TranslationLanguageService); translationStatusService = TestBed.inject(TranslationStatusService); graphDataService = TestBed.inject(GraphDataService); translationTabActiveModeService = TestBed.inject( - TranslationTabActiveModeService); + TranslationTabActiveModeService + ); focusManagerService = TestBed.inject(FocusManagerService); routerService = TestBed.inject(RouterService); entityTranslationsService = TestBed.inject(EntityTranslationsService); userExplorationPermissionsService = TestBed.inject( - UserExplorationPermissionsService); + UserExplorationPermissionsService + ); changeListService = TestBed.inject(ChangeListService); windowRef = TestBed.inject(WindowRef); - spyOn(translationTabActiveModeService, 'isTranslationModeActive').and - .returnValue(true); - spyOn(translationTabActiveModeService, 'isVoiceoverModeActive').and - .returnValue(true); - spyOn(entityTranslationsService, 'getEntityTranslationsAsync') - .and.resolveTo(); + spyOn( + translationTabActiveModeService, + 'isTranslationModeActive' + ).and.returnValue(true); + spyOn( + translationTabActiveModeService, + 'isVoiceoverModeActive' + ).and.returnValue(true); + spyOn( + entityTranslationsService, + 'getEntityTranslationsAsync' + ).and.resolveTo(); explorationLanguageCodeService.init(explorationLanguageCode); component.isTranslationTabBusy = false; @@ -134,124 +148,144 @@ describe('Translator Overview component', () => { describe('when change list contains changes', () => { beforeEach(() => { entityTranslation = new EntityTranslation( - 'entityId', 'entityType', 1, 'hi', { - content1: new TranslatedContent( - 'translated content', 'html', false) + 'entityId', + 'entityType', + 1, + 'hi', + { + content1: new TranslatedContent('translated content', 'html', false), } ); entityTranslationsService.languageCodeToEntityTranslations = { - hi: entityTranslation + hi: entityTranslation, }; - spyOn( - windowRef.nativeWindow.localStorage, 'getItem').and.returnValue('hi'); - entityTranslationsService.getEntityTranslationsAsync = ( - jasmine.createSpy().and.returnValue(Promise.resolve(entityTranslation)) + spyOn(windowRef.nativeWindow.localStorage, 'getItem').and.returnValue( + 'hi' ); + entityTranslationsService.getEntityTranslationsAsync = jasmine + .createSpy() + .and.returnValue(Promise.resolve(entityTranslation)); }); - it('should update entity translations with edit translation changes', - fakeAsync(() => { - expect( - entityTranslationsService.getHtmlTranslations('hi', ['content1']) - ).toEqual(['translated content']); + it('should update entity translations with edit translation changes', fakeAsync(() => { + expect( + entityTranslationsService.getHtmlTranslations('hi', ['content1']) + ).toEqual(['translated content']); - spyOn(changeListService, 'getTranslationChangeList').and.returnValue([{ + spyOn(changeListService, 'getTranslationChangeList').and.returnValue([ + { cmd: 'edit_translation', content_id: 'content1', language_code: 'hi', translation: { content_value: 'new translation', content_format: 'html', - needs_update: false - } - }]); + needs_update: false, + }, + }, + ]); - spyOn( - translationLanguageService, 'getActiveLanguageCode') - .and.returnValue(undefined as unknown as string); + spyOn( + translationLanguageService, + 'getActiveLanguageCode' + ).and.returnValue(undefined as unknown as string); - component.ngOnInit(); - tick(); + component.ngOnInit(); + tick(); - expect( - entityTranslationsService.getHtmlTranslations('hi', ['content1']) - ).toEqual(['new translation']); - })); + expect( + entityTranslationsService.getHtmlTranslations('hi', ['content1']) + ).toEqual(['new translation']); + })); - it('should handle mark needs update translation changes', - fakeAsync(() => { - let translatedContent = entityTranslation.getWrittenTranslation( - 'content1') as TranslatedContent; - expect(translatedContent.needsUpdate).toBeFalse(); + it('should handle mark needs update translation changes', fakeAsync(() => { + let translatedContent = entityTranslation.getWrittenTranslation( + 'content1' + ) as TranslatedContent; + expect(translatedContent.needsUpdate).toBeFalse(); - spyOn(changeListService, 'getTranslationChangeList').and.returnValue([{ + spyOn(changeListService, 'getTranslationChangeList').and.returnValue([ + { cmd: 'mark_translations_needs_update', content_id: 'content1', - }]); + }, + ]); - component.ngOnInit(); - tick(); + component.ngOnInit(); + tick(); - translatedContent = entityTranslation.getWrittenTranslation( - 'content1') as TranslatedContent; - expect(translatedContent.needsUpdate).toBeTrue(); - })); + translatedContent = entityTranslation.getWrittenTranslation( + 'content1' + ) as TranslatedContent; + expect(translatedContent.needsUpdate).toBeTrue(); + })); - it('should update entity translations with remove translation changes', - fakeAsync(() => { - expect(entityTranslation.hasWrittenTranslation('content1')).toBeTrue(); + it('should update entity translations with remove translation changes', fakeAsync(() => { + expect(entityTranslation.hasWrittenTranslation('content1')).toBeTrue(); - spyOn(changeListService, 'getTranslationChangeList').and.returnValue([{ + spyOn(changeListService, 'getTranslationChangeList').and.returnValue([ + { cmd: 'remove_translations', content_id: 'content1', - }]); - - component.ngOnInit(); - tick(); - - expect(entityTranslation.hasWrittenTranslation('content1')).toBeFalse(); - })); - - it('should set language code to previously selected one when there is no' + - 'active language code selected', fakeAsync(() => { - spyOn( - translationLanguageService, 'getActiveLanguageCode') - .and.returnValue(undefined as unknown as string); + }, + ]); component.ngOnInit(); tick(); - expect(component.languageCode).toBe('hi'); + expect(entityTranslation.hasWrittenTranslation('content1')).toBeFalse(); })); + + it( + 'should set language code to previously selected one when there is no' + + 'active language code selected', + fakeAsync(() => { + spyOn( + translationLanguageService, + 'getActiveLanguageCode' + ).and.returnValue(undefined as unknown as string); + + component.ngOnInit(); + tick(); + + expect(component.languageCode).toBe('hi'); + }) + ); }); - it('should initialize component properties after controller is initialized', - () => { - spyOn(userExplorationPermissionsService, 'getPermissionsAsync').and - .returnValue(Promise.resolve({ - canUnpublish: false, - canReleaseOwnership: false, - canPublish: false, - canVoiceover: true, - canDelete: false, - canModifyRoles: false, - canEdit: false, - canManageVoiceArtist: false - })); - spyOn(contextService, 'isExplorationLinkedToStory').and.returnValue(true); - component.canShowTabModeSwitcher(); - - expect(component.inTranslationMode).toBe(true); - expect(component.inVoiceoverMode).toBe(true); - expect(component.languageCodesAndDescriptions.length).toBe( - languageUtilService.getAllVoiceoverLanguageCodes().length - 1); - expect(languageUtilService.getAllVoiceoverLanguageCodes()).toContain( - explorationLanguageCode); - expect(component.languageCodesAndDescriptions).not.toContain({ - id: explorationLanguageCode, - description: languageUtilService.getAudioLanguageDescription( - explorationLanguageCode) - }); + it('should initialize component properties after controller is initialized', () => { + spyOn( + userExplorationPermissionsService, + 'getPermissionsAsync' + ).and.returnValue( + Promise.resolve({ + canUnpublish: false, + canReleaseOwnership: false, + canPublish: false, + canVoiceover: true, + canDelete: false, + canModifyRoles: false, + canEdit: false, + canManageVoiceArtist: false, + }) + ); + spyOn(contextService, 'isExplorationLinkedToStory').and.returnValue(true); + component.canShowTabModeSwitcher(); + + expect(component.inTranslationMode).toBe(true); + expect(component.inVoiceoverMode).toBe(true); + expect(component.languageCodesAndDescriptions.length).toBe( + languageUtilService.getAllVoiceoverLanguageCodes().length - 1 + ); + expect(languageUtilService.getAllVoiceoverLanguageCodes()).toContain( + explorationLanguageCode + ); + expect(component.languageCodesAndDescriptions).not.toContain({ + id: explorationLanguageCode, + description: languageUtilService.getAudioLanguageDescription( + explorationLanguageCode + ), }); + }); describe('when selected language is not exploration language', () => { beforeEach(() => { @@ -264,128 +298,82 @@ describe('Translator Overview component', () => { expect(component.canShowTabModeSwitcher()).toBeTrue; }); - it('should not show mode switcher if exploration is not linked to story', - () => { - spyOn(contextService, 'isExplorationLinkedToStory').and.returnValue( - false - ); - expect(component.canShowTabModeSwitcher()).toBeFalse; - } - ); + it('should not show mode switcher if exploration is not linked to story', () => { + spyOn(contextService, 'isExplorationLinkedToStory').and.returnValue( + false + ); + expect(component.canShowTabModeSwitcher()).toBeFalse; + }); }); - it('should change to voiceover active mode when changing translation tab', - fakeAsync(() => { - spyOn(userExplorationPermissionsService, 'getPermissionsAsync').and - .returnValue(Promise.resolve({ - canUnpublish: false, - canReleaseOwnership: false, - canPublish: false, - canVoiceover: true, - canDelete: false, - canModifyRoles: false, - canEdit: false, - canManageVoiceArtist: false - })); - spyOn(translationTabActiveModeService, 'activateVoiceoverMode'); - spyOn(translationStatusService, 'refresh'); + it('should change to voiceover active mode when changing translation tab', fakeAsync(() => { + spyOn( + userExplorationPermissionsService, + 'getPermissionsAsync' + ).and.returnValue( + Promise.resolve({ + canUnpublish: false, + canReleaseOwnership: false, + canPublish: false, + canVoiceover: true, + canDelete: false, + canModifyRoles: false, + canEdit: false, + canManageVoiceArtist: false, + }) + ); + spyOn(translationTabActiveModeService, 'activateVoiceoverMode'); + spyOn(translationStatusService, 'refresh'); - component.changeActiveMode('Voiceover'); + component.changeActiveMode('Voiceover'); - expect(translationTabActiveModeService.activateVoiceoverMode) - .toHaveBeenCalled(); - expect(translationStatusService.refresh).toHaveBeenCalled(); + expect( + translationTabActiveModeService.activateVoiceoverMode + ).toHaveBeenCalled(); + expect(translationStatusService.refresh).toHaveBeenCalled(); - flush(); - discardPeriodicTasks(); - }) - ); + flush(); + discardPeriodicTasks(); + })); - it('should change to translation active mode when changing translation tab', - fakeAsync(() => { - spyOn(userExplorationPermissionsService, 'getPermissionsAsync').and - .returnValue(Promise.resolve({ - canUnpublish: false, - canReleaseOwnership: false, - canPublish: false, - canVoiceover: true, - canDelete: false, - canModifyRoles: false, - canEdit: false, - canManageVoiceArtist: false - })); - spyOn(translationTabActiveModeService, 'activateTranslationMode'); - spyOn(graphDataService, 'recompute'); - spyOn(translationStatusService, 'refresh'); - - component.changeActiveMode('Translate'); - - expect(translationTabActiveModeService.activateTranslationMode) - .toHaveBeenCalled(); - expect(translationStatusService.refresh).toHaveBeenCalled(); - - flush(); - discardPeriodicTasks(); - expect(graphDataService.recompute).toHaveBeenCalled(); - }) - ); + it('should change to translation active mode when changing translation tab', fakeAsync(() => { + spyOn( + userExplorationPermissionsService, + 'getPermissionsAsync' + ).and.returnValue( + Promise.resolve({ + canUnpublish: false, + canReleaseOwnership: false, + canPublish: false, + canVoiceover: true, + canDelete: false, + canModifyRoles: false, + canEdit: false, + canManageVoiceArtist: false, + }) + ); + spyOn(translationTabActiveModeService, 'activateTranslationMode'); + spyOn(graphDataService, 'recompute'); + spyOn(translationStatusService, 'refresh'); - it('should change translation language when translation tab is not busy', - fakeAsync(() => { - spyOn(userExplorationPermissionsService, 'getPermissionsAsync').and - .returnValue(Promise.resolve({ - canUnpublish: false, - canReleaseOwnership: false, - canPublish: false, - canVoiceover: true, - canDelete: false, - canModifyRoles: false, - canEdit: false, - canManageVoiceArtist: false - })); - spyOn(translationLanguageService, 'setActiveLanguageCode'); - component.languageCode = 'es'; - component.changeTranslationLanguage(); - - flush(); - discardPeriodicTasks(); - expect(translationLanguageService.setActiveLanguageCode) - .toHaveBeenCalled(); - }) - ); + component.changeActiveMode('Translate'); - it('should not change translation language when translation tab is busy', - fakeAsync(() => { - spyOn(userExplorationPermissionsService, 'getPermissionsAsync').and - .returnValue(Promise.resolve({ - canUnpublish: false, - canReleaseOwnership: false, - canPublish: false, - canVoiceover: true, - canDelete: false, - canModifyRoles: false, - canEdit: false, - canManageVoiceArtist: false - })); - component.isTranslationTabBusy = true; - let showTranslationTabBusyModalEmitter = new EventEmitter(); - spyOn(showTranslationTabBusyModalEmitter, 'emit'); - spyOnProperty(stateEditorService, 'onShowTranslationTabBusyModal').and - .returnValue(showTranslationTabBusyModalEmitter); - component.changeTranslationLanguage(); - - flush(); - discardPeriodicTasks(); - expect(showTranslationTabBusyModalEmitter.emit).toHaveBeenCalled(); - - // Reset value for isTranslationTabBusy. - component.isTranslationTabBusy = false; - })); + expect( + translationTabActiveModeService.activateTranslationMode + ).toHaveBeenCalled(); + expect(translationStatusService.refresh).toHaveBeenCalled(); - it('should get translation bar progress data when there are more' + - ' than 1 item to be translated', () => { - spyOn(userExplorationPermissionsService, 'getPermissionsAsync').and - .returnValue(Promise.resolve({ + flush(); + discardPeriodicTasks(); + expect(graphDataService.recompute).toHaveBeenCalled(); + })); + + it('should change translation language when translation tab is not busy', fakeAsync(() => { + spyOn( + userExplorationPermissionsService, + 'getPermissionsAsync' + ).and.returnValue( + Promise.resolve({ canUnpublish: false, canReleaseOwnership: false, canPublish: false, @@ -393,21 +381,24 @@ describe('Translator Overview component', () => { canDelete: false, canModifyRoles: false, canEdit: false, - canManageVoiceArtist: false - })); - spyOn(translationStatusService, 'getExplorationContentRequiredCount').and - .returnValue(3); - spyOn(translationStatusService, 'getExplorationContentNotAvailableCount') - .and.returnValue(1); - component.getTranslationProgressStyle(); - expect(component.getTranslationProgressAriaLabel()).toBe( - '2 items translated out of 3 items'); - }); + canManageVoiceArtist: false, + }) + ); + spyOn(translationLanguageService, 'setActiveLanguageCode'); + component.languageCode = 'es'; + component.changeTranslationLanguage(); + + flush(); + discardPeriodicTasks(); + expect(translationLanguageService.setActiveLanguageCode).toHaveBeenCalled(); + })); - it('should get translation bar progress data when there is 1 item to be' + - ' translated', () => { - spyOn(userExplorationPermissionsService, 'getPermissionsAsync').and - .returnValue(Promise.resolve({ + it('should not change translation language when translation tab is busy', fakeAsync(() => { + spyOn( + userExplorationPermissionsService, + 'getPermissionsAsync' + ).and.returnValue( + Promise.resolve({ canUnpublish: false, canReleaseOwnership: false, canPublish: false, @@ -415,21 +406,35 @@ describe('Translator Overview component', () => { canDelete: false, canModifyRoles: false, canEdit: false, - canManageVoiceArtist: false - })); - spyOn(translationStatusService, 'getExplorationContentRequiredCount') - .and.returnValue(2); - spyOn(translationStatusService, 'getExplorationContentNotAvailableCount') - .and.returnValue(1); - component.getTranslationProgressStyle(); - expect(component.getTranslationProgressAriaLabel()).toBe( - '1 item translated out of 2 items'); - }); + canManageVoiceArtist: false, + }) + ); + component.isTranslationTabBusy = true; + let showTranslationTabBusyModalEmitter = new EventEmitter(); + spyOn(showTranslationTabBusyModalEmitter, 'emit'); + spyOnProperty( + stateEditorService, + 'onShowTranslationTabBusyModal' + ).and.returnValue(showTranslationTabBusyModalEmitter); + component.changeTranslationLanguage(); + + flush(); + discardPeriodicTasks(); + expect(showTranslationTabBusyModalEmitter.emit).toHaveBeenCalled(); + + // Reset value for isTranslationTabBusy. + component.isTranslationTabBusy = false; + })); - it('should apply autofocus to history tab element when tab is switched', - fakeAsync(() => { - spyOn(userExplorationPermissionsService, 'getPermissionsAsync').and - .returnValue(Promise.resolve({ + it( + 'should get translation bar progress data when there are more' + + ' than 1 item to be translated', + () => { + spyOn( + userExplorationPermissionsService, + 'getPermissionsAsync' + ).and.returnValue( + Promise.resolve({ canUnpublish: false, canReleaseOwnership: false, canPublish: false, @@ -437,17 +442,84 @@ describe('Translator Overview component', () => { canDelete: false, canModifyRoles: false, canEdit: false, - canManageVoiceArtist: false - })); - spyOn(routerService, 'getActiveTabName').and.returnValue('translation'); - spyOn(focusManagerService, 'setFocus'); + canManageVoiceArtist: false, + }) + ); + spyOn( + translationStatusService, + 'getExplorationContentRequiredCount' + ).and.returnValue(3); + spyOn( + translationStatusService, + 'getExplorationContentNotAvailableCount' + ).and.returnValue(1); + component.getTranslationProgressStyle(); + expect(component.getTranslationProgressAriaLabel()).toBe( + '2 items translated out of 3 items' + ); + } + ); - component.ngOnInit(); + it( + 'should get translation bar progress data when there is 1 item to be' + + ' translated', + () => { + spyOn( + userExplorationPermissionsService, + 'getPermissionsAsync' + ).and.returnValue( + Promise.resolve({ + canUnpublish: false, + canReleaseOwnership: false, + canPublish: false, + canVoiceover: true, + canDelete: false, + canModifyRoles: false, + canEdit: false, + canManageVoiceArtist: false, + }) + ); + spyOn( + translationStatusService, + 'getExplorationContentRequiredCount' + ).and.returnValue(2); + spyOn( + translationStatusService, + 'getExplorationContentNotAvailableCount' + ).and.returnValue(1); + component.getTranslationProgressStyle(); + expect(component.getTranslationProgressAriaLabel()).toBe( + '1 item translated out of 2 items' + ); + } + ); - flush(); - discardPeriodicTasks(); + it('should apply autofocus to history tab element when tab is switched', fakeAsync(() => { + spyOn( + userExplorationPermissionsService, + 'getPermissionsAsync' + ).and.returnValue( + Promise.resolve({ + canUnpublish: false, + canReleaseOwnership: false, + canPublish: false, + canVoiceover: true, + canDelete: false, + canModifyRoles: false, + canEdit: false, + canManageVoiceArtist: false, + }) + ); + spyOn(routerService, 'getActiveTabName').and.returnValue('translation'); + spyOn(focusManagerService, 'setFocus'); - expect(focusManagerService.setFocus).toHaveBeenCalledWith( - 'audioTranslationLanguageCodeField'); - })); + component.ngOnInit(); + + flush(); + discardPeriodicTasks(); + + expect(focusManagerService.setFocus).toHaveBeenCalledWith( + 'audioTranslationLanguageCodeField' + ); + })); }); diff --git a/core/templates/pages/exploration-editor-page/translation-tab/translator-overview/translator-overview.component.ts b/core/templates/pages/exploration-editor-page/translation-tab/translator-overview/translator-overview.component.ts index a4b756849427..a0140c030049 100644 --- a/core/templates/pages/exploration-editor-page/translation-tab/translator-overview/translator-overview.component.ts +++ b/core/templates/pages/exploration-editor-page/translation-tab/translator-overview/translator-overview.component.ts @@ -17,35 +17,35 @@ * translation language. */ -import { Component, Input, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { LanguageUtilService } from 'domain/utilities/language-util.service'; -import { ExplorationLanguageCodeService } from 'pages/exploration-editor-page/services/exploration-language-code.service'; -import { GraphDataService } from 'pages/exploration-editor-page/services/graph-data.service'; -import { RouterService } from 'pages/exploration-editor-page/services/router.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { TranslationLanguageService } from '../services/translation-language.service'; -import { TranslationStatusService } from '../services/translation-status.service'; -import { TranslationTabActiveModeService } from '../services/translation-tab-active-mode.service'; -import { ExplorationEditorPageConstants } from 'pages/exploration-editor-page/exploration-editor-page.constants'; -import { ContextService } from 'services/context.service'; -import { EntityTranslationsService } from 'services/entity-translations.services'; -import { LoaderService } from 'services/loader.service'; -import { ChangeListService } from 'pages/exploration-editor-page/services/change-list.service'; -import { EntityTranslation } from 'domain/translation/EntityTranslationObjectFactory'; -import { TranslatedContent } from 'domain/exploration/TranslatedContentObjectFactory'; +import {Component, Input, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {LanguageUtilService} from 'domain/utilities/language-util.service'; +import {ExplorationLanguageCodeService} from 'pages/exploration-editor-page/services/exploration-language-code.service'; +import {GraphDataService} from 'pages/exploration-editor-page/services/graph-data.service'; +import {RouterService} from 'pages/exploration-editor-page/services/router.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {TranslationLanguageService} from '../services/translation-language.service'; +import {TranslationStatusService} from '../services/translation-status.service'; +import {TranslationTabActiveModeService} from '../services/translation-tab-active-mode.service'; +import {ExplorationEditorPageConstants} from 'pages/exploration-editor-page/exploration-editor-page.constants'; +import {ContextService} from 'services/context.service'; +import {EntityTranslationsService} from 'services/entity-translations.services'; +import {LoaderService} from 'services/loader.service'; +import {ChangeListService} from 'pages/exploration-editor-page/services/change-list.service'; +import {EntityTranslation} from 'domain/translation/EntityTranslationObjectFactory'; +import {TranslatedContent} from 'domain/exploration/TranslatedContentObjectFactory'; import { ExplorationChangeEditTranslation, ExplorationChangeMarkTranslationsNeedsUpdate, ExplorationChangeRemoveTranslations, - ExplorationTranslationChange + ExplorationTranslationChange, } from 'domain/exploration/exploration-draft.model'; @Component({ selector: 'oppia-translator-overview', - templateUrl: './translator-overview.component.html' + templateUrl: './translator-overview.component.html', }) export class TranslatorOverviewComponent implements OnInit { // These properties are initialized using Angular lifecycle hooks @@ -62,14 +62,13 @@ export class TranslatorOverviewComponent implements OnInit { TRANSLATION_MODE!: string; allAudioLanguageCodes!: string[]; LAST_SELECTED_TRANSLATION_LANGUAGE!: string; - languageCodesAndDescriptions!: { id: string; description: string}[]; + languageCodesAndDescriptions!: {id: string; description: string}[]; constructor( private contextService: ContextService, private entityTranslationsService: EntityTranslationsService, private changeListService: ChangeListService, - private explorationLanguageCodeService: - ExplorationLanguageCodeService, + private explorationLanguageCodeService: ExplorationLanguageCodeService, private focusManagerService: FocusManagerService, private graphDataService: GraphDataService, private languageUtilService: LanguageUtilService, @@ -80,38 +79,41 @@ export class TranslatorOverviewComponent implements OnInit { private translationStatusService: TranslationStatusService, private translationTabActiveModeService: TranslationTabActiveModeService, private windowRef: WindowRef - ) { } + ) {} canShowTabModeSwitcher(): boolean { - return this.contextService.isExplorationLinkedToStory() && ( - this.languageCode !== this.explorationLanguageCodeService.displayed); + return ( + this.contextService.isExplorationLinkedToStory() && + this.languageCode !== this.explorationLanguageCodeService.displayed + ); } refreshDirectiveScope(): void { - this.inTranslationMode = ( - this.translationTabActiveModeService.isTranslationModeActive()); - this.inVoiceoverMode = ( - this.translationTabActiveModeService.isVoiceoverModeActive()); - this.allAudioLanguageCodes = ( - this.languageUtilService.getAllVoiceoverLanguageCodes()); + this.inTranslationMode = + this.translationTabActiveModeService.isTranslationModeActive(); + this.inVoiceoverMode = + this.translationTabActiveModeService.isVoiceoverModeActive(); + this.allAudioLanguageCodes = + this.languageUtilService.getAllVoiceoverLanguageCodes(); if (this.inTranslationMode) { let index = this.allAudioLanguageCodes.indexOf( - this.explorationLanguageCodeService.displayed as string); + this.explorationLanguageCodeService.displayed as string + ); if (index !== -1) { this.allAudioLanguageCodes.splice(index, 1); } } - this.languageCodesAndDescriptions = ( - this.allAudioLanguageCodes.map((languageCode) => { + this.languageCodesAndDescriptions = this.allAudioLanguageCodes.map( + languageCode => { return { id: languageCode, - description: ( - this.languageUtilService.getAudioLanguageDescription( - languageCode)) + description: + this.languageUtilService.getAudioLanguageDescription(languageCode), }; - })); + } + ); } changeActiveMode(modeName: string): void { @@ -131,65 +133,71 @@ export class TranslatorOverviewComponent implements OnInit { getTranslationProgressStyle(): { width: string; height: string; - } { - this.numberOfRequiredAudio = this.translationStatusService - .getExplorationContentRequiredCount(); - this.numberOfAudioNotAvailable = this.translationStatusService - .getExplorationContentNotAvailableCount(); + } { + this.numberOfRequiredAudio = + this.translationStatusService.getExplorationContentRequiredCount(); + this.numberOfAudioNotAvailable = + this.translationStatusService.getExplorationContentNotAvailableCount(); - let progressPercent = ( - 100 - ( - this.numberOfAudioNotAvailable / - this.numberOfRequiredAudio) * 100); + let progressPercent = + 100 - (this.numberOfAudioNotAvailable / this.numberOfRequiredAudio) * 100; - return { width: progressPercent + '%', height: '100%' }; + return {width: progressPercent + '%', height: '100%'}; } changeTranslationLanguage(): void { if (this.isTranslationTabBusy) { - let lastSelectedTranslationLanguage = ( + let lastSelectedTranslationLanguage = this.windowRef.nativeWindow.localStorage.getItem( - this.LAST_SELECTED_TRANSLATION_LANGUAGE)); - this.languageCode = lastSelectedTranslationLanguage ? ( - lastSelectedTranslationLanguage) : ( - ExplorationEditorPageConstants.DEFAULT_AUDIO_LANGUAGE); + this.LAST_SELECTED_TRANSLATION_LANGUAGE + ); + this.languageCode = lastSelectedTranslationLanguage + ? lastSelectedTranslationLanguage + : ExplorationEditorPageConstants.DEFAULT_AUDIO_LANGUAGE; this.stateEditorService.onShowTranslationTabBusyModal.emit(); return; } this.loaderService.showLoadingScreen('Loading'); - this.entityTranslationsService.getEntityTranslationsAsync( - this.languageCode - ).then((entityTranslations) => { - this.updateTranslationWithChangeList(entityTranslations); - this.translationLanguageService.setActiveLanguageCode( - this.languageCode); - this.translationStatusService.refresh(); - this.windowRef.nativeWindow.localStorage.setItem( - this.LAST_SELECTED_TRANSLATION_LANGUAGE, this.languageCode); - this.routerService.onCenterGraph.emit(); - this.loaderService.hideLoadingScreen(); - }); + this.entityTranslationsService + .getEntityTranslationsAsync(this.languageCode) + .then(entityTranslations => { + this.updateTranslationWithChangeList(entityTranslations); + this.translationLanguageService.setActiveLanguageCode( + this.languageCode + ); + this.translationStatusService.refresh(); + this.windowRef.nativeWindow.localStorage.setItem( + this.LAST_SELECTED_TRANSLATION_LANGUAGE, + this.languageCode + ); + this.routerService.onCenterGraph.emit(); + this.loaderService.hideLoadingScreen(); + }); } getTranslationProgressAriaLabel(): string { - if (this.numberOfRequiredAudio - - this.numberOfAudioNotAvailable === 1) { + if (this.numberOfRequiredAudio - this.numberOfAudioNotAvailable === 1) { return ( this.numberOfRequiredAudio - - this.numberOfAudioNotAvailable + ' item translated out of ' + - this.numberOfRequiredAudio + ' items'); + this.numberOfAudioNotAvailable + + ' item translated out of ' + + this.numberOfRequiredAudio + + ' items' + ); } else { return ( this.numberOfRequiredAudio - this.numberOfAudioNotAvailable + ' items translated out of ' + - this.numberOfRequiredAudio + ' items'); + this.numberOfRequiredAudio + + ' items' + ); } } updateTranslationWithChangeList(entityTranslation: EntityTranslation): void { - this.changeListService.getTranslationChangeList().forEach((changeDict) => { + this.changeListService.getTranslationChangeList().forEach(changeDict => { changeDict = changeDict as ExplorationTranslationChange; switch (changeDict.cmd) { case 'edit_translation': @@ -206,8 +214,8 @@ export class TranslatorOverviewComponent implements OnInit { entityTranslation.removeTranslation(changeDict.content_id); break; case 'mark_translations_needs_update': - changeDict = ( - changeDict as ExplorationChangeMarkTranslationsNeedsUpdate); + changeDict = + changeDict as ExplorationChangeMarkTranslationsNeedsUpdate; entityTranslation.markTranslationAsNeedingUpdate( changeDict.content_id ); @@ -217,16 +225,16 @@ export class TranslatorOverviewComponent implements OnInit { } ngOnInit(): void { - this.LAST_SELECTED_TRANSLATION_LANGUAGE = ( - 'last_selected_translation_lang'); - let lastSelectedTranslationLanguage = ( + this.LAST_SELECTED_TRANSLATION_LANGUAGE = 'last_selected_translation_lang'; + let lastSelectedTranslationLanguage = this.windowRef.nativeWindow.localStorage.getItem( - this.LAST_SELECTED_TRANSLATION_LANGUAGE)); - let prevLanguageCode = lastSelectedTranslationLanguage ? ( - lastSelectedTranslationLanguage) : ( - ExplorationEditorPageConstants.DEFAULT_AUDIO_LANGUAGE); - let allAudioLanguageCodes = this.languageUtilService - .getAllVoiceoverLanguageCodes(); + this.LAST_SELECTED_TRANSLATION_LANGUAGE + ); + let prevLanguageCode = lastSelectedTranslationLanguage + ? lastSelectedTranslationLanguage + : ExplorationEditorPageConstants.DEFAULT_AUDIO_LANGUAGE; + let allAudioLanguageCodes = + this.languageUtilService.getAllVoiceoverLanguageCodes(); if (this.routerService.getActiveTabName() === 'translation') { this.focusManagerService.setFocus('audioTranslationLanguageCodeField'); @@ -237,29 +245,33 @@ export class TranslatorOverviewComponent implements OnInit { this.languageCode = this.translationLanguageService.getActiveLanguageCode(); if (!this.languageCode) { this.languageCode = - allAudioLanguageCodes.indexOf(prevLanguageCode) !== -1 ? - prevLanguageCode : ( - ExplorationEditorPageConstants.DEFAULT_AUDIO_LANGUAGE); + allAudioLanguageCodes.indexOf(prevLanguageCode) !== -1 + ? prevLanguageCode + : ExplorationEditorPageConstants.DEFAULT_AUDIO_LANGUAGE; } - this.entityTranslationsService.getEntityTranslationsAsync( - this.languageCode - ).then((entityTranslation) => { - this.updateTranslationWithChangeList(entityTranslation); - this.translationLanguageService.setActiveLanguageCode(this.languageCode); - // We need to refresh the status service once the active language is - // set. - this.translationStatusService.refresh(); - this.routerService.onCenterGraph.emit(); + this.entityTranslationsService + .getEntityTranslationsAsync(this.languageCode) + .then(entityTranslation => { + this.updateTranslationWithChangeList(entityTranslation); + this.translationLanguageService.setActiveLanguageCode( + this.languageCode + ); + // We need to refresh the status service once the active language is + // set. + this.translationStatusService.refresh(); + this.routerService.onCenterGraph.emit(); - this.inTranslationMode = false; - this.inVoiceoverMode = false; - this.refreshDirectiveScope(); - }); + this.inTranslationMode = false; + this.inVoiceoverMode = false; + this.refreshDirectiveScope(); + }); } } -angular.module('oppia').directive('oppiaTranslatorOverview', +angular.module('oppia').directive( + 'oppiaTranslatorOverview', downgradeComponent({ - component: TranslatorOverviewComponent - }) as angular.IDirectiveFactory); + component: TranslatorOverviewComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-player-page/exploration-player-page-root.component.ts b/core/templates/pages/exploration-player-page/exploration-player-page-root.component.ts index d7deaf96c06d..0be673f4495a 100644 --- a/core/templates/pages/exploration-player-page/exploration-player-page-root.component.ts +++ b/core/templates/pages/exploration-player-page/exploration-player-page-root.component.ts @@ -16,11 +16,11 @@ * @fileoverview Exploration player page root component. */ -import { Component, ViewEncapsulation } from '@angular/core'; +import {Component, ViewEncapsulation} from '@angular/core'; @Component({ selector: 'oppia-exploration-player-page-root', templateUrl: './exploration-player-page-root.component.html', - encapsulation: ViewEncapsulation.None + encapsulation: ViewEncapsulation.None, }) export class ExplorationPlayerPageRootComponent {} diff --git a/core/templates/pages/exploration-player-page/exploration-player-page-routing.module.ts b/core/templates/pages/exploration-player-page/exploration-player-page-routing.module.ts index 571f5c6577fc..9818ffe6c488 100644 --- a/core/templates/pages/exploration-player-page/exploration-player-page-routing.module.ts +++ b/core/templates/pages/exploration-player-page/exploration-player-page-routing.module.ts @@ -16,24 +16,19 @@ * @fileoverview Routing module for exploration player page. */ -import { NgModule } from '@angular/core'; -import { Route, RouterModule } from '@angular/router'; -import { ExplorationPlayerPageRootComponent } from './exploration-player-page-root.component'; +import {NgModule} from '@angular/core'; +import {Route, RouterModule} from '@angular/router'; +import {ExplorationPlayerPageRootComponent} from './exploration-player-page-root.component'; const routes: Route[] = [ { path: '', - component: ExplorationPlayerPageRootComponent - } + component: ExplorationPlayerPageRootComponent, + }, ]; @NgModule({ - imports: [ - RouterModule.forChild(routes) - ], - exports: [ - RouterModule - ] + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], }) - export class ExplorationPlayerPageRoutingModule {} diff --git a/core/templates/pages/exploration-player-page/exploration-player-page.component.spec.ts b/core/templates/pages/exploration-player-page/exploration-player-page.component.spec.ts index 12b3015dc920..45481e12598f 100644 --- a/core/templates/pages/exploration-player-page/exploration-player-page.component.spec.ts +++ b/core/templates/pages/exploration-player-page/exploration-player-page.component.spec.ts @@ -12,20 +12,29 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; - -import { FetchExplorationBackendResponse, ReadOnlyExplorationBackendApiService } from 'domain/exploration/read-only-exploration-backend-api.service'; -import { ContextService } from 'services/context.service'; -import { MetaTagCustomizationService } from 'services/contextual/meta-tag-customization.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { KeyboardShortcutService } from 'services/keyboard-shortcut.service'; -import { PageTitleService } from 'services/page-title.service'; -import { ExplorationPermissions } from 'domain/exploration/exploration-permissions.model'; -import { ExplorationPermissionsBackendApiService } from 'domain/exploration/exploration-permissions-backend-api.service'; -import { ExplorationPlayerPageComponent } from './exploration-player-page.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; + +import { + FetchExplorationBackendResponse, + ReadOnlyExplorationBackendApiService, +} from 'domain/exploration/read-only-exploration-backend-api.service'; +import {ContextService} from 'services/context.service'; +import {MetaTagCustomizationService} from 'services/contextual/meta-tag-customization.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {KeyboardShortcutService} from 'services/keyboard-shortcut.service'; +import {PageTitleService} from 'services/page-title.service'; +import {ExplorationPermissions} from 'domain/exploration/exploration-permissions.model'; +import {ExplorationPermissionsBackendApiService} from 'domain/exploration/exploration-permissions-backend-api.service'; +import {ExplorationPlayerPageComponent} from './exploration-player-page.component'; /** * @fileoverview Unit tests for exploration player page component. @@ -45,26 +54,21 @@ describe('Exploration Player Page', () => { let keyboardShortcutService: KeyboardShortcutService; let metaTagCustomizationService: MetaTagCustomizationService; let pageTitleService: PageTitleService; - let readOnlyExplorationBackendApiService: - ReadOnlyExplorationBackendApiService; + let readOnlyExplorationBackendApiService: ReadOnlyExplorationBackendApiService; let translateService: TranslateService; - let explorationPermissionsBackendApiService: - ExplorationPermissionsBackendApiService; + let explorationPermissionsBackendApiService: ExplorationPermissionsBackendApiService; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - ExplorationPlayerPageComponent, - MockTranslatePipe - ], + declarations: [ExplorationPlayerPageComponent, MockTranslatePipe], providers: [ { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(ExplorationPlayerPageComponent); @@ -74,7 +78,8 @@ describe('Exploration Player Page', () => { metaTagCustomizationService = TestBed.inject(MetaTagCustomizationService); pageTitleService = TestBed.inject(PageTitleService); readOnlyExplorationBackendApiService = TestBed.inject( - ReadOnlyExplorationBackendApiService); + ReadOnlyExplorationBackendApiService + ); translateService = TestBed.inject(TranslateService); explorationPermissionsBackendApiService = TestBed.inject( ExplorationPermissionsBackendApiService @@ -91,71 +96,84 @@ describe('Exploration Player Page', () => { exploration: { title: 'Test', objective: 'test objective', - } + }, }; const explorationPermissionResponse = { - canPublish: true + canPublish: true, }; spyOn(contextService, 'getExplorationId').and.returnValue(expId); - spyOn(readOnlyExplorationBackendApiService, 'fetchExplorationAsync') - .and.returnValue(Promise.resolve( - response as FetchExplorationBackendResponse)); + spyOn( + readOnlyExplorationBackendApiService, + 'fetchExplorationAsync' + ).and.returnValue( + Promise.resolve(response as FetchExplorationBackendResponse) + ); spyOn(componentInstance, 'setPageTitle'); spyOn(componentInstance, 'subscribeToOnLangChange'); spyOn(metaTagCustomizationService, 'addOrReplaceMetaTags'); spyOn(keyboardShortcutService, 'bindExplorationPlayerShortcuts'); - spyOn(explorationPermissionsBackendApiService, 'getPermissionsAsync') - .and.returnValue(Promise.resolve( - explorationPermissionResponse as ExplorationPermissions)); + spyOn( + explorationPermissionsBackendApiService, + 'getPermissionsAsync' + ).and.returnValue( + Promise.resolve(explorationPermissionResponse as ExplorationPermissions) + ); componentInstance.ngOnInit(); tick(); expect(contextService.getExplorationId).toHaveBeenCalled(); - expect(readOnlyExplorationBackendApiService.fetchExplorationAsync) - .toHaveBeenCalledWith(expId, null); + expect( + readOnlyExplorationBackendApiService.fetchExplorationAsync + ).toHaveBeenCalledWith(expId, null); expect(componentInstance.setPageTitle).toHaveBeenCalled(); expect(componentInstance.subscribeToOnLangChange).toHaveBeenCalled(); - expect(explorationPermissionsBackendApiService.getPermissionsAsync) - .toHaveBeenCalled(); - expect(metaTagCustomizationService.addOrReplaceMetaTags) - .toHaveBeenCalledWith([ - { - propertyType: 'itemprop', - propertyValue: 'name', - content: response.exploration.title - }, - { - propertyType: 'itemprop', - propertyValue: 'description', - content: response.exploration.objective - }, - { - propertyType: 'property', - propertyValue: 'og:title', - content: response.exploration.title - }, - { - propertyType: 'property', - propertyValue: 'og:description', - content: response.exploration.objective - } - ]); - expect(keyboardShortcutService.bindExplorationPlayerShortcuts) - .toHaveBeenCalled(); + expect( + explorationPermissionsBackendApiService.getPermissionsAsync + ).toHaveBeenCalled(); + expect( + metaTagCustomizationService.addOrReplaceMetaTags + ).toHaveBeenCalledWith([ + { + propertyType: 'itemprop', + propertyValue: 'name', + content: response.exploration.title, + }, + { + propertyType: 'itemprop', + propertyValue: 'description', + content: response.exploration.objective, + }, + { + propertyType: 'property', + propertyValue: 'og:title', + content: response.exploration.title, + }, + { + propertyType: 'property', + propertyValue: 'og:description', + content: response.exploration.objective, + }, + ]); + expect( + keyboardShortcutService.bindExplorationPlayerShortcuts + ).toHaveBeenCalled(); expect(componentInstance.explorationIsUnpublished).toBe(true); })); - it('should obtain translated page title whenever the selected' + - 'language changes', () => { - componentInstance.subscribeToOnLangChange(); - spyOn(componentInstance, 'setPageTitle'); - translateService.onLangChange.emit(); + it( + 'should obtain translated page title whenever the selected' + + 'language changes', + () => { + componentInstance.subscribeToOnLangChange(); + spyOn(componentInstance, 'setPageTitle'); + translateService.onLangChange.emit(); - expect(componentInstance.directiveSubscriptions.closed).toBe(false); - expect(componentInstance.setPageTitle).toHaveBeenCalled(); - }); + expect(componentInstance.directiveSubscriptions.closed).toBe(false); + expect(componentInstance.setPageTitle).toHaveBeenCalled(); + } + ); it('should set new page title', () => { spyOn(translateService, 'instant').and.callThrough(); @@ -164,11 +182,14 @@ describe('Exploration Player Page', () => { componentInstance.setPageTitle(); expect(translateService.instant).toHaveBeenCalledWith( - 'I18N_EXPLORATION_PLAYER_PAGE_TITLE', { - explorationTitle: 'dummy_name' - }); + 'I18N_EXPLORATION_PLAYER_PAGE_TITLE', + { + explorationTitle: 'dummy_name', + } + ); expect(pageTitleService.setDocumentTitle).toHaveBeenCalledWith( - 'I18N_EXPLORATION_PLAYER_PAGE_TITLE'); + 'I18N_EXPLORATION_PLAYER_PAGE_TITLE' + ); }); it('should unsubscribe on component destruction', () => { diff --git a/core/templates/pages/exploration-player-page/exploration-player-page.component.ts b/core/templates/pages/exploration-player-page/exploration-player-page.component.ts index 54314cbd2dbc..5bf4c3eb312d 100644 --- a/core/templates/pages/exploration-player-page/exploration-player-page.component.ts +++ b/core/templates/pages/exploration-player-page/exploration-player-page.component.ts @@ -16,24 +16,27 @@ * @fileoverview Component for the exploration player page. */ -import { Component, OnDestroy } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { ExplorationPermissionsBackendApiService } from 'domain/exploration/exploration-permissions-backend-api.service'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; +import {Component, OnDestroy} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {ExplorationPermissionsBackendApiService} from 'domain/exploration/exploration-permissions-backend-api.service'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; -import { FetchExplorationBackendResponse, ReadOnlyExplorationBackendApiService } from 'domain/exploration/read-only-exploration-backend-api.service'; -import { ContextService } from 'services/context.service'; -import { MetaTagCustomizationService } from 'services/contextual/meta-tag-customization.service'; -import { UrlService } from 'services/contextual/url.service'; -import { KeyboardShortcutService } from 'services/keyboard-shortcut.service'; -import { PageTitleService } from 'services/page-title.service'; +import { + FetchExplorationBackendResponse, + ReadOnlyExplorationBackendApiService, +} from 'domain/exploration/read-only-exploration-backend-api.service'; +import {ContextService} from 'services/context.service'; +import {MetaTagCustomizationService} from 'services/contextual/meta-tag-customization.service'; +import {UrlService} from 'services/contextual/url.service'; +import {KeyboardShortcutService} from 'services/keyboard-shortcut.service'; +import {PageTitleService} from 'services/page-title.service'; require('interactions/interactionsRequires.ts'); @Component({ selector: 'oppia-exploration-player-page', - templateUrl: './exploration-player-page.component.html' + templateUrl: './exploration-player-page.component.html', }) export class ExplorationPlayerPageComponent implements OnDestroy { directiveSubscriptions = new Subscription(); @@ -44,61 +47,60 @@ export class ExplorationPlayerPageComponent implements OnDestroy { constructor( private contextService: ContextService, - private explorationPermissionsBackendApiService: - ExplorationPermissionsBackendApiService, + private explorationPermissionsBackendApiService: ExplorationPermissionsBackendApiService, private keyboardShortcutService: KeyboardShortcutService, private metaTagCustomizationService: MetaTagCustomizationService, private pageTitleService: PageTitleService, - private readOnlyExplorationBackendApiService: - ReadOnlyExplorationBackendApiService, + private readOnlyExplorationBackendApiService: ReadOnlyExplorationBackendApiService, private urlService: UrlService, private translateService: TranslateService ) {} ngOnInit(): void { let explorationId = this.contextService.getExplorationId(); - this.readOnlyExplorationBackendApiService.fetchExplorationAsync( - explorationId, null - ).then((response: FetchExplorationBackendResponse) => { - this.explorationTitle = response.exploration.title; - // The onLangChange event is initially fired before the exploration is - // loaded. Hence the first setpageTitle() call needs to made - // manually, and the onLangChange subscription is added after - // the exploration is fetch from the backend. - this.setPageTitle(); - this.subscribeToOnLangChange(); - this.metaTagCustomizationService.addOrReplaceMetaTags([ - { - propertyType: 'itemprop', - propertyValue: 'name', - content: response.exploration.title - }, - { - propertyType: 'itemprop', - propertyValue: 'description', - content: response.exploration.objective - }, - { - propertyType: 'property', - propertyValue: 'og:title', - content: response.exploration.title - }, - { - propertyType: 'property', - propertyValue: 'og:description', - content: response.exploration.objective - } - ]); - }).finally(() => { - this.isLoadingExploration = false; - } - ); + this.readOnlyExplorationBackendApiService + .fetchExplorationAsync(explorationId, null) + .then((response: FetchExplorationBackendResponse) => { + this.explorationTitle = response.exploration.title; + // The onLangChange event is initially fired before the exploration is + // loaded. Hence the first setpageTitle() call needs to made + // manually, and the onLangChange subscription is added after + // the exploration is fetch from the backend. + this.setPageTitle(); + this.subscribeToOnLangChange(); + this.metaTagCustomizationService.addOrReplaceMetaTags([ + { + propertyType: 'itemprop', + propertyValue: 'name', + content: response.exploration.title, + }, + { + propertyType: 'itemprop', + propertyValue: 'description', + content: response.exploration.objective, + }, + { + propertyType: 'property', + propertyValue: 'og:title', + content: response.exploration.title, + }, + { + propertyType: 'property', + propertyValue: 'og:description', + content: response.exploration.objective, + }, + ]); + }) + .finally(() => { + this.isLoadingExploration = false; + }); this.pageIsIframed = this.urlService.isIframed(); this.keyboardShortcutService.bindExplorationPlayerShortcuts(); - this.explorationPermissionsBackendApiService.getPermissionsAsync() - .then((response) => { + this.explorationPermissionsBackendApiService + .getPermissionsAsync() + .then(response => { this.explorationIsUnpublished = response.canPublish; }); } @@ -113,9 +115,11 @@ export class ExplorationPlayerPageComponent implements OnDestroy { setPageTitle(): void { let translatedTitle = this.translateService.instant( - 'I18N_EXPLORATION_PLAYER_PAGE_TITLE', { - explorationTitle: this.explorationTitle - }); + 'I18N_EXPLORATION_PLAYER_PAGE_TITLE', + { + explorationTitle: this.explorationTitle, + } + ); this.pageTitleService.setDocumentTitle(translatedTitle); } @@ -124,8 +128,9 @@ export class ExplorationPlayerPageComponent implements OnDestroy { } } -angular.module('oppia').directive('oppiaExplorationPlayerPage', +angular.module('oppia').directive( + 'oppiaExplorationPlayerPage', downgradeComponent({ - component: ExplorationPlayerPageComponent + component: ExplorationPlayerPageComponent, }) as angular.IDirectiveFactory ); diff --git a/core/templates/pages/exploration-player-page/exploration-player-page.constants.ts b/core/templates/pages/exploration-player-page/exploration-player-page.constants.ts index f361717aca99..58f6a10b245a 100644 --- a/core/templates/pages/exploration-player-page/exploration-player-page.constants.ts +++ b/core/templates/pages/exploration-player-page/exploration-player-page.constants.ts @@ -57,13 +57,14 @@ export const ExplorationPlayerConstants = { HINT_REQUEST_STRING_I18N_IDS: [ 'I18N_PLAYER_HINT_REQUEST_STRING_1', 'I18N_PLAYER_HINT_REQUEST_STRING_2', - 'I18N_PLAYER_HINT_REQUEST_STRING_3'], + 'I18N_PLAYER_HINT_REQUEST_STRING_3', + ], // Array of i18n IDs for nudging the learner towards checking the spelling. I18N_ANSWER_MISSPELLED_RESPONSE_TEXT_IDS: [ 'I18N_ANSWER_MISSPELLED_RESPONSE_TEXT_0', 'I18N_ANSWER_MISSPELLED_RESPONSE_TEXT_1', - 'I18N_ANSWER_MISSPELLED_RESPONSE_TEXT_2' + 'I18N_ANSWER_MISSPELLED_RESPONSE_TEXT_2', ], // Threshold value of edit distance for judging an answer as a misspelling. @@ -107,20 +108,20 @@ export const ExplorationPlayerConstants = { STATS_REPORTING_URLS: { ANSWER_SUBMITTED: '/explorehandler/answer_submitted_event/', - EXPLORATION_COMPLETED: ( - '/explorehandler/exploration_complete_event/'), - EXPLORATION_MAYBE_LEFT: ( - '/explorehandler/exploration_maybe_leave_event/'), - EXPLORATION_STARTED: ( - '/explorehandler/exploration_start_event/'), + EXPLORATION_COMPLETED: + '/explorehandler/exploration_complete_event/', + EXPLORATION_MAYBE_LEFT: + '/explorehandler/exploration_maybe_leave_event/', + EXPLORATION_STARTED: + '/explorehandler/exploration_start_event/', STATE_HIT: '/explorehandler/state_hit_event/', STATE_COMPLETED: '/explorehandler/state_complete_event/', - EXPLORATION_ACTUALLY_STARTED: ( - '/explorehandler/exploration_actual_start_event/'), + EXPLORATION_ACTUALLY_STARTED: + '/explorehandler/exploration_actual_start_event/', SOLUTION_HIT: '/explorehandler/solution_hit_event/', - LEAVE_FOR_REFRESHER_EXP: ( - '/explorehandler/leave_for_refresher_exp_event/'), - STATS_EVENTS: '/explorehandler/stats_events/' + LEAVE_FOR_REFRESHER_EXP: + '/explorehandler/leave_for_refresher_exp_event/', + STATS_EVENTS: '/explorehandler/stats_events/', }, FEEDBACK_POPOVER_PATH: diff --git a/core/templates/pages/exploration-player-page/exploration-player-page.module.ts b/core/templates/pages/exploration-player-page/exploration-player-page.module.ts index 819b08400e18..452040692221 100644 --- a/core/templates/pages/exploration-player-page/exploration-player-page.module.ts +++ b/core/templates/pages/exploration-player-page/exploration-player-page.module.ts @@ -16,31 +16,31 @@ * @fileoverview Module for the exploration player page. */ -import { NgModule } from '@angular/core'; -import { NgbModalModule, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'; -import { CommonModule } from '@angular/common'; -import { ExplorationPlayerViewerCommonModule } from './exploration-player-viewer-common.module'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { InteractionExtensionsModule } from 'interactions/interactions.module'; -import { MatButtonModule } from '@angular/material/button'; -import { LearnerLocalNavComponent } from './layout-directives/learner-local-nav.component'; -import { FlagExplorationModalComponent } from './modals/flag-exploration-modal.component'; -import { ExplorationSuccessfullyFlaggedModalComponent } from './modals/exploration-successfully-flagged-modal.component'; -import { LearnerViewInfoComponent } from './layout-directives/learner-view-info.component'; -import { MaterialModule } from 'modules/material.module'; -import { RefresherExplorationConfirmationModal } from './modals/refresher-exploration-confirmation-modal.component'; -import { ExplorationPlayerPageComponent } from './exploration-player-page.component'; -import { ExplorationPlayerPageRoutingModule } from './exploration-player-page-routing.module'; -import { ExplorationPlayerPageRootComponent } from './exploration-player-page-root.component'; -import { ProgressReminderModalComponent } from './templates/progress-reminder-modal.component'; -import { HintAndSolutionModalService } from './services/hint-and-solution-modal.service'; -import { FatigueDetectionService } from './services/fatigue-detection.service'; +import {NgModule} from '@angular/core'; +import {NgbModalModule, NgbPopoverModule} from '@ng-bootstrap/ng-bootstrap'; +import {CommonModule} from '@angular/common'; +import {ExplorationPlayerViewerCommonModule} from './exploration-player-viewer-common.module'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {InteractionExtensionsModule} from 'interactions/interactions.module'; +import {MatButtonModule} from '@angular/material/button'; +import {LearnerLocalNavComponent} from './layout-directives/learner-local-nav.component'; +import {FlagExplorationModalComponent} from './modals/flag-exploration-modal.component'; +import {ExplorationSuccessfullyFlaggedModalComponent} from './modals/exploration-successfully-flagged-modal.component'; +import {LearnerViewInfoComponent} from './layout-directives/learner-view-info.component'; +import {MaterialModule} from 'modules/material.module'; +import {RefresherExplorationConfirmationModal} from './modals/refresher-exploration-confirmation-modal.component'; +import {ExplorationPlayerPageComponent} from './exploration-player-page.component'; +import {ExplorationPlayerPageRoutingModule} from './exploration-player-page-routing.module'; +import {ExplorationPlayerPageRootComponent} from './exploration-player-page-root.component'; +import {ProgressReminderModalComponent} from './templates/progress-reminder-modal.component'; +import {HintAndSolutionModalService} from './services/hint-and-solution-modal.service'; +import {FatigueDetectionService} from './services/fatigue-detection.service'; import 'third-party-imports/guppy.import'; import 'third-party-imports/midi-js.import'; import 'third-party-imports/skulpt.import'; -import { ToastrModule } from 'ngx-toastr'; -import { toastrConfig } from 'pages/oppia-root/app.module'; +import {ToastrModule} from 'ngx-toastr'; +import {toastrConfig} from 'pages/oppia-root/app.module'; @NgModule({ imports: [ @@ -76,13 +76,7 @@ import { toastrConfig } from 'pages/oppia-root/app.module'; LearnerViewInfoComponent, RefresherExplorationConfirmationModal, ], - providers: [ - HintAndSolutionModalService, - FatigueDetectionService, - ], - exports: [ - LearnerLocalNavComponent, - LearnerViewInfoComponent, - ], + providers: [HintAndSolutionModalService, FatigueDetectionService], + exports: [LearnerLocalNavComponent, LearnerViewInfoComponent], }) export class ExplorationPlayerPageModule {} diff --git a/core/templates/pages/exploration-player-page/exploration-player-viewer-common.module.ts b/core/templates/pages/exploration-player-page/exploration-player-viewer-common.module.ts index be87604ca571..9f2ed6fa4f75 100644 --- a/core/templates/pages/exploration-player-page/exploration-player-viewer-common.module.ts +++ b/core/templates/pages/exploration-player-page/exploration-player-viewer-common.module.ts @@ -19,29 +19,25 @@ import 'core-js/es7/reflect'; import 'zone.js'; -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { SwitchContentLanguageRefreshRequiredModalComponent } from './switch-content-language-refresh-required-modal.component'; -import { LessonInformationCardModalComponent } from './templates/lesson-information-card-modal.component'; -import { SharedComponentsModule } from 'components/shared-component.module'; +import {CommonModule} from '@angular/common'; +import {NgModule} from '@angular/core'; +import {SwitchContentLanguageRefreshRequiredModalComponent} from './switch-content-language-refresh-required-modal.component'; +import {LessonInformationCardModalComponent} from './templates/lesson-information-card-modal.component'; +import {SharedComponentsModule} from 'components/shared-component.module'; @NgModule({ - imports: [ - CommonModule, - SharedComponentsModule - ], + imports: [CommonModule, SharedComponentsModule], declarations: [ SwitchContentLanguageRefreshRequiredModalComponent, - LessonInformationCardModalComponent + LessonInformationCardModalComponent, ], entryComponents: [ SwitchContentLanguageRefreshRequiredModalComponent, - LessonInformationCardModalComponent + LessonInformationCardModalComponent, ], exports: [ SwitchContentLanguageRefreshRequiredModalComponent, - LessonInformationCardModalComponent + LessonInformationCardModalComponent, ], }) - -export class ExplorationPlayerViewerCommonModule { } +export class ExplorationPlayerViewerCommonModule {} diff --git a/core/templates/pages/exploration-player-page/layout-directives/audio-bar.component.spec.ts b/core/templates/pages/exploration-player-page/layout-directives/audio-bar.component.spec.ts index c68a3858b1f4..81b7881b6f96 100644 --- a/core/templates/pages/exploration-player-page/layout-directives/audio-bar.component.spec.ts +++ b/core/templates/pages/exploration-player-page/layout-directives/audio-bar.component.spec.ts @@ -16,23 +16,31 @@ * @fileoverview Unit tests for the AudioBarComponent. */ -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, discardPeriodicTasks, fakeAsync, flush, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; - -import { AudioBarComponent } from 'pages/exploration-player-page/layout-directives/audio-bar.component'; -import { Voiceover } from 'domain/exploration/voiceover.model'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; -import { AudioBarStatusService } from 'services/audio-bar-status.service'; -import { AudioPlayerService } from 'services/audio-player.service'; -import { AutogeneratedAudioPlayerService } from 'services/autogenerated-audio-player.service'; -import { AudioPreloaderService } from '../services/audio-preloader.service'; -import { AudioTranslationLanguageService } from '../services/audio-translation-language.service'; -import { AudioTranslationManagerService } from '../services/audio-translation-manager.service'; -import { PlayerPositionService } from '../services/player-position.service'; -import { ContextService } from 'services/context.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import { + ComponentFixture, + discardPeriodicTasks, + fakeAsync, + flush, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; + +import {AudioBarComponent} from 'pages/exploration-player-page/layout-directives/audio-bar.component'; +import {Voiceover} from 'domain/exploration/voiceover.model'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; +import {AudioBarStatusService} from 'services/audio-bar-status.service'; +import {AudioPlayerService} from 'services/audio-player.service'; +import {AutogeneratedAudioPlayerService} from 'services/autogenerated-audio-player.service'; +import {AudioPreloaderService} from '../services/audio-preloader.service'; +import {AudioTranslationLanguageService} from '../services/audio-translation-language.service'; +import {AudioTranslationManagerService} from '../services/audio-translation-manager.service'; +import {PlayerPositionService} from '../services/player-position.service'; +import {ContextService} from 'services/context.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; describe('Audio Bar Component', () => { let component: AudioBarComponent; @@ -52,12 +60,9 @@ describe('Audio Bar Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - AudioBarComponent, - MockTranslatePipe - ], + declarations: [AudioBarComponent, MockTranslatePipe], providers: [], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -67,13 +72,16 @@ describe('Audio Bar Component', () => { audioPlayerService = TestBed.inject(AudioPlayerService); audioBarStatusService = TestBed.inject(AudioBarStatusService); audioTranslationLanguageService = TestBed.inject( - AudioTranslationLanguageService); + AudioTranslationLanguageService + ); audioPreloaderService = TestBed.inject(AudioPreloaderService); assetsBackendApiService = TestBed.inject(AssetsBackendApiService); audioTranslationManagerService = TestBed.inject( - AudioTranslationManagerService); + AudioTranslationManagerService + ); autogeneratedAudioPlayerService = TestBed.inject( - AutogeneratedAudioPlayerService); + AutogeneratedAudioPlayerService + ); playerPositionService = TestBed.inject(PlayerPositionService); contextService = TestBed.inject(ContextService); i18nLanguageCodeService = TestBed.inject(I18nLanguageCodeService); @@ -90,58 +98,71 @@ describe('Audio Bar Component', () => { component.ngOnDestroy(); }); - it('should set secondary audio translations when audio bar ' + - 'is opened and audio is playing', fakeAsync(() => { - let params = { - audioTranslations: {}, - componentName: 'feedback', - html: '' - }; - let mockOnAutoplayAudioEventEmitter = new EventEmitter(); - spyOnProperty(audioPlayerService, 'onAutoplayAudio') - .and.returnValue(mockOnAutoplayAudioEventEmitter); - let secondaryTranslaionsSpy = spyOn( - audioTranslationManagerService, 'setSecondaryAudioTranslations') - .and.callThrough(); - - component.ngOnInit(); - component.expandAudioBar(); - component.isPaused = false; - fixture.detectChanges(); + it( + 'should set secondary audio translations when audio bar ' + + 'is opened and audio is playing', + fakeAsync(() => { + let params = { + audioTranslations: {}, + componentName: 'feedback', + html: '', + }; + let mockOnAutoplayAudioEventEmitter = new EventEmitter(); + spyOnProperty(audioPlayerService, 'onAutoplayAudio').and.returnValue( + mockOnAutoplayAudioEventEmitter + ); + let secondaryTranslaionsSpy = spyOn( + audioTranslationManagerService, + 'setSecondaryAudioTranslations' + ).and.callThrough(); - mockOnAutoplayAudioEventEmitter.emit(params); - flush(); - discardPeriodicTasks(); - fixture.detectChanges(); + component.ngOnInit(); + component.expandAudioBar(); + component.isPaused = false; + fixture.detectChanges(); - expect(secondaryTranslaionsSpy).toHaveBeenCalledWith( - params.audioTranslations, params.html, params.componentName); - })); + mockOnAutoplayAudioEventEmitter.emit(params); + flush(); + discardPeriodicTasks(); + fixture.detectChanges(); + + expect(secondaryTranslaionsSpy).toHaveBeenCalledWith( + params.audioTranslations, + params.html, + params.componentName + ); + }) + ); - it('should set current time when calling \'setProgress\'', () => { + it("should set current time when calling 'setProgress'", () => { // This time period is used to set progress // when user pulls the drag button in audio bar. let param = { - value: 100 + value: 100, }; let currentTimeSpy = spyOn( - audioPlayerService, 'setCurrentTime').and.callThrough(); + audioPlayerService, + 'setCurrentTime' + ).and.callThrough(); component.setProgress(param); expect(currentTimeSpy).toHaveBeenCalledWith(100); }); - it('should check whether the auto generated language ' + - 'code is selected', () => { - let autoGeneratedLanguageSpy = spyOn( - audioTranslationLanguageService, 'isAutogeneratedLanguageCodeSelected') - .and.returnValue(false); - let result = component.isAutogeneratedLanguageCodeSelected(); + it( + 'should check whether the auto generated language ' + 'code is selected', + () => { + let autoGeneratedLanguageSpy = spyOn( + audioTranslationLanguageService, + 'isAutogeneratedLanguageCodeSelected' + ).and.returnValue(false); + let result = component.isAutogeneratedLanguageCodeSelected(); - expect(result).toBe(false); - expect(autoGeneratedLanguageSpy).toHaveBeenCalled(); - }); + expect(result).toBe(false); + expect(autoGeneratedLanguageSpy).toHaveBeenCalled(); + } + ); it('should check if the audio bar is available', () => { // Audio bar is only accessible if the number of @@ -149,11 +170,12 @@ describe('Audio Bar Component', () => { component.languagesInExploration = [ { value: 'en', - displayed: 'english' - }, { + displayed: 'english', + }, + { value: 'es', - displayed: 'spanish' - } + displayed: 'spanish', + }, ]; let result = component.isAudioBarAvailable(); @@ -166,35 +188,43 @@ describe('Audio Bar Component', () => { it('should return true if the selected language is RTL', () => { spyOn(i18nLanguageCodeService, 'isCurrentLanguageRTL').and.returnValue( - true); + true + ); expect(component.isLanguageRTL()).toBe(true); }); it('should return false if the selected language is not RTL', () => { spyOn(i18nLanguageCodeService, 'isCurrentLanguageRTL').and.returnValue( - false); + false + ); expect(component.isLanguageRTL()).toBe(false); }); - it('should forward audio with time interval of five seconds ' + - 'when audio forward button is clicked', () => { - let forwardSpy = spyOn(audioPlayerService, 'forward').and.callThrough(); + it( + 'should forward audio with time interval of five seconds ' + + 'when audio forward button is clicked', + () => { + let forwardSpy = spyOn(audioPlayerService, 'forward').and.callThrough(); - component.onForwardButtonClicked(); + component.onForwardButtonClicked(); - expect(forwardSpy).toHaveBeenCalledWith(5); - }); + expect(forwardSpy).toHaveBeenCalledWith(5); + } + ); - it('should rewind audio with time interval of five seconds ' + - 'when audio rewind button is clicked', () => { - let rewindSpy = spyOn(audioPlayerService, 'rewind').and.callThrough(); + it( + 'should rewind audio with time interval of five seconds ' + + 'when audio rewind button is clicked', + () => { + let rewindSpy = spyOn(audioPlayerService, 'rewind').and.callThrough(); - component.onBackwardButtonClicked(); + component.onBackwardButtonClicked(); - expect(rewindSpy).toHaveBeenCalledWith(5); - }); + expect(rewindSpy).toHaveBeenCalledWith(5); + } + ); it('should expand audio bar when clicking expand button', () => { // Setting audio bar in collapsed view. @@ -211,15 +241,19 @@ describe('Audio Bar Component', () => { }); it('should return selected audio language code', () => { - spyOn(audioTranslationLanguageService, 'getCurrentAudioLanguageCode') - .and.returnValue('en'); + spyOn( + audioTranslationLanguageService, + 'getCurrentAudioLanguageCode' + ).and.returnValue('en'); let result = component.getCurrentAudioLanguageCode(); expect(result).toBe('en'); }); it('should return selected audio language description', () => { - spyOn(audioTranslationLanguageService, 'getCurrentAudioLanguageDescription') - .and.returnValue('description'); + spyOn( + audioTranslationLanguageService, + 'getCurrentAudioLanguageDescription' + ).and.returnValue('description'); let result = component.getCurrentAudioLanguageDescription(); expect(result).toBe('description'); }); @@ -230,44 +264,53 @@ describe('Audio Bar Component', () => { filename: 'audio-en.mp3', file_size_bytes: 0.5, needs_update: false, - duration_secs: 0.5 + duration_secs: 0.5, }), es: Voiceover.createFromBackendDict({ filename: 'audio-es.mp3', file_size_bytes: 0.5, needs_update: false, - duration_secs: 0.5 - }) + duration_secs: 0.5, + }), }; - spyOn(audioTranslationManagerService, 'getCurrentAudioTranslations') - .and.returnValue(audioTranslation); + spyOn( + audioTranslationManagerService, + 'getCurrentAudioTranslations' + ).and.returnValue(audioTranslation); // Setting selected language to be 'en'. - spyOn(audioTranslationLanguageService, 'getCurrentAudioLanguageCode') - .and.returnValue('en'); + spyOn( + audioTranslationLanguageService, + 'getCurrentAudioLanguageCode' + ).and.returnValue('en'); let result = component.getVoiceoverInCurrentLanguage(); expect(result).toBe(audioTranslation.en); }); it('should check whether the audio is playing currently', () => { - let isPlayingSpy = spyOn( - audioPlayerService, 'isPlaying').and.returnValue(false); + let isPlayingSpy = spyOn(audioPlayerService, 'isPlaying').and.returnValue( + false + ); let result = component.isAudioPlaying(); expect(result).toBe(false); expect(isPlayingSpy).toHaveBeenCalled(); }); - it('should check whether the audio is selected by ' + - 'auto generated language code', () => { - let autogeneratedLanguageSpy = spyOn( - audioTranslationLanguageService, 'isAutogeneratedLanguageCodeSelected') - .and.returnValue(false); - let result = component.isAutogeneratedLanguageCodeSelected(); - - expect(result).toBe(false); - expect(autogeneratedLanguageSpy).toHaveBeenCalled(); - }); + it( + 'should check whether the audio is selected by ' + + 'auto generated language code', + () => { + let autogeneratedLanguageSpy = spyOn( + audioTranslationLanguageService, + 'isAutogeneratedLanguageCodeSelected' + ).and.returnValue(false); + let result = component.isAutogeneratedLanguageCodeSelected(); + + expect(result).toBe(false); + expect(autogeneratedLanguageSpy).toHaveBeenCalled(); + } + ); it('should check if the audio is available in selected language', () => { let audioTranslation = { @@ -275,319 +318,401 @@ describe('Audio Bar Component', () => { filename: 'audio-en.mp3', file_size_bytes: 0.5, needs_update: false, - duration_secs: 0.5 + duration_secs: 0.5, }), es: Voiceover.createFromBackendDict({ filename: 'audio-es.mp3', file_size_bytes: 0.5, needs_update: false, - duration_secs: 0.5 - }) - }; - spyOn(audioTranslationManagerService, 'getCurrentAudioTranslations') - .and.returnValue(audioTranslation); - // Setting selected language to be 'en'. - spyOn(audioTranslationLanguageService, 'getCurrentAudioLanguageCode') - .and.returnValue('en'); - - let result = component.isAudioAvailableInCurrentLanguage(); - expect(result).toBe(true); - }); - - it('should return true if the selected audio translation ' + - 'needs to be updated which is not auto generated language code', () => { - let audioTranslation = { - en: Voiceover.createFromBackendDict({ - filename: 'audio-en.mp3', - file_size_bytes: 0.5, - needs_update: true, - duration_secs: 0.5 + duration_secs: 0.5, }), - es: Voiceover.createFromBackendDict({ - filename: 'audio-es.mp3', - file_size_bytes: 0.5, - needs_update: true, - duration_secs: 0.5 - }) }; - spyOn(audioTranslationManagerService, 'getCurrentAudioTranslations') - .and.returnValue(audioTranslation); + spyOn( + audioTranslationManagerService, + 'getCurrentAudioTranslations' + ).and.returnValue(audioTranslation); // Setting selected language to be 'en'. - spyOn(audioTranslationLanguageService, 'getCurrentAudioLanguageCode') - .and.returnValue('en'); - - let result = component.doesCurrentAudioTranslationNeedUpdate(); - - expect(result).toBe(true); - }); - - it('should not check whether the auto generated audio ' + - 'language code is upto to date', () => { spyOn( - audioTranslationLanguageService, 'isAutogeneratedLanguageCodeSelected') - .and.returnValue(true); - - let result = component.doesCurrentAudioTranslationNeedUpdate(); + audioTranslationLanguageService, + 'getCurrentAudioLanguageCode' + ).and.returnValue('en'); - expect(result).toBe(false); + let result = component.isAudioAvailableInCurrentLanguage(); + expect(result).toBe(true); }); - describe('on clicking play pause button ', () => { - it('should play auto generated audio translation when ' + - 'play button is clicked', () => { - // Setting auto generated langugae to be true. - spyOn( - audioTranslationLanguageService, 'isAutogeneratedLanguageCodeSelected') - .and.returnValue(true); - // Setting audio is playing to be false. - spyOn(autogeneratedAudioPlayerService, 'isPlaying') - .and.returnValue(false); - spyOn(audioTranslationLanguageService, 'getSpeechSynthesisLanguageCode') - .and.returnValue(''); - spyOn( - audioTranslationManagerService, - 'getCurrentHtmlForAutogeneratedSequentialAudio' - ).and.returnValue('

test

'); - let playSpy = spyOn(autogeneratedAudioPlayerService, 'play') - .and.callFake((html, language, cb) => { - cb(); - }); - - component.onPlayButtonClicked(); - expect(playSpy).toHaveBeenCalled(); - }); - - it('should throw error if speech synthesis language code ' + - 'is null', () => { - // Setting auto generated langugae to be true. - spyOn( - audioTranslationLanguageService, 'isAutogeneratedLanguageCodeSelected') - .and.returnValue(true); - // Setting audio is playing to be false. - spyOn(autogeneratedAudioPlayerService, 'isPlaying') - .and.returnValue(false); - spyOn(audioTranslationLanguageService, 'getSpeechSynthesisLanguageCode') - .and.returnValue(null); - spyOn(autogeneratedAudioPlayerService, 'play') - .and.callFake((html, language, cb) => { - cb(); - }); - - expect(() => { - component.onPlayButtonClicked(); - }).toThrowError( - 'speechSynthesisLanguageCode cannot be null at this point.'); - }); - - it('should pause auto generated audio translation when ' + - 'pause button is clicked', () => { - // Setting auto generated langugae to be true. - spyOn( - audioTranslationLanguageService, 'isAutogeneratedLanguageCodeSelected') - .and.returnValue(true); - // Setting audio is playing to be true. - spyOn(autogeneratedAudioPlayerService, 'isPlaying') - .and.returnValue(true); - let pauseSpy = spyOn(autogeneratedAudioPlayerService, 'cancel') - .and.callThrough(); - - component.onPlayButtonClicked(); - expect(pauseSpy).toHaveBeenCalled(); - }); - - it('should play uploaded audio translation when ' + - 'play button is clicked and when tracks are loaded', () => { + it( + 'should return true if the selected audio translation ' + + 'needs to be updated which is not auto generated language code', + () => { let audioTranslation = { en: Voiceover.createFromBackendDict({ filename: 'audio-en.mp3', file_size_bytes: 0.5, - needs_update: false, - duration_secs: 0.5 + needs_update: true, + duration_secs: 0.5, }), es: Voiceover.createFromBackendDict({ filename: 'audio-es.mp3', file_size_bytes: 0.5, - needs_update: false, - duration_secs: 0.5 - }) - }; - spyOn(audioTranslationManagerService, 'getCurrentAudioTranslations') - .and.returnValue(audioTranslation); - // Setting selected language to be 'en'. - spyOn(audioTranslationLanguageService, 'getCurrentAudioLanguageCode') - .and.returnValue('en'); - // Setting auto generated langugae to be false. - spyOn( - audioTranslationLanguageService, 'isAutogeneratedLanguageCodeSelected') - .and.returnValue(false); - // Setting audio is playing to be true. - spyOn(audioPlayerService, 'isPlaying') - .and.returnValue(false); - // Settings audio tracks loaded to be true. - spyOn(audioPlayerService, 'isTrackLoaded').and.returnValue(true); - let playSpy = spyOn(audioPlayerService, 'play') - .and.callThrough(); - - component.onPlayButtonClicked(); - expect(playSpy).toHaveBeenCalled(); - }); - - it('should load audio track and play audio when ' + - 'play button is clicked', () => { - let audioTranslation = { - en: Voiceover.createFromBackendDict({ - filename: 'audio-en.mp3', - file_size_bytes: 0.5, - needs_update: false, - duration_secs: 0.5 + needs_update: true, + duration_secs: 0.5, }), - es: Voiceover.createFromBackendDict({ - filename: 'audio-es.mp3', - file_size_bytes: 0.5, - needs_update: false, - duration_secs: 0.5 - }) }; - spyOn(audioTranslationManagerService, 'getCurrentAudioTranslations') - .and.returnValue(audioTranslation); - // Setting selected language to be 'en'. - spyOn(audioTranslationLanguageService, 'getCurrentAudioLanguageCode') - .and.returnValue('en'); - // Setting auto generated langugae to be false. spyOn( - audioTranslationLanguageService, 'isAutogeneratedLanguageCodeSelected') - .and.returnValue(false); - // Setting audio is playing to be true. - spyOn(audioPlayerService, 'isPlaying') - .and.returnValue(false); - // Settings audio tracks loaded to be false. - spyOn(audioPlayerService, 'isTrackLoaded').and.returnValue(false); - let loadAndPlaySpy = spyOn(component, 'loadAndPlayAudioTranslation') - .and.returnValue(); - spyOn(playerPositionService, 'getCurrentStateName') - .and.returnValue('Start'); - spyOn(audioPreloaderService, 'restartAudioPreloader') - .and.returnValue(); - - component.onPlayButtonClicked(); - expect(loadAndPlaySpy).toHaveBeenCalled(); - }); - - it('should pause uploaded audio translation when ' + - 'pause button is clicked', () => { - let audioTranslation = { - en: Voiceover.createFromBackendDict({ - filename: 'audio-en.mp3', - file_size_bytes: 0.5, - needs_update: false, - duration_secs: 0.5 - }), - es: Voiceover.createFromBackendDict({ - filename: 'audio-es.mp3', - file_size_bytes: 0.5, - needs_update: false, - duration_secs: 0.5 - }) - }; - spyOn(audioTranslationManagerService, 'getCurrentAudioTranslations') - .and.returnValue(audioTranslation); + audioTranslationManagerService, + 'getCurrentAudioTranslations' + ).and.returnValue(audioTranslation); // Setting selected language to be 'en'. - spyOn(audioTranslationLanguageService, 'getCurrentAudioLanguageCode') - .and.returnValue('en'); - // Setting auto generated langugae to be false. spyOn( - audioTranslationLanguageService, 'isAutogeneratedLanguageCodeSelected') - .and.returnValue(false); - // Setting audio is playing to be true. - spyOn(audioPlayerService, 'isPlaying') - .and.returnValue(true); - let pauseSpy = spyOn(audioPlayerService, 'pause') - .and.callThrough(); - - component.onPlayButtonClicked(); - expect(pauseSpy).toHaveBeenCalled(); - }); - - it('should load audio track and play audio ' + - 'which are stored in cache', fakeAsync(() => { - let audioTranslation = { - en: Voiceover.createFromBackendDict({ - filename: 'audio-en.mp3', - file_size_bytes: 0.5, - needs_update: false, - duration_secs: 0.5 - }), - es: Voiceover.createFromBackendDict({ - filename: 'audio-es.mp3', - file_size_bytes: 0.5, - needs_update: false, - duration_secs: 0.5 - }) - }; - spyOn(audioTranslationManagerService, 'getCurrentAudioTranslations') - .and.returnValue(audioTranslation); - // Setting selected language to be 'en'. - spyOn(audioTranslationLanguageService, 'getCurrentAudioLanguageCode') - .and.returnValue('en'); - spyOn(audioPreloaderService, 'setMostRecentlyRequestedAudioFilename') - .and.callThrough(); - // Setting cached value to be true. - spyOn(assetsBackendApiService, 'isCached').and.returnValue(true); - spyOn(audioPlayerService, 'loadAsync').and.returnValue(Promise.resolve()); - let playCacheAudioSpy = spyOn(component, 'playCachedAudioTranslation') - .and.callThrough(); - let playSpy = spyOn(audioPlayerService, 'play') - .and.callThrough(); - - component.loadAndPlayAudioTranslation(); - tick(); - discardPeriodicTasks(); + audioTranslationLanguageService, + 'getCurrentAudioLanguageCode' + ).and.returnValue('en'); - expect(playCacheAudioSpy).toHaveBeenCalled(); - expect(playSpy).toHaveBeenCalled(); - })); + let result = component.doesCurrentAudioTranslationNeedUpdate(); - it('should restart audio track if audio is not' + - 'stored in cache', fakeAsync(() => { - let audioTranslation = { - en: Voiceover.createFromBackendDict({ - filename: 'audio-en.mp3', - file_size_bytes: 0.5, - needs_update: false, - duration_secs: 0.5 - }), - es: Voiceover.createFromBackendDict({ - filename: 'audio-es.mp3', - file_size_bytes: 0.5, - needs_update: false, - duration_secs: 0.5 - }) - }; - spyOn(audioTranslationManagerService, 'getCurrentAudioTranslations') - .and.returnValue(audioTranslation); - // Setting selected language to be 'en'. - spyOn(audioTranslationLanguageService, 'getCurrentAudioLanguageCode') - .and.returnValue('en'); - spyOn(audioPreloaderService, 'setMostRecentlyRequestedAudioFilename') - .and.callThrough(); - // Setting cached value to be true. - spyOn(assetsBackendApiService, 'isCached').and.returnValue(false); - spyOn(playerPositionService, 'getCurrentStateName').and.returnValue( - 'Start'); - let restartAudioSpy = spyOn( - audioPreloaderService, 'restartAudioPreloader').and.returnValue(); - - component.loadAndPlayAudioTranslation(); - tick(); - - expect(restartAudioSpy).toHaveBeenCalled(); - })); + expect(result).toBe(true); + } + ); + + it( + 'should not check whether the auto generated audio ' + + 'language code is upto to date', + () => { + spyOn( + audioTranslationLanguageService, + 'isAutogeneratedLanguageCodeSelected' + ).and.returnValue(true); + + let result = component.doesCurrentAudioTranslationNeedUpdate(); + + expect(result).toBe(false); + } + ); + + describe('on clicking play pause button ', () => { + it( + 'should play auto generated audio translation when ' + + 'play button is clicked', + () => { + // Setting auto generated langugae to be true. + spyOn( + audioTranslationLanguageService, + 'isAutogeneratedLanguageCodeSelected' + ).and.returnValue(true); + // Setting audio is playing to be false. + spyOn(autogeneratedAudioPlayerService, 'isPlaying').and.returnValue( + false + ); + spyOn( + audioTranslationLanguageService, + 'getSpeechSynthesisLanguageCode' + ).and.returnValue(''); + spyOn( + audioTranslationManagerService, + 'getCurrentHtmlForAutogeneratedSequentialAudio' + ).and.returnValue('

test

'); + let playSpy = spyOn( + autogeneratedAudioPlayerService, + 'play' + ).and.callFake((html, language, cb) => { + cb(); + }); + + component.onPlayButtonClicked(); + expect(playSpy).toHaveBeenCalled(); + } + ); + + it( + 'should throw error if speech synthesis language code ' + 'is null', + () => { + // Setting auto generated langugae to be true. + spyOn( + audioTranslationLanguageService, + 'isAutogeneratedLanguageCodeSelected' + ).and.returnValue(true); + // Setting audio is playing to be false. + spyOn(autogeneratedAudioPlayerService, 'isPlaying').and.returnValue( + false + ); + spyOn( + audioTranslationLanguageService, + 'getSpeechSynthesisLanguageCode' + ).and.returnValue(null); + spyOn(autogeneratedAudioPlayerService, 'play').and.callFake( + (html, language, cb) => { + cb(); + } + ); + + expect(() => { + component.onPlayButtonClicked(); + }).toThrowError( + 'speechSynthesisLanguageCode cannot be null at this point.' + ); + } + ); + + it( + 'should pause auto generated audio translation when ' + + 'pause button is clicked', + () => { + // Setting auto generated langugae to be true. + spyOn( + audioTranslationLanguageService, + 'isAutogeneratedLanguageCodeSelected' + ).and.returnValue(true); + // Setting audio is playing to be true. + spyOn(autogeneratedAudioPlayerService, 'isPlaying').and.returnValue( + true + ); + let pauseSpy = spyOn( + autogeneratedAudioPlayerService, + 'cancel' + ).and.callThrough(); + + component.onPlayButtonClicked(); + expect(pauseSpy).toHaveBeenCalled(); + } + ); + + it( + 'should play uploaded audio translation when ' + + 'play button is clicked and when tracks are loaded', + () => { + let audioTranslation = { + en: Voiceover.createFromBackendDict({ + filename: 'audio-en.mp3', + file_size_bytes: 0.5, + needs_update: false, + duration_secs: 0.5, + }), + es: Voiceover.createFromBackendDict({ + filename: 'audio-es.mp3', + file_size_bytes: 0.5, + needs_update: false, + duration_secs: 0.5, + }), + }; + spyOn( + audioTranslationManagerService, + 'getCurrentAudioTranslations' + ).and.returnValue(audioTranslation); + // Setting selected language to be 'en'. + spyOn( + audioTranslationLanguageService, + 'getCurrentAudioLanguageCode' + ).and.returnValue('en'); + // Setting auto generated langugae to be false. + spyOn( + audioTranslationLanguageService, + 'isAutogeneratedLanguageCodeSelected' + ).and.returnValue(false); + // Setting audio is playing to be true. + spyOn(audioPlayerService, 'isPlaying').and.returnValue(false); + // Settings audio tracks loaded to be true. + spyOn(audioPlayerService, 'isTrackLoaded').and.returnValue(true); + let playSpy = spyOn(audioPlayerService, 'play').and.callThrough(); + + component.onPlayButtonClicked(); + expect(playSpy).toHaveBeenCalled(); + } + ); + + it( + 'should load audio track and play audio when ' + 'play button is clicked', + () => { + let audioTranslation = { + en: Voiceover.createFromBackendDict({ + filename: 'audio-en.mp3', + file_size_bytes: 0.5, + needs_update: false, + duration_secs: 0.5, + }), + es: Voiceover.createFromBackendDict({ + filename: 'audio-es.mp3', + file_size_bytes: 0.5, + needs_update: false, + duration_secs: 0.5, + }), + }; + spyOn( + audioTranslationManagerService, + 'getCurrentAudioTranslations' + ).and.returnValue(audioTranslation); + // Setting selected language to be 'en'. + spyOn( + audioTranslationLanguageService, + 'getCurrentAudioLanguageCode' + ).and.returnValue('en'); + // Setting auto generated langugae to be false. + spyOn( + audioTranslationLanguageService, + 'isAutogeneratedLanguageCodeSelected' + ).and.returnValue(false); + // Setting audio is playing to be true. + spyOn(audioPlayerService, 'isPlaying').and.returnValue(false); + // Settings audio tracks loaded to be false. + spyOn(audioPlayerService, 'isTrackLoaded').and.returnValue(false); + let loadAndPlaySpy = spyOn( + component, + 'loadAndPlayAudioTranslation' + ).and.returnValue(); + spyOn(playerPositionService, 'getCurrentStateName').and.returnValue( + 'Start' + ); + spyOn(audioPreloaderService, 'restartAudioPreloader').and.returnValue(); + + component.onPlayButtonClicked(); + expect(loadAndPlaySpy).toHaveBeenCalled(); + } + ); + + it( + 'should pause uploaded audio translation when ' + + 'pause button is clicked', + () => { + let audioTranslation = { + en: Voiceover.createFromBackendDict({ + filename: 'audio-en.mp3', + file_size_bytes: 0.5, + needs_update: false, + duration_secs: 0.5, + }), + es: Voiceover.createFromBackendDict({ + filename: 'audio-es.mp3', + file_size_bytes: 0.5, + needs_update: false, + duration_secs: 0.5, + }), + }; + spyOn( + audioTranslationManagerService, + 'getCurrentAudioTranslations' + ).and.returnValue(audioTranslation); + // Setting selected language to be 'en'. + spyOn( + audioTranslationLanguageService, + 'getCurrentAudioLanguageCode' + ).and.returnValue('en'); + // Setting auto generated langugae to be false. + spyOn( + audioTranslationLanguageService, + 'isAutogeneratedLanguageCodeSelected' + ).and.returnValue(false); + // Setting audio is playing to be true. + spyOn(audioPlayerService, 'isPlaying').and.returnValue(true); + let pauseSpy = spyOn(audioPlayerService, 'pause').and.callThrough(); + + component.onPlayButtonClicked(); + expect(pauseSpy).toHaveBeenCalled(); + } + ); + + it( + 'should load audio track and play audio ' + 'which are stored in cache', + fakeAsync(() => { + let audioTranslation = { + en: Voiceover.createFromBackendDict({ + filename: 'audio-en.mp3', + file_size_bytes: 0.5, + needs_update: false, + duration_secs: 0.5, + }), + es: Voiceover.createFromBackendDict({ + filename: 'audio-es.mp3', + file_size_bytes: 0.5, + needs_update: false, + duration_secs: 0.5, + }), + }; + spyOn( + audioTranslationManagerService, + 'getCurrentAudioTranslations' + ).and.returnValue(audioTranslation); + // Setting selected language to be 'en'. + spyOn( + audioTranslationLanguageService, + 'getCurrentAudioLanguageCode' + ).and.returnValue('en'); + spyOn( + audioPreloaderService, + 'setMostRecentlyRequestedAudioFilename' + ).and.callThrough(); + // Setting cached value to be true. + spyOn(assetsBackendApiService, 'isCached').and.returnValue(true); + spyOn(audioPlayerService, 'loadAsync').and.returnValue( + Promise.resolve() + ); + let playCacheAudioSpy = spyOn( + component, + 'playCachedAudioTranslation' + ).and.callThrough(); + let playSpy = spyOn(audioPlayerService, 'play').and.callThrough(); + + component.loadAndPlayAudioTranslation(); + tick(); + discardPeriodicTasks(); + + expect(playCacheAudioSpy).toHaveBeenCalled(); + expect(playSpy).toHaveBeenCalled(); + }) + ); + + it( + 'should restart audio track if audio is not' + 'stored in cache', + fakeAsync(() => { + let audioTranslation = { + en: Voiceover.createFromBackendDict({ + filename: 'audio-en.mp3', + file_size_bytes: 0.5, + needs_update: false, + duration_secs: 0.5, + }), + es: Voiceover.createFromBackendDict({ + filename: 'audio-es.mp3', + file_size_bytes: 0.5, + needs_update: false, + duration_secs: 0.5, + }), + }; + spyOn( + audioTranslationManagerService, + 'getCurrentAudioTranslations' + ).and.returnValue(audioTranslation); + // Setting selected language to be 'en'. + spyOn( + audioTranslationLanguageService, + 'getCurrentAudioLanguageCode' + ).and.returnValue('en'); + spyOn( + audioPreloaderService, + 'setMostRecentlyRequestedAudioFilename' + ).and.callThrough(); + // Setting cached value to be true. + spyOn(assetsBackendApiService, 'isCached').and.returnValue(false); + spyOn(playerPositionService, 'getCurrentStateName').and.returnValue( + 'Start' + ); + let restartAudioSpy = spyOn( + audioPreloaderService, + 'restartAudioPreloader' + ).and.returnValue(); + + component.loadAndPlayAudioTranslation(); + tick(); + + expect(restartAudioSpy).toHaveBeenCalled(); + }) + ); }); it('should play audio from cache after finishing loading', () => { spyOn( - audioPreloaderService, 'getMostRecentlyRequestedAudioFilename') - .and.returnValue('audio-en.mp3'); + audioPreloaderService, + 'getMostRecentlyRequestedAudioFilename' + ).and.returnValue('audio-en.mp3'); component.audioLoadingIndicatorIsShown = true; let playCacheAudioSpy = spyOn(component, 'playCachedAudioTranslation'); @@ -599,11 +724,12 @@ describe('Audio Bar Component', () => { component.languagesInExploration = [ { value: 'en', - displayed: 'english' - }, { + displayed: 'english', + }, + { value: 'es', - displayed: 'spanish' - } + displayed: 'spanish', + }, ]; component.selectedLanguage.value = 'en'; let audioTranslation = { @@ -611,34 +737,44 @@ describe('Audio Bar Component', () => { filename: 'audio-en.mp3', file_size_bytes: 0.5, needs_update: false, - duration_secs: 0.5 + duration_secs: 0.5, }), es: Voiceover.createFromBackendDict({ filename: 'audio-es.mp3', file_size_bytes: 0.5, needs_update: false, - duration_secs: 0.5 - }) + duration_secs: 0.5, + }), }; - spyOn(playerPositionService, 'getCurrentStateName') - .and.returnValue('Start'); + spyOn(playerPositionService, 'getCurrentStateName').and.returnValue( + 'Start' + ); + spyOn( + audioTranslationLanguageService, + 'isAutogeneratedLanguageCodeSelected' + ).and.returnValue(false); spyOn( - audioTranslationLanguageService, 'isAutogeneratedLanguageCodeSelected') - .and.returnValue(false); - spyOn(audioTranslationLanguageService, 'setCurrentAudioLanguageCode') - .and.callThrough(); - spyOn(audioTranslationManagerService, 'getCurrentAudioTranslations') - .and.returnValue(audioTranslation); + audioTranslationLanguageService, + 'setCurrentAudioLanguageCode' + ).and.callThrough(); + spyOn( + audioTranslationManagerService, + 'getCurrentAudioTranslations' + ).and.returnValue(audioTranslation); // Setting selected language to be 'en'. - spyOn(audioTranslationLanguageService, 'getCurrentAudioLanguageCode') - .and.returnValue('en'); + spyOn( + audioTranslationLanguageService, + 'getCurrentAudioLanguageCode' + ).and.returnValue('en'); let languageSetSpy = spyOn( - audioPreloaderService, 'setMostRecentlyRequestedAudioFilename') - .and.callThrough(); + audioPreloaderService, + 'setMostRecentlyRequestedAudioFilename' + ).and.callThrough(); let restartAudioBarSpy = spyOn( - audioPreloaderService, 'restartAudioPreloader') - .and.returnValue(); + audioPreloaderService, + 'restartAudioPreloader' + ).and.returnValue(); component.onNewLanguageSelected(); expect(languageSetSpy).toHaveBeenCalledWith('audio-en.mp3'); diff --git a/core/templates/pages/exploration-player-page/layout-directives/audio-bar.component.ts b/core/templates/pages/exploration-player-page/layout-directives/audio-bar.component.ts index 92a48401fe6b..8d22dd9fc83b 100644 --- a/core/templates/pages/exploration-player-page/layout-directives/audio-bar.component.ts +++ b/core/templates/pages/exploration-player-page/layout-directives/audio-bar.component.ts @@ -17,25 +17,31 @@ * audio translation in the learner view. */ -import { Component } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { Voiceover } from 'domain/exploration/voiceover.model'; -import { Subscription } from 'rxjs'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; -import { AudioBarStatusService } from 'services/audio-bar-status.service'; -import { AudioPlayerService, AutoPlayAudioEvent } from 'services/audio-player.service'; -import { AutogeneratedAudioPlayerService } from 'services/autogenerated-audio-player.service'; -import { ContextService } from 'services/context.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { AudioPreloaderService } from '../services/audio-preloader.service'; -import { AudioTranslationLanguageService, ExplorationLanguageInfo } from '../services/audio-translation-language.service'; -import { AudioTranslationManagerService } from '../services/audio-translation-manager.service'; -import { PlayerPositionService } from '../services/player-position.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; +import {Component} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {Voiceover} from 'domain/exploration/voiceover.model'; +import {Subscription} from 'rxjs'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; +import {AudioBarStatusService} from 'services/audio-bar-status.service'; +import { + AudioPlayerService, + AutoPlayAudioEvent, +} from 'services/audio-player.service'; +import {AutogeneratedAudioPlayerService} from 'services/autogenerated-audio-player.service'; +import {ContextService} from 'services/context.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {AudioPreloaderService} from '../services/audio-preloader.service'; +import { + AudioTranslationLanguageService, + ExplorationLanguageInfo, +} from '../services/audio-translation-language.service'; +import {AudioTranslationManagerService} from '../services/audio-translation-manager.service'; +import {PlayerPositionService} from '../services/player-position.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; @Component({ selector: 'oppia-audio-bar', - templateUrl: './audio-bar.component.html' + templateUrl: './audio-bar.component.html', }) export class AudioBarComponent { lastScrollTop: number = 0; @@ -47,7 +53,7 @@ export class AudioBarComponent { audioLoadingIndicatorIsShown: boolean = false; explorationPlayerModeIsActive: boolean; // Value may be null if the language is not available. - selectedLanguage: { value: string | null }; + selectedLanguage: {value: string | null}; constructor( private assetsBackendApiService: AssetsBackendApiService, @@ -64,10 +70,10 @@ export class AudioBarComponent { ) { this.explorationPlayerModeIsActive = this.contextService.isInExplorationPlayerPage(); - this.languagesInExploration = this.audioTranslationLanguageService - .getLanguageOptionsForDropdown(); + this.languagesInExploration = + this.audioTranslationLanguageService.getLanguageOptionsForDropdown(); this.selectedLanguage = { - value: this.getCurrentAudioLanguageCode() + value: this.getCurrentAudioLanguageCode(), }; } @@ -84,12 +90,11 @@ export class AudioBarComponent { // address delays with autogenerated audio callbacks. setTimeout(() => { if (params) { - this.audioTranslationManagerService - .setSecondaryAudioTranslations( - params.audioTranslations, - params.html, - params.componentName - ); + this.audioTranslationManagerService.setSecondaryAudioTranslations( + params.audioTranslations, + params.html, + params.componentName + ); if (!this.isPaused) { this.onPlayButtonClicked(); } @@ -97,12 +102,14 @@ export class AudioBarComponent { }, 100); } } - )); + ) + ); this.audioBarIsExpanded = false; this.progressBarIsShown = false; this.audioLoadingIndicatorIsShown = false; this.audioPreloaderService.setAudioLoadedCallback( - this.onFinishedLoadingAudio.bind(this)); + this.onFinishedLoadingAudio.bind(this) + ); } ngOnDestroy(): void { @@ -126,7 +133,8 @@ export class AudioBarComponent { throw new Error('Expected a valid language code.'); } this.audioTranslationLanguageService.setCurrentAudioLanguageCode( - this.selectedLanguage.value); + this.selectedLanguage.value + ); this.audioPlayerService.stop(); this.audioPlayerService.clear(); this.autogeneratedAudioPlayerService.cancel(); @@ -137,10 +145,12 @@ export class AudioBarComponent { voiceoverInCurrentLanguage ) { let audioTranslation: Voiceover = voiceoverInCurrentLanguage; - this.audioPreloaderService - .setMostRecentlyRequestedAudioFilename(audioTranslation.filename); + this.audioPreloaderService.setMostRecentlyRequestedAudioFilename( + audioTranslation.filename + ); this.audioPreloaderService.restartAudioPreloader( - this.playerPositionService.getCurrentStateName()); + this.playerPositionService.getCurrentStateName() + ); } } @@ -159,14 +169,12 @@ export class AudioBarComponent { // Returns null if the audio is not available in the current language. getCurrentAudioLanguageCode(): string | null { - return this.audioTranslationLanguageService - .getCurrentAudioLanguageCode(); + return this.audioTranslationLanguageService.getCurrentAudioLanguageCode(); } // Returns null if the audio is not available in the current language. getCurrentAudioLanguageDescription(): string | null { - return this.audioTranslationLanguageService - .getCurrentAudioLanguageDescription(); + return this.audioTranslationLanguageService.getCurrentAudioLanguageDescription(); } // Returns null if the audio is not available in the current language. @@ -174,19 +182,24 @@ export class AudioBarComponent { const currentAudioLanguageCode = this.getCurrentAudioLanguageCode(); if (currentAudioLanguageCode !== null) { return this.audioTranslationManagerService.getCurrentAudioTranslations()[ - currentAudioLanguageCode]; + currentAudioLanguageCode + ]; } return null; } isAudioPlaying(): boolean { - return this.audioPlayerService.isPlaying() || - this.autogeneratedAudioPlayerService.isPlaying(); + return ( + this.audioPlayerService.isPlaying() || + this.autogeneratedAudioPlayerService.isPlaying() + ); } isAudioAvailableInCurrentLanguage(): boolean { - return Boolean(this.getVoiceoverInCurrentLanguage()) || - this.isAutogeneratedLanguageCodeSelected(); + return ( + Boolean(this.getVoiceoverInCurrentLanguage()) || + this.isAutogeneratedLanguageCodeSelected() + ); } doesCurrentAudioTranslationNeedUpdate(): boolean { @@ -196,15 +209,14 @@ export class AudioBarComponent { voiceoverInCurrentLanguage ) { let audioTranslation: Voiceover = voiceoverInCurrentLanguage; - return (audioTranslation && audioTranslation.needsUpdate); + return audioTranslation && audioTranslation.needsUpdate; } else { return false; } } isAutogeneratedLanguageCodeSelected(): boolean { - return this.audioTranslationLanguageService - .isAutogeneratedLanguageCodeSelected(); + return this.audioTranslationLanguageService.isAutogeneratedLanguageCodeSelected(); } onBackwardButtonClicked(): void { @@ -245,27 +257,28 @@ export class AudioBarComponent { if (this.autogeneratedAudioPlayerService.isPlaying()) { this.autogeneratedAudioPlayerService.cancel(); } else { - const speechSynthesisLanguageCode = ( - this.audioTranslationLanguageService.getSpeechSynthesisLanguageCode()); + const speechSynthesisLanguageCode = + this.audioTranslationLanguageService.getSpeechSynthesisLanguageCode(); if (speechSynthesisLanguageCode === null) { throw new Error( - 'speechSynthesisLanguageCode cannot be null at this point.'); + 'speechSynthesisLanguageCode cannot be null at this point.' + ); } this.autogeneratedAudioPlayerService.play( - this.audioTranslationManagerService - .getCurrentHtmlForAutogeneratedAudio(), + this.audioTranslationManagerService.getCurrentHtmlForAutogeneratedAudio(), speechSynthesisLanguageCode, () => { // Used to update bindings to show a silent speaker after // autogenerated audio has finished playing. - this.audioTranslationManagerService - .clearSecondaryAudioTranslations(); - let sequentialAudio = this.audioTranslationManagerService - .getCurrentHtmlForAutogeneratedSequentialAudio(); + this.audioTranslationManagerService.clearSecondaryAudioTranslations(); + let sequentialAudio = + this.audioTranslationManagerService.getCurrentHtmlForAutogeneratedSequentialAudio(); if (sequentialAudio) { this.autogeneratedAudioPlayerService.play( sequentialAudio, - speechSynthesisLanguageCode, () => {}); + speechSynthesisLanguageCode, + () => {} + ); } } ); @@ -285,11 +298,10 @@ export class AudioBarComponent { } playCachedAudioTranslation(audioFilename: string): void { - this.audioPlayerService.loadAsync(audioFilename) - .then(() => { - this.audioLoadingIndicatorIsShown = false; - this.audioPlayerService.play(); - }); + this.audioPlayerService.loadAsync(audioFilename).then(() => { + this.audioLoadingIndicatorIsShown = false; + this.audioPlayerService.play(); + }); } /** @@ -300,8 +312,10 @@ export class AudioBarComponent { onFinishedLoadingAudio(audioFilename: string): void { let mostRecentlyRequestedAudioFilename = this.audioPreloaderService.getMostRecentlyRequestedAudioFilename(); - if (this.audioLoadingIndicatorIsShown && - audioFilename === mostRecentlyRequestedAudioFilename) { + if ( + this.audioLoadingIndicatorIsShown && + audioFilename === mostRecentlyRequestedAudioFilename + ) { this.playCachedAudioTranslation(audioFilename); } } @@ -311,20 +325,26 @@ export class AudioBarComponent { let audioTranslation = this.getVoiceoverInCurrentLanguage(); if (audioTranslation) { this.audioPreloaderService.setMostRecentlyRequestedAudioFilename( - audioTranslation.filename); + audioTranslation.filename + ); if (this.isCached(audioTranslation)) { - this.playCachedAudioTranslation( - audioTranslation.filename); - } else if (!this.audioPreloaderService.isLoadingAudioFile( - audioTranslation.filename)) { + this.playCachedAudioTranslation(audioTranslation.filename); + } else if ( + !this.audioPreloaderService.isLoadingAudioFile( + audioTranslation.filename + ) + ) { this.audioPreloaderService.restartAudioPreloader( - this.playerPositionService.getCurrentStateName()); + this.playerPositionService.getCurrentStateName() + ); } } } } -angular.module('oppia').directive('oppiaAudioBar', +angular.module('oppia').directive( + 'oppiaAudioBar', downgradeComponent({ - component: AudioBarComponent - }) as angular.IDirectiveFactory); + component: AudioBarComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-player-page/layout-directives/content-language-selector.component.spec.ts b/core/templates/pages/exploration-player-page/layout-directives/content-language-selector.component.spec.ts index e571eb2afb81..7fa7cb75ce30 100644 --- a/core/templates/pages/exploration-player-page/layout-directives/content-language-selector.component.spec.ts +++ b/core/templates/pages/exploration-player-page/layout-directives/content-language-selector.component.spec.ts @@ -16,31 +16,29 @@ * @fileoverview Unit tests for the CkEditor copy toolbar component. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { BrowserDynamicTestingModule } from - '@angular/platform-browser-dynamic/testing'; -import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {BrowserDynamicTestingModule} from '@angular/platform-browser-dynamic/testing'; +import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; -import { ContentLanguageSelectorComponent } from +import { + ContentLanguageSelectorComponent, // eslint-disable-next-line max-len - 'pages/exploration-player-page/layout-directives/content-language-selector.component'; -import { ContentTranslationLanguageService } from - 'pages/exploration-player-page/services/content-translation-language.service'; -import { FormsModule } from '@angular/forms'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { PlayerTranscriptService } from - 'pages/exploration-player-page/services/player-transcript.service'; -import { StateCard } from 'domain/state_card/state-card.model'; -import { RecordedVoiceovers } from 'domain/exploration/recorded-voiceovers.model'; -import { SwitchContentLanguageRefreshRequiredModalComponent } from +} from 'pages/exploration-player-page/layout-directives/content-language-selector.component'; +import {ContentTranslationLanguageService} from 'pages/exploration-player-page/services/content-translation-language.service'; +import {FormsModule} from '@angular/forms'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {PlayerTranscriptService} from 'pages/exploration-player-page/services/player-transcript.service'; +import {StateCard} from 'domain/state_card/state-card.model'; +import {RecordedVoiceovers} from 'domain/exploration/recorded-voiceovers.model'; +import { + SwitchContentLanguageRefreshRequiredModalComponent, // eslint-disable-next-line max-len - 'pages/exploration-player-page/switch-content-language-refresh-required-modal.component'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { AudioTranslationLanguageService} from - 'pages/exploration-player-page/services/audio-translation-language.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { InteractionObjectFactory } from 'domain/exploration/InteractionObjectFactory'; -import { WindowRef } from 'services/contextual/window-ref.service'; +} from 'pages/exploration-player-page/switch-content-language-refresh-required-modal.component'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {AudioTranslationLanguageService} from 'pages/exploration-player-page/services/audio-translation-language.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {InteractionObjectFactory} from 'domain/exploration/InteractionObjectFactory'; +import {WindowRef} from 'services/contextual/window-ref.service'; class MockContentTranslationLanguageService { currentLanguageCode!: string; @@ -53,7 +51,7 @@ class MockContentTranslationLanguageService { return [ {value: 'fr', displayed: 'français (French)'}, {value: 'zh', displayed: '中文 (Chinese)'}, - {value: 'en', displayed: 'English'} + {value: 'en', displayed: 'English'}, ]; } @@ -72,8 +70,8 @@ class MockWindowRef { nativeWindow = { location: { href: 'http://localhost:8181/explore/wZiXFx1iV5bz', - pathname: '/explore/wZiXFx1iV5bz' - } + pathname: '/explore/wZiXFx1iV5bz', + }, }; } @@ -88,63 +86,68 @@ describe('Content language selector component', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [ - FormsModule, - HttpClientTestingModule, - NgbModule - ], + imports: [FormsModule, HttpClientTestingModule, NgbModule], declarations: [ ContentLanguageSelectorComponent, MockTranslatePipe, - SwitchContentLanguageRefreshRequiredModalComponent + SwitchContentLanguageRefreshRequiredModalComponent, ], - providers: [{ - provide: WindowRef, - useClass: MockWindowRef - }, { - provide: ContentTranslationLanguageService, - useClass: MockContentTranslationLanguageService - }, { - provide: I18nLanguageCodeService, - useClass: MockI18nLanguageCodeService - }] - }).overrideModule(BrowserDynamicTestingModule, { - set: { - entryComponents: [ - SwitchContentLanguageRefreshRequiredModalComponent - ], - } - }).compileComponents(); + providers: [ + { + provide: WindowRef, + useClass: MockWindowRef, + }, + { + provide: ContentTranslationLanguageService, + useClass: MockContentTranslationLanguageService, + }, + { + provide: I18nLanguageCodeService, + useClass: MockI18nLanguageCodeService, + }, + ], + }) + .overrideModule(BrowserDynamicTestingModule, { + set: { + entryComponents: [SwitchContentLanguageRefreshRequiredModalComponent], + }, + }) + .compileComponents(); contentTranslationLanguageService = TestBed.get( - ContentTranslationLanguageService); + ContentTranslationLanguageService + ); interactionObjectFactory = TestBed.inject(InteractionObjectFactory); playerTranscriptService = TestBed.get(PlayerTranscriptService); audioTranslationLanguageService = TestBed.get( - AudioTranslationLanguageService); + AudioTranslationLanguageService + ); fixture = TestBed.createComponent(ContentLanguageSelectorComponent); windowRef = TestBed.inject(WindowRef); component = fixture.componentInstance; fixture.detectChanges(); })); - it('should correctly initialize selectedLanguageCode, ' + - 'and languagesInExploration', () => { - expect(component.selectedLanguageCode).toBe('fr'); - expect(component.languageOptions).toEqual([ - { value: 'fr', displayed: 'français (French)' }, - { value: 'zh', displayed: '中文 (Chinese)' }, - { value: 'en', displayed: 'English' } - ]); - }); + it( + 'should correctly initialize selectedLanguageCode, ' + + 'and languagesInExploration', + () => { + expect(component.selectedLanguageCode).toBe('fr'); + expect(component.languageOptions).toEqual([ + {value: 'fr', displayed: 'français (French)'}, + {value: 'zh', displayed: '中文 (Chinese)'}, + {value: 'en', displayed: 'English'}, + ]); + } + ); it('should correcly initialize newLanguageCode', () => { component.ngOnInit(); expect(component.newLanguageCode).toBe('fr'); - windowRef.nativeWindow.location.href = ( - 'http://localhost:8181/explore/wZiXFx1iV5bz?initialContentLanguageCode=en'); - windowRef.nativeWindow.location.pathname = ( - '/explore/wZiXFx1iV5bz?initialContentLanguageCode=en'); + windowRef.nativeWindow.location.href = + 'http://localhost:8181/explore/wZiXFx1iV5bz?initialContentLanguageCode=en'; + windowRef.nativeWindow.location.pathname = + '/explore/wZiXFx1iV5bz?initialContentLanguageCode=en'; component.ngOnInit(); @@ -154,10 +157,13 @@ describe('Content language selector component', () => { it('should correctly select an option when refresh is not needed', () => { const setCurrentContentLanguageCodeSpy = spyOn( contentTranslationLanguageService, - 'setCurrentContentLanguageCode'); + 'setCurrentContentLanguageCode' + ); const card = StateCard.createNewCard( - 'State 1', '

Content

', '', + 'State 1', + '

Content

', + '', interactionObjectFactory.createFromBackendDict({ id: 'GraphInput', answer_groups: [ @@ -208,10 +214,12 @@ describe('Content language selector component', () => { content_id: '2', html: 'test_explanation1', }, - } + }, }), RecordedVoiceovers.createEmpty(), - 'content', audioTranslationLanguageService); + 'content', + audioTranslationLanguageService + ); spyOn(playerTranscriptService, 'getCard').and.returnValue(card); component.selectedLanguageCode = 'en'; @@ -221,76 +229,84 @@ describe('Content language selector component', () => { expect(component.selectedLanguageCode).toBe('fr'); }); - it('should correctly open the refresh required modal when refresh is ' + - 'needed', () => { - const setCurrentContentLanguageCodeSpy = spyOn( - contentTranslationLanguageService, - 'setCurrentContentLanguageCode'); + it( + 'should correctly open the refresh required modal when refresh is ' + + 'needed', + () => { + const setCurrentContentLanguageCodeSpy = spyOn( + contentTranslationLanguageService, + 'setCurrentContentLanguageCode' + ); - const card = StateCard.createNewCard( - 'State 1', '

Content

', '', - interactionObjectFactory.createFromBackendDict({ - id: 'GraphInput', - answer_groups: [ - { - outcome: { - dest: 'State', - dest_if_really_stuck: null, - feedback: { - html: '', - content_id: 'This is a new feedback text', + const card = StateCard.createNewCard( + 'State 1', + '

Content

', + '', + interactionObjectFactory.createFromBackendDict({ + id: 'GraphInput', + answer_groups: [ + { + outcome: { + dest: 'State', + dest_if_really_stuck: null, + feedback: { + html: '', + content_id: 'This is a new feedback text', + }, + refresher_exploration_id: 'test', + missing_prerequisite_skill_id: 'test_skill_id', + labelled_as_correct: true, + param_changes: [], }, - refresher_exploration_id: 'test', - missing_prerequisite_skill_id: 'test_skill_id', - labelled_as_correct: true, - param_changes: [], + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: '', }, - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: '', - }, - ], - default_outcome: { - dest: 'Hola', - dest_if_really_stuck: null, - feedback: { - content_id: '', - html: '', - }, - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: 'test', - missing_prerequisite_skill_id: 'test_skill_id', - }, - confirmed_unclassified_answers: [], - customization_args: { - rows: { - value: true, + ], + default_outcome: { + dest: 'Hola', + dest_if_really_stuck: null, + feedback: { + content_id: '', + html: '', + }, + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: 'test', + missing_prerequisite_skill_id: 'test_skill_id', }, - placeholder: { - value: 1, + confirmed_unclassified_answers: [], + customization_args: { + rows: { + value: true, + }, + placeholder: { + value: 1, + }, }, - }, - hints: [], - solution: { - answer_is_exclusive: true, - correct_answer: 'test_answer', - explanation: { - content_id: '2', - html: 'test_explanation1', + hints: [], + solution: { + answer_is_exclusive: true, + correct_answer: 'test_answer', + explanation: { + content_id: '2', + html: 'test_explanation1', + }, }, - } - }), - RecordedVoiceovers.createEmpty(), - 'content', audioTranslationLanguageService); - card.addInputResponsePair({ - learnerInput: '', - oppiaResponse: '', - isHint: false - }); - spyOn(playerTranscriptService, 'getCard').and.returnValue(card); + }), + RecordedVoiceovers.createEmpty(), + 'content', + audioTranslationLanguageService + ); + card.addInputResponsePair({ + learnerInput: '', + oppiaResponse: '', + isHint: false, + }); + spyOn(playerTranscriptService, 'getCard').and.returnValue(card); - component.onSelectLanguage('fr'); - expect(setCurrentContentLanguageCodeSpy).not.toHaveBeenCalled(); - }); + component.onSelectLanguage('fr'); + expect(setCurrentContentLanguageCodeSpy).not.toHaveBeenCalled(); + } + ); }); diff --git a/core/templates/pages/exploration-player-page/layout-directives/content-language-selector.component.ts b/core/templates/pages/exploration-player-page/layout-directives/content-language-selector.component.ts index 799777021724..091db6f5ed19 100644 --- a/core/templates/pages/exploration-player-page/layout-directives/content-language-selector.component.ts +++ b/core/templates/pages/exploration-player-page/layout-directives/content-language-selector.component.ts @@ -17,42 +17,38 @@ * playing an exploration. */ -import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import {ChangeDetectorRef, Component, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; -import { ContentTranslationLanguageService } from - 'pages/exploration-player-page/services/content-translation-language.service'; -import { ExplorationLanguageInfo } from - 'pages/exploration-player-page/services/audio-translation-language.service'; -import { PlayerPositionService } from - 'pages/exploration-player-page/services/player-position.service'; -import { PlayerTranscriptService } from - 'pages/exploration-player-page/services/player-transcript.service'; -import { SwitchContentLanguageRefreshRequiredModalComponent } from +import {ContentTranslationLanguageService} from 'pages/exploration-player-page/services/content-translation-language.service'; +import {ExplorationLanguageInfo} from 'pages/exploration-player-page/services/audio-translation-language.service'; +import {PlayerPositionService} from 'pages/exploration-player-page/services/player-position.service'; +import {PlayerTranscriptService} from 'pages/exploration-player-page/services/player-transcript.service'; +import { + SwitchContentLanguageRefreshRequiredModalComponent, // eslint-disable-next-line max-len - 'pages/exploration-player-page/switch-content-language-refresh-required-modal.component'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { ContentTranslationManagerService } from '../services/content-translation-manager.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; +} from 'pages/exploration-player-page/switch-content-language-refresh-required-modal.component'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {ContentTranslationManagerService} from '../services/content-translation-manager.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; @Component({ selector: 'oppia-content-language-selector', templateUrl: './content-language-selector.component.html', - styleUrls: [] + styleUrls: [], }) export class ContentLanguageSelectorComponent implements OnInit { constructor( private changeDetectorRef: ChangeDetectorRef, - private contentTranslationLanguageService: - ContentTranslationLanguageService, + private contentTranslationLanguageService: ContentTranslationLanguageService, private contentTranslationManagerService: ContentTranslationManagerService, private playerPositionService: PlayerPositionService, private playerTranscriptService: PlayerTranscriptService, private ngbModal: NgbModal, private i18nLanguageCodeService: I18nLanguageCodeService, private windowRef: WindowRef - ) { } + ) {} // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see @@ -64,22 +60,22 @@ export class ContentLanguageSelectorComponent implements OnInit { ngOnInit(): void { const url = new URL(this.windowRef.nativeWindow.location.href); - this.currentGlobalLanguageCode = ( - this.i18nLanguageCodeService.getCurrentI18nLanguageCode()); - this.selectedLanguageCode = ( - this.contentTranslationLanguageService.getCurrentContentLanguageCode()); - this.languageOptions = ( - this.contentTranslationLanguageService.getLanguageOptionsForDropdown()); - this.newLanguageCode = ( + this.currentGlobalLanguageCode = + this.i18nLanguageCodeService.getCurrentI18nLanguageCode(); + this.selectedLanguageCode = + this.contentTranslationLanguageService.getCurrentContentLanguageCode(); + this.languageOptions = + this.contentTranslationLanguageService.getLanguageOptionsForDropdown(); + this.newLanguageCode = url.searchParams.get('initialContentLanguageCode') || - this.currentGlobalLanguageCode); + this.currentGlobalLanguageCode; for (let option of this.languageOptions) { if (option.value === this.newLanguageCode) { this.contentTranslationLanguageService.setCurrentContentLanguageCode( - option.value); - this.selectedLanguageCode = ( - this.contentTranslationLanguageService.getCurrentContentLanguageCode() + option.value ); + this.selectedLanguageCode = + this.contentTranslationLanguageService.getCurrentContentLanguageCode(); break; } } @@ -88,13 +84,16 @@ export class ContentLanguageSelectorComponent implements OnInit { onSelectLanguage(newLanguageCode: string): void { if (this.shouldPromptForRefresh()) { const modalRef = this.ngbModal.open( - SwitchContentLanguageRefreshRequiredModalComponent); + SwitchContentLanguageRefreshRequiredModalComponent + ); modalRef.componentInstance.languageCode = newLanguageCode; } else if (this.selectedLanguageCode !== newLanguageCode) { this.contentTranslationLanguageService.setCurrentContentLanguageCode( - newLanguageCode); + newLanguageCode + ); this.contentTranslationManagerService.displayTranslations( - newLanguageCode); + newLanguageCode + ); this.selectedLanguageCode = newLanguageCode; this.changeDetectorRef.detectChanges(); } @@ -103,7 +102,8 @@ export class ContentLanguageSelectorComponent implements OnInit { shouldDisplaySelector(): boolean { return ( this.languageOptions.length > 1 && - this.playerPositionService.displayedCardIndex === 0); + this.playerPositionService.displayedCardIndex === 0 + ); } private shouldPromptForRefresh(): boolean { @@ -112,6 +112,9 @@ export class ContentLanguageSelectorComponent implements OnInit { } } -angular.module('oppia').directive( - 'oppiaContentLanguageSelector', - downgradeComponent({ component: ContentLanguageSelectorComponent })); +angular + .module('oppia') + .directive( + 'oppiaContentLanguageSelector', + downgradeComponent({component: ContentLanguageSelectorComponent}) + ); diff --git a/core/templates/pages/exploration-player-page/layout-directives/correctness-footer.component.ts b/core/templates/pages/exploration-player-page/layout-directives/correctness-footer.component.ts index 19ea257acd36..b664deb7d90a 100644 --- a/core/templates/pages/exploration-player-page/layout-directives/correctness-footer.component.ts +++ b/core/templates/pages/exploration-player-page/layout-directives/correctness-footer.component.ts @@ -16,14 +16,18 @@ * @fileoverview Component for the correctness footer in the exploration player. */ -import { Component } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; @Component({ selector: 'oppia-correctness-footer', - templateUrl: './correctness-footer.component.html' + templateUrl: './correctness-footer.component.html', }) export class CorrectnessFooterComponent {} -angular.module('oppia').directive('oppiaCorrectnessFooter', - downgradeComponent({ component: CorrectnessFooterComponent })); +angular + .module('oppia') + .directive( + 'oppiaCorrectnessFooter', + downgradeComponent({component: CorrectnessFooterComponent}) + ); diff --git a/core/templates/pages/exploration-player-page/layout-directives/exploration-footer.component.spec.ts b/core/templates/pages/exploration-player-page/layout-directives/exploration-footer.component.spec.ts index e7e0bea25a32..652d67120c45 100644 --- a/core/templates/pages/exploration-player-page/layout-directives/exploration-footer.component.spec.ts +++ b/core/templates/pages/exploration-player-page/layout-directives/exploration-footer.component.spec.ts @@ -17,40 +17,55 @@ * in exploration player. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { ExplorationFooterComponent } from './exploration-footer.component'; -import { NgbModal, NgbModalRef, NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { TranslateService } from '@ngx-translate/core'; -import { MockTranslateService } from 'components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { LimitToPipe } from 'filters/limit-to.pipe'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ContextService } from 'services/context.service'; -import { UrlService } from 'services/contextual/url.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { ExplorationSummaryBackendApiService, ExplorationSummaryDict } from 'domain/summary/exploration-summary-backend-api.service'; -import { EventEmitter } from '@angular/core'; -import { QuestionPlayerStateService } from 'components/question-directives/question-player/services/question-player-state.service'; -import { LearnerExplorationSummaryBackendDict } from 'domain/summary/learner-exploration-summary.model'; -import { LearnerViewInfoBackendApiService } from '../services/learner-view-info-backend-api.service'; -import { LoggerService } from 'services/contextual/logger.service'; -import { FetchExplorationBackendResponse, ReadOnlyExplorationBackendApiService } from 'domain/exploration/read-only-exploration-backend-api.service'; -import { ExplorationEngineService } from '../services/exploration-engine.service'; -import { StateObjectFactory } from 'domain/state/StateObjectFactory'; -import { EditableExplorationBackendApiService } from 'domain/exploration/editable-exploration-backend-api.service'; -import { PlayerPositionService } from '../services/player-position.service'; -import { PlayerTranscriptService } from '../services/player-transcript.service'; -import { StateCard } from 'domain/state_card/state-card.model'; -import { RecordedVoiceovers } from 'domain/exploration/recorded-voiceovers.model'; -import { AudioTranslationLanguageService } from '../services/audio-translation-language.service'; -import { UserInfo } from 'domain/user/user-info.model'; -import { UserService } from 'services/user.service'; -import { Interaction, InteractionObjectFactory } from 'domain/exploration/InteractionObjectFactory'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { CheckpointCelebrationUtilityService } from 'pages/exploration-player-page/services/checkpoint-celebration-utility.service'; -import { ConceptCardManagerService } from '../services/concept-card-manager.service'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + async, + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {ExplorationFooterComponent} from './exploration-footer.component'; +import {NgbModal, NgbModalRef, NgbModule} from '@ng-bootstrap/ng-bootstrap'; +import {TranslateService} from '@ngx-translate/core'; +import {MockTranslateService} from 'components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {LimitToPipe} from 'filters/limit-to.pipe'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ContextService} from 'services/context.service'; +import {UrlService} from 'services/contextual/url.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import { + ExplorationSummaryBackendApiService, + ExplorationSummaryDict, +} from 'domain/summary/exploration-summary-backend-api.service'; +import {EventEmitter} from '@angular/core'; +import {QuestionPlayerStateService} from 'components/question-directives/question-player/services/question-player-state.service'; +import {LearnerExplorationSummaryBackendDict} from 'domain/summary/learner-exploration-summary.model'; +import {LearnerViewInfoBackendApiService} from '../services/learner-view-info-backend-api.service'; +import {LoggerService} from 'services/contextual/logger.service'; +import { + FetchExplorationBackendResponse, + ReadOnlyExplorationBackendApiService, +} from 'domain/exploration/read-only-exploration-backend-api.service'; +import {ExplorationEngineService} from '../services/exploration-engine.service'; +import {StateObjectFactory} from 'domain/state/StateObjectFactory'; +import {EditableExplorationBackendApiService} from 'domain/exploration/editable-exploration-backend-api.service'; +import {PlayerPositionService} from '../services/player-position.service'; +import {PlayerTranscriptService} from '../services/player-transcript.service'; +import {StateCard} from 'domain/state_card/state-card.model'; +import {RecordedVoiceovers} from 'domain/exploration/recorded-voiceovers.model'; +import {AudioTranslationLanguageService} from '../services/audio-translation-language.service'; +import {UserInfo} from 'domain/user/user-info.model'; +import {UserService} from 'services/user.service'; +import { + Interaction, + InteractionObjectFactory, +} from 'domain/exploration/InteractionObjectFactory'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {CheckpointCelebrationUtilityService} from 'pages/exploration-player-page/services/checkpoint-celebration-utility.service'; +import {ConceptCardManagerService} from '../services/concept-card-manager.service'; class MockCheckpointCelebrationUtilityService { private _openLessonInformationModalEmitter = new EventEmitter(); @@ -72,30 +87,30 @@ class MockWindowRef { reload: () => {}, toString: () => { return 'http://localhost:8181/?lang=es'; - } + }, }, localStorage: { last_uploaded_audio_lang: 'en', - removeItem: (name: string) => {} + removeItem: (name: string) => {}, }, gtag: () => {}, history: { - pushState(data: object, title: string, url?: string | null) {} + pushState(data: object, title: string, url?: string | null) {}, }, document: { body: { style: { overflowY: 'auto', - } - } - } + }, + }, + }, }; } class MockNgbModalRef { componentInstance = { skillId: null, - explorationId: null + explorationId: null, }; } @@ -106,23 +121,20 @@ describe('ExplorationFooterComponent', () => { let urlService: UrlService; let learnerViewInfoBackendApiService: LearnerViewInfoBackendApiService; let loggerService: LoggerService; - let readOnlyExplorationBackendApiService: - ReadOnlyExplorationBackendApiService; + let readOnlyExplorationBackendApiService: ReadOnlyExplorationBackendApiService; let windowDimensionsService: WindowDimensionsService; let questionPlayerStateService: QuestionPlayerStateService; let mockResizeEventEmitter = new EventEmitter(); let explorationSummaryBackendApiService: ExplorationSummaryBackendApiService; let stateObjectFactory: StateObjectFactory; let explorationEngineService: ExplorationEngineService; - let editableExplorationBackendApiService: - EditableExplorationBackendApiService; + let editableExplorationBackendApiService: EditableExplorationBackendApiService; let playerPositionService: PlayerPositionService; let playerTranscriptService: PlayerTranscriptService; let audioTranslationLanguageService: AudioTranslationLanguageService; let userService: UserService; let urlInterpolationService: UrlInterpolationService; - let checkpointCelebrationUtilityService: - CheckpointCelebrationUtilityService; + let checkpointCelebrationUtilityService: CheckpointCelebrationUtilityService; let ngbModal: NgbModal; let conceptCardManagerService: ConceptCardManagerService; let interactionObjectFactory: InteractionObjectFactory; @@ -137,7 +149,7 @@ describe('ExplorationFooterComponent', () => { 2: 0, 3: 0, 4: 0, - 5: 0 + 5: 0, }, id: 'dummy_id', created_on_msec: 2000, @@ -149,7 +161,7 @@ describe('ExplorationFooterComponent', () => { tags: ['tag1', 'tag2'], thumbnail_bg_color: 'bg_color_test', thumbnail_icon_url: 'icon_url', - title: 'expTitle' + title: 'expTitle', }; let mockResultsLoadedEventEmitter = new EventEmitter(); @@ -160,7 +172,7 @@ describe('ExplorationFooterComponent', () => { declarations: [ ExplorationFooterComponent, MockTranslatePipe, - LimitToPipe + LimitToPipe, ], providers: [ QuestionPlayerStateService, @@ -169,18 +181,18 @@ describe('ExplorationFooterComponent', () => { UrlInterpolationService, { provide: CheckpointCelebrationUtilityService, - useClass: MockCheckpointCelebrationUtilityService + useClass: MockCheckpointCelebrationUtilityService, }, { provide: WindowRef, - useClass: MockWindowRef + useClass: MockWindowRef, }, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -189,28 +201,32 @@ describe('ExplorationFooterComponent', () => { urlService = TestBed.inject(UrlService); windowDimensionsService = TestBed.inject(WindowDimensionsService); learnerViewInfoBackendApiService = TestBed.inject( - LearnerViewInfoBackendApiService); + LearnerViewInfoBackendApiService + ); loggerService = TestBed.inject(LoggerService); readOnlyExplorationBackendApiService = TestBed.inject( - ReadOnlyExplorationBackendApiService); + ReadOnlyExplorationBackendApiService + ); explorationSummaryBackendApiService = TestBed.inject( - ExplorationSummaryBackendApiService); + ExplorationSummaryBackendApiService + ); editableExplorationBackendApiService = TestBed.inject( - EditableExplorationBackendApiService); - questionPlayerStateService = TestBed.inject( - QuestionPlayerStateService); + EditableExplorationBackendApiService + ); + questionPlayerStateService = TestBed.inject(QuestionPlayerStateService); explorationEngineService = TestBed.inject(ExplorationEngineService); stateObjectFactory = TestBed.inject(StateObjectFactory); playerPositionService = TestBed.inject(PlayerPositionService); playerTranscriptService = TestBed.inject(PlayerTranscriptService); audioTranslationLanguageService = TestBed.inject( - AudioTranslationLanguageService); + AudioTranslationLanguageService + ); userService = TestBed.inject(UserService); urlInterpolationService = TestBed.inject(UrlInterpolationService); checkpointCelebrationUtilityService = TestBed.inject( - CheckpointCelebrationUtilityService); - conceptCardManagerService = TestBed.inject( - ConceptCardManagerService); + CheckpointCelebrationUtilityService + ); + conceptCardManagerService = TestBed.inject(ConceptCardManagerService); fixture = TestBed.createComponent(ExplorationFooterComponent); ngbModal = TestBed.inject(NgbModal); interactionObjectFactory = TestBed.inject(InteractionObjectFactory); @@ -218,159 +234,178 @@ describe('ExplorationFooterComponent', () => { fixture.detectChanges(); spyOn(playerPositionService, 'onNewCardOpened').and.returnValue( - new EventEmitter()); + new EventEmitter() + ); }); afterEach(() => { component.ngOnDestroy(); }); - it('should initialise component when user opens exploration ' + - 'player', fakeAsync(() => { - spyOn(contextService, 'getExplorationId').and.returnValue('exp1'); - spyOn(playerPositionService.onNewCardOpened, 'subscribe'); - spyOn(urlService, 'isIframed').and.returnValue(true); - spyOn(windowDimensionsService, 'isWindowNarrow').and.returnValue(false); - spyOn(windowDimensionsService, 'getResizeEvent').and.returnValue( - mockResizeEventEmitter); - spyOn(playerPositionService.onLoadedMostRecentCheckpoint, 'subscribe'); - spyOn(conceptCardManagerService, 'reset'); - spyOn( - checkpointCelebrationUtilityService - .getOpenLessonInformationModalEmitter(), 'subscribe'); - spyOn(component, 'getCheckpointCount').and.returnValue(Promise.resolve()); - spyOn(component, 'showProgressReminderModal'); - spyOn(contextService, 'isInQuestionPlayerMode').and.returnValue(false); - spyOn(contextService, 'getQuestionPlayerIsManuallySet').and - .returnValue(true); - spyOn( - explorationSummaryBackendApiService, - 'loadPublicAndPrivateExplorationSummariesAsync').and.resolveTo({ - summaries: [ - { - category: 'Coding', - community_owned: true, - thumbnail_bg_color: '#a33f40', - title: 'Project Euler Problem 1', - num_views: 263, - tags: [], - human_readable_contributors_summary: { - contributor_1: { - num_commits: 1 - }, - contributor_2: { - num_commits: 3 - }, - contributor_3: { - num_commits: 2 - } - }, - status: 'public', - language_code: 'en', - objective: 'Solve problem 1 on the Project Euler site', - thumbnail_icon_url: '/subjects/Lightbulb.svg', - id: 'exp1', - } as ExplorationSummaryDict - ] - }); - spyOn(userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(new UserInfo( - [], false, false, - false, false, false, '', '', '', true))); - - // A StateCard which supports hints. - let newCard = StateCard.createNewCard( - 'State 2', '

Content

', '', - interactionObjectFactory.createFromBackendDict({ - id: 'TextInput', - answer_groups: [ + it( + 'should initialise component when user opens exploration ' + 'player', + fakeAsync(() => { + spyOn(contextService, 'getExplorationId').and.returnValue('exp1'); + spyOn(playerPositionService.onNewCardOpened, 'subscribe'); + spyOn(urlService, 'isIframed').and.returnValue(true); + spyOn(windowDimensionsService, 'isWindowNarrow').and.returnValue(false); + spyOn(windowDimensionsService, 'getResizeEvent').and.returnValue( + mockResizeEventEmitter + ); + spyOn(playerPositionService.onLoadedMostRecentCheckpoint, 'subscribe'); + spyOn(conceptCardManagerService, 'reset'); + spyOn( + checkpointCelebrationUtilityService.getOpenLessonInformationModalEmitter(), + 'subscribe' + ); + spyOn(component, 'getCheckpointCount').and.returnValue(Promise.resolve()); + spyOn(component, 'showProgressReminderModal'); + spyOn(contextService, 'isInQuestionPlayerMode').and.returnValue(false); + spyOn(contextService, 'getQuestionPlayerIsManuallySet').and.returnValue( + true + ); + spyOn( + explorationSummaryBackendApiService, + 'loadPublicAndPrivateExplorationSummariesAsync' + ).and.resolveTo({ + summaries: [ { - outcome: { - dest: 'State', - dest_if_really_stuck: null, - feedback: { - html: '', - content_id: 'This is a new feedback text', + category: 'Coding', + community_owned: true, + thumbnail_bg_color: '#a33f40', + title: 'Project Euler Problem 1', + num_views: 263, + tags: [], + human_readable_contributors_summary: { + contributor_1: { + num_commits: 1, + }, + contributor_2: { + num_commits: 3, + }, + contributor_3: { + num_commits: 2, }, - refresher_exploration_id: 'test', - missing_prerequisite_skill_id: 'test_skill_id', - labelled_as_correct: true, - param_changes: [], }, - rule_specs: [], - training_data: [], - tagged_skill_misconception_id: '', - }, + status: 'public', + language_code: 'en', + objective: 'Solve problem 1 on the Project Euler site', + thumbnail_icon_url: '/subjects/Lightbulb.svg', + id: 'exp1', + } as ExplorationSummaryDict, ], - default_outcome: { - dest: 'Hola', - dest_if_really_stuck: null, - feedback: { - content_id: '', - html: '', + }); + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve( + new UserInfo([], false, false, false, false, false, '', '', '', true) + ) + ); + + // A StateCard which supports hints. + let newCard = StateCard.createNewCard( + 'State 2', + '

Content

', + '', + interactionObjectFactory.createFromBackendDict({ + id: 'TextInput', + answer_groups: [ + { + outcome: { + dest: 'State', + dest_if_really_stuck: null, + feedback: { + html: '', + content_id: 'This is a new feedback text', + }, + refresher_exploration_id: 'test', + missing_prerequisite_skill_id: 'test_skill_id', + labelled_as_correct: true, + param_changes: [], + }, + rule_specs: [], + training_data: [], + tagged_skill_misconception_id: '', + }, + ], + default_outcome: { + dest: 'Hola', + dest_if_really_stuck: null, + feedback: { + content_id: '', + html: '', + }, + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: 'test', + missing_prerequisite_skill_id: 'test_skill_id', }, - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: 'test', - missing_prerequisite_skill_id: 'test_skill_id', - }, - confirmed_unclassified_answers: [], - customization_args: { - rows: { - value: true, + confirmed_unclassified_answers: [], + customization_args: { + rows: { + value: true, + }, + placeholder: { + value: 1, + }, }, - placeholder: { - value: 1, - } - }, - hints: [], - solution: { - answer_is_exclusive: true, - correct_answer: 'test_answer', - explanation: { - content_id: '2', - html: 'test_explanation1', + hints: [], + solution: { + answer_is_exclusive: true, + correct_answer: 'test_answer', + explanation: { + content_id: '2', + html: 'test_explanation1', + }, }, - } - }), - RecordedVoiceovers.createEmpty(), - 'content', audioTranslationLanguageService); + }), + RecordedVoiceovers.createEmpty(), + 'content', + audioTranslationLanguageService + ); - component.ngOnInit(); - playerPositionService.onNewCardOpened.emit(newCard); - tick(); + component.ngOnInit(); + playerPositionService.onNewCardOpened.emit(newCard); + tick(); - expect(component.explorationId).toBe('exp1'); - expect(component.iframed).toBeTrue(); - expect(component.windowIsNarrow).toBeFalse(); - expect( - explorationSummaryBackendApiService. - loadPublicAndPrivateExplorationSummariesAsync) - .toHaveBeenCalledWith(['exp1']); - expect(component.contributorNames).toEqual([ - 'contributor_2', 'contributor_3', 'contributor_1']); - expect(playerPositionService.onLoadedMostRecentCheckpoint.subscribe) - .toHaveBeenCalled(); - expect(playerPositionService.onNewCardOpened.subscribe).toHaveBeenCalled(); - expect(conceptCardManagerService.reset).toHaveBeenCalled(); + expect(component.explorationId).toBe('exp1'); + expect(component.iframed).toBeTrue(); + expect(component.windowIsNarrow).toBeFalse(); + expect( + explorationSummaryBackendApiService.loadPublicAndPrivateExplorationSummariesAsync + ).toHaveBeenCalledWith(['exp1']); + expect(component.contributorNames).toEqual([ + 'contributor_2', + 'contributor_3', + 'contributor_1', + ]); + expect( + playerPositionService.onLoadedMostRecentCheckpoint.subscribe + ).toHaveBeenCalled(); + expect( + playerPositionService.onNewCardOpened.subscribe + ).toHaveBeenCalled(); + expect(conceptCardManagerService.reset).toHaveBeenCalled(); - component.checkpointCount = 5; + component.checkpointCount = 5; - playerPositionService.onLoadedMostRecentCheckpoint.emit(); + playerPositionService.onLoadedMostRecentCheckpoint.emit(); - expect(component.getCheckpointCount).toHaveBeenCalledTimes(1); - expect(component.showProgressReminderModal).toHaveBeenCalled(); + expect(component.getCheckpointCount).toHaveBeenCalledTimes(1); + expect(component.showProgressReminderModal).toHaveBeenCalled(); - component.checkpointCount = 0; + component.checkpointCount = 0; - playerPositionService.onLoadedMostRecentCheckpoint.emit(); + playerPositionService.onLoadedMostRecentCheckpoint.emit(); - expect(component.getCheckpointCount).toHaveBeenCalledTimes(2); - })); + expect(component.getCheckpointCount).toHaveBeenCalledTimes(2); + }) + ); it('should check if progress reminder modal can be shown and show it', () => { const recentlyReachedCheckpointSpy = spyOn( - component, 'getMostRecentlyReachedCheckpointIndex').and.returnValue(1); + component, + 'getMostRecentlyReachedCheckpointIndex' + ).and.returnValue(1); spyOn(component, 'openProgressReminderModal'); component.showProgressReminderModal(); @@ -386,11 +421,15 @@ describe('ExplorationFooterComponent', () => { }); it('should fetch exploration info first if not present', fakeAsync(() => { - spyOn(component, 'getMostRecentlyReachedCheckpointIndex') - .and.returnValue(3); + spyOn(component, 'getMostRecentlyReachedCheckpointIndex').and.returnValue( + 3 + ); spyOn(component, 'openProgressReminderModal'); - spyOn(learnerViewInfoBackendApiService, 'fetchLearnerInfoAsync') - .and.returnValue(Promise.resolve({ + spyOn( + learnerViewInfoBackendApiService, + 'fetchLearnerInfoAsync' + ).and.returnValue( + Promise.resolve({ summaries: [ { category: 'dummy_category', @@ -402,7 +441,7 @@ describe('ExplorationFooterComponent', () => { 2: 0, 3: 0, 4: 0, - 5: 0 + 5: 0, }, id: 'dummy_id', created_on_msec: 2000, @@ -414,15 +453,17 @@ describe('ExplorationFooterComponent', () => { tags: ['tag1', 'tag2'], thumbnail_bg_color: 'bg_color_test', thumbnail_icon_url: 'icon_url', - title: 'expTitle' - } - ] - })); + title: 'expTitle', + }, + ], + }) + ); component.showProgressReminderModal(); - expect(learnerViewInfoBackendApiService.fetchLearnerInfoAsync) - .toHaveBeenCalled(); + expect( + learnerViewInfoBackendApiService.fetchLearnerInfoAsync + ).toHaveBeenCalled(); })); it('should open progress reminder modal', fakeAsync(() => { @@ -432,26 +473,32 @@ describe('ExplorationFooterComponent', () => { componentInstance: { checkpointCount: 0, completedCheckpointsCount: 0, - explorationTitle: '' + explorationTitle: '', }, - result: Promise.resolve() + result: Promise.resolve(), } as NgbModalRef); spyOn( - editableExplorationBackendApiService, 'resetExplorationProgressAsync') - .and.returnValue(Promise.resolve()); + editableExplorationBackendApiService, + 'resetExplorationProgressAsync' + ).and.returnValue(Promise.resolve()); const stateCard = new StateCard( - 'End', '

Testing

', null, new Interaction( - [], [], null, null, [], 'EndExploration', null), - [], null, 'content', null + 'End', + '

Testing

', + null, + new Interaction([], [], null, null, [], 'EndExploration', null), + [], + null, + 'content', + null ); const endState = { classifier_model_id: null, recorded_voiceovers: { voiceovers_mapping: { - content: {} - } + content: {}, + }, }, solicit_answer_details: false, interaction: { @@ -461,147 +508,173 @@ describe('ExplorationFooterComponent', () => { hints: [], customization_args: { recommendedExplorationIds: { - value: ['recommendedExplorationId'] - } + value: ['recommendedExplorationId'], + }, }, answer_groups: [], - default_outcome: null + default_outcome: null, }, param_changes: [], card_is_checkpoint: false, linked_skill_id: null, content: { content_id: 'content', - html: 'Congratulations, you have finished!' - } + html: 'Congratulations, you have finished!', + }, }; component.expInfo = sampleExpInfo; component.checkpointCount = 2; spyOn(playerPositionService, 'getDisplayedCardIndex').and.returnValue(2); - spyOn(explorationEngineService, 'getStateCardByName') - .and.returnValue(stateCard); - spyOn(explorationEngineService, 'getState') - .and.returnValue( - stateObjectFactory.createFromBackendDict('End', endState)); + spyOn(explorationEngineService, 'getStateCardByName').and.returnValue( + stateCard + ); + spyOn(explorationEngineService, 'getState').and.returnValue( + stateObjectFactory.createFromBackendDict('End', endState) + ); component.openProgressReminderModal(); tick(); fixture.detectChanges(); expect(ngbModal.open).toHaveBeenCalled(); - expect(editableExplorationBackendApiService.resetExplorationProgressAsync) - .toHaveBeenCalled(); + expect( + editableExplorationBackendApiService.resetExplorationProgressAsync + ).toHaveBeenCalled(); })); - it('should show \'Need help? Take a look at the concept' + - ' card for refreshing your concepts.\' tooltip', () => { - spyOn(conceptCardManagerService, 'isConceptCardTooltipOpen') - .and.returnValues(true, false); + it( + "should show 'Need help? Take a look at the concept" + + " card for refreshing your concepts.' tooltip", + () => { + spyOn( + conceptCardManagerService, + 'isConceptCardTooltipOpen' + ).and.returnValues(true, false); - expect(component.isTooltipVisible()).toBe(true); - expect(component.isTooltipVisible()).toBe(false); - }); + expect(component.isTooltipVisible()).toBe(true); + expect(component.isTooltipVisible()).toBe(false); + } + ); - it('should resume exploration if progress reminder modal is canceled', - fakeAsync(() => { - const ngbModal = TestBed.inject(NgbModal); + it('should resume exploration if progress reminder modal is canceled', fakeAsync(() => { + const ngbModal = TestBed.inject(NgbModal); - spyOn(ngbModal, 'open').and.returnValue({ - componentInstance: { - checkpointCount: 0, - completedCheckpointsCount: 0, - explorationTitle: '' - }, - result: Promise.reject() - } as NgbModalRef); - spyOn( - editableExplorationBackendApiService, 'resetExplorationProgressAsync'); + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: { + checkpointCount: 0, + completedCheckpointsCount: 0, + explorationTitle: '', + }, + result: Promise.reject(), + } as NgbModalRef); + spyOn( + editableExplorationBackendApiService, + 'resetExplorationProgressAsync' + ); - const stateCard = new StateCard( - 'End', '

Testing

', null, new Interaction( - [], [], null, null, [], 'EndExploration', null), - [], null, 'content', null - ); + const stateCard = new StateCard( + 'End', + '

Testing

', + null, + new Interaction([], [], null, null, [], 'EndExploration', null), + [], + null, + 'content', + null + ); - const endState = { - classifier_model_id: null, - recorded_voiceovers: { - voiceovers_mapping: { - content: {} - } + const endState = { + classifier_model_id: null, + recorded_voiceovers: { + voiceovers_mapping: { + content: {}, }, - solicit_answer_details: false, - interaction: { - solution: null, - confirmed_unclassified_answers: [], - id: 'EndExploration', - hints: [], - customization_args: { - recommendedExplorationIds: { - value: ['recommendedExplorationId'] - } + }, + solicit_answer_details: false, + interaction: { + solution: null, + confirmed_unclassified_answers: [], + id: 'EndExploration', + hints: [], + customization_args: { + recommendedExplorationIds: { + value: ['recommendedExplorationId'], }, - answer_groups: [], - default_outcome: null }, - param_changes: [], - card_is_checkpoint: false, - linked_skill_id: null, - content: { - content_id: 'content', - html: 'Congratulations, you have finished!' - } - }; - - component.expInfo = sampleExpInfo; - component.checkpointCount = 2; - spyOn(playerPositionService, 'getDisplayedCardIndex').and.returnValue(2); - spyOn(explorationEngineService, 'getStateCardByName') - .and.returnValue(stateCard); - spyOn(explorationEngineService, 'getState') - .and.returnValue( - stateObjectFactory.createFromBackendDict('End', endState)); - - component.openProgressReminderModal(); - tick(); - fixture.detectChanges(); - - expect(ngbModal.open).toHaveBeenCalled(); - expect(editableExplorationBackendApiService.resetExplorationProgressAsync) - .not.toHaveBeenCalled(); - })); + answer_groups: [], + default_outcome: null, + }, + param_changes: [], + card_is_checkpoint: false, + linked_skill_id: null, + content: { + content_id: 'content', + html: 'Congratulations, you have finished!', + }, + }; - it('should handle error if backend call to learnerViewInfoBackendApiService' + - ' fails while opening progress reminder modal', fakeAsync(() => { - component.explorationId = 'expId'; - component.expInfo = null; - spyOn(learnerViewInfoBackendApiService, 'fetchLearnerInfoAsync') - .and.returnValue(Promise.reject()); - spyOn(component, 'getMostRecentlyReachedCheckpointIndex') - .and.returnValue(3); - spyOn(loggerService, 'error'); + component.expInfo = sampleExpInfo; + component.checkpointCount = 2; + spyOn(playerPositionService, 'getDisplayedCardIndex').and.returnValue(2); + spyOn(explorationEngineService, 'getStateCardByName').and.returnValue( + stateCard + ); + spyOn(explorationEngineService, 'getState').and.returnValue( + stateObjectFactory.createFromBackendDict('End', endState) + ); - component.showProgressReminderModal(); + component.openProgressReminderModal(); tick(); + fixture.detectChanges(); - expect(loggerService.error).toHaveBeenCalled(); + expect(ngbModal.open).toHaveBeenCalled(); + expect( + editableExplorationBackendApiService.resetExplorationProgressAsync + ).not.toHaveBeenCalled(); })); - it('should not show hints after user finishes practice session' + - ' and results are loaded.', () => { - spyOn(contextService, 'getExplorationId').and.returnValue('exp1'); - spyOn(contextService, 'isInQuestionPlayerMode').and.returnValue(true); - expect(component.hintsAndSolutionsAreSupported).toBeTrue(); + it( + 'should handle error if backend call to learnerViewInfoBackendApiService' + + ' fails while opening progress reminder modal', + fakeAsync(() => { + component.explorationId = 'expId'; + component.expInfo = null; + spyOn( + learnerViewInfoBackendApiService, + 'fetchLearnerInfoAsync' + ).and.returnValue(Promise.reject()); + spyOn(component, 'getMostRecentlyReachedCheckpointIndex').and.returnValue( + 3 + ); + spyOn(loggerService, 'error'); - spyOnProperty(questionPlayerStateService, 'resultsPageIsLoadedEventEmitter') - .and.returnValue(mockResultsLoadedEventEmitter); + component.showProgressReminderModal(); + tick(); - component.ngOnInit(); - mockResultsLoadedEventEmitter.emit(true); + expect(loggerService.error).toHaveBeenCalled(); + }) + ); - expect(component.hintsAndSolutionsAreSupported).toBeFalse(); - }); + it( + 'should not show hints after user finishes practice session' + + ' and results are loaded.', + () => { + spyOn(contextService, 'getExplorationId').and.returnValue('exp1'); + spyOn(contextService, 'isInQuestionPlayerMode').and.returnValue(true); + expect(component.hintsAndSolutionsAreSupported).toBeTrue(); + + spyOnProperty( + questionPlayerStateService, + 'resultsPageIsLoadedEventEmitter' + ).and.returnValue(mockResultsLoadedEventEmitter); + + component.ngOnInit(); + mockResultsLoadedEventEmitter.emit(true); + + expect(component.hintsAndSolutionsAreSupported).toBeFalse(); + } + ); it('should open the lesson information card', fakeAsync(() => { let ngbModal = TestBed.inject(NgbModal); @@ -611,14 +684,14 @@ describe('ExplorationFooterComponent', () => { numberofCheckpoints: 0, completedWidth: 0, contributorNames: [], - expInfo: null + expInfo: null, }, result: { then: (successCallback: () => void, errorCallback: () => void) => { successCallback(); errorCallback(); - } - } + }, + }, } as NgbModalRef); let sampleExpResponse: FetchExplorationBackendResponse = { @@ -644,8 +717,8 @@ describe('ExplorationFooterComponent', () => { feedback_1: {}, rule_input_2: {}, content: {}, - default_outcome: {} - } + default_outcome: {}, + }, }, solicit_answer_details: false, interaction: { @@ -655,17 +728,17 @@ describe('ExplorationFooterComponent', () => { hints: [], customization_args: { rows: { - value: 1 + value: 1, }, placeholder: { value: { unicode_str: '', - content_id: 'ca_placeholder_0' - } + content_id: 'ca_placeholder_0', + }, }, catchMisspellings: { - value: false - } + value: false, + }, }, answer_groups: [ { @@ -675,28 +748,26 @@ describe('ExplorationFooterComponent', () => { labelled_as_correct: false, feedback: { content_id: 'feedback_1', - html: '

Good Job

' + html: '

Good Job

', }, param_changes: [], dest_if_really_stuck: null, - dest: 'Mid' + dest: 'Mid', }, training_data: [], rule_specs: [ { inputs: { x: { - normalizedStrSet: [ - 'answer' - ], - contentId: 'rule_input_2' - } + normalizedStrSet: ['answer'], + contentId: 'rule_input_2', + }, }, - rule_type: 'FuzzyEquals' - } + rule_type: 'FuzzyEquals', + }, ], - tagged_skill_misconception_id: null - } + tagged_skill_misconception_id: null, + }, ], default_outcome: { missing_prerequisite_skill_id: null, @@ -704,27 +775,27 @@ describe('ExplorationFooterComponent', () => { labelled_as_correct: false, feedback: { content_id: 'default_outcome', - html: '

Try again.

' + html: '

Try again.

', }, param_changes: [], dest_if_really_stuck: null, - dest: 'Start' - } + dest: 'Start', + }, }, param_changes: [], card_is_checkpoint: true, linked_skill_id: null, content: { content_id: 'content', - html: '

First Question

' - } + html: '

First Question

', + }, }, End: { classifier_model_id: null, recorded_voiceovers: { voiceovers_mapping: { - content: {} - } + content: {}, + }, }, solicit_answer_details: false, interaction: { @@ -734,19 +805,19 @@ describe('ExplorationFooterComponent', () => { hints: [], customization_args: { recommendedExplorationIds: { - value: ['recommnendedExplorationId'] - } + value: ['recommnendedExplorationId'], + }, }, answer_groups: [], - default_outcome: null + default_outcome: null, }, param_changes: [], card_is_checkpoint: false, linked_skill_id: null, content: { content_id: 'content', - html: 'Congratulations, you have finished!' - } + html: 'Congratulations, you have finished!', + }, }, Mid: { classifier_model_id: null, @@ -756,8 +827,8 @@ describe('ExplorationFooterComponent', () => { feedback_1: {}, rule_input_2: {}, content: {}, - default_outcome: {} - } + default_outcome: {}, + }, }, solicit_answer_details: false, interaction: { @@ -767,17 +838,17 @@ describe('ExplorationFooterComponent', () => { hints: [], customization_args: { rows: { - value: 1 + value: 1, }, placeholder: { value: { unicode_str: '', - content_id: 'ca_placeholder_0' - } + content_id: 'ca_placeholder_0', + }, }, catchMisspellings: { - value: false - } + value: false, + }, }, answer_groups: [ { @@ -787,28 +858,26 @@ describe('ExplorationFooterComponent', () => { labelled_as_correct: false, feedback: { content_id: 'feedback_1', - html: '

Good Job

' + html: '

Good Job

', }, param_changes: [], dest_if_really_stuck: null, - dest: 'End' + dest: 'End', }, training_data: [], rule_specs: [ { inputs: { x: { - normalizedStrSet: [ - 'answer' - ], - contentId: 'rule_input_2' - } + normalizedStrSet: ['answer'], + contentId: 'rule_input_2', + }, }, - rule_type: 'FuzzyEquals' - } + rule_type: 'FuzzyEquals', + }, ], - tagged_skill_misconception_id: null - } + tagged_skill_misconception_id: null, + }, ], default_outcome: { missing_prerequisite_skill_id: null, @@ -816,22 +885,22 @@ describe('ExplorationFooterComponent', () => { labelled_as_correct: false, feedback: { content_id: 'default_outcome', - html: '

try again.

' + html: '

try again.

', }, param_changes: [], dest_if_really_stuck: null, - dest: 'Mid' - } + dest: 'Mid', + }, }, param_changes: [], card_is_checkpoint: true, linked_skill_id: null, content: { content_id: 'content', - html: '

Second Question

' - } - } - } + html: '

Second Question

', + }, + }, + }, }, exploration_metadata: { title: 'Exploration', @@ -846,7 +915,7 @@ describe('ExplorationFooterComponent', () => { param_specs: {}, param_changes: [], auto_tts_enabled: false, - edits_allowed: true + edits_allowed: true, }, version: 1, can_edit: true, @@ -858,29 +927,40 @@ describe('ExplorationFooterComponent', () => { furthest_reached_checkpoint_exp_version: 1, furthest_reached_checkpoint_state_name: 'Mid', most_recently_reached_checkpoint_state_name: 'Mid', - most_recently_reached_checkpoint_exp_version: 1 + most_recently_reached_checkpoint_exp_version: 1, }; - spyOn(readOnlyExplorationBackendApiService, 'loadLatestExplorationAsync') - .and.returnValue(Promise.resolve(sampleExpResponse)); + spyOn( + readOnlyExplorationBackendApiService, + 'loadLatestExplorationAsync' + ).and.returnValue(Promise.resolve(sampleExpResponse)); component.checkpointCount = 2; - spyOn(component, 'getMostRecentlyReachedCheckpointIndex') - .and.returnValue(2); - spyOn(explorationEngineService, 'getState') - .and.returnValue(stateObjectFactory.createFromBackendDict( - 'End', sampleExpResponse.exploration.states.End - )); + spyOn(component, 'getMostRecentlyReachedCheckpointIndex').and.returnValue( + 2 + ); + spyOn(explorationEngineService, 'getState').and.returnValue( + stateObjectFactory.createFromBackendDict( + 'End', + sampleExpResponse.exploration.states.End + ) + ); let stateCard = new StateCard( - 'End', '

Testing

', null, new Interaction( - [], [], null, null, [], 'EndExploration', null), - [], null, 'content', null + 'End', + '

Testing

', + null, + new Interaction([], [], null, null, [], 'EndExploration', null), + [], + null, + 'content', + null ); - spyOn(explorationEngineService, 'getStateCardByName') - .and.returnValue(stateCard); + spyOn(explorationEngineService, 'getStateCardByName').and.returnValue( + stateCard + ); spyOn(playerPositionService, 'getDisplayedCardIndex').and.returnValue(2); component.openInformationCardModal(); tick(); @@ -893,10 +973,10 @@ describe('ExplorationFooterComponent', () => { it('should open concept card when user clicks on the icon', () => { const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ( - { componentInstance: MockNgbModalRef, - result: Promise.resolve() - }) as NgbModalRef; + return { + componentInstance: MockNgbModalRef, + result: Promise.resolve(), + } as NgbModalRef; }); component.openConceptCardModal(); expect(modalSpy).toHaveBeenCalled(); @@ -909,14 +989,14 @@ describe('ExplorationFooterComponent', () => { classifier_model_id: null, recorded_voiceovers: { voiceovers_mapping: { - content: {} - } + content: {}, + }, }, solicit_answer_details: false, written_translations: { translations_mapping: { - content: {} - } + content: {}, + }, }, interaction: { solution: null, @@ -925,11 +1005,11 @@ describe('ExplorationFooterComponent', () => { hints: [], customization_args: { recommendedExplorationIds: { - value: ['recommendedExplorationId'] - } + value: ['recommendedExplorationId'], + }, }, answer_groups: [], - default_outcome: null + default_outcome: null, }, param_changes: [], next_content_id_index: 0, @@ -937,12 +1017,12 @@ describe('ExplorationFooterComponent', () => { linked_skill_id: 'Id', content: { content_id: 'content', - html: 'Congratulations, you have finished!' - } + html: 'Congratulations, you have finished!', + }, }; - spyOn(explorationEngineService, 'getState') - .and.returnValue( - stateObjectFactory.createFromBackendDict('End', endState)); + spyOn(explorationEngineService, 'getState').and.returnValue( + stateObjectFactory.createFromBackendDict('End', endState) + ); component.showConceptCard(); @@ -956,10 +1036,14 @@ describe('ExplorationFooterComponent', () => { spyOn(component, 'openInformationCardModal'); component.showInformationCard(); - spyOn(learnerViewInfoBackendApiService, 'fetchLearnerInfoAsync') - .and.returnValue(Promise.resolve({ - summaries: [] - })); + spyOn( + learnerViewInfoBackendApiService, + 'fetchLearnerInfoAsync' + ).and.returnValue( + Promise.resolve({ + summaries: [], + }) + ); expect(component.openInformationCardModal).toHaveBeenCalled(); component.expInfo = null; @@ -967,78 +1051,86 @@ describe('ExplorationFooterComponent', () => { component.showInformationCard(); tick(); - expect(learnerViewInfoBackendApiService.fetchLearnerInfoAsync) - .toHaveBeenCalled(); + expect( + learnerViewInfoBackendApiService.fetchLearnerInfoAsync + ).toHaveBeenCalled(); })); it('should get footer image url', () => { spyOn(urlInterpolationService, 'getStaticImageUrl').and.returnValue( - 'dummy_image_url'); + 'dummy_image_url' + ); - expect(component.getStaticImageUrl('general/apple.svg')) - .toEqual('dummy_image_url'); + expect(component.getStaticImageUrl('general/apple.svg')).toEqual( + 'dummy_image_url' + ); expect(urlInterpolationService.getStaticImageUrl).toHaveBeenCalledWith( - 'general/apple.svg'); + 'general/apple.svg' + ); }); it('should get checkpoint index from state name', fakeAsync(() => { spyOn(contextService, 'getExplorationId').and.returnValue('exp1'); spyOn(playerTranscriptService, 'getNumCards').and.returnValue(1); const card = StateCard.createNewCard( - 'State A', '

Content

', '', + 'State A', + '

Content

', + '', null, RecordedVoiceovers.createEmpty(), - 'content', audioTranslationLanguageService); + 'content', + audioTranslationLanguageService + ); spyOn(playerTranscriptService, 'getCard').and.returnValue(card); - spyOn(explorationEngineService, 'getStateFromStateName') - .and.returnValue(stateObjectFactory.createFromBackendDict( - 'State A', { - classifier_model_id: null, - content: { - html: '', - content_id: 'content' - }, - interaction: { - id: 'FractionInput', - customization_args: { - requireSimplestForm: { value: false }, - allowImproperFraction: { value: true }, - allowNonzeroIntegerPart: { value: true }, - customPlaceholder: { value: { + spyOn(explorationEngineService, 'getStateFromStateName').and.returnValue( + stateObjectFactory.createFromBackendDict('State A', { + classifier_model_id: null, + content: { + html: '', + content_id: 'content', + }, + interaction: { + id: 'FractionInput', + customization_args: { + requireSimplestForm: {value: false}, + allowImproperFraction: {value: true}, + allowNonzeroIntegerPart: {value: true}, + customPlaceholder: { + value: { content_id: '', - unicode_str: '' - } }, - }, - answer_groups: [], - default_outcome: { - dest: 'Introduction', - dest_if_really_stuck: null, - feedback: { - content_id: 'default_outcome', - html: '' + unicode_str: '', }, - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null }, - confirmed_unclassified_answers: [], - hints: [], - solution: null }, - linked_skill_id: null, - param_changes: [], - recorded_voiceovers: { - voiceovers_mapping: { - content: {}, - default_outcome: {} - } + answer_groups: [], + default_outcome: { + dest: 'Introduction', + dest_if_really_stuck: null, + feedback: { + content_id: 'default_outcome', + html: '', + }, + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, }, - solicit_answer_details: false, - card_is_checkpoint: true, - } - - )); + confirmed_unclassified_answers: [], + hints: [], + solution: null, + }, + linked_skill_id: null, + param_changes: [], + recorded_voiceovers: { + voiceovers_mapping: { + content: {}, + default_outcome: {}, + }, + }, + solicit_answer_details: false, + card_is_checkpoint: true, + }) + ); let checkpointIndex = component.getMostRecentlyReachedCheckpointIndex(); tick(); @@ -1046,21 +1138,26 @@ describe('ExplorationFooterComponent', () => { expect(checkpointIndex).toEqual(1); })); - it('should handle error if backend call' + - 'to learnerViewInfoBackendApiService fails', fakeAsync(() => { - let explorationId = 'expId'; - component.explorationId = explorationId; - component.expInfo = null; + it( + 'should handle error if backend call' + + 'to learnerViewInfoBackendApiService fails', + fakeAsync(() => { + let explorationId = 'expId'; + component.explorationId = explorationId; + component.expInfo = null; - spyOn(learnerViewInfoBackendApiService, 'fetchLearnerInfoAsync') - .and.returnValue(Promise.reject()); - spyOn(loggerService, 'error'); + spyOn( + learnerViewInfoBackendApiService, + 'fetchLearnerInfoAsync' + ).and.returnValue(Promise.reject()); + spyOn(loggerService, 'error'); - component.showInformationCard(); - tick(); + component.showInformationCard(); + tick(); - expect(loggerService.error).toHaveBeenCalled(); - })); + expect(loggerService.error).toHaveBeenCalled(); + }) + ); it('should fetch number of checkpoints correctly', fakeAsync(() => { let sampleDataResults: FetchExplorationBackendResponse = { @@ -1086,7 +1183,7 @@ describe('ExplorationFooterComponent', () => { linked_skill_id: null, content: { html: '', - content_id: 'content' + content_id: 'content', }, interaction: { customization_args: {}, @@ -1099,17 +1196,17 @@ describe('ExplorationFooterComponent', () => { dest: 'Introduction', feedback: { html: '', - content_id: 'content' + content_id: 'content', }, labelled_as_correct: true, refresher_exploration_id: 'exp', - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, confirmed_unclassified_answers: [], - id: null - } - } - } + id: null, + }, + }, + }, }, exploration_metadata: { title: 'Exploration', @@ -1124,7 +1221,7 @@ describe('ExplorationFooterComponent', () => { param_specs: {}, param_changes: [], auto_tts_enabled: false, - edits_allowed: true + edits_allowed: true, }, version: 1, can_edit: true, @@ -1137,13 +1234,15 @@ describe('ExplorationFooterComponent', () => { furthest_reached_checkpoint_exp_version: 1, furthest_reached_checkpoint_state_name: 'State B', most_recently_reached_checkpoint_state_name: 'State A', - most_recently_reached_checkpoint_exp_version: 1 + most_recently_reached_checkpoint_exp_version: 1, }; component.explorationId = 'expId'; - spyOn(readOnlyExplorationBackendApiService, 'fetchExplorationAsync') - .and.returnValue(Promise.resolve(sampleDataResults)); + spyOn( + readOnlyExplorationBackendApiService, + 'fetchExplorationAsync' + ).and.returnValue(Promise.resolve(sampleDataResults)); expect(component.checkpointCount).toEqual(0); component.getCheckpointCount(); @@ -1177,7 +1276,7 @@ describe('ExplorationFooterComponent', () => { linked_skill_id: null, content: { html: '', - content_id: 'content' + content_id: 'content', }, interaction: { customization_args: {}, @@ -1190,17 +1289,17 @@ describe('ExplorationFooterComponent', () => { dest_if_really_stuck: null, feedback: { html: '', - content_id: 'content' + content_id: 'content', }, labelled_as_correct: true, refresher_exploration_id: 'exp', - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, confirmed_unclassified_answers: [], - id: null - } - } - } + id: null, + }, + }, + }, }, exploration_metadata: { title: 'Exploration', @@ -1215,7 +1314,7 @@ describe('ExplorationFooterComponent', () => { param_specs: {}, param_changes: [], auto_tts_enabled: false, - edits_allowed: true + edits_allowed: true, }, version: 1, can_edit: true, @@ -1228,11 +1327,13 @@ describe('ExplorationFooterComponent', () => { furthest_reached_checkpoint_exp_version: 1, furthest_reached_checkpoint_state_name: 'State B', most_recently_reached_checkpoint_state_name: 'State A', - most_recently_reached_checkpoint_exp_version: 1 + most_recently_reached_checkpoint_exp_version: 1, }; - spyOn(readOnlyExplorationBackendApiService, 'fetchExplorationAsync') - .and.returnValue(Promise.resolve(sampleDataResults)); + spyOn( + readOnlyExplorationBackendApiService, + 'fetchExplorationAsync' + ).and.returnValue(Promise.resolve(sampleDataResults)); component.explorationId = 'expId'; @@ -1245,40 +1346,48 @@ describe('ExplorationFooterComponent', () => { it('should correctly mark lesson info tooltip as viewed', () => { spyOn( editableExplorationBackendApiService, - 'recordLearnerHasViewedLessonInfoModalOnce').and.returnValue( - Promise.resolve()); + 'recordLearnerHasViewedLessonInfoModalOnce' + ).and.returnValue(Promise.resolve()); expect(component.hasLearnerHasViewedLessonInfoTooltip()).toBeFalse(); component.userIsLoggedIn = true; component.learnerHasViewedLessonInfo(); expect(component.hasLearnerHasViewedLessonInfoTooltip()).toBeTrue(); expect( - editableExplorationBackendApiService. - recordLearnerHasViewedLessonInfoModalOnce).toHaveBeenCalled(); + editableExplorationBackendApiService.recordLearnerHasViewedLessonInfoModalOnce + ).toHaveBeenCalled(); }); - it('should show hints when initialized in question player when user is' + - ' going through the practice session and should add subscription.', () => { - spyOn(contextService, 'getExplorationId').and.returnValue('expId'); - spyOn(contextService, 'isInQuestionPlayerMode').and.returnValue(true); - spyOn( - questionPlayerStateService.resultsPageIsLoadedEventEmitter, 'subscribe'); + it( + 'should show hints when initialized in question player when user is' + + ' going through the practice session and should add subscription.', + () => { + spyOn(contextService, 'getExplorationId').and.returnValue('expId'); + spyOn(contextService, 'isInQuestionPlayerMode').and.returnValue(true); + spyOn( + questionPlayerStateService.resultsPageIsLoadedEventEmitter, + 'subscribe' + ); - component.ngOnInit(); + component.ngOnInit(); - expect(component.hintsAndSolutionsAreSupported).toBeTrue(); - expect(questionPlayerStateService.resultsPageIsLoadedEventEmitter.subscribe) - .toHaveBeenCalled(); - }); + expect(component.hintsAndSolutionsAreSupported).toBeTrue(); + expect( + questionPlayerStateService.resultsPageIsLoadedEventEmitter.subscribe + ).toHaveBeenCalled(); + } + ); it('should check if window is narrow when user resizes window', () => { spyOn(contextService, 'getExplorationId').and.returnValue('exp1'); spyOn(urlService, 'isIframed').and.returnValue(true); spyOn(windowDimensionsService, 'isWindowNarrow').and.returnValue(false); spyOn(windowDimensionsService, 'getResizeEvent').and.returnValue( - mockResizeEventEmitter); + mockResizeEventEmitter + ); spyOn(contextService, 'isInQuestionPlayerMode').and.returnValue(false); - spyOn(contextService, 'getQuestionPlayerIsManuallySet').and - .returnValue(false); + spyOn(contextService, 'getQuestionPlayerIsManuallySet').and.returnValue( + false + ); component.windowIsNarrow = true; component.ngOnInit(); @@ -1291,38 +1400,45 @@ describe('ExplorationFooterComponent', () => { spyOn(contextService, 'getExplorationId').and.returnValue('expId'); spyOn(contextService, 'isInQuestionPlayerMode').and.returnValue(true); spyOn( - checkpointCelebrationUtilityService - .getOpenLessonInformationModalEmitter(), 'subscribe') - .and.callThrough(); + checkpointCelebrationUtilityService.getOpenLessonInformationModalEmitter(), + 'subscribe' + ).and.callThrough(); spyOn(component, 'showInformationCard'); component.ngOnInit(); checkpointCelebrationUtilityService.openLessonInformationModal(); expect( - checkpointCelebrationUtilityService - .getOpenLessonInformationModalEmitter().subscribe).toHaveBeenCalled(); + checkpointCelebrationUtilityService.getOpenLessonInformationModalEmitter() + .subscribe + ).toHaveBeenCalled(); expect(component.showInformationCard).toHaveBeenCalled(); }); - it('should not display author names when exploration is in question' + - ' player mode', () => { - spyOn(contextService, 'getExplorationId').and.returnValue('exp1'); - spyOn(urlService, 'isIframed').and.returnValue(true); - spyOn(windowDimensionsService, 'isWindowNarrow').and.returnValue(false); - spyOn(windowDimensionsService, 'getResizeEvent').and.returnValue( - mockResizeEventEmitter); - spyOn(contextService, 'isInQuestionPlayerMode').and.returnValue(true); - spyOn(contextService, 'getQuestionPlayerIsManuallySet').and - .returnValue(false); - spyOn( - explorationSummaryBackendApiService, - 'loadPublicAndPrivateExplorationSummariesAsync'); + it( + 'should not display author names when exploration is in question' + + ' player mode', + () => { + spyOn(contextService, 'getExplorationId').and.returnValue('exp1'); + spyOn(urlService, 'isIframed').and.returnValue(true); + spyOn(windowDimensionsService, 'isWindowNarrow').and.returnValue(false); + spyOn(windowDimensionsService, 'getResizeEvent').and.returnValue( + mockResizeEventEmitter + ); + spyOn(contextService, 'isInQuestionPlayerMode').and.returnValue(true); + spyOn(contextService, 'getQuestionPlayerIsManuallySet').and.returnValue( + false + ); + spyOn( + explorationSummaryBackendApiService, + 'loadPublicAndPrivateExplorationSummariesAsync' + ); - component.ngOnInit(); + component.ngOnInit(); - expect( - explorationSummaryBackendApiService. - loadPublicAndPrivateExplorationSummariesAsync).not.toHaveBeenCalled(); - }); + expect( + explorationSummaryBackendApiService.loadPublicAndPrivateExplorationSummariesAsync + ).not.toHaveBeenCalled(); + } + ); }); diff --git a/core/templates/pages/exploration-player-page/layout-directives/exploration-footer.component.ts b/core/templates/pages/exploration-player-page/layout-directives/exploration-footer.component.ts index 84bfcdbb74f2..274bc8f96823 100644 --- a/core/templates/pages/exploration-player-page/layout-directives/exploration-footer.component.ts +++ b/core/templates/pages/exploration-player-page/layout-directives/exploration-footer.component.ts @@ -17,43 +17,45 @@ * in exploration player. */ -import { Component, ElementRef, ViewChild } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { QuestionPlayerStateService } from 'components/question-directives/question-player/services/question-player-state.service'; -import { EditableExplorationBackendApiService } from 'domain/exploration/editable-exploration-backend-api.service'; -import { FetchExplorationBackendResponse, ReadOnlyExplorationBackendApiService } from 'domain/exploration/read-only-exploration-backend-api.service'; -import { StateObjectsBackendDict } from 'domain/exploration/StatesObjectFactory'; -import { ExplorationSummaryBackendApiService } from 'domain/summary/exploration-summary-backend-api.service'; -import { LearnerExplorationSummaryBackendDict } from 'domain/summary/learner-exploration-summary.model'; -import { Subscription } from 'rxjs'; -import { ContextService } from 'services/context.service'; -import { LoggerService } from 'services/contextual/logger.service'; -import { UrlService } from 'services/contextual/url.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { UserService } from 'services/user.service'; -import { ExplorationEngineService } from '../services/exploration-engine.service'; -import { ExplorationPlayerStateService } from '../services/exploration-player-state.service'; -import { LearnerViewInfoBackendApiService } from '../services/learner-view-info-backend-api.service'; -import { PlayerPositionService } from '../services/player-position.service'; -import { PlayerTranscriptService } from '../services/player-transcript.service'; -import { LessonInformationCardModalComponent } from 'pages/exploration-player-page/templates/lesson-information-card-modal.component'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { ProgressReminderModalComponent } from 'pages/exploration-player-page/templates/progress-reminder-modal.component'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { CheckpointCelebrationUtilityService } from 'pages/exploration-player-page/services/checkpoint-celebration-utility.service'; +import {Component, ElementRef, ViewChild} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {QuestionPlayerStateService} from 'components/question-directives/question-player/services/question-player-state.service'; +import {EditableExplorationBackendApiService} from 'domain/exploration/editable-exploration-backend-api.service'; +import { + FetchExplorationBackendResponse, + ReadOnlyExplorationBackendApiService, +} from 'domain/exploration/read-only-exploration-backend-api.service'; +import {StateObjectsBackendDict} from 'domain/exploration/StatesObjectFactory'; +import {ExplorationSummaryBackendApiService} from 'domain/summary/exploration-summary-backend-api.service'; +import {LearnerExplorationSummaryBackendDict} from 'domain/summary/learner-exploration-summary.model'; +import {Subscription} from 'rxjs'; +import {ContextService} from 'services/context.service'; +import {LoggerService} from 'services/contextual/logger.service'; +import {UrlService} from 'services/contextual/url.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {UserService} from 'services/user.service'; +import {ExplorationEngineService} from '../services/exploration-engine.service'; +import {ExplorationPlayerStateService} from '../services/exploration-player-state.service'; +import {LearnerViewInfoBackendApiService} from '../services/learner-view-info-backend-api.service'; +import {PlayerPositionService} from '../services/player-position.service'; +import {PlayerTranscriptService} from '../services/player-transcript.service'; +import {LessonInformationCardModalComponent} from 'pages/exploration-player-page/templates/lesson-information-card-modal.component'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {ProgressReminderModalComponent} from 'pages/exploration-player-page/templates/progress-reminder-modal.component'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {CheckpointCelebrationUtilityService} from 'pages/exploration-player-page/services/checkpoint-celebration-utility.service'; import './exploration-footer.component.css'; -import { OppiaNoninteractiveSkillreviewConceptCardModalComponent } from 'rich_text_components/Skillreview/directives/oppia-noninteractive-skillreview-concept-card-modal.component'; -import { ConceptCardManagerService } from '../services/concept-card-manager.service'; -import { StateCard } from 'domain/state_card/state-card.model'; - +import {OppiaNoninteractiveSkillreviewConceptCardModalComponent} from 'rich_text_components/Skillreview/directives/oppia-noninteractive-skillreview-concept-card-modal.component'; +import {ConceptCardManagerService} from '../services/concept-card-manager.service'; +import {StateCard} from 'domain/state_card/state-card.model'; @Component({ selector: 'oppia-exploration-footer', templateUrl: './exploration-footer.component.html', - styleUrls: ['./exploration-footer.component.css'] + styleUrls: ['./exploration-footer.component.css'], }) export class ExplorationFooterComponent { directiveSubscriptions = new Subscription(); @@ -87,15 +89,13 @@ export class ExplorationFooterComponent { constructor( private contextService: ContextService, - private explorationSummaryBackendApiService: - ExplorationSummaryBackendApiService, + private explorationSummaryBackendApiService: ExplorationSummaryBackendApiService, private i18nLanguageCodeService: I18nLanguageCodeService, private ngbModal: NgbModal, private urlService: UrlService, private windowDimensionsService: WindowDimensionsService, private questionPlayerStateService: QuestionPlayerStateService, - private readOnlyExplorationBackendApiService: - ReadOnlyExplorationBackendApiService, + private readOnlyExplorationBackendApiService: ReadOnlyExplorationBackendApiService, private learnerViewInfoBackendApiService: LearnerViewInfoBackendApiService, private loggerService: LoggerService, private playerTranscriptService: PlayerTranscriptService, @@ -103,12 +103,10 @@ export class ExplorationFooterComponent { private explorationEngineService: ExplorationEngineService, private explorationPlayerStateService: ExplorationPlayerStateService, private userService: UserService, - private editableExplorationBackendApiService: - EditableExplorationBackendApiService, + private editableExplorationBackendApiService: EditableExplorationBackendApiService, private urlInterpolationService: UrlInterpolationService, private windowRef: WindowRef, - private checkpointCelebrationUtilityService: - CheckpointCelebrationUtilityService, + private checkpointCelebrationUtilityService: CheckpointCelebrationUtilityService, private conceptCardManagerService: ConceptCardManagerService ) {} @@ -124,7 +122,7 @@ export class ExplorationFooterComponent { try { this.explorationId = this.contextService.getExplorationId(); this.iframed = this.urlService.isIframed(); - this.userService.getUserInfoAsync().then((userInfo) => { + this.userService.getUserInfoAsync().then(userInfo => { this.userIsLoggedIn = userInfo.isLoggedIn(); }); this.windowIsNarrow = this.windowDimensionsService.isWindowNarrow(); @@ -133,34 +131,37 @@ export class ExplorationFooterComponent { this.windowIsNarrow = this.windowDimensionsService.isWindowNarrow(); }) ); - if (!this.contextService.isInQuestionPlayerMode() || - this.contextService.getQuestionPlayerIsManuallySet()) { + if ( + !this.contextService.isInQuestionPlayerMode() || + this.contextService.getQuestionPlayerIsManuallySet() + ) { this.explorationSummaryBackendApiService .loadPublicAndPrivateExplorationSummariesAsync([this.explorationId]) - .then((responseObject) => { + .then(responseObject => { let summaries = responseObject.summaries; if (summaries.length > 0) { - let contributorSummary = ( - summaries[0].human_readable_contributors_summary); + let contributorSummary = + summaries[0].human_readable_contributors_summary; this.contributorNames = Object.keys(contributorSummary).sort( (contributorUsername1, contributorUsername2) => { - let commitsOfContributor1 = contributorSummary[ - contributorUsername1].num_commits; - let commitsOfContributor2 = contributorSummary[ - contributorUsername2].num_commits; + let commitsOfContributor1 = + contributorSummary[contributorUsername1].num_commits; + let commitsOfContributor2 = + contributorSummary[contributorUsername2].num_commits; return commitsOfContributor2 - commitsOfContributor1; } ); } }); } - } catch (err) { } + } catch (err) {} if (this.contextService.isInQuestionPlayerMode()) { - this.questionPlayerStateService.resultsPageIsLoadedEventEmitter - .subscribe((resultsLoaded: boolean) => { + this.questionPlayerStateService.resultsPageIsLoadedEventEmitter.subscribe( + (resultsLoaded: boolean) => { this.hintsAndSolutionsAreSupported = !resultsLoaded; - }); + } + ); this.footerIsInQuestionPlayerMode = true; } else if (this.explorationId) { // Fetching the number of checkpoints. @@ -180,7 +181,8 @@ export class ExplorationFooterComponent { ); this.directiveSubscriptions.add( this.checkpointCelebrationUtilityService - .getOpenLessonInformationModalEmitter().subscribe(() => { + .getOpenLessonInformationModalEmitter() + .subscribe(() => { this.showInformationCard(); }) ); @@ -198,9 +200,8 @@ export class ExplorationFooterComponent { } showProgressReminderModal(): void { - const mostRecentlyReachedCheckpointIndex = ( - this.getMostRecentlyReachedCheckpointIndex() - ); + const mostRecentlyReachedCheckpointIndex = + this.getMostRecentlyReachedCheckpointIndex(); this.completedCheckpointsCount = mostRecentlyReachedCheckpointIndex - 1; @@ -212,21 +213,23 @@ export class ExplorationFooterComponent { if (this.expInfo) { this.openProgressReminderModal(); } else { - let stringifiedExpIds = JSON.stringify( - [this.explorationId]); + let stringifiedExpIds = JSON.stringify([this.explorationId]); let includePrivateExplorations = JSON.stringify(true); - this.learnerViewInfoBackendApiService.fetchLearnerInfoAsync( - stringifiedExpIds, - includePrivateExplorations - ).then((response) => { - this.expInfo = response.summaries[0]; - this.openProgressReminderModal(); - }, () => { - this.loggerService.error( - 'Information card failed to load for exploration ' + - this.explorationId); - }); + this.learnerViewInfoBackendApiService + .fetchLearnerInfoAsync(stringifiedExpIds, includePrivateExplorations) + .then( + response => { + this.expInfo = response.summaries[0]; + this.openProgressReminderModal(); + }, + () => { + this.loggerService.error( + 'Information card failed to load for exploration ' + + this.explorationId + ); + } + ); } } @@ -236,60 +239,61 @@ export class ExplorationFooterComponent { openProgressReminderModal(): void { let modalRef = this.ngbModal.open(ProgressReminderModalComponent, { - windowClass: 'oppia-progress-reminder-modal' + windowClass: 'oppia-progress-reminder-modal', }); this.explorationPlayerStateService.onShowProgressModal.emit(); - let displayedCardIndex = ( - this.playerPositionService.getDisplayedCardIndex() - ); + let displayedCardIndex = this.playerPositionService.getDisplayedCardIndex(); if (displayedCardIndex > 0) { let state = this.explorationEngineService.getState(); let stateCard = this.explorationEngineService.getStateCardByName( - state.name); + state.name + ); if (stateCard.isTerminal()) { this.completedCheckpointsCount += 1; } } modalRef.componentInstance.checkpointCount = this.checkpointCount; - modalRef.componentInstance.completedCheckpointsCount = ( - this.completedCheckpointsCount); + modalRef.componentInstance.completedCheckpointsCount = + this.completedCheckpointsCount; modalRef.componentInstance.explorationTitle = this.expInfo.title; - modalRef.result.then(() => { - // This callback is used for when the learner chooses to restart - // the exploration. - this.editableExplorationBackendApiService.resetExplorationProgressAsync( - this.explorationId).then(() => { - this.windowRef.nativeWindow.location.reload(); - }); - }, () => { - // This callback is used for when the learner chooses to resume - // the exploration. - }); + modalRef.result.then( + () => { + // This callback is used for when the learner chooses to restart + // the exploration. + this.editableExplorationBackendApiService + .resetExplorationProgressAsync(this.explorationId) + .then(() => { + this.windowRef.nativeWindow.location.reload(); + }); + }, + () => { + // This callback is used for when the learner chooses to resume + // the exploration. + } + ); } openInformationCardModal(): void { let modalRef = this.ngbModal.open(LessonInformationCardModalComponent, { - windowClass: 'oppia-modal-lesson-information-card' + windowClass: 'oppia-modal-lesson-information-card', }); modalRef.componentInstance.checkpointCount = this.checkpointCount; - let mostRecentlyReachedCheckpointIndex = ( - this.getMostRecentlyReachedCheckpointIndex() - ); + let mostRecentlyReachedCheckpointIndex = + this.getMostRecentlyReachedCheckpointIndex(); this.completedCheckpointsCount = mostRecentlyReachedCheckpointIndex - 1; - let displayedCardIndex = ( - this.playerPositionService.getDisplayedCardIndex() - ); + let displayedCardIndex = this.playerPositionService.getDisplayedCardIndex(); if (displayedCardIndex > 0) { let state = this.explorationEngineService.getState(); let stateCard = this.explorationEngineService.getStateCardByName( - state.name); + state.name + ); if (stateCard.isTerminal()) { this.completedCheckpointsCount += 1; } @@ -303,17 +307,20 @@ export class ExplorationFooterComponent { this.completedCheckpointsCount = this.checkpointCount; } - modalRef.componentInstance.completedCheckpointsCount = ( - this.completedCheckpointsCount); + modalRef.componentInstance.completedCheckpointsCount = + this.completedCheckpointsCount; modalRef.componentInstance.contributorNames = this.contributorNames; modalRef.componentInstance.expInfo = this.expInfo; modalRef.componentInstance.userIsLoggedIn = this.userIsLoggedIn; - modalRef.result.then(() => {}, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + modalRef.result.then( + () => {}, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } openConceptCardModal(): void { @@ -326,8 +333,7 @@ export class ExplorationFooterComponent { } showInformationCard(): void { - let stringifiedExpIds = JSON.stringify( - [this.explorationId]); + let stringifiedExpIds = JSON.stringify([this.explorationId]); let includePrivateExplorations = JSON.stringify(true); if (this.expInfo) { this.openInformationCardModal(); @@ -338,17 +344,20 @@ export class ExplorationFooterComponent { this.learnerHasViewedLessonInfo(); } } else { - this.learnerViewInfoBackendApiService.fetchLearnerInfoAsync( - stringifiedExpIds, - includePrivateExplorations - ).then((response) => { - this.expInfo = response.summaries[0]; - this.openInformationCardModal(); - }, () => { - this.loggerService.error( - 'Information card failed to load for exploration ' + - this.explorationId); - }); + this.learnerViewInfoBackendApiService + .fetchLearnerInfoAsync(stringifiedExpIds, includePrivateExplorations) + .then( + response => { + this.expInfo = response.summaries[0]; + this.openInformationCardModal(); + }, + () => { + this.loggerService.error( + 'Information card failed to load for exploration ' + + this.explorationId + ); + } + ); } } @@ -373,8 +382,8 @@ export class ExplorationFooterComponent { let numberOfCards = this.playerTranscriptService.getNumCards(); for (let i = 0; i < numberOfCards; i++) { let stateName = this.playerTranscriptService.getCard(i).getStateName(); - let correspondingState = this.explorationEngineService. - getStateFromStateName(stateName); + let correspondingState = + this.explorationEngineService.getStateFromStateName(stateName); if (correspondingState.cardIsCheckpoint) { checkpointIndex++; } @@ -384,17 +393,17 @@ export class ExplorationFooterComponent { async getCheckpointCount(): Promise { return this.readOnlyExplorationBackendApiService - .fetchExplorationAsync(this.explorationId, null).then( - (response: FetchExplorationBackendResponse) => { - this.expStates = response.exploration.states; - let count = 0; - for (let [, value] of Object.entries(this.expStates)) { - if (value.card_is_checkpoint) { - count++; - } + .fetchExplorationAsync(this.explorationId, null) + .then((response: FetchExplorationBackendResponse) => { + this.expStates = response.exploration.states; + let count = 0; + for (let [, value] of Object.entries(this.expStates)) { + if (value.card_is_checkpoint) { + count++; } - this.checkpointCount = count; - }); + } + this.checkpointCount = count; + }); } isLanguageRTL(): boolean { @@ -403,18 +412,18 @@ export class ExplorationFooterComponent { async setLearnerHasViewedLessonInfoTooltip(): Promise { return this.readOnlyExplorationBackendApiService - .fetchExplorationAsync(this.explorationId, null).then( - (response: FetchExplorationBackendResponse) => { - this.learnerHasViewedLessonInfoTooltip = ( - response.has_viewed_lesson_info_modal_once); - }); + .fetchExplorationAsync(this.explorationId, null) + .then((response: FetchExplorationBackendResponse) => { + this.learnerHasViewedLessonInfoTooltip = + response.has_viewed_lesson_info_modal_once; + }); } shouldRenderLessonInfoTooltip(): boolean { const shouldRenderLessonInfoTooltip = - !this.footerIsInQuestionPlayerMode && - !this.hasLearnerHasViewedLessonInfoTooltip() && - this.getMostRecentlyReachedCheckpointIndex() === 2; + !this.footerIsInQuestionPlayerMode && + !this.hasLearnerHasViewedLessonInfoTooltip() && + this.getMostRecentlyReachedCheckpointIndex() === 2; if (shouldRenderLessonInfoTooltip) { this.lessonInfoButton.nativeElement.focus(); @@ -425,8 +434,7 @@ export class ExplorationFooterComponent { learnerHasViewedLessonInfo(): void { this.learnerHasViewedLessonInfoTooltip = true; if (this.userIsLoggedIn) { - this.editableExplorationBackendApiService - .recordLearnerHasViewedLessonInfoModalOnce(); + this.editableExplorationBackendApiService.recordLearnerHasViewedLessonInfoModalOnce(); } } @@ -435,5 +443,9 @@ export class ExplorationFooterComponent { } } -angular.module('oppia').directive('oppiaExplorationFooter', - downgradeComponent({ component: ExplorationFooterComponent })); +angular + .module('oppia') + .directive( + 'oppiaExplorationFooter', + downgradeComponent({component: ExplorationFooterComponent}) + ); diff --git a/core/templates/pages/exploration-player-page/layout-directives/feedback-popup.component.spec.ts b/core/templates/pages/exploration-player-page/layout-directives/feedback-popup.component.spec.ts index c5b6fadc6ad0..eeaf79a53af2 100644 --- a/core/templates/pages/exploration-player-page/layout-directives/feedback-popup.component.spec.ts +++ b/core/templates/pages/exploration-player-page/layout-directives/feedback-popup.component.spec.ts @@ -16,19 +16,25 @@ * @fileoverview Unit tests for FeedbackPopupComponent */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; -import { MockTranslateService } from 'components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { FeedbackPopupComponent } from './feedback-popup.component'; -import { UserService } from 'services/user.service'; -import { UserInfo } from 'domain/user/user-info.model'; -import { PlayerPositionService } from '../services/player-position.service'; -import { BackgroundMaskService } from 'services/stateful/background-mask.service'; -import { FeedbackPopupBackendApiService } from '../services/feedback-popup-backend-api.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + async, + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; +import {MockTranslateService} from 'components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {FeedbackPopupComponent} from './feedback-popup.component'; +import {UserService} from 'services/user.service'; +import {UserInfo} from 'domain/user/user-info.model'; +import {PlayerPositionService} from '../services/player-position.service'; +import {BackgroundMaskService} from 'services/stateful/background-mask.service'; +import {FeedbackPopupBackendApiService} from '../services/feedback-popup-backend-api.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; describe('FeedbackPopupComponent', () => { let component: FeedbackPopupComponent; @@ -41,18 +47,15 @@ describe('FeedbackPopupComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - FeedbackPopupComponent, - MockTranslatePipe - ], + declarations: [FeedbackPopupComponent, MockTranslatePipe], providers: [ BackgroundMaskService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -61,12 +64,13 @@ describe('FeedbackPopupComponent', () => { playerPositionService = TestBed.get(PlayerPositionService); windowDimensionsService = TestBed.get(WindowDimensionsService); feedbackPopupBackendApiService = TestBed.get( - FeedbackPopupBackendApiService); + FeedbackPopupBackendApiService + ); fixture = TestBed.createComponent(FeedbackPopupComponent); component = fixture.componentInstance; spyOn(userService, 'getUserInfoAsync').and.resolveTo({ - isLoggedIn: () => true + isLoggedIn: () => true, } as UserInfo); spyOn(playerPositionService, 'getCurrentStateName').and.returnValue('Hola'); spyOn(windowDimensionsService, 'isWindowNarrow').and.returnValue(true); @@ -86,14 +90,17 @@ describe('FeedbackPopupComponent', () => { expect(component.isLoggedIn).toBe(true); expect(component.feedbackPopoverId).toContain('feedbackPopover'); - expect(component.feedbackTitle) - .toBe('Feedback when the user was at card "Hola"'); + expect(component.feedbackTitle).toBe( + 'Feedback when the user was at card "Hola"' + ); })); it('should save feedback submitted by user', fakeAsync(() => { component.feedbackText = 'Nice exploration!'; - spyOn(feedbackPopupBackendApiService, 'submitFeedbackAsync') - .and.resolveTo(); + spyOn( + feedbackPopupBackendApiService, + 'submitFeedbackAsync' + ).and.resolveTo(); spyOn(component.closePopover, 'emit'); expect(component.feedbackSubmitted).toBe(false); diff --git a/core/templates/pages/exploration-player-page/layout-directives/feedback-popup.component.ts b/core/templates/pages/exploration-player-page/layout-directives/feedback-popup.component.ts index b35c898b8222..b8c55211bd17 100644 --- a/core/templates/pages/exploration-player-page/layout-directives/feedback-popup.component.ts +++ b/core/templates/pages/exploration-player-page/layout-directives/feedback-popup.component.ts @@ -16,18 +16,18 @@ * @fileoverview Component for the feedback popup. */ -import { Component, Output, EventEmitter } from '@angular/core'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { BackgroundMaskService } from 'services/stateful/background-mask.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { UserService } from 'services/user.service'; -import { FeedbackPopupBackendApiService } from '../services/feedback-popup-backend-api.service'; -import { PlayerPositionService } from '../services/player-position.service'; -import { AppConstants } from 'app.constants'; +import {Component, Output, EventEmitter} from '@angular/core'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {BackgroundMaskService} from 'services/stateful/background-mask.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {UserService} from 'services/user.service'; +import {FeedbackPopupBackendApiService} from '../services/feedback-popup-backend-api.service'; +import {PlayerPositionService} from '../services/player-position.service'; +import {AppConstants} from 'app.constants'; @Component({ selector: 'oppia-feedback-popup', - templateUrl: './feedback-popup.component.html' + templateUrl: './feedback-popup.component.html', }) export class FeedbackPopupComponent { // These properties below are initialized using Angular lifecycle hooks @@ -53,14 +53,15 @@ export class FeedbackPopupComponent { ) {} ngOnInit(): void { - this.userService.getUserInfoAsync().then((userInfo) => { + this.userService.getUserInfoAsync().then(userInfo => { this.isLoggedIn = userInfo.isLoggedIn(); }); - this.feedbackPopoverId = ( - 'feedbackPopover' + Math.random().toString(36).slice(2)); - this.feedbackTitle = ( + this.feedbackPopoverId = + 'feedbackPopover' + Math.random().toString(36).slice(2); + this.feedbackTitle = 'Feedback when the user was at card "' + - this.playerPositionService.getCurrentStateName() + '"'); + this.playerPositionService.getCurrentStateName() + + '"'; if (this.windowDimensionsService.isWindowNarrow()) { this.backgroundMaskService.activateMask(); @@ -71,17 +72,19 @@ export class FeedbackPopupComponent { saveFeedback(): void { if (this.feedbackText) { - this.feedbackPopupBackendApiService.submitFeedbackAsync( - this.feedbackTitle, - this.feedbackText, - !this.isSubmitterAnonymized && this.isLoggedIn, - this.playerPositionService.getCurrentStateName() - ).then(() => { - this.feedbackSubmitted = true; - setTimeout(() => { - this.close(); - }, 2000); - }); + this.feedbackPopupBackendApiService + .submitFeedbackAsync( + this.feedbackTitle, + this.feedbackText, + !this.isSubmitterAnonymized && this.isLoggedIn, + this.playerPositionService.getCurrentStateName() + ) + .then(() => { + this.feedbackSubmitted = true; + setTimeout(() => { + this.close(); + }, 2000); + }); } } diff --git a/core/templates/pages/exploration-player-page/layout-directives/learner-local-nav.component.spec.ts b/core/templates/pages/exploration-player-page/layout-directives/learner-local-nav.component.spec.ts index 3fded4113fc3..2e61dccfed0f 100644 --- a/core/templates/pages/exploration-player-page/layout-directives/learner-local-nav.component.spec.ts +++ b/core/templates/pages/exploration-player-page/layout-directives/learner-local-nav.component.spec.ts @@ -16,38 +16,44 @@ * @fileoverview Tests for Learner Local Nav Component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { NgbModal, NgbModalRef, NgbPopover } from '@ng-bootstrap/ng-bootstrap'; -import { TranslateService } from '@ngx-translate/core'; -import { MockTranslateService } from 'components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; - -import { AppConstants } from 'app.constants'; -import { ReadOnlyExplorationBackendApiService } from 'domain/exploration/read-only-exploration-backend-api.service'; -import { AlertsService } from 'services/alerts.service'; -import { AttributionService } from 'services/attribution.service'; -import { LoaderService } from 'services/loader.service'; -import { UserService } from 'services/user.service'; -import { ExplorationEngineService } from '../services/exploration-engine.service'; -import { LearnerLocalNavBackendApiService } from '../services/learner-local-nav-backend-api.service'; - -import { LearnerLocalNavComponent } from './learner-local-nav.component'; -import { FlagExplorationModalComponent } from '../modals/flag-exploration-modal.component'; -import { UserInfo } from 'domain/user/user-info.model'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {NgbModal, NgbModalRef, NgbPopover} from '@ng-bootstrap/ng-bootstrap'; +import {TranslateService} from '@ngx-translate/core'; +import {MockTranslateService} from 'components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; + +import {AppConstants} from 'app.constants'; +import {ReadOnlyExplorationBackendApiService} from 'domain/exploration/read-only-exploration-backend-api.service'; +import {AlertsService} from 'services/alerts.service'; +import {AttributionService} from 'services/attribution.service'; +import {LoaderService} from 'services/loader.service'; +import {UserService} from 'services/user.service'; +import {ExplorationEngineService} from '../services/exploration-engine.service'; +import {LearnerLocalNavBackendApiService} from '../services/learner-local-nav-backend-api.service'; + +import {LearnerLocalNavComponent} from './learner-local-nav.component'; +import {FlagExplorationModalComponent} from '../modals/flag-exploration-modal.component'; +import {UserInfo} from 'domain/user/user-info.model'; describe('Learner Local Nav Component ', () => { let component: LearnerLocalNavComponent; let fixture: ComponentFixture; let ngbModal: NgbModal; let attributionService: AttributionService; - let readOnlyExplorationBackendApiService: - ReadOnlyExplorationBackendApiService; + let readOnlyExplorationBackendApiService: ReadOnlyExplorationBackendApiService; let userService: UserService; - const MockNgbPopover = jasmine.createSpyObj( - 'NgbPopover', ['close', 'toggle']); + const MockNgbPopover = jasmine.createSpyObj('NgbPopover', [ + 'close', + 'toggle', + ]); const explorationBackendResponse = { can_edit: true, @@ -61,7 +67,7 @@ describe('Learner Local Nav Component ', () => { title: '', language_code: '', objective: '', - next_content_id_index: 0 + next_content_id_index: 0, }, exploration_metadata: { title: '', @@ -76,7 +82,7 @@ describe('Learner Local Nav Component ', () => { param_specs: {}, param_changes: [], auto_tts_enabled: false, - edits_allowed: true + edits_allowed: true, }, exploration_id: 'test_id', is_logged_in: true, @@ -90,7 +96,7 @@ describe('Learner Local Nav Component ', () => { furthest_reached_checkpoint_exp_version: 1, furthest_reached_checkpoint_state_name: 'State B', most_recently_reached_checkpoint_state_name: 'State A', - most_recently_reached_checkpoint_exp_version: 1 + most_recently_reached_checkpoint_exp_version: 1, }; const userInfoForCollectionCreator = UserInfo.createDefault(); @@ -98,11 +104,7 @@ describe('Learner Local Nav Component ', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - LearnerLocalNavComponent, - MockTranslatePipe, - NgbPopover - ], + declarations: [LearnerLocalNavComponent, MockTranslatePipe, NgbPopover], providers: [ AlertsService, AttributionService, @@ -113,10 +115,10 @@ describe('Learner Local Nav Component ', () => { LearnerLocalNavBackendApiService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(LearnerLocalNavComponent); @@ -127,7 +129,8 @@ describe('Learner Local Nav Component ', () => { userService = TestBed.inject(UserService); ngbModal = TestBed.inject(NgbModal); readOnlyExplorationBackendApiService = TestBed.inject( - ReadOnlyExplorationBackendApiService); + ReadOnlyExplorationBackendApiService + ); attributionService = TestBed.inject(AttributionService); }); @@ -141,10 +144,13 @@ describe('Learner Local Nav Component ', () => { }); it('should set properties when initialized', fakeAsync(() => { - spyOn(readOnlyExplorationBackendApiService, 'loadExplorationAsync') - .and.resolveTo(explorationBackendResponse); - spyOn(userService, 'getUserInfoAsync') - .and.returnValue(Promise.resolve(userInfoForCollectionCreator)); + spyOn( + readOnlyExplorationBackendApiService, + 'loadExplorationAsync' + ).and.resolveTo(explorationBackendResponse); + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve(userInfoForCollectionCreator) + ); // This throws 'Cannot assign to 'ENABLE_EXP_FEEDBACK_FOR_LOGGED_OUT_USERS' // because it is a read-only property.'. We need to suppress this error @@ -164,20 +170,24 @@ describe('Learner Local Nav Component ', () => { expect(component.canEdit).toBe(true); })); - it('should open a modal to report exploration when ' + - 'clicking on flag button', () => { - const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ( - { componentInstance: {}, - result: Promise.resolve() - }) as NgbModalRef; - }); - - component.showFlagExplorationModal(); - - expect(modalSpy).toHaveBeenCalledWith( - FlagExplorationModalComponent, {backdrop: 'static'}); - }); + it( + 'should open a modal to report exploration when ' + + 'clicking on flag button', + () => { + const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + componentInstance: {}, + result: Promise.resolve(), + } as NgbModalRef; + }); + + component.showFlagExplorationModal(); + + expect(modalSpy).toHaveBeenCalledWith(FlagExplorationModalComponent, { + backdrop: 'static', + }); + } + ); it('should toggle feedback popover', () => { component.feedbackPopOver = MockNgbPopover; @@ -198,7 +208,9 @@ describe('Learner Local Nav Component ', () => { it('should hide attribution modal', () => { spyOn(attributionService, 'isAttributionModalShown').and.returnValue(true); const hideModalSpy = spyOn( - attributionService, 'hideAttributionModal').and.callThrough(); + attributionService, + 'hideAttributionModal' + ).and.callThrough(); component.toggleAttributionModal(); @@ -208,7 +220,9 @@ describe('Learner Local Nav Component ', () => { it('should show attribution modal', () => { spyOn(attributionService, 'isAttributionModalShown').and.returnValue(false); const showModalSpy = spyOn( - attributionService, 'showAttributionModal').and.callThrough(); + attributionService, + 'showAttributionModal' + ).and.callThrough(); component.toggleAttributionModal(); diff --git a/core/templates/pages/exploration-player-page/layout-directives/learner-local-nav.component.ts b/core/templates/pages/exploration-player-page/layout-directives/learner-local-nav.component.ts index 8ed545339467..d9eec00d810b 100644 --- a/core/templates/pages/exploration-player-page/layout-directives/learner-local-nav.component.ts +++ b/core/templates/pages/exploration-player-page/layout-directives/learner-local-nav.component.ts @@ -16,23 +16,26 @@ * @fileoverview Component for the local navigation in the learner view. */ -import { Component, OnInit, ViewChild } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbModal, NgbPopover } from '@ng-bootstrap/ng-bootstrap'; -import { AppConstants } from 'app.constants'; -import { ReadOnlyExplorationBackendApiService } from 'domain/exploration/read-only-exploration-backend-api.service'; -import { AlertsService } from 'services/alerts.service'; -import { AttributionService } from 'services/attribution.service'; -import { LoaderService } from 'services/loader.service'; -import { UserService } from 'services/user.service'; -import { ExplorationSuccessfullyFlaggedModalComponent } from '../modals/exploration-successfully-flagged-modal.component'; -import { FlagExplorationModalComponent, FlagExplorationModalResult } from '../modals/flag-exploration-modal.component'; -import { ExplorationEngineService } from '../services/exploration-engine.service'; -import { LearnerLocalNavBackendApiService } from '../services/learner-local-nav-backend-api.service'; +import {Component, OnInit, ViewChild} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbModal, NgbPopover} from '@ng-bootstrap/ng-bootstrap'; +import {AppConstants} from 'app.constants'; +import {ReadOnlyExplorationBackendApiService} from 'domain/exploration/read-only-exploration-backend-api.service'; +import {AlertsService} from 'services/alerts.service'; +import {AttributionService} from 'services/attribution.service'; +import {LoaderService} from 'services/loader.service'; +import {UserService} from 'services/user.service'; +import {ExplorationSuccessfullyFlaggedModalComponent} from '../modals/exploration-successfully-flagged-modal.component'; +import { + FlagExplorationModalComponent, + FlagExplorationModalResult, +} from '../modals/flag-exploration-modal.component'; +import {ExplorationEngineService} from '../services/exploration-engine.service'; +import {LearnerLocalNavBackendApiService} from '../services/learner-local-nav-backend-api.service'; @Component({ selector: 'oppia-learner-local-nav', - templateUrl: './learner-local-nav.component.html' + templateUrl: './learner-local-nav.component.html', }) export class LearnerLocalNavComponent implements OnInit { canEdit: boolean = false; @@ -49,35 +52,46 @@ export class LearnerLocalNavComponent implements OnInit { private attributionService: AttributionService, private explorationEngineService: ExplorationEngineService, private loaderService: LoaderService, - private readOnlyExplorationBackendApiService: - ReadOnlyExplorationBackendApiService, + private readOnlyExplorationBackendApiService: ReadOnlyExplorationBackendApiService, private userService: UserService, private learnerLocalNavBackendApiService: LearnerLocalNavBackendApiService ) {} showFlagExplorationModal(): void { - this.ngbModal.open(FlagExplorationModalComponent, { - backdrop: 'static' - }).result.then((result: FlagExplorationModalResult) => { - this.learnerLocalNavBackendApiService - .postReportAsync(this.explorationId, result).then( - () => {}, - (error) => { - this.alertsService.addWarning(error); - }); + this.ngbModal + .open(FlagExplorationModalComponent, { + backdrop: 'static', + }) + .result.then( + (result: FlagExplorationModalResult) => { + this.learnerLocalNavBackendApiService + .postReportAsync(this.explorationId, result) + .then( + () => {}, + error => { + this.alertsService.addWarning(error); + } + ); - this.ngbModal.open(ExplorationSuccessfullyFlaggedModalComponent, { - backdrop: true - }).result.then(() => {}, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + this.ngbModal + .open(ExplorationSuccessfullyFlaggedModalComponent, { + backdrop: true, + }) + .result.then( + () => {}, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } toggleAttributionModal(): void { @@ -94,12 +108,12 @@ export class LearnerLocalNavComponent implements OnInit { if (version) { this.readOnlyExplorationBackendApiService .loadExplorationAsync(this.explorationId, version) - .then((exploration) => { + .then(exploration => { this.canEdit = exploration.can_edit; }); } this.loaderService.showLoadingScreen('Loading'); - this.userService.getUserInfoAsync().then((userInfo) => { + this.userService.getUserInfoAsync().then(userInfo => { this.username = userInfo.getUsername(); if ( this.username === null && @@ -120,7 +134,9 @@ export class LearnerLocalNavComponent implements OnInit { } } -angular.module('oppia').directive('oppiaLearnerLocalNav', +angular.module('oppia').directive( + 'oppiaLearnerLocalNav', downgradeComponent({ - component: LearnerLocalNavComponent - }) as angular.IDirectiveFactory); + component: LearnerLocalNavComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-player-page/layout-directives/learner-view-info.component.spec.ts b/core/templates/pages/exploration-player-page/layout-directives/learner-view-info.component.spec.ts index d0d63ed912c4..82f3baca0cc9 100644 --- a/core/templates/pages/exploration-player-page/layout-directives/learner-view-info.component.spec.ts +++ b/core/templates/pages/exploration-player-page/layout-directives/learner-view-info.component.spec.ts @@ -16,27 +16,38 @@ * @fileoverview Unit tests for learner view info component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { FetchExplorationBackendResponse, ReadOnlyExplorationBackendApiService } from 'domain/exploration/read-only-exploration-backend-api.service'; -import { ReadOnlyTopicBackendDict, ReadOnlyTopicObjectFactory } from 'domain/topic_viewer/read-only-topic-object.factory'; -import { TopicViewerBackendApiService } from 'domain/topic_viewer/topic-viewer-backend-api.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { ContextService } from 'services/context.service'; -import { UrlService } from 'services/contextual/url.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { StatsReportingService } from '../services/stats-reporting.service'; -import { LearnerViewInfoComponent } from './learner-view-info.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import { + FetchExplorationBackendResponse, + ReadOnlyExplorationBackendApiService, +} from 'domain/exploration/read-only-exploration-backend-api.service'; +import { + ReadOnlyTopicBackendDict, + ReadOnlyTopicObjectFactory, +} from 'domain/topic_viewer/read-only-topic-object.factory'; +import {TopicViewerBackendApiService} from 'domain/topic_viewer/topic-viewer-backend-api.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {ContextService} from 'services/context.service'; +import {UrlService} from 'services/contextual/url.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {StatsReportingService} from '../services/stats-reporting.service'; +import {LearnerViewInfoComponent} from './learner-view-info.component'; describe('Learner view info component', () => { let fixture: ComponentFixture; let componentInstance: LearnerViewInfoComponent; let contextService: ContextService; - let readOnlyExplorationBackendApiService: - ReadOnlyExplorationBackendApiService; + let readOnlyExplorationBackendApiService: ReadOnlyExplorationBackendApiService; let siteAnalyticsService: SiteAnalyticsService; let statsReportingService: StatsReportingService; let urlInterpolationService: UrlInterpolationService; @@ -47,13 +58,8 @@ describe('Learner view info component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - LearnerViewInfoComponent, - MockTranslatePipe - ], + imports: [HttpClientTestingModule], + declarations: [LearnerViewInfoComponent, MockTranslatePipe], providers: [ ContextService, ReadOnlyExplorationBackendApiService, @@ -62,9 +68,9 @@ describe('Learner view info component', () => { UrlInterpolationService, UrlService, TopicViewerBackendApiService, - ReadOnlyTopicObjectFactory + ReadOnlyTopicObjectFactory, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -73,15 +79,15 @@ describe('Learner view info component', () => { componentInstance = fixture.componentInstance; contextService = TestBed.inject(ContextService); readOnlyExplorationBackendApiService = TestBed.inject( - ReadOnlyExplorationBackendApiService); + ReadOnlyExplorationBackendApiService + ); siteAnalyticsService = TestBed.inject(SiteAnalyticsService); statsReportingService = TestBed.inject(StatsReportingService); urlInterpolationService = TestBed.inject(UrlInterpolationService); urlService = TestBed.inject(UrlService); i18nLanguageCodeService = TestBed.inject(I18nLanguageCodeService); readOnlyTopicObjectFactory = TestBed.inject(ReadOnlyTopicObjectFactory); - topicViewerBackendApiService = TestBed.inject( - TopicViewerBackendApiService); + topicViewerBackendApiService = TestBed.inject(TopicViewerBackendApiService); spyOn(topicViewerBackendApiService, 'fetchTopicDataAsync').and.resolveTo( readOnlyTopicObjectFactory.createFromBackendDict({ @@ -101,7 +107,8 @@ describe('Learner view info component', () => { ); spyOn(i18nLanguageCodeService, 'isCurrentLanguageRTL').and.returnValue( - true); + true + ); }); afterEach(() => { @@ -115,17 +122,22 @@ describe('Learner view info component', () => { spyOn(urlService, 'getPathname').and.returnValue('/explore/'); spyOn(contextService, 'getExplorationId').and.returnValue(explorationId); - spyOn(readOnlyExplorationBackendApiService, 'fetchExplorationAsync') - .and.returnValue(Promise.resolve({ + spyOn( + readOnlyExplorationBackendApiService, + 'fetchExplorationAsync' + ).and.returnValue( + Promise.resolve({ exploration: { - title: explorationTitle - } - } as FetchExplorationBackendResponse)); + title: explorationTitle, + }, + } as FetchExplorationBackendResponse) + ); spyOn(urlService, 'getExplorationVersionFromUrl').and.returnValue(1); spyOn(componentInstance, 'getTopicUrl').and.returnValue(topicUrl); spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue(''); - spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl') - .and.returnValue(''); + spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl').and.returnValue( + '' + ); spyOn(statsReportingService, 'setTopicName'); spyOn(siteAnalyticsService, 'registerCuratedLessonStarted'); @@ -135,15 +147,17 @@ describe('Learner view info component', () => { expect(urlService.getPathname).toHaveBeenCalled(); expect(contextService.getExplorationId).toHaveBeenCalled(); - expect(readOnlyExplorationBackendApiService.fetchExplorationAsync) - .toHaveBeenCalled(); + expect( + readOnlyExplorationBackendApiService.fetchExplorationAsync + ).toHaveBeenCalled(); expect(urlService.getExplorationVersionFromUrl).toHaveBeenCalled(); expect(componentInstance.getTopicUrl).toHaveBeenCalled(); expect(urlService.getTopicUrlFragmentFromLearnerUrl).toHaveBeenCalled(); expect(topicViewerBackendApiService.fetchTopicDataAsync).toHaveBeenCalled(); expect(statsReportingService.setTopicName).toHaveBeenCalled(); - expect(siteAnalyticsService.registerCuratedLessonStarted) - .toHaveBeenCalled(); + expect( + siteAnalyticsService.registerCuratedLessonStarted + ).toHaveBeenCalled(); })); it('should register community lesson start event', fakeAsync(() => { @@ -152,17 +166,22 @@ describe('Learner view info component', () => { spyOn(urlService, 'getPathname').and.returnValue('/explore/'); spyOn(contextService, 'getExplorationId').and.returnValue(explorationId); - spyOn(readOnlyExplorationBackendApiService, 'fetchExplorationAsync') - .and.returnValue(Promise.resolve({ + spyOn( + readOnlyExplorationBackendApiService, + 'fetchExplorationAsync' + ).and.returnValue( + Promise.resolve({ exploration: { - title: explorationTitle - } - } as FetchExplorationBackendResponse)); + title: explorationTitle, + }, + } as FetchExplorationBackendResponse) + ); spyOn(urlService, 'getExplorationVersionFromUrl').and.returnValue(1); spyOn(componentInstance, 'getTopicUrl').and.returnValue(''); spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue(''); - spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl') - .and.returnValue(''); + spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl').and.returnValue( + '' + ); spyOn(statsReportingService, 'setTopicName'); spyOn(siteAnalyticsService, 'registerCommunityLessonStarted'); @@ -170,52 +189,66 @@ describe('Learner view info component', () => { tick(); tick(); - expect(siteAnalyticsService.registerCommunityLessonStarted) - .toHaveBeenCalled(); + expect( + siteAnalyticsService.registerCommunityLessonStarted + ).toHaveBeenCalled(); })); it('should get topic url from fragment correctly', () => { let topicUrl = 'topic_url'; spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue( - 'topic_url_fragment'); + 'topic_url_fragment' + ); spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl').and.returnValue( - 'classroom_url_fragment'); + 'classroom_url_fragment' + ); spyOn(urlInterpolationService, 'interpolateUrl').and.returnValue(topicUrl); expect(componentInstance.getTopicUrl()).toEqual(topicUrl); }); - it('should set topic name and subtopic title translation key and ' + - 'check whether hacky translations are displayed or not correctly', - waitForAsync(() => { - spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl') - .and.returnValue('topic_url_fragment'); - spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl') - .and.returnValue('classroom_url_fragment'); - spyOn(componentInstance, 'getTopicUrl').and.returnValue('topic_url'); - - componentInstance.ngOnInit(); - fixture.whenStable().then(() => { - fixture.detectChanges(); - - expect(componentInstance.topicNameTranslationKey) - .toBe('I18N_TOPIC_topic1_TITLE'); - expect(componentInstance.explorationTitleTranslationKey) - .toBe('I18N_EXPLORATION_test_id_TITLE'); - - spyOn(i18nLanguageCodeService, 'isHackyTranslationAvailable') - .and.returnValues(true, false); - spyOn(i18nLanguageCodeService, 'isCurrentLanguageEnglish') - .and.returnValues(false, false); - - let hackyTopicNameTranslationIsDisplayed = - componentInstance.isHackyTopicNameTranslationDisplayed(); - expect(hackyTopicNameTranslationIsDisplayed).toBe(true); - - let hackyExpTitleTranslationIsDisplayed = - componentInstance.isHackyExpTitleTranslationDisplayed(); - expect(hackyExpTitleTranslationIsDisplayed).toBe(false); - }); - })); + it( + 'should set topic name and subtopic title translation key and ' + + 'check whether hacky translations are displayed or not correctly', + waitForAsync(() => { + spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue( + 'topic_url_fragment' + ); + spyOn( + urlService, + 'getClassroomUrlFragmentFromLearnerUrl' + ).and.returnValue('classroom_url_fragment'); + spyOn(componentInstance, 'getTopicUrl').and.returnValue('topic_url'); + + componentInstance.ngOnInit(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + + expect(componentInstance.topicNameTranslationKey).toBe( + 'I18N_TOPIC_topic1_TITLE' + ); + expect(componentInstance.explorationTitleTranslationKey).toBe( + 'I18N_EXPLORATION_test_id_TITLE' + ); + + spyOn( + i18nLanguageCodeService, + 'isHackyTranslationAvailable' + ).and.returnValues(true, false); + spyOn( + i18nLanguageCodeService, + 'isCurrentLanguageEnglish' + ).and.returnValues(false, false); + + let hackyTopicNameTranslationIsDisplayed = + componentInstance.isHackyTopicNameTranslationDisplayed(); + expect(hackyTopicNameTranslationIsDisplayed).toBe(true); + + let hackyExpTitleTranslationIsDisplayed = + componentInstance.isHackyExpTitleTranslationDisplayed(); + expect(hackyExpTitleTranslationIsDisplayed).toBe(false); + }); + }) + ); }); diff --git a/core/templates/pages/exploration-player-page/layout-directives/learner-view-info.component.ts b/core/templates/pages/exploration-player-page/layout-directives/learner-view-info.component.ts index 904c75bf9726..2393115b4a0c 100644 --- a/core/templates/pages/exploration-player-page/layout-directives/learner-view-info.component.ts +++ b/core/templates/pages/exploration-player-page/layout-directives/learner-view-info.component.ts @@ -17,29 +17,31 @@ * footer. */ -import { Component } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { ClassroomDomainConstants } from 'domain/classroom/classroom-domain.constants'; -import { ReadOnlyExplorationBackendApiService } from 'domain/exploration/read-only-exploration-backend-api.service'; -import { StoryPlaythrough } from 'domain/story_viewer/story-playthrough.model'; -import { LearnerExplorationSummaryBackendDict } from 'domain/summary/learner-exploration-summary.model'; -import { ReadOnlyTopic } from 'domain/topic_viewer/read-only-topic-object.factory'; -import { TopicViewerBackendApiService } from 'domain/topic_viewer/topic-viewer-backend-api.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { Subscription } from 'rxjs'; -import { ContextService } from 'services/context.service'; -import { UrlService } from 'services/contextual/url.service'; -import { I18nLanguageCodeService, TranslationKeyType } from 'services/i18n-language-code.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { StatsReportingService } from '../services/stats-reporting.service'; +import {Component} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {ClassroomDomainConstants} from 'domain/classroom/classroom-domain.constants'; +import {ReadOnlyExplorationBackendApiService} from 'domain/exploration/read-only-exploration-backend-api.service'; +import {StoryPlaythrough} from 'domain/story_viewer/story-playthrough.model'; +import {LearnerExplorationSummaryBackendDict} from 'domain/summary/learner-exploration-summary.model'; +import {ReadOnlyTopic} from 'domain/topic_viewer/read-only-topic-object.factory'; +import {TopicViewerBackendApiService} from 'domain/topic_viewer/topic-viewer-backend-api.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {Subscription} from 'rxjs'; +import {ContextService} from 'services/context.service'; +import {UrlService} from 'services/contextual/url.service'; +import { + I18nLanguageCodeService, + TranslationKeyType, +} from 'services/i18n-language-code.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {StatsReportingService} from '../services/stats-reporting.service'; import './learner-view-info.component.css'; - @Component({ selector: 'oppia-learner-view-info', templateUrl: './learner-view-info.component.html', - styleUrls: ['./learner-view-info.component.css'] + styleUrls: ['./learner-view-info.component.css'], }) export class LearnerViewInfoComponent { // These properties are initialized using Angular lifecycle hooks @@ -57,8 +59,7 @@ export class LearnerViewInfoComponent { constructor( private contextService: ContextService, - private readOnlyExplorationBackendApiService: - ReadOnlyExplorationBackendApiService, + private readOnlyExplorationBackendApiService: ReadOnlyExplorationBackendApiService, private siteAnalyticsService: SiteAnalyticsService, private statsReportingService: StatsReportingService, private urlInterpolationService: UrlInterpolationService, @@ -72,59 +73,64 @@ export class LearnerViewInfoComponent { let explorationContext = false; for (let i = 0; i < pathnameArray.length; i++) { - if (pathnameArray[i] === 'explore' || - pathnameArray[i] === 'create' || - pathnameArray[i] === 'skill_editor' || - pathnameArray[i] === 'embed' || - pathnameArray[i] === 'lesson') { + if ( + pathnameArray[i] === 'explore' || + pathnameArray[i] === 'create' || + pathnameArray[i] === 'skill_editor' || + pathnameArray[i] === 'embed' || + pathnameArray[i] === 'lesson' + ) { explorationContext = true; break; } } - this.explorationId = explorationContext ? - this.contextService.getExplorationId() : 'test_id'; + this.explorationId = explorationContext + ? this.contextService.getExplorationId() + : 'test_id'; this.explorationTitle = 'Loading...'; - this.readOnlyExplorationBackendApiService.fetchExplorationAsync( - this.explorationId, - this.urlService.getExplorationVersionFromUrl(), - this.urlService.getPidFromUrl()) - .then((response) => { + this.readOnlyExplorationBackendApiService + .fetchExplorationAsync( + this.explorationId, + this.urlService.getExplorationVersionFromUrl(), + this.urlService.getPidFromUrl() + ) + .then(response => { this.explorationTitle = response.exploration.title; }); - this.explorationTitleTranslationKey = ( + this.explorationTitleTranslationKey = this.i18nLanguageCodeService.getExplorationTranslationKey( this.explorationId, TranslationKeyType.TITLE - ) - ); + ); // To check if the exploration is linked to the topic or not. this.isLinkedToTopic = this.getTopicUrl() ? true : false; // If linked to topic then print topic name in the lesson player. if (this.isLinkedToTopic) { - let topicUrlFragment = ( - this.urlService.getTopicUrlFragmentFromLearnerUrl()); - let classroomUrlFragment = ( - this.urlService.getClassroomUrlFragmentFromLearnerUrl()); - this.topicViewerBackendApiService.fetchTopicDataAsync( - topicUrlFragment, classroomUrlFragment).then( - (readOnlyTopic: ReadOnlyTopic) => { + let topicUrlFragment = + this.urlService.getTopicUrlFragmentFromLearnerUrl(); + let classroomUrlFragment = + this.urlService.getClassroomUrlFragmentFromLearnerUrl(); + this.topicViewerBackendApiService + .fetchTopicDataAsync(topicUrlFragment, classroomUrlFragment) + .then((readOnlyTopic: ReadOnlyTopic) => { this.topicName = readOnlyTopic.getTopicName(); this.statsReportingService.setTopicName(this.topicName); this.siteAnalyticsService.registerCuratedLessonStarted( - this.topicName, this.explorationId); - this.topicNameTranslationKey = ( + this.topicName, + this.explorationId + ); + this.topicNameTranslationKey = this.i18nLanguageCodeService.getTopicTranslationKey( readOnlyTopic.getTopicId(), TranslationKeyType.TITLE - ) - ); - } - ); + ); + }); } else { this.siteAnalyticsService.registerCommunityLessonStarted( - this.explorationId); + this.explorationId + ); } } @@ -135,19 +141,22 @@ export class LearnerViewInfoComponent { let classroomUrlFragment: string | null = null; try { - topicUrlFragment = ( - this.urlService.getTopicUrlFragmentFromLearnerUrl()); - classroomUrlFragment = ( - this.urlService.getClassroomUrlFragmentFromLearnerUrl()); + topicUrlFragment = this.urlService.getTopicUrlFragmentFromLearnerUrl(); + classroomUrlFragment = + this.urlService.getClassroomUrlFragmentFromLearnerUrl(); } catch (e) {} - return topicUrlFragment && + return ( + topicUrlFragment && classroomUrlFragment && this.urlInterpolationService.interpolateUrl( - ClassroomDomainConstants.TOPIC_VIEWER_STORY_URL_TEMPLATE, { + ClassroomDomainConstants.TOPIC_VIEWER_STORY_URL_TEMPLATE, + { topic_url_fragment: topicUrlFragment, classroom_url_fragment: classroomUrlFragment, - }); + } + ) + ); } isHackyTopicNameTranslationDisplayed(): boolean { @@ -171,7 +180,9 @@ export class LearnerViewInfoComponent { } } -angular.module('oppia').directive('oppiaLearnerViewInfo', +angular.module('oppia').directive( + 'oppiaLearnerViewInfo', downgradeComponent({ - component: LearnerViewInfoComponent - }) as angular.IDirectiveFactory); + component: LearnerViewInfoComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-player-page/layout-directives/progress-nav.component.spec.ts b/core/templates/pages/exploration-player-page/layout-directives/progress-nav.component.spec.ts index 841bb2da0e9d..847b51ab4811 100644 --- a/core/templates/pages/exploration-player-page/layout-directives/progress-nav.component.spec.ts +++ b/core/templates/pages/exploration-player-page/layout-directives/progress-nav.component.spec.ts @@ -16,28 +16,37 @@ * @fileoverview Unit tests for progress nav component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; -import { MockTranslateService } from 'components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec'; -import { StateCard } from 'domain/state_card/state-card.model'; -import { UrlService } from 'services/contextual/url.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { ExplorationPlayerConstants } from '../exploration-player-page.constants'; -import { ExplorationEngineService } from '../services/exploration-engine.service'; -import { ExplorationPlayerStateService } from '../services/exploration-player-state.service'; -import { HelpCardEventResponse, PlayerPositionService } from '../services/player-position.service'; -import { PlayerTranscriptService } from '../services/player-transcript.service'; -import { ProgressNavComponent } from './progress-nav.component'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { SchemaFormSubmittedService } from 'services/schema-form-submitted.service'; -import { ContentTranslationManagerService } from '../services/content-translation-manager.service'; -import { Interaction } from 'domain/exploration/InteractionObjectFactory'; -import { RecordedVoiceovers } from 'domain/exploration/recorded-voiceovers.model'; -import { AudioTranslationLanguageService } from '../services/audio-translation-language.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; +import {MockTranslateService} from 'components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec'; +import {StateCard} from 'domain/state_card/state-card.model'; +import {UrlService} from 'services/contextual/url.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {ExplorationPlayerConstants} from '../exploration-player-page.constants'; +import {ExplorationEngineService} from '../services/exploration-engine.service'; +import {ExplorationPlayerStateService} from '../services/exploration-player-state.service'; +import { + HelpCardEventResponse, + PlayerPositionService, +} from '../services/player-position.service'; +import {PlayerTranscriptService} from '../services/player-transcript.service'; +import {ProgressNavComponent} from './progress-nav.component'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {SchemaFormSubmittedService} from 'services/schema-form-submitted.service'; +import {ContentTranslationManagerService} from '../services/content-translation-manager.service'; +import {Interaction} from 'domain/exploration/InteractionObjectFactory'; +import {RecordedVoiceovers} from 'domain/exploration/recorded-voiceovers.model'; +import {AudioTranslationLanguageService} from '../services/audio-translation-language.service'; describe('Progress nav component', () => { let fixture: ComponentFixture; @@ -53,21 +62,30 @@ describe('Progress nav component', () => { let schemaFormSubmittedService: SchemaFormSubmittedService; let contentTranslationManagerService: ContentTranslationManagerService; let mockDisplayedCard = new StateCard( - '', '', '', {} as Interaction, [], - {} as RecordedVoiceovers, '', {} as AudioTranslationLanguageService); + '', + '', + '', + {} as Interaction, + [], + {} as RecordedVoiceovers, + '', + {} as AudioTranslationLanguageService + ); let mockDisplayedCard2 = new StateCard( - 'state', 'name', 'html', {} as Interaction, [], - {} as RecordedVoiceovers, '', {} as AudioTranslationLanguageService); + 'state', + 'name', + 'html', + {} as Interaction, + [], + {} as RecordedVoiceovers, + '', + {} as AudioTranslationLanguageService + ); beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - ProgressNavComponent, - MockTranslatePipe - ], + imports: [HttpClientTestingModule], + declarations: [ProgressNavComponent, MockTranslatePipe], providers: [ ExplorationEngineService, ExplorationPlayerStateService, @@ -79,10 +97,10 @@ describe('Progress nav component', () => { SchemaFormSubmittedService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -92,17 +110,20 @@ describe('Progress nav component', () => { urlService = TestBed.inject(UrlService); playerPositionService = TestBed.inject(PlayerPositionService); explorationPlayerStateService = TestBed.inject( - ExplorationPlayerStateService); + ExplorationPlayerStateService + ); focusManagerService = TestBed.inject(FocusManagerService); playerTranscriptService = TestBed.inject(PlayerTranscriptService); windowDimensionsService = TestBed.inject(WindowDimensionsService); i18nLanguageCodeService = TestBed.inject(I18nLanguageCodeService); schemaFormSubmittedService = TestBed.inject(SchemaFormSubmittedService); contentTranslationManagerService = TestBed.inject( - ContentTranslationManagerService); + ContentTranslationManagerService + ); spyOn(i18nLanguageCodeService, 'isCurrentLanguageRTL').and.returnValue( - true); + true + ); }); afterEach(() => { @@ -111,21 +132,24 @@ describe('Progress nav component', () => { it('should initialize', fakeAsync(() => { let isIframed = true; - let mockOnHelpCardAvailableEventEmitter = ( - new EventEmitter()); + let mockOnHelpCardAvailableEventEmitter = + new EventEmitter(); let mockSchemaFormSubmittedEventEmitter = new EventEmitter(); spyOn(urlService, 'isIframed').and.returnValue(isIframed); spyOn(componentInstance.submit, 'emit'); - spyOnProperty(playerPositionService, 'onHelpCardAvailable') - .and.returnValue(mockOnHelpCardAvailableEventEmitter); + spyOnProperty(playerPositionService, 'onHelpCardAvailable').and.returnValue( + mockOnHelpCardAvailableEventEmitter + ); spyOn(playerPositionService, 'getDisplayedCardIndex').and.returnValue(0); - spyOnProperty(schemaFormSubmittedService, 'onSubmittedSchemaBasedForm') - .and.returnValue(mockSchemaFormSubmittedEventEmitter); + spyOnProperty( + schemaFormSubmittedService, + 'onSubmittedSchemaBasedForm' + ).and.returnValue(mockSchemaFormSubmittedEventEmitter); componentInstance.ngOnInit(); mockOnHelpCardAvailableEventEmitter.emit({ - hasContinueButton: true + hasContinueButton: true, } as HelpCardEventResponse); mockSchemaFormSubmittedEventEmitter.emit(); tick(); @@ -140,12 +164,15 @@ describe('Progress nav component', () => { let displayedCardIndex = 0; spyOn(playerTranscriptService, 'getNumCards').and.returnValue( - transcriptLength); + transcriptLength + ); spyOn(playerPositionService, 'getDisplayedCardIndex').and.returnValue( - displayedCardIndex); + displayedCardIndex + ); spyOn(playerTranscriptService, 'isLastCard').and.returnValue(true); - spyOn(explorationPlayerStateService, 'isInQuestionMode') - .and.returnValue(true); + spyOn(explorationPlayerStateService, 'isInQuestionMode').and.returnValue( + true + ); spyOn(focusManagerService, 'setFocusWithoutScroll'); componentInstance.displayedCard = mockDisplayedCard; @@ -159,16 +186,20 @@ describe('Progress nav component', () => { expect(playerTranscriptService.isLastCard).toHaveBeenCalled(); expect(componentInstance.helpCardHasContinueButton).toBeFalse(); expect(componentInstance.interactionIsInline).toEqual( - mockDisplayedCard.isInteractionInline()); + mockDisplayedCard.isInteractionInline() + ); expect(componentInstance.interactionCustomizationArgs).toEqual( - mockDisplayedCard.getInteractionCustomizationArgs()); + mockDisplayedCard.getInteractionCustomizationArgs() + ); })); it('should respond to state card content updates', fakeAsync(() => { let mockOnStateCardContentUpdate = new EventEmitter(); spyOn(componentInstance, 'updateDisplayedCardInfo'); - spyOnProperty(contentTranslationManagerService, 'onStateCardContentUpdate') - .and.returnValue(mockOnStateCardContentUpdate); + spyOnProperty( + contentTranslationManagerService, + 'onStateCardContentUpdate' + ).and.returnValue(mockOnStateCardContentUpdate); componentInstance.ngOnInit(); tick(); @@ -182,14 +213,17 @@ describe('Progress nav component', () => { it('should tell if window can show two cards', () => { spyOn(windowDimensionsService, 'getWidth').and.returnValue( - ExplorationPlayerConstants.TWO_CARD_THRESHOLD_PX + 1); + ExplorationPlayerConstants.TWO_CARD_THRESHOLD_PX + 1 + ); expect(componentInstance.canWindowShowTwoCards()).toBeTrue(); }); it('should tell if generic submit button should be shown', () => { - spyOn(componentInstance, 'doesInteractionHaveNavSubmitButton') - .and.returnValues(false, true); + spyOn( + componentInstance, + 'doesInteractionHaveNavSubmitButton' + ).and.returnValues(false, true); spyOn(componentInstance, 'canWindowShowTwoCards').and.returnValue(false); expect(componentInstance.shouldGenericSubmitButtonBeShown()).toBeFalse(); diff --git a/core/templates/pages/exploration-player-page/layout-directives/progress-nav.component.ts b/core/templates/pages/exploration-player-page/layout-directives/progress-nav.component.ts index eb88d5f4800d..f9e848520ff5 100644 --- a/core/templates/pages/exploration-player-page/layout-directives/progress-nav.component.ts +++ b/core/templates/pages/exploration-player-page/layout-directives/progress-nav.component.ts @@ -16,26 +16,34 @@ * @fileoverview Component for navigation in the conversation skin. */ -import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { StateCard } from 'domain/state_card/state-card.model'; -import { InteractionSpecsConstants, InteractionSpecsKey } from 'pages/interaction-specs.constants'; -import { Subscription } from 'rxjs'; -import { UrlService } from 'services/contextual/url.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { ExplorationPlayerConstants } from '../exploration-player-page.constants'; -import { ExplorationPlayerStateService } from '../services/exploration-player-state.service'; -import { PlayerPositionService } from '../services/player-position.service'; -import { PlayerTranscriptService } from '../services/player-transcript.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { SchemaFormSubmittedService } from 'services/schema-form-submitted.service'; -import { animate, keyframes, style, transition, trigger } from '@angular/animations'; -import { ContentTranslationManagerService } from '../services/content-translation-manager.service'; +import {Component, EventEmitter, Input, Output} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {StateCard} from 'domain/state_card/state-card.model'; +import { + InteractionSpecsConstants, + InteractionSpecsKey, +} from 'pages/interaction-specs.constants'; +import {Subscription} from 'rxjs'; +import {UrlService} from 'services/contextual/url.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {ExplorationPlayerConstants} from '../exploration-player-page.constants'; +import {ExplorationPlayerStateService} from '../services/exploration-player-state.service'; +import {PlayerPositionService} from '../services/player-position.service'; +import {PlayerTranscriptService} from '../services/player-transcript.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {SchemaFormSubmittedService} from 'services/schema-form-submitted.service'; +import { + animate, + keyframes, + style, + transition, + trigger, +} from '@angular/animations'; +import {ContentTranslationManagerService} from '../services/content-translation-manager.service'; import './progress-nav.component.css'; -import { InteractionCustomizationArgs } from 'interactions/customization-args-defs'; - +import {InteractionCustomizationArgs} from 'interactions/customization-args-defs'; @Component({ selector: 'oppia-progress-nav', @@ -45,14 +53,14 @@ import { InteractionCustomizationArgs } from 'interactions/customization-args-de trigger('fadeInOut', [ transition('void => *', []), transition('* <=> *', [ - style({ opacity: 0 }), - animate('1s ease', keyframes([ - style({ opacity: 0 }), - style({ opacity: 1 }) - ])) - ]) - ]) - ] + style({opacity: 0}), + animate( + '1s ease', + keyframes([style({opacity: 0}), style({opacity: 1})]) + ), + ]), + ]), + ], }) export class ProgressNavComponent { // These properties are initialized using Angular lifecycle hooks @@ -76,14 +84,12 @@ export class ProgressNavComponent { explorationId!: string; newCardStateName!: string; currentCardIndex!: number; - @Output() submit: EventEmitter = ( - new EventEmitter()); + @Output() submit: EventEmitter = new EventEmitter(); - @Output() clickContinueButton: EventEmitter = ( - new EventEmitter()); + @Output() clickContinueButton: EventEmitter = new EventEmitter(); - @Output() clickContinueToReviseButton: EventEmitter = ( - new EventEmitter()); + @Output() clickContinueToReviseButton: EventEmitter = + new EventEmitter(); @Output() changeCard: EventEmitter = new EventEmitter(); @@ -92,11 +98,13 @@ export class ProgressNavComponent { directiveSubscriptions = new Subscription(); transcriptLength: number = 0; interactionIsInline: boolean = true; - CONTINUE_BUTTON_FOCUS_LABEL: string = ( - ExplorationPlayerConstants.CONTINUE_BUTTON_FOCUS_LABEL); + CONTINUE_BUTTON_FOCUS_LABEL: string = + ExplorationPlayerConstants.CONTINUE_BUTTON_FOCUS_LABEL; SHOW_SUBMIT_INTERACTIONS_ONLY_FOR_MOBILE: string[] = [ - 'ItemSelectionInput', 'MultipleChoiceInput']; + 'ItemSelectionInput', + 'MultipleChoiceInput', + ]; constructor( private explorationPlayerStateService: ExplorationPlayerStateService, @@ -107,7 +115,7 @@ export class ProgressNavComponent { private urlService: UrlService, private schemaFormSubmittedService: SchemaFormSubmittedService, private windowDimensionsService: WindowDimensionsService, - private contentTranslationManagerService: ContentTranslationManagerService, + private contentTranslationManagerService: ContentTranslationManagerService ) {} ngOnChanges(): void { @@ -121,11 +129,9 @@ export class ProgressNavComponent { this.isIframed = this.urlService.isIframed(); this.directiveSubscriptions.add( - this.playerPositionService.onHelpCardAvailable.subscribe( - (helpCard) => { - this.helpCardHasContinueButton = helpCard.hasContinueButton; - } - ) + this.playerPositionService.onHelpCardAvailable.subscribe(helpCard => { + this.helpCardHasContinueButton = helpCard.hasContinueButton; + }) ); this.directiveSubscriptions.add( this.schemaFormSubmittedService.onSubmittedSchemaBasedForm.subscribe( @@ -153,22 +159,22 @@ export class ProgressNavComponent { updateDisplayedCardInfo(): void { this.transcriptLength = this.playerTranscriptService.getNumCards(); - this.displayedCardIndex = ( - this.playerPositionService.getDisplayedCardIndex()); + this.displayedCardIndex = + this.playerPositionService.getDisplayedCardIndex(); this.hasPrevious = this.displayedCardIndex > 0; this.hasNext = !this.playerTranscriptService.isLastCard( - this.displayedCardIndex); + this.displayedCardIndex + ); this.explorationPlayerStateService.isInQuestionMode(); - this.conceptCardIsBeingShown = ( + this.conceptCardIsBeingShown = this.displayedCard.getStateName() === null && - !this.explorationPlayerStateService.isPresentingIsolatedQuestions() - ); + !this.explorationPlayerStateService.isPresentingIsolatedQuestions(); if (!this.conceptCardIsBeingShown) { this.interactionIsInline = this.displayedCard.isInteractionInline(); - this.interactionCustomizationArgs = this.displayedCard - .getInteractionCustomizationArgs(); + this.interactionCustomizationArgs = + this.displayedCard.getInteractionCustomizationArgs(); this.interactionId = this.displayedCard.getInteractionId(); if (this.interactionId === 'Continue') { @@ -189,15 +195,15 @@ export class ProgressNavComponent { Boolean(this.interactionId) && InteractionSpecsConstants.INTERACTION_SPECS[ this.interactionId as InteractionSpecsKey - ].show_generic_submit_button); - // We use unknown type because we are unsure of the type of error - // that was thrown. Since the catch block cannot identify the - // specific type of error, we are unable to further optimise the - // code by introducing more types of errors. + ].show_generic_submit_button + ); + // We use unknown type because we are unsure of the type of error + // that was thrown. Since the catch block cannot identify the + // specific type of error, we are unable to further optimise the + // code by introducing more types of errors. } catch (e: unknown) { - let additionalInfo = ( - '\nSubmit button debug logs:\ninterationId: ' + - this.interactionId); + let additionalInfo = + '\nSubmit button debug logs:\ninterationId: ' + this.interactionId; if (e instanceof Error) { e.message += additionalInfo; } @@ -216,15 +222,17 @@ export class ProgressNavComponent { // Returns whether the screen is wide enough to fit two // cards (e.g., the tutor and supplemental cards) side-by-side. canWindowShowTwoCards(): boolean { - return this.windowDimensionsService.getWidth() > - ExplorationPlayerConstants.TWO_CARD_THRESHOLD_PX; + return ( + this.windowDimensionsService.getWidth() > + ExplorationPlayerConstants.TWO_CARD_THRESHOLD_PX + ); } shouldGenericSubmitButtonBeShown(): boolean { - return (this.doesInteractionHaveNavSubmitButton() && ( - this.interactionIsInline || - !this.canWindowShowTwoCards() - )); + return ( + this.doesInteractionHaveNavSubmitButton() && + (this.interactionIsInline || !this.canWindowShowTwoCards()) + ); } shouldContinueButtonBeShown(): boolean { @@ -234,12 +242,15 @@ export class ProgressNavComponent { return Boolean( this.interactionIsInline && - this.displayedCard.isCompleted() && - this.displayedCard.getLastOppiaResponse()); + this.displayedCard.isCompleted() && + this.displayedCard.getLastOppiaResponse() + ); } } -angular.module('oppia').directive('oppiaProgressNav', +angular.module('oppia').directive( + 'oppiaProgressNav', downgradeComponent({ - component: ProgressNavComponent - }) as angular.IDirectiveFactory); + component: ProgressNavComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-player-page/learner-experience/continue-button.component.spec.ts b/core/templates/pages/exploration-player-page/learner-experience/continue-button.component.spec.ts index 5314d6547410..fc36cd32548e 100644 --- a/core/templates/pages/exploration-player-page/learner-experience/continue-button.component.spec.ts +++ b/core/templates/pages/exploration-player-page/learner-experience/continue-button.component.spec.ts @@ -16,12 +16,12 @@ * @fileoverview Unit tests for the continue button component */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; -import { ContinueButtonComponent } from './continue-button.component'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; +import {ContinueButtonComponent} from './continue-button.component'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; class MockI18nLanguageCodeService { isCurrentLanguageRTL() { @@ -35,17 +35,14 @@ describe('Continue Button Root', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - ContinueButtonComponent, - MockTranslatePipe - ], + declarations: [ContinueButtonComponent, MockTranslatePipe], providers: [ { provide: I18nLanguageCodeService, - useClass: MockI18nLanguageCodeService - } + useClass: MockI18nLanguageCodeService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); diff --git a/core/templates/pages/exploration-player-page/learner-experience/continue-button.component.ts b/core/templates/pages/exploration-player-page/learner-experience/continue-button.component.ts index 36e47fa5617b..b2f7aba97a51 100644 --- a/core/templates/pages/exploration-player-page/learner-experience/continue-button.component.ts +++ b/core/templates/pages/exploration-player-page/learner-experience/continue-button.component.ts @@ -17,13 +17,13 @@ * editor. */ -import { Component, Input, Output, EventEmitter } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; +import {Component, Input, Output, EventEmitter} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; @Component({ selector: 'oppia-continue-button', - templateUrl: './continue-button.component.html' + templateUrl: './continue-button.component.html', }) export class ContinueButtonComponent { @Input() isLearnAgainButton: boolean = false; @@ -31,16 +31,18 @@ export class ContinueButtonComponent { // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @Input() focusLabel!: string; - @Output() clickContinueButton: EventEmitter = ( - new EventEmitter()); + @Output() clickContinueButton: EventEmitter = new EventEmitter(); - constructor( - private i18nLanguageCodeService: I18nLanguageCodeService) {} + constructor(private i18nLanguageCodeService: I18nLanguageCodeService) {} isLanguageRTL(): boolean { return this.i18nLanguageCodeService.isCurrentLanguageRTL(); } } -angular.module('oppia').directive('oppiaContinueButton', - downgradeComponent({ component: ContinueButtonComponent })); +angular + .module('oppia') + .directive( + 'oppiaContinueButton', + downgradeComponent({component: ContinueButtonComponent}) + ); diff --git a/core/templates/pages/exploration-player-page/learner-experience/conversation-skin.component.spec.ts b/core/templates/pages/exploration-player-page/learner-experience/conversation-skin.component.spec.ts index 1b8f20524980..40ce288c2fa2 100644 --- a/core/templates/pages/exploration-player-page/learner-experience/conversation-skin.component.spec.ts +++ b/core/templates/pages/exploration-player-page/learner-experience/conversation-skin.component.spec.ts @@ -16,87 +16,105 @@ * @fileoverview Unit tests for Conversation skin component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync, flush } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; -import { MockTranslateService } from 'components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec'; -import { QuestionPlayerStateService } from 'components/question-directives/question-player/services/question-player-state.service'; -import { Collection } from 'domain/collection/collection.model'; -import { GuestCollectionProgressService } from 'domain/collection/guest-collection-progress.service'; -import { ReadOnlyCollectionBackendApiService } from 'domain/collection/read-only-collection-backend-api.service'; -import { Interaction, InteractionObjectFactory } from 'domain/exploration/InteractionObjectFactory'; -import { FetchExplorationBackendResponse, ReadOnlyExplorationBackendApiService } from 'domain/exploration/read-only-exploration-backend-api.service'; -import { BindableVoiceovers } from 'domain/exploration/recorded-voiceovers.model'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { ConceptCardBackendApiService } from 'domain/skill/concept-card-backend-api.service'; -import { ConceptCard } from 'domain/skill/concept-card.model'; -import { StateObjectFactory } from 'domain/state/StateObjectFactory'; -import { StateCard } from 'domain/state_card/state-card.model'; -import { ReadOnlyStoryNode } from 'domain/story_viewer/read-only-story-node.model'; -import { StoryPlaythrough } from 'domain/story_viewer/story-playthrough.model'; -import { StoryViewerBackendApiService } from 'domain/story_viewer/story-viewer-backend-api.service'; -import { ExplorationSummaryBackendApiService, ExplorationSummaryDict } from 'domain/summary/exploration-summary-backend-api.service'; -import { UserInfo } from 'domain/user/user-info.model'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { CollectionPlayerBackendApiService } from 'pages/collection-player-page/services/collection-player-backend-api.service'; -import { AlertsService } from 'services/alerts.service'; -import { AudioPlayerService } from 'services/audio-player.service'; -import { AutogeneratedAudioPlayerService } from 'services/autogenerated-audio-player.service'; -import { ContextService } from 'services/context.service'; -import { UrlService } from 'services/contextual/url.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { LoaderService } from 'services/loader.service'; -import { LocalStorageService } from 'services/local-storage.service'; -import { MessengerService } from 'services/messenger.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { UserService } from 'services/user.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { ExplorationPlayerConstants } from '../exploration-player-page.constants'; -import { AnswerClassificationService, InteractionRulesService } from '../services/answer-classification.service'; -import { ContentTranslationLanguageService } from '../services/content-translation-language.service'; -import { ContentTranslationManagerService } from '../services/content-translation-manager.service'; -import { CurrentInteractionService } from '../services/current-interaction.service'; -import { ExplorationEngineService } from '../services/exploration-engine.service'; -import { ExplorationPlayerStateService } from '../services/exploration-player-state.service'; -import { ExplorationRecommendationsService } from '../services/exploration-recommendations.service'; -import { FatigueDetectionService } from '../services/fatigue-detection.service'; -import { HintsAndSolutionManagerService } from '../services/hints-and-solution-manager.service'; -import { ImagePreloaderService } from '../services/image-preloader.service'; -import { LearnerAnswerInfoService } from '../services/learner-answer-info.service'; -import { LearnerParamsService } from '../services/learner-params.service'; -import { NumberAttemptsService } from '../services/number-attempts.service'; -import { PlayerPositionService } from '../services/player-position.service'; -import { PlayerTranscriptService } from '../services/player-transcript.service'; -import { QuestionPlayerEngineService } from '../services/question-player-engine.service'; -import { RefresherExplorationConfirmationModalService } from '../services/refresher-exploration-confirmation-modal.service'; -import { StatsReportingService } from '../services/stats-reporting.service'; -import { ConversationSkinComponent } from './conversation-skin.component'; -import { PlatformFeatureService } from 'services/platform-feature.service'; -import { LearnerDashboardBackendApiService } from 'domain/learner_dashboard/learner-dashboard-backend-api.service'; -import { EditableExplorationBackendApiService } from 'domain/exploration/editable-exploration-backend-api.service'; -import { DiagnosticTestTopicTrackerModel } from 'pages/diagnostic-test-player-page/diagnostic-test-topic-tracker.model'; -import { TranslateService } from '@ngx-translate/core'; -import { MockTranslateService } from 'components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec'; -import { AudioTranslationLanguageService } from '../services/audio-translation-language.service'; -import { ConceptCardManagerService } from '../services/concept-card-manager.service'; -import { SolutionObjectFactory } from 'domain/exploration/SolutionObjectFactory'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, + flush, +} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; +import {MockTranslateService} from 'components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec'; +import {QuestionPlayerStateService} from 'components/question-directives/question-player/services/question-player-state.service'; +import {Collection} from 'domain/collection/collection.model'; +import {GuestCollectionProgressService} from 'domain/collection/guest-collection-progress.service'; +import {ReadOnlyCollectionBackendApiService} from 'domain/collection/read-only-collection-backend-api.service'; +import { + Interaction, + InteractionObjectFactory, +} from 'domain/exploration/InteractionObjectFactory'; +import { + FetchExplorationBackendResponse, + ReadOnlyExplorationBackendApiService, +} from 'domain/exploration/read-only-exploration-backend-api.service'; +import {BindableVoiceovers} from 'domain/exploration/recorded-voiceovers.model'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {ConceptCardBackendApiService} from 'domain/skill/concept-card-backend-api.service'; +import {ConceptCard} from 'domain/skill/concept-card.model'; +import {StateObjectFactory} from 'domain/state/StateObjectFactory'; +import {StateCard} from 'domain/state_card/state-card.model'; +import {ReadOnlyStoryNode} from 'domain/story_viewer/read-only-story-node.model'; +import {StoryPlaythrough} from 'domain/story_viewer/story-playthrough.model'; +import {StoryViewerBackendApiService} from 'domain/story_viewer/story-viewer-backend-api.service'; +import { + ExplorationSummaryBackendApiService, + ExplorationSummaryDict, +} from 'domain/summary/exploration-summary-backend-api.service'; +import {UserInfo} from 'domain/user/user-info.model'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {CollectionPlayerBackendApiService} from 'pages/collection-player-page/services/collection-player-backend-api.service'; +import {AlertsService} from 'services/alerts.service'; +import {AudioPlayerService} from 'services/audio-player.service'; +import {AutogeneratedAudioPlayerService} from 'services/autogenerated-audio-player.service'; +import {ContextService} from 'services/context.service'; +import {UrlService} from 'services/contextual/url.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {LoaderService} from 'services/loader.service'; +import {LocalStorageService} from 'services/local-storage.service'; +import {MessengerService} from 'services/messenger.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {UserService} from 'services/user.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {ExplorationPlayerConstants} from '../exploration-player-page.constants'; +import { + AnswerClassificationService, + InteractionRulesService, +} from '../services/answer-classification.service'; +import {ContentTranslationLanguageService} from '../services/content-translation-language.service'; +import {ContentTranslationManagerService} from '../services/content-translation-manager.service'; +import {CurrentInteractionService} from '../services/current-interaction.service'; +import {ExplorationEngineService} from '../services/exploration-engine.service'; +import {ExplorationPlayerStateService} from '../services/exploration-player-state.service'; +import {ExplorationRecommendationsService} from '../services/exploration-recommendations.service'; +import {FatigueDetectionService} from '../services/fatigue-detection.service'; +import {HintsAndSolutionManagerService} from '../services/hints-and-solution-manager.service'; +import {ImagePreloaderService} from '../services/image-preloader.service'; +import {LearnerAnswerInfoService} from '../services/learner-answer-info.service'; +import {LearnerParamsService} from '../services/learner-params.service'; +import {NumberAttemptsService} from '../services/number-attempts.service'; +import {PlayerPositionService} from '../services/player-position.service'; +import {PlayerTranscriptService} from '../services/player-transcript.service'; +import {QuestionPlayerEngineService} from '../services/question-player-engine.service'; +import {RefresherExplorationConfirmationModalService} from '../services/refresher-exploration-confirmation-modal.service'; +import {StatsReportingService} from '../services/stats-reporting.service'; +import {ConversationSkinComponent} from './conversation-skin.component'; +import {PlatformFeatureService} from 'services/platform-feature.service'; +import {LearnerDashboardBackendApiService} from 'domain/learner_dashboard/learner-dashboard-backend-api.service'; +import {EditableExplorationBackendApiService} from 'domain/exploration/editable-exploration-backend-api.service'; +import {DiagnosticTestTopicTrackerModel} from 'pages/diagnostic-test-player-page/diagnostic-test-topic-tracker.model'; +import {TranslateService} from '@ngx-translate/core'; +import {MockTranslateService} from 'components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec'; +import {AudioTranslationLanguageService} from '../services/audio-translation-language.service'; +import {ConceptCardManagerService} from '../services/concept-card-manager.service'; +import {SolutionObjectFactory} from 'domain/exploration/SolutionObjectFactory'; class MockWindowRef { nativeWindow = { location: { pathname: '/path/name', - reload: () => {} - }, - onresize: () => { + reload: () => {}, }, + onresize: () => {}, addEventListener(event: string, callback) { callback({returnValue: null}); }, - scrollTo: (x, y) => {} + scrollTo: (x, y) => {}, }; } @@ -104,8 +122,8 @@ class MockPlatformFeatureService { get status(): object { return { EndChapterCelebration: { - isEnabled: true - } + isEnabled: true, + }, }; } } @@ -122,12 +140,10 @@ describe('Conversation skin component', () => { let contentTranslationManagerService: ContentTranslationManagerService; let contextService: ContextService; let currentInteractionService: CurrentInteractionService; - let editableExplorationBackendApiService: - EditableExplorationBackendApiService; + let editableExplorationBackendApiService: EditableExplorationBackendApiService; let explorationEngineService: ExplorationEngineService; let explorationPlayerStateService: ExplorationPlayerStateService; - let explorationRecommendationsService: - ExplorationRecommendationsService; + let explorationRecommendationsService: ExplorationRecommendationsService; let explorationSummaryBackendApiService: ExplorationSummaryBackendApiService; let fatigueDetectionService: FatigueDetectionService; let focusManagerService: FocusManagerService; @@ -148,8 +164,7 @@ describe('Conversation skin component', () => { let questionPlayerStateService: QuestionPlayerStateService; let answerClassificationService: AnswerClassificationService; let readOnlyCollectionBackendApiService: ReadOnlyCollectionBackendApiService; - let refresherExplorationConfirmationModalService: - RefresherExplorationConfirmationModalService; + let refresherExplorationConfirmationModalService: RefresherExplorationConfirmationModalService; let siteAnalyticsService: SiteAnalyticsService; let statsReportingService: StatsReportingService; let storyViewerBackendApiService: StoryViewerBackendApiService; @@ -158,8 +173,7 @@ describe('Conversation skin component', () => { let userService: UserService; let windowDimensionsService: WindowDimensionsService; let windowRef: WindowRef; - let readOnlyExplorationBackendApiService: - ReadOnlyExplorationBackendApiService; + let readOnlyExplorationBackendApiService: ReadOnlyExplorationBackendApiService; let stateObjectFactory: StateObjectFactory; let platformFeatureService: PlatformFeatureService; let translateService: TranslateService; @@ -168,11 +182,16 @@ describe('Conversation skin component', () => { let conceptCardManagerService: ConceptCardManagerService; let solutionObjectFactory: SolutionObjectFactory; - let displayedCard = new StateCard( - null, null, null, new Interaction( - [], [], null, null, [], '', null), - [], null, '', null); + null, + null, + null, + new Interaction([], [], null, null, [], '', null), + [], + null, + '', + null + ); let explorationDict = { states: { @@ -184,8 +203,8 @@ describe('Conversation skin component', () => { feedback_1: {}, rule_input_2: {}, content: {}, - default_outcome: {} - } + default_outcome: {}, + }, }, solicit_answer_details: false, interaction: { @@ -195,17 +214,17 @@ describe('Conversation skin component', () => { hints: [], customization_args: { rows: { - value: 1 + value: 1, }, placeholder: { value: { unicode_str: '', - content_id: 'ca_placeholder_0' - } + content_id: 'ca_placeholder_0', + }, }, catchMisspellings: { - value: false - } + value: false, + }, }, answer_groups: [ { @@ -215,28 +234,26 @@ describe('Conversation skin component', () => { labelled_as_correct: false, feedback: { content_id: 'feedback_1', - html: '

Good Job

' + html: '

Good Job

', }, param_changes: [], dest_if_really_stuck: null, - dest: 'Mid' + dest: 'Mid', }, training_data: [], rule_specs: [ { inputs: { x: { - normalizedStrSet: [ - 'answer' - ], - contentId: 'rule_input_2' - } + normalizedStrSet: ['answer'], + contentId: 'rule_input_2', + }, }, - rule_type: 'FuzzyEquals' - } + rule_type: 'FuzzyEquals', + }, ], - tagged_skill_misconception_id: null - } + tagged_skill_misconception_id: null, + }, ], default_outcome: { missing_prerequisite_skill_id: null, @@ -244,27 +261,27 @@ describe('Conversation skin component', () => { labelled_as_correct: false, feedback: { content_id: 'default_outcome', - html: '

Try again.

' + html: '

Try again.

', }, param_changes: [], dest_if_really_stuck: null, - dest: 'Start' - } + dest: 'Start', + }, }, param_changes: [], card_is_checkpoint: true, linked_skill_id: null, content: { content_id: 'content', - html: '

First Question

' - } + html: '

First Question

', + }, }, End: { classifier_model_id: null, recorded_voiceovers: { voiceovers_mapping: { - content: {} - } + content: {}, + }, }, solicit_answer_details: false, interaction: { @@ -274,19 +291,19 @@ describe('Conversation skin component', () => { hints: [], customization_args: { recommendedExplorationIds: { - value: ['recommnendedExplorationId'] - } + value: ['recommnendedExplorationId'], + }, }, answer_groups: [], - default_outcome: null + default_outcome: null, }, param_changes: [], card_is_checkpoint: false, linked_skill_id: null, content: { content_id: 'content', - html: 'Congratulations, you have finished!' - } + html: 'Congratulations, you have finished!', + }, }, Mid: { classifier_model_id: null, @@ -296,8 +313,8 @@ describe('Conversation skin component', () => { feedback_1: {}, rule_input_2: {}, content: {}, - default_outcome: {} - } + default_outcome: {}, + }, }, solicit_answer_details: false, interaction: { @@ -307,17 +324,17 @@ describe('Conversation skin component', () => { hints: [], customization_args: { rows: { - value: 1 + value: 1, }, placeholder: { value: { unicode_str: '', - content_id: 'ca_placeholder_0' - } + content_id: 'ca_placeholder_0', + }, }, catchMisspellings: { - value: false - } + value: false, + }, }, answer_groups: [ { @@ -327,28 +344,26 @@ describe('Conversation skin component', () => { labelled_as_correct: false, feedback: { content_id: 'feedback_1', - html: '

Good Job

' + html: '

Good Job

', }, param_changes: [], dest_if_really_stuck: null, - dest: 'End' + dest: 'End', }, training_data: [], rule_specs: [ { inputs: { x: { - normalizedStrSet: [ - 'answer' - ], - contentId: 'rule_input_2' - } + normalizedStrSet: ['answer'], + contentId: 'rule_input_2', + }, }, - rule_type: 'FuzzyEquals' - } + rule_type: 'FuzzyEquals', + }, ], - tagged_skill_misconception_id: null - } + tagged_skill_misconception_id: null, + }, ], default_outcome: { missing_prerequisite_skill_id: null, @@ -356,21 +371,21 @@ describe('Conversation skin component', () => { labelled_as_correct: false, feedback: { content_id: 'default_outcome', - html: '

try again.

' + html: '

try again.

', }, param_changes: [], dest_if_really_stuck: null, - dest: 'Mid' - } + dest: 'Mid', + }, }, param_changes: [], card_is_checkpoint: false, linked_skill_id: null, content: { content_id: 'content', - html: '

Second Question

' - } - } + html: '

Second Question

', + }, + }, }, auto_tts_enabled: true, version: 2, @@ -398,7 +413,7 @@ describe('Conversation skin component', () => { language_code: 'en', objective: 'To learn', states: explorationDict.states, - next_content_id_index: explorationDict.next_content_id_index + next_content_id_index: explorationDict.next_content_id_index, }, exploration_metadata: { title: 'Exploration', @@ -413,7 +428,7 @@ describe('Conversation skin component', () => { param_specs: {}, param_changes: [], auto_tts_enabled: false, - edits_allowed: true + edits_allowed: true, }, version: 2, can_edit: true, @@ -426,7 +441,7 @@ describe('Conversation skin component', () => { furthest_reached_checkpoint_exp_version: 1, furthest_reached_checkpoint_state_name: 'End', most_recently_reached_checkpoint_state_name: 'Mid', - most_recently_reached_checkpoint_exp_version: 2 + most_recently_reached_checkpoint_exp_version: 2, }; let sampleExpResponse: FetchExplorationBackendResponse = { @@ -442,7 +457,7 @@ describe('Conversation skin component', () => { language_code: 'en', objective: 'To learn', states: explorationDict.states, - next_content_id_index: explorationDict.next_content_id_index + next_content_id_index: explorationDict.next_content_id_index, }, exploration_metadata: { title: 'Exploration', @@ -457,7 +472,7 @@ describe('Conversation skin component', () => { param_specs: {}, param_changes: [], auto_tts_enabled: false, - edits_allowed: true + edits_allowed: true, }, version: 2, can_edit: true, @@ -470,33 +485,30 @@ describe('Conversation skin component', () => { furthest_reached_checkpoint_exp_version: 1, furthest_reached_checkpoint_state_name: 'End', most_recently_reached_checkpoint_state_name: null, - most_recently_reached_checkpoint_exp_version: 2 + most_recently_reached_checkpoint_exp_version: 2, }; let uniqueProgressIdResponse = '123456'; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - ConversationSkinComponent, - MockTranslatePipe - ], + declarations: [ConversationSkinComponent, MockTranslatePipe], providers: [ SolutionObjectFactory, { provide: WindowRef, - useClass: MockWindowRef + useClass: MockWindowRef, }, { provide: PlatformFeatureService, - useClass: MockPlatformFeatureService + useClass: MockPlatformFeatureService, }, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(ConversationSkinComponent); @@ -505,34 +517,45 @@ describe('Conversation skin component', () => { alertsService = TestBed.inject(AlertsService); audioPlayerService = TestBed.inject(AudioPlayerService); autogeneratedAudioPlayerService = TestBed.inject( - AutogeneratedAudioPlayerService); + AutogeneratedAudioPlayerService + ); collectionPlayerBackendApiService = TestBed.inject( - CollectionPlayerBackendApiService); + CollectionPlayerBackendApiService + ); conceptCardBackendApiService = TestBed.inject(ConceptCardBackendApiService); contentTranslationLanguageService = TestBed.inject( - ContentTranslationLanguageService); + ContentTranslationLanguageService + ); contentTranslationManagerService = TestBed.inject( - ContentTranslationManagerService); + ContentTranslationManagerService + ); contextService = TestBed.inject(ContextService); currentInteractionService = TestBed.inject(CurrentInteractionService); editableExplorationBackendApiService = TestBed.inject( - EditableExplorationBackendApiService); + EditableExplorationBackendApiService + ); explorationEngineService = TestBed.inject(ExplorationEngineService); explorationPlayerStateService = TestBed.inject( - ExplorationPlayerStateService); + ExplorationPlayerStateService + ); explorationRecommendationsService = TestBed.inject( - ExplorationRecommendationsService); + ExplorationRecommendationsService + ); explorationSummaryBackendApiService = TestBed.inject( - ExplorationSummaryBackendApiService); + ExplorationSummaryBackendApiService + ); fatigueDetectionService = TestBed.inject(FatigueDetectionService); interactionObjectFactory = TestBed.inject(InteractionObjectFactory); focusManagerService = TestBed.inject(FocusManagerService); - audioTranslationLanguageService = ( - TestBed.inject(AudioTranslationLanguageService)); + audioTranslationLanguageService = TestBed.inject( + AudioTranslationLanguageService + ); guestCollectionProgressService = TestBed.inject( - GuestCollectionProgressService); + GuestCollectionProgressService + ); hintsAndSolutionManagerService = TestBed.inject( - HintsAndSolutionManagerService); + HintsAndSolutionManagerService + ); i18nLanguageCodeService = TestBed.inject(I18nLanguageCodeService); imagePreloaderService = TestBed.inject(ImagePreloaderService); learnerAnswerInfoService = TestBed.inject(LearnerAnswerInfoService); @@ -546,9 +569,11 @@ describe('Conversation skin component', () => { questionPlayerEngineService = TestBed.inject(QuestionPlayerEngineService); questionPlayerStateService = TestBed.inject(QuestionPlayerStateService); readOnlyCollectionBackendApiService = TestBed.inject( - ReadOnlyCollectionBackendApiService); + ReadOnlyCollectionBackendApiService + ); refresherExplorationConfirmationModalService = TestBed.inject( - RefresherExplorationConfirmationModalService); + RefresherExplorationConfirmationModalService + ); siteAnalyticsService = TestBed.inject(SiteAnalyticsService); statsReportingService = TestBed.inject(StatsReportingService); solutionObjectFactory = TestBed.inject(SolutionObjectFactory); @@ -559,30 +584,32 @@ describe('Conversation skin component', () => { windowDimensionsService = TestBed.inject(WindowDimensionsService); windowRef = TestBed.inject(WindowRef); readOnlyExplorationBackendApiService = TestBed.inject( - ReadOnlyExplorationBackendApiService); + ReadOnlyExplorationBackendApiService + ); stateObjectFactory = TestBed.inject(StateObjectFactory); answerClassificationService = TestBed.inject(AnswerClassificationService); platformFeatureService = TestBed.inject(PlatformFeatureService); conceptCardManagerService = TestBed.inject(ConceptCardManagerService); translateService = TestBed.inject(TranslateService); learnerDashboardBackendApiService = TestBed.inject( - LearnerDashboardBackendApiService); + LearnerDashboardBackendApiService + ); audioTranslationLanguageService = TestBed.inject( - AudioTranslationLanguageService); + AudioTranslationLanguageService + ); })); - it('should create && adjust page height on resize of window', - fakeAsync(() => { - spyOn(componentInstance, 'adjustPageHeight').and.stub(); - componentInstance.adjustPageHeightOnresize(); + it('should create && adjust page height on resize of window', fakeAsync(() => { + spyOn(componentInstance, 'adjustPageHeight').and.stub(); + componentInstance.adjustPageHeightOnresize(); - expect(componentInstance).toBeDefined(); + expect(componentInstance).toBeDefined(); - windowRef.nativeWindow.onresize(null); - tick(200); + windowRef.nativeWindow.onresize(null); + tick(200); - expect(componentInstance.adjustPageHeight).toHaveBeenCalled(); - })); + expect(componentInstance.adjustPageHeight).toHaveBeenCalled(); + })); it('should initialize component', fakeAsync(() => { let collectionId = 'id'; @@ -594,37 +621,50 @@ describe('Conversation skin component', () => { summaries: [], user_email: '', is_topic_manager: false, - username: true + username: true, }; let newStateName = 'newState'; spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(false); spyOn(userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(new UserInfo( - [], false, false, - false, false, false, '', '', '', true))); - spyOn(urlService, 'getCollectionIdFromExplorationUrl') - .and.returnValues(collectionId, null); + Promise.resolve( + new UserInfo([], false, false, false, false, false, '', '', '', true) + ) + ); + spyOn(urlService, 'getCollectionIdFromExplorationUrl').and.returnValues( + collectionId, + null + ); spyOn(urlService, 'getPidFromUrl').and.returnValue(null); - spyOn(readOnlyCollectionBackendApiService, 'loadCollectionAsync') - .and.returnValue(Promise.resolve(new Collection( - '', '', '', '', [], null, '', 6, 8, []))); + spyOn( + readOnlyCollectionBackendApiService, + 'loadCollectionAsync' + ).and.returnValue( + Promise.resolve(new Collection('', '', '', '', [], null, '', 6, 8, [])) + ); spyOn(explorationEngineService, 'getExplorationId').and.returnValue(expId); - spyOn(explorationEngineService, 'isInPreviewMode') - .and.returnValue(isInPreviewMode); - spyOn(explorationPlayerStateService, 'getCurrentEngineService') - .and.returnValue(explorationEngineService); + spyOn(explorationEngineService, 'isInPreviewMode').and.returnValue( + isInPreviewMode + ); + spyOn( + explorationPlayerStateService, + 'getCurrentEngineService' + ).and.returnValue(explorationEngineService); spyOn(explorationEngineService, 'getLanguageCode').and.returnValue('en'); spyOn(urlService, 'isIframed').and.returnValue(isIframed); spyOn(loaderService, 'showLoadingScreen'); - spyOn(urlInterpolationService, 'getStaticImageUrl') - .and.returnValue('oppia_avatar_url'); - spyOn(explorationPlayerStateService, 'isInQuestionPlayerMode') - .and.returnValues(true, false); + spyOn(urlInterpolationService, 'getStaticImageUrl').and.returnValue( + 'oppia_avatar_url' + ); + spyOn( + explorationPlayerStateService, + 'isInQuestionPlayerMode' + ).and.returnValues(true, false); spyOn(componentInstance, 'initializePage'); - spyOn(collectionPlayerBackendApiService, 'fetchCollectionSummariesAsync') - .and.returnValue( - Promise.resolve(collectionSummary)); + spyOn( + collectionPlayerBackendApiService, + 'fetchCollectionSummariesAsync' + ).and.returnValue(Promise.resolve(collectionSummary)); spyOn(questionPlayerStateService, 'hintUsed'); spyOn(questionPlayerEngineService, 'getCurrentQuestion'); spyOn(questionPlayerStateService, 'solutionViewed'); @@ -633,22 +673,31 @@ describe('Conversation skin component', () => { spyOn(statsReportingService, 'recordExplorationCompleted'); spyOn(statsReportingService, 'recordExplorationActuallyStarted'); spyOn( - guestCollectionProgressService, 'recordExplorationCompletedInCollection'); - spyOn(componentInstance, 'doesCollectionAllowsGuestProgress') - .and.returnValue(true); + guestCollectionProgressService, + 'recordExplorationCompletedInCollection' + ); + spyOn( + componentInstance, + 'doesCollectionAllowsGuestProgress' + ).and.returnValue(true); spyOn(statsReportingService, 'recordMaybeLeaveEvent'); spyOn(playerTranscriptService, 'getLastStateName').and.returnValue(''); spyOn(learnerParamsService, 'getAllParams').and.returnValue({}); spyOn(messengerService, 'sendMessage'); - spyOn(readOnlyExplorationBackendApiService, 'loadLatestExplorationAsync') - .and.returnValue(Promise.resolve(explorationResponse)); - spyOn(explorationEngineService, 'getShortestPathToState') - .and.returnValue(['Start', 'Mid']); + spyOn( + readOnlyExplorationBackendApiService, + 'loadLatestExplorationAsync' + ).and.returnValue(Promise.resolve(explorationResponse)); + spyOn(explorationEngineService, 'getShortestPathToState').and.returnValue([ + 'Start', + 'Mid', + ]); spyOn( editableExplorationBackendApiService, - 'recordProgressAndFetchUniqueProgressIdOfLoggedOutLearner') - .and.returnValue(Promise.resolve( - {unique_progress_url_id: uniqueProgressIdResponse})); + 'recordProgressAndFetchUniqueProgressIdOfLoggedOutLearner' + ).and.returnValue( + Promise.resolve({unique_progress_url_id: uniqueProgressIdResponse}) + ); let mockOnHintConsumed = new EventEmitter(); let mockOnSolutionViewedEventEmitter = new EventEmitter(); @@ -658,26 +707,44 @@ describe('Conversation skin component', () => { let mockOnLearnerReallyStuck = new EventEmitter(); let mockOnLearnerGetsReallyStuck = new EventEmitter(); - spyOnProperty(playerPositionService, 'onNewCardOpened') - .and.returnValue(mockOnNewCardOpened); - spyOnProperty(hintsAndSolutionManagerService, 'onHintsExhausted') - .and.returnValue(mockOnHintsExhausted); - spyOnProperty(conceptCardManagerService, 'onLearnerGetsReallyStuck') - .and.returnValue(mockOnLearnerGetsReallyStuck); - spyOnProperty(hintsAndSolutionManagerService, 'onLearnerReallyStuck') - .and.returnValue(mockOnLearnerReallyStuck); - spyOnProperty(hintsAndSolutionManagerService, 'onHintConsumed') - .and.returnValue(mockOnHintConsumed); + spyOnProperty(playerPositionService, 'onNewCardOpened').and.returnValue( + mockOnNewCardOpened + ); + spyOnProperty( + hintsAndSolutionManagerService, + 'onHintsExhausted' + ).and.returnValue(mockOnHintsExhausted); spyOnProperty( - hintsAndSolutionManagerService, 'onSolutionViewedEventEmitter') - .and.returnValue(mockOnSolutionViewedEventEmitter); - spyOnProperty(explorationPlayerStateService, 'onPlayerStateChange') - .and.returnValue(mockOnPlayerStateChange); + conceptCardManagerService, + 'onLearnerGetsReallyStuck' + ).and.returnValue(mockOnLearnerGetsReallyStuck); + spyOnProperty( + hintsAndSolutionManagerService, + 'onLearnerReallyStuck' + ).and.returnValue(mockOnLearnerReallyStuck); + spyOnProperty( + hintsAndSolutionManagerService, + 'onHintConsumed' + ).and.returnValue(mockOnHintConsumed); + spyOnProperty( + hintsAndSolutionManagerService, + 'onSolutionViewedEventEmitter' + ).and.returnValue(mockOnSolutionViewedEventEmitter); + spyOnProperty( + explorationPlayerStateService, + 'onPlayerStateChange' + ).and.returnValue(mockOnPlayerStateChange); componentInstance.nextCard = new StateCard( - null, null, null, new Interaction( - [], [], null, null, [], 'EndExploration', null), - [], null, '', null); + null, + null, + null, + new Interaction([], [], null, null, [], 'EndExploration', null), + [], + null, + '', + null + ); componentInstance.isLoggedIn = false; componentInstance.hasInteractedAtLeastOnce = true; componentInstance.displayedCard = displayedCard; @@ -714,40 +781,53 @@ describe('Conversation skin component', () => { summaries: [], user_email: '', is_topic_manager: false, - username: true + username: true, }; spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(false); spyOn(userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(new UserInfo( - [], false, false, - false, false, false, '', '', '', true))); - spyOn(urlService, 'getCollectionIdFromExplorationUrl') - .and.returnValues(collectionId, null); + Promise.resolve( + new UserInfo([], false, false, false, false, false, '', '', '', true) + ) + ); + spyOn(urlService, 'getCollectionIdFromExplorationUrl').and.returnValues( + collectionId, + null + ); spyOn(urlService, 'getPidFromUrl').and.returnValue(null); - spyOn(readOnlyCollectionBackendApiService, 'loadCollectionAsync') - .and.returnValue(Promise.resolve(new Collection( - '', '', '', '', [], null, '', 6, 8, []))); + spyOn( + readOnlyCollectionBackendApiService, + 'loadCollectionAsync' + ).and.returnValue( + Promise.resolve(new Collection('', '', '', '', [], null, '', 6, 8, [])) + ); spyOn(componentInstance, 'fetchCompletedChaptersCount').and.callThrough(); spyOn( learnerDashboardBackendApiService, - 'fetchLearnerCompletedChaptersCountDataAsync').and.returnValue( + 'fetchLearnerCompletedChaptersCountDataAsync' + ).and.returnValue( Promise.resolve({ completedChaptersCount: 1, - })); + }) + ); spyOn(explorationEngineService, 'getExplorationId').and.returnValue(expId); - spyOn(explorationEngineService, 'isInPreviewMode') - .and.returnValue(isInPreviewMode); + spyOn(explorationEngineService, 'isInPreviewMode').and.returnValue( + isInPreviewMode + ); spyOn(urlService, 'isIframed').and.returnValue(isIframed); spyOn(loaderService, 'showLoadingScreen'); - spyOn(urlInterpolationService, 'getStaticImageUrl') - .and.returnValue('oppia_avatar_url'); - spyOn(explorationPlayerStateService, 'isInQuestionPlayerMode') - .and.returnValues(true, false); + spyOn(urlInterpolationService, 'getStaticImageUrl').and.returnValue( + 'oppia_avatar_url' + ); + spyOn( + explorationPlayerStateService, + 'isInQuestionPlayerMode' + ).and.returnValues(true, false); spyOn(componentInstance, 'initializePage'); - spyOn(collectionPlayerBackendApiService, 'fetchCollectionSummariesAsync') - .and.returnValue( - Promise.resolve(collectionSummary)); + spyOn( + collectionPlayerBackendApiService, + 'fetchCollectionSummariesAsync' + ).and.returnValue(Promise.resolve(collectionSummary)); spyOn(questionPlayerStateService, 'hintUsed'); spyOn(questionPlayerEngineService, 'getCurrentQuestion'); spyOn(questionPlayerStateService, 'solutionViewed'); @@ -755,15 +835,21 @@ describe('Conversation skin component', () => { spyOn(statsReportingService, 'recordExplorationCompleted'); spyOn(statsReportingService, 'recordExplorationActuallyStarted'); spyOn( - guestCollectionProgressService, 'recordExplorationCompletedInCollection'); - spyOn(componentInstance, 'doesCollectionAllowsGuestProgress') - .and.returnValue(true); + guestCollectionProgressService, + 'recordExplorationCompletedInCollection' + ); + spyOn( + componentInstance, + 'doesCollectionAllowsGuestProgress' + ).and.returnValue(true); spyOn(statsReportingService, 'recordMaybeLeaveEvent'); spyOn(playerTranscriptService, 'getLastStateName').and.returnValue(''); spyOn(learnerParamsService, 'getAllParams').and.returnValue({}); spyOn(messengerService, 'sendMessage'); - spyOn(readOnlyExplorationBackendApiService, 'loadLatestExplorationAsync') - .and.returnValue(Promise.resolve(sampleExpResponse)); + spyOn( + readOnlyExplorationBackendApiService, + 'loadLatestExplorationAsync' + ).and.returnValue(Promise.resolve(sampleExpResponse)); let mockOnHintConsumed = new EventEmitter(); let mockOnSolutionViewedEventEmitter = new EventEmitter(); @@ -773,26 +859,44 @@ describe('Conversation skin component', () => { let mockOnLearnerReallyStuck = new EventEmitter(); let mockOnLearnerGetsReallyStuck = new EventEmitter(); - spyOnProperty(playerPositionService, 'onNewCardOpened') - .and.returnValue(mockOnNewCardOpened); - spyOnProperty(hintsAndSolutionManagerService, 'onHintsExhausted') - .and.returnValue(mockOnHintsExhausted); - spyOnProperty(conceptCardManagerService, 'onLearnerGetsReallyStuck') - .and.returnValue(mockOnLearnerGetsReallyStuck); - spyOnProperty(hintsAndSolutionManagerService, 'onLearnerReallyStuck') - .and.returnValue(mockOnLearnerReallyStuck); - spyOnProperty(hintsAndSolutionManagerService, 'onHintConsumed') - .and.returnValue(mockOnHintConsumed); + spyOnProperty(playerPositionService, 'onNewCardOpened').and.returnValue( + mockOnNewCardOpened + ); + spyOnProperty( + hintsAndSolutionManagerService, + 'onHintsExhausted' + ).and.returnValue(mockOnHintsExhausted); + spyOnProperty( + conceptCardManagerService, + 'onLearnerGetsReallyStuck' + ).and.returnValue(mockOnLearnerGetsReallyStuck); + spyOnProperty( + hintsAndSolutionManagerService, + 'onLearnerReallyStuck' + ).and.returnValue(mockOnLearnerReallyStuck); + spyOnProperty( + hintsAndSolutionManagerService, + 'onHintConsumed' + ).and.returnValue(mockOnHintConsumed); + spyOnProperty( + hintsAndSolutionManagerService, + 'onSolutionViewedEventEmitter' + ).and.returnValue(mockOnSolutionViewedEventEmitter); spyOnProperty( - hintsAndSolutionManagerService, 'onSolutionViewedEventEmitter') - .and.returnValue(mockOnSolutionViewedEventEmitter); - spyOnProperty(explorationPlayerStateService, 'onPlayerStateChange') - .and.returnValue(mockOnPlayerStateChange); + explorationPlayerStateService, + 'onPlayerStateChange' + ).and.returnValue(mockOnPlayerStateChange); componentInstance.nextCard = new StateCard( - null, null, null, new Interaction( - [], [], null, null, [], 'EndExploration', null), - [], null, '', null); + null, + null, + null, + new Interaction([], [], null, null, [], 'EndExploration', null), + [], + null, + '', + null + ); componentInstance.isLoggedIn = true; componentInstance.isIframed = false; componentInstance.hasInteractedAtLeastOnce = true; @@ -818,35 +922,46 @@ describe('Conversation skin component', () => { summaries: [], user_email: '', is_topic_manager: false, - username: true + username: true, }; let expResponse = sampleExpResponse; expResponse.is_logged_in = false; spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(false); spyOn(userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(new UserInfo( - [], false, false, - false, false, false, '', '', '', false))); - spyOn(urlService, 'getCollectionIdFromExplorationUrl') - .and.returnValues(collectionId, null); + Promise.resolve( + new UserInfo([], false, false, false, false, false, '', '', '', false) + ) + ); + spyOn(urlService, 'getCollectionIdFromExplorationUrl').and.returnValues( + collectionId, + null + ); spyOn(urlService, 'getPidFromUrl').and.returnValue(null); - spyOn(readOnlyCollectionBackendApiService, 'loadCollectionAsync') - .and.returnValue(Promise.resolve(new Collection( - '', '', '', '', [], null, '', 6, 8, []))); + spyOn( + readOnlyCollectionBackendApiService, + 'loadCollectionAsync' + ).and.returnValue( + Promise.resolve(new Collection('', '', '', '', [], null, '', 6, 8, [])) + ); spyOn(explorationEngineService, 'getExplorationId').and.returnValue(expId); - spyOn(explorationEngineService, 'isInPreviewMode') - .and.returnValue(isInPreviewMode); + spyOn(explorationEngineService, 'isInPreviewMode').and.returnValue( + isInPreviewMode + ); spyOn(urlService, 'isIframed').and.returnValue(isIframed); spyOn(loaderService, 'showLoadingScreen'); - spyOn(urlInterpolationService, 'getStaticImageUrl') - .and.returnValue('oppia_avatar_url'); - spyOn(explorationPlayerStateService, 'isInQuestionPlayerMode') - .and.returnValues(true, false); + spyOn(urlInterpolationService, 'getStaticImageUrl').and.returnValue( + 'oppia_avatar_url' + ); + spyOn( + explorationPlayerStateService, + 'isInQuestionPlayerMode' + ).and.returnValues(true, false); spyOn(componentInstance, 'initializePage'); - spyOn(collectionPlayerBackendApiService, 'fetchCollectionSummariesAsync') - .and.returnValue( - Promise.resolve(collectionSummary)); + spyOn( + collectionPlayerBackendApiService, + 'fetchCollectionSummariesAsync' + ).and.returnValue(Promise.resolve(collectionSummary)); spyOn(questionPlayerStateService, 'hintUsed'); spyOn(questionPlayerEngineService, 'getCurrentQuestion'); spyOn(questionPlayerStateService, 'solutionViewed'); @@ -855,20 +970,27 @@ describe('Conversation skin component', () => { spyOn(statsReportingService, 'recordExplorationCompleted'); spyOn(statsReportingService, 'recordExplorationActuallyStarted'); spyOn( - guestCollectionProgressService, 'recordExplorationCompletedInCollection'); - spyOn(componentInstance, 'doesCollectionAllowsGuestProgress') - .and.returnValue(true); + guestCollectionProgressService, + 'recordExplorationCompletedInCollection' + ); + spyOn( + componentInstance, + 'doesCollectionAllowsGuestProgress' + ).and.returnValue(true); spyOn(statsReportingService, 'recordMaybeLeaveEvent'); spyOn(playerTranscriptService, 'getLastStateName').and.returnValue(''); spyOn(learnerParamsService, 'getAllParams').and.returnValue({}); spyOn(messengerService, 'sendMessage'); - spyOn(readOnlyExplorationBackendApiService, 'loadLatestExplorationAsync') - .and.returnValue(Promise.resolve(expResponse)); + spyOn( + readOnlyExplorationBackendApiService, + 'loadLatestExplorationAsync' + ).and.returnValue(Promise.resolve(expResponse)); spyOn( editableExplorationBackendApiService, - 'recordProgressAndFetchUniqueProgressIdOfLoggedOutLearner') - .and.returnValue(Promise.resolve( - {unique_progress_url_id: uniqueProgressIdResponse})); + 'recordProgressAndFetchUniqueProgressIdOfLoggedOutLearner' + ).and.returnValue( + Promise.resolve({unique_progress_url_id: uniqueProgressIdResponse}) + ); let mockOnHintConsumed = new EventEmitter(); let mockOnSolutionViewedEventEmitter = new EventEmitter(); @@ -878,26 +1000,44 @@ describe('Conversation skin component', () => { let mockOnLearnerReallyStuck = new EventEmitter(); let mockOnLearnerGetsReallyStuck = new EventEmitter(); - spyOnProperty(playerPositionService, 'onNewCardOpened') - .and.returnValue(mockOnNewCardOpened); - spyOnProperty(hintsAndSolutionManagerService, 'onHintsExhausted') - .and.returnValue(mockOnHintsExhausted); - spyOnProperty(conceptCardManagerService, 'onLearnerGetsReallyStuck') - .and.returnValue(mockOnLearnerGetsReallyStuck); - spyOnProperty(hintsAndSolutionManagerService, 'onLearnerReallyStuck') - .and.returnValue(mockOnLearnerReallyStuck); - spyOnProperty(hintsAndSolutionManagerService, 'onHintConsumed') - .and.returnValue(mockOnHintConsumed); + spyOnProperty(playerPositionService, 'onNewCardOpened').and.returnValue( + mockOnNewCardOpened + ); + spyOnProperty( + hintsAndSolutionManagerService, + 'onHintsExhausted' + ).and.returnValue(mockOnHintsExhausted); spyOnProperty( - hintsAndSolutionManagerService, 'onSolutionViewedEventEmitter') - .and.returnValue(mockOnSolutionViewedEventEmitter); - spyOnProperty(explorationPlayerStateService, 'onPlayerStateChange') - .and.returnValue(mockOnPlayerStateChange); + conceptCardManagerService, + 'onLearnerGetsReallyStuck' + ).and.returnValue(mockOnLearnerGetsReallyStuck); + spyOnProperty( + hintsAndSolutionManagerService, + 'onLearnerReallyStuck' + ).and.returnValue(mockOnLearnerReallyStuck); + spyOnProperty( + hintsAndSolutionManagerService, + 'onHintConsumed' + ).and.returnValue(mockOnHintConsumed); + spyOnProperty( + hintsAndSolutionManagerService, + 'onSolutionViewedEventEmitter' + ).and.returnValue(mockOnSolutionViewedEventEmitter); + spyOnProperty( + explorationPlayerStateService, + 'onPlayerStateChange' + ).and.returnValue(mockOnPlayerStateChange); componentInstance.nextCard = new StateCard( - null, null, null, new Interaction( - [], [], null, null, [], 'EndExploration', null), - [], null, '', null); + null, + null, + null, + new Interaction([], [], null, null, [], 'EndExploration', null), + [], + null, + '', + null + ); componentInstance.isLoggedIn = false; componentInstance.isIframed = false; componentInstance.hasInteractedAtLeastOnce = true; @@ -913,273 +1053,376 @@ describe('Conversation skin component', () => { tick(100); })); - it('should convert logged out progress to logged in progress when user ' + - 'signs in', fakeAsync(() => { - let collectionId = 'id'; - let expId = 'exp_id'; - let isInPreviewMode = false; - let isIframed = false; - let collectionSummary = { - is_admin: true, - summaries: [], - user_email: '', - is_topic_manager: false, - username: true - }; - spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(false); - spyOn(userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(new UserInfo( - [], false, false, - false, false, false, '', '', '', true))); - spyOn(urlService, 'getCollectionIdFromExplorationUrl') - .and.returnValues(collectionId, null); - spyOn(urlService, 'getPidFromUrl').and.returnValue(null); + it( + 'should convert logged out progress to logged in progress when user ' + + 'signs in', + fakeAsync(() => { + let collectionId = 'id'; + let expId = 'exp_id'; + let isInPreviewMode = false; + let isIframed = false; + let collectionSummary = { + is_admin: true, + summaries: [], + user_email: '', + is_topic_manager: false, + username: true, + }; + spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(false); + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve( + new UserInfo([], false, false, false, false, false, '', '', '', true) + ) + ); + spyOn(urlService, 'getCollectionIdFromExplorationUrl').and.returnValues( + collectionId, + null + ); + spyOn(urlService, 'getPidFromUrl').and.returnValue(null); - spyOn(readOnlyCollectionBackendApiService, 'loadCollectionAsync') - .and.returnValue(Promise.resolve(new Collection( - '', '', '', '', [], null, '', 6, 8, []))); - spyOn(explorationEngineService, 'getExplorationId').and.returnValue(expId); - spyOn(explorationEngineService, 'isInPreviewMode') - .and.returnValue(isInPreviewMode); - spyOn(urlService, 'isIframed').and.returnValue(isIframed); - spyOn(loaderService, 'showLoadingScreen'); - spyOn(urlInterpolationService, 'getStaticImageUrl') - .and.returnValue('oppia_avatar_url'); - spyOn(explorationPlayerStateService, 'isInQuestionPlayerMode') - .and.returnValues(true, false); - spyOn(componentInstance, 'initializePage'); - spyOn(collectionPlayerBackendApiService, 'fetchCollectionSummariesAsync') - .and.returnValue( - Promise.resolve(collectionSummary)); - spyOn(questionPlayerStateService, 'hintUsed'); - spyOn(questionPlayerEngineService, 'getCurrentQuestion'); - spyOn(questionPlayerStateService, 'solutionViewed'); - spyOn(imagePreloaderService, 'onStateChange'); - spyOn(statsReportingService, 'recordExplorationCompleted'); - spyOn(statsReportingService, 'recordExplorationActuallyStarted'); - spyOn( - guestCollectionProgressService, 'recordExplorationCompletedInCollection'); - spyOn(componentInstance, 'doesCollectionAllowsGuestProgress') - .and.returnValue(true); - spyOn(statsReportingService, 'recordMaybeLeaveEvent'); - spyOn(playerTranscriptService, 'getLastStateName').and.returnValue(''); - spyOn(learnerParamsService, 'getAllParams').and.returnValue({}); - spyOn(messengerService, 'sendMessage'); - spyOn(readOnlyExplorationBackendApiService, 'loadLatestExplorationAsync') - .and.returnValue(Promise.resolve(sampleExpResponse)); - spyOn( - editableExplorationBackendApiService, - 'changeLoggedOutProgressToLoggedInProgressAsync') - .and.returnValue(Promise.resolve()); + spyOn( + readOnlyCollectionBackendApiService, + 'loadCollectionAsync' + ).and.returnValue( + Promise.resolve(new Collection('', '', '', '', [], null, '', 6, 8, [])) + ); + spyOn(explorationEngineService, 'getExplorationId').and.returnValue( + expId + ); + spyOn(explorationEngineService, 'isInPreviewMode').and.returnValue( + isInPreviewMode + ); + spyOn(urlService, 'isIframed').and.returnValue(isIframed); + spyOn(loaderService, 'showLoadingScreen'); + spyOn(urlInterpolationService, 'getStaticImageUrl').and.returnValue( + 'oppia_avatar_url' + ); + spyOn( + explorationPlayerStateService, + 'isInQuestionPlayerMode' + ).and.returnValues(true, false); + spyOn(componentInstance, 'initializePage'); + spyOn( + collectionPlayerBackendApiService, + 'fetchCollectionSummariesAsync' + ).and.returnValue(Promise.resolve(collectionSummary)); + spyOn(questionPlayerStateService, 'hintUsed'); + spyOn(questionPlayerEngineService, 'getCurrentQuestion'); + spyOn(questionPlayerStateService, 'solutionViewed'); + spyOn(imagePreloaderService, 'onStateChange'); + spyOn(statsReportingService, 'recordExplorationCompleted'); + spyOn(statsReportingService, 'recordExplorationActuallyStarted'); + spyOn( + guestCollectionProgressService, + 'recordExplorationCompletedInCollection' + ); + spyOn( + componentInstance, + 'doesCollectionAllowsGuestProgress' + ).and.returnValue(true); + spyOn(statsReportingService, 'recordMaybeLeaveEvent'); + spyOn(playerTranscriptService, 'getLastStateName').and.returnValue(''); + spyOn(learnerParamsService, 'getAllParams').and.returnValue({}); + spyOn(messengerService, 'sendMessage'); + spyOn( + readOnlyExplorationBackendApiService, + 'loadLatestExplorationAsync' + ).and.returnValue(Promise.resolve(sampleExpResponse)); + spyOn( + editableExplorationBackendApiService, + 'changeLoggedOutProgressToLoggedInProgressAsync' + ).and.returnValue(Promise.resolve()); - let mockOnHintConsumed = new EventEmitter(); - let mockOnSolutionViewedEventEmitter = new EventEmitter(); - let mockOnPlayerStateChange = new EventEmitter(); - let mockOnNewCardOpened = new EventEmitter(); - let mockOnHintsExhausted = new EventEmitter(); - let mockOnLearnerReallyStuck = new EventEmitter(); - let mockOnLearnerGetsReallyStuck = new EventEmitter(); + let mockOnHintConsumed = new EventEmitter(); + let mockOnSolutionViewedEventEmitter = new EventEmitter(); + let mockOnPlayerStateChange = new EventEmitter(); + let mockOnNewCardOpened = new EventEmitter(); + let mockOnHintsExhausted = new EventEmitter(); + let mockOnLearnerReallyStuck = new EventEmitter(); + let mockOnLearnerGetsReallyStuck = new EventEmitter(); - spyOnProperty(playerPositionService, 'onNewCardOpened') - .and.returnValue(mockOnNewCardOpened); - spyOnProperty(hintsAndSolutionManagerService, 'onHintsExhausted') - .and.returnValue(mockOnHintsExhausted); - spyOnProperty(conceptCardManagerService, 'onLearnerGetsReallyStuck') - .and.returnValue(mockOnLearnerGetsReallyStuck); - spyOnProperty(hintsAndSolutionManagerService, 'onLearnerReallyStuck') - .and.returnValue(mockOnLearnerReallyStuck); - spyOnProperty(hintsAndSolutionManagerService, 'onHintConsumed') - .and.returnValue(mockOnHintConsumed); - spyOnProperty( - hintsAndSolutionManagerService, 'onSolutionViewedEventEmitter') - .and.returnValue(mockOnSolutionViewedEventEmitter); - spyOnProperty(explorationPlayerStateService, 'onPlayerStateChange') - .and.returnValue(mockOnPlayerStateChange); - spyOn(localStorageService, 'getUniqueProgressIdOfLoggedOutLearner') - .and.returnValue('abcdef'); - spyOn(localStorageService, 'removeUniqueProgressIdOfLoggedOutLearner'); + spyOnProperty(playerPositionService, 'onNewCardOpened').and.returnValue( + mockOnNewCardOpened + ); + spyOnProperty( + hintsAndSolutionManagerService, + 'onHintsExhausted' + ).and.returnValue(mockOnHintsExhausted); + spyOnProperty( + conceptCardManagerService, + 'onLearnerGetsReallyStuck' + ).and.returnValue(mockOnLearnerGetsReallyStuck); + spyOnProperty( + hintsAndSolutionManagerService, + 'onLearnerReallyStuck' + ).and.returnValue(mockOnLearnerReallyStuck); + spyOnProperty( + hintsAndSolutionManagerService, + 'onHintConsumed' + ).and.returnValue(mockOnHintConsumed); + spyOnProperty( + hintsAndSolutionManagerService, + 'onSolutionViewedEventEmitter' + ).and.returnValue(mockOnSolutionViewedEventEmitter); + spyOnProperty( + explorationPlayerStateService, + 'onPlayerStateChange' + ).and.returnValue(mockOnPlayerStateChange); + spyOn( + localStorageService, + 'getUniqueProgressIdOfLoggedOutLearner' + ).and.returnValue('abcdef'); + spyOn(localStorageService, 'removeUniqueProgressIdOfLoggedOutLearner'); - componentInstance.nextCard = new StateCard( - null, null, null, new Interaction( - [], [], null, null, [], 'EndExploration', null), - [], null, '', null); - componentInstance.isLoggedIn = true; - componentInstance.isIframed = false; - componentInstance.hasInteractedAtLeastOnce = true; - componentInstance.displayedCard = displayedCard; + componentInstance.nextCard = new StateCard( + null, + null, + null, + new Interaction([], [], null, null, [], 'EndExploration', null), + [], + null, + '', + null + ); + componentInstance.isLoggedIn = true; + componentInstance.isIframed = false; + componentInstance.hasInteractedAtLeastOnce = true; + componentInstance.displayedCard = displayedCard; + + componentInstance.ngOnInit(); + tick(100); + + expect( + editableExplorationBackendApiService.changeLoggedOutProgressToLoggedInProgressAsync + ).toHaveBeenCalled(); + expect( + localStorageService.removeUniqueProgressIdOfLoggedOutLearner + ).toHaveBeenCalled(); + }) + ); + + it('should show alert when collection summaries are not loaded', fakeAsync(() => { + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve( + new UserInfo([], false, false, false, false, false, '', '', '', true) + ) + ); + spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(true); + spyOn(urlService, 'getCollectionIdFromExplorationUrl').and.returnValue( + 'collection_id' + ); + spyOn(urlService, 'getPidFromUrl').and.returnValue(null); + spyOn( + localStorageService, + 'getUniqueProgressIdOfLoggedOutLearner' + ).and.returnValue(null); + spyOn( + collectionPlayerBackendApiService, + 'fetchCollectionSummariesAsync' + ).and.returnValue(Promise.reject()); + spyOn(alertsService, 'addWarning'); componentInstance.ngOnInit(); - tick(100); + tick(); + expect(contextService.isInExplorationEditorPage).toHaveBeenCalled(); + expect(urlService.getCollectionIdFromExplorationUrl).toHaveBeenCalled(); expect( - editableExplorationBackendApiService - .changeLoggedOutProgressToLoggedInProgressAsync).toHaveBeenCalled(); - expect( - localStorageService. - removeUniqueProgressIdOfLoggedOutLearner).toHaveBeenCalled(); + collectionPlayerBackendApiService.fetchCollectionSummariesAsync + ).toHaveBeenCalled(); + expect(alertsService.addWarning).toHaveBeenCalledWith( + 'There was an error while fetching the collection ' + 'summary.' + ); })); - it('should show alert when collection summaries are not loaded', - fakeAsync(() => { - spyOn(userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(new UserInfo( - [], false, false, - false, false, false, '', '', '', true))); - spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(true); - spyOn(urlService, 'getCollectionIdFromExplorationUrl').and.returnValue( - 'collection_id'); - spyOn(urlService, 'getPidFromUrl').and.returnValue(null); - spyOn(localStorageService, 'getUniqueProgressIdOfLoggedOutLearner') - .and.returnValue(null); - spyOn(collectionPlayerBackendApiService, 'fetchCollectionSummariesAsync') - .and.returnValue(Promise.reject()); - spyOn(alertsService, 'addWarning'); - - componentInstance.ngOnInit(); - tick(); - - expect(contextService.isInExplorationEditorPage).toHaveBeenCalled(); - expect(urlService.getCollectionIdFromExplorationUrl).toHaveBeenCalled(); - expect(collectionPlayerBackendApiService.fetchCollectionSummariesAsync) - .toHaveBeenCalled(); - expect(alertsService.addWarning).toHaveBeenCalledWith( - 'There was an error while fetching the collection ' + - 'summary.' - ); - })); - it('should tell if submit button is disabled', () => { let displayedCardIndex = 1; spyOn(playerPositionService, 'getDisplayedCardIndex').and.returnValue( - displayedCardIndex); + displayedCardIndex + ); spyOn(playerTranscriptService, 'isLastCard').and.returnValues(true, false); spyOn(currentInteractionService, 'isSubmitButtonDisabled').and.returnValue( - true); + true + ); expect(componentInstance.isSubmitButtonDisabled()).toBeTrue(); expect(componentInstance.isSubmitButtonDisabled()).toBeFalse(); }); - it('should release solution when the learner gets stuck' + - ' if no stuck state exists after a predetermined time', fakeAsync(() => { - // Release solution if stuck state is null. - componentInstance.nextCardIfStuck = null; - let solutionSpy = spyOn(hintsAndSolutionManagerService, 'releaseSolution'); - let redirectionSpy = spyOn(componentInstance, 'showUpcomingCard'); - componentInstance.solutionForState = solutionObjectFactory.createNew( - true, 'answer', 'Html', 'XyzID'); - componentInstance.numberOfIncorrectSubmissions = 3; - componentInstance.triggerIfLearnerStuckAction(); - tick( - ExplorationPlayerConstants.WAIT_BEFORE_RESPONSE_FOR_STUCK_LEARNER_MSEC); - tick(ExplorationPlayerConstants.WAIT_BEFORE_REALLY_STUCK_MSEC); - - expect(solutionSpy).toHaveBeenCalled(); - expect(redirectionSpy).not.toHaveBeenCalled(); - flush(); - })); + it( + 'should release solution when the learner gets stuck' + + ' if no stuck state exists after a predetermined time', + fakeAsync(() => { + // Release solution if stuck state is null. + componentInstance.nextCardIfStuck = null; + let solutionSpy = spyOn( + hintsAndSolutionManagerService, + 'releaseSolution' + ); + let redirectionSpy = spyOn(componentInstance, 'showUpcomingCard'); + componentInstance.solutionForState = solutionObjectFactory.createNew( + true, + 'answer', + 'Html', + 'XyzID' + ); + componentInstance.numberOfIncorrectSubmissions = 3; + componentInstance.triggerIfLearnerStuckAction(); + tick( + ExplorationPlayerConstants.WAIT_BEFORE_RESPONSE_FOR_STUCK_LEARNER_MSEC + ); + tick(ExplorationPlayerConstants.WAIT_BEFORE_REALLY_STUCK_MSEC); - it('should direct the learner to the stuck' + - ' when the learner gets stuck and such a state exists after a' + - ' predetermined time', fakeAsync(() => { - spyOn(componentInstance, 'showPendingCard'); - spyOn(translateService, 'instant').and.callThrough(); - spyOn(playerTranscriptService, 'addNewResponseToExistingFeedback'); + expect(solutionSpy).toHaveBeenCalled(); + expect(redirectionSpy).not.toHaveBeenCalled(); + flush(); + }) + ); - expect(componentInstance.continueToReviseStateButtonIsVisible). - toEqual(false); - componentInstance.nextCardIfStuck = new StateCard( - null, null, null, new Interaction( - [], [], null, null, [], 'EndExploration', null), - [], null, '', null); - componentInstance.triggerIfLearnerStuckAction(); - tick( - ExplorationPlayerConstants.WAIT_BEFORE_RESPONSE_FOR_STUCK_LEARNER_MSEC); - - expect(translateService.instant).toHaveBeenCalledWith( - 'I18N_REDIRECTION_TO_STUCK_STATE_MESSAGE'); - expect(componentInstance.continueToReviseStateButtonIsVisible). - toEqual(true); - flush(); - })); + it( + 'should direct the learner to the stuck' + + ' when the learner gets stuck and such a state exists after a' + + ' predetermined time', + fakeAsync(() => { + spyOn(componentInstance, 'showPendingCard'); + spyOn(translateService, 'instant').and.callThrough(); + spyOn(playerTranscriptService, 'addNewResponseToExistingFeedback'); - it('should immediately release solution when the learner gets stuck' + - ' if no stuck state exists', fakeAsync(() => { - // Release solution if stuck state is null. - componentInstance.nextCardIfStuck = null; - let solutionSpy = spyOn(hintsAndSolutionManagerService, 'releaseSolution'); - let redirectionSpy = spyOn(componentInstance, 'showUpcomingCard'); - componentInstance.solutionForState = solutionObjectFactory.createNew( - true, 'answer', 'Html', 'XyzID'); - componentInstance.numberOfIncorrectSubmissions = 3; - componentInstance.triggerIfLearnerStuckActionDirectly(); - - expect(solutionSpy).toHaveBeenCalled(); - expect(redirectionSpy).not.toHaveBeenCalled(); - })); + expect(componentInstance.continueToReviseStateButtonIsVisible).toEqual( + false + ); + componentInstance.nextCardIfStuck = new StateCard( + null, + null, + null, + new Interaction([], [], null, null, [], 'EndExploration', null), + [], + null, + '', + null + ); + componentInstance.triggerIfLearnerStuckAction(); + tick( + ExplorationPlayerConstants.WAIT_BEFORE_RESPONSE_FOR_STUCK_LEARNER_MSEC + ); - it('should immediately direct the learner to the stuck' + - ' when the learner gets stuck and such a state exists', fakeAsync(() => { - spyOn(translateService, 'instant').and.callThrough(); - spyOn(componentInstance, 'showPendingCard'); - spyOn(playerTranscriptService, 'addNewResponseToExistingFeedback'); - expect(componentInstance.continueToReviseStateButtonIsVisible). - toEqual(false); - componentInstance.nextCardIfStuck = new StateCard( - null, null, null, new Interaction( - [], [], null, null, [], 'EndExploration', null), - [], null, '', null); - componentInstance.triggerIfLearnerStuckActionDirectly(); - - expect(translateService.instant).toHaveBeenCalledWith( - 'I18N_REDIRECTION_TO_STUCK_STATE_MESSAGE'); - expect(componentInstance.continueToReviseStateButtonIsVisible). - toEqual(true); - })); + expect(translateService.instant).toHaveBeenCalledWith( + 'I18N_REDIRECTION_TO_STUCK_STATE_MESSAGE' + ); + expect(componentInstance.continueToReviseStateButtonIsVisible).toEqual( + true + ); + flush(); + }) + ); + + it( + 'should immediately release solution when the learner gets stuck' + + ' if no stuck state exists', + fakeAsync(() => { + // Release solution if stuck state is null. + componentInstance.nextCardIfStuck = null; + let solutionSpy = spyOn( + hintsAndSolutionManagerService, + 'releaseSolution' + ); + let redirectionSpy = spyOn(componentInstance, 'showUpcomingCard'); + componentInstance.solutionForState = solutionObjectFactory.createNew( + true, + 'answer', + 'Html', + 'XyzID' + ); + componentInstance.numberOfIncorrectSubmissions = 3; + componentInstance.triggerIfLearnerStuckActionDirectly(); + + expect(solutionSpy).toHaveBeenCalled(); + expect(redirectionSpy).not.toHaveBeenCalled(); + }) + ); + + it( + 'should immediately direct the learner to the stuck' + + ' when the learner gets stuck and such a state exists', + fakeAsync(() => { + spyOn(translateService, 'instant').and.callThrough(); + spyOn(componentInstance, 'showPendingCard'); + spyOn(playerTranscriptService, 'addNewResponseToExistingFeedback'); + expect(componentInstance.continueToReviseStateButtonIsVisible).toEqual( + false + ); + componentInstance.nextCardIfStuck = new StateCard( + null, + null, + null, + new Interaction([], [], null, null, [], 'EndExploration', null), + [], + null, + '', + null + ); + componentInstance.triggerIfLearnerStuckActionDirectly(); + + expect(translateService.instant).toHaveBeenCalledWith( + 'I18N_REDIRECTION_TO_STUCK_STATE_MESSAGE' + ); + expect(componentInstance.continueToReviseStateButtonIsVisible).toEqual( + true + ); + }) + ); it('should redirect the learner to stuck state', fakeAsync(() => { spyOn(componentInstance, 'showPendingCard'); componentInstance.nextCardIfStuck = new StateCard( - null, null, null, new Interaction( - [], [], null, null, [], 'EndExploration', null), - [], null, '', null); + null, + null, + null, + new Interaction([], [], null, null, [], 'EndExploration', null), + [], + null, + '', + null + ); componentInstance.triggerRedirectionToStuckState(); expect(componentInstance.nextCard).toEqual( - componentInstance.nextCardIfStuck); + componentInstance.nextCardIfStuck + ); expect(componentInstance.showPendingCard).toHaveBeenCalled(); })); - it('should fetch completed chapters count if user is logged in', - fakeAsync(() => { - spyOn( - learnerDashboardBackendApiService, - 'fetchLearnerCompletedChaptersCountDataAsync').and.returnValue( - Promise.resolve({ - completedChaptersCount: 1, - })); - componentInstance.isLoggedIn = false; + it('should fetch completed chapters count if user is logged in', fakeAsync(() => { + spyOn( + learnerDashboardBackendApiService, + 'fetchLearnerCompletedChaptersCountDataAsync' + ).and.returnValue( + Promise.resolve({ + completedChaptersCount: 1, + }) + ); + componentInstance.isLoggedIn = false; - componentInstance.fetchCompletedChaptersCount(); - tick(); + componentInstance.fetchCompletedChaptersCount(); + tick(); - expect( - learnerDashboardBackendApiService - .fetchLearnerCompletedChaptersCountDataAsync).not.toHaveBeenCalled(); - expect(componentInstance.completedChaptersCount).toBeUndefined(); + expect( + learnerDashboardBackendApiService.fetchLearnerCompletedChaptersCountDataAsync + ).not.toHaveBeenCalled(); + expect(componentInstance.completedChaptersCount).toBeUndefined(); - componentInstance.isLoggedIn = true; + componentInstance.isLoggedIn = true; - componentInstance.fetchCompletedChaptersCount(); - tick(); + componentInstance.fetchCompletedChaptersCount(); + tick(); - expect( - learnerDashboardBackendApiService - .fetchLearnerCompletedChaptersCountDataAsync).toHaveBeenCalled(); + expect( + learnerDashboardBackendApiService.fetchLearnerCompletedChaptersCountDataAsync + ).toHaveBeenCalled(); - expect(componentInstance.completedChaptersCount).toEqual(1); - })); + expect(componentInstance.completedChaptersCount).toEqual(1); + })); it('should tell if collection allows guest progress', () => { expect(componentInstance.doesCollectionAllowsGuestProgress('')).toBeFalse(); @@ -1190,210 +1433,287 @@ describe('Conversation skin component', () => { spyOn(playerPositionService, 'setDisplayedCardIndex'); spyOn(explorationEngineService.onUpdateActiveStateIfInEditor, 'emit'); spyOn(playerPositionService, 'getCurrentStateName').and.returnValue( - 'state_name'); + 'state_name' + ); spyOn(playerPositionService, 'changeCurrentQuestion'); componentInstance.changeCard(1); - expect(playerPositionService.recordNavigationButtonClick) - .toHaveBeenCalled(); + expect( + playerPositionService.recordNavigationButtonClick + ).toHaveBeenCalled(); expect(playerPositionService.setDisplayedCardIndex).toHaveBeenCalled(); - expect(explorationEngineService.onUpdateActiveStateIfInEditor.emit) - .toHaveBeenCalled(); + expect( + explorationEngineService.onUpdateActiveStateIfInEditor.emit + ).toHaveBeenCalled(); expect(playerPositionService.getCurrentStateName).toHaveBeenCalled(); expect(playerPositionService.changeCurrentQuestion).toHaveBeenCalled(); }); - it('should navigate to the most recently reached checkpoint ' + - 'on page load if user is logged in', fakeAsync(() => { - let stateCardNames = ['Start', 'Mid', 'End']; - let stateCards: StateCard[] = []; - for (let stateName in stateCardNames) { - stateCards.push(new StateCard( - stateName, - '

Testing

', null, new Interaction( - [], [], null, null, [], 'Continue', null), - [], null, 'content', null) - ); - } - let alertMessageElement = document.createElement('div'); - alertMessageElement.className = - 'oppia-exploration-checkpoints-message'; - const expResponse = explorationResponse; - expResponse.exploration.states.Mid.card_is_checkpoint = true; + it( + 'should navigate to the most recently reached checkpoint ' + + 'on page load if user is logged in', + fakeAsync(() => { + let stateCardNames = ['Start', 'Mid', 'End']; + let stateCards: StateCard[] = []; + for (let stateName in stateCardNames) { + stateCards.push( + new StateCard( + stateName, + '

Testing

', + null, + new Interaction([], [], null, null, [], 'Continue', null), + [], + null, + 'content', + null + ) + ); + } + let alertMessageElement = document.createElement('div'); + alertMessageElement.className = 'oppia-exploration-checkpoints-message'; + const expResponse = explorationResponse; + expResponse.exploration.states.Mid.card_is_checkpoint = true; - spyOn(userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(new UserInfo( - [], false, false, - false, false, false, '', '', '', true))); - spyOn(playerPositionService, 'init').and.callFake((callb) => { - callb(); - }); - componentInstance.questionPlayerConfig = {}; - spyOn(explorationPlayerStateService.onPlayerStateChange, 'emit'); - spyOn(playerPositionService.onLoadedMostRecentCheckpoint, 'emit'); - spyOn(focusManagerService, 'setFocusIfOnDesktop'); - spyOn(loaderService, 'hideLoadingScreen'); - spyOn(urlService, 'getPidFromUrl').and.returnValue(null); - spyOn(explorationPlayerStateService, 'getLanguageCode') - .and.returnValues('en', 'en', 'en', 'pq'); - spyOn(explorationPlayerStateService, 'initializeQuestionPlayer') - .and.callFake((config, callb, questionAreAvailable) => { + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve( + new UserInfo([], false, false, false, false, false, '', '', '', true) + ) + ); + spyOn(playerPositionService, 'init').and.callFake(callb => { + callb(); + }); + componentInstance.questionPlayerConfig = {}; + spyOn(explorationPlayerStateService.onPlayerStateChange, 'emit'); + spyOn(playerPositionService.onLoadedMostRecentCheckpoint, 'emit'); + spyOn(focusManagerService, 'setFocusIfOnDesktop'); + spyOn(loaderService, 'hideLoadingScreen'); + spyOn(urlService, 'getPidFromUrl').and.returnValue(null); + spyOn(explorationPlayerStateService, 'getLanguageCode').and.returnValues( + 'en', + 'en', + 'en', + 'pq' + ); + spyOn( + explorationPlayerStateService, + 'initializeQuestionPlayer' + ).and.callFake((config, callb, questionAreAvailable) => { callb(displayedCard, 'label'); }); - spyOn(explorationPlayerStateService, 'isInQuestionPlayerMode') - .and.returnValue(false); - spyOn(componentInstance, 'adjustPageHeight'); - spyOn(playerPositionService.onNewCardOpened, 'emit'); - componentInstance.isIframed = true; - spyOn(playerPositionService, 'setDisplayedCardIndex'); - spyOn(playerPositionService, 'getCurrentStateName') - .and.returnValues('Start', 'Mid', 'End'); - spyOn(playerTranscriptService, 'getNumCards').and.returnValue(0); - spyOn(readOnlyExplorationBackendApiService, 'loadLatestExplorationAsync') - .and.returnValue(Promise.resolve(expResponse)); - spyOn(explorationEngineService, 'getShortestPathToState') - .and.returnValue(['Start', 'Mid']); - - spyOn(explorationEngineService, 'getStateCardByName') - .and.returnValues(stateCards[0], stateCards[1], stateCards[2]); - - spyOn(playerPositionService, 'getDisplayedCardIndex') - .and.returnValue(1); - spyOn(explorationEngineService, 'getState') - .and.returnValue(stateObjectFactory.createFromBackendDict( - 'Mid', expResponse.exploration.states.Mid - )); - spyOn(document, 'querySelector').withArgs( - '.oppia-exploration-checkpoints-message') - .and.returnValue(alertMessageElement); + spyOn( + explorationPlayerStateService, + 'isInQuestionPlayerMode' + ).and.returnValue(false); + spyOn(componentInstance, 'adjustPageHeight'); + spyOn(playerPositionService.onNewCardOpened, 'emit'); + componentInstance.isIframed = true; + spyOn(playerPositionService, 'setDisplayedCardIndex'); + spyOn(playerPositionService, 'getCurrentStateName').and.returnValues( + 'Start', + 'Mid', + 'End' + ); + spyOn(playerTranscriptService, 'getNumCards').and.returnValue(0); + spyOn( + readOnlyExplorationBackendApiService, + 'loadLatestExplorationAsync' + ).and.returnValue(Promise.resolve(expResponse)); + spyOn(explorationEngineService, 'getShortestPathToState').and.returnValue( + ['Start', 'Mid'] + ); - componentInstance.explorationId = expResponse.exploration_id; - componentInstance.displayedCard = displayedCard; - componentInstance.isLoggedIn = true; - componentInstance.isIframed = false; - componentInstance.alertMessageTimeout = 5; + spyOn(explorationEngineService, 'getStateCardByName').and.returnValues( + stateCards[0], + stateCards[1], + stateCards[2] + ); - componentInstance.initializePage(); - tick(100); + spyOn(playerPositionService, 'getDisplayedCardIndex').and.returnValue(1); + spyOn(explorationEngineService, 'getState').and.returnValue( + stateObjectFactory.createFromBackendDict( + 'Mid', + expResponse.exploration.states.Mid + ) + ); + spyOn(document, 'querySelector') + .withArgs('.oppia-exploration-checkpoints-message') + .and.returnValue(alertMessageElement); - expect(componentInstance.prevSessionStatesProgress).toEqual( - ['Start']); - expect(componentInstance.mostRecentlyReachedCheckpoint).toBe('Mid'); - })); + componentInstance.explorationId = expResponse.exploration_id; + componentInstance.displayedCard = displayedCard; + componentInstance.isLoggedIn = true; + componentInstance.isIframed = false; + componentInstance.alertMessageTimeout = 5; - it('should display the exploration after the the progress reminder modal' + - 'has loaded', () => { - spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(false); - spyOn(contextService, 'isInExplorationPlayerPage').and.returnValue(true); - spyOn(urlService, 'getCollectionIdFromExplorationUrl').and.returnValue( - null); - spyOn(urlService, 'getPidFromUrl').and.returnValue(null); - spyOn(explorationEngineService, 'getExplorationId').and.returnValue( - 'expl_1'); - spyOn(explorationEngineService, 'isInPreviewMode').and.returnValue(false); - spyOn(urlService, 'isIframed').and.returnValue(false); + componentInstance.initializePage(); + tick(100); - componentInstance.ngOnInit(); - expect(componentInstance.hasFullyLoaded).toBe(false); - explorationPlayerStateService.onShowProgressModal.emit(); - expect(componentInstance.hasFullyLoaded).toBe(true); - }); + expect(componentInstance.prevSessionStatesProgress).toEqual(['Start']); + expect(componentInstance.mostRecentlyReachedCheckpoint).toBe('Mid'); + }) + ); - it('should determine if chapter was completed for the first time', - fakeAsync(() => { - componentInstance.isLoggedIn = true; - componentInstance.completedChaptersCount = 0; - spyOn(explorationPlayerStateService, 'recordNewCardAdded'); - spyOn(focusManagerService, 'setFocusIfOnDesktop'); - spyOn(componentInstance, 'scrollToTop'); - spyOn(playerPositionService.onNewCardOpened, 'emit'); - spyOn(explorationPlayerStateService, 'getLanguageCode') - .and.returnValue('en'); - spyOn(playerTranscriptService, 'getNumCards').and.returnValue(10); - spyOn(contentTranslationManagerService, 'displayTranslations'); - spyOn(playerPositionService, 'getDisplayedCardIndex').and.returnValue(0); - spyOn(componentInstance, 'canWindowShowTwoCards').and.returnValue(true); - spyOn(playerPositionService, 'setDisplayedCardIndex'); - spyOn(playerPositionService, 'changeCurrentQuestion'); - spyOn(urlService, 'getQueryFieldValuesAsList').and.returnValue(['123']); - spyOn(explorationPlayerStateService, 'isInStoryChapterMode') - .and.returnValue(true); - spyOn(urlService, 'getUrlParams').and.returnValue({ - topic_url_fragment: 'topicUrlFragment', - classroom_url_fragment: 'classroomUrlFragment', - story_url_fragment: 'storyUrlFragment', - node_id: 'nodeId' - }); - spyOn(urlInterpolationService, 'interpolateUrl').and.returnValue('story'); - let readOnlyStoryNode = new ReadOnlyStoryNode( - 'nodeId', '', '', [], [], [], '', false, '', null, false, '', ''); - spyOn(storyViewerBackendApiService, 'fetchStoryDataAsync') - .and.returnValue(Promise.resolve( - new StoryPlaythrough( - 'nodeId', [readOnlyStoryNode, readOnlyStoryNode], '', '', '', '') - )); - spyOn( - learnerDashboardBackendApiService, - 'fetchLearnerCompletedChaptersCountDataAsync').and.returnValue( - Promise.resolve({ - completedChaptersCount: 1, - })); - spyOn(storyViewerBackendApiService, 'recordChapterCompletionAsync') - .and.returnValue(Promise.resolve({ - readyForReviewTest: true, - nextNodeId: '', - summaries: [] - })); + it( + 'should display the exploration after the the progress reminder modal' + + 'has loaded', + () => { + spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(false); + spyOn(contextService, 'isInExplorationPlayerPage').and.returnValue(true); + spyOn(urlService, 'getCollectionIdFromExplorationUrl').and.returnValue( + null + ); + spyOn(urlService, 'getPidFromUrl').and.returnValue(null); + spyOn(explorationEngineService, 'getExplorationId').and.returnValue( + 'expl_1' + ); + spyOn(explorationEngineService, 'isInPreviewMode').and.returnValue(false); + spyOn(urlService, 'isIframed').and.returnValue(false); - componentInstance.displayedCard = new StateCard( - null, null, null, new Interaction( - [], [], null, null, [], 'EndExploration', null), - [], null, '', null); - componentInstance.isLoggedIn = true; - spyOn(componentInstance, 'isSupplementalCardNonempty') - .and.returnValues(false, true, true, false); - spyOn(componentInstance, 'animateToOneCard').and.callFake((callb) => { - callb(); - }); + componentInstance.ngOnInit(); + expect(componentInstance.hasFullyLoaded).toBe(false); + explorationPlayerStateService.onShowProgressModal.emit(); + expect(componentInstance.hasFullyLoaded).toBe(true); + } + ); - componentInstance.showPendingCard(); - tick(1000); + it('should determine if chapter was completed for the first time', fakeAsync(() => { + componentInstance.isLoggedIn = true; + componentInstance.completedChaptersCount = 0; + spyOn(explorationPlayerStateService, 'recordNewCardAdded'); + spyOn(focusManagerService, 'setFocusIfOnDesktop'); + spyOn(componentInstance, 'scrollToTop'); + spyOn(playerPositionService.onNewCardOpened, 'emit'); + spyOn(explorationPlayerStateService, 'getLanguageCode').and.returnValue( + 'en' + ); + spyOn(playerTranscriptService, 'getNumCards').and.returnValue(10); + spyOn(contentTranslationManagerService, 'displayTranslations'); + spyOn(playerPositionService, 'getDisplayedCardIndex').and.returnValue(0); + spyOn(componentInstance, 'canWindowShowTwoCards').and.returnValue(true); + spyOn(playerPositionService, 'setDisplayedCardIndex'); + spyOn(playerPositionService, 'changeCurrentQuestion'); + spyOn(urlService, 'getQueryFieldValuesAsList').and.returnValue(['123']); + spyOn( + explorationPlayerStateService, + 'isInStoryChapterMode' + ).and.returnValue(true); + spyOn(urlService, 'getUrlParams').and.returnValue({ + topic_url_fragment: 'topicUrlFragment', + classroom_url_fragment: 'classroomUrlFragment', + story_url_fragment: 'storyUrlFragment', + node_id: 'nodeId', + }); + spyOn(urlInterpolationService, 'interpolateUrl').and.returnValue('story'); + let readOnlyStoryNode = new ReadOnlyStoryNode( + 'nodeId', + '', + '', + [], + [], + [], + '', + false, + '', + null, + false, + '', + '' + ); + spyOn(storyViewerBackendApiService, 'fetchStoryDataAsync').and.returnValue( + Promise.resolve( + new StoryPlaythrough( + 'nodeId', + [readOnlyStoryNode, readOnlyStoryNode], + '', + '', + '', + '' + ) + ) + ); + spyOn( + learnerDashboardBackendApiService, + 'fetchLearnerCompletedChaptersCountDataAsync' + ).and.returnValue( + Promise.resolve({ + completedChaptersCount: 1, + }) + ); + spyOn( + storyViewerBackendApiService, + 'recordChapterCompletionAsync' + ).and.returnValue( + Promise.resolve({ + readyForReviewTest: true, + nextNodeId: '', + summaries: [], + }) + ); - expect(componentInstance.chapterIsCompletedForTheFirstTime).toBe(true); - expect(componentInstance.completedChaptersCount).toBe(1); + componentInstance.displayedCard = new StateCard( + null, + null, + null, + new Interaction([], [], null, null, [], 'EndExploration', null), + [], + null, + '', + null + ); + componentInstance.isLoggedIn = true; + spyOn(componentInstance, 'isSupplementalCardNonempty').and.returnValues( + false, + true, + true, + false + ); + spyOn(componentInstance, 'animateToOneCard').and.callFake(callb => { + callb(); + }); - componentInstance.completedChaptersCount = 1; - componentInstance.chapterIsCompletedForTheFirstTime = false; + componentInstance.showPendingCard(); + tick(1000); - componentInstance.showPendingCard(); - tick(1000); + expect(componentInstance.chapterIsCompletedForTheFirstTime).toBe(true); + expect(componentInstance.completedChaptersCount).toBe(1); - expect(componentInstance.chapterIsCompletedForTheFirstTime).toBe(false); + componentInstance.completedChaptersCount = 1; + componentInstance.chapterIsCompletedForTheFirstTime = false; - flush(); - })); + componentInstance.showPendingCard(); + tick(1000); + + expect(componentInstance.chapterIsCompletedForTheFirstTime).toBe(false); + + flush(); + })); it('should unsubscribe on destroy', () => { spyOn(componentInstance.directiveSubscriptions, 'unsubscribe'); componentInstance.ngOnDestroy(); - expect(componentInstance.directiveSubscriptions.unsubscribe) - .toHaveBeenCalled(); + expect( + componentInstance.directiveSubscriptions.unsubscribe + ).toHaveBeenCalled(); }); it('should always ask learner for answer details', () => { - spyOn(explorationEngineService, 'getAlwaysAskLearnerForAnswerDetails') - .and.returnValues(true, false); + spyOn( + explorationEngineService, + 'getAlwaysAskLearnerForAnswerDetails' + ).and.returnValues(true, false); expect(componentInstance.alwaysAskLearnerForAnswerDetails()).toBeTrue(); expect(componentInstance.alwaysAskLearnerForAnswerDetails()).toBeFalse(); }); it('should get can ask learner for answer info', () => { - spyOn(learnerAnswerInfoService, 'getCanAskLearnerForAnswerInfo') - .and.returnValues(true, false); + spyOn( + learnerAnswerInfoService, + 'getCanAskLearnerForAnswerInfo' + ).and.returnValues(true, false); expect(componentInstance.getCanAskLearnerForAnswerInfo()).toBeTrue(); expect(componentInstance.getCanAskLearnerForAnswerInfo()).toBeFalse(); @@ -1403,17 +1723,25 @@ describe('Conversation skin component', () => { spyOn(learnerAnswerInfoService, 'initLearnerAnswerInfoService'); componentInstance.initLearnerAnswerInfoService( - null, null, null, null, false); + null, + null, + null, + null, + false + ); - expect(learnerAnswerInfoService.initLearnerAnswerInfoService) - .toHaveBeenCalled(); + expect( + learnerAnswerInfoService.initLearnerAnswerInfoService + ).toHaveBeenCalled(); }); it('should tell if correctness footer is enabled', () => { componentInstance.answerIsCorrect = true; - spyOn(playerPositionService, 'hasLearnerJustSubmittedAnAnswer') - .and.returnValue(true); + spyOn( + playerPositionService, + 'hasLearnerJustSubmittedAnAnswer' + ).and.returnValue(true); expect(componentInstance.isCorrectnessFooterEnabled()).toBeTrue(); }); @@ -1421,7 +1749,8 @@ describe('Conversation skin component', () => { it('should get static image url', () => { let imageUrl = 'image_url'; spyOn(urlInterpolationService, 'getStaticImageUrl').and.returnValue( - imageUrl); + imageUrl + ); expect(componentInstance.getStaticImageUrl('')).toEqual(imageUrl); }); @@ -1430,7 +1759,8 @@ describe('Conversation skin component', () => { let index = 1; expect(componentInstance.getContentFocusLabel(index)).toEqual( - ExplorationPlayerConstants.CONTENT_FOCUS_LABEL_PREFIX + index); + ExplorationPlayerConstants.CONTENT_FOCUS_LABEL_PREFIX + index + ); }); it('should reload exploration', () => { @@ -1443,16 +1773,23 @@ describe('Conversation skin component', () => { it('should tell if display card is terminal', () => { componentInstance.displayedCard = new StateCard( - null, null, null, new Interaction( - [], [], null, null, [], 'EndExploration', null), - [], null, '', null); + null, + null, + null, + new Interaction([], [], null, null, [], 'EndExploration', null), + [], + null, + '', + null + ); expect(componentInstance.isOnTerminalCard()).toBeTrue(); }); it('should tell if supplemental card is non empty', () => { - expect(componentInstance.isSupplementalCardNonempty(displayedCard)) - .toBeFalse(); + expect( + componentInstance.isSupplementalCardNonempty(displayedCard) + ).toBeFalse(); }); it('should return to exploration after concept card is compeleted', () => { @@ -1465,15 +1802,17 @@ describe('Conversation skin component', () => { expect(playerTranscriptService.addPreviousCard).toHaveBeenCalled(); expect(playerTranscriptService.getNumCards).toHaveBeenCalled(); - expect(playerPositionService.setDisplayedCardIndex) - .toHaveBeenCalledOnceWith(numCards - 1); + expect( + playerPositionService.setDisplayedCardIndex + ).toHaveBeenCalledOnceWith(numCards - 1); }); it('should tell if current is at end of transcript', () => { let index = 1; spyOn(playerTranscriptService, 'isLastCard').and.returnValue(true); spyOn(playerPositionService, 'getDisplayedCardIndex').and.returnValue( - index); + index + ); expect(componentInstance.isCurrentCardAtEndOfTranscript()).toBeTrue(); expect(playerTranscriptService.isLastCard).toHaveBeenCalledWith(index); @@ -1488,32 +1827,44 @@ describe('Conversation skin component', () => { }); it('should initialize page', fakeAsync(() => { - spyOn(playerPositionService, 'init').and.callFake((callb) => { + spyOn(playerPositionService, 'init').and.callFake(callb => { callb(); }); - spyOn(urlService, 'getUrlParams').and.returnValues({ - lang: 'pq' - }, { - lang: 'en' - }, { - lang: 'en' - }, { - lang: 'pq' - }); + spyOn(urlService, 'getUrlParams').and.returnValues( + { + lang: 'pq', + }, + { + lang: 'en', + }, + { + lang: 'en', + }, + { + lang: 'pq', + } + ); spyOn(userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(new UserInfo( - [], false, false, - false, false, false, '', '', '', true))); + Promise.resolve( + new UserInfo([], false, false, false, false, false, '', '', '', true) + ) + ); componentInstance.questionPlayerConfig = {}; spyOn(explorationPlayerStateService.onPlayerStateChange, 'emit'); spyOn(focusManagerService, 'setFocusIfOnDesktop'); spyOn(loaderService, 'hideLoadingScreen'); - spyOn(explorationPlayerStateService, 'getLanguageCode') - .and.returnValues('pq', 'en', 'en', 'pq'); - spyOn(explorationPlayerStateService, 'initializeQuestionPlayer') - .and.callFake((config, callb, questionAreAvailable) => { - callb(displayedCard, 'label'); - }); + spyOn(explorationPlayerStateService, 'getLanguageCode').and.returnValues( + 'pq', + 'en', + 'en', + 'pq' + ); + spyOn( + explorationPlayerStateService, + 'initializeQuestionPlayer' + ).and.callFake((config, callb, questionAreAvailable) => { + callb(displayedCard, 'label'); + }); spyOn(componentInstance, 'adjustPageHeight'); spyOn(playerPositionService.onNewCardOpened, 'emit'); componentInstance.isIframed = true; @@ -1537,10 +1888,11 @@ describe('Conversation skin component', () => { spyOn(autogeneratedAudioPlayerService, 'cancel'); spyOn(playerTranscriptService, 'isLastCard').and.returnValues(true, false); spyOn(componentInstance, 'getContentFocusLabel'); - spyOn(explorationPlayerStateService, 'initializePlayer') - .and.callFake((callb) => { + spyOn(explorationPlayerStateService, 'initializePlayer').and.callFake( + callb => { callb(displayedCard, 'label'); - }); + } + ); componentInstance._nextFocusLabel = 'focus_label'; componentInstance.initializePage(); @@ -1550,11 +1902,11 @@ describe('Conversation skin component', () => { let topicIdToPrerequisiteTopicIds = { topicId1: [], topicId2: ['topicId1'], - topicId3: ['topicId2'] + topicId3: ['topicId2'], }; - componentInstance.diagnosticTestTopicTrackerModel = ( - new DiagnosticTestTopicTrackerModel(topicIdToPrerequisiteTopicIds)); + componentInstance.diagnosticTestTopicTrackerModel = + new DiagnosticTestTopicTrackerModel(topicIdToPrerequisiteTopicIds); spyOn(explorationPlayerStateService, 'initializeDiagnosticPlayer'); @@ -1562,13 +1914,15 @@ describe('Conversation skin component', () => { tick(100); expect(playerPositionService.init).toHaveBeenCalled(); - expect(explorationPlayerStateService.initializeDiagnosticPlayer) - .toHaveBeenCalled(); + expect( + explorationPlayerStateService.initializeDiagnosticPlayer + ).toHaveBeenCalled(); })); it('should tell if window can show two cards', () => { spyOn(windowDimensionsService, 'getWidth').and.returnValue( - ExplorationPlayerConstants.TWO_CARD_THRESHOLD_PX + 1); + ExplorationPlayerConstants.TWO_CARD_THRESHOLD_PX + 1 + ); expect(componentInstance.canWindowShowTwoCards()).toBeTrue(); }); @@ -1578,8 +1932,9 @@ describe('Conversation skin component', () => { componentInstance.onNavigateFromIframe(); - expect(siteAnalyticsService.registerVisitOppiaFromIframeEvent) - .toHaveBeenCalled(); + expect( + siteAnalyticsService.registerVisitOppiaFromIframeEvent + ).toHaveBeenCalled(); }); it('should submit answer from progress nav and toggle submit clicked', () => { @@ -1596,11 +1951,15 @@ describe('Conversation skin component', () => { it('should show learn again button', () => { componentInstance.displayedCard = { - getStateName: () => null + getStateName: () => null, } as StateCard; spyOn(explorationPlayerStateService, 'isInQuestionMode').and.returnValues( - false, true, true, true); + false, + true, + true, + true + ); expect(componentInstance.isLearnAgainButton()).toBeFalse(); @@ -1609,16 +1968,28 @@ describe('Conversation skin component', () => { expect(componentInstance.isLearnAgainButton()).toBeFalse(); componentInstance.displayedCard = new StateCard( - null, null, null, new Interaction( - [], [], null, null, [], 'Continue', null), - [], null, '', null); + null, + null, + null, + new Interaction([], [], null, null, [], 'Continue', null), + [], + null, + '', + null + ); expect(componentInstance.isLearnAgainButton()).toBeFalse(); componentInstance.displayedCard = new StateCard( - null, null, null, new Interaction( - [], [], null, null, [], 'ImageClickInput', null), - [], null, '', null); + null, + null, + null, + new Interaction([], [], null, null, [], 'ImageClickInput', null), + [], + null, + '', + null + ); componentInstance.pendingCardWasSeenBefore = true; componentInstance.answerIsCorrect = false; @@ -1628,21 +1999,36 @@ describe('Conversation skin component', () => { it('should jump to the revision state via changing card', () => { const currentCard = new StateCard( - 'currentCard', null, null, new Interaction( - [], [], null, null, [], 'Continue', null), - [], null, '', null); + 'currentCard', + null, + null, + new Interaction([], [], null, null, [], 'Continue', null), + [], + null, + '', + null + ); componentInstance.displayedCard = currentCard; componentInstance.nextCard = new StateCard( - 'revisionState', null, null, new Interaction( - [], [], null, null, [], 'ImageClickInput', null), - [], null, '', null); + 'revisionState', + null, + null, + new Interaction([], [], null, null, [], 'ImageClickInput', null), + [], + null, + '', + null + ); spyOn(componentInstance, 'isLearnAgainButton').and.returnValue(true); spyOn(playerTranscriptService, 'findIndexOfLatestStateWithName') - .withArgs('revisionState').and.returnValue(2); + .withArgs('revisionState') + .and.returnValue(2); const changeCard = spyOn(componentInstance, 'changeCard'); const recordNewCardAdded = spyOn( - explorationPlayerStateService, 'recordNewCardAdded'); + explorationPlayerStateService, + 'recordNewCardAdded' + ); componentInstance.showUpcomingCard(); @@ -1678,57 +2064,88 @@ describe('Conversation skin component', () => { let nodeId = 'node_id'; let topicUrlFragment = 'topic_url_fragment'; let classroomUrlFragment = 'classroom_fragment'; - componentInstance.recommendedExplorationSummaries = [{ - id: expId, - parentExplorationIds: ['4566', 's9af0'], - nextNodeId: nodeId - }]; - spyOn(urlService, 'getUrlParams').and.returnValues({ - collection_id: collectionId - }, { - story_url_fragment: storyUrlFragment, - node_id: nodeId, - topic_url_fragment: topicUrlFragment, - classroom_url_fragment: classroomUrlFragment - }, { - story_url_fragment: storyUrlFragment, - node_id: nodeId, - topic_url_fragment: topicUrlFragment, - classroom_url_fragment: classroomUrlFragment - }); + componentInstance.recommendedExplorationSummaries = [ + { + id: expId, + parentExplorationIds: ['4566', 's9af0'], + nextNodeId: nodeId, + }, + ]; + spyOn(urlService, 'getUrlParams').and.returnValues( + { + collection_id: collectionId, + }, + { + story_url_fragment: storyUrlFragment, + node_id: nodeId, + topic_url_fragment: topicUrlFragment, + classroom_url_fragment: classroomUrlFragment, + }, + { + story_url_fragment: storyUrlFragment, + node_id: nodeId, + topic_url_fragment: topicUrlFragment, + classroom_url_fragment: classroomUrlFragment, + } + ); expect(componentInstance.getExplorationLink()).toEqual( - '/explore/' + expId + '?collection_id=' + collectionId + - '&parent=' + componentInstance.recommendedExplorationSummaries[0] - .parentExplorationIds[0]); + '/explore/' + + expId + + '?collection_id=' + + collectionId + + '&parent=' + + componentInstance.recommendedExplorationSummaries[0] + .parentExplorationIds[0] + ); expect(urlService.getUrlParams).toHaveBeenCalled(); componentInstance.parentExplorationIds = null; componentInstance.storyNodeIdToAdd = nodeId; expect(componentInstance.getExplorationLink()).toEqual( - '/explore/' + expId + '?parent=' + - componentInstance.recommendedExplorationSummaries[0] - .parentExplorationIds[0] + '&topic_url_fragment=' + topicUrlFragment + - '&classroom_url_fragment=' + classroomUrlFragment + - '&story_url_fragment=' + storyUrlFragment + '&node_id=' + nodeId + '/explore/' + + expId + + '?parent=' + + componentInstance.recommendedExplorationSummaries[0] + .parentExplorationIds[0] + + '&topic_url_fragment=' + + topicUrlFragment + + '&classroom_url_fragment=' + + classroomUrlFragment + + '&story_url_fragment=' + + storyUrlFragment + + '&node_id=' + + nodeId ); spyOn(urlService, 'getPathname').and.returnValue( - '/story/story-url-fragment'); + '/story/story-url-fragment' + ); spyOn(urlService, 'getStoryUrlFragmentFromLearnerUrl').and.returnValue( - storyUrlFragment); + storyUrlFragment + ); spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue( - topicUrlFragment); + topicUrlFragment + ); spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl').and.returnValue( - classroomUrlFragment); + classroomUrlFragment + ); expect(componentInstance.getExplorationLink()).toEqual( - '/explore/' + expId + '?parent=' + - componentInstance.recommendedExplorationSummaries[0] - .parentExplorationIds[0] + '&topic_url_fragment=' + topicUrlFragment + - '&classroom_url_fragment=' + classroomUrlFragment + - '&story_url_fragment=' + storyUrlFragment + '&node_id=' + nodeId + '/explore/' + + expId + + '?parent=' + + componentInstance.recommendedExplorationSummaries[0] + .parentExplorationIds[0] + + '&topic_url_fragment=' + + topicUrlFragment + + '&classroom_url_fragment=' + + classroomUrlFragment + + '&story_url_fragment=' + + storyUrlFragment + + '&node_id=' + + nodeId ); expect(urlService.getPathname).toHaveBeenCalled(); expect(urlService.getStoryUrlFragmentFromLearnerUrl).toHaveBeenCalled(); @@ -1739,7 +2156,9 @@ describe('Conversation skin component', () => { it('should tell if current supplemental card is non empty', () => { componentInstance.displayedCard = displayedCard; spyOn(componentInstance, 'isSupplementalCardNonempty').and.returnValues( - true, false); + true, + false + ); expect(componentInstance.isCurrentSupplementalCardNonempty()).toBeTrue(); expect(componentInstance.isCurrentSupplementalCardNonempty()).toBeFalse(); @@ -1747,13 +2166,22 @@ describe('Conversation skin component', () => { it('should tell if supplemental nav is shown', () => { componentInstance.displayedCard = new StateCard( - null, null, null, new Interaction( - [], [], null, null, [], 'NumberWithUnits', null), - [], null, '', null); + null, + null, + null, + new Interaction([], [], null, null, [], 'NumberWithUnits', null), + [], + null, + '', + null + ); spyOn(explorationPlayerStateService, 'isInQuestionMode').and.returnValues( - false, true); - spyOn(componentInstance, 'isCurrentCardAtEndOfTranscript') - .and.returnValue(true); + false, + true + ); + spyOn(componentInstance, 'isCurrentCardAtEndOfTranscript').and.returnValue( + true + ); expect(componentInstance.isSupplementalNavShown()).toBeFalse(); expect(componentInstance.isSupplementalNavShown()).toBeTrue(); }); @@ -1781,8 +2209,9 @@ describe('Conversation skin component', () => { spyOn(focusManagerService, 'setFocusIfOnDesktop'); spyOn(componentInstance, 'scrollToTop'); spyOn(playerPositionService.onNewCardOpened, 'emit'); - spyOn(explorationPlayerStateService, 'getLanguageCode') - .and.returnValue('en'); + spyOn(explorationPlayerStateService, 'getLanguageCode').and.returnValue( + 'en' + ); spyOn(playerTranscriptService, 'getNumCards').and.returnValue(10); spyOn(contentTranslationManagerService, 'displayTranslations'); spyOn(playerPositionService, 'getDisplayedCardIndex').and.returnValue(0); @@ -1790,39 +2219,75 @@ describe('Conversation skin component', () => { spyOn(playerPositionService, 'setDisplayedCardIndex'); spyOn(playerPositionService, 'changeCurrentQuestion'); spyOn(urlService, 'getQueryFieldValuesAsList').and.returnValue(['123']); - spyOn(explorationPlayerStateService, 'isInStoryChapterMode') - .and.returnValue(true); + spyOn( + explorationPlayerStateService, + 'isInStoryChapterMode' + ).and.returnValue(true); spyOn(urlService, 'getUrlParams').and.returnValue({ topic_url_fragment: 'topicUrlFragment', classroom_url_fragment: 'classroomUrlFragment', story_url_fragment: 'storyUrlFragment', - node_id: 'nodeId' + node_id: 'nodeId', }); spyOn(urlInterpolationService, 'interpolateUrl').and.returnValue('story'); let readOnlyStoryNode = new ReadOnlyStoryNode( - 'nodeId', '', '', [], [], [], '', false, '', null, false, '', ''); - spyOn(storyViewerBackendApiService, 'fetchStoryDataAsync') - .and.returnValue(Promise.resolve( + 'nodeId', + '', + '', + [], + [], + [], + '', + false, + '', + null, + false, + '', + '' + ); + spyOn(storyViewerBackendApiService, 'fetchStoryDataAsync').and.returnValue( + Promise.resolve( new StoryPlaythrough( - 'nodeId', [readOnlyStoryNode, readOnlyStoryNode], '', '', '', '') - )); - spyOn(storyViewerBackendApiService, 'recordChapterCompletionAsync') - .and.returnValues(Promise.resolve({ + 'nodeId', + [readOnlyStoryNode, readOnlyStoryNode], + '', + '', + '', + '' + ) + ) + ); + spyOn( + storyViewerBackendApiService, + 'recordChapterCompletionAsync' + ).and.returnValues( + Promise.resolve({ readyForReviewTest: true, nextNodeId: '', - summaries: [] - })); + summaries: [], + }) + ); componentInstance.alertMessageTimeout = 5; componentInstance.displayedCard = new StateCard( - null, null, null, new Interaction( - [], [], null, null, [], 'EndExploration', null), - [], null, '', null); + null, + null, + null, + new Interaction([], [], null, null, [], 'EndExploration', null), + [], + null, + '', + null + ); componentInstance.isLoggedIn = true; - spyOn(componentInstance, 'isSupplementalCardNonempty') - .and.returnValues(false, true, true, false); - spyOn(componentInstance, 'animateToOneCard').and.callFake((callb) => { + spyOn(componentInstance, 'isSupplementalCardNonempty').and.returnValues( + false, + true, + true, + false + ); + spyOn(componentInstance, 'animateToOneCard').and.callFake(callb => { callb(); }); @@ -1848,7 +2313,7 @@ describe('Conversation skin component', () => { outerHeight: () => 10, scrollTop: () => 0, height: () => 0, - animate: () => {} + animate: () => {}, } as unknown as JQLite); componentInstance.scrollToBottom(); @@ -1859,40 +2324,53 @@ describe('Conversation skin component', () => { it('should scroll to top', fakeAsync(() => { let animateSpy = jasmine.createSpy('jquery spy'); spyOn(window, '$').and.returnValue({ - animate: animateSpy + animate: animateSpy, } as unknown as JQLite); componentInstance.scrollToTop(); tick(1000); expect(animateSpy).toHaveBeenCalled(); })); - it('should determine if endChapterCelebrationFeature is enabled or not', - () => { - const featureSpy = ( - spyOnProperty(platformFeatureService, 'status', 'get') - .and.callThrough()); + it('should determine if endChapterCelebrationFeature is enabled or not', () => { + const featureSpy = spyOnProperty( + platformFeatureService, + 'status', + 'get' + ).and.callThrough(); - expect(componentInstance.isEndChapterCelebrationFeatureEnabled()) - .toBe(true); - - featureSpy.and.returnValue({ - EndChapterCelebration: { - isEnabled: false - } - }); + expect(componentInstance.isEndChapterCelebrationFeatureEnabled()).toBe( + true + ); - expect(componentInstance.isEndChapterCelebrationFeatureEnabled()) - .toBe(false); + featureSpy.and.returnValue({ + EndChapterCelebration: { + isEnabled: false, + }, }); + expect(componentInstance.isEndChapterCelebrationFeatureEnabled()).toBe( + false + ); + }); + it('should show upcoming card', () => { spyOn(playerPositionService, 'getDisplayedCardIndex').and.returnValue(0); spyOn(displayedCard, 'getStateName').and.returnValue(null); componentInstance.displayedCard = displayedCard; spyOn(explorationPlayerStateService, 'isInQuestionMode').and.returnValues( - false, true, true, true, true); - spyOn(playerTranscriptService, 'isLastCard') - .and.returnValues(true, false, false, false, false); + false, + true, + true, + true, + true + ); + spyOn(playerTranscriptService, 'isLastCard').and.returnValues( + true, + false, + false, + false, + false + ); spyOn(componentInstance, 'returnToExplorationAfterConceptCard'); componentInstance.showUpcomingCard(); @@ -1911,44 +2389,62 @@ describe('Conversation skin component', () => { componentInstance.moveToExploration = false; let stateCard = new StateCard( - 'stateName', null, null, new Interaction( - [], [], null, null, [], 'EndExploration', null), - [], null, '', null); + 'stateName', + null, + null, + new Interaction([], [], null, null, [], 'EndExploration', null), + [], + null, + '', + null + ); stateCard.markAsCompleted(); componentInstance.displayedCard = stateCard; componentInstance.nextCard = stateCard; componentInstance.conceptCard = new ConceptCard( - new SubtitledHtml('', ''), [], null); + new SubtitledHtml('', ''), + [], + null + ); spyOn(explorationPlayerStateService, 'recordNewCardAdded'); spyOn(playerTranscriptService, 'addNewCard'); - spyOn(explorationPlayerStateService, 'getLanguageCode') - .and.returnValue('en'); - spyOn(contentTranslationLanguageService, 'getCurrentContentLanguageCode') - .and.returnValue('en'); + spyOn(explorationPlayerStateService, 'getLanguageCode').and.returnValue( + 'en' + ); + spyOn( + contentTranslationLanguageService, + 'getCurrentContentLanguageCode' + ).and.returnValue('en'); spyOn(contentTranslationManagerService, 'displayTranslations'); spyOn(playerTranscriptService, 'getNumCards').and.returnValue(10); - spyOn(componentInstance, 'isSupplementalCardNonempty') - .and.returnValues(false, true); + spyOn(componentInstance, 'isSupplementalCardNonempty').and.returnValues( + false, + true + ); spyOn(playerTranscriptService, 'getCard'); spyOn(componentInstance, 'canWindowShowTwoCards').and.returnValue(true); spyOn(playerPositionService, 'setDisplayedCardIndex'); - spyOn(componentInstance, 'animateToTwoCards').and.callFake((callb) => { + spyOn(componentInstance, 'animateToTwoCards').and.callFake(callb => { callb(); }); spyOn(playerPositionService, 'changeCurrentQuestion'); spyOn(componentInstance, 'showPendingCard'); spyOn(urlService, 'getQueryFieldValuesAsList').and.returnValue([]); - spyOn(explorationEngineService, 'getAuthorRecommendedExpIdsByStateName') - .and.returnValue([]); - spyOn(explorationPlayerStateService, 'isInStoryChapterMode') - .and.returnValue(true); + spyOn( + explorationEngineService, + 'getAuthorRecommendedExpIdsByStateName' + ).and.returnValue([]); + spyOn( + explorationPlayerStateService, + 'isInStoryChapterMode' + ).and.returnValue(true); spyOn(userService, 'setReturnUrl'); spyOn(urlService, 'getUrlParams').and.returnValue({ topic_url_fragment: 'topicUrlFragment', classroom_url_fragment: 'classroomUrlFragment', story_url_fragment: 'storyUrlFragment', - node_id: 'nodeId' + node_id: 'nodeId', }); componentInstance.isLoggedIn = false; @@ -1972,14 +2468,19 @@ describe('Conversation skin component', () => { componentInstance.answerIsBeingProcessed = false; spyOn(explorationEngineService, 'getLanguageCode').and.returnValue('en'); spyOn(componentInstance, 'isCurrentCardAtEndOfTranscript').and.returnValue( - true); + true + ); let explorationModeSpy = spyOn( - explorationPlayerStateService, 'isPresentingIsolatedQuestions'); + explorationPlayerStateService, + 'isPresentingIsolatedQuestions' + ); explorationModeSpy.and.returnValue(false); componentInstance.isInPreviewMode = false; spyOn(fatigueDetectionService, 'recordSubmissionTimestamp'); spyOn(fatigueDetectionService, 'isSubmittingTooFast').and.returnValues( - true, false); + true, + false + ); spyOn(fatigueDetectionService, 'displayTakeBreakMessage'); let lastCardInteraction = interactionObjectFactory.createFromBackendDict({ id: 'TextInput', @@ -1992,27 +2493,39 @@ describe('Conversation skin component', () => { }, placeholder: { value: 1, - } + }, }, hints: [], - solution: null + solution: null, }); let lastCard = StateCard.createNewCard( - 'Card 1', 'Content html', 'Interaction text', lastCardInteraction, - null, 'content_id', audioTranslationLanguageService); + 'Card 1', + 'Content html', + 'Interaction text', + lastCardInteraction, + null, + 'content_id', + audioTranslationLanguageService + ); spyOn(playerTranscriptService, 'getLastCard').and.returnValue(lastCard); spyOn(explorationPlayerStateService.onOppiaFeedbackAvailable, 'emit'); spyOn(componentInstance, 'showPendingCard'); componentInstance.submitAnswer('', null); - spyOn(explorationPlayerStateService, 'isInQuestionMode') - .and.returnValues(false, false, false, true); + spyOn(explorationPlayerStateService, 'isInQuestionMode').and.returnValues( + false, + false, + false, + true + ); spyOn(componentInstance, 'initLearnerAnswerInfoService'); spyOn(explorationEngineService, 'getState'); spyOn(numberAttemptsService, 'submitAttempt'); spyOn(playerTranscriptService, 'addNewInput'); spyOn(componentInstance, 'getCanAskLearnerForAnswerInfo').and.returnValues( - true, false); + true, + false + ); spyOn(playerTranscriptService, 'addNewResponse'); spyOn(learnerAnswerInfoService, 'getSolicitAnswerDetailsQuestion'); spyOn(playerPositionService.onHelpCardAvailable, 'emit'); @@ -2022,14 +2535,18 @@ describe('Conversation skin component', () => { tick(200); spyOn(playerPositionService, 'recordAnswerSubmission'); - spyOn(explorationPlayerStateService, 'getCurrentEngineService') - .and.returnValue(explorationEngineService); - spyOn(explorationPlayerStateService, 'getLanguageCode') - .and.returnValue('en'); + spyOn( + explorationPlayerStateService, + 'getCurrentEngineService' + ).and.returnValue(explorationEngineService); + spyOn(explorationPlayerStateService, 'getLanguageCode').and.returnValue( + 'en' + ); let callback = ( - answer: string, interactionRulesService: InteractionRulesService, - successCallback: ( + answer: string, + interactionRulesService: InteractionRulesService, + successCallback: ( nextCard: StateCard, refreshInteraction: boolean, feedbackHtml: string, @@ -2046,81 +2563,195 @@ describe('Conversation skin component', () => { ) => void ) => { let stateCard = new StateCard( - null, null, null, new Interaction( - [], [], null, null, [], 'EndExploration', null), - [], null, '', null); + null, + null, + null, + new Interaction([], [], null, null, [], 'EndExploration', null), + [], + null, + '', + null + ); successCallback( - stateCard, true, 'feedback', null, 'refresherId', '', false, '', true, - false, true, null, ''); + stateCard, + true, + 'feedback', + null, + 'refresherId', + '', + false, + '', + true, + false, + true, + null, + '' + ); successCallback( - stateCard, true, '', null, 'refresherId', '', false, '', true, - false, true, null, ''); + stateCard, + true, + '', + null, + 'refresherId', + '', + false, + '', + true, + false, + true, + null, + '' + ); successCallback( - stateCard, true, 'feedback', null, 'refresherId', '', false, '', true, - false, false, null, ''); + stateCard, + true, + 'feedback', + null, + 'refresherId', + '', + false, + '', + true, + false, + false, + null, + '' + ); successCallback( - stateCard, true, '', null, 'refresherId', '', false, '', true, - false, false, null, ''); + stateCard, + true, + '', + null, + 'refresherId', + '', + false, + '', + true, + false, + false, + null, + '' + ); successCallback( - stateCard, true, 'feedback', null, '', 'skill_id', true, '', true, - false, false, null, ''); + stateCard, + true, + 'feedback', + null, + '', + 'skill_id', + true, + '', + true, + false, + false, + null, + '' + ); explorationModeSpy.and.returnValue(true); componentInstance.displayedCard = new StateCard( - null, null, null, new Interaction( - [], [], null, null, [], 'TextInput', null), - [], null, '', null); - spyOn(explorationPlayerStateService, 'isInDiagnosticTestPlayerMode') - .and.returnValue(true); + null, + null, + null, + new Interaction([], [], null, null, [], 'TextInput', null), + [], + null, + '', + null + ); + spyOn( + explorationPlayerStateService, + 'isInDiagnosticTestPlayerMode' + ).and.returnValue(true); successCallback( - stateCard, true, 'feedback', null, '', 'skill_id', true, '', true, - false, false, null, ''); + stateCard, + true, + 'feedback', + null, + '', + 'skill_id', + true, + '', + true, + false, + false, + null, + '' + ); componentInstance.displayedCard = new StateCard( - null, null, null, new Interaction( - [], [], null, null, [], 'ImageClickInput', null), - [], null, '', null); + null, + null, + null, + new Interaction([], [], null, null, [], 'ImageClickInput', null), + [], + null, + '', + null + ); explorationModeSpy.and.returnValue(false); successCallback( - stateCard, true, 'feedback', null, 'refresherId', 'skill_id', true, - '', true, false, false, null, ''); + stateCard, + true, + 'feedback', + null, + 'refresherId', + 'skill_id', + true, + '', + true, + false, + false, + null, + '' + ); return false; }; - spyOn(answerClassificationService, 'isAnswerOnlyMisspelled'). - and.returnValue(true); + spyOn( + answerClassificationService, + 'isAnswerOnlyMisspelled' + ).and.returnValue(true); spyOn(explorationEngineService, 'submitAnswer').and.callFake(callback); - spyOn(playerPositionService, 'getCurrentStateName') - .and.returnValue('oldState'); + spyOn(playerPositionService, 'getCurrentStateName').and.returnValue( + 'oldState' + ); spyOn(statsReportingService, 'recordStateTransition'); spyOn(learnerParamsService, 'getAllParams'); spyOn(statsReportingService, 'recordStateCompleted'); spyOn(statsReportingService, 'recordExplorationActuallyStarted'); - spyOn(explorationPlayerStateService, 'isInQuestionPlayerMode') - .and.returnValue(true); + spyOn( + explorationPlayerStateService, + 'isInQuestionPlayerMode' + ).and.returnValue(true); spyOn(componentInstance, 'showUpcomingCard'); spyOn(fatigueDetectionService, 'reset'); spyOn(numberAttemptsService, 'reset'); spyOn(questionPlayerStateService, 'answerSubmitted'); spyOn(questionPlayerEngineService, 'getCurrentQuestion'); spyOn(playerTranscriptService, 'updateLatestInteractionHtml'); - spyOn(conceptCardBackendApiService, 'loadConceptCardsAsync') - .and.returnValue(Promise.resolve([ - new ConceptCard( - new SubtitledHtml('', ''), [], null) - ])); + spyOn( + conceptCardBackendApiService, + 'loadConceptCardsAsync' + ).and.returnValue( + Promise.resolve([new ConceptCard(new SubtitledHtml('', ''), [], null)]) + ); spyOn( explorationSummaryBackendApiService, - 'loadPublicExplorationSummariesAsync') - .and.returnValue(Promise.resolve({ - summaries: [{} as ExplorationSummaryDict] - })); + 'loadPublicExplorationSummariesAsync' + ).and.returnValue( + Promise.resolve({ + summaries: [{} as ExplorationSummaryDict], + }) + ); spyOn( refresherExplorationConfirmationModalService, - 'displayRedirectConfirmationModal').and.callFake((id, callb) => { + 'displayRedirectConfirmationModal' + ).and.callFake((id, callb) => { callb(); }); spyOn(statsReportingService, 'recordLeaveForRefresherExp'); - spyOn(playerTranscriptService, 'hasEncounteredStateBefore') - .and.returnValue(true); + spyOn(playerTranscriptService, 'hasEncounteredStateBefore').and.returnValue( + true + ); spyOn(explorationPlayerStateService, 'recordNewCardAdded'); componentInstance.explorationActuallyStarted = false; @@ -2129,58 +2760,78 @@ describe('Conversation skin component', () => { tick(2000); })); - it('should get recommended summaries when exploration in story chapter mode', - fakeAsync(() => { - let alertMessageElement = document.createElement('div'); - alertMessageElement.className = - 'oppia-exploration-checkpoints-message'; - spyOn(explorationPlayerStateService, 'recordNewCardAdded'); - spyOn(focusManagerService, 'setFocusIfOnDesktop'); - spyOn(componentInstance, 'scrollToTop'); - spyOn(playerPositionService.onNewCardOpened, 'emit'); - spyOn(playerTranscriptService, 'addNewCard'); - spyOn(explorationPlayerStateService, 'getLanguageCode') - .and.returnValue('en'); - spyOn(contentTranslationLanguageService, 'getCurrentContentLanguageCode') - .and.returnValue('es'); - spyOn(contentTranslationManagerService, 'displayTranslations') - .and.returnValue(); - spyOn(playerTranscriptService, 'getNumCards').and.returnValue(0); - spyOn(componentInstance, 'isSupplementalCardNonempty').and.returnValue( - false); - spyOn(playerPositionService, 'setDisplayedCardIndex'); - spyOn(playerPositionService, 'getDisplayedCardIndex').and.returnValue(0); - spyOn(playerPositionService, 'changeCurrentQuestion'); - spyOn(urlService, 'getQueryFieldValuesAsList').and.returnValue([]); - spyOn(explorationEngineService, 'getAuthorRecommendedExpIdsByStateName') - .and.returnValue([]); - spyOn(explorationPlayerStateService, 'isInStoryChapterMode') - .and.returnValue(false); - spyOn(explorationRecommendationsService, 'getRecommendedSummaryDicts') - .and.callFake((ids, recommendations, callb) => { - callb(null); - }); - spyOn(document, 'querySelector').withArgs( - '.oppia-exploration-checkpoints-message') - .and.returnValue(alertMessageElement); + it('should get recommended summaries when exploration in story chapter mode', fakeAsync(() => { + let alertMessageElement = document.createElement('div'); + alertMessageElement.className = 'oppia-exploration-checkpoints-message'; + spyOn(explorationPlayerStateService, 'recordNewCardAdded'); + spyOn(focusManagerService, 'setFocusIfOnDesktop'); + spyOn(componentInstance, 'scrollToTop'); + spyOn(playerPositionService.onNewCardOpened, 'emit'); + spyOn(playerTranscriptService, 'addNewCard'); + spyOn(explorationPlayerStateService, 'getLanguageCode').and.returnValue( + 'en' + ); + spyOn( + contentTranslationLanguageService, + 'getCurrentContentLanguageCode' + ).and.returnValue('es'); + spyOn( + contentTranslationManagerService, + 'displayTranslations' + ).and.returnValue(); + spyOn(playerTranscriptService, 'getNumCards').and.returnValue(0); + spyOn(componentInstance, 'isSupplementalCardNonempty').and.returnValue( + false + ); + spyOn(playerPositionService, 'setDisplayedCardIndex'); + spyOn(playerPositionService, 'getDisplayedCardIndex').and.returnValue(0); + spyOn(playerPositionService, 'changeCurrentQuestion'); + spyOn(urlService, 'getQueryFieldValuesAsList').and.returnValue([]); + spyOn( + explorationEngineService, + 'getAuthorRecommendedExpIdsByStateName' + ).and.returnValue([]); + spyOn( + explorationPlayerStateService, + 'isInStoryChapterMode' + ).and.returnValue(false); + spyOn( + explorationRecommendationsService, + 'getRecommendedSummaryDicts' + ).and.callFake((ids, recommendations, callb) => { + callb(null); + }); + spyOn(document, 'querySelector') + .withArgs('.oppia-exploration-checkpoints-message') + .and.returnValue(alertMessageElement); - componentInstance.alertMessageTimeout = 5; + componentInstance.alertMessageTimeout = 5; - componentInstance.displayedCard = new StateCard( - null, null, null, new Interaction( - [], [], null, null, [], 'EndExploration', null), - [], null, '', null); + componentInstance.displayedCard = new StateCard( + null, + null, + null, + new Interaction([], [], null, null, [], 'EndExploration', null), + [], + null, + '', + null + ); - componentInstance.nextCard = displayedCard; - componentInstance.showPendingCard(); - tick(2000); - })); + componentInstance.nextCard = displayedCard; + componentInstance.showPendingCard(); + tick(2000); + })); it('should check whether hacky translations are displayed or not', () => { - spyOn(i18nLanguageCodeService, 'isHackyTranslationAvailable') - .and.returnValues(false, true); - spyOn(i18nLanguageCodeService, 'isCurrentLanguageEnglish') - .and.returnValues(false, false); + spyOn( + i18nLanguageCodeService, + 'isHackyTranslationAvailable' + ).and.returnValues(false, true); + spyOn(i18nLanguageCodeService, 'isCurrentLanguageEnglish').and.returnValues( + false, + false + ); let expId = 'exp_id'; @@ -2192,20 +2843,27 @@ describe('Conversation skin component', () => { expect(hackyStoryTitleTranslationIsDisplayed).toBe(true); }); - it('should check if current card was completed in a previous session', - () => { - let mockStateCard = new StateCard( - 'Temp2', '', '', new Interaction([], [], null, null, [], null, null) - , [], null, '', null); - componentInstance.displayedCard = mockStateCard; - componentInstance.prevSessionStatesProgress = ['Temp1', 'Temp2']; - expect(componentInstance.isDisplayedCardCompletedInPrevSession()). - toBeTrue(); - componentInstance.prevSessionStatesProgress = ['Temp1']; - expect(componentInstance.isDisplayedCardCompletedInPrevSession()). - toBeFalse(); - } - ); + it('should check if current card was completed in a previous session', () => { + let mockStateCard = new StateCard( + 'Temp2', + '', + '', + new Interaction([], [], null, null, [], null, null), + [], + null, + '', + null + ); + componentInstance.displayedCard = mockStateCard; + componentInstance.prevSessionStatesProgress = ['Temp1', 'Temp2']; + expect( + componentInstance.isDisplayedCardCompletedInPrevSession() + ).toBeTrue(); + componentInstance.prevSessionStatesProgress = ['Temp1']; + expect( + componentInstance.isDisplayedCardCompletedInPrevSession() + ).toBeFalse(); + }); it('should tell if progress clearance message is shown or not', () => { expect(componentInstance.isProgressClearanceMessageShown()).toBeFalse(); @@ -2218,7 +2876,8 @@ describe('Conversation skin component', () => { it('should update when submit button is enabled', () => { componentInstance.submitButtonIsDisabled = false; spyOn(componentInstance, 'isSubmitButtonDisabled').and.returnValue( - !componentInstance.submitButtonIsDisabled); + !componentInstance.submitButtonIsDisabled + ); componentInstance.ngAfterViewChecked(); @@ -2226,144 +2885,177 @@ describe('Conversation skin component', () => { expect(componentInstance.isSubmitButtonDisabled).toHaveBeenCalled(); }); - it( - 'should be able to set appropriate flags for the diagnostic test', - fakeAsync(() => { - let collectionId = 'id'; - let expId = 'exp_id'; - let isInPreviewMode = false; - let isIframed = true; - let collectionSummary = { - is_admin: true, - summaries: [], - user_email: '', - is_topic_manager: false, - username: true - }; - spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(false); - spyOn(userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(new UserInfo( - [], false, false, - false, false, false, '', '', '', true))); - spyOn(urlService, 'getCollectionIdFromExplorationUrl') - .and.returnValues(collectionId, null); - spyOn(urlService, 'getPidFromUrl').and.returnValue(null); + it('should be able to set appropriate flags for the diagnostic test', fakeAsync(() => { + let collectionId = 'id'; + let expId = 'exp_id'; + let isInPreviewMode = false; + let isIframed = true; + let collectionSummary = { + is_admin: true, + summaries: [], + user_email: '', + is_topic_manager: false, + username: true, + }; + spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(false); + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve( + new UserInfo([], false, false, false, false, false, '', '', '', true) + ) + ); + spyOn(urlService, 'getCollectionIdFromExplorationUrl').and.returnValues( + collectionId, + null + ); + spyOn(urlService, 'getPidFromUrl').and.returnValue(null); - spyOn(readOnlyCollectionBackendApiService, 'loadCollectionAsync') - .and.returnValue(Promise.resolve(new Collection( - '', '', '', '', [], null, '', 6, 8, []))); - spyOn(explorationEngineService, 'getExplorationId') - .and.returnValue(expId); - spyOn(explorationEngineService, 'isInPreviewMode') - .and.returnValue(isInPreviewMode); - spyOn(urlService, 'isIframed').and.returnValue(isIframed); - spyOn(loaderService, 'showLoadingScreen'); - spyOn(urlInterpolationService, 'getStaticImageUrl') - .and.returnValue('oppia_avatar_url'); - spyOn(explorationPlayerStateService, 'isInQuestionPlayerMode') - .and.returnValues(true, false); - spyOn(componentInstance, 'initializePage'); - spyOn(collectionPlayerBackendApiService, 'fetchCollectionSummariesAsync') - .and.returnValue(Promise.resolve(collectionSummary)); - spyOn(questionPlayerStateService, 'hintUsed'); - spyOn(questionPlayerEngineService, 'getCurrentQuestion'); - spyOn(questionPlayerStateService, 'solutionViewed'); - spyOn(imagePreloaderService, 'onStateChange'); - spyOn(componentInstance, 'fetchCompletedChaptersCount'); - spyOn(statsReportingService, 'recordExplorationCompleted'); - spyOn(statsReportingService, 'recordExplorationActuallyStarted'); - spyOn( - guestCollectionProgressService, - 'recordExplorationCompletedInCollection' - ); - spyOn(componentInstance, 'doesCollectionAllowsGuestProgress') - .and.returnValue(true); - spyOn(statsReportingService, 'recordMaybeLeaveEvent'); - spyOn(playerTranscriptService, 'getLastStateName').and.returnValue(''); - spyOn(learnerParamsService, 'getAllParams').and.returnValue({}); - spyOn(messengerService, 'sendMessage'); - spyOn(readOnlyExplorationBackendApiService, 'loadLatestExplorationAsync') - .and.returnValue(Promise.resolve(explorationResponse)); - spyOn(explorationEngineService, 'getShortestPathToState') - .and.returnValue(['Start', 'Mid']); - spyOn( - editableExplorationBackendApiService, - 'recordProgressAndFetchUniqueProgressIdOfLoggedOutLearner') - .and.returnValue(Promise.resolve( - {unique_progress_url_id: uniqueProgressIdResponse})); + spyOn( + readOnlyCollectionBackendApiService, + 'loadCollectionAsync' + ).and.returnValue( + Promise.resolve(new Collection('', '', '', '', [], null, '', 6, 8, [])) + ); + spyOn(explorationEngineService, 'getExplorationId').and.returnValue(expId); + spyOn(explorationEngineService, 'isInPreviewMode').and.returnValue( + isInPreviewMode + ); + spyOn(urlService, 'isIframed').and.returnValue(isIframed); + spyOn(loaderService, 'showLoadingScreen'); + spyOn(urlInterpolationService, 'getStaticImageUrl').and.returnValue( + 'oppia_avatar_url' + ); + spyOn( + explorationPlayerStateService, + 'isInQuestionPlayerMode' + ).and.returnValues(true, false); + spyOn(componentInstance, 'initializePage'); + spyOn( + collectionPlayerBackendApiService, + 'fetchCollectionSummariesAsync' + ).and.returnValue(Promise.resolve(collectionSummary)); + spyOn(questionPlayerStateService, 'hintUsed'); + spyOn(questionPlayerEngineService, 'getCurrentQuestion'); + spyOn(questionPlayerStateService, 'solutionViewed'); + spyOn(imagePreloaderService, 'onStateChange'); + spyOn(componentInstance, 'fetchCompletedChaptersCount'); + spyOn(statsReportingService, 'recordExplorationCompleted'); + spyOn(statsReportingService, 'recordExplorationActuallyStarted'); + spyOn( + guestCollectionProgressService, + 'recordExplorationCompletedInCollection' + ); + spyOn( + componentInstance, + 'doesCollectionAllowsGuestProgress' + ).and.returnValue(true); + spyOn(statsReportingService, 'recordMaybeLeaveEvent'); + spyOn(playerTranscriptService, 'getLastStateName').and.returnValue(''); + spyOn(learnerParamsService, 'getAllParams').and.returnValue({}); + spyOn(messengerService, 'sendMessage'); + spyOn( + readOnlyExplorationBackendApiService, + 'loadLatestExplorationAsync' + ).and.returnValue(Promise.resolve(explorationResponse)); + spyOn(explorationEngineService, 'getShortestPathToState').and.returnValue([ + 'Start', + 'Mid', + ]); + spyOn( + editableExplorationBackendApiService, + 'recordProgressAndFetchUniqueProgressIdOfLoggedOutLearner' + ).and.returnValue( + Promise.resolve({unique_progress_url_id: uniqueProgressIdResponse}) + ); - let mockOnHintConsumed = new EventEmitter(); - let mockOnSolutionViewedEventEmitter = new EventEmitter(); - let mockOnPlayerStateChange = new EventEmitter(); + let mockOnHintConsumed = new EventEmitter(); + let mockOnSolutionViewedEventEmitter = new EventEmitter(); + let mockOnPlayerStateChange = new EventEmitter(); - spyOnProperty(hintsAndSolutionManagerService, 'onHintConsumed') - .and.returnValue(mockOnHintConsumed); - spyOnProperty( - hintsAndSolutionManagerService, 'onSolutionViewedEventEmitter') - .and.returnValue(mockOnSolutionViewedEventEmitter); - spyOnProperty(explorationPlayerStateService, 'onPlayerStateChange') - .and.returnValue(mockOnPlayerStateChange); + spyOnProperty( + hintsAndSolutionManagerService, + 'onHintConsumed' + ).and.returnValue(mockOnHintConsumed); + spyOnProperty( + hintsAndSolutionManagerService, + 'onSolutionViewedEventEmitter' + ).and.returnValue(mockOnSolutionViewedEventEmitter); + spyOnProperty( + explorationPlayerStateService, + 'onPlayerStateChange' + ).and.returnValue(mockOnPlayerStateChange); - componentInstance.nextCard = new StateCard( - null, null, null, new Interaction( - [], [], null, null, [], 'EndExploration', null), - [], null, '', null); - componentInstance.isLoggedIn = false; - componentInstance.hasInteractedAtLeastOnce = true; - componentInstance.displayedCard = displayedCard; + componentInstance.nextCard = new StateCard( + null, + null, + null, + new Interaction([], [], null, null, [], 'EndExploration', null), + [], + null, + '', + null + ); + componentInstance.isLoggedIn = false; + componentInstance.hasInteractedAtLeastOnce = true; + componentInstance.displayedCard = displayedCard; - expect(componentInstance.feedbackIsEnabled).toBeTrue(); - expect(componentInstance.learnerCanOnlyAttemptQuestionOnce).toBeFalse(); - expect(componentInstance.inputOutputHistoryIsShown).toBeTrue(); - expect(componentInstance.navigationThroughCardHistoryIsEnabled) - .toBeTrue(); - expect(componentInstance.checkpointCelebrationModalIsEnabled).toBeTrue(); - expect(componentInstance.skipButtonIsShown).toBeFalse(); - - const topicIdToPrerequisiteTopicIds = { - topicId1: [], - topicId2: ['topicId1'], - topicId3: ['topicId2'] - }; + expect(componentInstance.feedbackIsEnabled).toBeTrue(); + expect(componentInstance.learnerCanOnlyAttemptQuestionOnce).toBeFalse(); + expect(componentInstance.inputOutputHistoryIsShown).toBeTrue(); + expect(componentInstance.navigationThroughCardHistoryIsEnabled).toBeTrue(); + expect(componentInstance.checkpointCelebrationModalIsEnabled).toBeTrue(); + expect(componentInstance.skipButtonIsShown).toBeFalse(); - componentInstance.diagnosticTestTopicTrackerModel = ( - new DiagnosticTestTopicTrackerModel(topicIdToPrerequisiteTopicIds)); - tick(); + const topicIdToPrerequisiteTopicIds = { + topicId1: [], + topicId2: ['topicId1'], + topicId3: ['topicId2'], + }; - componentInstance.ngOnInit(); - tick(2000); + componentInstance.diagnosticTestTopicTrackerModel = + new DiagnosticTestTopicTrackerModel(topicIdToPrerequisiteTopicIds); + tick(); - expect(componentInstance.feedbackIsEnabled).toBeFalse(); - expect(componentInstance.learnerCanOnlyAttemptQuestionOnce).toBeTrue(); - expect(componentInstance.inputOutputHistoryIsShown).toBeFalse(); - expect(componentInstance.navigationThroughCardHistoryIsEnabled) - .toBeFalse(); - expect(componentInstance.checkpointCelebrationModalIsEnabled).toBeFalse(); - expect(componentInstance.skipButtonIsShown).toBeTrue(); - })); + componentInstance.ngOnInit(); + tick(2000); + + expect(componentInstance.feedbackIsEnabled).toBeFalse(); + expect(componentInstance.learnerCanOnlyAttemptQuestionOnce).toBeTrue(); + expect(componentInstance.inputOutputHistoryIsShown).toBeFalse(); + expect(componentInstance.navigationThroughCardHistoryIsEnabled).toBeFalse(); + expect(componentInstance.checkpointCelebrationModalIsEnabled).toBeFalse(); + expect(componentInstance.skipButtonIsShown).toBeTrue(); + })); it('should be able to skip the current question', fakeAsync(() => { let sampleCard = StateCard.createNewCard( - 'State 2', '

Content

', '', + 'State 2', + '

Content

', + '', // This throws "Type null is not assignable to type // 'string'." We need to suppress this error // because of the need to test validations. This // throws an error only in the frontend test and // not in the frontend. // @ts-ignore - null, null, 'content', audioTranslationLanguageService); + null, + null, + 'content', + audioTranslationLanguageService + ); let callback = (successCallback: (nextCard: StateCard) => void) => { successCallback(sampleCard); }; - spyOn(explorationPlayerStateService, 'skipCurrentQuestion') - .and.callFake(callback); + spyOn(explorationPlayerStateService, 'skipCurrentQuestion').and.callFake( + callback + ); spyOn(componentInstance, 'showPendingCard'); componentInstance.skipCurrentQuestion(); - expect(explorationPlayerStateService.skipCurrentQuestion) - .toHaveBeenCalled(); + expect( + explorationPlayerStateService.skipCurrentQuestion + ).toHaveBeenCalled(); expect(componentInstance.showPendingCard).toHaveBeenCalled(); })); }); diff --git a/core/templates/pages/exploration-player-page/learner-experience/conversation-skin.component.ts b/core/templates/pages/exploration-player-page/learner-experience/conversation-skin.component.ts index c35504b5a279..0829062d17db 100644 --- a/core/templates/pages/exploration-player-page/learner-experience/conversation-skin.component.ts +++ b/core/templates/pages/exploration-player-page/learner-experience/conversation-skin.component.ts @@ -16,71 +16,73 @@ * @fileoverview Component for the conversation skin. */ -import { Subscription } from 'rxjs'; -import { StateCard } from 'domain/state_card/state-card.model'; -import { ServicesConstants } from 'services/services.constants'; -import { ChangeDetectorRef, Component, Input } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { AlertsService } from 'services/alerts.service'; -import { AudioPlayerService } from 'services/audio-player.service'; -import { AudioTranslationLanguageService } from '../services/audio-translation-language.service'; -import { AutogeneratedAudioPlayerService } from 'services/autogenerated-audio-player.service'; -import { ConceptCardBackendApiService } from 'domain/skill/concept-card-backend-api.service'; -import { ContentTranslationLanguageService } from '../services/content-translation-language.service'; -import { ContentTranslationManagerService } from '../services/content-translation-manager.service'; -import { ContextService } from 'services/context.service'; -import { CurrentInteractionService } from '../services/current-interaction.service'; -import { ExplorationEngineService } from '../services/exploration-engine.service'; -import { ExplorationPlayerStateService } from '../services/exploration-player-state.service'; -import { ExplorationRecommendationsService } from '../services/exploration-recommendations.service'; -import { FatigueDetectionService } from '../services/fatigue-detection.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { GuestCollectionProgressService } from 'domain/collection/guest-collection-progress.service'; -import { HintsAndSolutionManagerService } from '../services/hints-and-solution-manager.service'; -import { I18nLanguageCodeService, TranslationKeyType } from 'services/i18n-language-code.service'; -import { ImagePreloaderService } from '../services/image-preloader.service'; -import { LearnerAnswerInfoService } from '../services/learner-answer-info.service'; -import { LearnerParamsService } from '../services/learner-params.service'; -import { LoaderService } from 'services/loader.service'; -import { MessengerService } from 'services/messenger.service'; -import { NumberAttemptsService } from '../services/number-attempts.service'; -import { PlayerPositionService } from '../services/player-position.service'; -import { PlayerTranscriptService } from '../services/player-transcript.service'; -import { QuestionPlayerEngineService } from '../services/question-player-engine.service'; -import { ReadOnlyCollectionBackendApiService } from 'domain/collection/read-only-collection-backend-api.service'; -import { RefresherExplorationConfirmationModalService } from '../services/refresher-exploration-confirmation-modal.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { StatsReportingService } from '../services/stats-reporting.service'; -import { StoryViewerBackendApiService } from 'domain/story_viewer/story-viewer-backend-api.service'; -import { UrlService } from 'services/contextual/url.service'; -import { UserService } from 'services/user.service'; -import { LocalStorageService } from 'services/local-storage.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { QuestionPlayerStateService } from 'components/question-directives/question-player/services/question-player-state.service'; -import { State } from 'domain/state/StateObjectFactory'; -import { InteractionRulesService } from '../services/answer-classification.service'; +import {Subscription} from 'rxjs'; +import {StateCard} from 'domain/state_card/state-card.model'; +import {ServicesConstants} from 'services/services.constants'; +import {ChangeDetectorRef, Component, Input} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {AlertsService} from 'services/alerts.service'; +import {AudioPlayerService} from 'services/audio-player.service'; +import {AudioTranslationLanguageService} from '../services/audio-translation-language.service'; +import {AutogeneratedAudioPlayerService} from 'services/autogenerated-audio-player.service'; +import {ConceptCardBackendApiService} from 'domain/skill/concept-card-backend-api.service'; +import {ContentTranslationLanguageService} from '../services/content-translation-language.service'; +import {ContentTranslationManagerService} from '../services/content-translation-manager.service'; +import {ContextService} from 'services/context.service'; +import {CurrentInteractionService} from '../services/current-interaction.service'; +import {ExplorationEngineService} from '../services/exploration-engine.service'; +import {ExplorationPlayerStateService} from '../services/exploration-player-state.service'; +import {ExplorationRecommendationsService} from '../services/exploration-recommendations.service'; +import {FatigueDetectionService} from '../services/fatigue-detection.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {GuestCollectionProgressService} from 'domain/collection/guest-collection-progress.service'; +import {HintsAndSolutionManagerService} from '../services/hints-and-solution-manager.service'; +import { + I18nLanguageCodeService, + TranslationKeyType, +} from 'services/i18n-language-code.service'; +import {ImagePreloaderService} from '../services/image-preloader.service'; +import {LearnerAnswerInfoService} from '../services/learner-answer-info.service'; +import {LearnerParamsService} from '../services/learner-params.service'; +import {LoaderService} from 'services/loader.service'; +import {MessengerService} from 'services/messenger.service'; +import {NumberAttemptsService} from '../services/number-attempts.service'; +import {PlayerPositionService} from '../services/player-position.service'; +import {PlayerTranscriptService} from '../services/player-transcript.service'; +import {QuestionPlayerEngineService} from '../services/question-player-engine.service'; +import {ReadOnlyCollectionBackendApiService} from 'domain/collection/read-only-collection-backend-api.service'; +import {RefresherExplorationConfirmationModalService} from '../services/refresher-exploration-confirmation-modal.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {StatsReportingService} from '../services/stats-reporting.service'; +import {StoryViewerBackendApiService} from 'domain/story_viewer/story-viewer-backend-api.service'; +import {UrlService} from 'services/contextual/url.service'; +import {UserService} from 'services/user.service'; +import {LocalStorageService} from 'services/local-storage.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {QuestionPlayerStateService} from 'components/question-directives/question-player/services/question-player-state.service'; +import {State} from 'domain/state/StateObjectFactory'; +import {InteractionRulesService} from '../services/answer-classification.service'; import INTERACTION_SPECS from 'interactions/interaction_specs.json'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { ExplorationPlayerConstants } from '../exploration-player-page.constants'; -import { AppConstants } from 'app.constants'; -import { TopicViewerDomainConstants } from 'domain/topic_viewer/topic-viewer-domain.constants'; -import { StoryViewerDomainConstants } from 'domain/story_viewer/story-viewer-domain.constants'; -import { ConceptCard } from 'domain/skill/concept-card.model'; -import { CollectionPlayerBackendApiService } from 'pages/collection-player-page/services/collection-player-backend-api.service'; -import { ExplorationSummaryBackendApiService } from 'domain/summary/exploration-summary-backend-api.service'; -import { LearnerExplorationSummary } from 'domain/summary/learner-exploration-summary.model'; -import { EditableExplorationBackendApiService } from 'domain/exploration/editable-exploration-backend-api.service'; -import { ReadOnlyExplorationBackendApiService } from 'domain/exploration/read-only-exploration-backend-api.service'; -import { StateObjectsBackendDict } from 'domain/exploration/StatesObjectFactory'; -import { PlatformFeatureService } from 'services/platform-feature.service'; -import { LearnerDashboardBackendApiService } from 'domain/learner_dashboard/learner-dashboard-backend-api.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {ExplorationPlayerConstants} from '../exploration-player-page.constants'; +import {AppConstants} from 'app.constants'; +import {TopicViewerDomainConstants} from 'domain/topic_viewer/topic-viewer-domain.constants'; +import {StoryViewerDomainConstants} from 'domain/story_viewer/story-viewer-domain.constants'; +import {ConceptCard} from 'domain/skill/concept-card.model'; +import {CollectionPlayerBackendApiService} from 'pages/collection-player-page/services/collection-player-backend-api.service'; +import {ExplorationSummaryBackendApiService} from 'domain/summary/exploration-summary-backend-api.service'; +import {LearnerExplorationSummary} from 'domain/summary/learner-exploration-summary.model'; +import {EditableExplorationBackendApiService} from 'domain/exploration/editable-exploration-backend-api.service'; +import {ReadOnlyExplorationBackendApiService} from 'domain/exploration/read-only-exploration-backend-api.service'; +import {StateObjectsBackendDict} from 'domain/exploration/StatesObjectFactory'; +import {PlatformFeatureService} from 'services/platform-feature.service'; +import {LearnerDashboardBackendApiService} from 'domain/learner_dashboard/learner-dashboard-backend-api.service'; import './conversation-skin.component.css'; -import { ConceptCardManagerService } from '../services/concept-card-manager.service'; -import { TranslateService } from '@ngx-translate/core'; -import { Solution } from 'domain/exploration/SolutionObjectFactory'; - +import {ConceptCardManagerService} from '../services/concept-card-manager.service'; +import {TranslateService} from '@ngx-translate/core'; +import {Solution} from 'domain/exploration/SolutionObjectFactory'; // Note: This file should be assumed to be in an IIFE, and the constants below // should only be used within this file. @@ -92,7 +94,7 @@ const TIME_NUM_CARDS_CHANGE_MSEC = 500; @Component({ selector: 'oppia-conversation-skin', templateUrl: './conversation-skin.component.html', - styleUrls: ['./conversation-skin.component.css'] + styleUrls: ['./conversation-skin.component.css'], }) export class ConversationSkinComponent { @Input() questionPlayerConfig; @@ -109,8 +111,8 @@ export class ConversationSkinComponent { _editorPreviewMode; explorationActuallyStarted: boolean = false; - CONTINUE_BUTTON_FOCUS_LABEL = ( - ExplorationPlayerConstants.CONTINUE_BUTTON_FOCUS_LABEL); + CONTINUE_BUTTON_FOCUS_LABEL = + ExplorationPlayerConstants.CONTINUE_BUTTON_FOCUS_LABEL; isLoggedIn: boolean; storyNodeIdToAdd: string; @@ -132,8 +134,8 @@ export class ConversationSkinComponent { displayedCard: StateCard; upcomingInlineInteractionHtml; responseTimeout: NodeJS.Timeout | null = null; - DEFAULT_TWITTER_SHARE_MESSAGE_PLAYER = ( - AppConstants.DEFAULT_TWITTER_SHARE_MESSAGE_EDITOR); + DEFAULT_TWITTER_SHARE_MESSAGE_PLAYER = + AppConstants.DEFAULT_TWITTER_SHARE_MESSAGE_EDITOR; // If the exploration is iframed, send data to its parent about // its height so that the parent can be resized as necessary. @@ -184,20 +186,16 @@ export class ConversationSkinComponent { private audioTranslationLanguageService: AudioTranslationLanguageService, private autogeneratedAudioPlayerService: AutogeneratedAudioPlayerService, private changeDetectorRef: ChangeDetectorRef, - private collectionPlayerBackendApiService: - CollectionPlayerBackendApiService, + private collectionPlayerBackendApiService: CollectionPlayerBackendApiService, private conceptCardBackendApiService: ConceptCardBackendApiService, - private contentTranslationLanguageService: - ContentTranslationLanguageService, + private contentTranslationLanguageService: ContentTranslationLanguageService, private contentTranslationManagerService: ContentTranslationManagerService, private contextService: ContextService, private currentInteractionService: CurrentInteractionService, private explorationEngineService: ExplorationEngineService, private explorationPlayerStateService: ExplorationPlayerStateService, - private explorationRecommendationsService: - ExplorationRecommendationsService, - private explorationSummaryBackendApiService: - ExplorationSummaryBackendApiService, + private explorationRecommendationsService: ExplorationRecommendationsService, + private explorationSummaryBackendApiService: ExplorationSummaryBackendApiService, private fatigueDetectionService: FatigueDetectionService, private focusManagerService: FocusManagerService, private guestCollectionProgressService: GuestCollectionProgressService, @@ -215,10 +213,8 @@ export class ConversationSkinComponent { private playerTranscriptService: PlayerTranscriptService, private questionPlayerEngineService: QuestionPlayerEngineService, private questionPlayerStateService: QuestionPlayerStateService, - private readOnlyCollectionBackendApiService: - ReadOnlyCollectionBackendApiService, - private refresherExplorationConfirmationModalService: - RefresherExplorationConfirmationModalService, + private readOnlyCollectionBackendApiService: ReadOnlyCollectionBackendApiService, + private refresherExplorationConfirmationModalService: RefresherExplorationConfirmationModalService, private siteAnalyticsService: SiteAnalyticsService, private statsReportingService: StatsReportingService, private storyViewerBackendApiService: StoryViewerBackendApiService, @@ -226,10 +222,8 @@ export class ConversationSkinComponent { private urlService: UrlService, private userService: UserService, private windowDimensionsService: WindowDimensionsService, - private editableExplorationBackendApiService: - EditableExplorationBackendApiService, - private readOnlyExplorationBackendApiService: - ReadOnlyExplorationBackendApiService, + private editableExplorationBackendApiService: EditableExplorationBackendApiService, + private readOnlyExplorationBackendApiService: ReadOnlyExplorationBackendApiService, private platformFeatureService: PlatformFeatureService, private translateService: TranslateService, private learnerDashboardBackendApiService: LearnerDashboardBackendApiService @@ -248,10 +242,11 @@ export class ConversationSkinComponent { this.pidInUrl = this.urlService.getPidFromUrl(); if (this.collectionId) { - this.readOnlyCollectionBackendApiService.loadCollectionAsync( - this.collectionId).then((collection) => { - this.collectionTitle = collection.getTitle(); - }); + this.readOnlyCollectionBackendApiService + .loadCollectionAsync(this.collectionId) + .then(collection => { + this.collectionTitle = collection.getTitle(); + }); } else { this.collectionTitle = null; } @@ -274,36 +269,35 @@ export class ConversationSkinComponent { this.isIframed = this.urlService.isIframed(); this.loaderService.showLoadingScreen('Loading'); - this.OPPIA_AVATAR_IMAGE_URL = ( + this.OPPIA_AVATAR_IMAGE_URL = this.urlInterpolationService.getStaticImageUrl( - '/avatar/oppia_avatar_100px.svg')); + '/avatar/oppia_avatar_100px.svg' + ); if (this.explorationPlayerStateService.isInQuestionPlayerMode()) { this.directiveSubscriptions.add( - this.hintsAndSolutionManagerService.onHintConsumed.subscribe( - () => { - this.questionPlayerStateService.hintUsed( - this.questionPlayerEngineService.getCurrentQuestion()); - } - ) + this.hintsAndSolutionManagerService.onHintConsumed.subscribe(() => { + this.questionPlayerStateService.hintUsed( + this.questionPlayerEngineService.getCurrentQuestion() + ); + }) ); this.directiveSubscriptions.add( - this.hintsAndSolutionManagerService.onSolutionViewedEventEmitter - .subscribe(() => { + this.hintsAndSolutionManagerService.onSolutionViewedEventEmitter.subscribe( + () => { this.questionPlayerStateService.solutionViewed( this.questionPlayerEngineService.getCurrentQuestion() ); - }) + } + ) ); } this.directiveSubscriptions.add( - this.explorationPlayerStateService.onShowProgressModal.subscribe( - () => { - this.hasFullyLoaded = true; - } - ) + this.explorationPlayerStateService.onShowProgressModal.subscribe(() => { + this.hasFullyLoaded = true; + }) ); this.directiveSubscriptions.add( @@ -319,32 +313,27 @@ export class ConversationSkinComponent { ); this.directiveSubscriptions.add( - this.hintsAndSolutionManagerService.onLearnerReallyStuck.subscribe( - () => { - this.triggerIfLearnerStuckActionDirectly(); - } - ) + this.hintsAndSolutionManagerService.onLearnerReallyStuck.subscribe(() => { + this.triggerIfLearnerStuckActionDirectly(); + }) ); this.directiveSubscriptions.add( - this.hintsAndSolutionManagerService.onHintsExhausted.subscribe( - () => { - this.triggerIfLearnerStuckAction(); - } - ) + this.hintsAndSolutionManagerService.onHintsExhausted.subscribe(() => { + this.triggerIfLearnerStuckAction(); + }) ); this.directiveSubscriptions.add( - this.conceptCardManagerService.onLearnerGetsReallyStuck - .subscribe(() => { - this.isLearnerReallyStuck = true; - this.triggerIfLearnerStuckActionDirectly(); - }) + this.conceptCardManagerService.onLearnerGetsReallyStuck.subscribe(() => { + this.isLearnerReallyStuck = true; + this.triggerIfLearnerStuckActionDirectly(); + }) ); this.directiveSubscriptions.add( this.explorationPlayerStateService.onPlayerStateChange.subscribe( - (newStateName) => { + newStateName => { if (!newStateName) { return; } @@ -354,11 +343,9 @@ export class ConversationSkinComponent { } // Ensure the transition to a terminal state properly logs // the end of the exploration. - if ( - !this._editorPreviewMode && this.nextCard.isTerminal()) { - const currentEngineService = ( - this.explorationPlayerStateService.getCurrentEngineService() - ); + if (!this._editorPreviewMode && this.nextCard.isTerminal()) { + const currentEngineService = + this.explorationPlayerStateService.getCurrentEngineService(); this.statsReportingService.recordExplorationCompleted( newStateName, this.learnerParamsService.getAllParams(), @@ -373,11 +360,14 @@ export class ConversationSkinComponent { // within the context of a collection, and the collection is // allowlisted, record their temporary progress. - if (this.doesCollectionAllowsGuestProgress( - this.collectionId) && !this.isLoggedIn) { - this.guestCollectionProgressService. - recordExplorationCompletedInCollection( - this.collectionId, this.explorationId); + if ( + this.doesCollectionAllowsGuestProgress(this.collectionId) && + !this.isLoggedIn + ) { + this.guestCollectionProgressService.recordExplorationCompletedInCollection( + this.collectionId, + this.explorationId + ); } // For single state explorations, when the exploration @@ -385,7 +375,8 @@ export class ConversationSkinComponent { // is false, record exploration actual start event. if (!this.explorationActuallyStarted) { this.statsReportingService.recordExplorationActuallyStarted( - newStateName); + newStateName + ); this.explorationActuallyStarted = true; } } @@ -395,47 +386,52 @@ export class ConversationSkinComponent { // Moved the following code to then section as isLoggedIn // variable needs to be defined before the following code is executed. - this.userService.getUserInfoAsync().then(async(userInfo) => { + this.userService.getUserInfoAsync().then(async userInfo => { this.isLoggedIn = userInfo.isLoggedIn(); - this.windowRef.nativeWindow.addEventListener('beforeunload', (e) => { + this.windowRef.nativeWindow.addEventListener('beforeunload', e => { if (this.redirectToRefresherExplorationConfirmed) { return; } - if (this.hasInteractedAtLeastOnce && !this.isInPreviewMode && - !this.displayedCard.isTerminal() && - !this.explorationPlayerStateService.isInQuestionMode()) { + if ( + this.hasInteractedAtLeastOnce && + !this.isInPreviewMode && + !this.displayedCard.isTerminal() && + !this.explorationPlayerStateService.isInQuestionMode() + ) { this.statsReportingService.recordMaybeLeaveEvent( this.playerTranscriptService.getLastStateName(), - this.learnerParamsService.getAllParams()); + this.learnerParamsService.getAllParams() + ); - let isLoggedOutProgressTracked = ( - this.explorationPlayerStateService - .isLoggedOutLearnerProgressTracked()); + let isLoggedOutProgressTracked = + this.explorationPlayerStateService.isLoggedOutLearnerProgressTracked(); if (!this.isLoggedIn && !isLoggedOutProgressTracked) { - let confirmationMessage = ( + let confirmationMessage = 'Please save your progress before navigating away from the' + - ' page; else, you will lose your exploration progress.'); - (e || this.windowRef.nativeWindow.event).returnValue = ( - confirmationMessage); + ' page; else, you will lose your exploration progress.'; + (e || this.windowRef.nativeWindow.event).returnValue = + confirmationMessage; return confirmationMessage; } } }); - let pid = this.localStorageService - .getUniqueProgressIdOfLoggedOutLearner(); + let pid = + this.localStorageService.getUniqueProgressIdOfLoggedOutLearner(); if (pid && this.isLoggedIn) { - await this.editableExplorationBackendApiService - .changeLoggedOutProgressToLoggedInProgressAsync( - this.explorationId, pid); + await this.editableExplorationBackendApiService.changeLoggedOutProgressToLoggedInProgressAsync( + this.explorationId, + pid + ); this.localStorageService.removeUniqueProgressIdOfLoggedOutLearner(); } this.adjustPageHeightOnresize(); this.currentInteractionService.setOnSubmitFn( - this.submitAnswer.bind(this)); + this.submitAnswer.bind(this) + ); this.startCardChangeAnimation = false; this.initializePage(); @@ -445,13 +441,13 @@ export class ConversationSkinComponent { this.collectionPlayerBackendApiService .fetchCollectionSummariesAsync(this.collectionId) .then( - (response) => { + response => { this.collectionSummary = response.summaries[0]; }, () => { this.alertsService.addWarning( - 'There was an error while fetching the collection ' + - 'summary.'); + 'There was an error while fetching the collection ' + 'summary.' + ); } ); } @@ -459,49 +455,47 @@ export class ConversationSkinComponent { this.fetchCompletedChaptersCount(); // We do not save checkpoints progress for iframes. - if (!this.isIframed && !this._editorPreviewMode && - !this.explorationPlayerStateService.isInQuestionPlayerMode()) { + if ( + !this.isIframed && + !this._editorPreviewMode && + !this.explorationPlayerStateService.isInQuestionPlayerMode() + ) { // For the first state which is always a checkpoint. let firstStateName: string; let expVersion: number; - this.readOnlyExplorationBackendApiService. - loadLatestExplorationAsync(this.explorationId, this.pidInUrl).then( - response => { - expVersion = response.version; - firstStateName = response.exploration.init_state_name; - this.mostRecentlyReachedCheckpoint = ( - response.most_recently_reached_checkpoint_state_name + this.readOnlyExplorationBackendApiService + .loadLatestExplorationAsync(this.explorationId, this.pidInUrl) + .then(response => { + expVersion = response.version; + firstStateName = response.exploration.init_state_name; + this.mostRecentlyReachedCheckpoint = + response.most_recently_reached_checkpoint_state_name; + // If the exploration is freshly started, mark the first state + // as the most recently reached checkpoint. + if (!this.mostRecentlyReachedCheckpoint && this.isLoggedIn) { + this.editableExplorationBackendApiService.recordMostRecentlyReachedCheckpointAsync( + this.explorationId, + expVersion, + firstStateName, + true ); - // If the exploration is freshly started, mark the first state - // as the most recently reached checkpoint. - if (!this.mostRecentlyReachedCheckpoint && this.isLoggedIn) { - this.editableExplorationBackendApiService. - recordMostRecentlyReachedCheckpointAsync( - this.explorationId, - expVersion, - firstStateName, - true - ); - } - this.explorationPlayerStateService.setLastCompletedCheckpoint( - firstStateName); } - ); + this.explorationPlayerStateService.setLastCompletedCheckpoint( + firstStateName + ); + }); this.visitedStateNames.push(firstStateName); } }); } doesCollectionAllowsGuestProgress(collectionId: string | never): boolean { - let allowedCollectionIds = ( - AppConstants. - ALLOWED_COLLECTION_IDS_FOR_SAVING_GUEST_PROGRESS - ); + let allowedCollectionIds = + AppConstants.ALLOWED_COLLECTION_IDS_FOR_SAVING_GUEST_PROGRESS; return ( - ( - allowedCollectionIds as readonly[] - ). - indexOf(collectionId as never) !== -1); + (allowedCollectionIds as readonly []).indexOf(collectionId as never) !== + -1 + ); } isSubmitButtonDisabled(): boolean { @@ -530,7 +524,8 @@ export class ConversationSkinComponent { this.playerPositionService.recordNavigationButtonClick(); this.playerPositionService.setDisplayedCardIndex(index); this.explorationEngineService.onUpdateActiveStateIfInEditor.emit( - this.playerPositionService.getCurrentStateName()); + this.playerPositionService.getCurrentStateName() + ); this.playerPositionService.changeCurrentQuestion(index); } @@ -549,31 +544,40 @@ export class ConversationSkinComponent { fetchCompletedChaptersCount(): void { if (this.isLoggedIn) { this.learnerDashboardBackendApiService - .fetchLearnerCompletedChaptersCountDataAsync().then((data) => { + .fetchLearnerCompletedChaptersCountDataAsync() + .then(data => { this.completedChaptersCount = data.completedChaptersCount; }); } } initLearnerAnswerInfoService( - entityId: string, state: State, answer: string, - interactionRulesService: InteractionRulesService, - alwaysAskLearnerForAnswerInfo: boolean): void { + entityId: string, + state: State, + answer: string, + interactionRulesService: InteractionRulesService, + alwaysAskLearnerForAnswerInfo: boolean + ): void { this.learnerAnswerInfoService.initLearnerAnswerInfoService( - entityId, state, answer, interactionRulesService, - alwaysAskLearnerForAnswerInfo); + entityId, + state, + answer, + interactionRulesService, + alwaysAskLearnerForAnswerInfo + ); } isCorrectnessFooterEnabled(): boolean { return ( this.answerIsCorrect && - this.playerPositionService.hasLearnerJustSubmittedAnAnswer()); + this.playerPositionService.hasLearnerJustSubmittedAnAnswer() + ); } isLearnAgainButton(): boolean { - let conceptCardIsBeingShown = ( + let conceptCardIsBeingShown = this.displayedCard.getStateName() === null && - !this.explorationPlayerStateService.isInQuestionMode()); + !this.explorationPlayerStateService.isInQuestionMode(); if (conceptCardIsBeingShown) { return false; } @@ -588,8 +592,7 @@ export class ConversationSkinComponent { if (INTERACTION_SPECS[interaction.id].is_linear) { return false; } - return ( - this.pendingCardWasSeenBefore && !this.answerIsCorrect); + return this.pendingCardWasSeenBefore && !this.answerIsCorrect; } private _getRandomSuffix(): string { @@ -618,16 +621,20 @@ export class ConversationSkinComponent { adjustPageHeight(scroll: boolean, callback: () => void): void { setTimeout(() => { let newHeight = document.body.scrollHeight; - if (Math.abs(this.lastRequestedHeight - newHeight) > 50.5 || - (scroll && !this.lastRequestedScroll)) { + if ( + Math.abs(this.lastRequestedHeight - newHeight) > 50.5 || + (scroll && !this.lastRequestedScroll) + ) { // Sometimes setting iframe height to the exact content height // still produces scrollbar, so adding 50 extra px. newHeight += 50; this.messengerService.sendMessage( - ServicesConstants.MESSENGER_PAYLOAD.HEIGHT_CHANGE, { + ServicesConstants.MESSENGER_PAYLOAD.HEIGHT_CHANGE, + { height: newHeight, - scroll: scroll - }); + scroll: scroll, + } + ); this.lastRequestedHeight = newHeight; this.lastRequestedScroll = scroll; } @@ -639,17 +646,17 @@ export class ConversationSkinComponent { } getExplorationLink(): string { - if (this.recommendedExplorationSummaries && - this.recommendedExplorationSummaries[0]) { + if ( + this.recommendedExplorationSummaries && + this.recommendedExplorationSummaries[0] + ) { if (!this.recommendedExplorationSummaries[0].id) { return '#'; } else { - let result = '/explore/' + - this.recommendedExplorationSummaries[0].id; + let result = '/explore/' + this.recommendedExplorationSummaries[0].id; let urlParams = this.urlService.getUrlParams(); - let parentExplorationIds = ( - this.recommendedExplorationSummaries[0] - .parentExplorationIds); + let parentExplorationIds = + this.recommendedExplorationSummaries[0].parentExplorationIds; let collectionIdToAdd = this.collectionId; let storyUrlFragmentToAdd = null; @@ -657,23 +664,24 @@ export class ConversationSkinComponent { let classroomUrlFragment = null; // Replace the collection ID with the one in the URL if it // exists in urlParams. - if (parentExplorationIds && - urlParams.hasOwnProperty('collection_id')) { + if (parentExplorationIds && urlParams.hasOwnProperty('collection_id')) { collectionIdToAdd = urlParams.collection_id; } else if ( this.urlService.getPathname().match(/\/story\/(\w|-){12}/g) && - this.recommendedExplorationSummaries[0].nextNodeId) { - storyUrlFragmentToAdd = ( - this.urlService.getStoryUrlFragmentFromLearnerUrl()); - topicUrlFragment = ( - this.urlService.getTopicUrlFragmentFromLearnerUrl()); - classroomUrlFragment = ( - this.urlService.getClassroomUrlFragmentFromLearnerUrl()); + this.recommendedExplorationSummaries[0].nextNodeId + ) { + storyUrlFragmentToAdd = + this.urlService.getStoryUrlFragmentFromLearnerUrl(); + topicUrlFragment = + this.urlService.getTopicUrlFragmentFromLearnerUrl(); + classroomUrlFragment = + this.urlService.getClassroomUrlFragmentFromLearnerUrl(); } else if ( urlParams.hasOwnProperty('story_url_fragment') && - urlParams.hasOwnProperty('node_id') && - urlParams.hasOwnProperty('topic_url_fragment') && - urlParams.hasOwnProperty('classroom_url_fragment')) { + urlParams.hasOwnProperty('node_id') && + urlParams.hasOwnProperty('topic_url_fragment') && + urlParams.hasOwnProperty('classroom_url_fragment') + ) { topicUrlFragment = urlParams.topic_url_fragment; classroomUrlFragment = urlParams.classroom_url_fragment; storyUrlFragmentToAdd = urlParams.story_url_fragment; @@ -681,23 +689,41 @@ export class ConversationSkinComponent { if (collectionIdToAdd) { result = this.urlService.addField( - result, 'collection_id', collectionIdToAdd); + result, + 'collection_id', + collectionIdToAdd + ); } if (parentExplorationIds) { for (let i = 0; i < parentExplorationIds.length - 1; i++) { result = this.urlService.addField( - result, 'parent', parentExplorationIds[i]); + result, + 'parent', + parentExplorationIds[i] + ); } } if (storyUrlFragmentToAdd && this.storyNodeIdToAdd) { result = this.urlService.addField( - result, 'topic_url_fragment', topicUrlFragment); + result, + 'topic_url_fragment', + topicUrlFragment + ); result = this.urlService.addField( - result, 'classroom_url_fragment', classroomUrlFragment); + result, + 'classroom_url_fragment', + classroomUrlFragment + ); result = this.urlService.addField( - result, 'story_url_fragment', storyUrlFragmentToAdd); + result, + 'story_url_fragment', + storyUrlFragmentToAdd + ); result = this.urlService.addField( - result, 'node_id', this.storyNodeIdToAdd); + result, + 'node_id', + this.storyNodeIdToAdd + ); } return result; } @@ -713,8 +739,7 @@ export class ConversationSkinComponent { } isOnTerminalCard(): boolean { - return ( - this.displayedCard && this.displayedCard.isTerminal()); + return this.displayedCard && this.displayedCard.isTerminal(); } isSupplementalCardNonempty(card: StateCard): boolean { @@ -722,92 +747,93 @@ export class ConversationSkinComponent { } isCurrentSupplementalCardNonempty(): boolean { - return this.displayedCard && this.isSupplementalCardNonempty( - this.displayedCard); + return ( + this.displayedCard && this.isSupplementalCardNonempty(this.displayedCard) + ); } isSupplementalNavShown(): boolean { if ( this.displayedCard.getStateName() === null && - !this.explorationPlayerStateService.isInQuestionMode()) { + !this.explorationPlayerStateService.isInQuestionMode() + ) { return false; } let interaction = this.displayedCard.getInteraction(); return ( Boolean(interaction.id) && INTERACTION_SPECS[interaction.id].show_generic_submit_button && - this.isCurrentCardAtEndOfTranscript()); + this.isCurrentCardAtEndOfTranscript() + ); } - private _recordLeaveForRefresherExp(refresherExpId): void { if (!this._editorPreviewMode) { this.statsReportingService.recordLeaveForRefresherExp( this.playerPositionService.getCurrentStateName(), - refresherExpId); + refresherExpId + ); } } private _navigateToMostRecentlyReachedCheckpoint() { let states: StateObjectsBackendDict; - this.readOnlyExplorationBackendApiService. - loadLatestExplorationAsync(this.explorationId, this.pidInUrl).then( - response => { - states = response.exploration.states; - this.mostRecentlyReachedCheckpoint = ( - response.most_recently_reached_checkpoint_state_name - ); - - this.prevSessionStatesProgress = ( - this.explorationEngineService.getShortestPathToState( - states, this.mostRecentlyReachedCheckpoint - ) + this.readOnlyExplorationBackendApiService + .loadLatestExplorationAsync(this.explorationId, this.pidInUrl) + .then(response => { + states = response.exploration.states; + this.mostRecentlyReachedCheckpoint = + response.most_recently_reached_checkpoint_state_name; + + this.prevSessionStatesProgress = + this.explorationEngineService.getShortestPathToState( + states, + this.mostRecentlyReachedCheckpoint ); - let indexToRedirectTo = 0; - - for (let i = 0; i < this.prevSessionStatesProgress.length; i++) { - // Set state name of a previously completed state. - let stateName = this.prevSessionStatesProgress[i]; - // Skip the card if it has already been added to transcript. - if (!this.playerTranscriptService.hasEncounteredStateBefore( - stateName) - ) { - let stateCard = ( - this.explorationEngineService.getStateCardByName(stateName) - ); - this._addNewCard(stateCard); - } + let indexToRedirectTo = 0; - if (this.mostRecentlyReachedCheckpoint === stateName) { - break; - } + for (let i = 0; i < this.prevSessionStatesProgress.length; i++) { + // Set state name of a previously completed state. + let stateName = this.prevSessionStatesProgress[i]; + // Skip the card if it has already been added to transcript. + if ( + !this.playerTranscriptService.hasEncounteredStateBefore(stateName) + ) { + let stateCard = + this.explorationEngineService.getStateCardByName(stateName); + this._addNewCard(stateCard); + } - this.visitedStateNames.push(stateName); - indexToRedirectTo += 1; + if (this.mostRecentlyReachedCheckpoint === stateName) { + break; } - // Remove the last card from progress as it is not completed - // yet and is only most recently reached. - this.prevSessionStatesProgress.pop(); + this.visitedStateNames.push(stateName); + indexToRedirectTo += 1; + } - if (indexToRedirectTo > 0) { - setTimeout(() => { - let alertInfoElement = document.querySelector( - '.oppia-exploration-checkpoints-message'); + // Remove the last card from progress as it is not completed + // yet and is only most recently reached. + this.prevSessionStatesProgress.pop(); - // Remove the alert message after 6 sec. - if (alertInfoElement) { - alertInfoElement.remove(); - } - }, this.alertMessageTimeout); - } + if (indexToRedirectTo > 0) { + setTimeout(() => { + let alertInfoElement = document.querySelector( + '.oppia-exploration-checkpoints-message' + ); - // Move to most recently reached checkpoint card. - this.changeCard(indexToRedirectTo); - this.playerPositionService.onLoadedMostRecentCheckpoint.emit(); + // Remove the alert message after 6 sec. + if (alertInfoElement) { + alertInfoElement.remove(); + } + }, this.alertMessageTimeout); } - ); + + // Move to most recently reached checkpoint card. + this.changeCard(indexToRedirectTo); + this.playerPositionService.onLoadedMostRecentCheckpoint.emit(); + }); } // Navigates to the currently-active card, and resets the @@ -816,28 +842,33 @@ export class ConversationSkinComponent { let index = this.playerPositionService.getDisplayedCardIndex(); this.displayedCard = this.playerTranscriptService.getCard(index); - if (index > 0 && !this.isIframed && !this._editorPreviewMode && - !this.explorationPlayerStateService.isInQuestionPlayerMode()) { + if ( + index > 0 && + !this.isIframed && + !this._editorPreviewMode && + !this.explorationPlayerStateService.isInQuestionPlayerMode() + ) { let currentState = this.explorationEngineService.getState(); let currentStateName = currentState.name; - if (currentState.cardIsCheckpoint && - !this.visitedStateNames.includes(currentStateName) && - !this.prevSessionStatesProgress.includes(currentStateName)) { - this.readOnlyExplorationBackendApiService. - loadLatestExplorationAsync(this.explorationId).then( - response => { - this.explorationPlayerStateService.setLastCompletedCheckpoint( - currentStateName); - this.editableExplorationBackendApiService. - recordMostRecentlyReachedCheckpointAsync( - this.explorationId, - response.version, - currentStateName, - this.isLoggedIn, - this.explorationPlayerStateService.getUniqueProgressUrlId() - ); - } - ); + if ( + currentState.cardIsCheckpoint && + !this.visitedStateNames.includes(currentStateName) && + !this.prevSessionStatesProgress.includes(currentStateName) + ) { + this.readOnlyExplorationBackendApiService + .loadLatestExplorationAsync(this.explorationId) + .then(response => { + this.explorationPlayerStateService.setLastCompletedCheckpoint( + currentStateName + ); + this.editableExplorationBackendApiService.recordMostRecentlyReachedCheckpointAsync( + this.explorationId, + response.version, + currentStateName, + this.isLoggedIn, + this.explorationPlayerStateService.getUniqueProgressUrlId() + ); + }); this.visitedStateNames.push(currentStateName); } } @@ -856,12 +887,15 @@ export class ConversationSkinComponent { // bug where the autogenerated audio player generates duplicate // utterances occurs. this.autogeneratedAudioPlayerService.cancel(); - if (this._nextFocusLabel && - this.playerTranscriptService.isLastCard(index)) { + if ( + this._nextFocusLabel && + this.playerTranscriptService.isLastCard(index) + ) { this.focusManagerService.setFocusIfOnDesktop(this._nextFocusLabel); } else { this.focusManagerService.setFocusIfOnDesktop( - this.getContentFocusLabel(index)); + this.getContentFocusLabel(index) + ); } } @@ -873,13 +907,15 @@ export class ConversationSkinComponent { animateToTwoCards(doneCallback: () => void): void { this.isAnimatingToTwoCards = true; - setTimeout(() => { - this.isAnimatingToTwoCards = false; - if (doneCallback) { - doneCallback(); - } - }, TIME_NUM_CARDS_CHANGE_MSEC + TIME_FADEIN_MSEC + - this.TIME_PADDING_MSEC); + setTimeout( + () => { + this.isAnimatingToTwoCards = false; + if (doneCallback) { + doneCallback(); + } + }, + TIME_NUM_CARDS_CHANGE_MSEC + TIME_FADEIN_MSEC + this.TIME_PADDING_MSEC + ); } animateToOneCard(doneCallback: () => void): void { @@ -892,46 +928,50 @@ export class ConversationSkinComponent { }, TIME_NUM_CARDS_CHANGE_MSEC); } - isCurrentCardAtEndOfTranscript(): boolean { return this.playerTranscriptService.isLastCard( - this.playerPositionService.getDisplayedCardIndex()); + this.playerPositionService.getDisplayedCardIndex() + ); } private _addNewCard(newCard): void { this.playerTranscriptService.addNewCard(newCard); - const explorationLanguageCode = ( - this.explorationPlayerStateService.getLanguageCode()); - const selectedLanguageCode = ( - this.contentTranslationLanguageService.getCurrentContentLanguageCode() - ); + const explorationLanguageCode = + this.explorationPlayerStateService.getLanguageCode(); + const selectedLanguageCode = + this.contentTranslationLanguageService.getCurrentContentLanguageCode(); if (explorationLanguageCode !== selectedLanguageCode) { this.contentTranslationManagerService.displayTranslations( - selectedLanguageCode); + selectedLanguageCode + ); } let totalNumCards = this.playerTranscriptService.getNumCards(); - let previousSupplementalCardIsNonempty = ( + let previousSupplementalCardIsNonempty = totalNumCards > 1 && this.isSupplementalCardNonempty( - this.playerTranscriptService.getCard(totalNumCards - 2))); + this.playerTranscriptService.getCard(totalNumCards - 2) + ); let nextSupplementalCardIsNonempty = this.isSupplementalCardNonempty( - this.playerTranscriptService.getLastCard()); + this.playerTranscriptService.getLastCard() + ); if ( totalNumCards > 1 && this.canWindowShowTwoCards() && !previousSupplementalCardIsNonempty && - nextSupplementalCardIsNonempty) { + nextSupplementalCardIsNonempty + ) { this.playerPositionService.setDisplayedCardIndex(totalNumCards - 1); - this.animateToTwoCards(function() {}); + this.animateToTwoCards(function () {}); } else if ( totalNumCards > 1 && this.canWindowShowTwoCards() && previousSupplementalCardIsNonempty && - !nextSupplementalCardIsNonempty) { + !nextSupplementalCardIsNonempty + ) { this.animateToOneCard(() => { this.playerPositionService.setDisplayedCardIndex(totalNumCards - 1); }); @@ -939,7 +979,8 @@ export class ConversationSkinComponent { this.playerPositionService.setDisplayedCardIndex(totalNumCards - 1); } this.playerPositionService.changeCurrentQuestion( - this.playerPositionService.getDisplayedCardIndex()); + this.playerPositionService.getDisplayedCardIndex() + ); if (this.displayedCard && this.displayedCard.isTerminal()) { this.isRefresherExploration = false; @@ -950,8 +991,8 @@ export class ConversationSkinComponent { if (this.parentExplorationIds.length > 0) { this.isRefresherExploration = true; - let parentExplorationId = this.parentExplorationIds[ - this.parentExplorationIds.length - 1]; + let parentExplorationId = + this.parentExplorationIds[this.parentExplorationIds.length - 1]; recommendedExplorationIds.push(parentExplorationId); } else { recommendedExplorationIds = @@ -961,56 +1002,63 @@ export class ConversationSkinComponent { includeAutogeneratedRecommendations = true; } - if (this.explorationPlayerStateService.isInStoryChapterMode() && - AppConstants.ENABLE_NEW_STRUCTURE_VIEWER_UPDATES) { + if ( + this.explorationPlayerStateService.isInStoryChapterMode() && + AppConstants.ENABLE_NEW_STRUCTURE_VIEWER_UPDATES + ) { recommendedExplorationIds = []; includeAutogeneratedRecommendations = false; - let topicUrlFragment = ( - this.urlService.getUrlParams().topic_url_fragment); - let classroomUrlFragment = ( - this.urlService.getUrlParams().classroom_url_fragment); - let storyUrlFragment = ( - this.urlService.getUrlParams().story_url_fragment); + let topicUrlFragment = + this.urlService.getUrlParams().topic_url_fragment; + let classroomUrlFragment = + this.urlService.getUrlParams().classroom_url_fragment; + let storyUrlFragment = + this.urlService.getUrlParams().story_url_fragment; let nodeId = this.urlService.getUrlParams().node_id; this.inStoryMode = true; - this.storyViewerBackendApiService.fetchStoryDataAsync( - topicUrlFragment, classroomUrlFragment, - storyUrlFragment).then( - (res) => { + this.storyViewerBackendApiService + .fetchStoryDataAsync( + topicUrlFragment, + classroomUrlFragment, + storyUrlFragment + ) + .then(res => { let nextStoryNode: LearnerExplorationSummary[] = []; for (let i = 0; i < res.nodes.length; i++) { - if (res.nodes[i].id === nodeId && - (i + 1) < res.nodes.length) { - this.storyNodeIdToAdd = ( - res.nodes[i].destinationNodeIds[0]); - nextStoryNode.push( - res.nodes[i + 1].explorationSummary); + if (res.nodes[i].id === nodeId && i + 1 < res.nodes.length) { + this.storyNodeIdToAdd = res.nodes[i].destinationNodeIds[0]; + nextStoryNode.push(res.nodes[i + 1].explorationSummary); break; } } this.recommendedExplorationSummaries = nextStoryNode; }); if (this.isLoggedIn) { - this.storyViewerBackendApiService.recordChapterCompletionAsync( - topicUrlFragment, classroomUrlFragment, - storyUrlFragment, nodeId - ).then((returnObject) => { - if (returnObject.readyForReviewTest) { - ( - this.windowRef.nativeWindow as {location: string | Location} - ).location = - this.urlInterpolationService.interpolateUrl( - TopicViewerDomainConstants.REVIEW_TESTS_URL_TEMPLATE, { + this.storyViewerBackendApiService + .recordChapterCompletionAsync( + topicUrlFragment, + classroomUrlFragment, + storyUrlFragment, + nodeId + ) + .then(returnObject => { + if (returnObject.readyForReviewTest) { + ( + this.windowRef.nativeWindow as {location: string | Location} + ).location = this.urlInterpolationService.interpolateUrl( + TopicViewerDomainConstants.REVIEW_TESTS_URL_TEMPLATE, + { topic_url_fragment: topicUrlFragment, classroom_url_fragment: classroomUrlFragment, - story_url_fragment: storyUrlFragment - }); - } - this.learnerDashboardBackendApiService - .fetchLearnerCompletedChaptersCountDataAsync().then( - (responseData) => { - let newCompletedChaptersCount = ( - responseData.completedChaptersCount); + story_url_fragment: storyUrlFragment, + } + ); + } + this.learnerDashboardBackendApiService + .fetchLearnerCompletedChaptersCountDataAsync() + .then(responseData => { + let newCompletedChaptersCount = + responseData.completedChaptersCount; if ( newCompletedChaptersCount !== this.completedChaptersCount ) { @@ -1018,31 +1066,35 @@ export class ConversationSkinComponent { this.chapterIsCompletedForTheFirstTime = true; } }); - }); + }); } else { let loginRedirectUrl = this.urlInterpolationService.interpolateUrl( - StoryViewerDomainConstants.STORY_PROGRESS_URL_TEMPLATE, { + StoryViewerDomainConstants.STORY_PROGRESS_URL_TEMPLATE, + { topic_url_fragment: topicUrlFragment, classroom_url_fragment: classroomUrlFragment, story_url_fragment: storyUrlFragment, - node_id: nodeId - }); + node_id: nodeId, + } + ); this.userService.setReturnUrl(loginRedirectUrl); } } else { this.explorationRecommendationsService.getRecommendedSummaryDicts( recommendedExplorationIds, includeAutogeneratedRecommendations, - (summaries) => { + summaries => { this.recommendedExplorationSummaries = summaries; - }); + } + ); } if (!this.showProgressClearanceMessage) { this.showProgressClearanceMessage = true; setTimeout(() => { let alertInfoElement = document.querySelector( - '.oppia-exploration-checkpoints-message'); + '.oppia-exploration-checkpoints-message' + ); // Remove the alert message after 6 sec. if (alertInfoElement) { @@ -1064,14 +1116,16 @@ export class ConversationSkinComponent { // for clearing concepts. this.playerTranscriptService.addNewResponseToExistingFeedback( this.translateService.instant( - 'I18N_REDIRECTION_TO_STUCK_STATE_MESSAGE') + 'I18N_REDIRECTION_TO_STUCK_STATE_MESSAGE' + ) ); // Enable visibility of ContinueToRevise button. this.continueToReviseStateButtonIsVisible = true; - } else if (this.solutionForState !== null && + } else if ( + this.solutionForState !== null && this.numberOfIncorrectSubmissions >= - ExplorationPlayerConstants. - MAX_INCORRECT_ANSWERS_BEFORE_RELEASING_SOLUTION) { + ExplorationPlayerConstants.MAX_INCORRECT_ANSWERS_BEFORE_RELEASING_SOLUTION + ) { // Release solution if no separate state for addressing // the stuck learner exists and the solution exists. this.hintsAndSolutionManagerService.releaseSolution(); @@ -1087,14 +1141,15 @@ export class ConversationSkinComponent { // Directly trigger action for the really stuck learner. if (this.nextCardIfStuck && this.nextCardIfStuck !== this.displayedCard) { this.playerTranscriptService.addNewResponseToExistingFeedback( - this.translateService.instant( - 'I18N_REDIRECTION_TO_STUCK_STATE_MESSAGE')); + this.translateService.instant('I18N_REDIRECTION_TO_STUCK_STATE_MESSAGE') + ); // Enable visibility of ContinueToRevise button. this.continueToReviseStateButtonIsVisible = true; - } else if (this.solutionForState !== null && + } else if ( + this.solutionForState !== null && this.numberOfIncorrectSubmissions >= - ExplorationPlayerConstants. - MAX_INCORRECT_ANSWERS_BEFORE_RELEASING_SOLUTION) { + ExplorationPlayerConstants.MAX_INCORRECT_ANSWERS_BEFORE_RELEASING_SOLUTION + ) { // Release solution if it exists. this.hintsAndSolutionManagerService.releaseSolution(); } @@ -1116,14 +1171,18 @@ export class ConversationSkinComponent { this.nextCard = initialCard; if (!this.explorationPlayerStateService.isInDiagnosticTestPlayerMode()) { this.explorationPlayerStateService.onPlayerStateChange.emit( - this.nextCard.getStateName()); + this.nextCard.getStateName() + ); } // We do not store checkpoints progress for iframes hence we do not // need to consider redirecting the user to the most recently // reached checkpoint on exploration initial load in that case. - if (!this.isIframed && !this._editorPreviewMode && - !this.explorationPlayerStateService.isInQuestionPlayerMode()) { + if ( + !this.isIframed && + !this._editorPreviewMode && + !this.explorationPlayerStateService.isInQuestionPlayerMode() + ) { // Navigate the learner to the most recently reached checkpoint state. this._navigateToMostRecentlyReachedCheckpoint(); } @@ -1135,15 +1194,13 @@ export class ConversationSkinComponent { // If the exploration is embedded, use the url language code // as site language. If the url language code is not supported // as site language, English is used as default. - let langCodes = AppConstants.SUPPORTED_SITE_LANGUAGES.map((language) => { + let langCodes = AppConstants.SUPPORTED_SITE_LANGUAGES.map(language => { return language.id; }) as string[]; if (this.isIframed) { - let urlLanguageCode = ( - this.urlService.getUrlParams().lang); + let urlLanguageCode = this.urlService.getUrlParams().lang; if (urlLanguageCode && langCodes.indexOf(urlLanguageCode) !== -1) { - this.i18nLanguageCodeService.setI18nLanguageCode( - urlLanguageCode); + this.i18nLanguageCodeService.setI18nLanguageCode(urlLanguageCode); } else { this.i18nLanguageCodeService.setI18nLanguageCode('en'); } @@ -1159,12 +1216,10 @@ export class ConversationSkinComponent { } skipCurrentQuestion(): void { - this.explorationPlayerStateService.skipCurrentQuestion( - (nextCard) => { - this.nextCard = nextCard; - this.showPendingCard(); - } - ); + this.explorationPlayerStateService.skipCurrentQuestion(nextCard => { + this.nextCard = nextCard; + this.showPendingCard(); + }); } initializePage(): void { @@ -1175,7 +1230,8 @@ export class ConversationSkinComponent { this.explorationPlayerStateService.initializeQuestionPlayer( this.questionPlayerConfig, this._initializeDirectiveComponents.bind(this), - this.showQuestionAreNotAvailable); + this.showQuestionAreNotAvailable + ); } else if (this.diagnosticTestTopicTrackerModel) { this.explorationPlayerStateService.initializeDiagnosticPlayer( this.diagnosticTestTopicTrackerModel, @@ -1183,18 +1239,23 @@ export class ConversationSkinComponent { ); } else { this.explorationPlayerStateService.initializePlayer( - this._initializeDirectiveComponents.bind(this)); + this._initializeDirectiveComponents.bind(this) + ); } } submitAnswer( - answer: string, interactionRulesService: InteractionRulesService): void { + answer: string, + interactionRulesService: InteractionRulesService + ): void { this.displayedCard.updateCurrentAnswer(null); // Safety check to prevent double submissions from occurring. - if (this.answerIsBeingProcessed || + if ( + this.answerIsBeingProcessed || !this.isCurrentCardAtEndOfTranscript() || - this.displayedCard.isCompleted()) { + this.displayedCard.isCompleted() + ) { return; } @@ -1207,14 +1268,18 @@ export class ConversationSkinComponent { } } - if (!this.isInPreviewMode && - !this.explorationPlayerStateService.isPresentingIsolatedQuestions() && - AppConstants.ENABLE_SOLICIT_ANSWER_DETAILS_FEATURE + if ( + !this.isInPreviewMode && + !this.explorationPlayerStateService.isPresentingIsolatedQuestions() && + AppConstants.ENABLE_SOLICIT_ANSWER_DETAILS_FEATURE ) { this.initLearnerAnswerInfoService( - this.explorationId, this.explorationEngineService.getState(), - answer, interactionRulesService, - this.alwaysAskLearnerForAnswerDetails()); + this.explorationId, + this.explorationEngineService.getState(), + answer, + interactionRulesService, + this.alwaysAskLearnerForAnswerDetails() + ); } this.numberAttemptsService.submitAttempt(); @@ -1227,12 +1292,13 @@ export class ConversationSkinComponent { if (this.getCanAskLearnerForAnswerInfo()) { setTimeout(() => { this.playerTranscriptService.addNewResponse( - this.learnerAnswerInfoService.getSolicitAnswerDetailsQuestion()); + this.learnerAnswerInfoService.getSolicitAnswerDetailsQuestion() + ); this.answerIsBeingProcessed = false; this.playerPositionService.onHelpCardAvailable.emit({ - helpCardHtml: ( - this.learnerAnswerInfoService.getSolicitAnswerDetailsQuestion()), - hasContinueButton: false + helpCardHtml: + this.learnerAnswerInfoService.getSolicitAnswerDetailsQuestion(), + hasContinueButton: false, }); }, 100); return; @@ -1243,23 +1309,37 @@ export class ConversationSkinComponent { let currentEngineService = this.explorationPlayerStateService.getCurrentEngineService(); this.answerIsCorrect = currentEngineService.submitAnswer( - answer, interactionRulesService, ( - nextCard, refreshInteraction, feedbackHtml, - feedbackAudioTranslations, refresherExplorationId, - missingPrerequisiteSkillId, remainOnCurrentCard, - taggedSkillMisconceptionId, wasOldStateInitial, - isFirstHit, isFinalQuestion, nextCardIfReallyStuck, focusLabel,) => { + answer, + interactionRulesService, + ( + nextCard, + refreshInteraction, + feedbackHtml, + feedbackAudioTranslations, + refresherExplorationId, + missingPrerequisiteSkillId, + remainOnCurrentCard, + taggedSkillMisconceptionId, + wasOldStateInitial, + isFirstHit, + isFinalQuestion, + nextCardIfReallyStuck, + focusLabel + ) => { this.nextCard = nextCard; this.nextCardIfStuck = nextCardIfReallyStuck; - if (!this._editorPreviewMode && - !this.explorationPlayerStateService.isPresentingIsolatedQuestions() + if ( + !this._editorPreviewMode && + !this.explorationPlayerStateService.isPresentingIsolatedQuestions() ) { - let oldStateName = - this.playerPositionService.getCurrentStateName(); + let oldStateName = this.playerPositionService.getCurrentStateName(); if (!remainOnCurrentCard) { this.statsReportingService.recordStateTransition( - oldStateName, nextCard.getStateName(), answer, - this.learnerParamsService.getAllParams(), isFirstHit, + oldStateName, + nextCard.getStateName(), + answer, + this.learnerParamsService.getAllParams(), + isFirstHit, String( this.completedChaptersCount && this.completedChaptersCount + 1 ), @@ -1267,16 +1347,17 @@ export class ConversationSkinComponent { currentEngineService.getLanguageCode() ); - this.statsReportingService.recordStateCompleted( - oldStateName); + this.statsReportingService.recordStateCompleted(oldStateName); } if (nextCard.isTerminal()) { this.statsReportingService.recordStateCompleted( - nextCard.getStateName()); + nextCard.getStateName() + ); } if (wasOldStateInitial && !this.explorationActuallyStarted) { this.statsReportingService.recordExplorationActuallyStarted( - oldStateName); + oldStateName + ); this.explorationActuallyStarted = true; } } @@ -1285,14 +1366,16 @@ export class ConversationSkinComponent { !this.explorationPlayerStateService.isPresentingIsolatedQuestions() ) { this.explorationPlayerStateService.onPlayerStateChange.emit( - nextCard.getStateName()); + nextCard.getStateName() + ); } else if ( this.explorationPlayerStateService.isInQuestionPlayerMode() ) { this.questionPlayerStateService.answerSubmitted( this.questionPlayerEngineService.getCurrentQuestion(), !remainOnCurrentCard, - taggedSkillMisconceptionId); + taggedSkillMisconceptionId + ); } let millisecsLeftToWait: number; @@ -1308,8 +1391,11 @@ export class ConversationSkinComponent { // is not required. millisecsLeftToWait = 1.0; } else { - millisecsLeftToWait = Math.max(this.MIN_CARD_LOADING_DELAY_MSEC - ( - new Date().getTime() - timeAtServerCall), 1.0); + millisecsLeftToWait = Math.max( + this.MIN_CARD_LOADING_DELAY_MSEC - + (new Date().getTime() - timeAtServerCall), + 1.0 + ); } setTimeout(() => { @@ -1318,7 +1404,7 @@ export class ConversationSkinComponent { this.audioPlayerService.onAutoplayAudio.emit({ audioTranslations: feedbackAudioTranslations, html: feedbackHtml, - componentName: AppConstants.COMPONENT_NAME_FEEDBACK + componentName: AppConstants.COMPONENT_NAME_FEEDBACK, }); if (remainOnCurrentCard) { @@ -1326,7 +1412,8 @@ export class ConversationSkinComponent { feedbackHtml, missingPrerequisiteSkillId, refreshInteraction, - refresherExplorationId); + refresherExplorationId + ); } else { this.moveToNewCard(feedbackHtml, isFinalQuestion, nextCard); } @@ -1337,49 +1424,47 @@ export class ConversationSkinComponent { } private giveFeedbackAndStayOnCurrentCard( - feedbackHtml: string | null, - missingPrerequisiteSkillId: string | null, - refreshInteraction: boolean, - refresherExplorationId: string | null + feedbackHtml: string | null, + missingPrerequisiteSkillId: string | null, + refreshInteraction: boolean, + refresherExplorationId: string | null ) { this.numberOfIncorrectSubmissions++; this.hintsAndSolutionManagerService.recordWrongAnswer(); this.conceptCardManagerService.recordWrongAnswer(); this.playerTranscriptService.addNewResponse(feedbackHtml); let helpCardAvailable = false; - if (feedbackHtml && - !this.displayedCard.isInteractionInline()) { + if (feedbackHtml && !this.displayedCard.isInteractionInline()) { helpCardAvailable = true; } if (helpCardAvailable) { this.playerPositionService.onHelpCardAvailable.emit({ helpCardHtml: feedbackHtml, - hasContinueButton: false + hasContinueButton: false, }); } if (missingPrerequisiteSkillId) { this.displayedCard.markAsCompleted(); - this.conceptCardBackendApiService.loadConceptCardsAsync( - [missingPrerequisiteSkillId] - ).then((conceptCardObject) => { - this.conceptCard = conceptCardObject[0]; - if (helpCardAvailable) { - this.playerPositionService.onHelpCardAvailable.emit({ - helpCardHtml: feedbackHtml, - hasContinueButton: true - }); - } - }); + this.conceptCardBackendApiService + .loadConceptCardsAsync([missingPrerequisiteSkillId]) + .then(conceptCardObject => { + this.conceptCard = conceptCardObject[0]; + if (helpCardAvailable) { + this.playerPositionService.onHelpCardAvailable.emit({ + helpCardHtml: feedbackHtml, + hasContinueButton: true, + }); + } + }); } if (refreshInteraction) { // Replace the previous interaction with another of the // same type. - this._nextFocusLabel = ( - this.focusManagerService.generateFocusLabel()); + this._nextFocusLabel = this.focusManagerService.generateFocusLabel(); this.playerTranscriptService.updateLatestInteractionHtml( - this.displayedCard.getInteractionHtml() + - this._getRandomSuffix()); + this.displayedCard.getInteractionHtml() + this._getRandomSuffix() + ); } this.redirectToRefresherExplorationConfirmed = false; @@ -1393,11 +1478,12 @@ export class ConversationSkinComponent { }; this.explorationSummaryBackendApiService .loadPublicExplorationSummariesAsync([refresherExplorationId]) - .then((response) => { + .then(response => { if (response.summaries.length > 0) { - this.refresherExplorationConfirmationModalService. - displayRedirectConfirmationModal( - refresherExplorationId, confirmRedirection); + this.refresherExplorationConfirmationModalService.displayRedirectConfirmationModal( + refresherExplorationId, + confirmRedirection + ); } }); } @@ -1406,9 +1492,10 @@ export class ConversationSkinComponent { } private moveToNewCard( - feedbackHtml: string | null, - isFinalQuestion: boolean, - nextCard: StateCard) { + feedbackHtml: string | null, + isFinalQuestion: boolean, + nextCard: StateCard + ) { // There is a new card. If there is no feedback, move on // immediately. Otherwise, give the learner a chance to read // the feedback, and display a 'Continue' button. @@ -1422,11 +1509,10 @@ export class ConversationSkinComponent { this.moveToExploration = true; if (feedbackHtml) { this.playerTranscriptService.addNewResponse(feedbackHtml); - if ( - !this.displayedCard.isInteractionInline()) { + if (!this.displayedCard.isInteractionInline()) { this.playerPositionService.onHelpCardAvailable.emit({ helpCardHtml: feedbackHtml, - hasContinueButton: true + hasContinueButton: true, }); } } else { @@ -1438,32 +1524,32 @@ export class ConversationSkinComponent { this.fatigueDetectionService.reset(); this.numberAttemptsService.reset(); - let _isNextInteractionInline = - this.nextCard.isInteractionInline(); - this.upcomingInlineInteractionHtml = ( - _isNextInteractionInline ? - this.nextCard.getInteractionHtml() : ''); - this.upcomingInteractionInstructions = ( - this.nextCard.getInteractionInstructions()); + let _isNextInteractionInline = this.nextCard.isInteractionInline(); + this.upcomingInlineInteractionHtml = _isNextInteractionInline + ? this.nextCard.getInteractionHtml() + : ''; + this.upcomingInteractionInstructions = + this.nextCard.getInteractionInstructions(); if (feedbackHtml) { if ( this.playerTranscriptService.hasEncounteredStateBefore( - nextCard.getStateName())) { + nextCard.getStateName() + ) + ) { this.pendingCardWasSeenBefore = true; } this.playerTranscriptService.addNewResponse(feedbackHtml); if (!this.displayedCard.isInteractionInline()) { this.playerPositionService.onHelpCardAvailable.emit({ helpCardHtml: feedbackHtml, - hasContinueButton: true + hasContinueButton: true, }); } this.playerPositionService.onNewCardAvailable.emit(); - this._nextFocusLabel = ( - ExplorationPlayerConstants.CONTINUE_BUTTON_FOCUS_LABEL); - this.focusManagerService.setFocusIfOnDesktop( - this._nextFocusLabel); + this._nextFocusLabel = + ExplorationPlayerConstants.CONTINUE_BUTTON_FOCUS_LABEL; + this.focusManagerService.setFocusIfOnDesktop(this._nextFocusLabel); this.scrollToBottom(); } else { this.playerTranscriptService.addNewResponse(feedbackHtml); @@ -1480,65 +1566,86 @@ export class ConversationSkinComponent { this.startCardChangeAnimation = true; this.explorationPlayerStateService.recordNewCardAdded(); - setTimeout(() => { - this._addNewCard(this.nextCard); + setTimeout( + () => { + this._addNewCard(this.nextCard); - this.upcomingInlineInteractionHtml = null; - this.upcomingInteractionInstructions = null; - }, 0.1 * TIME_FADEOUT_MSEC + 0.1 * TIME_HEIGHT_CHANGE_MSEC); + this.upcomingInlineInteractionHtml = null; + this.upcomingInteractionInstructions = null; + }, + 0.1 * TIME_FADEOUT_MSEC + 0.1 * TIME_HEIGHT_CHANGE_MSEC + ); - setTimeout(() => { - this.focusManagerService.setFocusIfOnDesktop(this._nextFocusLabel); - this.scrollToTop(); - }, - 0.1 * TIME_FADEOUT_MSEC + TIME_HEIGHT_CHANGE_MSEC + - 0.5 * TIME_FADEIN_MSEC); + setTimeout( + () => { + this.focusManagerService.setFocusIfOnDesktop(this._nextFocusLabel); + this.scrollToTop(); + }, + 0.1 * TIME_FADEOUT_MSEC + TIME_HEIGHT_CHANGE_MSEC + 0.5 * TIME_FADEIN_MSEC + ); - setTimeout(() => { - this.startCardChangeAnimation = false; - }, - 0.1 * TIME_FADEOUT_MSEC + TIME_HEIGHT_CHANGE_MSEC + TIME_FADEIN_MSEC + - this.TIME_PADDING_MSEC); + setTimeout( + () => { + this.startCardChangeAnimation = false; + }, + 0.1 * TIME_FADEOUT_MSEC + + TIME_HEIGHT_CHANGE_MSEC + + TIME_FADEIN_MSEC + + this.TIME_PADDING_MSEC + ); this.playerPositionService.onNewCardOpened.emit(this.nextCard); } showUpcomingCard(): void { let currentIndex = this.playerPositionService.getDisplayedCardIndex(); - let conceptCardIsBeingShown = ( + let conceptCardIsBeingShown = this.displayedCard.getStateName() === null && - !this.explorationPlayerStateService.isInQuestionMode()); - if (conceptCardIsBeingShown && - this.playerTranscriptService.isLastCard(currentIndex)) { + !this.explorationPlayerStateService.isInQuestionMode(); + if ( + conceptCardIsBeingShown && + this.playerTranscriptService.isLastCard(currentIndex) + ) { this.returnToExplorationAfterConceptCard(); return; } if (this.questionSessionCompleted) { this.questionPlayerStateService.onQuestionSessionCompleted.emit( - this.questionPlayerStateService.getQuestionPlayerStateData()); + this.questionPlayerStateService.getQuestionPlayerStateData() + ); return; } if (this.moveToExploration) { this.moveToExploration = false; this.explorationPlayerStateService.moveToExploration( - this._initializeDirectiveComponents.bind(this)); + this._initializeDirectiveComponents.bind(this) + ); return; } if ( this.displayedCard.isCompleted() && - (this.nextCard.getStateName() === - this.displayedCard.getStateName()) && this.conceptCard) { + this.nextCard.getStateName() === this.displayedCard.getStateName() && + this.conceptCard + ) { this.explorationPlayerStateService.recordNewCardAdded(); this._addNewCard( StateCard.createNewCard( - null, this.conceptCard.getExplanation().html, null, null, null, - null, this.audioTranslationLanguageService)); + null, + this.conceptCard.getExplanation().html, + null, + null, + null, + null, + this.audioTranslationLanguageService + ) + ); return; } if (this.isLearnAgainButton()) { const indexOfRevisionCard = this.playerTranscriptService.findIndexOfLatestStateWithName( - this.nextCard.getStateName()); + this.nextCard.getStateName() + ); if (indexOfRevisionCard !== null) { this.displayedCard.markAsNotCompleted(); this.changeCard(indexOfRevisionCard); @@ -1568,25 +1675,30 @@ export class ConversationSkinComponent { if (tutorCard && tutorCard.length === 0) { return; } - let tutorCardBottom = ( - tutorCard.offset().top + tutorCard.outerHeight()); - if ($(window).scrollTop() + - $(window).height() < tutorCardBottom) { - $('html, body').animate({ - scrollTop: tutorCardBottom - $(window).height() + 12 - }, { - duration: this.TIME_SCROLL_MSEC, - easing: 'easeOutQuad' - }); + let tutorCardBottom = tutorCard.offset().top + tutorCard.outerHeight(); + if ($(window).scrollTop() + $(window).height() < tutorCardBottom) { + $('html, body').animate( + { + scrollTop: tutorCardBottom - $(window).height() + 12, + }, + { + duration: this.TIME_SCROLL_MSEC, + easing: 'easeOutQuad', + } + ); } }, 100); } scrollToTop(): void { setTimeout(() => { - $('html, body').animate({ - scrollTop: 0 - }, 800, 'easeOutQuart'); + $('html, body').animate( + { + scrollTop: 0, + }, + 800, + 'easeOutQuart' + ); return false; }); } @@ -1594,13 +1706,16 @@ export class ConversationSkinComponent { // Returns whether the screen is wide enough to fit two // cards (e.g., the tutor and supplemental cards) side-by-side. canWindowShowTwoCards(): boolean { - return this.windowDimensionsService.getWidth() > - ExplorationPlayerConstants.TWO_CARD_THRESHOLD_PX; + return ( + this.windowDimensionsService.getWidth() > + ExplorationPlayerConstants.TWO_CARD_THRESHOLD_PX + ); } onNavigateFromIframe(): void { this.siteAnalyticsService.registerVisitOppiaFromIframeEvent( - this.explorationId); + this.explorationId + ); } submitAnswerFromProgressNav(): void { @@ -1609,18 +1724,15 @@ export class ConversationSkinComponent { } getRecommendedExpTitleTranslationKey(explorationId: string): string { - return ( - this.i18nLanguageCodeService.getExplorationTranslationKey( - explorationId, - TranslationKeyType.TITLE - ) + return this.i18nLanguageCodeService.getExplorationTranslationKey( + explorationId, + TranslationKeyType.TITLE ); } isHackyExpTitleTranslationDisplayed(explorationId: string): boolean { - let recommendedExpTitleTranslationKey = ( - this.getRecommendedExpTitleTranslationKey(explorationId) - ); + let recommendedExpTitleTranslationKey = + this.getRecommendedExpTitleTranslationKey(explorationId); return ( this.i18nLanguageCodeService.isHackyTranslationAvailable( recommendedExpTitleTranslationKey @@ -1631,9 +1743,9 @@ export class ConversationSkinComponent { isDisplayedCardCompletedInPrevSession(): boolean { return ( this.displayedCard.getInteraction() && - (this.prevSessionStatesProgress.indexOf( - this.displayedCard.getStateName()) !== -1 - ) + this.prevSessionStatesProgress.indexOf( + this.displayedCard.getStateName() + ) !== -1 ); } @@ -1642,7 +1754,9 @@ export class ConversationSkinComponent { } } -angular.module('oppia').directive('oppiaConversationSkin', +angular.module('oppia').directive( + 'oppiaConversationSkin', downgradeComponent({ - component: ConversationSkinComponent - }) as angular.IDirectiveFactory); + component: ConversationSkinComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-player-page/learner-experience/end-chapter-check-mark.component.spec.ts b/core/templates/pages/exploration-player-page/learner-experience/end-chapter-check-mark.component.spec.ts index f1fc74f9d603..195a8e455166 100644 --- a/core/templates/pages/exploration-player-page/learner-experience/end-chapter-check-mark.component.spec.ts +++ b/core/templates/pages/exploration-player-page/learner-experience/end-chapter-check-mark.component.spec.ts @@ -17,16 +17,16 @@ * check mark component. */ -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { EndChapterCheckMarkComponent } from './end-chapter-check-mark.component'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {EndChapterCheckMarkComponent} from './end-chapter-check-mark.component'; -describe('End chapter check mark component', function() { +describe('End chapter check mark component', function () { let component: EndChapterCheckMarkComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [EndChapterCheckMarkComponent] + declarations: [EndChapterCheckMarkComponent], }).compileComponents(); })); diff --git a/core/templates/pages/exploration-player-page/learner-experience/end-chapter-check-mark.component.ts b/core/templates/pages/exploration-player-page/learner-experience/end-chapter-check-mark.component.ts index fcc24b331071..0572211e5d4b 100644 --- a/core/templates/pages/exploration-player-page/learner-experience/end-chapter-check-mark.component.ts +++ b/core/templates/pages/exploration-player-page/learner-experience/end-chapter-check-mark.component.ts @@ -16,7 +16,7 @@ * @fileoverview Component for the end chapter celebration check mark component. */ -import { Component } from '@angular/core'; +import {Component} from '@angular/core'; @Component({ selector: 'oppia-end-chapter-check-mark', diff --git a/core/templates/pages/exploration-player-page/learner-experience/end-chapter-confetti.component.spec.ts b/core/templates/pages/exploration-player-page/learner-experience/end-chapter-confetti.component.spec.ts index dac6202daa0e..12bef829fef0 100644 --- a/core/templates/pages/exploration-player-page/learner-experience/end-chapter-confetti.component.spec.ts +++ b/core/templates/pages/exploration-player-page/learner-experience/end-chapter-confetti.component.spec.ts @@ -1,4 +1,3 @@ - // Copyright 2022 The Oppia Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,11 +17,11 @@ * confetti component. */ -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { EndChapterConfettiComponent } from './end-chapter-confetti.component'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {EndChapterConfettiComponent} from './end-chapter-confetti.component'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; -describe('End chapter confetti component', function() { +describe('End chapter confetti component', function () { let component: EndChapterConfettiComponent; let fixture: ComponentFixture; @@ -35,10 +34,12 @@ describe('End chapter confetti component', function() { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [EndChapterConfettiComponent], - providers: [{ - provide: UrlInterpolationService, - useClass: MockUrlInterpolationService - }] + providers: [ + { + provide: UrlInterpolationService, + useClass: MockUrlInterpolationService, + }, + ], }).compileComponents(); })); diff --git a/core/templates/pages/exploration-player-page/learner-experience/end-chapter-confetti.component.ts b/core/templates/pages/exploration-player-page/learner-experience/end-chapter-confetti.component.ts index 1d6ccbb8a7d6..4ec8bf498cf3 100644 --- a/core/templates/pages/exploration-player-page/learner-experience/end-chapter-confetti.component.ts +++ b/core/templates/pages/exploration-player-page/learner-experience/end-chapter-confetti.component.ts @@ -16,8 +16,8 @@ * @fileoverview Component for the end chapter celebration confetti component. */ -import { Component, OnInit } from '@angular/core'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; +import {Component, OnInit} from '@angular/core'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; @Component({ selector: 'oppia-end-chapter-confetti', @@ -32,7 +32,8 @@ export class EndChapterConfettiComponent implements OnInit { ngOnInit(): void { this.endChapterCelebratoryAudio.src = this.urlInterpolationService.getStaticAudioUrl( - '/end_chapter_celebratory_tadaa.mp3'); + '/end_chapter_celebratory_tadaa.mp3' + ); this.endChapterCelebratoryAudio.load(); } diff --git a/core/templates/pages/exploration-player-page/learner-experience/input-response-pair.component.spec.ts b/core/templates/pages/exploration-player-page/learner-experience/input-response-pair.component.spec.ts index ed983e64bc2b..be8588da9b5d 100644 --- a/core/templates/pages/exploration-player-page/learner-experience/input-response-pair.component.spec.ts +++ b/core/templates/pages/exploration-player-page/learner-experience/input-response-pair.component.spec.ts @@ -16,26 +16,26 @@ * @fileoverview Unit tests for LearnerAnswerInfoCard */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { BackgroundMaskService } from 'services/stateful/background-mask.service'; -import { ExplorationHtmlFormatterService } from 'services/exploration-html-formatter.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { PlayerTranscriptService } from '../services/player-transcript.service'; -import { InputResponsePairComponent } from './input-response-pair.component'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { RecordedVoiceovers } from 'domain/exploration/recorded-voiceovers.model'; -import { StateCard } from 'domain/state_card/state-card.model'; -import { InteractionObjectFactory } from 'domain/exploration/InteractionObjectFactory'; -import { AudioTranslationLanguageService } from '../services/audio-translation-language.service'; -import { AudioTranslationManagerService } from '../services/audio-translation-manager.service'; -import { AppConstants } from 'app.constants'; -import { AudioPlayerService } from 'services/audio-player.service'; -import { ExplorationPlayerConstants } from '../exploration-player-page.constants'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { AutogeneratedAudioPlayerService } from 'services/autogenerated-audio-player.service'; -import { NumberConversionService } from 'services/number-conversion.service'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {BackgroundMaskService} from 'services/stateful/background-mask.service'; +import {ExplorationHtmlFormatterService} from 'services/exploration-html-formatter.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {PlayerTranscriptService} from '../services/player-transcript.service'; +import {InputResponsePairComponent} from './input-response-pair.component'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; +import {RecordedVoiceovers} from 'domain/exploration/recorded-voiceovers.model'; +import {StateCard} from 'domain/state_card/state-card.model'; +import {InteractionObjectFactory} from 'domain/exploration/InteractionObjectFactory'; +import {AudioTranslationLanguageService} from '../services/audio-translation-language.service'; +import {AudioTranslationManagerService} from '../services/audio-translation-manager.service'; +import {AppConstants} from 'app.constants'; +import {AudioPlayerService} from 'services/audio-player.service'; +import {ExplorationPlayerConstants} from '../exploration-player-page.constants'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {AutogeneratedAudioPlayerService} from 'services/autogenerated-audio-player.service'; +import {NumberConversionService} from 'services/number-conversion.service'; describe('InputResponsePairComponent', () => { let component: InputResponsePairComponent; @@ -53,14 +53,9 @@ describe('InputResponsePairComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule, NgbModule], - declarations: [ - InputResponsePairComponent, - MockTranslatePipe - ], - providers: [ - BackgroundMaskService - ], - schemas: [NO_ERRORS_SCHEMA] + declarations: [InputResponsePairComponent, MockTranslatePipe], + providers: [BackgroundMaskService], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -70,22 +65,28 @@ describe('InputResponsePairComponent', () => { playerTranscriptService = TestBed.get(PlayerTranscriptService); interactionObjectFactory = TestBed.get(InteractionObjectFactory); autogeneratedAudioPlayerService = TestBed.get( - AutogeneratedAudioPlayerService); + AutogeneratedAudioPlayerService + ); audioTranslationManagerService = TestBed.get( - AudioTranslationManagerService); + AudioTranslationManagerService + ); audioTranslationLanguageService = TestBed.get( - AudioTranslationLanguageService); + AudioTranslationLanguageService + ); i18nLanguageCodeService = TestBed.get(I18nLanguageCodeService); numberConversionService = TestBed.inject(NumberConversionService); fixture = TestBed.createComponent(InputResponsePairComponent); component = fixture.componentInstance; spyOn(i18nLanguageCodeService, 'isCurrentLanguageRTL').and.returnValue( - true); + true + ); - spyOn(playerTranscriptService, 'getCard') - .and.returnValue(StateCard.createNewCard( - 'State 2', '

Content

', '', + spyOn(playerTranscriptService, 'getCard').and.returnValue( + StateCard.createNewCard( + 'State 2', + '

Content

', + '', interactionObjectFactory.createFromBackendDict({ id: 'GraphInput', answer_groups: [ @@ -128,8 +129,8 @@ describe('InputResponsePairComponent', () => { value: 1, }, catchMisspellings: { - value: false - } + value: false, + }, }, hints: [], solution: { @@ -139,33 +140,38 @@ describe('InputResponsePairComponent', () => { content_id: '2', html: 'test_explanation1', }, - } + }, }), RecordedVoiceovers.createEmpty(), - 'content', audioTranslationLanguageService - )); + 'content', + audioTranslationLanguageService + ) + ); }); it('should check if input response contains video rte element', () => { component.data = { learnerInput: '', oppiaResponse: 'oppia-noninteractive-video-response', - isHint: true + isHint: true, }; expect(component.isVideoRteElementPresentInResponse()).toBe(true); }); - it('should return false if input response does not contain ' + - 'video rte element', () => { - component.data = { - learnerInput: '', - oppiaResponse: null, - isHint: true - }; + it( + 'should return false if input response does not contain ' + + 'video rte element', + () => { + component.data = { + learnerInput: '', + oppiaResponse: null, + isHint: true, + }; - expect(component.isVideoRteElementPresentInResponse()).toBeFalse(); - }); + expect(component.isVideoRteElementPresentInResponse()).toBeFalse(); + } + ); it('should get answer html for the displayed card', () => { spyOn(explorationHtmlFormatter, 'getAnswerHtml').and.returnValue( @@ -174,7 +180,7 @@ describe('InputResponsePairComponent', () => { component.data = { learnerInput: '', oppiaResponse: 'oppia-noninteractive-video-response', - isHint: true + isHint: true, }; expect(component.getAnswerHtml()).toBe('

HTML Answer

'); @@ -187,7 +193,7 @@ describe('InputResponsePairComponent', () => { // to only test the toggle function. // @ts-expect-error component.popover = { - toggle: () => {} + toggle: () => {}, }; spyOn(component.popover, 'toggle'); @@ -197,8 +203,9 @@ describe('InputResponsePairComponent', () => { }); it('should get a short summary of the answer', () => { - spyOn(explorationHtmlFormatter, 'getShortAnswerHtml') - .and.returnValue('Short Answer'); + spyOn(explorationHtmlFormatter, 'getShortAnswerHtml').and.returnValue( + 'Short Answer' + ); component.data = { // This throws "Type '{ answerDetails: string; }' is not assignable to // type 'string'.". We need to suppress this error because we need to @@ -206,10 +213,10 @@ describe('InputResponsePairComponent', () => { // avoid the lint error "This test should have at least one expectation.". // @ts-ignore learnerInput: { - answerDetails: 'Answer Details' + answerDetails: 'Answer Details', }, oppiaResponse: 'oppia-noninteractive-video-response', - isHint: true + isHint: true, }; expect(component.getShortAnswerHtml()).toBe('Answer Details'); @@ -217,7 +224,7 @@ describe('InputResponsePairComponent', () => { component.data = { learnerInput: '', oppiaResponse: 'oppia-noninteractive-video-response', - isHint: true + isHint: true, }; expect(component.getShortAnswerHtml()).toBe('Short Answer'); @@ -230,8 +237,10 @@ describe('InputResponsePairComponent', () => { }); it('should get the css class for feedback audio highlight', () => { - spyOn(audioTranslationManagerService, 'getCurrentComponentName') - .and.returnValue(AppConstants.COMPONENT_NAME_FEEDBACK); + spyOn( + audioTranslationManagerService, + 'getCurrentComponentName' + ).and.returnValue(AppConstants.COMPONENT_NAME_FEEDBACK); spyOn(audioPlayerService, 'isPlaying').and.returnValue(true); component.isLastPair = false; @@ -244,46 +253,65 @@ describe('InputResponsePairComponent', () => { ); }); - it('should return empty css class for feedback audio highlight ' + - 'when audio player service is not playing', () => { - spyOn(audioTranslationManagerService, 'getCurrentComponentName') - .and.returnValue(AppConstants.COMPONENT_NAME_FEEDBACK); - spyOn(audioPlayerService, 'isPlaying').and.returnValue(false); - component.isLastPair = true; - - expect(component.getFeedbackAudioHighlightClass()).toBe(''); - }); - - it('should return empty css class for feedback audio highlight ' + - 'when auto generated audio player service is not playing', () => { - spyOn(audioTranslationManagerService, 'getCurrentComponentName') - .and.returnValue(AppConstants.COMPONENT_NAME_FEEDBACK); - spyOn(autogeneratedAudioPlayerService, 'isPlaying').and.returnValue(false); - component.isLastPair = true; - - expect(component.getFeedbackAudioHighlightClass()).toBe(''); - }); - - it('should return empty css class for feedback audio highlight ' + - 'when current component name does not match', () => { - spyOn(audioTranslationManagerService, 'getCurrentComponentName') - .and.returnValue('sample'); - spyOn(audioPlayerService, 'isPlaying').and.returnValue(true); - component.isLastPair = true; - - expect(component.getFeedbackAudioHighlightClass()).toBe(''); - }); - - it('should determine if a string is a number', ()=>{ + it( + 'should return empty css class for feedback audio highlight ' + + 'when audio player service is not playing', + () => { + spyOn( + audioTranslationManagerService, + 'getCurrentComponentName' + ).and.returnValue(AppConstants.COMPONENT_NAME_FEEDBACK); + spyOn(audioPlayerService, 'isPlaying').and.returnValue(false); + component.isLastPair = true; + + expect(component.getFeedbackAudioHighlightClass()).toBe(''); + } + ); + + it( + 'should return empty css class for feedback audio highlight ' + + 'when auto generated audio player service is not playing', + () => { + spyOn( + audioTranslationManagerService, + 'getCurrentComponentName' + ).and.returnValue(AppConstants.COMPONENT_NAME_FEEDBACK); + spyOn(autogeneratedAudioPlayerService, 'isPlaying').and.returnValue( + false + ); + component.isLastPair = true; + + expect(component.getFeedbackAudioHighlightClass()).toBe(''); + } + ); + + it( + 'should return empty css class for feedback audio highlight ' + + 'when current component name does not match', + () => { + spyOn( + audioTranslationManagerService, + 'getCurrentComponentName' + ).and.returnValue('sample'); + spyOn(audioPlayerService, 'isPlaying').and.returnValue(true); + component.isLastPair = true; + + expect(component.getFeedbackAudioHighlightClass()).toBe(''); + } + ); + + it('should determine if a string is a number', () => { let number = '-12.4'; expect(component.isStringifiedNumber(number)).toEqual(true); number = 'ab12'; expect(component.isStringifiedNumber(number)).toEqual(false); }); - it('should convert answer to a local format if it is a number', ()=>{ - spyOn(numberConversionService, 'currentDecimalSeparator') - .and.returnValues(',', '.'); + it('should convert answer to a local format if it is a number', () => { + spyOn(numberConversionService, 'currentDecimalSeparator').and.returnValues( + ',', + '.' + ); let number = '12.2'; expect(component.convertAnswerToLocalFormat(number)).toEqual('12,2'); diff --git a/core/templates/pages/exploration-player-page/learner-experience/input-response-pair.component.ts b/core/templates/pages/exploration-player-page/learner-experience/input-response-pair.component.ts index 060e9fdc9070..126282d33336 100644 --- a/core/templates/pages/exploration-player-page/learner-experience/input-response-pair.component.ts +++ b/core/templates/pages/exploration-player-page/learner-experience/input-response-pair.component.ts @@ -16,32 +16,34 @@ * @fileoverview Component for an input/response pair in the learner view. */ -import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'; -import { AppConstants } from 'app.constants'; -import { InputResponsePair } from 'domain/state_card/state-card.model'; -import { InteractionSpecsConstants, InteractionSpecsKey } from 'pages/interaction-specs.constants'; -import { AudioPlayerService } from 'services/audio-player.service'; -import { AutogeneratedAudioPlayerService } from 'services/autogenerated-audio-player.service'; -import { ExplorationHtmlFormatterService } from 'services/exploration-html-formatter.service'; -import { ExplorationPlayerConstants } from '../exploration-player-page.constants'; -import { AudioTranslationManagerService } from '../services/audio-translation-manager.service'; -import { PlayerPositionService } from '../services/player-position.service'; -import { PlayerTranscriptService } from '../services/player-transcript.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { Interaction } from 'domain/exploration/InteractionObjectFactory'; -import { NumberConversionService } from 'services/number-conversion.service'; +import {Component, EventEmitter, Input, Output, ViewChild} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbPopover} from '@ng-bootstrap/ng-bootstrap'; +import {AppConstants} from 'app.constants'; +import {InputResponsePair} from 'domain/state_card/state-card.model'; +import { + InteractionSpecsConstants, + InteractionSpecsKey, +} from 'pages/interaction-specs.constants'; +import {AudioPlayerService} from 'services/audio-player.service'; +import {AutogeneratedAudioPlayerService} from 'services/autogenerated-audio-player.service'; +import {ExplorationHtmlFormatterService} from 'services/exploration-html-formatter.service'; +import {ExplorationPlayerConstants} from '../exploration-player-page.constants'; +import {AudioTranslationManagerService} from '../services/audio-translation-manager.service'; +import {PlayerPositionService} from '../services/player-position.service'; +import {PlayerTranscriptService} from '../services/player-transcript.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {Interaction} from 'domain/exploration/InteractionObjectFactory'; +import {NumberConversionService} from 'services/number-conversion.service'; import isNumber from 'lodash/isNumber'; import isString from 'lodash/isString'; import './input-response-pair.component.css'; - @Component({ selector: 'oppia-input-response-pair', templateUrl: './input-response-pair.component.html', - styleUrls: ['./input-response-pair.component.css'] + styleUrls: ['./input-response-pair.component.css'], }) export class InputResponsePairComponent { // This property is initialized using component interactions @@ -66,7 +68,7 @@ export class InputResponsePairComponent { private i18nLanguageCodeService: I18nLanguageCodeService, private playerPositionService: PlayerPositionService, private playerTranscriptService: PlayerTranscriptService, - private numberConversionService: NumberConversionService, + private numberConversionService: NumberConversionService ) {} isVideoRteElementPresentInResponse(): boolean { @@ -78,7 +80,8 @@ export class InputResponsePairComponent { isCurrentCardAtEndOfTranscript(): boolean { return this.playerTranscriptService.isLastCard( - this.playerPositionService.getDisplayedCardIndex()); + this.playerPositionService.getDisplayedCardIndex() + ); } isStringifiedNumber(value: string): boolean { @@ -100,36 +103,39 @@ export class InputResponsePairComponent { getAnswerHtml(): string { let displayedCard = this.playerTranscriptService.getCard( - this.playerPositionService.getDisplayedCardIndex()); + this.playerPositionService.getDisplayedCardIndex() + ); let interaction = displayedCard.getInteraction(); return this.explorationHtmlFormatterService.getAnswerHtml( this.convertAnswerToLocalFormat(this.data.learnerInput as string), interaction.id, - interaction.customizationArgs); + interaction.customizationArgs + ); } // Returns a HTML string representing a short summary of the answer // , or null if the answer does not have to be summarized. getShortAnswerHtml(): string { let displayedCard = this.playerTranscriptService.getCard( - this.playerPositionService.getDisplayedCardIndex()); + this.playerPositionService.getDisplayedCardIndex() + ); let interaction: Interaction = displayedCard.getInteraction(); let shortAnswerHtml = ''; if (this.data.learnerInput.hasOwnProperty('answerDetails')) { - shortAnswerHtml = ( - this.data.learnerInput as { answerDetails: string } - ).answerDetails; + shortAnswerHtml = (this.data.learnerInput as {answerDetails: string}) + .answerDetails; } else if ( - this.data && interaction.id && + this.data && + interaction.id && InteractionSpecsConstants.INTERACTION_SPECS[ interaction.id as InteractionSpecsKey ].needs_summary ) { - shortAnswerHtml = ( - this.explorationHtmlFormatterService.getShortAnswerHtml( - this.convertAnswerToLocalFormat( - this.data.learnerInput as string), interaction.id, - interaction.customizationArgs)); + shortAnswerHtml = this.explorationHtmlFormatterService.getShortAnswerHtml( + this.convertAnswerToLocalFormat(this.data.learnerInput as string), + interaction.id, + interaction.customizationArgs + ); } return shortAnswerHtml; } @@ -138,11 +144,12 @@ export class InputResponsePairComponent { if (!this.isLastPair) { return ''; } - if (this.audioTranslationManagerService - .getCurrentComponentName() === - AppConstants.COMPONENT_NAME_FEEDBACK && + if ( + this.audioTranslationManagerService.getCurrentComponentName() === + AppConstants.COMPONENT_NAME_FEEDBACK && (this.audioPlayerService.isPlaying() || - this.autogeneratedAudioPlayerService.isPlaying())) { + this.autogeneratedAudioPlayerService.isPlaying()) + ) { return ExplorationPlayerConstants.AUDIO_HIGHLIGHT_CSS_CLASS; } else { return ''; @@ -154,7 +161,9 @@ export class InputResponsePairComponent { } } -angular.module('oppia').directive('oppiaInputResponsePair', +angular.module('oppia').directive( + 'oppiaInputResponsePair', downgradeComponent({ - component: InputResponsePairComponent - }) as angular.IDirectiveFactory); + component: InputResponsePairComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-player-page/learner-experience/learner-answer-info-card.component.spec.ts b/core/templates/pages/exploration-player-page/learner-experience/learner-answer-info-card.component.spec.ts index 53965f4041e9..fa8c1056ea17 100644 --- a/core/templates/pages/exploration-player-page/learner-experience/learner-answer-info-card.component.spec.ts +++ b/core/templates/pages/exploration-player-page/learner-experience/learner-answer-info-card.component.spec.ts @@ -16,18 +16,18 @@ * @fileoverview Unit tests for LearnerAnswerInfoCard */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; -import { MockTranslateService } from 'components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec'; -import { BackgroundMaskService } from 'services/stateful/background-mask.service'; -import { LearnerAnswerInfoCard } from './learner-answer-info-card.component'; -import { ExplorationEngineService } from '../services/exploration-engine.service'; -import { StateObjectFactory } from 'domain/state/StateObjectFactory'; -import { ExplorationHtmlFormatterService } from 'services/exploration-html-formatter.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { PlayerTranscriptService } from '../services/player-transcript.service'; -import { LearnerAnswerInfoService } from '../services/learner-answer-info.service'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; +import {MockTranslateService} from 'components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec'; +import {BackgroundMaskService} from 'services/stateful/background-mask.service'; +import {LearnerAnswerInfoCard} from './learner-answer-info-card.component'; +import {ExplorationEngineService} from '../services/exploration-engine.service'; +import {StateObjectFactory} from 'domain/state/StateObjectFactory'; +import {ExplorationHtmlFormatterService} from 'services/exploration-html-formatter.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {PlayerTranscriptService} from '../services/player-transcript.service'; +import {LearnerAnswerInfoService} from '../services/learner-answer-info.service'; describe('LearnerAnswerInfoCard', () => { let component: LearnerAnswerInfoCard; @@ -41,17 +41,15 @@ describe('LearnerAnswerInfoCard', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - LearnerAnswerInfoCard - ], + declarations: [LearnerAnswerInfoCard], providers: [ BackgroundMaskService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -60,56 +58,56 @@ describe('LearnerAnswerInfoCard', () => { explorationHtmlFormatter = TestBed.get(ExplorationHtmlFormatterService); learnerAnswerInfoService = TestBed.get(LearnerAnswerInfoService); playerTranscriptService = TestBed.get(PlayerTranscriptService); - explorationEngineService = TestBed.get( - ExplorationEngineService); - spyOn(explorationEngineService, 'getState') - .and.returnValue(stateObjectFactory.createFromBackendDict( - 'stateName', { - classifier_model_id: null, - content: { - html: '', - content_id: 'content' - }, - interaction: { - id: 'FractionInput', - customization_args: { - requireSimplestForm: { value: false }, - allowImproperFraction: { value: true }, - allowNonzeroIntegerPart: { value: true }, - customPlaceholder: { value: { + explorationEngineService = TestBed.get(ExplorationEngineService); + spyOn(explorationEngineService, 'getState').and.returnValue( + stateObjectFactory.createFromBackendDict('stateName', { + classifier_model_id: null, + content: { + html: '', + content_id: 'content', + }, + interaction: { + id: 'FractionInput', + customization_args: { + requireSimplestForm: {value: false}, + allowImproperFraction: {value: true}, + allowNonzeroIntegerPart: {value: true}, + customPlaceholder: { + value: { content_id: '', - unicode_str: '' - } }, - }, - answer_groups: [], - default_outcome: { - dest: 'Introduction', - dest_if_really_stuck: null, - feedback: { - content_id: 'default_outcome', - html: '' + unicode_str: '', }, - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null }, - confirmed_unclassified_answers: [], - hints: [], - solution: null }, - linked_skill_id: null, - param_changes: [], - recorded_voiceovers: { - voiceovers_mapping: { - content: {}, - default_outcome: {} - } + answer_groups: [], + default_outcome: { + dest: 'Introduction', + dest_if_really_stuck: null, + feedback: { + content_id: 'default_outcome', + html: '', + }, + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + }, + confirmed_unclassified_answers: [], + hints: [], + solution: null, + }, + linked_skill_id: null, + param_changes: [], + recorded_voiceovers: { + voiceovers_mapping: { + content: {}, + default_outcome: {}, }, - solicit_answer_details: false, - card_is_checkpoint: false - } - )); + }, + solicit_answer_details: false, + card_is_checkpoint: false, + }) + ); fixture = TestBed.createComponent(LearnerAnswerInfoCard); component = fixture.componentInstance; diff --git a/core/templates/pages/exploration-player-page/learner-experience/learner-answer-info-card.component.ts b/core/templates/pages/exploration-player-page/learner-experience/learner-answer-info-card.component.ts index 04a7580b714e..14c270be91a1 100644 --- a/core/templates/pages/exploration-player-page/learner-experience/learner-answer-info-card.component.ts +++ b/core/templates/pages/exploration-player-page/learner-experience/learner-answer-info-card.component.ts @@ -16,14 +16,14 @@ * @fileoverview Component for asking learner for answer details. */ -import { Component, EventEmitter, Output } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { Interaction } from 'domain/exploration/InteractionObjectFactory'; -import { ExplorationHtmlFormatterService } from 'services/exploration-html-formatter.service'; -import { InteractionRulesService } from '../services/answer-classification.service'; -import { ExplorationEngineService } from '../services/exploration-engine.service'; -import { LearnerAnswerInfoService } from '../services/learner-answer-info.service'; -import { PlayerTranscriptService } from '../services/player-transcript.service'; +import {Component, EventEmitter, Output} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {Interaction} from 'domain/exploration/InteractionObjectFactory'; +import {ExplorationHtmlFormatterService} from 'services/exploration-html-formatter.service'; +import {InteractionRulesService} from '../services/answer-classification.service'; +import {ExplorationEngineService} from '../services/exploration-engine.service'; +import {LearnerAnswerInfoService} from '../services/learner-answer-info.service'; +import {PlayerTranscriptService} from '../services/player-transcript.service'; interface SubmitAnswerEventDataInterface { currentAnswer: string; @@ -32,11 +32,11 @@ interface SubmitAnswerEventDataInterface { @Component({ selector: 'oppia-learner-answer-info-card', - templateUrl: './learner-answer-info-card.component.html' + templateUrl: './learner-answer-info-card.component.html', }) export class LearnerAnswerInfoCard { - @Output() submitAnswer: EventEmitter = ( - new EventEmitter()); + @Output() submitAnswer: EventEmitter = + new EventEmitter(); // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see @@ -54,23 +54,30 @@ export class LearnerAnswerInfoCard { } submitLearnerAnswerInfo(): void { - this.learnerAnswerInfoService.recordLearnerAnswerInfo( - this.answerDetails); + this.learnerAnswerInfoService.recordLearnerAnswerInfo(this.answerDetails); this.playerTranscriptService.addNewInput(this.answerDetails, false); this.playerTranscriptService.addNewResponse( - this.learnerAnswerInfoService.getSolicitAnswerDetailsFeedback()); + this.learnerAnswerInfoService.getSolicitAnswerDetailsFeedback() + ); this.submitAnswer.emit({ currentAnswer: this.learnerAnswerInfoService.getCurrentAnswer(), rulesService: - this.learnerAnswerInfoService.getCurrentInteractionRulesService()}); + this.learnerAnswerInfoService.getCurrentInteractionRulesService(), + }); } displayCurrentAnswer(): string { return this.explorationHtmlFormatter.getAnswerHtml( - this.learnerAnswerInfoService.getCurrentAnswer(), this.interaction.id, - this.interaction.customizationArgs); + this.learnerAnswerInfoService.getCurrentAnswer(), + this.interaction.id, + this.interaction.customizationArgs + ); } } -angular.module('oppia').directive('oppiaLearnerAnswerInfoCard', - downgradeComponent({ component: LearnerAnswerInfoCard })); +angular + .module('oppia') + .directive( + 'oppiaLearnerAnswerInfoCard', + downgradeComponent({component: LearnerAnswerInfoCard}) + ); diff --git a/core/templates/pages/exploration-player-page/learner-experience/ratings-and-recommendations.component.spec.ts b/core/templates/pages/exploration-player-page/learner-experience/ratings-and-recommendations.component.spec.ts index e9ff55d81555..b856087b7037 100644 --- a/core/templates/pages/exploration-player-page/learner-experience/ratings-and-recommendations.component.spec.ts +++ b/core/templates/pages/exploration-player-page/learner-experience/ratings-and-recommendations.component.spec.ts @@ -16,39 +16,45 @@ * @fileoverview Unit tests for ratings and recommendations component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'; -import { TranslateService } from '@ngx-translate/core'; -import { MockTranslateService } from 'components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec'; -import { AlertsService } from 'services/alerts.service'; -import { UrlService } from 'services/contextual/url.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { UserService } from 'services/user.service'; -import { LearnerViewRatingService } from '../services/learner-view-rating.service'; -import { MockLimitToPipe } from 'pages/exploration-player-page/templates/lesson-information-card-modal.component.spec'; -import { RatingsAndRecommendationsComponent } from './ratings-and-recommendations.component'; -import { ExplorationPlayerStateService } from './../services/exploration-player-state.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { PlatformFeatureService } from 'services/platform-feature.service'; -import { LocalStorageService } from 'services/local-storage.service'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; -import { StoryViewerBackendApiService } from 'domain/story_viewer/story-viewer-backend-api.service'; -import { TopicViewerBackendApiService } from 'domain/topic_viewer/topic-viewer-backend-api.service'; -import { StoryPlaythrough } from 'domain/story_viewer/story-playthrough.model'; -import { ReadOnlyStoryNode } from 'domain/story_viewer/read-only-story-node.model'; -import { ReadOnlyTopic } from 'domain/topic_viewer/read-only-topic-object.factory'; -import { LearnerExplorationSummary } from 'domain/summary/learner-exploration-summary.model'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { FeatureStatusChecker } from 'domain/feature-flag/feature-status-summary.model'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {NgbPopoverModule} from '@ng-bootstrap/ng-bootstrap'; +import {TranslateService} from '@ngx-translate/core'; +import {MockTranslateService} from 'components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec'; +import {AlertsService} from 'services/alerts.service'; +import {UrlService} from 'services/contextual/url.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {UserService} from 'services/user.service'; +import {LearnerViewRatingService} from '../services/learner-view-rating.service'; +import {MockLimitToPipe} from 'pages/exploration-player-page/templates/lesson-information-card-modal.component.spec'; +import {RatingsAndRecommendationsComponent} from './ratings-and-recommendations.component'; +import {ExplorationPlayerStateService} from './../services/exploration-player-state.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {PlatformFeatureService} from 'services/platform-feature.service'; +import {LocalStorageService} from 'services/local-storage.service'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; +import {StoryViewerBackendApiService} from 'domain/story_viewer/story-viewer-backend-api.service'; +import {TopicViewerBackendApiService} from 'domain/topic_viewer/topic-viewer-backend-api.service'; +import {StoryPlaythrough} from 'domain/story_viewer/story-playthrough.model'; +import {ReadOnlyStoryNode} from 'domain/story_viewer/read-only-story-node.model'; +import {ReadOnlyTopic} from 'domain/topic_viewer/read-only-topic-object.factory'; +import {LearnerExplorationSummary} from 'domain/summary/learner-exploration-summary.model'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {FeatureStatusChecker} from 'domain/feature-flag/feature-status-summary.model'; class MockPlatformFeatureService { get status(): object { return { EndChapterCelebration: { - isEnabled: true - } + isEnabled: true, + }, }; } } @@ -69,30 +75,26 @@ describe('Ratings and recommendations component', () => { let topicViewerBackendApiService: TopicViewerBackendApiService; let siteAnalyticsService: SiteAnalyticsService; - const mockNgbPopover = jasmine.createSpyObj( - 'NgbPopover', ['close', 'toggle', 'open']); - + const mockNgbPopover = jasmine.createSpyObj('NgbPopover', [ + 'close', + 'toggle', + 'open', + ]); class MockWindowRef { nativeWindow = { location: { search: '', pathname: '/path/name', - reload: () => {} - } + reload: () => {}, + }, }; } beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - NgbPopoverModule - ], - declarations: [ - RatingsAndRecommendationsComponent, - MockLimitToPipe - ], + imports: [HttpClientTestingModule, NgbPopoverModule], + declarations: [RatingsAndRecommendationsComponent, MockLimitToPipe], providers: [ AlertsService, LearnerViewRatingService, @@ -106,18 +108,18 @@ describe('Ratings and recommendations component', () => { LocalStorageService, { provide: PlatformFeatureService, - useClass: MockPlatformFeatureService + useClass: MockPlatformFeatureService, }, { provide: WindowRef, - useClass: MockWindowRef + useClass: MockWindowRef, }, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -129,121 +131,198 @@ describe('Ratings and recommendations component', () => { urlService = TestBed.inject(UrlService); userService = TestBed.inject(UserService); explorationPlayerStateService = TestBed.inject( - ExplorationPlayerStateService); + ExplorationPlayerStateService + ); urlInterpolationService = TestBed.inject(UrlInterpolationService); platformFeatureService = TestBed.inject(PlatformFeatureService); localStorageService = TestBed.inject(LocalStorageService); assetsBackendApiService = TestBed.inject(AssetsBackendApiService); - storyViewerBackendApiService = TestBed.inject( - StoryViewerBackendApiService); - topicViewerBackendApiService = TestBed.inject( - TopicViewerBackendApiService); + storyViewerBackendApiService = TestBed.inject(StoryViewerBackendApiService); + topicViewerBackendApiService = TestBed.inject(TopicViewerBackendApiService); siteAnalyticsService = TestBed.inject(SiteAnalyticsService); }); - it('should populate internal properties and subscribe to event' + - ' listeners on initialize', fakeAsync(() => { - const collectionId = 'collection_id'; - const userRating = 5; - const mockOnRatingUpdated = new EventEmitter(); - const readOnlyStoryNode1 = new ReadOnlyStoryNode( - 'node_1', '', '', [], [], [], '', false, '', - {} as LearnerExplorationSummary, false, 'bg_color_1', 'filename_1'); - const readOnlyStoryNode2 = new ReadOnlyStoryNode( - 'node_2', '', '', [], [], [], '', false, '', - {} as LearnerExplorationSummary, false, 'bg_color_2', 'filename_2'); - - expect(componentInstance.inStoryMode).toBe(undefined); - expect(componentInstance.storyViewerUrl).toBe(undefined); - expect(componentInstance.practiceQuestionsAreEnabled).toBe(false); - - spyOn(urlService, 'getCollectionIdFromExplorationUrl').and.returnValue( - collectionId); - spyOn(learnerViewRatingService, 'getUserRating').and.returnValue( - userRating); - spyOn(alertsService, 'addSuccessMessage'); - spyOn(learnerViewRatingService, 'init').and.callFake( - (callb: (rating: number) => void) => { - callb(userRating); + it( + 'should populate internal properties and subscribe to event' + + ' listeners on initialize', + fakeAsync(() => { + const collectionId = 'collection_id'; + const userRating = 5; + const mockOnRatingUpdated = new EventEmitter(); + const readOnlyStoryNode1 = new ReadOnlyStoryNode( + 'node_1', + '', + '', + [], + [], + [], + '', + false, + '', + {} as LearnerExplorationSummary, + false, + 'bg_color_1', + 'filename_1' + ); + const readOnlyStoryNode2 = new ReadOnlyStoryNode( + 'node_2', + '', + '', + [], + [], + [], + '', + false, + '', + {} as LearnerExplorationSummary, + false, + 'bg_color_2', + 'filename_2' + ); + + expect(componentInstance.inStoryMode).toBe(undefined); + expect(componentInstance.storyViewerUrl).toBe(undefined); + expect(componentInstance.practiceQuestionsAreEnabled).toBe(false); + + spyOn(urlService, 'getCollectionIdFromExplorationUrl').and.returnValue( + collectionId + ); + spyOn(learnerViewRatingService, 'getUserRating').and.returnValue( + userRating + ); + spyOn(alertsService, 'addSuccessMessage'); + spyOn(learnerViewRatingService, 'init').and.callFake( + (callb: (rating: number) => void) => { + callb(userRating); + } + ); + spyOn( + explorationPlayerStateService, + 'isInStoryChapterMode' + ).and.returnValue(true); + spyOn(urlInterpolationService, 'interpolateUrl').and.returnValue( + 'dummy_story_viewer_page_url' + ); + spyOnProperty( + learnerViewRatingService, + 'onRatingUpdated' + ).and.returnValue(mockOnRatingUpdated); + spyOn(componentInstance, 'getIconUrl').and.returnValue('thumbnail_url'); + spyOn(urlService, 'getUrlParams').and.returnValue({ + story_url_fragment: 'story_url_fragment', + topic_url_fragment: 'topic_url_fragment', + classroom_url_fragment: 'classroom_url_fragment', + node_id: 'node_1', }); - spyOn(explorationPlayerStateService, 'isInStoryChapterMode') - .and.returnValue(true); - spyOn(urlInterpolationService, 'interpolateUrl').and.returnValue( - 'dummy_story_viewer_page_url'); - spyOnProperty(learnerViewRatingService, 'onRatingUpdated').and.returnValue( - mockOnRatingUpdated); - spyOn(componentInstance, 'getIconUrl').and.returnValue('thumbnail_url'); - spyOn(urlService, 'getUrlParams').and.returnValue({ - story_url_fragment: 'story_url_fragment', - topic_url_fragment: 'topic_url_fragment', - classroom_url_fragment: 'classroom_url_fragment', - node_id: 'node_1' - }); - spyOn(storyViewerBackendApiService, 'fetchStoryDataAsync').and.returnValue( - Promise.resolve(new StoryPlaythrough( - 'story_id', [readOnlyStoryNode1, readOnlyStoryNode2], '', '', '', ''))); - spyOn(topicViewerBackendApiService, 'fetchTopicDataAsync').and.returnValue( - Promise.resolve(new ReadOnlyTopic( - 'topic_name', 'topic_Id', 'description', - [], [], [], [], {}, {}, true, 'metatag', 'page_title_fragment'))); - - // This throws "Type 'null' is not assignable to parameter of type - // 'QuestionPlayerConfig'." We need to suppress this error because - // of the need to test validations. This throws an error because - // the value is null. - // @ts-ignore - componentInstance.questionPlayerConfig = null; - - componentInstance.ngOnInit(); - mockOnRatingUpdated.emit(); - tick(1000); - - expect(explorationPlayerStateService.isInStoryChapterMode) - .toHaveBeenCalled(); - expect(componentInstance.inStoryMode).toBe(true); - expect(componentInstance.storyId).toBe('story_id'); - expect(componentInstance.nextStoryNode).toBe(readOnlyStoryNode2); - expect(componentInstance.getIconUrl).toHaveBeenCalledWith( - 'story_id', 'filename_2'); - expect(componentInstance.nextStoryNodeIconUrl).toBe('thumbnail_url'); - expect(urlInterpolationService.interpolateUrl).toHaveBeenCalled(); - expect(componentInstance.storyViewerUrl).toBe( - 'dummy_story_viewer_page_url'); - expect(componentInstance.practiceQuestionsAreEnabled).toBe(true); - expect(componentInstance.userRating).toEqual(userRating); - expect(alertsService.addSuccessMessage).toHaveBeenCalled(); - expect(learnerViewRatingService.getUserRating).toHaveBeenCalled(); - expect(componentInstance.collectionId).toEqual(collectionId); - })); - - it('should not generate story page url and determine the next story node' + - 'if not in story mode', fakeAsync(() => { - expect(componentInstance.inStoryMode).toBe(undefined); - expect(componentInstance.storyViewerUrl).toBe(undefined); - - spyOn(explorationPlayerStateService, 'isInStoryChapterMode') - .and.returnValue(false); - spyOn(urlInterpolationService, 'interpolateUrl').and.returnValue( - 'dummy_story_viewer_page_url'); - - componentInstance.ngOnInit(); - tick(); + spyOn( + storyViewerBackendApiService, + 'fetchStoryDataAsync' + ).and.returnValue( + Promise.resolve( + new StoryPlaythrough( + 'story_id', + [readOnlyStoryNode1, readOnlyStoryNode2], + '', + '', + '', + '' + ) + ) + ); + spyOn( + topicViewerBackendApiService, + 'fetchTopicDataAsync' + ).and.returnValue( + Promise.resolve( + new ReadOnlyTopic( + 'topic_name', + 'topic_Id', + 'description', + [], + [], + [], + [], + {}, + {}, + true, + 'metatag', + 'page_title_fragment' + ) + ) + ); + + // This throws "Type 'null' is not assignable to parameter of type + // 'QuestionPlayerConfig'." We need to suppress this error because + // of the need to test validations. This throws an error because + // the value is null. + // @ts-ignore + componentInstance.questionPlayerConfig = null; + + componentInstance.ngOnInit(); + mockOnRatingUpdated.emit(); + tick(1000); + + expect( + explorationPlayerStateService.isInStoryChapterMode + ).toHaveBeenCalled(); + expect(componentInstance.inStoryMode).toBe(true); + expect(componentInstance.storyId).toBe('story_id'); + expect(componentInstance.nextStoryNode).toBe(readOnlyStoryNode2); + expect(componentInstance.getIconUrl).toHaveBeenCalledWith( + 'story_id', + 'filename_2' + ); + expect(componentInstance.nextStoryNodeIconUrl).toBe('thumbnail_url'); + expect(urlInterpolationService.interpolateUrl).toHaveBeenCalled(); + expect(componentInstance.storyViewerUrl).toBe( + 'dummy_story_viewer_page_url' + ); + expect(componentInstance.practiceQuestionsAreEnabled).toBe(true); + expect(componentInstance.userRating).toEqual(userRating); + expect(alertsService.addSuccessMessage).toHaveBeenCalled(); + expect(learnerViewRatingService.getUserRating).toHaveBeenCalled(); + expect(componentInstance.collectionId).toEqual(collectionId); + }) + ); + + it( + 'should not generate story page url and determine the next story node' + + 'if not in story mode', + fakeAsync(() => { + expect(componentInstance.inStoryMode).toBe(undefined); + expect(componentInstance.storyViewerUrl).toBe(undefined); + + spyOn( + explorationPlayerStateService, + 'isInStoryChapterMode' + ).and.returnValue(false); + spyOn(urlInterpolationService, 'interpolateUrl').and.returnValue( + 'dummy_story_viewer_page_url' + ); + + componentInstance.ngOnInit(); + tick(); - expect(explorationPlayerStateService.isInStoryChapterMode) - .toHaveBeenCalled(); - expect(componentInstance.inStoryMode).toBe(false); - expect(urlInterpolationService.interpolateUrl).not.toHaveBeenCalled(); - expect(componentInstance.storyViewerUrl).toBe(undefined); - })); + expect( + explorationPlayerStateService.isInStoryChapterMode + ).toHaveBeenCalled(); + expect(componentInstance.inStoryMode).toBe(false); + expect(urlInterpolationService.interpolateUrl).not.toHaveBeenCalled(); + expect(componentInstance.storyViewerUrl).toBe(undefined); + }) + ); it('should obtain next chapter thumbnail url', () => { - spyOn(assetsBackendApiService, 'getThumbnailUrlForPreview') - .and.returnValue('dummy_thumbnail_url'); + spyOn(assetsBackendApiService, 'getThumbnailUrlForPreview').and.returnValue( + 'dummy_thumbnail_url' + ); - expect(componentInstance.getIconUrl('story_id', 'thumbnail_filename')) - .toBe('dummy_thumbnail_url'); - expect(assetsBackendApiService.getThumbnailUrlForPreview) - .toHaveBeenCalledWith('story', 'story_id', 'thumbnail_filename'); + expect(componentInstance.getIconUrl('story_id', 'thumbnail_filename')).toBe( + 'dummy_thumbnail_url' + ); + expect( + assetsBackendApiService.getThumbnailUrlForPreview + ).toHaveBeenCalledWith('story', 'story_id', 'thumbnail_filename'); }); it('should toggle popover when user clicks on rating stars', () => { @@ -278,27 +357,15 @@ describe('Ratings and recommendations component', () => { componentInstance.submitUserRating(userRating); expect(learnerViewRatingService.submitUserRating).toHaveBeenCalledWith( - userRating); + userRating + ); }); - it('should redirect to sign in page when user clicks on signin button', - fakeAsync(() => { - spyOn(siteAnalyticsService, 'registerNewSignupEvent'); - spyOn(userService, 'getLoginUrlAsync').and.returnValue( - Promise.resolve('login_url')); - - componentInstance.signIn('.sign-in-button'); - tick(); - - expect(userService.getLoginUrlAsync).toHaveBeenCalled(); - expect(siteAnalyticsService.registerNewSignupEvent).toHaveBeenCalled(); - })); - - it('should reload the page if user clicks on signin button and ' + - 'login url is not available', fakeAsync(() => { + it('should redirect to sign in page when user clicks on signin button', fakeAsync(() => { spyOn(siteAnalyticsService, 'registerNewSignupEvent'); spyOn(userService, 'getLoginUrlAsync').and.returnValue( - Promise.resolve('')); + Promise.resolve('login_url') + ); componentInstance.signIn('.sign-in-button'); tick(); @@ -307,23 +374,43 @@ describe('Ratings and recommendations component', () => { expect(siteAnalyticsService.registerNewSignupEvent).toHaveBeenCalled(); })); - it('should save user\'s sign up section preference to localStorage', () => { + it( + 'should reload the page if user clicks on signin button and ' + + 'login url is not available', + fakeAsync(() => { + spyOn(siteAnalyticsService, 'registerNewSignupEvent'); + spyOn(userService, 'getLoginUrlAsync').and.returnValue( + Promise.resolve('') + ); + + componentInstance.signIn('.sign-in-button'); + tick(); + + expect(userService.getLoginUrlAsync).toHaveBeenCalled(); + expect(siteAnalyticsService.registerNewSignupEvent).toHaveBeenCalled(); + }) + ); + + it("should save user's sign up section preference to localStorage", () => { spyOn(localStorageService, 'updateEndChapterSignUpSectionHiddenPreference'); componentInstance.hideSignUpSection(); - expect(localStorageService.updateEndChapterSignUpSectionHiddenPreference) - .toHaveBeenCalledWith('true'); + expect( + localStorageService.updateEndChapterSignUpSectionHiddenPreference + ).toHaveBeenCalledWith('true'); }); - it('should get user\'s sign up section preference from localStorage', () => { - const getPreferenceSpy = ( - spyOn(localStorageService, 'getEndChapterSignUpSectionHiddenPreference') - .and.returnValue('true')); + it("should get user's sign up section preference from localStorage", () => { + const getPreferenceSpy = spyOn( + localStorageService, + 'getEndChapterSignUpSectionHiddenPreference' + ).and.returnValue('true'); expect(componentInstance.isSignUpSectionHidden()).toBe(true); - expect(localStorageService.getEndChapterSignUpSectionHiddenPreference) - .toHaveBeenCalled(); + expect( + localStorageService.getEndChapterSignUpSectionHiddenPreference + ).toHaveBeenCalled(); getPreferenceSpy.and.returnValue(null); @@ -331,18 +418,19 @@ describe('Ratings and recommendations component', () => { }); it('should correctly determine if the feature is enabled or not', () => { - const featureSpy = ( - spyOnProperty(platformFeatureService, 'status', 'get').and.callThrough()); + const featureSpy = spyOnProperty( + platformFeatureService, + 'status', + 'get' + ).and.callThrough(); expect(componentInstance.isEndChapterFeatureEnabled()).toBe(true); - featureSpy.and.returnValue( - { - EndChapterCelebration: { - isEnabled: false - } - } as FeatureStatusChecker - ); + featureSpy.and.returnValue({ + EndChapterCelebration: { + isEnabled: false, + }, + } as FeatureStatusChecker); expect(componentInstance.isEndChapterFeatureEnabled()).toBe(false); }); diff --git a/core/templates/pages/exploration-player-page/learner-experience/ratings-and-recommendations.component.ts b/core/templates/pages/exploration-player-page/learner-experience/ratings-and-recommendations.component.ts index 8152376f1701..a688d48e7244 100644 --- a/core/templates/pages/exploration-player-page/learner-experience/ratings-and-recommendations.component.ts +++ b/core/templates/pages/exploration-player-page/learner-experience/ratings-and-recommendations.component.ts @@ -17,29 +17,29 @@ * on conversation skin. */ -import { Component, Input, ViewChild } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'; -import { CollectionSummary } from 'domain/collection/collection-summary.model'; -import { LearnerExplorationSummary } from 'domain/summary/learner-exploration-summary.model'; -import { Subscription } from 'rxjs'; -import { AlertsService } from 'services/alerts.service'; -import { UrlService } from 'services/contextual/url.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { UserService } from 'services/user.service'; -import { LearnerViewRatingService } from '../services/learner-view-rating.service'; -import { ExplorationPlayerStateService } from './../services/exploration-player-state.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { TopicViewerDomainConstants } from 'domain/topic_viewer/topic-viewer-domain.constants'; -import { PlatformFeatureService } from 'services/platform-feature.service'; -import { LocalStorageService } from 'services/local-storage.service'; -import { StoryViewerBackendApiService } from 'domain/story_viewer/story-viewer-backend-api.service'; -import { TopicViewerBackendApiService } from 'domain/topic_viewer/topic-viewer-backend-api.service'; -import { ReadOnlyTopic } from 'domain/topic_viewer/read-only-topic-object.factory'; -import { ReadOnlyStoryNode } from 'domain/story_viewer/read-only-story-node.model'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; -import { AppConstants } from 'app.constants'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; +import {Component, Input, ViewChild} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbPopover} from '@ng-bootstrap/ng-bootstrap'; +import {CollectionSummary} from 'domain/collection/collection-summary.model'; +import {LearnerExplorationSummary} from 'domain/summary/learner-exploration-summary.model'; +import {Subscription} from 'rxjs'; +import {AlertsService} from 'services/alerts.service'; +import {UrlService} from 'services/contextual/url.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {UserService} from 'services/user.service'; +import {LearnerViewRatingService} from '../services/learner-view-rating.service'; +import {ExplorationPlayerStateService} from './../services/exploration-player-state.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {TopicViewerDomainConstants} from 'domain/topic_viewer/topic-viewer-domain.constants'; +import {PlatformFeatureService} from 'services/platform-feature.service'; +import {LocalStorageService} from 'services/local-storage.service'; +import {StoryViewerBackendApiService} from 'domain/story_viewer/story-viewer-backend-api.service'; +import {TopicViewerBackendApiService} from 'domain/topic_viewer/topic-viewer-backend-api.service'; +import {ReadOnlyTopic} from 'domain/topic_viewer/read-only-topic-object.factory'; +import {ReadOnlyStoryNode} from 'domain/story_viewer/read-only-story-node.model'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; +import {AppConstants} from 'app.constants'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; interface ResultActionButton { type: string; @@ -61,7 +61,7 @@ export interface QuestionPlayerConfig { @Component({ selector: 'oppia-ratings-and-recommendations', - templateUrl: './ratings-and-recommendations.component.html' + templateUrl: './ratings-and-recommendations.component.html', }) export class RatingsAndRecommendationsComponent { // These properties are initialized using Angular lifecycle hooks @@ -107,43 +107,51 @@ export class RatingsAndRecommendationsComponent { ) {} ngOnInit(): void { - this.inStoryMode = ( - this.explorationPlayerStateService.isInStoryChapterMode()); + this.inStoryMode = + this.explorationPlayerStateService.isInStoryChapterMode(); if (this.inStoryMode) { let topicUrlFragment = this.urlService.getUrlParams().topic_url_fragment; let storyUrlFragment = this.urlService.getUrlParams().story_url_fragment; - let classroomUrlFragment = ( - this.urlService.getUrlParams().classroom_url_fragment); + let classroomUrlFragment = + this.urlService.getUrlParams().classroom_url_fragment; let nodeId = this.urlService.getUrlParams().node_id; - this.storyViewerBackendApiService.fetchStoryDataAsync( - topicUrlFragment, classroomUrlFragment, - storyUrlFragment - ).then((storyData) => { - this.storyId = storyData.id; - for (let i = 0; i < storyData.nodes.length; i++) { - if ( - storyData.nodes[i].id === nodeId && (i + 1) < storyData.nodes.length - ) { - this.nextStoryNode = storyData.nodes[i + 1]; - this.nextStoryNodeIconUrl = this.getIconUrl( - this.storyId, this.nextStoryNode.thumbnailFilename); - break; + this.storyViewerBackendApiService + .fetchStoryDataAsync( + topicUrlFragment, + classroomUrlFragment, + storyUrlFragment + ) + .then(storyData => { + this.storyId = storyData.id; + for (let i = 0; i < storyData.nodes.length; i++) { + if ( + storyData.nodes[i].id === nodeId && + i + 1 < storyData.nodes.length + ) { + this.nextStoryNode = storyData.nodes[i + 1]; + this.nextStoryNodeIconUrl = this.getIconUrl( + this.storyId, + this.nextStoryNode.thumbnailFilename + ); + break; + } } - } - }); + }); this.storyViewerUrl = this.urlInterpolationService.interpolateUrl( - TopicViewerDomainConstants.STORY_VIEWER_URL_TEMPLATE, { + TopicViewerDomainConstants.STORY_VIEWER_URL_TEMPLATE, + { topic_url_fragment: topicUrlFragment, classroom_url_fragment: classroomUrlFragment, - story_url_fragment: storyUrlFragment - }); + story_url_fragment: storyUrlFragment, + } + ); - this.topicViewerBackendApiService.fetchTopicDataAsync( - topicUrlFragment, classroomUrlFragment - ).then((topicData: ReadOnlyTopic) => { - this.practiceQuestionsAreEnabled = ( - topicData.getPracticeTabIsDisplayed()); - }); + this.topicViewerBackendApiService + .fetchTopicDataAsync(topicUrlFragment, classroomUrlFragment) + .then((topicData: ReadOnlyTopic) => { + this.practiceQuestionsAreEnabled = + topicData.getPracticeTabIsDisplayed(); + }); } this.collectionId = this.urlService.getCollectionIdFromExplorationUrl(); @@ -155,7 +163,7 @@ export class RatingsAndRecommendationsComponent { ); if (!this.questionPlayerConfig) { - this.learnerViewRatingService.init((userRating) => { + this.learnerViewRatingService.init(userRating => { this.userRating = userRating; }); } @@ -163,7 +171,10 @@ export class RatingsAndRecommendationsComponent { getIconUrl(storyId: string, thumbnailFilename: string): string { return this.assetsBackendApiService.getThumbnailUrlForPreview( - AppConstants.ENTITY_TYPE.STORY, storyId, thumbnailFilename); + AppConstants.ENTITY_TYPE.STORY, + storyId, + thumbnailFilename + ); } togglePopover(): void { @@ -180,7 +191,7 @@ export class RatingsAndRecommendationsComponent { signIn(srcElement: string): void { this.siteAnalyticsService.registerNewSignupEvent(srcElement); - this.userService.getLoginUrlAsync().then((loginUrl) => { + this.userService.getLoginUrlAsync().then(loginUrl => { if (loginUrl) { ( this.windowRef.nativeWindow as {location: string | Location} @@ -192,13 +203,16 @@ export class RatingsAndRecommendationsComponent { } hideSignUpSection(): void { - this.localStorageService - .updateEndChapterSignUpSectionHiddenPreference('true'); + this.localStorageService.updateEndChapterSignUpSectionHiddenPreference( + 'true' + ); } isSignUpSectionHidden(): boolean { - return this.localStorageService - .getEndChapterSignUpSectionHiddenPreference() === 'true'; + return ( + this.localStorageService.getEndChapterSignUpSectionHiddenPreference() === + 'true' + ); } isEndChapterFeatureEnabled(): boolean { @@ -206,7 +220,9 @@ export class RatingsAndRecommendationsComponent { } } -angular.module('oppia').directive('oppiaRatingsAndRecommendations', +angular.module('oppia').directive( + 'oppiaRatingsAndRecommendations', downgradeComponent({ - component: RatingsAndRecommendationsComponent - }) as angular.IDirectiveFactory); + component: RatingsAndRecommendationsComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-player-page/learner-experience/supplemental-card.component.spec.ts b/core/templates/pages/exploration-player-page/learner-experience/supplemental-card.component.spec.ts index 3d0707f84995..a0d1e1c39543 100644 --- a/core/templates/pages/exploration-player-page/learner-experience/supplemental-card.component.spec.ts +++ b/core/templates/pages/exploration-player-page/learner-experience/supplemental-card.component.spec.ts @@ -16,24 +16,38 @@ * @fileoverview Unit tests for supplemental card component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ChangeDetectorRef, EventEmitter, NO_ERRORS_SCHEMA, SimpleChanges } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { AppConstants } from 'app.constants'; -import { StateCard } from 'domain/state_card/state-card.model'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { AudioPlayerService } from 'services/audio-player.service'; -import { AutogeneratedAudioPlayerService } from 'services/autogenerated-audio-player.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { ExplorationPlayerConstants } from '../exploration-player-page.constants'; -import { AudioTranslationManagerService } from '../services/audio-translation-manager.service'; -import { CurrentInteractionService } from '../services/current-interaction.service'; -import { HelpCardEventResponse, PlayerPositionService } from '../services/player-position.service'; -import { SupplementalCardComponent } from './supplemental-card.component'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { Interaction } from 'domain/exploration/InteractionObjectFactory'; -import { RecordedVoiceovers } from 'domain/exploration/recorded-voiceovers.model'; -import { AudioTranslationLanguageService } from '../services/audio-translation-language.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import { + ChangeDetectorRef, + EventEmitter, + NO_ERRORS_SCHEMA, + SimpleChanges, +} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {AppConstants} from 'app.constants'; +import {StateCard} from 'domain/state_card/state-card.model'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {AudioPlayerService} from 'services/audio-player.service'; +import {AutogeneratedAudioPlayerService} from 'services/autogenerated-audio-player.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {ExplorationPlayerConstants} from '../exploration-player-page.constants'; +import {AudioTranslationManagerService} from '../services/audio-translation-manager.service'; +import {CurrentInteractionService} from '../services/current-interaction.service'; +import { + HelpCardEventResponse, + PlayerPositionService, +} from '../services/player-position.service'; +import {SupplementalCardComponent} from './supplemental-card.component'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {Interaction} from 'domain/exploration/InteractionObjectFactory'; +import {RecordedVoiceovers} from 'domain/exploration/recorded-voiceovers.model'; +import {AudioTranslationLanguageService} from '../services/audio-translation-language.service'; describe('Supplemental card component', () => { let fixture: ComponentFixture; @@ -48,8 +62,15 @@ describe('Supplemental card component', () => { let i18nLanguageCodeService: I18nLanguageCodeService; let recordedVoiceovers = new RecordedVoiceovers({}); let mockStateCard = new StateCard( - 'state_name', 'html', 'html', {} as Interaction, [], recordedVoiceovers, - '', {} as AudioTranslationLanguageService); + 'state_name', + 'html', + 'html', + {} as Interaction, + [], + recordedVoiceovers, + '', + {} as AudioTranslationLanguageService + ); class MockChangeDetectorRef { detectChanges(): void {} @@ -57,26 +78,22 @@ describe('Supplemental card component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - SupplementalCardComponent - ], + imports: [HttpClientTestingModule], + declarations: [SupplementalCardComponent], providers: [ AudioPlayerService, AudioTranslationManagerService, AutogeneratedAudioPlayerService, { provide: ChangeDetectorRef, - useClass: MockChangeDetectorRef + useClass: MockChangeDetectorRef, }, CurrentInteractionService, PlayerPositionService, UrlInterpolationService, - WindowRef + WindowRef, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -85,9 +102,11 @@ describe('Supplemental card component', () => { componentInstance = fixture.componentInstance; audioPlayerService = TestBed.inject(AudioPlayerService); audioTranslationManagerService = TestBed.inject( - AudioTranslationManagerService); + AudioTranslationManagerService + ); autogeneratedAudioPlayerService = TestBed.inject( - AutogeneratedAudioPlayerService); + AutogeneratedAudioPlayerService + ); currentInteractionService = TestBed.inject(CurrentInteractionService); playerPositionService = TestBed.inject(PlayerPositionService); urlInterpolationService = TestBed.inject(UrlInterpolationService); @@ -95,7 +114,8 @@ describe('Supplemental card component', () => { i18nLanguageCodeService = TestBed.inject(I18nLanguageCodeService); spyOn(i18nLanguageCodeService, 'isCurrentLanguageRTL').and.returnValue( - true); + true + ); }); afterEach(() => { @@ -108,37 +128,44 @@ describe('Supplemental card component', () => { let hasContinueButton = true; componentInstance.currentDisplayedCard = mockStateCard; spyOn(urlInterpolationService, 'getStaticImageUrl').and.returnValue( - imageUrl); + imageUrl + ); spyOn(currentInteractionService, 'registerPresubmitHook').and.callFake( callb => { callb(); - }); - spyOn(componentInstance.currentDisplayedCard, 'isCompleted') - .and.returnValues(false, true); + } + ); + spyOn( + componentInstance.currentDisplayedCard, + 'isCompleted' + ).and.returnValues(false, true); spyOn(componentInstance, 'clearHelpCard'); let mockOnActiveCardChanged = new EventEmitter(); let mockOnHelpCardAvailable = new EventEmitter(); - spyOnProperty(playerPositionService, 'onActiveCardChanged') - .and.returnValue(mockOnActiveCardChanged); + spyOnProperty(playerPositionService, 'onActiveCardChanged').and.returnValue( + mockOnActiveCardChanged + ); spyOn(componentInstance, 'updateDisplayedCard'); spyOnProperty(playerPositionService, 'onHelpCardAvailable').and.returnValue( - mockOnHelpCardAvailable); + mockOnHelpCardAvailable + ); componentInstance.ngOnInit(); mockOnActiveCardChanged.emit(); mockOnHelpCardAvailable.emit({ helpCardHtml, - hasContinueButton + hasContinueButton, }); tick(); tick(); componentInstance.ngOnInit(); expect(componentInstance.helpCardHtml).toEqual(helpCardHtml); expect(componentInstance.helpCardHasContinueButton).toEqual( - hasContinueButton); + hasContinueButton + ); expect(componentInstance.clearHelpCard).toHaveBeenCalled(); expect(componentInstance.updateDisplayedCard).toHaveBeenCalled(); })); @@ -148,8 +175,9 @@ describe('Supplemental card component', () => { mockStateCard.markAsCompleted(); componentInstance.displayedCard = mockStateCard; spyOn(componentInstance, 'clearHelpCard'); - spyOn(componentInstance.displayedCard, 'getLastAnswer') - .and.returnValue(lastAnswer); + spyOn(componentInstance.displayedCard, 'getLastAnswer').and.returnValue( + lastAnswer + ); componentInstance.updateDisplayedCard(); expect(componentInstance.lastAnswer).toEqual(lastAnswer); }); @@ -163,13 +191,13 @@ describe('Supplemental card component', () => { it('should tell if help card is tall', () => { let height = 400; - spyOn( - fixture.elementRef.nativeElement, 'getElementsByClassName' - ).withArgs('conversation-skin-help-card').and.returnValue([ - { - offsetHeight: 400 - } - ]); + spyOn(fixture.elementRef.nativeElement, 'getElementsByClassName') + .withArgs('conversation-skin-help-card') + .and.returnValue([ + { + offsetHeight: 400, + }, + ]); spyOn(componentInstance, 'updateHelpCardBottomPosition'); componentInstance.maxHelpCardHeightSeen = height - 100; expect(componentInstance.isHelpCardTall()).toBeFalse(); @@ -183,13 +211,13 @@ describe('Supplemental card component', () => { componentInstance.currentDisplayedCard = mockStateCard; componentInstance.helpCard = { nativeElement: { - clientHeight: helpCardHeight - } + clientHeight: helpCardHeight, + }, }; componentInstance.interactionContainer = { nativeElement: { - clientHeight: interactionContainerHeight - } + clientHeight: interactionContainerHeight, + }, }; componentInstance.updateHelpCardBottomPosition(); @@ -198,18 +226,21 @@ describe('Supplemental card component', () => { it('should get feedback audio highlight class', () => { spyOn( - audioTranslationManagerService, 'getCurrentComponentName') - .and.returnValue(AppConstants.COMPONENT_NAME_FEEDBACK); + audioTranslationManagerService, + 'getCurrentComponentName' + ).and.returnValue(AppConstants.COMPONENT_NAME_FEEDBACK); spyOn(audioPlayerService, 'isPlaying').and.returnValue(true); spyOn(autogeneratedAudioPlayerService, 'isPlaying').and.returnValue(true); expect(componentInstance.getFeedbackAudioHighlightClass()).toEqual( - ExplorationPlayerConstants.AUDIO_HIGHLIGHT_CSS_CLASS); + ExplorationPlayerConstants.AUDIO_HIGHLIGHT_CSS_CLASS + ); }); it('should return null if audio player is not playing', () => { spyOn( - audioTranslationManagerService, 'getCurrentComponentName') - .and.returnValue(AppConstants.COMPONENT_NAME_FEEDBACK); + audioTranslationManagerService, + 'getCurrentComponentName' + ).and.returnValue(AppConstants.COMPONENT_NAME_FEEDBACK); spyOn(audioPlayerService, 'isPlaying').and.returnValue(false); spyOn(autogeneratedAudioPlayerService, 'isPlaying').and.returnValue(false); expect(componentInstance.getFeedbackAudioHighlightClass()).toEqual(null); @@ -217,16 +248,22 @@ describe('Supplemental card component', () => { it('should update the displayedCard when changes are detected', () => { let updatedStateCard = new StateCard( - 'state_name', 'new content', 'html', {} as Interaction, [], - recordedVoiceovers, '', - {} as AudioTranslationLanguageService); + 'state_name', + 'new content', + 'html', + {} as Interaction, + [], + recordedVoiceovers, + '', + {} as AudioTranslationLanguageService + ); const changes: SimpleChanges = { displayedCard: { previousValue: mockStateCard, currentValue: updatedStateCard, firstChange: false, - isFirstChange: () => false - } + isFirstChange: () => false, + }, }; componentInstance.displayedCard = mockStateCard; expect(componentInstance.displayedCard.contentHtml).toBe('html'); diff --git a/core/templates/pages/exploration-player-page/learner-experience/supplemental-card.component.ts b/core/templates/pages/exploration-player-page/learner-experience/supplemental-card.component.ts index 831a7cc4da6f..cae53664f7be 100644 --- a/core/templates/pages/exploration-player-page/learner-experience/supplemental-card.component.ts +++ b/core/templates/pages/exploration-player-page/learner-experience/supplemental-card.component.ts @@ -16,28 +16,38 @@ * @fileoverview Component for the supplemental card. */ -import { Component, Output, EventEmitter, Input, OnInit, OnDestroy, ElementRef, ViewChild, ChangeDetectorRef, SimpleChanges } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { AppConstants } from 'app.constants'; -import { StateCard } from 'domain/state_card/state-card.model'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { Subscription } from 'rxjs'; -import { AudioPlayerService } from 'services/audio-player.service'; -import { AutogeneratedAudioPlayerService } from 'services/autogenerated-audio-player.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { ExplorationPlayerConstants } from '../exploration-player-page.constants'; -import { AudioTranslationManagerService } from '../services/audio-translation-manager.service'; -import { CurrentInteractionService } from '../services/current-interaction.service'; -import { PlayerPositionService } from '../services/player-position.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; +import { + Component, + Output, + EventEmitter, + Input, + OnInit, + OnDestroy, + ElementRef, + ViewChild, + ChangeDetectorRef, + SimpleChanges, +} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {AppConstants} from 'app.constants'; +import {StateCard} from 'domain/state_card/state-card.model'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {Subscription} from 'rxjs'; +import {AudioPlayerService} from 'services/audio-player.service'; +import {AutogeneratedAudioPlayerService} from 'services/autogenerated-audio-player.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {ExplorationPlayerConstants} from '../exploration-player-page.constants'; +import {AudioTranslationManagerService} from '../services/audio-translation-manager.service'; +import {CurrentInteractionService} from '../services/current-interaction.service'; +import {PlayerPositionService} from '../services/player-position.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; import './supplemental-card.component.css'; - @Component({ selector: 'oppia-supplemental-card', templateUrl: './supplemental-card.component.html', - styleUrls: ['./supplemental-card.component.css'] + styleUrls: ['./supplemental-card.component.css'], }) export class SupplementalCardComponent implements OnInit, OnDestroy { @Output() clickContinueButton: EventEmitter = new EventEmitter(); @@ -55,11 +65,11 @@ export class SupplementalCardComponent implements OnInit, OnDestroy { OPPIA_AVATAR_IMAGE_URL!: string; directiveSubscriptions = new Subscription(); // Last answer is null if their is no answer. - lastAnswer: { answerDetails: string } | string | null = null; + lastAnswer: {answerDetails: string} | string | null = null; maxHelpCardHeightSeen: number = 0; helpCardHasContinueButton: boolean = false; - CONTINUE_BUTTON_FOCUS_LABEL: string = ( - ExplorationPlayerConstants.CONTINUE_BUTTON_FOCUS_LABEL); + CONTINUE_BUTTON_FOCUS_LABEL: string = + ExplorationPlayerConstants.CONTINUE_BUTTON_FOCUS_LABEL; helpCardBottomPosition: number = 0; @@ -77,9 +87,10 @@ export class SupplementalCardComponent implements OnInit, OnDestroy { ) {} ngOnInit(): void { - this.OPPIA_AVATAR_IMAGE_URL = ( + this.OPPIA_AVATAR_IMAGE_URL = this.urlInterpolationService.getStaticImageUrl( - '/avatar/oppia_avatar_100px.svg')); + '/avatar/oppia_avatar_100px.svg' + ); this.currentInteractionService.registerPresubmitHook(() => { // Do not clear the help card or submit an answer if there is an @@ -92,20 +103,16 @@ export class SupplementalCardComponent implements OnInit, OnDestroy { }); this.directiveSubscriptions.add( - this.playerPositionService.onActiveCardChanged.subscribe( - () => { - this.updateDisplayedCard(); - } - ) + this.playerPositionService.onActiveCardChanged.subscribe(() => { + this.updateDisplayedCard(); + }) ); this.directiveSubscriptions.add( - this.playerPositionService.onHelpCardAvailable.subscribe( - (helpCard) => { - this.helpCardHtml = helpCard.helpCardHtml; - this.helpCardHasContinueButton = helpCard.hasContinueButton; - } - ) + this.playerPositionService.onHelpCardAvailable.subscribe(helpCard => { + this.helpCardHtml = helpCard.helpCardHtml; + this.helpCardHasContinueButton = helpCard.hasContinueButton; + }) ); this.updateDisplayedCard(); } @@ -148,8 +155,10 @@ export class SupplementalCardComponent implements OnInit, OnDestroy { this.maxHelpCardHeightSeen = helpCardHeight; } - if (this.maxHelpCardHeightSeen > - this.windowRef.nativeWindow.innerHeight - 100) { + if ( + this.maxHelpCardHeightSeen > + this.windowRef.nativeWindow.innerHeight - 100 + ) { return true; } @@ -163,8 +172,7 @@ export class SupplementalCardComponent implements OnInit, OnDestroy { helpCardHeight = this.helpCard.nativeElement.clientHeight; } - let containerHeight = ( - this.interactionContainer.nativeElement.clientHeight); + let containerHeight = this.interactionContainer.nativeElement.clientHeight; let bottomPosition = Math.max(containerHeight - helpCardHeight / 2, 0); @@ -176,18 +184,21 @@ export class SupplementalCardComponent implements OnInit, OnDestroy { // This function returns null if audio is not available. getFeedbackAudioHighlightClass(): string | null { - if (this.audioTranslationManagerService - .getCurrentComponentName() === - AppConstants.COMPONENT_NAME_FEEDBACK && + if ( + this.audioTranslationManagerService.getCurrentComponentName() === + AppConstants.COMPONENT_NAME_FEEDBACK && (this.audioPlayerService.isPlaying() || - this.autogeneratedAudioPlayerService.isPlaying())) { + this.autogeneratedAudioPlayerService.isPlaying()) + ) { return ExplorationPlayerConstants.AUDIO_HIGHLIGHT_CSS_CLASS; } return null; } } -angular.module('oppia').directive('oppiaSupplementalCard', +angular.module('oppia').directive( + 'oppiaSupplementalCard', downgradeComponent({ - component: SupplementalCardComponent - }) as angular.IDirectiveFactory); + component: SupplementalCardComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-player-page/learner-experience/tutor-card.component.spec.ts b/core/templates/pages/exploration-player-page/learner-experience/tutor-card.component.spec.ts index 2281b86b1b4d..cfab173ad8ca 100644 --- a/core/templates/pages/exploration-player-page/learner-experience/tutor-card.component.spec.ts +++ b/core/templates/pages/exploration-player-page/learner-experience/tutor-card.component.spec.ts @@ -16,52 +16,59 @@ * @fileoverview Unit tests for the Tutor Card Component. */ -import { SimpleChanges } from '@angular/core'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync, flush } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; -import { AppConstants } from 'app.constants'; -import { Interaction } from 'domain/exploration/InteractionObjectFactory'; -import { StateCard } from 'domain/state_card/state-card.model'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { AudioBarStatusService } from 'services/audio-bar-status.service'; -import { AudioPlayerService } from 'services/audio-player.service'; -import { AutogeneratedAudioPlayerService } from 'services/autogenerated-audio-player.service'; -import { ContextService } from 'services/context.service'; -import { DeviceInfoService } from 'services/contextual/device-info.service'; -import { UrlService } from 'services/contextual/url.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { UserService } from 'services/user.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { ExplorationPlayerConstants } from '../exploration-player-page.constants'; -import { AudioPreloaderService } from '../services/audio-preloader.service'; -import { AudioTranslationManagerService } from '../services/audio-translation-manager.service'; -import { CurrentInteractionService } from '../services/current-interaction.service'; -import { ExplorationPlayerStateService } from '../services/exploration-player-state.service'; -import { LearnerAnswerInfoService } from '../services/learner-answer-info.service'; -import { PlayerPositionService } from '../services/player-position.service'; -import { TutorCardComponent } from './tutor-card.component'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { EndChapterCheckMarkComponent } from './end-chapter-check-mark.component'; -import { EndChapterConfettiComponent } from './end-chapter-confetti.component'; -import { PlatformFeatureService } from 'services/platform-feature.service'; -import { InteractionCustomizationArgs } from 'interactions/customization-args-defs'; -import { UserInfo } from 'domain/user/user-info.model'; -import { FeatureStatusChecker } from 'domain/feature-flag/feature-status-summary.model'; +import {SimpleChanges} from '@angular/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, + flush, +} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; +import {AppConstants} from 'app.constants'; +import {Interaction} from 'domain/exploration/InteractionObjectFactory'; +import {StateCard} from 'domain/state_card/state-card.model'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {AudioBarStatusService} from 'services/audio-bar-status.service'; +import {AudioPlayerService} from 'services/audio-player.service'; +import {AutogeneratedAudioPlayerService} from 'services/autogenerated-audio-player.service'; +import {ContextService} from 'services/context.service'; +import {DeviceInfoService} from 'services/contextual/device-info.service'; +import {UrlService} from 'services/contextual/url.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {UserService} from 'services/user.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {ExplorationPlayerConstants} from '../exploration-player-page.constants'; +import {AudioPreloaderService} from '../services/audio-preloader.service'; +import {AudioTranslationManagerService} from '../services/audio-translation-manager.service'; +import {CurrentInteractionService} from '../services/current-interaction.service'; +import {ExplorationPlayerStateService} from '../services/exploration-player-state.service'; +import {LearnerAnswerInfoService} from '../services/learner-answer-info.service'; +import {PlayerPositionService} from '../services/player-position.service'; +import {TutorCardComponent} from './tutor-card.component'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {EndChapterCheckMarkComponent} from './end-chapter-check-mark.component'; +import {EndChapterConfettiComponent} from './end-chapter-confetti.component'; +import {PlatformFeatureService} from 'services/platform-feature.service'; +import {InteractionCustomizationArgs} from 'interactions/customization-args-defs'; +import {UserInfo} from 'domain/user/user-info.model'; +import {FeatureStatusChecker} from 'domain/feature-flag/feature-status-summary.model'; class MockWindowRef { nativeWindow = { location: { hash: '', - pathname: '/path/name' + pathname: '/path/name', }, - matchMedia: function(query: string) { + matchMedia: function (query: string) { return { - matches: false + matches: false, }; - } + }, }; } @@ -69,8 +76,8 @@ class MockPlatformFeatureService { get status(): object { return { EndChapterCelebration: { - isEnabled: true - } + isEnabled: true, + }, }; } } @@ -106,24 +113,38 @@ describe('Tutor card component', () => { let translateService: TranslateService; let mockDisplayedCard = new StateCard( - '', '', '', new Interaction( - [], [], {} as InteractionCustomizationArgs, null, [], 'EndExploration', + '', + '', + '', + new Interaction( + [], + [], + {} as InteractionCustomizationArgs, + null, + [], + 'EndExploration', // This throws "Argument of type 'null' is not assignable to parameter of // type 'RecordedVoiceovers'." We need to suppress this error because of // the need to test validations. This throws an error only in the // frontend tests and not in the frontend. // @ts-ignore - null), [], null, '', null); + null + ), + [], + // This throws "Argument of type 'null' is not assignable to parameter of + // type 'RecordedVoiceovers'." We need to suppress this error because of + // the need to test validations. This throws an error only in the + // frontend tests and not in the frontend. + // @ts-ignore + null, + '', + null + ); beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - TutorCardComponent, - MockTranslatePipe - ], + imports: [HttpClientTestingModule], + declarations: [TutorCardComponent, MockTranslatePipe], providers: [ AudioBarStatusService, AudioPlayerService, @@ -142,18 +163,18 @@ describe('Tutor card component', () => { WindowDimensionsService, { provide: WindowRef, - useClass: MockWindowRef + useClass: MockWindowRef, }, { provide: PlatformFeatureService, - useClass: MockPlatformFeatureService + useClass: MockPlatformFeatureService, }, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -164,14 +185,17 @@ describe('Tutor card component', () => { audioPlayerService = TestBed.inject(AudioPlayerService); audioPreloaderService = TestBed.inject(AudioPreloaderService); audioTranslationManagerService = TestBed.inject( - AudioTranslationManagerService); + AudioTranslationManagerService + ); autogeneratedAudioPlayerService = TestBed.inject( - AutogeneratedAudioPlayerService); + AutogeneratedAudioPlayerService + ); contextService = TestBed.inject(ContextService); currentInteractionService = TestBed.inject(CurrentInteractionService); deviceInfoService = TestBed.inject(DeviceInfoService); explorationPlayerStateService = TestBed.inject( - ExplorationPlayerStateService); + ExplorationPlayerStateService + ); playerPositionService = TestBed.inject(PlayerPositionService); urlInterpolationService = TestBed.inject(UrlInterpolationService); urlService = TestBed.inject(UrlService); @@ -183,9 +207,12 @@ describe('Tutor card component', () => { translateService = TestBed.inject(TranslateService); spyOn(i18nLanguageCodeService, 'isCurrentLanguageRTL').and.returnValue( - true); - spyOn(userService, 'getProfileImageDataUrl').and.returnValue( - ['default-image-url-png', 'default-image-url-webp']); + true + ); + spyOn(userService, 'getProfileImageDataUrl').and.returnValue([ + 'default-image-url-png', + 'default-image-url-webp', + ]); }); afterEach(() => { @@ -203,30 +230,40 @@ describe('Tutor card component', () => { preferred_site_language_code: null, username: 'tester', email: 'test@test.com', - user_is_logged_in: true + user_is_logged_in: true, }; const sampleUserInfo = UserInfo.createFromBackendDict( - sampleUserInfoBackendObject); + sampleUserInfoBackendObject + ); let mockOnActiveCardChangedEventEmitter = new EventEmitter(); let mockOnOppiaFeedbackAvailableEventEmitter = new EventEmitter(); let isIframed = false; spyOn(contextService, 'isInExplorationEditorPage').and.returnValues( - true, false); + true, + false + ); spyOn(urlService, 'isIframed').and.returnValue(isIframed); spyOn(deviceInfoService, 'isMobileDevice').and.returnValue(true); spyOn(audioBarStatusService, 'isAudioBarExpanded').and.returnValue(true); - spyOn(urlInterpolationService, 'getStaticImageUrl') - .and.returnValues('avatar_url', 'profile_url', 'default image path'); + spyOn(urlInterpolationService, 'getStaticImageUrl').and.returnValues( + 'avatar_url', + 'profile_url', + 'default image path' + ); spyOn(componentInstance, 'updateDisplayedCard'); spyOnProperty(playerPositionService, 'onActiveCardChanged').and.returnValue( - mockOnActiveCardChangedEventEmitter); - spyOnProperty(explorationPlayerStateService, 'onOppiaFeedbackAvailable') - .and.returnValue(mockOnOppiaFeedbackAvailableEventEmitter); + mockOnActiveCardChangedEventEmitter + ); + spyOnProperty( + explorationPlayerStateService, + 'onOppiaFeedbackAvailable' + ).and.returnValue(mockOnOppiaFeedbackAvailableEventEmitter); spyOn(componentInstance, 'getInputResponsePairId').and.returnValue('hash'); componentInstance.displayedCard = mockDisplayedCard; spyOn(userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(sampleUserInfo)); + Promise.resolve(sampleUserInfo) + ); componentInstance.ngOnInit(); componentInstance.isAudioBarExpandedOnMobileDevice(); @@ -239,9 +276,11 @@ describe('Tutor card component', () => { tick(); tick(); expect(componentInstance.profilePicturePngDataUrl).toEqual( - 'default-image-url-png'); + 'default-image-url-png' + ); expect(componentInstance.profilePictureWebpDataUrl).toEqual( - 'default-image-url-webp'); + 'default-image-url-webp' + ); expect(componentInstance.isIframed).toEqual(isIframed); expect(contextService.isInExplorationEditorPage).toHaveBeenCalled(); @@ -252,31 +291,35 @@ describe('Tutor card component', () => { expect(componentInstance.getInputResponsePairId).toHaveBeenCalled(); })); - it('should set default profile pictures when username is null', - fakeAsync(() => { - spyOn(componentInstance, 'updateDisplayedCard'); - let userInfo = { - isLoggedIn: () => true, - getUsername: () => null - }; + it('should set default profile pictures when username is null', fakeAsync(() => { + spyOn(componentInstance, 'updateDisplayedCard'); + let userInfo = { + isLoggedIn: () => true, + getUsername: () => null, + }; - spyOn(userService, 'getUserInfoAsync') - .and.resolveTo(userInfo as UserInfo); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo as UserInfo); - componentInstance.ngOnInit(); - tick(); + componentInstance.ngOnInit(); + tick(); - expect(componentInstance.profilePicturePngDataUrl).toBe( - urlInterpolationService.getStaticImageUrl( - AppConstants.DEFAULT_PROFILE_IMAGE_PNG_PATH)); - expect(componentInstance.profilePictureWebpDataUrl).toBe( - urlInterpolationService.getStaticImageUrl( - AppConstants.DEFAULT_PROFILE_IMAGE_WEBP_PATH)); - })); + expect(componentInstance.profilePicturePngDataUrl).toBe( + urlInterpolationService.getStaticImageUrl( + AppConstants.DEFAULT_PROFILE_IMAGE_PNG_PATH + ) + ); + expect(componentInstance.profilePictureWebpDataUrl).toBe( + urlInterpolationService.getStaticImageUrl( + AppConstants.DEFAULT_PROFILE_IMAGE_WEBP_PATH + ) + ); + })); it('should refresh displayed card on changes', fakeAsync(() => { let updateDisplayedCardSpy = spyOn( - componentInstance, 'updateDisplayedCard'); + componentInstance, + 'updateDisplayedCard' + ); spyOn(componentInstance, 'isOnTerminalCard'); const changes: SimpleChanges = { displayedCard: { @@ -284,154 +327,168 @@ describe('Tutor card component', () => { currentValue: true, firstChange: false, isFirstChange: () => false, - } + }, }; componentInstance.ngOnChanges(changes); expect(updateDisplayedCardSpy).toHaveBeenCalled(); expect(componentInstance.isOnTerminalCard).toHaveBeenCalled(); })); - it('should trigger celebratory animation if on the last card of a chapter', - fakeAsync(() => { - spyOn(componentInstance, 'updateDisplayedCard'); - spyOn(componentInstance, 'isOnTerminalCard').and.returnValue(true); - spyOn(componentInstance, 'triggerCelebratoryAnimation'); - componentInstance.animationHasPlayedOnce = false; - componentInstance.inStoryMode = true; - const changes: SimpleChanges = { - displayedCard: { - previousValue: false, - currentValue: true, - firstChange: false, - isFirstChange: () => false - } - }; - - componentInstance.ngOnChanges(changes); - - expect(componentInstance.updateDisplayedCard).toHaveBeenCalled(); - expect(componentInstance.triggerCelebratoryAnimation).toHaveBeenCalled(); - })); - - it('should not trigger celebratory animation if the feature is not enabled', - () => { - spyOn(componentInstance, 'updateDisplayedCard'); - spyOn(componentInstance, 'isOnTerminalCard').and.returnValue(true); - spyOnProperty(platformFeatureService, 'status', 'get').and.returnValue( - { - EndChapterCelebration: { - isEnabled: false - } - } as FeatureStatusChecker - ); - spyOn(componentInstance, 'triggerCelebratoryAnimation'); - componentInstance.animationHasPlayedOnce = false; - componentInstance.inStoryMode = true; - const changes: SimpleChanges = { - displayedCard: { - previousValue: false, - currentValue: true, - firstChange: false, - isFirstChange: () => false - } - }; - - componentInstance.ngOnChanges(changes); + it('should trigger celebratory animation if on the last card of a chapter', fakeAsync(() => { + spyOn(componentInstance, 'updateDisplayedCard'); + spyOn(componentInstance, 'isOnTerminalCard').and.returnValue(true); + spyOn(componentInstance, 'triggerCelebratoryAnimation'); + componentInstance.animationHasPlayedOnce = false; + componentInstance.inStoryMode = true; + const changes: SimpleChanges = { + displayedCard: { + previousValue: false, + currentValue: true, + firstChange: false, + isFirstChange: () => false, + }, + }; - expect(componentInstance.updateDisplayedCard).toHaveBeenCalled(); - expect( - componentInstance.triggerCelebratoryAnimation).not.toHaveBeenCalled(); - }); + componentInstance.ngOnChanges(changes); - it('should not trigger celebratory animation if not in story mode', - fakeAsync(() => { - spyOn(componentInstance, 'updateDisplayedCard'); - spyOn(componentInstance, 'isOnTerminalCard').and.returnValue(true); - spyOn(componentInstance, 'triggerCelebratoryAnimation'); - componentInstance.animationHasPlayedOnce = false; - componentInstance.inStoryMode = false; - const changes: SimpleChanges = { - displayedCard: { - previousValue: false, - currentValue: true, - firstChange: false, - isFirstChange: () => false - } - }; + expect(componentInstance.updateDisplayedCard).toHaveBeenCalled(); + expect(componentInstance.triggerCelebratoryAnimation).toHaveBeenCalled(); + })); - componentInstance.ngOnChanges(changes); + it('should not trigger celebratory animation if the feature is not enabled', () => { + spyOn(componentInstance, 'updateDisplayedCard'); + spyOn(componentInstance, 'isOnTerminalCard').and.returnValue(true); + spyOnProperty(platformFeatureService, 'status', 'get').and.returnValue({ + EndChapterCelebration: { + isEnabled: false, + }, + } as FeatureStatusChecker); + spyOn(componentInstance, 'triggerCelebratoryAnimation'); + componentInstance.animationHasPlayedOnce = false; + componentInstance.inStoryMode = true; + const changes: SimpleChanges = { + displayedCard: { + previousValue: false, + currentValue: true, + firstChange: false, + isFirstChange: () => false, + }, + }; - expect(componentInstance.updateDisplayedCard).toHaveBeenCalled(); - expect( - componentInstance.triggerCelebratoryAnimation).not.toHaveBeenCalled(); - })); + componentInstance.ngOnChanges(changes); - it('should animate the check mark and the confetti ' + - 'if animations are enabled', fakeAsync(() => { - expect(componentInstance.checkMarkHidden).toBe(true); - expect(componentInstance.animationHasPlayedOnce).toBe(false); - expect(componentInstance.checkMarkSkipped).toBe(false); + expect(componentInstance.updateDisplayedCard).toHaveBeenCalled(); + expect( + componentInstance.triggerCelebratoryAnimation + ).not.toHaveBeenCalled(); + }); - spyOn(windowRef.nativeWindow, 'matchMedia').and.callThrough(); - componentInstance.checkMarkComponent = - jasmine.createSpyObj( - 'EndChapterCheckMarkComponent', ['animateCheckMark']); - componentInstance.confettiComponent = - jasmine.createSpyObj( - 'EndChapterConfettiComponent', ['animateConfetti']); + it('should not trigger celebratory animation if not in story mode', fakeAsync(() => { + spyOn(componentInstance, 'updateDisplayedCard'); + spyOn(componentInstance, 'isOnTerminalCard').and.returnValue(true); + spyOn(componentInstance, 'triggerCelebratoryAnimation'); + componentInstance.animationHasPlayedOnce = false; + componentInstance.inStoryMode = false; + const changes: SimpleChanges = { + displayedCard: { + previousValue: false, + currentValue: true, + firstChange: false, + isFirstChange: () => false, + }, + }; - componentInstance.triggerCelebratoryAnimation(); + componentInstance.ngOnChanges(changes); - tick(1); - expect(componentInstance.checkMarkComponent.animateCheckMark) - .toHaveBeenCalled(); - expect(componentInstance.animationHasPlayedOnce).toBe(true); + expect(componentInstance.updateDisplayedCard).toHaveBeenCalled(); + expect( + componentInstance.triggerCelebratoryAnimation + ).not.toHaveBeenCalled(); + })); - tick(2000); - expect(componentInstance.confettiComponent.animateConfetti) - .toHaveBeenCalled(); + it( + 'should animate the check mark and the confetti ' + + 'if animations are enabled', + fakeAsync(() => { + expect(componentInstance.checkMarkHidden).toBe(true); + expect(componentInstance.animationHasPlayedOnce).toBe(false); + expect(componentInstance.checkMarkSkipped).toBe(false); - tick(4000); - expect(componentInstance.checkMarkHidden).toBe(true); - })); + spyOn(windowRef.nativeWindow, 'matchMedia').and.callThrough(); + componentInstance.checkMarkComponent = + jasmine.createSpyObj( + 'EndChapterCheckMarkComponent', + ['animateCheckMark'] + ); + componentInstance.confettiComponent = + jasmine.createSpyObj( + 'EndChapterConfettiComponent', + ['animateConfetti'] + ); + + componentInstance.triggerCelebratoryAnimation(); + + tick(1); + expect( + componentInstance.checkMarkComponent.animateCheckMark + ).toHaveBeenCalled(); + expect(componentInstance.animationHasPlayedOnce).toBe(true); - it('should not animate the confetti if animations ' + - 'are not enabled', fakeAsync(() => { - expect(componentInstance.checkMarkHidden).toBe(true); - expect(componentInstance.animationHasPlayedOnce).toBe(false); - expect(componentInstance.checkMarkSkipped).toBe(false); + tick(2000); + expect( + componentInstance.confettiComponent.animateConfetti + ).toHaveBeenCalled(); - spyOn(windowRef.nativeWindow, 'matchMedia').and.returnValue({ - matches: true, - media: 'prefers-reduced-motion', - addListener: () => {}, - removeListener: () => {}, - addEventListener: () => {}, - removeEventListener: () => {}, - onchange: () => {}, - dispatchEvent: (ev: Event) => true - }); - componentInstance.checkMarkComponent = - jasmine.createSpyObj( - 'EndChapterCheckMarkComponent', ['animateCheckMark']); - componentInstance.confettiComponent = - jasmine.createSpyObj( - 'EndChapterConfettiComponent', ['animateConfetti']); + tick(4000); + expect(componentInstance.checkMarkHidden).toBe(true); + }) + ); - componentInstance.triggerCelebratoryAnimation(); + it( + 'should not animate the confetti if animations ' + 'are not enabled', + fakeAsync(() => { + expect(componentInstance.checkMarkHidden).toBe(true); + expect(componentInstance.animationHasPlayedOnce).toBe(false); + expect(componentInstance.checkMarkSkipped).toBe(false); - tick(1); - expect(componentInstance.checkMarkComponent.animateCheckMark) - .toHaveBeenCalled(); - expect(componentInstance.animationHasPlayedOnce).toBe(true); + spyOn(windowRef.nativeWindow, 'matchMedia').and.returnValue({ + matches: true, + media: 'prefers-reduced-motion', + addListener: () => {}, + removeListener: () => {}, + addEventListener: () => {}, + removeEventListener: () => {}, + onchange: () => {}, + dispatchEvent: (ev: Event) => true, + }); + componentInstance.checkMarkComponent = + jasmine.createSpyObj( + 'EndChapterCheckMarkComponent', + ['animateCheckMark'] + ); + componentInstance.confettiComponent = + jasmine.createSpyObj( + 'EndChapterConfettiComponent', + ['animateConfetti'] + ); + + componentInstance.triggerCelebratoryAnimation(); + + tick(1); + expect( + componentInstance.checkMarkComponent.animateCheckMark + ).toHaveBeenCalled(); + expect(componentInstance.animationHasPlayedOnce).toBe(true); - tick(2000); - expect(componentInstance.confettiComponent.animateConfetti) - .not.toHaveBeenCalled(); + tick(2000); + expect( + componentInstance.confettiComponent.animateConfetti + ).not.toHaveBeenCalled(); - tick(500); - expect(componentInstance.checkMarkHidden).toBe(true); - })); + tick(500); + expect(componentInstance.checkMarkHidden).toBe(true); + }) + ); it('should skip animation when a click is made onscreen', fakeAsync(() => { expect(componentInstance.checkMarkSkipped).toBe(false); @@ -439,10 +496,14 @@ describe('Tutor card component', () => { spyOn(window, 'clearTimeout'); componentInstance.checkMarkComponent = jasmine.createSpyObj( - 'EndChapterCheckMarkComponent', ['animateCheckMark']); + 'EndChapterCheckMarkComponent', + ['animateCheckMark'] + ); componentInstance.confettiComponent = jasmine.createSpyObj( - 'EndChapterConfettiComponent', ['animateConfetti']); + 'EndChapterConfettiComponent', + ['animateConfetti'] + ); componentInstance.triggerCelebratoryAnimation(); let fakeClickEvent = new MouseEvent('click'); document.dispatchEvent(fakeClickEvent); @@ -456,13 +517,12 @@ describe('Tutor card component', () => { flush(); })); - it('should not skip animation if it hasn\'t been triggered yet', - fakeAsync(() => { - let fakeClickEvent = new MouseEvent('click'); - document.dispatchEvent(fakeClickEvent); + it("should not skip animation if it hasn't been triggered yet", fakeAsync(() => { + let fakeClickEvent = new MouseEvent('click'); + document.dispatchEvent(fakeClickEvent); - expect(componentInstance.checkMarkSkipped).toBe(false); - })); + expect(componentInstance.checkMarkSkipped).toBe(false); + })); it('should correctly generate the milestone message', () => { componentInstance.inStoryMode = true; @@ -472,34 +532,26 @@ describe('Tutor card component', () => { spyOn(translateService, 'instant').and.callThrough(); expect(componentInstance.generateMilestoneMessage()).toBe( - 'I18N_END_CHAPTER_MILESTONE_MESSAGE_1'); + 'I18N_END_CHAPTER_MILESTONE_MESSAGE_1' + ); expect(translateService.instant).toHaveBeenCalledWith( - 'I18N_END_CHAPTER_MILESTONE_MESSAGE_1'); + 'I18N_END_CHAPTER_MILESTONE_MESSAGE_1' + ); componentInstance.completedChaptersCount = 5; expect(componentInstance.generateMilestoneMessage()).toBe( - 'I18N_END_CHAPTER_MILESTONE_MESSAGE_2'); + 'I18N_END_CHAPTER_MILESTONE_MESSAGE_2' + ); expect(translateService.instant).toHaveBeenCalledWith( - 'I18N_END_CHAPTER_MILESTONE_MESSAGE_2'); + 'I18N_END_CHAPTER_MILESTONE_MESSAGE_2' + ); }); - it('should generate an empty message if the milestone is not to be displayed', - () => { - componentInstance.inStoryMode = true; - componentInstance.completedChaptersCount = 1; - componentInstance.milestoneMessageIsToBeDisplayed = false; - spyOn(componentInstance, 'generateMilestoneMessage').and.callThrough(); - spyOn(translateService, 'instant').and.callThrough(); - - expect(componentInstance.generateMilestoneMessage()).toBe(''); - expect(translateService.instant).not.toHaveBeenCalled(); - }); - - it('should generate an empty message if not in story mode', () => { - componentInstance.inStoryMode = false; - componentInstance.milestoneMessageIsToBeDisplayed = true; + it('should generate an empty message if the milestone is not to be displayed', () => { + componentInstance.inStoryMode = true; componentInstance.completedChaptersCount = 1; + componentInstance.milestoneMessageIsToBeDisplayed = false; spyOn(componentInstance, 'generateMilestoneMessage').and.callThrough(); spyOn(translateService, 'instant').and.callThrough(); @@ -507,11 +559,10 @@ describe('Tutor card component', () => { expect(translateService.instant).not.toHaveBeenCalled(); }); - it('should generate an empty message if completed chapters count ' + - 'is not eligible for a milestone', () => { - componentInstance.inStoryMode = true; + it('should generate an empty message if not in story mode', () => { + componentInstance.inStoryMode = false; componentInstance.milestoneMessageIsToBeDisplayed = true; - componentInstance.completedChaptersCount = 2; + componentInstance.completedChaptersCount = 1; spyOn(componentInstance, 'generateMilestoneMessage').and.callThrough(); spyOn(translateService, 'instant').and.callThrough(); @@ -519,121 +570,173 @@ describe('Tutor card component', () => { expect(translateService.instant).not.toHaveBeenCalled(); }); + it( + 'should generate an empty message if completed chapters count ' + + 'is not eligible for a milestone', + () => { + componentInstance.inStoryMode = true; + componentInstance.milestoneMessageIsToBeDisplayed = true; + componentInstance.completedChaptersCount = 2; + spyOn(componentInstance, 'generateMilestoneMessage').and.callThrough(); + spyOn(translateService, 'instant').and.callThrough(); + + expect(componentInstance.generateMilestoneMessage()).toBe(''); + expect(translateService.instant).not.toHaveBeenCalled(); + } + ); + it('should correctly show milestone progress bar', () => { componentInstance.inStoryMode = true; componentInstance.milestoneMessageIsToBeDisplayed = false; componentInstance.completedChaptersCount = 2; - spyOn(componentInstance, 'setNextMilestoneAndCheckIfProgressBarIsShown') - .and.callThrough(); + spyOn( + componentInstance, + 'setNextMilestoneAndCheckIfProgressBarIsShown' + ).and.callThrough(); - expect(componentInstance.setNextMilestoneAndCheckIfProgressBarIsShown()) - .toBe(true); + expect( + componentInstance.setNextMilestoneAndCheckIfProgressBarIsShown() + ).toBe(true); expect(componentInstance.nextMilestoneChapterCount).toBe(5); componentInstance.completedChaptersCount = 4; - expect(componentInstance.setNextMilestoneAndCheckIfProgressBarIsShown()) - .toBe(true); + expect( + componentInstance.setNextMilestoneAndCheckIfProgressBarIsShown() + ).toBe(true); expect(componentInstance.nextMilestoneChapterCount).toBe(5); - spyOn(componentInstance, 'isCompletedChaptersCountGreaterThanLastMilestone') - .and.returnValue(false); spyOn( - componentInstance, 'isMilestoneReachedAndMilestoneMessageToBeDisplayed') - .and.returnValue(false); + componentInstance, + 'isCompletedChaptersCountGreaterThanLastMilestone' + ).and.returnValue(false); + spyOn( + componentInstance, + 'isMilestoneReachedAndMilestoneMessageToBeDisplayed' + ).and.returnValue(false); componentInstance.completedChaptersCount = 55; componentInstance.milestoneMessageIsToBeDisplayed = true; - expect(componentInstance.setNextMilestoneAndCheckIfProgressBarIsShown()) - .toBe(false); + expect( + componentInstance.setNextMilestoneAndCheckIfProgressBarIsShown() + ).toBe(false); }); - it('should not show milestone progress bar if completed chapters count ' + - 'is greater than 50', () => { - componentInstance.inStoryMode = true; - componentInstance.milestoneMessageIsToBeDisplayed = false; - componentInstance.completedChaptersCount = 51; - spyOn(componentInstance, 'setNextMilestoneAndCheckIfProgressBarIsShown') - .and.callThrough(); + it( + 'should not show milestone progress bar if completed chapters count ' + + 'is greater than 50', + () => { + componentInstance.inStoryMode = true; + componentInstance.milestoneMessageIsToBeDisplayed = false; + componentInstance.completedChaptersCount = 51; + spyOn( + componentInstance, + 'setNextMilestoneAndCheckIfProgressBarIsShown' + ).and.callThrough(); - expect(componentInstance.setNextMilestoneAndCheckIfProgressBarIsShown()) - .toBe(false); - expect(componentInstance.nextMilestoneChapterCount).toBeNull(); - }); + expect( + componentInstance.setNextMilestoneAndCheckIfProgressBarIsShown() + ).toBe(false); + expect(componentInstance.nextMilestoneChapterCount).toBeNull(); + } + ); it('should not show milestone progress bar if not in story mode', () => { componentInstance.inStoryMode = false; componentInstance.milestoneMessageIsToBeDisplayed = false; componentInstance.completedChaptersCount = 1; - spyOn(componentInstance, 'setNextMilestoneAndCheckIfProgressBarIsShown') - .and.callThrough(); + spyOn( + componentInstance, + 'setNextMilestoneAndCheckIfProgressBarIsShown' + ).and.callThrough(); - expect(componentInstance.setNextMilestoneAndCheckIfProgressBarIsShown()) - .toBe(false); + expect( + componentInstance.setNextMilestoneAndCheckIfProgressBarIsShown() + ).toBe(false); expect(componentInstance.nextMilestoneChapterCount).toBeNull(); }); - it('should not show milestone progress bar if milestone message is to be ' + - 'displayed, and completed chapters count is a milestone', () => { - componentInstance.inStoryMode = true; - componentInstance.milestoneMessageIsToBeDisplayed = true; - componentInstance.completedChaptersCount = 1; - spyOn(componentInstance, 'setNextMilestoneAndCheckIfProgressBarIsShown') - .and.callThrough(); + it( + 'should not show milestone progress bar if milestone message is to be ' + + 'displayed, and completed chapters count is a milestone', + () => { + componentInstance.inStoryMode = true; + componentInstance.milestoneMessageIsToBeDisplayed = true; + componentInstance.completedChaptersCount = 1; + spyOn( + componentInstance, + 'setNextMilestoneAndCheckIfProgressBarIsShown' + ).and.callThrough(); - expect(componentInstance.setNextMilestoneAndCheckIfProgressBarIsShown()) - .toBe(false); - expect(componentInstance.nextMilestoneChapterCount).toBeNull(); - }); + expect( + componentInstance.setNextMilestoneAndCheckIfProgressBarIsShown() + ).toBe(false); + expect(componentInstance.nextMilestoneChapterCount).toBeNull(); + } + ); - it('should show milestone progress bar even if the completed chapters count' + - ' is a milestone, if the milestone message is not to be displayed', () => { - componentInstance.inStoryMode = true; - componentInstance.milestoneMessageIsToBeDisplayed = false; - componentInstance.completedChaptersCount = 10; - spyOn(componentInstance, 'setNextMilestoneAndCheckIfProgressBarIsShown') - .and.callThrough(); + it( + 'should show milestone progress bar even if the completed chapters count' + + ' is a milestone, if the milestone message is not to be displayed', + () => { + componentInstance.inStoryMode = true; + componentInstance.milestoneMessageIsToBeDisplayed = false; + componentInstance.completedChaptersCount = 10; + spyOn( + componentInstance, + 'setNextMilestoneAndCheckIfProgressBarIsShown' + ).and.callThrough(); - expect(componentInstance.setNextMilestoneAndCheckIfProgressBarIsShown()) - .toBe(true); - expect(componentInstance.nextMilestoneChapterCount).toBe(25); - }); + expect( + componentInstance.setNextMilestoneAndCheckIfProgressBarIsShown() + ).toBe(true); + expect(componentInstance.nextMilestoneChapterCount).toBe(25); + } + ); - it('should correctly determine if new milestone is reached and message is ' + - 'to be displayed', () => { - componentInstance.milestoneMessageIsToBeDisplayed = true; - componentInstance.completedChaptersCount = 1; + it( + 'should correctly determine if new milestone is reached and message is ' + + 'to be displayed', + () => { + componentInstance.milestoneMessageIsToBeDisplayed = true; + componentInstance.completedChaptersCount = 1; - expect( - componentInstance.isMilestoneReachedAndMilestoneMessageToBeDisplayed()) - .toBe(true); + expect( + componentInstance.isMilestoneReachedAndMilestoneMessageToBeDisplayed() + ).toBe(true); - componentInstance.completedChaptersCount = 2; + componentInstance.completedChaptersCount = 2; - expect( - componentInstance.isMilestoneReachedAndMilestoneMessageToBeDisplayed()) - .toBe(false); + expect( + componentInstance.isMilestoneReachedAndMilestoneMessageToBeDisplayed() + ).toBe(false); - componentInstance.milestoneMessageIsToBeDisplayed = false; - componentInstance.completedChaptersCount = 1; + componentInstance.milestoneMessageIsToBeDisplayed = false; + componentInstance.completedChaptersCount = 1; - expect( - componentInstance.isMilestoneReachedAndMilestoneMessageToBeDisplayed()) - .toBe(false); - }); + expect( + componentInstance.isMilestoneReachedAndMilestoneMessageToBeDisplayed() + ).toBe(false); + } + ); - it('should correctly determine if completed chapters count is greater than ' + - 'last milestone', () => { - componentInstance.completedChaptersCount = 1; + it( + 'should correctly determine if completed chapters count is greater than ' + + 'last milestone', + () => { + componentInstance.completedChaptersCount = 1; - expect(componentInstance.isCompletedChaptersCountGreaterThanLastMilestone()) - .toBe(false); + expect( + componentInstance.isCompletedChaptersCountGreaterThanLastMilestone() + ).toBe(false); - componentInstance.completedChaptersCount = 51; + componentInstance.completedChaptersCount = 51; - expect(componentInstance.isCompletedChaptersCountGreaterThanLastMilestone()) - .toBe(true); - }); + expect( + componentInstance.isCompletedChaptersCountGreaterThanLastMilestone() + ).toBe(true); + } + ); it('should update displayed card', fakeAsync(() => { mockDisplayedCard.markAsCompleted(); @@ -641,9 +744,11 @@ describe('Tutor card component', () => { let mockOnNewCardAvailableEventEmitter = new EventEmitter(); spyOnProperty(playerPositionService, 'onNewCardAvailable').and.returnValue( - mockOnNewCardAvailableEventEmitter); - spyOn(currentInteractionService, 'registerPresubmitHook') - .and.callFake(callb => callb()); + mockOnNewCardAvailableEventEmitter + ); + spyOn(currentInteractionService, 'registerPresubmitHook').and.callFake( + callb => callb() + ); spyOn(audioTranslationManagerService, 'clearSecondaryAudioTranslations'); spyOn(audioTranslationManagerService, 'setContentAudioTranslations'); spyOn(audioPlayerService, 'clear'); @@ -654,25 +759,32 @@ describe('Tutor card component', () => { mockOnNewCardAvailableEventEmitter.emit(); tick(); - expect(audioTranslationManagerService.clearSecondaryAudioTranslations) - .toHaveBeenCalled(); - expect(audioTranslationManagerService.setContentAudioTranslations) - .toHaveBeenCalled(); + expect( + audioTranslationManagerService.clearSecondaryAudioTranslations + ).toHaveBeenCalled(); + expect( + audioTranslationManagerService.setContentAudioTranslations + ).toHaveBeenCalled(); expect(audioPlayerService.clear).toHaveBeenCalled(); - expect(audioPreloaderService.clearMostRecentlyRequestedAudioFilename) - .toHaveBeenCalled(); + expect( + audioPreloaderService.clearMostRecentlyRequestedAudioFilename + ).toHaveBeenCalled(); expect(autogeneratedAudioPlayerService.cancel).toHaveBeenCalled(); })); it('should get the static image url from the image path', () => { spyOn(urlInterpolationService, 'getStaticImageUrl').and.returnValue( - '/assets/images/general/milestone-message-star-icon.svg'); + '/assets/images/general/milestone-message-star-icon.svg' + ); - expect(componentInstance.getStaticImageUrl( - '/general/milestone-message-star-icon.svg')).toBe( - '/assets/images/general/milestone-message-star-icon.svg'); - expect(urlInterpolationService.getStaticImageUrl) - .toHaveBeenCalledWith('/general/milestone-message-star-icon.svg'); + expect( + componentInstance.getStaticImageUrl( + '/general/milestone-message-star-icon.svg' + ) + ).toBe('/assets/images/general/milestone-message-star-icon.svg'); + expect(urlInterpolationService.getStaticImageUrl).toHaveBeenCalledWith( + '/general/milestone-message-star-icon.svg' + ); }); it('should tell if audio bar is expanded on mobile device', () => { @@ -685,17 +797,20 @@ describe('Tutor card component', () => { componentInstance.displayedCard = mockDisplayedCard; let mockOnNewCardAvailableEventEmitter = new EventEmitter(); spyOnProperty(playerPositionService, 'onNewCardAvailable').and.returnValue( - mockOnNewCardAvailableEventEmitter); + mockOnNewCardAvailableEventEmitter + ); mockOnNewCardAvailableEventEmitter.emit(); - spyOn(currentInteractionService, 'registerPresubmitHook') - .and.callFake(callb => callb()); + spyOn(currentInteractionService, 'registerPresubmitHook').and.callFake( + callb => callb() + ); spyOn(mockDisplayedCard, 'getInteraction').and.returnValue( // This throws "Type 'null' is not assignable to type // 'InteractionCustomizationArgs'." We need to suppress this error // because of the need to test validations. This throws an error // because the value of interaction is null. // @ts-ignore - new Interaction([], [], null, null, [], '', null)); + new Interaction([], [], null, null, [], '', null) + ); spyOn(mockDisplayedCard, 'isCompleted').and.returnValue(true); spyOn(audioTranslationManagerService, 'setContentAudioTranslations'); spyOn(audioPlayerService, 'clear'); @@ -718,12 +833,15 @@ describe('Tutor card component', () => { }); it('should get content audio highlight class', () => { - spyOn(audioTranslationManagerService, 'getCurrentComponentName') - .and.returnValue(AppConstants.COMPONENT_NAME_CONTENT); + spyOn( + audioTranslationManagerService, + 'getCurrentComponentName' + ).and.returnValue(AppConstants.COMPONENT_NAME_CONTENT); spyOn(audioPlayerService, 'isPlaying').and.returnValue(false); spyOn(autogeneratedAudioPlayerService, 'isPlaying').and.returnValue(true); expect(componentInstance.getContentAudioHighlightClass()).toEqual( - ExplorationPlayerConstants.AUDIO_HIGHLIGHT_CSS_CLASS); + ExplorationPlayerConstants.AUDIO_HIGHLIGHT_CSS_CLASS + ); }); it('should throw error if background image are empty', fakeAsync(() => { @@ -741,7 +859,8 @@ describe('Tutor card component', () => { it('should get content focus label', () => { expect(componentInstance.getContentFocusLabel(1)).toEqual( - ExplorationPlayerConstants.CONTENT_FOCUS_LABEL_PREFIX + 1); + ExplorationPlayerConstants.CONTENT_FOCUS_LABEL_PREFIX + 1 + ); }); it('should toggle show previous responses', () => { @@ -762,8 +881,9 @@ describe('Tutor card component', () => { it('should tell if audio bar can be shown', () => { componentInstance.isIframed = false; - spyOn(explorationPlayerStateService, 'isInQuestionMode') - .and.returnValue(false); + spyOn(explorationPlayerStateService, 'isInQuestionMode').and.returnValue( + false + ); expect(componentInstance.showAudioBar()).toBeTrue(); }); @@ -772,8 +892,10 @@ describe('Tutor card component', () => { componentInstance.displayedCard = mockDisplayedCard; expect(componentInstance.isContentAudioTranslationAvailable()).toBeFalse(); componentInstance.conceptCardIsBeingShown = false; - spyOn(mockDisplayedCard, 'isContentAudioTranslationAvailable') - .and.returnValue(true); + spyOn( + mockDisplayedCard, + 'isContentAudioTranslationAvailable' + ).and.returnValue(true); componentInstance.displayedCard = mockDisplayedCard; expect(componentInstance.isContentAudioTranslationAvailable()).toBeTrue(); }); @@ -792,6 +914,7 @@ describe('Tutor card component', () => { it('should get input response pair id', () => { expect(componentInstance.getInputResponsePairId(1)).toEqual( - 'input-response-pair-1'); + 'input-response-pair-1' + ); }); }); diff --git a/core/templates/pages/exploration-player-page/learner-experience/tutor-card.component.ts b/core/templates/pages/exploration-player-page/learner-experience/tutor-card.component.ts index 6dd25bbb09dc..3b68f4499ff2 100644 --- a/core/templates/pages/exploration-player-page/learner-experience/tutor-card.component.ts +++ b/core/templates/pages/exploration-player-page/learner-experience/tutor-card.component.ts @@ -16,40 +16,53 @@ * @fileoverview Component for the Tutor Card. */ -import { Component, Input, SimpleChanges, ViewChild, Renderer2 } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { TranslateService } from '@ngx-translate/core'; -import { AppConstants } from 'app.constants'; -import { BindableVoiceovers } from 'domain/exploration/recorded-voiceovers.model'; -import { StateCard } from 'domain/state_card/state-card.model'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; +import { + Component, + Input, + SimpleChanges, + ViewChild, + Renderer2, +} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {TranslateService} from '@ngx-translate/core'; +import {AppConstants} from 'app.constants'; +import {BindableVoiceovers} from 'domain/exploration/recorded-voiceovers.model'; +import {StateCard} from 'domain/state_card/state-card.model'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; import cloneDeep from 'lodash/cloneDeep'; import isEqual from 'lodash/isEqual'; -import { Subscription } from 'rxjs'; -import { AudioBarStatusService } from 'services/audio-bar-status.service'; -import { AudioPlayerService } from 'services/audio-player.service'; -import { AutogeneratedAudioPlayerService } from 'services/autogenerated-audio-player.service'; -import { ContextService } from 'services/context.service'; -import { DeviceInfoService } from 'services/contextual/device-info.service'; -import { UrlService } from 'services/contextual/url.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { UserService } from 'services/user.service'; -import { ExplorationPlayerConstants } from '../exploration-player-page.constants'; -import { AudioPreloaderService } from '../services/audio-preloader.service'; -import { AudioTranslationManagerService } from '../services/audio-translation-manager.service'; -import { CurrentInteractionService } from '../services/current-interaction.service'; -import { ExplorationPlayerStateService } from '../services/exploration-player-state.service'; -import { LearnerAnswerInfoService } from '../services/learner-answer-info.service'; -import { PlayerPositionService } from '../services/player-position.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { animate, keyframes, state, style, transition, trigger } from '@angular/animations'; -import { CollectionSummary } from 'domain/collection/collection-summary.model'; -import { LearnerExplorationSummary } from 'domain/summary/learner-exploration-summary.model'; -import { EndChapterCheckMarkComponent } from './end-chapter-check-mark.component'; -import { EndChapterConfettiComponent } from './end-chapter-confetti.component'; -import { PlatformFeatureService } from 'services/platform-feature.service'; -import { QuestionPlayerConfig } from './ratings-and-recommendations.component'; +import {Subscription} from 'rxjs'; +import {AudioBarStatusService} from 'services/audio-bar-status.service'; +import {AudioPlayerService} from 'services/audio-player.service'; +import {AutogeneratedAudioPlayerService} from 'services/autogenerated-audio-player.service'; +import {ContextService} from 'services/context.service'; +import {DeviceInfoService} from 'services/contextual/device-info.service'; +import {UrlService} from 'services/contextual/url.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {UserService} from 'services/user.service'; +import {ExplorationPlayerConstants} from '../exploration-player-page.constants'; +import {AudioPreloaderService} from '../services/audio-preloader.service'; +import {AudioTranslationManagerService} from '../services/audio-translation-manager.service'; +import {CurrentInteractionService} from '../services/current-interaction.service'; +import {ExplorationPlayerStateService} from '../services/exploration-player-state.service'; +import {LearnerAnswerInfoService} from '../services/learner-answer-info.service'; +import {PlayerPositionService} from '../services/player-position.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import { + animate, + keyframes, + state, + style, + transition, + trigger, +} from '@angular/animations'; +import {CollectionSummary} from 'domain/collection/collection-summary.model'; +import {LearnerExplorationSummary} from 'domain/summary/learner-exploration-summary.model'; +import {EndChapterCheckMarkComponent} from './end-chapter-check-mark.component'; +import {EndChapterConfettiComponent} from './end-chapter-confetti.component'; +import {PlatformFeatureService} from 'services/platform-feature.service'; +import {QuestionPlayerConfig} from './ratings-and-recommendations.component'; const CHECK_MARK_HIDE_DELAY_IN_MSECS = 500; const REDUCED_MOTION_ANIMATION_DURATION_IN_MSECS = 2000; @@ -59,39 +72,44 @@ const MILESTONE_SPECIFIC_COMPLETED_CHAPTER_COUNTS = [1, 5, 10, 25, 50]; import './tutor-card.component.css'; - @Component({ selector: 'oppia-tutor-card', templateUrl: './tutor-card.component.html', styleUrls: ['./tutor-card.component.css'], animations: [ trigger('expandInOut', [ - state('in', style({ - overflow: 'visible', - height: '*' - })), - state('out', style({ - overflow: 'hidden', - height: '0px', - display: 'none' - })), + state( + 'in', + style({ + overflow: 'visible', + height: '*', + }) + ), + state( + 'out', + style({ + overflow: 'hidden', + height: '0px', + display: 'none', + }) + ), transition('in => out', animate('500ms ease-in-out')), transition('out => in', [ - style({ display: 'block' }), - animate('500ms ease-in-out') - ]) + style({display: 'block'}), + animate('500ms ease-in-out'), + ]), ]), trigger('fadeInOut', [ transition('void => *', []), transition('* <=> *', [ - style({ opacity: 0 }), - animate('1s ease', keyframes([ - style({ opacity: 0 }), - style({ opacity: 1 }) - ])) - ]) - ]) - ] + style({opacity: 0}), + animate( + '1s ease', + keyframes([style({opacity: 0}), style({opacity: 1})]) + ), + ]), + ]), + ], }) export class TutorCardComponent { // These properties are initialized using Angular lifecycle hooks @@ -120,7 +138,7 @@ export class TutorCardComponent { @Input() inputOutputHistoryIsShown!: boolean; @Input() checkpointCelebrationModalIsEnabled!: boolean; private _editorPreviewMode!: boolean; - lastAnswer!: { answerDetails: string } | string | null; + lastAnswer!: {answerDetails: string} | string | null; conceptCardIsBeingShown!: boolean; interactionIsActive!: boolean; waitingForOppiaFeedback: boolean = false; @@ -169,23 +187,27 @@ export class TutorCardComponent { this.username = userInfo.getUsername(); if (!this._editorPreviewMode) { if (this.username !== null) { - [this.profilePicturePngDataUrl, this.profilePictureWebpDataUrl] = ( - this.userService.getProfileImageDataUrl(this.username)); + [this.profilePicturePngDataUrl, this.profilePictureWebpDataUrl] = + this.userService.getProfileImageDataUrl(this.username); } else { - this.profilePictureWebpDataUrl = ( + this.profilePictureWebpDataUrl = this.urlInterpolationService.getStaticImageUrl( - AppConstants.DEFAULT_PROFILE_IMAGE_WEBP_PATH)); - this.profilePicturePngDataUrl = ( + AppConstants.DEFAULT_PROFILE_IMAGE_WEBP_PATH + ); + this.profilePicturePngDataUrl = this.urlInterpolationService.getStaticImageUrl( - AppConstants.DEFAULT_PROFILE_IMAGE_PNG_PATH)); + AppConstants.DEFAULT_PROFILE_IMAGE_PNG_PATH + ); } } else { - this.profilePictureWebpDataUrl = ( + this.profilePictureWebpDataUrl = this.urlInterpolationService.getStaticImageUrl( - AppConstants.DEFAULT_PROFILE_IMAGE_WEBP_PATH)); - this.profilePicturePngDataUrl = ( + AppConstants.DEFAULT_PROFILE_IMAGE_WEBP_PATH + ); + this.profilePicturePngDataUrl = this.urlInterpolationService.getStaticImageUrl( - AppConstants.DEFAULT_PROFILE_IMAGE_PNG_PATH)); + AppConstants.DEFAULT_PROFILE_IMAGE_PNG_PATH + ); } } @@ -193,11 +215,12 @@ export class TutorCardComponent { this._editorPreviewMode = this.contextService.isInExplorationEditorPage(); this.getUserInfoAsync(); this.isIframed = this.urlService.isIframed(); - this.getCanAskLearnerForAnswerInfo = ( - this.learnerAnswerInfoService.getCanAskLearnerForAnswerInfo); - this.OPPIA_AVATAR_IMAGE_URL = ( - this.urlInterpolationService - .getStaticImageUrl('/avatar/oppia_avatar_100px.svg')); + this.getCanAskLearnerForAnswerInfo = + this.learnerAnswerInfoService.getCanAskLearnerForAnswerInfo; + this.OPPIA_AVATAR_IMAGE_URL = + this.urlInterpolationService.getStaticImageUrl( + '/avatar/oppia_avatar_100px.svg' + ); this.directiveSubscriptions.add( this.explorationPlayerStateService.onOppiaFeedbackAvailable.subscribe( @@ -206,11 +229,11 @@ export class TutorCardComponent { // Auto scroll to the new feedback on mobile device. if (this.deviceInfoService.isMobileDevice()) { - let latestFeedbackIndex = ( - this.displayedCard.getInputResponsePairs().length - 1); + let latestFeedbackIndex = + this.displayedCard.getInputResponsePairs().length - 1; - this.windowRef.nativeWindow.location.hash = ( - this.getInputResponsePairId(latestFeedbackIndex)); + this.windowRef.nativeWindow.location.hash = + this.getInputResponsePairId(latestFeedbackIndex); } } ) @@ -226,7 +249,9 @@ export class TutorCardComponent { changes.displayedCard && !isEqual( changes.displayedCard.previousValue, - changes.displayedCard.currentValue)) { + changes.displayedCard.currentValue + ) + ) { this.updateDisplayedCard(); } if ( @@ -242,17 +267,17 @@ export class TutorCardComponent { triggerCelebratoryAnimation(): void { this.checkMarkHidden = false; this.checkMarkComponent.animateCheckMark(); - this.skipClickListener = this.renderer.listen( - 'document', 'click', () => { - clearTimeout(this.confettiAnimationTimeout); - this.checkMarkSkipped = true; - setTimeout(() => { - this.checkMarkHidden = true; - }, CHECK_MARK_HIDE_DELAY_IN_MSECS); - }); + this.skipClickListener = this.renderer.listen('document', 'click', () => { + clearTimeout(this.confettiAnimationTimeout); + this.checkMarkSkipped = true; + setTimeout(() => { + this.checkMarkHidden = true; + }, CHECK_MARK_HIDE_DELAY_IN_MSECS); + }); this.animationHasPlayedOnce = true; - let mediaQuery = - this.windowRef.nativeWindow.matchMedia('(prefers-reduced-motion)'); + let mediaQuery = this.windowRef.nativeWindow.matchMedia( + '(prefers-reduced-motion)' + ); if (mediaQuery.matches) { setTimeout(() => { this.checkMarkSkipped = true; @@ -279,18 +304,22 @@ export class TutorCardComponent { } generateMilestoneMessage(): string { - if (!this.inStoryMode || - !this.milestoneMessageIsToBeDisplayed || - !this.completedChaptersCount || - !MILESTONE_SPECIFIC_COMPLETED_CHAPTER_COUNTS.includes( - this.completedChaptersCount)) { + if ( + !this.inStoryMode || + !this.milestoneMessageIsToBeDisplayed || + !this.completedChaptersCount || + !MILESTONE_SPECIFIC_COMPLETED_CHAPTER_COUNTS.includes( + this.completedChaptersCount + ) + ) { return ''; } - let chapterCountMessageIndex = ( + let chapterCountMessageIndex = MILESTONE_SPECIFIC_COMPLETED_CHAPTER_COUNTS.indexOf( - this.completedChaptersCount)) + 1; - let milestoneMessageTranslationKey = ( - 'I18N_END_CHAPTER_MILESTONE_MESSAGE_' + chapterCountMessageIndex); + this.completedChaptersCount + ) + 1; + let milestoneMessageTranslationKey = + 'I18N_END_CHAPTER_MILESTONE_MESSAGE_' + chapterCountMessageIndex; return this.translateService.instant(milestoneMessageTranslationKey); } @@ -307,13 +336,15 @@ export class TutorCardComponent { if ( !this.milestoneMessageIsToBeDisplayed && MILESTONE_SPECIFIC_COMPLETED_CHAPTER_COUNTS.includes( - this.completedChaptersCount) + this.completedChaptersCount + ) ) { - let chapterCountIndex = ( + let chapterCountIndex = MILESTONE_SPECIFIC_COMPLETED_CHAPTER_COUNTS.indexOf( - this.completedChaptersCount)); - this.nextMilestoneChapterCount = ( - MILESTONE_SPECIFIC_COMPLETED_CHAPTER_COUNTS[chapterCountIndex + 1]); + this.completedChaptersCount + ); + this.nextMilestoneChapterCount = + MILESTONE_SPECIFIC_COMPLETED_CHAPTER_COUNTS[chapterCountIndex + 1]; return true; } @@ -359,11 +390,12 @@ export class TutorCardComponent { this.arePreviousResponsesShown = false; this.lastAnswer = null; this.conceptCardIsBeingShown = Boolean( - !this.displayedCard.getInteraction()); + !this.displayedCard.getInteraction() + ); this.interactionIsActive = !this.displayedCard.isCompleted(); this.directiveSubscriptions.add( this.playerPositionService.onNewCardAvailable.subscribe( - () => this.interactionIsActive = false + () => (this.interactionIsActive = false) ) ); this.currentInteractionService.registerPresubmitHook(() => { @@ -375,16 +407,15 @@ export class TutorCardComponent { } if (!this.conceptCardIsBeingShown) { - this.interactionInstructions = ( - this.displayedCard.getInteractionInstructions()); - this.contentAudioTranslations = ( - this.displayedCard.getVoiceovers()); - this.audioTranslationManagerService - .clearSecondaryAudioTranslations(); + this.interactionInstructions = + this.displayedCard.getInteractionInstructions(); + this.contentAudioTranslations = this.displayedCard.getVoiceovers(); + this.audioTranslationManagerService.clearSecondaryAudioTranslations(); this.audioTranslationManagerService.setContentAudioTranslations( cloneDeep(this.contentAudioTranslations), this.displayedCard.getContentHtml(), - AppConstants.COMPONENT_NAME_CONTENT); + AppConstants.COMPONENT_NAME_CONTENT + ); this.audioPlayerService.clear(); this.audioPreloaderService.clearMostRecentlyRequestedAudioFilename(); this.autogeneratedAudioPlayerService.cancel(); @@ -400,11 +431,12 @@ export class TutorCardComponent { // This function returns null if audio is not available. getContentAudioHighlightClass(): string | null { - if (this.audioTranslationManagerService - .getCurrentComponentName() === - AppConstants.COMPONENT_NAME_CONTENT && + if ( + this.audioTranslationManagerService.getCurrentComponentName() === + AppConstants.COMPONENT_NAME_CONTENT && (this.audioPlayerService.isPlaying() || - this.autogeneratedAudioPlayerService.isPlaying())) { + this.autogeneratedAudioPlayerService.isPlaying()) + ) { return ExplorationPlayerConstants.AUDIO_HIGHLIGHT_CSS_CLASS; } return null; @@ -415,8 +447,7 @@ export class TutorCardComponent { } toggleShowPreviousResponses(): void { - this.arePreviousResponsesShown = - !this.arePreviousResponsesShown; + this.arePreviousResponsesShown = !this.arePreviousResponsesShown; } isWindowNarrow(): boolean { @@ -424,22 +455,23 @@ export class TutorCardComponent { } canWindowShowTwoCards(): boolean { - return this.windowDimensionsService.getWidth() > - ExplorationPlayerConstants.TWO_CARD_THRESHOLD_PX; + return ( + this.windowDimensionsService.getWidth() > + ExplorationPlayerConstants.TWO_CARD_THRESHOLD_PX + ); } showAudioBar(): boolean { return ( - !this.isIframed && - !this.explorationPlayerStateService.isInQuestionMode()); + !this.isIframed && !this.explorationPlayerStateService.isInQuestionMode() + ); } isContentAudioTranslationAvailable(): boolean { if (this.conceptCardIsBeingShown) { return false; } - return ( - this.displayedCard.isContentAudioTranslationAvailable()); + return this.displayedCard.isContentAudioTranslationAvailable(); } isCurrentCardAtEndOfTranscript(): boolean { @@ -455,7 +487,9 @@ export class TutorCardComponent { } } -angular.module('oppia').directive('oppiaTutorCard', +angular.module('oppia').directive( + 'oppiaTutorCard', downgradeComponent({ - component: TutorCardComponent - }) as angular.IDirectiveFactory); + component: TutorCardComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-player-page/modals/display-hint-modal.component.spec.ts b/core/templates/pages/exploration-player-page/modals/display-hint-modal.component.spec.ts index cde36d4d00ca..eb1777a4584e 100644 --- a/core/templates/pages/exploration-player-page/modals/display-hint-modal.component.spec.ts +++ b/core/templates/pages/exploration-player-page/modals/display-hint-modal.component.spec.ts @@ -16,23 +16,23 @@ * @fileoverview Unit tests for DisplayHintModalComponent. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { RecordedVoiceovers } from 'domain/exploration/recorded-voiceovers.model'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { StateCard } from 'domain/state_card/state-card.model'; -import { AudioPlayerService } from 'services/audio-player.service'; -import { AutogeneratedAudioPlayerService } from 'services/autogenerated-audio-player.service'; -import { AudioTranslationManagerService } from '../services/audio-translation-manager.service'; -import { HintsAndSolutionManagerService } from '../services/hints-and-solution-manager.service'; -import { PlayerPositionService } from '../services/player-position.service'; -import { PlayerTranscriptService } from '../services/player-transcript.service'; -import { DisplayHintModalComponent } from './display-hint-modal.component'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { Interaction } from 'domain/exploration/InteractionObjectFactory'; -import { AudioTranslationLanguageService } from '../services/audio-translation-language.service'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {RecordedVoiceovers} from 'domain/exploration/recorded-voiceovers.model'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {StateCard} from 'domain/state_card/state-card.model'; +import {AudioPlayerService} from 'services/audio-player.service'; +import {AutogeneratedAudioPlayerService} from 'services/autogenerated-audio-player.service'; +import {AudioTranslationManagerService} from '../services/audio-translation-manager.service'; +import {HintsAndSolutionManagerService} from '../services/hints-and-solution-manager.service'; +import {PlayerPositionService} from '../services/player-position.service'; +import {PlayerTranscriptService} from '../services/player-transcript.service'; +import {DisplayHintModalComponent} from './display-hint-modal.component'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {Interaction} from 'domain/exploration/InteractionObjectFactory'; +import {AudioTranslationLanguageService} from '../services/audio-translation-language.service'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; describe('Display hint modal', () => { let fixture: ComponentFixture; @@ -47,10 +47,7 @@ describe('Display hint modal', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - DisplayHintModalComponent, - MockTranslatePipe, - ], + declarations: [DisplayHintModalComponent, MockTranslatePipe], providers: [ NgbActiveModal, AudioPlayerService, @@ -58,24 +55,26 @@ describe('Display hint modal', () => { AutogeneratedAudioPlayerService, HintsAndSolutionManagerService, PlayerPositionService, - PlayerTranscriptService + PlayerTranscriptService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(DisplayHintModalComponent); componentInstance = fixture.componentInstance; - hintsAndSolutionManagerService = ( - TestBed.inject(HintsAndSolutionManagerService)); - playerTranscriptService = (TestBed.inject( - PlayerTranscriptService)); - audioTranslationManagerService = (TestBed.inject( - AudioTranslationManagerService)); + hintsAndSolutionManagerService = TestBed.inject( + HintsAndSolutionManagerService + ); + playerTranscriptService = TestBed.inject(PlayerTranscriptService); + audioTranslationManagerService = TestBed.inject( + AudioTranslationManagerService + ); audioPlayerService = TestBed.inject(AudioPlayerService); - autogeneratedAudioPlayerService = (TestBed.inject( - AutogeneratedAudioPlayerService)); + autogeneratedAudioPlayerService = TestBed.inject( + AutogeneratedAudioPlayerService + ); ngbActiveModal = TestBed.inject(NgbActiveModal); }); @@ -88,9 +87,15 @@ describe('Display hint modal', () => { let recordedVoiceovers = new RecordedVoiceovers({}); let hint = new SubtitledHtml('html', contentId); let displayedCard = new StateCard( - 'test_name', 'content', 'interaction', {} as Interaction, [], - recordedVoiceovers, contentId, - {} as AudioTranslationLanguageService); + 'test_name', + 'content', + 'interaction', + {} as Interaction, + [], + recordedVoiceovers, + contentId, + {} as AudioTranslationLanguageService + ); spyOn(hintsAndSolutionManagerService, 'displayHint').and.returnValue(hint); spyOn(playerTranscriptService, 'getCard').and.returnValue(displayedCard); spyOn(audioTranslationManagerService, 'setSecondaryAudioTranslations'); @@ -100,11 +105,13 @@ describe('Display hint modal', () => { expect(componentInstance.hint).toEqual(hint); expect(componentInstance.displayedCard).toEqual(displayedCard); - expect(componentInstance.recordedVoiceovers) - .toEqual(displayedCard.getRecordedVoiceovers()); + expect(componentInstance.recordedVoiceovers).toEqual( + displayedCard.getRecordedVoiceovers() + ); expect(componentInstance.hintContentId).toEqual(contentId); - expect(audioTranslationManagerService.setSecondaryAudioTranslations) - .toHaveBeenCalled(); + expect( + audioTranslationManagerService.setSecondaryAudioTranslations + ).toHaveBeenCalled(); expect(audioPlayerService.onAutoplayAudio.emit).toHaveBeenCalled(); }); @@ -121,9 +128,15 @@ describe('Display hint modal', () => { let recordedVoiceovers = new RecordedVoiceovers({}); let hint = new SubtitledHtml('html', null); let displayedCard = new StateCard( - 'test_name', 'content', 'interaction', {} as Interaction, [], - recordedVoiceovers, contentId, - {} as AudioTranslationLanguageService); + 'test_name', + 'content', + 'interaction', + {} as Interaction, + [], + recordedVoiceovers, + contentId, + {} as AudioTranslationLanguageService + ); spyOn(hintsAndSolutionManagerService, 'displayHint').and.returnValue(hint); spyOn(playerTranscriptService, 'getCard').and.returnValue(displayedCard); spyOn(audioTranslationManagerService, 'setSecondaryAudioTranslations'); @@ -144,8 +157,9 @@ describe('Display hint modal', () => { expect(audioPlayerService.stop).toHaveBeenCalled(); expect(autogeneratedAudioPlayerService.cancel).toHaveBeenCalled(); - expect(audioTranslationManagerService.clearSecondaryAudioTranslations) - .toHaveBeenCalled(); + expect( + audioTranslationManagerService.clearSecondaryAudioTranslations + ).toHaveBeenCalled(); expect(ngbActiveModal.dismiss).toHaveBeenCalledWith('cancel'); }); }); diff --git a/core/templates/pages/exploration-player-page/modals/display-hint-modal.component.ts b/core/templates/pages/exploration-player-page/modals/display-hint-modal.component.ts index 7af22e402155..f58363ac005e 100644 --- a/core/templates/pages/exploration-player-page/modals/display-hint-modal.component.ts +++ b/core/templates/pages/exploration-player-page/modals/display-hint-modal.component.ts @@ -16,21 +16,21 @@ * @fileoverview Component for display hint modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { RecordedVoiceovers } from 'domain/exploration/recorded-voiceovers.model'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { StateCard } from 'domain/state_card/state-card.model'; -import { AudioPlayerService } from 'services/audio-player.service'; -import { AutogeneratedAudioPlayerService } from 'services/autogenerated-audio-player.service'; -import { AudioTranslationManagerService } from '../services/audio-translation-manager.service'; -import { HintsAndSolutionManagerService } from '../services/hints-and-solution-manager.service'; -import { PlayerPositionService } from '../services/player-position.service'; -import { PlayerTranscriptService } from '../services/player-transcript.service'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {RecordedVoiceovers} from 'domain/exploration/recorded-voiceovers.model'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {StateCard} from 'domain/state_card/state-card.model'; +import {AudioPlayerService} from 'services/audio-player.service'; +import {AutogeneratedAudioPlayerService} from 'services/autogenerated-audio-player.service'; +import {AudioTranslationManagerService} from '../services/audio-translation-manager.service'; +import {HintsAndSolutionManagerService} from '../services/hints-and-solution-manager.service'; +import {PlayerPositionService} from '../services/player-position.service'; +import {PlayerTranscriptService} from '../services/player-transcript.service'; @Component({ selector: 'oppia-display-hint-modal', - templateUrl: './display-hint-modal.component.html' + templateUrl: './display-hint-modal.component.html', }) export class DisplayHintModalComponent { // These properties are initialized using Angular lifecycle hooks @@ -54,14 +54,16 @@ export class DisplayHintModalComponent { ) {} ngOnInit(): void { - let displayHint = ( - this.hintsAndSolutionManagerService.displayHint(this.index)); + let displayHint = this.hintsAndSolutionManagerService.displayHint( + this.index + ); if (displayHint === null) { throw new Error('Hint not found.'); } this.hint = displayHint; this.displayedCard = this.playerTranscriptService.getCard( - this.playerPositionService.getDisplayedCardIndex()); + this.playerPositionService.getDisplayedCardIndex() + ); this.recordedVoiceovers = this.displayedCard.getRecordedVoiceovers(); let contentId = this.hint.contentId; if (contentId === null) { @@ -69,10 +71,11 @@ export class DisplayHintModalComponent { } this.hintContentId = contentId; - this.audioTranslationManagerService - .setSecondaryAudioTranslations( - this.recordedVoiceovers.getBindableVoiceovers(this.hintContentId), - this.hint.html, this.COMPONENT_NAME_HINT); + this.audioTranslationManagerService.setSecondaryAudioTranslations( + this.recordedVoiceovers.getBindableVoiceovers(this.hintContentId), + this.hint.html, + this.COMPONENT_NAME_HINT + ); this.audioPlayerService.onAutoplayAudio.emit(); } diff --git a/core/templates/pages/exploration-player-page/modals/display-solution-interstitial-modal.component.spec.ts b/core/templates/pages/exploration-player-page/modals/display-solution-interstitial-modal.component.spec.ts index f1fa1cc3267d..edbcae87f61f 100644 --- a/core/templates/pages/exploration-player-page/modals/display-solution-interstitial-modal.component.spec.ts +++ b/core/templates/pages/exploration-player-page/modals/display-solution-interstitial-modal.component.spec.ts @@ -16,10 +16,10 @@ * @fileoverview Unit tests for DisplaySolutionInterstitialModalComponent. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { DisplaySolutionInterstititalModalComponent } from './display-solution-interstitial-modal.component'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {DisplaySolutionInterstititalModalComponent} from './display-solution-interstitial-modal.component'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; describe('Display Interstitial Solution Modal', () => { let fixture: ComponentFixture; @@ -29,17 +29,16 @@ describe('Display Interstitial Solution Modal', () => { TestBed.configureTestingModule({ declarations: [ DisplaySolutionInterstititalModalComponent, - MockTranslatePipe + MockTranslatePipe, ], - providers: [ - NgbActiveModal - ] + providers: [NgbActiveModal], }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent( - DisplaySolutionInterstititalModalComponent); + DisplaySolutionInterstititalModalComponent + ); componentInstance = fixture.componentInstance; }); diff --git a/core/templates/pages/exploration-player-page/modals/display-solution-interstitial-modal.component.ts b/core/templates/pages/exploration-player-page/modals/display-solution-interstitial-modal.component.ts index 73608a746471..b7e9165da884 100644 --- a/core/templates/pages/exploration-player-page/modals/display-solution-interstitial-modal.component.ts +++ b/core/templates/pages/exploration-player-page/modals/display-solution-interstitial-modal.component.ts @@ -16,19 +16,16 @@ * @fileoverview Component for display solution interstitial modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-display-interstitial-modal', - templateUrl: './display-solution-interstitial-modal.component.html' + templateUrl: './display-solution-interstitial-modal.component.html', }) -export class DisplaySolutionInterstititalModalComponent - extends ConfirmOrCancelModal { - constructor( - private ngbActiveModal: NgbActiveModal - ) { +export class DisplaySolutionInterstititalModalComponent extends ConfirmOrCancelModal { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } } diff --git a/core/templates/pages/exploration-player-page/modals/display-solution-modal.component.spec.ts b/core/templates/pages/exploration-player-page/modals/display-solution-modal.component.spec.ts index 8c92e8f0161b..d316ada32aa1 100644 --- a/core/templates/pages/exploration-player-page/modals/display-solution-modal.component.spec.ts +++ b/core/templates/pages/exploration-player-page/modals/display-solution-modal.component.spec.ts @@ -16,23 +16,23 @@ * @fileoverview Unit tests for DisplaySolutionModalComponent. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { RecordedVoiceovers } from 'domain/exploration/recorded-voiceovers.model'; -import { StateCard } from 'domain/state_card/state-card.model'; -import { AudioPlayerService } from 'services/audio-player.service'; -import { AutogeneratedAudioPlayerService } from 'services/autogenerated-audio-player.service'; -import { AudioTranslationManagerService } from '../services/audio-translation-manager.service'; -import { HintsAndSolutionManagerService } from '../services/hints-and-solution-manager.service'; -import { PlayerPositionService } from '../services/player-position.service'; -import { PlayerTranscriptService } from '../services/player-transcript.service'; -import { DisplaySolutionModalComponent } from './display-solution-modal.component'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { Interaction } from 'domain/exploration/InteractionObjectFactory'; -import { AudioTranslationLanguageService } from '../services/audio-translation-language.service'; -import { InteractionDisplayComponent } from 'components/interaction-display/interaction-display.component'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {RecordedVoiceovers} from 'domain/exploration/recorded-voiceovers.model'; +import {StateCard} from 'domain/state_card/state-card.model'; +import {AudioPlayerService} from 'services/audio-player.service'; +import {AutogeneratedAudioPlayerService} from 'services/autogenerated-audio-player.service'; +import {AudioTranslationManagerService} from '../services/audio-translation-manager.service'; +import {HintsAndSolutionManagerService} from '../services/hints-and-solution-manager.service'; +import {PlayerPositionService} from '../services/player-position.service'; +import {PlayerTranscriptService} from '../services/player-transcript.service'; +import {DisplaySolutionModalComponent} from './display-solution-modal.component'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {Interaction} from 'domain/exploration/InteractionObjectFactory'; +import {AudioTranslationLanguageService} from '../services/audio-translation-language.service'; +import {InteractionDisplayComponent} from 'components/interaction-display/interaction-display.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; describe('Display Solution Modal', () => { let fixture: ComponentFixture; @@ -58,7 +58,7 @@ describe('Display Solution Modal', () => { }, getOppiaSolutionExplanationResponseHtml(): string { return solutionHtml; - } + }, }; } } @@ -69,7 +69,7 @@ describe('Display Solution Modal', () => { declarations: [ DisplaySolutionModalComponent, MockTranslatePipe, - InteractionDisplayComponent + InteractionDisplayComponent, ], providers: [ NgbActiveModal, @@ -78,25 +78,26 @@ describe('Display Solution Modal', () => { AutogeneratedAudioPlayerService, { provide: HintsAndSolutionManagerService, - useClass: MockHintsAndSolutionManagerService + useClass: MockHintsAndSolutionManagerService, }, PlayerPositionService, - PlayerTranscriptService + PlayerTranscriptService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(DisplaySolutionModalComponent); componentInstance = fixture.componentInstance; - playerTranscriptService = (TestBed.inject( - PlayerTranscriptService)); - audioTranslationManagerService = (TestBed.inject( - AudioTranslationManagerService)); - audioPlayerService = (TestBed.inject(AudioPlayerService)); - autogeneratedAudioPlayerService = (TestBed.inject( - AutogeneratedAudioPlayerService)); + playerTranscriptService = TestBed.inject(PlayerTranscriptService); + audioTranslationManagerService = TestBed.inject( + AudioTranslationManagerService + ); + audioPlayerService = TestBed.inject(AudioPlayerService); + autogeneratedAudioPlayerService = TestBed.inject( + AutogeneratedAudioPlayerService + ); ngbActiveModal = TestBed.inject(NgbActiveModal); }); @@ -110,8 +111,15 @@ describe('Display Solution Modal', () => { let recordedVoiceovers = new RecordedVoiceovers({}); let audioTranslation = {} as AudioTranslationLanguageService; let displayedCard = new StateCard( - 'test_name', 'content', 'interaction', interaction, [], - recordedVoiceovers, contentId, audioTranslation); + 'test_name', + 'content', + 'interaction', + interaction, + [], + recordedVoiceovers, + contentId, + audioTranslation + ); spyOn(playerTranscriptService, 'getCard').and.returnValue(displayedCard); spyOn(audioTranslationManagerService, 'setSecondaryAudioTranslations'); spyOn(audioPlayerService.onAutoplayAudio, 'emit'); @@ -121,11 +129,13 @@ describe('Display Solution Modal', () => { expect(componentInstance.solutionContentId).toEqual(contentId); expect(componentInstance.displayedCard).toEqual(displayedCard); expect(componentInstance.recordedVoiceovers).toEqual(recordedVoiceovers); - expect(audioTranslationManagerService.setSecondaryAudioTranslations) - .toHaveBeenCalled(); + expect( + audioTranslationManagerService.setSecondaryAudioTranslations + ).toHaveBeenCalled(); expect(audioPlayerService.onAutoplayAudio.emit).toHaveBeenCalled(); expect(componentInstance.interaction).toEqual( - displayedCard.getInteraction()); + displayedCard.getInteraction() + ); expect(componentInstance.shortAnswerHtml).toEqual(shortAnswerHtml); expect(componentInstance.solutionExplanationHtml).toEqual(solutionHtml); }); @@ -140,8 +150,9 @@ describe('Display Solution Modal', () => { expect(audioPlayerService.stop).toHaveBeenCalled(); expect(autogeneratedAudioPlayerService.cancel).toHaveBeenCalled(); - expect(audioTranslationManagerService.clearSecondaryAudioTranslations) - .toHaveBeenCalled(); + expect( + audioTranslationManagerService.clearSecondaryAudioTranslations + ).toHaveBeenCalled(); expect(ngbActiveModal.dismiss).toHaveBeenCalledWith('cancel'); }); }); diff --git a/core/templates/pages/exploration-player-page/modals/display-solution-modal.component.ts b/core/templates/pages/exploration-player-page/modals/display-solution-modal.component.ts index 06267c5e291c..f26050363cdc 100644 --- a/core/templates/pages/exploration-player-page/modals/display-solution-modal.component.ts +++ b/core/templates/pages/exploration-player-page/modals/display-solution-modal.component.ts @@ -16,22 +16,25 @@ * @fileoverview Component for display solution modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { Interaction } from 'domain/exploration/InteractionObjectFactory'; -import { RecordedVoiceovers } from 'domain/exploration/recorded-voiceovers.model'; -import { ShortAnswerResponse, Solution } from 'domain/exploration/SolutionObjectFactory'; -import { StateCard } from 'domain/state_card/state-card.model'; -import { AudioPlayerService } from 'services/audio-player.service'; -import { AutogeneratedAudioPlayerService } from 'services/autogenerated-audio-player.service'; -import { AudioTranslationManagerService } from '../services/audio-translation-manager.service'; -import { HintsAndSolutionManagerService } from '../services/hints-and-solution-manager.service'; -import { PlayerPositionService } from '../services/player-position.service'; -import { PlayerTranscriptService } from '../services/player-transcript.service'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {Interaction} from 'domain/exploration/InteractionObjectFactory'; +import {RecordedVoiceovers} from 'domain/exploration/recorded-voiceovers.model'; +import { + ShortAnswerResponse, + Solution, +} from 'domain/exploration/SolutionObjectFactory'; +import {StateCard} from 'domain/state_card/state-card.model'; +import {AudioPlayerService} from 'services/audio-player.service'; +import {AutogeneratedAudioPlayerService} from 'services/autogenerated-audio-player.service'; +import {AudioTranslationManagerService} from '../services/audio-translation-manager.service'; +import {HintsAndSolutionManagerService} from '../services/hints-and-solution-manager.service'; +import {PlayerPositionService} from '../services/player-position.service'; +import {PlayerTranscriptService} from '../services/player-transcript.service'; @Component({ selector: 'oppia-display-modal', - templateUrl: './display-solution-modal.component.html' + templateUrl: './display-solution-modal.component.html', }) export class DisplaySolutionModalComponent { // These properties are initialized using Angular lifecycle hooks @@ -60,20 +63,23 @@ export class DisplaySolutionModalComponent { this.solution = this.hintsAndSolutionManagerService.displaySolution(); this.solutionContentId = this.solution.explanation.contentId as string; this.displayedCard = this.playerTranscriptService.getCard( - this.playerPositionService.getDisplayedCardIndex()); + this.playerPositionService.getDisplayedCardIndex() + ); this.recordedVoiceovers = this.displayedCard.getRecordedVoiceovers(); this.audioTranslationManagerService.setSecondaryAudioTranslations( this.recordedVoiceovers.getBindableVoiceovers(this.solutionContentId), - this.solution.explanation.html, this.COMPONENT_NAME_SOLUTION + this.solution.explanation.html, + this.COMPONENT_NAME_SOLUTION ); this.audioPlayerService.onAutoplayAudio.emit(); this.interaction = this.displayedCard.getInteraction(); this.shortAnswerHtml = this.solution.getOppiaShortAnswerResponseHtml( - this.interaction); - this.solutionExplanationHtml = ( - this.solution.getOppiaSolutionExplanationResponseHtml()); + this.interaction + ); + this.solutionExplanationHtml = + this.solution.getOppiaSolutionExplanationResponseHtml(); } closeModal(): void { diff --git a/core/templates/pages/exploration-player-page/modals/exploration-successfully-flagged-modal.component.spec.ts b/core/templates/pages/exploration-player-page/modals/exploration-successfully-flagged-modal.component.spec.ts index 31963fae4171..5d1ecd08ba01 100644 --- a/core/templates/pages/exploration-player-page/modals/exploration-successfully-flagged-modal.component.spec.ts +++ b/core/templates/pages/exploration-player-page/modals/exploration-successfully-flagged-modal.component.spec.ts @@ -16,12 +16,12 @@ * @fileoverview Unit tests for FlagExplorationModalComponent. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { SharedPipesModule } from 'filters/shared-pipes.module'; -import { ExplorationSuccessfullyFlaggedModalComponent } from './exploration-successfully-flagged-modal.component'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {SharedPipesModule} from 'filters/shared-pipes.module'; +import {ExplorationSuccessfullyFlaggedModalComponent} from './exploration-successfully-flagged-modal.component'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; describe('Exploration Successfully flagged modal', () => { let component: ExplorationSuccessfullyFlaggedModalComponent; @@ -29,23 +29,19 @@ describe('Exploration Successfully flagged modal', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - SharedPipesModule - ], + imports: [HttpClientTestingModule, SharedPipesModule], declarations: [ ExplorationSuccessfullyFlaggedModalComponent, - MockTranslatePipe + MockTranslatePipe, ], - providers: [ - NgbActiveModal - ] + providers: [NgbActiveModal], }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent( - ExplorationSuccessfullyFlaggedModalComponent); + ExplorationSuccessfullyFlaggedModalComponent + ); component = fixture.componentInstance; }); diff --git a/core/templates/pages/exploration-player-page/modals/exploration-successfully-flagged-modal.component.ts b/core/templates/pages/exploration-player-page/modals/exploration-successfully-flagged-modal.component.ts index 49b6d7476fe3..3be11456960e 100644 --- a/core/templates/pages/exploration-player-page/modals/exploration-successfully-flagged-modal.component.ts +++ b/core/templates/pages/exploration-player-page/modals/exploration-successfully-flagged-modal.component.ts @@ -16,19 +16,16 @@ * @fileoverview Component for the exploration successfully flagged modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-exploration-successfully-flagged-modal', - templateUrl: './exploration-successfully-flagged-modal.component.html' + templateUrl: './exploration-successfully-flagged-modal.component.html', }) -export class ExplorationSuccessfullyFlaggedModalComponent - extends ConfirmOrCancelModal { - constructor( - private ngbActiveModal: NgbActiveModal - ) { +export class ExplorationSuccessfullyFlaggedModalComponent extends ConfirmOrCancelModal { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } } diff --git a/core/templates/pages/exploration-player-page/modals/flag-exploration-modal.component.spec.ts b/core/templates/pages/exploration-player-page/modals/flag-exploration-modal.component.spec.ts index 12aec74e14eb..a3d2a10ea7a9 100644 --- a/core/templates/pages/exploration-player-page/modals/flag-exploration-modal.component.spec.ts +++ b/core/templates/pages/exploration-player-page/modals/flag-exploration-modal.component.spec.ts @@ -16,15 +16,15 @@ * @fileoverview Unit tests for FlagExplorationModalComponent. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { FormsModule } from '@angular/forms'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { SharedPipesModule } from 'filters/shared-pipes.module'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { PlayerPositionService } from '../services/player-position.service'; -import { FlagExplorationModalComponent } from './flag-exploration-modal.component'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {FormsModule} from '@angular/forms'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {SharedPipesModule} from 'filters/shared-pipes.module'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {PlayerPositionService} from '../services/player-position.service'; +import {FlagExplorationModalComponent} from './flag-exploration-modal.component'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; describe('Flag Exploration modal', () => { let component: FlagExplorationModalComponent; @@ -41,23 +41,16 @@ describe('Flag Exploration modal', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - SharedPipesModule, - FormsModule - ], - declarations: [ - FlagExplorationModalComponent, - MockTranslatePipe - ], + imports: [HttpClientTestingModule, SharedPipesModule, FormsModule], + declarations: [FlagExplorationModalComponent, MockTranslatePipe], providers: [ NgbActiveModal, FocusManagerService, { provide: PlayerPositionService, - useClass: MockPlayerPositionService - } - ] + useClass: MockPlayerPositionService, + }, + ], }).compileComponents(); })); @@ -90,7 +83,7 @@ describe('Flag Exploration modal', () => { expect(ngbActiveModal.close).toHaveBeenCalledWith({ report_type: flag, report_text: flagMessageTextareaIsShown, - state: stateName + state: stateName, }); }); }); diff --git a/core/templates/pages/exploration-player-page/modals/flag-exploration-modal.component.ts b/core/templates/pages/exploration-player-page/modals/flag-exploration-modal.component.ts index b26345fe4124..c27d11175205 100644 --- a/core/templates/pages/exploration-player-page/modals/flag-exploration-modal.component.ts +++ b/core/templates/pages/exploration-player-page/modals/flag-exploration-modal.component.ts @@ -16,21 +16,21 @@ * @fileoverview Component for flag exploration modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { PlayerPositionService } from '../services/player-position.service'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {PlayerPositionService} from '../services/player-position.service'; export interface FlagExplorationModalResult { - 'report_type': boolean; - 'report_text': string; + report_type: boolean; + report_text: string; state: string; } @Component({ selector: 'oppia-flag-exploration-modal', - templateUrl: './flag-exploration-modal.component.html' + templateUrl: './flag-exploration-modal.component.html', }) export class FlagExplorationModalComponent extends ConfirmOrCancelModal { // These properties are initialized using Angular lifecycle hooks @@ -62,7 +62,7 @@ export class FlagExplorationModalComponent extends ConfirmOrCancelModal { this.ngbActiveModal.close({ report_type: this.flag, report_text: this.flagMessageTextareaIsShown, - state: this.stateName + state: this.stateName, }); } } diff --git a/core/templates/pages/exploration-player-page/modals/refresher-exploration-confirmation-modal.component.spec.ts b/core/templates/pages/exploration-player-page/modals/refresher-exploration-confirmation-modal.component.spec.ts index b06e9b35fc65..ac65646924c9 100644 --- a/core/templates/pages/exploration-player-page/modals/refresher-exploration-confirmation-modal.component.spec.ts +++ b/core/templates/pages/exploration-player-page/modals/refresher-exploration-confirmation-modal.component.spec.ts @@ -16,17 +16,23 @@ * @fileoverview Unit tests for RefresherExplorationConfirmationModalComponent. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { TranslateService } from '@ngx-translate/core'; -import { MockTranslateService } from 'components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { UrlService } from 'services/contextual/url.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { ExplorationEngineService } from '../services/exploration-engine.service'; -import { RefresherExplorationConfirmationModal } from './refresher-exploration-confirmation-modal.component'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {TranslateService} from '@ngx-translate/core'; +import {MockTranslateService} from 'components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {UrlService} from 'services/contextual/url.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {ExplorationEngineService} from '../services/exploration-engine.service'; +import {RefresherExplorationConfirmationModal} from './refresher-exploration-confirmation-modal.component'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; describe('Refresher Exploration Confirmation Modal', () => { let fixture: ComponentFixture; @@ -38,7 +44,7 @@ describe('Refresher Exploration Confirmation Modal', () => { class MockUrlService { getUrlParams(): object { - return { collection_id: collectionId }; + return {collection_id: collectionId}; } getQueryFieldValuesAsList(feildName: string): string[] { @@ -55,24 +61,21 @@ describe('Refresher Exploration Confirmation Modal', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - MockTranslatePipe, - RefresherExplorationConfirmationModal - ], + declarations: [MockTranslatePipe, RefresherExplorationConfirmationModal], providers: [ WindowRef, ExplorationEngineService, UrlInterpolationService, { provide: UrlService, - useClass: MockUrlService + useClass: MockUrlService, }, NgbActiveModal, { provide: TranslateService, - useClass: MockTranslateService - } - ] + useClass: MockTranslateService, + }, + ], }); })); diff --git a/core/templates/pages/exploration-player-page/modals/refresher-exploration-confirmation-modal.component.ts b/core/templates/pages/exploration-player-page/modals/refresher-exploration-confirmation-modal.component.ts index ec35dfc29c5a..bff621e86cfb 100644 --- a/core/templates/pages/exploration-player-page/modals/refresher-exploration-confirmation-modal.component.ts +++ b/core/templates/pages/exploration-player-page/modals/refresher-exploration-confirmation-modal.component.ts @@ -16,31 +16,30 @@ * @fileoverview Component for refresher exploration confirmation modal. */ -import { Component, EventEmitter } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { UrlService } from 'services/contextual/url.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { ExplorationEngineService } from '../services/exploration-engine.service'; +import {Component, EventEmitter} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {UrlService} from 'services/contextual/url.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {ExplorationEngineService} from '../services/exploration-engine.service'; @Component({ selector: 'oppia-refresher-confirmation-modal', - templateUrl: './refresher-exploration-confirmation-modal.component.html' + templateUrl: './refresher-exploration-confirmation-modal.component.html', }) -export class RefresherExplorationConfirmationModal - extends ConfirmOrCancelModal { +export class RefresherExplorationConfirmationModal extends ConfirmOrCancelModal { confirmRedirectEventEmitter: EventEmitter = new EventEmitter(); // This property is initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 refresherExplorationId!: string; constructor( - private ngbActiveModal: NgbActiveModal, - private windowRef: WindowRef, - private explorationEngineService: ExplorationEngineService, - private urlInterpolationService: UrlInterpolationService, - private urlService: UrlService + private ngbActiveModal: NgbActiveModal, + private windowRef: WindowRef, + private explorationEngineService: ExplorationEngineService, + private urlInterpolationService: UrlInterpolationService, + private urlService: UrlService ) { super(ngbActiveModal); } @@ -49,13 +48,15 @@ export class RefresherExplorationConfirmationModal this.confirmRedirectEventEmitter.emit(); let collectionId: string = this.urlService.getUrlParams().collection_id; - let parentIdList: string[] = this.urlService.getQueryFieldValuesAsList( - 'parent'); + let parentIdList: string[] = + this.urlService.getQueryFieldValuesAsList('parent'); let EXPLORATION_URL_TEMPLATE: string = '/explore/'; let url = this.urlInterpolationService.interpolateUrl( - EXPLORATION_URL_TEMPLATE, { - exploration_id: this.refresherExplorationId - }); + EXPLORATION_URL_TEMPLATE, + { + exploration_id: this.refresherExplorationId, + } + ); if (collectionId) { url = this.urlService.addField(url, 'collection_id', collectionId); @@ -65,7 +66,10 @@ export class RefresherExplorationConfirmationModal url = this.urlService.addField(url, 'parent', parentIdList[i]); } url = this.urlService.addField( - url, 'parent', this.explorationEngineService.getExplorationId()); + url, + 'parent', + this.explorationEngineService.getExplorationId() + ); // Wait a little before redirecting the page to ensure other // tasks started here (e.g. event recording) have sufficient diff --git a/core/templates/pages/exploration-player-page/new-lesson-player/README.md b/core/templates/pages/exploration-player-page/new-lesson-player/README.md index 81d3e0966a60..b6a8332bcea1 100644 --- a/core/templates/pages/exploration-player-page/new-lesson-player/README.md +++ b/core/templates/pages/exploration-player-page/new-lesson-player/README.md @@ -1,10 +1,11 @@ This folder contains the code for redesigning the exploration player -To test it - +To test it - + 1. Enable the new_lesson_player flag from the release coordinator page 2. Open any lesson in the player 3. Change the URL from '/explore' to '/lesson' -Eg: Change 'localhost:8081/explore/6' to 'localhost:8081/lesson/6' + Eg: Change 'localhost:8081/explore/6' to 'localhost:8081/lesson/6' Refer to the following doc for more info: https://docs.google.com/document/d/1Dnq6E2vTBXxHEDn-CYM5N0zt_7Qk4lX3lBOytX8svDc/ diff --git a/core/templates/pages/exploration-player-page/new-lesson-player/lesson-player-flag.guard.spec.ts b/core/templates/pages/exploration-player-page/new-lesson-player/lesson-player-flag.guard.spec.ts index 8042bd51506f..e74452aacec5 100644 --- a/core/templates/pages/exploration-player-page/new-lesson-player/lesson-player-flag.guard.spec.ts +++ b/core/templates/pages/exploration-player-page/new-lesson-player/lesson-player-flag.guard.spec.ts @@ -16,22 +16,26 @@ * @fileoverview Tests for new lesson player flag guard */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TestBed } from '@angular/core/testing'; -import { ActivatedRouteSnapshot, Router, RouterStateSnapshot, NavigationExtras } from '@angular/router'; - -import { AppConstants } from 'app.constants'; -import { IsNewLessonPlayerGuard } from './lesson-player-flag.guard'; -import { PlatformFeatureService } from 'services/platform-feature.service'; -import { FeatureStatusChecker } from 'domain/feature-flag/feature-status-summary.model'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TestBed} from '@angular/core/testing'; +import { + ActivatedRouteSnapshot, + Router, + RouterStateSnapshot, + NavigationExtras, +} from '@angular/router'; +import {AppConstants} from 'app.constants'; +import {IsNewLessonPlayerGuard} from './lesson-player-flag.guard'; +import {PlatformFeatureService} from 'services/platform-feature.service'; +import {FeatureStatusChecker} from 'domain/feature-flag/feature-status-summary.model'; class MockPlatformFeatureService { get status(): object { return { NewLessonPlayer: { - isEnabled: true - } + isEnabled: true, + }, }; } } @@ -53,13 +57,13 @@ describe('IsNewLessonPlayerGuard', () => { providers: [ { provide: PlatformFeatureService, - useClass: MockPlatformFeatureService + useClass: MockPlatformFeatureService, }, { provide: Router, - useClass: MockRouter - } - ] + useClass: MockRouter, + }, + ], }).compileComponents(); guard = TestBed.inject(IsNewLessonPlayerGuard); @@ -67,39 +71,36 @@ describe('IsNewLessonPlayerGuard', () => { router = TestBed.inject(Router); }); - it('should redirect user to 404 page if flag is disabled', (done) => { - spyOnProperty(platformFeatureService, 'status', 'get').and.returnValue( - { - NewLessonPlayer: { - isEnabled: false - } - } as FeatureStatusChecker - ); + it('should redirect user to 404 page if flag is disabled', done => { + spyOnProperty(platformFeatureService, 'status', 'get').and.returnValue({ + NewLessonPlayer: { + isEnabled: false, + }, + } as FeatureStatusChecker); const navigateSpy = spyOn(router, 'navigate').and.callThrough(); - guard.canActivate( - new ActivatedRouteSnapshot(), {} as RouterStateSnapshot).then( - (canActivate) => { + guard + .canActivate(new ActivatedRouteSnapshot(), {} as RouterStateSnapshot) + .then(canActivate => { expect(canActivate).toBeFalse(); expect(navigateSpy).toHaveBeenCalledWith([ - `${AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ERROR.ROUTE}/404`]); + `${AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ERROR.ROUTE}/404`, + ]); done(); }); }); - it('should not redirect user to 404 page if flag is enabled', (done) => { - spyOnProperty(platformFeatureService, 'status', 'get').and.returnValue( - { - NewLessonPlayer: { - isEnabled: true - } - } as FeatureStatusChecker - ); + it('should not redirect user to 404 page if flag is enabled', done => { + spyOnProperty(platformFeatureService, 'status', 'get').and.returnValue({ + NewLessonPlayer: { + isEnabled: true, + }, + } as FeatureStatusChecker); const navigateSpy = spyOn(router, 'navigate').and.callThrough(); - guard.canActivate( - new ActivatedRouteSnapshot(), {} as RouterStateSnapshot).then( - (canActivate) => { + guard + .canActivate(new ActivatedRouteSnapshot(), {} as RouterStateSnapshot) + .then(canActivate => { expect(canActivate).toBeTrue(); expect(navigateSpy).not.toHaveBeenCalled(); done(); diff --git a/core/templates/pages/exploration-player-page/new-lesson-player/lesson-player-flag.guard.ts b/core/templates/pages/exploration-player-page/new-lesson-player/lesson-player-flag.guard.ts index d942980ff7a6..6dfd91f0333e 100644 --- a/core/templates/pages/exploration-player-page/new-lesson-player/lesson-player-flag.guard.ts +++ b/core/templates/pages/exploration-player-page/new-lesson-player/lesson-player-flag.guard.ts @@ -17,7 +17,7 @@ * "NewLessonPlayer" flag is enabled by the user. */ -import { Injectable } from '@angular/core'; +import {Injectable} from '@angular/core'; import { ActivatedRouteSnapshot, CanActivate, @@ -25,12 +25,12 @@ import { RouterStateSnapshot, } from '@angular/router'; -import { AppConstants } from 'app.constants'; -import { Location } from '@angular/common'; -import { PlatformFeatureService } from 'services/platform-feature.service'; +import {AppConstants} from 'app.constants'; +import {Location} from '@angular/common'; +import {PlatformFeatureService} from 'services/platform-feature.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class IsNewLessonPlayerGuard implements CanActivate { constructor( @@ -40,18 +40,20 @@ export class IsNewLessonPlayerGuard implements CanActivate { ) {} async canActivate( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot ): Promise { if (this.platformFeatureService.status.NewLessonPlayer.isEnabled) { return true; } - this.router.navigate( - [`${AppConstants.PAGES_REGISTERED_WITH_FRONTEND - .ERROR.ROUTE}/404`]).then(() => { - this.location.replaceState(state.url); - }); + this.router + .navigate([ + `${AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ERROR.ROUTE}/404`, + ]) + .then(() => { + this.location.replaceState(state.url); + }); return false; } } diff --git a/core/templates/pages/exploration-player-page/new-lesson-player/lesson-player-page-root.component.ts b/core/templates/pages/exploration-player-page/new-lesson-player/lesson-player-page-root.component.ts index c08ecd06da58..0aec608b6e72 100644 --- a/core/templates/pages/exploration-player-page/new-lesson-player/lesson-player-page-root.component.ts +++ b/core/templates/pages/exploration-player-page/new-lesson-player/lesson-player-page-root.component.ts @@ -16,11 +16,11 @@ * @fileoverview New lesson player page root component. */ -import { Component, ViewEncapsulation } from '@angular/core'; +import {Component, ViewEncapsulation} from '@angular/core'; @Component({ selector: 'oppia-new-lesson-player-page-root', templateUrl: './lesson-player-page-root.component.html', - encapsulation: ViewEncapsulation.None + encapsulation: ViewEncapsulation.None, }) export class NewLessonPlayerPageRootComponent {} diff --git a/core/templates/pages/exploration-player-page/new-lesson-player/lesson-player-page-routing.module.ts b/core/templates/pages/exploration-player-page/new-lesson-player/lesson-player-page-routing.module.ts index 33c2c0afeb35..302ae3ac60f4 100644 --- a/core/templates/pages/exploration-player-page/new-lesson-player/lesson-player-page-routing.module.ts +++ b/core/templates/pages/exploration-player-page/new-lesson-player/lesson-player-page-routing.module.ts @@ -16,24 +16,19 @@ * @fileoverview Routing module for new lesson player page. */ -import { NgModule } from '@angular/core'; -import { Route, RouterModule } from '@angular/router'; -import { NewLessonPlayerPageRootComponent } from './lesson-player-page-root.component'; +import {NgModule} from '@angular/core'; +import {Route, RouterModule} from '@angular/router'; +import {NewLessonPlayerPageRootComponent} from './lesson-player-page-root.component'; const routes: Route[] = [ { path: '', - component: NewLessonPlayerPageRootComponent - } + component: NewLessonPlayerPageRootComponent, + }, ]; @NgModule({ - imports: [ - RouterModule.forChild(routes) - ], - exports: [ - RouterModule - ] + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], }) - export class NewLessonPlayerPageRoutingModule {} diff --git a/core/templates/pages/exploration-player-page/new-lesson-player/lesson-player-page.component.spec.ts b/core/templates/pages/exploration-player-page/new-lesson-player/lesson-player-page.component.spec.ts index 73b7b6f2d743..6e03a3dc6850 100644 --- a/core/templates/pages/exploration-player-page/new-lesson-player/lesson-player-page.component.spec.ts +++ b/core/templates/pages/exploration-player-page/new-lesson-player/lesson-player-page.component.spec.ts @@ -12,17 +12,26 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; - -import { FetchExplorationBackendResponse, ReadOnlyExplorationBackendApiService } from 'domain/exploration/read-only-exploration-backend-api.service'; -import { ContextService } from 'services/context.service'; -import { MetaTagCustomizationService } from 'services/contextual/meta-tag-customization.service'; -import { KeyboardShortcutService } from 'services/keyboard-shortcut.service'; -import { PageTitleService } from 'services/page-title.service'; -import { NewLessonPlayerPageComponent } from './lesson-player-page.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; + +import { + FetchExplorationBackendResponse, + ReadOnlyExplorationBackendApiService, +} from 'domain/exploration/read-only-exploration-backend-api.service'; +import {ContextService} from 'services/context.service'; +import {MetaTagCustomizationService} from 'services/contextual/meta-tag-customization.service'; +import {KeyboardShortcutService} from 'services/keyboard-shortcut.service'; +import {PageTitleService} from 'services/page-title.service'; +import {NewLessonPlayerPageComponent} from './lesson-player-page.component'; /** * @fileoverview Unit tests for new lesson player page component. @@ -42,23 +51,20 @@ describe('New Lesson Player Page', () => { let keyboardShortcutService: KeyboardShortcutService; let metaTagCustomizationService: MetaTagCustomizationService; let pageTitleService: PageTitleService; - let readOnlyExplorationBackendApiService: - ReadOnlyExplorationBackendApiService; + let readOnlyExplorationBackendApiService: ReadOnlyExplorationBackendApiService; let translateService: TranslateService; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - NewLessonPlayerPageComponent - ], + declarations: [NewLessonPlayerPageComponent], providers: [ { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(NewLessonPlayerPageComponent); @@ -68,7 +74,8 @@ describe('New Lesson Player Page', () => { metaTagCustomizationService = TestBed.inject(MetaTagCustomizationService); pageTitleService = TestBed.inject(PageTitleService); readOnlyExplorationBackendApiService = TestBed.inject( - ReadOnlyExplorationBackendApiService); + ReadOnlyExplorationBackendApiService + ); translateService = TestBed.inject(TranslateService); })); @@ -82,13 +89,16 @@ describe('New Lesson Player Page', () => { exploration: { title: 'Test', objective: 'test objective', - } + }, }; spyOn(contextService, 'getExplorationId').and.returnValue(expId); - spyOn(readOnlyExplorationBackendApiService, 'fetchExplorationAsync') - .and.returnValue(Promise.resolve( - response as FetchExplorationBackendResponse)); + spyOn( + readOnlyExplorationBackendApiService, + 'fetchExplorationAsync' + ).and.returnValue( + Promise.resolve(response as FetchExplorationBackendResponse) + ); spyOn(componentInstance, 'setPageTitle'); spyOn(componentInstance, 'subscribeToOnLangChange'); spyOn(metaTagCustomizationService, 'addOrReplaceMetaTags'); @@ -98,46 +108,52 @@ describe('New Lesson Player Page', () => { tick(); expect(contextService.getExplorationId).toHaveBeenCalled(); - expect(readOnlyExplorationBackendApiService.fetchExplorationAsync) - .toHaveBeenCalledWith(expId, null); + expect( + readOnlyExplorationBackendApiService.fetchExplorationAsync + ).toHaveBeenCalledWith(expId, null); expect(componentInstance.setPageTitle).toHaveBeenCalled(); expect(componentInstance.subscribeToOnLangChange).toHaveBeenCalled(); - expect(metaTagCustomizationService.addOrReplaceMetaTags) - .toHaveBeenCalledWith([ - { - propertyType: 'itemprop', - propertyValue: 'name', - content: response.exploration.title - }, - { - propertyType: 'itemprop', - propertyValue: 'description', - content: response.exploration.objective - }, - { - propertyType: 'property', - propertyValue: 'og:title', - content: response.exploration.title - }, - { - propertyType: 'property', - propertyValue: 'og:description', - content: response.exploration.objective - } - ]); - expect(keyboardShortcutService.bindExplorationPlayerShortcuts) - .toHaveBeenCalled(); + expect( + metaTagCustomizationService.addOrReplaceMetaTags + ).toHaveBeenCalledWith([ + { + propertyType: 'itemprop', + propertyValue: 'name', + content: response.exploration.title, + }, + { + propertyType: 'itemprop', + propertyValue: 'description', + content: response.exploration.objective, + }, + { + propertyType: 'property', + propertyValue: 'og:title', + content: response.exploration.title, + }, + { + propertyType: 'property', + propertyValue: 'og:description', + content: response.exploration.objective, + }, + ]); + expect( + keyboardShortcutService.bindExplorationPlayerShortcuts + ).toHaveBeenCalled(); })); - it('should obtain translated page title whenever the selected' + - 'language changes', () => { - componentInstance.subscribeToOnLangChange(); - spyOn(componentInstance, 'setPageTitle'); - translateService.onLangChange.emit(); + it( + 'should obtain translated page title whenever the selected' + + 'language changes', + () => { + componentInstance.subscribeToOnLangChange(); + spyOn(componentInstance, 'setPageTitle'); + translateService.onLangChange.emit(); - expect(componentInstance.directiveSubscriptions.closed).toBe(false); - expect(componentInstance.setPageTitle).toHaveBeenCalled(); - }); + expect(componentInstance.directiveSubscriptions.closed).toBe(false); + expect(componentInstance.setPageTitle).toHaveBeenCalled(); + } + ); it('should set new page title', () => { spyOn(translateService, 'instant').and.callThrough(); @@ -146,11 +162,14 @@ describe('New Lesson Player Page', () => { componentInstance.setPageTitle(); expect(translateService.instant).toHaveBeenCalledWith( - 'I18N_EXPLORATION_PLAYER_PAGE_TITLE', { - explorationTitle: 'dummy_name' - }); + 'I18N_EXPLORATION_PLAYER_PAGE_TITLE', + { + explorationTitle: 'dummy_name', + } + ); expect(pageTitleService.setDocumentTitle).toHaveBeenCalledWith( - 'I18N_EXPLORATION_PLAYER_PAGE_TITLE'); + 'I18N_EXPLORATION_PLAYER_PAGE_TITLE' + ); }); it('should unsubscribe on component destruction', () => { diff --git a/core/templates/pages/exploration-player-page/new-lesson-player/lesson-player-page.component.ts b/core/templates/pages/exploration-player-page/new-lesson-player/lesson-player-page.component.ts index 5264d157e003..a26fd3f911dc 100644 --- a/core/templates/pages/exploration-player-page/new-lesson-player/lesson-player-page.component.ts +++ b/core/templates/pages/exploration-player-page/new-lesson-player/lesson-player-page.component.ts @@ -16,17 +16,20 @@ * @fileoverview Component for the new lesson player page. */ -import { Component, OnDestroy } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; +import {Component, OnDestroy} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; -import { FetchExplorationBackendResponse, ReadOnlyExplorationBackendApiService } from 'domain/exploration/read-only-exploration-backend-api.service'; -import { ContextService } from 'services/context.service'; -import { MetaTagCustomizationService } from 'services/contextual/meta-tag-customization.service'; -import { UrlService } from 'services/contextual/url.service'; -import { KeyboardShortcutService } from 'services/keyboard-shortcut.service'; -import { PageTitleService } from 'services/page-title.service'; +import { + FetchExplorationBackendResponse, + ReadOnlyExplorationBackendApiService, +} from 'domain/exploration/read-only-exploration-backend-api.service'; +import {ContextService} from 'services/context.service'; +import {MetaTagCustomizationService} from 'services/contextual/meta-tag-customization.service'; +import {UrlService} from 'services/contextual/url.service'; +import {KeyboardShortcutService} from 'services/keyboard-shortcut.service'; +import {PageTitleService} from 'services/page-title.service'; import './lesson-player-page.component.css'; require('interactions/interactionsRequires.ts'); @@ -34,7 +37,7 @@ require('interactions/interactionsRequires.ts'); @Component({ selector: 'oppia-new-lesson-player-page', templateUrl: './lesson-player-page.component.html', - styleUrls: ['./lesson-player-page.component.css'] + styleUrls: ['./lesson-player-page.component.css'], }) export class NewLessonPlayerPageComponent implements OnDestroy { directiveSubscriptions = new Subscription(); @@ -47,50 +50,49 @@ export class NewLessonPlayerPageComponent implements OnDestroy { private keyboardShortcutService: KeyboardShortcutService, private metaTagCustomizationService: MetaTagCustomizationService, private pageTitleService: PageTitleService, - private readOnlyExplorationBackendApiService: - ReadOnlyExplorationBackendApiService, + private readOnlyExplorationBackendApiService: ReadOnlyExplorationBackendApiService, private urlService: UrlService, private translateService: TranslateService ) {} ngOnInit(): void { let explorationId = this.contextService.getExplorationId(); - this.readOnlyExplorationBackendApiService.fetchExplorationAsync( - explorationId, null - ).then((response: FetchExplorationBackendResponse) => { - this.explorationTitle = response.exploration.title; - // The onLangChange event is initially fired before the exploration is - // loaded. Hence the first setpageTitle() call needs to made - // manually, and the onLangChange subscription is added after - // the exploration is fetch from the backend. - this.setPageTitle(); - this.subscribeToOnLangChange(); - this.metaTagCustomizationService.addOrReplaceMetaTags([ - { - propertyType: 'itemprop', - propertyValue: 'name', - content: response.exploration.title - }, - { - propertyType: 'itemprop', - propertyValue: 'description', - content: response.exploration.objective - }, - { - propertyType: 'property', - propertyValue: 'og:title', - content: response.exploration.title - }, - { - propertyType: 'property', - propertyValue: 'og:description', - content: response.exploration.objective - } - ]); - }).finally(() => { - this.isLoadingExploration = false; - } - ); + this.readOnlyExplorationBackendApiService + .fetchExplorationAsync(explorationId, null) + .then((response: FetchExplorationBackendResponse) => { + this.explorationTitle = response.exploration.title; + // The onLangChange event is initially fired before the exploration is + // loaded. Hence the first setpageTitle() call needs to made + // manually, and the onLangChange subscription is added after + // the exploration is fetch from the backend. + this.setPageTitle(); + this.subscribeToOnLangChange(); + this.metaTagCustomizationService.addOrReplaceMetaTags([ + { + propertyType: 'itemprop', + propertyValue: 'name', + content: response.exploration.title, + }, + { + propertyType: 'itemprop', + propertyValue: 'description', + content: response.exploration.objective, + }, + { + propertyType: 'property', + propertyValue: 'og:title', + content: response.exploration.title, + }, + { + propertyType: 'property', + propertyValue: 'og:description', + content: response.exploration.objective, + }, + ]); + }) + .finally(() => { + this.isLoadingExploration = false; + }); this.pageIsIframed = this.urlService.isIframed(); this.keyboardShortcutService.bindExplorationPlayerShortcuts(); @@ -106,9 +108,11 @@ export class NewLessonPlayerPageComponent implements OnDestroy { setPageTitle(): void { let translatedTitle = this.translateService.instant( - 'I18N_EXPLORATION_PLAYER_PAGE_TITLE', { - explorationTitle: this.explorationTitle - }); + 'I18N_EXPLORATION_PLAYER_PAGE_TITLE', + { + explorationTitle: this.explorationTitle, + } + ); this.pageTitleService.setDocumentTitle(translatedTitle); } @@ -117,8 +121,9 @@ export class NewLessonPlayerPageComponent implements OnDestroy { } } -angular.module('oppia').directive('oppiaNewLessonPlayerPage', +angular.module('oppia').directive( + 'oppiaNewLessonPlayerPage', downgradeComponent({ - component: NewLessonPlayerPageComponent + component: NewLessonPlayerPageComponent, }) as angular.IDirectiveFactory ); diff --git a/core/templates/pages/exploration-player-page/new-lesson-player/lesson-player-page.constants.ts b/core/templates/pages/exploration-player-page/new-lesson-player/lesson-player-page.constants.ts index 474ec85cb30a..13475f670418 100644 --- a/core/templates/pages/exploration-player-page/new-lesson-player/lesson-player-page.constants.ts +++ b/core/templates/pages/exploration-player-page/new-lesson-player/lesson-player-page.constants.ts @@ -57,13 +57,14 @@ export const NewLessonPlayerConstants = { HINT_REQUEST_STRING_I18N_IDS: [ 'I18N_PLAYER_HINT_REQUEST_STRING_1', 'I18N_PLAYER_HINT_REQUEST_STRING_2', - 'I18N_PLAYER_HINT_REQUEST_STRING_3'], + 'I18N_PLAYER_HINT_REQUEST_STRING_3', + ], // Array of i18n IDs for nudging the learner towards checking the spelling. I18N_ANSWER_MISSPELLED_RESPONSE_TEXT_IDS: [ 'I18N_ANSWER_MISSPELLED_RESPONSE_TEXT_0', 'I18N_ANSWER_MISSPELLED_RESPONSE_TEXT_1', - 'I18N_ANSWER_MISSPELLED_RESPONSE_TEXT_2' + 'I18N_ANSWER_MISSPELLED_RESPONSE_TEXT_2', ], // Threshold value of edit distance for judging an answer as a misspelling. @@ -107,20 +108,20 @@ export const NewLessonPlayerConstants = { STATS_REPORTING_URLS: { ANSWER_SUBMITTED: '/explorehandler/answer_submitted_event/', - EXPLORATION_COMPLETED: ( - '/explorehandler/exploration_complete_event/'), - EXPLORATION_MAYBE_LEFT: ( - '/explorehandler/exploration_maybe_leave_event/'), - EXPLORATION_STARTED: ( - '/explorehandler/exploration_start_event/'), + EXPLORATION_COMPLETED: + '/explorehandler/exploration_complete_event/', + EXPLORATION_MAYBE_LEFT: + '/explorehandler/exploration_maybe_leave_event/', + EXPLORATION_STARTED: + '/explorehandler/exploration_start_event/', STATE_HIT: '/explorehandler/state_hit_event/', STATE_COMPLETED: '/explorehandler/state_complete_event/', - EXPLORATION_ACTUALLY_STARTED: ( - '/explorehandler/exploration_actual_start_event/'), + EXPLORATION_ACTUALLY_STARTED: + '/explorehandler/exploration_actual_start_event/', SOLUTION_HIT: '/explorehandler/solution_hit_event/', - LEAVE_FOR_REFRESHER_EXP: ( - '/explorehandler/leave_for_refresher_exp_event/'), - STATS_EVENTS: '/explorehandler/stats_events/' + LEAVE_FOR_REFRESHER_EXP: + '/explorehandler/leave_for_refresher_exp_event/', + STATS_EVENTS: '/explorehandler/stats_events/', }, FEEDBACK_POPOVER_PATH: diff --git a/core/templates/pages/exploration-player-page/new-lesson-player/lesson-player-page.module.ts b/core/templates/pages/exploration-player-page/new-lesson-player/lesson-player-page.module.ts index ff0ca13ea7f3..1f7f8a345e2b 100644 --- a/core/templates/pages/exploration-player-page/new-lesson-player/lesson-player-page.module.ts +++ b/core/templates/pages/exploration-player-page/new-lesson-player/lesson-player-page.module.ts @@ -16,30 +16,30 @@ * @fileoverview Module for the new lesson player page. */ -import { NgModule } from '@angular/core'; -import { NgbModalModule, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'; -import { CommonModule } from '@angular/common'; -import { ExplorationPlayerViewerCommonModule } from '../exploration-player-viewer-common.module'; -import { ExplorationPlayerPageModule } from '../exploration-player-page.module'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { InteractionExtensionsModule } from 'interactions/interactions.module'; -import { MatButtonModule } from '@angular/material/button'; -import { MaterialModule } from 'modules/material.module'; -import { NewLessonPlayerPageComponent } from './lesson-player-page.component'; -import { NewLessonPlayerPageRoutingModule } from './lesson-player-page-routing.module'; -import { NewLessonPlayerPageRootComponent } from './lesson-player-page-root.component'; -import { HintAndSolutionModalService } from '../services/hint-and-solution-modal.service'; -import { FatigueDetectionService } from '../services/fatigue-detection.service'; +import {NgModule} from '@angular/core'; +import {NgbModalModule, NgbPopoverModule} from '@ng-bootstrap/ng-bootstrap'; +import {CommonModule} from '@angular/common'; +import {ExplorationPlayerViewerCommonModule} from '../exploration-player-viewer-common.module'; +import {ExplorationPlayerPageModule} from '../exploration-player-page.module'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {InteractionExtensionsModule} from 'interactions/interactions.module'; +import {MatButtonModule} from '@angular/material/button'; +import {MaterialModule} from 'modules/material.module'; +import {NewLessonPlayerPageComponent} from './lesson-player-page.component'; +import {NewLessonPlayerPageRoutingModule} from './lesson-player-page-routing.module'; +import {NewLessonPlayerPageRootComponent} from './lesson-player-page-root.component'; +import {HintAndSolutionModalService} from '../services/hint-and-solution-modal.service'; +import {FatigueDetectionService} from '../services/fatigue-detection.service'; import 'third-party-imports/guppy.import'; import 'third-party-imports/midi-js.import'; import 'third-party-imports/skulpt.import'; -import { ToastrModule } from 'ngx-toastr'; -import { toastrConfig } from 'pages/oppia-root/app.module'; -import { PlayerHeaderComponent } from './new-lesson-player-components/player-header.component'; -import { PlayerSidebarComponent } from './new-lesson-player-components/player-sidebar.component'; -import { PlayerFooterComponent } from './new-lesson-player-components/player-footer.component'; -import { NewAudioBarComponent } from './new-lesson-player-components/new-audio-bar.component'; +import {ToastrModule} from 'ngx-toastr'; +import {toastrConfig} from 'pages/oppia-root/app.module'; +import {PlayerHeaderComponent} from './new-lesson-player-components/player-header.component'; +import {PlayerSidebarComponent} from './new-lesson-player-components/player-sidebar.component'; +import {PlayerFooterComponent} from './new-lesson-player-components/player-footer.component'; +import {NewAudioBarComponent} from './new-lesson-player-components/new-audio-bar.component'; @NgModule({ imports: [ @@ -72,9 +72,6 @@ import { NewAudioBarComponent } from './new-lesson-player-components/new-audio-b PlayerFooterComponent, NewAudioBarComponent, ], - providers: [ - HintAndSolutionModalService, - FatigueDetectionService, - ] + providers: [HintAndSolutionModalService, FatigueDetectionService], }) export class NewLessonPlayerPageModule {} diff --git a/core/templates/pages/exploration-player-page/new-lesson-player/new-lesson-player-components/new-audio-bar.component.ts b/core/templates/pages/exploration-player-page/new-lesson-player/new-lesson-player-components/new-audio-bar.component.ts index 0b13dc229445..edb9b181b881 100644 --- a/core/templates/pages/exploration-player-page/new-lesson-player/new-lesson-player-components/new-audio-bar.component.ts +++ b/core/templates/pages/exploration-player-page/new-lesson-player/new-lesson-player-components/new-audio-bar.component.ts @@ -16,18 +16,20 @@ * @fileoverview Component for the new audio bar */ -import { Component } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; import './new-audio-bar.component.css'; @Component({ selector: 'oppia-new-audio-bar', templateUrl: './new-audio-bar.component.html', - styleUrls: ['./new-audio-bar.component.css'] + styleUrls: ['./new-audio-bar.component.css'], }) -export class NewAudioBarComponent { } +export class NewAudioBarComponent {} -angular.module('oppia').directive('oppiaNewAudioBar', +angular.module('oppia').directive( + 'oppiaNewAudioBar', downgradeComponent({ - component: NewAudioBarComponent - }) as angular.IDirectiveFactory); + component: NewAudioBarComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-player-page/new-lesson-player/new-lesson-player-components/player-footer.component.ts b/core/templates/pages/exploration-player-page/new-lesson-player/new-lesson-player-components/player-footer.component.ts index 0304e7a46e74..bdd70f5ea0b1 100644 --- a/core/templates/pages/exploration-player-page/new-lesson-player/new-lesson-player-components/player-footer.component.ts +++ b/core/templates/pages/exploration-player-page/new-lesson-player/new-lesson-player-components/player-footer.component.ts @@ -16,18 +16,20 @@ * @fileoverview Component for the new lesson player footer */ -import { Component } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; import './player-footer.component.css'; @Component({ selector: 'oppia-new-lesson-player-footer', templateUrl: './player-footer.component.html', - styleUrls: ['./player-footer.component.css'] + styleUrls: ['./player-footer.component.css'], }) -export class PlayerFooterComponent { } +export class PlayerFooterComponent {} -angular.module('oppia').directive('oppiaPlayerFooter', +angular.module('oppia').directive( + 'oppiaPlayerFooter', downgradeComponent({ - component: PlayerFooterComponent - }) as angular.IDirectiveFactory); + component: PlayerFooterComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-player-page/new-lesson-player/new-lesson-player-components/player-header.component.spec.ts b/core/templates/pages/exploration-player-page/new-lesson-player/new-lesson-player-components/player-header.component.spec.ts index 5e4c13a4d246..d0f5a2b9868b 100644 --- a/core/templates/pages/exploration-player-page/new-lesson-player/new-lesson-player-components/player-header.component.spec.ts +++ b/core/templates/pages/exploration-player-page/new-lesson-player/new-lesson-player-components/player-header.component.spec.ts @@ -16,28 +16,39 @@ * @fileoverview Unit tests for new lesson player header component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { FetchExplorationBackendResponse, ReadOnlyExplorationBackendApiService } from 'domain/exploration/read-only-exploration-backend-api.service'; -import { ReadOnlyTopicBackendDict, ReadOnlyTopicObjectFactory } from 'domain/topic_viewer/read-only-topic-object.factory'; -import { TopicViewerBackendApiService } from 'domain/topic_viewer/topic-viewer-backend-api.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { ContextService } from 'services/context.service'; -import { UrlService } from 'services/contextual/url.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { StatsReportingService } from '../../services/stats-reporting.service'; -import { PlayerHeaderComponent } from './player-header.component'; -import { MobileMenuService } from '../new-lesson-player-services/mobile-menu.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import { + FetchExplorationBackendResponse, + ReadOnlyExplorationBackendApiService, +} from 'domain/exploration/read-only-exploration-backend-api.service'; +import { + ReadOnlyTopicBackendDict, + ReadOnlyTopicObjectFactory, +} from 'domain/topic_viewer/read-only-topic-object.factory'; +import {TopicViewerBackendApiService} from 'domain/topic_viewer/topic-viewer-backend-api.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {ContextService} from 'services/context.service'; +import {UrlService} from 'services/contextual/url.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {StatsReportingService} from '../../services/stats-reporting.service'; +import {PlayerHeaderComponent} from './player-header.component'; +import {MobileMenuService} from '../new-lesson-player-services/mobile-menu.service'; describe('Lesson player header component', () => { let fixture: ComponentFixture; let componentInstance: PlayerHeaderComponent; let contextService: ContextService; - let readOnlyExplorationBackendApiService: - ReadOnlyExplorationBackendApiService; + let readOnlyExplorationBackendApiService: ReadOnlyExplorationBackendApiService; let siteAnalyticsService: SiteAnalyticsService; let statsReportingService: StatsReportingService; let urlInterpolationService: UrlInterpolationService; @@ -49,13 +60,8 @@ describe('Lesson player header component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - PlayerHeaderComponent, - MockTranslatePipe - ], + imports: [HttpClientTestingModule], + declarations: [PlayerHeaderComponent, MockTranslatePipe], providers: [ ContextService, ReadOnlyExplorationBackendApiService, @@ -67,7 +73,7 @@ describe('Lesson player header component', () => { ReadOnlyTopicObjectFactory, MobileMenuService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -76,15 +82,15 @@ describe('Lesson player header component', () => { componentInstance = fixture.componentInstance; contextService = TestBed.inject(ContextService); readOnlyExplorationBackendApiService = TestBed.inject( - ReadOnlyExplorationBackendApiService); + ReadOnlyExplorationBackendApiService + ); siteAnalyticsService = TestBed.inject(SiteAnalyticsService); statsReportingService = TestBed.inject(StatsReportingService); urlInterpolationService = TestBed.inject(UrlInterpolationService); urlService = TestBed.inject(UrlService); i18nLanguageCodeService = TestBed.inject(I18nLanguageCodeService); readOnlyTopicObjectFactory = TestBed.inject(ReadOnlyTopicObjectFactory); - topicViewerBackendApiService = TestBed.inject( - TopicViewerBackendApiService); + topicViewerBackendApiService = TestBed.inject(TopicViewerBackendApiService); mobileMenuService = TestBed.inject(MobileMenuService); spyOn(topicViewerBackendApiService, 'fetchTopicDataAsync').and.resolveTo( @@ -105,7 +111,8 @@ describe('Lesson player header component', () => { ); spyOn(i18nLanguageCodeService, 'isCurrentLanguageRTL').and.returnValue( - true); + true + ); }); afterEach(() => { @@ -119,17 +126,22 @@ describe('Lesson player header component', () => { spyOn(urlService, 'getPathname').and.returnValue('/explore/'); spyOn(contextService, 'getExplorationId').and.returnValue(explorationId); - spyOn(readOnlyExplorationBackendApiService, 'fetchExplorationAsync') - .and.returnValue(Promise.resolve({ + spyOn( + readOnlyExplorationBackendApiService, + 'fetchExplorationAsync' + ).and.returnValue( + Promise.resolve({ exploration: { - title: explorationTitle - } - } as FetchExplorationBackendResponse)); + title: explorationTitle, + }, + } as FetchExplorationBackendResponse) + ); spyOn(urlService, 'getExplorationVersionFromUrl').and.returnValue(1); spyOn(componentInstance, 'getTopicUrl').and.returnValue(topicUrl); spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue(''); - spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl') - .and.returnValue(''); + spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl').and.returnValue( + '' + ); spyOn(statsReportingService, 'setTopicName'); spyOn(siteAnalyticsService, 'registerCuratedLessonStarted'); @@ -139,15 +151,17 @@ describe('Lesson player header component', () => { expect(urlService.getPathname).toHaveBeenCalled(); expect(contextService.getExplorationId).toHaveBeenCalled(); - expect(readOnlyExplorationBackendApiService.fetchExplorationAsync) - .toHaveBeenCalled(); + expect( + readOnlyExplorationBackendApiService.fetchExplorationAsync + ).toHaveBeenCalled(); expect(urlService.getExplorationVersionFromUrl).toHaveBeenCalled(); expect(componentInstance.getTopicUrl).toHaveBeenCalled(); expect(urlService.getTopicUrlFragmentFromLearnerUrl).toHaveBeenCalled(); expect(topicViewerBackendApiService.fetchTopicDataAsync).toHaveBeenCalled(); expect(statsReportingService.setTopicName).toHaveBeenCalled(); - expect(siteAnalyticsService.registerCuratedLessonStarted) - .toHaveBeenCalled(); + expect( + siteAnalyticsService.registerCuratedLessonStarted + ).toHaveBeenCalled(); })); it('should register community lesson start event', fakeAsync(() => { @@ -156,17 +170,22 @@ describe('Lesson player header component', () => { spyOn(urlService, 'getPathname').and.returnValue('/explore/'); spyOn(contextService, 'getExplorationId').and.returnValue(explorationId); - spyOn(readOnlyExplorationBackendApiService, 'fetchExplorationAsync') - .and.returnValue(Promise.resolve({ + spyOn( + readOnlyExplorationBackendApiService, + 'fetchExplorationAsync' + ).and.returnValue( + Promise.resolve({ exploration: { - title: explorationTitle - } - } as FetchExplorationBackendResponse)); + title: explorationTitle, + }, + } as FetchExplorationBackendResponse) + ); spyOn(urlService, 'getExplorationVersionFromUrl').and.returnValue(1); spyOn(componentInstance, 'getTopicUrl').and.returnValue(''); spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue(''); - spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl') - .and.returnValue(''); + spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl').and.returnValue( + '' + ); spyOn(statsReportingService, 'setTopicName'); spyOn(siteAnalyticsService, 'registerCommunityLessonStarted'); @@ -174,54 +193,68 @@ describe('Lesson player header component', () => { tick(); tick(); - expect(siteAnalyticsService.registerCommunityLessonStarted) - .toHaveBeenCalled(); + expect( + siteAnalyticsService.registerCommunityLessonStarted + ).toHaveBeenCalled(); })); it('should get topic url from fragment correctly', () => { let topicUrl = 'topic_url'; spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue( - 'topic_url_fragment'); + 'topic_url_fragment' + ); spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl').and.returnValue( - 'classroom_url_fragment'); + 'classroom_url_fragment' + ); spyOn(urlInterpolationService, 'interpolateUrl').and.returnValue(topicUrl); expect(componentInstance.getTopicUrl()).toEqual(topicUrl); }); - it('should set topic name and subtopic title translation key and ' + - 'check whether hacky translations are displayed or not correctly', - waitForAsync(() => { - spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl') - .and.returnValue('topic_url_fragment'); - spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl') - .and.returnValue('classroom_url_fragment'); - spyOn(componentInstance, 'getTopicUrl').and.returnValue('topic_url'); + it( + 'should set topic name and subtopic title translation key and ' + + 'check whether hacky translations are displayed or not correctly', + waitForAsync(() => { + spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue( + 'topic_url_fragment' + ); + spyOn( + urlService, + 'getClassroomUrlFragmentFromLearnerUrl' + ).and.returnValue('classroom_url_fragment'); + spyOn(componentInstance, 'getTopicUrl').and.returnValue('topic_url'); - componentInstance.ngOnInit(); - fixture.whenStable().then(() => { - fixture.detectChanges(); - - expect(componentInstance.topicNameTranslationKey) - .toBe('I18N_TOPIC_topic1_TITLE'); - expect(componentInstance.explorationTitleTranslationKey) - .toBe('I18N_EXPLORATION_test_id_TITLE'); - - spyOn(i18nLanguageCodeService, 'isHackyTranslationAvailable') - .and.returnValues(true, false); - spyOn(i18nLanguageCodeService, 'isCurrentLanguageEnglish') - .and.returnValues(false, false); - - let hackyTopicNameTranslationIsDisplayed = - componentInstance.isHackyTopicNameTranslationDisplayed(); - expect(hackyTopicNameTranslationIsDisplayed).toBe(true); - - let hackyExpTitleTranslationIsDisplayed = - componentInstance.isHackyExpTitleTranslationDisplayed(); - expect(hackyExpTitleTranslationIsDisplayed).toBe(false); - }); - })); + componentInstance.ngOnInit(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + + expect(componentInstance.topicNameTranslationKey).toBe( + 'I18N_TOPIC_topic1_TITLE' + ); + expect(componentInstance.explorationTitleTranslationKey).toBe( + 'I18N_EXPLORATION_test_id_TITLE' + ); + + spyOn( + i18nLanguageCodeService, + 'isHackyTranslationAvailable' + ).and.returnValues(true, false); + spyOn( + i18nLanguageCodeService, + 'isCurrentLanguageEnglish' + ).and.returnValues(false, false); + + let hackyTopicNameTranslationIsDisplayed = + componentInstance.isHackyTopicNameTranslationDisplayed(); + expect(hackyTopicNameTranslationIsDisplayed).toBe(true); + + let hackyExpTitleTranslationIsDisplayed = + componentInstance.isHackyExpTitleTranslationDisplayed(); + expect(hackyExpTitleTranslationIsDisplayed).toBe(false); + }); + }) + ); it('should toggle menu on mobile', () => { spyOn(mobileMenuService, 'toggleMenuVisibility'); diff --git a/core/templates/pages/exploration-player-page/new-lesson-player/new-lesson-player-components/player-header.component.ts b/core/templates/pages/exploration-player-page/new-lesson-player/new-lesson-player-components/player-header.component.ts index b69120836dce..91d09e3e8188 100644 --- a/core/templates/pages/exploration-player-page/new-lesson-player/new-lesson-player-components/player-header.component.ts +++ b/core/templates/pages/exploration-player-page/new-lesson-player/new-lesson-player-components/player-header.component.ts @@ -16,30 +16,32 @@ * @fileoverview Component for the new lesson player header */ -import { Component } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { ClassroomDomainConstants } from 'domain/classroom/classroom-domain.constants'; -import { ReadOnlyExplorationBackendApiService } from 'domain/exploration/read-only-exploration-backend-api.service'; -import { StoryPlaythrough } from 'domain/story_viewer/story-playthrough.model'; -import { LearnerExplorationSummaryBackendDict } from 'domain/summary/learner-exploration-summary.model'; -import { ReadOnlyTopic } from 'domain/topic_viewer/read-only-topic-object.factory'; -import { TopicViewerBackendApiService } from 'domain/topic_viewer/topic-viewer-backend-api.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { Subscription } from 'rxjs'; -import { ContextService } from 'services/context.service'; -import { UrlService } from 'services/contextual/url.service'; -import { I18nLanguageCodeService, TranslationKeyType } from 'services/i18n-language-code.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { StatsReportingService } from '../../services/stats-reporting.service'; -import { MobileMenuService } from '../new-lesson-player-services/mobile-menu.service'; +import {Component} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {ClassroomDomainConstants} from 'domain/classroom/classroom-domain.constants'; +import {ReadOnlyExplorationBackendApiService} from 'domain/exploration/read-only-exploration-backend-api.service'; +import {StoryPlaythrough} from 'domain/story_viewer/story-playthrough.model'; +import {LearnerExplorationSummaryBackendDict} from 'domain/summary/learner-exploration-summary.model'; +import {ReadOnlyTopic} from 'domain/topic_viewer/read-only-topic-object.factory'; +import {TopicViewerBackendApiService} from 'domain/topic_viewer/topic-viewer-backend-api.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {Subscription} from 'rxjs'; +import {ContextService} from 'services/context.service'; +import {UrlService} from 'services/contextual/url.service'; +import { + I18nLanguageCodeService, + TranslationKeyType, +} from 'services/i18n-language-code.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {StatsReportingService} from '../../services/stats-reporting.service'; +import {MobileMenuService} from '../new-lesson-player-services/mobile-menu.service'; import './player-header.component.css'; - @Component({ selector: 'oppia-player-header', templateUrl: './player-header.component.html', - styleUrls: ['./player-header.component.css'] + styleUrls: ['./player-header.component.css'], }) export class PlayerHeaderComponent { // These properties are initialized using Angular lifecycle hooks @@ -58,8 +60,7 @@ export class PlayerHeaderComponent { constructor( private contextService: ContextService, - private readOnlyExplorationBackendApiService: - ReadOnlyExplorationBackendApiService, + private readOnlyExplorationBackendApiService: ReadOnlyExplorationBackendApiService, private siteAnalyticsService: SiteAnalyticsService, private statsReportingService: StatsReportingService, private urlInterpolationService: UrlInterpolationService, @@ -74,59 +75,64 @@ export class PlayerHeaderComponent { let explorationContext = false; for (let i = 0; i < pathnameArray.length; i++) { - if (pathnameArray[i] === 'explore' || - pathnameArray[i] === 'create' || - pathnameArray[i] === 'skill_editor' || - pathnameArray[i] === 'embed' || - pathnameArray[i] === 'lesson') { + if ( + pathnameArray[i] === 'explore' || + pathnameArray[i] === 'create' || + pathnameArray[i] === 'skill_editor' || + pathnameArray[i] === 'embed' || + pathnameArray[i] === 'lesson' + ) { explorationContext = true; break; } } - this.explorationId = explorationContext ? - this.contextService.getExplorationId() : 'test_id'; + this.explorationId = explorationContext + ? this.contextService.getExplorationId() + : 'test_id'; this.explorationTitle = 'Loading...'; - this.readOnlyExplorationBackendApiService.fetchExplorationAsync( - this.explorationId, - this.urlService.getExplorationVersionFromUrl(), - this.urlService.getPidFromUrl()) - .then((response) => { + this.readOnlyExplorationBackendApiService + .fetchExplorationAsync( + this.explorationId, + this.urlService.getExplorationVersionFromUrl(), + this.urlService.getPidFromUrl() + ) + .then(response => { this.explorationTitle = response.exploration.title; }); - this.explorationTitleTranslationKey = ( + this.explorationTitleTranslationKey = this.i18nLanguageCodeService.getExplorationTranslationKey( this.explorationId, TranslationKeyType.TITLE - ) - ); + ); // To check if the exploration is linked to the topic or not. this.isLinkedToTopic = this.getTopicUrl() ? true : false; // If linked to topic then print topic name in the lesson player. if (this.isLinkedToTopic) { - let topicUrlFragment = ( - this.urlService.getTopicUrlFragmentFromLearnerUrl()); - let classroomUrlFragment = ( - this.urlService.getClassroomUrlFragmentFromLearnerUrl()); - this.topicViewerBackendApiService.fetchTopicDataAsync( - topicUrlFragment, classroomUrlFragment).then( - (readOnlyTopic: ReadOnlyTopic) => { + let topicUrlFragment = + this.urlService.getTopicUrlFragmentFromLearnerUrl(); + let classroomUrlFragment = + this.urlService.getClassroomUrlFragmentFromLearnerUrl(); + this.topicViewerBackendApiService + .fetchTopicDataAsync(topicUrlFragment, classroomUrlFragment) + .then((readOnlyTopic: ReadOnlyTopic) => { this.topicName = readOnlyTopic.getTopicName(); this.statsReportingService.setTopicName(this.topicName); this.siteAnalyticsService.registerCuratedLessonStarted( - this.topicName, this.explorationId); - this.topicNameTranslationKey = ( + this.topicName, + this.explorationId + ); + this.topicNameTranslationKey = this.i18nLanguageCodeService.getTopicTranslationKey( readOnlyTopic.getTopicId(), TranslationKeyType.TITLE - ) - ); - } - ); + ); + }); } else { this.siteAnalyticsService.registerCommunityLessonStarted( - this.explorationId); + this.explorationId + ); } } @@ -137,19 +143,22 @@ export class PlayerHeaderComponent { let classroomUrlFragment: string | null = null; try { - topicUrlFragment = ( - this.urlService.getTopicUrlFragmentFromLearnerUrl()); - classroomUrlFragment = ( - this.urlService.getClassroomUrlFragmentFromLearnerUrl()); + topicUrlFragment = this.urlService.getTopicUrlFragmentFromLearnerUrl(); + classroomUrlFragment = + this.urlService.getClassroomUrlFragmentFromLearnerUrl(); } catch (e) {} - return topicUrlFragment && + return ( + topicUrlFragment && classroomUrlFragment && this.urlInterpolationService.interpolateUrl( - ClassroomDomainConstants.TOPIC_VIEWER_STORY_URL_TEMPLATE, { + ClassroomDomainConstants.TOPIC_VIEWER_STORY_URL_TEMPLATE, + { topic_url_fragment: topicUrlFragment, classroom_url_fragment: classroomUrlFragment, - }); + } + ) + ); } isHackyTopicNameTranslationDisplayed(): boolean { @@ -177,7 +186,9 @@ export class PlayerHeaderComponent { } } -angular.module('oppia').directive('oppiaPlayerHeader', +angular.module('oppia').directive( + 'oppiaPlayerHeader', downgradeComponent({ - component: PlayerHeaderComponent - }) as angular.IDirectiveFactory); + component: PlayerHeaderComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-player-page/new-lesson-player/new-lesson-player-components/player-sidebar.component.spec.ts b/core/templates/pages/exploration-player-page/new-lesson-player/new-lesson-player-components/player-sidebar.component.spec.ts index 135e4b98c9f8..5fbf3c8713ef 100644 --- a/core/templates/pages/exploration-player-page/new-lesson-player/new-lesson-player-components/player-sidebar.component.spec.ts +++ b/core/templates/pages/exploration-player-page/new-lesson-player/new-lesson-player-components/player-sidebar.component.spec.ts @@ -16,21 +16,29 @@ * @fileoverview Unit tests for new lesson player sidebar component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, TestBed, fakeAsync, tick, waitForAsync } from '@angular/core/testing'; -import { PlayerSidebarComponent } from './player-sidebar.component'; -import { NO_ERRORS_SCHEMA, Pipe } from '@angular/core'; -import { MobileMenuService } from '../new-lesson-player-services/mobile-menu.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import { + ComponentFixture, + TestBed, + fakeAsync, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {PlayerSidebarComponent} from './player-sidebar.component'; +import {NO_ERRORS_SCHEMA, Pipe} from '@angular/core'; +import {MobileMenuService} from '../new-lesson-player-services/mobile-menu.service'; import './player-sidebar.component.css'; -import { I18nLanguageCodeService } from - 'services/i18n-language-code.service'; -import { FetchExplorationBackendResponse, ReadOnlyExplorationBackendApiService } from 'domain/exploration/read-only-exploration-backend-api.service'; -import { UrlService } from 'services/contextual/url.service'; -import { BehaviorSubject } from 'rxjs'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { TranslateService } from '@ngx-translate/core'; -import { MockTranslateService } from 'components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec'; -import { ContextService } from 'services/context.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import { + FetchExplorationBackendResponse, + ReadOnlyExplorationBackendApiService, +} from 'domain/exploration/read-only-exploration-backend-api.service'; +import {UrlService} from 'services/contextual/url.service'; +import {BehaviorSubject} from 'rxjs'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {TranslateService} from '@ngx-translate/core'; +import {MockTranslateService} from 'components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec'; +import {ContextService} from 'services/context.service'; @Pipe({name: 'truncateAndCapitalize'}) class MockTruncteAndCapitalizePipe { @@ -45,8 +53,7 @@ describe('PlayerSidebarComponent', () => { let mockMobileMenuService: Partial; let contextService: ContextService; let i18nLanguageCodeService: I18nLanguageCodeService; - let readOnlyExplorationBackendApiService: - ReadOnlyExplorationBackendApiService; + let readOnlyExplorationBackendApiService: ReadOnlyExplorationBackendApiService; let urlService: UrlService; beforeEach(waitForAsync(() => { @@ -69,14 +76,14 @@ describe('PlayerSidebarComponent', () => { UrlService, { provide: MobileMenuService, - useValue: mockMobileMenuService + useValue: mockMobileMenuService, }, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -85,7 +92,8 @@ describe('PlayerSidebarComponent', () => { component = fixture.componentInstance; contextService = TestBed.inject(ContextService); readOnlyExplorationBackendApiService = TestBed.inject( - ReadOnlyExplorationBackendApiService); + ReadOnlyExplorationBackendApiService + ); urlService = TestBed.inject(UrlService); i18nLanguageCodeService = TestBed.inject(I18nLanguageCodeService); }); @@ -97,13 +105,17 @@ describe('PlayerSidebarComponent', () => { spyOn(urlService, 'getPathname').and.returnValue('/lesson/'); spyOn(contextService, 'getExplorationId').and.returnValue(explorationId); - spyOn(readOnlyExplorationBackendApiService, 'fetchExplorationAsync') - .and.returnValue(Promise.resolve({ + spyOn( + readOnlyExplorationBackendApiService, + 'fetchExplorationAsync' + ).and.returnValue( + Promise.resolve({ exploration: { title: explorationTitle, - objective: explorationObjective - } - } as FetchExplorationBackendResponse)); + objective: explorationObjective, + }, + } as FetchExplorationBackendResponse) + ); spyOn(urlService, 'getExplorationVersionFromUrl').and.returnValue(1); spyOn(urlService, 'getPidFromUrl').and.returnValue(''); spyOn(i18nLanguageCodeService, 'getExplorationTranslationKey'); @@ -116,10 +128,12 @@ describe('PlayerSidebarComponent', () => { expect(urlService.getExplorationVersionFromUrl).toHaveBeenCalled(); expect(urlService.getPidFromUrl).toHaveBeenCalled(); expect(contextService.getExplorationId).toHaveBeenCalled(); - expect(readOnlyExplorationBackendApiService.fetchExplorationAsync) - .toHaveBeenCalled(); - expect(i18nLanguageCodeService.getExplorationTranslationKey) - .toHaveBeenCalled(); + expect( + readOnlyExplorationBackendApiService.fetchExplorationAsync + ).toHaveBeenCalled(); + expect( + i18nLanguageCodeService.getExplorationTranslationKey + ).toHaveBeenCalled(); })); it('should toggle sidebar', () => { @@ -133,8 +147,8 @@ describe('PlayerSidebarComponent', () => { it('should check if hacky exp desc translation is displayed', () => { // Translation is only displayed if the language is not English // and its hacky translation is available. - let hackyExpDescTranslationIsDisplayed = ( - component.isHackyExpDescTranslationDisplayed()); + let hackyExpDescTranslationIsDisplayed = + component.isHackyExpDescTranslationDisplayed(); expect(hackyExpDescTranslationIsDisplayed).toBe(false); }); }); diff --git a/core/templates/pages/exploration-player-page/new-lesson-player/new-lesson-player-components/player-sidebar.component.ts b/core/templates/pages/exploration-player-page/new-lesson-player/new-lesson-player-components/player-sidebar.component.ts index f94159b1bc50..5fb479a9bd8d 100644 --- a/core/templates/pages/exploration-player-page/new-lesson-player/new-lesson-player-components/player-sidebar.component.ts +++ b/core/templates/pages/exploration-player-page/new-lesson-player/new-lesson-player-components/player-sidebar.component.ts @@ -16,15 +16,17 @@ * @fileoverview Component for the new lesson player sidebar */ -import { Component, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { MobileMenuService } from '../new-lesson-player-services/mobile-menu.service'; +import {Component, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {MobileMenuService} from '../new-lesson-player-services/mobile-menu.service'; import './player-sidebar.component.css'; -import { ContextService } from 'services/context.service'; -import { I18nLanguageCodeService, TranslationKeyType } from - 'services/i18n-language-code.service'; -import { ReadOnlyExplorationBackendApiService } from 'domain/exploration/read-only-exploration-backend-api.service'; -import { UrlService } from 'services/contextual/url.service'; +import {ContextService} from 'services/context.service'; +import { + I18nLanguageCodeService, + TranslationKeyType, +} from 'services/i18n-language-code.service'; +import {ReadOnlyExplorationBackendApiService} from 'domain/exploration/read-only-exploration-backend-api.service'; +import {UrlService} from 'services/contextual/url.service'; @Component({ selector: 'oppia-player-sidebar', @@ -45,44 +47,48 @@ export class PlayerSidebarComponent implements OnInit { private mobileMenuService: MobileMenuService, private contextService: ContextService, private i18nLanguageCodeService: I18nLanguageCodeService, - private readOnlyExplorationBackendApiService: - ReadOnlyExplorationBackendApiService, - private urlService: UrlService, + private readOnlyExplorationBackendApiService: ReadOnlyExplorationBackendApiService, + private urlService: UrlService ) {} ngOnInit(): void { - this.mobileMenuService.getMenuVisibility().subscribe((visibility) => { + this.mobileMenuService.getMenuVisibility().subscribe(visibility => { this.mobileMenuVisible = visibility; }); let pathnameArray = this.urlService.getPathname().split('/'); let explorationContext = false; for (let i = 0; i < pathnameArray.length; i++) { - if (pathnameArray[i] === 'explore' || - pathnameArray[i] === 'create' || - pathnameArray[i] === 'skill_editor' || - pathnameArray[i] === 'embed' || - pathnameArray[i] === 'lesson') { + if ( + pathnameArray[i] === 'explore' || + pathnameArray[i] === 'create' || + pathnameArray[i] === 'skill_editor' || + pathnameArray[i] === 'embed' || + pathnameArray[i] === 'lesson' + ) { explorationContext = true; break; } } - this.explorationId = explorationContext ? - this.contextService.getExplorationId() : 'test_id'; + this.explorationId = explorationContext + ? this.contextService.getExplorationId() + : 'test_id'; this.expDescription = 'Loading...'; - this.readOnlyExplorationBackendApiService.fetchExplorationAsync( - this.explorationId, - this.urlService.getExplorationVersionFromUrl(), - this.urlService.getPidFromUrl()) - .then((response) => { + this.readOnlyExplorationBackendApiService + .fetchExplorationAsync( + this.explorationId, + this.urlService.getExplorationVersionFromUrl(), + this.urlService.getPidFromUrl() + ) + .then(response => { this.expDescription = response.exploration.objective; }); - this.expDescTranslationKey = ( - this.i18nLanguageCodeService. - getExplorationTranslationKey( - this.explorationId, TranslationKeyType.DESCRIPTION) - ); + this.expDescTranslationKey = + this.i18nLanguageCodeService.getExplorationTranslationKey( + this.explorationId, + TranslationKeyType.DESCRIPTION + ); } toggleSidebar(): void { @@ -98,7 +104,9 @@ export class PlayerSidebarComponent implements OnInit { } } -angular.module('oppia').directive('oppiaPlayerHeader', +angular.module('oppia').directive( + 'oppiaPlayerHeader', downgradeComponent({ - component: PlayerSidebarComponent - }) as angular.IDirectiveFactory); + component: PlayerSidebarComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/exploration-player-page/new-lesson-player/new-lesson-player-services/mobile-menu.service.spec.ts b/core/templates/pages/exploration-player-page/new-lesson-player/new-lesson-player-services/mobile-menu.service.spec.ts index a8661747fc83..f066601440ea 100644 --- a/core/templates/pages/exploration-player-page/new-lesson-player/new-lesson-player-services/mobile-menu.service.spec.ts +++ b/core/templates/pages/exploration-player-page/new-lesson-player/new-lesson-player-services/mobile-menu.service.spec.ts @@ -16,15 +16,15 @@ * @fileoverview Tests for obile menu service for new lesson player. */ -import { TestBed } from '@angular/core/testing'; -import { MobileMenuService } from './mobile-menu.service'; +import {TestBed} from '@angular/core/testing'; +import {MobileMenuService} from './mobile-menu.service'; describe('MobileMenuService', () => { let service: MobileMenuService; beforeEach(() => { TestBed.configureTestingModule({ - providers: [MobileMenuService] + providers: [MobileMenuService], }); service = TestBed.inject(MobileMenuService); }); @@ -34,14 +34,14 @@ describe('MobileMenuService', () => { }); it('should initially have menu visibility set to false', () => { - service.getMenuVisibility().subscribe((visibility) => { + service.getMenuVisibility().subscribe(visibility => { expect(visibility).toBe(false); }); }); it('should update menu visibility when toggled', () => { let currentValue: boolean | undefined; - service.getMenuVisibility().subscribe(value => currentValue = value); + service.getMenuVisibility().subscribe(value => (currentValue = value)); service.toggleMenuVisibility(); expect(currentValue).toBe(true); service.toggleMenuVisibility(); diff --git a/core/templates/pages/exploration-player-page/new-lesson-player/new-lesson-player-services/mobile-menu.service.ts b/core/templates/pages/exploration-player-page/new-lesson-player/new-lesson-player-services/mobile-menu.service.ts index e9c2d75c2e93..a5e83f488c67 100644 --- a/core/templates/pages/exploration-player-page/new-lesson-player/new-lesson-player-services/mobile-menu.service.ts +++ b/core/templates/pages/exploration-player-page/new-lesson-player/new-lesson-player-services/mobile-menu.service.ts @@ -16,11 +16,11 @@ * @fileoverview Mobile menu service for new lesson player. */ -import { Injectable } from '@angular/core'; -import { BehaviorSubject, Observable } from 'rxjs'; +import {Injectable} from '@angular/core'; +import {BehaviorSubject, Observable} from 'rxjs'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class MobileMenuService { private menuVisibleSubject = new BehaviorSubject(false); diff --git a/core/templates/pages/exploration-player-page/services/answer-classification.service.spec.ts b/core/templates/pages/exploration-player-page/services/answer-classification.service.spec.ts index 889292dfe989..39a080c6aa0b 100644 --- a/core/templates/pages/exploration-player-page/services/answer-classification.service.spec.ts +++ b/core/templates/pages/exploration-player-page/services/answer-classification.service.spec.ts @@ -16,24 +16,27 @@ * @fileoverview Unit tests for the answer classification service */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TestBed } from '@angular/core/testing'; - -import { AnswerClassificationResult } from 'domain/classifier/answer-classification-result.model'; -import { AnswerClassificationService, InteractionRulesService } from 'pages/exploration-player-page/services/answer-classification.service'; -import { AppService } from 'services/app.service'; -import { CamelCaseToHyphensPipe } from 'filters/string-utility-filters/camel-case-to-hyphens.pipe'; -import { Classifier } from 'domain/classifier/classifier.model'; -import { ExplorationPlayerConstants } from 'pages/exploration-player-page/exploration-player-page.constants'; -import { InteractionSpecsService } from 'services/interaction-specs.service'; -import { OutcomeObjectFactory } from 'domain/exploration/OutcomeObjectFactory'; -import { PredictionAlgorithmRegistryService } from 'pages/exploration-player-page/services/prediction-algorithm-registry.service'; -import { StateClassifierMappingService } from 'pages/exploration-player-page/services/state-classifier-mapping.service'; -import { StateObjectFactory } from 'domain/state/StateObjectFactory'; -import { TextClassifierFrozenModel } from 'classifiers/proto/text_classifier'; -import { TextInputRulesService } from 'interactions/TextInput/directives/text-input-rules.service'; -import { AlertsService } from 'services/alerts.service'; -import { TextInputPredictionService } from 'interactions/TextInput/text-input-prediction.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TestBed} from '@angular/core/testing'; + +import {AnswerClassificationResult} from 'domain/classifier/answer-classification-result.model'; +import { + AnswerClassificationService, + InteractionRulesService, +} from 'pages/exploration-player-page/services/answer-classification.service'; +import {AppService} from 'services/app.service'; +import {CamelCaseToHyphensPipe} from 'filters/string-utility-filters/camel-case-to-hyphens.pipe'; +import {Classifier} from 'domain/classifier/classifier.model'; +import {ExplorationPlayerConstants} from 'pages/exploration-player-page/exploration-player-page.constants'; +import {InteractionSpecsService} from 'services/interaction-specs.service'; +import {OutcomeObjectFactory} from 'domain/exploration/OutcomeObjectFactory'; +import {PredictionAlgorithmRegistryService} from 'pages/exploration-player-page/services/prediction-algorithm-registry.service'; +import {StateClassifierMappingService} from 'pages/exploration-player-page/services/state-classifier-mapping.service'; +import {StateObjectFactory} from 'domain/state/StateObjectFactory'; +import {TextClassifierFrozenModel} from 'classifiers/proto/text_classifier'; +import {TextInputRulesService} from 'interactions/TextInput/directives/text-input-rules.service'; +import {AlertsService} from 'services/alerts.service'; +import {TextInputPredictionService} from 'interactions/TextInput/text-input-prediction.service'; describe('Answer Classification Service', () => { const stateName = 'Test State'; @@ -60,7 +63,8 @@ describe('Answer Classification Service', () => { interactionSpecsService = TestBed.get(InteractionSpecsService); outcomeObjectFactory = TestBed.get(OutcomeObjectFactory); predictionAlgorithmRegistryService = TestBed.get( - PredictionAlgorithmRegistryService); + PredictionAlgorithmRegistryService + ); stateClassifierMappingService = TestBed.get(StateClassifierMappingService); stateObjectFactory = TestBed.get(StateObjectFactory); textInputRulesService = TestBed.get(TextInputRulesService); @@ -71,25 +75,27 @@ describe('Answer Classification Service', () => { let expId = '0'; beforeEach(() => { + spyOn(interactionSpecsService, 'isInteractionTrainable').and.returnValue( + false + ); spyOn( - interactionSpecsService, 'isInteractionTrainable' + appService, + 'isMachineLearningClassificationEnabled' ).and.returnValue(false); - spyOn(appService, 'isMachineLearningClassificationEnabled') - .and.returnValue(false); stateClassifierMappingService.init(expId, 0); stateDict = { content: { content_id: 'content', - html: 'content' + html: 'content', }, recorded_voiceovers: { voiceovers_mapping: { content: {}, default_outcome: {}, feedback_1: {}, - feedback_2: {} - } + feedback_2: {}, + }, }, interaction: { id: 'TextInput', @@ -97,336 +103,445 @@ describe('Answer Classification Service', () => { placeholder: { value: { content_id: 'ca_placeholder_0', - unicode_str: '' - } + unicode_str: '', + }, }, - rows: { value: 1 }, + rows: {value: 1}, catchMisspellings: { - value: false - } + value: false, + }, }, - answer_groups: [{ - outcome: { - dest: 'outcome 1', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_1', - html: '' + answer_groups: [ + { + outcome: { + dest: 'outcome 1', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: '', + }, + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, }, - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null + rule_specs: [ + { + rule_type: 'Equals', + inputs: { + x: { + contentId: 'rule_input_0', + normalizedStrSet: ['10'], + }, + }, + }, + ], }, - rule_specs: [{ - rule_type: 'Equals', - inputs: { - x: { - contentId: 'rule_input_0', - normalizedStrSet: ['10'] - } - } - }], - }, { - outcome: { - dest: 'outcome 2', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: '' + { + outcome: { + dest: 'outcome 2', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: '', + }, + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, }, - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null + rule_specs: [ + { + rule_type: 'Equals', + inputs: { + x: { + contentId: 'rule_input_1', + normalizedStrSet: ['5'], + }, + }, + }, + { + rule_type: 'Equals', + inputs: { + x: { + contentId: 'rule_input_2', + normalizedStrSet: ['6'], + }, + }, + }, + { + rule_type: 'FuzzyEquals', + inputs: { + x: { + contentId: 'rule_input_3', + normalizedStrSet: ['7'], + }, + }, + }, + ], }, - rule_specs: [{ - rule_type: 'Equals', - inputs: { - x: { - contentId: 'rule_input_1', - normalizedStrSet: ['5'] - } - } - }, { - rule_type: 'Equals', - inputs: { - x: { - contentId: 'rule_input_2', - normalizedStrSet: ['6'] - } - } - }, { - rule_type: 'FuzzyEquals', - inputs: { - x: { - contentId: 'rule_input_3', - normalizedStrSet: ['7'] - } - } - }], - }, { - outcome: { - dest: 'outcome 2', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: '' + { + outcome: { + dest: 'outcome 2', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: '', + }, + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, }, - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null + rule_specs: [ + { + rule_type: 'Equals', + inputs: { + x: { + contentId: 'rule_input_1', + normalizedStrSet: ['correct'], + }, + }, + }, + { + rule_type: 'Equals', + inputs: { + x: { + contentId: 'rule_input_2', + normalizedStrSet: ['CorrectAnswer'], + }, + }, + }, + { + rule_type: 'FuzzyEquals', + inputs: { + x: { + contentId: 'rule_input_3', + normalizedStrSet: ['Right'], + }, + }, + }, + ], }, - rule_specs: [{ - rule_type: 'Equals', - inputs: { - x: { - contentId: 'rule_input_1', - normalizedStrSet: ['correct'] - } - } - }, { - rule_type: 'Equals', - inputs: { - x: { - contentId: 'rule_input_2', - normalizedStrSet: ['CorrectAnswer'] - } - } - }, { - rule_type: 'FuzzyEquals', - inputs: { - x: { - contentId: 'rule_input_3', - normalizedStrSet: ['Right'] - } - } - }], - }], + ], default_outcome: { dest: 'default', dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: '' + html: '', }, labelled_as_correct: false, param_changes: [], refresher_exploration_id: null, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, - hints: [] + hints: [], }, param_changes: [], - solicit_answer_details: false + solicit_answer_details: false, }; }); it('should fail if no frontend rules are provided', () => { - const state = ( - stateObjectFactory.createFromBackendDict(stateName, stateDict)); + const state = stateObjectFactory.createFromBackendDict( + stateName, + stateDict + ); - expect( - () => answerClassificationService.getMatchingClassificationResult( - state.name, state.interaction, '0', null) + expect(() => + answerClassificationService.getMatchingClassificationResult( + state.name, + state.interaction, + '0', + null + ) ).toThrowError( - 'No interactionRulesService was available to classify the answer.'); + 'No interactionRulesService was available to classify the answer.' + ); }); - it('should return the first matching answer group and first matching ' + - 'rule spec', () => { - const state = ( - stateObjectFactory.createFromBackendDict(stateName, stateDict)); + it( + 'should return the first matching answer group and first matching ' + + 'rule spec', + () => { + const state = stateObjectFactory.createFromBackendDict( + stateName, + stateDict + ); - expect( - answerClassificationService.getMatchingClassificationResult( - state.name, state.interaction, '10', textInputRulesService) - ).toEqual( - new AnswerClassificationResult( - outcomeObjectFactory.createNew('outcome 1', 'feedback_1', '', []), - 0, 0, - ExplorationPlayerConstants.EXPLICIT_CLASSIFICATION)); + expect( + answerClassificationService.getMatchingClassificationResult( + state.name, + state.interaction, + '10', + textInputRulesService + ) + ).toEqual( + new AnswerClassificationResult( + outcomeObjectFactory.createNew('outcome 1', 'feedback_1', '', []), + 0, + 0, + ExplorationPlayerConstants.EXPLICIT_CLASSIFICATION + ) + ); - expect( - answerClassificationService.getMatchingClassificationResult( - state.name, state.interaction, '5', textInputRulesService) - ).toEqual( - new AnswerClassificationResult( - outcomeObjectFactory.createNew('outcome 2', 'feedback_2', '', []), - 1, 0, - ExplorationPlayerConstants.EXPLICIT_CLASSIFICATION)); + expect( + answerClassificationService.getMatchingClassificationResult( + state.name, + state.interaction, + '5', + textInputRulesService + ) + ).toEqual( + new AnswerClassificationResult( + outcomeObjectFactory.createNew('outcome 2', 'feedback_2', '', []), + 1, + 0, + ExplorationPlayerConstants.EXPLICIT_CLASSIFICATION + ) + ); - expect( - answerClassificationService.getMatchingClassificationResult( - state.name, state.interaction, '6', textInputRulesService) - ).toEqual( - new AnswerClassificationResult( - outcomeObjectFactory.createNew('outcome 2', 'feedback_2', '', []), - 1, 1, - ExplorationPlayerConstants.EXPLICIT_CLASSIFICATION)); - }); + expect( + answerClassificationService.getMatchingClassificationResult( + state.name, + state.interaction, + '6', + textInputRulesService + ) + ).toEqual( + new AnswerClassificationResult( + outcomeObjectFactory.createNew('outcome 2', 'feedback_2', '', []), + 1, + 1, + ExplorationPlayerConstants.EXPLICIT_CLASSIFICATION + ) + ); + } + ); it('should return the default rule if no answer group matches', () => { - const state = ( - stateObjectFactory.createFromBackendDict(stateName, stateDict)); + const state = stateObjectFactory.createFromBackendDict( + stateName, + stateDict + ); expect( answerClassificationService.getMatchingClassificationResult( - state.name, state.interaction, '777', textInputRulesService) + state.name, + state.interaction, + '777', + textInputRulesService + ) ).toEqual( new AnswerClassificationResult( outcomeObjectFactory.createNew('default', 'default_outcome', '', []), - 3, 0, + 3, + 0, ExplorationPlayerConstants.DEFAULT_OUTCOME_CLASSIFICATION ) ); }); - it('should fail if no answer group matches and' + - 'default outcome of interaction is not defined', () => { - spyOn(alertsService, 'addWarning').and.callThrough(); + it( + 'should fail if no answer group matches and' + + 'default outcome of interaction is not defined', + () => { + spyOn(alertsService, 'addWarning').and.callThrough(); - stateDict.interaction.default_outcome = null; - const state = ( - stateObjectFactory.createFromBackendDict(stateName, stateDict)); + stateDict.interaction.default_outcome = null; + const state = stateObjectFactory.createFromBackendDict( + stateName, + stateDict + ); - expect( - () => answerClassificationService.getMatchingClassificationResult( - state.name, state.interaction, 'abc', textInputRulesService) - ).toThrowError( - 'No defaultOutcome was available to classify the answer.'); + expect(() => + answerClassificationService.getMatchingClassificationResult( + state.name, + state.interaction, + 'abc', + textInputRulesService + ) + ).toThrowError( + 'No defaultOutcome was available to classify the answer.' + ); - expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Something went wrong with the exploration.'); - }); + expect(alertsService.addWarning).toHaveBeenCalledWith( + 'Something went wrong with the exploration.' + ); + } + ); it( 'should fail if no answer group matches and no default rule is ' + 'provided', () => { - stateDict.interaction.answer_groups = [{ + stateDict.interaction.answer_groups = [ + { + outcome: { + dest: 'outcome 1', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: '', + }, + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + }, + rule_specs: [ + { + rule_type: 'Equals', + inputs: { + x: { + contentId: 'rule_input_0', + normalizedStrSet: ['10'], + }, + }, + }, + ], + }, + ]; + + const state = stateObjectFactory.createFromBackendDict( + stateName, + stateDict + ); + + expect(() => + answerClassificationService.getMatchingClassificationResult( + state.name, + state.interaction, + '0', + null + ) + ).toThrowError( + 'No interactionRulesService was available to classify the answer.' + ); + } + ); + + it('should check for misspellings correctly.', () => { + stateDict.interaction.answer_groups = [ + { outcome: { dest: 'outcome 1', dest_if_really_stuck: null, feedback: { content_id: 'feedback_1', - html: '' + html: '', }, labelled_as_correct: false, param_changes: [], refresher_exploration_id: null, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, - rule_specs: [{ - rule_type: 'Equals', - inputs: { - x: { - contentId: 'rule_input_0', - normalizedStrSet: ['10'] - } - } - }], - }]; - - const state = ( - stateObjectFactory.createFromBackendDict(stateName, stateDict)); - - expect( - () => answerClassificationService.getMatchingClassificationResult( - state.name, state.interaction, '0', null) - ).toThrowError( - 'No interactionRulesService was available to classify the answer.'); - }); - - it('should check for misspellings correctly.', () => { - stateDict.interaction.answer_groups = [{ - outcome: { - dest: 'outcome 1', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_1', - html: '' - }, - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null + rule_specs: [ + { + rule_type: 'Equals', + inputs: { + x: { + contentId: 'rule_input_0', + normalizedStrSet: ['IncorrectAnswer'], + }, + }, + }, + ], }, - rule_specs: [{ - rule_type: 'Equals', - inputs: { - x: { - contentId: 'rule_input_0', - normalizedStrSet: ['IncorrectAnswer'] - } - } - }], - }, { - outcome: { - dest: 'outcome 2', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: '' + { + outcome: { + dest: 'outcome 2', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: '', + }, + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, }, - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null + rule_specs: [ + { + rule_type: 'Equals', + inputs: { + x: { + contentId: 'rule_input_1', + normalizedStrSet: ['Answer'], + }, + }, + }, + { + rule_type: 'Equals', + inputs: { + x: { + contentId: 'rule_input_2', + normalizedStrSet: ['MaybeCorrect'], + }, + }, + }, + { + rule_type: 'FuzzyEquals', + inputs: { + x: { + contentId: 'rule_input_3', + normalizedStrSet: ['FuzzilyCorrect'], + }, + }, + }, + { + rule_type: 'Equals', + inputs: { + x: { + contentId: 'rule_input_short_answer', + normalizedStrSet: ['ans'], + }, + }, + }, + ], }, - rule_specs: [{ - rule_type: 'Equals', - inputs: { - x: { - contentId: 'rule_input_1', - normalizedStrSet: ['Answer'] - } - } - }, { - rule_type: 'Equals', - inputs: { - x: { - contentId: 'rule_input_2', - normalizedStrSet: ['MaybeCorrect'] - } - } - }, { - rule_type: 'FuzzyEquals', - inputs: { - x: { - contentId: 'rule_input_3', - normalizedStrSet: ['FuzzilyCorrect'] - } - } - }, { - rule_type: 'Equals', - inputs: { - x: { - contentId: 'rule_input_short_answer', - normalizedStrSet: ['ans'] - } - } - }], - }]; - - const state = ( - stateObjectFactory.createFromBackendDict(stateName, stateDict)); - - expect(answerClassificationService.isAnswerOnlyMisspelled( - state.interaction, 'anSwkp')).toEqual(true); - - expect(answerClassificationService.isAnswerOnlyMisspelled( - state.interaction, 'anSwer')).toEqual(true); - - expect(answerClassificationService.isAnswerOnlyMisspelled( - state.interaction, 'fuZZilyCeerect')).toEqual(true); - - expect(answerClassificationService.isAnswerOnlyMisspelled( - state.interaction, 'InCORrectAnkwpr')).toEqual(false); - - expect(answerClassificationService.isAnswerOnlyMisspelled( - state.interaction, 'an')).toEqual(false); + ]; + + const state = stateObjectFactory.createFromBackendDict( + stateName, + stateDict + ); + + expect( + answerClassificationService.isAnswerOnlyMisspelled( + state.interaction, + 'anSwkp' + ) + ).toEqual(true); + + expect( + answerClassificationService.isAnswerOnlyMisspelled( + state.interaction, + 'anSwer' + ) + ).toEqual(true); + + expect( + answerClassificationService.isAnswerOnlyMisspelled( + state.interaction, + 'fuZZilyCeerect' + ) + ).toEqual(true); + + expect( + answerClassificationService.isAnswerOnlyMisspelled( + state.interaction, + 'InCORrectAnkwpr' + ) + ).toEqual(false); + + expect( + answerClassificationService.isAnswerOnlyMisspelled( + state.interaction, + 'an' + ) + ).toEqual(false); }); }); @@ -435,8 +550,10 @@ describe('Answer Classification Service', () => { let expId = '0'; beforeEach(() => { - spyOn(appService, 'isMachineLearningClassificationEnabled') - .and.returnValue(true); + spyOn( + appService, + 'isMachineLearningClassificationEnabled' + ).and.returnValue(true); let modelJson = { KNN: { @@ -445,7 +562,7 @@ describe('Answer Classification Service', () => { T: 20, top: 10, fingerprint_data: {}, - token_to_id: {} + token_to_id: {}, }, SVM: { classes: [], @@ -460,9 +577,9 @@ describe('Answer Classification Service', () => { probA: [], support_vectors: [[]], probB: [], - dual_coef: [[]] + dual_coef: [[]], }, - cv_vocabulary: {} + cv_vocabulary: {}, }; let textClassifierModel = new TextClassifierFrozenModel(); // The model_json attribute in TextClassifierFrozenModel class can't be @@ -470,30 +587,36 @@ describe('Answer Classification Service', () => { // compiled with the help of protoc. textClassifierModel.model_json = JSON.stringify(modelJson); let testClassifier = new Classifier( - 'TestClassifier', textClassifierModel.serialize(), 1); + 'TestClassifier', + textClassifierModel.serialize(), + 1 + ); stateClassifierMappingService.init(expId, 0); stateClassifierMappingService.testOnlySetClassifierData( - stateName, testClassifier); + stateName, + testClassifier + ); predictionAlgorithmRegistryService.testOnlySetPredictionService( - 'TestClassifier', 1, + 'TestClassifier', + 1, { - predict: (classifierData, answer) => 1 + predict: (classifierData, answer) => 1, } as TextInputPredictionService ); stateDict = { content: { content_id: 'content', - html: 'content' + html: 'content', }, recorded_voiceovers: { voiceovers_mapping: { content: {}, default_outcome: {}, feedback_1: {}, - feedback_2: {} - } + feedback_2: {}, + }, }, interaction: { id: 'TextInput', @@ -501,149 +624,193 @@ describe('Answer Classification Service', () => { placeholder: { value: { content_id: 'ca_placeholder_0', - unicode_str: '' - } + unicode_str: '', + }, }, - rows: { value: 1 }, + rows: {value: 1}, catchMisspellings: { - value: false - } + value: false, + }, }, - answer_groups: [{ - outcome: { - dest: 'outcome 1', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_1', - html: '' + answer_groups: [ + { + outcome: { + dest: 'outcome 1', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: '', + }, + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, }, - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null + rule_specs: [ + { + rule_type: 'Equals', + inputs: { + x: { + contentId: 'rule_input_0', + normalizedStrSet: ['10'], + }, + }, + }, + ], }, - rule_specs: [{ - rule_type: 'Equals', - inputs: { - x: { - contentId: 'rule_input_0', - normalizedStrSet: ['10'] - } - } - }], - }, { - outcome: { - dest: 'outcome 2', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: '' + { + outcome: { + dest: 'outcome 2', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: '', + }, + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, }, - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null + rule_input_translations: {}, + rule_specs: [ + { + rule_type: 'Equals', + inputs: { + x: { + contentId: 'rule_input_1', + normalizedStrSet: ['5'], + }, + }, + }, + { + rule_type: 'Equals', + inputs: { + x: { + contentId: 'rule_input_2', + normalizedStrSet: ['7'], + }, + }, + }, + ], }, - rule_input_translations: {}, - rule_specs: [{ - rule_type: 'Equals', - inputs: { - x: { - contentId: 'rule_input_1', - normalizedStrSet: ['5'] - } - } - }, { - rule_type: 'Equals', - inputs: { - x: { - contentId: 'rule_input_2', - normalizedStrSet: ['7'] - } - } - }], - }], + ], default_outcome: { dest: 'default', dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: '' + html: '', }, labelled_as_correct: false, param_changes: [], refresher_exploration_id: null, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, - hints: [] + hints: [], }, param_changes: [], solicit_answer_details: false, }; }); - it('should query the prediction service if no answer group matches and ' + - 'interaction is trainable', () => { - spyOn( - interactionSpecsService, 'isInteractionTrainable' - ).and.returnValue(true); - - const state = ( - stateObjectFactory.createFromBackendDict(stateName, stateDict)); - - expect( - answerClassificationService.getMatchingClassificationResult( - state.name, state.interaction, '0', textInputRulesService) - ).toEqual( - new AnswerClassificationResult( - state.interaction.answerGroups[1].outcome, 1, null, - ExplorationPlayerConstants.STATISTICAL_CLASSIFICATION)); - }); - - it('should get default rule if the answer group can not be predicted', + it( + 'should query the prediction service if no answer group matches and ' + + 'interaction is trainable', () => { spyOn( - predictionAlgorithmRegistryService, 'getPredictionService' - ).and.returnValue({ - predict: (classifierData, answer) => -1 - } as TextInputPredictionService); + interactionSpecsService, + 'isInteractionTrainable' + ).and.returnValue(true); - const state = ( - stateObjectFactory.createFromBackendDict(stateName, stateDict)); + const state = stateObjectFactory.createFromBackendDict( + stateName, + stateDict + ); expect( answerClassificationService.getMatchingClassificationResult( - state.name, state.interaction, '0', textInputRulesService) + state.name, + state.interaction, + '0', + textInputRulesService + ) ).toEqual( new AnswerClassificationResult( - outcomeObjectFactory.createNew( - 'default', 'default_outcome', '', []), 2, 0, - ExplorationPlayerConstants.DEFAULT_OUTCOME_CLASSIFICATION)); - }); + state.interaction.answerGroups[1].outcome, + 1, + null, + ExplorationPlayerConstants.STATISTICAL_CLASSIFICATION + ) + ); + } + ); + + it('should get default rule if the answer group can not be predicted', () => { + spyOn( + predictionAlgorithmRegistryService, + 'getPredictionService' + ).and.returnValue({ + predict: (classifierData, answer) => -1, + } as TextInputPredictionService); + + const state = stateObjectFactory.createFromBackendDict( + stateName, + stateDict + ); + + expect( + answerClassificationService.getMatchingClassificationResult( + state.name, + state.interaction, + '0', + textInputRulesService + ) + ).toEqual( + new AnswerClassificationResult( + outcomeObjectFactory.createNew('default', 'default_outcome', '', []), + 2, + 0, + ExplorationPlayerConstants.DEFAULT_OUTCOME_CLASSIFICATION + ) + ); + }); it( 'should return the default rule if no answer group matches and ' + 'interaction is not trainable', () => { spyOn( - interactionSpecsService, 'isInteractionTrainable' + interactionSpecsService, + 'isInteractionTrainable' ).and.returnValue(false); - const state = ( - stateObjectFactory.createFromBackendDict(stateName, stateDict)); + const state = stateObjectFactory.createFromBackendDict( + stateName, + stateDict + ); expect( answerClassificationService.getMatchingClassificationResult( - state.name, state.interaction, '0', textInputRulesService) + state.name, + state.interaction, + '0', + textInputRulesService + ) ).toEqual( new AnswerClassificationResult( outcomeObjectFactory.createNew( - 'default', 'default_outcome', '', []), - 2, 0, + 'default', + 'default_outcome', + '', + [] + ), + 2, + 0, ExplorationPlayerConstants.DEFAULT_OUTCOME_CLASSIFICATION ) ); - }); + } + ); }); describe('with training data classification', () => { @@ -651,25 +818,27 @@ describe('Answer Classification Service', () => { let expId = '0'; beforeEach(() => { + spyOn(interactionSpecsService, 'isInteractionTrainable').and.returnValue( + true + ); spyOn( - interactionSpecsService, 'isInteractionTrainable' + appService, + 'isMachineLearningClassificationEnabled' ).and.returnValue(true); - spyOn(appService, 'isMachineLearningClassificationEnabled') - .and.returnValue(true); stateClassifierMappingService.init(expId, 0); stateDict = { content: { content_id: 'content', - html: 'content' + html: 'content', }, recorded_voiceovers: { voiceovers_mapping: { content: {}, default_outcome: {}, feedback_1: {}, - feedback_2: {} - } + feedback_2: {}, + }, }, interaction: { id: 'TextInput', @@ -677,74 +846,81 @@ describe('Answer Classification Service', () => { placeholder: { value: { content_id: 'ca_placeholder_0', - unicode_str: '' - } + unicode_str: '', + }, }, - rows: { value: 1 }, + rows: {value: 1}, catchMisspellings: { - value: false - } + value: false, + }, }, - answer_groups: [{ - outcome: { - dest: 'outcome 1', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_1', - html: '' + answer_groups: [ + { + outcome: { + dest: 'outcome 1', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: '', + }, + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, }, - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null + training_data: ['abc', 'input'], + rule_specs: [ + { + rule_type: 'Equals', + inputs: { + x: { + contentId: 'rule_input_0', + normalizedStrSet: ['equal'], + }, + }, + }, + ], }, - training_data: ['abc', 'input'], - rule_specs: [{ - rule_type: 'Equals', - inputs: { - x: { - contentId: 'rule_input_0', - normalizedStrSet: ['equal'] - } - } - }], - }, { - outcome: { - dest: 'outcome 2', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: '' + { + outcome: { + dest: 'outcome 2', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: '', + }, + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, }, - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null + training_data: ['xyz'], + rule_specs: [ + { + rule_type: 'Contains', + inputs: { + x: { + contentId: 'rule_input_5', + normalizedStrSet: ['npu'], + }, + }, + }, + ], }, - training_data: ['xyz'], - rule_specs: [{ - rule_type: 'Contains', - inputs: { - x: { - contentId: 'rule_input_5', - normalizedStrSet: ['npu'] - } - } - }], - }], + ], default_outcome: { dest: 'default', dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: '' + html: '', }, labelled_as_correct: false, param_changes: [], refresher_exploration_id: null, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, - hints: [] + hints: [], }, param_changes: [], solicit_answer_details: false, @@ -755,79 +931,134 @@ describe('Answer Classification Service', () => { 'should use training data classification if no answer group matches ' + 'and interaction is trainable', () => { - const state = ( - stateObjectFactory.createFromBackendDict(stateName, stateDict)); + const state = stateObjectFactory.createFromBackendDict( + stateName, + stateDict + ); expect( answerClassificationService.getMatchingClassificationResult( - state.name, state.interaction, 'abc', textInputRulesService) + state.name, + state.interaction, + 'abc', + textInputRulesService + ) ).toEqual( new AnswerClassificationResult( - state.interaction.answerGroups[0].outcome, 0, null, - ExplorationPlayerConstants.TRAINING_DATA_CLASSIFICATION)); + state.interaction.answerGroups[0].outcome, + 0, + null, + ExplorationPlayerConstants.TRAINING_DATA_CLASSIFICATION + ) + ); expect( answerClassificationService.getMatchingClassificationResult( - state.name, state.interaction, 'xyz', textInputRulesService) + state.name, + state.interaction, + 'xyz', + textInputRulesService + ) ).toEqual( new AnswerClassificationResult( - state.interaction.answerGroups[1].outcome, 1, null, - ExplorationPlayerConstants.TRAINING_DATA_CLASSIFICATION)); - }); + state.interaction.answerGroups[1].outcome, + 1, + null, + ExplorationPlayerConstants.TRAINING_DATA_CLASSIFICATION + ) + ); + } + ); it( 'should perform explicit classification before doing training data ' + 'classification', () => { - const state = ( - stateObjectFactory.createFromBackendDict(stateName, stateDict)); + const state = stateObjectFactory.createFromBackendDict( + stateName, + stateDict + ); expect( answerClassificationService.getMatchingClassificationResult( - state.name, state.interaction, 'input', textInputRulesService) + state.name, + state.interaction, + 'input', + textInputRulesService + ) ).toEqual( new AnswerClassificationResult( - state.interaction.answerGroups[1].outcome, 1, 0, - ExplorationPlayerConstants.EXPLICIT_CLASSIFICATION)); - }); + state.interaction.answerGroups[1].outcome, + 1, + 0, + ExplorationPlayerConstants.EXPLICIT_CLASSIFICATION + ) + ); + } + ); - it('should check whether answer is classified explicitly ' + - 'or goes into new state', () => { - spyOn(answerClassificationService, 'getMatchingClassificationResult') - .and.callThrough(); + it( + 'should check whether answer is classified explicitly ' + + 'or goes into new state', + () => { + spyOn( + answerClassificationService, + 'getMatchingClassificationResult' + ).and.callThrough(); - // Returns false when no answer group matches and - // default outcome has destination equal to state name. + // Returns false when no answer group matches and + // default outcome has destination equal to state name. - stateDict.interaction.default_outcome.dest = stateName; - let state1 = ( - stateObjectFactory.createFromBackendDict(stateName, stateDict)); + stateDict.interaction.default_outcome.dest = stateName; + let state1 = stateObjectFactory.createFromBackendDict( + stateName, + stateDict + ); - let res1 = ( - answerClassificationService.isClassifiedExplicitlyOrGoesToNewState( - state1.name, state1, '777', textInputRulesService)); + let res1 = + answerClassificationService.isClassifiedExplicitlyOrGoesToNewState( + state1.name, + state1, + '777', + textInputRulesService + ); - expect(res1).toBeFalse(); - expect( - answerClassificationService.getMatchingClassificationResult - ).toHaveBeenCalledWith( - state1.name, state1.interaction, '777', textInputRulesService); + expect(res1).toBeFalse(); + expect( + answerClassificationService.getMatchingClassificationResult + ).toHaveBeenCalledWith( + state1.name, + state1.interaction, + '777', + textInputRulesService + ); - // Returns true if any answer group matches. + // Returns true if any answer group matches. - stateDict.interaction.default_outcome.dest = 'default'; - let state2 = ( - stateObjectFactory.createFromBackendDict(stateName, stateDict)); + stateDict.interaction.default_outcome.dest = 'default'; + let state2 = stateObjectFactory.createFromBackendDict( + stateName, + stateDict + ); - let res2 = ( - answerClassificationService.isClassifiedExplicitlyOrGoesToNewState( - state2.name, state2, 'equal', textInputRulesService)); + let res2 = + answerClassificationService.isClassifiedExplicitlyOrGoesToNewState( + state2.name, + state2, + 'equal', + textInputRulesService + ); - expect(res2).toBeTrue(); - expect( - answerClassificationService.getMatchingClassificationResult - ).toHaveBeenCalledWith( - state2.name, state2.interaction, 'equal', textInputRulesService); - }); + expect(res2).toBeTrue(); + expect( + answerClassificationService.getMatchingClassificationResult + ).toHaveBeenCalledWith( + state2.name, + state2.interaction, + 'equal', + textInputRulesService + ); + } + ); }); }); diff --git a/core/templates/pages/exploration-player-page/services/answer-classification.service.ts b/core/templates/pages/exploration-player-page/services/answer-classification.service.ts index def209029bac..108a0b5bb0ae 100644 --- a/core/templates/pages/exploration-player-page/services/answer-classification.service.ts +++ b/core/templates/pages/exploration-player-page/services/answer-classification.service.ts @@ -16,38 +16,42 @@ * @fileoverview Classification service for answer groups. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; - -import { AlertsService } from 'services/alerts.service'; -import { AnswerClassificationResult } from 'domain/classifier/answer-classification-result.model'; -import { AnswerGroup } from 'domain/exploration/AnswerGroupObjectFactory'; -import { AppService } from 'services/app.service'; -import { ExplorationPlayerConstants } from 'pages/exploration-player-page/exploration-player-page.constants'; -import { InteractionAnswer, TextInputAnswer } from 'interactions/answer-defs'; -import { Interaction } from 'domain/exploration/InteractionObjectFactory'; -import { InteractionSpecsService } from 'services/interaction-specs.service'; -import { Outcome } from 'domain/exploration/OutcomeObjectFactory'; -import { PredictionAlgorithmRegistryService } from 'pages/exploration-player-page/services/prediction-algorithm-registry.service'; -import { State } from 'domain/state/StateObjectFactory'; -import { StateClassifierMappingService } from 'pages/exploration-player-page/services/state-classifier-mapping.service'; -import { InteractionRuleInputs, TranslatableSetOfNormalizedString } from 'interactions/rule-input-defs'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {AlertsService} from 'services/alerts.service'; +import {AnswerClassificationResult} from 'domain/classifier/answer-classification-result.model'; +import {AnswerGroup} from 'domain/exploration/AnswerGroupObjectFactory'; +import {AppService} from 'services/app.service'; +import {ExplorationPlayerConstants} from 'pages/exploration-player-page/exploration-player-page.constants'; +import {InteractionAnswer, TextInputAnswer} from 'interactions/answer-defs'; +import {Interaction} from 'domain/exploration/InteractionObjectFactory'; +import {InteractionSpecsService} from 'services/interaction-specs.service'; +import {Outcome} from 'domain/exploration/OutcomeObjectFactory'; +import {PredictionAlgorithmRegistryService} from 'pages/exploration-player-page/services/prediction-algorithm-registry.service'; +import {State} from 'domain/state/StateObjectFactory'; +import {StateClassifierMappingService} from 'pages/exploration-player-page/services/state-classifier-mapping.service'; +import { + InteractionRuleInputs, + TranslatableSetOfNormalizedString, +} from 'interactions/rule-input-defs'; export interface InteractionRulesService { [ruleName: string]: ( - answer: InteractionAnswer, ruleInputs: InteractionRuleInputs) => boolean; + answer: InteractionAnswer, + ruleInputs: InteractionRuleInputs + ) => boolean; } @Injectable({providedIn: 'root'}) export class AnswerClassificationService { constructor( - private alertsService: AlertsService, - private appService: AppService, - private interactionSpecsService: InteractionSpecsService, - private predictionAlgorithmRegistryService: - PredictionAlgorithmRegistryService, - private stateClassifierMappingService: StateClassifierMappingService) {} + private alertsService: AlertsService, + private appService: AppService, + private interactionSpecsService: InteractionSpecsService, + private predictionAlgorithmRegistryService: PredictionAlgorithmRegistryService, + private stateClassifierMappingService: StateClassifierMappingService + ) {} /** * Finds the first answer group with a rule that returns true. @@ -62,10 +66,10 @@ export class AnswerClassificationService { * @return AnswerClassificationResult domain object. */ private classifyAnswer( - answer: InteractionAnswer, - answerGroups: AnswerGroup[], - defaultOutcome: Outcome | null, - interactionRulesService: InteractionRulesService + answer: InteractionAnswer, + answerGroups: AnswerGroup[], + defaultOutcome: Outcome | null, + interactionRulesService: InteractionRulesService ): AnswerClassificationResult { // Find the first group that contains a rule which returns true // TODO(bhenning): Implement training data classification. @@ -75,8 +79,11 @@ export class AnswerClassificationService { const rule = answerGroup.rules[j]; if (interactionRulesService[rule.type](answer, rule.inputs)) { return new AnswerClassificationResult( - answerGroup.outcome, i, j, - ExplorationPlayerConstants.EXPLICIT_CLASSIFICATION); + answerGroup.outcome, + i, + j, + ExplorationPlayerConstants.EXPLICIT_CLASSIFICATION + ); } } } @@ -85,13 +92,18 @@ export class AnswerClassificationService { // returned. Throws an error if the default outcome is not defined. if (defaultOutcome) { return new AnswerClassificationResult( - defaultOutcome, answerGroups.length, 0, - ExplorationPlayerConstants.DEFAULT_OUTCOME_CLASSIFICATION); + defaultOutcome, + answerGroups.length, + 0, + ExplorationPlayerConstants.DEFAULT_OUTCOME_CLASSIFICATION + ); } else { this.alertsService.addWarning( - 'Something went wrong with the exploration.'); + 'Something went wrong with the exploration.' + ); throw new Error( - 'No defaultOutcome was available to classify the answer.'); + 'No defaultOutcome was available to classify the answer.' + ); } } @@ -110,33 +122,40 @@ export class AnswerClassificationService { * @return The resulting AnswerClassificationResult domain object. */ getMatchingClassificationResult( - stateName: string, - interactionInOldState: Interaction, - answer: InteractionAnswer, - interactionRulesService: InteractionRulesService): - AnswerClassificationResult { + stateName: string, + interactionInOldState: Interaction, + answer: InteractionAnswer, + interactionRulesService: InteractionRulesService + ): AnswerClassificationResult { var answerClassificationResult = null; const answerGroups = interactionInOldState.answerGroups; const defaultOutcome = interactionInOldState.defaultOutcome; if (interactionRulesService) { answerClassificationResult = this.classifyAnswer( - answer, answerGroups, defaultOutcome, interactionRulesService); + answer, + answerGroups, + defaultOutcome, + interactionRulesService + ); } else { this.alertsService.addWarning( 'Something went wrong with the exploration: no ' + - 'interactionRulesService was available.'); + 'interactionRulesService was available.' + ); throw new Error( - 'No interactionRulesService was available to classify the answer.'); + 'No interactionRulesService was available to classify the answer.' + ); } - const ruleBasedOutcomeIsDefault = ( - answerClassificationResult.outcome === defaultOutcome); + const ruleBasedOutcomeIsDefault = + answerClassificationResult.outcome === defaultOutcome; let interactionIsTrainable = false; if (interactionInOldState.id !== null) { - interactionIsTrainable = ( + interactionIsTrainable = this.interactionSpecsService.isInteractionTrainable( - interactionInOldState.id)); + interactionInOldState.id + ); } if (ruleBasedOutcomeIsDefault && interactionIsTrainable) { @@ -148,35 +167,49 @@ export class AnswerClassificationService { for (const trainingDatum of answerGroup.trainingData) { if (angular.equals(answer, trainingDatum)) { return new AnswerClassificationResult( - answerGroup.outcome, i, null, - ExplorationPlayerConstants.TRAINING_DATA_CLASSIFICATION); + answerGroup.outcome, + i, + null, + ExplorationPlayerConstants.TRAINING_DATA_CLASSIFICATION + ); } } } if (this.appService.isMachineLearningClassificationEnabled()) { - const classifier = ( - this.stateClassifierMappingService.getClassifier(stateName)); - if (classifier && classifier.classifierData && - classifier.algorithmId && classifier.algorithmVersion) { - const predictionService = ( + const classifier = + this.stateClassifierMappingService.getClassifier(stateName); + if ( + classifier && + classifier.classifierData && + classifier.algorithmId && + classifier.algorithmVersion + ) { + const predictionService = this.predictionAlgorithmRegistryService.getPredictionService( - classifier.algorithmId, classifier.algorithmVersion)); + classifier.algorithmId, + classifier.algorithmVersion + ); // If prediction service exists, we run classifier. We return the // default outcome otherwise. if (predictionService) { const predictedAnswerGroupIndex = predictionService.predict( - classifier.classifierData, answer); + classifier.classifierData, + answer + ); if (predictedAnswerGroupIndex === -1) { - answerClassificationResult = ( - new AnswerClassificationResult( - defaultOutcome as Outcome, answerGroups.length, 0, - ExplorationPlayerConstants.DEFAULT_OUTCOME_CLASSIFICATION)); + answerClassificationResult = new AnswerClassificationResult( + defaultOutcome as Outcome, + answerGroups.length, + 0, + ExplorationPlayerConstants.DEFAULT_OUTCOME_CLASSIFICATION + ); } else { - answerClassificationResult = ( - new AnswerClassificationResult( - answerGroups[predictedAnswerGroupIndex].outcome, - predictedAnswerGroupIndex, null, - ExplorationPlayerConstants.STATISTICAL_CLASSIFICATION)); + answerClassificationResult = new AnswerClassificationResult( + answerGroups[predictedAnswerGroupIndex].outcome, + predictedAnswerGroupIndex, + null, + ExplorationPlayerConstants.STATISTICAL_CLASSIFICATION + ); } } } @@ -187,21 +220,24 @@ export class AnswerClassificationService { } checkForMisspellings( - answer: TextInputAnswer, inputStrings: string[] + answer: TextInputAnswer, + inputStrings: string[] ): boolean { const normalizedAnswer = answer.toLowerCase(); - const normalizedInput = inputStrings.map( - input => input.toLowerCase()); - return normalizedInput.some( - input => this.checkEditDistance( - input, normalizedAnswer, - ExplorationPlayerConstants.THRESHOLD_EDIT_DISTANCE_FOR_MISSPELLINGS)); + const normalizedInput = inputStrings.map(input => input.toLowerCase()); + return normalizedInput.some(input => + this.checkEditDistance( + input, + normalizedAnswer, + ExplorationPlayerConstants.THRESHOLD_EDIT_DISTANCE_FOR_MISSPELLINGS + ) + ); } checkEditDistance( - inputString: string, - matchString: string, - requiredEditDistance: number + inputString: string, + matchString: string, + requiredEditDistance: number ): boolean { if (inputString === matchString) { return true; @@ -218,23 +254,26 @@ export class AnswerClassificationService { if (inputString.charAt(i - 1) === matchString.charAt(j - 1)) { editDistance[i][j] = editDistance[i - 1][j - 1]; } else { - editDistance[i][j] = Math.min( - editDistance[i - 1][j - 1], editDistance[i][j - 1], - editDistance[i - 1][j]) + 1; + editDistance[i][j] = + Math.min( + editDistance[i - 1][j - 1], + editDistance[i][j - 1], + editDistance[i - 1][j] + ) + 1; } } } return ( editDistance[inputString.length][matchString.length] <= - requiredEditDistance); + requiredEditDistance + ); } - isAnswerOnlyMisspelled( - interaction: Interaction, - answer: string - ): boolean { - if (answer.length < - ExplorationPlayerConstants.MIN_ANSWER_LENGTH_TO_CHECK_MISSPELLINGS) { + isAnswerOnlyMisspelled(interaction: Interaction, answer: string): boolean { + if ( + answer.length < + ExplorationPlayerConstants.MIN_ANSWER_LENGTH_TO_CHECK_MISSPELLINGS + ) { return false; } var answerIsMisspelled = false; @@ -245,12 +284,9 @@ export class AnswerClassificationService { for (var j = 0; j < answerGroup.rules.length; ++j) { let rule = answerGroup.rules[j]; let inputStrings = ( - rule.inputs.x as TranslatableSetOfNormalizedString) - .normalizedStrSet; - if (this.checkForMisspellings( - answer, - inputStrings - )) { + rule.inputs.x as TranslatableSetOfNormalizedString + ).normalizedStrSet; + if (this.checkForMisspellings(answer, inputStrings)) { return true; } } @@ -260,17 +296,28 @@ export class AnswerClassificationService { } isClassifiedExplicitlyOrGoesToNewState( - stateName: string, state: State, answer: InteractionAnswer, - interactionRulesService: InteractionRulesService): boolean { + stateName: string, + state: State, + answer: InteractionAnswer, + interactionRulesService: InteractionRulesService + ): boolean { const result = this.getMatchingClassificationResult( - stateName, state.interaction, answer, interactionRulesService); + stateName, + state.interaction, + answer, + interactionRulesService + ); return ( result.outcome.dest !== state.name || result.classificationCategorization !== - ExplorationPlayerConstants.DEFAULT_OUTCOME_CLASSIFICATION); + ExplorationPlayerConstants.DEFAULT_OUTCOME_CLASSIFICATION + ); } } -angular.module('oppia').factory( - 'AnswerClassificationService', - downgradeInjectable(AnswerClassificationService)); +angular + .module('oppia') + .factory( + 'AnswerClassificationService', + downgradeInjectable(AnswerClassificationService) + ); diff --git a/core/templates/pages/exploration-player-page/services/audio-preloader.service.spec.ts b/core/templates/pages/exploration-player-page/services/audio-preloader.service.spec.ts index baae44e89a2f..0e722419f8ad 100644 --- a/core/templates/pages/exploration-player-page/services/audio-preloader.service.spec.ts +++ b/core/templates/pages/exploration-player-page/services/audio-preloader.service.spec.ts @@ -16,14 +16,20 @@ * @fileoverview Unit tests for the audio preloader service. */ -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; -import { ExplorationBackendDict, ExplorationObjectFactory } from 'domain/exploration/ExplorationObjectFactory'; -import { InteractionAnswer } from 'interactions/answer-defs'; -import { AudioPreloaderService } from 'pages/exploration-player-page/services/audio-preloader.service'; -import { AudioTranslationLanguageService } from 'pages/exploration-player-page/services/audio-translation-language.service'; -import { ContextService } from 'services/context.service'; +import { + ExplorationBackendDict, + ExplorationObjectFactory, +} from 'domain/exploration/ExplorationObjectFactory'; +import {InteractionAnswer} from 'interactions/answer-defs'; +import {AudioPreloaderService} from 'pages/exploration-player-page/services/audio-preloader.service'; +import {AudioTranslationLanguageService} from 'pages/exploration-player-page/services/audio-translation-language.service'; +import {ContextService} from 'services/context.service'; describe('Audio preloader service', () => { let httpTestingController: HttpTestingController; @@ -60,7 +66,7 @@ describe('Audio preloader service', () => { param_changes: [], content: { content_id: 'content', - html: '

State 1 Content

' + html: '

State 1 Content

', }, recorded_voiceovers: { voiceovers_mapping: { @@ -69,18 +75,18 @@ describe('Audio preloader service', () => { filename: 'en-2.mp3', file_size_bytes: 120000, needs_update: false, - duration_secs: 1.2 - } + duration_secs: 1.2, + }, }, - default_outcome: {} - } + default_outcome: {}, + }, }, interaction: { id: 'Continue', default_outcome: { feedback: { content_id: 'default_outcome', - html: '' + html: '', }, dest: 'State 3', dest_if_really_stuck: null, @@ -92,12 +98,12 @@ describe('Audio preloader service', () => { confirmed_unclassified_answers: [], customization_args: { buttonText: { - value: 'Continue' - } + value: 'Continue', + }, }, solution: null, answer_groups: [], - hints: [] + hints: [], }, solicit_answer_details: false, card_is_checkpoint: false, @@ -117,10 +123,10 @@ describe('Audio preloader service', () => { filename: 'en-4.mp3', file_size_bytes: 120000, needs_update: false, - duration_secs: 1.2 - } - } - } + duration_secs: 1.2, + }, + }, + }, }, interaction: { id: 'EndExploration', @@ -128,12 +134,12 @@ describe('Audio preloader service', () => { confirmed_unclassified_answers: [], customization_args: { recommendedExplorationIds: { - value: [] - } + value: [], + }, }, solution: null, answer_groups: [], - hints: [] + hints: [], }, solicit_answer_details: false, card_is_checkpoint: false, @@ -144,7 +150,7 @@ describe('Audio preloader service', () => { param_changes: [], content: { content_id: 'content', - html: '

State 2 Content

' + html: '

State 2 Content

', }, recorded_voiceovers: { voiceovers_mapping: { @@ -153,18 +159,18 @@ describe('Audio preloader service', () => { filename: 'en-3.mp3', file_size_bytes: 120000, needs_update: false, - duration_secs: 1.2 - } + duration_secs: 1.2, + }, }, - default_outcome: {} - } + default_outcome: {}, + }, }, interaction: { id: 'Continue', default_outcome: { feedback: { content_id: 'default_outcome', - html: '' + html: '', }, dest: 'State 3', dest_if_really_stuck: null, @@ -176,12 +182,12 @@ describe('Audio preloader service', () => { confirmed_unclassified_answers: [], customization_args: { buttonText: { - value: 'Continue' - } + value: 'Continue', + }, }, solution: null, answer_groups: [], - hints: [] + hints: [], }, solicit_answer_details: false, card_is_checkpoint: false, @@ -201,12 +207,12 @@ describe('Audio preloader service', () => { filename: 'en-1.mp3', file_size_bytes: 120000, needs_update: false, - duration_secs: 1.2 - } + duration_secs: 1.2, + }, }, default_outcome: {}, - feedback_1: {} - } + feedback_1: {}, + }, }, interaction: { id: 'TextInput', @@ -215,7 +221,7 @@ describe('Audio preloader service', () => { dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: '

Try again.

' + html: '

Try again.

', }, labelled_as_correct: false, param_changes: [], @@ -225,68 +231,79 @@ describe('Audio preloader service', () => { confirmed_unclassified_answers: [], customization_args: { rows: { - value: 1 + value: 1, }, placeholder: { - value: '' + value: '', }, catchMisspellings: { - value: false - } + value: false, + }, }, solution: null, - answer_groups: [{ - rule_specs: [{ - rule_type: 'Contains', - inputs: {x: { - contentId: 'rule_input', - normalizedStrSet: ['1'] - }} - }], - outcome: { - dest: 'State 1', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_1', - html: "

Let's go to State 1

" + answer_groups: [ + { + rule_specs: [ + { + rule_type: 'Contains', + inputs: { + x: { + contentId: 'rule_input', + normalizedStrSet: ['1'], + }, + }, + }, + ], + outcome: { + dest: 'State 1', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: "

Let's go to State 1

", + }, + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, }, - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, + training_data: interactionAnswer, + tagged_skill_misconception_id: null, }, - training_data: interactionAnswer, - tagged_skill_misconception_id: null, - }, { - rule_specs: [{ - rule_type: 'Contains', - inputs: {x: { - contentId: 'rule_input', - normalizedStrSet: ['2'] - }} - }], - outcome: { - dest: 'State 2', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: "

Let's go to State 2

" + { + rule_specs: [ + { + rule_type: 'Contains', + inputs: { + x: { + contentId: 'rule_input', + normalizedStrSet: ['2'], + }, + }, + }, + ], + outcome: { + dest: 'State 2', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: "

Let's go to State 2

", + }, + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, }, - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, + training_data: interactionAnswer, + tagged_skill_misconception_id: null, }, - training_data: interactionAnswer, - tagged_skill_misconception_id: null, - }], - hints: [] + ], + hints: [], }, solicit_answer_details: false, card_is_checkpoint: true, linked_skill_id: null, classifier_model_id: null, - } + }, }, param_specs: {}, param_changes: [], @@ -303,8 +320,8 @@ describe('Audio preloader service', () => { param_specs: {}, param_changes: [], auto_tts_enabled: false, - edits_allowed: true - } + edits_allowed: true, + }, }; let requestUrl1 = '/assetsdevhandler/exploration/1/assets/audio/en-1.mp3'; let requestUrl2 = '/assetsdevhandler/exploration/1/assets/audio/en-2.mp3'; @@ -314,105 +331,121 @@ describe('Audio preloader service', () => { beforeEach(() => { audioPreloaderService = TestBed.inject(AudioPreloaderService); audioPreloaderService.setAudioLoadedCallback((_: string): void => {}); - audioTranslationLanguageService = ( - TestBed.inject(AudioTranslationLanguageService)); + audioTranslationLanguageService = TestBed.inject( + AudioTranslationLanguageService + ); explorationObjectFactory = TestBed.inject(ExplorationObjectFactory); contextService = TestBed.inject(ContextService); spyOn(contextService, 'getExplorationId').and.returnValue('1'); }); - it('should maintain the correct number of download requests in queue', - fakeAsync(() => { - const exploration = ( - explorationObjectFactory.createFromBackendDict(explorationDict)); - audioPreloaderService.init(exploration); - audioTranslationLanguageService.init(['en'], 'en', 'en', false); - audioPreloaderService.kickOffAudioPreloader( - exploration.getInitialState().name as string); + it('should maintain the correct number of download requests in queue', fakeAsync(() => { + const exploration = + explorationObjectFactory.createFromBackendDict(explorationDict); + audioPreloaderService.init(exploration); + audioTranslationLanguageService.init(['en'], 'en', 'en', false); + audioPreloaderService.kickOffAudioPreloader( + exploration.getInitialState().name as string + ); - expect(audioPreloaderService.getFilenamesOfAudioCurrentlyDownloading()) - .toEqual(['en-1.mp3', 'en-2.mp3', 'en-3.mp3']); - expect(audioPreloaderService.isLoadingAudioFile('en-1.mp3')).toBeTrue(); - expect(audioPreloaderService.isLoadingAudioFile('en-2.mp3')).toBeTrue(); - expect(audioPreloaderService.isLoadingAudioFile('en-3.mp3')).toBeTrue(); - expect(audioPreloaderService.isLoadingAudioFile('en-4.mp3')).toBeFalse(); + expect( + audioPreloaderService.getFilenamesOfAudioCurrentlyDownloading() + ).toEqual(['en-1.mp3', 'en-2.mp3', 'en-3.mp3']); + expect(audioPreloaderService.isLoadingAudioFile('en-1.mp3')).toBeTrue(); + expect(audioPreloaderService.isLoadingAudioFile('en-2.mp3')).toBeTrue(); + expect(audioPreloaderService.isLoadingAudioFile('en-3.mp3')).toBeTrue(); + expect(audioPreloaderService.isLoadingAudioFile('en-4.mp3')).toBeFalse(); - httpTestingController.expectOne(requestUrl1).flush(audioBlob); - flushMicrotasks(); + httpTestingController.expectOne(requestUrl1).flush(audioBlob); + flushMicrotasks(); - expect(audioPreloaderService.getFilenamesOfAudioCurrentlyDownloading()) - .toEqual(['en-2.mp3', 'en-3.mp3', 'en-4.mp3']); - expect(audioPreloaderService.isLoadingAudioFile('en-4.mp3')).toBeTrue(); + expect( + audioPreloaderService.getFilenamesOfAudioCurrentlyDownloading() + ).toEqual(['en-2.mp3', 'en-3.mp3', 'en-4.mp3']); + expect(audioPreloaderService.isLoadingAudioFile('en-4.mp3')).toBeTrue(); - httpTestingController.expectOne(requestUrl2).flush(audioBlob); - flushMicrotasks(); + httpTestingController.expectOne(requestUrl2).flush(audioBlob); + flushMicrotasks(); - expect(audioPreloaderService.getFilenamesOfAudioCurrentlyDownloading()) - .toEqual(['en-3.mp3', 'en-4.mp3']); + expect( + audioPreloaderService.getFilenamesOfAudioCurrentlyDownloading() + ).toEqual(['en-3.mp3', 'en-4.mp3']); - httpTestingController.expectOne(requestUrl3).flush(audioBlob); - flushMicrotasks(); + httpTestingController.expectOne(requestUrl3).flush(audioBlob); + flushMicrotasks(); - expect(audioPreloaderService.getFilenamesOfAudioCurrentlyDownloading()) - .toEqual(['en-4.mp3']); + expect( + audioPreloaderService.getFilenamesOfAudioCurrentlyDownloading() + ).toEqual(['en-4.mp3']); - httpTestingController.expectOne(requestUrl4).flush(audioBlob); - flushMicrotasks(); + httpTestingController.expectOne(requestUrl4).flush(audioBlob); + flushMicrotasks(); - expect(audioPreloaderService.getFilenamesOfAudioCurrentlyDownloading()) - .toEqual([]); - })); + expect( + audioPreloaderService.getFilenamesOfAudioCurrentlyDownloading() + ).toEqual([]); + })); it('should return empty audioFiles list if language code is null', () => { - spyOn(audioTranslationLanguageService, 'getCurrentAudioLanguageCode') - .and.returnValue(null); + spyOn( + audioTranslationLanguageService, + 'getCurrentAudioLanguageCode' + ).and.returnValue(null); - const exploration = ( - explorationObjectFactory.createFromBackendDict(explorationDict)); + const exploration = + explorationObjectFactory.createFromBackendDict(explorationDict); audioPreloaderService.init(exploration); audioTranslationLanguageService.init(['en'], 'en', 'en', false); audioPreloaderService.kickOffAudioPreloader( - exploration.getInitialState().name as string); + exploration.getInitialState().name as string + ); - expect(audioPreloaderService.getFilenamesOfAudioCurrentlyDownloading()) - .toEqual([]); + expect( + audioPreloaderService.getFilenamesOfAudioCurrentlyDownloading() + ).toEqual([]); }); it('should properly restart pre-loading from a new state', () => { - const exploration = ( - explorationObjectFactory.createFromBackendDict(explorationDict)); + const exploration = + explorationObjectFactory.createFromBackendDict(explorationDict); audioPreloaderService.init(exploration); audioTranslationLanguageService.init(['en'], 'en', 'en', false); audioPreloaderService.kickOffAudioPreloader( - exploration.getInitialState().name as string); + exploration.getInitialState().name as string + ); httpTestingController.expectOne(requestUrl1); httpTestingController.expectOne(requestUrl2); httpTestingController.expectOne(requestUrl3); - expect(audioPreloaderService.getFilenamesOfAudioCurrentlyDownloading()) - .toEqual(['en-1.mp3', 'en-2.mp3', 'en-3.mp3']); + expect( + audioPreloaderService.getFilenamesOfAudioCurrentlyDownloading() + ).toEqual(['en-1.mp3', 'en-2.mp3', 'en-3.mp3']); audioPreloaderService.restartAudioPreloader('State 3'); httpTestingController.expectOne(requestUrl4); - expect(audioPreloaderService.getFilenamesOfAudioCurrentlyDownloading()) - .toEqual(['en-4.mp3']); + expect( + audioPreloaderService.getFilenamesOfAudioCurrentlyDownloading() + ).toEqual(['en-4.mp3']); audioPreloaderService.restartAudioPreloader('State 2'); httpTestingController.expectOne(requestUrl3); httpTestingController.expectOne(requestUrl4); - expect(audioPreloaderService.getFilenamesOfAudioCurrentlyDownloading()) - .toEqual(['en-3.mp3', 'en-4.mp3']); + expect( + audioPreloaderService.getFilenamesOfAudioCurrentlyDownloading() + ).toEqual(['en-3.mp3', 'en-4.mp3']); }); it('should properly set most recently requested audio filename', () => { audioPreloaderService.clearMostRecentlyRequestedAudioFilename(); - expect(audioPreloaderService.getMostRecentlyRequestedAudioFilename()) - .toEqual(null); + expect( + audioPreloaderService.getMostRecentlyRequestedAudioFilename() + ).toEqual(null); var filename = 'test_file'; audioPreloaderService.setMostRecentlyRequestedAudioFilename(filename); - expect(audioPreloaderService.getMostRecentlyRequestedAudioFilename()) - .toEqual(filename); + expect( + audioPreloaderService.getMostRecentlyRequestedAudioFilename() + ).toEqual(filename); }); }); diff --git a/core/templates/pages/exploration-player-page/services/audio-preloader.service.ts b/core/templates/pages/exploration-player-page/services/audio-preloader.service.ts index 240cd916cf6a..cd9bda41b401 100644 --- a/core/templates/pages/exploration-player-page/services/audio-preloader.service.ts +++ b/core/templates/pages/exploration-player-page/services/audio-preloader.service.ts @@ -16,18 +16,18 @@ * @fileoverview Service to preload audio into AssetsBackendApiService's cache. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; -import { AppConstants } from 'app.constants'; -import { Exploration } from 'domain/exploration/ExplorationObjectFactory'; -import { AudioTranslationLanguageService } from 'pages/exploration-player-page/services/audio-translation-language.service'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; -import { ComputeGraphService } from 'services/compute-graph.service'; -import { ContextService } from 'services/context.service'; +import {AppConstants} from 'app.constants'; +import {Exploration} from 'domain/exploration/ExplorationObjectFactory'; +import {AudioTranslationLanguageService} from 'pages/exploration-player-page/services/audio-translation-language.service'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; +import {ComputeGraphService} from 'services/compute-graph.service'; +import {ContextService} from 'services/context.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class AudioPreloaderService { private filenamesOfAudioCurrentlyDownloading: string[] = []; @@ -43,24 +43,27 @@ export class AudioPreloaderService { private mostRecentlyRequestedAudioFilename: string | null = null; constructor( - private assetsBackendApiService: AssetsBackendApiService, - private audioTranslationLanguageService: AudioTranslationLanguageService, - private computeGraphService: ComputeGraphService, - private contextService: ContextService) {} + private assetsBackendApiService: AssetsBackendApiService, + private audioTranslationLanguageService: AudioTranslationLanguageService, + private computeGraphService: ComputeGraphService, + private contextService: ContextService + ) {} init(exploration: Exploration): void { this.exploration = exploration; } kickOffAudioPreloader(sourceStateName: string): void { - this.filenamesOfAudioToBeDownloaded = ( - this.getAudioFilenamesInBfsOrder(sourceStateName)); - const numFilesToDownload = ( + this.filenamesOfAudioToBeDownloaded = + this.getAudioFilenamesInBfsOrder(sourceStateName); + const numFilesToDownload = AppConstants.MAX_NUM_AUDIO_FILES_TO_DOWNLOAD_SIMULTANEOUSLY - - this.filenamesOfAudioCurrentlyDownloading.length); + this.filenamesOfAudioCurrentlyDownloading.length; if (numFilesToDownload > 0) { - const filesToDownload = ( - this.filenamesOfAudioToBeDownloaded.splice(0, numFilesToDownload)); + const filesToDownload = this.filenamesOfAudioToBeDownloaded.splice( + 0, + numFilesToDownload + ); filesToDownload.forEach(filename => this.loadAudio(filename)); this.filenamesOfAudioCurrentlyDownloading.push(...filesToDownload); } @@ -98,8 +101,8 @@ export class AudioPreloaderService { } private getAudioFilenamesInBfsOrder(sourceStateName: string): string[] { - const languageCode = ( - this.audioTranslationLanguageService.getCurrentAudioLanguageCode()); + const languageCode = + this.audioTranslationLanguageService.getCurrentAudioLanguageCode(); // If the language code is not selected then there are no audio // files available, so we directly return empty array. if (languageCode === null) { @@ -109,10 +112,12 @@ export class AudioPreloaderService { const initialStateName = this.exploration.getInitialState().name; let bfsTraversalOfStates: string[] = []; if (initialStateName !== null) { - bfsTraversalOfStates = ( + bfsTraversalOfStates = this.computeGraphService.computeBfsTraversalOfStates( - initialStateName, this.exploration.getStates(), - sourceStateName)); + initialStateName, + this.exploration.getStates(), + sourceStateName + ); } const audioFilenamesInBfsOrder = []; for (const stateName of bfsTraversalOfStates) { @@ -124,27 +129,28 @@ export class AudioPreloaderService { } private loadAudio(audioFilename: string): void { - this.assetsBackendApiService.loadAudio( - this.contextService.getExplorationId(), audioFilename - ).then(loadedAudio => { - const index = this.filenamesOfAudioCurrentlyDownloading.findIndex( - filename => filename === loadedAudio.filename); - if (index !== -1) { - this.filenamesOfAudioCurrentlyDownloading.splice(index, 1); - } + this.assetsBackendApiService + .loadAudio(this.contextService.getExplorationId(), audioFilename) + .then(loadedAudio => { + const index = this.filenamesOfAudioCurrentlyDownloading.findIndex( + filename => filename === loadedAudio.filename + ); + if (index !== -1) { + this.filenamesOfAudioCurrentlyDownloading.splice(index, 1); + } - if (this.filenamesOfAudioToBeDownloaded.length > 0) { - const nextAudioFilename = this.filenamesOfAudioToBeDownloaded.shift(); - if (nextAudioFilename !== undefined) { - this.loadAudio(nextAudioFilename); - this.filenamesOfAudioCurrentlyDownloading.push(nextAudioFilename); + if (this.filenamesOfAudioToBeDownloaded.length > 0) { + const nextAudioFilename = this.filenamesOfAudioToBeDownloaded.shift(); + if (nextAudioFilename !== undefined) { + this.loadAudio(nextAudioFilename); + this.filenamesOfAudioCurrentlyDownloading.push(nextAudioFilename); + } } - } - if (this.audioLoadedCallback) { - this.audioLoadedCallback(loadedAudio.filename); - } - }); + if (this.audioLoadedCallback) { + this.audioLoadedCallback(loadedAudio.filename); + } + }); } private cancelPreloading(): void { @@ -153,5 +159,6 @@ export class AudioPreloaderService { } } -angular.module('oppia').factory( - 'AudioPreloaderService', downgradeInjectable(AudioPreloaderService)); +angular + .module('oppia') + .factory('AudioPreloaderService', downgradeInjectable(AudioPreloaderService)); diff --git a/core/templates/pages/exploration-player-page/services/audio-translation-language.service.spec.ts b/core/templates/pages/exploration-player-page/services/audio-translation-language.service.spec.ts index 4a22c8a70be7..adc1dbf4078b 100644 --- a/core/templates/pages/exploration-player-page/services/audio-translation-language.service.spec.ts +++ b/core/templates/pages/exploration-player-page/services/audio-translation-language.service.spec.ts @@ -16,10 +16,9 @@ * @fileoverview Unit tests for the audio translation language service. */ -import { TestBed } from '@angular/core/testing'; +import {TestBed} from '@angular/core/testing'; -import { AudioTranslationLanguageService } from - 'pages/exploration-player-page/services/audio-translation-language.service'; +import {AudioTranslationLanguageService} from 'pages/exploration-player-page/services/audio-translation-language.service'; describe('Audio translation language service', () => { let atls: AudioTranslationLanguageService; @@ -28,218 +27,297 @@ describe('Audio translation language service', () => { beforeEach(() => { atls = TestBed.get(AudioTranslationLanguageService); - spyOn(window.speechSynthesis, 'getVoices').and.returnValue([{ - 'default': false, - lang: 'en-US', - localService: false, - name: 'US English', - voiceURI: 'US English' - }]); + spyOn(window.speechSynthesis, 'getVoices').and.returnValue([ + { + default: false, + lang: 'en-US', + localService: false, + name: 'US English', + voiceURI: 'US English', + }, + ]); }); - it('should properly initialize the current audio language when ' + - 'a preferred language is set', () => { - allAudioLanguageCodesInExploration = ['hi-en', 'en']; - let preferredLanguageCode = 'hi-en'; - let explorationLanguageCode = 'hi'; - const automaticTextToSpeechEnabled = false; - - atls.init( - allAudioLanguageCodesInExploration, preferredLanguageCode, - explorationLanguageCode, automaticTextToSpeechEnabled); - expect(atls.isAutomaticTextToSpeechEnabled()).toBe( - automaticTextToSpeechEnabled); - expect(atls.getCurrentAudioLanguageCode()).toEqual('hi-en'); - expect(atls.getAllAudioLanguageCodesInExploration()).toBe( - allAudioLanguageCodesInExploration); - atls.clearCurrentAudioLanguageCode(); - - allAudioLanguageCodesInExploration = ['hi-en', 'en']; - preferredLanguageCode = 'en'; - explorationLanguageCode = 'hi'; + it( + 'should properly initialize the current audio language when ' + + 'a preferred language is set', + () => { + allAudioLanguageCodesInExploration = ['hi-en', 'en']; + let preferredLanguageCode = 'hi-en'; + let explorationLanguageCode = 'hi'; + const automaticTextToSpeechEnabled = false; - atls.init( - allAudioLanguageCodesInExploration, preferredLanguageCode, - explorationLanguageCode, automaticTextToSpeechEnabled); - expect(atls.getCurrentAudioLanguageCode()).toEqual('en'); - atls.clearCurrentAudioLanguageCode(); + atls.init( + allAudioLanguageCodesInExploration, + preferredLanguageCode, + explorationLanguageCode, + automaticTextToSpeechEnabled + ); + expect(atls.isAutomaticTextToSpeechEnabled()).toBe( + automaticTextToSpeechEnabled + ); + expect(atls.getCurrentAudioLanguageCode()).toEqual('hi-en'); + expect(atls.getAllAudioLanguageCodesInExploration()).toBe( + allAudioLanguageCodesInExploration + ); + atls.clearCurrentAudioLanguageCode(); - allAudioLanguageCodesInExploration = ['hi-en']; - preferredLanguageCode = 'en'; - explorationLanguageCode = 'hi'; + allAudioLanguageCodesInExploration = ['hi-en', 'en']; + preferredLanguageCode = 'en'; + explorationLanguageCode = 'hi'; - atls.init( - allAudioLanguageCodesInExploration, preferredLanguageCode, - explorationLanguageCode, false); - expect(atls.getCurrentAudioLanguageCode()).toEqual('hi-en'); - expect(atls.getAllAudioLanguageCodesInExploration()).toBe( - allAudioLanguageCodesInExploration); - }); + atls.init( + allAudioLanguageCodesInExploration, + preferredLanguageCode, + explorationLanguageCode, + automaticTextToSpeechEnabled + ); + expect(atls.getCurrentAudioLanguageCode()).toEqual('en'); + atls.clearCurrentAudioLanguageCode(); - it('should initialize the current audio language when ' + - 'no preferred language is set and the exploration contains an audio ' + - 'language that is related to the exploration language', () => { - allAudioLanguageCodesInExploration = ['hi-en', 'en']; - const preferredLanguageCode = null; - const explorationLanguageCode = 'hi'; - atls.init( - allAudioLanguageCodesInExploration, preferredLanguageCode, - explorationLanguageCode, false); - expect(atls.getCurrentAudioLanguageCode()).toEqual('hi-en'); - expect(atls.getAllAudioLanguageCodesInExploration()).toBe( - allAudioLanguageCodesInExploration); - }); + allAudioLanguageCodesInExploration = ['hi-en']; + preferredLanguageCode = 'en'; + explorationLanguageCode = 'hi'; - it('should initialize the current audio language when ' + - 'no preferred language is set and the exploration contains an audio ' + - 'language that is no related to the exploration language', () => { - allAudioLanguageCodesInExploration = ['hi', 'en']; - const preferredLanguageCode = null; - const explorationLanguageCode = 'hi-en'; - atls.init( - allAudioLanguageCodesInExploration, preferredLanguageCode, - explorationLanguageCode, false); - expect(atls.getCurrentAudioLanguageCode()).toEqual('hi'); - expect(atls.getAllAudioLanguageCodesInExploration()).toBe( - allAudioLanguageCodesInExploration); - }); + atls.init( + allAudioLanguageCodesInExploration, + preferredLanguageCode, + explorationLanguageCode, + false + ); + expect(atls.getCurrentAudioLanguageCode()).toEqual('hi-en'); + expect(atls.getAllAudioLanguageCodesInExploration()).toBe( + allAudioLanguageCodesInExploration + ); + } + ); - it('should initialize the current audio language to the most ' + - 'relevant language when multiple audio languages are related ' + - 'to the exploration language', () => { - allAudioLanguageCodesInExploration = ['hi-en', 'en']; - const preferredLanguageCode = null; - const explorationLanguageCode = 'en'; - const languageOptions = [{ - value: 'hi-en', - displayed: 'Hinglish' - }, { - value: 'en', - displayed: 'English' - }]; - atls.init( - allAudioLanguageCodesInExploration, preferredLanguageCode, - explorationLanguageCode, false); - expect(atls.getCurrentAudioLanguageCode()).toEqual('en'); - expect(atls.getAllAudioLanguageCodesInExploration()).toBe( - allAudioLanguageCodesInExploration); - expect(atls.getLanguageOptionsForDropdown()).toEqual(languageOptions); - }); + it( + 'should initialize the current audio language when ' + + 'no preferred language is set and the exploration contains an audio ' + + 'language that is related to the exploration language', + () => { + allAudioLanguageCodesInExploration = ['hi-en', 'en']; + const preferredLanguageCode = null; + const explorationLanguageCode = 'hi'; + atls.init( + allAudioLanguageCodesInExploration, + preferredLanguageCode, + explorationLanguageCode, + false + ); + expect(atls.getCurrentAudioLanguageCode()).toEqual('hi-en'); + expect(atls.getAllAudioLanguageCodesInExploration()).toBe( + allAudioLanguageCodesInExploration + ); + } + ); - it('should not have any audio language option when no audio is available ' + - 'and automatic Text-to-speech is disabled in an exploration', () => { - allAudioLanguageCodesInExploration = []; - const explorationLanguageCode = 'en'; - const automaticTextToSpeechEnabled = false; + it( + 'should initialize the current audio language when ' + + 'no preferred language is set and the exploration contains an audio ' + + 'language that is no related to the exploration language', + () => { + allAudioLanguageCodesInExploration = ['hi', 'en']; + const preferredLanguageCode = null; + const explorationLanguageCode = 'hi-en'; + atls.init( + allAudioLanguageCodesInExploration, + preferredLanguageCode, + explorationLanguageCode, + false + ); + expect(atls.getCurrentAudioLanguageCode()).toEqual('hi'); + expect(atls.getAllAudioLanguageCodesInExploration()).toBe( + allAudioLanguageCodesInExploration + ); + } + ); - // When preferredLanguageCode is set. - let preferredLanguageCode: string | null = 'hi'; - atls.init( - allAudioLanguageCodesInExploration, preferredLanguageCode, - explorationLanguageCode, automaticTextToSpeechEnabled); - expect(atls.getCurrentAudioLanguageCode()).toEqual(null); - expect(atls.getLanguageOptionsForDropdown()).toEqual([]); - atls.clearCurrentAudioLanguageCode(); + it( + 'should initialize the current audio language to the most ' + + 'relevant language when multiple audio languages are related ' + + 'to the exploration language', + () => { + allAudioLanguageCodesInExploration = ['hi-en', 'en']; + const preferredLanguageCode = null; + const explorationLanguageCode = 'en'; + const languageOptions = [ + { + value: 'hi-en', + displayed: 'Hinglish', + }, + { + value: 'en', + displayed: 'English', + }, + ]; + atls.init( + allAudioLanguageCodesInExploration, + preferredLanguageCode, + explorationLanguageCode, + false + ); + expect(atls.getCurrentAudioLanguageCode()).toEqual('en'); + expect(atls.getAllAudioLanguageCodesInExploration()).toBe( + allAudioLanguageCodesInExploration + ); + expect(atls.getLanguageOptionsForDropdown()).toEqual(languageOptions); + } + ); - // When preferredLanguageCode is not set. - preferredLanguageCode = null; - atls.init( - allAudioLanguageCodesInExploration, preferredLanguageCode, - explorationLanguageCode, automaticTextToSpeechEnabled); - expect(atls.getCurrentAudioLanguageCode()).toEqual(null); - expect(atls.getLanguageOptionsForDropdown()).toEqual([]); - }); + it( + 'should not have any audio language option when no audio is available ' + + 'and automatic Text-to-speech is disabled in an exploration', + () => { + allAudioLanguageCodesInExploration = []; + const explorationLanguageCode = 'en'; + const automaticTextToSpeechEnabled = false; - it('should initialize the current audio language when audio is available ' + - 'and automatic Text-to-speech is enabled in an exploration', () => { - allAudioLanguageCodesInExploration = []; - const explorationLanguageCode = 'en'; - const preferredLanguageCode = null; - const automaticTextToSpeechEnabled = true; - const languageOptions = [{ - value: 'en-auto', - displayed: 'English (auto)', - }]; - atls.init( - allAudioLanguageCodesInExploration, preferredLanguageCode, - explorationLanguageCode, automaticTextToSpeechEnabled); - expect(atls.isAutogeneratedAudioAllowed()).toBe(true); - expect(atls.isAutogeneratedLanguageCodeSelected()).toBe(true); - expect(atls.getSpeechSynthesisLanguageCode()).toBe('en-US'); - expect(atls.getCurrentAudioLanguageCode()).toEqual('en-auto'); - expect(atls.getLanguageOptionsForDropdown()).toEqual(languageOptions); - }); + // When preferredLanguageCode is set. + let preferredLanguageCode: string | null = 'hi'; + atls.init( + allAudioLanguageCodesInExploration, + preferredLanguageCode, + explorationLanguageCode, + automaticTextToSpeechEnabled + ); + expect(atls.getCurrentAudioLanguageCode()).toEqual(null); + expect(atls.getLanguageOptionsForDropdown()).toEqual([]); + atls.clearCurrentAudioLanguageCode(); - it('should correctly identify if autogenerated language code ' + - 'is selected', () => { - allAudioLanguageCodesInExploration = []; - const explorationLanguageCode = 'en'; - const preferredLanguageCode = null; - const automaticTextToSpeechEnabled = true; - atls.init( - allAudioLanguageCodesInExploration, preferredLanguageCode, - explorationLanguageCode, automaticTextToSpeechEnabled); - expect(atls.isAutogeneratedAudioAllowed()).toBe(true); - expect(atls.isAutogeneratedLanguageCodeSelected()).toBe(true); - atls.clearCurrentAudioLanguageCode(); - expect(atls.isAutogeneratedLanguageCodeSelected()).toBe(false); - }); + // When preferredLanguageCode is not set. + preferredLanguageCode = null; + atls.init( + allAudioLanguageCodesInExploration, + preferredLanguageCode, + explorationLanguageCode, + automaticTextToSpeechEnabled + ); + expect(atls.getCurrentAudioLanguageCode()).toEqual(null); + expect(atls.getLanguageOptionsForDropdown()).toEqual([]); + } + ); - it('should get correct speech synthesis language code', + it( + 'should initialize the current audio language when audio is available ' + + 'and automatic Text-to-speech is enabled in an exploration', () => { - expect(atls.getSpeechSynthesisLanguageCode()).toBe(null); allAudioLanguageCodesInExploration = []; - let explorationLanguageCode = 'en'; + const explorationLanguageCode = 'en'; const preferredLanguageCode = null; const automaticTextToSpeechEnabled = true; + const languageOptions = [ + { + value: 'en-auto', + displayed: 'English (auto)', + }, + ]; atls.init( - allAudioLanguageCodesInExploration, preferredLanguageCode, - explorationLanguageCode, automaticTextToSpeechEnabled); + allAudioLanguageCodesInExploration, + preferredLanguageCode, + explorationLanguageCode, + automaticTextToSpeechEnabled + ); + expect(atls.isAutogeneratedAudioAllowed()).toBe(true); + expect(atls.isAutogeneratedLanguageCodeSelected()).toBe(true); expect(atls.getSpeechSynthesisLanguageCode()).toBe('en-US'); - }); - - it('should initialize the current audio language when exploration has ' + - 'audio language codes and audio is available and automatic ' + - 'Text-to-speech is enabled in it', () => { - allAudioLanguageCodesInExploration = ['hi-en', 'en']; - const explorationLanguageCode = 'en'; - const preferredLanguageCode = null; - const languageOptions = [{ - value: 'hi-en', - displayed: 'Hinglish' - }, { - value: 'en', - displayed: 'English' - }, { - value: 'en-auto', - displayed: 'English (auto)', - }]; - atls.init( - allAudioLanguageCodesInExploration, preferredLanguageCode, - explorationLanguageCode, true); - expect(atls.getCurrentAudioLanguageCode()).toEqual('en'); - expect(atls.getLanguageOptionsForDropdown()).toEqual(languageOptions); - }); + expect(atls.getCurrentAudioLanguageCode()).toEqual('en-auto'); + expect(atls.getLanguageOptionsForDropdown()).toEqual(languageOptions); + } + ); - it('should get speech synthesis language code when userAgent is mobile', + it( + 'should correctly identify if autogenerated language code ' + 'is selected', () => { - spyOnProperty(navigator, 'userAgent').and.returnValue('iPhone'); allAudioLanguageCodesInExploration = []; const explorationLanguageCode = 'en'; const preferredLanguageCode = null; const automaticTextToSpeechEnabled = true; atls.init( - allAudioLanguageCodesInExploration, preferredLanguageCode, - explorationLanguageCode, automaticTextToSpeechEnabled); + allAudioLanguageCodesInExploration, + preferredLanguageCode, + explorationLanguageCode, + automaticTextToSpeechEnabled + ); expect(atls.isAutogeneratedAudioAllowed()).toBe(true); expect(atls.isAutogeneratedLanguageCodeSelected()).toBe(true); - expect(atls.getSpeechSynthesisLanguageCode()).toBe('en_US'); - expect(atls.getCurrentAudioLanguageCode()).toEqual('en-auto'); - expect(atls.getLanguageOptionsForDropdown()).toEqual([{ + atls.clearCurrentAudioLanguageCode(); + expect(atls.isAutogeneratedLanguageCodeSelected()).toBe(false); + } + ); + + it('should get correct speech synthesis language code', () => { + expect(atls.getSpeechSynthesisLanguageCode()).toBe(null); + allAudioLanguageCodesInExploration = []; + let explorationLanguageCode = 'en'; + const preferredLanguageCode = null; + const automaticTextToSpeechEnabled = true; + atls.init( + allAudioLanguageCodesInExploration, + preferredLanguageCode, + explorationLanguageCode, + automaticTextToSpeechEnabled + ); + expect(atls.getSpeechSynthesisLanguageCode()).toBe('en-US'); + }); + + it( + 'should initialize the current audio language when exploration has ' + + 'audio language codes and audio is available and automatic ' + + 'Text-to-speech is enabled in it', + () => { + allAudioLanguageCodesInExploration = ['hi-en', 'en']; + const explorationLanguageCode = 'en'; + const preferredLanguageCode = null; + const languageOptions = [ + { + value: 'hi-en', + displayed: 'Hinglish', + }, + { + value: 'en', + displayed: 'English', + }, + { + value: 'en-auto', + displayed: 'English (auto)', + }, + ]; + atls.init( + allAudioLanguageCodesInExploration, + preferredLanguageCode, + explorationLanguageCode, + true + ); + expect(atls.getCurrentAudioLanguageCode()).toEqual('en'); + expect(atls.getLanguageOptionsForDropdown()).toEqual(languageOptions); + } + ); + + it('should get speech synthesis language code when userAgent is mobile', () => { + spyOnProperty(navigator, 'userAgent').and.returnValue('iPhone'); + allAudioLanguageCodesInExploration = []; + const explorationLanguageCode = 'en'; + const preferredLanguageCode = null; + const automaticTextToSpeechEnabled = true; + atls.init( + allAudioLanguageCodesInExploration, + preferredLanguageCode, + explorationLanguageCode, + automaticTextToSpeechEnabled + ); + expect(atls.isAutogeneratedAudioAllowed()).toBe(true); + expect(atls.isAutogeneratedLanguageCodeSelected()).toBe(true); + expect(atls.getSpeechSynthesisLanguageCode()).toBe('en_US'); + expect(atls.getCurrentAudioLanguageCode()).toEqual('en-auto'); + expect(atls.getLanguageOptionsForDropdown()).toEqual([ + { value: 'en-auto', displayed: 'English (auto)', - }]); - }); + }, + ]); + }); it('should change current audio language after initialization', () => { allAudioLanguageCodesInExploration = ['hi-en', 'en']; @@ -247,15 +325,20 @@ describe('Audio translation language service', () => { const explorationLanguageCode = 'hi'; const automaticTextToSpeechEnabled = false; atls.init( - allAudioLanguageCodesInExploration, preferredLanguageCode, - explorationLanguageCode, automaticTextToSpeechEnabled); + allAudioLanguageCodesInExploration, + preferredLanguageCode, + explorationLanguageCode, + automaticTextToSpeechEnabled + ); expect(atls.getCurrentAudioLanguageCode()).toEqual('hi-en'); expect(atls.getAllAudioLanguageCodesInExploration()).toBe( - allAudioLanguageCodesInExploration); + allAudioLanguageCodesInExploration + ); atls.setCurrentAudioLanguageCode('en'); expect(atls.getCurrentAudioLanguageCode()).toEqual('en'); expect(atls.getAllAudioLanguageCodesInExploration()).toBe( - allAudioLanguageCodesInExploration); + allAudioLanguageCodesInExploration + ); }); it('should get the current language description correctly', () => { @@ -263,8 +346,11 @@ describe('Audio translation language service', () => { const preferredLanguageCode = 'hi-en'; const explorationLanguageCode = 'hi'; atls.init( - allAudioLanguageCodesInExploration, preferredLanguageCode, - explorationLanguageCode, false); + allAudioLanguageCodesInExploration, + preferredLanguageCode, + explorationLanguageCode, + false + ); expect(atls.getCurrentAudioLanguageCode()).toEqual('hi-en'); expect(atls.getCurrentAudioLanguageDescription()).toBe('Hinglish'); atls.clearCurrentAudioLanguageCode(); diff --git a/core/templates/pages/exploration-player-page/services/audio-translation-language.service.ts b/core/templates/pages/exploration-player-page/services/audio-translation-language.service.ts index 0b4e3aa1037d..2e548389f262 100644 --- a/core/templates/pages/exploration-player-page/services/audio-translation-language.service.ts +++ b/core/templates/pages/exploration-player-page/services/audio-translation-language.service.ts @@ -17,12 +17,11 @@ * used for audio translations. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { BrowserCheckerService } from - 'domain/utilities/browser-checker.service'; -import { LanguageUtilService } from 'domain/utilities/language-util.service'; +import {BrowserCheckerService} from 'domain/utilities/browser-checker.service'; +import {LanguageUtilService} from 'domain/utilities/language-util.service'; export interface ExplorationLanguageInfo { /** @@ -35,12 +34,13 @@ export interface ExplorationLanguageInfo { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class AudioTranslationLanguageService { constructor( private browserCheckerService: BrowserCheckerService, - private languageUtilService: LanguageUtilService) {} + private languageUtilService: LanguageUtilService + ) {} _currentAudioLanguageCode: string | null = null; _allAudioLanguageCodesInExploration: string[] = []; @@ -58,30 +58,38 @@ export class AudioTranslationLanguageService { (audioLanguageCode: string) => { let relatedLanguageCodes = this.languageUtilService.getLanguageCodesRelatedToAudioLanguageCode( - audioLanguageCode); - if (relatedLanguageCodes.length < numRelatedLanguages && + audioLanguageCode + ); + if ( + relatedLanguageCodes.length < numRelatedLanguages && this._explorationLanguageCode && - relatedLanguageCodes.indexOf(this._explorationLanguageCode) !== -1) { + relatedLanguageCodes.indexOf(this._explorationLanguageCode) !== -1 + ) { this._currentAudioLanguageCode = audioLanguageCode; numRelatedLanguages = relatedLanguageCodes.length; } - }); + } + ); } _isAutogeneratedAudioAllowed(): boolean { - return this._automaticTextToSpeechEnabled && - this._explorationLanguageCode !== null && - this.languageUtilService.supportsAutogeneratedAudio( - this._explorationLanguageCode); + return ( + this._automaticTextToSpeechEnabled && + this._explorationLanguageCode !== null && + this.languageUtilService.supportsAutogeneratedAudio( + this._explorationLanguageCode + ) + ); } _init( - allAudioLanguageCodesInExploration: string[], - preferredAudioLanguageCode: string | null, - explorationLanguageCode: string, - automaticTextToSpeechEnabled: boolean): void { + allAudioLanguageCodesInExploration: string[], + preferredAudioLanguageCode: string | null, + explorationLanguageCode: string, + automaticTextToSpeechEnabled: boolean + ): void { this._allAudioLanguageCodesInExploration = - allAudioLanguageCodesInExploration; + allAudioLanguageCodesInExploration; this._explorationLanguageCode = explorationLanguageCode; this._automaticTextToSpeechEnabled = automaticTextToSpeechEnabled; this._languagesInExploration = []; @@ -95,9 +103,11 @@ export class AudioTranslationLanguageService { // to that. // 4. Otherwise, just pick an available non-autogenerated audio language // at random. - if (preferredAudioLanguageCode && - allAudioLanguageCodesInExploration.indexOf( - preferredAudioLanguageCode) !== -1) { + if ( + preferredAudioLanguageCode && + allAudioLanguageCodesInExploration.indexOf(preferredAudioLanguageCode) !== + -1 + ) { this._currentAudioLanguageCode = preferredAudioLanguageCode; } @@ -105,50 +115,60 @@ export class AudioTranslationLanguageService { this.attemptToSetAudioLanguageToExplorationLanguage(); } - if (this._currentAudioLanguageCode === null && - this._allAudioLanguageCodesInExploration.length >= 1) { + if ( + this._currentAudioLanguageCode === null && + this._allAudioLanguageCodesInExploration.length >= 1 + ) { this._currentAudioLanguageCode = - this._allAudioLanguageCodesInExploration[0]; + this._allAudioLanguageCodesInExploration[0]; } - if (this._currentAudioLanguageCode === null && - this._allAudioLanguageCodesInExploration.length === 0 && - this._isAutogeneratedAudioAllowed()) { + if ( + this._currentAudioLanguageCode === null && + this._allAudioLanguageCodesInExploration.length === 0 && + this._isAutogeneratedAudioAllowed() + ) { this._currentAudioLanguageCode = - this.languageUtilService.getAutogeneratedAudioLanguage( - this._explorationLanguageCode).id; + this.languageUtilService.getAutogeneratedAudioLanguage( + this._explorationLanguageCode + ).id; } this._allAudioLanguageCodesInExploration.forEach((languageCode: string) => { - let languageDescription = ( - this.languageUtilService.getAudioLanguageDescription(languageCode)); + let languageDescription = + this.languageUtilService.getAudioLanguageDescription(languageCode); if (languageDescription) { this._languagesInExploration.push({ value: languageCode, - displayed: languageDescription + displayed: languageDescription, }); } }); if (this._isAutogeneratedAudioAllowed()) { let autogeneratedAudioLanguage = - this.languageUtilService.getAutogeneratedAudioLanguage( - this._explorationLanguageCode); + this.languageUtilService.getAutogeneratedAudioLanguage( + this._explorationLanguageCode + ); this._languagesInExploration.push({ value: autogeneratedAudioLanguage.id, - displayed: autogeneratedAudioLanguage.description + displayed: autogeneratedAudioLanguage.description, }); } } init( - allAudioLanguageCodesInExploration: string[], - preferredAudioLanguageCode: string | null, - explorationLanguageCode: string, - automaticTextToSpeechEnabled: boolean): void { + allAudioLanguageCodesInExploration: string[], + preferredAudioLanguageCode: string | null, + explorationLanguageCode: string, + automaticTextToSpeechEnabled: boolean + ): void { this._init( - allAudioLanguageCodesInExploration, preferredAudioLanguageCode, - explorationLanguageCode, automaticTextToSpeechEnabled); + allAudioLanguageCodesInExploration, + preferredAudioLanguageCode, + explorationLanguageCode, + automaticTextToSpeechEnabled + ); } /** @@ -163,9 +183,8 @@ export class AudioTranslationLanguageService { */ getCurrentAudioLanguageDescription(): string | null { if (this._currentAudioLanguageCode) { - return ( - this.languageUtilService.getAudioLanguageDescription( - this._currentAudioLanguageCode) + return this.languageUtilService.getAudioLanguageDescription( + this._currentAudioLanguageCode ); } return null; @@ -212,9 +231,8 @@ export class AudioTranslationLanguageService { */ isAutogeneratedLanguageCodeSelected(): boolean { if (this._currentAudioLanguageCode) { - return ( - this.languageUtilService.isAutogeneratedAudioLanguage( - this._currentAudioLanguageCode) + return this.languageUtilService.isAutogeneratedAudioLanguage( + this._currentAudioLanguageCode ); } return false; @@ -235,8 +253,9 @@ export class AudioTranslationLanguageService { return null; } let autogeneratedAudioLanguage = - this.languageUtilService.getAutogeneratedAudioLanguage( - this._explorationLanguageCode); + this.languageUtilService.getAutogeneratedAudioLanguage( + this._explorationLanguageCode + ); if (this.browserCheckerService.isMobileDevice()) { return autogeneratedAudioLanguage.speechSynthesisCodeMobile; } @@ -244,6 +263,9 @@ export class AudioTranslationLanguageService { } } -angular.module('oppia').factory( - 'AudioTranslationLanguageService', - downgradeInjectable(AudioTranslationLanguageService)); +angular + .module('oppia') + .factory( + 'AudioTranslationLanguageService', + downgradeInjectable(AudioTranslationLanguageService) + ); diff --git a/core/templates/pages/exploration-player-page/services/audio-translation-manager.service.spec.ts b/core/templates/pages/exploration-player-page/services/audio-translation-manager.service.spec.ts index 10e2b4661687..050327728d9d 100644 --- a/core/templates/pages/exploration-player-page/services/audio-translation-manager.service.spec.ts +++ b/core/templates/pages/exploration-player-page/services/audio-translation-manager.service.spec.ts @@ -16,18 +16,20 @@ * @fileoverview Unit tests for the audio translation manager service. */ -import { TestBed } from '@angular/core/testing'; -import { Voiceover } from 'domain/exploration/voiceover.model'; +import {TestBed} from '@angular/core/testing'; +import {Voiceover} from 'domain/exploration/voiceover.model'; -import { AudioTranslationManagerService, AudioTranslations } from - 'pages/exploration-player-page/services/audio-translation-manager.service'; +import { + AudioTranslationManagerService, + AudioTranslations, +} from 'pages/exploration-player-page/services/audio-translation-manager.service'; describe('Audio translation manager service', () => { let atms: AudioTranslationManagerService; beforeEach(() => { TestBed.configureTestingModule({ - providers: [AudioTranslationManagerService] + providers: [AudioTranslationManagerService], }); atms = TestBed.get(AudioTranslationManagerService); @@ -41,14 +43,14 @@ describe('Audio translation manager service', () => { filename: 'audio-en.mp3', file_size_bytes: 0.5, needs_update: false, - duration_secs: 0.5 + duration_secs: 0.5, }), es: Voiceover.createFromBackendDict({ filename: 'audio-es.mp3', file_size_bytes: 0.5, needs_update: false, - duration_secs: 0.5 - }) + duration_secs: 0.5, + }), }; testAudioTranslations2 = { @@ -56,103 +58,110 @@ describe('Audio translation manager service', () => { filename: 'audio-zh.mp3', file_size_bytes: 0.5, needs_update: false, - duration_secs: 0.5 + duration_secs: 0.5, }), 'hi-en': Voiceover.createFromBackendDict({ filename: 'audio-hi-en.mp3', file_size_bytes: 0.5, needs_update: false, - duration_secs: 0.5 - }) + duration_secs: 0.5, + }), }; }); - it('should properly set primary, secondary and sequential audio translations', - () => { - atms.setContentAudioTranslations(testAudioTranslations, '', ''); - expect(atms.getCurrentAudioTranslations()).toEqual({ - en: Voiceover.createFromBackendDict({ - filename: 'audio-en.mp3', - file_size_bytes: 0.5, - needs_update: false, - duration_secs: 0.5 - }), - es: Voiceover.createFromBackendDict({ - filename: 'audio-es.mp3', - file_size_bytes: 0.5, - needs_update: false, - duration_secs: 0.5 - }) - }); - atms.setSecondaryAudioTranslations(testAudioTranslations2, '', ''); - expect(atms.getCurrentAudioTranslations()).toEqual({ - zh: Voiceover.createFromBackendDict({ - filename: 'audio-zh.mp3', - file_size_bytes: 0.5, - needs_update: false, - duration_secs: 0.5 - }), - 'hi-en': Voiceover.createFromBackendDict({ - filename: 'audio-hi-en.mp3', - file_size_bytes: 0.5, - needs_update: false, - duration_secs: 0.5 - }) - }); - atms.clearSecondaryAudioTranslations(); - expect(atms.getCurrentAudioTranslations()).toEqual({ - en: Voiceover.createFromBackendDict({ - filename: 'audio-en.mp3', - file_size_bytes: 0.5, - needs_update: false, - duration_secs: 0.5 - }), - es: Voiceover.createFromBackendDict({ - filename: 'audio-es.mp3', - file_size_bytes: 0.5, - needs_update: false, - duration_secs: 0.5 - }) - }); - atms.setSequentialAudioTranslations( - testAudioTranslations, '

test

', ''); - expect(atms.getCurrentHtmlForAutogeneratedSequentialAudio()) - .toEqual('

test

'); + it('should properly set primary, secondary and sequential audio translations', () => { + atms.setContentAudioTranslations(testAudioTranslations, '', ''); + expect(atms.getCurrentAudioTranslations()).toEqual({ + en: Voiceover.createFromBackendDict({ + filename: 'audio-en.mp3', + file_size_bytes: 0.5, + needs_update: false, + duration_secs: 0.5, + }), + es: Voiceover.createFromBackendDict({ + filename: 'audio-es.mp3', + file_size_bytes: 0.5, + needs_update: false, + duration_secs: 0.5, + }), }); + atms.setSecondaryAudioTranslations(testAudioTranslations2, '', ''); + expect(atms.getCurrentAudioTranslations()).toEqual({ + zh: Voiceover.createFromBackendDict({ + filename: 'audio-zh.mp3', + file_size_bytes: 0.5, + needs_update: false, + duration_secs: 0.5, + }), + 'hi-en': Voiceover.createFromBackendDict({ + filename: 'audio-hi-en.mp3', + file_size_bytes: 0.5, + needs_update: false, + duration_secs: 0.5, + }), + }); + atms.clearSecondaryAudioTranslations(); + expect(atms.getCurrentAudioTranslations()).toEqual({ + en: Voiceover.createFromBackendDict({ + filename: 'audio-en.mp3', + file_size_bytes: 0.5, + needs_update: false, + duration_secs: 0.5, + }), + es: Voiceover.createFromBackendDict({ + filename: 'audio-es.mp3', + file_size_bytes: 0.5, + needs_update: false, + duration_secs: 0.5, + }), + }); + atms.setSequentialAudioTranslations( + testAudioTranslations, + '

test

', + '' + ); + expect(atms.getCurrentHtmlForAutogeneratedSequentialAudio()).toEqual( + '

test

' + ); + }); - it('should properly get html for autogenerated audio', - () => { - const _contentHtmlForAutogeneratedAudio = + it('should properly get html for autogenerated audio', () => { + const _contentHtmlForAutogeneratedAudio = '

contentHtmlForAutogeneratedAudio

'; - const _secondaryHtmlForAutogeneratedAudio = + const _secondaryHtmlForAutogeneratedAudio = '

secondaryHtmlForAutogeneratedAudio

'; - atms.setContentAudioTranslations( - testAudioTranslations, _contentHtmlForAutogeneratedAudio, ''); - expect(atms.getCurrentHtmlForAutogeneratedAudio()).toEqual( - _contentHtmlForAutogeneratedAudio - ); - atms.setSecondaryAudioTranslations( - testAudioTranslations2, _secondaryHtmlForAutogeneratedAudio, ''); - expect(atms.getCurrentHtmlForAutogeneratedAudio()).toEqual( - _secondaryHtmlForAutogeneratedAudio - ); - }); + atms.setContentAudioTranslations( + testAudioTranslations, + _contentHtmlForAutogeneratedAudio, + '' + ); + expect(atms.getCurrentHtmlForAutogeneratedAudio()).toEqual( + _contentHtmlForAutogeneratedAudio + ); + atms.setSecondaryAudioTranslations( + testAudioTranslations2, + _secondaryHtmlForAutogeneratedAudio, + '' + ); + expect(atms.getCurrentHtmlForAutogeneratedAudio()).toEqual( + _secondaryHtmlForAutogeneratedAudio + ); + }); - it('should properly get component name', - () => { - const _contentComponentName = - '

contentComponentName

'; - const _secondaryComponentName = - '

secondaryComponentName

'; - atms.setContentAudioTranslations( - testAudioTranslations, '', _contentComponentName); - expect(atms.getCurrentComponentName()).toEqual( - _contentComponentName - ); - atms.setSecondaryAudioTranslations( - testAudioTranslations2, '', _secondaryComponentName); - expect(atms.getCurrentComponentName()).toEqual( - _secondaryComponentName - ); - }); + it('should properly get component name', () => { + const _contentComponentName = '

contentComponentName

'; + const _secondaryComponentName = '

secondaryComponentName

'; + atms.setContentAudioTranslations( + testAudioTranslations, + '', + _contentComponentName + ); + expect(atms.getCurrentComponentName()).toEqual(_contentComponentName); + atms.setSecondaryAudioTranslations( + testAudioTranslations2, + '', + _secondaryComponentName + ); + expect(atms.getCurrentComponentName()).toEqual(_secondaryComponentName); + }); }); diff --git a/core/templates/pages/exploration-player-page/services/audio-translation-manager.service.ts b/core/templates/pages/exploration-player-page/services/audio-translation-manager.service.ts index 8d32b7c7a3b6..398858686ddc 100644 --- a/core/templates/pages/exploration-player-page/services/audio-translation-manager.service.ts +++ b/core/templates/pages/exploration-player-page/services/audio-translation-manager.service.ts @@ -17,18 +17,17 @@ * being played or paused. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; -import { Voiceover } from - 'domain/exploration/voiceover.model'; +import {Voiceover} from 'domain/exploration/voiceover.model'; export interface AudioTranslations { [languageCode: string]: Voiceover; } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class AudioTranslationManagerService { // Audio translations for the main content of a card at the top. @@ -49,8 +48,10 @@ export class AudioTranslationManagerService { _currentSequentialComponentName: string = ''; setContentAudioTranslations( - audioTranslations: AudioTranslations, html: string, - componentName: string): void { + audioTranslations: AudioTranslations, + html: string, + componentName: string + ): void { this._contentAudioTranslations = audioTranslations; this._contentHtmlForAutogeneratedAudio = html; this._currentPrimaryComponentName = componentName; @@ -62,8 +63,10 @@ export class AudioTranslationManagerService { } setSecondaryAudioTranslations( - audioTranslations: AudioTranslations, html: string, - componentName: string): void { + audioTranslations: AudioTranslations, + html: string, + componentName: string + ): void { this._secondaryAudioTranslations = audioTranslations; this._secondaryHtmlForAutogeneratedAudio = html; this._currentSecondaryComponentName = componentName; @@ -76,9 +79,9 @@ export class AudioTranslationManagerService { // This needs to be called *after* setContentAudioTranslations; e.g. inside // ngOnInit() of the component. setSequentialAudioTranslations( - audioTranslations: AudioTranslations, - html: string, - componentName: string + audioTranslations: AudioTranslations, + html: string, + componentName: string ): void { this._sequentialAudioTranslations = audioTranslations; this._sequentialHtmlForAutogeneratedAudio = html; @@ -101,8 +104,10 @@ export class AudioTranslationManagerService { // is no audio for the seconday text, we return // an empty object if there is secondary HTML. getCurrentAudioTranslations(): AudioTranslations { - if (Object.keys(this._secondaryAudioTranslations).length !== 0 || - this._secondaryHtmlForAutogeneratedAudio !== '') { + if ( + Object.keys(this._secondaryAudioTranslations).length !== 0 || + this._secondaryHtmlForAutogeneratedAudio !== '' + ) { return this._secondaryAudioTranslations; } return this._contentAudioTranslations; @@ -136,6 +141,9 @@ export class AudioTranslationManagerService { } } -angular.module('oppia').factory( - 'AudioTranslationManagerService', - downgradeInjectable(AudioTranslationManagerService)); +angular + .module('oppia') + .factory( + 'AudioTranslationManagerService', + downgradeInjectable(AudioTranslationManagerService) + ); diff --git a/core/templates/pages/exploration-player-page/services/checkpoint-celebration-utility.service.spec.ts b/core/templates/pages/exploration-player-page/services/checkpoint-celebration-utility.service.spec.ts index aef8d9cd110f..8ba38bc9e929 100644 --- a/core/templates/pages/exploration-player-page/services/checkpoint-celebration-utility.service.spec.ts +++ b/core/templates/pages/exploration-player-page/services/checkpoint-celebration-utility.service.spec.ts @@ -16,14 +16,14 @@ * @fileoverview Unit tests for the checkpoint celebration utility service. */ -import { TestBed } from '@angular/core/testing'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; +import {TestBed} from '@angular/core/testing'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; -import { CheckpointCelebrationUtilityService } from 'pages/exploration-player-page/services/checkpoint-celebration-utility.service'; -import { ComputeGraphService } from 'services/compute-graph.service'; -import { StateObjectsBackendDict } from 'domain/exploration/StatesObjectFactory'; -import { StatesObjectFactory } from 'domain/exploration/StatesObjectFactory'; +import {CheckpointCelebrationUtilityService} from 'pages/exploration-player-page/services/checkpoint-celebration-utility.service'; +import {ComputeGraphService} from 'services/compute-graph.service'; +import {StateObjectsBackendDict} from 'domain/exploration/StatesObjectFactory'; +import {StatesObjectFactory} from 'domain/exploration/StatesObjectFactory'; class MockTranslateService { onLangChange: EventEmitter = new EventEmitter(); @@ -46,16 +46,17 @@ describe('Checkpoint celebration utility service', () => { ComputeGraphService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }); }); beforeEach(() => { checkpointCelebrationUtilityService = TestBed.inject( - CheckpointCelebrationUtilityService); + CheckpointCelebrationUtilityService + ); translateService = TestBed.inject(TranslateService); computeGraphService = TestBed.inject(ComputeGraphService); statesObjectFactory = TestBed.inject(StatesObjectFactory); @@ -67,195 +68,226 @@ describe('Checkpoint celebration utility service', () => { classifier_model_id: null, content: { content_id: 'content', - html: '' + html: '', }, recorded_voiceovers: { voiceovers_mapping: { content: {}, - default_outcome: {} - } + default_outcome: {}, + }, }, interaction: { answer_groups: [], confirmed_unclassified_answers: [], customization_args: { buttonText: { - value: 'Continue' - } + value: 'Continue', + }, }, default_outcome: { dest: 'End State', dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: '' + html: '', }, param_changes: [], labelled_as_correct: true, refresher_exploration_id: null, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, hints: [], solution: null, - id: 'Continue' + id: 'Continue', }, linked_skill_id: null, param_changes: [], solicit_answer_details: false, - card_is_checkpoint: true + card_is_checkpoint: true, }, 'End State': { classifier_model_id: null, content: { content_id: 'content', - html: '' + html: '', }, recorded_voiceovers: { voiceovers_mapping: { content: {}, - default_outcome: {} - } + default_outcome: {}, + }, }, interaction: { answer_groups: [], confirmed_unclassified_answers: [], customization_args: { recommendedExplorationIds: { - value: [] - } + value: [], + }, }, default_outcome: null, hints: [], solution: null, - id: 'EndExploration' + id: 'EndExploration', }, linked_skill_id: null, param_changes: [], solicit_answer_details: false, - card_is_checkpoint: false - } + card_is_checkpoint: false, + }, }; - const states = statesObjectFactory.createFromBackendDict( - statesBackendDict); + const states = statesObjectFactory.createFromBackendDict(statesBackendDict); spyOn(computeGraphService, 'computeBfsTraversalOfStates').and.returnValue([ - 'First State', 'End State']); + 'First State', + 'End State', + ]); expect( checkpointCelebrationUtilityService.getStateListForCheckpointMessages( - statesBackendDict, 'First State')).toEqual(['First State']); - expect(computeGraphService.computeBfsTraversalOfStates) - .toHaveBeenCalledWith('First State', states, 'First State'); + statesBackendDict, + 'First State' + ) + ).toEqual(['First State']); + expect( + computeGraphService.computeBfsTraversalOfStates + ).toHaveBeenCalledWith('First State', states, 'First State'); }); it('should get a random i18n key when message kind is specified', () => { spyOn(Math, 'random').and.returnValue(0.45); - expect(checkpointCelebrationUtilityService.getRandomI18nKey( - 'KEY_PREFIX', 10, 'KIND_A')).toEqual('KEY_PREFIX_KIND_A_5'); + expect( + checkpointCelebrationUtilityService.getRandomI18nKey( + 'KEY_PREFIX', + 10, + 'KIND_A' + ) + ).toEqual('KEY_PREFIX_KIND_A_5'); }); it('should get a random i18n key when message kind is not specified', () => { spyOn(Math, 'random').and.returnValue(0.45); - expect(checkpointCelebrationUtilityService.getRandomI18nKey( - 'KEY_PREFIX', 10, null)).toEqual('KEY_PREFIX_5'); + expect( + checkpointCelebrationUtilityService.getRandomI18nKey( + 'KEY_PREFIX', + 10, + null + ) + ).toEqual('KEY_PREFIX_5'); }); it('should get the right kind of checkpoint message i18n key', () => { spyOn(Math, 'random').and.returnValue(0.45); expect( - checkpointCelebrationUtilityService.getCheckpointMessageI18nKey(1, 8)) - .toEqual('I18N_CONGRATULATORY_CHECKPOINT_MESSAGE_FIRST_2'); + checkpointCelebrationUtilityService.getCheckpointMessageI18nKey(1, 8) + ).toEqual('I18N_CONGRATULATORY_CHECKPOINT_MESSAGE_FIRST_2'); expect( - checkpointCelebrationUtilityService.getCheckpointMessageI18nKey(1, 5)) - .toEqual('I18N_CONGRATULATORY_CHECKPOINT_MESSAGE_FIRST_2'); + checkpointCelebrationUtilityService.getCheckpointMessageI18nKey(1, 5) + ).toEqual('I18N_CONGRATULATORY_CHECKPOINT_MESSAGE_FIRST_2'); expect( - checkpointCelebrationUtilityService.getCheckpointMessageI18nKey(2, 8)) - .toEqual('I18N_CONGRATULATORY_CHECKPOINT_MESSAGE_SECOND_2'); + checkpointCelebrationUtilityService.getCheckpointMessageI18nKey(2, 8) + ).toEqual('I18N_CONGRATULATORY_CHECKPOINT_MESSAGE_SECOND_2'); expect( - checkpointCelebrationUtilityService.getCheckpointMessageI18nKey(2, 5)) - .toEqual('I18N_CONGRATULATORY_CHECKPOINT_MESSAGE_SECOND_2'); + checkpointCelebrationUtilityService.getCheckpointMessageI18nKey(2, 5) + ).toEqual('I18N_CONGRATULATORY_CHECKPOINT_MESSAGE_SECOND_2'); expect( - checkpointCelebrationUtilityService.getCheckpointMessageI18nKey(4, 8)) - .toEqual('I18N_CONGRATULATORY_CHECKPOINT_MESSAGE_MIDWAY_2'); + checkpointCelebrationUtilityService.getCheckpointMessageI18nKey(4, 8) + ).toEqual('I18N_CONGRATULATORY_CHECKPOINT_MESSAGE_MIDWAY_2'); expect( - checkpointCelebrationUtilityService.getCheckpointMessageI18nKey(2, 5)) - .toEqual('I18N_CONGRATULATORY_CHECKPOINT_MESSAGE_SECOND_2'); + checkpointCelebrationUtilityService.getCheckpointMessageI18nKey(2, 5) + ).toEqual('I18N_CONGRATULATORY_CHECKPOINT_MESSAGE_SECOND_2'); expect( - checkpointCelebrationUtilityService.getCheckpointMessageI18nKey(3, 5)) - .toEqual('I18N_CONGRATULATORY_CHECKPOINT_MESSAGE_MIDWAY_2'); + checkpointCelebrationUtilityService.getCheckpointMessageI18nKey(3, 5) + ).toEqual('I18N_CONGRATULATORY_CHECKPOINT_MESSAGE_MIDWAY_2'); expect( - checkpointCelebrationUtilityService.getCheckpointMessageI18nKey(4, 5)) - .toEqual('I18N_CONGRATULATORY_CHECKPOINT_MESSAGE_ONE_REMAINING_2'); + checkpointCelebrationUtilityService.getCheckpointMessageI18nKey(4, 5) + ).toEqual('I18N_CONGRATULATORY_CHECKPOINT_MESSAGE_ONE_REMAINING_2'); expect( - checkpointCelebrationUtilityService.getCheckpointMessageI18nKey(3, 7)) - .toEqual('I18N_CONGRATULATORY_CHECKPOINT_MESSAGE_GENERIC_2'); + checkpointCelebrationUtilityService.getCheckpointMessageI18nKey(3, 7) + ).toEqual('I18N_CONGRATULATORY_CHECKPOINT_MESSAGE_GENERIC_2'); expect( - checkpointCelebrationUtilityService.getCheckpointMessageI18nKey(4, 7)) - .toEqual('I18N_CONGRATULATORY_CHECKPOINT_MESSAGE_MIDWAY_2'); + checkpointCelebrationUtilityService.getCheckpointMessageI18nKey(4, 7) + ).toEqual('I18N_CONGRATULATORY_CHECKPOINT_MESSAGE_MIDWAY_2'); expect( - checkpointCelebrationUtilityService.getCheckpointMessageI18nKey(5, 7)) - .toEqual('I18N_CONGRATULATORY_CHECKPOINT_MESSAGE_TWO_REMAINING_2'); + checkpointCelebrationUtilityService.getCheckpointMessageI18nKey(5, 7) + ).toEqual('I18N_CONGRATULATORY_CHECKPOINT_MESSAGE_TWO_REMAINING_2'); expect( - checkpointCelebrationUtilityService.getCheckpointMessageI18nKey(6, 8)) - .toEqual('I18N_CONGRATULATORY_CHECKPOINT_MESSAGE_TWO_REMAINING_2'); + checkpointCelebrationUtilityService.getCheckpointMessageI18nKey(6, 8) + ).toEqual('I18N_CONGRATULATORY_CHECKPOINT_MESSAGE_TWO_REMAINING_2'); expect( - checkpointCelebrationUtilityService.getCheckpointMessageI18nKey(7, 8)) - .toEqual('I18N_CONGRATULATORY_CHECKPOINT_MESSAGE_ONE_REMAINING_2'); + checkpointCelebrationUtilityService.getCheckpointMessageI18nKey(7, 8) + ).toEqual('I18N_CONGRATULATORY_CHECKPOINT_MESSAGE_ONE_REMAINING_2'); expect( - checkpointCelebrationUtilityService.getCheckpointMessageI18nKey(3, 8)) - .toEqual('I18N_CONGRATULATORY_CHECKPOINT_MESSAGE_GENERIC_2'); + checkpointCelebrationUtilityService.getCheckpointMessageI18nKey(3, 8) + ).toEqual('I18N_CONGRATULATORY_CHECKPOINT_MESSAGE_GENERIC_2'); }); it('should get the checkpoint message', () => { - spyOn(checkpointCelebrationUtilityService, 'getCheckpointMessageI18nKey') - .and.returnValue('DUMMY_CHECKPOINT_MESSAGE_KEY'); + spyOn( + checkpointCelebrationUtilityService, + 'getCheckpointMessageI18nKey' + ).and.returnValue('DUMMY_CHECKPOINT_MESSAGE_KEY'); spyOn(translateService, 'instant').and.callThrough(); - expect(checkpointCelebrationUtilityService.getCheckpointMessage( - 2, 8)).toEqual('DUMMY_CHECKPOINT_MESSAGE_KEY'); - expect(checkpointCelebrationUtilityService.getCheckpointMessageI18nKey) - .toHaveBeenCalledWith(2, 8); + expect( + checkpointCelebrationUtilityService.getCheckpointMessage(2, 8) + ).toEqual('DUMMY_CHECKPOINT_MESSAGE_KEY'); + expect( + checkpointCelebrationUtilityService.getCheckpointMessageI18nKey + ).toHaveBeenCalledWith(2, 8); expect(translateService.instant).toHaveBeenCalledWith( - 'DUMMY_CHECKPOINT_MESSAGE_KEY'); + 'DUMMY_CHECKPOINT_MESSAGE_KEY' + ); }); it('should get a random checkpoint title i18n key', () => { spyOn(Math, 'random').and.returnValue(0.45); - spyOn(checkpointCelebrationUtilityService, 'getRandomI18nKey') - .and.callThrough(); + spyOn( + checkpointCelebrationUtilityService, + 'getRandomI18nKey' + ).and.callThrough(); - expect(checkpointCelebrationUtilityService.getCheckpointTitleI18nKey()) - .toEqual('I18N_CONGRATULATORY_CHECKPOINT_TITLE_3'); + expect( + checkpointCelebrationUtilityService.getCheckpointTitleI18nKey() + ).toEqual('I18N_CONGRATULATORY_CHECKPOINT_TITLE_3'); }); it('should get the checkpoint title', () => { - spyOn(checkpointCelebrationUtilityService, 'getCheckpointTitleI18nKey') - .and.returnValue('DUMMY_CHECKPOINT_TITLE_I18N_KEY'); + spyOn( + checkpointCelebrationUtilityService, + 'getCheckpointTitleI18nKey' + ).and.returnValue('DUMMY_CHECKPOINT_TITLE_I18N_KEY'); spyOn(translateService, 'instant').and.callThrough(); - expect(checkpointCelebrationUtilityService.getCheckpointTitle()) - .toEqual('DUMMY_CHECKPOINT_TITLE_I18N_KEY'); - expect(checkpointCelebrationUtilityService.getCheckpointTitleI18nKey) - .toHaveBeenCalled(); + expect(checkpointCelebrationUtilityService.getCheckpointTitle()).toEqual( + 'DUMMY_CHECKPOINT_TITLE_I18N_KEY' + ); + expect( + checkpointCelebrationUtilityService.getCheckpointTitleI18nKey + ).toHaveBeenCalled(); expect(translateService.instant).toHaveBeenCalledWith( - 'DUMMY_CHECKPOINT_TITLE_I18N_KEY'); + 'DUMMY_CHECKPOINT_TITLE_I18N_KEY' + ); }); it('should correctly set and retrieve isOnCheckpointedState value', () => { - expect(checkpointCelebrationUtilityService.getIsOnCheckpointedState()) - .toBe(false); + expect(checkpointCelebrationUtilityService.getIsOnCheckpointedState()).toBe( + false + ); checkpointCelebrationUtilityService.setIsOnCheckpointedState(true); - expect(checkpointCelebrationUtilityService.getIsOnCheckpointedState()) - .toBe(true); + expect(checkpointCelebrationUtilityService.getIsOnCheckpointedState()).toBe( + true + ); checkpointCelebrationUtilityService.setIsOnCheckpointedState(false); - expect(checkpointCelebrationUtilityService.getIsOnCheckpointedState()) - .toBe(false); + expect(checkpointCelebrationUtilityService.getIsOnCheckpointedState()).toBe( + false + ); }); it('should get emitter meant to open the lesson info modal', () => { @@ -268,13 +300,15 @@ describe('Checkpoint celebration utility service', () => { it('should emit the lesson info modal open event', () => { spyOn( - checkpointCelebrationUtilityService - .getOpenLessonInformationModalEmitter(), 'emit'); + checkpointCelebrationUtilityService.getOpenLessonInformationModalEmitter(), + 'emit' + ); checkpointCelebrationUtilityService.openLessonInformationModal(); expect( - checkpointCelebrationUtilityService - .getOpenLessonInformationModalEmitter().emit).toHaveBeenCalled(); + checkpointCelebrationUtilityService.getOpenLessonInformationModalEmitter() + .emit + ).toHaveBeenCalled(); }); }); diff --git a/core/templates/pages/exploration-player-page/services/checkpoint-celebration-utility.service.ts b/core/templates/pages/exploration-player-page/services/checkpoint-celebration-utility.service.ts index 40f6a276d930..4cec37e6e968 100644 --- a/core/templates/pages/exploration-player-page/services/checkpoint-celebration-utility.service.ts +++ b/core/templates/pages/exploration-player-page/services/checkpoint-celebration-utility.service.ts @@ -17,12 +17,12 @@ * checkpoint celebration feature. */ -import { Injectable, EventEmitter } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; +import {Injectable, EventEmitter} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; -import { ComputeGraphService } from 'services/compute-graph.service'; -import { StateObjectsBackendDict } from 'domain/exploration/StatesObjectFactory'; -import { StatesObjectFactory } from 'domain/exploration/StatesObjectFactory'; +import {ComputeGraphService} from 'services/compute-graph.service'; +import {StateObjectsBackendDict} from 'domain/exploration/StatesObjectFactory'; +import {StatesObjectFactory} from 'domain/exploration/StatesObjectFactory'; enum CheckpointMessageTypes { FIRST = 'FIRST', @@ -30,11 +30,11 @@ enum CheckpointMessageTypes { MIDWAY = 'MIDWAY', TWO_REMAINING = 'TWO_REMAINING', ONE_REMAINING = 'ONE_REMAINING', - GENERIC = 'GENERIC' + GENERIC = 'GENERIC', } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class CheckpointCelebrationUtilityService { private isOnCheckpointedState: boolean = false; @@ -46,14 +46,18 @@ export class CheckpointCelebrationUtilityService { ) {} getStateListForCheckpointMessages( - statesbackendDict: StateObjectsBackendDict, initStateName: string + statesbackendDict: StateObjectsBackendDict, + initStateName: string ): string[] { - const states = this.statesObjectFactory.createFromBackendDict( - statesbackendDict); + const states = + this.statesObjectFactory.createFromBackendDict(statesbackendDict); const bfsStateList = this.computeGraphService.computeBfsTraversalOfStates( - initStateName, states, initStateName); + initStateName, + states, + initStateName + ); let stateListForCheckpointMessages: string[] = []; - bfsStateList.forEach((state) => { + bfsStateList.forEach(state => { if (statesbackendDict[state].card_is_checkpoint) { stateListForCheckpointMessages.push(state); } @@ -62,9 +66,9 @@ export class CheckpointCelebrationUtilityService { } getRandomI18nKey( - i18nKeyPrefix: string, - availableKeyCount: number, - messageKind: string | null, + i18nKeyPrefix: string, + availableKeyCount: number, + messageKind: string | null ): string { // 'randomValue' is being set to lie between 1 and availableKeyCount, the // total number of i18n keys available to choose from. @@ -84,15 +88,22 @@ export class CheckpointCelebrationUtilityService { // The function returns a random key from the 3 available ones for the // correct type, using the 'getRandomI18nKey' method. getCheckpointMessageI18nKey( - completedCheckpointCount: number, totalCheckpointCount: number + completedCheckpointCount: number, + totalCheckpointCount: number ): string { const messageI18nKeyPrefix = 'I18N_CONGRATULATORY_CHECKPOINT_MESSAGE'; if (completedCheckpointCount === 1) { return this.getRandomI18nKey( - messageI18nKeyPrefix, 3, CheckpointMessageTypes.FIRST); + messageI18nKeyPrefix, + 3, + CheckpointMessageTypes.FIRST + ); } else if (completedCheckpointCount === 2) { return this.getRandomI18nKey( - messageI18nKeyPrefix, 3, CheckpointMessageTypes.SECOND); + messageI18nKeyPrefix, + 3, + CheckpointMessageTypes.SECOND + ); // The condition below evaluates to true when the learner has just // completed the middle checkpoint and returns the midway message's // i18n key. @@ -100,24 +111,39 @@ export class CheckpointCelebrationUtilityService { Math.ceil(totalCheckpointCount / 2) === completedCheckpointCount ) { return this.getRandomI18nKey( - messageI18nKeyPrefix, 3, CheckpointMessageTypes.MIDWAY); + messageI18nKeyPrefix, + 3, + CheckpointMessageTypes.MIDWAY + ); } else if (totalCheckpointCount - completedCheckpointCount === 2) { return this.getRandomI18nKey( - messageI18nKeyPrefix, 3, CheckpointMessageTypes.TWO_REMAINING); + messageI18nKeyPrefix, + 3, + CheckpointMessageTypes.TWO_REMAINING + ); } else if (totalCheckpointCount - completedCheckpointCount === 1) { return this.getRandomI18nKey( - messageI18nKeyPrefix, 3, CheckpointMessageTypes.ONE_REMAINING); + messageI18nKeyPrefix, + 3, + CheckpointMessageTypes.ONE_REMAINING + ); } else { return this.getRandomI18nKey( - messageI18nKeyPrefix, 3, CheckpointMessageTypes.GENERIC); + messageI18nKeyPrefix, + 3, + CheckpointMessageTypes.GENERIC + ); } } getCheckpointMessage( - completedCheckpointCount: number, totalCheckpointCount: number + completedCheckpointCount: number, + totalCheckpointCount: number ): string { const messageI18nKey = this.getCheckpointMessageI18nKey( - completedCheckpointCount, totalCheckpointCount); + completedCheckpointCount, + totalCheckpointCount + ); return this.translateService.instant(messageI18nKey); } diff --git a/core/templates/pages/exploration-player-page/services/concept-card-manager.service.spec.ts b/core/templates/pages/exploration-player-page/services/concept-card-manager.service.spec.ts index ac1c8e2aadf6..7bb2f17d02e0 100644 --- a/core/templates/pages/exploration-player-page/services/concept-card-manager.service.spec.ts +++ b/core/templates/pages/exploration-player-page/services/concept-card-manager.service.spec.ts @@ -16,19 +16,19 @@ * @fileoverview Unit tests for the Concept Card Manager service. */ -import { EventEmitter } from '@angular/core'; -import { TestBed, fakeAsync, flush, tick } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TranslateService } from '@ngx-translate/core'; -import { MockTranslateService } from 'components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec'; -import { StateCard } from 'domain/state_card/state-card.model'; -import { PlayerPositionService } from 'pages/exploration-player-page/services/player-position.service'; -import { ConceptCardManagerService } from './concept-card-manager.service'; -import { ExplorationEngineService } from './exploration-engine.service'; -import { StateObjectFactory } from 'domain/state/StateObjectFactory'; -import { InteractionObjectFactory } from 'domain/exploration/InteractionObjectFactory'; -import { RecordedVoiceovers } from 'domain/exploration/recorded-voiceovers.model'; -import { AudioTranslationLanguageService } from './audio-translation-language.service'; +import {EventEmitter} from '@angular/core'; +import {TestBed, fakeAsync, flush, tick} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TranslateService} from '@ngx-translate/core'; +import {MockTranslateService} from 'components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec'; +import {StateCard} from 'domain/state_card/state-card.model'; +import {PlayerPositionService} from 'pages/exploration-player-page/services/player-position.service'; +import {ConceptCardManagerService} from './concept-card-manager.service'; +import {ExplorationEngineService} from './exploration-engine.service'; +import {StateObjectFactory} from 'domain/state/StateObjectFactory'; +import {InteractionObjectFactory} from 'domain/exploration/InteractionObjectFactory'; +import {RecordedVoiceovers} from 'domain/exploration/recorded-voiceovers.model'; +import {AudioTranslationLanguageService} from './audio-translation-language.service'; describe('ConceptCardManager service', () => { let ccms: ConceptCardManagerService; @@ -51,26 +51,29 @@ describe('ConceptCardManager service', () => { providers: [ { provide: TranslateService, - useClass: MockTranslateService - } - ] + useClass: MockTranslateService, + }, + ], }); pps = TestBed.inject(PlayerPositionService); ees = TestBed.inject(ExplorationEngineService); stateObjectFactory = TestBed.inject(StateObjectFactory); spyOn(pps, 'onNewCardAvailable').and.returnValue( - mockNewCardAvailableEmitter); - spyOn(pps, 'onNewCardOpened').and.returnValue( - mockNewCardOpenedEmitter); + mockNewCardAvailableEmitter + ); + spyOn(pps, 'onNewCardOpened').and.returnValue(mockNewCardOpenedEmitter); ccms = TestBed.inject(ConceptCardManagerService); interactionObjectFactory = TestBed.inject(InteractionObjectFactory); audioTranslationLanguageService = TestBed.inject( - AudioTranslationLanguageService); + AudioTranslationLanguageService + ); })); beforeEach(() => { stateCard = StateCard.createNewCard( - 'State 2', '

Content

', '', + 'State 2', + '

Content

', + '', interactionObjectFactory.createFromBackendDict({ id: 'TextInput', answer_groups: [ @@ -111,7 +114,7 @@ describe('ConceptCardManager service', () => { }, placeholder: { value: 1, - } + }, }, hints: [], solution: { @@ -121,10 +124,12 @@ describe('ConceptCardManager service', () => { content_id: '2', html: 'test_explanation1', }, - } + }, }), RecordedVoiceovers.createEmpty(), - 'content', audioTranslationLanguageService); + 'content', + audioTranslationLanguageService + ); }); it('should show concept card icon at the right time', fakeAsync(() => { @@ -167,73 +172,73 @@ describe('ConceptCardManager service', () => { expect(ccms.isConceptCardConsumed()).toBe(false); })); - it('should reset the service when timeouts was called before', - fakeAsync(() => { - // Initialize the service with two hints and a solution. - spyOn(ccms, 'conceptCardForStateExists').and.returnValue(true); - ccms.reset(stateCard); - - // Set timeout. - tick(WAIT_FOR_CONCEPT_CARD_MSEC); - // Set tooltipTimeout. - tick(WAIT_FOR_TOOLTIP_TO_BE_SHOWN_MSEC); - - // Reset service to 0 solutions so releaseHint timeout won't be called. - ccms.reset(stateCard); - - // There is no timeout to flush. timeout and tooltipTimeout variables - // were cleaned. - expect(flush()).toBe(60000); - })); - - it('should return if concept card for the state with the new name exists', - fakeAsync(() => { - const endState = { - classifier_model_id: null, - recorded_voiceovers: { - voiceovers_mapping: { - content: {} - } + it('should reset the service when timeouts was called before', fakeAsync(() => { + // Initialize the service with two hints and a solution. + spyOn(ccms, 'conceptCardForStateExists').and.returnValue(true); + ccms.reset(stateCard); + + // Set timeout. + tick(WAIT_FOR_CONCEPT_CARD_MSEC); + // Set tooltipTimeout. + tick(WAIT_FOR_TOOLTIP_TO_BE_SHOWN_MSEC); + + // Reset service to 0 solutions so releaseHint timeout won't be called. + ccms.reset(stateCard); + + // There is no timeout to flush. timeout and tooltipTimeout variables + // were cleaned. + expect(flush()).toBe(60000); + })); + + it('should return if concept card for the state with the new name exists', fakeAsync(() => { + const endState = { + classifier_model_id: null, + recorded_voiceovers: { + voiceovers_mapping: { + content: {}, }, - solicit_answer_details: false, - interaction: { - solution: null, - confirmed_unclassified_answers: [], - id: 'EndExploration', - hints: [], - customization_args: { - recommendedExplorationIds: { - value: ['recommendedExplorationId'] - } + }, + solicit_answer_details: false, + interaction: { + solution: null, + confirmed_unclassified_answers: [], + id: 'EndExploration', + hints: [], + customization_args: { + recommendedExplorationIds: { + value: ['recommendedExplorationId'], }, - answer_groups: [], - default_outcome: null }, - param_changes: [], - next_content_id_index: 0, - card_is_checkpoint: false, - linked_skill_id: 'Id', - content: { - content_id: 'content', - html: 'Congratulations, you have finished!' - } - }; - spyOn(ees, 'getStateFromStateName').withArgs('State 2') - .and.returnValue( - stateObjectFactory.createFromBackendDict('End', endState)); - - ccms.hintsAvailable = 0; - ccms.reset(stateCard); - - // Time delay before concept card is released. - tick(WAIT_FOR_CONCEPT_CARD_MSEC); - // Time delay before tooltip for the concept card is shown. - tick(WAIT_FOR_TOOLTIP_TO_BE_SHOWN_MSEC); - - expect(ccms.isConceptCardTooltipOpen()).toBe(true); - expect(ccms.isConceptCardViewable()).toBe(true); - expect(ccms.isConceptCardConsumed()).toBe(false); - })); + answer_groups: [], + default_outcome: null, + }, + param_changes: [], + next_content_id_index: 0, + card_is_checkpoint: false, + linked_skill_id: 'Id', + content: { + content_id: 'content', + html: 'Congratulations, you have finished!', + }, + }; + spyOn(ees, 'getStateFromStateName') + .withArgs('State 2') + .and.returnValue( + stateObjectFactory.createFromBackendDict('End', endState) + ); + + ccms.hintsAvailable = 0; + ccms.reset(stateCard); + + // Time delay before concept card is released. + tick(WAIT_FOR_CONCEPT_CARD_MSEC); + // Time delay before tooltip for the concept card is shown. + tick(WAIT_FOR_TOOLTIP_TO_BE_SHOWN_MSEC); + + expect(ccms.isConceptCardTooltipOpen()).toBe(true); + expect(ccms.isConceptCardViewable()).toBe(true); + expect(ccms.isConceptCardConsumed()).toBe(false); + })); it('should set the number of hints available', fakeAsync(() => { spyOn(pps.onNewCardOpened, 'subscribe'); diff --git a/core/templates/pages/exploration-player-page/services/concept-card-manager.service.ts b/core/templates/pages/exploration-player-page/services/concept-card-manager.service.ts index 4ab367b242e9..0d2c3720ce7a 100644 --- a/core/templates/pages/exploration-player-page/services/concept-card-manager.service.ts +++ b/core/templates/pages/exploration-player-page/services/concept-card-manager.service.ts @@ -16,16 +16,16 @@ * @fileoverview Utility service for Concept Card in the learner's view. */ -import { EventEmitter, Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { StateCard } from 'domain/state_card/state-card.model'; +import {EventEmitter, Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {StateCard} from 'domain/state_card/state-card.model'; -import { ExplorationPlayerConstants } from 'pages/exploration-player-page/exploration-player-page.constants'; -import { PlayerPositionService } from 'pages/exploration-player-page/services/player-position.service'; -import { ExplorationEngineService } from './exploration-engine.service'; +import {ExplorationPlayerConstants} from 'pages/exploration-player-page/exploration-player-page.constants'; +import {PlayerPositionService} from 'pages/exploration-player-page/services/player-position.service'; +import {ExplorationEngineService} from './exploration-engine.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ConceptCardManagerService { // The following are set to null when the timeouts are cleared @@ -47,7 +47,6 @@ export class ConceptCardManagerService { wrongAnswersSinceConceptCardConsumed: number = 0; learnerIsReallyStuck: boolean = false; - // Variable tooltipIsOpen is a flag which says that the tooltip is currently // visible to the learner. tooltipIsOpen: boolean = false; @@ -56,7 +55,7 @@ export class ConceptCardManagerService { conceptCardDiscovered: boolean = false; constructor( - playerPositionService: PlayerPositionService, + playerPositionService: PlayerPositionService, private explorationEngineService: ExplorationEngineService ) { // TODO(#10904): Refactor to move subscriptions into components. @@ -87,7 +86,9 @@ export class ConceptCardManagerService { this.conceptCardReleased = true; if (!this.conceptCardDiscovered && !this.tooltipTimeout) { this.tooltipTimeout = setTimeout( - this.showTooltip.bind(this), this.WAIT_FOR_TOOLTIP_TO_BE_SHOWN_MSEC); + this.showTooltip.bind(this), + this.WAIT_FOR_TOOLTIP_TO_BE_SHOWN_MSEC + ); } this._timeoutElapsedEventEmitter.emit(); } @@ -110,7 +111,8 @@ export class ConceptCardManagerService { this.wrongAnswersSinceConceptCardConsumed = 0; this.enqueueTimeout( this.emitLearnerStuckedness, - ExplorationPlayerConstants.WAIT_BEFORE_REALLY_STUCK_MSEC); + ExplorationPlayerConstants.WAIT_BEFORE_REALLY_STUCK_MSEC + ); } reset(newCard: StateCard): void { @@ -132,13 +134,15 @@ export class ConceptCardManagerService { if (this.conceptCardForStateExists(newCard)) { this.enqueueTimeout( this.releaseConceptCard, - ExplorationPlayerConstants.WAIT_FOR_CONCEPT_CARD_MSEC); + ExplorationPlayerConstants.WAIT_FOR_CONCEPT_CARD_MSEC + ); } } conceptCardForStateExists(newCard: StateCard): boolean { let state = this.explorationEngineService.getStateFromStateName( - newCard.getStateName()); + newCard.getStateName() + ); return state.linkedSkillId !== null; } @@ -158,9 +162,10 @@ export class ConceptCardManagerService { if (this.isConceptCardViewable()) { this.wrongAnswersSinceConceptCardConsumed++; } - if (this.wrongAnswersSinceConceptCardConsumed > - ExplorationPlayerConstants. - MAX_INCORRECT_ANSWERS_BEFORE_REALLY_STUCK) { + if ( + this.wrongAnswersSinceConceptCardConsumed > + ExplorationPlayerConstants.MAX_INCORRECT_ANSWERS_BEFORE_REALLY_STUCK + ) { // Learner is really stuck. this.emitLearnerStuckedness(); } @@ -171,6 +176,9 @@ export class ConceptCardManagerService { } } -angular.module('oppia').factory( - 'ConceptCardManagerService', - downgradeInjectable(ConceptCardManagerService)); +angular + .module('oppia') + .factory( + 'ConceptCardManagerService', + downgradeInjectable(ConceptCardManagerService) + ); diff --git a/core/templates/pages/exploration-player-page/services/content-translation-language.service.spec.ts b/core/templates/pages/exploration-player-page/services/content-translation-language.service.spec.ts index b30ee6981919..68bbec4212fc 100644 --- a/core/templates/pages/exploration-player-page/services/content-translation-language.service.spec.ts +++ b/core/templates/pages/exploration-player-page/services/content-translation-language.service.spec.ts @@ -16,12 +16,11 @@ * @fileoverview Unit tests for the content translation language service. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TestBed } from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TestBed} from '@angular/core/testing'; -import { ContentTranslationLanguageService } from - 'pages/exploration-player-page/services/content-translation-language.service'; -import { UrlService } from 'services/contextual/url.service'; +import {ContentTranslationLanguageService} from 'pages/exploration-player-page/services/content-translation-language.service'; +import {UrlService} from 'services/contextual/url.service'; describe('Content translation language service', () => { let ctls: ContentTranslationLanguageService; @@ -30,7 +29,7 @@ describe('Content translation language service', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] + imports: [HttpClientTestingModule], }); ctls = TestBed.inject(ContentTranslationLanguageService); @@ -40,37 +39,43 @@ describe('Content translation language service', () => { it('should correctly set the language to a valid URL parameter', () => { spyOn(us, 'getUrlParams').and.returnValue({ - initialContentLanguageCode: 'fr' + initialContentLanguageCode: 'fr', }); ctls.init(availableLanguageCodes, [], 'en'); expect(ctls.getCurrentContentLanguageCode()).toBe('fr'); }); - it('should correctly set the language to the first available preferred ' + - 'exploration language if there is no valid URL parameter', () => { - spyOn(us, 'getUrlParams').and.returnValue({}); - - ctls.init(availableLanguageCodes, ['fr'], 'en'); - expect(ctls.getCurrentContentLanguageCode()).toBe('fr'); - - ctls.init(availableLanguageCodes, ['zh'], 'en'); - expect(ctls.getCurrentContentLanguageCode()).toBe('zh'); - }); - - it('should correctly set the language to the exploration language code ' + - 'if there is no valid URL parameter and there are no matches with the ' + - 'preferred exploration languages', () => { - spyOn(us, 'getUrlParams').and.returnValue({ - initialContentLanguageCode: 'invalidLanguageCode' - }); - - ctls.init(availableLanguageCodes, [], 'fr'); - expect(ctls.getCurrentContentLanguageCode()).toBe('fr'); - - ctls.init(availableLanguageCodes, ['zz'], 'zh'); - expect(ctls.getCurrentContentLanguageCode()).toBe('zh'); - }); + it( + 'should correctly set the language to the first available preferred ' + + 'exploration language if there is no valid URL parameter', + () => { + spyOn(us, 'getUrlParams').and.returnValue({}); + + ctls.init(availableLanguageCodes, ['fr'], 'en'); + expect(ctls.getCurrentContentLanguageCode()).toBe('fr'); + + ctls.init(availableLanguageCodes, ['zh'], 'en'); + expect(ctls.getCurrentContentLanguageCode()).toBe('zh'); + } + ); + + it( + 'should correctly set the language to the exploration language code ' + + 'if there is no valid URL parameter and there are no matches with the ' + + 'preferred exploration languages', + () => { + spyOn(us, 'getUrlParams').and.returnValue({ + initialContentLanguageCode: 'invalidLanguageCode', + }); + + ctls.init(availableLanguageCodes, [], 'fr'); + expect(ctls.getCurrentContentLanguageCode()).toBe('fr'); + + ctls.init(availableLanguageCodes, ['zz'], 'zh'); + expect(ctls.getCurrentContentLanguageCode()).toBe('zh'); + } + ); it('should throw error if the exploration language code is invalid', () => { expect(() => { @@ -83,7 +88,7 @@ describe('Content translation language service', () => { expect(ctls.getLanguageOptionsForDropdown()).toEqual([ {value: 'fr', displayed: 'français (French)'}, {value: 'zh', displayed: '中文 (Chinese)'}, - {value: 'en', displayed: 'English'} + {value: 'en', displayed: 'English'}, ]); }); diff --git a/core/templates/pages/exploration-player-page/services/content-translation-language.service.ts b/core/templates/pages/exploration-player-page/services/content-translation-language.service.ts index 651ba94286a5..f75fc906be55 100644 --- a/core/templates/pages/exploration-player-page/services/content-translation-language.service.ts +++ b/core/templates/pages/exploration-player-page/services/content-translation-language.service.ts @@ -17,18 +17,17 @@ * used for content translations. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { ExplorationLanguageInfo } from - 'pages/exploration-player-page/services/audio-translation-language.service'; -import { LanguageUtilService } from 'domain/utilities/language-util.service'; -import { UrlService } from 'services/contextual/url.service'; +import {ExplorationLanguageInfo} from 'pages/exploration-player-page/services/audio-translation-language.service'; +import {LanguageUtilService} from 'domain/utilities/language-util.service'; +import {UrlService} from 'services/contextual/url.service'; -import { INITIAL_CONTENT_LANGUAGE_CODE_URL_PARAM } from 'pages/exploration-player-page/switch-content-language-refresh-required-modal.component'; +import {INITIAL_CONTENT_LANGUAGE_CODE_URL_PARAM} from 'pages/exploration-player-page/switch-content-language-refresh-required-modal.component'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ContentTranslationLanguageService { constructor( @@ -43,9 +42,9 @@ export class ContentTranslationLanguageService { private languageOptions: ExplorationLanguageInfo[] = []; _init( - allContentLanguageCodesInExploration: string[], - preferredContentLanguageCodes: string[], - explorationLanguageCode: string + allContentLanguageCodesInExploration: string[], + preferredContentLanguageCodes: string[], + explorationLanguageCode: string ): void { this.currentContentLanguageCode = ''; this.languageOptions = []; @@ -59,10 +58,12 @@ export class ContentTranslationLanguageService { if ( urlParams.hasOwnProperty(INITIAL_CONTENT_LANGUAGE_CODE_URL_PARAM) && allContentLanguageCodesInExploration.includes( - urlParams[INITIAL_CONTENT_LANGUAGE_CODE_URL_PARAM]) + urlParams[INITIAL_CONTENT_LANGUAGE_CODE_URL_PARAM] + ) ) { - this.setCurrentContentLanguageCode(urlParams[ - INITIAL_CONTENT_LANGUAGE_CODE_URL_PARAM]); + this.setCurrentContentLanguageCode( + urlParams[INITIAL_CONTENT_LANGUAGE_CODE_URL_PARAM] + ); } if ( @@ -77,43 +78,39 @@ export class ContentTranslationLanguageService { } } - if ( - !this.currentContentLanguageCode && - explorationLanguageCode !== null - ) { + if (!this.currentContentLanguageCode && explorationLanguageCode !== null) { this.currentContentLanguageCode = explorationLanguageCode; } allContentLanguageCodesInExploration.push(explorationLanguageCode); - allContentLanguageCodesInExploration.forEach( - (languageCode: string) => { - // TODO(#12341): Change getContentanguageDescription to instead refer to - // the list of languages that we support written translations for. (Note - // that this is not the same as "getContentLanguageDescription", because - // the latter refers to the language that the exploration is written - // in.) - let languageDescription = ( - this.languageUtilService.getContentLanguageDescription( - languageCode - ) - ); - if (!languageDescription) { - throw new Error('The exploration language code is invalid'); - } - this.languageOptions.push({ - value: languageCode, - displayed: languageDescription - }); + allContentLanguageCodesInExploration.forEach((languageCode: string) => { + // TODO(#12341): Change getContentanguageDescription to instead refer to + // the list of languages that we support written translations for. (Note + // that this is not the same as "getContentLanguageDescription", because + // the latter refers to the language that the exploration is written + // in.) + let languageDescription = + this.languageUtilService.getContentLanguageDescription(languageCode); + if (!languageDescription) { + throw new Error('The exploration language code is invalid'); + } + this.languageOptions.push({ + value: languageCode, + displayed: languageDescription, }); + }); } init( - allContentLanguageCodesInExploration: string[], - preferredContentLanguageCodes: string[], - explorationLanguageCode: string): void { + allContentLanguageCodesInExploration: string[], + preferredContentLanguageCodes: string[], + explorationLanguageCode: string + ): void { this._init( - allContentLanguageCodesInExploration, preferredContentLanguageCodes, - explorationLanguageCode); + allContentLanguageCodesInExploration, + preferredContentLanguageCodes, + explorationLanguageCode + ); } /** @@ -143,6 +140,9 @@ export class ContentTranslationLanguageService { } } -angular.module('oppia').factory( - 'ContentTranslationLanguageService', - downgradeInjectable(ContentTranslationLanguageService)); +angular + .module('oppia') + .factory( + 'ContentTranslationLanguageService', + downgradeInjectable(ContentTranslationLanguageService) + ); diff --git a/core/templates/pages/exploration-player-page/services/content-translation-manager.service.spec.ts b/core/templates/pages/exploration-player-page/services/content-translation-manager.service.spec.ts index 4f4233ef64b2..156a57aa798b 100644 --- a/core/templates/pages/exploration-player-page/services/content-translation-manager.service.spec.ts +++ b/core/templates/pages/exploration-player-page/services/content-translation-manager.service.spec.ts @@ -16,22 +16,27 @@ * @fileoverview Unit tests for the content translation manager service. */ -import { discardPeriodicTasks, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; - -import { InteractionObjectFactory } from 'domain/exploration/InteractionObjectFactory'; -import { RecordedVoiceovers } from 'domain/exploration/recorded-voiceovers.model'; -import { SubtitledUnicodeObjectFactory } from 'domain/exploration/SubtitledUnicodeObjectFactory'; -import { StateCard } from 'domain/state_card/state-card.model'; -import { ContentTranslationManagerService } from 'pages/exploration-player-page/services/content-translation-manager.service'; -import { PlayerTranscriptService } from 'pages/exploration-player-page/services/player-transcript.service'; -import { InteractionSpecsConstants } from 'pages/interaction-specs.constants'; -import { ExplorationHtmlFormatterService } from 'services/exploration-html-formatter.service'; -import { AudioTranslationLanguageService} from 'pages/exploration-player-page/services/audio-translation-language.service'; -import { EntityTranslationBackendApiService } from 'pages/exploration-editor-page/services/entity-translation-backend-api.service'; -import { EntityTranslation } from 'domain/translation/EntityTranslationObjectFactory'; -import { TranslatedContent } from 'domain/exploration/TranslatedContentObjectFactory'; -import { ImagePreloaderService } from 'pages/exploration-player-page/services/image-preloader.service'; +import { + discardPeriodicTasks, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; + +import {InteractionObjectFactory} from 'domain/exploration/InteractionObjectFactory'; +import {RecordedVoiceovers} from 'domain/exploration/recorded-voiceovers.model'; +import {SubtitledUnicodeObjectFactory} from 'domain/exploration/SubtitledUnicodeObjectFactory'; +import {StateCard} from 'domain/state_card/state-card.model'; +import {ContentTranslationManagerService} from 'pages/exploration-player-page/services/content-translation-manager.service'; +import {PlayerTranscriptService} from 'pages/exploration-player-page/services/player-transcript.service'; +import {InteractionSpecsConstants} from 'pages/interaction-specs.constants'; +import {ExplorationHtmlFormatterService} from 'services/exploration-html-formatter.service'; +import {AudioTranslationLanguageService} from 'pages/exploration-player-page/services/audio-translation-language.service'; +import {EntityTranslationBackendApiService} from 'pages/exploration-editor-page/services/entity-translation-backend-api.service'; +import {EntityTranslation} from 'domain/translation/EntityTranslationObjectFactory'; +import {TranslatedContent} from 'domain/exploration/TranslatedContentObjectFactory'; +import {ImagePreloaderService} from 'pages/exploration-player-page/services/image-preloader.service'; describe('Content translation manager service', () => { let ctms: ContentTranslationManagerService; @@ -46,7 +51,7 @@ describe('Content translation manager service', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] + imports: [HttpClientTestingModule], }).compileComponents(); ctms = TestBed.inject(ContentTranslationManagerService); ehfs = TestBed.inject(ExplorationHtmlFormatterService); @@ -66,90 +71,95 @@ describe('Content translation manager service', () => { content: { content_format: 'html', content_value: '

fr content

', - needs_update: false + needs_update: false, }, hint_0: { content_format: 'html', content_value: '

fr hint

', - needs_update: false + needs_update: false, }, solution: { content_format: 'html', content_value: '

fr solution

', - needs_update: false + needs_update: false, }, ca_placeholder_0: { content_format: 'unicode', content_value: 'fr placeholder', - needs_update: false + needs_update: false, }, outcome_1: { content_format: 'html', content_value: '

fr feedback

', - needs_update: false + needs_update: false, }, default_outcome: { content_format: 'html', content_value: '

fr default outcome

', - needs_update: false + needs_update: false, }, rule_input_3: { content_format: 'set_of_normalized_string', content_value: ['fr rule input 1', 'fr rule input 2'], - needs_update: false - } - } + needs_update: false, + }, + }, }); spyOn(etbs, 'fetchEntityTranslationAsync').and.returnValue( Promise.resolve(entityTranslation) ); spyOn(imagePreloaderService, 'restartImagePreloader').and.returnValue( - undefined); + undefined + ); let defaultOutcomeDict = { dest: 'dest_default', dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: '

en default outcome

' + html: '

en default outcome

', }, labelled_as_correct: false, param_changes: [], refresher_exploration_id: null, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }; - let answerGroupsDict = [{ - rule_specs: [{ - inputs: { - x: { - contentId: 'rule_input_3', - normalizedStrSet: ['InputString'] - } - }, - rule_type: 'Equals' - }], - outcome: { - dest: 'dest_1', - dest_if_really_stuck: null, - feedback: { - content_id: 'outcome_1', - html: '

en feedback

' + let answerGroupsDict = [ + { + rule_specs: [ + { + inputs: { + x: { + contentId: 'rule_input_3', + normalizedStrSet: ['InputString'], + }, + }, + rule_type: 'Equals', + }, + ], + outcome: { + dest: 'dest_1', + dest_if_really_stuck: null, + feedback: { + content_id: 'outcome_1', + html: '

en feedback

', + }, + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, }, - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null + training_data: ['training_data'], + tagged_skill_misconception_id: 'skill_id-1', }, - training_data: ['training_data'], - tagged_skill_misconception_id: 'skill_id-1' - }]; + ]; let hintsDict = [ { hint_content: { html: '

en hint

', - content_id: 'hint_0' - } - } + content_id: 'hint_0', + }, + }, ]; let solutionDict = { @@ -157,8 +167,8 @@ describe('Content translation manager service', () => { correct_answer: 'This is a correct answer!', explanation: { content_id: 'solution', - html: '

en solution

' - } + html: '

en solution

', + }, }; let interactionDict = { @@ -168,18 +178,18 @@ describe('Content translation manager service', () => { placeholder: { value: { content_id: 'ca_placeholder_0', - unicode_str: 'en placeholder' - } + unicode_str: 'en placeholder', + }, }, - rows: { value: 1 }, + rows: {value: 1}, catchMisspellings: { - value: false - } + value: false, + }, }, default_outcome: defaultOutcomeDict, hints: hintsDict, id: 'TextInput', - solution: solutionDict + solution: solutionDict, }; const interaction = iof.createFromBackendDict(interactionDict); @@ -214,13 +224,13 @@ describe('Content translation manager service', () => { placeholder: { value: suof.createFromBackendDict({ unicode_str: 'fr placeholder', - content_id: 'ca_placeholder_0' - }) + content_id: 'ca_placeholder_0', + }), }, rows: {value: 1}, catchMisspellings: { - value: false - } + value: false, + }, }; expect(card.contentHtml).toBe('

fr content

'); @@ -228,56 +238,58 @@ describe('Content translation manager service', () => { expect(interaction.solution?.explanation.html).toBe('

fr solution

'); expect(interaction.customizationArgs).toEqual(translatedCustomizationArgs); expect(interaction.answerGroups[0].outcome.feedback.html).toBe( - '

fr feedback

'); + '

fr feedback

' + ); expect(interaction.answerGroups[0].rules[0].inputs.x).toEqual({ contentId: 'rule_input_3', - normalizedStrSet: ['fr rule input 1', 'fr rule input 2'] + normalizedStrSet: ['fr rule input 1', 'fr rule input 2'], }); expect(interaction.defaultOutcome?.feedback.html).toBe( - '

fr default outcome

'); + '

fr default outcome

' + ); discardPeriodicTasks(); })); - it('should switch to a new language expect invalid translations', fakeAsync( - () => { - ctms.setOriginalTranscript('en'); - const card = pts.transcript[0]; - const interaction = card.getInteraction(); - const translatedCustomizationArgs = { - placeholder: { - value: suof.createFromBackendDict({ - unicode_str: 'fr placeholder', - content_id: 'ca_placeholder_0' - }) - }, - rows: {value: 1}, - catchMisspellings: { - value: false - } - }; + it('should switch to a new language expect invalid translations', fakeAsync(() => { + ctms.setOriginalTranscript('en'); + const card = pts.transcript[0]; + const interaction = card.getInteraction(); + const translatedCustomizationArgs = { + placeholder: { + value: suof.createFromBackendDict({ + unicode_str: 'fr placeholder', + content_id: 'ca_placeholder_0', + }), + }, + rows: {value: 1}, + catchMisspellings: { + value: false, + }, + }; - entityTranslation.markTranslationAsNeedingUpdate('hint_0'); - etbs.fetchEntityTranslationAsync = jasmine.createSpy().and.returnValue( - Promise.resolve(entityTranslation) - ); - ctms.displayTranslations('fr'); - tick(); + entityTranslation.markTranslationAsNeedingUpdate('hint_0'); + etbs.fetchEntityTranslationAsync = jasmine + .createSpy() + .and.returnValue(Promise.resolve(entityTranslation)); + ctms.displayTranslations('fr'); + tick(); - expect(card.contentHtml).toBe('

fr content

'); - expect(interaction.hints[0].hintContent.html).toBe('

fr hint

'); - expect(interaction.solution?.explanation.html).toBe('

fr solution

'); - expect(interaction.customizationArgs).toEqual( - translatedCustomizationArgs); - expect(interaction.answerGroups[0].outcome.feedback.html).toBe( - '

fr feedback

'); - expect(interaction.answerGroups[0].rules[0].inputs.x).toEqual({ - contentId: 'rule_input_3', - normalizedStrSet: ['fr rule input 1', 'fr rule input 2'] - }); - expect(interaction.defaultOutcome?.feedback.html).toBe( - '

fr default outcome

'); - discardPeriodicTasks(); - })); + expect(card.contentHtml).toBe('

fr content

'); + expect(interaction.hints[0].hintContent.html).toBe('

fr hint

'); + expect(interaction.solution?.explanation.html).toBe('

fr solution

'); + expect(interaction.customizationArgs).toEqual(translatedCustomizationArgs); + expect(interaction.answerGroups[0].outcome.feedback.html).toBe( + '

fr feedback

' + ); + expect(interaction.answerGroups[0].rules[0].inputs.x).toEqual({ + contentId: 'rule_input_3', + normalizedStrSet: ['fr rule input 1', 'fr rule input 2'], + }); + expect(interaction.defaultOutcome?.feedback.html).toBe( + '

fr default outcome

' + ); + discardPeriodicTasks(); + })); it('should switch back to the original language', fakeAsync(() => { ctms.setOriginalTranscript('en'); @@ -291,13 +303,13 @@ describe('Content translation manager service', () => { placeholder: { value: suof.createFromBackendDict({ unicode_str: 'en placeholder', - content_id: 'ca_placeholder_0' - }) + content_id: 'ca_placeholder_0', + }), }, rows: {value: 1}, catchMisspellings: { - value: false - } + value: false, + }, }; expect(card.contentHtml).toBe('

en content

'); @@ -305,72 +317,83 @@ describe('Content translation manager service', () => { expect(interaction.solution?.explanation.html).toBe('

en solution

'); expect(interaction.customizationArgs).toEqual(originalCustomizationArgs); expect(interaction.answerGroups[0].outcome.feedback.html).toBe( - '

en feedback

'); + '

en feedback

' + ); expect(interaction.answerGroups[0].rules[0].inputs.x).toEqual({ contentId: 'rule_input_3', - normalizedStrSet: ['InputString'] + normalizedStrSet: ['InputString'], }); expect(interaction.defaultOutcome?.feedback.html).toBe( - '

en default outcome

'); + '

en default outcome

' + ); discardPeriodicTasks(); })); - it('should emit to onStateCardContentUpdateEmitter when the ' + - 'language is changed', fakeAsync(() => { - const onStateCardContentUpdate = spyOn( - ctms.onStateCardContentUpdate, 'emit'); - ctms.setOriginalTranscript('en'); - ctms.displayTranslations('fr'); - tick(); + it( + 'should emit to onStateCardContentUpdateEmitter when the ' + + 'language is changed', + fakeAsync(() => { + const onStateCardContentUpdate = spyOn( + ctms.onStateCardContentUpdate, + 'emit' + ); + ctms.setOriginalTranscript('en'); + ctms.displayTranslations('fr'); + tick(); - expect(onStateCardContentUpdate).toHaveBeenCalled(); - discardPeriodicTasks(); - })); + expect(onStateCardContentUpdate).toHaveBeenCalled(); + discardPeriodicTasks(); + }) + ); it('should not switch rules if the replacement is empty', () => { let newInteractionDict = { - answer_groups: [{ - rule_specs: [{ - inputs: { - x: { - contentId: 'rule_input_3', - normalizedStrSet: ['InputString'] - } - }, - rule_type: 'Equals' - }], - outcome: { - dest: 'dest_1', - dest_if_really_stuck: null, - feedback: { - content_id: 'outcome_1', - html: '

en feedback

' + answer_groups: [ + { + rule_specs: [ + { + inputs: { + x: { + contentId: 'rule_input_3', + normalizedStrSet: ['InputString'], + }, + }, + rule_type: 'Equals', + }, + ], + outcome: { + dest: 'dest_1', + dest_if_really_stuck: null, + feedback: { + content_id: 'outcome_1', + html: '

en feedback

', + }, + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, }, - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null + training_data: [], + tagged_skill_misconception_id: null, }, - training_data: [], - tagged_skill_misconception_id: null - }], + ], confirmed_unclassified_answers: [], customization_args: { placeholder: { value: { content_id: 'ca_placeholder_0', - unicode_str: 'en placeholder' - } + unicode_str: 'en placeholder', + }, }, - rows: { value: 1 }, + rows: {value: 1}, catchMisspellings: { - value: false - } + value: false, + }, }, default_outcome: null, hints: [], id: 'TextInput', - solution: null + solution: null, }; pts.init(); @@ -384,7 +407,8 @@ describe('Content translation manager service', () => { newInteraction.customizationArgs, true, null, - null), + null + ), newInteraction, RecordedVoiceovers.createEmpty(), 'content', @@ -396,7 +420,7 @@ describe('Content translation manager service', () => { ctms.displayTranslations('fr'); expect(newInteraction.answerGroups[0].rules[0].inputs.x).toEqual({ contentId: 'rule_input_3', - normalizedStrSet: ['InputString'] + normalizedStrSet: ['InputString'], }); }); @@ -406,27 +430,32 @@ describe('Content translation manager service', () => { // "Property 'DummyInteraction' does not exist on type". // @ts-expect-error InteractionSpecsConstants.INTERACTION_SPECS.DummyInteraction = { - customization_arg_specs: [{ - name: 'dummyCustArg', - schema: { - type: 'list', - items: { - type: 'dict', - properties: [{ - name: 'content', - schema: { - type: 'custom', - obj_type: 'SubtitledUnicode' - } - }, { - name: 'show', - schema: { - type: 'boolean' - } - }] - } - } - }] + customization_arg_specs: [ + { + name: 'dummyCustArg', + schema: { + type: 'list', + items: { + type: 'dict', + properties: [ + { + name: 'content', + schema: { + type: 'custom', + obj_type: 'SubtitledUnicode', + }, + }, + { + name: 'show', + schema: { + type: 'boolean', + }, + }, + ], + }, + }, + }, + ], }; }); @@ -442,30 +471,40 @@ describe('Content translation manager service', () => { const interaction = card.getInteraction(); entityTranslation.translationMapping.ca_0 = new TranslatedContent( - 'fr 1', 'unicode', false); + 'fr 1', + 'unicode', + false + ); entityTranslation.translationMapping.ca_1 = new TranslatedContent( - 'fr 2', 'unicode', false); - - etbs.fetchEntityTranslationAsync = jasmine.createSpy().and.returnValue( - Promise.resolve(entityTranslation) + 'fr 2', + 'unicode', + false ); + etbs.fetchEntityTranslationAsync = jasmine + .createSpy() + .and.returnValue(Promise.resolve(entityTranslation)); + interaction.id = 'DummyInteraction'; interaction.customizationArgs = { - dummyCustArg: {value: [{ - content: suof.createFromBackendDict({ - unicode_str: 'first', - content_id: 'ca_0' - }), - show: true + dummyCustArg: { + value: [ + { + content: suof.createFromBackendDict({ + unicode_str: 'first', + content_id: 'ca_0', + }), + show: true, + }, + { + content: suof.createFromBackendDict({ + unicode_str: 'first', + content_id: 'ca_1', + }), + show: true, + }, + ], }, - { - content: suof.createFromBackendDict({ - unicode_str: 'first', - content_id: 'ca_1' - }), - show: true - }]} }; ctms.setOriginalTranscript('en'); @@ -473,20 +512,24 @@ describe('Content translation manager service', () => { tick(); expect(interaction.customizationArgs).toEqual({ - dummyCustArg: {value: [{ - content: suof.createFromBackendDict({ - unicode_str: 'fr 1', - content_id: 'ca_0' - }), - show: true + dummyCustArg: { + value: [ + { + content: suof.createFromBackendDict({ + unicode_str: 'fr 1', + content_id: 'ca_0', + }), + show: true, + }, + { + content: suof.createFromBackendDict({ + unicode_str: 'fr 2', + content_id: 'ca_1', + }), + show: true, + }, + ], }, - { - content: suof.createFromBackendDict({ - unicode_str: 'fr 2', - content_id: 'ca_1' - }), - show: true - }]} }); discardPeriodicTasks(); })); diff --git a/core/templates/pages/exploration-player-page/services/content-translation-manager.service.ts b/core/templates/pages/exploration-player-page/services/content-translation-manager.service.ts index 426c157f0b22..4fe19d1d97ab 100644 --- a/core/templates/pages/exploration-player-page/services/content-translation-manager.service.ts +++ b/core/templates/pages/exploration-player-page/services/content-translation-manager.service.ts @@ -16,29 +16,29 @@ * @fileoverview Service to manage the content translations displayed. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { EventEmitter, Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {EventEmitter, Injectable} from '@angular/core'; import cloneDeep from 'lodash/cloneDeep'; -import { PlayerTranscriptService } from 'pages/exploration-player-page/services/player-transcript.service'; -import { StateCard } from 'domain/state_card/state-card.model'; -import { ExtensionTagAssemblerService } from 'services/extension-tag-assembler.service'; -import { EntityTranslation } from 'domain/translation/EntityTranslationObjectFactory'; -import { InteractionCustomizationArgs } from 'interactions/customization-args-defs'; -import { EntityTranslationsService } from 'services/entity-translations.services'; -import { ContextService } from 'services/context.service'; -import { ImagePreloaderService } from './image-preloader.service'; +import {PlayerTranscriptService} from 'pages/exploration-player-page/services/player-transcript.service'; +import {StateCard} from 'domain/state_card/state-card.model'; +import {ExtensionTagAssemblerService} from 'services/extension-tag-assembler.service'; +import {EntityTranslation} from 'domain/translation/EntityTranslationObjectFactory'; +import {InteractionCustomizationArgs} from 'interactions/customization-args-defs'; +import {EntityTranslationsService} from 'services/entity-translations.services'; +import {ContextService} from 'services/context.service'; +import {ImagePreloaderService} from './image-preloader.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ContentTranslationManagerService { // This is initialized using the class initialization method. // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 private explorationLanguageCode!: string; - private onStateCardContentUpdateEmitter: EventEmitter = ( - new EventEmitter()); + private onStateCardContentUpdateEmitter: EventEmitter = + new EventEmitter(); // The 'originalTranscript' is a copy of the transcript in the exploration // language in it's initial state. @@ -55,7 +55,8 @@ export class ContentTranslationManagerService { setOriginalTranscript(explorationLanguageCode: string): void { this.explorationLanguageCode = explorationLanguageCode; this.originalTranscript = cloneDeep( - this.playerTranscriptService.transcript); + this.playerTranscriptService.transcript + ); } get onStateCardContentUpdate(): EventEmitter { @@ -76,28 +77,32 @@ export class ContentTranslationManagerService { displayTranslations(languageCode: string): void { if (languageCode === this.explorationLanguageCode) { this.playerTranscriptService.restoreImmutably( - cloneDeep(this.originalTranscript)); + cloneDeep(this.originalTranscript) + ); this.onStateCardContentUpdateEmitter.emit(); } else { - this.entityTranslationsService.getEntityTranslationsAsync( - languageCode - ).then((entityTranslations) => { - // Image preloading is disabled in the exploration editor preview mode. - if (!this.contextService.isInExplorationEditorPage()) { - this.imagePreloaderService.restartImagePreloader( - this.playerTranscriptService.getCard(0).getStateName()); - } + this.entityTranslationsService + .getEntityTranslationsAsync(languageCode) + .then(entityTranslations => { + // Image preloading is disabled in the exploration editor preview mode. + if (!this.contextService.isInExplorationEditorPage()) { + this.imagePreloaderService.restartImagePreloader( + this.playerTranscriptService.getCard(0).getStateName() + ); + } - const cards = this.playerTranscriptService.transcript; - cards.forEach( - card => this._displayTranslationsForCard(card, entityTranslations)); - this.onStateCardContentUpdateEmitter.emit(); - }); + const cards = this.playerTranscriptService.transcript; + cards.forEach(card => + this._displayTranslationsForCard(card, entityTranslations) + ); + this.onStateCardContentUpdateEmitter.emit(); + }); } } _displayTranslationsForCard( - card: StateCard, entityTranslations: EntityTranslation + card: StateCard, + entityTranslations: EntityTranslation ): void { card.swapContentsWithTranslation(entityTranslations); if (card.getInteractionId()) { @@ -105,7 +110,8 @@ export class ContentTranslationManagerService { // the HTML string and it's body contains our required element // as a childnode. const element = new DOMParser().parseFromString( - card.getInteractionHtml(), 'text/html' + card.getInteractionHtml(), + 'text/html' ).body.childNodes[0] as HTMLElement; this.extensionTagAssemblerService.formatCustomizationArgAttrs( element, @@ -116,6 +122,9 @@ export class ContentTranslationManagerService { } } -angular.module('oppia').factory( - 'ContentTranslationManagerService', - downgradeInjectable(ContentTranslationManagerService)); +angular + .module('oppia') + .factory( + 'ContentTranslationManagerService', + downgradeInjectable(ContentTranslationManagerService) + ); diff --git a/core/templates/pages/exploration-player-page/services/current-interaction.service.spec.ts b/core/templates/pages/exploration-player-page/services/current-interaction.service.spec.ts index 9e59bd39b02d..2a5433ab6626 100644 --- a/core/templates/pages/exploration-player-page/services/current-interaction.service.spec.ts +++ b/core/templates/pages/exploration-player-page/services/current-interaction.service.spec.ts @@ -16,18 +16,22 @@ * @fileoverview Unit tests for CurrentInteractionService. */ -import { TestBed } from '@angular/core/testing'; - -import { CurrentInteractionService, OnSubmitFn, ValidityCheckFn } from 'pages/exploration-player-page/services/current-interaction.service'; -import { UrlService } from 'services/contextual/url.service'; -import { PlayerPositionService } from 'pages/exploration-player-page/services/player-position.service'; -import { PlayerTranscriptService } from 'pages/exploration-player-page/services/player-transcript.service'; -import { StateCard } from 'domain/state_card/state-card.model'; -import { ContextService } from 'services/context.service'; -import { InteractionRulesService } from './answer-classification.service'; -import { Interaction } from 'domain/exploration/InteractionObjectFactory'; -import { RecordedVoiceovers } from 'domain/exploration/recorded-voiceovers.model'; -import { AudioTranslationLanguageService } from './audio-translation-language.service'; +import {TestBed} from '@angular/core/testing'; + +import { + CurrentInteractionService, + OnSubmitFn, + ValidityCheckFn, +} from 'pages/exploration-player-page/services/current-interaction.service'; +import {UrlService} from 'services/contextual/url.service'; +import {PlayerPositionService} from 'pages/exploration-player-page/services/player-position.service'; +import {PlayerTranscriptService} from 'pages/exploration-player-page/services/player-transcript.service'; +import {StateCard} from 'domain/state_card/state-card.model'; +import {ContextService} from 'services/context.service'; +import {InteractionRulesService} from './answer-classification.service'; +import {Interaction} from 'domain/exploration/InteractionObjectFactory'; +import {RecordedVoiceovers} from 'domain/exploration/recorded-voiceovers.model'; +import {AudioTranslationLanguageService} from './audio-translation-language.service'; describe('Current Interaction Service', () => { let urlService: UrlService; @@ -39,8 +43,15 @@ describe('Current Interaction Service', () => { let interactionRulesService: InteractionRulesService; let audioTranslationLanguageService: AudioTranslationLanguageService; const displayedCard = new StateCard( - '', '', '', {} as Interaction, [], - {} as RecordedVoiceovers, '', {} as AudioTranslationLanguageService); + '', + '', + '', + {} as Interaction, + [], + {} as RecordedVoiceovers, + '', + {} as AudioTranslationLanguageService + ); // This mock is required since ContextService is used in // CurrentInteractionService to obtain the explorationId. So, in the @@ -58,7 +69,6 @@ describe('Current Interaction Service', () => { contextService = TestBed.inject(ContextService); }); - it('should properly register onSubmitFn and submitAnswerFn', () => { let answerState = null; let dummyOnSubmitFn: OnSubmitFn = (answer: Object) => { @@ -77,7 +87,9 @@ describe('Current Interaction Service', () => { currentInteractionService.onSubmit(DUMMY_ANSWER, interactionRulesService); }; currentInteractionService.registerCurrentInteraction( - dummySubmitAnswerFn, dummyValidityCheckFn); + dummySubmitAnswerFn, + dummyValidityCheckFn + ); currentInteractionService.submitAnswer(); expect(answerState).toEqual(DUMMY_ANSWER); }); @@ -90,9 +102,12 @@ describe('Current Interaction Service', () => { return false; }; currentInteractionService.registerCurrentInteraction( - dummySubmitAnswerFn, dummyValidityCheckFn); + dummySubmitAnswerFn, + dummyValidityCheckFn + ); expect(currentInteractionService.isSubmitButtonDisabled()).toBe( - !dummyValidityCheckFn()); + !dummyValidityCheckFn() + ); }); it('should handle case where validityCheckFn is null', () => { @@ -100,13 +115,14 @@ describe('Current Interaction Service', () => { return false; }; currentInteractionService.registerCurrentInteraction( - dummySubmitAnswerFn, null); + dummySubmitAnswerFn, + null + ); expect(currentInteractionService.isSubmitButtonDisabled()).toBe(false); }); it('should handle case where submitAnswerFn is null', () => { - currentInteractionService.registerCurrentInteraction( - null, null); + currentInteractionService.registerCurrentInteraction(null, null); expect(currentInteractionService.isSubmitButtonDisabled()).toBe(true); }); @@ -142,25 +158,33 @@ describe('Current Interaction Service', () => { spyOn(playerPositionService, 'getDisplayedCardIndex').and.returnValue(1); spyOn(playerTranscriptService, 'getCard').and.returnValue( StateCard.createNewCard( - 'First State', 'Content HTML', + 'First State', + 'Content HTML', '', - interaction, recordedVoiceovers, '', audioTranslationLanguageService)); + interaction, + recordedVoiceovers, + '', + audioTranslationLanguageService + ) + ); spyOn(contextService, 'getExplorationId').and.returnValue('abc'); spyOn(contextService, 'getPageContext').and.returnValue('learner'); - let additionalInfo = ( + let additionalInfo = '\nUndefined submit answer debug logs:' + '\nInteraction ID: null' + '\nExploration ID: abc' + '\nState Name: First State' + '\nContext: learner' + - '\nErrored at index: 1'); + '\nErrored at index: 1'; currentInteractionService.registerCurrentInteraction(null, null); expect(() => currentInteractionService.submitAnswer()).toThrowError( - 'The current interaction did not ' + 'register a _submitAnswerFn.' + - additionalInfo); + 'The current interaction did not ' + + 'register a _submitAnswerFn.' + + additionalInfo + ); }); it('should update view with new answer', () => { @@ -178,8 +202,7 @@ describe('Current Interaction Service', () => { }); it('should return display card', () => { - spyOn( - playerPositionService, 'getDisplayedCardIndex').and.returnValue(1); + spyOn(playerPositionService, 'getDisplayedCardIndex').and.returnValue(1); spyOn(playerTranscriptService, 'getCard').and.returnValue(displayedCard); expect(currentInteractionService.getDisplayedCard()).toEqual(displayedCard); @@ -187,20 +210,21 @@ describe('Current Interaction Service', () => { it('should update current answer', () => { spyOn(displayedCard, 'updateCurrentAnswer'); - spyOn( - currentInteractionService, - 'getDisplayedCard').and.returnValue(displayedCard); + spyOn(currentInteractionService, 'getDisplayedCard').and.returnValue( + displayedCard + ); currentInteractionService.updateCurrentAnswer('answer'); - expect( - displayedCard.updateCurrentAnswer).toHaveBeenCalledOnceWith('answer'); + expect(displayedCard.updateCurrentAnswer).toHaveBeenCalledOnceWith( + 'answer' + ); }); it('should check if "no response error" should be displayed', () => { - spyOn( - currentInteractionService, - 'getDisplayedCard').and.returnValue(displayedCard); + spyOn(currentInteractionService, 'getDisplayedCard').and.returnValue( + displayedCard + ); spyOn(displayedCard, 'showNoResponseError').and.returnValue(true); expect(currentInteractionService.showNoResponseError()).toBeTrue(); diff --git a/core/templates/pages/exploration-player-page/services/current-interaction.service.ts b/core/templates/pages/exploration-player-page/services/current-interaction.service.ts index b8c60ec63241..a830c5f7d757 100644 --- a/core/templates/pages/exploration-player-page/services/current-interaction.service.ts +++ b/core/templates/pages/exploration-player-page/services/current-interaction.service.ts @@ -19,46 +19,45 @@ * answer submission process. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; - -import { ContextService } from 'services/context.service'; -import { PlayerPositionService } from 'pages/exploration-player-page/services/player-position.service'; -import { PlayerTranscriptService } from 'pages/exploration-player-page/services/player-transcript.service'; -import { Observable, Subject } from 'rxjs'; -import { AlgebraicExpressionInputRulesService } from 'interactions/AlgebraicExpressionInput/directives/algebraic-expression-input-rules.service'; -import { CodeReplRulesService } from 'interactions/CodeRepl/directives/code-repl-rules.service'; -import { ContinueRulesService } from 'interactions/Continue/directives/continue-rules.service'; -import { FractionInputRulesService } from 'interactions/FractionInput/directives/fraction-input-rules.service'; -import { ImageClickInputRulesService } from 'interactions/ImageClickInput/directives/image-click-input-rules.service'; -import { InteractiveMapRulesService } from 'interactions/InteractiveMap/directives/interactive-map-rules.service'; -import { MathEquationInputRulesService } from 'interactions/MathEquationInput/directives/math-equation-input-rules.service'; -import { NumericExpressionInputRulesService } from 'interactions/NumericExpressionInput/directives/numeric-expression-input-rules.service'; -import { NumericInputRulesService } from 'interactions/NumericInput/directives/numeric-input-rules.service'; -import { PencilCodeEditorRulesService } from 'interactions/PencilCodeEditor/directives/pencil-code-editor-rules.service'; -import { GraphInputRulesService } from 'interactions/GraphInput/directives/graph-input-rules.service'; -import { SetInputRulesService } from 'interactions/SetInput/directives/set-input-rules.service'; -import { TextInputRulesService } from 'interactions/TextInput/directives/text-input-rules.service'; -import { InteractionAnswer } from 'interactions/answer-defs'; -import { StateCard } from 'domain/state_card/state-card.model'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; + +import {ContextService} from 'services/context.service'; +import {PlayerPositionService} from 'pages/exploration-player-page/services/player-position.service'; +import {PlayerTranscriptService} from 'pages/exploration-player-page/services/player-transcript.service'; +import {Observable, Subject} from 'rxjs'; +import {AlgebraicExpressionInputRulesService} from 'interactions/AlgebraicExpressionInput/directives/algebraic-expression-input-rules.service'; +import {CodeReplRulesService} from 'interactions/CodeRepl/directives/code-repl-rules.service'; +import {ContinueRulesService} from 'interactions/Continue/directives/continue-rules.service'; +import {FractionInputRulesService} from 'interactions/FractionInput/directives/fraction-input-rules.service'; +import {ImageClickInputRulesService} from 'interactions/ImageClickInput/directives/image-click-input-rules.service'; +import {InteractiveMapRulesService} from 'interactions/InteractiveMap/directives/interactive-map-rules.service'; +import {MathEquationInputRulesService} from 'interactions/MathEquationInput/directives/math-equation-input-rules.service'; +import {NumericExpressionInputRulesService} from 'interactions/NumericExpressionInput/directives/numeric-expression-input-rules.service'; +import {NumericInputRulesService} from 'interactions/NumericInput/directives/numeric-input-rules.service'; +import {PencilCodeEditorRulesService} from 'interactions/PencilCodeEditor/directives/pencil-code-editor-rules.service'; +import {GraphInputRulesService} from 'interactions/GraphInput/directives/graph-input-rules.service'; +import {SetInputRulesService} from 'interactions/SetInput/directives/set-input-rules.service'; +import {TextInputRulesService} from 'interactions/TextInput/directives/text-input-rules.service'; +import {InteractionAnswer} from 'interactions/answer-defs'; +import {StateCard} from 'domain/state_card/state-card.model'; type SubmitAnswerFn = () => void; -type InteractionRulesService = ( - AlgebraicExpressionInputRulesService | - CodeReplRulesService | - ContinueRulesService | - FractionInputRulesService | - ImageClickInputRulesService | - InteractiveMapRulesService | - MathEquationInputRulesService | - NumericExpressionInputRulesService | - NumericInputRulesService | - PencilCodeEditorRulesService | - GraphInputRulesService | - SetInputRulesService | - TextInputRulesService -); +type InteractionRulesService = + | AlgebraicExpressionInputRulesService + | CodeReplRulesService + | ContinueRulesService + | FractionInputRulesService + | ImageClickInputRulesService + | InteractiveMapRulesService + | MathEquationInputRulesService + | NumericExpressionInputRulesService + | NumericInputRulesService + | PencilCodeEditorRulesService + | GraphInputRulesService + | SetInputRulesService + | TextInputRulesService; export type OnSubmitFn = ( answer: InteractionAnswer, @@ -70,13 +69,14 @@ export type ValidityCheckFn = () => boolean; export type PresubmitHookFn = () => void; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class CurrentInteractionService { constructor( private contextService: ContextService, private playerPositionService: PlayerPositionService, - private playerTranscriptService: PlayerTranscriptService) {} + private playerTranscriptService: PlayerTranscriptService + ) {} // The 'submitAnswerFn' function should grab the learner's answer and pass // it to onSubmit. The interaction can pass in 'null' for this property if @@ -103,8 +103,8 @@ export class CurrentInteractionService { } registerCurrentInteraction( - submitAnswerFn: SubmitAnswerFn | null, - validityCheckFn: ValidityCheckFn | null + submitAnswerFn: SubmitAnswerFn | null, + validityCheckFn: ValidityCheckFn | null ): void { /** * Each interaction directive should call registerCurrentInteraction @@ -139,11 +139,10 @@ export class CurrentInteractionService { } onSubmit( - answer: InteractionAnswer, - interactionRulesService: InteractionRulesService + answer: InteractionAnswer, + interactionRulesService: InteractionRulesService ): void { - for ( - let i = 0; i < CurrentInteractionService.presubmitHooks.length; i++) { + for (let i = 0; i < CurrentInteractionService.presubmitHooks.length; i++) { CurrentInteractionService.presubmitHooks[i](); } CurrentInteractionService.onSubmitFn(answer, interactionRulesService); @@ -169,16 +168,23 @@ export class CurrentInteractionService { if (CurrentInteractionService.submitAnswerFn === null) { let index = this.playerPositionService.getDisplayedCardIndex(); let displayedCard = this.playerTranscriptService.getCard(index); - let additionalInfo = ( + let additionalInfo = '\nUndefined submit answer debug logs:' + - '\nInteraction ID: ' + displayedCard.getInteractionId() + - '\nExploration ID: ' + this.contextService.getExplorationId() + - '\nState Name: ' + displayedCard.getStateName() + - '\nContext: ' + this.contextService.getPageContext() + - '\nErrored at index: ' + index); + '\nInteraction ID: ' + + displayedCard.getInteractionId() + + '\nExploration ID: ' + + this.contextService.getExplorationId() + + '\nState Name: ' + + displayedCard.getStateName() + + '\nContext: ' + + this.contextService.getPageContext() + + '\nErrored at index: ' + + index; throw new Error( 'The current interaction did not ' + - 'register a _submitAnswerFn.' + additionalInfo); + 'register a _submitAnswerFn.' + + additionalInfo + ); } else { CurrentInteractionService.submitAnswerFn(); } @@ -213,5 +219,9 @@ export class CurrentInteractionService { return CurrentInteractionService.answerChangedSubject.asObservable(); } } -angular.module('oppia').factory('CurrentInteractionService', - downgradeInjectable(CurrentInteractionService)); +angular + .module('oppia') + .factory( + 'CurrentInteractionService', + downgradeInjectable(CurrentInteractionService) + ); diff --git a/core/templates/pages/exploration-player-page/services/diagnostic-test-player-engine.service.spec.ts b/core/templates/pages/exploration-player-page/services/diagnostic-test-player-engine.service.spec.ts index 861a95336859..1b22146733c5 100644 --- a/core/templates/pages/exploration-player-page/services/diagnostic-test-player-engine.service.spec.ts +++ b/core/templates/pages/exploration-player-page/services/diagnostic-test-player-engine.service.spec.ts @@ -16,22 +16,27 @@ * @fileoverview Unit tests for the diagnostic test player engine service. */ - -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { fakeAsync, TestBed, tick} from '@angular/core/testing'; -import { DiagnosticTestQuestionsModel } from 'domain/question/diagnostic-test-questions.model'; -import { QuestionBackendApiService } from 'domain/question/question-backend-api.service'; -import { Question, QuestionObjectFactory, QuestionBackendDict } from 'domain/question/QuestionObjectFactory'; -import { StateObjectFactory } from 'domain/state/StateObjectFactory'; -import { DiagnosticTestTopicTrackerModel } from 'pages/diagnostic-test-player-page/diagnostic-test-topic-tracker.model'; -import { DiagnosticTestPlayerEngineService } from './diagnostic-test-player-engine.service'; -import { TextInputRulesService } from 'interactions/TextInput/directives/text-input-rules.service'; -import { AnswerClassificationResult } from 'domain/classifier/answer-classification-result.model'; -import { OutcomeObjectFactory } from 'domain/exploration/OutcomeObjectFactory'; -import { AnswerClassificationService, InteractionRulesService } from './answer-classification.service'; -import { AlertsService } from 'services/alerts.service'; -import { ExpressionInterpolationService } from 'expressions/expression-interpolation.service'; - +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {fakeAsync, TestBed, tick} from '@angular/core/testing'; +import {DiagnosticTestQuestionsModel} from 'domain/question/diagnostic-test-questions.model'; +import {QuestionBackendApiService} from 'domain/question/question-backend-api.service'; +import { + Question, + QuestionObjectFactory, + QuestionBackendDict, +} from 'domain/question/QuestionObjectFactory'; +import {StateObjectFactory} from 'domain/state/StateObjectFactory'; +import {DiagnosticTestTopicTrackerModel} from 'pages/diagnostic-test-player-page/diagnostic-test-topic-tracker.model'; +import {DiagnosticTestPlayerEngineService} from './diagnostic-test-player-engine.service'; +import {TextInputRulesService} from 'interactions/TextInput/directives/text-input-rules.service'; +import {AnswerClassificationResult} from 'domain/classifier/answer-classification-result.model'; +import {OutcomeObjectFactory} from 'domain/exploration/OutcomeObjectFactory'; +import { + AnswerClassificationService, + InteractionRulesService, +} from './answer-classification.service'; +import {AlertsService} from 'services/alerts.service'; +import {ExpressionInterpolationService} from 'expressions/expression-interpolation.service'; describe('Diagnostic test engine service', () => { let diagnosticTestPlayerEngineService: DiagnosticTestPlayerEngineService; @@ -48,11 +53,12 @@ describe('Diagnostic test engine service', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] + imports: [HttpClientTestingModule], }); diagnosticTestPlayerEngineService = TestBed.inject( - DiagnosticTestPlayerEngineService); + DiagnosticTestPlayerEngineService + ); questionBackendApiService = TestBed.inject(QuestionBackendApiService); stateObject = TestBed.inject(StateObjectFactory); textInputService = TestBed.get(TextInputRulesService); @@ -60,8 +66,9 @@ describe('Diagnostic test engine service', () => { answerClassificationService = TestBed.inject(AnswerClassificationService); alertsService = TestBed.inject(AlertsService); questionObjectFactory = TestBed.inject(QuestionObjectFactory); - expressionInterpolationService = ( - TestBed.inject(ExpressionInterpolationService)); + expressionInterpolationService = TestBed.inject( + ExpressionInterpolationService + ); let questionBackendDict1: QuestionBackendDict = { id: '', @@ -71,29 +78,33 @@ describe('Diagnostic test engine service', () => { solicit_answer_details: false, content: { content_id: '2', - html: 'Question 2' + html: 'Question 2', }, interaction: { - answer_groups: [{ - outcome: { - dest: 'State 1', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_1', - html: '

Try Again.

' + answer_groups: [ + { + outcome: { + dest: 'State 1', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: '

Try Again.

', + }, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + labelled_as_correct: true, }, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - labelled_as_correct: true, + rule_specs: [ + { + rule_type: 'Equals', + inputs: {x: 0}, + }, + ], + training_data: [], + tagged_skill_misconception_id: '', }, - rule_specs: [{ - rule_type: 'Equals', - inputs: {x: 0} - }], - training_data: [], - tagged_skill_misconception_id: '', - }], + ], default_outcome: { dest: '', dest_if_really_stuck: null, @@ -103,38 +114,38 @@ describe('Diagnostic test engine service', () => { param_changes: [], feedback: { content_id: 'feedback_id', - html: '

Dummy Feedback

' - } + html: '

Dummy Feedback

', + }, }, id: 'TextInput', customization_args: { rows: { - value: 1 + value: 1, }, placeholder: { value: { unicode_str: '', - content_id: 'ca_placeholder_0' - } - } + content_id: 'ca_placeholder_0', + }, + }, }, confirmed_unclassified_answers: [], hints: [ { hint_content: { content_id: 'hint_1', - html: '

This is a hint.

' - } - } + html: '

This is a hint.

', + }, + }, ], solution: { correct_answer: 'Solution', explanation: { content_id: 'solution', - html: '

This is a solution.

' + html: '

This is a solution.

', }, - answer_is_exclusive: false - } + answer_is_exclusive: false, + }, }, linked_skill_id: null, card_is_checkpoint: true, @@ -144,111 +155,133 @@ describe('Diagnostic test engine service', () => { ca_placeholder_0: {}, feedback_id: {}, solution: {}, - hint_1: {} - } - } + hint_1: {}, + }, + }, }, question_state_data_schema_version: 2, language_code: '', version: 1, linked_skill_ids: [], inapplicable_skill_misconception_ids: [], - next_content_id_index: 5 + next_content_id_index: 5, }; - question1 = questionObjectFactory.createFromBackendDict( - questionBackendDict1); + question1 = + questionObjectFactory.createFromBackendDict(questionBackendDict1); question2 = new Question( 'question2', stateObject.createDefaultState('state', 'content_0', 'default_outcome_1'), - '', 1, ['skillID2'], [], 5 + '', + 1, + ['skillID2'], + [], + 5 ); question3 = new Question( 'question3', stateObject.createDefaultState('state', 'content_0', 'default_outcome_1'), - '', 1, ['skillID3'], [], 5 + '', + 1, + ['skillID3'], + [], + 5 ); question4 = new Question( 'question4', stateObject.createDefaultState('state', 'content_0', 'default_outcome_1'), - '', 1, ['skillID4'], [], 5 + '', + 1, + ['skillID4'], + [], + 5 ); }); - it( - 'should be able to load first card after initialization', fakeAsync(() => { - let initSuccessCb = jasmine.createSpy('success'); + it('should be able to load first card after initialization', fakeAsync(() => { + let initSuccessCb = jasmine.createSpy('success'); - // A linear graph with 3 nodes. - const topicIdToPrerequisiteTopicIds = { - topicId1: [], - topicId2: ['topicId1'], - topicId3: ['topicId2'] - }; + // A linear graph with 3 nodes. + const topicIdToPrerequisiteTopicIds = { + topicId1: [], + topicId2: ['topicId1'], + topicId3: ['topicId2'], + }; - const diagnosticTestCurrentTopicStatusModel = { - skillId1: new DiagnosticTestQuestionsModel(question1, question2), - skillId2: new DiagnosticTestQuestionsModel(question3, question4) - }; + const diagnosticTestCurrentTopicStatusModel = { + skillId1: new DiagnosticTestQuestionsModel(question1, question2), + skillId2: new DiagnosticTestQuestionsModel(question3, question4), + }; - const diagnosticTestTopicTrackerModel = ( - new DiagnosticTestTopicTrackerModel(topicIdToPrerequisiteTopicIds)); + const diagnosticTestTopicTrackerModel = new DiagnosticTestTopicTrackerModel( + topicIdToPrerequisiteTopicIds + ); - spyOn( - questionBackendApiService, - 'fetchDiagnosticTestQuestionsAsync').and.returnValue( - Promise.resolve(diagnosticTestCurrentTopicStatusModel)); + spyOn( + questionBackendApiService, + 'fetchDiagnosticTestQuestionsAsync' + ).and.returnValue(Promise.resolve(diagnosticTestCurrentTopicStatusModel)); - // Initially, the current question should be undefined since the engine - // is not initialized. - expect(diagnosticTestPlayerEngineService.getCurrentQuestion()).toEqual( - undefined); + // Initially, the current question should be undefined since the engine + // is not initialized. + expect(diagnosticTestPlayerEngineService.getCurrentQuestion()).toEqual( + undefined + ); - diagnosticTestPlayerEngineService.init( - diagnosticTestTopicTrackerModel, initSuccessCb); - tick(); + diagnosticTestPlayerEngineService.init( + diagnosticTestTopicTrackerModel, + initSuccessCb + ); + tick(); - expect(diagnosticTestPlayerEngineService.getCurrentSkillId()).toEqual( - 'skillId1'); - // Getting the main question from the current skill. - expect(diagnosticTestPlayerEngineService.getCurrentQuestion()).toEqual( - question1); - })); + expect(diagnosticTestPlayerEngineService.getCurrentSkillId()).toEqual( + 'skillId1' + ); + // Getting the main question from the current skill. + expect(diagnosticTestPlayerEngineService.getCurrentQuestion()).toEqual( + question1 + ); + })); it( 'should be able to show a warning if the initial question has failed ' + - 'to load', fakeAsync(() => { + 'to load', + fakeAsync(() => { let initSuccessCb = jasmine.createSpy('success'); // A linear graph with 3 nodes. const topicIdToPrerequisiteTopicIds = { topicId1: [], topicId2: ['topicId1'], - topicId3: ['topicId2'] + topicId3: ['topicId2'], }; - const diagnosticTestTopicTrackerModel = ( - new DiagnosticTestTopicTrackerModel(topicIdToPrerequisiteTopicIds)); + const diagnosticTestTopicTrackerModel = + new DiagnosticTestTopicTrackerModel(topicIdToPrerequisiteTopicIds); spyOn( questionBackendApiService, - 'fetchDiagnosticTestQuestionsAsync').and.returnValue( - Promise.reject() - ); + 'fetchDiagnosticTestQuestionsAsync' + ).and.returnValue(Promise.reject()); spyOn(alertsService, 'addWarning'); // Initially, the current question should be undefined since the engine // is not initialized. expect(diagnosticTestPlayerEngineService.getCurrentQuestion()).toEqual( - undefined); + undefined + ); diagnosticTestPlayerEngineService.init( - diagnosticTestTopicTrackerModel, initSuccessCb); + diagnosticTestTopicTrackerModel, + initSuccessCb + ); tick(); expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Failed to load the questions.'); - })); + 'Failed to load the questions.' + ); + }) + ); it('should throw warning when question html is empty', fakeAsync(() => { let initSuccessCb = jasmine.createSpy('success'); @@ -257,43 +290,47 @@ describe('Diagnostic test engine service', () => { const topicIdToPrerequisiteTopicIds = { topicId1: [], topicId2: ['topicId1'], - topicId3: ['topicId2'] + topicId3: ['topicId2'], }; const diagnosticTestCurrentTopicStatusModel = { skillId1: new DiagnosticTestQuestionsModel(question1, question2), }; - const diagnosticTestTopicTrackerModel = ( - new DiagnosticTestTopicTrackerModel(topicIdToPrerequisiteTopicIds)); + const diagnosticTestTopicTrackerModel = new DiagnosticTestTopicTrackerModel( + topicIdToPrerequisiteTopicIds + ); spyOn( questionBackendApiService, - 'fetchDiagnosticTestQuestionsAsync').and.returnValue( - Promise.resolve(diagnosticTestCurrentTopicStatusModel)); + 'fetchDiagnosticTestQuestionsAsync' + ).and.returnValue(Promise.resolve(diagnosticTestCurrentTopicStatusModel)); - spyOn( - alertsService, 'addWarning').and.callThrough(); + spyOn(alertsService, 'addWarning').and.callThrough(); - spyOn(expressionInterpolationService, 'processHtml').and.returnValue( - ''); + spyOn(expressionInterpolationService, 'processHtml').and.returnValue(''); // Initially, the current question should be undefined since the engine // is not initialized. expect(diagnosticTestPlayerEngineService.getCurrentQuestion()).toEqual( - undefined); + undefined + ); diagnosticTestPlayerEngineService.init( - diagnosticTestTopicTrackerModel, initSuccessCb); + diagnosticTestTopicTrackerModel, + initSuccessCb + ); tick(); expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Question name should not be empty.'); + 'Question name should not be empty.' + ); })); it( 'should be able to get the backup question if the main question of the ' + - 'current skill is answered incorrectly', fakeAsync(() => { + 'current skill is answered incorrectly', + fakeAsync(() => { let submitAnswerSuccessCb = jasmine.createSpy('success'); let initSuccessCb = jasmine.createSpy('success'); @@ -301,50 +338,62 @@ describe('Diagnostic test engine service', () => { const topicIdToPrerequisiteTopicIds = { topicId1: [], topicId2: ['topicId1'], - topicId3: ['topicId2'] + topicId3: ['topicId2'], }; const diagnosticTestCurrentTopicStatusModel = { skillId1: new DiagnosticTestQuestionsModel(question1, question2), - skillId2: new DiagnosticTestQuestionsModel(question3, question4) + skillId2: new DiagnosticTestQuestionsModel(question3, question4), }; - const diagnosticTestTopicTrackerModel = ( - new DiagnosticTestTopicTrackerModel(topicIdToPrerequisiteTopicIds)); + const diagnosticTestTopicTrackerModel = + new DiagnosticTestTopicTrackerModel(topicIdToPrerequisiteTopicIds); spyOn( questionBackendApiService, - 'fetchDiagnosticTestQuestionsAsync').and.returnValue( - Promise.resolve(diagnosticTestCurrentTopicStatusModel)); + 'fetchDiagnosticTestQuestionsAsync' + ).and.returnValue(Promise.resolve(diagnosticTestCurrentTopicStatusModel)); // Initially, the current question should be undefined since the engine // is not initialized. expect(diagnosticTestPlayerEngineService.getCurrentQuestion()).toEqual( - undefined); + undefined + ); diagnosticTestPlayerEngineService.init( - diagnosticTestTopicTrackerModel, initSuccessCb); + diagnosticTestTopicTrackerModel, + initSuccessCb + ); tick(); expect(diagnosticTestPlayerEngineService.getCurrentSkillId()).toEqual( - 'skillId1'); + 'skillId1' + ); // Getting the main question from the current skill. expect(diagnosticTestPlayerEngineService.getCurrentQuestion()).toEqual( - question1); + question1 + ); let answer = 'answer'; let answerClassificationResult = new AnswerClassificationResult( - outcomeObjectFactory - .createNew('default', '', '', []), 1, 0, 'default_outcome' + outcomeObjectFactory.createNew('default', '', '', []), + 1, + 0, + 'default_outcome' ); answerClassificationResult.outcome.labelledAsCorrect = false; - spyOn(answerClassificationService, 'getMatchingClassificationResult') - .and.returnValue(answerClassificationResult); + spyOn( + answerClassificationService, + 'getMatchingClassificationResult' + ).and.returnValue(answerClassificationResult); // Submitting incorrect answer. diagnosticTestPlayerEngineService.submitAnswer( - answer, textInputService, submitAnswerSuccessCb); + answer, + textInputService, + submitAnswerSuccessCb + ); tick(); expect(submitAnswerSuccessCb).toHaveBeenCalled(); @@ -352,180 +401,218 @@ describe('Diagnostic test engine service', () => { // An incorrect attempt does not change the skill, instead, the engine // presents the backup question of the same skill. expect(diagnosticTestPlayerEngineService.getCurrentSkillId()).toEqual( - 'skillId1'); + 'skillId1' + ); // Getting the backup question i.e., question2. expect(diagnosticTestPlayerEngineService.getCurrentQuestion()).toEqual( - question2); - })); + question2 + ); + }) + ); - it( - 'should be able to show a warning if the next question has not loaded', - fakeAsync(() => { - let submitAnswerSuccessCb = jasmine.createSpy('success'); - let initSuccessCb = jasmine.createSpy('success'); + it('should be able to show a warning if the next question has not loaded', fakeAsync(() => { + let submitAnswerSuccessCb = jasmine.createSpy('success'); + let initSuccessCb = jasmine.createSpy('success'); - // A linear graph with 3 nodes. - const topicIdToPrerequisiteTopicIds = { - topicId1: [], - topicId2: ['topicId1'], - topicId3: ['topicId2'] - }; + // A linear graph with 3 nodes. + const topicIdToPrerequisiteTopicIds = { + topicId1: [], + topicId2: ['topicId1'], + topicId3: ['topicId2'], + }; - const diagnosticTestCurrentTopicStatusModel = { - skillId1: new DiagnosticTestQuestionsModel(question1, question2) - }; + const diagnosticTestCurrentTopicStatusModel = { + skillId1: new DiagnosticTestQuestionsModel(question1, question2), + }; - const diagnosticTestTopicTrackerModel = ( - new DiagnosticTestTopicTrackerModel(topicIdToPrerequisiteTopicIds)); + const diagnosticTestTopicTrackerModel = new DiagnosticTestTopicTrackerModel( + topicIdToPrerequisiteTopicIds + ); - let fetchDiagnosticTestQuestionsAsyncSpy = spyOn( - questionBackendApiService, 'fetchDiagnosticTestQuestionsAsync'); + let fetchDiagnosticTestQuestionsAsyncSpy = spyOn( + questionBackendApiService, + 'fetchDiagnosticTestQuestionsAsync' + ); - fetchDiagnosticTestQuestionsAsyncSpy.and.returnValue( - Promise.resolve(diagnosticTestCurrentTopicStatusModel)); + fetchDiagnosticTestQuestionsAsyncSpy.and.returnValue( + Promise.resolve(diagnosticTestCurrentTopicStatusModel) + ); - // Initially, the current question should be undefined since the engine - // is not initialized. - expect(diagnosticTestPlayerEngineService.getCurrentQuestion()).toEqual( - undefined); + // Initially, the current question should be undefined since the engine + // is not initialized. + expect(diagnosticTestPlayerEngineService.getCurrentQuestion()).toEqual( + undefined + ); - diagnosticTestPlayerEngineService.init( - diagnosticTestTopicTrackerModel, initSuccessCb); - tick(); + diagnosticTestPlayerEngineService.init( + diagnosticTestTopicTrackerModel, + initSuccessCb + ); + tick(); - expect(diagnosticTestPlayerEngineService.getCurrentSkillId()).toEqual( - 'skillId1'); - // Getting the main question from the current skill. - expect(diagnosticTestPlayerEngineService.getCurrentQuestion()).toEqual( - question1); + expect(diagnosticTestPlayerEngineService.getCurrentSkillId()).toEqual( + 'skillId1' + ); + // Getting the main question from the current skill. + expect(diagnosticTestPlayerEngineService.getCurrentQuestion()).toEqual( + question1 + ); - let answer = 'answer'; - let answerClassificationResult = new AnswerClassificationResult( - outcomeObjectFactory - .createNew('default', '', '', []), 1, 0, 'default_outcome' - ); + let answer = 'answer'; + let answerClassificationResult = new AnswerClassificationResult( + outcomeObjectFactory.createNew('default', '', '', []), + 1, + 0, + 'default_outcome' + ); - // Setting it as correct answer so this will mark the skill as passed and - // move to the next pending skill ID. And as per the spy, the next - // fetching of skill ID will trigger a rejection handler. - answerClassificationResult.outcome.labelledAsCorrect = true; + // Setting it as correct answer so this will mark the skill as passed and + // move to the next pending skill ID. And as per the spy, the next + // fetching of skill ID will trigger a rejection handler. + answerClassificationResult.outcome.labelledAsCorrect = true; - spyOn(answerClassificationService, 'getMatchingClassificationResult') - .and.returnValue(answerClassificationResult); - spyOn(alertsService, 'addWarning'); - spyOn(diagnosticTestPlayerEngineService, 'isDiagnosticTestFinished') - .and.returnValue(false); + spyOn( + answerClassificationService, + 'getMatchingClassificationResult' + ).and.returnValue(answerClassificationResult); + spyOn(alertsService, 'addWarning'); + spyOn( + diagnosticTestPlayerEngineService, + 'isDiagnosticTestFinished' + ).and.returnValue(false); - fetchDiagnosticTestQuestionsAsyncSpy.and.returnValue(Promise.reject()); + fetchDiagnosticTestQuestionsAsyncSpy.and.returnValue(Promise.reject()); - // Submitting incorrect answer. - diagnosticTestPlayerEngineService.submitAnswer( - answer, textInputService, submitAnswerSuccessCb); - tick(); + // Submitting incorrect answer. + diagnosticTestPlayerEngineService.submitAnswer( + answer, + textInputService, + submitAnswerSuccessCb + ); + tick(); - expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Failed to load the questions.'); - })); + expect(alertsService.addWarning).toHaveBeenCalledWith( + 'Failed to load the questions.' + ); + })); - it( - 'should be able to get next question after skipping the current question', - fakeAsync(() => { - let initSuccessCb = jasmine.createSpy('success'); + it('should be able to get next question after skipping the current question', fakeAsync(() => { + let initSuccessCb = jasmine.createSpy('success'); - // A linear graph with 3 nodes. - const topicIdToPrerequisiteTopicIds = { - topicId1: [], - topicId2: ['topicId1'], - topicId3: ['topicId2'] - }; + // A linear graph with 3 nodes. + const topicIdToPrerequisiteTopicIds = { + topicId1: [], + topicId2: ['topicId1'], + topicId3: ['topicId2'], + }; - const diagnosticTestCurrentTopicStatusModel = { - skillId1: new DiagnosticTestQuestionsModel(question1, question2) - }; + const diagnosticTestCurrentTopicStatusModel = { + skillId1: new DiagnosticTestQuestionsModel(question1, question2), + }; - const diagnosticTestTopicTrackerModel = ( - new DiagnosticTestTopicTrackerModel(topicIdToPrerequisiteTopicIds)); + const diagnosticTestTopicTrackerModel = new DiagnosticTestTopicTrackerModel( + topicIdToPrerequisiteTopicIds + ); - let fetchDiagnosticTestQuestionsAsyncSpy = spyOn( - questionBackendApiService, 'fetchDiagnosticTestQuestionsAsync'); + let fetchDiagnosticTestQuestionsAsyncSpy = spyOn( + questionBackendApiService, + 'fetchDiagnosticTestQuestionsAsync' + ); - fetchDiagnosticTestQuestionsAsyncSpy.and.returnValue( - Promise.resolve(diagnosticTestCurrentTopicStatusModel)); + fetchDiagnosticTestQuestionsAsyncSpy.and.returnValue( + Promise.resolve(diagnosticTestCurrentTopicStatusModel) + ); - // Initially, the current question should be undefined since the engine - // is not initialized. - expect(diagnosticTestPlayerEngineService.getCurrentQuestion()).toEqual( - undefined); + // Initially, the current question should be undefined since the engine + // is not initialized. + expect(diagnosticTestPlayerEngineService.getCurrentQuestion()).toEqual( + undefined + ); - diagnosticTestPlayerEngineService.init( - diagnosticTestTopicTrackerModel, initSuccessCb); - tick(); + diagnosticTestPlayerEngineService.init( + diagnosticTestTopicTrackerModel, + initSuccessCb + ); + tick(); - spyOn(diagnosticTestPlayerEngineService, 'getNextQuestionAsync') - .and.returnValue(Promise.resolve(question3)); + spyOn( + diagnosticTestPlayerEngineService, + 'getNextQuestionAsync' + ).and.returnValue(Promise.resolve(question3)); - let successCallback = () => {}; + let successCallback = () => {}; - diagnosticTestPlayerEngineService.skipCurrentQuestion(successCallback); - tick(); + diagnosticTestPlayerEngineService.skipCurrentQuestion(successCallback); + tick(); - expect(diagnosticTestPlayerEngineService.getNextQuestionAsync) - .toHaveBeenCalled(); - })); + expect( + diagnosticTestPlayerEngineService.getNextQuestionAsync + ).toHaveBeenCalled(); + })); - it( - 'should be able to finish test if no more questions is left for testing', - fakeAsync(() => { - let initSuccessCb = jasmine.createSpy('success'); + it('should be able to finish test if no more questions is left for testing', fakeAsync(() => { + let initSuccessCb = jasmine.createSpy('success'); - // A linear graph with 3 nodes. - const topicIdToPrerequisiteTopicIds = { - topicId1: [], - topicId2: ['topicId1'], - topicId3: ['topicId2'] - }; + // A linear graph with 3 nodes. + const topicIdToPrerequisiteTopicIds = { + topicId1: [], + topicId2: ['topicId1'], + topicId3: ['topicId2'], + }; - const diagnosticTestCurrentTopicStatusModel = { - skillId1: new DiagnosticTestQuestionsModel(question1, question2) - }; + const diagnosticTestCurrentTopicStatusModel = { + skillId1: new DiagnosticTestQuestionsModel(question1, question2), + }; - const diagnosticTestTopicTrackerModel = ( - new DiagnosticTestTopicTrackerModel(topicIdToPrerequisiteTopicIds)); + const diagnosticTestTopicTrackerModel = new DiagnosticTestTopicTrackerModel( + topicIdToPrerequisiteTopicIds + ); - let fetchDiagnosticTestQuestionsAsyncSpy = spyOn( - questionBackendApiService, 'fetchDiagnosticTestQuestionsAsync'); + let fetchDiagnosticTestQuestionsAsyncSpy = spyOn( + questionBackendApiService, + 'fetchDiagnosticTestQuestionsAsync' + ); - fetchDiagnosticTestQuestionsAsyncSpy.and.returnValue( - Promise.resolve(diagnosticTestCurrentTopicStatusModel)); + fetchDiagnosticTestQuestionsAsyncSpy.and.returnValue( + Promise.resolve(diagnosticTestCurrentTopicStatusModel) + ); - // Initially, the current question should be undefined since the engine - // is not initialized. - expect(diagnosticTestPlayerEngineService.getCurrentQuestion()).toEqual( - undefined); + // Initially, the current question should be undefined since the engine + // is not initialized. + expect(diagnosticTestPlayerEngineService.getCurrentQuestion()).toEqual( + undefined + ); - diagnosticTestPlayerEngineService.init( - diagnosticTestTopicTrackerModel, initSuccessCb); - tick(); + diagnosticTestPlayerEngineService.init( + diagnosticTestTopicTrackerModel, + initSuccessCb + ); + tick(); - spyOn(diagnosticTestPlayerEngineService, 'getNextQuestionAsync') - .and.returnValue(Promise.reject()); - spyOn(diagnosticTestPlayerEngineService, 'getRecommendedTopicIds'); + spyOn( + diagnosticTestPlayerEngineService, + 'getNextQuestionAsync' + ).and.returnValue(Promise.reject()); + spyOn(diagnosticTestPlayerEngineService, 'getRecommendedTopicIds'); - let successCallback = () => {}; + let successCallback = () => {}; - diagnosticTestPlayerEngineService.skipCurrentQuestion(successCallback); - tick(); + diagnosticTestPlayerEngineService.skipCurrentQuestion(successCallback); + tick(); - expect(diagnosticTestPlayerEngineService.getNextQuestionAsync) - .toHaveBeenCalled(); - expect(diagnosticTestPlayerEngineService.getRecommendedTopicIds) - .toHaveBeenCalled(); - })); + expect( + diagnosticTestPlayerEngineService.getNextQuestionAsync + ).toHaveBeenCalled(); + expect( + diagnosticTestPlayerEngineService.getRecommendedTopicIds + ).toHaveBeenCalled(); + })); it( 'should be able to get the main question from next skill if the question ' + - 'from current skill is answered correctly', fakeAsync(() => { + 'from current skill is answered correctly', + fakeAsync(() => { let submitAnswerSuccessCb = jasmine.createSpy('success'); let initSuccessCb = jasmine.createSpy('success'); @@ -533,44 +620,54 @@ describe('Diagnostic test engine service', () => { const topicIdToPrerequisiteTopicIds = { topicId1: [], topicId2: ['topicId1'], - topicId3: ['topicId2'] + topicId3: ['topicId2'], }; const diagnosticTestCurrentTopicStatusModel = { skillId1: new DiagnosticTestQuestionsModel(question1, question2), - skillId2: new DiagnosticTestQuestionsModel(question3, question4) + skillId2: new DiagnosticTestQuestionsModel(question3, question4), }; - const diagnosticTestTopicTrackerModel = ( - new DiagnosticTestTopicTrackerModel(topicIdToPrerequisiteTopicIds)); + const diagnosticTestTopicTrackerModel = + new DiagnosticTestTopicTrackerModel(topicIdToPrerequisiteTopicIds); spyOn( questionBackendApiService, - 'fetchDiagnosticTestQuestionsAsync').and.returnValue( - Promise.resolve(diagnosticTestCurrentTopicStatusModel)); + 'fetchDiagnosticTestQuestionsAsync' + ).and.returnValue(Promise.resolve(diagnosticTestCurrentTopicStatusModel)); // Initially, the current question should be undefined since the engine // is not initialized. expect(diagnosticTestPlayerEngineService.getCurrentQuestion()).toEqual( - undefined); + undefined + ); diagnosticTestPlayerEngineService.init( - diagnosticTestTopicTrackerModel, initSuccessCb); + diagnosticTestTopicTrackerModel, + initSuccessCb + ); tick(); let answer = 'answer'; let answerClassificationResult = new AnswerClassificationResult( - outcomeObjectFactory - .createNew('default', '', '', []), 1, 0, 'default_outcome' + outcomeObjectFactory.createNew('default', '', '', []), + 1, + 0, + 'default_outcome' ); answerClassificationResult.outcome.labelledAsCorrect = true; - spyOn(answerClassificationService, 'getMatchingClassificationResult') - .and.returnValue(answerClassificationResult); + spyOn( + answerClassificationService, + 'getMatchingClassificationResult' + ).and.returnValue(answerClassificationResult); // Submitting the correct answer. diagnosticTestPlayerEngineService.submitAnswer( - answer, textInputService, submitAnswerSuccessCb); + answer, + textInputService, + submitAnswerSuccessCb + ); tick(); expect(submitAnswerSuccessCb).toHaveBeenCalled(); @@ -578,70 +675,85 @@ describe('Diagnostic test engine service', () => { // A correct attempt presents another diagnostic test skill from // the same topic. expect(diagnosticTestPlayerEngineService.getCurrentSkillId()).toEqual( - 'skillId2'); + 'skillId2' + ); // Getting the main question from the next skill i.e., question3. expect(diagnosticTestPlayerEngineService.getCurrentQuestion()).toEqual( - question3); - })); + question3 + ); + }) + ); - it( - 'should be able to finish test if all the eligible topics are tested', - fakeAsync(() => { - let submitAnswerSuccessCb = jasmine.createSpy('success'); - let initSuccessCb = jasmine.createSpy('success'); + it('should be able to finish test if all the eligible topics are tested', fakeAsync(() => { + let submitAnswerSuccessCb = jasmine.createSpy('success'); + let initSuccessCb = jasmine.createSpy('success'); - // A linear graph with a single node. - const topicIdToPrerequisiteTopicIds = { - topicId1: [] - }; + // A linear graph with a single node. + const topicIdToPrerequisiteTopicIds = { + topicId1: [], + }; - const diagnosticTestCurrentTopicStatusModel = { - skillId1: new DiagnosticTestQuestionsModel(question1, question2), - }; + const diagnosticTestCurrentTopicStatusModel = { + skillId1: new DiagnosticTestQuestionsModel(question1, question2), + }; - const diagnosticTestTopicTrackerModel = ( - new DiagnosticTestTopicTrackerModel(topicIdToPrerequisiteTopicIds)); + const diagnosticTestTopicTrackerModel = new DiagnosticTestTopicTrackerModel( + topicIdToPrerequisiteTopicIds + ); - spyOn( - questionBackendApiService, - 'fetchDiagnosticTestQuestionsAsync').and.returnValue( - Promise.resolve(diagnosticTestCurrentTopicStatusModel)); + spyOn( + questionBackendApiService, + 'fetchDiagnosticTestQuestionsAsync' + ).and.returnValue(Promise.resolve(diagnosticTestCurrentTopicStatusModel)); - diagnosticTestPlayerEngineService.init( - diagnosticTestTopicTrackerModel, initSuccessCb); - tick(); + diagnosticTestPlayerEngineService.init( + diagnosticTestTopicTrackerModel, + initSuccessCb + ); + tick(); - let answer = 'answer'; - let answerClassificationResult = new AnswerClassificationResult( - outcomeObjectFactory - .createNew('default', '', '', []), 1, 0, 'default_outcome' - ); - answerClassificationResult.outcome.labelledAsCorrect = true; + let answer = 'answer'; + let answerClassificationResult = new AnswerClassificationResult( + outcomeObjectFactory.createNew('default', '', '', []), + 1, + 0, + 'default_outcome' + ); + answerClassificationResult.outcome.labelledAsCorrect = true; - spyOn(answerClassificationService, 'getMatchingClassificationResult') - .and.returnValue(answerClassificationResult); + spyOn( + answerClassificationService, + 'getMatchingClassificationResult' + ).and.returnValue(answerClassificationResult); - expect(diagnosticTestPlayerEngineService.isDiagnosticTestFinished()) - .toBeFalse(); + expect( + diagnosticTestPlayerEngineService.isDiagnosticTestFinished() + ).toBeFalse(); - // Submitting the correct answer. - diagnosticTestPlayerEngineService.submitAnswer( - answer, textInputService, submitAnswerSuccessCb); - tick(); + // Submitting the correct answer. + diagnosticTestPlayerEngineService.submitAnswer( + answer, + textInputService, + submitAnswerSuccessCb + ); + tick(); - expect(diagnosticTestPlayerEngineService.isDiagnosticTestFinished()) - .toBeTrue(); - })); + expect( + diagnosticTestPlayerEngineService.isDiagnosticTestFinished() + ).toBeTrue(); + })); it( 'should be able to finish test if the number of attempted questions has ' + - 'reached the upper limit', fakeAsync(() => { + 'reached the upper limit', + fakeAsync(() => { // For testing purposes only, the maximum number of questions in the // diagnostic test is set to 2. spyOnProperty( DiagnosticTestPlayerEngineService, - 'MAX_ALLOWED_QUESTIONS_IN_THE_DIAGNOSTIC_TEST', 'get' + 'MAX_ALLOWED_QUESTIONS_IN_THE_DIAGNOSTIC_TEST', + 'get' ).and.returnValue(2); let submitAnswerSuccessCb = jasmine.createSpy('success'); @@ -651,83 +763,104 @@ describe('Diagnostic test engine service', () => { const topicIdToPrerequisiteTopicIds = { topicId1: [], topicId2: ['topicId1'], - topicId3: ['topicId2'] + topicId3: ['topicId2'], }; const diagnosticTestCurrentTopicStatusModel = { skillId1: new DiagnosticTestQuestionsModel(question1, question2), - skillId2: new DiagnosticTestQuestionsModel(question3, question4) + skillId2: new DiagnosticTestQuestionsModel(question3, question4), }; - const diagnosticTestTopicTrackerModel = ( - new DiagnosticTestTopicTrackerModel(topicIdToPrerequisiteTopicIds)); + const diagnosticTestTopicTrackerModel = + new DiagnosticTestTopicTrackerModel(topicIdToPrerequisiteTopicIds); spyOn( questionBackendApiService, - 'fetchDiagnosticTestQuestionsAsync').and.returnValue( - Promise.resolve(diagnosticTestCurrentTopicStatusModel)); + 'fetchDiagnosticTestQuestionsAsync' + ).and.returnValue(Promise.resolve(diagnosticTestCurrentTopicStatusModel)); // Initially, the current question should be undefined since the engine // is not initialized. expect(diagnosticTestPlayerEngineService.getCurrentQuestion()).toEqual( - undefined); + undefined + ); diagnosticTestPlayerEngineService.init( - diagnosticTestTopicTrackerModel, initSuccessCb); + diagnosticTestTopicTrackerModel, + initSuccessCb + ); tick(); let answer = 'answer'; let answerClassificationResult = new AnswerClassificationResult( - outcomeObjectFactory - .createNew('default', '', '', []), 1, 0, 'default_outcome' + outcomeObjectFactory.createNew('default', '', '', []), + 1, + 0, + 'default_outcome' ); answerClassificationResult.outcome.labelledAsCorrect = false; - spyOn(answerClassificationService, 'getMatchingClassificationResult') - .and.returnValue(answerClassificationResult); + spyOn( + answerClassificationService, + 'getMatchingClassificationResult' + ).and.returnValue(answerClassificationResult); // Encountering the main question from skill 1. - expect(diagnosticTestPlayerEngineService.getCurrentQuestion()) - .toEqual(question1); + expect(diagnosticTestPlayerEngineService.getCurrentQuestion()).toEqual( + question1 + ); // Submitting incorrect answer. diagnosticTestPlayerEngineService.submitAnswer( - answer, textInputService, submitAnswerSuccessCb); + answer, + textInputService, + submitAnswerSuccessCb + ); tick(); // Encountering the backup question from skill 1, since the earlier // attempt was incorrect. - expect(diagnosticTestPlayerEngineService.getCurrentQuestion()) - .toEqual(question2); + expect(diagnosticTestPlayerEngineService.getCurrentQuestion()).toEqual( + question2 + ); - expect(diagnosticTestPlayerEngineService.isDiagnosticTestFinished()) - .toBeFalse(); + expect( + diagnosticTestPlayerEngineService.isDiagnosticTestFinished() + ).toBeFalse(); expect(diagnosticTestPlayerEngineService.getCurrentTopicId()).toEqual( - 'topicId2'); + 'topicId2' + ); // Submitting incorrect answer. diagnosticTestPlayerEngineService.submitAnswer( - answer, textInputService, submitAnswerSuccessCb); + answer, + textInputService, + submitAnswerSuccessCb + ); tick(); // Submitting two incorrect answers for the questions associated with a // topic marks the current topic as failed. - expect(diagnosticTestPlayerEngineService.getFailedTopicIds()).toEqual( - ['topicId2']); + expect(diagnosticTestPlayerEngineService.getFailedTopicIds()).toEqual([ + 'topicId2', + ]); expect( diagnosticTestPlayerEngineService.getTotalNumberOfAttemptedQuestions() ).toEqual(2); // Since the learner has attempted the maximum number of questions (2) in // the test so the test should be terminated. - expect(diagnosticTestPlayerEngineService.isDiagnosticTestFinished()) - .toBeTrue(); - })); + expect( + diagnosticTestPlayerEngineService.isDiagnosticTestFinished() + ).toBeTrue(); + }) + ); it( 'should return the language code correctly when an answer is ' + - 'submitted and a new card is recorded', fakeAsync(() => { + 'submitted and a new card is recorded', + fakeAsync(() => { let submitAnswerSuccessCb = jasmine.createSpy('success'); let initSuccessCb = jasmine.createSpy('success'); @@ -735,29 +868,32 @@ describe('Diagnostic test engine service', () => { const topicIdToPrerequisiteTopicIds = { topicId1: [], topicId2: ['topicId1'], - topicId3: ['topicId2'] + topicId3: ['topicId2'], }; const diagnosticTestCurrentTopicStatusModel = { skillId1: new DiagnosticTestQuestionsModel(question1, question2), - skillId2: new DiagnosticTestQuestionsModel(question3, question4) + skillId2: new DiagnosticTestQuestionsModel(question3, question4), }; - const diagnosticTestTopicTrackerModel = ( - new DiagnosticTestTopicTrackerModel(topicIdToPrerequisiteTopicIds)); + const diagnosticTestTopicTrackerModel = + new DiagnosticTestTopicTrackerModel(topicIdToPrerequisiteTopicIds); spyOn( questionBackendApiService, - 'fetchDiagnosticTestQuestionsAsync').and.returnValue( - Promise.resolve(diagnosticTestCurrentTopicStatusModel)); + 'fetchDiagnosticTestQuestionsAsync' + ).and.returnValue(Promise.resolve(diagnosticTestCurrentTopicStatusModel)); // Initially, the current question should be undefined since the engine // is not initialized. expect(diagnosticTestPlayerEngineService.getCurrentQuestion()).toEqual( - undefined); + undefined + ); diagnosticTestPlayerEngineService.init( - diagnosticTestTopicTrackerModel, initSuccessCb); + diagnosticTestTopicTrackerModel, + initSuccessCb + ); tick(); let languageCode = diagnosticTestPlayerEngineService.getLanguageCode(); @@ -765,24 +901,33 @@ describe('Diagnostic test engine service', () => { let answer = 'answer'; let answerClassificationResult = new AnswerClassificationResult( - outcomeObjectFactory - .createNew('default', '', '', []), 1, 0, 'default_outcome' + outcomeObjectFactory.createNew('default', '', '', []), + 1, + 0, + 'default_outcome' ); answerClassificationResult.outcome.labelledAsCorrect = false; - spyOn(answerClassificationService, 'getMatchingClassificationResult') - .and.returnValue(answerClassificationResult); + spyOn( + answerClassificationService, + 'getMatchingClassificationResult' + ).and.returnValue(answerClassificationResult); // Encountering the main question from skill 1. - expect(diagnosticTestPlayerEngineService.getCurrentQuestion()) - .toEqual(question1); + expect(diagnosticTestPlayerEngineService.getCurrentQuestion()).toEqual( + question1 + ); // Submitting incorrect answer. diagnosticTestPlayerEngineService.submitAnswer( - answer, textInputService, submitAnswerSuccessCb); + answer, + textInputService, + submitAnswerSuccessCb + ); tick(); diagnosticTestPlayerEngineService.recordNewCardAdded(); - })); + }) + ); it('should progress through topics in the diagnostic test', fakeAsync(() => { let submitAnswerSuccessCb = jasmine.createSpy('success'); @@ -792,51 +937,63 @@ describe('Diagnostic test engine service', () => { const topicIdToPrerequisiteTopicIds = { topicId1: [], topicId2: ['topicId1'], - topicId3: ['topicId2'] + topicId3: ['topicId2'], }; const diagnosticTestCurrentTopicStatusModel = { skillId1: new DiagnosticTestQuestionsModel(question1, question2), }; - const diagnosticTestTopicTrackerModel = ( - new DiagnosticTestTopicTrackerModel(topicIdToPrerequisiteTopicIds)); + const diagnosticTestTopicTrackerModel = new DiagnosticTestTopicTrackerModel( + topicIdToPrerequisiteTopicIds + ); spyOn( questionBackendApiService, - 'fetchDiagnosticTestQuestionsAsync').and.returnValue( - Promise.resolve(diagnosticTestCurrentTopicStatusModel)); + 'fetchDiagnosticTestQuestionsAsync' + ).and.returnValue(Promise.resolve(diagnosticTestCurrentTopicStatusModel)); diagnosticTestPlayerEngineService.init( - diagnosticTestTopicTrackerModel, initSuccessCb); + diagnosticTestTopicTrackerModel, + initSuccessCb + ); tick(); let answer = 'answer'; let answerClassificationResult = new AnswerClassificationResult( - outcomeObjectFactory - .createNew('default', '', '', []), 1, 0, 'default_outcome' + outcomeObjectFactory.createNew('default', '', '', []), + 1, + 0, + 'default_outcome' ); answerClassificationResult.outcome.labelledAsCorrect = true; - spyOn(answerClassificationService, 'getMatchingClassificationResult') - .and.returnValue(answerClassificationResult); + spyOn( + answerClassificationService, + 'getMatchingClassificationResult' + ).and.returnValue(answerClassificationResult); // Assuming L = min(length of ancestors, length of successors). Among all // the eligible topic IDs, topic2 and topic3 have the maximum value for L. // Since topic2 appears before topic3, thus topic2 should be selected as // the current eligible topic ID. expect(diagnosticTestPlayerEngineService.getCurrentTopicId()).toEqual( - 'topicId2'); + 'topicId2' + ); // Submitting the correct answer for the only question marks the topic as // passed and removes its ancestor (topicId1) from the eligible topic IDs // list. diagnosticTestPlayerEngineService.submitAnswer( - answer, textInputService, submitAnswerSuccessCb); + answer, + textInputService, + submitAnswerSuccessCb + ); tick(); expect(diagnosticTestPlayerEngineService.getCurrentTopicId()).toEqual( - 'topicId3'); + 'topicId3' + ); })); it('should be able to sort the dependency sample graph 1', fakeAsync(() => { @@ -845,33 +1002,37 @@ describe('Diagnostic test engine service', () => { const topicIdToPrerequisiteTopicIds = { topicId1: [], topicId2: ['topicId1'], - topicId3: ['topicId2'] + topicId3: ['topicId2'], }; const diagnosticTestCurrentTopicStatusModel = { skillId1: new DiagnosticTestQuestionsModel(question1, question2), - skillId2: new DiagnosticTestQuestionsModel(question3, question4) + skillId2: new DiagnosticTestQuestionsModel(question3, question4), }; - const diagnosticTestTopicTrackerModel = ( - new DiagnosticTestTopicTrackerModel(topicIdToPrerequisiteTopicIds)); + const diagnosticTestTopicTrackerModel = new DiagnosticTestTopicTrackerModel( + topicIdToPrerequisiteTopicIds + ); spyOn( questionBackendApiService, - 'fetchDiagnosticTestQuestionsAsync').and.returnValue( - Promise.resolve(diagnosticTestCurrentTopicStatusModel)); + 'fetchDiagnosticTestQuestionsAsync' + ).and.returnValue(Promise.resolve(diagnosticTestCurrentTopicStatusModel)); // Initially, the current question should be undefined since the engine // is not initialized. expect(diagnosticTestPlayerEngineService.getCurrentQuestion()).toEqual( - undefined); + undefined + ); diagnosticTestPlayerEngineService.init( - diagnosticTestTopicTrackerModel, initSuccessCb); + diagnosticTestTopicTrackerModel, + initSuccessCb + ); tick(); - let sortedTopicIds: string[] = ( - diagnosticTestPlayerEngineService.getTopologicallySortedTopicIds()); + let sortedTopicIds: string[] = + diagnosticTestPlayerEngineService.getTopologicallySortedTopicIds(); expect(sortedTopicIds).toEqual(['topicId1', 'topicId2', 'topicId3']); })); @@ -883,42 +1044,46 @@ describe('Diagnostic test engine service', () => { topicId4: ['topicId1'], topicId3: ['topicId2'], topicId2: ['topicId1'], - topicId1: [] + topicId1: [], }; const diagnosticTestCurrentTopicStatusModel = { skillId1: new DiagnosticTestQuestionsModel(question1, question2), - skillId2: new DiagnosticTestQuestionsModel(question3, question4) + skillId2: new DiagnosticTestQuestionsModel(question3, question4), }; - const diagnosticTestTopicTrackerModel = ( - new DiagnosticTestTopicTrackerModel(topicIdToPrerequisiteTopicIds)); + const diagnosticTestTopicTrackerModel = new DiagnosticTestTopicTrackerModel( + topicIdToPrerequisiteTopicIds + ); spyOn( questionBackendApiService, - 'fetchDiagnosticTestQuestionsAsync').and.returnValue( - Promise.resolve(diagnosticTestCurrentTopicStatusModel)); + 'fetchDiagnosticTestQuestionsAsync' + ).and.returnValue(Promise.resolve(diagnosticTestCurrentTopicStatusModel)); // Initially, the current question should be undefined since the engine // is not initialized. expect(diagnosticTestPlayerEngineService.getCurrentQuestion()).toEqual( - undefined); + undefined + ); diagnosticTestPlayerEngineService.init( - diagnosticTestTopicTrackerModel, initSuccessCb); + diagnosticTestTopicTrackerModel, + initSuccessCb + ); tick(); // The list of topic IDs is converted into CSV because the topological sort // of the topics prerequisite contains multiple solutions. And comparing a // string in comparison to a list is easy, hence the list is converted into // a string. - let sortedTopicIds: string = ( - diagnosticTestPlayerEngineService.getTopologicallySortedTopicIds() - ).join(','); + let sortedTopicIds: string = diagnosticTestPlayerEngineService + .getTopologicallySortedTopicIds() + .join(','); let possibleSortedAnswers = [ ['topicId1', 'topicId2', 'topicId3', 'topicId4', 'topicId5'].join(','), - ['topicId1', 'topicId4', 'topicId5', 'topicId2', 'topicId3'].join(',') + ['topicId1', 'topicId4', 'topicId5', 'topicId2', 'topicId3'].join(','), ]; expect(possibleSortedAnswers).toContain(sortedTopicIds); @@ -931,93 +1096,124 @@ describe('Diagnostic test engine service', () => { const topicIdToPrerequisiteTopicIds = { topicId1: [], topicId2: ['topicId1'], - topicId3: ['topicId2'] + topicId3: ['topicId2'], }; const diagnosticTestCurrentTopicStatusModel = { skillId1: new DiagnosticTestQuestionsModel(question1, question2), - skillId2: new DiagnosticTestQuestionsModel(question3, question4) + skillId2: new DiagnosticTestQuestionsModel(question3, question4), }; - const diagnosticTestTopicTrackerModel = ( - new DiagnosticTestTopicTrackerModel(topicIdToPrerequisiteTopicIds)); + const diagnosticTestTopicTrackerModel = new DiagnosticTestTopicTrackerModel( + topicIdToPrerequisiteTopicIds + ); spyOn( questionBackendApiService, - 'fetchDiagnosticTestQuestionsAsync').and.returnValue( - Promise.resolve(diagnosticTestCurrentTopicStatusModel)); + 'fetchDiagnosticTestQuestionsAsync' + ).and.returnValue(Promise.resolve(diagnosticTestCurrentTopicStatusModel)); diagnosticTestPlayerEngineService.init( - diagnosticTestTopicTrackerModel, initSuccessCb); + diagnosticTestTopicTrackerModel, + initSuccessCb + ); tick(); - expect(diagnosticTestPlayerEngineService.getRootTopicIds()).toEqual( - ['topicId1']); + expect(diagnosticTestPlayerEngineService.getRootTopicIds()).toEqual([ + 'topicId1', + ]); })); it('should be able to recommend two root topic IDs', fakeAsync(() => { - spyOn(diagnosticTestPlayerEngineService, 'getRootTopicIds') - .and.returnValue(['topicId1', 'topicId2', 'topicId3']); + spyOn(diagnosticTestPlayerEngineService, 'getRootTopicIds').and.returnValue( + ['topicId1', 'topicId2', 'topicId3'] + ); - spyOn(diagnosticTestPlayerEngineService, 'getFailedTopicIds') - .and.returnValue(['topicId1', 'topicId2']); + spyOn( + diagnosticTestPlayerEngineService, + 'getFailedTopicIds' + ).and.returnValue(['topicId1', 'topicId2']); - let recommendedTopicIds = ( - diagnosticTestPlayerEngineService.getRecommendedTopicIds()); + let recommendedTopicIds = + diagnosticTestPlayerEngineService.getRecommendedTopicIds(); expect(recommendedTopicIds).toEqual(['topicId1', 'topicId2']); })); it('should be able to recomemnd one root topic ID', fakeAsync(() => { - spyOn(diagnosticTestPlayerEngineService, 'getRootTopicIds') - .and.returnValue(['topicId1', 'topicId2', 'topicId3']); + spyOn(diagnosticTestPlayerEngineService, 'getRootTopicIds').and.returnValue( + ['topicId1', 'topicId2', 'topicId3'] + ); - spyOn(diagnosticTestPlayerEngineService, 'getFailedTopicIds') - .and.returnValue(['topicId2']); + spyOn( + diagnosticTestPlayerEngineService, + 'getFailedTopicIds' + ).and.returnValue(['topicId2']); - let recommendedTopicIds = ( - diagnosticTestPlayerEngineService.getRecommendedTopicIds()); + let recommendedTopicIds = + diagnosticTestPlayerEngineService.getRecommendedTopicIds(); expect(recommendedTopicIds).toEqual(['topicId2']); })); - it( - 'should be able to recommend one topic ID from the sorted list', - fakeAsync(() => { - spyOn(diagnosticTestPlayerEngineService, 'getRootTopicIds') - .and.returnValue(['topicId1']); - - spyOn(diagnosticTestPlayerEngineService, 'getFailedTopicIds') - .and.returnValue(['topicId2', 'topicId3']); + it('should be able to recommend one topic ID from the sorted list', fakeAsync(() => { + spyOn(diagnosticTestPlayerEngineService, 'getRootTopicIds').and.returnValue( + ['topicId1'] + ); - spyOn(diagnosticTestPlayerEngineService, 'getTopologicallySortedTopicIds') - .and.returnValue( - ['topicId1', 'topicId2', 'topicId3', 'topicId4', 'topicId5']); + spyOn( + diagnosticTestPlayerEngineService, + 'getFailedTopicIds' + ).and.returnValue(['topicId2', 'topicId3']); - let recommendedTopicIds = ( - diagnosticTestPlayerEngineService.getRecommendedTopicIds()); + spyOn( + diagnosticTestPlayerEngineService, + 'getTopologicallySortedTopicIds' + ).and.returnValue([ + 'topicId1', + 'topicId2', + 'topicId3', + 'topicId4', + 'topicId5', + ]); + + let recommendedTopicIds = + diagnosticTestPlayerEngineService.getRecommendedTopicIds(); - expect(recommendedTopicIds).toEqual(['topicId2']); - })); + expect(recommendedTopicIds).toEqual(['topicId2']); + })); it( 'should be able to recommend zero topic IDs if the learner is not ' + - 'failed in any topic', fakeAsync(() => { - spyOn(diagnosticTestPlayerEngineService, 'getRootTopicIds') - .and.returnValue(['topicId1']); - - spyOn(diagnosticTestPlayerEngineService, 'getFailedTopicIds') - .and.returnValue([]); + 'failed in any topic', + fakeAsync(() => { + spyOn( + diagnosticTestPlayerEngineService, + 'getRootTopicIds' + ).and.returnValue(['topicId1']); - spyOn(diagnosticTestPlayerEngineService, 'getTopologicallySortedTopicIds') - .and.returnValue( - ['topicId1', 'topicId2', 'topicId3', 'topicId4', 'topicId5']); + spyOn( + diagnosticTestPlayerEngineService, + 'getFailedTopicIds' + ).and.returnValue([]); - let recommendedTopicIds = ( - diagnosticTestPlayerEngineService.getRecommendedTopicIds()); + spyOn( + diagnosticTestPlayerEngineService, + 'getTopologicallySortedTopicIds' + ).and.returnValue([ + 'topicId1', + 'topicId2', + 'topicId3', + 'topicId4', + 'topicId5', + ]); + + let recommendedTopicIds = + diagnosticTestPlayerEngineService.getRecommendedTopicIds(); expect(recommendedTopicIds).toEqual([]); - })); + }) + ); it('should be able to compute test completion percentage', fakeAsync(() => { let submitAnswerSuccessCb = jasmine.createSpy('success'); @@ -1028,76 +1224,98 @@ describe('Diagnostic test engine service', () => { topicId2: [], topicId3: [], topicId4: [], - topicId5: [] + topicId5: [], }; const diagnosticTestCurrentTopicStatusModel = { skillId1: new DiagnosticTestQuestionsModel(question1, question2), - skillId2: new DiagnosticTestQuestionsModel(question3, question4) + skillId2: new DiagnosticTestQuestionsModel(question3, question4), }; - const diagnosticTestTopicTrackerModel = ( - new DiagnosticTestTopicTrackerModel(topicIdToPrerequisiteTopicIds)); + const diagnosticTestTopicTrackerModel = new DiagnosticTestTopicTrackerModel( + topicIdToPrerequisiteTopicIds + ); spyOn( questionBackendApiService, - 'fetchDiagnosticTestQuestionsAsync').and.returnValue( - Promise.resolve(diagnosticTestCurrentTopicStatusModel)); + 'fetchDiagnosticTestQuestionsAsync' + ).and.returnValue(Promise.resolve(diagnosticTestCurrentTopicStatusModel)); diagnosticTestPlayerEngineService.init( - diagnosticTestTopicTrackerModel, initSuccessCb); + diagnosticTestTopicTrackerModel, + initSuccessCb + ); tick(); - expect(diagnosticTestPlayerEngineService.getCurrentTopicId()) - .toEqual('topicId1'); + expect(diagnosticTestPlayerEngineService.getCurrentTopicId()).toEqual( + 'topicId1' + ); expect(diagnosticTestPlayerEngineService.getCurrentSkillId()).toEqual( - 'skillId1'); + 'skillId1' + ); // Getting the main question from the current skill. expect(diagnosticTestPlayerEngineService.getCurrentQuestion()).toEqual( - question1); + question1 + ); let answer = 'answer'; let answerClassificationResult = new AnswerClassificationResult( - outcomeObjectFactory - .createNew('default', '', '', []), 1, 0, 'default_outcome' + outcomeObjectFactory.createNew('default', '', '', []), + 1, + 0, + 'default_outcome' ); answerClassificationResult.outcome.labelledAsCorrect = false; - spyOn(answerClassificationService, 'getMatchingClassificationResult') - .and.returnValue(answerClassificationResult); + spyOn( + answerClassificationService, + 'getMatchingClassificationResult' + ).and.returnValue(answerClassificationResult); // Submitting incorrect answer. diagnosticTestPlayerEngineService.submitAnswer( - answer, textInputService, submitAnswerSuccessCb); + answer, + textInputService, + submitAnswerSuccessCb + ); tick(); - expect(diagnosticTestPlayerEngineService.computeProgressPercentage()) - .toEqual(3); + expect( + diagnosticTestPlayerEngineService.computeProgressPercentage() + ).toEqual(3); - expect(diagnosticTestPlayerEngineService.getCurrentTopicId()) - .toEqual('topicId1'); + expect(diagnosticTestPlayerEngineService.getCurrentTopicId()).toEqual( + 'topicId1' + ); // An incorrect attempt does not change the skill, instead, the engine // presents the backup question of the same skill. expect(diagnosticTestPlayerEngineService.getCurrentSkillId()).toEqual( - 'skillId1'); + 'skillId1' + ); // Getting the backup question i.e., question2. expect(diagnosticTestPlayerEngineService.getCurrentQuestion()).toEqual( - question2); + question2 + ); // Submitting incorrect answer. diagnosticTestPlayerEngineService.submitAnswer( - answer, textInputService, submitAnswerSuccessCb); + answer, + textInputService, + submitAnswerSuccessCb + ); tick(); // Topic 1 is marked as failed. - expect(diagnosticTestPlayerEngineService.getFailedTopicIds()).toEqual( - ['topicId1']); + expect(diagnosticTestPlayerEngineService.getFailedTopicIds()).toEqual([ + 'topicId1', + ]); // Out of the five topics, one topic is tested. So the test completion // percentage should be 20%. - expect(diagnosticTestPlayerEngineService.computeProgressPercentage()) - .toEqual(20); + expect( + diagnosticTestPlayerEngineService.computeProgressPercentage() + ).toEqual(20); })); }); diff --git a/core/templates/pages/exploration-player-page/services/diagnostic-test-player-engine.service.ts b/core/templates/pages/exploration-player-page/services/diagnostic-test-player-engine.service.ts index a37742f3f347..09567d585e1c 100644 --- a/core/templates/pages/exploration-player-page/services/diagnostic-test-player-engine.service.ts +++ b/core/templates/pages/exploration-player-page/services/diagnostic-test-player-engine.service.ts @@ -16,38 +16,39 @@ * @fileoverview Utility service for the diagnostic test player. */ -import { Injectable } from '@angular/core'; +import {Injectable} from '@angular/core'; import cloneDeep from 'lodash/cloneDeep'; -import { AppConstants } from 'app.constants'; -import { BindableVoiceovers } from 'domain/exploration/recorded-voiceovers.model'; -import { Question } from 'domain/question/QuestionObjectFactory'; -import { State } from 'domain/state/StateObjectFactory'; -import { StateCard } from 'domain/state_card/state-card.model'; -import { ExpressionInterpolationService } from 'expressions/expression-interpolation.service'; -import { InteractionAnswer } from 'interactions/answer-defs'; -import { AnswerClassificationService, InteractionRulesService } from 'pages/exploration-player-page/services/answer-classification.service'; -import { AlertsService } from 'services/alerts.service'; -import { ContextService } from 'services/context.service'; -import { ExplorationHtmlFormatterService } from 'services/exploration-html-formatter.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { AudioTranslationLanguageService } from - 'pages/exploration-player-page/services/audio-translation-language.service'; -import { DiagnosticTestCurrentTopicStatusModel } from 'pages/diagnostic-test-player-page/diagnostic-test-current-topic-status.model'; -import { DiagnosticTestTopicTrackerModel } from 'pages/diagnostic-test-player-page/diagnostic-test-topic-tracker.model'; -import { QuestionBackendApiService } from 'domain/question/question-backend-api.service'; -import { DiagnosticTestPlayerStatusService } from 'pages/diagnostic-test-player-page/diagnostic-test-player-status.service'; -import { AnswerClassificationResult } from 'domain/classifier/answer-classification-result.model'; +import {AppConstants} from 'app.constants'; +import {BindableVoiceovers} from 'domain/exploration/recorded-voiceovers.model'; +import {Question} from 'domain/question/QuestionObjectFactory'; +import {State} from 'domain/state/StateObjectFactory'; +import {StateCard} from 'domain/state_card/state-card.model'; +import {ExpressionInterpolationService} from 'expressions/expression-interpolation.service'; +import {InteractionAnswer} from 'interactions/answer-defs'; +import { + AnswerClassificationService, + InteractionRulesService, +} from 'pages/exploration-player-page/services/answer-classification.service'; +import {AlertsService} from 'services/alerts.service'; +import {ContextService} from 'services/context.service'; +import {ExplorationHtmlFormatterService} from 'services/exploration-html-formatter.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {AudioTranslationLanguageService} from 'pages/exploration-player-page/services/audio-translation-language.service'; +import {DiagnosticTestCurrentTopicStatusModel} from 'pages/diagnostic-test-player-page/diagnostic-test-current-topic-status.model'; +import {DiagnosticTestTopicTrackerModel} from 'pages/diagnostic-test-player-page/diagnostic-test-topic-tracker.model'; +import {QuestionBackendApiService} from 'domain/question/question-backend-api.service'; +import {DiagnosticTestPlayerStatusService} from 'pages/diagnostic-test-player-page/diagnostic-test-player-status.service'; +import {AnswerClassificationResult} from 'domain/classifier/answer-classification-result.model'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class DiagnosticTestPlayerEngineService { private _answerIsBeingProcessed: boolean = false; private _diagnosticTestTopicTrackerModel!: DiagnosticTestTopicTrackerModel; private _initialCopyOfTopicTrackerModel!: DiagnosticTestTopicTrackerModel; - private _diagnosticTestCurrentTopicStatusModel!: - DiagnosticTestCurrentTopicStatusModel; + private _diagnosticTestCurrentTopicStatusModel!: DiagnosticTestCurrentTopicStatusModel; private _currentQuestion!: Question; private _currentTopicId!: string; @@ -65,108 +66,121 @@ export class DiagnosticTestPlayerEngineService { private expressionInterpolationService: ExpressionInterpolationService, private focusManagerService: FocusManagerService, private questionBackendApiService: QuestionBackendApiService, - private diagnosticTestPlayerStatusService: - DiagnosticTestPlayerStatusService) { + private diagnosticTestPlayerStatusService: DiagnosticTestPlayerStatusService + ) { this._numberOfAttemptedQuestions = 0; this._encounteredQuestionIds = []; } init( - diagnosticTestTopicTrackerModel: DiagnosticTestTopicTrackerModel, - successCallback: (initialCard: StateCard, nextFocusLabel: string) => void + diagnosticTestTopicTrackerModel: DiagnosticTestTopicTrackerModel, + successCallback: (initialCard: StateCard, nextFocusLabel: string) => void ): void { this._diagnosticTestTopicTrackerModel = diagnosticTestTopicTrackerModel; this._initialCopyOfTopicTrackerModel = cloneDeep( - diagnosticTestTopicTrackerModel); - this._currentTopicId = ( - this._diagnosticTestTopicTrackerModel.selectNextTopicIdToTest()); - - this.questionBackendApiService.fetchDiagnosticTestQuestionsAsync( - this._currentTopicId, this._encounteredQuestionIds).then((response) => { - this._diagnosticTestCurrentTopicStatusModel = ( - new DiagnosticTestCurrentTopicStatusModel(response)); - - // The diagnostic test current topic status model is initialized in the - // above step, so there will always be at least one skill ID in the - // pending list. Hence accessing the first element from the pending list - // in the below line is safe. - this._currentSkillId = ( - this._diagnosticTestCurrentTopicStatusModel.getPendingSkillIds()[0]); - this._currentQuestion = ( - this._diagnosticTestCurrentTopicStatusModel.getNextQuestion( - this._currentSkillId) + diagnosticTestTopicTrackerModel + ); + this._currentTopicId = + this._diagnosticTestTopicTrackerModel.selectNextTopicIdToTest(); + + this.questionBackendApiService + .fetchDiagnosticTestQuestionsAsync( + this._currentTopicId, + this._encounteredQuestionIds + ) + .then( + response => { + this._diagnosticTestCurrentTopicStatusModel = + new DiagnosticTestCurrentTopicStatusModel(response); + + // The diagnostic test current topic status model is initialized in the + // above step, so there will always be at least one skill ID in the + // pending list. Hence accessing the first element from the pending list + // in the below line is safe. + this._currentSkillId = + this._diagnosticTestCurrentTopicStatusModel.getPendingSkillIds()[0]; + this._currentQuestion = + this._diagnosticTestCurrentTopicStatusModel.getNextQuestion( + this._currentSkillId + ); + const stateCard = this.createCard(this._currentQuestion); + successCallback(stateCard, this._focusLabel); + }, + () => { + this.alertsService.addWarning('Failed to load the questions.'); + } ); - const stateCard = this.createCard(this._currentQuestion); - successCallback(stateCard, this._focusLabel); - }, () => { - this.alertsService.addWarning('Failed to load the questions.'); - }); } private _getNextQuestion( - successCallback: (value: Question) => void, - errorCallback: () => void + successCallback: (value: Question) => void, + errorCallback: () => void ): void { - let currentTopicIsCompletelyTested = ( - this._diagnosticTestCurrentTopicStatusModel.isTopicCompletelyTested()); + let currentTopicIsCompletelyTested = + this._diagnosticTestCurrentTopicStatusModel.isTopicCompletelyTested(); if (currentTopicIsCompletelyTested) { - let topicIsPassed = ( - this._diagnosticTestCurrentTopicStatusModel.isTopicPassed()); + let topicIsPassed = + this._diagnosticTestCurrentTopicStatusModel.isTopicPassed(); if (topicIsPassed) { this._diagnosticTestTopicTrackerModel.recordTopicPassed( - this._currentTopicId); + this._currentTopicId + ); } else { this._diagnosticTestTopicTrackerModel.recordTopicFailed( - this._currentTopicId); + this._currentTopicId + ); } if (this.isDiagnosticTestFinished()) { errorCallback(); } - this._currentTopicId = ( - this._diagnosticTestTopicTrackerModel.selectNextTopicIdToTest()); - - this.questionBackendApiService.fetchDiagnosticTestQuestionsAsync( - this._currentTopicId, this._encounteredQuestionIds).then( - skillIdToQuestionsModel => { - this._diagnosticTestCurrentTopicStatusModel = ( - new DiagnosticTestCurrentTopicStatusModel( - skillIdToQuestionsModel) - ); - // The diagnostic test current topic status model is initialized in - // the above step, so there will always be at least one skill ID in - // the pending list. Hence accessing the first element from the - // pending list in the below line is safe. - this._currentSkillId = ( - this._diagnosticTestCurrentTopicStatusModel - .getPendingSkillIds()[0] - ); - - this._currentQuestion = ( - this._diagnosticTestCurrentTopicStatusModel.getNextQuestion( - this._currentSkillId) - ); - return successCallback(this._currentQuestion); - }, () => { - this.alertsService.addWarning('Failed to load the questions.'); - } - ); + this._currentTopicId = + this._diagnosticTestTopicTrackerModel.selectNextTopicIdToTest(); + + this.questionBackendApiService + .fetchDiagnosticTestQuestionsAsync( + this._currentTopicId, + this._encounteredQuestionIds + ) + .then( + skillIdToQuestionsModel => { + this._diagnosticTestCurrentTopicStatusModel = + new DiagnosticTestCurrentTopicStatusModel( + skillIdToQuestionsModel + ); + // The diagnostic test current topic status model is initialized in + // the above step, so there will always be at least one skill ID in + // the pending list. Hence accessing the first element from the + // pending list in the below line is safe. + this._currentSkillId = + this._diagnosticTestCurrentTopicStatusModel.getPendingSkillIds()[0]; + + this._currentQuestion = + this._diagnosticTestCurrentTopicStatusModel.getNextQuestion( + this._currentSkillId + ); + return successCallback(this._currentQuestion); + }, + () => { + this.alertsService.addWarning('Failed to load the questions.'); + } + ); } else { if (!this._diagnosticTestCurrentTopicStatusModel.isLifelineConsumed()) { // The topic completion is checked in the above step, so there will // always be at least one skill ID left for checking. Hence accessing // the first element from the pending list in the below line is safe. - this._currentSkillId = ( - this._diagnosticTestCurrentTopicStatusModel.getPendingSkillIds()[0]); + this._currentSkillId = + this._diagnosticTestCurrentTopicStatusModel.getPendingSkillIds()[0]; } - this._currentQuestion = ( + this._currentQuestion = this._diagnosticTestCurrentTopicStatusModel.getNextQuestion( - this._currentSkillId) - ); + this._currentSkillId + ); return successCallback(this._currentQuestion); } } @@ -178,29 +192,32 @@ export class DiagnosticTestPlayerEngineService { } submitAnswer( - answer: InteractionAnswer, - interactionRulesService: InteractionRulesService, - successCallback: ( - nextCard: StateCard, - refreshInteraction: boolean, - feedbackHtml: string, - feedbackAudioTranslations: BindableVoiceovers, - refresherExplorationId: string, - missingPrerequisiteSkillId: string, - remainOnCurrentCard: boolean, - taggedSkillMisconceptionId: string, - wasOldStateInitial: boolean, - isFirstHit: boolean, - isFinalQuestion: boolean, - nextCardIfReallyStuck: StateCard, - focusLabel: string - ) => void + answer: InteractionAnswer, + interactionRulesService: InteractionRulesService, + successCallback: ( + nextCard: StateCard, + refreshInteraction: boolean, + feedbackHtml: string, + feedbackAudioTranslations: BindableVoiceovers, + refresherExplorationId: string, + missingPrerequisiteSkillId: string, + remainOnCurrentCard: boolean, + taggedSkillMisconceptionId: string, + wasOldStateInitial: boolean, + isFirstHit: boolean, + isFinalQuestion: boolean, + nextCardIfReallyStuck: StateCard, + focusLabel: string + ) => void ): boolean { const oldState: State = this._currentQuestion.getStateData(); - const classificationResult: AnswerClassificationResult = ( + const classificationResult: AnswerClassificationResult = this.answerClassificationService.getMatchingClassificationResult( - oldState.name as string, oldState.interaction, answer, - interactionRulesService)); + oldState.name as string, + oldState.interaction, + answer, + interactionRulesService + ); const answerIsCorrect = classificationResult.outcome.labelledAsCorrect; let stateCard: StateCard; @@ -220,54 +237,71 @@ export class DiagnosticTestPlayerEngineService { if (answerIsCorrect) { this._diagnosticTestCurrentTopicStatusModel.recordCorrectAttempt( - this._currentSkillId); + this._currentSkillId + ); } else { this._diagnosticTestCurrentTopicStatusModel.recordIncorrectAttempt( - this._currentSkillId); + this._currentSkillId + ); } - this.getNextQuestionAsync().then((question: Question) => { - stateCard = this.createCard(question); - focusLabel = this._focusLabel; - - successCallback( - stateCard, refreshInteraction, feedbackHtml, - feedbackAudioTranslations, refresherExplorationId, - missingPrerequisiteSkillId, remainOnCurrentCard, - taggedSkillMisconceptionId, wasOldStateInitial, isFirstHit, - isFinalQuestion, stateCard, focusLabel - ); - this.diagnosticTestPlayerStatusService - .onDiagnosticTestSessionProgressChange.emit( + this.getNextQuestionAsync().then( + (question: Question) => { + stateCard = this.createCard(question); + focusLabel = this._focusLabel; + + successCallback( + stateCard, + refreshInteraction, + feedbackHtml, + feedbackAudioTranslations, + refresherExplorationId, + missingPrerequisiteSkillId, + remainOnCurrentCard, + taggedSkillMisconceptionId, + wasOldStateInitial, + isFirstHit, + isFinalQuestion, + stateCard, + focusLabel + ); + this.diagnosticTestPlayerStatusService.onDiagnosticTestSessionProgressChange.emit( this.computeProgressPercentage() ); - }, () => { - // Test is finished. - const recommendedTopicIds: string[] = this.getRecommendedTopicIds(); - this.diagnosticTestPlayerStatusService - .onDiagnosticTestSessionCompleted.emit(recommendedTopicIds); - }); + }, + () => { + // Test is finished. + const recommendedTopicIds: string[] = this.getRecommendedTopicIds(); + this.diagnosticTestPlayerStatusService.onDiagnosticTestSessionCompleted.emit( + recommendedTopicIds + ); + } + ); return answerIsCorrect; } skipCurrentQuestion(successCallback: (stateCard: StateCard) => void): void { this._diagnosticTestCurrentTopicStatusModel.recordIncorrectAttempt( - this._currentSkillId); + this._currentSkillId + ); - this.getNextQuestionAsync().then((question: Question) => { - let stateCard = this.createCard(question); + this.getNextQuestionAsync().then( + (question: Question) => { + let stateCard = this.createCard(question); - successCallback(stateCard); - this.diagnosticTestPlayerStatusService - .onDiagnosticTestSessionProgressChange.emit( + successCallback(stateCard); + this.diagnosticTestPlayerStatusService.onDiagnosticTestSessionProgressChange.emit( this.computeProgressPercentage() ); - }, () => { - // Test is finished. - const recommendedTopicIds: string[] = this.getRecommendedTopicIds(); - this.diagnosticTestPlayerStatusService - .onDiagnosticTestSessionCompleted.emit(recommendedTopicIds); - }); + }, + () => { + // Test is finished. + const recommendedTopicIds: string[] = this.getRecommendedTopicIds(); + this.diagnosticTestPlayerStatusService.onDiagnosticTestSessionCompleted.emit( + recommendedTopicIds + ); + } + ); } getRecommendedTopicIds(): string[] { @@ -308,8 +342,8 @@ export class DiagnosticTestPlayerEngineService { } getRootTopicIds(): string[] { - let topicIdToPrerequisiteTopicId = ( - this._initialCopyOfTopicTrackerModel.getTopicIdToPrerequisiteTopicIds()); + let topicIdToPrerequisiteTopicId = + this._initialCopyOfTopicTrackerModel.getTopicIdToPrerequisiteTopicIds(); // The topics which do not contain any prerequisites are referred to as // root topics. let rootTopicIds: string[] = []; @@ -324,8 +358,8 @@ export class DiagnosticTestPlayerEngineService { getTopologicallySortedTopicIds(): string[] { let visitedTopicIds: string[] = []; - let topicIdToPrerequisiteTopicId = ( - this._initialCopyOfTopicTrackerModel.getTopicIdToPrerequisiteTopicIds()); + let topicIdToPrerequisiteTopicId = + this._initialCopyOfTopicTrackerModel.getTopicIdToPrerequisiteTopicIds(); for (let currentTopicId in topicIdToPrerequisiteTopicId) { if (visitedTopicIds.indexOf(currentTopicId) !== -1) { @@ -359,35 +393,35 @@ export class DiagnosticTestPlayerEngineService { } computeProgressPercentage(): number { - let numberOfAttemptedQuestionsInCurrentTopic = ( - this._diagnosticTestCurrentTopicStatusModel.numberOfAttemptedQuestions); - let initialTopicIdsList = ( - this._initialCopyOfTopicTrackerModel.getPendingTopicIdsToTest()); - let pendingTopicIdsToTest = ( - this._diagnosticTestTopicTrackerModel.getPendingTopicIdsToTest()); + let numberOfAttemptedQuestionsInCurrentTopic = + this._diagnosticTestCurrentTopicStatusModel.numberOfAttemptedQuestions; + let initialTopicIdsList = + this._initialCopyOfTopicTrackerModel.getPendingTopicIdsToTest(); + let pendingTopicIdsToTest = + this._diagnosticTestTopicTrackerModel.getPendingTopicIdsToTest(); // Each topic can contain a maximum of 3 diagnostic test skills and at most // 2 questions [main question & backup question] can be presented from each // skill. Thus, the maximum number of questions that can be asked from a // topic is 6. - let completionMetric = ((( - initialTopicIdsList.length - pendingTopicIdsToTest.length) * 6 + - numberOfAttemptedQuestionsInCurrentTopic) / ( - initialTopicIdsList.length * 6)); + let completionMetric = + ((initialTopicIdsList.length - pendingTopicIdsToTest.length) * 6 + + numberOfAttemptedQuestionsInCurrentTopic) / + (initialTopicIdsList.length * 6); return Math.round(completionMetric * 100); } getLanguageCode(): string { - return ( - this._diagnosticTestCurrentTopicStatusModel.getNextQuestion( - this._currentSkillId).getLanguageCode() - ); + return this._diagnosticTestCurrentTopicStatusModel + .getNextQuestion(this._currentSkillId) + .getLanguageCode(); } recordNewCardAdded(): void { this.contextService.setCustomEntityContext( - AppConstants.ENTITY_TYPE.QUESTION, this._currentQuestion.getId() as string + AppConstants.ENTITY_TYPE.QUESTION, + this._currentQuestion.getId() as string ); } @@ -395,10 +429,11 @@ export class DiagnosticTestPlayerEngineService { this._encounteredQuestionIds.push(question.getId() as string); const stateData: State = question.getStateData(); - const questionHtml: string = ( + const questionHtml: string = this.expressionInterpolationService.processHtml( - stateData.content.html, []) - ); + stateData.content.html, + [] + ); if (questionHtml === '') { this.alertsService.addWarning('Question name should not be empty.'); @@ -410,16 +445,21 @@ export class DiagnosticTestPlayerEngineService { let interactionHtml: string = ''; if (interactionId) { - interactionHtml = ( - this.explorationHtmlFormatterService.getInteractionHtml( - interactionId, interaction.customizationArgs, - true, this._focusLabel, null) + interactionHtml = this.explorationHtmlFormatterService.getInteractionHtml( + interactionId, + interaction.customizationArgs, + true, + this._focusLabel, + null ); } return StateCard.createNewCard( - stateData.name as string, questionHtml, interactionHtml as string, - interaction, stateData.recordedVoiceovers, + stateData.name as string, + questionHtml, + interactionHtml as string, + interaction, + stateData.recordedVoiceovers, stateData.content.contentId as string, this.audioTranslationLanguageService ); @@ -430,15 +470,15 @@ export class DiagnosticTestPlayerEngineService { } isDiagnosticTestFinished(): boolean { - const pendingTopicIdsToTest = ( - this._diagnosticTestTopicTrackerModel.getPendingTopicIdsToTest().length - ); + const pendingTopicIdsToTest = + this._diagnosticTestTopicTrackerModel.getPendingTopicIdsToTest().length; if (pendingTopicIdsToTest === 0) { return true; } else if ( - this._numberOfAttemptedQuestions >= DiagnosticTestPlayerEngineService - .MAX_ALLOWED_QUESTIONS_IN_THE_DIAGNOSTIC_TEST) { + this._numberOfAttemptedQuestions >= + DiagnosticTestPlayerEngineService.MAX_ALLOWED_QUESTIONS_IN_THE_DIAGNOSTIC_TEST + ) { return true; } else { return false; diff --git a/core/templates/pages/exploration-player-page/services/exploration-engine.service.spec.ts b/core/templates/pages/exploration-player-page/services/exploration-engine.service.spec.ts index 3bb4b4b18e96..2a208f9bd84d 100644 --- a/core/templates/pages/exploration-player-page/services/exploration-engine.service.spec.ts +++ b/core/templates/pages/exploration-player-page/services/exploration-engine.service.spec.ts @@ -16,34 +16,48 @@ * @fileoverview Unit tests for the exploration engine service. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { EventEmitter } from '@angular/core'; -import { fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; -import { MockTranslateService } from 'components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec'; -import { AnswerClassificationResult } from 'domain/classifier/answer-classification-result.model'; -import { InteractionObjectFactory } from 'domain/exploration/InteractionObjectFactory'; -import { ExplorationBackendDict, ExplorationObjectFactory } from 'domain/exploration/ExplorationObjectFactory'; -import { OutcomeObjectFactory } from 'domain/exploration/OutcomeObjectFactory'; -import { ParamChangeBackendDict, ParamChangeObjectFactory } from 'domain/exploration/ParamChangeObjectFactory'; -import { FetchExplorationBackendResponse, ReadOnlyExplorationBackendApiService } from 'domain/exploration/read-only-exploration-backend-api.service'; -import { StateCard } from 'domain/state_card/state-card.model'; -import { ExpressionInterpolationService } from 'expressions/expression-interpolation.service'; -import { TextInputRulesService } from 'interactions/TextInput/directives/text-input-rules.service'; -import { AlertsService } from 'services/alerts.service'; -import { ContextService } from 'services/context.service'; -import { UrlService } from 'services/contextual/url.service'; -import { ExplorationFeatures, ExplorationFeaturesBackendApiService } from 'services/exploration-features-backend-api.service'; -import { AnswerClassificationService, InteractionRulesService } from './answer-classification.service'; -import { AudioPreloaderService } from './audio-preloader.service'; -import { ContentTranslationLanguageService } from './content-translation-language.service'; -import { ExplorationEngineService } from './exploration-engine.service'; -import { ImagePreloaderService } from './image-preloader.service'; -import { LearnerParamsService } from './learner-params.service'; -import { PlayerTranscriptService } from './player-transcript.service'; -import { StatsReportingService } from './stats-reporting.service'; -import { AudioTranslationLanguageService } from - 'pages/exploration-player-page/services/audio-translation-language.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {EventEmitter} from '@angular/core'; +import {fakeAsync, TestBed, tick} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; +import {MockTranslateService} from 'components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec'; +import {AnswerClassificationResult} from 'domain/classifier/answer-classification-result.model'; +import {InteractionObjectFactory} from 'domain/exploration/InteractionObjectFactory'; +import { + ExplorationBackendDict, + ExplorationObjectFactory, +} from 'domain/exploration/ExplorationObjectFactory'; +import {OutcomeObjectFactory} from 'domain/exploration/OutcomeObjectFactory'; +import { + ParamChangeBackendDict, + ParamChangeObjectFactory, +} from 'domain/exploration/ParamChangeObjectFactory'; +import { + FetchExplorationBackendResponse, + ReadOnlyExplorationBackendApiService, +} from 'domain/exploration/read-only-exploration-backend-api.service'; +import {StateCard} from 'domain/state_card/state-card.model'; +import {ExpressionInterpolationService} from 'expressions/expression-interpolation.service'; +import {TextInputRulesService} from 'interactions/TextInput/directives/text-input-rules.service'; +import {AlertsService} from 'services/alerts.service'; +import {ContextService} from 'services/context.service'; +import {UrlService} from 'services/contextual/url.service'; +import { + ExplorationFeatures, + ExplorationFeaturesBackendApiService, +} from 'services/exploration-features-backend-api.service'; +import { + AnswerClassificationService, + InteractionRulesService, +} from './answer-classification.service'; +import {AudioPreloaderService} from './audio-preloader.service'; +import {ContentTranslationLanguageService} from './content-translation-language.service'; +import {ExplorationEngineService} from './exploration-engine.service'; +import {ImagePreloaderService} from './image-preloader.service'; +import {LearnerParamsService} from './learner-params.service'; +import {PlayerTranscriptService} from './player-transcript.service'; +import {StatsReportingService} from './stats-reporting.service'; +import {AudioTranslationLanguageService} from 'pages/exploration-player-page/services/audio-translation-language.service'; describe('Exploration engine service ', () => { let alertsService: AlertsService; @@ -53,16 +67,14 @@ describe('Exploration engine service ', () => { let contextService: ContextService; let contentTranslationLanguageService: ContentTranslationLanguageService; let expressionInterpolationService: ExpressionInterpolationService; - let explorationFeaturesBackendApiService: - ExplorationFeaturesBackendApiService; + let explorationFeaturesBackendApiService: ExplorationFeaturesBackendApiService; let explorationEngineService: ExplorationEngineService; let explorationObjectFactory: ExplorationObjectFactory; let imagePreloaderService: ImagePreloaderService; let interactionObjectFactory: InteractionObjectFactory; let learnerParamsService: LearnerParamsService; let playerTranscriptService: PlayerTranscriptService; - let readOnlyExplorationBackendApiService: - ReadOnlyExplorationBackendApiService; + let readOnlyExplorationBackendApiService: ReadOnlyExplorationBackendApiService; let statsReportingService: StatsReportingService; let urlService: UrlService; let paramChangeObjectFactory: ParamChangeObjectFactory; @@ -85,8 +97,8 @@ describe('Exploration engine service ', () => { feedback_1: {}, rule_input_2: {}, content: {}, - default_outcome: {} - } + default_outcome: {}, + }, }, solicit_answer_details: false, interaction: { @@ -96,17 +108,17 @@ describe('Exploration engine service ', () => { hints: [], customization_args: { rows: { - value: 1 + value: 1, }, placeholder: { value: { unicode_str: '', - content_id: 'ca_placeholder_0' - } + content_id: 'ca_placeholder_0', + }, }, catchMisspellings: { - value: false - } + value: false, + }, }, answer_groups: [ { @@ -116,28 +128,26 @@ describe('Exploration engine service ', () => { labelled_as_correct: false, feedback: { content_id: 'feedback_1', - html: '

Good Job

' + html: '

Good Job

', }, param_changes: [], dest_if_really_stuck: 'Mid', - dest: 'Mid' + dest: 'Mid', }, training_data: [], rule_specs: [ { inputs: { x: { - normalizedStrSet: [ - 'answer' - ], - contentId: 'rule_input_2' - } + normalizedStrSet: ['answer'], + contentId: 'rule_input_2', + }, }, - rule_type: 'FuzzyEquals' - } + rule_type: 'FuzzyEquals', + }, ], - tagged_skill_misconception_id: null - } + tagged_skill_misconception_id: null, + }, ], default_outcome: { missing_prerequisite_skill_id: null, @@ -145,27 +155,27 @@ describe('Exploration engine service ', () => { labelled_as_correct: false, feedback: { content_id: 'default_outcome', - html: '

Try again.

' + html: '

Try again.

', }, param_changes: [], dest_if_really_stuck: 'Mid', - dest: 'Start' - } + dest: 'Start', + }, }, param_changes: [], card_is_checkpoint: true, linked_skill_id: null, content: { content_id: 'content', - html: '

First Question

' - } + html: '

First Question

', + }, }, End: { classifier_model_id: null, recorded_voiceovers: { voiceovers_mapping: { - content: {} - } + content: {}, + }, }, solicit_answer_details: false, interaction: { @@ -175,19 +185,19 @@ describe('Exploration engine service ', () => { hints: [], customization_args: { recommendedExplorationIds: { - value: ['recommnendedExplorationId'] - } + value: ['recommnendedExplorationId'], + }, }, answer_groups: [], - default_outcome: null + default_outcome: null, }, param_changes: [], card_is_checkpoint: false, linked_skill_id: null, content: { content_id: 'content', - html: 'Congratulations, you have finished!' - } + html: 'Congratulations, you have finished!', + }, }, Mid: { classifier_model_id: null, @@ -197,8 +207,8 @@ describe('Exploration engine service ', () => { feedback_1: {}, rule_input_2: {}, content: {}, - default_outcome: {} - } + default_outcome: {}, + }, }, solicit_answer_details: false, interaction: { @@ -208,17 +218,17 @@ describe('Exploration engine service ', () => { hints: [], customization_args: { rows: { - value: 1 + value: 1, }, placeholder: { value: { unicode_str: '', - content_id: 'ca_placeholder_0' - } + content_id: 'ca_placeholder_0', + }, }, catchMisspellings: { - value: false - } + value: false, + }, }, answer_groups: [ { @@ -228,28 +238,26 @@ describe('Exploration engine service ', () => { labelled_as_correct: false, feedback: { content_id: 'feedback_1', - html: '

Good Job

' + html: '

Good Job

', }, param_changes: [], dest_if_really_stuck: 'Mid', - dest: 'End' + dest: 'End', }, training_data: [], rule_specs: [ { inputs: { x: { - normalizedStrSet: [ - 'answer' - ], - contentId: 'rule_input_2' - } + normalizedStrSet: ['answer'], + contentId: 'rule_input_2', + }, }, - rule_type: 'FuzzyEquals' - } + rule_type: 'FuzzyEquals', + }, ], - tagged_skill_misconception_id: null - } + tagged_skill_misconception_id: null, + }, ], default_outcome: { missing_prerequisite_skill_id: null, @@ -257,31 +265,31 @@ describe('Exploration engine service ', () => { labelled_as_correct: false, feedback: { content_id: 'default_outcome', - html: '

try again.

' + html: '

try again.

', }, param_changes: [], dest_if_really_stuck: 'Mid', - dest: 'Mid' - } + dest: 'Mid', + }, }, param_changes: [], card_is_checkpoint: false, linked_skill_id: null, content: { content_id: 'content', - html: '

Second Question

' - } - } + html: '

Second Question

', + }, + }, }, auto_tts_enabled: true, version: 2, param_specs: { x: { - obj_type: 'UnicodeString' + obj_type: 'UnicodeString', }, y: { - obj_type: 'UnicodeString' - } + obj_type: 'UnicodeString', + }, }, param_changes: [], title: 'My Exploration Title', @@ -304,18 +312,18 @@ describe('Exploration engine service ', () => { param_specs: {}, param_changes: [], auto_tts_enabled: false, - edits_allowed: true - } + edits_allowed: true, + }, }; paramChangeDict = { customization_args: { parse_with_jinja: false, value: 'val', - list_of_values: ['val1, val2'] + list_of_values: ['val1, val2'], }, generator_id: 'Copier', - name: 'answer' + name: 'answer', }; explorationBackendResponse = { @@ -329,7 +337,7 @@ describe('Exploration engine service ', () => { title: '', language_code: '', objective: '', - next_content_id_index: 1 + next_content_id_index: 1, }, exploration_metadata: { title: '', @@ -344,7 +352,7 @@ describe('Exploration engine service ', () => { param_specs: {}, param_changes: [], auto_tts_enabled: false, - edits_allowed: true + edits_allowed: true, }, exploration_id: 'test_id', is_logged_in: true, @@ -359,47 +367,50 @@ describe('Exploration engine service ', () => { furthest_reached_checkpoint_exp_version: 1, furthest_reached_checkpoint_state_name: 'State B', most_recently_reached_checkpoint_state_name: 'State A', - most_recently_reached_checkpoint_exp_version: 1 + most_recently_reached_checkpoint_exp_version: 1, }; explorationFeatures = { explorationIsCurated: true, - alwaysAskLearnersForAnswerDetails: true + alwaysAskLearnersForAnswerDetails: true, }; }); beforeEach(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - ], + imports: [HttpClientTestingModule], providers: [ { provide: TranslateService, - useClass: MockTranslateService - } - ] + useClass: MockTranslateService, + }, + ], }); alertsService = TestBed.inject(AlertsService); answerClassificationService = TestBed.inject(AnswerClassificationService); audioPreloaderService = TestBed.inject(AudioPreloaderService); audioTranslationLanguageService = TestBed.inject( - AudioTranslationLanguageService); + AudioTranslationLanguageService + ); contextService = TestBed.inject(ContextService); contentTranslationLanguageService = TestBed.inject( - ContentTranslationLanguageService); + ContentTranslationLanguageService + ); expressionInterpolationService = TestBed.inject( - ExpressionInterpolationService); + ExpressionInterpolationService + ); explorationFeaturesBackendApiService = TestBed.inject( - ExplorationFeaturesBackendApiService); + ExplorationFeaturesBackendApiService + ); explorationObjectFactory = TestBed.inject(ExplorationObjectFactory); interactionObjectFactory = TestBed.inject(InteractionObjectFactory); imagePreloaderService = TestBed.inject(ImagePreloaderService); learnerParamsService = TestBed.inject(LearnerParamsService); playerTranscriptService = TestBed.inject(PlayerTranscriptService); readOnlyExplorationBackendApiService = TestBed.inject( - ReadOnlyExplorationBackendApiService); + ReadOnlyExplorationBackendApiService + ); statsReportingService = TestBed.inject(StatsReportingService); urlService = TestBed.inject(UrlService); explorationEngineService = TestBed.inject(ExplorationEngineService); @@ -417,193 +428,299 @@ describe('Exploration engine service ', () => { spyOn(imagePreloaderService, 'kickOffImagePreloader').and.returnValue(null); spyOn(audioPreloaderService, 'init').and.returnValue(null); spyOn(audioPreloaderService, 'kickOffAudioPreloader').and.returnValue(null); - spyOn(statsReportingService, 'recordExplorationStarted') - .and.returnValue(null); + spyOn(statsReportingService, 'recordExplorationStarted').and.returnValue( + null + ); spyOn(statsReportingService, 'recordAnswerSubmitted').and.returnValue(null); - spyOn(statsReportingService, 'recordAnswerSubmitAction') - .and.returnValue(null); - spyOn(expressionInterpolationService, 'processHtml') - .and.callFake((html, envs) => html); - spyOn(readOnlyExplorationBackendApiService, 'loadExplorationAsync') - .and.returnValue(Promise.resolve(explorationBackendResponse)); - }); - - it('should load exploration when initialized in ' + - 'exploration player page', () => { - let initSuccessCb = jasmine.createSpy('success'); - // Setting exploration player page. - spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(false); - - expect(explorationEngineService.isInPreviewMode()).toBe(false); - expect(() => { - explorationEngineService.getExplorationTitle(); - }).toThrowError('Cannot read properties of undefined (reading \'title\')'); - - explorationEngineService.init( - explorationDict, 1, null, true, ['en'], [], initSuccessCb); - - const explorationTitle = explorationEngineService.getExplorationTitle(); - expect(explorationTitle).toBe('My Exploration Title'); - expect(initSuccessCb).toHaveBeenCalled(); - }); - - it('should load exploration when initialized in ' + - 'exploration editor page', () => { - let initSuccessCb = jasmine.createSpy('success'); - let paramChanges = paramChangeObjectFactory.createFromBackendDict( - paramChangeDict); - // Setting exploration editor page. - spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(true); - spyOn(urlService, 'getPathname') - .and.returnValue('/create/in/path/name'); - spyOn(contextService, 'isInQuestionPlayerMode').and.returnValue(false); - - // Since the constructor will be automatically called in unit tests, it - // is hard to test or spy on the constructor. So, we have created a - // function to manually trigger and tests different edge cases. - explorationEngineService.setExplorationProperties(); - - expect(explorationEngineService.isInPreviewMode()).toBe(true); - expect(() => { - explorationEngineService.getExplorationTitle(); - }).toThrowError('Cannot read properties of undefined (reading \'title\')'); - - explorationEngineService.initSettingsFromEditor('Start', [paramChanges]); - explorationEngineService.init( - explorationDict, 1, null, true, ['en'], [], initSuccessCb); - - const explorationTitle = explorationEngineService.getExplorationTitle(); - expect(explorationTitle).toBe('My Exploration Title'); - expect(initSuccessCb).toHaveBeenCalled(); + spyOn(statsReportingService, 'recordAnswerSubmitAction').and.returnValue( + null + ); + spyOn(expressionInterpolationService, 'processHtml').and.callFake( + (html, envs) => html + ); + spyOn( + readOnlyExplorationBackendApiService, + 'loadExplorationAsync' + ).and.returnValue(Promise.resolve(explorationBackendResponse)); }); - describe('on submitting answer ', () => { - it('should call success callback if the submitted ' + - 'answer is correct', () => { + it( + 'should load exploration when initialized in ' + 'exploration player page', + () => { let initSuccessCb = jasmine.createSpy('success'); - let submitAnswerSuccessCb = jasmine.createSpy('success'); - let answer = 'answer'; - let answerClassificationResult = new AnswerClassificationResult( - outcomeObjectFactory.createFromBackendDict({ - dest: 'Mid', - dest_if_really_stuck: 'Mid', - feedback: { - content_id: 'feedback_1', - html: 'Answer is correct!' - }, - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null - }), 1, 0, 'default_outcome'); - - let lastCard = StateCard.createNewCard( - 'Card 1', 'Content html', 'Interaction text', null, - null, 'content_id', audioTranslationLanguageService); - + // Setting exploration player page. spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(false); - spyOn(playerTranscriptService, 'getLastStateName') - .and.returnValue('Start'); - spyOn(playerTranscriptService, 'getLastCard').and.returnValue(lastCard); - spyOn(answerClassificationService, 'getMatchingClassificationResult') - .and.returnValue(answerClassificationResult); - - explorationEngineService.init( - explorationDict, 1, null, true, ['en'], [], initSuccessCb); - - const isAnswerCorrect = explorationEngineService.submitAnswer( - answer, textInputService, submitAnswerSuccessCb); - - expect(submitAnswerSuccessCb).toHaveBeenCalled(); - expect(explorationEngineService.isAnswerBeingProcessed()).toBe(false); - expect(isAnswerCorrect).toBe(true); - }); - - it('should not submit answer again if the answer ' + - 'is already being processed', () => { - let initSuccessCb = jasmine.createSpy('success'); - let submitAnswerSuccessCb = jasmine.createSpy('success'); - let answer = 'answer'; - let answerClassificationResult = new AnswerClassificationResult( - outcomeObjectFactory.createFromBackendDict({ - dest: 'Mid', - dest_if_really_stuck: 'Mid', - feedback: { - content_id: 'feedback_1', - html: 'Answer is correct!' - }, - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null - }), 1, 0, 'default_outcome'); - - let lastCard = StateCard.createNewCard( - 'Card 1', 'Content html', 'Interaction text', null, - null, 'content_id', audioTranslationLanguageService); - spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(false); - spyOn(playerTranscriptService, 'getLastStateName') - .and.returnValue('Start'); - spyOn(playerTranscriptService, 'getLastCard').and.returnValue(lastCard); - spyOn(answerClassificationService, 'getMatchingClassificationResult') - .and.returnValue(answerClassificationResult); + expect(explorationEngineService.isInPreviewMode()).toBe(false); + expect(() => { + explorationEngineService.getExplorationTitle(); + }).toThrowError("Cannot read properties of undefined (reading 'title')"); explorationEngineService.init( - explorationDict, 1, null, true, ['en'], [], initSuccessCb); - - // Setting answer is being processed to true. - explorationEngineService.answerIsBeingProcessed = true; - explorationEngineService.submitAnswer( - answer, textInputService, submitAnswerSuccessCb); - - expect(submitAnswerSuccessCb).not.toHaveBeenCalled(); - }); - - it('should show warning message if the feedback ' + - 'content is empty', () => { + explorationDict, + 1, + null, + true, + ['en'], + [], + initSuccessCb + ); + + const explorationTitle = explorationEngineService.getExplorationTitle(); + expect(explorationTitle).toBe('My Exploration Title'); + expect(initSuccessCb).toHaveBeenCalled(); + } + ); + + it( + 'should load exploration when initialized in ' + 'exploration editor page', + () => { let initSuccessCb = jasmine.createSpy('success'); - let submitAnswerSuccessCb = jasmine.createSpy('success'); - let answer = 'answer'; - let answerClassificationResult = new AnswerClassificationResult( - outcomeObjectFactory.createFromBackendDict({ - dest: 'Mid', - dest_if_really_stuck: 'Mid', - feedback: { - content_id: 'feedback_1', - html: null - }, - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null - }), 1, 0, 'default_outcome'); - - let lastCard = StateCard.createNewCard( - 'Card 1', 'Content html', 'Interaction text', null, - null, 'content_id', audioTranslationLanguageService); - - spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(false); - spyOn(playerTranscriptService, 'getLastStateName') - .and.returnValue('Start'); - spyOn(playerTranscriptService, 'getLastCard').and.returnValue(lastCard); - spyOn(answerClassificationService, 'getMatchingClassificationResult') - .and.returnValue(answerClassificationResult); - let alertsServiceSpy = spyOn( - alertsService, 'addWarning').and.callThrough(); + let paramChanges = + paramChangeObjectFactory.createFromBackendDict(paramChangeDict); + // Setting exploration editor page. + spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(true); + spyOn(urlService, 'getPathname').and.returnValue('/create/in/path/name'); + spyOn(contextService, 'isInQuestionPlayerMode').and.returnValue(false); + + // Since the constructor will be automatically called in unit tests, it + // is hard to test or spy on the constructor. So, we have created a + // function to manually trigger and tests different edge cases. + explorationEngineService.setExplorationProperties(); + + expect(explorationEngineService.isInPreviewMode()).toBe(true); + expect(() => { + explorationEngineService.getExplorationTitle(); + }).toThrowError("Cannot read properties of undefined (reading 'title')"); + explorationEngineService.initSettingsFromEditor('Start', [paramChanges]); explorationEngineService.init( - explorationDict, 1, null, true, ['en'], [], initSuccessCb); + explorationDict, + 1, + null, + true, + ['en'], + [], + initSuccessCb + ); + + const explorationTitle = explorationEngineService.getExplorationTitle(); + expect(explorationTitle).toBe('My Exploration Title'); + expect(initSuccessCb).toHaveBeenCalled(); + } + ); - explorationEngineService.submitAnswer( - answer, textInputService, submitAnswerSuccessCb); + describe('on submitting answer ', () => { + it( + 'should call success callback if the submitted ' + 'answer is correct', + () => { + let initSuccessCb = jasmine.createSpy('success'); + let submitAnswerSuccessCb = jasmine.createSpy('success'); + let answer = 'answer'; + let answerClassificationResult = new AnswerClassificationResult( + outcomeObjectFactory.createFromBackendDict({ + dest: 'Mid', + dest_if_really_stuck: 'Mid', + feedback: { + content_id: 'feedback_1', + html: 'Answer is correct!', + }, + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + }), + 1, + 0, + 'default_outcome' + ); + + let lastCard = StateCard.createNewCard( + 'Card 1', + 'Content html', + 'Interaction text', + null, + null, + 'content_id', + audioTranslationLanguageService + ); + + spyOn(contextService, 'isInExplorationEditorPage').and.returnValue( + false + ); + spyOn(playerTranscriptService, 'getLastStateName').and.returnValue( + 'Start' + ); + spyOn(playerTranscriptService, 'getLastCard').and.returnValue(lastCard); + spyOn( + answerClassificationService, + 'getMatchingClassificationResult' + ).and.returnValue(answerClassificationResult); + + explorationEngineService.init( + explorationDict, + 1, + null, + true, + ['en'], + [], + initSuccessCb + ); + + const isAnswerCorrect = explorationEngineService.submitAnswer( + answer, + textInputService, + submitAnswerSuccessCb + ); + + expect(submitAnswerSuccessCb).toHaveBeenCalled(); + expect(explorationEngineService.isAnswerBeingProcessed()).toBe(false); + expect(isAnswerCorrect).toBe(true); + } + ); - expect(alertsServiceSpy) - .toHaveBeenCalledWith('Feedback content should not be empty.'); - }); + it( + 'should not submit answer again if the answer ' + + 'is already being processed', + () => { + let initSuccessCb = jasmine.createSpy('success'); + let submitAnswerSuccessCb = jasmine.createSpy('success'); + let answer = 'answer'; + let answerClassificationResult = new AnswerClassificationResult( + outcomeObjectFactory.createFromBackendDict({ + dest: 'Mid', + dest_if_really_stuck: 'Mid', + feedback: { + content_id: 'feedback_1', + html: 'Answer is correct!', + }, + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + }), + 1, + 0, + 'default_outcome' + ); + + let lastCard = StateCard.createNewCard( + 'Card 1', + 'Content html', + 'Interaction text', + null, + null, + 'content_id', + audioTranslationLanguageService + ); + + spyOn(contextService, 'isInExplorationEditorPage').and.returnValue( + false + ); + spyOn(playerTranscriptService, 'getLastStateName').and.returnValue( + 'Start' + ); + spyOn(playerTranscriptService, 'getLastCard').and.returnValue(lastCard); + spyOn( + answerClassificationService, + 'getMatchingClassificationResult' + ).and.returnValue(answerClassificationResult); + + explorationEngineService.init( + explorationDict, + 1, + null, + true, + ['en'], + [], + initSuccessCb + ); + + // Setting answer is being processed to true. + explorationEngineService.answerIsBeingProcessed = true; + explorationEngineService.submitAnswer( + answer, + textInputService, + submitAnswerSuccessCb + ); + + expect(submitAnswerSuccessCb).not.toHaveBeenCalled(); + } + ); - it('should show warning message if the parameters ' + - 'are empty', () => { + it( + 'should show warning message if the feedback ' + 'content is empty', + () => { + let initSuccessCb = jasmine.createSpy('success'); + let submitAnswerSuccessCb = jasmine.createSpy('success'); + let answer = 'answer'; + let answerClassificationResult = new AnswerClassificationResult( + outcomeObjectFactory.createFromBackendDict({ + dest: 'Mid', + dest_if_really_stuck: 'Mid', + feedback: { + content_id: 'feedback_1', + html: null, + }, + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + }), + 1, + 0, + 'default_outcome' + ); + + let lastCard = StateCard.createNewCard( + 'Card 1', + 'Content html', + 'Interaction text', + null, + null, + 'content_id', + audioTranslationLanguageService + ); + + spyOn(contextService, 'isInExplorationEditorPage').and.returnValue( + false + ); + spyOn(playerTranscriptService, 'getLastStateName').and.returnValue( + 'Start' + ); + spyOn(playerTranscriptService, 'getLastCard').and.returnValue(lastCard); + spyOn( + answerClassificationService, + 'getMatchingClassificationResult' + ).and.returnValue(answerClassificationResult); + let alertsServiceSpy = spyOn( + alertsService, + 'addWarning' + ).and.callThrough(); + + explorationEngineService.init( + explorationDict, + 1, + null, + true, + ['en'], + [], + initSuccessCb + ); + + explorationEngineService.submitAnswer( + answer, + textInputService, + submitAnswerSuccessCb + ); + + expect(alertsServiceSpy).toHaveBeenCalledWith( + 'Feedback content should not be empty.' + ); + } + ); + + it('should show warning message if the parameters ' + 'are empty', () => { let initSuccessCb = jasmine.createSpy('success'); let submitAnswerSuccessCb = jasmine.createSpy('success'); let answer = 'answer'; @@ -613,42 +730,66 @@ describe('Exploration engine service ', () => { dest_if_really_stuck: 'Mid', feedback: { content_id: 'feedback_1', - html: 'feedback' + html: 'feedback', }, labelled_as_correct: true, param_changes: [], refresher_exploration_id: null, - missing_prerequisite_skill_id: null - }), 1, 0, 'default_outcome'); + missing_prerequisite_skill_id: null, + }), + 1, + 0, + 'default_outcome' + ); let lastCard = StateCard.createNewCard( - 'Card 1', 'Content html', 'Interaction text', null, - null, 'content_id', audioTranslationLanguageService); + 'Card 1', + 'Content html', + 'Interaction text', + null, + null, + 'content_id', + audioTranslationLanguageService + ); spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(false); - spyOn(playerTranscriptService, 'getLastStateName') - .and.returnValue('Start'); + spyOn(playerTranscriptService, 'getLastStateName').and.returnValue( + 'Start' + ); spyOn(playerTranscriptService, 'getLastCard').and.returnValue(lastCard); - spyOn(answerClassificationService, 'getMatchingClassificationResult') - .and.returnValue(answerClassificationResult); + spyOn( + answerClassificationService, + 'getMatchingClassificationResult' + ).and.returnValue(answerClassificationResult); let alertsServiceSpy = spyOn( - alertsService, 'addWarning').and.callThrough(); + alertsService, + 'addWarning' + ).and.callThrough(); spyOn(learnerParamsService, 'getAllParams').and.returnValue({}); - spyOn(explorationEngineService, 'makeParams') - .and.returnValue(null); + spyOn(explorationEngineService, 'makeParams').and.returnValue(null); explorationEngineService.init( - explorationDict, 1, null, true, ['en'], [], initSuccessCb); + explorationDict, + 1, + null, + true, + ['en'], + [], + initSuccessCb + ); explorationEngineService.submitAnswer( - answer, textInputService, submitAnswerSuccessCb); - - expect(alertsServiceSpy) - .toHaveBeenCalledWith('Parameters should not be empty.'); + answer, + textInputService, + submitAnswerSuccessCb + ); + + expect(alertsServiceSpy).toHaveBeenCalledWith( + 'Parameters should not be empty.' + ); }); - it('should show warning message if the question ' + - 'name is empty', () => { + it('should show warning message if the question ' + 'name is empty', () => { let initSuccessCb = jasmine.createSpy('success'); let submitAnswerSuccessCb = jasmine.createSpy('success'); let answer = 'answer'; @@ -658,37 +799,62 @@ describe('Exploration engine service ', () => { dest_if_really_stuck: 'Mid', feedback: { content_id: 'feedback_1', - html: 'feedback' + html: 'feedback', }, labelled_as_correct: true, param_changes: [], refresher_exploration_id: null, - missing_prerequisite_skill_id: null - }), 1, 0, 'default_outcome'); + missing_prerequisite_skill_id: null, + }), + 1, + 0, + 'default_outcome' + ); let lastCard = StateCard.createNewCard( - 'Card 1', 'Content html', 'Interaction text', null, - null, 'content_id', audioTranslationLanguageService); + 'Card 1', + 'Content html', + 'Interaction text', + null, + null, + 'content_id', + audioTranslationLanguageService + ); spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(false); - spyOn(playerTranscriptService, 'getLastStateName') - .and.returnValue('Start'); + spyOn(playerTranscriptService, 'getLastStateName').and.returnValue( + 'Start' + ); spyOn(playerTranscriptService, 'getLastCard').and.returnValue(lastCard); - spyOn(answerClassificationService, 'getMatchingClassificationResult') - .and.returnValue(answerClassificationResult); - spyOn(explorationEngineService, 'makeQuestion') - .and.returnValue(null); + spyOn( + answerClassificationService, + 'getMatchingClassificationResult' + ).and.returnValue(answerClassificationResult); + spyOn(explorationEngineService, 'makeQuestion').and.returnValue(null); let alertsServiceSpy = spyOn( - alertsService, 'addWarning').and.callThrough(); + alertsService, + 'addWarning' + ).and.callThrough(); explorationEngineService.init( - explorationDict, 1, null, true, ['en'], [], initSuccessCb); + explorationDict, + 1, + null, + true, + ['en'], + [], + initSuccessCb + ); explorationEngineService.submitAnswer( - answer, textInputService, submitAnswerSuccessCb); - - expect(alertsServiceSpy) - .toHaveBeenCalledWith('Question content should not be empty.'); + answer, + textInputService, + submitAnswerSuccessCb + ); + + expect(alertsServiceSpy).toHaveBeenCalledWith( + 'Question content should not be empty.' + ); }); it('should return a different feedback for misspellings', () => { @@ -700,16 +866,19 @@ describe('Exploration engine service ', () => { dest_if_really_stuck: null, feedback: { content_id: 'feedback_1', - html: 'default feedback' + html: 'default feedback', }, labelled_as_correct: false, param_changes: [], refresher_exploration_id: null, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }; let answerClassificationResult = new AnswerClassificationResult( outcomeObjectFactory.createFromBackendDict(defaultOutcomeDict), - 1, 0, 'default_outcome'); + 1, + 0, + 'default_outcome' + ); const lastCardInteraction = interactionObjectFactory.createFromBackendDict({ @@ -722,28 +891,26 @@ describe('Exploration engine service ', () => { labelled_as_correct: true, feedback: { content_id: 'feedback_1', - html: '

Good Job

' + html: '

Good Job

', }, param_changes: [], dest_if_really_stuck: null, - dest: 'Mid' + dest: 'Mid', }, training_data: [], rule_specs: [ { inputs: { x: { - normalizedStrSet: [ - 'answer' - ], - contentId: 'rule_input_2' - } + normalizedStrSet: ['answer'], + contentId: 'rule_input_2', + }, }, - rule_type: 'Equals' - } + rule_type: 'Equals', + }, ], - tagged_skill_misconception_id: null - } + tagged_skill_misconception_id: null, + }, ], default_outcome: defaultOutcomeDict, confirmed_unclassified_answers: [], @@ -756,72 +923,110 @@ describe('Exploration engine service ', () => { }, catch_misspellings: { value: true, - } + }, }, hints: [], - solution: null + solution: null, }); const lastCard = StateCard.createNewCard( - 'Card 1', 'Content html', 'Interaction text', lastCardInteraction, - null, 'content_id', audioTranslationLanguageService); + 'Card 1', + 'Content html', + 'Interaction text', + lastCardInteraction, + null, + 'content_id', + audioTranslationLanguageService + ); spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(false); - spyOn(playerTranscriptService, 'getLastStateName') - .and.returnValue('Start'); + spyOn(playerTranscriptService, 'getLastStateName').and.returnValue( + 'Start' + ); spyOn(playerTranscriptService, 'getLastCard').and.returnValue(lastCard); - spyOn(answerClassificationService, 'getMatchingClassificationResult') - .and.returnValue(answerClassificationResult); - spyOn(translateService, 'instant').and.callFake((key) => { - if ((key as string) - .startsWith('I18N_ANSWER_MISSPELLED_RESPONSE_TEXT')) { + spyOn( + answerClassificationService, + 'getMatchingClassificationResult' + ).and.returnValue(answerClassificationResult); + spyOn(translateService, 'instant').and.callFake(key => { + if ( + (key as string).startsWith('I18N_ANSWER_MISSPELLED_RESPONSE_TEXT') + ) { return 'misspelled feedback'; } }); explorationEngineService.init( - explorationDict, 1, null, true, ['en'], [], initSuccessCb); + explorationDict, + 1, + null, + true, + ['en'], + [], + initSuccessCb + ); explorationEngineService.submitAnswer( - answer, textInputService, submitAnswerSuccessCb); + answer, + textInputService, + submitAnswerSuccessCb + ); expect(submitAnswerSuccessCb).toHaveBeenCalled(); const feedbackArgPosition = 2; - expect(submitAnswerSuccessCb.calls.argsFor(0)[feedbackArgPosition]) - .toBe('misspelled feedback'); + expect(submitAnswerSuccessCb.calls.argsFor(0)[feedbackArgPosition]).toBe( + 'misspelled feedback' + ); // Make outcome non-default, so that misspelling is not checked anymore. answerClassificationResult.outcome.dest = 'End'; - answerClassificationService.getMatchingClassificationResult = - jasmine.createSpy().and.returnValue(answerClassificationResult); + answerClassificationService.getMatchingClassificationResult = jasmine + .createSpy() + .and.returnValue(answerClassificationResult); explorationEngineService.submitAnswer( - answer, textInputService, submitAnswerSuccessCb); + answer, + textInputService, + submitAnswerSuccessCb + ); expect(submitAnswerSuccessCb).toHaveBeenCalledTimes(2); - expect(submitAnswerSuccessCb.calls.argsFor(1)[feedbackArgPosition]) - .toBe('default feedback'); + expect(submitAnswerSuccessCb.calls.argsFor(1)[feedbackArgPosition]).toBe( + 'default feedback' + ); }); }); - it('should check whether we can ask learner for answer ' + - 'details', fakeAsync(() => { - let initSuccessCb = jasmine.createSpy('success'); - - spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(false); - spyOn(explorationFeaturesBackendApiService, 'fetchExplorationFeaturesAsync') - .and.returnValue(Promise.resolve(explorationFeatures)); + it( + 'should check whether we can ask learner for answer ' + 'details', + fakeAsync(() => { + let initSuccessCb = jasmine.createSpy('success'); - // Here default value is set to false. - expect(explorationEngineService.getAlwaysAskLearnerForAnswerDetails()) - .toBe(false); + spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(false); + spyOn( + explorationFeaturesBackendApiService, + 'fetchExplorationFeaturesAsync' + ).and.returnValue(Promise.resolve(explorationFeatures)); - explorationEngineService.init( - explorationDict, 1, null, true, ['en'], [], initSuccessCb); - tick(); + // Here default value is set to false. + expect( + explorationEngineService.getAlwaysAskLearnerForAnswerDetails() + ).toBe(false); - const answerDetails = ( - explorationEngineService.getAlwaysAskLearnerForAnswerDetails()); - expect(answerDetails).toBe(true); - })); + explorationEngineService.init( + explorationDict, + 1, + null, + true, + ['en'], + [], + initSuccessCb + ); + tick(); + + const answerDetails = + explorationEngineService.getAlwaysAskLearnerForAnswerDetails(); + expect(answerDetails).toBe(true); + }) + ); it('should return default exploration id', () => { // Please note that default exploration id is 'test_id'. @@ -831,114 +1036,172 @@ describe('Exploration engine service ', () => { expect(explorationId).toBe('test_id'); }); - it('should return exploration title ' + - 'when calling \'getExplorationTitle\'', () => { - let initSuccessCb = jasmine.createSpy('success'); - - spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(false); - - expect(() => { - explorationEngineService.getExplorationTitle(); - }).toThrowError('Cannot read properties of undefined (reading \'title\')'); - - explorationEngineService.init( - explorationDict, 1, null, true, ['en'], [], initSuccessCb); - - const explorationTitle = explorationEngineService.getExplorationTitle(); - expect(explorationTitle).toBe('My Exploration Title'); - }); - - it('should return exploration version ' + - 'when calling \'getExplorationVersion\'', () => { - let initSuccessCb = jasmine.createSpy('success'); - - spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(false); - - // Here 1 is default value, this is being initialized in the constructor. - expect(explorationEngineService.getExplorationVersion()).toBe(1); + it( + 'should return exploration title ' + "when calling 'getExplorationTitle'", + () => { + let initSuccessCb = jasmine.createSpy('success'); - explorationEngineService.init( - explorationDict, 2, null, true, ['en'], [], initSuccessCb); + spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(false); - const explorationVersion = explorationEngineService.getExplorationVersion(); - expect(explorationVersion).toBe(2); - }); + expect(() => { + explorationEngineService.getExplorationTitle(); + }).toThrowError("Cannot read properties of undefined (reading 'title')"); - it('should return author recommended exploration id\'s ' + - 'when calling \'getAuthorRecommendedExpIdsByStateName\'', () => { - let initSuccessCb = jasmine.createSpy('success'); + explorationEngineService.init( + explorationDict, + 1, + null, + true, + ['en'], + [], + initSuccessCb + ); + + const explorationTitle = explorationEngineService.getExplorationTitle(); + expect(explorationTitle).toBe('My Exploration Title'); + } + ); + + it( + 'should return exploration version ' + + "when calling 'getExplorationVersion'", + () => { + let initSuccessCb = jasmine.createSpy('success'); - spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(false); + spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(false); - expect(() => { - explorationEngineService.getAuthorRecommendedExpIdsByStateName('Start'); - }).toThrowError( - 'Cannot read properties of undefined ' + - '(reading \'getAuthorRecommendedExpIds\')'); + // Here 1 is default value, this is being initialized in the constructor. + expect(explorationEngineService.getExplorationVersion()).toBe(1); - explorationEngineService.init( - explorationDict, 1, null, true, ['en'], [], initSuccessCb); + explorationEngineService.init( + explorationDict, + 2, + null, + true, + ['en'], + [], + initSuccessCb + ); + + const explorationVersion = + explorationEngineService.getExplorationVersion(); + expect(explorationVersion).toBe(2); + } + ); + + it( + "should return author recommended exploration id's " + + "when calling 'getAuthorRecommendedExpIdsByStateName'", + () => { + let initSuccessCb = jasmine.createSpy('success'); - expect(() => { - explorationEngineService.getAuthorRecommendedExpIdsByStateName('Start'); - }).toThrowError( - 'Tried to get recommendations for a non-terminal state: Start'); + spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(false); - // Please note that in order to get author recommended exploration id's - // current should be the last state. - const recommendedId = explorationEngineService - .getAuthorRecommendedExpIdsByStateName('End'); - expect(recommendedId).toContain('recommnendedExplorationId'); - }); + expect(() => { + explorationEngineService.getAuthorRecommendedExpIdsByStateName('Start'); + }).toThrowError( + 'Cannot read properties of undefined ' + + "(reading 'getAuthorRecommendedExpIds')" + ); - it('should update current state when an answer is submitted ' + - 'and a new card is recorded', () => { - let initSuccessCb = jasmine.createSpy('success'); - let submitAnswerSuccessCb = jasmine.createSpy('success'); - let answer = 'answer'; - let answerClassificationResult = new AnswerClassificationResult( - outcomeObjectFactory.createFromBackendDict({ - dest: 'Mid', - dest_if_really_stuck: 'Mid', - feedback: { - content_id: 'feedback_1', - html: 'Answer is correct!' - }, - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null - }), 1, 0, 'default_outcome'); + explorationEngineService.init( + explorationDict, + 1, + null, + true, + ['en'], + [], + initSuccessCb + ); + + expect(() => { + explorationEngineService.getAuthorRecommendedExpIdsByStateName('Start'); + }).toThrowError( + 'Tried to get recommendations for a non-terminal state: Start' + ); + + // Please note that in order to get author recommended exploration id's + // current should be the last state. + const recommendedId = + explorationEngineService.getAuthorRecommendedExpIdsByStateName('End'); + expect(recommendedId).toContain('recommnendedExplorationId'); + } + ); + + it( + 'should update current state when an answer is submitted ' + + 'and a new card is recorded', + () => { + let initSuccessCb = jasmine.createSpy('success'); + let submitAnswerSuccessCb = jasmine.createSpy('success'); + let answer = 'answer'; + let answerClassificationResult = new AnswerClassificationResult( + outcomeObjectFactory.createFromBackendDict({ + dest: 'Mid', + dest_if_really_stuck: 'Mid', + feedback: { + content_id: 'feedback_1', + html: 'Answer is correct!', + }, + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + }), + 1, + 0, + 'default_outcome' + ); - let lastCard = StateCard.createNewCard( - 'Card 1', 'Content html', 'Interaction text', null, - null, 'content_id', audioTranslationLanguageService); + let lastCard = StateCard.createNewCard( + 'Card 1', + 'Content html', + 'Interaction text', + null, + null, + 'content_id', + audioTranslationLanguageService + ); - spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(false); - spyOn(playerTranscriptService, 'getLastStateName') - .and.returnValue('Start'); - spyOn(playerTranscriptService, 'getLastCard').and.returnValue(lastCard); - spyOn(answerClassificationService, 'getMatchingClassificationResult') - .and.returnValue(answerClassificationResult); + spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(false); + spyOn(playerTranscriptService, 'getLastStateName').and.returnValue( + 'Start' + ); + spyOn(playerTranscriptService, 'getLastCard').and.returnValue(lastCard); + spyOn( + answerClassificationService, + 'getMatchingClassificationResult' + ).and.returnValue(answerClassificationResult); - expect(explorationEngineService.currentStateName).toBe(undefined); + expect(explorationEngineService.currentStateName).toBe(undefined); - explorationEngineService.init( - explorationDict, 1, null, true, ['en'], [], initSuccessCb); + explorationEngineService.init( + explorationDict, + 1, + null, + true, + ['en'], + [], + initSuccessCb + ); - explorationEngineService.submitAnswer( - answer, textInputService, submitAnswerSuccessCb); - expect(explorationEngineService.currentStateName).toBe('Start'); - explorationEngineService.recordNewCardAdded(); - expect(explorationEngineService.currentStateName).toBe('Mid'); - }); + explorationEngineService.submitAnswer( + answer, + textInputService, + submitAnswerSuccessCb + ); + expect(explorationEngineService.currentStateName).toBe('Start'); + explorationEngineService.recordNewCardAdded(); + expect(explorationEngineService.currentStateName).toBe('Mid'); + } + ); it('should load initial state when moved to new exploration', () => { let moveToExplorationCb = jasmine.createSpy('success'); spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(false); - explorationEngineService.exploration = explorationObjectFactory - .createFromBackendDict(explorationDict); + explorationEngineService.exploration = + explorationObjectFactory.createFromBackendDict(explorationDict); let currentStateName = explorationEngineService.currentStateName; expect(currentStateName).toBe(undefined); @@ -951,35 +1214,52 @@ describe('Exploration engine service ', () => { expect(currentStateName).toBe(initalState); }); - it('should return true if current state is initial state ' + - 'when calling \'isCurrentStateInitial\'', () => { - let initSuccessCb = jasmine.createSpy('success'); - - spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(false); - - expect(() => { - explorationEngineService.isCurrentStateInitial(); - }).toThrowError( - 'Cannot read properties of undefined (reading \'initStateName\')'); + it( + 'should return true if current state is initial state ' + + "when calling 'isCurrentStateInitial'", + () => { + let initSuccessCb = jasmine.createSpy('success'); - explorationEngineService.init( - explorationDict, 1, null, true, ['en'], [], initSuccessCb); + spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(false); - expect(explorationEngineService.isCurrentStateInitial()).toBe(true); - }); + expect(() => { + explorationEngineService.isCurrentStateInitial(); + }).toThrowError( + "Cannot read properties of undefined (reading 'initStateName')" + ); - it('should return current state when calling \'getState\'', () => { + explorationEngineService.init( + explorationDict, + 1, + null, + true, + ['en'], + [], + initSuccessCb + ); + + expect(explorationEngineService.isCurrentStateInitial()).toBe(true); + } + ); + + it("should return current state when calling 'getState'", () => { let initSuccessCb = jasmine.createSpy('success'); spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(false); let lastStateNameSpy = spyOn(playerTranscriptService, 'getLastStateName'); expect(() => { explorationEngineService.getState(); - }).toThrowError( - 'Cannot read properties of undefined (reading \'getState\')'); + }).toThrowError("Cannot read properties of undefined (reading 'getState')"); explorationEngineService.init( - explorationDict, 1, null, true, ['en'], [], initSuccessCb); + explorationDict, + 1, + null, + true, + ['en'], + [], + initSuccessCb + ); // Check for first state. lastStateNameSpy.and.returnValue('Start'); @@ -1002,60 +1282,86 @@ describe('Exploration engine service ', () => { expect(currentState.name).toBe('End'); }); - it('should return language code when calling \'getLanguageCode\'', () => { + it("should return language code when calling 'getLanguageCode'", () => { let initSuccessCb = jasmine.createSpy('success'); spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(false); expect(() => { explorationEngineService.getLanguageCode(); }).toThrowError( - 'Cannot read properties of undefined (reading \'getLanguageCode\')'); + "Cannot read properties of undefined (reading 'getLanguageCode')" + ); // First exploration has language code 'en'. explorationEngineService.init( - explorationDict, 1, null, true, ['en'], [], initSuccessCb); + explorationDict, + 1, + null, + true, + ['en'], + [], + initSuccessCb + ); expect(explorationEngineService.getLanguageCode()).toBe('en'); // Setting next exploration language code to 'bn'. explorationDict.language_code = 'bn'; explorationEngineService.init( - explorationDict, 1, null, true, ['en'], [], initSuccessCb); + explorationDict, + 1, + null, + true, + ['en'], + [], + initSuccessCb + ); expect(explorationEngineService.getLanguageCode()).toBe('bn'); }); it('should get the update active state event emitter', () => { let mockEventEmitter = new EventEmitter(); - expect(explorationEngineService.onUpdateActiveStateIfInEditor) - .toEqual(mockEventEmitter); - }); - - it('should throw error if we populate exploration data ' + - 'in exploration player page', () => { - // Please note that 'initSettingsFromEditor' function is strictly - // used for the exploration editor page before initialization. - // This method should not be called from the exploration player page. - let paramChanges = paramChangeObjectFactory.createFromBackendDict( - paramChangeDict); - - // Checking if we are currently in exploration editor preview mode. - expect(explorationEngineService.isInPreviewMode()).toBe(false); - expect(() => { - explorationEngineService.initSettingsFromEditor('Start', [paramChanges]); - }).toThrowError('Cannot populate exploration in learner mode.'); + expect(explorationEngineService.onUpdateActiveStateIfInEditor).toEqual( + mockEventEmitter + ); }); - it('should return state when calling \'getStateFromStateName\'', () => { + it( + 'should throw error if we populate exploration data ' + + 'in exploration player page', + () => { + // Please note that 'initSettingsFromEditor' function is strictly + // used for the exploration editor page before initialization. + // This method should not be called from the exploration player page. + let paramChanges = + paramChangeObjectFactory.createFromBackendDict(paramChangeDict); + + // Checking if we are currently in exploration editor preview mode. + expect(explorationEngineService.isInPreviewMode()).toBe(false); + expect(() => { + explorationEngineService.initSettingsFromEditor('Start', [ + paramChanges, + ]); + }).toThrowError('Cannot populate exploration in learner mode.'); + } + ); + + it("should return state when calling 'getStateFromStateName'", () => { let initSuccessCb = jasmine.createSpy('success'); spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(false); expect(() => { explorationEngineService.getStateFromStateName('Start'); - }).toThrowError( - 'Cannot read properties of undefined (reading \'getState\')' - ); + }).toThrowError("Cannot read properties of undefined (reading 'getState')"); explorationEngineService.init( - explorationDict, 1, null, true, ['en'], [], initSuccessCb); + explorationDict, + 1, + null, + true, + ['en'], + [], + initSuccessCb + ); // Check for first state. let state = explorationEngineService.getStateFromStateName('Start'); @@ -1068,18 +1374,25 @@ describe('Exploration engine service ', () => { expect(state.name).toBe('Mid'); }); - it('should return state card when calling \'getStateCardByName\'', () => { + it("should return state card when calling 'getStateCardByName'", () => { let initSuccessCb = jasmine.createSpy('success'); spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(false); expect(() => { explorationEngineService.getStateCardByName('Start'); }).toThrowError( - 'Cannot read properties of undefined (reading \'getInteraction\')' + "Cannot read properties of undefined (reading 'getInteraction')" ); explorationEngineService.init( - explorationDict, 1, null, true, ['en'], [], initSuccessCb); + explorationDict, + 1, + null, + true, + ['en'], + [], + initSuccessCb + ); // Check for first state. let stateCard = explorationEngineService.getStateCardByName('Start'); @@ -1092,20 +1405,32 @@ describe('Exploration engine service ', () => { expect(stateCard.getStateName()).toBe('Mid'); }); - it('should return shortest path to state when calling ' + - '\'getShortestPathToState\'', () => { - let initSuccessCb = jasmine.createSpy('success'); - spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(false); - - explorationEngineService.init( - explorationDict, 1, null, true, ['en'], [], initSuccessCb); - - // Check for first state. - let shortestPathToState = explorationEngineService.getShortestPathToState( - explorationDict.states, 'Mid'); + it( + 'should return shortest path to state when calling ' + + "'getShortestPathToState'", + () => { + let initSuccessCb = jasmine.createSpy('success'); + spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(false); - expect(shortestPathToState).toEqual(['Start', 'Mid']); - }); + explorationEngineService.init( + explorationDict, + 1, + null, + true, + ['en'], + [], + initSuccessCb + ); + + // Check for first state. + let shortestPathToState = explorationEngineService.getShortestPathToState( + explorationDict.states, + 'Mid' + ); + + expect(shortestPathToState).toEqual(['Start', 'Mid']); + } + ); describe('on validating parameters ', () => { it('should create new parameters successfully', () => { @@ -1114,48 +1439,60 @@ describe('Exploration engine service ', () => { let oldParams = { guess: '-1', - answer: 'val' + answer: 'val', }; let expectedParams = { guess: '-1', - answer: 'val1, val2' + answer: 'val1, val2', }; - let paramChanges = paramChangeObjectFactory.createFromBackendDict( - paramChangeDict); + let paramChanges = + paramChangeObjectFactory.createFromBackendDict(paramChangeDict); const newParams = explorationEngineService.makeParams( - oldParams, [paramChanges], []); + oldParams, + [paramChanges], + [] + ); expect(newParams).toEqual(expectedParams); }); - it('should not create new parameters if paramater ' + - 'values are empty', () => { - paramChangeDict.customization_args.parse_with_jinja = true; - let oldParams = {}; - - let paramChanges = paramChangeObjectFactory.createFromBackendDict( - paramChangeDict); - spyOn(expressionInterpolationService, 'processUnicode') - .and.returnValue(null); - - const newParams = explorationEngineService.makeParams( - oldParams, [paramChanges], []); - - expect(newParams).toBe(null); - }); + it( + 'should not create new parameters if paramater ' + 'values are empty', + () => { + paramChangeDict.customization_args.parse_with_jinja = true; + let oldParams = {}; + + let paramChanges = + paramChangeObjectFactory.createFromBackendDict(paramChangeDict); + spyOn(expressionInterpolationService, 'processUnicode').and.returnValue( + null + ); + + const newParams = explorationEngineService.makeParams( + oldParams, + [paramChanges], + [] + ); + + expect(newParams).toBe(null); + } + ); it('should return old parameters', () => { paramChangeDict.customization_args.parse_with_jinja = true; let oldParams = { guess: '-1', - answer: 'val' + answer: 'val', }; - let paramChanges = paramChangeObjectFactory.createFromBackendDict( - paramChangeDict); - const newParams = explorationEngineService - .makeParams(oldParams, [paramChanges], []); + let paramChanges = + paramChangeObjectFactory.createFromBackendDict(paramChangeDict); + const newParams = explorationEngineService.makeParams( + oldParams, + [paramChanges], + [] + ); expect(newParams).toEqual(oldParams); }); diff --git a/core/templates/pages/exploration-player-page/services/exploration-engine.service.ts b/core/templates/pages/exploration-player-page/services/exploration-engine.service.ts index d838657368f8..28495de5eb23 100644 --- a/core/templates/pages/exploration-player-page/services/exploration-engine.service.ts +++ b/core/templates/pages/exploration-player-page/services/exploration-engine.service.ts @@ -16,50 +16,62 @@ * @fileoverview Utility service for the learner's view of an exploration. */ -import { EventEmitter, Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { TranslateService } from '@ngx-translate/core'; -import { AppConstants } from 'app.constants'; -import { AnswerClassificationResult } from 'domain/classifier/answer-classification-result.model'; -import { Exploration, ExplorationBackendDict, ExplorationObjectFactory } from 'domain/exploration/ExplorationObjectFactory'; -import { Interaction } from 'domain/exploration/InteractionObjectFactory'; -import { ParamChange } from 'domain/exploration/ParamChangeObjectFactory'; -import { ReadOnlyExplorationBackendApiService } from 'domain/exploration/read-only-exploration-backend-api.service'; -import { BindableVoiceovers, RecordedVoiceovers } from 'domain/exploration/recorded-voiceovers.model'; -import { Outcome } from 'domain/exploration/OutcomeObjectFactory'; -import { StateObjectsBackendDict } from 'domain/exploration/StatesObjectFactory'; -import { State } from 'domain/state/StateObjectFactory'; -import { StateCard } from 'domain/state_card/state-card.model'; -import { ExpressionInterpolationService } from 'expressions/expression-interpolation.service'; -import { TextInputCustomizationArgs } from 'interactions/customization-args-defs'; -import { AlertsService } from 'services/alerts.service'; -import { ContextService } from 'services/context.service'; -import { UrlService } from 'services/contextual/url.service'; -import { EntityTranslationsService } from 'services/entity-translations.services'; -import { ExplorationFeaturesBackendApiService } from 'services/exploration-features-backend-api.service'; -import { ExplorationHtmlFormatterService } from 'services/exploration-html-formatter.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { AnswerClassificationService, InteractionRulesService } from './answer-classification.service'; -import { AudioPreloaderService } from './audio-preloader.service'; -import { AudioTranslationLanguageService } from './audio-translation-language.service'; -import { ContentTranslationLanguageService } from './content-translation-language.service'; -import { ContentTranslationManagerService } from './content-translation-manager.service'; -import { ImagePreloaderService } from './image-preloader.service'; -import { ExplorationParams, LearnerParamsService } from './learner-params.service'; -import { PlayerTranscriptService } from './player-transcript.service'; -import { StatsReportingService } from './stats-reporting.service'; -import { ExplorationPlayerConstants } from '../exploration-player-page.constants'; - - @Injectable({ - providedIn: 'root' - }) +import {EventEmitter, Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {TranslateService} from '@ngx-translate/core'; +import {AppConstants} from 'app.constants'; +import {AnswerClassificationResult} from 'domain/classifier/answer-classification-result.model'; +import { + Exploration, + ExplorationBackendDict, + ExplorationObjectFactory, +} from 'domain/exploration/ExplorationObjectFactory'; +import {Interaction} from 'domain/exploration/InteractionObjectFactory'; +import {ParamChange} from 'domain/exploration/ParamChangeObjectFactory'; +import {ReadOnlyExplorationBackendApiService} from 'domain/exploration/read-only-exploration-backend-api.service'; +import { + BindableVoiceovers, + RecordedVoiceovers, +} from 'domain/exploration/recorded-voiceovers.model'; +import {Outcome} from 'domain/exploration/OutcomeObjectFactory'; +import {StateObjectsBackendDict} from 'domain/exploration/StatesObjectFactory'; +import {State} from 'domain/state/StateObjectFactory'; +import {StateCard} from 'domain/state_card/state-card.model'; +import {ExpressionInterpolationService} from 'expressions/expression-interpolation.service'; +import {TextInputCustomizationArgs} from 'interactions/customization-args-defs'; +import {AlertsService} from 'services/alerts.service'; +import {ContextService} from 'services/context.service'; +import {UrlService} from 'services/contextual/url.service'; +import {EntityTranslationsService} from 'services/entity-translations.services'; +import {ExplorationFeaturesBackendApiService} from 'services/exploration-features-backend-api.service'; +import {ExplorationHtmlFormatterService} from 'services/exploration-html-formatter.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import { + AnswerClassificationService, + InteractionRulesService, +} from './answer-classification.service'; +import {AudioPreloaderService} from './audio-preloader.service'; +import {AudioTranslationLanguageService} from './audio-translation-language.service'; +import {ContentTranslationLanguageService} from './content-translation-language.service'; +import {ContentTranslationManagerService} from './content-translation-manager.service'; +import {ImagePreloaderService} from './image-preloader.service'; +import { + ExplorationParams, + LearnerParamsService, +} from './learner-params.service'; +import {PlayerTranscriptService} from './player-transcript.service'; +import {StatsReportingService} from './stats-reporting.service'; +import {ExplorationPlayerConstants} from '../exploration-player-page.constants'; + +@Injectable({ + providedIn: 'root', +}) export class ExplorationEngineService { private _explorationId: string; private _editorPreviewMode: boolean; private _questionPlayerMode: boolean; - private _updateActiveStateIfInEditorEventEmitter: EventEmitter = ( - new EventEmitter() - ); + private _updateActiveStateIfInEditorEventEmitter: EventEmitter = + new EventEmitter(); answerIsBeingProcessed: boolean = false; alwaysAskLearnersForAnswerDetails: boolean = false; @@ -82,13 +94,11 @@ export class ExplorationEngineService { private answerClassificationService: AnswerClassificationService, private audioPreloaderService: AudioPreloaderService, private audioTranslationLanguageService: AudioTranslationLanguageService, - private contentTranslationLanguageService: - ContentTranslationLanguageService, + private contentTranslationLanguageService: ContentTranslationLanguageService, private contextService: ContextService, private contentTranslationManagerService: ContentTranslationManagerService, private entityTranslationsService: EntityTranslationsService, - private explorationFeaturesBackendApiService: - ExplorationFeaturesBackendApiService, + private explorationFeaturesBackendApiService: ExplorationFeaturesBackendApiService, private explorationHtmlFormatterService: ExplorationHtmlFormatterService, private explorationObjectFactory: ExplorationObjectFactory, private expressionInterpolationService: ExpressionInterpolationService, @@ -96,8 +106,7 @@ export class ExplorationEngineService { private imagePreloaderService: ImagePreloaderService, private learnerParamsService: LearnerParamsService, private playerTranscriptService: PlayerTranscriptService, - private readOnlyExplorationBackendApiService: - ReadOnlyExplorationBackendApiService, + private readOnlyExplorationBackendApiService: ReadOnlyExplorationBackendApiService, private statsReportingService: StatsReportingService, private translateService: TranslateService, private urlService: UrlService @@ -136,7 +145,7 @@ export class ExplorationEngineService { ) { this.readOnlyExplorationBackendApiService .loadExplorationAsync(this._explorationId, this.version) - .then((exploration) => { + .then(exploration => { this.version = exploration.version; }); } @@ -153,12 +162,14 @@ export class ExplorationEngineService { } private _getFeedback( - answer: string, oldStateCard: StateCard, - outcome: Outcome, envs: Record[] + answer: string, + oldStateCard: StateCard, + outcome: Outcome, + envs: Record[] ): string { const oldInteractionId = oldStateCard.getInteractionId(); - const oldInteractionArgs = oldStateCard. - getInteractionCustomizationArgs() as TextInputCustomizationArgs; + const oldInteractionArgs = + oldStateCard.getInteractionCustomizationArgs() as TextInputCustomizationArgs; const defaultOutcome = oldStateCard.getInteraction()?.defaultOutcome; const shouldCheckForMisspelling = oldInteractionId === AppConstants.INTERACTION_NAMES.TEXT_INPUT && @@ -166,17 +177,23 @@ export class ExplorationEngineService { angular.equals(outcome, defaultOutcome); if (shouldCheckForMisspelling) { - const answerIsOnlyMisspelled = this.answerClassificationService. - isAnswerOnlyMisspelled(oldStateCard.getInteraction(), answer); + const answerIsOnlyMisspelled = + this.answerClassificationService.isAnswerOnlyMisspelled( + oldStateCard.getInteraction(), + answer + ); if (answerIsOnlyMisspelled) { const randomResponse = this.randomFromArray( - ExplorationPlayerConstants.I18N_ANSWER_MISSPELLED_RESPONSE_TEXT_IDS); + ExplorationPlayerConstants.I18N_ANSWER_MISSPELLED_RESPONSE_TEXT_IDS + ); return this.translateService.instant(randomResponse); } } return this.expressionInterpolationService.processHtml( - outcome.feedback.html, envs); + outcome.feedback.html, + envs + ); } private _getRandomSuffix(): string { @@ -196,31 +213,36 @@ export class ExplorationEngineService { // Evaluate parameters. Returns null if any evaluation fails. makeParams( - oldParams: ExplorationParams, - paramChanges: ParamChange[], - envs: Record[] + oldParams: ExplorationParams, + paramChanges: ParamChange[], + envs: Record[] ): ExplorationParams { - let newParams: ExplorationParams = { ...oldParams }; - if (paramChanges.every((pc) => { - if (pc.generatorId === 'Copier') { - if (!pc.customizationArgs.parse_with_jinja) { - newParams[pc.name] = pc.customizationArgs.value; - } else { - let paramValue: string = ( - this.expressionInterpolationService.processUnicode( - pc.customizationArgs.value, [newParams].concat(envs))); - if (paramValue === null) { - return false; + let newParams: ExplorationParams = {...oldParams}; + if ( + paramChanges.every(pc => { + if (pc.generatorId === 'Copier') { + if (!pc.customizationArgs.parse_with_jinja) { + newParams[pc.name] = pc.customizationArgs.value; + } else { + let paramValue: string = + this.expressionInterpolationService.processUnicode( + pc.customizationArgs.value, + [newParams].concat(envs) + ); + if (paramValue === null) { + return false; + } + newParams[pc.name] = paramValue; } - newParams[pc.name] = paramValue; + } else { + // RandomSelector. + newParams[pc.name] = this.randomFromArray( + pc.customizationArgs.list_of_values + ); } - } else { - // RandomSelector. - newParams[pc.name] = this.randomFromArray( - pc.customizationArgs.list_of_values); - } - return true; - })) { + return true; + }) + ) { // All parameters were evaluated successfully. return newParams; } @@ -231,17 +253,22 @@ export class ExplorationEngineService { // Evaluate question string. makeQuestion(newState: State, envs: Record[]): string { return this.expressionInterpolationService.processHtml( - newState.content.html, envs); + newState.content.html, + envs + ); } // This should only be called when 'exploration' is non-null. _loadInitialState( - successCallback: (stateCard: StateCard, str: string) => void + successCallback: (stateCard: StateCard, str: string) => void ): void { let initialState: State = this.exploration.getInitialState(); let oldParams: ExplorationParams = this.learnerParamsService.getAllParams(); let newParams: ExplorationParams = this.makeParams( - oldParams, initialState.paramChanges, [oldParams]); + oldParams, + initialState.paramChanges, + [oldParams] + ); if (newParams === null) { this.alertsService.addWarning('Expression parsing error.'); return; @@ -253,7 +280,8 @@ export class ExplorationEngineService { this.nextStateName = this.exploration.initStateName; let interaction: Interaction = this.exploration.getInteraction( - this.exploration.initStateName); + this.exploration.initStateName + ); let nextFocusLabel: string = this.focusManagerService.generateFocusLabel(); let interactionId = interaction.id; @@ -277,13 +305,20 @@ export class ExplorationEngineService { if (!this._editorPreviewMode) { this.statsReportingService.recordExplorationStarted( - this.exploration.initStateName, newParams); + this.exploration.initStateName, + newParams + ); } let initialCard = StateCard.createNewCard( - this.currentStateName, questionHtml, interactionHtml, - interaction, initialState.recordedVoiceovers, - initialState.content.contentId, this.audioTranslationLanguageService); + this.currentStateName, + questionHtml, + interactionHtml, + interaction, + initialState.recordedVoiceovers, + initialState.content.contentId, + this.audioTranslationLanguageService + ); successCallback(initialCard, nextFocusLabel); } @@ -299,39 +334,42 @@ export class ExplorationEngineService { let startingParams = this.makeParams( baseParams, this.exploration.paramChanges.concat(manualParamChanges), - [baseParams]); + [baseParams] + ); this.learnerParamsService.init(startingParams); } private _getInteractionHtmlByStateName( - labelForFocusTarget: string, stateName: string + labelForFocusTarget: string, + stateName: string ): string { - let interactionId: string = this.exploration.getInteractionId( - stateName); + let interactionId: string = this.exploration.getInteractionId(stateName); return this.explorationHtmlFormatterService.getInteractionHtml( interactionId, this.exploration.getInteractionCustomizationArgs(stateName), true, - labelForFocusTarget, null); + labelForFocusTarget, + null + ); } checkAlwaysAskLearnersForAnswerDetails(): void { - this.explorationFeaturesBackendApiService.fetchExplorationFeaturesAsync( - this._explorationId - ).then((featuresData) => { - this.alwaysAskLearnersForAnswerDetails = ( - featuresData.alwaysAskLearnersForAnswerDetails); - }); + this.explorationFeaturesBackendApiService + .fetchExplorationFeaturesAsync(this._explorationId) + .then(featuresData => { + this.alwaysAskLearnersForAnswerDetails = + featuresData.alwaysAskLearnersForAnswerDetails; + }); } // This should only be used in editor preview mode. It sets the // exploration data from what's currently specified in the editor, and // also initializes the parameters to empty strings. initSettingsFromEditor( - activeStateNameFromPreviewTab: string, - manualParamChangesToInit: ParamChange[] + activeStateNameFromPreviewTab: string, + manualParamChangesToInit: ParamChange[] ): void { if (this._editorPreviewMode) { this.manualParamChanges = manualParamChangesToInit; @@ -342,30 +380,30 @@ export class ExplorationEngineService { } /** - * Initializes an exploration, passing the data for the first state to - * successCallback. - * - * In editor preview mode, populateExploration() must be called before - * calling init(). - * - * @param {function} successCallback - The function to execute after the - * initial exploration data is successfully loaded. This function will - * be passed two arguments: - * - stateName {string}, the name of the first state - * - initHtml {string}, an HTML string representing the content of the - * first state. - */ + * Initializes an exploration, passing the data for the first state to + * successCallback. + * + * In editor preview mode, populateExploration() must be called before + * calling init(). + * + * @param {function} successCallback - The function to execute after the + * initial exploration data is successfully loaded. This function will + * be passed two arguments: + * - stateName {string}, the name of the first state + * - initHtml {string}, an HTML string representing the content of the + * first state. + */ init( - explorationDict: ExplorationBackendDict, - explorationVersion: number, - preferredAudioLanguage: string | null, - autoTtsEnabled: boolean, - preferredContentLanguageCodes: string[], - displayableLanguageCodes: string[], - successCallback: (stateCard: StateCard, label: string) => void + explorationDict: ExplorationBackendDict, + explorationVersion: number, + preferredAudioLanguage: string | null, + autoTtsEnabled: boolean, + preferredContentLanguageCodes: string[], + displayableLanguageCodes: string[], + successCallback: (stateCard: StateCard, label: string) => void ): void { - this.exploration = this.explorationObjectFactory.createFromBackendDict( - explorationDict); + this.exploration = + this.explorationObjectFactory.createFromBackendDict(explorationDict); this.answerIsBeingProcessed = false; if (this._editorPreviewMode) { this.exploration.setInitialStateName(this.initStateName); @@ -375,7 +413,8 @@ export class ExplorationEngineService { this.exploration.getAllVoiceoverLanguageCodes(), null, this.exploration.getLanguageCode(), - explorationDict.auto_tts_enabled); + explorationDict.auto_tts_enabled + ); this.audioPreloaderService.init(this.exploration); this.audioPreloaderService.kickOffAudioPreloader(this.initStateName); this._loadInitialState(successCallback); @@ -387,21 +426,28 @@ export class ExplorationEngineService { this.exploration.getAllVoiceoverLanguageCodes(), preferredAudioLanguage, this.exploration.getLanguageCode(), - autoTtsEnabled); + autoTtsEnabled + ); this.audioPreloaderService.init(this.exploration); this.audioPreloaderService.kickOffAudioPreloader( - this.exploration.getInitialState().name); + this.exploration.getInitialState().name + ); this.imagePreloaderService.init(this.exploration); this.imagePreloaderService.kickOffImagePreloader( - this.exploration.getInitialState().name); + this.exploration.getInitialState().name + ); this.checkAlwaysAskLearnersForAnswerDetails(); this._loadInitialState(successCallback); } this.entityTranslationsService.init( - this._explorationId, 'exploration', this.version); + this._explorationId, + 'exploration', + this.version + ); this.contentTranslationManagerService.setOriginalTranscript( - this.exploration.getLanguageCode()); + this.exploration.getLanguageCode() + ); this.contentTranslationLanguageService.init( displayableLanguageCodes, @@ -456,22 +502,23 @@ export class ExplorationEngineService { } submitAnswer( - answer: string, interactionRulesService: InteractionRulesService, - successCallback: ( - nextCard: StateCard, - refreshInteraction: boolean, - feedbackHtml: string, - feedbackAudioTranslations: BindableVoiceovers, - refresherExplorationId: string, - missingPrerequisiteSkillId: string, - remainOnCurrentCard: boolean, - taggedSkillMisconceptionId: string, - wasOldStateInitial: boolean, - isFirstHit: boolean, - isFinalQuestion: boolean, - nextCardIfReallyStuck: StateCard | null, - focusLabel: string - ) => void + answer: string, + interactionRulesService: InteractionRulesService, + successCallback: ( + nextCard: StateCard, + refreshInteraction: boolean, + feedbackHtml: string, + feedbackAudioTranslations: BindableVoiceovers, + refresherExplorationId: string, + missingPrerequisiteSkillId: string, + remainOnCurrentCard: boolean, + taggedSkillMisconceptionId: string, + wasOldStateInitial: boolean, + isFirstHit: boolean, + isFinalQuestion: boolean, + nextCardIfReallyStuck: StateCard | null, + focusLabel: string + ) => void ): boolean { if (this.answerIsBeingProcessed) { return; @@ -481,12 +528,15 @@ export class ExplorationEngineService { let oldState: State = this.exploration.getState(oldStateName); let recordedVoiceovers: RecordedVoiceovers = oldState.recordedVoiceovers; let oldStateCard: StateCard = this.playerTranscriptService.getLastCard(); - let classificationResult: AnswerClassificationResult = ( + let classificationResult: AnswerClassificationResult = this.answerClassificationService.getMatchingClassificationResult( - oldStateName, oldStateCard.getInteraction(), answer, - interactionRulesService)); - let answerIsCorrect: boolean = ( - classificationResult.outcome.labelledAsCorrect); + oldStateName, + oldStateCard.getInteraction(), + answer, + interactionRulesService + ); + let answerIsCorrect: boolean = + classificationResult.outcome.labelledAsCorrect; // Use {...} to clone the object // since classificationResult.outcome points @@ -495,9 +545,13 @@ export class ExplorationEngineService { let newStateName: string = outcome.dest; if (!this._editorPreviewMode) { - let feedbackIsUseful: boolean = ( + let feedbackIsUseful: boolean = this.answerClassificationService.isClassifiedExplicitlyOrGoesToNewState( - oldStateName, oldState, answer, interactionRulesService)); + oldStateName, + oldState, + answer, + interactionRulesService + ); this.statsReportingService.recordAnswerSubmitted( oldStateName, this.learnerParamsService.getAllParams(), @@ -507,18 +561,24 @@ export class ExplorationEngineService { classificationResult.answerGroupIndex, classificationResult.ruleIndex, classificationResult.classificationCategorization, - feedbackIsUseful); + feedbackIsUseful + ); this.statsReportingService.recordAnswerSubmitAction( - oldStateName, newStateName, oldState.interaction.id, answer, - outcome.feedback.html); + oldStateName, + newStateName, + oldState.interaction.id, + answer, + outcome.feedback.html + ); } let refresherExplorationId = outcome.refresherExplorationId; let missingPrerequisiteSkillId = outcome.missingPrerequisiteSkillId; let newState = this.exploration.getState(newStateName); let isFirstHit = Boolean( - this.visitedStateNames.indexOf(newStateName) === -1); + this.visitedStateNames.indexOf(newStateName) === -1 + ); if (oldStateName !== newStateName) { this.visitedStateNames.push(newStateName); } @@ -526,27 +586,34 @@ export class ExplorationEngineService { let oldParams: ExplorationParams = this.learnerParamsService.getAllParams(); oldParams.answer = answer; let feedbackHtml: string = this._getFeedback( - answer, oldStateCard, classificationResult.outcome, [oldParams]); + answer, + oldStateCard, + classificationResult.outcome, + [oldParams] + ); let feedbackContentId: string = outcome.feedback.contentId; - let feedbackAudioTranslations: BindableVoiceovers = ( - recordedVoiceovers.getBindableVoiceovers(feedbackContentId)); + let feedbackAudioTranslations: BindableVoiceovers = + recordedVoiceovers.getBindableVoiceovers(feedbackContentId); if (feedbackHtml === null) { this.answerIsBeingProcessed = false; this.alertsService.addWarning('Feedback content should not be empty.'); return; } - let newParams = ( - newState ? this.makeParams( - oldParams, newState.paramChanges, [oldParams]) : oldParams); + let newParams = newState + ? this.makeParams(oldParams, newState.paramChanges, [oldParams]) + : oldParams; if (newParams === null) { this.answerIsBeingProcessed = false; this.alertsService.addWarning('Parameters should not be empty.'); return; } - let questionHtml = this.makeQuestion(newState, [newParams, { - answer: 'answer' - }]); + let questionHtml = this.makeQuestion(newState, [ + newParams, + { + answer: 'answer', + }, + ]); if (questionHtml === null) { this.answerIsBeingProcessed = false; // TODO(#13133): Remove all question related naming conventions. @@ -559,20 +626,20 @@ export class ExplorationEngineService { this.answerIsBeingProcessed = false; - let refreshInteraction = ( + let refreshInteraction = oldStateName !== newStateName || - this.exploration.isInteractionInline(oldStateName) - ); + this.exploration.isInteractionInline(oldStateName); this.nextStateName = newStateName; - let onSameCard: boolean = (oldStateName === newStateName); + let onSameCard: boolean = oldStateName === newStateName; this._updateActiveStateIfInEditorEventEmitter.emit(newStateName); let _nextFocusLabel = this.focusManagerService.generateFocusLabel(); let nextInteractionHtml = null; if (this.exploration.getInteraction(this.nextStateName).id) { - nextInteractionHtml = ( - this._getInteractionHtmlByStateName(_nextFocusLabel, this.nextStateName) + nextInteractionHtml = this._getInteractionHtmlByStateName( + _nextFocusLabel, + this.nextStateName ); } @@ -584,38 +651,59 @@ export class ExplorationEngineService { nextInteractionHtml = nextInteractionHtml + this._getRandomSuffix(); let nextCard = StateCard.createNewCard( - this.nextStateName, questionHtml, nextInteractionHtml, + this.nextStateName, + questionHtml, + nextInteractionHtml, this.exploration.getInteraction(this.nextStateName), this.exploration.getState(this.nextStateName).recordedVoiceovers, this.exploration.getState(this.nextStateName).content.contentId, - this.audioTranslationLanguageService); + this.audioTranslationLanguageService + ); const nextCardIfReallyStuck = this._getNextCardIfReallyStuck( - answer, outcome.destIfReallyStuck, oldParams, _nextFocusLabel); + answer, + outcome.destIfReallyStuck, + oldParams, + _nextFocusLabel + ); successCallback( - nextCard, refreshInteraction, feedbackHtml, - feedbackAudioTranslations, refresherExplorationId, - missingPrerequisiteSkillId, onSameCard, null, - (oldStateName === this.exploration.initStateName), isFirstHit, false, - nextCardIfReallyStuck, _nextFocusLabel); + nextCard, + refreshInteraction, + feedbackHtml, + feedbackAudioTranslations, + refresherExplorationId, + missingPrerequisiteSkillId, + onSameCard, + null, + oldStateName === this.exploration.initStateName, + isFirstHit, + false, + nextCardIfReallyStuck, + _nextFocusLabel + ); return answerIsCorrect; } private _getNextCardIfReallyStuck( - answer: string, newStateNameIfStuck: string | null, - oldParams: ExplorationParams, nextFocusLabel: string): StateCard | null { + answer: string, + newStateNameIfStuck: string | null, + oldParams: ExplorationParams, + nextFocusLabel: string + ): StateCard | null { if (newStateNameIfStuck === null) { return null; } let newStateIfStuck = this.exploration.getState(newStateNameIfStuck); - let newParamsIfStuck = ( - newStateIfStuck ? this.makeParams( - oldParams, newStateIfStuck.paramChanges, [oldParams]) : oldParams); + let newParamsIfStuck = newStateIfStuck + ? this.makeParams(oldParams, newStateIfStuck.paramChanges, [oldParams]) + : oldParams; - let questionHtmlIfStuck = this.makeQuestion( - newStateIfStuck, [newParamsIfStuck, { - answer: 'answer' - }]); + let questionHtmlIfStuck = this.makeQuestion(newStateIfStuck, [ + newParamsIfStuck, + { + answer: 'answer', + }, + ]); newParamsIfStuck.answer = answer; @@ -623,23 +711,25 @@ export class ExplorationEngineService { let nextInteractionIfStuckHtml = null; if (this.exploration.getInteraction(this.nextStateIfStuckName).id) { - nextInteractionIfStuckHtml = ( - this._getInteractionHtmlByStateName( - nextFocusLabel, this.nextStateIfStuckName) + nextInteractionIfStuckHtml = this._getInteractionHtmlByStateName( + nextFocusLabel, + this.nextStateIfStuckName ); } questionHtmlIfStuck = questionHtmlIfStuck + this._getRandomSuffix(); - nextInteractionIfStuckHtml = ( - nextInteractionIfStuckHtml + this._getRandomSuffix()); + nextInteractionIfStuckHtml = + nextInteractionIfStuckHtml + this._getRandomSuffix(); return StateCard.createNewCard( - this.nextStateIfStuckName, questionHtmlIfStuck, + this.nextStateIfStuckName, + questionHtmlIfStuck, nextInteractionIfStuckHtml, this.exploration.getInteraction(this.nextStateIfStuckName), this.exploration.getState(this.nextStateIfStuckName).recordedVoiceovers, this.exploration.getState(this.nextStateIfStuckName).content.contentId, - this.audioTranslationLanguageService); + this.audioTranslationLanguageService + ); } isAnswerBeingProcessed(): boolean { @@ -658,30 +748,32 @@ export class ExplorationEngineService { const _nextFocusLabel = this.focusManagerService.generateFocusLabel(); let interactionHtml = null; if (this.exploration.getInteraction(stateName).id) { - interactionHtml = ( - this._getInteractionHtmlByStateName( - _nextFocusLabel, stateName - ) + interactionHtml = this._getInteractionHtmlByStateName( + _nextFocusLabel, + stateName ); } - let contentHtml = ( + let contentHtml = this.exploration.getState(stateName).content.html + - this._getRandomSuffix() - ); + this._getRandomSuffix(); interactionHtml = interactionHtml + this._getRandomSuffix(); return StateCard.createNewCard( - stateName, contentHtml, interactionHtml, + stateName, + contentHtml, + interactionHtml, this.exploration.getInteraction(stateName), this.exploration.getState(stateName).recordedVoiceovers, this.exploration.getState(stateName).content.contentId, - this.audioTranslationLanguageService); + this.audioTranslationLanguageService + ); } getShortestPathToState( - allStates: StateObjectsBackendDict, destStateName: string + allStates: StateObjectsBackendDict, + destStateName: string ): string[] { - let stateGraphLinks: { source: string; target: string }[] = []; + let stateGraphLinks: {source: string; target: string}[] = []; // Create a list of all possible links between states. for (let stateName of Object.keys(allStates)) { @@ -726,8 +818,10 @@ export class ExplorationEngineService { for (let e = 0; e < stateGraphLinks.length; e++) { let edge = stateGraphLinks[e]; let dest = edge.target; - if (edge.source === currStateName && - !visitedNodes.hasOwnProperty(dest)) { + if ( + edge.source === currStateName && + !visitedNodes.hasOwnProperty(dest) + ) { visitedNodes[dest] = true; nodeToParentMap[dest] = currStateName; pathsQueue.push(dest); @@ -748,5 +842,9 @@ export class ExplorationEngineService { } } -angular.module('oppia').factory('ExplorationEngineService', - downgradeInjectable(ExplorationEngineService)); +angular + .module('oppia') + .factory( + 'ExplorationEngineService', + downgradeInjectable(ExplorationEngineService) + ); diff --git a/core/templates/pages/exploration-player-page/services/exploration-player-state.service.spec.ts b/core/templates/pages/exploration-player-page/services/exploration-player-state.service.spec.ts index c6dac54f72de..2f2c38556aa5 100644 --- a/core/templates/pages/exploration-player-page/services/exploration-player-state.service.spec.ts +++ b/core/templates/pages/exploration-player-page/services/exploration-player-state.service.spec.ts @@ -16,34 +16,40 @@ * @fileoverview Unit tests for ExplorationPlayerStateService. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; -import { MockTranslateService } from 'components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec'; -import { EditableExplorationBackendApiService } - from 'domain/exploration/editable-exploration-backend-api.service'; -import { ExplorationBackendDict } from 'domain/exploration/ExplorationObjectFactory'; -import { FetchExplorationBackendResponse, ReadOnlyExplorationBackendApiService } - from 'domain/exploration/read-only-exploration-backend-api.service'; -import { PretestQuestionBackendApiService } - from 'domain/question/pretest-question-backend-api.service'; -import { QuestionBackendApiService } from 'domain/question/question-backend-api.service'; -import { Question, QuestionBackendDict, QuestionObjectFactory } from 'domain/question/QuestionObjectFactory'; -import { DiagnosticTestTopicTrackerModel } from 'pages/diagnostic-test-player-page/diagnostic-test-topic-tracker.model'; -import { ContextService } from 'services/context.service'; -import { UrlService } from 'services/contextual/url.service'; -import { ExplorationFeatures, ExplorationFeaturesBackendApiService } - from 'services/exploration-features-backend-api.service'; -import { ExplorationFeaturesService } from 'services/exploration-features.service'; -import { PlaythroughService } from 'services/playthrough.service'; -import { ExplorationPlayerConstants } from '../exploration-player-page.constants'; -import { DiagnosticTestPlayerEngineService } from './diagnostic-test-player-engine.service'; -import { ExplorationEngineService } from './exploration-engine.service'; -import { ExplorationPlayerStateService } from './exploration-player-state.service'; -import { NumberAttemptsService } from './number-attempts.service'; -import { PlayerTranscriptService } from './player-transcript.service'; -import { QuestionPlayerEngineService } from './question-player-engine.service'; -import { StatsReportingService } from './stats-reporting.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {fakeAsync, TestBed, tick, waitForAsync} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; +import {MockTranslateService} from 'components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec'; +import {EditableExplorationBackendApiService} from 'domain/exploration/editable-exploration-backend-api.service'; +import {ExplorationBackendDict} from 'domain/exploration/ExplorationObjectFactory'; +import { + FetchExplorationBackendResponse, + ReadOnlyExplorationBackendApiService, +} from 'domain/exploration/read-only-exploration-backend-api.service'; +import {PretestQuestionBackendApiService} from 'domain/question/pretest-question-backend-api.service'; +import {QuestionBackendApiService} from 'domain/question/question-backend-api.service'; +import { + Question, + QuestionBackendDict, + QuestionObjectFactory, +} from 'domain/question/QuestionObjectFactory'; +import {DiagnosticTestTopicTrackerModel} from 'pages/diagnostic-test-player-page/diagnostic-test-topic-tracker.model'; +import {ContextService} from 'services/context.service'; +import {UrlService} from 'services/contextual/url.service'; +import { + ExplorationFeatures, + ExplorationFeaturesBackendApiService, +} from 'services/exploration-features-backend-api.service'; +import {ExplorationFeaturesService} from 'services/exploration-features.service'; +import {PlaythroughService} from 'services/playthrough.service'; +import {ExplorationPlayerConstants} from '../exploration-player-page.constants'; +import {DiagnosticTestPlayerEngineService} from './diagnostic-test-player-engine.service'; +import {ExplorationEngineService} from './exploration-engine.service'; +import {ExplorationPlayerStateService} from './exploration-player-state.service'; +import {NumberAttemptsService} from './number-attempts.service'; +import {PlayerTranscriptService} from './player-transcript.service'; +import {QuestionPlayerEngineService} from './question-player-engine.service'; +import {StatsReportingService} from './stats-reporting.service'; describe('Exploration Player State Service', () => { let explorationPlayerStateService: ExplorationPlayerStateService; @@ -52,15 +58,12 @@ describe('Exploration Player State Service', () => { let playthroughService: PlaythroughService; let explorationEngineService: ExplorationEngineService; let questionPlayerEngineService: QuestionPlayerEngineService; - let editableExplorationBackendApiService: - EditableExplorationBackendApiService; - let explorationFeaturesBackendApiService: - ExplorationFeaturesBackendApiService; + let editableExplorationBackendApiService: EditableExplorationBackendApiService; + let explorationFeaturesBackendApiService: ExplorationFeaturesBackendApiService; let explorationFeaturesService: ExplorationFeaturesService; let numberAttemptsService: NumberAttemptsService; let questionBackendApiService: QuestionBackendApiService; - let pretestQuestionBackendApiService: - PretestQuestionBackendApiService; + let pretestQuestionBackendApiService: PretestQuestionBackendApiService; let questionObjectFactory: QuestionObjectFactory; let urlService: UrlService; let questionObject: Question; @@ -77,7 +80,7 @@ describe('Exploration Player State Service', () => { title: '', language_code: '', objective: '', - next_content_id_index: 0 + next_content_id_index: 0, }, exploration_metadata: { title: '', @@ -92,7 +95,7 @@ describe('Exploration Player State Service', () => { param_specs: {}, param_changes: [], auto_tts_enabled: false, - edits_allowed: true + edits_allowed: true, }, exploration_id: 'test_id', is_logged_in: true, @@ -107,7 +110,7 @@ describe('Exploration Player State Service', () => { furthest_reached_checkpoint_exp_version: 1, furthest_reached_checkpoint_state_name: 'State B', most_recently_reached_checkpoint_state_name: 'State A', - most_recently_reached_checkpoint_exp_version: 1 + most_recently_reached_checkpoint_exp_version: 1, }; let questionBackendDict: QuestionBackendDict = { @@ -118,49 +121,55 @@ describe('Exploration Player State Service', () => { solicit_answer_details: false, content: { content_id: '1', - html: 'Question 1' + html: 'Question 1', }, interaction: { - answer_groups: [{ - outcome: { - dest: 'State 1', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_1', - html: '

Try Again.

' + answer_groups: [ + { + outcome: { + dest: 'State 1', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: '

Try Again.

', + }, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + labelled_as_correct: true, }, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - labelled_as_correct: true, + rule_specs: [ + { + rule_type: 'Equals', + inputs: {x: 0}, + }, + ], + training_data: [], + tagged_skill_misconception_id: null, }, - rule_specs: [{ - rule_type: 'Equals', - inputs: {x: 0} - }], - training_data: [], - tagged_skill_misconception_id: null, - }, - { - outcome: { - dest: 'State 2', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: '

Try Again.

' + { + outcome: { + dest: 'State 2', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: '

Try Again.

', + }, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + labelled_as_correct: true, }, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - labelled_as_correct: true, + rule_specs: [ + { + rule_type: 'Equals', + inputs: {x: 0}, + }, + ], + training_data: [], + tagged_skill_misconception_id: 'misconceptionId', }, - rule_specs: [{ - rule_type: 'Equals', - inputs: {x: 0} - }], - training_data: [], - tagged_skill_misconception_id: 'misconceptionId', - }], + ], default_outcome: { dest: 'dest', dest_if_really_stuck: null, @@ -170,41 +179,41 @@ describe('Exploration Player State Service', () => { param_changes: [], feedback: { content_id: 'feedback_id', - html: '

Dummy Feedback

' - } + html: '

Dummy Feedback

', + }, }, id: 'TextInput', customization_args: { rows: { - value: 1 + value: 1, }, placeholder: { value: { unicode_str: '', - content_id: 'ca_placeholder_0' - } + content_id: 'ca_placeholder_0', + }, }, catchMisspellings: { - value: false - } + value: false, + }, }, confirmed_unclassified_answers: [], hints: [ { hint_content: { content_id: 'hint_1', - html: '

This is a hint.

' - } - } + html: '

This is a hint.

', + }, + }, ], solution: { correct_answer: 'Solution', explanation: { content_id: 'solution', - html: '

This is a solution.

' + html: '

This is a solution.

', }, - answer_is_exclusive: false - } + answer_is_exclusive: false, + }, }, linked_skill_id: null, card_is_checkpoint: true, @@ -214,16 +223,16 @@ describe('Exploration Player State Service', () => { ca_placeholder_0: {}, feedback_id: {}, solution: {}, - hint_1: {} - } - } + hint_1: {}, + }, + }, }, question_state_data_schema_version: 2, language_code: '', next_content_id_index: 4, version: 1, linked_skill_ids: [], - inapplicable_skill_misconception_ids: [] + inapplicable_skill_misconception_ids: [], }; class MockContextService { @@ -241,13 +250,16 @@ describe('Exploration Player State Service', () => { } class MockReadOnlyExplorationBackendApiService { - async loadLatestExplorationAsync(explorationId: string): - Promise { + async loadLatestExplorationAsync( + explorationId: string + ): Promise { return Promise.resolve(returnDict); } - async loadExplorationAsync(explorationId: string, version: number): - Promise { + async loadExplorationAsync( + explorationId: string, + version: number + ): Promise { return Promise.resolve(returnDict); } } @@ -261,72 +273,71 @@ describe('Exploration Player State Service', () => { StatsReportingService, { provide: ContextService, - useClass: MockContextService + useClass: MockContextService, }, UrlService, { provide: ReadOnlyExplorationBackendApiService, - useClass: MockReadOnlyExplorationBackendApiService + useClass: MockReadOnlyExplorationBackendApiService, }, { provide: TranslateService, - useClass: MockTranslateService - } - ] + useClass: MockTranslateService, + }, + ], }).compileComponents(); })); beforeEach(() => { - explorationPlayerStateService = TestBed - .inject(ExplorationPlayerStateService); + explorationPlayerStateService = TestBed.inject( + ExplorationPlayerStateService + ); playerTranscriptService = TestBed.inject(PlayerTranscriptService); - playerTranscriptService = playerTranscriptService as - jasmine.SpyObj; + playerTranscriptService = + playerTranscriptService as jasmine.SpyObj; statsReportingService = TestBed.inject(StatsReportingService); - statsReportingService = statsReportingService as - jasmine.SpyObj; + statsReportingService = + statsReportingService as jasmine.SpyObj; playthroughService = TestBed.inject(PlaythroughService); - playthroughService = playthroughService as - jasmine.SpyObj; + playthroughService = + playthroughService as jasmine.SpyObj; explorationEngineService = TestBed.inject(ExplorationEngineService); - explorationEngineService = explorationEngineService as - jasmine.SpyObj; + explorationEngineService = + explorationEngineService as jasmine.SpyObj; questionPlayerEngineService = TestBed.inject(QuestionPlayerEngineService); - questionPlayerEngineService = questionPlayerEngineService as - jasmine.SpyObj; + questionPlayerEngineService = + questionPlayerEngineService as jasmine.SpyObj; editableExplorationBackendApiService = TestBed.inject( - EditableExplorationBackendApiService); - editableExplorationBackendApiService = ( - editableExplorationBackendApiService) as - jasmine.SpyObj; + EditableExplorationBackendApiService + ); + editableExplorationBackendApiService = + editableExplorationBackendApiService as jasmine.SpyObj; explorationFeaturesBackendApiService = TestBed.inject( - ExplorationFeaturesBackendApiService); - explorationFeaturesBackendApiService = ( - explorationFeaturesBackendApiService) as - jasmine.SpyObj; + ExplorationFeaturesBackendApiService + ); + explorationFeaturesBackendApiService = + explorationFeaturesBackendApiService as jasmine.SpyObj; explorationFeaturesService = TestBed.inject(ExplorationFeaturesService); - explorationFeaturesService = ( - explorationFeaturesService) as - jasmine.SpyObj; + explorationFeaturesService = + explorationFeaturesService as jasmine.SpyObj; numberAttemptsService = TestBed.inject(NumberAttemptsService); - numberAttemptsService = ( - numberAttemptsService) as - jasmine.SpyObj; + numberAttemptsService = + numberAttemptsService as jasmine.SpyObj; questionBackendApiService = TestBed.inject(QuestionBackendApiService); - questionBackendApiService = ( - questionBackendApiService) as - jasmine.SpyObj; + questionBackendApiService = + questionBackendApiService as jasmine.SpyObj; pretestQuestionBackendApiService = TestBed.inject( - PretestQuestionBackendApiService); - pretestQuestionBackendApiService = ( - pretestQuestionBackendApiService) as - jasmine.SpyObj; + PretestQuestionBackendApiService + ); + pretestQuestionBackendApiService = + pretestQuestionBackendApiService as jasmine.SpyObj; questionObjectFactory = TestBed.inject(QuestionObjectFactory); - questionObject = questionObjectFactory.createFromBackendDict( - questionBackendDict); + questionObject = + questionObjectFactory.createFromBackendDict(questionBackendDict); urlService = TestBed.inject(UrlService); diagnosticTestPlayerEngineService = TestBed.inject( - DiagnosticTestPlayerEngineService); + DiagnosticTestPlayerEngineService + ); }); it('should properly initialize player', () => { @@ -337,13 +348,15 @@ describe('Exploration Player State Service', () => { explorationPlayerStateService.editorPreviewMode = true; explorationPlayerStateService.initializePlayer(callback); expect(playerTranscriptService.init).toHaveBeenCalled(); - expect(explorationPlayerStateService.initExplorationPreviewPlayer) - .toHaveBeenCalledWith(callback); + expect( + explorationPlayerStateService.initExplorationPreviewPlayer + ).toHaveBeenCalledWith(callback); explorationPlayerStateService.editorPreviewMode = false; explorationPlayerStateService.initializePlayer(callback); expect(playerTranscriptService.init).toHaveBeenCalled(); - expect(explorationPlayerStateService.initExplorationPreviewPlayer) - .toHaveBeenCalledWith(callback); + expect( + explorationPlayerStateService.initExplorationPreviewPlayer + ).toHaveBeenCalledWith(callback); }); it('should initialize exploration services', () => { @@ -352,7 +365,10 @@ describe('Exploration Player State Service', () => { spyOn(explorationEngineService, 'init'); explorationPlayerStateService.initializeExplorationServices( - returnDict, false, () => {}); + returnDict, + false, + () => {} + ); expect(statsReportingService.initSession).toHaveBeenCalled(); expect(playthroughService.initSession).toHaveBeenCalled(); @@ -365,7 +381,9 @@ describe('Exploration Player State Service', () => { let callback = () => {}; explorationPlayerStateService.initializePretestServices( - pretestQuestionObjects, callback); + pretestQuestionObjects, + callback + ); expect(questionPlayerEngineService.init).toHaveBeenCalled(); }); @@ -377,10 +395,16 @@ describe('Exploration Player State Service', () => { let errorCallback = () => {}; explorationPlayerStateService.initializeQuestionPlayerServices( - questions, successCallback, errorCallback); + questions, + successCallback, + errorCallback + ); expect(questionPlayerEngineService.init).toHaveBeenCalledWith( - questionObjects, successCallback, errorCallback); + questionObjects, + successCallback, + errorCallback + ); }); it('should be able to skip the current question', () => { @@ -389,51 +413,58 @@ describe('Exploration Player State Service', () => { explorationPlayerStateService.skipCurrentQuestion(successCallback); - expect(diagnosticTestPlayerEngineService.skipCurrentQuestion) - .toHaveBeenCalledOnceWith(successCallback); + expect( + diagnosticTestPlayerEngineService.skipCurrentQuestion + ).toHaveBeenCalledOnceWith(successCallback); }); it('should set exploration mode', () => { explorationPlayerStateService.setExplorationMode(); expect(explorationPlayerStateService.explorationMode).toEqual( - ExplorationPlayerConstants - .EXPLORATION_MODE.EXPLORATION); - expect(explorationPlayerStateService.currentEngineService) - .toEqual(explorationEngineService); + ExplorationPlayerConstants.EXPLORATION_MODE.EXPLORATION + ); + expect(explorationPlayerStateService.currentEngineService).toEqual( + explorationEngineService + ); }); it('should set pretest mode', () => { explorationPlayerStateService.setPretestMode(); expect(explorationPlayerStateService.explorationMode).toEqual( - ExplorationPlayerConstants - .EXPLORATION_MODE.PRETEST); - expect(explorationPlayerStateService.currentEngineService) - .toEqual(questionPlayerEngineService); + ExplorationPlayerConstants.EXPLORATION_MODE.PRETEST + ); + expect(explorationPlayerStateService.currentEngineService).toEqual( + questionPlayerEngineService + ); }); it('should set question player mode', () => { explorationPlayerStateService.setQuestionPlayerMode(); expect(explorationPlayerStateService.explorationMode).toEqual( - ExplorationPlayerConstants - .EXPLORATION_MODE.QUESTION_PLAYER); - expect(explorationPlayerStateService.currentEngineService) - .toEqual(questionPlayerEngineService); + ExplorationPlayerConstants.EXPLORATION_MODE.QUESTION_PLAYER + ); + expect(explorationPlayerStateService.currentEngineService).toEqual( + questionPlayerEngineService + ); }); it('should set story chapter mode', () => { explorationPlayerStateService.setStoryChapterMode(); expect(explorationPlayerStateService.explorationMode).toEqual( - ExplorationPlayerConstants - .EXPLORATION_MODE.STORY_CHAPTER); - expect(explorationPlayerStateService.currentEngineService) - .toEqual(explorationEngineService); + ExplorationPlayerConstants.EXPLORATION_MODE.STORY_CHAPTER + ); + expect(explorationPlayerStateService.currentEngineService).toEqual( + explorationEngineService + ); }); it('should init exploration preview player', fakeAsync(() => { spyOn(explorationPlayerStateService, 'setExplorationMode'); spyOn( - editableExplorationBackendApiService, 'fetchApplyDraftExplorationAsync') - .and.returnValue(Promise.resolve({ + editableExplorationBackendApiService, + 'fetchApplyDraftExplorationAsync' + ).and.returnValue( + Promise.resolve({ auto_tts_enabled: false, draft_changes: [], is_version_of_draft_valid: true, @@ -458,14 +489,19 @@ describe('Exploration Player State Service', () => { param_specs: {}, param_changes: [], auto_tts_enabled: false, - edits_allowed: true - } - } as ExplorationBackendDict)); - spyOn(explorationFeaturesBackendApiService, 'fetchExplorationFeaturesAsync') - .and.returnValue(Promise.resolve({ + edits_allowed: true, + }, + } as ExplorationBackendDict) + ); + spyOn( + explorationFeaturesBackendApiService, + 'fetchExplorationFeaturesAsync' + ).and.returnValue( + Promise.resolve({ explorationIsCurated: true, - alwaysAskLearnersForAnswerDetails: false - })); + alwaysAskLearnersForAnswerDetails: false, + }) + ); spyOn(explorationFeaturesService, 'init'); spyOn(explorationEngineService, 'init'); spyOn(numberAttemptsService, 'reset'); @@ -480,37 +516,46 @@ describe('Exploration Player State Service', () => { it('should init question player', fakeAsync(() => { spyOn(explorationPlayerStateService, 'setQuestionPlayerMode'); - spyOn(questionBackendApiService, 'fetchQuestionsAsync') - .and.returnValue(Promise.resolve([questionBackendDict])); + spyOn(questionBackendApiService, 'fetchQuestionsAsync').and.returnValue( + Promise.resolve([questionBackendDict]) + ); spyOn(explorationPlayerStateService.onTotalQuestionsReceived, 'emit'); spyOn(explorationPlayerStateService, 'initializeQuestionPlayerServices'); let successCallback = () => {}; let errorCallback = () => {}; - explorationPlayerStateService.initQuestionPlayer({ - skillList: [], - questionCount: 1, - questionsSortedByDifficulty: true - }, successCallback, errorCallback); + explorationPlayerStateService.initQuestionPlayer( + { + skillList: [], + questionCount: 1, + questionsSortedByDifficulty: true, + }, + successCallback, + errorCallback + ); tick(); expect( - explorationPlayerStateService - .onTotalQuestionsReceived.emit) - .toHaveBeenCalled(); - expect(explorationPlayerStateService.initializeQuestionPlayerServices) - .toHaveBeenCalled(); + explorationPlayerStateService.onTotalQuestionsReceived.emit + ).toHaveBeenCalled(); + expect( + explorationPlayerStateService.initializeQuestionPlayerServices + ).toHaveBeenCalled(); })); it('should init exploration player', fakeAsync(() => { let explorationFeatures: ExplorationFeatures = { explorationIsCurated: true, - alwaysAskLearnersForAnswerDetails: false + alwaysAskLearnersForAnswerDetails: false, }; - spyOn(explorationFeaturesBackendApiService, 'fetchExplorationFeaturesAsync') - .and.returnValue(Promise.resolve(explorationFeatures)); - spyOn(pretestQuestionBackendApiService, 'fetchPretestQuestionsAsync') - .and.returnValue(Promise.resolve([questionObject])); + spyOn( + explorationFeaturesBackendApiService, + 'fetchExplorationFeaturesAsync' + ).and.returnValue(Promise.resolve(explorationFeatures)); + spyOn( + pretestQuestionBackendApiService, + 'fetchPretestQuestionsAsync' + ).and.returnValue(Promise.resolve([questionObject])); spyOn(explorationFeaturesService, 'init'); spyOn(explorationPlayerStateService, 'initializePretestServices'); spyOn(explorationPlayerStateService, 'setPretestMode'); @@ -521,21 +566,27 @@ describe('Exploration Player State Service', () => { tick(); expect(explorationFeaturesService.init).toHaveBeenCalled(); expect(explorationPlayerStateService.setPretestMode).toHaveBeenCalled(); - expect(explorationPlayerStateService.initializeExplorationServices) - .toHaveBeenCalled(); - expect(explorationPlayerStateService.initializePretestServices) - .toHaveBeenCalled(); + expect( + explorationPlayerStateService.initializeExplorationServices + ).toHaveBeenCalled(); + expect( + explorationPlayerStateService.initializePretestServices + ).toHaveBeenCalled(); })); it('should init exploration player without pretests', fakeAsync(() => { let explorationFeatures: ExplorationFeatures = { explorationIsCurated: true, - alwaysAskLearnersForAnswerDetails: false + alwaysAskLearnersForAnswerDetails: false, }; - spyOn(explorationFeaturesBackendApiService, 'fetchExplorationFeaturesAsync') - .and.returnValue(Promise.resolve(explorationFeatures)); - spyOn(pretestQuestionBackendApiService, 'fetchPretestQuestionsAsync') - .and.returnValue(Promise.resolve([])); + spyOn( + explorationFeaturesBackendApiService, + 'fetchExplorationFeaturesAsync' + ).and.returnValue(Promise.resolve(explorationFeatures)); + spyOn( + pretestQuestionBackendApiService, + 'fetchPretestQuestionsAsync' + ).and.returnValue(Promise.resolve([])); spyOn(explorationFeaturesService, 'init'); spyOn(explorationPlayerStateService, 'setExplorationMode'); spyOn(explorationPlayerStateService, 'initializeExplorationServices'); @@ -544,24 +595,28 @@ describe('Exploration Player State Service', () => { explorationPlayerStateService.initExplorationPlayer(successCallback); tick(); expect(explorationPlayerStateService.setExplorationMode).toHaveBeenCalled(); - expect(explorationPlayerStateService.initializeExplorationServices) - .toHaveBeenCalled(); + expect( + explorationPlayerStateService.initializeExplorationServices + ).toHaveBeenCalled(); })); it('should init exploration player with story chapter mode', fakeAsync(() => { let explorationFeatures: ExplorationFeatures = { explorationIsCurated: true, - alwaysAskLearnersForAnswerDetails: false + alwaysAskLearnersForAnswerDetails: false, }; spyOn(urlService, 'getUrlParams').and.returnValue({ story_url_fragment: 'fragment', - node_id: 'id' + node_id: 'id', }); spyOn( - explorationFeaturesBackendApiService, 'fetchExplorationFeaturesAsync') - .and.returnValue(Promise.resolve(explorationFeatures)); - spyOn(pretestQuestionBackendApiService, 'fetchPretestQuestionsAsync') - .and.returnValue(Promise.resolve([])); + explorationFeaturesBackendApiService, + 'fetchExplorationFeaturesAsync' + ).and.returnValue(Promise.resolve(explorationFeatures)); + spyOn( + pretestQuestionBackendApiService, + 'fetchPretestQuestionsAsync' + ).and.returnValue(Promise.resolve([])); spyOn(explorationFeaturesService, 'init'); spyOn(explorationPlayerStateService, 'setStoryChapterMode'); spyOn(explorationPlayerStateService, 'initializeExplorationServices'); @@ -569,10 +624,12 @@ describe('Exploration Player State Service', () => { let successCallback = () => {}; explorationPlayerStateService.initExplorationPlayer(successCallback); tick(); - expect(explorationPlayerStateService.setStoryChapterMode) - .toHaveBeenCalled(); - expect(explorationPlayerStateService.initializeExplorationServices) - .toHaveBeenCalled(); + expect( + explorationPlayerStateService.setStoryChapterMode + ).toHaveBeenCalled(); + expect( + explorationPlayerStateService.initializeExplorationServices + ).toHaveBeenCalled(); })); it('should intialize question player', () => { @@ -580,11 +637,15 @@ describe('Exploration Player State Service', () => { spyOn(explorationPlayerStateService, 'initQuestionPlayer'); let successCallback = () => {}; let errorCallback = () => {}; - explorationPlayerStateService.initializeQuestionPlayer({ - skillList: [], - questionCount: 1, - questionsSortedByDifficulty: true - }, successCallback, errorCallback); + explorationPlayerStateService.initializeQuestionPlayer( + { + skillList: [], + questionCount: 1, + questionsSortedByDifficulty: true, + }, + successCallback, + errorCallback + ); }); it('should intialize diagnostic test player', () => { @@ -593,22 +654,26 @@ describe('Exploration Player State Service', () => { let topicIdToPrerequisiteTopicIds = { topicId1: [], topicId2: ['topicId1'], - topicId3: ['topicId2'] + topicId3: ['topicId2'], }; let diagnosticTestTopicTrackerModel = new DiagnosticTestTopicTrackerModel( - topicIdToPrerequisiteTopicIds); + topicIdToPrerequisiteTopicIds + ); explorationPlayerStateService.initializeDiagnosticPlayer( - diagnosticTestTopicTrackerModel, successCallback); + diagnosticTestTopicTrackerModel, + successCallback + ); expect(diagnosticTestPlayerEngineService.init).toHaveBeenCalled(); }); it('should get current engine service', () => { explorationPlayerStateService.setExplorationMode(); - expect(explorationPlayerStateService.getCurrentEngineService()) - .toEqual(explorationPlayerStateService.currentEngineService); + expect(explorationPlayerStateService.getCurrentEngineService()).toEqual( + explorationPlayerStateService.currentEngineService + ); }); it('should tell if is in pretest mode', () => { @@ -623,27 +688,28 @@ describe('Exploration Player State Service', () => { it('should tell if is in diagnostic test player mode', () => { explorationPlayerStateService.setDiagnosticTestPlayerMode(); - expect(explorationPlayerStateService.isInDiagnosticTestPlayerMode()) - .toBeTrue(); + expect( + explorationPlayerStateService.isInDiagnosticTestPlayerMode() + ).toBeTrue(); }); - it( - 'should tell if the mode can only present isolated questions or not', - fakeAsync(() => { - explorationPlayerStateService.setDiagnosticTestPlayerMode(); - expect(explorationPlayerStateService.isPresentingIsolatedQuestions()) - .toBeTrue(); - - explorationPlayerStateService.setExplorationMode(); - expect(explorationPlayerStateService.isPresentingIsolatedQuestions()) - .toBeFalse(); - - explorationPlayerStateService.explorationMode = 'invalidMode'; - expect(() => { - explorationPlayerStateService.isPresentingIsolatedQuestions(); - tick(10); - }).toThrowError('Invalid mode received: invalidMode.'); - })); + it('should tell if the mode can only present isolated questions or not', fakeAsync(() => { + explorationPlayerStateService.setDiagnosticTestPlayerMode(); + expect( + explorationPlayerStateService.isPresentingIsolatedQuestions() + ).toBeTrue(); + + explorationPlayerStateService.setExplorationMode(); + expect( + explorationPlayerStateService.isPresentingIsolatedQuestions() + ).toBeFalse(); + + explorationPlayerStateService.explorationMode = 'invalidMode'; + expect(() => { + explorationPlayerStateService.isPresentingIsolatedQuestions(); + tick(10); + }).toThrowError('Invalid mode received: invalidMode.'); + })); it('should tell if is in question player mode', () => { explorationPlayerStateService.setQuestionPlayerMode(); @@ -660,13 +726,14 @@ describe('Exploration Player State Service', () => { spyOn(explorationPlayerStateService, 'setStoryChapterMode'); spyOn(urlService, 'getUrlParams').and.returnValue({ story_url_fragment: 'fragment', - node_id: 'id' + node_id: 'id', }); let callback = () => {}; explorationPlayerStateService.moveToExploration(callback); - expect(explorationPlayerStateService.setStoryChapterMode) - .toHaveBeenCalled(); + expect( + explorationPlayerStateService.setStoryChapterMode + ).toHaveBeenCalled(); expect(explorationEngineService.moveToExploration).toHaveBeenCalled(); }); @@ -679,11 +746,13 @@ describe('Exploration Player State Service', () => { it('should get language code', () => { let languageCode: string = 'test_lang_code'; - spyOn(explorationEngineService, 'getLanguageCode') - .and.returnValue(languageCode); + spyOn(explorationEngineService, 'getLanguageCode').and.returnValue( + languageCode + ); explorationPlayerStateService.setExplorationMode(); - expect(explorationPlayerStateService.getLanguageCode()) - .toEqual(languageCode); + expect(explorationPlayerStateService.getLanguageCode()).toEqual( + languageCode + ); }); it('should record new card added', () => { @@ -694,60 +763,72 @@ describe('Exploration Player State Service', () => { }); it('should test getters', () => { - expect(explorationPlayerStateService.onTotalQuestionsReceived) - .toBeDefined(); + expect( + explorationPlayerStateService.onTotalQuestionsReceived + ).toBeDefined(); expect(explorationPlayerStateService.onPlayerStateChange).toBeDefined(); - expect(explorationPlayerStateService.onOppiaFeedbackAvailable) - .toBeDefined(); - }); - - it('should set exploration version from url if the url' + - 'has exploration context when initialized', () => { - // Here exploration context consists of 'explore', 'create', - // 'skill_editor' and 'embed'. - spyOn(urlService, 'getPathname') - .and.returnValue('/create/in/path/name'); - spyOn(urlService, 'getExplorationVersionFromUrl') - .and.returnValue(2); - - explorationPlayerStateService.version = null; - - explorationPlayerStateService.init(); - expect(explorationPlayerStateService.version).toBe(2); + expect( + explorationPlayerStateService.onOppiaFeedbackAvailable + ).toBeDefined(); }); - it('should set exploration version to default value if the url' + - 'does not have exploration context when initialized', () => { - // Here default value is 1. - spyOn(urlService, 'getPathname') - .and.returnValue('/create_is_not/in/path/name'); - - explorationPlayerStateService.version = null; + it( + 'should set exploration version from url if the url' + + 'has exploration context when initialized', + () => { + // Here exploration context consists of 'explore', 'create', + // 'skill_editor' and 'embed'. + spyOn(urlService, 'getPathname').and.returnValue('/create/in/path/name'); + spyOn(urlService, 'getExplorationVersionFromUrl').and.returnValue(2); + + explorationPlayerStateService.version = null; + + explorationPlayerStateService.init(); + expect(explorationPlayerStateService.version).toBe(2); + } + ); - explorationPlayerStateService.init(); - expect(explorationPlayerStateService.version).toBe(1); - }); + it( + 'should set exploration version to default value if the url' + + 'does not have exploration context when initialized', + () => { + // Here default value is 1. + spyOn(urlService, 'getPathname').and.returnValue( + '/create_is_not/in/path/name' + ); + + explorationPlayerStateService.version = null; + + explorationPlayerStateService.init(); + expect(explorationPlayerStateService.version).toBe(1); + } + ); it('should tell if logged out learner progress is tracked', () => { - expect(explorationPlayerStateService.isLoggedOutLearnerProgressTracked()) - .toBeFalse(); + expect( + explorationPlayerStateService.isLoggedOutLearnerProgressTracked() + ).toBeFalse(); explorationPlayerStateService.trackLoggedOutLearnerProgress(); - expect(explorationPlayerStateService.isLoggedOutLearnerProgressTracked()) - .toBeTrue(); + expect( + explorationPlayerStateService.isLoggedOutLearnerProgressTracked() + ).toBeTrue(); }); it('should set unique progress URL id correctly', fakeAsync(() => { spyOn( editableExplorationBackendApiService, - 'recordProgressAndFetchUniqueProgressIdOfLoggedOutLearner') - .and.returnValue(Promise.resolve({ - unique_progress_url_id: '123456' - })); + 'recordProgressAndFetchUniqueProgressIdOfLoggedOutLearner' + ).and.returnValue( + Promise.resolve({ + unique_progress_url_id: '123456', + }) + ); expect(explorationPlayerStateService.getUniqueProgressUrlId()).toBeNull(); explorationPlayerStateService.setLastCompletedCheckpoint('abc'); explorationPlayerStateService.setUniqueProgressUrlId(); tick(100); expect(explorationPlayerStateService.getUniqueProgressUrlId()).toEqual( - '123456'); + '123456' + ); })); }); diff --git a/core/templates/pages/exploration-player-page/services/exploration-player-state.service.ts b/core/templates/pages/exploration-player-page/services/exploration-player-state.service.ts index 286cc5d8235d..58e6947a19df 100644 --- a/core/templates/pages/exploration-player-page/services/exploration-player-state.service.ts +++ b/core/templates/pages/exploration-player-page/services/exploration-player-state.service.ts @@ -17,27 +17,37 @@ * like engine service. */ -import { Injectable, EventEmitter } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { EditableExplorationBackendApiService } from 'domain/exploration/editable-exploration-backend-api.service'; -import { FetchExplorationBackendResponse, ReadOnlyExplorationBackendApiService } from 'domain/exploration/read-only-exploration-backend-api.service'; -import { PretestQuestionBackendApiService } from 'domain/question/pretest-question-backend-api.service'; -import { QuestionBackendApiService } from 'domain/question/question-backend-api.service'; -import { Question, QuestionBackendDict, QuestionObjectFactory } from 'domain/question/QuestionObjectFactory'; -import { StateCard } from 'domain/state_card/state-card.model'; -import { DiagnosticTestTopicTrackerModel } from 'pages/diagnostic-test-player-page/diagnostic-test-topic-tracker.model'; -import { ContextService } from 'services/context.service'; -import { UrlService } from 'services/contextual/url.service'; -import { ExplorationFeatures, ExplorationFeaturesBackendApiService } from 'services/exploration-features-backend-api.service'; -import { ExplorationFeaturesService } from 'services/exploration-features.service'; -import { PlaythroughService } from 'services/playthrough.service'; -import { ExplorationPlayerConstants } from '../exploration-player-page.constants'; -import { ExplorationEngineService } from './exploration-engine.service'; -import { NumberAttemptsService } from './number-attempts.service'; -import { PlayerTranscriptService } from './player-transcript.service'; -import { QuestionPlayerEngineService } from './question-player-engine.service'; -import { DiagnosticTestPlayerEngineService } from './diagnostic-test-player-engine.service'; -import { StatsReportingService } from './stats-reporting.service'; +import {Injectable, EventEmitter} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {EditableExplorationBackendApiService} from 'domain/exploration/editable-exploration-backend-api.service'; +import { + FetchExplorationBackendResponse, + ReadOnlyExplorationBackendApiService, +} from 'domain/exploration/read-only-exploration-backend-api.service'; +import {PretestQuestionBackendApiService} from 'domain/question/pretest-question-backend-api.service'; +import {QuestionBackendApiService} from 'domain/question/question-backend-api.service'; +import { + Question, + QuestionBackendDict, + QuestionObjectFactory, +} from 'domain/question/QuestionObjectFactory'; +import {StateCard} from 'domain/state_card/state-card.model'; +import {DiagnosticTestTopicTrackerModel} from 'pages/diagnostic-test-player-page/diagnostic-test-topic-tracker.model'; +import {ContextService} from 'services/context.service'; +import {UrlService} from 'services/contextual/url.service'; +import { + ExplorationFeatures, + ExplorationFeaturesBackendApiService, +} from 'services/exploration-features-backend-api.service'; +import {ExplorationFeaturesService} from 'services/exploration-features.service'; +import {PlaythroughService} from 'services/playthrough.service'; +import {ExplorationPlayerConstants} from '../exploration-player-page.constants'; +import {ExplorationEngineService} from './exploration-engine.service'; +import {NumberAttemptsService} from './number-attempts.service'; +import {PlayerTranscriptService} from './player-transcript.service'; +import {QuestionPlayerEngineService} from './question-player-engine.service'; +import {DiagnosticTestPlayerEngineService} from './diagnostic-test-player-engine.service'; +import {StatsReportingService} from './stats-reporting.service'; interface QuestionPlayerConfigDict { skillList: string[]; @@ -46,20 +56,19 @@ interface QuestionPlayerConfigDict { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ExplorationPlayerStateService { - private _totalQuestionsReceivedEventEmitter: EventEmitter = ( - new EventEmitter()); + private _totalQuestionsReceivedEventEmitter: EventEmitter = + new EventEmitter(); - private _oppiaFeedbackAvailableEventEmitter: EventEmitter = ( - new EventEmitter()); + private _oppiaFeedbackAvailableEventEmitter: EventEmitter = + new EventEmitter(); - currentEngineService: ( - ExplorationEngineService | - QuestionPlayerEngineService | - DiagnosticTestPlayerEngineService - ); + currentEngineService: + | ExplorationEngineService + | QuestionPlayerEngineService + | DiagnosticTestPlayerEngineService; explorationMode: string; editorPreviewMode: boolean; @@ -71,33 +80,27 @@ export class ExplorationPlayerStateService { lastCompletedCheckpoint: string; isLoggedOutProgressTracked: boolean = false; uniqueProgressUrlId: string | null = null; - private _playerStateChangeEventEmitter: EventEmitter = ( - new EventEmitter()); + private _playerStateChangeEventEmitter: EventEmitter = + new EventEmitter(); - private _playerProgressModalShownEventEmitter: EventEmitter = ( - new EventEmitter()); + private _playerProgressModalShownEventEmitter: EventEmitter = + new EventEmitter(); constructor( private contextService: ContextService, - private editableExplorationBackendApiService: - EditableExplorationBackendApiService, - private explorationEngineService: - ExplorationEngineService, - private explorationFeaturesBackendApiService: - ExplorationFeaturesBackendApiService, + private editableExplorationBackendApiService: EditableExplorationBackendApiService, + private explorationEngineService: ExplorationEngineService, + private explorationFeaturesBackendApiService: ExplorationFeaturesBackendApiService, private explorationFeaturesService: ExplorationFeaturesService, private numberAttemptsService: NumberAttemptsService, - private playerTranscriptService: - PlayerTranscriptService, + private playerTranscriptService: PlayerTranscriptService, private playthroughService: PlaythroughService, private pretestQuestionBackendApiService: PretestQuestionBackendApiService, private questionBackendApiService: QuestionBackendApiService, private questionObjectFactory: QuestionObjectFactory, private questionPlayerEngineService: QuestionPlayerEngineService, - private diagnosticTestPlayerEngineService: - DiagnosticTestPlayerEngineService, - private readOnlyExplorationBackendApiService: - ReadOnlyExplorationBackendApiService, + private diagnosticTestPlayerEngineService: DiagnosticTestPlayerEngineService, + private readOnlyExplorationBackendApiService: ReadOnlyExplorationBackendApiService, private statsReportingService: StatsReportingService, private urlService: UrlService ) { @@ -109,11 +112,13 @@ export class ExplorationPlayerStateService { let explorationContext = false; for (let i = 0; i < pathnameArray.length; i++) { - if (pathnameArray[i] === 'explore' || - pathnameArray[i] === 'create' || - pathnameArray[i] === 'skill_editor' || - pathnameArray[i] === 'embed' || - pathnameArray[i] === 'lesson') { + if ( + pathnameArray[i] === 'explore' || + pathnameArray[i] === 'create' || + pathnameArray[i] === 'skill_editor' || + pathnameArray[i] === 'embed' || + pathnameArray[i] === 'lesson' + ) { explorationContext = true; break; } @@ -125,13 +130,18 @@ export class ExplorationPlayerStateService { this.explorationId = this.contextService.getExplorationId(); this.version = this.urlService.getExplorationVersionFromUrl(); - if (!this.questionPlayerMode && - !('skill_editor' === this.urlService.getPathname() - .split('/')[1].replace(/"/g, "'"))) { - this.readOnlyExplorationBackendApiService.loadExplorationAsync( - this.explorationId, this.version).then((exploration) => { - this.version = exploration.version; - }); + if ( + !this.questionPlayerMode && + !( + 'skill_editor' === + this.urlService.getPathname().split('/')[1].replace(/"/g, "'") + ) + ) { + this.readOnlyExplorationBackendApiService + .loadExplorationAsync(this.explorationId, this.version) + .then(exploration => { + this.version = exploration.version; + }); } } else { this.explorationId = 'test_id'; @@ -144,20 +154,25 @@ export class ExplorationPlayerStateService { } initializeExplorationServices( - returnDict: FetchExplorationBackendResponse, - arePretestsAvailable: boolean, - callback: (stateCard: StateCard, str: string) => void + returnDict: FetchExplorationBackendResponse, + arePretestsAvailable: boolean, + callback: (stateCard: StateCard, str: string) => void ): void { // For some cases, version is set only after // ReadOnlyExplorationBackendApiService.loadExploration() has completed. // Use returnDict.version for non-null version value. this.statsReportingService.initSession( - this.explorationId, returnDict.exploration.title, returnDict.version, + this.explorationId, + returnDict.exploration.title, + returnDict.version, returnDict.session_id, - this.urlService.getCollectionIdFromExplorationUrl()); + this.urlService.getCollectionIdFromExplorationUrl() + ); this.playthroughService.initSession( - this.explorationId, returnDict.version, - returnDict.record_playthrough_probability); + this.explorationId, + returnDict.version, + returnDict.record_playthrough_probability + ); this.explorationEngineService.init( { auto_tts_enabled: returnDict.auto_tts_enabled, @@ -172,40 +187,46 @@ export class ExplorationPlayerStateService { language_code: returnDict.exploration.language_code, version: returnDict.version, next_content_id_index: returnDict.exploration.next_content_id_index, - exploration_metadata: returnDict.exploration_metadata + exploration_metadata: returnDict.exploration_metadata, }, returnDict.version, returnDict.preferred_audio_language_code, returnDict.auto_tts_enabled, returnDict.preferred_language_codes, returnDict.displayable_language_codes, - arePretestsAvailable ? () => {} : callback); + arePretestsAvailable ? () => {} : callback + ); } initializePretestServices( - pretestQuestionObjects: Question[], - callback: ( - initialCard: StateCard, nextFocusLabel: string) => void): void { + pretestQuestionObjects: Question[], + callback: (initialCard: StateCard, nextFocusLabel: string) => void + ): void { this.questionPlayerEngineService.init( - pretestQuestionObjects, callback, () => {}); + pretestQuestionObjects, + callback, + () => {} + ); } initializeQuestionPlayerServices( - questionDicts: QuestionBackendDict[], - successCallback: (initialCard: StateCard, nextFocusLabel: string) => void, - errorCallback: () => void): void { - let questionObjects = questionDicts.map( - function(questionDict) { - return this.questionObjectFactory.createFromBackendDict( - questionDict); - }, this); + questionDicts: QuestionBackendDict[], + successCallback: (initialCard: StateCard, nextFocusLabel: string) => void, + errorCallback: () => void + ): void { + let questionObjects = questionDicts.map(function (questionDict) { + return this.questionObjectFactory.createFromBackendDict(questionDict); + }, this); this.questionPlayerEngineService.init( - questionObjects, successCallback, errorCallback); + questionObjects, + successCallback, + errorCallback + ); } setExplorationMode(): void { - this.explorationMode = ExplorationPlayerConstants - .EXPLORATION_MODE.EXPLORATION; + this.explorationMode = + ExplorationPlayerConstants.EXPLORATION_MODE.EXPLORATION; this.currentEngineService = this.explorationEngineService; } @@ -215,88 +236,118 @@ export class ExplorationPlayerStateService { } setQuestionPlayerMode(): void { - this.explorationMode = ExplorationPlayerConstants - .EXPLORATION_MODE.QUESTION_PLAYER; + this.explorationMode = + ExplorationPlayerConstants.EXPLORATION_MODE.QUESTION_PLAYER; this.currentEngineService = this.questionPlayerEngineService; } setDiagnosticTestPlayerMode(): void { - this.explorationMode = ( - ExplorationPlayerConstants.EXPLORATION_MODE.DIAGNOSTIC_TEST_PLAYER); + this.explorationMode = + ExplorationPlayerConstants.EXPLORATION_MODE.DIAGNOSTIC_TEST_PLAYER; this.currentEngineService = this.diagnosticTestPlayerEngineService; } setStoryChapterMode(): void { - this.explorationMode = ExplorationPlayerConstants - .EXPLORATION_MODE.STORY_CHAPTER; + this.explorationMode = + ExplorationPlayerConstants.EXPLORATION_MODE.STORY_CHAPTER; this.currentEngineService = this.explorationEngineService; } initExplorationPreviewPlayer( - callback: (sateCard: StateCard, str: string) => void): void { + callback: (sateCard: StateCard, str: string) => void + ): void { this.setExplorationMode(); Promise.all([ this.editableExplorationBackendApiService.fetchApplyDraftExplorationAsync( - this.explorationId), + this.explorationId + ), this.explorationFeaturesBackendApiService.fetchExplorationFeaturesAsync( - this.explorationId), - ]).then((combinedData) => { + this.explorationId + ), + ]).then(combinedData => { let explorationData = combinedData[0]; let featuresData: ExplorationFeatures = combinedData[1]; - this.explorationFeaturesService.init({ - param_changes: explorationData.param_changes, - states: explorationData.states - }, featuresData); + this.explorationFeaturesService.init( + { + param_changes: explorationData.param_changes, + states: explorationData.states, + }, + featuresData + ); this.explorationEngineService.init( - explorationData, null, null, null, null, [], callback); + explorationData, + null, + null, + null, + null, + [], + callback + ); this.numberAttemptsService.reset(); }); } initQuestionPlayer( - questionPlayerConfig: QuestionPlayerConfigDict, - successCallback: (initialCard: StateCard, nextFocusLabel: string) => void, - errorCallback: () => void): void { + questionPlayerConfig: QuestionPlayerConfigDict, + successCallback: (initialCard: StateCard, nextFocusLabel: string) => void, + errorCallback: () => void + ): void { this.setQuestionPlayerMode(); - this.questionBackendApiService.fetchQuestionsAsync( - questionPlayerConfig.skillList, - questionPlayerConfig.questionCount, - questionPlayerConfig.questionsSortedByDifficulty - ).then((questionData) => { - this._totalQuestionsReceivedEventEmitter.emit(questionData.length); - this.initializeQuestionPlayerServices( - questionData, successCallback, errorCallback); - }); + this.questionBackendApiService + .fetchQuestionsAsync( + questionPlayerConfig.skillList, + questionPlayerConfig.questionCount, + questionPlayerConfig.questionsSortedByDifficulty + ) + .then(questionData => { + this._totalQuestionsReceivedEventEmitter.emit(questionData.length); + this.initializeQuestionPlayerServices( + questionData, + successCallback, + errorCallback + ); + }); } initExplorationPlayer( - callback: (stateCard: StateCard, str: string) => void): void { - let explorationDataPromise = this.version ? - this.readOnlyExplorationBackendApiService.loadExplorationAsync( - this.explorationId, this.version) : - this.readOnlyExplorationBackendApiService.loadLatestExplorationAsync( - this.explorationId); + callback: (stateCard: StateCard, str: string) => void + ): void { + let explorationDataPromise = this.version + ? this.readOnlyExplorationBackendApiService.loadExplorationAsync( + this.explorationId, + this.version + ) + : this.readOnlyExplorationBackendApiService.loadLatestExplorationAsync( + this.explorationId + ); Promise.all([ explorationDataPromise, this.pretestQuestionBackendApiService.fetchPretestQuestionsAsync( - this.explorationId, this.storyUrlFragment), + this.explorationId, + this.storyUrlFragment + ), this.explorationFeaturesBackendApiService.fetchExplorationFeaturesAsync( - this.explorationId), - ]).then((combinedData) => { + this.explorationId + ), + ]).then(combinedData => { let explorationData: FetchExplorationBackendResponse = combinedData[0]; let pretestQuestionsData = combinedData[1]; let featuresData = combinedData[2]; - this.explorationFeaturesService.init({ - ...explorationData.exploration, - }, featuresData); + this.explorationFeaturesService.init( + { + ...explorationData.exploration, + }, + featuresData + ); if (pretestQuestionsData.length > 0) { this.setPretestMode(); this.initializeExplorationServices(explorationData, true, callback); this.initializePretestServices(pretestQuestionsData, callback); } else if ( this.urlService.getUrlParams().hasOwnProperty('story_url_fragment') && - this.urlService.getUrlParams().hasOwnProperty('node_id')) { + this.urlService.getUrlParams().hasOwnProperty('node_id') + ) { this.setStoryChapterMode(); this.initializeExplorationServices(explorationData, false, callback); } else { @@ -307,7 +358,8 @@ export class ExplorationPlayerStateService { } initializePlayer( - callback: (stateCard: StateCard, str: string) => void): void { + callback: (stateCard: StateCard, str: string) => void + ): void { this.playerTranscriptService.init(); if (this.editorPreviewMode) { this.initExplorationPreviewPlayer(callback); @@ -317,16 +369,17 @@ export class ExplorationPlayerStateService { } initializeQuestionPlayer( - config: QuestionPlayerConfigDict, - successCallback: (initialCard: StateCard, nextFocusLabel: string) => void, - errorCallback: () => void): void { + config: QuestionPlayerConfigDict, + successCallback: (initialCard: StateCard, nextFocusLabel: string) => void, + errorCallback: () => void + ): void { this.playerTranscriptService.init(); this.initQuestionPlayer(config, successCallback, errorCallback); } initializeDiagnosticPlayer( - diagnosticTestTopicTrackerModel: DiagnosticTestTopicTrackerModel, - successCallback: (initialCard: StateCard, nextFocusLabel: string) => void + diagnosticTestTopicTrackerModel: DiagnosticTestTopicTrackerModel, + successCallback: (initialCard: StateCard, nextFocusLabel: string) => void ): void { this.setDiagnosticTestPlayerMode(); this.diagnosticTestPlayerEngineService.init( @@ -336,26 +389,33 @@ export class ExplorationPlayerStateService { } getCurrentEngineService(): - ExplorationEngineService | - QuestionPlayerEngineService | - DiagnosticTestPlayerEngineService { + | ExplorationEngineService + | QuestionPlayerEngineService + | DiagnosticTestPlayerEngineService { return this.currentEngineService; } isInPretestMode(): boolean { - return this.explorationMode === - ExplorationPlayerConstants.EXPLORATION_MODE.PRETEST; + return ( + this.explorationMode === + ExplorationPlayerConstants.EXPLORATION_MODE.PRETEST + ); } isInQuestionMode(): boolean { - return this.explorationMode === ExplorationPlayerConstants - .EXPLORATION_MODE.PRETEST || this.explorationMode === - ExplorationPlayerConstants.EXPLORATION_MODE.QUESTION_PLAYER; + return ( + this.explorationMode === + ExplorationPlayerConstants.EXPLORATION_MODE.PRETEST || + this.explorationMode === + ExplorationPlayerConstants.EXPLORATION_MODE.QUESTION_PLAYER + ); } isInQuestionPlayerMode(): boolean { - return this.explorationMode === ExplorationPlayerConstants - .EXPLORATION_MODE.QUESTION_PLAYER; + return ( + this.explorationMode === + ExplorationPlayerConstants.EXPLORATION_MODE.QUESTION_PLAYER + ); } isPresentingIsolatedQuestions(): boolean { @@ -367,18 +427,19 @@ export class ExplorationPlayerStateService { // with questions are presented. if ( this.explorationMode === - ExplorationPlayerConstants.EXPLORATION_MODE.QUESTION_PLAYER || + ExplorationPlayerConstants.EXPLORATION_MODE.QUESTION_PLAYER || this.explorationMode === - ExplorationPlayerConstants.EXPLORATION_MODE.DIAGNOSTIC_TEST_PLAYER || + ExplorationPlayerConstants.EXPLORATION_MODE.DIAGNOSTIC_TEST_PLAYER || this.explorationMode === - ExplorationPlayerConstants.EXPLORATION_MODE.PRETEST + ExplorationPlayerConstants.EXPLORATION_MODE.PRETEST ) { return true; } else if ( this.explorationMode === - ExplorationPlayerConstants.EXPLORATION_MODE.EXPLORATION || + ExplorationPlayerConstants.EXPLORATION_MODE.EXPLORATION || this.explorationMode === - ExplorationPlayerConstants.EXPLORATION_MODE.STORY_CHAPTER) { + ExplorationPlayerConstants.EXPLORATION_MODE.STORY_CHAPTER + ) { return false; } else { throw new Error('Invalid mode received: ' + this.explorationMode + '.'); @@ -388,19 +449,24 @@ export class ExplorationPlayerStateService { isInDiagnosticTestPlayerMode(): boolean { return ( this.explorationMode === - ExplorationPlayerConstants.EXPLORATION_MODE.DIAGNOSTIC_TEST_PLAYER); + ExplorationPlayerConstants.EXPLORATION_MODE.DIAGNOSTIC_TEST_PLAYER + ); } isInStoryChapterMode(): boolean { - return this.explorationMode === - ExplorationPlayerConstants.EXPLORATION_MODE.STORY_CHAPTER; + return ( + this.explorationMode === + ExplorationPlayerConstants.EXPLORATION_MODE.STORY_CHAPTER + ); } moveToExploration( - callback: (stateCard: StateCard, label: string) => void): void { + callback: (stateCard: StateCard, label: string) => void + ): void { if ( this.urlService.getUrlParams().hasOwnProperty('story_url_fragment') && - this.urlService.getUrlParams().hasOwnProperty('node_id')) { + this.urlService.getUrlParams().hasOwnProperty('node_id') + ) { this.setStoryChapterMode(); } else { this.setExplorationMode(); @@ -421,15 +487,14 @@ export class ExplorationPlayerStateService { } async setUniqueProgressUrlId(): Promise { - await this.editableExplorationBackendApiService. - recordProgressAndFetchUniqueProgressIdOfLoggedOutLearner( + await this.editableExplorationBackendApiService + .recordProgressAndFetchUniqueProgressIdOfLoggedOutLearner( this.explorationId, this.version, this.lastCompletedCheckpoint ) - .then((response) => { - this.uniqueProgressUrlId = ( - response.unique_progress_url_id); + .then(response => { + this.uniqueProgressUrlId = response.unique_progress_url_id; this.trackLoggedOutLearnerProgress(); }); } @@ -467,5 +532,9 @@ export class ExplorationPlayerStateService { } } -angular.module('oppia').factory('ExplorationPlayerStateService', - downgradeInjectable(ExplorationPlayerStateService)); +angular + .module('oppia') + .factory( + 'ExplorationPlayerStateService', + downgradeInjectable(ExplorationPlayerStateService) + ); diff --git a/core/templates/pages/exploration-player-page/services/exploration-recommendations.service.spec.ts b/core/templates/pages/exploration-player-page/services/exploration-recommendations.service.spec.ts index 677099f4e7cc..c0a444ece607 100644 --- a/core/templates/pages/exploration-player-page/services/exploration-recommendations.service.spec.ts +++ b/core/templates/pages/exploration-player-page/services/exploration-recommendations.service.spec.ts @@ -16,16 +16,15 @@ * @fileoverview Unit tests for ExplorationRecommendationsService. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { async, TestBed } from '@angular/core/testing'; - -import { ExplorationRecommendationsBackendApiService } from 'domain/recommendations/exploration-recommendations-backend-api.service'; -import { LearnerExplorationSummary } from 'domain/summary/learner-exploration-summary.model'; -import { ExplorationRecommendationsService } from 'pages/exploration-player-page/services/exploration-recommendations.service'; -import { ContextService } from 'services/context.service'; -import { UrlService } from 'services/contextual/url.service'; -import { ServicesConstants } from 'services/services.constants'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {async, TestBed} from '@angular/core/testing'; +import {ExplorationRecommendationsBackendApiService} from 'domain/recommendations/exploration-recommendations-backend-api.service'; +import {LearnerExplorationSummary} from 'domain/summary/learner-exploration-summary.model'; +import {ExplorationRecommendationsService} from 'pages/exploration-player-page/services/exploration-recommendations.service'; +import {ContextService} from 'services/context.service'; +import {UrlService} from 'services/contextual/url.service'; +import {ServicesConstants} from 'services/services.constants'; describe('Exploration Recommendations Service', () => { let expRecsService: ExplorationRecommendationsService; @@ -39,12 +38,8 @@ describe('Exploration Recommendations Service', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - ], - providers: [ - ExplorationRecommendationsService, - ] + imports: [HttpClientTestingModule], + providers: [ExplorationRecommendationsService], }); })); @@ -53,11 +48,12 @@ describe('Exploration Recommendations Service', () => { contextService = TestBed.inject(ContextService); spyOn(urlService, 'getCollectionIdFromExplorationUrl').and.returnValue( - COLLECTION_ID); + COLLECTION_ID + ); spyOn(urlService, 'getUrlParams').and.returnValue({ story_id: STORY_ID, - node_id: NODE_ID + node_id: NODE_ID, }); spyOn(contextService, 'getExplorationId').and.returnValue(EXPLORATION_ID); @@ -120,12 +116,12 @@ describe('Exploration Recommendations Service', () => { beforeEach(() => { systemRecommendations = [ new MockExplorationSummary('system_rec_1') as LearnerExplorationSummary, - new MockExplorationSummary('system_rec_2') as LearnerExplorationSummary + new MockExplorationSummary('system_rec_2') as LearnerExplorationSummary, ]; authorRecommendations = [ new MockExplorationSummary('author_rec_1') as LearnerExplorationSummary, - new MockExplorationSummary('author_rec_2') as LearnerExplorationSummary + new MockExplorationSummary('author_rec_2') as LearnerExplorationSummary, ]; expRecsBackendApiService = TestBed.inject( @@ -134,13 +130,14 @@ describe('Exploration Recommendations Service', () => { spyOn( expRecsBackendApiService, - 'getRecommendedSummaryDictsAsync').and.callFake( - async(_, includeSystemRecommendations: string) => { - return Promise.resolve( - includeSystemRecommendations === 'true' ? - systemRecommendations.concat(authorRecommendations) : - authorRecommendations); - }); + 'getRecommendedSummaryDictsAsync' + ).and.callFake(async (_, includeSystemRecommendations: string) => { + return Promise.resolve( + includeSystemRecommendations === 'true' + ? systemRecommendations.concat(authorRecommendations) + : authorRecommendations + ); + }); }); describe('when used in other page context', () => { @@ -153,20 +150,24 @@ describe('Exploration Recommendations Service', () => { it('should include author recommendations', () => { expRecsService.getRecommendedSummaryDicts( - AUTHOR_REC_IDS, true, - (expSummaries) => { + AUTHOR_REC_IDS, + true, + expSummaries => { expect(expSummaries).toEqual( - jasmine.arrayContaining(authorRecommendations)); + jasmine.arrayContaining(authorRecommendations) + ); } ); }); it('should include system recommendations', () => { expRecsService.getRecommendedSummaryDicts( - AUTHOR_REC_IDS, true, - (expSummaries) => { + AUTHOR_REC_IDS, + true, + expSummaries => { expect(expSummaries).toEqual( - jasmine.arrayContaining(systemRecommendations)); + jasmine.arrayContaining(systemRecommendations) + ); } ); }); @@ -182,20 +183,24 @@ describe('Exploration Recommendations Service', () => { it('should include author recommendations', () => { expRecsService.getRecommendedSummaryDicts( - AUTHOR_REC_IDS, true, - (expSummaries) => { + AUTHOR_REC_IDS, + true, + expSummaries => { expect(expSummaries).toEqual( - jasmine.arrayContaining(authorRecommendations)); + jasmine.arrayContaining(authorRecommendations) + ); } ); }); it('should not include system recommendations', () => { expRecsService.getRecommendedSummaryDicts( - AUTHOR_REC_IDS, true, - (expSummaries) => { + AUTHOR_REC_IDS, + true, + expSummaries => { expect(expSummaries).not.toEqual( - jasmine.arrayContaining(systemRecommendations)); + jasmine.arrayContaining(systemRecommendations) + ); } ); }); diff --git a/core/templates/pages/exploration-player-page/services/exploration-recommendations.service.ts b/core/templates/pages/exploration-player-page/services/exploration-recommendations.service.ts index b6eb41db56a7..108a4b3fe454 100644 --- a/core/templates/pages/exploration-player-page/services/exploration-recommendations.service.ts +++ b/core/templates/pages/exploration-player-page/services/exploration-recommendations.service.ts @@ -17,20 +17,18 @@ * exploration. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { ContextService } from 'services/context.service'; -import { ServicesConstants } from 'services/services.constants'; -import { UrlService } from 'services/contextual/url.service'; +import {ContextService} from 'services/context.service'; +import {ServicesConstants} from 'services/services.constants'; +import {UrlService} from 'services/contextual/url.service'; -import { ExplorationRecommendationsBackendApiService } from - 'domain/recommendations/exploration-recommendations-backend-api.service'; -import { LearnerExplorationSummary } from - 'domain/summary/learner-exploration-summary.model'; +import {ExplorationRecommendationsBackendApiService} from 'domain/recommendations/exploration-recommendations-backend-api.service'; +import {LearnerExplorationSummary} from 'domain/summary/learner-exploration-summary.model'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ExplorationRecommendationsService { isIframed: boolean = false; @@ -44,22 +42,23 @@ export class ExplorationRecommendationsService { constructor( private contextService: ContextService, private urlService: UrlService, - private expRecommendationBackendApiService: - ExplorationRecommendationsBackendApiService) { + private expRecommendationBackendApiService: ExplorationRecommendationsBackendApiService + ) { this.isIframed = this.urlService.isIframed(); - this.isInEditorPage = ( + this.isInEditorPage = this.contextService.getPageContext() === - ServicesConstants.PAGE_CONTEXT.EXPLORATION_EDITOR); - this.isInEditorPreviewMode = ( - this.isInEditorPage && ( - this.contextService.getEditorTabContext() === - ServicesConstants.EXPLORATION_EDITOR_TAB_CONTEXT.PREVIEW)); + ServicesConstants.PAGE_CONTEXT.EXPLORATION_EDITOR; + this.isInEditorPreviewMode = + this.isInEditorPage && + this.contextService.getEditorTabContext() === + ServicesConstants.EXPLORATION_EDITOR_TAB_CONTEXT.PREVIEW; } getRecommendedSummaryDicts( - authorRecommendedExpIds: string[], - includeAutogeneratedRecommendations: boolean, - successCallback: (value: LearnerExplorationSummary[]) => void): void { + authorRecommendedExpIds: string[], + includeAutogeneratedRecommendations: boolean, + successCallback: (value: LearnerExplorationSummary[]) => void + ): void { let collectionId = this.urlService.getCollectionIdFromExplorationUrl(); let storyId = this.urlService.getUrlParams().story_id; let currentNodeId = this.urlService.getUrlParams().node_id; @@ -67,19 +66,27 @@ export class ExplorationRecommendationsService { let includeSystemRecommendations = 'false'; - if ( - includeAutogeneratedRecommendations && - !this.isInEditorPage) { + if (includeAutogeneratedRecommendations && !this.isInEditorPage) { includeSystemRecommendations = 'true'; } - this.expRecommendationBackendApiService.getRecommendedSummaryDictsAsync( - authorRecommendedExpIds, includeSystemRecommendations, - collectionId, storyId, currentNodeId, this.explorationId - ).then(expSummaries => { - successCallback(expSummaries); - }); + this.expRecommendationBackendApiService + .getRecommendedSummaryDictsAsync( + authorRecommendedExpIds, + includeSystemRecommendations, + collectionId, + storyId, + currentNodeId, + this.explorationId + ) + .then(expSummaries => { + successCallback(expSummaries); + }); } } -angular.module('oppia').factory('ExplorationRecommendationsService', - downgradeInjectable(ExplorationRecommendationsService)); +angular + .module('oppia') + .factory( + 'ExplorationRecommendationsService', + downgradeInjectable(ExplorationRecommendationsService) + ); diff --git a/core/templates/pages/exploration-player-page/services/extract-image-filenames-from-model.service.spec.ts b/core/templates/pages/exploration-player-page/services/extract-image-filenames-from-model.service.spec.ts index 165a5472d6d9..a5d30bb801cd 100644 --- a/core/templates/pages/exploration-player-page/services/extract-image-filenames-from-model.service.spec.ts +++ b/core/templates/pages/exploration-player-page/services/extract-image-filenames-from-model.service.spec.ts @@ -1,4 +1,3 @@ - // Copyright 2017 The Oppia Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the 'License'); @@ -17,20 +16,24 @@ * @fileoverview Unit tests for the extracting image files in state service. */ -import { TestBed } from '@angular/core/testing'; +import {TestBed} from '@angular/core/testing'; -import { CamelCaseToHyphensPipe } from - 'filters/string-utility-filters/camel-case-to-hyphens.pipe'; -import { ContextService } from 'services/context.service'; -import { ExplorationBackendDict, ExplorationObjectFactory } from - 'domain/exploration/ExplorationObjectFactory'; -import { ExtractImageFilenamesFromModelService } from +import {CamelCaseToHyphensPipe} from 'filters/string-utility-filters/camel-case-to-hyphens.pipe'; +import {ContextService} from 'services/context.service'; +import { + ExplorationBackendDict, + ExplorationObjectFactory, +} from 'domain/exploration/ExplorationObjectFactory'; +import { + ExtractImageFilenamesFromModelService, // eslint-disable-next-line max-len - 'pages/exploration-player-page/services/extract-image-filenames-from-model.service'; - -import { SkillBackendDict, SkillObjectFactory } from 'domain/skill/SkillObjectFactory'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; +} from 'pages/exploration-player-page/services/extract-image-filenames-from-model.service'; +import { + SkillBackendDict, + SkillObjectFactory, +} from 'domain/skill/SkillObjectFactory'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; describe('Extracting Image file names in the state service', () => { let eifms: ExtractImageFilenamesFromModelService; @@ -38,13 +41,13 @@ describe('Extracting Image file names in the state service', () => { let sof: SkillObjectFactory; let ecs: ContextService; let explorationDict: ExplorationBackendDict; - let ImageFilenamesInExploration: { [x: string]: string[] }; + let ImageFilenamesInExploration: {[x: string]: string[]}; let skillDict: SkillBackendDict; let expectedImageFilenamesInSkill: string[]; beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [CamelCaseToHyphensPipe] + providers: [CamelCaseToHyphensPipe], }); eof = TestBed.inject(ExplorationObjectFactory); ecs = TestBed.inject(ContextService); @@ -66,57 +69,57 @@ describe('Extracting Image file names in the state service', () => { param_changes: [], content: { content_id: 'content', - html: '' + html: '', }, recorded_voiceovers: { voiceovers_mapping: { content: {}, - default_outcome: {} - } + default_outcome: {}, + }, }, interaction: { id: 'Continue', default_outcome: { feedback: { content_id: 'default_outcome', - html: '' + html: '', }, dest: 'State 3', dest_if_really_stuck: null, param_changes: [], labelled_as_correct: false, refresher_exploration_id: null, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, confirmed_unclassified_answers: [], customization_args: { buttonText: { value: { content_id: 'ca_buttonText_0', - unicode_str: 'Continue' - } - } + unicode_str: 'Continue', + }, + }, }, solution: null, answer_groups: [], - hints: [] + hints: [], }, linked_skill_id: null, solicit_answer_details: false, classifier_model_id: null, - card_is_checkpoint: false + card_is_checkpoint: false, }, 'State 3': { param_changes: [], content: { content_id: 'content', - html: 'Congratulations, you have finished!' + html: 'Congratulations, you have finished!', }, recorded_voiceovers: { voiceovers_mapping: { content: {}, - default_outcome: {} - } + default_outcome: {}, + }, }, interaction: { id: 'EndExploration', @@ -124,32 +127,32 @@ describe('Extracting Image file names in the state service', () => { confirmed_unclassified_answers: [], customization_args: { recommendedExplorationIds: { - value: [] - } + value: [], + }, }, solution: null, answer_groups: [], - hints: [] + hints: [], }, linked_skill_id: null, solicit_answer_details: false, classifier_model_id: null, - card_is_checkpoint: false + card_is_checkpoint: false, }, Introduction: { classifier_model_id: null, param_changes: [], content: { content_id: 'content', - html: 'Multiple Choice' + html: 'Multiple Choice', }, recorded_voiceovers: { voiceovers_mapping: { content: {}, default_outcome: {}, feedback_1: {}, - feedback_2: {} - } + feedback_2: {}, + }, }, interaction: { id: 'MultipleChoiceInput', @@ -158,29 +161,34 @@ describe('Extracting Image file names in the state service', () => { dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: 'Try Again!' + html: 'Try Again!', }, labelled_as_correct: false, param_changes: [], refresher_exploration_id: null, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, confirmed_unclassified_answers: [], customization_args: { choices: { - value: [{ - content_id: 'ca_choices_3', - html: '

Go to ItemSelection

' - }, { - content_id: 'ca_choices_4', - html: '

Go to ImageAndRegion

' - }] - }, - showChoicesInShuffledOrder: {value: false} + value: [ + { + content_id: 'ca_choices_3', + html: + '

Go to ItemSelection

', + }, + { + content_id: 'ca_choices_4', + html: + '

Go to ImageAndRegion

', + }, + ], + }, + showChoicesInShuffledOrder: {value: false}, }, answer_groups: [ { @@ -189,22 +197,25 @@ describe('Extracting Image file names in the state service', () => { dest_if_really_stuck: null, feedback: { content_id: 'feedback_1', - html: '

We are going to ItemSelection' + - '' + - '

' + html: + '

We are going to ItemSelection' + + '' + + '

', }, param_changes: [], refresher_exploration_id: null, missing_prerequisite_skill_id: null, - labelled_as_correct: false + labelled_as_correct: false, }, - rule_specs: [{ - rule_type: 'Equals', - inputs: {x: 0} - }], + rule_specs: [ + { + rule_type: 'Equals', + inputs: {x: 0}, + }, + ], tagged_skill_misconception_id: null, - training_data: [] + training_data: [], }, { outcome: { @@ -212,82 +223,91 @@ describe('Extracting Image file names in the state service', () => { dest_if_really_stuck: null, feedback: { content_id: 'feedback_2', - html: "Let's go to state 5 ImageAndRegion" + html: "Let's go to state 5 ImageAndRegion", }, param_changes: [], refresher_exploration_id: null, missing_prerequisite_skill_id: null, - labelled_as_correct: false + labelled_as_correct: false, }, - rule_specs: [{ - rule_type: 'Equals', - inputs: {x: 1} - }], + rule_specs: [ + { + rule_type: 'Equals', + inputs: {x: 1}, + }, + ], tagged_skill_misconception_id: null, - training_data: [] - } + training_data: [], + }, ], hints: [], - solution: null + solution: null, }, linked_skill_id: null, solicit_answer_details: false, - card_is_checkpoint: false + card_is_checkpoint: false, }, 'State 4': { param_changes: [], content: { content_id: 'content', - html: '

' + - '

' + html: + '

' + + '

', }, recorded_voiceovers: { voiceovers_mapping: { content: {}, default_outcome: {}, feedback_1: {}, - feedback_2: {} - } + feedback_2: {}, + }, }, interaction: { id: 'ItemSelectionInput', default_outcome: { feedback: { content_id: 'content', - html: '

Try Again! ' + - '

' + html: + '

Try Again! ' + + '

', }, dest: 'State 4', dest_if_really_stuck: null, param_changes: [], labelled_as_correct: false, refresher_exploration_id: null, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, confirmed_unclassified_answers: [], customization_args: { minAllowableSelectionCount: { - value: 1 + value: 1, }, maxAllowableSelectionCount: { - value: 2 + value: 2, }, choices: { - value: [{ - content_id: 'ca_choices_3', - html: '

' + - '

' - }, { - content_id: 'ca_choices_4', - html: '

' + - '

' - }] - } + value: [ + { + content_id: 'ca_choices_3', + html: + '

' + + '

', + }, + { + content_id: 'ca_choices_4', + html: + '

' + + '

', + }, + ], + }, }, hints: [], solution: null, @@ -298,25 +318,27 @@ describe('Extracting Image file names in the state service', () => { dest_if_really_stuck: null, feedback: { content_id: 'feedback_1', - html: "It is choice number 1. Let's go to the Text Input" + html: "It is choice number 1. Let's go to the Text Input", }, param_changes: [], refresher_exploration_id: null, labelled_as_correct: false, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, - rule_specs: [{ - rule_type: 'Equals', - inputs: { - x: [ - '

' - ] - } - }], + rule_specs: [ + { + rule_type: 'Equals', + inputs: { + x: [ + '

', + ], + }, + }, + ], tagged_skill_misconception_id: null, - training_data: [] + training_data: [], }, { outcome: { @@ -324,39 +346,41 @@ describe('Extracting Image file names in the state service', () => { dest_if_really_stuck: null, feedback: { content_id: 'feedback_2', - html: 'It is choice number 2' + html: 'It is choice number 2', }, param_changes: [], refresher_exploration_id: null, labelled_as_correct: true, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, - rule_specs: [{ - rule_type: 'Equals', - inputs: { - x: [ - '

' + - '

' - ] - } - }], + rule_specs: [ + { + rule_type: 'Equals', + inputs: { + x: [ + '

' + + '

', + ], + }, + }, + ], tagged_skill_misconception_id: null, - training_data: [] - } - ] + training_data: [], + }, + ], }, linked_skill_id: null, solicit_answer_details: false, classifier_model_id: null, - card_is_checkpoint: false + card_is_checkpoint: false, }, 'State 5': { classifier_model_id: null, param_changes: [], content: { content_id: 'content', - html: '' + html: '', }, recorded_voiceovers: { voiceovers_mapping: { @@ -366,8 +390,8 @@ describe('Extracting Image file names in the state service', () => { feedback_2: {}, feedback_3: {}, feedback_4: {}, - feedback_5: {} - } + feedback_5: {}, + }, }, interaction: { id: 'ImageClickInput', @@ -377,12 +401,12 @@ describe('Extracting Image file names in the state service', () => { dest_if_really_stuck: null, feedback: { content_id: 'content', - html: 'Try Again!' + html: 'Try Again!', }, param_changes: [], refresher_exploration_id: null, labelled_as_correct: true, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, answer_groups: [ { @@ -391,19 +415,21 @@ describe('Extracting Image file names in the state service', () => { dest_if_really_stuck: null, feedback: { content_id: 'feeedback_1', - html: '

That is the class definition. Try again.

' + html: '

That is the class definition. Try again.

', }, param_changes: [], refresher_exploration_id: null, labelled_as_correct: false, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, - rule_specs: [{ - rule_type: 'IsInRegion', - inputs: { x: 'classdef' } - }], + rule_specs: [ + { + rule_type: 'IsInRegion', + inputs: {x: 'classdef'}, + }, + ], tagged_skill_misconception_id: null, - training_data: [] + training_data: [], }, { outcome: { @@ -411,20 +437,23 @@ describe('Extracting Image file names in the state service', () => { dest_if_really_stuck: null, feedback: { content_id: 'feeedback_2', - html: '

That is a function, which is close to what you' + - 'are looking for. Try again!

' + html: + '

That is a function, which is close to what you' + + 'are looking for. Try again!

', }, param_changes: [], refresher_exploration_id: null, labelled_as_correct: false, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, - rule_specs: [{ - rule_type: 'IsInRegion', - inputs: { x: 'instancefunc' } - }], + rule_specs: [ + { + rule_type: 'IsInRegion', + inputs: {x: 'instancefunc'}, + }, + ], tagged_skill_misconception_id: null, - training_data: [] + training_data: [], }, { outcome: { @@ -432,19 +461,21 @@ describe('Extracting Image file names in the state service', () => { dest_if_really_stuck: null, feedback: { content_id: 'feeedback_3', - html: '

That is the class docstring. Try again.

' + html: '

That is the class docstring. Try again.

', }, param_changes: [], refresher_exploration_id: null, labelled_as_correct: false, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, - rule_specs: [{ - rule_type: 'IsInRegion', - inputs: { x: 'docstring' } - }], + rule_specs: [ + { + rule_type: 'IsInRegion', + inputs: {x: 'docstring'}, + }, + ], tagged_skill_misconception_id: null, - training_data: [] + training_data: [], }, { outcome: { @@ -452,20 +483,23 @@ describe('Extracting Image file names in the state service', () => { dest_if_really_stuck: null, feedback: { content_id: 'feeedback_4', - html: "

That's a classmethod. It does execute code," + - "but it doesn't construct anything. Try again!

" + html: + "

That's a classmethod. It does execute code," + + "but it doesn't construct anything. Try again!

", }, param_changes: [], refresher_exploration_id: null, labelled_as_correct: false, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, - rule_specs: [{ - rule_type: 'IsInRegion', - inputs: { x: 'classfunc' } - }], + rule_specs: [ + { + rule_type: 'IsInRegion', + inputs: {x: 'classfunc'}, + }, + ], tagged_skill_misconception_id: null, - training_data: [] + training_data: [], }, { outcome: { @@ -473,84 +507,89 @@ describe('Extracting Image file names in the state service', () => { dest_if_really_stuck: null, feedback: { content_id: 'feeedback_5', - html: '

You found it! This is the code responsible for' + - 'constructing a new class object.

' + html: + '

You found it! This is the code responsible for' + + 'constructing a new class object.

', }, param_changes: [], refresher_exploration_id: null, labelled_as_correct: false, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, - rule_specs: [{ - rule_type: 'IsInRegion', - inputs: { x: 'ctor' } - }], + rule_specs: [ + { + rule_type: 'IsInRegion', + inputs: {x: 'ctor'}, + }, + ], tagged_skill_misconception_id: null, - training_data: [] - } + training_data: [], + }, ], customization_args: { highlightRegionsOnHover: { - value: true + value: true, }, imageAndRegions: { value: { imagePath: 's5ImagePath.png', - labeledRegions: [{ - label: 'classdef', - region: { - area: [ - [0.004291845493562232, 0.004692192192192192], - [0.40987124463519314, 0.05874624624624625] - ], - regionType: 'Rectangle' - } - }, - { - label: 'docstring', - region: { - area: [ - [0.07296137339055794, 0.06475225225225226], - [0.9892703862660944, 0.1218093093093093] - ], - regionType: 'Rectangle' - } - }, - { - label: 'instancefunc', - region: { - area: [ - [0.07296137339055794, 0.15183933933933935], - [0.6995708154506438, 0.44012762762762764] - ], - regionType: 'Rectangle' - } - }, - { - label: 'classfunc', - region: { - area: [ - [0.06866952789699571, 0.46114864864864863], - [0.6931330472103004, 0.776463963963964] - ], - regionType: 'Rectangle' - } - }, - { - label: 'ctor', - region: { - area: [ - [0.06437768240343347, 0.821509009009009], - [0.740343347639485, 0.9926801801801802] - ], - regionType: 'Rectangle' - } - }] - } - } + labeledRegions: [ + { + label: 'classdef', + region: { + area: [ + [0.004291845493562232, 0.004692192192192192], + [0.40987124463519314, 0.05874624624624625], + ], + regionType: 'Rectangle', + }, + }, + { + label: 'docstring', + region: { + area: [ + [0.07296137339055794, 0.06475225225225226], + [0.9892703862660944, 0.1218093093093093], + ], + regionType: 'Rectangle', + }, + }, + { + label: 'instancefunc', + region: { + area: [ + [0.07296137339055794, 0.15183933933933935], + [0.6995708154506438, 0.44012762762762764], + ], + regionType: 'Rectangle', + }, + }, + { + label: 'classfunc', + region: { + area: [ + [0.06866952789699571, 0.46114864864864863], + [0.6931330472103004, 0.776463963963964], + ], + regionType: 'Rectangle', + }, + }, + { + label: 'ctor', + region: { + area: [ + [0.06437768240343347, 0.821509009009009], + [0.740343347639485, 0.9926801801801802], + ], + regionType: 'Rectangle', + }, + }, + ], + }, + }, }, hints: [], - solution: null + solution: null, }, solicit_answer_details: false, linked_skill_id: null, @@ -560,7 +599,7 @@ describe('Extracting Image file names in the state service', () => { param_changes: [], content: { content_id: 'content', - html: '

Text Input Content

' + html: '

Text Input Content

', }, recorded_voiceovers: { voiceovers_mapping: { @@ -569,8 +608,8 @@ describe('Extracting Image file names in the state service', () => { feedback_1: {}, feedback_2: {}, hint_1: {}, - solution: {} - } + solution: {}, + }, }, interaction: { id: 'TextInput', @@ -579,168 +618,188 @@ describe('Extracting Image file names in the state service', () => { dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: '

Try again.

' + html: '

Try again.

', }, labelled_as_correct: false, param_changes: [], refresher_exploration_id: null, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, confirmed_unclassified_answers: [], customization_args: { rows: { - value: 1 + value: 1, }, placeholder: { value: { content_id: 'ca_placeholder_3', - unicode_str: '' - } + unicode_str: '', + }, }, catchMisspellings: { - value: false - } + value: false, + }, }, - answer_groups: [{ - rule_specs: [{ - rule_type: 'Contains', - inputs: {x: { - contentId: 'rule_input', - normalizedStrSet: ['1'] - }} - }], - outcome: { - dest: 'State 1', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_1', - html: "

Let's go to State 1

" + answer_groups: [ + { + rule_specs: [ + { + rule_type: 'Contains', + inputs: { + x: { + contentId: 'rule_input', + normalizedStrSet: ['1'], + }, + }, + }, + ], + outcome: { + dest: 'State 1', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: "

Let's go to State 1

", + }, + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, }, - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null - }, - tagged_skill_misconception_id: null, - training_data: [] - }, { - rule_specs: [{ - rule_type: 'Contains', - inputs: {x: { - contentId: 'rule_input', - normalizedStrSet: ['2'] - }} - }], - outcome: { - dest: 'State 1', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: '

Let\'s go to State 1

' + tagged_skill_misconception_id: null, + training_data: [], + }, + { + rule_specs: [ + { + rule_type: 'Contains', + inputs: { + x: { + contentId: 'rule_input', + normalizedStrSet: ['2'], + }, + }, + }, + ], + outcome: { + dest: 'State 1', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: + "

Let's go to State 1

', + }, + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + }, + tagged_skill_misconception_id: null, + training_data: [], + }, + ], + hints: [ + { + hint_content: { + content_id: 'hint_1', + html: + '

' + + '

', }, - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null - }, - tagged_skill_misconception_id: null, - training_data: [] - }], - hints: [{ - hint_content: { - content_id: 'hint_1', - html: '

' + - '

' - } - }], + }, + ], solution: { answer_is_exclusive: false, correct_answer: 'cat', explanation: { content_id: 'solution', - html: '

' + - '

' - } + html: + '

' + + '

', + }, }, }, linked_skill_id: null, solicit_answer_details: false, classifier_model_id: null, - card_is_checkpoint: false + card_is_checkpoint: false, }, 'State 7': { param_changes: [], content: { content_id: 'content', - html: '

' + - '' + - '' + - '' + - '

' + html: + '

' + + '' + + '' + + '' + + '

', }, recorded_voiceovers: { voiceovers_mapping: { content: {}, default_outcome: {}, feedback_1: {}, - feedback_2: {} - } + feedback_2: {}, + }, }, interaction: { id: 'ItemSelectionInput', default_outcome: { feedback: { content_id: 'content', - html: '

Try again!

' + html: '

Try again!

', }, dest: 'State 4', dest_if_really_stuck: null, param_changes: [], labelled_as_correct: false, refresher_exploration_id: null, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, confirmed_unclassified_answers: [], customization_args: { minAllowableSelectionCount: { - value: 1 + value: 1, }, maxAllowableSelectionCount: { - value: 2 + value: 2, }, choices: { - value: [{ - content_id: 'ca_choices_3', - html: '

Choice 1

' - }, { - content_id: 'ca_choices_4', - html: '

Choice 2

' - }] - } + value: [ + { + content_id: 'ca_choices_3', + html: '

Choice 1

', + }, + { + content_id: 'ca_choices_4', + html: '

Choice 2

', + }, + ], + }, }, hints: [], solution: null, @@ -751,23 +810,23 @@ describe('Extracting Image file names in the state service', () => { dest_if_really_stuck: null, feedback: { content_id: 'feedback_1', - html: 'It is choice number 1.' + html: 'It is choice number 1.', }, param_changes: [], refresher_exploration_id: null, labelled_as_correct: false, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, - rule_specs: [{ - rule_type: 'Equals', - inputs: { - x: [ - '

Choice 1

' - ] - } - }], + rule_specs: [ + { + rule_type: 'Equals', + inputs: { + x: ['

Choice 1

'], + }, + }, + ], tagged_skill_misconception_id: null, - training_data: [] + training_data: [], }, { outcome: { @@ -775,31 +834,31 @@ describe('Extracting Image file names in the state service', () => { dest_if_really_stuck: null, feedback: { content_id: 'feedback_2', - html: 'It is choice number 2' + html: 'It is choice number 2', }, param_changes: [], refresher_exploration_id: null, labelled_as_correct: false, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, - rule_specs: [{ - rule_type: 'Equals', - inputs: { - x: [ - '

Choice 2

' - ] - } - }], + rule_specs: [ + { + rule_type: 'Equals', + inputs: { + x: ['

Choice 2

'], + }, + }, + ], tagged_skill_misconception_id: null, - training_data: [] - } - ] + training_data: [], + }, + ], }, solicit_answer_details: false, classifier_model_id: null, card_is_checkpoint: false, - linked_skill_id: null - } + linked_skill_id: null, + }, }, param_specs: {}, param_changes: [], @@ -817,62 +876,67 @@ describe('Extracting Image file names in the state service', () => { param_specs: {}, param_changes: [], auto_tts_enabled: false, - edits_allowed: true - } + edits_allowed: true, + }, }; ImageFilenamesInExploration = { 'State 1': [], 'State 3': [], - 'State 4': ['s4Content.png', 's4Choice1.png', 's4Choice2.png', - 's4DefaultOutcomeFeedback.png'], + 'State 4': [ + 's4Content.png', + 's4Choice1.png', + 's4Choice2.png', + 's4DefaultOutcomeFeedback.png', + ], 'State 5': ['s5ImagePath.png'], 'State 6': [ - 's6Hint1.png', 's6SolutionExplanation.png', + 's6Hint1.png', + 's6SolutionExplanation.png', 'mathImg_20207261338jhi1j6rvob_height_1d34' + - '5_width_3d124_vertical_0d124.svg'], + '5_width_3d124_vertical_0d124.svg', + ], 'State 7': ['s7Image.png', 's7CollapsibleImage.png', 's7TabImage.png'], - Introduction: ['sIMultipleChoice1.png', 'sIMultipleChoice2.png', - 'sIOutcomeFeedback.png'] + Introduction: [ + 'sIMultipleChoice1.png', + 'sIMultipleChoice2.png', + 'sIOutcomeFeedback.png', + ], }; const misconceptionDict1 = { id: 2, name: 'test name 1', - notes: ( + notes: '

This is a text ' + - 'input.

' - ), - feedback: ( + 'input.

', + feedback: '

This is a text ' + - 'input.

' - ), + 'input.

', must_be_addressed: true, }; const misconceptionDict2 = { id: 4, name: 'test name 2', - notes: ( + notes: '

This is a text ' + - 'input.

' - ), - feedback: ( + 'input.

', + feedback: '

This is a text ' + - 'input.

' - ), + 'input.

', must_be_addressed: true, }; @@ -880,68 +944,63 @@ describe('Extracting Image file names in the state service', () => { difficulty: 'Easy', explanations: [ '

This is a text ' + - 'input.

' + 'quot;f&quot;" caption-with-value="&quot;&quot;"' + + 'filepath-with-value="&quot;rubric-dict-easy-explanation' + + '.png&quot;">This is a text ' + + 'input.

', ], }; const example1 = { question: { - html: ( + html: '

This is a text ' + - 'input.

' - ), + 'input.

', content_id: 'worked_example_q_1', }, explanation: { - html: ( + html: '

This is a text ' + - 'input.

' - ), + 'input.

', content_id: 'worked_example_e_1', }, }; const example2 = { question: { - html: ( + html: '

This is a text ' + - 'input.

' - ), + 'input.

', content_id: 'worked_example_q_2', }, explanation: { - html: ( + html: '

This is a text ' + - 'input.

' - ), + 'input.

', content_id: 'worked_example_e_2', }, }; const skillContentsDict = { explanation: { - html: ( + html: '

This is a text ' + - 'input.

' - ), + 'input.

', content_id: 'explanation', }, worked_examples: [example1, example2], @@ -967,7 +1026,7 @@ describe('Extracting Image file names in the state service', () => { prerequisite_skill_ids: ['skill_1'], all_questions_merged: false, next_misconception_id: 0, - superseding_skill_id: '' + superseding_skill_id: '', }; expectedImageFilenamesInSkill = [ 'misconception-dict-1-notes.png', @@ -979,29 +1038,27 @@ describe('Extracting Image file names in the state service', () => { 'worked-example-1-explanation.png', 'worked-example-2-question.png', 'worked-example-2-explanation.png', - 'skill-concept-card-explanation.png' + 'skill-concept-card-explanation.png', ]; }); - it('should get all the filenames of the images in a state', - () => { - let exploration = eof.createFromBackendDict(explorationDict); - let states = exploration.getStates(); - let stateNames = states.getStateNames(); - stateNames.forEach((statename) => { - let filenamesInState = ( - eifms.getImageFilenamesInState(states.getState(statename))); - filenamesInState.forEach(function(filename) { - expect(ImageFilenamesInExploration[statename]).toContain(filename); - }); + it('should get all the filenames of the images in a state', () => { + let exploration = eof.createFromBackendDict(explorationDict); + let states = exploration.getStates(); + let stateNames = states.getStateNames(); + stateNames.forEach(statename => { + let filenamesInState = eifms.getImageFilenamesInState( + states.getState(statename) + ); + filenamesInState.forEach(function (filename) { + expect(ImageFilenamesInExploration[statename]).toContain(filename); }); }); + }); - it('should get all the filenames of the images in a skill', - () => { - let skill = sof.createFromBackendDict(skillDict); - let imageFilenamesInSkill = eifms.getImageFilenamesInSkill(skill).sort(); - expect(imageFilenamesInSkill).toEqual( - expectedImageFilenamesInSkill.sort()); - }); + it('should get all the filenames of the images in a skill', () => { + let skill = sof.createFromBackendDict(skillDict); + let imageFilenamesInSkill = eifms.getImageFilenamesInSkill(skill).sort(); + expect(imageFilenamesInSkill).toEqual(expectedImageFilenamesInSkill.sort()); + }); }); diff --git a/core/templates/pages/exploration-player-page/services/extract-image-filenames-from-model.service.ts b/core/templates/pages/exploration-player-page/services/extract-image-filenames-from-model.service.ts index 599fa7d488b5..7271ae168590 100644 --- a/core/templates/pages/exploration-player-page/services/extract-image-filenames-from-model.service.ts +++ b/core/templates/pages/exploration-player-page/services/extract-image-filenames-from-model.service.ts @@ -16,19 +16,18 @@ * @fileoverview Service to extract image filenames in a State. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { ContentTranslationLanguageService } from - 'pages/exploration-player-page/services/content-translation-language.service'; -import { HtmlEscaperService } from 'services/html-escaper.service'; -import { State } from 'domain/state/StateObjectFactory'; -import { Skill } from 'domain/skill/SkillObjectFactory'; -import { ImageClickInputCustomizationArgs } from 'interactions/customization-args-defs'; -import { EntityTranslationsService } from 'services/entity-translations.services'; +import {ContentTranslationLanguageService} from 'pages/exploration-player-page/services/content-translation-language.service'; +import {HtmlEscaperService} from 'services/html-escaper.service'; +import {State} from 'domain/state/StateObjectFactory'; +import {Skill} from 'domain/skill/SkillObjectFactory'; +import {ImageClickInputCustomizationArgs} from 'interactions/customization-args-defs'; +import {EntityTranslationsService} from 'services/entity-translations.services'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ExtractImageFilenamesFromModelService { constructor( @@ -51,23 +50,27 @@ export class ExtractImageFilenamesFromModelService { * filepath should be extracted. */ _extractFilepathValueFromOppiaNonInteractiveImageTag( - htmlString: string + htmlString: string ): string[] { let filenames = []; - let unescapedHtmlString = ( - this.htmlEscaperService.escapedStrToUnescapedStr(htmlString)); - let dummyDocument = ( - new DOMParser().parseFromString(unescapedHtmlString, 'text/html')); + let unescapedHtmlString = + this.htmlEscaperService.escapedStrToUnescapedStr(htmlString); + let dummyDocument = new DOMParser().parseFromString( + unescapedHtmlString, + 'text/html' + ); let imageTagLists = []; // Add images that are in the base content (not embedded). imageTagLists.push( - dummyDocument.getElementsByTagName('oppia-noninteractive-image')); + dummyDocument.getElementsByTagName('oppia-noninteractive-image') + ); // Add images that are embedded in collapsibles. let collapsibleTagList = dummyDocument.getElementsByTagName( - 'oppia-noninteractive-collapsible'); + 'oppia-noninteractive-collapsible' + ); for (let i = 0; i < collapsibleTagList.length; i++) { // Get attribute from a collapsible. If the given attribute does not // exist, then attribute is null which means skip the current @@ -76,17 +79,20 @@ export class ExtractImageFilenamesFromModelService { let attribute = collapsibleTagList[i].getAttribute('content-with-value'); if (attribute !== null) { let contentWithValue = JSON.parse(attribute); - let collapsibleDocument = ( - new DOMParser().parseFromString(contentWithValue, 'text/html')); + let collapsibleDocument = new DOMParser().parseFromString( + contentWithValue, + 'text/html' + ); imageTagLists.push( - collapsibleDocument.getElementsByTagName( - 'oppia-noninteractive-image')); + collapsibleDocument.getElementsByTagName('oppia-noninteractive-image') + ); } } // Add images that are embedded in tabs. let tabsTagList = dummyDocument.getElementsByTagName( - 'oppia-noninteractive-tabs'); + 'oppia-noninteractive-tabs' + ); for (let i = 0; i < tabsTagList.length; i++) { // Get attribute from a tab. If the given attribute does not exist, // then attribute is null which means skip the current tab. If the @@ -96,11 +102,13 @@ export class ExtractImageFilenamesFromModelService { if (attribute !== null) { let contentsWithValue = JSON.parse(attribute); for (let contentWithValue of contentsWithValue) { - let tabDocument = ( - new DOMParser().parseFromString( - contentWithValue.content, 'text/html')); + let tabDocument = new DOMParser().parseFromString( + contentWithValue.content, + 'text/html' + ); imageTagLists.push( - tabDocument.getElementsByTagName('oppia-noninteractive-image')); + tabDocument.getElementsByTagName('oppia-noninteractive-image') + ); } } } @@ -119,8 +127,8 @@ export class ExtractImageFilenamesFromModelService { let attribute = imageTagList[i].getAttribute('filepath-with-value'); if (attribute !== null) { let filename = JSON.parse( - this.htmlEscaperService.escapedStrToUnescapedStr( - attribute)); + this.htmlEscaperService.escapedStrToUnescapedStr(attribute) + ); filenames.push(filename); } } @@ -135,16 +143,19 @@ export class ExtractImageFilenamesFromModelService { * filepath should be extracted. */ _extractSvgFilenameFromOppiaNonInteractiveMathTag( - htmlString: string + htmlString: string ): string[] { let filenames = []; - let unescapedHtmlString = ( - this.htmlEscaperService.escapedStrToUnescapedStr(htmlString)); - let dummyDocument = ( - new DOMParser().parseFromString(unescapedHtmlString, 'text/html')); + let unescapedHtmlString = + this.htmlEscaperService.escapedStrToUnescapedStr(htmlString); + let dummyDocument = new DOMParser().parseFromString( + unescapedHtmlString, + 'text/html' + ); let mathTagList = dummyDocument.getElementsByTagName( - 'oppia-noninteractive-math'); + 'oppia-noninteractive-math' + ); for (let i = 0; i < mathTagList.length; i++) { // Get attribute from a mathTag. If the given attribute does not exist, // then attribute is null which means skip the current mathTag. @@ -169,18 +180,17 @@ export class ExtractImageFilenamesFromModelService { // directly stored in the customizationArgs.imageAndRegion.value // .imagePath. if (state.interaction.id === this.INTERACTION_TYPE_IMAGE_CLICK_INPUT) { - let filename = (( + let filename = ( state.interaction.customizationArgs as ImageClickInputCustomizationArgs - ).imageAndRegions.value.imagePath); + ).imageAndRegions.value.imagePath; filenamesInState.push(filename); } let allHtmlOfState: string[] = state.getAllHTMLStrings(); - let htmlTranslations: string[] = ( + let htmlTranslations: string[] = this.entityTranslationsService.getHtmlTranslations( this.contentTranslationLanguageService.getCurrentContentLanguageCode(), state.getAllContentIds() - ) - ); + ); return [ ...filenamesInState, ...this._extractFilenamesFromHtmlList(allHtmlOfState), @@ -200,7 +210,9 @@ export class ExtractImageFilenamesFromModelService { } for (let workedExample of skill.getConceptCard().getWorkedExamples()) { htmlList.push( - workedExample.getExplanation().html, workedExample.getQuestion().html); + workedExample.getExplanation().html, + workedExample.getQuestion().html + ); } htmlList.push(skill.getConceptCard().getExplanation().html); skill.getRubrics().forEach(rubric => { @@ -211,10 +223,11 @@ export class ExtractImageFilenamesFromModelService { _extractFilenamesFromHtmlList(htmlList: string[]): string[] { let filenames: string[] = []; - htmlList.forEach((htmlStr) => { + htmlList.forEach(htmlStr => { filenames.push( ...this._extractFilepathValueFromOppiaNonInteractiveImageTag(htmlStr), - ...this._extractSvgFilenameFromOppiaNonInteractiveMathTag(htmlStr)); + ...this._extractSvgFilenameFromOppiaNonInteractiveMathTag(htmlStr) + ); }); return filenames; } @@ -223,7 +236,9 @@ export class ExtractImageFilenamesFromModelService { getImageFilenamesInState = this._getImageFilenamesInState; } - -angular.module('oppia').factory( - 'ExtractImageFilenamesFromModelService', - downgradeInjectable(ExtractImageFilenamesFromModelService)); +angular + .module('oppia') + .factory( + 'ExtractImageFilenamesFromModelService', + downgradeInjectable(ExtractImageFilenamesFromModelService) + ); diff --git a/core/templates/pages/exploration-player-page/services/fatigue-detection-service.spec.ts b/core/templates/pages/exploration-player-page/services/fatigue-detection-service.spec.ts index b1739501b9b7..7880268df945 100644 --- a/core/templates/pages/exploration-player-page/services/fatigue-detection-service.spec.ts +++ b/core/templates/pages/exploration-player-page/services/fatigue-detection-service.spec.ts @@ -16,11 +16,9 @@ * @fileoverview Unit tests for the Fatigue Detection service. */ - - -import { TestBed } from '@angular/core/testing'; -import { FatigueDetectionService } from 'pages/exploration-player-page/services/fatigue-detection.service'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import {TestBed} from '@angular/core/testing'; +import {FatigueDetectionService} from 'pages/exploration-player-page/services/fatigue-detection.service'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; describe('Fatigue detection service', () => { let fatigueDetectionService: FatigueDetectionService; @@ -42,37 +40,35 @@ describe('Fatigue detection service', () => { }); describe('isSubmittingTooFast', () => { - it('should return true for 4 or more submissions in under 10 seconds', - () => { - fatigueDetectionService.recordSubmissionTimestamp(); - jasmine.clock().tick(100); - fatigueDetectionService.recordSubmissionTimestamp(); - jasmine.clock().tick(100); - fatigueDetectionService.recordSubmissionTimestamp(); - jasmine.clock().tick(100); - fatigueDetectionService.recordSubmissionTimestamp(); - expect(fatigueDetectionService.isSubmittingTooFast()).toBe(true); - - jasmine.clock().tick(100); - fatigueDetectionService.recordSubmissionTimestamp(); - expect(fatigueDetectionService.isSubmittingTooFast()).toBe(true); - }); - - it('should return false for 4 or more submissions in over 10 seconds', - () => { - fatigueDetectionService.recordSubmissionTimestamp(); - jasmine.clock().tick(100); - fatigueDetectionService.recordSubmissionTimestamp(); - jasmine.clock().tick(100); - fatigueDetectionService.recordSubmissionTimestamp(); - jasmine.clock().tick(10000); - fatigueDetectionService.recordSubmissionTimestamp(); - expect(fatigueDetectionService.isSubmittingTooFast()).toBe(false); - - jasmine.clock().tick(100); - fatigueDetectionService.recordSubmissionTimestamp(); - expect(fatigueDetectionService.isSubmittingTooFast()).toBe(false); - }); + it('should return true for 4 or more submissions in under 10 seconds', () => { + fatigueDetectionService.recordSubmissionTimestamp(); + jasmine.clock().tick(100); + fatigueDetectionService.recordSubmissionTimestamp(); + jasmine.clock().tick(100); + fatigueDetectionService.recordSubmissionTimestamp(); + jasmine.clock().tick(100); + fatigueDetectionService.recordSubmissionTimestamp(); + expect(fatigueDetectionService.isSubmittingTooFast()).toBe(true); + + jasmine.clock().tick(100); + fatigueDetectionService.recordSubmissionTimestamp(); + expect(fatigueDetectionService.isSubmittingTooFast()).toBe(true); + }); + + it('should return false for 4 or more submissions in over 10 seconds', () => { + fatigueDetectionService.recordSubmissionTimestamp(); + jasmine.clock().tick(100); + fatigueDetectionService.recordSubmissionTimestamp(); + jasmine.clock().tick(100); + fatigueDetectionService.recordSubmissionTimestamp(); + jasmine.clock().tick(10000); + fatigueDetectionService.recordSubmissionTimestamp(); + expect(fatigueDetectionService.isSubmittingTooFast()).toBe(false); + + jasmine.clock().tick(100); + fatigueDetectionService.recordSubmissionTimestamp(); + expect(fatigueDetectionService.isSubmittingTooFast()).toBe(false); + }); it('should return false for less than 4 submissions', () => { fatigueDetectionService.recordSubmissionTimestamp(); @@ -108,7 +104,7 @@ describe('Fatigue detection service', () => { it('should display take break message', () => { const modalSpy = spyOn(ngbModal, 'open').and.callFake(() => { return { - result: Promise.resolve() + result: Promise.resolve(), } as NgbModalRef; }); fatigueDetectionService.displayTakeBreakMessage(); diff --git a/core/templates/pages/exploration-player-page/services/fatigue-detection.service.ts b/core/templates/pages/exploration-player-page/services/fatigue-detection.service.ts index 0242bafff952..1ec782e1090b 100644 --- a/core/templates/pages/exploration-player-page/services/fatigue-detection.service.ts +++ b/core/templates/pages/exploration-player-page/services/fatigue-detection.service.ts @@ -16,14 +16,13 @@ * @fileoverview Service for detecting spamming behavior from the learner. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { TakeBreakModalComponent } from 'pages/exploration-player-page/templates/take-break-modal.component'; - +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {TakeBreakModalComponent} from 'pages/exploration-player-page/templates/take-break-modal.component'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class FatigueDetectionService { private submissionTimesMsec: number[] = []; @@ -34,11 +33,10 @@ export class FatigueDetectionService { private windowStartTime!: number | undefined; private windowEndTime!: number; - constructor( - private ngbModal: NgbModal) { } + constructor(private ngbModal: NgbModal) {} recordSubmissionTimestamp(): void { - this.submissionTimesMsec.push((new Date()).getTime()); + this.submissionTimesMsec.push(new Date().getTime()); } isSubmittingTooFast(): boolean { @@ -46,8 +44,10 @@ export class FatigueDetectionService { this.windowStartTime = this.submissionTimesMsec.shift(); this.windowEndTime = this.submissionTimesMsec[this.submissionTimesMsec.length - 1]; - if (this.windowStartTime !== undefined && (this.windowEndTime.valueOf() - - this.windowStartTime.valueOf() < this.SPAM_WINDOW_MSEC) + if ( + this.windowStartTime !== undefined && + this.windowEndTime.valueOf() - this.windowStartTime.valueOf() < + this.SPAM_WINDOW_MSEC ) { return true; } @@ -56,15 +56,18 @@ export class FatigueDetectionService { } displayTakeBreakMessage(): void { - this.ngbModal.open( - TakeBreakModalComponent, - { - backdrop: 'static' - }).result.then(() => { }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + this.ngbModal + .open(TakeBreakModalComponent, { + backdrop: 'static', + }) + .result.then( + () => {}, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } reset(): void { @@ -72,6 +75,9 @@ export class FatigueDetectionService { } } -angular.module('oppia').factory( - 'FatigueDetectionService', - downgradeInjectable(FatigueDetectionService)); +angular + .module('oppia') + .factory( + 'FatigueDetectionService', + downgradeInjectable(FatigueDetectionService) + ); diff --git a/core/templates/pages/exploration-player-page/services/feedback-popup-backend-api.service.spec.ts b/core/templates/pages/exploration-player-page/services/feedback-popup-backend-api.service.spec.ts index e331e9fd9662..e3b8152c234c 100644 --- a/core/templates/pages/exploration-player-page/services/feedback-popup-backend-api.service.spec.ts +++ b/core/templates/pages/exploration-player-page/services/feedback-popup-backend-api.service.spec.ts @@ -16,12 +16,14 @@ * @fileoverview Unit tests for FeedbackPopupBackendApiService. */ -import { HttpClientTestingModule, HttpTestingController } from - '@angular/common/http/testing'; -import { fakeAsync, flushMicrotasks, TestBed } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; -import { MockTranslateService } from 'components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec'; -import { FeedbackPopupBackendApiService } from './feedback-popup-backend-api.service'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {fakeAsync, flushMicrotasks, TestBed} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; +import {MockTranslateService} from 'components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec'; +import {FeedbackPopupBackendApiService} from './feedback-popup-backend-api.service'; describe('Feedback Popup Backend Api Service', () => { let fbpas: FeedbackPopupBackendApiService; @@ -36,9 +38,9 @@ describe('Feedback Popup Backend Api Service', () => { FeedbackPopupBackendApiService, { provide: TranslateService, - useClass: MockTranslateService - } - ] + useClass: MockTranslateService, + }, + ], }); httpTestingController = TestBed.inject(HttpTestingController); fbpas = TestBed.inject(FeedbackPopupBackendApiService); @@ -52,7 +54,8 @@ describe('Feedback Popup Backend Api Service', () => { let feedback: string = 'test feedback'; let includeAuthor: boolean = true; let stateName: string = 'test state name'; - fbpas.submitFeedbackAsync(subject, feedback, includeAuthor, stateName) + fbpas + .submitFeedbackAsync(subject, feedback, includeAuthor, stateName) .then(successHandler, failHandler); let req = httpTestingController.expectOne(fbpas.feedbackUrl); expect(req.request.method).toEqual('POST'); @@ -62,6 +65,5 @@ describe('Feedback Popup Backend Api Service', () => { expect(successHandler).toHaveBeenCalled(); expect(failHandler).not.toHaveBeenCalled(); - } - )); + })); }); diff --git a/core/templates/pages/exploration-player-page/services/feedback-popup-backend-api.service.ts b/core/templates/pages/exploration-player-page/services/feedback-popup-backend-api.service.ts index d86bb47427f7..2e1e3bc8bc21 100644 --- a/core/templates/pages/exploration-player-page/services/feedback-popup-backend-api.service.ts +++ b/core/templates/pages/exploration-player-page/services/feedback-popup-backend-api.service.ts @@ -16,12 +16,12 @@ * @fileoverview Backend Api Service for Feedback Popup. */ -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { ExplorationEngineService } from './exploration-engine.service'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {ExplorationEngineService} from './exploration-engine.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class FeedbackPopupBackendApiService { feedbackUrl: string; @@ -30,21 +30,24 @@ export class FeedbackPopupBackendApiService { private httpClient: HttpClient, private explorationEngineService: ExplorationEngineService ) { - this.feedbackUrl = '/explorehandler/give_feedback/' + + this.feedbackUrl = + '/explorehandler/give_feedback/' + this.explorationEngineService.getExplorationId(); } async submitFeedbackAsync( - subject: string, - feedback: string, - includeAuthor: boolean, - stateName: string + subject: string, + feedback: string, + includeAuthor: boolean, + stateName: string ): Promise { - return this.httpClient.post(this.feedbackUrl, { - subject, - feedback, - include_author: includeAuthor, - state_name: stateName - }).toPromise(); + return this.httpClient + .post(this.feedbackUrl, { + subject, + feedback, + include_author: includeAuthor, + state_name: stateName, + }) + .toPromise(); } } diff --git a/core/templates/pages/exploration-player-page/services/hint-and-solution-modal.service.spec.ts b/core/templates/pages/exploration-player-page/services/hint-and-solution-modal.service.spec.ts index ea12acd6099e..a2a1667cdf53 100644 --- a/core/templates/pages/exploration-player-page/services/hint-and-solution-modal.service.spec.ts +++ b/core/templates/pages/exploration-player-page/services/hint-and-solution-modal.service.spec.ts @@ -16,19 +16,20 @@ * @fileoverview Tests for HintAndSolutionModalService. */ -import { TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { HintAndSolutionModalService } from './hint-and-solution-modal.service'; +import {TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {HintAndSolutionModalService} from './hint-and-solution-modal.service'; describe('Hint and Solution Modal Service', () => { let hintAndSolutionModalService: HintAndSolutionModalService; class MockNgbModal { - open(): { componentInstance: { index: number } } { + open(): {componentInstance: {index: number}} { return { componentInstance: { - index: 0 - }}; + index: 0, + }, + }; } } @@ -37,9 +38,9 @@ describe('Hint and Solution Modal Service', () => { providers: [ { provide: NgbModal, - useClass: MockNgbModal - } - ] + useClass: MockNgbModal, + }, + ], }).compileComponents(); })); @@ -56,7 +57,8 @@ describe('Hint and Solution Modal Service', () => { }); it('should display solution interstitial modal', () => { - expect(hintAndSolutionModalService.displaySolutionInterstitialModal()) - .toBeDefined(); + expect( + hintAndSolutionModalService.displaySolutionInterstitialModal() + ).toBeDefined(); }); }); diff --git a/core/templates/pages/exploration-player-page/services/hint-and-solution-modal.service.ts b/core/templates/pages/exploration-player-page/services/hint-and-solution-modal.service.ts index 4afcb6c45733..d5e4e50db0a5 100644 --- a/core/templates/pages/exploration-player-page/services/hint-and-solution-modal.service.ts +++ b/core/templates/pages/exploration-player-page/services/hint-and-solution-modal.service.ts @@ -16,42 +16,43 @@ * @fileoverview Service for showing the hint and solution modals. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { DisplayHintModalComponent } from '../modals/display-hint-modal.component'; -import { DisplaySolutionInterstititalModalComponent } from '../modals/display-solution-interstitial-modal.component'; -import { DisplaySolutionModalComponent } from '../modals/display-solution-modal.component'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {DisplayHintModalComponent} from '../modals/display-hint-modal.component'; +import {DisplaySolutionInterstititalModalComponent} from '../modals/display-solution-interstitial-modal.component'; +import {DisplaySolutionModalComponent} from '../modals/display-solution-modal.component'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class HintAndSolutionModalService { - constructor( - private ngbModal: NgbModal - ) {} + constructor(private ngbModal: NgbModal) {} displayHintModal(index: number): NgbModalRef { - let modalRef: NgbModalRef = this.ngbModal.open( - DisplayHintModalComponent, { - backdrop: 'static' - }); + let modalRef: NgbModalRef = this.ngbModal.open(DisplayHintModalComponent, { + backdrop: 'static', + }); modalRef.componentInstance.index = index; return modalRef; } displaySolutionModal(): NgbModalRef { return this.ngbModal.open(DisplaySolutionModalComponent, { - backdrop: 'static' + backdrop: 'static', }); } displaySolutionInterstitialModal(): NgbModalRef { return this.ngbModal.open(DisplaySolutionInterstititalModalComponent, { - backdrop: 'static' + backdrop: 'static', }); } } -angular.module('oppia').factory('HintAndSolutionModalService', - downgradeInjectable(HintAndSolutionModalService)); +angular + .module('oppia') + .factory( + 'HintAndSolutionModalService', + downgradeInjectable(HintAndSolutionModalService) + ); diff --git a/core/templates/pages/exploration-player-page/services/hints-and-solution-manager.service.spec.ts b/core/templates/pages/exploration-player-page/services/hints-and-solution-manager.service.spec.ts index 941cc8f696d0..d6562122d704 100644 --- a/core/templates/pages/exploration-player-page/services/hints-and-solution-manager.service.spec.ts +++ b/core/templates/pages/exploration-player-page/services/hints-and-solution-manager.service.spec.ts @@ -16,13 +16,16 @@ * @fileoverview Unit tests for the Hints/Solution Manager service. */ -import { EventEmitter } from '@angular/core'; -import { TestBed, fakeAsync, flush, tick } from '@angular/core/testing'; +import {EventEmitter} from '@angular/core'; +import {TestBed, fakeAsync, flush, tick} from '@angular/core/testing'; -import { Hint } from 'domain/exploration/hint-object.model'; -import { Solution, SolutionObjectFactory } from 'domain/exploration/SolutionObjectFactory'; -import { HintsAndSolutionManagerService } from 'pages/exploration-player-page/services/hints-and-solution-manager.service'; -import { PlayerPositionService } from 'pages/exploration-player-page/services/player-position.service'; +import {Hint} from 'domain/exploration/hint-object.model'; +import { + Solution, + SolutionObjectFactory, +} from 'domain/exploration/SolutionObjectFactory'; +import {HintsAndSolutionManagerService} from 'pages/exploration-player-page/services/hints-and-solution-manager.service'; +import {PlayerPositionService} from 'pages/exploration-player-page/services/player-position.service'; describe('HintsAndSolutionManager service', () => { let hasms: HintsAndSolutionManagerService; @@ -43,7 +46,8 @@ describe('HintsAndSolutionManager service', () => { beforeEach(fakeAsync(() => { pps = TestBed.inject(PlayerPositionService); spyOnProperty(pps, 'onNewCardAvailable').and.returnValue( - mockNewCardAvailableEmitter); + mockNewCardAvailableEmitter + ); hasms = TestBed.inject(HintsAndSolutionManagerService); sof = TestBed.inject(SolutionObjectFactory); @@ -51,19 +55,19 @@ describe('HintsAndSolutionManager service', () => { hint_content: { content_id: 'one', html: 'one', - } + }, }); secondHint = Hint.createFromBackendDict({ hint_content: { content_id: 'two', html: 'two', - } + }, }); thirdHint = Hint.createFromBackendDict({ hint_content: { content_id: 'three', html: 'three', - } + }, }); solution = sof.createFromBackendDict({ answer_is_exclusive: false, @@ -71,7 +75,7 @@ describe('HintsAndSolutionManager service', () => { explanation: { content_id: 'sol-one', html: 'This is the explanation to the answer', - } + }, }); })); @@ -116,55 +120,57 @@ describe('HintsAndSolutionManager service', () => { expect(hasms.isHintConsumed(1)).toBe(true); })); - it('should correctly show release solution and show tooltip', - fakeAsync(() => { - hasms.solutionDiscovered = false; - let mockSetTimeout = setTimeout(() => {}); - hasms.solutionTooltipTimeout = mockSetTimeout; - hasms.reset([], solution); + it('should correctly show release solution and show tooltip', fakeAsync(() => { + hasms.solutionDiscovered = false; + let mockSetTimeout = setTimeout(() => {}); + hasms.solutionTooltipTimeout = mockSetTimeout; + hasms.reset([], solution); - expect(hasms.isSolutionViewable()).toBe(false); - expect(hasms.isSolutionTooltipOpen()).toBe(false); + expect(hasms.isSolutionViewable()).toBe(false); + expect(hasms.isSolutionTooltipOpen()).toBe(false); - hasms.releaseSolution(); + hasms.releaseSolution(); - tick(1000); - expect(hasms.solutionReleased).toBe(true); - expect(hasms.solutionDiscovered).toBe(true); - expect(hasms.solutionTooltipIsOpen).toBe(true); - })); + tick(1000); + expect(hasms.solutionReleased).toBe(true); + expect(hasms.solutionDiscovered).toBe(true); + expect(hasms.solutionTooltipIsOpen).toBe(true); + })); - it('should not continue to display hints after after a correct answer is' + - 'submitted', fakeAsync(() => { - // Initialize the service with two hints and a solution. - hasms.reset([firstHint, secondHint], solution); + it( + 'should not continue to display hints after after a correct answer is' + + 'submitted', + fakeAsync(() => { + // Initialize the service with two hints and a solution. + hasms.reset([firstHint, secondHint], solution); - expect(hasms.isHintViewable(0)).toBe(false); - expect(hasms.isHintViewable(1)).toBe(false); - expect(hasms.isSolutionViewable()).toBe(false); - expect(hasms.isHintConsumed(0)).toBe(false); - expect(hasms.isHintConsumed(1)).toBe(false); + expect(hasms.isHintViewable(0)).toBe(false); + expect(hasms.isHintViewable(1)).toBe(false); + expect(hasms.isSolutionViewable()).toBe(false); + expect(hasms.isHintConsumed(0)).toBe(false); + expect(hasms.isHintConsumed(1)).toBe(false); - tick(WAIT_FOR_FIRST_HINT_MSEC); + tick(WAIT_FOR_FIRST_HINT_MSEC); - expect(hasms.isHintViewable(0)).toBe(true); - expect(hasms.isHintViewable(1)).toBe(false); - expect(hasms.isSolutionViewable()).toBe(false); - expect(hasms.displayHint(0)?.html).toBe('one'); - expect(hasms.isHintConsumed(0)).toBe(true); - expect(hasms.isHintConsumed(1)).toBe(false); + expect(hasms.isHintViewable(0)).toBe(true); + expect(hasms.isHintViewable(1)).toBe(false); + expect(hasms.isSolutionViewable()).toBe(false); + expect(hasms.displayHint(0)?.html).toBe('one'); + expect(hasms.isHintConsumed(0)).toBe(true); + expect(hasms.isHintConsumed(1)).toBe(false); - mockNewCardAvailableEmitter.emit(); - tick(WAIT_FOR_SUBSEQUENT_HINTS_MSEC); + mockNewCardAvailableEmitter.emit(); + tick(WAIT_FOR_SUBSEQUENT_HINTS_MSEC); - expect(hasms.isHintViewable(0)).toBe(true); - expect(hasms.isHintViewable(1)).toBe(false); - expect(hasms.displayHint(0)?.html).toBe('one'); - expect(hasms.displayHint(1)).toBeNull(); - expect(hasms.isHintConsumed(0)).toBe(true); - expect(hasms.isHintConsumed(1)).toBe(false); - expect(hasms.isSolutionViewable()).toBe(false); - })); + expect(hasms.isHintViewable(0)).toBe(true); + expect(hasms.isHintViewable(1)).toBe(false); + expect(hasms.displayHint(0)?.html).toBe('one'); + expect(hasms.displayHint(1)).toBeNull(); + expect(hasms.isHintConsumed(0)).toBe(true); + expect(hasms.isHintConsumed(1)).toBe(false); + expect(hasms.isSolutionViewable()).toBe(false); + }) + ); it('should show the correct number of hints', () => { // Initialize the service with two hints and a solution. @@ -180,7 +186,8 @@ describe('HintsAndSolutionManager service', () => { hasms.solutionTooltipTimeout = mockSetTimeout; expect(hasms.isSolutionConsumed()).toBe(false); expect(hasms.displaySolution()?.correctAnswer).toBe( - 'This is a correct answer!'); + 'This is a correct answer!' + ); expect(hasms.isSolutionConsumed()).toBe(true); })); @@ -224,52 +231,50 @@ describe('HintsAndSolutionManager service', () => { expect(hasms.displayHint(2)?.html).toBe('three'); })); - it('should reset the service when timeouts was called before', - fakeAsync(() => { - // Initialize the service with two hints and a solution. - hasms.reset([firstHint, secondHint], solution); + it('should reset the service when timeouts was called before', fakeAsync(() => { + // Initialize the service with two hints and a solution. + hasms.reset([firstHint, secondHint], solution); - // Set timeout. - tick(WAIT_FOR_FIRST_HINT_MSEC); - // Set tooltipTimeout. - tick(WAIT_FOR_TOOLTIP_TO_BE_SHOWN_MSEC); - tick(2000); + // Set timeout. + tick(WAIT_FOR_FIRST_HINT_MSEC); + // Set tooltipTimeout. + tick(WAIT_FOR_TOOLTIP_TO_BE_SHOWN_MSEC); + tick(2000); - // Reset service to 0 hints so releaseHint timeout won't be called. - hasms.reset([], solution); + // Reset service to 0 hints so releaseHint timeout won't be called. + hasms.reset([], solution); - // There is no timeout to flush. timeout and tooltipTimeout variables - // were cleaned. - expect(flush()).toBe(0); - })); + // There is no timeout to flush. timeout and tooltipTimeout variables + // were cleaned. + expect(flush()).toBe(0); + })); - it('should not record the wrong answer when a hint is already released', - fakeAsync(() => { - // Initialize the service with two hints and a solution. - hasms.reset([firstHint, secondHint], solution); + it('should not record the wrong answer when a hint is already released', fakeAsync(() => { + // Initialize the service with two hints and a solution. + hasms.reset([firstHint, secondHint], solution); - expect(hasms.isHintTooltipOpen()).toBe(false); - expect(hasms.isHintViewable(0)).toBe(false); - expect(hasms.isHintViewable(1)).toBe(false); - expect(hasms.isSolutionViewable()).toBe(false); + expect(hasms.isHintTooltipOpen()).toBe(false); + expect(hasms.isHintViewable(0)).toBe(false); + expect(hasms.isHintViewable(1)).toBe(false); + expect(hasms.isSolutionViewable()).toBe(false); - tick(WAIT_FOR_FIRST_HINT_MSEC); - tick(WAIT_FOR_TOOLTIP_TO_BE_SHOWN_MSEC); + tick(WAIT_FOR_FIRST_HINT_MSEC); + tick(WAIT_FOR_TOOLTIP_TO_BE_SHOWN_MSEC); - expect(hasms.isHintTooltipOpen()).toBe(true); - // It only changes hint visibility. - expect(hasms.isHintViewable(0)).toBe(true); - expect(hasms.isHintViewable(1)).toBe(false); - expect(hasms.isHintViewable(2)).toBe(false); - expect(hasms.isSolutionViewable()).toBe(false); + expect(hasms.isHintTooltipOpen()).toBe(true); + // It only changes hint visibility. + expect(hasms.isHintViewable(0)).toBe(true); + expect(hasms.isHintViewable(1)).toBe(false); + expect(hasms.isHintViewable(2)).toBe(false); + expect(hasms.isSolutionViewable()).toBe(false); - hasms.recordWrongAnswer(); + hasms.recordWrongAnswer(); - expect(hasms.isHintTooltipOpen()).toBe(true); - expect(hasms.isHintViewable(0)).toBe(true); - expect(hasms.isHintViewable(1)).toBe(false); - expect(hasms.isSolutionViewable()).toBe(false); - })); + expect(hasms.isHintTooltipOpen()).toBe(true); + expect(hasms.isHintViewable(0)).toBe(true); + expect(hasms.isHintViewable(1)).toBe(false); + expect(hasms.isSolutionViewable()).toBe(false); + })); it('should record the wrong answer twice', fakeAsync(() => { // Initialize the service with two hints and a solution. @@ -314,7 +319,8 @@ describe('HintsAndSolutionManager service', () => { it('should send the solution viewed event emitter', () => { let mockSolutionViewedEventEmitter = new EventEmitter(); expect(hasms.onSolutionViewedEventEmitter).toEqual( - mockSolutionViewedEventEmitter); + mockSolutionViewedEventEmitter + ); }); it('should fetch EventEmitter for consumption of hint', () => { @@ -336,6 +342,7 @@ describe('HintsAndSolutionManager service', () => { hasms.reset([], null); expect(() => hasms.displaySolution()).toThrowError( - 'Solution must be not null to be displayed.'); + 'Solution must be not null to be displayed.' + ); }); }); diff --git a/core/templates/pages/exploration-player-page/services/hints-and-solution-manager.service.ts b/core/templates/pages/exploration-player-page/services/hints-and-solution-manager.service.ts index 38d20579ea35..490bab03f00e 100644 --- a/core/templates/pages/exploration-player-page/services/hints-and-solution-manager.service.ts +++ b/core/templates/pages/exploration-player-page/services/hints-and-solution-manager.service.ts @@ -16,17 +16,17 @@ * @fileoverview Utility service for Hints in the learner's view. */ -import { EventEmitter, Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {EventEmitter, Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; -import { Hint } from 'domain/exploration/hint-object.model'; -import { Solution } from 'domain/exploration/SolutionObjectFactory'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { ExplorationPlayerConstants } from 'pages/exploration-player-page/exploration-player-page.constants'; -import { PlayerPositionService } from 'pages/exploration-player-page/services/player-position.service'; +import {Hint} from 'domain/exploration/hint-object.model'; +import {Solution} from 'domain/exploration/SolutionObjectFactory'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {ExplorationPlayerConstants} from 'pages/exploration-player-page/exploration-player-page.constants'; +import {PlayerPositionService} from 'pages/exploration-player-page/services/player-position.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class HintsAndSolutionManagerService { solutionForLatestCard: Solution | null = null; @@ -74,13 +74,11 @@ export class HintsAndSolutionManagerService { constructor(private playerPositionService: PlayerPositionService) { // TODO(#10904): Refactor to move subscriptions into components. - playerPositionService.onNewCardAvailable.subscribe( - () => { - this.correctAnswerSubmitted = true; - this.tooltipIsOpen = false; - this.solutionTooltipIsOpen = false; - } - ); + playerPositionService.onNewCardAvailable.subscribe(() => { + this.correctAnswerSubmitted = true; + this.tooltipIsOpen = false; + this.solutionTooltipIsOpen = false; + }); } // This replaces any timeouts that are already queued. @@ -109,7 +107,9 @@ export class HintsAndSolutionManagerService { this.numHintsReleased++; if (!this.hintsDiscovered && !this.tooltipTimeout) { this.tooltipTimeout = setTimeout( - this.showTooltip.bind(this), this.WAIT_FOR_TOOLTIP_TO_BE_SHOWN_MSEC); + this.showTooltip.bind(this), + this.WAIT_FOR_TOOLTIP_TO_BE_SHOWN_MSEC + ); } } this._timeoutElapsedEventEmitter.emit(); @@ -122,7 +122,8 @@ export class HintsAndSolutionManagerService { if (!this.solutionDiscovered && !this.solutionTooltipTimeout) { this.solutionTooltipTimeout = setTimeout( this.showSolutionTooltip.bind(this), - this.WAIT_FOR_TOOLTIP_TO_BE_SHOWN_MSEC); + this.WAIT_FOR_TOOLTIP_TO_BE_SHOWN_MSEC + ); } this._timeoutElapsedEventEmitter.emit(); } @@ -159,7 +160,8 @@ export class HintsAndSolutionManagerService { if (funcToEnqueue) { this.enqueueTimeout( funcToEnqueue, - ExplorationPlayerConstants.WAIT_FOR_SUBSEQUENT_HINTS_MSEC); + ExplorationPlayerConstants.WAIT_FOR_SUBSEQUENT_HINTS_MSEC + ); } } @@ -189,7 +191,8 @@ export class HintsAndSolutionManagerService { if (this.hintsForLatestCard.length > 0) { this.enqueueTimeout( this.releaseHint, - ExplorationPlayerConstants.WAIT_FOR_FIRST_HINT_MSEC); + ExplorationPlayerConstants.WAIT_FOR_FIRST_HINT_MSEC + ); } } @@ -197,8 +200,10 @@ export class HintsAndSolutionManagerService { // pending hint that's being viewed, it starts the timer for the next // hint. displayHint(index: number): SubtitledHtml | null { - if (index === this.numHintsConsumed && - this.numHintsConsumed < this.numHintsReleased) { + if ( + index === this.numHintsConsumed && + this.numHintsConsumed < this.numHintsReleased + ) { // The latest hint has been consumed. Start the timer. this.consumeHint(); } @@ -259,11 +264,13 @@ export class HintsAndSolutionManagerService { if (!this.areAllHintsExhausted()) { if ( this.numHintsReleased === 0 && - this.wrongAnswersSinceLastHintConsumed >= 2) { + this.wrongAnswersSinceLastHintConsumed >= 2 + ) { this.accelerateHintRelease(); } else if ( this.numHintsReleased > 0 && - this.wrongAnswersSinceLastHintConsumed >= 1) { + this.wrongAnswersSinceLastHintConsumed >= 1 + ) { this.accelerateHintRelease(); } } else if (this.getNumHints()) { @@ -291,6 +298,9 @@ export class HintsAndSolutionManagerService { } } -angular.module('oppia').factory( - 'HintsAndSolutionManagerService', - downgradeInjectable(HintsAndSolutionManagerService)); +angular + .module('oppia') + .factory( + 'HintsAndSolutionManagerService', + downgradeInjectable(HintsAndSolutionManagerService) + ); diff --git a/core/templates/pages/exploration-player-page/services/image-preloader.service.spec.ts b/core/templates/pages/exploration-player-page/services/image-preloader.service.spec.ts index cf97a17c1dcb..2feb0b61696d 100644 --- a/core/templates/pages/exploration-player-page/services/image-preloader.service.spec.ts +++ b/core/templates/pages/exploration-player-page/services/image-preloader.service.spec.ts @@ -16,18 +16,22 @@ * @fileoverview Unit tests for the image preloader service. */ -import { HttpClientTestingModule, HttpTestingController } from - '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; - -import { Exploration, ExplorationBackendDict, ExplorationObjectFactory } from - 'domain/exploration/ExplorationObjectFactory'; -import { ImagePreloaderService } from - 'pages/exploration-player-page/services/image-preloader.service'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; -import { EntityTranslationsService } from 'services/entity-translations.services'; -import { ContextService } from 'services/context.service'; -import { SvgSanitizerService } from 'services/svg-sanitizer.service'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; + +import { + Exploration, + ExplorationBackendDict, + ExplorationObjectFactory, +} from 'domain/exploration/ExplorationObjectFactory'; +import {ImagePreloaderService} from 'pages/exploration-player-page/services/image-preloader.service'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; +import {EntityTranslationsService} from 'services/entity-translations.services'; +import {ContextService} from 'services/context.service'; +import {SvgSanitizerService} from 'services/svg-sanitizer.service'; describe('Image preloader service', () => { let httpTestingController: HttpTestingController; @@ -48,7 +52,6 @@ describe('Image preloader service', () => { let entityTranslationsService: EntityTranslationsService; let svgSanitizerService: SvgSanitizerService; - const initStateName = 'Introduction'; const explorationDict: ExplorationBackendDict = { draft_changes: [], @@ -63,20 +66,20 @@ describe('Image preloader service', () => { param_changes: [], content: { html: '', - content_id: 'content' + content_id: 'content', }, recorded_voiceovers: { voiceovers_mapping: { content: {}, - default_outcome: {} - } + default_outcome: {}, + }, }, interaction: { id: 'Continue', default_outcome: { feedback: { content_id: 'default_outcome', - html: '' + html: '', }, dest: 'State 3', dest_if_really_stuck: null, @@ -90,13 +93,13 @@ describe('Image preloader service', () => { buttonText: { value: { unicode_str: 'Continue', - content_id: '' - } - } + content_id: '', + }, + }, }, solution: null, answer_groups: [], - hints: [] + hints: [], }, solicit_answer_details: false, card_is_checkpoint: false, @@ -107,12 +110,12 @@ describe('Image preloader service', () => { param_changes: [], content: { content_id: 'content', - html: 'Congratulations, you have finished!' + html: 'Congratulations, you have finished!', }, recorded_voiceovers: { voiceovers_mapping: { - content: {} - } + content: {}, + }, }, interaction: { id: 'EndExploration', @@ -120,12 +123,12 @@ describe('Image preloader service', () => { confirmed_unclassified_answers: [], customization_args: { recommendedExplorationIds: { - value: [] - } + value: [], + }, }, solution: null, answer_groups: [], - hints: [] + hints: [], }, solicit_answer_details: false, card_is_checkpoint: false, @@ -137,15 +140,15 @@ describe('Image preloader service', () => { param_changes: [], content: { content_id: 'content', - html: 'Multiple Choice' + html: 'Multiple Choice', }, recorded_voiceovers: { voiceovers_mapping: { content: {}, default_outcome: {}, feedback_1: {}, - feedback_2: {} - } + feedback_2: {}, + }, }, interaction: { id: 'MultipleChoiceInput', @@ -154,7 +157,7 @@ describe('Image preloader service', () => { dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: 'Try Again!' + html: 'Try Again!', }, param_changes: [], labelled_as_correct: null, @@ -164,21 +167,26 @@ describe('Image preloader service', () => { confirmed_unclassified_answers: [], customization_args: { choices: { - value: [{ - html: '

Go to ItemSelection

', - content_id: '' - }, { - html: '

Go to ImageAndRegion

', - content_id: '' - }] + value: [ + { + html: + '

Go to ItemSelection

', + content_id: '', + }, + { + html: + '

Go to ImageAndRegion

', + content_id: '', + }, + ], }, - showChoicesInShuffledOrder: {value: false} + showChoicesInShuffledOrder: {value: false}, }, answer_groups: [ { @@ -187,20 +195,23 @@ describe('Image preloader service', () => { dest_if_really_stuck: null, feedback: { content_id: 'feedback_1', - html: '

We are going to ItemSelection' + - '

' + html: + '

We are going to ItemSelection' + + '

', }, param_changes: [], refresher_exploration_id: null, missing_prerequisite_skill_id: null, labelled_as_correct: null, }, - rule_specs: [{ - rule_type: 'Equals', - inputs: {x: 0} - }], + rule_specs: [ + { + rule_type: 'Equals', + inputs: {x: 0}, + }, + ], training_data: null, tagged_skill_misconception_id: null, }, @@ -210,23 +221,25 @@ describe('Image preloader service', () => { dest_if_really_stuck: null, feedback: { content_id: 'feedback_2', - html: "Let's go to state 1 ImageAndRegion" + html: "Let's go to state 1 ImageAndRegion", }, param_changes: [], refresher_exploration_id: null, missing_prerequisite_skill_id: null, labelled_as_correct: false, }, - rule_specs: [{ - rule_type: 'Equals', - inputs: {x: 1} - }], + rule_specs: [ + { + rule_type: 'Equals', + inputs: {x: 1}, + }, + ], training_data: null, tagged_skill_misconception_id: null, - } + }, ], hints: [], - solution: null + solution: null, }, solicit_answer_details: false, card_is_checkpoint: true, @@ -236,7 +249,7 @@ describe('Image preloader service', () => { param_changes: [], content: { content_id: 'content', - html: '

Text Input Content

' + html: '

Text Input Content

', }, recorded_voiceovers: { voiceovers_mapping: { @@ -244,8 +257,8 @@ describe('Image preloader service', () => { default_outcome: {}, feedback_1: {}, feedback_2: {}, - hint_1: {} - } + hint_1: {}, + }, }, interaction: { id: 'TextInput', @@ -254,88 +267,102 @@ describe('Image preloader service', () => { dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: '' + html: '', }, labelled_as_correct: false, param_changes: [], refresher_exploration_id: null, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, confirmed_unclassified_answers: [], customization_args: { rows: { - value: 1 + value: 1, }, placeholder: { value: { unicode_str: '', - content_id: '' - } + content_id: '', + }, }, catchMisspellings: { - value: false - } + value: false, + }, }, - answer_groups: [{ - rule_specs: [{ - rule_type: 'Contains', - inputs: {x: { - contentId: 'rule_input', - normalizedStrSet: ['1'] - }} - }], - outcome: { - dest: 'State 1', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_1', - html: "

Let's go to State 1

" + answer_groups: [ + { + rule_specs: [ + { + rule_type: 'Contains', + inputs: { + x: { + contentId: 'rule_input', + normalizedStrSet: ['1'], + }, + }, + }, + ], + outcome: { + dest: 'State 1', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: "

Let's go to State 1

", + }, + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, }, - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null + training_data: null, + tagged_skill_misconception_id: null, + }, + { + rule_specs: [ + { + rule_type: 'Contains', + inputs: { + x: { + contentId: 'rule_input', + normalizedStrSet: ['2'], + }, + }, + }, + ], + outcome: { + dest: 'State 1', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: "

Let's go to State 1

", + }, + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + }, + training_data: null, + tagged_skill_misconception_id: null, }, - training_data: null, - tagged_skill_misconception_id: null, - }, { - rule_specs: [{ - rule_type: 'Contains', - inputs: {x: { - contentId: 'rule_input', - normalizedStrSet: ['2'] - }} - }], - outcome: { - dest: 'State 1', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: "

Let's go to State 1

" + ], + hints: [ + { + hint_content: { + content_id: 'hint_1', + html: + '

' + + '

', }, - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null }, - training_data: null, - tagged_skill_misconception_id: null, - }], - hints: [{ - hint_content: { - content_id: 'hint_1', - html: '

' + - '

' - } - }], + ], solution: null, }, solicit_answer_details: false, card_is_checkpoint: false, linked_skill_id: null, classifier_model_id: null, - } + }, }, param_specs: {}, param_changes: [], @@ -352,8 +379,8 @@ describe('Image preloader service', () => { param_specs: {}, param_changes: [], auto_tts_enabled: false, - edits_allowed: true - } + edits_allowed: true, + }, } as unknown as ExplorationBackendDict; class mockReaderObject { result = null; @@ -374,14 +401,10 @@ describe('Image preloader service', () => { const filename3 = 'sIOFeedback_height_50_width_50.png'; const filename4 = 's6Hint1_height_60_width_60.png'; - const requestUrl1 = ( - `/assetsdevhandler/exploration/1/assets/image/${filename1}`); - const requestUrl2 = ( - `/assetsdevhandler/exploration/1/assets/image/${filename2}`); - const requestUrl3 = ( - `/assetsdevhandler/exploration/1/assets/image/${filename3}`); - const requestUrl4 = ( - `/assetsdevhandler/exploration/1/assets/image/${filename4}`); + const requestUrl1 = `/assetsdevhandler/exploration/1/assets/image/${filename1}`; + const requestUrl2 = `/assetsdevhandler/exploration/1/assets/image/${filename2}`; + const requestUrl3 = `/assetsdevhandler/exploration/1/assets/image/${filename3}`; + const requestUrl4 = `/assetsdevhandler/exploration/1/assets/image/${filename4}`; const imageBlob = new Blob(['image data'], {type: 'imagetype'}); @@ -401,10 +424,11 @@ describe('Image preloader service', () => { spyOn(entityTranslationsService, 'getHtmlTranslations').and.callFake( (unusedLanguageCode, unusedContentIds) => { return []; - }); + } + ); - exploration = ( - explorationObjectFactory.createFromBackendDict(explorationDict)); + exploration = + explorationObjectFactory.createFromBackendDict(explorationDict); }); it('should be in exploration player after init is called', () => { @@ -422,48 +446,52 @@ describe('Image preloader service', () => { expect(imagePreloaderService.inExplorationPlayer()).toBeFalse(); }); - it('should maintain the correct number of download requests in queue', - fakeAsync(() => { - imagePreloaderService.init(exploration); - imagePreloaderService.kickOffImagePreloader(initStateName); + it('should maintain the correct number of download requests in queue', fakeAsync(() => { + imagePreloaderService.init(exploration); + imagePreloaderService.kickOffImagePreloader(initStateName); - // Max files to download simultaneously is 3. - httpTestingController.expectOne(requestUrl1).flush(imageBlob); - expect(imagePreloaderService.getFilenamesOfImageCurrentlyDownloading()) - .toEqual([filename1, filename2, filename3]); - expect(imagePreloaderService.isLoadingImageFile(filename1)).toBeTrue(); - expect(imagePreloaderService.isLoadingImageFile(filename2)).toBeTrue(); - expect(imagePreloaderService.isLoadingImageFile(filename3)).toBeTrue(); - expect(imagePreloaderService.isLoadingImageFile(filename4)).toBeFalse(); + // Max files to download simultaneously is 3. + httpTestingController.expectOne(requestUrl1).flush(imageBlob); + expect( + imagePreloaderService.getFilenamesOfImageCurrentlyDownloading() + ).toEqual([filename1, filename2, filename3]); + expect(imagePreloaderService.isLoadingImageFile(filename1)).toBeTrue(); + expect(imagePreloaderService.isLoadingImageFile(filename2)).toBeTrue(); + expect(imagePreloaderService.isLoadingImageFile(filename3)).toBeTrue(); + expect(imagePreloaderService.isLoadingImageFile(filename4)).toBeFalse(); - flushMicrotasks(); + flushMicrotasks(); - httpTestingController.expectOne(requestUrl2).flush(imageBlob); - expect(imagePreloaderService.getFilenamesOfImageCurrentlyDownloading()) - .toEqual([filename2, filename3, filename4]); - expect(imagePreloaderService.isLoadingImageFile(filename4)).toBeTrue(); + httpTestingController.expectOne(requestUrl2).flush(imageBlob); + expect( + imagePreloaderService.getFilenamesOfImageCurrentlyDownloading() + ).toEqual([filename2, filename3, filename4]); + expect(imagePreloaderService.isLoadingImageFile(filename4)).toBeTrue(); - flushMicrotasks(); + flushMicrotasks(); - httpTestingController.expectOne(requestUrl3).flush(imageBlob); - expect(imagePreloaderService.getFilenamesOfImageCurrentlyDownloading()) - .toEqual([filename3, filename4]); + httpTestingController.expectOne(requestUrl3).flush(imageBlob); + expect( + imagePreloaderService.getFilenamesOfImageCurrentlyDownloading() + ).toEqual([filename3, filename4]); - flushMicrotasks(); + flushMicrotasks(); - httpTestingController.expectOne(requestUrl4).flush(imageBlob); - expect(imagePreloaderService.getFilenamesOfImageCurrentlyDownloading()) - .toEqual([filename4]); + httpTestingController.expectOne(requestUrl4).flush(imageBlob); + expect( + imagePreloaderService.getFilenamesOfImageCurrentlyDownloading() + ).toEqual([filename4]); - flushMicrotasks(); + flushMicrotasks(); - expect(imagePreloaderService.getFilenamesOfImageCurrentlyDownloading()) - .toEqual([]); - expect(imagePreloaderService.isLoadingImageFile(filename1)).toBeFalse(); - expect(imagePreloaderService.isLoadingImageFile(filename2)).toBeFalse(); - expect(imagePreloaderService.isLoadingImageFile(filename3)).toBeFalse(); - expect(imagePreloaderService.isLoadingImageFile(filename4)).toBeFalse(); - })); + expect( + imagePreloaderService.getFilenamesOfImageCurrentlyDownloading() + ).toEqual([]); + expect(imagePreloaderService.isLoadingImageFile(filename1)).toBeFalse(); + expect(imagePreloaderService.isLoadingImageFile(filename2)).toBeFalse(); + expect(imagePreloaderService.isLoadingImageFile(filename3)).toBeFalse(); + expect(imagePreloaderService.isLoadingImageFile(filename4)).toBeFalse(); + })); it('should properly restart pre-loading from a new state', () => { imagePreloaderService.init(exploration); @@ -472,127 +500,150 @@ describe('Image preloader service', () => { httpTestingController.expectOne(requestUrl1); httpTestingController.expectOne(requestUrl2); httpTestingController.expectOne(requestUrl3); - expect(imagePreloaderService.getFilenamesOfImageCurrentlyDownloading()) - .toEqual([filename1, filename2, filename3]); + expect( + imagePreloaderService.getFilenamesOfImageCurrentlyDownloading() + ).toEqual([filename1, filename2, filename3]); imagePreloaderService.restartImagePreloader('State 6'); httpTestingController.expectOne(requestUrl4); - expect(imagePreloaderService.getFilenamesOfImageCurrentlyDownloading()) - .toEqual([filename4]); + expect( + imagePreloaderService.getFilenamesOfImageCurrentlyDownloading() + ).toEqual([filename4]); expect(imagePreloaderService.isLoadingImageFile(filename4)).toBeTrue(); }); - it('should start preloader when state changes and there is at least' + - ' one file downloading', fakeAsync(() => { - imagePreloaderService.init(exploration); - imagePreloaderService.kickOffImagePreloader(initStateName); + it( + 'should start preloader when state changes and there is at least' + + ' one file downloading', + fakeAsync(() => { + imagePreloaderService.init(exploration); + imagePreloaderService.kickOffImagePreloader(initStateName); - httpTestingController.expectOne(requestUrl1); - httpTestingController.expectOne(requestUrl2); - httpTestingController.expectOne(requestUrl3); - expect(imagePreloaderService.getFilenamesOfImageCurrentlyDownloading()) - .toEqual([filename1, filename2, filename3]); - expect(imagePreloaderService.isLoadingImageFile(filename4)).toBeFalse(); + httpTestingController.expectOne(requestUrl1); + httpTestingController.expectOne(requestUrl2); + httpTestingController.expectOne(requestUrl3); + expect( + imagePreloaderService.getFilenamesOfImageCurrentlyDownloading() + ).toEqual([filename1, filename2, filename3]); + expect(imagePreloaderService.isLoadingImageFile(filename4)).toBeFalse(); - imagePreloaderService.onStateChange('State 6'); + imagePreloaderService.onStateChange('State 6'); - httpTestingController.expectOne(requestUrl4).flush(imageBlob); - expect(imagePreloaderService.getFilenamesOfImageCurrentlyDownloading()) - .toEqual([filename4]); - expect(imagePreloaderService.isLoadingImageFile(filename4)).toBeTrue(); + httpTestingController.expectOne(requestUrl4).flush(imageBlob); + expect( + imagePreloaderService.getFilenamesOfImageCurrentlyDownloading() + ).toEqual([filename4]); + expect(imagePreloaderService.isLoadingImageFile(filename4)).toBeTrue(); - flushMicrotasks(); + flushMicrotasks(); - expect(imagePreloaderService.isLoadingImageFile(filename4)).toBeFalse(); - })); + expect(imagePreloaderService.isLoadingImageFile(filename4)).toBeFalse(); + }) + ); - it('should not start preloader when state changes and there is no' + - ' file downloading', fakeAsync(() => { - imagePreloaderService.init(exploration); - imagePreloaderService.kickOffImagePreloader(initStateName); + it( + 'should not start preloader when state changes and there is no' + + ' file downloading', + fakeAsync(() => { + imagePreloaderService.init(exploration); + imagePreloaderService.kickOffImagePreloader(initStateName); - httpTestingController.expectOne(requestUrl1).flush(imageBlob); - httpTestingController.expectOne(requestUrl2).flush(imageBlob); - httpTestingController.expectOne(requestUrl3).flush(imageBlob); - expect(imagePreloaderService.getFilenamesOfImageCurrentlyDownloading()) - .toEqual([filename1, filename2, filename3]); + httpTestingController.expectOne(requestUrl1).flush(imageBlob); + httpTestingController.expectOne(requestUrl2).flush(imageBlob); + httpTestingController.expectOne(requestUrl3).flush(imageBlob); + expect( + imagePreloaderService.getFilenamesOfImageCurrentlyDownloading() + ).toEqual([filename1, filename2, filename3]); - flushMicrotasks(); + flushMicrotasks(); - httpTestingController.expectOne(requestUrl4).flush(imageBlob); - expect(imagePreloaderService.getFilenamesOfImageCurrentlyDownloading()) - .toEqual([filename4]); + httpTestingController.expectOne(requestUrl4).flush(imageBlob); + expect( + imagePreloaderService.getFilenamesOfImageCurrentlyDownloading() + ).toEqual([filename4]); - imagePreloaderService.onStateChange('State 6'); - expect(imagePreloaderService.isLoadingImageFile(filename4)).toBeTrue(); + imagePreloaderService.onStateChange('State 6'); + expect(imagePreloaderService.isLoadingImageFile(filename4)).toBeTrue(); - flushMicrotasks(); + flushMicrotasks(); - expect(imagePreloaderService.getFilenamesOfImageCurrentlyDownloading()) - .toEqual([]); - expect(imagePreloaderService.isLoadingImageFile(filename4)).toBeFalse(); - })); + expect( + imagePreloaderService.getFilenamesOfImageCurrentlyDownloading() + ).toEqual([]); + expect(imagePreloaderService.isLoadingImageFile(filename4)).toBeFalse(); + }) + ); - it('should check that there is sync between AssetsBackendApi Service and' + - 'ImagePreloader Service', fakeAsync(() => { + it( + 'should check that there is sync between AssetsBackendApi Service and' + + 'ImagePreloader Service', + fakeAsync(() => { + imagePreloaderService.init(exploration); + imagePreloaderService.kickOffImagePreloader(initStateName); + flushMicrotasks(); + + expect( + imagePreloaderService.getFilenamesOfImageCurrentlyDownloading() + ).toEqual([filename1, filename2, filename3]); + expect( + assetsBackendApiService + .getAssetsFilesCurrentlyBeingRequested() + .image.map(fileDownloadRequest => fileDownloadRequest.filename) + ).toEqual([filename1, filename2, filename3]); + + httpTestingController.expectOne(requestUrl1).flush(imageBlob); + httpTestingController.expectOne(requestUrl2); + httpTestingController.expectOne(requestUrl3); + flushMicrotasks(); + + httpTestingController.expectOne(requestUrl4); + expect( + imagePreloaderService.getFilenamesOfImageCurrentlyDownloading() + ).toEqual([filename2, filename3, filename4]); + expect( + assetsBackendApiService + .getAssetsFilesCurrentlyBeingRequested() + .image.map(fileDownloadRequest => fileDownloadRequest.filename) + ).toEqual([filename2, filename3, filename4]); + }) + ); + + it('should maintain the filenames of image which failed to download', fakeAsync(() => { imagePreloaderService.init(exploration); imagePreloaderService.kickOffImagePreloader(initStateName); - flushMicrotasks(); - expect(imagePreloaderService.getFilenamesOfImageCurrentlyDownloading()) - .toEqual([filename1, filename2, filename3]); + httpTestingController.expectOne(requestUrl1).flush(imageBlob); + httpTestingController.expectOne(requestUrl2).flush(imageBlob); expect( - assetsBackendApiService.getAssetsFilesCurrentlyBeingRequested().image.map( - fileDownloadRequest => fileDownloadRequest.filename)) - .toEqual([filename1, filename2, filename3]); + imagePreloaderService.getFilenamesOfImageCurrentlyDownloading() + ).toEqual([filename1, filename2, filename3]); - httpTestingController.expectOne(requestUrl1).flush(imageBlob); - httpTestingController.expectOne(requestUrl2); - httpTestingController.expectOne(requestUrl3); flushMicrotasks(); + httpTestingController + .expectOne(requestUrl3) + .flush(imageBlob, {status: 404, statusText: 'Status Text'}); httpTestingController.expectOne(requestUrl4); - expect(imagePreloaderService.getFilenamesOfImageCurrentlyDownloading()) - .toEqual([filename2, filename3, filename4]); expect( - assetsBackendApiService.getAssetsFilesCurrentlyBeingRequested().image.map( - fileDownloadRequest => fileDownloadRequest.filename)) - .toEqual([filename2, filename3, filename4]); - })); + imagePreloaderService.getFilenamesOfImageCurrentlyDownloading() + ).toEqual([filename3, filename4]); + expect(imagePreloaderService.isInFailedDownload(filename3)).toBeFalse(); - it('should maintain the filenames of image which failed to download', - fakeAsync(() => { - imagePreloaderService.init(exploration); - imagePreloaderService.kickOffImagePreloader(initStateName); - - httpTestingController.expectOne(requestUrl1).flush(imageBlob); - httpTestingController.expectOne(requestUrl2).flush(imageBlob); - expect(imagePreloaderService.getFilenamesOfImageCurrentlyDownloading()) - .toEqual([filename1, filename2, filename3]); - - flushMicrotasks(); - - httpTestingController.expectOne(requestUrl3) - .flush(imageBlob, {status: 404, statusText: 'Status Text'}); - httpTestingController.expectOne(requestUrl4); - expect(imagePreloaderService.getFilenamesOfImageCurrentlyDownloading()) - .toEqual([filename3, filename4]); - expect(imagePreloaderService.isInFailedDownload(filename3)).toBeFalse(); - - flushMicrotasks(); + flushMicrotasks(); - expect(imagePreloaderService.isInFailedDownload(filename3)).toBeTrue(); - expect(imagePreloaderService.isInFailedDownload(filename4)).toBeFalse(); + expect(imagePreloaderService.isInFailedDownload(filename3)).toBeTrue(); + expect(imagePreloaderService.isInFailedDownload(filename4)).toBeFalse(); - imagePreloaderService.restartImagePreloader('State 6'); + imagePreloaderService.restartImagePreloader('State 6'); - httpTestingController.expectOne(requestUrl4) - .flush(imageBlob, {status: 408, statusText: 'Status Text'}); - flushMicrotasks(); + httpTestingController + .expectOne(requestUrl4) + .flush(imageBlob, {status: 408, statusText: 'Status Text'}); + flushMicrotasks(); - expect(imagePreloaderService.isInFailedDownload(filename4)).toBeTrue(); - })); + expect(imagePreloaderService.isInFailedDownload(filename4)).toBeTrue(); + })); it('should calculate the dimensions of the image file', () => { imagePreloaderService.init(exploration); @@ -607,7 +658,8 @@ describe('Image preloader service', () => { expect(dimensions1.height).toEqual(50); const dimensions2 = imagePreloaderService.getDimensionsOfImage( - 'sIOFeedback_height_30_width_45_height_56_width_56.png'); + 'sIOFeedback_height_30_width_45_height_56_width_56.png' + ); expect(dimensions2.width).toEqual(56); expect(dimensions2.height).toEqual(56); @@ -621,14 +673,16 @@ describe('Image preloader service', () => { const mathSvgDimensions = imagePreloaderService.getDimensionsOfMathSvg( 'mathImg_20207261338r3ir43lmfd_height_2d456_width_6d124_vertical_0' + - 'd231.svg'); + 'd231.svg' + ); expect(mathSvgDimensions.height).toEqual(2.456); expect(mathSvgDimensions.width).toEqual(6.124); expect(mathSvgDimensions.verticalPadding).toEqual(0.231); expect(() => { imagePreloaderService.getDimensionsOfMathSvg( - 'mathImg_20207261338r3ir43lmfd_2d456_width_6d124_vertical_0d231.svg'); + 'mathImg_20207261338r3ir43lmfd_2d456_width_6d124_vertical_0d231.svg' + ); }).toThrowError(/it does not contain dimensions/); }); @@ -639,15 +693,17 @@ describe('Image preloader service', () => { httpTestingController.expectOne(requestUrl1).flush(imageBlob); httpTestingController.expectOne(requestUrl2); httpTestingController.expectOne(requestUrl3); - expect(imagePreloaderService.getFilenamesOfImageCurrentlyDownloading()) - .toEqual([filename1, filename2, filename3]); + expect( + imagePreloaderService.getFilenamesOfImageCurrentlyDownloading() + ).toEqual([filename1, filename2, filename3]); expect(imagePreloaderService.isLoadingImageFile(filename1)).toBeTrue(); flushMicrotasks(); httpTestingController.expectOne(requestUrl4); - expect(imagePreloaderService.getFilenamesOfImageCurrentlyDownloading()) - .toEqual([filename2, filename3, filename4]); + expect( + imagePreloaderService.getFilenamesOfImageCurrentlyDownloading() + ).toEqual([filename2, filename3, filename4]); expect(imagePreloaderService.isLoadingImageFile(filename1)).toBeFalse(); var onSuccess = jasmine.createSpy('success'); @@ -659,7 +715,8 @@ describe('Image preloader service', () => { // @ts-expect-error spyOn(window, 'FileReader').and.returnValue(new mockReaderObject()); - imagePreloaderService.getImageUrlAsync(filename1) + imagePreloaderService + .getImageUrlAsync(filename1) .then(onSuccess, onFailure); flushMicrotasks(); @@ -671,19 +728,22 @@ describe('Image preloader service', () => { imagePreloaderService.init(exploration); imagePreloaderService.kickOffImagePreloader(initStateName); - httpTestingController.expectOne(requestUrl1).flush( - new Blob(['svg image'], { type: 'image/svg+xml' })); + httpTestingController + .expectOne(requestUrl1) + .flush(new Blob(['svg image'], {type: 'image/svg+xml'})); httpTestingController.expectOne(requestUrl2); httpTestingController.expectOne(requestUrl3); - expect(imagePreloaderService.getFilenamesOfImageCurrentlyDownloading()) - .toEqual([filename1, filename2, filename3]); + expect( + imagePreloaderService.getFilenamesOfImageCurrentlyDownloading() + ).toEqual([filename1, filename2, filename3]); expect(imagePreloaderService.isLoadingImageFile(filename1)).toBeTrue(); flushMicrotasks(); httpTestingController.expectOne(requestUrl4); - expect(imagePreloaderService.getFilenamesOfImageCurrentlyDownloading()) - .toEqual([filename2, filename3, filename4]); + expect( + imagePreloaderService.getFilenamesOfImageCurrentlyDownloading() + ).toEqual([filename2, filename3, filename4]); expect(imagePreloaderService.isLoadingImageFile(filename1)).toBeFalse(); var onSuccess = jasmine.createSpy('success'); @@ -696,7 +756,8 @@ describe('Image preloader service', () => { spyOn(window, 'FileReader').and.returnValue(new mockReaderObject()); spyOn(svgSanitizerService, 'getTrustedSvgResourceUrl'); - imagePreloaderService.getImageUrlAsync(filename1) + imagePreloaderService + .getImageUrlAsync(filename1) .then(onSuccess, onFailure); flushMicrotasks(); @@ -705,142 +766,166 @@ describe('Image preloader service', () => { expect(onFailure).not.toHaveBeenCalled(); })); - it('should get image url when first loading fails and the second' + - ' one is successful', fakeAsync(() => { + it( + 'should get image url when first loading fails and the second' + + ' one is successful', + fakeAsync(() => { + imagePreloaderService.init(exploration); + imagePreloaderService.kickOffImagePreloader(initStateName); + + httpTestingController + .expectOne(requestUrl1) + .flush(imageBlob, {status: 404, statusText: 'Status Text'}); + httpTestingController.expectOne(requestUrl2); + httpTestingController.expectOne(requestUrl3); + expect( + imagePreloaderService.getFilenamesOfImageCurrentlyDownloading() + ).toEqual([filename1, filename2, filename3]); + expect(imagePreloaderService.isInFailedDownload(filename1)).toBeFalse(); + + flushMicrotasks(); + + httpTestingController.expectOne(requestUrl4); + expect( + imagePreloaderService.getFilenamesOfImageCurrentlyDownloading() + ).toEqual([filename2, filename3, filename4]); + expect(imagePreloaderService.isInFailedDownload(filename1)).toBeTrue(); + + var onSuccess = jasmine.createSpy('success'); + var onFailure = jasmine.createSpy('fail'); + // This throws "Argument of type 'mockReaderObject' is not assignable + // to parameter of type 'FileReader'.". We need to suppress this error + // because 'FileReader' has around 15 more properties. We have only defined + // the properties we need in 'mockReaderObject'. + // @ts-expect-error + spyOn(window, 'FileReader').and.returnValue(new mockReaderObject()); + + imagePreloaderService + .getImageUrlAsync(filename1) + .then(onSuccess, onFailure); + + httpTestingController.expectOne(requestUrl1).flush(imageBlob); + flushMicrotasks(); + + expect(imagePreloaderService.isInFailedDownload(filename1)).toBeFalse(); + expect(onSuccess).toHaveBeenCalled(); + expect(onFailure).not.toHaveBeenCalled(); + }) + ); + + it('should not get image url when loading image fails in both requests', fakeAsync(() => { imagePreloaderService.init(exploration); imagePreloaderService.kickOffImagePreloader(initStateName); - httpTestingController.expectOne(requestUrl1) + httpTestingController + .expectOne(requestUrl1) .flush(imageBlob, {status: 404, statusText: 'Status Text'}); httpTestingController.expectOne(requestUrl2); httpTestingController.expectOne(requestUrl3); - expect(imagePreloaderService.getFilenamesOfImageCurrentlyDownloading()) - .toEqual([filename1, filename2, filename3]); + expect( + imagePreloaderService.getFilenamesOfImageCurrentlyDownloading() + ).toEqual([filename1, filename2, filename3]); expect(imagePreloaderService.isInFailedDownload(filename1)).toBeFalse(); flushMicrotasks(); httpTestingController.expectOne(requestUrl4); - expect(imagePreloaderService.getFilenamesOfImageCurrentlyDownloading()) - .toEqual([filename2, filename3, filename4]); + expect( + imagePreloaderService.getFilenamesOfImageCurrentlyDownloading() + ).toEqual([filename2, filename3, filename4]); expect(imagePreloaderService.isInFailedDownload(filename1)).toBeTrue(); var onSuccess = jasmine.createSpy('success'); var onFailure = jasmine.createSpy('fail'); - // This throws "Argument of type 'mockReaderObject' is not assignable - // to parameter of type 'FileReader'.". We need to suppress this error - // because 'FileReader' has around 15 more properties. We have only defined - // the properties we need in 'mockReaderObject'. - // @ts-expect-error - spyOn(window, 'FileReader').and.returnValue(new mockReaderObject()); - imagePreloaderService.getImageUrlAsync(filename1) + imagePreloaderService + .getImageUrlAsync(filename1) .then(onSuccess, onFailure); - httpTestingController.expectOne(requestUrl1).flush(imageBlob); + httpTestingController + .expectOne(requestUrl1) + .flush(imageBlob, {status: 404, statusText: 'Status Text'}); flushMicrotasks(); - expect(imagePreloaderService.isInFailedDownload(filename1)).toBeFalse(); - expect(onSuccess).toHaveBeenCalled(); - expect(onFailure).not.toHaveBeenCalled(); + expect(imagePreloaderService.isInFailedDownload(filename1)).toBeTrue(); + expect(onSuccess).not.toHaveBeenCalled(); + expect(onFailure).toHaveBeenCalled(); })); - it('should not get image url when loading image fails in both requests', + it( + 'should call the successful callback when loading an image after' + + ' trying to get its image url', fakeAsync(() => { imagePreloaderService.init(exploration); imagePreloaderService.kickOffImagePreloader(initStateName); - httpTestingController.expectOne(requestUrl1) - .flush(imageBlob, {status: 404, statusText: 'Status Text'}); + var onSuccess = jasmine.createSpy('success'); + var onFailure = jasmine.createSpy('fail'); + // This throws "Argument of type 'mockReaderObject' is not assignable + // to parameter of type 'FileReader'.". We need to suppress this error + // because 'FileReader' has around 15 more properties. We have only defined + // the properties we need in 'mockReaderObject'. + // @ts-expect-error + spyOn(window, 'FileReader').and.returnValue(new mockReaderObject()); + + imagePreloaderService + .getImageUrlAsync(filename1) + .then(onSuccess, onFailure); + + httpTestingController.expectOne(requestUrl1).flush(imageBlob); httpTestingController.expectOne(requestUrl2); httpTestingController.expectOne(requestUrl3); - expect(imagePreloaderService.getFilenamesOfImageCurrentlyDownloading()) - .toEqual([filename1, filename2, filename3]); - expect(imagePreloaderService.isInFailedDownload(filename1)).toBeFalse(); + expect( + imagePreloaderService.getFilenamesOfImageCurrentlyDownloading() + ).toEqual([filename1, filename2, filename3]); + expect(imagePreloaderService.isLoadingImageFile(filename1)).toBeTrue(); flushMicrotasks(); httpTestingController.expectOne(requestUrl4); - expect(imagePreloaderService.getFilenamesOfImageCurrentlyDownloading()) - .toEqual([filename2, filename3, filename4]); - expect(imagePreloaderService.isInFailedDownload(filename1)).toBeTrue(); + expect( + imagePreloaderService.getFilenamesOfImageCurrentlyDownloading() + ).toEqual([filename2, filename3, filename4]); + + expect(onSuccess).toHaveBeenCalled(); + expect(onFailure).not.toHaveBeenCalled(); + }) + ); + + it( + 'should call the failed callback when loading an image fails after' + + ' trying to get its image url', + fakeAsync(() => { + imagePreloaderService.init(exploration); + imagePreloaderService.kickOffImagePreloader(initStateName); var onSuccess = jasmine.createSpy('success'); var onFailure = jasmine.createSpy('fail'); - imagePreloaderService.getImageUrlAsync(filename1) + imagePreloaderService + .getImageUrlAsync(filename1) .then(onSuccess, onFailure); - httpTestingController.expectOne(requestUrl1) + httpTestingController + .expectOne(requestUrl1) .flush(imageBlob, {status: 404, statusText: 'Status Text'}); + httpTestingController.expectOne(requestUrl2); + httpTestingController.expectOne(requestUrl3); + expect( + imagePreloaderService.getFilenamesOfImageCurrentlyDownloading() + ).toEqual([filename1, filename2, filename3]); + expect(imagePreloaderService.isInFailedDownload(filename1)).toBeFalse(); + flushMicrotasks(); + httpTestingController.expectOne(requestUrl4); + expect( + imagePreloaderService.getFilenamesOfImageCurrentlyDownloading() + ).toEqual([filename2, filename3, filename4]); expect(imagePreloaderService.isInFailedDownload(filename1)).toBeTrue(); + expect(onSuccess).not.toHaveBeenCalled(); expect(onFailure).toHaveBeenCalled(); - })); - - it('should call the successful callback when loading an image after' + - ' trying to get its image url', fakeAsync(() => { - imagePreloaderService.init(exploration); - imagePreloaderService.kickOffImagePreloader(initStateName); - - var onSuccess = jasmine.createSpy('success'); - var onFailure = jasmine.createSpy('fail'); - // This throws "Argument of type 'mockReaderObject' is not assignable - // to parameter of type 'FileReader'.". We need to suppress this error - // because 'FileReader' has around 15 more properties. We have only defined - // the properties we need in 'mockReaderObject'. - // @ts-expect-error - spyOn(window, 'FileReader').and.returnValue(new mockReaderObject()); - - imagePreloaderService.getImageUrlAsync(filename1) - .then(onSuccess, onFailure); - - httpTestingController.expectOne(requestUrl1).flush(imageBlob); - httpTestingController.expectOne(requestUrl2); - httpTestingController.expectOne(requestUrl3); - expect(imagePreloaderService.getFilenamesOfImageCurrentlyDownloading()) - .toEqual([filename1, filename2, filename3]); - expect(imagePreloaderService.isLoadingImageFile(filename1)).toBeTrue(); - - flushMicrotasks(); - - httpTestingController.expectOne(requestUrl4); - expect(imagePreloaderService.getFilenamesOfImageCurrentlyDownloading()) - .toEqual([filename2, filename3, filename4]); - - expect(onSuccess).toHaveBeenCalled(); - expect(onFailure).not.toHaveBeenCalled(); - })); - - it('should call the failed callback when loading an image fails after' + - ' trying to get its image url', fakeAsync(() => { - imagePreloaderService.init(exploration); - imagePreloaderService.kickOffImagePreloader(initStateName); - - var onSuccess = jasmine.createSpy('success'); - var onFailure = jasmine.createSpy('fail'); - - imagePreloaderService.getImageUrlAsync(filename1) - .then(onSuccess, onFailure); - - httpTestingController.expectOne(requestUrl1) - .flush(imageBlob, {status: 404, statusText: 'Status Text'}); - httpTestingController.expectOne(requestUrl2); - httpTestingController.expectOne(requestUrl3); - expect(imagePreloaderService.getFilenamesOfImageCurrentlyDownloading()) - .toEqual([filename1, filename2, filename3]); - expect(imagePreloaderService.isInFailedDownload(filename1)).toBeFalse(); - - flushMicrotasks(); - - httpTestingController.expectOne(requestUrl4); - expect(imagePreloaderService.getFilenamesOfImageCurrentlyDownloading()) - .toEqual([filename2, filename3, filename4]); - expect(imagePreloaderService.isInFailedDownload(filename1)).toBeTrue(); - - expect(onSuccess).not.toHaveBeenCalled(); - expect(onFailure).toHaveBeenCalled(); - })); + }) + ); }); diff --git a/core/templates/pages/exploration-player-page/services/image-preloader.service.ts b/core/templates/pages/exploration-player-page/services/image-preloader.service.ts index ea7db88c0731..f6ff07e4c84e 100644 --- a/core/templates/pages/exploration-player-page/services/image-preloader.service.ts +++ b/core/templates/pages/exploration-player-page/services/image-preloader.service.ts @@ -16,17 +16,17 @@ * @fileoverview Service to preload image into AssetsBackendApiService's cache. */ -import { Injectable } from '@angular/core'; -import { SafeResourceUrl } from '@angular/platform-browser'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {SafeResourceUrl} from '@angular/platform-browser'; +import {downgradeInjectable} from '@angular/upgrade/static'; -import { AppConstants } from 'app.constants'; -import { Exploration } from 'domain/exploration/ExplorationObjectFactory'; -import { ExtractImageFilenamesFromModelService } from 'pages/exploration-player-page/services/extract-image-filenames-from-model.service'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; -import { ComputeGraphService } from 'services/compute-graph.service'; -import { ContextService } from 'services/context.service'; -import { SvgSanitizerService } from 'services/svg-sanitizer.service'; +import {AppConstants} from 'app.constants'; +import {Exploration} from 'domain/exploration/ExplorationObjectFactory'; +import {ExtractImageFilenamesFromModelService} from 'pages/exploration-player-page/services/extract-image-filenames-from-model.service'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; +import {ComputeGraphService} from 'services/compute-graph.service'; +import {ContextService} from 'services/context.service'; +import {SvgSanitizerService} from 'services/svg-sanitizer.service'; interface ImageCallback { // This property will be null when the SVG uploaded is not valid or when @@ -42,16 +42,16 @@ export interface ImageDimensions { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ImagePreloaderService { constructor( - private assetsBackendApiService: AssetsBackendApiService, - private computeGraphService: ComputeGraphService, - private contextService: ContextService, - private ExtractImageFilenamesFromModelService: - ExtractImageFilenamesFromModelService, - private svgSanitizerService: SvgSanitizerService) {} + private assetsBackendApiService: AssetsBackendApiService, + private computeGraphService: ComputeGraphService, + private contextService: ContextService, + private ExtractImageFilenamesFromModelService: ExtractImageFilenamesFromModelService, + private svgSanitizerService: SvgSanitizerService + ) {} // This property is initialized using int method and we need to do // non-null assertion. For more information, see @@ -89,17 +89,19 @@ export class ImagePreloaderService { * preloader should start. */ kickOffImagePreloader(sourceStateName: string): void { - this.filenamesOfImageToBeDownloaded = ( - this.getImageFilenamesInBfsOrder(sourceStateName)); - const imageFilesInGivenState = ( + this.filenamesOfImageToBeDownloaded = + this.getImageFilenamesInBfsOrder(sourceStateName); + const imageFilesInGivenState = this.ExtractImageFilenamesFromModelService.getImageFilenamesInState( - this.exploration.states.getState(sourceStateName))); - this.filenamesOfImageFailedToDownload = ( + this.exploration.states.getState(sourceStateName) + ); + this.filenamesOfImageFailedToDownload = this.filenamesOfImageFailedToDownload.filter( - filename => !imageFilesInGivenState.includes(filename))); + filename => !imageFilesInGivenState.includes(filename) + ); while ( this.filenamesOfImageCurrentlyDownloading.length < - AppConstants.MAX_NUM_IMAGE_FILES_TO_DOWNLOAD_SIMULTANEOUSLY && + AppConstants.MAX_NUM_IMAGE_FILES_TO_DOWNLOAD_SIMULTANEOUSLY && this.filenamesOfImageToBeDownloaded.length > 0 ) { const imageFilename = this.filenamesOfImageToBeDownloaded.shift(); @@ -135,10 +137,12 @@ export class ImagePreloaderService { this.ExtractImageFilenamesFromModelService.getImageFilenamesInState( state ).forEach(filename => { - var isFileCurrentlyDownloading = ( - this.filenamesOfImageCurrentlyDownloading.includes(filename)); - if (!this.assetsBackendApiService.isCached(filename) && - !isFileCurrentlyDownloading) { + var isFileCurrentlyDownloading = + this.filenamesOfImageCurrentlyDownloading.includes(filename); + if ( + !this.assetsBackendApiService.isCached(filename) && + !isFileCurrentlyDownloading + ) { numImagesNeitherInCacheNorDownloading += 1; } if (isFileCurrentlyDownloading) { @@ -146,8 +150,10 @@ export class ImagePreloaderService { } }); - if (numImagesNeitherInCacheNorDownloading > 0 && - numImageFilesCurrentlyDownloading <= 1) { + if ( + numImagesNeitherInCacheNorDownloading > 0 && + numImageFilesCurrentlyDownloading <= 1 + ) { this.cancelPreloading(); this.kickOffImagePreloader(stateName); } @@ -155,45 +161,51 @@ export class ImagePreloaderService { } /** - * Gets the dimensions of the images from the filename provided. - * @param {string} filename - The string from which the dimensions of the - * images should be extracted. - */ + * Gets the dimensions of the images from the filename provided. + * @param {string} filename - The string from which the dimensions of the + * images should be extracted. + */ getDimensionsOfImage(filename: string): ImageDimensions { const dimensionsRegex = RegExp( - '[^/]+_height_([0-9]+)_width_([0-9]+)\\.(png|jpeg|jpg|gif|svg)$', 'g'); + '[^/]+_height_([0-9]+)_width_([0-9]+)\\.(png|jpeg|jpg|gif|svg)$', + 'g' + ); var imageDimensions = dimensionsRegex.exec(filename); if (imageDimensions) { var dimensions = { height: Number(imageDimensions[1]), - width: Number(imageDimensions[2]) + width: Number(imageDimensions[2]), }; return dimensions; } else { throw new Error( - `Input path ${filename} is invalid, it does not contain dimensions.`); + `Input path ${filename} is invalid, it does not contain dimensions.` + ); } } /** - * Gets the dimensions of the math SVGs from the SVG filename provided. - * @param {string} filename - The string from which the dimensions of the - * math SVGs should be extracted. - */ + * Gets the dimensions of the math SVGs from the SVG filename provided. + * @param {string} filename - The string from which the dimensions of the + * math SVGs should be extracted. + */ getDimensionsOfMathSvg(filename: string): ImageDimensions { var dimensionsRegex = RegExp( - '[^/]+_height_([0-9d]+)_width_([0-9d]+)_vertical_([0-9d]+)\\.svg', 'g'); + '[^/]+_height_([0-9d]+)_width_([0-9d]+)_vertical_([0-9d]+)\\.svg', + 'g' + ); var imageDimensions = dimensionsRegex.exec(filename); if (imageDimensions) { var dimensions = { height: Number(imageDimensions[1].replace('d', '.')), width: Number(imageDimensions[2].replace('d', '.')), - verticalPadding: Number(imageDimensions[3].replace('d', '.')) + verticalPadding: Number(imageDimensions[3].replace('d', '.')), }; return dimensions; } else { throw new Error( - `Input path ${filename} is invalid, it does not contain dimensions.`); + `Input path ${filename} is invalid, it does not contain dimensions.` + ); } } @@ -211,13 +223,14 @@ export class ImagePreloaderService { } private convertImageFileToSafeBase64Url( - imageFile: Blob, - callback: ( - // This property will be null when the SVG uploaded is not valid or when - // the image is not yet uploaded. Also FileReader.result dependency is - // of type null. - src: string | SafeResourceUrl | null - ) => void): void { + imageFile: Blob, + callback: ( + // This property will be null when the SVG uploaded is not valid or when + // the image is not yet uploaded. Also FileReader.result dependency is + // of type null. + src: string | SafeResourceUrl | null + ) => void + ): void { const reader = new FileReader(); reader.onloadend = () => { if (imageFile.type === 'image/svg+xml') { @@ -225,7 +238,9 @@ export class ImagePreloaderService { this.svgSanitizerService.getTrustedSvgResourceUrl( // Typecasting is needed because FileReader.result dependency is of // type ArrayBuffer | string | null. - reader.result as string)); + reader.result as string + ) + ); } else { callback(reader.result); } @@ -241,26 +256,27 @@ export class ImagePreloaderService { * Url of the loaded image is obtained. */ async getImageUrlAsync( - filename: string + filename: string ): Promise { return new Promise((resolve, reject) => { let entityType = this.contextService.getEntityType(); - if (entityType && (this.assetsBackendApiService.isCached(filename) || - this.isInFailedDownload(filename))) { - this.assetsBackendApiService.loadImage( - entityType, this.contextService.getEntityId(), filename - ).then( - loadedImageFile => { + if ( + entityType && + (this.assetsBackendApiService.isCached(filename) || + this.isInFailedDownload(filename)) + ) { + this.assetsBackendApiService + .loadImage(entityType, this.contextService.getEntityId(), filename) + .then(loadedImageFile => { if (this.isInFailedDownload(loadedImageFile.filename)) { this.removeFromFailedDownload(loadedImageFile.filename); } this.convertImageFileToSafeBase64Url(loadedImageFile.data, resolve); - }, - reject); + }, reject); } else { this.imageLoadedCallback[filename] = { resolveMethod: resolve, - rejectMethod: reject + rejectMethod: reject, }; } }); @@ -291,15 +307,18 @@ export class ImagePreloaderService { const explorationInitStateName = this.exploration.getInitialState().name; var imageFilenames: string[] = []; if (explorationInitStateName) { - var stateNamesInBfsOrder = ( + var stateNamesInBfsOrder = this.computeGraphService.computeBfsTraversalOfStates( - explorationInitStateName, this.exploration.getStates(), - sourceStateName)); + explorationInitStateName, + this.exploration.getStates(), + sourceStateName + ); stateNamesInBfsOrder.forEach(stateName => { var state = this.exploration.states.getState(stateName); this.ExtractImageFilenamesFromModelService.getImageFilenamesInState( - state).forEach(filename => imageFilenames.push(filename)); + state + ).forEach(filename => imageFilenames.push(filename)); }); } return imageFilenames; @@ -314,8 +333,10 @@ export class ImagePreloaderService { private removeCurrentAndLoadNextImage(filename: string): void { this.filenamesOfImageCurrentlyDownloading.splice( this.filenamesOfImageCurrentlyDownloading.findIndex( - imageFilename => filename === imageFilename), - 1); + imageFilename => filename === imageFilename + ), + 1 + ); if (this.filenamesOfImageToBeDownloaded.length > 0) { var nextImageFilename = this.filenamesOfImageToBeDownloaded.shift(); if (nextImageFilename) { @@ -330,30 +351,37 @@ export class ImagePreloaderService { * @param {string} imageFilename - The filename of the image to be loaded. */ private loadImage(imageFilename: string): void { - this.assetsBackendApiService.loadImage( - AppConstants.ENTITY_TYPE.EXPLORATION, - this.contextService.getExplorationId(), imageFilename - ).then( - loadedImage => { - this.removeCurrentAndLoadNextImage(loadedImage.filename); - if (this.imageLoadedCallback[loadedImage.filename]) { - var onLoadImageResolve = ( - this.imageLoadedCallback[loadedImage.filename].resolveMethod); - this.convertImageFileToSafeBase64Url( - loadedImage.data, onLoadImageResolve); - delete this.imageLoadedCallback[loadedImage.filename]; + this.assetsBackendApiService + .loadImage( + AppConstants.ENTITY_TYPE.EXPLORATION, + this.contextService.getExplorationId(), + imageFilename + ) + .then( + loadedImage => { + this.removeCurrentAndLoadNextImage(loadedImage.filename); + if (this.imageLoadedCallback[loadedImage.filename]) { + var onLoadImageResolve = + this.imageLoadedCallback[loadedImage.filename].resolveMethod; + this.convertImageFileToSafeBase64Url( + loadedImage.data, + onLoadImageResolve + ); + delete this.imageLoadedCallback[loadedImage.filename]; + } + }, + filename => { + if (this.imageLoadedCallback[filename]) { + this.imageLoadedCallback[filename].rejectMethod(); + delete this.imageLoadedCallback[filename]; + } + this.filenamesOfImageFailedToDownload.push(filename); + this.removeCurrentAndLoadNextImage(filename); } - }, - filename => { - if (this.imageLoadedCallback[filename]) { - this.imageLoadedCallback[filename].rejectMethod(); - delete this.imageLoadedCallback[filename]; - } - this.filenamesOfImageFailedToDownload.push(filename); - this.removeCurrentAndLoadNextImage(filename); - }); + ); } } -angular.module('oppia').factory( - 'ImagePreloaderService', downgradeInjectable(ImagePreloaderService)); +angular + .module('oppia') + .factory('ImagePreloaderService', downgradeInjectable(ImagePreloaderService)); diff --git a/core/templates/pages/exploration-player-page/services/learner-answer-info.service.spec.ts b/core/templates/pages/exploration-player-page/services/learner-answer-info.service.spec.ts index 75ee59ebab6d..ff1ff2e31f19 100644 --- a/core/templates/pages/exploration-player-page/services/learner-answer-info.service.spec.ts +++ b/core/templates/pages/exploration-player-page/services/learner-answer-info.service.spec.ts @@ -16,18 +16,25 @@ * @fileoverview Unit tests for the learner answer info service. */ -import { TestBed } from '@angular/core/testing'; -import { AnswerClassificationResult } from 'domain/classifier/answer-classification-result.model'; -import { OutcomeObjectFactory } from 'domain/exploration/OutcomeObjectFactory'; -import { State, StateBackendDict, StateObjectFactory } from 'domain/state/StateObjectFactory'; -import { LearnerAnswerDetailsBackendApiService } from 'domain/statistics/learner-answer-details-backend-api.service'; -import { AnswerClassificationService, InteractionRulesService } from 'pages/exploration-player-page/services/answer-classification.service'; -import { LearnerAnswerInfoService } from 'pages/exploration-player-page/services/learner-answer-info.service'; -import { ExplorationPlayerConstants } from 'pages/exploration-player-page/exploration-player-page.constants'; -import { TextInputRulesService } from 'interactions/TextInput/directives/text-input-rules.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; +import {TestBed} from '@angular/core/testing'; +import {AnswerClassificationResult} from 'domain/classifier/answer-classification-result.model'; +import {OutcomeObjectFactory} from 'domain/exploration/OutcomeObjectFactory'; +import { + State, + StateBackendDict, + StateObjectFactory, +} from 'domain/state/StateObjectFactory'; +import {LearnerAnswerDetailsBackendApiService} from 'domain/statistics/learner-answer-details-backend-api.service'; +import { + AnswerClassificationService, + InteractionRulesService, +} from 'pages/exploration-player-page/services/answer-classification.service'; +import {LearnerAnswerInfoService} from 'pages/exploration-player-page/services/learner-answer-info.service'; +import {ExplorationPlayerConstants} from 'pages/exploration-player-page/exploration-player-page.constants'; +import {TextInputRulesService} from 'interactions/TextInput/directives/text-input-rules.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; -describe('Learner answer info service', () =>{ +describe('Learner answer info service', () => { let sof: StateObjectFactory; let oof: OutcomeObjectFactory; let stateDict: StateBackendDict; @@ -44,20 +51,20 @@ describe('Learner answer info service', () =>{ beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [LearnerAnswerInfoService] + providers: [LearnerAnswerInfoService], }); stateDict = { content: { content_id: 'content', - html: 'content' + html: 'content', }, recorded_voiceovers: { voiceovers_mapping: { content: {}, default_outcome: {}, feedback_1: {}, - feedback_2: {} - } + feedback_2: {}, + }, }, interaction: { id: 'TextInput', @@ -65,94 +72,103 @@ describe('Learner answer info service', () =>{ placeholder: { value: { content_id: 'ca_placeholder_0', - unicode_str: '' - } + unicode_str: '', + }, }, - rows: { value: 1 }, + rows: {value: 1}, catchMisspellings: { - value: false - } + value: false, + }, }, - answer_groups: [{ - outcome: { - dest: 'outcome 1', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_1', - html: '' + answer_groups: [ + { + outcome: { + dest: 'outcome 1', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: '', + }, + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, }, - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null + rule_specs: [ + { + rule_type: 'Equals', + inputs: { + x: { + contentId: 'rule_input_0', + normalizedStrSet: ['10'], + }, + }, + }, + ], + training_data: [], + tagged_skill_misconception_id: '', }, - rule_specs: [{ - rule_type: 'Equals', - inputs: { - x: { - contentId: 'rule_input_0', - normalizedStrSet: ['10'] - } - } - }], - training_data: [], - tagged_skill_misconception_id: '' - }, { - outcome: { - dest: 'outcome 2', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: '' + { + outcome: { + dest: 'outcome 2', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: '', + }, + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, }, - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null + rule_specs: [ + { + rule_type: 'Equals', + inputs: { + x: { + contentId: 'rule_input_1', + normalizedStrSet: ['5'], + }, + }, + }, + { + rule_type: 'Equals', + inputs: { + x: { + contentId: 'rule_input_2', + normalizedStrSet: ['6'], + }, + }, + }, + { + rule_type: 'FuzzyEquals', + inputs: { + x: { + contentId: 'rule_input_3', + normalizedStrSet: ['7'], + }, + }, + }, + ], + training_data: [], + tagged_skill_misconception_id: '', }, - rule_specs: [{ - rule_type: 'Equals', - inputs: { - x: { - contentId: 'rule_input_1', - normalizedStrSet: ['5'] - } - } - }, { - rule_type: 'Equals', - inputs: { - x: { - contentId: 'rule_input_2', - normalizedStrSet: ['6'] - } - } - }, { - rule_type: 'FuzzyEquals', - inputs: { - x: { - contentId: 'rule_input_3', - normalizedStrSet: ['7'] - } - } - }], - training_data: [], - tagged_skill_misconception_id: '' - }], + ], default_outcome: { dest: 'default', dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: '' + html: '', }, labelled_as_correct: false, param_changes: [], refresher_exploration_id: null, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, hints: [], confirmed_unclassified_answers: [], - solution: null + solution: null, }, param_changes: [], solicit_answer_details: true, @@ -165,8 +181,7 @@ describe('Learner answer info service', () =>{ oof = TestBed.get(OutcomeObjectFactory); learnerAnswerInfoService = TestBed.get(LearnerAnswerInfoService); answerClassificationService = TestBed.get(AnswerClassificationService); - ladbas = TestBed.get( - LearnerAnswerDetailsBackendApiService); + ladbas = TestBed.get(LearnerAnswerDetailsBackendApiService); DEFAULT_OUTCOME_CLASSIFICATION = ExplorationPlayerConstants.DEFAULT_OUTCOME_CLASSIFICATION; firstState = sof.createFromBackendDict('new state', stateDict); @@ -174,10 +189,17 @@ describe('Learner answer info service', () =>{ thirdState = sof.createFromBackendDict('demo state', stateDict); tirs = TestBed.get(TextInputRulesService); - spyOn(answerClassificationService, 'getMatchingClassificationResult') - .and.returnValue(new AnswerClassificationResult( - oof.createNew('default', 'default_outcome', '', []), 2, 0, - DEFAULT_OUTCOME_CLASSIFICATION)); + spyOn( + answerClassificationService, + 'getMatchingClassificationResult' + ).and.returnValue( + new AnswerClassificationResult( + oof.createNew('default', 'default_outcome', '', []), + 2, + 0, + DEFAULT_OUTCOME_CLASSIFICATION + ) + ); mockAnswer = 'This is my answer'; // Spying the random function to return 0, so that @@ -189,122 +211,173 @@ describe('Learner answer info service', () =>{ }); describe('.initLearnerAnswerInfo', () => { - beforeEach(function() { + beforeEach(function () { learnerAnswerInfoService.initLearnerAnswerInfoService( - '10', firstState, mockAnswer, tirs, false); + '10', + firstState, + mockAnswer, + tirs, + false + ); }); it('should return can ask learner for answer info true', () => { expect(learnerAnswerInfoService.getCanAskLearnerForAnswerInfo()).toEqual( - true); + true + ); }); it('should return current answer', () => { expect(learnerAnswerInfoService.getCurrentAnswer()).toEqual( - 'This is my answer'); + 'This is my answer' + ); }); it('should return current interaction rules service', () => { expect( - learnerAnswerInfoService.getCurrentInteractionRulesService()).toEqual( - tirs); + learnerAnswerInfoService.getCurrentInteractionRulesService() + ).toEqual(tirs); }); }); describe('learner answer info service', () => { - beforeEach(function() { + beforeEach(function () { learnerAnswerInfoService.initLearnerAnswerInfoService( - '10', firstState, mockAnswer, tirs, false); + '10', + firstState, + mockAnswer, + tirs, + false + ); }); it('should not ask for answer details for same state', () => { expect(learnerAnswerInfoService.getCanAskLearnerForAnswerInfo()).toEqual( - true); + true + ); learnerAnswerInfoService.recordLearnerAnswerInfo('My answer details'); expect(learnerAnswerInfoService.getCanAskLearnerForAnswerInfo()).toEqual( - false); + false + ); learnerAnswerInfoService.initLearnerAnswerInfoService( - '10', firstState, mockAnswer, tirs, false); + '10', + firstState, + mockAnswer, + tirs, + false + ); expect(learnerAnswerInfoService.getCanAskLearnerForAnswerInfo()).toEqual( - false); + false + ); }); }); - describe( - 'should not ask for answer details for trivial interaction ids', - () => { - beforeEach(function() { - firstState.interaction.id = 'EndExploration'; - learnerAnswerInfoService.initLearnerAnswerInfoService( - '10', firstState, mockAnswer, tirs, false); - }); - - it('should return can ask learner for answer info false', () => { - expect( - learnerAnswerInfoService.getCanAskLearnerForAnswerInfo()).toEqual( - false); - }); + describe('should not ask for answer details for trivial interaction ids', () => { + beforeEach(function () { + firstState.interaction.id = 'EndExploration'; + learnerAnswerInfoService.initLearnerAnswerInfoService( + '10', + firstState, + mockAnswer, + tirs, + false + ); }); - describe('init learner answer info service with solicit answer details false', - () => { - beforeEach(function() { - firstState.solicitAnswerDetails = false; - learnerAnswerInfoService.initLearnerAnswerInfoService( - '10', firstState, mockAnswer, tirs, false); - }); - it('should return can ask learner for answer info false', () => { - expect( - learnerAnswerInfoService.getCanAskLearnerForAnswerInfo()).toEqual( - false); - }); + it('should return can ask learner for answer info false', () => { + expect(learnerAnswerInfoService.getCanAskLearnerForAnswerInfo()).toEqual( + false + ); }); + }); + describe('init learner answer info service with solicit answer details false', () => { + beforeEach(function () { + firstState.solicitAnswerDetails = false; + learnerAnswerInfoService.initLearnerAnswerInfoService( + '10', + firstState, + mockAnswer, + tirs, + false + ); + }); + it('should return can ask learner for answer info false', () => { + expect(learnerAnswerInfoService.getCanAskLearnerForAnswerInfo()).toEqual( + false + ); + }); + }); describe('.recordLearnerAnswerInfo', () => { - beforeEach(function() { + beforeEach(function () { learnerAnswerInfoService.initLearnerAnswerInfoService( - '10', firstState, mockAnswer, tirs, false); + '10', + firstState, + mockAnswer, + tirs, + false + ); }); - it('should record learner answer details', (() => { + it('should record learner answer details', () => { spyOn(ladbas, 'recordLearnerAnswerDetailsAsync'); learnerAnswerInfoService.recordLearnerAnswerInfo('My details'); - expect( - ladbas.recordLearnerAnswerDetailsAsync).toHaveBeenCalledWith( - '10', 'new state', 'TextInput', 'This is my answer', 'My details'); - })); + expect(ladbas.recordLearnerAnswerDetailsAsync).toHaveBeenCalledWith( + '10', + 'new state', + 'TextInput', + 'This is my answer', + 'My details' + ); + }); }); describe('learner answer info service', () => { - beforeEach(function() { + beforeEach(function () { learnerAnswerInfoService.initLearnerAnswerInfoService( - '10', firstState, mockAnswer, tirs, false); + '10', + firstState, + mockAnswer, + tirs, + false + ); learnerAnswerInfoService.recordLearnerAnswerInfo('My details 1'); learnerAnswerInfoService.initLearnerAnswerInfoService( - '10', secondState, mockAnswer, tirs, false); + '10', + secondState, + mockAnswer, + tirs, + false + ); learnerAnswerInfoService.recordLearnerAnswerInfo('My details 1'); }); it('should not record answer details more than two times', () => { learnerAnswerInfoService.initLearnerAnswerInfoService( - '10', thirdState, mockAnswer, tirs, false); + '10', + thirdState, + mockAnswer, + tirs, + false + ); expect(learnerAnswerInfoService.getCanAskLearnerForAnswerInfo()).toEqual( - false); + false + ); }); }); describe('return html from the service', () => { it('should return solicit answer details question', () => { expect( - learnerAnswerInfoService.getSolicitAnswerDetailsQuestion()).toEqual( - '

'); + learnerAnswerInfoService.getSolicitAnswerDetailsQuestion() + ).toEqual('

'); }); it('should return solicit answer details feedabck', () => { expect( - learnerAnswerInfoService.getSolicitAnswerDetailsFeedback()).toEqual( - '

'); + learnerAnswerInfoService.getSolicitAnswerDetailsFeedback() + ).toEqual('

'); }); }); }); diff --git a/core/templates/pages/exploration-player-page/services/learner-answer-info.service.ts b/core/templates/pages/exploration-player-page/services/learner-answer-info.service.ts index d33f4d8add39..d1265e42151d 100644 --- a/core/templates/pages/exploration-player-page/services/learner-answer-info.service.ts +++ b/core/templates/pages/exploration-player-page/services/learner-answer-info.service.ts @@ -16,25 +16,25 @@ * @fileoverview Service for learner answer info. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; -import { AppConstants } from 'app.constants'; -import { State } from 'domain/state/StateObjectFactory'; -import { LearnerAnswerDetailsBackendApiService } from - 'domain/statistics/learner-answer-details-backend-api.service'; -import { AnswerClassificationService, InteractionRulesService } from - 'pages/exploration-player-page/services/answer-classification.service'; +import {AppConstants} from 'app.constants'; +import {State} from 'domain/state/StateObjectFactory'; +import {LearnerAnswerDetailsBackendApiService} from 'domain/statistics/learner-answer-details-backend-api.service'; +import { + AnswerClassificationService, + InteractionRulesService, +} from 'pages/exploration-player-page/services/answer-classification.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) - export class LearnerAnswerInfoService { constructor( private answerClassificationService: AnswerClassificationService, - private learnerAnswerDetailsBackendApiService: - LearnerAnswerDetailsBackendApiService) {} + private learnerAnswerDetailsBackendApiService: LearnerAnswerDetailsBackendApiService + ) {} // These properties are initialized using init method and we need to do // non-null assertion. For more information, see @@ -58,14 +58,14 @@ export class LearnerAnswerInfoService { typeA: 0.25, // The probability index when the outcome is marked as correct i.e // labelled_as_correct property is true. - typeB: 0.10, + typeB: 0.1, // The probability index when the outcome is not the default outcome // and it is not marked as correct i.e. it is any general outcome. - typeC: 0.05 + typeC: 0.05, }; - INTERACTION_IDS_WITHOUT_ANSWER_DETAILS: readonly string[] = ( - AppConstants.INTERACTION_IDS_WITHOUT_ANSWER_DETAILS); + INTERACTION_IDS_WITHOUT_ANSWER_DETAILS: readonly string[] = + AppConstants.INTERACTION_IDS_WITHOUT_ANSWER_DETAILS; getRandomProbabilityIndex(): number { const min = 0; @@ -74,9 +74,12 @@ export class LearnerAnswerInfoService { } initLearnerAnswerInfoService( - entityId: string, state: State, answer: string, - interactionRulesService: InteractionRulesService, - alwaysAskLearnerForAnswerInfo: boolean): void { + entityId: string, + state: State, + answer: string, + interactionRulesService: InteractionRulesService, + alwaysAskLearnerForAnswerInfo: boolean + ): void { this.currentEntityId = entityId; this.currentAnswer = answer; this.currentInteractionRulesService = interactionRulesService; @@ -96,8 +99,11 @@ export class LearnerAnswerInfoService { throw new Error('Interaction id cannot be null.'); } - if (this.INTERACTION_IDS_WITHOUT_ANSWER_DETAILS.indexOf( - this.interactionId) !== -1) { + if ( + this.INTERACTION_IDS_WITHOUT_ANSWER_DETAILS.indexOf( + this.interactionId + ) !== -1 + ) { return; } @@ -114,10 +120,13 @@ export class LearnerAnswerInfoService { return; } - const classificationResult = ( + const classificationResult = this.answerClassificationService.getMatchingClassificationResult( - this.stateName, state.interaction, answer, - interactionRulesService)); + this.stateName, + state.interaction, + answer, + interactionRulesService + ); const outcome = classificationResult.outcome; let thresholdProbabilityIndex = null; const randomProbabilityIndex = this.getRandomProbabilityIndex(); @@ -130,8 +139,8 @@ export class LearnerAnswerInfoService { thresholdProbabilityIndex = this.probabilityIndexes.typeC; } - this.canAskLearnerForAnswerInfo = ( - randomProbabilityIndex <= thresholdProbabilityIndex); + this.canAskLearnerForAnswerInfo = + randomProbabilityIndex <= thresholdProbabilityIndex; } resetSubmittedAnswerInfoCount(): void { @@ -147,8 +156,12 @@ export class LearnerAnswerInfoService { throw new Error('State name cannot be null.'); } this.learnerAnswerDetailsBackendApiService.recordLearnerAnswerDetailsAsync( - this.currentEntityId, this.stateName, this.interactionId, - this.currentAnswer, answerDetails); + this.currentEntityId, + this.stateName, + this.interactionId, + this.currentAnswer, + answerDetails + ); this.submittedAnswerInfoCount++; this.visitedStates.push(this.stateName); this.canAskLearnerForAnswerInfo = false; @@ -169,16 +182,19 @@ export class LearnerAnswerInfoService { getSolicitAnswerDetailsQuestion(): string { var el = $('

'); el.attr('translate', 'I18N_SOLICIT_ANSWER_DETAILS_QUESTION'); - return ($('').append(el)).html(); + return $('').append(el).html(); } getSolicitAnswerDetailsFeedback(): string { var el = $('

'); el.attr('translate', 'I18N_SOLICIT_ANSWER_DETAILS_FEEDBACK'); - return ($('').append(el)).html(); + return $('').append(el).html(); } } -angular.module('oppia').factory( - 'LearnerAnswerInfoService', - downgradeInjectable(LearnerAnswerInfoService)); +angular + .module('oppia') + .factory( + 'LearnerAnswerInfoService', + downgradeInjectable(LearnerAnswerInfoService) + ); diff --git a/core/templates/pages/exploration-player-page/services/learner-local-nav-backend-api.service.spec.ts b/core/templates/pages/exploration-player-page/services/learner-local-nav-backend-api.service.spec.ts index 054c19a101ac..1387b1806270 100644 --- a/core/templates/pages/exploration-player-page/services/learner-local-nav-backend-api.service.spec.ts +++ b/core/templates/pages/exploration-player-page/services/learner-local-nav-backend-api.service.spec.ts @@ -15,10 +15,12 @@ * @fileoverview Unit tests for LearnerViewInfoBackendApiService. */ -import { HttpClientTestingModule, HttpTestingController } from - '@angular/common/http/testing'; -import { fakeAsync, flushMicrotasks, TestBed } from '@angular/core/testing'; -import { LearnerLocalNavBackendApiService } from './learner-local-nav-backend-api.service'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {fakeAsync, flushMicrotasks, TestBed} from '@angular/core/testing'; +import {LearnerLocalNavBackendApiService} from './learner-local-nav-backend-api.service'; describe('Learner Local Nav Backend Api Service', () => { let llnba: LearnerLocalNavBackendApiService; @@ -33,7 +35,7 @@ describe('Learner Local Nav Backend Api Service', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [LearnerLocalNavBackendApiService] + providers: [LearnerLocalNavBackendApiService], }); httpTestingController = TestBed.inject(HttpTestingController); llnba = TestBed.inject(LearnerLocalNavBackendApiService); @@ -43,11 +45,13 @@ describe('Learner Local Nav Backend Api Service', () => { }); it('should post report', fakeAsync(() => { - llnba.postReportAsync( - expId, { + llnba + .postReportAsync(expId, { report_type: reportType, report_text: reportText, - state}).then(successHandler, failHandler); + state, + }) + .then(successHandler, failHandler); let req = httpTestingController.expectOne(llnba.flagExplorationUrl); expect(req.request.method).toEqual('POST'); @@ -57,6 +61,5 @@ describe('Learner Local Nav Backend Api Service', () => { expect(successHandler).toHaveBeenCalled(); expect(failHandler).not.toHaveBeenCalled(); - } - )); + })); }); diff --git a/core/templates/pages/exploration-player-page/services/learner-local-nav-backend-api.service.ts b/core/templates/pages/exploration-player-page/services/learner-local-nav-backend-api.service.ts index a0dea77a7381..4d076098ba9a 100644 --- a/core/templates/pages/exploration-player-page/services/learner-local-nav-backend-api.service.ts +++ b/core/templates/pages/exploration-player-page/services/learner-local-nav-backend-api.service.ts @@ -16,14 +16,14 @@ * @fileoverview Backend Api Service for learner local nav. */ -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { ExplorationPlayerConstants } from '../exploration-player-page.constants'; -import { FlagExplorationModalResult } from '../modals/flag-exploration-modal.component'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {ExplorationPlayerConstants} from '../exploration-player-page.constants'; +import {FlagExplorationModalResult} from '../modals/flag-exploration-modal.component'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class LearnerLocalNavBackendApiService { // These properties are initialized using postReportAsync method and we @@ -37,18 +37,26 @@ export class LearnerLocalNavBackendApiService { ) {} async postReportAsync( - explorationId: string, result: FlagExplorationModalResult): - Promise { + explorationId: string, + result: FlagExplorationModalResult + ): Promise { this.flagExplorationUrl = this.urlInterpolationService.interpolateUrl( - ExplorationPlayerConstants.FLAG_EXPLORATION_URL_TEMPLATE, { - exploration_id: explorationId + ExplorationPlayerConstants.FLAG_EXPLORATION_URL_TEMPLATE, + { + exploration_id: explorationId, } ); - let report = ( - '[' + result.state + '] (' + result.report_type + ')' + - result.report_text); - return this.httpClient.post(this.flagExplorationUrl, { - report_text: report - }).toPromise(); + let report = + '[' + + result.state + + '] (' + + result.report_type + + ')' + + result.report_text; + return this.httpClient + .post(this.flagExplorationUrl, { + report_text: report, + }) + .toPromise(); } } diff --git a/core/templates/pages/exploration-player-page/services/learner-params.service.spec.ts b/core/templates/pages/exploration-player-page/services/learner-params.service.spec.ts index 7ddfe7154e9b..b1bf39b036e1 100644 --- a/core/templates/pages/exploration-player-page/services/learner-params.service.spec.ts +++ b/core/templates/pages/exploration-player-page/services/learner-params.service.spec.ts @@ -18,8 +18,7 @@ // TODO(#7222): Remove the following block of unnnecessary imports once // learner-params.service.ts is upgraded to Angular 8. -import { LearnerParamsService } from - 'pages/exploration-player-page/services/learner-params.service'; +import {LearnerParamsService} from 'pages/exploration-player-page/services/learner-params.service'; // ^^^ This block is to be removed. describe('Learner parameters service', () => { @@ -33,16 +32,16 @@ describe('Learner parameters service', () => { it('should correctly initialize parameters', () => { expect(learnerParamsService.getAllParams()).toEqual({}); learnerParamsService.init({ - a: 'b' + a: 'b', }); expect(learnerParamsService.getAllParams()).toEqual({ - a: 'b' + a: 'b', }); }); it('should correctly get and set parameters', () => { learnerParamsService.init({ - a: 'b' + a: 'b', }); expect(learnerParamsService.getValue('a')).toEqual('b'); learnerParamsService.setValue('a', 'c'); @@ -51,7 +50,7 @@ describe('Learner parameters service', () => { it('should not get an invalid parameter', () => { learnerParamsService.init({ - a: 'b' + a: 'b', }); expect(() => { learnerParamsService.getValue('b'); @@ -60,7 +59,7 @@ describe('Learner parameters service', () => { it('should not set an invalid parameter', () => { learnerParamsService.init({ - a: 'b' + a: 'b', }); expect(() => { learnerParamsService.setValue('b', 'c'); diff --git a/core/templates/pages/exploration-player-page/services/learner-params.service.ts b/core/templates/pages/exploration-player-page/services/learner-params.service.ts index 94d698e9def0..62ecbc5eb3cd 100644 --- a/core/templates/pages/exploration-player-page/services/learner-params.service.ts +++ b/core/templates/pages/exploration-player-page/services/learner-params.service.ts @@ -19,15 +19,15 @@ import cloneDeep from 'lodash/cloneDeep'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; export interface ExplorationParams { [paramName: string]: string; } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class LearnerParamsService { private _paramDict: ExplorationParams = {}; @@ -59,5 +59,6 @@ export class LearnerParamsService { } } -angular.module('oppia').factory( - 'LearnerParamsService', downgradeInjectable(LearnerParamsService)); +angular + .module('oppia') + .factory('LearnerParamsService', downgradeInjectable(LearnerParamsService)); diff --git a/core/templates/pages/exploration-player-page/services/learner-view-info-backend-api.service.spec.ts b/core/templates/pages/exploration-player-page/services/learner-view-info-backend-api.service.spec.ts index a6057ffabfc7..094eaea34e00 100644 --- a/core/templates/pages/exploration-player-page/services/learner-view-info-backend-api.service.spec.ts +++ b/core/templates/pages/exploration-player-page/services/learner-view-info-backend-api.service.spec.ts @@ -15,10 +15,12 @@ * @fileoverview Unit tests for LearnerViewInfoBackendApiService. */ -import { HttpClientTestingModule, HttpTestingController } from - '@angular/common/http/testing'; -import { fakeAsync, flushMicrotasks, TestBed } from '@angular/core/testing'; -import { LearnerViewInfoBackendApiService } from './learner-view-info-backend-api.service'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {fakeAsync, flushMicrotasks, TestBed} from '@angular/core/testing'; +import {LearnerViewInfoBackendApiService} from './learner-view-info-backend-api.service'; describe('Learner View Info Backend Api Service', () => { let lvibas: LearnerViewInfoBackendApiService; @@ -29,7 +31,7 @@ describe('Learner View Info Backend Api Service', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [LearnerViewInfoBackendApiService] + providers: [LearnerViewInfoBackendApiService], }); httpTestingController = TestBed.get(HttpTestingController); lvibas = TestBed.get(LearnerViewInfoBackendApiService); @@ -38,28 +40,26 @@ describe('Learner View Info Backend Api Service', () => { httpTestingController.verify(); }); - it('should fetch Learner View Info when calling fetchLearnerInfo', - fakeAsync(() => { - let stringifiedExpIds = ''; - let includePrivateExplorations = ''; - let jobOutput = { - summaries: ['response1', - 'response2'] - }; - lvibas.fetchLearnerInfoAsync( - stringifiedExpIds, - includePrivateExplorations).then(successHandler, failHandler); + it('should fetch Learner View Info when calling fetchLearnerInfo', fakeAsync(() => { + let stringifiedExpIds = ''; + let includePrivateExplorations = ''; + let jobOutput = { + summaries: ['response1', 'response2'], + }; + lvibas + .fetchLearnerInfoAsync(stringifiedExpIds, includePrivateExplorations) + .then(successHandler, failHandler); - let req = httpTestingController.expectOne( - '/explorationsummarieshandler/' + - 'data?stringified_exp_ids=&include_private_explorations='); - expect(req.request.method).toEqual('GET'); - req.flush(jobOutput); + let req = httpTestingController.expectOne( + '/explorationsummarieshandler/' + + 'data?stringified_exp_ids=&include_private_explorations=' + ); + expect(req.request.method).toEqual('GET'); + req.flush(jobOutput); - flushMicrotasks(); + flushMicrotasks(); - expect(successHandler).toHaveBeenCalled(); - expect(failHandler).not.toHaveBeenCalled(); - } - )); + expect(successHandler).toHaveBeenCalled(); + expect(failHandler).not.toHaveBeenCalled(); + })); }); diff --git a/core/templates/pages/exploration-player-page/services/learner-view-info-backend-api.service.ts b/core/templates/pages/exploration-player-page/services/learner-view-info-backend-api.service.ts index 7827d74bb7fe..c556b97681ca 100644 --- a/core/templates/pages/exploration-player-page/services/learner-view-info-backend-api.service.ts +++ b/core/templates/pages/exploration-player-page/services/learner-view-info-backend-api.service.ts @@ -15,36 +15,43 @@ * @fileoverview Backend api service for fetching the learner data; */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { LearnerExplorationSummaryBackendDict } from 'domain/summary/learner-exploration-summary.model'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {AppConstants} from 'app.constants'; +import {LearnerExplorationSummaryBackendDict} from 'domain/summary/learner-exploration-summary.model'; interface LearnerViewBackendDict { - 'summaries': LearnerExplorationSummaryBackendDict[]; + summaries: LearnerExplorationSummaryBackendDict[]; } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class LearnerViewInfoBackendApiService { - constructor( - private http: HttpClient - ) {} + constructor(private http: HttpClient) {} async fetchLearnerInfoAsync( - stringifiedExpIds: string, - includePrivateExplorations: string): Promise { - return this.http.get( - AppConstants.EXPLORATION_SUMMARY_DATA_URL_TEMPLATE, { - params: { - stringified_exp_ids: stringifiedExpIds, - include_private_explorations: includePrivateExplorations - }}).toPromise(); + stringifiedExpIds: string, + includePrivateExplorations: string + ): Promise { + return this.http + .get( + AppConstants.EXPLORATION_SUMMARY_DATA_URL_TEMPLATE, + { + params: { + stringified_exp_ids: stringifiedExpIds, + include_private_explorations: includePrivateExplorations, + }, + } + ) + .toPromise(); } } -angular.module('oppia').factory( - 'LearnerViewInfoBackendApiService', - downgradeInjectable(LearnerViewInfoBackendApiService)); +angular + .module('oppia') + .factory( + 'LearnerViewInfoBackendApiService', + downgradeInjectable(LearnerViewInfoBackendApiService) + ); diff --git a/core/templates/pages/exploration-player-page/services/learner-view-rating-backend-api.service.spec.ts b/core/templates/pages/exploration-player-page/services/learner-view-rating-backend-api.service.spec.ts index c7ad6af56a6a..e1c73af4912c 100644 --- a/core/templates/pages/exploration-player-page/services/learner-view-rating-backend-api.service.spec.ts +++ b/core/templates/pages/exploration-player-page/services/learner-view-rating-backend-api.service.spec.ts @@ -15,12 +15,14 @@ * @fileoverview Unit tests for LearnerViewRatingBackendApiService. */ -import { HttpClientTestingModule, HttpTestingController } from - '@angular/common/http/testing'; -import { fakeAsync, flushMicrotasks, TestBed } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; -import { MockTranslateService } from 'components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec'; -import { LearnerViewRatingBackendApiService } from './learner-view-rating-backend-api.service'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {fakeAsync, flushMicrotasks, TestBed} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; +import {MockTranslateService} from 'components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec'; +import {LearnerViewRatingBackendApiService} from './learner-view-rating-backend-api.service'; describe('Learner View Rating Backend Api Service', () => { let lvrbas: LearnerViewRatingBackendApiService; @@ -35,9 +37,9 @@ describe('Learner View Rating Backend Api Service', () => { LearnerViewRatingBackendApiService, { provide: TranslateService, - useClass: MockTranslateService - } - ] + useClass: MockTranslateService, + }, + ], }); httpTestingController = TestBed.inject(HttpTestingController); lvrbas = TestBed.inject(LearnerViewRatingBackendApiService); @@ -48,12 +50,11 @@ describe('Learner View Rating Backend Api Service', () => { it('should fetch user rating', fakeAsync(() => { let jobOutput = { - user_rating: 5 + user_rating: 5, }; lvrbas.getUserRatingAsync().then(successHandler, failHandler); - let req = httpTestingController.expectOne( - lvrbas.ratingsUrl); + let req = httpTestingController.expectOne(lvrbas.ratingsUrl); expect(req.request.method).toEqual('GET'); req.flush(jobOutput); @@ -65,8 +66,7 @@ describe('Learner View Rating Backend Api Service', () => { it('should submit user rating', fakeAsync(() => { lvrbas.submitUserRatingAsync(3).then(successHandler, failHandler); - let req = httpTestingController.expectOne( - lvrbas.ratingsUrl); + let req = httpTestingController.expectOne(lvrbas.ratingsUrl); expect(req.request.method).toEqual('PUT'); req.flush({}); diff --git a/core/templates/pages/exploration-player-page/services/learner-view-rating-backend-api.service.ts b/core/templates/pages/exploration-player-page/services/learner-view-rating-backend-api.service.ts index edebe549bbe9..5eac4f735cc1 100644 --- a/core/templates/pages/exploration-player-page/services/learner-view-rating-backend-api.service.ts +++ b/core/templates/pages/exploration-player-page/services/learner-view-rating-backend-api.service.ts @@ -17,16 +17,16 @@ * in the learner view. */ -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { ExplorationEngineService } from './exploration-engine.service'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {ExplorationEngineService} from './exploration-engine.service'; interface LearnerViewRatingBackendResponse { - 'user_rating': number; + user_rating: number; } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class LearnerViewRatingBackendApiService { explorationId: string; @@ -41,15 +41,18 @@ export class LearnerViewRatingBackendApiService { } async getUserRatingAsync(): Promise { - return this.httpClient.get( - this.ratingsUrl).toPromise(); + return this.httpClient + .get(this.ratingsUrl) + .toPromise(); } - async submitUserRatingAsync(ratingValue: number): - Promise { - return this.httpClient.put( - this.ratingsUrl, { - user_rating: ratingValue - }).toPromise(); + async submitUserRatingAsync( + ratingValue: number + ): Promise { + return this.httpClient + .put(this.ratingsUrl, { + user_rating: ratingValue, + }) + .toPromise(); } } diff --git a/core/templates/pages/exploration-player-page/services/learner-view-rating.service.spec.ts b/core/templates/pages/exploration-player-page/services/learner-view-rating.service.spec.ts index 9cbd8b70407b..a328d4f596a0 100644 --- a/core/templates/pages/exploration-player-page/services/learner-view-rating.service.spec.ts +++ b/core/templates/pages/exploration-player-page/services/learner-view-rating.service.spec.ts @@ -16,12 +16,12 @@ * @fileoverview Tests for Learner View Rating Service. */ -import { async, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TranslateService } from '@ngx-translate/core'; -import { MockTranslateService } from 'components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec'; -import { LearnerViewRatingService } from './learner-view-rating.service'; -import { LearnerViewRatingBackendApiService } from './learner-view-rating-backend-api.service'; +import {async, fakeAsync, TestBed, tick} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TranslateService} from '@ngx-translate/core'; +import {MockTranslateService} from 'components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec'; +import {LearnerViewRatingService} from './learner-view-rating.service'; +import {LearnerViewRatingBackendApiService} from './learner-view-rating-backend-api.service'; describe('Learner View Rating Service', () => { let learnerViewRatingService: LearnerViewRatingService; @@ -33,44 +33,53 @@ describe('Learner View Rating Service', () => { providers: [ { provide: TranslateService, - useClass: MockTranslateService - } - ] + useClass: MockTranslateService, + }, + ], }); })); beforeEach(() => { learnerViewRatingService = TestBed.inject(LearnerViewRatingService); learnerViewRatingBackendApiService = TestBed.inject( - LearnerViewRatingBackendApiService); + LearnerViewRatingBackendApiService + ); }); - it('should send request to backend for fetching user rating info' + - ' when initialized', fakeAsync(() => { - let userRatingSpy = spyOn( - learnerViewRatingBackendApiService, 'getUserRatingAsync') - .and.resolveTo({ - user_rating: 2 + it( + 'should send request to backend for fetching user rating info' + + ' when initialized', + fakeAsync(() => { + let userRatingSpy = spyOn( + learnerViewRatingBackendApiService, + 'getUserRatingAsync' + ).and.resolveTo({ + user_rating: 2, }); - let successCb = jasmine.createSpy('success'); + let successCb = jasmine.createSpy('success'); - learnerViewRatingService.init(successCb); - tick(); + learnerViewRatingService.init(successCb); + tick(); - expect(userRatingSpy).toHaveBeenCalled(); - })); + expect(userRatingSpy).toHaveBeenCalled(); + }) + ); - it('should send request to backend for submiting user rating info' + - ' when calling \'submitUserRating\'', fakeAsync(() => { - let userRatingSpy = spyOn( - learnerViewRatingBackendApiService, 'submitUserRatingAsync') - .and.resolveTo(); + it( + 'should send request to backend for submiting user rating info' + + " when calling 'submitUserRating'", + fakeAsync(() => { + let userRatingSpy = spyOn( + learnerViewRatingBackendApiService, + 'submitUserRatingAsync' + ).and.resolveTo(); - learnerViewRatingService.submitUserRating(2); - tick(); + learnerViewRatingService.submitUserRating(2); + tick(); - expect(userRatingSpy).toHaveBeenCalled(); - })); + expect(userRatingSpy).toHaveBeenCalled(); + }) + ); it('should test getters', () => { let userRating = 4; diff --git a/core/templates/pages/exploration-player-page/services/learner-view-rating.service.ts b/core/templates/pages/exploration-player-page/services/learner-view-rating.service.ts index e6dbdf83a14f..95736b0677b6 100644 --- a/core/templates/pages/exploration-player-page/services/learner-view-rating.service.ts +++ b/core/templates/pages/exploration-player-page/services/learner-view-rating.service.ts @@ -16,15 +16,15 @@ * @fileoverview Service for the rating functionality in the learner view. */ -import { EventEmitter } from '@angular/core'; +import {EventEmitter} from '@angular/core'; -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { ExplorationEngineService } from './exploration-engine.service'; -import { LearnerViewRatingBackendApiService } from './learner-view-rating-backend-api.service'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {ExplorationEngineService} from './exploration-engine.service'; +import {LearnerViewRatingBackendApiService} from './learner-view-rating-backend-api.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class LearnerViewRatingService { // This property is initialized using int method and we need to do @@ -35,20 +35,21 @@ export class LearnerViewRatingService { constructor( private explorationEngineService: ExplorationEngineService, - private learnerViewRatingBackendApiService: - LearnerViewRatingBackendApiService + private learnerViewRatingBackendApiService: LearnerViewRatingBackendApiService ) {} init(successCallback: (usrRating: number) => void): void { - this.learnerViewRatingBackendApiService.getUserRatingAsync() - .then((response) => { + this.learnerViewRatingBackendApiService + .getUserRatingAsync() + .then(response => { successCallback(response.user_rating); this.userRating = response.user_rating; }); } submitUserRating(ratingValue: number): void { - this.learnerViewRatingBackendApiService.submitUserRatingAsync(ratingValue) + this.learnerViewRatingBackendApiService + .submitUserRatingAsync(ratingValue) .then(() => { this.userRating = ratingValue; this._ratingUpdatedEventEmitter.emit(); @@ -64,5 +65,9 @@ export class LearnerViewRatingService { } } -angular.module('oppia').factory('LearnerViewRatingService', - downgradeInjectable(LearnerViewRatingService)); +angular + .module('oppia') + .factory( + 'LearnerViewRatingService', + downgradeInjectable(LearnerViewRatingService) + ); diff --git a/core/templates/pages/exploration-player-page/services/number-attempts.service.spec.ts b/core/templates/pages/exploration-player-page/services/number-attempts.service.spec.ts index a88c9700a31e..62808ef8fe4a 100644 --- a/core/templates/pages/exploration-player-page/services/number-attempts.service.spec.ts +++ b/core/templates/pages/exploration-player-page/services/number-attempts.service.spec.ts @@ -16,8 +16,7 @@ * @fileoverview Unit tests for the number attempts service. */ -import { NumberAttemptsService } from - 'pages/exploration-player-page/services/number-attempts.service'; +import {NumberAttemptsService} from 'pages/exploration-player-page/services/number-attempts.service'; describe('Number attempts service', () => { let numberAttemptsService: NumberAttemptsService; diff --git a/core/templates/pages/exploration-player-page/services/number-attempts.service.ts b/core/templates/pages/exploration-player-page/services/number-attempts.service.ts index 135829403935..dd46d9e0bfb3 100644 --- a/core/templates/pages/exploration-player-page/services/number-attempts.service.ts +++ b/core/templates/pages/exploration-player-page/services/number-attempts.service.ts @@ -17,11 +17,11 @@ * within a card. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class NumberAttemptsService { /** @@ -53,5 +53,6 @@ export class NumberAttemptsService { } } -angular.module('oppia').factory( - 'NumberAttemptsService', downgradeInjectable(NumberAttemptsService)); +angular + .module('oppia') + .factory('NumberAttemptsService', downgradeInjectable(NumberAttemptsService)); diff --git a/core/templates/pages/exploration-player-page/services/player-position.service.spec.ts b/core/templates/pages/exploration-player-page/services/player-position.service.spec.ts index 59788d0db7a2..259c4a637e69 100644 --- a/core/templates/pages/exploration-player-page/services/player-position.service.spec.ts +++ b/core/templates/pages/exploration-player-page/services/player-position.service.spec.ts @@ -16,20 +16,17 @@ * @fileoverview Unit tests for the player position service. */ -import { EventEmitter } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; +import {EventEmitter} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; -import { Subscription } from 'rxjs'; +import {Subscription} from 'rxjs'; -import { PlayerPositionService } from - 'pages/exploration-player-page/services/player-position.service'; -import { PlayerTranscriptService } from - 'pages/exploration-player-page/services/player-transcript.service'; -import { StateCard } from 'domain/state_card/state-card.model'; -import { AudioTranslationLanguageService } from - 'pages/exploration-player-page/services/audio-translation-language.service'; -import { Interaction } from 'domain/exploration/InteractionObjectFactory'; -import { RecordedVoiceovers } from 'domain/exploration/recorded-voiceovers.model'; +import {PlayerPositionService} from 'pages/exploration-player-page/services/player-position.service'; +import {PlayerTranscriptService} from 'pages/exploration-player-page/services/player-transcript.service'; +import {StateCard} from 'domain/state_card/state-card.model'; +import {AudioTranslationLanguageService} from 'pages/exploration-player-page/services/audio-translation-language.service'; +import {Interaction} from 'domain/exploration/InteractionObjectFactory'; +import {RecordedVoiceovers} from 'domain/exploration/recorded-voiceovers.model'; describe('Player position service', () => { let pts: PlayerTranscriptService; @@ -44,8 +41,9 @@ describe('Player position service', () => { atls = TestBed.inject(AudioTranslationLanguageService); onQuestionChangeSpy = jasmine.createSpy('onQuestionChangeSpy'); subscriptions = new Subscription(); - subscriptions.add(pps.onCurrentQuestionChange.subscribe( - onQuestionChangeSpy)); + subscriptions.add( + pps.onCurrentQuestionChange.subscribe(onQuestionChangeSpy) + ); }); afterEach(() => { @@ -75,15 +73,29 @@ describe('Player position service', () => { }); it('should get current state name', () => { - pts.addNewCard(StateCard.createNewCard( - 'First state', 'Content HTML', - '', - {} as Interaction, {} as RecordedVoiceovers, '', atls)); - - pts.addNewCard(StateCard.createNewCard( - 'Second state', 'Content HTML', - '', - {} as Interaction, {} as RecordedVoiceovers, '', atls)); + pts.addNewCard( + StateCard.createNewCard( + 'First state', + 'Content HTML', + '', + {} as Interaction, + {} as RecordedVoiceovers, + '', + atls + ) + ); + + pts.addNewCard( + StateCard.createNewCard( + 'Second state', + 'Content HTML', + '', + {} as Interaction, + {} as RecordedVoiceovers, + '', + atls + ) + ); let callBack = () => {}; pps.init(callBack); pps.setDisplayedCardIndex(0); @@ -92,25 +104,27 @@ describe('Player position service', () => { expect(pps.getCurrentStateName()).toBe('Second state'); }); - it('should throw error if callback ftn is not defined on changing index', - () => { - expect(() => { - pps.setDisplayedCardIndex(3); - }).toThrowError('The callback function has not been initialized'); - }); - - it('should not change displayed card index if it is the same as the ' + - 'previously displayed card index', () => { - let callBack = () => {}; - expect(pps.getDisplayedCardIndex()).toBeUndefined(); - pps.init(callBack); - pps.setDisplayedCardIndex(4); - pps.setDisplayedCardIndex(4); - expect(pps.getDisplayedCardIndex()).toBe(4); - pps.setDisplayedCardIndex(5); - expect(pps.getDisplayedCardIndex()).toBe(5); + it('should throw error if callback ftn is not defined on changing index', () => { + expect(() => { + pps.setDisplayedCardIndex(3); + }).toThrowError('The callback function has not been initialized'); }); + it( + 'should not change displayed card index if it is the same as the ' + + 'previously displayed card index', + () => { + let callBack = () => {}; + expect(pps.getDisplayedCardIndex()).toBeUndefined(); + pps.init(callBack); + pps.setDisplayedCardIndex(4); + pps.setDisplayedCardIndex(4); + expect(pps.getDisplayedCardIndex()).toBe(4); + pps.setDisplayedCardIndex(5); + expect(pps.getDisplayedCardIndex()).toBe(5); + } + ); + it('should get onNewCardOpened EventEmitter', () => { let mockNewCardOpenedEventEmitter = new EventEmitter(); expect(pps.onNewCardOpened).toEqual(mockNewCardOpenedEventEmitter); @@ -134,6 +148,7 @@ describe('Player position service', () => { it('should fetch EventEmitter for loading most recent checkpoint', () => { let mockLoadMostRecentCheckpointEvent = new EventEmitter(); expect(pps.onLoadedMostRecentCheckpoint).toEqual( - mockLoadMostRecentCheckpointEvent); + mockLoadMostRecentCheckpointEvent + ); }); }); diff --git a/core/templates/pages/exploration-player-page/services/player-position.service.ts b/core/templates/pages/exploration-player-page/services/player-position.service.ts index fa00ea85d451..b72caca39016 100644 --- a/core/templates/pages/exploration-player-page/services/player-position.service.ts +++ b/core/templates/pages/exploration-player-page/services/player-position.service.ts @@ -16,12 +16,11 @@ * @fileoverview Service for keeping track of the learner's position. */ -import { EventEmitter, Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {EventEmitter, Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; -import { PlayerTranscriptService } from - 'pages/exploration-player-page/services/player-transcript.service'; -import { StateCard } from 'domain/state_card/state-card.model'; +import {PlayerTranscriptService} from 'pages/exploration-player-page/services/player-transcript.service'; +import {StateCard} from 'domain/state_card/state-card.model'; export interface HelpCardEventResponse { helpCardHtml: string; @@ -29,7 +28,7 @@ export interface HelpCardEventResponse { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class PlayerPositionService { constructor(private playerTranscriptService: PlayerTranscriptService) {} @@ -60,9 +59,9 @@ export class PlayerPositionService { * @return {string} a string that shows the name of current state. */ getCurrentStateName(): string { - return ( - this.playerTranscriptService.getCard( - this.displayedCardIndex).getStateName()); + return this.playerTranscriptService + .getCard(this.displayedCardIndex) + .getStateName(); } /** @@ -142,6 +141,6 @@ export class PlayerPositionService { } } -angular.module('oppia').factory( - 'PlayerPositionService', - downgradeInjectable(PlayerPositionService)); +angular + .module('oppia') + .factory('PlayerPositionService', downgradeInjectable(PlayerPositionService)); diff --git a/core/templates/pages/exploration-player-page/services/player-transcript.service.spec.ts b/core/templates/pages/exploration-player-page/services/player-transcript.service.spec.ts index f07251a62285..d75f20c21b51 100644 --- a/core/templates/pages/exploration-player-page/services/player-transcript.service.spec.ts +++ b/core/templates/pages/exploration-player-page/services/player-transcript.service.spec.ts @@ -16,16 +16,14 @@ * @fileoverview Unit tests for the player transcript service. */ -import { TestBed } from '@angular/core/testing'; +import {TestBed} from '@angular/core/testing'; -import { PlayerTranscriptService } from - 'pages/exploration-player-page/services/player-transcript.service'; -import { StateCard } from 'domain/state_card/state-card.model'; -import { AudioTranslationLanguageService } from - 'pages/exploration-player-page/services/audio-translation-language.service'; -import { Interaction } from 'domain/exploration/InteractionObjectFactory'; -import { RecordedVoiceovers } from 'domain/exploration/recorded-voiceovers.model'; -import { LoggerService } from 'services/contextual/logger.service'; +import {PlayerTranscriptService} from 'pages/exploration-player-page/services/player-transcript.service'; +import {StateCard} from 'domain/state_card/state-card.model'; +import {AudioTranslationLanguageService} from 'pages/exploration-player-page/services/audio-translation-language.service'; +import {Interaction} from 'domain/exploration/InteractionObjectFactory'; +import {RecordedVoiceovers} from 'domain/exploration/recorded-voiceovers.model'; +import {LoggerService} from 'services/contextual/logger.service'; describe('Player transcript service', () => { let pts: PlayerTranscriptService; @@ -39,67 +37,129 @@ describe('Player transcript service', () => { }); it('should reset the transcript correctly', () => { - pts.addNewCard(StateCard.createNewCard( - 'First state', 'Content HTML', - '', - {} as Interaction, {} as RecordedVoiceovers, '', atls)); - pts.addNewCard(StateCard.createNewCard( - 'Second state', 'Content HTML', - '', - {} as Interaction, {} as RecordedVoiceovers, '', atls)); + pts.addNewCard( + StateCard.createNewCard( + 'First state', + 'Content HTML', + '', + {} as Interaction, + {} as RecordedVoiceovers, + '', + atls + ) + ); + pts.addNewCard( + StateCard.createNewCard( + 'Second state', + 'Content HTML', + '', + {} as Interaction, + {} as RecordedVoiceovers, + '', + atls + ) + ); expect(pts.getNumCards()).toBe(2); pts.init(); expect(pts.getNumCards()).toBe(0); - pts.addNewCard(StateCard.createNewCard( - 'Third state', 'Content HTML', - '', - {} as Interaction, {} as RecordedVoiceovers, '', atls)); + pts.addNewCard( + StateCard.createNewCard( + 'Third state', + 'Content HTML', + '', + {} as Interaction, + {} as RecordedVoiceovers, + '', + atls + ) + ); expect(pts.getCard(0).getStateName()).toBe('Third state'); }); - it( - 'should correctly check whether a state have been encountered before', - () => { - pts.addNewCard(StateCard.createNewCard( - 'First state', 'Content HTML', + it('should correctly check whether a state have been encountered before', () => { + pts.addNewCard( + StateCard.createNewCard( + 'First state', + 'Content HTML', '', - {} as Interaction, {} as RecordedVoiceovers, '', atls)); - pts.addNewCard(StateCard.createNewCard( - 'Second state', 'Content HTML', + {} as Interaction, + {} as RecordedVoiceovers, + '', + atls + ) + ); + pts.addNewCard( + StateCard.createNewCard( + 'Second state', + 'Content HTML', '', - {} as Interaction, {} as RecordedVoiceovers, '', atls)); - pts.addNewCard(StateCard.createNewCard( - 'First state', 'Content HTML', + {} as Interaction, + {} as RecordedVoiceovers, + '', + atls + ) + ); + pts.addNewCard( + StateCard.createNewCard( + 'First state', + 'Content HTML', '', - {} as Interaction, {} as RecordedVoiceovers, '', atls)); - expect(pts.hasEncounteredStateBefore('First state')).toEqual(true); - expect(pts.hasEncounteredStateBefore('Third state')).toEqual(false); - }); + {} as Interaction, + {} as RecordedVoiceovers, + '', + atls + ) + ); + expect(pts.hasEncounteredStateBefore('First state')).toEqual(true); + expect(pts.hasEncounteredStateBefore('Third state')).toEqual(false); + }); it('should add a new card correctly', () => { - pts.addNewCard(StateCard.createNewCard( - 'First state', 'Content HTML', - '', - {} as Interaction, {} as RecordedVoiceovers, '', atls)); + pts.addNewCard( + StateCard.createNewCard( + 'First state', + 'Content HTML', + '', + {} as Interaction, + {} as RecordedVoiceovers, + '', + atls + ) + ); let firstCard = pts.getCard(0); expect(firstCard.getStateName()).toEqual('First state'); expect(firstCard.getContentHtml()).toEqual('Content HTML'); expect(firstCard.getInteractionHtml()).toEqual( - ''); + '' + ); }); it('should add a previous card correctly', () => { - pts.addNewCard(StateCard.createNewCard( - 'First state', 'Content HTML', - '', - {} as Interaction, {} as RecordedVoiceovers, '', atls)); - pts.addNewCard(StateCard.createNewCard( - 'Second state', 'Content HTML', - '', - {} as Interaction, {} as RecordedVoiceovers, '', atls)); + pts.addNewCard( + StateCard.createNewCard( + 'First state', + 'Content HTML', + '', + {} as Interaction, + {} as RecordedVoiceovers, + '', + atls + ) + ); + pts.addNewCard( + StateCard.createNewCard( + 'Second state', + 'Content HTML', + '', + {} as Interaction, + {} as RecordedVoiceovers, + '', + atls + ) + ); pts.addPreviousCard(); expect(pts.getNumCards()).toEqual(3); @@ -108,38 +168,70 @@ describe('Player transcript service', () => { expect(pts.getCard(2).getStateName()).toEqual('First state'); }); - it('should throw error when there is only one card and' + - 'adding previous card fails', () => { - pts.addNewCard(StateCard.createNewCard( - 'First state', 'Content HTML', - '', - {} as Interaction, {} as RecordedVoiceovers, '', atls)); - - expect(() => pts.addPreviousCard()).toThrowError( - 'Exploration player is on the first card and hence no previous ' + - 'card exists.'); - }); + it( + 'should throw error when there is only one card and' + + 'adding previous card fails', + () => { + pts.addNewCard( + StateCard.createNewCard( + 'First state', + 'Content HTML', + '', + {} as Interaction, + {} as RecordedVoiceovers, + '', + atls + ) + ); + + expect(() => pts.addPreviousCard()).toThrowError( + 'Exploration player is on the first card and hence no previous ' + + 'card exists.' + ); + } + ); it('should set lastAnswer correctly', () => { - pts.addNewCard(StateCard.createNewCard( - 'First state', 'Content HTML', - '', - {} as Interaction, {} as RecordedVoiceovers, '', atls)); + pts.addNewCard( + StateCard.createNewCard( + 'First state', + 'Content HTML', + '', + {} as Interaction, + {} as RecordedVoiceovers, + '', + atls + ) + ); let lastAnswer = pts.getLastAnswerOnDisplayedCard(0); expect(lastAnswer).toEqual(null); pts.addNewInput('first answer', false); - pts.addNewCard(StateCard.createNewCard( - 'Second state', 'Content HTML', - '', - {} as Interaction, {} as RecordedVoiceovers, '', atls)); + pts.addNewCard( + StateCard.createNewCard( + 'Second state', + 'Content HTML', + '', + {} as Interaction, + {} as RecordedVoiceovers, + '', + atls + ) + ); lastAnswer = pts.getLastAnswerOnDisplayedCard(0); expect(lastAnswer).toEqual('first answer'); - pts.addNewCard(StateCard.createNewCard( - 'Third state', 'Content HTML', - '', - {} as Interaction, {} as RecordedVoiceovers, '', atls)); + pts.addNewCard( + StateCard.createNewCard( + 'Third state', + 'Content HTML', + '', + {} as Interaction, + {} as RecordedVoiceovers, + '', + atls + ) + ); // Variable lastAnswer should be null as no answers were provided in the // second state. lastAnswer = pts.getLastAnswerOnDisplayedCard(1); @@ -147,16 +239,24 @@ describe('Player transcript service', () => { }); it('should record answer/feedback pairs in the correct order', () => { - pts.addNewCard(StateCard.createNewCard( - 'First state', 'Content HTML', - '', - {} as Interaction, {} as RecordedVoiceovers, '', atls)); + pts.addNewCard( + StateCard.createNewCard( + 'First state', + 'Content HTML', + '', + {} as Interaction, + {} as RecordedVoiceovers, + '', + atls + ) + ); pts.addNewInput('first answer', false); expect(() => { pts.addNewInput('invalid answer', false); }).toThrowError( 'Trying to add an input before the response for the previous ' + - 'input has been received.'); + 'input has been received.' + ); pts.addNewResponse('feedback'); pts.addNewInput('second answer', false); @@ -165,31 +265,49 @@ describe('Player transcript service', () => { pts.addNewInput('third answer', true); let firstCard = pts.getCard(0); - expect(firstCard.getInputResponsePairs()).toEqual([{ - learnerInput: 'first answer', - oppiaResponse: 'feedback', - isHint: false - }, { - learnerInput: 'second answer', - oppiaResponse: 'feedback\nfeedback_2', - isHint: false - }, { - learnerInput: 'third answer', - oppiaResponse: null, - isHint: true - }]); + expect(firstCard.getInputResponsePairs()).toEqual([ + { + learnerInput: 'first answer', + oppiaResponse: 'feedback', + isHint: false, + }, + { + learnerInput: 'second answer', + oppiaResponse: 'feedback\nfeedback_2', + isHint: false, + }, + { + learnerInput: 'third answer', + oppiaResponse: null, + isHint: true, + }, + ]); expect(pts.getNumSubmitsForLastCard()).toBe(2); }); it('should retrieve the last card of the transcript correctly', () => { - pts.addNewCard(StateCard.createNewCard( - 'First state', 'Content HTML', - '', - {} as Interaction, {} as RecordedVoiceovers, '', atls)); - pts.addNewCard(StateCard.createNewCard( - 'Second state', 'Content HTML', - '', - {} as Interaction, {} as RecordedVoiceovers, '', atls)); + pts.addNewCard( + StateCard.createNewCard( + 'First state', + 'Content HTML', + '', + {} as Interaction, + {} as RecordedVoiceovers, + '', + atls + ) + ); + pts.addNewCard( + StateCard.createNewCard( + 'Second state', + 'Content HTML', + '', + {} as Interaction, + {} as RecordedVoiceovers, + '', + atls + ) + ); expect(pts.getNumCards()).toBe(2); expect(pts.getLastCard().getStateName()).toBe('Second state'); expect(pts.isLastCard(0)).toBe(false); @@ -207,42 +325,75 @@ describe('Player transcript service', () => { }); it('should update interaction html of the latest card', () => { - pts.addNewCard(StateCard.createNewCard( - 'First state', 'Content HTML', - '', - {} as Interaction, {} as RecordedVoiceovers, '', atls)); + pts.addNewCard( + StateCard.createNewCard( + 'First state', + 'Content HTML', + '', + {} as Interaction, + {} as RecordedVoiceovers, + '', + atls + ) + ); let secondCard = StateCard.createNewCard( - 'Second state', 'Content HTML', + 'Second state', + 'Content HTML', '', - {} as Interaction, {} as RecordedVoiceovers, '', atls); + {} as Interaction, + {} as RecordedVoiceovers, + '', + atls + ); pts.updateLatestInteractionHtml(secondCard.getInteractionHtml()); expect(pts.getLastCard().getInteractionHtml()).toEqual( - secondCard.getInteractionHtml()); + secondCard.getInteractionHtml() + ); }); it('should restore the old transcript', () => { let card1 = StateCard.createNewCard( - 'First State', 'Content HTML', + 'First State', + 'Content HTML', '', - {} as Interaction, {} as RecordedVoiceovers, '', atls); + {} as Interaction, + {} as RecordedVoiceovers, + '', + atls + ); let card2 = StateCard.createNewCard( - 'Second State', 'Content HTML', + 'Second State', + 'Content HTML', '', - {} as Interaction, {} as RecordedVoiceovers, '', atls); + {} as Interaction, + {} as RecordedVoiceovers, + '', + atls + ); let card3 = StateCard.createNewCard( - 'Third State', 'Content HTML', + 'Third State', + 'Content HTML', '', - {} as Interaction, {} as RecordedVoiceovers, '', atls); + {} as Interaction, + {} as RecordedVoiceovers, + '', + atls + ); let card4 = StateCard.createNewCard( - 'Fourth State', 'Content HTML', + 'Fourth State', + 'Content HTML', '', - {} as Interaction, {} as RecordedVoiceovers, '', atls); + {} as Interaction, + {} as RecordedVoiceovers, + '', + atls + ); let oldTranscript = [card3, card4]; @@ -260,24 +411,44 @@ describe('Player transcript service', () => { it('should restore the old transcript immutably', () => { let card1 = StateCard.createNewCard( - 'First State', 'Content HTML', + 'First State', + 'Content HTML', '', - {} as Interaction, {} as RecordedVoiceovers, '', atls); + {} as Interaction, + {} as RecordedVoiceovers, + '', + atls + ); let card2 = StateCard.createNewCard( - 'Second State', 'Content HTML', + 'Second State', + 'Content HTML', '', - {} as Interaction, {} as RecordedVoiceovers, '', atls); + {} as Interaction, + {} as RecordedVoiceovers, + '', + atls + ); let card3 = StateCard.createNewCard( - 'Third State', 'Content HTML', + 'Third State', + 'Content HTML', '', - {} as Interaction, {} as RecordedVoiceovers, '', atls); + {} as Interaction, + {} as RecordedVoiceovers, + '', + atls + ); let card4 = StateCard.createNewCard( - 'Fourth State', 'Content HTML', + 'Fourth State', + 'Content HTML', '', - {} as Interaction, {} as RecordedVoiceovers, '', atls); + {} as Interaction, + {} as RecordedVoiceovers, + '', + atls + ); let oldTranscript = [card3, card4]; @@ -296,33 +467,65 @@ describe('Player transcript service', () => { it('should show error on the console when invalid index is provided', () => { spyOn(ls, 'error'); - pts.addNewCard(StateCard.createNewCard( - 'First State', 'Content HTML', - '', - {} as Interaction, {} as RecordedVoiceovers, '', atls)); + pts.addNewCard( + StateCard.createNewCard( + 'First State', + 'Content HTML', + '', + {} as Interaction, + {} as RecordedVoiceovers, + '', + atls + ) + ); pts.getCard(1); expect(ls.error).toHaveBeenCalledWith( - 'Requested card with index 1, but transcript only has length 1 cards.'); + 'Requested card with index 1, but transcript only has length 1 cards.' + ); }); it('should find index of latest state', () => { let card1 = StateCard.createNewCard( - 'first', '', '', - {} as Interaction, {} as RecordedVoiceovers, '', atls); + 'first', + '', + '', + {} as Interaction, + {} as RecordedVoiceovers, + '', + atls + ); let card2 = StateCard.createNewCard( - 'second', '', '', - {} as Interaction, {} as RecordedVoiceovers, '', atls); + 'second', + '', + '', + {} as Interaction, + {} as RecordedVoiceovers, + '', + atls + ); let card3 = StateCard.createNewCard( - 'third', '', '', - {} as Interaction, {} as RecordedVoiceovers, '', atls); + 'third', + '', + '', + {} as Interaction, + {} as RecordedVoiceovers, + '', + atls + ); let card4 = StateCard.createNewCard( - 'first', '', '', - {} as Interaction, {} as RecordedVoiceovers, '', atls); + 'first', + '', + '', + {} as Interaction, + {} as RecordedVoiceovers, + '', + atls + ); pts.addNewCard(card1); pts.addNewCard(card2); diff --git a/core/templates/pages/exploration-player-page/services/player-transcript.service.ts b/core/templates/pages/exploration-player-page/services/player-transcript.service.ts index 8837265d012d..ae68d91cf301 100644 --- a/core/templates/pages/exploration-player-page/services/player-transcript.service.ts +++ b/core/templates/pages/exploration-player-page/services/player-transcript.service.ts @@ -21,16 +21,16 @@ // not maintain the currently-active card -- it's more like a log of what the // learner has 'discovered' so far. -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; import cloneDeep from 'lodash/cloneDeep'; -import { LoggerService } from 'services/contextual/logger.service'; -import { StateCard } from 'domain/state_card/state-card.model'; +import {LoggerService} from 'services/contextual/logger.service'; +import {StateCard} from 'domain/state_card/state-card.model'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class PlayerTranscriptService { constructor(private log: LoggerService) {} @@ -75,12 +75,14 @@ export class PlayerTranscriptService { if (this.transcript.length === 1) { throw new Error( 'Exploration player is on the first card and hence no previous ' + - 'card exists.'); + 'card exists.' + ); } // TODO(aks681): Once worked examples are introduced, modify the below // line to take into account the number of worked examples displayed. - let copyOfPreviousCard = - cloneDeep(this.transcript[this.transcript.length - 2]); + let copyOfPreviousCard = cloneDeep( + this.transcript[this.transcript.length - 2] + ); copyOfPreviousCard.markAsNotCompleted(); this.transcript.push(copyOfPreviousCard); } @@ -100,7 +102,7 @@ export class PlayerTranscriptService { this.transcript[this.transcript.length - 1].addInputResponsePair({ learnerInput: input, oppiaResponse: null, - isHint: isHint + isHint: isHint, }); } @@ -121,25 +123,29 @@ export class PlayerTranscriptService { getCard(index: number): StateCard { if (index !== null && (index < 0 || index >= this.transcript.length)) { this.log.error( - 'Requested card with index ' + index + + 'Requested card with index ' + + index + ', but transcript only has length ' + - this.transcript.length + ' cards.'); + this.transcript.length + + ' cards.' + ); } return this.transcript[index]; } getLastAnswerOnDisplayedCard( - displayedCardIndex: number - ): { answerDetails: string } | string | null { + displayedCardIndex: number + ): {answerDetails: string} | string | null { if ( this.isLastCard(displayedCardIndex) || - this.transcript[displayedCardIndex].getStateName() === null || - this.transcript[displayedCardIndex].getInputResponsePairs().length === - 0) { + this.transcript[displayedCardIndex].getStateName() === null || + this.transcript[displayedCardIndex].getInputResponsePairs().length === 0 + ) { return null; } else { - return this.transcript[displayedCardIndex]. - getInputResponsePairs().slice(-1)[0].learnerInput; + return this.transcript[displayedCardIndex] + .getInputResponsePairs() + .slice(-1)[0].learnerInput; } } @@ -173,6 +179,9 @@ export class PlayerTranscriptService { } } -angular.module('oppia').factory( - 'PlayerTranscriptService', - downgradeInjectable(PlayerTranscriptService)); +angular + .module('oppia') + .factory( + 'PlayerTranscriptService', + downgradeInjectable(PlayerTranscriptService) + ); diff --git a/core/templates/pages/exploration-player-page/services/prediction-algorithm-registry.service.spec.ts b/core/templates/pages/exploration-player-page/services/prediction-algorithm-registry.service.spec.ts index 01c122b54537..0167be30decc 100644 --- a/core/templates/pages/exploration-player-page/services/prediction-algorithm-registry.service.spec.ts +++ b/core/templates/pages/exploration-player-page/services/prediction-algorithm-registry.service.spec.ts @@ -16,10 +16,10 @@ * @fileoverview Unit tests for the prediction algorithm registry service. */ -import { TestBed } from '@angular/core/testing'; +import {TestBed} from '@angular/core/testing'; -import { PredictionAlgorithmRegistryService } from 'pages/exploration-player-page/services/prediction-algorithm-registry.service'; -import { TextInputPredictionService } from 'interactions/TextInput/text-input-prediction.service'; +import {PredictionAlgorithmRegistryService} from 'pages/exploration-player-page/services/prediction-algorithm-registry.service'; +import {TextInputPredictionService} from 'interactions/TextInput/text-input-prediction.service'; describe('Prediction Algorithm Registry Service', () => { let predictionAlgorithmRegistryService: PredictionAlgorithmRegistryService; @@ -27,77 +27,103 @@ describe('Prediction Algorithm Registry Service', () => { beforeEach(() => { predictionAlgorithmRegistryService = TestBed.inject( - PredictionAlgorithmRegistryService); + PredictionAlgorithmRegistryService + ); textInputPredictionService = TestBed.inject(TextInputPredictionService); }); it('should return service for TextClassifier at schema version 1.', () => { expect( - predictionAlgorithmRegistryService - .getPredictionService('TextClassifier', 1) + predictionAlgorithmRegistryService.getPredictionService( + 'TextClassifier', + 1 + ) ).toEqual(textInputPredictionService); }); it('should return null for TextClassifier at schema version 999.', () => { expect( - predictionAlgorithmRegistryService - .getPredictionService('TextClassifier', 999) + predictionAlgorithmRegistryService.getPredictionService( + 'TextClassifier', + 999 + ) ).toBeNull(); }); it('should return null for NullClassifier which does not exist.', () => { expect( - predictionAlgorithmRegistryService - .getPredictionService('NullClassifier', 1) + predictionAlgorithmRegistryService.getPredictionService( + 'NullClassifier', + 1 + ) ).toBeNull(); }); describe('when trying to mock prediction services in tests', () => { it('should overwrite corresponding service if one exists.', () => { expect( - predictionAlgorithmRegistryService - .getPredictionService('TextClassifier', 1) + predictionAlgorithmRegistryService.getPredictionService( + 'TextClassifier', + 1 + ) ).toEqual(textInputPredictionService); predictionAlgorithmRegistryService.testOnlySetPredictionService( - 'TextClassifier', 1, textInputPredictionService); + 'TextClassifier', + 1, + textInputPredictionService + ); expect( - predictionAlgorithmRegistryService - .getPredictionService('TextClassifier', 1) + predictionAlgorithmRegistryService.getPredictionService( + 'TextClassifier', + 1 + ) ).toEqual(textInputPredictionService); }); it('should create new algorithm id entry when it does not exist.', () => { expect( - predictionAlgorithmRegistryService - .getPredictionService('NullClassifier', 1) + predictionAlgorithmRegistryService.getPredictionService( + 'NullClassifier', + 1 + ) ).toBeNull(); predictionAlgorithmRegistryService.testOnlySetPredictionService( - 'NullClassifier', 1, textInputPredictionService); + 'NullClassifier', + 1, + textInputPredictionService + ); expect( - predictionAlgorithmRegistryService - .getPredictionService('NullClassifier', 1) + predictionAlgorithmRegistryService.getPredictionService( + 'NullClassifier', + 1 + ) ).toEqual(textInputPredictionService); }); - it( - 'should create new data schema version entry when it does not exist.', - () => { - expect( - predictionAlgorithmRegistryService - .getPredictionService('TextClassifier', 999) - ).toBeNull(); - - predictionAlgorithmRegistryService.testOnlySetPredictionService( - 'TextClassifier', 999, textInputPredictionService); - - expect( - predictionAlgorithmRegistryService - .getPredictionService('TextClassifier', 999) - ).toEqual(textInputPredictionService); - }); + it('should create new data schema version entry when it does not exist.', () => { + expect( + predictionAlgorithmRegistryService.getPredictionService( + 'TextClassifier', + 999 + ) + ).toBeNull(); + + predictionAlgorithmRegistryService.testOnlySetPredictionService( + 'TextClassifier', + 999, + textInputPredictionService + ); + + expect( + predictionAlgorithmRegistryService.getPredictionService( + 'TextClassifier', + 999 + ) + ).toEqual(textInputPredictionService); + }); }); }); diff --git a/core/templates/pages/exploration-player-page/services/prediction-algorithm-registry.service.ts b/core/templates/pages/exploration-player-page/services/prediction-algorithm-registry.service.ts index b4875806df0b..0b982867485a 100644 --- a/core/templates/pages/exploration-player-page/services/prediction-algorithm-registry.service.ts +++ b/core/templates/pages/exploration-player-page/services/prediction-algorithm-registry.service.ts @@ -16,35 +16,37 @@ * @fileoverview Service for mapping algorithmId to PredictionAlgorithmService. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { TextInputPredictionService } from 'interactions/TextInput/text-input-prediction.service'; +import {TextInputPredictionService} from 'interactions/TextInput/text-input-prediction.service'; -type AlgorithmIdPredictionServiceMap = ( - Map>); +type AlgorithmIdPredictionServiceMap = Map< + string, + Map +>; @Injectable({providedIn: 'root'}) export class PredictionAlgorithmRegistryService { private algorithmIdPredictionServiceMapping: AlgorithmIdPredictionServiceMap; - constructor( - private textInputPredictionService: TextInputPredictionService) { - this.algorithmIdPredictionServiceMapping = new Map(Object.entries({ - TextClassifier: new Map([ - [1, this.textInputPredictionService] - ]) - })); + constructor(private textInputPredictionService: TextInputPredictionService) { + this.algorithmIdPredictionServiceMapping = new Map( + Object.entries({ + TextClassifier: new Map([[1, this.textInputPredictionService]]), + }) + ); } getPredictionService( - algorithmId: string, dataSchemaVersion: number): - TextInputPredictionService | null { - const predictionServicesByAlgorithmId = ( - this.algorithmIdPredictionServiceMapping.get(algorithmId)); + algorithmId: string, + dataSchemaVersion: number + ): TextInputPredictionService | null { + const predictionServicesByAlgorithmId = + this.algorithmIdPredictionServiceMapping.get(algorithmId); if (predictionServicesByAlgorithmId) { - const predictionServicesByDataSchemaVersion = ( - predictionServicesByAlgorithmId).get(dataSchemaVersion); + const predictionServicesByDataSchemaVersion = + predictionServicesByAlgorithmId.get(dataSchemaVersion); if (predictionServicesByDataSchemaVersion) { return predictionServicesByDataSchemaVersion; } @@ -53,17 +55,22 @@ export class PredictionAlgorithmRegistryService { } testOnlySetPredictionService( - algorithmId: string, dataSchemaVersion: number, - service: TextInputPredictionService): void { + algorithmId: string, + dataSchemaVersion: number, + service: TextInputPredictionService + ): void { if (!this.algorithmIdPredictionServiceMapping.get(algorithmId)) { this.algorithmIdPredictionServiceMapping.set(algorithmId, new Map()); } - let _algorithmId = ( - this.algorithmIdPredictionServiceMapping.get(algorithmId)); + let _algorithmId = + this.algorithmIdPredictionServiceMapping.get(algorithmId); _algorithmId?.set(dataSchemaVersion, service); } } -angular.module('oppia').factory( - 'PredictionAlgorithmRegistryService', - downgradeInjectable(PredictionAlgorithmRegistryService)); +angular + .module('oppia') + .factory( + 'PredictionAlgorithmRegistryService', + downgradeInjectable(PredictionAlgorithmRegistryService) + ); diff --git a/core/templates/pages/exploration-player-page/services/question-player-engine.service.spec.ts b/core/templates/pages/exploration-player-page/services/question-player-engine.service.spec.ts index 7ad9d087722a..8e1d9ed2dcd3 100644 --- a/core/templates/pages/exploration-player-page/services/question-player-engine.service.spec.ts +++ b/core/templates/pages/exploration-player-page/services/question-player-engine.service.spec.ts @@ -16,21 +16,27 @@ * @fileoverview Unit tests for the question player engine service. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TestBed} from '@angular/core/testing'; -import { AnswerClassificationResult } from 'domain/classifier/answer-classification-result.model'; -import { OutcomeObjectFactory } from 'domain/exploration/OutcomeObjectFactory'; -import { Question, QuestionBackendDict, QuestionObjectFactory } from 'domain/question/QuestionObjectFactory'; -import { StateCard } from 'domain/state_card/state-card.model'; -import { ExpressionInterpolationService } from 'expressions/expression-interpolation.service'; -import { TextInputRulesService } from 'interactions/TextInput/directives/text-input-rules.service'; -import { AlertsService } from 'services/alerts.service'; -import { ContextService } from 'services/context.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { AnswerClassificationService, InteractionRulesService } from './answer-classification.service'; -import { QuestionPlayerEngineService } from './question-player-engine.service'; -import { AudioTranslationLanguageService } from - 'pages/exploration-player-page/services/audio-translation-language.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TestBed} from '@angular/core/testing'; +import {AnswerClassificationResult} from 'domain/classifier/answer-classification-result.model'; +import {OutcomeObjectFactory} from 'domain/exploration/OutcomeObjectFactory'; +import { + Question, + QuestionBackendDict, + QuestionObjectFactory, +} from 'domain/question/QuestionObjectFactory'; +import {StateCard} from 'domain/state_card/state-card.model'; +import {ExpressionInterpolationService} from 'expressions/expression-interpolation.service'; +import {TextInputRulesService} from 'interactions/TextInput/directives/text-input-rules.service'; +import {AlertsService} from 'services/alerts.service'; +import {ContextService} from 'services/context.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import { + AnswerClassificationService, + InteractionRulesService, +} from './answer-classification.service'; +import {QuestionPlayerEngineService} from './question-player-engine.service'; +import {AudioTranslationLanguageService} from 'pages/exploration-player-page/services/audio-translation-language.service'; describe('Question player engine service ', () => { let audioTranslationLanguageService: AudioTranslationLanguageService; @@ -57,49 +63,55 @@ describe('Question player engine service ', () => { solicit_answer_details: false, content: { content_id: '1', - html: 'Question 1' + html: 'Question 1', }, interaction: { - answer_groups: [{ - outcome: { - dest: 'State 1', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_1', - html: '

Try Again.

' + answer_groups: [ + { + outcome: { + dest: 'State 1', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_1', + html: '

Try Again.

', + }, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + labelled_as_correct: true, }, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - labelled_as_correct: true, + rule_specs: [ + { + rule_type: 'Equals', + inputs: {x: 0}, + }, + ], + training_data: null, + tagged_skill_misconception_id: null, }, - rule_specs: [{ - rule_type: 'Equals', - inputs: {x: 0} - }], - training_data: null, - tagged_skill_misconception_id: null, - }, - { - outcome: { - dest: 'State 2', - dest_if_really_stuck: null, - feedback: { - content_id: 'feedback_2', - html: '

Try Again.

' + { + outcome: { + dest: 'State 2', + dest_if_really_stuck: null, + feedback: { + content_id: 'feedback_2', + html: '

Try Again.

', + }, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + labelled_as_correct: true, }, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, - labelled_as_correct: true, + rule_specs: [ + { + rule_type: 'Equals', + inputs: {x: 0}, + }, + ], + training_data: null, + tagged_skill_misconception_id: 'misconceptionId', }, - rule_specs: [{ - rule_type: 'Equals', - inputs: {x: 0} - }], - training_data: null, - tagged_skill_misconception_id: 'misconceptionId', - }], + ], default_outcome: { dest: null, dest_if_really_stuck: null, @@ -109,41 +121,41 @@ describe('Question player engine service ', () => { param_changes: [], feedback: { content_id: 'feedback_id', - html: '

Dummy Feedback

' - } + html: '

Dummy Feedback

', + }, }, id: 'TextInput', customization_args: { rows: { - value: 1 + value: 1, }, placeholder: { value: { unicode_str: '', - content_id: 'ca_placeholder_0' - } + content_id: 'ca_placeholder_0', + }, }, catchMisspellings: { - value: false - } + value: false, + }, }, confirmed_unclassified_answers: [], hints: [ { hint_content: { content_id: 'hint_1', - html: '

This is a hint.

' - } - } + html: '

This is a hint.

', + }, + }, ], solution: { correct_answer: 'Solution', explanation: { content_id: 'solution', - html: '

This is a solution.

' + html: '

This is a solution.

', }, - answer_is_exclusive: false - } + answer_is_exclusive: false, + }, }, linked_skill_id: null, card_is_checkpoint: true, @@ -153,9 +165,9 @@ describe('Question player engine service ', () => { ca_placeholder_0: {}, feedback_id: {}, solution: {}, - hint_1: {} - } - } + hint_1: {}, + }, + }, }, question_state_data_schema_version: 45, next_content_id_index: 5, @@ -165,248 +177,252 @@ describe('Question player engine service ', () => { inapplicable_skill_misconception_ids: [], }; - multipleQuestionsBackendDict = [{ - id: 'questionId1', - question_state_data: { - classifier_model_id: null, - param_changes: [], - solicit_answer_details: false, - content: { - content_id: '1', - html: 'Question 1' - }, - interaction: { - answer_groups: [], - default_outcome: { - dest: null, - dest_if_really_stuck: null, - labelled_as_correct: true, - missing_prerequisite_skill_id: null, - refresher_exploration_id: null, - param_changes: [], - feedback: { - content_id: 'feedback_id', - html: '

Dummy Feedback

' - } + multipleQuestionsBackendDict = [ + { + id: 'questionId1', + question_state_data: { + classifier_model_id: null, + param_changes: [], + solicit_answer_details: false, + content: { + content_id: '1', + html: 'Question 1', }, - id: 'TextInput', - customization_args: { - rows: { - value: 1 + interaction: { + answer_groups: [], + default_outcome: { + dest: null, + dest_if_really_stuck: null, + labelled_as_correct: true, + missing_prerequisite_skill_id: null, + refresher_exploration_id: null, + param_changes: [], + feedback: { + content_id: 'feedback_id', + html: '

Dummy Feedback

', + }, }, - placeholder: { - value: { - unicode_str: '', - content_id: 'ca_placeholder_0' - } + id: 'TextInput', + customization_args: { + rows: { + value: 1, + }, + placeholder: { + value: { + unicode_str: '', + content_id: 'ca_placeholder_0', + }, + }, + catchMisspellings: { + value: false, + }, + }, + confirmed_unclassified_answers: [], + hints: [ + { + hint_content: { + content_id: 'hint_1', + html: '

This is a hint.

', + }, + }, + ], + solution: { + correct_answer: 'Solution', + explanation: { + content_id: 'solution', + html: '

This is a solution.

', + }, + answer_is_exclusive: false, }, - catchMisspellings: { - value: false - } }, - confirmed_unclassified_answers: [], - hints: [ - { - hint_content: { - content_id: 'hint_1', - html: '

This is a hint.

' - } - } - ], - solution: { - correct_answer: 'Solution', - explanation: { - content_id: 'solution', - html: '

This is a solution.

' + linked_skill_id: null, + card_is_checkpoint: true, + recorded_voiceovers: { + voiceovers_mapping: { + 1: {}, + ca_placeholder_0: {}, + feedback_id: {}, + solution: {}, + hint_1: {}, }, - answer_is_exclusive: false - } + }, }, - linked_skill_id: null, - card_is_checkpoint: true, - recorded_voiceovers: { - voiceovers_mapping: { - 1: {}, - ca_placeholder_0: {}, - feedback_id: {}, - solution: {}, - hint_1: {} - } - } + question_state_data_schema_version: 45, + language_code: 'en', + next_content_id_index: 6, + version: 1, + linked_skill_ids: [], + inapplicable_skill_misconception_ids: [], }, - question_state_data_schema_version: 45, - language_code: 'en', - next_content_id_index: 6, - version: 1, - linked_skill_ids: [], - inapplicable_skill_misconception_ids: [], - }, - { - id: 'questionId2', - question_state_data: { - classifier_model_id: null, - param_changes: [], - solicit_answer_details: false, - content: { - content_id: '2', - html: 'Question 2' - }, - interaction: { - answer_groups: [], - default_outcome: { - dest: null, - dest_if_really_stuck: null, - labelled_as_correct: true, - missing_prerequisite_skill_id: null, - refresher_exploration_id: null, - param_changes: [], - feedback: { - content_id: 'feedback_id', - html: '

Dummy Feedback

' - } + { + id: 'questionId2', + question_state_data: { + classifier_model_id: null, + param_changes: [], + solicit_answer_details: false, + content: { + content_id: '2', + html: 'Question 2', }, - id: 'TextInput', - customization_args: { - rows: { - value: 1 + interaction: { + answer_groups: [], + default_outcome: { + dest: null, + dest_if_really_stuck: null, + labelled_as_correct: true, + missing_prerequisite_skill_id: null, + refresher_exploration_id: null, + param_changes: [], + feedback: { + content_id: 'feedback_id', + html: '

Dummy Feedback

', + }, }, - placeholder: { - value: { - unicode_str: '', - content_id: 'ca_placeholder_0' - } + id: 'TextInput', + customization_args: { + rows: { + value: 1, + }, + placeholder: { + value: { + unicode_str: '', + content_id: 'ca_placeholder_0', + }, + }, + catchMisspellings: { + value: false, + }, + }, + confirmed_unclassified_answers: [], + hints: [ + { + hint_content: { + content_id: 'hint_1', + html: '

This is a hint.

', + }, + }, + ], + solution: { + correct_answer: 'Solution', + explanation: { + content_id: 'solution', + html: '

This is a solution.

', + }, + answer_is_exclusive: false, }, - catchMisspellings: { - value: false - } }, - confirmed_unclassified_answers: [], - hints: [ - { - hint_content: { - content_id: 'hint_1', - html: '

This is a hint.

' - } - } - ], - solution: { - correct_answer: 'Solution', - explanation: { - content_id: 'solution', - html: '

This is a solution.

' + linked_skill_id: null, + card_is_checkpoint: true, + recorded_voiceovers: { + voiceovers_mapping: { + 1: {}, + ca_placeholder_0: {}, + feedback_id: {}, + solution: {}, + hint_1: {}, }, - answer_is_exclusive: false - } + }, }, - linked_skill_id: null, - card_is_checkpoint: true, - recorded_voiceovers: { - voiceovers_mapping: { - 1: {}, - ca_placeholder_0: {}, - feedback_id: {}, - solution: {}, - hint_1: {} - } - } + question_state_data_schema_version: 45, + language_code: 'br', + next_content_id_index: 2, + version: 1, + linked_skill_ids: [], + inapplicable_skill_misconception_ids: [], }, - question_state_data_schema_version: 45, - language_code: 'br', - next_content_id_index: 2, - version: 1, - linked_skill_ids: [], - inapplicable_skill_misconception_ids: [], - }, - { - id: 'questionId3', - question_state_data: { - classifier_model_id: null, - param_changes: [], - solicit_answer_details: false, - content: { - content_id: '3', - html: 'Question 3' - }, - interaction: { - answer_groups: [], - default_outcome: { - dest: null, - dest_if_really_stuck: null, - labelled_as_correct: true, - missing_prerequisite_skill_id: null, - refresher_exploration_id: null, - param_changes: [], - feedback: { - content_id: 'feedback_id', - html: '

Dummy Feedback

' - } + { + id: 'questionId3', + question_state_data: { + classifier_model_id: null, + param_changes: [], + solicit_answer_details: false, + content: { + content_id: '3', + html: 'Question 3', }, - id: 'TextInput', - customization_args: { - rows: { - value: 1 + interaction: { + answer_groups: [], + default_outcome: { + dest: null, + dest_if_really_stuck: null, + labelled_as_correct: true, + missing_prerequisite_skill_id: null, + refresher_exploration_id: null, + param_changes: [], + feedback: { + content_id: 'feedback_id', + html: '

Dummy Feedback

', + }, }, - placeholder: { - value: { - unicode_str: '', - content_id: 'ca_placeholder_0' - } + id: 'TextInput', + customization_args: { + rows: { + value: 1, + }, + placeholder: { + value: { + unicode_str: '', + content_id: 'ca_placeholder_0', + }, + }, + catchMisspellings: { + value: false, + }, + }, + confirmed_unclassified_answers: [], + hints: [ + { + hint_content: { + content_id: 'hint_1', + html: '

This is a hint.

', + }, + }, + ], + solution: { + correct_answer: 'Solution', + explanation: { + content_id: 'solution', + html: '

This is a solution.

', + }, + answer_is_exclusive: false, }, - catchMisspellings: { - value: false - } }, - confirmed_unclassified_answers: [], - hints: [ - { - hint_content: { - content_id: 'hint_1', - html: '

This is a hint.

' - } - } - ], - solution: { - correct_answer: 'Solution', - explanation: { - content_id: 'solution', - html: '

This is a solution.

' + linked_skill_id: null, + card_is_checkpoint: true, + recorded_voiceovers: { + voiceovers_mapping: { + 1: {}, + ca_placeholder_0: {}, + feedback_id: {}, + solution: {}, + hint_1: {}, }, - answer_is_exclusive: false - } + }, }, - linked_skill_id: null, - card_is_checkpoint: true, - recorded_voiceovers: { - voiceovers_mapping: { - 1: {}, - ca_placeholder_0: {}, - feedback_id: {}, - solution: {}, - hint_1: {} - } - } + question_state_data_schema_version: 45, + language_code: 'ab', + version: 1, + next_content_id_index: 6, + linked_skill_ids: [], + inapplicable_skill_misconception_ids: [], }, - question_state_data_schema_version: 45, - language_code: 'ab', - version: 1, - next_content_id_index: 6, - linked_skill_ids: [], - inapplicable_skill_misconception_ids: [], - }]; + ]; }); beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] + imports: [HttpClientTestingModule], }); audioTranslationLanguageService = TestBed.inject( - AudioTranslationLanguageService); + AudioTranslationLanguageService + ); alertsService = TestBed.inject(AlertsService); answerClassificationService = TestBed.inject(AnswerClassificationService); contextService = TestBed.inject(ContextService); - expressionInterpolationService = - TestBed.inject(ExpressionInterpolationService); + expressionInterpolationService = TestBed.inject( + ExpressionInterpolationService + ); questionObjectFactory = TestBed.inject(QuestionObjectFactory); questionPlayerEngineService = TestBed.inject(QuestionPlayerEngineService); outcomeObjectFactory = TestBed.inject(OutcomeObjectFactory); @@ -414,11 +430,13 @@ describe('Question player engine service ', () => { textInputService = TestBed.get(TextInputRulesService); singleQuestionObject = questionObjectFactory.createFromBackendDict( - singleQuestionBackendDict); + singleQuestionBackendDict + ); multipleQuestionsObjects = multipleQuestionsBackendDict.map( - function(questionDict) { + function (questionDict) { return questionObjectFactory.createFromBackendDict(questionDict); - }); + } + ); }); it('should load questions when initialized', () => { @@ -431,7 +449,10 @@ describe('Question player engine service ', () => { expect(questionPlayerEngineService.getQuestionCount()).toBe(0); questionPlayerEngineService.init( - multipleQuestionsObjects, initSuccessCb, initErrorCb); + multipleQuestionsObjects, + initSuccessCb, + initErrorCb + ); expect(questionPlayerEngineService.getQuestionCount()).toBe(3); }); @@ -443,39 +464,57 @@ describe('Question player engine service ', () => { expect(contextService.isInQuestionPlayerMode()).toBe(false); questionPlayerEngineService.init( - multipleQuestionsObjects, initSuccessCb, initErrorCb); + multipleQuestionsObjects, + initSuccessCb, + initErrorCb + ); expect(contextService.isInQuestionPlayerMode()).toBe(true); }); - it('should update the current question ID when an answer is ' + - 'submitted and a new card is recorded', () => { - let submitAnswerSuccessCb = jasmine.createSpy('success'); - let initSuccessCb = jasmine.createSpy('success'); - let initErrorCb = jasmine.createSpy('fail'); - let answer = 'answer'; - let answerClassificationResult = new AnswerClassificationResult( - outcomeObjectFactory.createNew( - 'default', '', '', []), 1, 0, 'default_outcome'); + it( + 'should update the current question ID when an answer is ' + + 'submitted and a new card is recorded', + () => { + let submitAnswerSuccessCb = jasmine.createSpy('success'); + let initSuccessCb = jasmine.createSpy('success'); + let initErrorCb = jasmine.createSpy('fail'); + let answer = 'answer'; + let answerClassificationResult = new AnswerClassificationResult( + outcomeObjectFactory.createNew('default', '', '', []), + 1, + 0, + 'default_outcome' + ); - spyOn(contextService, 'setQuestionPlayerIsOpen'); - spyOn(contextService, 'isInQuestionPlayerMode').and.returnValue(true); - spyOn(answerClassificationService, 'getMatchingClassificationResult') - .and.returnValue(answerClassificationResult); - spyOn(expressionInterpolationService, 'processHtml') - .and.callFake((html, envs) => html); + spyOn(contextService, 'setQuestionPlayerIsOpen'); + spyOn(contextService, 'isInQuestionPlayerMode').and.returnValue(true); + spyOn( + answerClassificationService, + 'getMatchingClassificationResult' + ).and.returnValue(answerClassificationResult); + spyOn(expressionInterpolationService, 'processHtml').and.callFake( + (html, envs) => html + ); - questionPlayerEngineService.init( - multipleQuestionsObjects, initSuccessCb, initErrorCb); - let currentQuestion1 = questionPlayerEngineService.getCurrentQuestion(); - expect(currentQuestion1.getId()).toBe(multipleQuestionsObjects[0]._id); - - questionPlayerEngineService.submitAnswer( - answer, textInputService, submitAnswerSuccessCb); - questionPlayerEngineService.recordNewCardAdded(); - let currentQuestion2 = questionPlayerEngineService.getCurrentQuestion(); - expect(currentQuestion2.getId()).toBe(multipleQuestionsObjects[1]._id); - }); + questionPlayerEngineService.init( + multipleQuestionsObjects, + initSuccessCb, + initErrorCb + ); + let currentQuestion1 = questionPlayerEngineService.getCurrentQuestion(); + expect(currentQuestion1.getId()).toBe(multipleQuestionsObjects[0]._id); + + questionPlayerEngineService.submitAnswer( + answer, + textInputService, + submitAnswerSuccessCb + ); + questionPlayerEngineService.recordNewCardAdded(); + let currentQuestion2 = questionPlayerEngineService.getCurrentQuestion(); + expect(currentQuestion2.getId()).toBe(multipleQuestionsObjects[1]._id); + } + ); it('should return the current question Id', () => { let initSuccessCb = jasmine.createSpy('success'); @@ -486,14 +525,17 @@ describe('Question player engine service ', () => { expect(() => { questionPlayerEngineService.getCurrentQuestionId(); - }).toThrowError( - 'Cannot read properties of undefined (reading \'getId\')'); + }).toThrowError("Cannot read properties of undefined (reading 'getId')"); questionPlayerEngineService.init( - multipleQuestionsObjects, initSuccessCb, initErrorCb); + multipleQuestionsObjects, + initSuccessCb, + initErrorCb + ); - expect(questionPlayerEngineService.getCurrentQuestionId()) - .toBe(multipleQuestionsObjects[0]._id); + expect(questionPlayerEngineService.getCurrentQuestionId()).toBe( + multipleQuestionsObjects[0]._id + ); }); it('should return number of questions', () => { @@ -504,7 +546,10 @@ describe('Question player engine service ', () => { spyOn(contextService, 'isInQuestionPlayerMode').and.returnValue(true); questionPlayerEngineService.init( - multipleQuestionsObjects, initSuccessCb, initErrorCb); + multipleQuestionsObjects, + initSuccessCb, + initErrorCb + ); let totalQuestions = questionPlayerEngineService.getQuestionCount(); expect(totalQuestions).toBe(3); @@ -513,7 +558,10 @@ describe('Question player engine service ', () => { expect(totalQuestions).toBe(0); questionPlayerEngineService.init( - [singleQuestionObject], initSuccessCb, initErrorCb); + [singleQuestionObject], + initSuccessCb, + initErrorCb + ); totalQuestions = questionPlayerEngineService.getQuestionCount(); expect(totalQuestions).toBe(1); }); @@ -526,7 +574,10 @@ describe('Question player engine service ', () => { spyOn(contextService, 'isInQuestionPlayerMode').and.returnValue(true); questionPlayerEngineService.init( - multipleQuestionsObjects, initSuccessCb, initErrorCb); + multipleQuestionsObjects, + initSuccessCb, + initErrorCb + ); expect(questionPlayerEngineService.getQuestionCount()).toBe(3); @@ -535,199 +586,283 @@ describe('Question player engine service ', () => { expect(questionPlayerEngineService.getQuestionCount()).toBe(0); }); - it('should return the language code correctly when an answer is ' + - 'submitted and a new card is recorded', () => { - let submitAnswerSuccessCb = jasmine.createSpy('success'); - let initSuccessCb = jasmine.createSpy('success'); - let initErrorCb = jasmine.createSpy('fail'); - let answer = 'answer'; - let answerClassificationResult = new AnswerClassificationResult( - outcomeObjectFactory - .createNew('default', '', '', []), 1, 0, 'default_outcome' - ); + it( + 'should return the language code correctly when an answer is ' + + 'submitted and a new card is recorded', + () => { + let submitAnswerSuccessCb = jasmine.createSpy('success'); + let initSuccessCb = jasmine.createSpy('success'); + let initErrorCb = jasmine.createSpy('fail'); + let answer = 'answer'; + let answerClassificationResult = new AnswerClassificationResult( + outcomeObjectFactory.createNew('default', '', '', []), + 1, + 0, + 'default_outcome' + ); - spyOn(contextService, 'setQuestionPlayerIsOpen'); - spyOn(contextService, 'isInQuestionPlayerMode').and.returnValue(true); - spyOn(answerClassificationService, 'getMatchingClassificationResult') - .and.returnValue(answerClassificationResult); - spyOn(expressionInterpolationService, 'processHtml') - .and.callFake((html, envs) => html); + spyOn(contextService, 'setQuestionPlayerIsOpen'); + spyOn(contextService, 'isInQuestionPlayerMode').and.returnValue(true); + spyOn( + answerClassificationService, + 'getMatchingClassificationResult' + ).and.returnValue(answerClassificationResult); + spyOn(expressionInterpolationService, 'processHtml').and.callFake( + (html, envs) => html + ); - questionPlayerEngineService.init( - multipleQuestionsObjects, initSuccessCb, initErrorCb); - let languageCode = questionPlayerEngineService.getLanguageCode(); + questionPlayerEngineService.init( + multipleQuestionsObjects, + initSuccessCb, + initErrorCb + ); + let languageCode = questionPlayerEngineService.getLanguageCode(); - expect(languageCode).toBe(multipleQuestionsObjects[0]._languageCode); + expect(languageCode).toBe(multipleQuestionsObjects[0]._languageCode); - questionPlayerEngineService.submitAnswer( - answer, textInputService, submitAnswerSuccessCb); - questionPlayerEngineService.recordNewCardAdded(); + questionPlayerEngineService.submitAnswer( + answer, + textInputService, + submitAnswerSuccessCb + ); + questionPlayerEngineService.recordNewCardAdded(); - languageCode = questionPlayerEngineService.getLanguageCode(); - expect(languageCode).toBe(multipleQuestionsObjects[1]._languageCode); + languageCode = questionPlayerEngineService.getLanguageCode(); + expect(languageCode).toBe(multipleQuestionsObjects[1]._languageCode); - questionPlayerEngineService.submitAnswer( - answer, textInputService, submitAnswerSuccessCb); - questionPlayerEngineService.recordNewCardAdded(); + questionPlayerEngineService.submitAnswer( + answer, + textInputService, + submitAnswerSuccessCb + ); + questionPlayerEngineService.recordNewCardAdded(); - languageCode = questionPlayerEngineService.getLanguageCode(); - expect(languageCode).toBe(multipleQuestionsObjects[2]._languageCode); - }); + languageCode = questionPlayerEngineService.getLanguageCode(); + expect(languageCode).toBe(multipleQuestionsObjects[2]._languageCode); + } + ); - it('should always return false when calling \'isInPreviewMode()\'', () => { + it("should always return false when calling 'isInPreviewMode()'", () => { let previewMode = questionPlayerEngineService.isInPreviewMode(); expect(previewMode).toBe(false); }); - it('should show warning message while loading a question ' + - 'if the question name is empty', () => { - let initSuccessCb = jasmine.createSpy('success'); - let initErrorCb = jasmine.createSpy('fail'); + it( + 'should show warning message while loading a question ' + + 'if the question name is empty', + () => { + let initSuccessCb = jasmine.createSpy('success'); + let initErrorCb = jasmine.createSpy('fail'); - singleQuestionBackendDict.question_state_data - .content.html = null; - let alertsServiceSpy = spyOn( - alertsService, 'addWarning').and.callThrough(); - spyOn(expressionInterpolationService, 'processHtml') - .and.callFake((html, envs) => html); + singleQuestionBackendDict.question_state_data.content.html = null; + let alertsServiceSpy = spyOn( + alertsService, + 'addWarning' + ).and.callThrough(); + spyOn(expressionInterpolationService, 'processHtml').and.callFake( + (html, envs) => html + ); - questionPlayerEngineService.init( - [questionObjectFactory.createFromBackendDict( - singleQuestionBackendDict)], initSuccessCb, initErrorCb); + questionPlayerEngineService.init( + [ + questionObjectFactory.createFromBackendDict( + singleQuestionBackendDict + ), + ], + initSuccessCb, + initErrorCb + ); - expect(alertsServiceSpy).toHaveBeenCalledWith( - 'Question name should not be empty.'); - }); + expect(alertsServiceSpy).toHaveBeenCalledWith( + 'Question name should not be empty.' + ); + } + ); it('should not load questions if there are no questions', () => { let initSuccessCb = jasmine.createSpy('success'); let initErrorCb = jasmine.createSpy('fail'); - let alertsServiceSpy = spyOn( - alertsService, 'addWarning').and.callThrough(); + let alertsServiceSpy = spyOn(alertsService, 'addWarning').and.callThrough(); - questionPlayerEngineService.init( - [], initSuccessCb, initErrorCb); + questionPlayerEngineService.init([], initSuccessCb, initErrorCb); expect(alertsServiceSpy).toHaveBeenCalledWith( - 'There are no questions to display.'); + 'There are no questions to display.' + ); expect(initSuccessCb).not.toHaveBeenCalled(); expect(initErrorCb).toHaveBeenCalled(); }); describe('on submitting answer ', () => { - it('should call success callback if the submitted ' + - 'answer is correct', () => { - let submitAnswerSuccessCb = jasmine.createSpy('success'); - let initSuccessCb = jasmine.createSpy('success'); - let initErrorCb = jasmine.createSpy('fail'); - let answer = 'answer'; - let answerClassificationResult = new AnswerClassificationResult( - outcomeObjectFactory - .createNew('default', '', '', []), 1, 0, 'default_outcome' - ); - answerClassificationResult.outcome.labelledAsCorrect = true; - - spyOn(answerClassificationService, 'getMatchingClassificationResult') - .and.returnValue(answerClassificationResult); - - questionPlayerEngineService.init( - multipleQuestionsObjects, initSuccessCb, initErrorCb); - questionPlayerEngineService.submitAnswer( - answer, textInputService, submitAnswerSuccessCb); - - expect(questionPlayerEngineService.isAnswerBeingProcessed()).toBe(false); - expect(submitAnswerSuccessCb).toHaveBeenCalled(); - }); - - it('should not submit answer again if the answer ' + - 'is already being processed', () => { - let submitAnswerSuccessCb = jasmine.createSpy('success'); - let answer = 'answer'; - let answerClassificationResult = new AnswerClassificationResult( - outcomeObjectFactory - .createNew('default', '', '', []), 1, 0, 'default_outcome' - ); - - spyOn(answerClassificationService, 'getMatchingClassificationResult') - .and.returnValue(answerClassificationResult); - - questionPlayerEngineService.setAnswerIsBeingProcessed(true); - questionPlayerEngineService.submitAnswer( - answer, textInputService, submitAnswerSuccessCb); - - expect(submitAnswerSuccessCb).not.toHaveBeenCalled(); - }); - - it('should show warning message if the feedback ' + - 'content is empty', () => { - let submitAnswerSuccessCb = jasmine.createSpy('success'); - let initSuccessCb = jasmine.createSpy('success'); - let initErrorCb = jasmine.createSpy('fail'); - let answer = 'answer'; - let answerClassificationResult = new AnswerClassificationResult( - outcomeObjectFactory - .createNew('default', null, null, []), 1, 0, 'default_outcome' - ); - answerClassificationResult.outcome.labelledAsCorrect = true; - - spyOn(answerClassificationService, 'getMatchingClassificationResult') - .and.returnValue(answerClassificationResult); - let alertsServiceSpy = spyOn( - alertsService, 'addWarning').and.callThrough(); - spyOn(expressionInterpolationService, 'processHtml') - .and.callFake((html, envs) => html); - - singleQuestionBackendDict.question_state_data - .interaction.default_outcome.feedback.html = null; - questionPlayerEngineService.init( - [questionObjectFactory.createFromBackendDict( - singleQuestionBackendDict)], initSuccessCb, initErrorCb); + it( + 'should call success callback if the submitted ' + 'answer is correct', + () => { + let submitAnswerSuccessCb = jasmine.createSpy('success'); + let initSuccessCb = jasmine.createSpy('success'); + let initErrorCb = jasmine.createSpy('fail'); + let answer = 'answer'; + let answerClassificationResult = new AnswerClassificationResult( + outcomeObjectFactory.createNew('default', '', '', []), + 1, + 0, + 'default_outcome' + ); + answerClassificationResult.outcome.labelledAsCorrect = true; + + spyOn( + answerClassificationService, + 'getMatchingClassificationResult' + ).and.returnValue(answerClassificationResult); + + questionPlayerEngineService.init( + multipleQuestionsObjects, + initSuccessCb, + initErrorCb + ); + questionPlayerEngineService.submitAnswer( + answer, + textInputService, + submitAnswerSuccessCb + ); + + expect(questionPlayerEngineService.isAnswerBeingProcessed()).toBe( + false + ); + expect(submitAnswerSuccessCb).toHaveBeenCalled(); + } + ); - questionPlayerEngineService.submitAnswer( - answer, textInputService, submitAnswerSuccessCb); + it( + 'should not submit answer again if the answer ' + + 'is already being processed', + () => { + let submitAnswerSuccessCb = jasmine.createSpy('success'); + let answer = 'answer'; + let answerClassificationResult = new AnswerClassificationResult( + outcomeObjectFactory.createNew('default', '', '', []), + 1, + 0, + 'default_outcome' + ); + + spyOn( + answerClassificationService, + 'getMatchingClassificationResult' + ).and.returnValue(answerClassificationResult); + + questionPlayerEngineService.setAnswerIsBeingProcessed(true); + questionPlayerEngineService.submitAnswer( + answer, + textInputService, + submitAnswerSuccessCb + ); + + expect(submitAnswerSuccessCb).not.toHaveBeenCalled(); + } + ); - expect(alertsServiceSpy) - .toHaveBeenCalledWith('Feedback content should not be empty.'); - }); + it( + 'should show warning message if the feedback ' + 'content is empty', + () => { + let submitAnswerSuccessCb = jasmine.createSpy('success'); + let initSuccessCb = jasmine.createSpy('success'); + let initErrorCb = jasmine.createSpy('fail'); + let answer = 'answer'; + let answerClassificationResult = new AnswerClassificationResult( + outcomeObjectFactory.createNew('default', null, null, []), + 1, + 0, + 'default_outcome' + ); + answerClassificationResult.outcome.labelledAsCorrect = true; + + spyOn( + answerClassificationService, + 'getMatchingClassificationResult' + ).and.returnValue(answerClassificationResult); + let alertsServiceSpy = spyOn( + alertsService, + 'addWarning' + ).and.callThrough(); + spyOn(expressionInterpolationService, 'processHtml').and.callFake( + (html, envs) => html + ); + + singleQuestionBackendDict.question_state_data.interaction.default_outcome.feedback.html = + null; + questionPlayerEngineService.init( + [ + questionObjectFactory.createFromBackendDict( + singleQuestionBackendDict + ), + ], + initSuccessCb, + initErrorCb + ); + + questionPlayerEngineService.submitAnswer( + answer, + textInputService, + submitAnswerSuccessCb + ); + + expect(alertsServiceSpy).toHaveBeenCalledWith( + 'Feedback content should not be empty.' + ); + } + ); - it('should show warning message if the question ' + - 'name is empty', () => { + it('should show warning message if the question ' + 'name is empty', () => { let submitAnswerSuccessCb = jasmine.createSpy('success'); let initSuccessCb = jasmine.createSpy('success'); let initErrorCb = jasmine.createSpy('fail'); let answer = 'answer'; let answerClassificationResult = new AnswerClassificationResult( - outcomeObjectFactory - .createNew('default', '', '', []), 1, 0, 'default_outcome' + outcomeObjectFactory.createNew('default', '', '', []), + 1, + 0, + 'default_outcome' ); answerClassificationResult.outcome.labelledAsCorrect = true; - singleQuestionBackendDict.question_state_data - .content.html = null; + singleQuestionBackendDict.question_state_data.content.html = null; let sampleQuestion = questionObjectFactory.createFromBackendDict( - singleQuestionBackendDict); + singleQuestionBackendDict + ); - spyOn(answerClassificationService, 'getMatchingClassificationResult') - .and.returnValue(answerClassificationResult); + spyOn( + answerClassificationService, + 'getMatchingClassificationResult' + ).and.returnValue(answerClassificationResult); let alertsServiceSpy = spyOn( - alertsService, 'addWarning').and.callThrough(); + alertsService, + 'addWarning' + ).and.callThrough(); spyOn(questionPlayerEngineService, 'init').and.callFake(() => { questionPlayerEngineService.addQuestion(sampleQuestion); }); - spyOn(expressionInterpolationService, 'processHtml') - .and.callFake((html, envs) => html); + spyOn(expressionInterpolationService, 'processHtml').and.callFake( + (html, envs) => html + ); questionPlayerEngineService.init( - [sampleQuestion], initSuccessCb, initErrorCb); + [sampleQuestion], + initSuccessCb, + initErrorCb + ); questionPlayerEngineService.setCurrentIndex(0); questionPlayerEngineService.submitAnswer( - answer, textInputService, submitAnswerSuccessCb); + answer, + textInputService, + submitAnswerSuccessCb + ); - expect(alertsServiceSpy) - .toHaveBeenCalledWith('Question name should not be empty.'); + expect(alertsServiceSpy).toHaveBeenCalledWith( + 'Question name should not be empty.' + ); }); it('should update the current index when a card is added', () => { @@ -736,22 +871,33 @@ describe('Question player engine service ', () => { let initErrorCb = jasmine.createSpy('fail'); let answer = 'answer'; let answerClassificationResult = new AnswerClassificationResult( - outcomeObjectFactory - .createNew('default', '', '', []), 1, 0, 'default_outcome' + outcomeObjectFactory.createNew('default', '', '', []), + 1, + 0, + 'default_outcome' ); answerClassificationResult.outcome.labelledAsCorrect = true; spyOn(contextService, 'setQuestionPlayerIsOpen'); spyOn(contextService, 'isInQuestionPlayerMode').and.returnValue(true); - spyOn(answerClassificationService, 'getMatchingClassificationResult') - .and.returnValue(answerClassificationResult); - spyOn(expressionInterpolationService, 'processHtml') - .and.callFake((html, envs) => html); + spyOn( + answerClassificationService, + 'getMatchingClassificationResult' + ).and.returnValue(answerClassificationResult); + spyOn(expressionInterpolationService, 'processHtml').and.callFake( + (html, envs) => html + ); questionPlayerEngineService.init( - multipleQuestionsObjects, initSuccessCb, initErrorCb); + multipleQuestionsObjects, + initSuccessCb, + initErrorCb + ); questionPlayerEngineService.submitAnswer( - answer, textInputService, submitAnswerSuccessCb); + answer, + textInputService, + submitAnswerSuccessCb + ); expect(questionPlayerEngineService.getCurrentIndex()).toBe(0); @@ -760,68 +906,96 @@ describe('Question player engine service ', () => { expect(questionPlayerEngineService.getCurrentIndex()).toBe(1); }); - it('should not create next card if the existing ' + - 'card is the last one', () => { - let submitAnswerSuccessCb = jasmine.createSpy('success'); - let initSuccessCb = jasmine.createSpy('success'); - let initErrorCb = jasmine.createSpy('fail'); - let answer = 'answer'; - let answerClassificationResult = new AnswerClassificationResult( - outcomeObjectFactory - .createNew('default', '', '', []), 1, 0, 'default_outcome' - ); - let sampleCard = StateCard.createNewCard( - 'Card 1', 'Content html', 'Interaction text', null, - null, 'content_id', audioTranslationLanguageService); - - answerClassificationResult.outcome.labelledAsCorrect = true; - - spyOn(answerClassificationService, 'getMatchingClassificationResult') - .and.returnValue(answerClassificationResult); - spyOn(expressionInterpolationService, 'processHtml') - .and.callFake((html, envs) => html); - spyOn(focusManagerService, 'generateFocusLabel') - .and.returnValue('focusLabel'); - - // We are using a stub backend dict which consists of three questions. - questionPlayerEngineService.init( - multipleQuestionsObjects, initSuccessCb, initErrorCb); - - let createNewCardSpy = spyOn( - StateCard, 'createNewCard').and.returnValue(sampleCard); - - expect(createNewCardSpy).toHaveBeenCalledTimes(0); - - // Submitting answer to the first question. - questionPlayerEngineService.submitAnswer( - answer, textInputService, submitAnswerSuccessCb); - - expect( - questionPlayerEngineService.getCurrentQuestionId()).toBe( - multipleQuestionsObjects[0]._id); - expect(createNewCardSpy).toHaveBeenCalledTimes(1); - - questionPlayerEngineService.recordNewCardAdded(); - // Submitting answer to the second question. - questionPlayerEngineService.submitAnswer( - answer, textInputService, submitAnswerSuccessCb); - - expect( - questionPlayerEngineService.getCurrentQuestionId()).toBe( - multipleQuestionsObjects[1]._id); - expect(createNewCardSpy).toHaveBeenCalledTimes(2); - - questionPlayerEngineService.recordNewCardAdded(); - // Submitting answer to the last question. - questionPlayerEngineService.submitAnswer( - answer, textInputService, submitAnswerSuccessCb); - - expect( - questionPlayerEngineService.getCurrentQuestionId()).toBe( - multipleQuestionsObjects[2]._id); - // Please note that after submitting answer to the final question, - // a new card was not created, hence createNewCardSpy was not called. - expect(createNewCardSpy).toHaveBeenCalledTimes(2); - }); + it( + 'should not create next card if the existing ' + 'card is the last one', + () => { + let submitAnswerSuccessCb = jasmine.createSpy('success'); + let initSuccessCb = jasmine.createSpy('success'); + let initErrorCb = jasmine.createSpy('fail'); + let answer = 'answer'; + let answerClassificationResult = new AnswerClassificationResult( + outcomeObjectFactory.createNew('default', '', '', []), + 1, + 0, + 'default_outcome' + ); + let sampleCard = StateCard.createNewCard( + 'Card 1', + 'Content html', + 'Interaction text', + null, + null, + 'content_id', + audioTranslationLanguageService + ); + + answerClassificationResult.outcome.labelledAsCorrect = true; + + spyOn( + answerClassificationService, + 'getMatchingClassificationResult' + ).and.returnValue(answerClassificationResult); + spyOn(expressionInterpolationService, 'processHtml').and.callFake( + (html, envs) => html + ); + spyOn(focusManagerService, 'generateFocusLabel').and.returnValue( + 'focusLabel' + ); + + // We are using a stub backend dict which consists of three questions. + questionPlayerEngineService.init( + multipleQuestionsObjects, + initSuccessCb, + initErrorCb + ); + + let createNewCardSpy = spyOn( + StateCard, + 'createNewCard' + ).and.returnValue(sampleCard); + + expect(createNewCardSpy).toHaveBeenCalledTimes(0); + + // Submitting answer to the first question. + questionPlayerEngineService.submitAnswer( + answer, + textInputService, + submitAnswerSuccessCb + ); + + expect(questionPlayerEngineService.getCurrentQuestionId()).toBe( + multipleQuestionsObjects[0]._id + ); + expect(createNewCardSpy).toHaveBeenCalledTimes(1); + + questionPlayerEngineService.recordNewCardAdded(); + // Submitting answer to the second question. + questionPlayerEngineService.submitAnswer( + answer, + textInputService, + submitAnswerSuccessCb + ); + + expect(questionPlayerEngineService.getCurrentQuestionId()).toBe( + multipleQuestionsObjects[1]._id + ); + expect(createNewCardSpy).toHaveBeenCalledTimes(2); + + questionPlayerEngineService.recordNewCardAdded(); + // Submitting answer to the last question. + questionPlayerEngineService.submitAnswer( + answer, + textInputService, + submitAnswerSuccessCb + ); + + expect(questionPlayerEngineService.getCurrentQuestionId()).toBe( + multipleQuestionsObjects[2]._id + ); + // Please note that after submitting answer to the final question, + // a new card was not created, hence createNewCardSpy was not called. + expect(createNewCardSpy).toHaveBeenCalledTimes(2); + } + ); }); }); diff --git a/core/templates/pages/exploration-player-page/services/question-player-engine.service.ts b/core/templates/pages/exploration-player-page/services/question-player-engine.service.ts index e0cf873ecad9..c46ffdf3d11f 100644 --- a/core/templates/pages/exploration-player-page/services/question-player-engine.service.ts +++ b/core/templates/pages/exploration-player-page/services/question-player-engine.service.ts @@ -16,27 +16,32 @@ * @fileoverview Utility service for the question player for an exploration. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; - -import { AppConstants } from 'app.constants'; -import { BindableVoiceovers } from 'domain/exploration/recorded-voiceovers.model'; -import { Question, QuestionObjectFactory } from 'domain/question/QuestionObjectFactory'; -import { State } from 'domain/state/StateObjectFactory'; -import { StateCard } from 'domain/state_card/state-card.model'; -import { ExpressionInterpolationService } from 'expressions/expression-interpolation.service'; -import { InteractionAnswer } from 'interactions/answer-defs'; -import { AnswerClassificationService, InteractionRulesService } from 'pages/exploration-player-page/services/answer-classification.service'; -import { InteractionSpecsConstants } from 'pages/interaction-specs.constants'; -import { AlertsService } from 'services/alerts.service'; -import { ContextService } from 'services/context.service'; -import { ExplorationHtmlFormatterService } from 'services/exploration-html-formatter.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { AudioTranslationLanguageService } from - 'pages/exploration-player-page/services/audio-translation-language.service'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; + +import {AppConstants} from 'app.constants'; +import {BindableVoiceovers} from 'domain/exploration/recorded-voiceovers.model'; +import { + Question, + QuestionObjectFactory, +} from 'domain/question/QuestionObjectFactory'; +import {State} from 'domain/state/StateObjectFactory'; +import {StateCard} from 'domain/state_card/state-card.model'; +import {ExpressionInterpolationService} from 'expressions/expression-interpolation.service'; +import {InteractionAnswer} from 'interactions/answer-defs'; +import { + AnswerClassificationService, + InteractionRulesService, +} from 'pages/exploration-player-page/services/answer-classification.service'; +import {InteractionSpecsConstants} from 'pages/interaction-specs.constants'; +import {AlertsService} from 'services/alerts.service'; +import {ContextService} from 'services/context.service'; +import {ExplorationHtmlFormatterService} from 'services/exploration-html-formatter.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {AudioTranslationLanguageService} from 'pages/exploration-player-page/services/audio-translation-language.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class QuestionPlayerEngineService { private answerIsBeingProcessed: boolean = false; @@ -45,27 +50,33 @@ export class QuestionPlayerEngineService { private nextIndex: number = null; constructor( - private alertsService: AlertsService, - private answerClassificationService: AnswerClassificationService, - private audioTranslationLanguageService: AudioTranslationLanguageService, - private contextService: ContextService, - private explorationHtmlFormatterService: ExplorationHtmlFormatterService, - private expressionInterpolationService: ExpressionInterpolationService, - private focusManagerService: FocusManagerService, - private questionObjectFactory: QuestionObjectFactory) { - } + private alertsService: AlertsService, + private answerClassificationService: AnswerClassificationService, + private audioTranslationLanguageService: AudioTranslationLanguageService, + private contextService: ContextService, + private explorationHtmlFormatterService: ExplorationHtmlFormatterService, + private expressionInterpolationService: ExpressionInterpolationService, + private focusManagerService: FocusManagerService, + private questionObjectFactory: QuestionObjectFactory + ) {} // Evaluate feedback. private makeFeedback( - feedbackHtml: string, envs: Record[]): string { + feedbackHtml: string, + envs: Record[] + ): string { return this.expressionInterpolationService.processHtml(feedbackHtml, envs); } // Evaluate question string. private makeQuestion( - newState: State, envs: Record[]): string { + newState: State, + envs: Record[] + ): string { return this.expressionInterpolationService.processHtml( - newState.content.html, envs); + newState.content.html, + envs + ); } private getRandomSuffix(): string { @@ -85,10 +96,13 @@ export class QuestionPlayerEngineService { // This should only be called when 'exploration' is non-null. private loadInitialQuestion( - successCallback: (initialCard: StateCard, nextFocusLabel: string) => void, - errorCallback: () => void): void { + successCallback: (initialCard: StateCard, nextFocusLabel: string) => void, + errorCallback: () => void + ): void { this.contextService.setCustomEntityContext( - AppConstants.ENTITY_TYPE.QUESTION, this.questions[0].getId()); + AppConstants.ENTITY_TYPE.QUESTION, + this.questions[0].getId() + ); const initialState = this.questions[0].getStateData(); const questionHtml = this.makeQuestion(initialState, []); @@ -109,14 +123,22 @@ export class QuestionPlayerEngineService { if (interactionId) { interactionHtml = this.explorationHtmlFormatterService.getInteractionHtml( - interactionId, interaction.customizationArgs, true, nextFocusLabel, - null); + interactionId, + interaction.customizationArgs, + true, + nextFocusLabel, + null + ); } - const initialCard = - StateCard.createNewCard( - null, questionHtml, interactionHtml, interaction, - initialState.recordedVoiceovers, initialState.content.contentId, - this.audioTranslationLanguageService); + const initialCard = StateCard.createNewCard( + null, + questionHtml, + interactionHtml, + interaction, + initialState.recordedVoiceovers, + initialState.content.contentId, + this.audioTranslationLanguageService + ); successCallback(initialCard, nextFocusLabel); } @@ -135,13 +157,15 @@ export class QuestionPlayerEngineService { this.getNextStateData().interaction.customizationArgs, true, labelForFocusTarget, - null); + null + ); } init( - questionObjects: Question[], - successCallback: (initialCard: StateCard, nextFocusLabel: string) => void, - errorCallback?: () => void): void { + questionObjects: Question[], + successCallback: (initialCard: StateCard, nextFocusLabel: string) => void, + errorCallback?: () => void + ): void { this.contextService.setQuestionPlayerIsOpen(); this.setAnswerIsBeingProcessed(false); let currentIndex = questionObjects.length; @@ -152,7 +176,9 @@ export class QuestionPlayerEngineService { currentIndex--; [questionObjects[currentIndex], questionObjects[randomIndex]] = [ - questionObjects[randomIndex], questionObjects[currentIndex]]; + questionObjects[randomIndex], + questionObjects[currentIndex], + ]; } for (let i = 0; i < questionObjects.length; i++) { this.addQuestion(questionObjects[i]); @@ -168,7 +194,9 @@ export class QuestionPlayerEngineService { recordNewCardAdded(): void { this.currentIndex = this.nextIndex; this.contextService.setCustomEntityContext( - AppConstants.ENTITY_TYPE.QUESTION, this.getCurrentQuestionId()); + AppConstants.ENTITY_TYPE.QUESTION, + this.getCurrentQuestionId() + ); } getCurrentIndex(): number { @@ -216,22 +244,24 @@ export class QuestionPlayerEngineService { } submitAnswer( - answer: InteractionAnswer, - interactionRulesService: InteractionRulesService, - successCallback: ( - nextCard: StateCard, - refreshInteraction: boolean, - feedbackHtml: string, - feedbackAudioTranslations: BindableVoiceovers, - refresherExplorationId, - missingPrerequisiteSkillId, - remainOnCurrentCard: boolean, - taggedSkillMisconceptionId: string, - wasOldStateInitial, - isFirstHit, - isFinalQuestion: boolean, - nextCardIfReallyStuck: null, - focusLabel: string) => void): boolean { + answer: InteractionAnswer, + interactionRulesService: InteractionRulesService, + successCallback: ( + nextCard: StateCard, + refreshInteraction: boolean, + feedbackHtml: string, + feedbackAudioTranslations: BindableVoiceovers, + refresherExplorationId, + missingPrerequisiteSkillId, + remainOnCurrentCard: boolean, + taggedSkillMisconceptionId: string, + wasOldStateInitial, + isFirstHit, + isFinalQuestion: boolean, + nextCardIfReallyStuck: null, + focusLabel: string + ) => void + ): boolean { if (this.answerIsBeingProcessed) { return; } @@ -240,10 +270,13 @@ export class QuestionPlayerEngineService { this.setAnswerIsBeingProcessed(true); const oldState = this.getCurrentStateData(); const recordedVoiceovers = oldState.recordedVoiceovers; - const classificationResult = ( + const classificationResult = this.answerClassificationService.getMatchingClassificationResult( - null, oldState.interaction, answer, - interactionRulesService)); + null, + oldState.interaction, + answer, + interactionRulesService + ); const answerGroupIndex = classificationResult.answerGroupIndex; const answerIsCorrect = classificationResult.outcome.labelledAsCorrect; let taggedSkillMisconceptionId = null; @@ -259,13 +292,12 @@ export class QuestionPlayerEngineService { const outcome = angular.copy(classificationResult.outcome); // Compute the data for the next state. const oldParams = { - answer: answerString + answer: answerString, }; - const feedbackHtml = - this.makeFeedback(outcome.feedback.html, [oldParams]); + const feedbackHtml = this.makeFeedback(outcome.feedback.html, [oldParams]); const feedbackContentId = outcome.feedback.contentId; - const feedbackAudioTranslations = ( - recordedVoiceovers.getBindableVoiceovers(feedbackContentId)); + const feedbackAudioTranslations = + recordedVoiceovers.getBindableVoiceovers(feedbackContentId); if (feedbackHtml === null) { this.setAnswerIsBeingProcessed(false); this.alertsService.addWarning('Feedback content should not be empty.'); @@ -273,15 +305,18 @@ export class QuestionPlayerEngineService { } let newState = null; - if (answerIsCorrect && (this.currentIndex < this.questions.length - 1)) { + if (answerIsCorrect && this.currentIndex < this.questions.length - 1) { newState = this.questions[this.currentIndex + 1].getStateData(); } else { newState = oldState; } - let questionHtml = this.makeQuestion(newState, [oldParams, { - answer: 'answer' - }]); + let questionHtml = this.makeQuestion(newState, [ + oldParams, + { + answer: 'answer', + }, + ]); if (questionHtml === null) { this.setAnswerIsBeingProcessed(false); this.alertsService.addWarning('Question name should not be empty.'); @@ -290,16 +325,14 @@ export class QuestionPlayerEngineService { this.setAnswerIsBeingProcessed(false); const interactionId = oldState.interaction.id; - const interactionIsInline = ( + const interactionIsInline = !interactionId || - InteractionSpecsConstants. - INTERACTION_SPECS[interactionId].display_mode === - AppConstants.INTERACTION_DISPLAY_MODE_INLINE); - const refreshInteraction = ( - answerIsCorrect || interactionIsInline); + InteractionSpecsConstants.INTERACTION_SPECS[interactionId] + .display_mode === AppConstants.INTERACTION_DISPLAY_MODE_INLINE; + const refreshInteraction = answerIsCorrect || interactionIsInline; this.nextIndex = this.currentIndex + 1; - const isFinalQuestion = (this.nextIndex === this.questions.length); + const isFinalQuestion = this.nextIndex === this.questions.length; const onSameCard = !answerIsCorrect; const _nextFocusLabel = this.focusManagerService.generateFocusLabel(); @@ -311,7 +344,9 @@ export class QuestionPlayerEngineService { questionHtml = questionHtml + this.getRandomSuffix(); nextInteractionHtml = nextInteractionHtml + this.getRandomSuffix(); nextCard = StateCard.createNewCard( - 'true', questionHtml, nextInteractionHtml, + 'true', + questionHtml, + nextInteractionHtml, this.getNextStateData().interaction, this.getNextStateData().recordedVoiceovers, this.getNextStateData().content.contentId, @@ -319,14 +354,27 @@ export class QuestionPlayerEngineService { ); } successCallback( - nextCard, refreshInteraction, feedbackHtml, + nextCard, + refreshInteraction, + feedbackHtml, feedbackAudioTranslations, - null, null, onSameCard, taggedSkillMisconceptionId, - null, null, isFinalQuestion, nextCardIfReallyStuck, _nextFocusLabel); + null, + null, + onSameCard, + taggedSkillMisconceptionId, + null, + null, + isFinalQuestion, + nextCardIfReallyStuck, + _nextFocusLabel + ); return answerIsCorrect; } } -angular.module('oppia').factory( - 'QuestionPlayerEngineService', - downgradeInjectable(QuestionPlayerEngineService)); +angular + .module('oppia') + .factory( + 'QuestionPlayerEngineService', + downgradeInjectable(QuestionPlayerEngineService) + ); diff --git a/core/templates/pages/exploration-player-page/services/refresher-exploration-confirmation-modal.service.spec.ts b/core/templates/pages/exploration-player-page/services/refresher-exploration-confirmation-modal.service.spec.ts index f1cf616bd66f..c108b7f82619 100644 --- a/core/templates/pages/exploration-player-page/services/refresher-exploration-confirmation-modal.service.spec.ts +++ b/core/templates/pages/exploration-player-page/services/refresher-exploration-confirmation-modal.service.spec.ts @@ -16,11 +16,10 @@ * @fileoverview Unit tests for refresher exploration confirmation modal service */ -import { EventEmitter } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { RefresherExplorationConfirmationModalService } - from './refresher-exploration-confirmation-modal.service'; +import {EventEmitter} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {RefresherExplorationConfirmationModalService} from './refresher-exploration-confirmation-modal.service'; describe('Refresher exploration confirmation modal service', () => { let recms: RefresherExplorationConfirmationModalService; @@ -33,16 +32,17 @@ describe('Refresher exploration confirmation modal service', () => { it('should display redirect confirmation modal', () => { const redirectConfirmationCallback = jasmine.createSpy( - 'redirectConfirmationCallback', () => {} + 'redirectConfirmationCallback', + () => {} ); const mockComponentInstance = { confirmRedirectEventEmitter: new EventEmitter(), - refresherExplorationId: '' + refresherExplorationId: '', }; const modalSpy = spyOn(ngbModal, 'open').and.callFake(() => { return { componentInstance: mockComponentInstance, - result: Promise.resolve() + result: Promise.resolve(), } as NgbModalRef; }); diff --git a/core/templates/pages/exploration-player-page/services/refresher-exploration-confirmation-modal.service.ts b/core/templates/pages/exploration-player-page/services/refresher-exploration-confirmation-modal.service.ts index b46e4215db10..deb1f178443f 100644 --- a/core/templates/pages/exploration-player-page/services/refresher-exploration-confirmation-modal.service.ts +++ b/core/templates/pages/exploration-player-page/services/refresher-exploration-confirmation-modal.service.ts @@ -17,38 +17,46 @@ * exploration. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { RefresherExplorationConfirmationModal } from '../modals/refresher-exploration-confirmation-modal.component'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {RefresherExplorationConfirmationModal} from '../modals/refresher-exploration-confirmation-modal.component'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class RefresherExplorationConfirmationModalService { - constructor( - private ngbModal: NgbModal - ) {} + constructor(private ngbModal: NgbModal) {} displayRedirectConfirmationModal( - refresherExplorationId: string, - redirectConfirmationCallback: () => void): void { + refresherExplorationId: string, + redirectConfirmationCallback: () => void + ): void { let modalRef: NgbModalRef = this.ngbModal.open( - RefresherExplorationConfirmationModal, { - backdrop: 'static' - }); + RefresherExplorationConfirmationModal, + { + backdrop: 'static', + } + ); modalRef.componentInstance.confirmRedirectEventEmitter.subscribe(() => { redirectConfirmationCallback(); }); modalRef.componentInstance.refresherExplorationId = refresherExplorationId; - modalRef.result.then(() => {}, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + modalRef.result.then( + () => {}, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } } -angular.module('oppia').factory('RefresherExplorationConfirmationModalService', - downgradeInjectable(RefresherExplorationConfirmationModalService)); +angular + .module('oppia') + .factory( + 'RefresherExplorationConfirmationModalService', + downgradeInjectable(RefresherExplorationConfirmationModalService) + ); diff --git a/core/templates/pages/exploration-player-page/services/state-classifier-mapping.service.spec.ts b/core/templates/pages/exploration-player-page/services/state-classifier-mapping.service.spec.ts index 5f9c6c5d17cb..f2ae72c7999d 100644 --- a/core/templates/pages/exploration-player-page/services/state-classifier-mapping.service.spec.ts +++ b/core/templates/pages/exploration-player-page/services/state-classifier-mapping.service.spec.ts @@ -15,176 +15,188 @@ /** * @fileoverview Unit tests for the State classifier mapping service. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TestBed, waitForAsync } from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TestBed, waitForAsync} from '@angular/core/testing'; -import { AppService } from 'services/app.service'; -import { Classifier } from 'domain/classifier/classifier.model'; -import { ClassifierDataBackendApiService } from 'services/classifier-data-backend-api.service'; -import { StateClassifierMappingService } from - 'pages/exploration-player-page/services/state-classifier-mapping.service'; -import { TextClassifierFrozenModel } from 'classifiers/proto/text_classifier'; +import {AppService} from 'services/app.service'; +import {Classifier} from 'domain/classifier/classifier.model'; +import {ClassifierDataBackendApiService} from 'services/classifier-data-backend-api.service'; +import {StateClassifierMappingService} from 'pages/exploration-player-page/services/state-classifier-mapping.service'; +import {TextClassifierFrozenModel} from 'classifiers/proto/text_classifier'; describe('State classifier mapping service', () => { - describe('Test that classifier data is fetched properly when ML is enabled', - () => { - let mappingService: StateClassifierMappingService; - let appService: AppService; - let classifierFrozenModel = new TextClassifierFrozenModel(); - let classifierDataBackendApiService: ClassifierDataBackendApiService; - const expId = '0'; - const expVersion = 0; - const stateName = 'stateName1'; - - // The model_json attribute in TextClassifierFrozenModel class can't be - // changed to camelcase since the class definition is automatically - // compiled with the help of protoc. - classifierFrozenModel.model_json = JSON.stringify({ - KNN: { - occurrence: 0, - K: 0, - T: 0, - top: 0, - fingerprint_data: { - 0: { - 'class': 0, - fingerprint: [[0]] - } + describe('Test that classifier data is fetched properly when ML is enabled', () => { + let mappingService: StateClassifierMappingService; + let appService: AppService; + let classifierFrozenModel = new TextClassifierFrozenModel(); + let classifierDataBackendApiService: ClassifierDataBackendApiService; + const expId = '0'; + const expVersion = 0; + const stateName = 'stateName1'; + + // The model_json attribute in TextClassifierFrozenModel class can't be + // changed to camelcase since the class definition is automatically + // compiled with the help of protoc. + classifierFrozenModel.model_json = JSON.stringify({ + KNN: { + occurrence: 0, + K: 0, + T: 0, + top: 0, + fingerprint_data: { + 0: { + class: 0, + fingerprint: [[0]], }, - token_to_id: { - a: 0 - } }, - SVM: { - classes: [0], - kernel_params: { - kernel: 'string', - coef0: 0, - degree: 0, - gamma: 0 - }, - intercept: [0], - n_support: [0], - probA: [0], - support_vectors: [[0]], - probB: [0], - dual_coef: [[0]] + token_to_id: { + a: 0, + }, + }, + SVM: { + classes: [0], + kernel_params: { + kernel: 'string', + coef0: 0, + degree: 0, + gamma: 0, }, - cv_vocabulary: { - a: 0 - } + intercept: [0], + n_support: [0], + probA: [0], + support_vectors: [[0]], + probB: [0], + dual_coef: [[0]], + }, + cv_vocabulary: { + a: 0, + }, + }); + + let classifierData = new Classifier( + 'TestClassifier', + classifierFrozenModel.serialize(), + 1 + ); + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [StateClassifierMappingService], }); - let classifierData = new Classifier( - 'TestClassifier', classifierFrozenModel.serialize(), 1); + mappingService = TestBed.get(StateClassifierMappingService); + appService = TestBed.get(AppService); + classifierDataBackendApiService = TestBed.inject( + ClassifierDataBackendApiService + ); + spyOn( + appService, + 'isMachineLearningClassificationEnabled' + ).and.returnValue(true); + }); - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [HttpClientTestingModule], - providers: [StateClassifierMappingService] + it('should fetch classifier data correctly', waitForAsync(async () => { + spyOn( + classifierDataBackendApiService, + 'getClassifierDataAsync' + ).and.callFake(() => { + return new Promise((resolve, reject) => { + resolve(classifierData); }); - - mappingService = TestBed.get(StateClassifierMappingService); - appService = TestBed.get(AppService); - classifierDataBackendApiService = TestBed.inject( - ClassifierDataBackendApiService); - spyOn(appService, 'isMachineLearningClassificationEnabled') - .and.returnValue(true); }); - - it('should fetch classifier data correctly', waitForAsync(async() => { - spyOn( - classifierDataBackendApiService, - 'getClassifierDataAsync').and.callFake(() => { - return new Promise((resolve, reject) => { - resolve(classifierData); - }); + mappingService.init(expId, expVersion); + await mappingService.initializeClassifierDataForState(stateName); + expect(mappingService.hasClassifierData(stateName)).toBe(true); + })); + + it('should handle failure of fetching classifier data', waitForAsync(async () => { + spyOn( + classifierDataBackendApiService, + 'getClassifierDataAsync' + ).and.callFake(() => { + return new Promise((resolve, reject) => { + reject('No classifier data found for exploration'); }); - mappingService.init(expId, expVersion); - await mappingService.initializeClassifierDataForState(stateName); - expect(mappingService.hasClassifierData(stateName)).toBe(true); - })); - - it('should handle failure of fetching classifier data', waitForAsync( - async() => { - spyOn( - classifierDataBackendApiService, - 'getClassifierDataAsync').and.callFake(() => { - return new Promise((resolve, reject) => { - reject('No classifier data found for exploration'); - }); - }); - mappingService.init(expId, expVersion); - await mappingService.initializeClassifierDataForState(stateName); - expect(mappingService.hasClassifierData(stateName)).toBe(false); - })); - - it('should return classifier data when it exists.', () => { - mappingService.init(expId, expVersion); - - mappingService.testOnlySetClassifierData(stateName, classifierData); - let retrievedClassifier = ( - mappingService.getClassifier(stateName) as Classifier); - - expect(retrievedClassifier.algorithmId).toEqual('TestClassifier'); - expect(retrievedClassifier.classifierData).toEqual( - classifierFrozenModel.serialize()); - expect(retrievedClassifier.algorithmVersion).toEqual(1); }); + mappingService.init(expId, expVersion); + await mappingService.initializeClassifierDataForState(stateName); + expect(mappingService.hasClassifierData(stateName)).toBe(false); + })); + + it('should return classifier data when it exists.', () => { + mappingService.init(expId, expVersion); + + mappingService.testOnlySetClassifierData(stateName, classifierData); + let retrievedClassifier = mappingService.getClassifier( + stateName + ) as Classifier; + + expect(retrievedClassifier.algorithmId).toEqual('TestClassifier'); + expect(retrievedClassifier.classifierData).toEqual( + classifierFrozenModel.serialize() + ); + expect(retrievedClassifier.algorithmVersion).toEqual(1); + }); - it('should return undefined when classifier data does not exist.', () => { - mappingService.init(expId, expVersion); - var stateNameNonexistent = 'stateName2'; - var nonExistentClassifier = mappingService.getClassifier( - stateNameNonexistent); - expect(nonExistentClassifier).toBeNull(); - }); + it('should return undefined when classifier data does not exist.', () => { + mappingService.init(expId, expVersion); + var stateNameNonexistent = 'stateName2'; + var nonExistentClassifier = + mappingService.getClassifier(stateNameNonexistent); + expect(nonExistentClassifier).toBeNull(); + }); - it('should return true when it has classifier data.', () => { - mappingService.init(expId, expVersion); - mappingService.testOnlySetClassifierData(stateName, classifierData); - expect(mappingService.hasClassifierData(stateName)).toBe(true); - }); + it('should return true when it has classifier data.', () => { + mappingService.init(expId, expVersion); + mappingService.testOnlySetClassifierData(stateName, classifierData); + expect(mappingService.hasClassifierData(stateName)).toBe(true); + }); - it('should return false when it does not have classifier data .', () => { - mappingService.init(expId, expVersion); - var stateNameNonexistent = 'stateName2'; - expect(mappingService.hasClassifierData( - stateNameNonexistent)).toBe(false); - }); + it('should return false when it does not have classifier data .', () => { + mappingService.init(expId, expVersion); + var stateNameNonexistent = 'stateName2'; + expect(mappingService.hasClassifierData(stateNameNonexistent)).toBe( + false + ); + }); - it('should not return correct classifier details when init is not ' + - 'called', () => { + it( + 'should not return correct classifier details when init is not ' + + 'called', + () => { mappingService.stateClassifierMapping = {}; var retrievedClassifier = mappingService.getClassifier(stateName); expect(retrievedClassifier).toBeNull(); + } + ); + }); + + describe('Test that classifier data is not fetched when ML is disabled', () => { + let mappingService: StateClassifierMappingService; + let appService: AppService; + const expId = '0'; + const stateName = 'stateName1'; + const expVersion = 0; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [StateClassifierMappingService], }); - }); - describe('Test that classifier data is not fetched when ML is disabled', - () => { - let mappingService: StateClassifierMappingService; - let appService: AppService; - const expId = '0'; - const stateName = 'stateName1'; - const expVersion = 0; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [HttpClientTestingModule], - providers: [StateClassifierMappingService] - }); - - mappingService = TestBed.get(StateClassifierMappingService); - appService = TestBed.get(AppService); - spyOn(appService, 'isMachineLearningClassificationEnabled') - .and.returnValue(false); - }); + mappingService = TestBed.get(StateClassifierMappingService); + appService = TestBed.get(AppService); + spyOn( + appService, + 'isMachineLearningClassificationEnabled' + ).and.returnValue(false); + }); - it('should not return classifier data.', () => { - mappingService.init(expId, expVersion); - expect(mappingService.hasClassifierData(stateName)).toBe(false); - expect(mappingService.getClassifier(stateName)).toBeNull(); - }); + it('should not return classifier data.', () => { + mappingService.init(expId, expVersion); + expect(mappingService.hasClassifierData(stateName)).toBe(false); + expect(mappingService.getClassifier(stateName)).toBeNull(); }); + }); }); diff --git a/core/templates/pages/exploration-player-page/services/state-classifier-mapping.service.ts b/core/templates/pages/exploration-player-page/services/state-classifier-mapping.service.ts index 2e4638767ded..3dc17b691481 100644 --- a/core/templates/pages/exploration-player-page/services/state-classifier-mapping.service.ts +++ b/core/templates/pages/exploration-player-page/services/state-classifier-mapping.service.ts @@ -16,13 +16,13 @@ * @fileoverview Services for mapping state names to classifier details. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { AppService } from 'services/app.service'; -import { Classifier } from 'domain/classifier/classifier.model'; -import { ClassifierDataBackendApiService } from 'services/classifier-data-backend-api.service'; -import { LoggerService } from 'services/contextual/logger.service'; +import {AppService} from 'services/app.service'; +import {Classifier} from 'domain/classifier/classifier.model'; +import {ClassifierDataBackendApiService} from 'services/classifier-data-backend-api.service'; +import {LoggerService} from 'services/contextual/logger.service'; interface StateClassifierMapping { // The classifier corresponding to a state will be null if machine learning @@ -31,7 +31,7 @@ interface StateClassifierMapping { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class StateClassifierMappingService { _explorationId!: string; @@ -41,7 +41,8 @@ export class StateClassifierMappingService { constructor( private appService: AppService, private classifierDataService: ClassifierDataBackendApiService, - private loggerService: LoggerService) {} + private loggerService: LoggerService + ) {} init(explorationId: string, explorationVersion: number): void { this.loggerService.info('Initializing state classifier mapping service'); @@ -55,14 +56,20 @@ export class StateClassifierMappingService { this.stateClassifierMapping[stateName] = null; this.loggerService.info('Fetching classifier data for ' + stateName); try { - const classifier = ( + const classifier = await this.classifierDataService.getClassifierDataAsync( - this._explorationId, this._explorationVersion, stateName)); + this._explorationId, + this._explorationVersion, + stateName + ); this.stateClassifierMapping[stateName] = classifier; } catch (error) { this.loggerService.error( - 'Fetching classifier data for ' + stateName + - ' failed with error: ' + error); + 'Fetching classifier data for ' + + stateName + + ' failed with error: ' + + error + ); } } } @@ -71,16 +78,20 @@ export class StateClassifierMappingService { if (!this.appService.isMachineLearningClassificationEnabled()) { return false; } - return this.stateClassifierMapping && - stateName in this.stateClassifierMapping && - this.stateClassifierMapping[stateName] !== null; + return ( + this.stateClassifierMapping && + stateName in this.stateClassifierMapping && + this.stateClassifierMapping[stateName] !== null + ); } // Function returns null if Machine Learning Classification is not enabled. getClassifier(stateName: string): Classifier | null { - if (this.stateClassifierMapping[stateName] && - this.appService.isMachineLearningClassificationEnabled() && - this.hasClassifierData(stateName)) { + if ( + this.stateClassifierMapping[stateName] && + this.appService.isMachineLearningClassificationEnabled() && + this.hasClassifierData(stateName) + ) { return this.stateClassifierMapping[stateName]; } return null; @@ -88,11 +99,16 @@ export class StateClassifierMappingService { // NOTE TO DEVELOPERS: This method should only be used for tests. testOnlySetClassifierData( - stateName: string, classifierData: Classifier): void { + stateName: string, + classifierData: Classifier + ): void { this.stateClassifierMapping[stateName] = classifierData; } } -angular.module('oppia').factory( - 'StateClassifierMappingService', - downgradeInjectable(StateClassifierMappingService)); +angular + .module('oppia') + .factory( + 'StateClassifierMappingService', + downgradeInjectable(StateClassifierMappingService) + ); diff --git a/core/templates/pages/exploration-player-page/services/stats-reporting.service.spec.ts b/core/templates/pages/exploration-player-page/services/stats-reporting.service.spec.ts index 616f70f84d5c..5c07a0d3357b 100644 --- a/core/templates/pages/exploration-player-page/services/stats-reporting.service.spec.ts +++ b/core/templates/pages/exploration-player-page/services/stats-reporting.service.spec.ts @@ -16,18 +16,25 @@ * @fileoverview Unit tests for stats reporting service. */ -import { discardPeriodicTasks, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; - -import { AggregatedStats, StatsReportingBackendApiService } from 'domain/exploration/stats-reporting-backend-api.service'; -import { ContextService } from 'services/context.service'; -import { UrlService } from 'services/contextual/url.service'; -import { MessengerService } from 'services/messenger.service'; -import { PlaythroughService } from 'services/playthrough.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { StatsReportingService } from './stats-reporting.service'; -import { Stopwatch } from 'domain/utilities/stopwatch.model'; - +import { + discardPeriodicTasks, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; + +import { + AggregatedStats, + StatsReportingBackendApiService, +} from 'domain/exploration/stats-reporting-backend-api.service'; +import {ContextService} from 'services/context.service'; +import {UrlService} from 'services/contextual/url.service'; +import {MessengerService} from 'services/messenger.service'; +import {PlaythroughService} from 'services/playthrough.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {StatsReportingService} from './stats-reporting.service'; +import {Stopwatch} from 'domain/utilities/stopwatch.model'; describe('Stats reporting service ', () => { let contextService: ContextService; @@ -46,13 +53,14 @@ describe('Stats reporting service ', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] + imports: [HttpClientTestingModule], }); contextService = TestBed.inject(ContextService); statsReportingService = TestBed.inject(StatsReportingService); statsReportingBackendApiService = TestBed.inject( - StatsReportingBackendApiService); + StatsReportingBackendApiService + ); messengerService = TestBed.inject(MessengerService); siteAnalyticsService = TestBed.inject(SiteAnalyticsService); playthroughService = TestBed.inject(PlaythroughService); @@ -62,73 +70,82 @@ describe('Stats reporting service ', () => { beforeEach(() => { spyOn(messengerService, 'sendMessage').and.callThrough(); spyOn(siteAnalyticsService, 'registerNewCard').and.callThrough(); - spyOn(siteAnalyticsService, 'registerStartExploration') - .and.callThrough(); - spyOn(siteAnalyticsService, 'registerFinishExploration') - .and.callThrough(); - spyOn(siteAnalyticsService, 'registerCuratedLessonCompleted') - .and.callThrough(); - spyOn(siteAnalyticsService, 'registerAnswerSubmitted') - .and.callThrough(); - spyOn(playthroughService, 'recordExplorationStartAction') - .and.callThrough(); - spyOn(playthroughService, 'recordExplorationQuitAction') - .and.callThrough(); - spyOn(playthroughService, 'storePlaythrough') - .and.callThrough(); + spyOn(siteAnalyticsService, 'registerStartExploration').and.callThrough(); + spyOn(siteAnalyticsService, 'registerFinishExploration').and.callThrough(); + spyOn( + siteAnalyticsService, + 'registerCuratedLessonCompleted' + ).and.callThrough(); + spyOn(siteAnalyticsService, 'registerAnswerSubmitted').and.callThrough(); + spyOn(playthroughService, 'recordExplorationStartAction').and.callThrough(); + spyOn(playthroughService, 'recordExplorationQuitAction').and.callThrough(); + spyOn(playthroughService, 'storePlaythrough').and.callThrough(); spyOn(contextService, 'isInExplorationEditorPage').and.returnValue(true); spyOn(contextService, 'isInQuestionPlayerMode').and.returnValue(true); statsReportingService.stateStopwatch = Stopwatch.create(); }); it('should create default aggregated stats when initialized', () => { - spyOn(urlService, 'getUrlParams') - .and.returnValue({classroom_url_fragment: 'classroom'}); + spyOn(urlService, 'getUrlParams').and.returnValue({ + classroom_url_fragment: 'classroom', + }); let defaultValues: AggregatedStats = { num_starts: 0, num_completions: 0, num_actual_starts: 0, - state_stats_mapping: {} + state_stats_mapping: {}, }; expect(statsReportingService.aggregatedStats).toEqual(defaultValues); }); - it('should set session properties when calling ' + - '\'initSession\'', fakeAsync(() => { - spyOn(urlService, 'getUrlParams') - .and.returnValue({classroom_url_fragment: 'classroom'}); - // Prechecks. - expect(statsReportingService.explorationId).toBeUndefined(); - expect(statsReportingService.explorationTitle).toBeUndefined(); - expect(statsReportingService.explorationVersion) - .toBeUndefined(); - expect(statsReportingService.sessionId).toBeUndefined(); - expect(statsReportingService.optionalCollectionId).toBeUndefined(); - statsReportingService.initSession( - explorationId, explorationTitle, explorationVersion, - sessionId, collectionId); - tick(300001); - discardPeriodicTasks(); - - expect(statsReportingService.explorationId).toEqual(explorationId); - expect(statsReportingService.explorationTitle).toEqual(explorationTitle); - expect(statsReportingService.explorationVersion) - .toEqual(explorationVersion); - expect(statsReportingService.sessionId).toEqual(sessionId); - expect(statsReportingService.optionalCollectionId).toEqual(collectionId); - })); - - it('should record exploration\'s stats when it is about to start', () => { - spyOn(urlService, 'getUrlParams') - .and.returnValue({classroom_url_fragment: 'classroom'}); + it( + 'should set session properties when calling ' + "'initSession'", + fakeAsync(() => { + spyOn(urlService, 'getUrlParams').and.returnValue({ + classroom_url_fragment: 'classroom', + }); + // Prechecks. + expect(statsReportingService.explorationId).toBeUndefined(); + expect(statsReportingService.explorationTitle).toBeUndefined(); + expect(statsReportingService.explorationVersion).toBeUndefined(); + expect(statsReportingService.sessionId).toBeUndefined(); + expect(statsReportingService.optionalCollectionId).toBeUndefined(); + statsReportingService.initSession( + explorationId, + explorationTitle, + explorationVersion, + sessionId, + collectionId + ); + tick(300001); + discardPeriodicTasks(); + + expect(statsReportingService.explorationId).toEqual(explorationId); + expect(statsReportingService.explorationTitle).toEqual(explorationTitle); + expect(statsReportingService.explorationVersion).toEqual( + explorationVersion + ); + expect(statsReportingService.sessionId).toEqual(sessionId); + expect(statsReportingService.optionalCollectionId).toEqual(collectionId); + }) + ); + + it("should record exploration's stats when it is about to start", () => { + spyOn(urlService, 'getUrlParams').and.returnValue({ + classroom_url_fragment: 'classroom', + }); let recordExplorationStartedSpy = spyOn( - statsReportingBackendApiService, 'recordExpStartedAsync') - .and.returnValue(Promise.resolve({})); - spyOn(statsReportingBackendApiService, 'recordStateHitAsync') - .and.returnValue(Promise.resolve({})); - spyOn(statsReportingBackendApiService, 'postsStatsAsync') - .and.returnValue(Promise.resolve({})); + statsReportingBackendApiService, + 'recordExpStartedAsync' + ).and.returnValue(Promise.resolve({})); + spyOn( + statsReportingBackendApiService, + 'recordStateHitAsync' + ).and.returnValue(Promise.resolve({})); + spyOn(statsReportingBackendApiService, 'postsStatsAsync').and.returnValue( + Promise.resolve({}) + ); let sampleStats = { total_answers_count: 1, @@ -136,10 +153,10 @@ describe('Stats reporting service ', () => { total_hit_count: 1, first_hit_count: 1, num_times_solution_viewed: 1, - num_completions: 1 + num_completions: 1, }; - statsReportingService.aggregatedStats.state_stats_mapping.firstState = ( - sampleStats); + statsReportingService.aggregatedStats.state_stats_mapping.firstState = + sampleStats; expect(statsReportingService.explorationStarted).toBe(false); @@ -150,47 +167,66 @@ describe('Stats reporting service ', () => { expect(statsReportingBackendApiService.postsStatsAsync).toHaveBeenCalled(); }); - it('should not again record exploration\'s stats when ' + - 'it is about to start and already recorded', () => { - spyOn(urlService, 'getUrlParams') - .and.returnValue({classroom_url_fragment: 'classroom'}); - let recordExplorationStartedSpy = spyOn( - statsReportingBackendApiService, 'recordExpStartedAsync') - .and.returnValue(Promise.resolve({})); - statsReportingService.explorationStarted = true; - - statsReportingService.recordExplorationStarted('firstState', {}); - - expect(recordExplorationStartedSpy).not.toHaveBeenCalled(); - }); - - it('should not send request to backend when an exploration ' + - 'it is about to start and already recorded', () => { - spyOn(urlService, 'getUrlParams') - .and.returnValue({classroom_url_fragment: 'classroom'}); - let postStatsSpy = spyOn(statsReportingBackendApiService, 'postsStatsAsync') - .and.returnValue(Promise.resolve({})); - spyOn(statsReportingBackendApiService, 'recordExpStartedAsync') - .and.returnValue(Promise.resolve({})); - spyOn(statsReportingBackendApiService, 'recordStateHitAsync') - .and.returnValue(Promise.resolve({})); - statsReportingService.explorationIsComplete = true; - - statsReportingService.recordExplorationStarted('firstState', {}); - - expect(postStatsSpy).not.toHaveBeenCalled(); - expect(statsReportingBackendApiService.recordExpStartedAsync) - .toHaveBeenCalled(); - expect(statsReportingBackendApiService.recordStateHitAsync) - .toHaveBeenCalled(); - }); - - it('should record exploration\'s stats when it is actually started', () => { - spyOn(urlService, 'getUrlParams') - .and.returnValue({classroom_url_fragment: 'classroom'}); + it( + "should not again record exploration's stats when " + + 'it is about to start and already recorded', + () => { + spyOn(urlService, 'getUrlParams').and.returnValue({ + classroom_url_fragment: 'classroom', + }); + let recordExplorationStartedSpy = spyOn( + statsReportingBackendApiService, + 'recordExpStartedAsync' + ).and.returnValue(Promise.resolve({})); + statsReportingService.explorationStarted = true; + + statsReportingService.recordExplorationStarted('firstState', {}); + + expect(recordExplorationStartedSpy).not.toHaveBeenCalled(); + } + ); + + it( + 'should not send request to backend when an exploration ' + + 'it is about to start and already recorded', + () => { + spyOn(urlService, 'getUrlParams').and.returnValue({ + classroom_url_fragment: 'classroom', + }); + let postStatsSpy = spyOn( + statsReportingBackendApiService, + 'postsStatsAsync' + ).and.returnValue(Promise.resolve({})); + spyOn( + statsReportingBackendApiService, + 'recordExpStartedAsync' + ).and.returnValue(Promise.resolve({})); + spyOn( + statsReportingBackendApiService, + 'recordStateHitAsync' + ).and.returnValue(Promise.resolve({})); + statsReportingService.explorationIsComplete = true; + + statsReportingService.recordExplorationStarted('firstState', {}); + + expect(postStatsSpy).not.toHaveBeenCalled(); + expect( + statsReportingBackendApiService.recordExpStartedAsync + ).toHaveBeenCalled(); + expect( + statsReportingBackendApiService.recordStateHitAsync + ).toHaveBeenCalled(); + } + ); + + it("should record exploration's stats when it is actually started", () => { + spyOn(urlService, 'getUrlParams').and.returnValue({ + classroom_url_fragment: 'classroom', + }); let recordExplorationActuallyStartedSpy = spyOn( - statsReportingBackendApiService, 'recordExplorationActuallyStartedAsync') - .and.returnValue(Promise.resolve({})); + statsReportingBackendApiService, + 'recordExplorationActuallyStartedAsync' + ).and.returnValue(Promise.resolve({})); expect(statsReportingService.currentStateName).toBeUndefined(); expect(statsReportingService.explorationActuallyStarted).toBe(false); @@ -201,26 +237,33 @@ describe('Stats reporting service ', () => { expect(recordExplorationActuallyStartedSpy).toHaveBeenCalled(); }); - it('should not again record exploration\'s stats when ' + - 'it is actually started and already recorded', () => { - spyOn(urlService, 'getUrlParams') - .and.returnValue({classroom_url_fragment: 'classroom'}); - let recordExplorationActuallyStartedSpy = spyOn( - statsReportingBackendApiService, 'recordExplorationActuallyStartedAsync') - .and.returnValue(Promise.resolve({})); - statsReportingService.explorationActuallyStarted = true; - - statsReportingService.recordExplorationActuallyStarted('firstState'); - - expect(recordExplorationActuallyStartedSpy).not.toHaveBeenCalled(); - }); + it( + "should not again record exploration's stats when " + + 'it is actually started and already recorded', + () => { + spyOn(urlService, 'getUrlParams').and.returnValue({ + classroom_url_fragment: 'classroom', + }); + let recordExplorationActuallyStartedSpy = spyOn( + statsReportingBackendApiService, + 'recordExplorationActuallyStartedAsync' + ).and.returnValue(Promise.resolve({})); + statsReportingService.explorationActuallyStarted = true; + + statsReportingService.recordExplorationActuallyStarted('firstState'); + + expect(recordExplorationActuallyStartedSpy).not.toHaveBeenCalled(); + } + ); it('should record stats status of solution', () => { - spyOn(urlService, 'getUrlParams') - .and.returnValue({classroom_url_fragment: 'classroom'}); + spyOn(urlService, 'getUrlParams').and.returnValue({ + classroom_url_fragment: 'classroom', + }); let recordSolutionHitSpy = spyOn( - statsReportingBackendApiService, 'recordSolutionHitAsync') - .and.returnValue(Promise.resolve({})); + statsReportingBackendApiService, + 'recordSolutionHitAsync' + ).and.returnValue(Promise.resolve({})); statsReportingService.recordSolutionHit('firstState'); @@ -228,15 +271,19 @@ describe('Stats reporting service ', () => { }); it('should record stats when refresher exploration is opened', () => { - spyOn(urlService, 'getUrlParams') - .and.returnValue({classroom_url_fragment: 'classroom'}); + spyOn(urlService, 'getUrlParams').and.returnValue({ + classroom_url_fragment: 'classroom', + }); let recordLeaveForRefresherExpSpy = spyOn( - statsReportingBackendApiService, 'recordLeaveForRefresherExpAsync') - .and.returnValue(Promise.resolve({})); + statsReportingBackendApiService, + 'recordLeaveForRefresherExpAsync' + ).and.returnValue(Promise.resolve({})); expect(statsReportingService.nextExpId).toBeUndefined(); statsReportingService.recordLeaveForRefresherExp( - 'firstState', 'refresherExp'); + 'firstState', + 'refresherExp' + ); expect(statsReportingService.nextExpId).toBe('refresherExp'); expect(recordLeaveForRefresherExpSpy).toHaveBeenCalled(); @@ -244,25 +291,47 @@ describe('Stats reporting service ', () => { it('should record stats for community exp when state is changed', () => { let recordStateHitSpy = spyOn( - statsReportingBackendApiService, 'recordStateHitAsync') - .and.returnValue(Promise.resolve({})); + statsReportingBackendApiService, + 'recordStateHitAsync' + ).and.returnValue(Promise.resolve({})); spyOn(urlService, 'getUrlParams').and.returnValue({}); expect(statsReportingService.statesVisited.size).toBe(0); // First transition. statsReportingService.recordStateTransition( - 'firstState', 'secondState', 'answer', {}, true, - '1', '2', 'en'); + 'firstState', + 'secondState', + 'answer', + {}, + true, + '1', + '2', + 'en' + ); // Second transition. statsReportingService.recordStateTransition( - 'secondState', 'thirdState', 'answer', {}, true, - '1', '2', 'en'); + 'secondState', + 'thirdState', + 'answer', + {}, + true, + '1', + '2', + 'en' + ); // Third transition. statsReportingService.recordStateTransition( - 'thirdState', 'fourthState', 'answer', {}, true, - '1', '2', 'en'); + 'thirdState', + 'fourthState', + 'answer', + {}, + true, + '1', + '2', + 'en' + ); expect(recordStateHitSpy).toHaveBeenCalled(); expect(statsReportingService.statesVisited.size).toBe(3); @@ -270,40 +339,62 @@ describe('Stats reporting service ', () => { it('should record stats for curated exp when state is changed', () => { let recordStateHitSpy = spyOn( - statsReportingBackendApiService, 'recordStateHitAsync') - .and.returnValue(Promise.resolve({})); - spyOn(urlService, 'getUrlParams').and.returnValue( - { - classroom_url_fragment: 'classroom' - } - ); + statsReportingBackendApiService, + 'recordStateHitAsync' + ).and.returnValue(Promise.resolve({})); + spyOn(urlService, 'getUrlParams').and.returnValue({ + classroom_url_fragment: 'classroom', + }); expect(statsReportingService.statesVisited.size).toBe(0); // First transition. statsReportingService.recordStateTransition( - 'firstState', 'secondState', 'answer', {}, true, - '1', '2', 'en'); + 'firstState', + 'secondState', + 'answer', + {}, + true, + '1', + '2', + 'en' + ); // Second transition. statsReportingService.recordStateTransition( - 'secondState', 'thirdState', 'answer', {}, true, - '1', '2', 'en'); + 'secondState', + 'thirdState', + 'answer', + {}, + true, + '1', + '2', + 'en' + ); // Third transition. statsReportingService.recordStateTransition( - 'thirdState', 'fourthState', 'answer', {}, true, - '1', '2', 'en'); + 'thirdState', + 'fourthState', + 'answer', + {}, + true, + '1', + '2', + 'en' + ); expect(recordStateHitSpy).toHaveBeenCalled(); expect(statsReportingService.statesVisited.size).toBe(3); }); it('should record stats when a card in exploration is finished', () => { - spyOn(urlService, 'getUrlParams') - .and.returnValue({classroom_url_fragment: 'classroom'}); + spyOn(urlService, 'getUrlParams').and.returnValue({ + classroom_url_fragment: 'classroom', + }); let recordStateCompletedSpy = spyOn( - statsReportingBackendApiService, 'recordStateCompletedAsync') - .and.returnValue(Promise.resolve({})); + statsReportingBackendApiService, + 'recordStateCompletedAsync' + ).and.returnValue(Promise.resolve({})); expect(statsReportingService.currentStateName).toBeUndefined(); statsReportingService.recordStateCompleted('firstState'); @@ -313,17 +404,25 @@ describe('Stats reporting service ', () => { }); it('should record stats for classroom lesson when finished', () => { - spyOn(urlService, 'getUrlParams') - .and.returnValue({classroom_url_fragment: 'classroom'}); + spyOn(urlService, 'getUrlParams').and.returnValue({ + classroom_url_fragment: 'classroom', + }); let recordExplorationCompletedSpy = spyOn( - statsReportingBackendApiService, 'recordExplorationCompletedAsync') - .and.returnValue(Promise.resolve({})); - spyOn(statsReportingBackendApiService, 'postsStatsAsync') - .and.returnValue(Promise.resolve({})); + statsReportingBackendApiService, + 'recordExplorationCompletedAsync' + ).and.returnValue(Promise.resolve({})); + spyOn(statsReportingBackendApiService, 'postsStatsAsync').and.returnValue( + Promise.resolve({}) + ); expect(statsReportingService.explorationIsComplete).toBe(false); statsReportingService.recordExplorationCompleted( - 'firstState', {}, '1', '2', 'en'); + 'firstState', + {}, + '1', + '2', + 'en' + ); expect( siteAnalyticsService.registerCuratedLessonCompleted @@ -336,15 +435,22 @@ describe('Stats reporting service ', () => { it('should record stats for community lesson when finished', () => { spyOn(urlService, 'getUrlParams').and.returnValue({}); let recordExplorationCompletedSpy = spyOn( - statsReportingBackendApiService, 'recordExplorationCompletedAsync') - .and.returnValue(Promise.resolve({})); - spyOn(statsReportingBackendApiService, 'postsStatsAsync') - .and.returnValue(Promise.resolve({})); + statsReportingBackendApiService, + 'recordExplorationCompletedAsync' + ).and.returnValue(Promise.resolve({})); + spyOn(statsReportingBackendApiService, 'postsStatsAsync').and.returnValue( + Promise.resolve({}) + ); spyOn(siteAnalyticsService, 'registerCommunityLessonCompleted'); expect(statsReportingService.explorationIsComplete).toBe(false); statsReportingService.recordExplorationCompleted( - 'firstState', {}, '1', '2', 'en'); + 'firstState', + {}, + '1', + '2', + 'en' + ); expect( siteAnalyticsService.registerCommunityLessonCompleted @@ -355,13 +461,16 @@ describe('Stats reporting service ', () => { }); it('should record stats when a leave event is triggered', () => { - spyOn(urlService, 'getUrlParams') - .and.returnValue({classroom_url_fragment: 'classroom'}); + spyOn(urlService, 'getUrlParams').and.returnValue({ + classroom_url_fragment: 'classroom', + }); let recordMaybeLeaveEventSpy = spyOn( - statsReportingBackendApiService, 'recordMaybeLeaveEventAsync') - .and.returnValue(Promise.resolve({})); - spyOn(statsReportingBackendApiService, 'postsStatsAsync') - .and.returnValue(Promise.resolve({})); + statsReportingBackendApiService, + 'recordMaybeLeaveEventAsync' + ).and.returnValue(Promise.resolve({})); + spyOn(statsReportingBackendApiService, 'postsStatsAsync').and.returnValue( + Promise.resolve({}) + ); statsReportingService.recordMaybeLeaveEvent('firstState', {}); @@ -370,36 +479,57 @@ describe('Stats reporting service ', () => { }); it('should record stats when an answer submit button is clicked', () => { - spyOn(urlService, 'getUrlParams') - .and.returnValue({classroom_url_fragment: 'classroom'}); + spyOn(urlService, 'getUrlParams').and.returnValue({ + classroom_url_fragment: 'classroom', + }); let recordAnswerSubmitActionSpy = spyOn( - playthroughService, 'recordAnswerSubmitAction') - .and.callThrough(); + playthroughService, + 'recordAnswerSubmitAction' + ).and.callThrough(); statsReportingService.recordAnswerSubmitAction( - 'oldState', 'newState', 'expId', 'answer', 'feedback'); + 'oldState', + 'newState', + 'expId', + 'answer', + 'feedback' + ); expect(recordAnswerSubmitActionSpy).toHaveBeenCalled(); }); it('should record stats when an answer is actually submitted', () => { - spyOn(urlService, 'getUrlParams') - .and.returnValue({classroom_url_fragment: 'classroom'}); + spyOn(urlService, 'getUrlParams').and.returnValue({ + classroom_url_fragment: 'classroom', + }); let recordAnswerSubmittedSpy = spyOn( - statsReportingBackendApiService, 'recordAnswerSubmittedAsync') - .and.returnValue(Promise.resolve({})); + statsReportingBackendApiService, + 'recordAnswerSubmittedAsync' + ).and.returnValue(Promise.resolve({})); statsReportingService.recordAnswerSubmitted( - 'firstState', {}, 'answer', explorationId, true, 0, 0, 'category', true); + 'firstState', + {}, + 'answer', + explorationId, + true, + 0, + 0, + 'category', + true + ); expect(recordAnswerSubmittedSpy).toHaveBeenCalled(); - expect(siteAnalyticsService.registerAnswerSubmitted) - .toHaveBeenCalledWith(explorationId, true); + expect(siteAnalyticsService.registerAnswerSubmitted).toHaveBeenCalledWith( + explorationId, + true + ); }); it('should set topic name', () => { - spyOn(urlService, 'getUrlParams') - .and.returnValue({classroom_url_fragment: 'classroom'}); + spyOn(urlService, 'getUrlParams').and.returnValue({ + classroom_url_fragment: 'classroom', + }); expect(statsReportingService.topicName).toBeUndefined(); statsReportingService.setTopicName('newTopic'); diff --git a/core/templates/pages/exploration-player-page/services/stats-reporting.service.ts b/core/templates/pages/exploration-player-page/services/stats-reporting.service.ts index 69836df7d165..c6e9e22ba15e 100644 --- a/core/templates/pages/exploration-player-page/services/stats-reporting.service.ts +++ b/core/templates/pages/exploration-player-page/services/stats-reporting.service.ts @@ -16,36 +16,36 @@ * @fileoverview Services for stats reporting. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable, NgZone } from '@angular/core'; - -import { ContextService } from 'services/context.service'; -import { UrlService } from 'services/contextual/url.service'; -import { MessengerService } from 'services/messenger.service'; -import { PlaythroughService } from 'services/playthrough.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { AggregatedStats, StatsReportingBackendApiService } from - 'domain/exploration/stats-reporting-backend-api.service'; -import { Stopwatch } from 'domain/utilities/stopwatch.model'; -import { ServicesConstants } from 'services/services.constants'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable, NgZone} from '@angular/core'; + +import {ContextService} from 'services/context.service'; +import {UrlService} from 'services/contextual/url.service'; +import {MessengerService} from 'services/messenger.service'; +import {PlaythroughService} from 'services/playthrough.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import { + AggregatedStats, + StatsReportingBackendApiService, +} from 'domain/exploration/stats-reporting-backend-api.service'; +import {Stopwatch} from 'domain/utilities/stopwatch.model'; +import {ServicesConstants} from 'services/services.constants'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class StatsReportingService { constructor( - private contextService: ContextService, - private messengerService: MessengerService, - private playthroughService: PlaythroughService, - private siteAnalyticsService: SiteAnalyticsService, - private statsReportingBackendApiService: StatsReportingBackendApiService, - private urlService: UrlService, - private ngZone: NgZone + private contextService: ContextService, + private messengerService: MessengerService, + private playthroughService: PlaythroughService, + private siteAnalyticsService: SiteAnalyticsService, + private statsReportingBackendApiService: StatsReportingBackendApiService, + private urlService: UrlService, + private ngZone: NgZone ) { - this.editorPreviewMode = ( - this.contextService.isInExplorationEditorPage()); - this.questionPlayerMode = ( - this.contextService.isInQuestionPlayerMode()); + this.editorPreviewMode = this.contextService.isInExplorationEditorPage(); + this.questionPlayerMode = this.contextService.isInQuestionPlayerMode(); this.refreshAggregatedStats(); } @@ -81,14 +81,12 @@ export class StatsReportingService { num_starts: 0, num_completions: 0, num_actual_starts: 0, - state_stats_mapping: {} + state_stats_mapping: {}, }; } private createDefaultStateStatsMappingIfMissing(stateName: string): void { - if ( - this.aggregatedStats.state_stats_mapping.hasOwnProperty( - stateName)) { + if (this.aggregatedStats.state_stats_mapping.hasOwnProperty(stateName)) { return; } this.aggregatedStats.state_stats_mapping[stateName] = { @@ -97,13 +95,12 @@ export class StatsReportingService { total_hit_count: 0, first_hit_count: 0, num_times_solution_viewed: 0, - num_completions: 0 + num_completions: 0, }; } private startStatsTimer(): void { - if (!this.editorPreviewMode && - !this.questionPlayerMode) { + if (!this.editorPreviewMode && !this.questionPlayerMode) { this.ngZone.runOutsideAngular(() => { setInterval(() => { this.ngZone.run(() => { @@ -122,16 +119,19 @@ export class StatsReportingService { return; } - this.statsReportingBackendApiService.postsStatsAsync( - this.aggregatedStats, - this.explorationVersion, - this.explorationId, - this.currentStateName, - this.nextExpId, - this.previousStateName, - this.nextStateName).then(() => { - // Required for the post operation to deliver data to backend. - }); + this.statsReportingBackendApiService + .postsStatsAsync( + this.aggregatedStats, + this.explorationVersion, + this.explorationId, + this.currentStateName, + this.nextExpId, + this.previousStateName, + this.nextStateName + ) + .then(() => { + // Required for the post operation to deliver data to backend. + }); this.refreshAggregatedStats(); } @@ -141,9 +141,12 @@ export class StatsReportingService { } initSession( - newExplorationId: string, newExplorationTitle: string, - newExplorationVersion: number, newSessionId: string, - collectionId: string): void { + newExplorationId: string, + newExplorationTitle: string, + newExplorationVersion: number, + newSessionId: string, + collectionId: string + ): void { this.explorationId = newExplorationId; this.explorationTitle = newExplorationTitle; this.explorationVersion = newExplorationVersion; @@ -164,50 +167,60 @@ export class StatsReportingService { this.aggregatedStats.num_starts += 1; this.createDefaultStateStatsMappingIfMissing(stateName); - this.aggregatedStats.state_stats_mapping[ - stateName].total_hit_count += 1; - this.aggregatedStats.state_stats_mapping[ - stateName].first_hit_count += 1; + this.aggregatedStats.state_stats_mapping[stateName].total_hit_count += 1; + this.aggregatedStats.state_stats_mapping[stateName].first_hit_count += 1; this.postStatsToBackend(); this.currentStateName = stateName; - this.statsReportingBackendApiService.recordExpStartedAsync( - params, this.sessionId, stateName, - this.explorationVersion, - this.explorationId, - this.currentStateName, - this.nextExpId, - this.previousStateName, - this.nextStateName).then(() => { - // Required for the post operation to deliver data to backend. - }); - - this.statsReportingBackendApiService.recordStateHitAsync( - 0.0, this.explorationVersion, stateName, - params, this.sessionId, - this.explorationId, - this.currentStateName, - this.nextExpId, - this.previousStateName, - this.nextStateName).then(() => { - // Required for the post operation to deliver data to backend. - }); + this.statsReportingBackendApiService + .recordExpStartedAsync( + params, + this.sessionId, + stateName, + this.explorationVersion, + this.explorationId, + this.currentStateName, + this.nextExpId, + this.previousStateName, + this.nextStateName + ) + .then(() => { + // Required for the post operation to deliver data to backend. + }); + + this.statsReportingBackendApiService + .recordStateHitAsync( + 0.0, + this.explorationVersion, + stateName, + params, + this.sessionId, + this.explorationId, + this.currentStateName, + this.nextExpId, + this.previousStateName, + this.nextStateName + ) + .then(() => { + // Required for the post operation to deliver data to backend. + }); this.messengerService.sendMessage( - ServicesConstants.MESSENGER_PAYLOAD.EXPLORATION_LOADED, { + ServicesConstants.MESSENGER_PAYLOAD.EXPLORATION_LOADED, + { explorationVersion: this.explorationVersion, - explorationTitle: this.explorationTitle - }); + explorationTitle: this.explorationTitle, + } + ); this.statesVisited.add(stateName); this.siteAnalyticsService.registerNewCard(1, this.explorationId); this.stateStopwatch.reset(); this.explorationStarted = true; - this.siteAnalyticsService.registerStartExploration( - this.explorationId); + this.siteAnalyticsService.registerStartExploration(this.explorationId); } recordExplorationActuallyStarted(stateName: string): void { @@ -217,16 +230,20 @@ export class StatsReportingService { this.aggregatedStats.num_actual_starts += 1; this.currentStateName = stateName; - this.statsReportingBackendApiService.recordExplorationActuallyStartedAsync( - this.explorationVersion, stateName, - this.sessionId, - this.explorationId, - this.currentStateName, - this.nextExpId, - this.previousStateName, - this.nextStateName).then(() => { - // Required for the post operation to deliver data to backend. - }); + this.statsReportingBackendApiService + .recordExplorationActuallyStartedAsync( + this.explorationVersion, + stateName, + this.sessionId, + this.explorationId, + this.currentStateName, + this.nextExpId, + this.previousStateName, + this.nextStateName + ) + .then(() => { + // Required for the post operation to deliver data to backend. + }); this.playthroughService.recordExplorationStartAction(stateName); this.explorationActuallyStarted = true; @@ -235,90 +252,107 @@ export class StatsReportingService { recordSolutionHit(stateName: string): void { this.createDefaultStateStatsMappingIfMissing(stateName); this.aggregatedStats.state_stats_mapping[ - stateName].num_times_solution_viewed += 1; + stateName + ].num_times_solution_viewed += 1; this.currentStateName = stateName; - this.statsReportingBackendApiService.recordSolutionHitAsync( - this.stateStopwatch.getTimeInSecs(), - this.explorationVersion, - stateName, - this.sessionId, - this.explorationId, - this.currentStateName, - this.nextExpId, - this.previousStateName, - this.nextStateName).then(() => { - // Required for the post operation to deliver data to backend. - }); + this.statsReportingBackendApiService + .recordSolutionHitAsync( + this.stateStopwatch.getTimeInSecs(), + this.explorationVersion, + stateName, + this.sessionId, + this.explorationId, + this.currentStateName, + this.nextExpId, + this.previousStateName, + this.nextStateName + ) + .then(() => { + // Required for the post operation to deliver data to backend. + }); } - recordLeaveForRefresherExp( - stateName: string, refresherExpId: string): void { + recordLeaveForRefresherExp(stateName: string, refresherExpId: string): void { this.currentStateName = stateName; this.nextExpId = refresherExpId; - this.statsReportingBackendApiService.recordLeaveForRefresherExpAsync( - this.explorationVersion, - refresherExpId, - stateName, - this.sessionId, - this.stateStopwatch.getTimeInSecs(), - this.explorationId, - this.currentStateName, - this.nextExpId, - this.previousStateName, - this.nextStateName).then(() => { - // Required for the post operation to deliver data to backend. - }); + this.statsReportingBackendApiService + .recordLeaveForRefresherExpAsync( + this.explorationVersion, + refresherExpId, + stateName, + this.sessionId, + this.stateStopwatch.getTimeInSecs(), + this.explorationId, + this.currentStateName, + this.nextExpId, + this.previousStateName, + this.nextStateName + ) + .then(() => { + // Required for the post operation to deliver data to backend. + }); } // Note that this also resets the stateStopwatch. // The type of oldParams is declared as Object since it can vary depending // on the oldStateName. recordStateTransition( - oldStateName: string, newStateName: string, answer: string, - oldParams: Object, isFirstHit: boolean, - chapterNumber: string, cardCount: string, language: string): void { + oldStateName: string, + newStateName: string, + answer: string, + oldParams: Object, + isFirstHit: boolean, + chapterNumber: string, + cardCount: string, + language: string + ): void { this.createDefaultStateStatsMappingIfMissing(newStateName); - this.aggregatedStats.state_stats_mapping[ - newStateName].total_hit_count += 1; + this.aggregatedStats.state_stats_mapping[newStateName].total_hit_count += 1; if (isFirstHit) { - this.aggregatedStats.state_stats_mapping[ - newStateName].first_hit_count += 1; + this.aggregatedStats.state_stats_mapping[newStateName].first_hit_count += + 1; } this.previousStateName = oldStateName; this.nextStateName = newStateName; - this.statsReportingBackendApiService.recordStateHitAsync( - this.stateStopwatch.getTimeInSecs(), - this.explorationVersion, - newStateName, - oldParams, - this.sessionId, - this.explorationId, - this.currentStateName, - this.nextExpId, - this.previousStateName, - this.nextStateName).then(() => { - // Required for the post operation to deliver data to backend. - }); + this.statsReportingBackendApiService + .recordStateHitAsync( + this.stateStopwatch.getTimeInSecs(), + this.explorationVersion, + newStateName, + oldParams, + this.sessionId, + this.explorationId, + this.currentStateName, + this.nextExpId, + this.previousStateName, + this.nextStateName + ) + .then(() => { + // Required for the post operation to deliver data to backend. + }); // Broadcast information about the state transition to listeners. this.messengerService.sendMessage( - ServicesConstants.MESSENGER_PAYLOAD.STATE_TRANSITION, { + ServicesConstants.MESSENGER_PAYLOAD.STATE_TRANSITION, + { explorationVersion: this.explorationVersion, jsonAnswer: JSON.stringify(answer), newStateName: newStateName, oldStateName: oldStateName, - paramValues: oldParams - }); + paramValues: oldParams, + } + ); if (!this.statesVisited.has(newStateName)) { this.statesVisited.add(newStateName); this.siteAnalyticsService.registerNewCard( this.statesVisited.size, - this.explorationId); + this.explorationId + ); } let numberOfStatesVisited = this.statesVisited.size; if (numberOfStatesVisited === this.MINIMUM_NUMBER_OF_VISITED_STATES) { @@ -351,60 +385,66 @@ export class StatsReportingService { recordStateCompleted(stateName: string): void { this.createDefaultStateStatsMappingIfMissing(stateName); - this.aggregatedStats.state_stats_mapping[ - stateName].num_completions += 1; + this.aggregatedStats.state_stats_mapping[stateName].num_completions += 1; this.currentStateName = stateName; - this.statsReportingBackendApiService.recordStateCompletedAsync( - this.explorationVersion, - this.sessionId, - stateName, - this.stateStopwatch.getTimeInSecs(), - this.explorationId, - this.currentStateName, - this.nextExpId, - this.previousStateName, - this.nextStateName).then(() => { - // Required for the post operation to deliver data to backend. - }); + this.statsReportingBackendApiService + .recordStateCompletedAsync( + this.explorationVersion, + this.sessionId, + stateName, + this.stateStopwatch.getTimeInSecs(), + this.explorationId, + this.currentStateName, + this.nextExpId, + this.previousStateName, + this.nextStateName + ) + .then(() => { + // Required for the post operation to deliver data to backend. + }); } // The type of params is declared as Object since it can vary depending // on the stateName. recordExplorationCompleted( - stateName: string, - params: Object, - chapterNumber: string, - cardCount: string, - language: string + stateName: string, + params: Object, + chapterNumber: string, + cardCount: string, + language: string ): void { this.aggregatedStats.num_completions += 1; this.currentStateName = stateName; - this.statsReportingBackendApiService.recordExplorationCompletedAsync( - this.stateStopwatch.getTimeInSecs(), - this.optionalCollectionId, - params, - this.sessionId, - stateName, - this.explorationVersion, - this.explorationId, - this.currentStateName, - this.nextExpId, - this.previousStateName, - this.nextStateName).then(() => { - // Required for the post operation to deliver data to backend. - }); + this.statsReportingBackendApiService + .recordExplorationCompletedAsync( + this.stateStopwatch.getTimeInSecs(), + this.optionalCollectionId, + params, + this.sessionId, + stateName, + this.explorationVersion, + this.explorationId, + this.currentStateName, + this.nextExpId, + this.previousStateName, + this.nextStateName + ) + .then(() => { + // Required for the post operation to deliver data to backend. + }); this.messengerService.sendMessage( - ServicesConstants.MESSENGER_PAYLOAD.EXPLORATION_COMPLETED, { + ServicesConstants.MESSENGER_PAYLOAD.EXPLORATION_COMPLETED, + { explorationVersion: this.explorationVersion, - paramValues: params - }); + paramValues: params, + } + ); - this.siteAnalyticsService.registerFinishExploration( - this.explorationId); + this.siteAnalyticsService.registerFinishExploration(this.explorationId); let urlParams = this.urlService.getUrlParams(); if (urlParams.hasOwnProperty('classroom_url_fragment')) { this.siteAnalyticsService.registerCuratedLessonCompleted( @@ -424,7 +464,9 @@ export class StatsReportingService { this.postStatsToBackend(); this.playthroughService.recordExplorationQuitAction( - stateName, this.stateStopwatch.getTimeInSecs()); + stateName, + this.stateStopwatch.getTimeInSecs() + ); this.explorationIsComplete = true; } @@ -432,37 +474,51 @@ export class StatsReportingService { // The type of params is declared as Object since it can vary depending // on the stateName. recordAnswerSubmitted( - stateName: string, params: Object, answer: string, - explorationId: string, answerIsCorrect: boolean, - answerGroupIndex: number, ruleIndex: number, - classificationCategorization: string, feedbackIsUseful: boolean): void { + stateName: string, + params: Object, + answer: string, + explorationId: string, + answerIsCorrect: boolean, + answerGroupIndex: number, + ruleIndex: number, + classificationCategorization: string, + feedbackIsUseful: boolean + ): void { this.createDefaultStateStatsMappingIfMissing(stateName); - this.aggregatedStats.state_stats_mapping[ - stateName].total_answers_count += 1; + this.aggregatedStats.state_stats_mapping[stateName].total_answers_count += + 1; if (feedbackIsUseful) { this.aggregatedStats.state_stats_mapping[ - stateName].useful_feedback_count += 1; + stateName + ].useful_feedback_count += 1; } this.currentStateName = stateName; - this.statsReportingBackendApiService.recordAnswerSubmittedAsync( - answer, params, this.explorationVersion, - this.sessionId, - this.stateStopwatch.getTimeInSecs(), - stateName, - answerGroupIndex, - ruleIndex, - classificationCategorization, - this.explorationId, - this.currentStateName, - this.nextExpId, - this.previousStateName, - this.nextStateName).then(() => { - // Required for the post operation to deliver data to backend. - }); + this.statsReportingBackendApiService + .recordAnswerSubmittedAsync( + answer, + params, + this.explorationVersion, + this.sessionId, + this.stateStopwatch.getTimeInSecs(), + stateName, + answerGroupIndex, + ruleIndex, + classificationCategorization, + this.explorationId, + this.currentStateName, + this.nextExpId, + this.previousStateName, + this.nextStateName + ) + .then(() => { + // Required for the post operation to deliver data to backend. + }); this.siteAnalyticsService.registerAnswerSubmitted( - explorationId, answerIsCorrect); + explorationId, + answerIsCorrect + ); } // The type of params is declared as Object since it can vary depending @@ -470,35 +526,50 @@ export class StatsReportingService { recordMaybeLeaveEvent(stateName: string, params: Object): void { this.currentStateName = stateName; - this.statsReportingBackendApiService.recordMaybeLeaveEventAsync( - this.stateStopwatch.getTimeInSecs(), - this.optionalCollectionId, - params, - this.sessionId, - stateName, - this.explorationVersion, - this.explorationId, - this.currentStateName, - this.nextExpId, - this.previousStateName, - this.nextStateName).then(() => { - // Required for the post operation to deliver data to backend. - }); + this.statsReportingBackendApiService + .recordMaybeLeaveEventAsync( + this.stateStopwatch.getTimeInSecs(), + this.optionalCollectionId, + params, + this.sessionId, + stateName, + this.explorationVersion, + this.explorationId, + this.currentStateName, + this.nextExpId, + this.previousStateName, + this.nextStateName + ) + .then(() => { + // Required for the post operation to deliver data to backend. + }); this.postStatsToBackend(); this.playthroughService.recordExplorationQuitAction( - stateName, this.stateStopwatch.getTimeInSecs()); + stateName, + this.stateStopwatch.getTimeInSecs() + ); this.playthroughService.storePlaythrough(); } recordAnswerSubmitAction( - stateName: string, destStateName: string, - interactionId: string, answer: string, feedback: string): void { + stateName: string, + destStateName: string, + interactionId: string, + answer: string, + feedback: string + ): void { this.playthroughService.recordAnswerSubmitAction( - stateName, destStateName, interactionId, answer, feedback, - this.stateStopwatch.getTimeInSecs()); + stateName, + destStateName, + interactionId, + answer, + feedback, + this.stateStopwatch.getTimeInSecs() + ); } } -angular.module('oppia').factory('StatsReportingService', - downgradeInjectable(StatsReportingService)); +angular + .module('oppia') + .factory('StatsReportingService', downgradeInjectable(StatsReportingService)); diff --git a/core/templates/pages/exploration-player-page/switch-content-language-refresh-required-modal.component.spec.ts b/core/templates/pages/exploration-player-page/switch-content-language-refresh-required-modal.component.spec.ts index a05e5583b0d4..b8034f3fc0de 100644 --- a/core/templates/pages/exploration-player-page/switch-content-language-refresh-required-modal.component.spec.ts +++ b/core/templates/pages/exploration-player-page/switch-content-language-refresh-required-modal.component.spec.ts @@ -17,14 +17,14 @@ * SwitchContentLanguageRefreshRequiredModalComponent. */ -import { async, ComponentFixture, TestBed } from - '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; -import { SwitchContentLanguageRefreshRequiredModalComponent } from +import { + SwitchContentLanguageRefreshRequiredModalComponent, // eslint-disable-next-line max-len - 'pages/exploration-player-page/switch-content-language-refresh-required-modal.component'; -import { WindowRef } from 'services/contextual/window-ref.service'; +} from 'pages/exploration-player-page/switch-content-language-refresh-required-modal.component'; +import {WindowRef} from 'services/contextual/window-ref.service'; class MockActiveModal { dismiss(): void { @@ -37,7 +37,7 @@ class MockActiveModal { class MockWindowRef { _window = { location: { - href: 'host.name:1234/explore/0' + href: 'host.name:1234/explore/0', }, }; @@ -46,10 +46,9 @@ class MockWindowRef { } } -describe('SwitchContentLanguageRefreshRequiredModalComponent', function() { +describe('SwitchContentLanguageRefreshRequiredModalComponent', function () { let component: SwitchContentLanguageRefreshRequiredModalComponent; - let fixture: ComponentFixture< - SwitchContentLanguageRefreshRequiredModalComponent>; + let fixture: ComponentFixture; let ngbActiveModal: NgbActiveModal; let windowRef: MockWindowRef; @@ -58,15 +57,16 @@ describe('SwitchContentLanguageRefreshRequiredModalComponent', function() { TestBed.configureTestingModule({ declarations: [SwitchContentLanguageRefreshRequiredModalComponent], providers: [ - { provide: NgbActiveModal, useClass: MockActiveModal }, - { provide: WindowRef, useValue: windowRef } - ] + {provide: NgbActiveModal, useClass: MockActiveModal}, + {provide: WindowRef, useValue: windowRef}, + ], }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent( - SwitchContentLanguageRefreshRequiredModalComponent); + SwitchContentLanguageRefreshRequiredModalComponent + ); component = fixture.componentInstance; ngbActiveModal = TestBed.get(NgbActiveModal); windowRef = TestBed.get(WindowRef); @@ -78,19 +78,21 @@ describe('SwitchContentLanguageRefreshRequiredModalComponent', function() { expect(dismissSpy).toHaveBeenCalled(); }); - it('should set the href with the correct URL parameters on confirm', - () => { - expect(windowRef.nativeWindow.location.href).toBe( - 'host.name:1234/explore/0'); + it('should set the href with the correct URL parameters on confirm', () => { + expect(windowRef.nativeWindow.location.href).toBe( + 'host.name:1234/explore/0' + ); - component.languageCode = 'fr'; - component.confirm(); - expect(windowRef.nativeWindow.location.href).toBe( - 'host.name:1234/explore/0?initialContentLanguageCode=fr'); + component.languageCode = 'fr'; + component.confirm(); + expect(windowRef.nativeWindow.location.href).toBe( + 'host.name:1234/explore/0?initialContentLanguageCode=fr' + ); - component.languageCode = 'en'; - component.confirm(); - expect(windowRef.nativeWindow.location.href).toBe( - 'host.name:1234/explore/0?initialContentLanguageCode=en'); - }); + component.languageCode = 'en'; + component.confirm(); + expect(windowRef.nativeWindow.location.href).toBe( + 'host.name:1234/explore/0?initialContentLanguageCode=en' + ); + }); }); diff --git a/core/templates/pages/exploration-player-page/switch-content-language-refresh-required-modal.component.ts b/core/templates/pages/exploration-player-page/switch-content-language-refresh-required-modal.component.ts index a9229c54a432..57d8bbd5e642 100644 --- a/core/templates/pages/exploration-player-page/switch-content-language-refresh-required-modal.component.ts +++ b/core/templates/pages/exploration-player-page/switch-content-language-refresh-required-modal.component.ts @@ -17,20 +17,20 @@ * refresh the exploration when changing languages. */ -import { Component, Input } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import {Component, Input} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; -export const INITIAL_CONTENT_LANGUAGE_CODE_URL_PARAM = ( - 'initialContentLanguageCode'); +export const INITIAL_CONTENT_LANGUAGE_CODE_URL_PARAM = + 'initialContentLanguageCode'; @Component({ selector: 'switch-content-language-refresh-required-modal', templateUrl: './switch-content-language-refresh-required-modal.component.html', - styleUrls: [] + styleUrls: [], }) export class SwitchContentLanguageRefreshRequiredModalComponent { @Input() languageCode!: string; @@ -47,12 +47,16 @@ export class SwitchContentLanguageRefreshRequiredModalComponent { confirm(): void { const url = new URL(this.windowRef.nativeWindow.location.href); url.searchParams.set( - INITIAL_CONTENT_LANGUAGE_CODE_URL_PARAM, this.languageCode); + INITIAL_CONTENT_LANGUAGE_CODE_URL_PARAM, + this.languageCode + ); this.windowRef.nativeWindow.location.href = url.href; } } angular.module('oppia').factory( 'SwitchContentLanguageRefreshRequiredModalComponent', - downgradeComponent( - {component: SwitchContentLanguageRefreshRequiredModalComponent})); + downgradeComponent({ + component: SwitchContentLanguageRefreshRequiredModalComponent, + }) +); diff --git a/core/templates/pages/exploration-player-page/templates/lesson-information-card-modal.component.spec.ts b/core/templates/pages/exploration-player-page/templates/lesson-information-card-modal.component.spec.ts index 9c9b2bdf1431..a3636a11fb0b 100644 --- a/core/templates/pages/exploration-player-page/templates/lesson-information-card-modal.component.spec.ts +++ b/core/templates/pages/exploration-player-page/templates/lesson-information-card-modal.component.spec.ts @@ -16,29 +16,33 @@ * @fileoverview Unit tests for lesson information card modal component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA, Pipe, PipeTransform } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync, fakeAsync, tick } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { TranslateService } from '@ngx-translate/core'; -import { MockTranslateService } from 'components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec'; -import { ExplorationRatings } from 'domain/summary/learner-exploration-summary.model'; -import { UrlService } from 'services/contextual/url.service'; -import { UserService } from 'services/user.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { ExplorationEngineService } from '../services/exploration-engine.service'; -import { PlayerTranscriptService } from '../services/player-transcript.service'; -import { LessonInformationCardModalComponent } from './lesson-information-card-modal.component'; -import { LocalStorageService } from 'services/local-storage.service'; -import { DateTimeFormatService } from 'services/date-time-format.service'; -import { ExplorationPlayerStateService } from 'pages/exploration-player-page/services/exploration-player-state.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { RatingComputationService } from 'components/ratings/rating-computation/rating-computation.service'; -import { CheckpointCelebrationUtilityService } from 'pages/exploration-player-page/services/checkpoint-celebration-utility.service'; - - +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA, Pipe, PipeTransform} from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, + fakeAsync, + tick, +} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {TranslateService} from '@ngx-translate/core'; +import {MockTranslateService} from 'components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec'; +import {ExplorationRatings} from 'domain/summary/learner-exploration-summary.model'; +import {UrlService} from 'services/contextual/url.service'; +import {UserService} from 'services/user.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {ExplorationEngineService} from '../services/exploration-engine.service'; +import {PlayerTranscriptService} from '../services/player-transcript.service'; +import {LessonInformationCardModalComponent} from './lesson-information-card-modal.component'; +import {LocalStorageService} from 'services/local-storage.service'; +import {DateTimeFormatService} from 'services/date-time-format.service'; +import {ExplorationPlayerStateService} from 'pages/exploration-player-page/services/exploration-player-state.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {RatingComputationService} from 'components/ratings/rating-computation/rating-computation.service'; +import {CheckpointCelebrationUtilityService} from 'pages/exploration-player-page/services/checkpoint-celebration-utility.service'; @Pipe({name: 'truncateAndCapitalize'}) class MockTruncteAndCapitalizePipe { @@ -65,10 +69,12 @@ class MockCheckpointCelebrationUtilityService { private isOnCheckpointedState: boolean = false; getCheckpointMessage( - completedCheckpointCount: number, totalCheckpointCount: number + completedCheckpointCount: number, + totalCheckpointCount: number ): string { return ( - 'checkpoint ' + completedCheckpointCount + '/' + totalCheckpointCount); + 'checkpoint ' + completedCheckpointCount + '/' + totalCheckpointCount + ); } getIsOnCheckpointedState(): boolean { @@ -84,23 +90,23 @@ class MockWindowRef { reload: () => {}, toString: () => { return 'http://localhost:8181/?lang=es'; - } + }, }, localStorage: { last_uploaded_audio_lang: 'en', - removeItem: (name: string) => {} + removeItem: (name: string) => {}, }, gtag: () => {}, history: { - pushState(data: object, title: string, url?: string | null) {} + pushState(data: object, title: string, url?: string | null) {}, }, document: { body: { style: { overflowY: 'auto', - } - } - } + }, + }, + }, }; } @@ -116,8 +122,7 @@ describe('Lesson Information card modal component', () => { let userService: UserService; let explorationPlayerStateService: ExplorationPlayerStateService; let localStorageService: LocalStorageService; - let checkpointCelebrationUtilityService: - CheckpointCelebrationUtilityService; + let checkpointCelebrationUtilityService: CheckpointCelebrationUtilityService; let expId = 'expId'; let expTitle = 'Exploration Title'; @@ -128,15 +133,13 @@ describe('Lesson Information card modal component', () => { beforeEach(waitForAsync(() => { mockWindowRef = new MockWindowRef(); TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], + imports: [HttpClientTestingModule], declarations: [ LessonInformationCardModalComponent, MockTranslatePipe, MockTruncteAndCapitalizePipe, MockSummarizeNonnegativeNumberPipe, - MockLimitToPipe + MockLimitToPipe, ], providers: [ NgbActiveModal, @@ -147,18 +150,18 @@ describe('Lesson Information card modal component', () => { UrlInterpolationService, { provide: CheckpointCelebrationUtilityService, - useClass: MockCheckpointCelebrationUtilityService + useClass: MockCheckpointCelebrationUtilityService, }, { provide: WindowRef, - useValue: mockWindowRef + useValue: mockWindowRef, }, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -176,11 +179,11 @@ describe('Lesson Information card modal component', () => { created_on_msec: 2, human_readable_contributors_summary: { 'contributer 1': { - num_commits: 2 + num_commits: 2, }, 'contributer 2': { - num_commits: 2 - } + num_commits: 2, + }, }, language_code: '', num_views: 100, @@ -189,7 +192,7 @@ describe('Lesson Information card modal component', () => { tags: ['tag1', 'tag2'], thumbnail_bg_color: '#fff', thumbnail_icon_url: 'icon_url', - title: expTitle + title: expTitle, }; i18nLanguageCodeService = TestBed.inject(I18nLanguageCodeService); @@ -200,14 +203,20 @@ describe('Lesson Information card modal component', () => { userService = TestBed.inject(UserService); localStorageService = TestBed.inject(LocalStorageService); explorationPlayerStateService = TestBed.inject( - ExplorationPlayerStateService); + ExplorationPlayerStateService + ); checkpointCelebrationUtilityService = TestBed.inject( - CheckpointCelebrationUtilityService); - - spyOn(i18nLanguageCodeService, 'isHackyTranslationAvailable') - .and.returnValues(true, false); - spyOn(i18nLanguageCodeService, 'isCurrentLanguageEnglish') - .and.returnValues(false, true); + CheckpointCelebrationUtilityService + ); + + spyOn( + i18nLanguageCodeService, + 'isHackyTranslationAvailable' + ).and.returnValues(true, false); + spyOn(i18nLanguageCodeService, 'isCurrentLanguageEnglish').and.returnValues( + false, + true + ); }); it('should initialize the component', () => { @@ -227,40 +236,42 @@ describe('Lesson Information card modal component', () => { expect(componentInstance.explorationIsPrivate).toBe(true); expect(componentInstance.explorationTags).toEqual({ tagsToShow: ['tag1', 'tag2'], - tagsInTooltip: [] + tagsInTooltip: [], }); expect(componentInstance.infoCardBackgroundCss).toEqual({ - 'background-color': '#fff' + 'background-color': '#fff', }); expect(componentInstance.infoCardBackgroundImageUrl).toEqual('icon_url'); expect(componentInstance.expTitleTranslationKey).toEqual( - 'I18N_EXPLORATION_expId_TITLE'); + 'I18N_EXPLORATION_expId_TITLE' + ); expect(componentInstance.expDescTranslationKey).toEqual( - 'I18N_EXPLORATION_expId_DESCRIPTION'); + 'I18N_EXPLORATION_expId_DESCRIPTION' + ); // Translation is only displayed if the language is not English // and it's hacky translation is available. - let hackyExpTitleTranslationIsDisplayed = ( - componentInstance.isHackyExpTitleTranslationDisplayed()); + let hackyExpTitleTranslationIsDisplayed = + componentInstance.isHackyExpTitleTranslationDisplayed(); expect(hackyExpTitleTranslationIsDisplayed).toBe(true); - let hackyExpDescTranslationIsDisplayed = ( - componentInstance.isHackyExpDescTranslationDisplayed()); + let hackyExpDescTranslationIsDisplayed = + componentInstance.isHackyExpDescTranslationDisplayed(); expect(hackyExpDescTranslationIsDisplayed).toBe(false); }); - it('should determine if exploration isn\'t private upon initialization', - () => { - componentInstance.expInfo.status = 'public'; + it("should determine if exploration isn't private upon initialization", () => { + componentInstance.expInfo.status = 'public'; - componentInstance.ngOnInit(); + componentInstance.ngOnInit(); - expect(componentInstance.explorationIsPrivate).toBe(false); - }); + expect(componentInstance.explorationIsPrivate).toBe(false); + }); it('should throw error if unique url id is null', fakeAsync(() => { spyOn(userService, 'getLoginUrlAsync').and.returnValue( - Promise.resolve('https://oppia.org/login')); + Promise.resolve('https://oppia.org/login') + ); componentInstance.loggedOutProgressUniqueUrlId = null; expect(() => { componentInstance.onLoginButtonClicked(); @@ -274,24 +285,29 @@ describe('Lesson Information card modal component', () => { componentInstance.ngOnInit(); - expect(componentInstance.checkpointStatusArray).toEqual( - ['completed', 'in-progress', 'incomplete']); + expect(componentInstance.checkpointStatusArray).toEqual([ + 'completed', + 'in-progress', + 'incomplete', + ]); componentInstance.checkpointCount = 1; componentInstance.completedCheckpointsCount = 0; componentInstance.ngOnInit(); - expect(componentInstance.checkpointStatusArray).toEqual( - ['in-progress']); + expect(componentInstance.checkpointStatusArray).toEqual(['in-progress']); componentInstance.checkpointCount = 3; componentInstance.completedCheckpointsCount = 3; componentInstance.ngOnInit(); - expect(componentInstance.checkpointStatusArray).toEqual( - ['completed', 'completed', 'completed']); + expect(componentInstance.checkpointStatusArray).toEqual([ + 'completed', + 'completed', + 'completed', + ]); }); it('should get completed progress-bar width', () => { @@ -309,63 +325,89 @@ describe('Lesson Information card modal component', () => { expect(componentInstance.getCompletedProgressBarWidth()).toEqual(75); }); - it('should correctly set logged-out progress learner URL ' + - 'when unique progress URL ID exists', fakeAsync (() => { - spyOn(explorationPlayerStateService, 'isInStoryChapterMode') - .and.returnValue(true); - spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue(''); - spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl') - .and.returnValue(''); - spyOn(urlService, 'getOrigin').and.returnValue('https://oppia.org'); - spyOn(urlService, 'getStoryUrlFragmentFromLearnerUrl').and.returnValue(''); - spyOn(explorationPlayerStateService, 'getUniqueProgressUrlId') - .and.returnValue('abcdef'); + it( + 'should correctly set logged-out progress learner URL ' + + 'when unique progress URL ID exists', + fakeAsync(() => { + spyOn( + explorationPlayerStateService, + 'isInStoryChapterMode' + ).and.returnValue(true); + spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue( + '' + ); + spyOn( + urlService, + 'getClassroomUrlFragmentFromLearnerUrl' + ).and.returnValue(''); + spyOn(urlService, 'getOrigin').and.returnValue('https://oppia.org'); + spyOn(urlService, 'getStoryUrlFragmentFromLearnerUrl').and.returnValue( + '' + ); + spyOn( + explorationPlayerStateService, + 'getUniqueProgressUrlId' + ).and.returnValue('abcdef'); - componentInstance.ngOnInit(); + componentInstance.ngOnInit(); - expect(componentInstance.loggedOutProgressUniqueUrl).toEqual( - 'https://oppia.org/progress/abcdef'); - })); + expect(componentInstance.loggedOutProgressUniqueUrl).toEqual( + 'https://oppia.org/progress/abcdef' + ); + }) + ); it('should fetch checkpoint message if on checkpointed state', () => { const getIsOnCheckpointedState = spyOn( - checkpointCelebrationUtilityService, 'getIsOnCheckpointedState').and - .returnValue(false); - spyOn(checkpointCelebrationUtilityService, 'getCheckpointMessage').and - .returnValue('checkpoint message'); + checkpointCelebrationUtilityService, + 'getIsOnCheckpointedState' + ).and.returnValue(false); + spyOn( + checkpointCelebrationUtilityService, + 'getCheckpointMessage' + ).and.returnValue('checkpoint message'); componentInstance.ngOnInit(); expect(getIsOnCheckpointedState).toHaveBeenCalled(); - expect(checkpointCelebrationUtilityService.getCheckpointMessage) - .not.toHaveBeenCalled(); - expect(componentInstance.translatedCongratulatoryCheckpointMessage) - .toBeUndefined(); + expect( + checkpointCelebrationUtilityService.getCheckpointMessage + ).not.toHaveBeenCalled(); + expect( + componentInstance.translatedCongratulatoryCheckpointMessage + ).toBeUndefined(); getIsOnCheckpointedState.and.returnValue(true); componentInstance.ngOnInit(); - expect(checkpointCelebrationUtilityService.getCheckpointMessage) - .toHaveBeenCalled(); - expect(componentInstance.translatedCongratulatoryCheckpointMessage) - .toEqual('checkpoint message'); + expect( + checkpointCelebrationUtilityService.getCheckpointMessage + ).toHaveBeenCalled(); + expect(componentInstance.translatedCongratulatoryCheckpointMessage).toEqual( + 'checkpoint message' + ); }); it('should get image url correctly', () => { let imageUrl = 'image_url'; - spyOn(urlInterpolationService, 'getStaticImageUrl') - .and.returnValue('interpolated_url'); + spyOn(urlInterpolationService, 'getStaticImageUrl').and.returnValue( + 'interpolated_url' + ); - expect(componentInstance.getStaticImageUrl(imageUrl)) - .toEqual('interpolated_url'); + expect(componentInstance.getStaticImageUrl(imageUrl)).toEqual( + 'interpolated_url' + ); expect(urlInterpolationService.getStaticImageUrl).toHaveBeenCalledWith( - imageUrl); + imageUrl + ); }); it('should determine if current language is RTL', () => { const isLanguageRTLSpy = spyOn( - i18nLanguageCodeService, 'isCurrentLanguageRTL').and.returnValue(true); + i18nLanguageCodeService, + 'isCurrentLanguageRTL' + ).and.returnValue(true); expect(componentInstance.isLanguageRTL()).toBe(true); @@ -377,63 +419,74 @@ describe('Lesson Information card modal component', () => { it('should get exploration tags summary', () => { let arrayOfTags = ['tag1', 'tag2']; - expect(componentInstance.getExplorationTagsSummary(['tag1', 'tag2'])) - .toEqual({ - tagsToShow: arrayOfTags, - tagsInTooltip: [] - }); + expect( + componentInstance.getExplorationTagsSummary(['tag1', 'tag2']) + ).toEqual({ + tagsToShow: arrayOfTags, + tagsInTooltip: [], + }); arrayOfTags = [ - 'this is a long tag.', 'this is also a long tag', - 'this takes the tags length past 45 characters']; + 'this is a long tag.', + 'this is also a long tag', + 'this takes the tags length past 45 characters', + ]; expect(componentInstance.getExplorationTagsSummary(arrayOfTags)).toEqual({ tagsToShow: [arrayOfTags[0], arrayOfTags[1]], - tagsInTooltip: [arrayOfTags[2]] + tagsInTooltip: [arrayOfTags[2]], }); }); it('should get updated string', () => { let dateTimeString = 'datetime_string'; - spyOn(dateTimeFormatService, 'getLocaleAbbreviatedDatetimeString') - .and.returnValue(dateTimeString); + spyOn( + dateTimeFormatService, + 'getLocaleAbbreviatedDatetimeString' + ).and.returnValue(dateTimeString); expect(componentInstance.getLastUpdatedString(12)).toEqual(dateTimeString); }); it('should save logged-out learner progress correctly', fakeAsync(() => { - spyOn(explorationPlayerStateService, 'setUniqueProgressUrlId') - .and.returnValue(Promise.resolve()); - spyOn(explorationPlayerStateService, 'getUniqueProgressUrlId') - .and.returnValue('abcdef'); + spyOn( + explorationPlayerStateService, + 'setUniqueProgressUrlId' + ).and.returnValue(Promise.resolve()); + spyOn( + explorationPlayerStateService, + 'getUniqueProgressUrlId' + ).and.returnValue('abcdef'); spyOn(urlService, 'getOrigin').and.returnValue('https://oppia.org'); componentInstance.saveLoggedOutProgress(); tick(100); expect(componentInstance.loggedOutProgressUniqueUrl).toEqual( - 'https://oppia.org/progress/abcdef'); + 'https://oppia.org/progress/abcdef' + ); expect(componentInstance.loggedOutProgressUniqueUrlId).toEqual('abcdef'); })); - it('should store unique progress URL ID when login button is clicked', - fakeAsync(() => { - spyOn(userService, 'getLoginUrlAsync').and.returnValue( - Promise.resolve('https://oppia.org/login')); - spyOn(localStorageService, 'updateUniqueProgressIdOfLoggedOutLearner'); - componentInstance.loggedOutProgressUniqueUrlId = 'abcdef'; + it('should store unique progress URL ID when login button is clicked', fakeAsync(() => { + spyOn(userService, 'getLoginUrlAsync').and.returnValue( + Promise.resolve('https://oppia.org/login') + ); + spyOn(localStorageService, 'updateUniqueProgressIdOfLoggedOutLearner'); + componentInstance.loggedOutProgressUniqueUrlId = 'abcdef'; - expect(mockWindowRef.nativeWindow.location.href).toEqual(''); + expect(mockWindowRef.nativeWindow.location.href).toEqual(''); - componentInstance.onLoginButtonClicked(); - tick(100); + componentInstance.onLoginButtonClicked(); + tick(100); - expect(localStorageService.updateUniqueProgressIdOfLoggedOutLearner) - .toHaveBeenCalledWith('abcdef'); - expect(mockWindowRef.nativeWindow.location.href).toEqual( - 'https://oppia.org/login'); - }) - ); + expect( + localStorageService.updateUniqueProgressIdOfLoggedOutLearner + ).toHaveBeenCalledWith('abcdef'); + expect(mockWindowRef.nativeWindow.location.href).toEqual( + 'https://oppia.org/login' + ); + })); it('should correctly close save progress menu', () => { componentInstance.saveProgressMenuIsShown = true; diff --git a/core/templates/pages/exploration-player-page/templates/lesson-information-card-modal.component.ts b/core/templates/pages/exploration-player-page/templates/lesson-information-card-modal.component.ts index 3d02e9dd336f..3e971b1b0d5e 100644 --- a/core/templates/pages/exploration-player-page/templates/lesson-information-card-modal.component.ts +++ b/core/templates/pages/exploration-player-page/templates/lesson-information-card-modal.component.ts @@ -16,24 +16,25 @@ * @fileoverview Component for lesson information card modal. */ -import { Clipboard } from '@angular/cdk/clipboard'; -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { StateCard } from 'domain/state_card/state-card.model'; -import { LearnerExplorationSummaryBackendDict } from - 'domain/summary/learner-exploration-summary.model'; -import { UrlService } from 'services/contextual/url.service'; -import { UserService } from 'services/user.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { LocalStorageService } from 'services/local-storage.service'; -import { I18nLanguageCodeService, TranslationKeyType } from - 'services/i18n-language-code.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { RatingComputationService } from 'components/ratings/rating-computation/rating-computation.service'; -import { DateTimeFormatService } from 'services/date-time-format.service'; -import { ExplorationPlayerStateService } from 'pages/exploration-player-page/services/exploration-player-state.service'; -import { CheckpointCelebrationUtilityService } from 'pages/exploration-player-page/services/checkpoint-celebration-utility.service'; +import {Clipboard} from '@angular/cdk/clipboard'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {StateCard} from 'domain/state_card/state-card.model'; +import {LearnerExplorationSummaryBackendDict} from 'domain/summary/learner-exploration-summary.model'; +import {UrlService} from 'services/contextual/url.service'; +import {UserService} from 'services/user.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {LocalStorageService} from 'services/local-storage.service'; +import { + I18nLanguageCodeService, + TranslationKeyType, +} from 'services/i18n-language-code.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {RatingComputationService} from 'components/ratings/rating-computation/rating-computation.service'; +import {DateTimeFormatService} from 'services/date-time-format.service'; +import {ExplorationPlayerStateService} from 'pages/exploration-player-page/services/exploration-player-state.service'; +import {CheckpointCelebrationUtilityService} from 'pages/exploration-player-page/services/checkpoint-celebration-utility.service'; interface ExplorationTagSummary { tagsToShow: string[]; @@ -47,12 +48,11 @@ const EXPLORATION_STATUS_PRIVATE = 'private'; import './lesson-information-card-modal.component.css'; - - @Component({ - selector: 'oppia-lesson-information-card-modal', - templateUrl: './lesson-information-card-modal.component.html', - styleUrls: ['./lesson-information-card-modal.component.css'] - }) +@Component({ + selector: 'oppia-lesson-information-card-modal', + templateUrl: './lesson-information-card-modal.component.html', + styleUrls: ['./lesson-information-card-modal.component.css'], +}) export class LessonInformationCardModalComponent extends ConfirmOrCancelModal { // These properties below are initialized using Angular lifecycle hooks // where we need to do non-null assertion. For more information see @@ -99,40 +99,41 @@ export class LessonInformationCardModalComponent extends ConfirmOrCancelModal { private windowRef: WindowRef, private localStorageService: LocalStorageService, private explorationPlayerStateService: ExplorationPlayerStateService, - private checkpointCelebrationUtilityService: - CheckpointCelebrationUtilityService, + private checkpointCelebrationUtilityService: CheckpointCelebrationUtilityService ) { super(ngbActiveModal); } ngOnInit(): void { this.averageRating = this.ratingComputationService.computeAverageRating( - this.expInfo.ratings); + this.expInfo.ratings + ); this.numViews = this.expInfo.num_views; this.lastUpdatedString = this.getLastUpdatedString( - this.expInfo.last_updated_msec); - this.explorationIsPrivate = ( - this.expInfo.status === EXPLORATION_STATUS_PRIVATE); + this.expInfo.last_updated_msec + ); + this.explorationIsPrivate = + this.expInfo.status === EXPLORATION_STATUS_PRIVATE; this.explorationTags = this.getExplorationTagsSummary(this.expInfo.tags); this.explorationId = this.expInfo.id; this.expTitle = this.expInfo.title; this.expCategory = this.expInfo.category; this.expDesc = this.expInfo.objective; this.infoCardBackgroundCss = { - 'background-color': this.expInfo.thumbnail_bg_color + 'background-color': this.expInfo.thumbnail_bg_color, }; this.infoCardBackgroundImageUrl = this.expInfo.thumbnail_icon_url; - this.expTitleTranslationKey = ( - this.i18nLanguageCodeService. - getExplorationTranslationKey( - this.explorationId, TranslationKeyType.TITLE) - ); - this.expDescTranslationKey = ( - this.i18nLanguageCodeService. - getExplorationTranslationKey( - this.explorationId, TranslationKeyType.DESCRIPTION) - ); + this.expTitleTranslationKey = + this.i18nLanguageCodeService.getExplorationTranslationKey( + this.explorationId, + TranslationKeyType.TITLE + ); + this.expDescTranslationKey = + this.i18nLanguageCodeService.getExplorationTranslationKey( + this.explorationId, + TranslationKeyType.DESCRIPTION + ); // This array is used to keep track of the status of each checkpoint, // i.e. whether it is completed, in-progress, or yet-to-be-completed by the @@ -145,8 +146,8 @@ export class LessonInformationCardModalComponent extends ConfirmOrCancelModal { // If not all checkpoints are completed, then the checkpoint immediately // following the last completed checkpoint is labeled 'in-progress'. if (this.checkpointCount > this.completedCheckpointsCount) { - this.checkpointStatusArray[this.completedCheckpointsCount] = ( - CHECKPOINT_STATUS_IN_PROGRESS); + this.checkpointStatusArray[this.completedCheckpointsCount] = + CHECKPOINT_STATUS_IN_PROGRESS; } for ( let i = this.completedCheckpointsCount + 1; @@ -155,17 +156,20 @@ export class LessonInformationCardModalComponent extends ConfirmOrCancelModal { ) { this.checkpointStatusArray[i] = CHECKPOINT_STATUS_INCOMPLETE; } - this.loggedOutProgressUniqueUrlId = ( - this.explorationPlayerStateService.getUniqueProgressUrlId()); + this.loggedOutProgressUniqueUrlId = + this.explorationPlayerStateService.getUniqueProgressUrlId(); if (this.loggedOutProgressUniqueUrlId) { - this.loggedOutProgressUniqueUrl = ( + this.loggedOutProgressUniqueUrl = this.urlService.getOrigin() + - '/progress/' + this.loggedOutProgressUniqueUrlId); + '/progress/' + + this.loggedOutProgressUniqueUrlId; } if (this.checkpointCelebrationUtilityService.getIsOnCheckpointedState()) { - this.translatedCongratulatoryCheckpointMessage = ( + this.translatedCongratulatoryCheckpointMessage = this.checkpointCelebrationUtilityService.getCheckpointMessage( - this.completedCheckpointsCount, this.checkpointCount)); + this.completedCheckpointsCount, + this.checkpointCount + ); } } @@ -175,8 +179,9 @@ export class LessonInformationCardModalComponent extends ConfirmOrCancelModal { } const spaceBetweenEachNode = 100 / (this.checkpointCount - 1); return ( - ((this.completedCheckpointsCount - 1) * spaceBetweenEachNode) + - (spaceBetweenEachNode / 2)); + (this.completedCheckpointsCount - 1) * spaceBetweenEachNode + + spaceBetweenEachNode / 2 + ); } getProgressPercentage(): string { @@ -209,13 +214,14 @@ export class LessonInformationCardModalComponent extends ConfirmOrCancelModal { return { tagsToShow: tagsToShow, - tagsInTooltip: tagsInTooltip + tagsInTooltip: tagsInTooltip, }; } getLastUpdatedString(millisSinceEpoch: number): string { - return this.dateTimeFormatService - .getLocaleAbbreviatedDatetimeString(millisSinceEpoch); + return this.dateTimeFormatService.getLocaleAbbreviatedDatetimeString( + millisSinceEpoch + ); } getStaticImageUrl(imageUrl: string): string { @@ -244,32 +250,30 @@ export class LessonInformationCardModalComponent extends ConfirmOrCancelModal { async saveLoggedOutProgress(): Promise { if (!this.loggedOutProgressUniqueUrlId) { - this.explorationPlayerStateService - .setUniqueProgressUrlId() - .then(() => { - this.loggedOutProgressUniqueUrlId = ( - this.explorationPlayerStateService.getUniqueProgressUrlId()); - this.loggedOutProgressUniqueUrl = ( - this.urlService.getOrigin() + - '/progress/' + this.loggedOutProgressUniqueUrlId); - }); + this.explorationPlayerStateService.setUniqueProgressUrlId().then(() => { + this.loggedOutProgressUniqueUrlId = + this.explorationPlayerStateService.getUniqueProgressUrlId(); + this.loggedOutProgressUniqueUrl = + this.urlService.getOrigin() + + '/progress/' + + this.loggedOutProgressUniqueUrlId; + }); } this.saveProgressMenuIsShown = true; } onLoginButtonClicked(): void { - this.userService.getLoginUrlAsync().then( - (loginUrl) => { - let urlId = this.loggedOutProgressUniqueUrlId; - if (urlId === null) { - throw new Error( - 'User should not be able to login if ' + - 'loggedOutProgressUniqueUrlId is not null.'); - } - this.localStorageService.updateUniqueProgressIdOfLoggedOutLearner( - urlId); - this.windowRef.nativeWindow.location.href = loginUrl; - }); + this.userService.getLoginUrlAsync().then(loginUrl => { + let urlId = this.loggedOutProgressUniqueUrlId; + if (urlId === null) { + throw new Error( + 'User should not be able to login if ' + + 'loggedOutProgressUniqueUrlId is not null.' + ); + } + this.localStorageService.updateUniqueProgressIdOfLoggedOutLearner(urlId); + this.windowRef.nativeWindow.location.href = loginUrl; + }); } closeSaveProgressMenu(): void { diff --git a/core/templates/pages/exploration-player-page/templates/progress-reminder-modal.component.spec.ts b/core/templates/pages/exploration-player-page/templates/progress-reminder-modal.component.spec.ts index fe1a6848a862..15ca912b2a8b 100644 --- a/core/templates/pages/exploration-player-page/templates/progress-reminder-modal.component.spec.ts +++ b/core/templates/pages/exploration-player-page/templates/progress-reminder-modal.component.spec.ts @@ -16,12 +16,12 @@ * @fileoverview Unit tests for the progress reminder modal component. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { ProgressReminderModalComponent } from './progress-reminder-modal.component'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {ProgressReminderModalComponent} from './progress-reminder-modal.component'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; describe('Progress reminder modal component', () => { let fixture: ComponentFixture; @@ -30,15 +30,9 @@ describe('Progress reminder modal component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - ProgressReminderModalComponent, - MockTranslatePipe - ], - providers: [ - NgbActiveModal, - I18nLanguageCodeService - ], - schemas: [NO_ERRORS_SCHEMA] + declarations: [ProgressReminderModalComponent, MockTranslatePipe], + providers: [NgbActiveModal, I18nLanguageCodeService], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -54,13 +48,19 @@ describe('Progress reminder modal component', () => { component.ngOnInit(); - expect(component.checkpointStatusArray).toEqual( - ['completed', 'completed', 'in-progress', 'incomplete']); + expect(component.checkpointStatusArray).toEqual([ + 'completed', + 'completed', + 'in-progress', + 'incomplete', + ]); }); it('should determine if language is RTL', () => { const i18nSpy = spyOn( - i18nLanguageCodeService, 'isLanguageRTL').and.returnValues(true); + i18nLanguageCodeService, + 'isLanguageRTL' + ).and.returnValues(true); expect(component.isLanguageRTL()).toBe(true); diff --git a/core/templates/pages/exploration-player-page/templates/progress-reminder-modal.component.ts b/core/templates/pages/exploration-player-page/templates/progress-reminder-modal.component.ts index 293ac8006a36..dab7cfdc8eab 100644 --- a/core/templates/pages/exploration-player-page/templates/progress-reminder-modal.component.ts +++ b/core/templates/pages/exploration-player-page/templates/progress-reminder-modal.component.ts @@ -16,10 +16,10 @@ * @fileoverview Component for the progress reminder modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; const CHECKPOINT_STATUS_INCOMPLETE = 'incomplete'; const CHECKPOINT_STATUS_COMPLETED = 'completed'; @@ -30,9 +30,8 @@ import './progress-reminder-modal.component.css'; @Component({ selector: 'oppia-progress-reminder-modal', templateUrl: './progress-reminder-modal.component.html', - styleUrls: ['./progress-reminder-modal.component.css'] + styleUrls: ['./progress-reminder-modal.component.css'], }) - export class ProgressReminderModalComponent extends ConfirmOrCancelModal { // These properties below are initialized using Angular lifecycle hooks, // and hence we need non-null assertion here. For more information see @@ -57,8 +56,8 @@ export class ProgressReminderModalComponent extends ConfirmOrCancelModal { // If not all checkpoints are completed, then the checkpoint immediately // following the last completed checkpoint is labeled 'in-progress'. if (this.checkpointCount > this.completedCheckpointsCount) { - this.checkpointStatusArray[this.completedCheckpointsCount] = ( - CHECKPOINT_STATUS_IN_PROGRESS); + this.checkpointStatusArray[this.completedCheckpointsCount] = + CHECKPOINT_STATUS_IN_PROGRESS; } for ( let i = this.completedCheckpointsCount + 1; @@ -79,8 +78,9 @@ export class ProgressReminderModalComponent extends ConfirmOrCancelModal { } const spaceBetweenEachNode = 100 / (this.checkpointCount - 1); return ( - ((this.completedCheckpointsCount - 1) * spaceBetweenEachNode) + - (spaceBetweenEachNode / 2)); + (this.completedCheckpointsCount - 1) * spaceBetweenEachNode + + spaceBetweenEachNode / 2 + ); } getProgressInFractionForm(): string { diff --git a/core/templates/pages/exploration-player-page/templates/take-break-modal.component.spec.ts b/core/templates/pages/exploration-player-page/templates/take-break-modal.component.spec.ts index f47709d838f8..6f53b0593527 100644 --- a/core/templates/pages/exploration-player-page/templates/take-break-modal.component.spec.ts +++ b/core/templates/pages/exploration-player-page/templates/take-break-modal.component.spec.ts @@ -16,13 +16,11 @@ * @fileoverview Unit tests for TakeBreakModalComponent. */ -import { async, ComponentFixture, TestBed } from - '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; -import { TakeBreakModalComponent } from - './take-break-modal.component'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; +import {TakeBreakModalComponent} from './take-break-modal.component'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; class MockActiveModal { dismiss(): void { @@ -34,7 +32,7 @@ class MockActiveModal { } } -describe('TakeBreakModalComponent', function() { +describe('TakeBreakModalComponent', function () { let component: TakeBreakModalComponent; let fixture: ComponentFixture; let ngbActiveModal: NgbActiveModal; @@ -45,9 +43,9 @@ describe('TakeBreakModalComponent', function() { providers: [ { provide: NgbActiveModal, - useClass: MockActiveModal - } - ] + useClass: MockActiveModal, + }, + ], }).compileComponents(); })); diff --git a/core/templates/pages/exploration-player-page/templates/take-break-modal.component.ts b/core/templates/pages/exploration-player-page/templates/take-break-modal.component.ts index 1390c2296083..8515796f5922 100644 --- a/core/templates/pages/exploration-player-page/templates/take-break-modal.component.ts +++ b/core/templates/pages/exploration-player-page/templates/take-break-modal.component.ts @@ -16,23 +16,19 @@ * @fileoverview Controller for Take a Break Modal. */ -import { Component } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; - +import {Component} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; @Component({ selector: 'take-break-modal', templateUrl: './take-break-modal.component.html', - styleUrls: [] + styleUrls: [], }) export class TakeBreakModalComponent { - constructor( - private activeModal: NgbActiveModal - ) {} + constructor(private activeModal: NgbActiveModal) {} - ngOnInit(): void { - } + ngOnInit(): void {} cancel(): void { this.activeModal.dismiss(); @@ -43,7 +39,9 @@ export class TakeBreakModalComponent { } } -angular.module('oppia').factory( - 'TakeBreakModalComponent', - downgradeComponent( - {component: TakeBreakModalComponent})); +angular + .module('oppia') + .factory( + 'TakeBreakModalComponent', + downgradeComponent({component: TakeBreakModalComponent}) + ); diff --git a/core/templates/pages/facilitator-dashboard-page/facilitator-dashboard-page.component.spec.ts b/core/templates/pages/facilitator-dashboard-page/facilitator-dashboard-page.component.spec.ts index 2f7712c8c9bb..4d6c59d45d0f 100644 --- a/core/templates/pages/facilitator-dashboard-page/facilitator-dashboard-page.component.spec.ts +++ b/core/templates/pages/facilitator-dashboard-page/facilitator-dashboard-page.component.spec.ts @@ -16,21 +16,22 @@ * @fileoverview Unit tests for facilitator dashboard page. */ -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick } from - '@angular/core/testing'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { LearnerGroupPagesConstants } from - 'pages/learner-group-pages/learner-group-pages.constants'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; -import { TranslateService } from '@ngx-translate/core'; -import { PageTitleService } from 'services/page-title.service'; -import { FacilitatorDashboardPageComponent } from - './facilitator-dashboard-page.component'; -import { FacilitatorDashboardBackendApiService } from 'domain/learner_group/facilitator-dashboard-backend-api.service'; -import { ShortLearnerGroupSummary } from 'domain/learner_group/short-learner-group-summary.model'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {LearnerGroupPagesConstants} from 'pages/learner-group-pages/learner-group-pages.constants'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {TranslateService} from '@ngx-translate/core'; +import {PageTitleService} from 'services/page-title.service'; +import {FacilitatorDashboardPageComponent} from './facilitator-dashboard-page.component'; +import {FacilitatorDashboardBackendApiService} from 'domain/learner_group/facilitator-dashboard-backend-api.service'; +import {ShortLearnerGroupSummary} from 'domain/learner_group/short-learner-group-summary.model'; class MockTranslateService { onLangChange: EventEmitter = new EventEmitter(); @@ -45,8 +46,7 @@ describe('FacilitatorDashboardPageComponent', () => { let fixture: ComponentFixture; let urlInterpolationService: UrlInterpolationService; let translateService: TranslateService; - let facilitatorDashboardBackendApiService: - FacilitatorDashboardBackendApiService; + let facilitatorDashboardBackendApiService: FacilitatorDashboardBackendApiService; let pageTitleService: PageTitleService; const shortSummaryBackendDict = { @@ -54,26 +54,22 @@ describe('FacilitatorDashboardPageComponent', () => { title: 'group title', description: 'group description', facilitator_usernames: ['facilitator1'], - learners_count: 4 + learners_count: 4, }; - const shortLearnerGroupSummary = ( - ShortLearnerGroupSummary.createFromBackendDict( - shortSummaryBackendDict)); + const shortLearnerGroupSummary = + ShortLearnerGroupSummary.createFromBackendDict(shortSummaryBackendDict); beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - FacilitatorDashboardPageComponent, - MockTranslatePipe - ], + declarations: [FacilitatorDashboardPageComponent, MockTranslatePipe], providers: [ { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); @@ -82,7 +78,8 @@ describe('FacilitatorDashboardPageComponent', () => { translateService = TestBed.inject(TranslateService); pageTitleService = TestBed.inject(PageTitleService); facilitatorDashboardBackendApiService = TestBed.inject( - FacilitatorDashboardBackendApiService); + FacilitatorDashboardBackendApiService + ); fixture = TestBed.createComponent(FacilitatorDashboardPageComponent); component = fixture.componentInstance; fixture.detectChanges(); @@ -101,9 +98,11 @@ describe('FacilitatorDashboardPageComponent', () => { expect(component.subscribeToOnLangChange).toHaveBeenCalled(); expect(component.createLearnerGroupPageUrl).toBe( - LearnerGroupPagesConstants.CREATE_LEARNER_GROUP_PAGE_URL); - expect(component.shortLearnerGroupSummaries).toEqual( - [shortLearnerGroupSummary]); + LearnerGroupPagesConstants.CREATE_LEARNER_GROUP_PAGE_URL + ); + expect(component.shortLearnerGroupSummaries).toEqual([ + shortLearnerGroupSummary, + ]); })); it('should call set page title whenever the language is changed', () => { @@ -122,16 +121,20 @@ describe('FacilitatorDashboardPageComponent', () => { component.setPageTitle(); expect(translateService.instant).toHaveBeenCalledWith( - 'I18N_FACILITATOR_DASHBOARD_PAGE_TITLE'); + 'I18N_FACILITATOR_DASHBOARD_PAGE_TITLE' + ); expect(pageTitleService.setDocumentTitle).toHaveBeenCalledWith( - 'I18N_FACILITATOR_DASHBOARD_PAGE_TITLE'); + 'I18N_FACILITATOR_DASHBOARD_PAGE_TITLE' + ); }); it('should get learner group page url correctly', () => { spyOn(urlInterpolationService, 'interpolateUrl').and.returnValue( - '/create-learner-group/groupId1'); + '/create-learner-group/groupId1' + ); expect(component.getLearnerGroupPageUrl('groupId1')).toBe( - '/create-learner-group/groupId1'); + '/create-learner-group/groupId1' + ); }); }); diff --git a/core/templates/pages/facilitator-dashboard-page/facilitator-dashboard-page.component.ts b/core/templates/pages/facilitator-dashboard-page/facilitator-dashboard-page.component.ts index 9a95cf536d30..bc1b08c03f88 100644 --- a/core/templates/pages/facilitator-dashboard-page/facilitator-dashboard-page.component.ts +++ b/core/templates/pages/facilitator-dashboard-page/facilitator-dashboard-page.component.ts @@ -16,29 +16,24 @@ * @fileoverview Component for the facilitator dashboard page. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; -import { LoaderService } from 'services/loader.service'; -import { PageTitleService } from 'services/page-title.service'; -import { LearnerGroupPagesConstants } from - 'pages/learner-group-pages/learner-group-pages.constants'; -import { ShortLearnerGroupSummary } from - 'domain/learner_group/short-learner-group-summary.model'; -import { FacilitatorDashboardBackendApiService } from - 'domain/learner_group/facilitator-dashboard-backend-api.service'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; +import {LoaderService} from 'services/loader.service'; +import {PageTitleService} from 'services/page-title.service'; +import {LearnerGroupPagesConstants} from 'pages/learner-group-pages/learner-group-pages.constants'; +import {ShortLearnerGroupSummary} from 'domain/learner_group/short-learner-group-summary.model'; +import {FacilitatorDashboardBackendApiService} from 'domain/learner_group/facilitator-dashboard-backend-api.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; import './facilitator-dashboard-page.component.css'; - @Component({ selector: 'oppia-facilitator-dashboard-page', templateUrl: './facilitator-dashboard-page.component.html', - styleUrls: ['./facilitator-dashboard-page.component.css'] + styleUrls: ['./facilitator-dashboard-page.component.css'], }) export class FacilitatorDashboardPageComponent implements OnInit, OnDestroy { directiveSubscriptions = new Subscription(); @@ -48,8 +43,7 @@ export class FacilitatorDashboardPageComponent implements OnInit, OnDestroy { constructor( private pageTitleService: PageTitleService, private translateService: TranslateService, - private facilitatorDashboardBackendApiService: - FacilitatorDashboardBackendApiService, + private facilitatorDashboardBackendApiService: FacilitatorDashboardBackendApiService, private urlInterpolationService: UrlInterpolationService, private loaderService: LoaderService ) {} @@ -64,32 +58,30 @@ export class FacilitatorDashboardPageComponent implements OnInit, OnDestroy { setPageTitle(): void { let translatedTitle = this.translateService.instant( - 'I18N_FACILITATOR_DASHBOARD_PAGE_TITLE'); + 'I18N_FACILITATOR_DASHBOARD_PAGE_TITLE' + ); this.pageTitleService.setDocumentTitle(translatedTitle); } getLearnerGroupPageUrl(learnerGroupId: string): string { - return ( - this.urlInterpolationService.interpolateUrl( - '/edit-learner-group/', { - groupId: learnerGroupId - } - ) + return this.urlInterpolationService.interpolateUrl( + '/edit-learner-group/', + { + groupId: learnerGroupId, + } ); } ngOnInit(): void { this.loaderService.showLoadingScreen('Loading your learner groups'); - this.createLearnerGroupPageUrl = ( - LearnerGroupPagesConstants.CREATE_LEARNER_GROUP_PAGE_URL - ); + this.createLearnerGroupPageUrl = + LearnerGroupPagesConstants.CREATE_LEARNER_GROUP_PAGE_URL; this.facilitatorDashboardBackendApiService - .fetchTeacherDashboardLearnerGroupsAsync().then( - (shortGroupSummaries) => { - this.shortLearnerGroupSummaries = shortGroupSummaries; - this.loaderService.hideLoadingScreen(); - } - ); + .fetchTeacherDashboardLearnerGroupsAsync() + .then(shortGroupSummaries => { + this.shortLearnerGroupSummaries = shortGroupSummaries; + this.loaderService.hideLoadingScreen(); + }); this.subscribeToOnLangChange(); } @@ -98,6 +90,9 @@ export class FacilitatorDashboardPageComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive( - 'oppiaFacilitatorDashboardPage', - downgradeComponent({component: FacilitatorDashboardPageComponent})); +angular + .module('oppia') + .directive( + 'oppiaFacilitatorDashboardPage', + downgradeComponent({component: FacilitatorDashboardPageComponent}) + ); diff --git a/core/templates/pages/facilitator-dashboard-page/facilitator-dashboard-page.import.ts b/core/templates/pages/facilitator-dashboard-page/facilitator-dashboard-page.import.ts index 247cea401f12..d16895b11ad5 100644 --- a/core/templates/pages/facilitator-dashboard-page/facilitator-dashboard-page.import.ts +++ b/core/templates/pages/facilitator-dashboard-page/facilitator-dashboard-page.import.ts @@ -22,18 +22,21 @@ import 'zone.js'; import uiValidate from 'angular-ui-validate'; angular.module('oppia', [ - require('angular-cookies'), 'ngAnimate', - 'ngMaterial', 'ngSanitize', 'ngTouch', 'pascalprecht.translate', - 'ui.bootstrap', uiValidate + require('angular-cookies'), + 'ngAnimate', + 'ngMaterial', + 'ngSanitize', + 'ngTouch', + 'pascalprecht.translate', + 'ui.bootstrap', + uiValidate, ]); require('Polyfills.ts'); // The module needs to be loaded directly after jquery since it defines the // main module the elements are attached to. -require( - 'pages/facilitator-dashboard-page/facilitator-dashboard-page.module.ts' -); +require('pages/facilitator-dashboard-page/facilitator-dashboard-page.module.ts'); require('App.ts'); require('base-components/oppia-root.directive.ts'); diff --git a/core/templates/pages/facilitator-dashboard-page/facilitator-dashboard-page.module.ts b/core/templates/pages/facilitator-dashboard-page/facilitator-dashboard-page.module.ts index 5d80f7698e4f..f45915d81f68 100644 --- a/core/templates/pages/facilitator-dashboard-page/facilitator-dashboard-page.module.ts +++ b/core/templates/pages/facilitator-dashboard-page/facilitator-dashboard-page.module.ts @@ -16,21 +16,20 @@ * @fileoverview Module for the facilitator dashboard page. */ -import { APP_INITIALIZER, NgModule, StaticProvider } from '@angular/core'; -import { BrowserModule, HAMMER_GESTURE_CONFIG } from '@angular/platform-browser'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { HttpClientModule } from '@angular/common/http'; -import { HTTP_INTERCEPTORS } from '@angular/common/http'; -import { RequestInterceptor } from 'services/request-interceptor.service'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { RouterModule } from '@angular/router'; -import { APP_BASE_HREF } from '@angular/common'; +import {APP_INITIALIZER, NgModule, StaticProvider} from '@angular/core'; +import {BrowserModule, HAMMER_GESTURE_CONFIG} from '@angular/platform-browser'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {HttpClientModule} from '@angular/common/http'; +import {HTTP_INTERCEPTORS} from '@angular/common/http'; +import {RequestInterceptor} from 'services/request-interceptor.service'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {RouterModule} from '@angular/router'; +import {APP_BASE_HREF} from '@angular/common'; -import { FacilitatorDashboardPageComponent } from - './facilitator-dashboard-page.component'; +import {FacilitatorDashboardPageComponent} from './facilitator-dashboard-page.component'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { MyHammerConfig, toastrConfig } from 'pages/oppia-root/app.module'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {MyHammerConfig, toastrConfig} from 'pages/oppia-root/app.module'; @NgModule({ imports: [ @@ -42,50 +41,48 @@ import { MyHammerConfig, toastrConfig } from 'pages/oppia-root/app.module'; SmartRouterModule, RouterModule.forRoot([]), SharedComponentsModule, - ToastrModule.forRoot(toastrConfig) - ], - declarations: [ - FacilitatorDashboardPageComponent - ], - entryComponents: [ - FacilitatorDashboardPageComponent + ToastrModule.forRoot(toastrConfig), ], + declarations: [FacilitatorDashboardPageComponent], + entryComponents: [FacilitatorDashboardPageComponent], providers: [ { provide: HTTP_INTERCEPTORS, useClass: RequestInterceptor, - multi: true + multi: true, }, { provide: APP_INITIALIZER, useFactory: platformFeatureInitFactory, deps: [PlatformFeatureService], - multi: true + multi: true, }, { provide: HAMMER_GESTURE_CONFIG, - useClass: MyHammerConfig + useClass: MyHammerConfig, }, { provide: APP_BASE_HREF, - useValue: '/' - } - ] + useValue: '/', + }, + ], }) class FacilitatorDashboardPageModule { // Empty placeholder method to satisfy the `Compiler`. ngDoBootstrap() {} } -import { platformFeatureInitFactory, PlatformFeatureService } from - 'services/platform-feature.service'; -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { downgradeModule } from '@angular/upgrade/static'; -import { ToastrModule } from 'ngx-toastr'; -import { SmartRouterModule } from 'hybrid-router-module-provider'; -import { OppiaAngularRootComponent } from 'components/oppia-angular-root.component'; +import { + platformFeatureInitFactory, + PlatformFeatureService, +} from 'services/platform-feature.service'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {downgradeModule} from '@angular/upgrade/static'; +import {ToastrModule} from 'ngx-toastr'; +import {SmartRouterModule} from 'hybrid-router-module-provider'; +import {OppiaAngularRootComponent} from 'components/oppia-angular-root.component'; -const bootstrapFnAsync = async(extraProviders: StaticProvider[]) => { +const bootstrapFnAsync = async (extraProviders: StaticProvider[]) => { const platformRef = platformBrowserDynamic(extraProviders); return platformRef.bootstrapModule(FacilitatorDashboardPageModule); }; @@ -100,5 +97,6 @@ angular.module('oppia').directive( // bootstrap the Angular 8. 'oppiaAngularRoot', downgradeComponent({ - component: OppiaAngularRootComponent - }) as angular.IDirectiveFactory); + component: OppiaAngularRootComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/feedback-updates-page/feedback-updates-page-root.component.spec.ts b/core/templates/pages/feedback-updates-page/feedback-updates-page-root.component.spec.ts index 7610b5ca75e8..81717151b535 100644 --- a/core/templates/pages/feedback-updates-page/feedback-updates-page-root.component.spec.ts +++ b/core/templates/pages/feedback-updates-page/feedback-updates-page-root.component.spec.ts @@ -16,16 +16,22 @@ * @fileoverview Unit tests for the feedback-updates page root component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; -import { AppConstants } from 'app.constants'; -import { AccessValidationBackendApiService } from 'pages/oppia-root/routing/access-validation-backend-api.service'; -import { LoaderService } from 'services/loader.service'; -import { PageHeadService } from 'services/page-head.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { FeedbackUpdatesPageRootComponent } from './feedback-updates-page-root.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; +import {AppConstants} from 'app.constants'; +import {AccessValidationBackendApiService} from 'pages/oppia-root/routing/access-validation-backend-api.service'; +import {LoaderService} from 'services/loader.service'; +import {PageHeadService} from 'services/page-head.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {FeedbackUpdatesPageRootComponent} from './feedback-updates-page-root.component'; class MockTranslateService { onLangChange: EventEmitter = new EventEmitter(); @@ -44,21 +50,16 @@ describe('FeedbackUpdates Page Root', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - FeedbackUpdatesPageRootComponent, - MockTranslatePipe - ], + imports: [HttpClientTestingModule], + declarations: [FeedbackUpdatesPageRootComponent, MockTranslatePipe], providers: [ PageHeadService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -68,7 +69,8 @@ describe('FeedbackUpdates Page Root', () => { pageHeadService = TestBed.inject(PageHeadService); loaderService = TestBed.inject(LoaderService); accessValidationBackendApiService = TestBed.inject( - AccessValidationBackendApiService); + AccessValidationBackendApiService + ); translateService = TestBed.inject(TranslateService); }); @@ -76,14 +78,15 @@ describe('FeedbackUpdates Page Root', () => { component.ngOnDestroy(); }); - it('should successfully instantiate the component', - () => { - expect(component).toBeDefined(); - }); + it('should successfully instantiate the component', () => { + expect(component).toBeDefined(); + }); it('should initialize and show page when access is valid', fakeAsync(() => { - spyOn(accessValidationBackendApiService, 'validateCanManageOwnAccount') - .and.returnValue(Promise.resolve()); + spyOn( + accessValidationBackendApiService, + 'validateCanManageOwnAccount' + ).and.returnValue(Promise.resolve()); spyOn(loaderService, 'showLoadingScreen'); spyOn(loaderService, 'hideLoadingScreen'); @@ -91,34 +94,39 @@ describe('FeedbackUpdates Page Root', () => { tick(); expect(loaderService.showLoadingScreen).toHaveBeenCalled(); - expect(accessValidationBackendApiService.validateCanManageOwnAccount) - .toHaveBeenCalled(); + expect( + accessValidationBackendApiService.validateCanManageOwnAccount + ).toHaveBeenCalled(); expect(component.pageIsShown).toBeTrue(); expect(component.errorPageIsShown).toBeFalse(); expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); })); - it('should initialize and show error page when server respond with error', - fakeAsync(() => { - spyOn(accessValidationBackendApiService, 'validateCanManageOwnAccount') - .and.returnValue(Promise.reject()); - spyOn(loaderService, 'showLoadingScreen'); - spyOn(loaderService, 'hideLoadingScreen'); + it('should initialize and show error page when server respond with error', fakeAsync(() => { + spyOn( + accessValidationBackendApiService, + 'validateCanManageOwnAccount' + ).and.returnValue(Promise.reject()); + spyOn(loaderService, 'showLoadingScreen'); + spyOn(loaderService, 'hideLoadingScreen'); - component.ngOnInit(); - tick(); + component.ngOnInit(); + tick(); - expect(loaderService.showLoadingScreen).toHaveBeenCalled(); - expect(accessValidationBackendApiService.validateCanManageOwnAccount) - .toHaveBeenCalled(); - expect(component.pageIsShown).toBeFalse(); - expect(component.errorPageIsShown).toBeTrue(); - expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); - })); + expect(loaderService.showLoadingScreen).toHaveBeenCalled(); + expect( + accessValidationBackendApiService.validateCanManageOwnAccount + ).toHaveBeenCalled(); + expect(component.pageIsShown).toBeFalse(); + expect(component.errorPageIsShown).toBeTrue(); + expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); + })); it('should initialize and subscribe to onLangChange', fakeAsync(() => { - spyOn(accessValidationBackendApiService, 'validateCanManageOwnAccount') - .and.returnValue(Promise.resolve()); + spyOn( + accessValidationBackendApiService, + 'validateCanManageOwnAccount' + ).and.returnValue(Promise.resolve()); spyOn(component.directiveSubscriptions, 'add'); spyOn(translateService.onLangChange, 'subscribe'); @@ -130,8 +138,10 @@ describe('FeedbackUpdates Page Root', () => { })); it('should update page title whenever the language changes', () => { - spyOn(accessValidationBackendApiService, 'validateCanManageOwnAccount') - .and.returnValue(Promise.resolve()); + spyOn( + accessValidationBackendApiService, + 'validateCanManageOwnAccount' + ).and.returnValue(Promise.resolve()); component.ngOnInit(); spyOn(component, 'setPageTitleAndMetaTags'); @@ -147,10 +157,12 @@ describe('FeedbackUpdates Page Root', () => { component.setPageTitleAndMetaTags(); expect(translateService.instant).toHaveBeenCalledWith( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.FEEDBACK_UPDATES.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.FEEDBACK_UPDATES.TITLE + ); expect(pageHeadService.updateTitleAndMetaTags).toHaveBeenCalledWith( AppConstants.PAGES_REGISTERED_WITH_FRONTEND.FEEDBACK_UPDATES.TITLE, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.FEEDBACK_UPDATES.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.FEEDBACK_UPDATES.META + ); }); it('should unsubscribe on component destruction', () => { diff --git a/core/templates/pages/feedback-updates-page/feedback-updates-page-root.component.ts b/core/templates/pages/feedback-updates-page/feedback-updates-page-root.component.ts index 46a1b53958b6..9f267d7255e1 100644 --- a/core/templates/pages/feedback-updates-page/feedback-updates-page-root.component.ts +++ b/core/templates/pages/feedback-updates-page/feedback-updates-page-root.component.ts @@ -16,18 +16,18 @@ * @fileoverview Root component for Feedback-Updates Page. */ -import { Component, OnDestroy } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; +import {Component, OnDestroy} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; -import { AppConstants } from 'app.constants'; -import { AccessValidationBackendApiService } from 'pages/oppia-root/routing/access-validation-backend-api.service'; -import { LoaderService } from 'services/loader.service'; -import { PageHeadService } from 'services/page-head.service'; +import {AppConstants} from 'app.constants'; +import {AccessValidationBackendApiService} from 'pages/oppia-root/routing/access-validation-backend-api.service'; +import {LoaderService} from 'services/loader.service'; +import {PageHeadService} from 'services/page-head.service'; @Component({ selector: 'oppia-feedback-updates-page-root', - templateUrl: './feedback-updates-page-root.component.html' + templateUrl: './feedback-updates-page-root.component.html', }) export class FeedbackUpdatesPageRootComponent implements OnDestroy { directiveSubscriptions = new Subscription(); @@ -35,8 +35,7 @@ export class FeedbackUpdatesPageRootComponent implements OnDestroy { errorPageIsShown: boolean = false; constructor( - private accessValidationBackendApiService: - AccessValidationBackendApiService, + private accessValidationBackendApiService: AccessValidationBackendApiService, private loaderService: LoaderService, private pageHeadService: PageHeadService, private translateService: TranslateService @@ -44,10 +43,12 @@ export class FeedbackUpdatesPageRootComponent implements OnDestroy { setPageTitleAndMetaTags(): void { let translatedTitle = this.translateService.instant( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.FEEDBACK_UPDATES.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.FEEDBACK_UPDATES.TITLE + ); this.pageHeadService.updateTitleAndMetaTags( translatedTitle, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.FEEDBACK_UPDATES.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.FEEDBACK_UPDATES.META + ); } ngOnInit(): void { @@ -57,12 +58,17 @@ export class FeedbackUpdatesPageRootComponent implements OnDestroy { }) ); this.loaderService.showLoadingScreen('Loading'); - this.accessValidationBackendApiService.validateCanManageOwnAccount() - .then(() => { - this.pageIsShown = true; - }, (err) => { - this.errorPageIsShown = true; - }).finally(() => { + this.accessValidationBackendApiService + .validateCanManageOwnAccount() + .then( + () => { + this.pageIsShown = true; + }, + err => { + this.errorPageIsShown = true; + } + ) + .finally(() => { this.loaderService.hideLoadingScreen(); }); } diff --git a/core/templates/pages/feedback-updates-page/feedback-updates-page-routing.module.ts b/core/templates/pages/feedback-updates-page/feedback-updates-page-routing.module.ts index 06f46a4e6953..0ef1463e5648 100644 --- a/core/templates/pages/feedback-updates-page/feedback-updates-page-routing.module.ts +++ b/core/templates/pages/feedback-updates-page/feedback-updates-page-routing.module.ts @@ -16,24 +16,19 @@ * @fileoverview Routing module for feedback-updates page. */ -import { NgModule } from '@angular/core'; -import { Route, RouterModule } from '@angular/router'; -import { FeedbackUpdatesPageRootComponent } from './feedback-updates-page-root.component'; +import {NgModule} from '@angular/core'; +import {Route, RouterModule} from '@angular/router'; +import {FeedbackUpdatesPageRootComponent} from './feedback-updates-page-root.component'; const routes: Route[] = [ { path: '', - component: FeedbackUpdatesPageRootComponent - } + component: FeedbackUpdatesPageRootComponent, + }, ]; @NgModule({ - imports: [ - RouterModule.forChild(routes) - ], - exports: [ - RouterModule - ] + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], }) - export class FeedbackUpdatesPageRoutingModule {} diff --git a/core/templates/pages/feedback-updates-page/feedback-updates-page.component.spec.ts b/core/templates/pages/feedback-updates-page/feedback-updates-page.component.spec.ts index 3b5a66174ca0..f67af99e9a27 100644 --- a/core/templates/pages/feedback-updates-page/feedback-updates-page.component.spec.ts +++ b/core/templates/pages/feedback-updates-page/feedback-updates-page.component.spec.ts @@ -16,31 +16,37 @@ * @fileoverview Unit tests for feedback updates page. */ -import { FeedbackThreadSummary } from - 'domain/feedback_thread/feedback-thread-summary.model'; - -import { FeedbackUpdatesPageComponent } from './feedback-updates-page.component'; -import { async, ComponentFixture, fakeAsync, flush, TestBed, tick } from '@angular/core/testing'; -import { MaterialModule } from 'modules/material.module'; -import { FormsModule } from '@angular/forms'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { Component, EventEmitter, NO_ERRORS_SCHEMA, Pipe } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; - -import { AlertsService } from 'services/alerts.service'; -import { CsrfTokenService } from 'services/csrf-token.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { DateTimeFormatService } from 'services/date-time-format.service'; -import { FeedbackUpdatesBackendApiService } from 'domain/feedback_updates/feedback-updates-backend-api.service'; -import { SortByPipe } from 'filters/string-utility-filters/sort-by.pipe'; -import { UserService } from 'services/user.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { PageTitleService } from 'services/page-title.service'; -import { UrlService } from 'services/contextual/url.service'; -import { UserInfo } from 'domain/user/user-info.model'; +import {FeedbackThreadSummary} from 'domain/feedback_thread/feedback-thread-summary.model'; + +import {FeedbackUpdatesPageComponent} from './feedback-updates-page.component'; +import { + async, + ComponentFixture, + fakeAsync, + flush, + TestBed, + tick, +} from '@angular/core/testing'; +import {MaterialModule} from 'modules/material.module'; +import {FormsModule} from '@angular/forms'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {Component, EventEmitter, NO_ERRORS_SCHEMA, Pipe} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; + +import {AlertsService} from 'services/alerts.service'; +import {CsrfTokenService} from 'services/csrf-token.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {DateTimeFormatService} from 'services/date-time-format.service'; +import {FeedbackUpdatesBackendApiService} from 'domain/feedback_updates/feedback-updates-backend-api.service'; +import {SortByPipe} from 'filters/string-utility-filters/sort-by.pipe'; +import {UserService} from 'services/user.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {PageTitleService} from 'services/page-title.service'; +import {UrlService} from 'services/contextual/url.service'; +import {UserInfo} from 'domain/user/user-info.model'; @Pipe({name: 'slice'}) class MockSlicePipe { @@ -64,13 +70,10 @@ class MockTranslateService { } @Component({selector: 'background-banner', template: ''}) -class BackgroundBannerComponentStub { -} - +class BackgroundBannerComponentStub {} @Component({selector: 'loading-dots', template: ''}) -class LoadingDotsComponentStub { -} +class LoadingDotsComponentStub {} describe('Feedback updates page', () => { let component: FeedbackUpdatesPageComponent; @@ -79,8 +82,7 @@ describe('Feedback updates page', () => { let csrfTokenService: CsrfTokenService; let dateTimeFormatService: DateTimeFormatService; let focusManagerService: FocusManagerService; - let feedbackUpdatesBackendApiService: - FeedbackUpdatesBackendApiService; + let feedbackUpdatesBackendApiService: FeedbackUpdatesBackendApiService; let windowDimensionsService: WindowDimensionsService; let mockResizeEmitter: EventEmitter; let userService: UserService; @@ -88,62 +90,63 @@ describe('Feedback updates page', () => { let pageTitleService: PageTitleService; let urlService: UrlService; - let threadSummaryList = [{ - status: 'open', - original_author_id: '1', - last_updated_msecs: 1000, - last_message_text: 'Last Message', - total_message_count: 5, - last_message_is_read: false, - second_last_message_is_read: true, - author_last_message: '2', - author_second_last_message: 'Last Message', - exploration_title: 'Biology', - exploration_id: 'exp1', - thread_id: 'thread_1' - }, - { - status: 'open', - original_author_id: '2', - last_updated_msecs: 1001, - last_message_text: 'Last Message', - total_message_count: 5, - last_message_is_read: false, - second_last_message_is_read: true, - author_last_message: '2', - author_second_last_message: 'Last Message', - exploration_title: 'Algebra', - exploration_id: 'exp1', - thread_id: 'thread_1' - }, - { - status: 'open', - original_author_id: '3', - last_updated_msecs: 1002, - last_message_text: 'Last Message', - total_message_count: 5, - last_message_is_read: false, - second_last_message_is_read: true, - author_last_message: '2', - author_second_last_message: 'Last Message', - exploration_title: 'Three Balls', - exploration_id: 'exp1', - thread_id: 'thread_1' - }, - { - status: 'open', - original_author_id: '4', - last_updated_msecs: 1003, - last_message_text: 'Last Message', - total_message_count: 5, - last_message_is_read: false, - second_last_message_is_read: true, - author_last_message: '2', - author_second_last_message: 'Last Message', - exploration_title: 'Zebra', - exploration_id: 'exp1', - thread_id: 'thread_1' - } + let threadSummaryList = [ + { + status: 'open', + original_author_id: '1', + last_updated_msecs: 1000, + last_message_text: 'Last Message', + total_message_count: 5, + last_message_is_read: false, + second_last_message_is_read: true, + author_last_message: '2', + author_second_last_message: 'Last Message', + exploration_title: 'Biology', + exploration_id: 'exp1', + thread_id: 'thread_1', + }, + { + status: 'open', + original_author_id: '2', + last_updated_msecs: 1001, + last_message_text: 'Last Message', + total_message_count: 5, + last_message_is_read: false, + second_last_message_is_read: true, + author_last_message: '2', + author_second_last_message: 'Last Message', + exploration_title: 'Algebra', + exploration_id: 'exp1', + thread_id: 'thread_1', + }, + { + status: 'open', + original_author_id: '3', + last_updated_msecs: 1002, + last_message_text: 'Last Message', + total_message_count: 5, + last_message_is_read: false, + second_last_message_is_read: true, + author_last_message: '2', + author_second_last_message: 'Last Message', + exploration_title: 'Three Balls', + exploration_id: 'exp1', + thread_id: 'thread_1', + }, + { + status: 'open', + original_author_id: '4', + last_updated_msecs: 1003, + last_message_text: 'Last Message', + total_message_count: 5, + last_message_is_read: false, + second_last_message_is_read: true, + author_last_message: '2', + author_second_last_message: 'Last Message', + exploration_title: 'Zebra', + exploration_id: 'exp1', + thread_id: 'thread_1', + }, ]; let FeedbackUpdatesData = { @@ -172,10 +175,10 @@ describe('Feedback updates page', () => { isQuestionAdmin: () => false, isTranslationCoordinator: () => false, canCreateCollections: () => true, - getPreferredSiteLanguageCode: () =>'en', + getPreferredSiteLanguageCode: () => 'en', getUsername: () => 'username1', getEmail: () => 'tester@example.org', - isLoggedIn: () => true + isLoggedIn: () => true, }; afterEach(() => { @@ -190,7 +193,7 @@ describe('Feedback updates page', () => { BrowserAnimationsModule, MaterialModule, FormsModule, - HttpClientTestingModule + HttpClientTestingModule, ], declarations: [ FeedbackUpdatesPageComponent, @@ -211,17 +214,17 @@ describe('Feedback updates page', () => { useValue: { isWindowNarrow: () => true, getResizeEvent: () => mockResizeEmitter, - } + }, }, UrlInterpolationService, UserService, PageTitleService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -234,32 +237,36 @@ describe('Feedback updates page', () => { dateTimeFormatService = TestBed.inject(DateTimeFormatService); focusManagerService = TestBed.inject(FocusManagerService); windowDimensionsService = TestBed.inject(WindowDimensionsService); - feedbackUpdatesBackendApiService = - TestBed.inject(FeedbackUpdatesBackendApiService); + feedbackUpdatesBackendApiService = TestBed.inject( + FeedbackUpdatesBackendApiService + ); userService = TestBed.inject(UserService); translateService = TestBed.inject(TranslateService); pageTitleService = TestBed.inject(PageTitleService); urlService = TestBed.inject(UrlService); - spyOn(csrfTokenService, 'getTokenAsync').and.callFake(async() => { + spyOn(csrfTokenService, 'getTokenAsync').and.callFake(async () => { return Promise.resolve('sample-csrf-token'); }); - spyOn(userService, 'getProfileImageDataUrl').and.returnValue( - ['profile-image-url-png', 'profile-image-url-webp']); + spyOn(userService, 'getProfileImageDataUrl').and.returnValue([ + 'profile-image-url-png', + 'profile-image-url-webp', + ]); spyOn( feedbackUpdatesBackendApiService, - 'fetchFeedbackUpdatesDataAsync') - .and.returnValue(Promise.resolve({ - numberOfUnreadThreads: FeedbackUpdatesData. - number_of_unread_threads, - threadSummaries: ( - FeedbackUpdatesData.thread_summaries.map( - threadSummary => FeedbackThreadSummary - .createFromBackendDict(threadSummary))), - paginatedThreadsList: [] - })); + 'fetchFeedbackUpdatesDataAsync' + ).and.returnValue( + Promise.resolve({ + numberOfUnreadThreads: FeedbackUpdatesData.number_of_unread_threads, + threadSummaries: FeedbackUpdatesData.thread_summaries.map( + threadSummary => + FeedbackThreadSummary.createFromBackendDict(threadSummary) + ), + paginatedThreadsList: [], + }) + ); spyOn(urlService, 'getUrlParams').and.returnValue({ active_tab: 'learner-groups', @@ -270,41 +277,47 @@ describe('Feedback updates page', () => { fixture.detectChanges(); })); - it('should initialize correctly component properties after its' + - ' initialization and get data from backend', fakeAsync(() => { - spyOn(userService, 'getUserInfoAsync').and - .callFake(async() => { + it( + 'should initialize correctly component properties after its' + + ' initialization and get data from backend', + fakeAsync(() => { + spyOn(userService, 'getUserInfoAsync').and.callFake(async () => { return Promise.resolve(userInfo as UserInfo); }); + component.ngOnInit(); + flush(); + + expect(component.profilePicturePngDataUrl).toEqual( + 'profile-image-url-png' + ); + expect(component.profilePictureWebpDataUrl).toEqual( + 'profile-image-url-webp' + ); + expect(component.username).toBe(userInfo.getUsername()); + expect(component.windowIsNarrow).toBeTrue(); + }) + ); + + it('should get default profile pictures when username is null', fakeAsync(() => { + let userInfo = { + getUsername: () => null, + isSuperAdmin: () => true, + getEmail: () => 'test_email@example.com', + }; + spyOn(userService, 'getUserInfoAsync').and.resolveTo( + userInfo as UserInfo + ); component.ngOnInit(); flush(); expect(component.profilePicturePngDataUrl).toEqual( - 'profile-image-url-png'); + '/assets/images/avatar/user_blue_150px.png' + ); expect(component.profilePictureWebpDataUrl).toEqual( - 'profile-image-url-webp'); - expect(component.username).toBe(userInfo.getUsername()); - expect(component.windowIsNarrow).toBeTrue(); + '/assets/images/avatar/user_blue_150px.webp' + ); })); - it('should get default profile pictures when username is null', - fakeAsync(() => { - let userInfo = { - getUsername: () => null, - isSuperAdmin: () => true, - getEmail: () => 'test_email@example.com' - }; - spyOn(userService, 'getUserInfoAsync') - .and.resolveTo(userInfo as UserInfo); - component.ngOnInit(); - flush(); - - expect(component.profilePicturePngDataUrl).toEqual( - '/assets/images/avatar/user_blue_150px.png'); - expect(component.profilePictureWebpDataUrl).toEqual( - '/assets/images/avatar/user_blue_150px.webp'); - })); - it('should check whether window is narrow on resizing the screen', () => { spyOn(windowDimensionsService, 'isWindowNarrow').and.returnValue(false); @@ -317,10 +330,9 @@ describe('Feedback updates page', () => { it('should set focus without scroll on browse lesson btn', fakeAsync(() => { const focusSpy = spyOn(focusManagerService, 'setFocusWithoutScroll'); - spyOn(userService, 'getUserInfoAsync').and - .callFake(async() => { - return Promise.resolve(userInfo as UserInfo); - }); + spyOn(userService, 'getUserInfoAsync').and.callFake(async () => { + return Promise.resolve(userInfo as UserInfo); + }); component.ngOnInit(); flush(); @@ -328,22 +340,25 @@ describe('Feedback updates page', () => { expect(focusSpy).toHaveBeenCalledWith('ourLessonsBtn'); })); - it('should subscribe to onLangChange upon initialisation and set page ' + - 'title whenever language changes', fakeAsync(() => { - spyOn(component.directiveSubscriptions, 'add'); - spyOn(translateService.onLangChange, 'subscribe'); - spyOn(component, 'setPageTitle'); + it( + 'should subscribe to onLangChange upon initialisation and set page ' + + 'title whenever language changes', + fakeAsync(() => { + spyOn(component.directiveSubscriptions, 'add'); + spyOn(translateService.onLangChange, 'subscribe'); + spyOn(component, 'setPageTitle'); - component.ngOnInit(); - flush(); + component.ngOnInit(); + flush(); - expect(component.directiveSubscriptions.add).toHaveBeenCalled(); - expect(translateService.onLangChange.subscribe).toHaveBeenCalled(); + expect(component.directiveSubscriptions.add).toHaveBeenCalled(); + expect(translateService.onLangChange.subscribe).toHaveBeenCalled(); - translateService.onLangChange.emit(); + translateService.onLangChange.emit(); - expect(component.setPageTitle).toHaveBeenCalled(); - })); + expect(component.setPageTitle).toHaveBeenCalled(); + }) + ); it('should obtain translated page title and set it', () => { spyOn(translateService, 'instant').and.callThrough(); @@ -352,26 +367,31 @@ describe('Feedback updates page', () => { component.setPageTitle(); expect(translateService.instant).toHaveBeenCalledWith( - 'I18N_FEEDBACK_UPDATES_PAGE_TITLE'); + 'I18N_FEEDBACK_UPDATES_PAGE_TITLE' + ); expect(pageTitleService.setDocumentTitle).toHaveBeenCalledWith( - 'I18N_FEEDBACK_UPDATES_PAGE_TITLE'); + 'I18N_FEEDBACK_UPDATES_PAGE_TITLE' + ); }); it('should get static image url', () => { let imagePath = '/path/to/image.png'; expect(component.getStaticImageUrl(imagePath)).toBe( - '/assets/images/path/to/image.png'); + '/assets/images/path/to/image.png' + ); }); it('should get user profile image png data url correctly', () => { expect(component.getauthorPicturePngDataUrl('username')).toBe( - 'profile-image-url-png'); + 'profile-image-url-png' + ); }); it('should get user profile image webp data url correctly', () => { expect(component.getauthorPictureWebpDataUrl('username')).toBe( - 'profile-image-url-webp'); + 'profile-image-url-webp' + ); }); it('should show username popover based on its length', () => { @@ -379,212 +399,329 @@ describe('Feedback updates page', () => { expect(component.showUsernamePopover('abc')).toBe('none'); }); - it('should change feedback sorting options by last update msecs when' + - ' changing sorting type', () => { - expect(component.isCurrentFeedbackSortDescending).toBe(true); - expect(component.currentFeedbackThreadsSortType).toBe('lastUpdatedMsecs'); + it( + 'should change feedback sorting options by last update msecs when' + + ' changing sorting type', + () => { + expect(component.isCurrentFeedbackSortDescending).toBe(true); + expect(component.currentFeedbackThreadsSortType).toBe( + 'lastUpdatedMsecs' + ); - component.setFeedbackSortingOptions('lastUpdatedMsecs'); + component.setFeedbackSortingOptions('lastUpdatedMsecs'); - expect(component.isCurrentFeedbackSortDescending).toBe(false); - }); + expect(component.isCurrentFeedbackSortDescending).toBe(false); + } + ); - it('should change feedback sorting options by exploration when changing' + - ' sorting type', () => { - component.setFeedbackSortingOptions('exploration'); + it( + 'should change feedback sorting options by exploration when changing' + + ' sorting type', + () => { + component.setFeedbackSortingOptions('exploration'); - expect(component.currentFeedbackThreadsSortType).toBe('exploration'); - expect(component.isCurrentFeedbackSortDescending).toBe(true); - }); + expect(component.currentFeedbackThreadsSortType).toBe('exploration'); + expect(component.isCurrentFeedbackSortDescending).toBe(true); + } + ); - it('should sort feedback updates given sorting property as last updated' + - ' in ascending order', fakeAsync(() => { - // The default sort option for Feedback Updates is last updated. - expect(component.currentFeedbackThreadsSortType) - .toBe('lastUpdatedMsecs'); - expect(component.isCurrentFeedbackSortDescending).toBeTrue(); - expect(component.getValueOfFeedbackThreadSortKey()) - .toBe('lastUpdatedMsecs'); + it( + 'should sort feedback updates given sorting property as last updated' + + ' in ascending order', + fakeAsync(() => { + // The default sort option for Feedback Updates is last updated. + expect(component.currentFeedbackThreadsSortType).toBe( + 'lastUpdatedMsecs' + ); + expect(component.isCurrentFeedbackSortDescending).toBeTrue(); + expect(component.getValueOfFeedbackThreadSortKey()).toBe( + 'lastUpdatedMsecs' + ); - fixture.detectChanges(); + fixture.detectChanges(); - const feedbackListNameNodes = - fixture.debugElement.nativeElement - .querySelectorAll('.e2e-test-feedback-exploration') as - NodeListOf; - - // The forEach loop is being used here because - // getValueOfSubscriptionSortKey is used in a *ngFor directive. - // Note that given subscription list is not sorted. - const expectedInnerText = ['Biology', 'Algebra', 'Three Balls', 'Zebra']; - feedbackListNameNodes.forEach((titleNode, index: number) => { - expect(titleNode.innerText).toBe(expectedInnerText[index]); - }); - })); + const feedbackListNameNodes = + fixture.debugElement.nativeElement.querySelectorAll( + '.e2e-test-feedback-exploration' + ) as NodeListOf; + + // The forEach loop is being used here because + // getValueOfSubscriptionSortKey is used in a *ngFor directive. + // Note that given subscription list is not sorted. + const expectedInnerText = [ + 'Biology', + 'Algebra', + 'Three Balls', + 'Zebra', + ]; + feedbackListNameNodes.forEach((titleNode, index: number) => { + expect(titleNode.innerText).toBe(expectedInnerText[index]); + }); + }) + ); - it('should sort feedback updates given sorting property as last updated' + - ' in descending order', fakeAsync(() => { - // The default sort option for Feedback Updates is last updated. - expect(component.currentFeedbackThreadsSortType) - .toBe('lastUpdatedMsecs'); - expect(component.isCurrentFeedbackSortDescending).toBeTrue(); - expect(component.getValueOfFeedbackThreadSortKey()) - .toBe('lastUpdatedMsecs'); + it( + 'should sort feedback updates given sorting property as last updated' + + ' in descending order', + fakeAsync(() => { + // The default sort option for Feedback Updates is last updated. + expect(component.currentFeedbackThreadsSortType).toBe( + 'lastUpdatedMsecs' + ); + expect(component.isCurrentFeedbackSortDescending).toBeTrue(); + expect(component.getValueOfFeedbackThreadSortKey()).toBe( + 'lastUpdatedMsecs' + ); - component.setFeedbackSortingOptions('lastUpdatedMsecs'); + component.setFeedbackSortingOptions('lastUpdatedMsecs'); - expect(component.isCurrentFeedbackSortDescending).toBeFalse(); + expect(component.isCurrentFeedbackSortDescending).toBeFalse(); - tick(); - fixture.detectChanges(); + tick(); + fixture.detectChanges(); - const feedbackListNameNodes = - fixture.debugElement.nativeElement - .querySelectorAll('.e2e-test-feedback-exploration') as - NodeListOf; - - // The forEach loop is being used here because - // getValueOfSubscriptionSortKey is used in a *ngFor directive. - // Note that given subscription list is not sorted. - const expectedInnerText = ['Zebra', 'Three Balls', 'Algebra', 'Biology']; - feedbackListNameNodes.forEach((titleNode, index: number) => { - expect(titleNode.innerText).toBe(expectedInnerText[index]); - }); - })); + const feedbackListNameNodes = + fixture.debugElement.nativeElement.querySelectorAll( + '.e2e-test-feedback-exploration' + ) as NodeListOf; + + // The forEach loop is being used here because + // getValueOfSubscriptionSortKey is used in a *ngFor directive. + // Note that given subscription list is not sorted. + const expectedInnerText = [ + 'Zebra', + 'Three Balls', + 'Algebra', + 'Biology', + ]; + feedbackListNameNodes.forEach((titleNode, index: number) => { + expect(titleNode.innerText).toBe(expectedInnerText[index]); + }); + }) + ); - it('should sort feedback updates given sorting property as exploration' + - ' in ascending order', fakeAsync(() => { - // The default sort option for Feedback Updates is last updated. - expect(component.currentFeedbackThreadsSortType) - .toBe('lastUpdatedMsecs'); - expect(component.isCurrentFeedbackSortDescending).toBeTrue(); - expect(component.getValueOfFeedbackThreadSortKey()) - .toBe('lastUpdatedMsecs'); + it( + 'should sort feedback updates given sorting property as exploration' + + ' in ascending order', + fakeAsync(() => { + // The default sort option for Feedback Updates is last updated. + expect(component.currentFeedbackThreadsSortType).toBe( + 'lastUpdatedMsecs' + ); + expect(component.isCurrentFeedbackSortDescending).toBeTrue(); + expect(component.getValueOfFeedbackThreadSortKey()).toBe( + 'lastUpdatedMsecs' + ); + + component.setFeedbackSortingOptions('explorationTitle'); + + expect(component.currentFeedbackThreadsSortType).toBe( + 'explorationTitle' + ); + expect(component.getValueOfFeedbackThreadSortKey()).toBe( + 'explorationTitle' + ); - component.setFeedbackSortingOptions('explorationTitle'); + tick(); + fixture.detectChanges(); - expect(component.currentFeedbackThreadsSortType) - .toBe('explorationTitle'); - expect(component.getValueOfFeedbackThreadSortKey()) - .toBe('explorationTitle'); + const feedbackListNameNodes = + fixture.debugElement.nativeElement.querySelectorAll( + '.e2e-test-feedback-exploration' + ) as NodeListOf; + + // The forEach loop is being used here because + // getValueOfSubscriptionSortKey is used in a *ngFor directive. + // Note that given subscription list is not sorted. + const expectedInnerText = [ + 'Algebra', + 'Biology', + 'Three Balls', + 'zebra', + ]; + feedbackListNameNodes.forEach((titleNode, index: number) => { + expect(titleNode.innerText).toBe(expectedInnerText[index]); + }); + }) + ); - tick(); - fixture.detectChanges(); + it( + 'should sort feedback updates given sorting property as exploration' + + ' in descending order', + fakeAsync(() => { + // The default sort option for Feedback Updates is last updated. + expect(component.currentFeedbackThreadsSortType).toBe( + 'lastUpdatedMsecs' + ); + expect(component.isCurrentFeedbackSortDescending).toBeTrue(); + expect(component.getValueOfFeedbackThreadSortKey()).toBe( + 'lastUpdatedMsecs' + ); - const feedbackListNameNodes = - fixture.debugElement.nativeElement - .querySelectorAll('.e2e-test-feedback-exploration') as - NodeListOf; - - // The forEach loop is being used here because - // getValueOfSubscriptionSortKey is used in a *ngFor directive. - // Note that given subscription list is not sorted. - const expectedInnerText = ['Algebra', 'Biology', 'Three Balls', 'zebra']; - feedbackListNameNodes.forEach((titleNode, index: number) => { - expect(titleNode.innerText).toBe(expectedInnerText[index]); - }); - })); + component.setFeedbackSortingOptions('explorationTitle'); - it('should sort feedback updates given sorting property as exploration' + - ' in descending order', fakeAsync(() => { - // The default sort option for Feedback Updates is last updated. - expect(component.currentFeedbackThreadsSortType) - .toBe('lastUpdatedMsecs'); - expect(component.isCurrentFeedbackSortDescending).toBeTrue(); - expect(component.getValueOfFeedbackThreadSortKey()) - .toBe('lastUpdatedMsecs'); + expect(component.currentFeedbackThreadsSortType).toBe( + 'explorationTitle' + ); + expect(component.getValueOfFeedbackThreadSortKey()).toBe( + 'explorationTitle' + ); - component.setFeedbackSortingOptions('explorationTitle'); + component.setFeedbackSortingOptions('explorationTitle'); - expect(component.currentFeedbackThreadsSortType) - .toBe('explorationTitle'); - expect(component.getValueOfFeedbackThreadSortKey()) - .toBe('explorationTitle'); + expect(component.isCurrentFeedbackSortDescending).toBeFalse(); - component.setFeedbackSortingOptions('explorationTitle'); + tick(); + fixture.detectChanges(); - expect(component.isCurrentFeedbackSortDescending).toBeFalse(); + const feedbackListNameNodes = + fixture.debugElement.nativeElement.querySelectorAll( + '.e2e-test-feedback-exploration' + ) as NodeListOf; + + // The forEach loop is being used here because + // getValueOfSubscriptionSortKey is used in a *ngFor directive. + // Note that given subscription list is not sorted. + const expectedInnerText = [ + 'Zebra', + 'Three Balls', + 'Biology', + 'Algebra', + ]; + feedbackListNameNodes.forEach((titleNode, index: number) => { + expect(titleNode.innerText).toBe(expectedInnerText[index]); + }); + }) + ); - tick(); - fixture.detectChanges(); + it( + 'should get messages in the thread from the backend when a thread is' + + ' selected', + fakeAsync(() => { + let threadStatus = 'open'; + let explorationId = 'exp1'; + let threadId = 'thread_1'; + let explorationTitle = 'Exploration Title'; + let threadMessages = [ + { + message_id: 1, + text: 'Feedback 1', + updated_status: 'open', + suggestion_html: 'An instead of a', + current_content_html: 'A orange', + description: 'Suggestion for english grammar', + author_username: 'username2', + created_on_msecs: 1200, + }, + ]; + const threadSpy = spyOn( + feedbackUpdatesBackendApiService, + 'onClickThreadAsync' + ).and.returnValue(Promise.resolve(threadMessages)); - const feedbackListNameNodes = - fixture.debugElement.nativeElement - .querySelectorAll('.e2e-test-feedback-exploration') as - NodeListOf; - - // The forEach loop is being used here because - // getValueOfSubscriptionSortKey is used in a *ngFor directive. - // Note that given subscription list is not sorted. - const expectedInnerText = ['Zebra', 'Three Balls', 'Biology', 'Algebra']; - feedbackListNameNodes.forEach((titleNode, index: number) => { - expect(titleNode.innerText).toBe(expectedInnerText[index]); - }); - })); + expect(component.numberOfUnreadThreads).toBe(10); + expect(component.loadingFeedback).toBe(false); - it('should get messages in the thread from the backend when a thread is' + - ' selected', fakeAsync(() => { - let threadStatus = 'open'; - let explorationId = 'exp1'; - let threadId = 'thread_1'; - let explorationTitle = 'Exploration Title'; - let threadMessages = [{ - message_id: 1, - text: 'Feedback 1', - updated_status: 'open', - suggestion_html: 'An instead of a', - current_content_html: 'A orange', - description: 'Suggestion for english grammar', - author_username: 'username2', - created_on_msecs: 1200 - }]; - const threadSpy = spyOn( - feedbackUpdatesBackendApiService, 'onClickThreadAsync') - .and.returnValue(Promise.resolve(threadMessages)); + component.onClickThread( + threadStatus, + explorationId, + threadId, + explorationTitle + ); - expect(component.numberOfUnreadThreads).toBe(10); - expect(component.loadingFeedback).toBe(false); + expect(component.loadingFeedback).toBe(true); - component.onClickThread( - threadStatus, explorationId, threadId, explorationTitle); + tick(); + fixture.detectChanges(); - expect(component.loadingFeedback).toBe(true); + expect(component.loadingFeedback).toBe(false); + expect(component.feedbackThreadActive).toBe(true); + expect(component.numberOfUnreadThreads).toBe(6); + expect(component.messageSummaries.length).toBe(1); + expect(threadSpy).toHaveBeenCalled(); + }) + ); - tick(); - fixture.detectChanges(); + it( + 'should set a new section as active when fetching message summary' + + ' list from backend', + fakeAsync(() => { + let threadStatus = 'open'; + let explorationId = 'exp1'; + let threadId = 'thread_1'; + let explorationTitle = 'Exploration Title'; + let threadMessages = [ + { + message_id: 1, + text: 'Feedback 1', + updated_status: 'open', + suggestion_html: 'An instead of a', + current_content_html: 'A orange', + description: 'Suggestion for english grammar', + author_username: 'username2', + created_on_msecs: 1200, + }, + ]; + const threadSpy = spyOn( + feedbackUpdatesBackendApiService, + 'onClickThreadAsync' + ).and.returnValue(Promise.resolve(threadMessages)); - expect(component.loadingFeedback).toBe(false); - expect(component.feedbackThreadActive).toBe(true); - expect(component.numberOfUnreadThreads).toBe(6); - expect(component.messageSummaries.length).toBe(1); - expect(threadSpy).toHaveBeenCalled(); - })); + expect(component.numberOfUnreadThreads).toBe(10); + expect(component.loadingFeedback).toBe(false); - it('should set a new section as active when fetching message summary' + - ' list from backend', fakeAsync(() => { + component.onClickThread( + threadStatus, + explorationId, + threadId, + explorationTitle + ); + + expect(component.loadingFeedback).toBe(true); + + tick(); + fixture.detectChanges(); + + expect(component.loadingFeedback).toBe(false); + expect(component.feedbackThreadActive).toBe(true); + expect(component.numberOfUnreadThreads).toBe(6); + expect(component.messageSummaries.length).toBe(1); + expect(threadSpy).toHaveBeenCalled(); + }) + ); + + it('should show all threads when a thread is not selected', fakeAsync(() => { let threadStatus = 'open'; let explorationId = 'exp1'; let threadId = 'thread_1'; let explorationTitle = 'Exploration Title'; - let threadMessages = [{ - message_id: 1, - text: 'Feedback 1', - updated_status: 'open', - suggestion_html: 'An instead of a', - current_content_html: 'A orange', - description: 'Suggestion for english grammar', - author_username: 'username2', - created_on_msecs: 1200 - }]; + let threadMessages = [ + { + message_id: 1, + text: 'Feedback 1', + updated_status: 'open', + suggestion_html: 'An instead of a', + current_content_html: 'A orange', + description: 'Suggestion for english grammar', + author_username: 'username2', + created_on_msecs: 1200, + }, + ]; + const threadSpy = spyOn( - feedbackUpdatesBackendApiService, 'onClickThreadAsync') - .and.returnValue(Promise.resolve(threadMessages)); + feedbackUpdatesBackendApiService, + 'onClickThreadAsync' + ).and.returnValue(Promise.resolve(threadMessages)); expect(component.numberOfUnreadThreads).toBe(10); expect(component.loadingFeedback).toBe(false); component.onClickThread( - threadStatus, explorationId, threadId, explorationTitle); + threadStatus, + explorationId, + threadId, + explorationTitle + ); expect(component.loadingFeedback).toBe(true); @@ -596,60 +733,21 @@ describe('Feedback updates page', () => { expect(component.numberOfUnreadThreads).toBe(6); expect(component.messageSummaries.length).toBe(1); expect(threadSpy).toHaveBeenCalled(); - })); - it('should show all threads when a thread is not selected', - fakeAsync(() => { - let threadStatus = 'open'; - let explorationId = 'exp1'; - let threadId = 'thread_1'; - let explorationTitle = 'Exploration Title'; - let threadMessages = [{ - message_id: 1, - text: 'Feedback 1', - updated_status: 'open', - suggestion_html: 'An instead of a', - current_content_html: 'A orange', - description: 'Suggestion for english grammar', - author_username: 'username2', - created_on_msecs: 1200 - }]; + component.showAllThreads(); - const threadSpy = - spyOn(feedbackUpdatesBackendApiService, 'onClickThreadAsync') - .and.returnValue(Promise.resolve(threadMessages)); - - expect(component.numberOfUnreadThreads).toBe(10); - expect(component.loadingFeedback).toBe(false); - - component.onClickThread( - threadStatus, explorationId, threadId, explorationTitle); - - expect(component.loadingFeedback).toBe(true); - - tick(); - fixture.detectChanges(); - - expect(component.loadingFeedback).toBe(false); - expect(component.feedbackThreadActive).toBe(true); - expect(component.numberOfUnreadThreads).toBe(6); - expect(component.messageSummaries.length).toBe(1); - expect(threadSpy).toHaveBeenCalled(); - - component.showAllThreads(); - - expect(component.feedbackThreadActive).toBe(false); - expect(component.numberOfUnreadThreads).toBe(6); - })); + expect(component.feedbackThreadActive).toBe(false); + expect(component.numberOfUnreadThreads).toBe(6); + })); - it('should add a new message in a thread when there is a thread selected', - fakeAsync(() => { - let threadStatus = 'open'; - let explorationId = 'exp1'; - let threadId = 'thread_1'; - let explorationTitle = 'Exploration Title'; - let message = 'This is a new message'; - let threadMessages = [{ + it('should add a new message in a thread when there is a thread selected', fakeAsync(() => { + let threadStatus = 'open'; + let explorationId = 'exp1'; + let threadId = 'thread_1'; + let explorationTitle = 'Exploration Title'; + let message = 'This is a new message'; + let threadMessages = [ + { message_id: 1, text: 'Feedback 1', updated_status: 'open', @@ -657,44 +755,51 @@ describe('Feedback updates page', () => { current_content_html: 'A orange', description: 'Suggestion for english grammar', author_username: 'username2', - created_on_msecs: 1200 - }]; + created_on_msecs: 1200, + }, + ]; - const threadSpy = spyOn( - feedbackUpdatesBackendApiService, 'onClickThreadAsync') - .and.returnValue(Promise.resolve(threadMessages)); + const threadSpy = spyOn( + feedbackUpdatesBackendApiService, + 'onClickThreadAsync' + ).and.returnValue(Promise.resolve(threadMessages)); - const addMessageSpy = spyOn( - feedbackUpdatesBackendApiService, 'addNewMessageAsync') - .and.returnValue(Promise.resolve()); + const addMessageSpy = spyOn( + feedbackUpdatesBackendApiService, + 'addNewMessageAsync' + ).and.returnValue(Promise.resolve()); - expect(component.numberOfUnreadThreads).toBe(10); - expect(component.loadingFeedback).toBe(false); + expect(component.numberOfUnreadThreads).toBe(10); + expect(component.loadingFeedback).toBe(false); - component.onClickThread( - threadStatus, explorationId, threadId, explorationTitle); + component.onClickThread( + threadStatus, + explorationId, + threadId, + explorationTitle + ); - expect(component.loadingFeedback).toBe(true); + expect(component.loadingFeedback).toBe(true); - tick(); - fixture.detectChanges(); + tick(); + fixture.detectChanges(); - expect(component.loadingFeedback).toBe(false); - expect(component.feedbackThreadActive).toBe(true); - expect(component.numberOfUnreadThreads).toBe(6); - expect(component.messageSummaries.length).toBe(1); - expect(threadSpy).toHaveBeenCalled(); + expect(component.loadingFeedback).toBe(false); + expect(component.feedbackThreadActive).toBe(true); + expect(component.numberOfUnreadThreads).toBe(6); + expect(component.messageSummaries.length).toBe(1); + expect(threadSpy).toHaveBeenCalled(); - component.addNewMessage(threadId, message); + component.addNewMessage(threadId, message); - expect(component.messageSendingInProgress).toBe(true); + expect(component.messageSendingInProgress).toBe(true); - tick(); - fixture.detectChanges(); + tick(); + fixture.detectChanges(); - expect(component.messageSendingInProgress).toBe(false); - expect(addMessageSpy).toHaveBeenCalled(); - })); + expect(component.messageSendingInProgress).toBe(false); + expect(addMessageSpy).toHaveBeenCalled(); + })); it('should get css classes based on status', () => { expect(component.getLabelClass('open')).toBe('badge badge-info'); @@ -706,19 +811,21 @@ describe('Feedback updates page', () => { expect(component.getHumanReadableStatus('open')).toBe('Open'); expect(component.getHumanReadableStatus('compliment')).toBe('Compliment'); expect(component.getHumanReadableStatus('not_actionable')).toBe( - 'Not Actionable'); + 'Not Actionable' + ); }); - it('should get formatted date string from the timestamp in milliseconds', - () => { - // This corresponds to Fri, 2 Apr 2021 09:45:00 GMT. - let NOW_MILLIS = 1617393321345; - spyOn(dateTimeFormatService, 'getLocaleAbbreviatedDatetimeString') - .withArgs(NOW_MILLIS).and.returnValue('4/2/2021'); + it('should get formatted date string from the timestamp in milliseconds', () => { + // This corresponds to Fri, 2 Apr 2021 09:45:00 GMT. + let NOW_MILLIS = 1617393321345; + spyOn(dateTimeFormatService, 'getLocaleAbbreviatedDatetimeString') + .withArgs(NOW_MILLIS) + .and.returnValue('4/2/2021'); - expect(component.getLocaleAbbreviatedDatetimeString(NOW_MILLIS)) - .toBe('4/2/2021'); - }); + expect(component.getLocaleAbbreviatedDatetimeString(NOW_MILLIS)).toBe( + '4/2/2021' + ); + }); it('should sanitize given png base64 data and generate url', () => { let result = component.decodePngURIData('%D1%88%D0%B5%D0%BB%D0%BB%D1%8B'); @@ -736,7 +843,7 @@ describe('Feedback updates page', () => { BrowserAnimationsModule, MaterialModule, FormsModule, - HttpClientTestingModule + HttpClientTestingModule, ], declarations: [ FeedbackUpdatesPageComponent, @@ -755,10 +862,10 @@ describe('Feedback updates page', () => { PageTitleService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -767,44 +874,49 @@ describe('Feedback updates page', () => { component = fixture.componentInstance; alertsService = TestBed.inject(AlertsService); csrfTokenService = TestBed.inject(CsrfTokenService); - feedbackUpdatesBackendApiService = - TestBed.inject(FeedbackUpdatesBackendApiService); + feedbackUpdatesBackendApiService = TestBed.inject( + FeedbackUpdatesBackendApiService + ); userService = TestBed.inject(UserService); translateService = TestBed.inject(TranslateService); pageTitleService = TestBed.inject(PageTitleService); spyOn(csrfTokenService, 'getTokenAsync').and.returnValue( - Promise.resolve('sample-csrf-token')); + Promise.resolve('sample-csrf-token') + ); - spyOn(userService, 'getProfileImageDataUrl').and.returnValue( - ['default-image-url-png', 'default-image-url-webp']); + spyOn(userService, 'getProfileImageDataUrl').and.returnValue([ + 'default-image-url-png', + 'default-image-url-webp', + ]); spyOn(userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(userInfo as UserInfo)); + Promise.resolve(userInfo as UserInfo) + ); })); afterEach(() => { component.ngOnDestroy(); }); - it('should show an alert warning when fails to get feedback updates data', - fakeAsync(() => { - const fetchDataSpy = spyOn( - feedbackUpdatesBackendApiService, - 'fetchFeedbackUpdatesDataAsync') - .and.rejectWith(404); - const alertsSpy = spyOn(alertsService, 'addWarning').and.callThrough(); + it('should show an alert warning when fails to get feedback updates data', fakeAsync(() => { + const fetchDataSpy = spyOn( + feedbackUpdatesBackendApiService, + 'fetchFeedbackUpdatesDataAsync' + ).and.rejectWith(404); + const alertsSpy = spyOn(alertsService, 'addWarning').and.callThrough(); - component.ngOnInit(); + component.ngOnInit(); - tick(1000); - fixture.detectChanges(); + tick(1000); + fixture.detectChanges(); - expect(alertsSpy).toHaveBeenCalledWith( - 'Failed to get feedback updates data'); - expect(fetchDataSpy).toHaveBeenCalled(); - flush(); - })); + expect(alertsSpy).toHaveBeenCalledWith( + 'Failed to get feedback updates data' + ); + expect(fetchDataSpy).toHaveBeenCalled(); + flush(); + })); it('should unsubscribe upon component destruction', () => { spyOn(component.directiveSubscriptions, 'unsubscribe'); diff --git a/core/templates/pages/feedback-updates-page/feedback-updates-page.component.ts b/core/templates/pages/feedback-updates-page/feedback-updates-page.component.ts index 93582ac65e7f..703f36c63d2f 100644 --- a/core/templates/pages/feedback-updates-page/feedback-updates-page.component.ts +++ b/core/templates/pages/feedback-updates-page/feedback-updates-page.component.ts @@ -12,34 +12,42 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Component for the feedback Updates page. */ -import { Component, OnInit, OnDestroy } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { trigger, state, style, transition, - animate, group } from '@angular/animations'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; - -import { AppConstants } from 'app.constants'; -import { FeedbackThreadSummary, FeedbackThreadSummaryBackendDict } from 'domain/feedback_thread/feedback-thread-summary.model'; -import { FeedbackMessageSummary } from 'domain/feedback_message/feedback-message-summary.model'; -import { FeedbackUpdatesBackendApiService } from 'domain/feedback_updates/feedback-updates-backend-api.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { ThreadStatusDisplayService } from 'pages/exploration-editor-page/feedback-tab/services/thread-status-display.service'; -import { FeedbackUpdatesPageConstants } from 'pages/feedback-updates-page/feedback-updates-page.constants'; -import { AlertsService } from 'services/alerts.service'; -import { DateTimeFormatService } from 'services/date-time-format.service'; -import { LoaderService } from 'services/loader.service'; -import { UserService } from 'services/user.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { PageTitleService } from 'services/page-title.service'; -import { UrlService } from 'services/contextual/url.service'; +import {Component, OnInit, OnDestroy} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import { + trigger, + state, + style, + transition, + animate, + group, +} from '@angular/animations'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; + +import {AppConstants} from 'app.constants'; +import { + FeedbackThreadSummary, + FeedbackThreadSummaryBackendDict, +} from 'domain/feedback_thread/feedback-thread-summary.model'; +import {FeedbackMessageSummary} from 'domain/feedback_message/feedback-message-summary.model'; +import {FeedbackUpdatesBackendApiService} from 'domain/feedback_updates/feedback-updates-backend-api.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {ThreadStatusDisplayService} from 'pages/exploration-editor-page/feedback-tab/services/thread-status-display.service'; +import {FeedbackUpdatesPageConstants} from 'pages/feedback-updates-page/feedback-updates-page.constants'; +import {AlertsService} from 'services/alerts.service'; +import {DateTimeFormatService} from 'services/date-time-format.service'; +import {LoaderService} from 'services/loader.service'; +import {UserService} from 'services/user.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {PageTitleService} from 'services/page-title.service'; +import {UrlService} from 'services/contextual/url.service'; import './feedback-updates-page.component.css'; @@ -49,46 +57,75 @@ import './feedback-updates-page.component.css'; styleUrls: ['./feedback-updates-page.component.css'], animations: [ trigger('slideInOut', [ - state('true', style({ - 'max-height': '500px', opacity: '1', visibility: 'visible' - })), - state('false', style({ - 'max-height': '0px', opacity: '0', visibility: 'hidden' - })), - transition('true => false', [group([ - animate('500ms ease-in-out', style({ - opacity: '0' - })), - animate('500ms ease-in-out', style({ - 'max-height': '0px' - })), - animate('500ms ease-in-out', style({ - visibility: 'hidden' - })) - ] - )]), - transition('false => true', [group([ - animate('500ms ease-in-out', style({ - visibility: 'visible' - })), - animate('500ms ease-in-out', style({ - 'max-height': '500px' - })), - animate('500ms ease-in-out', style({ - opacity: '1' - })) - ] - )]) - ]) - ] + state( + 'true', + style({ + 'max-height': '500px', + opacity: '1', + visibility: 'visible', + }) + ), + state( + 'false', + style({ + 'max-height': '0px', + opacity: '0', + visibility: 'hidden', + }) + ), + transition('true => false', [ + group([ + animate( + '500ms ease-in-out', + style({ + opacity: '0', + }) + ), + animate( + '500ms ease-in-out', + style({ + 'max-height': '0px', + }) + ), + animate( + '500ms ease-in-out', + style({ + visibility: 'hidden', + }) + ), + ]), + ]), + transition('false => true', [ + group([ + animate( + '500ms ease-in-out', + style({ + visibility: 'visible', + }) + ), + animate( + '500ms ease-in-out', + style({ + 'max-height': '500px', + }) + ), + animate( + '500ms ease-in-out', + style({ + opacity: '1', + }) + ), + ]), + ]), + ]), + ], }) export class FeedbackUpdatesPageComponent implements OnInit, OnDestroy { - FEEDBACK_THREADS_SORT_BY_KEYS_AND_I18N_IDS = ( - FeedbackUpdatesPageConstants.FEEDBACK_THREADS_SORT_BY_KEYS_AND_I18N_IDS); + FEEDBACK_THREADS_SORT_BY_KEYS_AND_I18N_IDS = + FeedbackUpdatesPageConstants.FEEDBACK_THREADS_SORT_BY_KEYS_AND_I18N_IDS; username: string = ''; - PAGES_REGISTERED_WITH_FRONTEND = ( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND); + PAGES_REGISTERED_WITH_FRONTEND = AppConstants.PAGES_REGISTERED_WITH_FRONTEND; DEFAULT_CLASSROOM_URL_FRAGMENT = AppConstants.DEFAULT_CLASSROOM_URL_FRAGMENT; @@ -107,7 +144,7 @@ export class FeedbackUpdatesPageComponent implements OnInit, OnDestroy { profilePicturePngDataUrl!: string; profilePictureWebpDataUrl!: string; newMessage!: { - 'text': string | null; + text: string | null; }; loadingFeedback!: boolean; @@ -117,8 +154,8 @@ export class FeedbackUpdatesPageComponent implements OnInit, OnDestroy { threadId!: string; messageSummaries!: FeedbackMessageSummary[]; threadSummary!: FeedbackThreadSummary; - communityLibraryUrl = ( - '/' + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LIBRARY_INDEX.ROUTE); + communityLibraryUrl = + '/' + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LIBRARY_INDEX.ROUTE; loadingIndicatorIsShown: boolean = false; homeImageUrl: string = ''; @@ -133,8 +170,7 @@ export class FeedbackUpdatesPageComponent implements OnInit, OnDestroy { private dateTimeFormatService: DateTimeFormatService, private focusManagerService: FocusManagerService, private i18nLanguageCodeService: I18nLanguageCodeService, - private feedbackUpdatesBackendApiService: - FeedbackUpdatesBackendApiService, + private feedbackUpdatesBackendApiService: FeedbackUpdatesBackendApiService, private loaderService: LoaderService, private threadStatusDisplayService: ThreadStatusDisplayService, private urlInterpolationService: UrlInterpolationService, @@ -152,43 +188,46 @@ export class FeedbackUpdatesPageComponent implements OnInit, OnDestroy { const username = userInfo.getUsername(); if (username) { this.username = username; - [this.profilePicturePngDataUrl, this.profilePictureWebpDataUrl] = ( - this.userService.getProfileImageDataUrl(username)); + [this.profilePicturePngDataUrl, this.profilePictureWebpDataUrl] = + this.userService.getProfileImageDataUrl(username); } else { - this.profilePictureWebpDataUrl = ( + this.profilePictureWebpDataUrl = this.urlInterpolationService.getStaticImageUrl( - AppConstants.DEFAULT_PROFILE_IMAGE_WEBP_PATH)); - this.profilePicturePngDataUrl = ( + AppConstants.DEFAULT_PROFILE_IMAGE_WEBP_PATH + ); + this.profilePicturePngDataUrl = this.urlInterpolationService.getStaticImageUrl( - AppConstants.DEFAULT_PROFILE_IMAGE_PNG_PATH)); + AppConstants.DEFAULT_PROFILE_IMAGE_PNG_PATH + ); } }); this.fetchFeedbackUpdates(); - Promise.all([ - userInfoPromise, - ]).then(() => { - setTimeout(() => { - this.loaderService.hideLoadingScreen(); - // This focus is applied after the loading screen has disappeared. - this.focusManagerService.setFocusWithoutScroll('ourLessonsBtn'); - }, 0); - }).catch(errorResponse => { - // This is placed here in order to satisfy Unit tests. - }); + Promise.all([userInfoPromise]) + .then(() => { + setTimeout(() => { + this.loaderService.hideLoadingScreen(); + // This focus is applied after the loading screen has disappeared. + this.focusManagerService.setFocusWithoutScroll('ourLessonsBtn'); + }, 0); + }) + .catch(errorResponse => { + // This is placed here in order to satisfy Unit tests. + }); this.loadingFeedback = false; this.newMessage = { - text: '' + text: '', }; this.windowIsNarrow = this.windowDimensionService.isWindowNarrow(); this.directiveSubscriptions.add( this.windowDimensionService.getResizeEvent().subscribe(() => { this.windowIsNarrow = this.windowDimensionService.isWindowNarrow(); - })); + }) + ); this.directiveSubscriptions.add( this.translateService.onLangChange.subscribe(() => { this.setPageTitle(); @@ -201,49 +240,48 @@ export class FeedbackUpdatesPageComponent implements OnInit, OnDestroy { } getauthorPicturePngDataUrl(username: string): string { - let [pngImageUrl, _] = this.userService.getProfileImageDataUrl( - username); + let [pngImageUrl, _] = this.userService.getProfileImageDataUrl(username); return pngImageUrl; } getauthorPictureWebpDataUrl(username: string): string { - let [_, webpImageUrl] = this.userService.getProfileImageDataUrl( - username); + let [_, webpImageUrl] = this.userService.getProfileImageDataUrl(username); return webpImageUrl; } setPageTitle(): void { let translatedTitle = this.translateService.instant( - 'I18N_FEEDBACK_UPDATES_PAGE_TITLE'); + 'I18N_FEEDBACK_UPDATES_PAGE_TITLE' + ); this.pageTitleService.setDocumentTitle(translatedTitle); } fetchFeedbackUpdates(): void { this.loadingIndicatorIsShown = true; - let dashboardFeedbackUpdatesDataPromise = ( - this.feedbackUpdatesBackendApiService - .fetchFeedbackUpdatesDataAsync( - this.paginatedThreadsList)); + let dashboardFeedbackUpdatesDataPromise = + this.feedbackUpdatesBackendApiService.fetchFeedbackUpdatesDataAsync( + this.paginatedThreadsList + ); dashboardFeedbackUpdatesDataPromise.then( responseData => { this.isCurrentFeedbackSortDescending = true; - this.currentFeedbackThreadsSortType = ( - FeedbackUpdatesPageConstants - .FEEDBACK_THREADS_SORT_BY_KEYS_AND_I18N_IDS.LAST_UPDATED.key); + this.currentFeedbackThreadsSortType = + FeedbackUpdatesPageConstants.FEEDBACK_THREADS_SORT_BY_KEYS_AND_I18N_IDS.LAST_UPDATED.key; this.threadSummaries = [ - ... this.threadSummaries, - ... responseData.threadSummaries]; + ...this.threadSummaries, + ...responseData.threadSummaries, + ]; this.paginatedThreadsList = responseData.paginatedThreadsList; - this.numberOfUnreadThreads = - responseData.numberOfUnreadThreads; + this.numberOfUnreadThreads = responseData.numberOfUnreadThreads; this.feedbackThreadActive = false; this.loadingIndicatorIsShown = false; - }, errorResponseStatus => { + }, + errorResponseStatus => { this.loadingIndicatorIsShown = false; if ( - AppConstants.FATAL_ERROR_CODES.indexOf(errorResponseStatus) !== -1) { - this.alertsService.addWarning( - 'Failed to get feedback updates data'); + AppConstants.FATAL_ERROR_CODES.indexOf(errorResponseStatus) !== -1 + ) { + this.alertsService.addWarning('Failed to get feedback updates data'); } } ); @@ -266,8 +304,8 @@ export class FeedbackUpdatesPageComponent implements OnInit, OnDestroy { setFeedbackSortingOptions(sortType: string): void { if (sortType === this.currentFeedbackThreadsSortType) { - this.isCurrentFeedbackSortDescending = ( - !this.isCurrentFeedbackSortDescending); + this.isCurrentFeedbackSortDescending = + !this.isCurrentFeedbackSortDescending; } else { this.currentFeedbackThreadsSortType = sortType; } @@ -281,13 +319,18 @@ export class FeedbackUpdatesPageComponent implements OnInit, OnDestroy { } onClickThread( - threadStatus: string, explorationId: string, - threadId: string, explorationTitle: string): void { + threadStatus: string, + explorationId: string, + threadId: string, + explorationTitle: string + ): void { this.loadingFeedback = true; let threadDataUrl = this.urlInterpolationService.interpolateUrl( - '/feedbackupdatesthreadhandler/', { - threadId: threadId - }); + '/feedbackupdatesthreadhandler/', + { + threadId: threadId, + } + ); this.explorationTitle = explorationTitle; this.feedbackThreadActive = true; this.threadStatus = threadStatus; @@ -305,14 +348,17 @@ export class FeedbackUpdatesPageComponent implements OnInit, OnDestroy { } } - this.feedbackUpdatesBackendApiService.onClickThreadAsync(threadDataUrl) - .then((messageSummaryList) => { + this.feedbackUpdatesBackendApiService + .onClickThreadAsync(threadDataUrl) + .then(messageSummaryList => { let messageSummaryDicts = messageSummaryList; this.messageSummaries = []; for (let index = 0; index < messageSummaryDicts.length; index++) { this.messageSummaries.push( FeedbackMessageSummary.createFromBackendDict( - messageSummaryDicts[index])); + messageSummaryDicts[index] + ) + ); } this.loadingFeedback = false; }); @@ -324,26 +370,29 @@ export class FeedbackUpdatesPageComponent implements OnInit, OnDestroy { addNewMessage(threadId: string, newMessage: string): void { let url = this.urlInterpolationService.interpolateUrl( - '/threadhandler/', { - threadId: threadId - }); + '/threadhandler/', + { + threadId: threadId, + } + ); let payload = { updated_status: false, updated_subject: null, - text: newMessage + text: newMessage, }; this.messageSendingInProgress = true; this.feedbackUpdatesBackendApiService - .addNewMessageAsync(url, payload).then(() => { + .addNewMessageAsync(url, payload) + .then(() => { this.threadSummary = this.threadSummaries[this.threadIndex]; - this.threadSummary.appendNewMessage( - newMessage, this.username); + this.threadSummary.appendNewMessage(newMessage, this.username); this.messageSendingInProgress = false; this.newMessage.text = null; - let newMessageSummary = ( - FeedbackMessageSummary.createNewMessage( - this.threadSummary.totalMessageCount, newMessage, - this.username)); + let newMessageSummary = FeedbackMessageSummary.createNewMessage( + this.threadSummary.totalMessageCount, + newMessage, + this.username + ); this.messageSummaries.push(newMessageSummary); }); } @@ -358,7 +407,8 @@ export class FeedbackUpdatesPageComponent implements OnInit, OnDestroy { getLocaleAbbreviatedDatetimeString(millisSinceEpoch: number): string { return this.dateTimeFormatService.getLocaleAbbreviatedDatetimeString( - millisSinceEpoch); + millisSinceEpoch + ); } decodePngURIData(base64ImageData: string): string { @@ -366,7 +416,9 @@ export class FeedbackUpdatesPageComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive('oppiaFeedbackUpdatesPage', +angular.module('oppia').directive( + 'oppiaFeedbackUpdatesPage', downgradeComponent({ - component: FeedbackUpdatesPageComponent - }) as angular.IDirectiveFactory); + component: FeedbackUpdatesPageComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/feedback-updates-page/feedback-updates-page.constants.ts b/core/templates/pages/feedback-updates-page/feedback-updates-page.constants.ts index 595f00af73cc..8593b68bb05d 100644 --- a/core/templates/pages/feedback-updates-page/feedback-updates-page.constants.ts +++ b/core/templates/pages/feedback-updates-page/feedback-updates-page.constants.ts @@ -20,11 +20,11 @@ export const FeedbackUpdatesPageConstants = { FEEDBACK_THREADS_SORT_BY_KEYS_AND_I18N_IDS: { LAST_UPDATED: { key: 'lastUpdatedMsecs', - i18nId: 'I18N_DASHBOARD_EXPLORATIONS_SORT_BY_LAST_UPDATED' + i18nId: 'I18N_DASHBOARD_EXPLORATIONS_SORT_BY_LAST_UPDATED', }, EXPLORATION: { key: 'explorationTitle', - i18nId: 'I18N_DASHBOARD_TABLE_HEADING_EXPLORATION' - } - } + i18nId: 'I18N_DASHBOARD_TABLE_HEADING_EXPLORATION', + }, + }, } as const; diff --git a/core/templates/pages/feedback-updates-page/feedback-updates-page.module.ts b/core/templates/pages/feedback-updates-page/feedback-updates-page.module.ts index 6a1979e67e5d..3d1af7662cf0 100644 --- a/core/templates/pages/feedback-updates-page/feedback-updates-page.module.ts +++ b/core/templates/pages/feedback-updates-page/feedback-updates-page.module.ts @@ -16,16 +16,16 @@ * @fileoverview Module for the feedback-updates page. */ -import { NgModule } from '@angular/core'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { FeedbackUpdatesPageComponent } from './feedback-updates-page.component'; -import { ReactiveFormsModule } from '@angular/forms'; -import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'; -import { FeedbackUpdatesPageRootComponent } from './feedback-updates-page-root.component'; -import { CommonModule } from '@angular/common'; -import { FeedbackUpdatesPageRoutingModule } from './feedback-updates-page-routing.module'; -import { Error404PageModule } from 'pages/error-pages/error-404/error-404-page.module'; -import { SmartRouterModule } from 'hybrid-router-module-provider'; +import {NgModule} from '@angular/core'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {FeedbackUpdatesPageComponent} from './feedback-updates-page.component'; +import {ReactiveFormsModule} from '@angular/forms'; +import {NgbPopoverModule} from '@ng-bootstrap/ng-bootstrap'; +import {FeedbackUpdatesPageRootComponent} from './feedback-updates-page-root.component'; +import {CommonModule} from '@angular/common'; +import {FeedbackUpdatesPageRoutingModule} from './feedback-updates-page-routing.module'; +import {Error404PageModule} from 'pages/error-pages/error-404/error-404-page.module'; +import {SmartRouterModule} from 'hybrid-router-module-provider'; @NgModule({ imports: [ @@ -37,15 +37,15 @@ import { SmartRouterModule } from 'hybrid-router-module-provider'; ReactiveFormsModule, SharedComponentsModule, FeedbackUpdatesPageRoutingModule, - Error404PageModule + Error404PageModule, ], declarations: [ FeedbackUpdatesPageComponent, - FeedbackUpdatesPageRootComponent + FeedbackUpdatesPageRootComponent, ], entryComponents: [ FeedbackUpdatesPageComponent, - FeedbackUpdatesPageRootComponent - ] + FeedbackUpdatesPageRootComponent, + ], }) export class FeedbackUpdatesPageModule {} diff --git a/core/templates/pages/get-started-page/get-started-page-root.component.spec.ts b/core/templates/pages/get-started-page/get-started-page-root.component.spec.ts index 53c6a25129d0..464e38efb231 100644 --- a/core/templates/pages/get-started-page/get-started-page-root.component.spec.ts +++ b/core/templates/pages/get-started-page/get-started-page-root.component.spec.ts @@ -16,15 +16,15 @@ * @fileoverview Unit tests for the get started page root component. */ -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { GetStartedPageRootComponent } from './get-started-page-root.component'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {GetStartedPageRootComponent} from './get-started-page-root.component'; class MockTranslateService { onLangChange: EventEmitter = new EventEmitter(); @@ -41,18 +41,15 @@ describe('Get Started Page Root', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - GetStartedPageRootComponent, - MockTranslatePipe - ], + declarations: [GetStartedPageRootComponent, MockTranslatePipe], providers: [ PageHeadService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -63,10 +60,9 @@ describe('Get Started Page Root', () => { translateService = TestBed.inject(TranslateService); }); - it('should successfully instantiate the component', - () => { - expect(component).toBeDefined(); - }); + it('should successfully instantiate the component', () => { + expect(component).toBeDefined(); + }); it('should initialize and subscribe to onLangChange', () => { spyOn(translateService.onLangChange, 'subscribe'); @@ -92,10 +88,12 @@ describe('Get Started Page Root', () => { component.setPageTitleAndMetaTags(); expect(translateService.instant).toHaveBeenCalledWith( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.GET_STARTED.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.GET_STARTED.TITLE + ); expect(pageHeadService.updateTitleAndMetaTags).toHaveBeenCalledWith( AppConstants.PAGES_REGISTERED_WITH_FRONTEND.GET_STARTED.TITLE, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.GET_STARTED.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.GET_STARTED.META + ); }); it('should unsubscribe on component destruction', () => { diff --git a/core/templates/pages/get-started-page/get-started-page-root.component.ts b/core/templates/pages/get-started-page/get-started-page-root.component.ts index f6e35282cd40..7492b989ec18 100644 --- a/core/templates/pages/get-started-page/get-started-page-root.component.ts +++ b/core/templates/pages/get-started-page/get-started-page-root.component.ts @@ -16,16 +16,16 @@ * @fileoverview Root component for get started page. */ -import { Component, OnDestroy } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; +import {Component, OnDestroy} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; @Component({ selector: 'oppia-get-started-page-root', - templateUrl: './get-started-page-root.component.html' + templateUrl: './get-started-page-root.component.html', }) export class GetStartedPageRootComponent implements OnDestroy { directiveSubscriptions = new Subscription(); @@ -36,10 +36,12 @@ export class GetStartedPageRootComponent implements OnDestroy { setPageTitleAndMetaTags(): void { let translatedTitle = this.translateService.instant( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.GET_STARTED.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.GET_STARTED.TITLE + ); this.pageHeadService.updateTitleAndMetaTags( translatedTitle, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.GET_STARTED.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.GET_STARTED.META + ); } ngOnInit(): void { diff --git a/core/templates/pages/get-started-page/get-started-page-routing.module.ts b/core/templates/pages/get-started-page/get-started-page-routing.module.ts index 5e61b828e4c6..6f4408521d14 100644 --- a/core/templates/pages/get-started-page/get-started-page-routing.module.ts +++ b/core/templates/pages/get-started-page/get-started-page-routing.module.ts @@ -16,24 +16,19 @@ * @fileoverview Routing module for get started page. */ -import { NgModule } from '@angular/core'; -import { Route, RouterModule } from '@angular/router'; -import { GetStartedPageRootComponent } from './get-started-page-root.component'; +import {NgModule} from '@angular/core'; +import {Route, RouterModule} from '@angular/router'; +import {GetStartedPageRootComponent} from './get-started-page-root.component'; const routes: Route[] = [ { path: '', - component: GetStartedPageRootComponent - } + component: GetStartedPageRootComponent, + }, ]; @NgModule({ - imports: [ - RouterModule.forChild(routes) - ], - exports: [ - RouterModule - ] + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], }) - export class GetStartedPageRoutingModule {} diff --git a/core/templates/pages/get-started-page/get-started-page.module.ts b/core/templates/pages/get-started-page/get-started-page.module.ts index 10cfb9b5d665..488dc16e676e 100644 --- a/core/templates/pages/get-started-page/get-started-page.module.ts +++ b/core/templates/pages/get-started-page/get-started-page.module.ts @@ -16,20 +16,14 @@ * @fileoverview Module for the get started page. */ -import { NgModule } from '@angular/core'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { GetStartedPageRootComponent } from './get-started-page-root.component'; -import { CommonModule } from '@angular/common'; -import { GetStartedPageRoutingModule } from './get-started-page-routing.module'; +import {NgModule} from '@angular/core'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {GetStartedPageRootComponent} from './get-started-page-root.component'; +import {CommonModule} from '@angular/common'; +import {GetStartedPageRoutingModule} from './get-started-page-routing.module'; @NgModule({ - imports: [ - CommonModule, - SharedComponentsModule, - GetStartedPageRoutingModule - ], - declarations: [ - GetStartedPageRootComponent - ] + imports: [CommonModule, SharedComponentsModule, GetStartedPageRoutingModule], + declarations: [GetStartedPageRootComponent], }) export class GetStartedPageModule {} diff --git a/core/templates/pages/interaction-specs.constants.ajs.ts b/core/templates/pages/interaction-specs.constants.ajs.ts index e53514e17e92..dbc75af49ee8 100644 --- a/core/templates/pages/interaction-specs.constants.ajs.ts +++ b/core/templates/pages/interaction-specs.constants.ajs.ts @@ -16,7 +16,8 @@ * @fileoverview Constant file for the INTERACTION_SPECS constant. */ -import { InteractionSpecsConstants } from 'pages/interaction-specs.constants'; +import {InteractionSpecsConstants} from 'pages/interaction-specs.constants'; -angular.module('oppia').constant( - 'INTERACTION_SPECS', InteractionSpecsConstants.INTERACTION_SPECS); +angular + .module('oppia') + .constant('INTERACTION_SPECS', InteractionSpecsConstants.INTERACTION_SPECS); diff --git a/core/templates/pages/interaction-specs.constants.ts b/core/templates/pages/interaction-specs.constants.ts index 679ed0ec90bb..d0d347b28912 100644 --- a/core/templates/pages/interaction-specs.constants.ts +++ b/core/templates/pages/interaction-specs.constants.ts @@ -19,7 +19,7 @@ import INTERACTION_SPECS from 'interactions/interaction_specs.json'; export const InteractionSpecsConstants = { - INTERACTION_SPECS: INTERACTION_SPECS + INTERACTION_SPECS: INTERACTION_SPECS, } as const; export type InteractionSpecsKey = keyof typeof INTERACTION_SPECS; diff --git a/core/templates/pages/landing-pages/topic-landing-page/topic-landing-page-root.component.ts b/core/templates/pages/landing-pages/topic-landing-page/topic-landing-page-root.component.ts index 899fad963022..c58216e23a0d 100644 --- a/core/templates/pages/landing-pages/topic-landing-page/topic-landing-page-root.component.ts +++ b/core/templates/pages/landing-pages/topic-landing-page/topic-landing-page-root.component.ts @@ -16,11 +16,11 @@ * @fileoverview Root component for topic landing psage. */ -import { Component } from '@angular/core'; +import {Component} from '@angular/core'; @Component({ selector: 'oppia-topic-landing-page-root', - templateUrl: './topic-landing-page-root.component.html' + templateUrl: './topic-landing-page-root.component.html', }) export class TopicLandingPageRootComponent { // Page title and meta tags are not required to be updated here diff --git a/core/templates/pages/landing-pages/topic-landing-page/topic-landing-page-routing.module.ts b/core/templates/pages/landing-pages/topic-landing-page/topic-landing-page-routing.module.ts index af04be443064..0f9958f527b3 100644 --- a/core/templates/pages/landing-pages/topic-landing-page/topic-landing-page-routing.module.ts +++ b/core/templates/pages/landing-pages/topic-landing-page/topic-landing-page-routing.module.ts @@ -16,24 +16,19 @@ * @fileoverview Routing module for topic landing page. */ -import { NgModule } from '@angular/core'; -import { Route, RouterModule } from '@angular/router'; -import { TopicLandingPageRootComponent } from './topic-landing-page-root.component'; +import {NgModule} from '@angular/core'; +import {Route, RouterModule} from '@angular/router'; +import {TopicLandingPageRootComponent} from './topic-landing-page-root.component'; const routes: Route[] = [ { path: '', - component: TopicLandingPageRootComponent - } + component: TopicLandingPageRootComponent, + }, ]; @NgModule({ - imports: [ - RouterModule.forChild(routes) - ], - exports: [ - RouterModule - ] + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], }) - export class TopicLandingPageRoutingModule {} diff --git a/core/templates/pages/landing-pages/topic-landing-page/topic-landing-page.component.spec.ts b/core/templates/pages/landing-pages/topic-landing-page/topic-landing-page.component.spec.ts index ae35a31c2a3b..6bf498669d04 100644 --- a/core/templates/pages/landing-pages/topic-landing-page/topic-landing-page.component.spec.ts +++ b/core/templates/pages/landing-pages/topic-landing-page/topic-landing-page.component.spec.ts @@ -16,20 +16,23 @@ * @fileoverview Unit tests for topicLandingPage. */ -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { ComponentFixture, TestBed, async, tick, fakeAsync } - from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; - -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; -import { TopicLandingPageComponent } from - 'pages/landing-pages/topic-landing-page/topic-landing-page.component'; -import { PageTitleService } from 'services/page-title.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; - -import { AppConstants } from 'app.constants'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import { + ComponentFixture, + TestBed, + async, + tick, + fakeAsync, +} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; + +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {TopicLandingPageComponent} from 'pages/landing-pages/topic-landing-page/topic-landing-page.component'; +import {PageTitleService} from 'services/page-title.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; + +import {AppConstants} from 'app.constants'; class MockWindowRef { _window = { @@ -47,9 +50,9 @@ class MockWindowRef { }, set href(val) { this._href = val; - } + }, }, - gtag: () => {} + gtag: () => {}, }; get nativeWindow() { @@ -85,12 +88,12 @@ describe('Topic Landing Page', () => { declarations: [TopicLandingPageComponent], providers: [ PageTitleService, - { provide: SiteAnalyticsService, useClass: MockSiteAnalyticsService }, + {provide: SiteAnalyticsService, useClass: MockSiteAnalyticsService}, UrlInterpolationService, - { provide: WindowRef, useValue: windowRef }, - {provide: TranslateService, useClass: MockTranslateService} + {provide: WindowRef, useValue: windowRef}, + {provide: TranslateService, useClass: MockTranslateService}, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -103,20 +106,24 @@ describe('Topic Landing Page', () => { fixture.detectChanges(); }); - it('should get topic title from topic id in pathname and subscribe to ' + - 'onLangChange emitterwhen component is initialized', () => { - spyOn(translateService.onLangChange, 'subscribe'); - windowRef.nativeWindow.location.pathname = '/math/ratios'; - component.ngOnInit(); - expect(component.topicTitle).toBe('Ratios'); - expect(translateService.onLangChange.subscribe).toHaveBeenCalled(); - }); + it( + 'should get topic title from topic id in pathname and subscribe to ' + + 'onLangChange emitterwhen component is initialized', + () => { + spyOn(translateService.onLangChange, 'subscribe'); + windowRef.nativeWindow.location.pathname = '/math/ratios'; + component.ngOnInit(); + expect(component.topicTitle).toBe('Ratios'); + expect(translateService.onLangChange.subscribe).toHaveBeenCalled(); + } + ); it('should click get started button', fakeAsync(() => { windowRef.nativeWindow.location.pathname = '/math/ratios'; let analyticsSpy = spyOn( - siteAnalyticsService, 'registerOpenCollectionFromLandingPageEvent') - .and.callThrough(); + siteAnalyticsService, + 'registerOpenCollectionFromLandingPageEvent' + ).and.callThrough(); // Get collection id from ratios. component.ngOnInit(); @@ -128,8 +135,7 @@ describe('Topic Landing Page', () => { tick(150); fixture.detectChanges(); - expect(windowRef.nativeWindow.location.href).toBe( - '/learn/math/ratios'); + expect(windowRef.nativeWindow.location.href).toBe('/learn/math/ratios'); })); it('should click learn more button', fakeAsync(() => { @@ -139,10 +145,11 @@ describe('Topic Landing Page', () => { fixture.detectChanges(); expect(windowRef.nativeWindow.location.href).toBe( - `/learn/${AppConstants.DEFAULT_CLASSROOM_URL_FRAGMENT}`); + `/learn/${AppConstants.DEFAULT_CLASSROOM_URL_FRAGMENT}` + ); })); - it('should return correct lesson quality image src', function() { + it('should return correct lesson quality image src', function () { let imageSrc = component.getLessonQualityImageSrc('someImage.png'); expect(imageSrc).toBe('/assets/images/landing/someImage.png'); @@ -150,14 +157,17 @@ describe('Topic Landing Page', () => { expect(imageSrc).toBe('/assets/images/landing/someOtherImage.png'); }); - it('should obtain translated page title whenever the selected' + - 'language changes', () => { - component.ngOnInit(); - spyOn(component, 'setPageTitle'); - translateService.onLangChange.emit(); + it( + 'should obtain translated page title whenever the selected' + + 'language changes', + () => { + component.ngOnInit(); + spyOn(component, 'setPageTitle'); + translateService.onLangChange.emit(); - expect(component.setPageTitle).toHaveBeenCalled(); - }); + expect(component.setPageTitle).toHaveBeenCalled(); + } + ); it('should set new page title', () => { spyOn(translateService, 'instant').and.callThrough(); @@ -167,17 +177,20 @@ describe('Topic Landing Page', () => { topicTitle: 'dummy_title', topicTagline: 'dummy_tagline', collectionId: 'dummy_collectionId', - chapters: ['chapter1', 'chapter2'] + chapters: ['chapter1', 'chapter2'], }; component.setPageTitle(); expect(translateService.instant).toHaveBeenCalledWith( - 'I18N_TOPIC_LANDING_PAGE_TITLE', { + 'I18N_TOPIC_LANDING_PAGE_TITLE', + { topicTitle: 'dummy_title', - topicTagline: 'dummy_tagline' - }); + topicTagline: 'dummy_tagline', + } + ); expect(pageTitleService.setDocumentTitle).toHaveBeenCalledWith( - 'I18N_TOPIC_LANDING_PAGE_TITLE'); + 'I18N_TOPIC_LANDING_PAGE_TITLE' + ); }); it('should unsubscribe on component destruction', () => { diff --git a/core/templates/pages/landing-pages/topic-landing-page/topic-landing-page.component.ts b/core/templates/pages/landing-pages/topic-landing-page/topic-landing-page.component.ts index 8ef8f1641039..5200c391b23b 100644 --- a/core/templates/pages/landing-pages/topic-landing-page/topic-landing-page.component.ts +++ b/core/templates/pages/landing-pages/topic-landing-page/topic-landing-page.component.ts @@ -18,27 +18,25 @@ require('base-components/base-content.component.ts'); -import { Component, OnInit, OnDestroy } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; +import {Component, OnInit, OnDestroy} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; -import { TopicLandingPageConstants } from - 'pages/landing-pages/topic-landing-page/topic-landing-page.constants'; -import { UrlService } from 'services/contextual/url.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { PageTitleService } from 'services/page-title.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {TopicLandingPageConstants} from 'pages/landing-pages/topic-landing-page/topic-landing-page.constants'; +import {UrlService} from 'services/contextual/url.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {PageTitleService} from 'services/page-title.service'; -import { AppConstants } from 'app.constants'; +import {AppConstants} from 'app.constants'; -type TopicLandingPageDataKey = ( - keyof typeof TopicLandingPageConstants.TOPIC_LANDING_PAGE_DATA); +type TopicLandingPageDataKey = + keyof typeof TopicLandingPageConstants.TOPIC_LANDING_PAGE_DATA; -type TopicLandingPageMathDataKey = ( - keyof typeof TopicLandingPageConstants.TOPIC_LANDING_PAGE_DATA.math); +type TopicLandingPageMathDataKey = + keyof typeof TopicLandingPageConstants.TOPIC_LANDING_PAGE_DATA.math; interface LessonsQuality { title: string; @@ -58,7 +56,7 @@ interface TopicData { @Component({ selector: 'topic-landing-page', templateUrl: './topic-landing-page.component.html', - styleUrls: [] + styleUrls: [], }) export class TopicLandingPageComponent implements OnInit, OnDestroy { // These properties are initialized using Angular lifecycle hooks @@ -82,38 +80,42 @@ export class TopicLandingPageComponent implements OnInit, OnDestroy { ) {} getLessonQualities(): LessonsQuality[] { - return [{ - title: 'Fun storytelling', - description: ( - 'Oppia\'s lessons tell stories using video and images to ' + - 'help learners apply math concepts in everyday life.'), - imagePngFilename: 'fun_storytelling.png', - imageWebpFilename: 'fun_storytelling.webp', - imageAlt: 'Storytelling lessons presentation.' - }, { - title: 'Accessible lessons', - description: ( - 'Our lessons come with audio translations in different ' + - 'languages, can be used on mobile phones, and don\'t require a ' + + return [ + { + title: 'Fun storytelling', + description: + "Oppia's lessons tell stories using video and images to " + + 'help learners apply math concepts in everyday life.', + imagePngFilename: 'fun_storytelling.png', + imageWebpFilename: 'fun_storytelling.webp', + imageAlt: 'Storytelling lessons presentation.', + }, + { + title: 'Accessible lessons', + description: + 'Our lessons come with audio translations in different ' + + "languages, can be used on mobile phones, and don't require a " + 'lot of data so that they can be used and enjoyed by anyone, ' + - 'anywhere.'), - imagePngFilename: 'accessible_lessons.png', - imageWebpFilename: 'accessible_lessons.webp', - imageAlt: 'Lesson accessibility presentation.' - }, { - title: 'Suitable for all', - description: ( - 'No matter your level, there\'s a lesson for you! From learning ' + - this.topicData.chapters[0].toLowerCase() + ', to ' + + 'anywhere.', + imagePngFilename: 'accessible_lessons.png', + imageWebpFilename: 'accessible_lessons.webp', + imageAlt: 'Lesson accessibility presentation.', + }, + { + title: 'Suitable for all', + description: + "No matter your level, there's a lesson for you! From learning " + + this.topicData.chapters[0].toLowerCase() + + ', to ' + this.topicData.chapters[1].toLowerCase() + - ' - Oppia has you covered.'), - imagePngFilename: 'suitable_for_all.png', - imageWebpFilename: 'suitable_for_all.webp', - imageAlt: 'Lesson viewers and learners.' - }]; + ' - Oppia has you covered.', + imagePngFilename: 'suitable_for_all.png', + imageWebpFilename: 'suitable_for_all.webp', + imageAlt: 'Lesson viewers and learners.', + }, + ]; } - ngOnInit(): void { let pathArray = this.windowRef.nativeWindow.location.pathname.split('/'); let subjectName = pathArray[1]; @@ -121,32 +123,32 @@ export class TopicLandingPageComponent implements OnInit, OnDestroy { this.topicData = TopicLandingPageConstants.TOPIC_LANDING_PAGE_DATA[ - subjectName as TopicLandingPageDataKey][ - topicName as TopicLandingPageMathDataKey]; + subjectName as TopicLandingPageDataKey + ][topicName as TopicLandingPageMathDataKey]; this.topicTitle = this.topicData.topicTitle; this.lessonsQualities = this.getLessonQualities(); - this.backgroundBannerUrl = ( - this.urlInterpolationService.getStaticImageUrl( - '/background/bannerB.svg')); + this.backgroundBannerUrl = this.urlInterpolationService.getStaticImageUrl( + '/background/bannerB.svg' + ); let topicImageUrlTemplate = '/landing///'; - this.lessonInDevicesPngImageSrc = ( + this.lessonInDevicesPngImageSrc = this.urlInterpolationService.getStaticImageUrl( - this.urlInterpolationService.interpolateUrl( - topicImageUrlTemplate, { - subject: subjectName, - topic: topicName, - filename: 'lesson_in_devices.png' - }))); - this.lessonInDevicesWebpImageSrc = ( + this.urlInterpolationService.interpolateUrl(topicImageUrlTemplate, { + subject: subjectName, + topic: topicName, + filename: 'lesson_in_devices.png', + }) + ); + this.lessonInDevicesWebpImageSrc = this.urlInterpolationService.getStaticImageUrl( - this.urlInterpolationService.interpolateUrl( - topicImageUrlTemplate, { - subject: subjectName, - topic: topicName, - filename: 'lesson_in_devices.webp' - }))); + this.urlInterpolationService.interpolateUrl(topicImageUrlTemplate, { + subject: subjectName, + topic: topicName, + filename: 'lesson_in_devices.webp', + }) + ); this.directiveSubscriptions.add( this.translateService.onLangChange.subscribe(() => { this.setPageTitle(); @@ -156,33 +158,36 @@ export class TopicLandingPageComponent implements OnInit, OnDestroy { getLessonQualityImageSrc(filename: string): string { return this.urlInterpolationService.getStaticImageUrl( - this.urlInterpolationService.interpolateUrl( - '/landing/', {filename: filename})); + this.urlInterpolationService.interpolateUrl('/landing/', { + filename: filename, + }) + ); } setPageTitle(): void { let translatedTitle = this.translateService.instant( - 'I18N_TOPIC_LANDING_PAGE_TITLE', { + 'I18N_TOPIC_LANDING_PAGE_TITLE', + { topicTitle: this.topicTitle, - topicTagline: this.topicData.topicTagline - }); + topicTagline: this.topicData.topicTagline, + } + ); this.pageTitleService.setDocumentTitle(translatedTitle); } onClickGetStartedButton(): void { let collectionId = this.topicData.collectionId; this.siteAnalyticsService.registerOpenCollectionFromLandingPageEvent( - collectionId); + collectionId + ); setTimeout(() => { - this.windowRef.nativeWindow.location.href = ( - `/learn${this.urlService.getPathname()}`); + this.windowRef.nativeWindow.location.href = `/learn${this.urlService.getPathname()}`; }, 150); } goToClassroom(): void { setTimeout(() => { - this.windowRef.nativeWindow.location.href = ( - `/learn/${AppConstants.DEFAULT_CLASSROOM_URL_FRAGMENT}`); + this.windowRef.nativeWindow.location.href = `/learn/${AppConstants.DEFAULT_CLASSROOM_URL_FRAGMENT}`; }, 150); } @@ -191,5 +196,9 @@ export class TopicLandingPageComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive('topicLandingPage', - downgradeComponent({component: TopicLandingPageComponent})); +angular + .module('oppia') + .directive( + 'topicLandingPage', + downgradeComponent({component: TopicLandingPageComponent}) + ); diff --git a/core/templates/pages/landing-pages/topic-landing-page/topic-landing-page.constants.ajs.ts b/core/templates/pages/landing-pages/topic-landing-page/topic-landing-page.constants.ajs.ts index 497273cc1ef0..806ded8508d7 100644 --- a/core/templates/pages/landing-pages/topic-landing-page/topic-landing-page.constants.ajs.ts +++ b/core/templates/pages/landing-pages/topic-landing-page/topic-landing-page.constants.ajs.ts @@ -18,10 +18,13 @@ // TODO(#7092): Delete this file once migration is complete and these AngularJS // equivalents of the Angular constants are no longer needed. -import { TopicLandingPageConstants } from - 'pages/landing-pages/topic-landing-page/topic-landing-page.constants'; +import {TopicLandingPageConstants} from 'pages/landing-pages/topic-landing-page/topic-landing-page.constants'; // Note: This oppia constant needs to be keep in sync with // AVAILABLE_LANDING_PAGES constant defined in feconf.py file. -angular.module('oppia').constant( - 'TOPIC_LANDING_PAGE_DATA', TopicLandingPageConstants.TOPIC_LANDING_PAGE_DATA); +angular + .module('oppia') + .constant( + 'TOPIC_LANDING_PAGE_DATA', + TopicLandingPageConstants.TOPIC_LANDING_PAGE_DATA + ); diff --git a/core/templates/pages/landing-pages/topic-landing-page/topic-landing-page.constants.ts b/core/templates/pages/landing-pages/topic-landing-page/topic-landing-page.constants.ts index e7659c262ffe..574ef5b137fe 100644 --- a/core/templates/pages/landing-pages/topic-landing-page/topic-landing-page.constants.ts +++ b/core/templates/pages/landing-pages/topic-landing-page/topic-landing-page.constants.ts @@ -26,21 +26,23 @@ export const TopicLandingPageConstants = { topicTitle: 'Fractions', topicTagline: 'Add, Subtract, Multiply and Divide', collectionId: '4UgTQUc1tala', - chapters: ['The meaning of equal parts', 'Comparing fractions'] + chapters: ['The meaning of equal parts', 'Comparing fractions'], }, 'negative-numbers': { topicTitle: 'Negative Numbers', topicTagline: 'Add, Subtract, Multiply and Divide', collectionId: 'GdYIgsfRZwG7', chapters: [ - 'The meaning of the number line', 'Calculating with negative numbers'] + 'The meaning of the number line', + 'Calculating with negative numbers', + ], }, ratios: { topicTitle: 'Ratios', topicTagline: 'Equivalent Ratios, Combining Ratios', collectionId: '53gXGLIR044l', - chapters: ['The meaning of equivalent ratios', 'Combining Ratios'] - } - } - } + chapters: ['The meaning of equivalent ratios', 'Combining Ratios'], + }, + }, + }, } as const; diff --git a/core/templates/pages/landing-pages/topic-landing-page/topic-landing-page.module.ts b/core/templates/pages/landing-pages/topic-landing-page/topic-landing-page.module.ts index 4079bd86cdd4..32a131523907 100644 --- a/core/templates/pages/landing-pages/topic-landing-page/topic-landing-page.module.ts +++ b/core/templates/pages/landing-pages/topic-landing-page/topic-landing-page.module.ts @@ -16,27 +16,20 @@ * @fileoverview Module for the topic landing page. */ -import { NgModule } from '@angular/core'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { TopicLandingPageComponent } from - 'pages/landing-pages/topic-landing-page/topic-landing-page.component'; -import { TopicLandingPageRootComponent } from './topic-landing-page-root.component'; -import { CommonModule } from '@angular/common'; -import { TopicLandingPageRoutingModule } from './topic-landing-page-routing.module'; +import {NgModule} from '@angular/core'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {TopicLandingPageComponent} from 'pages/landing-pages/topic-landing-page/topic-landing-page.component'; +import {TopicLandingPageRootComponent} from './topic-landing-page-root.component'; +import {CommonModule} from '@angular/common'; +import {TopicLandingPageRoutingModule} from './topic-landing-page-routing.module'; @NgModule({ imports: [ CommonModule, SharedComponentsModule, - TopicLandingPageRoutingModule + TopicLandingPageRoutingModule, ], - declarations: [ - TopicLandingPageComponent, - TopicLandingPageRootComponent - ], - entryComponents: [ - TopicLandingPageComponent, - TopicLandingPageRootComponent - ] + declarations: [TopicLandingPageComponent, TopicLandingPageRootComponent], + entryComponents: [TopicLandingPageComponent, TopicLandingPageRootComponent], }) export class TopicLandingPageModule {} diff --git a/core/templates/pages/learner-dashboard-page/community-lessons-tab.component.spec.ts b/core/templates/pages/learner-dashboard-page/community-lessons-tab.component.spec.ts index 64f90b634c49..679c14042ab4 100644 --- a/core/templates/pages/learner-dashboard-page/community-lessons-tab.component.spec.ts +++ b/core/templates/pages/learner-dashboard-page/community-lessons-tab.component.spec.ts @@ -16,28 +16,32 @@ * @fileoverview Unit tests for for CommunityLessonsTabComponent. */ -import { async, ComponentFixture, fakeAsync, TestBed } from - '@angular/core/testing'; -import { MaterialModule } from 'modules/material.module'; -import { FormsModule } from '@angular/forms'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { LearnerDashboardActivityBackendApiService } from 'domain/learner_dashboard/learner-dashboard-activity-backend-api.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; -import { LearnerExplorationSummary } from 'domain/summary/learner-exploration-summary.model'; -import { CollectionSummary } from 'domain/collection/collection-summary.model'; -import { CommunityLessonsTabComponent } from './community-lessons-tab.component'; -import { EventEmitter, NO_ERRORS_SCHEMA, Pipe } from '@angular/core'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { UserService } from 'services/user.service'; +import { + async, + ComponentFixture, + fakeAsync, + TestBed, +} from '@angular/core/testing'; +import {MaterialModule} from 'modules/material.module'; +import {FormsModule} from '@angular/forms'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {LearnerDashboardActivityBackendApiService} from 'domain/learner_dashboard/learner-dashboard-activity-backend-api.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {LearnerExplorationSummary} from 'domain/summary/learner-exploration-summary.model'; +import {CollectionSummary} from 'domain/collection/collection-summary.model'; +import {CommunityLessonsTabComponent} from './community-lessons-tab.component'; +import {EventEmitter, NO_ERRORS_SCHEMA, Pipe} from '@angular/core'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {UserService} from 'services/user.service'; class MockRemoveActivityNgbModalRef { componentInstance = { sectionNameI18nId: null, subsectionName: null, activityId: null, - activityTitle: null + activityTitle: null, }; } @@ -51,8 +55,7 @@ class MockTruncatePipe { describe('Community lessons tab Component', () => { let component: CommunityLessonsTabComponent; let fixture: ComponentFixture; - let learnerDashboardActivityBackendApiService: - LearnerDashboardActivityBackendApiService; + let learnerDashboardActivityBackendApiService: LearnerDashboardActivityBackendApiService; let ngbModal: NgbModal; let windowDimensionsService: WindowDimensionsService; let mockResizeEmitter: EventEmitter; @@ -65,12 +68,12 @@ describe('Community lessons tab Component', () => { BrowserAnimationsModule, MaterialModule, FormsModule, - HttpClientTestingModule + HttpClientTestingModule, ], declarations: [ CommunityLessonsTabComponent, MockTranslatePipe, - MockTruncatePipe + MockTruncatePipe, ], providers: [ LearnerDashboardActivityBackendApiService, @@ -79,18 +82,19 @@ describe('Community lessons tab Component', () => { useValue: { isWindowNarrow: () => true, getResizeEvent: () => mockResizeEmitter, - } + }, }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(CommunityLessonsTabComponent); component = fixture.componentInstance; - learnerDashboardActivityBackendApiService = - TestBed.inject(LearnerDashboardActivityBackendApiService); + learnerDashboardActivityBackendApiService = TestBed.inject( + LearnerDashboardActivityBackendApiService + ); ngbModal = TestBed.inject(NgbModal); windowDimensionsService = TestBed.inject(WindowDimensionsService); userService = TestBed.inject(UserService); @@ -103,13 +107,15 @@ describe('Community lessons tab Component', () => { component.subscriptionsList = []; component.completedToIncompleteCollections = []; - spyOn(userService, 'getProfileImageDataUrl').and.returnValue( - ['default-image-url-png', 'default-image-url-webp']); + spyOn(userService, 'getProfileImageDataUrl').and.returnValue([ + 'default-image-url-png', + 'default-image-url-webp', + ]); fixture.detectChanges(); }); - it ('should initilize values on init for web view', () => { + it('should initilize values on init for web view', () => { spyOn(windowDimensionsService, 'isWindowNarrow').and.returnValue(false); component.ngOnInit(); @@ -140,7 +146,7 @@ describe('Community lessons tab Component', () => { expect(component.displayLessonsInPlaylist).toEqual([]); }); - it ('should initilize values on init for mobile view', () => { + it('should initilize values on init for mobile view', () => { component.ngOnInit(); expect(component.displayLessonsInPlaylist).toEqual([]); @@ -190,16 +196,16 @@ describe('Community lessons tab Component', () => { 2: 0, 3: 0, 4: 0, - 5: 0 + 5: 0, }, status: 'public', tags: [], activity_type: 'exploration', category: 'Algebra', - title: 'Test Title' + title: 'Test Title', }; - let incompleteSummary = LearnerExplorationSummary.createFromBackendDict( - incomplete); + let incompleteSummary = + LearnerExplorationSummary.createFromBackendDict(incomplete); component.totalIncompleteLessonsList = [incompleteSummary]; let result = component.getLessonType(incompleteSummary); expect(result).toEqual('Incomplete'); @@ -220,16 +226,16 @@ describe('Community lessons tab Component', () => { 2: 0, 3: 0, 4: 0, - 5: 0 + 5: 0, }, status: 'public', tags: [], activity_type: 'exploration', category: 'Art', - title: 'Test Title 1' + title: 'Test Title 1', }; - let completedSummary = LearnerExplorationSummary.createFromBackendDict( - completed); + let completedSummary = + LearnerExplorationSummary.createFromBackendDict(completed); component.totalCompletedLessonsList = [completedSummary]; result = component.getLessonType(completedSummary); expect(result).toEqual('Completed'); @@ -237,12 +243,14 @@ describe('Community lessons tab Component', () => { it('should get user profile image png data url correctly', () => { expect(component.getProfileImagePngDataUrl('username')).toBe( - 'default-image-url-png'); + 'default-image-url-png' + ); }); it('should get user profile image webp data url correctly', () => { expect(component.getProfileImageWebpDataUrl('username')).toBe( - 'default-image-url-webp'); + 'default-image-url-webp' + ); }); it('should show username popover based on its length', () => { @@ -268,16 +276,15 @@ describe('Community lessons tab Component', () => { 2: 0, 3: 0, 4: 0, - 5: 0 + 5: 0, }, status: 'public', tags: [], activity_type: 'exploration', category: 'Algebra', - title: 'Test Title' + title: 'Test Title', }; - let summary1 = LearnerExplorationSummary.createFromBackendDict( - exp1); + let summary1 = LearnerExplorationSummary.createFromBackendDict(exp1); const exp2 = { last_updated_msec: 1591296737470.528, community_owned: false, @@ -294,16 +301,15 @@ describe('Community lessons tab Component', () => { 2: 0, 3: 0, 4: 0, - 5: 0 + 5: 0, }, status: 'public', tags: [], activity_type: 'exploration', category: 'Algebra', - title: 'Test Title' + title: 'Test Title', }; - let summary2 = LearnerExplorationSummary.createFromBackendDict( - exp2); + let summary2 = LearnerExplorationSummary.createFromBackendDict(exp2); const exp3 = { last_updated_msec: 1591296737470.528, community_owned: false, @@ -320,16 +326,15 @@ describe('Community lessons tab Component', () => { 2: 0, 3: 0, 4: 0, - 5: 0 + 5: 0, }, status: 'public', tags: [], activity_type: 'exploration', category: 'Algebra', - title: 'Test Title' + title: 'Test Title', }; - let summary3 = LearnerExplorationSummary.createFromBackendDict( - exp3); + let summary3 = LearnerExplorationSummary.createFromBackendDict(exp3); const exp4 = { last_updated_msec: 1591296737470.528, community_owned: false, @@ -346,51 +351,62 @@ describe('Community lessons tab Component', () => { 2: 0, 3: 0, 4: 0, - 5: 0 + 5: 0, }, status: 'public', tags: [], activity_type: 'exploration', category: 'Algebra', - title: 'Test Title' + title: 'Test Title', }; - let summary4 = LearnerExplorationSummary.createFromBackendDict( - exp4); + let summary4 = LearnerExplorationSummary.createFromBackendDict(exp4); component.totalIncompleteLessonsList = [ - summary1, summary2, summary3, summary4]; + summary1, + summary2, + summary3, + summary4, + ]; component.handleShowMore('incomplete'); expect(component.showMoreInSection.incomplete).toEqual(true); expect(component.displayIncompleteLessonsList).toEqual( - component.totalIncompleteLessonsList); + component.totalIncompleteLessonsList + ); component.showMoreInSection.incomplete = true; component.handleShowMore('incomplete'); expect(component.showMoreInSection.incomplete).toEqual(false); expect(component.displayIncompleteLessonsList).toEqual( - component.totalIncompleteLessonsList.slice(0, 3)); + component.totalIncompleteLessonsList.slice(0, 3) + ); component.totalCompletedLessonsList = [ - summary1, summary2, summary3, summary4]; + summary1, + summary2, + summary3, + summary4, + ]; component.totalIncompleteLessonsList = []; component.handleShowMore('completed'); expect(component.showMoreInSection.completed).toEqual(true); expect(component.displayCompletedLessonsList).toEqual( - component.totalCompletedLessonsList); + component.totalCompletedLessonsList + ); component.showMoreInSection.completed = true; component.handleShowMore('completed'); expect(component.showMoreInSection.completed).toEqual(false); expect(component.displayCompletedLessonsList).toEqual( - component.totalCompletedLessonsList.slice(0, 3)); + component.totalCompletedLessonsList.slice(0, 3) + ); - component.totalLessonsInPlaylist = [ - summary1, summary2, summary3, summary4]; + component.totalLessonsInPlaylist = [summary1, summary2, summary3, summary4]; component.totalCompletedLessonsList = []; component.handleShowMore('playlist'); expect(component.showMoreInSection.playlist).toEqual(true); expect(component.displayLessonsInPlaylist).toEqual( - component.totalLessonsInPlaylist); + component.totalLessonsInPlaylist + ); component.showMoreInSection.playlist = true; component.handleShowMore('playlist'); @@ -412,10 +428,9 @@ describe('Community lessons tab Component', () => { status: 'public', category: 'Algebra', title: 'Test Title', - node_count: 0 + node_count: 0, }; - let collectionSummary = CollectionSummary.createFromBackendDict( - collection); + let collectionSummary = CollectionSummary.createFromBackendDict(collection); const exploration = { last_updated_msec: 1591296737470.528, @@ -433,16 +448,16 @@ describe('Community lessons tab Component', () => { 2: 0, 3: 0, 4: 0, - 5: 0 + 5: 0, }, status: 'public', tags: [], activity_type: 'exploration', category: 'Algebra', - title: 'Test Title' + title: 'Test Title', }; - let explorationSummary = LearnerExplorationSummary.createFromBackendDict( - exploration); + let explorationSummary = + LearnerExplorationSummary.createFromBackendDict(exploration); let result = component.getTileType(explorationSummary); expect(result).toEqual('exploration'); @@ -451,7 +466,7 @@ describe('Community lessons tab Component', () => { expect(result).toEqual('collection'); }); - it ('should change page by one', () => { + it('should change page by one', () => { const exp1 = { last_updated_msec: 1591296737470.528, community_owned: false, @@ -468,16 +483,15 @@ describe('Community lessons tab Component', () => { 2: 0, 3: 0, 4: 0, - 5: 0 + 5: 0, }, status: 'public', tags: [], activity_type: 'exploration', category: 'Algebra', - title: 'Test Title' + title: 'Test Title', }; - let summary1 = LearnerExplorationSummary.createFromBackendDict( - exp1); + let summary1 = LearnerExplorationSummary.createFromBackendDict(exp1); const exp2 = { last_updated_msec: 1591296737470.528, community_owned: false, @@ -494,16 +508,15 @@ describe('Community lessons tab Component', () => { 2: 0, 3: 0, 4: 0, - 5: 0 + 5: 0, }, status: 'public', tags: [], activity_type: 'exploration', category: 'Algebra', - title: 'Test Title' + title: 'Test Title', }; - let summary2 = LearnerExplorationSummary.createFromBackendDict( - exp2); + let summary2 = LearnerExplorationSummary.createFromBackendDict(exp2); const exp3 = { last_updated_msec: 1591296737470.528, community_owned: false, @@ -520,16 +533,15 @@ describe('Community lessons tab Component', () => { 2: 0, 3: 0, 4: 0, - 5: 0 + 5: 0, }, status: 'public', tags: [], activity_type: 'exploration', category: 'Algebra', - title: 'Test Title' + title: 'Test Title', }; - let summary3 = LearnerExplorationSummary.createFromBackendDict( - exp3); + let summary3 = LearnerExplorationSummary.createFromBackendDict(exp3); const exp4 = { last_updated_msec: 1591296737470.528, community_owned: false, @@ -546,18 +558,21 @@ describe('Community lessons tab Component', () => { 2: 0, 3: 0, 4: 0, - 5: 0 + 5: 0, }, status: 'public', tags: [], activity_type: 'exploration', category: 'Algebra', - title: 'Test Title' + title: 'Test Title', }; - let summary4 = LearnerExplorationSummary.createFromBackendDict( - exp4); + let summary4 = LearnerExplorationSummary.createFromBackendDict(exp4); component.displayInCommunityLessons = [ - summary1, summary2, summary3, summary4]; + summary1, + summary2, + summary3, + summary4, + ]; component.changePageByOne('MOVE_TO_NEXT_PAGE', 'communityLessons'); expect(component.pageNumberInCommunityLessons).toEqual(2); @@ -567,7 +582,11 @@ describe('Community lessons tab Component', () => { component.displayInCommunityLessons = []; component.displayLessonsInPlaylist = [ - summary1, summary2, summary3, summary4]; + summary1, + summary2, + summary3, + summary4, + ]; component.changePageByOne('MOVE_TO_NEXT_PAGE', 'playlist'); expect(component.pageNumberInPlaylist).toEqual(2); @@ -576,188 +595,188 @@ describe('Community lessons tab Component', () => { expect(component.pageNumberInPlaylist).toEqual(1); }); - it('should open a modal to remove an exploration from playlist', - fakeAsync(() => { - spyOnProperty(navigator, 'userAgent').and.returnValue('iPhone'); - expect( - learnerDashboardActivityBackendApiService.removeActivityModalStatus) - .toBeUndefined; - - const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ( - { componentInstance: MockRemoveActivityNgbModalRef, - result: Promise.resolve('success') - }) as NgbModalRef; - }); - const exp1 = { - last_updated_msec: 1591296737470.528, - community_owned: false, - objective: 'Test Objective', - id: '44LKoKLlIbGe', - num_views: 0, - thumbnail_icon_url: '/subjects/Algebra.svg', - human_readable_contributors_summary: {}, - language_code: 'en', - thumbnail_bg_color: '#cc4b00', - created_on_msec: 1591296635736.666, - ratings: { - 1: 0, - 2: 0, - 3: 0, - 4: 0, - 5: 0 - }, - status: 'public', - tags: [], - activity_type: 'exploration', - category: 'Algebra', - title: 'Test Title' - }; - let summary1 = LearnerExplorationSummary.createFromBackendDict( - exp1); - component.explorationPlaylist = [summary1]; - component.totalLessonsInPlaylist = [summary1]; - let sectionNameI18nId = 'I18N_LEARNER_DASHBOARD_PLAYLIST_SECTION'; - let subsectionName = 'I18N_DASHBOARD_EXPLORATIONS'; - - component.openRemoveActivityModal( - sectionNameI18nId, subsectionName, summary1); - fixture.detectChanges(); - - expect(modalSpy).toHaveBeenCalled(); - })); - - it('should open a modal to remove a collection from playlist', - fakeAsync(() => { - expect( - learnerDashboardActivityBackendApiService.removeActivityModalStatus) - .toBeUndefined; - - const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ( - { componentInstance: MockRemoveActivityNgbModalRef, - result: Promise.resolve('success') - }) as NgbModalRef; - }); - - const collection = { - last_updated_msec: 1591296737470.528, - community_owned: false, - objective: 'Test Objective', - id: '44LKoKLlIbGe', - thumbnail_icon_url: '/subjects/Algebra.svg', - language_code: 'en', - thumbnail_bg_color: '#cc4b00', - created_on: 1591296635736.666, - status: 'public', - category: 'Algebra', - title: 'Test Title', - node_count: 0 - }; - let collectionSummary = CollectionSummary.createFromBackendDict( - collection); - component.collectionPlaylist = [collectionSummary]; - component.totalLessonsInPlaylist = [collectionSummary]; - component.showMoreInSection.playlist = true; - let sectionNameI18nId = 'I18N_LEARNER_DASHBOARD_PLAYLIST_SECTION'; - let subsectionName = 'I18N_DASHBOARD_COLLECTIONS'; - - component.openRemoveActivityModal( - sectionNameI18nId, subsectionName, collectionSummary); - fixture.detectChanges(); - - expect(modalSpy).toHaveBeenCalled(); - })); - - it('should open a modal to remove an exploration from incomplete list', - fakeAsync(() => { - expect( - learnerDashboardActivityBackendApiService.removeActivityModalStatus) - .toBeUndefined; - - const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ( - { componentInstance: MockRemoveActivityNgbModalRef, - result: Promise.resolve('success') - }) as NgbModalRef; - }); - const exp1 = { - last_updated_msec: 1591296737470.528, - community_owned: false, - objective: 'Test Objective', - id: '44LKoKLlIbGe', - num_views: 0, - thumbnail_icon_url: '/subjects/Algebra.svg', - human_readable_contributors_summary: {}, - language_code: 'en', - thumbnail_bg_color: '#cc4b00', - created_on_msec: 1591296635736.666, - ratings: { - 1: 0, - 2: 0, - 3: 0, - 4: 0, - 5: 0 - }, - status: 'public', - tags: [], - activity_type: 'exploration', - category: 'Algebra', - title: 'Test Title' - }; - let summary1 = LearnerExplorationSummary.createFromBackendDict( - exp1); - component.incompleteExplorationsList = [summary1]; - component.totalIncompleteLessonsList = [summary1]; - let sectionNameI18nId = 'I18N_LEARNER_DASHBOARD_INCOMPLETE_SECTION'; - let subsectionName = 'I18N_DASHBOARD_EXPLORATIONS'; - - component.openRemoveActivityModal( - sectionNameI18nId, subsectionName, summary1); - fixture.detectChanges(); - - expect(modalSpy).toHaveBeenCalled(); - })); - - it('should open a modal to remove a collection from incomplete list', - fakeAsync(() => { - expect( - learnerDashboardActivityBackendApiService.removeActivityModalStatus) - .toBeUndefined; - - const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ( - { componentInstance: MockRemoveActivityNgbModalRef, - result: Promise.resolve('success') - }) as NgbModalRef; - }); - - const collection = { - last_updated_msec: 1591296737470.528, - community_owned: false, - objective: 'Test Objective', - id: '44LKoKLlIbGe', - thumbnail_icon_url: '/subjects/Algebra.svg', - language_code: 'en', - thumbnail_bg_color: '#cc4b00', - created_on: 1591296635736.666, - status: 'public', - category: 'Algebra', - title: 'Test Title', - node_count: 0 - }; - let collectionSummary = CollectionSummary.createFromBackendDict( - collection); - component.incompleteCollectionsList = [collectionSummary]; - component.totalIncompleteLessonsList = [collectionSummary]; - component.showMoreInSection.incomplete = true; - let sectionNameI18nId = 'I18N_LEARNER_DASHBOARD_INCOMPLETE_SECTION'; - let subsectionName = 'I18N_DASHBOARD_COLLECTIONS'; - - component.openRemoveActivityModal( - sectionNameI18nId, subsectionName, collectionSummary); - fixture.detectChanges(); - - expect(modalSpy).toHaveBeenCalled(); - })); + it('should open a modal to remove an exploration from playlist', fakeAsync(() => { + spyOnProperty(navigator, 'userAgent').and.returnValue('iPhone'); + expect(learnerDashboardActivityBackendApiService.removeActivityModalStatus) + .toBeUndefined; + + const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + componentInstance: MockRemoveActivityNgbModalRef, + result: Promise.resolve('success'), + } as NgbModalRef; + }); + const exp1 = { + last_updated_msec: 1591296737470.528, + community_owned: false, + objective: 'Test Objective', + id: '44LKoKLlIbGe', + num_views: 0, + thumbnail_icon_url: '/subjects/Algebra.svg', + human_readable_contributors_summary: {}, + language_code: 'en', + thumbnail_bg_color: '#cc4b00', + created_on_msec: 1591296635736.666, + ratings: { + 1: 0, + 2: 0, + 3: 0, + 4: 0, + 5: 0, + }, + status: 'public', + tags: [], + activity_type: 'exploration', + category: 'Algebra', + title: 'Test Title', + }; + let summary1 = LearnerExplorationSummary.createFromBackendDict(exp1); + component.explorationPlaylist = [summary1]; + component.totalLessonsInPlaylist = [summary1]; + let sectionNameI18nId = 'I18N_LEARNER_DASHBOARD_PLAYLIST_SECTION'; + let subsectionName = 'I18N_DASHBOARD_EXPLORATIONS'; + + component.openRemoveActivityModal( + sectionNameI18nId, + subsectionName, + summary1 + ); + fixture.detectChanges(); + + expect(modalSpy).toHaveBeenCalled(); + })); + + it('should open a modal to remove a collection from playlist', fakeAsync(() => { + expect(learnerDashboardActivityBackendApiService.removeActivityModalStatus) + .toBeUndefined; + + const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + componentInstance: MockRemoveActivityNgbModalRef, + result: Promise.resolve('success'), + } as NgbModalRef; + }); + + const collection = { + last_updated_msec: 1591296737470.528, + community_owned: false, + objective: 'Test Objective', + id: '44LKoKLlIbGe', + thumbnail_icon_url: '/subjects/Algebra.svg', + language_code: 'en', + thumbnail_bg_color: '#cc4b00', + created_on: 1591296635736.666, + status: 'public', + category: 'Algebra', + title: 'Test Title', + node_count: 0, + }; + let collectionSummary = CollectionSummary.createFromBackendDict(collection); + component.collectionPlaylist = [collectionSummary]; + component.totalLessonsInPlaylist = [collectionSummary]; + component.showMoreInSection.playlist = true; + let sectionNameI18nId = 'I18N_LEARNER_DASHBOARD_PLAYLIST_SECTION'; + let subsectionName = 'I18N_DASHBOARD_COLLECTIONS'; + + component.openRemoveActivityModal( + sectionNameI18nId, + subsectionName, + collectionSummary + ); + fixture.detectChanges(); + + expect(modalSpy).toHaveBeenCalled(); + })); + + it('should open a modal to remove an exploration from incomplete list', fakeAsync(() => { + expect(learnerDashboardActivityBackendApiService.removeActivityModalStatus) + .toBeUndefined; + + const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + componentInstance: MockRemoveActivityNgbModalRef, + result: Promise.resolve('success'), + } as NgbModalRef; + }); + const exp1 = { + last_updated_msec: 1591296737470.528, + community_owned: false, + objective: 'Test Objective', + id: '44LKoKLlIbGe', + num_views: 0, + thumbnail_icon_url: '/subjects/Algebra.svg', + human_readable_contributors_summary: {}, + language_code: 'en', + thumbnail_bg_color: '#cc4b00', + created_on_msec: 1591296635736.666, + ratings: { + 1: 0, + 2: 0, + 3: 0, + 4: 0, + 5: 0, + }, + status: 'public', + tags: [], + activity_type: 'exploration', + category: 'Algebra', + title: 'Test Title', + }; + let summary1 = LearnerExplorationSummary.createFromBackendDict(exp1); + component.incompleteExplorationsList = [summary1]; + component.totalIncompleteLessonsList = [summary1]; + let sectionNameI18nId = 'I18N_LEARNER_DASHBOARD_INCOMPLETE_SECTION'; + let subsectionName = 'I18N_DASHBOARD_EXPLORATIONS'; + + component.openRemoveActivityModal( + sectionNameI18nId, + subsectionName, + summary1 + ); + fixture.detectChanges(); + + expect(modalSpy).toHaveBeenCalled(); + })); + + it('should open a modal to remove a collection from incomplete list', fakeAsync(() => { + expect(learnerDashboardActivityBackendApiService.removeActivityModalStatus) + .toBeUndefined; + + const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + componentInstance: MockRemoveActivityNgbModalRef, + result: Promise.resolve('success'), + } as NgbModalRef; + }); + + const collection = { + last_updated_msec: 1591296737470.528, + community_owned: false, + objective: 'Test Objective', + id: '44LKoKLlIbGe', + thumbnail_icon_url: '/subjects/Algebra.svg', + language_code: 'en', + thumbnail_bg_color: '#cc4b00', + created_on: 1591296635736.666, + status: 'public', + category: 'Algebra', + title: 'Test Title', + node_count: 0, + }; + let collectionSummary = CollectionSummary.createFromBackendDict(collection); + component.incompleteCollectionsList = [collectionSummary]; + component.totalIncompleteLessonsList = [collectionSummary]; + component.showMoreInSection.incomplete = true; + let sectionNameI18nId = 'I18N_LEARNER_DASHBOARD_INCOMPLETE_SECTION'; + let subsectionName = 'I18N_DASHBOARD_COLLECTIONS'; + + component.openRemoveActivityModal( + sectionNameI18nId, + subsectionName, + collectionSummary + ); + fixture.detectChanges(); + + expect(modalSpy).toHaveBeenCalled(); + })); }); diff --git a/core/templates/pages/learner-dashboard-page/community-lessons-tab.component.ts b/core/templates/pages/learner-dashboard-page/community-lessons-tab.component.ts index 62147eaf496a..45356c3e762c 100644 --- a/core/templates/pages/learner-dashboard-page/community-lessons-tab.component.ts +++ b/core/templates/pages/learner-dashboard-page/community-lessons-tab.component.ts @@ -17,38 +17,36 @@ * page. */ -import { Component, Input } from '@angular/core'; -import { LearnerDashboardActivityBackendApiService } from 'domain/learner_dashboard/learner-dashboard-activity-backend-api.service'; -import { LearnerDashboardPageConstants } from './learner-dashboard-page.constants'; -import { LearnerExplorationSummary } from 'domain/summary/learner-exploration-summary.model'; -import { CollectionSummary } from 'domain/collection/collection-summary.model'; -import { ProfileSummary } from 'domain/user/profile-summary.model'; -import { AppConstants } from 'app.constants'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { Subscription } from 'rxjs'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { UserService } from 'services/user.service'; +import {Component, Input} from '@angular/core'; +import {LearnerDashboardActivityBackendApiService} from 'domain/learner_dashboard/learner-dashboard-activity-backend-api.service'; +import {LearnerDashboardPageConstants} from './learner-dashboard-page.constants'; +import {LearnerExplorationSummary} from 'domain/summary/learner-exploration-summary.model'; +import {CollectionSummary} from 'domain/collection/collection-summary.model'; +import {ProfileSummary} from 'domain/user/profile-summary.model'; +import {AppConstants} from 'app.constants'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {Subscription} from 'rxjs'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {UserService} from 'services/user.service'; import './community-lessons-tab.component.css'; - interface ShowMoreInSectionDict { [section: string]: boolean; } - @Component({ - selector: 'oppia-community-lessons-tab', - templateUrl: './community-lessons-tab.component.html', - styleUrls: ['./community-lessons-tab.component.css'] - }) +@Component({ + selector: 'oppia-community-lessons-tab', + templateUrl: './community-lessons-tab.component.html', + styleUrls: ['./community-lessons-tab.component.css'], +}) export class CommunityLessonsTabComponent { constructor( - private learnerDashboardActivityBackendApiService: - LearnerDashboardActivityBackendApiService, + private learnerDashboardActivityBackendApiService: LearnerDashboardActivityBackendApiService, private i18nLanguageCodeService: I18nLanguageCodeService, private windowDimensionService: WindowDimensionsService, - private userService: UserService) { - } + private userService: UserService + ) {} // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see @@ -65,28 +63,33 @@ export class CommunityLessonsTabComponent { noCommunityLessonActivity: boolean = false; noPlaylistActivity: boolean = false; totalIncompleteLessonsList: ( - LearnerExplorationSummary | CollectionSummary)[] = []; + | LearnerExplorationSummary + | CollectionSummary + )[] = []; - totalCompletedLessonsList: ( - LearnerExplorationSummary | CollectionSummary)[] = []; + totalCompletedLessonsList: (LearnerExplorationSummary | CollectionSummary)[] = + []; - totalLessonsInPlaylist: ( - LearnerExplorationSummary | CollectionSummary)[] = []; + totalLessonsInPlaylist: (LearnerExplorationSummary | CollectionSummary)[] = + []; - allCommunityLessons: ( - LearnerExplorationSummary | CollectionSummary)[] = []; + allCommunityLessons: (LearnerExplorationSummary | CollectionSummary)[] = []; displayIncompleteLessonsList: ( - LearnerExplorationSummary | CollectionSummary)[] = []; + | LearnerExplorationSummary + | CollectionSummary + )[] = []; displayCompletedLessonsList: ( - LearnerExplorationSummary | CollectionSummary)[] = []; + | LearnerExplorationSummary + | CollectionSummary + )[] = []; - displayLessonsInPlaylist: ( - LearnerExplorationSummary | CollectionSummary)[] = []; + displayLessonsInPlaylist: (LearnerExplorationSummary | CollectionSummary)[] = + []; - displayInCommunityLessons: ( - LearnerExplorationSummary | CollectionSummary)[] = []; + displayInCommunityLessons: (LearnerExplorationSummary | CollectionSummary)[] = + []; completed: string = 'Completed'; incomplete: string = 'Incomplete'; @@ -98,7 +101,7 @@ export class CommunityLessonsTabComponent { incomplete: false, completed: false, playlist: false, - subscriptions: false + subscriptions: false, }; pageNumberInCommunityLessons: number = 1; @@ -108,40 +111,57 @@ export class CommunityLessonsTabComponent { pageNumberInPlaylist: number = 1; startIndexInPlaylist: number = 0; endIndexInPlaylist: number = 3; - communityLibraryUrl = ( - '/' + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LIBRARY_INDEX.ROUTE); + communityLibraryUrl = + '/' + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LIBRARY_INDEX.ROUTE; windowIsNarrow: boolean = false; directiveSubscriptions = new Subscription(); ngOnInit(): void { var tempIncompleteLessonsList: ( - LearnerExplorationSummary | CollectionSummary)[] = []; + | LearnerExplorationSummary + | CollectionSummary + )[] = []; var tempCompletedLessonsList: ( - LearnerExplorationSummary | CollectionSummary)[] = []; - this.noCommunityLessonActivity = ( - (this.incompleteExplorationsList.length === 0) && - (this.completedExplorationsList.length === 0) && - (this.incompleteCollectionsList.length === 0) && - (this.completedCollectionsList.length === 0)); - this.noPlaylistActivity = ( - (this.explorationPlaylist.length === 0) && - (this.collectionPlaylist.length === 0)); + | LearnerExplorationSummary + | CollectionSummary + )[] = []; + this.noCommunityLessonActivity = + this.incompleteExplorationsList.length === 0 && + this.completedExplorationsList.length === 0 && + this.incompleteCollectionsList.length === 0 && + this.completedCollectionsList.length === 0; + this.noPlaylistActivity = + this.explorationPlaylist.length === 0 && + this.collectionPlaylist.length === 0; tempIncompleteLessonsList.push( - ...this.incompleteExplorationsList, ...this.incompleteCollectionsList); + ...this.incompleteExplorationsList, + ...this.incompleteCollectionsList + ); this.totalIncompleteLessonsList = tempIncompleteLessonsList.reverse(); tempCompletedLessonsList.push( - ...this.completedExplorationsList, ...this.completedCollectionsList); + ...this.completedExplorationsList, + ...this.completedCollectionsList + ); this.totalCompletedLessonsList = tempCompletedLessonsList.reverse(); this.totalLessonsInPlaylist.push( - ...this.explorationPlaylist, ...this.collectionPlaylist); + ...this.explorationPlaylist, + ...this.collectionPlaylist + ); this.allCommunityLessons.push( - ...this.incompleteExplorationsList, ...this.incompleteCollectionsList, - ...this.completedCollectionsList, ...this.completedExplorationsList); + ...this.incompleteExplorationsList, + ...this.incompleteCollectionsList, + ...this.completedCollectionsList, + ...this.completedExplorationsList + ); this.displayIncompleteLessonsList = this.totalIncompleteLessonsList.slice( - 0, 3); + 0, + 3 + ); this.displayCompletedLessonsList = this.totalCompletedLessonsList.slice( - 0, 3); + 0, + 3 + ); this.windowIsNarrow = this.windowDimensionService.isWindowNarrow(); this.setDisplayLessonsInPlaylist(); @@ -150,7 +170,8 @@ export class CommunityLessonsTabComponent { this.windowDimensionService.getResizeEvent().subscribe(() => { this.windowIsNarrow = this.windowDimensionService.isWindowNarrow(); this.setDisplayLessonsInPlaylist(); - })); + }) + ); this.displayInCommunityLessons = this.allCommunityLessons; this.selectedSection = this.all; @@ -158,14 +179,12 @@ export class CommunityLessonsTabComponent { } getProfileImagePngDataUrl(username: string): string { - let [pngImageUrl, _] = this.userService.getProfileImageDataUrl( - username); + let [pngImageUrl, _] = this.userService.getProfileImageDataUrl(username); return pngImageUrl; } getProfileImageWebpDataUrl(username: string): string { - let [_, webpImageUrl] = this.userService.getProfileImageDataUrl( - username); + let [_, webpImageUrl] = this.userService.getProfileImageDataUrl(username); return webpImageUrl; } @@ -195,7 +214,9 @@ export class CommunityLessonsTabComponent { } else if (section === this.all) { this.displayInCommunityLessons = []; this.displayInCommunityLessons.push( - ...this.totalIncompleteLessonsList, ...this.totalCompletedLessonsList); + ...this.totalIncompleteLessonsList, + ...this.totalCompletedLessonsList + ); } this.pageNumberInCommunityLessons = 1; this.startIndexInCommunityLessons = 0; @@ -228,26 +249,42 @@ export class CommunityLessonsTabComponent { handleShowMore(section: string): void { this.showMoreInSection[section] = !this.showMoreInSection[section]; if ( - section === 'incomplete' && this.showMoreInSection.incomplete === true) { + section === 'incomplete' && + this.showMoreInSection.incomplete === true + ) { this.displayIncompleteLessonsList = this.totalIncompleteLessonsList; } else if ( - section === 'incomplete' && this.showMoreInSection.incomplete === false) { + section === 'incomplete' && + this.showMoreInSection.incomplete === false + ) { this.displayIncompleteLessonsList = this.totalIncompleteLessonsList.slice( - 0, 3); + 0, + 3 + ); } else if ( - section === 'completed' && this.showMoreInSection.completed === true) { + section === 'completed' && + this.showMoreInSection.completed === true + ) { this.displayCompletedLessonsList = this.totalCompletedLessonsList; } else if ( - section === 'completed' && this.showMoreInSection.completed === false) { + section === 'completed' && + this.showMoreInSection.completed === false + ) { this.displayCompletedLessonsList = this.totalCompletedLessonsList.slice( - 0, 3); + 0, + 3 + ); } else if ( - section === 'playlist' && this.showMoreInSection.playlist === true) { + section === 'playlist' && + this.showMoreInSection.playlist === true + ) { this.displayLessonsInPlaylist = this.totalLessonsInPlaylist; this.startIndexInPlaylist = 0; this.endIndexInPlaylist = this.totalLessonsInPlaylist.length; } else if ( - section === 'playlist' && this.showMoreInSection.playlist === false) { + section === 'playlist' && + this.showMoreInSection.playlist === false + ) { this.startIndexInPlaylist = 0; this.endIndexInPlaylist = this.pageSize; } @@ -263,126 +300,168 @@ export class CommunityLessonsTabComponent { changePageByOne(direction: string, section: string): void { if (section === 'communityLessons') { let totalPages = this.displayInCommunityLessons.length / this.pageSize; - if (direction === this.moveToPrevPage && - this.pageNumberInCommunityLessons > 1) { + if ( + direction === this.moveToPrevPage && + this.pageNumberInCommunityLessons > 1 + ) { this.pageNumberInCommunityLessons -= 1; } if (totalPages > Math.floor(totalPages)) { totalPages = Math.floor(totalPages) + 1; } - if (direction === this.moveToNextPage && - this.pageNumberInCommunityLessons < totalPages) { + if ( + direction === this.moveToNextPage && + this.pageNumberInCommunityLessons < totalPages + ) { this.pageNumberInCommunityLessons += 1; } - this.startIndexInCommunityLessons = ( - this.pageNumberInCommunityLessons - 1) * this.pageSize; + this.startIndexInCommunityLessons = + (this.pageNumberInCommunityLessons - 1) * this.pageSize; this.endIndexInCommunityLessons = Math.min( this.startIndexInCommunityLessons + this.pageSize, - this.displayInCommunityLessons.length); + this.displayInCommunityLessons.length + ); } else if (section === 'playlist') { let totalPages = this.displayLessonsInPlaylist.length / this.pageSize; - if (direction === this.moveToPrevPage && - this.pageNumberInPlaylist > 1) { + if (direction === this.moveToPrevPage && this.pageNumberInPlaylist > 1) { this.pageNumberInPlaylist -= 1; } if (totalPages > Math.floor(totalPages)) { totalPages = Math.floor(totalPages) + 1; } - if (direction === this.moveToNextPage && - this.pageNumberInPlaylist < totalPages) { + if ( + direction === this.moveToNextPage && + this.pageNumberInPlaylist < totalPages + ) { this.pageNumberInPlaylist += 1; } - this.startIndexInPlaylist = ( - this.pageNumberInPlaylist - 1) * this.pageSize; + this.startIndexInPlaylist = + (this.pageNumberInPlaylist - 1) * this.pageSize; this.endIndexInPlaylist = Math.min( this.startIndexInPlaylist + this.pageSize, - this.displayLessonsInPlaylist.length); + this.displayLessonsInPlaylist.length + ); } } openRemoveActivityModal( - sectionNameI18nId: string, subsectionName: string, - activity: LearnerExplorationSummary | CollectionSummary): void { - this.learnerDashboardActivityBackendApiService.removeActivityModalAsync( - sectionNameI18nId, subsectionName, - activity.id, activity.title) + sectionNameI18nId: string, + subsectionName: string, + activity: LearnerExplorationSummary | CollectionSummary + ): void { + this.learnerDashboardActivityBackendApiService + .removeActivityModalAsync( + sectionNameI18nId, + subsectionName, + activity.id, + activity.title + ) .then(() => { - if (sectionNameI18nId === - LearnerDashboardPageConstants - .LEARNER_DASHBOARD_SECTION_I18N_IDS.INCOMPLETE) { - if (subsectionName === - LearnerDashboardPageConstants - .LEARNER_DASHBOARD_SUBSECTION_I18N_IDS.EXPLORATIONS) { + if ( + sectionNameI18nId === + LearnerDashboardPageConstants.LEARNER_DASHBOARD_SECTION_I18N_IDS + .INCOMPLETE + ) { + if ( + subsectionName === + LearnerDashboardPageConstants.LEARNER_DASHBOARD_SUBSECTION_I18N_IDS + .EXPLORATIONS + ) { let index = this.totalIncompleteLessonsList.findIndex( - exp => exp.id === activity.id); + exp => exp.id === activity.id + ); if (index !== -1) { this.totalIncompleteLessonsList.splice(index, 1); } - } else if (subsectionName === - LearnerDashboardPageConstants - .LEARNER_DASHBOARD_SUBSECTION_I18N_IDS.COLLECTIONS) { + } else if ( + subsectionName === + LearnerDashboardPageConstants.LEARNER_DASHBOARD_SUBSECTION_I18N_IDS + .COLLECTIONS + ) { let index = this.totalIncompleteLessonsList.findIndex( - collection => collection.id === activity.id); + collection => collection.id === activity.id + ); if (index !== -1) { this.totalIncompleteLessonsList.splice(index, 1); } - } if (this.showMoreInSection.incomplete === true) { - this.displayIncompleteLessonsList = ( - this.totalIncompleteLessonsList); + } + if (this.showMoreInSection.incomplete === true) { + this.displayIncompleteLessonsList = this.totalIncompleteLessonsList; } else if (this.showMoreInSection.incomplete === false) { - this.displayIncompleteLessonsList = ( - this.totalIncompleteLessonsList.slice(0, 3)); - } if (this.selectedSection === this.all) { + this.displayIncompleteLessonsList = + this.totalIncompleteLessonsList.slice(0, 3); + } + if (this.selectedSection === this.all) { this.displayInCommunityLessons = []; this.displayInCommunityLessons.push( ...this.totalIncompleteLessonsList, - ...this.totalCompletedLessonsList); - } if (this.displayInCommunityLessons.slice( - this.startIndexInCommunityLessons, - this.endIndexInCommunityLessons).length === 0) { + ...this.totalCompletedLessonsList + ); + } + if ( + this.displayInCommunityLessons.slice( + this.startIndexInCommunityLessons, + this.endIndexInCommunityLessons + ).length === 0 + ) { this.pageNumberInCommunityLessons = 1; this.startIndexInCommunityLessons = 0; this.endIndexInCommunityLessons = 3; } - } else if (sectionNameI18nId === - LearnerDashboardPageConstants - .LEARNER_DASHBOARD_SECTION_I18N_IDS.PLAYLIST) { - if (subsectionName === - LearnerDashboardPageConstants - .LEARNER_DASHBOARD_SUBSECTION_I18N_IDS.EXPLORATIONS) { + } else if ( + sectionNameI18nId === + LearnerDashboardPageConstants.LEARNER_DASHBOARD_SECTION_I18N_IDS + .PLAYLIST + ) { + if ( + subsectionName === + LearnerDashboardPageConstants.LEARNER_DASHBOARD_SUBSECTION_I18N_IDS + .EXPLORATIONS + ) { let index = this.totalLessonsInPlaylist.findIndex( - exp => exp.id === activity.id); + exp => exp.id === activity.id + ); if (index !== -1) { this.totalLessonsInPlaylist.splice(index, 1); } - } else if (subsectionName === - LearnerDashboardPageConstants - .LEARNER_DASHBOARD_SUBSECTION_I18N_IDS.COLLECTIONS) { + } else if ( + subsectionName === + LearnerDashboardPageConstants.LEARNER_DASHBOARD_SUBSECTION_I18N_IDS + .COLLECTIONS + ) { let index = this.totalLessonsInPlaylist.findIndex( - collection => collection.id === activity.id); + collection => collection.id === activity.id + ); if (index !== -1) { this.totalLessonsInPlaylist.splice(index, 1); } - } if (this.showMoreInSection.playlist === true) { + } + if (this.showMoreInSection.playlist === true) { this.displayLessonsInPlaylist = this.totalLessonsInPlaylist; } else if (this.showMoreInSection.playlist === false) { - this.displayLessonsInPlaylist = ( - this.totalLessonsInPlaylist.slice(0, 3)); - } if (this.windowIsNarrow) { + this.displayLessonsInPlaylist = this.totalLessonsInPlaylist.slice( + 0, + 3 + ); + } + if (this.windowIsNarrow) { this.displayLessonsInPlaylist = this.totalLessonsInPlaylist; - } if (this.displayLessonsInPlaylist.slice( - this.startIndexInPlaylist, - this.endIndexInPlaylist).length === 0) { + } + if ( + this.displayLessonsInPlaylist.slice( + this.startIndexInPlaylist, + this.endIndexInPlaylist + ).length === 0 + ) { this.pageNumberInPlaylist = 1; this.startIndexInPlaylist = 0; this.endIndexInPlaylist = 3; } } - this.noCommunityLessonActivity = ( - (this.totalIncompleteLessonsList.length === 0) && - (this.totalCompletedLessonsList.length === 0)); - this.noPlaylistActivity = ( - (this.totalLessonsInPlaylist.length === 0)); + this.noCommunityLessonActivity = + this.totalIncompleteLessonsList.length === 0 && + this.totalCompletedLessonsList.length === 0; + this.noPlaylistActivity = this.totalLessonsInPlaylist.length === 0; }); } } diff --git a/core/templates/pages/learner-dashboard-page/goals-tab.component.spec.ts b/core/templates/pages/learner-dashboard-page/goals-tab.component.spec.ts index 5fcc3c898834..134918b1a604 100644 --- a/core/templates/pages/learner-dashboard-page/goals-tab.component.spec.ts +++ b/core/templates/pages/learner-dashboard-page/goals-tab.component.spec.ts @@ -16,21 +16,25 @@ * @fileoverview Unit tests for for GoalsTabComponent. */ -import { async, ComponentFixture, fakeAsync, TestBed } from - '@angular/core/testing'; -import { MaterialModule } from 'modules/material.module'; -import { FormsModule } from '@angular/forms'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { LearnerDashboardActivityBackendApiService } from 'domain/learner_dashboard/learner-dashboard-activity-backend-api.service'; -import { LearnerDashboardIdsBackendApiService } from 'domain/learner_dashboard/learner-dashboard-ids-backend-api.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; -import { LearnerTopicSummary} from 'domain/topic/learner-topic-summary.model'; -import { GoalsTabComponent } from './goals-tab.component'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; +import { + async, + ComponentFixture, + fakeAsync, + TestBed, +} from '@angular/core/testing'; +import {MaterialModule} from 'modules/material.module'; +import {FormsModule} from '@angular/forms'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {LearnerDashboardActivityBackendApiService} from 'domain/learner_dashboard/learner-dashboard-activity-backend-api.service'; +import {LearnerDashboardIdsBackendApiService} from 'domain/learner_dashboard/learner-dashboard-ids-backend-api.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {LearnerTopicSummary} from 'domain/topic/learner-topic-summary.model'; +import {GoalsTabComponent} from './goals-tab.component'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; class MockRemoveActivityNgbModalRef { componentInstance = { @@ -44,8 +48,7 @@ class MockRemoveActivityNgbModalRef { describe('Goals tab Component', () => { let component: GoalsTabComponent; let fixture: ComponentFixture; - let learnerDashboardActivityBackendApiService: - LearnerDashboardActivityBackendApiService; + let learnerDashboardActivityBackendApiService: LearnerDashboardActivityBackendApiService; let urlInterpolationService: UrlInterpolationService; let ngbModal: NgbModal; let windowDimensionsService: WindowDimensionsService; @@ -58,12 +61,9 @@ describe('Goals tab Component', () => { BrowserAnimationsModule, MaterialModule, FormsModule, - HttpClientTestingModule - ], - declarations: [ - GoalsTabComponent, - MockTranslatePipe + HttpClientTestingModule, ], + declarations: [GoalsTabComponent, MockTranslatePipe], providers: [ LearnerDashboardActivityBackendApiService, LearnerDashboardIdsBackendApiService, @@ -73,18 +73,19 @@ describe('Goals tab Component', () => { useValue: { isWindowNarrow: () => true, getResizeEvent: () => mockResizeEmitter, - } + }, }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(GoalsTabComponent); component = fixture.componentInstance; - learnerDashboardActivityBackendApiService = - TestBed.inject(LearnerDashboardActivityBackendApiService); + learnerDashboardActivityBackendApiService = TestBed.inject( + LearnerDashboardActivityBackendApiService + ); ngbModal = TestBed.inject(NgbModal); urlInterpolationService = TestBed.inject(UrlInterpolationService); windowDimensionsService = TestBed.inject(WindowDimensionsService); @@ -94,7 +95,7 @@ describe('Goals tab Component', () => { title: 'subtopic_name', thumbnail_filename: 'image.svg', thumbnail_bg_color: '#F8BF74', - url_fragment: 'subtopic-name' + url_fragment: 'subtopic-name', }; let nodeDict = { @@ -113,7 +114,7 @@ describe('Goals tab Component', () => { planned_publication_date_msecs: 100, last_modified_msecs: 100, first_publication_date_msecs: 200, - unpublishing_reason: null + unpublishing_reason: null, }; const learnerTopicSummaryBackendDict1 = { id: 'sample_topic_id', @@ -127,28 +128,30 @@ describe('Goals tab Component', () => { thumbnail_bg_color: '#C6DCDA', classroom: 'math', practice_tab_is_displayed: false, - canonical_story_summary_dict: [{ - id: '0', - title: 'Story Title', - description: 'Story Description', - node_titles: ['Chapter 1'], - thumbnail_filename: 'image.svg', - thumbnail_bg_color: '#F8BF74', - story_is_published: true, - completed_node_titles: ['Chapter 1'], - url_fragment: 'story-title', - all_node_dicts: [nodeDict] - }], + canonical_story_summary_dict: [ + { + id: '0', + title: 'Story Title', + description: 'Story Description', + node_titles: ['Chapter 1'], + thumbnail_filename: 'image.svg', + thumbnail_bg_color: '#F8BF74', + story_is_published: true, + completed_node_titles: ['Chapter 1'], + url_fragment: 'story-title', + all_node_dicts: [nodeDict], + }, + ], url_fragment: 'topic-name', subtopics: [subtopic], degrees_of_mastery: { skill_id_1: 0.5, - skill_id_2: 0.3 + skill_id_2: 0.3, }, skill_descriptions: { skill_id_1: 'Skill Description 1', - skill_id_2: 'Skill Description 2' - } + skill_id_2: 'Skill Description 2', + }, }; const learnerTopicSummaryBackendDict2 = { id: 'sample_topic_2', @@ -162,28 +165,30 @@ describe('Goals tab Component', () => { thumbnail_bg_color: '#C6DCDA', classroom: 'math', practice_tab_is_displayed: false, - canonical_story_summary_dict: [{ - id: '0', - title: 'Story Title', - description: 'Story Description', - node_titles: ['Chapter 1'], - thumbnail_filename: 'image.svg', - thumbnail_bg_color: '#F8BF74', - story_is_published: true, - completed_node_titles: ['Chapter 1'], - url_fragment: 'story-title', - all_node_dicts: [] - }], + canonical_story_summary_dict: [ + { + id: '0', + title: 'Story Title', + description: 'Story Description', + node_titles: ['Chapter 1'], + thumbnail_filename: 'image.svg', + thumbnail_bg_color: '#F8BF74', + story_is_published: true, + completed_node_titles: ['Chapter 1'], + url_fragment: 'story-title', + all_node_dicts: [], + }, + ], url_fragment: 'topic-name', subtopics: [subtopic], degrees_of_mastery: { skill_id_1: 0.5, - skill_id_2: 0.3 + skill_id_2: 0.3, }, skill_descriptions: { skill_id_1: 'Skill Description 1', - skill_id_2: 'Skill Description 2' - } + skill_id_2: 'Skill Description 2', + }, }; const learnerTopicSummaryBackendDict3 = { id: 'sample_topic_3', @@ -197,44 +202,60 @@ describe('Goals tab Component', () => { thumbnail_bg_color: '#C6DCDA', classroom: 'math', practice_tab_is_displayed: false, - canonical_story_summary_dict: [{ - id: '0', - title: 'Story Title', - description: 'Story Description', - node_titles: ['Chapter 1'], - thumbnail_filename: 'image.svg', - thumbnail_bg_color: '#F8BF74', - story_is_published: true, - completed_node_titles: ['Chapter 1'], - url_fragment: 'story-title', - all_node_dicts: [nodeDict] - }], + canonical_story_summary_dict: [ + { + id: '0', + title: 'Story Title', + description: 'Story Description', + node_titles: ['Chapter 1'], + thumbnail_filename: 'image.svg', + thumbnail_bg_color: '#F8BF74', + story_is_published: true, + completed_node_titles: ['Chapter 1'], + url_fragment: 'story-title', + all_node_dicts: [nodeDict], + }, + ], url_fragment: 'topic-name', subtopics: [subtopic], degrees_of_mastery: { skill_id_1: 0.5, - skill_id_2: 0.3 + skill_id_2: 0.3, }, skill_descriptions: { skill_id_1: 'Skill Description 1', - skill_id_2: 'Skill Description 2' - } + skill_id_2: 'Skill Description 2', + }, }; - component.currentGoals = [LearnerTopicSummary.createFromBackendDict( - learnerTopicSummaryBackendDict1), - LearnerTopicSummary.createFromBackendDict( - learnerTopicSummaryBackendDict2)]; - component.editGoals = [LearnerTopicSummary.createFromBackendDict( - learnerTopicSummaryBackendDict1), - LearnerTopicSummary.createFromBackendDict( - learnerTopicSummaryBackendDict2), - LearnerTopicSummary.createFromBackendDict( - learnerTopicSummaryBackendDict3)]; - component.completedGoals = [LearnerTopicSummary.createFromBackendDict( - learnerTopicSummaryBackendDict3)]; + component.currentGoals = [ + LearnerTopicSummary.createFromBackendDict( + learnerTopicSummaryBackendDict1 + ), + LearnerTopicSummary.createFromBackendDict( + learnerTopicSummaryBackendDict2 + ), + ]; + component.editGoals = [ + LearnerTopicSummary.createFromBackendDict( + learnerTopicSummaryBackendDict1 + ), + LearnerTopicSummary.createFromBackendDict( + learnerTopicSummaryBackendDict2 + ), + LearnerTopicSummary.createFromBackendDict( + learnerTopicSummaryBackendDict3 + ), + ]; + component.completedGoals = [ + LearnerTopicSummary.createFromBackendDict( + learnerTopicSummaryBackendDict3 + ), + ]; component.partiallyLearntTopicsList = [ LearnerTopicSummary.createFromBackendDict( - learnerTopicSummaryBackendDict1)]; + learnerTopicSummaryBackendDict1 + ), + ]; component.untrackedTopics = {}; component.learntToPartiallyLearntTopics = []; component.currentGoalsStoryIsShown = []; @@ -280,16 +301,15 @@ describe('Goals tab Component', () => { expect(topicBelongsTo).toEqual(1); }); - it('should check if the topicName belongs to learntToPartiallyLearntTopics', - () => { - component.learntToPartiallyLearntTopics = ['topic', 'topic2', 'topic3']; + it('should check if the topicName belongs to learntToPartiallyLearntTopics', () => { + component.learntToPartiallyLearntTopics = ['topic', 'topic2', 'topic3']; - let topicBelongsTo = ( - component.doesTopicBelongToLearntToPartiallyLearntTopics('topic')); - fixture.detectChanges(); + let topicBelongsTo = + component.doesTopicBelongToLearntToPartiallyLearntTopics('topic'); + fixture.detectChanges(); - expect(topicBelongsTo).toEqual(true); - }); + expect(topicBelongsTo).toEqual(true); + }); it('should toggle story', () => { component.currentGoalsStoryIsShown = [true]; @@ -304,8 +324,9 @@ describe('Goals tab Component', () => { component.topicIdsInCurrentGoals.length = 0; component.topicIdsInCompletedGoals = ['1', '2']; const learnerGoalsSpy = spyOn( - learnerDashboardActivityBackendApiService, 'addToLearnerGoals') - .and.returnValue(Promise.resolve(true)); + learnerDashboardActivityBackendApiService, + 'addToLearnerGoals' + ).and.returnValue(Promise.resolve(true)); component.untrackedTopics = {math: [component.editGoals[0]]}; component.addToLearnerGoals(component.editGoals[0], 'sample_topic_id', 1); fixture.detectChanges(); @@ -316,8 +337,9 @@ describe('Goals tab Component', () => { component.topicIdsInCurrentGoals = ['1', '2', '3']; const learnerGoalsSpy = spyOn( - learnerDashboardActivityBackendApiService, 'addToLearnerGoals') - .and.returnValue(Promise.resolve(true)); + learnerDashboardActivityBackendApiService, + 'addToLearnerGoals' + ).and.returnValue(Promise.resolve(true)); const removeTopicSpy = spyOn(component, 'removeFromLearnerGoals'); component.addToLearnerGoals(component.editGoals[0], '2', 1); @@ -333,24 +355,33 @@ describe('Goals tab Component', () => { component.topicIdsInCurrentGoals = ['1', '2', '3']; const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ( - { componentInstance: MockRemoveActivityNgbModalRef, - result: Promise.resolve('success') - }) as NgbModalRef; + return { + componentInstance: MockRemoveActivityNgbModalRef, + result: Promise.resolve('success'), + } as NgbModalRef; }); component.removeFromLearnerGoals( - component.editGoals[0], '1', 'topicName', 0); + component.editGoals[0], + '1', + 'topicName', + 0 + ); component.removeFromLearnerGoals( - component.editGoals[1], '2', 'topicName', 0); + component.editGoals[1], + '2', + 'topicName', + 0 + ); expect(modalSpy).toHaveBeenCalled(); }); it('should get static image url', () => { const urlSpy = spyOn( - urlInterpolationService, 'getStaticImageUrl') - .and.returnValue('/assets/images/learner_dashboard/star.svg'); + urlInterpolationService, + 'getStaticImageUrl' + ).and.returnValue('/assets/images/learner_dashboard/star.svg'); component.getStaticImageUrl('/learner_dashboard/star.svg'); fixture.detectChanges(); @@ -370,10 +401,9 @@ describe('Goals tab Component', () => { expect(component.showThreeDotsDropdown[i]).toBe(true); let fakeClickAwayEvent = new MouseEvent('click'); - Object.defineProperty( - fakeClickAwayEvent, - 'target', - {value: document.createElement('div')}); + Object.defineProperty(fakeClickAwayEvent, 'target', { + value: document.createElement('div'), + }); component.onDocumentClick(fakeClickAwayEvent); fixture.detectChanges(); expect(component.showThreeDotsDropdown[i]).toBe(false); diff --git a/core/templates/pages/learner-dashboard-page/goals-tab.component.ts b/core/templates/pages/learner-dashboard-page/goals-tab.component.ts index 45837916fbb1..6c01f48ea848 100644 --- a/core/templates/pages/learner-dashboard-page/goals-tab.component.ts +++ b/core/templates/pages/learner-dashboard-page/goals-tab.component.ts @@ -16,35 +16,41 @@ * @fileoverview Component for goals tab in the Learner Dashboard page. */ -import { AppConstants } from 'app.constants'; -import { Component, ElementRef, HostListener, Input, OnInit, ViewChild } from '@angular/core'; -import { LearnerTopicSummary } from 'domain/topic/learner-topic-summary.model'; -import { LearnerDashboardActivityBackendApiService } from 'domain/learner_dashboard/learner-dashboard-activity-backend-api.service'; -import { LearnerDashboardActivityIds } from 'domain/learner_dashboard/learner-dashboard-activity-ids.model'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { ClassroomDomainConstants } from 'domain/classroom/classroom-domain.constants'; -import { LearnerDashboardPageConstants } from './learner-dashboard-page.constants'; -import { DeviceInfoService } from 'services/contextual/device-info.service'; -import { Subscription } from 'rxjs'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; +import {AppConstants} from 'app.constants'; +import { + Component, + ElementRef, + HostListener, + Input, + OnInit, + ViewChild, +} from '@angular/core'; +import {LearnerTopicSummary} from 'domain/topic/learner-topic-summary.model'; +import {LearnerDashboardActivityBackendApiService} from 'domain/learner_dashboard/learner-dashboard-activity-backend-api.service'; +import {LearnerDashboardActivityIds} from 'domain/learner_dashboard/learner-dashboard-activity-ids.model'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {ClassroomDomainConstants} from 'domain/classroom/classroom-domain.constants'; +import {LearnerDashboardPageConstants} from './learner-dashboard-page.constants'; +import {DeviceInfoService} from 'services/contextual/device-info.service'; +import {Subscription} from 'rxjs'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; import './goals-tab.component.css'; @Component({ selector: 'oppia-goals-tab', templateUrl: './goals-tab.component.html', - styleUrls: ['./goals-tab.component.css'] + styleUrls: ['./goals-tab.component.css'], }) export class GoalsTabComponent implements OnInit { constructor( private windowDimensionService: WindowDimensionsService, private urlInterpolationService: UrlInterpolationService, private i18nLanguageCodeService: I18nLanguageCodeService, - private learnerDashboardActivityBackendApiService: - LearnerDashboardActivityBackendApiService, - private deviceInfoService: DeviceInfoService) { - } + private learnerDashboardActivityBackendApiService: LearnerDashboardActivityBackendApiService, + private deviceInfoService: DeviceInfoService + ) {} // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see @@ -58,7 +64,7 @@ export class GoalsTabComponent implements OnInit { // Child dropdown is undefined because initially it is in closed state using // the following property: {'static' = false}. - @ViewChild('dropdown', {'static': false}) dropdownRef: ElementRef | undefined; + @ViewChild('dropdown', {static: false}) dropdownRef: ElementRef | undefined; learnerDashboardActivityIds!: LearnerDashboardActivityIds; MAX_CURRENT_GOALS_LENGTH!: number; currentGoalsStoryIsShown!: boolean[]; @@ -74,7 +80,7 @@ export class GoalsTabComponent implements OnInit { topicToIndexMapping = { CURRENT: 0, COMPLETED: 1, - NEITHER: 2 + NEITHER: 2, }; indexOfSelectedTopic: number = -1; @@ -93,7 +99,8 @@ export class GoalsTabComponent implements OnInit { this.currentGoalsStoryIsShown[0] = true; this.pawImageUrl = this.getStaticImageUrl('/learner_dashboard/paw.svg'); this.bookImageUrl = this.getStaticImageUrl( - '/learner_dashboard/book_icon.png'); + '/learner_dashboard/book_icon.png' + ); this.starImageUrl = this.getStaticImageUrl('/learner_dashboard/star.svg'); let topic: LearnerTopicSummary; for (topic of this.currentGoals) { @@ -101,17 +108,21 @@ export class GoalsTabComponent implements OnInit { } for (topic of this.completedGoals) { this.topicIdsInCompletedGoals.push(topic.id); - this.completedGoalsTopicPageUrl.push(this.getTopicPageUrl( - topic.urlFragment, topic.classroom)); + this.completedGoalsTopicPageUrl.push( + this.getTopicPageUrl(topic.urlFragment, topic.classroom) + ); } for (topic of this.editGoals) { this.topicIdsInEditGoals.push(topic.id); - this.editGoalsTopicPageUrl.push(this.getTopicPageUrl( - topic.urlFragment, topic.classroom)); + this.editGoalsTopicPageUrl.push( + this.getTopicPageUrl(topic.urlFragment, topic.classroom) + ); this.editGoalsTopicClassification.push( - this.getTopicClassification(topic.id)); + this.getTopicClassification(topic.id) + ); this.editGoalsTopicBelongToLearntToPartiallyLearntTopic.push( - this.doesTopicBelongToLearntToPartiallyLearntTopics(topic.name)); + this.doesTopicBelongToLearntToPartiallyLearntTopics(topic.name) + ); } for (topic of this.partiallyLearntTopicsList) { this.topicIdsInPartiallyLearntTopics.push(topic.id); @@ -120,16 +131,21 @@ export class GoalsTabComponent implements OnInit { this.directiveSubscriptions.add( this.windowDimensionService.getResizeEvent().subscribe(() => { this.windowIsNarrow = this.windowDimensionService.isWindowNarrow(); - })); + }) + ); } getTopicPageUrl( - topicUrlFragment: string, classroomUrlFragment: string): string { + topicUrlFragment: string, + classroomUrlFragment: string + ): string { return this.urlInterpolationService.interpolateUrl( - ClassroomDomainConstants.TOPIC_VIEWER_URL_TEMPLATE, { + ClassroomDomainConstants.TOPIC_VIEWER_URL_TEMPLATE, + { topic_url_fragment: topicUrlFragment, - classroom_url_fragment: classroomUrlFragment - }); + classroom_url_fragment: classroomUrlFragment, + } + ); } getTopicClassification(topicId: string): number { @@ -154,31 +170,40 @@ export class GoalsTabComponent implements OnInit { } toggleStory(index: number): void { - this.currentGoalsStoryIsShown[index] = !( - this.currentGoalsStoryIsShown[index]); + this.currentGoalsStoryIsShown[index] = + !this.currentGoalsStoryIsShown[index]; } async addToLearnerGoals( - topic: LearnerTopicSummary, topicId: string, - index: number): Promise { + topic: LearnerTopicSummary, + topicId: string, + index: number + ): Promise { var activityId = topicId; var activityType = AppConstants.ACTIVITY_TYPE_LEARN_TOPIC; if (!this.topicIdsInCurrentGoals.includes(activityId)) { - var isSuccessfullyAdded = ( + var isSuccessfullyAdded = await this.learnerDashboardActivityBackendApiService.addToLearnerGoals( - activityId, activityType)); - if (isSuccessfullyAdded && + activityId, + activityType + ); + if ( + isSuccessfullyAdded && this.topicIdsInCurrentGoals.length < this.MAX_CURRENT_GOALS_LENGTH && - !this.topicIdsInCompletedGoals.includes(activityId)) { + !this.topicIdsInCompletedGoals.includes(activityId) + ) { this.currentGoalsStoryIsShown.push(false); this.currentGoals.push(topic); this.topicIdsInCurrentGoals.push(activityId); this.editGoalsTopicClassification.splice( - index, 1, this.getTopicClassification(topic.id)); + index, + 1, + this.getTopicClassification(topic.id) + ); if (this.untrackedTopics[topic.classroom]) { let indexInNewTopics = this.untrackedTopics[ - topic.classroom].findIndex( - topic => topic.id === topicId); + topic.classroom + ].findIndex(topic => topic.id === topicId); if (indexInNewTopics !== -1) { this.untrackedTopics[topic.classroom].splice(indexInNewTopics, 1); if (this.untrackedTopics[topic.classroom].length === 0) { @@ -211,35 +236,44 @@ export class GoalsTabComponent implements OnInit { onDocumentClick(event: MouseEvent): void { const targetElement = event.target as HTMLElement; for (let i = 0; i < this.currentGoals.length; i++) { - if (targetElement && - this.showThreeDotsDropdown[i]) { + if (targetElement && this.showThreeDotsDropdown[i]) { this.showThreeDotsDropdown[i] = false; } } } removeFromLearnerGoals( - topic: LearnerTopicSummary, topicId: string, - topicName: string, index: number): void { + topic: LearnerTopicSummary, + topicId: string, + topicName: string, + index: number + ): void { var activityId = topicId; var activityTitle = topicName; this.learnerDashboardActivityBackendApiService .removeActivityModalAsync( - LearnerDashboardPageConstants - .LEARNER_DASHBOARD_SECTION_I18N_IDS.CURRENT_GOALS - , LearnerDashboardPageConstants - .LEARNER_DASHBOARD_SUBSECTION_I18N_IDS.LEARN_TOPIC, - activityId, activityTitle) + LearnerDashboardPageConstants.LEARNER_DASHBOARD_SECTION_I18N_IDS + .CURRENT_GOALS, + LearnerDashboardPageConstants.LEARNER_DASHBOARD_SUBSECTION_I18N_IDS + .LEARN_TOPIC, + activityId, + activityTitle + ) .then(() => { this.currentGoalsStoryIsShown.splice(index, 1); this.currentGoals.splice(index, 1); this.topicIdsInCurrentGoals.splice(index, 1); let indexOfTopic = this.topicIdsInEditGoals.indexOf(topicId); this.editGoalsTopicClassification.splice( - indexOfTopic, 1, this.getTopicClassification(topicId)); - if (!this.topicIdsInCompletedGoals.includes(topicId) && + indexOfTopic, + 1, + this.getTopicClassification(topicId) + ); + if ( + !this.topicIdsInCompletedGoals.includes(topicId) && !this.topicIdsInCurrentGoals.includes(topicId) && - !this.topicIdsInPartiallyLearntTopics.includes(topicId)) { + !this.topicIdsInPartiallyLearntTopics.includes(topicId) + ) { if (this.untrackedTopics[topic.classroom]) { this.untrackedTopics[topic.classroom].push(topic); } else { diff --git a/core/templates/pages/learner-dashboard-page/home-tab.component.spec.ts b/core/templates/pages/learner-dashboard-page/home-tab.component.spec.ts index 2788d0cb65a9..3505d7fec794 100644 --- a/core/templates/pages/learner-dashboard-page/home-tab.component.spec.ts +++ b/core/templates/pages/learner-dashboard-page/home-tab.component.spec.ts @@ -16,19 +16,18 @@ * @fileoverview Unit tests for for HomeTabComponent. */ -import { async, ComponentFixture, TestBed } from - '@angular/core/testing'; -import { AppConstants } from 'app.constants'; -import { MaterialModule } from 'modules/material.module'; -import { FormsModule } from '@angular/forms'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { HomeTabComponent } from './home-tab.component'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { LearnerTopicSummary } from 'domain/topic/learner-topic-summary.model'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {AppConstants} from 'app.constants'; +import {MaterialModule} from 'modules/material.module'; +import {FormsModule} from '@angular/forms'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {HomeTabComponent} from './home-tab.component'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import {LearnerTopicSummary} from 'domain/topic/learner-topic-summary.model'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; describe('Home tab Component', () => { let component: HomeTabComponent; @@ -41,15 +40,8 @@ describe('Home tab Component', () => { beforeEach(async(() => { mockResizeEmitter = new EventEmitter(); TestBed.configureTestingModule({ - imports: [ - MaterialModule, - FormsModule, - HttpClientTestingModule - ], - declarations: [ - MockTranslatePipe, - HomeTabComponent - ], + imports: [MaterialModule, FormsModule, HttpClientTestingModule], + declarations: [MockTranslatePipe, HomeTabComponent], providers: [ UrlInterpolationService, { @@ -57,10 +49,10 @@ describe('Home tab Component', () => { useValue: { isWindowNarrow: () => true, getResizeEvent: () => mockResizeEmitter, - } - } + }, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -72,14 +64,15 @@ describe('Home tab Component', () => { i18nLanguageCodeService = TestBed.inject(I18nLanguageCodeService); spyOn(i18nLanguageCodeService, 'isCurrentLanguageRTL').and.returnValue( - true); + true + ); let subtopic = { skill_ids: ['skill_id_2'], id: 1, title: 'subtopic_name', thumbnail_filename: 'image.svg', thumbnail_bg_color: '#F8BF74', - url_fragment: 'subtopic-name' + url_fragment: 'subtopic-name', }; let nodeDict = { @@ -98,7 +91,7 @@ describe('Home tab Component', () => { planned_publication_date_msecs: 100, last_modified_msecs: 100, first_publication_date_msecs: 200, - unpublishing_reason: null + unpublishing_reason: null, }; const learnerTopicSummaryBackendDict1 = { id: 'sample_topic_id', @@ -112,33 +105,41 @@ describe('Home tab Component', () => { thumbnail_bg_color: '#C6DCDA', classroom: 'math', practice_tab_is_displayed: false, - canonical_story_summary_dict: [{ - id: '0', - title: 'Story Title', - description: 'Story Description', - node_titles: ['Chapter 1'], - thumbnail_filename: 'image.svg', - thumbnail_bg_color: '#F8BF74', - story_is_published: true, - completed_node_titles: ['Chapter 1'], - url_fragment: 'story-title', - all_node_dicts: [nodeDict] - }], + canonical_story_summary_dict: [ + { + id: '0', + title: 'Story Title', + description: 'Story Description', + node_titles: ['Chapter 1'], + thumbnail_filename: 'image.svg', + thumbnail_bg_color: '#F8BF74', + story_is_published: true, + completed_node_titles: ['Chapter 1'], + url_fragment: 'story-title', + all_node_dicts: [nodeDict], + }, + ], url_fragment: 'topic-name', subtopics: [subtopic], degrees_of_mastery: { skill_id_1: 0.5, - skill_id_2: 0.3 + skill_id_2: 0.3, }, skill_descriptions: { skill_id_1: 'Skill Description 1', - skill_id_2: 'Skill Description 2' - } + skill_id_2: 'Skill Description 2', + }, }; - component.currentGoals = [LearnerTopicSummary.createFromBackendDict( - learnerTopicSummaryBackendDict1)]; - component.goalTopics = [LearnerTopicSummary.createFromBackendDict( - learnerTopicSummaryBackendDict1)]; + component.currentGoals = [ + LearnerTopicSummary.createFromBackendDict( + learnerTopicSummaryBackendDict1 + ), + ]; + component.goalTopics = [ + LearnerTopicSummary.createFromBackendDict( + learnerTopicSummaryBackendDict1 + ), + ]; component.partiallyLearntTopicsList = []; component.untrackedTopics = {}; component.username = 'username'; @@ -165,8 +166,9 @@ describe('Home tab Component', () => { baseTime.setHours(11); jasmine.clock().mockDate(baseTime); - expect(component.getTimeOfDay()) - .toEqual('I18N_LEARNER_DASHBOARD_MORNING_GREETING'); + expect(component.getTimeOfDay()).toEqual( + 'I18N_LEARNER_DASHBOARD_MORNING_GREETING' + ); }); it('should get time of day as afternoon', () => { @@ -174,8 +176,9 @@ describe('Home tab Component', () => { baseTime.setHours(15); jasmine.clock().mockDate(baseTime); - expect(component.getTimeOfDay()) - .toEqual('I18N_LEARNER_DASHBOARD_AFTERNOON_GREETING'); + expect(component.getTimeOfDay()).toEqual( + 'I18N_LEARNER_DASHBOARD_AFTERNOON_GREETING' + ); }); it('should get time of day as evening', () => { @@ -183,8 +186,9 @@ describe('Home tab Component', () => { baseTime.setHours(20); jasmine.clock().mockDate(baseTime); - expect(component.getTimeOfDay()) - .toEqual('I18N_LEARNER_DASHBOARD_EVENING_GREETING'); + expect(component.getTimeOfDay()).toEqual( + 'I18N_LEARNER_DASHBOARD_EVENING_GREETING' + ); }); it('should switch the tab to Goals', () => { @@ -193,22 +197,25 @@ describe('Home tab Component', () => { expect(setActiveSection).toHaveBeenCalled(); }); - it('should check whether an object is non empty when calling ' + - '\'isNonemptyObject\'', () => { - let result = component.isNonemptyObject({}); - expect(result).toBe(false); + it( + 'should check whether an object is non empty when calling ' + + "'isNonemptyObject'", + () => { + let result = component.isNonemptyObject({}); + expect(result).toBe(false); - result = component.isNonemptyObject({description: 'description'}); - expect(result).toBe(true); - }); + result = component.isNonemptyObject({description: 'description'}); + expect(result).toBe(true); + } + ); it('should get the classroom link', () => { component.classroomUrlFragment = 'math'; const urlSpy = spyOn( - urlInterpolationService, 'interpolateUrl') - .and.returnValue('/learn/math'); - expect(component.getClassroomLink('math')).toEqual( - '/learn/math'); + urlInterpolationService, + 'interpolateUrl' + ).and.returnValue('/learn/math'); + expect(component.getClassroomLink('math')).toEqual('/learn/math'); expect(urlSpy).toHaveBeenCalled(); }); @@ -217,29 +224,38 @@ describe('Home tab Component', () => { expect(component.getWidth(3)).toEqual(662); }); - it('should show empty learn something new tab' + - '\'when goal selection limit is reached\'', () => { - component.currentGoalsLength = AppConstants.MAX_CURRENT_GOALS_COUNT; + it( + 'should show empty learn something new tab' + + "'when goal selection limit is reached'", + () => { + component.currentGoalsLength = AppConstants.MAX_CURRENT_GOALS_COUNT; - expect(component.isGoalLimitReached()).toBeTrue(); + expect(component.isGoalLimitReached()).toBeTrue(); - component.currentGoalsLength = 2; - component.goalTopicsLength = 2; - expect(component.isGoalLimitReached()).toBeTrue(); - }); + component.currentGoalsLength = 2; + component.goalTopicsLength = 2; + expect(component.isGoalLimitReached()).toBeTrue(); + } + ); - it('should not show empty learn something new tab' + - '\'when goal selection limit is not reached\'', () => { - component.goalTopicsLength = 0; - expect(component.isGoalLimitReached()).toBeFalse(); - }); + it( + 'should not show empty learn something new tab' + + "'when goal selection limit is not reached'", + () => { + component.goalTopicsLength = 0; + expect(component.isGoalLimitReached()).toBeFalse(); + } + ); - it('should not show empty learn something new tab' + - '\'when goal selection limit is reached and goal selection limit' + - ' is not reached\'', () => { - component.goalTopicsLength = 2; - component.currentGoalsLength = 0; - component.goalTopicsLength = 3; - expect(component.isGoalLimitReached()).toBeFalse(); - }); + it( + 'should not show empty learn something new tab' + + "'when goal selection limit is reached and goal selection limit" + + " is not reached'", + () => { + component.goalTopicsLength = 2; + component.currentGoalsLength = 0; + component.goalTopicsLength = 3; + expect(component.isGoalLimitReached()).toBeFalse(); + } + ); }); diff --git a/core/templates/pages/learner-dashboard-page/home-tab.component.ts b/core/templates/pages/learner-dashboard-page/home-tab.component.ts index b7053bd6dcf7..ca9cb6b0db23 100644 --- a/core/templates/pages/learner-dashboard-page/home-tab.component.ts +++ b/core/templates/pages/learner-dashboard-page/home-tab.component.ts @@ -16,22 +16,22 @@ * @fileoverview Component for home tab in the Learner Dashboard page. */ -import { AppConstants } from 'app.constants'; -import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { LearnerTopicSummary } from 'domain/topic/learner-topic-summary.model'; -import { LearnerDashboardPageConstants } from 'pages/learner-dashboard-page/learner-dashboard-page.constants'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { Subscription } from 'rxjs'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; +import {AppConstants} from 'app.constants'; +import {Component, EventEmitter, Input, Output} from '@angular/core'; +import {LearnerTopicSummary} from 'domain/topic/learner-topic-summary.model'; +import {LearnerDashboardPageConstants} from 'pages/learner-dashboard-page/learner-dashboard-page.constants'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {Subscription} from 'rxjs'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; import './home-tab.component.css'; - @Component({ - selector: 'oppia-home-tab', - templateUrl: './home-tab.component.html', - styleUrls: ['./home-tab.component.css'] - }) +@Component({ + selector: 'oppia-home-tab', + templateUrl: './home-tab.component.html', + styleUrls: ['./home-tab.component.css'], +}) export class HomeTabComponent { @Output() setActiveSection: EventEmitter = new EventEmitter(); // These properties are initialized using Angular lifecycle hooks @@ -56,11 +56,11 @@ export class HomeTabComponent { constructor( private i18nLanguageCodeService: I18nLanguageCodeService, private windowDimensionService: WindowDimensionsService, - private urlInterpolationService: UrlInterpolationService, + private urlInterpolationService: UrlInterpolationService ) {} ngOnInit(): void { - this.width = this.widthConst * (this.currentGoals.length); + this.width = this.widthConst * this.currentGoals.length; var allGoals = [...this.currentGoals, ...this.partiallyLearntTopicsList]; this.currentGoalsLength = this.currentGoals.length; this.goalTopicsLength = this.goalTopics.length; @@ -79,7 +79,8 @@ export class HomeTabComponent { this.directiveSubscriptions.add( this.windowDimensionService.getResizeEvent().subscribe(() => { this.windowIsNarrow = this.windowDimensionService.isWindowNarrow(); - })); + }) + ); } getTimeOfDay(): string { @@ -100,8 +101,9 @@ export class HomeTabComponent { getClassroomLink(classroomUrlFragment: string): string { this.classroomUrlFragment = classroomUrlFragment; return this.urlInterpolationService.interpolateUrl( - this.CLASSROOM_LINK_URL_TEMPLATE, { - classroom_url_fragment: this.classroomUrlFragment + this.CLASSROOM_LINK_URL_TEMPLATE, + { + classroom_url_fragment: this.classroomUrlFragment, } ); } @@ -119,7 +121,7 @@ export class HomeTabComponent { /** * If there are 3 or more topics for each untrackedTopic, the total * width of the section will be 662px in mobile view to enable scrolling. - */ + */ if (length >= 3) { return 662; } @@ -128,12 +130,13 @@ export class HomeTabComponent { * width of the section will be calculated by multiplying the addition of * number of topics and one classroom card with 164px in mobile view to * enable scrolling. - */ + */ return (length + 1) * 164; } changeActiveSection(): void { this.setActiveSection.emit( - LearnerDashboardPageConstants.LEARNER_DASHBOARD_SECTION_I18N_IDS.GOALS); + LearnerDashboardPageConstants.LEARNER_DASHBOARD_SECTION_I18N_IDS.GOALS + ); } } diff --git a/core/templates/pages/learner-dashboard-page/learner-dashboard-icons.component.spec.ts b/core/templates/pages/learner-dashboard-page/learner-dashboard-icons.component.spec.ts index e266f5e3a58a..cd06ee34f757 100644 --- a/core/templates/pages/learner-dashboard-page/learner-dashboard-icons.component.spec.ts +++ b/core/templates/pages/learner-dashboard-page/learner-dashboard-icons.component.spec.ts @@ -16,27 +16,33 @@ * @fileoverview Unit tests for LearnerDashboardIconsComponent. */ -import { async, ComponentFixture, fakeAsync, flushMicrotasks, TestBed, tick } from - '@angular/core/testing'; - -import { LearnerDashboardIdsBackendApiService } from 'domain/learner_dashboard/learner-dashboard-ids-backend-api.service'; -import { LearnerDashboardActivityBackendApiService } from 'domain/learner_dashboard/learner-dashboard-activity-backend-api.service'; -import { LearnerDashboardActivityIds } from 'domain/learner_dashboard/learner-dashboard-activity-ids.model'; - -import { LearnerDashboardIconsComponent } from './learner-dashboard-icons.component'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NgbModal, NgbModalRef, NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; -import { LearnerPlaylistModalComponent } from './modal-templates/learner-playlist-modal.component'; +import { + async, + ComponentFixture, + fakeAsync, + flushMicrotasks, + TestBed, + tick, +} from '@angular/core/testing'; + +import {LearnerDashboardIdsBackendApiService} from 'domain/learner_dashboard/learner-dashboard-ids-backend-api.service'; +import {LearnerDashboardActivityBackendApiService} from 'domain/learner_dashboard/learner-dashboard-activity-backend-api.service'; +import {LearnerDashboardActivityIds} from 'domain/learner_dashboard/learner-dashboard-activity-ids.model'; + +import {LearnerDashboardIconsComponent} from './learner-dashboard-icons.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {FormsModule} from '@angular/forms'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NgbModal, NgbModalRef, NgbModule} from '@ng-bootstrap/ng-bootstrap'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {BrowserDynamicTestingModule} from '@angular/platform-browser-dynamic/testing'; +import {LearnerPlaylistModalComponent} from './modal-templates/learner-playlist-modal.component'; class MockNgbModalRef { componentInstance = { activityId: null, activityTitle: null, - activityType: null + activityType: null, }; } @@ -44,44 +50,42 @@ describe('Learner Dashboard Icons Component', () => { let component: LearnerDashboardIconsComponent; let fixture: ComponentFixture; let ngbModal: NgbModal; - let learnerDashboardIdsBackendApiService: - LearnerDashboardIdsBackendApiService; - let learnerDashboardActivityBackendApiService: - LearnerDashboardActivityBackendApiService; + let learnerDashboardIdsBackendApiService: LearnerDashboardIdsBackendApiService; + let learnerDashboardActivityBackendApiService: LearnerDashboardActivityBackendApiService; let learnerPlaylistSpy: jasmine.Spy; beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [ - FormsModule, - HttpClientTestingModule, - NgbModule - ], + imports: [FormsModule, HttpClientTestingModule, NgbModule], declarations: [ LearnerDashboardIconsComponent, LearnerPlaylistModalComponent, - MockTranslatePipe + MockTranslatePipe, ], providers: [ LearnerDashboardIdsBackendApiService, - LearnerDashboardActivityBackendApiService + LearnerDashboardActivityBackendApiService, ], - schemas: [NO_ERRORS_SCHEMA] - }).overrideModule(BrowserDynamicTestingModule, { - set: { - entryComponents: [LearnerPlaylistModalComponent], - } - }).compileComponents(); + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideModule(BrowserDynamicTestingModule, { + set: { + entryComponents: [LearnerPlaylistModalComponent], + }, + }) + .compileComponents(); })); beforeEach(() => { ngbModal = TestBed.inject(NgbModal); fixture = TestBed.createComponent(LearnerDashboardIconsComponent); component = fixture.componentInstance; - learnerDashboardIdsBackendApiService = - TestBed.inject(LearnerDashboardIdsBackendApiService); - learnerDashboardActivityBackendApiService = - TestBed.inject(LearnerDashboardActivityBackendApiService); + learnerDashboardIdsBackendApiService = TestBed.inject( + LearnerDashboardIdsBackendApiService + ); + learnerDashboardActivityBackendApiService = TestBed.inject( + LearnerDashboardActivityBackendApiService + ); fixture.detectChanges(); learnerPlaylistSpy = spyOn( learnerDashboardActivityBackendApiService, @@ -90,8 +94,8 @@ describe('Learner Dashboard Icons Component', () => { }); it('should intialize the component and set values', fakeAsync(() => { - let learnerDashboardActivityIds = LearnerDashboardActivityIds - .createFromBackendDict({ + let learnerDashboardActivityIds = + LearnerDashboardActivityIds.createFromBackendDict({ incomplete_exploration_ids: [], incomplete_collection_ids: [], partially_learnt_topic_ids: [], @@ -103,677 +107,723 @@ describe('Learner Dashboard Icons Component', () => { all_topic_ids: [], untracked_topic_ids: [], exploration_playlist_ids: [], - collection_playlist_ids: [] + collection_playlist_ids: [], }); const learnerDashboardSpy = spyOn( - learnerDashboardIdsBackendApiService, 'fetchLearnerDashboardIdsAsync') - .and.callFake(async() => { - return Promise.resolve(learnerDashboardActivityIds); - }); + learnerDashboardIdsBackendApiService, + 'fetchLearnerDashboardIdsAsync' + ).and.callFake(async () => { + return Promise.resolve(learnerDashboardActivityIds); + }); component.ngOnInit(); tick(); fixture.detectChanges(); - expect(component.learnerDashboardActivityIds) - .toBe(learnerDashboardActivityIds); + expect(component.learnerDashboardActivityIds).toBe( + learnerDashboardActivityIds + ); expect(learnerDashboardSpy).toHaveBeenCalled(); - } - )); + })); it('should enable the tooltip', () => { component.enablePlaylistTooltip(); expect(component.playlistTooltipIsEnabled).toBe(true); - } - ); + }); it('should disable the tooltip', () => { component.disablePlaylistTooltip(); expect(component.playlistTooltipIsEnabled).toBe(false); - } - ); + }); - it('should return true if the activity can be added to' + - ' learner playlist', fakeAsync(() => { - component.isContainerNarrow = true; - component.learnerDashboardActivityIds = LearnerDashboardActivityIds - .createFromBackendDict({ - incomplete_exploration_ids: [], - incomplete_collection_ids: [], - partially_learnt_topic_ids: [], - completed_exploration_ids: [], - completed_collection_ids: [], - completed_story_ids: [], - learnt_topic_ids: [], - topic_ids_to_learn: [], - all_topic_ids: [], - untracked_topic_ids: [], - exploration_playlist_ids: [], - collection_playlist_ids: [] - }); + it( + 'should return true if the activity can be added to' + ' learner playlist', + fakeAsync(() => { + component.isContainerNarrow = true; + component.learnerDashboardActivityIds = + LearnerDashboardActivityIds.createFromBackendDict({ + incomplete_exploration_ids: [], + incomplete_collection_ids: [], + partially_learnt_topic_ids: [], + completed_exploration_ids: [], + completed_collection_ids: [], + completed_story_ids: [], + learnt_topic_ids: [], + topic_ids_to_learn: [], + all_topic_ids: [], + untracked_topic_ids: [], + exploration_playlist_ids: [], + collection_playlist_ids: [], + }); - let result = component.canActivityBeAddedToLearnerPlaylist('1'); + let result = component.canActivityBeAddedToLearnerPlaylist('1'); - fixture.detectChanges(); + fixture.detectChanges(); - expect(result).toBe(true); - } - )); + expect(result).toBe(true); + }) + ); - it('should return false if the activity can not be added to' + - ' learner playlist', fakeAsync(() => { - component.isContainerNarrow = true; - component.learnerDashboardActivityIds = LearnerDashboardActivityIds - .createFromBackendDict({ - incomplete_exploration_ids: [], - incomplete_collection_ids: [], - partially_learnt_topic_ids: [], - completed_exploration_ids: [], - completed_collection_ids: [], - completed_story_ids: [], - learnt_topic_ids: [], - topic_ids_to_learn: [], - all_topic_ids: [], - untracked_topic_ids: [], - exploration_playlist_ids: ['1'], - collection_playlist_ids: [] - }); + it( + 'should return false if the activity can not be added to' + + ' learner playlist', + fakeAsync(() => { + component.isContainerNarrow = true; + component.learnerDashboardActivityIds = + LearnerDashboardActivityIds.createFromBackendDict({ + incomplete_exploration_ids: [], + incomplete_collection_ids: [], + partially_learnt_topic_ids: [], + completed_exploration_ids: [], + completed_collection_ids: [], + completed_story_ids: [], + learnt_topic_ids: [], + topic_ids_to_learn: [], + all_topic_ids: [], + untracked_topic_ids: [], + exploration_playlist_ids: ['1'], + collection_playlist_ids: [], + }); - let result = component.canActivityBeAddedToLearnerPlaylist('1'); + let result = component.canActivityBeAddedToLearnerPlaylist('1'); - fixture.detectChanges(); + fixture.detectChanges(); - expect(result).toBe(false); - } - )); + expect(result).toBe(false); + }) + ); - it('should return true if the curent exploration belongs to the' + - ' learner playlist', fakeAsync(() => { - component.activityType = 'exploration'; - component.activityId = '1'; - component.learnerDashboardActivityIds = LearnerDashboardActivityIds - .createFromBackendDict({ - incomplete_exploration_ids: [], - incomplete_collection_ids: [], - partially_learnt_topic_ids: [], - completed_exploration_ids: [], - completed_collection_ids: [], - completed_story_ids: [], - learnt_topic_ids: [], - topic_ids_to_learn: [], - all_topic_ids: [], - untracked_topic_ids: [], - exploration_playlist_ids: ['1'], - collection_playlist_ids: [] - }); + it( + 'should return true if the curent exploration belongs to the' + + ' learner playlist', + fakeAsync(() => { + component.activityType = 'exploration'; + component.activityId = '1'; + component.learnerDashboardActivityIds = + LearnerDashboardActivityIds.createFromBackendDict({ + incomplete_exploration_ids: [], + incomplete_collection_ids: [], + partially_learnt_topic_ids: [], + completed_exploration_ids: [], + completed_collection_ids: [], + completed_story_ids: [], + learnt_topic_ids: [], + topic_ids_to_learn: [], + all_topic_ids: [], + untracked_topic_ids: [], + exploration_playlist_ids: ['1'], + collection_playlist_ids: [], + }); - let result = component.belongsToLearnerPlaylist(); + let result = component.belongsToLearnerPlaylist(); - fixture.detectChanges(); + fixture.detectChanges(); - expect(result).toBe(true); - } - )); + expect(result).toBe(true); + }) + ); - it('should return true if the curent collection belongs to the' + - ' learner playlist', fakeAsync(() => { - component.activityType = 'collection'; - component.activityId = '1'; - component.learnerDashboardActivityIds = LearnerDashboardActivityIds - .createFromBackendDict({ - incomplete_exploration_ids: [], - incomplete_collection_ids: [], - partially_learnt_topic_ids: [], - completed_exploration_ids: [], - completed_collection_ids: [], - completed_story_ids: [], - learnt_topic_ids: [], - topic_ids_to_learn: [], - all_topic_ids: [], - untracked_topic_ids: [], - exploration_playlist_ids: [], - collection_playlist_ids: ['1'] - }); + it( + 'should return true if the curent collection belongs to the' + + ' learner playlist', + fakeAsync(() => { + component.activityType = 'collection'; + component.activityId = '1'; + component.learnerDashboardActivityIds = + LearnerDashboardActivityIds.createFromBackendDict({ + incomplete_exploration_ids: [], + incomplete_collection_ids: [], + partially_learnt_topic_ids: [], + completed_exploration_ids: [], + completed_collection_ids: [], + completed_story_ids: [], + learnt_topic_ids: [], + topic_ids_to_learn: [], + all_topic_ids: [], + untracked_topic_ids: [], + exploration_playlist_ids: [], + collection_playlist_ids: ['1'], + }); - let result = component.belongsToLearnerPlaylist(); + let result = component.belongsToLearnerPlaylist(); - fixture.detectChanges(); + fixture.detectChanges(); - expect(result).toBe(true); - } - )); + expect(result).toBe(true); + }) + ); - it('should return true if the curent exploration belongs to the' + - ' completed playlist', fakeAsync(() => { - component.activityType = 'exploration'; - component.activityId = '1'; - component.learnerDashboardActivityIds = LearnerDashboardActivityIds - .createFromBackendDict({ - incomplete_exploration_ids: [], - incomplete_collection_ids: [], - partially_learnt_topic_ids: [], - completed_exploration_ids: ['1'], - completed_collection_ids: [], - completed_story_ids: [], - learnt_topic_ids: [], - topic_ids_to_learn: [], - all_topic_ids: [], - untracked_topic_ids: [], - exploration_playlist_ids: [], - collection_playlist_ids: [] - }); + it( + 'should return true if the curent exploration belongs to the' + + ' completed playlist', + fakeAsync(() => { + component.activityType = 'exploration'; + component.activityId = '1'; + component.learnerDashboardActivityIds = + LearnerDashboardActivityIds.createFromBackendDict({ + incomplete_exploration_ids: [], + incomplete_collection_ids: [], + partially_learnt_topic_ids: [], + completed_exploration_ids: ['1'], + completed_collection_ids: [], + completed_story_ids: [], + learnt_topic_ids: [], + topic_ids_to_learn: [], + all_topic_ids: [], + untracked_topic_ids: [], + exploration_playlist_ids: [], + collection_playlist_ids: [], + }); - let result = component.belongsToCompletedActivities(); + let result = component.belongsToCompletedActivities(); - fixture.detectChanges(); + fixture.detectChanges(); - expect(result).toBe(true); - } - )); + expect(result).toBe(true); + }) + ); - it('should return true if the curent collection belongs to the' + - ' completed playlist', fakeAsync(() => { - component.activityType = 'collection'; - component.activityId = '1'; - component.learnerDashboardActivityIds = LearnerDashboardActivityIds - .createFromBackendDict({ - incomplete_exploration_ids: [], - incomplete_collection_ids: [], - partially_learnt_topic_ids: [], - completed_exploration_ids: [], - completed_collection_ids: ['1'], - completed_story_ids: [], - learnt_topic_ids: [], - topic_ids_to_learn: [], - all_topic_ids: [], - untracked_topic_ids: [], - exploration_playlist_ids: [], - collection_playlist_ids: [] - }); + it( + 'should return true if the curent collection belongs to the' + + ' completed playlist', + fakeAsync(() => { + component.activityType = 'collection'; + component.activityId = '1'; + component.learnerDashboardActivityIds = + LearnerDashboardActivityIds.createFromBackendDict({ + incomplete_exploration_ids: [], + incomplete_collection_ids: [], + partially_learnt_topic_ids: [], + completed_exploration_ids: [], + completed_collection_ids: ['1'], + completed_story_ids: [], + learnt_topic_ids: [], + topic_ids_to_learn: [], + all_topic_ids: [], + untracked_topic_ids: [], + exploration_playlist_ids: [], + collection_playlist_ids: [], + }); - let result = component.belongsToCompletedActivities(); + let result = component.belongsToCompletedActivities(); - fixture.detectChanges(); + fixture.detectChanges(); - expect(result).toBe(true); - } - )); + expect(result).toBe(true); + }) + ); - it('should return true if the curent story belongs to the' + - ' completed playlist', fakeAsync(() => { - component.activityType = 'story'; - component.activityId = '1'; - component.learnerDashboardActivityIds = LearnerDashboardActivityIds - .createFromBackendDict({ - incomplete_exploration_ids: [], - incomplete_collection_ids: [], - partially_learnt_topic_ids: [], - completed_exploration_ids: [], - completed_collection_ids: [], - completed_story_ids: ['1'], - learnt_topic_ids: [], - topic_ids_to_learn: [], - all_topic_ids: [], - untracked_topic_ids: [], - exploration_playlist_ids: [], - collection_playlist_ids: [] - }); + it( + 'should return true if the curent story belongs to the' + + ' completed playlist', + fakeAsync(() => { + component.activityType = 'story'; + component.activityId = '1'; + component.learnerDashboardActivityIds = + LearnerDashboardActivityIds.createFromBackendDict({ + incomplete_exploration_ids: [], + incomplete_collection_ids: [], + partially_learnt_topic_ids: [], + completed_exploration_ids: [], + completed_collection_ids: [], + completed_story_ids: ['1'], + learnt_topic_ids: [], + topic_ids_to_learn: [], + all_topic_ids: [], + untracked_topic_ids: [], + exploration_playlist_ids: [], + collection_playlist_ids: [], + }); - let result = component.belongsToCompletedActivities(); + let result = component.belongsToCompletedActivities(); - fixture.detectChanges(); + fixture.detectChanges(); - expect(result).toBe(true); - } - )); + expect(result).toBe(true); + }) + ); - it('should return true if the curent topic belongs to the' + - ' learnt playlist', fakeAsync(() => { - component.activityType = 'learntopic'; - component.activityId = '1'; - component.learnerDashboardActivityIds = LearnerDashboardActivityIds - .createFromBackendDict({ - incomplete_exploration_ids: [], - incomplete_collection_ids: [], - partially_learnt_topic_ids: [], - completed_exploration_ids: [], - completed_collection_ids: [], - completed_story_ids: [], - learnt_topic_ids: ['1'], - topic_ids_to_learn: [], - all_topic_ids: [], - untracked_topic_ids: [], - exploration_playlist_ids: [], - collection_playlist_ids: [] - }); + it( + 'should return true if the curent topic belongs to the' + + ' learnt playlist', + fakeAsync(() => { + component.activityType = 'learntopic'; + component.activityId = '1'; + component.learnerDashboardActivityIds = + LearnerDashboardActivityIds.createFromBackendDict({ + incomplete_exploration_ids: [], + incomplete_collection_ids: [], + partially_learnt_topic_ids: [], + completed_exploration_ids: [], + completed_collection_ids: [], + completed_story_ids: [], + learnt_topic_ids: ['1'], + topic_ids_to_learn: [], + all_topic_ids: [], + untracked_topic_ids: [], + exploration_playlist_ids: [], + collection_playlist_ids: [], + }); - let result = component.belongsToCompletedActivities(); + let result = component.belongsToCompletedActivities(); - fixture.detectChanges(); + fixture.detectChanges(); - expect(result).toBe(true); - } - )); - - it('should return true if the curent exploration belongs to the' + - ' incomplete playlist', fakeAsync(() => { - component.activityType = 'exploration'; - component.activityId = '1'; - component.learnerDashboardActivityIds = LearnerDashboardActivityIds - .createFromBackendDict({ - incomplete_exploration_ids: ['1'], - incomplete_collection_ids: [], - partially_learnt_topic_ids: [], - completed_exploration_ids: [], - completed_collection_ids: [], - completed_story_ids: [], - learnt_topic_ids: [], - topic_ids_to_learn: [], - all_topic_ids: [], - untracked_topic_ids: [], - exploration_playlist_ids: [], - collection_playlist_ids: [] - }); + expect(result).toBe(true); + }) + ); - let result = component.belongsToIncompleteActivities(); + it( + 'should return true if the curent exploration belongs to the' + + ' incomplete playlist', + fakeAsync(() => { + component.activityType = 'exploration'; + component.activityId = '1'; + component.learnerDashboardActivityIds = + LearnerDashboardActivityIds.createFromBackendDict({ + incomplete_exploration_ids: ['1'], + incomplete_collection_ids: [], + partially_learnt_topic_ids: [], + completed_exploration_ids: [], + completed_collection_ids: [], + completed_story_ids: [], + learnt_topic_ids: [], + topic_ids_to_learn: [], + all_topic_ids: [], + untracked_topic_ids: [], + exploration_playlist_ids: [], + collection_playlist_ids: [], + }); - fixture.detectChanges(); + let result = component.belongsToIncompleteActivities(); - expect(result).toBe(true); - } - )); + fixture.detectChanges(); - it('should return true if the curent collection belongs to the' + - ' incomplete playlist', fakeAsync(() => { - component.activityType = 'collection'; - component.activityId = '1'; - component.learnerDashboardActivityIds = LearnerDashboardActivityIds - .createFromBackendDict({ - incomplete_exploration_ids: [], - incomplete_collection_ids: ['1'], - partially_learnt_topic_ids: [], - completed_exploration_ids: [], - completed_collection_ids: [], - completed_story_ids: [], - learnt_topic_ids: [], - topic_ids_to_learn: [], - all_topic_ids: [], - untracked_topic_ids: [], - exploration_playlist_ids: [], - collection_playlist_ids: [] - }); + expect(result).toBe(true); + }) + ); - let result = component.belongsToIncompleteActivities(); + it( + 'should return true if the curent collection belongs to the' + + ' incomplete playlist', + fakeAsync(() => { + component.activityType = 'collection'; + component.activityId = '1'; + component.learnerDashboardActivityIds = + LearnerDashboardActivityIds.createFromBackendDict({ + incomplete_exploration_ids: [], + incomplete_collection_ids: ['1'], + partially_learnt_topic_ids: [], + completed_exploration_ids: [], + completed_collection_ids: [], + completed_story_ids: [], + learnt_topic_ids: [], + topic_ids_to_learn: [], + all_topic_ids: [], + untracked_topic_ids: [], + exploration_playlist_ids: [], + collection_playlist_ids: [], + }); - fixture.detectChanges(); + let result = component.belongsToIncompleteActivities(); - expect(result).toBe(true); - } - )); + fixture.detectChanges(); - it('should return true if the curent topic belongs to the' + - ' partially learnt playlist', fakeAsync(() => { - component.activityType = 'learntopic'; - component.activityId = '1'; - component.learnerDashboardActivityIds = LearnerDashboardActivityIds - .createFromBackendDict({ - incomplete_exploration_ids: [], - incomplete_collection_ids: [], - partially_learnt_topic_ids: ['1'], - completed_exploration_ids: [], - completed_collection_ids: [], - completed_story_ids: [], - learnt_topic_ids: [], - topic_ids_to_learn: [], - all_topic_ids: [], - untracked_topic_ids: [], - exploration_playlist_ids: [], - collection_playlist_ids: [] - }); + expect(result).toBe(true); + }) + ); - let result = component.belongsToIncompleteActivities(); + it( + 'should return true if the curent topic belongs to the' + + ' partially learnt playlist', + fakeAsync(() => { + component.activityType = 'learntopic'; + component.activityId = '1'; + component.learnerDashboardActivityIds = + LearnerDashboardActivityIds.createFromBackendDict({ + incomplete_exploration_ids: [], + incomplete_collection_ids: [], + partially_learnt_topic_ids: ['1'], + completed_exploration_ids: [], + completed_collection_ids: [], + completed_story_ids: [], + learnt_topic_ids: [], + topic_ids_to_learn: [], + all_topic_ids: [], + untracked_topic_ids: [], + exploration_playlist_ids: [], + collection_playlist_ids: [], + }); - fixture.detectChanges(); + let result = component.belongsToIncompleteActivities(); - expect(result).toBe(true); - } - )); + fixture.detectChanges(); - it('should return false if the curent exploration belongs to the' + - ' learner playlist', fakeAsync(() => { - component.activityType = 'exploration'; - component.activityId = '1'; - component.learnerDashboardActivityIds = LearnerDashboardActivityIds - .createFromBackendDict({ - incomplete_exploration_ids: [], - incomplete_collection_ids: [], - partially_learnt_topic_ids: [], - completed_exploration_ids: [], - completed_collection_ids: [], - completed_story_ids: [], - learnt_topic_ids: [], - topic_ids_to_learn: [], - all_topic_ids: [], - untracked_topic_ids: [], - exploration_playlist_ids: [], - collection_playlist_ids: [] - }); + expect(result).toBe(true); + }) + ); - let result = component.belongsToLearnerPlaylist(); + it( + 'should return false if the curent exploration belongs to the' + + ' learner playlist', + fakeAsync(() => { + component.activityType = 'exploration'; + component.activityId = '1'; + component.learnerDashboardActivityIds = + LearnerDashboardActivityIds.createFromBackendDict({ + incomplete_exploration_ids: [], + incomplete_collection_ids: [], + partially_learnt_topic_ids: [], + completed_exploration_ids: [], + completed_collection_ids: [], + completed_story_ids: [], + learnt_topic_ids: [], + topic_ids_to_learn: [], + all_topic_ids: [], + untracked_topic_ids: [], + exploration_playlist_ids: [], + collection_playlist_ids: [], + }); - fixture.detectChanges(); + let result = component.belongsToLearnerPlaylist(); - expect(result).toBe(false); - } - )); + fixture.detectChanges(); - it('should return false if the curent collection belongs to the' + - ' learner playlist', fakeAsync(() => { - component.activityType = 'collection'; - component.activityId = '1'; - component.learnerDashboardActivityIds = LearnerDashboardActivityIds - .createFromBackendDict({ - incomplete_exploration_ids: [], - incomplete_collection_ids: [], - partially_learnt_topic_ids: [], - completed_exploration_ids: [], - completed_collection_ids: [], - completed_story_ids: [], - learnt_topic_ids: [], - topic_ids_to_learn: [], - all_topic_ids: [], - untracked_topic_ids: [], - exploration_playlist_ids: [], - collection_playlist_ids: [] - }); + expect(result).toBe(false); + }) + ); - let result = component.belongsToLearnerPlaylist(); + it( + 'should return false if the curent collection belongs to the' + + ' learner playlist', + fakeAsync(() => { + component.activityType = 'collection'; + component.activityId = '1'; + component.learnerDashboardActivityIds = + LearnerDashboardActivityIds.createFromBackendDict({ + incomplete_exploration_ids: [], + incomplete_collection_ids: [], + partially_learnt_topic_ids: [], + completed_exploration_ids: [], + completed_collection_ids: [], + completed_story_ids: [], + learnt_topic_ids: [], + topic_ids_to_learn: [], + all_topic_ids: [], + untracked_topic_ids: [], + exploration_playlist_ids: [], + collection_playlist_ids: [], + }); - fixture.detectChanges(); + let result = component.belongsToLearnerPlaylist(); - expect(result).toBe(false); - } - )); + fixture.detectChanges(); - it('should return false if the curent exploration belongs to the' + - ' completed playlist', fakeAsync(() => { - component.activityType = 'exploration'; - component.activityId = '1'; - component.learnerDashboardActivityIds = LearnerDashboardActivityIds - .createFromBackendDict({ - incomplete_exploration_ids: [], - incomplete_collection_ids: [], - partially_learnt_topic_ids: [], - completed_exploration_ids: [], - completed_collection_ids: [], - completed_story_ids: [], - learnt_topic_ids: [], - topic_ids_to_learn: [], - all_topic_ids: [], - untracked_topic_ids: [], - exploration_playlist_ids: [], - collection_playlist_ids: [] - }); + expect(result).toBe(false); + }) + ); - let result = component.belongsToCompletedActivities(); + it( + 'should return false if the curent exploration belongs to the' + + ' completed playlist', + fakeAsync(() => { + component.activityType = 'exploration'; + component.activityId = '1'; + component.learnerDashboardActivityIds = + LearnerDashboardActivityIds.createFromBackendDict({ + incomplete_exploration_ids: [], + incomplete_collection_ids: [], + partially_learnt_topic_ids: [], + completed_exploration_ids: [], + completed_collection_ids: [], + completed_story_ids: [], + learnt_topic_ids: [], + topic_ids_to_learn: [], + all_topic_ids: [], + untracked_topic_ids: [], + exploration_playlist_ids: [], + collection_playlist_ids: [], + }); - fixture.detectChanges(); + let result = component.belongsToCompletedActivities(); - expect(result).toBe(false); - } - )); + fixture.detectChanges(); + + expect(result).toBe(false); + }) + ); - it('should return false if the curent collection belongs to the' + - ' completed playlist', fakeAsync(() => { - component.activityType = 'collection'; - component.activityId = '1'; - component.learnerDashboardActivityIds = LearnerDashboardActivityIds - .createFromBackendDict({ - incomplete_exploration_ids: [], - incomplete_collection_ids: [], - partially_learnt_topic_ids: [], - completed_exploration_ids: [], - completed_collection_ids: [], - completed_story_ids: [], - learnt_topic_ids: [], - topic_ids_to_learn: [], - all_topic_ids: [], - untracked_topic_ids: [], - exploration_playlist_ids: [], - collection_playlist_ids: [] - }); + it( + 'should return false if the curent collection belongs to the' + + ' completed playlist', + fakeAsync(() => { + component.activityType = 'collection'; + component.activityId = '1'; + component.learnerDashboardActivityIds = + LearnerDashboardActivityIds.createFromBackendDict({ + incomplete_exploration_ids: [], + incomplete_collection_ids: [], + partially_learnt_topic_ids: [], + completed_exploration_ids: [], + completed_collection_ids: [], + completed_story_ids: [], + learnt_topic_ids: [], + topic_ids_to_learn: [], + all_topic_ids: [], + untracked_topic_ids: [], + exploration_playlist_ids: [], + collection_playlist_ids: [], + }); - let result = component.belongsToCompletedActivities(); + let result = component.belongsToCompletedActivities(); - fixture.detectChanges(); + fixture.detectChanges(); - expect(result).toBe(false); - } - )); + expect(result).toBe(false); + }) + ); - it('should return false if the curent story belongs to the' + - ' completed playlist', fakeAsync(() => { - component.activityType = 'story'; - component.activityId = '1'; - component.learnerDashboardActivityIds = LearnerDashboardActivityIds - .createFromBackendDict({ - incomplete_exploration_ids: [], - incomplete_collection_ids: [], - partially_learnt_topic_ids: [], - completed_exploration_ids: [], - completed_collection_ids: [], - completed_story_ids: [], - learnt_topic_ids: [], - topic_ids_to_learn: [], - all_topic_ids: [], - untracked_topic_ids: [], - exploration_playlist_ids: [], - collection_playlist_ids: [] - }); + it( + 'should return false if the curent story belongs to the' + + ' completed playlist', + fakeAsync(() => { + component.activityType = 'story'; + component.activityId = '1'; + component.learnerDashboardActivityIds = + LearnerDashboardActivityIds.createFromBackendDict({ + incomplete_exploration_ids: [], + incomplete_collection_ids: [], + partially_learnt_topic_ids: [], + completed_exploration_ids: [], + completed_collection_ids: [], + completed_story_ids: [], + learnt_topic_ids: [], + topic_ids_to_learn: [], + all_topic_ids: [], + untracked_topic_ids: [], + exploration_playlist_ids: [], + collection_playlist_ids: [], + }); - let result = component.belongsToCompletedActivities(); + let result = component.belongsToCompletedActivities(); - fixture.detectChanges(); + fixture.detectChanges(); - expect(result).toBe(false); - } - )); + expect(result).toBe(false); + }) + ); - it('should return false if the curent topic belongs to the' + - ' learnt playlist', fakeAsync(() => { - component.activityType = 'learntopic'; - component.activityId = '1'; - component.learnerDashboardActivityIds = LearnerDashboardActivityIds - .createFromBackendDict({ - incomplete_exploration_ids: [], - incomplete_collection_ids: [], - partially_learnt_topic_ids: [], - completed_exploration_ids: [], - completed_collection_ids: [], - completed_story_ids: [], - learnt_topic_ids: [], - topic_ids_to_learn: [], - all_topic_ids: [], - untracked_topic_ids: [], - exploration_playlist_ids: [], - collection_playlist_ids: [] - }); + it( + 'should return false if the curent topic belongs to the' + + ' learnt playlist', + fakeAsync(() => { + component.activityType = 'learntopic'; + component.activityId = '1'; + component.learnerDashboardActivityIds = + LearnerDashboardActivityIds.createFromBackendDict({ + incomplete_exploration_ids: [], + incomplete_collection_ids: [], + partially_learnt_topic_ids: [], + completed_exploration_ids: [], + completed_collection_ids: [], + completed_story_ids: [], + learnt_topic_ids: [], + topic_ids_to_learn: [], + all_topic_ids: [], + untracked_topic_ids: [], + exploration_playlist_ids: [], + collection_playlist_ids: [], + }); - let result = component.belongsToCompletedActivities(); + let result = component.belongsToCompletedActivities(); - fixture.detectChanges(); + fixture.detectChanges(); - expect(result).toBe(false); - } - )); + expect(result).toBe(false); + }) + ); - it('should return false if the curent exploration belongs to the' + - ' incomplete playlist', fakeAsync(() => { - component.activityType = 'exploration'; - component.activityId = '1'; - component.learnerDashboardActivityIds = LearnerDashboardActivityIds - .createFromBackendDict({ - incomplete_exploration_ids: [], - incomplete_collection_ids: [], - partially_learnt_topic_ids: [], - completed_exploration_ids: [], - completed_collection_ids: [], - completed_story_ids: [], - learnt_topic_ids: [], - topic_ids_to_learn: [], - all_topic_ids: [], - untracked_topic_ids: [], - exploration_playlist_ids: [], - collection_playlist_ids: [] - }); + it( + 'should return false if the curent exploration belongs to the' + + ' incomplete playlist', + fakeAsync(() => { + component.activityType = 'exploration'; + component.activityId = '1'; + component.learnerDashboardActivityIds = + LearnerDashboardActivityIds.createFromBackendDict({ + incomplete_exploration_ids: [], + incomplete_collection_ids: [], + partially_learnt_topic_ids: [], + completed_exploration_ids: [], + completed_collection_ids: [], + completed_story_ids: [], + learnt_topic_ids: [], + topic_ids_to_learn: [], + all_topic_ids: [], + untracked_topic_ids: [], + exploration_playlist_ids: [], + collection_playlist_ids: [], + }); - let result = component.belongsToIncompleteActivities(); + let result = component.belongsToIncompleteActivities(); - fixture.detectChanges(); + fixture.detectChanges(); - expect(result).toBe(false); - } - )); + expect(result).toBe(false); + }) + ); - it('should return false if the curent collection belongs to the' + - ' incomplete playlist', fakeAsync(() => { - component.activityType = 'collection'; - component.activityId = '1'; - component.learnerDashboardActivityIds = LearnerDashboardActivityIds - .createFromBackendDict({ - incomplete_exploration_ids: [], - incomplete_collection_ids: [], - partially_learnt_topic_ids: [], - completed_exploration_ids: [], - completed_collection_ids: [], - completed_story_ids: [], - learnt_topic_ids: [], - topic_ids_to_learn: [], - all_topic_ids: [], - untracked_topic_ids: [], - exploration_playlist_ids: [], - collection_playlist_ids: [] - }); + it( + 'should return false if the curent collection belongs to the' + + ' incomplete playlist', + fakeAsync(() => { + component.activityType = 'collection'; + component.activityId = '1'; + component.learnerDashboardActivityIds = + LearnerDashboardActivityIds.createFromBackendDict({ + incomplete_exploration_ids: [], + incomplete_collection_ids: [], + partially_learnt_topic_ids: [], + completed_exploration_ids: [], + completed_collection_ids: [], + completed_story_ids: [], + learnt_topic_ids: [], + topic_ids_to_learn: [], + all_topic_ids: [], + untracked_topic_ids: [], + exploration_playlist_ids: [], + collection_playlist_ids: [], + }); - let result = component.belongsToIncompleteActivities(); + let result = component.belongsToIncompleteActivities(); - fixture.detectChanges(); + fixture.detectChanges(); - expect(result).toBe(false); - } - )); + expect(result).toBe(false); + }) + ); - it('should return false if the curent topic belongs to the' + - ' partially learnt playlist', fakeAsync(() => { - component.activityType = 'learntopic'; - component.activityId = '1'; - component.learnerDashboardActivityIds = LearnerDashboardActivityIds - .createFromBackendDict({ - incomplete_exploration_ids: [], - incomplete_collection_ids: [], - partially_learnt_topic_ids: [], - completed_exploration_ids: [], - completed_collection_ids: [], - completed_story_ids: [], - learnt_topic_ids: [], - topic_ids_to_learn: [], - all_topic_ids: [], - untracked_topic_ids: [], - exploration_playlist_ids: [], - collection_playlist_ids: [] - }); + it( + 'should return false if the curent topic belongs to the' + + ' partially learnt playlist', + fakeAsync(() => { + component.activityType = 'learntopic'; + component.activityId = '1'; + component.learnerDashboardActivityIds = + LearnerDashboardActivityIds.createFromBackendDict({ + incomplete_exploration_ids: [], + incomplete_collection_ids: [], + partially_learnt_topic_ids: [], + completed_exploration_ids: [], + completed_collection_ids: [], + completed_story_ids: [], + learnt_topic_ids: [], + topic_ids_to_learn: [], + all_topic_ids: [], + untracked_topic_ids: [], + exploration_playlist_ids: [], + collection_playlist_ids: [], + }); - let result = component.belongsToIncompleteActivities(); + let result = component.belongsToIncompleteActivities(); - fixture.detectChanges(); + fixture.detectChanges(); - expect(result).toBe(false); - } - )); + expect(result).toBe(false); + }) + ); - it('should open a modal for removing exploration from' + - ' learner playlist', fakeAsync(() => { - const learnerDashboardActivityIds = LearnerDashboardActivityIds - .createFromBackendDict({ - incomplete_exploration_ids: [], - incomplete_collection_ids: [], - completed_exploration_ids: [], - completed_collection_ids: [], - exploration_playlist_ids: [], - collection_playlist_ids: [], - completed_story_ids: [], - learnt_topic_ids: [], - partially_learnt_topic_ids: [], - topic_ids_to_learn: [], - all_topic_ids: [], - untracked_topic_ids: [] + it( + 'should open a modal for removing exploration from' + ' learner playlist', + fakeAsync(() => { + const learnerDashboardActivityIds = + LearnerDashboardActivityIds.createFromBackendDict({ + incomplete_exploration_ids: [], + incomplete_collection_ids: [], + completed_exploration_ids: [], + completed_collection_ids: [], + exploration_playlist_ids: [], + collection_playlist_ids: [], + completed_story_ids: [], + learnt_topic_ids: [], + partially_learnt_topic_ids: [], + topic_ids_to_learn: [], + all_topic_ids: [], + untracked_topic_ids: [], + }); + component.learnerDashboardActivityIds = learnerDashboardActivityIds; + const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + componentInstance: MockNgbModalRef, + result: Promise.resolve('url'), + } as NgbModalRef; }); - component.learnerDashboardActivityIds = learnerDashboardActivityIds; - const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ( - { componentInstance: MockNgbModalRef, - result: Promise.resolve('url') - } as NgbModalRef); - }); - let activityId = '1'; - let activityTitle = 'Title'; - let activityType = 'exploration'; + let activityId = '1'; + let activityTitle = 'Title'; + let activityType = 'exploration'; - component.removeFromLearnerPlaylist( - activityId, activityTitle, activityType); - fixture.detectChanges(); - flushMicrotasks(); + component.removeFromLearnerPlaylist( + activityId, + activityTitle, + activityType + ); + fixture.detectChanges(); + flushMicrotasks(); - expect(modalSpy).toHaveBeenCalled(); - expect(learnerPlaylistSpy).toHaveBeenCalled(); - } - )); + expect(modalSpy).toHaveBeenCalled(); + expect(learnerPlaylistSpy).toHaveBeenCalled(); + }) + ); - it('should open a modal for removing collection from' + - ' learner playlist', fakeAsync(() => { - const learnerDashboardActivityIds = LearnerDashboardActivityIds - .createFromBackendDict({ - incomplete_exploration_ids: [], - incomplete_collection_ids: [], - completed_exploration_ids: [], - completed_collection_ids: [], - exploration_playlist_ids: [], - collection_playlist_ids: [], - completed_story_ids: [], - learnt_topic_ids: [], - partially_learnt_topic_ids: [], - topic_ids_to_learn: [], - all_topic_ids: [], - untracked_topic_ids: [] + it( + 'should open a modal for removing collection from' + ' learner playlist', + fakeAsync(() => { + const learnerDashboardActivityIds = + LearnerDashboardActivityIds.createFromBackendDict({ + incomplete_exploration_ids: [], + incomplete_collection_ids: [], + completed_exploration_ids: [], + completed_collection_ids: [], + exploration_playlist_ids: [], + collection_playlist_ids: [], + completed_story_ids: [], + learnt_topic_ids: [], + partially_learnt_topic_ids: [], + topic_ids_to_learn: [], + all_topic_ids: [], + untracked_topic_ids: [], + }); + component.learnerDashboardActivityIds = learnerDashboardActivityIds; + const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + componentInstance: MockNgbModalRef, + result: Promise.resolve('url'), + } as NgbModalRef; }); - component.learnerDashboardActivityIds = learnerDashboardActivityIds; - const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ( - { componentInstance: MockNgbModalRef, - result: Promise.resolve('url') - } as NgbModalRef); - }); - let activityId = '1'; - let activityTitle = 'Title'; - let activityType = 'collection'; + let activityId = '1'; + let activityTitle = 'Title'; + let activityType = 'collection'; - component.removeFromLearnerPlaylist( - activityId, activityTitle, activityType); - fixture.detectChanges(); - flushMicrotasks(); + component.removeFromLearnerPlaylist( + activityId, + activityTitle, + activityType + ); + fixture.detectChanges(); + flushMicrotasks(); - expect(modalSpy).toHaveBeenCalled(); - expect(learnerPlaylistSpy).toHaveBeenCalled(); - } - )); + expect(modalSpy).toHaveBeenCalled(); + expect(learnerPlaylistSpy).toHaveBeenCalled(); + }) + ); it( 'should open an ngbModal when removing from learner playlist' + - ' when calling removeFromLearnerPlaylistModal', + ' when calling removeFromLearnerPlaylistModal', fakeAsync(() => { - const learnerDashboardActivityIds = LearnerDashboardActivityIds - .createFromBackendDict({ + const learnerDashboardActivityIds = + LearnerDashboardActivityIds.createFromBackendDict({ incomplete_exploration_ids: [], incomplete_collection_ids: [], completed_exploration_ids: [], @@ -785,46 +835,50 @@ describe('Learner Dashboard Icons Component', () => { partially_learnt_topic_ids: [], topic_ids_to_learn: [], all_topic_ids: [], - untracked_topic_ids: [] + untracked_topic_ids: [], }); component.learnerDashboardActivityIds = learnerDashboardActivityIds; const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ( - { componentInstance: MockNgbModalRef, - result: Promise.resolve('url') - } as NgbModalRef); + return { + componentInstance: MockNgbModalRef, + result: Promise.resolve('url'), + } as NgbModalRef; }); - component.removeFromLearnerPlaylist( - '0', 'title', 'exploration'); + component.removeFromLearnerPlaylist('0', 'title', 'exploration'); expect(modalSpy).toHaveBeenCalled(); flushMicrotasks(); expect(learnerPlaylistSpy).toHaveBeenCalledWith( - '0', 'exploration', learnerDashboardActivityIds, 'url'); - })); + '0', + 'exploration', + learnerDashboardActivityIds, + 'url' + ); + }) + ); it( 'should not remove anything from learner playlist when cancel ' + - 'button is clicked when calling removeFromLearnerPlaylistModal', + 'button is clicked when calling removeFromLearnerPlaylistModal', fakeAsync(() => { spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ( - { componentInstance: MockNgbModalRef, - result: Promise.reject('success') - } as NgbModalRef); + return { + componentInstance: MockNgbModalRef, + result: Promise.reject('success'), + } as NgbModalRef; }); - component.removeFromLearnerPlaylist( - '0', 'title', 'collection'); + component.removeFromLearnerPlaylist('0', 'title', 'collection'); flushMicrotasks(); expect(learnerPlaylistSpy).not.toHaveBeenCalled(); - })); + }) + ); it('should add exploration to learner playlist', fakeAsync(() => { let activityId = '1'; let activityType = 'exploration'; - component.learnerDashboardActivityIds = LearnerDashboardActivityIds - .createFromBackendDict({ + component.learnerDashboardActivityIds = + LearnerDashboardActivityIds.createFromBackendDict({ incomplete_exploration_ids: [], incomplete_collection_ids: [], partially_learnt_topic_ids: [], @@ -836,32 +890,32 @@ describe('Learner Dashboard Icons Component', () => { all_topic_ids: [], untracked_topic_ids: [], exploration_playlist_ids: [], - collection_playlist_ids: [] + collection_playlist_ids: [], }); - const learnerPlaylistSpy = - spyOn(learnerDashboardActivityBackendApiService, 'addToLearnerPlaylist') - .and.returnValue(Promise.resolve(true)); + const learnerPlaylistSpy = spyOn( + learnerDashboardActivityBackendApiService, + 'addToLearnerPlaylist' + ).and.returnValue(Promise.resolve(true)); - component.addToLearnerPlaylist( - activityId, activityType); + component.addToLearnerPlaylist(activityId, activityType); tick(); fixture.detectChanges(); expect(learnerPlaylistSpy).toHaveBeenCalled(); - expect(component.learnerDashboardActivityIds.explorationPlaylistIds) - .toEqual(['1']); - } - )); + expect( + component.learnerDashboardActivityIds.explorationPlaylistIds + ).toEqual(['1']); + })); it('should add collection to learner playlist', fakeAsync(() => { let activityId = '1'; let activityType = 'collection'; - component.learnerDashboardActivityIds = LearnerDashboardActivityIds - .createFromBackendDict({ + component.learnerDashboardActivityIds = + LearnerDashboardActivityIds.createFromBackendDict({ incomplete_exploration_ids: [], incomplete_collection_ids: [], partially_learnt_topic_ids: [], @@ -873,32 +927,32 @@ describe('Learner Dashboard Icons Component', () => { all_topic_ids: [], untracked_topic_ids: [], exploration_playlist_ids: [], - collection_playlist_ids: [] + collection_playlist_ids: [], }); - const learnerPlaylistSpy = - spyOn(learnerDashboardActivityBackendApiService, 'addToLearnerPlaylist') - .and.returnValue(Promise.resolve(true)); + const learnerPlaylistSpy = spyOn( + learnerDashboardActivityBackendApiService, + 'addToLearnerPlaylist' + ).and.returnValue(Promise.resolve(true)); - component.addToLearnerPlaylist( - activityId, activityType); + component.addToLearnerPlaylist(activityId, activityType); tick(); fixture.detectChanges(); expect(learnerPlaylistSpy).toHaveBeenCalled(); - expect(component.learnerDashboardActivityIds.collectionPlaylistIds) - .toEqual(['1']); - } - )); + expect(component.learnerDashboardActivityIds.collectionPlaylistIds).toEqual( + ['1'] + ); + })); it('should fail to add exploration to learner playlist', fakeAsync(() => { let activityId = '1'; let activityType = 'exploration'; - component.learnerDashboardActivityIds = LearnerDashboardActivityIds - .createFromBackendDict({ + component.learnerDashboardActivityIds = + LearnerDashboardActivityIds.createFromBackendDict({ incomplete_exploration_ids: [], incomplete_collection_ids: [], partially_learnt_topic_ids: [], @@ -910,30 +964,30 @@ describe('Learner Dashboard Icons Component', () => { all_topic_ids: [], untracked_topic_ids: [], exploration_playlist_ids: [], - collection_playlist_ids: [] + collection_playlist_ids: [], }); - const learnerPlaylistSpy = - spyOn(learnerDashboardActivityBackendApiService, 'addToLearnerPlaylist') - .and.returnValue(Promise.resolve(true)); + const learnerPlaylistSpy = spyOn( + learnerDashboardActivityBackendApiService, + 'addToLearnerPlaylist' + ).and.returnValue(Promise.resolve(true)); - component.addToLearnerPlaylist( - activityId, activityType); + component.addToLearnerPlaylist(activityId, activityType); fixture.detectChanges(); expect(learnerPlaylistSpy).toHaveBeenCalled(); - expect(component.learnerDashboardActivityIds.explorationPlaylistIds) - .toEqual([]); - } - )); + expect( + component.learnerDashboardActivityIds.explorationPlaylistIds + ).toEqual([]); + })); it('should fail to add collection to learner playlist', fakeAsync(() => { let activityId = '1'; let activityType = 'collection'; - component.learnerDashboardActivityIds = LearnerDashboardActivityIds - .createFromBackendDict({ + component.learnerDashboardActivityIds = + LearnerDashboardActivityIds.createFromBackendDict({ incomplete_exploration_ids: [], incomplete_collection_ids: [], partially_learnt_topic_ids: [], @@ -945,21 +999,21 @@ describe('Learner Dashboard Icons Component', () => { all_topic_ids: [], untracked_topic_ids: [], exploration_playlist_ids: [], - collection_playlist_ids: [] + collection_playlist_ids: [], }); - const learnerPlaylistSpy = - spyOn(learnerDashboardActivityBackendApiService, 'addToLearnerPlaylist') - .and.returnValue(Promise.resolve(true)); + const learnerPlaylistSpy = spyOn( + learnerDashboardActivityBackendApiService, + 'addToLearnerPlaylist' + ).and.returnValue(Promise.resolve(true)); - component.addToLearnerPlaylist( - activityId, activityType); + component.addToLearnerPlaylist(activityId, activityType); fixture.detectChanges(); expect(learnerPlaylistSpy).toHaveBeenCalled(); - expect(component.learnerDashboardActivityIds.collectionPlaylistIds) - .toEqual([]); - } - )); + expect(component.learnerDashboardActivityIds.collectionPlaylistIds).toEqual( + [] + ); + })); }); diff --git a/core/templates/pages/learner-dashboard-page/learner-dashboard-icons.component.ts b/core/templates/pages/learner-dashboard-page/learner-dashboard-icons.component.ts index 4f1d93506bea..0665b921001a 100644 --- a/core/templates/pages/learner-dashboard-page/learner-dashboard-icons.component.ts +++ b/core/templates/pages/learner-dashboard-page/learner-dashboard-icons.component.ts @@ -16,17 +16,14 @@ * @fileoverview Component for showing learner dashboard icons. */ -import { Component, OnInit, Input } from '@angular/core'; +import {Component, OnInit, Input} from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { LearnerDashboardIdsBackendApiService } from - 'domain/learner_dashboard/learner-dashboard-ids-backend-api.service'; -import { LearnerDashboardActivityBackendApiService } from - 'domain/learner_dashboard/learner-dashboard-activity-backend-api.service'; -import { LearnerDashboardActivityIds } from - 'domain/learner_dashboard/learner-dashboard-activity-ids.model'; -import { LearnerPlaylistModalComponent } from './modal-templates/learner-playlist-modal.component'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import {AppConstants} from 'app.constants'; +import {LearnerDashboardIdsBackendApiService} from 'domain/learner_dashboard/learner-dashboard-ids-backend-api.service'; +import {LearnerDashboardActivityBackendApiService} from 'domain/learner_dashboard/learner-dashboard-activity-backend-api.service'; +import {LearnerDashboardActivityIds} from 'domain/learner_dashboard/learner-dashboard-activity-ids.model'; +import {LearnerPlaylistModalComponent} from './modal-templates/learner-playlist-modal.component'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; @Component({ selector: 'oppia-learner-dashboard-icons', @@ -46,23 +43,19 @@ export class LearnerDashboardIconsComponent implements OnInit { playlistTooltipIsEnabled: boolean = false; constructor( - private learnerDashboardIdsBackendApiService: - LearnerDashboardIdsBackendApiService, - private learnerDashboardActivityBackendApiService: - LearnerDashboardActivityBackendApiService, + private learnerDashboardIdsBackendApiService: LearnerDashboardIdsBackendApiService, + private learnerDashboardActivityBackendApiService: LearnerDashboardActivityBackendApiService, private ngbModal: NgbModal ) {} ngOnInit(): void { - this.learnerDashboardIdsBackendApiService. - fetchLearnerDashboardIdsAsync().then( - (learnerDashboardActivityIds) => { - this.learnerDashboardActivityIds = learnerDashboardActivityIds; - } - ); + this.learnerDashboardIdsBackendApiService + .fetchLearnerDashboardIdsAsync() + .then(learnerDashboardActivityIds => { + this.learnerDashboardActivityIds = learnerDashboardActivityIds; + }); } - enablePlaylistTooltip(): void { this.playlistTooltipIsEnabled = true; } @@ -71,11 +64,9 @@ export class LearnerDashboardIconsComponent implements OnInit { this.playlistTooltipIsEnabled = false; } - canActivityBeAddedToLearnerPlaylist(activityId: string): boolean { if (this.learnerDashboardActivityIds) { - if (this.learnerDashboardActivityIds.includesActivity( - activityId)) { + if (this.learnerDashboardActivityIds.includesActivity(activityId)) { return false; } } @@ -86,13 +77,13 @@ export class LearnerDashboardIconsComponent implements OnInit { var activityType = this.activityType; if (this.learnerDashboardActivityIds) { if (activityType === AppConstants.ACTIVITY_TYPE_EXPLORATION) { - return ( - this.learnerDashboardActivityIds.belongsToExplorationPlaylist( - this.activityId)); + return this.learnerDashboardActivityIds.belongsToExplorationPlaylist( + this.activityId + ); } else if (activityType === AppConstants.ACTIVITY_TYPE_COLLECTION) { - return ( - this.learnerDashboardActivityIds.belongsToCollectionPlaylist( - this.activityId)); + return this.learnerDashboardActivityIds.belongsToCollectionPlaylist( + this.activityId + ); } } return false; @@ -102,21 +93,21 @@ export class LearnerDashboardIconsComponent implements OnInit { var activityType = this.activityType; if (this.learnerDashboardActivityIds) { if (activityType === AppConstants.ACTIVITY_TYPE_EXPLORATION) { - return ( - this.learnerDashboardActivityIds.belongsToCompletedExplorations( - this.activityId)); + return this.learnerDashboardActivityIds.belongsToCompletedExplorations( + this.activityId + ); } else if (activityType === AppConstants.ACTIVITY_TYPE_COLLECTION) { - return ( - this.learnerDashboardActivityIds.belongsToCompletedCollections( - this.activityId)); + return this.learnerDashboardActivityIds.belongsToCompletedCollections( + this.activityId + ); } else if (activityType === AppConstants.ACTIVITY_TYPE_STORY) { - return ( - this.learnerDashboardActivityIds.belongsToCompletedStories( - this.activityId)); + return this.learnerDashboardActivityIds.belongsToCompletedStories( + this.activityId + ); } else if (activityType === AppConstants.ACTIVITY_TYPE_LEARN_TOPIC) { - return ( - this.learnerDashboardActivityIds.belongsToLearntTopics( - this.activityId)); + return this.learnerDashboardActivityIds.belongsToLearntTopics( + this.activityId + ); } } return false; @@ -126,34 +117,40 @@ export class LearnerDashboardIconsComponent implements OnInit { var activityType = this.activityType; if (this.learnerDashboardActivityIds) { if (activityType === AppConstants.ACTIVITY_TYPE_EXPLORATION) { - return ( - this.learnerDashboardActivityIds.belongsToIncompleteExplorations( - this.activityId)); + return this.learnerDashboardActivityIds.belongsToIncompleteExplorations( + this.activityId + ); } else if (activityType === AppConstants.ACTIVITY_TYPE_COLLECTION) { - return ( - this.learnerDashboardActivityIds.belongsToIncompleteCollections( - this.activityId)); + return this.learnerDashboardActivityIds.belongsToIncompleteCollections( + this.activityId + ); } else if (activityType === AppConstants.ACTIVITY_TYPE_LEARN_TOPIC) { - return ( - this.learnerDashboardActivityIds.belongsToPartiallyLearntTopics( - this.activityId)); + return this.learnerDashboardActivityIds.belongsToPartiallyLearntTopics( + this.activityId + ); } } return false; } async addToLearnerPlaylist( - activityId: string, activityType: string): Promise { - var isSuccessfullyAdded = ( + activityId: string, + activityType: string + ): Promise { + var isSuccessfullyAdded = await this.learnerDashboardActivityBackendApiService.addToLearnerPlaylist( - activityId, activityType)); + activityId, + activityType + ); if (isSuccessfullyAdded) { if (activityType === AppConstants.ACTIVITY_TYPE_EXPLORATION) { this.learnerDashboardActivityIds.addToExplorationLearnerPlaylist( - activityId); + activityId + ); } else if (activityType === AppConstants.ACTIVITY_TYPE_COLLECTION) { this.learnerDashboardActivityIds.addToCollectionLearnerPlaylist( - activityId); + activityId + ); } this.disablePlaylistTooltip(); } @@ -162,7 +159,10 @@ export class LearnerDashboardIconsComponent implements OnInit { // This function will open a modal to remove an exploration // from the 'Play Later' list in the Library Page. removeFromLearnerPlaylist( - activityId: string, activityTitle: string, activityType: string): void { + activityId: string, + activityTitle: string, + activityType: string + ): void { // This following logic of showing a modal for confirmation previously // resided in learnerDashboardActivityBackendApiService. However, in // issue #14225, we noticed some errors with dynamic component creation. @@ -175,20 +175,26 @@ export class LearnerDashboardIconsComponent implements OnInit { // TODO(#14290): Find a better way to refactor code that opens modals // into new services that use the page specific injector rather than // the root injector. - const modelRef = this.ngbModal.open( - LearnerPlaylistModalComponent, {backdrop: true}); + const modelRef = this.ngbModal.open(LearnerPlaylistModalComponent, { + backdrop: true, + }); modelRef.componentInstance.activityId = activityId; modelRef.componentInstance.activityTitle = activityTitle; modelRef.componentInstance.activityType = activityType; - modelRef.result.then((playlistUrl) => { - this.learnerDashboardActivityBackendApiService - .removeFromLearnerPlaylist( - activityId, activityType, - this.learnerDashboardActivityIds, playlistUrl); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + modelRef.result.then( + playlistUrl => { + this.learnerDashboardActivityBackendApiService.removeFromLearnerPlaylist( + activityId, + activityType, + this.learnerDashboardActivityIds, + playlistUrl + ); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } } diff --git a/core/templates/pages/learner-dashboard-page/learner-dashboard-page-root.component.spec.ts b/core/templates/pages/learner-dashboard-page/learner-dashboard-page-root.component.spec.ts index 29e96f4363be..f8e1a65c8fc9 100644 --- a/core/templates/pages/learner-dashboard-page/learner-dashboard-page-root.component.spec.ts +++ b/core/templates/pages/learner-dashboard-page/learner-dashboard-page-root.component.spec.ts @@ -16,14 +16,14 @@ * @fileoverview Unit tests for learner dashboard page root component. */ -import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { TranslateModule } from '@ngx-translate/core'; +import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {TranslateModule} from '@ngx-translate/core'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; -import { LearnerDashboardPageRootComponent } from './learner-dashboard-page-root.component'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; +import {LearnerDashboardPageRootComponent} from './learner-dashboard-page-root.component'; describe('LearnerDashboardPageRootComponent', () => { let fixture: ComponentFixture; @@ -43,8 +43,10 @@ describe('LearnerDashboardPageRootComponent', () => { it('should have the title and meta tags set', () => { expect(component.title).toEqual( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LEARNER_DASHBOARD.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LEARNER_DASHBOARD.TITLE + ); expect(component.meta).toEqual( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LEARNER_DASHBOARD.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LEARNER_DASHBOARD.META + ); }); }); diff --git a/core/templates/pages/learner-dashboard-page/learner-dashboard-page-root.component.ts b/core/templates/pages/learner-dashboard-page/learner-dashboard-page-root.component.ts index 327779d97df3..778f4e83e2b2 100644 --- a/core/templates/pages/learner-dashboard-page/learner-dashboard-page-root.component.ts +++ b/core/templates/pages/learner-dashboard-page/learner-dashboard-page-root.component.ts @@ -16,9 +16,9 @@ * @fileoverview Learner dashboard page root component. */ -import { Component } from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { BaseRootComponent, MetaTagData } from 'pages/base-root.component'; +import {Component} from '@angular/core'; +import {AppConstants} from 'app.constants'; +import {BaseRootComponent, MetaTagData} from 'pages/base-root.component'; @Component({ selector: 'oppia-learner-dashboard-page-root', @@ -28,7 +28,6 @@ export class LearnerDashboardPageRootComponent extends BaseRootComponent { title: string = AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LEARNER_DASHBOARD.TITLE; - meta: MetaTagData[] = - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LEARNER_DASHBOARD.META as - unknown as Readonly[]; + meta: MetaTagData[] = AppConstants.PAGES_REGISTERED_WITH_FRONTEND + .LEARNER_DASHBOARD.META as unknown as Readonly[]; } diff --git a/core/templates/pages/learner-dashboard-page/learner-dashboard-page.component.spec.ts b/core/templates/pages/learner-dashboard-page/learner-dashboard-page.component.spec.ts index d3c2cd387c51..aac9a862afc0 100644 --- a/core/templates/pages/learner-dashboard-page/learner-dashboard-page.component.spec.ts +++ b/core/templates/pages/learner-dashboard-page/learner-dashboard-page.component.spec.ts @@ -16,44 +16,55 @@ * @fileoverview Unit tests for learner dashboard parge. */ - -import { Collection, CollectionBackendDict } from 'domain/collection/collection.model'; -import { LearnerExplorationSummary } from 'domain/summary/learner-exploration-summary.model'; - - -import { CollectionSummary } from 'domain/collection/collection-summary.model'; -import { ProfileSummary } from 'domain/user/profile-summary.model'; -import { LearnerDashboardPageComponent } from './learner-dashboard-page.component'; -import { async, ComponentFixture, fakeAsync, flush, TestBed, tick } from '@angular/core/testing'; -import { MaterialModule } from 'modules/material.module'; -import { FormsModule } from '@angular/forms'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { Component, EventEmitter, NO_ERRORS_SCHEMA, Pipe } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; - -import { AlertsService } from 'services/alerts.service'; -import { CsrfTokenService } from 'services/csrf-token.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { DateTimeFormatService } from 'services/date-time-format.service'; -import { ExplorationBackendDict, ExplorationObjectFactory } from 'domain/exploration/ExplorationObjectFactory'; -import { LearnerDashboardBackendApiService } from 'domain/learner_dashboard/learner-dashboard-backend-api.service'; -import { LearnerDashboardActivityBackendApiService } from 'domain/learner_dashboard/learner-dashboard-activity-backend-api.service'; -import { SuggestionModalForLearnerDashboardService } from './suggestion-modal/suggestion-modal-for-learner-dashboard.service'; -import { SortByPipe } from 'filters/string-utility-filters/sort-by.pipe'; -import { UserService } from 'services/user.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { StorySummary } from 'domain/story/story-summary.model'; -import { LearnerTopicSummary } from 'domain/topic/learner-topic-summary.model'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { NonExistentTopicsAndStories } from 'domain/learner_dashboard/non-existent-topics-and-stories.model'; -import { NonExistentCollections } from 'domain/learner_dashboard/non-existent-collections.model'; -import { NonExistentExplorations } from 'domain/learner_dashboard/non-existent-explorations.model'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { PageTitleService } from 'services/page-title.service'; -import { LearnerGroupBackendApiService } from 'domain/learner_group/learner-group-backend-api.service'; -import { UrlService } from 'services/contextual/url.service'; -import { UserInfo } from 'domain/user/user-info.model'; +import { + Collection, + CollectionBackendDict, +} from 'domain/collection/collection.model'; +import {LearnerExplorationSummary} from 'domain/summary/learner-exploration-summary.model'; + +import {CollectionSummary} from 'domain/collection/collection-summary.model'; +import {ProfileSummary} from 'domain/user/profile-summary.model'; +import {LearnerDashboardPageComponent} from './learner-dashboard-page.component'; +import { + async, + ComponentFixture, + fakeAsync, + flush, + TestBed, + tick, +} from '@angular/core/testing'; +import {MaterialModule} from 'modules/material.module'; +import {FormsModule} from '@angular/forms'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {Component, EventEmitter, NO_ERRORS_SCHEMA, Pipe} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; + +import {AlertsService} from 'services/alerts.service'; +import {CsrfTokenService} from 'services/csrf-token.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {DateTimeFormatService} from 'services/date-time-format.service'; +import { + ExplorationBackendDict, + ExplorationObjectFactory, +} from 'domain/exploration/ExplorationObjectFactory'; +import {LearnerDashboardBackendApiService} from 'domain/learner_dashboard/learner-dashboard-backend-api.service'; +import {LearnerDashboardActivityBackendApiService} from 'domain/learner_dashboard/learner-dashboard-activity-backend-api.service'; +import {SuggestionModalForLearnerDashboardService} from './suggestion-modal/suggestion-modal-for-learner-dashboard.service'; +import {SortByPipe} from 'filters/string-utility-filters/sort-by.pipe'; +import {UserService} from 'services/user.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {StorySummary} from 'domain/story/story-summary.model'; +import {LearnerTopicSummary} from 'domain/topic/learner-topic-summary.model'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {NonExistentTopicsAndStories} from 'domain/learner_dashboard/non-existent-topics-and-stories.model'; +import {NonExistentCollections} from 'domain/learner_dashboard/non-existent-collections.model'; +import {NonExistentExplorations} from 'domain/learner_dashboard/non-existent-explorations.model'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {PageTitleService} from 'services/page-title.service'; +import {LearnerGroupBackendApiService} from 'domain/learner_group/learner-group-backend-api.service'; +import {UrlService} from 'services/contextual/url.service'; +import {UserInfo} from 'domain/user/user-info.model'; @Pipe({name: 'slice'}) class MockSlicePipe { @@ -85,20 +96,16 @@ class MockTranslateService { } @Component({selector: 'background-banner', template: ''}) -class BackgroundBannerComponentStub { -} +class BackgroundBannerComponentStub {} @Component({selector: 'exploration-summary-tile', template: ''}) -class ExplorationSummaryTileComponentStub { -} +class ExplorationSummaryTileComponentStub {} @Component({selector: 'collection-summary-tile', template: ''}) -class CollectionSummaryTileComponentStub { -} +class CollectionSummaryTileComponentStub {} @Component({selector: 'loading-dots', template: ''}) -class LoadingDotsComponentStub { -} +class LoadingDotsComponentStub {} describe('Learner dashboard page', () => { let component: LearnerDashboardPageComponent; @@ -108,10 +115,10 @@ describe('Learner dashboard page', () => { let dateTimeFormatService: DateTimeFormatService = null; let explorationObjectFactory: ExplorationObjectFactory = null; let focusManagerService: FocusManagerService; - let learnerDashboardBackendApiService: - LearnerDashboardBackendApiService = null; - let suggestionModalForLearnerDashboardService: - SuggestionModalForLearnerDashboardService = null; + let learnerDashboardBackendApiService: LearnerDashboardBackendApiService = + null; + let suggestionModalForLearnerDashboardService: SuggestionModalForLearnerDashboardService = + null; let windowDimensionsService: WindowDimensionsService; let mockResizeEmitter: EventEmitter; let userService: UserService = null; @@ -146,38 +153,54 @@ describe('Learner dashboard page', () => { param_specs: {}, param_changes: [], auto_tts_enabled: false, - edits_allowed: true - } + edits_allowed: true, + }, }; let titleList = [ - 'World War III', 'Quantum Mechanics', 'Algebra', - 'Nouns', 'Counting Stars', 'Hip Hop', 'Consiousness', - 'Database Management', 'Plant Cell', 'Zebra' + 'World War III', + 'Quantum Mechanics', + 'Algebra', + 'Nouns', + 'Counting Stars', + 'Hip Hop', + 'Consiousness', + 'Database Management', + 'Plant Cell', + 'Zebra', ]; let categoryList = [ - 'Social', 'Science', 'Mathematics', 'English', - 'French', 'Arts', 'Pyschology', - 'Computer Science', 'Biology', 'Zoo' + 'Social', + 'Science', + 'Mathematics', + 'English', + 'French', + 'Arts', + 'Pyschology', + 'Computer Science', + 'Biology', + 'Zoo', ]; - let subscriptionsList = [{ - creator_impact: 0, - creator_username: 'Bucky', - }, - { - creator_impact: 1, - creator_username: 'Arrow', - }, - { - creator_impact: 3, - creator_username: 'Deadpool', - }, - { - creator_impact: 2, - creator_username: 'Captain America', - }]; + let subscriptionsList = [ + { + creator_impact: 0, + creator_username: 'Bucky', + }, + { + creator_impact: 1, + creator_username: 'Arrow', + }, + { + creator_impact: 3, + creator_username: 'Deadpool', + }, + { + creator_impact: 2, + creator_username: 'Captain America', + }, + ]; let collectionDict: CollectionBackendDict = { id: 'sample_collection_id', @@ -191,11 +214,10 @@ describe('Learner dashboard page', () => { tags: null, playthrough_dict: { next_exploration_id: 'expId', - completed_exploration_ids: ['expId2'] - } + completed_exploration_ids: ['expId2'], + }, }; - let learnerDashboardTopicAndStoriesData = { completed_stories_list: [], learnt_topic_list: [], @@ -213,7 +235,6 @@ describe('Learner dashboard page', () => { }, }; - let learnerDashboardCollectionsData = { completed_collections_list: [], incomplete_collections_list: [], @@ -221,12 +242,11 @@ describe('Learner dashboard page', () => { number_of_nonexistent_collections: { incomplete_collections: 0, completed_collections: 0, - collection_playlist: 0 + collection_playlist: 0, }, - collection_playlist: [] + collection_playlist: [], }; - let learnerDashboardExplorationsData = { completed_explorations_list: [], incomplete_explorations_list: [], @@ -261,10 +281,10 @@ describe('Learner dashboard page', () => { isQuestionCoordinator: () => false, isTranslationCoordinator: () => false, canCreateCollections: () => true, - getPreferredSiteLanguageCode: () =>'en', + getPreferredSiteLanguageCode: () => 'en', getUsername: () => 'username1', getEmail: () => 'tester@example.org', - isLoggedIn: () => true + isLoggedIn: () => true, }; describe('when succesfully fetching learner dashboard data', () => { @@ -275,7 +295,7 @@ describe('Learner dashboard page', () => { BrowserAnimationsModule, MaterialModule, FormsModule, - HttpClientTestingModule + HttpClientTestingModule, ], declarations: [ LearnerDashboardPageComponent, @@ -296,14 +316,14 @@ describe('Learner dashboard page', () => { LearnerDashboardBackendApiService, { provide: LearnerDashboardActivityBackendApiService, - useClass: MockLearnerDashboardActivityBackendApiService + useClass: MockLearnerDashboardActivityBackendApiService, }, { provide: WindowDimensionsService, useValue: { isWindowNarrow: () => true, getResizeEvent: () => mockResizeEmitter, - } + }, }, SuggestionModalForLearnerDashboardService, UrlInterpolationService, @@ -311,10 +331,10 @@ describe('Learner dashboard page', () => { PageTitleService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -328,71 +348,74 @@ describe('Learner dashboard page', () => { explorationObjectFactory = TestBed.inject(ExplorationObjectFactory); focusManagerService = TestBed.inject(FocusManagerService); windowDimensionsService = TestBed.inject(WindowDimensionsService); - learnerDashboardBackendApiService = - TestBed.inject(LearnerDashboardBackendApiService); - suggestionModalForLearnerDashboardService = - TestBed.inject(SuggestionModalForLearnerDashboardService); + learnerDashboardBackendApiService = TestBed.inject( + LearnerDashboardBackendApiService + ); + suggestionModalForLearnerDashboardService = TestBed.inject( + SuggestionModalForLearnerDashboardService + ); userService = TestBed.inject(UserService); translateService = TestBed.inject(TranslateService); pageTitleService = TestBed.inject(PageTitleService); urlService = TestBed.inject(UrlService); learnerGroupBackendApiService = TestBed.inject( - LearnerGroupBackendApiService); + LearnerGroupBackendApiService + ); const mockElement = document.createElement('div'); mockElement.className = 'oppia-exploration-title'; document.body.appendChild(mockElement); - spyOn(csrfTokenService, 'getTokenAsync').and.callFake(async() => { + spyOn(csrfTokenService, 'getTokenAsync').and.callFake(async () => { return Promise.resolve('sample-csrf-token'); }); // Generate completed explorations and exploration playlist. for (let i = 0; i < 10; i++) { - learnerDashboardExplorationsData.completed_explorations_list[i] = ( + learnerDashboardExplorationsData.completed_explorations_list[i] = explorationObjectFactory.createFromBackendDict( Object.assign(explorationDict, { id: i + 1, title: titleList[i], - category: categoryList[i] + category: categoryList[i], }) - )); - learnerDashboardExplorationsData.exploration_playlist[i] = ({ - id: Number(i + 1).toString() - }); + ); + learnerDashboardExplorationsData.exploration_playlist[i] = { + id: Number(i + 1).toString(), + }; } // Generate incomplete explorations and incomplete exploration playlist. for (let i = 0; i < 12; i++) { - learnerDashboardExplorationsData.incomplete_explorations_list[i] = ( + learnerDashboardExplorationsData.incomplete_explorations_list[i] = explorationObjectFactory.createFromBackendDict( Object.assign(explorationDict, { // Create ids from 11 to 22. // (1 to 10 is the complete explorations). id: Number(i + 11).toString(), title: titleList[i], - category: categoryList[i] + category: categoryList[i], }) - )); + ); } // Generate completed collections and collection playlist. for (let i = 0; i < 8; i++) { - learnerDashboardCollectionsData.completed_collections_list[i] = ( + learnerDashboardCollectionsData.completed_collections_list[i] = // TODO(#10875): Fix type mismatch. Collection.create( Object.assign(collectionDict, { title: titleList[i], - category: categoryList[i] + category: categoryList[i], }) as CollectionBackendDict - )); - learnerDashboardCollectionsData.collection_playlist[i] = ({ - id: Number(i + 1).toString() - }); + ); + learnerDashboardCollectionsData.collection_playlist[i] = { + id: Number(i + 1).toString(), + }; } // Generate incomplete collections. for (let i = 0; i < 8; i++) { - learnerDashboardCollectionsData.incomplete_collections_list[i] = ( + learnerDashboardCollectionsData.incomplete_collections_list[i] = // TODO(#10875): Fix type mismatch. Collection.create( Object.assign(collectionDict, { @@ -401,52 +424,59 @@ describe('Learner dashboard page', () => { id: Number(i + 9).toString(), title: 'Collection Title ' + (i + 7), }) as CollectionBackendDict - )); + ); } - spyOn(userService, 'getProfileImageDataUrl').and.returnValue( - ['profile-image-url-png', 'profile-image-url-webp']); + spyOn(userService, 'getProfileImageDataUrl').and.returnValue([ + 'profile-image-url-png', + 'profile-image-url-webp', + ]); spyOn( learnerDashboardBackendApiService, - 'fetchLearnerDashboardTopicsAndStoriesDataAsync') - .and.returnValue(Promise.resolve({ - completedStoriesList: ( + 'fetchLearnerDashboardTopicsAndStoriesDataAsync' + ).and.returnValue( + Promise.resolve({ + completedStoriesList: learnerDashboardTopicAndStoriesData.completed_stories_list.map( - storySummary => StorySummary.createFromBackendDict( - storySummary))), - learntTopicsList: ( + storySummary => StorySummary.createFromBackendDict(storySummary) + ), + learntTopicsList: learnerDashboardTopicAndStoriesData.learnt_topic_list.map( - topicSummary => LearnerTopicSummary.createFromBackendDict( - topicSummary))), - partiallyLearntTopicsList: ( - learnerDashboardTopicAndStoriesData - .partially_learnt_topics_list.map( - topicSummary => LearnerTopicSummary.createFromBackendDict( - topicSummary))), - topicsToLearnList: ( + topicSummary => + LearnerTopicSummary.createFromBackendDict(topicSummary) + ), + partiallyLearntTopicsList: + learnerDashboardTopicAndStoriesData.partially_learnt_topics_list.map( + topicSummary => + LearnerTopicSummary.createFromBackendDict(topicSummary) + ), + topicsToLearnList: learnerDashboardTopicAndStoriesData.topics_to_learn_list.map( - topicSummary => LearnerTopicSummary - .createFromBackendDict(topicSummary))), - allTopicsList: ( + topicSummary => + LearnerTopicSummary.createFromBackendDict(topicSummary) + ), + allTopicsList: learnerDashboardTopicAndStoriesData.all_topics_list.map( - topicSummary => LearnerTopicSummary - .createFromBackendDict(topicSummary))), + topicSummary => + LearnerTopicSummary.createFromBackendDict(topicSummary) + ), untrackedTopics: learnerDashboardTopicAndStoriesData.untracked_topics, - completedToIncompleteStories: ( - learnerDashboardTopicAndStoriesData - .completed_to_incomplete_stories), - learntToPartiallyLearntTopics: ( - learnerDashboardTopicAndStoriesData - .learnt_to_partially_learnt_topics), - numberOfNonexistentTopicsAndStories: ( + completedToIncompleteStories: + learnerDashboardTopicAndStoriesData.completed_to_incomplete_stories, + learntToPartiallyLearntTopics: + learnerDashboardTopicAndStoriesData.learnt_to_partially_learnt_topics, + numberOfNonexistentTopicsAndStories: NonExistentTopicsAndStories.createFromBackendDict( - learnerDashboardTopicAndStoriesData. - number_of_nonexistent_topics_and_stories)), - })); + learnerDashboardTopicAndStoriesData.number_of_nonexistent_topics_and_stories + ), + }) + ); - spyOn(learnerGroupBackendApiService, 'isLearnerGroupFeatureEnabledAsync') - .and.returnValue(Promise.resolve(true)); + spyOn( + learnerGroupBackendApiService, + 'isLearnerGroupFeatureEnabledAsync' + ).and.returnValue(Promise.resolve(true)); spyOn(urlService, 'getUrlParams').and.returnValue({ active_tab: 'learner-groups', @@ -458,41 +488,47 @@ describe('Learner dashboard page', () => { flush(); })); - it('should initialize correctly component properties after its' + - ' initialization and get data from backend', fakeAsync(() => { - spyOn(userService, 'getUserInfoAsync').and - .callFake(async() => { + it( + 'should initialize correctly component properties after its' + + ' initialization and get data from backend', + fakeAsync(() => { + spyOn(userService, 'getUserInfoAsync').and.callFake(async () => { return Promise.resolve(userInfo); }); + component.ngOnInit(); + flush(); + + expect(component.profilePicturePngDataUrl).toEqual( + 'profile-image-url-png' + ); + expect(component.profilePictureWebpDataUrl).toEqual( + 'profile-image-url-webp' + ); + expect(component.username).toBe(userInfo.getUsername()); + expect(component.windowIsNarrow).toBeTrue(); + }) + ); + + it('should get default profile pictures when username is null', fakeAsync(() => { + let userInfo = { + getUsername: () => null, + isSuperAdmin: () => true, + getEmail: () => 'test_email@example.com', + }; + spyOn(userService, 'getUserInfoAsync').and.resolveTo( + userInfo as UserInfo + ); component.ngOnInit(); flush(); expect(component.profilePicturePngDataUrl).toEqual( - 'profile-image-url-png'); + '/assets/images/avatar/user_blue_150px.png' + ); expect(component.profilePictureWebpDataUrl).toEqual( - 'profile-image-url-webp'); - expect(component.username).toBe(userInfo.getUsername()); - expect(component.windowIsNarrow).toBeTrue(); + '/assets/images/avatar/user_blue_150px.webp' + ); })); - it('should get default profile pictures when username is null', - fakeAsync(() => { - let userInfo = { - getUsername: () => null, - isSuperAdmin: () => true, - getEmail: () => 'test_email@example.com' - }; - spyOn(userService, 'getUserInfoAsync') - .and.resolveTo(userInfo as UserInfo); - component.ngOnInit(); - flush(); - - expect(component.profilePicturePngDataUrl).toEqual( - '/assets/images/avatar/user_blue_150px.png'); - expect(component.profilePictureWebpDataUrl).toEqual( - '/assets/images/avatar/user_blue_150px.webp'); - })); - it('should check whether window is narrow on resizing the screen', () => { spyOn(windowDimensionsService, 'isWindowNarrow').and.returnValue(false); @@ -505,10 +541,9 @@ describe('Learner dashboard page', () => { it('should set focus without scroll on browse lesson btn', fakeAsync(() => { const focusSpy = spyOn(focusManagerService, 'setFocusWithoutScroll'); - spyOn(userService, 'getUserInfoAsync').and - .callFake(async() => { - return Promise.resolve(userInfo); - }); + spyOn(userService, 'getUserInfoAsync').and.callFake(async () => { + return Promise.resolve(userInfo); + }); component.ngOnInit(); flush(); @@ -516,22 +551,25 @@ describe('Learner dashboard page', () => { expect(focusSpy).toHaveBeenCalledWith('ourLessonsBtn'); })); - it('should subscribe to onLangChange upon initialisation and set page ' + - 'title whenever language changes', fakeAsync(() => { - spyOn(component.directiveSubscriptions, 'add'); - spyOn(translateService.onLangChange, 'subscribe'); - spyOn(component, 'setPageTitle'); + it( + 'should subscribe to onLangChange upon initialisation and set page ' + + 'title whenever language changes', + fakeAsync(() => { + spyOn(component.directiveSubscriptions, 'add'); + spyOn(translateService.onLangChange, 'subscribe'); + spyOn(component, 'setPageTitle'); - component.ngOnInit(); - flush(); + component.ngOnInit(); + flush(); - expect(component.directiveSubscriptions.add).toHaveBeenCalled(); - expect(translateService.onLangChange.subscribe).toHaveBeenCalled(); + expect(component.directiveSubscriptions.add).toHaveBeenCalled(); + expect(translateService.onLangChange.subscribe).toHaveBeenCalled(); - translateService.onLangChange.emit(); + translateService.onLangChange.emit(); - expect(component.setPageTitle).toHaveBeenCalled(); - })); + expect(component.setPageTitle).toHaveBeenCalled(); + }) + ); it('should obtain translated page title and set it', () => { spyOn(translateService, 'instant').and.callThrough(); @@ -540,63 +578,70 @@ describe('Learner dashboard page', () => { component.setPageTitle(); expect(translateService.instant).toHaveBeenCalledWith( - 'I18N_LEARNER_DASHBOARD_PAGE_TITLE'); + 'I18N_LEARNER_DASHBOARD_PAGE_TITLE' + ); expect(pageTitleService.setDocumentTitle).toHaveBeenCalledWith( - 'I18N_LEARNER_DASHBOARD_PAGE_TITLE'); + 'I18N_LEARNER_DASHBOARD_PAGE_TITLE' + ); }); it('should get static image url', () => { let imagePath = '/path/to/image.png'; expect(component.getStaticImageUrl(imagePath)).toBe( - '/assets/images/path/to/image.png'); + '/assets/images/path/to/image.png' + ); }); it('should get user profile image png data url correctly', () => { expect(component.getauthorPicturePngDataUrl('username')).toBe( - 'profile-image-url-png'); + 'profile-image-url-png' + ); }); it('should get user profile image webp data url correctly', () => { expect(component.getauthorPictureWebpDataUrl('username')).toBe( - 'profile-image-url-webp'); + 'profile-image-url-webp' + ); }); - it('should toggle active subsection type when changing subsection type', - () => { - // Active subsection is set as I18N_DASHBOARD_SKILL_PROFICIENCY when - // component is initialized. - expect(component.activeSubsection).toBe( - 'I18N_DASHBOARD_SKILL_PROFICIENCY'); + it('should toggle active subsection type when changing subsection type', () => { + // Active subsection is set as I18N_DASHBOARD_SKILL_PROFICIENCY when + // component is initialized. + expect(component.activeSubsection).toBe( + 'I18N_DASHBOARD_SKILL_PROFICIENCY' + ); - let newActiveSubsection2 = 'I18N_DASHBOARD_SKILL_PROFICIENCY'; - component.setActiveSubsection(newActiveSubsection2); + let newActiveSubsection2 = 'I18N_DASHBOARD_SKILL_PROFICIENCY'; + component.setActiveSubsection(newActiveSubsection2); - expect(component.activeSubsection).toBe(newActiveSubsection2); - }); + expect(component.activeSubsection).toBe(newActiveSubsection2); + }); it('should show username popover based on its length', () => { expect(component.showUsernamePopover('abcdefghijk')).toBe('mouseenter'); expect(component.showUsernamePopover('abc')).toBe('none'); }); - it('should show new and old content when opening suggestion modal', - () => { - spyOn(suggestionModalForLearnerDashboardService, 'showSuggestionModal') - .and.returnValue(null); - - let newContent = 'New content'; - let oldContent = 'Old content'; - let description = 'Description'; - component.showSuggestionModal(newContent, oldContent, description); - - expect(suggestionModalForLearnerDashboardService.showSuggestionModal) - .toHaveBeenCalledWith('edit_exploration_state_content', { - newContent: newContent, - oldContent: oldContent, - description: description - }); + it('should show new and old content when opening suggestion modal', () => { + spyOn( + suggestionModalForLearnerDashboardService, + 'showSuggestionModal' + ).and.returnValue(null); + + let newContent = 'New content'; + let oldContent = 'Old content'; + let description = 'Description'; + component.showSuggestionModal(newContent, oldContent, description); + + expect( + suggestionModalForLearnerDashboardService.showSuggestionModal + ).toHaveBeenCalledWith('edit_exploration_state_content', { + newContent: newContent, + oldContent: oldContent, + description: description, }); + }); it('should get css classes based on status', () => { expect(component.getLabelClass('open')).toBe('badge badge-info'); @@ -608,19 +653,21 @@ describe('Learner dashboard page', () => { expect(component.getHumanReadableStatus('open')).toBe('Open'); expect(component.getHumanReadableStatus('compliment')).toBe('Compliment'); expect(component.getHumanReadableStatus('not_actionable')).toBe( - 'Not Actionable'); + 'Not Actionable' + ); }); - it('should get formatted date string from the timestamp in milliseconds', - () => { - // This corresponds to Fri, 2 Apr 2021 09:45:00 GMT. - let NOW_MILLIS = 1617393321345; - spyOn(dateTimeFormatService, 'getLocaleAbbreviatedDatetimeString') - .withArgs(NOW_MILLIS).and.returnValue('4/2/2021'); + it('should get formatted date string from the timestamp in milliseconds', () => { + // This corresponds to Fri, 2 Apr 2021 09:45:00 GMT. + let NOW_MILLIS = 1617393321345; + spyOn(dateTimeFormatService, 'getLocaleAbbreviatedDatetimeString') + .withArgs(NOW_MILLIS) + .and.returnValue('4/2/2021'); - expect(component.getLocaleAbbreviatedDatetimeString(NOW_MILLIS)) - .toBe('4/2/2021'); - }); + expect(component.getLocaleAbbreviatedDatetimeString(NOW_MILLIS)).toBe( + '4/2/2021' + ); + }); it('should sanitize given png base64 data and generate url', () => { let result = component.decodePngURIData('%D1%88%D0%B5%D0%BB%D0%BB%D1%8B'); @@ -638,7 +685,7 @@ describe('Learner dashboard page', () => { BrowserAnimationsModule, MaterialModule, FormsModule, - HttpClientTestingModule + HttpClientTestingModule, ], declarations: [ LearnerDashboardPageComponent, @@ -659,10 +706,10 @@ describe('Learner dashboard page', () => { PageTitleService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -671,254 +718,299 @@ describe('Learner dashboard page', () => { component = fixture.componentInstance; alertsService = TestBed.inject(AlertsService); csrfTokenService = TestBed.inject(CsrfTokenService); - learnerDashboardBackendApiService = - TestBed.inject(LearnerDashboardBackendApiService); + learnerDashboardBackendApiService = TestBed.inject( + LearnerDashboardBackendApiService + ); userService = TestBed.inject(UserService); translateService = TestBed.inject(TranslateService); pageTitleService = TestBed.inject(PageTitleService); spyOn(csrfTokenService, 'getTokenAsync').and.returnValue( - Promise.resolve('sample-csrf-token')); + Promise.resolve('sample-csrf-token') + ); - spyOn(userService, 'getProfileImageDataUrl').and.returnValue( - ['default-image-url-png', 'default-image-url-webp']); + spyOn(userService, 'getProfileImageDataUrl').and.returnValue([ + 'default-image-url-png', + 'default-image-url-webp', + ]); spyOn(userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(userInfo)); + Promise.resolve(userInfo) + ); })); - it('should show an alert warning when fails to get topics and' + - ' stories data', fakeAsync(() => { - const fetchDataSpy = spyOn( - learnerDashboardBackendApiService, - 'fetchLearnerDashboardTopicsAndStoriesDataAsync') - .and.rejectWith(404); - const alertsSpy = spyOn(alertsService, 'addWarning').and.callThrough(); - - component.ngOnInit(); - - tick(); - fixture.detectChanges(); - - expect(alertsSpy).toHaveBeenCalledWith( - 'Failed to get learner dashboard topics and stories data'); - expect(fetchDataSpy).toHaveBeenCalled(); - })); - - it('should show an alert warning when fails to get collections data' + - 'in mobile view', - fakeAsync(() => { - const fetchDataSpy = spyOn( - learnerDashboardBackendApiService, - 'fetchLearnerDashboardCollectionsDataAsync') - .and.rejectWith(404); - const alertsSpy = spyOn(alertsService, 'addWarning').and.callThrough(); - - let newActiveSectionName = 'I18N_DASHBOARD_LESSONS'; - component.setActiveSubsection(newActiveSectionName); - - tick(); - fixture.detectChanges(); - - expect(alertsSpy).toHaveBeenCalledWith( - 'Failed to get learner dashboard collections data'); - expect(fetchDataSpy).toHaveBeenCalled(); - })); - - it('should show an alert warning when fails to get explorations data in' + - 'mobile view', - fakeAsync(() => { - const fetchDataSpy = spyOn( - learnerDashboardBackendApiService, - 'fetchLearnerDashboardExplorationsDataAsync') - .and.rejectWith(404); - const alertsSpy = spyOn(alertsService, 'addWarning').and.callThrough(); - - let newActiveSectionName = 'I18N_DASHBOARD_LESSONS'; - component.setActiveSubsection(newActiveSectionName); - - tick(); - fixture.detectChanges(); - - expect(alertsSpy).toHaveBeenCalledWith( - 'Failed to get learner dashboard explorations data'); - expect(fetchDataSpy).toHaveBeenCalled(); - })); - - it('should get explorations and collections data when user clicks ' + - 'communtiy lessons tab in mobile view', - fakeAsync(() => { - const fetchCollectionsDataSpy = spyOn( - learnerDashboardBackendApiService, - 'fetchLearnerDashboardCollectionsDataAsync') - .and.returnValue(Promise.resolve({ - completedCollectionsList: ( - learnerDashboardCollectionsData.completed_collections_list.map( - collectionSummary => CollectionSummary - .createFromBackendDict(collectionSummary))), - incompleteCollectionsList: ( - learnerDashboardCollectionsData.incomplete_collections_list.map( - collectionSummary => CollectionSummary - .createFromBackendDict(collectionSummary))), - collectionPlaylist: ( - learnerDashboardCollectionsData.collection_playlist.map( - collectionSummary => CollectionSummary - .createFromBackendDict(collectionSummary))), - completedToIncompleteCollections: ( - learnerDashboardCollectionsData - .completed_to_incomplete_collections), - numberOfNonexistentCollections: ( - NonExistentCollections.createFromBackendDict( - learnerDashboardCollectionsData - .number_of_nonexistent_collections)), - })); - - const fetchExplorationsDataSpy = spyOn( - learnerDashboardBackendApiService, - 'fetchLearnerDashboardExplorationsDataAsync') - .and.returnValue(Promise.resolve({ - completedExplorationsList: ( - learnerDashboardExplorationsData.completed_explorations_list.map( - expSummary => LearnerExplorationSummary.createFromBackendDict( - expSummary))), - incompleteExplorationsList: ( - learnerDashboardExplorationsData.incomplete_explorations_list.map( - expSummary => LearnerExplorationSummary.createFromBackendDict( - expSummary))), - explorationPlaylist: ( - learnerDashboardExplorationsData.exploration_playlist.map( - expSummary => LearnerExplorationSummary.createFromBackendDict( - expSummary))), - numberOfNonexistentExplorations: ( - NonExistentExplorations.createFromBackendDict( - learnerDashboardExplorationsData - .number_of_nonexistent_explorations)), - subscriptionList: ( - learnerDashboardExplorationsData.subscription_list.map( - profileSummary => ProfileSummary - .createFromCreatorBackendDict(profileSummary))) - })); - - let newActiveSectionName = 'I18N_DASHBOARD_LESSONS'; - component.setActiveSubsection(newActiveSectionName); - - tick(); - fixture.detectChanges(); - - expect(fetchCollectionsDataSpy).toHaveBeenCalled(); - flush(); - expect(fetchExplorationsDataSpy).toHaveBeenCalled(); - expect(component.communtiyLessonsDataLoaded).toEqual(true); - })); - - it('should show an alert warning when fails to get collections data ' + - 'in web view', - fakeAsync(() => { - const fetchDataSpy = spyOn( - learnerDashboardBackendApiService, - 'fetchLearnerDashboardCollectionsDataAsync') - .and.rejectWith(404); - const alertsSpy = spyOn(alertsService, 'addWarning').and.callThrough(); - - let newActiveSectionName = ( - 'I18N_LEARNER_DASHBOARD_COMMUNITY_LESSONS_SECTION'); - component.setActiveSection(newActiveSectionName); - - tick(); - fixture.detectChanges(); - - expect(alertsSpy).toHaveBeenCalledWith( - 'Failed to get learner dashboard collections data'); - expect(fetchDataSpy).toHaveBeenCalled(); - })); - - it('should show an alert warning when fails to get explorations data in ' + - 'web view', - fakeAsync(() => { - const fetchDataSpy = spyOn( - learnerDashboardBackendApiService, - 'fetchLearnerDashboardExplorationsDataAsync') - .and.rejectWith(404); - const alertsSpy = spyOn(alertsService, 'addWarning').and.callThrough(); - - let newActiveSectionName = ( - 'I18N_LEARNER_DASHBOARD_COMMUNITY_LESSONS_SECTION'); - component.setActiveSection(newActiveSectionName); + it( + 'should show an alert warning when fails to get topics and' + + ' stories data', + fakeAsync(() => { + const fetchDataSpy = spyOn( + learnerDashboardBackendApiService, + 'fetchLearnerDashboardTopicsAndStoriesDataAsync' + ).and.rejectWith(404); + const alertsSpy = spyOn(alertsService, 'addWarning').and.callThrough(); - tick(); - fixture.detectChanges(); + component.ngOnInit(); - expect(alertsSpy).toHaveBeenCalledWith( - 'Failed to get learner dashboard explorations data'); - expect(fetchDataSpy).toHaveBeenCalled(); - })); + tick(); + fixture.detectChanges(); - it('should get explorations and collections data when user clicks ' + - 'communtiy lessons tab in web view', - fakeAsync(() => { - const fetchCollectionsDataSpy = spyOn( - learnerDashboardBackendApiService, - 'fetchLearnerDashboardCollectionsDataAsync') - .and.returnValue(Promise.resolve({ - completedCollectionsList: ( - learnerDashboardCollectionsData.completed_collections_list.map( - collectionSummary => CollectionSummary - .createFromBackendDict(collectionSummary))), - incompleteCollectionsList: ( - learnerDashboardCollectionsData.incomplete_collections_list.map( - collectionSummary => CollectionSummary - .createFromBackendDict(collectionSummary))), - collectionPlaylist: ( - learnerDashboardCollectionsData.collection_playlist.map( - collectionSummary => CollectionSummary - .createFromBackendDict(collectionSummary))), - completedToIncompleteCollections: ( - learnerDashboardCollectionsData - .completed_to_incomplete_collections), - numberOfNonexistentCollections: ( - NonExistentCollections.createFromBackendDict( - learnerDashboardCollectionsData - .number_of_nonexistent_collections)), - })); - - const fetchExplorationsDataSpy = spyOn( - learnerDashboardBackendApiService, - 'fetchLearnerDashboardExplorationsDataAsync') - .and.returnValue(Promise.resolve({ - completedExplorationsList: ( - learnerDashboardExplorationsData.completed_explorations_list.map( - expSummary => LearnerExplorationSummary.createFromBackendDict( - expSummary))), - incompleteExplorationsList: ( - learnerDashboardExplorationsData.incomplete_explorations_list.map( - expSummary => LearnerExplorationSummary.createFromBackendDict( - expSummary))), - explorationPlaylist: ( - learnerDashboardExplorationsData.exploration_playlist.map( - expSummary => LearnerExplorationSummary.createFromBackendDict( - expSummary))), - numberOfNonexistentExplorations: ( - NonExistentExplorations.createFromBackendDict( - learnerDashboardExplorationsData - .number_of_nonexistent_explorations)), - subscriptionList: ( - learnerDashboardExplorationsData.subscription_list.map( - profileSummary => ProfileSummary - .createFromCreatorBackendDict(profileSummary))) - })); - - let newActiveSectionName = ( - 'I18N_LEARNER_DASHBOARD_COMMUNITY_LESSONS_SECTION'); - component.setActiveSection(newActiveSectionName); - - tick(); - fixture.detectChanges(); + expect(alertsSpy).toHaveBeenCalledWith( + 'Failed to get learner dashboard topics and stories data' + ); + expect(fetchDataSpy).toHaveBeenCalled(); + }) + ); - expect(fetchCollectionsDataSpy).toHaveBeenCalled(); - flush(); - expect(fetchExplorationsDataSpy).toHaveBeenCalled(); - expect(component.communtiyLessonsDataLoaded).toEqual(true); - })); + it( + 'should show an alert warning when fails to get collections data' + + 'in mobile view', + fakeAsync(() => { + const fetchDataSpy = spyOn( + learnerDashboardBackendApiService, + 'fetchLearnerDashboardCollectionsDataAsync' + ).and.rejectWith(404); + const alertsSpy = spyOn(alertsService, 'addWarning').and.callThrough(); + + let newActiveSectionName = 'I18N_DASHBOARD_LESSONS'; + component.setActiveSubsection(newActiveSectionName); + + tick(); + fixture.detectChanges(); + + expect(alertsSpy).toHaveBeenCalledWith( + 'Failed to get learner dashboard collections data' + ); + expect(fetchDataSpy).toHaveBeenCalled(); + }) + ); + + it( + 'should show an alert warning when fails to get explorations data in' + + 'mobile view', + fakeAsync(() => { + const fetchDataSpy = spyOn( + learnerDashboardBackendApiService, + 'fetchLearnerDashboardExplorationsDataAsync' + ).and.rejectWith(404); + const alertsSpy = spyOn(alertsService, 'addWarning').and.callThrough(); + + let newActiveSectionName = 'I18N_DASHBOARD_LESSONS'; + component.setActiveSubsection(newActiveSectionName); + + tick(); + fixture.detectChanges(); + + expect(alertsSpy).toHaveBeenCalledWith( + 'Failed to get learner dashboard explorations data' + ); + expect(fetchDataSpy).toHaveBeenCalled(); + }) + ); + + it( + 'should get explorations and collections data when user clicks ' + + 'communtiy lessons tab in mobile view', + fakeAsync(() => { + const fetchCollectionsDataSpy = spyOn( + learnerDashboardBackendApiService, + 'fetchLearnerDashboardCollectionsDataAsync' + ).and.returnValue( + Promise.resolve({ + completedCollectionsList: + learnerDashboardCollectionsData.completed_collections_list.map( + collectionSummary => + CollectionSummary.createFromBackendDict(collectionSummary) + ), + incompleteCollectionsList: + learnerDashboardCollectionsData.incomplete_collections_list.map( + collectionSummary => + CollectionSummary.createFromBackendDict(collectionSummary) + ), + collectionPlaylist: + learnerDashboardCollectionsData.collection_playlist.map( + collectionSummary => + CollectionSummary.createFromBackendDict(collectionSummary) + ), + completedToIncompleteCollections: + learnerDashboardCollectionsData.completed_to_incomplete_collections, + numberOfNonexistentCollections: + NonExistentCollections.createFromBackendDict( + learnerDashboardCollectionsData.number_of_nonexistent_collections + ), + }) + ); + + const fetchExplorationsDataSpy = spyOn( + learnerDashboardBackendApiService, + 'fetchLearnerDashboardExplorationsDataAsync' + ).and.returnValue( + Promise.resolve({ + completedExplorationsList: + learnerDashboardExplorationsData.completed_explorations_list.map( + expSummary => + LearnerExplorationSummary.createFromBackendDict(expSummary) + ), + incompleteExplorationsList: + learnerDashboardExplorationsData.incomplete_explorations_list.map( + expSummary => + LearnerExplorationSummary.createFromBackendDict(expSummary) + ), + explorationPlaylist: + learnerDashboardExplorationsData.exploration_playlist.map( + expSummary => + LearnerExplorationSummary.createFromBackendDict(expSummary) + ), + numberOfNonexistentExplorations: + NonExistentExplorations.createFromBackendDict( + learnerDashboardExplorationsData.number_of_nonexistent_explorations + ), + subscriptionList: + learnerDashboardExplorationsData.subscription_list.map( + profileSummary => + ProfileSummary.createFromCreatorBackendDict(profileSummary) + ), + }) + ); + + let newActiveSectionName = 'I18N_DASHBOARD_LESSONS'; + component.setActiveSubsection(newActiveSectionName); + + tick(); + fixture.detectChanges(); + + expect(fetchCollectionsDataSpy).toHaveBeenCalled(); + flush(); + expect(fetchExplorationsDataSpy).toHaveBeenCalled(); + expect(component.communtiyLessonsDataLoaded).toEqual(true); + }) + ); + + it( + 'should show an alert warning when fails to get collections data ' + + 'in web view', + fakeAsync(() => { + const fetchDataSpy = spyOn( + learnerDashboardBackendApiService, + 'fetchLearnerDashboardCollectionsDataAsync' + ).and.rejectWith(404); + const alertsSpy = spyOn(alertsService, 'addWarning').and.callThrough(); + + let newActiveSectionName = + 'I18N_LEARNER_DASHBOARD_COMMUNITY_LESSONS_SECTION'; + component.setActiveSection(newActiveSectionName); + + tick(); + fixture.detectChanges(); + + expect(alertsSpy).toHaveBeenCalledWith( + 'Failed to get learner dashboard collections data' + ); + expect(fetchDataSpy).toHaveBeenCalled(); + }) + ); + + it( + 'should show an alert warning when fails to get explorations data in ' + + 'web view', + fakeAsync(() => { + const fetchDataSpy = spyOn( + learnerDashboardBackendApiService, + 'fetchLearnerDashboardExplorationsDataAsync' + ).and.rejectWith(404); + const alertsSpy = spyOn(alertsService, 'addWarning').and.callThrough(); + + let newActiveSectionName = + 'I18N_LEARNER_DASHBOARD_COMMUNITY_LESSONS_SECTION'; + component.setActiveSection(newActiveSectionName); + + tick(); + fixture.detectChanges(); + + expect(alertsSpy).toHaveBeenCalledWith( + 'Failed to get learner dashboard explorations data' + ); + expect(fetchDataSpy).toHaveBeenCalled(); + }) + ); + + it( + 'should get explorations and collections data when user clicks ' + + 'communtiy lessons tab in web view', + fakeAsync(() => { + const fetchCollectionsDataSpy = spyOn( + learnerDashboardBackendApiService, + 'fetchLearnerDashboardCollectionsDataAsync' + ).and.returnValue( + Promise.resolve({ + completedCollectionsList: + learnerDashboardCollectionsData.completed_collections_list.map( + collectionSummary => + CollectionSummary.createFromBackendDict(collectionSummary) + ), + incompleteCollectionsList: + learnerDashboardCollectionsData.incomplete_collections_list.map( + collectionSummary => + CollectionSummary.createFromBackendDict(collectionSummary) + ), + collectionPlaylist: + learnerDashboardCollectionsData.collection_playlist.map( + collectionSummary => + CollectionSummary.createFromBackendDict(collectionSummary) + ), + completedToIncompleteCollections: + learnerDashboardCollectionsData.completed_to_incomplete_collections, + numberOfNonexistentCollections: + NonExistentCollections.createFromBackendDict( + learnerDashboardCollectionsData.number_of_nonexistent_collections + ), + }) + ); + + const fetchExplorationsDataSpy = spyOn( + learnerDashboardBackendApiService, + 'fetchLearnerDashboardExplorationsDataAsync' + ).and.returnValue( + Promise.resolve({ + completedExplorationsList: + learnerDashboardExplorationsData.completed_explorations_list.map( + expSummary => + LearnerExplorationSummary.createFromBackendDict(expSummary) + ), + incompleteExplorationsList: + learnerDashboardExplorationsData.incomplete_explorations_list.map( + expSummary => + LearnerExplorationSummary.createFromBackendDict(expSummary) + ), + explorationPlaylist: + learnerDashboardExplorationsData.exploration_playlist.map( + expSummary => + LearnerExplorationSummary.createFromBackendDict(expSummary) + ), + numberOfNonexistentExplorations: + NonExistentExplorations.createFromBackendDict( + learnerDashboardExplorationsData.number_of_nonexistent_explorations + ), + subscriptionList: + learnerDashboardExplorationsData.subscription_list.map( + profileSummary => + ProfileSummary.createFromCreatorBackendDict(profileSummary) + ), + }) + ); + + let newActiveSectionName = + 'I18N_LEARNER_DASHBOARD_COMMUNITY_LESSONS_SECTION'; + component.setActiveSection(newActiveSectionName); + + tick(); + fixture.detectChanges(); + + expect(fetchCollectionsDataSpy).toHaveBeenCalled(); + flush(); + expect(fetchExplorationsDataSpy).toHaveBeenCalled(); + expect(component.communtiyLessonsDataLoaded).toEqual(true); + }) + ); it('should unsubscribe upon component destruction', () => { spyOn(component.directiveSubscriptions, 'unsubscribe'); diff --git a/core/templates/pages/learner-dashboard-page/learner-dashboard-page.component.ts b/core/templates/pages/learner-dashboard-page/learner-dashboard-page.component.ts index 1121c6a8255b..133e3aabf4da 100644 --- a/core/templates/pages/learner-dashboard-page/learner-dashboard-page.component.ts +++ b/core/templates/pages/learner-dashboard-page/learner-dashboard-page.component.ts @@ -12,38 +12,43 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Component for the learner dashboard. */ -import { Component, OnInit, OnDestroy } from '@angular/core'; -import { trigger, state, style, transition, - animate, group } from '@angular/animations'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; - -import { AppConstants } from 'app.constants'; -import { LearnerExplorationSummary } from 'domain/summary/learner-exploration-summary.model'; -import { CollectionSummary } from 'domain/collection/collection-summary.model'; -import { ProfileSummary } from 'domain/user/profile-summary.model'; -import { LearnerDashboardBackendApiService } from 'domain/learner_dashboard/learner-dashboard-backend-api.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { ThreadStatusDisplayService } from 'pages/exploration-editor-page/feedback-tab/services/thread-status-display.service'; -import { SuggestionModalForLearnerDashboardService } from 'pages/learner-dashboard-page/suggestion-modal/suggestion-modal-for-learner-dashboard.service'; -import { LearnerDashboardPageConstants } from 'pages/learner-dashboard-page/learner-dashboard-page.constants'; -import { AlertsService } from 'services/alerts.service'; -import { DateTimeFormatService } from 'services/date-time-format.service'; -import { LoaderService } from 'services/loader.service'; -import { UserService } from 'services/user.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { StorySummary } from 'domain/story/story-summary.model'; -import { LearnerTopicSummary } from 'domain/topic/learner-topic-summary.model'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { PageTitleService } from 'services/page-title.service'; -import { LearnerGroupBackendApiService } from 'domain/learner_group/learner-group-backend-api.service'; -import { UrlService } from 'services/contextual/url.service'; +import {Component, OnInit, OnDestroy} from '@angular/core'; +import { + trigger, + state, + style, + transition, + animate, + group, +} from '@angular/animations'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; + +import {AppConstants} from 'app.constants'; +import {LearnerExplorationSummary} from 'domain/summary/learner-exploration-summary.model'; +import {CollectionSummary} from 'domain/collection/collection-summary.model'; +import {ProfileSummary} from 'domain/user/profile-summary.model'; +import {LearnerDashboardBackendApiService} from 'domain/learner_dashboard/learner-dashboard-backend-api.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {ThreadStatusDisplayService} from 'pages/exploration-editor-page/feedback-tab/services/thread-status-display.service'; +import {SuggestionModalForLearnerDashboardService} from 'pages/learner-dashboard-page/suggestion-modal/suggestion-modal-for-learner-dashboard.service'; +import {LearnerDashboardPageConstants} from 'pages/learner-dashboard-page/learner-dashboard-page.constants'; +import {AlertsService} from 'services/alerts.service'; +import {DateTimeFormatService} from 'services/date-time-format.service'; +import {LoaderService} from 'services/loader.service'; +import {UserService} from 'services/user.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {StorySummary} from 'domain/story/story-summary.model'; +import {LearnerTopicSummary} from 'domain/topic/learner-topic-summary.model'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {PageTitleService} from 'services/page-title.service'; +import {LearnerGroupBackendApiService} from 'domain/learner_group/learner-group-backend-api.service'; +import {UrlService} from 'services/contextual/url.service'; import './learner-dashboard-page.component.css'; @@ -53,49 +58,78 @@ import './learner-dashboard-page.component.css'; styleUrls: ['./learner-dashboard-page.component.css'], animations: [ trigger('slideInOut', [ - state('true', style({ - 'max-height': '500px', opacity: '1', visibility: 'visible' - })), - state('false', style({ - 'max-height': '0px', opacity: '0', visibility: 'hidden' - })), - transition('true => false', [group([ - animate('500ms ease-in-out', style({ - opacity: '0' - })), - animate('500ms ease-in-out', style({ - 'max-height': '0px' - })), - animate('500ms ease-in-out', style({ - visibility: 'hidden' - })) - ] - )]), - transition('false => true', [group([ - animate('500ms ease-in-out', style({ - visibility: 'visible' - })), - animate('500ms ease-in-out', style({ - 'max-height': '500px' - })), - animate('500ms ease-in-out', style({ - opacity: '1' - })) - ] - )]) - ]) - ] + state( + 'true', + style({ + 'max-height': '500px', + opacity: '1', + visibility: 'visible', + }) + ), + state( + 'false', + style({ + 'max-height': '0px', + opacity: '0', + visibility: 'hidden', + }) + ), + transition('true => false', [ + group([ + animate( + '500ms ease-in-out', + style({ + opacity: '0', + }) + ), + animate( + '500ms ease-in-out', + style({ + 'max-height': '0px', + }) + ), + animate( + '500ms ease-in-out', + style({ + visibility: 'hidden', + }) + ), + ]), + ]), + transition('false => true', [ + group([ + animate( + '500ms ease-in-out', + style({ + visibility: 'visible', + }) + ), + animate( + '500ms ease-in-out', + style({ + 'max-height': '500px', + }) + ), + animate( + '500ms ease-in-out', + style({ + opacity: '1', + }) + ), + ]), + ]), + ]), + ], }) export class LearnerDashboardPageComponent implements OnInit, OnDestroy { - LEARNER_DASHBOARD_SECTION_I18N_IDS = ( - LearnerDashboardPageConstants.LEARNER_DASHBOARD_SECTION_I18N_IDS); + LEARNER_DASHBOARD_SECTION_I18N_IDS = + LearnerDashboardPageConstants.LEARNER_DASHBOARD_SECTION_I18N_IDS; - LEARNER_DASHBOARD_SUBSECTION_I18N_IDS = ( - LearnerDashboardPageConstants.LEARNER_DASHBOARD_SUBSECTION_I18N_IDS); + LEARNER_DASHBOARD_SUBSECTION_I18N_IDS = + LearnerDashboardPageConstants.LEARNER_DASHBOARD_SUBSECTION_I18N_IDS; username: string = ''; - PAGES_REGISTERED_WITH_FRONTEND = ( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND); + PAGES_REGISTERED_WITH_FRONTEND = AppConstants.PAGES_REGISTERED_WITH_FRONTEND; // These properties below are initialized using Angular lifecycle hooks // where we need to do non-null assertion. For more information see @@ -126,8 +160,8 @@ export class LearnerDashboardPageComponent implements OnInit, OnDestroy { explorationTitle!: string; explorationId!: string; - communityLibraryUrl = ( - '/' + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LIBRARY_INDEX.ROUTE); + communityLibraryUrl = + '/' + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LIBRARY_INDEX.ROUTE; communtiyLessonsDataLoaded: boolean = false; loadingIndicatorIsShown: boolean = false; @@ -144,11 +178,9 @@ export class LearnerDashboardPageComponent implements OnInit, OnDestroy { private dateTimeFormatService: DateTimeFormatService, private focusManagerService: FocusManagerService, private i18nLanguageCodeService: I18nLanguageCodeService, - private learnerDashboardBackendApiService: - LearnerDashboardBackendApiService, + private learnerDashboardBackendApiService: LearnerDashboardBackendApiService, private loaderService: LoaderService, - private suggestionModalForLearnerDashboardService: - SuggestionModalForLearnerDashboardService, + private suggestionModalForLearnerDashboardService: SuggestionModalForLearnerDashboardService, private threadStatusDisplayService: ThreadStatusDisplayService, private urlInterpolationService: UrlInterpolationService, private userService: UserService, @@ -166,90 +198,87 @@ export class LearnerDashboardPageComponent implements OnInit, OnDestroy { const username = userInfo.getUsername(); if (username) { this.username = username; - [this.profilePicturePngDataUrl, this.profilePictureWebpDataUrl] = ( - this.userService.getProfileImageDataUrl(username)); + [this.profilePicturePngDataUrl, this.profilePictureWebpDataUrl] = + this.userService.getProfileImageDataUrl(username); } else { - this.profilePictureWebpDataUrl = ( + this.profilePictureWebpDataUrl = this.urlInterpolationService.getStaticImageUrl( - AppConstants.DEFAULT_PROFILE_IMAGE_WEBP_PATH)); - this.profilePicturePngDataUrl = ( + AppConstants.DEFAULT_PROFILE_IMAGE_WEBP_PATH + ); + this.profilePicturePngDataUrl = this.urlInterpolationService.getStaticImageUrl( - AppConstants.DEFAULT_PROFILE_IMAGE_PNG_PATH)); + AppConstants.DEFAULT_PROFILE_IMAGE_PNG_PATH + ); } }); this.homeImageUrl = this.getStaticImageUrl('/learner_dashboard/home.svg'); this.todolistImageUrl = this.getStaticImageUrl( - '/learner_dashboard/todolist.svg'); + '/learner_dashboard/todolist.svg' + ); this.progressImageUrl = this.getStaticImageUrl( - '/learner_dashboard/progress.svg'); + '/learner_dashboard/progress.svg' + ); - let dashboardTopicAndStoriesDataPromise = ( - this.learnerDashboardBackendApiService - .fetchLearnerDashboardTopicsAndStoriesDataAsync()); + let dashboardTopicAndStoriesDataPromise = + this.learnerDashboardBackendApiService.fetchLearnerDashboardTopicsAndStoriesDataAsync(); dashboardTopicAndStoriesDataPromise.then( responseData => { - this.completedStoriesList = ( - responseData.completedStoriesList); - this.learntTopicsList = ( - responseData.learntTopicsList); - this.partiallyLearntTopicsList = ( - responseData.partiallyLearntTopicsList); - this.topicsToLearn = ( - responseData.topicsToLearnList); - this.untrackedTopics = ( - responseData.untrackedTopics); - this.allTopics = ( - responseData.allTopicsList); - this.learntToPartiallyLearntTopics = ( - responseData.learntToPartiallyLearntTopics); - this.activeSection = ( - LearnerDashboardPageConstants - .LEARNER_DASHBOARD_SECTION_I18N_IDS.HOME); - this.activeSubsection = ( - LearnerDashboardPageConstants - .LEARNER_DASHBOARD_SUBSECTION_I18N_IDS.SKILL_PROFICIENCY - ); + this.completedStoriesList = responseData.completedStoriesList; + this.learntTopicsList = responseData.learntTopicsList; + this.partiallyLearntTopicsList = responseData.partiallyLearntTopicsList; + this.topicsToLearn = responseData.topicsToLearnList; + this.untrackedTopics = responseData.untrackedTopics; + this.allTopics = responseData.allTopicsList; + this.learntToPartiallyLearntTopics = + responseData.learntToPartiallyLearntTopics; + this.activeSection = + LearnerDashboardPageConstants.LEARNER_DASHBOARD_SECTION_I18N_IDS.HOME; + this.activeSubsection = + LearnerDashboardPageConstants.LEARNER_DASHBOARD_SUBSECTION_I18N_IDS.SKILL_PROFICIENCY; if (this.urlService.getUrlParams().active_tab === 'learner-groups') { - this.activeSection = LearnerDashboardPageConstants - .LEARNER_DASHBOARD_SECTION_I18N_IDS.LEARNER_GROUPS; + this.activeSection = + LearnerDashboardPageConstants.LEARNER_DASHBOARD_SECTION_I18N_IDS.LEARNER_GROUPS; } - }, errorResponseStatus => { + }, + errorResponseStatus => { if ( - AppConstants.FATAL_ERROR_CODES.indexOf(errorResponseStatus) !== -1) { + AppConstants.FATAL_ERROR_CODES.indexOf(errorResponseStatus) !== -1 + ) { this.alertsService.addWarning( - 'Failed to get learner dashboard topics and stories data'); + 'Failed to get learner dashboard topics and stories data' + ); } } ); - let learnerGroupFeatureIsEnabledPromise = ( - this.learnerGroupBackendApiService.isLearnerGroupFeatureEnabledAsync() - ); + let learnerGroupFeatureIsEnabledPromise = + this.learnerGroupBackendApiService.isLearnerGroupFeatureEnabledAsync(); learnerGroupFeatureIsEnabledPromise.then(featureIsEnabled => { this.LEARNER_GROUP_FEATURE_IS_ENABLED = featureIsEnabled; }); - Promise.all([ userInfoPromise, dashboardTopicAndStoriesDataPromise, - learnerGroupFeatureIsEnabledPromise - ]).then(() => { - setTimeout(() => { - this.loaderService.hideLoadingScreen(); - // So that focus is applied after the loading screen has dissapeared. - this.focusManagerService.setFocusWithoutScroll('ourLessonsBtn'); - }, 0); - }).catch(errorResponse => { - // This is placed here in order to satisfy Unit tests. - }); - + learnerGroupFeatureIsEnabledPromise, + ]) + .then(() => { + setTimeout(() => { + this.loaderService.hideLoadingScreen(); + // So that focus is applied after the loading screen has dissapeared. + this.focusManagerService.setFocusWithoutScroll('ourLessonsBtn'); + }, 0); + }) + .catch(errorResponse => { + // This is placed here in order to satisfy Unit tests. + }); this.windowIsNarrow = this.windowDimensionService.isWindowNarrow(); this.directiveSubscriptions.add( this.windowDimensionService.getResizeEvent().subscribe(() => { this.windowIsNarrow = this.windowDimensionService.isWindowNarrow(); - })); + }) + ); this.directiveSubscriptions.add( this.translateService.onLangChange.subscribe(() => { this.setPageTitle(); @@ -262,153 +291,161 @@ export class LearnerDashboardPageComponent implements OnInit, OnDestroy { } getauthorPicturePngDataUrl(username: string): string { - let [pngImageUrl, _] = this.userService.getProfileImageDataUrl( - username); + let [pngImageUrl, _] = this.userService.getProfileImageDataUrl(username); return pngImageUrl; } getauthorPictureWebpDataUrl(username: string): string { - let [_, webpImageUrl] = this.userService.getProfileImageDataUrl( - username); + let [_, webpImageUrl] = this.userService.getProfileImageDataUrl(username); return webpImageUrl; } setPageTitle(): void { let translatedTitle = this.translateService.instant( - 'I18N_LEARNER_DASHBOARD_PAGE_TITLE'); + 'I18N_LEARNER_DASHBOARD_PAGE_TITLE' + ); this.pageTitleService.setDocumentTitle(translatedTitle); } - getStaticImageUrl(imagePath: string): string { return this.urlInterpolationService.getStaticImageUrl(imagePath); } setActiveSection(newActiveSectionName: string): void { this.activeSection = newActiveSectionName; - if (this.activeSection === - LearnerDashboardPageConstants - .LEARNER_DASHBOARD_SECTION_I18N_IDS.COMMUNITY_LESSONS) { + if ( + this.activeSection === + LearnerDashboardPageConstants.LEARNER_DASHBOARD_SECTION_I18N_IDS + .COMMUNITY_LESSONS + ) { this.loaderService.showLoadingScreen('Loading'); - let dashboardCollectionsDataPromise = ( - this.learnerDashboardBackendApiService - .fetchLearnerDashboardCollectionsDataAsync()); + let dashboardCollectionsDataPromise = + this.learnerDashboardBackendApiService.fetchLearnerDashboardCollectionsDataAsync(); dashboardCollectionsDataPromise.then( responseData => { - this.completedCollectionsList = ( - responseData.completedCollectionsList); - this.incompleteCollectionsList = ( - responseData.incompleteCollectionsList); - this.completedToIncompleteCollections = ( - responseData.completedToIncompleteCollections); + this.completedCollectionsList = responseData.completedCollectionsList; + this.incompleteCollectionsList = + responseData.incompleteCollectionsList; + this.completedToIncompleteCollections = + responseData.completedToIncompleteCollections; this.collectionPlaylist = responseData.collectionPlaylist; - }, errorResponseStatus => { + }, + errorResponseStatus => { if ( - AppConstants.FATAL_ERROR_CODES.indexOf(errorResponseStatus - ) !== -1) { + AppConstants.FATAL_ERROR_CODES.indexOf(errorResponseStatus) !== -1 + ) { this.alertsService.addWarning( - 'Failed to get learner dashboard collections data'); + 'Failed to get learner dashboard collections data' + ); } } ); - let dashboardExplorationsDataPromise = ( - this.learnerDashboardBackendApiService - .fetchLearnerDashboardExplorationsDataAsync()); + let dashboardExplorationsDataPromise = + this.learnerDashboardBackendApiService.fetchLearnerDashboardExplorationsDataAsync(); dashboardExplorationsDataPromise.then( responseData => { - this.completedExplorationsList = ( - responseData.completedExplorationsList); - this.incompleteExplorationsList = ( - responseData.incompleteExplorationsList); + this.completedExplorationsList = + responseData.completedExplorationsList; + this.incompleteExplorationsList = + responseData.incompleteExplorationsList; this.subscriptionsList = responseData.subscriptionList; this.explorationPlaylist = responseData.explorationPlaylist; - }, errorResponseStatus => { + }, + errorResponseStatus => { if ( - AppConstants.FATAL_ERROR_CODES.indexOf(errorResponseStatus - ) !== -1) { + AppConstants.FATAL_ERROR_CODES.indexOf(errorResponseStatus) !== -1 + ) { this.alertsService.addWarning( - 'Failed to get learner dashboard explorations data'); + 'Failed to get learner dashboard explorations data' + ); } } ); Promise.all([ dashboardCollectionsDataPromise, dashboardExplorationsDataPromise, - ]).then(() => { - setTimeout(() => { - this.loaderService.hideLoadingScreen(); - this.communtiyLessonsDataLoaded = true; - // So that focus is applied after the loading screen has dissapeared. - this.focusManagerService.setFocusWithoutScroll('ourLessonsBtn'); - }, 0); - }).catch(errorResponse => { - // This is placed here in order to satisfy Unit tests. - }); + ]) + .then(() => { + setTimeout(() => { + this.loaderService.hideLoadingScreen(); + this.communtiyLessonsDataLoaded = true; + // So that focus is applied after the loading screen has dissapeared. + this.focusManagerService.setFocusWithoutScroll('ourLessonsBtn'); + }, 0); + }) + .catch(errorResponse => { + // This is placed here in order to satisfy Unit tests. + }); } } setActiveSubsection(newActiveSubsectionName: string): void { this.activeSubsection = newActiveSubsectionName; - if (this.activeSubsection === - LearnerDashboardPageConstants - .LEARNER_DASHBOARD_SUBSECTION_I18N_IDS.LESSONS) { + if ( + this.activeSubsection === + LearnerDashboardPageConstants.LEARNER_DASHBOARD_SUBSECTION_I18N_IDS + .LESSONS + ) { this.loaderService.showLoadingScreen('Loading'); - let dashboardCollectionsDataPromise = ( - this.learnerDashboardBackendApiService - .fetchLearnerDashboardCollectionsDataAsync()); + let dashboardCollectionsDataPromise = + this.learnerDashboardBackendApiService.fetchLearnerDashboardCollectionsDataAsync(); dashboardCollectionsDataPromise.then( responseData => { - this.completedCollectionsList = ( - responseData.completedCollectionsList); - this.incompleteCollectionsList = ( - responseData.incompleteCollectionsList); - this.completedToIncompleteCollections = ( - responseData.completedToIncompleteCollections); + this.completedCollectionsList = responseData.completedCollectionsList; + this.incompleteCollectionsList = + responseData.incompleteCollectionsList; + this.completedToIncompleteCollections = + responseData.completedToIncompleteCollections; this.collectionPlaylist = responseData.collectionPlaylist; - }, errorResponseStatus => { + }, + errorResponseStatus => { if ( - AppConstants.FATAL_ERROR_CODES.indexOf(errorResponseStatus - ) !== -1) { + AppConstants.FATAL_ERROR_CODES.indexOf(errorResponseStatus) !== -1 + ) { this.alertsService.addWarning( - 'Failed to get learner dashboard collections data'); + 'Failed to get learner dashboard collections data' + ); } } ); - let dashboardExplorationsDataPromise = ( - this.learnerDashboardBackendApiService - .fetchLearnerDashboardExplorationsDataAsync()); + let dashboardExplorationsDataPromise = + this.learnerDashboardBackendApiService.fetchLearnerDashboardExplorationsDataAsync(); dashboardExplorationsDataPromise.then( responseData => { - this.completedExplorationsList = ( - responseData.completedExplorationsList); - this.incompleteExplorationsList = ( - responseData.incompleteExplorationsList); + this.completedExplorationsList = + responseData.completedExplorationsList; + this.incompleteExplorationsList = + responseData.incompleteExplorationsList; this.subscriptionsList = responseData.subscriptionList; this.explorationPlaylist = responseData.explorationPlaylist; - }, errorResponseStatus => { + }, + errorResponseStatus => { if ( - AppConstants.FATAL_ERROR_CODES.indexOf(errorResponseStatus - ) !== -1) { + AppConstants.FATAL_ERROR_CODES.indexOf(errorResponseStatus) !== -1 + ) { this.alertsService.addWarning( - 'Failed to get learner dashboard explorations data'); + 'Failed to get learner dashboard explorations data' + ); } } ); Promise.all([ dashboardCollectionsDataPromise, dashboardExplorationsDataPromise, - ]).then(() => { - setTimeout(() => { - this.loaderService.hideLoadingScreen(); - this.communtiyLessonsDataLoaded = true; - // So that focus is applied after the loading screen has dissapeared. - this.focusManagerService.setFocusWithoutScroll('ourLessonsBtn'); - }, 0); - }).catch(errorResponse => { - // This is placed here in order to satisfy Unit tests. - }); + ]) + .then(() => { + setTimeout(() => { + this.loaderService.hideLoadingScreen(); + this.communtiyLessonsDataLoaded = true; + // So that focus is applied after the loading screen has dissapeared. + this.focusManagerService.setFocusWithoutScroll('ourLessonsBtn'); + }, 0); + }) + .catch(errorResponse => { + // This is placed here in order to satisfy Unit tests. + }); } } @@ -424,13 +461,16 @@ export class LearnerDashboardPageComponent implements OnInit, OnDestroy { } showSuggestionModal( - newContent: string, oldContent: string, description: string): void { + newContent: string, + oldContent: string, + description: string + ): void { this.suggestionModalForLearnerDashboardService.showSuggestionModal( 'edit_exploration_state_content', { newContent: newContent, oldContent: oldContent, - description: description + description: description, } ); } @@ -445,7 +485,8 @@ export class LearnerDashboardPageComponent implements OnInit, OnDestroy { getLocaleAbbreviatedDatetimeString(millisSinceEpoch: number): string { return this.dateTimeFormatService.getLocaleAbbreviatedDatetimeString( - millisSinceEpoch); + millisSinceEpoch + ); } decodePngURIData(base64ImageData: string): string { diff --git a/core/templates/pages/learner-dashboard-page/learner-dashboard-page.constants.ajs.ts b/core/templates/pages/learner-dashboard-page/learner-dashboard-page.constants.ajs.ts index 2438f19ce1ed..f477e5517e82 100644 --- a/core/templates/pages/learner-dashboard-page/learner-dashboard-page.constants.ajs.ts +++ b/core/templates/pages/learner-dashboard-page/learner-dashboard-page.constants.ajs.ts @@ -18,17 +18,25 @@ // TODO(#7092): Delete this file once migration is complete and these AngularJS // equivalents of the Angular constants are no longer needed. -import { LearnerDashboardPageConstants } from - 'pages/learner-dashboard-page/learner-dashboard-page.constants'; +import {LearnerDashboardPageConstants} from 'pages/learner-dashboard-page/learner-dashboard-page.constants'; -angular.module('oppia').constant( - 'LEARNER_DASHBOARD_SECTION_I18N_IDS', - LearnerDashboardPageConstants.LEARNER_DASHBOARD_SECTION_I18N_IDS); +angular + .module('oppia') + .constant( + 'LEARNER_DASHBOARD_SECTION_I18N_IDS', + LearnerDashboardPageConstants.LEARNER_DASHBOARD_SECTION_I18N_IDS + ); -angular.module('oppia').constant( - 'LEARNER_DASHBOARD_SUBSECTION_I18N_IDS', - LearnerDashboardPageConstants.LEARNER_DASHBOARD_SUBSECTION_I18N_IDS); +angular + .module('oppia') + .constant( + 'LEARNER_DASHBOARD_SUBSECTION_I18N_IDS', + LearnerDashboardPageConstants.LEARNER_DASHBOARD_SUBSECTION_I18N_IDS + ); -angular.module('oppia').constant( - 'FEEDBACK_THREADS_SORT_BY_KEYS_AND_I18N_IDS', - LearnerDashboardPageConstants.FEEDBACK_THREADS_SORT_BY_KEYS_AND_I18N_IDS); +angular + .module('oppia') + .constant( + 'FEEDBACK_THREADS_SORT_BY_KEYS_AND_I18N_IDS', + LearnerDashboardPageConstants.FEEDBACK_THREADS_SORT_BY_KEYS_AND_I18N_IDS + ); diff --git a/core/templates/pages/learner-dashboard-page/learner-dashboard-page.constants.ts b/core/templates/pages/learner-dashboard-page/learner-dashboard-page.constants.ts index f93f3131f4dc..dd594715cfee 100644 --- a/core/templates/pages/learner-dashboard-page/learner-dashboard-page.constants.ts +++ b/core/templates/pages/learner-dashboard-page/learner-dashboard-page.constants.ts @@ -27,7 +27,7 @@ export const LearnerDashboardPageConstants = { CURRENT_GOALS: 'I18N_LEARNER_DASHBOARD_CURRENT_GOALS_SECTION', COMPLETED_GOALS: 'I18N_LEARNER_DASHBOARD_COMPLETED_GOALS_SECTION', LEARNER_GROUPS: 'I18N_LEARNER_DASHBOARD_LEARNER_GROUPS_SECTION', - COMMUNITY_LESSONS: 'I18N_LEARNER_DASHBOARD_COMMUNITY_LESSONS_SECTION' + COMMUNITY_LESSONS: 'I18N_LEARNER_DASHBOARD_COMMUNITY_LESSONS_SECTION', }, LEARNER_DASHBOARD_SUBSECTION_I18N_IDS: { @@ -36,17 +36,17 @@ export const LearnerDashboardPageConstants = { LEARN_TOPIC: 'I18N_DASHBOARD_LEARN_TOPIC', STORIES: 'I18N_DASHBOARD_STORIES', SKILL_PROFICIENCY: 'I18N_DASHBOARD_SKILL_PROFICIENCY', - LESSONS: 'I18N_DASHBOARD_LESSONS' + LESSONS: 'I18N_DASHBOARD_LESSONS', }, FEEDBACK_THREADS_SORT_BY_KEYS_AND_I18N_IDS: { LAST_UPDATED: { key: 'lastUpdatedMsecs', - i18nId: 'I18N_DASHBOARD_EXPLORATIONS_SORT_BY_LAST_UPDATED' + i18nId: 'I18N_DASHBOARD_EXPLORATIONS_SORT_BY_LAST_UPDATED', }, EXPLORATION: { key: 'explorationTitle', - i18nId: 'I18N_DASHBOARD_TABLE_HEADING_EXPLORATION' - } - } + i18nId: 'I18N_DASHBOARD_TABLE_HEADING_EXPLORATION', + }, + }, } as const; diff --git a/core/templates/pages/learner-dashboard-page/learner-dashboard-page.import.ts b/core/templates/pages/learner-dashboard-page/learner-dashboard-page.import.ts index ffa331b21be4..1072150eb109 100644 --- a/core/templates/pages/learner-dashboard-page/learner-dashboard-page.import.ts +++ b/core/templates/pages/learner-dashboard-page/learner-dashboard-page.import.ts @@ -22,9 +22,14 @@ import 'zone.js'; import uiValidate from 'angular-ui-validate'; angular.module('oppia', [ - require('angular-cookies'), 'ngAnimate', - 'ngMaterial', 'ngSanitize', 'ngTouch', 'pascalprecht.translate', - 'ui.bootstrap', uiValidate + require('angular-cookies'), + 'ngAnimate', + 'ngMaterial', + 'ngSanitize', + 'ngTouch', + 'pascalprecht.translate', + 'ui.bootstrap', + uiValidate, ]); require('Polyfills.ts'); diff --git a/core/templates/pages/learner-dashboard-page/learner-dashboard-page.module.ts b/core/templates/pages/learner-dashboard-page/learner-dashboard-page.module.ts index 605702fb0ced..047f8b27302c 100644 --- a/core/templates/pages/learner-dashboard-page/learner-dashboard-page.module.ts +++ b/core/templates/pages/learner-dashboard-page/learner-dashboard-page.module.ts @@ -16,30 +16,29 @@ * @fileoverview Module for the learner dashboard page. */ -import { NgModule } from '@angular/core'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { RouterModule } from '@angular/router'; -import { CommonModule } from '@angular/common'; +import {NgModule} from '@angular/core'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {RouterModule} from '@angular/router'; +import {CommonModule} from '@angular/common'; -import { LearnerStorySummaryTileComponent } from 'components/summary-tile/learner-story-summary-tile.component'; -import { LearnerTopicGoalsSummaryTileComponent } from 'components/summary-tile/learner-topic-goals-summary-tile.component'; -import { ProgressTabComponent } from './progress-tab.component'; -import { GoalsTabComponent } from './goals-tab.component'; -import { CommunityLessonsTabComponent } from './community-lessons-tab.component'; -import { LearnerTopicSummaryTileComponent } from 'components/summary-tile/learner-topic-summary-tile.component'; -import { HomeTabComponent } from './home-tab.component'; -import { LearnerGroupsTabComponent } from './learner-groups-tab.component'; -import { LearnerDashboardPageComponent } from './learner-dashboard-page.component'; -import { LearnerDashboardPageRootComponent } from './learner-dashboard-page-root.component'; -import { RemoveActivityModalComponent } from 'pages/learner-dashboard-page/modal-templates/remove-activity-modal.component'; -import { DeclineInvitationModalComponent } from './modal-templates/decline-invitaiton-modal.component'; -import { ViewLearnerGroupInvitationModalComponent } from './modal-templates/view-learner-group-invitation-modal.component'; -import { LearnerDashboardSuggestionModalComponent } from './suggestion-modal/learner-dashboard-suggestion-modal.component'; -import { ViewLearnerGroupDetailsModalComponent } from './modal-templates/view-learner-group-details-modal.component'; -import { toastrConfig } from 'pages/oppia-root/app.module'; -import { ToastrModule } from 'ngx-toastr'; -import { LearnerDashboardActivityBackendApiService } from - 'domain/learner_dashboard/learner-dashboard-activity-backend-api.service'; +import {LearnerStorySummaryTileComponent} from 'components/summary-tile/learner-story-summary-tile.component'; +import {LearnerTopicGoalsSummaryTileComponent} from 'components/summary-tile/learner-topic-goals-summary-tile.component'; +import {ProgressTabComponent} from './progress-tab.component'; +import {GoalsTabComponent} from './goals-tab.component'; +import {CommunityLessonsTabComponent} from './community-lessons-tab.component'; +import {LearnerTopicSummaryTileComponent} from 'components/summary-tile/learner-topic-summary-tile.component'; +import {HomeTabComponent} from './home-tab.component'; +import {LearnerGroupsTabComponent} from './learner-groups-tab.component'; +import {LearnerDashboardPageComponent} from './learner-dashboard-page.component'; +import {LearnerDashboardPageRootComponent} from './learner-dashboard-page-root.component'; +import {RemoveActivityModalComponent} from 'pages/learner-dashboard-page/modal-templates/remove-activity-modal.component'; +import {DeclineInvitationModalComponent} from './modal-templates/decline-invitaiton-modal.component'; +import {ViewLearnerGroupInvitationModalComponent} from './modal-templates/view-learner-group-invitation-modal.component'; +import {LearnerDashboardSuggestionModalComponent} from './suggestion-modal/learner-dashboard-suggestion-modal.component'; +import {ViewLearnerGroupDetailsModalComponent} from './modal-templates/view-learner-group-details-modal.component'; +import {toastrConfig} from 'pages/oppia-root/app.module'; +import {ToastrModule} from 'ngx-toastr'; +import {LearnerDashboardActivityBackendApiService} from 'domain/learner_dashboard/learner-dashboard-activity-backend-api.service'; @NgModule({ imports: [ @@ -52,7 +51,6 @@ import { LearnerDashboardActivityBackendApiService } from component: LearnerDashboardPageRootComponent, }, ]), - ], declarations: [ LearnerDashboardPageComponent, @@ -69,7 +67,7 @@ import { LearnerDashboardActivityBackendApiService } from LearnerDashboardSuggestionModalComponent, DeclineInvitationModalComponent, ViewLearnerGroupInvitationModalComponent, - ViewLearnerGroupDetailsModalComponent + ViewLearnerGroupDetailsModalComponent, ], entryComponents: [ LearnerDashboardPageComponent, @@ -85,10 +83,8 @@ import { LearnerDashboardActivityBackendApiService } from LearnerDashboardSuggestionModalComponent, DeclineInvitationModalComponent, ViewLearnerGroupInvitationModalComponent, - ViewLearnerGroupDetailsModalComponent + ViewLearnerGroupDetailsModalComponent, ], - providers: [ - LearnerDashboardActivityBackendApiService - ] + providers: [LearnerDashboardActivityBackendApiService], }) export class LearnerDashboardPageModule {} diff --git a/core/templates/pages/learner-dashboard-page/learner-groups-tab.component.spec.ts b/core/templates/pages/learner-dashboard-page/learner-groups-tab.component.spec.ts index de1bebdc46a7..32b387d90d86 100644 --- a/core/templates/pages/learner-dashboard-page/learner-groups-tab.component.spec.ts +++ b/core/templates/pages/learner-dashboard-page/learner-groups-tab.component.spec.ts @@ -16,22 +16,27 @@ * @fileoverview Unit tests for for LearnerGroupsTabComponent. */ -import { async, ComponentFixture, fakeAsync, TestBed, tick } from - '@angular/core/testing'; -import { MaterialModule } from 'modules/material.module'; -import { FormsModule } from '@angular/forms'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { LearnerGroupsTabComponent } from './learner-groups-tab.component'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { ShortLearnerGroupSummary } from 'domain/learner_group/short-learner-group-summary.model'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { LearnerDashboardBackendApiService } from 'domain/learner_dashboard/learner-dashboard-backend-api.service'; -import { LearnerGroupData } from 'domain/learner_group/learner-group.model'; -import { LearnerGroupBackendApiService } from 'domain/learner_group/learner-group-backend-api.service'; +import { + async, + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {MaterialModule} from 'modules/material.module'; +import {FormsModule} from '@angular/forms'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {LearnerGroupsTabComponent} from './learner-groups-tab.component'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {ShortLearnerGroupSummary} from 'domain/learner_group/short-learner-group-summary.model'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {LearnerDashboardBackendApiService} from 'domain/learner_dashboard/learner-dashboard-backend-api.service'; +import {LearnerGroupData} from 'domain/learner_group/learner-group.model'; +import {LearnerGroupBackendApiService} from 'domain/learner_group/learner-group-backend-api.service'; describe('Learner groups tab Component', () => { let component: LearnerGroupsTabComponent; @@ -44,11 +49,21 @@ describe('Learner groups tab Component', () => { let mockResizeEmitter: EventEmitter; const sampleShortLearnerGroupSummary = new ShortLearnerGroupSummary( - 'sampleId', 'sampleTitle', 'sampleDescription', ['username1'], 2 + 'sampleId', + 'sampleTitle', + 'sampleDescription', + ['username1'], + 2 ); const sampleLearnerGroupSummary = new LearnerGroupData( - 'sampleId', 'sampleTitle', 'sampleDescription', ['username1'], - ['user1', 'user2'], [], ['subtopic1', 'subtopic2'], ['story1', 'story2'] + 'sampleId', + 'sampleTitle', + 'sampleDescription', + ['username1'], + ['user1', 'user2'], + [], + ['subtopic1', 'subtopic2'], + ['story1', 'story2'] ); beforeEach(async(() => { @@ -58,12 +73,9 @@ describe('Learner groups tab Component', () => { BrowserAnimationsModule, MaterialModule, FormsModule, - HttpClientTestingModule - ], - declarations: [ - MockTranslatePipe, - LearnerGroupsTabComponent + HttpClientTestingModule, ], + declarations: [MockTranslatePipe, LearnerGroupsTabComponent], providers: [ UrlInterpolationService, { @@ -71,10 +83,10 @@ describe('Learner groups tab Component', () => { useValue: { isWindowNarrow: () => true, getResizeEvent: () => mockResizeEmitter, - } - } + }, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -84,9 +96,11 @@ describe('Learner groups tab Component', () => { urlInterpolationService = TestBed.inject(UrlInterpolationService); windowDimensionsService = TestBed.inject(WindowDimensionsService); learnerDashboardBackendApiService = TestBed.inject( - LearnerDashboardBackendApiService); + LearnerDashboardBackendApiService + ); learnerGroupBackendApiService = TestBed.inject( - LearnerGroupBackendApiService); + LearnerGroupBackendApiService + ); ngbModal = TestBed.inject(NgbModal); component.username = 'username'; @@ -101,7 +115,7 @@ describe('Learner groups tab Component', () => { ).and.returnValue( Promise.resolve({ learnerGroupsJoined: [sampleShortLearnerGroupSummary], - invitedToLearnerGroups: [] + invitedToLearnerGroups: [], }) ); @@ -109,8 +123,9 @@ describe('Learner groups tab Component', () => { tick(); expect(component.invitedToLearnerGroups).toEqual([]); - expect(component.learnerGroupsJoined).toEqual( - [sampleShortLearnerGroupSummary]); + expect(component.learnerGroupsJoined).toEqual([ + sampleShortLearnerGroupSummary, + ]); expect(component.windowIsNarrow).toBe(false); })); @@ -131,70 +146,68 @@ describe('Learner groups tab Component', () => { it('should get url of the learner group page', () => { const learnerGroupUrl = urlInterpolationService.interpolateUrl( - '/learner-group/', { - groupId: 'groupId' + '/learner-group/', + { + groupId: 'groupId', } ); expect(component.getLearnerGroupPageUrl('groupId')).toBe(learnerGroupUrl); }); - it('should decline learner group invitation successfully', - fakeAsync(() => { - spyOn(ngbModal, 'open').and.returnValue({ - componentInstance: { - learnerGroupTitle: 'sampleTitle', - }, - result: Promise.resolve() - } as NgbModalRef); + it('should decline learner group invitation successfully', fakeAsync(() => { + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: { + learnerGroupTitle: 'sampleTitle', + }, + result: Promise.resolve(), + } as NgbModalRef); - component.invitedToLearnerGroups = [sampleShortLearnerGroupSummary]; + component.invitedToLearnerGroups = [sampleShortLearnerGroupSummary]; - component.declineLearnerGroupInvitation(sampleShortLearnerGroupSummary); - tick(); - fixture.detectChanges(); + component.declineLearnerGroupInvitation(sampleShortLearnerGroupSummary); + tick(); + fixture.detectChanges(); - expect(component.invitedToLearnerGroups).toEqual([]); - }) - ); + expect(component.invitedToLearnerGroups).toEqual([]); + })); - it('should accept learner group invitation successfully', - fakeAsync(() => { - spyOn(ngbModal, 'open').and.returnValue({ - componentInstance: { - learnerGroup: sampleShortLearnerGroupSummary, - }, - result: Promise.resolve({ - progressSharingPermission: false - }) - } as NgbModalRef); - spyOn(learnerGroupBackendApiService, 'updateLearnerGroupInviteAsync') - .and.returnValue(Promise.resolve(sampleLearnerGroupSummary)); - - component.invitedToLearnerGroups = [sampleShortLearnerGroupSummary]; - component.learnerGroupsJoined = []; - - component.acceptLearnerGroupInvitation(sampleShortLearnerGroupSummary); - tick(); - fixture.detectChanges(); - - expect(component.invitedToLearnerGroups).toEqual([]); - expect(component.learnerGroupsJoined).toEqual( - [sampleShortLearnerGroupSummary]); - }) - ); + it('should accept learner group invitation successfully', fakeAsync(() => { + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: { + learnerGroup: sampleShortLearnerGroupSummary, + }, + result: Promise.resolve({ + progressSharingPermission: false, + }), + } as NgbModalRef); + spyOn( + learnerGroupBackendApiService, + 'updateLearnerGroupInviteAsync' + ).and.returnValue(Promise.resolve(sampleLearnerGroupSummary)); - it('should view learner group details successfully', - fakeAsync(() => { - spyOn(ngbModal, 'open').and.returnValue({ - componentInstance: { - learnerGroup: sampleShortLearnerGroupSummary, - }, - result: Promise.resolve() - } as NgbModalRef); + component.invitedToLearnerGroups = [sampleShortLearnerGroupSummary]; + component.learnerGroupsJoined = []; - component.viewLearnerGroupDetails(sampleShortLearnerGroupSummary); - tick(); - fixture.detectChanges(); - }) - ); + component.acceptLearnerGroupInvitation(sampleShortLearnerGroupSummary); + tick(); + fixture.detectChanges(); + + expect(component.invitedToLearnerGroups).toEqual([]); + expect(component.learnerGroupsJoined).toEqual([ + sampleShortLearnerGroupSummary, + ]); + })); + + it('should view learner group details successfully', fakeAsync(() => { + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: { + learnerGroup: sampleShortLearnerGroupSummary, + }, + result: Promise.resolve(), + } as NgbModalRef); + + component.viewLearnerGroupDetails(sampleShortLearnerGroupSummary); + tick(); + fixture.detectChanges(); + })); }); diff --git a/core/templates/pages/learner-dashboard-page/learner-groups-tab.component.ts b/core/templates/pages/learner-dashboard-page/learner-groups-tab.component.ts index 60bf08683eb9..6532d36634b5 100644 --- a/core/templates/pages/learner-dashboard-page/learner-groups-tab.component.ts +++ b/core/templates/pages/learner-dashboard-page/learner-groups-tab.component.ts @@ -16,26 +16,26 @@ * @fileoverview Component for learner groups tab in the Learner Dashboard page. */ -import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { LearnerDashboardPageConstants } from 'pages/learner-dashboard-page/learner-dashboard-page.constants'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { Subscription } from 'rxjs'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { ShortLearnerGroupSummary } from 'domain/learner_group/short-learner-group-summary.model'; -import { LearnerDashboardBackendApiService } from 'domain/learner_dashboard/learner-dashboard-backend-api.service'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { AlertsService } from 'services/alerts.service'; -import { DeclineInvitationModalComponent } from './modal-templates/decline-invitaiton-modal.component'; -import { LearnerGroupBackendApiService } from 'domain/learner_group/learner-group-backend-api.service'; -import { ViewLearnerGroupInvitationModalComponent } from './modal-templates/view-learner-group-invitation-modal.component'; -import { ViewLearnerGroupDetailsModalComponent } from './modal-templates/view-learner-group-details-modal.component'; +import {Component, EventEmitter, Input, Output} from '@angular/core'; +import {LearnerDashboardPageConstants} from 'pages/learner-dashboard-page/learner-dashboard-page.constants'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {Subscription} from 'rxjs'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {ShortLearnerGroupSummary} from 'domain/learner_group/short-learner-group-summary.model'; +import {LearnerDashboardBackendApiService} from 'domain/learner_dashboard/learner-dashboard-backend-api.service'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {AlertsService} from 'services/alerts.service'; +import {DeclineInvitationModalComponent} from './modal-templates/decline-invitaiton-modal.component'; +import {LearnerGroupBackendApiService} from 'domain/learner_group/learner-group-backend-api.service'; +import {ViewLearnerGroupInvitationModalComponent} from './modal-templates/view-learner-group-invitation-modal.component'; +import {ViewLearnerGroupDetailsModalComponent} from './modal-templates/view-learner-group-details-modal.component'; import './learner-groups-tab.component.css'; - @Component({ - selector: 'oppia-learner-groups-tab', - templateUrl: './learner-groups-tab.component.html' - }) +@Component({ + selector: 'oppia-learner-groups-tab', + templateUrl: './learner-groups-tab.component.html', +}) export class LearnerGroupsTabComponent { @Output() setActiveSection: EventEmitter = new EventEmitter(); // These properties are initialized using Angular lifecycle hooks @@ -51,8 +51,7 @@ export class LearnerGroupsTabComponent { constructor( private windowDimensionService: WindowDimensionsService, private urlInterpolationService: UrlInterpolationService, - private learnerDashboardBackendApiService: - LearnerDashboardBackendApiService, + private learnerDashboardBackendApiService: LearnerDashboardBackendApiService, private ngbModal: NgbModal, private learnerGroupBackendApiService: LearnerGroupBackendApiService, private alertsService: AlertsService @@ -60,123 +59,137 @@ export class LearnerGroupsTabComponent { ngOnInit(): void { this.learnerDashboardBackendApiService - .fetchLearnerDashboardLearnerGroupsAsync().then( - (learnerDashboardLearnerGroups) => { - this.learnerGroupsJoined = ( - learnerDashboardLearnerGroups.learnerGroupsJoined); - this.invitedToLearnerGroups = ( - learnerDashboardLearnerGroups.invitedToLearnerGroups); - } - ); + .fetchLearnerDashboardLearnerGroupsAsync() + .then(learnerDashboardLearnerGroups => { + this.learnerGroupsJoined = + learnerDashboardLearnerGroups.learnerGroupsJoined; + this.invitedToLearnerGroups = + learnerDashboardLearnerGroups.invitedToLearnerGroups; + }); this.windowIsNarrow = this.windowDimensionService.isWindowNarrow(); this.directiveSubscriptions.add( this.windowDimensionService.getResizeEvent().subscribe(() => { this.windowIsNarrow = this.windowDimensionService.isWindowNarrow(); - })); + }) + ); } getLearnerGroupPageUrl(learnerGroupId: string): string { - return ( - this.urlInterpolationService.interpolateUrl( - '/learner-group/', { - groupId: learnerGroupId - } - ) + return this.urlInterpolationService.interpolateUrl( + '/learner-group/', + { + groupId: learnerGroupId, + } ); } changeActiveSection(): void { this.setActiveSection.emit( - LearnerDashboardPageConstants - .LEARNER_DASHBOARD_SECTION_I18N_IDS.LEARNER_GROUPS); + LearnerDashboardPageConstants.LEARNER_DASHBOARD_SECTION_I18N_IDS + .LEARNER_GROUPS + ); } declineLearnerGroupInvitation( - learnerGroupSummary: ShortLearnerGroupSummary + learnerGroupSummary: ShortLearnerGroupSummary ): void { - let modalRef = this.ngbModal.open( - DeclineInvitationModalComponent, - { - backdrop: 'static', - windowClass: 'decline-learner-group-invitation-modal' + let modalRef = this.ngbModal.open(DeclineInvitationModalComponent, { + backdrop: 'static', + windowClass: 'decline-learner-group-invitation-modal', + }); + modalRef.componentInstance.learnerGroupTitle = learnerGroupSummary.title; + + modalRef.result.then( + () => { + this.invitedToLearnerGroups = this.invitedToLearnerGroups.filter( + invitedGroup => invitedGroup.id !== learnerGroupSummary.id + ); + this.learnerGroupBackendApiService + .updateLearnerGroupInviteAsync( + learnerGroupSummary.id, + this.username, + false + ) + .then(); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. } ); - modalRef.componentInstance.learnerGroupTitle = learnerGroupSummary.title; + } - modalRef.result.then(() => { - this.invitedToLearnerGroups = this.invitedToLearnerGroups.filter( - (invitedGroup) => invitedGroup.id !== learnerGroupSummary.id - ); - this.learnerGroupBackendApiService.updateLearnerGroupInviteAsync( - learnerGroupSummary.id, this.username, false - ).then(); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. + viewLearnerGroupDetails(learnerGroupSummary: ShortLearnerGroupSummary): void { + let modalRef = this.ngbModal.open(ViewLearnerGroupDetailsModalComponent, { + backdrop: 'static', + windowClass: 'view-learner-group-details-modal', }); - } + modalRef.componentInstance.learnerGroup = learnerGroupSummary; - viewLearnerGroupDetails( - learnerGroupSummary: ShortLearnerGroupSummary - ): void { - let modalRef = this.ngbModal.open( - ViewLearnerGroupDetailsModalComponent, - { - backdrop: 'static', - windowClass: 'view-learner-group-details-modal' + modalRef.result.then( + () => { + // Note to developers: + // This callback is triggered when the Confirm button is clicked. + // No further action is needed. + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. } ); - modalRef.componentInstance.learnerGroup = learnerGroupSummary; - - modalRef.result.then(() => { - // Note to developers: - // This callback is triggered when the Confirm button is clicked. - // No further action is needed. - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); } acceptLearnerGroupInvitation( - learnerGroupSummary: ShortLearnerGroupSummary + learnerGroupSummary: ShortLearnerGroupSummary ): void { let modalRef = this.ngbModal.open( ViewLearnerGroupInvitationModalComponent, { backdrop: 'static', - windowClass: 'view-learner-group-invitation-modal' + windowClass: 'view-learner-group-invitation-modal', } ); modalRef.componentInstance.learnerGroup = learnerGroupSummary; - modalRef.result.then((data) => { - this.learnerGroupBackendApiService.updateLearnerGroupInviteAsync( - learnerGroupSummary.id, this.username, true, - data.progressSharingPermission - ).then((learnerGroup) => { - // Show a message to indicate that the learner has successfully joined - // the learner group. - this.alertsService.addSuccessMessage( - 'You have successfully joined ' + learnerGroup.title + - ' learner group.', this.alertTimeout); + modalRef.result.then( + data => { + this.learnerGroupBackendApiService + .updateLearnerGroupInviteAsync( + learnerGroupSummary.id, + this.username, + true, + data.progressSharingPermission + ) + .then(learnerGroup => { + // Show a message to indicate that the learner has successfully joined + // the learner group. + this.alertsService.addSuccessMessage( + 'You have successfully joined ' + + learnerGroup.title + + ' learner group.', + this.alertTimeout + ); - let acceptedLearnerGroupSummary = new ShortLearnerGroupSummary( - learnerGroup.id, learnerGroup.title, learnerGroup.description, - learnerGroup.facilitatorUsernames, - learnerGroup.learnerUsernames.length - ); - this.invitedToLearnerGroups = this.invitedToLearnerGroups.filter( - (invitedGroup) => invitedGroup.id !== learnerGroupSummary.id - ); - this.learnerGroupsJoined.push(acceptedLearnerGroupSummary); - }); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + let acceptedLearnerGroupSummary = new ShortLearnerGroupSummary( + learnerGroup.id, + learnerGroup.title, + learnerGroup.description, + learnerGroup.facilitatorUsernames, + learnerGroup.learnerUsernames.length + ); + this.invitedToLearnerGroups = this.invitedToLearnerGroups.filter( + invitedGroup => invitedGroup.id !== learnerGroupSummary.id + ); + this.learnerGroupsJoined.push(acceptedLearnerGroupSummary); + }); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } } diff --git a/core/templates/pages/learner-dashboard-page/modal-templates/decline-invitaiton-modal.component.ts b/core/templates/pages/learner-dashboard-page/modal-templates/decline-invitaiton-modal.component.ts index cecfc10648df..253a0fe9d8eb 100644 --- a/core/templates/pages/learner-dashboard-page/modal-templates/decline-invitaiton-modal.component.ts +++ b/core/templates/pages/learner-dashboard-page/modal-templates/decline-invitaiton-modal.component.ts @@ -16,20 +16,18 @@ * @fileoverview Component for decline learner group invitation modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-decline-invitation-modal', - templateUrl: './decline-invitation-modal.component.html' + templateUrl: './decline-invitation-modal.component.html', }) export class DeclineInvitationModalComponent extends ConfirmOrCancelModal { learnerGroupTitle!: string; - constructor( - private ngbActiveModal: NgbActiveModal, - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } } diff --git a/core/templates/pages/learner-dashboard-page/modal-templates/decline-invitations-modal.component.spec.ts b/core/templates/pages/learner-dashboard-page/modal-templates/decline-invitations-modal.component.spec.ts index 30eafc71fc8b..5d8e5126cdec 100644 --- a/core/templates/pages/learner-dashboard-page/modal-templates/decline-invitations-modal.component.spec.ts +++ b/core/templates/pages/learner-dashboard-page/modal-templates/decline-invitations-modal.component.spec.ts @@ -12,17 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Unit tests for the decline learner group invitation * modal component. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { DeclineInvitationModalComponent } from './decline-invitaiton-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {DeclineInvitationModalComponent} from './decline-invitaiton-modal.component'; class MockActiveModal { close(): void { @@ -34,21 +33,20 @@ class MockActiveModal { } } -describe('Decline Invitation Modal Component', function() { +describe('Decline Invitation Modal Component', function () { let component: DeclineInvitationModalComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - DeclineInvitationModalComponent, - MockTranslatePipe + declarations: [DeclineInvitationModalComponent, MockTranslatePipe], + providers: [ + { + provide: NgbActiveModal, + useClass: MockActiveModal, + }, ], - providers: [{ - provide: NgbActiveModal, - useClass: MockActiveModal - }], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); diff --git a/core/templates/pages/learner-dashboard-page/modal-templates/learner-playlist-modal.component.spec.ts b/core/templates/pages/learner-dashboard-page/modal-templates/learner-playlist-modal.component.spec.ts index 424900388afd..38e5e274d6e2 100644 --- a/core/templates/pages/learner-dashboard-page/modal-templates/learner-playlist-modal.component.spec.ts +++ b/core/templates/pages/learner-dashboard-page/modal-templates/learner-playlist-modal.component.spec.ts @@ -16,11 +16,17 @@ * @fileoverview Unit tests for for learnerPlaylistModal. */ -import { async, ComponentFixture, fakeAsync, flushMicrotasks, TestBed } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { LearnerPlaylistModalComponent } from './learner-playlist-modal.component'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; +import { + async, + ComponentFixture, + fakeAsync, + flushMicrotasks, + TestBed, +} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {LearnerPlaylistModalComponent} from './learner-playlist-modal.component'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; class MockActiveModal { close(): void { @@ -38,7 +44,7 @@ class MockUrlInterpolationService { } } -describe('Learner Playlist Modal Component', function() { +describe('Learner Playlist Modal Component', function () { let component: LearnerPlaylistModalComponent; let fixture: ComponentFixture; let ngbActiveModal: NgbActiveModal; @@ -48,13 +54,14 @@ describe('Learner Playlist Modal Component', function() { declarations: [LearnerPlaylistModalComponent, MockTranslatePipe], providers: [ { - provide: NgbActiveModal, useClass: MockActiveModal + provide: NgbActiveModal, + useClass: MockActiveModal, }, { provide: UrlInterpolationService, - useClass: MockUrlInterpolationService + useClass: MockUrlInterpolationService, }, - ] + ], }).compileComponents(); })); @@ -65,27 +72,33 @@ describe('Learner Playlist Modal Component', function() { component.activityId = '0'; component.activityTitle = 'Title'; component.activityType = 'exploration'; - component.removeFromLearnerPlaylistUrl = ( - '/learnerplaylistactivityhandler/exploration/0'); + component.removeFromLearnerPlaylistUrl = + '/learnerplaylistactivityhandler/exploration/0'; fixture.detectChanges(); }); + it( + 'should remove exploration in learner playlist when clicking on' + + 'remove button', + fakeAsync(() => { + const closeSpy = spyOn(ngbActiveModal, 'close').and.callThrough(); + component.removeFromLearnerPlaylistUrl = + '/learnerplaylistactivityhandler/exploration/0'; + component.remove(); + flushMicrotasks(); + expect(closeSpy).toHaveBeenCalledWith( + component.removeFromLearnerPlaylistUrl + ); + }) + ); - it('should remove exploration in learner playlist when clicking on' + - 'remove button', fakeAsync(() => { - const closeSpy = spyOn(ngbActiveModal, 'close').and.callThrough(); - component.removeFromLearnerPlaylistUrl = ( - '/learnerplaylistactivityhandler/exploration/0'); - component.remove(); - flushMicrotasks(); - expect(closeSpy).toHaveBeenCalledWith( - component.removeFromLearnerPlaylistUrl); - })); - - it('should not remove exploration in learner playlist' + - 'when clicking on cancel button', () => { - const dismissSpy = spyOn(ngbActiveModal, 'dismiss').and.callThrough(); - component.cancel(); - expect(dismissSpy).toHaveBeenCalled(); - }); + it( + 'should not remove exploration in learner playlist' + + 'when clicking on cancel button', + () => { + const dismissSpy = spyOn(ngbActiveModal, 'dismiss').and.callThrough(); + component.cancel(); + expect(dismissSpy).toHaveBeenCalled(); + } + ); }); diff --git a/core/templates/pages/learner-dashboard-page/modal-templates/learner-playlist-modal.component.ts b/core/templates/pages/learner-dashboard-page/modal-templates/learner-playlist-modal.component.ts index 7ad9c98bb473..f9fc77e1edff 100644 --- a/core/templates/pages/learner-dashboard-page/modal-templates/learner-playlist-modal.component.ts +++ b/core/templates/pages/learner-dashboard-page/modal-templates/learner-playlist-modal.component.ts @@ -16,14 +16,14 @@ * @fileoverview Component for learnerPlaylistModal. */ -import { Component, Input, OnInit } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; +import {Component, Input, OnInit} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; @Component({ selector: 'oppia-learner-playlist-modal', templateUrl: './learner-playlist-modal.component.html', - styleUrls: [] + styleUrls: [], }) export class LearnerPlaylistModalComponent implements OnInit { // These properties are initialized using Angular lifecycle hooks @@ -38,18 +38,19 @@ export class LearnerPlaylistModalComponent implements OnInit { constructor( private activeModal: NgbActiveModal, - private urlInterpolationService: UrlInterpolationService, + private urlInterpolationService: UrlInterpolationService ) {} ngOnInit(): void { this.sectionNameI18nId = 'I18N_LEARNER_DASHBOARD_PLAYLIST_SECTION'; - this.removeFromLearnerPlaylistUrl = ( + this.removeFromLearnerPlaylistUrl = this.urlInterpolationService.interpolateUrl( - '/learnerplaylistactivityhandler/' + - '/', { + '/learnerplaylistactivityhandler/' + '/', + { activityType: this.activityType, - activityId: this.activityId - })); + activityId: this.activityId, + } + ); } remove(): void { diff --git a/core/templates/pages/learner-dashboard-page/modal-templates/remove-activity-modal.component.spec.ts b/core/templates/pages/learner-dashboard-page/modal-templates/remove-activity-modal.component.spec.ts index 01b3f01efc16..906362bef45d 100644 --- a/core/templates/pages/learner-dashboard-page/modal-templates/remove-activity-modal.component.spec.ts +++ b/core/templates/pages/learner-dashboard-page/modal-templates/remove-activity-modal.component.spec.ts @@ -16,12 +16,18 @@ * @fileoverview Unit tests for for RemoveActivityModalComponent. */ -import { async, ComponentFixture, fakeAsync, flushMicrotasks, TestBed } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; - -import { RemoveActivityModalComponent } from './remove-activity-modal.component'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; +import { + async, + ComponentFixture, + fakeAsync, + flushMicrotasks, + TestBed, +} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; + +import {RemoveActivityModalComponent} from './remove-activity-modal.component'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; class MockActiveModal { close(): void { @@ -39,27 +45,24 @@ class MockUrlInterpolationService { } } -describe('Remove Activity Modal Component', function() { +describe('Remove Activity Modal Component', function () { let component: RemoveActivityModalComponent; let fixture: ComponentFixture; let ngbActiveModal: NgbActiveModal; beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ - RemoveActivityModalComponent, - MockTranslatePipe - ], + declarations: [RemoveActivityModalComponent, MockTranslatePipe], providers: [ { provide: NgbActiveModal, - useClass: MockActiveModal + useClass: MockActiveModal, }, { provide: UrlInterpolationService, - useClass: MockUrlInterpolationService + useClass: MockUrlInterpolationService, }, - ] + ], }).compileComponents(); })); @@ -69,226 +72,249 @@ describe('Remove Activity Modal Component', function() { ngbActiveModal = TestBed.inject(NgbActiveModal); }); - it('should remove exploration in learner playlist when clicking on' + - ' remove button', fakeAsync(() => { - const closeSpy = spyOn(ngbActiveModal, 'close').and.callThrough(); - - component.sectionNameI18nId = 'I18N_LEARNER_DASHBOARD_PLAYLIST_SECTION'; - component.subsectionName = 'I18N_DASHBOARD_EXPLORATIONS'; - component.activityId = '0'; - component.activityTitle = 'Title'; - component.removeActivityUrl = ( - '/learnerplaylistactivityhandler/exploration/0'); - - component.remove(); - flushMicrotasks(); - fixture.detectChanges(); - - expect(closeSpy).toHaveBeenCalledWith( - '/learnerplaylistactivityhandler/exploration/0'); - } - )); - - it('should not remove exploration in learner playlist' + - ' when clicking on cancel button', fakeAsync(() => { - const dismissSpy = spyOn(ngbActiveModal, 'dismiss').and.callThrough(); - - component.sectionNameI18nId = 'I18N_LEARNER_DASHBOARD_PLAYLIST_SECTION'; - component.subsectionName = 'I18N_DASHBOARD_EXPLORATIONS'; - component.activityId = '0'; - component.activityTitle = 'Title'; - component.removeActivityUrl = ( - '/learnerplaylistactivityhandler/exploration/0'); - - component.cancel(); - flushMicrotasks(); - fixture.detectChanges(); - - expect(dismissSpy).toHaveBeenCalled(); - } - )); - - it('should remove collection in learner playlist when clicking on' + - ' remove button', fakeAsync(() => { - const closeSpy = spyOn(ngbActiveModal, 'close').and.callThrough(); - - component.sectionNameI18nId = 'I18N_LEARNER_DASHBOARD_PLAYLIST_SECTION'; - component.subsectionName = 'I18N_DASHBOARD_COLLECTIONS'; - component.activityId = '0'; - component.activityTitle = 'Title'; - component.removeActivityUrl = ( - '/learnerplaylistactivityhandler/collection/0'); - - component.remove(); - flushMicrotasks(); - fixture.detectChanges(); - - expect(closeSpy).toHaveBeenCalledWith( - '/learnerplaylistactivityhandler/collection/0'); - } - )); - - it('should not remove collection in learner playlist' + - ' when clicking on cancel button', fakeAsync(() => { - const dismissSpy = spyOn(ngbActiveModal, 'dismiss').and.callThrough(); - - component.sectionNameI18nId = 'I18N_LEARNER_DASHBOARD_PLAYLIST_SECTION'; - component.subsectionName = 'I18N_DASHBOARD_COLLECTIONS'; - component.activityId = '0'; - component.activityTitle = 'Title'; - component.removeActivityUrl = ( - '/learnerplaylistactivityhandler/collection/0'); - - component.cancel(); - flushMicrotasks(); - fixture.detectChanges(); - - expect(dismissSpy).toHaveBeenCalled(); - } - )); - - it('should remove topic from current goals when clicking on' + - ' remove button', fakeAsync(() => { - const closeSpy = spyOn(ngbActiveModal, 'close').and.callThrough(); - - component.sectionNameI18nId = ( - 'I18N_LEARNER_DASHBOARD_CURRENT_GOALS_SECTION'); - component.subsectionName = 'I18N_DASHBOARD_LEARN_TOPIC'; - component.activityId = '0'; - component.activityTitle = 'Title'; - component.removeActivityUrl = ( - '/learnergoalshandler/topic/0'); - - component.remove(); - flushMicrotasks(); - fixture.detectChanges(); - - expect(closeSpy).toHaveBeenCalledWith( - '/learnergoalshandler/topic/0'); - } - )); - - it('should not remove topic from current goals' + - ' when clicking on cancel button', fakeAsync(() => { - const dismissSpy = spyOn(ngbActiveModal, 'dismiss').and.callThrough(); - - component.sectionNameI18nId = ( - 'I18N_LEARNER_DASHBOARD_CURRENT_GOALS_SECTION'); - component.subsectionName = 'I18N_DASHBOARD_LEARN_TOPIC'; - component.activityId = '0'; - component.activityTitle = 'Title'; - component.removeActivityUrl = ( - '/learnergoalshandler/topic/0'); - - component.cancel(); - flushMicrotasks(); - fixture.detectChanges(); - - expect(dismissSpy).toHaveBeenCalled(); - } - )); - - it('should remove exploration in incomplete playlist when clicking on' + - ' remove button', fakeAsync(() => { - const closeSpy = spyOn(ngbActiveModal, 'close').and.callThrough(); - - component.sectionNameI18nId = 'I18N_LEARNER_DASHBOARD_INCOMPLETE_SECTION'; - component.subsectionName = 'I18N_DASHBOARD_EXPLORATIONS'; - component.activityId = '0'; - component.activityTitle = 'Title'; - component.removeActivityUrl = ( - '/learnerincompleteactivityhandler/exploration/0'); - - component.remove(); - flushMicrotasks(); - fixture.detectChanges(); - - expect(closeSpy).toHaveBeenCalledWith( - '/learnerincompleteactivityhandler/exploration/0'); - } - )); - - it('should not remove exploration in incomplete playlist' + - ' when clicking on cancel button', fakeAsync(() => { - const dismissSpy = spyOn(ngbActiveModal, 'dismiss').and.callThrough(); - - component.sectionNameI18nId = 'I18N_LEARNER_DASHBOARD_INCOMPLETE_SECTION'; - component.subsectionName = 'I18N_DASHBOARD_EXPLORATIONS'; - component.activityId = '0'; - component.activityTitle = 'Title'; - component.removeActivityUrl = ( - '/learnerincompleteactivityhandler/exploration/0'); - - component.cancel(); - flushMicrotasks(); - fixture.detectChanges(); - - expect(dismissSpy).toHaveBeenCalled(); - } - )); - - it('should remove collection in incomplete playlist when clicking on' + - ' remove button', fakeAsync(() => { - const closeSpy = spyOn(ngbActiveModal, 'close').and.callThrough(); - - component.sectionNameI18nId = 'I18N_LEARNER_DASHBOARD_INCOMPLETE_SECTION'; - component.subsectionName = 'I18N_DASHBOARD_COLLECTIONS'; - component.activityId = '0'; - component.activityTitle = 'Title'; - component.removeActivityUrl = ( - '/learnerincompleteactivityhandler/collection/0'); - - component.remove(); - flushMicrotasks(); - fixture.detectChanges(); - - expect(closeSpy).toHaveBeenCalledWith( - '/learnerincompleteactivityhandler/collection/0'); - } - )); - - it('should not remove collection in incomplete playlist' + - ' when clicking on cancel button', fakeAsync(() => { - const dismissSpy = spyOn(ngbActiveModal, 'dismiss').and.callThrough(); - - component.sectionNameI18nId = 'I18N_LEARNER_DASHBOARD_INCOMPLETE_SECTION'; - component.subsectionName = 'I18N_DASHBOARD_COLLECTIONS'; - component.activityId = '0'; - component.activityTitle = 'Title'; - component.removeActivityUrl = ( - '/learnerincompleteactivityhandler/collection/0'); - - component.cancel(); - flushMicrotasks(); - fixture.detectChanges(); - - expect(dismissSpy).toHaveBeenCalled(); - } - )); - - it('should throw error if given section name is' + - ' not valid', fakeAsync(() => { - component.sectionNameI18nId = 'InvalidSection'; - component.subsectionName = 'I18N_DASHBOARD_COLLECTIONS'; - component.activityId = '0'; - component.activityTitle = 'Title'; - - expect(() => { - component.ngOnInit(); - }).toThrowError('Section name is not valid.'); - } - )); - - it('should throw error if given section name is' + - ' not valid', fakeAsync(() => { - component.sectionNameI18nId = 'I18N_LEARNER_DASHBOARD_INCOMPLETE_SECTION'; - component.subsectionName = 'InvalidSubSection'; - component.activityId = '0'; - component.activityTitle = 'Title'; - - expect(() => { - component.ngOnInit(); - }).toThrowError('Subsection name is not valid.'); - } - )); + it( + 'should remove exploration in learner playlist when clicking on' + + ' remove button', + fakeAsync(() => { + const closeSpy = spyOn(ngbActiveModal, 'close').and.callThrough(); + + component.sectionNameI18nId = 'I18N_LEARNER_DASHBOARD_PLAYLIST_SECTION'; + component.subsectionName = 'I18N_DASHBOARD_EXPLORATIONS'; + component.activityId = '0'; + component.activityTitle = 'Title'; + component.removeActivityUrl = + '/learnerplaylistactivityhandler/exploration/0'; + + component.remove(); + flushMicrotasks(); + fixture.detectChanges(); + + expect(closeSpy).toHaveBeenCalledWith( + '/learnerplaylistactivityhandler/exploration/0' + ); + }) + ); + + it( + 'should not remove exploration in learner playlist' + + ' when clicking on cancel button', + fakeAsync(() => { + const dismissSpy = spyOn(ngbActiveModal, 'dismiss').and.callThrough(); + + component.sectionNameI18nId = 'I18N_LEARNER_DASHBOARD_PLAYLIST_SECTION'; + component.subsectionName = 'I18N_DASHBOARD_EXPLORATIONS'; + component.activityId = '0'; + component.activityTitle = 'Title'; + component.removeActivityUrl = + '/learnerplaylistactivityhandler/exploration/0'; + + component.cancel(); + flushMicrotasks(); + fixture.detectChanges(); + + expect(dismissSpy).toHaveBeenCalled(); + }) + ); + + it( + 'should remove collection in learner playlist when clicking on' + + ' remove button', + fakeAsync(() => { + const closeSpy = spyOn(ngbActiveModal, 'close').and.callThrough(); + + component.sectionNameI18nId = 'I18N_LEARNER_DASHBOARD_PLAYLIST_SECTION'; + component.subsectionName = 'I18N_DASHBOARD_COLLECTIONS'; + component.activityId = '0'; + component.activityTitle = 'Title'; + component.removeActivityUrl = + '/learnerplaylistactivityhandler/collection/0'; + + component.remove(); + flushMicrotasks(); + fixture.detectChanges(); + + expect(closeSpy).toHaveBeenCalledWith( + '/learnerplaylistactivityhandler/collection/0' + ); + }) + ); + + it( + 'should not remove collection in learner playlist' + + ' when clicking on cancel button', + fakeAsync(() => { + const dismissSpy = spyOn(ngbActiveModal, 'dismiss').and.callThrough(); + + component.sectionNameI18nId = 'I18N_LEARNER_DASHBOARD_PLAYLIST_SECTION'; + component.subsectionName = 'I18N_DASHBOARD_COLLECTIONS'; + component.activityId = '0'; + component.activityTitle = 'Title'; + component.removeActivityUrl = + '/learnerplaylistactivityhandler/collection/0'; + + component.cancel(); + flushMicrotasks(); + fixture.detectChanges(); + + expect(dismissSpy).toHaveBeenCalled(); + }) + ); + + it( + 'should remove topic from current goals when clicking on' + + ' remove button', + fakeAsync(() => { + const closeSpy = spyOn(ngbActiveModal, 'close').and.callThrough(); + + component.sectionNameI18nId = + 'I18N_LEARNER_DASHBOARD_CURRENT_GOALS_SECTION'; + component.subsectionName = 'I18N_DASHBOARD_LEARN_TOPIC'; + component.activityId = '0'; + component.activityTitle = 'Title'; + component.removeActivityUrl = '/learnergoalshandler/topic/0'; + + component.remove(); + flushMicrotasks(); + fixture.detectChanges(); + + expect(closeSpy).toHaveBeenCalledWith('/learnergoalshandler/topic/0'); + }) + ); + + it( + 'should not remove topic from current goals' + + ' when clicking on cancel button', + fakeAsync(() => { + const dismissSpy = spyOn(ngbActiveModal, 'dismiss').and.callThrough(); + + component.sectionNameI18nId = + 'I18N_LEARNER_DASHBOARD_CURRENT_GOALS_SECTION'; + component.subsectionName = 'I18N_DASHBOARD_LEARN_TOPIC'; + component.activityId = '0'; + component.activityTitle = 'Title'; + component.removeActivityUrl = '/learnergoalshandler/topic/0'; + + component.cancel(); + flushMicrotasks(); + fixture.detectChanges(); + + expect(dismissSpy).toHaveBeenCalled(); + }) + ); + + it( + 'should remove exploration in incomplete playlist when clicking on' + + ' remove button', + fakeAsync(() => { + const closeSpy = spyOn(ngbActiveModal, 'close').and.callThrough(); + + component.sectionNameI18nId = 'I18N_LEARNER_DASHBOARD_INCOMPLETE_SECTION'; + component.subsectionName = 'I18N_DASHBOARD_EXPLORATIONS'; + component.activityId = '0'; + component.activityTitle = 'Title'; + component.removeActivityUrl = + '/learnerincompleteactivityhandler/exploration/0'; + + component.remove(); + flushMicrotasks(); + fixture.detectChanges(); + + expect(closeSpy).toHaveBeenCalledWith( + '/learnerincompleteactivityhandler/exploration/0' + ); + }) + ); + + it( + 'should not remove exploration in incomplete playlist' + + ' when clicking on cancel button', + fakeAsync(() => { + const dismissSpy = spyOn(ngbActiveModal, 'dismiss').and.callThrough(); + + component.sectionNameI18nId = 'I18N_LEARNER_DASHBOARD_INCOMPLETE_SECTION'; + component.subsectionName = 'I18N_DASHBOARD_EXPLORATIONS'; + component.activityId = '0'; + component.activityTitle = 'Title'; + component.removeActivityUrl = + '/learnerincompleteactivityhandler/exploration/0'; + + component.cancel(); + flushMicrotasks(); + fixture.detectChanges(); + + expect(dismissSpy).toHaveBeenCalled(); + }) + ); + + it( + 'should remove collection in incomplete playlist when clicking on' + + ' remove button', + fakeAsync(() => { + const closeSpy = spyOn(ngbActiveModal, 'close').and.callThrough(); + + component.sectionNameI18nId = 'I18N_LEARNER_DASHBOARD_INCOMPLETE_SECTION'; + component.subsectionName = 'I18N_DASHBOARD_COLLECTIONS'; + component.activityId = '0'; + component.activityTitle = 'Title'; + component.removeActivityUrl = + '/learnerincompleteactivityhandler/collection/0'; + + component.remove(); + flushMicrotasks(); + fixture.detectChanges(); + + expect(closeSpy).toHaveBeenCalledWith( + '/learnerincompleteactivityhandler/collection/0' + ); + }) + ); + + it( + 'should not remove collection in incomplete playlist' + + ' when clicking on cancel button', + fakeAsync(() => { + const dismissSpy = spyOn(ngbActiveModal, 'dismiss').and.callThrough(); + + component.sectionNameI18nId = 'I18N_LEARNER_DASHBOARD_INCOMPLETE_SECTION'; + component.subsectionName = 'I18N_DASHBOARD_COLLECTIONS'; + component.activityId = '0'; + component.activityTitle = 'Title'; + component.removeActivityUrl = + '/learnerincompleteactivityhandler/collection/0'; + + component.cancel(); + flushMicrotasks(); + fixture.detectChanges(); + + expect(dismissSpy).toHaveBeenCalled(); + }) + ); + + it( + 'should throw error if given section name is' + ' not valid', + fakeAsync(() => { + component.sectionNameI18nId = 'InvalidSection'; + component.subsectionName = 'I18N_DASHBOARD_COLLECTIONS'; + component.activityId = '0'; + component.activityTitle = 'Title'; + + expect(() => { + component.ngOnInit(); + }).toThrowError('Section name is not valid.'); + }) + ); + + it( + 'should throw error if given section name is' + ' not valid', + fakeAsync(() => { + component.sectionNameI18nId = 'I18N_LEARNER_DASHBOARD_INCOMPLETE_SECTION'; + component.subsectionName = 'InvalidSubSection'; + component.activityId = '0'; + component.activityTitle = 'Title'; + + expect(() => { + component.ngOnInit(); + }).toThrowError('Subsection name is not valid.'); + }) + ); }); diff --git a/core/templates/pages/learner-dashboard-page/modal-templates/remove-activity-modal.component.ts b/core/templates/pages/learner-dashboard-page/modal-templates/remove-activity-modal.component.ts index 98666a25d4e6..2740f405a639 100644 --- a/core/templates/pages/learner-dashboard-page/modal-templates/remove-activity-modal.component.ts +++ b/core/templates/pages/learner-dashboard-page/modal-templates/remove-activity-modal.component.ts @@ -16,12 +16,12 @@ * @fileoverview Component for removeActivityModal. */ -import { Component, Input, OnInit } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import {Component, Input, OnInit} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; -import { AppConstants } from 'app.constants'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { LearnerDashboardPageConstants } from 'pages/learner-dashboard-page/learner-dashboard-page.constants'; +import {AppConstants} from 'app.constants'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {LearnerDashboardPageConstants} from 'pages/learner-dashboard-page/learner-dashboard-page.constants'; @Component({ selector: 'oppia-remove-activity-modal', @@ -39,56 +39,62 @@ export class RemoveActivityModalComponent implements OnInit { constructor( private activeModal: NgbActiveModal, - private urlInterpolationService: UrlInterpolationService, + private urlInterpolationService: UrlInterpolationService ) {} ngOnInit(): void { let activityType = ''; - if (this.subsectionName === - LearnerDashboardPageConstants - .LEARNER_DASHBOARD_SUBSECTION_I18N_IDS.EXPLORATIONS) { + if ( + this.subsectionName === + LearnerDashboardPageConstants.LEARNER_DASHBOARD_SUBSECTION_I18N_IDS + .EXPLORATIONS + ) { activityType = AppConstants.ACTIVITY_TYPE_EXPLORATION; - } else if (this.subsectionName === - LearnerDashboardPageConstants - .LEARNER_DASHBOARD_SUBSECTION_I18N_IDS - .COLLECTIONS) { + } else if ( + this.subsectionName === + LearnerDashboardPageConstants.LEARNER_DASHBOARD_SUBSECTION_I18N_IDS + .COLLECTIONS + ) { activityType = AppConstants.ACTIVITY_TYPE_COLLECTION; - } else if (this.subsectionName === - LearnerDashboardPageConstants - .LEARNER_DASHBOARD_SUBSECTION_I18N_IDS - .LEARN_TOPIC) { + } else if ( + this.subsectionName === + LearnerDashboardPageConstants.LEARNER_DASHBOARD_SUBSECTION_I18N_IDS + .LEARN_TOPIC + ) { activityType = AppConstants.ACTIVITY_TYPE_LEARN_TOPIC; } else { throw new Error('Subsection name is not valid.'); } let removeActivityUrlPrefix = ''; - if (this.sectionNameI18nId === - LearnerDashboardPageConstants - .LEARNER_DASHBOARD_SECTION_I18N_IDS.PLAYLIST) { - removeActivityUrlPrefix = - '/learnerplaylistactivityhandler/'; - } else if (this.sectionNameI18nId === - LearnerDashboardPageConstants - .LEARNER_DASHBOARD_SECTION_I18N_IDS.INCOMPLETE) { - removeActivityUrlPrefix = - '/learnerincompleteactivityhandler/'; - } else if (this.sectionNameI18nId === - LearnerDashboardPageConstants - .LEARNER_DASHBOARD_SECTION_I18N_IDS.CURRENT_GOALS) { - removeActivityUrlPrefix = - '/learnergoalshandler/'; + if ( + this.sectionNameI18nId === + LearnerDashboardPageConstants.LEARNER_DASHBOARD_SECTION_I18N_IDS.PLAYLIST + ) { + removeActivityUrlPrefix = '/learnerplaylistactivityhandler/'; + } else if ( + this.sectionNameI18nId === + LearnerDashboardPageConstants.LEARNER_DASHBOARD_SECTION_I18N_IDS + .INCOMPLETE + ) { + removeActivityUrlPrefix = '/learnerincompleteactivityhandler/'; + } else if ( + this.sectionNameI18nId === + LearnerDashboardPageConstants.LEARNER_DASHBOARD_SECTION_I18N_IDS + .CURRENT_GOALS + ) { + removeActivityUrlPrefix = '/learnergoalshandler/'; } else { throw new Error('Section name is not valid.'); } - this.removeActivityUrl = ( - this.urlInterpolationService.interpolateUrl( - removeActivityUrlPrefix + - '/', { - activityType: activityType, - activityId: this.activityId - })); + this.removeActivityUrl = this.urlInterpolationService.interpolateUrl( + removeActivityUrlPrefix + '/', + { + activityType: activityType, + activityId: this.activityId, + } + ); } remove(): void { diff --git a/core/templates/pages/learner-dashboard-page/modal-templates/view-learner-group-details-modal.component.spec.ts b/core/templates/pages/learner-dashboard-page/modal-templates/view-learner-group-details-modal.component.spec.ts index 9df5e837125d..7ed337a2c3aa 100644 --- a/core/templates/pages/learner-dashboard-page/modal-templates/view-learner-group-details-modal.component.spec.ts +++ b/core/templates/pages/learner-dashboard-page/modal-templates/view-learner-group-details-modal.component.spec.ts @@ -12,19 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Unit tests for the view learner group details * modal component. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ShortLearnerGroupSummary } from 'domain/learner_group/short-learner-group-summary.model'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { ViewLearnerGroupDetailsModalComponent } from - './view-learner-group-details-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ShortLearnerGroupSummary} from 'domain/learner_group/short-learner-group-summary.model'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {ViewLearnerGroupDetailsModalComponent} from './view-learner-group-details-modal.component'; class MockActiveModal { close(): void { @@ -36,25 +34,28 @@ class MockActiveModal { } } -describe('View Learner Group Details Modal Component', function() { +describe('View Learner Group Details Modal Component', function () { let component: ViewLearnerGroupDetailsModalComponent; let fixture: ComponentFixture; const shortLearnerGroup = new ShortLearnerGroupSummary( - 'sampleId2', 'sampleTitle 2', 'sampleDescription 2', ['username1'], 7 + 'sampleId2', + 'sampleTitle 2', + 'sampleDescription 2', + ['username1'], + 7 ); beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - ViewLearnerGroupDetailsModalComponent, - MockTranslatePipe + declarations: [ViewLearnerGroupDetailsModalComponent, MockTranslatePipe], + providers: [ + { + provide: NgbActiveModal, + useClass: MockActiveModal, + }, ], - providers: [{ - provide: NgbActiveModal, - useClass: MockActiveModal - }], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); diff --git a/core/templates/pages/learner-dashboard-page/modal-templates/view-learner-group-details-modal.component.ts b/core/templates/pages/learner-dashboard-page/modal-templates/view-learner-group-details-modal.component.ts index c5e935894de4..1ad12012818b 100644 --- a/core/templates/pages/learner-dashboard-page/modal-templates/view-learner-group-details-modal.component.ts +++ b/core/templates/pages/learner-dashboard-page/modal-templates/view-learner-group-details-modal.component.ts @@ -16,22 +16,19 @@ * @fileoverview Component for view learner group details modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { ShortLearnerGroupSummary } from 'domain/learner_group/short-learner-group-summary.model'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {ShortLearnerGroupSummary} from 'domain/learner_group/short-learner-group-summary.model'; @Component({ selector: 'oppia-view-learner-group-details-modal', - templateUrl: './view-learner-group-details-modal.component.html' + templateUrl: './view-learner-group-details-modal.component.html', }) -export class ViewLearnerGroupDetailsModalComponent - extends ConfirmOrCancelModal { +export class ViewLearnerGroupDetailsModalComponent extends ConfirmOrCancelModal { learnerGroup!: ShortLearnerGroupSummary; - constructor( - private ngbActiveModal: NgbActiveModal, - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } } diff --git a/core/templates/pages/learner-dashboard-page/modal-templates/view-learner-group-invitation-modal.component.spec.ts b/core/templates/pages/learner-dashboard-page/modal-templates/view-learner-group-invitation-modal.component.spec.ts index 38e032e7e165..48e3e6ddea3d 100644 --- a/core/templates/pages/learner-dashboard-page/modal-templates/view-learner-group-invitation-modal.component.spec.ts +++ b/core/templates/pages/learner-dashboard-page/modal-templates/view-learner-group-invitation-modal.component.spec.ts @@ -12,19 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Unit tests for the view learner group invitation * modal component. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ShortLearnerGroupSummary } from 'domain/learner_group/short-learner-group-summary.model'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { ViewLearnerGroupInvitationModalComponent } from - './view-learner-group-invitation-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ShortLearnerGroupSummary} from 'domain/learner_group/short-learner-group-summary.model'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {ViewLearnerGroupInvitationModalComponent} from './view-learner-group-invitation-modal.component'; class MockActiveModal { close(): void { @@ -36,33 +34,38 @@ class MockActiveModal { } } -describe('View Learner Group Invitation Modal Component', function() { +describe('View Learner Group Invitation Modal Component', function () { let component: ViewLearnerGroupInvitationModalComponent; let fixture: ComponentFixture; let ngbActiveModal: NgbActiveModal; const shortLearnerGroup = new ShortLearnerGroupSummary( - 'sampleId2', 'sampleTitle 2', 'sampleDescription 2', ['username1'], 7 + 'sampleId2', + 'sampleTitle 2', + 'sampleDescription 2', + ['username1'], + 7 ); beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ ViewLearnerGroupInvitationModalComponent, - MockTranslatePipe + MockTranslatePipe, + ], + providers: [ + { + provide: NgbActiveModal, + useClass: MockActiveModal, + }, ], - providers: [{ - provide: NgbActiveModal, - useClass: MockActiveModal - }], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); beforeEach(() => { ngbActiveModal = TestBed.inject(NgbActiveModal); - fixture = TestBed.createComponent( - ViewLearnerGroupInvitationModalComponent); + fixture = TestBed.createComponent(ViewLearnerGroupInvitationModalComponent); component = fixture.componentInstance; component.learnerGroup = shortLearnerGroup; @@ -79,7 +82,7 @@ describe('View Learner Group Invitation Modal Component', function() { component.confirm(); expect(ngbActiveModal.close).toHaveBeenCalledWith({ - progressSharingPermission: component.progressSharingPermission + progressSharingPermission: component.progressSharingPermission, }); }); diff --git a/core/templates/pages/learner-dashboard-page/modal-templates/view-learner-group-invitation-modal.component.ts b/core/templates/pages/learner-dashboard-page/modal-templates/view-learner-group-invitation-modal.component.ts index 829eccf668dc..4cf30778836e 100644 --- a/core/templates/pages/learner-dashboard-page/modal-templates/view-learner-group-invitation-modal.component.ts +++ b/core/templates/pages/learner-dashboard-page/modal-templates/view-learner-group-invitation-modal.component.ts @@ -16,23 +16,20 @@ * @fileoverview Component for view learner group invitation modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { ShortLearnerGroupSummary } from 'domain/learner_group/short-learner-group-summary.model'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {ShortLearnerGroupSummary} from 'domain/learner_group/short-learner-group-summary.model'; @Component({ selector: 'oppia-view-learner-group-invitation-modal', - templateUrl: './view-learner-group-invitation-modal.component.html' + templateUrl: './view-learner-group-invitation-modal.component.html', }) -export class ViewLearnerGroupInvitationModalComponent - extends ConfirmOrCancelModal { +export class ViewLearnerGroupInvitationModalComponent extends ConfirmOrCancelModal { learnerGroup!: ShortLearnerGroupSummary; progressSharingPermission: boolean = true; - constructor( - private ngbActiveModal: NgbActiveModal, - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } diff --git a/core/templates/pages/learner-dashboard-page/progress-tab.component.spec.ts b/core/templates/pages/learner-dashboard-page/progress-tab.component.spec.ts index fd159febb3e5..a3f05724037f 100644 --- a/core/templates/pages/learner-dashboard-page/progress-tab.component.spec.ts +++ b/core/templates/pages/learner-dashboard-page/progress-tab.component.spec.ts @@ -16,40 +16,37 @@ * @fileoverview Unit tests for for ProgressTabComponent. */ -import { async, ComponentFixture, fakeAsync, TestBed, tick } from - '@angular/core/testing'; -import { MaterialModule } from 'modules/material.module'; -import { FormsModule } from '@angular/forms'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { ProgressTabComponent } from './progress-tab.component'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { LearnerDashboardBackendApiService } from 'domain/learner_dashboard/learner-dashboard-backend-api.service'; -import { StorySummary } from 'domain/story/story-summary.model'; -import { LearnerTopicSummary } from 'domain/topic/learner-topic-summary.model'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; +import { + async, + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {MaterialModule} from 'modules/material.module'; +import {FormsModule} from '@angular/forms'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {ProgressTabComponent} from './progress-tab.component'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import {LearnerDashboardBackendApiService} from 'domain/learner_dashboard/learner-dashboard-backend-api.service'; +import {StorySummary} from 'domain/story/story-summary.model'; +import {LearnerTopicSummary} from 'domain/topic/learner-topic-summary.model'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; describe('Progress tab Component', () => { let component: ProgressTabComponent; let fixture: ComponentFixture; let urlInterpolationService: UrlInterpolationService; - let learnerDashboardBackendApiService: - LearnerDashboardBackendApiService; + let learnerDashboardBackendApiService: LearnerDashboardBackendApiService; let windowDimensionsService: WindowDimensionsService; let mockResizeEmitter: EventEmitter = new EventEmitter(); beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [ - MaterialModule, - FormsModule, - HttpClientTestingModule - ], - declarations: [ - MockTranslatePipe, - ProgressTabComponent - ], + imports: [MaterialModule, FormsModule, HttpClientTestingModule], + declarations: [MockTranslatePipe, ProgressTabComponent], providers: [ UrlInterpolationService, LearnerDashboardBackendApiService, @@ -58,10 +55,10 @@ describe('Progress tab Component', () => { useValue: { isWindowNarrow: () => true, getResizeEvent: () => mockResizeEmitter, - } + }, }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -69,8 +66,9 @@ describe('Progress tab Component', () => { fixture = TestBed.createComponent(ProgressTabComponent); component = fixture.componentInstance; urlInterpolationService = TestBed.inject(UrlInterpolationService); - learnerDashboardBackendApiService = - TestBed.inject(LearnerDashboardBackendApiService); + learnerDashboardBackendApiService = TestBed.inject( + LearnerDashboardBackendApiService + ); windowDimensionsService = TestBed.inject(WindowDimensionsService); const sampleStorySummaryBackendDict = { id: '0', @@ -85,10 +83,11 @@ describe('Progress tab Component', () => { all_node_dicts: [], topic_name: 'Topic', classroom_url_fragment: 'math', - topic_url_fragment: 'topic' + topic_url_fragment: 'topic', }; let storySummary = StorySummary.createFromBackendDict( - sampleStorySummaryBackendDict); + sampleStorySummaryBackendDict + ); component.completedStoriesList = [storySummary]; let subtopic = { skill_ids: ['skill_id_2'], @@ -96,7 +95,7 @@ describe('Progress tab Component', () => { title: 'subtopic_name', thumbnail_filename: 'image.svg', thumbnail_bg_color: '#F8BF74', - url_fragment: 'subtopic-name' + url_fragment: 'subtopic-name', }; let nodeDict = { @@ -115,7 +114,7 @@ describe('Progress tab Component', () => { planned_publication_date_msecs: 100, last_modified_msecs: 100, first_publication_date_msecs: 200, - unpublishing_reason: null + unpublishing_reason: null, }; const learnerTopicSummaryBackendDict1 = { id: 'BqXdwH8YOsGX', @@ -129,35 +128,41 @@ describe('Progress tab Component', () => { thumbnail_bg_color: '#C6DCDA', classroom: 'math', practice_tab_is_displayed: true, - canonical_story_summary_dict: [{ - id: '0', - title: 'Story Title', - description: 'Story Description', - node_titles: ['Chapter 1'], - thumbnail_filename: 'image.svg', - thumbnail_bg_color: '#F8BF74', - story_is_published: true, - completed_node_titles: ['Chapter 1'], - url_fragment: 'story-title', - all_node_dicts: [nodeDict] - }], + canonical_story_summary_dict: [ + { + id: '0', + title: 'Story Title', + description: 'Story Description', + node_titles: ['Chapter 1'], + thumbnail_filename: 'image.svg', + thumbnail_bg_color: '#F8BF74', + story_is_published: true, + completed_node_titles: ['Chapter 1'], + url_fragment: 'story-title', + all_node_dicts: [nodeDict], + }, + ], url_fragment: 'topic-name', subtopics: [subtopic], degrees_of_mastery: { skill_id_1: 0.5, - skill_id_2: 0.3 + skill_id_2: 0.3, }, skill_descriptions: { skill_id_1: 'Skill Description 1', - skill_id_2: 'Skill Description 2' - } + skill_id_2: 'Skill Description 2', + }, }; - component.partiallyLearntTopicsList = - [LearnerTopicSummary.createFromBackendDict( - learnerTopicSummaryBackendDict1)]; + component.partiallyLearntTopicsList = [ + LearnerTopicSummary.createFromBackendDict( + learnerTopicSummaryBackendDict1 + ), + ]; component.learntTopicsList = []; - spyOn(learnerDashboardBackendApiService, 'fetchSubtopicMastery') - .and.returnValue(Promise.resolve({})); + spyOn( + learnerDashboardBackendApiService, + 'fetchSubtopicMastery' + ).and.returnValue(Promise.resolve({})); component.displaySkills = [false]; fixture.detectChanges(); }); @@ -182,8 +187,9 @@ describe('Progress tab Component', () => { it('should get static image url', () => { const urlSpy = spyOn( - urlInterpolationService, 'getStaticImageUrl') - .and.returnValue('/assets/images/learner_dashboard/star.svg'); + urlInterpolationService, + 'getStaticImageUrl' + ).and.returnValue('/assets/images/learner_dashboard/star.svg'); component.getStaticImageUrl('/learner_dashboard/star.svg'); fixture.detectChanges(); @@ -206,12 +212,12 @@ describe('Progress tab Component', () => { component.subtopicMastery = { BqXdwH8YOsGX: { 1: 1, - 2: 0 + 2: 0, }, QqXdwH8YOsGX: { 1: 0, - 2: 0 - } + 2: 0, + }, }; let subtopic = { skill_ids: ['skill_id_2'], @@ -219,7 +225,7 @@ describe('Progress tab Component', () => { title: 'subtopic_name', thumbnail_filename: 'image.svg', thumbnail_bg_color: '#F8BF74', - url_fragment: 'subtopic-name' + url_fragment: 'subtopic-name', }; let nodeDict = { id: 'node_1', @@ -237,7 +243,7 @@ describe('Progress tab Component', () => { planned_publication_date_msecs: 100, last_modified_msecs: 100, first_publication_date_msecs: 200, - unpublishing_reason: null + unpublishing_reason: null, }; const learnerTopicSummaryBackendDict = { id: 'BqXdwH8YOsGX', @@ -251,28 +257,30 @@ describe('Progress tab Component', () => { thumbnail_bg_color: '#C6DCDA', classroom: 'math', practice_tab_is_displayed: false, - canonical_story_summary_dict: [{ - id: '0', - title: 'Story Title', - description: 'Story Description', - node_titles: ['Chapter 1'], - thumbnail_filename: 'image.svg', - thumbnail_bg_color: '#F8BF74', - story_is_published: true, - completed_node_titles: ['Chapter 1'], - url_fragment: 'story-title', - all_node_dicts: [nodeDict] - }], + canonical_story_summary_dict: [ + { + id: '0', + title: 'Story Title', + description: 'Story Description', + node_titles: ['Chapter 1'], + thumbnail_filename: 'image.svg', + thumbnail_bg_color: '#F8BF74', + story_is_published: true, + completed_node_titles: ['Chapter 1'], + url_fragment: 'story-title', + all_node_dicts: [nodeDict], + }, + ], url_fragment: 'topic-name', subtopics: [subtopic], degrees_of_mastery: { skill_id_1: 0.5, - skill_id_2: 0.3 + skill_id_2: 0.3, }, skill_descriptions: { skill_id_1: 'Skill Description 1', - skill_id_2: 'Skill Description 2' - } + skill_id_2: 'Skill Description 2', + }, }; let subtopic1 = { skill_ids: ['skill_id_2'], @@ -280,7 +288,7 @@ describe('Progress tab Component', () => { title: 'subtopic_name', thumbnail_filename: 'image.svg', thumbnail_bg_color: '#F8BF74', - url_fragment: 'subtopic-name' + url_fragment: 'subtopic-name', }; const learnerTopicSummaryBackendDict1 = { id: 'QqXdwH8YOsGX', @@ -294,39 +302,42 @@ describe('Progress tab Component', () => { thumbnail_bg_color: '#C6DCDA', classroom: 'math', practice_tab_is_displayed: false, - canonical_story_summary_dict: [{ - id: '0', - title: 'Story Title', - description: 'Story Description', - node_titles: ['Chapter 1'], - thumbnail_filename: 'image.svg', - thumbnail_bg_color: '#F8BF74', - story_is_published: true, - completed_node_titles: ['Chapter 1'], - url_fragment: 'story-title', - all_node_dicts: [nodeDict] - }], + canonical_story_summary_dict: [ + { + id: '0', + title: 'Story Title', + description: 'Story Description', + node_titles: ['Chapter 1'], + thumbnail_filename: 'image.svg', + thumbnail_bg_color: '#F8BF74', + story_is_published: true, + completed_node_titles: ['Chapter 1'], + url_fragment: 'story-title', + all_node_dicts: [nodeDict], + }, + ], url_fragment: 'topic-name', subtopics: [subtopic1], degrees_of_mastery: { skill_id_1: 0, - skill_id_2: 0 + skill_id_2: 0, }, skill_descriptions: { skill_id_1: 'Skill Description 1', - skill_id_2: 'Skill Description 2' - } + skill_id_2: 'Skill Description 2', + }, }; component.topicsInSkillProficiency = [ + LearnerTopicSummary.createFromBackendDict(learnerTopicSummaryBackendDict), LearnerTopicSummary.createFromBackendDict( - learnerTopicSummaryBackendDict), - LearnerTopicSummary.createFromBackendDict( - learnerTopicSummaryBackendDict1) + learnerTopicSummaryBackendDict1 + ), ]; component.getTopicMastery(); - expect(component.topicMastery).toEqual( - [[100, component.topicsInSkillProficiency[0]], - [0, component.topicsInSkillProficiency[1]]]); + expect(component.topicMastery).toEqual([ + [100, component.topicsInSkillProficiency[0]], + [0, component.topicsInSkillProficiency[1]], + ]); }); it('should get circular progress', () => { @@ -336,7 +347,7 @@ describe('Progress tab Component', () => { title: 'subtopic_name', thumbnail_filename: 'image.svg', thumbnail_bg_color: '#F8BF74', - url_fragment: 'subtopic-name' + url_fragment: 'subtopic-name', }; let nodeDict = { id: 'node_1', @@ -354,7 +365,7 @@ describe('Progress tab Component', () => { planned_publication_date_msecs: 100, last_modified_msecs: 100, first_publication_date_msecs: 200, - unpublishing_reason: null + unpublishing_reason: null, }; const learnerTopicSummaryBackendDict = { id: 'BqXdwH8YOsGX', @@ -368,42 +379,46 @@ describe('Progress tab Component', () => { thumbnail_bg_color: '#C6DCDA', classroom: 'math', practice_tab_is_displayed: false, - canonical_story_summary_dict: [{ - id: '0', - title: 'Story Title', - description: 'Story Description', - node_titles: ['Chapter 1'], - thumbnail_filename: 'image.svg', - thumbnail_bg_color: '#F8BF74', - story_is_published: true, - completed_node_titles: ['Chapter 1'], - url_fragment: 'story-title', - all_node_dicts: [nodeDict] - }], + canonical_story_summary_dict: [ + { + id: '0', + title: 'Story Title', + description: 'Story Description', + node_titles: ['Chapter 1'], + thumbnail_filename: 'image.svg', + thumbnail_bg_color: '#F8BF74', + story_is_published: true, + completed_node_titles: ['Chapter 1'], + url_fragment: 'story-title', + all_node_dicts: [nodeDict], + }, + ], url_fragment: 'topic-name', subtopics: [subtopic], degrees_of_mastery: { skill_id_1: 0.5, - skill_id_2: 0.3 + skill_id_2: 0.3, }, skill_descriptions: { skill_id_1: 'Skill Description 1', - skill_id_2: 'Skill Description 2' - } + skill_id_2: 'Skill Description 2', + }, }; var topic = LearnerTopicSummary.createFromBackendDict( - learnerTopicSummaryBackendDict); + learnerTopicSummaryBackendDict + ); component.topicMastery = [[20, topic]]; var cssStyle = component.calculateCircularProgress(0); expect(cssStyle).toEqual( 'linear-gradient(162deg, transparent 50%, #CCCCCC 50%)' + - ', linear-gradient(90deg, #CCCCCC 50%, transparent 50%)'); + ', linear-gradient(90deg, #CCCCCC 50%, transparent 50%)' + ); component.topicMastery = [[60, topic]]; cssStyle = component.calculateCircularProgress(0); expect(cssStyle).toEqual( 'linear-gradient(270deg, #00645C 50%, transparent 50%), ' + - 'linear-gradient(-54deg, #00645C 50%, #CCCCCC 50%)' + 'linear-gradient(-54deg, #00645C 50%, #CCCCCC 50%)' ); }); }); diff --git a/core/templates/pages/learner-dashboard-page/progress-tab.component.ts b/core/templates/pages/learner-dashboard-page/progress-tab.component.ts index 428e19c6f5f6..dce2c8557196 100644 --- a/core/templates/pages/learner-dashboard-page/progress-tab.component.ts +++ b/core/templates/pages/learner-dashboard-page/progress-tab.component.ts @@ -16,21 +16,23 @@ * @fileoverview Component for progress tab in the Learner Dashboard page. */ -import { OnInit } from '@angular/core'; -import { Component, Input, EventEmitter, Output } from '@angular/core'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { StorySummary } from 'domain/story/story-summary.model'; -import { LearnerTopicSummary } from 'domain/topic/learner-topic-summary.model'; -import { LearnerDashboardPageConstants } from './learner-dashboard-page.constants'; -import { LearnerDashboardBackendApiService, SubtopicMasterySummaryBackendDict } from 'domain/learner_dashboard/learner-dashboard-backend-api.service'; -import { Subscription } from 'rxjs'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; +import {OnInit} from '@angular/core'; +import {Component, Input, EventEmitter, Output} from '@angular/core'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {StorySummary} from 'domain/story/story-summary.model'; +import {LearnerTopicSummary} from 'domain/topic/learner-topic-summary.model'; +import {LearnerDashboardPageConstants} from './learner-dashboard-page.constants'; +import { + LearnerDashboardBackendApiService, + SubtopicMasterySummaryBackendDict, +} from 'domain/learner_dashboard/learner-dashboard-backend-api.service'; +import {Subscription} from 'rxjs'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; - - @Component({ - selector: 'oppia-progress-tab', - templateUrl: './progress-tab.component.html' - }) +@Component({ + selector: 'oppia-progress-tab', + templateUrl: './progress-tab.component.html', +}) export class ProgressTabComponent implements OnInit { @Output() setActiveSection: EventEmitter = new EventEmitter(); // These properties are initialized using Angular lifecycle hooks @@ -52,8 +54,8 @@ export class ProgressTabComponent implements OnInit { silverBadgeImageUrl: string = ''; emptyBadgeImageUrl: string = ''; topicMastery: [number, LearnerTopicSummary][] = []; - LEARNER_DASHBOARD_SUBSECTION_I18N_IDS = ( - LearnerDashboardPageConstants.LEARNER_DASHBOARD_SUBSECTION_I18N_IDS); + LEARNER_DASHBOARD_SUBSECTION_I18N_IDS = + LearnerDashboardPageConstants.LEARNER_DASHBOARD_SUBSECTION_I18N_IDS; windowIsNarrow: boolean = false; directiveSubscriptions = new Subscription(); @@ -65,28 +67,36 @@ export class ProgressTabComponent implements OnInit { ) {} async ngOnInit(): Promise { - this.width = this.widthConst * (this.completedStoriesList.length); + this.width = this.widthConst * this.completedStoriesList.length; this.topicsInSkillProficiency.push( - ...this.partiallyLearntTopicsList, ...this.learntTopicsList); + ...this.partiallyLearntTopicsList, + ...this.learntTopicsList + ); let topic: LearnerTopicSummary; for (topic of this.topicsInSkillProficiency) { this.topicIdsInSkillProficiency.push(topic.id); } this.goldBadgeImageUrl = this.getStaticImageUrl( - '/learner_dashboard/gold.png'); + '/learner_dashboard/gold.png' + ); this.bronzeBadgeImageUrl = this.getStaticImageUrl( - '/learner_dashboard/bronze.png'); + '/learner_dashboard/bronze.png' + ); this.silverBadgeImageUrl = this.getStaticImageUrl( - '/learner_dashboard/silver.png'); + '/learner_dashboard/silver.png' + ); this.emptyBadgeImageUrl = this.getStaticImageUrl( - '/learner_dashboard/empty_badge.png'); + '/learner_dashboard/empty_badge.png' + ); if (this.topicsInSkillProficiency.length !== 0) { - this.subtopicMastery = await ( - this.learnerDashboardBackendApiService.fetchSubtopicMastery( - this.topicIdsInSkillProficiency)); + this.subtopicMastery = + await this.learnerDashboardBackendApiService.fetchSubtopicMastery( + this.topicIdsInSkillProficiency + ); } - this.displaySkills = new Array( - this.topicsInSkillProficiency.length).fill(false); + this.displaySkills = new Array(this.topicsInSkillProficiency.length).fill( + false + ); let atLeastOnetopicHasPracticeTabEnabled = false; for (topic of this.topicsInSkillProficiency) { if (topic.practiceTabIsDisplayed === true) { @@ -94,8 +104,10 @@ export class ProgressTabComponent implements OnInit { break; } } - if (atLeastOnetopicHasPracticeTabEnabled === true && - this.topicsInSkillProficiency.length !== 0) { + if ( + atLeastOnetopicHasPracticeTabEnabled === true && + this.topicsInSkillProficiency.length !== 0 + ) { this.emptySkillProficiency = false; } this.getTopicMastery(); @@ -104,12 +116,13 @@ export class ProgressTabComponent implements OnInit { this.directiveSubscriptions.add( this.windowDimensionService.getResizeEvent().subscribe(() => { this.windowIsNarrow = this.windowDimensionService.isWindowNarrow(); - })); + }) + ); } showSkills(index: number): void { this.displaySkills[index] = !this.displaySkills[index]; - this.width = this.widthConst * (this.completedStoriesList.length); + this.width = this.widthConst * this.completedStoriesList.length; } getStaticImageUrl(imagePath: string): string { @@ -119,36 +132,38 @@ export class ProgressTabComponent implements OnInit { getTopicMastery(): void { let keyArr = Object.keys(this.subtopicMastery); for (let i = 0; i < keyArr.length; i++) { - let valArr = Object.values(this.subtopicMastery[ - this.topicsInSkillProficiency[i].id]); + let valArr = Object.values( + this.subtopicMastery[this.topicsInSkillProficiency[i].id] + ); let sum = valArr.reduce((a, b) => a + b, 0); let arrLength = this.topicsInSkillProficiency[i].subtopics.length; - this.topicMastery.push( - [Math.floor(sum / arrLength * 100), - this.topicsInSkillProficiency[i]]); + this.topicMastery.push([ + Math.floor((sum / arrLength) * 100), + this.topicsInSkillProficiency[i], + ]); } - this.topicMastery = this.topicMastery.sort( - function(a, b) { - return b[0] - a[0]; - }); + this.topicMastery = this.topicMastery.sort(function (a, b) { + return b[0] - a[0]; + }); } calculateCircularProgress(i: number): string { - let degree = (90 + (360 * (this.topicMastery[i][0])) / 100); - let cssStyle = ( + let degree = 90 + (360 * this.topicMastery[i][0]) / 100; + let cssStyle = `linear-gradient(${degree}deg, transparent 50%, #CCCCCC 50%)` + - ', linear-gradient(90deg, #CCCCCC 50%, transparent 50%)'); + ', linear-gradient(90deg, #CCCCCC 50%, transparent 50%)'; if (this.topicMastery[i][0] > 50) { degree = 3.6 * (this.topicMastery[i][0] - 50) - 90; - cssStyle = ( + cssStyle = 'linear-gradient(270deg, #00645C 50%, transparent 50%), ' + - `linear-gradient(${degree}deg, #00645C 50%, #CCCCCC 50%)`); + `linear-gradient(${degree}deg, #00645C 50%, #CCCCCC 50%)`; } return cssStyle; } changeActiveSection(): void { this.setActiveSection.emit( - LearnerDashboardPageConstants.LEARNER_DASHBOARD_SECTION_I18N_IDS.GOALS); + LearnerDashboardPageConstants.LEARNER_DASHBOARD_SECTION_I18N_IDS.GOALS + ); } } diff --git a/core/templates/pages/learner-dashboard-page/suggestion-modal/learner-dashboard-suggestion-modal.component.spec.ts b/core/templates/pages/learner-dashboard-page/suggestion-modal/learner-dashboard-suggestion-modal.component.spec.ts index ac43403f133f..1a2058b5cea2 100644 --- a/core/templates/pages/learner-dashboard-page/suggestion-modal/learner-dashboard-suggestion-modal.component.spec.ts +++ b/core/templates/pages/learner-dashboard-page/suggestion-modal/learner-dashboard-suggestion-modal.component.spec.ts @@ -12,17 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Unit tests for LearnerDashboardSuggestionModalComponent. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; -import { LearnerDashboardSuggestionModalComponent } from './learner-dashboard-suggestion-modal.component'; +import {LearnerDashboardSuggestionModalComponent} from './learner-dashboard-suggestion-modal.component'; class MockActiveModal { close(): void { @@ -34,7 +33,6 @@ class MockActiveModal { } } - describe('Learner Dashboard Suggestion Modal Component', () => { let description = 'This is a description string'; let newContent = 'new content'; @@ -52,10 +50,11 @@ describe('Learner Dashboard Suggestion Modal Component', () => { ], providers: [ { - provide: NgbActiveModal, useClass: MockActiveModal + provide: NgbActiveModal, + useClass: MockActiveModal, }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); diff --git a/core/templates/pages/learner-dashboard-page/suggestion-modal/learner-dashboard-suggestion-modal.component.ts b/core/templates/pages/learner-dashboard-page/suggestion-modal/learner-dashboard-suggestion-modal.component.ts index e4a1e78bb65b..bdb1a9385082 100644 --- a/core/templates/pages/learner-dashboard-page/suggestion-modal/learner-dashboard-suggestion-modal.component.ts +++ b/core/templates/pages/learner-dashboard-page/suggestion-modal/learner-dashboard-suggestion-modal.component.ts @@ -16,18 +16,17 @@ * @fileoverview Component for learner dashboard suggestion modal. */ -import { Component, Input } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import {Component, Input} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-learner-dashboard-suggestion-modal', templateUrl: './learner-dashboard-suggestion-modal.component.html', - styleUrls: [] + styleUrls: [], }) -export class LearnerDashboardSuggestionModalComponent - extends ConfirmOrCancelModal { +export class LearnerDashboardSuggestionModalComponent extends ConfirmOrCancelModal { // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -35,9 +34,7 @@ export class LearnerDashboardSuggestionModalComponent @Input() oldContent!: string; @Input() description!: string; - constructor( - private activeModal: NgbActiveModal, - ) { + constructor(private activeModal: NgbActiveModal) { super(activeModal); } diff --git a/core/templates/pages/learner-dashboard-page/suggestion-modal/suggestion-modal-for-learner-dashboard.service.spec.ts b/core/templates/pages/learner-dashboard-page/suggestion-modal/suggestion-modal-for-learner-dashboard.service.spec.ts index 3bc994ef3925..d50d7f0c46ec 100644 --- a/core/templates/pages/learner-dashboard-page/suggestion-modal/suggestion-modal-for-learner-dashboard.service.spec.ts +++ b/core/templates/pages/learner-dashboard-page/suggestion-modal/suggestion-modal-for-learner-dashboard.service.spec.ts @@ -16,118 +16,133 @@ * @fileoverview Tests for SuggestionModalForLearnerDashboardService. */ -import { async, TestBed } from '@angular/core/testing'; -import { CsrfTokenService } from 'services/csrf-token.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {async, TestBed} from '@angular/core/testing'; +import {CsrfTokenService} from 'services/csrf-token.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; -import { SuggestionModalForLearnerDashboardService } from './suggestion-modal-for-learner-dashboard.service'; +import {SuggestionModalForLearnerDashboardService} from './suggestion-modal-for-learner-dashboard.service'; class MockNgbModalRef { componentInstance = { newContent: null, oldContent: null, - description: null + description: null, }; } describe('Suggestion Modal Service For Learners Dashboard', () => { - let suggestionModalForLearnerDashboardService: - SuggestionModalForLearnerDashboardService; + let suggestionModalForLearnerDashboardService: SuggestionModalForLearnerDashboardService; let csrfService: CsrfTokenService; let ngbModal: NgbModal; beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] + imports: [HttpClientTestingModule], }); })); beforeEach(() => { - suggestionModalForLearnerDashboardService = - TestBed.inject(SuggestionModalForLearnerDashboardService); + suggestionModalForLearnerDashboardService = TestBed.inject( + SuggestionModalForLearnerDashboardService + ); ngbModal = TestBed.inject(NgbModal); csrfService = TestBed.inject(CsrfTokenService); - spyOn(csrfService, 'getTokenAsync').and.callFake(async() => { - return new Promise((resolve) => { + spyOn(csrfService, 'getTokenAsync').and.callFake(async () => { + return new Promise(resolve => { resolve('sample-csrf-token'); }); }); }); - it('should open an ngbModal for suggestions requested' + - ' when calling showSuggestionModal', () => { - const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - if (opt?.beforeDismiss !== undefined) { - setTimeout(opt.beforeDismiss); - } - return ( - { componentInstance: MockNgbModalRef, - result: Promise.resolve('success') - }) as NgbModalRef; - }); - - let suggestionType = 'edit_exploration_state_content'; - let extraParams = { - newContent: 'new', - oldContent: 'old', - description: 'description' - }; - - suggestionModalForLearnerDashboardService.showSuggestionModal( - suggestionType, extraParams); - - expect(modalSpy).toHaveBeenCalled(); - }); - - it('should not open an ngbModal for suggestions requested' + - ' when calling showSuggestionModal', () => { - const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - if (opt?.beforeDismiss !== undefined) { - setTimeout(opt.beforeDismiss); - } - return ( - { componentInstance: MockNgbModalRef, - result: Promise.resolve('success') - }) as NgbModalRef; - }); - - let suggestionType = 'invalidType'; - let extraParams = { - newContent: 'new', - oldContent: 'old', - description: 'description' - }; - - suggestionModalForLearnerDashboardService.showSuggestionModal( - suggestionType, extraParams); - - expect(modalSpy).not.toHaveBeenCalled(); - }); - - it('should do nothing when cancel button is clicked' + - ' when calling showSuggestionModal', () => { - const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - if (opt?.beforeDismiss !== undefined) { - setTimeout(opt.beforeDismiss); - } - return ( - { componentInstance: MockNgbModalRef, - result: Promise.reject('cancel') - }) as NgbModalRef; - }); + it( + 'should open an ngbModal for suggestions requested' + + ' when calling showSuggestionModal', + () => { + const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + if (opt?.beforeDismiss !== undefined) { + setTimeout(opt.beforeDismiss); + } + return { + componentInstance: MockNgbModalRef, + result: Promise.resolve('success'), + } as NgbModalRef; + }); - let suggestionType = 'edit_exploration_state_content'; - let extraParams = { - newContent: 'new', - oldContent: 'old', - description: 'description' - }; + let suggestionType = 'edit_exploration_state_content'; + let extraParams = { + newContent: 'new', + oldContent: 'old', + description: 'description', + }; + + suggestionModalForLearnerDashboardService.showSuggestionModal( + suggestionType, + extraParams + ); + + expect(modalSpy).toHaveBeenCalled(); + } + ); + + it( + 'should not open an ngbModal for suggestions requested' + + ' when calling showSuggestionModal', + () => { + const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + if (opt?.beforeDismiss !== undefined) { + setTimeout(opt.beforeDismiss); + } + return { + componentInstance: MockNgbModalRef, + result: Promise.resolve('success'), + } as NgbModalRef; + }); - suggestionModalForLearnerDashboardService.showSuggestionModal( - suggestionType, extraParams); + let suggestionType = 'invalidType'; + let extraParams = { + newContent: 'new', + oldContent: 'old', + description: 'description', + }; + + suggestionModalForLearnerDashboardService.showSuggestionModal( + suggestionType, + extraParams + ); + + expect(modalSpy).not.toHaveBeenCalled(); + } + ); + + it( + 'should do nothing when cancel button is clicked' + + ' when calling showSuggestionModal', + () => { + const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + if (opt?.beforeDismiss !== undefined) { + setTimeout(opt.beforeDismiss); + } + return { + componentInstance: MockNgbModalRef, + result: Promise.reject('cancel'), + } as NgbModalRef; + }); - expect(modalSpy).toHaveBeenCalled(); - }); + let suggestionType = 'edit_exploration_state_content'; + let extraParams = { + newContent: 'new', + oldContent: 'old', + description: 'description', + }; + + suggestionModalForLearnerDashboardService.showSuggestionModal( + suggestionType, + extraParams + ); + + expect(modalSpy).toHaveBeenCalled(); + } + ); }); diff --git a/core/templates/pages/learner-dashboard-page/suggestion-modal/suggestion-modal-for-learner-dashboard.service.ts b/core/templates/pages/learner-dashboard-page/suggestion-modal/suggestion-modal-for-learner-dashboard.service.ts index 9ef471518557..95a8b087bbbd 100644 --- a/core/templates/pages/learner-dashboard-page/suggestion-modal/suggestion-modal-for-learner-dashboard.service.ts +++ b/core/templates/pages/learner-dashboard-page/suggestion-modal/suggestion-modal-for-learner-dashboard.service.ts @@ -16,36 +16,42 @@ * @fileoverview Service to display suggestion modal in learner view. */ -import { Injectable } from '@angular/core'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import {Injectable} from '@angular/core'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; -import { LearnerDashboardSuggestionModalComponent } from './learner-dashboard-suggestion-modal.component'; +import {LearnerDashboardSuggestionModalComponent} from './learner-dashboard-suggestion-modal.component'; interface ExtraParams { - 'newContent': string; - 'oldContent': string; - 'description': string; + newContent: string; + oldContent: string; + description: string; } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class SuggestionModalForLearnerDashboardService { - constructor( - private ngbModal: NgbModal, - ) {} + constructor(private ngbModal: NgbModal) {} private _showEditStateContentSuggestionModal( - newContent: string, oldContent: string, description: string): void { + newContent: string, + oldContent: string, + description: string + ): void { const modelRef = this.ngbModal.open( - LearnerDashboardSuggestionModalComponent, {backdrop: true}); + LearnerDashboardSuggestionModalComponent, + {backdrop: true} + ); modelRef.componentInstance.newContent = newContent; modelRef.componentInstance.oldContent = oldContent; modelRef.componentInstance.description = description; - modelRef.result.then(() => {}, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + modelRef.result.then( + () => {}, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } showSuggestionModal(suggestionType: string, extraParams: ExtraParams): void { diff --git a/core/templates/pages/learner-group-pages/create-group/add-syllabus-items.component.spec.ts b/core/templates/pages/learner-group-pages/create-group/add-syllabus-items.component.spec.ts index 570c2eaedc28..66c85f757cc1 100644 --- a/core/templates/pages/learner-group-pages/create-group/add-syllabus-items.component.spec.ts +++ b/core/templates/pages/learner-group-pages/create-group/add-syllabus-items.component.spec.ts @@ -16,26 +16,29 @@ * @fileoverview Unit tests for adding syllabus items to learner group. */ -import { EventEmitter, NO_ERRORS_SCHEMA, Pipe } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { AddSyllabusItemsComponent } from './add-syllabus-items.component'; -import { NavigationService } from 'services/navigation.service'; -import { TranslateService } from '@ngx-translate/core'; -import { WindowDimensionsService } from - 'services/contextual/window-dimensions.service'; -import { LearnerGroupSyllabusBackendApiService, SyllabusSelectionDetails } from - 'domain/learner_group/learner-group-syllabus-backend-api.service'; -import { StorySummary } from 'domain/story/story-summary.model'; -import { LearnerGroupSubtopicSummary } from - 'domain/learner_group/learner-group-subtopic-summary.model'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; -import { ConstructTranslationIdsService } from - 'services/construct-translation-ids.service'; -import { LearnerGroupSyllabus } from - 'domain/learner_group/learner-group-syllabus.model'; -import { LanguageUtilService } from 'domain/utilities/language-util.service'; +import {EventEmitter, NO_ERRORS_SCHEMA, Pipe} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {AddSyllabusItemsComponent} from './add-syllabus-items.component'; +import {NavigationService} from 'services/navigation.service'; +import {TranslateService} from '@ngx-translate/core'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import { + LearnerGroupSyllabusBackendApiService, + SyllabusSelectionDetails, +} from 'domain/learner_group/learner-group-syllabus-backend-api.service'; +import {StorySummary} from 'domain/story/story-summary.model'; +import {LearnerGroupSubtopicSummary} from 'domain/learner_group/learner-group-subtopic-summary.model'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; +import {ConstructTranslationIdsService} from 'services/construct-translation-ids.service'; +import {LearnerGroupSyllabus} from 'domain/learner_group/learner-group-syllabus.model'; +import {LanguageUtilService} from 'domain/utilities/language-util.service'; @Pipe({name: 'truncate'}) class MockTrunctePipe { @@ -73,8 +76,7 @@ describe('AddSyllabusItemsComponent', () => { let windowDimensionsService: WindowDimensionsService; let assetsBackendApiService: AssetsBackendApiService; let constructTranslationIdsService: ConstructTranslationIdsService; - let learnerGroupSyllabusBackendApiService: - LearnerGroupSyllabusBackendApiService; + let learnerGroupSyllabusBackendApiService: LearnerGroupSyllabusBackendApiService; let languageUtilService: LanguageUtilService; let selectionDetailsStub: SyllabusSelectionDetails; @@ -85,11 +87,12 @@ describe('AddSyllabusItemsComponent', () => { parent_topic_name: 'parentTopicName', thumbnail_filename: 'thumbnailFilename', thumbnail_bg_color: 'red', - subtopic_mastery: 0.5 + subtopic_mastery: 0.5, }; - const sampleLearnerGroupSubtopicSummary = ( + const sampleLearnerGroupSubtopicSummary = LearnerGroupSubtopicSummary.createFromBackendDict( - sampleSubtopicSummaryDict)); + sampleSubtopicSummaryDict + ); const sampleStorySummaryBackendDict = { id: 'story_id_0', @@ -104,18 +107,20 @@ describe('AddSyllabusItemsComponent', () => { all_node_dicts: [], topic_name: 'Topic', classroom_url_fragment: 'math', - topic_url_fragment: 'topic' + topic_url_fragment: 'topic', }; const sampleStorySummary = StorySummary.createFromBackendDict( - sampleStorySummaryBackendDict); + sampleStorySummaryBackendDict + ); const mockSyllabusItemsBackendDict = { learner_group_id: 'groupId', story_summary_dicts: [sampleStorySummaryBackendDict], - subtopic_summary_dicts: [sampleSubtopicSummaryDict] + subtopic_summary_dicts: [sampleSubtopicSummaryDict], }; const mockSyllabusItems = LearnerGroupSyllabus.createFromBackendDict( - mockSyllabusItemsBackendDict); + mockSyllabusItemsBackendDict + ); beforeEach(() => { TestBed.configureTestingModule({ @@ -123,23 +128,23 @@ describe('AddSyllabusItemsComponent', () => { declarations: [ AddSyllabusItemsComponent, MockTranslatePipe, - MockTrunctePipe + MockTrunctePipe, ], providers: [ { provide: TranslateService, - useClass: MockTranslateService + useClass: MockTranslateService, }, { provide: NavigationService, - useClass: MockNavigationService + useClass: MockNavigationService, }, { provide: WindowDimensionsService, - useClass: MockWindowDimensionsService - } + useClass: MockWindowDimensionsService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); @@ -151,16 +156,16 @@ describe('AddSyllabusItemsComponent', () => { masterList: [ { id: 'skill', - text: 'Skill' + text: 'Skill', }, { id: 'story', - text: 'Story' - } + text: 'Story', + }, ], selection: 'skill', defaultValue: 'All', - summary: 'Type' + summary: 'Type', }, categories: { description: 'Maths', @@ -168,16 +173,16 @@ describe('AddSyllabusItemsComponent', () => { masterList: [ { id: 'math', - text: 'Maths' + text: 'Maths', }, { id: 'science', - text: 'Science' - } + text: 'Science', + }, ], selection: 'math', defaultValue: 'All', - summary: 'Category' + summary: 'Category', }, languageCodes: { description: 'English', @@ -185,17 +190,17 @@ describe('AddSyllabusItemsComponent', () => { masterList: [ { id: 'en', - text: 'English' + text: 'English', }, { id: 'es', - text: 'Spanish' - } + text: 'Spanish', + }, ], selection: 'en', defaultValue: 'All', - summary: 'Language' - } + summary: 'Language', + }, }; navigationService = TestBed.inject(NavigationService); @@ -203,9 +208,11 @@ describe('AddSyllabusItemsComponent', () => { windowDimensionsService = TestBed.inject(WindowDimensionsService); assetsBackendApiService = TestBed.inject(AssetsBackendApiService); constructTranslationIdsService = TestBed.inject( - ConstructTranslationIdsService); + ConstructTranslationIdsService + ); learnerGroupSyllabusBackendApiService = TestBed.inject( - LearnerGroupSyllabusBackendApiService); + LearnerGroupSyllabusBackendApiService + ); languageUtilService = TestBed.inject(LanguageUtilService); fixture = TestBed.createComponent(AddSyllabusItemsComponent); component = fixture.componentInstance; @@ -213,8 +220,10 @@ describe('AddSyllabusItemsComponent', () => { }); it('should determine if mobile view is active', () => { - const windowWidthSpy = spyOn(windowDimensionsService, 'getWidth') - .and.returnValue(766); + const windowWidthSpy = spyOn( + windowDimensionsService, + 'getWidth' + ).and.returnValue(766); expect(component.isMobileViewActive()).toBe(true); windowWidthSpy.and.returnValue(1000); @@ -228,7 +237,9 @@ describe('AddSyllabusItemsComponent', () => { component.openSubmenu(clickEvent, 'category'); expect(navigationService.openSubmenu).toHaveBeenCalledWith( - clickEvent, 'category'); + clickEvent, + 'category' + ); }); it('should toggle selection', () => { @@ -250,12 +261,14 @@ describe('AddSyllabusItemsComponent', () => { it('should update selection details if a language is selected', () => { component.ngOnInit(); expect(component.selectionDetails.languageCodes.description).toEqual( - 'Language'); + 'Language' + ); component.selectionDetails = selectionDetailsStub; spyOn(translateService, 'instant').and.returnValue('English'); component.updateSelectionDetails('languageCodes'); expect(component.selectionDetails.languageCodes.description).toEqual( - 'English'); + 'English' + ); }); it('should refresh search bar labels', () => { @@ -281,15 +294,15 @@ describe('AddSyllabusItemsComponent', () => { expect(component.isSearchInProgress()).toBe(true); }); - it ('should search', fakeAsync(() => { + it('should search', fakeAsync(() => { component.debounceTimeout = 50; component.searchQuery = 'hello'; component.ngOnInit(); const search = { target: { - value: 'search' - } + value: 'search', + }, }; spyOn(component, 'onSearchQueryChangeExec'); @@ -299,34 +312,33 @@ describe('AddSyllabusItemsComponent', () => { expect(component.onSearchQueryChangeExec).toHaveBeenCalled(); })); - it('should execute search when search query is changed', - fakeAsync(() => { - spyOn( - learnerGroupSyllabusBackendApiService, 'searchNewSyllabusItemsAsync' - ).and.returnValue(Promise.resolve(mockSyllabusItems)); - component.ngOnInit(); - component.selectionDetails = selectionDetailsStub; - component.selectionDetails.languageCodes.selection = ''; - component.selectionDetails.categories.selection = ''; - component.selectionDetails.types.selection = ''; - component.onSearchQueryChangeExec(); - tick(); - fixture.detectChanges(); - - expect(component.storySummaries).toEqual([]); - expect(component.subtopicSummaries).toEqual([]); - - component.searchQuery = 'dummy topic'; - component.onSearchQueryChangeExec(); - tick(); - fixture.detectChanges(); - - expect(component.storySummaries).toEqual( - mockSyllabusItems.storySummaries); - expect(component.subtopicSummaries).toEqual( - mockSyllabusItems.subtopicPageSummaries); - }) - ); + it('should execute search when search query is changed', fakeAsync(() => { + spyOn( + learnerGroupSyllabusBackendApiService, + 'searchNewSyllabusItemsAsync' + ).and.returnValue(Promise.resolve(mockSyllabusItems)); + component.ngOnInit(); + component.selectionDetails = selectionDetailsStub; + component.selectionDetails.languageCodes.selection = ''; + component.selectionDetails.categories.selection = ''; + component.selectionDetails.types.selection = ''; + component.onSearchQueryChangeExec(); + tick(); + fixture.detectChanges(); + + expect(component.storySummaries).toEqual([]); + expect(component.subtopicSummaries).toEqual([]); + + component.searchQuery = 'dummy topic'; + component.onSearchQueryChangeExec(); + tick(); + fixture.detectChanges(); + + expect(component.storySummaries).toEqual(mockSyllabusItems.storySummaries); + expect(component.subtopicSummaries).toEqual( + mockSyllabusItems.subtopicPageSummaries + ); + })); it('should initialize', fakeAsync(() => { spyOn(component, 'searchDropdownCategories').and.returnValue([]); @@ -346,19 +358,23 @@ describe('AddSyllabusItemsComponent', () => { expect(translateService.onLangChange.subscribe).toHaveBeenCalled(); })); - it('should call refresh search bar labels whenever the language is ' + - 'changed', () => { - component.ngOnInit(); - spyOn(component, 'refreshSearchBarLabels'); + it( + 'should call refresh search bar labels whenever the language is ' + + 'changed', + () => { + component.ngOnInit(); + spyOn(component, 'refreshSearchBarLabels'); - translateService.onLangChange.emit(); + translateService.onLangChange.emit(); - expect(component.refreshSearchBarLabels).toHaveBeenCalled(); - }); + expect(component.refreshSearchBarLabels).toHaveBeenCalled(); + } + ); it('should get subtopic thumbnail url', () => { - spyOn(assetsBackendApiService, 'getThumbnailUrlForPreview') - .and.returnValue('/topic/thumbnail/url'); + spyOn(assetsBackendApiService, 'getThumbnailUrlForPreview').and.returnValue( + '/topic/thumbnail/url' + ); expect( component.getSubtopicThumbnailUrl(sampleLearnerGroupSubtopicSummary) @@ -366,11 +382,13 @@ describe('AddSyllabusItemsComponent', () => { }); it('should get story thumbnail url', () => { - spyOn(assetsBackendApiService, 'getThumbnailUrlForPreview') - .and.returnValue('/story/thumbnail/url'); + spyOn(assetsBackendApiService, 'getThumbnailUrlForPreview').and.returnValue( + '/story/thumbnail/url' + ); expect(component.getStoryThumbnailUrl(sampleStorySummary)).toEqual( - '/story/thumbnail/url'); + '/story/thumbnail/url' + ); }); it('should add story to syllabus successfully', () => { @@ -394,8 +412,9 @@ describe('AddSyllabusItemsComponent', () => { component.addSubtopicToSyllabus(sampleLearnerGroupSubtopicSummary); expect(component.syllabusSubtopicPageIds).toEqual(['topicId1:1']); - expect(component.syllabusSubtopicSummaries).toEqual( - [sampleLearnerGroupSubtopicSummary]); + expect(component.syllabusSubtopicSummaries).toEqual([ + sampleLearnerGroupSubtopicSummary, + ]); }); it('should remove story from syllabus successfully', () => { diff --git a/core/templates/pages/learner-group-pages/create-group/add-syllabus-items.component.ts b/core/templates/pages/learner-group-pages/create-group/add-syllabus-items.component.ts index 057545ac1b5c..f1233d0cb331 100644 --- a/core/templates/pages/learner-group-pages/create-group/add-syllabus-items.component.ts +++ b/core/templates/pages/learner-group-pages/create-group/add-syllabus-items.component.ts @@ -16,33 +16,38 @@ * @fileoverview Component for adding syllabus items to the learner group. */ -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from - '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { TranslateService } from '@ngx-translate/core'; -import { Subject, Subscription } from 'rxjs'; - -import { WindowDimensionsService } from - 'services/contextual/window-dimensions.service'; -import { LanguageIdAndText, LanguageUtilService } from - 'domain/utilities/language-util.service'; -import { NavigationService } from 'services/navigation.service'; -import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; -import { ConstructTranslationIdsService } from - 'services/construct-translation-ids.service'; +import { + Component, + EventEmitter, + Input, + OnDestroy, + OnInit, + Output, +} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {TranslateService} from '@ngx-translate/core'; +import {Subject, Subscription} from 'rxjs'; + +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import { + LanguageIdAndText, + LanguageUtilService, +} from 'domain/utilities/language-util.service'; +import {NavigationService} from 'services/navigation.service'; +import {debounceTime, distinctUntilChanged} from 'rxjs/operators'; +import {ConstructTranslationIdsService} from 'services/construct-translation-ids.service'; import { LearnerGroupSyllabusFilter, LearnerGroupSyllabusBackendApiService, - SyllabusSelectionDetails + SyllabusSelectionDetails, } from 'domain/learner_group/learner-group-syllabus-backend-api.service'; -import { StorySummary } from 'domain/story/story-summary.model'; -import { LearnerGroupSubtopicSummary } from - 'domain/learner_group/learner-group-subtopic-summary.model'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; -import { AppConstants } from 'app.constants'; +import {StorySummary} from 'domain/story/story-summary.model'; +import {LearnerGroupSubtopicSummary} from 'domain/learner_group/learner-group-subtopic-summary.model'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; +import {AppConstants} from 'app.constants'; import './add-syllabus-items.component.css'; -import { LearnerGroupData } from 'domain/learner_group/learner-group.model'; +import {LearnerGroupData} from 'domain/learner_group/learner-group.model'; interface SearchDropDownItems { id: string; @@ -52,7 +57,7 @@ interface SearchDropDownItems { @Component({ selector: 'oppia-add-syllabus-items', templateUrl: './add-syllabus-items.component.html', - styleUrls: ['./add-syllabus-items.component.css'] + styleUrls: ['./add-syllabus-items.component.css'], }) export class AddSyllabusItemsComponent implements OnInit, OnDestroy { @Input() learnerGroupId: string = ''; @@ -61,17 +66,18 @@ export class AddSyllabusItemsComponent implements OnInit, OnDestroy { @Input() syllabusSubtopicSummaries: LearnerGroupSubtopicSummary[] = []; @Input() syllabusStoryIds: string[] = []; @Input() syllabusSubtopicPageIds: string[] = []; - @Output() updateLearnerGroupStoryIds: - EventEmitter = new EventEmitter(); + @Output() updateLearnerGroupStoryIds: EventEmitter = + new EventEmitter(); - @Output() updateLearnerGroupSubtopicIds: - EventEmitter = new EventEmitter(); + @Output() updateLearnerGroupSubtopicIds: EventEmitter = + new EventEmitter(); - @Output() updateLearnerGroupStories: - EventEmitter = new EventEmitter(); + @Output() updateLearnerGroupStories: EventEmitter = + new EventEmitter(); - @Output() updateLearnerGroupSubtopics: - EventEmitter = new EventEmitter(); + @Output() updateLearnerGroupSubtopics: EventEmitter< + LearnerGroupSubtopicSummary[] + > = new EventEmitter(); enableDropup = false; storySummaries: StorySummary[] = []; @@ -100,8 +106,7 @@ export class AddSyllabusItemsComponent implements OnInit, OnDestroy { private navigationService: NavigationService, private languageUtilService: LanguageUtilService, private constructTranslationIdsService: ConstructTranslationIdsService, - private learnerGroupSyllabusBackendApiService: - LearnerGroupSyllabusBackendApiService, + private learnerGroupSyllabusBackendApiService: LearnerGroupSyllabusBackendApiService, private assetsBackendApiService: AssetsBackendApiService ) {} @@ -113,8 +118,8 @@ export class AddSyllabusItemsComponent implements OnInit, OnDestroy { this.SEARCH_DROPDOWN_CATEGORIES = this.searchDropdownCategories(); this.ACTION_OPEN = this.navigationService.ACTION_OPEN; this.ACTION_CLOSE = this.navigationService.ACTION_CLOSE; - this.SUPPORTED_CONTENT_LANGUAGES = ( - this.languageUtilService.getLanguageIdsAndTexts()); + this.SUPPORTED_CONTENT_LANGUAGES = + this.languageUtilService.getLanguageIdsAndTexts(); this.SEARCH_DROPDOWN_TYPES = this.searchDropdownTypes(); this.selectionDetails = { types: { @@ -123,7 +128,7 @@ export class AddSyllabusItemsComponent implements OnInit, OnDestroy { masterList: this.SEARCH_DROPDOWN_TYPES, selection: '', defaultValue: 'All', - summary: 'Type' + summary: 'Type', }, categories: { description: '', @@ -131,7 +136,7 @@ export class AddSyllabusItemsComponent implements OnInit, OnDestroy { masterList: this.SEARCH_DROPDOWN_CATEGORIES, selection: '', defaultValue: 'All', - summary: 'Category' + summary: 'Category', }, languageCodes: { description: '', @@ -139,8 +144,8 @@ export class AddSyllabusItemsComponent implements OnInit, OnDestroy { masterList: this.SUPPORTED_CONTENT_LANGUAGES, selection: '', defaultValue: 'All', - summary: 'Language' - } + summary: 'Language', + }, }; // Initialize the selection descriptions and summaries. for (let itemsType in this.selectionDetails) { @@ -158,8 +163,10 @@ export class AddSyllabusItemsComponent implements OnInit, OnDestroy { }); this.directiveSubscriptions.add( - this.translateService.onLangChange - .subscribe(() => this.refreshSearchBarLabels())); + this.translateService.onLangChange.subscribe(() => + this.refreshSearchBarLabels() + ) + ); } // Update the description field of the relevant entry of selectionDetails. @@ -174,31 +181,32 @@ export class AddSyllabusItemsComponent implements OnInit, OnDestroy { } if (selectedItemText.length > 0) { - this.selectionDetails[itemsType].description = ( - this.translateService.instant(selectedItemText)); + this.selectionDetails[itemsType].description = + this.translateService.instant(selectedItemText); } else { - this.selectionDetails[itemsType].description = ( - this.selectionDetails[itemsType].summary - ); + this.selectionDetails[itemsType].description = + this.selectionDetails[itemsType].summary; } } searchDropdownCategories(): SearchDropDownItems[] { - return AppConstants.SEARCH_DROPDOWN_CLASSROOMS.map((classroomName) => { + return AppConstants.SEARCH_DROPDOWN_CLASSROOMS.map(classroomName => { return { id: classroomName, text: this.constructTranslationIdsService.getClassroomTitleId( - classroomName) + classroomName + ), }; }); } searchDropdownTypes(): SearchDropDownItems[] { - return AppConstants.SEARCH_DROPDOWN_TYPES.map((typeName) => { + return AppConstants.SEARCH_DROPDOWN_TYPES.map(typeName => { return { id: typeName, text: this.constructTranslationIdsService.getSyllabusTypeTitleId( - typeName) + typeName + ), }; }); } @@ -223,18 +231,22 @@ export class AddSyllabusItemsComponent implements OnInit, OnDestroy { // exception, we translate them here and update the translation // every time the language is changed. this.searchBarPlaceholder = this.translateService.instant( - 'I18N_ADD_SYLLABUS_SEARCH_PLACEHOLDER'); + 'I18N_ADD_SYLLABUS_SEARCH_PLACEHOLDER' + ); // 'messageformat' is the interpolation method for plural forms. // http://angular-translate.github.io/docs/#/guide/14_pluralization. this.categoryButtonText = this.translateService.instant( this.selectionDetails.categories.description, - {...this.translationData, messageFormat: true}); + {...this.translationData, messageFormat: true} + ); this.languageButtonText = this.translateService.instant( this.selectionDetails.languageCodes.description, - {...this.translationData, messageFormat: true}); + {...this.translationData, messageFormat: true} + ); this.typeButtonText = this.translateService.instant( this.selectionDetails.types.description, - {...this.translationData, messageFormat: true}); + {...this.translationData, messageFormat: true} + ); } /** @@ -252,27 +264,24 @@ export class AddSyllabusItemsComponent implements OnInit, OnDestroy { this.syllabusFilter = { learnerGroupId: this.learnerGroupId, keyword: this.searchQuery, - type: ( + type: this.selectionDetails.types.selection || - this.selectionDetails.types.defaultValue - ), - category: ( + this.selectionDetails.types.defaultValue, + category: this.selectionDetails.categories.selection || - this.selectionDetails.categories.defaultValue - ), - languageCode: ( + this.selectionDetails.categories.defaultValue, + languageCode: this.selectionDetails.languageCodes.selection || - this.selectionDetails.languageCodes.defaultValue - ) + this.selectionDetails.languageCodes.defaultValue, }; if (this.isValidSearch()) { - this.learnerGroupSyllabusBackendApiService.searchNewSyllabusItemsAsync( - this.syllabusFilter - ).then((syllabusItems) => { - this.searchIsInProgress = false; - this.storySummaries = syllabusItems.storySummaries; - this.subtopicSummaries = syllabusItems.subtopicPageSummaries; - }); + this.learnerGroupSyllabusBackendApiService + .searchNewSyllabusItemsAsync(this.syllabusFilter) + .then(syllabusItems => { + this.searchIsInProgress = false; + this.storySummaries = syllabusItems.storySummaries; + this.subtopicSummaries = syllabusItems.subtopicPageSummaries; + }); } else { this.searchIsInProgress = false; this.storySummaries = []; @@ -298,15 +307,14 @@ export class AddSyllabusItemsComponent implements OnInit, OnDestroy { } getSubtopicThumbnailUrl( - subtopicSummary: LearnerGroupSubtopicSummary + subtopicSummary: LearnerGroupSubtopicSummary ): string { let thumbnailUrl = ''; if (subtopicSummary.thumbnailFilename) { - thumbnailUrl = ( - this.assetsBackendApiService.getThumbnailUrlForPreview( - AppConstants.ENTITY_TYPE.TOPIC, subtopicSummary.parentTopicId, - subtopicSummary.thumbnailFilename - ) + thumbnailUrl = this.assetsBackendApiService.getThumbnailUrlForPreview( + AppConstants.ENTITY_TYPE.TOPIC, + subtopicSummary.parentTopicId, + subtopicSummary.thumbnailFilename ); } return thumbnailUrl; @@ -315,11 +323,10 @@ export class AddSyllabusItemsComponent implements OnInit, OnDestroy { getStoryThumbnailUrl(storySummary: StorySummary): string { let thumbnailUrl = ''; if (storySummary.getThumbnailFilename()) { - thumbnailUrl = ( - this.assetsBackendApiService.getThumbnailUrlForPreview( - AppConstants.ENTITY_TYPE.STORY, storySummary.getId(), - storySummary.getThumbnailFilename() - ) + thumbnailUrl = this.assetsBackendApiService.getThumbnailUrlForPreview( + AppConstants.ENTITY_TYPE.STORY, + storySummary.getId(), + storySummary.getThumbnailFilename() ); } return thumbnailUrl; @@ -346,17 +353,19 @@ export class AddSyllabusItemsComponent implements OnInit, OnDestroy { removeStoryFromSyllabus(storyId: string): void { this.syllabusStorySummaries = this.syllabusStorySummaries.filter( - (s) => s.getId() !== storyId); - this.syllabusStoryIds = this.syllabusStoryIds.filter( - (id) => id !== storyId); + s => s.getId() !== storyId + ); + this.syllabusStoryIds = this.syllabusStoryIds.filter(id => id !== storyId); this.updateLearnerGroupSyllabus(); } removeSubtopicFromSyllabus(subtopicPageId: string): void { this.syllabusSubtopicSummaries = this.syllabusSubtopicSummaries.filter( - (s) => s.subtopicPageId !== subtopicPageId); + s => s.subtopicPageId !== subtopicPageId + ); this.syllabusSubtopicPageIds = this.syllabusSubtopicPageIds.filter( - (id) => id !== subtopicPageId); + id => id !== subtopicPageId + ); this.updateLearnerGroupSyllabus(); } @@ -386,6 +395,9 @@ export class AddSyllabusItemsComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive( - 'oppiaLearnerGroupDetails', - downgradeComponent({component: AddSyllabusItemsComponent})); +angular + .module('oppia') + .directive( + 'oppiaLearnerGroupDetails', + downgradeComponent({component: AddSyllabusItemsComponent}) + ); diff --git a/core/templates/pages/learner-group-pages/create-group/create-learner-group-page-auth.guard.spec.ts b/core/templates/pages/learner-group-pages/create-group/create-learner-group-page-auth.guard.spec.ts index e94f74892bff..b00c0b2599ea 100644 --- a/core/templates/pages/learner-group-pages/create-group/create-learner-group-page-auth.guard.spec.ts +++ b/core/templates/pages/learner-group-pages/create-group/create-learner-group-page-auth.guard.spec.ts @@ -15,15 +15,18 @@ /** * @fileoverview Tests for CreateLearnerGroupPageAuthGuard */ -import { Location } from '@angular/common'; -import { TestBed, fakeAsync, tick } from '@angular/core/testing'; -import { ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { AppConstants } from 'app.constants'; -import { CreateLearnerGroupPageAuthGuard } from './create-learner-group-page-auth.guard'; -import { AccessValidationBackendApiService } from 'pages/oppia-root/routing/access-validation-backend-api.service'; - +import {Location} from '@angular/common'; +import {TestBed, fakeAsync, tick} from '@angular/core/testing'; +import { + ActivatedRouteSnapshot, + RouterStateSnapshot, + Router, +} from '@angular/router'; +import {RouterTestingModule} from '@angular/router/testing'; + +import {AppConstants} from 'app.constants'; +import {CreateLearnerGroupPageAuthGuard} from './create-learner-group-page-auth.guard'; +import {AccessValidationBackendApiService} from 'pages/oppia-root/routing/access-validation-backend-api.service'; class MockAccessValidationBackendApiService { validateAccessToLearnerGroupCreatorPage() { @@ -47,9 +50,11 @@ describe('CreateLearnerGroupPageAuthGuard', () => { imports: [RouterTestingModule], providers: [ CreateLearnerGroupPageAuthGuard, - { provide: AccessValidationBackendApiService, - useClass: MockAccessValidationBackendApiService }, - { provide: Router, useClass: MockRouter }, + { + provide: AccessValidationBackendApiService, + useClass: MockAccessValidationBackendApiService, + }, + {provide: Router, useClass: MockRouter}, Location, ], }); @@ -64,15 +69,17 @@ describe('CreateLearnerGroupPageAuthGuard', () => { it('should allow access if validation succeeds', fakeAsync(() => { const validateAccessSpy = spyOn( accessValidationBackendApiService, - 'validateAccessToLearnerGroupCreatorPage') - .and.returnValue(Promise.resolve()); - const navigateSpy = spyOn(router, 'navigate') - .and.returnValue(Promise.resolve(true)); + 'validateAccessToLearnerGroupCreatorPage' + ).and.returnValue(Promise.resolve()); + const navigateSpy = spyOn(router, 'navigate').and.returnValue( + Promise.resolve(true) + ); let canActivateResult: boolean | null = null; - guard.canActivate(new ActivatedRouteSnapshot(), {} as RouterStateSnapshot) - .then((result) => { + guard + .canActivate(new ActivatedRouteSnapshot(), {} as RouterStateSnapshot) + .then(result => { canActivateResult = result; }); @@ -86,23 +93,25 @@ describe('CreateLearnerGroupPageAuthGuard', () => { it('should redirect to 404 page if validation fails', fakeAsync(() => { spyOn( accessValidationBackendApiService, - 'validateAccessToLearnerGroupCreatorPage') - .and.returnValue(Promise.reject()); - const navigateSpy = spyOn(router, 'navigate') - .and.returnValue(Promise.resolve(true)); + 'validateAccessToLearnerGroupCreatorPage' + ).and.returnValue(Promise.reject()); + const navigateSpy = spyOn(router, 'navigate').and.returnValue( + Promise.resolve(true) + ); let canActivateResult: boolean | null = null; - guard.canActivate(new ActivatedRouteSnapshot(), {} as RouterStateSnapshot) - .then((result) => { + guard + .canActivate(new ActivatedRouteSnapshot(), {} as RouterStateSnapshot) + .then(result => { canActivateResult = result; }); tick(); expect(canActivateResult).toBeFalse(); - expect(navigateSpy).toHaveBeenCalledWith( - [`${AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ERROR.ROUTE}/404`] - ); + expect(navigateSpy).toHaveBeenCalledWith([ + `${AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ERROR.ROUTE}/404`, + ]); })); }); diff --git a/core/templates/pages/learner-group-pages/create-group/create-learner-group-page-auth.guard.ts b/core/templates/pages/learner-group-pages/create-group/create-learner-group-page-auth.guard.ts index d49b8123077c..5b45e63780f0 100644 --- a/core/templates/pages/learner-group-pages/create-group/create-learner-group-page-auth.guard.ts +++ b/core/templates/pages/learner-group-pages/create-group/create-learner-group-page-auth.guard.ts @@ -17,8 +17,8 @@ * if the user is not a valid facilitator. */ -import { Location } from '@angular/common'; -import { Injectable } from '@angular/core'; +import {Location} from '@angular/common'; +import {Injectable} from '@angular/core'; import { ActivatedRouteSnapshot, CanActivate, @@ -26,33 +26,34 @@ import { RouterStateSnapshot, } from '@angular/router'; -import { AppConstants } from 'app.constants'; -import { AccessValidationBackendApiService } from 'pages/oppia-root/routing/access-validation-backend-api.service'; +import {AppConstants} from 'app.constants'; +import {AccessValidationBackendApiService} from 'pages/oppia-root/routing/access-validation-backend-api.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class CreateLearnerGroupPageAuthGuard implements CanActivate { constructor( - private accessValidationBackendApiService: - AccessValidationBackendApiService, + private accessValidationBackendApiService: AccessValidationBackendApiService, private router: Router, private location: Location ) {} async canActivate( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot ): Promise { - return new Promise((resolve) => { + return new Promise(resolve => { this.accessValidationBackendApiService .validateAccessToLearnerGroupCreatorPage() .then(() => { resolve(true); }) - .catch((err) => { - this.router.navigate( - [`${AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ERROR.ROUTE}/404`]) + .catch(err => { + this.router + .navigate([ + `${AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ERROR.ROUTE}/404`, + ]) .then(() => { this.location.replaceState(state.url); resolve(false); diff --git a/core/templates/pages/learner-group-pages/create-group/create-learner-group-page-root.component.spec.ts b/core/templates/pages/learner-group-pages/create-group/create-learner-group-page-root.component.spec.ts index d65edc3c80be..01a3c7495728 100644 --- a/core/templates/pages/learner-group-pages/create-group/create-learner-group-page-root.component.spec.ts +++ b/core/templates/pages/learner-group-pages/create-group/create-learner-group-page-root.component.spec.ts @@ -16,14 +16,14 @@ * @fileoverview Unit tests for Learner Group Creator Root component. */ -import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { TranslateModule } from '@ngx-translate/core'; +import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {TranslateModule} from '@ngx-translate/core'; -import { AppConstants } from '../../../app.constants'; -import { PageHeadService } from '../../../services/page-head.service'; -import { CreateLearnerGroupPageRootComponent } from './create-learner-group-page-root.component'; +import {AppConstants} from '../../../app.constants'; +import {PageHeadService} from '../../../services/page-head.service'; +import {CreateLearnerGroupPageRootComponent} from './create-learner-group-page-root.component'; describe('CreateLearnerGroupPageRootComponent', () => { let fixture: ComponentFixture; @@ -43,8 +43,10 @@ describe('CreateLearnerGroupPageRootComponent', () => { it('should have the title and meta tags set', () => { expect(component.title).toEqual( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LEARNER_GROUP_CREATOR.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LEARNER_GROUP_CREATOR.TITLE + ); expect(component.meta).toEqual( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LEARNER_GROUP_CREATOR.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LEARNER_GROUP_CREATOR.META + ); }); }); diff --git a/core/templates/pages/learner-group-pages/create-group/create-learner-group-page-root.component.ts b/core/templates/pages/learner-group-pages/create-group/create-learner-group-page-root.component.ts index 84bfc4a82331..f41c1a325969 100644 --- a/core/templates/pages/learner-group-pages/create-group/create-learner-group-page-root.component.ts +++ b/core/templates/pages/learner-group-pages/create-group/create-learner-group-page-root.component.ts @@ -16,19 +16,18 @@ * @fileoverview Learner Group Creator page root component. */ -import { Component } from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { BaseRootComponent, MetaTagData } from 'pages/base-root.component'; +import {Component} from '@angular/core'; +import {AppConstants} from 'app.constants'; +import {BaseRootComponent, MetaTagData} from 'pages/base-root.component'; @Component({ selector: 'oppia-create-learner-group-page-root', templateUrl: './create-learner-group-page-root.component.html', }) export class CreateLearnerGroupPageRootComponent extends BaseRootComponent { - title: string = AppConstants. - PAGES_REGISTERED_WITH_FRONTEND.LEARNER_GROUP_CREATOR.TITLE; + title: string = + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LEARNER_GROUP_CREATOR.TITLE; - meta: MetaTagData[] = - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LEARNER_GROUP_CREATOR.META as - unknown as Readonly[]; + meta: MetaTagData[] = AppConstants.PAGES_REGISTERED_WITH_FRONTEND + .LEARNER_GROUP_CREATOR.META as unknown as Readonly[]; } diff --git a/core/templates/pages/learner-group-pages/create-group/create-learner-group-page.component.spec.ts b/core/templates/pages/learner-group-pages/create-group/create-learner-group-page.component.spec.ts index 838d251c048d..3dd89c85611d 100644 --- a/core/templates/pages/learner-group-pages/create-group/create-learner-group-page.component.spec.ts +++ b/core/templates/pages/learner-group-pages/create-group/create-learner-group-page.component.spec.ts @@ -16,27 +16,26 @@ * @fileoverview Unit tests for create learner group page. */ -import { Clipboard } from '@angular/cdk/clipboard'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick } from - '@angular/core/testing'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { LearnerGroupBackendApiService } from - 'domain/learner_group/learner-group-backend-api.service'; -import { CreateLearnerGroupPageComponent } from - './create-learner-group-page.component'; -import { LearnerGroupPagesConstants } from '../learner-group-pages.constants'; -import { LearnerGroupSubtopicSummary } from - 'domain/learner_group/learner-group-subtopic-summary.model'; -import { StorySummary } from 'domain/story/story-summary.model'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; -import { LearnerGroupData } from 'domain/learner_group/learner-group.model'; -import { TranslateService } from '@ngx-translate/core'; -import { PageTitleService } from 'services/page-title.service'; -import { LearnerGroupUserInfo } from - 'domain/learner_group/learner-group-user-info.model'; +import {Clipboard} from '@angular/cdk/clipboard'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {LearnerGroupBackendApiService} from 'domain/learner_group/learner-group-backend-api.service'; +import {CreateLearnerGroupPageComponent} from './create-learner-group-page.component'; +import {LearnerGroupPagesConstants} from '../learner-group-pages.constants'; +import {LearnerGroupSubtopicSummary} from 'domain/learner_group/learner-group-subtopic-summary.model'; +import {StorySummary} from 'domain/story/story-summary.model'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {LearnerGroupData} from 'domain/learner_group/learner-group.model'; +import {TranslateService} from '@ngx-translate/core'; +import {PageTitleService} from 'services/page-title.service'; +import {LearnerGroupUserInfo} from 'domain/learner_group/learner-group-user-info.model'; class MockTranslateService { onLangChange: EventEmitter = new EventEmitter(); @@ -57,7 +56,7 @@ describe('CreateLearnerGroupPageComponent', () => { const userInfo = LearnerGroupUserInfo.createFromBackendDict({ username: 'username1', - error: '' + error: '', }); const sampleSubtopicSummaryDict = { @@ -67,11 +66,12 @@ describe('CreateLearnerGroupPageComponent', () => { parent_topic_name: 'parentTopicName', thumbnail_filename: 'thumbnailFilename', thumbnail_bg_color: 'red', - subtopic_mastery: 0.5 + subtopic_mastery: 0.5, }; - const sampleLearnerGroupSubtopicSummary = ( + const sampleLearnerGroupSubtopicSummary = LearnerGroupSubtopicSummary.createFromBackendDict( - sampleSubtopicSummaryDict)); + sampleSubtopicSummaryDict + ); const sampleStorySummaryBackendDict = { id: 'story_id_0', @@ -86,31 +86,30 @@ describe('CreateLearnerGroupPageComponent', () => { all_node_dicts: [], topic_name: 'Topic', classroom_url_fragment: 'math', - topic_url_fragment: 'topic' + topic_url_fragment: 'topic', }; const sampleStorySummary = StorySummary.createFromBackendDict( - sampleStorySummaryBackendDict); + sampleStorySummaryBackendDict + ); beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - CreateLearnerGroupPageComponent, - MockTranslatePipe - ], + declarations: [CreateLearnerGroupPageComponent, MockTranslatePipe], providers: [ { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); beforeEach(() => { learnerGroupBackendApiService = TestBed.inject( - LearnerGroupBackendApiService); + LearnerGroupBackendApiService + ); urlInterpolationService = TestBed.inject(UrlInterpolationService); translateService = TestBed.inject(TranslateService); pageTitleService = TestBed.inject(PageTitleService); @@ -130,7 +129,8 @@ describe('CreateLearnerGroupPageComponent', () => { expect(component.subscribeToOnLangChange).toHaveBeenCalled(); expect(component.activeSection).toEqual( LearnerGroupPagesConstants.LEARNER_GROUP_CREATION_SECTION_I18N_IDS - .GROUP_DETAILS); + .GROUP_DETAILS + ); })); it('should call set page title whenever the language is changed', () => { @@ -149,32 +149,35 @@ describe('CreateLearnerGroupPageComponent', () => { component.setPageTitle(); expect(translateService.instant).toHaveBeenCalledWith( - 'I18N_CREATE_LEARNER_GROUP_PAGE_TITLE'); + 'I18N_CREATE_LEARNER_GROUP_PAGE_TITLE' + ); expect(pageTitleService.setDocumentTitle).toHaveBeenCalledWith( - 'I18N_CREATE_LEARNER_GROUP_PAGE_TITLE'); + 'I18N_CREATE_LEARNER_GROUP_PAGE_TITLE' + ); }); it('should set active section correctly', () => { component.setActiveSection( LearnerGroupPagesConstants.LEARNER_GROUP_CREATION_SECTION_I18N_IDS - .ADD_SYLLABUS_ITEMS, 2); + .ADD_SYLLABUS_ITEMS, + 2 + ); expect(component.activeSection).toEqual( LearnerGroupPagesConstants.LEARNER_GROUP_CREATION_SECTION_I18N_IDS - .ADD_SYLLABUS_ITEMS); + .ADD_SYLLABUS_ITEMS + ); expect(component.furthestReachedSectionNumber).toEqual(2); }); - it('should check if next button on group details section is disabled', - () => { - expect(component.isGroupDetailsNextButtonDisabled()).toBeTrue(); + it('should check if next button on group details section is disabled', () => { + expect(component.isGroupDetailsNextButtonDisabled()).toBeTrue(); - component.learnerGroupTitle = 'title'; - component.learnerGroupDescription = 'description'; + component.learnerGroupTitle = 'title'; + component.learnerGroupDescription = 'description'; - expect(component.isGroupDetailsNextButtonDisabled()).toBeFalse(); - } - ); + expect(component.isGroupDetailsNextButtonDisabled()).toBeFalse(); + }); it('should check if next button on add syllabus section is disabled', () => { expect(component.isAddSyllabusNextButtonDisabled()).toBeTrue(); @@ -209,10 +212,10 @@ describe('CreateLearnerGroupPageComponent', () => { expect(component.learnerGroupTitle).toBe('title'); expect(component.learnerGroupDescription).toBe('description'); expect(component.learnerGroupStoryIds).toEqual(['story_id']); - expect(component.learnerGroupSubtopicPageIds).toEqual( - ['subtopic_page_id']); - expect(component.syllabusSubtopicSummaries).toEqual( - [sampleLearnerGroupSubtopicSummary]); + expect(component.learnerGroupSubtopicPageIds).toEqual(['subtopic_page_id']); + expect(component.syllabusSubtopicSummaries).toEqual([ + sampleLearnerGroupSubtopicSummary, + ]); expect(component.syllabusStorySummaries).toEqual([sampleStorySummary]); expect(component.learnerGroupInvitedLearners).toEqual(['username1']); expect(component.learnerGroupInvitedLearnersInfo).toEqual([userInfo]); @@ -227,10 +230,12 @@ describe('CreateLearnerGroupPageComponent', () => { it('should get oppia large avatar url', () => { spyOn(urlInterpolationService, 'getStaticImageUrl').and.returnValue( - '/avatar/oppia_avatar_large_100px.svg'); + '/avatar/oppia_avatar_large_100px.svg' + ); expect(component.getOppiaLargeAvatarUrl()).toBe( - '/avatar/oppia_avatar_large_100px.svg'); + '/avatar/oppia_avatar_large_100px.svg' + ); }); it('should create new learner group successfully', fakeAsync(() => { @@ -242,13 +247,16 @@ describe('CreateLearnerGroupPageComponent', () => { learner_usernames: [], invited_learner_usernames: ['username1'], subtopic_page_ids: ['subtopic_page_id'], - story_ids: [] + story_ids: [], }; const learnerGroup = LearnerGroupData.createFromBackendDict( - learnerGroupBackendDict); + learnerGroupBackendDict + ); - spyOn(learnerGroupBackendApiService, 'createNewLearnerGroupAsync') - .and.returnValue(Promise.resolve(learnerGroup)); + spyOn( + learnerGroupBackendApiService, + 'createNewLearnerGroupAsync' + ).and.returnValue(Promise.resolve(learnerGroup)); expect(component.learnerGroup).toBeUndefined(); diff --git a/core/templates/pages/learner-group-pages/create-group/create-learner-group-page.component.ts b/core/templates/pages/learner-group-pages/create-group/create-learner-group-page.component.ts index 9634a9ceab1a..1a992f8babb9 100644 --- a/core/templates/pages/learner-group-pages/create-group/create-learner-group-page.component.ts +++ b/core/templates/pages/learner-group-pages/create-group/create-learner-group-page.component.ts @@ -16,38 +16,33 @@ * @fileoverview Component for the create learner group page. */ -import { Clipboard } from '@angular/cdk/clipboard'; -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; - -import { LoaderService } from 'services/loader.service'; -import { PageTitleService } from 'services/page-title.service'; -import { LearnerGroupPagesConstants } from '../learner-group-pages.constants'; -import { LearnerGroupData } from 'domain/learner_group/learner-group.model'; -import { LearnerGroupBackendApiService } from - 'domain/learner_group/learner-group-backend-api.service'; -import { LearnerGroupSubtopicSummary } from - 'domain/learner_group/learner-group-subtopic-summary.model'; -import { StorySummary } from 'domain/story/story-summary.model'; -import { LearnerGroupUserInfo } from - 'domain/learner_group/learner-group-user-info.model'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import {Clipboard} from '@angular/cdk/clipboard'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; + +import {LoaderService} from 'services/loader.service'; +import {PageTitleService} from 'services/page-title.service'; +import {LearnerGroupPagesConstants} from '../learner-group-pages.constants'; +import {LearnerGroupData} from 'domain/learner_group/learner-group.model'; +import {LearnerGroupBackendApiService} from 'domain/learner_group/learner-group-backend-api.service'; +import {LearnerGroupSubtopicSummary} from 'domain/learner_group/learner-group-subtopic-summary.model'; +import {StorySummary} from 'domain/story/story-summary.model'; +import {LearnerGroupUserInfo} from 'domain/learner_group/learner-group-user-info.model'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; import './create-learner-group-page.component.css'; - @Component({ selector: 'oppia-create-learner-group-page', templateUrl: './create-learner-group-page.component.html', - styleUrls: ['./create-learner-group-page.component.css'] + styleUrls: ['./create-learner-group-page.component.css'], }) export class CreateLearnerGroupPageComponent implements OnInit, OnDestroy { directiveSubscriptions = new Subscription(); - LEARNER_GROUP_CREATION_SECTION_I18N_IDS = ( - LearnerGroupPagesConstants.LEARNER_GROUP_CREATION_SECTION_I18N_IDS); + LEARNER_GROUP_CREATION_SECTION_I18N_IDS = + LearnerGroupPagesConstants.LEARNER_GROUP_CREATION_SECTION_I18N_IDS; activeSection!: string; furthestReachedSectionNumber: number = 1; @@ -73,9 +68,8 @@ export class CreateLearnerGroupPageComponent implements OnInit, OnDestroy { ) {} ngOnInit(): void { - this.activeSection = ( - this.LEARNER_GROUP_CREATION_SECTION_I18N_IDS.GROUP_DETAILS - ); + this.activeSection = + this.LEARNER_GROUP_CREATION_SECTION_I18N_IDS.GROUP_DETAILS; this.subscribeToOnLangChange(); } @@ -89,7 +83,8 @@ export class CreateLearnerGroupPageComponent implements OnInit, OnDestroy { setPageTitle(): void { let translatedTitle = this.translateService.instant( - 'I18N_CREATE_LEARNER_GROUP_PAGE_TITLE'); + 'I18N_CREATE_LEARNER_GROUP_PAGE_TITLE' + ); this.pageTitleService.setDocumentTitle(translatedTitle); } @@ -101,10 +96,7 @@ export class CreateLearnerGroupPageComponent implements OnInit, OnDestroy { } isGroupDetailsNextButtonDisabled(): boolean { - return ( - !this.learnerGroupTitle || - !this.learnerGroupDescription - ); + return !this.learnerGroupTitle || !this.learnerGroupDescription; } updateLearnerGroupTitle(title: string): void { @@ -116,7 +108,7 @@ export class CreateLearnerGroupPageComponent implements OnInit, OnDestroy { } updateLearnerGroupSubtopics( - subtopicSummaries: LearnerGroupSubtopicSummary[] + subtopicSummaries: LearnerGroupSubtopicSummary[] ): void { this.syllabusSubtopicSummaries = subtopicSummaries; } @@ -138,7 +130,7 @@ export class CreateLearnerGroupPageComponent implements OnInit, OnDestroy { } updateLearnerGroupInvitedLearnersInfo( - invitedUsersInfo: LearnerGroupUserInfo[] + invitedUsersInfo: LearnerGroupUserInfo[] ): void { this.learnerGroupInvitedLearnersInfo = invitedUsersInfo; } @@ -168,21 +160,24 @@ export class CreateLearnerGroupPageComponent implements OnInit, OnDestroy { createLearnerGroup(): void { this.loaderService.showLoadingScreen('Creating learner group'); - this.learnerGroupBackendApiService.createNewLearnerGroupAsync( - this.learnerGroupTitle, - this.learnerGroupDescription, - this.learnerGroupInvitedLearners, - this.learnerGroupSubtopicPageIds, - this.learnerGroupStoryIds - ).then((responseLearnerGroup: LearnerGroupData) => { - this.learnerGroup = responseLearnerGroup; - this.learnerGroupUrl = ( - this.windowRef.nativeWindow.location.protocol + '//' + - this.windowRef.nativeWindow.location.host + - '/edit-learner-group/' + this.learnerGroup.id - ); - this.loaderService.hideLoadingScreen(); - }); + this.learnerGroupBackendApiService + .createNewLearnerGroupAsync( + this.learnerGroupTitle, + this.learnerGroupDescription, + this.learnerGroupInvitedLearners, + this.learnerGroupSubtopicPageIds, + this.learnerGroupStoryIds + ) + .then((responseLearnerGroup: LearnerGroupData) => { + this.learnerGroup = responseLearnerGroup; + this.learnerGroupUrl = + this.windowRef.nativeWindow.location.protocol + + '//' + + this.windowRef.nativeWindow.location.host + + '/edit-learner-group/' + + this.learnerGroup.id; + this.loaderService.hideLoadingScreen(); + }); } copyCreatedGroupUrl(): void { diff --git a/core/templates/pages/learner-group-pages/create-group/create-learner-group-page.import.ts b/core/templates/pages/learner-group-pages/create-group/create-learner-group-page.import.ts index 8c5f21f15d7b..6c3b1f5bb998 100644 --- a/core/templates/pages/learner-group-pages/create-group/create-learner-group-page.import.ts +++ b/core/templates/pages/learner-group-pages/create-group/create-learner-group-page.import.ts @@ -22,9 +22,14 @@ import 'zone.js'; import uiValidate from 'angular-ui-validate'; angular.module('oppia', [ - require('angular-cookies'), 'ngAnimate', - 'ngMaterial', 'ngSanitize', 'ngTouch', 'pascalprecht.translate', - 'ui.bootstrap', uiValidate + require('angular-cookies'), + 'ngAnimate', + 'ngMaterial', + 'ngSanitize', + 'ngTouch', + 'pascalprecht.translate', + 'ui.bootstrap', + uiValidate, ]); require('Polyfills.ts'); diff --git a/core/templates/pages/learner-group-pages/create-group/create-learner-group-page.module.ts b/core/templates/pages/learner-group-pages/create-group/create-learner-group-page.module.ts index 0ddc1cc58db2..88da4a2ea862 100644 --- a/core/templates/pages/learner-group-pages/create-group/create-learner-group-page.module.ts +++ b/core/templates/pages/learner-group-pages/create-group/create-learner-group-page.module.ts @@ -16,16 +16,16 @@ * @fileoverview Module for the create learner group page. */ -import { NgModule } from '@angular/core'; -import { ToastrModule } from 'ngx-toastr'; -import { toastrConfig } from 'pages/oppia-root/app.module'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { RouterModule } from '@angular/router'; +import {NgModule} from '@angular/core'; +import {ToastrModule} from 'ngx-toastr'; +import {toastrConfig} from 'pages/oppia-root/app.module'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {RouterModule} from '@angular/router'; -import { CreateLearnerGroupPageComponent } from './create-learner-group-page.component'; -import { CreateLearnerGroupPageRootComponent } from './create-learner-group-page-root.component'; -import { CreateLearnerGroupPageAuthGuard } from './create-learner-group-page-auth.guard'; -import { SharedLearnerGroupComponentsModule } from 'pages/learner-group-pages/shared-learner-group-component.module'; +import {CreateLearnerGroupPageComponent} from './create-learner-group-page.component'; +import {CreateLearnerGroupPageRootComponent} from './create-learner-group-page-root.component'; +import {CreateLearnerGroupPageAuthGuard} from './create-learner-group-page-auth.guard'; +import {SharedLearnerGroupComponentsModule} from 'pages/learner-group-pages/shared-learner-group-component.module'; @NgModule({ imports: [ @@ -42,11 +42,8 @@ import { SharedLearnerGroupComponentsModule } from 'pages/learner-group-pages/sh ], declarations: [ CreateLearnerGroupPageRootComponent, - CreateLearnerGroupPageComponent - ], - entryComponents: [ CreateLearnerGroupPageComponent, ], + entryComponents: [CreateLearnerGroupPageComponent], }) - export class CreateLearnerGroupPageModule {} diff --git a/core/templates/pages/learner-group-pages/create-group/invite-learners.component.spec.ts b/core/templates/pages/learner-group-pages/create-group/invite-learners.component.spec.ts index 359c8c7dbc14..675c903ef0b5 100644 --- a/core/templates/pages/learner-group-pages/create-group/invite-learners.component.spec.ts +++ b/core/templates/pages/learner-group-pages/create-group/invite-learners.component.spec.ts @@ -16,16 +16,19 @@ * @fileoverview Unit tests for inviting learners to learner group. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { InviteLearnersComponent } from './invite-learners.component'; -import { LearnerGroupBackendApiService } from - 'domain/learner_group/learner-group-backend-api.service'; -import { LearnerGroupUserInfo } from - 'domain/learner_group/learner-group-user-info.model'; -import { UserService } from 'services/user.service'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {InviteLearnersComponent} from './invite-learners.component'; +import {LearnerGroupBackendApiService} from 'domain/learner_group/learner-group-backend-api.service'; +import {LearnerGroupUserInfo} from 'domain/learner_group/learner-group-user-info.model'; +import {UserService} from 'services/user.service'; describe('InviteLearnersComponent', () => { let component: InviteLearnersComponent; @@ -35,95 +38,107 @@ describe('InviteLearnersComponent', () => { const userInfo = LearnerGroupUserInfo.createFromBackendDict({ username: 'username1', - error: '' + error: '', }); beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - InviteLearnersComponent, - MockTranslatePipe - ], + declarations: [InviteLearnersComponent, MockTranslatePipe], providers: [], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); beforeEach(() => { learnerGroupBackendApiService = TestBed.inject( - LearnerGroupBackendApiService); + LearnerGroupBackendApiService + ); fixture = TestBed.createComponent(InviteLearnersComponent); userService = TestBed.inject(UserService); component = fixture.componentInstance; - spyOn(userService, 'getProfileImageDataUrl').and.returnValue( - ['default-image-url-png', 'default-image-url-webp']); + spyOn(userService, 'getProfileImageDataUrl').and.returnValue([ + 'default-image-url-png', + 'default-image-url-webp', + ]); fixture.detectChanges(); }); - it('should search new learners to add to the learner group', - fakeAsync(() => { - spyOn(learnerGroupBackendApiService, 'searchNewLearnerToAddAsync') - .and.returnValue(Promise.resolve(userInfo)); + it('should search new learners to add to the learner group', fakeAsync(() => { + spyOn( + learnerGroupBackendApiService, + 'searchNewLearnerToAddAsync' + ).and.returnValue(Promise.resolve(userInfo)); - expect(component.invitedUsersInfo).toEqual([]); - expect(component.invitedUsernames).toEqual([]); + expect(component.invitedUsersInfo).toEqual([]); + expect(component.invitedUsernames).toEqual([]); - component.onSearchQueryChangeExec('username1'); - tick(); - fixture.detectChanges(); + component.onSearchQueryChangeExec('username1'); + tick(); + fixture.detectChanges(); - expect(component.invitedUsersInfo).toEqual([userInfo]); - expect(component.invitedUsernames).toEqual(['username1']); - }) - ); + expect(component.invitedUsersInfo).toEqual([userInfo]); + expect(component.invitedUsernames).toEqual(['username1']); + })); it('should get user profile image png data url correctly', () => { expect(component.getProfileImagePngDataUrl('username')).toBe( - 'default-image-url-png'); + 'default-image-url-png' + ); }); it('should get user profile image webp data url correctly', () => { expect(component.getProfileImageWebpDataUrl('username')).toBe( - 'default-image-url-webp'); + 'default-image-url-webp' + ); }); - it('should show error message when trying to add an invalid learners to ' + - 'the learner group', fakeAsync(() => { - const userInfo2 = LearnerGroupUserInfo.createFromBackendDict({ - username: 'username2', - error: 'You cannot invite yourself to the learner group.' - }); - spyOn(learnerGroupBackendApiService, 'searchNewLearnerToAddAsync') - .and.returnValue(Promise.resolve(userInfo2)); + it( + 'should show error message when trying to add an invalid learners to ' + + 'the learner group', + fakeAsync(() => { + const userInfo2 = LearnerGroupUserInfo.createFromBackendDict({ + username: 'username2', + error: 'You cannot invite yourself to the learner group.', + }); + spyOn( + learnerGroupBackendApiService, + 'searchNewLearnerToAddAsync' + ).and.returnValue(Promise.resolve(userInfo2)); - expect(component.invitedUsersInfo).toEqual([]); - expect(component.invitedUsernames).toEqual([]); + expect(component.invitedUsersInfo).toEqual([]); + expect(component.invitedUsernames).toEqual([]); - component.onSearchQueryChangeExec('username2'); + component.onSearchQueryChangeExec('username2'); - tick(); + tick(); - expect(component.invitedUsersInfo).toEqual([]); - expect(component.invitedUsernames).toEqual([]); - expect(component.errorMessage).toEqual( - 'You cannot invite yourself to the learner group.'); - })); + expect(component.invitedUsersInfo).toEqual([]); + expect(component.invitedUsernames).toEqual([]); + expect(component.errorMessage).toEqual( + 'You cannot invite yourself to the learner group.' + ); + }) + ); - it('should show error message when trying to add an already invited ' + - 'learner to the learner group', fakeAsync(() => { - component.invitedUsersInfo = [userInfo]; - component.invitedUsernames = ['username1']; + it( + 'should show error message when trying to add an already invited ' + + 'learner to the learner group', + fakeAsync(() => { + component.invitedUsersInfo = [userInfo]; + component.invitedUsernames = ['username1']; - component.onSearchQueryChangeExec('username1'); + component.onSearchQueryChangeExec('username1'); - tick(); + tick(); - expect(component.invitedUsersInfo).toEqual([userInfo]); - expect(component.invitedUsernames).toEqual(['username1']); - expect(component.errorMessage).toEqual( - 'User with username username1 has been already invited.'); - })); + expect(component.invitedUsersInfo).toEqual([userInfo]); + expect(component.invitedUsernames).toEqual(['username1']); + expect(component.errorMessage).toEqual( + 'User with username username1 has been already invited.' + ); + }) + ); it('should remove invited learner successfully', () => { component.invitedUsersInfo = [userInfo]; diff --git a/core/templates/pages/learner-group-pages/create-group/invite-learners.component.ts b/core/templates/pages/learner-group-pages/create-group/invite-learners.component.ts index 50668efce7ce..e8e9f7acad62 100644 --- a/core/templates/pages/learner-group-pages/create-group/invite-learners.component.ts +++ b/core/templates/pages/learner-group-pages/create-group/invite-learners.component.ts @@ -16,31 +16,29 @@ * @fileoverview Component for the inviting learners to learner group. */ -import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { LearnerGroupBackendApiService } from - 'domain/learner_group/learner-group-backend-api.service'; -import { LearnerGroupUserInfo } from - 'domain/learner_group/learner-group-user-info.model'; -import { UserService } from 'services/user.service'; +import {Component, EventEmitter, Input, Output} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {LearnerGroupBackendApiService} from 'domain/learner_group/learner-group-backend-api.service'; +import {LearnerGroupUserInfo} from 'domain/learner_group/learner-group-user-info.model'; +import {UserService} from 'services/user.service'; import './invite-learners.component.css'; - @Component({ selector: 'oppia-invite-learners', templateUrl: './invite-learners.component.html', - styleUrls: ['./invite-learners.component.css'] + styleUrls: ['./invite-learners.component.css'], }) export class InviteLearnersComponent { @Input() learnerGroupID: string = ''; @Input() invitedUsersInfo: LearnerGroupUserInfo[] = []; @Input() invitedUsernames: string[] = []; - @Output() updateLearnerGroupInvitedLearners: - EventEmitter = new EventEmitter(); + @Output() updateLearnerGroupInvitedLearners: EventEmitter = + new EventEmitter(); - @Output() updateLearnerGroupInvitedLearnersInfo: - EventEmitter = new EventEmitter(); + @Output() updateLearnerGroupInvitedLearnersInfo: EventEmitter< + LearnerGroupUserInfo[] + > = new EventEmitter(); searchedUsername: string = ''; errorMessage!: string; @@ -51,59 +49,59 @@ export class InviteLearnersComponent { ) {} updateInvitedLearners(): void { - this.updateLearnerGroupInvitedLearners.emit( - this.invitedUsernames); - this.updateLearnerGroupInvitedLearnersInfo.emit( - this.invitedUsersInfo); + this.updateLearnerGroupInvitedLearners.emit(this.invitedUsernames); + this.updateLearnerGroupInvitedLearnersInfo.emit(this.invitedUsersInfo); } onSearchQueryChangeExec(username: string): void { if (username) { const isUserAlreadyInvited = this.invitedUsernames.some( - (name) => name.toLowerCase() === username.toLowerCase() + name => name.toLowerCase() === username.toLowerCase() ); if (isUserAlreadyInvited) { - this.errorMessage = ( - 'User with username ' + username + ' has been already invited.' - ); + this.errorMessage = + 'User with username ' + username + ' has been already invited.'; return; } - this.learnerGroupBackendApiService.searchNewLearnerToAddAsync( - this.learnerGroupID, username - ).then(userInfo => { - if (!userInfo.error) { - this.errorMessage = ''; - this.invitedUsersInfo.push(userInfo); - this.invitedUsernames.push(userInfo.username); - this.updateInvitedLearners(); - } else { - this.errorMessage = userInfo.error; - } - }); + this.learnerGroupBackendApiService + .searchNewLearnerToAddAsync(this.learnerGroupID, username) + .then(userInfo => { + if (!userInfo.error) { + this.errorMessage = ''; + this.invitedUsersInfo.push(userInfo); + this.invitedUsernames.push(userInfo.username); + this.updateInvitedLearners(); + } else { + this.errorMessage = userInfo.error; + } + }); } } removeInvitedLearner(username: string): void { this.invitedUsersInfo = this.invitedUsersInfo.filter( - (userInfo) => userInfo.username !== username); + userInfo => userInfo.username !== username + ); this.invitedUsernames = this.invitedUsernames.filter( - (username) => username !== username); + username => username !== username + ); this.updateInvitedLearners(); } getProfileImagePngDataUrl(username: string): string { - let [pngImageUrl, _] = this.userService.getProfileImageDataUrl( - username); + let [pngImageUrl, _] = this.userService.getProfileImageDataUrl(username); return pngImageUrl; } getProfileImageWebpDataUrl(username: string): string { - let [_, webpImageUrl] = this.userService.getProfileImageDataUrl( - username); + let [_, webpImageUrl] = this.userService.getProfileImageDataUrl(username); return webpImageUrl; } } -angular.module('oppia').directive( - 'oppiaInviteLearners', - downgradeComponent({component: InviteLearnersComponent})); +angular + .module('oppia') + .directive( + 'oppiaInviteLearners', + downgradeComponent({component: InviteLearnersComponent}) + ); diff --git a/core/templates/pages/learner-group-pages/create-group/learner-group-details.component.spec.ts b/core/templates/pages/learner-group-pages/create-group/learner-group-details.component.spec.ts index c91db475c487..c616f9414a1b 100644 --- a/core/templates/pages/learner-group-pages/create-group/learner-group-details.component.spec.ts +++ b/core/templates/pages/learner-group-pages/create-group/learner-group-details.component.spec.ts @@ -16,11 +16,11 @@ * @fileoverview Unit tests for adding learner group details. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { LearnerGroupDetailsComponent } from './learner-group-details.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {LearnerGroupDetailsComponent} from './learner-group-details.component'; describe('LearnerGroupDetailsComponent', () => { let component: LearnerGroupDetailsComponent; @@ -29,12 +29,9 @@ describe('LearnerGroupDetailsComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - LearnerGroupDetailsComponent, - MockTranslatePipe - ], + declarations: [LearnerGroupDetailsComponent, MockTranslatePipe], providers: [], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); diff --git a/core/templates/pages/learner-group-pages/create-group/learner-group-details.component.ts b/core/templates/pages/learner-group-pages/create-group/learner-group-details.component.ts index 318fbf73f333..f056c0d8c9f3 100644 --- a/core/templates/pages/learner-group-pages/create-group/learner-group-details.component.ts +++ b/core/templates/pages/learner-group-pages/create-group/learner-group-details.component.ts @@ -16,16 +16,15 @@ * @fileoverview Component for the learner group details. */ -import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component, EventEmitter, Input, Output} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; import './learner-group-details.component.css'; - @Component({ selector: 'oppia-learner-group-details', templateUrl: './learner-group-details.component.html', - styleUrls: ['./learner-group-details.component.css'] + styleUrls: ['./learner-group-details.component.css'], }) export class LearnerGroupDetailsComponent { @Input() learnerGroupTitle!: string; @@ -51,6 +50,9 @@ export class LearnerGroupDetailsComponent { } } -angular.module('oppia').directive( - 'oppiaLearnerGroupDetails', - downgradeComponent({component: LearnerGroupDetailsComponent})); +angular + .module('oppia') + .directive( + 'oppiaLearnerGroupDetails', + downgradeComponent({component: LearnerGroupDetailsComponent}) + ); diff --git a/core/templates/pages/learner-group-pages/edit-group/edit-learner-group-page-auth.guard.spec.ts b/core/templates/pages/learner-group-pages/edit-group/edit-learner-group-page-auth.guard.spec.ts index 41c60fae77b4..50e8d42b84fe 100644 --- a/core/templates/pages/learner-group-pages/edit-group/edit-learner-group-page-auth.guard.spec.ts +++ b/core/templates/pages/learner-group-pages/edit-group/edit-learner-group-page-auth.guard.spec.ts @@ -15,15 +15,18 @@ /** * @fileoverview Tests for EditLearnerGroupPageAuthGuard */ -import { Location } from '@angular/common'; -import { TestBed, fakeAsync, tick } from '@angular/core/testing'; -import { ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { AppConstants } from 'app.constants'; -import { EditLearnerGroupPageAuthGuard } from './edit-learner-group-page-auth.guard'; -import { AccessValidationBackendApiService } from 'pages/oppia-root/routing/access-validation-backend-api.service'; - +import {Location} from '@angular/common'; +import {TestBed, fakeAsync, tick} from '@angular/core/testing'; +import { + ActivatedRouteSnapshot, + RouterStateSnapshot, + Router, +} from '@angular/router'; +import {RouterTestingModule} from '@angular/router/testing'; + +import {AppConstants} from 'app.constants'; +import {EditLearnerGroupPageAuthGuard} from './edit-learner-group-page-auth.guard'; +import {AccessValidationBackendApiService} from 'pages/oppia-root/routing/access-validation-backend-api.service'; class MockAccessValidationBackendApiService { validateAccessToLearnerGroupEditorPage(learnerGroupId: string) { @@ -47,9 +50,11 @@ describe('EditLearnerGroupPageAuthGuard', () => { imports: [RouterTestingModule], providers: [ EditLearnerGroupPageAuthGuard, - { provide: AccessValidationBackendApiService, - useClass: MockAccessValidationBackendApiService }, - { provide: Router, useClass: MockRouter }, + { + provide: AccessValidationBackendApiService, + useClass: MockAccessValidationBackendApiService, + }, + {provide: Router, useClass: MockRouter}, Location, ], }); @@ -64,15 +69,17 @@ describe('EditLearnerGroupPageAuthGuard', () => { it('should allow access if validation succeeds', fakeAsync(() => { const validateAccessSpy = spyOn( accessValidationBackendApiService, - 'validateAccessToLearnerGroupEditorPage') - .and.returnValue(Promise.resolve()); - const navigateSpy = spyOn(router, 'navigate') - .and.returnValue(Promise.resolve(true)); + 'validateAccessToLearnerGroupEditorPage' + ).and.returnValue(Promise.resolve()); + const navigateSpy = spyOn(router, 'navigate').and.returnValue( + Promise.resolve(true) + ); let canActivateResult: boolean | null = null; - guard.canActivate(new ActivatedRouteSnapshot(), {} as RouterStateSnapshot) - .then((result) => { + guard + .canActivate(new ActivatedRouteSnapshot(), {} as RouterStateSnapshot) + .then(result => { canActivateResult = result; }); @@ -86,23 +93,25 @@ describe('EditLearnerGroupPageAuthGuard', () => { it('should redirect to 401 page if validation fails', fakeAsync(() => { spyOn( accessValidationBackendApiService, - 'validateAccessToLearnerGroupEditorPage') - .and.returnValue(Promise.reject()); - const navigateSpy = spyOn(router, 'navigate') - .and.returnValue(Promise.resolve(true)); + 'validateAccessToLearnerGroupEditorPage' + ).and.returnValue(Promise.reject()); + const navigateSpy = spyOn(router, 'navigate').and.returnValue( + Promise.resolve(true) + ); let canActivateResult: boolean | null = null; - guard.canActivate(new ActivatedRouteSnapshot(), {} as RouterStateSnapshot) - .then((result) => { + guard + .canActivate(new ActivatedRouteSnapshot(), {} as RouterStateSnapshot) + .then(result => { canActivateResult = result; }); tick(); expect(canActivateResult).toBeFalse(); - expect(navigateSpy).toHaveBeenCalledWith( - [`${AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ERROR.ROUTE}/401`] - ); + expect(navigateSpy).toHaveBeenCalledWith([ + `${AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ERROR.ROUTE}/401`, + ]); })); }); diff --git a/core/templates/pages/learner-group-pages/edit-group/edit-learner-group-page-auth.guard.ts b/core/templates/pages/learner-group-pages/edit-group/edit-learner-group-page-auth.guard.ts index db24a07664b2..230fd7f99229 100644 --- a/core/templates/pages/learner-group-pages/edit-group/edit-learner-group-page-auth.guard.ts +++ b/core/templates/pages/learner-group-pages/edit-group/edit-learner-group-page-auth.guard.ts @@ -17,8 +17,8 @@ * if the user is not a valid facilitator. */ -import { Location } from '@angular/common'; -import { Injectable } from '@angular/core'; +import {Location} from '@angular/common'; +import {Injectable} from '@angular/core'; import { ActivatedRouteSnapshot, CanActivate, @@ -26,35 +26,36 @@ import { RouterStateSnapshot, } from '@angular/router'; -import { AppConstants } from 'app.constants'; -import { AccessValidationBackendApiService } from 'pages/oppia-root/routing/access-validation-backend-api.service'; +import {AppConstants} from 'app.constants'; +import {AccessValidationBackendApiService} from 'pages/oppia-root/routing/access-validation-backend-api.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class EditLearnerGroupPageAuthGuard implements CanActivate { constructor( - private accessValidationBackendApiService: - AccessValidationBackendApiService, + private accessValidationBackendApiService: AccessValidationBackendApiService, private router: Router, private location: Location ) {} async canActivate( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot ): Promise { let learnerGroupId = route.paramMap.get('learner_group_id') || ''; - return new Promise((resolve) => { + return new Promise(resolve => { this.accessValidationBackendApiService .validateAccessToLearnerGroupEditorPage(learnerGroupId) .then(() => { resolve(true); }) - .catch((err) => { - this.router.navigate( - [`${AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ERROR.ROUTE}/401`]) + .catch(err => { + this.router + .navigate([ + `${AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ERROR.ROUTE}/401`, + ]) .then(() => { this.location.replaceState(state.url); resolve(false); diff --git a/core/templates/pages/learner-group-pages/edit-group/edit-learner-group-page-root.component.spec.ts b/core/templates/pages/learner-group-pages/edit-group/edit-learner-group-page-root.component.spec.ts index 3fd62d19fafb..83cb57fa1360 100644 --- a/core/templates/pages/learner-group-pages/edit-group/edit-learner-group-page-root.component.spec.ts +++ b/core/templates/pages/learner-group-pages/edit-group/edit-learner-group-page-root.component.spec.ts @@ -16,14 +16,14 @@ * @fileoverview Unit tests for Learner Group Editor Root component. */ -import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { TranslateModule } from '@ngx-translate/core'; +import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {TranslateModule} from '@ngx-translate/core'; -import { AppConstants } from '../../../app.constants'; -import { PageHeadService } from '../../../services/page-head.service'; -import { EditLearnerGroupPageRootComponent } from './edit-learner-group-page-root.component'; +import {AppConstants} from '../../../app.constants'; +import {PageHeadService} from '../../../services/page-head.service'; +import {EditLearnerGroupPageRootComponent} from './edit-learner-group-page-root.component'; describe('EditLearnerGroupPageRootComponent', () => { let fixture: ComponentFixture; @@ -43,8 +43,10 @@ describe('EditLearnerGroupPageRootComponent', () => { it('should have the title and meta tags set', () => { expect(component.title).toEqual( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LEARNER_GROUP_EDITOR.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LEARNER_GROUP_EDITOR.TITLE + ); expect(component.meta).toEqual( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LEARNER_GROUP_EDITOR.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LEARNER_GROUP_EDITOR.META + ); }); }); diff --git a/core/templates/pages/learner-group-pages/edit-group/edit-learner-group-page-root.component.ts b/core/templates/pages/learner-group-pages/edit-group/edit-learner-group-page-root.component.ts index f30dc0243625..c4530eaf1c71 100644 --- a/core/templates/pages/learner-group-pages/edit-group/edit-learner-group-page-root.component.ts +++ b/core/templates/pages/learner-group-pages/edit-group/edit-learner-group-page-root.component.ts @@ -16,19 +16,18 @@ * @fileoverview Learner Group Editor page root component. */ -import { Component } from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { BaseRootComponent, MetaTagData } from 'pages/base-root.component'; +import {Component} from '@angular/core'; +import {AppConstants} from 'app.constants'; +import {BaseRootComponent, MetaTagData} from 'pages/base-root.component'; @Component({ selector: 'oppia-edit-learner-group-page-root', templateUrl: './edit-learner-group-page-root.component.html', }) export class EditLearnerGroupPageRootComponent extends BaseRootComponent { - title: string = AppConstants. - PAGES_REGISTERED_WITH_FRONTEND.LEARNER_GROUP_EDITOR.TITLE; + title: string = + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LEARNER_GROUP_EDITOR.TITLE; - meta: MetaTagData[] = - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LEARNER_GROUP_EDITOR.META as - unknown as Readonly[]; + meta: MetaTagData[] = AppConstants.PAGES_REGISTERED_WITH_FRONTEND + .LEARNER_GROUP_EDITOR.META as unknown as Readonly[]; } diff --git a/core/templates/pages/learner-group-pages/edit-group/edit-learner-group-page.component.spec.ts b/core/templates/pages/learner-group-pages/edit-group/edit-learner-group-page.component.spec.ts index 8f2b0e721722..2990b405fd0f 100644 --- a/core/templates/pages/learner-group-pages/edit-group/edit-learner-group-page.component.spec.ts +++ b/core/templates/pages/learner-group-pages/edit-group/edit-learner-group-page.component.spec.ts @@ -16,19 +16,22 @@ * @fileoverview Unit tests for edit learner group page. */ -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick } from - '@angular/core/testing'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { LearnerGroupBackendApiService } from - 'domain/learner_group/learner-group-backend-api.service'; -import { LearnerGroupPagesConstants } from '../learner-group-pages.constants'; -import { LearnerGroupData } from 'domain/learner_group/learner-group.model'; -import { TranslateService } from '@ngx-translate/core'; -import { PageTitleService } from 'services/page-title.service'; -import { EditLearnerGroupPageComponent } from './edit-learner-group-page.component'; -import { ContextService } from 'services/context.service'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {LearnerGroupBackendApiService} from 'domain/learner_group/learner-group-backend-api.service'; +import {LearnerGroupPagesConstants} from '../learner-group-pages.constants'; +import {LearnerGroupData} from 'domain/learner_group/learner-group.model'; +import {TranslateService} from '@ngx-translate/core'; +import {PageTitleService} from 'services/page-title.service'; +import {EditLearnerGroupPageComponent} from './edit-learner-group-page.component'; +import {ContextService} from 'services/context.service'; class MockTranslateService { onLangChange: EventEmitter = new EventEmitter(); @@ -54,31 +57,30 @@ describe('EditLearnerGroupPageComponent', () => { learner_usernames: ['username2'], invited_learner_usernames: ['username1'], subtopic_page_ids: ['subtopic_page_id'], - story_ids: [] + story_ids: [], }; const learnerGroup = LearnerGroupData.createFromBackendDict( - learnerGroupBackendDict); + learnerGroupBackendDict + ); beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - EditLearnerGroupPageComponent, - MockTranslatePipe - ], + declarations: [EditLearnerGroupPageComponent, MockTranslatePipe], providers: [ { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); beforeEach(() => { learnerGroupBackendApiService = TestBed.inject( - LearnerGroupBackendApiService); + LearnerGroupBackendApiService + ); translateService = TestBed.inject(TranslateService); pageTitleService = TestBed.inject(PageTitleService); contextService = TestBed.inject(ContextService); @@ -91,8 +93,10 @@ describe('EditLearnerGroupPageComponent', () => { }); it('should initialize', fakeAsync(() => { - spyOn(learnerGroupBackendApiService, 'fetchLearnerGroupInfoAsync') - .and.returnValue(Promise.resolve(learnerGroup)); + spyOn( + learnerGroupBackendApiService, + 'fetchLearnerGroupInfoAsync' + ).and.returnValue(Promise.resolve(learnerGroup)); spyOn(component, 'subscribeToOnLangChange'); spyOn(translateService.onLangChange, 'subscribe'); @@ -101,25 +105,26 @@ describe('EditLearnerGroupPageComponent', () => { expect(component.subscribeToOnLangChange).toHaveBeenCalled(); expect(component.activeTab).toEqual( - LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_TABS.OVERVIEW); + LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_TABS.OVERVIEW + ); expect(component.learnerGroup).toEqual(learnerGroup); expect(component.getLearnersCount()).toBe(1); })); - it('should call set page title whenever the language is changed', - fakeAsync(() => { - spyOn(learnerGroupBackendApiService, 'fetchLearnerGroupInfoAsync') - .and.returnValue(Promise.resolve(learnerGroup)); + it('should call set page title whenever the language is changed', fakeAsync(() => { + spyOn( + learnerGroupBackendApiService, + 'fetchLearnerGroupInfoAsync' + ).and.returnValue(Promise.resolve(learnerGroup)); - component.ngOnInit(); - tick(); - spyOn(component, 'setPageTitle'); + component.ngOnInit(); + tick(); + spyOn(component, 'setPageTitle'); - translateService.onLangChange.emit(); + translateService.onLangChange.emit(); - expect(component.setPageTitle).toHaveBeenCalled(); - }) - ); + expect(component.setPageTitle).toHaveBeenCalled(); + })); it('should set page title', () => { spyOn(translateService, 'instant').and.callThrough(); @@ -128,24 +133,30 @@ describe('EditLearnerGroupPageComponent', () => { component.setPageTitle(); expect(translateService.instant).toHaveBeenCalledWith( - 'I18N_EDIT_LEARNER_GROUP_PAGE_TITLE'); + 'I18N_EDIT_LEARNER_GROUP_PAGE_TITLE' + ); expect(pageTitleService.setDocumentTitle).toHaveBeenCalledWith( - 'I18N_EDIT_LEARNER_GROUP_PAGE_TITLE'); + 'I18N_EDIT_LEARNER_GROUP_PAGE_TITLE' + ); }); it('should set active tab and check if tab is active correctly', () => { component.setActiveTab( - LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_TABS.LEARNERS_PROGRESS); + LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_TABS.LEARNERS_PROGRESS + ); expect(component.activeTab).toEqual( - LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_TABS.LEARNERS_PROGRESS); + LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_TABS.LEARNERS_PROGRESS + ); let tabIsActive = component.isTabActive( - LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_TABS.LEARNERS_PROGRESS); + LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_TABS.LEARNERS_PROGRESS + ); expect(tabIsActive).toBeTrue(); tabIsActive = component.isTabActive( - LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_TABS.OVERVIEW); + LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_TABS.OVERVIEW + ); expect(tabIsActive).toBeFalse(); }); }); diff --git a/core/templates/pages/learner-group-pages/edit-group/edit-learner-group-page.component.ts b/core/templates/pages/learner-group-pages/edit-group/edit-learner-group-page.component.ts index 54ecfb45390c..5ad12450f033 100644 --- a/core/templates/pages/learner-group-pages/edit-group/edit-learner-group-page.component.ts +++ b/core/templates/pages/learner-group-pages/edit-group/edit-learner-group-page.component.ts @@ -16,30 +16,28 @@ * @fileoverview Component for the edit learner group page. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; -import { LoaderService } from 'services/loader.service'; -import { PageTitleService } from 'services/page-title.service'; -import { LearnerGroupPagesConstants } from '../learner-group-pages.constants'; -import { LearnerGroupData } from 'domain/learner_group/learner-group.model'; -import { LearnerGroupBackendApiService } from - 'domain/learner_group/learner-group-backend-api.service'; -import { ContextService } from 'services/context.service'; +import {LoaderService} from 'services/loader.service'; +import {PageTitleService} from 'services/page-title.service'; +import {LearnerGroupPagesConstants} from '../learner-group-pages.constants'; +import {LearnerGroupData} from 'domain/learner_group/learner-group.model'; +import {LearnerGroupBackendApiService} from 'domain/learner_group/learner-group-backend-api.service'; +import {ContextService} from 'services/context.service'; import './edit-learner-group-page.component.css'; - @Component({ selector: 'oppia-edit-learner-group-page', templateUrl: './edit-learner-group-page.component.html', - styleUrls: ['./edit-learner-group-page.component.css'] + styleUrls: ['./edit-learner-group-page.component.css'], }) export class EditLearnerGroupPageComponent implements OnInit, OnDestroy { directiveSubscriptions = new Subscription(); - EDIT_LEARNER_GROUP_TABS_I18N_IDS = ( - LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_TABS); + EDIT_LEARNER_GROUP_TABS_I18N_IDS = + LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_TABS; activeTab!: string; learnerGroupId!: string; @@ -58,12 +56,12 @@ export class EditLearnerGroupPageComponent implements OnInit, OnDestroy { this.activeTab = this.EDIT_LEARNER_GROUP_TABS_I18N_IDS.OVERVIEW; if (this.learnerGroupId) { this.loaderService.showLoadingScreen('Loading'); - this.learnerGroupBackendApiService.fetchLearnerGroupInfoAsync( - this.learnerGroupId - ).then(learnerGroup => { - this.learnerGroup = learnerGroup; - this.loaderService.hideLoadingScreen(); - }); + this.learnerGroupBackendApiService + .fetchLearnerGroupInfoAsync(this.learnerGroupId) + .then(learnerGroup => { + this.learnerGroup = learnerGroup; + this.loaderService.hideLoadingScreen(); + }); } this.subscribeToOnLangChange(); } @@ -78,7 +76,8 @@ export class EditLearnerGroupPageComponent implements OnInit, OnDestroy { setPageTitle(): void { let translatedTitle = this.translateService.instant( - 'I18N_EDIT_LEARNER_GROUP_PAGE_TITLE'); + 'I18N_EDIT_LEARNER_GROUP_PAGE_TITLE' + ); this.pageTitleService.setDocumentTitle(translatedTitle); } diff --git a/core/templates/pages/learner-group-pages/edit-group/edit-learner-group-page.import.ts b/core/templates/pages/learner-group-pages/edit-group/edit-learner-group-page.import.ts index 835640f12dde..a4f3339fcd0b 100644 --- a/core/templates/pages/learner-group-pages/edit-group/edit-learner-group-page.import.ts +++ b/core/templates/pages/learner-group-pages/edit-group/edit-learner-group-page.import.ts @@ -22,9 +22,14 @@ import 'zone.js'; import uiValidate from 'angular-ui-validate'; angular.module('oppia', [ - require('angular-cookies'), 'ngAnimate', - 'ngMaterial', 'ngSanitize', 'ngTouch', 'pascalprecht.translate', - 'ui.bootstrap', uiValidate + require('angular-cookies'), + 'ngAnimate', + 'ngMaterial', + 'ngSanitize', + 'ngTouch', + 'pascalprecht.translate', + 'ui.bootstrap', + uiValidate, ]); require('Polyfills.ts'); diff --git a/core/templates/pages/learner-group-pages/edit-group/edit-learner-group-page.module.ts b/core/templates/pages/learner-group-pages/edit-group/edit-learner-group-page.module.ts index b2b277db1f45..541229cb88e8 100644 --- a/core/templates/pages/learner-group-pages/edit-group/edit-learner-group-page.module.ts +++ b/core/templates/pages/learner-group-pages/edit-group/edit-learner-group-page.module.ts @@ -16,37 +16,28 @@ * @fileoverview Module for the edit learner group page. */ -import { NgModule } from '@angular/core'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { ToastrModule } from 'ngx-toastr'; -import { toastrConfig } from 'pages/oppia-root/app.module'; -import { RouterModule } from '@angular/router'; -import { CommonModule } from '@angular/common'; +import {NgModule} from '@angular/core'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {ToastrModule} from 'ngx-toastr'; +import {toastrConfig} from 'pages/oppia-root/app.module'; +import {RouterModule} from '@angular/router'; +import {CommonModule} from '@angular/common'; -import { EditLearnerGroupPageAuthGuard } from './edit-learner-group-page-auth.guard'; -import { EditLearnerGroupPageRootComponent } from './edit-learner-group-page-root.component'; -import { EditLearnerGroupPageComponent } from - './edit-learner-group-page.component'; -import { LearnerGroupSyllabusComponent } from - './learner-group-syllabus.component'; +import {EditLearnerGroupPageAuthGuard} from './edit-learner-group-page-auth.guard'; +import {EditLearnerGroupPageRootComponent} from './edit-learner-group-page-root.component'; +import {EditLearnerGroupPageComponent} from './edit-learner-group-page.component'; +import {LearnerGroupSyllabusComponent} from './learner-group-syllabus.component'; -import { RemoveItemModalComponent } from - '../templates/remove-item-modal.component'; -import { SyllabusAdditionSuccessModalComponent } from - '../templates/syllabus-addition-success-modal.component'; -import { LearnerGroupPreferencesComponent } from - './learner-group-preferences.component'; +import {RemoveItemModalComponent} from '../templates/remove-item-modal.component'; +import {SyllabusAdditionSuccessModalComponent} from '../templates/syllabus-addition-success-modal.component'; +import {LearnerGroupPreferencesComponent} from './learner-group-preferences.component'; -import { InviteLearnersModalComponent } from - '../templates/invite-learners-modal.component'; +import {InviteLearnersModalComponent} from '../templates/invite-learners-modal.component'; -import { LearnerGroupLearnersProgressComponent } from - './learner-group-learners-progress.component'; -import { InviteSuccessfulModalComponent } from - '../templates/invite-successful-modal.component'; -import { DeleteLearnerGroupModalComponent } from - '../templates/delete-learner-group-modal.component'; -import { SharedLearnerGroupComponentsModule } from 'pages/learner-group-pages/shared-learner-group-component.module'; +import {LearnerGroupLearnersProgressComponent} from './learner-group-learners-progress.component'; +import {InviteSuccessfulModalComponent} from '../templates/invite-successful-modal.component'; +import {DeleteLearnerGroupModalComponent} from '../templates/delete-learner-group-modal.component'; +import {SharedLearnerGroupComponentsModule} from 'pages/learner-group-pages/shared-learner-group-component.module'; @NgModule({ imports: [ @@ -72,7 +63,7 @@ import { SharedLearnerGroupComponentsModule } from 'pages/learner-group-pages/sh SyllabusAdditionSuccessModalComponent, InviteLearnersModalComponent, InviteSuccessfulModalComponent, - DeleteLearnerGroupModalComponent + DeleteLearnerGroupModalComponent, ], entryComponents: [ EditLearnerGroupPageComponent, @@ -83,8 +74,7 @@ import { SharedLearnerGroupComponentsModule } from 'pages/learner-group-pages/sh SyllabusAdditionSuccessModalComponent, InviteLearnersModalComponent, InviteSuccessfulModalComponent, - DeleteLearnerGroupModalComponent + DeleteLearnerGroupModalComponent, ], }) - export class EditLearnerGroupPageModule {} diff --git a/core/templates/pages/learner-group-pages/edit-group/learner-group-learner-specific-progress.component.spec.ts b/core/templates/pages/learner-group-pages/edit-group/learner-group-learner-specific-progress.component.spec.ts index 20b9b8f6bdc5..35a3f9b73b0e 100644 --- a/core/templates/pages/learner-group-pages/edit-group/learner-group-learner-specific-progress.component.spec.ts +++ b/core/templates/pages/learner-group-pages/edit-group/learner-group-learner-specific-progress.component.spec.ts @@ -16,16 +16,21 @@ * @fileoverview Unit tests for learner group preferences tab. */ -import { NO_ERRORS_SCHEMA, Pipe } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NavigationService } from 'services/navigation.service'; -import { LearnerGroupUserProgress } from 'domain/learner_group/learner-group-user-progress.model'; -import { LearnerGroupLearnerSpecificProgressComponent } from './learner-group-learner-specific-progress.component'; -import { ChapterProgressSummary } from 'domain/exploration/chapter-progress-summary.model'; -import { StoryViewerBackendApiService } from 'domain/story_viewer/story-viewer-backend-api.service'; -import { LearnerGroupPagesConstants } from '../learner-group-pages.constants'; +import {NO_ERRORS_SCHEMA, Pipe} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NavigationService} from 'services/navigation.service'; +import {LearnerGroupUserProgress} from 'domain/learner_group/learner-group-user-progress.model'; +import {LearnerGroupLearnerSpecificProgressComponent} from './learner-group-learner-specific-progress.component'; +import {ChapterProgressSummary} from 'domain/exploration/chapter-progress-summary.model'; +import {StoryViewerBackendApiService} from 'domain/story_viewer/story-viewer-backend-api.service'; +import {LearnerGroupPagesConstants} from '../learner-group-pages.constants'; @Pipe({name: 'truncate'}) class MockTrunctePipe { @@ -50,7 +55,7 @@ describe('LearnerGroupLearnerSpecificProgressComponent', () => { parent_topic_name: 'parentTopicName', thumbnail_filename: 'thumbnailFilename', thumbnail_bg_color: 'red', - subtopic_mastery: 0.5 + subtopic_mastery: 0.5, }; let nodeDict = { @@ -69,7 +74,7 @@ describe('LearnerGroupLearnerSpecificProgressComponent', () => { planned_publication_date_msecs: 100, last_modified_msecs: 100, first_publication_date_msecs: 200, - unpublishing_reason: null + unpublishing_reason: null, }; const sampleStorySummaryBackendDict = { id: 'sample_story_id', @@ -84,7 +89,7 @@ describe('LearnerGroupLearnerSpecificProgressComponent', () => { all_node_dicts: [nodeDict], topic_name: 'Topic one', topic_url_fragment: 'topic-one', - classroom_url_fragment: 'math' + classroom_url_fragment: 'math', }; const sampleStorySummaryBackendDict2 = { id: 'story_id_1', @@ -99,20 +104,22 @@ describe('LearnerGroupLearnerSpecificProgressComponent', () => { all_node_dicts: [], topic_name: 'Topic', classroom_url_fragment: 'math', - topic_url_fragment: 'topic' + topic_url_fragment: 'topic', }; const sampleLearnerGroupUserProgDict = { username: 'username2', progress_sharing_is_turned_on: true, stories_progress: [ - sampleStorySummaryBackendDict, sampleStorySummaryBackendDict2], - subtopic_pages_progress: [sampleLearnerGroupSubtopicSummaryDict] + sampleStorySummaryBackendDict, + sampleStorySummaryBackendDict2, + ], + subtopic_pages_progress: [sampleLearnerGroupSubtopicSummaryDict], }; - const sampleLearnerGroupUserProg = ( + const sampleLearnerGroupUserProg = LearnerGroupUserProgress.createFromBackendDict( - sampleLearnerGroupUserProgDict) - ); + sampleLearnerGroupUserProgDict + ); beforeEach(() => { TestBed.configureTestingModule({ @@ -120,23 +127,23 @@ describe('LearnerGroupLearnerSpecificProgressComponent', () => { declarations: [ LearnerGroupLearnerSpecificProgressComponent, MockTranslatePipe, - MockTrunctePipe + MockTrunctePipe, ], providers: [ { provide: NavigationService, - useClass: MockNavigationService - } + useClass: MockNavigationService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); beforeEach(() => { - storyViewerBackendApiService = TestBed.inject( - StoryViewerBackendApiService); + storyViewerBackendApiService = TestBed.inject(StoryViewerBackendApiService); fixture = TestBed.createComponent( - LearnerGroupLearnerSpecificProgressComponent); + LearnerGroupLearnerSpecificProgressComponent + ); component = fixture.componentInstance; component.learnerProgress = sampleLearnerGroupUserProg; @@ -145,73 +152,84 @@ describe('LearnerGroupLearnerSpecificProgressComponent', () => { it('should initialize', fakeAsync(() => { const chapterProgressSummaryDict = { total_checkpoints_count: 6, - visited_checkpoints_count: 4 + visited_checkpoints_count: 4, }; const chaptersProgress = ChapterProgressSummary.createFromBackendDict( - chapterProgressSummaryDict); + chapterProgressSummaryDict + ); - spyOn(storyViewerBackendApiService, 'fetchProgressInStoriesChapters') - .and.returnValue(Promise.resolve([chaptersProgress])); + spyOn( + storyViewerBackendApiService, + 'fetchProgressInStoriesChapters' + ).and.returnValue(Promise.resolve([chaptersProgress])); component.ngOnInit(); tick(100); expect(component.topicNames).toEqual(['parentTopicName']); - expect(component.cummulativeStoryChaptersCount).toEqual( - [1, 3]); + expect(component.cummulativeStoryChaptersCount).toEqual([1, 3]); })); it('should set active tab and check if tab is active correctly', () => { component.setActiveTab( LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_OVERVIEW_SECTIONS - .PROGRESS_IN_STORIES); + .PROGRESS_IN_STORIES + ); expect(component.activeTab).toEqual( LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_OVERVIEW_SECTIONS - .PROGRESS_IN_STORIES); + .PROGRESS_IN_STORIES + ); let tabIsActive = component.isTabActive( LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_OVERVIEW_SECTIONS - .PROGRESS_IN_STORIES); + .PROGRESS_IN_STORIES + ); expect(tabIsActive).toBeTrue(); tabIsActive = component.isTabActive( LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_OVERVIEW_SECTIONS - .SKILLS_ANALYSIS); + .SKILLS_ANALYSIS + ); expect(tabIsActive).toBeFalse(); }); - it('should get all checkpoints progress of chapter correctly', - fakeAsync(() => { - const chapterProgressSummaryDict1 = { - total_checkpoints_count: 6, - visited_checkpoints_count: 4 - }; - const chapterProgressSummaryDict2 = { - total_checkpoints_count: 4, - visited_checkpoints_count: 3 - }; - - const chaptersProgress1 = ChapterProgressSummary.createFromBackendDict( - chapterProgressSummaryDict1); - const chaptersProgress2 = ChapterProgressSummary.createFromBackendDict( - chapterProgressSummaryDict2); - - spyOn(storyViewerBackendApiService, 'fetchProgressInStoriesChapters') - .and.returnValue(Promise.resolve( - [chaptersProgress1, chaptersProgress2, chaptersProgress2])); - - component.ngOnInit(); - tick(100); - - let checkpointsProgress = component.getAllCheckpointsProgressOfChapter( - 0, 0); - expect(checkpointsProgress).toEqual([1, 1, 1, 1, 1, 1]); - - checkpointsProgress = component.getAllCheckpointsProgressOfChapter(1, 1); - expect(checkpointsProgress).toEqual([1, 1, 2, 0]); - }) - ); + it('should get all checkpoints progress of chapter correctly', fakeAsync(() => { + const chapterProgressSummaryDict1 = { + total_checkpoints_count: 6, + visited_checkpoints_count: 4, + }; + const chapterProgressSummaryDict2 = { + total_checkpoints_count: 4, + visited_checkpoints_count: 3, + }; + + const chaptersProgress1 = ChapterProgressSummary.createFromBackendDict( + chapterProgressSummaryDict1 + ); + const chaptersProgress2 = ChapterProgressSummary.createFromBackendDict( + chapterProgressSummaryDict2 + ); + + spyOn( + storyViewerBackendApiService, + 'fetchProgressInStoriesChapters' + ).and.returnValue( + Promise.resolve([chaptersProgress1, chaptersProgress2, chaptersProgress2]) + ); + + component.ngOnInit(); + tick(100); + + let checkpointsProgress = component.getAllCheckpointsProgressOfChapter( + 0, + 0 + ); + expect(checkpointsProgress).toEqual([1, 1, 1, 1, 1, 1]); + + checkpointsProgress = component.getAllCheckpointsProgressOfChapter(1, 1); + expect(checkpointsProgress).toEqual([1, 1, 2, 0]); + })); it('should check whether chapter is completed correctly', () => { component.learnerProgress = sampleLearnerGroupUserProg; @@ -225,7 +243,9 @@ describe('LearnerGroupLearnerSpecificProgressComponent', () => { it('should get completed chapter progress bar width correctly', () => { spyOn(component, 'getAllCheckpointsProgressOfChapter').and.returnValues( - [1, 1, 1, 2, 0, 0], [1, 2, 0, 0, 0]); + [1, 1, 1, 2, 0, 0], + [1, 2, 0, 0, 0] + ); component.learnerProgress = sampleLearnerGroupUserProg; let progressWidth = component.getCompletedProgressBarWidth(0, 0); @@ -237,7 +257,11 @@ describe('LearnerGroupLearnerSpecificProgressComponent', () => { it('should get visited checkpoints count of chapter correctly', () => { spyOn(component, 'getAllCheckpointsProgressOfChapter').and.returnValues( - [1, 1, 1, 2, 0, 0], [1, 2, 0, 0, 0], [0, 0, 0], [1, 1, 1]); + [1, 1, 1, 2, 0, 0], + [1, 2, 0, 0, 0], + [0, 0, 0], + [1, 1, 1] + ); component.learnerProgress = sampleLearnerGroupUserProg; let visitedCheckpointsCount = component.getVisitedCheckpointsCount(0, 0); @@ -253,15 +277,13 @@ describe('LearnerGroupLearnerSpecificProgressComponent', () => { expect(visitedCheckpointsCount).toBe(3); }); - it('should get count of subtopics learner is struggling with correctly', - () => { - component.learnerProgress = sampleLearnerGroupUserProg; + it('should get count of subtopics learner is struggling with correctly', () => { + component.learnerProgress = sampleLearnerGroupUserProg; - let strugglingSubtopicsCount = component.getStrugglingWithSubtopicsCount( - 'parentTopicName'); - expect(strugglingSubtopicsCount).toBe(1); - } - ); + let strugglingSubtopicsCount = + component.getStrugglingWithSubtopicsCount('parentTopicName'); + expect(strugglingSubtopicsCount).toBe(1); + }); it('should get user profile image data url correctly', () => { const dataUrl = '%2Fimages%2Furl%2F1'; diff --git a/core/templates/pages/learner-group-pages/edit-group/learner-group-learner-specific-progress.component.ts b/core/templates/pages/learner-group-pages/edit-group/learner-group-learner-specific-progress.component.ts index fa8fedeb0704..044a2e1f8ecf 100644 --- a/core/templates/pages/learner-group-pages/edit-group/learner-group-learner-specific-progress.component.ts +++ b/core/templates/pages/learner-group-pages/edit-group/learner-group-learner-specific-progress.component.ts @@ -16,24 +16,20 @@ * @fileoverview Component for the learner group learner specific progress. */ -import { Component, Input } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { ChapterProgressSummary } from - 'domain/exploration/chapter-progress-summary.model'; -import { LearnerGroupSyllabusBackendApiService } - from 'domain/learner_group/learner-group-syllabus-backend-api.service'; -import { LearnerGroupUserProgress } from - 'domain/learner_group/learner-group-user-progress.model'; -import { StoryViewerBackendApiService } from 'domain/story_viewer/story-viewer-backend-api.service'; -import { LearnerGroupPagesConstants } from '../learner-group-pages.constants'; +import {Component, Input} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {ChapterProgressSummary} from 'domain/exploration/chapter-progress-summary.model'; +import {LearnerGroupSyllabusBackendApiService} from 'domain/learner_group/learner-group-syllabus-backend-api.service'; +import {LearnerGroupUserProgress} from 'domain/learner_group/learner-group-user-progress.model'; +import {StoryViewerBackendApiService} from 'domain/story_viewer/story-viewer-backend-api.service'; +import {LearnerGroupPagesConstants} from '../learner-group-pages.constants'; import './learner-group-learner-specific-progress.component.css'; - @Component({ selector: 'oppia-learner-group-learner-specific-progress', templateUrl: './learner-group-learner-specific-progress.component.html', - styleUrls: ['./learner-group-learner-specific-progress.component.css'] + styleUrls: ['./learner-group-learner-specific-progress.component.css'], }) export class LearnerGroupLearnerSpecificProgressComponent { @Input() learnerProgress!: LearnerGroupUserProgress; @@ -43,12 +39,11 @@ export class LearnerGroupLearnerSpecificProgressComponent { storyIds: string[] = []; latestChapterProgressIndex = 0; cummulativeStoryChaptersCount: number[] = []; - EDIT_OVERVIEW_SECTIONS_I18N_IDS = ( - LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_OVERVIEW_SECTIONS); + EDIT_OVERVIEW_SECTIONS_I18N_IDS = + LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_OVERVIEW_SECTIONS; constructor( - private learnerGroupSyllabusBackendApiService: - LearnerGroupSyllabusBackendApiService, + private learnerGroupSyllabusBackendApiService: LearnerGroupSyllabusBackendApiService, private storyViewerBackendApiService: StoryViewerBackendApiService ) {} @@ -62,22 +57,28 @@ export class LearnerGroupLearnerSpecificProgressComponent { }); this.learnerProgress.storiesProgress.forEach(storyProgress => { this.storyIds.push(storyProgress.getId()); - let previousChaptersCount = ( - this.cummulativeStoryChaptersCount.slice(-1).pop()); + let previousChaptersCount = this.cummulativeStoryChaptersCount + .slice(-1) + .pop(); if (previousChaptersCount) { this.cummulativeStoryChaptersCount.push( - previousChaptersCount + storyProgress.getNodeTitles().length); + previousChaptersCount + storyProgress.getNodeTitles().length + ); } else { this.cummulativeStoryChaptersCount.push( - storyProgress.getNodeTitles().length); + storyProgress.getNodeTitles().length + ); } }); - this.storyViewerBackendApiService.fetchProgressInStoriesChapters( - this.learnerProgress.username, this.storyIds - ).then(storiesChaptersProgress => { - this.storiesChaptersProgress = storiesChaptersProgress; - }); + this.storyViewerBackendApiService + .fetchProgressInStoriesChapters( + this.learnerProgress.username, + this.storyIds + ) + .then(storiesChaptersProgress => { + this.storiesChaptersProgress = storiesChaptersProgress; + }); } } @@ -94,13 +95,13 @@ export class LearnerGroupLearnerSpecificProgressComponent { } getAllCheckpointsProgressOfChapter( - storyIndex: number, chapterIndex: number + storyIndex: number, + chapterIndex: number ): number[] { let chapterProgressIndex = chapterIndex; if (storyIndex !== 0) { - chapterProgressIndex += ( - this.cummulativeStoryChaptersCount[storyIndex - 1] - ); + chapterProgressIndex += + this.cummulativeStoryChaptersCount[storyIndex - 1]; } const chapterProgress = this.storiesChaptersProgress[chapterProgressIndex]; if (this.isChapterCompleted(storyIndex, chapterIndex)) { @@ -108,9 +109,9 @@ export class LearnerGroupLearnerSpecificProgressComponent { } let allCheckpointsProgress: number[] = []; for (let i = 0; i < chapterProgress.totalCheckpoints; i++) { - if (i < (chapterProgress.visitedCheckpoints - 1)) { + if (i < chapterProgress.visitedCheckpoints - 1) { allCheckpointsProgress.push(1); - } else if (i === (chapterProgress.visitedCheckpoints - 1)) { + } else if (i === chapterProgress.visitedCheckpoints - 1) { allCheckpointsProgress.push(2); } else { allCheckpointsProgress.push(0); @@ -129,45 +130,46 @@ export class LearnerGroupLearnerSpecificProgressComponent { } getCompletedProgressBarWidth( - storyIndex: number, chapterIndex: number + storyIndex: number, + chapterIndex: number ): number { const checkpointsProgress = this.getAllCheckpointsProgressOfChapter( - storyIndex, chapterIndex); + storyIndex, + chapterIndex + ); const completedCheckpoints = checkpointsProgress.filter( checkpointProgress => checkpointProgress === 1 ).length; const spaceBetweenEachNode = 100 / (checkpointsProgress.length - 1); return ( - ((completedCheckpoints - 1) * spaceBetweenEachNode) + - (spaceBetweenEachNode / 2)); + (completedCheckpoints - 1) * spaceBetweenEachNode + + spaceBetweenEachNode / 2 + ); } - getVisitedCheckpointsCount( - storyIndex: number, chapterIndex: number - ): number { + getVisitedCheckpointsCount(storyIndex: number, chapterIndex: number): number { const checkpointsProgress = this.getAllCheckpointsProgressOfChapter( - storyIndex, chapterIndex); + storyIndex, + chapterIndex + ); return checkpointsProgress.filter( - checkpointProgress => ( - checkpointProgress === 1 || checkpointProgress === 2 - ) + checkpointProgress => checkpointProgress === 1 || checkpointProgress === 2 ).length; } getStrugglingWithSubtopicsCount(topicName: string): number { return this.learnerProgress.subtopicsProgress.filter( - subtopicProgress => ( + subtopicProgress => subtopicProgress.parentTopicName === topicName && subtopicProgress.subtopicMastery && subtopicProgress.subtopicMastery < 0.6 - ) ).length; } } angular.module('oppia').directive( 'oppiaLearnerGroupLearnerSpecificProgress', - downgradeComponent( - {component: LearnerGroupLearnerSpecificProgressComponent} - ) + downgradeComponent({ + component: LearnerGroupLearnerSpecificProgressComponent, + }) ); diff --git a/core/templates/pages/learner-group-pages/edit-group/learner-group-learners-progress.component.spec.ts b/core/templates/pages/learner-group-pages/edit-group/learner-group-learners-progress.component.spec.ts index 9c4bbdbd7206..b982ffb775eb 100644 --- a/core/templates/pages/learner-group-pages/edit-group/learner-group-learners-progress.component.spec.ts +++ b/core/templates/pages/learner-group-pages/edit-group/learner-group-learners-progress.component.spec.ts @@ -16,19 +16,23 @@ * @fileoverview Unit tests for learner group preferences tab. */ -import { NO_ERRORS_SCHEMA, Pipe } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NavigationService } from 'services/navigation.service'; -import { LearnerGroupSyllabusBackendApiService } from - 'domain/learner_group/learner-group-syllabus-backend-api.service'; -import { LearnerGroupData } from 'domain/learner_group/learner-group.model'; -import { LearnerGroupLearnersProgressComponent } from './learner-group-learners-progress.component'; -import { LearnerGroupUserProgress } from 'domain/learner_group/learner-group-user-progress.model'; -import { StoryViewerBackendApiService } from 'domain/story_viewer/story-viewer-backend-api.service'; -import { ChapterProgressSummary } from 'domain/exploration/chapter-progress-summary.model'; -import { UserService } from 'services/user.service'; +import {NO_ERRORS_SCHEMA, Pipe} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NavigationService} from 'services/navigation.service'; +import {LearnerGroupSyllabusBackendApiService} from 'domain/learner_group/learner-group-syllabus-backend-api.service'; +import {LearnerGroupData} from 'domain/learner_group/learner-group.model'; +import {LearnerGroupLearnersProgressComponent} from './learner-group-learners-progress.component'; +import {LearnerGroupUserProgress} from 'domain/learner_group/learner-group-user-progress.model'; +import {StoryViewerBackendApiService} from 'domain/story_viewer/story-viewer-backend-api.service'; +import {ChapterProgressSummary} from 'domain/exploration/chapter-progress-summary.model'; +import {UserService} from 'services/user.service'; @Pipe({name: 'truncate'}) class MockTrunctePipe { @@ -44,8 +48,7 @@ class MockNavigationService { describe('LearnerGroupLearnersProgressComponent', () => { let component: LearnerGroupLearnersProgressComponent; let fixture: ComponentFixture; - let learnerGroupSyllabusBackendApiService: - LearnerGroupSyllabusBackendApiService; + let learnerGroupSyllabusBackendApiService: LearnerGroupSyllabusBackendApiService; let navigationService: NavigationService; let storyViewerBackendApiService: StoryViewerBackendApiService; let userService: UserService; @@ -57,7 +60,7 @@ describe('LearnerGroupLearnersProgressComponent', () => { parent_topic_name: 'parentTopicName', thumbnail_filename: 'thumbnailFilename', thumbnail_bg_color: 'red', - subtopic_mastery: 0.5 + subtopic_mastery: 0.5, }; let nodeDict = { @@ -76,7 +79,7 @@ describe('LearnerGroupLearnersProgressComponent', () => { planned_publication_date_msecs: 100, last_modified_msecs: 100, first_publication_date_msecs: 200, - unpublishing_reason: null + unpublishing_reason: null, }; const sampleStorySummaryBackendDict = { id: 'sample_story_id', @@ -91,19 +94,19 @@ describe('LearnerGroupLearnersProgressComponent', () => { all_node_dicts: [nodeDict], topic_name: 'Topic one', topic_url_fragment: 'topic-one', - classroom_url_fragment: 'math' + classroom_url_fragment: 'math', }; const sampleLearnerGroupUserProgDict = { username: 'username2', progress_sharing_is_turned_on: true, stories_progress: [sampleStorySummaryBackendDict], - subtopic_pages_progress: [sampleLearnerGroupSubtopicSummaryDict] + subtopic_pages_progress: [sampleLearnerGroupSubtopicSummaryDict], }; - const sampleLearnerGroupUserProg = ( + const sampleLearnerGroupUserProg = LearnerGroupUserProgress.createFromBackendDict( - sampleLearnerGroupUserProgDict) - ); + sampleLearnerGroupUserProgDict + ); const learnerGroupBackendDict = { id: 'groupId', @@ -113,10 +116,11 @@ describe('LearnerGroupLearnersProgressComponent', () => { learner_usernames: ['username1'], invited_learner_usernames: ['username2'], subtopic_page_ids: [], - story_ids: ['story_id_1'] + story_ids: ['story_id_1'], }; const learnerGroup = LearnerGroupData.createFromBackendDict( - learnerGroupBackendDict); + learnerGroupBackendDict + ); beforeEach(() => { TestBed.configureTestingModule({ @@ -124,31 +128,33 @@ describe('LearnerGroupLearnersProgressComponent', () => { declarations: [ LearnerGroupLearnersProgressComponent, MockTranslatePipe, - MockTrunctePipe + MockTrunctePipe, ], providers: [ { provide: NavigationService, - useClass: MockNavigationService - } + useClass: MockNavigationService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); beforeEach(() => { learnerGroupSyllabusBackendApiService = TestBed.inject( - LearnerGroupSyllabusBackendApiService); + LearnerGroupSyllabusBackendApiService + ); navigationService = TestBed.inject(NavigationService); - storyViewerBackendApiService = TestBed.inject( - StoryViewerBackendApiService); + storyViewerBackendApiService = TestBed.inject(StoryViewerBackendApiService); fixture = TestBed.createComponent(LearnerGroupLearnersProgressComponent); userService = TestBed.inject(UserService); component = fixture.componentInstance; component.learnerGroup = learnerGroup; - spyOn(userService, 'getProfileImageDataUrl').and.returnValue( - ['default-image-url-png', 'default-image-url-webp']); + spyOn(userService, 'getProfileImageDataUrl').and.returnValue([ + 'default-image-url-png', + 'default-image-url-webp', + ]); }); it('should initialize', fakeAsync(() => { @@ -163,8 +169,9 @@ describe('LearnerGroupLearnersProgressComponent', () => { tick(100); expect(component.learnersProgress).toEqual([sampleLearnerGroupUserProg]); - expect(component.matchingUsersProgress).toEqual( - [sampleLearnerGroupUserProg]); + expect(component.matchingUsersProgress).toEqual([ + sampleLearnerGroupUserProg, + ]); })); it('should get count of completed stories by learner correctly', () => { @@ -191,32 +198,34 @@ describe('LearnerGroupLearnersProgressComponent', () => { expect(component.isLearnerSpecificViewActive()).toBeFalse(); }); - it('should search learner progress with username matching keyword correctly', - () => { - component.learnersProgress = [sampleLearnerGroupUserProg]; - component.matchingUsersProgress = [sampleLearnerGroupUserProg]; + it('should search learner progress with username matching keyword correctly', () => { + component.learnersProgress = [sampleLearnerGroupUserProg]; + component.matchingUsersProgress = [sampleLearnerGroupUserProg]; - component.searchUsernameQuery = ''; - expect(component.getSearchUsernameResults()).toEqual( - [sampleLearnerGroupUserProg]); + component.searchUsernameQuery = ''; + expect(component.getSearchUsernameResults()).toEqual([ + sampleLearnerGroupUserProg, + ]); - component.searchUsernameQuery = 'some'; - expect(component.getSearchUsernameResults()).toEqual([]); + component.searchUsernameQuery = 'some'; + expect(component.getSearchUsernameResults()).toEqual([]); - component.searchUsernameQuery = 'Usern'; - expect(component.getSearchUsernameResults()).toEqual( - [sampleLearnerGroupUserProg]); - } - ); + component.searchUsernameQuery = 'Usern'; + expect(component.getSearchUsernameResults()).toEqual([ + sampleLearnerGroupUserProg, + ]); + }); it('should get user profile image png data url correctly', () => { expect(component.getProfileImagePngDataUrl('username')).toBe( - 'default-image-url-png'); + 'default-image-url-png' + ); }); it('should get user profile image webp data url correctly', () => { expect(component.getProfileImageWebpDataUrl('username')).toBe( - 'default-image-url-webp'); + 'default-image-url-webp' + ); }); it('should open submenu', () => { @@ -226,25 +235,31 @@ describe('LearnerGroupLearnersProgressComponent', () => { component.openSubmenu(clickEvent, 'learner'); expect(navigationService.openSubmenu).toHaveBeenCalledWith( - clickEvent, 'learner'); + clickEvent, + 'learner' + ); }); it('should update learner specific progress successfully', fakeAsync(() => { const chapterProgressSummaryDict = { total_checkpoints_count: 6, - visited_checkpoints_count: 4 + visited_checkpoints_count: 4, }; const chaptersProgress = ChapterProgressSummary.createFromBackendDict( - chapterProgressSummaryDict); + chapterProgressSummaryDict + ); - spyOn(storyViewerBackendApiService, 'fetchProgressInStoriesChapters') - .and.returnValue(Promise.resolve([chaptersProgress])); + spyOn( + storyViewerBackendApiService, + 'fetchProgressInStoriesChapters' + ).and.returnValue(Promise.resolve([chaptersProgress])); component.updateLearnerSpecificProgress(sampleLearnerGroupUserProg); tick(100); expect(component.specificLearnerProgress).toEqual( - sampleLearnerGroupUserProg); + sampleLearnerGroupUserProg + ); expect(component.storiesChaptersProgress).toEqual([chaptersProgress]); })); }); diff --git a/core/templates/pages/learner-group-pages/edit-group/learner-group-learners-progress.component.ts b/core/templates/pages/learner-group-pages/edit-group/learner-group-learners-progress.component.ts index 89beb4ad9e7e..e4f4e7158ebd 100644 --- a/core/templates/pages/learner-group-pages/edit-group/learner-group-learners-progress.component.ts +++ b/core/templates/pages/learner-group-pages/edit-group/learner-group-learners-progress.component.ts @@ -16,22 +16,21 @@ * @fileoverview Component for the learner group all learners progress. */ -import { Component, Input, OnInit } from '@angular/core'; -import { ChapterProgressSummary } from 'domain/exploration/chapter-progress-summary.model'; -import { LearnerGroupSyllabusBackendApiService } from 'domain/learner_group/learner-group-syllabus-backend-api.service'; -import { LearnerGroupUserProgress } from 'domain/learner_group/learner-group-user-progress.model'; -import { LearnerGroupData } from 'domain/learner_group/learner-group.model'; -import { StoryViewerBackendApiService } from 'domain/story_viewer/story-viewer-backend-api.service'; -import { NavigationService } from 'services/navigation.service'; -import { UserService } from 'services/user.service'; +import {Component, Input, OnInit} from '@angular/core'; +import {ChapterProgressSummary} from 'domain/exploration/chapter-progress-summary.model'; +import {LearnerGroupSyllabusBackendApiService} from 'domain/learner_group/learner-group-syllabus-backend-api.service'; +import {LearnerGroupUserProgress} from 'domain/learner_group/learner-group-user-progress.model'; +import {LearnerGroupData} from 'domain/learner_group/learner-group.model'; +import {StoryViewerBackendApiService} from 'domain/story_viewer/story-viewer-backend-api.service'; +import {NavigationService} from 'services/navigation.service'; +import {UserService} from 'services/user.service'; import './learner-group-learners-progress.component.css'; - @Component({ selector: 'oppia-learner-group-learners-progress', templateUrl: './learner-group-learners-progress.component.html', - styleUrls: ['./learner-group-learners-progress.component.css'] + styleUrls: ['./learner-group-learners-progress.component.css'], }) export class LearnerGroupLearnersProgressComponent implements OnInit { @Input() learnerGroup!: LearnerGroupData; @@ -43,8 +42,7 @@ export class LearnerGroupLearnersProgressComponent implements OnInit { storiesChaptersProgress: ChapterProgressSummary[] = []; constructor( - private learnerGroupSyllabusBackendApiService: - LearnerGroupSyllabusBackendApiService, + private learnerGroupSyllabusBackendApiService: LearnerGroupSyllabusBackendApiService, private navigationService: NavigationService, private storyViewerBackendApiService: StoryViewerBackendApiService, private userService: UserService @@ -54,8 +52,10 @@ export class LearnerGroupLearnersProgressComponent implements OnInit { if (this.learnerGroup.learnerUsernames.length > 0) { this.learnerGroupSyllabusBackendApiService .fetchLearnersProgressInAssignedSyllabus( - this.learnerGroup.id, this.learnerGroup.learnerUsernames - ).then(learnersProgress => { + this.learnerGroup.id, + this.learnerGroup.learnerUsernames + ) + .then(learnersProgress => { this.learnersProgress = learnersProgress; this.matchingUsersProgress = this.learnersProgress; }); @@ -78,10 +78,11 @@ export class LearnerGroupLearnersProgressComponent implements OnInit { getStrugglingSubtopicsCountOfLearner(index: number): number { let strugglingSubtopicsCount = 0; - const subtopicsProgress = ( - this.matchingUsersProgress[index].subtopicsProgress); + const subtopicsProgress = + this.matchingUsersProgress[index].subtopicsProgress; subtopicsProgress.forEach(subtopicProgress => { - if (subtopicProgress.subtopicMastery && + if ( + subtopicProgress.subtopicMastery && subtopicProgress.subtopicMastery < 0.6 ) { strugglingSubtopicsCount += 1; @@ -91,20 +92,16 @@ export class LearnerGroupLearnersProgressComponent implements OnInit { } getProfileImagePngDataUrl(username: string): string { - let [pngImageUrl, _] = this.userService.getProfileImageDataUrl( - username); + let [pngImageUrl, _] = this.userService.getProfileImageDataUrl(username); return pngImageUrl; } getProfileImageWebpDataUrl(username: string): string { - let [_, webpImageUrl] = this.userService.getProfileImageDataUrl( - username); + let [_, webpImageUrl] = this.userService.getProfileImageDataUrl(username); return webpImageUrl; } - activateLearnerSpecificView( - learnerProgress: LearnerGroupUserProgress - ): void { + activateLearnerSpecificView(learnerProgress: LearnerGroupUserProgress): void { this.learnerSpecificProgressViewIsActive = true; this.specificLearnerProgress = learnerProgress; } @@ -118,7 +115,7 @@ export class LearnerGroupLearnersProgressComponent implements OnInit { } updateLearnerSpecificProgress( - learnerProgress: LearnerGroupUserProgress + learnerProgress: LearnerGroupUserProgress ): void { this.specificLearnerProgress = learnerProgress; let syllabusStoryIds: string[] = []; @@ -126,20 +123,24 @@ export class LearnerGroupLearnersProgressComponent implements OnInit { syllabusStoryIds.push(storyProgress.getId()); }); - this.storyViewerBackendApiService.fetchProgressInStoriesChapters( - learnerProgress.username, syllabusStoryIds - ).then(storiesChaptersProgress => { - this.storiesChaptersProgress = storiesChaptersProgress; - }); + this.storyViewerBackendApiService + .fetchProgressInStoriesChapters( + learnerProgress.username, + syllabusStoryIds + ) + .then(storiesChaptersProgress => { + this.storiesChaptersProgress = storiesChaptersProgress; + }); } getSearchUsernameResults(): LearnerGroupUserProgress[] { if (this.searchUsernameQuery === '') { this.matchingUsersProgress = this.learnersProgress; } - this.matchingUsersProgress = this.learnersProgress.filter( - learnerProgress => learnerProgress.username.toLowerCase().includes( - this.searchUsernameQuery.toLocaleLowerCase()) + this.matchingUsersProgress = this.learnersProgress.filter(learnerProgress => + learnerProgress.username + .toLowerCase() + .includes(this.searchUsernameQuery.toLocaleLowerCase()) ); return this.matchingUsersProgress; } diff --git a/core/templates/pages/learner-group-pages/edit-group/learner-group-overview.component.spec.ts b/core/templates/pages/learner-group-pages/edit-group/learner-group-overview.component.spec.ts index 3e44788708c6..5a3e4d65df5b 100644 --- a/core/templates/pages/learner-group-pages/edit-group/learner-group-overview.component.spec.ts +++ b/core/templates/pages/learner-group-pages/edit-group/learner-group-overview.component.spec.ts @@ -16,22 +16,23 @@ * @fileoverview Unit tests for learner group overview component. */ -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick } from - '@angular/core/testing'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { LearnerGroupPagesConstants } from '../learner-group-pages.constants'; -import { LearnerGroupData } from 'domain/learner_group/learner-group.model'; -import { TranslateService } from '@ngx-translate/core'; -import { LearnerGroupOverviewComponent } from - './learner-group-overview.component'; -import { LearnerGroupSyllabusBackendApiService } from - 'domain/learner_group/learner-group-syllabus-backend-api.service'; -import { LearnerGroupUserProgress } from - 'domain/learner_group/learner-group-user-progress.model'; -import { LearnerGroupUserInfo } from 'domain/learner_group/learner-group-user-info.model'; -import { UserService } from 'services/user.service'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {LearnerGroupPagesConstants} from '../learner-group-pages.constants'; +import {LearnerGroupData} from 'domain/learner_group/learner-group.model'; +import {TranslateService} from '@ngx-translate/core'; +import {LearnerGroupOverviewComponent} from './learner-group-overview.component'; +import {LearnerGroupSyllabusBackendApiService} from 'domain/learner_group/learner-group-syllabus-backend-api.service'; +import {LearnerGroupUserProgress} from 'domain/learner_group/learner-group-user-progress.model'; +import {LearnerGroupUserInfo} from 'domain/learner_group/learner-group-user-info.model'; +import {UserService} from 'services/user.service'; class MockTranslateService { onLangChange: EventEmitter = new EventEmitter(); @@ -44,8 +45,7 @@ class MockTranslateService { describe('LearnerGroupOverviewComponent', () => { let component: LearnerGroupOverviewComponent; let fixture: ComponentFixture; - let learnerGroupSyllabusBackendApiService: - LearnerGroupSyllabusBackendApiService; + let learnerGroupSyllabusBackendApiService: LearnerGroupSyllabusBackendApiService; let userService: UserService; const sampleLearnerGroupSubtopicSummaryDict = { @@ -55,7 +55,7 @@ describe('LearnerGroupOverviewComponent', () => { parent_topic_name: 'parentTopicName', thumbnail_filename: 'thumbnailFilename', thumbnail_bg_color: 'red', - subtopic_mastery: 0.5 + subtopic_mastery: 0.5, }; let nodeDict = { @@ -74,7 +74,7 @@ describe('LearnerGroupOverviewComponent', () => { planned_publication_date_msecs: 100, last_modified_msecs: 100, first_publication_date_msecs: 200, - unpublishing_reason: null + unpublishing_reason: null, }; const sampleStorySummaryBackendDict = { id: 'sample_story_id', @@ -89,23 +89,23 @@ describe('LearnerGroupOverviewComponent', () => { all_node_dicts: [nodeDict], topic_name: 'Topic one', topic_url_fragment: 'topic-one', - classroom_url_fragment: 'math' + classroom_url_fragment: 'math', }; const userInfo = LearnerGroupUserInfo.createFromBackendDict({ username: 'username2', - error: '' + error: '', }); const sampleLearnerGroupUserProgDict = { username: 'username2', progress_sharing_is_turned_on: true, stories_progress: [sampleStorySummaryBackendDict], - subtopic_pages_progress: [sampleLearnerGroupSubtopicSummaryDict] + subtopic_pages_progress: [sampleLearnerGroupSubtopicSummaryDict], }; - const sampleLearnerGroupUserProg = ( + const sampleLearnerGroupUserProg = LearnerGroupUserProgress.createFromBackendDict( - sampleLearnerGroupUserProgDict) - ); + sampleLearnerGroupUserProgDict + ); const learnerGroupBackendDict = { id: 'groupId', @@ -115,38 +115,39 @@ describe('LearnerGroupOverviewComponent', () => { learner_usernames: ['username2'], invited_learner_usernames: ['username1'], subtopic_page_ids: ['topicId1:1'], - story_ids: ['sample_story_id'] + story_ids: ['sample_story_id'], }; const learnerGroup = LearnerGroupData.createFromBackendDict( - learnerGroupBackendDict); + learnerGroupBackendDict + ); beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - LearnerGroupOverviewComponent, - MockTranslatePipe - ], + declarations: [LearnerGroupOverviewComponent, MockTranslatePipe], providers: [ { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); beforeEach(() => { learnerGroupSyllabusBackendApiService = TestBed.inject( - LearnerGroupSyllabusBackendApiService); + LearnerGroupSyllabusBackendApiService + ); fixture = TestBed.createComponent(LearnerGroupOverviewComponent); userService = TestBed.inject(UserService); component = fixture.componentInstance; component.learnerGroup = learnerGroup; - spyOn(userService, 'getProfileImageDataUrl').and.returnValue( - ['default-image-url-png', 'default-image-url-webp']); + spyOn(userService, 'getProfileImageDataUrl').and.returnValue([ + 'default-image-url-png', + 'default-image-url-webp', + ]); }); it('should initialize', fakeAsync(() => { @@ -162,7 +163,8 @@ describe('LearnerGroupOverviewComponent', () => { expect(component.activeTab).toEqual( LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_OVERVIEW_SECTIONS - .SKILLS_ANALYSIS); + .SKILLS_ANALYSIS + ); expect(component.learnerGroup).toEqual(learnerGroup); expect(component.learnersProgress).toEqual([sampleLearnerGroupUserProg]); })); @@ -170,25 +172,30 @@ describe('LearnerGroupOverviewComponent', () => { it('should check whether the given tab is active successfully', () => { component.setActiveTab( LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_OVERVIEW_SECTIONS - .SKILLS_ANALYSIS); + .SKILLS_ANALYSIS + ); let tabIsActive = component.isTabActive( LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_OVERVIEW_SECTIONS - .SKILLS_ANALYSIS); + .SKILLS_ANALYSIS + ); expect(tabIsActive).toBeTrue(); tabIsActive = component.isTabActive( LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_OVERVIEW_SECTIONS - .PROGRESS_IN_STORIES); + .PROGRESS_IN_STORIES + ); expect(tabIsActive).toBeFalse(); }); it('should set active tab correctly', () => { component.setActiveTab( - LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_TABS.LEARNERS_PROGRESS); + LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_TABS.LEARNERS_PROGRESS + ); expect(component.activeTab).toEqual( - LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_TABS.LEARNERS_PROGRESS); + LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_TABS.LEARNERS_PROGRESS + ); }); it('should get story completions info correctly', fakeAsync(() => { @@ -204,38 +211,38 @@ describe('LearnerGroupOverviewComponent', () => { expect(component.learnersProgress).toEqual([sampleLearnerGroupUserProg]); - const storyCompletionsInfo = component.getStoryCompletionsInfo( - 'sample_story_id'); + const storyCompletionsInfo = + component.getStoryCompletionsInfo('sample_story_id'); expect(storyCompletionsInfo).toEqual([userInfo]); })); - it('should get info of learners struggling with subtopics correctly', - fakeAsync(() => { - spyOn( - learnerGroupSyllabusBackendApiService, - 'fetchLearnersProgressInAssignedSyllabus' - ).and.returnValue(Promise.resolve([sampleLearnerGroupUserProg])); + it('should get info of learners struggling with subtopics correctly', fakeAsync(() => { + spyOn( + learnerGroupSyllabusBackendApiService, + 'fetchLearnersProgressInAssignedSyllabus' + ).and.returnValue(Promise.resolve([sampleLearnerGroupUserProg])); - component.learnerGroup = learnerGroup; + component.learnerGroup = learnerGroup; - component.ngOnInit(); - tick(100); + component.ngOnInit(); + tick(100); - expect(component.learnersProgress).toEqual([sampleLearnerGroupUserProg]); + expect(component.learnersProgress).toEqual([sampleLearnerGroupUserProg]); - const strugglingLearnersInfo = ( - component.getStrugglingLearnersInfoInSubtopics('topicId1:1')); - expect(strugglingLearnersInfo).toEqual([userInfo]); - }) - ); + const strugglingLearnersInfo = + component.getStrugglingLearnersInfoInSubtopics('topicId1:1'); + expect(strugglingLearnersInfo).toEqual([userInfo]); + })); it('should get user profile image png data url correctly', () => { expect(component.getProfileImagePngDataUrl('username')).toBe( - 'default-image-url-png'); + 'default-image-url-png' + ); }); it('should get user profile image webp data url correctly', () => { expect(component.getProfileImageWebpDataUrl('username')).toBe( - 'default-image-url-webp'); + 'default-image-url-webp' + ); }); }); diff --git a/core/templates/pages/learner-group-pages/edit-group/learner-group-overview.component.ts b/core/templates/pages/learner-group-pages/edit-group/learner-group-overview.component.ts index 2fccdef93147..8232d9fe01aa 100644 --- a/core/templates/pages/learner-group-pages/edit-group/learner-group-overview.component.ts +++ b/core/templates/pages/learner-group-pages/edit-group/learner-group-overview.component.ts @@ -16,37 +16,31 @@ * @fileoverview Component for the learner group overview. */ -import { Component, Input, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { LearnerGroupSyllabusBackendApiService } from - 'domain/learner_group/learner-group-syllabus-backend-api.service'; -import { LearnerGroupUserInfo } from - 'domain/learner_group/learner-group-user-info.model'; -import { LearnerGroupUserProgress } from - 'domain/learner_group/learner-group-user-progress.model'; -import { LearnerGroupData } from 'domain/learner_group/learner-group.model'; -import { UserService } from 'services/user.service'; -import { LearnerGroupPagesConstants } from '../learner-group-pages.constants'; +import {Component, Input, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {LearnerGroupSyllabusBackendApiService} from 'domain/learner_group/learner-group-syllabus-backend-api.service'; +import {LearnerGroupUserInfo} from 'domain/learner_group/learner-group-user-info.model'; +import {LearnerGroupUserProgress} from 'domain/learner_group/learner-group-user-progress.model'; +import {LearnerGroupData} from 'domain/learner_group/learner-group.model'; +import {UserService} from 'services/user.service'; +import {LearnerGroupPagesConstants} from '../learner-group-pages.constants'; import './learner-group-overview.component.css'; - @Component({ selector: 'oppia-learner-group-overview', templateUrl: './learner-group-overview.component.html', - styleUrls: ['./learner-group-overview.component.css'] + styleUrls: ['./learner-group-overview.component.css'], }) export class LearnerGroupOverviewComponent implements OnInit { @Input() learnerGroup!: LearnerGroupData; learnersProgress!: LearnerGroupUserProgress[]; activeTab!: string; - EDIT_OVERVIEW_SECTIONS_I18N_IDS = ( - LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_OVERVIEW_SECTIONS - ); + EDIT_OVERVIEW_SECTIONS_I18N_IDS = + LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_OVERVIEW_SECTIONS; constructor( - private learnerGroupSyllabusBackendApiService: - LearnerGroupSyllabusBackendApiService, + private learnerGroupSyllabusBackendApiService: LearnerGroupSyllabusBackendApiService, private userService: UserService ) {} @@ -55,8 +49,10 @@ export class LearnerGroupOverviewComponent implements OnInit { if (this.learnerGroup && this.learnerGroup.learnerUsernames.length > 0) { this.learnerGroupSyllabusBackendApiService .fetchLearnersProgressInAssignedSyllabus( - this.learnerGroup.id, this.learnerGroup.learnerUsernames - ).then(learnersProgress => { + this.learnerGroup.id, + this.learnerGroup.learnerUsernames + ) + .then(learnersProgress => { this.learnersProgress = learnersProgress; }); } @@ -74,15 +70,13 @@ export class LearnerGroupOverviewComponent implements OnInit { let storyCompletionsInfo: LearnerGroupUserInfo[] = []; this.learnersProgress.forEach(learnerProgress => { learnerProgress.storiesProgress.map(storyProgress => { - if (storyProgress.getId() === storyId && + if ( + storyProgress.getId() === storyId && storyProgress.getCompletedNodeTitles().length === - storyProgress.getNodeTitles().length + storyProgress.getNodeTitles().length ) { storyCompletionsInfo.push( - new LearnerGroupUserInfo( - learnerProgress.username, - '' - ) + new LearnerGroupUserInfo(learnerProgress.username, '') ); } }); @@ -91,20 +85,18 @@ export class LearnerGroupOverviewComponent implements OnInit { } getStrugglingLearnersInfoInSubtopics( - subtopicPageId: string + subtopicPageId: string ): LearnerGroupUserInfo[] { let strugglingLearnerInfo: LearnerGroupUserInfo[] = []; this.learnersProgress.forEach(learnerProgress => { learnerProgress.subtopicsProgress.map(subtopicProgress => { - if (subtopicProgress.subtopicPageId === subtopicPageId && - subtopicProgress.subtopicMastery && - subtopicProgress.subtopicMastery < 0.6 + if ( + subtopicProgress.subtopicPageId === subtopicPageId && + subtopicProgress.subtopicMastery && + subtopicProgress.subtopicMastery < 0.6 ) { strugglingLearnerInfo.push( - new LearnerGroupUserInfo( - learnerProgress.username, - '' - ) + new LearnerGroupUserInfo(learnerProgress.username, '') ); } }); @@ -113,18 +105,19 @@ export class LearnerGroupOverviewComponent implements OnInit { } getProfileImagePngDataUrl(username: string): string { - let [pngImageUrl, _] = this.userService.getProfileImageDataUrl( - username); + let [pngImageUrl, _] = this.userService.getProfileImageDataUrl(username); return pngImageUrl; } getProfileImageWebpDataUrl(username: string): string { - let [_, webpImageUrl] = this.userService.getProfileImageDataUrl( - username); + let [_, webpImageUrl] = this.userService.getProfileImageDataUrl(username); return webpImageUrl; } } -angular.module('oppia').directive( - 'oppiaLearnerGroupOverview', - downgradeComponent({component: LearnerGroupOverviewComponent})); +angular + .module('oppia') + .directive( + 'oppiaLearnerGroupOverview', + downgradeComponent({component: LearnerGroupOverviewComponent}) + ); diff --git a/core/templates/pages/learner-group-pages/edit-group/learner-group-preferences.component.spec.ts b/core/templates/pages/learner-group-pages/edit-group/learner-group-preferences.component.spec.ts index 85ad39c3f7b8..8b6d1548bb8f 100644 --- a/core/templates/pages/learner-group-pages/edit-group/learner-group-preferences.component.spec.ts +++ b/core/templates/pages/learner-group-pages/edit-group/learner-group-preferences.component.spec.ts @@ -16,20 +16,25 @@ * @fileoverview Unit tests for learner group preferences tab. */ -import { NO_ERRORS_SCHEMA, Pipe } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { LearnerGroupData } from 'domain/learner_group/learner-group.model'; -import { LearnerGroupBackendApiService } from 'domain/learner_group/learner-group-backend-api.service'; -import { LearnerGroupPreferencesComponent } from './learner-group-preferences.component'; -import { LearnerGroupPagesConstants } from '../learner-group-pages.constants'; -import { LearnerGroupUserInfo } from 'domain/learner_group/learner-group-user-info.model'; -import { LearnerGroupAllLearnersInfo } from 'domain/learner_group/learner-group-all-learners-info.model'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { LoaderService } from 'services/loader.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { UserService } from 'services/user.service'; +import {NO_ERRORS_SCHEMA, Pipe} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {LearnerGroupData} from 'domain/learner_group/learner-group.model'; +import {LearnerGroupBackendApiService} from 'domain/learner_group/learner-group-backend-api.service'; +import {LearnerGroupPreferencesComponent} from './learner-group-preferences.component'; +import {LearnerGroupPagesConstants} from '../learner-group-pages.constants'; +import {LearnerGroupUserInfo} from 'domain/learner_group/learner-group-user-info.model'; +import {LearnerGroupAllLearnersInfo} from 'domain/learner_group/learner-group-all-learners-info.model'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {LoaderService} from 'services/loader.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {UserService} from 'services/user.service'; @Pipe({name: 'truncate'}) class MockTrunctePipe { @@ -43,7 +48,7 @@ class MockWindowRef { location: { href: '', }, - gtag: () => {} + gtag: () => {}, }; } @@ -64,10 +69,11 @@ describe('LearnerGroupPreferencesComponent', () => { learner_usernames: ['username1'], invited_learner_usernames: ['username2'], subtopic_page_ids: [], - story_ids: ['story_id_1'] + story_ids: ['story_id_1'], }; const learnerGroup = LearnerGroupData.createFromBackendDict( - learnerGroupBackendDict); + learnerGroupBackendDict + ); beforeEach(() => { windowRef = new MockWindowRef(); @@ -76,21 +82,22 @@ describe('LearnerGroupPreferencesComponent', () => { declarations: [ LearnerGroupPreferencesComponent, MockTranslatePipe, - MockTrunctePipe + MockTrunctePipe, ], providers: [ { provide: WindowRef, - useValue: windowRef - } + useValue: windowRef, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); beforeEach(() => { learnerGroupBackendApiService = TestBed.inject( - LearnerGroupBackendApiService); + LearnerGroupBackendApiService + ); ngbModal = TestBed.inject(NgbModal); loaderService = TestBed.inject(LoaderService); fixture = TestBed.createComponent(LearnerGroupPreferencesComponent); @@ -98,27 +105,33 @@ describe('LearnerGroupPreferencesComponent', () => { component = fixture.componentInstance; component.learnerGroup = learnerGroup; - spyOn(userService, 'getProfileImageDataUrl').and.returnValue( - ['default-image-url-png', 'default-image-url-webp']); + spyOn(userService, 'getProfileImageDataUrl').and.returnValue([ + 'default-image-url-png', + 'default-image-url-webp', + ]); }); it('should set active tab and check if tab is active correctly', () => { component.setActiveTab( LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_PREFERENCES_SECTIONS - .GROUP_DETAILS); + .GROUP_DETAILS + ); expect(component.activeTab).toEqual( LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_PREFERENCES_SECTIONS - .GROUP_DETAILS); + .GROUP_DETAILS + ); let tabIsActive = component.isTabActive( LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_PREFERENCES_SECTIONS - .GROUP_DETAILS); + .GROUP_DETAILS + ); expect(tabIsActive).toBeTrue(); tabIsActive = component.isTabActive( LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_PREFERENCES_SECTIONS - .GROUP_LEARNERS); + .GROUP_LEARNERS + ); expect(tabIsActive).toBeFalse(); }); @@ -146,32 +159,38 @@ describe('LearnerGroupPreferencesComponent', () => { it('should initialize', fakeAsync(() => { const learnerInfo1 = LearnerGroupUserInfo.createFromBackendDict({ username: 'username1', - error: '' + error: '', }); const learnerInfo2 = LearnerGroupUserInfo.createFromBackendDict({ username: 'username2', - error: '' + error: '', }); const learnerInfo3 = LearnerGroupUserInfo.createFromBackendDict({ username: 'user3', - error: '' + error: '', }); const allLearnersInfo = LearnerGroupAllLearnersInfo.createFromBackendDict({ - learners_info: [{ - username: 'user3', - error: '' - }], - invited_learners_info: [{ - username: 'username1', - error: '' - }, - { - username: 'username2', - error: '' - }] + learners_info: [ + { + username: 'user3', + error: '', + }, + ], + invited_learners_info: [ + { + username: 'username1', + error: '', + }, + { + username: 'username2', + error: '', + }, + ], }); - spyOn(learnerGroupBackendApiService, 'fetchLearnersInfoAsync') - .and.returnValue(Promise.resolve(allLearnersInfo)); + spyOn( + learnerGroupBackendApiService, + 'fetchLearnersInfoAsync' + ).and.returnValue(Promise.resolve(allLearnersInfo)); expect(component.learnerGroup).toEqual(learnerGroup); @@ -191,13 +210,16 @@ describe('LearnerGroupPreferencesComponent', () => { learner_usernames: ['username1'], invited_learner_usernames: ['username2'], subtopic_page_ids: [], - story_ids: ['story_id_1'] + story_ids: ['story_id_1'], }; const demoLearnerGroup = LearnerGroupData.createFromBackendDict( - demoLearnerGroupBackendDict); + demoLearnerGroupBackendDict + ); - spyOn(learnerGroupBackendApiService, 'updateLearnerGroupAsync') - .and.returnValue(Promise.resolve(learnerGroup)); + spyOn( + learnerGroupBackendApiService, + 'updateLearnerGroupAsync' + ).and.returnValue(Promise.resolve(learnerGroup)); component.learnerGroup = demoLearnerGroup; component.newLearnerGroupTitle = 'title'; @@ -219,12 +241,14 @@ describe('LearnerGroupPreferencesComponent', () => { it('should get user profile image png data url correctly', () => { expect(component.getProfileImagePngDataUrl('username')).toBe( - 'default-image-url-png'); + 'default-image-url-png' + ); }); it('should get user profile image webp data url correctly', () => { expect(component.getProfileImageWebpDataUrl('username')).toBe( - 'default-image-url-webp'); + 'default-image-url-webp' + ); }); it('should open invite learners modal successfully', fakeAsync(() => { @@ -236,34 +260,39 @@ describe('LearnerGroupPreferencesComponent', () => { learner_usernames: [], invited_learner_usernames: ['username1', 'username2'], subtopic_page_ids: [], - story_ids: ['story_id_1'] + story_ids: ['story_id_1'], }; const demoLearnerGroup = LearnerGroupData.createFromBackendDict( - demoLearnerGroupBackendDict); + demoLearnerGroupBackendDict + ); const newLearnerInfo = LearnerGroupUserInfo.createFromBackendDict({ username: 'username1', - error: '' + error: '', }); - spyOn(ngbModal, 'open').and.returnValues({ - componentInstance: { - learnerGroupId: 'groupId' - }, - result: Promise.resolve({ - invitedLearners: ['username1'], - invitedLearnersInfo: [newLearnerInfo] - }) - } as NgbModalRef, - { - componentInstance: { - successMessage: 'message', - invitedUsernames: ['user1', 'user2'] - }, - result: Promise.resolve() - } as NgbModalRef); + spyOn(ngbModal, 'open').and.returnValues( + { + componentInstance: { + learnerGroupId: 'groupId', + }, + result: Promise.resolve({ + invitedLearners: ['username1'], + invitedLearnersInfo: [newLearnerInfo], + }), + } as NgbModalRef, + { + componentInstance: { + successMessage: 'message', + invitedUsernames: ['user1', 'user2'], + }, + result: Promise.resolve(), + } as NgbModalRef + ); - spyOn(learnerGroupBackendApiService, 'updateLearnerGroupAsync') - .and.returnValue(Promise.resolve(learnerGroup)); + spyOn( + learnerGroupBackendApiService, + 'updateLearnerGroupAsync' + ).and.returnValue(Promise.resolve(learnerGroup)); component.learnerGroup = demoLearnerGroup; component.invitedLearnersInfo = []; @@ -276,191 +305,199 @@ describe('LearnerGroupPreferencesComponent', () => { expect(component.invitedLearnersInfo).toEqual([newLearnerInfo]); })); - it('should close invite learners modal successfully', - fakeAsync(() => { - const demoLearnerGroupBackendDict = { - id: 'groupId', - title: 'title', - description: 'description', - facilitator_usernames: ['facilitator_username'], - learner_usernames: ['username1', 'user3'], - invited_learner_usernames: ['username2'], - subtopic_page_ids: [], - story_ids: ['story_id_1'] - }; - const demoLearnerGroup = LearnerGroupData.createFromBackendDict( - demoLearnerGroupBackendDict); - const learnerInfo = LearnerGroupUserInfo.createFromBackendDict({ - username: 'user3', - error: '' - }); - - spyOn(ngbModal, 'open').and.returnValue({ - componentInstance: { - learnerGroupId: 'groupId' - }, - result: Promise.reject() - } as NgbModalRef); + it('should close invite learners modal successfully', fakeAsync(() => { + const demoLearnerGroupBackendDict = { + id: 'groupId', + title: 'title', + description: 'description', + facilitator_usernames: ['facilitator_username'], + learner_usernames: ['username1', 'user3'], + invited_learner_usernames: ['username2'], + subtopic_page_ids: [], + story_ids: ['story_id_1'], + }; + const demoLearnerGroup = LearnerGroupData.createFromBackendDict( + demoLearnerGroupBackendDict + ); + const learnerInfo = LearnerGroupUserInfo.createFromBackendDict({ + username: 'user3', + error: '', + }); - spyOn(learnerGroupBackendApiService, 'updateLearnerGroupAsync') - .and.returnValue(Promise.resolve(demoLearnerGroup)); + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: { + learnerGroupId: 'groupId', + }, + result: Promise.reject(), + } as NgbModalRef); - component.learnerGroup = demoLearnerGroup; - component.currentLearnersInfo = [learnerInfo]; + spyOn( + learnerGroupBackendApiService, + 'updateLearnerGroupAsync' + ).and.returnValue(Promise.resolve(demoLearnerGroup)); - component.openInviteLearnersModal(); - tick(); - fixture.detectChanges(); + component.learnerGroup = demoLearnerGroup; + component.currentLearnersInfo = [learnerInfo]; - expect(component.learnerGroup).toEqual(demoLearnerGroup); - expect(component.currentLearnersInfo).toEqual([learnerInfo]); - }) - ); + component.openInviteLearnersModal(); + tick(); + fixture.detectChanges(); + + expect(component.learnerGroup).toEqual(demoLearnerGroup); + expect(component.currentLearnersInfo).toEqual([learnerInfo]); + })); - it('should close invitation successful modal successfully', - fakeAsync(() => { - const demoLearnerGroupBackendDict = { - id: 'groupId', - title: 'title', - description: 'description', - facilitator_usernames: ['facilitator_username'], - learner_usernames: [], - invited_learner_usernames: ['username1', 'username2'], - subtopic_page_ids: [], - story_ids: ['story_id_1'] - }; - const demoLearnerGroup = LearnerGroupData.createFromBackendDict( - demoLearnerGroupBackendDict); - const newLearnerInfo = LearnerGroupUserInfo.createFromBackendDict({ - username: 'username1', - error: '' - }); - - spyOn(ngbModal, 'open').and.returnValues({ + it('should close invitation successful modal successfully', fakeAsync(() => { + const demoLearnerGroupBackendDict = { + id: 'groupId', + title: 'title', + description: 'description', + facilitator_usernames: ['facilitator_username'], + learner_usernames: [], + invited_learner_usernames: ['username1', 'username2'], + subtopic_page_ids: [], + story_ids: ['story_id_1'], + }; + const demoLearnerGroup = LearnerGroupData.createFromBackendDict( + demoLearnerGroupBackendDict + ); + const newLearnerInfo = LearnerGroupUserInfo.createFromBackendDict({ + username: 'username1', + error: '', + }); + + spyOn(ngbModal, 'open').and.returnValues( + { componentInstance: { - learnerGroupId: 'groupId' + learnerGroupId: 'groupId', }, result: Promise.resolve({ invitedLearners: ['username1'], - invitedLearnersInfo: [newLearnerInfo] - }) + invitedLearnersInfo: [newLearnerInfo], + }), } as NgbModalRef, { componentInstance: { successMessage: 'message', - invitedUsernames: ['user1', 'user2'] + invitedUsernames: ['user1', 'user2'], }, - result: Promise.reject() - } as NgbModalRef); + result: Promise.reject(), + } as NgbModalRef + ); - spyOn(learnerGroupBackendApiService, 'updateLearnerGroupAsync') - .and.returnValue(Promise.resolve(learnerGroup)); + spyOn( + learnerGroupBackendApiService, + 'updateLearnerGroupAsync' + ).and.returnValue(Promise.resolve(learnerGroup)); - component.learnerGroup = demoLearnerGroup; - component.invitedLearnersInfo = []; + component.learnerGroup = demoLearnerGroup; + component.invitedLearnersInfo = []; - component.openInviteLearnersModal(); - tick(); - fixture.detectChanges(); + component.openInviteLearnersModal(); + tick(); + fixture.detectChanges(); - expect(component.learnerGroup).toEqual(learnerGroup); - expect(component.invitedLearnersInfo).toEqual([newLearnerInfo]); - }) - ); + expect(component.learnerGroup).toEqual(learnerGroup); + expect(component.invitedLearnersInfo).toEqual([newLearnerInfo]); + })); - it('should open remove learner from group modal successfully', - fakeAsync(() => { - const demoLearnerGroupBackendDict = { - id: 'groupId', - title: 'title', - description: 'description', - facilitator_usernames: ['facilitator_username'], - learner_usernames: ['username1', 'user3'], - invited_learner_usernames: ['username2'], - subtopic_page_ids: [], - story_ids: ['story_id_1'] - }; - const demoLearnerGroup = LearnerGroupData.createFromBackendDict( - demoLearnerGroupBackendDict); - const learnerInfo = LearnerGroupUserInfo.createFromBackendDict({ - username: 'user3', - error: '' - }); - - spyOn(ngbModal, 'open').and.returnValue({ - componentInstance: { - confirmationTitle: 'Remove Learner', - confirmationMessage: 'Some confirmation message.' - }, - result: Promise.resolve() - } as NgbModalRef); + it('should open remove learner from group modal successfully', fakeAsync(() => { + const demoLearnerGroupBackendDict = { + id: 'groupId', + title: 'title', + description: 'description', + facilitator_usernames: ['facilitator_username'], + learner_usernames: ['username1', 'user3'], + invited_learner_usernames: ['username2'], + subtopic_page_ids: [], + story_ids: ['story_id_1'], + }; + const demoLearnerGroup = LearnerGroupData.createFromBackendDict( + demoLearnerGroupBackendDict + ); + const learnerInfo = LearnerGroupUserInfo.createFromBackendDict({ + username: 'user3', + error: '', + }); - spyOn(learnerGroupBackendApiService, 'updateLearnerGroupAsync') - .and.returnValue(Promise.resolve(learnerGroup)); + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: { + confirmationTitle: 'Remove Learner', + confirmationMessage: 'Some confirmation message.', + }, + result: Promise.resolve(), + } as NgbModalRef); - component.learnerGroup = demoLearnerGroup; - component.currentLearnersInfo = [learnerInfo]; + spyOn( + learnerGroupBackendApiService, + 'updateLearnerGroupAsync' + ).and.returnValue(Promise.resolve(learnerGroup)); - component.openRemoveLearnerFromGroupModal(learnerInfo); - tick(); - fixture.detectChanges(); + component.learnerGroup = demoLearnerGroup; + component.currentLearnersInfo = [learnerInfo]; - expect(component.learnerGroup).toEqual(learnerGroup); - expect(component.currentLearnersInfo).toEqual([]); - }) - ); + component.openRemoveLearnerFromGroupModal(learnerInfo); + tick(); + fixture.detectChanges(); - it('should open withdraw learner invitation modal successfully', - fakeAsync(() => { - const demoLearnerGroupBackendDict = { - id: 'groupId', - title: 'title', - description: 'description', - facilitator_usernames: ['facilitator_username'], - learner_usernames: ['username1'], - invited_learner_usernames: ['username2', 'user3'], - subtopic_page_ids: [], - story_ids: ['story_id_1'] - }; - const demoLearnerGroup = LearnerGroupData.createFromBackendDict( - demoLearnerGroupBackendDict); - const learnerInfo = LearnerGroupUserInfo.createFromBackendDict({ - username: 'user3', - error: '' - }); - - spyOn(ngbModal, 'open').and.returnValue({ - componentInstance: { - confirmationTitle: 'Withdraw Invitation', - confirmationMessage: 'Some confirmation message.' - }, - result: Promise.resolve() - } as NgbModalRef); + expect(component.learnerGroup).toEqual(learnerGroup); + expect(component.currentLearnersInfo).toEqual([]); + })); + + it('should open withdraw learner invitation modal successfully', fakeAsync(() => { + const demoLearnerGroupBackendDict = { + id: 'groupId', + title: 'title', + description: 'description', + facilitator_usernames: ['facilitator_username'], + learner_usernames: ['username1'], + invited_learner_usernames: ['username2', 'user3'], + subtopic_page_ids: [], + story_ids: ['story_id_1'], + }; + const demoLearnerGroup = LearnerGroupData.createFromBackendDict( + demoLearnerGroupBackendDict + ); + const learnerInfo = LearnerGroupUserInfo.createFromBackendDict({ + username: 'user3', + error: '', + }); - spyOn(learnerGroupBackendApiService, 'updateLearnerGroupAsync') - .and.returnValue(Promise.resolve(learnerGroup)); + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: { + confirmationTitle: 'Withdraw Invitation', + confirmationMessage: 'Some confirmation message.', + }, + result: Promise.resolve(), + } as NgbModalRef); - component.learnerGroup = demoLearnerGroup; - component.invitedLearnersInfo = [learnerInfo]; + spyOn( + learnerGroupBackendApiService, + 'updateLearnerGroupAsync' + ).and.returnValue(Promise.resolve(learnerGroup)); - component.openWithdrawLearnerInvitationModal(learnerInfo); - tick(); - fixture.detectChanges(); + component.learnerGroup = demoLearnerGroup; + component.invitedLearnersInfo = [learnerInfo]; - expect(component.learnerGroup).toEqual(learnerGroup); - expect(component.invitedLearnersInfo).toEqual([]); - }) - ); + component.openWithdrawLearnerInvitationModal(learnerInfo); + tick(); + fixture.detectChanges(); + + expect(component.learnerGroup).toEqual(learnerGroup); + expect(component.invitedLearnersInfo).toEqual([]); + })); it('should successfully delete learner group', fakeAsync(() => { spyOn(ngbModal, 'open').and.returnValue({ componentInstance: { learnerGroupTitle: learnerGroup.title, }, - result: Promise.resolve() + result: Promise.resolve(), } as NgbModalRef); - spyOn(learnerGroupBackendApiService, 'deleteLearnerGroupAsync') - .and.returnValue(Promise.resolve(true)); + spyOn( + learnerGroupBackendApiService, + 'deleteLearnerGroupAsync' + ).and.returnValue(Promise.resolve(true)); spyOn(loaderService, 'showLoadingScreen'); component.learnerGroup = learnerGroup; @@ -468,9 +505,9 @@ describe('LearnerGroupPreferencesComponent', () => { component.deleteLearnerGroup(); tick(); - expect(windowRef.nativeWindow.location.href).toBe( - '/facilitator-dashboard'); + expect(windowRef.nativeWindow.location.href).toBe('/facilitator-dashboard'); expect(loaderService.showLoadingScreen).toHaveBeenCalledWith( - 'Deleting Group'); + 'Deleting Group' + ); })); }); diff --git a/core/templates/pages/learner-group-pages/edit-group/learner-group-preferences.component.ts b/core/templates/pages/learner-group-pages/edit-group/learner-group-preferences.component.ts index 203638531def..4573d3907a8d 100644 --- a/core/templates/pages/learner-group-pages/edit-group/learner-group-preferences.component.ts +++ b/core/templates/pages/learner-group-pages/edit-group/learner-group-preferences.component.ts @@ -16,28 +16,26 @@ * @fileoverview Component for the learner group preferences. */ -import { Component, Input, OnInit } from '@angular/core'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { LearnerGroupBackendApiService } from 'domain/learner_group/learner-group-backend-api.service'; -import { LearnerGroupUserInfo } from 'domain/learner_group/learner-group-user-info.model'; -import { LearnerGroupData } from 'domain/learner_group/learner-group.model'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { LoaderService } from 'services/loader.service'; -import { UserService } from 'services/user.service'; -import { LearnerGroupPagesConstants } from '../learner-group-pages.constants'; -import { DeleteLearnerGroupModalComponent } from '../templates/delete-learner-group-modal.component'; -import { InviteLearnersModalComponent } from '../templates/invite-learners-modal.component'; -import { InviteSuccessfulModalComponent } from '../templates/invite-successful-modal.component'; -import { RemoveItemModalComponent } from - '../templates/remove-item-modal.component'; +import {Component, Input, OnInit} from '@angular/core'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {LearnerGroupBackendApiService} from 'domain/learner_group/learner-group-backend-api.service'; +import {LearnerGroupUserInfo} from 'domain/learner_group/learner-group-user-info.model'; +import {LearnerGroupData} from 'domain/learner_group/learner-group.model'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {LoaderService} from 'services/loader.service'; +import {UserService} from 'services/user.service'; +import {LearnerGroupPagesConstants} from '../learner-group-pages.constants'; +import {DeleteLearnerGroupModalComponent} from '../templates/delete-learner-group-modal.component'; +import {InviteLearnersModalComponent} from '../templates/invite-learners-modal.component'; +import {InviteSuccessfulModalComponent} from '../templates/invite-successful-modal.component'; +import {RemoveItemModalComponent} from '../templates/remove-item-modal.component'; import './learner-group-preferences.component.css'; - @Component({ selector: 'oppia-learner-group-preferences', templateUrl: './learner-group-preferences.component.html', - styleUrls: ['./learner-group-preferences.component.css'] + styleUrls: ['./learner-group-preferences.component.css'], }) export class LearnerGroupPreferencesComponent implements OnInit { @Input() learnerGroup!: LearnerGroupData; @@ -48,26 +46,26 @@ export class LearnerGroupPreferencesComponent implements OnInit { invitedLearnersInfo!: LearnerGroupUserInfo[]; currentLearnersInfo!: LearnerGroupUserInfo[]; invitedLearners: string[] = []; - EDIT_PREFERENCES_SECTIONS_I18N_IDS = ( - LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_PREFERENCES_SECTIONS); + EDIT_PREFERENCES_SECTIONS_I18N_IDS = + LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_PREFERENCES_SECTIONS; constructor( private ngbModal: NgbModal, private windowRef: WindowRef, private loaderService: LoaderService, - private learnerGroupBackendApiService: - LearnerGroupBackendApiService, + private learnerGroupBackendApiService: LearnerGroupBackendApiService, private userService: UserService ) {} ngOnInit(): void { this.activeTab = this.EDIT_PREFERENCES_SECTIONS_I18N_IDS.GROUP_DETAILS; if (this.learnerGroup) { - this.learnerGroupBackendApiService.fetchLearnersInfoAsync( - this.learnerGroup.id).then((learnersInfo) => { - this.currentLearnersInfo = learnersInfo.learnersInfo; - this.invitedLearnersInfo = learnersInfo.invitedLearnersInfo; - }); + this.learnerGroupBackendApiService + .fetchLearnersInfoAsync(this.learnerGroup.id) + .then(learnersInfo => { + this.currentLearnersInfo = learnersInfo.learnersInfo; + this.invitedLearnersInfo = learnersInfo.invitedLearnersInfo; + }); } } @@ -97,115 +95,112 @@ export class LearnerGroupPreferencesComponent implements OnInit { saveLearnerGroupInfo(): void { if (this.newLearnerGroupTitle || this.newLearnerGroupDescription) { - this.learnerGroup.title = ( - this.newLearnerGroupTitle ? - this.newLearnerGroupTitle : this.learnerGroup.title - ); - this.learnerGroup.description = ( - this.newLearnerGroupDescription ? - this.newLearnerGroupDescription : this.learnerGroup.description - ); - this.learnerGroupBackendApiService.updateLearnerGroupAsync( - this.learnerGroup).then((learnerGroup) => { - this.learnerGroup = learnerGroup; - }); + this.learnerGroup.title = this.newLearnerGroupTitle + ? this.newLearnerGroupTitle + : this.learnerGroup.title; + this.learnerGroup.description = this.newLearnerGroupDescription + ? this.newLearnerGroupDescription + : this.learnerGroup.description; + this.learnerGroupBackendApiService + .updateLearnerGroupAsync(this.learnerGroup) + .then(learnerGroup => { + this.learnerGroup = learnerGroup; + }); } this.toggleReadOnlyMode(); } openInviteLearnersModal(): void { - let modalRef = this.ngbModal.open( - InviteLearnersModalComponent, - { - backdrop: 'static', - windowClass: 'invite-learners-modal' - } - ); + let modalRef = this.ngbModal.open(InviteLearnersModalComponent, { + backdrop: 'static', + windowClass: 'invite-learners-modal', + }); modalRef.componentInstance.learnerGroupId = this.learnerGroup.id; - modalRef.result.then((data) => { - this.invitedLearners = data.invitedLearners; - this.learnerGroup.inviteLearners(this.invitedLearners); - this.learnerGroupBackendApiService.updateLearnerGroupAsync( - this.learnerGroup).then((learnerGroup) => { - this.learnerGroup = learnerGroup; - this.invitedLearnersInfo.push(...data.invitedLearnersInfo); - }); - let successModalRef = this.ngbModal.open( - InviteSuccessfulModalComponent, - { - backdrop: 'static', - windowClass: 'invite-successful-modal' - } - ); - successModalRef.componentInstance.successMessage = ( - 'An invitation has been sent to '); - successModalRef.componentInstance.invitedUsernames = ( - this.invitedLearners); - - successModalRef.result.then(() => { - // Note to developers: - // This callback is triggered when the Confirm button is clicked. - // No further action is needed. - }, () => { + modalRef.result.then( + data => { + this.invitedLearners = data.invitedLearners; + this.learnerGroup.inviteLearners(this.invitedLearners); + this.learnerGroupBackendApiService + .updateLearnerGroupAsync(this.learnerGroup) + .then(learnerGroup => { + this.learnerGroup = learnerGroup; + this.invitedLearnersInfo.push(...data.invitedLearnersInfo); + }); + let successModalRef = this.ngbModal.open( + InviteSuccessfulModalComponent, + { + backdrop: 'static', + windowClass: 'invite-successful-modal', + } + ); + successModalRef.componentInstance.successMessage = + 'An invitation has been sent to '; + successModalRef.componentInstance.invitedUsernames = + this.invitedLearners; + + successModalRef.result.then( + () => { + // Note to developers: + // This callback is triggered when the Confirm button is clicked. + // No further action is needed. + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); + }, + () => { // Note to developers: // This callback is triggered when the Cancel button is clicked. // No further action is needed. - }); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + } + ); } openRemoveLearnerFromGroupModal(learner: LearnerGroupUserInfo): void { - let modalRef = this.ngbModal.open( - RemoveItemModalComponent, - { - backdrop: 'static', - windowClass: 'remove-learner-modal' - } - ); + let modalRef = this.ngbModal.open(RemoveItemModalComponent, { + backdrop: 'static', + windowClass: 'remove-learner-modal', + }); modalRef.componentInstance.confirmationTitle = 'Remove Learner'; - modalRef.componentInstance.confirmationMessage = ( - 'Are you sure you want to remove this learner from the group?' - ); + modalRef.componentInstance.confirmationMessage = + 'Are you sure you want to remove this learner from the group?'; modalRef.result.then(() => { this.learnerGroup.removeLearner(learner.username); - this.learnerGroupBackendApiService.updateLearnerGroupAsync( - this.learnerGroup).then((learnerGroup) => { - this.learnerGroup = learnerGroup; - this.currentLearnersInfo = this.currentLearnersInfo.filter( - (currentLearner) => currentLearner.username !== learner.username - ); - }); + this.learnerGroupBackendApiService + .updateLearnerGroupAsync(this.learnerGroup) + .then(learnerGroup => { + this.learnerGroup = learnerGroup; + this.currentLearnersInfo = this.currentLearnersInfo.filter( + currentLearner => currentLearner.username !== learner.username + ); + }); }); } openWithdrawLearnerInvitationModal(learner: LearnerGroupUserInfo): void { - let modalRef = this.ngbModal.open( - RemoveItemModalComponent, - { - backdrop: 'static', - windowClass: 'withdraw-learner-invitation-modal' - } - ); + let modalRef = this.ngbModal.open(RemoveItemModalComponent, { + backdrop: 'static', + windowClass: 'withdraw-learner-invitation-modal', + }); modalRef.componentInstance.confirmationTitle = 'Withdraw Invitation'; - modalRef.componentInstance.confirmationMessage = ( - 'Are you sure you want to withdraw this invitation?' - ); + modalRef.componentInstance.confirmationMessage = + 'Are you sure you want to withdraw this invitation?'; modalRef.result.then(() => { this.learnerGroup.revokeInvitation(learner.username); this.invitedLearnersInfo = this.invitedLearnersInfo.filter( - (invitedLearner) => invitedLearner.username !== learner.username + invitedLearner => invitedLearner.username !== learner.username ); - this.learnerGroupBackendApiService.updateLearnerGroupAsync( - this.learnerGroup).then((learnerGroup) => { - this.learnerGroup = learnerGroup; - }); + this.learnerGroupBackendApiService + .updateLearnerGroupAsync(this.learnerGroup) + .then(learnerGroup => { + this.learnerGroup = learnerGroup; + }); }); } @@ -214,38 +209,37 @@ export class LearnerGroupPreferencesComponent implements OnInit { } getProfileImagePngDataUrl(username: string): string { - let [pngImageUrl, _] = this.userService.getProfileImageDataUrl( - username); + let [pngImageUrl, _] = this.userService.getProfileImageDataUrl(username); return pngImageUrl; } getProfileImageWebpDataUrl(username: string): string { - let [_, webpImageUrl] = this.userService.getProfileImageDataUrl( - username); + let [_, webpImageUrl] = this.userService.getProfileImageDataUrl(username); return webpImageUrl; } deleteLearnerGroup(): void { - let modalRef = this.ngbModal.open( - DeleteLearnerGroupModalComponent, - { - backdrop: 'static', - windowClass: 'delete-learner-group-modal' - } - ); + let modalRef = this.ngbModal.open(DeleteLearnerGroupModalComponent, { + backdrop: 'static', + windowClass: 'delete-learner-group-modal', + }); modalRef.componentInstance.learnerGroupTitle = this.learnerGroup.title; - modalRef.result.then(() => { - this.loaderService.showLoadingScreen('Deleting Group'); - this.learnerGroupBackendApiService.deleteLearnerGroupAsync( - this.learnerGroup.id - ).then(() => { - this.windowRef.nativeWindow.location.href = '/facilitator-dashboard'; - }); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + modalRef.result.then( + () => { + this.loaderService.showLoadingScreen('Deleting Group'); + this.learnerGroupBackendApiService + .deleteLearnerGroupAsync(this.learnerGroup.id) + .then(() => { + this.windowRef.nativeWindow.location.href = + '/facilitator-dashboard'; + }); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } } diff --git a/core/templates/pages/learner-group-pages/edit-group/learner-group-syllabus.component.spec.ts b/core/templates/pages/learner-group-pages/edit-group/learner-group-syllabus.component.spec.ts index fdb1da3fc490..548e5fb0c58b 100644 --- a/core/templates/pages/learner-group-pages/edit-group/learner-group-syllabus.component.spec.ts +++ b/core/templates/pages/learner-group-pages/edit-group/learner-group-syllabus.component.spec.ts @@ -16,22 +16,24 @@ * @fileoverview Unit tests for learner group syllabus tab. */ -import { NO_ERRORS_SCHEMA, Pipe } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { LearnerGroupSyllabusBackendApiService } from - 'domain/learner_group/learner-group-syllabus-backend-api.service'; -import { StorySummary } from 'domain/story/story-summary.model'; -import { LearnerGroupSubtopicSummary } from - 'domain/learner_group/learner-group-subtopic-summary.model'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; -import { LearnerGroupSyllabus } from - 'domain/learner_group/learner-group-syllabus.model'; -import { LearnerGroupSyllabusComponent } from './learner-group-syllabus.component'; -import { LearnerGroupData } from 'domain/learner_group/learner-group.model'; -import { LearnerGroupBackendApiService } from 'domain/learner_group/learner-group-backend-api.service'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import {NO_ERRORS_SCHEMA, Pipe} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {LearnerGroupSyllabusBackendApiService} from 'domain/learner_group/learner-group-syllabus-backend-api.service'; +import {StorySummary} from 'domain/story/story-summary.model'; +import {LearnerGroupSubtopicSummary} from 'domain/learner_group/learner-group-subtopic-summary.model'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; +import {LearnerGroupSyllabus} from 'domain/learner_group/learner-group-syllabus.model'; +import {LearnerGroupSyllabusComponent} from './learner-group-syllabus.component'; +import {LearnerGroupData} from 'domain/learner_group/learner-group.model'; +import {LearnerGroupBackendApiService} from 'domain/learner_group/learner-group-backend-api.service'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; @Pipe({name: 'truncate'}) class MockTrunctePipe { @@ -44,8 +46,7 @@ describe('LearnerGroupSyllabusComponent', () => { let component: LearnerGroupSyllabusComponent; let fixture: ComponentFixture; let assetsBackendApiService: AssetsBackendApiService; - let learnerGroupSyllabusBackendApiService: - LearnerGroupSyllabusBackendApiService; + let learnerGroupSyllabusBackendApiService: LearnerGroupSyllabusBackendApiService; let learnerGroupBackendApiService: LearnerGroupBackendApiService; let ngbModal: NgbModal; @@ -56,11 +57,12 @@ describe('LearnerGroupSyllabusComponent', () => { parent_topic_name: 'parentTopicName', thumbnail_filename: 'thumbnailFilename', thumbnail_bg_color: 'red', - subtopic_mastery: 0.5 + subtopic_mastery: 0.5, }; - const sampleLearnerGroupSubtopicSummary = ( + const sampleLearnerGroupSubtopicSummary = LearnerGroupSubtopicSummary.createFromBackendDict( - sampleSubtopicSummaryDict)); + sampleSubtopicSummaryDict + ); const sampleSubtopicSummaryDict2 = { subtopic_id: 0, @@ -69,11 +71,12 @@ describe('LearnerGroupSyllabusComponent', () => { parent_topic_name: 'parentTopicName', thumbnail_filename: 'thumbnailFilename', thumbnail_bg_color: 'red', - subtopic_mastery: 0.6 + subtopic_mastery: 0.6, }; - const sampleLearnerGroupSubtopicSummary2 = ( + const sampleLearnerGroupSubtopicSummary2 = LearnerGroupSubtopicSummary.createFromBackendDict( - sampleSubtopicSummaryDict2)); + sampleSubtopicSummaryDict2 + ); const sampleStorySummaryBackendDict = { id: 'story_id_0', @@ -88,10 +91,11 @@ describe('LearnerGroupSyllabusComponent', () => { all_node_dicts: [], topic_name: 'Topic', classroom_url_fragment: 'math', - topic_url_fragment: 'topic' + topic_url_fragment: 'topic', }; const sampleStorySummary = StorySummary.createFromBackendDict( - sampleStorySummaryBackendDict); + sampleStorySummaryBackendDict + ); const sampleStorySummaryBackendDict2 = { id: 'story_id_1', @@ -106,20 +110,23 @@ describe('LearnerGroupSyllabusComponent', () => { all_node_dicts: [], topic_name: 'Topic', classroom_url_fragment: 'math', - topic_url_fragment: 'topic' + topic_url_fragment: 'topic', }; const mockSyllabusItemsBackendDict = { learner_group_id: 'groupId', story_summary_dicts: [ sampleStorySummaryBackendDict, - sampleStorySummaryBackendDict2], + sampleStorySummaryBackendDict2, + ], subtopic_summary_dicts: [ sampleSubtopicSummaryDict, - sampleSubtopicSummaryDict2] + sampleSubtopicSummaryDict2, + ], }; const mockSyllabusItems = LearnerGroupSyllabus.createFromBackendDict( - mockSyllabusItemsBackendDict); + mockSyllabusItemsBackendDict + ); const learnerGroupBackendDict = { id: 'groupId', @@ -129,10 +136,11 @@ describe('LearnerGroupSyllabusComponent', () => { learner_usernames: [], invited_learner_usernames: ['username1'], subtopic_page_ids: ['topicId1:1'], - story_ids: ['story_id_0'] + story_ids: ['story_id_0'], }; const learnerGroup = LearnerGroupData.createFromBackendDict( - learnerGroupBackendDict); + learnerGroupBackendDict + ); beforeEach(() => { TestBed.configureTestingModule({ @@ -140,18 +148,20 @@ describe('LearnerGroupSyllabusComponent', () => { declarations: [ LearnerGroupSyllabusComponent, MockTranslatePipe, - MockTrunctePipe + MockTrunctePipe, ], providers: [], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); beforeEach(() => { learnerGroupSyllabusBackendApiService = TestBed.inject( - LearnerGroupSyllabusBackendApiService); + LearnerGroupSyllabusBackendApiService + ); learnerGroupBackendApiService = TestBed.inject( - LearnerGroupBackendApiService); + LearnerGroupBackendApiService + ); assetsBackendApiService = TestBed.inject(AssetsBackendApiService); ngbModal = TestBed.inject(NgbModal); fixture = TestBed.createComponent(LearnerGroupSyllabusComponent); @@ -175,7 +185,8 @@ describe('LearnerGroupSyllabusComponent', () => { it('should initialize', fakeAsync(() => { spyOn( - learnerGroupSyllabusBackendApiService, 'fetchLearnerGroupSyllabus' + learnerGroupSyllabusBackendApiService, + 'fetchLearnerGroupSyllabus' ).and.returnValue(Promise.resolve(mockSyllabusItems)); expect(component.learnerGroup).toEqual(learnerGroup); @@ -184,14 +195,20 @@ describe('LearnerGroupSyllabusComponent', () => { expect(component.storySummaries).toEqual(mockSyllabusItems.storySummaries); expect(component.subtopicSummaries).toEqual( - mockSyllabusItems.subtopicPageSummaries); + mockSyllabusItems.subtopicPageSummaries + ); expect(component.displayOrderOfSyllabusItems).toEqual([ - 'story-0', 'story-1', 'subtopic-0', 'subtopic-1']); + 'story-0', + 'story-1', + 'subtopic-0', + 'subtopic-1', + ]); })); it('should get subtopic thumbnail url', () => { - spyOn(assetsBackendApiService, 'getThumbnailUrlForPreview') - .and.returnValue('/topic/thumbnail/url'); + spyOn(assetsBackendApiService, 'getThumbnailUrlForPreview').and.returnValue( + '/topic/thumbnail/url' + ); expect( component.getSubtopicThumbnailUrl(sampleLearnerGroupSubtopicSummary) @@ -199,29 +216,31 @@ describe('LearnerGroupSyllabusComponent', () => { }); it('should get story thumbnail url', () => { - spyOn(assetsBackendApiService, 'getThumbnailUrlForPreview') - .and.returnValue('/story/thumbnail/url'); + spyOn(assetsBackendApiService, 'getThumbnailUrlForPreview').and.returnValue( + '/story/thumbnail/url' + ); expect(component.getStoryThumbnailUrl(sampleStorySummary)).toEqual( - '/story/thumbnail/url'); + '/story/thumbnail/url' + ); }); it('should update newly added subtopic summaries successfully', () => { expect(component.newlyAddedSubtopicSummaries).toEqual([]); - component.updateNewlyAddedSubtopicSummaries( - [sampleLearnerGroupSubtopicSummary]); + component.updateNewlyAddedSubtopicSummaries([ + sampleLearnerGroupSubtopicSummary, + ]); - expect(component.newlyAddedSubtopicSummaries).toEqual( - [sampleLearnerGroupSubtopicSummary]); + expect(component.newlyAddedSubtopicSummaries).toEqual([ + sampleLearnerGroupSubtopicSummary, + ]); }); it('should update newly added story summaries successfully', () => { expect(component.newlyAddedStorySummaries).toEqual([]); - component.updateNewlyAddedStorySummaries( - [sampleStorySummary]); + component.updateNewlyAddedStorySummaries([sampleStorySummary]); - expect(component.newlyAddedStorySummaries).toEqual( - [sampleStorySummary]); + expect(component.newlyAddedStorySummaries).toEqual([sampleStorySummary]); }); it('should update newly added story and subtopic ids successfully', () => { @@ -232,8 +251,10 @@ describe('LearnerGroupSyllabusComponent', () => { component.updateNewlyAddedSubtopicIds(['subtopicId1', 'subtopicId2']); expect(component.newlyAddedStoryIds).toEqual(['storyId', 'storyId2']); - expect(component.newlyAddedSubtopicIds).toEqual( - ['subtopicId1', 'subtopicId2']); + expect(component.newlyAddedSubtopicIds).toEqual([ + 'subtopicId1', + 'subtopicId2', + ]); }); it('should determine if new syllabus was added or not', () => { @@ -262,20 +283,23 @@ describe('LearnerGroupSyllabusComponent', () => { learner_usernames: [], invited_learner_usernames: ['username1'], subtopic_page_ids: [], - story_ids: ['story_id_1'] + story_ids: ['story_id_1'], }; const demoLearnerGroup = LearnerGroupData.createFromBackendDict( - demoLearnerGroupBackendDict); + demoLearnerGroupBackendDict + ); spyOn(ngbModal, 'open').and.returnValue({ componentInstance: { - itemsAddedCount: 2 + itemsAddedCount: 2, }, - result: Promise.resolve() + result: Promise.resolve(), } as NgbModalRef); - spyOn(learnerGroupBackendApiService, 'updateLearnerGroupAsync') - .and.returnValue(Promise.resolve(learnerGroup)); + spyOn( + learnerGroupBackendApiService, + 'updateLearnerGroupAsync' + ).and.returnValue(Promise.resolve(learnerGroup)); component.learnerGroup = demoLearnerGroup; component.subtopicSummaries = []; @@ -291,69 +315,69 @@ describe('LearnerGroupSyllabusComponent', () => { expect(component.newlyAddedStoryIds).toEqual([]); })); - it('should close save new syllabus items modal successfully', - fakeAsync(() => { - spyOn(ngbModal, 'open').and.returnValue({ - componentInstance: { - itemsAddedCount: 2 - }, - result: Promise.reject() - } as NgbModalRef); - - component.learnerGroup = learnerGroup; - component.newlyAddedStoryIds = ['story_id_0']; - component.newlyAddedSubtopicIds = ['topicId1:2']; - - component.saveNewSyllabusItems(); - tick(100); - fixture.detectChanges(); - - expect(component.learnerGroup).toEqual(learnerGroup); - }) - ); + it('should close save new syllabus items modal successfully', fakeAsync(() => { + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: { + itemsAddedCount: 2, + }, + result: Promise.reject(), + } as NgbModalRef); - it('should remove subtopic page id from syllabus successfully', - fakeAsync(() => { - const demoLearnerGroupBackendDict = { - id: 'groupId', - title: 'title', - description: 'description', - facilitator_usernames: ['facilitator_username'], - learner_usernames: [], - invited_learner_usernames: ['username1'], - subtopic_page_ids: ['topicId1:0', 'topicId1:1'], - story_ids: ['story_id_1'] - }; - const demoLearnerGroup = LearnerGroupData.createFromBackendDict( - demoLearnerGroupBackendDict); - - spyOn(ngbModal, 'open').and.returnValue({ - componentInstance: { - confirmationTitle: 'title', - confirmationMessage: 'message', - }, - result: Promise.resolve() - } as NgbModalRef); - - spyOn(learnerGroupBackendApiService, 'updateLearnerGroupAsync') - .and.returnValue(Promise.resolve(learnerGroup)); - - component.learnerGroup = demoLearnerGroup; - component.subtopicSummaries = [ - sampleLearnerGroupSubtopicSummary, - sampleLearnerGroupSubtopicSummary2 - ]; - component.storySummaries = []; - - component.removeSubtopicPageIdFromSyllabus('topicId1:0'); - tick(100); - fixture.detectChanges(); - - expect(component.learnerGroup).toEqual(learnerGroup); - expect(component.subtopicSummaries).toEqual( - [sampleLearnerGroupSubtopicSummary]); - }) - ); + component.learnerGroup = learnerGroup; + component.newlyAddedStoryIds = ['story_id_0']; + component.newlyAddedSubtopicIds = ['topicId1:2']; + + component.saveNewSyllabusItems(); + tick(100); + fixture.detectChanges(); + + expect(component.learnerGroup).toEqual(learnerGroup); + })); + + it('should remove subtopic page id from syllabus successfully', fakeAsync(() => { + const demoLearnerGroupBackendDict = { + id: 'groupId', + title: 'title', + description: 'description', + facilitator_usernames: ['facilitator_username'], + learner_usernames: [], + invited_learner_usernames: ['username1'], + subtopic_page_ids: ['topicId1:0', 'topicId1:1'], + story_ids: ['story_id_1'], + }; + const demoLearnerGroup = LearnerGroupData.createFromBackendDict( + demoLearnerGroupBackendDict + ); + + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: { + confirmationTitle: 'title', + confirmationMessage: 'message', + }, + result: Promise.resolve(), + } as NgbModalRef); + + spyOn( + learnerGroupBackendApiService, + 'updateLearnerGroupAsync' + ).and.returnValue(Promise.resolve(learnerGroup)); + + component.learnerGroup = demoLearnerGroup; + component.subtopicSummaries = [ + sampleLearnerGroupSubtopicSummary, + sampleLearnerGroupSubtopicSummary2, + ]; + component.storySummaries = []; + + component.removeSubtopicPageIdFromSyllabus('topicId1:0'); + tick(100); + fixture.detectChanges(); + + expect(component.learnerGroup).toEqual(learnerGroup); + expect(component.subtopicSummaries).toEqual([ + sampleLearnerGroupSubtopicSummary, + ]); + })); it('should remove story id from syllabus successfully', fakeAsync(() => { const demoLearnerGroupBackendDict = { @@ -364,26 +388,27 @@ describe('LearnerGroupSyllabusComponent', () => { learner_usernames: [], invited_learner_usernames: ['username1'], subtopic_page_ids: ['topicId1:1'], - story_ids: ['story_id_0', 'story_id_1'] + story_ids: ['story_id_0', 'story_id_1'], }; const demoLearnerGroup = LearnerGroupData.createFromBackendDict( - demoLearnerGroupBackendDict); + demoLearnerGroupBackendDict + ); spyOn(ngbModal, 'open').and.returnValue({ componentInstance: { confirmationTitle: 'title', confirmationMessage: 'message', }, - result: Promise.resolve() + result: Promise.resolve(), } as NgbModalRef); - spyOn(learnerGroupBackendApiService, 'updateLearnerGroupAsync') - .and.returnValue(Promise.resolve(learnerGroup)); + spyOn( + learnerGroupBackendApiService, + 'updateLearnerGroupAsync' + ).and.returnValue(Promise.resolve(learnerGroup)); component.learnerGroup = demoLearnerGroup; - component.storySummaries = [ - sampleStorySummary - ]; + component.storySummaries = [sampleStorySummary]; component.subtopicSummaries = []; component.removeStoryIdFromSyllabus('story_id_1'); @@ -393,43 +418,39 @@ describe('LearnerGroupSyllabusComponent', () => { expect(component.learnerGroup).toEqual(learnerGroup); })); - it('should close remove story from syllabus modal successfully', - fakeAsync(() => { - spyOn(ngbModal, 'open').and.returnValue({ - componentInstance: { - confirmationTitle: 'title', - confirmationMessage: 'message', - }, - result: Promise.reject() - } as NgbModalRef); + it('should close remove story from syllabus modal successfully', fakeAsync(() => { + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: { + confirmationTitle: 'title', + confirmationMessage: 'message', + }, + result: Promise.reject(), + } as NgbModalRef); - component.learnerGroup = learnerGroup; + component.learnerGroup = learnerGroup; - component.removeStoryIdFromSyllabus('story_id_0'); - tick(100); - fixture.detectChanges(); + component.removeStoryIdFromSyllabus('story_id_0'); + tick(100); + fixture.detectChanges(); - expect(component.learnerGroup).toEqual(learnerGroup); - }) - ); + expect(component.learnerGroup).toEqual(learnerGroup); + })); - it('should close remove subtopic from syllabus modal successfully', - fakeAsync(() => { - spyOn(ngbModal, 'open').and.returnValue({ - componentInstance: { - confirmationTitle: 'title', - confirmationMessage: 'message', - }, - result: Promise.reject() - } as NgbModalRef); + it('should close remove subtopic from syllabus modal successfully', fakeAsync(() => { + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: { + confirmationTitle: 'title', + confirmationMessage: 'message', + }, + result: Promise.reject(), + } as NgbModalRef); - component.learnerGroup = learnerGroup; + component.learnerGroup = learnerGroup; - component.removeSubtopicPageIdFromSyllabus('topicId1:1'); - tick(100); - fixture.detectChanges(); + component.removeSubtopicPageIdFromSyllabus('topicId1:1'); + tick(100); + fixture.detectChanges(); - expect(component.learnerGroup).toEqual(learnerGroup); - }) - ); + expect(component.learnerGroup).toEqual(learnerGroup); + })); }); diff --git a/core/templates/pages/learner-group-pages/edit-group/learner-group-syllabus.component.ts b/core/templates/pages/learner-group-pages/edit-group/learner-group-syllabus.component.ts index 88c7c08ac98e..fa1c7d33d7e2 100644 --- a/core/templates/pages/learner-group-pages/edit-group/learner-group-syllabus.component.ts +++ b/core/templates/pages/learner-group-pages/edit-group/learner-group-syllabus.component.ts @@ -16,26 +16,24 @@ * @fileoverview Component for the learner group syllabus. */ -import { Component, Input, OnInit } from '@angular/core'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { AppConstants } from 'app.constants'; -import { LearnerGroupBackendApiService } from 'domain/learner_group/learner-group-backend-api.service'; -import { LearnerGroupSubtopicSummary } from 'domain/learner_group/learner-group-subtopic-summary.model'; -import { LearnerGroupSyllabusBackendApiService } from 'domain/learner_group/learner-group-syllabus-backend-api.service'; -import { LearnerGroupData } from 'domain/learner_group/learner-group.model'; -import { StorySummary } from 'domain/story/story-summary.model'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; -import { SyllabusAdditionSuccessModalComponent } from '../templates/syllabus-addition-success-modal.component'; -import { RemoveItemModalComponent } from - '../templates/remove-item-modal.component'; +import {Component, Input, OnInit} from '@angular/core'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {AppConstants} from 'app.constants'; +import {LearnerGroupBackendApiService} from 'domain/learner_group/learner-group-backend-api.service'; +import {LearnerGroupSubtopicSummary} from 'domain/learner_group/learner-group-subtopic-summary.model'; +import {LearnerGroupSyllabusBackendApiService} from 'domain/learner_group/learner-group-syllabus-backend-api.service'; +import {LearnerGroupData} from 'domain/learner_group/learner-group.model'; +import {StorySummary} from 'domain/story/story-summary.model'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; +import {SyllabusAdditionSuccessModalComponent} from '../templates/syllabus-addition-success-modal.component'; +import {RemoveItemModalComponent} from '../templates/remove-item-modal.component'; import './learner-group-syllabus.component.css'; - @Component({ selector: 'oppia-learner-group-syllabus', templateUrl: './learner-group-syllabus.component.html', - styleUrls: ['./learner-group-syllabus.component.css'] + styleUrls: ['./learner-group-syllabus.component.css'], }) export class LearnerGroupSyllabusComponent implements OnInit { @Input() learnerGroup!: LearnerGroupData; @@ -51,20 +49,19 @@ export class LearnerGroupSyllabusComponent implements OnInit { constructor( private assetsBackendApiService: AssetsBackendApiService, private ngbModal: NgbModal, - private learnerGroupBackendApiService: - LearnerGroupBackendApiService, - private learnerGroupSyllabusBackendApiService: - LearnerGroupSyllabusBackendApiService + private learnerGroupBackendApiService: LearnerGroupBackendApiService, + private learnerGroupSyllabusBackendApiService: LearnerGroupSyllabusBackendApiService ) {} ngOnInit(): void { if (this.learnerGroup) { - this.learnerGroupSyllabusBackendApiService.fetchLearnerGroupSyllabus( - this.learnerGroup.id).then(groupSyllabus => { - this.subtopicSummaries = groupSyllabus.subtopicPageSummaries; - this.storySummaries = groupSyllabus.storySummaries; - this.setDisplayOrderOfSyllabusItems(); - }); + this.learnerGroupSyllabusBackendApiService + .fetchLearnerGroupSyllabus(this.learnerGroup.id) + .then(groupSyllabus => { + this.subtopicSummaries = groupSyllabus.subtopicPageSummaries; + this.storySummaries = groupSyllabus.storySummaries; + this.setDisplayOrderOfSyllabusItems(); + }); } } @@ -108,15 +105,14 @@ export class LearnerGroupSyllabusComponent implements OnInit { } getSubtopicThumbnailUrl( - subtopicSummary: LearnerGroupSubtopicSummary + subtopicSummary: LearnerGroupSubtopicSummary ): string { let thumbnailUrl = ''; if (subtopicSummary.thumbnailFilename) { - thumbnailUrl = ( - this.assetsBackendApiService.getThumbnailUrlForPreview( - AppConstants.ENTITY_TYPE.TOPIC, subtopicSummary.parentTopicId, - subtopicSummary.thumbnailFilename - ) + thumbnailUrl = this.assetsBackendApiService.getThumbnailUrlForPreview( + AppConstants.ENTITY_TYPE.TOPIC, + subtopicSummary.parentTopicId, + subtopicSummary.thumbnailFilename ); } return thumbnailUrl; @@ -125,20 +121,18 @@ export class LearnerGroupSyllabusComponent implements OnInit { getStoryThumbnailUrl(storySummary: StorySummary): string { let thumbnailUrl = ''; if (storySummary.getThumbnailFilename()) { - thumbnailUrl = ( - this.assetsBackendApiService.getThumbnailUrlForPreview( - AppConstants.ENTITY_TYPE.STORY, storySummary.getId(), - storySummary.getThumbnailFilename() - ) + thumbnailUrl = this.assetsBackendApiService.getThumbnailUrlForPreview( + AppConstants.ENTITY_TYPE.STORY, + storySummary.getId(), + storySummary.getThumbnailFilename() ); } return thumbnailUrl; } toggleAddNewSyllabusItemsMode(): void { - this.addNewSyllabusItemsModeIsActive = ( - !this.addNewSyllabusItemsModeIsActive - ); + this.addNewSyllabusItemsModeIsActive = + !this.addNewSyllabusItemsModeIsActive; } isAddNewSyllabusItemsModeActive(): boolean { @@ -165,7 +159,7 @@ export class LearnerGroupSyllabusComponent implements OnInit { } updateNewlyAddedSubtopicSummaries( - subtopicSummaries: LearnerGroupSubtopicSummary[] + subtopicSummaries: LearnerGroupSubtopicSummary[] ): void { this.newlyAddedSubtopicSummaries = subtopicSummaries; } @@ -173,86 +167,94 @@ export class LearnerGroupSyllabusComponent implements OnInit { saveNewSyllabusItems(): void { this.learnerGroup.addStoryIds(this.newlyAddedStoryIds); this.learnerGroup.addSubtopicPageIds(this.newlyAddedSubtopicIds); - this.learnerGroupBackendApiService.updateLearnerGroupAsync( - this.learnerGroup).then((learnerGroup) => { - this.subtopicSummaries.push(...this.newlyAddedSubtopicSummaries); - this.storySummaries.push(...this.newlyAddedStorySummaries); - this.newlyAddedStoryIds = []; - this.newlyAddedSubtopicIds = []; - this.newlyAddedStorySummaries = []; - this.newlyAddedSubtopicSummaries = []; - this.learnerGroup = learnerGroup; - this.setDisplayOrderOfSyllabusItems(); - }); - - let modelRef = this.ngbModal.open( - SyllabusAdditionSuccessModalComponent, { - backdrop: true, - windowClass: 'added-syllabus-items-successfully-modal' + this.learnerGroupBackendApiService + .updateLearnerGroupAsync(this.learnerGroup) + .then(learnerGroup => { + this.subtopicSummaries.push(...this.newlyAddedSubtopicSummaries); + this.storySummaries.push(...this.newlyAddedStorySummaries); + this.newlyAddedStoryIds = []; + this.newlyAddedSubtopicIds = []; + this.newlyAddedStorySummaries = []; + this.newlyAddedSubtopicSummaries = []; + this.learnerGroup = learnerGroup; + this.setDisplayOrderOfSyllabusItems(); }); - modelRef.componentInstance.itemsAddedCount = ( - this.newlyAddedStoryIds.length + this.newlyAddedSubtopicIds.length - ); - modelRef.result.then(() => { - this.toggleAddNewSyllabusItemsMode(); - }, () => { - this.ngOnInit(); + let modelRef = this.ngbModal.open(SyllabusAdditionSuccessModalComponent, { + backdrop: true, + windowClass: 'added-syllabus-items-successfully-modal', }); + + modelRef.componentInstance.itemsAddedCount = + this.newlyAddedStoryIds.length + this.newlyAddedSubtopicIds.length; + modelRef.result.then( + () => { + this.toggleAddNewSyllabusItemsMode(); + }, + () => { + this.ngOnInit(); + } + ); } removeSubtopicPageIdFromSyllabus(subtopicPageId: string): void { - let modalRef = this.ngbModal.open( - RemoveItemModalComponent, { - backdrop: true, - windowClass: 'remove-syllabus-item-modal' - }); + let modalRef = this.ngbModal.open(RemoveItemModalComponent, { + backdrop: true, + windowClass: 'remove-syllabus-item-modal', + }); modalRef.componentInstance.confirmationTitle = 'Remove Skill'; - modalRef.componentInstance.confirmationMessage = ( - 'Are you sure you want to remove this item from the syllabus?' - ); + modalRef.componentInstance.confirmationMessage = + 'Are you sure you want to remove this item from the syllabus?'; - modalRef.result.then(() => { - this.learnerGroup.removeSubtopicPageId(subtopicPageId); - this.learnerGroupBackendApiService.updateLearnerGroupAsync( - this.learnerGroup).then((learnerGroup) => { - this.subtopicSummaries = this.subtopicSummaries.filter( - subtopicSummary => subtopicSummary.subtopicPageId !== subtopicPageId - ); - this.learnerGroup = learnerGroup; - this.setDisplayOrderOfSyllabusItems(); - }); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + modalRef.result.then( + () => { + this.learnerGroup.removeSubtopicPageId(subtopicPageId); + this.learnerGroupBackendApiService + .updateLearnerGroupAsync(this.learnerGroup) + .then(learnerGroup => { + this.subtopicSummaries = this.subtopicSummaries.filter( + subtopicSummary => + subtopicSummary.subtopicPageId !== subtopicPageId + ); + this.learnerGroup = learnerGroup; + this.setDisplayOrderOfSyllabusItems(); + }); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } removeStoryIdFromSyllabus(storyId: string): void { - let modalRef = this.ngbModal.open( - RemoveItemModalComponent, { - backdrop: true, - windowClass: 'remove-syllabus-item-modal' - }); + let modalRef = this.ngbModal.open(RemoveItemModalComponent, { + backdrop: true, + windowClass: 'remove-syllabus-item-modal', + }); modalRef.componentInstance.confirmationTitle = 'Remove Lesson'; - modalRef.componentInstance.confirmationMessage = ( - 'Are you sure you want to remove this item from the syllabus?' - ); + modalRef.componentInstance.confirmationMessage = + 'Are you sure you want to remove this item from the syllabus?'; - modalRef.result.then(() => { - this.learnerGroup.removeStoryId(storyId); - this.learnerGroupBackendApiService.updateLearnerGroupAsync( - this.learnerGroup).then((learnerGroup) => { - this.storySummaries = this.storySummaries.filter( - storySummary => storySummary.getId() !== storyId); - this.learnerGroup = learnerGroup; - this.setDisplayOrderOfSyllabusItems(); - }); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + modalRef.result.then( + () => { + this.learnerGroup.removeStoryId(storyId); + this.learnerGroupBackendApiService + .updateLearnerGroupAsync(this.learnerGroup) + .then(learnerGroup => { + this.storySummaries = this.storySummaries.filter( + storySummary => storySummary.getId() !== storyId + ); + this.learnerGroup = learnerGroup; + this.setDisplayOrderOfSyllabusItems(); + }); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } } diff --git a/core/templates/pages/learner-group-pages/learner-group-pages.constants.ajs.ts b/core/templates/pages/learner-group-pages/learner-group-pages.constants.ajs.ts index 9b8b18b6c077..9d65d04fc173 100644 --- a/core/templates/pages/learner-group-pages/learner-group-pages.constants.ajs.ts +++ b/core/templates/pages/learner-group-pages/learner-group-pages.constants.ajs.ts @@ -18,28 +18,46 @@ // TODO(#7092): Delete this file once migration is complete and these AngularJS // equivalents of the Angular constants are no longer needed. -import { LearnerGroupPagesConstants } from './learner-group-pages.constants'; - -angular.module('oppia').constant( - 'LEARNER_GROUP_CREATION_SECTION_I18N_IDS', - LearnerGroupPagesConstants.LEARNER_GROUP_CREATION_SECTION_I18N_IDS); - -angular.module('oppia').constant( - 'CREATE_LEARNER_GROUP_PAGE_URL', - LearnerGroupPagesConstants.CREATE_LEARNER_GROUP_PAGE_URL); - -angular.module('oppia').constant( - 'EDIT_LEARNER_GROUP_PAGE_URL', - LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_PAGE_URL); - -angular.module('oppia').constant( - 'EDIT_LEARNER_GROUP_TABS', - LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_TABS); - -angular.module('oppia').constant( - 'EDIT_LEARNER_GROUP_PREFERENCES_SECTIONS', - LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_PREFERENCES_SECTIONS); - -angular.module('oppia').constant( - 'EDIT_LEARNER_GROUP_OVERVIEW_SECTIONS', - LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_OVERVIEW_SECTIONS); +import {LearnerGroupPagesConstants} from './learner-group-pages.constants'; + +angular + .module('oppia') + .constant( + 'LEARNER_GROUP_CREATION_SECTION_I18N_IDS', + LearnerGroupPagesConstants.LEARNER_GROUP_CREATION_SECTION_I18N_IDS + ); + +angular + .module('oppia') + .constant( + 'CREATE_LEARNER_GROUP_PAGE_URL', + LearnerGroupPagesConstants.CREATE_LEARNER_GROUP_PAGE_URL + ); + +angular + .module('oppia') + .constant( + 'EDIT_LEARNER_GROUP_PAGE_URL', + LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_PAGE_URL + ); + +angular + .module('oppia') + .constant( + 'EDIT_LEARNER_GROUP_TABS', + LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_TABS + ); + +angular + .module('oppia') + .constant( + 'EDIT_LEARNER_GROUP_PREFERENCES_SECTIONS', + LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_PREFERENCES_SECTIONS + ); + +angular + .module('oppia') + .constant( + 'EDIT_LEARNER_GROUP_OVERVIEW_SECTIONS', + LearnerGroupPagesConstants.EDIT_LEARNER_GROUP_OVERVIEW_SECTIONS + ); diff --git a/core/templates/pages/learner-group-pages/learner-group-pages.constants.ts b/core/templates/pages/learner-group-pages/learner-group-pages.constants.ts index 25157b229909..7450f5f63b84 100644 --- a/core/templates/pages/learner-group-pages/learner-group-pages.constants.ts +++ b/core/templates/pages/learner-group-pages/learner-group-pages.constants.ts @@ -31,7 +31,7 @@ export const LearnerGroupPagesConstants = { OVERVIEW: 'I18N_LEARNER_GROUP_OVERVIEW_TAB', LEARNERS_PROGRESS: 'I18N_LEARNER_GROUP_LEARNERS_PROGRESS_TAB', SYLLABUS: 'I18N_LEARNER_GROUP_SYLLABUS_TAB', - PREFERENCES: 'I18N_LEARNER_GROUP_PREFERENCES_TAB' + PREFERENCES: 'I18N_LEARNER_GROUP_PREFERENCES_TAB', }, EDIT_LEARNER_GROUP_PREFERENCES_SECTIONS: { @@ -46,6 +46,6 @@ export const LearnerGroupPagesConstants = { VIEW_LEARNER_GROUP_TABS: { OVERVIEW: 'I18N_LEARNER_GROUP_OVERVIEW_TAB', - ASSIGNED_SYLLABUS: 'I18N_LEARNER_GROUP_ASSIGNED_SYLLABUS_TAB' - } + ASSIGNED_SYLLABUS: 'I18N_LEARNER_GROUP_ASSIGNED_SYLLABUS_TAB', + }, } as const; diff --git a/core/templates/pages/learner-group-pages/shared-learner-group-component.module.ts b/core/templates/pages/learner-group-pages/shared-learner-group-component.module.ts index bb977e703a74..5f6616d036cb 100644 --- a/core/templates/pages/learner-group-pages/shared-learner-group-component.module.ts +++ b/core/templates/pages/learner-group-pages/shared-learner-group-component.module.ts @@ -16,40 +16,37 @@ * @fileoverview Module for the shared learner group components. */ -import { NgModule } from '@angular/core'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { CommonModule } from '@angular/common'; -import { AddSyllabusItemsComponent } from './create-group/add-syllabus-items.component'; -import { InviteLearnersComponent } from './create-group/invite-learners.component'; -import { LearnerGroupDetailsComponent } from './create-group/learner-group-details.component'; -import { LearnerGroupOverviewComponent } from './edit-group/learner-group-overview.component'; -import { LearnerGroupLearnerSpecificProgressComponent } from './edit-group/learner-group-learner-specific-progress.component'; +import {NgModule} from '@angular/core'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {CommonModule} from '@angular/common'; +import {AddSyllabusItemsComponent} from './create-group/add-syllabus-items.component'; +import {InviteLearnersComponent} from './create-group/invite-learners.component'; +import {LearnerGroupDetailsComponent} from './create-group/learner-group-details.component'; +import {LearnerGroupOverviewComponent} from './edit-group/learner-group-overview.component'; +import {LearnerGroupLearnerSpecificProgressComponent} from './edit-group/learner-group-learner-specific-progress.component'; @NgModule({ - imports: [ - CommonModule, - SharedComponentsModule - ], + imports: [CommonModule, SharedComponentsModule], declarations: [ LearnerGroupDetailsComponent, LearnerGroupOverviewComponent, LearnerGroupLearnerSpecificProgressComponent, AddSyllabusItemsComponent, - InviteLearnersComponent + InviteLearnersComponent, ], entryComponents: [ LearnerGroupDetailsComponent, LearnerGroupOverviewComponent, LearnerGroupLearnerSpecificProgressComponent, AddSyllabusItemsComponent, - InviteLearnersComponent + InviteLearnersComponent, ], exports: [ LearnerGroupDetailsComponent, LearnerGroupOverviewComponent, LearnerGroupLearnerSpecificProgressComponent, AddSyllabusItemsComponent, - InviteLearnersComponent - ] + InviteLearnersComponent, + ], }) export class SharedLearnerGroupComponentsModule {} diff --git a/core/templates/pages/learner-group-pages/templates/delete-learner-group-modal.component.spec.ts b/core/templates/pages/learner-group-pages/templates/delete-learner-group-modal.component.spec.ts index fb8785a13f72..a606cfcd42ac 100644 --- a/core/templates/pages/learner-group-pages/templates/delete-learner-group-modal.component.spec.ts +++ b/core/templates/pages/learner-group-pages/templates/delete-learner-group-modal.component.spec.ts @@ -12,16 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Unit tests for the delete learner group modal component. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { DeleteLearnerGroupModalComponent } from './delete-learner-group-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {DeleteLearnerGroupModalComponent} from './delete-learner-group-modal.component'; class MockActiveModal { close(): void { @@ -33,21 +32,20 @@ class MockActiveModal { } } -describe('Delete Learner Group Modal Component', function() { +describe('Delete Learner Group Modal Component', function () { let component: DeleteLearnerGroupModalComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - DeleteLearnerGroupModalComponent, - MockTranslatePipe + declarations: [DeleteLearnerGroupModalComponent, MockTranslatePipe], + providers: [ + { + provide: NgbActiveModal, + useClass: MockActiveModal, + }, ], - providers: [{ - provide: NgbActiveModal, - useClass: MockActiveModal - }], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); diff --git a/core/templates/pages/learner-group-pages/templates/delete-learner-group-modal.component.ts b/core/templates/pages/learner-group-pages/templates/delete-learner-group-modal.component.ts index 6aca7b35b8cb..fc4c437aa065 100644 --- a/core/templates/pages/learner-group-pages/templates/delete-learner-group-modal.component.ts +++ b/core/templates/pages/learner-group-pages/templates/delete-learner-group-modal.component.ts @@ -16,20 +16,18 @@ * @fileoverview Component for Delete Learner Group modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-delete-learner-group-modal', - templateUrl: './delete-learner-group-modal.component.html' + templateUrl: './delete-learner-group-modal.component.html', }) export class DeleteLearnerGroupModalComponent extends ConfirmOrCancelModal { learnerGroupTitle!: string; - constructor( - private ngbActiveModal: NgbActiveModal, - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } } diff --git a/core/templates/pages/learner-group-pages/templates/exit-learner-group-modal.component.spec.ts b/core/templates/pages/learner-group-pages/templates/exit-learner-group-modal.component.spec.ts index c010667d145e..081a18e1aca9 100644 --- a/core/templates/pages/learner-group-pages/templates/exit-learner-group-modal.component.spec.ts +++ b/core/templates/pages/learner-group-pages/templates/exit-learner-group-modal.component.spec.ts @@ -12,16 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Unit tests for the exit learner group modal component. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { ExitLearnerGroupModalComponent } from './exit-learner-group-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {ExitLearnerGroupModalComponent} from './exit-learner-group-modal.component'; class MockActiveModal { close(): void { @@ -33,21 +32,20 @@ class MockActiveModal { } } -describe('Exit Learner Group Modal Component', function() { +describe('Exit Learner Group Modal Component', function () { let component: ExitLearnerGroupModalComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - ExitLearnerGroupModalComponent, - MockTranslatePipe + declarations: [ExitLearnerGroupModalComponent, MockTranslatePipe], + providers: [ + { + provide: NgbActiveModal, + useClass: MockActiveModal, + }, ], - providers: [{ - provide: NgbActiveModal, - useClass: MockActiveModal - }], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); diff --git a/core/templates/pages/learner-group-pages/templates/exit-learner-group-modal.component.ts b/core/templates/pages/learner-group-pages/templates/exit-learner-group-modal.component.ts index 03d276a034c5..d68828c54b62 100644 --- a/core/templates/pages/learner-group-pages/templates/exit-learner-group-modal.component.ts +++ b/core/templates/pages/learner-group-pages/templates/exit-learner-group-modal.component.ts @@ -16,20 +16,18 @@ * @fileoverview Component for exit learner group modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-exit-learner-group-modal', - templateUrl: './exit-learner-group-modal.component.html' + templateUrl: './exit-learner-group-modal.component.html', }) export class ExitLearnerGroupModalComponent extends ConfirmOrCancelModal { learnerGroupTitle!: string; - constructor( - private ngbActiveModal: NgbActiveModal, - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } } diff --git a/core/templates/pages/learner-group-pages/templates/invite-learners-modal.component.spec.ts b/core/templates/pages/learner-group-pages/templates/invite-learners-modal.component.spec.ts index 1a083491c1b1..558469d98a12 100644 --- a/core/templates/pages/learner-group-pages/templates/invite-learners-modal.component.spec.ts +++ b/core/templates/pages/learner-group-pages/templates/invite-learners-modal.component.spec.ts @@ -12,17 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Unit tests for the invite learners modal component. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { LearnerGroupUserInfo } from 'domain/learner_group/learner-group-user-info.model'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { InviteLearnersModalComponent } from './invite-learners-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {LearnerGroupUserInfo} from 'domain/learner_group/learner-group-user-info.model'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {InviteLearnersModalComponent} from './invite-learners-modal.component'; class MockActiveModal { close(): void { @@ -34,22 +33,21 @@ class MockActiveModal { } } -describe('Invite Learners Modal Component', function() { +describe('Invite Learners Modal Component', function () { let component: InviteLearnersModalComponent; let fixture: ComponentFixture; let ngbActiveModal: NgbActiveModal; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - InviteLearnersModalComponent, - MockTranslatePipe + declarations: [InviteLearnersModalComponent, MockTranslatePipe], + providers: [ + { + provide: NgbActiveModal, + useClass: MockActiveModal, + }, ], - providers: [{ - provide: NgbActiveModal, - useClass: MockActiveModal - }], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -70,14 +68,14 @@ describe('Invite Learners Modal Component', function() { component.confirm(); expect(ngbActiveModal.close).toHaveBeenCalledWith({ invitedLearners: component.invitedLearners, - invitedLearnersInfo: component.invitedLearnersInfo + invitedLearnersInfo: component.invitedLearnersInfo, }); }); it('should update newly invited learners and their info', () => { const learnerInfo = LearnerGroupUserInfo.createFromBackendDict({ username: 'user1', - error: '' + error: '', }); expect(component.invitedLearners).toEqual([]); diff --git a/core/templates/pages/learner-group-pages/templates/invite-learners-modal.component.ts b/core/templates/pages/learner-group-pages/templates/invite-learners-modal.component.ts index 542d5e23b2c0..a6d7f054d731 100644 --- a/core/templates/pages/learner-group-pages/templates/invite-learners-modal.component.ts +++ b/core/templates/pages/learner-group-pages/templates/invite-learners-modal.component.ts @@ -16,31 +16,28 @@ * @fileoverview Component for Invite learners modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { LearnerGroupUserInfo } from 'domain/learner_group/learner-group-user-info.model'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {LearnerGroupUserInfo} from 'domain/learner_group/learner-group-user-info.model'; @Component({ selector: 'oppia-invite-learners-modal', - templateUrl: './invite-learners-modal.component.html' + templateUrl: './invite-learners-modal.component.html', }) export class InviteLearnersModalComponent extends ConfirmOrCancelModal { learnerGroupId!: string; invitedLearners: string[] = []; invitedLearnersInfo: LearnerGroupUserInfo[] = []; - - constructor( - private ngbActiveModal: NgbActiveModal, - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } confirm(): void { this.ngbActiveModal.close({ invitedLearners: this.invitedLearners, - invitedLearnersInfo: this.invitedLearnersInfo + invitedLearnersInfo: this.invitedLearnersInfo, }); } @@ -49,7 +46,7 @@ export class InviteLearnersModalComponent extends ConfirmOrCancelModal { } updateNewlyInvitedLearnersInfo( - invitedLearnersInfo: LearnerGroupUserInfo[] + invitedLearnersInfo: LearnerGroupUserInfo[] ): void { this.invitedLearnersInfo = invitedLearnersInfo; } diff --git a/core/templates/pages/learner-group-pages/templates/invite-successful-modal.component.spec.ts b/core/templates/pages/learner-group-pages/templates/invite-successful-modal.component.spec.ts index c972eeb0a4b0..67bc29ed1ffd 100644 --- a/core/templates/pages/learner-group-pages/templates/invite-successful-modal.component.spec.ts +++ b/core/templates/pages/learner-group-pages/templates/invite-successful-modal.component.spec.ts @@ -12,16 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Unit tests for the invite successful modal component. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { InviteSuccessfulModalComponent } from './invite-successful-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {InviteSuccessfulModalComponent} from './invite-successful-modal.component'; class MockActiveModal { close(): void { @@ -33,21 +32,20 @@ class MockActiveModal { } } -describe('Invite successful Modal Component', function() { +describe('Invite successful Modal Component', function () { let component: InviteSuccessfulModalComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - InviteSuccessfulModalComponent, - MockTranslatePipe + declarations: [InviteSuccessfulModalComponent, MockTranslatePipe], + providers: [ + { + provide: NgbActiveModal, + useClass: MockActiveModal, + }, ], - providers: [{ - provide: NgbActiveModal, - useClass: MockActiveModal - }], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); diff --git a/core/templates/pages/learner-group-pages/templates/invite-successful-modal.component.ts b/core/templates/pages/learner-group-pages/templates/invite-successful-modal.component.ts index 1c9152fe269b..1a22b4aaa738 100644 --- a/core/templates/pages/learner-group-pages/templates/invite-successful-modal.component.ts +++ b/core/templates/pages/learner-group-pages/templates/invite-successful-modal.component.ts @@ -16,21 +16,19 @@ * @fileoverview Component for Invite successful modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-invite-successful-modal', - templateUrl: './invite-successful-modal.component.html' + templateUrl: './invite-successful-modal.component.html', }) export class InviteSuccessfulModalComponent extends ConfirmOrCancelModal { successMessage!: string; invitedUsernames!: string[]; - constructor( - private ngbActiveModal: NgbActiveModal, - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } } diff --git a/core/templates/pages/learner-group-pages/templates/learner-group-preferences-modal.component.spec.ts b/core/templates/pages/learner-group-pages/templates/learner-group-preferences-modal.component.spec.ts index 1f742ebb047f..70a7f53c66d1 100644 --- a/core/templates/pages/learner-group-pages/templates/learner-group-preferences-modal.component.spec.ts +++ b/core/templates/pages/learner-group-pages/templates/learner-group-preferences-modal.component.spec.ts @@ -12,17 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Unit tests for the learner group preferences modal component. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { LearnerGroupData } from 'domain/learner_group/learner-group.model'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { LearnerGroupPreferencesModalComponent } from './learner-group-preferences-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {LearnerGroupData} from 'domain/learner_group/learner-group.model'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {LearnerGroupPreferencesModalComponent} from './learner-group-preferences-modal.component'; class MockActiveModal { close(): void { @@ -34,27 +33,32 @@ class MockActiveModal { } } -describe('Learner Group Preferences Modal Component', function() { +describe('Learner Group Preferences Modal Component', function () { let component: LearnerGroupPreferencesModalComponent; let ngbActiveModal: NgbActiveModal; let fixture: ComponentFixture; const learnerGroup = new LearnerGroupData( - 'groupId', 'title', 'description', ['facilitator_username'], - ['username2'], ['username1'], ['subtopic_page_id'], [] + 'groupId', + 'title', + 'description', + ['facilitator_username'], + ['username2'], + ['username1'], + ['subtopic_page_id'], + [] ); beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - LearnerGroupPreferencesModalComponent, - MockTranslatePipe + declarations: [LearnerGroupPreferencesModalComponent, MockTranslatePipe], + providers: [ + { + provide: NgbActiveModal, + useClass: MockActiveModal, + }, ], - providers: [{ - provide: NgbActiveModal, - useClass: MockActiveModal - }], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -77,7 +81,7 @@ describe('Learner Group Preferences Modal Component', function() { component.confirm(); expect(ngbActiveModal.close).toHaveBeenCalledWith({ - progressSharingPermission: component.progressSharingPermission + progressSharingPermission: component.progressSharingPermission, }); }); diff --git a/core/templates/pages/learner-group-pages/templates/learner-group-preferences-modal.component.ts b/core/templates/pages/learner-group-pages/templates/learner-group-preferences-modal.component.ts index f580d819c6dc..696d4a24c6ce 100644 --- a/core/templates/pages/learner-group-pages/templates/learner-group-preferences-modal.component.ts +++ b/core/templates/pages/learner-group-pages/templates/learner-group-preferences-modal.component.ts @@ -16,23 +16,20 @@ * @fileoverview Component for view learner group invitation modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { LearnerGroupData } from 'domain/learner_group/learner-group.model'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {LearnerGroupData} from 'domain/learner_group/learner-group.model'; @Component({ selector: 'oppia-learner-group-preferences-modal', - templateUrl: './learner-group-preferences-modal.component.html' + templateUrl: './learner-group-preferences-modal.component.html', }) -export class LearnerGroupPreferencesModalComponent - extends ConfirmOrCancelModal { +export class LearnerGroupPreferencesModalComponent extends ConfirmOrCancelModal { learnerGroup!: LearnerGroupData; progressSharingPermission: boolean = true; - constructor( - private ngbActiveModal: NgbActiveModal, - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } diff --git a/core/templates/pages/learner-group-pages/templates/remove-item-modal.component.spec.ts b/core/templates/pages/learner-group-pages/templates/remove-item-modal.component.spec.ts index 12f8c605ff52..6087ef799c19 100644 --- a/core/templates/pages/learner-group-pages/templates/remove-item-modal.component.spec.ts +++ b/core/templates/pages/learner-group-pages/templates/remove-item-modal.component.spec.ts @@ -12,16 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Unit tests for the remove item modal component. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { RemoveItemModalComponent } from './remove-item-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {RemoveItemModalComponent} from './remove-item-modal.component'; class MockActiveModal { close(): void { @@ -33,21 +32,20 @@ class MockActiveModal { } } -describe('Remove Item Modal Component', function() { +describe('Remove Item Modal Component', function () { let component: RemoveItemModalComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - RemoveItemModalComponent, - MockTranslatePipe + declarations: [RemoveItemModalComponent, MockTranslatePipe], + providers: [ + { + provide: NgbActiveModal, + useClass: MockActiveModal, + }, ], - providers: [{ - provide: NgbActiveModal, - useClass: MockActiveModal - }], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); diff --git a/core/templates/pages/learner-group-pages/templates/remove-item-modal.component.ts b/core/templates/pages/learner-group-pages/templates/remove-item-modal.component.ts index 7d3da992a766..977a80fbcc72 100644 --- a/core/templates/pages/learner-group-pages/templates/remove-item-modal.component.ts +++ b/core/templates/pages/learner-group-pages/templates/remove-item-modal.component.ts @@ -16,21 +16,19 @@ * @fileoverview Component for Remove Item modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-remove-item-modal', - templateUrl: './remove-item-modal.component.html' + templateUrl: './remove-item-modal.component.html', }) export class RemoveItemModalComponent extends ConfirmOrCancelModal { confirmationTitle!: string; confirmationMessage!: string; - constructor( - private ngbActiveModal: NgbActiveModal, - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } } diff --git a/core/templates/pages/learner-group-pages/templates/syllabus-addition-success-modal.component.spec.ts b/core/templates/pages/learner-group-pages/templates/syllabus-addition-success-modal.component.spec.ts index d55ce1c63a86..26b00027a61a 100644 --- a/core/templates/pages/learner-group-pages/templates/syllabus-addition-success-modal.component.spec.ts +++ b/core/templates/pages/learner-group-pages/templates/syllabus-addition-success-modal.component.spec.ts @@ -12,17 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Unit tests for syllabus addition success modal component. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, waitForAsync, TestBed } from - '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { SyllabusAdditionSuccessModalComponent } from - './syllabus-addition-success-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {SyllabusAdditionSuccessModalComponent} from './syllabus-addition-success-modal.component'; class MockActiveModal { close(): void { @@ -34,27 +31,25 @@ class MockActiveModal { } } -describe('Delete Exploration Modal Component', function() { +describe('Delete Exploration Modal Component', function () { let component: SyllabusAdditionSuccessModalComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - SyllabusAdditionSuccessModalComponent + declarations: [SyllabusAdditionSuccessModalComponent], + providers: [ + { + provide: NgbActiveModal, + useClass: MockActiveModal, + }, ], - providers: [{ - provide: NgbActiveModal, - useClass: MockActiveModal - }], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent( - SyllabusAdditionSuccessModalComponent - ); + fixture = TestBed.createComponent(SyllabusAdditionSuccessModalComponent); component = fixture.componentInstance; TestBed.inject(NgbActiveModal); diff --git a/core/templates/pages/learner-group-pages/templates/syllabus-addition-success-modal.component.ts b/core/templates/pages/learner-group-pages/templates/syllabus-addition-success-modal.component.ts index 39bf0668758f..e263756fa615 100644 --- a/core/templates/pages/learner-group-pages/templates/syllabus-addition-success-modal.component.ts +++ b/core/templates/pages/learner-group-pages/templates/syllabus-addition-success-modal.component.ts @@ -16,21 +16,18 @@ * @fileoverview Component for Syllabus addition success modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-syllabus-addition-success-modal', - templateUrl: './syllabus-addition-success-modal.component.html' + templateUrl: './syllabus-addition-success-modal.component.html', }) -export class SyllabusAdditionSuccessModalComponent - extends ConfirmOrCancelModal { +export class SyllabusAdditionSuccessModalComponent extends ConfirmOrCancelModal { itemsAddedCount!: number; - constructor( - private ngbActiveModal: NgbActiveModal, - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } } diff --git a/core/templates/pages/learner-group-pages/view-group/learner-group-view-assigned-syllabus.component.spec.ts b/core/templates/pages/learner-group-pages/view-group/learner-group-view-assigned-syllabus.component.spec.ts index 781a1d757151..e5ceb15951cb 100644 --- a/core/templates/pages/learner-group-pages/view-group/learner-group-view-assigned-syllabus.component.spec.ts +++ b/core/templates/pages/learner-group-pages/view-group/learner-group-view-assigned-syllabus.component.spec.ts @@ -16,22 +16,22 @@ * @fileoverview Unit tests for view learner group assigned syllabus tab. */ -import { NO_ERRORS_SCHEMA, Pipe } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick } from - '@angular/core/testing'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { LearnerGroupSyllabusBackendApiService } from - 'domain/learner_group/learner-group-syllabus-backend-api.service'; -import { StorySummary } from 'domain/story/story-summary.model'; -import { LearnerGroupSubtopicSummary } from - 'domain/learner_group/learner-group-subtopic-summary.model'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; -import { LearnerGroupViewAssignedSyllabusComponent } from - './learner-group-view-assigned-syllabus.component'; -import { LearnerGroupData } from 'domain/learner_group/learner-group.model'; -import { LearnerGroupUserProgress } from - 'domain/learner_group/learner-group-user-progress.model'; +import {NO_ERRORS_SCHEMA, Pipe} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {LearnerGroupSyllabusBackendApiService} from 'domain/learner_group/learner-group-syllabus-backend-api.service'; +import {StorySummary} from 'domain/story/story-summary.model'; +import {LearnerGroupSubtopicSummary} from 'domain/learner_group/learner-group-subtopic-summary.model'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; +import {LearnerGroupViewAssignedSyllabusComponent} from './learner-group-view-assigned-syllabus.component'; +import {LearnerGroupData} from 'domain/learner_group/learner-group.model'; +import {LearnerGroupUserProgress} from 'domain/learner_group/learner-group-user-progress.model'; @Pipe({name: 'truncate'}) class MockTrunctePipe { @@ -44,8 +44,7 @@ describe('LearnerGroupViewAssignedSyllabusComponent', () => { let component: LearnerGroupViewAssignedSyllabusComponent; let fixture: ComponentFixture; let assetsBackendApiService: AssetsBackendApiService; - let learnerGroupSyllabusBackendApiService: - LearnerGroupSyllabusBackendApiService; + let learnerGroupSyllabusBackendApiService: LearnerGroupSyllabusBackendApiService; const sampleSubtopicSummaryDict = { subtopic_id: 1, @@ -56,11 +55,12 @@ describe('LearnerGroupViewAssignedSyllabusComponent', () => { thumbnail_bg_color: 'red', subtopic_mastery: 0.5, parent_topic_url_fragment: 'topic_1', - classroom_url_fragment: 'classroom_1' + classroom_url_fragment: 'classroom_1', }; - const sampleLearnerGroupSubtopicSummary = ( + const sampleLearnerGroupSubtopicSummary = LearnerGroupSubtopicSummary.createFromBackendDict( - sampleSubtopicSummaryDict)); + sampleSubtopicSummaryDict + ); const sampleSubtopicSummaryDict2 = { subtopic_id: 0, @@ -71,11 +71,12 @@ describe('LearnerGroupViewAssignedSyllabusComponent', () => { thumbnail_bg_color: 'red', subtopic_mastery: 0.6, parent_topic_url_fragment: 'topic_1', - classroom_url_fragment: undefined + classroom_url_fragment: undefined, }; - const sampleLearnerGroupSubtopicSummary2 = ( + const sampleLearnerGroupSubtopicSummary2 = LearnerGroupSubtopicSummary.createFromBackendDict( - sampleSubtopicSummaryDict2)); + sampleSubtopicSummaryDict2 + ); const nodeDict1 = { id: 'node_1', @@ -93,7 +94,7 @@ describe('LearnerGroupViewAssignedSyllabusComponent', () => { planned_publication_date_msecs: 100, last_modified_msecs: 100, first_publication_date_msecs: 200, - unpublishing_reason: null + unpublishing_reason: null, }; const sampleStorySummaryBackendDict = { id: 'story_id_0', @@ -108,10 +109,11 @@ describe('LearnerGroupViewAssignedSyllabusComponent', () => { all_node_dicts: [nodeDict1], topic_name: 'Topic', classroom_url_fragment: 'math', - topic_url_fragment: 'topic' + topic_url_fragment: 'topic', }; const sampleStorySummary = StorySummary.createFromBackendDict( - sampleStorySummaryBackendDict); + sampleStorySummaryBackendDict + ); const sampleStorySummaryBackendDict2 = { id: 'story_id_1', @@ -126,20 +128,22 @@ describe('LearnerGroupViewAssignedSyllabusComponent', () => { all_node_dicts: [], topic_name: 'Topic', classroom_url_fragment: 'math', - topic_url_fragment: 'topic' + topic_url_fragment: 'topic', }; const learnerProgressDict = { username: 'user1', progress_sharing_is_turned_on: true, stories_progress: [ - sampleStorySummaryBackendDict, sampleStorySummaryBackendDict2 + sampleStorySummaryBackendDict, + sampleStorySummaryBackendDict2, ], subtopic_pages_progress: [ - sampleSubtopicSummaryDict, sampleSubtopicSummaryDict2 - ] + sampleSubtopicSummaryDict, + sampleSubtopicSummaryDict2, + ], }; - const mockLearnerProgress = LearnerGroupUserProgress.createFromBackendDict( - learnerProgressDict); + const mockLearnerProgress = + LearnerGroupUserProgress.createFromBackendDict(learnerProgressDict); const learnerGroupBackendDict = { id: 'groupId', @@ -149,10 +153,11 @@ describe('LearnerGroupViewAssignedSyllabusComponent', () => { learner_usernames: [], invited_learner_usernames: ['username1'], subtopic_page_ids: ['topicId1:1'], - story_ids: ['story_id_0'] + story_ids: ['story_id_0'], }; const learnerGroup = LearnerGroupData.createFromBackendDict( - learnerGroupBackendDict); + learnerGroupBackendDict + ); beforeEach(() => { TestBed.configureTestingModule({ @@ -160,19 +165,21 @@ describe('LearnerGroupViewAssignedSyllabusComponent', () => { declarations: [ LearnerGroupViewAssignedSyllabusComponent, MockTranslatePipe, - MockTrunctePipe + MockTrunctePipe, ], providers: [], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); beforeEach(() => { learnerGroupSyllabusBackendApiService = TestBed.inject( - LearnerGroupSyllabusBackendApiService); + LearnerGroupSyllabusBackendApiService + ); assetsBackendApiService = TestBed.inject(AssetsBackendApiService); fixture = TestBed.createComponent( - LearnerGroupViewAssignedSyllabusComponent); + LearnerGroupViewAssignedSyllabusComponent + ); component = fixture.componentInstance; fixture.detectChanges(); @@ -202,16 +209,23 @@ describe('LearnerGroupViewAssignedSyllabusComponent', () => { tick(); expect(component.storySummaries).toEqual( - mockLearnerProgress.storiesProgress); + mockLearnerProgress.storiesProgress + ); expect(component.subtopicSummaries).toEqual( - mockLearnerProgress.subtopicsProgress); + mockLearnerProgress.subtopicsProgress + ); expect(component.displayOrderOfSyllabusItems).toEqual([ - 'story-0', 'story-1', 'subtopic-0', 'subtopic-1']); + 'story-0', + 'story-1', + 'subtopic-0', + 'subtopic-1', + ]); })); it('should get subtopic thumbnail url', () => { - spyOn(assetsBackendApiService, 'getThumbnailUrlForPreview') - .and.returnValue('/topic/thumbnail/url'); + spyOn(assetsBackendApiService, 'getThumbnailUrlForPreview').and.returnValue( + '/topic/thumbnail/url' + ); expect( component.getSubtopicThumbnailUrl(sampleLearnerGroupSubtopicSummary) @@ -219,23 +233,26 @@ describe('LearnerGroupViewAssignedSyllabusComponent', () => { }); it('should get story thumbnail url', () => { - spyOn(assetsBackendApiService, 'getThumbnailUrlForPreview') - .and.returnValue('/story/thumbnail/url'); + spyOn(assetsBackendApiService, 'getThumbnailUrlForPreview').and.returnValue( + '/story/thumbnail/url' + ); expect(component.getStoryThumbnailUrl(sampleStorySummary)).toEqual( - '/story/thumbnail/url'); + '/story/thumbnail/url' + ); }); it('should get circular progress', () => { let cssStyle = component.calculateCircularProgressCss(0); expect(cssStyle).toEqual( 'linear-gradient(90deg, transparent 50%, #CCCCCC 50%)' + - ', linear-gradient(90deg, #CCCCCC 50%, transparent 50%)'); + ', linear-gradient(90deg, #CCCCCC 50%, transparent 50%)' + ); cssStyle = component.calculateCircularProgressCss(60); expect(cssStyle).toEqual( 'linear-gradient(270deg, #00645C 50%, transparent 50%), ' + - 'linear-gradient(-54deg, #00645C 50%, #CCCCCC 50%)' + 'linear-gradient(-54deg, #00645C 50%, #CCCCCC 50%)' ); }); @@ -246,108 +263,139 @@ describe('LearnerGroupViewAssignedSyllabusComponent', () => { it('should get story link correctly', () => { expect(component.getStoryLink(sampleStorySummary)).toBe( - '/learn/math/topic/story/story-title'); + '/learn/math/topic/story/story-title' + ); }); - it('should get # as story link url when classroom or topic url is not ' + - 'present', () => { - const sampleStorySummaryBackendDict = { - id: '0', - title: 'Story Title', - description: 'Story Description', - node_titles: ['Chapter 1'], - thumbnail_filename: 'image.svg', - thumbnail_bg_color: '#F8BF74', - story_is_published: true, - completed_node_titles: ['Chapter 1'], - url_fragment: 'story-title', - all_node_dicts: [], - topic_name: 'Topic', - classroom_url_fragment: undefined, - topic_url_fragment: 'topic' - }; - const storySummary = StorySummary.createFromBackendDict( - sampleStorySummaryBackendDict); - - expect(component.getStoryLink(storySummary)).toBe('#'); - }); + it( + 'should get # as story link url when classroom or topic url is not ' + + 'present', + () => { + const sampleStorySummaryBackendDict = { + id: '0', + title: 'Story Title', + description: 'Story Description', + node_titles: ['Chapter 1'], + thumbnail_filename: 'image.svg', + thumbnail_bg_color: '#F8BF74', + story_is_published: true, + completed_node_titles: ['Chapter 1'], + url_fragment: 'story-title', + all_node_dicts: [], + topic_name: 'Topic', + classroom_url_fragment: undefined, + topic_url_fragment: 'topic', + }; + const storySummary = StorySummary.createFromBackendDict( + sampleStorySummaryBackendDict + ); + + expect(component.getStoryLink(storySummary)).toBe('#'); + } + ); it('should get practice session link correctly', () => { - expect(component.getPracticeSessionLink(sampleLearnerGroupSubtopicSummary)) - .toBe( - '/learn/classroom_1/topic_1/practice/session?' + + expect( + component.getPracticeSessionLink(sampleLearnerGroupSubtopicSummary) + ).toBe( + '/learn/classroom_1/topic_1/practice/session?' + 'selected_subtopic_ids=%5B1%5D' - ); + ); }); - it('should get # as practice link url when classroom or topic url is not ' + - 'present', () => { - expect( - component.getPracticeSessionLink(sampleLearnerGroupSubtopicSummary2) - ).toBe('#'); - }); + it( + 'should get # as practice link url when classroom or topic url is not ' + + 'present', + () => { + expect( + component.getPracticeSessionLink(sampleLearnerGroupSubtopicSummary2) + ).toBe('#'); + } + ); it('should get subtopic mastery level correctly', () => { const sampleSubtopicSummary3 = new LearnerGroupSubtopicSummary( - 'topicId1', 'topic name', 3, 'sub title', 'filename', '#F8BF74', 0.85 + 'topicId1', + 'topic name', + 3, + 'sub title', + 'filename', + '#F8BF74', + 0.85 ); const sampleSubtopicSummary4 = new LearnerGroupSubtopicSummary( - 'topicId1', 'topic name', 4, 'sub title', 'filename4', '#F8BF74', 1 + 'topicId1', + 'topic name', + 4, + 'sub title', + 'filename4', + '#F8BF74', + 1 ); const sampleSubtopicSummary5 = new LearnerGroupSubtopicSummary( - 'topicId1', 'topic name', 4, 'sub title', 'filename4', '#F8BF74' + 'topicId1', + 'topic name', + 4, + 'sub title', + 'filename4', + '#F8BF74' ); let masteryLevel = component.getSubtopicMasteryLevel( - sampleLearnerGroupSubtopicSummary); + sampleLearnerGroupSubtopicSummary + ); expect(masteryLevel).toBe('I18N_SKILL_LEVEL_NEEDS_WORK'); masteryLevel = component.getSubtopicMasteryLevel( - sampleLearnerGroupSubtopicSummary2); + sampleLearnerGroupSubtopicSummary2 + ); expect(masteryLevel).toBe('I18N_SKILL_LEVEL_BEGINNER'); - masteryLevel = component.getSubtopicMasteryLevel( - sampleSubtopicSummary3); + masteryLevel = component.getSubtopicMasteryLevel(sampleSubtopicSummary3); expect(masteryLevel).toBe('I18N_SKILL_LEVEL_INTERMEDIATE'); - masteryLevel = component.getSubtopicMasteryLevel( - sampleSubtopicSummary4); + masteryLevel = component.getSubtopicMasteryLevel(sampleSubtopicSummary4); expect(masteryLevel).toBe('I18N_SKILL_LEVEL_PROFICIENT'); - masteryLevel = component.getSubtopicMasteryLevel( - sampleSubtopicSummary5); + masteryLevel = component.getSubtopicMasteryLevel(sampleSubtopicSummary5); expect(masteryLevel).toBe( - 'I18N_LEARNER_GROUP_SYLLABUS_ITEM_NOT_STARTED_YET'); + 'I18N_LEARNER_GROUP_SYLLABUS_ITEM_NOT_STARTED_YET' + ); }); it('should get story node link correctly', () => { - let storyNodeLink = '/explore/exp_1?topic_url_fragment=topic&' + - 'classroom_url_fragment=math&story_url_fragment=story-title&' + - 'node_id=node_1'; + let storyNodeLink = + '/explore/exp_1?topic_url_fragment=topic&' + + 'classroom_url_fragment=math&story_url_fragment=story-title&' + + 'node_id=node_1'; expect(component.getStoryNodeLink(sampleStorySummary)).toBe(storyNodeLink); }); - it('should get # as story node link url when classroom or topic url is ' + - 'not present', () => { - const sampleStorySummaryBackendDict = { - id: '0', - title: 'Story Title', - description: 'Story Description', - node_titles: ['Chapter 1'], - thumbnail_filename: 'image.svg', - thumbnail_bg_color: '#F8BF74', - story_is_published: true, - completed_node_titles: [], - url_fragment: 'story-title', - all_node_dicts: [nodeDict1], - topic_name: 'Topic', - classroom_url_fragment: undefined, - topic_url_fragment: 'topic' - }; - const storySummary = StorySummary.createFromBackendDict( - sampleStorySummaryBackendDict); - - expect(component.getStoryNodeLink(storySummary)).toBe('#'); - }); + it( + 'should get # as story node link url when classroom or topic url is ' + + 'not present', + () => { + const sampleStorySummaryBackendDict = { + id: '0', + title: 'Story Title', + description: 'Story Description', + node_titles: ['Chapter 1'], + thumbnail_filename: 'image.svg', + thumbnail_bg_color: '#F8BF74', + story_is_published: true, + completed_node_titles: [], + url_fragment: 'story-title', + all_node_dicts: [nodeDict1], + topic_name: 'Topic', + classroom_url_fragment: undefined, + topic_url_fragment: 'topic', + }; + const storySummary = StorySummary.createFromBackendDict( + sampleStorySummaryBackendDict + ); + + expect(component.getStoryNodeLink(storySummary)).toBe('#'); + } + ); }); diff --git a/core/templates/pages/learner-group-pages/view-group/learner-group-view-assigned-syllabus.component.ts b/core/templates/pages/learner-group-pages/view-group/learner-group-view-assigned-syllabus.component.ts index eeaa18499e6d..1517fedea427 100644 --- a/core/templates/pages/learner-group-pages/view-group/learner-group-view-assigned-syllabus.component.ts +++ b/core/templates/pages/learner-group-pages/view-group/learner-group-view-assigned-syllabus.component.ts @@ -16,25 +16,25 @@ * @fileoverview Component for the learner group view of assigned syllabus. */ -import { Component, Input, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { AppConstants } from 'app.constants'; -import { UrlService } from 'services/contextual/url.service'; -import { LearnerGroupSubtopicSummary } from 'domain/learner_group/learner-group-subtopic-summary.model'; -import { LearnerGroupSyllabusBackendApiService } from 'domain/learner_group/learner-group-syllabus-backend-api.service'; -import { LearnerGroupData } from 'domain/learner_group/learner-group.model'; -import { StorySummary } from 'domain/story/story-summary.model'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; -import { LearnerGroupUserProgress } from 'domain/learner_group/learner-group-user-progress.model'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { TopicViewerDomainConstants } from 'domain/topic_viewer/topic-viewer-domain.constants'; -import { PracticeSessionPageConstants } from 'pages/practice-session-page/practice-session-page.constants'; +import {Component, Input, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {AppConstants} from 'app.constants'; +import {UrlService} from 'services/contextual/url.service'; +import {LearnerGroupSubtopicSummary} from 'domain/learner_group/learner-group-subtopic-summary.model'; +import {LearnerGroupSyllabusBackendApiService} from 'domain/learner_group/learner-group-syllabus-backend-api.service'; +import {LearnerGroupData} from 'domain/learner_group/learner-group.model'; +import {StorySummary} from 'domain/story/story-summary.model'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; +import {LearnerGroupUserProgress} from 'domain/learner_group/learner-group-user-progress.model'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {TopicViewerDomainConstants} from 'domain/topic_viewer/topic-viewer-domain.constants'; +import {PracticeSessionPageConstants} from 'pages/practice-session-page/practice-session-page.constants'; import './learner-group-view-assigned-syllabus.component.css'; @Component({ selector: 'oppia-learner-group-view-assigned-syllabus', - templateUrl: './learner-group-view-assigned-syllabus.component.html' + templateUrl: './learner-group-view-assigned-syllabus.component.html', }) export class LearnerGroupViewAssignedSyllabusComponent implements OnInit { @Input() learnerGroup!: LearnerGroupData; @@ -47,15 +47,14 @@ export class LearnerGroupViewAssignedSyllabusComponent implements OnInit { private assetsBackendApiService: AssetsBackendApiService, private urlInterpolationService: UrlInterpolationService, private urlService: UrlService, - private learnerGroupSyllabusBackendApiService: - LearnerGroupSyllabusBackendApiService + private learnerGroupSyllabusBackendApiService: LearnerGroupSyllabusBackendApiService ) {} ngOnInit(): void { if (this.learnerGroup) { this.learnerGroupSyllabusBackendApiService - .fetchLearnerSpecificProgressInAssignedSyllabus( - this.learnerGroup.id).then(groupSyllabus => { + .fetchLearnerSpecificProgressInAssignedSyllabus(this.learnerGroup.id) + .then(groupSyllabus => { this.subtopicSummaries = groupSyllabus.subtopicsProgress; this.storySummaries = groupSyllabus.storiesProgress; this.setDisplayOrderOfSyllabusItems(); @@ -103,15 +102,14 @@ export class LearnerGroupViewAssignedSyllabusComponent implements OnInit { } getSubtopicThumbnailUrl( - subtopicSummary: LearnerGroupSubtopicSummary + subtopicSummary: LearnerGroupSubtopicSummary ): string { let thumbnailUrl = ''; if (subtopicSummary.thumbnailFilename) { - thumbnailUrl = ( - this.assetsBackendApiService.getThumbnailUrlForPreview( - AppConstants.ENTITY_TYPE.TOPIC, subtopicSummary.parentTopicId, - subtopicSummary.thumbnailFilename - ) + thumbnailUrl = this.assetsBackendApiService.getThumbnailUrlForPreview( + AppConstants.ENTITY_TYPE.TOPIC, + subtopicSummary.parentTopicId, + subtopicSummary.thumbnailFilename ); } return thumbnailUrl; @@ -120,37 +118,37 @@ export class LearnerGroupViewAssignedSyllabusComponent implements OnInit { getStoryThumbnailUrl(storySummary: StorySummary): string { let thumbnailUrl = ''; if (storySummary.getThumbnailFilename()) { - thumbnailUrl = ( - this.assetsBackendApiService.getThumbnailUrlForPreview( - AppConstants.ENTITY_TYPE.STORY, storySummary.getId(), - storySummary.getThumbnailFilename() - ) + thumbnailUrl = this.assetsBackendApiService.getThumbnailUrlForPreview( + AppConstants.ENTITY_TYPE.STORY, + storySummary.getId(), + storySummary.getThumbnailFilename() ); } return thumbnailUrl; } calculateCircularProgressCss(progress: number): string { - let degree = (90 + (360 * progress / 100)); - let cssStyle = ( + let degree = 90 + (360 * progress) / 100; + let cssStyle = `linear-gradient(${degree}deg, transparent 50%, #CCCCCC 50%)` + - ', linear-gradient(90deg, #CCCCCC 50%, transparent 50%)'); + ', linear-gradient(90deg, #CCCCCC 50%, transparent 50%)'; if (progress > 50) { degree = 3.6 * (progress - 50) - 90; - cssStyle = ( + cssStyle = 'linear-gradient(270deg, #00645C 50%, transparent 50%), ' + - `linear-gradient(${degree}deg, #00645C 50%, #CCCCCC 50%)`); + `linear-gradient(${degree}deg, #00645C 50%, #CCCCCC 50%)`; } return cssStyle; } getProgressOfStory(storySummary: StorySummary): number { - return Math.round( - ( - 100 * storySummary.getCompletedNodeTitles().length / - storySummary.getNodeTitles().length - ) * 10 - ) / 10; + return ( + Math.round( + ((100 * storySummary.getCompletedNodeTitles().length) / + storySummary.getNodeTitles().length) * + 10 + ) / 10 + ); } getStoryLink(storySummary: StorySummary): string { @@ -160,11 +158,13 @@ export class LearnerGroupViewAssignedSyllabusComponent implements OnInit { return '#'; } let storyLink = this.urlInterpolationService.interpolateUrl( - TopicViewerDomainConstants.STORY_VIEWER_URL_TEMPLATE, { + TopicViewerDomainConstants.STORY_VIEWER_URL_TEMPLATE, + { classroom_url_fragment: classroomUrlFragment, story_url_fragment: storySummary.getUrlFragment(), - topic_url_fragment: topicUrlFragment - }); + topic_url_fragment: topicUrlFragment, + } + ); return storyLink; } @@ -180,42 +180,59 @@ export class LearnerGroupViewAssignedSyllabusComponent implements OnInit { storyNodeToDisplay = storySummary.getAllNodes()[0]; } const explorationId = storyNodeToDisplay.getExplorationId(); - if (classroomUrlFragment === undefined || topicUrlFragment === undefined || - explorationId === null) { + if ( + classroomUrlFragment === undefined || + topicUrlFragment === undefined || + explorationId === null + ) { return '#'; } let storyNodeLink = this.urlInterpolationService.interpolateUrl( - '/explore/', { exp_id: explorationId }); + '/explore/', + {exp_id: explorationId} + ); storyNodeLink = this.urlService.addField( - storyNodeLink, 'topic_url_fragment', topicUrlFragment); + storyNodeLink, + 'topic_url_fragment', + topicUrlFragment + ); storyNodeLink = this.urlService.addField( - storyNodeLink, 'classroom_url_fragment', classroomUrlFragment); + storyNodeLink, + 'classroom_url_fragment', + classroomUrlFragment + ); storyNodeLink = this.urlService.addField( - storyNodeLink, 'story_url_fragment', storySummary.getUrlFragment()); + storyNodeLink, + 'story_url_fragment', + storySummary.getUrlFragment() + ); storyNodeLink = this.urlService.addField( - storyNodeLink, 'node_id', storyNodeToDisplay.getId()); + storyNodeLink, + 'node_id', + storyNodeToDisplay.getId() + ); return storyNodeLink; } - getPracticeSessionLink( - subtopicSummary: LearnerGroupSubtopicSummary - ): string { + getPracticeSessionLink(subtopicSummary: LearnerGroupSubtopicSummary): string { const classroomUrlFragment = subtopicSummary.classroomUrlFragment; const topicUrlFragment = subtopicSummary.parentTopicUrlFragment; if (classroomUrlFragment === undefined || topicUrlFragment === undefined) { return '#'; } let practiceSessionsLink = this.urlInterpolationService.interpolateUrl( - PracticeSessionPageConstants.PRACTICE_SESSIONS_URL, { + PracticeSessionPageConstants.PRACTICE_SESSIONS_URL, + { topic_url_fragment: topicUrlFragment, classroom_url_fragment: classroomUrlFragment, - stringified_subtopic_ids: JSON.stringify([subtopicSummary.subtopicId]) - }); + stringified_subtopic_ids: JSON.stringify([subtopicSummary.subtopicId]), + } + ); return practiceSessionsLink; } getSubtopicMasteryLevel( - subtopicSummary: LearnerGroupSubtopicSummary + subtopicSummary: LearnerGroupSubtopicSummary ): string { let masteryLevel = 'I18N_LEARNER_GROUP_SYLLABUS_ITEM_NOT_STARTED_YET'; const subtopicMastery = subtopicSummary.subtopicMastery; @@ -235,6 +252,9 @@ export class LearnerGroupViewAssignedSyllabusComponent implements OnInit { } } -angular.module('oppia').directive( - 'oppiaLearnerViewAssignedGroupSyllabus', - downgradeComponent({component: LearnerGroupViewAssignedSyllabusComponent})); +angular + .module('oppia') + .directive( + 'oppiaLearnerViewAssignedGroupSyllabus', + downgradeComponent({component: LearnerGroupViewAssignedSyllabusComponent}) + ); diff --git a/core/templates/pages/learner-group-pages/view-group/view-learner-group-page-root.component.spec.ts b/core/templates/pages/learner-group-pages/view-group/view-learner-group-page-root.component.spec.ts index 6a2742172f36..31ac7d478e13 100644 --- a/core/templates/pages/learner-group-pages/view-group/view-learner-group-page-root.component.spec.ts +++ b/core/templates/pages/learner-group-pages/view-group/view-learner-group-page-root.component.spec.ts @@ -16,20 +16,26 @@ * @fileoverview Unit tests for the view learner group page root component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; - -import { AppConstants } from 'app.constants'; -import { AccessValidationBackendApiService } from 'pages/oppia-root/routing/access-validation-backend-api.service'; -import { ContextService } from 'services/context.service'; -import { MetaTagCustomizationService } from 'services/contextual/meta-tag-customization.service'; -import { LoaderService } from 'services/loader.service'; -import { PageHeadService } from 'services/page-head.service'; - -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { ViewLearnerGroupPageRootComponent } from './view-learner-group-page-root.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; + +import {AppConstants} from 'app.constants'; +import {AccessValidationBackendApiService} from 'pages/oppia-root/routing/access-validation-backend-api.service'; +import {ContextService} from 'services/context.service'; +import {MetaTagCustomizationService} from 'services/contextual/meta-tag-customization.service'; +import {LoaderService} from 'services/loader.service'; +import {PageHeadService} from 'services/page-head.service'; + +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {ViewLearnerGroupPageRootComponent} from './view-learner-group-page-root.component'; class MockTranslateService { onLangChange: EventEmitter = new EventEmitter(); @@ -49,22 +55,17 @@ describe('View Learner Group Page Root', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - ViewLearnerGroupPageRootComponent, - MockTranslatePipe - ], + imports: [HttpClientTestingModule], + declarations: [ViewLearnerGroupPageRootComponent, MockTranslatePipe], providers: [ PageHeadService, MetaTagCustomizationService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -74,21 +75,23 @@ describe('View Learner Group Page Root', () => { pageHeadService = TestBed.inject(PageHeadService); loaderService = TestBed.inject(LoaderService); accessValidationBackendApiService = TestBed.inject( - AccessValidationBackendApiService); + AccessValidationBackendApiService + ); translateService = TestBed.inject(TranslateService); contextService = TestBed.inject(ContextService); spyOn(contextService, 'getLearnerGroupId').and.returnValue('groupId'); }); - it('should successfully instantiate the component', - () => { - expect(component).toBeDefined(); - }); + it('should successfully instantiate the component', () => { + expect(component).toBeDefined(); + }); it('should initialize and show page when access is valid', fakeAsync(() => { - spyOn(accessValidationBackendApiService, 'doesLearnerGroupExist') - .and.returnValue(Promise.resolve()); + spyOn( + accessValidationBackendApiService, + 'doesLearnerGroupExist' + ).and.returnValue(Promise.resolve()); spyOn(loaderService, 'showLoadingScreen'); spyOn(loaderService, 'hideLoadingScreen'); @@ -96,34 +99,39 @@ describe('View Learner Group Page Root', () => { tick(); expect(loaderService.showLoadingScreen).toHaveBeenCalled(); - expect(accessValidationBackendApiService.doesLearnerGroupExist) - .toHaveBeenCalled(); + expect( + accessValidationBackendApiService.doesLearnerGroupExist + ).toHaveBeenCalled(); expect(component.pageIsShown).toBeTrue(); expect(component.errorPageIsShown).toBeFalse(); expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); })); - it('should initialize and show error page when server respond with error', - fakeAsync(() => { - spyOn(accessValidationBackendApiService, 'doesLearnerGroupExist') - .and.returnValue(Promise.reject()); - spyOn(loaderService, 'showLoadingScreen'); - spyOn(loaderService, 'hideLoadingScreen'); + it('should initialize and show error page when server respond with error', fakeAsync(() => { + spyOn( + accessValidationBackendApiService, + 'doesLearnerGroupExist' + ).and.returnValue(Promise.reject()); + spyOn(loaderService, 'showLoadingScreen'); + spyOn(loaderService, 'hideLoadingScreen'); - component.ngOnInit(); - tick(); + component.ngOnInit(); + tick(); - expect(loaderService.showLoadingScreen).toHaveBeenCalled(); - expect(accessValidationBackendApiService.doesLearnerGroupExist) - .toHaveBeenCalled(); - expect(component.pageIsShown).toBeFalse(); - expect(component.errorPageIsShown).toBeTrue(); - expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); - })); + expect(loaderService.showLoadingScreen).toHaveBeenCalled(); + expect( + accessValidationBackendApiService.doesLearnerGroupExist + ).toHaveBeenCalled(); + expect(component.pageIsShown).toBeFalse(); + expect(component.errorPageIsShown).toBeTrue(); + expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); + })); it('should initialize and subscribe to onLangChange', fakeAsync(() => { - spyOn(accessValidationBackendApiService, 'doesLearnerGroupExist') - .and.returnValue(Promise.resolve()); + spyOn( + accessValidationBackendApiService, + 'doesLearnerGroupExist' + ).and.returnValue(Promise.resolve()); spyOn(component.directiveSubscriptions, 'add'); spyOn(translateService.onLangChange, 'subscribe'); @@ -135,8 +143,10 @@ describe('View Learner Group Page Root', () => { })); it('should update page title whenever the language changes', () => { - spyOn(accessValidationBackendApiService, 'doesLearnerGroupExist') - .and.returnValue(Promise.resolve()); + spyOn( + accessValidationBackendApiService, + 'doesLearnerGroupExist' + ).and.returnValue(Promise.resolve()); component.ngOnInit(); spyOn(component, 'setPageTitleAndMetaTags').and.callThrough(); spyOn(translateService, 'instant').and.callThrough(); @@ -158,10 +168,12 @@ describe('View Learner Group Page Root', () => { component.setPageTitleAndMetaTags(); expect(translateService.instant).toHaveBeenCalledWith( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LEARNER_GROUP_VIEWER.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LEARNER_GROUP_VIEWER.TITLE + ); expect(pageHeadService.updateTitleAndMetaTags).toHaveBeenCalledWith( AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LEARNER_GROUP_VIEWER.TITLE, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LEARNER_GROUP_VIEWER.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LEARNER_GROUP_VIEWER.META + ); }); it('should unsubscribe on component destruction', () => { diff --git a/core/templates/pages/learner-group-pages/view-group/view-learner-group-page-root.component.ts b/core/templates/pages/learner-group-pages/view-group/view-learner-group-page-root.component.ts index 30c16d476234..400db16c6fe2 100644 --- a/core/templates/pages/learner-group-pages/view-group/view-learner-group-page-root.component.ts +++ b/core/templates/pages/learner-group-pages/view-group/view-learner-group-page-root.component.ts @@ -16,19 +16,19 @@ * @fileoverview Root component for View Learner Group Page. */ -import { Component, OnDestroy } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; +import {Component, OnDestroy} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; -import { AppConstants } from 'app.constants'; -import { AccessValidationBackendApiService } from 'pages/oppia-root/routing/access-validation-backend-api.service'; -import { LoaderService } from 'services/loader.service'; -import { PageHeadService } from 'services/page-head.service'; -import { ContextService } from 'services/context.service'; +import {AppConstants} from 'app.constants'; +import {AccessValidationBackendApiService} from 'pages/oppia-root/routing/access-validation-backend-api.service'; +import {LoaderService} from 'services/loader.service'; +import {PageHeadService} from 'services/page-head.service'; +import {ContextService} from 'services/context.service'; @Component({ selector: 'oppia-view-learner-group-page-root', - templateUrl: './view-learner-group-page-root.component.html' + templateUrl: './view-learner-group-page-root.component.html', }) export class ViewLearnerGroupPageRootComponent implements OnDestroy { directiveSubscriptions = new Subscription(); @@ -36,8 +36,7 @@ export class ViewLearnerGroupPageRootComponent implements OnDestroy { errorPageIsShown: boolean = false; constructor( - private accessValidationBackendApiService: - AccessValidationBackendApiService, + private accessValidationBackendApiService: AccessValidationBackendApiService, private loaderService: LoaderService, private pageHeadService: PageHeadService, private contextService: ContextService, @@ -46,10 +45,12 @@ export class ViewLearnerGroupPageRootComponent implements OnDestroy { setPageTitleAndMetaTags(): void { let translatedTitle = this.translateService.instant( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LEARNER_GROUP_VIEWER.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LEARNER_GROUP_VIEWER.TITLE + ); this.pageHeadService.updateTitleAndMetaTags( translatedTitle, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LEARNER_GROUP_VIEWER.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LEARNER_GROUP_VIEWER.META + ); } ngOnInit(): void { @@ -61,12 +62,17 @@ export class ViewLearnerGroupPageRootComponent implements OnDestroy { let learnerGroupId = this.contextService.getLearnerGroupId(); this.loaderService.showLoadingScreen('Loading'); - this.accessValidationBackendApiService.doesLearnerGroupExist(learnerGroupId) + this.accessValidationBackendApiService + .doesLearnerGroupExist(learnerGroupId) + .then( + () => { + this.pageIsShown = true; + }, + err => { + this.errorPageIsShown = true; + } + ) .then(() => { - this.pageIsShown = true; - }, (err) => { - this.errorPageIsShown = true; - }).then(() => { this.loaderService.hideLoadingScreen(); }); } diff --git a/core/templates/pages/learner-group-pages/view-group/view-learner-group-page-routing.module.ts b/core/templates/pages/learner-group-pages/view-group/view-learner-group-page-routing.module.ts index 9dff290fe355..a00b4984cc1d 100644 --- a/core/templates/pages/learner-group-pages/view-group/view-learner-group-page-routing.module.ts +++ b/core/templates/pages/learner-group-pages/view-group/view-learner-group-page-routing.module.ts @@ -16,25 +16,19 @@ * @fileoverview Routing module for view learner group page. */ -import { NgModule } from '@angular/core'; -import { Route, RouterModule } from '@angular/router'; -import { ViewLearnerGroupPageRootComponent } from - './view-learner-group-page-root.component'; +import {NgModule} from '@angular/core'; +import {Route, RouterModule} from '@angular/router'; +import {ViewLearnerGroupPageRootComponent} from './view-learner-group-page-root.component'; const routes: Route[] = [ { path: '', - component: ViewLearnerGroupPageRootComponent - } + component: ViewLearnerGroupPageRootComponent, + }, ]; @NgModule({ - imports: [ - RouterModule.forChild(routes) - ], - exports: [ - RouterModule - ] + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], }) - export class ViewLearnerGroupPageRoutingModule {} diff --git a/core/templates/pages/learner-group-pages/view-group/view-learner-group-page.component.spec.ts b/core/templates/pages/learner-group-pages/view-group/view-learner-group-page.component.spec.ts index 6f22f1e6a7bb..e0d1a80c5673 100644 --- a/core/templates/pages/learner-group-pages/view-group/view-learner-group-page.component.spec.ts +++ b/core/templates/pages/learner-group-pages/view-group/view-learner-group-page.component.spec.ts @@ -16,25 +16,28 @@ * @fileoverview Unit tests for view learner group page. */ -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick } from - '@angular/core/testing'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { LearnerGroupBackendApiService } from - 'domain/learner_group/learner-group-backend-api.service'; -import { LearnerGroupPagesConstants } from '../learner-group-pages.constants'; -import { LearnerGroupData } from 'domain/learner_group/learner-group.model'; -import { TranslateService } from '@ngx-translate/core'; -import { ViewLearnerGroupPageComponent } from './view-learner-group-page.component'; -import { ContextService } from 'services/context.service'; -import { LearnerGroupUserProgress } from 'domain/learner_group/learner-group-user-progress.model'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { LoaderService } from 'services/loader.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { LearnerGroupSyllabusBackendApiService } from 'domain/learner_group/learner-group-syllabus-backend-api.service'; -import { UserService } from 'services/user.service'; -import { UserInfo } from 'domain/user/user-info.model'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {LearnerGroupBackendApiService} from 'domain/learner_group/learner-group-backend-api.service'; +import {LearnerGroupPagesConstants} from '../learner-group-pages.constants'; +import {LearnerGroupData} from 'domain/learner_group/learner-group.model'; +import {TranslateService} from '@ngx-translate/core'; +import {ViewLearnerGroupPageComponent} from './view-learner-group-page.component'; +import {ContextService} from 'services/context.service'; +import {LearnerGroupUserProgress} from 'domain/learner_group/learner-group-user-progress.model'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {LoaderService} from 'services/loader.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {LearnerGroupSyllabusBackendApiService} from 'domain/learner_group/learner-group-syllabus-backend-api.service'; +import {UserService} from 'services/user.service'; +import {UserInfo} from 'domain/user/user-info.model'; class MockTranslateService { onLangChange: EventEmitter = new EventEmitter(); @@ -49,7 +52,7 @@ class MockWindowRef { location: { href: '', }, - gtag: () => {} + gtag: () => {}, }; } @@ -62,8 +65,7 @@ describe('ViewLearnerGroupPageComponent', () => { let loaderService: LoaderService; let windowRef: MockWindowRef; let userService: UserService; - let learnerGroupSyllabusBackendApiService: - LearnerGroupSyllabusBackendApiService; + let learnerGroupSyllabusBackendApiService: LearnerGroupSyllabusBackendApiService; const sampleLearnerGroupSubtopicSummaryDict = { subtopic_id: 1, @@ -72,7 +74,7 @@ describe('ViewLearnerGroupPageComponent', () => { parent_topic_name: 'parentTopicName', thumbnail_filename: 'thumbnailFilename', thumbnail_bg_color: 'red', - subtopic_mastery: 0.95 + subtopic_mastery: 0.95, }; const sampleStorySummaryBackendDict = { @@ -88,61 +90,74 @@ describe('ViewLearnerGroupPageComponent', () => { all_node_dicts: [], topic_name: 'Topic one', topic_url_fragment: 'topic-one', - classroom_url_fragment: 'math' + classroom_url_fragment: 'math', }; const sampleLearnerGroupUserProgDict = { username: 'username2', progress_sharing_is_turned_on: true, stories_progress: [sampleStorySummaryBackendDict], - subtopic_pages_progress: [sampleLearnerGroupSubtopicSummaryDict] + subtopic_pages_progress: [sampleLearnerGroupSubtopicSummaryDict], }; - const sampleLearnerGroupUserProg = ( + const sampleLearnerGroupUserProg = LearnerGroupUserProgress.createFromBackendDict( - sampleLearnerGroupUserProgDict) - ); + sampleLearnerGroupUserProgDict + ); const learnerGroup = new LearnerGroupData( - 'groupId', 'title', 'description', ['facilitator_username'], - ['username2'], ['username1'], ['subtopic_page_id'], [] + 'groupId', + 'title', + 'description', + ['facilitator_username'], + ['username2'], + ['username1'], + ['subtopic_page_id'], + [] ); const userInfo = new UserInfo( - ['USER_ROLE'], true, false, false, false, true, - 'en', 'username1', 'tester@example.com', true + ['USER_ROLE'], + true, + false, + false, + false, + true, + 'en', + 'username1', + 'tester@example.com', + true ); beforeEach(() => { windowRef = new MockWindowRef(); TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - ViewLearnerGroupPageComponent, - MockTranslatePipe - ], + declarations: [ViewLearnerGroupPageComponent, MockTranslatePipe], providers: [ { provide: WindowRef, - useValue: windowRef + useValue: windowRef, }, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); beforeEach(() => { learnerGroupBackendApiService = TestBed.inject( - LearnerGroupBackendApiService); + LearnerGroupBackendApiService + ); contextService = TestBed.inject(ContextService); ngbModal = TestBed.inject(NgbModal); loaderService = TestBed.inject(LoaderService); userService = TestBed.inject(UserService); learnerGroupSyllabusBackendApiService = TestBed.inject( - LearnerGroupSyllabusBackendApiService); + LearnerGroupSyllabusBackendApiService + ); fixture = TestBed.createComponent(ViewLearnerGroupPageComponent); component = fixture.componentInstance; @@ -152,8 +167,10 @@ describe('ViewLearnerGroupPageComponent', () => { }); it('should initialize', fakeAsync(() => { - spyOn(learnerGroupBackendApiService, 'fetchLearnerGroupInfoAsync') - .and.returnValue(Promise.resolve(learnerGroup)); + spyOn( + learnerGroupBackendApiService, + 'fetchLearnerGroupInfoAsync' + ).and.returnValue(Promise.resolve(learnerGroup)); spyOn( learnerGroupBackendApiService, 'fetchProgressSharingPermissionOfLearnerAsync' @@ -163,13 +180,15 @@ describe('ViewLearnerGroupPageComponent', () => { 'fetchLearnerSpecificProgressInAssignedSyllabus' ).and.returnValue(Promise.resolve(sampleLearnerGroupUserProg)); spyOn(userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(userInfo)); + Promise.resolve(userInfo) + ); component.ngOnInit(); tick(); expect(component.activeTab).toEqual( - LearnerGroupPagesConstants.VIEW_LEARNER_GROUP_TABS.OVERVIEW); + LearnerGroupPagesConstants.VIEW_LEARNER_GROUP_TABS.OVERVIEW + ); expect(component.learnerGroup).toEqual(learnerGroup); expect(component.getLearnersCount()).toBe(1); expect(component.username).toBe('username1'); @@ -178,17 +197,21 @@ describe('ViewLearnerGroupPageComponent', () => { it('should set active tab and check if tab is active correctly', () => { component.setActiveTab( - LearnerGroupPagesConstants.VIEW_LEARNER_GROUP_TABS.ASSIGNED_SYLLABUS); + LearnerGroupPagesConstants.VIEW_LEARNER_GROUP_TABS.ASSIGNED_SYLLABUS + ); expect(component.activeTab).toEqual( - LearnerGroupPagesConstants.VIEW_LEARNER_GROUP_TABS.ASSIGNED_SYLLABUS); + LearnerGroupPagesConstants.VIEW_LEARNER_GROUP_TABS.ASSIGNED_SYLLABUS + ); let tabIsActive = component.isTabActive( - LearnerGroupPagesConstants.VIEW_LEARNER_GROUP_TABS.ASSIGNED_SYLLABUS); + LearnerGroupPagesConstants.VIEW_LEARNER_GROUP_TABS.ASSIGNED_SYLLABUS + ); expect(tabIsActive).toBeTrue(); tabIsActive = component.isTabActive( - LearnerGroupPagesConstants.VIEW_LEARNER_GROUP_TABS.OVERVIEW); + LearnerGroupPagesConstants.VIEW_LEARNER_GROUP_TABS.OVERVIEW + ); expect(tabIsActive).toBeFalse(); }); @@ -207,10 +230,12 @@ describe('ViewLearnerGroupPageComponent', () => { componentInstance: { learnerGroupTitle: learnerGroup.title, }, - result: Promise.resolve() + result: Promise.resolve(), } as NgbModalRef); - spyOn(learnerGroupBackendApiService, 'exitLearnerGroupAsync') - .and.returnValue(Promise.resolve(learnerGroup)); + spyOn( + learnerGroupBackendApiService, + 'exitLearnerGroupAsync' + ).and.returnValue(Promise.resolve(learnerGroup)); spyOn(loaderService, 'showLoadingScreen'); component.learnerGroup = learnerGroup; @@ -220,34 +245,34 @@ describe('ViewLearnerGroupPageComponent', () => { tick(); expect(windowRef.nativeWindow.location.href).toBe( - '/learner-dashboard?active_tab=learner-groups'); + '/learner-dashboard?active_tab=learner-groups' + ); expect(loaderService.showLoadingScreen).toHaveBeenCalledWith( - 'Exiting Group'); + 'Exiting Group' + ); })); - it('should correctly view and update learner group preferences', - fakeAsync(() => { - spyOn(ngbModal, 'open').and.returnValue({ - componentInstance: { - learnerGroup: learnerGroup, - progressSharingPermission: true - }, - result: Promise.resolve({ - progressSharingPermission: false - }) - } as NgbModalRef); - spyOn( - learnerGroupBackendApiService, - 'updateProgressSharingPermissionAsync' - ).and.returnValue(Promise.resolve(false)); - - component.learnerGroup = learnerGroup; - component.progressSharingPermission = true; - - component.viewLearnerGroupPreferences(); - tick(); - - expect(component.progressSharingPermission).toBe(false); - }) - ); + it('should correctly view and update learner group preferences', fakeAsync(() => { + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: { + learnerGroup: learnerGroup, + progressSharingPermission: true, + }, + result: Promise.resolve({ + progressSharingPermission: false, + }), + } as NgbModalRef); + spyOn( + learnerGroupBackendApiService, + 'updateProgressSharingPermissionAsync' + ).and.returnValue(Promise.resolve(false)); + + component.learnerGroup = learnerGroup; + component.progressSharingPermission = true; + + component.viewLearnerGroupPreferences(); + tick(); + + expect(component.progressSharingPermission).toBe(false); + })); }); diff --git a/core/templates/pages/learner-group-pages/view-group/view-learner-group-page.component.ts b/core/templates/pages/learner-group-pages/view-group/view-learner-group-page.component.ts index d48c2bfc01fa..55ccb00930c4 100644 --- a/core/templates/pages/learner-group-pages/view-group/view-learner-group-page.component.ts +++ b/core/templates/pages/learner-group-pages/view-group/view-learner-group-page.component.ts @@ -16,35 +16,31 @@ * @fileoverview Component for the view learner group page. */ -import { Component, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; - -import { LoaderService } from 'services/loader.service'; -import { LearnerGroupPagesConstants } from '../learner-group-pages.constants'; -import { LearnerGroupData } from 'domain/learner_group/learner-group.model'; -import { LearnerGroupBackendApiService } from - 'domain/learner_group/learner-group-backend-api.service'; -import { ContextService } from 'services/context.service'; -import { LearnerGroupSyllabusBackendApiService } from - 'domain/learner_group/learner-group-syllabus-backend-api.service'; -import { UserService } from 'services/user.service'; -import { LearnerGroupUserProgress } from 'domain/learner_group/learner-group-user-progress.model'; -import { ExitLearnerGroupModalComponent } from - '../templates/exit-learner-group-modal.component'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { LearnerGroupPreferencesModalComponent } from - '../templates/learner-group-preferences-modal.component'; +import {Component, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; + +import {LoaderService} from 'services/loader.service'; +import {LearnerGroupPagesConstants} from '../learner-group-pages.constants'; +import {LearnerGroupData} from 'domain/learner_group/learner-group.model'; +import {LearnerGroupBackendApiService} from 'domain/learner_group/learner-group-backend-api.service'; +import {ContextService} from 'services/context.service'; +import {LearnerGroupSyllabusBackendApiService} from 'domain/learner_group/learner-group-syllabus-backend-api.service'; +import {UserService} from 'services/user.service'; +import {LearnerGroupUserProgress} from 'domain/learner_group/learner-group-user-progress.model'; +import {ExitLearnerGroupModalComponent} from '../templates/exit-learner-group-modal.component'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {LearnerGroupPreferencesModalComponent} from '../templates/learner-group-preferences-modal.component'; import './view-learner-group-page.component.css'; @Component({ selector: 'oppia-view-learner-group-page', - templateUrl: './view-learner-group-page.component.html' + templateUrl: './view-learner-group-page.component.html', }) export class ViewLearnerGroupPageComponent implements OnInit { - VIEW_LEARNER_GROUP_TABS_I18N_IDS = ( - LearnerGroupPagesConstants.VIEW_LEARNER_GROUP_TABS); + VIEW_LEARNER_GROUP_TABS_I18N_IDS = + LearnerGroupPagesConstants.VIEW_LEARNER_GROUP_TABS; activeTab!: string; learnerGroupId!: string; @@ -60,8 +56,7 @@ export class ViewLearnerGroupPageComponent implements OnInit { private userService: UserService, private ngbModal: NgbModal, private windowRef: WindowRef, - private learnerGroupSyllabusBackendApiService: - LearnerGroupSyllabusBackendApiService + private learnerGroupSyllabusBackendApiService: LearnerGroupSyllabusBackendApiService ) {} ngOnInit(): void { @@ -69,26 +64,25 @@ export class ViewLearnerGroupPageComponent implements OnInit { this.activeTab = this.VIEW_LEARNER_GROUP_TABS_I18N_IDS.OVERVIEW; if (this.learnerGroupId) { this.loaderService.showLoadingScreen('Loading'); - this.learnerGroupBackendApiService.fetchLearnerGroupInfoAsync( - this.learnerGroupId - ).then(learnerGroupInfo => { - this.learnerGroup = learnerGroupInfo; - this.learnerGroupBackendApiService - .fetchProgressSharingPermissionOfLearnerAsync(this.learnerGroup.id) - .then(progressSharingPermission => { - this.progressSharingPermission = progressSharingPermission; + this.learnerGroupBackendApiService + .fetchLearnerGroupInfoAsync(this.learnerGroupId) + .then(learnerGroupInfo => { + this.learnerGroup = learnerGroupInfo; + this.learnerGroupBackendApiService + .fetchProgressSharingPermissionOfLearnerAsync(this.learnerGroup.id) + .then(progressSharingPermission => { + this.progressSharingPermission = progressSharingPermission; + }); + this.userService.getUserInfoAsync().then(userInfo => { + this.username = userInfo.getUsername(); }); - this.userService.getUserInfoAsync().then(userInfo => { - this.username = userInfo.getUsername(); + this.learnerGroupSyllabusBackendApiService + .fetchLearnerSpecificProgressInAssignedSyllabus(this.learnerGroupId) + .then(learnerProgress => { + this.learnerProgress = learnerProgress; + this.loaderService.hideLoadingScreen(); + }); }); - this.learnerGroupSyllabusBackendApiService - .fetchLearnerSpecificProgressInAssignedSyllabus( - this.learnerGroupId - ).then(learnerProgress => { - this.learnerProgress = learnerProgress; - this.loaderService.hideLoadingScreen(); - }); - }); } } @@ -120,7 +114,8 @@ export class ViewLearnerGroupPageComponent implements OnInit { getMasteredSubtopicsCountOfLearner(): number { let masteredSubtopicsCount = 0; this.learnerProgress.subtopicsProgress.forEach(subtopicProgress => { - if (subtopicProgress.subtopicMastery && + if ( + subtopicProgress.subtopicMastery && subtopicProgress.subtopicMastery >= 0.9 ) { masteredSubtopicsCount += 1; @@ -130,59 +125,64 @@ export class ViewLearnerGroupPageComponent implements OnInit { } exitLearnerGroup(): void { - let modalRef = this.ngbModal.open( - ExitLearnerGroupModalComponent, - { - backdrop: 'static', - windowClass: 'exit-learner-group-modal' - } - ); + let modalRef = this.ngbModal.open(ExitLearnerGroupModalComponent, { + backdrop: 'static', + windowClass: 'exit-learner-group-modal', + }); modalRef.componentInstance.learnerGroupTitle = this.learnerGroup.title; - modalRef.result.then(() => { - if (this.username) { - this.loaderService.showLoadingScreen('Exiting Group'); - this.learnerGroupBackendApiService.exitLearnerGroupAsync( - this.learnerGroup.id, this.username - ).then(() => { - this.windowRef.nativeWindow.location.href = ( - '/learner-dashboard?active_tab=learner-groups'); - }); + modalRef.result.then( + () => { + if (this.username) { + this.loaderService.showLoadingScreen('Exiting Group'); + this.learnerGroupBackendApiService + .exitLearnerGroupAsync(this.learnerGroup.id, this.username) + .then(() => { + this.windowRef.nativeWindow.location.href = + '/learner-dashboard?active_tab=learner-groups'; + }); + } + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. } - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + ); } viewLearnerGroupPreferences(): void { - let modalRef = this.ngbModal.open( - LearnerGroupPreferencesModalComponent, - { - backdrop: 'static', - windowClass: 'learner-group-preferences-modal' - } - ); + let modalRef = this.ngbModal.open(LearnerGroupPreferencesModalComponent, { + backdrop: 'static', + windowClass: 'learner-group-preferences-modal', + }); modalRef.componentInstance.learnerGroup = this.learnerGroup; - modalRef.componentInstance.progressSharingPermission = ( - this.progressSharingPermission); + modalRef.componentInstance.progressSharingPermission = + this.progressSharingPermission; - modalRef.result.then((data) => { - this.learnerGroupBackendApiService - .updateProgressSharingPermissionAsync( - this.learnerGroup.id, data.progressSharingPermission - ).then((updatedProgressSharingPermission) => { - this.progressSharingPermission = updatedProgressSharingPermission; - }); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + modalRef.result.then( + data => { + this.learnerGroupBackendApiService + .updateProgressSharingPermissionAsync( + this.learnerGroup.id, + data.progressSharingPermission + ) + .then(updatedProgressSharingPermission => { + this.progressSharingPermission = updatedProgressSharingPermission; + }); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } } -angular.module('oppia').directive( - 'oppiaViewLearnerGroupPage', - downgradeComponent({component: ViewLearnerGroupPageComponent})); +angular + .module('oppia') + .directive( + 'oppiaViewLearnerGroupPage', + downgradeComponent({component: ViewLearnerGroupPageComponent}) + ); diff --git a/core/templates/pages/learner-group-pages/view-group/view-learner-group-page.module.ts b/core/templates/pages/learner-group-pages/view-group/view-learner-group-page.module.ts index 7eb3b775f88c..1b3974f1812c 100644 --- a/core/templates/pages/learner-group-pages/view-group/view-learner-group-page.module.ts +++ b/core/templates/pages/learner-group-pages/view-group/view-learner-group-page.module.ts @@ -16,19 +16,19 @@ * @fileoverview Module for the view learner group page. */ -import { NgModule } from '@angular/core'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'; -import { CommonModule } from '@angular/common'; -import { Error404PageModule } from 'pages/error-pages/error-404/error-404-page.module'; -import { SmartRouterModule } from 'hybrid-router-module-provider'; -import { ViewLearnerGroupPageRootComponent } from './view-learner-group-page-root.component'; -import { ViewLearnerGroupPageRoutingModule } from './view-learner-group-page-routing.module'; -import { ViewLearnerGroupPageComponent } from './view-learner-group-page.component'; -import { LearnerGroupViewAssignedSyllabusComponent } from './learner-group-view-assigned-syllabus.component'; -import { LearnerGroupPreferencesModalComponent } from '../templates/learner-group-preferences-modal.component'; -import { ExitLearnerGroupModalComponent } from '../templates/exit-learner-group-modal.component'; -import { SharedLearnerGroupComponentsModule } from '../shared-learner-group-component.module'; +import {NgModule} from '@angular/core'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {NgbPopoverModule} from '@ng-bootstrap/ng-bootstrap'; +import {CommonModule} from '@angular/common'; +import {Error404PageModule} from 'pages/error-pages/error-404/error-404-page.module'; +import {SmartRouterModule} from 'hybrid-router-module-provider'; +import {ViewLearnerGroupPageRootComponent} from './view-learner-group-page-root.component'; +import {ViewLearnerGroupPageRoutingModule} from './view-learner-group-page-routing.module'; +import {ViewLearnerGroupPageComponent} from './view-learner-group-page.component'; +import {LearnerGroupViewAssignedSyllabusComponent} from './learner-group-view-assigned-syllabus.component'; +import {LearnerGroupPreferencesModalComponent} from '../templates/learner-group-preferences-modal.component'; +import {ExitLearnerGroupModalComponent} from '../templates/exit-learner-group-modal.component'; +import {SharedLearnerGroupComponentsModule} from '../shared-learner-group-component.module'; @NgModule({ imports: [ @@ -40,21 +40,21 @@ import { SharedLearnerGroupComponentsModule } from '../shared-learner-group-comp SmartRouterModule, ViewLearnerGroupPageRoutingModule, SharedLearnerGroupComponentsModule, - Error404PageModule + Error404PageModule, ], declarations: [ ViewLearnerGroupPageComponent, ViewLearnerGroupPageRootComponent, LearnerGroupViewAssignedSyllabusComponent, ExitLearnerGroupModalComponent, - LearnerGroupPreferencesModalComponent + LearnerGroupPreferencesModalComponent, ], entryComponents: [ ViewLearnerGroupPageComponent, ViewLearnerGroupPageRootComponent, LearnerGroupViewAssignedSyllabusComponent, ExitLearnerGroupModalComponent, - LearnerGroupPreferencesModalComponent - ] + LearnerGroupPreferencesModalComponent, + ], }) export class ViewLearnerGroupPageModule {} diff --git a/core/templates/pages/library-page/library-footer/library-footer.component.spec.ts b/core/templates/pages/library-page/library-footer/library-footer.component.spec.ts index 16c21f1b3af5..b33d9b1427e6 100644 --- a/core/templates/pages/library-page/library-footer/library-footer.component.spec.ts +++ b/core/templates/pages/library-page/library-footer/library-footer.component.spec.ts @@ -16,16 +16,16 @@ * @fileoverview Unit tests for libraryFooter. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { LibraryFooterComponent } from './library-footer.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {LibraryFooterComponent} from './library-footer.component'; class MockWindowRef { nativeWindow = { location: { - pathname: '/search/find' - } + pathname: '/search/find', + }, }; } @@ -35,16 +35,14 @@ describe('Library footer component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - LibraryFooterComponent - ], + declarations: [LibraryFooterComponent], providers: [ { provide: WindowRef, - useClass: MockWindowRef - } + useClass: MockWindowRef, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); diff --git a/core/templates/pages/library-page/library-footer/library-footer.component.ts b/core/templates/pages/library-page/library-footer/library-footer.component.ts index 209c2be67e34..adce18b1f809 100644 --- a/core/templates/pages/library-page/library-footer/library-footer.component.ts +++ b/core/templates/pages/library-page/library-footer/library-footer.component.ts @@ -16,34 +16,36 @@ * @fileoverview Component for the library footer. */ -import { Component } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { LibraryPageConstants } from '../library-page.constants'; +import {Component} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {LibraryPageConstants} from '../library-page.constants'; -type LibraryPathToModesKeys = ( - keyof typeof LibraryPageConstants.LIBRARY_PATHS_TO_MODES); +type LibraryPathToModesKeys = + keyof typeof LibraryPageConstants.LIBRARY_PATHS_TO_MODES; @Component({ selector: 'oppia-library-footer', - templateUrl: './library-footer.component.html' + templateUrl: './library-footer.component.html', }) export class LibraryFooterComponent { footerIsDisplayed: boolean = false; - constructor( - private windowRef: WindowRef - ) {} + constructor(private windowRef: WindowRef) {} ngOnInit(): void { - let pageMode = LibraryPageConstants.LIBRARY_PATHS_TO_MODES[ - this.windowRef.nativeWindow.location.pathname as LibraryPathToModesKeys]; - this.footerIsDisplayed = ( - pageMode !== LibraryPageConstants.LIBRARY_PAGE_MODES.SEARCH); + let pageMode = + LibraryPageConstants.LIBRARY_PATHS_TO_MODES[ + this.windowRef.nativeWindow.location.pathname as LibraryPathToModesKeys + ]; + this.footerIsDisplayed = + pageMode !== LibraryPageConstants.LIBRARY_PAGE_MODES.SEARCH; } } -angular.module('oppia').directive('oppiaLibraryFooter', +angular.module('oppia').directive( + 'oppiaLibraryFooter', downgradeComponent({ - component: LibraryFooterComponent - }) as angular.IDirectiveFactory); + component: LibraryFooterComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/library-page/library-page-root.component.spec.ts b/core/templates/pages/library-page/library-page-root.component.spec.ts index 6ce4b7498100..542c238dffa1 100644 --- a/core/templates/pages/library-page/library-page-root.component.spec.ts +++ b/core/templates/pages/library-page/library-page-root.component.spec.ts @@ -16,14 +16,14 @@ * @fileoverview Unit tests for the library page root component. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { LibraryPageRootComponent } from './library-page-root.component'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {LibraryPageRootComponent} from './library-page-root.component'; describe('Library Page Root', () => { let fixture: ComponentFixture; @@ -32,14 +32,9 @@ describe('Library Page Root', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - LibraryPageRootComponent, - MockTranslatePipe - ], - providers: [ - PageHeadService - ], - schemas: [NO_ERRORS_SCHEMA] + declarations: [LibraryPageRootComponent, MockTranslatePipe], + providers: [PageHeadService], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -49,10 +44,9 @@ describe('Library Page Root', () => { pageHeadService = TestBed.inject(PageHeadService); }); - it('should successfully instantiate the component', - () => { - expect(component).toBeDefined(); - }); + it('should successfully instantiate the component', () => { + expect(component).toBeDefined(); + }); it('should initialize', () => { spyOn(pageHeadService, 'updateTitleAndMetaTags'); @@ -61,6 +55,7 @@ describe('Library Page Root', () => { expect(pageHeadService.updateTitleAndMetaTags).toHaveBeenCalledWith( AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LIBRARY_INDEX.TITLE, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LIBRARY_INDEX.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LIBRARY_INDEX.META + ); }); }); diff --git a/core/templates/pages/library-page/library-page-root.component.ts b/core/templates/pages/library-page/library-page-root.component.ts index 1e39f42413be..e3dca756b4aa 100644 --- a/core/templates/pages/library-page/library-page-root.component.ts +++ b/core/templates/pages/library-page/library-page-root.component.ts @@ -16,23 +16,22 @@ * @fileoverview Root component for library page. */ -import { Component } from '@angular/core'; +import {Component} from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; @Component({ selector: 'oppia-library-page-root', - templateUrl: './library-page-root.component.html' + templateUrl: './library-page-root.component.html', }) export class LibraryPageRootComponent { - constructor( - private pageHeadService: PageHeadService - ) {} + constructor(private pageHeadService: PageHeadService) {} ngOnInit(): void { this.pageHeadService.updateTitleAndMetaTags( AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LIBRARY_INDEX.TITLE, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LIBRARY_INDEX.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LIBRARY_INDEX.META + ); } } diff --git a/core/templates/pages/library-page/library-page-routing.module.ts b/core/templates/pages/library-page/library-page-routing.module.ts index b8e8ebdb27bb..398a38ec9938 100644 --- a/core/templates/pages/library-page/library-page-routing.module.ts +++ b/core/templates/pages/library-page/library-page-routing.module.ts @@ -16,24 +16,19 @@ * @fileoverview Routing module for library page. */ -import { NgModule } from '@angular/core'; -import { Route, RouterModule } from '@angular/router'; -import { LibraryPageRootComponent } from './library-page-root.component'; +import {NgModule} from '@angular/core'; +import {Route, RouterModule} from '@angular/router'; +import {LibraryPageRootComponent} from './library-page-root.component'; const routes: Route[] = [ { path: '', - component: LibraryPageRootComponent - } + component: LibraryPageRootComponent, + }, ]; @NgModule({ - imports: [ - RouterModule.forChild(routes) - ], - exports: [ - RouterModule - ] + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], }) - export class LibraryPageRoutingModule {} diff --git a/core/templates/pages/library-page/library-page.component.spec.ts b/core/templates/pages/library-page/library-page.component.spec.ts index 836727a76070..57cc5e3ae1c8 100644 --- a/core/templates/pages/library-page/library-page.component.spec.ts +++ b/core/templates/pages/library-page/library-page.component.spec.ts @@ -16,36 +16,46 @@ * @fileoverview Unit tests for the component of the library page. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; - -import { AppConstants } from 'app.constants'; -import { CollectionSummaryBackendDict } from 'domain/collection/collection-summary.model'; -import { CreatorExplorationSummaryBackendDict } from 'domain/summary/creator-exploration-summary.model'; -import { UserInfo } from 'domain/user/user-info.model'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { LoggerService } from 'services/contextual/logger.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { KeyboardShortcutService } from 'services/keyboard-shortcut.service'; -import { LoaderService } from 'services/loader.service'; -import { PageTitleService } from 'services/page-title.service'; -import { SearchService } from 'services/search.service'; -import { UserService } from 'services/user.service'; -import { MockTranslateModule } from 'tests/unit-test-utils'; -import { LibraryPageComponent } from './library-page.component'; -import { ActivityDict, LibraryIndexData, LibraryPageBackendApiService } from './services/library-page-backend-api.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; + +import {AppConstants} from 'app.constants'; +import {CollectionSummaryBackendDict} from 'domain/collection/collection-summary.model'; +import {CreatorExplorationSummaryBackendDict} from 'domain/summary/creator-exploration-summary.model'; +import {UserInfo} from 'domain/user/user-info.model'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {LoggerService} from 'services/contextual/logger.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {KeyboardShortcutService} from 'services/keyboard-shortcut.service'; +import {LoaderService} from 'services/loader.service'; +import {PageTitleService} from 'services/page-title.service'; +import {SearchService} from 'services/search.service'; +import {UserService} from 'services/user.service'; +import {MockTranslateModule} from 'tests/unit-test-utils'; +import {LibraryPageComponent} from './library-page.component'; +import { + ActivityDict, + LibraryIndexData, + LibraryPageBackendApiService, +} from './services/library-page-backend-api.service'; class MockWindowRef { nativeWindow = { location: { pathname: '/community-library/top-rated', - href: '' - } + href: '', + }, }; } @@ -55,9 +65,9 @@ class MockWindowDimensionsService { subscribe: (callb: () => void) => { callb(); return { - unsubscribe() {} + unsubscribe() {}, }; - } + }, }; } @@ -89,116 +99,119 @@ describe('Library Page Component', () => { let searchService: SearchService; let translateService: TranslateService; - let explorationList: CreatorExplorationSummaryBackendDict[] = [{ - category: '', - community_owned: true, - activity_type: AppConstants.ACTIVITY_TYPE_EXPLORATION, - last_updated_msec: 1, - ratings: { - 1: 0, - 2: 0, - 3: 0, - 4: 0, - 5: 0 + let explorationList: CreatorExplorationSummaryBackendDict[] = [ + { + category: '', + community_owned: true, + activity_type: AppConstants.ACTIVITY_TYPE_EXPLORATION, + last_updated_msec: 1, + ratings: { + 1: 0, + 2: 0, + 3: 0, + 4: 0, + 5: 0, + }, + id: 'id1', + created_on_msec: 12, + human_readable_contributors_summary: {}, + language_code: '', + num_views: 2, + objective: '', + status: '', + tags: [], + thumbnail_bg_color: '', + thumbnail_icon_url: '', + title: '', + num_total_threads: 3, + num_open_threads: 3, + }, + ]; + + let collectionList: CollectionSummaryBackendDict[] = [ + { + category: '', + community_owned: true, + last_updated_msec: 2, + id: '', + created_on: 2, + language_code: '', + objective: '', + status: '', + thumbnail_bg_color: '', + thumbnail_icon_url: '', + title: '', + node_count: 2, }, - id: 'id1', - created_on_msec: 12, - human_readable_contributors_summary: {}, - language_code: '', - num_views: 2, - objective: '', - status: '', - tags: [], - thumbnail_bg_color: '', - thumbnail_icon_url: '', - title: '', - num_total_threads: 3, - num_open_threads: 3 - }]; - - let collectionList: CollectionSummaryBackendDict[] = [{ - category: '', - community_owned: true, - last_updated_msec: 2, - id: '', - created_on: 2, - language_code: '', - objective: '', - status: '', - thumbnail_bg_color: '', - thumbnail_icon_url: '', - title: '', - node_count: 2 - }]; + ]; let libraryIndexData: LibraryIndexData = { - activity_summary_dicts_by_category: [{ - activity_summary_dicts: [{ - activity_type: AppConstants.ACTIVITY_TYPE_EXPLORATION, - category: '', - community_owned: true, - id: 'id1', - language_code: '', - num_views: 5, - objective: '', - status: '', - tags: [], - thumbnail_bg_color: '', - thumbnail_icon_url: '', - title: '' - }, + activity_summary_dicts_by_category: [ { - activity_type: AppConstants.ACTIVITY_TYPE_COLLECTION, - category: '', - community_owned: true, - id: 'id2', - language_code: '', - num_views: 5, - objective: '', - status: '', - tags: [], - thumbnail_bg_color: '', - thumbnail_icon_url: '', - title: '' + activity_summary_dicts: [ + { + activity_type: AppConstants.ACTIVITY_TYPE_EXPLORATION, + category: '', + community_owned: true, + id: 'id1', + language_code: '', + num_views: 5, + objective: '', + status: '', + tags: [], + thumbnail_bg_color: '', + thumbnail_icon_url: '', + title: '', + }, + { + activity_type: AppConstants.ACTIVITY_TYPE_COLLECTION, + category: '', + community_owned: true, + id: 'id2', + language_code: '', + num_views: 5, + objective: '', + status: '', + tags: [], + thumbnail_bg_color: '', + thumbnail_icon_url: '', + title: '', + }, + { + activity_type: '', + category: '', + community_owned: true, + id: 'id1', + language_code: '', + num_views: 5, + objective: '', + status: '', + tags: [], + thumbnail_bg_color: '', + thumbnail_icon_url: '', + title: '', + }, + ], + categories: [], + header_i18n_id: 'id', + has_full_results_page: true, + full_results_url: '', + protractor_id: '', }, - { - activity_type: '', - category: '', - community_owned: true, - id: 'id1', - language_code: '', - num_views: 5, - objective: '', - status: '', - tags: [], - thumbnail_bg_color: '', - thumbnail_icon_url: '', - title: '' - }], - categories: [], - header_i18n_id: 'id', - has_full_results_page: true, - full_results_url: '', - protractor_id: '' - }], - preferred_language_codes: [] + ], + preferred_language_codes: [], }; beforeEach(waitForAsync(() => { windowRef = new MockWindowRef(); TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - MockTranslateModule - ], - declarations: [ - LibraryPageComponent - ], + imports: [HttpClientTestingModule, MockTranslateModule], + declarations: [LibraryPageComponent], providers: [ LoggerService, { provide: WindowRef, - useValue: windowRef + useValue: windowRef, }, I18nLanguageCodeService, KeyboardShortcutService, @@ -209,15 +222,15 @@ describe('Library Page Component', () => { UserService, { provide: WindowDimensionsService, - useClass: MockWindowDimensionsService + useClass: MockWindowDimensionsService, }, PageTitleService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -249,12 +262,16 @@ describe('Library Page Component', () => { spyOn(loaderService, 'showLoadingScreen'); spyOn(urlInterpolationService, 'getStaticImageUrl'); spyOn(translateService.onLangChange, 'subscribe'); - spyOn(libraryPageBackendApiService, 'fetchLibraryGroupDataAsync') - .and.returnValue(Promise.resolve({ + spyOn( + libraryPageBackendApiService, + 'fetchLibraryGroupDataAsync' + ).and.returnValue( + Promise.resolve({ activity_list: [], header_i18n_id: '', - preferred_language_codes: [] - })); + preferred_language_codes: [], + }) + ); spyOn(i18nLanguageCodeService.onPreferredLanguageCodesLoaded, 'emit'); spyOn(loaderService, 'hideLoadingScreen'); spyOn(windowDimensionsService, 'getWidth').and.returnValue(900); @@ -263,12 +280,15 @@ describe('Library Page Component', () => { tick(); tick(); expect(loaderService.showLoadingScreen).toHaveBeenCalledWith( - 'I18N_LIBRARY_LOADING'); + 'I18N_LIBRARY_LOADING' + ); expect(translateService.onLangChange.subscribe).toHaveBeenCalled(); - expect(libraryPageBackendApiService.fetchLibraryGroupDataAsync) - .toHaveBeenCalled(); - expect(i18nLanguageCodeService.onPreferredLanguageCodesLoaded.emit) - .toHaveBeenCalled(); + expect( + libraryPageBackendApiService.fetchLibraryGroupDataAsync + ).toHaveBeenCalled(); + expect( + i18nLanguageCodeService.onPreferredLanguageCodesLoaded.emit + ).toHaveBeenCalled(); expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); expect(windowDimensionsService.getWidth).toHaveBeenCalled(); })); @@ -279,17 +299,35 @@ describe('Library Page Component', () => { spyOn(translateService.onLangChange, 'subscribe'); windowRef.nativeWindow.location.pathname = '/community-library'; fixture.detectChanges(); - spyOn(libraryPageBackendApiService, 'fetchLibraryIndexDataAsync') - .and.returnValue(Promise.resolve(libraryIndexData)); - spyOn(userService, 'getUserInfoAsync').and.returnValue(Promise.resolve( - new UserInfo( - ['role'], true, true, true, true, true, 'en', 'user', - 'user@user.com', true))); - spyOn(libraryPageBackendApiService, 'fetchCreatorDashboardDataAsync') - .and.returnValue(Promise.resolve({ + spyOn( + libraryPageBackendApiService, + 'fetchLibraryIndexDataAsync' + ).and.returnValue(Promise.resolve(libraryIndexData)); + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve( + new UserInfo( + ['role'], + true, + true, + true, + true, + true, + 'en', + 'user', + 'user@user.com', + true + ) + ) + ); + spyOn( + libraryPageBackendApiService, + 'fetchCreatorDashboardDataAsync' + ).and.returnValue( + Promise.resolve({ explorations_list: explorationList, - collections_list: collectionList - })); + collections_list: collectionList, + }) + ); spyOn(loaderService, 'hideLoadingScreen'); spyOn(i18nLanguageCodeService.onPreferredLanguageCodesLoaded, 'emit'); spyOn(keyboardShortcutService, 'bindLibraryPageShortcuts'); @@ -304,52 +342,58 @@ describe('Library Page Component', () => { expect(loaderService.showLoadingScreen).toHaveBeenCalled(); expect(urlInterpolationService.getStaticImageUrl).toHaveBeenCalled(); expect(translateService.onLangChange.subscribe).toHaveBeenCalled(); - expect(libraryPageBackendApiService.fetchLibraryIndexDataAsync) - .toHaveBeenCalled(); + expect( + libraryPageBackendApiService.fetchLibraryIndexDataAsync + ).toHaveBeenCalled(); expect(userService.getUserInfoAsync).toHaveBeenCalled(); expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); expect(componentInstance.initCarousels).toHaveBeenCalled(); })); - it('should initialize for non group pages and user is not logged in', - fakeAsync(() => { - spyOn(loaderService, 'showLoadingScreen'); - spyOn(urlInterpolationService, 'getStaticImageUrl'); - spyOn(translateService.onLangChange, 'subscribe'); - windowRef.nativeWindow.location.pathname = '/community-library'; - fixture.detectChanges(); - spyOn(libraryPageBackendApiService, 'fetchLibraryIndexDataAsync') - .and.returnValue(Promise.resolve(libraryIndexData)); - spyOn(userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve({ isLoggedIn: () => false } as UserInfo)); - spyOn(loaderService, 'hideLoadingScreen'); - spyOn(i18nLanguageCodeService.onPreferredLanguageCodesLoaded, 'emit'); - spyOn(keyboardShortcutService, 'bindLibraryPageShortcuts'); - spyOn(componentInstance, 'initCarousels'); - spyOn(loggerService, 'error'); - let actualWidth = 200; - spyOn(window, '$').and.returnValue({ - width: () => { - return actualWidth; - } - } as JQLite); - componentInstance.ngOnInit(); - tick(); - tick(); - tick(); - tick(); - tick(4000); - expect(loaderService.showLoadingScreen).toHaveBeenCalled(); - expect(urlInterpolationService.getStaticImageUrl).toHaveBeenCalled(); - expect(translateService.onLangChange.subscribe).toHaveBeenCalled(); - expect(userService.getUserInfoAsync).toHaveBeenCalled(); - expect(loggerService.error).toHaveBeenCalledWith( - 'The actual width of tile is different than either of the ' + - 'expected widths. Actual size: ' + actualWidth + - ', Expected sizes: ' + AppConstants.LIBRARY_TILE_WIDTH_PX + - '/' + AppConstants.LIBRARY_MOBILE_TILE_WIDTH_PX - ); - })); + it('should initialize for non group pages and user is not logged in', fakeAsync(() => { + spyOn(loaderService, 'showLoadingScreen'); + spyOn(urlInterpolationService, 'getStaticImageUrl'); + spyOn(translateService.onLangChange, 'subscribe'); + windowRef.nativeWindow.location.pathname = '/community-library'; + fixture.detectChanges(); + spyOn( + libraryPageBackendApiService, + 'fetchLibraryIndexDataAsync' + ).and.returnValue(Promise.resolve(libraryIndexData)); + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve({isLoggedIn: () => false} as UserInfo) + ); + spyOn(loaderService, 'hideLoadingScreen'); + spyOn(i18nLanguageCodeService.onPreferredLanguageCodesLoaded, 'emit'); + spyOn(keyboardShortcutService, 'bindLibraryPageShortcuts'); + spyOn(componentInstance, 'initCarousels'); + spyOn(loggerService, 'error'); + let actualWidth = 200; + spyOn(window, '$').and.returnValue({ + width: () => { + return actualWidth; + }, + } as JQLite); + componentInstance.ngOnInit(); + tick(); + tick(); + tick(); + tick(); + tick(4000); + expect(loaderService.showLoadingScreen).toHaveBeenCalled(); + expect(urlInterpolationService.getStaticImageUrl).toHaveBeenCalled(); + expect(translateService.onLangChange.subscribe).toHaveBeenCalled(); + expect(userService.getUserInfoAsync).toHaveBeenCalled(); + expect(loggerService.error).toHaveBeenCalledWith( + 'The actual width of tile is different than either of the ' + + 'expected widths. Actual size: ' + + actualWidth + + ', Expected sizes: ' + + AppConstants.LIBRARY_TILE_WIDTH_PX + + '/' + + AppConstants.LIBRARY_MOBILE_TILE_WIDTH_PX + ); + })); it('should log when invalid path is used', fakeAsync(() => { spyOn(loaderService, 'showLoadingScreen'); @@ -357,10 +401,13 @@ describe('Library Page Component', () => { spyOn(translateService.onLangChange, 'subscribe'); windowRef.nativeWindow.location.pathname = '/not-valid'; fixture.detectChanges(); - spyOn(libraryPageBackendApiService, 'fetchLibraryIndexDataAsync') - .and.returnValue(Promise.resolve(libraryIndexData)); + spyOn( + libraryPageBackendApiService, + 'fetchLibraryIndexDataAsync' + ).and.returnValue(Promise.resolve(libraryIndexData)); spyOn(userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve({ isLoggedIn: () => false } as UserInfo)); + Promise.resolve({isLoggedIn: () => false} as UserInfo) + ); spyOn(loaderService, 'hideLoadingScreen'); spyOn(loggerService, 'error'); spyOn(i18nLanguageCodeService.onPreferredLanguageCodesLoaded, 'emit'); @@ -379,17 +426,20 @@ describe('Library Page Component', () => { expect(loggerService.error).toHaveBeenCalled(); })); - it('should obtain translated page title whenever the selected' + - 'language changes', fakeAsync(() => { - componentInstance.ngOnInit(); - tick(); - spyOn(componentInstance, 'setPageTitle'); - translateService.onLangChange.emit(); - tick(); - tick(4000); + it( + 'should obtain translated page title whenever the selected' + + 'language changes', + fakeAsync(() => { + componentInstance.ngOnInit(); + tick(); + spyOn(componentInstance, 'setPageTitle'); + translateService.onLangChange.emit(); + tick(); + tick(4000); - expect(componentInstance.setPageTitle).toHaveBeenCalled(); - })); + expect(componentInstance.setPageTitle).toHaveBeenCalled(); + }) + ); it('should set appropriate new page title when not in browse mode', () => { spyOn(translateService, 'instant').and.callThrough(); @@ -398,9 +448,11 @@ describe('Library Page Component', () => { componentInstance.setPageTitle(); expect(translateService.instant).toHaveBeenCalledWith( - 'I18N_LIBRARY_PAGE_TITLE'); + 'I18N_LIBRARY_PAGE_TITLE' + ); expect(pageTitleService.setDocumentTitle).toHaveBeenCalledWith( - 'I18N_LIBRARY_PAGE_TITLE'); + 'I18N_LIBRARY_PAGE_TITLE' + ); }); it('should set appropriate new page title when in browse mode', () => { @@ -410,9 +462,11 @@ describe('Library Page Component', () => { componentInstance.setPageTitle(); expect(translateService.instant).toHaveBeenCalledWith( - 'I18N_LIBRARY_PAGE_BROWSE_MODE_TITLE'); + 'I18N_LIBRARY_PAGE_BROWSE_MODE_TITLE' + ); expect(pageTitleService.setDocumentTitle).toHaveBeenCalledWith( - 'I18N_LIBRARY_PAGE_BROWSE_MODE_TITLE'); + 'I18N_LIBRARY_PAGE_BROWSE_MODE_TITLE' + ); }); it('should not initiate carousels if in mobile view', () => { @@ -421,98 +475,111 @@ describe('Library Page Component', () => { expect(componentInstance.leftmostCardIndices.length).toEqual(0); }); - it('should toggle the correct button\'s text when clicked', () => { + it("should toggle the correct button's text when clicked", () => { componentInstance.mobileLibraryGroupsProperties = [ { inCollapsedState: true, - buttonText: 'See More' + buttonText: 'See More', }, { inCollapsedState: false, - buttonText: 'Collapse Section' - } + buttonText: 'Collapse Section', + }, ]; componentInstance.toggleButtonText(0); // Correct button text should be toggled. - expect(componentInstance.mobileLibraryGroupsProperties[0].buttonText) - .toBe('Collapse Section'); + expect(componentInstance.mobileLibraryGroupsProperties[0].buttonText).toBe( + 'Collapse Section' + ); // Other button's text should remain unchanged. - expect(componentInstance.mobileLibraryGroupsProperties[1].buttonText) - .toBe('Collapse Section'); + expect(componentInstance.mobileLibraryGroupsProperties[1].buttonText).toBe( + 'Collapse Section' + ); componentInstance.toggleButtonText(1); - expect(componentInstance.mobileLibraryGroupsProperties[1].buttonText) - .toBe('See More'); - expect(componentInstance.mobileLibraryGroupsProperties[0].buttonText) - .toBe('Collapse Section'); + expect(componentInstance.mobileLibraryGroupsProperties[1].buttonText).toBe( + 'See More' + ); + expect(componentInstance.mobileLibraryGroupsProperties[0].buttonText).toBe( + 'Collapse Section' + ); }); - it('should toggle the corresponding container\'s max-height' + - 'and toggle the corresponding button\'s text', () => { - let buttonTextToggleSpy = spyOn(componentInstance, 'toggleButtonText'); - componentInstance.mobileLibraryGroupsProperties = [ - { - inCollapsedState: true, - buttonText: 'See More' - }, - { - inCollapsedState: false, - buttonText: 'Collapse Section' - } - ]; - - componentInstance.toggleCardContainerHeightInMobileView(0); + it( + "should toggle the corresponding container's max-height" + + "and toggle the corresponding button's text", + () => { + let buttonTextToggleSpy = spyOn(componentInstance, 'toggleButtonText'); + componentInstance.mobileLibraryGroupsProperties = [ + { + inCollapsedState: true, + buttonText: 'See More', + }, + { + inCollapsedState: false, + buttonText: 'Collapse Section', + }, + ]; + + componentInstance.toggleCardContainerHeightInMobileView(0); + + // Correct container's height should be toggled. + expect( + componentInstance.mobileLibraryGroupsProperties[0].inCollapsedState + ).toBe(false); + // Other container's height should remain unchanged. + expect( + componentInstance.mobileLibraryGroupsProperties[1].inCollapsedState + ).toBe(false); + expect(buttonTextToggleSpy).toHaveBeenCalledWith(0); + } + ); - // Correct container's height should be toggled. - expect(componentInstance.mobileLibraryGroupsProperties[0].inCollapsedState) - .toBe(false); - // Other container's height should remain unchanged. - expect(componentInstance.mobileLibraryGroupsProperties[1].inCollapsedState) - .toBe(false); - expect(buttonTextToggleSpy).toHaveBeenCalledWith(0); + it('should show full results page when full results url is available', () => { + let fullResultsUrl = 'full_results_url'; + componentInstance.showFullResultsPage([], fullResultsUrl); + expect(windowRef.nativeWindow.location.href).toEqual(fullResultsUrl); }); - it('should show full results page when full results url is available', - () => { - let fullResultsUrl = 'full_results_url'; - componentInstance.showFullResultsPage([], fullResultsUrl); - expect(windowRef.nativeWindow.location.href).toEqual(fullResultsUrl); - }); - - it('should show full results page when results url is not available', - () => { - let urlQueryString = 'urlQueryString'; - spyOn(searchService, 'getSearchUrlQueryString').and.returnValue( - urlQueryString); - componentInstance.showFullResultsPage(['id'], ''); - expect(windowRef.nativeWindow.location.href).toEqual( - '/search/find?q=' + urlQueryString); - }); + it('should show full results page when results url is not available', () => { + let urlQueryString = 'urlQueryString'; + spyOn(searchService, 'getSearchUrlQueryString').and.returnValue( + urlQueryString + ); + componentInstance.showFullResultsPage(['id'], ''); + expect(windowRef.nativeWindow.location.href).toEqual( + '/search/find?q=' + urlQueryString + ); + }); it('should increment and decrement carousel', () => { - componentInstance.libraryGroups = [{ - activity_summary_dicts: [{ - activity_type: '', - category: '', - community_owned: true, - id: '', - language_code: '', - num_views: 2, - objective: '', - status: '', - tags: [], - thumbnail_bg_color: '', - thumbnail_icon_url: '', - title: '', - }], - categories: [], - header_i18n_id: '', - has_full_results_page: true, - full_results_url: '', - protractor_id: '' - }]; + componentInstance.libraryGroups = [ + { + activity_summary_dicts: [ + { + activity_type: '', + category: '', + community_owned: true, + id: '', + language_code: '', + num_views: 2, + objective: '', + status: '', + tags: [], + thumbnail_bg_color: '', + thumbnail_icon_url: '', + title: '', + }, + ], + categories: [], + header_i18n_id: '', + has_full_results_page: true, + full_results_url: '', + protractor_id: '', + }, + ]; componentInstance.tileDisplayCount = 0; componentInstance.leftmostCardIndices = [0]; componentInstance.incrementLeftmostCardIndex(0); @@ -540,23 +607,24 @@ describe('Library Page Component', () => { }); it('should intialize carousels', () => { - componentInstance.libraryGroups = [{ - activity_summary_dicts: [], - categories: [], - header_i18n_id: '', - has_full_results_page: true, - full_results_url: '', - protractor_id: '' - }]; + componentInstance.libraryGroups = [ + { + activity_summary_dicts: [], + categories: [], + header_i18n_id: '', + has_full_results_page: true, + full_results_url: '', + protractor_id: '', + }, + ]; componentInstance.initCarousels(); expect(componentInstance.leftmostCardIndices.length).toEqual(1); }); - it('should not initialize carousels if there are no library groups', - () => { - componentInstance.initCarousels(); - expect(componentInstance.leftmostCardIndices.length).toEqual(0); - }); + it('should not initialize carousels if there are no library groups', () => { + componentInstance.initCarousels(); + expect(componentInstance.leftmostCardIndices.length).toEqual(0); + }); it('should scroll carousel', () => { componentInstance.libraryGroups = []; @@ -575,7 +643,7 @@ describe('Library Page Component', () => { tags: [], thumbnail_bg_color: '', thumbnail_icon_url: '', - title: '' + title: '', }); } @@ -586,21 +654,24 @@ describe('Library Page Component', () => { header_i18n_id: '', has_full_results_page: true, full_results_url: '', - protractor_id: '' + protractor_id: '', }); } spyOn(window, '$').and.returnValue({ - animate: (options: string[], arg2: { - duration: number; - queue: boolean; - start: () => void; - complete: () => void; - }) => { + animate: ( + options: string[], + arg2: { + duration: number; + queue: boolean; + start: () => void; + complete: () => void; + } + ) => { arg2.start(); arg2.complete(); }, - scrollLeft: () => {} + scrollLeft: () => {}, } as JQLite); componentInstance.scroll(3, false); @@ -633,7 +704,7 @@ describe('Library Page Component', () => { tags: [], thumbnail_bg_color: '', thumbnail_icon_url: '', - title: '' + title: '', }); } @@ -644,21 +715,24 @@ describe('Library Page Component', () => { header_i18n_id: '', has_full_results_page: true, full_results_url: '', - protractor_id: '' + protractor_id: '', }); } spyOn(window, '$').and.returnValue({ - animate: (options: string[], arg2: { - duration: number; - queue: boolean; - start: () => void; - complete: () => void; - }) => { + animate: ( + options: string[], + arg2: { + duration: number; + queue: boolean; + start: () => void; + complete: () => void; + } + ) => { arg2.start(); arg2.complete(); }, - scrollLeft: () => {} + scrollLeft: () => {}, } as JQLite); componentInstance.tileDisplayCount = 5; @@ -666,17 +740,16 @@ describe('Library Page Component', () => { expect(componentInstance.leftmostCardIndices).toEqual([]); }); - it('should unsubscribe on component destruction', - () => { - componentInstance.translateSubscription = new Subscription(); - componentInstance.resizeSubscription = new Subscription(); - spyOn(componentInstance.translateSubscription, 'unsubscribe'); - spyOn(componentInstance.resizeSubscription, 'unsubscribe'); - componentInstance.ngOnDestroy(); - - expect(componentInstance.translateSubscription.unsubscribe) - .toHaveBeenCalled(); - expect(componentInstance.resizeSubscription.unsubscribe) - .toHaveBeenCalled(); - }); + it('should unsubscribe on component destruction', () => { + componentInstance.translateSubscription = new Subscription(); + componentInstance.resizeSubscription = new Subscription(); + spyOn(componentInstance.translateSubscription, 'unsubscribe'); + spyOn(componentInstance.resizeSubscription, 'unsubscribe'); + componentInstance.ngOnDestroy(); + + expect( + componentInstance.translateSubscription.unsubscribe + ).toHaveBeenCalled(); + expect(componentInstance.resizeSubscription.unsubscribe).toHaveBeenCalled(); + }); }); diff --git a/core/templates/pages/library-page/library-page.component.ts b/core/templates/pages/library-page/library-page.component.ts index ec73401a9653..67ce3105f119 100755 --- a/core/templates/pages/library-page/library-page.component.ts +++ b/core/templates/pages/library-page/library-page.component.ts @@ -16,30 +16,31 @@ * @fileoverview Data and component for the Oppia contributors' library page. */ -import { Component } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; - -import { AppConstants } from 'app.constants'; -import { ClassroomBackendApiService } from 'domain/classroom/classroom-backend-api.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { LoggerService } from 'services/contextual/logger.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { KeyboardShortcutService } from 'services/keyboard-shortcut.service'; -import { LoaderService } from 'services/loader.service'; -import { PageTitleService } from 'services/page-title.service'; -import { SearchService } from 'services/search.service'; -import { UserService } from 'services/user.service'; -import { LibraryPageConstants } from './library-page.constants'; -import { ActivityDict, +import {Component} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; + +import {AppConstants} from 'app.constants'; +import {ClassroomBackendApiService} from 'domain/classroom/classroom-backend-api.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {LoggerService} from 'services/contextual/logger.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {KeyboardShortcutService} from 'services/keyboard-shortcut.service'; +import {LoaderService} from 'services/loader.service'; +import {PageTitleService} from 'services/page-title.service'; +import {SearchService} from 'services/search.service'; +import {UserService} from 'services/user.service'; +import {LibraryPageConstants} from './library-page.constants'; +import { + ActivityDict, LibraryPageBackendApiService, - SummaryDict } from './services/library-page-backend-api.service'; + SummaryDict, +} from './services/library-page-backend-api.service'; import './library-page.component.css'; - interface MobileLibraryGroupProperties { inCollapsedState: boolean; buttonText: string; @@ -48,11 +49,15 @@ interface MobileLibraryGroupProperties { @Component({ selector: 'oppia-library-page', templateUrl: './library-page.component.html', - styleUrls: ['./library-page.component.css'] + styleUrls: ['./library-page.component.css'], }) export class LibraryPageComponent { possibleBannerFilenames: string[] = [ - 'banner1.svg', 'banner2.svg', 'banner3.svg', 'banner4.svg']; + 'banner1.svg', + 'banner2.svg', + 'banner3.svg', + 'banner4.svg', + ]; // If the value below is changed, the following CSS values in // oppia.css must be changed: @@ -65,7 +70,7 @@ export class LibraryPageComponent { LIBRARY_PAGE_MODES = LibraryPageConstants.LIBRARY_PAGE_MODES; activitiesOwned: {[key: string]: {[key: string]: boolean}} = { explorations: {}, - collections: {} + collections: {}, }; // These properties are initialized using Angular lifecycle hooks @@ -132,25 +137,26 @@ export class LibraryPageComponent { // number. this.tileDisplayCount = Math.min( Math.floor(windowWidth / (AppConstants.LIBRARY_TILE_WIDTH_PX + 20)), - this.MAX_NUM_TILES_PER_ROW); + this.MAX_NUM_TILES_PER_ROW + ); $('.oppia-library-carousel').css({ - 'max-width': ( - this.tileDisplayCount * AppConstants.LIBRARY_TILE_WIDTH_PX) + 'px' + 'max-width': + this.tileDisplayCount * AppConstants.LIBRARY_TILE_WIDTH_PX + 'px', }); // The following determines whether to enable left scroll after // resize. for (let i = 0; i < this.libraryGroups.length; i++) { - let carouselJQuerySelector = ( - '.oppia-library-carousel-tiles:eq(n)'.replace( - 'n', String(i))); + let carouselJQuerySelector = + '.oppia-library-carousel-tiles:eq(n)'.replace('n', String(i)); // The number 0 here is just to make sure that the type of width is // number, it is never assigned as the selector will never be undefined. - let carouselScrollPositionPx = $( - carouselJQuerySelector).scrollLeft() || 0; + let carouselScrollPositionPx = + $(carouselJQuerySelector).scrollLeft() || 0; let index = Math.ceil( - carouselScrollPositionPx / AppConstants.LIBRARY_TILE_WIDTH_PX); + carouselScrollPositionPx / AppConstants.LIBRARY_TILE_WIDTH_PX + ); this.leftmostCardIndices[i] = index; } } @@ -159,19 +165,22 @@ export class LibraryPageComponent { if (this.isAnyCarouselCurrentlyScrolling) { return; } - let carouselJQuerySelector = ( - '.oppia-library-carousel-tiles:eq(n)'.replace('n', ind.toString())); + let carouselJQuerySelector = '.oppia-library-carousel-tiles:eq(n)'.replace( + 'n', + ind.toString() + ); let direction = isLeftScroll ? -1 : 1; // The number 0 here is just to make sure that the type of width is // number, it is never assigned as the selector will never be undefined. - let carouselScrollPositionPx = $( - carouselJQuerySelector).scrollLeft() || 0; + let carouselScrollPositionPx = $(carouselJQuerySelector).scrollLeft() || 0; // Prevent scrolling if there more carousel pixed widths than // there are tile widths. - if (this.libraryGroups[ind].activity_summary_dicts.length <= - this.tileDisplayCount) { + if ( + this.libraryGroups[ind].activity_summary_dicts.length <= + this.tileDisplayCount + ) { return; } @@ -179,47 +188,57 @@ export class LibraryPageComponent { if (isLeftScroll) { this.leftmostCardIndices[ind] = Math.max( - 0, this.leftmostCardIndices[ind] - this.tileDisplayCount); + 0, + this.leftmostCardIndices[ind] - this.tileDisplayCount + ); } else { this.leftmostCardIndices[ind] = Math.min( this.libraryGroups[ind].activity_summary_dicts.length - - this.tileDisplayCount + 1, - this.leftmostCardIndices[ind] + this.tileDisplayCount); + this.tileDisplayCount + + 1, + this.leftmostCardIndices[ind] + this.tileDisplayCount + ); } - let newScrollPositionPx = carouselScrollPositionPx + - (this.tileDisplayCount * AppConstants.LIBRARY_TILE_WIDTH_PX * direction); + let newScrollPositionPx = + carouselScrollPositionPx + + this.tileDisplayCount * AppConstants.LIBRARY_TILE_WIDTH_PX * direction; - $(carouselJQuerySelector).animate({ - scrollLeft: newScrollPositionPx - }, { - duration: 800, - queue: false, - start: () => { - this.isAnyCarouselCurrentlyScrolling = true; + $(carouselJQuerySelector).animate( + { + scrollLeft: newScrollPositionPx, }, - complete: () => { - this.isAnyCarouselCurrentlyScrolling = false; + { + duration: 800, + queue: false, + start: () => { + this.isAnyCarouselCurrentlyScrolling = true; + }, + complete: () => { + this.isAnyCarouselCurrentlyScrolling = false; + }, } - }); + ); } - // The carousels do not work when the width is 1 card long, so we need // to handle this case discretely and also prevent swiping past the // first and last card. incrementLeftmostCardIndex(ind: number): void { - let lastItem = (( + let lastItem = this.libraryGroups[ind].activity_summary_dicts.length - - this.tileDisplayCount) <= this.leftmostCardIndices[ind]); + this.tileDisplayCount <= + this.leftmostCardIndices[ind]; if (!lastItem) { this.leftmostCardIndices[ind]++; } } decrementLeftmostCardIndex(ind: number): void { - this.leftmostCardIndices[ind] = ( - Math.max(this.leftmostCardIndices[ind] - 1, 0)); + this.leftmostCardIndices[ind] = Math.max( + this.leftmostCardIndices[ind] - 1, + 0 + ); } // The following loads explorations belonging to a particular group. @@ -236,9 +255,12 @@ export class LibraryPageComponent { } let targetSearchQueryUrl = this.searchService.getSearchUrlQueryString( - '', selectedCategories, {}); - this.windowRef.nativeWindow.location.href = ( - '/search/find?q=' + targetSearchQueryUrl); + '', + selectedCategories, + {} + ); + this.windowRef.nativeWindow.location.href = + '/search/find?q=' + targetSearchQueryUrl; } } @@ -262,42 +284,50 @@ export class LibraryPageComponent { setPageTitle(): void { let titleKey = 'I18N_LIBRARY_PAGE_TITLE'; - if (this.pageMode === LibraryPageConstants.LIBRARY_PAGE_MODES.GROUP || - this.pageMode === LibraryPageConstants.LIBRARY_PAGE_MODES.SEARCH) { + if ( + this.pageMode === LibraryPageConstants.LIBRARY_PAGE_MODES.GROUP || + this.pageMode === LibraryPageConstants.LIBRARY_PAGE_MODES.SEARCH + ) { titleKey = 'I18N_LIBRARY_PAGE_BROWSE_MODE_TITLE'; } this.pageTitleService.setDocumentTitle( - this.translateService.instant(titleKey)); + this.translateService.instant(titleKey) + ); } ngOnInit(): void { let libraryWindowCutoffPx = 536; - this.libraryWindowIsNarrow = ( - this.windowDimensionsService.getWidth() <= libraryWindowCutoffPx); + this.libraryWindowIsNarrow = + this.windowDimensionsService.getWidth() <= libraryWindowCutoffPx; this.loaderService.showLoadingScreen('I18N_LIBRARY_LOADING'); - this.bannerImageFilename = this.possibleBannerFilenames[ - Math.floor(Math.random() * this.possibleBannerFilenames.length)]; + this.bannerImageFilename = + this.possibleBannerFilenames[ + Math.floor(Math.random() * this.possibleBannerFilenames.length) + ]; this.bannerImageFileUrl = this.urlInterpolationService.getStaticImageUrl( - '/library/' + this.bannerImageFilename); + '/library/' + this.bannerImageFilename + ); let currentPath = this.windowRef.nativeWindow.location.pathname; - if (!LibraryPageConstants.LIBRARY_PATHS_TO_MODES.hasOwnProperty( - currentPath)) { + if ( + !LibraryPageConstants.LIBRARY_PATHS_TO_MODES.hasOwnProperty(currentPath) + ) { this.loggerService.error('INVALID URL PATH: ' + currentPath); } - const libraryContants: Record = ( - LibraryPageConstants.LIBRARY_PATHS_TO_MODES); + const libraryContants: Record = + LibraryPageConstants.LIBRARY_PATHS_TO_MODES; this.pageMode = libraryContants[currentPath]; this.LIBRARY_PAGE_MODES = LibraryPageConstants.LIBRARY_PAGE_MODES; this.translateSubscription = this.translateService.onLangChange.subscribe( () => { this.setPageTitle(); - }); + } + ); // Keeps track of the index of the left-most visible card of each // group. @@ -308,66 +338,78 @@ export class LibraryPageComponent { this.mobileLibraryGroupsProperties = []; if (this.pageMode === LibraryPageConstants.LIBRARY_PAGE_MODES.GROUP) { - let pathnameArray = ( - this.windowRef.nativeWindow.location.pathname.split('/')); + let pathnameArray = + this.windowRef.nativeWindow.location.pathname.split('/'); this.groupName = pathnameArray[2]; - this.libraryPageBackendApiService.fetchLibraryGroupDataAsync( - this.groupName).then((response) => { - this.activityList = response.activity_list; - this.groupHeaderI18nId = response.header_i18n_id; - this.i18nLanguageCodeService.onPreferredLanguageCodesLoaded.emit( - response.preferred_language_codes); - this.loaderService.hideLoadingScreen(); - this.initCarousels(); - }); + this.libraryPageBackendApiService + .fetchLibraryGroupDataAsync(this.groupName) + .then(response => { + this.activityList = response.activity_list; + this.groupHeaderI18nId = response.header_i18n_id; + this.i18nLanguageCodeService.onPreferredLanguageCodesLoaded.emit( + response.preferred_language_codes + ); + this.loaderService.hideLoadingScreen(); + this.initCarousels(); + }); } else { - this.libraryPageBackendApiService.fetchLibraryIndexDataAsync() - .then((response) => { + this.libraryPageBackendApiService + .fetchLibraryIndexDataAsync() + .then(response => { this.libraryGroups = response.activity_summary_dicts_by_category; - this.userService.getUserInfoAsync().then((userInfo) => { + this.userService.getUserInfoAsync().then(userInfo => { this.activitiesOwned = {explorations: {}, collections: {}}; if (userInfo.isLoggedIn()) { - this.libraryPageBackendApiService.fetchCreatorDashboardDataAsync() - .then((response) => { - this.libraryGroups.forEach((libraryGroup) => { - let activitySummaryDicts = ( - libraryGroup.activity_summary_dicts); + this.libraryPageBackendApiService + .fetchCreatorDashboardDataAsync() + .then(response => { + this.libraryGroups.forEach(libraryGroup => { + let activitySummaryDicts = + libraryGroup.activity_summary_dicts; let ACTIVITY_TYPE_EXPLORATION = 'exploration'; let ACTIVITY_TYPE_COLLECTION = 'collection'; - activitySummaryDicts.forEach((activitySummaryDict) => { - if (activitySummaryDict.activity_type === ( - ACTIVITY_TYPE_EXPLORATION)) { + activitySummaryDicts.forEach(activitySummaryDict => { + if ( + activitySummaryDict.activity_type === + ACTIVITY_TYPE_EXPLORATION + ) { this.activitiesOwned.explorations[ - activitySummaryDict.id] = false; - } else if (activitySummaryDict.activity_type === ( - ACTIVITY_TYPE_COLLECTION)) { + activitySummaryDict.id + ] = false; + } else if ( + activitySummaryDict.activity_type === + ACTIVITY_TYPE_COLLECTION + ) { this.activitiesOwned.collections[ - activitySummaryDict.id] = false; + activitySummaryDict.id + ] = false; } else { this.loggerService.error( 'INVALID ACTIVITY TYPE: Activity' + - '(id: ' + activitySummaryDict.id + - ', name: ' + activitySummaryDict.title + - ', type: ' + activitySummaryDict.activity_type + - ') has an invalid activity type, which could ' + - 'not be recorded as an exploration or a ' + - 'collection.' + '(id: ' + + activitySummaryDict.id + + ', name: ' + + activitySummaryDict.title + + ', type: ' + + activitySummaryDict.activity_type + + ') has an invalid activity type, which could ' + + 'not be recorded as an exploration or a ' + + 'collection.' ); } }); - response.explorations_list.forEach( - (ownedExplorations) => { - this.activitiesOwned.explorations[ - ownedExplorations.id] = true; - }); + response.explorations_list.forEach(ownedExplorations => { + this.activitiesOwned.explorations[ownedExplorations.id] = + true; + }); - response.collections_list.forEach((ownedCollections) => { - this.activitiesOwned.collections[ - ownedCollections.id] = true; + response.collections_list.forEach(ownedCollections => { + this.activitiesOwned.collections[ownedCollections.id] = + true; }); }); this.loaderService.hideLoadingScreen(); @@ -380,7 +422,8 @@ export class LibraryPageComponent { }); this.i18nLanguageCodeService.onPreferredLanguageCodesLoaded.emit( - response.preferred_language_codes); + response.preferred_language_codes + ); this.keyboardShortcutService.bindLibraryPageShortcuts(); @@ -388,14 +431,20 @@ export class LibraryPageComponent { // If not produce an error that would be caught by e2e tests. setTimeout(() => { let actualWidth = $('oppia-exploration-summary-tile').width(); - if (actualWidth && - (actualWidth !== AppConstants.LIBRARY_TILE_WIDTH_PX && - actualWidth !== AppConstants.LIBRARY_MOBILE_TILE_WIDTH_PX)) { + if ( + actualWidth && + actualWidth !== AppConstants.LIBRARY_TILE_WIDTH_PX && + actualWidth !== AppConstants.LIBRARY_MOBILE_TILE_WIDTH_PX + ) { this.loggerService.error( 'The actual width of tile is different than either of the ' + - 'expected widths. Actual size: ' + actualWidth + - ', Expected sizes: ' + AppConstants.LIBRARY_TILE_WIDTH_PX + - '/' + AppConstants.LIBRARY_MOBILE_TILE_WIDTH_PX); + 'expected widths. Actual size: ' + + actualWidth + + ', Expected sizes: ' + + AppConstants.LIBRARY_TILE_WIDTH_PX + + '/' + + AppConstants.LIBRARY_MOBILE_TILE_WIDTH_PX + ); } }, 3000); // The following initializes the tracker to have all @@ -411,18 +460,19 @@ export class LibraryPageComponent { for (let i = 0; i < this.libraryGroups.length; i++) { this.mobileLibraryGroupsProperties.push({ inCollapsedState: true, - buttonText: 'See More' + buttonText: 'See More', }); } }); } - this.resizeSubscription = this.windowDimensionsService.getResizeEvent() + this.resizeSubscription = this.windowDimensionsService + .getResizeEvent() .subscribe(evt => { this.initCarousels(); - this.libraryWindowIsNarrow = ( - this.windowDimensionsService.getWidth() <= libraryWindowCutoffPx); + this.libraryWindowIsNarrow = + this.windowDimensionsService.getWidth() <= libraryWindowCutoffPx; }); } diff --git a/core/templates/pages/library-page/library-page.constants.ts b/core/templates/pages/library-page/library-page.constants.ts index 83e8f04aef58..ad89c81f6e79 100644 --- a/core/templates/pages/library-page/library-page.constants.ts +++ b/core/templates/pages/library-page/library-page.constants.ts @@ -24,16 +24,15 @@ export const LibraryPageConstants = { LIBRARY_PAGE_MODES: { GROUP: 'group', INDEX: 'index', - SEARCH: 'search' + SEARCH: 'search', }, LIBRARY_PATHS_TO_MODES: { '/community-library': 'index', '/community-library/top-rated': 'group', '/community-library/recently-published': 'group', - '/search/find': 'search' + '/search/find': 'search', }, - SEARCH_EXPLORATION_URL_TEMPLATE: - '/exploration/metadata_search?q=' + SEARCH_EXPLORATION_URL_TEMPLATE: '/exploration/metadata_search?q=', } as const; diff --git a/core/templates/pages/library-page/library-page.module.ts b/core/templates/pages/library-page/library-page.module.ts index 87c82bcb73ca..e330a00e2b52 100644 --- a/core/templates/pages/library-page/library-page.module.ts +++ b/core/templates/pages/library-page/library-page.module.ts @@ -16,26 +16,26 @@ * @fileoverview Module for the library page. */ -import { NgModule } from '@angular/core'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { InfiniteScrollModule } from 'ngx-infinite-scroll'; +import {NgModule} from '@angular/core'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {InfiniteScrollModule} from 'ngx-infinite-scroll'; -import { LearnerPlaylistModalComponent } from 'pages/learner-dashboard-page/modal-templates/learner-playlist-modal.component'; +import {LearnerPlaylistModalComponent} from 'pages/learner-dashboard-page/modal-templates/learner-playlist-modal.component'; -import { LibraryFooterComponent } from './library-footer/library-footer.component'; -import { LibraryPageRootComponent } from './library-page-root.component'; -import { LibraryPageComponent } from './library-page.component'; -import { ActivityTilesInfinityGridComponent } from './search-results/activity-tiles-infinity-grid.component'; -import { SearchResultsComponent } from './search-results/search-results.component'; -import { CommonModule } from '@angular/common'; -import { LibraryPageRoutingModule } from './library-page-routing.module'; +import {LibraryFooterComponent} from './library-footer/library-footer.component'; +import {LibraryPageRootComponent} from './library-page-root.component'; +import {LibraryPageComponent} from './library-page.component'; +import {ActivityTilesInfinityGridComponent} from './search-results/activity-tiles-infinity-grid.component'; +import {SearchResultsComponent} from './search-results/search-results.component'; +import {CommonModule} from '@angular/common'; +import {LibraryPageRoutingModule} from './library-page-routing.module'; @NgModule({ imports: [ CommonModule, SharedComponentsModule, InfiniteScrollModule, - LibraryPageRoutingModule + LibraryPageRoutingModule, ], declarations: [ LearnerPlaylistModalComponent, @@ -43,7 +43,7 @@ import { LibraryPageRoutingModule } from './library-page-routing.module'; SearchResultsComponent, ActivityTilesInfinityGridComponent, LibraryPageComponent, - LibraryPageRootComponent + LibraryPageRootComponent, ], entryComponents: [ LearnerPlaylistModalComponent, @@ -51,7 +51,7 @@ import { LibraryPageRoutingModule } from './library-page-routing.module'; SearchResultsComponent, ActivityTilesInfinityGridComponent, LibraryPageComponent, - LibraryPageRootComponent - ] + LibraryPageRootComponent, + ], }) export class LibraryPageModule {} diff --git a/core/templates/pages/library-page/search-bar/search-bar.component.spec.ts b/core/templates/pages/library-page/search-bar/search-bar.component.spec.ts index fbf768cfff10..150f23b9e630 100644 --- a/core/templates/pages/library-page/search-bar/search-bar.component.spec.ts +++ b/core/templates/pages/library-page/search-bar/search-bar.component.spec.ts @@ -16,26 +16,23 @@ * @fileoverview Unit tests for Search bar. */ -import { EventEmitter, Pipe } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync} - from '@angular/core/testing'; -import { HttpClientTestingModule } from - '@angular/common/http/testing'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { SearchBarComponent } from 'pages/library-page/search-bar/search-bar.component'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { NavigationService } from 'services/navigation.service'; -import { ClassroomBackendApiService } from 'domain/classroom/classroom-backend-api.service'; -import { FormsModule } from '@angular/forms'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { TranslateService } from '@ngx-translate/core'; -import { SearchService, SelectionDetails } from 'services/search.service'; -import { ConstructTranslationIdsService } from 'services/construct-translation-ids.service'; -import { LanguageUtilService } from 'domain/utilities/language-util.service'; -import { UrlService } from 'services/contextual/url.service'; -import { Subject } from 'rxjs/internal/Subject'; - +import {EventEmitter, Pipe} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {SearchBarComponent} from 'pages/library-page/search-bar/search-bar.component'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {NavigationService} from 'services/navigation.service'; +import {ClassroomBackendApiService} from 'domain/classroom/classroom-backend-api.service'; +import {FormsModule} from '@angular/forms'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {TranslateService} from '@ngx-translate/core'; +import {SearchService, SelectionDetails} from 'services/search.service'; +import {ConstructTranslationIdsService} from 'services/construct-translation-ids.service'; +import {LanguageUtilService} from 'domain/utilities/language-util.service'; +import {UrlService} from 'services/contextual/url.service'; +import {Subject} from 'rxjs/internal/Subject'; @Pipe({name: 'truncate'}) class MockTrunctePipe { @@ -51,11 +48,11 @@ class MockWindowRef { href: '', toString() { return 'http://localhost/test_path'; - } + }, }, history: { - pushState(data: string, title: string, url?: string | null) {} - } + pushState(data: string, title: string, url?: string | null) {}, + }, }; } @@ -77,16 +74,16 @@ class MockNavigationService { KEYBOARD_EVENT_TO_KEY_CODES = { enter: { shiftKeyIsPressed: false, - keyCode: 13 + keyCode: 13, }, tab: { shiftKeyIsPressed: false, - keyCode: 9 + keyCode: 9, }, shiftTab: { shiftKeyIsPressed: true, - keyCode: 9 - } + keyCode: 9, + }, }; onMenuKeypress(): void {} @@ -116,32 +113,25 @@ describe('Search bar component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - FormsModule - ], - declarations: [ - SearchBarComponent, - MockTranslatePipe, - MockTrunctePipe - ], + imports: [HttpClientTestingModule, FormsModule], + declarations: [SearchBarComponent, MockTranslatePipe, MockTrunctePipe], providers: [ { provide: WindowRef, - useClass: MockWindowRef + useClass: MockWindowRef, }, { provide: TranslateService, - useClass: MockTranslateService + useClass: MockTranslateService, }, { provide: NavigationService, - useClass: MockNavigationService + useClass: MockNavigationService, }, { provide: WindowDimensionsService, - useClass: MockWindowDimensionsService - } + useClass: MockWindowDimensionsService, + }, ], }).compileComponents(); })); @@ -154,20 +144,20 @@ describe('Search bar component', () => { masterList: [ { id: 'id', - text: 'category 1' + text: 'category 1', }, { id: 'id_2', - text: 'category 2' + text: 'category 2', }, { id: 'id_3', - text: 'category 3' - } + text: 'category 3', + }, ], - selections: { id: true, id_2: true, id_3: true }, + selections: {id: true, id_2: true, id_3: true}, numSelections: 0, - summary: 'all categories' + summary: 'all categories', }, languageCodes: { description: 'English', @@ -175,17 +165,17 @@ describe('Search bar component', () => { masterList: [ { id: 'en', - text: 'English' + text: 'English', }, { id: 'es', - text: 'Spanish' - } + text: 'Spanish', + }, ], numSelections: 1, selections: {en: true}, - summary: 'English' - } + summary: 'English', + }, }; fixture = TestBed.createComponent(SearchBarComponent); @@ -197,15 +187,17 @@ describe('Search bar component', () => { windowDimensionsService = TestBed.inject(WindowDimensionsService); spyOnProperty( classroomBackendApiService, - 'onInitializeTranslation').and.returnValue(initTranslationEmitter); + 'onInitializeTranslation' + ).and.returnValue(initTranslationEmitter); spyOnProperty( i18nLanguageCodeService, - 'onPreferredLanguageCodesLoaded').and.returnValue( - preferredLanguageCodesLoadedEmitter); + 'onPreferredLanguageCodesLoaded' + ).and.returnValue(preferredLanguageCodesLoadedEmitter); searchService = TestBed.inject(SearchService); translateService = TestBed.inject(TranslateService); - constructTranslationIdsService = ( - TestBed.inject(ConstructTranslationIdsService)); + constructTranslationIdsService = TestBed.inject( + ConstructTranslationIdsService + ); languageUtilService = TestBed.inject(LanguageUtilService); urlService = TestBed.inject(UrlService); @@ -214,8 +206,10 @@ describe('Search bar component', () => { }); it('should determine if mobile view is active', () => { - const windowWidthSpy = - spyOn(windowDimensionsService, 'getWidth').and.returnValue(766); + const windowWidthSpy = spyOn( + windowDimensionsService, + 'getWidth' + ).and.returnValue(766); expect(component.isMobileViewActive()).toBe(true); windowWidthSpy.and.returnValue(1000); expect(component.isMobileViewActive()).toBe(false); @@ -232,16 +226,21 @@ describe('Search bar component', () => { expect(component.searchButtonIsActive).toBe(false); }); - it('should update selection details if selected languages' + - ' are greater than zero', () => { - expect(component.selectionDetails.languageCodes.description).toEqual( - 'I18N_LIBRARY_ALL_LANGUAGES_SELECTED'); - component.selectionDetails = selectionDetailsStub; - spyOn(translateService, 'instant').and.returnValue('English'); - component.updateSelectionDetails('languageCodes'); - expect(component.selectionDetails.languageCodes.description).toEqual( - 'English'); - }); + it( + 'should update selection details if selected languages' + + ' are greater than zero', + () => { + expect(component.selectionDetails.languageCodes.description).toEqual( + 'I18N_LIBRARY_ALL_LANGUAGES_SELECTED' + ); + component.selectionDetails = selectionDetailsStub; + spyOn(translateService, 'instant').and.returnValue('English'); + component.updateSelectionDetails('languageCodes'); + expect(component.selectionDetails.languageCodes.description).toEqual( + 'English' + ); + } + ); it('should update selection details if there are no selections', () => { spyOn(translateService, 'instant').and.returnValue('key'); @@ -249,12 +248,12 @@ describe('Search bar component', () => { expect(component.selectionDetails.categories.numSelections).toEqual(0); }); - it ('should search', () => { + it('should search', () => { component.searchButtonIsActive = true; const search = { target: { - value: 'search' - } + value: 'search', + }, }; component.searchToBeExec(search); @@ -264,7 +263,7 @@ describe('Search bar component', () => { expect(component.searchQueryChanged.next).toHaveBeenCalled(); }); - it ('should open submenu', () => { + it('should open submenu', () => { spyOn(navigationService, 'openSubmenu'); // This throws "Argument of type 'null' is not assignable to parameter of // type 'KeyboardEvent'." We need to suppress this error because of the @@ -306,60 +305,77 @@ describe('Search bar component', () => { expect(component.refreshSearchBarLabels).toHaveBeenCalled(); }); - it('should handle search query change with language param in URL', () => { spyOn(searchService, 'executeSearchQuery').and.callFake( ( - searchQuery: string, categorySelections: object, - languageCodeSelections: object, callb: () => void + searchQuery: string, + categorySelections: object, + languageCodeSelections: object, + callb: () => void ) => { callb(); } ); - spyOn(searchService, 'getSearchUrlQueryString') - .and.returnValue('search_query'); + spyOn(searchService, 'getSearchUrlQueryString').and.returnValue( + 'search_query' + ); spyOn(windowRef.nativeWindow.history, 'pushState'); component.searchQuery = 'test_query'; - windowRef.nativeWindow.location = new URL('http://localhost/search/find?lang=en'); + windowRef.nativeWindow.location = new URL( + 'http://localhost/search/find?lang=en' + ); component.onSearchQueryChangeExec(); expect(windowRef.nativeWindow.history.pushState).toHaveBeenCalled(); - windowRef.nativeWindow.location = new URL('http://localhost/not/search/find?lang=en'); + windowRef.nativeWindow.location = new URL( + 'http://localhost/not/search/find?lang=en' + ); component.onSearchQueryChangeExec(); - expect(windowRef.nativeWindow.location.href) - .toEqual('http://localhost/search/find?q=search_query&lang=en'); + expect(windowRef.nativeWindow.location.href).toEqual( + 'http://localhost/search/find?q=search_query&lang=en' + ); }); - it('should handle search query change with empty search query and ' + - 'language param in URL', () => { - spyOn(searchService, 'executeSearchQuery'); - spyOn(searchService, 'getSearchUrlQueryString').and.returnValue(''); - spyOn(windowRef.nativeWindow.history, 'pushState'); - - component.searchQuery = ''; - windowRef.nativeWindow.location = new URL('http://localhost/not/search/find?lang=en'); - component.onSearchQueryChangeExec(); - - expect(windowRef.nativeWindow.history.pushState).not.toHaveBeenCalled(); - expect(windowRef.nativeWindow.location.href).toEqual('http://localhost/not/search/find?lang=en'); - }); + it( + 'should handle search query change with empty search query and ' + + 'language param in URL', + () => { + spyOn(searchService, 'executeSearchQuery'); + spyOn(searchService, 'getSearchUrlQueryString').and.returnValue(''); + spyOn(windowRef.nativeWindow.history, 'pushState'); + + component.searchQuery = ''; + windowRef.nativeWindow.location = new URL( + 'http://localhost/not/search/find?lang=en' + ); + component.onSearchQueryChangeExec(); + + expect(windowRef.nativeWindow.history.pushState).not.toHaveBeenCalled(); + expect(windowRef.nativeWindow.location.href).toEqual( + 'http://localhost/not/search/find?lang=en' + ); + } + ); it('should handle search query change without language param in URL', () => { spyOn(searchService, 'executeSearchQuery').and.callFake( ( - searchQuery: string, categorySelections: object, - languageCodeSelections: object, callb: () => void + searchQuery: string, + categorySelections: object, + languageCodeSelections: object, + callb: () => void ) => { callb(); } ); - spyOn(searchService, 'getSearchUrlQueryString') - .and.returnValue('search_query'); + spyOn(searchService, 'getSearchUrlQueryString').and.returnValue( + 'search_query' + ); spyOn(windowRef.nativeWindow.history, 'pushState'); component.searchQuery = 'test_query'; @@ -369,39 +385,53 @@ describe('Search bar component', () => { expect(windowRef.nativeWindow.history.pushState).toHaveBeenCalled(); - windowRef.nativeWindow.location = new URL('http://localhost/not/search/find'); + windowRef.nativeWindow.location = new URL( + 'http://localhost/not/search/find' + ); component.onSearchQueryChangeExec(); - expect(windowRef.nativeWindow.location.href).toEqual('http://localhost/search/find?q=search_query'); + expect(windowRef.nativeWindow.location.href).toEqual( + 'http://localhost/search/find?q=search_query' + ); }); - it('should handle search query change with empty query string ' + - 'and without language param in URL', () => { - spyOn(searchService, 'executeSearchQuery'); - spyOn(searchService, 'getSearchUrlQueryString').and.returnValue(''); - spyOn(windowRef.nativeWindow.history, 'pushState'); + it( + 'should handle search query change with empty query string ' + + 'and without language param in URL', + () => { + spyOn(searchService, 'executeSearchQuery'); + spyOn(searchService, 'getSearchUrlQueryString').and.returnValue(''); + spyOn(windowRef.nativeWindow.history, 'pushState'); - component.searchQuery = ''; + component.searchQuery = ''; - windowRef.nativeWindow.location = new URL('http://localhost/search/find'); - component.onSearchQueryChangeExec(); + windowRef.nativeWindow.location = new URL('http://localhost/search/find'); + component.onSearchQueryChangeExec(); - expect(windowRef.nativeWindow.history.pushState).not.toHaveBeenCalled(); - expect(windowRef.nativeWindow.location.href).toEqual('http://localhost/search/find'); + expect(windowRef.nativeWindow.history.pushState).not.toHaveBeenCalled(); + expect(windowRef.nativeWindow.location.href).toEqual( + 'http://localhost/search/find' + ); - windowRef.nativeWindow.history.pushState.calls.reset(); + windowRef.nativeWindow.history.pushState.calls.reset(); - windowRef.nativeWindow.location = new URL('http://localhost/not/search/find'); - component.onSearchQueryChangeExec(); + windowRef.nativeWindow.location = new URL( + 'http://localhost/not/search/find' + ); + component.onSearchQueryChangeExec(); - expect(windowRef.nativeWindow.history.pushState).not.toHaveBeenCalled(); - expect(windowRef.nativeWindow.location.href).toEqual('http://localhost/not/search/find'); - }); + expect(windowRef.nativeWindow.history.pushState).not.toHaveBeenCalled(); + expect(windowRef.nativeWindow.location.href).toEqual( + 'http://localhost/not/search/find' + ); + } + ); it('should update search fields based on url query', () => { spyOn(component, 'updateSelectionDetails'); spyOn(component, 'onSearchQueryChangeExec'); - spyOn(searchService, 'updateSearchFieldsBasedOnUrlQuery') - .and.returnValue('test_query'); + spyOn(searchService, 'updateSearchFieldsBasedOnUrlQuery').and.returnValue( + 'test_query' + ); component.updateSearchFieldsBasedOnUrlQuery(); expect(component.updateSelectionDetails).toHaveBeenCalled(); expect(component.onSearchQueryChangeExec).toHaveBeenCalled(); @@ -429,30 +459,34 @@ describe('Search bar component', () => { spyOn(component, 'onSearchQueryChangeExec'); spyOn(component, 'updateSearchFieldsBasedOnUrlQuery'); spyOn(searchService.onSearchBarLoaded, 'emit'); - spyOn(i18nLanguageCodeService.onPreferredLanguageCodesLoaded, 'subscribe') - .and.callFake((callb: (arg0: string[]) => void) => { - callb(['en', 'es']); - callb(['en', 'es']); - return null; - }); - spyOn(translateService.onLangChange, 'subscribe').and.callFake((callb) => { + spyOn( + i18nLanguageCodeService.onPreferredLanguageCodesLoaded, + 'subscribe' + ).and.callFake((callb: (arg0: string[]) => void) => { + callb(['en', 'es']); + callb(['en', 'es']); + return null; + }); + spyOn(translateService.onLangChange, 'subscribe').and.callFake(callb => { callb(); return null; }); - spyOn(classroomBackendApiService.onInitializeTranslation, 'subscribe') - .and.callFake((callb: () => void) => { - callb(); - return null; - }); - spyOn(urlService, 'getUrlParams').and.returnValue({ q: '' }); + spyOn( + classroomBackendApiService.onInitializeTranslation, + 'subscribe' + ).and.callFake((callb: () => void) => { + callb(); + return null; + }); + spyOn(urlService, 'getUrlParams').and.returnValue({q: ''}); component.searchQueryChanged = { pipe: (param1: string, parm2: string) => { return { subscribe(callb: () => void) { callb(); - } + }, }; - } + }, } as Subject; component.ngOnInit(); expect(component.searchDropdownCategories).toHaveBeenCalled(); @@ -462,11 +496,13 @@ describe('Search bar component', () => { expect(component.onSearchQueryChangeExec).toHaveBeenCalled(); expect(component.updateSearchFieldsBasedOnUrlQuery).toHaveBeenCalled(); expect(searchService.onSearchBarLoaded.emit).toHaveBeenCalled(); - expect(i18nLanguageCodeService.onPreferredLanguageCodesLoaded.subscribe) - .toHaveBeenCalled(); + expect( + i18nLanguageCodeService.onPreferredLanguageCodesLoaded.subscribe + ).toHaveBeenCalled(); expect(translateService.onLangChange.subscribe).toHaveBeenCalled(); - expect(classroomBackendApiService.onInitializeTranslation.subscribe) - .toHaveBeenCalled(); + expect( + classroomBackendApiService.onInitializeTranslation.subscribe + ).toHaveBeenCalled(); expect(urlService.getUrlParams).toHaveBeenCalled(); }); diff --git a/core/templates/pages/library-page/search-bar/search-bar.component.ts b/core/templates/pages/library-page/search-bar/search-bar.component.ts index 823a896cd25d..71e9e2ca7afe 100755 --- a/core/templates/pages/library-page/search-bar/search-bar.component.ts +++ b/core/templates/pages/library-page/search-bar/search-bar.component.ts @@ -16,22 +16,22 @@ * @fileoverview Component for the Search Bar. */ -import { Subscription } from 'rxjs'; -import { Subject } from 'rxjs'; -import { AppConstants } from 'app.constants'; -import { EventToCodes, NavigationService } from 'services/navigation.service'; -import { Component, Input, OnDestroy, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { ClassroomBackendApiService } from 'domain/classroom/classroom-backend-api.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { SearchService, SelectionDetails } from 'services/search.service'; -import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { UrlService } from 'services/contextual/url.service'; -import { ConstructTranslationIdsService } from 'services/construct-translation-ids.service'; -import { LanguageUtilService } from 'domain/utilities/language-util.service'; -import { TranslateService } from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; +import {Subject} from 'rxjs'; +import {AppConstants} from 'app.constants'; +import {EventToCodes, NavigationService} from 'services/navigation.service'; +import {Component, Input, OnDestroy, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {ClassroomBackendApiService} from 'domain/classroom/classroom-backend-api.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {SearchService, SelectionDetails} from 'services/search.service'; +import {debounceTime, distinctUntilChanged} from 'rxjs/operators'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {UrlService} from 'services/contextual/url.service'; +import {ConstructTranslationIdsService} from 'services/construct-translation-ids.service'; +import {LanguageUtilService} from 'domain/utilities/language-util.service'; +import {TranslateService} from '@ngx-translate/core'; import './search-bar.component.css'; interface SearchDropDownCategories { @@ -47,7 +47,7 @@ interface LanguageIdAndText { @Component({ selector: 'oppia-search-bar', templateUrl: './search-bar.component.html', - styleUrls: ['./search-bar.component.css'] + styleUrls: ['./search-bar.component.css'], }) export class SearchBarComponent implements OnInit, OnDestroy { // These properties are initialized using Angular lifecycle hooks @@ -81,10 +81,11 @@ export class SearchBarComponent implements OnInit, OnDestroy { private classroomBackendApiService: ClassroomBackendApiService, private languageUtilService: LanguageUtilService, private constructTranslationIdsService: ConstructTranslationIdsService, - private translateService: TranslateService, + private translateService: TranslateService ) { - this.classroomPageIsActive = ( - this.urlService.getPathname().startsWith('/learn')); + this.classroomPageIsActive = this.urlService + .getPathname() + .startsWith('/learn'); this.isSearchButtonActive(); } @@ -93,8 +94,8 @@ export class SearchBarComponent implements OnInit, OnDestroy { } isSearchButtonActive(): boolean { - this.searchButtonIsActive = this.classroomPageIsActive || - this.isMobileViewActive(); + this.searchButtonIsActive = + this.classroomPageIsActive || this.isMobileViewActive(); return this.searchButtonIsActive; } @@ -130,9 +131,10 @@ export class SearchBarComponent implements OnInit, OnDestroy { * onMenuKeypress($event, 'category', {enter: 'open'}) */ onMenuKeypress( - evt: KeyboardEvent, - menuName: string, - eventsTobeHandled: EventToCodes): void { + evt: KeyboardEvent, + menuName: string, + eventsTobeHandled: EventToCodes + ): void { this.navigationService.onMenuKeypress(evt, menuName, eventsTobeHandled); this.activeMenuName = this.navigationService.activeMenuName; } @@ -153,10 +155,12 @@ export class SearchBarComponent implements OnInit, OnDestroy { let totalCount = selectedItems.length; this.selectionDetails[itemsType].numSelections = totalCount; - this.selectionDetails[itemsType].summary = ( - totalCount === 0 ? 'I18N_LIBRARY_ALL_' + itemsName.toUpperCase() : - totalCount === 1 ? selectedItems[0] : - 'I18N_LIBRARY_N_' + itemsName.toUpperCase()); + this.selectionDetails[itemsType].summary = + totalCount === 0 + ? 'I18N_LIBRARY_ALL_' + itemsName.toUpperCase() + : totalCount === 1 + ? selectedItems[0] + : 'I18N_LIBRARY_N_' + itemsName.toUpperCase(); this.translationData[itemsName + 'Count'] = totalCount; if (selectedItems.length > 0) { @@ -164,11 +168,10 @@ export class SearchBarComponent implements OnInit, OnDestroy { for (let i = 0; i < selectedItems.length; i++) { translatedItems.push(this.translateService.instant(selectedItems[i])); } - this.selectionDetails[itemsType].description = ( - translatedItems.join(', ')); + this.selectionDetails[itemsType].description = translatedItems.join(', '); } else { - this.selectionDetails[itemsType].description = ( - 'I18N_LIBRARY_ALL_' + itemsName.toUpperCase() + '_SELECTED'); + this.selectionDetails[itemsType].description = + 'I18N_LIBRARY_ALL_' + itemsName.toUpperCase() + '_SELECTED'; } } @@ -192,17 +195,19 @@ export class SearchBarComponent implements OnInit, OnDestroy { onSearchQueryChangeExec(): void { let searchUrlQueryString = this.searchService.getSearchUrlQueryString( - this.searchQuery, this.selectionDetails.categories.selections, + this.searchQuery, + this.selectionDetails.categories.selections, this.selectionDetails.languageCodes.selections ); this.searchService.executeSearchQuery( - this.searchQuery, this.selectionDetails.categories.selections, - this.selectionDetails.languageCodes.selections, () => { + this.searchQuery, + this.selectionDetails.categories.selections, + this.selectionDetails.languageCodes.selections, + () => { let url = new URL(this.windowRef.nativeWindow.location.toString()); let siteLangCode: string | null = url.searchParams.get('lang'); url.search = '?q=' + searchUrlQueryString; - if ( - this.windowRef.nativeWindow.location.pathname === ('/search/find')) { + if (this.windowRef.nativeWindow.location.pathname === '/search/find') { if (siteLangCode) { url.searchParams.append('lang', siteLangCode); } @@ -214,16 +219,18 @@ export class SearchBarComponent implements OnInit, OnDestroy { } this.windowRef.nativeWindow.location.href = url.toString(); } - }); + } + ); } updateSearchFieldsBasedOnUrlQuery(): void { this.selectionDetails.categories.selections = {}; this.selectionDetails.languageCodes.selections = {}; - let newQuery = ( - this.searchService.updateSearchFieldsBasedOnUrlQuery( - this.windowRef.nativeWindow.location.search, this.selectionDetails)); + let newQuery = this.searchService.updateSearchFieldsBasedOnUrlQuery( + this.windowRef.nativeWindow.location.search, + this.selectionDetails + ); if (this.searchQuery !== newQuery) { this.searchQuery = newQuery; @@ -242,35 +249,40 @@ export class SearchBarComponent implements OnInit, OnDestroy { // exception, we translate them here and update the translation // every time the language is changed. this.searchBarPlaceholder = this.translateService.instant( - 'I18N_LIBRARY_SEARCH_PLACEHOLDER'); + 'I18N_LIBRARY_SEARCH_PLACEHOLDER' + ); // 'messageformat' is the interpolation method for plural forms. // http://angular-translate.github.io/docs/#/guide/14_pluralization. this.categoryButtonText = this.translateService.instant( this.selectionDetails.categories.summary, - {...this.translationData, messageFormat: true}); + {...this.translationData, messageFormat: true} + ); this.languageButtonText = this.translateService.instant( this.selectionDetails.languageCodes.summary, - {...this.translationData, messageFormat: true}); + {...this.translationData, messageFormat: true} + ); } searchDropdownCategories(): SearchDropDownCategories[] { - return AppConstants.SEARCH_DROPDOWN_CATEGORIES.map((categoryName) => { + return AppConstants.SEARCH_DROPDOWN_CATEGORIES.map(categoryName => { return { id: categoryName, text: this.constructTranslationIdsService.getLibraryId( - 'categories', categoryName) + 'categories', + categoryName + ), }; }); } ngOnInit(): void { this.SEARCH_DROPDOWN_CATEGORIES = this.searchDropdownCategories(); - this.KEYBOARD_EVENT_TO_KEY_CODES = ( - this.navigationService.KEYBOARD_EVENT_TO_KEY_CODES); + this.KEYBOARD_EVENT_TO_KEY_CODES = + this.navigationService.KEYBOARD_EVENT_TO_KEY_CODES; this.ACTION_OPEN = this.navigationService.ACTION_OPEN; this.ACTION_CLOSE = this.navigationService.ACTION_CLOSE; - this.SUPPORTED_CONTENT_LANGUAGES = ( - this.languageUtilService.getLanguageIdsAndTexts()); + this.SUPPORTED_CONTENT_LANGUAGES = + this.languageUtilService.getLanguageIdsAndTexts(); this.selectionDetails = { categories: { description: '', @@ -278,7 +290,7 @@ export class SearchBarComponent implements OnInit, OnDestroy { masterList: this.SEARCH_DROPDOWN_CATEGORIES, numSelections: 0, selections: {}, - summary: '' + summary: '', }, languageCodes: { description: '', @@ -286,8 +298,8 @@ export class SearchBarComponent implements OnInit, OnDestroy { masterList: this.SUPPORTED_CONTENT_LANGUAGES, numSelections: 0, selections: {}, - summary: '' - } + summary: '', + }, }; // Non-translatable parts of the html strings, like numbers or user @@ -309,10 +321,9 @@ export class SearchBarComponent implements OnInit, OnDestroy { this.directiveSubscriptions.add( this.i18nLanguageCodeService.onPreferredLanguageCodesLoaded.subscribe( - (preferredLanguageCodesList) => { - preferredLanguageCodesList.forEach((languageCode) => { - let selections = - this.selectionDetails.languageCodes.selections; + preferredLanguageCodesList => { + preferredLanguageCodesList.forEach(languageCode => { + let selections = this.selectionDetails.languageCodes.selections; if (!selections.hasOwnProperty(languageCode)) { selections[languageCode] = true; } else { @@ -322,7 +333,8 @@ export class SearchBarComponent implements OnInit, OnDestroy { let selections = this.selectionDetails.languageCodes.selections; selections[ - this.i18nLanguageCodeService.getCurrentI18nLanguageCode()] = true; + this.i18nLanguageCodeService.getCurrentI18nLanguageCode() + ] = true; this.updateSelectionDetails('languageCodes'); @@ -331,7 +343,8 @@ export class SearchBarComponent implements OnInit, OnDestroy { } if ( - this.windowRef.nativeWindow.location.pathname === '/search/find') { + this.windowRef.nativeWindow.location.pathname === '/search/find' + ) { this.onSearchQueryChangeExec(); } @@ -345,12 +358,16 @@ export class SearchBarComponent implements OnInit, OnDestroy { ); this.directiveSubscriptions.add( - this.translateService.onLangChange - .subscribe(() => this.refreshSearchBarLabels())); + this.translateService.onLangChange.subscribe(() => + this.refreshSearchBarLabels() + ) + ); this.directiveSubscriptions.add( - this.classroomBackendApiService.onInitializeTranslation - .subscribe(() => this.refreshSearchBarLabels())); + this.classroomBackendApiService.onInitializeTranslation.subscribe(() => + this.refreshSearchBarLabels() + ) + ); } ngOnDestroy(): void { @@ -358,7 +375,9 @@ export class SearchBarComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive('oppiaSearchBar', +angular.module('oppia').directive( + 'oppiaSearchBar', downgradeComponent({ - component: SearchBarComponent - }) as angular.IDirectiveFactory); + component: SearchBarComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/library-page/search-bar/search-bar.module.ts b/core/templates/pages/library-page/search-bar/search-bar.module.ts index 337ab9bba14b..f18233601da1 100644 --- a/core/templates/pages/library-page/search-bar/search-bar.module.ts +++ b/core/templates/pages/library-page/search-bar/search-bar.module.ts @@ -16,14 +16,14 @@ * @fileoverview Module for the search bar component. */ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { TranslateModule } from '@ngx-translate/core'; -import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import {CommonModule} from '@angular/common'; +import {NgModule} from '@angular/core'; +import {FormsModule} from '@angular/forms'; +import {TranslateModule} from '@ngx-translate/core'; +import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; -import { SearchBarComponent } from 'pages/library-page/search-bar/search-bar.component'; -import { StringUtilityPipesModule } from 'filters/string-utility-filters/string-utility-pipes.module'; +import {SearchBarComponent} from 'pages/library-page/search-bar/search-bar.component'; +import {StringUtilityPipesModule} from 'filters/string-utility-filters/string-utility-pipes.module'; @NgModule({ imports: [ @@ -31,16 +31,10 @@ import { StringUtilityPipesModule } from 'filters/string-utility-filters/string- FormsModule, NgbModule, StringUtilityPipesModule, - TranslateModule + TranslateModule, ], - declarations: [ - SearchBarComponent - ], - entryComponents: [ - SearchBarComponent - ], - exports: [ - SearchBarComponent - ] + declarations: [SearchBarComponent], + entryComponents: [SearchBarComponent], + exports: [SearchBarComponent], }) export class SearchBarModule {} diff --git a/core/templates/pages/library-page/search-results/activity-tiles-infinity-grid.component.spec.ts b/core/templates/pages/library-page/search-results/activity-tiles-infinity-grid.component.spec.ts index 0cec4732dddd..4776bd284a02 100644 --- a/core/templates/pages/library-page/search-results/activity-tiles-infinity-grid.component.spec.ts +++ b/core/templates/pages/library-page/search-results/activity-tiles-infinity-grid.component.spec.ts @@ -16,15 +16,21 @@ * @fileoverview Unit tests for activityTilesInfinityGrid. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { of } from 'rxjs'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { LoaderService } from 'services/loader.service'; -import { SearchResponseBackendDict } from 'services/search-backend-api.service'; -import { SearchService } from 'services/search.service'; -import { ActivityTilesInfinityGridComponent } from './activity-tiles-infinity-grid.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {of} from 'rxjs'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {LoaderService} from 'services/loader.service'; +import {SearchResponseBackendDict} from 'services/search-backend-api.service'; +import {SearchService} from 'services/search.service'; +import {ActivityTilesInfinityGridComponent} from './activity-tiles-infinity-grid.component'; describe('Activity Tiles Infinity Grid Component', () => { let fixture: ComponentFixture; @@ -40,15 +46,9 @@ describe('Activity Tiles Infinity Grid Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - ActivityTilesInfinityGridComponent - ], - providers: [ - LoaderService, - SearchService, - WindowDimensionsService - ], - schemas: [NO_ERRORS_SCHEMA] + declarations: [ActivityTilesInfinityGridComponent], + providers: [LoaderService, SearchService, WindowDimensionsService], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -66,11 +66,15 @@ describe('Activity Tiles Infinity Grid Component', () => { it('should initialize', fakeAsync(() => { spyOnProperty(loaderService, 'onLoadingMessageChange').and.returnValue( - mockOnLoadingMessageChange); - spyOnProperty(searchService, 'onInitialSearchResultsLoaded') - .and.returnValue(mockOnInitialSearchResultsLoaded); + mockOnLoadingMessageChange + ); + spyOnProperty( + searchService, + 'onInitialSearchResultsLoaded' + ).and.returnValue(mockOnInitialSearchResultsLoaded); spyOn(windowDimensionsService, 'getResizeEvent').and.returnValue( - mockResizeEventEmitter); + mockResizeEventEmitter + ); spyOn(windowDimensionsService, 'getWidth').and.returnValue(400); componentInstance.ngOnInit(); @@ -89,14 +93,18 @@ describe('Activity Tiles Infinity Grid Component', () => { componentInstance.loadingMessage = ''; componentInstance.endOfPageIsReached = false; componentInstance.allActivitiesInOrder = []; - spyOn(searchService, 'loadMoreData').and.callFake(( + spyOn(searchService, 'loadMoreData').and.callFake( + ( successCallback: ( - SearchResponseData: SearchResponseBackendDict, boolean: boolean + SearchResponseData: SearchResponseBackendDict, + boolean: boolean ) => void, - failureCallback: (boolean: boolean) => void) => { - successCallback({ search_cursor: null, activity_list: [] }, true); - failureCallback(true); - }); + failureCallback: (boolean: boolean) => void + ) => { + successCallback({search_cursor: null, activity_list: []}, true); + failureCallback(true); + } + ); componentInstance.showMoreActivities(); expect(componentInstance.endOfPageIsReached).toBeTrue(); diff --git a/core/templates/pages/library-page/search-results/activity-tiles-infinity-grid.component.ts b/core/templates/pages/library-page/search-results/activity-tiles-infinity-grid.component.ts index 6874fd94c9ea..1e3641788937 100644 --- a/core/templates/pages/library-page/search-results/activity-tiles-infinity-grid.component.ts +++ b/core/templates/pages/library-page/search-results/activity-tiles-infinity-grid.component.ts @@ -16,17 +16,17 @@ * @fileoverview Component for an infinitely-scrollable view of activity tiles */ -import { Component } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { ExplorationSummaryDict } from 'domain/summary/exploration-summary-backend-api.service'; -import { Subscription } from 'rxjs'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { LoaderService } from 'services/loader.service'; -import { SearchService } from 'services/search.service'; +import {Component} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {ExplorationSummaryDict} from 'domain/summary/exploration-summary-backend-api.service'; +import {Subscription} from 'rxjs'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {LoaderService} from 'services/loader.service'; +import {SearchService} from 'services/search.service'; @Component({ selector: 'oppia-activity-tiles-infinity-grid', - templateUrl: './activity-tiles-infinity-grid.component.html' + templateUrl: './activity-tiles-infinity-grid.component.html', }) export class ActivityTilesInfinityGridComponent { // This property is initialized using Angular lifecycle hooks @@ -48,22 +48,26 @@ export class ActivityTilesInfinityGridComponent { showMoreActivities(): void { if (!this.loadingMessage && !this.endOfPageIsReached) { this.searchResultsAreLoading = true; - this.searchService.loadMoreData((data, endOfPageIsReached) => { - this.allActivitiesInOrder = ( - this.allActivitiesInOrder.concat(data.activity_list)); - this.endOfPageIsReached = endOfPageIsReached; - this.searchResultsAreLoading = false; - }, (endOfPageIsReached) => { - this.endOfPageIsReached = endOfPageIsReached; - this.searchResultsAreLoading = false; - }); + this.searchService.loadMoreData( + (data, endOfPageIsReached) => { + this.allActivitiesInOrder = this.allActivitiesInOrder.concat( + data.activity_list + ); + this.endOfPageIsReached = endOfPageIsReached; + this.searchResultsAreLoading = false; + }, + endOfPageIsReached => { + this.endOfPageIsReached = endOfPageIsReached; + this.searchResultsAreLoading = false; + } + ); } } ngOnInit(): void { this.directiveSubscriptions.add( this.loaderService.onLoadingMessageChange.subscribe( - (message: string) => this.loadingMessage = message + (message: string) => (this.loadingMessage = message) ) ); @@ -71,27 +75,30 @@ export class ActivityTilesInfinityGridComponent { // the server. this.directiveSubscriptions.add( this.searchService.onInitialSearchResultsLoaded.subscribe( - (activityList) => { + activityList => { this.allActivitiesInOrder = activityList; this.endOfPageIsReached = false; - }) + } + ) ); this.endOfPageIsReached = false; this.allActivitiesInOrder = []; var libraryWindowCutoffPx = 530; - this.libraryWindowIsNarrow = ( - this.windowDimensionsService.getWidth() <= libraryWindowCutoffPx); + this.libraryWindowIsNarrow = + this.windowDimensionsService.getWidth() <= libraryWindowCutoffPx; this.directiveSubscriptions.add( this.windowDimensionsService.getResizeEvent().subscribe(evt => { - this.libraryWindowIsNarrow = ( - this.windowDimensionsService.getWidth() <= libraryWindowCutoffPx); + this.libraryWindowIsNarrow = + this.windowDimensionsService.getWidth() <= libraryWindowCutoffPx; }) ); } } -angular.module('oppia').directive('oppiaActivityTilesInfinityGrid', +angular.module('oppia').directive( + 'oppiaActivityTilesInfinityGrid', downgradeComponent({ - component: ActivityTilesInfinityGridComponent - }) as angular.IDirectiveFactory); + component: ActivityTilesInfinityGridComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/library-page/search-results/search-results.component.spec.ts b/core/templates/pages/library-page/search-results/search-results.component.spec.ts index f106d34b3c3c..c7bcf7c2ab03 100644 --- a/core/templates/pages/library-page/search-results/search-results.component.spec.ts +++ b/core/templates/pages/library-page/search-results/search-results.component.spec.ts @@ -16,19 +16,25 @@ * @fileoverview Unit tests for searchResults. */ -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { SearchResultsComponent } from './search-results.component'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { LoaderService } from 'services/loader.service'; -import { SearchService } from 'services/search.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { UserService } from 'services/user.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { UserInfo } from 'domain/user/user-info.model'; -import { ExplorationSummaryDict } from 'domain/summary/exploration-summary-backend-api.service'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import {SearchResultsComponent} from './search-results.component'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {LoaderService} from 'services/loader.service'; +import {SearchService} from 'services/search.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {UserService} from 'services/user.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {UserInfo} from 'domain/user/user-info.model'; +import {ExplorationSummaryDict} from 'domain/summary/exploration-summary-backend-api.service'; describe('Search Results component', () => { let fixture: ComponentFixture; @@ -38,39 +44,35 @@ describe('Search Results component', () => { let userService: UserService; let loaderService: LoaderService; let urlInterpolationService: UrlInterpolationService; - let mockOnInitialSearchResultsLoaded = ( - new EventEmitter()); + let mockOnInitialSearchResultsLoaded = new EventEmitter< + ExplorationSummaryDict[] + >(); class MockWindowRef { nativeWindow = { location: { - href: '' + href: '', }, - gtag: () => {} + gtag: () => {}, }; } beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - SearchResultsComponent, - MockTranslatePipe - ], + imports: [HttpClientTestingModule], + declarations: [SearchResultsComponent, MockTranslatePipe], providers: [ { provide: WindowRef, - useClass: MockWindowRef + useClass: MockWindowRef, }, LoaderService, SearchService, SiteAnalyticsService, UrlInterpolationService, - UserService + UserService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -97,11 +99,25 @@ describe('Search Results component', () => { spyOn(loaderService, 'hideLoadingScreen'); let userIsLoggedIn = true; spyOn(userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(new UserInfo( - ['admin'], true, true, - true, true, true, 'en', 'test', null, userIsLoggedIn))); - spyOnProperty(searchService, 'onInitialSearchResultsLoaded') - .and.returnValue(mockOnInitialSearchResultsLoaded); + Promise.resolve( + new UserInfo( + ['admin'], + true, + true, + true, + true, + true, + 'en', + 'test', + null, + userIsLoggedIn + ) + ) + ); + spyOnProperty( + searchService, + 'onInitialSearchResultsLoaded' + ).and.returnValue(mockOnInitialSearchResultsLoaded); componentInstance.ngOnInit(); mockOnInitialSearchResultsLoaded.emit([]); tick(); @@ -120,8 +136,9 @@ describe('Search Results component', () => { it('should get static image url', () => { let staticUrl = 'test_url'; - spyOn(urlInterpolationService, 'getStaticAssetUrl') - .and.returnValue(staticUrl); + spyOn(urlInterpolationService, 'getStaticAssetUrl').and.returnValue( + staticUrl + ); expect(componentInstance.getStaticImageUrl('path')).toEqual(staticUrl); }); }); diff --git a/core/templates/pages/library-page/search-results/search-results.component.ts b/core/templates/pages/library-page/search-results/search-results.component.ts index 14f474f09ce6..719d5d523638 100644 --- a/core/templates/pages/library-page/search-results/search-results.component.ts +++ b/core/templates/pages/library-page/search-results/search-results.component.ts @@ -16,19 +16,19 @@ * @fileoverview Component for showing search results. */ -import { Component } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { Subscription } from 'rxjs'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { LoaderService } from 'services/loader.service'; -import { SearchService } from 'services/search.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { UserService } from 'services/user.service'; +import {Component} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {Subscription} from 'rxjs'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {LoaderService} from 'services/loader.service'; +import {SearchService} from 'services/search.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {UserService} from 'services/user.service'; @Component({ selector: 'oppia-search-results', - templateUrl: './search-results.component.html' + templateUrl: './search-results.component.html', }) export class SearchResultsComponent { directiveSubscriptions = new Subscription(); @@ -59,7 +59,7 @@ export class SearchResultsComponent { ngOnInit(): void { this.loaderService.showLoadingScreen('Loading'); let userInfoPromise = this.userService.getUserInfoAsync(); - userInfoPromise.then((userInfo) => { + userInfoPromise.then(userInfo => { this.userIsLoggedIn = userInfo.isLoggedIn(); }); @@ -67,14 +67,15 @@ export class SearchResultsComponent { // the server. this.directiveSubscriptions.add( this.searchService.onInitialSearchResultsLoaded.subscribe( - (activityList) => { + activityList => { this.someResultsExist = activityList.length > 0; - userInfoPromise.then((userInfo) => { + userInfoPromise.then(userInfo => { this.userIsLoggedIn = userInfo.isLoggedIn(); this.loaderService.hideLoadingScreen(); this.siteAnalyticsService.registerSearchResultsViewedEvent(); }); - }) + } + ) ); } @@ -83,7 +84,9 @@ export class SearchResultsComponent { } } -angular.module('oppia').directive('oppiaSearchResults', +angular.module('oppia').directive( + 'oppiaSearchResults', downgradeComponent({ - component: SearchResultsComponent - }) as angular.IDirectiveFactory); + component: SearchResultsComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/library-page/services/library-page-backend-api.service.spec.ts b/core/templates/pages/library-page/services/library-page-backend-api.service.spec.ts index 033721486359..25e876aac1d6 100644 --- a/core/templates/pages/library-page/services/library-page-backend-api.service.spec.ts +++ b/core/templates/pages/library-page/services/library-page-backend-api.service.spec.ts @@ -16,10 +16,12 @@ * @fileoverview Unit tests for LibraryPageBackendApiService. */ -import { HttpClientTestingModule, HttpTestingController } from - '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; -import { LibraryPageBackendApiService } from './library-page-backend-api.service'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; +import {LibraryPageBackendApiService} from './library-page-backend-api.service'; describe('Library page backend api service', () => { let lpbas: LibraryPageBackendApiService; @@ -27,7 +29,7 @@ describe('Library page backend api service', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] + imports: [HttpClientTestingModule], }); lpbas = TestBed.inject(LibraryPageBackendApiService); @@ -43,7 +45,7 @@ describe('Library page backend api service', () => { let failHandler = jasmine.createSpy('fail'); const resp = { activity_summary_dicts_by_category: [], - preferred_language_codes: ['en', 'es'] + preferred_language_codes: ['en', 'es'], }; lpbas.fetchLibraryIndexDataAsync().then(successHandler, failHandler); @@ -63,7 +65,7 @@ describe('Library page backend api service', () => { let failHandler = jasmine.createSpy('fail'); const resp = { explorations_list: [], - collections_list: [] + collections_list: [], }; lpbas.fetchCreatorDashboardDataAsync().then(successHandler, failHandler); @@ -84,13 +86,14 @@ describe('Library page backend api service', () => { const resp = { activity_list: [], header_i18n_id: 'I18N_TEST_ID', - preferred_language_codes: ['en', 'es'] + preferred_language_codes: ['en', 'es'], }; lpbas.fetchLibraryGroupDataAsync('g1').then(successHandler, failHandler); let req = httpTestingController.expectOne( - '/librarygrouphandler?group_name=g1'); + '/librarygrouphandler?group_name=g1' + ); expect(req.request.method).toEqual('GET'); req.flush(resp); diff --git a/core/templates/pages/library-page/services/library-page-backend-api.service.ts b/core/templates/pages/library-page/services/library-page-backend-api.service.ts index df728b177608..056e44b402b7 100644 --- a/core/templates/pages/library-page/services/library-page-backend-api.service.ts +++ b/core/templates/pages/library-page/services/library-page-backend-api.service.ts @@ -17,91 +17,93 @@ * backend. */ -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; -import { CollectionSummaryBackendDict } from 'domain/collection/collection-summary.model'; -import { CreatorExplorationSummaryBackendDict } from 'domain/summary/creator-exploration-summary.model'; +import {CollectionSummaryBackendDict} from 'domain/collection/collection-summary.model'; +import {CreatorExplorationSummaryBackendDict} from 'domain/summary/creator-exploration-summary.model'; interface LibraryGroupDataBackendDict { - 'activity_list': ActivityDict[]; - 'header_i18n_id': string; - 'preferred_language_codes': string[]; + activity_list: ActivityDict[]; + header_i18n_id: string; + preferred_language_codes: string[]; } export interface ActivityDict { - 'activity_type': string; - 'category': string; - 'community_owned': boolean; - 'id': string; - 'language_code': string; - 'num_views': number; - 'objective': string; - 'status': string; - 'tags': []; - 'thumbnail_bg_color': string; - 'thumbnail_icon_url': string; - 'title': string; + activity_type: string; + category: string; + community_owned: boolean; + id: string; + language_code: string; + num_views: number; + objective: string; + status: string; + tags: []; + thumbnail_bg_color: string; + thumbnail_icon_url: string; + title: string; } export interface SummaryDict { - 'activity_summary_dicts': ActivityDict[]; - 'categories': []; - 'header_i18n_id': string; - 'has_full_results_page': boolean; - 'full_results_url': string; - 'protractor_id': string; + activity_summary_dicts: ActivityDict[]; + categories: []; + header_i18n_id: string; + has_full_results_page: boolean; + full_results_url: string; + protractor_id: string; } interface CreatorDashboardDataBackendDict { - 'explorations_list': CreatorExplorationSummaryBackendDict[]; - 'collections_list': CollectionSummaryBackendDict[]; + explorations_list: CreatorExplorationSummaryBackendDict[]; + collections_list: CollectionSummaryBackendDict[]; } export interface LibraryIndexData { - 'activity_summary_dicts_by_category': SummaryDict[]; - 'preferred_language_codes': string[]; - } + activity_summary_dicts_by_category: SummaryDict[]; + preferred_language_codes: string[]; +} export interface CreatorDashboardData { - 'explorations_list': CreatorExplorationSummaryBackendDict[]; - 'collections_list': CollectionSummaryBackendDict[]; - } + explorations_list: CreatorExplorationSummaryBackendDict[]; + collections_list: CollectionSummaryBackendDict[]; +} export interface LibraryGroupData { - 'activity_list': ActivityDict[]; - 'header_i18n_id': string; - 'preferred_language_codes': string[]; - } + activity_list: ActivityDict[]; + header_i18n_id: string; + preferred_language_codes: string[]; +} @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class LibraryPageBackendApiService { LIBRARY_INDEX_HANDLER: string = '/libraryindexhandler'; CREATOR_DASHBOARD_HANDLER: string = '/creatordashboardhandler/data'; LIBRARY_GROUP_HANDLER: string = '/librarygrouphandler'; - constructor( - private http: HttpClient, - ) {} + constructor(private http: HttpClient) {} async fetchLibraryIndexDataAsync(): Promise { - return this.http.get( - this.LIBRARY_INDEX_HANDLER).toPromise(); + return this.http + .get(this.LIBRARY_INDEX_HANDLER) + .toPromise(); } async fetchCreatorDashboardDataAsync(): Promise { - return this.http.get( - this.CREATOR_DASHBOARD_HANDLER).toPromise(); + return this.http + .get(this.CREATOR_DASHBOARD_HANDLER) + .toPromise(); } - async fetchLibraryGroupDataAsync(groupName: string): - Promise { - return this.http.get( - this.LIBRARY_GROUP_HANDLER, { + async fetchLibraryGroupDataAsync( + groupName: string + ): Promise { + return this.http + .get(this.LIBRARY_GROUP_HANDLER, { params: { - group_name: groupName - } - }).toPromise(); + group_name: groupName, + }, + }) + .toPromise(); } } diff --git a/core/templates/pages/license-page/license-page-root.component.spec.ts b/core/templates/pages/license-page/license-page-root.component.spec.ts index 1769e87e19e4..24dc66b0a76a 100644 --- a/core/templates/pages/license-page/license-page-root.component.spec.ts +++ b/core/templates/pages/license-page/license-page-root.component.spec.ts @@ -16,15 +16,15 @@ * @fileoverview Unit tests for the license page root component. */ -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { LicensePageRootComponent } from './license-page-root.component'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {LicensePageRootComponent} from './license-page-root.component'; class MockTranslateService { onLangChange: EventEmitter = new EventEmitter(); @@ -41,18 +41,15 @@ describe('License Page Root', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - LicensePageRootComponent, - MockTranslatePipe - ], + declarations: [LicensePageRootComponent, MockTranslatePipe], providers: [ PageHeadService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -63,10 +60,9 @@ describe('License Page Root', () => { translateService = TestBed.inject(TranslateService); }); - it('should successfully instantiate the component', - () => { - expect(component).toBeDefined(); - }); + it('should successfully instantiate the component', () => { + expect(component).toBeDefined(); + }); it('should initialize and subscribe to onLangChange', () => { spyOn(translateService.onLangChange, 'subscribe'); @@ -92,10 +88,12 @@ describe('License Page Root', () => { component.setPageTitleAndMetaTags(); expect(translateService.instant).toHaveBeenCalledWith( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LICENSE.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LICENSE.TITLE + ); expect(pageHeadService.updateTitleAndMetaTags).toHaveBeenCalledWith( AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LICENSE.TITLE, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LICENSE.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LICENSE.META + ); }); it('should unsubscribe on component destruction', () => { diff --git a/core/templates/pages/license-page/license-page-root.component.ts b/core/templates/pages/license-page/license-page-root.component.ts index 24742b97078b..fe35db6b7aef 100644 --- a/core/templates/pages/license-page/license-page-root.component.ts +++ b/core/templates/pages/license-page/license-page-root.component.ts @@ -16,16 +16,16 @@ * @fileoverview Root component for License Page. */ -import { Component, OnDestroy } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; +import {Component, OnDestroy} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; @Component({ selector: 'oppia-license-page-root', - templateUrl: './license-page-root.component.html' + templateUrl: './license-page-root.component.html', }) export class LicensePageRootComponent implements OnDestroy { directiveSubscriptions = new Subscription(); @@ -36,10 +36,12 @@ export class LicensePageRootComponent implements OnDestroy { setPageTitleAndMetaTags(): void { let translatedTitle = this.translateService.instant( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LICENSE.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LICENSE.TITLE + ); this.pageHeadService.updateTitleAndMetaTags( translatedTitle, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LICENSE.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LICENSE.META + ); } ngOnInit(): void { diff --git a/core/templates/pages/license-page/license-page-routing.module.ts b/core/templates/pages/license-page/license-page-routing.module.ts index 1475c77199f1..43efbac0d6e3 100644 --- a/core/templates/pages/license-page/license-page-routing.module.ts +++ b/core/templates/pages/license-page/license-page-routing.module.ts @@ -16,24 +16,19 @@ * @fileoverview Routing module for license page. */ -import { NgModule } from '@angular/core'; -import { Route, RouterModule } from '@angular/router'; -import { LicensePageRootComponent } from './license-page-root.component'; +import {NgModule} from '@angular/core'; +import {Route, RouterModule} from '@angular/router'; +import {LicensePageRootComponent} from './license-page-root.component'; const routes: Route[] = [ { path: '', - component: LicensePageRootComponent - } + component: LicensePageRootComponent, + }, ]; @NgModule({ - imports: [ - RouterModule.forChild(routes) - ], - exports: [ - RouterModule - ] + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], }) - export class LicensePageRoutingModule {} diff --git a/core/templates/pages/license-page/license-page.component.spec.ts b/core/templates/pages/license-page/license-page.component.spec.ts index 1e4c8e77c92c..d3bc972d0fa4 100644 --- a/core/templates/pages/license-page/license-page.component.spec.ts +++ b/core/templates/pages/license-page/license-page.component.spec.ts @@ -16,13 +16,13 @@ * @fileoverview Unit tests for the license page. */ -import { NO_ERRORS_SCHEMA, Pipe, EventEmitter } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import {NO_ERRORS_SCHEMA, Pipe, EventEmitter} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; -import { LicensePageComponent } from './license-page.component'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; +import {LicensePageComponent} from './license-page.component'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; -@Pipe({ name: 'translate' }) +@Pipe({name: 'translate'}) class MockTranslatePipe { transform(value: string, params: Object | undefined): string { return value; @@ -50,10 +50,10 @@ describe('License Page', () => { providers: [ { provide: I18nLanguageCodeService, - useClass: MockI18nLanguageCodeService + useClass: MockI18nLanguageCodeService, }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); @@ -63,8 +63,7 @@ describe('License Page', () => { fixture.detectChanges(); }); - it('should successfully instantiate the component', - () => { - expect(component).toBeDefined(); - }); + it('should successfully instantiate the component', () => { + expect(component).toBeDefined(); + }); }); diff --git a/core/templates/pages/license-page/license-page.component.ts b/core/templates/pages/license-page/license-page.component.ts index 6f9941a60365..6c8092a83e11 100644 --- a/core/templates/pages/license-page/license-page.component.ts +++ b/core/templates/pages/license-page/license-page.component.ts @@ -16,16 +16,19 @@ * @fileoverview Component for the license page. */ -import { Component } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; @Component({ selector: 'oppia-license-page', templateUrl: './license-page.component.html', - styleUrls: [] + styleUrls: [], }) -export class LicensePageComponent { -} +export class LicensePageComponent {} -angular.module('oppia').directive('oppiaLicensePage', - downgradeComponent({component: LicensePageComponent})); +angular + .module('oppia') + .directive( + 'oppiaLicensePage', + downgradeComponent({component: LicensePageComponent}) + ); diff --git a/core/templates/pages/license-page/license.module.ts b/core/templates/pages/license-page/license.module.ts index 9d36aa02c119..97f5c1d3cec3 100644 --- a/core/templates/pages/license-page/license.module.ts +++ b/core/templates/pages/license-page/license.module.ts @@ -16,26 +16,16 @@ * @fileoverview Module for the license page. */ -import { LicensePageComponent } from './license-page.component'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { LicensePageRootComponent } from './license-page-root.component'; -import { CommonModule } from '@angular/common'; -import { LicensePageRoutingModule } from './license-page-routing.module'; -import { NgModule } from '@angular/core'; +import {LicensePageComponent} from './license-page.component'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {LicensePageRootComponent} from './license-page-root.component'; +import {CommonModule} from '@angular/common'; +import {LicensePageRoutingModule} from './license-page-routing.module'; +import {NgModule} from '@angular/core'; @NgModule({ - imports: [ - CommonModule, - SharedComponentsModule, - LicensePageRoutingModule - ], - declarations: [ - LicensePageComponent, - LicensePageRootComponent - ], - entryComponents: [ - LicensePageComponent, - LicensePageRootComponent, - ] + imports: [CommonModule, SharedComponentsModule, LicensePageRoutingModule], + declarations: [LicensePageComponent, LicensePageRootComponent], + entryComponents: [LicensePageComponent, LicensePageRootComponent], }) export class LicensePageModule {} diff --git a/core/templates/pages/lightweight-oppia-root/app.module.ts b/core/templates/pages/lightweight-oppia-root/app.module.ts index 0a8a528f180e..ab816828704e 100644 --- a/core/templates/pages/lightweight-oppia-root/app.module.ts +++ b/core/templates/pages/lightweight-oppia-root/app.module.ts @@ -16,24 +16,30 @@ * @fileoverview Module for the about page. */ -import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; -import { APP_INITIALIZER, NgModule } from '@angular/core'; - +import {HttpClientModule, HTTP_INTERCEPTORS} from '@angular/common/http'; +import {APP_INITIALIZER, NgModule} from '@angular/core'; // Modules. -import { BrowserModule, HAMMER_GESTURE_CONFIG, HammerGestureConfig } from '@angular/platform-browser'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { AppRoutingModule } from './routing/app.routing.module'; +import { + BrowserModule, + HAMMER_GESTURE_CONFIG, + HammerGestureConfig, +} from '@angular/platform-browser'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {AppRoutingModule} from './routing/app.routing.module'; // Components. -import { LightweightOppiaRootComponent } from './lightweight-oppia-root.component'; +import {LightweightOppiaRootComponent} from './lightweight-oppia-root.component'; // Miscellaneous. -import { platformFeatureInitFactory, PlatformFeatureService } from 'services/platform-feature.service'; -import { RequestInterceptor } from 'services/request-interceptor.service'; -import { CookieModule } from 'ngx-cookie'; -import { ToastrModule } from 'ngx-toastr'; -import { AppErrorHandlerProvider } from 'pages/oppia-root/app-error-handler'; +import { + platformFeatureInitFactory, + PlatformFeatureService, +} from 'services/platform-feature.service'; +import {RequestInterceptor} from 'services/request-interceptor.service'; +import {CookieModule} from 'ngx-cookie'; +import {ToastrModule} from 'ngx-toastr'; +import {AppErrorHandlerProvider} from 'pages/oppia-root/app-error-handler'; // This throws "TS2307". We need to // suppress this error because hammer come from hammerjs // dependency. We can't import it directly. @@ -47,26 +53,26 @@ export const toastrConfig = { error: 'toast-error', info: 'toast-info', success: 'toast-success', - warning: 'toast-warning' + warning: 'toast-warning', }, positionClass: 'toast-bottom-right', messageClass: 'toast-message e2e-test-toast-message', progressBar: false, tapToDismiss: true, - titleClass: 'toast-title' + titleClass: 'toast-title', }; export class HammerConfig extends HammerGestureConfig { overrides = { - swipe: { direction: hammer.DIRECTION_HORIZONTAL }, - pinch: { enable: false }, - rotate: { enable: false }, + swipe: {direction: hammer.DIRECTION_HORIZONTAL}, + pinch: {enable: false}, + rotate: {enable: false}, }; options = { cssProps: { - userSelect: true - } + userSelect: true, + }, }; } @@ -77,32 +83,28 @@ export class HammerConfig extends HammerGestureConfig { CookieModule.forRoot(), HttpClientModule, AppRoutingModule, - ToastrModule.forRoot(toastrConfig) - ], - declarations: [ - LightweightOppiaRootComponent, - ], - entryComponents: [ - LightweightOppiaRootComponent, + ToastrModule.forRoot(toastrConfig), ], + declarations: [LightweightOppiaRootComponent], + entryComponents: [LightweightOppiaRootComponent], providers: [ { provide: HTTP_INTERCEPTORS, useClass: RequestInterceptor, - multi: true + multi: true, }, AppErrorHandlerProvider, { provide: APP_INITIALIZER, useFactory: platformFeatureInitFactory, deps: [PlatformFeatureService], - multi: true + multi: true, }, { provide: HAMMER_GESTURE_CONFIG, - useClass: HammerConfig - } + useClass: HammerConfig, + }, ], - bootstrap: [LightweightOppiaRootComponent] + bootstrap: [LightweightOppiaRootComponent], }) export class LighweightAppModule {} diff --git a/core/templates/pages/lightweight-oppia-root/index.ts b/core/templates/pages/lightweight-oppia-root/index.ts index 4b5f444f2fde..717a87e1cf75 100644 --- a/core/templates/pages/lightweight-oppia-root/index.ts +++ b/core/templates/pages/lightweight-oppia-root/index.ts @@ -17,21 +17,21 @@ */ import 'pages/common-imports'; -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { enableProdMode } from '@angular/core'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {enableProdMode} from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { LighweightAppModule } from './app.module'; -import { LoggerService } from 'services/contextual/logger.service'; +import {AppConstants} from 'app.constants'; +import {LighweightAppModule} from './app.module'; +import {LoggerService} from 'services/contextual/logger.service'; if (!AppConstants.DEV_MODE) { enableProdMode(); } const loggerService = new LoggerService(); -platformBrowserDynamic().bootstrapModule(LighweightAppModule).catch( - (err) => loggerService.error(err) -); +platformBrowserDynamic() + .bootstrapModule(LighweightAppModule) + .catch(err => loggerService.error(err)); // This prevents angular pages to cause side effects to hybrid pages. // TODO(#13080): Remove window.name statement from import.ts files diff --git a/core/templates/pages/lightweight-oppia-root/lightweight-oppia-root.component.ts b/core/templates/pages/lightweight-oppia-root/lightweight-oppia-root.component.ts index b5b4cc99f69e..006102296fcf 100644 --- a/core/templates/pages/lightweight-oppia-root/lightweight-oppia-root.component.ts +++ b/core/templates/pages/lightweight-oppia-root/lightweight-oppia-root.component.ts @@ -16,10 +16,10 @@ * @fileoverview Oppia root component. */ -import { Component } from '@angular/core'; +import {Component} from '@angular/core'; @Component({ selector: 'lightweight-oppia-root', - templateUrl: './lightweight-oppia-root.component.html' + templateUrl: './lightweight-oppia-root.component.html', }) export class LightweightOppiaRootComponent {} diff --git a/core/templates/pages/lightweight-oppia-root/routing/app.routing.module.ts b/core/templates/pages/lightweight-oppia-root/routing/app.routing.module.ts index 1cb42f3dd06b..d36d1217b568 100644 --- a/core/templates/pages/lightweight-oppia-root/routing/app.routing.module.ts +++ b/core/templates/pages/lightweight-oppia-root/routing/app.routing.module.ts @@ -16,13 +16,12 @@ * @fileoverview Root routing module. */ -import { APP_BASE_HREF } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { Route, RouterModule } from '@angular/router'; - -import { AppConstants } from 'app.constants'; -import { CanAccessSplashPageGuard } from './guards/can-access-splash-page.guard'; +import {APP_BASE_HREF} from '@angular/common'; +import {NgModule} from '@angular/core'; +import {Route, RouterModule} from '@angular/router'; +import {AppConstants} from 'app.constants'; +import {CanAccessSplashPageGuard} from './guards/can-access-splash-page.guard'; // All paths must be defined in constants.ts file. // Otherwise pages will have false 404 status code. @@ -34,34 +33,33 @@ const routes: Route[] = [ path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.SPLASH.ROUTE, pathMatch: 'full', canLoad: [CanAccessSplashPageGuard], - loadChildren: () => import('pages/splash-page/splash-page.module') - .then(m => m.SplashPageModule) + loadChildren: () => + import('pages/splash-page/splash-page.module').then( + m => m.SplashPageModule + ), }, - ] - } + ], + }, ]; // '**' wildcard route must be kept at the end,as it can override all other // routes. routes.push({ path: '**', - loadChildren: () => import( - 'pages/error-pages/error-404/error-404-page.module').then( - m => m.Error404PageModule) + loadChildren: () => + import('pages/error-pages/error-404/error-404-page.module').then( + m => m.Error404PageModule + ), }); @NgModule({ - imports: [ - RouterModule.forRoot(routes), - ], - exports: [ - RouterModule - ], + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule], providers: [ { provide: APP_BASE_HREF, - useValue: '/' - } - ] + useValue: '/', + }, + ], }) export class AppRoutingModule {} diff --git a/core/templates/pages/lightweight-oppia-root/routing/guards/can-access-splash-page.guard.spec.ts b/core/templates/pages/lightweight-oppia-root/routing/guards/can-access-splash-page.guard.spec.ts index 6acf04a891b8..c43edf9e5521 100644 --- a/core/templates/pages/lightweight-oppia-root/routing/guards/can-access-splash-page.guard.spec.ts +++ b/core/templates/pages/lightweight-oppia-root/routing/guards/can-access-splash-page.guard.spec.ts @@ -16,20 +16,20 @@ * @fileoverview Unit tests for can access splash page guard. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { Route } from '@angular/router'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {fakeAsync, TestBed, tick} from '@angular/core/testing'; +import {Route} from '@angular/router'; -import { UserInfo } from 'domain/user/user-info.model'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { UserService } from 'services/user.service'; -import { CanAccessSplashPageGuard } from './can-access-splash-page.guard'; +import {UserInfo} from 'domain/user/user-info.model'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {UserService} from 'services/user.service'; +import {CanAccessSplashPageGuard} from './can-access-splash-page.guard'; class MockWindowRef { nativeWindow = { location: { - href: '' - } + href: '', + }, }; } @@ -40,16 +40,14 @@ describe('Can access splash page guard', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], + imports: [HttpClientTestingModule], providers: [ { provide: WindowRef, - useClass: MockWindowRef + useClass: MockWindowRef, }, - UserService - ] + UserService, + ], }).compileComponents(); caspg = TestBed.inject(CanAccessSplashPageGuard); windowRef = TestBed.inject(WindowRef); @@ -59,34 +57,38 @@ describe('Can access splash page guard', () => { it('should redirect user to default dashboard', fakeAsync(() => { let defaultDashboard = 'learner'; spyOn(userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(new UserInfo( - [], false, false, false, false, false, '', '', '', true))); + Promise.resolve( + new UserInfo([], false, false, false, false, false, '', '', '', true) + ) + ); spyOn(userService, 'getUserPreferredDashboardAsync').and.returnValue( - Promise.resolve('learner')); + Promise.resolve('learner') + ); caspg.canLoad({} as Route, []); tick(); tick(); expect(windowRef.nativeWindow.location.href).toEqual( - '/' + defaultDashboard + '-dashboard'); + '/' + defaultDashboard + '-dashboard' + ); })); it('should allow user to access page if not logged in', fakeAsync(() => { spyOn(userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(new UserInfo( - [], false, false, false, false, false, '', '', '', false))); - caspg.canLoad({} as Route, []).then((value) => { + Promise.resolve( + new UserInfo([], false, false, false, false, false, '', '', '', false) + ) + ); + caspg.canLoad({} as Route, []).then(value => { expect(value).toEqual(true); }); tick(); })); - it('should show user splash page if request to user hander fails', - fakeAsync(() => { - spyOn(userService, 'getUserInfoAsync').and.returnValue( - Promise.reject()); - caspg.canLoad({} as Route, []).then((value) => { - expect(value).toEqual(true); - }); - tick(); - })); + it('should show user splash page if request to user hander fails', fakeAsync(() => { + spyOn(userService, 'getUserInfoAsync').and.returnValue(Promise.reject()); + caspg.canLoad({} as Route, []).then(value => { + expect(value).toEqual(true); + }); + tick(); + })); }); diff --git a/core/templates/pages/lightweight-oppia-root/routing/guards/can-access-splash-page.guard.ts b/core/templates/pages/lightweight-oppia-root/routing/guards/can-access-splash-page.guard.ts index f3655334cbb0..3af7b93c2c55 100644 --- a/core/templates/pages/lightweight-oppia-root/routing/guards/can-access-splash-page.guard.ts +++ b/core/templates/pages/lightweight-oppia-root/routing/guards/can-access-splash-page.guard.ts @@ -16,13 +16,13 @@ * @fileoverview Route guard for validating access to splash page. */ -import { Injectable } from '@angular/core'; -import { CanLoad, Route, UrlSegment } from '@angular/router'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { UserService } from 'services/user.service'; +import {Injectable} from '@angular/core'; +import {CanLoad, Route, UrlSegment} from '@angular/router'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {UserService} from 'services/user.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class CanAccessSplashPageGuard implements CanLoad { constructor( @@ -31,22 +31,25 @@ export class CanAccessSplashPageGuard implements CanLoad { ) {} canLoad(route: Route, segments: UrlSegment[]): Promise { - return this.userService.getUserInfoAsync().then((userInfo) => { - if (userInfo.isLoggedIn()) { - this.userService.getUserPreferredDashboardAsync().then( - (preferredDashboard) => { - // Use router.navigate once both learner dashbaord page and - // creator dashboard page are migrated to angular router. - this.windowRef.nativeWindow.location.href = ( - '/' + preferredDashboard + '-dashboard'); - } - ); - return false; - } else { + return this.userService.getUserInfoAsync().then( + userInfo => { + if (userInfo.isLoggedIn()) { + this.userService + .getUserPreferredDashboardAsync() + .then(preferredDashboard => { + // Use router.navigate once both learner dashbaord page and + // creator dashboard page are migrated to angular router. + this.windowRef.nativeWindow.location.href = + '/' + preferredDashboard + '-dashboard'; + }); + return false; + } else { + return true; + } + }, + () => { return true; } - }, () => { - return true; - }); + ); } } diff --git a/core/templates/pages/lightweight-oppia-root/routing/guards/is-logged-in.guard.spec.ts b/core/templates/pages/lightweight-oppia-root/routing/guards/is-logged-in.guard.spec.ts index bc753fdf48f0..d1acb6b7724a 100644 --- a/core/templates/pages/lightweight-oppia-root/routing/guards/is-logged-in.guard.spec.ts +++ b/core/templates/pages/lightweight-oppia-root/routing/guards/is-logged-in.guard.spec.ts @@ -16,14 +16,19 @@ * @fileoverview Tests for IsLoggedInGuard. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TestBed } from '@angular/core/testing'; -import { ActivatedRouteSnapshot, Router, RouterStateSnapshot, NavigationExtras } from '@angular/router'; -import { AppConstants } from 'app.constants'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TestBed} from '@angular/core/testing'; +import { + ActivatedRouteSnapshot, + Router, + RouterStateSnapshot, + NavigationExtras, +} from '@angular/router'; +import {AppConstants} from 'app.constants'; -import { UserInfo } from 'domain/user/user-info.model'; -import { UserService } from 'services/user.service'; -import { IsLoggedInGuard } from './is-logged-in.guard'; +import {UserInfo} from 'domain/user/user-info.model'; +import {UserService} from 'services/user.service'; +import {IsLoggedInGuard} from './is-logged-in.guard'; class MockRouter { navigate(commands: string[], extras?: NavigationExtras): Promise { @@ -39,7 +44,7 @@ describe('IsLoggedInGuard', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [UserService, { provide: Router, useClass: MockRouter }], + providers: [UserService, {provide: Router, useClass: MockRouter}], }).compileComponents(); guard = TestBed.inject(IsLoggedInGuard); @@ -47,37 +52,42 @@ describe('IsLoggedInGuard', () => { router = TestBed.inject(Router); }); - it('should redirect user to login page if user is not logged in', (done) => { + it('should redirect user to login page if user is not logged in', done => { const getUserInfoAsyncSpy = spyOn( - userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(UserInfo.createDefault()) - ); + userService, + 'getUserInfoAsync' + ).and.returnValue(Promise.resolve(UserInfo.createDefault())); const navigateSpy = spyOn(router, 'navigate'); - const routerStateSnapshot = { url: 'url' } as RouterStateSnapshot; + const routerStateSnapshot = {url: 'url'} as RouterStateSnapshot; - guard.canActivate(new ActivatedRouteSnapshot(), routerStateSnapshot).then( - (canActivate) => { + guard + .canActivate(new ActivatedRouteSnapshot(), routerStateSnapshot) + .then(canActivate => { expect(canActivate).toBeFalse(); expect(getUserInfoAsyncSpy).toHaveBeenCalledTimes(1); expect(navigateSpy).toHaveBeenCalledWith( [`/${AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LOGIN.ROUTE}`], - { queryParams: { return_url: routerStateSnapshot.url } } + {queryParams: {return_url: routerStateSnapshot.url}} ); done(); }); }); - it('should not redirect user to login page if user is logged in', (done) => { + it('should not redirect user to login page if user is logged in', done => { const getUserInfoAsyncSpy = spyOn( - userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(new UserInfo( - [], false, false, false, false, false, '', '', '', true)) + userService, + 'getUserInfoAsync' + ).and.returnValue( + Promise.resolve( + new UserInfo([], false, false, false, false, false, '', '', '', true) + ) ); const navigateSpy = spyOn(router, 'navigate'); - const routerStateSnapshot = { url: 'url' } as RouterStateSnapshot; + const routerStateSnapshot = {url: 'url'} as RouterStateSnapshot; - guard.canActivate(new ActivatedRouteSnapshot(), routerStateSnapshot).then( - (canActivate) => { + guard + .canActivate(new ActivatedRouteSnapshot(), routerStateSnapshot) + .then(canActivate => { expect(canActivate).toBeTrue(); expect(getUserInfoAsyncSpy).toHaveBeenCalledTimes(1); expect(navigateSpy).not.toHaveBeenCalled(); diff --git a/core/templates/pages/lightweight-oppia-root/routing/guards/is-logged-in.guard.ts b/core/templates/pages/lightweight-oppia-root/routing/guards/is-logged-in.guard.ts index bc51bf4e30c3..13644da758ed 100644 --- a/core/templates/pages/lightweight-oppia-root/routing/guards/is-logged-in.guard.ts +++ b/core/templates/pages/lightweight-oppia-root/routing/guards/is-logged-in.guard.ts @@ -17,7 +17,7 @@ * if the user is not logged in. */ -import { Injectable } from '@angular/core'; +import {Injectable} from '@angular/core'; import { ActivatedRouteSnapshot, CanActivate, @@ -25,18 +25,21 @@ import { RouterStateSnapshot, } from '@angular/router'; -import { AppConstants } from 'app.constants'; -import { UserService } from 'services/user.service'; +import {AppConstants} from 'app.constants'; +import {UserService} from 'services/user.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class IsLoggedInGuard implements CanActivate { - constructor(private userService: UserService, private router: Router) {} + constructor( + private userService: UserService, + private router: Router + ) {} async canActivate( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot ): Promise { const userInfo = await this.userService.getUserInfoAsync(); if (userInfo.isLoggedIn()) { @@ -44,9 +47,11 @@ export class IsLoggedInGuard implements CanActivate { } this.router.navigate( - [`/${AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LOGIN.ROUTE}`], { - queryParams: { return_url: state.url }, - }); + [`/${AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LOGIN.ROUTE}`], + { + queryParams: {return_url: state.url}, + } + ); return false; } } diff --git a/core/templates/pages/login-page/login-page-root.component.spec.ts b/core/templates/pages/login-page/login-page-root.component.spec.ts index e814424390a7..dc0f1b746866 100644 --- a/core/templates/pages/login-page/login-page-root.component.spec.ts +++ b/core/templates/pages/login-page/login-page-root.component.spec.ts @@ -16,14 +16,14 @@ * @fileoverview Unit tests for the login page root component. */ -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { LoginPageRootComponent } from './login-page-root.component'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {LoginPageRootComponent} from './login-page-root.component'; class MockTranslateService { onLangChange: EventEmitter = new EventEmitter(); @@ -40,18 +40,15 @@ describe('Login Page Root', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - LoginPageRootComponent, - MockTranslatePipe - ], + declarations: [LoginPageRootComponent, MockTranslatePipe], providers: [ PageHeadService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -62,10 +59,9 @@ describe('Login Page Root', () => { translateService = TestBed.inject(TranslateService); }); - it('should successfully instantiate the component', - () => { - expect(component).toBeDefined(); - }); + it('should successfully instantiate the component', () => { + expect(component).toBeDefined(); + }); it('should initialize and subscribe to onLangChange', () => { spyOn(translateService.onLangChange, 'subscribe'); @@ -91,10 +87,12 @@ describe('Login Page Root', () => { component.setPageTitleAndMetaTags(); expect(translateService.instant).toHaveBeenCalledWith( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LOGIN.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LOGIN.TITLE + ); expect(pageHeadService.updateTitleAndMetaTags).toHaveBeenCalledWith( AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LOGIN.TITLE, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LOGIN.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LOGIN.META + ); }); it('should unsubscribe on component destruction', () => { diff --git a/core/templates/pages/login-page/login-page-root.component.ts b/core/templates/pages/login-page/login-page-root.component.ts index e90b1cb160c0..93033f863020 100644 --- a/core/templates/pages/login-page/login-page-root.component.ts +++ b/core/templates/pages/login-page/login-page-root.component.ts @@ -16,16 +16,16 @@ * @fileoverview Root component for login page. */ -import { Component, OnDestroy } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; +import {Component, OnDestroy} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; @Component({ selector: 'oppia-login-page-root', - templateUrl: './login-page-root.component.html' + templateUrl: './login-page-root.component.html', }) export class LoginPageRootComponent implements OnDestroy { directiveSubscriptions = new Subscription(); @@ -36,10 +36,12 @@ export class LoginPageRootComponent implements OnDestroy { setPageTitleAndMetaTags(): void { let translatedTitle = this.translateService.instant( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LOGIN.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LOGIN.TITLE + ); this.pageHeadService.updateTitleAndMetaTags( translatedTitle, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LOGIN.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LOGIN.META + ); } ngOnInit(): void { diff --git a/core/templates/pages/login-page/login-page-routing.module.ts b/core/templates/pages/login-page/login-page-routing.module.ts index d7dc775a5abe..95cf6ed35c4a 100644 --- a/core/templates/pages/login-page/login-page-routing.module.ts +++ b/core/templates/pages/login-page/login-page-routing.module.ts @@ -16,24 +16,19 @@ * @fileoverview Routing module for login page. */ -import { NgModule } from '@angular/core'; -import { Route, RouterModule } from '@angular/router'; -import { LoginPageRootComponent } from './login-page-root.component'; +import {NgModule} from '@angular/core'; +import {Route, RouterModule} from '@angular/router'; +import {LoginPageRootComponent} from './login-page-root.component'; const routes: Route[] = [ { path: '', - component: LoginPageRootComponent - } + component: LoginPageRootComponent, + }, ]; @NgModule({ - imports: [ - RouterModule.forChild(routes) - ], - exports: [ - RouterModule - ] + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], }) - export class LoginPageRoutingModule {} diff --git a/core/templates/pages/login-page/login-page.component.spec.ts b/core/templates/pages/login-page/login-page.component.spec.ts index fa638d4f827b..519076c088bb 100644 --- a/core/templates/pages/login-page/login-page.component.spec.ts +++ b/core/templates/pages/login-page/login-page.component.spec.ts @@ -16,24 +16,33 @@ * @fileoverview Unit tests for the login page. */ -import { ComponentFixture, fakeAsync, flush, flushMicrotasks, TestBed, tick } from '@angular/core/testing'; -import { ReactiveFormsModule } from '@angular/forms'; -import { MatAutocompleteModule } from '@angular/material/autocomplete'; -import { MatButtonModule } from '@angular/material/button'; -import { MatCardModule } from '@angular/material/card'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatInputModule } from '@angular/material/input'; -import { UserInfo } from 'domain/user/user-info.model'; -import { AlertsService } from 'services/alerts.service'; -import { AuthService } from 'services/auth.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { LoaderService } from 'services/loader.service'; -import { UserService } from 'services/user.service'; -import { LoginPageComponent } from './login-page.component'; +import { + ComponentFixture, + fakeAsync, + flush, + flushMicrotasks, + TestBed, + tick, +} from '@angular/core/testing'; +import {ReactiveFormsModule} from '@angular/forms'; +import {MatAutocompleteModule} from '@angular/material/autocomplete'; +import {MatButtonModule} from '@angular/material/button'; +import {MatCardModule} from '@angular/material/card'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatInputModule} from '@angular/material/input'; +import {UserInfo} from 'domain/user/user-info.model'; +import {AlertsService} from 'services/alerts.service'; +import {AuthService} from 'services/auth.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {LoaderService} from 'services/loader.service'; +import {UserService} from 'services/user.service'; +import {LoginPageComponent} from './login-page.component'; class MockWindowRef { constructor( - public location: string | null = null, public searchParams: string = '') {} + public location: string | null = null, + public searchParams: string = '' + ) {} get nativeWindow() { const that = this; @@ -45,7 +54,7 @@ class MockWindowRef { assign: (url: string) => { that.location = url; }, - } + }, }; } } @@ -126,11 +135,11 @@ describe('Login Page', () => { ], declarations: [LoginPageComponent], providers: [ - { provide: AlertsService, useValue: alertsService }, - { provide: AuthService, useValue: authService }, - { provide: LoaderService, useValue: loaderService }, - { provide: UserService, useValue: userService }, - { provide: WindowRef, useValue: windowRef }, + {provide: AlertsService, useValue: alertsService}, + {provide: AuthService, useValue: authService}, + {provide: LoaderService, useValue: loaderService}, + {provide: UserService, useValue: userService}, + {provide: WindowRef, useValue: windowRef}, ], }).compileComponents(); @@ -143,18 +152,20 @@ describe('Login Page', () => { }); it('should redirect to home page when already logged in', fakeAsync(() => { - userService.getUserInfoAsync.and.resolveTo(UserInfo.createFromBackendDict({ - roles: ['EXPLORATION_EDITOR'], - is_moderator: false, - is_curriculum_admin: false, - is_super_admin: false, - is_topic_manager: false, - can_create_collections: false, - preferred_site_language_code: null, - username: null, - email: null, - user_is_logged_in: true, - })); + userService.getUserInfoAsync.and.resolveTo( + UserInfo.createFromBackendDict({ + roles: ['EXPLORATION_EDITOR'], + is_moderator: false, + is_curriculum_admin: false, + is_super_admin: false, + is_topic_manager: false, + can_create_collections: false, + preferred_site_language_code: null, + username: null, + email: null, + user_is_logged_in: true, + }) + ); expect(windowRef.location).toBeNull(); @@ -164,11 +175,14 @@ describe('Login Page', () => { expect(windowRef.location).toEqual('/'); })); - describe('Emulator mode', function() { + describe('Emulator mode', function () { beforeEach(() => { email = 'a@a.com'; - spyOnProperty(loginPageComponent, 'emulatorModeIsEnabled', 'get') - .and.returnValue(true); + spyOnProperty( + loginPageComponent, + 'emulatorModeIsEnabled', + 'get' + ).and.returnValue(true); }); it('should not handle redirect results', fakeAsync(() => { @@ -245,8 +259,11 @@ describe('Login Page', () => { describe('Production mode', () => { beforeEach(() => { - spyOnProperty(loginPageComponent, 'emulatorModeIsEnabled', 'get') - .and.returnValue(false); + spyOnProperty( + loginPageComponent, + 'emulatorModeIsEnabled', + 'get' + ).and.returnValue(false); }); it('should redirect to sign-up after successful redirect', fakeAsync(() => { @@ -329,33 +346,37 @@ describe('Login Page', () => { expect(windowRef.location).toEqual('/signup?return_url=/admin'); })); - it('should redirect to home page when sign in with redirect fails', - fakeAsync(() => { - const signInWithRedirectAsyncPromise = spyOnSignInWithRedirectAsync(); + it('should redirect to home page when sign in with redirect fails', fakeAsync(() => { + const signInWithRedirectAsyncPromise = spyOnSignInWithRedirectAsync(); - loginPageComponent.ngOnInit(); - flushMicrotasks(); + loginPageComponent.ngOnInit(); + flushMicrotasks(); - expect(windowRef.location).toBeNull(); + expect(windowRef.location).toBeNull(); - signInWithRedirectAsyncPromise.reject( - {code: 'auth/unknown-error', message: '?'}); + signInWithRedirectAsyncPromise.reject({ + code: 'auth/unknown-error', + message: '?', + }); - flush(); + flush(); - expect(windowRef.location).toEqual('/'); - })); + expect(windowRef.location).toEqual('/'); + })); - it('should redirect to home page when it cannot determine if user is ' + - 'logged in', fakeAsync(() => { - userService.getUserInfoAsync.and.rejectWith(Error('uh-oh!')); + it( + 'should redirect to home page when it cannot determine if user is ' + + 'logged in', + fakeAsync(() => { + userService.getUserInfoAsync.and.rejectWith(Error('uh-oh!')); - expect(windowRef.location).toBeNull(); + expect(windowRef.location).toBeNull(); - loginPageComponent.ngOnInit(); - flush(); + loginPageComponent.ngOnInit(); + flush(); - expect(windowRef.location).toEqual('/'); - })); + expect(windowRef.location).toEqual('/'); + }) + ); }); }); diff --git a/core/templates/pages/login-page/login-page.component.ts b/core/templates/pages/login-page/login-page.component.ts index 7a6fae48e7e0..f24d7dfdefa8 100644 --- a/core/templates/pages/login-page/login-page.component.ts +++ b/core/templates/pages/login-page/login-page.component.ts @@ -16,30 +16,33 @@ * @fileoverview Component for the login page. */ -import { Component, OnInit } from '@angular/core'; -import { FormControl, FormGroup, Validators } from '@angular/forms'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component, OnInit} from '@angular/core'; +import {FormControl, FormGroup, Validators} from '@angular/forms'; +import {downgradeComponent} from '@angular/upgrade/static'; import firebase from 'firebase/app'; -import { AppConstants } from 'app.constants'; -import { AlertsService } from 'services/alerts.service'; -import { AuthService } from 'services/auth.service'; -import { LoaderService } from 'services/loader.service'; -import { UserService } from 'services/user.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import {AppConstants} from 'app.constants'; +import {AlertsService} from 'services/alerts.service'; +import {AuthService} from 'services/auth.service'; +import {LoaderService} from 'services/loader.service'; +import {UserService} from 'services/user.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; @Component({ selector: 'login-page', - templateUrl: './login-page.component.html' + templateUrl: './login-page.component.html', }) export class LoginPageComponent implements OnInit { email = new FormControl('', [Validators.email]); formGroup = new FormGroup({email: this.email}); constructor( - private alertsService: AlertsService, private authService: AuthService, - private loaderService: LoaderService, private userService: UserService, - private windowRef: WindowRef) {} + private alertsService: AlertsService, + private authService: AuthService, + private loaderService: LoaderService, + private userService: UserService, + private windowRef: WindowRef + ) {} get emulatorModeIsEnabled(): boolean { return AppConstants.EMULATOR_MODE; @@ -48,47 +51,49 @@ export class LoginPageComponent implements OnInit { ngOnInit(): void { this.loaderService.showLoadingScreen('I18N_SIGNIN_LOADING'); - this.userService.getUserInfoAsync().then(async(userInfo) => { - if (userInfo.isLoggedIn()) { - this.redirectToPath('/'); - return; + this.userService.getUserInfoAsync().then( + async userInfo => { + if (userInfo.isLoggedIn()) { + this.redirectToPath('/'); + return; + } + + if (this.emulatorModeIsEnabled) { + this.loaderService.hideLoadingScreen(); + return; + } + + let authSucceeded = false; + try { + authSucceeded = await this.authService.handleRedirectResultAsync(); + // We use unknown type because we are unsure of the type of error + // that was thrown. Since the catch block cannot identify the + // specific type of error, we are unable to further optimise the + // code by introducing more types of errors. + } catch (error: unknown) { + this.onSignInError(error as firebase.auth.Error); + return; + } + + if (authSucceeded) { + this.redirectToSignUp(); + return; + } + + try { + await this.authService.signInWithRedirectAsync(); + // We use unknown type because we are unsure of the type of error + // that was thrown. Since the catch block cannot identify the + // specific type of error, we are unable to further optimise the + // code by introducing more types of errors. + } catch (error: unknown) { + this.onSignInError(error as firebase.auth.Error); + } + }, + error => { + this.onSignInError(error); } - - if (this.emulatorModeIsEnabled) { - this.loaderService.hideLoadingScreen(); - return; - } - - let authSucceeded = false; - try { - authSucceeded = await this.authService.handleRedirectResultAsync(); - // We use unknown type because we are unsure of the type of error - // that was thrown. Since the catch block cannot identify the - // specific type of error, we are unable to further optimise the - // code by introducing more types of errors. - } catch (error: unknown) { - this.onSignInError(error as firebase.auth.Error); - return; - } - - if (authSucceeded) { - this.redirectToSignUp(); - return; - } - - try { - await this.authService.signInWithRedirectAsync(); - // We use unknown type because we are unsure of the type of error - // that was thrown. Since the catch block cannot identify the - // specific type of error, we are unable to further optimise the - // code by introducing more types of errors. - } catch (error: unknown) { - this.onSignInError(error as firebase.auth.Error); - } - }, - error => { - this.onSignInError(error); - }); + ); } async onClickSignInButtonAsync(email: string): Promise { @@ -96,10 +101,10 @@ export class LoginPageComponent implements OnInit { try { await this.authService.signInWithEmail(email); - // We use unknown type because we are unsure of the type of error - // that was thrown. Since the catch block cannot identify the - // specific type of error, we are unable to further optimise the - // code by introducing more types of errors. + // We use unknown type because we are unsure of the type of error + // that was thrown. Since the catch block cannot identify the + // specific type of error, we are unable to further optimise the + // code by introducing more types of errors. } catch (error: unknown) { this.onSignInError(error as firebase.auth.Error); return; @@ -135,5 +140,6 @@ export class LoginPageComponent implements OnInit { } } -angular.module('oppia').directive( - 'loginPage', downgradeComponent({component: LoginPageComponent})); +angular + .module('oppia') + .directive('loginPage', downgradeComponent({component: LoginPageComponent})); diff --git a/core/templates/pages/login-page/login-page.module.ts b/core/templates/pages/login-page/login-page.module.ts index a293476e84b4..984bbdf48598 100644 --- a/core/templates/pages/login-page/login-page.module.ts +++ b/core/templates/pages/login-page/login-page.module.ts @@ -16,19 +16,19 @@ * @fileoverview Module for the login page. */ -import { NgModule } from '@angular/core'; -import { ReactiveFormsModule } from '@angular/forms'; -import { MatAutocompleteModule } from '@angular/material/autocomplete'; -import { MatButtonModule } from '@angular/material/button'; -import { MatCardModule } from '@angular/material/card'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatInputModule } from '@angular/material/input'; +import {NgModule} from '@angular/core'; +import {ReactiveFormsModule} from '@angular/forms'; +import {MatAutocompleteModule} from '@angular/material/autocomplete'; +import {MatButtonModule} from '@angular/material/button'; +import {MatCardModule} from '@angular/material/card'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatInputModule} from '@angular/material/input'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { LoginPageComponent } from 'pages/login-page/login-page.component'; -import { LoginPageRootComponent } from './login-page-root.component'; -import { CommonModule } from '@angular/common'; -import { LoginPageRoutingModule } from './login-page-routing.module'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {LoginPageComponent} from 'pages/login-page/login-page.component'; +import {LoginPageRootComponent} from './login-page-root.component'; +import {CommonModule} from '@angular/common'; +import {LoginPageRoutingModule} from './login-page-routing.module'; @NgModule({ imports: [ @@ -40,15 +40,9 @@ import { LoginPageRoutingModule } from './login-page-routing.module'; MatFormFieldModule, ReactiveFormsModule, SharedComponentsModule, - LoginPageRoutingModule + LoginPageRoutingModule, ], - declarations: [ - LoginPageComponent, - LoginPageRootComponent, - ], - entryComponents: [ - LoginPageComponent, - LoginPageRootComponent, - ] + declarations: [LoginPageComponent, LoginPageRootComponent], + entryComponents: [LoginPageComponent, LoginPageRootComponent], }) export class LoginPageModule {} diff --git a/core/templates/pages/logout-page/logout-page-root.component.spec.ts b/core/templates/pages/logout-page/logout-page-root.component.spec.ts index b33439a9eb60..c4fcbafd84d7 100644 --- a/core/templates/pages/logout-page/logout-page-root.component.spec.ts +++ b/core/templates/pages/logout-page/logout-page-root.component.spec.ts @@ -16,15 +16,15 @@ * @fileoverview Unit tests for the logout page root component. */ -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { LogoutPageRootComponent } from './logout-page-root.component'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {LogoutPageRootComponent} from './logout-page-root.component'; class MockTranslateService { onLangChange: EventEmitter = new EventEmitter(); @@ -41,18 +41,15 @@ describe('Logout Page Root', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - LogoutPageRootComponent, - MockTranslatePipe - ], + declarations: [LogoutPageRootComponent, MockTranslatePipe], providers: [ PageHeadService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -63,10 +60,9 @@ describe('Logout Page Root', () => { translateService = TestBed.inject(TranslateService); }); - it('should successfully instantiate the component', - () => { - expect(component).toBeDefined(); - }); + it('should successfully instantiate the component', () => { + expect(component).toBeDefined(); + }); it('should initialize and subscribe to onLangChange', () => { spyOn(translateService.onLangChange, 'subscribe'); @@ -92,10 +88,12 @@ describe('Logout Page Root', () => { component.setPageTitleAndMetaTags(); expect(translateService.instant).toHaveBeenCalledWith( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LOGOUT.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LOGOUT.TITLE + ); expect(pageHeadService.updateTitleAndMetaTags).toHaveBeenCalledWith( AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LOGOUT.TITLE, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LOGOUT.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LOGOUT.META + ); }); it('should unsubscribe on component destruction', () => { diff --git a/core/templates/pages/logout-page/logout-page-root.component.ts b/core/templates/pages/logout-page/logout-page-root.component.ts index ed7f26731c76..77030d2f0336 100644 --- a/core/templates/pages/logout-page/logout-page-root.component.ts +++ b/core/templates/pages/logout-page/logout-page-root.component.ts @@ -16,16 +16,16 @@ * @fileoverview Root component for Logout Page. */ -import { Component, OnDestroy } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; +import {Component, OnDestroy} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; @Component({ selector: 'oppia-logout-page-root', - templateUrl: './logout-page-root.component.html' + templateUrl: './logout-page-root.component.html', }) export class LogoutPageRootComponent implements OnDestroy { directiveSubscriptions = new Subscription(); @@ -36,10 +36,12 @@ export class LogoutPageRootComponent implements OnDestroy { setPageTitleAndMetaTags(): void { let translatedTitle = this.translateService.instant( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LOGOUT.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LOGOUT.TITLE + ); this.pageHeadService.updateTitleAndMetaTags( translatedTitle, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LOGOUT.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LOGOUT.META + ); } ngOnInit(): void { diff --git a/core/templates/pages/logout-page/logout-page-routing.module.ts b/core/templates/pages/logout-page/logout-page-routing.module.ts index 4e1e7557b89e..fcde09b6fd3a 100644 --- a/core/templates/pages/logout-page/logout-page-routing.module.ts +++ b/core/templates/pages/logout-page/logout-page-routing.module.ts @@ -16,24 +16,19 @@ * @fileoverview Routing module for logout page. */ -import { NgModule } from '@angular/core'; -import { Route, RouterModule } from '@angular/router'; -import { LogoutPageRootComponent } from './logout-page-root.component'; +import {NgModule} from '@angular/core'; +import {Route, RouterModule} from '@angular/router'; +import {LogoutPageRootComponent} from './logout-page-root.component'; const routes: Route[] = [ { path: '', - component: LogoutPageRootComponent - } + component: LogoutPageRootComponent, + }, ]; @NgModule({ - imports: [ - RouterModule.forChild(routes) - ], - exports: [ - RouterModule - ] + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], }) - export class LogoutPageRoutingModule {} diff --git a/core/templates/pages/logout-page/logout-page.component.spec.ts b/core/templates/pages/logout-page/logout-page.component.spec.ts index 6346a7ba34f6..220fe9b8c34e 100644 --- a/core/templates/pages/logout-page/logout-page.component.spec.ts +++ b/core/templates/pages/logout-page/logout-page.component.spec.ts @@ -16,17 +16,26 @@ * @fileoverview Unit tests for the logout page. */ -import { ComponentFixture, fakeAsync, flush, flushMicrotasks, TestBed, tick } from '@angular/core/testing'; - -import { LogoutPageComponent } from 'pages/logout-page/logout-page.component'; -import { AlertsService } from 'services/alerts.service'; -import { AuthService } from 'services/auth.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { LoaderService } from 'services/loader.service'; +import { + ComponentFixture, + fakeAsync, + flush, + flushMicrotasks, + TestBed, + tick, +} from '@angular/core/testing'; + +import {LogoutPageComponent} from 'pages/logout-page/logout-page.component'; +import {AlertsService} from 'services/alerts.service'; +import {AuthService} from 'services/auth.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {LoaderService} from 'services/loader.service'; class MockWindowRef { constructor( - public location: string | null = null, public searchParams: string = '') {} + public location: string | null = null, + public searchParams: string = '' + ) {} get nativeWindow() { const that = this; @@ -38,7 +47,7 @@ class MockWindowRef { assign: (url: string) => { that.location = url; }, - } + }, }; } } @@ -61,7 +70,7 @@ class PendingPromise { } } -describe('Logout Page', function() { +describe('Logout Page', function () { let alertsService: jasmine.SpyObj; let authService: jasmine.SpyObj; let loaderService: jasmine.SpyObj; @@ -92,10 +101,10 @@ describe('Logout Page', function() { TestBed.configureTestingModule({ declarations: [LogoutPageComponent], providers: [ - { provide: AlertsService, useValue: alertsService }, - { provide: AuthService, useValue: authService }, - { provide: LoaderService, useValue: loaderService }, - { provide: WindowRef, useValue: windowRef } + {provide: AlertsService, useValue: alertsService}, + {provide: AuthService, useValue: authService}, + {provide: LoaderService, useValue: loaderService}, + {provide: WindowRef, useValue: windowRef}, ], }).compileComponents(); diff --git a/core/templates/pages/logout-page/logout-page.component.ts b/core/templates/pages/logout-page/logout-page.component.ts index 3dfe915d208e..4ce9c7148472 100644 --- a/core/templates/pages/logout-page/logout-page.component.ts +++ b/core/templates/pages/logout-page/logout-page.component.ts @@ -16,15 +16,15 @@ * @fileoverview Component for the logout page. */ -import { Component, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; import firebase from 'firebase/app'; -import { AlertsService } from 'services/alerts.service'; -import { AuthService } from 'services/auth.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { LoaderService } from 'services/loader.service'; -import { UtilsService } from 'services/utils.service'; +import {AlertsService} from 'services/alerts.service'; +import {AuthService} from 'services/auth.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {LoaderService} from 'services/loader.service'; +import {UtilsService} from 'services/utils.service'; @Component({ selector: 'logout-page', @@ -32,22 +32,29 @@ import { UtilsService } from 'services/utils.service'; }) export class LogoutPageComponent implements OnInit { constructor( - private alertsService: AlertsService, private authService: AuthService, - private loaderService: LoaderService, private windowRef: WindowRef, - private utilsService: UtilsService) {} + private alertsService: AlertsService, + private authService: AuthService, + private loaderService: LoaderService, + private windowRef: WindowRef, + private utilsService: UtilsService + ) {} ngOnInit(): void { this.loaderService.showLoadingScreen('I18N_LOGOUT_LOADING'); - this.authService.signOutAsync() - .then(() => this.redirect(), error => this.onSignOutError(error)); + this.authService.signOutAsync().then( + () => this.redirect(), + error => this.onSignOutError(error) + ); } private redirect(): void { - const searchParams = ( - new URLSearchParams(this.windowRef.nativeWindow.location.search)); + const searchParams = new URLSearchParams( + this.windowRef.nativeWindow.location.search + ); const redirectUrl = searchParams.get('redirect_url') ?? '/'; this.windowRef.nativeWindow.location.assign( - this.utilsService.getSafeReturnUrl(redirectUrl)); + this.utilsService.getSafeReturnUrl(redirectUrl) + ); } private onSignOutError(error: firebase.auth.Error): void { @@ -57,5 +64,9 @@ export class LogoutPageComponent implements OnInit { } } -angular.module('oppia').directive( - 'logoutPage', downgradeComponent({component: LogoutPageComponent})); +angular + .module('oppia') + .directive( + 'logoutPage', + downgradeComponent({component: LogoutPageComponent}) + ); diff --git a/core/templates/pages/logout-page/logout-page.module.ts b/core/templates/pages/logout-page/logout-page.module.ts index 58857342a285..d8f3d42930f6 100644 --- a/core/templates/pages/logout-page/logout-page.module.ts +++ b/core/templates/pages/logout-page/logout-page.module.ts @@ -16,27 +16,17 @@ * @fileoverview Module for the logout page. */ -import { NgModule } from '@angular/core'; +import {NgModule} from '@angular/core'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { LogoutPageComponent } from 'pages/logout-page/logout-page.component'; -import { LogoutPageRootComponent } from './logout-page-root.component'; -import { CommonModule } from '@angular/common'; -import { LogoutPageRoutingModule } from './logout-page-routing.module'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {LogoutPageComponent} from 'pages/logout-page/logout-page.component'; +import {LogoutPageRootComponent} from './logout-page-root.component'; +import {CommonModule} from '@angular/common'; +import {LogoutPageRoutingModule} from './logout-page-routing.module'; @NgModule({ - imports: [ - CommonModule, - SharedComponentsModule, - LogoutPageRoutingModule - ], - declarations: [ - LogoutPageComponent, - LogoutPageRootComponent - ], - entryComponents: [ - LogoutPageComponent, - LogoutPageRootComponent, - ] + imports: [CommonModule, SharedComponentsModule, LogoutPageRoutingModule], + declarations: [LogoutPageComponent, LogoutPageRootComponent], + entryComponents: [LogoutPageComponent, LogoutPageRootComponent], }) export class LogoutPageModule {} diff --git a/core/templates/pages/maintenance-page/maintenance-page.component.spec.ts b/core/templates/pages/maintenance-page/maintenance-page.component.spec.ts index 44c43c24c596..773a2fc60360 100644 --- a/core/templates/pages/maintenance-page/maintenance-page.component.spec.ts +++ b/core/templates/pages/maintenance-page/maintenance-page.component.spec.ts @@ -16,31 +16,27 @@ * @fileoverview Unit tests for maintenance page controller. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, async, TestBed } from '@angular/core/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, async, TestBed} from '@angular/core/testing'; -import { MaintenancePageComponent } from - 'pages/maintenance-page/maintenance-page.component'; -import { DocumentAttributeCustomizationService } from - 'services/contextual/document-attribute-customization.service'; +import {MaintenancePageComponent} from 'pages/maintenance-page/maintenance-page.component'; +import {DocumentAttributeCustomizationService} from 'services/contextual/document-attribute-customization.service'; let component: MaintenancePageComponent; let fixture: ComponentFixture; describe('Maintenance page', () => { - let documentAttributeCustomizationService: - DocumentAttributeCustomizationService; + let documentAttributeCustomizationService: DocumentAttributeCustomizationService; beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [MaintenancePageComponent], - providers: [ - DocumentAttributeCustomizationService - ], - schemas: [NO_ERRORS_SCHEMA] + providers: [DocumentAttributeCustomizationService], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - documentAttributeCustomizationService = - TestBed.get(DocumentAttributeCustomizationService); + documentAttributeCustomizationService = TestBed.get( + DocumentAttributeCustomizationService + ); })); beforeEach(() => { @@ -49,15 +45,17 @@ describe('Maintenance page', () => { fixture.detectChanges(); }); - it('should set document lang when $onInit is called', () => { - spyOn(documentAttributeCustomizationService, 'addAttribute').and - .callThrough(); + spyOn( + documentAttributeCustomizationService, + 'addAttribute' + ).and.callThrough(); component.ngOnInit(); expect(component.currentLang).toBe('en'); - expect(documentAttributeCustomizationService.addAttribute) - .toHaveBeenCalledWith('lang', 'en'); + expect( + documentAttributeCustomizationService.addAttribute + ).toHaveBeenCalledWith('lang', 'en'); }); it('should get static image url', () => { diff --git a/core/templates/pages/maintenance-page/maintenance-page.component.ts b/core/templates/pages/maintenance-page/maintenance-page.component.ts index 0f68837ee289..43fd03872006 100644 --- a/core/templates/pages/maintenance-page/maintenance-page.component.ts +++ b/core/templates/pages/maintenance-page/maintenance-page.component.ts @@ -16,31 +16,31 @@ * @fileoverview The component for the maintenance page. */ -import { Component, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; -import { DocumentAttributeCustomizationService } from - 'services/contextual/document-attribute-customization.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {DocumentAttributeCustomizationService} from 'services/contextual/document-attribute-customization.service'; @Component({ selector: 'oppia-maintenance-page', templateUrl: './maintenance-page.component.html', - styleUrls: [] + styleUrls: [], }) export class MaintenancePageComponent implements OnInit { currentLang: string = 'en'; constructor( - private documentAttributeCustomizationService: - DocumentAttributeCustomizationService, - private urlInterpolationService: UrlInterpolationService) {} + private documentAttributeCustomizationService: DocumentAttributeCustomizationService, + private urlInterpolationService: UrlInterpolationService + ) {} ngOnInit(): void { this.currentLang = 'en'; this.documentAttributeCustomizationService.addAttribute( - 'lang', this.currentLang); + 'lang', + this.currentLang + ); } getStaticImageUrl(imagePath: string): string { @@ -48,5 +48,9 @@ export class MaintenancePageComponent implements OnInit { } } -angular.module('oppia').directive('maintenancePage', downgradeComponent( - {component: MaintenancePageComponent})); +angular + .module('oppia') + .directive( + 'maintenancePage', + downgradeComponent({component: MaintenancePageComponent}) + ); diff --git a/core/templates/pages/maintenance-page/maintenance-page.import.ts b/core/templates/pages/maintenance-page/maintenance-page.import.ts index 947721e8051f..cbdf97927968 100644 --- a/core/templates/pages/maintenance-page/maintenance-page.import.ts +++ b/core/templates/pages/maintenance-page/maintenance-page.import.ts @@ -17,11 +17,11 @@ */ import 'pages/common-imports'; -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { AppConstants } from 'app.constants'; -import { enableProdMode } from '@angular/core'; -import { MaintenancePageModule } from './maintenance-page.module'; -import { LoggerService } from 'services/contextual/logger.service'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {AppConstants} from 'app.constants'; +import {enableProdMode} from '@angular/core'; +import {MaintenancePageModule} from './maintenance-page.module'; +import {LoggerService} from 'services/contextual/logger.service'; if (!AppConstants.DEV_MODE) { enableProdMode(); @@ -29,9 +29,9 @@ if (!AppConstants.DEV_MODE) { const loggerService = new LoggerService(); -platformBrowserDynamic().bootstrapModule(MaintenancePageModule).catch( - (err) => loggerService.error(err) -); +platformBrowserDynamic() + .bootstrapModule(MaintenancePageModule) + .catch(err => loggerService.error(err)); // This prevents angular pages to cause side effects to hybrid pages. // TODO(#13080): Remove window.name statement from import.ts files diff --git a/core/templates/pages/maintenance-page/maintenance-page.module.ts b/core/templates/pages/maintenance-page/maintenance-page.module.ts index 9ebb6b21a6ba..d79ae5705e6e 100644 --- a/core/templates/pages/maintenance-page/maintenance-page.module.ts +++ b/core/templates/pages/maintenance-page/maintenance-page.module.ts @@ -16,23 +16,24 @@ * @fileoverview Module for the maintenance page. */ -import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; -import { APP_INITIALIZER, NgModule } from '@angular/core'; -import { APP_BASE_HREF } from '@angular/common'; -import { BrowserModule, HAMMER_GESTURE_CONFIG } from '@angular/platform-browser'; -import { RouterModule } from '@angular/router'; +import {HttpClientModule, HTTP_INTERCEPTORS} from '@angular/common/http'; +import {APP_INITIALIZER, NgModule} from '@angular/core'; +import {APP_BASE_HREF} from '@angular/common'; +import {BrowserModule, HAMMER_GESTURE_CONFIG} from '@angular/platform-browser'; +import {RouterModule} from '@angular/router'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { MaintenancePageComponent } from - 'pages/maintenance-page/maintenance-page.component'; -import { RequestInterceptor } from 'services/request-interceptor.service'; -import { platformFeatureInitFactory, PlatformFeatureService } from - 'services/platform-feature.service'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { ToastrModule } from 'ngx-toastr'; -import { MyHammerConfig, toastrConfig } from 'pages/oppia-root/app.module'; -import { SmartRouterModule } from 'hybrid-router-module-provider'; -import { AppErrorHandlerProvider } from 'pages/oppia-root/app-error-handler'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {MaintenancePageComponent} from 'pages/maintenance-page/maintenance-page.component'; +import {RequestInterceptor} from 'services/request-interceptor.service'; +import { + platformFeatureInitFactory, + PlatformFeatureService, +} from 'services/platform-feature.service'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {ToastrModule} from 'ngx-toastr'; +import {MyHammerConfig, toastrConfig} from 'pages/oppia-root/app.module'; +import {SmartRouterModule} from 'hybrid-router-module-provider'; +import {AppErrorHandlerProvider} from 'pages/oppia-root/app-error-handler'; @NgModule({ imports: [ @@ -44,36 +45,32 @@ import { AppErrorHandlerProvider } from 'pages/oppia-root/app-error-handler'; SmartRouterModule, RouterModule.forRoot([]), SharedComponentsModule, - ToastrModule.forRoot(toastrConfig) - ], - declarations: [ - MaintenancePageComponent - ], - entryComponents: [ - MaintenancePageComponent + ToastrModule.forRoot(toastrConfig), ], + declarations: [MaintenancePageComponent], + entryComponents: [MaintenancePageComponent], providers: [ { provide: HTTP_INTERCEPTORS, useClass: RequestInterceptor, - multi: true + multi: true, }, { provide: APP_INITIALIZER, useFactory: platformFeatureInitFactory, deps: [PlatformFeatureService], - multi: true + multi: true, }, { provide: HAMMER_GESTURE_CONFIG, - useClass: MyHammerConfig + useClass: MyHammerConfig, }, AppErrorHandlerProvider, { provide: APP_BASE_HREF, - useValue: '/' - } + useValue: '/', + }, ], - bootstrap: [MaintenancePageComponent] + bootstrap: [MaintenancePageComponent], }) export class MaintenancePageModule {} diff --git a/core/templates/pages/mock-ajs.ts b/core/templates/pages/mock-ajs.ts index fe9801b54f09..4189d2df4710 100644 --- a/core/templates/pages/mock-ajs.ts +++ b/core/templates/pages/mock-ajs.ts @@ -28,7 +28,7 @@ // TODO(#13080): Remove the mock-ajs.ts file after the migration is complete. -import { VERSION } from '@angular/core'; +import {VERSION} from '@angular/core'; let mockAngular = { $$minErr: () => mockAngular, @@ -46,7 +46,7 @@ let mockAngular = { run: () => mockAngular, service: () => mockAngular, value: () => mockAngular, - version: VERSION + version: VERSION, }; // This throws "Property 'angular' does not exist on type 'Window & typeof diff --git a/core/templates/pages/moderator-page/moderator-auth.guard.spec.ts b/core/templates/pages/moderator-page/moderator-auth.guard.spec.ts index 119f87a56865..b4cf3451742c 100644 --- a/core/templates/pages/moderator-page/moderator-auth.guard.spec.ts +++ b/core/templates/pages/moderator-page/moderator-auth.guard.spec.ts @@ -16,14 +16,19 @@ * @fileoverview Tests for ModeratorAuthGuard */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TestBed } from '@angular/core/testing'; -import { ActivatedRouteSnapshot, Router, RouterStateSnapshot, NavigationExtras } from '@angular/router'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TestBed} from '@angular/core/testing'; +import { + ActivatedRouteSnapshot, + Router, + RouterStateSnapshot, + NavigationExtras, +} from '@angular/router'; -import { AppConstants } from 'app.constants'; -import { UserInfo } from 'domain/user/user-info.model'; -import { UserService } from 'services/user.service'; -import { ModeratorAuthGuard } from './moderator-auth.guard'; +import {AppConstants} from 'app.constants'; +import {UserInfo} from 'domain/user/user-info.model'; +import {UserService} from 'services/user.service'; +import {ModeratorAuthGuard} from './moderator-auth.guard'; class MockRouter { navigate(commands: string[], extras?: NavigationExtras): Promise { @@ -39,7 +44,7 @@ describe('ModeratorAuthGuard', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [UserService, { provide: Router, useClass: MockRouter }], + providers: [UserService, {provide: Router, useClass: MockRouter}], }).compileComponents(); guard = TestBed.inject(ModeratorAuthGuard); @@ -47,35 +52,39 @@ describe('ModeratorAuthGuard', () => { router = TestBed.inject(Router); }); - it('should redirect user to 401 page if user is not moderator', (done) => { + it('should redirect user to 401 page if user is not moderator', done => { const getUserInfoAsyncSpy = spyOn( - userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(UserInfo.createDefault()) - ); + userService, + 'getUserInfoAsync' + ).and.returnValue(Promise.resolve(UserInfo.createDefault())); const navigateSpy = spyOn(router, 'navigate').and.callThrough(); - guard.canActivate( - new ActivatedRouteSnapshot(), {} as RouterStateSnapshot).then( - (canActivate) => { + guard + .canActivate(new ActivatedRouteSnapshot(), {} as RouterStateSnapshot) + .then(canActivate => { expect(canActivate).toBeFalse(); expect(getUserInfoAsyncSpy).toHaveBeenCalledTimes(1); expect(navigateSpy).toHaveBeenCalledWith([ - `${AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ERROR.ROUTE}/401`]); + `${AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ERROR.ROUTE}/401`, + ]); done(); }); }); - it('should not redirect user to 401 page if user is moderator', (done) => { + it('should not redirect user to 401 page if user is moderator', done => { const getUserInfoAsyncSpy = spyOn( - userService, 'getUserInfoAsync').and.returnValue( - Promise.resolve(new UserInfo( - [], true, false, false, false, false, '', '', '', true)) + userService, + 'getUserInfoAsync' + ).and.returnValue( + Promise.resolve( + new UserInfo([], true, false, false, false, false, '', '', '', true) + ) ); const navigateSpy = spyOn(router, 'navigate').and.callThrough(); - guard.canActivate( - new ActivatedRouteSnapshot(), {} as RouterStateSnapshot).then( - (canActivate) => { + guard + .canActivate(new ActivatedRouteSnapshot(), {} as RouterStateSnapshot) + .then(canActivate => { expect(canActivate).toBeTrue(); expect(getUserInfoAsyncSpy).toHaveBeenCalledTimes(1); expect(navigateSpy).not.toHaveBeenCalled(); diff --git a/core/templates/pages/moderator-page/moderator-auth.guard.ts b/core/templates/pages/moderator-page/moderator-auth.guard.ts index 9415b0b14370..2d239b5dc454 100644 --- a/core/templates/pages/moderator-page/moderator-auth.guard.ts +++ b/core/templates/pages/moderator-page/moderator-auth.guard.ts @@ -17,8 +17,8 @@ * if the user is not a moderator. */ -import { Location } from '@angular/common'; -import { Injectable } from '@angular/core'; +import {Location} from '@angular/common'; +import {Injectable} from '@angular/core'; import { ActivatedRouteSnapshot, CanActivate, @@ -26,11 +26,11 @@ import { RouterStateSnapshot, } from '@angular/router'; -import { AppConstants } from 'app.constants'; -import { UserService } from 'services/user.service'; +import {AppConstants} from 'app.constants'; +import {UserService} from 'services/user.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ModeratorAuthGuard implements CanActivate { constructor( @@ -40,19 +40,21 @@ export class ModeratorAuthGuard implements CanActivate { ) {} async canActivate( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot ): Promise { const userInfo = await this.userService.getUserInfoAsync(); if (userInfo.isModerator()) { return true; } - this.router.navigate( - [`${AppConstants.PAGES_REGISTERED_WITH_FRONTEND - .ERROR.ROUTE}/401`]).then(() => { - this.location.replaceState(state.url); - }); + this.router + .navigate([ + `${AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ERROR.ROUTE}/401`, + ]) + .then(() => { + this.location.replaceState(state.url); + }); return false; } } diff --git a/core/templates/pages/moderator-page/moderator-page-root.component.spec.ts b/core/templates/pages/moderator-page/moderator-page-root.component.spec.ts index 442da97647fe..ccf0b537c858 100644 --- a/core/templates/pages/moderator-page/moderator-page-root.component.spec.ts +++ b/core/templates/pages/moderator-page/moderator-page-root.component.spec.ts @@ -16,14 +16,14 @@ * @fileoverview Unit tests for Moderator Page Root component. */ -import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { TranslateModule } from '@ngx-translate/core'; +import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {TranslateModule} from '@ngx-translate/core'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; -import { ModeratorPageRootComponent } from './moderator-page-root.component'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; +import {ModeratorPageRootComponent} from './moderator-page-root.component'; describe('AdminPageRootComponent', () => { let fixture: ComponentFixture; @@ -43,8 +43,10 @@ describe('AdminPageRootComponent', () => { it('should have the title and meta tags set', () => { expect(component.title).toEqual( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.MODERATOR.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.MODERATOR.TITLE + ); expect(component.meta).toEqual( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.MODERATOR.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.MODERATOR.META + ); }); }); diff --git a/core/templates/pages/moderator-page/moderator-page-root.component.ts b/core/templates/pages/moderator-page/moderator-page-root.component.ts index f2945e410ac2..b4704369eec5 100644 --- a/core/templates/pages/moderator-page/moderator-page-root.component.ts +++ b/core/templates/pages/moderator-page/moderator-page-root.component.ts @@ -16,9 +16,9 @@ * @fileoverview Moderator page root component. */ -import { Component } from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { BaseRootComponent, MetaTagData } from 'pages/base-root.component'; +import {Component} from '@angular/core'; +import {AppConstants} from 'app.constants'; +import {BaseRootComponent, MetaTagData} from 'pages/base-root.component'; @Component({ selector: 'oppia-moderator-page-root', @@ -26,7 +26,6 @@ import { BaseRootComponent, MetaTagData } from 'pages/base-root.component'; }) export class ModeratorPageRootComponent extends BaseRootComponent { title: string = AppConstants.PAGES_REGISTERED_WITH_FRONTEND.MODERATOR.TITLE; - meta: MetaTagData[] = - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ADMIN.META as - unknown as Readonly[]; + meta: MetaTagData[] = AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ADMIN + .META as unknown as Readonly[]; } diff --git a/core/templates/pages/moderator-page/moderator-page.component.spec.ts b/core/templates/pages/moderator-page/moderator-page.component.spec.ts index 6440d809cf40..cdc81b11593b 100644 --- a/core/templates/pages/moderator-page/moderator-page.component.spec.ts +++ b/core/templates/pages/moderator-page/moderator-page.component.spec.ts @@ -16,19 +16,29 @@ * @fileoverview Unit tests for Moderator Page Component. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, waitForAsync } - from '@angular/core/testing'; -import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'; -import { ThreadMessage, ThreadMessageBackendDict } - from 'domain/feedback_message/ThreadMessage.model'; -import { AlertsService } from 'services/alerts.service'; -import { DateTimeFormatService } from 'services/date-time-format.service'; -import { LoaderService } from 'services/loader.service'; -import { ModeratorPageComponent } from './moderator-page.component'; -import { ActivityIdTypeDict, FeaturedActivityResponse, - ModeratorPageBackendApiService, RecentCommitResponse, RecentFeedbackMessages } - from './services/moderator-page-backend-api.service'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import {NgbNavModule} from '@ng-bootstrap/ng-bootstrap'; +import { + ThreadMessage, + ThreadMessageBackendDict, +} from 'domain/feedback_message/ThreadMessage.model'; +import {AlertsService} from 'services/alerts.service'; +import {DateTimeFormatService} from 'services/date-time-format.service'; +import {LoaderService} from 'services/loader.service'; +import {ModeratorPageComponent} from './moderator-page.component'; +import { + ActivityIdTypeDict, + FeaturedActivityResponse, + ModeratorPageBackendApiService, + RecentCommitResponse, + RecentFeedbackMessages, +} from './services/moderator-page-backend-api.service'; describe('Moderator Page Component', () => { let fixture: ComponentFixture; @@ -44,103 +54,97 @@ describe('Moderator Page Component', () => { message_id: 12312, text: 'test_txt', updated_status: 'stastu', - updated_subject: 'asdf' + updated_subject: 'asdf', }; - let message: ThreadMessage = ThreadMessage - .createFromBackendDict(threadMessageBackendDict); + let message: ThreadMessage = ThreadMessage.createFromBackendDict( + threadMessageBackendDict + ); let recentCommits: RecentCommitResponse = { results: [], cursor: 'str1', more: true, - exp_ids_to_exp_data: [{ - category: 'test_category', - title: 'test_title' - }] + exp_ids_to_exp_data: [ + { + category: 'test_category', + title: 'test_title', + }, + ], }; let recentFeedbackMessages: RecentFeedbackMessages = { results: [threadMessageBackendDict], cursor: 'test', - more: true + more: true, }; let activityResponse: FeaturedActivityResponse = { - featured_activity_references: [] + featured_activity_references: [], }; class MockModeratorPageBackendApiService { getRecentCommitsAsync() { return { - then: ( - successCallback: (response: RecentCommitResponse) => void - ) => { + then: (successCallback: (response: RecentCommitResponse) => void) => { successCallback(recentCommits); - } + }, }; } getRecentFeedbackMessagesAsync() { return { - then: ( - successCallback: (response: RecentFeedbackMessages) => void - ) => { + then: (successCallback: (response: RecentFeedbackMessages) => void) => { successCallback(recentFeedbackMessages); - } + }, }; } getFeaturedActivityReferencesAsync() { return { then: ( - successCallback: (response: FeaturedActivityResponse) => void + successCallback: (response: FeaturedActivityResponse) => void ) => { successCallback(activityResponse); - } + }, }; } saveFeaturedActivityReferencesAsync(references: ActivityIdTypeDict[]) { return { - then: ( - successCallback: () => void - ) => { + then: (successCallback: () => void) => { successCallback(); - } + }, }; } } beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - NgbNavModule - ], - declarations: [ - ModeratorPageComponent - ], + imports: [NgbNavModule], + declarations: [ModeratorPageComponent], providers: [ { provide: ModeratorPageBackendApiService, - useClass: MockModeratorPageBackendApiService - } + useClass: MockModeratorPageBackendApiService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(ModeratorPageComponent); componentInstance = fixture.componentInstance; - loaderService = (TestBed.inject(LoaderService)) as - jasmine.SpyObj; - datetimeFormatService = ( - TestBed.inject(DateTimeFormatService)) as - jasmine.SpyObj; - alertsService = ( - TestBed.inject(AlertsService)) as - jasmine.SpyObj; + loaderService = TestBed.inject( + LoaderService + ) as jasmine.SpyObj; + datetimeFormatService = TestBed.inject( + DateTimeFormatService + ) as jasmine.SpyObj; + alertsService = TestBed.inject( + AlertsService + ) as jasmine.SpyObj; }); it('should create', () => { @@ -152,27 +156,33 @@ describe('Moderator Page Component', () => { spyOn(loaderService, 'hideLoadingScreen'); componentInstance.explorationData = []; componentInstance.ngOnInit(); - expect(componentInstance.explorationData) - .toEqual(recentCommits.exp_ids_to_exp_data); + expect(componentInstance.explorationData).toEqual( + recentCommits.exp_ids_to_exp_data + ); expect(loaderService.showLoadingScreen).toHaveBeenCalled(); expect(componentInstance.allCommits).toEqual(recentCommits.results); expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); - expect(componentInstance.allFeedbackMessages) - .toEqual(recentFeedbackMessages.results.map( - d => ThreadMessage.createFromBackendDict(d)) - ); + expect(componentInstance.allFeedbackMessages).toEqual( + recentFeedbackMessages.results.map(d => + ThreadMessage.createFromBackendDict(d) + ) + ); - expect(componentInstance.displayedFeaturedActivityReferences) - .toEqual(activityResponse.featured_activity_references); + expect(componentInstance.displayedFeaturedActivityReferences).toEqual( + activityResponse.featured_activity_references + ); expect(componentInstance.lastSavedFeaturedActivityReferences).toEqual( - componentInstance.displayedFeaturedActivityReferences); + componentInstance.displayedFeaturedActivityReferences + ); })); it('should get date time as string', () => { let testDatetime: string = 'test_datetime'; - spyOn(datetimeFormatService, 'getLocaleAbbreviatedDatetimeString') - .and.returnValue(testDatetime); + spyOn( + datetimeFormatService, + 'getLocaleAbbreviatedDatetimeString' + ).and.returnValue(testDatetime); expect(componentInstance.getDatetimeAsString(100)).toEqual(testDatetime); }); @@ -183,32 +193,38 @@ describe('Moderator Page Component', () => { }); it('should get exploration create url', () => { - expect(componentInstance.getExplorationCreateUrl('test')) - .toEqual('/create/test'); + expect(componentInstance.getExplorationCreateUrl('test')).toEqual( + '/create/test' + ); }); it('should get activity create url', () => { let reference: ActivityIdTypeDict = { id: 'test_id', - type: 'exploration' + type: 'exploration', }; - expect(componentInstance.getActivityCreateUrl(reference)) - .toEqual('/create/' + reference.id); + expect(componentInstance.getActivityCreateUrl(reference)).toEqual( + '/create/' + reference.id + ); reference.type = 'not_exploration'; - expect(componentInstance.getActivityCreateUrl(reference)) - .toEqual('/create_collection/' + reference.id); + expect(componentInstance.getActivityCreateUrl(reference)).toEqual( + '/create_collection/' + reference.id + ); }); it('should tell if save featured activies button is disabled', () => { - componentInstance.displayedFeaturedActivityReferences = componentInstance - .lastSavedFeaturedActivityReferences; - expect(componentInstance.isSaveFeaturedActivitiesButtonDisabled()) - .toBeTrue(); + componentInstance.displayedFeaturedActivityReferences = + componentInstance.lastSavedFeaturedActivityReferences; + expect( + componentInstance.isSaveFeaturedActivitiesButtonDisabled() + ).toBeTrue(); componentInstance.displayedFeaturedActivityReferences = []; componentInstance.lastSavedFeaturedActivityReferences = [ - { id: 'not', type: 'equal' }]; - expect(componentInstance.isSaveFeaturedActivitiesButtonDisabled()) - .toBeFalse(); + {id: 'not', type: 'equal'}, + ]; + expect( + componentInstance.isSaveFeaturedActivitiesButtonDisabled() + ).toBeFalse(); }); it('should save featured activity references', () => { @@ -220,18 +236,22 @@ describe('Moderator Page Component', () => { }); it('should get schema', () => { - expect(componentInstance.getSchema()) - .toEqual(componentInstance.FEATURED_ACTIVITY_REFERENCES_SCHEMA); + expect(componentInstance.getSchema()).toEqual( + componentInstance.FEATURED_ACTIVITY_REFERENCES_SCHEMA + ); }); it('should update displayed featured activity references', () => { - let newValue: ActivityIdTypeDict[] = [{ - id: 'test_id', - type: 'exploration' - }]; + let newValue: ActivityIdTypeDict[] = [ + { + id: 'test_id', + type: 'exploration', + }, + ]; componentInstance.displayedFeaturedActivityReferences = []; componentInstance.updateDisplayedFeaturedActivityReferences(newValue); - expect(componentInstance.displayedFeaturedActivityReferences) - .toEqual(newValue); + expect(componentInstance.displayedFeaturedActivityReferences).toEqual( + newValue + ); }); }); diff --git a/core/templates/pages/moderator-page/moderator-page.component.ts b/core/templates/pages/moderator-page/moderator-page.component.ts index ca1bd2b89080..a5490d00ee1b 100644 --- a/core/templates/pages/moderator-page/moderator-page.component.ts +++ b/core/templates/pages/moderator-page/moderator-page.component.ts @@ -16,21 +16,24 @@ * @fileoverview Component for the Oppia moderator page. */ -import { ChangeDetectorRef, Component } from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { ThreadMessage } from 'domain/feedback_message/ThreadMessage.model'; +import {ChangeDetectorRef, Component} from '@angular/core'; +import {AppConstants} from 'app.constants'; +import {ThreadMessage} from 'domain/feedback_message/ThreadMessage.model'; import isEqual from 'lodash/isEqual'; -import { AlertsService } from 'services/alerts.service'; -import { DateTimeFormatService } from 'services/date-time-format.service'; -import { LoaderService } from 'services/loader.service'; -import { Schema } from 'services/schema-default-value.service'; -import { ActivityIdTypeDict, CommitMessage, - ExplorationDict, ModeratorPageBackendApiService } - from './services/moderator-page-backend-api.service'; +import {AlertsService} from 'services/alerts.service'; +import {DateTimeFormatService} from 'services/date-time-format.service'; +import {LoaderService} from 'services/loader.service'; +import {Schema} from 'services/schema-default-value.service'; +import { + ActivityIdTypeDict, + CommitMessage, + ExplorationDict, + ModeratorPageBackendApiService, +} from './services/moderator-page-backend-api.service'; @Component({ selector: 'oppia-moderator-page', - templateUrl: './moderator-page.component.html' + templateUrl: './moderator-page.component.html', }) export class ModeratorPageComponent { allCommits: CommitMessage[] = []; @@ -45,20 +48,25 @@ export class ModeratorPageComponent { type: 'list', items: { type: 'dict', - properties: [{ - name: 'type', - schema: { - type: 'unicode', - choices: [AppConstants.ENTITY_TYPE.EXPLORATION, - AppConstants.ENTITY_TYPE.COLLECTION] - } - }, { - name: 'id', - schema: { - type: 'unicode' - } - }] - } + properties: [ + { + name: 'type', + schema: { + type: 'unicode', + choices: [ + AppConstants.ENTITY_TYPE.EXPLORATION, + AppConstants.ENTITY_TYPE.COLLECTION, + ], + }, + }, + { + name: 'id', + schema: { + type: 'unicode', + }, + }, + ], + }, }; constructor( @@ -70,7 +78,8 @@ export class ModeratorPageComponent { ) {} updateDisplayedFeaturedActivityReferences( - newValue: ActivityIdTypeDict[]): void { + newValue: ActivityIdTypeDict[] + ): void { if (this.displayedFeaturedActivityReferences !== newValue) { this.displayedFeaturedActivityReferences = newValue; this.changeDetectorRef.detectChanges(); @@ -79,39 +88,45 @@ export class ModeratorPageComponent { ngOnInit(): void { this.loaderService.showLoadingScreen('Loading'); - this.moderatorPageBackendApiService.getRecentCommitsAsync() - .then((response) => { + this.moderatorPageBackendApiService + .getRecentCommitsAsync() + .then(response => { // Update the explorationData object with information about newly- // discovered explorations. let explorationIdsToExplorationData = response.exp_ids_to_exp_data; for (let expId in explorationIdsToExplorationData) { if (!this.explorationData.hasOwnProperty(expId)) { - this.explorationData[expId] = ( - explorationIdsToExplorationData[expId]); + this.explorationData[expId] = + explorationIdsToExplorationData[expId]; } } this.allCommits = response.results; this.loaderService.hideLoadingScreen(); }); - this.moderatorPageBackendApiService.getRecentFeedbackMessagesAsync() - .then((response) => { - this.allFeedbackMessages = response.results.map( - d => ThreadMessage.createFromBackendDict(d)); + this.moderatorPageBackendApiService + .getRecentFeedbackMessagesAsync() + .then(response => { + this.allFeedbackMessages = response.results.map(d => + ThreadMessage.createFromBackendDict(d) + ); }); - this.moderatorPageBackendApiService.getFeaturedActivityReferencesAsync() - .then((response) => { - this.displayedFeaturedActivityReferences = response - .featured_activity_references; - this.lastSavedFeaturedActivityReferences = - [...this.displayedFeaturedActivityReferences]; + this.moderatorPageBackendApiService + .getFeaturedActivityReferencesAsync() + .then(response => { + this.displayedFeaturedActivityReferences = + response.featured_activity_references; + this.lastSavedFeaturedActivityReferences = [ + ...this.displayedFeaturedActivityReferences, + ]; }); } getDatetimeAsString(millisSinceEpoch: number): string { - return this.dateTimeFormatService - .getLocaleAbbreviatedDatetimeString(millisSinceEpoch); + return this.dateTimeFormatService.getLocaleAbbreviatedDatetimeString( + millisSinceEpoch + ); } isMessageFromExploration(message: ThreadMessage): boolean { @@ -123,24 +138,26 @@ export class ModeratorPageComponent { } getActivityCreateUrl(reference: ActivityIdTypeDict): string { - let path: string = ( - reference.type === AppConstants.ENTITY_TYPE.EXPLORATION ? - '/create' : - '/create_collection'); + let path: string = + reference.type === AppConstants.ENTITY_TYPE.EXPLORATION + ? '/create' + : '/create_collection'; return path + '/' + reference.id; } isSaveFeaturedActivitiesButtonDisabled(): boolean { return isEqual( this.displayedFeaturedActivityReferences, - this.lastSavedFeaturedActivityReferences); + this.lastSavedFeaturedActivityReferences + ); } saveFeaturedActivityReferences(): void { this.alertsService.clearWarnings(); - let activityReferencesToSave = - [...this.displayedFeaturedActivityReferences]; + let activityReferencesToSave = [ + ...this.displayedFeaturedActivityReferences, + ]; this.moderatorPageBackendApiService .saveFeaturedActivityReferencesAsync(activityReferencesToSave) diff --git a/core/templates/pages/moderator-page/moderator-page.import.ts b/core/templates/pages/moderator-page/moderator-page.import.ts index 0ff0996a6551..c338d0f170bb 100644 --- a/core/templates/pages/moderator-page/moderator-page.import.ts +++ b/core/templates/pages/moderator-page/moderator-page.import.ts @@ -20,8 +20,13 @@ import 'core-js/es7/reflect'; import 'zone.js'; angular.module('oppia', [ - require('angular-cookies'), 'ngAnimate', 'ngMaterial', - 'ngSanitize', 'ngTouch', 'pascalprecht.translate', 'ui.bootstrap' + require('angular-cookies'), + 'ngAnimate', + 'ngMaterial', + 'ngSanitize', + 'ngTouch', + 'pascalprecht.translate', + 'ui.bootstrap', ]); require('Polyfills.ts'); diff --git a/core/templates/pages/moderator-page/moderator-page.module.ts b/core/templates/pages/moderator-page/moderator-page.module.ts index 806982518d22..df39e736dbd2 100644 --- a/core/templates/pages/moderator-page/moderator-page.module.ts +++ b/core/templates/pages/moderator-page/moderator-page.module.ts @@ -16,16 +16,16 @@ * @fileoverview Module for the moderator page. */ -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; -import { CommonModule } from '@angular/common'; -import { ToastrModule } from 'ngx-toastr'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'; -import { ModeratorPageComponent } from './moderator-page.component'; -import { toastrConfig } from 'pages/oppia-root/app.module'; -import { ModeratorPageRootComponent } from './moderator-page-root.component'; -import { ModeratorAuthGuard } from './moderator-auth.guard'; +import {NgModule} from '@angular/core'; +import {RouterModule} from '@angular/router'; +import {CommonModule} from '@angular/common'; +import {ToastrModule} from 'ngx-toastr'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {NgbNavModule} from '@ng-bootstrap/ng-bootstrap'; +import {ModeratorPageComponent} from './moderator-page.component'; +import {toastrConfig} from 'pages/oppia-root/app.module'; +import {ModeratorPageRootComponent} from './moderator-page-root.component'; +import {ModeratorAuthGuard} from './moderator-auth.guard'; @NgModule({ imports: [ @@ -37,16 +37,11 @@ import { ModeratorAuthGuard } from './moderator-auth.guard'; { path: '', component: ModeratorPageRootComponent, - canActivate: [ModeratorAuthGuard] - } - ]) - ], - declarations: [ - ModeratorPageComponent, - ModeratorPageRootComponent - ], - entryComponents: [ - ModeratorPageComponent, + canActivate: [ModeratorAuthGuard], + }, + ]), ], + declarations: [ModeratorPageComponent, ModeratorPageRootComponent], + entryComponents: [ModeratorPageComponent], }) export class ModeratorPageModule {} diff --git a/core/templates/pages/moderator-page/services/moderator-page-backend-api.service.spec.ts b/core/templates/pages/moderator-page/services/moderator-page-backend-api.service.spec.ts index 58269cad5440..524a8b9846c1 100644 --- a/core/templates/pages/moderator-page/services/moderator-page-backend-api.service.spec.ts +++ b/core/templates/pages/moderator-page/services/moderator-page-backend-api.service.spec.ts @@ -17,13 +17,23 @@ * the Oppia moderator page. */ -import { HttpClientTestingModule, HttpTestingController } - from '@angular/common/http/testing'; -import { fakeAsync, flushMicrotasks, TestBed, waitForAsync } - from '@angular/core/testing'; -import { ActivityIdTypeDict, FeaturedActivityResponse, - ModeratorPageBackendApiService, RecentCommitResponse, - RecentFeedbackMessages } from './moderator-page-backend-api.service'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import { + fakeAsync, + flushMicrotasks, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { + ActivityIdTypeDict, + FeaturedActivityResponse, + ModeratorPageBackendApiService, + RecentCommitResponse, + RecentFeedbackMessages, +} from './moderator-page-backend-api.service'; describe('Moderator Page Backend Api Service', () => { let moderatorPageBackendApiService: ModeratorPageBackendApiService; @@ -31,26 +41,26 @@ describe('Moderator Page Backend Api Service', () => { let successHandler = jasmine.createSpy('success'); let failHandler = jasmine.createSpy('fail'); - beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ] + imports: [HttpClientTestingModule], }); })); beforeEach(() => { - moderatorPageBackendApiService = - TestBed.inject(ModeratorPageBackendApiService); + moderatorPageBackendApiService = TestBed.inject( + ModeratorPageBackendApiService + ); httpTestingController = TestBed.inject(HttpTestingController); }); it('should save featured activity references', fakeAsync(() => { - let activityReferences: ActivityIdTypeDict[] = [{ - id: 'test_id1', - type: 'type1' - }]; + let activityReferences: ActivityIdTypeDict[] = [ + { + id: 'test_id1', + type: 'type1', + }, + ]; moderatorPageBackendApiService .saveFeaturedActivityReferencesAsync(activityReferences) @@ -69,14 +79,16 @@ describe('Moderator Page Backend Api Service', () => { results: [], cursor: 'str1', more: true, - exp_ids_to_exp_data: [] + exp_ids_to_exp_data: [], }; moderatorPageBackendApiService - .getRecentCommitsAsync().then(successHandler, failHandler); + .getRecentCommitsAsync() + .then(successHandler, failHandler); let req = httpTestingController.expectOne( '/recentcommitshandler/recent_commits' + - '?query_type=all_non_private_commits'); + '?query_type=all_non_private_commits' + ); req.flush(expectedResponseData); flushMicrotasks(); @@ -88,10 +100,11 @@ describe('Moderator Page Backend Api Service', () => { let expectedResponseData: RecentFeedbackMessages = { results: [], cursor: 'str1', - more: true + more: true, }; moderatorPageBackendApiService - .getRecentFeedbackMessagesAsync().then(successHandler, failHandler); + .getRecentFeedbackMessagesAsync() + .then(successHandler, failHandler); let req = httpTestingController.expectOne('/recent_feedback_messages'); req.flush(expectedResponseData); @@ -103,10 +116,11 @@ describe('Moderator Page Backend Api Service', () => { it('should get recent featured activity references', fakeAsync(() => { let expectedResponseData: FeaturedActivityResponse = { - featured_activity_references: [] + featured_activity_references: [], }; moderatorPageBackendApiService - .getFeaturedActivityReferencesAsync().then(successHandler, failHandler); + .getFeaturedActivityReferencesAsync() + .then(successHandler, failHandler); let req = httpTestingController.expectOne('/moderatorhandler/featured'); req.flush(expectedResponseData); diff --git a/core/templates/pages/moderator-page/services/moderator-page-backend-api.service.ts b/core/templates/pages/moderator-page/services/moderator-page-backend-api.service.ts index e7fafd2f684d..2f5103f86236 100644 --- a/core/templates/pages/moderator-page/services/moderator-page-backend-api.service.ts +++ b/core/templates/pages/moderator-page/services/moderator-page-backend-api.service.ts @@ -16,19 +16,18 @@ * @fileoverview Backend Api Service for the Oppia moderator page. */ -import { HttpClient, HttpParams } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { ThreadMessageBackendDict } - from 'domain/feedback_message/ThreadMessage.model'; +import {HttpClient, HttpParams} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {ThreadMessageBackendDict} from 'domain/feedback_message/ThreadMessage.model'; export interface CommitMessage { - 'commit_message': string; - 'commit_type': string; - 'exploration_id': string; - 'last_updated': number; - 'post_commit_community_owned': boolean; - 'post_commit_is_private': boolean; - 'post_commit_status': string; + commit_message: string; + commit_type: string; + exploration_id: string; + last_updated: number; + post_commit_community_owned: boolean; + post_commit_is_private: boolean; + post_commit_status: string; username: string; version: number; } @@ -44,52 +43,63 @@ export interface ActivityIdTypeDict { } export interface RecentCommitResponse { - 'results': CommitMessage[]; - 'cursor': string; - 'more': boolean; - 'exp_ids_to_exp_data': ExplorationDict[]; + results: CommitMessage[]; + cursor: string; + more: boolean; + exp_ids_to_exp_data: ExplorationDict[]; } export interface RecentFeedbackMessages { - 'results': ThreadMessageBackendDict[]; - 'cursor': string; - 'more': boolean; + results: ThreadMessageBackendDict[]; + cursor: string; + more: boolean; } export interface FeaturedActivityResponse { - 'featured_activity_references': ActivityIdTypeDict[]; + featured_activity_references: ActivityIdTypeDict[]; } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ModeratorPageBackendApiService { - constructor( - private httpClient: HttpClient - ) {} + constructor(private httpClient: HttpClient) {} async saveFeaturedActivityReferencesAsync( - activityReferencesToSave: ActivityIdTypeDict[]): Promise { - return this.httpClient.post('/moderatorhandler/featured', { - featured_activity_reference_dicts: activityReferencesToSave - }, {}).toPromise(); + activityReferencesToSave: ActivityIdTypeDict[] + ): Promise { + return this.httpClient + .post( + '/moderatorhandler/featured', + { + featured_activity_reference_dicts: activityReferencesToSave, + }, + {} + ) + .toPromise(); } async getRecentCommitsAsync(): Promise { - let options = {params: new HttpParams() - .set('query_type', 'all_non_private_commits')}; - return this.httpClient.get( - '/recentcommitshandler/recent_commits', options).toPromise(); + let options = { + params: new HttpParams().set('query_type', 'all_non_private_commits'), + }; + return this.httpClient + .get( + '/recentcommitshandler/recent_commits', + options + ) + .toPromise(); } async getRecentFeedbackMessagesAsync(): Promise { return this.httpClient - .get('/recent_feedback_messages').toPromise(); + .get('/recent_feedback_messages') + .toPromise(); } - async getFeaturedActivityReferencesAsync(): - Promise { + async getFeaturedActivityReferencesAsync(): Promise { return this.httpClient - .get('/moderatorhandler/featured').toPromise(); + .get('/moderatorhandler/featured') + .toPromise(); } } diff --git a/core/templates/pages/oppia-root/app-error-handler.ts b/core/templates/pages/oppia-root/app-error-handler.ts index 7f80e2974ae4..1f5dbd7b8989 100644 --- a/core/templates/pages/oppia-root/app-error-handler.ts +++ b/core/templates/pages/oppia-root/app-error-handler.ts @@ -21,9 +21,9 @@ // use these here because we are explicitly specifying the dependencies of a // provider, which cannot be done using a injectable (service). // eslint-disable-next-line oppia/disallow-httpclient -import { HttpClient } from '@angular/common/http'; -import { ErrorHandler } from '@angular/core'; -import { LoggerService } from 'services/contextual/logger.service'; +import {HttpClient} from '@angular/common/http'; +import {ErrorHandler} from '@angular/core'; +import {LoggerService} from 'services/contextual/logger.service'; import firebase from 'firebase/app'; export class AppErrorHandler extends ErrorHandler { @@ -48,12 +48,12 @@ export class AppErrorHandler extends ErrorHandler { 'auth/user-not-found', ]; - private readonly UNHANDLED_REJECTION_STATUS_CODE_REGEX = ( - /Possibly unhandled rejection: {.*"status":-1/); + private readonly UNHANDLED_REJECTION_STATUS_CODE_REGEX = + /Possibly unhandled rejection: {.*"status":-1/; MIN_TIME_BETWEEN_ERRORS_MSEC = 5000; - timeOfLastPostedError: number = ( - Date.now() - this.MIN_TIME_BETWEEN_ERRORS_MSEC); + timeOfLastPostedError: number = + Date.now() - this.MIN_TIME_BETWEEN_ERRORS_MSEC; constructor( private http: HttpClient, @@ -63,19 +63,19 @@ export class AppErrorHandler extends ErrorHandler { } handleError(error: Error): void { - if (AppErrorHandler.EXPECTED_ERROR_CODES.includes( - // The firebase.auth.Error is not compatible with javascript's Error type. - // That's why explicit type conversion is used here. - (error as unknown as firebase.auth.Error).code) + if ( + AppErrorHandler.EXPECTED_ERROR_CODES.includes( + // The firebase.auth.Error is not compatible with javascript's Error type. + // That's why explicit type conversion is used here. + (error as unknown as firebase.auth.Error).code + ) ) { return; } // Suppress unhandled rejection errors status code -1, because -1 is the // status code for aborted requests. - if (this.UNHANDLED_REJECTION_STATUS_CODE_REGEX.test( - error.message - )) { + if (this.UNHANDLED_REJECTION_STATUS_CODE_REGEX.test(error.message)) { return; } @@ -101,19 +101,25 @@ export class AppErrorHandler extends ErrorHandler { '', error.message, '', - ' at URL: ' + window.location.href + ' at URL: ' + window.location.href, ].join('\n'); let timeDifference = Date.now() - this.timeOfLastPostedError; // To prevent an overdose of errors, throttle to at most 1 error // every MIN_TIME_BETWEEN_ERRORS_MSEC. if (timeDifference > this.MIN_TIME_BETWEEN_ERRORS_MSEC) { - this.http.post('/frontend_errors', { - error: messageAndStackTrace - }).toPromise().then(() => { - this.timeOfLastPostedError = Date.now(); - }, () => { - this.loggerService.warn('Error logging failed.'); - }); + this.http + .post('/frontend_errors', { + error: messageAndStackTrace, + }) + .toPromise() + .then( + () => { + this.timeOfLastPostedError = Date.now(); + }, + () => { + this.loggerService.warn('Error logging failed.'); + } + ); } this.loggerService.error(error.message); @@ -125,5 +131,5 @@ export class AppErrorHandler extends ErrorHandler { export const AppErrorHandlerProvider = { provide: ErrorHandler, useClass: AppErrorHandler, - deps: [HttpClient, LoggerService] + deps: [HttpClient, LoggerService], }; diff --git a/core/templates/pages/oppia-root/app.module.ts b/core/templates/pages/oppia-root/app.module.ts index cb36b19cd52e..680d582c9f4e 100644 --- a/core/templates/pages/oppia-root/app.module.ts +++ b/core/templates/pages/oppia-root/app.module.ts @@ -16,34 +16,43 @@ * @fileoverview Module for the about page. */ -import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; -import { APP_INITIALIZER, NgModule } from '@angular/core'; - +import {HttpClientModule, HTTP_INTERCEPTORS} from '@angular/common/http'; +import {APP_INITIALIZER, NgModule} from '@angular/core'; // Modules. -import { BrowserModule, HammerGestureConfig, HAMMER_GESTURE_CONFIG } from '@angular/platform-browser'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { AppRoutingModule } from './routing/app.routing.module'; +import { + BrowserModule, + HammerGestureConfig, + HAMMER_GESTURE_CONFIG, +} from '@angular/platform-browser'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {AppRoutingModule} from './routing/app.routing.module'; // Components. -import { OppiaRootComponent } from './oppia-root.component'; +import {OppiaRootComponent} from './oppia-root.component'; // Miscellaneous. -import { platformFeatureInitFactory, PlatformFeatureService } from 'services/platform-feature.service'; -import { RequestInterceptor } from 'services/request-interceptor.service'; -import { CookieModule } from 'ngx-cookie'; -import { ToastrModule } from 'ngx-toastr'; -import { AngularFireAuth, AngularFireAuthModule, USE_EMULATOR } from '@angular/fire/auth'; -import { AngularFireModule } from '@angular/fire'; -import { AuthService } from 'services/auth.service'; +import { + platformFeatureInitFactory, + PlatformFeatureService, +} from 'services/platform-feature.service'; +import {RequestInterceptor} from 'services/request-interceptor.service'; +import {CookieModule} from 'ngx-cookie'; +import {ToastrModule} from 'ngx-toastr'; +import { + AngularFireAuth, + AngularFireAuthModule, + USE_EMULATOR, +} from '@angular/fire/auth'; +import {AngularFireModule} from '@angular/fire'; +import {AuthService} from 'services/auth.service'; // This throws "TS2307". We need to // suppress this error because hammer come from hammerjs // dependency. We can't import it directly. // @ts-ignore import * as hammer from 'hammerjs'; -import { AppErrorHandlerProvider } from './app-error-handler'; -import { I18nModule } from 'i18n/i18n.module'; - +import {AppErrorHandlerProvider} from './app-error-handler'; +import {I18nModule} from 'i18n/i18n.module'; // Config for ToastrModule (helps in flashing messages and alerts). export const toastrConfig = { @@ -52,26 +61,26 @@ export const toastrConfig = { error: 'toast-error', info: 'toast-info', success: 'toast-success', - warning: 'toast-warning' + warning: 'toast-warning', }, positionClass: 'toast-bottom-right', messageClass: 'toast-message e2e-test-toast-message', progressBar: false, tapToDismiss: true, - titleClass: 'toast-title' + titleClass: 'toast-title', }; export class MyHammerConfig extends HammerGestureConfig { overrides = { - swipe: { direction: hammer.DIRECTION_HORIZONTAL }, - pinch: { enable: false }, - rotate: { enable: false }, + swipe: {direction: hammer.DIRECTION_HORIZONTAL}, + pinch: {enable: false}, + rotate: {enable: false}, }; options = { cssProps: { - userSelect: true - } + userSelect: true, + }, }; } @@ -85,37 +94,33 @@ export class MyHammerConfig extends HammerGestureConfig { AngularFireAuthModule, AppRoutingModule, I18nModule, - ToastrModule.forRoot(toastrConfig) - ], - declarations: [ - OppiaRootComponent, - ], - entryComponents: [ - OppiaRootComponent, + ToastrModule.forRoot(toastrConfig), ], + declarations: [OppiaRootComponent], + entryComponents: [OppiaRootComponent], providers: [ AngularFireAuth, { provide: HTTP_INTERCEPTORS, useClass: RequestInterceptor, - multi: true + multi: true, }, { provide: APP_INITIALIZER, useFactory: platformFeatureInitFactory, deps: [PlatformFeatureService], - multi: true + multi: true, }, { provide: USE_EMULATOR, - useValue: AuthService.firebaseEmulatorConfig + useValue: AuthService.firebaseEmulatorConfig, }, AppErrorHandlerProvider, { provide: HAMMER_GESTURE_CONFIG, - useClass: MyHammerConfig - } + useClass: MyHammerConfig, + }, ], - bootstrap: [OppiaRootComponent] + bootstrap: [OppiaRootComponent], }) export class AppModule {} diff --git a/core/templates/pages/oppia-root/index.ts b/core/templates/pages/oppia-root/index.ts index 666f21c6a566..052994f2e96c 100644 --- a/core/templates/pages/oppia-root/index.ts +++ b/core/templates/pages/oppia-root/index.ts @@ -17,20 +17,20 @@ */ import 'pages/common-imports'; -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { AppConstants } from 'app.constants'; -import { enableProdMode } from '@angular/core'; -import { AppModule } from './app.module'; -import { LoggerService } from 'services/contextual/logger.service'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {AppConstants} from 'app.constants'; +import {enableProdMode} from '@angular/core'; +import {AppModule} from './app.module'; +import {LoggerService} from 'services/contextual/logger.service'; if (!AppConstants.DEV_MODE) { enableProdMode(); } const loggerService = new LoggerService(); -platformBrowserDynamic().bootstrapModule(AppModule).catch( - (err) => loggerService.error(err) -); +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch(err => loggerService.error(err)); // This prevents angular pages to cause side effects to hybrid pages. // TODO(#13080): Remove window.name statement from import.ts files diff --git a/core/templates/pages/oppia-root/oppia-root.component.ts b/core/templates/pages/oppia-root/oppia-root.component.ts index 3cb31e276465..2bfd6cd6f5fa 100644 --- a/core/templates/pages/oppia-root/oppia-root.component.ts +++ b/core/templates/pages/oppia-root/oppia-root.component.ts @@ -16,10 +16,10 @@ * @fileoverview Oppia root component. */ -import { Component } from '@angular/core'; +import {Component} from '@angular/core'; @Component({ selector: 'oppia-root', - templateUrl: './oppia-root.component.html' + templateUrl: './oppia-root.component.html', }) export class OppiaRootComponent {} diff --git a/core/templates/pages/oppia-root/routing/access-validation-backend-api.service.spec.ts b/core/templates/pages/oppia-root/routing/access-validation-backend-api.service.spec.ts index 9404dc426c25..67cfc8492b2e 100644 --- a/core/templates/pages/oppia-root/routing/access-validation-backend-api.service.spec.ts +++ b/core/templates/pages/oppia-root/routing/access-validation-backend-api.service.spec.ts @@ -16,11 +16,18 @@ * @fileoverview Unit tests for access validation backend api service. */ -import { HttpClientTestingModule, HttpTestingController } from - '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks, waitForAsync } from '@angular/core/testing'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { AccessValidationBackendApiService } from './access-validation-backend-api.service'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import { + TestBed, + fakeAsync, + flushMicrotasks, + waitForAsync, +} from '@angular/core/testing'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {AccessValidationBackendApiService} from './access-validation-backend-api.service'; describe('Access validation backend api service', () => { let avbas: AccessValidationBackendApiService; @@ -31,12 +38,8 @@ describe('Access validation backend api service', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - providers: [ - UrlInterpolationService - ] + imports: [HttpClientTestingModule], + providers: [UrlInterpolationService], }).compileComponents(); })); @@ -58,7 +61,9 @@ describe('Access validation backend api service', () => { const req = httpTestingController.expectOne( '/access_validation_handler/can_access_classroom_page?' + - 'classroom_url_fragment=' + fragment); + 'classroom_url_fragment=' + + fragment + ); expect(req.request.method).toEqual('GET'); req.flush({}); @@ -71,7 +76,8 @@ describe('Access validation backend api service', () => { avbas.validateCanManageOwnAccount().then(successSpy, failSpy); const req = httpTestingController.expectOne( - '/access_validation_handler/can_manage_own_account'); + '/access_validation_handler/can_manage_own_account' + ); expect(req.request.method).toEqual('GET'); req.flush({}); @@ -90,7 +96,8 @@ describe('Access validation backend api service', () => { avbas.doesProfileExist(username).then(successSpy, failSpy); const req = httpTestingController.expectOne( - '/access_validation_handler/does_profile_exist/' + username); + '/access_validation_handler/does_profile_exist/' + username + ); expect(req.request.method).toEqual('GET'); req.flush({}); @@ -103,7 +110,8 @@ describe('Access validation backend api service', () => { avbas.validateAccessToReleaseCoordinatorPage().then(successSpy, failSpy); const req = httpTestingController.expectOne( - '/access_validation_handler/can_access_release_coordinator_page'); + '/access_validation_handler/can_access_release_coordinator_page' + ); expect(req.request.method).toEqual('GET'); req.flush({}); @@ -115,11 +123,13 @@ describe('Access validation backend api service', () => { it('should validate access to learner group editor page', fakeAsync(() => { let learnerGroupId = 'test_id'; - avbas.validateAccessToLearnerGroupEditorPage(learnerGroupId) + avbas + .validateAccessToLearnerGroupEditorPage(learnerGroupId) .then(successSpy, failSpy); const req = httpTestingController.expectOne( - '/access_validation_handler/can_access_edit_learner_group_page/test_id'); + '/access_validation_handler/can_access_edit_learner_group_page/test_id' + ); expect(req.request.method).toEqual('GET'); req.flush({}); @@ -132,7 +142,8 @@ describe('Access validation backend api service', () => { avbas.validateAccessToLearnerGroupCreatorPage().then(successSpy, failSpy); const req = httpTestingController.expectOne( - '/access_validation_handler/can_access_create_learner_group_page'); + '/access_validation_handler/can_access_create_learner_group_page' + ); expect(req.request.method).toEqual('GET'); req.flush({}); @@ -151,7 +162,8 @@ describe('Access validation backend api service', () => { avbas.doesLearnerGroupExist(learnerGroupId).then(successSpy, failSpy); const req = httpTestingController.expectOne( - '/access_validation_handler/does_learner_group_exist/' + learnerGroupId); + '/access_validation_handler/does_learner_group_exist/' + learnerGroupId + ); expect(req.request.method).toEqual('GET'); req.flush({}); @@ -160,112 +172,124 @@ describe('Access validation backend api service', () => { expect(failSpy).not.toHaveBeenCalled(); })); - it('should not validate access to blog home page with invalid access', - fakeAsync (() => { - avbas.validateAccessToBlogHomePage().then(successSpy, failSpy); + it('should not validate access to blog home page with invalid access', fakeAsync(() => { + avbas.validateAccessToBlogHomePage().then(successSpy, failSpy); - const req = httpTestingController.expectOne( - '/access_validation_handler/can_access_blog_home_page'); - expect(req.request.method).toEqual('GET'); - req.flush({ - error: 'Access Denied.' - }, { - status: 401, statusText: 'Access Denied.' - }); - - flushMicrotasks(); - expect(successSpy).not.toHaveBeenCalled(); - expect(failSpy).toHaveBeenCalled(); - }) - ); - - it('should validate access to blog home page with valid access', fakeAsync ( - () => { - avbas.validateAccessToBlogHomePage().then(successSpy, failSpy); + const req = httpTestingController.expectOne( + '/access_validation_handler/can_access_blog_home_page' + ); + expect(req.request.method).toEqual('GET'); + req.flush( + { + error: 'Access Denied.', + }, + { + status: 401, + statusText: 'Access Denied.', + } + ); - const req = httpTestingController.expectOne( - '/access_validation_handler/can_access_blog_home_page'); - expect(req.request.method).toEqual('GET'); - req.flush({}); + flushMicrotasks(); + expect(successSpy).not.toHaveBeenCalled(); + expect(failSpy).toHaveBeenCalled(); + })); - flushMicrotasks(); - expect(successSpy).toHaveBeenCalled(); - expect(failSpy).not.toHaveBeenCalled(); - })); - - it('should not validate access to blog post page with invalid access', - fakeAsync (() => { - avbas.validateAccessToBlogPostPage('invalid-post').then( - successSpy, failSpy - ); + it('should validate access to blog home page with valid access', fakeAsync(() => { + avbas.validateAccessToBlogHomePage().then(successSpy, failSpy); - const req = httpTestingController.expectOne( - '/access_validation_handler/can_access_blog_post_page?' + - 'blog_post_url_fragment=invalid-post'); - expect(req.request.method).toEqual('GET'); - req.flush({ - error: 'Access Denied.' - }, { - status: 401, statusText: 'Access Denied.' - }); + const req = httpTestingController.expectOne( + '/access_validation_handler/can_access_blog_home_page' + ); + expect(req.request.method).toEqual('GET'); + req.flush({}); - flushMicrotasks(); - expect(successSpy).not.toHaveBeenCalled(); - expect(failSpy).toHaveBeenCalled(); - }) - ); + flushMicrotasks(); + expect(successSpy).toHaveBeenCalled(); + expect(failSpy).not.toHaveBeenCalled(); + })); - it('should validate access to blog post page with valid access', fakeAsync ( - () => { - avbas.validateAccessToBlogPostPage('sample-post').then( - successSpy, failSpy); + it('should not validate access to blog post page with invalid access', fakeAsync(() => { + avbas + .validateAccessToBlogPostPage('invalid-post') + .then(successSpy, failSpy); - const req = httpTestingController.expectOne( - '/access_validation_handler/can_access_blog_post_page?' + - 'blog_post_url_fragment=sample-post'); - expect(req.request.method).toEqual('GET'); - req.flush({}); + const req = httpTestingController.expectOne( + '/access_validation_handler/can_access_blog_post_page?' + + 'blog_post_url_fragment=invalid-post' + ); + expect(req.request.method).toEqual('GET'); + req.flush( + { + error: 'Access Denied.', + }, + { + status: 401, + statusText: 'Access Denied.', + } + ); - flushMicrotasks(); - expect(successSpy).toHaveBeenCalled(); - expect(failSpy).not.toHaveBeenCalled(); - })); + flushMicrotasks(); + expect(successSpy).not.toHaveBeenCalled(); + expect(failSpy).toHaveBeenCalled(); + })); - it('should not validate access to blog author profile page with invalid ' + - 'access', fakeAsync (() => { - avbas.validateAccessToBlogAuthorProfilePage('username').then( - successSpy, failSpy); + it('should validate access to blog post page with valid access', fakeAsync(() => { + avbas.validateAccessToBlogPostPage('sample-post').then(successSpy, failSpy); const req = httpTestingController.expectOne( - '/access_validation_handler/can_access_blog_author_profile_page/username' + '/access_validation_handler/can_access_blog_post_page?' + + 'blog_post_url_fragment=sample-post' ); expect(req.request.method).toEqual('GET'); - req.flush({ - error: 'Access Denied.' - }, { - status: 401, statusText: 'Access Denied.' - }); + req.flush({}); flushMicrotasks(); - expect(successSpy).not.toHaveBeenCalled(); - expect(failSpy).toHaveBeenCalled(); - }) - ); + expect(successSpy).toHaveBeenCalled(); + expect(failSpy).not.toHaveBeenCalled(); + })); - it('should validate access to blog author profile page with valid access', - fakeAsync (() => { - avbas.validateAccessToBlogAuthorProfilePage('username').then( - successSpy, failSpy); + it( + 'should not validate access to blog author profile page with invalid ' + + 'access', + fakeAsync(() => { + avbas + .validateAccessToBlogAuthorProfilePage('username') + .then(successSpy, failSpy); const req = httpTestingController.expectOne( - '/access_validation_handler/can_access_blog_author_profile_page/' + - 'username' + '/access_validation_handler/can_access_blog_author_profile_page/username' ); expect(req.request.method).toEqual('GET'); - req.flush({}); + req.flush( + { + error: 'Access Denied.', + }, + { + status: 401, + statusText: 'Access Denied.', + } + ); flushMicrotasks(); - expect(successSpy).toHaveBeenCalled(); - expect(failSpy).not.toHaveBeenCalled(); - })); + expect(successSpy).not.toHaveBeenCalled(); + expect(failSpy).toHaveBeenCalled(); + }) + ); + + it('should validate access to blog author profile page with valid access', fakeAsync(() => { + avbas + .validateAccessToBlogAuthorProfilePage('username') + .then(successSpy, failSpy); + + const req = httpTestingController.expectOne( + '/access_validation_handler/can_access_blog_author_profile_page/' + + 'username' + ); + expect(req.request.method).toEqual('GET'); + req.flush({}); + + flushMicrotasks(); + expect(successSpy).toHaveBeenCalled(); + expect(failSpy).not.toHaveBeenCalled(); + })); }); diff --git a/core/templates/pages/oppia-root/routing/access-validation-backend-api.service.ts b/core/templates/pages/oppia-root/routing/access-validation-backend-api.service.ts index ab5158391614..3001f858fa2c 100644 --- a/core/templates/pages/oppia-root/routing/access-validation-backend-api.service.ts +++ b/core/templates/pages/oppia-root/routing/access-validation-backend-api.service.ts @@ -16,127 +16,135 @@ * @fileoverview Backend Api Service for access validation. */ -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class AccessValidationBackendApiService { - CLASSROOM_PAGE_ACCESS_VALIDATOR = ( - '/access_validation_handler/can_access_classroom_page'); + CLASSROOM_PAGE_ACCESS_VALIDATOR = + '/access_validation_handler/can_access_classroom_page'; - CAN_MANAGE_OWN_ACCOUNT_VALIDATOR = ( - '/access_validation_handler/can_manage_own_account'); + CAN_MANAGE_OWN_ACCOUNT_VALIDATOR = + '/access_validation_handler/can_manage_own_account'; - DOES_PROFILE_EXIST = ( - '/access_validation_handler/does_profile_exist/'); + DOES_PROFILE_EXIST = + '/access_validation_handler/does_profile_exist/'; - RELEASE_COORDINATOR_PAGE_ACCESS_VALIDATOR = ( - '/access_validation_handler/can_access_release_coordinator_page'); + RELEASE_COORDINATOR_PAGE_ACCESS_VALIDATOR = + '/access_validation_handler/can_access_release_coordinator_page'; - LEARNER_GROUP_EDITOR_PAGE_ACCESS_VALIDATOR = ( + LEARNER_GROUP_EDITOR_PAGE_ACCESS_VALIDATOR = '/access_validation_handler/can_access_edit_learner_group_page/' + - '' - ); + ''; - LEARNER_GROUP_CREATOR_PAGE_ACCESS_VALIDATOR = ( - '/access_validation_handler/can_access_create_learner_group_page' - ); + LEARNER_GROUP_CREATOR_PAGE_ACCESS_VALIDATOR = + '/access_validation_handler/can_access_create_learner_group_page'; - DOES_LEARNER_GROUP_EXIST = ( - '/access_validation_handler/does_learner_group_exist/'); + DOES_LEARNER_GROUP_EXIST = + '/access_validation_handler/does_learner_group_exist/'; - BLOG_HOME_PAGE_ACCESS_VALIDATOR = ( - '/access_validation_handler/can_access_blog_home_page'); + BLOG_HOME_PAGE_ACCESS_VALIDATOR = + '/access_validation_handler/can_access_blog_home_page'; - BLOG_POST_PAGE_ACCESS_VALIDATOR = ( - '/access_validation_handler/can_access_blog_post_page'); + BLOG_POST_PAGE_ACCESS_VALIDATOR = + '/access_validation_handler/can_access_blog_post_page'; - BLOG_AUTHOR_PROFILE_PAGE_ACCESS_VALIDATOR = ( - '/access_validation_handler/can_access_blog_author_profile_page/'); // eslint-disable-line max-len + BLOG_AUTHOR_PROFILE_PAGE_ACCESS_VALIDATOR = + '/access_validation_handler/can_access_blog_author_profile_page/'; // eslint-disable-line max-len constructor( private http: HttpClient, private urlInterpolationService: UrlInterpolationService ) {} - validateAccessToClassroomPage( - classroomUrlFragment: string - ): Promise { - return this.http.get(this.CLASSROOM_PAGE_ACCESS_VALIDATOR, { - params: { - classroom_url_fragment: classroomUrlFragment - } - }).toPromise(); + validateAccessToClassroomPage(classroomUrlFragment: string): Promise { + return this.http + .get(this.CLASSROOM_PAGE_ACCESS_VALIDATOR, { + params: { + classroom_url_fragment: classroomUrlFragment, + }, + }) + .toPromise(); } validateAccessToBlogHomePage(): Promise { - return this.http.get( - this.BLOG_HOME_PAGE_ACCESS_VALIDATOR).toPromise(); + return this.http + .get(this.BLOG_HOME_PAGE_ACCESS_VALIDATOR) + .toPromise(); } - validateAccessToBlogPostPage( - blogPostPageUrlFragment: string - ): Promise { - return this.http.get(this.BLOG_POST_PAGE_ACCESS_VALIDATOR, { - params: { - blog_post_url_fragment: blogPostPageUrlFragment - } - }).toPromise(); + validateAccessToBlogPostPage(blogPostPageUrlFragment: string): Promise { + return this.http + .get(this.BLOG_POST_PAGE_ACCESS_VALIDATOR, { + params: { + blog_post_url_fragment: blogPostPageUrlFragment, + }, + }) + .toPromise(); } - validateAccessToBlogAuthorProfilePage( - authorUsername: string - ): Promise { + validateAccessToBlogAuthorProfilePage(authorUsername: string): Promise { let url = this.urlInterpolationService.interpolateUrl( - this.BLOG_AUTHOR_PROFILE_PAGE_ACCESS_VALIDATOR, { - author_username: authorUsername - }); + this.BLOG_AUTHOR_PROFILE_PAGE_ACCESS_VALIDATOR, + { + author_username: authorUsername, + } + ); return this.http.get(url).toPromise(); } validateCanManageOwnAccount(): Promise { - return this.http.get( - this.CAN_MANAGE_OWN_ACCOUNT_VALIDATOR).toPromise(); + return this.http + .get(this.CAN_MANAGE_OWN_ACCOUNT_VALIDATOR) + .toPromise(); } doesProfileExist(username: string): Promise { let url = this.urlInterpolationService.interpolateUrl( - this.DOES_PROFILE_EXIST, { - username: username - }); + this.DOES_PROFILE_EXIST, + { + username: username, + } + ); return this.http.get(url).toPromise(); } - validateAccessToReleaseCoordinatorPage(): - Promise { - return this.http.get( - this.RELEASE_COORDINATOR_PAGE_ACCESS_VALIDATOR).toPromise(); + validateAccessToReleaseCoordinatorPage(): Promise { + return this.http + .get(this.RELEASE_COORDINATOR_PAGE_ACCESS_VALIDATOR) + .toPromise(); } validateAccessToLearnerGroupEditorPage( - learnerGroupId: string): Promise { + learnerGroupId: string + ): Promise { let url = this.urlInterpolationService.interpolateUrl( - this.LEARNER_GROUP_EDITOR_PAGE_ACCESS_VALIDATOR, { - learner_group_id: learnerGroupId - }); + this.LEARNER_GROUP_EDITOR_PAGE_ACCESS_VALIDATOR, + { + learner_group_id: learnerGroupId, + } + ); return this.http.get(url).toPromise(); } validateAccessToLearnerGroupCreatorPage(): Promise { - return this.http.get( - this.LEARNER_GROUP_CREATOR_PAGE_ACCESS_VALIDATOR).toPromise(); + return this.http + .get(this.LEARNER_GROUP_CREATOR_PAGE_ACCESS_VALIDATOR) + .toPromise(); } doesLearnerGroupExist(learnerGroupId: string): Promise { let url = this.urlInterpolationService.interpolateUrl( - this.DOES_LEARNER_GROUP_EXIST, { - learner_group_id: learnerGroupId - }); + this.DOES_LEARNER_GROUP_EXIST, + { + learner_group_id: learnerGroupId, + } + ); return this.http.get(url).toPromise(); } diff --git a/core/templates/pages/oppia-root/routing/app.routing.module.ts b/core/templates/pages/oppia-root/routing/app.routing.module.ts index b3d61b14fb9a..1d3bdec34717 100644 --- a/core/templates/pages/oppia-root/routing/app.routing.module.ts +++ b/core/templates/pages/oppia-root/routing/app.routing.module.ts @@ -16,322 +16,373 @@ * @fileoverview Root routing module. */ -import { APP_BASE_HREF } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { Route, RouterModule } from '@angular/router'; -import { AppConstants } from 'app.constants'; -import { IsLoggedInGuard } from 'pages/lightweight-oppia-root/routing/guards/is-logged-in.guard'; -import { IsNewLessonPlayerGuard } from 'pages/exploration-player-page/new-lesson-player/lesson-player-flag.guard'; +import {APP_BASE_HREF} from '@angular/common'; +import {NgModule} from '@angular/core'; +import {Route, RouterModule} from '@angular/router'; +import {AppConstants} from 'app.constants'; +import {IsLoggedInGuard} from 'pages/lightweight-oppia-root/routing/guards/is-logged-in.guard'; +import {IsNewLessonPlayerGuard} from 'pages/exploration-player-page/new-lesson-player/lesson-player-flag.guard'; // All paths must be defined in constants.ts file. // Otherwise pages will have false 404 status code. const routes: Route[] = [ { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ADMIN.ROUTE, - loadChildren: () => import('pages/admin-page/admin-page.module') - .then(m => m.AdminPageModule), - canActivate: [IsLoggedInGuard] + loadChildren: () => + import('pages/admin-page/admin-page.module').then(m => m.AdminPageModule), + canActivate: [IsLoggedInGuard], }, { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.MODERATOR.ROUTE, - loadChildren: () => import('pages/moderator-page/moderator-page.module') - .then(m => m.ModeratorPageModule), - canActivate: [IsLoggedInGuard] + loadChildren: () => + import('pages/moderator-page/moderator-page.module').then( + m => m.ModeratorPageModule + ), + canActivate: [IsLoggedInGuard], }, { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.BLOG_ADMIN.ROUTE, - loadChildren: () => import('pages/blog-admin-page/blog-admin-page.module') - .then(m => m.BlogAdminPageModule), - canActivate: [IsLoggedInGuard] + loadChildren: () => + import('pages/blog-admin-page/blog-admin-page.module').then( + m => m.BlogAdminPageModule + ), + canActivate: [IsLoggedInGuard], }, { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.BLOG_DASHBOARD.ROUTE, - loadChildren: () => import( - 'pages/blog-dashboard-page/blog-dashboard-page.module') - .then(m => m.BlogDashboardPageModule), - canActivate: [IsLoggedInGuard] + loadChildren: () => + import('pages/blog-dashboard-page/blog-dashboard-page.module').then( + m => m.BlogDashboardPageModule + ), + canActivate: [IsLoggedInGuard], }, { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.EMAIL_DASHBOARD.ROUTE, - loadChildren: () => import( - 'pages/email-dashboard-pages/email-dashboard-page.module') - .then(m => m.EmailDashboardPageModule), - canActivate: [IsLoggedInGuard] + loadChildren: () => + import('pages/email-dashboard-pages/email-dashboard-page.module').then( + m => m.EmailDashboardPageModule + ), + canActivate: [IsLoggedInGuard], }, { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.CLASSROOM.ROUTE, pathMatch: 'full', - loadChildren: () => import('pages/classroom-page/classroom-page.module') - .then(m => m.ClassroomPageModule) + loadChildren: () => + import('pages/classroom-page/classroom-page.module').then( + m => m.ClassroomPageModule + ), }, { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.CURRICULUM_ADMIN.ROUTE, - loadChildren: () => import( - 'pages/classroom-admin-page/classroom-admin-page.module') - .then(m => m.ClassroomAdminPageModule), - canActivate: [IsLoggedInGuard] + loadChildren: () => + import('pages/classroom-admin-page/classroom-admin-page.module').then( + m => m.ClassroomAdminPageModule + ), + canActivate: [IsLoggedInGuard], }, { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LEARNER_DASHBOARD.ROUTE, - loadChildren: () => import( - 'pages/learner-dashboard-page/learner-dashboard-page.module') - .then(m => m.LearnerDashboardPageModule), - canActivate: [IsLoggedInGuard] + loadChildren: () => + import('pages/learner-dashboard-page/learner-dashboard-page.module').then( + m => m.LearnerDashboardPageModule + ), + canActivate: [IsLoggedInGuard], }, { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LEARNER_GROUP_EDITOR .ROUTE, - loadChildren: () => import( - 'pages/learner-group-pages/edit-group/edit-learner-group-page.module') - .then(m => m.EditLearnerGroupPageModule), - canActivate: [IsLoggedInGuard] + loadChildren: () => + import( + 'pages/learner-group-pages/edit-group/edit-learner-group-page.module' + ).then(m => m.EditLearnerGroupPageModule), + canActivate: [IsLoggedInGuard], }, { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LEARNER_GROUP_CREATOR .ROUTE, - loadChildren: () => import( - 'pages/learner-group-pages/create-group/create-learner-group-page.module') - .then(m => m.CreateLearnerGroupPageModule), - canActivate: [IsLoggedInGuard] + loadChildren: () => + import( + 'pages/learner-group-pages/create-group/create-learner-group-page.module' + ).then(m => m.CreateLearnerGroupPageModule), + canActivate: [IsLoggedInGuard], }, { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ABOUT.ROUTE, - loadChildren: () => import('pages/about-page/about-page.module') - .then(m => m.AboutPageModule) + loadChildren: () => + import('pages/about-page/about-page.module').then(m => m.AboutPageModule), }, { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ABOUT_FOUNDATION.ROUTE, - loadChildren: () => import( - 'pages/about-foundation-page/about-foundation-page.module') - .then(m => m.AboutFoundationPageModule) + loadChildren: () => + import('pages/about-foundation-page/about-foundation-page.module').then( + m => m.AboutFoundationPageModule + ), }, { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND .CONTRIBUTOR_DASHBOARD_ADMIN.ROUTE, - loadChildren: () => import( - 'pages/contributor-dashboard-admin-page' + - '/contributor-dashboard-admin-page.module') - .then(m => m.ContributorDashboardAdminPageModule), - canActivate: [IsLoggedInGuard] + loadChildren: () => + import( + 'pages/contributor-dashboard-admin-page' + + '/contributor-dashboard-admin-page.module' + ).then(m => m.ContributorDashboardAdminPageModule), + canActivate: [IsLoggedInGuard], }, { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.EXPLORATION_PLAYER.ROUTE, - loadChildren: () => import( - 'pages/exploration-player-page/exploration-player-page.module') - .then(m => m.ExplorationPlayerPageModule) + loadChildren: () => + import( + 'pages/exploration-player-page/exploration-player-page.module' + ).then(m => m.ExplorationPlayerPageModule), }, { - path: ( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND - .EXPLORATION_PLAYER_EMBED.ROUTE), - loadChildren: () => import( - 'pages/exploration-player-page/exploration-player-page.module') - .then(m => m.ExplorationPlayerPageModule) + path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.EXPLORATION_PLAYER_EMBED + .ROUTE, + loadChildren: () => + import( + 'pages/exploration-player-page/exploration-player-page.module' + ).then(m => m.ExplorationPlayerPageModule), }, { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.NEW_LESSON_PLAYER.ROUTE, - loadChildren: () => import( - 'pages/exploration-player-page/new-lesson-player' + - '/lesson-player-page.module') - .then(m => m.NewLessonPlayerPageModule), - canActivate: [IsNewLessonPlayerGuard] + loadChildren: () => + import( + 'pages/exploration-player-page/new-lesson-player' + + '/lesson-player-page.module' + ).then(m => m.NewLessonPlayerPageModule), + canActivate: [IsNewLessonPlayerGuard], }, { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ANDROID.ROUTE, - loadChildren: () => import('pages/android-page/android-page.module') - .then(m => m.AndroidPageModule) + loadChildren: () => + import('pages/android-page/android-page.module').then( + m => m.AndroidPageModule + ), }, { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.DELETE_ACCOUNT.ROUTE, pathMatch: 'full', - loadChildren: () => import( - 'pages/delete-account-page/delete-account-page.module') - .then(m => m.DeleteAccountPageModule) + loadChildren: () => + import('pages/delete-account-page/delete-account-page.module').then( + m => m.DeleteAccountPageModule + ), }, { - path: ( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PENDING_ACCOUNT_DELETION.ROUTE - ), - loadChildren: () => import( - 'pages/pending-account-deletion-page/' + - 'pending-account-deletion-page.module') - .then(m => m.PendingAccountDeletionPageModule) + path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PENDING_ACCOUNT_DELETION + .ROUTE, + loadChildren: () => + import( + 'pages/pending-account-deletion-page/' + + 'pending-account-deletion-page.module' + ).then(m => m.PendingAccountDeletionPageModule), }, { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PREFERENCES.ROUTE, pathMatch: 'full', - loadChildren: () => import('pages/preferences-page/preferences-page.module') - .then(m => m.PreferencesPageModule) + loadChildren: () => + import('pages/preferences-page/preferences-page.module').then( + m => m.PreferencesPageModule + ), }, { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.FEEDBACK_UPDATES.ROUTE, pathMatch: 'full', - loadChildren: () => import( - 'pages/feedback-updates-page/feedback-updates-page.module') - .then(m => m.FeedbackUpdatesPageModule) + loadChildren: () => + import('pages/feedback-updates-page/feedback-updates-page.module').then( + m => m.FeedbackUpdatesPageModule + ), }, { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PROFILE.ROUTE, - loadChildren: () => import('pages/profile-page/profile-page.module') - .then(m => m.ProfilePageModule) + loadChildren: () => + import('pages/profile-page/profile-page.module').then( + m => m.ProfilePageModule + ), }, { - path: ( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.RELEASE_COORDINATOR_PAGE.ROUTE - ), - loadChildren: () => import( - 'pages/release-coordinator-page/release-coordinator-page.module') - .then(m => m.ReleaseCoordinatorPageModule) + path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.RELEASE_COORDINATOR_PAGE + .ROUTE, + loadChildren: () => + import( + 'pages/release-coordinator-page/release-coordinator-page.module' + ).then(m => m.ReleaseCoordinatorPageModule), }, { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LIBRARY_INDEX.ROUTE, pathMatch: 'full', - loadChildren: () => import('pages/library-page/library-page.module') - .then(m => m.LibraryPageModule) + loadChildren: () => + import('pages/library-page/library-page.module').then( + m => m.LibraryPageModule + ), }, { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LIBRARY_SEARCH.ROUTE, pathMatch: 'full', - loadChildren: () => import('pages/library-page/library-page.module') - .then(m => m.LibraryPageModule) + loadChildren: () => + import('pages/library-page/library-page.module').then( + m => m.LibraryPageModule + ), }, { - path: ( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LIBRARY_RECENTLY_PUBLISHED - .ROUTE - ), + path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LIBRARY_RECENTLY_PUBLISHED + .ROUTE, pathMatch: 'full', - loadChildren: () => import('pages/library-page/library-page.module') - .then(m => m.LibraryPageModule) + loadChildren: () => + import('pages/library-page/library-page.module').then( + m => m.LibraryPageModule + ), }, { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LIBRARY_TOP_RATED.ROUTE, pathMatch: 'full', - loadChildren: () => import('pages/library-page/library-page.module') - .then(m => m.LibraryPageModule) + loadChildren: () => + import('pages/library-page/library-page.module').then( + m => m.LibraryPageModule + ), }, { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.STORY_VIEWER.ROUTE, pathMatch: 'full', - loadChildren: () => import( - 'pages/story-viewer-page/story-viewer-page.module') - .then(m => m.StoryViewerPageModule) + loadChildren: () => + import('pages/story-viewer-page/story-viewer-page.module').then( + m => m.StoryViewerPageModule + ), }, { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.CONTACT.ROUTE, - loadChildren: () => import('pages/contact-page/contact-page.module') - .then(m => m.ContactPageModule) + loadChildren: () => + import('pages/contact-page/contact-page.module').then( + m => m.ContactPageModule + ), }, { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.DONATE.ROUTE, - loadChildren: () => import('pages/donate-page/donate-page.module') - .then(m => m.DonatePageModule) + loadChildren: () => + import('pages/donate-page/donate-page.module').then( + m => m.DonatePageModule + ), }, { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.GET_STARTED.ROUTE, - loadChildren: () => import('pages/get-started-page/get-started-page.module') - .then(m => m.GetStartedPageModule) + loadChildren: () => + import('pages/get-started-page/get-started-page.module').then( + m => m.GetStartedPageModule + ), }, { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LICENSE.ROUTE, - loadChildren: () => import('pages/license-page/license.module') - .then(m => m.LicensePageModule) + loadChildren: () => + import('pages/license-page/license.module').then( + m => m.LicensePageModule + ), }, { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LOGIN.ROUTE, - loadChildren: () => import('pages/login-page/login-page.module') - .then(m => m.LoginPageModule) + loadChildren: () => + import('pages/login-page/login-page.module').then(m => m.LoginPageModule), }, { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LOGOUT.ROUTE, - loadChildren: () => import('pages/logout-page/logout-page.module') - .then(m => m.LogoutPageModule) + loadChildren: () => + import('pages/logout-page/logout-page.module').then( + m => m.LogoutPageModule + ), }, { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PARTNERSHIPS.ROUTE, - loadChildren: () => import( - 'pages/partnerships-page/partnerships-page.module') - .then(m => m.PartnershipsPageModule) + loadChildren: () => + import('pages/partnerships-page/partnerships-page.module').then( + m => m.PartnershipsPageModule + ), }, { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PLAYBOOK.ROUTE, - loadChildren: () => import('pages/participation-playbook/playbook.module') - .then(m => m.PlaybookPageModule) + loadChildren: () => + import('pages/participation-playbook/playbook.module').then( + m => m.PlaybookPageModule + ), }, { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PRIVACY.ROUTE, - loadChildren: () => import('pages/privacy-page/privacy-page.module') - .then(m => m.PrivacyPageModule) + loadChildren: () => + import('pages/privacy-page/privacy-page.module').then( + m => m.PrivacyPageModule + ), }, { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.SIGNUP.ROUTE, - loadChildren: () => import('pages/signup-page/signup-page.module') - .then(m => m.SignupPageModule) + loadChildren: () => + import('pages/signup-page/signup-page.module').then( + m => m.SignupPageModule + ), }, { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.TEACH.ROUTE, - loadChildren: () => import('pages/teach-page/teach-page.module') - .then(m => m.TeachPageModule) + loadChildren: () => + import('pages/teach-page/teach-page.module').then(m => m.TeachPageModule), }, { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.TERMS.ROUTE, - loadChildren: () => import('pages/terms-page/terms-page.module') - .then(m => m.TermsPageModule) + loadChildren: () => + import('pages/terms-page/terms-page.module').then(m => m.TermsPageModule), }, { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.THANKS.ROUTE, - loadChildren: () => import('pages/thanks-page/thanks-page.module') - .then(m => m.ThanksPageModule) + loadChildren: () => + import('pages/thanks-page/thanks-page.module').then( + m => m.ThanksPageModule + ), }, { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.VOLUNTEER.ROUTE, - loadChildren: () => import( - 'pages/volunteer-page/volunteer-page.module') - .then(m => m.VolunteerPageModule) + loadChildren: () => + import('pages/volunteer-page/volunteer-page.module').then( + m => m.VolunteerPageModule + ), }, { - path: ( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LEARNER_GROUP_VIEWER.ROUTE - ), + path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LEARNER_GROUP_VIEWER + .ROUTE, pathMatch: 'full', - loadChildren: () => import( - 'pages/learner-group-pages/view-group/view-learner-group-page.module') - .then(m => m.ViewLearnerGroupPageModule) + loadChildren: () => + import( + 'pages/learner-group-pages/view-group/view-learner-group-page.module' + ).then(m => m.ViewLearnerGroupPageModule), }, { path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.BLOG_HOMEPAGE.ROUTE, pathMatch: 'full', - loadChildren: () => import( - 'pages/blog-home-page/blog-home-page.module') - .then(m => m.BlogHomePageModule) + loadChildren: () => + import('pages/blog-home-page/blog-home-page.module').then( + m => m.BlogHomePageModule + ), }, { - path: ( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.BLOG_HOMEPAGE_SEARCH.ROUTE - ), + path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.BLOG_HOMEPAGE_SEARCH + .ROUTE, pathMatch: 'full', - loadChildren: () => import( - 'pages/blog-home-page/blog-home-page.module') - .then(m => m.BlogHomePageModule) + loadChildren: () => + import('pages/blog-home-page/blog-home-page.module').then( + m => m.BlogHomePageModule + ), }, { - path: ( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.BLOG_AUTHOR_PROFILE_PAGE.ROUTE - ), + path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.BLOG_AUTHOR_PROFILE_PAGE + .ROUTE, pathMatch: 'full', - loadChildren: () => import( - 'pages/blog-author-profile-page/blog-author-profile-page.module') - .then(m => m.BlogAuthorProfilePageModule) + loadChildren: () => + import( + 'pages/blog-author-profile-page/blog-author-profile-page.module' + ).then(m => m.BlogAuthorProfilePageModule), }, { - path: ( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.BLOG_POST_PAGE.ROUTE - ), + path: AppConstants.PAGES_REGISTERED_WITH_FRONTEND.BLOG_POST_PAGE.ROUTE, pathMatch: 'full', - loadChildren: () => import( - 'pages/blog-post-page/blog-post-page.module') - .then(m => m.BlogPostPageModule) - } + loadChildren: () => + import('pages/blog-post-page/blog-post-page.module').then( + m => m.BlogPostPageModule + ), + }, ]; // Register stewards landing pages. @@ -339,9 +390,10 @@ for (let i = 0; i < AppConstants.STEWARDS_LANDING_PAGE.ROUTES.length; i++) { // Redirect old stewards landing pages to volunteer page. routes.push({ path: AppConstants.STEWARDS_LANDING_PAGE.ROUTES[i], - loadChildren: () => import( - 'pages/volunteer-page/volunteer-page.module').then( - m => m.VolunteerPageModule) + loadChildren: () => + import('pages/volunteer-page/volunteer-page.module').then( + m => m.VolunteerPageModule + ), }); } @@ -350,9 +402,10 @@ for (let key in AppConstants.AVAILABLE_LANDING_PAGES) { for (let i = 0; i < AppConstants.AVAILABLE_LANDING_PAGES[key].length; i++) { routes.push({ path: key + '/' + AppConstants.AVAILABLE_LANDING_PAGES[key][i], - loadChildren: () => import( - 'pages/landing-pages/topic-landing-page/topic-landing-page.module') - .then(m => m.TopicLandingPageModule) + loadChildren: () => + import( + 'pages/landing-pages/topic-landing-page/topic-landing-page.module' + ).then(m => m.TopicLandingPageModule), }); } } @@ -361,35 +414,32 @@ for (let key in AppConstants.AVAILABLE_LANDING_PAGES) { routes.push( // Route to register all the custom error pages on oppia. { - path: `${AppConstants.PAGES_REGISTERED_WITH_FRONTEND - .ERROR.ROUTE}/:status_code`, - loadChildren: () => import( - 'pages/error-pages/error-page.module').then( - m => m.ErrorPageModule) + path: `${AppConstants.PAGES_REGISTERED_WITH_FRONTEND.ERROR.ROUTE}/:status_code`, + loadChildren: () => + import('pages/error-pages/error-page.module').then( + m => m.ErrorPageModule + ), }, // '**' wildcard route must be kept at the end,as it can override all other // routes. // Add error page for not found routes. { path: '**', - loadChildren: () => import( - 'pages/error-pages/error-page.module').then( - m => m.ErrorPageModule) + loadChildren: () => + import('pages/error-pages/error-page.module').then( + m => m.ErrorPageModule + ), } ); @NgModule({ - imports: [ - RouterModule.forRoot(routes), - ], - exports: [ - RouterModule - ], + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule], providers: [ { provide: APP_BASE_HREF, - useValue: '/' - } - ] + useValue: '/', + }, + ], }) export class AppRoutingModule {} diff --git a/core/templates/pages/participation-playbook/playbook-page-root.component.spec.ts b/core/templates/pages/participation-playbook/playbook-page-root.component.spec.ts index 258c4a56aa1f..2b1e8fbabc78 100644 --- a/core/templates/pages/participation-playbook/playbook-page-root.component.spec.ts +++ b/core/templates/pages/participation-playbook/playbook-page-root.component.spec.ts @@ -16,15 +16,15 @@ * @fileoverview Unit tests for the playbook page root component. */ -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { PlaybookPageRootComponent } from './playbook-page-root.component'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {PlaybookPageRootComponent} from './playbook-page-root.component'; class MockTranslateService { onLangChange: EventEmitter = new EventEmitter(); @@ -41,18 +41,15 @@ describe('Playbook Page Root', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - PlaybookPageRootComponent, - MockTranslatePipe - ], + declarations: [PlaybookPageRootComponent, MockTranslatePipe], providers: [ PageHeadService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -63,10 +60,9 @@ describe('Playbook Page Root', () => { translateService = TestBed.inject(TranslateService); }); - it('should successfully instantiate the component', - () => { - expect(component).toBeDefined(); - }); + it('should successfully instantiate the component', () => { + expect(component).toBeDefined(); + }); it('should initialize and subscribe to onLangChange', () => { spyOn(translateService.onLangChange, 'subscribe'); @@ -92,10 +88,12 @@ describe('Playbook Page Root', () => { component.setPageTitleAndMetaTags(); expect(translateService.instant).toHaveBeenCalledWith( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PLAYBOOK.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PLAYBOOK.TITLE + ); expect(pageHeadService.updateTitleAndMetaTags).toHaveBeenCalledWith( AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PLAYBOOK.TITLE, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PLAYBOOK.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PLAYBOOK.META + ); }); it('should unsubscribe on component destruction', () => { diff --git a/core/templates/pages/participation-playbook/playbook-page-root.component.ts b/core/templates/pages/participation-playbook/playbook-page-root.component.ts index 70488189127b..58bface3ac15 100644 --- a/core/templates/pages/participation-playbook/playbook-page-root.component.ts +++ b/core/templates/pages/participation-playbook/playbook-page-root.component.ts @@ -16,16 +16,16 @@ * @fileoverview Root component for Playbook Page. */ -import { Component, OnDestroy } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; +import {Component, OnDestroy} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; @Component({ selector: 'oppia-playbook-page-root', - templateUrl: './playbook-page-root.component.html' + templateUrl: './playbook-page-root.component.html', }) export class PlaybookPageRootComponent implements OnDestroy { directiveSubscriptions = new Subscription(); @@ -36,10 +36,12 @@ export class PlaybookPageRootComponent implements OnDestroy { setPageTitleAndMetaTags(): void { let translatedTitle = this.translateService.instant( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PLAYBOOK.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PLAYBOOK.TITLE + ); this.pageHeadService.updateTitleAndMetaTags( translatedTitle, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PLAYBOOK.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PLAYBOOK.META + ); } ngOnInit(): void { diff --git a/core/templates/pages/participation-playbook/playbook-page-routing.module.ts b/core/templates/pages/participation-playbook/playbook-page-routing.module.ts index 09faa45ca3e7..7a4436448d0d 100644 --- a/core/templates/pages/participation-playbook/playbook-page-routing.module.ts +++ b/core/templates/pages/participation-playbook/playbook-page-routing.module.ts @@ -16,24 +16,19 @@ * @fileoverview Routing module for playbook page. */ -import { NgModule } from '@angular/core'; -import { Route, RouterModule } from '@angular/router'; -import { PlaybookPageRootComponent } from './playbook-page-root.component'; +import {NgModule} from '@angular/core'; +import {Route, RouterModule} from '@angular/router'; +import {PlaybookPageRootComponent} from './playbook-page-root.component'; const routes: Route[] = [ { path: '', - component: PlaybookPageRootComponent - } + component: PlaybookPageRootComponent, + }, ]; @NgModule({ - imports: [ - RouterModule.forChild(routes) - ], - exports: [ - RouterModule - ] + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], }) - export class PlaybookPageRoutingModule {} diff --git a/core/templates/pages/participation-playbook/playbook.component.spec.ts b/core/templates/pages/participation-playbook/playbook.component.spec.ts index 3c71a9db6cc7..d2c48cde82bc 100644 --- a/core/templates/pages/participation-playbook/playbook.component.spec.ts +++ b/core/templates/pages/participation-playbook/playbook.component.spec.ts @@ -16,16 +16,20 @@ * @fileoverview Unit tests for the teach page. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, fakeAsync, tick, waitForAsync } from - '@angular/core/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + TestBed, + fakeAsync, + tick, + waitForAsync, +} from '@angular/core/testing'; -import { PlaybookPageComponent } from './playbook.component'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; +import {PlaybookPageComponent} from './playbook.component'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; // Mocking window object here because changing location.href causes the // full page to reload. Page reloads raise an error in karma. @@ -35,7 +39,7 @@ class MockWindowRef { hash: '', hashChange: null, href: '', - reload: (val: string) => val + reload: (val: string) => val, }, get onhashchange() { return this.location.hashChange; @@ -44,7 +48,7 @@ class MockWindowRef { set onhashchange(val) { this.location.hashChange = val; }, - gtag: () => {} + gtag: () => {}, }; get nativeWindow() { @@ -71,15 +75,15 @@ describe('Playbook Page', () => { providers: [ { provide: SiteAnalyticsService, - useClass: MockSiteAnalyticsService + useClass: MockSiteAnalyticsService, }, UrlInterpolationService, { provide: WindowRef, - useValue: windowRef - } + useValue: windowRef, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); siteAnalyticsService = TestBed.inject(SiteAnalyticsService); })); @@ -92,13 +96,15 @@ describe('Playbook Page', () => { it('should get static image url', () => { expect(component.getStaticImageUrl('/path/to/image')).toBe( - '/assets/images/path/to/image'); + '/assets/images/path/to/image' + ); }); it('should apply to teach with oppia', fakeAsync(() => { const applyToTeachWithOppiaEventSpy = spyOn( - siteAnalyticsService, 'registerApplyToTeachWithOppiaEvent') - .and.callThrough(); + siteAnalyticsService, + 'registerApplyToTeachWithOppiaEvent' + ).and.callThrough(); component.ngOnInit(); spyOnProperty(windowRef, 'nativeWindow').and.returnValue({ @@ -115,7 +121,8 @@ describe('Playbook Page', () => { tick(150); fixture.detectChanges(); expect(windowRef.nativeWindow.location.href).toBe( - 'https://goo.gl/forms/0p3Axuw5tLjTfiri1'); + 'https://goo.gl/forms/0p3Axuw5tLjTfiri1' + ); expect(applyToTeachWithOppiaEventSpy).toHaveBeenCalled(); })); }); diff --git a/core/templates/pages/participation-playbook/playbook.component.ts b/core/templates/pages/participation-playbook/playbook.component.ts index adae07f19972..9e9f3a577c98 100644 --- a/core/templates/pages/participation-playbook/playbook.component.ts +++ b/core/templates/pages/participation-playbook/playbook.component.ts @@ -16,33 +16,31 @@ * @fileoverview Component for the playbook page. */ -import { Component, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { AppConstants } from 'app.constants'; +import {Component, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {AppConstants} from 'app.constants'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; import './playbook.component.css'; - @Component({ selector: 'participation-playbook', templateUrl: './playbook.component.html', - styleUrls: ['./playbook.component.css'] + styleUrls: ['./playbook.component.css'], }) export class PlaybookPageComponent implements OnInit { TAB_ID_PARTICIPATION: string = 'participation'; TEACH_FORM_URL: string = 'https://goo.gl/forms/0p3Axuw5tLjTfiri1'; - communityLibraryUrl = ( - '/' + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LIBRARY_INDEX.ROUTE); + communityLibraryUrl = + '/' + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LIBRARY_INDEX.ROUTE; constructor( private siteAnalyticsService: SiteAnalyticsService, private urlInterpolationService: UrlInterpolationService, - private windowRef: WindowRef, + private windowRef: WindowRef ) {} ngOnInit(): void {} @@ -60,5 +58,9 @@ export class PlaybookPageComponent implements OnInit { } } -angular.module('oppia').directive('participationPlaybook', - downgradeComponent({component: PlaybookPageComponent})); +angular + .module('oppia') + .directive( + 'participationPlaybook', + downgradeComponent({component: PlaybookPageComponent}) + ); diff --git a/core/templates/pages/participation-playbook/playbook.import.ts b/core/templates/pages/participation-playbook/playbook.import.ts index 8da60811b54a..0d792eefa1a8 100644 --- a/core/templates/pages/participation-playbook/playbook.import.ts +++ b/core/templates/pages/participation-playbook/playbook.import.ts @@ -17,11 +17,11 @@ */ import 'pages/common-imports'; -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { AppConstants } from 'app.constants'; -import { enableProdMode } from '@angular/core'; -import { PlaybookPageModule } from './playbook.module'; -import { LoggerService } from 'services/contextual/logger.service'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {AppConstants} from 'app.constants'; +import {enableProdMode} from '@angular/core'; +import {PlaybookPageModule} from './playbook.module'; +import {LoggerService} from 'services/contextual/logger.service'; if (!AppConstants.DEV_MODE) { enableProdMode(); @@ -29,9 +29,9 @@ if (!AppConstants.DEV_MODE) { const loggerService = new LoggerService(); -platformBrowserDynamic().bootstrapModule(PlaybookPageModule).catch( - (err) => loggerService.error(err) -); +platformBrowserDynamic() + .bootstrapModule(PlaybookPageModule) + .catch(err => loggerService.error(err)); // This prevents angular pages to cause side effects to hybrid pages. // TODO(#13080): Remove window.name statement from import.ts files diff --git a/core/templates/pages/participation-playbook/playbook.module.ts b/core/templates/pages/participation-playbook/playbook.module.ts index a5a13eec7163..64e72beafadb 100644 --- a/core/templates/pages/participation-playbook/playbook.module.ts +++ b/core/templates/pages/participation-playbook/playbook.module.ts @@ -16,13 +16,13 @@ * @fileoverview Module for the participation playbook page. */ -import { NgModule } from '@angular/core'; -import { PlaybookPageComponent } from './playbook.component'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { PlaybookPageRootComponent } from './playbook-page-root.component'; -import { CommonModule } from '@angular/common'; -import { PlaybookPageRoutingModule } from './playbook-page-routing.module'; -import { SmartRouterModule } from 'hybrid-router-module-provider'; +import {NgModule} from '@angular/core'; +import {PlaybookPageComponent} from './playbook.component'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {PlaybookPageRootComponent} from './playbook-page-root.component'; +import {CommonModule} from '@angular/common'; +import {PlaybookPageRoutingModule} from './playbook-page-routing.module'; +import {SmartRouterModule} from 'hybrid-router-module-provider'; @NgModule({ imports: [ @@ -31,15 +31,9 @@ import { SmartRouterModule } from 'hybrid-router-module-provider'; // TODO(#13443): Remove hybrid router module provider once all pages are // migrated to angular router. SmartRouterModule, - PlaybookPageRoutingModule + PlaybookPageRoutingModule, ], - declarations: [ - PlaybookPageComponent, - PlaybookPageRootComponent - ], - entryComponents: [ - PlaybookPageComponent, - PlaybookPageRootComponent, - ] + declarations: [PlaybookPageComponent, PlaybookPageRootComponent], + entryComponents: [PlaybookPageComponent, PlaybookPageRootComponent], }) export class PlaybookPageModule {} diff --git a/core/templates/pages/partnerships-page/partnerships-page-root.component.spec.ts b/core/templates/pages/partnerships-page/partnerships-page-root.component.spec.ts index 0ce57b1385c0..9b9dd79c34ae 100644 --- a/core/templates/pages/partnerships-page/partnerships-page-root.component.spec.ts +++ b/core/templates/pages/partnerships-page/partnerships-page-root.component.spec.ts @@ -16,13 +16,13 @@ * @fileoverview Unit tests for the partnerships page root component. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { PartnershipsPageRootComponent } from './partnerships-page-root.component'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {PartnershipsPageRootComponent} from './partnerships-page-root.component'; describe('Partnerships Page Root', () => { let fixture: ComponentFixture; @@ -31,14 +31,9 @@ describe('Partnerships Page Root', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - PartnershipsPageRootComponent, - MockTranslatePipe - ], - providers: [ - PageHeadService - ], - schemas: [NO_ERRORS_SCHEMA] + declarations: [PartnershipsPageRootComponent, MockTranslatePipe], + providers: [PageHeadService], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -48,10 +43,9 @@ describe('Partnerships Page Root', () => { pageHeadService = TestBed.inject(PageHeadService); }); - it('should successfully instantiate the component', - () => { - expect(component).toBeDefined(); - }); + it('should successfully instantiate the component', () => { + expect(component).toBeDefined(); + }); it('should initialize', () => { spyOn(pageHeadService, 'updateTitleAndMetaTags'); @@ -60,6 +54,7 @@ describe('Partnerships Page Root', () => { expect(pageHeadService.updateTitleAndMetaTags).toHaveBeenCalledWith( AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PARTNERSHIPS.TITLE, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PARTNERSHIPS.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PARTNERSHIPS.META + ); }); }); diff --git a/core/templates/pages/partnerships-page/partnerships-page-root.component.ts b/core/templates/pages/partnerships-page/partnerships-page-root.component.ts index f419fb652ea2..4f3cd1ab6c2f 100644 --- a/core/templates/pages/partnerships-page/partnerships-page-root.component.ts +++ b/core/templates/pages/partnerships-page/partnerships-page-root.component.ts @@ -16,23 +16,22 @@ * @fileoverview Root Component for partnerships page. */ -import { Component } from '@angular/core'; +import {Component} from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; @Component({ selector: 'oppia-partnerships-page-root', - templateUrl: './partnerships-page-root.component.html' + templateUrl: './partnerships-page-root.component.html', }) export class PartnershipsPageRootComponent { - constructor( - private pageHeadService: PageHeadService - ) {} + constructor(private pageHeadService: PageHeadService) {} ngOnInit(): void { this.pageHeadService.updateTitleAndMetaTags( AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PARTNERSHIPS.TITLE, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PARTNERSHIPS.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PARTNERSHIPS.META + ); } } diff --git a/core/templates/pages/partnerships-page/partnerships-page-routing.module.ts b/core/templates/pages/partnerships-page/partnerships-page-routing.module.ts index 462402c475e0..ef1ae20b6f5f 100644 --- a/core/templates/pages/partnerships-page/partnerships-page-routing.module.ts +++ b/core/templates/pages/partnerships-page/partnerships-page-routing.module.ts @@ -16,24 +16,19 @@ * @fileoverview Routing module for partnerships page. */ -import { NgModule } from '@angular/core'; -import { Route, RouterModule } from '@angular/router'; -import { PartnershipsPageRootComponent } from './partnerships-page-root.component'; +import {NgModule} from '@angular/core'; +import {Route, RouterModule} from '@angular/router'; +import {PartnershipsPageRootComponent} from './partnerships-page-root.component'; const routes: Route[] = [ { path: '', - component: PartnershipsPageRootComponent - } + component: PartnershipsPageRootComponent, + }, ]; @NgModule({ - imports: [ - RouterModule.forChild(routes) - ], - exports: [ - RouterModule - ] + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], }) - export class PartnershipsPageRoutingModule {} diff --git a/core/templates/pages/partnerships-page/partnerships-page.component.spec.ts b/core/templates/pages/partnerships-page/partnerships-page.component.spec.ts index d6684c81d01c..e75af6b7bedc 100644 --- a/core/templates/pages/partnerships-page/partnerships-page.component.spec.ts +++ b/core/templates/pages/partnerships-page/partnerships-page.component.spec.ts @@ -16,15 +16,14 @@ * @fileoverview Unit tests for partnerships page. */ -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; -import { PartnershipsPageComponent } from './partnerships-page.component'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; -import { PageTitleService } from 'services/page-title.service'; +import {PartnershipsPageComponent} from './partnerships-page.component'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {PageTitleService} from 'services/page-title.service'; class MockTranslateService { onLangChange: EventEmitter = new EventEmitter(); @@ -37,21 +36,18 @@ describe('Partnerships page', () => { let translateService: TranslateService; let pageTitleService: PageTitleService; - beforeEach(async() => { + beforeEach(async () => { TestBed.configureTestingModule({ - declarations: [ - PartnershipsPageComponent, - MockTranslatePipe - ], + declarations: [PartnershipsPageComponent, MockTranslatePipe], providers: [ UrlInterpolationService, PageTitleService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); @@ -59,38 +55,43 @@ describe('Partnerships page', () => { beforeEach(() => { const partnershipsPageComponent = TestBed.createComponent( - PartnershipsPageComponent); + PartnershipsPageComponent + ); component = partnershipsPageComponent.componentInstance; pageTitleService = TestBed.inject(PageTitleService); translateService = TestBed.inject(TranslateService); }); - it('should successfully instantiate the component from beforeEach block', - () => { - expect(component).toBeDefined(); - }); + it('should successfully instantiate the component from beforeEach block', () => { + expect(component).toBeDefined(); + }); it('should set component properties when ngOnInit() is called', () => { spyOn(translateService.onLangChange, 'subscribe'); component.ngOnInit(); expect(component.partnershipsImgUrl).toBe( - '/assets/images/general/partnerships_hero_image.png'); + '/assets/images/general/partnerships_hero_image.png' + ); expect(component.formIconUrl).toBe('/assets/images/icons/icon_form.png'); expect(component.callIconUrl).toBe('/assets/images/icons/icon_call.png'); expect(component.changeIconUrl).toBe( - '/assets/images/icons/icon_change.png'); + '/assets/images/icons/icon_change.png' + ); expect(translateService.onLangChange.subscribe).toHaveBeenCalled(); }); - it('should obtain translated page title whenever the selected' + - 'language changes', () => { - component.ngOnInit(); - spyOn(component, 'setPageTitle'); - translateService.onLangChange.emit(); + it( + 'should obtain translated page title whenever the selected' + + 'language changes', + () => { + component.ngOnInit(); + spyOn(component, 'setPageTitle'); + translateService.onLangChange.emit(); - expect(component.setPageTitle).toHaveBeenCalled(); - }); + expect(component.setPageTitle).toHaveBeenCalled(); + } + ); it('should set new page title', () => { spyOn(translateService, 'instant').and.callThrough(); @@ -98,19 +99,23 @@ describe('Partnerships page', () => { component.setPageTitle(); expect(translateService.instant).toHaveBeenCalledWith( - 'I18N_PARTNERSHIPS_PAGE_TITLE'); + 'I18N_PARTNERSHIPS_PAGE_TITLE' + ); expect(pageTitleService.setDocumentTitle).toHaveBeenCalledWith( - 'I18N_PARTNERSHIPS_PAGE_TITLE'); + 'I18N_PARTNERSHIPS_PAGE_TITLE' + ); }); - it('should obtain new form link whenever the selected' + - 'language changes', () => { - component.ngOnInit(); - spyOn(component, 'setFormLink'); - translateService.onLangChange.emit(); + it( + 'should obtain new form link whenever the selected' + 'language changes', + () => { + component.ngOnInit(); + spyOn(component, 'setFormLink'); + translateService.onLangChange.emit(); - expect(component.setFormLink).toHaveBeenCalled(); - }); + expect(component.setFormLink).toHaveBeenCalled(); + } + ); it('should set the correct form link for English language', () => { translateService.currentLang = 'en'; @@ -122,7 +127,8 @@ describe('Partnerships page', () => { it('should set the correct form link for Portuguese language', () => { translateService.currentLang = 'pt-br'; - const formLink = 'https://docs-google-com.translate.goog/forms/d/e/1FAIpQLSdL5mjFO7RxDtg8yfXluEtciYj8WnAqTL9fZWnwPgOqXV-9lg/viewform?_x_tr_sl=en&_x_tr_tl=pt&_x_tr_hl=en-US&_x_tr_pto=wapp'; + const formLink = + 'https://docs-google-com.translate.goog/forms/d/e/1FAIpQLSdL5mjFO7RxDtg8yfXluEtciYj8WnAqTL9fZWnwPgOqXV-9lg/viewform?_x_tr_sl=en&_x_tr_tl=pt&_x_tr_hl=en-US&_x_tr_pto=wapp'; component.setFormLink(); expect(component.formLink).toBe(formLink); @@ -130,20 +136,23 @@ describe('Partnerships page', () => { it('should set the correct form link for general languages', () => { translateService.currentLang = 'fr'; - const formLink = 'https://docs-google-com.translate.goog/forms/d/e/1FAIpQLSdL5mjFO7RxDtg8yfXluEtciYj8WnAqTL9fZWnwPgOqXV-9lg/viewform?_x_tr_sl=en&_x_tr_tl=fr&_x_tr_hl=en-US&_x_tr_pto=wapp'; + const formLink = + 'https://docs-google-com.translate.goog/forms/d/e/1FAIpQLSdL5mjFO7RxDtg8yfXluEtciYj8WnAqTL9fZWnwPgOqXV-9lg/viewform?_x_tr_sl=en&_x_tr_tl=fr&_x_tr_hl=en-US&_x_tr_pto=wapp'; component.setFormLink(); expect(component.formLink).toBe(formLink); }); - it('should set english link for languages not supported by' + - ' google forms', () => { - translateService.currentLang = 'pcm'; - const formLink = 'https://forms.gle/Y71U8FdhQwZpicJj8'; - component.setFormLink(); + it( + 'should set english link for languages not supported by' + ' google forms', + () => { + translateService.currentLang = 'pcm'; + const formLink = 'https://forms.gle/Y71U8FdhQwZpicJj8'; + component.setFormLink(); - expect(component.formLink).toBe(formLink); - }); + expect(component.formLink).toBe(formLink); + } + ); it('should unsubscribe on component destruction', () => { component.directiveSubscriptions.add( diff --git a/core/templates/pages/partnerships-page/partnerships-page.component.ts b/core/templates/pages/partnerships-page/partnerships-page.component.ts index 92c4cb73e9eb..6b176c5da56f 100644 --- a/core/templates/pages/partnerships-page/partnerships-page.component.ts +++ b/core/templates/pages/partnerships-page/partnerships-page.component.ts @@ -16,15 +16,13 @@ * @fileoverview Component for the partnerships page. */ -import { Component, OnInit, OnDestroy } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; - -import { PageTitleService } from 'services/page-title.service'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; +import {Component, OnInit, OnDestroy} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; +import {PageTitleService} from 'services/page-title.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; @Component({ selector: 'partnerships-page', @@ -64,7 +62,8 @@ export class PartnershipsPageComponent implements OnInit, OnDestroy { setPageTitle(): void { let translatedTitle = this.translateService.instant( - 'I18N_PARTNERSHIPS_PAGE_TITLE'); + 'I18N_PARTNERSHIPS_PAGE_TITLE' + ); this.pageTitleService.setDocumentTitle(translatedTitle); } @@ -74,7 +73,7 @@ export class PartnershipsPageComponent implements OnInit, OnDestroy { if (userLang === 'en' || userLang === 'pcm' || userLang === 'kab') { this.formLink = 'https://forms.gle/Y71U8FdhQwZpicJj8'; } else { - let interpolatedLanguage = (userLang === 'pt-br') ? 'pt' : userLang; + let interpolatedLanguage = userLang === 'pt-br' ? 'pt' : userLang; this.formLink = `https://docs-google-com.translate.goog/forms/d/e/1FAIpQLSdL5mjFO7RxDtg8yfXluEtciYj8WnAqTL9fZWnwPgOqXV-9lg/viewform?_x_tr_sl=en&_x_tr_tl=${interpolatedLanguage}&_x_tr_hl=en-US&_x_tr_pto=wapp`; } } @@ -87,45 +86,65 @@ export class PartnershipsPageComponent implements OnInit, OnDestroy { }) ); this.partnershipsImgUrl = this.urlInterpolationService.getStaticImageUrl( - '/general/partnerships_hero_image.png'); + '/general/partnerships_hero_image.png' + ); this.formIconUrl = this.urlInterpolationService.getStaticImageUrl( - '/icons/icon_form.png'); + '/icons/icon_form.png' + ); this.callIconUrl = this.urlInterpolationService.getStaticImageUrl( - '/icons/icon_call.png'); + '/icons/icon_call.png' + ); this.changeIconUrl = this.urlInterpolationService.getStaticImageUrl( - '/icons/icon_change.png'); + '/icons/icon_change.png' + ); this.peopleIconUrl = this.urlInterpolationService.getStaticImageUrl( - '/icons/icon_people.png'); + '/icons/icon_people.png' + ); this.agreeIconUrl = this.urlInterpolationService.getStaticImageUrl( - '/icons/icon_agree.png'); + '/icons/icon_agree.png' + ); this.serviceIconUrl = this.urlInterpolationService.getStaticImageUrl( - '/icons/icon_service.png'); + '/icons/icon_service.png' + ); this.partneringImgUrl = this.urlInterpolationService.getStaticImageUrl( - '/general/partnering_image.png'); + '/general/partnering_image.png' + ); this.org1Url = this.urlInterpolationService.getStaticImageUrl( - '/partner_logos/movimentoAmplia.png'); + '/partner_logos/movimentoAmplia.png' + ); this.org2Url = this.urlInterpolationService.getStaticImageUrl( - '/partner_logos/digitalCitizen.png'); + '/partner_logos/digitalCitizen.png' + ); this.org3Url = this.urlInterpolationService.getStaticImageUrl( - '/partner_logos/injazPalestine.png'); + '/partner_logos/injazPalestine.png' + ); this.org4Url = this.urlInterpolationService.getStaticImageUrl( - '/partner_logos/nairobits.png'); + '/partner_logos/nairobits.png' + ); this.org5Url = this.urlInterpolationService.getStaticImageUrl( - '/partner_logos/edri.png'); + '/partner_logos/edri.png' + ); this.org6Url = this.urlInterpolationService.getStaticImageUrl( - '/partner_logos/globalCommunities.png'); + '/partner_logos/globalCommunities.png' + ); this.partner1 = this.urlInterpolationService.getStaticImageUrl( - '/general/partner1.png'); + '/general/partner1.png' + ); this.partner2 = this.urlInterpolationService.getStaticImageUrl( - '/general/partner2.png'); + '/general/partner2.png' + ); this.partner3 = this.urlInterpolationService.getStaticImageUrl( - '/general/partner3.png'); + '/general/partner3.png' + ); this.learner1 = this.urlInterpolationService.getStaticImageUrl( - '/general/learner1.png'); + '/general/learner1.png' + ); this.learner2 = this.urlInterpolationService.getStaticImageUrl( - '/general/learner2.png'); + '/general/learner2.png' + ); this.learner3 = this.urlInterpolationService.getStaticImageUrl( - '/general/learner3.png'); + '/general/learner3.png' + ); } ngOnDestroy(): void { @@ -133,6 +152,9 @@ export class PartnershipsPageComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive( - 'partnershipsPage', - downgradeComponent({component: PartnershipsPageComponent})); +angular + .module('oppia') + .directive( + 'partnershipsPage', + downgradeComponent({component: PartnershipsPageComponent}) + ); diff --git a/core/templates/pages/partnerships-page/partnerships-page.module.ts b/core/templates/pages/partnerships-page/partnerships-page.module.ts index 380c645b73d7..3a26fd37b3b1 100644 --- a/core/templates/pages/partnerships-page/partnerships-page.module.ts +++ b/core/templates/pages/partnerships-page/partnerships-page.module.ts @@ -16,29 +16,22 @@ * @fileoverview Module for the partnerships page. */ -import { NgModule } from '@angular/core'; -import { PartnershipsPageComponent } from './partnerships-page.component'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { PartnershipsPageRootComponent } from - './partnerships-page-root.component'; -import { CommonModule } from '@angular/common'; -import { PartnershipsPageRoutingModule } from './partnerships-page-routing.module'; -import { NgbCarouselModule } from '@ng-bootstrap/ng-bootstrap'; +import {NgModule} from '@angular/core'; +import {PartnershipsPageComponent} from './partnerships-page.component'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {PartnershipsPageRootComponent} from './partnerships-page-root.component'; +import {CommonModule} from '@angular/common'; +import {PartnershipsPageRoutingModule} from './partnerships-page-routing.module'; +import {NgbCarouselModule} from '@ng-bootstrap/ng-bootstrap'; @NgModule({ imports: [ CommonModule, SharedComponentsModule, PartnershipsPageRoutingModule, - NgbCarouselModule + NgbCarouselModule, ], - declarations: [ - PartnershipsPageComponent, - PartnershipsPageRootComponent - ], - entryComponents: [ - PartnershipsPageComponent, - PartnershipsPageRootComponent - ] + declarations: [PartnershipsPageComponent, PartnershipsPageRootComponent], + entryComponents: [PartnershipsPageComponent, PartnershipsPageRootComponent], }) export class PartnershipsPageModule {} diff --git a/core/templates/pages/pending-account-deletion-page/pending-account-deletion-page-root.component.spec.ts b/core/templates/pages/pending-account-deletion-page/pending-account-deletion-page-root.component.spec.ts index 31aef5784173..23e8b05aab59 100644 --- a/core/templates/pages/pending-account-deletion-page/pending-account-deletion-page-root.component.spec.ts +++ b/core/templates/pages/pending-account-deletion-page/pending-account-deletion-page-root.component.spec.ts @@ -16,16 +16,16 @@ * @fileoverview Unit tests for the pending account deletion root component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { PendingAccountDeletionPageRootComponent } from './pending-account-deletion-page-root.component'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {PendingAccountDeletionPageRootComponent} from './pending-account-deletion-page-root.component'; class MockTranslateService { onLangChange: EventEmitter = new EventEmitter(); @@ -42,21 +42,19 @@ describe('Pending Account Deletion Page Root', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], + imports: [HttpClientTestingModule], declarations: [ PendingAccountDeletionPageRootComponent, - MockTranslatePipe + MockTranslatePipe, ], providers: [ PageHeadService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -67,10 +65,9 @@ describe('Pending Account Deletion Page Root', () => { translateService = TestBed.inject(TranslateService); }); - it('should successfully instantiate the component', - () => { - expect(component).toBeDefined(); - }); + it('should successfully instantiate the component', () => { + expect(component).toBeDefined(); + }); it('should initialize and subscribe to onLangChange', () => { spyOn(translateService.onLangChange, 'subscribe'); @@ -97,13 +94,13 @@ describe('Pending Account Deletion Page Root', () => { component.setPageTitleAndMetaTags(); expect(translateService.instant).toHaveBeenCalledWith( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND - .PENDING_ACCOUNT_DELETION.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PENDING_ACCOUNT_DELETION.TITLE + ); expect(pageHeadService.updateTitleAndMetaTags).toHaveBeenCalledWith( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND - .PENDING_ACCOUNT_DELETION.TITLE, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND - .PENDING_ACCOUNT_DELETION.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PENDING_ACCOUNT_DELETION + .TITLE, + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PENDING_ACCOUNT_DELETION.META + ); }); it('should unsubscribe on component destruction', () => { diff --git a/core/templates/pages/pending-account-deletion-page/pending-account-deletion-page-root.component.ts b/core/templates/pages/pending-account-deletion-page/pending-account-deletion-page-root.component.ts index 41a6dc662932..cb9ffca3f250 100644 --- a/core/templates/pages/pending-account-deletion-page/pending-account-deletion-page-root.component.ts +++ b/core/templates/pages/pending-account-deletion-page/pending-account-deletion-page-root.component.ts @@ -16,16 +16,16 @@ * @fileoverview Root component for pending account deletion Page. */ -import { Component, OnDestroy } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; +import {Component, OnDestroy} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; @Component({ selector: 'oppia-pending-account-deletion-page-root', - templateUrl: './pending-account-deletion-page-root.component.html' + templateUrl: './pending-account-deletion-page-root.component.html', }) export class PendingAccountDeletionPageRootComponent implements OnDestroy { directiveSubscriptions = new Subscription(); @@ -41,9 +41,12 @@ export class PendingAccountDeletionPageRootComponent implements OnDestroy { let pendingAccountDeletionPage = AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PENDING_ACCOUNT_DELETION; let translatedTitle = this.translateService.instant( - pendingAccountDeletionPage.TITLE); + pendingAccountDeletionPage.TITLE + ); this.pageHeadService.updateTitleAndMetaTags( - translatedTitle, pendingAccountDeletionPage.META); + translatedTitle, + pendingAccountDeletionPage.META + ); } ngOnInit(): void { diff --git a/core/templates/pages/pending-account-deletion-page/pending-account-deletion-page-routing.module.ts b/core/templates/pages/pending-account-deletion-page/pending-account-deletion-page-routing.module.ts index be9f7d04f788..130dac67a440 100644 --- a/core/templates/pages/pending-account-deletion-page/pending-account-deletion-page-routing.module.ts +++ b/core/templates/pages/pending-account-deletion-page/pending-account-deletion-page-routing.module.ts @@ -16,25 +16,19 @@ * @fileoverview Routing module for pending account deletion page. */ -import { NgModule } from '@angular/core'; -import { Route, RouterModule } from '@angular/router'; -import { PendingAccountDeletionPageRootComponent } from './pending-account-deletion-page-root.component'; - +import {NgModule} from '@angular/core'; +import {Route, RouterModule} from '@angular/router'; +import {PendingAccountDeletionPageRootComponent} from './pending-account-deletion-page-root.component'; const routes: Route[] = [ { path: '', - component: PendingAccountDeletionPageRootComponent - } + component: PendingAccountDeletionPageRootComponent, + }, ]; @NgModule({ - imports: [ - RouterModule.forChild(routes) - ], - exports: [ - RouterModule - ] + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], }) - export class PendingAccountDeletionPageRoutingModule {} diff --git a/core/templates/pages/pending-account-deletion-page/pending-account-deletion-page.component.ts b/core/templates/pages/pending-account-deletion-page/pending-account-deletion-page.component.ts index 77dcf0a91528..b537f1a790af 100644 --- a/core/templates/pages/pending-account-deletion-page/pending-account-deletion-page.component.ts +++ b/core/templates/pages/pending-account-deletion-page/pending-account-deletion-page.component.ts @@ -16,14 +16,18 @@ * @fileoverview Pending account deletion page component. */ -import { Component } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; @Component({ selector: 'oppia-pending-account-deletion-page', - templateUrl: './pending-account-deletion-page.component.html' + templateUrl: './pending-account-deletion-page.component.html', }) export class PendingAccountDeletionPageComponent {} -angular.module('oppia').directive('oppiaPendingAccountDeletionPage', - downgradeComponent({ component: PendingAccountDeletionPageComponent })); +angular + .module('oppia') + .directive( + 'oppiaPendingAccountDeletionPage', + downgradeComponent({component: PendingAccountDeletionPageComponent}) + ); diff --git a/core/templates/pages/pending-account-deletion-page/pending-account-deletion-page.module.ts b/core/templates/pages/pending-account-deletion-page/pending-account-deletion-page.module.ts index 91a05a4b7f31..b513fe494e2c 100644 --- a/core/templates/pages/pending-account-deletion-page/pending-account-deletion-page.module.ts +++ b/core/templates/pages/pending-account-deletion-page/pending-account-deletion-page.module.ts @@ -16,15 +16,15 @@ * @fileoverview Module for the pending account deletion page. */ -import { NgModule } from '@angular/core'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { PendingAccountDeletionPageComponent } from './pending-account-deletion-page.component'; -import { SharedPipesModule } from 'filters/shared-pipes.module'; -import { TranslateModule } from '@ngx-translate/core'; -import { PendingAccountDeletionPageRootComponent } from './pending-account-deletion-page-root.component'; -import { CommonModule } from '@angular/common'; -import { PendingAccountDeletionPageRoutingModule } from './pending-account-deletion-page-routing.module'; -import { Error404PageModule } from 'pages/error-pages/error-404/error-404-page.module'; +import {NgModule} from '@angular/core'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {PendingAccountDeletionPageComponent} from './pending-account-deletion-page.component'; +import {SharedPipesModule} from 'filters/shared-pipes.module'; +import {TranslateModule} from '@ngx-translate/core'; +import {PendingAccountDeletionPageRootComponent} from './pending-account-deletion-page-root.component'; +import {CommonModule} from '@angular/common'; +import {PendingAccountDeletionPageRoutingModule} from './pending-account-deletion-page-routing.module'; +import {Error404PageModule} from 'pages/error-pages/error-404/error-404-page.module'; @NgModule({ imports: [ @@ -33,7 +33,7 @@ import { Error404PageModule } from 'pages/error-pages/error-404/error-404-page.m SharedPipesModule, TranslateModule, PendingAccountDeletionPageRoutingModule, - Error404PageModule + Error404PageModule, ], declarations: [ PendingAccountDeletionPageComponent, @@ -42,6 +42,6 @@ import { Error404PageModule } from 'pages/error-pages/error-404/error-404-page.m entryComponents: [ PendingAccountDeletionPageComponent, PendingAccountDeletionPageRootComponent, - ] + ], }) export class PendingAccountDeletionPageModule {} diff --git a/core/templates/pages/practice-session-page/practice-session-backend-api.service.spec.ts b/core/templates/pages/practice-session-page/practice-session-backend-api.service.spec.ts index ecebbfb32bb7..e8405babf5b9 100644 --- a/core/templates/pages/practice-session-page/practice-session-backend-api.service.spec.ts +++ b/core/templates/pages/practice-session-page/practice-session-backend-api.service.spec.ts @@ -16,10 +16,12 @@ * @fileoverview Testing of Service Practice Sessions page. */ -import { HttpClientTestingModule, HttpTestingController } from - '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; -import { PracticeSessionsBackendApiService } from './practice-session-backend-api.service'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; +import {PracticeSessionsBackendApiService} from './practice-session-backend-api.service'; describe('Review test backend API service', () => { let practiceSessionsBackendApiService: PracticeSessionsBackendApiService; @@ -29,10 +31,11 @@ describe('Review test backend API service', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [PracticeSessionsBackendApiService] + providers: [PracticeSessionsBackendApiService], }); - practiceSessionsBackendApiService = ( - TestBed.inject(PracticeSessionsBackendApiService)); + practiceSessionsBackendApiService = TestBed.inject( + PracticeSessionsBackendApiService + ); httpTestingController = TestBed.inject(HttpTestingController); }); @@ -40,49 +43,46 @@ describe('Review test backend API service', () => { httpTestingController.verify(); }); - it('should use the rejection handler if the backend request failed', - fakeAsync(() => { - var successHandler = jasmine.createSpy('success'); - var failHandler = jasmine.createSpy('fail'); + it('should use the rejection handler if the backend request failed', fakeAsync(() => { + var successHandler = jasmine.createSpy('success'); + var failHandler = jasmine.createSpy('fail'); - practiceSessionsBackendApiService.fetchPracticeSessionsData('0').then( - successHandler, failHandler); + practiceSessionsBackendApiService + .fetchPracticeSessionsData('0') + .then(successHandler, failHandler); - var req = httpTestingController.expectOne( - '0'); - expect(req.request.method).toEqual('GET'); - req.flush('Error loading data.', { - status: ERROR_STATUS_CODE, statusText: 'Invalid Request' - }); + var req = httpTestingController.expectOne('0'); + expect(req.request.method).toEqual('GET'); + req.flush('Error loading data.', { + status: ERROR_STATUS_CODE, + statusText: 'Invalid Request', + }); - flushMicrotasks(); + flushMicrotasks(); - expect(successHandler).not.toHaveBeenCalled(); - expect(failHandler).toHaveBeenCalled(); - }) - ); + expect(successHandler).not.toHaveBeenCalled(); + expect(failHandler).toHaveBeenCalled(); + })); - it('should successfully fetch an practice test data from the backend', - fakeAsync(() => { - var successHandler = jasmine.createSpy('success'); - var failHandler = jasmine.createSpy('fail'); + it('should successfully fetch an practice test data from the backend', fakeAsync(() => { + var successHandler = jasmine.createSpy('success'); + var failHandler = jasmine.createSpy('fail'); - practiceSessionsBackendApiService.fetchPracticeSessionsData('0').then( - successHandler, failHandler); + practiceSessionsBackendApiService + .fetchPracticeSessionsData('0') + .then(successHandler, failHandler); - var sampleDataResults = { - skill_ids_to_descriptions_map: {story: 'Story Name'}, - topic_name: 'topic_name' - }; - var req = httpTestingController.expectOne( - '0'); - expect(req.request.method).toEqual('GET'); - req.flush(sampleDataResults); + var sampleDataResults = { + skill_ids_to_descriptions_map: {story: 'Story Name'}, + topic_name: 'topic_name', + }; + var req = httpTestingController.expectOne('0'); + expect(req.request.method).toEqual('GET'); + req.flush(sampleDataResults); - flushMicrotasks(); + flushMicrotasks(); - expect(successHandler).toHaveBeenCalled(); - expect(failHandler).not.toHaveBeenCalled(); - }) - ); + expect(successHandler).toHaveBeenCalled(); + expect(failHandler).not.toHaveBeenCalled(); + })); }); diff --git a/core/templates/pages/practice-session-page/practice-session-backend-api.service.ts b/core/templates/pages/practice-session-page/practice-session-backend-api.service.ts index 139c37654443..707c96c29f4a 100644 --- a/core/templates/pages/practice-session-page/practice-session-backend-api.service.ts +++ b/core/templates/pages/practice-session-page/practice-session-backend-api.service.ts @@ -16,9 +16,9 @@ * @fileoverview Service to get data of Practice Sessions page. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; interface PracticeSessionsData { skill_ids_to_descriptions_map: Record; @@ -26,30 +26,37 @@ interface PracticeSessionsData { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class PracticeSessionsBackendApiService { - constructor( - private http: HttpClient, - ) {} + constructor(private http: HttpClient) {} - async _fetchPracticeSessionsData(practiceSessionsDataUrl: string): - Promise { - return this.http.get( - practiceSessionsDataUrl - ).toPromise().then(backendResponse => { - return backendResponse; - }, errorResponse => { - throw new Error(errorResponse.error.error); - }); + async _fetchPracticeSessionsData( + practiceSessionsDataUrl: string + ): Promise { + return this.http + .get(practiceSessionsDataUrl) + .toPromise() + .then( + backendResponse => { + return backendResponse; + }, + errorResponse => { + throw new Error(errorResponse.error.error); + } + ); } - async fetchPracticeSessionsData(storyUrlFragment: string): - Promise { + async fetchPracticeSessionsData( + storyUrlFragment: string + ): Promise { return this._fetchPracticeSessionsData(storyUrlFragment); } } -angular.module('oppia').factory( - 'PracticeSessionsBackendApiService', - downgradeInjectable(PracticeSessionsBackendApiService)); +angular + .module('oppia') + .factory( + 'PracticeSessionsBackendApiService', + downgradeInjectable(PracticeSessionsBackendApiService) + ); diff --git a/core/templates/pages/practice-session-page/practice-session-page.component.spec.ts b/core/templates/pages/practice-session-page/practice-session-page.component.spec.ts index 5b947950a553..e3657760bb31 100644 --- a/core/templates/pages/practice-session-page/practice-session-page.component.spec.ts +++ b/core/templates/pages/practice-session-page/practice-session-page.component.spec.ts @@ -16,17 +16,28 @@ * @fileoverview Unit tests for practice session page component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { TranslateFakeLoader, TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; -import { PracticeSessionPageComponent } from 'pages/practice-session-page/practice-session-page.component'; -import { UrlService } from 'services/contextual/url.service'; -import { CsrfTokenService } from 'services/csrf-token.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { LoaderService } from 'services/loader.service'; -import { PageTitleService } from 'services/page-title.service'; -import { PracticeSessionsBackendApiService } from './practice-session-backend-api.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import { + TranslateFakeLoader, + TranslateLoader, + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; +import {PracticeSessionPageComponent} from 'pages/practice-session-page/practice-session-page.component'; +import {UrlService} from 'services/contextual/url.service'; +import {CsrfTokenService} from 'services/csrf-token.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {LoaderService} from 'services/loader.service'; +import {PageTitleService} from 'services/page-title.service'; +import {PracticeSessionsBackendApiService} from './practice-session-backend-api.service'; describe('Practice session page', () => { let component: PracticeSessionPageComponent; @@ -46,13 +57,11 @@ describe('Practice session page', () => { TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: TranslateFakeLoader - } - }) - ], - declarations: [ - PracticeSessionPageComponent, + useClass: TranslateFakeLoader, + }, + }), ], + declarations: [PracticeSessionPageComponent], providers: [ TranslateService, PracticeSessionsBackendApiService, @@ -62,7 +71,7 @@ describe('Practice session page', () => { LoaderService, I18nLanguageCodeService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -76,68 +85,82 @@ describe('Practice session page', () => { loaderService = TestBed.inject(LoaderService); translateService = TestBed.inject(TranslateService); i18nLanguageCodeService = TestBed.inject(I18nLanguageCodeService); - practiceSessionsBackendApiService = ( - TestBed.inject(PracticeSessionsBackendApiService)); + practiceSessionsBackendApiService = TestBed.inject( + PracticeSessionsBackendApiService + ); spyOn(translateService, 'use').and.stub(); spyOn(translateService, 'instant').and.returnValue('translatedTitle'); - spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl') - .and.returnValue('abbrev-topic'); - spyOn(urlService, 'getSelectedSubtopicsFromUrl') - .and.returnValue('["1","2","3","4","5"]'); - spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl') - .and.returnValue('math'); - spyOn(csrfTokenService, 'getTokenAsync') - .and.returnValue(Promise.resolve('sample-csrf-token')); + spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue( + 'abbrev-topic' + ); + spyOn(urlService, 'getSelectedSubtopicsFromUrl').and.returnValue( + '["1","2","3","4","5"]' + ); + spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl').and.returnValue( + 'math' + ); + spyOn(csrfTokenService, 'getTokenAsync').and.returnValue( + Promise.resolve('sample-csrf-token') + ); fixture.detectChanges(); }); - it('should load topic based on its id on url when component is initialized' + - ' and subscribe to languageCodeChange emitter', fakeAsync(() => { - spyOn(loaderService, 'hideLoadingScreen'); - spyOn(component, 'subscribeToOnLanguageCodeChange'); - - spyOn(practiceSessionsBackendApiService, 'fetchPracticeSessionsData') - .and.returnValue(Promise.resolve({ - skill_ids_to_descriptions_map: { - skill_1: 'Description 1', - skill_2: 'Description 2', - }, - topic_name: 'Foo Topic' - })); - - component.ngOnInit(); - tick(); - - expect(component.topicName).toBe('Foo Topic'); - expect(component.stringifiedSubtopicIds).toBe('["1","2","3","4","5"]'); - expect(component.questionPlayerConfig).toEqual({ - resultActionButtons: [ - { - type: 'REVIEW_LOWEST_SCORED_SKILL', - i18nId: 'I18N_QUESTION_PLAYER_REVIEW_LOWEST_SCORED_SKILL' - }, - { - type: 'DASHBOARD', - i18nId: 'I18N_QUESTION_PLAYER_MY_DASHBOARD', - url: '/learn/math/abbrev-topic' - }, - { - type: 'RETRY_SESSION', - i18nId: 'I18N_QUESTION_PLAYER_NEW_SESSION', - url: '/learn/math/abbrev-topic/practice/session?' + - 'selected_subtopic_ids=' + encodeURIComponent('["1","2","3","4","5"]') - } - ], - skillList: ['skill_1', 'skill_2'], - skillDescriptions: ['Description 1', 'Description 2'], - questionCount: 20, - questionsSortedByDifficulty: false - }); - expect(component.subscribeToOnLanguageCodeChange).toHaveBeenCalled(); - expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); - })); + it( + 'should load topic based on its id on url when component is initialized' + + ' and subscribe to languageCodeChange emitter', + fakeAsync(() => { + spyOn(loaderService, 'hideLoadingScreen'); + spyOn(component, 'subscribeToOnLanguageCodeChange'); + + spyOn( + practiceSessionsBackendApiService, + 'fetchPracticeSessionsData' + ).and.returnValue( + Promise.resolve({ + skill_ids_to_descriptions_map: { + skill_1: 'Description 1', + skill_2: 'Description 2', + }, + topic_name: 'Foo Topic', + }) + ); + + component.ngOnInit(); + tick(); + + expect(component.topicName).toBe('Foo Topic'); + expect(component.stringifiedSubtopicIds).toBe('["1","2","3","4","5"]'); + expect(component.questionPlayerConfig).toEqual({ + resultActionButtons: [ + { + type: 'REVIEW_LOWEST_SCORED_SKILL', + i18nId: 'I18N_QUESTION_PLAYER_REVIEW_LOWEST_SCORED_SKILL', + }, + { + type: 'DASHBOARD', + i18nId: 'I18N_QUESTION_PLAYER_MY_DASHBOARD', + url: '/learn/math/abbrev-topic', + }, + { + type: 'RETRY_SESSION', + i18nId: 'I18N_QUESTION_PLAYER_NEW_SESSION', + url: + '/learn/math/abbrev-topic/practice/session?' + + 'selected_subtopic_ids=' + + encodeURIComponent('["1","2","3","4","5"]'), + }, + ], + skillList: ['skill_1', 'skill_2'], + skillDescriptions: ['Description 1', 'Description 2'], + questionCount: 20, + questionsSortedByDifficulty: false, + }); + expect(component.subscribeToOnLanguageCodeChange).toHaveBeenCalled(); + expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); + }) + ); it('should subscribe to onLanguageCodeChange', () => { spyOn(component.directiveSubscriptions, 'add'); @@ -146,8 +169,9 @@ describe('Practice session page', () => { component.subscribeToOnLanguageCodeChange(); expect(component.directiveSubscriptions.add).toHaveBeenCalled(); - expect(i18nLanguageCodeService.onI18nLanguageCodeChange.subscribe) - .toHaveBeenCalled(); + expect( + i18nLanguageCodeService.onI18nLanguageCodeChange.subscribe + ).toHaveBeenCalled(); }); it('should update title whenever the language changes', () => { @@ -165,8 +189,9 @@ describe('Practice session page', () => { component.setPageTitle(); - expect(pageTitleService.setDocumentTitle) - .toHaveBeenCalledWith('translatedTitle'); + expect(pageTitleService.setDocumentTitle).toHaveBeenCalledWith( + 'translatedTitle' + ); }); it('should unsubscribe on component destruction', () => { diff --git a/core/templates/pages/practice-session-page/practice-session-page.component.ts b/core/templates/pages/practice-session-page/practice-session-page.component.ts index 5afcc9d0a5cc..1704921c75fe 100644 --- a/core/templates/pages/practice-session-page/practice-session-page.component.ts +++ b/core/templates/pages/practice-session-page/practice-session-page.component.ts @@ -16,22 +16,22 @@ * @fileoverview Component for the practice session. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; -import { UrlService } from 'services/contextual/url.service'; -import { PracticeSessionPageConstants } from 'pages/practice-session-page/practice-session-page.constants'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { QuestionPlayerConfig } from 'pages/exploration-player-page/learner-experience/ratings-and-recommendations.component'; -import { LoaderService } from 'services/loader.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { PageTitleService } from 'services/page-title.service'; -import { PracticeSessionsBackendApiService } from './practice-session-backend-api.service'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; +import {UrlService} from 'services/contextual/url.service'; +import {PracticeSessionPageConstants} from 'pages/practice-session-page/practice-session-page.constants'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {QuestionPlayerConfig} from 'pages/exploration-player-page/learner-experience/ratings-and-recommendations.component'; +import {LoaderService} from 'services/loader.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {PageTitleService} from 'services/page-title.service'; +import {PracticeSessionsBackendApiService} from './practice-session-backend-api.service'; @Component({ selector: 'practice-session-page', - templateUrl: './practice-session-page.component.html' + templateUrl: './practice-session-page.component.html', }) export class PracticeSessionPageComponent implements OnInit, OnDestroy { directiveSubscriptions = new Subscription(); @@ -54,11 +54,13 @@ export class PracticeSessionPageComponent implements OnInit, OnDestroy { setPageTitle(): void { this.translateService.use( - this.i18nLanguageCodeService.getCurrentI18nLanguageCode()); + this.i18nLanguageCodeService.getCurrentI18nLanguageCode() + ); const translatedTitle = this.translateService.instant( - 'I18N_PRACTICE_SESSION_PAGE_TITLE', { - topicName: this.topicName + 'I18N_PRACTICE_SESSION_PAGE_TITLE', + { + topicName: this.topicName, } ); @@ -74,72 +76,76 @@ export class PracticeSessionPageComponent implements OnInit, OnDestroy { } _fetchSkillDetails(): void { - const topicUrlFragment = ( - this.urlService.getTopicUrlFragmentFromLearnerUrl()); - const practiceSessionsDataUrl = this.urlInterpolationService - .interpolateUrl( - PracticeSessionPageConstants.PRACTICE_SESSIONS_DATA_URL, { - topic_url_fragment: topicUrlFragment, - classroom_url_fragment: ( - this.urlService.getClassroomUrlFragmentFromLearnerUrl()), - stringified_subtopic_ids: this.stringifiedSubtopicIds - }); + const topicUrlFragment = + this.urlService.getTopicUrlFragmentFromLearnerUrl(); + const practiceSessionsDataUrl = this.urlInterpolationService.interpolateUrl( + PracticeSessionPageConstants.PRACTICE_SESSIONS_DATA_URL, + { + topic_url_fragment: topicUrlFragment, + classroom_url_fragment: + this.urlService.getClassroomUrlFragmentFromLearnerUrl(), + stringified_subtopic_ids: this.stringifiedSubtopicIds, + } + ); const practiceSessionsUrl = this.urlInterpolationService.interpolateUrl( - PracticeSessionPageConstants.PRACTICE_SESSIONS_URL, { + PracticeSessionPageConstants.PRACTICE_SESSIONS_URL, + { topic_url_fragment: topicUrlFragment, - classroom_url_fragment: ( - this.urlService.getClassroomUrlFragmentFromLearnerUrl()), - stringified_subtopic_ids: this.stringifiedSubtopicIds - }); + classroom_url_fragment: + this.urlService.getClassroomUrlFragmentFromLearnerUrl(), + stringified_subtopic_ids: this.stringifiedSubtopicIds, + } + ); const topicViewerUrl = this.urlInterpolationService.interpolateUrl( - PracticeSessionPageConstants.TOPIC_VIEWER_PAGE, { + PracticeSessionPageConstants.TOPIC_VIEWER_PAGE, + { topic_url_fragment: topicUrlFragment, - classroom_url_fragment: ( - this.urlService.getClassroomUrlFragmentFromLearnerUrl()), - }); - - this.practiceSessionsBackendApiService.fetchPracticeSessionsData( - practiceSessionsDataUrl).then((result) => { - const skillList = []; - const skillDescriptions = []; - for (let skillId in result.skill_ids_to_descriptions_map) { - skillList.push(skillId); - skillDescriptions.push( - result.skill_ids_to_descriptions_map[skillId]); + classroom_url_fragment: + this.urlService.getClassroomUrlFragmentFromLearnerUrl(), } - this.questionPlayerConfig = { - resultActionButtons: [ - { - type: 'REVIEW_LOWEST_SCORED_SKILL', - i18nId: 'I18N_QUESTION_PLAYER_REVIEW_LOWEST_SCORED_SKILL' - }, - { - type: 'DASHBOARD', - i18nId: 'I18N_QUESTION_PLAYER_MY_DASHBOARD', - url: topicViewerUrl - }, - { - type: 'RETRY_SESSION', - i18nId: 'I18N_QUESTION_PLAYER_NEW_SESSION', - url: practiceSessionsUrl - }, - ], - skillList: skillList, - skillDescriptions: skillDescriptions, - questionCount: PracticeSessionPageConstants.TOTAL_QUESTIONS, - questionsSortedByDifficulty: false - }; - this.topicName = result.topic_name; - this.setPageTitle(); - this.subscribeToOnLanguageCodeChange(); - this.loaderService.hideLoadingScreen(); - }); + ); + + this.practiceSessionsBackendApiService + .fetchPracticeSessionsData(practiceSessionsDataUrl) + .then(result => { + const skillList = []; + const skillDescriptions = []; + for (let skillId in result.skill_ids_to_descriptions_map) { + skillList.push(skillId); + skillDescriptions.push(result.skill_ids_to_descriptions_map[skillId]); + } + this.questionPlayerConfig = { + resultActionButtons: [ + { + type: 'REVIEW_LOWEST_SCORED_SKILL', + i18nId: 'I18N_QUESTION_PLAYER_REVIEW_LOWEST_SCORED_SKILL', + }, + { + type: 'DASHBOARD', + i18nId: 'I18N_QUESTION_PLAYER_MY_DASHBOARD', + url: topicViewerUrl, + }, + { + type: 'RETRY_SESSION', + i18nId: 'I18N_QUESTION_PLAYER_NEW_SESSION', + url: practiceSessionsUrl, + }, + ], + skillList: skillList, + skillDescriptions: skillDescriptions, + questionCount: PracticeSessionPageConstants.TOTAL_QUESTIONS, + questionsSortedByDifficulty: false, + }; + this.topicName = result.topic_name; + this.setPageTitle(); + this.subscribeToOnLanguageCodeChange(); + this.loaderService.hideLoadingScreen(); + }); } ngOnInit(): void { this.topicName = this.urlService.getTopicUrlFragmentFromLearnerUrl(); - this.stringifiedSubtopicIds = ( - this.urlService.getSelectedSubtopicsFromUrl()); + this.stringifiedSubtopicIds = this.urlService.getSelectedSubtopicsFromUrl(); this._fetchSkillDetails(); } @@ -148,7 +154,9 @@ export class PracticeSessionPageComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive('practiceSessionPage', +angular.module('oppia').directive( + 'practiceSessionPage', downgradeComponent({ - component: PracticeSessionPageComponent - }) as angular.IDirectiveFactory); + component: PracticeSessionPageComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/practice-session-page/practice-session-page.constants.ajs.ts b/core/templates/pages/practice-session-page/practice-session-page.constants.ajs.ts index c7901bccdb36..6034f9a261fb 100644 --- a/core/templates/pages/practice-session-page/practice-session-page.constants.ajs.ts +++ b/core/templates/pages/practice-session-page/practice-session-page.constants.ajs.ts @@ -18,19 +18,29 @@ // TODO(#7092): Delete this file once migration is complete and these AngularJS // equivalents of the Angular constants are no longer needed. -import { PracticeSessionPageConstants } from - 'pages/practice-session-page/practice-session-page.constants'; +import {PracticeSessionPageConstants} from 'pages/practice-session-page/practice-session-page.constants'; -angular.module('oppia').constant( - 'TOTAL_QUESTIONS', PracticeSessionPageConstants.TOTAL_QUESTIONS); +angular + .module('oppia') + .constant('TOTAL_QUESTIONS', PracticeSessionPageConstants.TOTAL_QUESTIONS); -angular.module('oppia').constant( - 'PRACTICE_SESSIONS_DATA_URL', - PracticeSessionPageConstants.PRACTICE_SESSIONS_DATA_URL); +angular + .module('oppia') + .constant( + 'PRACTICE_SESSIONS_DATA_URL', + PracticeSessionPageConstants.PRACTICE_SESSIONS_DATA_URL + ); -angular.module('oppia').constant( - 'TOPIC_VIEWER_PAGE', PracticeSessionPageConstants.TOPIC_VIEWER_PAGE); +angular + .module('oppia') + .constant( + 'TOPIC_VIEWER_PAGE', + PracticeSessionPageConstants.TOPIC_VIEWER_PAGE + ); -angular.module('oppia').constant( - 'PRACTICE_SESSIONS_URL', - PracticeSessionPageConstants.PRACTICE_SESSIONS_URL); +angular + .module('oppia') + .constant( + 'PRACTICE_SESSIONS_URL', + PracticeSessionPageConstants.PRACTICE_SESSIONS_URL + ); diff --git a/core/templates/pages/practice-session-page/practice-session-page.constants.ts b/core/templates/pages/practice-session-page/practice-session-page.constants.ts index 3161a3abbaea..06de11b99db6 100644 --- a/core/templates/pages/practice-session-page/practice-session-page.constants.ts +++ b/core/templates/pages/practice-session-page/practice-session-page.constants.ts @@ -19,15 +19,14 @@ export const PracticeSessionPageConstants = { TOTAL_QUESTIONS: 20, - PRACTICE_SESSIONS_DATA_URL: ( + PRACTICE_SESSIONS_DATA_URL: '/practice_session/data//' + '?selected_subtopic_ids=' + - ''), + '', - TOPIC_VIEWER_PAGE: ( - '/learn//'), + TOPIC_VIEWER_PAGE: '/learn//', - PRACTICE_SESSIONS_URL: ( + PRACTICE_SESSIONS_URL: '/learn///practice/' + - 'session?selected_subtopic_ids=') + 'session?selected_subtopic_ids=', } as const; diff --git a/core/templates/pages/practice-session-page/practice-session-page.import.ts b/core/templates/pages/practice-session-page/practice-session-page.import.ts index 325be4bd098e..8bf821e575fa 100644 --- a/core/templates/pages/practice-session-page/practice-session-page.import.ts +++ b/core/templates/pages/practice-session-page/practice-session-page.import.ts @@ -23,9 +23,15 @@ import uiValidate from 'angular-ui-validate'; import 'third-party-imports/ui-tree.import'; angular.module('oppia', [ - require('angular-cookies'), 'ngAnimate', - 'ngMaterial', 'ngSanitize', 'ngTouch', 'pascalprecht.translate', - 'ui.bootstrap', 'ui.tree', uiValidate + require('angular-cookies'), + 'ngAnimate', + 'ngMaterial', + 'ngSanitize', + 'ngTouch', + 'pascalprecht.translate', + 'ui.bootstrap', + 'ui.tree', + uiValidate, ]); require('Polyfills.ts'); diff --git a/core/templates/pages/practice-session-page/practice-session-page.module.ts b/core/templates/pages/practice-session-page/practice-session-page.module.ts index dd9cf3244ede..92aa1a84728d 100644 --- a/core/templates/pages/practice-session-page/practice-session-page.module.ts +++ b/core/templates/pages/practice-session-page/practice-session-page.module.ts @@ -16,24 +16,25 @@ * @fileoverview Module for the practice session page. */ -import { BrowserModule, HAMMER_GESTURE_CONFIG } from '@angular/platform-browser'; -import { APP_INITIALIZER, NgModule, StaticProvider } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; -import { RouterModule } from '@angular/router'; -import { APP_BASE_HREF } from '@angular/common'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import {BrowserModule, HAMMER_GESTURE_CONFIG} from '@angular/platform-browser'; +import {APP_INITIALIZER, NgModule, StaticProvider} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http'; +import {RouterModule} from '@angular/router'; +import {APP_BASE_HREF} from '@angular/common'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; -import { RequestInterceptor } from 'services/request-interceptor.service'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { OppiaAngularRootComponent } from - 'components/oppia-angular-root.component'; -import { InteractionExtensionsModule } from 'interactions/interactions.module'; -import { platformFeatureInitFactory, PlatformFeatureService } from - 'services/platform-feature.service'; -import { MyHammerConfig, toastrConfig } from 'pages/oppia-root/app.module'; -import { SmartRouterModule } from 'hybrid-router-module-provider'; -import { AppErrorHandlerProvider } from 'pages/oppia-root/app-error-handler'; +import {RequestInterceptor} from 'services/request-interceptor.service'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {OppiaAngularRootComponent} from 'components/oppia-angular-root.component'; +import {InteractionExtensionsModule} from 'interactions/interactions.module'; +import { + platformFeatureInitFactory, + PlatformFeatureService, +} from 'services/platform-feature.service'; +import {MyHammerConfig, toastrConfig} from 'pages/oppia-root/app.module'; +import {SmartRouterModule} from 'hybrid-router-module-provider'; +import {AppErrorHandlerProvider} from 'pages/oppia-root/app-error-handler'; @NgModule({ imports: [ @@ -46,48 +47,44 @@ import { AppErrorHandlerProvider } from 'pages/oppia-root/app-error-handler'; SmartRouterModule, RouterModule.forRoot([]), SharedComponentsModule, - ToastrModule.forRoot(toastrConfig) - ], - declarations: [ - PracticeSessionPageComponent - ], - entryComponents: [ - PracticeSessionPageComponent + ToastrModule.forRoot(toastrConfig), ], + declarations: [PracticeSessionPageComponent], + entryComponents: [PracticeSessionPageComponent], providers: [ { provide: HTTP_INTERCEPTORS, useClass: RequestInterceptor, - multi: true + multi: true, }, { provide: APP_INITIALIZER, useFactory: platformFeatureInitFactory, deps: [PlatformFeatureService], - multi: true + multi: true, }, { provide: HAMMER_GESTURE_CONFIG, - useClass: MyHammerConfig + useClass: MyHammerConfig, }, AppErrorHandlerProvider, { provide: APP_BASE_HREF, - useValue: '/' - } - ] + useValue: '/', + }, + ], }) class PracticeSessionPageModule { // Empty placeholder method to satisfy the `Compiler`. ngDoBootstrap() {} } -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { downgradeModule } from '@angular/upgrade/static'; -import { ToastrModule } from 'ngx-toastr'; -import { PracticeSessionPageComponent } from './practice-session-page.component'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {downgradeModule} from '@angular/upgrade/static'; +import {ToastrModule} from 'ngx-toastr'; +import {PracticeSessionPageComponent} from './practice-session-page.component'; -const bootstrapFnAsync = async(extraProviders: StaticProvider[]) => { +const bootstrapFnAsync = async (extraProviders: StaticProvider[]) => { const platformRef = platformBrowserDynamic(extraProviders); return platformRef.bootstrapModule(PracticeSessionPageModule); }; @@ -102,5 +99,6 @@ angular.module('oppia').directive( // bootstrap the Angular 8. 'oppiaAngularRoot', downgradeComponent({ - component: OppiaAngularRootComponent - }) as angular.IDirectiveFactory); + component: OppiaAngularRootComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/preferences-page/form-fields/preferred-language-selector.component.spec.ts b/core/templates/pages/preferences-page/form-fields/preferred-language-selector.component.spec.ts index 28e6ddaa16ed..702d89562dfe 100644 --- a/core/templates/pages/preferences-page/form-fields/preferred-language-selector.component.spec.ts +++ b/core/templates/pages/preferences-page/form-fields/preferred-language-selector.component.spec.ts @@ -16,12 +16,12 @@ * @fileoverview Unit tests for the preferred site language component. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { FormsModule } from '@angular/forms'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { MaterialModule } from 'modules/material.module'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { PreferredSiteLanguageSelectorComponent } from './preferred-language-selector.component'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {FormsModule} from '@angular/forms'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {MaterialModule} from 'modules/material.module'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {PreferredSiteLanguageSelectorComponent} from './preferred-language-selector.component'; describe('Preferred Site Language Selector Component', () => { let componentInstance: PreferredSiteLanguageSelectorComponent; @@ -29,15 +29,8 @@ describe('Preferred Site Language Selector Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - BrowserAnimationsModule, - MaterialModule, - FormsModule, - ], - declarations: [ - MockTranslatePipe, - PreferredSiteLanguageSelectorComponent - ] + imports: [BrowserAnimationsModule, MaterialModule, FormsModule], + declarations: [MockTranslatePipe, PreferredSiteLanguageSelectorComponent], }).compileComponents(); })); @@ -51,25 +44,31 @@ describe('Preferred Site Language Selector Component', () => { }); it('should initialize', () => { - componentInstance.choices = [{ - id: 'en', - text: 'english', - dir: 'ltr' - }]; + componentInstance.choices = [ + { + id: 'en', + text: 'english', + dir: 'ltr', + }, + ]; componentInstance.ngOnInit(); expect(componentInstance.filteredChoices).toEqual( - componentInstance.choices); + componentInstance.choices + ); }); it('should filter choices', () => { - componentInstance.choices = [{ - id: 'en', - text: 'english', - dir: 'ltr' - }]; + componentInstance.choices = [ + { + id: 'en', + text: 'english', + dir: 'ltr', + }, + ]; componentInstance.filterChoices('eng'); expect(componentInstance.filteredChoices).toEqual( - componentInstance.choices); + componentInstance.choices + ); }); it('should update preferred language', () => { diff --git a/core/templates/pages/preferences-page/form-fields/preferred-language-selector.component.ts b/core/templates/pages/preferences-page/form-fields/preferred-language-selector.component.ts index d5a91445d108..979657a862ad 100644 --- a/core/templates/pages/preferences-page/form-fields/preferred-language-selector.component.ts +++ b/core/templates/pages/preferences-page/form-fields/preferred-language-selector.component.ts @@ -16,7 +16,7 @@ * @fileoverview Component for the preferred language selector. */ -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import {Component, EventEmitter, Input, Output} from '@angular/core'; interface Language { id: string; @@ -26,7 +26,7 @@ interface Language { @Component({ selector: 'oppia-preferred-language-selector', - templateUrl: './preferred-language-selector.component.html' + templateUrl: './preferred-language-selector.component.html', }) export class PreferredSiteLanguageSelectorComponent { // These properties are initialized using Angular lifecycle hooks @@ -35,8 +35,8 @@ export class PreferredSiteLanguageSelectorComponent { @Input() preferredLanguageCode!: string; @Input() choices!: Language[]; @Input() entity!: string; - @Output() preferredLanguageCodeChange: EventEmitter = ( - new EventEmitter()); + @Output() preferredLanguageCodeChange: EventEmitter = + new EventEmitter(); filteredChoices!: Language[]; @@ -46,7 +46,8 @@ export class PreferredSiteLanguageSelectorComponent { filterChoices(searchTerm: string): void { this.filteredChoices = this.choices.filter( - lang => lang.text.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1); + lang => lang.text.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1 + ); } updateLanguage(code: string): void { diff --git a/core/templates/pages/preferences-page/form-fields/preferred-languages.component.spec.ts b/core/templates/pages/preferences-page/form-fields/preferred-languages.component.spec.ts index 8fff228445b1..12a22fbcbb49 100644 --- a/core/templates/pages/preferences-page/form-fields/preferred-languages.component.spec.ts +++ b/core/templates/pages/preferences-page/form-fields/preferred-languages.component.spec.ts @@ -16,13 +16,13 @@ * @fileoverview Unit tests for the preferred languages component. */ -import { ElementRef } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { MaterialModule } from 'modules/material.module'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { PreferredLanguagesComponent } from './preferred-languages.component'; +import {ElementRef} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {MaterialModule} from 'modules/material.module'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {PreferredLanguagesComponent} from './preferred-languages.component'; describe('Preferred Languages Component', () => { let componentInstance: PreferredLanguagesComponent; @@ -34,12 +34,9 @@ describe('Preferred Languages Component', () => { BrowserAnimationsModule, MaterialModule, FormsModule, - ReactiveFormsModule + ReactiveFormsModule, ], - declarations: [ - MockTranslatePipe, - PreferredLanguagesComponent - ] + declarations: [MockTranslatePipe, PreferredLanguagesComponent], }).compileComponents(); })); @@ -56,11 +53,13 @@ describe('Preferred Languages Component', () => { fixture.detectChanges(); let value = 'en'; componentInstance.preferredLanguages = []; - componentInstance.choices = [{ - id: 'en', - text: 'English', - ariaLabelInEnglish: 'English' - }]; + componentInstance.choices = [ + { + id: 'en', + text: 'English', + ariaLabelInEnglish: 'English', + }, + ]; componentInstance.formCtrl = new FormControl(value); componentInstance.ngAfterViewInit(); fixture.detectChanges(); @@ -77,39 +76,43 @@ describe('Preferred Languages Component', () => { it('should validate input', () => { componentInstance.preferredLanguages = []; - componentInstance.choices = [{ - id: 'en', - text: 'English', - ariaLabelInEnglish: 'English' - }]; - componentInstance.filteredChoices = [{ - id: 'en', - text: 'English', - ariaLabelInEnglish: 'English' - }]; + componentInstance.choices = [ + { + id: 'en', + text: 'English', + ariaLabelInEnglish: 'English', + }, + ]; + componentInstance.filteredChoices = [ + { + id: 'en', + text: 'English', + ariaLabelInEnglish: 'English', + }, + ]; expect(componentInstance.validInput('en')).toBeTrue(); }); it('should filter choices when search query is non-empty', () => { const mockChoices = [ - { id: 'en', text: 'English', ariaLabelInEnglish: 'English' }, - { id: 'fr', text: 'French', ariaLabelInEnglish: 'French' }, - { id: 'de', text: 'German', ariaLabelInEnglish: 'German' } + {id: 'en', text: 'English', ariaLabelInEnglish: 'English'}, + {id: 'fr', text: 'French', ariaLabelInEnglish: 'French'}, + {id: 'de', text: 'German', ariaLabelInEnglish: 'German'}, ]; componentInstance.choices = [...mockChoices]; componentInstance.searchQuery = 'en'; componentInstance.onSearchInputChange(); const expectedFilteredChoice = [ - { id: 'en', text: 'English', ariaLabelInEnglish: 'English' }, - { id: 'fr', text: 'French', ariaLabelInEnglish: 'French' }, + {id: 'en', text: 'English', ariaLabelInEnglish: 'English'}, + {id: 'fr', text: 'French', ariaLabelInEnglish: 'French'}, ]; expect(componentInstance.filteredChoices).toEqual(expectedFilteredChoice); }); it('should not show any choices when search query does not match', () => { const mockChoices = [ - { id: 'en', text: 'English', ariaLabelInEnglish: 'English' }, - { id: 'fr', text: 'French', ariaLabelInEnglish: 'French' }, + {id: 'en', text: 'English', ariaLabelInEnglish: 'English'}, + {id: 'fr', text: 'French', ariaLabelInEnglish: 'French'}, ]; componentInstance.choices = [...mockChoices]; componentInstance.searchQuery = 'de'; @@ -120,15 +123,17 @@ describe('Preferred Languages Component', () => { spyOn(componentInstance.preferredLanguagesChange, 'emit'); spyOn(componentInstance, 'validInput').and.returnValue(true); componentInstance.preferredLanguages = []; - componentInstance.choices = [{ - id: 'en', - text: 'English', - ariaLabelInEnglish: 'English' - }]; + componentInstance.choices = [ + { + id: 'en', + text: 'English', + ariaLabelInEnglish: 'English', + }, + ]; componentInstance.languageInput = { nativeElement: { - value: '' - } + value: '', + }, } as ElementRef; componentInstance.add({value: 'en'}); componentInstance.add({value: ''}); @@ -137,11 +142,13 @@ describe('Preferred Languages Component', () => { it('should remove language', () => { componentInstance.preferredLanguages = ['en']; - let choices = [{ - id: 'en', - text: 'English', - ariaLabelInEnglish: 'English' - }]; + let choices = [ + { + id: 'en', + text: 'English', + ariaLabelInEnglish: 'English', + }, + ]; componentInstance.choices = choices; componentInstance.remove('en'); expect(componentInstance.preferredLanguages).toEqual([]); @@ -152,13 +159,11 @@ describe('Preferred Languages Component', () => { spyOn(componentInstance, 'add'); spyOn(componentInstance, 'remove'); componentInstance.preferredLanguages = ['en']; - componentInstance.selected( - { option: { value: 'en' }}); + componentInstance.selected({option: {value: 'en'}}); expect(componentInstance.remove).toHaveBeenCalled(); expect(componentInstance.add).not.toHaveBeenCalled(); componentInstance.preferredLanguages = []; - componentInstance.selected( - { option: { value: 'en' }}); + componentInstance.selected({option: {value: 'en'}}); expect(componentInstance.add).toHaveBeenCalled(); }); }); diff --git a/core/templates/pages/preferences-page/form-fields/preferred-languages.component.ts b/core/templates/pages/preferences-page/form-fields/preferred-languages.component.ts index 9c61888f9c82..36d35f2d1e5d 100644 --- a/core/templates/pages/preferences-page/form-fields/preferred-languages.component.ts +++ b/core/templates/pages/preferences-page/form-fields/preferred-languages.component.ts @@ -16,15 +16,23 @@ * @fileoverview Preferred languages component. */ -import { ENTER } from '@angular/cdk/keycodes'; -import { AfterViewInit, Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; -import { FormControl } from '@angular/forms'; -import { MatChipList } from '@angular/material/chips'; -import { LanguageIdAndText } from 'domain/utilities/language-util.service'; +import {ENTER} from '@angular/cdk/keycodes'; +import { + AfterViewInit, + Component, + ElementRef, + EventEmitter, + Input, + Output, + ViewChild, +} from '@angular/core'; +import {FormControl} from '@angular/forms'; +import {MatChipList} from '@angular/material/chips'; +import {LanguageIdAndText} from 'domain/utilities/language-util.service'; @Component({ selector: 'oppia-preferred-languages', - templateUrl: './preferred-languages.component.html' + templateUrl: './preferred-languages.component.html', }) export class PreferredLanguagesComponent implements AfterViewInit { // These properties are initialized using Angular lifecycle hooks @@ -34,8 +42,8 @@ export class PreferredLanguagesComponent implements AfterViewInit { @ViewChild('languageInput') languageInput!: ElementRef; @Input() preferredLanguages!: string[]; @Input() choices!: LanguageIdAndText[]; - @Output() preferredLanguagesChange: EventEmitter = ( - new EventEmitter()); + @Output() preferredLanguagesChange: EventEmitter = + new EventEmitter(); selectable = true; removable = true; @@ -67,11 +75,12 @@ export class PreferredLanguagesComponent implements AfterViewInit { break; } } - return availableLanguage && - this.preferredLanguages.indexOf(value) < 0 ? true : false; + return availableLanguage && this.preferredLanguages.indexOf(value) < 0 + ? true + : false; } - add(event: { value: string }): void { + add(event: {value: string}): void { const value = (event.value || '').trim(); if (!value) { return; @@ -94,7 +103,7 @@ export class PreferredLanguagesComponent implements AfterViewInit { } } - selected(event: { option: { value: string } }): void { + selected(event: {option: {value: string}}): void { if (this.preferredLanguages.indexOf(event.option.value) > -1) { this.remove(event.option.value); } else { @@ -107,8 +116,8 @@ export class PreferredLanguagesComponent implements AfterViewInit { this.filteredChoices = this.choices.filter(choice => { const lowerSearchQuery = this.searchQuery.toLowerCase(); return ( - (choice.text.toLowerCase().includes(lowerSearchQuery)) || - (choice.id.toLowerCase().includes(lowerSearchQuery)) + choice.text.toLowerCase().includes(lowerSearchQuery) || + choice.id.toLowerCase().includes(lowerSearchQuery) ); }); } else { diff --git a/core/templates/pages/preferences-page/form-fields/subject-interests.component.spec.ts b/core/templates/pages/preferences-page/form-fields/subject-interests.component.spec.ts index ef0715b2f111..768bb1c266fb 100644 --- a/core/templates/pages/preferences-page/form-fields/subject-interests.component.spec.ts +++ b/core/templates/pages/preferences-page/form-fields/subject-interests.component.spec.ts @@ -16,13 +16,13 @@ * @fileoverview Unit tests for the subject interests component. */ -import { ElementRef } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { MaterialModule } from 'modules/material.module'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { SubjectInterestsComponent } from './subject-interests.component'; +import {ElementRef} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {MaterialModule} from 'modules/material.module'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {SubjectInterestsComponent} from './subject-interests.component'; describe('Subject interests form field Component', () => { let componentInstance: SubjectInterestsComponent; @@ -34,12 +34,9 @@ describe('Subject interests form field Component', () => { BrowserAnimationsModule, MaterialModule, FormsModule, - ReactiveFormsModule + ReactiveFormsModule, ], - declarations: [ - MockTranslatePipe, - SubjectInterestsComponent - ] + declarations: [MockTranslatePipe, SubjectInterestsComponent], }).compileComponents(); })); @@ -60,8 +57,8 @@ describe('Subject interests form field Component', () => { valueChanges: { subscribe: (callb: (value: string) => void) => { callb(input); - } - } + }, + }, } as FormControl; componentInstance.ngOnInit(); input = 'math'; @@ -69,12 +66,13 @@ describe('Subject interests form field Component', () => { valueChanges: { subscribe(callb: (val: string) => void) { callb(input); - } - } + }, + }, } as FormControl; componentInstance.ngOnInit(); expect(componentInstance.allSubjectInterests).toEqual( - componentInstance.subjectInterests); + componentInstance.subjectInterests + ); }); it('should validate input', () => { @@ -89,8 +87,8 @@ describe('Subject interests form field Component', () => { componentInstance.allSubjectInterests = []; componentInstance.subjectInterestInput = { nativeElement: { - value: '' - } + value: '', + }, } as ElementRef; componentInstance.add({value: 'math'}); componentInstance.add({value: ''}); @@ -108,13 +106,11 @@ describe('Subject interests form field Component', () => { spyOn(componentInstance, 'add'); spyOn(componentInstance, 'remove'); componentInstance.subjectInterests = ['math']; - componentInstance.selected( - { option: { value: 'math' }}); + componentInstance.selected({option: {value: 'math'}}); expect(componentInstance.remove).toHaveBeenCalled(); expect(componentInstance.add).not.toHaveBeenCalled(); componentInstance.subjectInterests = []; - componentInstance.selected( - { option: { value: 'math' }}); + componentInstance.selected({option: {value: 'math'}}); expect(componentInstance.add).toHaveBeenCalled(); }); diff --git a/core/templates/pages/preferences-page/form-fields/subject-interests.component.ts b/core/templates/pages/preferences-page/form-fields/subject-interests.component.ts index 1370de82d1b4..e1d8fe16d500 100644 --- a/core/templates/pages/preferences-page/form-fields/subject-interests.component.ts +++ b/core/templates/pages/preferences-page/form-fields/subject-interests.component.ts @@ -16,22 +16,28 @@ * @fileoverview Component for subject interests form field. */ -import { ENTER } from '@angular/cdk/keycodes'; -import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; -import { FormControl } from '@angular/forms'; -import { MatChipList } from '@angular/material/chips'; +import {ENTER} from '@angular/cdk/keycodes'; +import { + Component, + ElementRef, + EventEmitter, + Input, + Output, + ViewChild, +} from '@angular/core'; +import {FormControl} from '@angular/forms'; +import {MatChipList} from '@angular/material/chips'; import cloneDeep from 'lodash/cloneDeep'; -import { Observable } from 'rxjs'; -import { map, startWith } from 'rxjs/operators'; +import {Observable} from 'rxjs'; +import {map, startWith} from 'rxjs/operators'; @Component({ selector: 'oppia-subject-interests', - templateUrl: './subject-interests.component.html' + templateUrl: './subject-interests.component.html', }) export class SubjectInterestsComponent { @Input() subjectInterests: string[] = []; - @Output() subjectInterestsChange: EventEmitter = ( - new EventEmitter()); + @Output() subjectInterestsChange: EventEmitter = new EventEmitter(); selectable = true; removable = true; @@ -43,14 +49,16 @@ export class SubjectInterestsComponent { // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @ViewChild('chipList') chipList!: MatChipList; - @ViewChild('subjectInterestInput') subjectInterestInput!: - ElementRef; + @ViewChild('subjectInterestInput') + subjectInterestInput!: ElementRef; constructor() { this.filteredSubjectInterests = this.formCtrl.valueChanges.pipe( startWith(null), - map((interest: string | null) => interest ? this.filter( - interest) : this.allSubjectInterests.slice())); + map((interest: string | null) => + interest ? this.filter(interest) : this.allSubjectInterests.slice() + ) + ); } ngOnInit(): void { @@ -66,10 +74,12 @@ export class SubjectInterestsComponent { validInput(value: string): boolean { return value === value.toLowerCase() && - this.subjectInterests.indexOf(value) < 0 ? true : false; + this.subjectInterests.indexOf(value) < 0 + ? true + : false; } - add(event: { value: string }): void { + add(event: {value: string}): void { const value = (event.value || '').trim(); if (!value) { return; @@ -94,7 +104,7 @@ export class SubjectInterestsComponent { } } - selected(event: { option: {value: string }}): void { + selected(event: {option: {value: string}}): void { if (this.subjectInterests.indexOf(event.option.value) > -1) { this.remove(event.option.value); } else { @@ -105,7 +115,8 @@ export class SubjectInterestsComponent { filter(value: string): string[] { const filterValue = value.toLowerCase(); - return this.allSubjectInterests.filter( - interest => interest.toLowerCase().includes(filterValue)); + return this.allSubjectInterests.filter(interest => + interest.toLowerCase().includes(filterValue) + ); } } diff --git a/core/templates/pages/preferences-page/modal-templates/edit-profile-picture-modal.component.spec.ts b/core/templates/pages/preferences-page/modal-templates/edit-profile-picture-modal.component.spec.ts index baf5a580a419..1ac995d3b122 100644 --- a/core/templates/pages/preferences-page/modal-templates/edit-profile-picture-modal.component.spec.ts +++ b/core/templates/pages/preferences-page/modal-templates/edit-profile-picture-modal.component.spec.ts @@ -16,15 +16,15 @@ * @fileoverview Unit tests for edit profile picture modal. */ -import { ChangeDetectorRef, ElementRef, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import {ChangeDetectorRef, ElementRef, NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; import Cropper from 'cropperjs'; -import { SvgSanitizerService } from 'services/svg-sanitizer.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { EditProfilePictureModalComponent } from './edit-profile-picture-modal.component'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { of } from 'rxjs'; +import {SvgSanitizerService} from 'services/svg-sanitizer.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {EditProfilePictureModalComponent} from './edit-profile-picture-modal.component'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {of} from 'rxjs'; describe('Edit Profile Picture Modal Component', () => { let fixture: ComponentFixture; @@ -38,26 +38,23 @@ describe('Edit Profile Picture Modal Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - EditProfilePictureModalComponent, - MockTranslatePipe - ], + declarations: [EditProfilePictureModalComponent, MockTranslatePipe], providers: [ NgbActiveModal, SvgSanitizerService, { provide: ChangeDetectorRef, - useClass: MockChangeDetectorRef + useClass: MockChangeDetectorRef, }, { provide: WindowDimensionsService, useValue: { isWindowNarrow: () => true, - getResizeEvent: () => of(resizeEvent) - } + getResizeEvent: () => of(resizeEvent), + }, }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -72,11 +69,11 @@ describe('Edit Profile Picture Modal Component', () => { }); it('should initialize cropper when window is not narrow', () => { - spyOn(windowDimensionsService, 'isWindowNarrow') - .and.returnValue(false); + spyOn(windowDimensionsService, 'isWindowNarrow').and.returnValue(false); fixture.detectChanges(); - componentInstance.croppableImageRef = ( - new ElementRef(document.createElement('img'))); + componentInstance.croppableImageRef = new ElementRef( + document.createElement('img') + ); componentInstance.initializeCropper(); @@ -84,11 +81,11 @@ describe('Edit Profile Picture Modal Component', () => { }); it('should initialize cropper when window is narrow', () => { - spyOn(windowDimensionsService, 'isWindowNarrow') - .and.returnValue(true); + spyOn(windowDimensionsService, 'isWindowNarrow').and.returnValue(true); fixture.detectChanges(); - componentInstance.croppableImageRef = ( - new ElementRef(document.createElement('img'))); + componentInstance.croppableImageRef = new ElementRef( + document.createElement('img') + ); componentInstance.initializeCropper(); @@ -105,15 +102,16 @@ describe('Edit Profile Picture Modal Component', () => { spyOn(componentInstance, 'initializeCropper'); // This is just a mock base 64 in order to test the FileReader event. let dataBase64Mock = 'VEhJUyBJUyBUSEUgQU5TV0VSCg=='; - const arrayBuffer = Uint8Array.from( - window.atob(dataBase64Mock), c => c.charCodeAt(0)); + const arrayBuffer = Uint8Array.from(window.atob(dataBase64Mock), c => + c.charCodeAt(0) + ); let file = new File([arrayBuffer], 'filename.mp3'); componentInstance.onFileChanged(file); expect(componentInstance.invalidImageWarningIsShown).toBeFalse(); }); - it('should remove invalid tags and attributes', ()=> { - const svgString = ( + it('should remove invalid tags and attributes', () => { + const svgString = ' { 'h="1" d="M52289Q59 331 106 386T222 442Q257 442 2864Q412 404 406 402' + 'Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T' + '463 140Q466 150 469 151T485 153H489Q504 153 504 145284 52 289Z" ' + - 'data-name="dataName"/>' - ); + 'data-name="dataName"/>'; let file = new File([svgString], 'test.svg', {type: 'image/svg+xml'}); componentInstance.invalidImageWarningIsShown = false; @@ -142,9 +139,9 @@ describe('Edit Profile Picture Modal Component', () => { componentInstance.cropper = { getCroppedCanvas(options) { return { - toDataURL: () => pictureDataUrl + toDataURL: () => pictureDataUrl, }; - } + }, } as Cropper; componentInstance.confirm(); expect(componentInstance.cropppedImageDataUrl).toEqual(pictureDataUrl); diff --git a/core/templates/pages/preferences-page/modal-templates/edit-profile-picture-modal.component.ts b/core/templates/pages/preferences-page/modal-templates/edit-profile-picture-modal.component.ts index 5cb5613bdd52..1ec40db05844 100644 --- a/core/templates/pages/preferences-page/modal-templates/edit-profile-picture-modal.component.ts +++ b/core/templates/pages/preferences-page/modal-templates/edit-profile-picture-modal.component.ts @@ -16,19 +16,24 @@ * @fileoverview Component for edit profile picture modal. */ -import { ChangeDetectorRef, Component, ElementRef, ViewChild } from '@angular/core'; -import { SafeResourceUrl } from '@angular/platform-browser'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { AppConstants } from 'app.constants'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import { + ChangeDetectorRef, + Component, + ElementRef, + ViewChild, +} from '@angular/core'; +import {SafeResourceUrl} from '@angular/platform-browser'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {AppConstants} from 'app.constants'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; import Cropper from 'cropperjs'; -import { SvgSanitizerService } from 'services/svg-sanitizer.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; +import {SvgSanitizerService} from 'services/svg-sanitizer.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; require('cropperjs/dist/cropper.min.css'); @Component({ selector: 'oppia-edit-profile-picture-modal', - templateUrl: './edit-profile-picture-modal.component.html' + templateUrl: './edit-profile-picture-modal.component.html', }) export class EditProfilePictureModalComponent extends ConfirmOrCancelModal { // 'uploadedImage' will be null if the uploaded svg is invalid or not trusted. @@ -37,9 +42,9 @@ export class EditProfilePictureModalComponent extends ConfirmOrCancelModal { invalidImageWarningIsShown: boolean = false; windowIsNarrow: boolean = false; allowedImageFormats: readonly string[] = AppConstants.ALLOWED_IMAGE_FORMATS; - invalidTagsAndAttributes: { tags: string[]; attrs: string[] } = { + invalidTagsAndAttributes: {tags: string[]; attrs: string[]} = { tags: [], - attrs: [] + attrs: [], }; // 'cropper' is initialized before it is to be used, hence we need to do @@ -52,7 +57,7 @@ export class EditProfilePictureModalComponent extends ConfirmOrCancelModal { private changeDetectorRef: ChangeDetectorRef, private ngbActiveModal: NgbActiveModal, private windowDimensionService: WindowDimensionsService, - private svgSanitizerService: SvgSanitizerService, + private svgSanitizerService: SvgSanitizerService ) { super(ngbActiveModal); } @@ -64,13 +69,13 @@ export class EditProfilePictureModalComponent extends ConfirmOrCancelModal { this.cropper = new Cropper(profilePicture, { minContainerWidth: 500, minContainerHeight: 350, - aspectRatio: 1 + aspectRatio: 1, }); } else { this.cropper = new Cropper(profilePicture, { minContainerWidth: 200, minContainerHeight: 200, - aspectRatio: 1 + aspectRatio: 1, }); } } @@ -79,21 +84,24 @@ export class EditProfilePictureModalComponent extends ConfirmOrCancelModal { onFileChanged(file: Blob): void { this.invalidImageWarningIsShown = false; let reader = new FileReader(); - reader.onload = (e) => { + reader.onload = e => { this.invalidTagsAndAttributes = { tags: [], - attrs: [] + attrs: [], }; let imageData = (e.target as FileReader).result as string; if (this.svgSanitizerService.isBase64Svg(imageData)) { - this.invalidTagsAndAttributes = this.svgSanitizerService - .getInvalidSvgTagsAndAttrsFromDataUri(imageData); - this.uploadedImage = this.svgSanitizerService.getTrustedSvgResourceUrl( - imageData); + this.invalidTagsAndAttributes = + this.svgSanitizerService.getInvalidSvgTagsAndAttrsFromDataUri( + imageData + ); + this.uploadedImage = + this.svgSanitizerService.getTrustedSvgResourceUrl(imageData); } if (!this.uploadedImage) { this.uploadedImage = decodeURIComponent( - (e.target as FileReader).result as string); + (e.target as FileReader).result as string + ); } try { this.changeDetectorRef.detectChanges(); @@ -112,7 +120,7 @@ export class EditProfilePictureModalComponent extends ConfirmOrCancelModal { reset(): void { this.invalidTagsAndAttributes = { tags: [], - attrs: [] + attrs: [], }; this.uploadedImage = null; this.cropppedImageDataUrl = ''; @@ -127,11 +135,12 @@ export class EditProfilePictureModalComponent extends ConfirmOrCancelModal { if (this.cropper === undefined) { throw new Error('Cropper has not been initialized'); } - this.cropppedImageDataUrl = ( - this.cropper.getCroppedCanvas({ + this.cropppedImageDataUrl = this.cropper + .getCroppedCanvas({ height: 150, - width: 150 - }).toDataURL()); + width: 150, + }) + .toDataURL(); super.confirm(this.cropppedImageDataUrl); } diff --git a/core/templates/pages/preferences-page/preferences-page-root.component.spec.ts b/core/templates/pages/preferences-page/preferences-page-root.component.spec.ts index bc34c9a8f84e..9129b5f7ffe8 100644 --- a/core/templates/pages/preferences-page/preferences-page-root.component.spec.ts +++ b/core/templates/pages/preferences-page/preferences-page-root.component.spec.ts @@ -16,18 +16,24 @@ * @fileoverview Unit tests for the preferences page root component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; - -import { AppConstants } from 'app.constants'; -import { AccessValidationBackendApiService } from 'pages/oppia-root/routing/access-validation-backend-api.service'; -import { LoaderService } from 'services/loader.service'; -import { PageHeadService } from 'services/page-head.service'; - -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { PreferencesPageRootComponent } from './preferences-page-root.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; + +import {AppConstants} from 'app.constants'; +import {AccessValidationBackendApiService} from 'pages/oppia-root/routing/access-validation-backend-api.service'; +import {LoaderService} from 'services/loader.service'; +import {PageHeadService} from 'services/page-head.service'; + +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {PreferencesPageRootComponent} from './preferences-page-root.component'; class MockTranslateService { onLangChange: EventEmitter = new EventEmitter(); @@ -46,21 +52,16 @@ describe('Preferences Page Root', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - PreferencesPageRootComponent, - MockTranslatePipe - ], + imports: [HttpClientTestingModule], + declarations: [PreferencesPageRootComponent, MockTranslatePipe], providers: [ PageHeadService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -70,18 +71,20 @@ describe('Preferences Page Root', () => { pageHeadService = TestBed.inject(PageHeadService); loaderService = TestBed.inject(LoaderService); accessValidationBackendApiService = TestBed.inject( - AccessValidationBackendApiService); + AccessValidationBackendApiService + ); translateService = TestBed.inject(TranslateService); }); - it('should successfully instantiate the component', - () => { - expect(component).toBeDefined(); - }); + it('should successfully instantiate the component', () => { + expect(component).toBeDefined(); + }); it('should initialize and show page when access is valid', fakeAsync(() => { - spyOn(accessValidationBackendApiService, 'validateCanManageOwnAccount') - .and.returnValue(Promise.resolve()); + spyOn( + accessValidationBackendApiService, + 'validateCanManageOwnAccount' + ).and.returnValue(Promise.resolve()); spyOn(loaderService, 'showLoadingScreen'); spyOn(loaderService, 'hideLoadingScreen'); @@ -89,34 +92,39 @@ describe('Preferences Page Root', () => { tick(); expect(loaderService.showLoadingScreen).toHaveBeenCalled(); - expect(accessValidationBackendApiService.validateCanManageOwnAccount) - .toHaveBeenCalled(); + expect( + accessValidationBackendApiService.validateCanManageOwnAccount + ).toHaveBeenCalled(); expect(component.pageIsShown).toBeTrue(); expect(component.errorPageIsShown).toBeFalse(); expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); })); - it('should initialize and show error page when server respond with error', - fakeAsync(() => { - spyOn(accessValidationBackendApiService, 'validateCanManageOwnAccount') - .and.returnValue(Promise.reject()); - spyOn(loaderService, 'showLoadingScreen'); - spyOn(loaderService, 'hideLoadingScreen'); + it('should initialize and show error page when server respond with error', fakeAsync(() => { + spyOn( + accessValidationBackendApiService, + 'validateCanManageOwnAccount' + ).and.returnValue(Promise.reject()); + spyOn(loaderService, 'showLoadingScreen'); + spyOn(loaderService, 'hideLoadingScreen'); - component.ngOnInit(); - tick(); + component.ngOnInit(); + tick(); - expect(loaderService.showLoadingScreen).toHaveBeenCalled(); - expect(accessValidationBackendApiService.validateCanManageOwnAccount) - .toHaveBeenCalled(); - expect(component.pageIsShown).toBeFalse(); - expect(component.errorPageIsShown).toBeTrue(); - expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); - })); + expect(loaderService.showLoadingScreen).toHaveBeenCalled(); + expect( + accessValidationBackendApiService.validateCanManageOwnAccount + ).toHaveBeenCalled(); + expect(component.pageIsShown).toBeFalse(); + expect(component.errorPageIsShown).toBeTrue(); + expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); + })); it('should initialize and subscribe to onLangChange', fakeAsync(() => { - spyOn(accessValidationBackendApiService, 'validateCanManageOwnAccount') - .and.returnValue(Promise.resolve()); + spyOn( + accessValidationBackendApiService, + 'validateCanManageOwnAccount' + ).and.returnValue(Promise.resolve()); spyOn(component.directiveSubscriptions, 'add'); spyOn(translateService.onLangChange, 'subscribe'); @@ -128,8 +136,10 @@ describe('Preferences Page Root', () => { })); it('should update page title whenever the language changes', () => { - spyOn(accessValidationBackendApiService, 'validateCanManageOwnAccount') - .and.returnValue(Promise.resolve()); + spyOn( + accessValidationBackendApiService, + 'validateCanManageOwnAccount' + ).and.returnValue(Promise.resolve()); component.ngOnInit(); spyOn(component, 'setPageTitleAndMetaTags'); @@ -145,10 +155,12 @@ describe('Preferences Page Root', () => { component.setPageTitleAndMetaTags(); expect(translateService.instant).toHaveBeenCalledWith( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PREFERENCES.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PREFERENCES.TITLE + ); expect(pageHeadService.updateTitleAndMetaTags).toHaveBeenCalledWith( AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PREFERENCES.TITLE, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PREFERENCES.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PREFERENCES.META + ); }); it('should unsubscribe on component destruction', () => { diff --git a/core/templates/pages/preferences-page/preferences-page-root.component.ts b/core/templates/pages/preferences-page/preferences-page-root.component.ts index d59452f2adb2..0357a58ddab0 100644 --- a/core/templates/pages/preferences-page/preferences-page-root.component.ts +++ b/core/templates/pages/preferences-page/preferences-page-root.component.ts @@ -16,18 +16,18 @@ * @fileoverview Root component for Preferences Page. */ -import { Component, OnDestroy } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; +import {Component, OnDestroy} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; -import { AppConstants } from 'app.constants'; -import { AccessValidationBackendApiService } from 'pages/oppia-root/routing/access-validation-backend-api.service'; -import { LoaderService } from 'services/loader.service'; -import { PageHeadService } from 'services/page-head.service'; +import {AppConstants} from 'app.constants'; +import {AccessValidationBackendApiService} from 'pages/oppia-root/routing/access-validation-backend-api.service'; +import {LoaderService} from 'services/loader.service'; +import {PageHeadService} from 'services/page-head.service'; @Component({ selector: 'oppia-preferences-page-root', - templateUrl: './preferences-page-root.component.html' + templateUrl: './preferences-page-root.component.html', }) export class PreferencesPageRootComponent implements OnDestroy { directiveSubscriptions = new Subscription(); @@ -35,8 +35,7 @@ export class PreferencesPageRootComponent implements OnDestroy { errorPageIsShown: boolean = false; constructor( - private accessValidationBackendApiService: - AccessValidationBackendApiService, + private accessValidationBackendApiService: AccessValidationBackendApiService, private loaderService: LoaderService, private pageHeadService: PageHeadService, private translateService: TranslateService @@ -44,10 +43,12 @@ export class PreferencesPageRootComponent implements OnDestroy { setPageTitleAndMetaTags(): void { let translatedTitle = this.translateService.instant( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PREFERENCES.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PREFERENCES.TITLE + ); this.pageHeadService.updateTitleAndMetaTags( translatedTitle, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PREFERENCES.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PREFERENCES.META + ); } ngOnInit(): void { @@ -57,12 +58,17 @@ export class PreferencesPageRootComponent implements OnDestroy { }) ); this.loaderService.showLoadingScreen('Loading'); - this.accessValidationBackendApiService.validateCanManageOwnAccount() + this.accessValidationBackendApiService + .validateCanManageOwnAccount() + .then( + () => { + this.pageIsShown = true; + }, + err => { + this.errorPageIsShown = true; + } + ) .then(() => { - this.pageIsShown = true; - }, (err) => { - this.errorPageIsShown = true; - }).then(() => { this.loaderService.hideLoadingScreen(); }); } diff --git a/core/templates/pages/preferences-page/preferences-page-routing.module.ts b/core/templates/pages/preferences-page/preferences-page-routing.module.ts index 7c43a4b7932f..d935b64de037 100644 --- a/core/templates/pages/preferences-page/preferences-page-routing.module.ts +++ b/core/templates/pages/preferences-page/preferences-page-routing.module.ts @@ -16,24 +16,19 @@ * @fileoverview Routing module for preferences page. */ -import { NgModule } from '@angular/core'; -import { Route, RouterModule } from '@angular/router'; -import { PreferencesPageRootComponent } from './preferences-page-root.component'; +import {NgModule} from '@angular/core'; +import {Route, RouterModule} from '@angular/router'; +import {PreferencesPageRootComponent} from './preferences-page-root.component'; const routes: Route[] = [ { path: '', - component: PreferencesPageRootComponent - } + component: PreferencesPageRootComponent, + }, ]; @NgModule({ - imports: [ - RouterModule.forChild(routes) - ], - exports: [ - RouterModule - ] + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], }) - export class PreferencesPageRoutingModule {} diff --git a/core/templates/pages/preferences-page/preferences-page.component.spec.ts b/core/templates/pages/preferences-page/preferences-page.component.spec.ts index 6509a7b48c80..21f8d0e26af1 100644 --- a/core/templates/pages/preferences-page/preferences-page.component.spec.ts +++ b/core/templates/pages/preferences-page/preferences-page.component.spec.ts @@ -16,25 +16,42 @@ * @fileoverview Unit tests for the Preferences page. */ -import { NO_ERRORS_SCHEMA, Pipe, ElementRef } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { NgbModal, NgbModalModule, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { UserInfo } from 'domain/user/user-info.model'; -import { LanguageUtilService } from 'domain/utilities/language-util.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { AlertsService } from 'services/alerts.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { LoaderService } from 'services/loader.service'; -import { PreventPageUnloadEventService } from 'services/prevent-page-unload-event.service'; -import { EmailPreferencesBackendDict, PreferencesBackendDict, UpdatePreferencesResponse, UserBackendApiService } from 'services/user-backend-api.service'; -import { UserService } from 'services/user.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { PreferencesPageComponent } from './preferences-page.component'; -import { HttpClientTestingModule, HttpTestingController } from - '@angular/common/http/testing'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; -import { ImageUploadHelperService } from '../../services/image-upload-helper.service'; +import {NO_ERRORS_SCHEMA, Pipe, ElementRef} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import { + NgbModal, + NgbModalModule, + NgbModalRef, +} from '@ng-bootstrap/ng-bootstrap'; +import {UserInfo} from 'domain/user/user-info.model'; +import {LanguageUtilService} from 'domain/utilities/language-util.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {AlertsService} from 'services/alerts.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {LoaderService} from 'services/loader.service'; +import {PreventPageUnloadEventService} from 'services/prevent-page-unload-event.service'; +import { + EmailPreferencesBackendDict, + PreferencesBackendDict, + UpdatePreferencesResponse, + UserBackendApiService, +} from 'services/user-backend-api.service'; +import {UserService} from 'services/user.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {PreferencesPageComponent} from './preferences-page.component'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; +import {ImageUploadHelperService} from '../../services/image-upload-helper.service'; describe('Preferences Page Component', () => { @Pipe({name: 'truncate'}) @@ -69,17 +86,19 @@ describe('Preferences Page Component', () => { can_receive_editor_role_email: true, can_receive_feedback_message_email: false, can_receive_subscription_email: true, - subscription_list: [{ - creator_username: 'creator', - creator_impact: 0 - }] + subscription_list: [ + { + creator_username: 'creator', + creator_impact: 0, + }, + ], }; class MockWindowRef { imageData: Record = {}; _window = { location: { - reload: () => {} + reload: () => {}, }, sessionStorage: { removeItem: (name: string) => { @@ -90,8 +109,8 @@ describe('Preferences Page Component', () => { }, getItem: (filename: string) => { return 'data:image/png;base64,JUMzJTg3JTJD'; - } - } + }, + }, }; get nativeWindow() { @@ -105,11 +124,11 @@ describe('Preferences Page Component', () => { } async updatePreferencesDataAsync( - updateType: string, - data: boolean | string | string[] | EmailPreferencesBackendDict + updateType: string, + data: boolean | string | string[] | EmailPreferencesBackendDict ): Promise { return Promise.resolve({ - bulk_email_signup_message_should_be_shown: false + bulk_email_signup_message_should_be_shown: false, }); } } @@ -117,14 +136,11 @@ describe('Preferences Page Component', () => { beforeEach(waitForAsync(() => { mockWindowRef = new MockWindowRef(); TestBed.configureTestingModule({ - imports: [ - NgbModalModule, - HttpClientTestingModule - ], + imports: [NgbModalModule, HttpClientTestingModule], declarations: [ MockTranslatePipe, MockTruncatePipe, - PreferencesPageComponent + PreferencesPageComponent, ], providers: [ AlertsService, @@ -135,15 +151,15 @@ describe('Preferences Page Component', () => { UrlInterpolationService, { provide: UserBackendApiService, - useClass: MockUserBackendApiService + useClass: MockUserBackendApiService, }, UserService, { provide: WindowRef, - useValue: mockWindowRef - } + useValue: mockWindowRef, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -155,15 +171,18 @@ describe('Preferences Page Component', () => { languageUtilService = TestBed.inject(LanguageUtilService); urlInterpolationService = TestBed.inject(UrlInterpolationService); preventPageUnloadEventService = TestBed.inject( - PreventPageUnloadEventService); + PreventPageUnloadEventService + ); alertsService = TestBed.inject(AlertsService); i18nLanguageCodeService = TestBed.inject(I18nLanguageCodeService); ngbModal = TestBed.inject(NgbModal); mockUserBackendApiService = TestBed.inject(UserBackendApiService); imageUploadHelperService = TestBed.inject(ImageUploadHelperService); - spyOn(userService, 'getProfileImageDataUrl').and.returnValue( - ['profile-image-url-png', 'profile-image-url-webp']); + spyOn(userService, 'getProfileImageDataUrl').and.returnValue([ + 'profile-image-url-png', + 'profile-image-url-webp', + ]); }); it('should be defined', () => { @@ -175,16 +194,29 @@ describe('Preferences Page Component', () => { let userEmail = 'test_email@example.com'; spyOn(loaderService, 'showLoadingScreen'); spyOn(loaderService, 'hideLoadingScreen'); - spyOn(userService, 'getUserInfoAsync').and - .returnValue(Promise.resolve(new UserInfo( - ['USER_ROLE'], false, false, false, false, false, 'en', username, - userEmail, true))); - spyOn(languageUtilService, 'getLanguageIdsAndTexts').and.returnValue( - [{ + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve( + new UserInfo( + ['USER_ROLE'], + false, + false, + false, + false, + false, + 'en', + username, + userEmail, + true + ) + ) + ); + spyOn(languageUtilService, 'getLanguageIdsAndTexts').and.returnValue([ + { id: 'en', text: 'English', - ariaLabelInEnglish: 'English' - }]); + ariaLabelInEnglish: 'English', + }, + ]); componentInstance.ngOnInit(); tick(); tick(); @@ -194,66 +226,82 @@ describe('Preferences Page Component', () => { expect(componentInstance.email).toEqual(userEmail); expect(componentInstance.userBio).toEqual(preferencesData.user_bio); expect(componentInstance.subjectInterests).toEqual( - preferencesData.subject_interests); + preferencesData.subject_interests + ); expect(componentInstance.preferredLanguageCodes).toEqual( - preferencesData.preferred_language_codes); + preferencesData.preferred_language_codes + ); expect(componentInstance.profilePicturePngDataUrl).toEqual( - 'profile-image-url-png'); + 'profile-image-url-png' + ); expect(componentInstance.profilePictureWebpDataUrl).toEqual( - 'profile-image-url-webp'); + 'profile-image-url-webp' + ); expect(componentInstance.defaultDashboard).toEqual( - preferencesData.default_dashboard); + preferencesData.default_dashboard + ); expect(componentInstance.canReceiveEmailUpdates).toEqual( - preferencesData.can_receive_email_updates); + preferencesData.can_receive_email_updates + ); expect(componentInstance.canReceiveEditorRoleEmail).toEqual( - preferencesData.can_receive_editor_role_email); + preferencesData.can_receive_editor_role_email + ); expect(componentInstance.canReceiveSubscriptionEmail).toEqual( - preferencesData.can_receive_subscription_email); + preferencesData.can_receive_subscription_email + ); expect(componentInstance.canReceiveFeedbackMessageEmail).toEqual( - preferencesData.can_receive_feedback_message_email); + preferencesData.can_receive_feedback_message_email + ); expect(componentInstance.preferredSiteLanguageCode).toEqual( - preferencesData.preferred_site_language_code); + preferencesData.preferred_site_language_code + ); expect(componentInstance.subscriptionList).toEqual( - preferencesData.subscription_list); + preferencesData.subscription_list + ); expect(loaderService.showLoadingScreen).toHaveBeenCalled(); expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); })); it('should get user profile image png data url correctly', () => { expect(componentInstance.getProfileImagePngDataUrl('username')).toBe( - 'profile-image-url-png'); + 'profile-image-url-png' + ); }); it('should get user profile image webp data url correctly', () => { expect(componentInstance.getProfileImageWebpDataUrl('username')).toBe( - 'profile-image-url-webp'); + 'profile-image-url-webp' + ); }); - it('should set default profile pictures when username is null', - fakeAsync(() => { - let userInfo = { - getUsername: () => null, - isSuperAdmin: () => true, - getEmail: () => 'test_email@example.com' - }; - spyOn(loaderService, 'showLoadingScreen'); - spyOn(loaderService, 'hideLoadingScreen'); - spyOn(userService, 'getUserInfoAsync') - .and.resolveTo(userInfo as UserInfo); - - componentInstance.ngOnInit(); - tick(); + it('should set default profile pictures when username is null', fakeAsync(() => { + let userInfo = { + getUsername: () => null, + isSuperAdmin: () => true, + getEmail: () => 'test_email@example.com', + }; + spyOn(loaderService, 'showLoadingScreen'); + spyOn(loaderService, 'hideLoadingScreen'); + spyOn(userService, 'getUserInfoAsync').and.resolveTo( + userInfo as UserInfo + ); - expect(componentInstance.profilePicturePngDataUrl).toEqual( - '/assets/images/avatar/user_blue_150px.png'); - expect(componentInstance.profilePictureWebpDataUrl).toEqual( - '/assets/images/avatar/user_blue_150px.webp'); - })); + componentInstance.ngOnInit(); + tick(); + + expect(componentInstance.profilePicturePngDataUrl).toEqual( + '/assets/images/avatar/user_blue_150px.png' + ); + expect(componentInstance.profilePictureWebpDataUrl).toEqual( + '/assets/images/avatar/user_blue_150px.webp' + ); + })); it('should get static image url', () => { let staticImageUrl = 'static_image_url'; spyOn(urlInterpolationService, 'getStaticImageUrl').and.returnValue( - staticImageUrl); + staticImageUrl + ); expect(componentInstance.getStaticImageUrl('')).toEqual(staticImageUrl); }); @@ -290,8 +338,8 @@ describe('Preferences Page Component', () => { componentInstance.savePreferredSiteLanguageCodes(code); tick(); expect( - i18nLanguageCodeService.setI18nLanguageCode).toHaveBeenCalledWith( - code); + i18nLanguageCodeService.setI18nLanguageCode + ).toHaveBeenCalledWith(code); expect(preventPageUnloadEventService.addListener).toHaveBeenCalled(); expect(preventPageUnloadEventService.removeListener).toHaveBeenCalled(); expect(alertsService.addInfoMessage).toHaveBeenCalled(); @@ -306,11 +354,14 @@ describe('Preferences Page Component', () => { })); it('should save email preferences', fakeAsync(() => { - spyOn(mockUserBackendApiService, 'updatePreferencesDataAsync') - .and.returnValue( - Promise.resolve({ - bulk_email_signup_message_should_be_shown: true - })); + spyOn( + mockUserBackendApiService, + 'updatePreferencesDataAsync' + ).and.returnValue( + Promise.resolve({ + bulk_email_signup_message_should_be_shown: true, + }) + ); componentInstance.saveEmailPreferences(true, true, true, true); tick(); expect(preventPageUnloadEventService.addListener).toHaveBeenCalled(); @@ -342,8 +393,9 @@ describe('Preferences Page Component', () => { }); it('should validate user popover when username is longer 10 chars', () => { - expect(componentInstance.showUsernamePopover('greaterthan10characters')) - .toEqual('mouseenter'); + expect( + componentInstance.showUsernamePopover('greaterthan10characters') + ).toEqual('mouseenter'); }); it('should not show popover when username is shorter than 10 chars', () => { @@ -359,37 +411,40 @@ describe('Preferences Page Component', () => { it('should show edit profile picture modal', fakeAsync(() => { let profilePictureDataUrl = 'data:image/png;base64,JUMzJTg3JTJD'; spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.resolve(profilePictureDataUrl) + result: Promise.resolve(profilePictureDataUrl), } as NgbModalRef); spyOn(userService, 'setProfileImageDataUrlAsync').and.returnValue( - Promise.resolve({ bulk_email_signup_message_should_be_shown: false })); + Promise.resolve({bulk_email_signup_message_should_be_shown: false}) + ); spyOn(mockWindowRef.nativeWindow.location, 'reload'); componentInstance.showEditProfilePictureModal(); tick(); tick(); expect(mockWindowRef.nativeWindow.sessionStorage.getItem('file')).toEqual( - profilePictureDataUrl); + profilePictureDataUrl + ); expect(mockWindowRef.nativeWindow.location.reload).toHaveBeenCalled(); })); - it('should edit profile picture modal raise error when image is invalid', - fakeAsync(() => { - let error = 'Image uploaded is not valid.'; - let profilePictureDataUrl = 'data:text/plain;base64,JUMzJTg3JTJD'; - spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.resolve(profilePictureDataUrl) - } as NgbModalRef); - spyOn(imageUploadHelperService, 'convertImageDataToImageFile') - .and.returnValue(null); - spyOn(alertsService, 'addWarning'); - componentInstance.showEditProfilePictureModal(); - tick(); - expect(alertsService.addWarning).toHaveBeenCalledWith(error); - })); + it('should edit profile picture modal raise error when image is invalid', fakeAsync(() => { + let error = 'Image uploaded is not valid.'; + let profilePictureDataUrl = 'data:text/plain;base64,JUMzJTg3JTJD'; + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.resolve(profilePictureDataUrl), + } as NgbModalRef); + spyOn( + imageUploadHelperService, + 'convertImageDataToImageFile' + ).and.returnValue(null); + spyOn(alertsService, 'addWarning'); + componentInstance.showEditProfilePictureModal(); + tick(); + expect(alertsService.addWarning).toHaveBeenCalledWith(error); + })); it('should handle edit profile picture modal is canceled', fakeAsync(() => { spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.reject() + result: Promise.reject(), } as NgbModalRef); spyOn(userService, 'setProfileImageDataUrlAsync'); componentInstance.showEditProfilePictureModal(); @@ -410,19 +465,19 @@ describe('Preferences Page Component', () => { class MockWindowRef { nativeWindow = { location: { - reload: () => {} - } + reload: () => {}, + }, }; } beforeEach(() => { - spyOnProperty(AssetsBackendApiService, 'EMULATOR_MODE', 'get') - .and.returnValue(false); + spyOnProperty( + AssetsBackendApiService, + 'EMULATOR_MODE', + 'get' + ).and.returnValue(false); TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - NgbModalModule - ], + imports: [HttpClientTestingModule, NgbModalModule], declarations: [ PreferencesPageComponent, MockTranslatePipe, @@ -433,10 +488,10 @@ describe('Preferences Page Component', () => { UserService, { provide: WindowRef, - useClass: MockWindowRef - } + useClass: MockWindowRef, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(PreferencesPageComponent); componentInstance = fixture.componentInstance; @@ -449,11 +504,11 @@ describe('Preferences Page Component', () => { it('should show edit profile picture modal', fakeAsync(() => { let profilePictureDataUrl = 'data:image/png;base64,JUMzJTg3JTJD'; spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.resolve(profilePictureDataUrl) + result: Promise.resolve(profilePictureDataUrl), } as NgbModalRef); spyOn(userService, 'setProfileImageDataUrlAsync').and.returnValue( - Promise.resolve( - { bulk_email_signup_message_should_be_shown: false })); + Promise.resolve({bulk_email_signup_message_should_be_shown: false}) + ); spyOn(mockWindowRef.nativeWindow.location, 'reload'); componentInstance.showEditProfilePictureModal(); tick(); @@ -465,7 +520,7 @@ describe('Preferences Page Component', () => { it('should handle tab key press for first radio', () => { const mockSecondRadio = new ElementRef(document.createElement('input')); const mockThirdRadio = new ElementRef(document.createElement('input')); - const event = new KeyboardEvent('keydown', { key: 'Tab' }); + const event = new KeyboardEvent('keydown', {key: 'Tab'}); componentInstance.secondRadio = mockSecondRadio; componentInstance.thirdRadio = mockThirdRadio; @@ -475,16 +530,18 @@ describe('Preferences Page Component', () => { componentInstance.handleTabForFirstRadio(event); - expect(componentInstance.secondRadio.nativeElement.focus) - .toHaveBeenCalled(); - expect(componentInstance.thirdRadio.nativeElement.focus) - .not.toHaveBeenCalled(); + expect( + componentInstance.secondRadio.nativeElement.focus + ).toHaveBeenCalled(); + expect( + componentInstance.thirdRadio.nativeElement.focus + ).not.toHaveBeenCalled(); }); it('should handle tab key press for second radio', () => { const mockFirstRadio = new ElementRef(document.createElement('input')); const mockThirdRadio = new ElementRef(document.createElement('input')); - const event = new KeyboardEvent('keydown', { key: 'Tab' }); + const event = new KeyboardEvent('keydown', {key: 'Tab'}); componentInstance.firstRadio = mockFirstRadio; componentInstance.thirdRadio = mockThirdRadio; @@ -494,17 +551,18 @@ describe('Preferences Page Component', () => { componentInstance.handleTabForSecondRadio(event); - expect(componentInstance.firstRadio.nativeElement.focus) - .not.toHaveBeenCalled(); - expect(componentInstance.thirdRadio.nativeElement.focus) - .toHaveBeenCalled(); + expect( + componentInstance.firstRadio.nativeElement.focus + ).not.toHaveBeenCalled(); + expect( + componentInstance.thirdRadio.nativeElement.focus + ).toHaveBeenCalled(); }); it('should handle shift+tab key press for second radio', () => { const mockFirstRadio = new ElementRef(document.createElement('input')); const mockThirdRadio = new ElementRef(document.createElement('input')); - const event = new KeyboardEvent( - 'keydown', { key: 'Tab', shiftKey: true }); + const event = new KeyboardEvent('keydown', {key: 'Tab', shiftKey: true}); componentInstance.firstRadio = mockFirstRadio; componentInstance.thirdRadio = mockThirdRadio; @@ -514,17 +572,18 @@ describe('Preferences Page Component', () => { componentInstance.handleTabForSecondRadio(event); - expect(componentInstance.firstRadio.nativeElement.focus) - .toHaveBeenCalled(); - expect(componentInstance.thirdRadio.nativeElement.focus) - .not.toHaveBeenCalled(); + expect( + componentInstance.firstRadio.nativeElement.focus + ).toHaveBeenCalled(); + expect( + componentInstance.thirdRadio.nativeElement.focus + ).not.toHaveBeenCalled(); }); it('should handle shift+tab key press for third radio', () => { const mockFirstRadio = new ElementRef(document.createElement('input')); const mockSecondRadio = new ElementRef(document.createElement('input')); - const event = new KeyboardEvent( - 'keydown', { key: 'Tab', shiftKey: true }); + const event = new KeyboardEvent('keydown', {key: 'Tab', shiftKey: true}); componentInstance.firstRadio = mockFirstRadio; componentInstance.secondRadio = mockSecondRadio; @@ -534,10 +593,12 @@ describe('Preferences Page Component', () => { componentInstance.handleTabForThirdRadio(event); - expect(componentInstance.firstRadio.nativeElement.focus) - .not.toHaveBeenCalled(); - expect(componentInstance.secondRadio.nativeElement.focus) - .toHaveBeenCalled(); + expect( + componentInstance.firstRadio.nativeElement.focus + ).not.toHaveBeenCalled(); + expect( + componentInstance.secondRadio.nativeElement.focus + ).toHaveBeenCalled(); }); afterEach(() => { diff --git a/core/templates/pages/preferences-page/preferences-page.component.ts b/core/templates/pages/preferences-page/preferences-page.component.ts index 00ee1271a67a..102e0fc845ca 100644 --- a/core/templates/pages/preferences-page/preferences-page.component.ts +++ b/core/templates/pages/preferences-page/preferences-page.component.ts @@ -16,25 +16,32 @@ * @fileoverview Component for the Oppia 'edit preferences' page. */ -import { Component, ViewChild, ElementRef } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; - -import { AppConstants } from 'app.constants'; -import { LanguageIdAndText, LanguageUtilService } from 'domain/utilities/language-util.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { AlertsService } from 'services/alerts.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { ImageLocalStorageService } from 'services/image-local-storage.service'; -import { ImageUploadHelperService } from 'services/image-upload-helper.service'; -import { LoaderService } from 'services/loader.service'; -import { PreventPageUnloadEventService } from 'services/prevent-page-unload-event.service'; -import { EmailPreferencesBackendDict, SubscriptionSummary, UserBackendApiService } from 'services/user-backend-api.service'; -import { UserService } from 'services/user.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; - -import { EditProfilePictureModalComponent } from './modal-templates/edit-profile-picture-modal.component'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; +import {Component, ViewChild, ElementRef} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; + +import {AppConstants} from 'app.constants'; +import { + LanguageIdAndText, + LanguageUtilService, +} from 'domain/utilities/language-util.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {AlertsService} from 'services/alerts.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {ImageLocalStorageService} from 'services/image-local-storage.service'; +import {ImageUploadHelperService} from 'services/image-upload-helper.service'; +import {LoaderService} from 'services/loader.service'; +import {PreventPageUnloadEventService} from 'services/prevent-page-unload-event.service'; +import { + EmailPreferencesBackendDict, + SubscriptionSummary, + UserBackendApiService, +} from 'services/user-backend-api.service'; +import {UserService} from 'services/user.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; + +import {EditProfilePictureModalComponent} from './modal-templates/edit-profile-picture-modal.component'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; require('cropperjs/dist/cropper.min.css'); import './preferences-page.component.css'; @@ -47,7 +54,7 @@ interface AudioLangaugeChoice { @Component({ selector: 'oppia-preferences-page', templateUrl: './preferences-page.component.html', - styleUrls: ['./preferences-page.component.css'] + styleUrls: ['./preferences-page.component.css'], }) export class PreferencesPageComponent { // These properties are initialized using Angular lifecycle hooks @@ -82,8 +89,7 @@ export class PreferencesPageComponent { canReceiveFeedbackMessageEmail: boolean = false; showEmailSignupLink: boolean = false; emailSignupLink: string = AppConstants.BULK_EMAIL_SERVICE_SIGNUP_URL; - PAGES_REGISTERED_WITH_FRONTEND = ( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND); + PAGES_REGISTERED_WITH_FRONTEND = AppConstants.PAGES_REGISTERED_WITH_FRONTEND; @ViewChild('firstRadio') firstRadio!: ElementRef; @@ -104,27 +110,28 @@ export class PreferencesPageComponent { private urlInterpolationService: UrlInterpolationService, private userBackendApiService: UserBackendApiService, private userService: UserService - ) { } + ) {} getStaticImageUrl(imagePath: string): string { return this.urlInterpolationService.getStaticImageUrl(imagePath); } private _saveDataItem( - updateType: string, - data: string | string[] | EmailPreferencesBackendDict + updateType: string, + data: string | string[] | EmailPreferencesBackendDict ): void { this.preventPageUnloadEventService.addListener(); - this.userBackendApiService.updatePreferencesDataAsync( - updateType, data).then((returnData) => { - this.preventPageUnloadEventService.removeListener(); - if (returnData.bulk_email_signup_message_should_be_shown) { - this.canReceiveEmailUpdates = false; - this.showEmailSignupLink = true; - } else { - this.alertsService.addInfoMessage('Saved!', 1000); - } - }); + this.userBackendApiService + .updatePreferencesDataAsync(updateType, data) + .then(returnData => { + this.preventPageUnloadEventService.removeListener(); + if (returnData.bulk_email_signup_message_should_be_shown) { + this.canReceiveEmailUpdates = false; + this.showEmailSignupLink = true; + } else { + this.alertsService.addInfoMessage('Saved!', 1000); + } + }); } saveUserBio(userBio: string): void { @@ -144,24 +151,29 @@ export class PreferencesPageComponent { savePreferredSiteLanguageCodes(preferredSiteLanguageCode: string): void { this.i18nLanguageCodeService.setI18nLanguageCode(preferredSiteLanguageCode); this._saveDataItem( - 'preferred_site_language_code', preferredSiteLanguageCode); + 'preferred_site_language_code', + preferredSiteLanguageCode + ); } savePreferredAudioLanguageCode(preferredAudioLanguageCode: string): void { this._saveDataItem( - 'preferred_audio_language_code', preferredAudioLanguageCode); + 'preferred_audio_language_code', + preferredAudioLanguageCode + ); } saveEmailPreferences( - canReceiveEmailUpdates: boolean, canReceiveEditorRoleEmail: boolean, - canReceiveFeedbackMessageEmail: boolean, - canReceiveSubscriptionEmail: boolean): void { + canReceiveEmailUpdates: boolean, + canReceiveEditorRoleEmail: boolean, + canReceiveFeedbackMessageEmail: boolean, + canReceiveSubscriptionEmail: boolean + ): void { let data: EmailPreferencesBackendDict = { can_receive_email_updates: canReceiveEmailUpdates, can_receive_editor_role_email: canReceiveEditorRoleEmail, - can_receive_feedback_message_email: ( - canReceiveFeedbackMessageEmail), - can_receive_subscription_email: canReceiveSubscriptionEmail + can_receive_feedback_message_email: canReceiveFeedbackMessageEmail, + can_receive_subscription_email: canReceiveSubscriptionEmail, }; this._saveDataItem('email_preferences', data); } @@ -200,8 +212,8 @@ export class PreferencesPageComponent { } private _saveProfileImageToLocalStorage(image: string): void { - const newImageFile = ( - this.imageUploadHelperService.convertImageDataToImageFile(image)); + const newImageFile = + this.imageUploadHelperService.convertImageDataToImageFile(image); if (newImageFile === null) { this.alertsService.addWarning('Image uploaded is not valid.'); return; @@ -210,7 +222,9 @@ export class PreferencesPageComponent { reader.onload = () => { const imageData = reader.result as string; this.imageLocalStorageService.saveImage( - this.username + '_profile_picture.png', imageData); + this.username + '_profile_picture.png', + imageData + ); }; reader.readAsDataURL(newImageFile); // The reload is needed in order to update the profile picture @@ -228,16 +242,19 @@ export class PreferencesPageComponent { showEditProfilePictureModal(): void { let modalRef = this.ngbModal.open(EditProfilePictureModalComponent, { - backdrop: 'static' + backdrop: 'static', }); - modalRef.result.then((newProfilePictureDataUrl) => { - this.saveProfileImage(newProfilePictureDataUrl); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + modalRef.result.then( + newProfilePictureDataUrl => { + this.saveProfileImage(newProfilePictureDataUrl); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } handleTabForFirstRadio(event: KeyboardEvent): void { @@ -265,41 +282,41 @@ export class PreferencesPageComponent { } getProfileImagePngDataUrl(username: string): string { - let [pngImageUrl, _] = this.userService.getProfileImageDataUrl( - username); + let [pngImageUrl, _] = this.userService.getProfileImageDataUrl(username); return pngImageUrl; } getProfileImageWebpDataUrl(username: string): string { - let [_, webpImageUrl] = this.userService.getProfileImageDataUrl( - username); + let [_, webpImageUrl] = this.userService.getProfileImageDataUrl(username); return webpImageUrl; } ngOnInit(): void { this.loaderService.showLoadingScreen('Loading'); let userInfoPromise = this.userService.getUserInfoAsync(); - userInfoPromise.then((userInfo) => { + userInfoPromise.then(userInfo => { this.username = userInfo.getUsername(); this.email = userInfo.getEmail(); if (this.username !== null) { - [this.profilePicturePngDataUrl, this.profilePictureWebpDataUrl] = ( - this.userService.getProfileImageDataUrl(this.username)); + [this.profilePicturePngDataUrl, this.profilePictureWebpDataUrl] = + this.userService.getProfileImageDataUrl(this.username); } else { - this.profilePictureWebpDataUrl = ( + this.profilePictureWebpDataUrl = this.urlInterpolationService.getStaticImageUrl( - AppConstants.DEFAULT_PROFILE_IMAGE_WEBP_PATH)); - this.profilePicturePngDataUrl = ( + AppConstants.DEFAULT_PROFILE_IMAGE_WEBP_PATH + ); + this.profilePicturePngDataUrl = this.urlInterpolationService.getStaticImageUrl( - AppConstants.DEFAULT_PROFILE_IMAGE_PNG_PATH)); + AppConstants.DEFAULT_PROFILE_IMAGE_PNG_PATH + ); } }); this.AUDIO_LANGUAGE_CHOICES = AppConstants.SUPPORTED_AUDIO_LANGUAGES.map( - (languageItem) => { + languageItem => { return { id: languageItem.id, - text: languageItem.description + text: languageItem.description, }; } ); @@ -307,22 +324,18 @@ export class PreferencesPageComponent { this.hasPageLoaded = false; let preferencesPromise = this.userBackendApiService.getPreferencesAsync(); - preferencesPromise.then((data) => { + preferencesPromise.then(data => { this.userBio = data.user_bio; this.subjectInterests = data.subject_interests; this.preferredLanguageCodes = data.preferred_language_codes; this.defaultDashboard = data.default_dashboard; this.canReceiveEmailUpdates = data.can_receive_email_updates; - this.canReceiveEditorRoleEmail = - data.can_receive_editor_role_email; - this.canReceiveSubscriptionEmail = - data.can_receive_subscription_email; - this.canReceiveFeedbackMessageEmail = ( - data.can_receive_feedback_message_email); - this.preferredSiteLanguageCode = - data.preferred_site_language_code; - this.preferredAudioLanguageCode = - data.preferred_audio_language_code; + this.canReceiveEditorRoleEmail = data.can_receive_editor_role_email; + this.canReceiveSubscriptionEmail = data.can_receive_subscription_email; + this.canReceiveFeedbackMessageEmail = + data.can_receive_feedback_message_email; + this.preferredSiteLanguageCode = data.preferred_site_language_code; + this.preferredAudioLanguageCode = data.preferred_audio_language_code; this.subscriptionList = data.subscription_list; this.hasPageLoaded = true; }); @@ -338,7 +351,9 @@ export class PreferencesPageComponent { } } -angular.module('oppia').directive('oppiaPreferencesPage', +angular.module('oppia').directive( + 'oppiaPreferencesPage', downgradeComponent({ - component: PreferencesPageComponent - }) as angular.IDirectiveFactory); + component: PreferencesPageComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/preferences-page/preferences-page.module.ts b/core/templates/pages/preferences-page/preferences-page.module.ts index f38c3e54a39c..7dc8390856cf 100644 --- a/core/templates/pages/preferences-page/preferences-page.module.ts +++ b/core/templates/pages/preferences-page/preferences-page.module.ts @@ -16,20 +16,20 @@ * @fileoverview Module for the preferences page. */ -import { NgModule } from '@angular/core'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { PreferencesPageComponent } from './preferences-page.component'; -import { ReactiveFormsModule } from '@angular/forms'; -import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'; -import { PreferredSiteLanguageSelectorComponent } from './form-fields/preferred-language-selector.component'; -import { PreferredLanguagesComponent } from './form-fields/preferred-languages.component'; -import { SubjectInterestsComponent } from './form-fields/subject-interests.component'; -import { PreferencesPageRootComponent } from './preferences-page-root.component'; -import { CommonModule } from '@angular/common'; -import { PreferencesPageRoutingModule } from './preferences-page-routing.module'; -import { Error404PageModule } from 'pages/error-pages/error-404/error-404-page.module'; -import { EditProfilePictureModalComponent } from './modal-templates/edit-profile-picture-modal.component'; -import { SmartRouterModule } from 'hybrid-router-module-provider'; +import {NgModule} from '@angular/core'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {PreferencesPageComponent} from './preferences-page.component'; +import {ReactiveFormsModule} from '@angular/forms'; +import {NgbPopoverModule} from '@ng-bootstrap/ng-bootstrap'; +import {PreferredSiteLanguageSelectorComponent} from './form-fields/preferred-language-selector.component'; +import {PreferredLanguagesComponent} from './form-fields/preferred-languages.component'; +import {SubjectInterestsComponent} from './form-fields/subject-interests.component'; +import {PreferencesPageRootComponent} from './preferences-page-root.component'; +import {CommonModule} from '@angular/common'; +import {PreferencesPageRoutingModule} from './preferences-page-routing.module'; +import {Error404PageModule} from 'pages/error-pages/error-404/error-404-page.module'; +import {EditProfilePictureModalComponent} from './modal-templates/edit-profile-picture-modal.component'; +import {SmartRouterModule} from 'hybrid-router-module-provider'; @NgModule({ imports: [ @@ -41,7 +41,7 @@ import { SmartRouterModule } from 'hybrid-router-module-provider'; ReactiveFormsModule, SharedComponentsModule, PreferencesPageRoutingModule, - Error404PageModule + Error404PageModule, ], declarations: [ EditProfilePictureModalComponent, @@ -49,7 +49,7 @@ import { SmartRouterModule } from 'hybrid-router-module-provider'; PreferencesPageRootComponent, PreferredLanguagesComponent, PreferredSiteLanguageSelectorComponent, - SubjectInterestsComponent + SubjectInterestsComponent, ], entryComponents: [ EditProfilePictureModalComponent, @@ -57,7 +57,7 @@ import { SmartRouterModule } from 'hybrid-router-module-provider'; PreferencesPageRootComponent, PreferredLanguagesComponent, PreferredSiteLanguageSelectorComponent, - SubjectInterestsComponent - ] + SubjectInterestsComponent, + ], }) export class PreferencesPageModule {} diff --git a/core/templates/pages/privacy-page/privacy-page-root.component.spec.ts b/core/templates/pages/privacy-page/privacy-page-root.component.spec.ts index e30b92785842..43c63b4e1c4f 100644 --- a/core/templates/pages/privacy-page/privacy-page-root.component.spec.ts +++ b/core/templates/pages/privacy-page/privacy-page-root.component.spec.ts @@ -16,15 +16,15 @@ * @fileoverview Unit tests for the privacy page root component. */ -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { PrivacyPageRootComponent } from './privacy-page-root.component'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {PrivacyPageRootComponent} from './privacy-page-root.component'; class MockTranslateService { onLangChange: EventEmitter = new EventEmitter(); @@ -41,18 +41,15 @@ describe('Privacy Page Root', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - PrivacyPageRootComponent, - MockTranslatePipe - ], + declarations: [PrivacyPageRootComponent, MockTranslatePipe], providers: [ PageHeadService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -63,10 +60,9 @@ describe('Privacy Page Root', () => { translateService = TestBed.inject(TranslateService); }); - it('should successfully instantiate the component', - () => { - expect(component).toBeDefined(); - }); + it('should successfully instantiate the component', () => { + expect(component).toBeDefined(); + }); it('should initialize and subscribe to onLangChange', () => { spyOn(translateService.onLangChange, 'subscribe'); @@ -92,10 +88,12 @@ describe('Privacy Page Root', () => { component.setPageTitleAndMetaTags(); expect(translateService.instant).toHaveBeenCalledWith( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PRIVACY.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PRIVACY.TITLE + ); expect(pageHeadService.updateTitleAndMetaTags).toHaveBeenCalledWith( AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PRIVACY.TITLE, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PRIVACY.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PRIVACY.META + ); }); it('should unsubscribe on component destruction', () => { diff --git a/core/templates/pages/privacy-page/privacy-page-root.component.ts b/core/templates/pages/privacy-page/privacy-page-root.component.ts index d91a55e2146c..b778e44c644d 100644 --- a/core/templates/pages/privacy-page/privacy-page-root.component.ts +++ b/core/templates/pages/privacy-page/privacy-page-root.component.ts @@ -16,16 +16,16 @@ * @fileoverview Root component for Privacy Page. */ -import { Component, OnDestroy } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; +import {Component, OnDestroy} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; @Component({ selector: 'oppia-privacy-page-root', - templateUrl: './privacy-page-root.component.html' + templateUrl: './privacy-page-root.component.html', }) export class PrivacyPageRootComponent implements OnDestroy { directiveSubscriptions = new Subscription(); @@ -36,10 +36,12 @@ export class PrivacyPageRootComponent implements OnDestroy { setPageTitleAndMetaTags(): void { let translatedTitle = this.translateService.instant( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PRIVACY.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PRIVACY.TITLE + ); this.pageHeadService.updateTitleAndMetaTags( translatedTitle, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PRIVACY.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PRIVACY.META + ); } ngOnInit(): void { diff --git a/core/templates/pages/privacy-page/privacy-page-routing.module.ts b/core/templates/pages/privacy-page/privacy-page-routing.module.ts index 3b3c60d8445e..b8bbf4a06899 100644 --- a/core/templates/pages/privacy-page/privacy-page-routing.module.ts +++ b/core/templates/pages/privacy-page/privacy-page-routing.module.ts @@ -16,24 +16,19 @@ * @fileoverview Routing module for privacy page. */ -import { NgModule } from '@angular/core'; -import { Route, RouterModule } from '@angular/router'; -import { PrivacyPageRootComponent } from './privacy-page-root.component'; +import {NgModule} from '@angular/core'; +import {Route, RouterModule} from '@angular/router'; +import {PrivacyPageRootComponent} from './privacy-page-root.component'; const routes: Route[] = [ { path: '', - component: PrivacyPageRootComponent - } + component: PrivacyPageRootComponent, + }, ]; @NgModule({ - imports: [ - RouterModule.forChild(routes) - ], - exports: [ - RouterModule - ] + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], }) - export class PrivacyPageRoutingModule {} diff --git a/core/templates/pages/privacy-page/privacy-page.component.ts b/core/templates/pages/privacy-page/privacy-page.component.ts index 229cd30be1a6..623543262c69 100644 --- a/core/templates/pages/privacy-page/privacy-page.component.ts +++ b/core/templates/pages/privacy-page/privacy-page.component.ts @@ -16,14 +16,18 @@ * @fileoverview Component for the Oppia privacy page. */ -import { Component } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; @Component({ selector: 'oppia-privacy-page', - templateUrl: './privacy-page.component.html' + templateUrl: './privacy-page.component.html', }) export class PrivacyPageComponent {} -angular.module('oppia').directive('oppiaPrivacyPage', - downgradeComponent({ component: PrivacyPageComponent })); +angular + .module('oppia') + .directive( + 'oppiaPrivacyPage', + downgradeComponent({component: PrivacyPageComponent}) + ); diff --git a/core/templates/pages/privacy-page/privacy-page.module.ts b/core/templates/pages/privacy-page/privacy-page.module.ts index c25973eabf7f..e5cfd0fe4d87 100644 --- a/core/templates/pages/privacy-page/privacy-page.module.ts +++ b/core/templates/pages/privacy-page/privacy-page.module.ts @@ -16,26 +16,16 @@ * @fileoverview Module for the privacy page. */ -import { NgModule } from '@angular/core'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { PrivacyPageComponent } from './privacy-page.component'; -import { PrivacyPageRootComponent } from './privacy-page-root.component'; -import { CommonModule } from '@angular/common'; -import { PrivacyPageRoutingModule } from './privacy-page-routing.module'; +import {NgModule} from '@angular/core'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {PrivacyPageComponent} from './privacy-page.component'; +import {PrivacyPageRootComponent} from './privacy-page-root.component'; +import {CommonModule} from '@angular/common'; +import {PrivacyPageRoutingModule} from './privacy-page-routing.module'; @NgModule({ - imports: [ - CommonModule, - SharedComponentsModule, - PrivacyPageRoutingModule - ], - declarations: [ - PrivacyPageRootComponent, - PrivacyPageComponent - ], - entryComponents: [ - PrivacyPageRootComponent, - PrivacyPageComponent - ] + imports: [CommonModule, SharedComponentsModule, PrivacyPageRoutingModule], + declarations: [PrivacyPageRootComponent, PrivacyPageComponent], + entryComponents: [PrivacyPageRootComponent, PrivacyPageComponent], }) export class PrivacyPageModule {} diff --git a/core/templates/pages/profile-page/profile-page-backend-api.service.spec.ts b/core/templates/pages/profile-page/profile-page-backend-api.service.spec.ts index 5391138e01df..46ced2b39419 100644 --- a/core/templates/pages/profile-page/profile-page-backend-api.service.spec.ts +++ b/core/templates/pages/profile-page/profile-page-backend-api.service.spec.ts @@ -16,30 +16,29 @@ * @fileoverview Unit tests for ProfilePageBackendApiService. */ +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; -import { HttpClientTestingModule, HttpTestingController } from - '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; - -import { ProfilePageBackendApiService } from - 'pages/profile-page/profile-page-backend-api.service'; -import { UrlService } from 'services/contextual/url.service'; +import {ProfilePageBackendApiService} from 'pages/profile-page/profile-page-backend-api.service'; +import {UrlService} from 'services/contextual/url.service'; describe('Profile test backend API service', () => { let profilePageBackendApiService: ProfilePageBackendApiService; let httpTestingController: HttpTestingController; let urlService: UrlService; - let expectedBody = { creator_username: 'testUsername' }; + let expectedBody = {creator_username: 'testUsername'}; let ERROR_STATUS_CODE = 500; beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [ProfilePageBackendApiService] + providers: [ProfilePageBackendApiService], }); - profilePageBackendApiService = TestBed.get( - ProfilePageBackendApiService); + profilePageBackendApiService = TestBed.get(ProfilePageBackendApiService); httpTestingController = TestBed.get(HttpTestingController); urlService = TestBed.get(UrlService); @@ -50,184 +49,211 @@ describe('Profile test backend API service', () => { httpTestingController.verify(); }); - it('should successfully post subscribe to ' + - 'the backend', fakeAsync(() => { - let successHandler = jasmine.createSpy('success'); - let failHandler = jasmine.createSpy('fail'); + it( + 'should successfully post subscribe to ' + 'the backend', + fakeAsync(() => { + let successHandler = jasmine.createSpy('success'); + let failHandler = jasmine.createSpy('fail'); - profilePageBackendApiService.subscribeAsync('testUsername').then( - successHandler, failHandler); + profilePageBackendApiService + .subscribeAsync('testUsername') + .then(successHandler, failHandler); - let req = httpTestingController.expectOne('/subscribehandler'); - expect(req.request.method).toEqual('POST'); - expect(req.request.body).toEqual(expectedBody); - req.flush({}); + let req = httpTestingController.expectOne('/subscribehandler'); + expect(req.request.method).toEqual('POST'); + expect(req.request.body).toEqual(expectedBody); + req.flush({}); - flushMicrotasks(); + flushMicrotasks(); - expect(successHandler).toHaveBeenCalled(); - expect(failHandler).not.toHaveBeenCalled(); - }) + expect(successHandler).toHaveBeenCalled(); + expect(failHandler).not.toHaveBeenCalled(); + }) ); - it('should use the rejection handler if the backend request' + - 'failed on subscribe', fakeAsync(() => { - let successHandler = jasmine.createSpy('success'); - let failHandler = jasmine.createSpy('fail'); - - profilePageBackendApiService.subscribeAsync('testUsername').then( - successHandler, failHandler); - - let req = httpTestingController.expectOne('/subscribehandler'); - expect(req.request.method).toEqual('POST'); - expect(req.request.body).toEqual(expectedBody); - req.flush('Error loading data.', { - status: ERROR_STATUS_CODE, statusText: 'Invalid Request' - }); - - flushMicrotasks(); - - expect(successHandler).not.toHaveBeenCalled(); - expect(failHandler).toHaveBeenCalled(); - })); - - it('should successfully post unsubscribe to ' + - 'the backend', fakeAsync(() => { - let successHandler = jasmine.createSpy('success'); - let failHandler = jasmine.createSpy('fail'); - - profilePageBackendApiService.unsubscribeAsync('testUsername').then( - successHandler, failHandler); - - let req = httpTestingController.expectOne('/unsubscribehandler'); - expect(req.request.method).toEqual('POST'); - expect(req.request.body).toEqual(expectedBody); - req.flush({}); - - flushMicrotasks(); - - expect(successHandler).toHaveBeenCalled(); - expect(failHandler).not.toHaveBeenCalled(); - }) + it( + 'should use the rejection handler if the backend request' + + 'failed on subscribe', + fakeAsync(() => { + let successHandler = jasmine.createSpy('success'); + let failHandler = jasmine.createSpy('fail'); + + profilePageBackendApiService + .subscribeAsync('testUsername') + .then(successHandler, failHandler); + + let req = httpTestingController.expectOne('/subscribehandler'); + expect(req.request.method).toEqual('POST'); + expect(req.request.body).toEqual(expectedBody); + req.flush('Error loading data.', { + status: ERROR_STATUS_CODE, + statusText: 'Invalid Request', + }); + + flushMicrotasks(); + + expect(successHandler).not.toHaveBeenCalled(); + expect(failHandler).toHaveBeenCalled(); + }) ); - it('should use the rejection handler if the backend request' + - 'failed on unsubscribe', fakeAsync(() => { - let successHandler = jasmine.createSpy('success'); - let failHandler = jasmine.createSpy('fail'); + it( + 'should successfully post unsubscribe to ' + 'the backend', + fakeAsync(() => { + let successHandler = jasmine.createSpy('success'); + let failHandler = jasmine.createSpy('fail'); - profilePageBackendApiService.unsubscribeAsync('testUsername').then( - successHandler, failHandler); + profilePageBackendApiService + .unsubscribeAsync('testUsername') + .then(successHandler, failHandler); - let req = httpTestingController.expectOne('/unsubscribehandler'); - expect(req.request.method).toEqual('POST'); - expect(req.request.body).toEqual(expectedBody); - req.flush('Error loading data.', { - status: ERROR_STATUS_CODE, statusText: 'Invalid Request' - }); + let req = httpTestingController.expectOne('/unsubscribehandler'); + expect(req.request.method).toEqual('POST'); + expect(req.request.body).toEqual(expectedBody); + req.flush({}); - flushMicrotasks(); - - expect(successHandler).not.toHaveBeenCalled(); - expect(failHandler).toHaveBeenCalled(); - })); - - it('should successfully fetch profile data from ' + - 'the backend', fakeAsync(() => { - let successHandler = jasmine.createSpy('success'); - let failHandler = jasmine.createSpy('fail'); - - profilePageBackendApiService.fetchProfileDataAsync().then( - successHandler, failHandler); - - let req = httpTestingController - .expectOne('/profilehandler/data/testUsername'); - expect(req.request.method).toEqual('GET'); - req.flush({ - username: 'user1', - profile_is_of_current_user: false, - is_user_visiting_own_profile: false, - created_exp_summary_dicts: [{ - last_updated_msec: 1591296737470.528, - community_owned: false, - objective: 'Test Objective', - id: '44LKoKLlIbGe', - num_views: 0, - thumbnail_icon_url: '/subjects/Algebra.svg', - human_readable_contributors_summary: {}, - language_code: 'en', - thumbnail_bg_color: '#cc4b00', - created_on_msec: 1591296635736.666, - ratings: { - 1: 0, - 2: 0, - 3: 0, - 4: 0, - 5: 0 - }, - status: 'public', - tags: [], - activity_type: 'exploration', - category: 'Algebra', - title: 'Test Title' - }], - is_already_subscribed: false, - first_contribution_msec: null, - user_impact_score: 0, - edited_exp_summary_dicts: [{ - last_updated_msec: 1591296737470.528, - community_owned: false, - objective: 'Test Objective', - id: '44LKoKLlIbGe', - num_views: 0, - thumbnail_icon_url: '/subjects/Algebra.svg', - human_readable_contributors_summary: {}, - language_code: 'en', - thumbnail_bg_color: '#cc4b00', - created_on_msec: 1591296635736.666, - ratings: { - 1: 0, - 2: 0, - 3: 0, - 4: 0, - 5: 0 - }, - status: 'public', - tags: [], - activity_type: 'exploration', - category: 'Algebra', - title: 'Test Title' - }], - subject_interests: [], - username_of_viewed_profile: 'user2', - user_bio: 'hi', - }); + flushMicrotasks(); - flushMicrotasks(); - - expect(successHandler).toHaveBeenCalled(); - expect(failHandler).not.toHaveBeenCalled(); - }) + expect(successHandler).toHaveBeenCalled(); + expect(failHandler).not.toHaveBeenCalled(); + }) ); - it('should use the rejection handler if the backend request' + - 'failed on fetch profile data', fakeAsync(() => { - let successHandler = jasmine.createSpy('success'); - let failHandler = jasmine.createSpy('fail'); - - profilePageBackendApiService.fetchProfileDataAsync().then( - successHandler, failHandler); - - let req = httpTestingController - .expectOne('/profilehandler/data/testUsername'); - expect(req.request.method).toEqual('GET'); - req.flush('Error loading data.', { - status: ERROR_STATUS_CODE, statusText: 'Invalid Request' - }); + it( + 'should use the rejection handler if the backend request' + + 'failed on unsubscribe', + fakeAsync(() => { + let successHandler = jasmine.createSpy('success'); + let failHandler = jasmine.createSpy('fail'); + + profilePageBackendApiService + .unsubscribeAsync('testUsername') + .then(successHandler, failHandler); + + let req = httpTestingController.expectOne('/unsubscribehandler'); + expect(req.request.method).toEqual('POST'); + expect(req.request.body).toEqual(expectedBody); + req.flush('Error loading data.', { + status: ERROR_STATUS_CODE, + statusText: 'Invalid Request', + }); + + flushMicrotasks(); + + expect(successHandler).not.toHaveBeenCalled(); + expect(failHandler).toHaveBeenCalled(); + }) + ); - flushMicrotasks(); + it( + 'should successfully fetch profile data from ' + 'the backend', + fakeAsync(() => { + let successHandler = jasmine.createSpy('success'); + let failHandler = jasmine.createSpy('fail'); + + profilePageBackendApiService + .fetchProfileDataAsync() + .then(successHandler, failHandler); + + let req = httpTestingController.expectOne( + '/profilehandler/data/testUsername' + ); + expect(req.request.method).toEqual('GET'); + req.flush({ + username: 'user1', + profile_is_of_current_user: false, + is_user_visiting_own_profile: false, + created_exp_summary_dicts: [ + { + last_updated_msec: 1591296737470.528, + community_owned: false, + objective: 'Test Objective', + id: '44LKoKLlIbGe', + num_views: 0, + thumbnail_icon_url: '/subjects/Algebra.svg', + human_readable_contributors_summary: {}, + language_code: 'en', + thumbnail_bg_color: '#cc4b00', + created_on_msec: 1591296635736.666, + ratings: { + 1: 0, + 2: 0, + 3: 0, + 4: 0, + 5: 0, + }, + status: 'public', + tags: [], + activity_type: 'exploration', + category: 'Algebra', + title: 'Test Title', + }, + ], + is_already_subscribed: false, + first_contribution_msec: null, + user_impact_score: 0, + edited_exp_summary_dicts: [ + { + last_updated_msec: 1591296737470.528, + community_owned: false, + objective: 'Test Objective', + id: '44LKoKLlIbGe', + num_views: 0, + thumbnail_icon_url: '/subjects/Algebra.svg', + human_readable_contributors_summary: {}, + language_code: 'en', + thumbnail_bg_color: '#cc4b00', + created_on_msec: 1591296635736.666, + ratings: { + 1: 0, + 2: 0, + 3: 0, + 4: 0, + 5: 0, + }, + status: 'public', + tags: [], + activity_type: 'exploration', + category: 'Algebra', + title: 'Test Title', + }, + ], + subject_interests: [], + username_of_viewed_profile: 'user2', + user_bio: 'hi', + }); + + flushMicrotasks(); + + expect(successHandler).toHaveBeenCalled(); + expect(failHandler).not.toHaveBeenCalled(); + }) + ); - expect(successHandler).not.toHaveBeenCalled(); - expect(failHandler).toHaveBeenCalled(); - })); + it( + 'should use the rejection handler if the backend request' + + 'failed on fetch profile data', + fakeAsync(() => { + let successHandler = jasmine.createSpy('success'); + let failHandler = jasmine.createSpy('fail'); + + profilePageBackendApiService + .fetchProfileDataAsync() + .then(successHandler, failHandler); + + let req = httpTestingController.expectOne( + '/profilehandler/data/testUsername' + ); + expect(req.request.method).toEqual('GET'); + req.flush('Error loading data.', { + status: ERROR_STATUS_CODE, + statusText: 'Invalid Request', + }); + + flushMicrotasks(); + + expect(successHandler).not.toHaveBeenCalled(); + expect(failHandler).toHaveBeenCalled(); + }) + ); }); diff --git a/core/templates/pages/profile-page/profile-page-backend-api.service.ts b/core/templates/pages/profile-page/profile-page-backend-api.service.ts index 61053cd971d7..d79cc4146834 100644 --- a/core/templates/pages/profile-page/profile-page-backend-api.service.ts +++ b/core/templates/pages/profile-page/profile-page-backend-api.service.ts @@ -17,20 +17,20 @@ * backend. */ -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; -import { ProfilePageDomainConstants } from - 'pages/profile-page/profile-page-domain.constants'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; -import { UrlService } from 'services/contextual/url.service'; -import { UserProfile, UserProfileBackendDict } from - 'domain/user/user-profile.model'; -import { UserService } from 'services/user.service'; +import {ProfilePageDomainConstants} from 'pages/profile-page/profile-page-domain.constants'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {UrlService} from 'services/contextual/url.service'; +import { + UserProfile, + UserProfileBackendDict, +} from 'domain/user/user-profile.model'; +import {UserService} from 'services/user.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ProfilePageBackendApiService { constructor( @@ -41,35 +41,49 @@ export class ProfilePageBackendApiService { ) {} async _postSubscribeAsync(creatorUsername: string): Promise { - return this.http.post( - ProfilePageDomainConstants.PROFILE_SUBSCRIBE_URL, - { creator_username: creatorUsername } - ).toPromise().then(() => {}, errorResponse => { - console.error(errorResponse.error.error); - throw new Error(errorResponse.error.error); - }); + return this.http + .post(ProfilePageDomainConstants.PROFILE_SUBSCRIBE_URL, { + creator_username: creatorUsername, + }) + .toPromise() + .then( + () => {}, + errorResponse => { + console.error(errorResponse.error.error); + throw new Error(errorResponse.error.error); + } + ); } async _postUnsubscribeAsync(creatorUsername: string): Promise { - return this.http.post( - ProfilePageDomainConstants.PROFILE_UNSUBSCRIBE_URL, - { creator_username: creatorUsername } - ).toPromise().then(() => {}, errorResponse => { - throw new Error(errorResponse.error.error); - }); + return this.http + .post(ProfilePageDomainConstants.PROFILE_UNSUBSCRIBE_URL, { + creator_username: creatorUsername, + }) + .toPromise() + .then( + () => {}, + errorResponse => { + throw new Error(errorResponse.error.error); + } + ); } async _fetchProfileDataAsync(): Promise { - return this.http.get( - this.urlInterpolationService.interpolateUrl( - ProfilePageDomainConstants.PROFILE_DATA_URL, - {username: this.urlService.getUsernameFromProfileUrl()} + return this.http + .get( + this.urlInterpolationService.interpolateUrl( + ProfilePageDomainConstants.PROFILE_DATA_URL, + {username: this.urlService.getUsernameFromProfileUrl()} + ) ) - ).toPromise().then( - userProfileDict => UserProfile.createFromBackendDict( - userProfileDict), errorResponse => { - throw new Error(errorResponse.error.error); - }); + .toPromise() + .then( + userProfileDict => UserProfile.createFromBackendDict(userProfileDict), + errorResponse => { + throw new Error(errorResponse.error.error); + } + ); } /** diff --git a/core/templates/pages/profile-page/profile-page-domain.constants.ts b/core/templates/pages/profile-page/profile-page-domain.constants.ts index 5c8b60a7dca1..c636ec3e1dcc 100644 --- a/core/templates/pages/profile-page/profile-page-domain.constants.ts +++ b/core/templates/pages/profile-page/profile-page-domain.constants.ts @@ -19,5 +19,5 @@ export const ProfilePageDomainConstants = { PROFILE_SUBSCRIBE_URL: '/subscribehandler', PROFILE_UNSUBSCRIBE_URL: '/unsubscribehandler', - PROFILE_DATA_URL: '/profilehandler/data/' + PROFILE_DATA_URL: '/profilehandler/data/', } as const; diff --git a/core/templates/pages/profile-page/profile-page-navbar.component.spec.ts b/core/templates/pages/profile-page/profile-page-navbar.component.spec.ts index f53538a6616b..00cf29622598 100644 --- a/core/templates/pages/profile-page/profile-page-navbar.component.spec.ts +++ b/core/templates/pages/profile-page/profile-page-navbar.component.spec.ts @@ -16,11 +16,10 @@ * @fileoverview Unit tests for the profile page navbar component. */ -import { ComponentFixture, TestBed, async } from - '@angular/core/testing'; +import {ComponentFixture, TestBed, async} from '@angular/core/testing'; -import { ProfilePageNavbarComponent } from './profile-page-navbar.component'; -import { UrlService } from 'services/contextual/url.service'; +import {ProfilePageNavbarComponent} from './profile-page-navbar.component'; +import {UrlService} from 'services/contextual/url.service'; class MockUrlService { getUsernameFromProfileUrl() { @@ -31,13 +30,11 @@ class MockUrlService { let component: ProfilePageNavbarComponent; let fixture: ComponentFixture; -describe('Profile Page Navbar Component', function() { +describe('Profile Page Navbar Component', function () { beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ProfilePageNavbarComponent], - providers: [ - { provide: UrlService, useClass: MockUrlService } - ], + providers: [{provide: UrlService, useClass: MockUrlService}], }).compileComponents(); })); @@ -47,10 +44,8 @@ describe('Profile Page Navbar Component', function() { fixture.detectChanges(); }); - it('should get username from profile url when component calls OnInit hook', - () => { - component.ngOnInit(); - expect(component.username).toBe('username1'); - } - ); + it('should get username from profile url when component calls OnInit hook', () => { + component.ngOnInit(); + expect(component.username).toBe('username1'); + }); }); diff --git a/core/templates/pages/profile-page/profile-page-navbar.component.ts b/core/templates/pages/profile-page/profile-page-navbar.component.ts index 78be90e1e747..a99f150825c8 100644 --- a/core/templates/pages/profile-page/profile-page-navbar.component.ts +++ b/core/templates/pages/profile-page/profile-page-navbar.component.ts @@ -1,4 +1,3 @@ - // Copyright 2019 The Oppia Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,22 +16,19 @@ * @fileoverview Component for profile page */ -import { Component, OnInit } from '@angular/core'; +import {Component, OnInit} from '@angular/core'; -import { UrlService } from 'services/contextual/url.service'; +import {UrlService} from 'services/contextual/url.service'; import './profile-page-navbar.component.css'; - @Component({ selector: 'profile-page-navbar', templateUrl: './profile-page-navbar.component.html', - styleUrls: ['./profile-page-navbar.component.css'] + styleUrls: ['./profile-page-navbar.component.css'], }) export class ProfilePageNavbarComponent implements OnInit { - constructor( - private urlService: UrlService - ) {} + constructor(private urlService: UrlService) {} username: string = ''; ngOnInit(): void { diff --git a/core/templates/pages/profile-page/profile-page-root.component.spec.ts b/core/templates/pages/profile-page/profile-page-root.component.spec.ts index 1d6a213a6739..b8840f2c89df 100644 --- a/core/templates/pages/profile-page/profile-page-root.component.spec.ts +++ b/core/templates/pages/profile-page/profile-page-root.component.spec.ts @@ -16,19 +16,25 @@ * @fileoverview Unit tests for the profile page root component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; - -import { AppConstants } from 'app.constants'; -import { AccessValidationBackendApiService } from 'pages/oppia-root/routing/access-validation-backend-api.service'; -import { MetaTagCustomizationService } from 'services/contextual/meta-tag-customization.service'; -import { LoaderService } from 'services/loader.service'; -import { PageHeadService } from 'services/page-head.service'; - -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { ProfilePageRootComponent } from './profile-page-root.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; + +import {AppConstants} from 'app.constants'; +import {AccessValidationBackendApiService} from 'pages/oppia-root/routing/access-validation-backend-api.service'; +import {MetaTagCustomizationService} from 'services/contextual/meta-tag-customization.service'; +import {LoaderService} from 'services/loader.service'; +import {PageHeadService} from 'services/page-head.service'; + +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {ProfilePageRootComponent} from './profile-page-root.component'; class MockTranslateService { onLangChange: EventEmitter = new EventEmitter(); @@ -47,22 +53,17 @@ describe('Profile Page Root', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - ProfilePageRootComponent, - MockTranslatePipe - ], + imports: [HttpClientTestingModule], + declarations: [ProfilePageRootComponent, MockTranslatePipe], providers: [ PageHeadService, MetaTagCustomizationService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -72,18 +73,20 @@ describe('Profile Page Root', () => { pageHeadService = TestBed.inject(PageHeadService); loaderService = TestBed.inject(LoaderService); accessValidationBackendApiService = TestBed.inject( - AccessValidationBackendApiService); + AccessValidationBackendApiService + ); translateService = TestBed.inject(TranslateService); }); - it('should successfully instantiate the component', - () => { - expect(component).toBeDefined(); - }); + it('should successfully instantiate the component', () => { + expect(component).toBeDefined(); + }); it('should initialize and show page when access is valid', fakeAsync(() => { - spyOn(accessValidationBackendApiService, 'doesProfileExist') - .and.returnValue(Promise.resolve()); + spyOn( + accessValidationBackendApiService, + 'doesProfileExist' + ).and.returnValue(Promise.resolve()); spyOn(loaderService, 'showLoadingScreen'); spyOn(loaderService, 'hideLoadingScreen'); @@ -92,33 +95,38 @@ describe('Profile Page Root', () => { expect(loaderService.showLoadingScreen).toHaveBeenCalled(); expect( - accessValidationBackendApiService.doesProfileExist).toHaveBeenCalled(); + accessValidationBackendApiService.doesProfileExist + ).toHaveBeenCalled(); expect(component.pageIsShown).toBeTrue(); expect(component.errorPageIsShown).toBeFalse(); expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); })); - it('should initialize and show error page when server respond with error', - fakeAsync(() => { - spyOn(accessValidationBackendApiService, 'doesProfileExist') - .and.returnValue(Promise.reject()); - spyOn(loaderService, 'showLoadingScreen'); - spyOn(loaderService, 'hideLoadingScreen'); + it('should initialize and show error page when server respond with error', fakeAsync(() => { + spyOn( + accessValidationBackendApiService, + 'doesProfileExist' + ).and.returnValue(Promise.reject()); + spyOn(loaderService, 'showLoadingScreen'); + spyOn(loaderService, 'hideLoadingScreen'); - component.ngOnInit(); - tick(); + component.ngOnInit(); + tick(); - expect(loaderService.showLoadingScreen).toHaveBeenCalled(); - expect(accessValidationBackendApiService.doesProfileExist) - .toHaveBeenCalled(); - expect(component.pageIsShown).toBeFalse(); - expect(component.errorPageIsShown).toBeTrue(); - expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); - })); + expect(loaderService.showLoadingScreen).toHaveBeenCalled(); + expect( + accessValidationBackendApiService.doesProfileExist + ).toHaveBeenCalled(); + expect(component.pageIsShown).toBeFalse(); + expect(component.errorPageIsShown).toBeTrue(); + expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); + })); it('should initialize and subscribe to onLangChange', fakeAsync(() => { - spyOn(accessValidationBackendApiService, 'doesProfileExist') - .and.returnValue(Promise.resolve()); + spyOn( + accessValidationBackendApiService, + 'doesProfileExist' + ).and.returnValue(Promise.resolve()); spyOn(component.directiveSubscriptions, 'add'); spyOn(translateService.onLangChange, 'subscribe'); @@ -130,8 +138,10 @@ describe('Profile Page Root', () => { })); it('should update page title whenever the language changes', () => { - spyOn(accessValidationBackendApiService, 'doesProfileExist') - .and.returnValue(Promise.resolve()); + spyOn( + accessValidationBackendApiService, + 'doesProfileExist' + ).and.returnValue(Promise.resolve()); component.ngOnInit(); spyOn(component, 'setPageTitleAndMetaTags'); @@ -147,10 +157,12 @@ describe('Profile Page Root', () => { component.setPageTitleAndMetaTags(); expect(translateService.instant).toHaveBeenCalledWith( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PROFILE.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PROFILE.TITLE + ); expect(pageHeadService.updateTitleAndMetaTags).toHaveBeenCalledWith( AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PROFILE.TITLE, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PROFILE.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PROFILE.META + ); }); it('should unsubscribe on component destruction', () => { diff --git a/core/templates/pages/profile-page/profile-page-root.component.ts b/core/templates/pages/profile-page/profile-page-root.component.ts index 00c2e5da12b0..e0e31ab44d04 100644 --- a/core/templates/pages/profile-page/profile-page-root.component.ts +++ b/core/templates/pages/profile-page/profile-page-root.component.ts @@ -16,19 +16,19 @@ * @fileoverview Root component for Profile Page. */ -import { Component, OnDestroy } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; +import {Component, OnDestroy} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; -import { AppConstants } from 'app.constants'; -import { AccessValidationBackendApiService } from 'pages/oppia-root/routing/access-validation-backend-api.service'; -import { UrlService } from 'services/contextual/url.service'; -import { LoaderService } from 'services/loader.service'; -import { PageHeadService } from 'services/page-head.service'; +import {AppConstants} from 'app.constants'; +import {AccessValidationBackendApiService} from 'pages/oppia-root/routing/access-validation-backend-api.service'; +import {UrlService} from 'services/contextual/url.service'; +import {LoaderService} from 'services/loader.service'; +import {PageHeadService} from 'services/page-head.service'; @Component({ selector: 'oppia-profile-page-root', - templateUrl: './profile-page-root.component.html' + templateUrl: './profile-page-root.component.html', }) export class ProfilePageRootComponent implements OnDestroy { directiveSubscriptions = new Subscription(); @@ -36,8 +36,7 @@ export class ProfilePageRootComponent implements OnDestroy { errorPageIsShown: boolean = false; constructor( - private accessValidationBackendApiService: - AccessValidationBackendApiService, + private accessValidationBackendApiService: AccessValidationBackendApiService, private loaderService: LoaderService, private pageHeadService: PageHeadService, private urlService: UrlService, @@ -46,10 +45,12 @@ export class ProfilePageRootComponent implements OnDestroy { setPageTitleAndMetaTags(): void { let translatedTitle = this.translateService.instant( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PROFILE.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PROFILE.TITLE + ); this.pageHeadService.updateTitleAndMetaTags( translatedTitle, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PROFILE.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PROFILE.META + ); } ngOnInit(): void { @@ -61,12 +62,17 @@ export class ProfilePageRootComponent implements OnDestroy { let username = this.urlService.getPathname().split('/')[2]; this.loaderService.showLoadingScreen('Loading'); - this.accessValidationBackendApiService.doesProfileExist(username) + this.accessValidationBackendApiService + .doesProfileExist(username) + .then( + () => { + this.pageIsShown = true; + }, + err => { + this.errorPageIsShown = true; + } + ) .then(() => { - this.pageIsShown = true; - }, (err) => { - this.errorPageIsShown = true; - }).then(() => { this.loaderService.hideLoadingScreen(); }); } diff --git a/core/templates/pages/profile-page/profile-page-routing.module.ts b/core/templates/pages/profile-page/profile-page-routing.module.ts index 1b11f72ce376..28748f4e6a60 100644 --- a/core/templates/pages/profile-page/profile-page-routing.module.ts +++ b/core/templates/pages/profile-page/profile-page-routing.module.ts @@ -16,24 +16,19 @@ * @fileoverview Routing module for profile page. */ -import { NgModule } from '@angular/core'; -import { Route, RouterModule } from '@angular/router'; -import { ProfilePageRootComponent } from './profile-page-root.component'; +import {NgModule} from '@angular/core'; +import {Route, RouterModule} from '@angular/router'; +import {ProfilePageRootComponent} from './profile-page-root.component'; const routes: Route[] = [ { path: '', - component: ProfilePageRootComponent - } + component: ProfilePageRootComponent, + }, ]; @NgModule({ - imports: [ - RouterModule.forChild(routes) - ], - exports: [ - RouterModule - ] + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], }) - export class ProfilePageRoutingModule {} diff --git a/core/templates/pages/profile-page/profile-page.component.spec.ts b/core/templates/pages/profile-page/profile-page.component.spec.ts index f0a2006f288c..a7e8d3837dc5 100644 --- a/core/templates/pages/profile-page/profile-page.component.spec.ts +++ b/core/templates/pages/profile-page/profile-page.component.spec.ts @@ -16,24 +16,30 @@ * @fileoverview Unit tests for profile page component. */ -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ProfilePageComponent } from './profile-page.component'; -import { ProfilePageBackendApiService } from './profile-page-backend-api.service'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { UrlService } from 'services/contextual/url.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { UserService } from 'services/user.service'; -import { CsrfTokenService } from 'services/csrf-token.service'; -import { DateTimeFormatService } from 'services/date-time-format.service'; -import { LoggerService } from 'services/contextual/logger.service'; -import { UserProfile } from 'domain/user/user-profile.model'; -import { MatCardModule } from '@angular/material/card'; -import { TruncatePipe } from 'filters/string-utility-filters/truncate.pipe'; -import { LoaderService } from 'services/loader.service'; -import { LearnerExplorationSummary } from 'domain/summary/learner-exploration-summary.model'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ProfilePageComponent} from './profile-page.component'; +import {ProfilePageBackendApiService} from './profile-page-backend-api.service'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {UrlService} from 'services/contextual/url.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {UserService} from 'services/user.service'; +import {CsrfTokenService} from 'services/csrf-token.service'; +import {DateTimeFormatService} from 'services/date-time-format.service'; +import {LoggerService} from 'services/contextual/logger.service'; +import {UserProfile} from 'domain/user/user-profile.model'; +import {MatCardModule} from '@angular/material/card'; +import {TruncatePipe} from 'filters/string-utility-filters/truncate.pipe'; +import {LoaderService} from 'services/loader.service'; +import {LearnerExplorationSummary} from 'domain/summary/learner-exploration-summary.model'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; describe('Profile page', () => { let fixture: ComponentFixture; @@ -54,56 +60,60 @@ describe('Profile page', () => { user_impact_score: 100, profile_is_of_current_user: false, is_user_visiting_own_profile: false, - created_exp_summary_dicts: [{ - last_updated_msec: 1591296737470.528, - community_owned: false, - objective: 'Test Objective', - id: '44LKoKLlIbGe', - num_views: 0, - thumbnail_icon_url: '/subjects/Algebra.svg', - human_readable_contributors_summary: {}, - language_code: 'en', - thumbnail_bg_color: '#cc4b00', - created_on_msec: 1591296635736.666, - ratings: { - 1: 0, - 2: 0, - 3: 0, - 4: 0, - 5: 0 + created_exp_summary_dicts: [ + { + last_updated_msec: 1591296737470.528, + community_owned: false, + objective: 'Test Objective', + id: '44LKoKLlIbGe', + num_views: 0, + thumbnail_icon_url: '/subjects/Algebra.svg', + human_readable_contributors_summary: {}, + language_code: 'en', + thumbnail_bg_color: '#cc4b00', + created_on_msec: 1591296635736.666, + ratings: { + 1: 0, + 2: 0, + 3: 0, + 4: 0, + 5: 0, + }, + status: 'public', + tags: [], + activity_type: 'exploration', + category: 'Algebra', + title: 'Test Title', }, - status: 'public', - tags: [], - activity_type: 'exploration', - category: 'Algebra', - title: 'Test Title' - }], + ], is_already_subscribed: false, first_contribution_msec: null, - edited_exp_summary_dicts: [{ - last_updated_msec: 1591296737470.528, - community_owned: false, - objective: 'Test Objective', - id: '44LKoKLlIbGe', - num_views: 0, - thumbnail_icon_url: '/subjects/Algebra.svg', - human_readable_contributors_summary: {}, - language_code: 'en', - thumbnail_bg_color: '#cc4b00', - created_on_msec: 1591296635736.666, - ratings: { - 1: 0, - 2: 0, - 3: 0, - 4: 0, - 5: 0 + edited_exp_summary_dicts: [ + { + last_updated_msec: 1591296737470.528, + community_owned: false, + objective: 'Test Objective', + id: '44LKoKLlIbGe', + num_views: 0, + thumbnail_icon_url: '/subjects/Algebra.svg', + human_readable_contributors_summary: {}, + language_code: 'en', + thumbnail_bg_color: '#cc4b00', + created_on_msec: 1591296635736.666, + ratings: { + 1: 0, + 2: 0, + 3: 0, + 4: 0, + 5: 0, + }, + status: 'public', + tags: [], + activity_type: 'exploration', + category: 'Algebra', + title: 'Test Title', }, - status: 'public', - tags: [], - activity_type: 'exploration', - category: 'Algebra', - title: 'Test Title' - }], + ], subject_interests: [], }); @@ -111,8 +121,8 @@ describe('Profile page', () => { nativeWindow = { location: { href: '', - reload: () => { } - } + reload: () => {}, + }, }; } @@ -138,30 +148,23 @@ describe('Profile page', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - MatCardModule - ], - declarations: [ - MockTranslatePipe, - ProfilePageComponent, - TruncatePipe - ], + imports: [HttpClientTestingModule, MatCardModule], + declarations: [MockTranslatePipe, ProfilePageComponent, TruncatePipe], providers: [ { provide: ProfilePageBackendApiService, - useClass: MockProfilePageBackendApiService + useClass: MockProfilePageBackendApiService, }, { provide: UrlService, - useClass: MockUrlService + useClass: MockUrlService, }, { provide: WindowRef, - useClass: MockWindowRef - } + useClass: MockWindowRef, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -169,26 +172,34 @@ describe('Profile page', () => { fixture = TestBed.createComponent(ProfilePageComponent); componentInstance = fixture.componentInstance; userService = TestBed.inject(UserService) as jasmine.SpyObj; - csrfTokenService = TestBed.inject(CsrfTokenService) as - jasmine.SpyObj; - dateTimeFormatService = TestBed.inject(DateTimeFormatService) as - jasmine.SpyObj; - loaderService = TestBed.inject(LoaderService) as - jasmine.SpyObj; - loggerService = TestBed.inject(LoggerService) as - jasmine.SpyObj; + csrfTokenService = TestBed.inject( + CsrfTokenService + ) as jasmine.SpyObj; + dateTimeFormatService = TestBed.inject( + DateTimeFormatService + ) as jasmine.SpyObj; + loaderService = TestBed.inject( + LoaderService + ) as jasmine.SpyObj; + loggerService = TestBed.inject( + LoggerService + ) as jasmine.SpyObj; mockWindowRef = TestBed.inject(WindowRef) as MockWindowRef; - profilePageBackendApiService = ( - TestBed.inject(ProfilePageBackendApiService) as - jasmine.SpyObj); + profilePageBackendApiService = TestBed.inject( + ProfilePageBackendApiService + ) as jasmine.SpyObj; spyOn(csrfTokenService, 'getTokenAsync').and.returnValue( - Promise.resolve('sample-csrf-token')); + Promise.resolve('sample-csrf-token') + ); i18nLanguageCodeService = TestBed.inject(I18nLanguageCodeService); spyOn(i18nLanguageCodeService, 'isCurrentLanguageRTL').and.returnValue( - true); - spyOn(userService, 'getProfileImageDataUrl').and.returnValue( - ['default-image-url-png', 'default-image-url-webp']); + true + ); + spyOn(userService, 'getProfileImageDataUrl').and.returnValue([ + 'default-image-url-png', + 'default-image-url-webp', + ]); }); afterEach(() => { @@ -203,60 +214,69 @@ describe('Profile page', () => { expect(componentInstance.data).toEqual(profileData); expect(componentInstance.userNotLoggedIn).toEqual(!profileData.username); expect(componentInstance.profileIsOfCurrentUser).toEqual( - profileData.profileIsOfCurrentUser); - expect(componentInstance.updateSubscriptionButtonPopoverText) - .toHaveBeenCalled(); + profileData.profileIsOfCurrentUser + ); + expect( + componentInstance.updateSubscriptionButtonPopoverText + ).toHaveBeenCalled(); expect(componentInstance.profilePicturePngDataUrl).toEqual( - 'default-image-url-png'); + 'default-image-url-png' + ); expect(componentInstance.profilePictureWebpDataUrl).toEqual( - 'default-image-url-webp'); + 'default-image-url-webp' + ); expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); })); - it('should get formatted date string from the timestamp in milliseconds', - () => { - // This corresponds to Fri, 21 Nov 2014 09:45:00 GMT. - let NOW_MILLIS = 1416563100000; - spyOn(dateTimeFormatService, 'getLocaleDateString').withArgs(NOW_MILLIS) - .and.returnValue('11/21/2014'); - expect(componentInstance.getLocaleDateString(NOW_MILLIS)) - .toBe('11/21/2014'); - }); - - it('should not change subscription status and change to login page', - fakeAsync(() => { - let loginUrl = 'login-url'; - spyOn(userService, 'getLoginUrlAsync').and.returnValue( - Promise.resolve(loginUrl)); - - componentInstance.ngOnInit(); - tick(); - componentInstance.changeSubscriptionStatus(); - tick(); - expect(mockWindowRef.nativeWindow.location.href).toBe(loginUrl); - })); + it('should get formatted date string from the timestamp in milliseconds', () => { + // This corresponds to Fri, 21 Nov 2014 09:45:00 GMT. + let NOW_MILLIS = 1416563100000; + spyOn(dateTimeFormatService, 'getLocaleDateString') + .withArgs(NOW_MILLIS) + .and.returnValue('11/21/2014'); + expect(componentInstance.getLocaleDateString(NOW_MILLIS)).toBe( + '11/21/2014' + ); + }); - it('should not change subscription status and reload the page when login' + - ' page is not provided', fakeAsync(() => { - spyOn(mockWindowRef.nativeWindow.location, 'reload'); + it('should not change subscription status and change to login page', fakeAsync(() => { + let loginUrl = 'login-url'; spyOn(userService, 'getLoginUrlAsync').and.returnValue( - Promise.resolve('')); + Promise.resolve(loginUrl) + ); componentInstance.ngOnInit(); tick(); componentInstance.changeSubscriptionStatus(); tick(); - expect(mockWindowRef.nativeWindow.location.reload).toHaveBeenCalled(); + expect(mockWindowRef.nativeWindow.location.href).toBe(loginUrl); })); - it('should update subscription button text to warn user to log in', + it( + 'should not change subscription status and reload the page when login' + + ' page is not provided', fakeAsync(() => { + spyOn(mockWindowRef.nativeWindow.location, 'reload'); + spyOn(userService, 'getLoginUrlAsync').and.returnValue( + Promise.resolve('') + ); + componentInstance.ngOnInit(); tick(); - componentInstance.updateSubscriptionButtonPopoverText(); - expect(componentInstance.subscriptionButtonPopoverText).toBe( - 'Log in or sign up to subscribe to your favorite creators.'); - })); + componentInstance.changeSubscriptionStatus(); + tick(); + expect(mockWindowRef.nativeWindow.location.reload).toHaveBeenCalled(); + }) + ); + + it('should update subscription button text to warn user to log in', fakeAsync(() => { + componentInstance.ngOnInit(); + tick(); + componentInstance.updateSubscriptionButtonPopoverText(); + expect(componentInstance.subscriptionButtonPopoverText).toBe( + 'Log in or sign up to subscribe to your favorite creators.' + ); + })); it('should subscribe and unsubscribe from a profile', fakeAsync(() => { let profileDataLocal = UserProfile.createFromBackendDict({ @@ -272,8 +292,10 @@ describe('Profile page', () => { first_contribution_msec: null, subject_interests: [], }); - spyOn(profilePageBackendApiService, 'fetchProfileDataAsync') - .and.returnValue(Promise.resolve(profileDataLocal)); + spyOn( + profilePageBackendApiService, + 'fetchProfileDataAsync' + ).and.returnValue(Promise.resolve(profileDataLocal)); componentInstance.ngOnInit(); tick(); expect(componentInstance.isAlreadySubscribed).toBe(false); @@ -283,8 +305,10 @@ describe('Profile page', () => { expect(componentInstance.isAlreadySubscribed).toBe(true); expect(componentInstance.subscriptionButtonPopoverText).toBe( 'Unsubscribe to stop receiving email notifications regarding new' + - ' explorations published by ' + profileDataLocal.usernameOfViewedProfile + - '.'); + ' explorations published by ' + + profileDataLocal.usernameOfViewedProfile + + '.' + ); componentInstance.changeSubscriptionStatus(); tick(); @@ -292,20 +316,21 @@ describe('Profile page', () => { expect(componentInstance.isAlreadySubscribed).toBe(false); expect(componentInstance.subscriptionButtonPopoverText).toBe( 'Receive email notifications, whenever ' + - profileDataLocal.usernameOfViewedProfile + - ' publishes a new exploration.'); + profileDataLocal.usernameOfViewedProfile + + ' publishes a new exploration.' + ); })); - it('should get explorations to display when edited explorations are empty', - fakeAsync(() => { - let profileDataLocal = UserProfile.createFromBackendDict({ - username: '', - username_of_viewed_profile: 'username1', - user_bio: 'User bio', - user_impact_score: 100, - profile_is_of_current_user: false, - is_user_visiting_own_profile: false, - created_exp_summary_dicts: [{ + it('should get explorations to display when edited explorations are empty', fakeAsync(() => { + let profileDataLocal = UserProfile.createFromBackendDict({ + username: '', + username_of_viewed_profile: 'username1', + user_bio: 'User bio', + user_impact_score: 100, + profile_is_of_current_user: false, + is_user_visiting_own_profile: false, + created_exp_summary_dicts: [ + { last_updated_msec: 1591296737470.528, community_owned: false, objective: 'Test Objective', @@ -321,77 +346,81 @@ describe('Profile page', () => { 2: 0, 3: 0, 4: 0, - 5: 0 + 5: 0, }, status: 'public', tags: [], activity_type: 'exploration', category: 'Algebra', - title: 'Test Title' - }], - is_already_subscribed: false, - first_contribution_msec: null, - edited_exp_summary_dicts: [], - subject_interests: [], - }); - spyOn(profilePageBackendApiService, 'fetchProfileDataAsync') - .and.returnValue(Promise.resolve(profileDataLocal)); - componentInstance.ngOnInit(); - tick(); - expect(componentInstance.getExplorationsToDisplay()).toEqual([]); - })); + title: 'Test Title', + }, + ], + is_already_subscribed: false, + first_contribution_msec: null, + edited_exp_summary_dicts: [], + subject_interests: [], + }); + spyOn( + profilePageBackendApiService, + 'fetchProfileDataAsync' + ).and.returnValue(Promise.resolve(profileDataLocal)); + componentInstance.ngOnInit(); + tick(); + expect(componentInstance.getExplorationsToDisplay()).toEqual([]); + })); - it('should get explorations to display', - fakeAsync(() => { - let profileDataLocal = UserProfile.createFromBackendDict({ - username: '', - username_of_viewed_profile: 'username1', - user_bio: 'User bio', - user_impact_score: 100, - profile_is_of_current_user: false, - is_user_visiting_own_profile: false, - created_exp_summary_dicts: [], - is_already_subscribed: false, - first_contribution_msec: null, - edited_exp_summary_dicts: [], - subject_interests: [], - }); - - for (let i = 0; i < 5; i++) { - profileDataLocal.editedExpSummaries.push( - LearnerExplorationSummary.createFromBackendDict({ - last_updated_msec: 1591296737470.528, - community_owned: false, - objective: 'Test Objective', - id: '44LKoKLlIbGe', - num_views: 10, - thumbnail_icon_url: '/subjects/Algebra.svg', - human_readable_contributors_summary: {}, - language_code: 'en', - thumbnail_bg_color: '#cc4b00', - created_on_msec: 1591296635736.666, - ratings: { - 1: 0, - 2: 0, - 3: 1, - 4: 0, - 5: 0 - }, - status: 'public', - tags: [], - activity_type: 'exploration', - category: 'Algebra', - title: 'Test Title' - })); - } - - - spyOn(profilePageBackendApiService, 'fetchProfileDataAsync') - .and.returnValue(Promise.resolve(profileDataLocal)); - componentInstance.ngOnInit(); - tick(); - expect(componentInstance.getExplorationsToDisplay().length).toEqual(5); - })); + it('should get explorations to display', fakeAsync(() => { + let profileDataLocal = UserProfile.createFromBackendDict({ + username: '', + username_of_viewed_profile: 'username1', + user_bio: 'User bio', + user_impact_score: 100, + profile_is_of_current_user: false, + is_user_visiting_own_profile: false, + created_exp_summary_dicts: [], + is_already_subscribed: false, + first_contribution_msec: null, + edited_exp_summary_dicts: [], + subject_interests: [], + }); + + for (let i = 0; i < 5; i++) { + profileDataLocal.editedExpSummaries.push( + LearnerExplorationSummary.createFromBackendDict({ + last_updated_msec: 1591296737470.528, + community_owned: false, + objective: 'Test Objective', + id: '44LKoKLlIbGe', + num_views: 10, + thumbnail_icon_url: '/subjects/Algebra.svg', + human_readable_contributors_summary: {}, + language_code: 'en', + thumbnail_bg_color: '#cc4b00', + created_on_msec: 1591296635736.666, + ratings: { + 1: 0, + 2: 0, + 3: 1, + 4: 0, + 5: 0, + }, + status: 'public', + tags: [], + activity_type: 'exploration', + category: 'Algebra', + title: 'Test Title', + }) + ); + } + + spyOn( + profilePageBackendApiService, + 'fetchProfileDataAsync' + ).and.returnValue(Promise.resolve(profileDataLocal)); + componentInstance.ngOnInit(); + tick(); + expect(componentInstance.getExplorationsToDisplay().length).toEqual(5); + })); it('should go back and forth between pages', fakeAsync(() => { for (let i = 0; i < 5; i++) { @@ -412,14 +441,15 @@ describe('Profile page', () => { 2: 0, 3: 1, 4: 0, - 5: 0 + 5: 0, }, status: 'public', tags: [], activity_type: 'exploration', category: 'Algebra', - title: 'Test Title' - })); + title: 'Test Title', + }) + ); } profileData.editedExpSummaries.push( @@ -439,14 +469,15 @@ describe('Profile page', () => { 2: 1, 3: 1, 4: 0, - 5: 0 + 5: 0, }, status: 'public', tags: [], activity_type: 'exploration', category: 'Algebra', - title: 'Test Title' - })); + title: 'Test Title', + }) + ); profileData.editedExpSummaries.push( LearnerExplorationSummary.createFromBackendDict({ @@ -465,14 +496,15 @@ describe('Profile page', () => { 2: 1, 3: 2, 4: 0, - 5: 0 + 5: 0, }, status: 'public', tags: [], activity_type: 'exploration', category: 'Algebra', - title: 'Test Title' - })); + title: 'Test Title', + }) + ); profileData.editedExpSummaries.push( LearnerExplorationSummary.createFromBackendDict({ @@ -491,14 +523,15 @@ describe('Profile page', () => { 2: 0, 3: 2, 4: 0, - 5: 1 + 5: 1, }, status: 'public', tags: [], activity_type: 'exploration', category: 'Algebra', - title: 'Test Title' - })); + title: 'Test Title', + }) + ); profileData.editedExpSummaries.push( LearnerExplorationSummary.createFromBackendDict({ @@ -517,14 +550,15 @@ describe('Profile page', () => { 2: 0, 3: 2, 4: 0, - 5: 1 + 5: 1, }, status: 'public', tags: [], activity_type: 'exploration', category: 'Algebra', - title: 'Test Title' - })); + title: 'Test Title', + }) + ); profileData.editedExpSummaries.push( LearnerExplorationSummary.createFromBackendDict({ @@ -543,17 +577,20 @@ describe('Profile page', () => { 2: 0, 3: 2, 4: 0, - 5: 1 + 5: 1, }, status: 'public', tags: [], activity_type: 'exploration', category: 'Algebra', - title: 'Test Title' - })); + title: 'Test Title', + }) + ); - spyOn(profilePageBackendApiService, 'fetchProfileDataAsync') - .and.returnValue(Promise.resolve(profileData)); + spyOn( + profilePageBackendApiService, + 'fetchProfileDataAsync' + ).and.returnValue(Promise.resolve(profileData)); componentInstance.ngOnInit(); tick(); @@ -568,7 +605,8 @@ describe('Profile page', () => { componentInstance.goToNextPage(); expect(loggerService.error).toHaveBeenCalledWith( - 'Error: Cannot increment page'); + 'Error: Cannot increment page' + ); componentInstance.goToPreviousPage(); @@ -580,6 +618,7 @@ describe('Profile page', () => { expect(componentInstance.currentPageNumber).toBe(0); expect(loggerService.error).toHaveBeenCalledWith( - 'Error: cannot decrement page'); + 'Error: cannot decrement page' + ); })); }); diff --git a/core/templates/pages/profile-page/profile-page.component.ts b/core/templates/pages/profile-page/profile-page.component.ts index 565709bd6820..a906370cc401 100644 --- a/core/templates/pages/profile-page/profile-page.component.ts +++ b/core/templates/pages/profile-page/profile-page.component.ts @@ -16,22 +16,21 @@ * @fileoverview Component for the Oppia profile page. */ -import { Component } from '@angular/core'; +import {Component} from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { RatingComputationService } from 'components/ratings/rating-computation/rating-computation.service'; -import { LearnerExplorationSummary } from 'domain/summary/learner-exploration-summary.model'; -import { UserProfile } from 'domain/user/user-profile.model'; -import { LoggerService } from 'services/contextual/logger.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { DateTimeFormatService } from 'services/date-time-format.service'; -import { LoaderService } from 'services/loader.service'; -import { UserService } from 'services/user.service'; -import { ProfilePageBackendApiService } from './profile-page-backend-api.service'; +import {AppConstants} from 'app.constants'; +import {RatingComputationService} from 'components/ratings/rating-computation/rating-computation.service'; +import {LearnerExplorationSummary} from 'domain/summary/learner-exploration-summary.model'; +import {UserProfile} from 'domain/user/user-profile.model'; +import {LoggerService} from 'services/contextual/logger.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {DateTimeFormatService} from 'services/date-time-format.service'; +import {LoaderService} from 'services/loader.service'; +import {UserService} from 'services/user.service'; +import {ProfilePageBackendApiService} from './profile-page-backend-api.service'; import './profile-page.component.css'; - interface ViewedProfileUsername { title: string; value: string; @@ -47,13 +46,13 @@ interface UserDisplayedStatistic { @Component({ selector: 'oppia-profile-page', templateUrl: './profile-page.component.html', - styleUrls: ['./profile-page.component.css'] + styleUrls: ['./profile-page.component.css'], }) export class ProfilePageComponent { username: ViewedProfileUsername = { title: '', value: '', - helpText: '' + helpText: '', }; // These properties are initialized using Angular lifecycle hooks @@ -83,8 +82,8 @@ export class ProfilePageComponent { subjectInterests: string[] = []; profilePicturePngDataUrl!: string; profilePictureWebpDataUrl!: string; - preferencesUrl = ( - '/' + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PREFERENCES.ROUTE); + preferencesUrl = + '/' + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.PREFERENCES.ROUTE; constructor( private dateTimeFormatService: DateTimeFormatService, @@ -93,133 +92,133 @@ export class ProfilePageComponent { private profilePageBackendApiService: ProfilePageBackendApiService, private ratingComputationService: RatingComputationService, private userService: UserService, - private windowRef: WindowRef, - ) { } + private windowRef: WindowRef + ) {} ngOnInit(): void { this.loaderService.showLoadingScreen('Loading'); - this.profilePageBackendApiService.fetchProfileDataAsync() - .then((data) => { - this.data = data; - this.username = { - title: 'Username', - value: data.usernameOfViewedProfile, - helpText: data.usernameOfViewedProfile - }; - this.usernameIsLong = data.usernameOfViewedProfile.length > 16; - this.userBio = data.userBio; - this.userDisplayedStatistics = [{ + this.profilePageBackendApiService.fetchProfileDataAsync().then(data => { + this.data = data; + this.username = { + title: 'Username', + value: data.usernameOfViewedProfile, + helpText: data.usernameOfViewedProfile, + }; + this.usernameIsLong = data.usernameOfViewedProfile.length > 16; + this.userBio = data.userBio; + this.userDisplayedStatistics = [ + { title: 'Impact', value: data.userImpactScore, - helpText: ( + helpText: 'A rough measure of the impact of explorations created by ' + 'this user. Better ratings and more playthroughs improve ' + - 'this score.') - }, { + 'this score.', + }, + { title: 'Created', value: data.createdExpSummaries.length, - helpText: null - }, { + helpText: null, + }, + { title: 'Edited', value: data.createdExpSummaries.length, - helpText: null - }]; + helpText: null, + }, + ]; - this.userEditedExplorations = data.editedExpSummaries.sort( - (exploration1, exploration2) => { - const avgRating1 = ( - this.ratingComputationService.computeAverageRating( - exploration1.ratings)); - const avgRating2 = ( - this.ratingComputationService.computeAverageRating( - exploration2.ratings)); - if (avgRating2 === null) { - return 1; - } - if (avgRating1 !== null && (avgRating1 > avgRating2)) { + this.userEditedExplorations = data.editedExpSummaries.sort( + (exploration1, exploration2) => { + const avgRating1 = this.ratingComputationService.computeAverageRating( + exploration1.ratings + ); + const avgRating2 = this.ratingComputationService.computeAverageRating( + exploration2.ratings + ); + if (avgRating2 === null) { + return 1; + } + if (avgRating1 !== null && avgRating1 > avgRating2) { + return 1; + } else if (avgRating1 === avgRating2) { + if (exploration1.numViews > exploration2.numViews) { return 1; - } else if (avgRating1 === avgRating2) { - if (exploration1.numViews > exploration2.numViews) { - return 1; - } else if ( - exploration1.numViews === exploration2.numViews) { - return 0; - } else { - return -1; - } + } else if (exploration1.numViews === exploration2.numViews) { + return 0; } else { return -1; } + } else { + return -1; } - ); + } + ); - this.userNotLoggedIn = !data.username; - this.isAlreadySubscribed = data.isAlreadySubscribed; - this.isUserVisitingOwnProfile = data.isUserVisitingOwnProfile; + this.userNotLoggedIn = !data.username; + this.isAlreadySubscribed = data.isAlreadySubscribed; + this.isUserVisitingOwnProfile = data.isUserVisitingOwnProfile; - this.subscriptionButtonPopoverText = ''; - this.currentPageNumber = 0; - this.PAGE_SIZE = 6; - this.startingExplorationNumber = 1; - this.endingExplorationNumber = 6; - this.profileIsOfCurrentUser = data.profileIsOfCurrentUser; + this.subscriptionButtonPopoverText = ''; + this.currentPageNumber = 0; + this.PAGE_SIZE = 6; + this.startingExplorationNumber = 1; + this.endingExplorationNumber = 6; + this.profileIsOfCurrentUser = data.profileIsOfCurrentUser; - this.updateSubscriptionButtonPopoverText(); - this.numUserPortfolioExplorations = data.editedExpSummaries.length; - this.subjectInterests = data.subjectInterests; - this.firstContributionMsec = data.firstContributionMsec; + this.updateSubscriptionButtonPopoverText(); + this.numUserPortfolioExplorations = data.editedExpSummaries.length; + this.subjectInterests = data.subjectInterests; + this.firstContributionMsec = data.firstContributionMsec; - [this.profilePicturePngDataUrl, this.profilePictureWebpDataUrl] = ( - this.userService.getProfileImageDataUrl(this.username.value)); - this.loaderService.hideLoadingScreen(); - }); + [this.profilePicturePngDataUrl, this.profilePictureWebpDataUrl] = + this.userService.getProfileImageDataUrl(this.username.value); + this.loaderService.hideLoadingScreen(); + }); } changeSubscriptionStatus(): void { if (this.userNotLoggedIn) { - this.userService.getLoginUrlAsync().then( - (loginUrl) => { - if (loginUrl) { - this.windowRef.nativeWindow.location.href = loginUrl; - } else { - this.windowRef.nativeWindow.location.reload(); - } + this.userService.getLoginUrlAsync().then(loginUrl => { + if (loginUrl) { + this.windowRef.nativeWindow.location.href = loginUrl; + } else { + this.windowRef.nativeWindow.location.reload(); } - ); + }); } else { if (!this.isAlreadySubscribed) { - this.profilePageBackendApiService.subscribeAsync( - this.data.usernameOfViewedProfile - ).then(() => { - this.isAlreadySubscribed = true; - this.updateSubscriptionButtonPopoverText(); - }); + this.profilePageBackendApiService + .subscribeAsync(this.data.usernameOfViewedProfile) + .then(() => { + this.isAlreadySubscribed = true; + this.updateSubscriptionButtonPopoverText(); + }); } else { - this.profilePageBackendApiService.unsubscribeAsync( - this.data.usernameOfViewedProfile - ).then(() => { - this.isAlreadySubscribed = false; - this.updateSubscriptionButtonPopoverText(); - }); + this.profilePageBackendApiService + .unsubscribeAsync(this.data.usernameOfViewedProfile) + .then(() => { + this.isAlreadySubscribed = false; + this.updateSubscriptionButtonPopoverText(); + }); } } } updateSubscriptionButtonPopoverText(): void { if (this.userNotLoggedIn) { - this.subscriptionButtonPopoverText = ( - 'Log in or sign up to subscribe to your ' + - 'favorite creators.'); + this.subscriptionButtonPopoverText = + 'Log in or sign up to subscribe to your ' + 'favorite creators.'; } else if (this.isAlreadySubscribed) { - this.subscriptionButtonPopoverText = ( + this.subscriptionButtonPopoverText = 'Unsubscribe to stop receiving email notifications ' + 'regarding new explorations published by ' + - this.username.value + '.'); + this.username.value + + '.'; } else { - this.subscriptionButtonPopoverText = ( + this.subscriptionButtonPopoverText = 'Receive email notifications, whenever ' + - this.username.value + ' publishes a new exploration.' - ); + this.username.value + + ' publishes a new exploration.'; } } @@ -228,10 +227,10 @@ export class ProfilePageComponent { this.loggerService.error('Error: cannot decrement page'); } else { this.currentPageNumber--; - this.startingExplorationNumber = ( - this.currentPageNumber * this.PAGE_SIZE + 1); - this.endingExplorationNumber = ( - (this.currentPageNumber + 1) * this.PAGE_SIZE); + this.startingExplorationNumber = + this.currentPageNumber * this.PAGE_SIZE + 1; + this.endingExplorationNumber = + (this.currentPageNumber + 1) * this.PAGE_SIZE; } } @@ -241,11 +240,12 @@ export class ProfilePageComponent { this.loggerService.error('Error: Cannot increment page'); } else { this.currentPageNumber++; - this.startingExplorationNumber = ( - this.currentPageNumber * this.PAGE_SIZE + 1); + this.startingExplorationNumber = + this.currentPageNumber * this.PAGE_SIZE + 1; this.endingExplorationNumber = Math.min( this.numUserPortfolioExplorations, - (this.currentPageNumber + 1) * this.PAGE_SIZE); + (this.currentPageNumber + 1) * this.PAGE_SIZE + ); } } diff --git a/core/templates/pages/profile-page/profile-page.module.ts b/core/templates/pages/profile-page/profile-page.module.ts index 859fd23f7514..bb69b12d3840 100644 --- a/core/templates/pages/profile-page/profile-page.module.ts +++ b/core/templates/pages/profile-page/profile-page.module.ts @@ -16,17 +16,16 @@ * @fileoverview Module for the profile page. */ -import { NgModule } from '@angular/core'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { ProfilePageNavbarComponent } from - 'pages/profile-page/profile-page-navbar.component'; -import { ProfilePageComponent } from './profile-page.component'; -import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'; -import { ProfilePageRootComponent } from './profile-page-root.component'; -import { CommonModule } from '@angular/common'; -import { ProfilePageRoutingModule } from './profile-page-routing.module'; -import { Error404PageModule } from 'pages/error-pages/error-404/error-404-page.module'; -import { SmartRouterModule } from 'hybrid-router-module-provider'; +import {NgModule} from '@angular/core'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {ProfilePageNavbarComponent} from 'pages/profile-page/profile-page-navbar.component'; +import {ProfilePageComponent} from './profile-page.component'; +import {NgbPopoverModule} from '@ng-bootstrap/ng-bootstrap'; +import {ProfilePageRootComponent} from './profile-page-root.component'; +import {CommonModule} from '@angular/common'; +import {ProfilePageRoutingModule} from './profile-page-routing.module'; +import {Error404PageModule} from 'pages/error-pages/error-404/error-404-page.module'; +import {SmartRouterModule} from 'hybrid-router-module-provider'; @NgModule({ imports: [ @@ -37,17 +36,17 @@ import { SmartRouterModule } from 'hybrid-router-module-provider'; // migrated to angular router. SmartRouterModule, ProfilePageRoutingModule, - Error404PageModule + Error404PageModule, ], declarations: [ ProfilePageNavbarComponent, ProfilePageComponent, - ProfilePageRootComponent + ProfilePageRootComponent, ], entryComponents: [ ProfilePageNavbarComponent, ProfilePageComponent, - ProfilePageRootComponent - ] + ProfilePageRootComponent, + ], }) export class ProfilePageModule {} diff --git a/core/templates/pages/release-coordinator-page/beam-jobs-tab/beam-jobs-tab.component.spec.ts b/core/templates/pages/release-coordinator-page/beam-jobs-tab/beam-jobs-tab.component.spec.ts index 6a2faca9aa49..de29826ce53c 100644 --- a/core/templates/pages/release-coordinator-page/beam-jobs-tab/beam-jobs-tab.component.spec.ts +++ b/core/templates/pages/release-coordinator-page/beam-jobs-tab/beam-jobs-tab.component.spec.ts @@ -16,40 +16,46 @@ * @fileoverview Unit tests for BeamJobsTabComponent. */ -import { ClipboardModule } from '@angular/cdk/clipboard'; -import { HarnessLoader } from '@angular/cdk/testing'; -import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { ReactiveFormsModule } from '@angular/forms'; -import { MatAutocompleteModule } from '@angular/material/autocomplete'; -import { MatAutocompleteHarness } from '@angular/material/autocomplete/testing'; -import { MatButtonModule } from '@angular/material/button'; -import { MatButtonHarness } from '@angular/material/button/testing'; -import { MatCardModule } from '@angular/material/card'; -import { MatDialogModule } from '@angular/material/dialog'; -import { MatDialogHarness } from '@angular/material/dialog/testing'; -import { MatIconModule } from '@angular/material/icon'; -import { MatInputModule } from '@angular/material/input'; -import { MatInputHarness } from '@angular/material/input/testing'; -import { MatProgressBarModule } from '@angular/material/progress-bar'; -import { MatTableModule } from '@angular/material/table'; -import { MatTableHarness } from '@angular/material/table/testing'; -import { MatTabsModule } from '@angular/material/tabs'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { of } from 'rxjs'; -import { marbles } from 'rxjs-marbles'; -import { BeamJobsTabComponent } from './beam-jobs-tab.component'; - -import { BeamJobRun } from 'domain/jobs/beam-job-run.model'; -import { BeamJob } from 'domain/jobs/beam-job.model'; -import { CancelBeamJobDialogComponent } from 'pages/release-coordinator-page/components/cancel-beam-job-dialog.component'; -import { StartNewBeamJobDialogComponent } from 'pages/release-coordinator-page/components/start-new-beam-job-dialog.component'; -import { ViewBeamJobOutputDialogComponent } from 'pages/release-coordinator-page/components/view-beam-job-output-dialog.component'; -import { ReleaseCoordinatorBackendApiService } from 'pages/release-coordinator-page/services/release-coordinator-backend-api.service'; -import { BeamJobRunResult } from 'domain/jobs/beam-job-run-result.model'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; +import {ClipboardModule} from '@angular/cdk/clipboard'; +import {HarnessLoader} from '@angular/cdk/testing'; +import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {ReactiveFormsModule} from '@angular/forms'; +import {MatAutocompleteModule} from '@angular/material/autocomplete'; +import {MatAutocompleteHarness} from '@angular/material/autocomplete/testing'; +import {MatButtonModule} from '@angular/material/button'; +import {MatButtonHarness} from '@angular/material/button/testing'; +import {MatCardModule} from '@angular/material/card'; +import {MatDialogModule} from '@angular/material/dialog'; +import {MatDialogHarness} from '@angular/material/dialog/testing'; +import {MatIconModule} from '@angular/material/icon'; +import {MatInputModule} from '@angular/material/input'; +import {MatInputHarness} from '@angular/material/input/testing'; +import {MatProgressBarModule} from '@angular/material/progress-bar'; +import {MatTableModule} from '@angular/material/table'; +import {MatTableHarness} from '@angular/material/table/testing'; +import {MatTabsModule} from '@angular/material/tabs'; +import {MatTooltipModule} from '@angular/material/tooltip'; +import {BrowserDynamicTestingModule} from '@angular/platform-browser-dynamic/testing'; +import {NoopAnimationsModule} from '@angular/platform-browser/animations'; +import {of} from 'rxjs'; +import {marbles} from 'rxjs-marbles'; +import {BeamJobsTabComponent} from './beam-jobs-tab.component'; + +import {BeamJobRun} from 'domain/jobs/beam-job-run.model'; +import {BeamJob} from 'domain/jobs/beam-job.model'; +import {CancelBeamJobDialogComponent} from 'pages/release-coordinator-page/components/cancel-beam-job-dialog.component'; +import {StartNewBeamJobDialogComponent} from 'pages/release-coordinator-page/components/start-new-beam-job-dialog.component'; +import {ViewBeamJobOutputDialogComponent} from 'pages/release-coordinator-page/components/view-beam-job-output-dialog.component'; +import {ReleaseCoordinatorBackendApiService} from 'pages/release-coordinator-page/services/release-coordinator-backend-api.service'; +import {BeamJobRunResult} from 'domain/jobs/beam-job-run-result.model'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; describe('Beam Jobs Tab Component', () => { let fixture: ComponentFixture; @@ -63,15 +69,13 @@ describe('Beam Jobs Tab Component', () => { const bazJob = new BeamJob('BazJob'); const beamJobs = [fooJob, barJob, bazJob]; - const runningFooJob = ( - new BeamJobRun('123', 'FooJob', 'RUNNING', 0, 0, false)); - const pendingBarJob = ( - new BeamJobRun('456', 'BarJob', 'PENDING', 0, 0, false)); + const runningFooJob = new BeamJobRun('123', 'FooJob', 'RUNNING', 0, 0, false); + const pendingBarJob = new BeamJobRun('456', 'BarJob', 'PENDING', 0, 0, false); const doneBarJob = new BeamJobRun('789', 'BarJob', 'DONE', 0, 0, false); const beamJobRuns = [runningFooJob, pendingBarJob, doneBarJob]; const terminalBeamJobRuns = [doneBarJob]; - beforeEach(waitForAsync(async() => { + beforeEach(waitForAsync(async () => { TestBed.configureTestingModule({ imports: [ HttpClientTestingModule, @@ -99,15 +103,18 @@ describe('Beam Jobs Tab Component', () => { { provide: ReleaseCoordinatorBackendApiService, useValue: jasmine.createSpyObj( - 'ReleaseCoordinatorBackendApiService', {}, { + 'ReleaseCoordinatorBackendApiService', + {}, + { getBeamJobs: () => of(beamJobs), getBeamJobRuns: () => of(beamJobRuns), - startNewBeamJob: () => ( - of(new BeamJobRun('123', 'FooJob', 'RUNNING', 0, 0, false))), - cancelBeamJobRun: () => ( - of(new BeamJobRun('123', 'FooJob', 'CANCELLED', 0, 0, false))), + startNewBeamJob: () => + of(new BeamJobRun('123', 'FooJob', 'RUNNING', 0, 0, false)), + cancelBeamJobRun: () => + of(new BeamJobRun('123', 'FooJob', 'CANCELLED', 0, 0, false)), getBeamJobRunOutput: () => of(new BeamJobRunResult('abc', '123')), - } as Partial), + } as Partial + ), }, ], }); @@ -120,7 +127,7 @@ describe('Beam Jobs Tab Component', () => { StartNewBeamJobDialogComponent, ViewBeamJobOutputDialogComponent, ], - } + }, }); await TestBed.compileComponents(); @@ -134,49 +141,62 @@ describe('Beam Jobs Tab Component', () => { loader = TestbedHarnessEnvironment.documentRootLoader(fixture); })); - it('should wait until both jobs and runs are emitted', marbles(m => { - const beamJobsOutput = m.hot(' ^-j---|', {j: beamJobs}); - const beamJobRunsOutput = m.hot('^---r-|', {r: beamJobRuns}); - const expectedNames = ' e---n--'; - const expectedRuns = ' e---r--'; - spyOn(backendApiService, 'getBeamJobs') - .and.returnValue(beamJobsOutput); - spyOn(backendApiService, 'getBeamJobRuns') - .and.returnValue(beamJobRunsOutput); - - fixture.detectChanges(); - - m.expect(component.jobNames) - .toBeObservable(expectedNames, {e: [], n: beamJobs.map(j => j.name)}); - m.expect(component.beamJobRuns) - .toBeObservable(expectedRuns, {e: [], r: beamJobRuns}); - - component.beamJobRunsRefreshIntervalSubscription.unsubscribe(); - })); - - it('should return empty array when jobs fail to load', marbles(m => { - const beamJobs = m.hot('^-#', undefined, new Error('err')); - const expectedNames = ' e-e'; - spyOn(backendApiService, 'getBeamJobs').and.returnValue(beamJobs); - - fixture.detectChanges(); - m.expect(component.jobNames).toBeObservable(expectedNames, { e: [] }); - - component.beamJobRunsRefreshIntervalSubscription.unsubscribe(); - })); - - it('should return empty array when runs fail to load', marbles(m => { - const beamJobRuns = m.hot('^-#', undefined, new Error('err')); - const expectedRuns = ' e-e'; - spyOn(backendApiService, 'getBeamJobRuns').and.returnValue(beamJobRuns); - - fixture.detectChanges(); - m.expect(component.beamJobRuns).toBeObservable(expectedRuns, { e: [] }); - - component.beamJobRunsRefreshIntervalSubscription.unsubscribe(); - })); - - it('should update the table when the job name input changes', async() => { + it( + 'should wait until both jobs and runs are emitted', + marbles(m => { + const beamJobsOutput = m.hot(' ^-j---|', {j: beamJobs}); + const beamJobRunsOutput = m.hot('^---r-|', {r: beamJobRuns}); + const expectedNames = ' e---n--'; + const expectedRuns = ' e---r--'; + spyOn(backendApiService, 'getBeamJobs').and.returnValue(beamJobsOutput); + spyOn(backendApiService, 'getBeamJobRuns').and.returnValue( + beamJobRunsOutput + ); + + fixture.detectChanges(); + + m.expect(component.jobNames).toBeObservable(expectedNames, { + e: [], + n: beamJobs.map(j => j.name), + }); + m.expect(component.beamJobRuns).toBeObservable(expectedRuns, { + e: [], + r: beamJobRuns, + }); + + component.beamJobRunsRefreshIntervalSubscription.unsubscribe(); + }) + ); + + it( + 'should return empty array when jobs fail to load', + marbles(m => { + const beamJobs = m.hot('^-#', undefined, new Error('err')); + const expectedNames = ' e-e'; + spyOn(backendApiService, 'getBeamJobs').and.returnValue(beamJobs); + + fixture.detectChanges(); + m.expect(component.jobNames).toBeObservable(expectedNames, {e: []}); + + component.beamJobRunsRefreshIntervalSubscription.unsubscribe(); + }) + ); + + it( + 'should return empty array when runs fail to load', + marbles(m => { + const beamJobRuns = m.hot('^-#', undefined, new Error('err')); + const expectedRuns = ' e-e'; + spyOn(backendApiService, 'getBeamJobRuns').and.returnValue(beamJobRuns); + + fixture.detectChanges(); + m.expect(component.beamJobRuns).toBeObservable(expectedRuns, {e: []}); + + component.beamJobRunsRefreshIntervalSubscription.unsubscribe(); + }) + ); + + it('should update the table when the job name input changes', async () => { const input = await loader.getHarness(MatInputHarness); const autocomplete = await loader.getHarness(MatAutocompleteHarness); const table = await loader.getHarness(MatTableHarness); @@ -202,7 +222,7 @@ describe('Beam Jobs Tab Component', () => { component.ngOnDestroy(); }); - it('should deselect a job after changing the input', async() => { + it('should deselect a job after changing the input', async () => { const autocomplete = await loader.getHarness(MatAutocompleteHarness); const input = await loader.getHarness(MatInputHarness); @@ -222,14 +242,22 @@ describe('Beam Jobs Tab Component', () => { component.ngOnDestroy(); }); - it('should add a new job after starting a new job run', async() => { + it('should add a new job after starting a new job run', async () => { const autocomplete = await loader.getHarness(MatAutocompleteHarness); const input = await loader.getHarness(MatInputHarness); - const newPendingFooJob = ( - new BeamJobRun('123', 'FooJob', 'PENDING', 0, 0, false)); - const startNewJobSpy = spyOn(backendApiService, 'startNewBeamJob') - .and.returnValue(of(newPendingFooJob)); + const newPendingFooJob = new BeamJobRun( + '123', + 'FooJob', + 'PENDING', + 0, + 0, + false + ); + const startNewJobSpy = spyOn( + backendApiService, + 'startNewBeamJob' + ).and.returnValue(of(newPendingFooJob)); await input.setValue('FooJob'); await autocomplete.selectOption({text: 'FooJob'}); @@ -237,16 +265,20 @@ describe('Beam Jobs Tab Component', () => { expect(component.beamJobRuns.value).not.toContain(newPendingFooJob); - const startNewButton = await loader.getHarness(MatButtonHarness.with({ - text: 'play_arrow' - })); + const startNewButton = await loader.getHarness( + MatButtonHarness.with({ + text: 'play_arrow', + }) + ); await startNewButton.click(); expect(await loader.getAllHarnesses(MatDialogHarness)).toHaveSize(1); - const confirmButton = await loader.getHarness(MatButtonHarness.with({ - text: 'Start New Job' - })); + const confirmButton = await loader.getHarness( + MatButtonHarness.with({ + text: 'Start New Job', + }) + ); await confirmButton.click(); await fixture.whenStable(); @@ -257,14 +289,22 @@ describe('Beam Jobs Tab Component', () => { component.ngOnDestroy(); }); - it('should cancel the job and update its status', async() => { + it('should cancel the job and update its status', async () => { const autocomplete = await loader.getHarness(MatAutocompleteHarness); const input = await loader.getHarness(MatInputHarness); - const cancellingFooJob = ( - new BeamJobRun('123', 'FooJob', 'CANCELLED', 0, 0, false)); - const cancelBeamJobRunSpy = spyOn(backendApiService, 'cancelBeamJobRun') - .and.returnValue(of(cancellingFooJob)); + const cancellingFooJob = new BeamJobRun( + '123', + 'FooJob', + 'CANCELLED', + 0, + 0, + false + ); + const cancelBeamJobRunSpy = spyOn( + backendApiService, + 'cancelBeamJobRun' + ).and.returnValue(of(cancellingFooJob)); await input.setValue('FooJob'); await autocomplete.selectOption({text: 'FooJob'}); @@ -273,16 +313,20 @@ describe('Beam Jobs Tab Component', () => { expect(component.beamJobRuns.value).toContain(runningFooJob); expect(component.beamJobRuns.value).not.toContain(cancellingFooJob); - const cancelButton = await loader.getHarness(MatButtonHarness.with({ - text: 'Cancel' - })); + const cancelButton = await loader.getHarness( + MatButtonHarness.with({ + text: 'Cancel', + }) + ); await cancelButton.click(); expect(await loader.getAllHarnesses(MatDialogHarness)).toHaveSize(1); - const confirmButton = await loader.getHarness(MatButtonHarness.with({ - text: 'Cancel this Job' - })); + const confirmButton = await loader.getHarness( + MatButtonHarness.with({ + text: 'Cancel this Job', + }) + ); await confirmButton.click(); await fixture.whenStable(); @@ -294,22 +338,24 @@ describe('Beam Jobs Tab Component', () => { component.ngOnDestroy(); }); - it('should show the job output', async() => { + it('should show the job output', async () => { const autocomplete = await loader.getHarness(MatAutocompleteHarness); const input = await loader.getHarness(MatInputHarness); - const getBeamJobRunOutputSpy = ( - spyOn(backendApiService, 'getBeamJobRunOutput') - .and.returnValue(of(new BeamJobRunResult('Lorem Ipsum', ''))) - ); + const getBeamJobRunOutputSpy = spyOn( + backendApiService, + 'getBeamJobRunOutput' + ).and.returnValue(of(new BeamJobRunResult('Lorem Ipsum', ''))); await input.setValue('BarJob'); await autocomplete.selectOption({text: 'BarJob'}); fixture.detectChanges(); - const viewOutputButton = await loader.getHarness(MatButtonHarness.with({ - text: 'View Output' - })); + const viewOutputButton = await loader.getHarness( + MatButtonHarness.with({ + text: 'View Output', + }) + ); await viewOutputButton.click(); const dialog = await loader.getHarness(MatDialogHarness); @@ -324,8 +370,10 @@ describe('Beam Jobs Tab Component', () => { }); it('should refresh the beam job runs every 15 seconds', fakeAsync(() => { - const getBeamJobRunsSpy = spyOn(backendApiService, 'getBeamJobRuns') - .and.returnValue(of(beamJobRuns)); + const getBeamJobRunsSpy = spyOn( + backendApiService, + 'getBeamJobRuns' + ).and.returnValue(of(beamJobRuns)); fixture.detectChanges(); @@ -342,8 +390,10 @@ describe('Beam Jobs Tab Component', () => { })); it('should not refresh beam jobs if all jobs are terminal', fakeAsync(() => { - const getBeamJobRunsSpy = spyOn(backendApiService, 'getBeamJobRuns') - .and.returnValue(of(terminalBeamJobRuns)); + const getBeamJobRunsSpy = spyOn( + backendApiService, + 'getBeamJobRuns' + ).and.returnValue(of(terminalBeamJobRuns)); fixture.detectChanges(); diff --git a/core/templates/pages/release-coordinator-page/beam-jobs-tab/beam-jobs-tab.component.ts b/core/templates/pages/release-coordinator-page/beam-jobs-tab/beam-jobs-tab.component.ts index 648b19afded8..21b73ead819a 100644 --- a/core/templates/pages/release-coordinator-page/beam-jobs-tab/beam-jobs-tab.component.ts +++ b/core/templates/pages/release-coordinator-page/beam-jobs-tab/beam-jobs-tab.component.ts @@ -17,31 +17,52 @@ * release-coordinator panel. */ -import { Component, NgZone, OnDestroy, OnInit } from '@angular/core'; -import { FormControl } from '@angular/forms'; -import { MatDialog } from '@angular/material/dialog'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { BehaviorSubject, combineLatest, interval, NEVER, Observable, of, Subscription, zip } from 'rxjs'; -import { catchError, distinctUntilChanged, first, map, startWith, switchMap } from 'rxjs/operators'; - -import { BeamJobRun } from 'domain/jobs/beam-job-run.model'; -import { BeamJob } from 'domain/jobs/beam-job.model'; -import { CancelBeamJobDialogComponent } from 'pages/release-coordinator-page/components/cancel-beam-job-dialog.component'; -import { StartNewBeamJobDialogComponent } from 'pages/release-coordinator-page/components/start-new-beam-job-dialog.component'; -import { ViewBeamJobOutputDialogComponent } from 'pages/release-coordinator-page/components/view-beam-job-output-dialog.component'; -import { ReleaseCoordinatorBackendApiService } from 'pages/release-coordinator-page/services/release-coordinator-backend-api.service'; -import { AlertsService } from 'services/alerts.service'; +import {Component, NgZone, OnDestroy, OnInit} from '@angular/core'; +import {FormControl} from '@angular/forms'; +import {MatDialog} from '@angular/material/dialog'; +import {downgradeComponent} from '@angular/upgrade/static'; +import { + BehaviorSubject, + combineLatest, + interval, + NEVER, + Observable, + of, + Subscription, + zip, +} from 'rxjs'; +import { + catchError, + distinctUntilChanged, + first, + map, + startWith, + switchMap, +} from 'rxjs/operators'; + +import {BeamJobRun} from 'domain/jobs/beam-job-run.model'; +import {BeamJob} from 'domain/jobs/beam-job.model'; +import {CancelBeamJobDialogComponent} from 'pages/release-coordinator-page/components/cancel-beam-job-dialog.component'; +import {StartNewBeamJobDialogComponent} from 'pages/release-coordinator-page/components/start-new-beam-job-dialog.component'; +import {ViewBeamJobOutputDialogComponent} from 'pages/release-coordinator-page/components/view-beam-job-output-dialog.component'; +import {ReleaseCoordinatorBackendApiService} from 'pages/release-coordinator-page/services/release-coordinator-backend-api.service'; +import {AlertsService} from 'services/alerts.service'; @Component({ selector: 'oppia-beam-jobs-tab', - templateUrl: './beam-jobs-tab.component.html' + templateUrl: './beam-jobs-tab.component.html', }) export class BeamJobsTabComponent implements OnInit, OnDestroy { static readonly BEAM_JOB_RUNS_REFRESH_INTERVAL_MSECS = 15000; public dataFailedToLoad = false; readonly jobRunTableColumns: readonly string[] = [ - 'run_status', 'job_name', 'started_on', 'updated_on', 'action']; + 'run_status', + 'job_name', + 'started_on', + 'updated_on', + 'action', + ]; jobNameControl = new FormControl(''); @@ -59,15 +80,18 @@ export class BeamJobsTabComponent implements OnInit, OnDestroy { beamJobRuns = new BehaviorSubject([]); constructor( - private backendApiService: ReleaseCoordinatorBackendApiService, - private alertsService: AlertsService, - private matDialog: MatDialog, - private ngZone: NgZone) {} + private backendApiService: ReleaseCoordinatorBackendApiService, + private alertsService: AlertsService, + private matDialog: MatDialog, + private ngZone: NgZone + ) {} ngOnInit(): void { - const initialBeamJobs = this.backendApiService.getBeamJobs() + const initialBeamJobs = this.backendApiService + .getBeamJobs() .pipe(catchError(error => this.onError(error))); - const initialBeamJobRuns = this.backendApiService.getBeamJobRuns() + const initialBeamJobRuns = this.backendApiService + .getBeamJobRuns() .pipe(catchError(error => this.onError(error))); zip(initialBeamJobs, initialBeamJobRuns) @@ -79,8 +103,10 @@ export class BeamJobsTabComponent implements OnInit, OnDestroy { this.dataIsReady = true; }); - const jobNameInputChanges = this.jobNameControl.valueChanges - .pipe(startWith(''), distinctUntilChanged()); + const jobNameInputChanges = this.jobNameControl.valueChanges.pipe( + startWith(''), + distinctUntilChanged() + ); jobNameInputChanges.subscribe(input => { if (this.selectedJob?.name !== input) { @@ -88,24 +114,27 @@ export class BeamJobsTabComponent implements OnInit, OnDestroy { } }); - this.filteredJobNames = combineLatest([jobNameInputChanges, this.jobNames]) - .pipe(map(filterArgs => this.filterJobNames(...filterArgs))); + this.filteredJobNames = combineLatest([ + jobNameInputChanges, + this.jobNames, + ]).pipe(map(filterArgs => this.filterJobNames(...filterArgs))); - this.filteredBeamJobRuns = ( - combineLatest([this.beamJobRuns, this.filteredJobNames]).pipe( - map( - ([runs, jobNames]) => runs.filter(r => jobNames.includes(r.jobName)) - ) - ) + this.filteredBeamJobRuns = combineLatest([ + this.beamJobRuns, + this.filteredJobNames, + ]).pipe( + map(([runs, jobNames]) => runs.filter(r => jobNames.includes(r.jobName))) ); // Intervals need to be executed *outside* of Angular so that they don't // interfere with testability (otherwise, the Angular component will never // be "ready" since an interval executes indefinitely). this.ngZone.runOutsideAngular(() => { - this.beamJobRunsRefreshIntervalSubscription = ( - interval(BeamJobsTabComponent.BEAM_JOB_RUNS_REFRESH_INTERVAL_MSECS) - .pipe(switchMap(() => { + this.beamJobRunsRefreshIntervalSubscription = interval( + BeamJobsTabComponent.BEAM_JOB_RUNS_REFRESH_INTERVAL_MSECS + ) + .pipe( + switchMap(() => { // If every job in the current list is in a terminal state (won't // ever change), then we return NEVER (an Observable which neither // completes nor emits values) to avoid calling out to the backend. @@ -119,13 +148,13 @@ export class BeamJobsTabComponent implements OnInit, OnDestroy { // update on the state of our jobs. return this.backendApiService.getBeamJobRuns(); } - })) - .subscribe(beamJobRuns => { - // When we're ready to update the beam jobs, we want the update to - // execute *inside* of Angular so that change detection can notice. - this.ngZone.run(() => this.beamJobRuns.next(beamJobRuns)); }) - ); + ) + .subscribe(beamJobRuns => { + // When we're ready to update the beam jobs, we want the update to + // execute *inside* of Angular so that change detection can notice. + this.ngZone.run(() => this.beamJobRuns.next(beamJobRuns)); + }); }); } @@ -150,8 +179,10 @@ export class BeamJobsTabComponent implements OnInit, OnDestroy { ev.stopPropagation(); this.matDialog - .open(StartNewBeamJobDialogComponent, { data: this.selectedJob }) - .afterClosed().pipe(first()).subscribe(newBeamJobRun => { + .open(StartNewBeamJobDialogComponent, {data: this.selectedJob}) + .afterClosed() + .pipe(first()) + .subscribe(newBeamJobRun => { if (newBeamJobRun) { this.addNewBeamJobRun(newBeamJobRun); } @@ -160,8 +191,10 @@ export class BeamJobsTabComponent implements OnInit, OnDestroy { onCancelClick(beamJobRun: BeamJobRun): void { this.matDialog - .open(CancelBeamJobDialogComponent, { data: beamJobRun }) - .afterClosed().pipe(first()).subscribe(cancelledBeamJobRun => { + .open(CancelBeamJobDialogComponent, {data: beamJobRun}) + .afterClosed() + .pipe(first()) + .subscribe(cancelledBeamJobRun => { if (cancelledBeamJobRun) { this.replaceBeamJobRun(beamJobRun, cancelledBeamJobRun); } @@ -169,7 +202,7 @@ export class BeamJobsTabComponent implements OnInit, OnDestroy { } onViewOutputClick(beamJobRun: BeamJobRun): void { - this.matDialog.open(ViewBeamJobOutputDialogComponent, { data: beamJobRun }); + this.matDialog.open(ViewBeamJobOutputDialogComponent, {data: beamJobRun}); } private filterJobNames(input: string, jobNames: string[]): string[] { @@ -190,5 +223,9 @@ export class BeamJobsTabComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive( - 'oppiaBeamJobsTab', downgradeComponent({ component: BeamJobsTabComponent })); +angular + .module('oppia') + .directive( + 'oppiaBeamJobsTab', + downgradeComponent({component: BeamJobsTabComponent}) + ); diff --git a/core/templates/pages/release-coordinator-page/components/cancel-beam-job-dialog.component.spec.ts b/core/templates/pages/release-coordinator-page/components/cancel-beam-job-dialog.component.spec.ts index d116a26e3991..689e29aa2a59 100644 --- a/core/templates/pages/release-coordinator-page/components/cancel-beam-job-dialog.component.spec.ts +++ b/core/templates/pages/release-coordinator-page/components/cancel-beam-job-dialog.component.spec.ts @@ -16,26 +16,29 @@ * @fileoverview Unit tests for the CancelBeamJobDialogComponent. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { MatButtonModule } from '@angular/material/button'; -import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; -import { MatProgressBarModule } from '@angular/material/progress-bar'; -import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; -import { of, throwError } from 'rxjs'; - -import { ReleaseCoordinatorBackendApiService } from 'pages/release-coordinator-page/services/release-coordinator-backend-api.service'; -import { CancelBeamJobDialogComponent } from 'pages/release-coordinator-page/components/cancel-beam-job-dialog.component'; -import { BeamJobRun } from 'domain/jobs/beam-job-run.model'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { AlertsService } from 'services/alerts.service'; -import { MatCardModule } from '@angular/material/card'; -import { MatIconModule } from '@angular/material/icon'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {MatButtonModule} from '@angular/material/button'; +import { + MatDialogModule, + MatDialogRef, + MAT_DIALOG_DATA, +} from '@angular/material/dialog'; +import {MatProgressBarModule} from '@angular/material/progress-bar'; +import {BrowserDynamicTestingModule} from '@angular/platform-browser-dynamic/testing'; +import {of, throwError} from 'rxjs'; + +import {ReleaseCoordinatorBackendApiService} from 'pages/release-coordinator-page/services/release-coordinator-backend-api.service'; +import {CancelBeamJobDialogComponent} from 'pages/release-coordinator-page/components/cancel-beam-job-dialog.component'; +import {BeamJobRun} from 'domain/jobs/beam-job-run.model'; +import {NoopAnimationsModule} from '@angular/platform-browser/animations'; +import {AlertsService} from 'services/alerts.service'; +import {MatCardModule} from '@angular/material/card'; +import {MatIconModule} from '@angular/material/icon'; +import {MatTooltipModule} from '@angular/material/tooltip'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; describe('Cancel beam job dialog', () => { - const beamJobRun = ( - new BeamJobRun('123', 'FooJob', 'RUNNING', 0, 0, false)); + const beamJobRun = new BeamJobRun('123', 'FooJob', 'RUNNING', 0, 0, false); let fixture: ComponentFixture; let component: CancelBeamJobDialogComponent; @@ -44,13 +47,11 @@ describe('Cancel beam job dialog', () => { let alertsService: AlertsService; let matDialogRef: MatDialogRef; - beforeEach(waitForAsync(async() => { - const mockDialogRef = { disableClose: false, close: () => {} }; + beforeEach(waitForAsync(async () => { + const mockDialogRef = {disableClose: false, close: () => {}}; TestBed.configureTestingModule({ - declarations: [ - CancelBeamJobDialogComponent, - ], + declarations: [CancelBeamJobDialogComponent], imports: [ HttpClientTestingModule, BrowserDynamicTestingModule, @@ -64,17 +65,15 @@ describe('Cancel beam job dialog', () => { ], providers: [ ReleaseCoordinatorBackendApiService, - { provide: MAT_DIALOG_DATA, useValue: beamJobRun }, - { provide: MatDialogRef, useValue: mockDialogRef }, + {provide: MAT_DIALOG_DATA, useValue: beamJobRun}, + {provide: MatDialogRef, useValue: mockDialogRef}, ], }); // NOTE: This allows tests to compile the DOM of each dialog component. TestBed.overrideModule(BrowserDynamicTestingModule, { set: { - entryComponents: [ - CancelBeamJobDialogComponent, - ], - } + entryComponents: [CancelBeamJobDialogComponent], + }, }); await TestBed.compileComponents(); @@ -88,10 +87,18 @@ describe('Cancel beam job dialog', () => { })); it('should lock the dialog and cancel the job before finally closing', () => { - const cancelledBeamJobRun = ( - new BeamJobRun('123', 'FooJob', 'CANCELLED', 0, 0, false)); - const caneclBeamJobRunSpy = spyOn(backendApiService, 'cancelBeamJobRun') - .and.returnValue(of(cancelledBeamJobRun)); + const cancelledBeamJobRun = new BeamJobRun( + '123', + 'FooJob', + 'CANCELLED', + 0, + 0, + false + ); + const caneclBeamJobRunSpy = spyOn( + backendApiService, + 'cancelBeamJobRun' + ).and.returnValue(of(cancelledBeamJobRun)); const closeDialogSpy = spyOn(matDialogRef, 'close'); expect(component.isRunning).toBeFalse(); @@ -108,10 +115,12 @@ describe('Cancel beam job dialog', () => { expect(closeDialogSpy).toHaveBeenCalledWith(cancelledBeamJobRun); }); - it('should show the error dialog if the operation failed', async() => { + it('should show the error dialog if the operation failed', async () => { const error = new Error(); - const cancelBeamJobRunSpy = spyOn(backendApiService, 'cancelBeamJobRun') - .and.returnValue(throwError(error)); + const cancelBeamJobRunSpy = spyOn( + backendApiService, + 'cancelBeamJobRun' + ).and.returnValue(throwError(error)); const addWarningSpy = spyOn(alertsService, 'addWarning'); component.onActionClick(); diff --git a/core/templates/pages/release-coordinator-page/components/cancel-beam-job-dialog.component.ts b/core/templates/pages/release-coordinator-page/components/cancel-beam-job-dialog.component.ts index a4c2fb81a7c0..9e42a08cbdc1 100644 --- a/core/templates/pages/release-coordinator-page/components/cancel-beam-job-dialog.component.ts +++ b/core/templates/pages/release-coordinator-page/components/cancel-beam-job-dialog.component.ts @@ -16,14 +16,14 @@ * @fileoverview Component for managing an Apache Beam job. */ -import { Component, Inject } from '@angular/core'; -import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; -import { catchError, take } from 'rxjs/operators'; +import {Component, Inject} from '@angular/core'; +import {MatDialogRef, MAT_DIALOG_DATA} from '@angular/material/dialog'; +import {catchError, take} from 'rxjs/operators'; -import { BeamJobRun } from 'domain/jobs/beam-job-run.model'; -import { ReleaseCoordinatorBackendApiService } from 'pages/release-coordinator-page/services/release-coordinator-backend-api.service'; -import { AlertsService } from 'services/alerts.service'; -import { of } from 'rxjs'; +import {BeamJobRun} from 'domain/jobs/beam-job-run.model'; +import {ReleaseCoordinatorBackendApiService} from 'pages/release-coordinator-page/services/release-coordinator-backend-api.service'; +import {AlertsService} from 'services/alerts.service'; +import {of} from 'rxjs'; @Component({ selector: 'cancel-beam-job-dialog', @@ -33,23 +33,26 @@ export class CancelBeamJobDialogComponent { isRunning = false; constructor( - @Inject(MAT_DIALOG_DATA) public beamJobRun: BeamJobRun, - private matDialogRef: - // JobRun may be null if the job failed to cancel. - MatDialogRef, - private alertsService: AlertsService, - private backendApiService: ReleaseCoordinatorBackendApiService) {} + @Inject(MAT_DIALOG_DATA) public beamJobRun: BeamJobRun, + private matDialogRef: // JobRun may be null if the job failed to cancel. + MatDialogRef, + private alertsService: AlertsService, + private backendApiService: ReleaseCoordinatorBackendApiService + ) {} onActionClick(): void { this.isRunning = true; this.matDialogRef.disableClose = true; - this.backendApiService.cancelBeamJobRun(this.beamJobRun).pipe( - take(1), - catchError(error => { - this.alertsService.addWarning(error.message); - return of(null); - }) - ).subscribe(cancelledJobRun => this.matDialogRef.close(cancelledJobRun)); + this.backendApiService + .cancelBeamJobRun(this.beamJobRun) + .pipe( + take(1), + catchError(error => { + this.alertsService.addWarning(error.message); + return of(null); + }) + ) + .subscribe(cancelledJobRun => this.matDialogRef.close(cancelledJobRun)); } } diff --git a/core/templates/pages/release-coordinator-page/components/start-new-beam-job-dialog.component.spec.ts b/core/templates/pages/release-coordinator-page/components/start-new-beam-job-dialog.component.spec.ts index 3be4ac2a0bd9..11a45b076da5 100644 --- a/core/templates/pages/release-coordinator-page/components/start-new-beam-job-dialog.component.spec.ts +++ b/core/templates/pages/release-coordinator-page/components/start-new-beam-job-dialog.component.spec.ts @@ -16,20 +16,24 @@ * @fileoverview Unit tests for the StartNewBeamJobDialogComponent. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { MatButtonModule } from '@angular/material/button'; -import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; -import { MatProgressBarModule } from '@angular/material/progress-bar'; -import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { of, throwError } from 'rxjs'; - -import { BeamJobRun } from 'domain/jobs/beam-job-run.model'; -import { BeamJob } from 'domain/jobs/beam-job.model'; -import { StartNewBeamJobDialogComponent } from 'pages/release-coordinator-page/components/start-new-beam-job-dialog.component'; -import { ReleaseCoordinatorBackendApiService } from 'pages/release-coordinator-page/services/release-coordinator-backend-api.service'; -import { AlertsService } from 'services/alerts.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {MatButtonModule} from '@angular/material/button'; +import { + MatDialogModule, + MatDialogRef, + MAT_DIALOG_DATA, +} from '@angular/material/dialog'; +import {MatProgressBarModule} from '@angular/material/progress-bar'; +import {BrowserDynamicTestingModule} from '@angular/platform-browser-dynamic/testing'; +import {NoopAnimationsModule} from '@angular/platform-browser/animations'; +import {of, throwError} from 'rxjs'; + +import {BeamJobRun} from 'domain/jobs/beam-job-run.model'; +import {BeamJob} from 'domain/jobs/beam-job.model'; +import {StartNewBeamJobDialogComponent} from 'pages/release-coordinator-page/components/start-new-beam-job-dialog.component'; +import {ReleaseCoordinatorBackendApiService} from 'pages/release-coordinator-page/services/release-coordinator-backend-api.service'; +import {AlertsService} from 'services/alerts.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; describe('Start new beam job dialog', () => { const beamJob = new BeamJob('FooJob'); @@ -41,13 +45,11 @@ describe('Start new beam job dialog', () => { let alertsService: AlertsService; let matDialogRef: MatDialogRef; - beforeEach(waitForAsync(async() => { - const mockDialogRef = { disableClose: false, close: () => {} }; + beforeEach(waitForAsync(async () => { + const mockDialogRef = {disableClose: false, close: () => {}}; TestBed.configureTestingModule({ - declarations: [ - StartNewBeamJobDialogComponent, - ], + declarations: [StartNewBeamJobDialogComponent], imports: [ HttpClientTestingModule, MatDialogModule, @@ -57,18 +59,16 @@ describe('Start new beam job dialog', () => { BrowserDynamicTestingModule, ], providers: [ - { provide: MAT_DIALOG_DATA, useValue: beamJob }, - { provide: MatDialogRef, useValue: mockDialogRef }, + {provide: MAT_DIALOG_DATA, useValue: beamJob}, + {provide: MatDialogRef, useValue: mockDialogRef}, ReleaseCoordinatorBackendApiService, ], }); // NOTE: This allows tests to compile the DOM of each dialog component. TestBed.overrideModule(BrowserDynamicTestingModule, { set: { - entryComponents: [ - StartNewBeamJobDialogComponent, - ], - } + entryComponents: [StartNewBeamJobDialogComponent], + }, }); await TestBed.compileComponents(); @@ -82,10 +82,18 @@ describe('Start new beam job dialog', () => { })); it('should lock the dialog and start a job before finally closing', () => { - const newBeamJobRun = ( - new BeamJobRun('123', 'FooJob', 'PENDING', 0, 0, false)); - const startNewBeamJobSpy = spyOn(backendApiService, 'startNewBeamJob') - .and.returnValue(of(newBeamJobRun)); + const newBeamJobRun = new BeamJobRun( + '123', + 'FooJob', + 'PENDING', + 0, + 0, + false + ); + const startNewBeamJobSpy = spyOn( + backendApiService, + 'startNewBeamJob' + ).and.returnValue(of(newBeamJobRun)); const closeDialogSpy = spyOn(matDialogRef, 'close'); expect(component.isRunning).toBeFalse(); @@ -102,10 +110,12 @@ describe('Start new beam job dialog', () => { expect(closeDialogSpy).toHaveBeenCalledWith(newBeamJobRun); }); - it('should show the error dialog if the operation failed', async() => { + it('should show the error dialog if the operation failed', async () => { const error = new Error(); - const startNewBeamJobSpy = spyOn(backendApiService, 'startNewBeamJob') - .and.returnValue(throwError(error)); + const startNewBeamJobSpy = spyOn( + backendApiService, + 'startNewBeamJob' + ).and.returnValue(throwError(error)); const addWarningSpy = spyOn(alertsService, 'addWarning'); component.onActionClick(); diff --git a/core/templates/pages/release-coordinator-page/components/start-new-beam-job-dialog.component.ts b/core/templates/pages/release-coordinator-page/components/start-new-beam-job-dialog.component.ts index cbef054bb2ff..7ec14a71df76 100644 --- a/core/templates/pages/release-coordinator-page/components/start-new-beam-job-dialog.component.ts +++ b/core/templates/pages/release-coordinator-page/components/start-new-beam-job-dialog.component.ts @@ -16,16 +16,16 @@ * @fileoverview Component for starting a new Apache Beam job. */ -import { Component, Inject } from '@angular/core'; -import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; -import { catchError, take } from 'rxjs/operators'; +import {Component, Inject} from '@angular/core'; +import {MatDialogRef, MAT_DIALOG_DATA} from '@angular/material/dialog'; +import {catchError, take} from 'rxjs/operators'; -import { BeamJob } from 'domain/jobs/beam-job.model'; -import { BeamJobRun } from 'domain/jobs/beam-job-run.model'; -import { ReleaseCoordinatorBackendApiService } from 'pages/release-coordinator-page/services/release-coordinator-backend-api.service'; -import { AlertsService } from 'services/alerts.service'; -import { of } from 'rxjs'; -import { AppConstants } from 'app.constants'; +import {BeamJob} from 'domain/jobs/beam-job.model'; +import {BeamJobRun} from 'domain/jobs/beam-job-run.model'; +import {ReleaseCoordinatorBackendApiService} from 'pages/release-coordinator-page/services/release-coordinator-backend-api.service'; +import {AlertsService} from 'services/alerts.service'; +import {of} from 'rxjs'; +import {AppConstants} from 'app.constants'; @Component({ selector: 'start-new-beam-job-dialog', @@ -36,23 +36,26 @@ export class StartNewBeamJobDialogComponent { isRunning = false; constructor( - @Inject(MAT_DIALOG_DATA) public beamJob: BeamJob, - private matDialogRef: - // JobRun may be null if the job failed to start. - MatDialogRef, - private alertsService: AlertsService, - private backendApiService: ReleaseCoordinatorBackendApiService) {} + @Inject(MAT_DIALOG_DATA) public beamJob: BeamJob, + private matDialogRef: // JobRun may be null if the job failed to start. + MatDialogRef, + private alertsService: AlertsService, + private backendApiService: ReleaseCoordinatorBackendApiService + ) {} onActionClick(): void { this.isRunning = true; this.matDialogRef.disableClose = true; - this.backendApiService.startNewBeamJob(this.beamJob).pipe( - take(1), - catchError(error => { - this.alertsService.addWarning(error.message); - return of(null); - }) - ).subscribe(newJobRun => this.matDialogRef.close(newJobRun)); + this.backendApiService + .startNewBeamJob(this.beamJob) + .pipe( + take(1), + catchError(error => { + this.alertsService.addWarning(error.message); + return of(null); + }) + ) + .subscribe(newJobRun => this.matDialogRef.close(newJobRun)); } } diff --git a/core/templates/pages/release-coordinator-page/components/view-beam-job-output-dialog.component.spec.ts b/core/templates/pages/release-coordinator-page/components/view-beam-job-output-dialog.component.spec.ts index 2863d1fca710..58b69a513a03 100644 --- a/core/templates/pages/release-coordinator-page/components/view-beam-job-output-dialog.component.spec.ts +++ b/core/templates/pages/release-coordinator-page/components/view-beam-job-output-dialog.component.spec.ts @@ -16,26 +16,30 @@ * @fileoverview Component for viewing the output of an Apache Beam job. */ -import { ClipboardModule } from '@angular/cdk/clipboard'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { MatButtonModule } from '@angular/material/button'; -import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; -import { MatProgressBarModule } from '@angular/material/progress-bar'; -import { MatTabsModule } from '@angular/material/tabs'; -import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { of, throwError } from 'rxjs'; -import { marbles } from 'rxjs-marbles'; - -import { BeamJobRunResult } from 'domain/jobs/beam-job-run-result.model'; -import { BeamJobRun } from 'domain/jobs/beam-job-run.model'; -import { ViewBeamJobOutputDialogComponent } from 'pages/release-coordinator-page/components/view-beam-job-output-dialog.component'; -import { ReleaseCoordinatorBackendApiService } from 'pages/release-coordinator-page/services/release-coordinator-backend-api.service'; -import { AlertsService } from 'services/alerts.service'; -import { MatIconModule } from '@angular/material/icon'; -import { MatCardModule } from '@angular/material/card'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; +import {ClipboardModule} from '@angular/cdk/clipboard'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {MatButtonModule} from '@angular/material/button'; +import { + MatDialogModule, + MatDialogRef, + MAT_DIALOG_DATA, +} from '@angular/material/dialog'; +import {MatProgressBarModule} from '@angular/material/progress-bar'; +import {MatTabsModule} from '@angular/material/tabs'; +import {BrowserDynamicTestingModule} from '@angular/platform-browser-dynamic/testing'; +import {NoopAnimationsModule} from '@angular/platform-browser/animations'; +import {of, throwError} from 'rxjs'; +import {marbles} from 'rxjs-marbles'; + +import {BeamJobRunResult} from 'domain/jobs/beam-job-run-result.model'; +import {BeamJobRun} from 'domain/jobs/beam-job-run.model'; +import {ViewBeamJobOutputDialogComponent} from 'pages/release-coordinator-page/components/view-beam-job-output-dialog.component'; +import {ReleaseCoordinatorBackendApiService} from 'pages/release-coordinator-page/services/release-coordinator-backend-api.service'; +import {AlertsService} from 'services/alerts.service'; +import {MatIconModule} from '@angular/material/icon'; +import {MatCardModule} from '@angular/material/card'; +import {MatTooltipModule} from '@angular/material/tooltip'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; describe('View beam job output dialog', () => { const beamJobRun = new BeamJobRun('123', 'FooJob', 'DONE', 0, 0, false); @@ -46,13 +50,11 @@ describe('View beam job output dialog', () => { let backendApiService: ReleaseCoordinatorBackendApiService; let alertsService: AlertsService; - beforeEach(waitForAsync(async() => { - const mockDialogRef = { disableClose: false, close: () => {} }; + beforeEach(waitForAsync(async () => { + const mockDialogRef = {disableClose: false, close: () => {}}; TestBed.configureTestingModule({ - declarations: [ - ViewBeamJobOutputDialogComponent, - ], + declarations: [ViewBeamJobOutputDialogComponent], imports: [ HttpClientTestingModule, BrowserDynamicTestingModule, @@ -67,18 +69,16 @@ describe('View beam job output dialog', () => { NoopAnimationsModule, ], providers: [ - { provide: MAT_DIALOG_DATA, useValue: beamJobRun }, - { provide: MatDialogRef, useValue: mockDialogRef }, + {provide: MAT_DIALOG_DATA, useValue: beamJobRun}, + {provide: MatDialogRef, useValue: mockDialogRef}, ReleaseCoordinatorBackendApiService, ], }); // NOTE: This allows tests to compile the DOM of each dialog component. TestBed.overrideModule(BrowserDynamicTestingModule, { set: { - entryComponents: [ - ViewBeamJobOutputDialogComponent, - ], - } + entryComponents: [ViewBeamJobOutputDialogComponent], + }, }); await TestBed.compileComponents(); @@ -89,35 +89,39 @@ describe('View beam job output dialog', () => { component = fixture.componentInstance; })); - it('should unsubscribe when ngOnDestroy() is called', marbles(m => { - // We will call ngOnDestroy() on frame 2, at which point an unsubscription - // should occur. "Frames" are units of time in marble diagrams. There are - // two kinds of marble diagrams: for Observables and for Subscriptions. - // - // In Observable marble diagrams, frame 0 is the '^' character, and each - // non-whitespace character is 1 frame "later" in time. We use '-' to - // move frames forward in time without taking action. The '|' character - // represents the completion of the stream. - // - // In Subscription marble diagrams, the '^' represents the beginning of a - // subscription and the '!' character represents its unsubscription. The '-' - // characters hold the same meaning. - const output = m.hot(' ^---|'); - const expectedSubscription = '^-! '; - m.scheduler.schedule(() => component.ngOnDestroy(), 2); - - spyOn(backendApiService, 'getBeamJobRunOutput').and.returnValue(output); - - fixture.detectChanges(); - - m.expect(output).toHaveSubscriptions(expectedSubscription); - })); + it( + 'should unsubscribe when ngOnDestroy() is called', + marbles(m => { + // We will call ngOnDestroy() on frame 2, at which point an unsubscription + // should occur. "Frames" are units of time in marble diagrams. There are + // two kinds of marble diagrams: for Observables and for Subscriptions. + // + // In Observable marble diagrams, frame 0 is the '^' character, and each + // non-whitespace character is 1 frame "later" in time. We use '-' to + // move frames forward in time without taking action. The '|' character + // represents the completion of the stream. + // + // In Subscription marble diagrams, the '^' represents the beginning of a + // subscription and the '!' character represents its unsubscription. The '-' + // characters hold the same meaning. + const output = m.hot(' ^---|'); + const expectedSubscription = '^-! '; + m.scheduler.schedule(() => component.ngOnDestroy(), 2); + + spyOn(backendApiService, 'getBeamJobRunOutput').and.returnValue(output); + + fixture.detectChanges(); + + m.expect(output).toHaveSubscriptions(expectedSubscription); + }) + ); it('should resolve the result and assign it to the output', () => { const result = new BeamJobRunResult('abc', '123'); - const getBeamJobRunOutputSpy = ( - spyOn(backendApiService, 'getBeamJobRunOutput') - .and.returnValue(of(result))); + const getBeamJobRunOutputSpy = spyOn( + backendApiService, + 'getBeamJobRunOutput' + ).and.returnValue(of(result)); fixture.detectChanges(); @@ -126,9 +130,10 @@ describe('View beam job output dialog', () => { }); it('should use the output corresponding to the selected tab', () => { - const getBeamJobRunOutputSpy = ( - spyOn(backendApiService, 'getBeamJobRunOutput') - .and.returnValue(of(new BeamJobRunResult('abc', '123')))); + const getBeamJobRunOutputSpy = spyOn( + backendApiService, + 'getBeamJobRunOutput' + ).and.returnValue(of(new BeamJobRunResult('abc', '123'))); fixture.detectChanges(); @@ -147,9 +152,10 @@ describe('View beam job output dialog', () => { }); it('should show stderr when stdout is empty', () => { - const getBeamJobRunOutputSpy = ( - spyOn(backendApiService, 'getBeamJobRunOutput') - .and.returnValue(of(new BeamJobRunResult('', '123')))); + const getBeamJobRunOutputSpy = spyOn( + backendApiService, + 'getBeamJobRunOutput' + ).and.returnValue(of(new BeamJobRunResult('', '123'))); fixture.detectChanges(); @@ -158,9 +164,10 @@ describe('View beam job output dialog', () => { }); it('should show stdout when stderr is empty', () => { - const getBeamJobRunOutputSpy = ( - spyOn(backendApiService, 'getBeamJobRunOutput') - .and.returnValue(of(new BeamJobRunResult('abc', '')))); + const getBeamJobRunOutputSpy = spyOn( + backendApiService, + 'getBeamJobRunOutput' + ).and.returnValue(of(new BeamJobRunResult('abc', ''))); fixture.detectChanges(); @@ -168,11 +175,12 @@ describe('View beam job output dialog', () => { expect(component.getOutput()).toEqual('abc'); }); - it('should show the error dialog if the operation failed', async() => { + it('should show the error dialog if the operation failed', async () => { const error = new Error(); - const getBeamJobRunOutputSpy = ( - spyOn(backendApiService, 'getBeamJobRunOutput') - .and.returnValue(throwError(error))); + const getBeamJobRunOutputSpy = spyOn( + backendApiService, + 'getBeamJobRunOutput' + ).and.returnValue(throwError(error)); const addWarningSpy = spyOn(alertsService, 'addWarning').and.stub(); fixture.detectChanges(); diff --git a/core/templates/pages/release-coordinator-page/components/view-beam-job-output-dialog.component.ts b/core/templates/pages/release-coordinator-page/components/view-beam-job-output-dialog.component.ts index 953c66d44d85..7674f070ded8 100644 --- a/core/templates/pages/release-coordinator-page/components/view-beam-job-output-dialog.component.ts +++ b/core/templates/pages/release-coordinator-page/components/view-beam-job-output-dialog.component.ts @@ -16,17 +16,16 @@ * @fileoverview Component for viewing the output of an Apache Beam job. */ -import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; -import { FormControl } from '@angular/forms'; -import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; -import { of, Subscription } from 'rxjs'; -import { catchError, first } from 'rxjs/operators'; - -import { BeamJobRunResult } from 'domain/jobs/beam-job-run-result.model'; -import { BeamJobRun } from 'domain/jobs/beam-job-run.model'; -import { ReleaseCoordinatorBackendApiService } from 'pages/release-coordinator-page/services/release-coordinator-backend-api.service'; -import { AlertsService } from 'services/alerts.service'; +import {Component, Inject, OnDestroy, OnInit} from '@angular/core'; +import {FormControl} from '@angular/forms'; +import {MatDialogRef, MAT_DIALOG_DATA} from '@angular/material/dialog'; +import {of, Subscription} from 'rxjs'; +import {catchError, first} from 'rxjs/operators'; +import {BeamJobRunResult} from 'domain/jobs/beam-job-run-result.model'; +import {BeamJobRun} from 'domain/jobs/beam-job-run.model'; +import {ReleaseCoordinatorBackendApiService} from 'pages/release-coordinator-page/services/release-coordinator-backend-api.service'; +import {AlertsService} from 'services/alerts.service'; @Component({ selector: 'view-beam-job-output-dialog', @@ -42,21 +41,23 @@ export class ViewBeamJobOutputDialogComponent implements OnInit, OnDestroy { subscription!: Subscription; constructor( - @Inject(MAT_DIALOG_DATA) public beamJobRun: BeamJobRun, - public matDialogRef: MatDialogRef, - private alertsService: AlertsService, - private backendApiService: ReleaseCoordinatorBackendApiService) {} + @Inject(MAT_DIALOG_DATA) public beamJobRun: BeamJobRun, + public matDialogRef: MatDialogRef, + private alertsService: AlertsService, + private backendApiService: ReleaseCoordinatorBackendApiService + ) {} ngOnInit(): void { - this.subscription = ( - this.backendApiService.getBeamJobRunOutput(this.beamJobRun).pipe( + this.subscription = this.backendApiService + .getBeamJobRunOutput(this.beamJobRun) + .pipe( first(), catchError(error => { this.alertsService.addWarning(error.message); return of(null); - }), + }) ) - ).subscribe(output => this.output = output); + .subscribe(output => (this.output = output)); } ngOnDestroy(): void { @@ -68,8 +69,7 @@ export class ViewBeamJobOutputDialogComponent implements OnInit, OnDestroy { return ''; } if (this.output.stdout && this.output.stderr) { - return this.selectedTab.value ? - this.output.stderr : this.output.stdout; + return this.selectedTab.value ? this.output.stderr : this.output.stdout; } else if (this.output.stdout) { return this.output.stdout; } else { diff --git a/core/templates/pages/release-coordinator-page/features-tab/features-tab.component.spec.ts b/core/templates/pages/release-coordinator-page/features-tab/features-tab.component.spec.ts index 38e91369d89d..78f62fbff91d 100644 --- a/core/templates/pages/release-coordinator-page/features-tab/features-tab.component.spec.ts +++ b/core/templates/pages/release-coordinator-page/features-tab/features-tab.component.spec.ts @@ -16,26 +16,31 @@ * @fileoverview Unit tests for the feature tab in release coordinator page. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, fakeAsync, async, TestBed, flushMicrotasks, tick } from - '@angular/core/testing'; -import { FormsModule } from '@angular/forms'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import { + ComponentFixture, + fakeAsync, + async, + TestBed, + flushMicrotasks, + tick, +} from '@angular/core/testing'; +import {FormsModule} from '@angular/forms'; import cloneDeep from 'lodash/cloneDeep'; -import { FeaturesTabComponent } from - 'pages/release-coordinator-page/features-tab/features-tab.component'; -import { FeatureFlagDummyBackendApiService } from - 'domain/feature-flag/feature-flag-dummy-backend-api.service'; -import { FeatureFlagBackendApiService, FeatureFlagsResponse } from - 'domain/feature-flag/feature-flag-backend-api.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { FeatureStage } from 'domain/platform-parameter/platform-parameter.model'; -import { FeatureFlag } from 'domain/feature-flag/feature-flag.model'; -import { PlatformFeatureService } from 'services/platform-feature.service'; -import { HttpErrorResponse } from '@angular/common/http'; - +import {FeaturesTabComponent} from 'pages/release-coordinator-page/features-tab/features-tab.component'; +import {FeatureFlagDummyBackendApiService} from 'domain/feature-flag/feature-flag-dummy-backend-api.service'; +import { + FeatureFlagBackendApiService, + FeatureFlagsResponse, +} from 'domain/feature-flag/feature-flag-backend-api.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {FeatureStage} from 'domain/platform-parameter/platform-parameter.model'; +import {FeatureFlag} from 'domain/feature-flag/feature-flag.model'; +import {PlatformFeatureService} from 'services/platform-feature.service'; +import {HttpErrorResponse} from '@angular/common/http'; let dummyFeatureStatus = false; const mockDummyFeatureFlagForE2ETestsStatus = (status: boolean) => { @@ -48,13 +53,13 @@ class MockPlatformFeatureService { DummyFeatureFlagForE2ETests: { get isEnabled() { return dummyFeatureStatus; - } - } + }, + }, }; } } -describe('Release coordinator page feature tab', function() { +describe('Release coordinator page feature tab', function () { let component: FeaturesTabComponent; let fixture: ComponentFixture; let featureApiService: FeatureFlagBackendApiService; @@ -65,19 +70,17 @@ describe('Release coordinator page feature tab', function() { let mockConfirmResult: (val: boolean) => void; beforeEach(async(() => { - TestBed - .configureTestingModule({ - imports: [FormsModule, HttpClientTestingModule], - declarations: [FeaturesTabComponent], - providers: [ - { - provide: PlatformFeatureService, - useClass: MockPlatformFeatureService - } - ], - schemas: [NO_ERRORS_SCHEMA] - }) - .compileComponents(); + TestBed.configureTestingModule({ + imports: [FormsModule, HttpClientTestingModule], + declarations: [FeaturesTabComponent], + providers: [ + { + provide: PlatformFeatureService, + useClass: MockPlatformFeatureService, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); fixture = TestBed.createComponent(FeaturesTabComponent); component = fixture.componentInstance; @@ -89,9 +92,9 @@ describe('Release coordinator page feature tab', function() { spyOnProperty(windowRef, 'nativeWindow').and.returnValue({ confirm: () => confirmResult, prompt: () => promptResult, - alert: () => null + alert: () => null, } as unknown as Window); - mockConfirmResult = val => confirmResult = val; + mockConfirmResult = val => (confirmResult = val); spyOn(featureApiService, 'getFeatureFlags').and.resolveTo({ featureFlags: [ @@ -102,14 +105,16 @@ describe('Release coordinator page feature tab', function() { force_enable_for_all_users: false, rollout_percentage: 0, user_group_ids: [], - last_updated: null - }) + last_updated: null, + }), ], - serverStage: 'dev' + serverStage: 'dev', } as FeatureFlagsResponse); - updateApiSpy = spyOn(featureApiService, 'updateFeatureFlag') - .and.resolveTo(); + updateApiSpy = spyOn( + featureApiService, + 'updateFeatureFlag' + ).and.resolveTo(); component.ngOnInit(); })); @@ -117,7 +122,8 @@ describe('Release coordinator page feature tab', function() { it('should load feature flags on init', () => { expect(component.featureFlags.length).toBe(1); expect(component.featureFlags[0].name).toEqual( - 'dummy_feature_flag_for_e2e_tests'); + 'dummy_feature_flag_for_e2e_tests' + ); }); describe('.clearChanges', () => { @@ -130,7 +136,7 @@ describe('Release coordinator page feature tab', function() { expect(featureFlag.forceEnableForAllUsers).toBeFalse(); }); - it('should not proceed if the user doesn\'t confirm', () => { + it("should not proceed if the user doesn't confirm", () => { mockConfirmResult(false); const featureFlag = component.featureFlags[0]; @@ -145,51 +151,58 @@ describe('Release coordinator page feature tab', function() { describe('.getSchema', () => { it('should return the schema for rollout-percentage', () => { - expect(component.getSchema()).toEqual( - { - type: 'int', - validators: [{ + expect(component.getSchema()).toEqual({ + type: 'int', + validators: [ + { id: 'is_at_least', - min_value: 1 - }, { + min_value: 1, + }, + { id: 'is_at_most', - max_value: 100 - }] - } - ); + max_value: 100, + }, + ], + }); }); }); describe('.getLastUpdatedDate', () => { - it('should return the string when the feature has not been ' + - 'updated yet', (() => { - expect(component.getLastUpdatedDate( - component.featureFlags[0])).toEqual( - 'The feature has not been updated yet.'); - })); + it( + 'should return the string when the feature has not been ' + 'updated yet', + () => { + expect(component.getLastUpdatedDate(component.featureFlags[0])).toEqual( + 'The feature has not been updated yet.' + ); + } + ); - it('should return the human readable last updated string from date-time ' + - 'object string', () => { - let featureFlag = FeatureFlag.createFromBackendDict({ - description: 'This is a dummy feature flag.', - feature_stage: FeatureStage.PROD, - name: 'dummy_feature_flag_for_e2e_tests', - force_enable_for_all_users: false, - rollout_percentage: 0, - user_group_ids: [], - last_updated: '08/17/2023, 15:30:45:123456' - }); + it( + 'should return the human readable last updated string from date-time ' + + 'object string', + () => { + let featureFlag = FeatureFlag.createFromBackendDict({ + description: 'This is a dummy feature flag.', + feature_stage: FeatureStage.PROD, + name: 'dummy_feature_flag_for_e2e_tests', + force_enable_for_all_users: false, + rollout_percentage: 0, + user_group_ids: [], + last_updated: '08/17/2023, 15:30:45:123456', + }); - expect(component.getLastUpdatedDate(featureFlag)).toEqual( - 'Aug 17, 2023'); - }); + expect(component.getLastUpdatedDate(featureFlag)).toEqual( + 'Aug 17, 2023' + ); + } + ); }); describe('.getFeatureStageString()', () => { it('should return text for dev feature stage', () => { - expect(component.getFeatureStageString( - component.featureFlags[0])).toBe( - 'Dev (can only be enabled on dev server).'); + expect(component.getFeatureStageString(component.featureFlags[0])).toBe( + 'Dev (can only be enabled on dev server).' + ); }); it('should return text for test feature stage', () => { @@ -200,11 +213,11 @@ describe('Release coordinator page feature tab', function() { force_enable_for_all_users: false, rollout_percentage: 0, user_group_ids: [], - last_updated: null + last_updated: null, }); - expect(component.getFeatureStageString( - featureFlagTestStage)).toBe( - 'Test (can only be enabled on dev and test server).'); + expect(component.getFeatureStageString(featureFlagTestStage)).toBe( + 'Test (can only be enabled on dev and test server).' + ); }); it('should return text for prod feature stage', () => { @@ -215,11 +228,11 @@ describe('Release coordinator page feature tab', function() { force_enable_for_all_users: false, rollout_percentage: 0, user_group_ids: [], - last_updated: null + last_updated: null, }); - expect(component.getFeatureStageString( - featureFlagProdStage)).toBe( - 'Prod (can only be enabled on dev, test and prod server).'); + expect(component.getFeatureStageString(featureFlagProdStage)).toBe( + 'Prod (can only be enabled on dev, test and prod server).' + ); }); }); @@ -231,7 +244,7 @@ describe('Release coordinator page feature tab', function() { force_enable_for_all_users: false, rollout_percentage: 0, user_group_ids: [], - last_updated: null + last_updated: null, }); let featureFlagProdStage = FeatureFlag.createFromBackendDict({ @@ -241,59 +254,80 @@ describe('Release coordinator page feature tab', function() { force_enable_for_all_users: false, rollout_percentage: 0, user_group_ids: [], - last_updated: null + last_updated: null, }); afterEach(() => { component.serverStage = ''; }); - it('should return true when the server in dev stage and feature ' + - 'stage is dev too', (() => { - component.serverStage = 'dev'; + it( + 'should return true when the server in dev stage and feature ' + + 'stage is dev too', + () => { + component.serverStage = 'dev'; - expect(component.getFeatureValidOnCurrentServer( - featureFlagDevStage)).toBe(true); - })); + expect( + component.getFeatureValidOnCurrentServer(featureFlagDevStage) + ).toBe(true); + } + ); - it('should return false when the server in test stage and feature ' + - 'stage is dev', (() => { - component.serverStage = 'test'; + it( + 'should return false when the server in test stage and feature ' + + 'stage is dev', + () => { + component.serverStage = 'test'; - expect(component.getFeatureValidOnCurrentServer( - featureFlagDevStage)).toBe(false); - })); + expect( + component.getFeatureValidOnCurrentServer(featureFlagDevStage) + ).toBe(false); + } + ); - it('should return true when the server in test stage and feature ' + - 'stage is prod', (() => { - component.serverStage = 'test'; + it( + 'should return true when the server in test stage and feature ' + + 'stage is prod', + () => { + component.serverStage = 'test'; - expect(component.getFeatureValidOnCurrentServer( - featureFlagProdStage)).toBe(true); - })); + expect( + component.getFeatureValidOnCurrentServer(featureFlagProdStage) + ).toBe(true); + } + ); - it('should return true when the server in prod stage and feature ' + - 'stage is prod', (() => { - component.serverStage = 'prod'; + it( + 'should return true when the server in prod stage and feature ' + + 'stage is prod', + () => { + component.serverStage = 'prod'; - expect(component.getFeatureValidOnCurrentServer( - featureFlagProdStage)).toBe(true); - })); + expect( + component.getFeatureValidOnCurrentServer(featureFlagProdStage) + ).toBe(true); + } + ); - it('should return false when the server in prod stage and feature ' + - 'stage is dev', (() => { - component.serverStage = 'prod'; + it( + 'should return false when the server in prod stage and feature ' + + 'stage is dev', + () => { + component.serverStage = 'prod'; - expect(component.getFeatureValidOnCurrentServer( - featureFlagDevStage)).toBe(false); - })); + expect( + component.getFeatureValidOnCurrentServer(featureFlagDevStage) + ).toBe(false); + } + ); - it('should return false when the server stage is unknown', (() => { + it('should return false when the server stage is unknown', () => { component.serverStage = 'unknown'; - expect(component.getFeatureValidOnCurrentServer( - featureFlagDevStage)).toBe(false); - })); + expect( + component.getFeatureValidOnCurrentServer(featureFlagDevStage) + ).toBe(false); + }); }); describe('.updateFeatureFlag', () => { @@ -315,8 +349,10 @@ describe('Release coordinator page feature tab', function() { flushMicrotasks(); expect(updateApiSpy).toHaveBeenCalledWith( - featureFlag.name, featureFlag.forceEnableForAllUsers, - featureFlag.rolloutPercentage, featureFlag.userGroupIds + featureFlag.name, + featureFlag.forceEnableForAllUsers, + featureFlag.rolloutPercentage, + featureFlag.userGroupIds ); expect(setStatusSpy).toHaveBeenCalledWith('Saved successfully.'); })); @@ -331,8 +367,9 @@ describe('Release coordinator page feature tab', function() { flushMicrotasks(); - expect(component.featureFlagNameToBackupMap.get(featureFlag.name)) - .toEqual(featureFlag); + expect( + component.featureFlagNameToBackupMap.get(featureFlag.name) + ).toEqual(featureFlag); })); it('should not update feature backup if update fails', fakeAsync(() => { @@ -340,7 +377,7 @@ describe('Release coordinator page feature tab', function() { const errorResponse = new HttpErrorResponse({ error: 'Error loading exploration 1.', status: 500, - statusText: 'Internal Server Error' + statusText: 'Internal Server Error', }); updateApiSpy.and.rejectWith(errorResponse); @@ -352,25 +389,24 @@ describe('Release coordinator page feature tab', function() { flushMicrotasks(); - expect(component.featureFlagNameToBackupMap.get(featureFlag.name)) - .toEqual(originalFeatureFlag); + expect( + component.featureFlagNameToBackupMap.get(featureFlag.name) + ).toEqual(originalFeatureFlag); })); - it('should not proceed if the user cancels the prompt', fakeAsync( - () => { - mockConfirmResult(false); + it('should not proceed if the user cancels the prompt', fakeAsync(() => { + mockConfirmResult(false); - const featureFlag = component.featureFlags[0]; + const featureFlag = component.featureFlags[0]; - featureFlag.userGroupIds = ['user_group_1']; - component.updateFeatureFlag(featureFlag); + featureFlag.userGroupIds = ['user_group_1']; + component.updateFeatureFlag(featureFlag); - flushMicrotasks(); + flushMicrotasks(); - expect(updateApiSpy).not.toHaveBeenCalled(); - expect(setStatusSpy).not.toHaveBeenCalled(); - }) - ); + expect(updateApiSpy).not.toHaveBeenCalled(); + expect(setStatusSpy).not.toHaveBeenCalled(); + })); it('should not proceed if there is any validation issue', fakeAsync(() => { mockConfirmResult(true); @@ -392,7 +428,7 @@ describe('Release coordinator page feature tab', function() { const errorResponse = new HttpErrorResponse({ error: 'Error loading exploration 1.', status: 500, - statusText: 'Internal Server Error' + statusText: 'Internal Server Error', }); updateApiSpy.and.rejectWith(errorResponse); const featureFlag = component.featureFlags[0]; @@ -411,10 +447,10 @@ describe('Release coordinator page feature tab', function() { const errorResponse = new HttpErrorResponse({ error: { - error: 'validation error.' + error: 'validation error.', }, status: 500, - statusText: 'Internal Server Error' + statusText: 'Internal Server Error', }); updateApiSpy.and.rejectWith(errorResponse); const featureFlag = component.featureFlags[0]; @@ -426,7 +462,8 @@ describe('Release coordinator page feature tab', function() { expect(updateApiSpy).toHaveBeenCalled(); expect(setStatusSpy).toHaveBeenCalledWith( - 'Update failed: validation error.'); + 'Update failed: validation error.' + ); })); it('should throw error if error resonse is unexpected', fakeAsync(() => { @@ -443,26 +480,19 @@ describe('Release coordinator page feature tab', function() { }); describe('.isFeatureFlagChanged', () => { - it('should return false if the feature is the same as the backup instance', - () => { - const featureFlag = component.featureFlags[0]; + it('should return false if the feature is the same as the backup instance', () => { + const featureFlag = component.featureFlags[0]; - expect(component.isFeatureFlagChanged(featureFlag)) - .toBeFalse(); - } - ); + expect(component.isFeatureFlagChanged(featureFlag)).toBeFalse(); + }); - it( - 'should return true if the feature is different from the backup instance', - () => { - const featureFlag = component.featureFlags[0]; + it('should return true if the feature is different from the backup instance', () => { + const featureFlag = component.featureFlags[0]; - featureFlag.userGroupIds = ['user_group_1']; + featureFlag.userGroupIds = ['user_group_1']; - expect(component.isFeatureFlagChanged(featureFlag)) - .toBeTrue(); - } - ); + expect(component.isFeatureFlagChanged(featureFlag)).toBeTrue(); + }); it('should throw error if the feature name is not found', () => { const featureFlag = FeatureFlag.createFromBackendDict({ @@ -472,7 +502,7 @@ describe('Release coordinator page feature tab', function() { force_enable_for_all_users: false, rollout_percentage: 0, user_group_ids: [], - last_updated: null + last_updated: null, }); expect(() => { @@ -491,30 +521,30 @@ describe('Release coordinator page feature tab', function() { force_enable_for_all_users: false, rollout_percentage: 0, user_group_ids: [], - last_updated: null + last_updated: null, }) ); expect(issues).toEqual([]); }); - it('should return issues if rollout percentage is not between 10 and 100', - () => { - const issues = component.validateFeatureFlag( - FeatureFlag.createFromBackendDict({ - description: 'This is a dummy feature flag.', - feature_stage: FeatureStage.DEV, - name: 'dummy_feature_flag_for_e2e_tests', - force_enable_for_all_users: false, - rollout_percentage: 110, - user_group_ids: [], - last_updated: null - }) - ); + it('should return issues if rollout percentage is not between 10 and 100', () => { + const issues = component.validateFeatureFlag( + FeatureFlag.createFromBackendDict({ + description: 'This is a dummy feature flag.', + feature_stage: FeatureStage.DEV, + name: 'dummy_feature_flag_for_e2e_tests', + force_enable_for_all_users: false, + rollout_percentage: 110, + user_group_ids: [], + last_updated: null, + }) + ); - expect(issues).toEqual( - ['Rollout percentage should be between 0 to 100.']); - }); + expect(issues).toEqual([ + 'Rollout percentage should be between 0 to 100.', + ]); + }); }); describe('.dummyFeatureFlagForE2eTestsIsEnabled', () => { @@ -537,32 +567,27 @@ describe('Release coordinator page feature tab', function() { beforeEach(() => { dummyApiService = TestBed.get(FeatureFlagDummyBackendApiService); - dummyApiSpy = spyOn(dummyApiService, 'isHandlerEnabled') - .and.resolveTo(); + dummyApiSpy = spyOn(dummyApiService, 'isHandlerEnabled').and.resolveTo(); }); - it('should not request dummy handler if the dummy feature is disabled', - fakeAsync(() => { - mockDummyFeatureFlagForE2ETestsStatus(false); + it('should not request dummy handler if the dummy feature is disabled', fakeAsync(() => { + mockDummyFeatureFlagForE2ETestsStatus(false); - component.reloadDummyHandlerStatusAsync(); + component.reloadDummyHandlerStatusAsync(); - flushMicrotasks(); + flushMicrotasks(); - expect(dummyApiSpy).not.toHaveBeenCalled(); - }) - ); + expect(dummyApiSpy).not.toHaveBeenCalled(); + })); - it('should request dummy handler if the dummy feature is enabled', - fakeAsync(() => { - mockDummyFeatureFlagForE2ETestsStatus(true); + it('should request dummy handler if the dummy feature is enabled', fakeAsync(() => { + mockDummyFeatureFlagForE2ETestsStatus(true); - component.reloadDummyHandlerStatusAsync(); + component.reloadDummyHandlerStatusAsync(); - flushMicrotasks(); + flushMicrotasks(); - expect(dummyApiSpy).toHaveBeenCalled(); - }) - ); + expect(dummyApiSpy).toHaveBeenCalled(); + })); }); }); diff --git a/core/templates/pages/release-coordinator-page/features-tab/features-tab.component.ts b/core/templates/pages/release-coordinator-page/features-tab/features-tab.component.ts index 8947a0bdf071..349d156ba8cd 100644 --- a/core/templates/pages/release-coordinator-page/features-tab/features-tab.component.ts +++ b/core/templates/pages/release-coordinator-page/features-tab/features-tab.component.ts @@ -16,23 +16,20 @@ * @fileoverview Component for the feature tab on the release coordinator page. */ -import { Component, OnInit, EventEmitter, Output } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component, OnInit, EventEmitter, Output} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; import cloneDeep from 'lodash/cloneDeep'; import isEqual from 'lodash/isEqual'; -import { Subscription } from 'rxjs'; - -import { LoaderService } from 'services/loader.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { FeatureFlagDummyBackendApiService } from - 'domain/feature-flag/feature-flag-dummy-backend-api.service'; -import { FeatureFlagBackendApiService } from - 'domain/feature-flag/feature-flag-backend-api.service'; -import { PlatformFeatureService } from 'services/platform-feature.service'; -import { FeatureFlag } from 'domain/feature-flag/feature-flag.model'; -import { HttpErrorResponse } from '@angular/common/http'; +import {Subscription} from 'rxjs'; +import {LoaderService} from 'services/loader.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {FeatureFlagDummyBackendApiService} from 'domain/feature-flag/feature-flag-dummy-backend-api.service'; +import {FeatureFlagBackendApiService} from 'domain/feature-flag/feature-flag-backend-api.service'; +import {PlatformFeatureService} from 'services/platform-feature.service'; +import {FeatureFlag} from 'domain/feature-flag/feature-flag.model'; +import {HttpErrorResponse} from '@angular/common/http'; interface IntSchema { type: 'int'; @@ -41,7 +38,7 @@ interface IntSchema { @Component({ selector: 'features-tab', - templateUrl: './features-tab.component.html' + templateUrl: './features-tab.component.html', }) export class FeaturesTabComponent implements OnInit { @Output() setStatusMessage = new EventEmitter(); @@ -66,7 +63,7 @@ export class FeaturesTabComponent implements OnInit { private apiService: FeatureFlagBackendApiService, private featureService: PlatformFeatureService, private dummyApiService: FeatureFlagDummyBackendApiService, - private loaderService: LoaderService, + private loaderService: LoaderService ) {} async reloadFeatureFlagsAsync(): Promise { @@ -75,20 +72,24 @@ export class FeaturesTabComponent implements OnInit { this.featureFlagsAreFetched = true; this.featureFlags = data.featureFlags; this.featureFlagNameToBackupMap = new Map( - this.featureFlags.map(feature => [feature.name, cloneDeep(feature)])); + this.featureFlags.map(feature => [feature.name, cloneDeep(feature)]) + ); this.loaderService.hideLoadingScreen(); } getSchema(): IntSchema { return { type: 'int', - validators: [{ - id: 'is_at_least', - min_value: 1 - }, { - id: 'is_at_most', - max_value: 100 - }] + validators: [ + { + id: 'is_at_least', + min_value: 1, + }, + { + id: 'is_at_most', + max_value: 100, + }, + ], }; } @@ -109,9 +110,18 @@ export class FeaturesTabComponent implements OnInit { const millisecond = parseInt(timeParts[3], 10); const parsedDate = new Date( - year, month, day, hour, minute, second, millisecond); + year, + month, + day, + hour, + minute, + second, + millisecond + ); const options: Intl.DateTimeFormatOptions = { - day: '2-digit', month: 'short', year: 'numeric' + day: '2-digit', + month: 'short', + year: 'numeric', }; return parsedDate.toLocaleDateString('en-US', options); } @@ -130,10 +140,10 @@ export class FeaturesTabComponent implements OnInit { if (this.serverStage === this.DEV_SERVER_STAGE) { return true; } else if (this.serverStage === this.TEST_SERVER_STAGE) { - return ( - feature.featureStage === this.TEST_SERVER_STAGE || + return feature.featureStage === this.TEST_SERVER_STAGE || feature.featureStage === this.PROD_SERVER_STAGE - ) ? true : false; + ? true + : false; } else if (this.serverStage === this.PROD_SERVER_STAGE) { return feature.featureStage === this.PROD_SERVER_STAGE ? true : false; } @@ -141,8 +151,11 @@ export class FeaturesTabComponent implements OnInit { } async updateFeatureFlag(feature: FeatureFlag): Promise { - if (!this.windowRef.nativeWindow.confirm( - 'This action is irreversible. Are you sure?')) { + if ( + !this.windowRef.nativeWindow.confirm( + 'This action is irreversible. Are you sure?' + ) + ) { return; } const issues = this.validateFeatureFlag(feature); @@ -152,16 +165,19 @@ export class FeaturesTabComponent implements OnInit { } try { await this.apiService.updateFeatureFlag( - feature.name, feature.forceEnableForAllUsers, feature.rolloutPercentage, - feature.userGroupIds); + feature.name, + feature.forceEnableForAllUsers, + feature.rolloutPercentage, + feature.userGroupIds + ); this.featureFlagNameToBackupMap.set(feature.name, cloneDeep(feature)); this.setStatusMessage.emit('Saved successfully.'); - // We use unknown type because we are unsure of the type of error - // that was thrown. Since the catch block cannot identify the - // specific type of error, we are unable to further optimise the - // code by introducing more types of errors. + // We use unknown type because we are unsure of the type of error + // that was thrown. Since the catch block cannot identify the + // specific type of error, we are unable to further optimise the + // code by introducing more types of errors. } catch (e: unknown) { if (e instanceof HttpErrorResponse) { if (e.error && e.error.error) { @@ -176,13 +192,14 @@ export class FeaturesTabComponent implements OnInit { } clearChanges(featureFlag: FeatureFlag): void { - if (!this.windowRef.nativeWindow.confirm( - 'This will revert all changes you made. Are you sure?')) { + if ( + !this.windowRef.nativeWindow.confirm( + 'This will revert all changes you made. Are you sure?' + ) + ) { return; } - const backup = this.featureFlagNameToBackupMap.get( - featureFlag.name - ); + const backup = this.featureFlagNameToBackupMap.get(featureFlag.name); if (backup) { featureFlag.forceEnableForAllUsers = backup.forceEnableForAllUsers; @@ -198,11 +215,11 @@ export class FeaturesTabComponent implements OnInit { } return ( !isEqual( - original.forceEnableForAllUsers, feature.forceEnableForAllUsers - ) || !isEqual( - original.rolloutPercentage, feature.rolloutPercentage - ) || !isEqual( - original.userGroupIds, feature.userGroupIds) + original.forceEnableForAllUsers, + feature.forceEnableForAllUsers + ) || + !isEqual(original.rolloutPercentage, feature.rolloutPercentage) || + !isEqual(original.userGroupIds, feature.userGroupIds) ); } @@ -234,11 +251,10 @@ export class FeaturesTabComponent implements OnInit { ngOnInit(): void { this.directiveSubscriptions.add( - this.loaderService.onLoadingMessageChange.subscribe( - (message: string) => { - this.loadingMessage = message; - } - )); + this.loaderService.onLoadingMessageChange.subscribe((message: string) => { + this.loadingMessage = message; + }) + ); this.loaderService.showLoadingScreen('Loading'); this.reloadFeatureFlagsAsync(); this.reloadDummyHandlerStatusAsync(); @@ -249,6 +265,9 @@ export class FeaturesTabComponent implements OnInit { } } -angular.module('oppia').directive( - 'adminFeaturesTab', downgradeComponent( - {component: FeaturesTabComponent})); +angular + .module('oppia') + .directive( + 'adminFeaturesTab', + downgradeComponent({component: FeaturesTabComponent}) + ); diff --git a/core/templates/pages/release-coordinator-page/features-tab/features-tab.constants.ts b/core/templates/pages/release-coordinator-page/features-tab/features-tab.constants.ts index d6d9cfa3dfe0..fc8d36241465 100644 --- a/core/templates/pages/release-coordinator-page/features-tab/features-tab.constants.ts +++ b/core/templates/pages/release-coordinator-page/features-tab/features-tab.constants.ts @@ -12,25 +12,27 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { AppConstants } from 'app.constants'; +import {AppConstants} from 'app.constants'; /** * @fileoverview Constants for the admin features tab. */ export const AdminFeaturesTabConstants = { - ALLOWED_PLATFORM_TYPES: ( - AppConstants.PLATFORM_PARAMETER_ALLOWED_PLATFORM_TYPES), + ALLOWED_PLATFORM_TYPES: + AppConstants.PLATFORM_PARAMETER_ALLOWED_PLATFORM_TYPES, // Matches app version with the numeric part only, hash and flavor are not // needed since hash is redundant and there is already app_version_flavor // filter. APP_VERSION_REGEXP: new RegExp( - AppConstants.PLATFORM_PARAMETER_APP_VERSION_WITHOUT_HASH_REGEXP), + AppConstants.PLATFORM_PARAMETER_APP_VERSION_WITHOUT_HASH_REGEXP + ), - ALLOWED_SITE_LANGUAGE_IDS: AppConstants.SUPPORTED_SITE_LANGUAGES - .map((lang: {id: string}) => lang.id), + ALLOWED_SITE_LANGUAGE_IDS: AppConstants.SUPPORTED_SITE_LANGUAGES.map( + (lang: {id: string}) => lang.id + ), - ALLOWED_APP_VERSION_FLAVORS: ( - AppConstants.PLATFORM_PARAMETER_ALLOWED_APP_VERSION_FLAVORS), + ALLOWED_APP_VERSION_FLAVORS: + AppConstants.PLATFORM_PARAMETER_ALLOWED_APP_VERSION_FLAVORS, } as const; diff --git a/core/templates/pages/release-coordinator-page/navbar/release-coordinator-navbar.component.spec.ts b/core/templates/pages/release-coordinator-page/navbar/release-coordinator-navbar.component.spec.ts index 1f7929ee415b..c896237dfb33 100644 --- a/core/templates/pages/release-coordinator-page/navbar/release-coordinator-navbar.component.spec.ts +++ b/core/templates/pages/release-coordinator-page/navbar/release-coordinator-navbar.component.spec.ts @@ -16,17 +16,21 @@ * @fileoverview Unit tests for release-coordinator navbar component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, fakeAsync, flush, TestBed } from '@angular/core/testing'; -import { RouterModule } from '@angular/router'; -import { APP_BASE_HREF } from '@angular/common'; -import { SmartRouterModule } from 'hybrid-router-module-provider'; - -import { UserService } from 'services/user.service'; -import { ReleaseCoordinatorPageConstants } from '../release-coordinator-page.constants'; -import { ReleaseCoordinatorNavbarComponent } from './release-coordinator-navbar.component'; -import { UserInfo } from 'domain/user/user-info.model'; - +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import { + ComponentFixture, + fakeAsync, + flush, + TestBed, +} from '@angular/core/testing'; +import {RouterModule} from '@angular/router'; +import {APP_BASE_HREF} from '@angular/common'; +import {SmartRouterModule} from 'hybrid-router-module-provider'; + +import {UserService} from 'services/user.service'; +import {ReleaseCoordinatorPageConstants} from '../release-coordinator-page.constants'; +import {ReleaseCoordinatorNavbarComponent} from './release-coordinator-navbar.component'; +import {UserInfo} from 'domain/user/user-info.model'; describe('Release coordinator navbar component', () => { let component: ReleaseCoordinatorNavbarComponent; @@ -40,13 +44,15 @@ describe('Release coordinator navbar component', () => { // TODO(#13443): Remove hybrid router module provider once all pages are // migrated to angular router. SmartRouterModule, - RouterModule.forRoot([]) + RouterModule.forRoot([]), ], declarations: [ReleaseCoordinatorNavbarComponent], - providers: [{ - provide: APP_BASE_HREF, - useValue: '/' - }] + providers: [ + { + provide: APP_BASE_HREF, + useValue: '/', + }, + ], }).compileComponents(); fixture = TestBed.createComponent(ReleaseCoordinatorNavbarComponent); @@ -54,8 +60,10 @@ describe('Release coordinator navbar component', () => { userService = TestBed.inject(UserService); fixture.detectChanges(); - spyOn(userService, 'getProfileImageDataUrl').and.returnValue( - ['profile-image-url-png', 'profile-image-url-webp']); + spyOn(userService, 'getProfileImageDataUrl').and.returnValue([ + 'profile-image-url-png', + 'profile-image-url-webp', + ]); spyOn(component.activeTabChange, 'emit'); component.ngOnInit(); })); @@ -64,56 +72,59 @@ describe('Release coordinator navbar component', () => { let userInfo = { isModerator: () => true, getUsername: () => 'username1', - isSuperAdmin: () => true + isSuperAdmin: () => true, }; - spyOn(userService, 'getUserInfoAsync') - .and.resolveTo(userInfo as UserInfo); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo as UserInfo); component.ngOnInit(); flush(); - expect(component.profilePicturePngDataUrl).toEqual( - 'profile-image-url-png'); + expect(component.profilePicturePngDataUrl).toEqual('profile-image-url-png'); expect(component.profilePictureWebpDataUrl).toEqual( - 'profile-image-url-webp'); + 'profile-image-url-webp' + ); expect(component.username).toBe('username1'); expect(component.profileUrl).toEqual(profileUrl); expect(component.logoutUrl).toEqual('/logout'); expect(component.profileDropdownIsActive).toBe(false); expect(component.activeTab).toBe( - ReleaseCoordinatorPageConstants.TAB_ID_BEAM_JOBS); + ReleaseCoordinatorPageConstants.TAB_ID_BEAM_JOBS + ); })); - it('should get default profile pictures when username is null', - fakeAsync(() => { - let userInfo = { - getUsername: () => null, - isSuperAdmin: () => true, - getEmail: () => 'test_email@example.com' - }; - spyOn(userService, 'getUserInfoAsync') - .and.resolveTo(userInfo as UserInfo); - component.ngOnInit(); - flush(); - - expect(component.profilePicturePngDataUrl).toEqual( - '/assets/images/avatar/user_blue_150px.png'); - expect(component.profilePictureWebpDataUrl).toEqual( - '/assets/images/avatar/user_blue_150px.webp'); - })); + it('should get default profile pictures when username is null', fakeAsync(() => { + let userInfo = { + getUsername: () => null, + isSuperAdmin: () => true, + getEmail: () => 'test_email@example.com', + }; + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo as UserInfo); + component.ngOnInit(); + flush(); + + expect(component.profilePicturePngDataUrl).toEqual( + '/assets/images/avatar/user_blue_150px.png' + ); + expect(component.profilePictureWebpDataUrl).toEqual( + '/assets/images/avatar/user_blue_150px.webp' + ); + })); it('should allow switching tabs correctly', () => { expect(component.activeTab).toBe( - ReleaseCoordinatorPageConstants.TAB_ID_BEAM_JOBS); + ReleaseCoordinatorPageConstants.TAB_ID_BEAM_JOBS + ); component.switchTab(ReleaseCoordinatorPageConstants.TAB_ID_MISC); - component.activeTabChange.subscribe( - (tabName: string) => expect(tabName).toBe( - ReleaseCoordinatorPageConstants.TAB_ID_MISC)); + component.activeTabChange.subscribe((tabName: string) => + expect(tabName).toBe(ReleaseCoordinatorPageConstants.TAB_ID_MISC) + ); expect(component.activeTab).toBe( - ReleaseCoordinatorPageConstants.TAB_ID_MISC); + ReleaseCoordinatorPageConstants.TAB_ID_MISC + ); expect(component.activeTabChange.emit).toHaveBeenCalledWith( - ReleaseCoordinatorPageConstants.TAB_ID_MISC); + ReleaseCoordinatorPageConstants.TAB_ID_MISC + ); }); it('should set profileDropdownIsActive to true', () => { diff --git a/core/templates/pages/release-coordinator-page/navbar/release-coordinator-navbar.component.ts b/core/templates/pages/release-coordinator-page/navbar/release-coordinator-navbar.component.ts index a613fca52414..2237be47f133 100644 --- a/core/templates/pages/release-coordinator-page/navbar/release-coordinator-navbar.component.ts +++ b/core/templates/pages/release-coordinator-page/navbar/release-coordinator-navbar.component.ts @@ -17,14 +17,13 @@ * panel. */ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; - -import { AppConstants } from 'app.constants'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { ReleaseCoordinatorPageConstants } from 'pages/release-coordinator-page/release-coordinator-page.constants'; -import { UserService } from 'services/user.service'; +import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {AppConstants} from 'app.constants'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {ReleaseCoordinatorPageConstants} from 'pages/release-coordinator-page/release-coordinator-page.constants'; +import {UserService} from 'services/user.service'; @Component({ selector: 'oppia-release-coordinator-navbar', @@ -43,27 +42,26 @@ export class ReleaseCoordinatorNavbarComponent implements OnInit { profileUrl!: string; logoWebpImageSrc!: string; logoPngImageSrc!: string; - logoutUrl: string = ( - '/' + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LOGOUT.ROUTE); + logoutUrl: string = + '/' + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.LOGOUT.ROUTE; profileDropdownIsActive: boolean = false; TAB_ID_BEAM_JOBS: string = ReleaseCoordinatorPageConstants.TAB_ID_BEAM_JOBS; TAB_ID_FEATURES: string = ReleaseCoordinatorPageConstants.TAB_ID_FEATURES; TAB_ID_MISC: string = ReleaseCoordinatorPageConstants.TAB_ID_MISC; - PAGES_REGISTERED_WITH_FRONTEND = ( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND); + PAGES_REGISTERED_WITH_FRONTEND = AppConstants.PAGES_REGISTERED_WITH_FRONTEND; constructor( private urlInterpolationService: UrlInterpolationService, - private userService: UserService, + private userService: UserService ) {} activateProfileDropdown(): boolean { - return this.profileDropdownIsActive = true; + return (this.profileDropdownIsActive = true); } deactivateProfileDropdown(): boolean { - return this.profileDropdownIsActive = false; + return (this.profileDropdownIsActive = false); } switchTab(tabName: string): void { @@ -77,20 +75,23 @@ export class ReleaseCoordinatorNavbarComponent implements OnInit { const userInfo = await this.userService.getUserInfoAsync(); this.username = userInfo.getUsername(); if (this.username) { - this.profileUrl = ( - this.urlInterpolationService.interpolateUrl( - '/profile/', { - username: this.username - })); - [this.profilePicturePngDataUrl, this.profilePictureWebpDataUrl] = ( - this.userService.getProfileImageDataUrl(this.username)); + this.profileUrl = this.urlInterpolationService.interpolateUrl( + '/profile/', + { + username: this.username, + } + ); + [this.profilePicturePngDataUrl, this.profilePictureWebpDataUrl] = + this.userService.getProfileImageDataUrl(this.username); } else { - this.profilePictureWebpDataUrl = ( + this.profilePictureWebpDataUrl = this.urlInterpolationService.getStaticImageUrl( - AppConstants.DEFAULT_PROFILE_IMAGE_WEBP_PATH)); - this.profilePicturePngDataUrl = ( + AppConstants.DEFAULT_PROFILE_IMAGE_WEBP_PATH + ); + this.profilePicturePngDataUrl = this.urlInterpolationService.getStaticImageUrl( - AppConstants.DEFAULT_PROFILE_IMAGE_PNG_PATH)); + AppConstants.DEFAULT_PROFILE_IMAGE_PNG_PATH + ); } } @@ -98,14 +99,19 @@ export class ReleaseCoordinatorNavbarComponent implements OnInit { this.getUserInfoAsync(); this.logoPngImageSrc = this.urlInterpolationService.getStaticImageUrl( - '/logo/288x128_logo_white.png'); + '/logo/288x128_logo_white.png' + ); this.logoWebpImageSrc = this.urlInterpolationService.getStaticImageUrl( - '/logo/288x128_logo_white.webp'); + '/logo/288x128_logo_white.webp' + ); this.activeTab = this.TAB_ID_BEAM_JOBS; } } -angular.module('oppia').directive( - 'oppiaReleaseCoordinatorNavbar', downgradeComponent( - {component: ReleaseCoordinatorNavbarComponent})); +angular + .module('oppia') + .directive( + 'oppiaReleaseCoordinatorNavbar', + downgradeComponent({component: ReleaseCoordinatorNavbarComponent}) + ); diff --git a/core/templates/pages/release-coordinator-page/release-coordinator-page-root.component.spec.ts b/core/templates/pages/release-coordinator-page/release-coordinator-page-root.component.spec.ts index 1adbc60abaab..36eec92d854f 100644 --- a/core/templates/pages/release-coordinator-page/release-coordinator-page-root.component.spec.ts +++ b/core/templates/pages/release-coordinator-page/release-coordinator-page-root.component.spec.ts @@ -16,19 +16,25 @@ * @fileoverview Unit tests for the release coordinator root component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; - -import { AppConstants } from 'app.constants'; -import { AccessValidationBackendApiService } from 'pages/oppia-root/routing/access-validation-backend-api.service'; -import { MetaTagCustomizationService } from 'services/contextual/meta-tag-customization.service'; -import { LoaderService } from 'services/loader.service'; -import { PageHeadService } from 'services/page-head.service'; - -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { ReleaseCoordinatorPageRootComponent } from './release-coordinator-page-root.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; + +import {AppConstants} from 'app.constants'; +import {AccessValidationBackendApiService} from 'pages/oppia-root/routing/access-validation-backend-api.service'; +import {MetaTagCustomizationService} from 'services/contextual/meta-tag-customization.service'; +import {LoaderService} from 'services/loader.service'; +import {PageHeadService} from 'services/page-head.service'; + +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {ReleaseCoordinatorPageRootComponent} from './release-coordinator-page-root.component'; class MockTranslateService { onLangChange: EventEmitter = new EventEmitter(); @@ -47,22 +53,17 @@ describe('Release Coordinator Page Root', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - ReleaseCoordinatorPageRootComponent, - MockTranslatePipe - ], + imports: [HttpClientTestingModule], + declarations: [ReleaseCoordinatorPageRootComponent, MockTranslatePipe], providers: [ PageHeadService, MetaTagCustomizationService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -72,20 +73,20 @@ describe('Release Coordinator Page Root', () => { pageHeadService = TestBed.inject(PageHeadService); loaderService = TestBed.inject(LoaderService); accessValidationBackendApiService = TestBed.inject( - AccessValidationBackendApiService); + AccessValidationBackendApiService + ); translateService = TestBed.inject(TranslateService); }); - it('should successfully instantiate the component', - () => { - expect(component).toBeDefined(); - }); + it('should successfully instantiate the component', () => { + expect(component).toBeDefined(); + }); it('should initialize and show page when access is valid', fakeAsync(() => { spyOn( accessValidationBackendApiService, - 'validateAccessToReleaseCoordinatorPage') - .and.returnValue(Promise.resolve()); + 'validateAccessToReleaseCoordinatorPage' + ).and.returnValue(Promise.resolve()); spyOn(loaderService, 'showLoadingScreen'); spyOn(loaderService, 'hideLoadingScreen'); @@ -94,40 +95,38 @@ describe('Release Coordinator Page Root', () => { expect(loaderService.showLoadingScreen).toHaveBeenCalled(); expect( - accessValidationBackendApiService.validateAccessToReleaseCoordinatorPage) - .toHaveBeenCalled(); + accessValidationBackendApiService.validateAccessToReleaseCoordinatorPage + ).toHaveBeenCalled(); expect(component.pageIsShown).toBeTrue(); expect(component.errorPageIsShown).toBeFalse(); expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); })); - it('should initialize and show error page when server respond with error', - fakeAsync(() => { - spyOn( - accessValidationBackendApiService, - 'validateAccessToReleaseCoordinatorPage') - .and.returnValue(Promise.reject()); - spyOn(loaderService, 'showLoadingScreen'); - spyOn(loaderService, 'hideLoadingScreen'); - - component.ngOnInit(); - tick(); - - expect(loaderService.showLoadingScreen).toHaveBeenCalled(); - expect( - accessValidationBackendApiService - .validateAccessToReleaseCoordinatorPage) - .toHaveBeenCalled(); - expect(component.pageIsShown).toBeFalse(); - expect(component.errorPageIsShown).toBeTrue(); - expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); - })); + it('should initialize and show error page when server respond with error', fakeAsync(() => { + spyOn( + accessValidationBackendApiService, + 'validateAccessToReleaseCoordinatorPage' + ).and.returnValue(Promise.reject()); + spyOn(loaderService, 'showLoadingScreen'); + spyOn(loaderService, 'hideLoadingScreen'); + + component.ngOnInit(); + tick(); + + expect(loaderService.showLoadingScreen).toHaveBeenCalled(); + expect( + accessValidationBackendApiService.validateAccessToReleaseCoordinatorPage + ).toHaveBeenCalled(); + expect(component.pageIsShown).toBeFalse(); + expect(component.errorPageIsShown).toBeTrue(); + expect(loaderService.hideLoadingScreen).toHaveBeenCalled(); + })); it('should initialize and subscribe to onLangChange', fakeAsync(() => { spyOn( accessValidationBackendApiService, - 'validateAccessToReleaseCoordinatorPage') - .and.returnValue(Promise.resolve()); + 'validateAccessToReleaseCoordinatorPage' + ).and.returnValue(Promise.resolve()); spyOn(component.directiveSubscriptions, 'add'); spyOn(translateService.onLangChange, 'subscribe'); @@ -141,8 +140,8 @@ describe('Release Coordinator Page Root', () => { it('should update page title whenever the language changes', () => { spyOn( accessValidationBackendApiService, - 'validateAccessToReleaseCoordinatorPage') - .and.returnValue(Promise.resolve()); + 'validateAccessToReleaseCoordinatorPage' + ).and.returnValue(Promise.resolve()); component.ngOnInit(); spyOn(component, 'setPageTitleAndMetaTags'); @@ -158,13 +157,13 @@ describe('Release Coordinator Page Root', () => { component.setPageTitleAndMetaTags(); expect(translateService.instant).toHaveBeenCalledWith( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND - .RELEASE_COORDINATOR_PAGE.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.RELEASE_COORDINATOR_PAGE.TITLE + ); expect(pageHeadService.updateTitleAndMetaTags).toHaveBeenCalledWith( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND - .RELEASE_COORDINATOR_PAGE.TITLE, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND - .RELEASE_COORDINATOR_PAGE.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.RELEASE_COORDINATOR_PAGE + .TITLE, + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.RELEASE_COORDINATOR_PAGE.META + ); }); it('should unsubscribe on component destruction', () => { diff --git a/core/templates/pages/release-coordinator-page/release-coordinator-page-root.component.ts b/core/templates/pages/release-coordinator-page/release-coordinator-page-root.component.ts index 63c5e6ee0822..89e75dc4985a 100644 --- a/core/templates/pages/release-coordinator-page/release-coordinator-page-root.component.ts +++ b/core/templates/pages/release-coordinator-page/release-coordinator-page-root.component.ts @@ -16,18 +16,18 @@ * @fileoverview Root component for Release Coordinator Page. */ -import { Component, OnDestroy } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; +import {Component, OnDestroy} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; -import { AppConstants } from 'app.constants'; -import { AccessValidationBackendApiService } from 'pages/oppia-root/routing/access-validation-backend-api.service'; -import { LoaderService } from 'services/loader.service'; -import { PageHeadService } from 'services/page-head.service'; +import {AppConstants} from 'app.constants'; +import {AccessValidationBackendApiService} from 'pages/oppia-root/routing/access-validation-backend-api.service'; +import {LoaderService} from 'services/loader.service'; +import {PageHeadService} from 'services/page-head.service'; @Component({ selector: 'oppia-release-coordinator-page-root', - templateUrl: './release-coordinator-page-root.component.html' + templateUrl: './release-coordinator-page-root.component.html', }) export class ReleaseCoordinatorPageRootComponent implements OnDestroy { directiveSubscriptions = new Subscription(); @@ -35,8 +35,7 @@ export class ReleaseCoordinatorPageRootComponent implements OnDestroy { pageIsShown: boolean = false; constructor( - private accessValidationBackendApiService: - AccessValidationBackendApiService, + private accessValidationBackendApiService: AccessValidationBackendApiService, private loaderService: LoaderService, private pageHeadService: PageHeadService, private translateService: TranslateService @@ -46,10 +45,12 @@ export class ReleaseCoordinatorPageRootComponent implements OnDestroy { const releaseCoordinatorPage = AppConstants.PAGES_REGISTERED_WITH_FRONTEND.RELEASE_COORDINATOR_PAGE; const translatedTitle = this.translateService.instant( - releaseCoordinatorPage.TITLE); + releaseCoordinatorPage.TITLE + ); this.pageHeadService.updateTitleAndMetaTags( translatedTitle, - releaseCoordinatorPage.META); + releaseCoordinatorPage.META + ); } ngOnInit(): void { @@ -62,11 +63,15 @@ export class ReleaseCoordinatorPageRootComponent implements OnDestroy { this.loaderService.showLoadingScreen('Loading'); this.accessValidationBackendApiService .validateAccessToReleaseCoordinatorPage() - .then((resp) => { - this.pageIsShown = true; - }, (err) => { - this.errorPageIsShown = true; - }).then(() => { + .then( + resp => { + this.pageIsShown = true; + }, + err => { + this.errorPageIsShown = true; + } + ) + .then(() => { this.loaderService.hideLoadingScreen(); }); } diff --git a/core/templates/pages/release-coordinator-page/release-coordinator-page-routing.module.ts b/core/templates/pages/release-coordinator-page/release-coordinator-page-routing.module.ts index cf92caeb6cdb..adbba63b8338 100644 --- a/core/templates/pages/release-coordinator-page/release-coordinator-page-routing.module.ts +++ b/core/templates/pages/release-coordinator-page/release-coordinator-page-routing.module.ts @@ -16,24 +16,19 @@ * @fileoverview Routing module for release coordinator page. */ -import { NgModule } from '@angular/core'; -import { Route, RouterModule } from '@angular/router'; -import { ReleaseCoordinatorPageRootComponent } from './release-coordinator-page-root.component'; +import {NgModule} from '@angular/core'; +import {Route, RouterModule} from '@angular/router'; +import {ReleaseCoordinatorPageRootComponent} from './release-coordinator-page-root.component'; const routes: Route[] = [ { path: '', - component: ReleaseCoordinatorPageRootComponent - } + component: ReleaseCoordinatorPageRootComponent, + }, ]; @NgModule({ - imports: [ - RouterModule.forChild(routes) - ], - exports: [ - RouterModule - ] + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], }) - export class ReleaseCoordinatorPageRoutingModule {} diff --git a/core/templates/pages/release-coordinator-page/release-coordinator-page.component.spec.ts b/core/templates/pages/release-coordinator-page/release-coordinator-page.component.spec.ts index 0a2f19a0c3d0..c71146ac5c57 100644 --- a/core/templates/pages/release-coordinator-page/release-coordinator-page.component.spec.ts +++ b/core/templates/pages/release-coordinator-page/release-coordinator-page.component.spec.ts @@ -16,15 +16,21 @@ * @fileoverview Unit tests for release coordinator page component. */ -import { TestBed, waitForAsync, ComponentFixture, fakeAsync, tick } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { FormBuilder } from '@angular/forms'; -import { PromoBarBackendApiService } from 'services/promo-bar-backend-api.service'; -import { ReleaseCoordinatorBackendApiService } from './services/release-coordinator-backend-api.service'; -import { ReleaseCoordinatorPageConstants } from './release-coordinator-page.constants'; -import { ReleaseCoordinatorPageComponent } from './release-coordinator-page.component'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { PromoBar } from 'domain/promo_bar/promo-bar.model'; +import { + TestBed, + waitForAsync, + ComponentFixture, + fakeAsync, + tick, +} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {FormBuilder} from '@angular/forms'; +import {PromoBarBackendApiService} from 'services/promo-bar-backend-api.service'; +import {ReleaseCoordinatorBackendApiService} from './services/release-coordinator-backend-api.service'; +import {ReleaseCoordinatorPageConstants} from './release-coordinator-page.constants'; +import {ReleaseCoordinatorPageComponent} from './release-coordinator-page.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {PromoBar} from 'domain/promo_bar/promo-bar.model'; describe('Release coordinator page', () => { let component: ReleaseCoordinatorPageComponent; @@ -36,25 +42,19 @@ describe('Release coordinator page', () => { let testMemoryCacheProfile = { peak_allocation: '1430120', total_allocation: '1014112', - total_keys_stored: '2' + total_keys_stored: '2', }; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - ], - declarations: [ - ReleaseCoordinatorPageComponent - ], + imports: [HttpClientTestingModule], + declarations: [ReleaseCoordinatorPageComponent], providers: [ PromoBarBackendApiService, ReleaseCoordinatorBackendApiService, - FormBuilder + FormBuilder, ], - schemas: [ - NO_ERRORS_SCHEMA - ] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -67,108 +67,115 @@ describe('Release coordinator page', () => { beforeEach(() => { spyOn(pbbas, 'getPromoBarDataAsync').and.returnValue( - Promise.resolve(testPromoBarData)); + Promise.resolve(testPromoBarData) + ); component.ngOnInit(); }); - it('should load the component with the correct properties' + - 'when user navigates to release coordinator page', fakeAsync(() => { - expect(component.statusMessage).toEqual(''); - expect(component.submitButtonDisabled).toBeTrue(); - expect(component.memoryCacheDataFetched).toBeFalse(); - expect(component.activeTab).toEqual( - ReleaseCoordinatorPageConstants.TAB_ID_BEAM_JOBS); + it( + 'should load the component with the correct properties' + + 'when user navigates to release coordinator page', + fakeAsync(() => { + expect(component.statusMessage).toEqual(''); + expect(component.submitButtonDisabled).toBeTrue(); + expect(component.memoryCacheDataFetched).toBeFalse(); + expect(component.activeTab).toEqual( + ReleaseCoordinatorPageConstants.TAB_ID_BEAM_JOBS + ); - tick(); + tick(); - expect(pbbas.getPromoBarDataAsync).toHaveBeenCalled(); - expect(component.promoBarConfigForm.enabled).toBeTrue(); - })); + expect(pbbas.getPromoBarDataAsync).toHaveBeenCalled(); + expect(component.promoBarConfigForm.enabled).toBeTrue(); + }) + ); - it('should update promo bar parameter and set success status', - fakeAsync(() => { - spyOn(pbbas, 'updatePromoBarDataAsync').and.returnValue( - Promise.resolve()); + it('should update promo bar parameter and set success status', fakeAsync(() => { + spyOn(pbbas, 'updatePromoBarDataAsync').and.returnValue(Promise.resolve()); - component.updatePromoBarParameter(); + component.updatePromoBarParameter(); - expect(component.statusMessage).toEqual( - 'Updating promo-bar platform parameter...'); + expect(component.statusMessage).toEqual( + 'Updating promo-bar platform parameter...' + ); - tick(); + tick(); - expect(pbbas.updatePromoBarDataAsync).toHaveBeenCalled(); - expect(component.statusMessage).toEqual('Success!'); - })); + expect(pbbas.updatePromoBarDataAsync).toHaveBeenCalled(); + expect(component.statusMessage).toEqual('Success!'); + })); - it('should set error status when update promo bar parameter fails', - fakeAsync(() => { - spyOn(pbbas, 'updatePromoBarDataAsync').and.returnValue( - Promise.reject('failed to update')); + it('should set error status when update promo bar parameter fails', fakeAsync(() => { + spyOn(pbbas, 'updatePromoBarDataAsync').and.returnValue( + Promise.reject('failed to update') + ); - component.updatePromoBarParameter(); + component.updatePromoBarParameter(); - expect(component.statusMessage).toEqual( - 'Updating promo-bar platform parameter...'); + expect(component.statusMessage).toEqual( + 'Updating promo-bar platform parameter...' + ); - tick(); + tick(); - expect(pbbas.updatePromoBarDataAsync).toHaveBeenCalled(); - expect(component.statusMessage).toEqual('Server error: failed to update'); - })); + expect(pbbas.updatePromoBarDataAsync).toHaveBeenCalled(); + expect(component.statusMessage).toEqual('Server error: failed to update'); + })); - it('should flush memory cache and set success status', - fakeAsync(() => { - spyOn(rcbas, 'flushMemoryCacheAsync').and.returnValue(Promise.resolve()); + it('should flush memory cache and set success status', fakeAsync(() => { + spyOn(rcbas, 'flushMemoryCacheAsync').and.returnValue(Promise.resolve()); - component.flushMemoryCache(); - tick(); + component.flushMemoryCache(); + tick(); - expect(rcbas.flushMemoryCacheAsync).toHaveBeenCalled(); - expect(component.statusMessage).toEqual('Success! Memory Cache Flushed.'); - expect(component.memoryCacheDataFetched).toBeFalse(); - })); + expect(rcbas.flushMemoryCacheAsync).toHaveBeenCalled(); + expect(component.statusMessage).toEqual('Success! Memory Cache Flushed.'); + expect(component.memoryCacheDataFetched).toBeFalse(); + })); - it('should set error status when failed to flush memory cache', - fakeAsync(() => { - spyOn(rcbas, 'flushMemoryCacheAsync').and.returnValue( - Promise.reject('failed to flush')); + it('should set error status when failed to flush memory cache', fakeAsync(() => { + spyOn(rcbas, 'flushMemoryCacheAsync').and.returnValue( + Promise.reject('failed to flush') + ); - component.flushMemoryCache(); - tick(); + component.flushMemoryCache(); + tick(); - expect(rcbas.flushMemoryCacheAsync).toHaveBeenCalled(); - expect(component.statusMessage).toEqual('Server error: failed to flush'); - })); + expect(rcbas.flushMemoryCacheAsync).toHaveBeenCalled(); + expect(component.statusMessage).toEqual('Server error: failed to flush'); + })); - it('should fetch memory cache profile and set success status', - fakeAsync(() => { - spyOn(rcbas, 'getMemoryCacheProfileAsync').and.returnValue( - Promise.resolve(testMemoryCacheProfile)); + it('should fetch memory cache profile and set success status', fakeAsync(() => { + spyOn(rcbas, 'getMemoryCacheProfileAsync').and.returnValue( + Promise.resolve(testMemoryCacheProfile) + ); - component.getMemoryCacheProfile(); - tick(); + component.getMemoryCacheProfile(); + tick(); - expect(rcbas.getMemoryCacheProfileAsync).toHaveBeenCalled(); - expect(component.memoryCacheProfile.totalAllocatedInBytes) - .toEqual(testMemoryCacheProfile.total_allocation); - expect(component.memoryCacheProfile.peakAllocatedInBytes) - .toEqual(testMemoryCacheProfile.peak_allocation); - expect(component.memoryCacheProfile.totalKeysStored) - .toEqual(testMemoryCacheProfile.total_keys_stored); - expect(component.memoryCacheDataFetched).toBeTrue(); - expect(component.statusMessage).toBe('Success!'); - })); - - it('should set error status when fetching memory cache profile fails', - fakeAsync(() => { - spyOn(rcbas, 'getMemoryCacheProfileAsync').and.returnValue( - Promise.reject('failed to fetch')); + expect(rcbas.getMemoryCacheProfileAsync).toHaveBeenCalled(); + expect(component.memoryCacheProfile.totalAllocatedInBytes).toEqual( + testMemoryCacheProfile.total_allocation + ); + expect(component.memoryCacheProfile.peakAllocatedInBytes).toEqual( + testMemoryCacheProfile.peak_allocation + ); + expect(component.memoryCacheProfile.totalKeysStored).toEqual( + testMemoryCacheProfile.total_keys_stored + ); + expect(component.memoryCacheDataFetched).toBeTrue(); + expect(component.statusMessage).toBe('Success!'); + })); - component.getMemoryCacheProfile(); - tick(); + it('should set error status when fetching memory cache profile fails', fakeAsync(() => { + spyOn(rcbas, 'getMemoryCacheProfileAsync').and.returnValue( + Promise.reject('failed to fetch') + ); + + component.getMemoryCacheProfile(); + tick(); - expect(rcbas.getMemoryCacheProfileAsync).toHaveBeenCalled(); - expect(component.statusMessage).toBe('Server error: failed to fetch'); - })); + expect(rcbas.getMemoryCacheProfileAsync).toHaveBeenCalled(); + expect(component.statusMessage).toBe('Server error: failed to fetch'); + })); }); diff --git a/core/templates/pages/release-coordinator-page/release-coordinator-page.component.ts b/core/templates/pages/release-coordinator-page/release-coordinator-page.component.ts index e478bc12f66c..2d2174b8ef47 100644 --- a/core/templates/pages/release-coordinator-page/release-coordinator-page.component.ts +++ b/core/templates/pages/release-coordinator-page/release-coordinator-page.component.ts @@ -16,15 +16,15 @@ * @fileoverview Component for the release coordinator page. */ -import { Component, OnInit, } from '@angular/core'; -import { FormBuilder, FormGroup } from '@angular/forms'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component, OnInit} from '@angular/core'; +import {FormBuilder, FormGroup} from '@angular/forms'; +import {downgradeComponent} from '@angular/upgrade/static'; -import { PromoBarBackendApiService } from 'services/promo-bar-backend-api.service'; -import { PlatformFeatureService } from 'services/platform-feature.service'; +import {PromoBarBackendApiService} from 'services/promo-bar-backend-api.service'; +import {PlatformFeatureService} from 'services/platform-feature.service'; -import { ReleaseCoordinatorBackendApiService } from './services/release-coordinator-backend-api.service'; -import { ReleaseCoordinatorPageConstants } from './release-coordinator-page.constants'; +import {ReleaseCoordinatorBackendApiService} from './services/release-coordinator-backend-api.service'; +import {ReleaseCoordinatorPageConstants} from './release-coordinator-page.constants'; interface MemoryCacheProfile { totalAllocatedInBytes: string; @@ -32,7 +32,6 @@ interface MemoryCacheProfile { totalKeysStored: string; } - @Component({ selector: 'oppia-release-coordinator-page', templateUrl: './release-coordinator-page.component.html', @@ -56,41 +55,54 @@ export class ReleaseCoordinatorPageComponent implements OnInit { private formBuilder: FormBuilder, private platformFeatureService: PlatformFeatureService, private backendApiService: ReleaseCoordinatorBackendApiService, - private promoBarBackendApiService: PromoBarBackendApiService) {} + private promoBarBackendApiService: PromoBarBackendApiService + ) {} flushMemoryCache(): void { - this.backendApiService.flushMemoryCacheAsync().then(() => { - this.statusMessage = 'Success! Memory Cache Flushed.'; - this.memoryCacheDataFetched = false; - }, errorResponse => { - this.statusMessage = 'Server error: ' + errorResponse; - }); + this.backendApiService.flushMemoryCacheAsync().then( + () => { + this.statusMessage = 'Success! Memory Cache Flushed.'; + this.memoryCacheDataFetched = false; + }, + errorResponse => { + this.statusMessage = 'Server error: ' + errorResponse; + } + ); } getMemoryCacheProfile(): void { - this.backendApiService.getMemoryCacheProfileAsync().then(response => { - this.memoryCacheProfile = { - totalAllocatedInBytes: response.total_allocation, - peakAllocatedInBytes: response.peak_allocation, - totalKeysStored: response.total_keys_stored - }; - this.memoryCacheDataFetched = true; - this.statusMessage = 'Success!'; - }, errorResponse => { - this.statusMessage = 'Server error: ' + errorResponse; - }); + this.backendApiService.getMemoryCacheProfileAsync().then( + response => { + this.memoryCacheProfile = { + totalAllocatedInBytes: response.total_allocation, + peakAllocatedInBytes: response.peak_allocation, + totalKeysStored: response.total_keys_stored, + }; + this.memoryCacheDataFetched = true; + this.statusMessage = 'Success!'; + }, + errorResponse => { + this.statusMessage = 'Server error: ' + errorResponse; + } + ); } updatePromoBarParameter(): void { this.statusMessage = 'Updating promo-bar platform parameter...'; - this.promoBarBackendApiService.updatePromoBarDataAsync( - this.promoBarConfigForm.controls.enabled.value, - this.promoBarConfigForm.controls.message.value).then(() => { - this.statusMessage = 'Success!'; - this.promoBarConfigForm.markAsPristine(); - }, errorResponse => { - this.statusMessage = 'Server error: ' + errorResponse; - }); + this.promoBarBackendApiService + .updatePromoBarDataAsync( + this.promoBarConfigForm.controls.enabled.value, + this.promoBarConfigForm.controls.message.value + ) + .then( + () => { + this.statusMessage = 'Success!'; + this.promoBarConfigForm.markAsPristine(); + }, + errorResponse => { + this.statusMessage = 'Server error: ' + errorResponse; + } + ); } ngOnInit(): void { @@ -98,22 +110,25 @@ export class ReleaseCoordinatorPageComponent implements OnInit { this.submitButtonDisabled = true; this.promoBarConfigForm = this.formBuilder.group({ enabled: false, - message: '' + message: '', }); this.promoBarConfigForm.valueChanges.subscribe(() => { this.submitButtonDisabled = false; }); this.memoryCacheDataFetched = false; this.activeTab = ReleaseCoordinatorPageConstants.TAB_ID_BEAM_JOBS; - this.promoBarBackendApiService.getPromoBarDataAsync().then((promoBar) => { + this.promoBarBackendApiService.getPromoBarDataAsync().then(promoBar => { this.promoBarConfigForm.patchValue({ enabled: promoBar.promoBarEnabled, - message: promoBar.promoBarMessage + message: promoBar.promoBarMessage, }); }); } } -angular.module('oppia').directive( - 'oppiaReleaseCoordinatorPage', downgradeComponent( - {component: ReleaseCoordinatorPageComponent})); +angular + .module('oppia') + .directive( + 'oppiaReleaseCoordinatorPage', + downgradeComponent({component: ReleaseCoordinatorPageComponent}) + ); diff --git a/core/templates/pages/release-coordinator-page/release-coordinator-page.constants.ajs.ts b/core/templates/pages/release-coordinator-page/release-coordinator-page.constants.ajs.ts index e7f533f3ee06..0804d2119cf3 100644 --- a/core/templates/pages/release-coordinator-page/release-coordinator-page.constants.ajs.ts +++ b/core/templates/pages/release-coordinator-page/release-coordinator-page.constants.ajs.ts @@ -18,9 +18,14 @@ // TODO(#7092): Delete this file once migration is complete and these AngularJS // equivalents of the Angular constants are no longer needed. -import { ReleaseCoordinatorPageConstants } from './release-coordinator-page.constants'; +import {ReleaseCoordinatorPageConstants} from './release-coordinator-page.constants'; -angular.module('oppia').constant( - 'TAB_ID_BEAM_JOBS', ReleaseCoordinatorPageConstants.TAB_ID_BEAM_JOBS); -angular.module('oppia').constant( - 'TAB_ID_MISC', ReleaseCoordinatorPageConstants.TAB_ID_MISC); +angular + .module('oppia') + .constant( + 'TAB_ID_BEAM_JOBS', + ReleaseCoordinatorPageConstants.TAB_ID_BEAM_JOBS + ); +angular + .module('oppia') + .constant('TAB_ID_MISC', ReleaseCoordinatorPageConstants.TAB_ID_MISC); diff --git a/core/templates/pages/release-coordinator-page/release-coordinator-page.module.ts b/core/templates/pages/release-coordinator-page/release-coordinator-page.module.ts index 546a97b2108d..4bb897248c5b 100644 --- a/core/templates/pages/release-coordinator-page/release-coordinator-page.module.ts +++ b/core/templates/pages/release-coordinator-page/release-coordinator-page.module.ts @@ -16,35 +16,35 @@ * @fileoverview Module for the release-coordinator page. */ -import { ClipboardModule } from '@angular/cdk/clipboard'; -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { MatAutocompleteModule } from '@angular/material/autocomplete'; -import { MatButtonModule } from '@angular/material/button'; -import { MatCardModule } from '@angular/material/card'; -import { MatDialogModule } from '@angular/material/dialog'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatIconModule } from '@angular/material/icon'; -import { MatInputModule } from '@angular/material/input'; -import { MatProgressBarModule } from '@angular/material/progress-bar'; -import { MatSortModule } from '@angular/material/sort'; -import { MatTabsModule } from '@angular/material/tabs'; -import { MatTableModule } from '@angular/material/table'; -import { MatTooltipModule } from '@angular/material/tooltip'; +import {ClipboardModule} from '@angular/cdk/clipboard'; +import {CommonModule} from '@angular/common'; +import {NgModule} from '@angular/core'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {MatAutocompleteModule} from '@angular/material/autocomplete'; +import {MatButtonModule} from '@angular/material/button'; +import {MatCardModule} from '@angular/material/card'; +import {MatDialogModule} from '@angular/material/dialog'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatIconModule} from '@angular/material/icon'; +import {MatInputModule} from '@angular/material/input'; +import {MatProgressBarModule} from '@angular/material/progress-bar'; +import {MatSortModule} from '@angular/material/sort'; +import {MatTabsModule} from '@angular/material/tabs'; +import {MatTableModule} from '@angular/material/table'; +import {MatTooltipModule} from '@angular/material/tooltip'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { BeamJobsTabComponent } from 'pages/release-coordinator-page/beam-jobs-tab/beam-jobs-tab.component'; -import { FeaturesTabComponent } from 'pages/release-coordinator-page/features-tab/features-tab.component'; -import { CancelBeamJobDialogComponent } from 'pages/release-coordinator-page/components/cancel-beam-job-dialog.component'; -import { StartNewBeamJobDialogComponent } from 'pages/release-coordinator-page/components/start-new-beam-job-dialog.component'; -import { ViewBeamJobOutputDialogComponent } from 'pages/release-coordinator-page/components/view-beam-job-output-dialog.component'; -import { ReleaseCoordinatorNavbarComponent } from 'pages/release-coordinator-page/navbar/release-coordinator-navbar.component'; -import { ReleaseCoordinatorPageComponent } from 'pages/release-coordinator-page/release-coordinator-page.component'; -import { ReleaseCoordinatorPageRootComponent } from './release-coordinator-page-root.component'; -import { ReleaseCoordinatorPageRoutingModule } from './release-coordinator-page-routing.module'; -import { Error404PageModule } from 'pages/error-pages/error-404/error-404-page.module'; -import { SmartRouterModule } from 'hybrid-router-module-provider'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {BeamJobsTabComponent} from 'pages/release-coordinator-page/beam-jobs-tab/beam-jobs-tab.component'; +import {FeaturesTabComponent} from 'pages/release-coordinator-page/features-tab/features-tab.component'; +import {CancelBeamJobDialogComponent} from 'pages/release-coordinator-page/components/cancel-beam-job-dialog.component'; +import {StartNewBeamJobDialogComponent} from 'pages/release-coordinator-page/components/start-new-beam-job-dialog.component'; +import {ViewBeamJobOutputDialogComponent} from 'pages/release-coordinator-page/components/view-beam-job-output-dialog.component'; +import {ReleaseCoordinatorNavbarComponent} from 'pages/release-coordinator-page/navbar/release-coordinator-navbar.component'; +import {ReleaseCoordinatorPageComponent} from 'pages/release-coordinator-page/release-coordinator-page.component'; +import {ReleaseCoordinatorPageRootComponent} from './release-coordinator-page-root.component'; +import {ReleaseCoordinatorPageRoutingModule} from './release-coordinator-page-routing.module'; +import {Error404PageModule} from 'pages/error-pages/error-404/error-404-page.module'; +import {SmartRouterModule} from 'hybrid-router-module-provider'; @NgModule({ imports: [ @@ -69,7 +69,7 @@ import { SmartRouterModule } from 'hybrid-router-module-provider'; // migrated to angular router. SmartRouterModule, ReleaseCoordinatorPageRoutingModule, - Error404PageModule + Error404PageModule, ], declarations: [ BeamJobsTabComponent, @@ -89,6 +89,6 @@ import { SmartRouterModule } from 'hybrid-router-module-provider'; StartNewBeamJobDialogComponent, ViewBeamJobOutputDialogComponent, FeaturesTabComponent, - ] + ], }) export class ReleaseCoordinatorPageModule {} diff --git a/core/templates/pages/release-coordinator-page/services/release-coordinator-backend-api.service.spec.ts b/core/templates/pages/release-coordinator-page/services/release-coordinator-backend-api.service.spec.ts index 67f9879837fc..ae5b4998e8d1 100644 --- a/core/templates/pages/release-coordinator-page/services/release-coordinator-backend-api.service.spec.ts +++ b/core/templates/pages/release-coordinator-page/services/release-coordinator-backend-api.service.spec.ts @@ -16,15 +16,17 @@ * @fileoverview Unit tests for ReleaseCoordinatorBackendApiService. */ -import { HttpClientTestingModule, HttpTestingController } from - '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; - -import { ReleaseCoordinatorBackendApiService } from './release-coordinator-backend-api.service'; -import { CsrfTokenService } from 'services/csrf-token.service'; -import { BeamJobRun } from 'domain/jobs/beam-job-run.model'; -import { BeamJob } from 'domain/jobs/beam-job.model'; -import { BeamJobRunResult } from 'domain/jobs/beam-job-run-result.model'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; + +import {ReleaseCoordinatorBackendApiService} from './release-coordinator-backend-api.service'; +import {CsrfTokenService} from 'services/csrf-token.service'; +import {BeamJobRun} from 'domain/jobs/beam-job-run.model'; +import {BeamJob} from 'domain/jobs/beam-job.model'; +import {BeamJobRunResult} from 'domain/jobs/beam-job-run-result.model'; describe('Release coordinator backend api service', () => { let rcbas: ReleaseCoordinatorBackendApiService; @@ -35,7 +37,7 @@ describe('Release coordinator backend api service', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] + imports: [HttpClientTestingModule], }); rcbas = TestBed.get(ReleaseCoordinatorBackendApiService); @@ -44,7 +46,7 @@ describe('Release coordinator backend api service', () => { successHandler = jasmine.createSpy('success'); failHandler = jasmine.createSpy('fail'); - spyOn(csrfService, 'getTokenAsync').and.callFake(async() => { + spyOn(csrfService, 'getTokenAsync').and.callFake(async () => { return Promise.resolve('sample-csrf-token'); }); }); @@ -53,89 +55,96 @@ describe('Release coordinator backend api service', () => { httpTestingController.verify(); }); - it('should flush the memory cache when calling' + - 'flushMemoryCacheAsync', fakeAsync(() => { - rcbas.flushMemoryCacheAsync() - .then(successHandler, failHandler); - - let req = httpTestingController.expectOne( - '/memorycachehandler'); - expect(req.request.method).toEqual('DELETE'); - req.flush(200); - flushMicrotasks(); - - expect(successHandler).toHaveBeenCalled(); - expect(failHandler).not.toHaveBeenCalled(); - })); - - it('should fail to flush the memory cache when calling' + - 'flushMemoryCacheAsync', fakeAsync(() => { - rcbas.flushMemoryCacheAsync() - .then(successHandler, failHandler); - - let req = httpTestingController.expectOne( - '/memorycachehandler'); - expect(req.request.method).toEqual('DELETE'); - req.flush({ - error: 'Failed to flush memory cache.' - }, { - status: 500, statusText: 'Internal Server Error' - }); - flushMicrotasks(); - - expect(successHandler).not.toHaveBeenCalled(); - expect(failHandler).toHaveBeenCalledWith('Failed to flush memory cache.'); - })); - - it('should get the data of memory cache profile when' + - 'calling getMemoryCacheProfileAsync', fakeAsync(() => { - rcbas.getMemoryCacheProfileAsync() - .then(successHandler, failHandler); - let req = httpTestingController.expectOne( - '/memorycachehandler'); - expect(req.request.method).toEqual('GET'); - req.flush(200); - flushMicrotasks(); - - expect(successHandler).toHaveBeenCalled(); - expect(failHandler).not.toHaveBeenCalled(); - })); - - it('should fail to get the data of memory cache profile' + - 'when calling getMemoryCacheProfileAsync', fakeAsync(() => { - rcbas.getMemoryCacheProfileAsync() - .then(successHandler, failHandler); - let req = httpTestingController.expectOne( - '/memorycachehandler'); - expect(req.request.method).toEqual('GET'); - req.flush({ - error: 'Failed to get data.' - }, { - status: 500, statusText: 'Internal Server Error' - }); - flushMicrotasks(); - - expect(successHandler).not.toHaveBeenCalled(); - expect(failHandler).toHaveBeenCalledWith('Failed to get data.'); - })); - - it('should get all beam jobs', fakeAsync(async() => { + it( + 'should flush the memory cache when calling' + 'flushMemoryCacheAsync', + fakeAsync(() => { + rcbas.flushMemoryCacheAsync().then(successHandler, failHandler); + + let req = httpTestingController.expectOne('/memorycachehandler'); + expect(req.request.method).toEqual('DELETE'); + req.flush(200); + flushMicrotasks(); + + expect(successHandler).toHaveBeenCalled(); + expect(failHandler).not.toHaveBeenCalled(); + }) + ); + + it( + 'should fail to flush the memory cache when calling' + + 'flushMemoryCacheAsync', + fakeAsync(() => { + rcbas.flushMemoryCacheAsync().then(successHandler, failHandler); + + let req = httpTestingController.expectOne('/memorycachehandler'); + expect(req.request.method).toEqual('DELETE'); + req.flush( + { + error: 'Failed to flush memory cache.', + }, + { + status: 500, + statusText: 'Internal Server Error', + } + ); + flushMicrotasks(); + + expect(successHandler).not.toHaveBeenCalled(); + expect(failHandler).toHaveBeenCalledWith('Failed to flush memory cache.'); + }) + ); + + it( + 'should get the data of memory cache profile when' + + 'calling getMemoryCacheProfileAsync', + fakeAsync(() => { + rcbas.getMemoryCacheProfileAsync().then(successHandler, failHandler); + let req = httpTestingController.expectOne('/memorycachehandler'); + expect(req.request.method).toEqual('GET'); + req.flush(200); + flushMicrotasks(); + + expect(successHandler).toHaveBeenCalled(); + expect(failHandler).not.toHaveBeenCalled(); + }) + ); + + it( + 'should fail to get the data of memory cache profile' + + 'when calling getMemoryCacheProfileAsync', + fakeAsync(() => { + rcbas.getMemoryCacheProfileAsync().then(successHandler, failHandler); + let req = httpTestingController.expectOne('/memorycachehandler'); + expect(req.request.method).toEqual('GET'); + req.flush( + { + error: 'Failed to get data.', + }, + { + status: 500, + statusText: 'Internal Server Error', + } + ); + flushMicrotasks(); + + expect(successHandler).not.toHaveBeenCalled(); + expect(failHandler).toHaveBeenCalledWith('Failed to get data.'); + }) + ); + + it('should get all beam jobs', fakeAsync(async () => { const beamJobsPromise = rcbas.getBeamJobs().toPromise(); const req = httpTestingController.expectOne('/beam_job'); expect(req.request.method).toEqual('GET'); req.flush({ - jobs: [ - { name: 'FooJob' }, - ], + jobs: [{name: 'FooJob'}], }); flushMicrotasks(); - expect(await beamJobsPromise).toEqual([ - new BeamJob('FooJob'), - ]); + expect(await beamJobsPromise).toEqual([new BeamJob('FooJob')]); })); - it('should get all beam job runs', fakeAsync(async() => { + it('should get all beam job runs', fakeAsync(async () => { const beamJobRunsPromise = rcbas.getBeamJobRuns().toPromise(); const req = httpTestingController.expectOne('/beam_job_run'); expect(req.request.method).toEqual('GET'); @@ -149,7 +158,7 @@ describe('Release coordinator backend api service', () => { job_updated_on_msecs: 0, job_is_synchronous: false, }, - ] + ], }); flushMicrotasks(); @@ -158,7 +167,7 @@ describe('Release coordinator backend api service', () => { ]); })); - it('should start a new job', fakeAsync(async() => { + it('should start a new job', fakeAsync(async () => { const beamJob = new BeamJob('FooJob'); const beamJobRunPromise = rcbas.startNewBeamJob(beamJob).toPromise(); const req = httpTestingController.expectOne('/beam_job_run'); @@ -175,12 +184,12 @@ describe('Release coordinator backend api service', () => { flushMicrotasks(); expect(await beamJobRunPromise).toEqual( - new BeamJobRun('abc', 'FooJob', 'RUNNING', 0, 0, false)); + new BeamJobRun('abc', 'FooJob', 'RUNNING', 0, 0, false) + ); })); - it('should cancel a running beam job', fakeAsync(async() => { - const beamJobRun = ( - new BeamJobRun('abc', 'FooJob', 'RUNNING', 0, 0, false)); + it('should cancel a running beam job', fakeAsync(async () => { + const beamJobRun = new BeamJobRun('abc', 'FooJob', 'RUNNING', 0, 0, false); const beamJobRunPromise = rcbas.cancelBeamJobRun(beamJobRun).toPromise(); const req = httpTestingController.expectOne('/beam_job_run?job_id=abc'); expect(req.request.method).toEqual('DELETE'); @@ -195,15 +204,16 @@ describe('Release coordinator backend api service', () => { flushMicrotasks(); expect(await beamJobRunPromise).toEqual( - new BeamJobRun('abc', 'FooJob', 'CANCELLING', 0, 0, false)); + new BeamJobRun('abc', 'FooJob', 'CANCELLING', 0, 0, false) + ); })); - it('should get the output of a beam job run', fakeAsync(async() => { - const beamJobRun = ( - new BeamJobRun('abc', 'FooJob', 'DONE', 0, 0, false)); + it('should get the output of a beam job run', fakeAsync(async () => { + const beamJobRun = new BeamJobRun('abc', 'FooJob', 'DONE', 0, 0, false); const resultPromise = rcbas.getBeamJobRunOutput(beamJobRun).toPromise(); - const req = ( - httpTestingController.expectOne('/beam_job_run_result?job_id=abc')); + const req = httpTestingController.expectOne( + '/beam_job_run_result?job_id=abc' + ); expect(req.request.method).toEqual('GET'); req.flush({ stdout: '123', diff --git a/core/templates/pages/release-coordinator-page/services/release-coordinator-backend-api.service.ts b/core/templates/pages/release-coordinator-page/services/release-coordinator-backend-api.service.ts index 36b77c3399eb..7b6d2258e1d1 100644 --- a/core/templates/pages/release-coordinator-page/services/release-coordinator-backend-api.service.ts +++ b/core/templates/pages/release-coordinator-page/services/release-coordinator-backend-api.service.ts @@ -16,89 +16,114 @@ * @fileoverview Service that manages release coordinator's backend api calls. */ -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Observable} from 'rxjs'; +import {map} from 'rxjs/operators'; -import { BeamJobRunResult, BeamJobRunResultBackendDict } from 'domain/jobs/beam-job-run-result.model'; -import { BeamJobRun, BeamJobRunBackendDict } from 'domain/jobs/beam-job-run.model'; -import { BeamJob, BeamJobBackendDict } from 'domain/jobs/beam-job.model'; +import { + BeamJobRunResult, + BeamJobRunResultBackendDict, +} from 'domain/jobs/beam-job-run-result.model'; +import { + BeamJobRun, + BeamJobRunBackendDict, +} from 'domain/jobs/beam-job-run.model'; +import {BeamJob, BeamJobBackendDict} from 'domain/jobs/beam-job.model'; interface MemoryCacheProfileResponse { - 'peak_allocation': string; - 'total_allocation': string; - 'total_keys_stored': string; + peak_allocation: string; + total_allocation: string; + total_keys_stored: string; } interface BeamJobsResponse { - 'jobs': BeamJobBackendDict[]; + jobs: BeamJobBackendDict[]; } interface BeamJobRunsResponse { - 'runs': BeamJobRunBackendDict[]; + runs: BeamJobRunBackendDict[]; } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ReleaseCoordinatorBackendApiService { constructor(private http: HttpClient) {} async getMemoryCacheProfileAsync(): Promise { return new Promise((resolve, reject) => { - this.http.get( - '/memorycachehandler').toPromise().then(response => { - resolve(response); - }, errorResponse => { - reject(errorResponse.error.error); - }); + this.http + .get('/memorycachehandler') + .toPromise() + .then( + response => { + resolve(response); + }, + errorResponse => { + reject(errorResponse.error.error); + } + ); }); } async flushMemoryCacheAsync(): Promise { return new Promise((resolve, reject) => { - this.http.delete( - '/memorycachehandler').toPromise().then(response => { - resolve(response); - }, errorResponse => { - reject(errorResponse.error.error); - }); + this.http + .delete('/memorycachehandler') + .toPromise() + .then( + response => { + resolve(response); + }, + errorResponse => { + reject(errorResponse.error.error); + } + ); }); } getBeamJobs(): Observable { - return this.http.get('/beam_job').pipe( - map(r => r.jobs.map(BeamJob.createFromBackendDict)) - ); + return this.http + .get('/beam_job') + .pipe(map(r => r.jobs.map(BeamJob.createFromBackendDict))); } getBeamJobRuns(): Observable { - return this.http.get('/beam_job_run').pipe( - map(r => r.runs.map(BeamJobRun.createFromBackendDict)) - ); + return this.http + .get('/beam_job_run') + .pipe(map(r => r.runs.map(BeamJobRun.createFromBackendDict))); } startNewBeamJob(beamJob: BeamJob): Observable { - return this.http.put('/beam_job_run', { - job_name: beamJob.name - }).pipe(map(BeamJobRun.createFromBackendDict)); + return this.http + .put('/beam_job_run', { + job_name: beamJob.name, + }) + .pipe(map(BeamJobRun.createFromBackendDict)); } cancelBeamJobRun(beamJobRun: BeamJobRun): Observable { - return this.http.delete('/beam_job_run', { - params: { job_id: beamJobRun.jobId } - }).pipe(map(BeamJobRun.createFromBackendDict)); + return this.http + .delete('/beam_job_run', { + params: {job_id: beamJobRun.jobId}, + }) + .pipe(map(BeamJobRun.createFromBackendDict)); } getBeamJobRunOutput(beamJobRun: BeamJobRun): Observable { - return this.http.get('/beam_job_run_result', { - params: { job_id: beamJobRun.jobId } - }).pipe(map(BeamJobRunResult.createFromBackendDict)); + return this.http + .get('/beam_job_run_result', { + params: {job_id: beamJobRun.jobId}, + }) + .pipe(map(BeamJobRunResult.createFromBackendDict)); } } -angular.module('oppia').factory( - 'ReleaseCoordinatorBackendApiService', - downgradeInjectable(ReleaseCoordinatorBackendApiService)); +angular + .module('oppia') + .factory( + 'ReleaseCoordinatorBackendApiService', + downgradeInjectable(ReleaseCoordinatorBackendApiService) + ); diff --git a/core/templates/pages/review-test-page/review-test-engine.service.spec.ts b/core/templates/pages/review-test-page/review-test-engine.service.spec.ts index f03d8a546500..c5473eabd7e8 100644 --- a/core/templates/pages/review-test-page/review-test-engine.service.spec.ts +++ b/core/templates/pages/review-test-page/review-test-engine.service.spec.ts @@ -16,17 +16,16 @@ * @fileoverview Unit tests for the review tests. */ -import { ReviewTestEngineService } from - 'pages/review-test-page/review-test-engine.service'; +import {ReviewTestEngineService} from 'pages/review-test-page/review-test-engine.service'; describe('Review test engine service', () => { let rtes: ReviewTestEngineService; beforeEach(() => { - rtes = new ReviewTestEngineService; + rtes = new ReviewTestEngineService(); }); - it('should return the correct count of review test questions', function() { + it('should return the correct count of review test questions', function () { expect(rtes.getReviewTestQuestionCount(-2)).toEqual(0); expect(rtes.getReviewTestQuestionCount(0)).toEqual(0); expect(rtes.getReviewTestQuestionCount(3)).toEqual(9); diff --git a/core/templates/pages/review-test-page/review-test-engine.service.ts b/core/templates/pages/review-test-page/review-test-engine.service.ts index 4c16026e6e9f..570035e66e8c 100644 --- a/core/templates/pages/review-test-page/review-test-engine.service.ts +++ b/core/templates/pages/review-test-page/review-test-engine.service.ts @@ -16,11 +16,11 @@ * @fileoverview Utility service for the review tests. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ReviewTestEngineService { getReviewTestQuestionCount(numOfSkills: number): number { @@ -37,5 +37,9 @@ export class ReviewTestEngineService { } } -angular.module('oppia').factory( - 'ReviewTestEngineService', downgradeInjectable(ReviewTestEngineService)); +angular + .module('oppia') + .factory( + 'ReviewTestEngineService', + downgradeInjectable(ReviewTestEngineService) + ); diff --git a/core/templates/pages/review-test-page/review-test-page.component.spec.ts b/core/templates/pages/review-test-page/review-test-page.component.spec.ts index 153f07214f69..28ec002ce27f 100644 --- a/core/templates/pages/review-test-page/review-test-page.component.spec.ts +++ b/core/templates/pages/review-test-page/review-test-page.component.spec.ts @@ -16,18 +16,29 @@ * @fileoverview Unit tests for reviewTestPage. */ -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { TranslateModule, TranslateLoader, TranslateFakeLoader, TranslateService } from '@ngx-translate/core'; -import { ReviewTestBackendApiService } from 'domain/review_test/review-test-backend-api.service'; -import { PageTitleService } from 'services/page-title.service'; -import { ReviewTestPageComponent } from './review-test-page.component'; -import { UrlService } from 'services/contextual/url.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { PracticeSessionsBackendApiService } from 'pages/practice-session-page/practice-session-backend-api.service'; -import { CsrfTokenService } from 'services/csrf-token.service'; -import { ReviewTest } from 'domain/review_test/review-test.model'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + TranslateModule, + TranslateLoader, + TranslateFakeLoader, + TranslateService, +} from '@ngx-translate/core'; +import {ReviewTestBackendApiService} from 'domain/review_test/review-test-backend-api.service'; +import {PageTitleService} from 'services/page-title.service'; +import {ReviewTestPageComponent} from './review-test-page.component'; +import {UrlService} from 'services/contextual/url.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {PracticeSessionsBackendApiService} from 'pages/practice-session-page/practice-session-backend-api.service'; +import {CsrfTokenService} from 'services/csrf-token.service'; +import {ReviewTest} from 'domain/review_test/review-test.model'; describe('Review test page component', () => { let component: ReviewTestPageComponent; @@ -45,13 +56,11 @@ describe('Review test page component', () => { TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: TranslateFakeLoader - } - }) - ], - declarations: [ - ReviewTestPageComponent, + useClass: TranslateFakeLoader, + }, + }), ], + declarations: [ReviewTestPageComponent], providers: [ TranslateService, PracticeSessionsBackendApiService, @@ -60,11 +69,10 @@ describe('Review test page component', () => { UrlService, I18nLanguageCodeService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); - beforeEach(() => { fixture = TestBed.createComponent(ReviewTestPageComponent); component = fixture.componentInstance; @@ -76,57 +84,68 @@ describe('Review test page component', () => { urlService = TestBed.inject(UrlService); spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue( - 'topic_1'); + 'topic_1' + ); spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl').and.returnValue( - 'classroom_1'); + 'classroom_1' + ); spyOn( - reviewTestBackendApiService, 'fetchReviewTestDataAsync').and.returnValue( - Promise.resolve( - new ReviewTest( - '', {skill_1: 'skill_1'}) - )); + reviewTestBackendApiService, + 'fetchReviewTestDataAsync' + ).and.returnValue( + Promise.resolve(new ReviewTest('', {skill_1: 'skill_1'})) + ); spyOn(translateService, 'use').and.stub(); spyOn(translateService, 'instant').and.returnValue('translatedTitle'); }); - it('should initialize correctly controller properties after its' + - ' initialization and get skill details from backend', fakeAsync(() => { - spyOn(urlService, 'getStoryUrlFragmentFromLearnerUrl').and.returnValue( - 'story_1'); - spyOn(component, 'subscribeToOnLanguageCodeChange'); - - component.ngOnInit(); - tick(); - - expect(component.subscribeToOnLanguageCodeChange).toHaveBeenCalled(); - expect(component.questionPlayerConfig).toEqual({ - resultActionButtons: [{ - type: 'REVIEW_LOWEST_SCORED_SKILL', - i18nId: 'I18N_QUESTION_PLAYER_REVIEW_LOWEST_SCORED_SKILL' - }, { - type: 'RETRY_SESSION', - i18nId: 'I18N_QUESTION_PLAYER_RETRY_TEST', - url: '/learn/classroom_1/topic_1/review-test/story_1' - }, { - type: 'DASHBOARD', - i18nId: 'I18N_QUESTION_PLAYER_RETURN_TO_STORY', - url: '/learn/classroom_1/topic_1/story/story_1' - }], - skillList: ['skill_1'], - skillDescriptions: ['skill_1'], - questionCount: 3, - questionPlayerMode: { - modeType: 'PASS_FAIL', - passCutoff: 0.75 - }, - questionsSortedByDifficulty: true - }); - })); + it( + 'should initialize correctly controller properties after its' + + ' initialization and get skill details from backend', + fakeAsync(() => { + spyOn(urlService, 'getStoryUrlFragmentFromLearnerUrl').and.returnValue( + 'story_1' + ); + spyOn(component, 'subscribeToOnLanguageCodeChange'); + + component.ngOnInit(); + tick(); + + expect(component.subscribeToOnLanguageCodeChange).toHaveBeenCalled(); + expect(component.questionPlayerConfig).toEqual({ + resultActionButtons: [ + { + type: 'REVIEW_LOWEST_SCORED_SKILL', + i18nId: 'I18N_QUESTION_PLAYER_REVIEW_LOWEST_SCORED_SKILL', + }, + { + type: 'RETRY_SESSION', + i18nId: 'I18N_QUESTION_PLAYER_RETRY_TEST', + url: '/learn/classroom_1/topic_1/review-test/story_1', + }, + { + type: 'DASHBOARD', + i18nId: 'I18N_QUESTION_PLAYER_RETURN_TO_STORY', + url: '/learn/classroom_1/topic_1/story/story_1', + }, + ], + skillList: ['skill_1'], + skillDescriptions: ['skill_1'], + questionCount: 3, + questionPlayerMode: { + modeType: 'PASS_FAIL', + passCutoff: 0.75, + }, + questionsSortedByDifficulty: true, + }); + }) + ); it('should throw error if story url is null', fakeAsync(() => { spyOn(urlService, 'getStoryUrlFragmentFromLearnerUrl').and.returnValue( - null); + null + ); expect(() => { component.ngOnInit(); tick(); @@ -135,20 +154,23 @@ describe('Review test page component', () => { it('should subscribe to onLanguageCodeChange', () => { spyOn(urlService, 'getStoryUrlFragmentFromLearnerUrl').and.returnValue( - 'story_1'); + 'story_1' + ); spyOn(component.directiveSubscriptions, 'add'); spyOn(i18nLanguageCodeService.onI18nLanguageCodeChange, 'subscribe'); component.subscribeToOnLanguageCodeChange(); expect(component.directiveSubscriptions.add).toHaveBeenCalled(); - expect(i18nLanguageCodeService.onI18nLanguageCodeChange.subscribe) - .toHaveBeenCalled(); + expect( + i18nLanguageCodeService.onI18nLanguageCodeChange.subscribe + ).toHaveBeenCalled(); }); it('should update title whenever the language changes', () => { spyOn(urlService, 'getStoryUrlFragmentFromLearnerUrl').and.returnValue( - 'story_1'); + 'story_1' + ); component.subscribeToOnLanguageCodeChange(); spyOn(component, 'setPageTitle'); @@ -159,19 +181,22 @@ describe('Review test page component', () => { it('should obtain translated title and set it', () => { spyOn(urlService, 'getStoryUrlFragmentFromLearnerUrl').and.returnValue( - 'story_1'); + 'story_1' + ); spyOn(pageTitleService, 'setDocumentTitle'); component.storyName = 'dummy_story_name'; component.setPageTitle(); - expect(pageTitleService.setDocumentTitle) - .toHaveBeenCalledWith('translatedTitle'); + expect(pageTitleService.setDocumentTitle).toHaveBeenCalledWith( + 'translatedTitle' + ); }); it('should unsubscribe on component destruction', () => { spyOn(urlService, 'getStoryUrlFragmentFromLearnerUrl').and.returnValue( - 'story_1'); + 'story_1' + ); spyOn(component.directiveSubscriptions, 'unsubscribe'); component.ngOnDestroy(); diff --git a/core/templates/pages/review-test-page/review-test-page.component.ts b/core/templates/pages/review-test-page/review-test-page.component.ts index 088d1004eda5..f21301842b6c 100644 --- a/core/templates/pages/review-test-page/review-test-page.component.ts +++ b/core/templates/pages/review-test-page/review-test-page.component.ts @@ -16,23 +16,23 @@ * @fileoverview Component for the review tests page. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; -import { ReviewTestBackendApiService } from 'domain/review_test/review-test-backend-api.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { PageTitleService } from 'services/page-title.service'; -import { QuestionPlayerConstants } from 'components/question-directives/question-player/question-player.constants'; -import { ReviewTestPageConstants } from './review-test-page.constants'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { UrlService } from 'services/contextual/url.service'; -import { ReviewTestEngineService } from './review-test-engine.service'; -import { QuestionPlayerConfig } from 'pages/exploration-player-page/learner-experience/ratings-and-recommendations.component'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; +import {ReviewTestBackendApiService} from 'domain/review_test/review-test-backend-api.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {PageTitleService} from 'services/page-title.service'; +import {QuestionPlayerConstants} from 'components/question-directives/question-player/question-player.constants'; +import {ReviewTestPageConstants} from './review-test-page.constants'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {UrlService} from 'services/contextual/url.service'; +import {ReviewTestEngineService} from './review-test-engine.service'; +import {QuestionPlayerConfig} from 'pages/exploration-player-page/learner-experience/ratings-and-recommendations.component'; @Component({ selector: 'review-test-page', - templateUrl: './review-test-page.component.html' + templateUrl: './review-test-page.component.html', }) export class ReviewTestPageComponent implements OnInit, OnDestroy { directiveSubscriptions = new Subscription(); @@ -53,30 +53,34 @@ export class ReviewTestPageComponent implements OnInit, OnDestroy { ) {} _fetchSkillDetails(): void { - const topicUrlFragment = ( - this.urlService.getTopicUrlFragmentFromLearnerUrl()); - const storyUrlFragment = ( - this.urlService.getStoryUrlFragmentFromLearnerUrl()); + const topicUrlFragment = + this.urlService.getTopicUrlFragmentFromLearnerUrl(); + const storyUrlFragment = + this.urlService.getStoryUrlFragmentFromLearnerUrl(); if (storyUrlFragment === null) { throw new Error('Story url fragment cannot be null.'); } - const classroomUrlFragment = ( - this.urlService.getClassroomUrlFragmentFromLearnerUrl()); + const classroomUrlFragment = + this.urlService.getClassroomUrlFragmentFromLearnerUrl(); const reviewTestsUrl = this.urlInterpolationService.interpolateUrl( - ReviewTestPageConstants.REVIEW_TESTS_URL, { + ReviewTestPageConstants.REVIEW_TESTS_URL, + { topic_url_fragment: topicUrlFragment, classroom_url_fragment: classroomUrlFragment, - story_url_fragment: storyUrlFragment - }); + story_url_fragment: storyUrlFragment, + } + ); const storyViewerUrl = this.urlInterpolationService.interpolateUrl( - ReviewTestPageConstants.STORY_VIEWER_PAGE, { + ReviewTestPageConstants.STORY_VIEWER_PAGE, + { topic_url_fragment: topicUrlFragment, classroom_url_fragment: classroomUrlFragment, - story_url_fragment: storyUrlFragment - }); - this.reviewTestBackendApiService.fetchReviewTestDataAsync( - storyUrlFragment).then( - (result) => { + story_url_fragment: storyUrlFragment, + } + ); + this.reviewTestBackendApiService + .fetchReviewTestDataAsync(storyUrlFragment) + .then(result => { const skillIdList = []; const skillDescriptions = []; this.storyName = result.storyName; @@ -84,48 +88,51 @@ export class ReviewTestPageComponent implements OnInit, OnDestroy { this.subscribeToOnLanguageCodeChange(); for (let skillId in result.skillDescriptions) { skillIdList.push(skillId); - skillDescriptions.push( - result.skillDescriptions[skillId]); + skillDescriptions.push(result.skillDescriptions[skillId]); } this.questionPlayerConfig = { resultActionButtons: [ { type: 'REVIEW_LOWEST_SCORED_SKILL', - i18nId: 'I18N_QUESTION_PLAYER_REVIEW_LOWEST_SCORED_SKILL' + i18nId: 'I18N_QUESTION_PLAYER_REVIEW_LOWEST_SCORED_SKILL', }, { type: 'RETRY_SESSION', i18nId: 'I18N_QUESTION_PLAYER_RETRY_TEST', - url: reviewTestsUrl + url: reviewTestsUrl, }, { type: 'DASHBOARD', i18nId: 'I18N_QUESTION_PLAYER_RETURN_TO_STORY', - url: storyViewerUrl - } + url: storyViewerUrl, + }, ], skillList: skillIdList, skillDescriptions: skillDescriptions, - questionCount: ( - this.reviewTestEngineService - .getReviewTestQuestionCount(skillIdList.length)), + questionCount: + this.reviewTestEngineService.getReviewTestQuestionCount( + skillIdList.length + ), questionPlayerMode: { - modeType: ( - QuestionPlayerConstants.QUESTION_PLAYER_MODE.PASS_FAIL_MODE), - passCutoff: 0.75 + modeType: + QuestionPlayerConstants.QUESTION_PLAYER_MODE.PASS_FAIL_MODE, + passCutoff: 0.75, }, - questionsSortedByDifficulty: true + questionsSortedByDifficulty: true, }; }); } setPageTitle(): void { this.translateService.use( - this.i18nLanguageCodeService.getCurrentI18nLanguageCode()); + this.i18nLanguageCodeService.getCurrentI18nLanguageCode() + ); const translatedTitle = this.translateService.instant( - 'I18N_REVIEW_TEST_PAGE_TITLE', { - storyName: this.storyName - }); + 'I18N_REVIEW_TEST_PAGE_TITLE', + { + storyName: this.storyName, + } + ); this.pageTitleService.setDocumentTitle(translatedTitle); } @@ -146,7 +153,9 @@ export class ReviewTestPageComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive('reviewTestPage', +angular.module('oppia').directive( + 'reviewTestPage', downgradeComponent({ - component: ReviewTestPageComponent - }) as angular.IDirectiveFactory); + component: ReviewTestPageComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/review-test-page/review-test-page.constants.ajs.ts b/core/templates/pages/review-test-page/review-test-page.constants.ajs.ts index 75a23b10f102..0371dfe93f2f 100644 --- a/core/templates/pages/review-test-page/review-test-page.constants.ajs.ts +++ b/core/templates/pages/review-test-page/review-test-page.constants.ajs.ts @@ -18,13 +18,18 @@ // TODO(#7092): Delete this file once migration is complete and these AngularJS // equivalents of the Angular constants are no longer needed. -import { ReviewTestPageConstants } from - 'pages/review-test-page/review-test-page.constants'; +import {ReviewTestPageConstants} from 'pages/review-test-page/review-test-page.constants'; -angular.module('oppia').constant( - 'REVIEW_TEST_DATA_URL', ReviewTestPageConstants.REVIEW_TEST_DATA_URL); -angular.module('oppia').constant( - 'REVIEW_TESTS_URL', ReviewTestPageConstants.REVIEW_TESTS_URL); +angular + .module('oppia') + .constant( + 'REVIEW_TEST_DATA_URL', + ReviewTestPageConstants.REVIEW_TEST_DATA_URL + ); +angular + .module('oppia') + .constant('REVIEW_TESTS_URL', ReviewTestPageConstants.REVIEW_TESTS_URL); -angular.module('oppia').constant( - 'STORY_VIEWER_PAGE', ReviewTestPageConstants.STORY_VIEWER_PAGE); +angular + .module('oppia') + .constant('STORY_VIEWER_PAGE', ReviewTestPageConstants.STORY_VIEWER_PAGE); diff --git a/core/templates/pages/review-test-page/review-test-page.constants.ts b/core/templates/pages/review-test-page/review-test-page.constants.ts index 3d0c075987b2..8cd2ff9ebc1b 100644 --- a/core/templates/pages/review-test-page/review-test-page.constants.ts +++ b/core/templates/pages/review-test-page/review-test-page.constants.ts @@ -17,15 +17,15 @@ */ export const ReviewTestPageConstants = { - REVIEW_TEST_DATA_URL: ( + REVIEW_TEST_DATA_URL: '/review_test_handler/data//' + - '/'), + '/', - REVIEW_TESTS_URL: ( + REVIEW_TESTS_URL: '/learn///' + - 'review-test/'), + 'review-test/', - STORY_VIEWER_PAGE: ( + STORY_VIEWER_PAGE: '/learn///' + - 'story/') + 'story/', } as const; diff --git a/core/templates/pages/review-test-page/review-test-page.import.ts b/core/templates/pages/review-test-page/review-test-page.import.ts index 32240a2ed7ab..414a640021b2 100644 --- a/core/templates/pages/review-test-page/review-test-page.import.ts +++ b/core/templates/pages/review-test-page/review-test-page.import.ts @@ -23,9 +23,15 @@ import uiValidate from 'angular-ui-validate'; import 'third-party-imports/ui-tree.import'; angular.module('oppia', [ - require('angular-cookies'), 'ngAnimate', - 'ngMaterial', 'ngSanitize', 'ngTouch', 'pascalprecht.translate', - 'ui.bootstrap', 'ui.tree', uiValidate + require('angular-cookies'), + 'ngAnimate', + 'ngMaterial', + 'ngSanitize', + 'ngTouch', + 'pascalprecht.translate', + 'ui.bootstrap', + 'ui.tree', + uiValidate, ]); require('Polyfills.ts'); diff --git a/core/templates/pages/review-test-page/review-test-page.module.ts b/core/templates/pages/review-test-page/review-test-page.module.ts index 5e23383e6511..cf415b732049 100644 --- a/core/templates/pages/review-test-page/review-test-page.module.ts +++ b/core/templates/pages/review-test-page/review-test-page.module.ts @@ -16,24 +16,25 @@ * @fileoverview Module for the review tests page. */ -import { APP_INITIALIZER, NgModule, StaticProvider } from '@angular/core'; -import { BrowserModule, HAMMER_GESTURE_CONFIG } from '@angular/platform-browser'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { HttpClientModule } from '@angular/common/http'; -import { HTTP_INTERCEPTORS } from '@angular/common/http'; -import { RouterModule } from '@angular/router'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import {APP_INITIALIZER, NgModule, StaticProvider} from '@angular/core'; +import {BrowserModule, HAMMER_GESTURE_CONFIG} from '@angular/platform-browser'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {HttpClientModule} from '@angular/common/http'; +import {HTTP_INTERCEPTORS} from '@angular/common/http'; +import {RouterModule} from '@angular/router'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; -import { InteractionExtensionsModule } from 'interactions/interactions.module'; -import { RequestInterceptor } from 'services/request-interceptor.service'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { OppiaAngularRootComponent } from - 'components/oppia-angular-root.component'; -import { platformFeatureInitFactory, PlatformFeatureService } from - 'services/platform-feature.service'; -import { MyHammerConfig, toastrConfig } from 'pages/oppia-root/app.module'; -import { SmartRouterModule } from 'hybrid-router-module-provider'; -import { AppErrorHandlerProvider } from 'pages/oppia-root/app-error-handler'; +import {InteractionExtensionsModule} from 'interactions/interactions.module'; +import {RequestInterceptor} from 'services/request-interceptor.service'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {OppiaAngularRootComponent} from 'components/oppia-angular-root.component'; +import { + platformFeatureInitFactory, + PlatformFeatureService, +} from 'services/platform-feature.service'; +import {MyHammerConfig, toastrConfig} from 'pages/oppia-root/app.module'; +import {SmartRouterModule} from 'hybrid-router-module-provider'; +import {AppErrorHandlerProvider} from 'pages/oppia-root/app-error-handler'; @NgModule({ imports: [ @@ -46,37 +47,37 @@ import { AppErrorHandlerProvider } from 'pages/oppia-root/app-error-handler'; // migrated to angular router. SmartRouterModule, RouterModule.forRoot([]), - ToastrModule.forRoot(toastrConfig) + ToastrModule.forRoot(toastrConfig), ], providers: [ { provide: HTTP_INTERCEPTORS, useClass: RequestInterceptor, - multi: true + multi: true, }, { provide: APP_INITIALIZER, useFactory: platformFeatureInitFactory, deps: [PlatformFeatureService], - multi: true + multi: true, }, { provide: HAMMER_GESTURE_CONFIG, - useClass: MyHammerConfig + useClass: MyHammerConfig, }, AppErrorHandlerProvider, - ] + ], }) class ReviewTestPageModule { // Empty placeholder method to satisfy the `Compiler`. ngDoBootstrap() {} } -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { downgradeModule } from '@angular/upgrade/static'; -import { ToastrModule } from 'ngx-toastr'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {downgradeModule} from '@angular/upgrade/static'; +import {ToastrModule} from 'ngx-toastr'; -const bootstrapFnAsync = async(extraProviders: StaticProvider[]) => { +const bootstrapFnAsync = async (extraProviders: StaticProvider[]) => { const platformRef = platformBrowserDynamic(extraProviders); return platformRef.bootstrapModule(ReviewTestPageModule); }; @@ -91,5 +92,6 @@ angular.module('oppia').directive( // bootstrap the Angular 8. 'oppiaAngularRoot', downgradeComponent({ - component: OppiaAngularRootComponent - }) as angular.IDirectiveFactory); + component: OppiaAngularRootComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/signup-page/modals/license-explanation-modal.component.spec.ts b/core/templates/pages/signup-page/modals/license-explanation-modal.component.spec.ts index 39c74ecab7b9..6290f40f5966 100644 --- a/core/templates/pages/signup-page/modals/license-explanation-modal.component.spec.ts +++ b/core/templates/pages/signup-page/modals/license-explanation-modal.component.spec.ts @@ -16,10 +16,10 @@ * @fileoverview Unit tests for license explanation modal. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap'; -import { MockTranslateModule } from 'tests/unit-test-utils'; -import { LicenseExplanationModalComponent } from './license-explanation-modal.component'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal, NgbModalModule} from '@ng-bootstrap/ng-bootstrap'; +import {MockTranslateModule} from 'tests/unit-test-utils'; +import {LicenseExplanationModalComponent} from './license-explanation-modal.component'; describe('License Explanation Modal Component', () => { let fixture: ComponentFixture; @@ -27,16 +27,9 @@ describe('License Explanation Modal Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - MockTranslateModule, - NgbModalModule - ], - declarations: [ - LicenseExplanationModalComponent - ], - providers: [ - NgbActiveModal - ] + imports: [MockTranslateModule, NgbModalModule], + declarations: [LicenseExplanationModalComponent], + providers: [NgbActiveModal], }).compileComponents(); })); diff --git a/core/templates/pages/signup-page/modals/license-explanation-modal.component.ts b/core/templates/pages/signup-page/modals/license-explanation-modal.component.ts index 34852de390f2..847e4f8dbe03 100644 --- a/core/templates/pages/signup-page/modals/license-explanation-modal.component.ts +++ b/core/templates/pages/signup-page/modals/license-explanation-modal.component.ts @@ -16,18 +16,19 @@ * @fileoverview Component for license explanation modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { AppConstants } from 'app.constants'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {AppConstants} from 'app.constants'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-license-explanation-modal', - templateUrl: './license-explanation-modal.component.html' + templateUrl: './license-explanation-modal.component.html', }) export class LicenseExplanationModalComponent extends ConfirmOrCancelModal { SITE_NAME = AppConstants.SITE_NAME; - LICENSE_LINK = 'CC-BY-SA v4.0'; + LICENSE_LINK = + 'CC-BY-SA v4.0'; constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); diff --git a/core/templates/pages/signup-page/modals/registration-session-expired-modal.component.spec.ts b/core/templates/pages/signup-page/modals/registration-session-expired-modal.component.spec.ts index 099d595f52be..965a3b96ba74 100644 --- a/core/templates/pages/signup-page/modals/registration-session-expired-modal.component.spec.ts +++ b/core/templates/pages/signup-page/modals/registration-session-expired-modal.component.spec.ts @@ -16,20 +16,26 @@ * @fileoverview Unit tests for registration session expired modal. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { UserService } from 'services/user.service'; -import { MockTranslateModule } from 'tests/unit-test-utils'; -import { RegistrationSessionExpiredModalComponent } from './registration-session-expired-modal.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {NgbActiveModal, NgbModalModule} from '@ng-bootstrap/ng-bootstrap'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {UserService} from 'services/user.service'; +import {MockTranslateModule} from 'tests/unit-test-utils'; +import {RegistrationSessionExpiredModalComponent} from './registration-session-expired-modal.component'; class MockWindowRef { nativeWindow = { location: { href: '', - reload: () => {} - } + reload: () => {}, + }, }; } @@ -41,22 +47,16 @@ describe('Registration Session Expired Modal Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - MockTranslateModule, - NgbModalModule, - HttpClientTestingModule - ], - declarations: [ - RegistrationSessionExpiredModalComponent - ], + imports: [MockTranslateModule, NgbModalModule, HttpClientTestingModule], + declarations: [RegistrationSessionExpiredModalComponent], providers: [ NgbActiveModal, UserService, { provide: WindowRef, - useClass: MockWindowRef - } - ] + useClass: MockWindowRef, + }, + ], }).compileComponents(); })); @@ -71,22 +71,20 @@ describe('Registration Session Expired Modal Component', () => { expect(componentInstance).toBeDefined(); }); - it('should continue registration when login url is available', - fakeAsync(() => { - spyOn(userService, 'getLoginUrlAsync').and.returnValue( - Promise.resolve('login_url')); - componentInstance.continueRegistration(); - tick(); - tick(200); - })); + it('should continue registration when login url is available', fakeAsync(() => { + spyOn(userService, 'getLoginUrlAsync').and.returnValue( + Promise.resolve('login_url') + ); + componentInstance.continueRegistration(); + tick(); + tick(200); + })); - it('should reload page when login url is not available', - fakeAsync(() => { - spyOn(userService, 'getLoginUrlAsync').and.returnValue( - Promise.resolve('')); - spyOn(windowRef.nativeWindow.location, 'reload'); - componentInstance.continueRegistration(); - tick(); - expect(windowRef.nativeWindow.location.reload).toHaveBeenCalled(); - })); + it('should reload page when login url is not available', fakeAsync(() => { + spyOn(userService, 'getLoginUrlAsync').and.returnValue(Promise.resolve('')); + spyOn(windowRef.nativeWindow.location, 'reload'); + componentInstance.continueRegistration(); + tick(); + expect(windowRef.nativeWindow.location.reload).toHaveBeenCalled(); + })); }); diff --git a/core/templates/pages/signup-page/modals/registration-session-expired-modal.component.ts b/core/templates/pages/signup-page/modals/registration-session-expired-modal.component.ts index b70998c793b9..f641376c750e 100644 --- a/core/templates/pages/signup-page/modals/registration-session-expired-modal.component.ts +++ b/core/templates/pages/signup-page/modals/registration-session-expired-modal.component.ts @@ -16,14 +16,14 @@ * @fileoverview Component for registration session expired modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { UserService } from 'services/user.service'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {UserService} from 'services/user.service'; @Component({ selector: 'oppia-registration-session-expired-modal', - templateUrl: './registration-session-expired-modal.component.html' + templateUrl: './registration-session-expired-modal.component.html', }) export class RegistrationSessionExpiredModalComponent { constructor( @@ -33,7 +33,7 @@ export class RegistrationSessionExpiredModalComponent { ) {} continueRegistration(): void { - this.userService.getLoginUrlAsync().then((loginUrl) => { + this.userService.getLoginUrlAsync().then(loginUrl => { if (loginUrl) { setTimeout(() => { this.windowRef.nativeWindow.location.href = loginUrl; diff --git a/core/templates/pages/signup-page/services/signup-page-backend-api.service.spec.ts b/core/templates/pages/signup-page/services/signup-page-backend-api.service.spec.ts index ce858080b67a..5764eba0d6ab 100644 --- a/core/templates/pages/signup-page/services/signup-page-backend-api.service.spec.ts +++ b/core/templates/pages/signup-page/services/signup-page-backend-api.service.spec.ts @@ -16,10 +16,12 @@ * @fileoverview Unit tests for Signup page backend api service. */ -import { HttpClientTestingModule, HttpTestingController } from - '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; -import { SignupPageBackendApiService } from './signup-page-backend-api.service'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; +import {SignupPageBackendApiService} from './signup-page-backend-api.service'; describe('Admin backend api service', () => { let spbas: SignupPageBackendApiService; @@ -27,7 +29,7 @@ describe('Admin backend api service', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] + imports: [HttpClientTestingModule], }); spbas = TestBed.inject(SignupPageBackendApiService); @@ -45,7 +47,7 @@ describe('Admin backend api service', () => { can_send_emails: true, has_agreed_to_latest_terms: true, has_ever_registered: true, - username: 'test_user' + username: 'test_user', }; spbas.fetchSignupPageDataAsync().then(successHandler, failHandler); @@ -63,7 +65,7 @@ describe('Admin backend api service', () => { let successHandler = jasmine.createSpy('success'); let failHandler = jasmine.createSpy('fail'); const resp = { - username_is_taken: false + username_is_taken: false, }; spbas.checkUsernameAvailableAsync('').then(successHandler, failHandler); @@ -81,14 +83,14 @@ describe('Admin backend api service', () => { let successHandler = jasmine.createSpy('success'); let failHandler = jasmine.createSpy('fail'); const resp = { - bulk_email_signup_message_should_be_shown: true + bulk_email_signup_message_should_be_shown: true, }; let params = { agreed_to_terms: true, can_receive_email_updates: true, default_dashboard: '', - username: '' + username: '', }; spbas.updateUsernameAsync(params).then(successHandler, failHandler); diff --git a/core/templates/pages/signup-page/services/signup-page-backend-api.service.ts b/core/templates/pages/signup-page/services/signup-page-backend-api.service.ts index 065dfd88dcbd..8576bf62ab12 100644 --- a/core/templates/pages/signup-page/services/signup-page-backend-api.service.ts +++ b/core/templates/pages/signup-page/services/signup-page-backend-api.service.ts @@ -16,33 +16,33 @@ * @fileoverview Backend api service for fetching the data signup page. */ -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; export interface SignupPageBackendDict { - 'can_send_emails': boolean; - 'has_agreed_to_latest_terms': boolean; - 'has_ever_registered': boolean; - 'username': string; + can_send_emails: boolean; + has_agreed_to_latest_terms: boolean; + has_ever_registered: boolean; + username: string; } export interface UsernameAvailabilityResponse { - 'username_is_taken': boolean; + username_is_taken: boolean; } export interface UpdateUsernameResponseAsync { - 'bulk_email_signup_message_should_be_shown': boolean; + bulk_email_signup_message_should_be_shown: boolean; } export interface UpdateUsernameRequestParams { - 'agreed_to_terms': boolean; - 'can_receive_email_updates': boolean; - 'default_dashboard': string; + agreed_to_terms: boolean; + can_receive_email_updates: boolean; + default_dashboard: string; username: string; } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class SignupPageBackendApiService { SIGNUP_DATA_URL = '/signuphandler/data'; @@ -51,23 +51,26 @@ export class SignupPageBackendApiService { constructor(private http: HttpClient) {} async fetchSignupPageDataAsync(): Promise { - return this.http.get( - this.SIGNUP_DATA_URL).toPromise(); + return this.http + .get(this.SIGNUP_DATA_URL) + .toPromise(); } async checkUsernameAvailableAsync( - username: string + username: string ): Promise { - return this.http.post( - this.USERNAME_HANDLER, { - username: username - }).toPromise(); + return this.http + .post(this.USERNAME_HANDLER, { + username: username, + }) + .toPromise(); } async updateUsernameAsync( - requestParams: UpdateUsernameRequestParams + requestParams: UpdateUsernameRequestParams ): Promise { - return this.http.post( - this.SIGNUP_DATA_URL, requestParams).toPromise(); + return this.http + .post(this.SIGNUP_DATA_URL, requestParams) + .toPromise(); } } diff --git a/core/templates/pages/signup-page/signup-page-root.component.spec.ts b/core/templates/pages/signup-page/signup-page-root.component.spec.ts index b4232fa83c78..a783c657f014 100644 --- a/core/templates/pages/signup-page/signup-page-root.component.spec.ts +++ b/core/templates/pages/signup-page/signup-page-root.component.spec.ts @@ -16,17 +16,17 @@ * @fileoverview Unit tests for the signup page root component. */ -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; -import { AppConstants } from 'app.constants'; -import { MetaTagCustomizationService } from 'services/contextual/meta-tag-customization.service'; -import { PageHeadService } from 'services/page-head.service'; -import { PageTitleService } from 'services/page-title.service'; +import {AppConstants} from 'app.constants'; +import {MetaTagCustomizationService} from 'services/contextual/meta-tag-customization.service'; +import {PageHeadService} from 'services/page-head.service'; +import {PageTitleService} from 'services/page-title.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { SignupPageRootComponent } from './signup-page-root.component'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {SignupPageRootComponent} from './signup-page-root.component'; class MockTranslateService { onLangChange: EventEmitter = new EventEmitter(); @@ -43,19 +43,16 @@ describe('Signup Page Root', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - SignupPageRootComponent, - MockTranslatePipe - ], + declarations: [SignupPageRootComponent, MockTranslatePipe], providers: [ PageTitleService, MetaTagCustomizationService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -66,10 +63,9 @@ describe('Signup Page Root', () => { translateService = TestBed.inject(TranslateService); }); - it('should successfully instantiate the component', - () => { - expect(component).toBeDefined(); - }); + it('should successfully instantiate the component', () => { + expect(component).toBeDefined(); + }); it('should initialize and subscribe to onLangChange', () => { spyOn(translateService.onLangChange, 'subscribe'); @@ -95,10 +91,12 @@ describe('Signup Page Root', () => { component.setPageTitleAndMetaTags(); expect(translateService.instant).toHaveBeenCalledWith( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.SIGNUP.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.SIGNUP.TITLE + ); expect(pageHeadService.updateTitleAndMetaTags).toHaveBeenCalledWith( AppConstants.PAGES_REGISTERED_WITH_FRONTEND.SIGNUP.TITLE, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.SIGNUP.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.SIGNUP.META + ); }); it('should unsubscribe on component destruction', () => { diff --git a/core/templates/pages/signup-page/signup-page-root.component.ts b/core/templates/pages/signup-page/signup-page-root.component.ts index 86d326db3dfd..cf64d00dc20a 100644 --- a/core/templates/pages/signup-page/signup-page-root.component.ts +++ b/core/templates/pages/signup-page/signup-page-root.component.ts @@ -16,16 +16,16 @@ * @fileoverview Root component for Signup Page. */ -import { Component, OnDestroy } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; +import {Component, OnDestroy} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; @Component({ selector: 'oppia-signup-page-root', - templateUrl: './signup-page-root.component.html' + templateUrl: './signup-page-root.component.html', }) export class SignupPageRootComponent implements OnDestroy { directiveSubscriptions = new Subscription(); @@ -36,10 +36,12 @@ export class SignupPageRootComponent implements OnDestroy { setPageTitleAndMetaTags(): void { let translatedTitle = this.translateService.instant( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.SIGNUP.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.SIGNUP.TITLE + ); this.pageHeadService.updateTitleAndMetaTags( translatedTitle, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.SIGNUP.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.SIGNUP.META + ); } ngOnInit(): void { diff --git a/core/templates/pages/signup-page/signup-page-routing.module.ts b/core/templates/pages/signup-page/signup-page-routing.module.ts index da58b11ed866..459b4c972297 100644 --- a/core/templates/pages/signup-page/signup-page-routing.module.ts +++ b/core/templates/pages/signup-page/signup-page-routing.module.ts @@ -16,25 +16,19 @@ * @fileoverview Routing module for signup page. */ -import { NgModule } from '@angular/core'; -import { Route, RouterModule } from '@angular/router'; -import { SignupPageRootComponent } from './signup-page-root.component'; - +import {NgModule} from '@angular/core'; +import {Route, RouterModule} from '@angular/router'; +import {SignupPageRootComponent} from './signup-page-root.component'; const routes: Route[] = [ { path: '', - component: SignupPageRootComponent - } + component: SignupPageRootComponent, + }, ]; @NgModule({ - imports: [ - RouterModule.forChild(routes) - ], - exports: [ - RouterModule - ] + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], }) - export class SignupPageRoutingModule {} diff --git a/core/templates/pages/signup-page/signup-page.component.spec.ts b/core/templates/pages/signup-page/signup-page.component.spec.ts index 07c9b9d9b908..33cadcedd354 100644 --- a/core/templates/pages/signup-page/signup-page.component.spec.ts +++ b/core/templates/pages/signup-page/signup-page.component.spec.ts @@ -16,27 +16,37 @@ * @fileoverview Unit tests for sign up page component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { FormsModule } from '@angular/forms'; -import { NgbModal, NgbModalModule, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { AlertsService } from 'services/alerts.service'; -import { UrlService } from 'services/contextual/url.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { LoaderService } from 'services/loader.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { MockTranslateModule } from 'tests/unit-test-utils'; -import { SignupPageBackendApiService } from './services/signup-page-backend-api.service'; -import { SignupPageComponent } from './signup-page.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {FormsModule} from '@angular/forms'; +import { + NgbModal, + NgbModalModule, + NgbModalRef, +} from '@ng-bootstrap/ng-bootstrap'; +import {AlertsService} from 'services/alerts.service'; +import {UrlService} from 'services/contextual/url.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {LoaderService} from 'services/loader.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {MockTranslateModule} from 'tests/unit-test-utils'; +import {SignupPageBackendApiService} from './services/signup-page-backend-api.service'; +import {SignupPageComponent} from './signup-page.component'; class MockWindowRef { nativeWindow = { location: { - href: '' + href: '', }, - gtag: () => {} + gtag: () => {}, }; } @@ -57,24 +67,22 @@ describe('Sign up page component', () => { HttpClientTestingModule, MockTranslateModule, NgbModalModule, - FormsModule - ], - declarations: [ - SignupPageComponent, + FormsModule, ], + declarations: [SignupPageComponent], providers: [ { provide: WindowRef, - useClass: MockWindowRef + useClass: MockWindowRef, }, AlertsService, FocusManagerService, LoaderService, SignupPageBackendApiService, SiteAnalyticsService, - UrlService + UrlService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -103,13 +111,17 @@ describe('Sign up page component', () => { spyOn(loaderService, 'showLoadingScreen'); spyOn(loaderService, 'hideLoadingScreen'); spyOn(focusManagerService, 'setFocus'); - spyOn(signupPageBackendApiService, 'fetchSignupPageDataAsync') - .and.returnValue(Promise.resolve({ + spyOn( + signupPageBackendApiService, + 'fetchSignupPageDataAsync' + ).and.returnValue( + Promise.resolve({ can_send_emails: canSendEmails, has_agreed_to_latest_terms: hasAgreedToLatestTerms, has_ever_registered: hasEverRegistered, - username: username - })); + username: username, + }) + ); componentInstance.ngOnInit(); tick(); @@ -119,7 +131,8 @@ describe('Sign up page component', () => { expect(componentInstance.username).toEqual(username); expect(componentInstance.hasEverRegistered).toEqual(hasEverRegistered); expect(componentInstance.hasAgreedToLatestTerms).toEqual( - hasAgreedToLatestTerms); + hasAgreedToLatestTerms + ); expect(componentInstance.username).toEqual(username); })); @@ -132,35 +145,35 @@ describe('Sign up page component', () => { it('should confirm license explanation modal', () => { spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.resolve() + result: Promise.resolve(), } as NgbModalRef); - componentInstance.showLicenseExplanationModal( - { target: { innerText: 'here' } }); + componentInstance.showLicenseExplanationModal({ + target: {innerText: 'here'}, + }); expect(ngbModal.open).toHaveBeenCalled(); }); it('should cancel license explanation modal', () => { spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.reject() + result: Promise.reject(), } as NgbModalRef); - componentInstance.showLicenseExplanationModal( - { target: { innerText: 'here' } }); + componentInstance.showLicenseExplanationModal({ + target: {innerText: 'here'}, + }); expect(ngbModal.open).toHaveBeenCalled(); }); - it('should not trigger license explanation modal if click elsewhere', - () => { - spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.reject() - } as NgbModalRef); - componentInstance.showLicenseExplanationModal( - { target: { innerText: '' } }); - expect(ngbModal.open).not.toHaveBeenCalled(); - }); + it('should not trigger license explanation modal if click elsewhere', () => { + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.reject(), + } as NgbModalRef); + componentInstance.showLicenseExplanationModal({target: {innerText: ''}}); + expect(ngbModal.open).not.toHaveBeenCalled(); + }); it('should confirm registration session expired modal', () => { spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.resolve() + result: Promise.resolve(), } as NgbModalRef); componentInstance.showRegistrationSessionExpiredModal(); expect(ngbModal.open).toHaveBeenCalled(); @@ -168,7 +181,7 @@ describe('Sign up page component', () => { it('should cancel registration session expired modal', () => { spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.reject() + result: Promise.reject(), } as NgbModalRef); componentInstance.showRegistrationSessionExpiredModal(); expect(ngbModal.open).toHaveBeenCalled(); @@ -177,17 +190,22 @@ describe('Sign up page component', () => { it('should handle username input blur event', fakeAsync(() => { spyOn(alertsService, 'clearWarnings'); spyOn(componentInstance, 'updateWarningText'); - spyOn(signupPageBackendApiService, 'checkUsernameAvailableAsync') - .and.returnValue(Promise.resolve({ - username_is_taken: true - })); + spyOn( + signupPageBackendApiService, + 'checkUsernameAvailableAsync' + ).and.returnValue( + Promise.resolve({ + username_is_taken: true, + }) + ); componentInstance.warningI18nCode = ''; componentInstance.onUsernameInputFormBlur(''); tick(); expect(componentInstance.blurredAtLeastOnce).toBeTrue(); expect(componentInstance.usernameCheckIsInProgress).toBeFalse(); expect(componentInstance.warningI18nCode).toEqual( - 'I18N_SIGNUP_ERROR_USERNAME_TAKEN'); + 'I18N_SIGNUP_ERROR_USERNAME_TAKEN' + ); })); it('should not perform checks for empty username', () => { @@ -200,23 +218,30 @@ describe('Sign up page component', () => { it('should update warning text', () => { componentInstance.updateWarningText(''); expect(componentInstance.warningI18nCode).toEqual( - 'I18N_SIGNUP_ERROR_NO_USERNAME'); + 'I18N_SIGNUP_ERROR_NO_USERNAME' + ); componentInstance.updateWarningText(' '); expect(componentInstance.warningI18nCode).toEqual( - 'I18N_SIGNUP_ERROR_USERNAME_WITH_SPACES'); + 'I18N_SIGNUP_ERROR_USERNAME_WITH_SPACES' + ); componentInstance.updateWarningText( - 'this_username_is_longer_than_thiry_characters'); + 'this_username_is_longer_than_thiry_characters' + ); expect(componentInstance.warningI18nCode).toEqual( - 'I18N_SIGNUP_ERROR_USERNAME_TOO_LONG'); + 'I18N_SIGNUP_ERROR_USERNAME_TOO_LONG' + ); componentInstance.updateWarningText('$%'); expect(componentInstance.warningI18nCode).toEqual( - 'I18N_SIGNUP_ERROR_USERNAME_ONLY_ALPHANUM'); + 'I18N_SIGNUP_ERROR_USERNAME_ONLY_ALPHANUM' + ); componentInstance.updateWarningText('admin'); expect(componentInstance.warningI18nCode).toEqual( - 'I18N_SIGNUP_ERROR_USERNAME_WITH_ADMIN'); + 'I18N_SIGNUP_ERROR_USERNAME_WITH_ADMIN' + ); componentInstance.updateWarningText('oppia'); expect(componentInstance.warningI18nCode).toEqual( - 'I18N_SIGNUP_ERROR_USERNAME_NOT_AVAILABLE'); + 'I18N_SIGNUP_ERROR_USERNAME_NOT_AVAILABLE' + ); componentInstance.updateWarningText('validusername'); expect(componentInstance.warningI18nCode).toEqual(''); }); @@ -230,7 +255,8 @@ describe('Sign up page component', () => { spyOn(alertsService, 'addWarning'); componentInstance.submitPrerequisitesForm(false, 'test', 'yes'); expect(alertsService.addWarning).toHaveBeenCalledWith( - 'I18N_SIGNUP_ERROR_MUST_AGREE_TO_TERMS'); + 'I18N_SIGNUP_ERROR_MUST_AGREE_TO_TERMS' + ); }); it('should not submit if username is not valid', () => { @@ -242,12 +268,13 @@ describe('Sign up page component', () => { it('should submit prerequisites form', fakeAsync(() => { spyOn(urlService, 'getUrlParams').and.returnValue({ - return_url: 'creator-dashboard' + return_url: 'creator-dashboard', }); - spyOn(signupPageBackendApiService, 'updateUsernameAsync') - .and.returnValue(Promise.resolve({ - bulk_email_signup_message_should_be_shown: true - })); + spyOn(signupPageBackendApiService, 'updateUsernameAsync').and.returnValue( + Promise.resolve({ + bulk_email_signup_message_should_be_shown: true, + }) + ); spyOn(siteAnalyticsService, 'registerNewSignupEvent'); componentInstance.hasUsername = false; componentInstance.showEmailPreferencesForm = true; @@ -260,29 +287,32 @@ describe('Sign up page component', () => { it('should not submit when receive emails not enabled', () => { spyOn(urlService, 'getUrlParams').and.returnValue({ - return_url: 'learner-dashboard' + return_url: 'learner-dashboard', }); - spyOn(signupPageBackendApiService, 'updateUsernameAsync') - .and.returnValue(Promise.resolve({ - bulk_email_signup_message_should_be_shown: true - })); + spyOn(signupPageBackendApiService, 'updateUsernameAsync').and.returnValue( + Promise.resolve({ + bulk_email_signup_message_should_be_shown: true, + }) + ); spyOn(siteAnalyticsService, 'registerNewSignupEvent'); componentInstance.hasUsername = false; componentInstance.showEmailPreferencesForm = true; componentInstance.submitPrerequisitesForm(true, 'username', null); expect(componentInstance.emailPreferencesWarningText).toEqual( - 'I18N_SIGNUP_FIELD_REQUIRED'); + 'I18N_SIGNUP_FIELD_REQUIRED' + ); }); it('should submit prerequisites form and save analytics', fakeAsync(() => { spyOn(urlService, 'getUrlParams').and.returnValue({ - return_url: 'creator-dashboard' + return_url: 'creator-dashboard', }); - spyOn(signupPageBackendApiService, 'updateUsernameAsync') - .and.returnValue(Promise.resolve({ - bulk_email_signup_message_should_be_shown: false - })); + spyOn(signupPageBackendApiService, 'updateUsernameAsync').and.returnValue( + Promise.resolve({ + bulk_email_signup_message_should_be_shown: false, + }) + ); spyOn(siteAnalyticsService, 'registerNewSignupEvent'); componentInstance.hasUsername = false; componentInstance.showEmailPreferencesForm = true; @@ -293,38 +323,45 @@ describe('Sign up page component', () => { expect(siteAnalyticsService.registerNewSignupEvent).toHaveBeenCalled(); })); - it('should submit prerequisites form with user\'s preferred default ' + - 'dashboard', fakeAsync(() => { - spyOn(urlService, 'getUrlParams').and.returnValue({ - return_url: 'contributor-dashboard' - }); - spyOn(signupPageBackendApiService, 'updateUsernameAsync').and.returnValue( - Promise.resolve({ - bulk_email_signup_message_should_be_shown: true - })); - - const sentRequestParams = { - agreed_to_terms: true, - can_receive_email_updates: false, - default_dashboard: 'contributor', - username: 'username' - }; - - componentInstance.hasUsername = false; - componentInstance.showEmailPreferencesForm = true; - - componentInstance.submitPrerequisitesForm( - sentRequestParams.agreed_to_terms, - sentRequestParams.username, 'no'); - tick(); - - expect(signupPageBackendApiService.updateUsernameAsync) - .toHaveBeenCalledWith(sentRequestParams); - })); + it( + "should submit prerequisites form with user's preferred default " + + 'dashboard', + fakeAsync(() => { + spyOn(urlService, 'getUrlParams').and.returnValue({ + return_url: 'contributor-dashboard', + }); + spyOn(signupPageBackendApiService, 'updateUsernameAsync').and.returnValue( + Promise.resolve({ + bulk_email_signup_message_should_be_shown: true, + }) + ); + + const sentRequestParams = { + agreed_to_terms: true, + can_receive_email_updates: false, + default_dashboard: 'contributor', + username: 'username', + }; + + componentInstance.hasUsername = false; + componentInstance.showEmailPreferencesForm = true; + + componentInstance.submitPrerequisitesForm( + sentRequestParams.agreed_to_terms, + sentRequestParams.username, + 'no' + ); + tick(); + + expect( + signupPageBackendApiService.updateUsernameAsync + ).toHaveBeenCalledWith(sentRequestParams); + }) + ); it('should throw error if canReceiveEmailUpdates param is not valid', () => { spyOn(urlService, 'getUrlParams').and.returnValue({ - return_url: 'creator-dashboard' + return_url: 'creator-dashboard', }); componentInstance.hasUsername = false; componentInstance.showEmailPreferencesForm = true; @@ -336,20 +373,22 @@ describe('Sign up page component', () => { it('should handle if form is not processed at backend', fakeAsync(() => { spyOn(urlService, 'getUrlParams').and.returnValue({ - return_url: 'creator-dashboard' + return_url: 'creator-dashboard', }); - spyOn(signupPageBackendApiService, 'updateUsernameAsync') - .and.returnValue(Promise.reject({ - status_code: 401 - })); + spyOn(signupPageBackendApiService, 'updateUsernameAsync').and.returnValue( + Promise.reject({ + status_code: 401, + }) + ); spyOn(componentInstance, 'showRegistrationSessionExpiredModal'); componentInstance.hasUsername = false; componentInstance.showEmailPreferencesForm = true; componentInstance.submitPrerequisitesForm(true, 'username', 'no'); tick(); - expect(componentInstance.showRegistrationSessionExpiredModal) - .toHaveBeenCalled(); + expect( + componentInstance.showRegistrationSessionExpiredModal + ).toHaveBeenCalled(); expect(componentInstance.submissionInProcess).toBeFalse(); })); }); diff --git a/core/templates/pages/signup-page/signup-page.component.ts b/core/templates/pages/signup-page/signup-page.component.ts index 73d7cbf8886e..8dabf99e48cc 100644 --- a/core/templates/pages/signup-page/signup-page.component.ts +++ b/core/templates/pages/signup-page/signup-page.component.ts @@ -16,24 +16,24 @@ * @fileoverview Component for the Oppia profile page. */ -import { Component } from '@angular/core'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { AppConstants } from 'app.constants'; -import { AlertsService } from 'services/alerts.service'; -import { UrlService } from 'services/contextual/url.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { LoaderService } from 'services/loader.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { UtilsService } from 'services/utils.service'; -import { LicenseExplanationModalComponent } from './modals/license-explanation-modal.component'; -import { RegistrationSessionExpiredModalComponent } from './modals/registration-session-expired-modal.component'; -import { SignupPageBackendApiService } from './services/signup-page-backend-api.service'; +import {Component} from '@angular/core'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {AppConstants} from 'app.constants'; +import {AlertsService} from 'services/alerts.service'; +import {UrlService} from 'services/contextual/url.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {LoaderService} from 'services/loader.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {UtilsService} from 'services/utils.service'; +import {LicenseExplanationModalComponent} from './modals/license-explanation-modal.component'; +import {RegistrationSessionExpiredModalComponent} from './modals/registration-session-expired-modal.component'; +import {SignupPageBackendApiService} from './services/signup-page-backend-api.service'; import analyticsConstants from 'analytics-constants'; @Component({ selector: 'oppia-signup-page', - templateUrl: './signup-page.component.html' + templateUrl: './signup-page.component.html', }) export class SignupPageComponent { // These properties are initialized using Angular lifecycle hooks @@ -71,42 +71,43 @@ export class SignupPageComponent { ngOnInit(): void { this.loaderService.showLoadingScreen('I18N_SIGNUP_LOADING'); - this.signupPageBackendApiService.fetchSignupPageDataAsync() - .then((data) => { - this.loaderService.hideLoadingScreen(); - this.username = data.username; - this.hasEverRegistered = data.has_ever_registered; - this.hasAgreedToLatestTerms = data.has_agreed_to_latest_terms; - this.showEmailPreferencesForm = data.can_send_emails; - this.hasUsername = Boolean(this.username); - this.focusManagerService.setFocus('usernameInputField'); - }); + this.signupPageBackendApiService.fetchSignupPageDataAsync().then(data => { + this.loaderService.hideLoadingScreen(); + this.username = data.username; + this.hasEverRegistered = data.has_ever_registered; + this.hasAgreedToLatestTerms = data.has_agreed_to_latest_terms; + this.showEmailPreferencesForm = data.can_send_emails; + this.hasUsername = Boolean(this.username); + this.focusManagerService.setFocus('usernameInputField'); + }); } isFormValid(): boolean { return ( - this.hasAgreedToLatestTerms && - (this.hasUsername || !this.warningI18nCode) + this.hasAgreedToLatestTerms && (this.hasUsername || !this.warningI18nCode) ); } - showLicenseExplanationModal(evt: { target: {innerText: string} }): void { + showLicenseExplanationModal(evt: {target: {innerText: string}}): void { if (evt.target.innerText !== 'here') { return; } let modalRef = this.ngbModal.open(LicenseExplanationModalComponent, { - backdrop: true + backdrop: true, }); - modalRef.result.then(() => { - // Note to developers: - // This callback is triggered when the Confirm button is clicked. - // No further action is needed. - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + modalRef.result.then( + () => { + // Note to developers: + // This callback is triggered when the Confirm button is clicked. + // No further action is needed. + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } onUsernameInputFormBlur(username: string): void { @@ -118,8 +119,9 @@ export class SignupPageComponent { this.updateWarningText(username); if (!this.warningI18nCode) { this.usernameCheckIsInProgress = true; - this.signupPageBackendApiService.checkUsernameAvailableAsync(username) - .then((response) => { + this.signupPageBackendApiService + .checkUsernameAvailableAsync(username) + .then(response => { if (response.username_is_taken) { this.warningI18nCode = 'I18N_SIGNUP_ERROR_USERNAME_TAKEN'; } @@ -157,9 +159,10 @@ export class SignupPageComponent { } submitPrerequisitesForm( - agreedToTerms: boolean, - username: string, - canReceiveEmailUpdates: string | null = null): void { + agreedToTerms: boolean, + username: string, + canReceiveEmailUpdates: string | null = null + ): void { if (!agreedToTerms) { this.alertsService.addWarning('I18N_SIGNUP_ERROR_MUST_AGREE_TO_TERMS'); return; @@ -170,8 +173,9 @@ export class SignupPageComponent { } let defaultDashboard: string = AppConstants.DASHBOARD_TYPE_LEARNER; - let returnUrl = ( - decodeURIComponent(this.urlService.getUrlParams().return_url)); + let returnUrl = decodeURIComponent( + this.urlService.getUrlParams().return_url + ); if (returnUrl.indexOf('creator-dashboard') !== -1) { defaultDashboard = AppConstants.DASHBOARD_TYPE_CREATOR; @@ -185,7 +189,7 @@ export class SignupPageComponent { agreed_to_terms: agreedToTerms, can_receive_email_updates: false, default_dashboard: defaultDashboard, - username: username + username: username, }; if (this.showEmailPreferencesForm && !this.hasUsername) { @@ -200,47 +204,57 @@ export class SignupPageComponent { requestParams.can_receive_email_updates = false; } else { throw new Error( - 'Invalid value for email preferences: ' + - canReceiveEmailUpdates); + 'Invalid value for email preferences: ' + canReceiveEmailUpdates + ); } } this.submissionInProcess = true; - this.signupPageBackendApiService.updateUsernameAsync(requestParams) - .then((returnValue) => { + this.signupPageBackendApiService.updateUsernameAsync(requestParams).then( + returnValue => { if (returnValue.bulk_email_signup_message_should_be_shown) { this.showEmailSignupLink = true; this.submissionInProcess = false; return; } this.siteAnalyticsService.registerNewSignupEvent('#signup-submit'); - setTimeout(() => { - this.windowRef.nativeWindow.location.href = ( - this.utilsService.getSafeReturnUrl(returnUrl)); - }, analyticsConstants.CAN_SEND_ANALYTICS_EVENTS ? 150 : 0); - }, (rejection) => { + setTimeout( + () => { + this.windowRef.nativeWindow.location.href = + this.utilsService.getSafeReturnUrl(returnUrl); + }, + analyticsConstants.CAN_SEND_ANALYTICS_EVENTS ? 150 : 0 + ); + }, + rejection => { if (rejection && rejection.status_code === 401) { this.showRegistrationSessionExpiredModal(); } this.submissionInProcess = false; - }); + } + ); } showRegistrationSessionExpiredModal(): void { let modalRef = this.ngbModal.open( - RegistrationSessionExpiredModalComponent, { + RegistrationSessionExpiredModalComponent, + { backdrop: 'static', - keyboard: false - }); - - modalRef.result.then(() => { - // Note to developers: - // This callback is triggered when the Confirm button is clicked. - // No further action is needed. - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + keyboard: false, + } + ); + + modalRef.result.then( + () => { + // Note to developers: + // This callback is triggered when the Confirm button is clicked. + // No further action is needed. + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } } diff --git a/core/templates/pages/signup-page/signup-page.module.ts b/core/templates/pages/signup-page/signup-page.module.ts index 6f2f77b05ef3..f43977b1c8f9 100644 --- a/core/templates/pages/signup-page/signup-page.module.ts +++ b/core/templates/pages/signup-page/signup-page.module.ts @@ -16,32 +16,28 @@ * @fileoverview Module for the signup page. */ -import { NgModule } from '@angular/core'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { RegistrationSessionExpiredModalComponent } from './modals/registration-session-expired-modal.component'; -import { LicenseExplanationModalComponent } from './modals/license-explanation-modal.component'; -import { SignupPageRootComponent } from './signup-page-root.component'; -import { SignupPageComponent } from './signup-page.component'; -import { CommonModule } from '@angular/common'; -import { SignupPageRoutingModule } from './signup-page-routing.module'; +import {NgModule} from '@angular/core'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {RegistrationSessionExpiredModalComponent} from './modals/registration-session-expired-modal.component'; +import {LicenseExplanationModalComponent} from './modals/license-explanation-modal.component'; +import {SignupPageRootComponent} from './signup-page-root.component'; +import {SignupPageComponent} from './signup-page.component'; +import {CommonModule} from '@angular/common'; +import {SignupPageRoutingModule} from './signup-page-routing.module'; @NgModule({ - imports: [ - CommonModule, - SharedComponentsModule, - SignupPageRoutingModule - ], + imports: [CommonModule, SharedComponentsModule, SignupPageRoutingModule], declarations: [ SignupPageComponent, SignupPageRootComponent, RegistrationSessionExpiredModalComponent, - LicenseExplanationModalComponent + LicenseExplanationModalComponent, ], entryComponents: [ SignupPageComponent, SignupPageRootComponent, RegistrationSessionExpiredModalComponent, - LicenseExplanationModalComponent - ] + LicenseExplanationModalComponent, + ], }) export class SignupPageModule {} diff --git a/core/templates/pages/skill-editor-page/editor-tab/skill-concept-card-editor/skill-concept-card-editor.component.spec.ts b/core/templates/pages/skill-editor-page/editor-tab/skill-concept-card-editor/skill-concept-card-editor.component.spec.ts index 027b2251eb1a..78de2074b41d 100644 --- a/core/templates/pages/skill-editor-page/editor-tab/skill-concept-card-editor/skill-concept-card-editor.component.spec.ts +++ b/core/templates/pages/skill-editor-page/editor-tab/skill-concept-card-editor/skill-concept-card-editor.component.spec.ts @@ -16,23 +16,29 @@ * @fileoverview Unit tests for the skill editor main tab component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { ConceptCard } from 'domain/skill/concept-card.model'; -import { SkillUpdateService } from 'domain/skill/skill-update.service'; -import { Skill } from 'domain/skill/SkillObjectFactory'; -import { SkillEditorStateService } from 'pages/skill-editor-page/services/skill-editor-state.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { SkillConceptCardEditorComponent } from './skill-concept-card-editor.component'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { AppConstants } from 'app.constants'; -import { RecordedVoiceovers } from 'domain/exploration/recorded-voiceovers.model'; -import { CdkDragSortEvent } from '@angular/cdk/drag-drop'; -import { WorkedExample } from 'domain/skill/worked-example.model'; -import { of } from 'rxjs'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {ConceptCard} from 'domain/skill/concept-card.model'; +import {SkillUpdateService} from 'domain/skill/skill-update.service'; +import {Skill} from 'domain/skill/SkillObjectFactory'; +import {SkillEditorStateService} from 'pages/skill-editor-page/services/skill-editor-state.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {SkillConceptCardEditorComponent} from './skill-concept-card-editor.component'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {AppConstants} from 'app.constants'; +import {RecordedVoiceovers} from 'domain/exploration/recorded-voiceovers.model'; +import {CdkDragSortEvent} from '@angular/cdk/drag-drop'; +import {WorkedExample} from 'domain/skill/worked-example.model'; +import {of} from 'rxjs'; class MockNgbModalRef { componentInstance = {}; @@ -61,9 +67,7 @@ describe('Skill Concept Card Editor Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - SkillConceptCardEditorComponent - ], + declarations: [SkillConceptCardEditorComponent], providers: [ SkillEditorStateService, SkillUpdateService, @@ -71,13 +75,12 @@ describe('Skill Concept Card Editor Component', () => { provide: WindowDimensionsService, useValue: { isWindowNarrow: () => true, - getResizeEvent: () => of(resizeEvent) - } + getResizeEvent: () => of(resizeEvent), + }, }, UrlInterpolationService, - ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -92,20 +95,33 @@ describe('Skill Concept Card Editor Component', () => { const conceptCard = new ConceptCard( SubtitledHtml.createDefault( - 'review material', AppConstants.COMPONENT_NAME_EXPLANATION), + 'review material', + AppConstants.COMPONENT_NAME_EXPLANATION + ), [], RecordedVoiceovers.createFromBackendDict({ voiceovers_mapping: { - COMPONENT_NAME_EXPLANATION: {} - } + COMPONENT_NAME_EXPLANATION: {}, + }, }) ); sampleSkill = new Skill( - 'id1', 'description', [], [], conceptCard, 'en', 1, 0, 'id1', false, [] + 'id1', + 'description', + [], + [], + conceptCard, + 'en', + 1, + 0, + 'id1', + false, + [] ); spyOn(skillEditorStateService, 'getSkill').and.returnValue(sampleSkill); - spyOnProperty(skillEditorStateService, 'onSkillChange') - .and.returnValue(mockEventEmitter); + spyOnProperty(skillEditorStateService, 'onSkillChange').and.returnValue( + mockEventEmitter + ); }); afterEach(() => { @@ -148,20 +164,26 @@ describe('Skill Concept Card Editor Component', () => { }); it('should return image url', () => { - spyOn(urlInterpolationService, 'getStaticImageUrl') - .and.returnValue('imagePath'); + spyOn(urlInterpolationService, 'getStaticImageUrl').and.returnValue( + 'imagePath' + ); expect(component.getStaticImageUrl('/imagePath')).toBe('imagePath'); }); it('should update skill on saving explanation', () => { - let updateSpy = spyOn(skillUpdateService, 'setConceptCardExplanation') - .and.callThrough(); + let updateSpy = spyOn( + skillUpdateService, + 'setConceptCardExplanation' + ).and.callThrough(); component.ngOnInit(); component.onSaveExplanation( SubtitledHtml.createDefault( - 'review material', AppConstants.COMPONENT_NAME_EXPLANATION)); + 'review material', + AppConstants.COMPONENT_NAME_EXPLANATION + ) + ); expect(updateSpy).toHaveBeenCalled(); }); @@ -184,109 +206,130 @@ describe('Skill Concept Card Editor Component', () => { expect(component.activeWorkedExampleIndex).toBe(3); }); - it('should open delete worked example modal when ' + - 'clicking on delete button', fakeAsync(() => { - let modalSpy = spyOn(ngbModal, 'open').and.returnValue({ - componentInstance: new MockNgbModalRef(), - result: Promise.resolve() - } as NgbModalRef); - let deleteWorkedExampleSpy = spyOn( - skillUpdateService, 'deleteWorkedExample').and.callThrough(); - - component.ngOnInit(); - component.deleteWorkedExample(0, ''); - tick(); - - expect(modalSpy).toHaveBeenCalled(); - expect(deleteWorkedExampleSpy).toHaveBeenCalled(); - })); - - it('should close delete worked example modal when ' + - 'clicking on cancel button', fakeAsync(() => { - let modalSpy = spyOn(ngbModal, 'open').and.returnValue({ - componentInstance: new MockNgbModalRef(), - result: Promise.reject() - } as NgbModalRef); - let deleteWorkedExampleSpy = spyOn( - skillUpdateService, 'deleteWorkedExample').and.callThrough(); - - component.ngOnInit(); - component.deleteWorkedExample(0, ''); - tick(); - - expect(modalSpy).toHaveBeenCalled(); - expect(deleteWorkedExampleSpy).not.toHaveBeenCalled(); - })); - - it('should open add worked example modal when ' + - 'clicking on add button', fakeAsync(() => { - let modalSpy = spyOn(ngbModal, 'open').and.returnValue({ - componentInstance: new MockNgbModalRef(), - result: Promise.resolve({ - workedExampleQuestionHtml: 'questionHtml', - workedExampleExplanationHtml: 'explanationHtml' - }) - } as NgbModalRef); - let addWorkedExampleSpy = spyOn( - skillUpdateService, 'addWorkedExample').and.callThrough(); - - component.ngOnInit(); - component.openAddWorkedExampleModal(); - tick(); - - expect(modalSpy).toHaveBeenCalled(); - expect(addWorkedExampleSpy).toHaveBeenCalled(); - })); - - it('should close add worked example modal when ' + - 'clicking on cancel button', fakeAsync(() => { - let modalSpy = spyOn(ngbModal, 'open').and.returnValue({ - componentInstance: new MockNgbModalRef(), - result: Promise.reject() - } as NgbModalRef); - let addWorkedExampleSpy = spyOn( - skillUpdateService, 'addWorkedExample').and.callThrough(); - - component.ngOnInit(); - component.openAddWorkedExampleModal(); - tick(); - - expect(modalSpy).toHaveBeenCalled(); - expect(addWorkedExampleSpy).not.toHaveBeenCalled(); - })); - - it('should open show skill preview modal when ' + - 'clicking on preview button', fakeAsync(() => { - let modalSpy = spyOn(ngbModal, 'open').and.returnValue({ - componentInstance: new MockNgbModalRefPreview(), - result: Promise.resolve() - } as NgbModalRef); - - component.ngOnInit(); - component.showSkillPreview(); - tick(); - - expect(modalSpy).toHaveBeenCalled(); - })); - - it('should close show skill preview modal when ' + - 'clicking on cancel button', fakeAsync(() => { - let modalSpy = spyOn(ngbModal, 'open').and.returnValue({ - componentInstance: new MockNgbModalRefPreview(), - result: Promise.reject() - } as NgbModalRef); - - component.ngOnInit(); - component.showSkillPreview(); - tick(); - - expect(modalSpy).toHaveBeenCalled(); - })); + it( + 'should open delete worked example modal when ' + + 'clicking on delete button', + fakeAsync(() => { + let modalSpy = spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: new MockNgbModalRef(), + result: Promise.resolve(), + } as NgbModalRef); + let deleteWorkedExampleSpy = spyOn( + skillUpdateService, + 'deleteWorkedExample' + ).and.callThrough(); + + component.ngOnInit(); + component.deleteWorkedExample(0, ''); + tick(); + + expect(modalSpy).toHaveBeenCalled(); + expect(deleteWorkedExampleSpy).toHaveBeenCalled(); + }) + ); + + it( + 'should close delete worked example modal when ' + + 'clicking on cancel button', + fakeAsync(() => { + let modalSpy = spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: new MockNgbModalRef(), + result: Promise.reject(), + } as NgbModalRef); + let deleteWorkedExampleSpy = spyOn( + skillUpdateService, + 'deleteWorkedExample' + ).and.callThrough(); + + component.ngOnInit(); + component.deleteWorkedExample(0, ''); + tick(); + + expect(modalSpy).toHaveBeenCalled(); + expect(deleteWorkedExampleSpy).not.toHaveBeenCalled(); + }) + ); + + it( + 'should open add worked example modal when ' + 'clicking on add button', + fakeAsync(() => { + let modalSpy = spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: new MockNgbModalRef(), + result: Promise.resolve({ + workedExampleQuestionHtml: 'questionHtml', + workedExampleExplanationHtml: 'explanationHtml', + }), + } as NgbModalRef); + let addWorkedExampleSpy = spyOn( + skillUpdateService, + 'addWorkedExample' + ).and.callThrough(); + + component.ngOnInit(); + component.openAddWorkedExampleModal(); + tick(); + + expect(modalSpy).toHaveBeenCalled(); + expect(addWorkedExampleSpy).toHaveBeenCalled(); + }) + ); + + it( + 'should close add worked example modal when ' + 'clicking on cancel button', + fakeAsync(() => { + let modalSpy = spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: new MockNgbModalRef(), + result: Promise.reject(), + } as NgbModalRef); + let addWorkedExampleSpy = spyOn( + skillUpdateService, + 'addWorkedExample' + ).and.callThrough(); + + component.ngOnInit(); + component.openAddWorkedExampleModal(); + tick(); + + expect(modalSpy).toHaveBeenCalled(); + expect(addWorkedExampleSpy).not.toHaveBeenCalled(); + }) + ); + + it( + 'should open show skill preview modal when ' + 'clicking on preview button', + fakeAsync(() => { + let modalSpy = spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: new MockNgbModalRefPreview(), + result: Promise.resolve(), + } as NgbModalRef); + + component.ngOnInit(); + component.showSkillPreview(); + tick(); + + expect(modalSpy).toHaveBeenCalled(); + }) + ); + + it( + 'should close show skill preview modal when ' + 'clicking on cancel button', + fakeAsync(() => { + let modalSpy = spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: new MockNgbModalRefPreview(), + result: Promise.reject(), + } as NgbModalRef); + + component.ngOnInit(); + component.showSkillPreview(); + tick(); + + expect(modalSpy).toHaveBeenCalled(); + }) + ); it('should toggle worked example on clicking', () => { component.workedExamplesListIsShown = true; - spyOn(windowDimensionsService, 'isWindowNarrow') - .and.returnValue(true); + spyOn(windowDimensionsService, 'isWindowNarrow').and.returnValue(true); component.toggleWorkedExampleList(); @@ -299,8 +342,7 @@ describe('Skill Concept Card Editor Component', () => { it('should toggle skill editor card on clicking', () => { component.skillEditorCardIsShown = true; - spyOn(windowDimensionsService, 'isWindowNarrow') - .and.returnValue(true); + spyOn(windowDimensionsService, 'isWindowNarrow').and.returnValue(true); component.toggleSkillEditorCard(); @@ -320,7 +362,8 @@ describe('Skill Concept Card Editor Component', () => { it('should show worked examples list when the window is narrow', () => { spyOn(windowDimensionsService, 'isWindowNarrow').and.returnValue(true); spyOn(windowDimensionsService, 'getResizeEvent').and.returnValue( - mockEventEmitter); + mockEventEmitter + ); component.windowIsNarrow = false; expect(component.workedExamplesListIsShown).toBe(false); diff --git a/core/templates/pages/skill-editor-page/editor-tab/skill-concept-card-editor/skill-concept-card-editor.component.ts b/core/templates/pages/skill-editor-page/editor-tab/skill-concept-card-editor/skill-concept-card-editor.component.ts index d7e1539cf956..d646b0eeadc9 100644 --- a/core/templates/pages/skill-editor-page/editor-tab/skill-concept-card-editor/skill-concept-card-editor.component.ts +++ b/core/templates/pages/skill-editor-page/editor-tab/skill-concept-card-editor/skill-concept-card-editor.component.ts @@ -16,24 +16,24 @@ * @fileoverview Component for the concept card editor. */ -import { CdkDragSortEvent, moveItemInArray } from '@angular/cdk/drag-drop'; -import { Component, EventEmitter, OnInit, Output } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { Subscription } from 'rxjs'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { SkillUpdateService } from 'domain/skill/skill-update.service'; -import { WorkedExample } from 'domain/skill/worked-example.model'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { AddWorkedExampleModalComponent } from 'pages/skill-editor-page/modal-templates/add-worked-example.component'; -import { DeleteWorkedExampleComponent } from 'pages/skill-editor-page/modal-templates/delete-worked-example-modal.component'; -import { SkillEditorStateService } from 'pages/skill-editor-page/services/skill-editor-state.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { GenerateContentIdService } from 'services/generate-content-id.service'; -import { FormatRtePreviewPipe } from 'filters/format-rte-preview.pipe'; -import { SkillPreviewModalComponent } from '../skill-preview-modal.component'; -import { Skill } from 'domain/skill/SkillObjectFactory'; -import { AppConstants } from 'app.constants'; +import {CdkDragSortEvent, moveItemInArray} from '@angular/cdk/drag-drop'; +import {Component, EventEmitter, OnInit, Output} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {Subscription} from 'rxjs'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {SkillUpdateService} from 'domain/skill/skill-update.service'; +import {WorkedExample} from 'domain/skill/worked-example.model'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {AddWorkedExampleModalComponent} from 'pages/skill-editor-page/modal-templates/add-worked-example.component'; +import {DeleteWorkedExampleComponent} from 'pages/skill-editor-page/modal-templates/delete-worked-example-modal.component'; +import {SkillEditorStateService} from 'pages/skill-editor-page/services/skill-editor-state.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {GenerateContentIdService} from 'services/generate-content-id.service'; +import {FormatRtePreviewPipe} from 'filters/format-rte-preview.pipe'; +import {SkillPreviewModalComponent} from '../skill-preview-modal.component'; +import {Skill} from 'domain/skill/SkillObjectFactory'; +import {AppConstants} from 'app.constants'; interface BindableFieldDict { displayedConceptCardExplanation: string; @@ -42,7 +42,7 @@ interface BindableFieldDict { @Component({ selector: 'oppia-skill-concept-card-editor', - templateUrl: './skill-concept-card-editor.component.html' + templateUrl: './skill-concept-card-editor.component.html', }) export class SkillConceptCardEditorComponent implements OnInit { @Output() getConceptCardChange: EventEmitter = new EventEmitter(); @@ -59,8 +59,7 @@ export class SkillConceptCardEditorComponent implements OnInit { skillEditorCardIsShown: boolean = false; workedExamplesListIsShown: boolean = false; windowIsNarrow!: boolean; - COMPONENT_NAME_WORKED_EXAMPLE = ( - AppConstants.COMPONENT_NAME_WORKED_EXAMPLE); + COMPONENT_NAME_WORKED_EXAMPLE = AppConstants.COMPONENT_NAME_WORKED_EXAMPLE; constructor( private formatRtePreviewPipe: FormatRtePreviewPipe, @@ -74,10 +73,14 @@ export class SkillConceptCardEditorComponent implements OnInit { drop(event: CdkDragSortEvent): void { moveItemInArray( - this.bindableFieldsDict.displayedWorkedExamples, event.previousIndex, - event.currentIndex); + this.bindableFieldsDict.displayedWorkedExamples, + event.previousIndex, + event.currentIndex + ); this.skillUpdateService.updateWorkedExamples( - this.skill, this.bindableFieldsDict.displayedWorkedExamples); + this.skill, + this.bindableFieldsDict.displayedWorkedExamples + ); this.getConceptCardChange.emit(); } @@ -87,16 +90,18 @@ export class SkillConceptCardEditorComponent implements OnInit { initBindableFieldsDict(): void { this.bindableFieldsDict = { - displayedConceptCardExplanation: - this.skill.getConceptCard().getExplanation().html, - displayedWorkedExamples: - this.skill.getConceptCard().getWorkedExamples() + displayedConceptCardExplanation: this.skill + .getConceptCard() + .getExplanation().html, + displayedWorkedExamples: this.skill.getConceptCard().getWorkedExamples(), }; } onSaveExplanation(explanationObject: SubtitledHtml): void { this.skillUpdateService.setConceptCardExplanation( - this.skill, explanationObject); + this.skill, + explanationObject + ); } onSaveDescription(): void { @@ -105,8 +110,9 @@ export class SkillConceptCardEditorComponent implements OnInit { changeActiveWorkedExampleIndex(idx: number): void { if (idx === this.activeWorkedExampleIndex) { - this.bindableFieldsDict.displayedWorkedExamples = ( - this.skill.getConceptCard().getWorkedExamples()); + this.bindableFieldsDict.displayedWorkedExamples = this.skill + .getConceptCard() + .getWorkedExamples(); this.activeWorkedExampleIndex = null; } else { this.activeWorkedExampleIndex = idx; @@ -114,82 +120,104 @@ export class SkillConceptCardEditorComponent implements OnInit { } deleteWorkedExample(index: number, evt: string): void { - this.ngbModal.open(DeleteWorkedExampleComponent, { - backdrop: 'static' - }).result.then(() => { - this.skillUpdateService.deleteWorkedExample(this.skill, index); - this.bindableFieldsDict.displayedWorkedExamples = - this.skill.getConceptCard().getWorkedExamples(); - this.activeWorkedExampleIndex = null; - this.getConceptCardChange.emit(); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + this.ngbModal + .open(DeleteWorkedExampleComponent, { + backdrop: 'static', + }) + .result.then( + () => { + this.skillUpdateService.deleteWorkedExample(this.skill, index); + this.bindableFieldsDict.displayedWorkedExamples = this.skill + .getConceptCard() + .getWorkedExamples(); + this.activeWorkedExampleIndex = null; + this.getConceptCardChange.emit(); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } getWorkedExampleSummary(workedExampleQuestion: string): string { - const summary = this.formatRtePreviewPipe.transform( - workedExampleQuestion); + const summary = this.formatRtePreviewPipe.transform(workedExampleQuestion); return summary; } openAddWorkedExampleModal(): void { - this.ngbModal.open(AddWorkedExampleModalComponent, { - backdrop: 'static' - }).result.then((result) => { - let newExample = WorkedExample.create( - SubtitledHtml.createDefault( - result.workedExampleQuestionHtml, - this.generateContentIdService.getNextId( - this.skill.getConceptCard().getRecordedVoiceovers( - ).getAllContentIds(), - this.COMPONENT_NAME_WORKED_EXAMPLE.QUESTION)), - SubtitledHtml.createDefault( - result.workedExampleExplanationHtml, - this.generateContentIdService.getNextId( - this.skill.getConceptCard().getRecordedVoiceovers( - ).getAllContentIds(), - this.COMPONENT_NAME_WORKED_EXAMPLE.EXPLANATION)) + this.ngbModal + .open(AddWorkedExampleModalComponent, { + backdrop: 'static', + }) + .result.then( + result => { + let newExample = WorkedExample.create( + SubtitledHtml.createDefault( + result.workedExampleQuestionHtml, + this.generateContentIdService.getNextId( + this.skill + .getConceptCard() + .getRecordedVoiceovers() + .getAllContentIds(), + this.COMPONENT_NAME_WORKED_EXAMPLE.QUESTION + ) + ), + SubtitledHtml.createDefault( + result.workedExampleExplanationHtml, + this.generateContentIdService.getNextId( + this.skill + .getConceptCard() + .getRecordedVoiceovers() + .getAllContentIds(), + this.COMPONENT_NAME_WORKED_EXAMPLE.EXPLANATION + ) + ) + ); + this.skillUpdateService.addWorkedExample(this.skill, newExample); + this.bindableFieldsDict.displayedWorkedExamples = this.skill + .getConceptCard() + .getWorkedExamples(); + this.getConceptCardChange.emit(); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } ); - this.skillUpdateService.addWorkedExample( - this.skill, newExample); - this.bindableFieldsDict.displayedWorkedExamples = ( - this.skill.getConceptCard().getWorkedExamples()); - this.getConceptCardChange.emit(); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); } showSkillPreview(): void { - let skillDescription = ( - this.skillEditorStateService.getSkill().getDescription()); - let skillExplanation = ( - this.bindableFieldsDict.displayedConceptCardExplanation); - let skillWorkedExamples = ( - this.bindableFieldsDict.displayedWorkedExamples); + let skillDescription = this.skillEditorStateService + .getSkill() + .getDescription(); + let skillExplanation = + this.bindableFieldsDict.displayedConceptCardExplanation; + let skillWorkedExamples = this.bindableFieldsDict.displayedWorkedExamples; const modalInstance: NgbModalRef = this.ngbModal.open( - SkillPreviewModalComponent, { + SkillPreviewModalComponent, + { backdrop: true, - }); + } + ); modalInstance.componentInstance.skillDescription = skillDescription; modalInstance.componentInstance.skillExplanation = skillExplanation; modalInstance.componentInstance.skillWorkedExamples = skillWorkedExamples; - modalInstance.result.then(() => {}, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + modalInstance.result.then( + () => {}, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } toggleWorkedExampleList(): void { if (this.windowDimensionsService.isWindowNarrow()) { - this.workedExamplesListIsShown = ( - !this.workedExamplesListIsShown); + this.workedExamplesListIsShown = !this.workedExamplesListIsShown; } } @@ -202,35 +230,28 @@ export class SkillConceptCardEditorComponent implements OnInit { ngOnInit(): void { this.windowIsNarrow = this.windowDimensionsService.isWindowNarrow(); this.directiveSubscriptions.add( - this.windowDimensionsService.getResizeEvent().subscribe( - () => { - this.windowIsNarrow = this.windowDimensionsService.isWindowNarrow(); - this.workedExamplesListIsShown = ( - !this.windowIsNarrow); - } - )); + this.windowDimensionsService.getResizeEvent().subscribe(() => { + this.windowIsNarrow = this.windowDimensionsService.isWindowNarrow(); + this.workedExamplesListIsShown = !this.windowIsNarrow; + }) + ); this.isEditable = true; this.skill = this.skillEditorStateService.getSkill(); this.initBindableFieldsDict(); this.skillEditorCardIsShown = true; - this.workedExamplesListIsShown = ( - !this.windowDimensionsService.isWindowNarrow()); + this.workedExamplesListIsShown = + !this.windowDimensionsService.isWindowNarrow(); this.directiveSubscriptions.add( - this.skillEditorStateService.onSkillChange.subscribe( - () => { - this.initBindableFieldsDict(); - } - ) + this.skillEditorStateService.onSkillChange.subscribe(() => { + this.initBindableFieldsDict(); + }) ); this.directiveSubscriptions.add( - this.windowDimensionsService.getResizeEvent().subscribe( - () => { - this.workedExamplesListIsShown = ( - !this.windowDimensionsService.isWindowNarrow() - ); - } - ) + this.windowDimensionsService.getResizeEvent().subscribe(() => { + this.workedExamplesListIsShown = + !this.windowDimensionsService.isWindowNarrow(); + }) ); } @@ -239,7 +260,9 @@ export class SkillConceptCardEditorComponent implements OnInit { } } -angular.module('oppia').directive('oppiaSkillConceptCardEditor', -downgradeComponent({ - component: SkillConceptCardEditorComponent -}) as angular.IDirectiveFactory); +angular.module('oppia').directive( + 'oppiaSkillConceptCardEditor', + downgradeComponent({ + component: SkillConceptCardEditorComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/skill-editor-page/editor-tab/skill-concept-card-editor/worked-example-editor.component.spec.ts b/core/templates/pages/skill-editor-page/editor-tab/skill-concept-card-editor/worked-example-editor.component.spec.ts index 0d1714f8e17d..000a12edfb00 100644 --- a/core/templates/pages/skill-editor-page/editor-tab/skill-concept-card-editor/worked-example-editor.component.spec.ts +++ b/core/templates/pages/skill-editor-page/editor-tab/skill-concept-card-editor/worked-example-editor.component.spec.ts @@ -16,15 +16,15 @@ * @fileoverview Unit tests for WorkedExampleEditorComponent */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ChangeDetectorRef, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { ConceptCard } from 'domain/skill/concept-card.model'; -import { SkillUpdateService } from 'domain/skill/skill-update.service'; -import { Skill } from 'domain/skill/SkillObjectFactory'; -import { WorkedExample } from 'domain/skill/worked-example.model'; -import { SkillEditorStateService } from 'pages/skill-editor-page/services/skill-editor-state.service'; -import { WorkedExampleEditorComponent } from './worked-example-editor.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ChangeDetectorRef, NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {ConceptCard} from 'domain/skill/concept-card.model'; +import {SkillUpdateService} from 'domain/skill/skill-update.service'; +import {Skill} from 'domain/skill/SkillObjectFactory'; +import {WorkedExample} from 'domain/skill/worked-example.model'; +import {SkillEditorStateService} from 'pages/skill-editor-page/services/skill-editor-state.service'; +import {WorkedExampleEditorComponent} from './worked-example-editor.component'; describe('Worked example editor component', () => { let component: WorkedExampleEditorComponent; @@ -36,15 +36,13 @@ describe('Worked example editor component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - WorkedExampleEditorComponent - ], + declarations: [WorkedExampleEditorComponent], providers: [ ChangeDetectorRef, SkillEditorStateService, - SkillUpdateService + SkillUpdateService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -55,8 +53,18 @@ describe('Worked example editor component', () => { skillUpdateService = TestBed.inject(SkillUpdateService); sampleSkill = new Skill( - 'id1', 'description', [], [], {} as ConceptCard, 'en', - 1, 0, 'id1', false, []); + 'id1', + 'description', + [], + [], + {} as ConceptCard, + 'en', + 1, + 0, + 'id1', + false, + [] + ); spyOn(skillEditorStateService, 'getSkill').and.returnValue(sampleSkill); component.isEditable = true; @@ -74,7 +82,7 @@ describe('Worked example editor component', () => { html: 'worked example explanation 1', content_id: 'worked_example_e_1', }; - } + }, } as WorkedExample; component.ngOnInit(); }); @@ -84,7 +92,7 @@ describe('Worked example editor component', () => { expect(component.explanationEditorIsOpen).toBe(false); expect(component.WORKED_EXAMPLE_FORM_SCHEMA).toEqual({ type: 'html', - ui_config: {} + ui_config: {}, }); }); @@ -141,8 +149,10 @@ describe('Worked example editor component', () => { }); it('should save worked example when clicking on save button', () => { - let skillUpdateSpy = spyOn(skillUpdateService, 'updateWorkedExample') - .and.returnValue(); + let skillUpdateSpy = spyOn( + skillUpdateService, + 'updateWorkedExample' + ).and.returnValue(); component.saveWorkedExample(true); @@ -150,12 +160,15 @@ describe('Worked example editor component', () => { sampleSkill, 2, 'worked example question 1', - 'worked example explanation 1'); + 'worked example explanation 1' + ); }); it('should save worked example when clicking on save button', () => { - let skillUpdateSpy = spyOn(skillUpdateService, 'updateWorkedExample') - .and.returnValue(); + let skillUpdateSpy = spyOn( + skillUpdateService, + 'updateWorkedExample' + ).and.returnValue(); component.saveWorkedExample(false); @@ -163,12 +176,12 @@ describe('Worked example editor component', () => { sampleSkill, 2, 'worked example question 1', - 'worked example explanation 1'); + 'worked example explanation 1' + ); }); it('should get schema', () => { - expect(component.getSchema()) - .toEqual(component.WORKED_EXAMPLE_FORM_SCHEMA); + expect(component.getSchema()).toEqual(component.WORKED_EXAMPLE_FORM_SCHEMA); }); it('should update tmpWorkedExampleQuestionHtml', () => { diff --git a/core/templates/pages/skill-editor-page/editor-tab/skill-concept-card-editor/worked-example-editor.component.ts b/core/templates/pages/skill-editor-page/editor-tab/skill-concept-card-editor/worked-example-editor.component.ts index 8a1b62a0351b..b68aaa69150f 100644 --- a/core/templates/pages/skill-editor-page/editor-tab/skill-concept-card-editor/worked-example-editor.component.ts +++ b/core/templates/pages/skill-editor-page/editor-tab/skill-concept-card-editor/worked-example-editor.component.ts @@ -16,26 +16,26 @@ * @fileoverview Component for the worked example editor. */ -import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {ChangeDetectorRef, Component, Input, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; import cloneDeep from 'lodash/cloneDeep'; -import { SkillUpdateService } from 'domain/skill/skill-update.service'; -import { SkillEditorStateService } from 'pages/skill-editor-page/services/skill-editor-state.service'; -import { WorkedExample } from 'domain/skill/worked-example.model'; +import {SkillUpdateService} from 'domain/skill/skill-update.service'; +import {SkillEditorStateService} from 'pages/skill-editor-page/services/skill-editor-state.service'; +import {WorkedExample} from 'domain/skill/worked-example.model'; interface HtmlFormSchema { type: 'html'; - 'ui_config': object; + ui_config: object; } interface Container { - 'workedExampleQuestionHtml': string; - 'workedExampleExplanationHtml': string; + workedExampleQuestionHtml: string; + workedExampleExplanationHtml: string; } @Component({ selector: 'oppia-worked-example-editor', - templateUrl: './worked-example-editor.component.html' + templateUrl: './worked-example-editor.component.html', }) export class WorkedExampleEditorComponent implements OnInit { // These properties are initialized using Angular lifecycle hooks @@ -54,23 +54,21 @@ export class WorkedExampleEditorComponent implements OnInit { questionEditorIsOpen: boolean = false; WORKED_EXAMPLE_FORM_SCHEMA: HtmlFormSchema = { type: 'html', - ui_config: {} + ui_config: {}, }; constructor( private changeDetectorRef: ChangeDetectorRef, private skillEditorStateService: SkillEditorStateService, - private skillUpdateService: SkillUpdateService, + private skillUpdateService: SkillUpdateService ) {} ngOnInit(): void { this.questionEditorIsOpen = false; this.explanationEditorIsOpen = false; this.container = { - workedExampleQuestionHtml: - this.workedExample.getQuestion().html, - workedExampleExplanationHtml: - this.workedExample.getExplanation().html + workedExampleQuestionHtml: this.workedExample.getQuestion().html, + workedExampleExplanationHtml: this.workedExample.getExplanation().html, }; } @@ -96,16 +94,18 @@ export class WorkedExampleEditorComponent implements OnInit { openQuestionEditor(): void { if (this.isEditable) { - this.workedExampleQuestionMemento = - cloneDeep(this.container.workedExampleQuestionHtml); + this.workedExampleQuestionMemento = cloneDeep( + this.container.workedExampleQuestionHtml + ); this.questionEditorIsOpen = true; } } openExplanationEditor(): void { if (this.isEditable) { - this.workedExampleExplanationMemento = - cloneDeep(this.container.workedExampleExplanationHtml); + this.workedExampleExplanationMemento = cloneDeep( + this.container.workedExampleExplanationHtml + ); this.explanationEditorIsOpen = true; } } @@ -116,12 +116,11 @@ export class WorkedExampleEditorComponent implements OnInit { } else { this.explanationEditorIsOpen = false; } - let contentHasChanged = (( + let contentHasChanged = this.workedExampleQuestionMemento !== - this.container.workedExampleQuestionHtml) || ( + this.container.workedExampleQuestionHtml || this.workedExampleExplanationMemento !== - this.container.workedExampleExplanationHtml) - ); + this.container.workedExampleExplanationHtml; this.workedExampleQuestionMemento = null; this.workedExampleExplanationMemento = null; @@ -130,7 +129,8 @@ export class WorkedExampleEditorComponent implements OnInit { this.skillEditorStateService.getSkill(), this.index, this.container.workedExampleQuestionHtml, - this.container.workedExampleExplanationHtml); + this.container.workedExampleExplanationHtml + ); } } @@ -139,7 +139,8 @@ export class WorkedExampleEditorComponent implements OnInit { return; } this.container.workedExampleQuestionHtml = cloneDeep( - this.workedExampleQuestionMemento); + this.workedExampleQuestionMemento + ); this.workedExampleQuestionMemento = null; this.questionEditorIsOpen = false; } @@ -149,11 +150,16 @@ export class WorkedExampleEditorComponent implements OnInit { return; } this.container.workedExampleExplanationHtml = cloneDeep( - this.workedExampleExplanationMemento); + this.workedExampleExplanationMemento + ); this.workedExampleExplanationMemento = null; this.explanationEditorIsOpen = false; } } -angular.module('oppia').directive('oppiaWorkedExampleEditor', - downgradeComponent({component: WorkedExampleEditorComponent})); +angular + .module('oppia') + .directive( + 'oppiaWorkedExampleEditor', + downgradeComponent({component: WorkedExampleEditorComponent}) + ); diff --git a/core/templates/pages/skill-editor-page/editor-tab/skill-description-editor/skill-description-editor.component.spec.ts b/core/templates/pages/skill-editor-page/editor-tab/skill-description-editor/skill-description-editor.component.spec.ts index cf78f81d8698..db49eb3cea6c 100644 --- a/core/templates/pages/skill-editor-page/editor-tab/skill-description-editor/skill-description-editor.component.spec.ts +++ b/core/templates/pages/skill-editor-page/editor-tab/skill-description-editor/skill-description-editor.component.spec.ts @@ -12,16 +12,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ConceptCard } from 'domain/skill/concept-card.model'; -import { SkillRights, SkillRightsBackendDict } from 'domain/skill/skill-rights.model'; - -import { SkillUpdateService } from 'domain/skill/skill-update.service'; -import { Skill, SkillObjectFactory } from 'domain/skill/SkillObjectFactory'; -import { SkillEditorStateService } from 'pages/skill-editor-page/services/skill-editor-state.service'; -import { SkillDescriptionEditorComponent } from './skill-description-editor.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {ConceptCard} from 'domain/skill/concept-card.model'; +import { + SkillRights, + SkillRightsBackendDict, +} from 'domain/skill/skill-rights.model'; + +import {SkillUpdateService} from 'domain/skill/skill-update.service'; +import {Skill, SkillObjectFactory} from 'domain/skill/SkillObjectFactory'; +import {SkillEditorStateService} from 'pages/skill-editor-page/services/skill-editor-state.service'; +import {SkillDescriptionEditorComponent} from './skill-description-editor.component'; /** * @fileoverview Unit tests for SkillDescriptionEditorComponent. @@ -43,11 +46,8 @@ describe('Skill Description Editor Component', () => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], declarations: [SkillDescriptionEditorComponent], - providers: [ - SkillUpdateService, - SkillEditorStateService - ], - schemas: [NO_ERRORS_SCHEMA] + providers: [SkillUpdateService, SkillEditorStateService], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); @@ -63,19 +63,29 @@ describe('Skill Description Editor Component', () => { beforeEach(() => { skillRightsDict = { skill_id: '0', - can_edit_skill_description: true + can_edit_skill_description: true, }; - sampleSkillRights = SkillRights.createFromBackendDict( - skillRightsDict); + sampleSkillRights = SkillRights.createFromBackendDict(skillRightsDict); sampleSkill = new Skill( - 'id1', 'Skill description loading', [], [], {} as ConceptCard, 'en', - 1, 0, 'id1', false, []); + 'id1', + 'Skill description loading', + [], + [], + {} as ConceptCard, + 'en', + 1, + 0, + 'id1', + false, + [] + ); spyOn(skillEditorStateService, 'getSkill').and.returnValue(sampleSkill); - spyOn(skillEditorStateService, 'getSkillRights') - .and.returnValue(sampleSkillRights); + spyOn(skillEditorStateService, 'getSkillRights').and.returnValue( + sampleSkillRights + ); }); afterEach(() => { @@ -100,8 +110,9 @@ describe('Skill Description Editor Component', () => { it('should set properties when initialized', () => { let mockEventEmitter = new EventEmitter(); - spyOnProperty(skillEditorStateService, 'onSkillChange') - .and.returnValue(mockEventEmitter); + spyOnProperty(skillEditorStateService, 'onSkillChange').and.returnValue( + mockEventEmitter + ); expect(component.errorMsg).toBe(''); @@ -116,7 +127,9 @@ describe('Skill Description Editor Component', () => { it('should save skill description successfully', () => { let saveSkillDescriptionSpy = spyOn( - skillUpdateService, 'setSkillDescription').and.callThrough(); + skillUpdateService, + 'setSkillDescription' + ).and.callThrough(); spyOn(component.onSaveDescription, 'emit').and.callThrough(); spyOn(skillObjectFactory, 'hasValidDescription').and.returnValue(true); component.ngOnInit(); @@ -130,7 +143,9 @@ describe('Skill Description Editor Component', () => { it('should not save skill description if it is old description', () => { let saveSkillDescriptionSpy = spyOn( - skillUpdateService, 'setSkillDescription').and.callThrough(); + skillUpdateService, + 'setSkillDescription' + ).and.callThrough(); component.ngOnInit(); // Old Description. expect(component.tmpSkillDescription).toBe('Skill description loading'); @@ -141,7 +156,9 @@ describe('Skill Description Editor Component', () => { it('should not save skill description if it has invalid character', () => { let saveSkillDescriptionSpy = spyOn( - skillUpdateService, 'setSkillDescription').and.callThrough(); + skillUpdateService, + 'setSkillDescription' + ).and.callThrough(); spyOn(skillObjectFactory, 'hasValidDescription').and.returnValue(false); component.ngOnInit(); // Old Description. diff --git a/core/templates/pages/skill-editor-page/editor-tab/skill-description-editor/skill-description-editor.component.ts b/core/templates/pages/skill-editor-page/editor-tab/skill-description-editor/skill-description-editor.component.ts index 79c4dea6e8f3..128c2c357c99 100644 --- a/core/templates/pages/skill-editor-page/editor-tab/skill-description-editor/skill-description-editor.component.ts +++ b/core/templates/pages/skill-editor-page/editor-tab/skill-description-editor/skill-description-editor.component.ts @@ -16,25 +16,30 @@ * @fileoverview Component for the skill description editor. */ -import { Subscription } from 'rxjs'; -import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core'; -import { SkillUpdateService } from 'domain/skill/skill-update.service'; -import { SkillEditorStateService } from 'pages/skill-editor-page/services/skill-editor-state.service'; -import { Skill, SkillObjectFactory } from 'domain/skill/SkillObjectFactory'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { AppConstants } from 'app.constants'; -import { SkillRights } from 'domain/skill/skill-rights.model'; +import {Subscription} from 'rxjs'; +import { + Component, + EventEmitter, + OnDestroy, + OnInit, + Output, +} from '@angular/core'; +import {SkillUpdateService} from 'domain/skill/skill-update.service'; +import {SkillEditorStateService} from 'pages/skill-editor-page/services/skill-editor-state.service'; +import {Skill, SkillObjectFactory} from 'domain/skill/SkillObjectFactory'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {AppConstants} from 'app.constants'; +import {SkillRights} from 'domain/skill/skill-rights.model'; @Component({ selector: 'oppia-skill-description-editor', - templateUrl: './skill-description-editor.component.html' + templateUrl: './skill-description-editor.component.html', }) export class SkillDescriptionEditorComponent implements OnInit, OnDestroy { @Output() onSaveDescription = new EventEmitter(); errorMsg: string = ''; directiveSubscriptions = new Subscription(); - MAX_CHARS_IN_SKILL_DESCRIPTION = ( - AppConstants.MAX_CHARS_IN_SKILL_DESCRIPTION); + MAX_CHARS_IN_SKILL_DESCRIPTION = AppConstants.MAX_CHARS_IN_SKILL_DESCRIPTION; // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see @@ -61,17 +66,17 @@ export class SkillDescriptionEditorComponent implements OnInit, OnDestroy { if (newSkillDescription === this.skill.getDescription()) { return; } - if (this.skillObjectFactory.hasValidDescription( - newSkillDescription)) { + if (this.skillObjectFactory.hasValidDescription(newSkillDescription)) { this.skillDescriptionEditorIsShown = false; this.skillUpdateService.setSkillDescription( this.skill, - newSkillDescription); + newSkillDescription + ); this.onSaveDescription.emit(); } else { - this.errorMsg = ( + this.errorMsg = 'Please use a non-empty description consisting of ' + - 'alphanumeric characters, spaces and/or hyphens.'); + 'alphanumeric characters, spaces and/or hyphens.'; } } @@ -86,12 +91,15 @@ export class SkillDescriptionEditorComponent implements OnInit, OnDestroy { this.errorMsg = ''; this.directiveSubscriptions.add( this.skillEditorStateService.onSkillChange.subscribe( - () => this.tmpSkillDescription = this.skill.getDescription() + () => (this.tmpSkillDescription = this.skill.getDescription()) ) ); } } -angular.module('oppia').directive( - 'oppiaSkillDescriptionEditor', downgradeComponent( - {component: SkillDescriptionEditorComponent})); +angular + .module('oppia') + .directive( + 'oppiaSkillDescriptionEditor', + downgradeComponent({component: SkillDescriptionEditorComponent}) + ); diff --git a/core/templates/pages/skill-editor-page/editor-tab/skill-editor-main-tab.component.spec.ts b/core/templates/pages/skill-editor-page/editor-tab/skill-editor-main-tab.component.spec.ts index dde003a900ba..a9531e6ad254 100644 --- a/core/templates/pages/skill-editor-page/editor-tab/skill-editor-main-tab.component.spec.ts +++ b/core/templates/pages/skill-editor-page/editor-tab/skill-editor-main-tab.component.spec.ts @@ -12,20 +12,24 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Unit tests for the skill editor main tab component. */ -import { ComponentFixture, fakeAsync, flush, TestBed } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { NgbModal, NgbModalRef, NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { SkillEditorMainTabComponent } from './skill-editor-main-tab.component'; -import { UndoRedoService } from 'domain/editor/undo_redo/undo-redo.service'; -import { SkillEditorRoutingService } from '../services/skill-editor-routing.service'; -import { SkillEditorStateService } from '../services/skill-editor-state.service'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + flush, + TestBed, +} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {NgbModal, NgbModalRef, NgbModule} from '@ng-bootstrap/ng-bootstrap'; +import {SkillEditorMainTabComponent} from './skill-editor-main-tab.component'; +import {UndoRedoService} from 'domain/editor/undo_redo/undo-redo.service'; +import {SkillEditorRoutingService} from '../services/skill-editor-routing.service'; +import {SkillEditorStateService} from '../services/skill-editor-state.service'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; class MockNgbModalRef { componentInstance!: { @@ -51,9 +55,9 @@ describe('Skill editor main tab component', () => { UndoRedoService, SkillEditorRoutingService, SkillEditorStateService, - FocusManagerService + FocusManagerService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); @@ -76,18 +80,20 @@ describe('Skill editor main tab component', () => { expect(component.subtopicName).toBeUndefined(); }); - it('should navigate to questions tab when unsaved changes are not present', - () => { - spyOn(undoRedoService, 'getChangeCount').and.returnValue(0); - let routingSpy = spyOn( - skillEditorRoutingService, 'navigateToQuestionsTab').and.callThrough(); - component.createQuestion(), - expect(routingSpy).toHaveBeenCalled(); - let createQuestionEventSpyon = spyOn( - skillEditorRoutingService, 'creatingNewQuestion').and.callThrough(); - component.createQuestion(); - expect(createQuestionEventSpyon).toHaveBeenCalled(); - }); + it('should navigate to questions tab when unsaved changes are not present', () => { + spyOn(undoRedoService, 'getChangeCount').and.returnValue(0); + let routingSpy = spyOn( + skillEditorRoutingService, + 'navigateToQuestionsTab' + ).and.callThrough(); + component.createQuestion(), expect(routingSpy).toHaveBeenCalled(); + let createQuestionEventSpyon = spyOn( + skillEditorRoutingService, + 'creatingNewQuestion' + ).and.callThrough(); + component.createQuestion(); + expect(createQuestionEventSpyon).toHaveBeenCalled(); + }); it('should return if skill has been loaded', () => { expect(component.hasLoadedSkill()).toBe(false); @@ -95,40 +101,45 @@ describe('Skill editor main tab component', () => { expect(component.hasLoadedSkill()).toBe(true); }); - it('should open save changes modal with ngbModal when unsaved changes are' + - ' present', () => { - spyOn(undoRedoService, 'getChangeCount').and.returnValue(1); - const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ - componentInstance: MockNgbModalRef, - result: Promise.resolve() - }) as NgbModalRef; - }); - - component.createQuestion(), - expect(modalSpy).toHaveBeenCalled(); - }); - - it('should close save changes modal with ngbModal when cancel button is' + - ' clicked', () => { - spyOn(undoRedoService, 'getChangeCount').and.returnValue(1); - const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ - componentInstance: MockNgbModalRef, - result: Promise.reject() - }) as NgbModalRef; - }); - - component.createQuestion(), - expect(modalSpy).toHaveBeenCalled(); - }); + it( + 'should open save changes modal with ngbModal when unsaved changes are' + + ' present', + () => { + spyOn(undoRedoService, 'getChangeCount').and.returnValue(1); + const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + componentInstance: MockNgbModalRef, + result: Promise.resolve(), + } as NgbModalRef; + }); + + component.createQuestion(), expect(modalSpy).toHaveBeenCalled(); + } + ); + + it( + 'should close save changes modal with ngbModal when cancel button is' + + ' clicked', + () => { + spyOn(undoRedoService, 'getChangeCount').and.returnValue(1); + const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + componentInstance: MockNgbModalRef, + result: Promise.reject(), + } as NgbModalRef; + }); + + component.createQuestion(), expect(modalSpy).toHaveBeenCalled(); + } + ); it('should return assigned Skill Topic Data', () => { expect(component.assignedSkillTopicData).toBeUndefined(); expect(component.getAssignedSkillTopicData()).toBeNull(); component.assignedSkillTopicData = assignedSkillTopicData; - expect( - component.getAssignedSkillTopicData()).toEqual(assignedSkillTopicData); + expect(component.getAssignedSkillTopicData()).toEqual( + assignedSkillTopicData + ); }); it('should return subtopic name', () => { diff --git a/core/templates/pages/skill-editor-page/editor-tab/skill-editor-main-tab.component.ts b/core/templates/pages/skill-editor-page/editor-tab/skill-editor-main-tab.component.ts index 474e4fffb84b..0ef398ab9b76 100644 --- a/core/templates/pages/skill-editor-page/editor-tab/skill-editor-main-tab.component.ts +++ b/core/templates/pages/skill-editor-page/editor-tab/skill-editor-main-tab.component.ts @@ -16,24 +16,33 @@ * @fileoverview Component for the main tab of the skill editor. */ -import { AfterContentChecked, ChangeDetectorRef, Component, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { SavePendingChangesModalComponent } from 'components/save-pending-changes/save-pending-changes-modal.component'; -import { UndoRedoService } from 'domain/editor/undo_redo/undo-redo.service'; -import { Skill } from 'domain/skill/SkillObjectFactory'; -import { Topic } from 'domain/topic/topic-object.model'; -import { PageTitleService } from 'services/page-title.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { SkillEditorRoutingService } from '../services/skill-editor-routing.service'; -import { AssignedSkillTopicData, SkillEditorStateService } from '../services/skill-editor-state.service'; +import { + AfterContentChecked, + ChangeDetectorRef, + Component, + OnInit, +} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {SavePendingChangesModalComponent} from 'components/save-pending-changes/save-pending-changes-modal.component'; +import {UndoRedoService} from 'domain/editor/undo_redo/undo-redo.service'; +import {Skill} from 'domain/skill/SkillObjectFactory'; +import {Topic} from 'domain/topic/topic-object.model'; +import {PageTitleService} from 'services/page-title.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {SkillEditorRoutingService} from '../services/skill-editor-routing.service'; +import { + AssignedSkillTopicData, + SkillEditorStateService, +} from '../services/skill-editor-state.service'; @Component({ selector: 'oppia-skill-editor-main-tab', - templateUrl: './skill-editor-main-tab.component.html' + templateUrl: './skill-editor-main-tab.component.html', }) -export class SkillEditorMainTabComponent implements OnInit, - AfterContentChecked { +export class SkillEditorMainTabComponent + implements OnInit, AfterContentChecked +{ // These properties below are initialized using Angular lifecycle hooks // where we need to do non-null assertion. For more information see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -53,7 +62,7 @@ export class SkillEditorMainTabComponent implements OnInit, private pageTitleService: PageTitleService, private skillEditorRoutingService: SkillEditorRoutingService, private skillEditorStateService: SkillEditorStateService, - private undoRedoService: UndoRedoService, + private undoRedoService: UndoRedoService ) {} createQuestion(): void { @@ -63,14 +72,13 @@ export class SkillEditorMainTabComponent implements OnInit, // discarded, the misconceptions won't be saved, but there will be // some questions with these now non-existent misconceptions. if (this.undoRedoService.getChangeCount() > 0) { - const modalRef = this.ngbModal.open( - SavePendingChangesModalComponent, { - backdrop: true - }); + const modalRef = this.ngbModal.open(SavePendingChangesModalComponent, { + backdrop: true, + }); - modalRef.componentInstance.body = ( + modalRef.componentInstance.body = 'Please save all pending ' + - 'changes before viewing the questions list.'); + 'changes before viewing the questions list.'; modalRef.result.then(null, () => { // Note to developers: @@ -93,15 +101,16 @@ export class SkillEditorMainTabComponent implements OnInit, this.changeSelectedTopic(this.topicName); return this.assignedSkillTopicData; } - this.assignedSkillTopicData = ( - this.skillEditorStateService.getAssignedSkillTopicData()); + this.assignedSkillTopicData = + this.skillEditorStateService.getAssignedSkillTopicData(); return this.assignedSkillTopicData; } isTopicDropdownEnabled(): boolean { this.topicDropdownEnabled = Boolean( this.assignedSkillTopicData && - Object.keys(this.assignedSkillTopicData).length); + Object.keys(this.assignedSkillTopicData).length + ); return this.topicDropdownEnabled; } @@ -133,7 +142,9 @@ export class SkillEditorMainTabComponent implements OnInit, } } -angular.module('oppia').directive('oppiaSkillEditorMainTab', +angular.module('oppia').directive( + 'oppiaSkillEditorMainTab', downgradeComponent({ - component: SkillEditorMainTabComponent - }) as angular.IDirectiveFactory); + component: SkillEditorMainTabComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/skill-editor-page/editor-tab/skill-misconceptions-editor/misconception-editor.component.spec.ts b/core/templates/pages/skill-editor-page/editor-tab/skill-misconceptions-editor/misconception-editor.component.spec.ts index 9d5a377a75e6..a48db9cf361c 100644 --- a/core/templates/pages/skill-editor-page/editor-tab/skill-misconceptions-editor/misconception-editor.component.spec.ts +++ b/core/templates/pages/skill-editor-page/editor-tab/skill-misconceptions-editor/misconception-editor.component.spec.ts @@ -16,15 +16,15 @@ * @fileoverview Unit tests for MisconceptionEditorComponent */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ChangeDetectorRef, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { ConceptCard } from 'domain/skill/concept-card.model'; -import { Misconception } from 'domain/skill/MisconceptionObjectFactory'; -import { SkillUpdateService } from 'domain/skill/skill-update.service'; -import { Skill } from 'domain/skill/SkillObjectFactory'; -import { SkillEditorStateService } from 'pages/skill-editor-page/services/skill-editor-state.service'; -import { MisconceptionEditorComponent } from './misconception-editor.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ChangeDetectorRef, NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {ConceptCard} from 'domain/skill/concept-card.model'; +import {Misconception} from 'domain/skill/MisconceptionObjectFactory'; +import {SkillUpdateService} from 'domain/skill/skill-update.service'; +import {Skill} from 'domain/skill/SkillObjectFactory'; +import {SkillEditorStateService} from 'pages/skill-editor-page/services/skill-editor-state.service'; +import {MisconceptionEditorComponent} from './misconception-editor.component'; describe('Misconception Editor Component', () => { let component: MisconceptionEditorComponent; @@ -36,15 +36,13 @@ describe('Misconception Editor Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - MisconceptionEditorComponent - ], + declarations: [MisconceptionEditorComponent], providers: [ ChangeDetectorRef, SkillEditorStateService, - SkillUpdateService + SkillUpdateService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -55,8 +53,18 @@ describe('Misconception Editor Component', () => { skillUpdateService = TestBed.inject(SkillUpdateService); sampleSkill = new Skill( - 'id1', 'description', [], [], {} as ConceptCard, 'en', - 1, 0, 'id1', false, []); + 'id1', + 'description', + [], + [], + {} as ConceptCard, + 'en', + 1, + 0, + 'id1', + false, + [] + ); spyOn(skillEditorStateService, 'getSkill').and.returnValue(sampleSkill); component.isEditable = true; @@ -79,7 +87,7 @@ describe('Misconception Editor Component', () => { isMandatory(): boolean { return false; - } + }, } as Misconception; component.ngOnInit(); }); @@ -117,7 +125,9 @@ describe('Misconception Editor Component', () => { it('should save name when clicking on save button', () => { let updateNameSpy = spyOn( - skillUpdateService, 'updateMisconceptionName').and.returnValue(); + skillUpdateService, + 'updateMisconceptionName' + ).and.returnValue(); component.openNameEditor(); // Setting new name. component.container.misconceptionName = 'newName'; @@ -129,7 +139,9 @@ describe('Misconception Editor Component', () => { it('should save notes when clicking on save button', () => { let updateNotesSpy = spyOn( - skillUpdateService, 'updateMisconceptionNotes').and.returnValue(); + skillUpdateService, + 'updateMisconceptionNotes' + ).and.returnValue(); component.openNotesEditor(); // Setting new notes content. component.container.misconceptionNotes = 'newNotes'; @@ -141,7 +153,9 @@ describe('Misconception Editor Component', () => { it('should save feedback when clicking on save button', () => { let updateFeedbackSpy = spyOn( - skillUpdateService, 'updateMisconceptionFeedback').and.returnValue(); + skillUpdateService, + 'updateMisconceptionFeedback' + ).and.returnValue(); component.openFeedbackEditor(); // Setting new feedback content. component.container.misconceptionFeedback = 'newFeedback'; @@ -149,7 +163,11 @@ describe('Misconception Editor Component', () => { component.saveFeedback(); expect(updateFeedbackSpy).toHaveBeenCalledWith( - sampleSkill, 1, 'feedback', 'newFeedback'); + sampleSkill, + 1, + 'feedback', + 'newFeedback' + ); }); it('should close name editor when clicking on cancel button', () => { @@ -188,16 +206,16 @@ describe('Misconception Editor Component', () => { expect(component.feedbackEditorIsOpen).toBe(false); }); - it('should address the misconception\'s updates', () => { + it("should address the misconception's updates", () => { let updatesSpy = spyOn( - skillUpdateService, 'updateMisconceptionMustBeAddressed') - .and.returnValue(); + skillUpdateService, + 'updateMisconceptionMustBeAddressed' + ).and.returnValue(); spyOn(component.onMisconceptionChange, 'emit').and.callThrough(); component.updateMustBeAddressed(); - expect(updatesSpy).toHaveBeenCalledWith( - sampleSkill, 1, true, false); + expect(updatesSpy).toHaveBeenCalledWith(sampleSkill, 1, true, false); expect(component.onMisconceptionChange.emit).toHaveBeenCalled(); }); }); diff --git a/core/templates/pages/skill-editor-page/editor-tab/skill-misconceptions-editor/misconception-editor.component.ts b/core/templates/pages/skill-editor-page/editor-tab/skill-misconceptions-editor/misconception-editor.component.ts index 9dc9bb22e33a..d380380032a0 100644 --- a/core/templates/pages/skill-editor-page/editor-tab/skill-misconceptions-editor/misconception-editor.component.ts +++ b/core/templates/pages/skill-editor-page/editor-tab/skill-misconceptions-editor/misconception-editor.component.ts @@ -16,30 +16,37 @@ * @fileoverview Component for the misconception editor. */ -import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import { + ChangeDetectorRef, + Component, + EventEmitter, + Input, + OnInit, + Output, +} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; import cloneDeep from 'lodash/cloneDeep'; -import { AppConstants } from 'app.constants'; -import { SkillUpdateService } from 'domain/skill/skill-update.service'; -import { SkillEditorStateService } from 'pages/skill-editor-page/services/skill-editor-state.service'; -import { Skill } from 'domain/skill/SkillObjectFactory'; -import { Misconception } from 'domain/skill/MisconceptionObjectFactory'; +import {AppConstants} from 'app.constants'; +import {SkillUpdateService} from 'domain/skill/skill-update.service'; +import {SkillEditorStateService} from 'pages/skill-editor-page/services/skill-editor-state.service'; +import {Skill} from 'domain/skill/SkillObjectFactory'; +import {Misconception} from 'domain/skill/MisconceptionObjectFactory'; interface MisconceptionFormSchema { type: 'html'; - 'ui_config': object; + ui_config: object; } interface Container { - 'misconceptionName': string; - 'misconceptionNotes': string; - 'misconceptionFeedback': string; - 'misconceptionMustBeAddressed': boolean; + misconceptionName: string; + misconceptionNotes: string; + misconceptionFeedback: string; + misconceptionMustBeAddressed: boolean; } @Component({ selector: 'oppia-misconception-editor', - templateUrl: './misconception-editor.component.html' + templateUrl: './misconception-editor.component.html', }) export class MisconceptionEditorComponent implements OnInit { @Output() onMisconceptionChange = new EventEmitter(); @@ -60,26 +67,26 @@ export class MisconceptionEditorComponent implements OnInit { feedbackEditorIsOpen: boolean = false; NOTES_FORM_SCHEMA: MisconceptionFormSchema = { type: 'html', - ui_config: {} + ui_config: {}, }; FEEDBACK_FORM_SCHEMA: MisconceptionFormSchema = { type: 'html', ui_config: { - hide_complex_extensions: 'true' - } + hide_complex_extensions: 'true', + }, }; constructor( private changeDetectorRef: ChangeDetectorRef, private skillEditorStateService: SkillEditorStateService, - private skillUpdateService: SkillUpdateService, + private skillUpdateService: SkillUpdateService ) {} ngOnInit(): void { this.skill = this.skillEditorStateService.getSkill(); - this.MAX_CHARS_IN_MISCONCEPTION_NAME = ( - AppConstants.MAX_CHARS_IN_MISCONCEPTION_NAME); + this.MAX_CHARS_IN_MISCONCEPTION_NAME = + AppConstants.MAX_CHARS_IN_MISCONCEPTION_NAME; this.nameEditorIsOpen = false; this.notesEditorIsOpen = false; this.feedbackEditorIsOpen = false; @@ -88,61 +95,57 @@ export class MisconceptionEditorComponent implements OnInit { misconceptionName: this.misconception.getName(), misconceptionNotes: this.misconception.getNotes(), misconceptionFeedback: this.misconception.getFeedback(), - misconceptionMustBeAddressed: this.misconception.isMandatory() + misconceptionMustBeAddressed: this.misconception.isMandatory(), }; } openNameEditor(): void { if (this.isEditable) { - this.nameMemento = cloneDeep( - this.container.misconceptionName); + this.nameMemento = cloneDeep(this.container.misconceptionName); this.nameEditorIsOpen = true; } } openNotesEditor(): void { if (this.isEditable) { - this.notesMemento = cloneDeep( - this.container.misconceptionNotes); + this.notesMemento = cloneDeep(this.container.misconceptionNotes); this.notesEditorIsOpen = true; } } openFeedbackEditor(): void { if (this.isEditable) { - this.feedbackMemento = cloneDeep( - this.container.misconceptionFeedback); + this.feedbackMemento = cloneDeep(this.container.misconceptionFeedback); this.feedbackEditorIsOpen = true; } } saveName(): void { this.nameEditorIsOpen = false; - let nameHasChanged = ( - this.nameMemento !== - this.container.misconceptionName); + let nameHasChanged = this.nameMemento !== this.container.misconceptionName; if (nameHasChanged) { this.skillUpdateService.updateMisconceptionName( this.skill, this.misconception.getId(), this.nameMemento, - this.container.misconceptionName); + this.container.misconceptionName + ); } } saveNotes(): void { this.notesEditorIsOpen = false; - let notesHasChanged = ( - this.notesMemento !== - this.container.misconceptionNotes); + let notesHasChanged = + this.notesMemento !== this.container.misconceptionNotes; if (notesHasChanged) { this.skillUpdateService.updateMisconceptionNotes( this.skill, this.misconception.getId(), this.notesMemento, - this.container.misconceptionNotes); + this.container.misconceptionNotes + ); } } @@ -151,22 +154,23 @@ export class MisconceptionEditorComponent implements OnInit { this.skill, this.misconception.getId(), !this.container.misconceptionMustBeAddressed, - this.container.misconceptionMustBeAddressed); + this.container.misconceptionMustBeAddressed + ); this.onMisconceptionChange.emit(); } saveFeedback(): void { this.feedbackEditorIsOpen = false; - var feedbackHasChanged = ( - this.feedbackMemento !== - this.container.misconceptionFeedback); + var feedbackHasChanged = + this.feedbackMemento !== this.container.misconceptionFeedback; if (feedbackHasChanged) { this.skillUpdateService.updateMisconceptionFeedback( this.skill, this.misconception.getId(), this.feedbackMemento, - this.container.misconceptionFeedback); + this.container.misconceptionFeedback + ); } } @@ -186,5 +190,9 @@ export class MisconceptionEditorComponent implements OnInit { } } -angular.module('oppia').directive('oppiaMisconceptionEditor', - downgradeComponent({component: MisconceptionEditorComponent})); +angular + .module('oppia') + .directive( + 'oppiaMisconceptionEditor', + downgradeComponent({component: MisconceptionEditorComponent}) + ); diff --git a/core/templates/pages/skill-editor-page/editor-tab/skill-misconceptions-editor/skill-misconceptions-editor.component.spec.ts b/core/templates/pages/skill-editor-page/editor-tab/skill-misconceptions-editor/skill-misconceptions-editor.component.spec.ts index 77fe16624bed..ae22e1ee477b 100644 --- a/core/templates/pages/skill-editor-page/editor-tab/skill-misconceptions-editor/skill-misconceptions-editor.component.spec.ts +++ b/core/templates/pages/skill-editor-page/editor-tab/skill-misconceptions-editor/skill-misconceptions-editor.component.spec.ts @@ -16,19 +16,28 @@ * @fileoverview Unit tests for SkillMisconceptionsEditorComponent. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ChangeDetectorRef, EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { of, Subscription } from 'rxjs'; -import { ConceptCard } from 'domain/skill/concept-card.model'; -import { Misconception, MisconceptionObjectFactory } from 'domain/skill/MisconceptionObjectFactory'; -import { SkillUpdateService } from 'domain/skill/skill-update.service'; -import { Skill } from 'domain/skill/SkillObjectFactory'; -import { DeleteMisconceptionModalComponent } from 'pages/skill-editor-page/modal-templates/delete-misconception-modal.component'; -import { SkillEditorStateService } from 'pages/skill-editor-page/services/skill-editor-state.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { SkillMisconceptionsEditorComponent } from './skill-misconceptions-editor.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ChangeDetectorRef, EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {of, Subscription} from 'rxjs'; +import {ConceptCard} from 'domain/skill/concept-card.model'; +import { + Misconception, + MisconceptionObjectFactory, +} from 'domain/skill/MisconceptionObjectFactory'; +import {SkillUpdateService} from 'domain/skill/skill-update.service'; +import {Skill} from 'domain/skill/SkillObjectFactory'; +import {DeleteMisconceptionModalComponent} from 'pages/skill-editor-page/modal-templates/delete-misconception-modal.component'; +import {SkillEditorStateService} from 'pages/skill-editor-page/services/skill-editor-state.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {SkillMisconceptionsEditorComponent} from './skill-misconceptions-editor.component'; describe('Skill Misconceptions Editor Component', () => { let component: SkillMisconceptionsEditorComponent; @@ -50,9 +59,7 @@ describe('Skill Misconceptions Editor Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - SkillMisconceptionsEditorComponent - ], + declarations: [SkillMisconceptionsEditorComponent], providers: [ ChangeDetectorRef, SkillEditorStateService, @@ -61,11 +68,11 @@ describe('Skill Misconceptions Editor Component', () => { provide: WindowDimensionsService, useValue: { isWindowNarrow: () => true, - getResizeEvent: () => of(resizeEvent) - } - } + getResizeEvent: () => of(resizeEvent), + }, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -79,10 +86,25 @@ describe('Skill Misconceptions Editor Component', () => { misconceptionObjectFactory = TestBed.inject(MisconceptionObjectFactory); sampleSkill = new Skill( - 'id1', 'description', [], [], {} as ConceptCard, 'en', - 1, 0, 'id1', false, []); + 'id1', + 'description', + [], + [], + {} as ConceptCard, + 'en', + 1, + 0, + 'id1', + false, + [] + ); sampleMisconception = misconceptionObjectFactory.create( - 1, 'misconceptionName', 'notes', 'feedback', false); + 1, + 'misconceptionName', + 'notes', + 'feedback', + false + ); sampleSkill._misconceptions = [sampleMisconception]; spyOn(skillEditorStateService, 'getSkill').and.returnValue(sampleSkill); @@ -90,8 +112,9 @@ describe('Skill Misconceptions Editor Component', () => { beforeEach(() => { testSubscriptions = new Subscription(); - testSubscriptions.add(skillEditorStateService.onSkillChange.subscribe( - skillChangeSpy)); + testSubscriptions.add( + skillEditorStateService.onSkillChange.subscribe(skillChangeSpy) + ); }); afterEach(() => { @@ -102,7 +125,8 @@ describe('Skill Misconceptions Editor Component', () => { // Misconception list is shown only when window is not narrow. spyOn(windowDimensionsService, 'isWindowNarrow').and.returnValue(false); spyOnProperty(skillEditorStateService, 'onSkillChange').and.returnValue( - mockOnSkillChangeEmitter); + mockOnSkillChangeEmitter + ); expect(component.skill).toBe(undefined); expect(component.misconceptions).toBeUndefined(); @@ -125,97 +149,118 @@ describe('Skill Misconceptions Editor Component', () => { expect(component.getMisconceptionsChange.emit).toHaveBeenCalled(); }); - it('should toggle misconceptionList when toggle ' + - 'button is clicked', () => { - spyOn(windowDimensionsService, 'isWindowNarrow').and.returnValue(true); - component.misconceptionsListIsShown = false; - - component.toggleMisconceptionLists(); - expect(component.misconceptionsListIsShown).toBe(true); - - component.toggleMisconceptionLists(); - expect(component.misconceptionsListIsShown).toBe(false); - }); - - it('should open add misconception modal when clicking on add ' + - 'button', fakeAsync(() => { - component.skill = sampleSkill; - spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.resolve({ - misconception: sampleMisconception - }) - } as NgbModalRef); - spyOn(skillUpdateService, 'addMisconception').and.callThrough(); - - component.openAddMisconceptionModal(); - tick(); - - expect(ngbModal.open).toHaveBeenCalled(); - expect(skillUpdateService.addMisconception).toHaveBeenCalledWith( - sampleSkill, sampleMisconception); - })); - - it('should close add misconception modal when clicking on close ' + - 'button', fakeAsync(() => { - spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.reject() - } as NgbModalRef); - spyOn(skillUpdateService, 'addMisconception').and.callThrough(); - - component.openAddMisconceptionModal(); - - expect(ngbModal.open).toHaveBeenCalled(); - expect(skillUpdateService.addMisconception).not.toHaveBeenCalled(); - })); - - it('should open delete misconception modal when clicking on delete ' + - 'button', fakeAsync(() => { - spyOn(ngbModal, 'open').and.returnValue({ - componentInstance: { - index: 'index' - }, - result: Promise.resolve({ - result: { - id: 'id' - } - }) - } as NgbModalRef); - spyOn(skillUpdateService, 'deleteMisconception').and.returnValue(); - - component.ngOnInit(); - component.openDeleteMisconceptionModal(1, '1'); - tick(); - - expect(ngbModal.open).toHaveBeenCalledWith( - DeleteMisconceptionModalComponent, {backdrop: 'static'}); - expect(skillUpdateService.deleteMisconception).toHaveBeenCalled(); - })); - - it('should close delete misconception modal when clicking on ' + - 'close button', fakeAsync(() => { - spyOn(ngbModal, 'open').and.returnValue({ - componentInstance: { - index: 'index' - }, - result: Promise.reject() - } as NgbModalRef); - spyOn(skillUpdateService, 'deleteMisconception').and.callThrough(); - - component.ngOnInit(); - component.openDeleteMisconceptionModal(1, '1'); - tick(); - - expect(ngbModal.open).toHaveBeenCalledWith( - DeleteMisconceptionModalComponent, {backdrop: 'static'}); - expect(skillUpdateService.deleteMisconception).not.toHaveBeenCalled(); - })); - - it('should return misconception name given input as misconception ' + - 'when calling \'getMisconceptionSummary \'', () => { - let name = component.getMisconceptionSummary(sampleMisconception); - - expect(name).toBe('misconceptionName'); - }); + it( + 'should toggle misconceptionList when toggle ' + 'button is clicked', + () => { + spyOn(windowDimensionsService, 'isWindowNarrow').and.returnValue(true); + component.misconceptionsListIsShown = false; + + component.toggleMisconceptionLists(); + expect(component.misconceptionsListIsShown).toBe(true); + + component.toggleMisconceptionLists(); + expect(component.misconceptionsListIsShown).toBe(false); + } + ); + + it( + 'should open add misconception modal when clicking on add ' + 'button', + fakeAsync(() => { + component.skill = sampleSkill; + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.resolve({ + misconception: sampleMisconception, + }), + } as NgbModalRef); + spyOn(skillUpdateService, 'addMisconception').and.callThrough(); + + component.openAddMisconceptionModal(); + tick(); + + expect(ngbModal.open).toHaveBeenCalled(); + expect(skillUpdateService.addMisconception).toHaveBeenCalledWith( + sampleSkill, + sampleMisconception + ); + }) + ); + + it( + 'should close add misconception modal when clicking on close ' + 'button', + fakeAsync(() => { + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.reject(), + } as NgbModalRef); + spyOn(skillUpdateService, 'addMisconception').and.callThrough(); + + component.openAddMisconceptionModal(); + + expect(ngbModal.open).toHaveBeenCalled(); + expect(skillUpdateService.addMisconception).not.toHaveBeenCalled(); + }) + ); + + it( + 'should open delete misconception modal when clicking on delete ' + + 'button', + fakeAsync(() => { + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: { + index: 'index', + }, + result: Promise.resolve({ + result: { + id: 'id', + }, + }), + } as NgbModalRef); + spyOn(skillUpdateService, 'deleteMisconception').and.returnValue(); + + component.ngOnInit(); + component.openDeleteMisconceptionModal(1, '1'); + tick(); + + expect(ngbModal.open).toHaveBeenCalledWith( + DeleteMisconceptionModalComponent, + {backdrop: 'static'} + ); + expect(skillUpdateService.deleteMisconception).toHaveBeenCalled(); + }) + ); + + it( + 'should close delete misconception modal when clicking on ' + + 'close button', + fakeAsync(() => { + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: { + index: 'index', + }, + result: Promise.reject(), + } as NgbModalRef); + spyOn(skillUpdateService, 'deleteMisconception').and.callThrough(); + + component.ngOnInit(); + component.openDeleteMisconceptionModal(1, '1'); + tick(); + + expect(ngbModal.open).toHaveBeenCalledWith( + DeleteMisconceptionModalComponent, + {backdrop: 'static'} + ); + expect(skillUpdateService.deleteMisconception).not.toHaveBeenCalled(); + }) + ); + + it( + 'should return misconception name given input as misconception ' + + "when calling 'getMisconceptionSummary '", + () => { + let name = component.getMisconceptionSummary(sampleMisconception); + + expect(name).toBe('misconceptionName'); + } + ); it('should change active misconception index', () => { component.activeMisconceptionIndex = 1; @@ -225,19 +270,21 @@ describe('Skill Misconceptions Editor Component', () => { expect(component.activeMisconceptionIndex).toBe(2); }); - it('should set active misconception index to null if ' + - 'oldIndex is newIndex', () => { - component.activeMisconceptionIndex = 1; + it( + 'should set active misconception index to null if ' + + 'oldIndex is newIndex', + () => { + component.activeMisconceptionIndex = 1; - component.changeActiveMisconceptionIndex(1); + component.changeActiveMisconceptionIndex(1); - expect(component.activeMisconceptionIndex).toBe(null); - }); + expect(component.activeMisconceptionIndex).toBe(null); + } + ); it('should toggle skill editor card on clicking', () => { component.skillEditorCardIsShown = true; - spyOn(windowDimensionsService, 'isWindowNarrow') - .and.returnValue(true); + spyOn(windowDimensionsService, 'isWindowNarrow').and.returnValue(true); component.toggleSkillEditorCard(); @@ -251,7 +298,8 @@ describe('Skill Misconceptions Editor Component', () => { it('should show Misconceptions list when the window is narrow', () => { spyOn(windowDimensionsService, 'isWindowNarrow').and.returnValue(true); spyOn(windowDimensionsService, 'getResizeEvent').and.returnValue( - mockEventEmitter); + mockEventEmitter + ); component.windowIsNarrow = false; expect(component.misconceptionsListIsShown).toBe(false); diff --git a/core/templates/pages/skill-editor-page/editor-tab/skill-misconceptions-editor/skill-misconceptions-editor.component.ts b/core/templates/pages/skill-editor-page/editor-tab/skill-misconceptions-editor/skill-misconceptions-editor.component.ts index e8870d630499..908d926bacce 100644 --- a/core/templates/pages/skill-editor-page/editor-tab/skill-misconceptions-editor/skill-misconceptions-editor.component.ts +++ b/core/templates/pages/skill-editor-page/editor-tab/skill-misconceptions-editor/skill-misconceptions-editor.component.ts @@ -16,21 +16,21 @@ * @fileoverview Component for the skill misconceptions editor. */ -import { Component, EventEmitter, OnInit, Output } from '@angular/core'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { Subscription } from 'rxjs'; -import { AddMisconceptionModalComponent } from 'pages/skill-editor-page/modal-templates/add-misconception-modal.component'; -import { DeleteMisconceptionModalComponent } from 'pages/skill-editor-page/modal-templates/delete-misconception-modal.component'; -import { SkillEditorStateService } from 'pages/skill-editor-page/services/skill-editor-state.service'; -import { SkillUpdateService } from 'domain/skill/skill-update.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { Misconception } from 'domain/skill/MisconceptionObjectFactory'; -import { Skill } from 'domain/skill/SkillObjectFactory'; +import {Component, EventEmitter, OnInit, Output} from '@angular/core'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {Subscription} from 'rxjs'; +import {AddMisconceptionModalComponent} from 'pages/skill-editor-page/modal-templates/add-misconception-modal.component'; +import {DeleteMisconceptionModalComponent} from 'pages/skill-editor-page/modal-templates/delete-misconception-modal.component'; +import {SkillEditorStateService} from 'pages/skill-editor-page/services/skill-editor-state.service'; +import {SkillUpdateService} from 'domain/skill/skill-update.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {Misconception} from 'domain/skill/MisconceptionObjectFactory'; +import {Skill} from 'domain/skill/SkillObjectFactory'; @Component({ selector: 'oppia-skill-misconceptions-editor', - templateUrl: './skill-misconceptions-editor.component.html' + templateUrl: './skill-misconceptions-editor.component.html', }) export class SkillMisconceptionsEditorComponent implements OnInit { @Output() getMisconceptionsChange = new EventEmitter(); @@ -53,29 +53,28 @@ export class SkillMisconceptionsEditorComponent implements OnInit { private ngbModal: NgbModal, private skillEditorStateService: SkillEditorStateService, private skillUpdateService: SkillUpdateService, - private windowDimensionsService: WindowDimensionsService, + private windowDimensionsService: WindowDimensionsService ) {} ngOnInit(): void { this.skillEditorCardIsShown = true; this.windowIsNarrow = this.windowDimensionsService.isWindowNarrow(); this.directiveSubscriptions.add( - this.windowDimensionsService.getResizeEvent().subscribe( - () => { - this.windowIsNarrow = this.windowDimensionsService.isWindowNarrow(); - this.misconceptionsListIsShown = ( - !this.windowDimensionsService.isWindowNarrow()); - } - ) + this.windowDimensionsService.getResizeEvent().subscribe(() => { + this.windowIsNarrow = this.windowDimensionsService.isWindowNarrow(); + this.misconceptionsListIsShown = + !this.windowDimensionsService.isWindowNarrow(); + }) ); this.skill = this.skillEditorStateService.getSkill(); - this.misconceptionsListIsShown = ( - !this.windowDimensionsService.isWindowNarrow()); + this.misconceptionsListIsShown = + !this.windowDimensionsService.isWindowNarrow(); this.misconceptions = this.skill.getMisconceptions(); this.directiveSubscriptions.add( this.skillEditorStateService.onSkillChange.subscribe( - () => this.misconceptions = this.skill.getMisconceptions()) + () => (this.misconceptions = this.skill.getMisconceptions()) + ) ); } @@ -97,35 +96,47 @@ export class SkillMisconceptionsEditorComponent implements OnInit { openDeleteMisconceptionModal(index: number, evt: string): void { const modalInstance: NgbModalRef = this.ngbModal.open( - DeleteMisconceptionModalComponent, { + DeleteMisconceptionModalComponent, + { backdrop: 'static', - }); + } + ); modalInstance.componentInstance.index = index; - modalInstance.result.then((result) => { - this.skillUpdateService.deleteMisconception(this.skill, result.id); - this.misconceptions = this.skill.getMisconceptions(); - this.activeMisconceptionIndex = null; - this.getMisconceptionsChange.emit(); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + modalInstance.result.then( + result => { + this.skillUpdateService.deleteMisconception(this.skill, result.id); + this.misconceptions = this.skill.getMisconceptions(); + this.activeMisconceptionIndex = null; + this.getMisconceptionsChange.emit(); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } openAddMisconceptionModal(): void { - this.ngbModal.open(AddMisconceptionModalComponent, { - backdrop: 'static' - }).result.then((result) => { - this.skillUpdateService.addMisconception( - this.skill, result.misconception); - this.misconceptions = this.skill.getMisconceptions(); - this.getMisconceptionsChange.emit(); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + this.ngbModal + .open(AddMisconceptionModalComponent, { + backdrop: 'static', + }) + .result.then( + result => { + this.skillUpdateService.addMisconception( + this.skill, + result.misconception + ); + this.misconceptions = this.skill.getMisconceptions(); + this.getMisconceptionsChange.emit(); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } onMisconceptionChange(): void { @@ -134,8 +145,7 @@ export class SkillMisconceptionsEditorComponent implements OnInit { toggleMisconceptionLists(): void { if (this.windowDimensionsService.isWindowNarrow()) { - this.misconceptionsListIsShown = ( - !this.misconceptionsListIsShown); + this.misconceptionsListIsShown = !this.misconceptionsListIsShown; } } @@ -146,5 +156,9 @@ export class SkillMisconceptionsEditorComponent implements OnInit { } } -angular.module('oppia').directive('oppiaSkillMisconceptionsEditor', - downgradeComponent({component: SkillMisconceptionsEditorComponent})); +angular + .module('oppia') + .directive( + 'oppiaSkillMisconceptionsEditor', + downgradeComponent({component: SkillMisconceptionsEditorComponent}) + ); diff --git a/core/templates/pages/skill-editor-page/editor-tab/skill-prerequisite-skills-editor/skill-prerequisite-skills-editor.component.spec.ts b/core/templates/pages/skill-editor-page/editor-tab/skill-prerequisite-skills-editor/skill-prerequisite-skills-editor.component.spec.ts index a3c9d67190fa..6d78f4dc99fc 100644 --- a/core/templates/pages/skill-editor-page/editor-tab/skill-prerequisite-skills-editor/skill-prerequisite-skills-editor.component.spec.ts +++ b/core/templates/pages/skill-editor-page/editor-tab/skill-prerequisite-skills-editor/skill-prerequisite-skills-editor.component.spec.ts @@ -12,25 +12,33 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Unit tests for the skill prerequisite skills editor. */ -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { SkillPrerequisiteSkillsEditorComponent } from './skill-prerequisite-skills-editor.component'; -import { SkillUpdateService } from 'domain/skill/skill-update.service'; -import { SkillEditorStateService } from 'pages/skill-editor-page/services/skill-editor-state.service'; -import { AlertsService } from 'services/alerts.service'; -import { TopicsAndSkillsDashboardBackendApiService, TopicsAndSkillDashboardData } from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { Skill } from 'domain/skill/SkillObjectFactory'; -import { SkillObjectFactory } from 'domain/skill/SkillObjectFactory'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { SkillSummaryBackendDict } from 'domain/skill/skill-summary.model'; -import { of } from 'rxjs'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {SkillPrerequisiteSkillsEditorComponent} from './skill-prerequisite-skills-editor.component'; +import {SkillUpdateService} from 'domain/skill/skill-update.service'; +import {SkillEditorStateService} from 'pages/skill-editor-page/services/skill-editor-state.service'; +import {AlertsService} from 'services/alerts.service'; +import { + TopicsAndSkillsDashboardBackendApiService, + TopicsAndSkillDashboardData, +} from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {Skill} from 'domain/skill/SkillObjectFactory'; +import {SkillObjectFactory} from 'domain/skill/SkillObjectFactory'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {SkillSummaryBackendDict} from 'domain/skill/skill-summary.model'; +import {of} from 'rxjs'; describe('Skill editor main tab Component', () => { let component: SkillPrerequisiteSkillsEditorComponent; @@ -38,8 +46,7 @@ describe('Skill editor main tab Component', () => { let skillUpdateService: SkillUpdateService; let skillEditorStateService: SkillEditorStateService; let alertsService: AlertsService; - let topicsAndSkillsDashboardBackendApiService: - TopicsAndSkillsDashboardBackendApiService; + let topicsAndSkillsDashboardBackendApiService: TopicsAndSkillsDashboardBackendApiService; let windowDimensionsService: WindowDimensionsService; let ngbModal: NgbModal; let skillObjectFactory: SkillObjectFactory; @@ -52,12 +59,8 @@ describe('Skill editor main tab Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - SkillPrerequisiteSkillsEditorComponent - ], + imports: [HttpClientTestingModule], + declarations: [SkillPrerequisiteSkillsEditorComponent], providers: [ SkillUpdateService, SkillEditorStateService, @@ -66,12 +69,12 @@ describe('Skill editor main tab Component', () => { provide: WindowDimensionsService, useValue: { isWindowNarrow: () => true, - getResizeEvent: () => of(resizeEvent) - } + getResizeEvent: () => of(resizeEvent), + }, }, - NgbModal + NgbModal, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -82,8 +85,9 @@ describe('Skill editor main tab Component', () => { skillUpdateService = TestBed.inject(SkillUpdateService); skillEditorStateService = TestBed.inject(SkillEditorStateService); alertsService = TestBed.inject(AlertsService); - topicsAndSkillsDashboardBackendApiService = - TestBed.inject(TopicsAndSkillsDashboardBackendApiService); + topicsAndSkillsDashboardBackendApiService = TestBed.inject( + TopicsAndSkillsDashboardBackendApiService + ); windowDimensionsService = TestBed.inject(WindowDimensionsService); ngbModal = TestBed.inject(NgbModal); skillObjectFactory = TestBed.inject(SkillObjectFactory); @@ -96,7 +100,7 @@ describe('Skill editor main tab Component', () => { misconception_count: 3, worked_examples_count: 3, skill_model_created_on: 1593138898626.193, - skill_model_last_updated: 1593138898626.193 + skill_model_last_updated: 1593138898626.193, }; let misconceptionDict1 = { @@ -104,12 +108,12 @@ describe('Skill editor main tab Component', () => { name: 'test name', notes: 'test notes', feedback: 'test feedback', - must_be_addressed: true + must_be_addressed: true, }; const rubricDict = { difficulty: 'medium', - explanations: ['explanation'] + explanations: ['explanation'], }; const skillContentsDict = { @@ -119,8 +123,8 @@ describe('Skill editor main tab Component', () => { }, worked_examples: [], recorded_voiceovers: { - voiceovers_mapping: {} - } + voiceovers_mapping: {}, + }, }; sampleSkill = skillObjectFactory.createFromBackendDict({ @@ -134,16 +138,14 @@ describe('Skill editor main tab Component', () => { next_misconception_id: 3, prerequisite_skill_ids: ['skill_1'], superseding_skill_id: 'skill0', - all_questions_merged: true + all_questions_merged: true, }); topicAndSkillsDashboardDataBackendDict = { - allClassroomNames: [ - 'math' - ], + allClassroomNames: ['math'], categorizedSkillsDict: { 'Empty Topic': { - uncategorized: [] + uncategorized: [], }, 'Dummy Topic 1': { uncategorized: [ @@ -155,8 +157,8 @@ describe('Skill editor main tab Component', () => { }, getDescription(): string { return this.description; - } - } + }, + }, ], 'Dummy Subtopic Title': [ { @@ -167,10 +169,10 @@ describe('Skill editor main tab Component', () => { }, getDescription(): string { return this.description; - } - } - ] - } + }, + }, + ], + }, }, topicSummaries: [ { @@ -262,8 +264,8 @@ describe('Skill editor main tab Component', () => { }, getPublishedChaptersCounts(): number[] { return this.publishedChaptersCounts; - } - } + }, + }, ], canDeleteSkill: true, untriagedSkillSummaries: [ @@ -275,8 +277,8 @@ describe('Skill editor main tab Component', () => { skillModelLastUpdated: 1623851495022.942, workedExamplesCount: 0, id: '4P77sLaU14DE', - misconceptionCount: 0 - } + misconceptionCount: 0, + }, ], totalSkillCount: 3, canCreateTopic: true, @@ -290,7 +292,7 @@ describe('Skill editor main tab Component', () => { skillModelLastUpdated: 1623851493737.808, workedExamplesCount: 0, id: 'BBB6dzfb5pPt', - misconceptionCount: 0 + misconceptionCount: 0, }, { version: 1, @@ -300,24 +302,25 @@ describe('Skill editor main tab Component', () => { skillModelLastUpdated: 1623851494780.529, workedExamplesCount: 0, id: 'D1FdmljJNXdt', - misconceptionCount: 0 - } + misconceptionCount: 0, + }, ], canDeleteTopic: true, }; spyOn(skillEditorStateService, 'getSkill').and.returnValue(sampleSkill); - spyOn(topicsAndSkillsDashboardBackendApiService, 'fetchDashboardDataAsync') - .and.resolveTo(topicAndSkillsDashboardDataBackendDict); + spyOn( + topicsAndSkillsDashboardBackendApiService, + 'fetchDashboardDataAsync' + ).and.resolveTo(topicAndSkillsDashboardDataBackendDict); fixture.detectChanges(); }); it('should fetch skill when initialized', fakeAsync(() => { - spyOn( - skillEditorStateService, 'getGroupedSkillSummaries').and.returnValue({ + spyOn(skillEditorStateService, 'getGroupedSkillSummaries').and.returnValue({ current: [], - others: [skillSummaryDict] + others: [skillSummaryDict], }); component.ngOnInit(); @@ -326,34 +329,39 @@ describe('Skill editor main tab Component', () => { expect(component.skill).toEqual(sampleSkill); })); - it('should remove skill id when calling \'removeSkillId\'', () => { - let deleteSpy = spyOn(skillUpdateService, 'deletePrerequisiteSkill') - .and.callThrough(); + it("should remove skill id when calling 'removeSkillId'", () => { + let deleteSpy = spyOn( + skillUpdateService, + 'deletePrerequisiteSkill' + ).and.callThrough(); component.removeSkillId('xyz'); expect(deleteSpy).toHaveBeenCalled(); }); - it('should return skill editor url when calling ' + - '\'getSkillEditorUrl\'', () => { - let result = component.getSkillEditorUrl('skillId'); + it( + 'should return skill editor url when calling ' + "'getSkillEditorUrl'", + () => { + let result = component.getSkillEditorUrl('skillId'); - expect(result).toBe('/skill_editor/skillId'); - }); + expect(result).toBe('/skill_editor/skillId'); + } + ); - it('should toggle prerequisite skills ' + - '\'togglePrerequisiteSkills\'', () => { - component.prerequisiteSkillsAreShown = false; - spyOn(windowDimensionsService, 'isWindowNarrow') - .and.returnValue(true); + it( + 'should toggle prerequisite skills ' + "'togglePrerequisiteSkills'", + () => { + component.prerequisiteSkillsAreShown = false; + spyOn(windowDimensionsService, 'isWindowNarrow').and.returnValue(true); - component.togglePrerequisiteSkills(); - expect(component.prerequisiteSkillsAreShown).toBe(true); + component.togglePrerequisiteSkills(); + expect(component.prerequisiteSkillsAreShown).toBe(true); - component.togglePrerequisiteSkills(); - expect(component.prerequisiteSkillsAreShown).toBe(false); - }); + component.togglePrerequisiteSkills(); + expect(component.prerequisiteSkillsAreShown).toBe(false); + } + ); it('should show skill description on skill-editor tab', fakeAsync(() => { component.ngOnInit(); @@ -370,72 +378,89 @@ describe('Skill editor main tab Component', () => { })); describe('while adding a skill', () => { - it('should show info message if we try ' + - 'to add a prerequisite skill to itself', fakeAsync(() => { - spyOn(ngbModal, 'open').and.callFake(() => { - return ({ - componentInstance: {}, - result: Promise.resolve({ - id: 'skill1' - }) - }) as NgbModalRef; - }); - let alertsSpy = spyOn(alertsService, 'addInfoMessage') - .and.callThrough(); - - component.skill = sampleSkill; - component.addSkill(); - tick(); - - expect(alertsSpy).toHaveBeenCalledWith( - 'A skill cannot be a prerequisite of itself', 5000); - })); - - it('should show info message if we try to add a prerequisite ' + - 'skill which has already been added', fakeAsync(() => { - spyOn(ngbModal, 'open').and.callFake(() => { - return ({ - componentInstance: {}, - result: Promise.resolve({ - id: 'skill_1' - }) - }) as NgbModalRef; - }); - - let alertsSpy = spyOn(alertsService, 'addInfoMessage') - .and.callThrough(); - - component.skill = sampleSkill; - component.addSkill(); - tick(); - - expect(alertsSpy).toHaveBeenCalledWith( - 'Given skill is already a prerequisite skill', 5000); - })); - - it('should add skill sucessfully when calling ' + - '\'addSkill\'', fakeAsync(() => { - let modalSpy = spyOn(ngbModal, 'open').and.callFake(() => { - return ({ - componentInstance: {}, - result: Promise.resolve({ - id: 'skillId' - }) - }) as NgbModalRef; - }); - - component.skill = sampleSkill; - component.addSkill(); - tick(); - - expect(modalSpy).toHaveBeenCalled(); - })); + it( + 'should show info message if we try ' + + 'to add a prerequisite skill to itself', + fakeAsync(() => { + spyOn(ngbModal, 'open').and.callFake(() => { + return { + componentInstance: {}, + result: Promise.resolve({ + id: 'skill1', + }), + } as NgbModalRef; + }); + let alertsSpy = spyOn( + alertsService, + 'addInfoMessage' + ).and.callThrough(); + + component.skill = sampleSkill; + component.addSkill(); + tick(); + + expect(alertsSpy).toHaveBeenCalledWith( + 'A skill cannot be a prerequisite of itself', + 5000 + ); + }) + ); + + it( + 'should show info message if we try to add a prerequisite ' + + 'skill which has already been added', + fakeAsync(() => { + spyOn(ngbModal, 'open').and.callFake(() => { + return { + componentInstance: {}, + result: Promise.resolve({ + id: 'skill_1', + }), + } as NgbModalRef; + }); + + let alertsSpy = spyOn( + alertsService, + 'addInfoMessage' + ).and.callThrough(); + + component.skill = sampleSkill; + component.addSkill(); + tick(); + + expect(alertsSpy).toHaveBeenCalledWith( + 'Given skill is already a prerequisite skill', + 5000 + ); + }) + ); + + it( + 'should add skill sucessfully when calling ' + "'addSkill'", + fakeAsync(() => { + let modalSpy = spyOn(ngbModal, 'open').and.callFake(() => { + return { + componentInstance: {}, + result: Promise.resolve({ + id: 'skillId', + }), + } as NgbModalRef; + }); + + component.skill = sampleSkill; + component.addSkill(); + tick(); + + expect(modalSpy).toHaveBeenCalled(); + }) + ); }); it('should check if window is narrow when user resizes window', () => { spyOn(windowDimensionsService, 'isWindowNarrow').and.returnValue(false); spyOn(windowDimensionsService, 'getResizeEvent').and.returnValue( - mockEventEmitter); + mockEventEmitter + ); expect(component.prerequisiteSkillsAreShown).toBeFalse(); @@ -449,8 +474,7 @@ describe('Skill editor main tab Component', () => { it('should toggle skill editor card on clicking', () => { component.skillEditorCardIsShown = true; - spyOn(windowDimensionsService, 'isWindowNarrow') - .and.returnValue(true); + spyOn(windowDimensionsService, 'isWindowNarrow').and.returnValue(true); component.toggleSkillEditorCard(); @@ -464,7 +488,8 @@ describe('Skill editor main tab Component', () => { it('should show Prerequisites list when the window is narrow', () => { spyOn(windowDimensionsService, 'isWindowNarrow').and.returnValue(true); spyOn(windowDimensionsService, 'getResizeEvent').and.returnValue( - mockEventEmitter); + mockEventEmitter + ); component.windowIsNarrow = false; expect(component.prerequisiteSkillsAreShown).toBe(false); diff --git a/core/templates/pages/skill-editor-page/editor-tab/skill-prerequisite-skills-editor/skill-prerequisite-skills-editor.component.ts b/core/templates/pages/skill-editor-page/editor-tab/skill-prerequisite-skills-editor/skill-prerequisite-skills-editor.component.ts index f24c959bb802..2e17dcc6965a 100644 --- a/core/templates/pages/skill-editor-page/editor-tab/skill-prerequisite-skills-editor/skill-prerequisite-skills-editor.component.ts +++ b/core/templates/pages/skill-editor-page/editor-tab/skill-prerequisite-skills-editor/skill-prerequisite-skills-editor.component.ts @@ -16,28 +16,30 @@ * @fileoverview Component for the skill prerequisite skills editor. */ -import { CategorizedSkills } from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; -import { GroupedSkillSummaries } from 'pages/skill-editor-page/services/skill-editor-state.service'; -import { SkillSummary } from 'domain/skill/skill-summary.model'; -import { SelectSkillModalComponent } from 'components/skill-selector/select-skill-modal.component'; -import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { Skill } from 'domain/skill/SkillObjectFactory'; -import { SkillUpdateService } from 'domain/skill/skill-update.service'; -import { SkillEditorStateService } from 'pages/skill-editor-page/services/skill-editor-state.service'; -import { AlertsService } from 'services/alerts.service'; -import { TopicsAndSkillsDashboardBackendApiService, TopicsAndSkillDashboardData } from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { Component, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { Subscription } from 'rxjs'; +import {CategorizedSkills} from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; +import {GroupedSkillSummaries} from 'pages/skill-editor-page/services/skill-editor-state.service'; +import {SkillSummary} from 'domain/skill/skill-summary.model'; +import {SelectSkillModalComponent} from 'components/skill-selector/select-skill-modal.component'; +import {NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {Skill} from 'domain/skill/SkillObjectFactory'; +import {SkillUpdateService} from 'domain/skill/skill-update.service'; +import {SkillEditorStateService} from 'pages/skill-editor-page/services/skill-editor-state.service'; +import {AlertsService} from 'services/alerts.service'; +import { + TopicsAndSkillsDashboardBackendApiService, + TopicsAndSkillDashboardData, +} from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {Component, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {Subscription} from 'rxjs'; @Component({ selector: 'oppia-skill-prerequisite-skills-editor', - templateUrl: './skill-prerequisite-skills-editor.component.html' + templateUrl: './skill-prerequisite-skills-editor.component.html', }) -export class SkillPrerequisiteSkillsEditorComponent -implements OnInit { +export class SkillPrerequisiteSkillsEditorComponent implements OnInit { // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -56,8 +58,7 @@ implements OnInit { private skillUpdateService: SkillUpdateService, private skillEditorStateService: SkillEditorStateService, private alertsService: AlertsService, - private topicsAndSkillsDashboardBackendApiService: - TopicsAndSkillsDashboardBackendApiService, + private topicsAndSkillsDashboardBackendApiService: TopicsAndSkillsDashboardBackendApiService, private windowDimensionsService: WindowDimensionsService, private ngbModal: NgbModal ) {} @@ -73,58 +74,65 @@ implements OnInit { addSkill(): void { // This contains the summaries of skill in the same topic as // the current skill as the initial entries followed by the others. - const skillsInSameTopicCount = - this.groupedSkillSummaries.current.length; + const skillsInSameTopicCount = this.groupedSkillSummaries.current.length; const sortedSkillSummaries = this.groupedSkillSummaries.current.concat( - this.groupedSkillSummaries.others); + this.groupedSkillSummaries.others + ); const allowSkillsFromOtherTopics = true; const modalRef: NgbModalRef = this.ngbModal.open( - SelectSkillModalComponent, { + SelectSkillModalComponent, + { backdrop: 'static', windowClass: 'skill-select-modal', - size: 'xl' - }); + size: 'xl', + } + ); modalRef.componentInstance.skillSummaries = sortedSkillSummaries; - modalRef.componentInstance.skillsInSameTopicCount = ( - skillsInSameTopicCount); + modalRef.componentInstance.skillsInSameTopicCount = skillsInSameTopicCount; modalRef.componentInstance.categorizedSkills = this.categorizedSkills; - modalRef.componentInstance.allowSkillsFromOtherTopics = ( - allowSkillsFromOtherTopics); - modalRef.componentInstance.untriagedSkillSummaries = ( - this.untriagedSkillSummaries); + modalRef.componentInstance.allowSkillsFromOtherTopics = + allowSkillsFromOtherTopics; + modalRef.componentInstance.untriagedSkillSummaries = + this.untriagedSkillSummaries; const whenResolved = (summary: SkillSummary): void => { let skillId = summary.id; if (skillId === this.skill.getId()) { this.alertsService.addInfoMessage( - 'A skill cannot be a prerequisite of itself', 5000); + 'A skill cannot be a prerequisite of itself', + 5000 + ); return; } for (let idx in this.skill.getPrerequisiteSkillIds()) { if (this.skill.getPrerequisiteSkillIds()[idx] === skillId) { this.alertsService.addInfoMessage( - 'Given skill is already a prerequisite skill', 5000); + 'Given skill is already a prerequisite skill', + 5000 + ); return; } } this.skillUpdateService.addPrerequisiteSkill(this.skill, skillId); }; - modalRef.result.then(function(summary) { - whenResolved(summary); - }, function() { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + modalRef.result.then( + function (summary) { + whenResolved(summary); + }, + function () { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } togglePrerequisiteSkills(): void { if (this.windowDimensionsService.isWindowNarrow()) { - this.prerequisiteSkillsAreShown = ( - !this.prerequisiteSkillsAreShown); + this.prerequisiteSkillsAreShown = !this.prerequisiteSkillsAreShown; } } @@ -148,24 +156,24 @@ implements OnInit { this.skillEditorCardIsShown = true; this.windowIsNarrow = this.windowDimensionsService.isWindowNarrow(); this.directiveSubscriptions.add( - this.windowDimensionsService.getResizeEvent().subscribe( - () => { - this.windowIsNarrow = this.windowDimensionsService.isWindowNarrow(); - this.prerequisiteSkillsAreShown = ( - !this.windowDimensionsService.isWindowNarrow()); - } - ) + this.windowDimensionsService.getResizeEvent().subscribe(() => { + this.windowIsNarrow = this.windowDimensionsService.isWindowNarrow(); + this.prerequisiteSkillsAreShown = + !this.windowDimensionsService.isWindowNarrow(); + }) ); - this.groupedSkillSummaries = this.skillEditorStateService - .getGroupedSkillSummaries(); + this.groupedSkillSummaries = + this.skillEditorStateService.getGroupedSkillSummaries(); - this.topicsAndSkillsDashboardBackendApiService.fetchDashboardDataAsync() + this.topicsAndSkillsDashboardBackendApiService + .fetchDashboardDataAsync() .then((response: TopicsAndSkillDashboardData) => { this.categorizedSkills = response.categorizedSkillsDict; this.untriagedSkillSummaries = response.untriagedSkillSummaries; this.allAvailableSkills = response.mergeableSkillSummaries.concat( - response.untriagedSkillSummaries); + response.untriagedSkillSummaries + ); }); this.skill = this.skillEditorStateService.getSkill(); @@ -174,8 +182,8 @@ implements OnInit { this.skillIdToSummaryMap = {}; for (let name in this.groupedSkillSummaries) { - let skillSummaries = ( - this.groupedSkillSummaries[name as keyof GroupedSkillSummaries]); + let skillSummaries = + this.groupedSkillSummaries[name as keyof GroupedSkillSummaries]; for (let idx in skillSummaries) { this.skillIdToSummaryMap[skillSummaries[idx].id] = skillSummaries[idx].description; @@ -189,6 +197,8 @@ implements OnInit { } angular.module('oppia').directive( - 'oppiaSkillPrerequisiteSkillsEditor', downgradeComponent({ - component: SkillPrerequisiteSkillsEditorComponent - }) as angular.IDirectiveFactory); + 'oppiaSkillPrerequisiteSkillsEditor', + downgradeComponent({ + component: SkillPrerequisiteSkillsEditorComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/skill-editor-page/editor-tab/skill-preview-modal.component.spec.ts b/core/templates/pages/skill-editor-page/editor-tab/skill-preview-modal.component.spec.ts index cd802044a23f..eb374c8c2803 100644 --- a/core/templates/pages/skill-editor-page/editor-tab/skill-preview-modal.component.spec.ts +++ b/core/templates/pages/skill-editor-page/editor-tab/skill-preview-modal.component.spec.ts @@ -16,10 +16,10 @@ * @fileoverview Unit tests for skill preview modal component. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { SkillPreviewModalComponent } from './skill-preview-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {SkillPreviewModalComponent} from './skill-preview-modal.component'; describe('Skill Preview Modal Component', () => { let fixture: ComponentFixture; @@ -27,13 +27,9 @@ describe('Skill Preview Modal Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - SkillPreviewModalComponent - ], - providers: [ - NgbActiveModal - ], - schemas: [NO_ERRORS_SCHEMA] + declarations: [SkillPreviewModalComponent], + providers: [NgbActiveModal], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); diff --git a/core/templates/pages/skill-editor-page/editor-tab/skill-preview-modal.component.ts b/core/templates/pages/skill-editor-page/editor-tab/skill-preview-modal.component.ts index 8363cdef13a5..b8788b7ee685 100644 --- a/core/templates/pages/skill-editor-page/editor-tab/skill-preview-modal.component.ts +++ b/core/templates/pages/skill-editor-page/editor-tab/skill-preview-modal.component.ts @@ -16,14 +16,14 @@ * @fileoverview Component for skill preview modal. */ -import { Component, Input } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { WorkedExample } from 'domain/skill/worked-example.model'; +import {Component, Input} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {WorkedExample} from 'domain/skill/worked-example.model'; @Component({ selector: 'skill-preview-modal', - templateUrl: './skill-preview-modal.component.html' + templateUrl: './skill-preview-modal.component.html', }) export class SkillPreviewModalComponent extends ConfirmOrCancelModal { // These properties are initialized using Angular lifecycle hooks @@ -33,9 +33,7 @@ export class SkillPreviewModalComponent extends ConfirmOrCancelModal { @Input() skillExplanation!: string; @Input() skillWorkedExamples!: WorkedExample[]; - constructor( - private ngbActiveModal: NgbActiveModal - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } } diff --git a/core/templates/pages/skill-editor-page/editor-tab/skill-rubrics-editor/skill-rubrics-editor.component.spec.ts b/core/templates/pages/skill-editor-page/editor-tab/skill-rubrics-editor/skill-rubrics-editor.component.spec.ts index f5517d680e9e..4ad333a247fe 100644 --- a/core/templates/pages/skill-editor-page/editor-tab/skill-rubrics-editor/skill-rubrics-editor.component.spec.ts +++ b/core/templates/pages/skill-editor-page/editor-tab/skill-rubrics-editor/skill-rubrics-editor.component.spec.ts @@ -16,18 +16,18 @@ * @fileoverview Unit tests for SkillRubricsEditorComponent */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { EventEmitter } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; - -import { SkillUpdateService } from 'domain/skill/skill-update.service'; -import { Skill, SkillObjectFactory } from 'domain/skill/SkillObjectFactory'; -import { SkillEditorStateService } from 'pages/skill-editor-page/services/skill-editor-state.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { SkillRubricsEditorComponent } from './skill-rubrics-editor.component'; -import { Rubric } from 'domain/skill/rubric.model'; -import { of } from 'rxjs'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {EventEmitter} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; + +import {SkillUpdateService} from 'domain/skill/skill-update.service'; +import {Skill, SkillObjectFactory} from 'domain/skill/SkillObjectFactory'; +import {SkillEditorStateService} from 'pages/skill-editor-page/services/skill-editor-state.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {SkillRubricsEditorComponent} from './skill-rubrics-editor.component'; +import {Rubric} from 'domain/skill/rubric.model'; +import {of} from 'rxjs'; describe('Skill Rubrics Editor Component', () => { let component: SkillRubricsEditorComponent; @@ -43,9 +43,7 @@ describe('Skill Rubrics Editor Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - SkillRubricsEditorComponent - ], + declarations: [SkillRubricsEditorComponent], providers: [ SkillUpdateService, SkillEditorStateService, @@ -53,11 +51,11 @@ describe('Skill Rubrics Editor Component', () => { provide: WindowDimensionsService, useValue: { isWindowNarrow: () => true, - getResizeEvent: () => of(resizeEvent) - } - } + getResizeEvent: () => of(resizeEvent), + }, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -79,9 +77,9 @@ describe('Skill Rubrics Editor Component', () => { voiceovers_mapping: { explanation: {}, worked_example_1: {}, - worked_example_2: {} - } - } + worked_example_2: {}, + }, + }, }; sampleSkill = skillObjectFactory.createFromBackendDict({ @@ -95,7 +93,7 @@ describe('Skill Rubrics Editor Component', () => { next_misconception_id: 6, superseding_skill_id: '2', all_questions_merged: false, - prerequisite_skill_ids: ['skill_1'] + prerequisite_skill_ids: ['skill_1'], }); fixture.detectChanges(); @@ -105,39 +103,43 @@ describe('Skill Rubrics Editor Component', () => { component.ngOnDestroy(); }); - describe('when user\'s window is narrow', () => { + describe("when user's window is narrow", () => { beforeEach(() => { spyOn(windowDimensionsService, 'isWindowNarrow').and.returnValue(true); spyOn(skillEditorStateService, 'getSkill').and.returnValue(sampleSkill); - spyOnProperty(skillEditorStateService, 'onSkillChange') - .and.returnValue(mockEventEmitter); + spyOnProperty(skillEditorStateService, 'onSkillChange').and.returnValue( + mockEventEmitter + ); component.ngOnInit(); }); - it('should initialise component when user open skill rubrics editor', - () => { - expect(component.skill).toEqual(sampleSkill); - expect(component.rubricsListIsShown).toBeFalse(); - }); + it('should initialise component when user open skill rubrics editor', () => { + expect(component.skill).toEqual(sampleSkill); + expect(component.rubricsListIsShown).toBeFalse(); + }); it('should fetch rubrics when skill changes', () => { - component.skill._rubrics = [Rubric.createFromBackendDict({ - difficulty: 'Easy', - explanations: ['explanation'], - })]; + component.skill._rubrics = [ + Rubric.createFromBackendDict({ + difficulty: 'Easy', + explanations: ['explanation'], + }), + ]; expect(component.rubrics).toBeUndefined(); mockEventEmitter.emit(); - expect(component.rubrics).toEqual([Rubric.createFromBackendDict({ - difficulty: 'Easy', - explanations: ['explanation'], - })]); + expect(component.rubrics).toEqual([ + Rubric.createFromBackendDict({ + difficulty: 'Easy', + explanations: ['explanation'], + }), + ]); }); - it('should show toggle rubrics list when user\'s window is narrow', () => { + it("should show toggle rubrics list when user's window is narrow", () => { expect(component.rubricsListIsShown).toBeFalse(); component.toggleRubricsList(); @@ -158,14 +160,14 @@ describe('Skill Rubrics Editor Component', () => { ]); expect(skillUpdateService.updateRubricForDifficulty).toHaveBeenCalledWith( - sampleSkill, 'Easy', [ - 'new explanation 1', - 'new explanation 2', - ]); + sampleSkill, + 'Easy', + ['new explanation 1', 'new explanation 2'] + ); }); }); - describe('when user\'s window is not narrow', () => { + describe("when user's window is not narrow", () => { beforeEach(() => { spyOn(windowDimensionsService, 'isWindowNarrow').and.returnValue(false); spyOn(skillEditorStateService, 'getSkill').and.returnValue(sampleSkill); @@ -189,8 +191,7 @@ describe('Skill Rubrics Editor Component', () => { it('should toggle skill editor card on clicking', () => { component.skillEditorCardIsShown = true; - spyOn(windowDimensionsService, 'isWindowNarrow') - .and.returnValue(true); + spyOn(windowDimensionsService, 'isWindowNarrow').and.returnValue(true); component.toggleSkillEditorCard(); @@ -204,7 +205,8 @@ describe('Skill Rubrics Editor Component', () => { it('should show Rubrics list when the window is narrow', () => { spyOn(windowDimensionsService, 'isWindowNarrow').and.returnValue(true); spyOn(windowDimensionsService, 'getResizeEvent').and.returnValue( - mockEventEmitter); + mockEventEmitter + ); component.windowIsNarrow = false; expect(component.rubricsListIsShown).toBe(false); diff --git a/core/templates/pages/skill-editor-page/editor-tab/skill-rubrics-editor/skill-rubrics-editor.component.ts b/core/templates/pages/skill-editor-page/editor-tab/skill-rubrics-editor/skill-rubrics-editor.component.ts index 48e49c10a502..f792c4b78c36 100644 --- a/core/templates/pages/skill-editor-page/editor-tab/skill-rubrics-editor/skill-rubrics-editor.component.ts +++ b/core/templates/pages/skill-editor-page/editor-tab/skill-rubrics-editor/skill-rubrics-editor.component.ts @@ -16,18 +16,18 @@ * @fileoverview Component for the skill rubrics editor. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { Subscription } from 'rxjs'; -import { Rubric } from 'domain/skill/rubric.model'; -import { SkillUpdateService } from 'domain/skill/skill-update.service'; -import { Skill } from 'domain/skill/SkillObjectFactory'; -import { SkillEditorStateService } from 'pages/skill-editor-page/services/skill-editor-state.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {Subscription} from 'rxjs'; +import {Rubric} from 'domain/skill/rubric.model'; +import {SkillUpdateService} from 'domain/skill/skill-update.service'; +import {Skill} from 'domain/skill/SkillObjectFactory'; +import {SkillEditorStateService} from 'pages/skill-editor-page/services/skill-editor-state.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; @Component({ selector: 'oppia-skill-rubrics-editor', - templateUrl: './skill-rubrics-editor.component.html' + templateUrl: './skill-rubrics-editor.component.html', }) export class SkillRubricsEditorComponent implements OnInit, OnDestroy { // These properties are initialized using Angular lifecycle hooks @@ -48,7 +48,10 @@ export class SkillRubricsEditorComponent implements OnInit, OnDestroy { onSaveRubric(difficulty: string, explanations: string[]): void { this.skillUpdateService.updateRubricForDifficulty( - this.skill, difficulty, explanations); + this.skill, + difficulty, + explanations + ); } toggleRubricsList(): void { @@ -68,22 +71,20 @@ export class SkillRubricsEditorComponent implements OnInit, OnDestroy { this.windowIsNarrow = this.windowDimensionsService.isWindowNarrow(); if (this.windowDimensionsService.getResizeEvent) { this.directiveSubscriptions.add( - this.windowDimensionsService.getResizeEvent().subscribe( - () => { - this.windowIsNarrow = this.windowDimensionsService.isWindowNarrow(); - this.rubricsListIsShown = ( - !this.windowDimensionsService.isWindowNarrow()); - } - ) + this.windowDimensionsService.getResizeEvent().subscribe(() => { + this.windowIsNarrow = this.windowDimensionsService.isWindowNarrow(); + this.rubricsListIsShown = + !this.windowDimensionsService.isWindowNarrow(); + }) ); } this.skill = this.skillEditorStateService.getSkill(); - this.rubricsListIsShown = ( - !this.windowDimensionsService.isWindowNarrow()); + this.rubricsListIsShown = !this.windowDimensionsService.isWindowNarrow(); this.directiveSubscriptions.add( this.skillEditorStateService.onSkillChange.subscribe( - () => this.rubrics = this.skill.getRubrics()) + () => (this.rubrics = this.skill.getRubrics()) + ) ); } @@ -92,7 +93,9 @@ export class SkillRubricsEditorComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive('oppiaSkillRubricsEditor', +angular.module('oppia').directive( + 'oppiaSkillRubricsEditor', downgradeComponent({ - component: SkillRubricsEditorComponent - }) as angular.IDirectiveFactory); + component: SkillRubricsEditorComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/skill-editor-page/modal-templates/add-misconception-modal.component.spec.ts b/core/templates/pages/skill-editor-page/modal-templates/add-misconception-modal.component.spec.ts index 02b933f71254..7a9173cbc2d6 100644 --- a/core/templates/pages/skill-editor-page/modal-templates/add-misconception-modal.component.spec.ts +++ b/core/templates/pages/skill-editor-page/modal-templates/add-misconception-modal.component.spec.ts @@ -12,20 +12,19 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Unit tests for AddMisconceptionModalComponent. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ChangeDetectorRef, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ChangeDetectorRef, NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; -import { MisconceptionObjectFactory } from 'domain/skill/MisconceptionObjectFactory'; -import { Skill, SkillObjectFactory } from 'domain/skill/SkillObjectFactory'; -import { SkillEditorStateService } from '../services/skill-editor-state.service'; -import { AddMisconceptionModalComponent } from './add-misconception-modal.component'; +import {MisconceptionObjectFactory} from 'domain/skill/MisconceptionObjectFactory'; +import {Skill, SkillObjectFactory} from 'domain/skill/SkillObjectFactory'; +import {SkillEditorStateService} from '../services/skill-editor-state.service'; +import {AddMisconceptionModalComponent} from './add-misconception-modal.component'; class MockActiveModal { close(): void { @@ -37,7 +36,7 @@ class MockActiveModal { } } -describe('Add Misconception Modal Component', function() { +describe('Add Misconception Modal Component', function () { let component: AddMisconceptionModalComponent; let fixture: ComponentFixture; let ngbActiveModal: NgbActiveModal; @@ -49,18 +48,16 @@ describe('Add Misconception Modal Component', function() { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - AddMisconceptionModalComponent - ], + declarations: [AddMisconceptionModalComponent], providers: [ SkillEditorStateService, ChangeDetectorRef, { provide: NgbActiveModal, - useClass: MockActiveModal + useClass: MockActiveModal, }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -77,18 +74,18 @@ describe('Add Misconception Modal Component', function() { name: 'test name 2', notes: 'test notes', feedback: 'test feedback', - must_be_addressed: true + must_be_addressed: true, }; let misconceptionDict2 = { id: 3, name: 'test name 3', notes: 'test notes', feedback: 'test feedback', - must_be_addressed: true + must_be_addressed: true, }; let rubricDict = { difficulty: 'easy', - explanations: ['explanation'] + explanations: ['explanation'], }; let skillContentsDict = { explanation: { @@ -97,8 +94,8 @@ describe('Add Misconception Modal Component', function() { }, worked_examples: [], recorded_voiceovers: { - voiceovers_mapping: {} - } + voiceovers_mapping: {}, + }, }; skillObject = skillObjectFactory.createFromBackendDict({ id: 'skill1', @@ -118,18 +115,18 @@ describe('Add Misconception Modal Component', function() { fixture.detectChanges(); }); - it('should initialize properties after component is initialized', - () => { - expect(component.skill).toEqual(skillObject); - expect(component.misconceptionName).toBe(''); - expect(component.misconceptionNotes).toBe(''); - expect(component.misconceptionFeedback).toBe(''); - expect(component.misconceptionMustBeAddressed).toBe(true); - expect(component.misconceptionNameIsDuplicate).toBe(false); - expect(component.existingMisconceptionNames).toEqual( - ['test name 2', 'test name 3'] - ); - }); + it('should initialize properties after component is initialized', () => { + expect(component.skill).toEqual(skillObject); + expect(component.misconceptionName).toBe(''); + expect(component.misconceptionNotes).toBe(''); + expect(component.misconceptionFeedback).toBe(''); + expect(component.misconceptionMustBeAddressed).toBe(true); + expect(component.misconceptionNameIsDuplicate).toBe(false); + expect(component.existingMisconceptionNames).toEqual([ + 'test name 2', + 'test name 3', + ]); + }); it('should save misconception when closing the modal', () => { spyOn(ngbActiveModal, 'close'); @@ -137,8 +134,7 @@ describe('Add Misconception Modal Component', function() { component.saveMisconception(); expect(ngbActiveModal.close).toHaveBeenCalledWith({ - misconception: misconceptionObjectFactory.create( - 3, '', '', '', true) + misconception: misconceptionObjectFactory.create(3, '', '', '', true), }); }); @@ -157,13 +153,15 @@ describe('Add Misconception Modal Component', function() { }); it('should get schema for form', () => { - expect(component.getSchemaForm()) - .toEqual(component.MISCONCEPTION_PROPERTY_FORM_SCHEMA); + expect(component.getSchemaForm()).toEqual( + component.MISCONCEPTION_PROPERTY_FORM_SCHEMA + ); }); it('should get schema for feedback', () => { - expect(component.getSchemaFeedback()) - .toEqual(component.MISCONCEPTION_FEEDBACK_PROPERTY_FORM_SCHEMA); + expect(component.getSchemaFeedback()).toEqual( + component.MISCONCEPTION_FEEDBACK_PROPERTY_FORM_SCHEMA + ); }); it('should update misconceptionNotes', () => { diff --git a/core/templates/pages/skill-editor-page/modal-templates/add-misconception-modal.component.ts b/core/templates/pages/skill-editor-page/modal-templates/add-misconception-modal.component.ts index d07d5db1d8c6..5e97be2eebd4 100644 --- a/core/templates/pages/skill-editor-page/modal-templates/add-misconception-modal.component.ts +++ b/core/templates/pages/skill-editor-page/modal-templates/add-misconception-modal.component.ts @@ -16,26 +16,28 @@ * @fileoverview Component for add misconception modal. */ -import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { AppConstants } from 'app.constants'; +import {ChangeDetectorRef, Component, OnInit} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {AppConstants} from 'app.constants'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { MisconceptionObjectFactory } from 'domain/skill/MisconceptionObjectFactory'; -import { Skill } from 'domain/skill/SkillObjectFactory'; -import { SkillEditorStateService } from '../services/skill-editor-state.service'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {MisconceptionObjectFactory} from 'domain/skill/MisconceptionObjectFactory'; +import {Skill} from 'domain/skill/SkillObjectFactory'; +import {SkillEditorStateService} from '../services/skill-editor-state.service'; interface MisconceptionFormSchema { type: 'html'; - 'ui_config': object; + ui_config: object; } @Component({ selector: 'oppia-add-misconception-modal', - templateUrl: './add-misconception-modal.component.html' + templateUrl: './add-misconception-modal.component.html', }) export class AddMisconceptionModalComponent - extends ConfirmOrCancelModal implements OnInit { + extends ConfirmOrCancelModal + implements OnInit +{ // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -46,21 +48,23 @@ export class AddMisconceptionModalComponent misconceptionNameIsDuplicate!: boolean; misconceptionNotes!: string; skill!: Skill; - MAX_CHARS_IN_MISCONCEPTION_NAME: number = ( - AppConstants.MAX_CHARS_IN_MISCONCEPTION_NAME); + MAX_CHARS_IN_MISCONCEPTION_NAME: number = + AppConstants.MAX_CHARS_IN_MISCONCEPTION_NAME; MISCONCEPTION_PROPERTY_FORM_SCHEMA: MisconceptionFormSchema = { type: 'html', ui_config: { - startupFocusEnabled: false - }}; + startupFocusEnabled: false, + }, + }; MISCONCEPTION_FEEDBACK_PROPERTY_FORM_SCHEMA: MisconceptionFormSchema = { type: 'html', ui_config: { hide_complex_extensions: true, - startupFocusEnabled: false - }}; + startupFocusEnabled: false, + }, + }; constructor( private ngbActiveModal: NgbActiveModal, @@ -78,9 +82,9 @@ export class AddMisconceptionModalComponent this.misconceptionMustBeAddressed = true; this.misconceptionNameIsDuplicate = false; this.skill = this.skillEditorStateService.getSkill(); - this.existingMisconceptionNames = this.skill.getMisconceptions().map( - misconception => misconception.getName() - ); + this.existingMisconceptionNames = this.skill + .getMisconceptions() + .map(misconception => misconception.getName()); } getSchemaForm(): MisconceptionFormSchema { @@ -116,13 +120,13 @@ export class AddMisconceptionModalComponent this.misconceptionName, this.misconceptionNotes, this.misconceptionFeedback, - this.misconceptionMustBeAddressed) + this.misconceptionMustBeAddressed + ), }); } checkIfMisconceptionNameIsDuplicate(): void { - this.misconceptionNameIsDuplicate = ( - this.existingMisconceptionNames.includes(this.misconceptionName) - ); + this.misconceptionNameIsDuplicate = + this.existingMisconceptionNames.includes(this.misconceptionName); } } diff --git a/core/templates/pages/skill-editor-page/modal-templates/add-worked-example.component.spec.ts b/core/templates/pages/skill-editor-page/modal-templates/add-worked-example.component.spec.ts index 0a7f3bd1ebf9..46e3e4de0fe0 100644 --- a/core/templates/pages/skill-editor-page/modal-templates/add-worked-example.component.spec.ts +++ b/core/templates/pages/skill-editor-page/modal-templates/add-worked-example.component.spec.ts @@ -16,11 +16,11 @@ * @fileoverview Unit tests for AddWorkedExampleModalComponent. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ChangeDetectorRef, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { AddWorkedExampleModalComponent } from './add-worked-example.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ChangeDetectorRef, NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {AddWorkedExampleModalComponent} from './add-worked-example.component'; class MockActiveModal { close(): void { @@ -40,17 +40,15 @@ describe('Add Worked Example Modal Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - AddWorkedExampleModalComponent - ], + declarations: [AddWorkedExampleModalComponent], providers: [ ChangeDetectorRef, { provide: NgbActiveModal, - useClass: MockActiveModal + useClass: MockActiveModal, }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -62,11 +60,10 @@ describe('Add Worked Example Modal Component', () => { fixture.detectChanges(); }); - it('should initialize properties after component is initialized', - () => { - expect(component.tmpWorkedExampleQuestionHtml).toEqual(''); - expect(component.tmpWorkedExampleExplanationHtml).toBe(''); - }); + it('should initialize properties after component is initialized', () => { + expect(component.tmpWorkedExampleQuestionHtml).toEqual(''); + expect(component.tmpWorkedExampleExplanationHtml).toBe(''); + }); it('should close modal when saving worked example', () => { spyOn(ngbActiveModal, 'close'); @@ -77,13 +74,12 @@ describe('Add Worked Example Modal Component', () => { expect(ngbActiveModal.close).toHaveBeenCalledWith({ workedExampleQuestionHtml: 'question', - workedExampleExplanationHtml: 'explanation' + workedExampleExplanationHtml: 'explanation', }); }); it('should get schema', () => { - expect(component.getSchema()) - .toEqual(component.WORKED_EXAMPLE_FORM_SCHEMA); + expect(component.getSchema()).toEqual(component.WORKED_EXAMPLE_FORM_SCHEMA); }); it('should update tmpWorkedExampleQuestionHtml', () => { diff --git a/core/templates/pages/skill-editor-page/modal-templates/add-worked-example.component.ts b/core/templates/pages/skill-editor-page/modal-templates/add-worked-example.component.ts index c8ec1e082a7f..f5587bb81e7e 100644 --- a/core/templates/pages/skill-editor-page/modal-templates/add-worked-example.component.ts +++ b/core/templates/pages/skill-editor-page/modal-templates/add-worked-example.component.ts @@ -16,21 +16,23 @@ * @fileoverview Component for add worked example modal. */ -import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {ChangeDetectorRef, Component, OnInit} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; interface HtmlFormSchema { type: 'html'; - 'ui_config': object; + ui_config: object; } @Component({ selector: 'oppia-add-worked-example-modal', - templateUrl: './add-worked-example.component.html' + templateUrl: './add-worked-example.component.html', }) export class AddWorkedExampleModalComponent - extends ConfirmOrCancelModal implements OnInit { + extends ConfirmOrCancelModal + implements OnInit +{ // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -38,7 +40,7 @@ export class AddWorkedExampleModalComponent tmpWorkedExampleQuestionHtml!: string; WORKED_EXAMPLE_FORM_SCHEMA: HtmlFormSchema = { type: 'html', - ui_config: {} + ui_config: {}, }; constructor( @@ -73,10 +75,8 @@ export class AddWorkedExampleModalComponent saveWorkedExample(): void { this.ngbActiveModal.close({ - workedExampleQuestionHtml: - this.tmpWorkedExampleQuestionHtml, - workedExampleExplanationHtml: - this.tmpWorkedExampleExplanationHtml + workedExampleQuestionHtml: this.tmpWorkedExampleQuestionHtml, + workedExampleExplanationHtml: this.tmpWorkedExampleExplanationHtml, }); } } diff --git a/core/templates/pages/skill-editor-page/modal-templates/delete-misconception-modal.component.spec.ts b/core/templates/pages/skill-editor-page/modal-templates/delete-misconception-modal.component.spec.ts index 49317d6322aa..37dd9f227e3f 100644 --- a/core/templates/pages/skill-editor-page/modal-templates/delete-misconception-modal.component.spec.ts +++ b/core/templates/pages/skill-editor-page/modal-templates/delete-misconception-modal.component.spec.ts @@ -16,12 +16,12 @@ * @fileoverview Unit tests for DeleteMisconceptionModalController. */ -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { DeleteMisconceptionModalComponent } from './delete-misconception-modal.component'; -import { Skill, SkillObjectFactory } from 'domain/skill/SkillObjectFactory'; -import { SkillEditorStateService } from 'pages/skill-editor-page/services/skill-editor-state.service'; -import { AppConstants } from 'app.constants'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {DeleteMisconceptionModalComponent} from './delete-misconception-modal.component'; +import {Skill, SkillObjectFactory} from 'domain/skill/SkillObjectFactory'; +import {SkillEditorStateService} from 'pages/skill-editor-page/services/skill-editor-state.service'; +import {AppConstants} from 'app.constants'; class MockActiveModal { close(value: string): void { @@ -51,13 +51,13 @@ describe('Delete Misconception Modal Component', () => { providers: [ { provide: NgbActiveModal, - useClass: MockActiveModal + useClass: MockActiveModal, }, { provide: SkillEditorStateService, - useClass: MockSkillEditorStateService - } - ] + useClass: MockSkillEditorStateService, + }, + ], }).compileComponents(); fixture = TestBed.createComponent(DeleteMisconceptionModalComponent); component = fixture.componentInstance; @@ -72,12 +72,12 @@ describe('Delete Misconception Modal Component', () => { name: 'test name', notes: 'test notes', feedback: 'test feedback', - must_be_addressed: true + must_be_addressed: true, }; let rubricDict = { difficulty: AppConstants.SKILL_DIFFICULTIES[0], - explanations: ['explanation'] + explanations: ['explanation'], }; let skillContentsDict = { @@ -87,8 +87,8 @@ describe('Delete Misconception Modal Component', () => { }, worked_examples: [], recorded_voiceovers: { - voiceovers_mapping: {} - } + voiceovers_mapping: {}, + }, }; skillObject = skillObjectFactory.createFromBackendDict({ @@ -102,23 +102,25 @@ describe('Delete Misconception Modal Component', () => { next_misconception_id: 3, prerequisite_skill_ids: ['skill_1'], superseding_skill_id: 'skill0', - all_questions_merged: true + all_questions_merged: true, }); spyOn(skillEditorStateService, 'getSkill').and.returnValue(skillObject); component.ngOnInit(); })); - it('should initialize properties after component is initialized', - () => { - expect(component.skill).toEqual(skillObject); - }); - - it('should close the modal with misconception id when clicking on save' + - ' button', () => { - component.confirm(); - expect(closeSpy).toHaveBeenCalledWith({ - id: 2 - }); + it('should initialize properties after component is initialized', () => { + expect(component.skill).toEqual(skillObject); }); + + it( + 'should close the modal with misconception id when clicking on save' + + ' button', + () => { + component.confirm(); + expect(closeSpy).toHaveBeenCalledWith({ + id: 2, + }); + } + ); }); diff --git a/core/templates/pages/skill-editor-page/modal-templates/delete-misconception-modal.component.ts b/core/templates/pages/skill-editor-page/modal-templates/delete-misconception-modal.component.ts index d98b5d780c09..10811182b10f 100644 --- a/core/templates/pages/skill-editor-page/modal-templates/delete-misconception-modal.component.ts +++ b/core/templates/pages/skill-editor-page/modal-templates/delete-misconception-modal.component.ts @@ -16,18 +16,21 @@ * @fileoverview Controller for delete misconception modal. */ -import { Component, OnInit } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { Skill } from 'domain/skill/SkillObjectFactory'; -import { SkillEditorStateService } from 'pages/skill-editor-page/services/skill-editor-state.service'; +import {Component, OnInit} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Skill} from 'domain/skill/SkillObjectFactory'; +import {SkillEditorStateService} from 'pages/skill-editor-page/services/skill-editor-state.service'; @Component({ selector: 'delete-misconception-modal', templateUrl: './delete-misconception-modal.component.html', - styleUrls: [] -}) export class DeleteMisconceptionModalComponent - extends ConfirmOrCancelModal implements OnInit { + styleUrls: [], +}) +export class DeleteMisconceptionModalComponent + extends ConfirmOrCancelModal + implements OnInit +{ // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -47,7 +50,7 @@ import { SkillEditorStateService } from 'pages/skill-editor-page/services/skill- confirm(): void { this.modalInstance.close({ - id: this.skill.getMisconceptionAtIndex(this.index).getId() + id: this.skill.getMisconceptionAtIndex(this.index).getId(), }); } } diff --git a/core/templates/pages/skill-editor-page/modal-templates/delete-worked-example-modal.component.spec.ts b/core/templates/pages/skill-editor-page/modal-templates/delete-worked-example-modal.component.spec.ts index 0fdb195c4a38..8a0f7608be10 100644 --- a/core/templates/pages/skill-editor-page/modal-templates/delete-worked-example-modal.component.spec.ts +++ b/core/templates/pages/skill-editor-page/modal-templates/delete-worked-example-modal.component.spec.ts @@ -16,24 +16,24 @@ * @fileoverview Unit tests for the DeleteWorkedExampleComponent. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { DeleteWorkedExampleComponent } from './delete-worked-example-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {DeleteWorkedExampleComponent} from './delete-worked-example-modal.component'; -describe('Delete Worked Example Modal Component', function() { +describe('Delete Worked Example Modal Component', function () { let component: DeleteWorkedExampleComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - DeleteWorkedExampleComponent + declarations: [DeleteWorkedExampleComponent], + providers: [ + { + provide: NgbActiveModal, + }, ], - providers: [{ - provide: NgbActiveModal - }], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); diff --git a/core/templates/pages/skill-editor-page/modal-templates/delete-worked-example-modal.component.ts b/core/templates/pages/skill-editor-page/modal-templates/delete-worked-example-modal.component.ts index ad97b23dbf89..b29d411da0bb 100644 --- a/core/templates/pages/skill-editor-page/modal-templates/delete-worked-example-modal.component.ts +++ b/core/templates/pages/skill-editor-page/modal-templates/delete-worked-example-modal.component.ts @@ -16,18 +16,16 @@ * @fileoverview Component for delete worked example modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-delete-worked-example-modal', - templateUrl: './delete-worked-example-modal.component.html' + templateUrl: './delete-worked-example-modal.component.html', }) export class DeleteWorkedExampleComponent extends ConfirmOrCancelModal { - constructor( - private ngbActiveModal: NgbActiveModal - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } } diff --git a/core/templates/pages/skill-editor-page/modal-templates/skill-editor-save-modal.component.spec.ts b/core/templates/pages/skill-editor-page/modal-templates/skill-editor-save-modal.component.spec.ts index 5bf90fc2d6b9..66654bd8cc9b 100644 --- a/core/templates/pages/skill-editor-page/modal-templates/skill-editor-save-modal.component.spec.ts +++ b/core/templates/pages/skill-editor-page/modal-templates/skill-editor-save-modal.component.spec.ts @@ -16,10 +16,10 @@ * @fileoverview Unit tests for skill editor save modal. */ -import { ChangeDetectorRef, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { SkillEditorSaveModalComponent } from './skill-editor-save-modal.component'; +import {ChangeDetectorRef, NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {SkillEditorSaveModalComponent} from './skill-editor-save-modal.component'; class MockChangeDetectorRef { detectChanges(): void {} @@ -32,17 +32,15 @@ describe('Skill editor save modal component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - SkillEditorSaveModalComponent - ], + declarations: [SkillEditorSaveModalComponent], providers: [ NgbActiveModal, { provide: ChangeDetectorRef, - useValue: changeDetectorRef - } + useValue: changeDetectorRef, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); diff --git a/core/templates/pages/skill-editor-page/modal-templates/skill-editor-save-modal.component.ts b/core/templates/pages/skill-editor-page/modal-templates/skill-editor-save-modal.component.ts index 16df21ea5509..0804ed9b1102 100644 --- a/core/templates/pages/skill-editor-page/modal-templates/skill-editor-save-modal.component.ts +++ b/core/templates/pages/skill-editor-page/modal-templates/skill-editor-save-modal.component.ts @@ -16,14 +16,14 @@ * @fileoverview Component for skill editor save modal. */ -import { ChangeDetectorRef, Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { AppConstants } from 'app.constants'; +import {ChangeDetectorRef, Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {AppConstants} from 'app.constants'; @Component({ selector: 'skill-editor-save-modal', - templateUrl: './skill-editor-save-modal.component.html' + templateUrl: './skill-editor-save-modal.component.html', }) export class SkillEditorSaveModalComponent extends ConfirmOrCancelModal { // These properties are initialized using Angular lifecycle hooks @@ -34,7 +34,7 @@ export class SkillEditorSaveModalComponent extends ConfirmOrCancelModal { constructor( private ngbActiveModal: NgbActiveModal, - private changeDetectorRef: ChangeDetectorRef, + private changeDetectorRef: ChangeDetectorRef ) { super(ngbActiveModal); } diff --git a/core/templates/pages/skill-editor-page/navbar/skill-editor-navbar-breadcrumb.component.spec.ts b/core/templates/pages/skill-editor-page/navbar/skill-editor-navbar-breadcrumb.component.spec.ts index 8b1e0a649505..dab976d88ac6 100644 --- a/core/templates/pages/skill-editor-page/navbar/skill-editor-navbar-breadcrumb.component.spec.ts +++ b/core/templates/pages/skill-editor-page/navbar/skill-editor-navbar-breadcrumb.component.spec.ts @@ -16,11 +16,11 @@ * @fileoverview Unit tests for the navbar breadcrumb of the skill editor. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { Skill, SkillObjectFactory } from 'domain/skill/SkillObjectFactory'; -import { SkillEditorStateService } from '../services/skill-editor-state.service'; -import { SkillEditorNavbarBreadcrumbComponent } from './skill-editor-navbar-breadcrumb.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {Skill, SkillObjectFactory} from 'domain/skill/SkillObjectFactory'; +import {SkillEditorStateService} from '../services/skill-editor-state.service'; +import {SkillEditorNavbarBreadcrumbComponent} from './skill-editor-navbar-breadcrumb.component'; describe('SkillEditorNavbarBreadcrumbComponent', () => { let component: SkillEditorNavbarBreadcrumbComponent; @@ -29,11 +29,10 @@ describe('SkillEditorNavbarBreadcrumbComponent', () => { let skillObject: Skill; let skillEditorStateService: SkillEditorStateService; - beforeEach(async(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [SkillEditorNavbarBreadcrumbComponent] + declarations: [SkillEditorNavbarBreadcrumbComponent], }).compileComponents(); })); @@ -47,12 +46,12 @@ describe('SkillEditorNavbarBreadcrumbComponent', () => { name: 'test name', notes: 'test notes', feedback: 'test feedback', - must_be_addressed: true + must_be_addressed: true, }; let rubricDict = { difficulty: 'Easy', - explanations: ['explanation'] + explanations: ['explanation'], }; let skillContentsDict = { @@ -62,8 +61,8 @@ describe('SkillEditorNavbarBreadcrumbComponent', () => { }, worked_examples: [], recorded_voiceovers: { - voiceovers_mapping: {} - } + voiceovers_mapping: {}, + }, }; skillObject = skillObjectFactory.createFromBackendDict({ @@ -77,22 +76,22 @@ describe('SkillEditorNavbarBreadcrumbComponent', () => { next_misconception_id: 3, prerequisite_skill_ids: ['skill_1'], superseding_skill_id: 'skill0', - all_questions_merged: true + all_questions_merged: true, }); }); - it('should not truncate skill descirption when skill editor navbar loads', - () => { - spyOn(skillEditorStateService, 'getSkill').and.returnValue(skillObject); + it('should not truncate skill descirption when skill editor navbar loads', () => { + spyOn(skillEditorStateService, 'getSkill').and.returnValue(skillObject); - expect(component.getTruncatedDescription()).toBe('test description 1'); - }); + expect(component.getTruncatedDescription()).toBe('test description 1'); + }); it('should truncate skill descirption when skill editor navbar loads', () => { skillObject.setDescription(Array(40).join('a')); spyOn(skillEditorStateService, 'getSkill').and.returnValue(skillObject); expect(component.getTruncatedDescription()).toBe( - Array(36).join('a') + '...'); + Array(36).join('a') + '...' + ); }); }); diff --git a/core/templates/pages/skill-editor-page/navbar/skill-editor-navbar-breadcrumb.component.ts b/core/templates/pages/skill-editor-page/navbar/skill-editor-navbar-breadcrumb.component.ts index 56885dfa34db..b2f2bf5b7d3c 100644 --- a/core/templates/pages/skill-editor-page/navbar/skill-editor-navbar-breadcrumb.component.ts +++ b/core/templates/pages/skill-editor-page/navbar/skill-editor-navbar-breadcrumb.component.ts @@ -16,23 +16,23 @@ * @fileoverview Component for the navbar breadcrumb of the skill editor. */ -import { Component } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { SkillEditorStateService } from 'pages/skill-editor-page/services/skill-editor-state.service'; +import {Component} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {SkillEditorStateService} from 'pages/skill-editor-page/services/skill-editor-state.service'; @Component({ selector: 'oppia-skill-editor-navbar-breadcrumb', templateUrl: './skill-editor-navbar-breadcrumb.component.html', - styleUrls: [] + styleUrls: [], }) export class SkillEditorNavbarBreadcrumbComponent { - constructor( - private skillEditorStateService: SkillEditorStateService) {} + constructor(private skillEditorStateService: SkillEditorStateService) {} getTruncatedDescription(): string { const skill = this.skillEditorStateService.getSkill(); - const skillDescription = ( - skill ? skill.getDescription() : 'Skill description loading'); + const skillDescription = skill + ? skill.getDescription() + : 'Skill description loading'; let truncatedDescription = skillDescription.substr(0, 35); if (skillDescription.length > 35) { truncatedDescription += '...'; @@ -41,6 +41,9 @@ export class SkillEditorNavbarBreadcrumbComponent { } } -angular.module('oppia').directive( - 'oppiaSkillEditorNavbarBreadcrumb', downgradeComponent( - {component: SkillEditorNavbarBreadcrumbComponent})); +angular + .module('oppia') + .directive( + 'oppiaSkillEditorNavbarBreadcrumb', + downgradeComponent({component: SkillEditorNavbarBreadcrumbComponent}) + ); diff --git a/core/templates/pages/skill-editor-page/navbar/skill-editor-navbar.component.spec.ts b/core/templates/pages/skill-editor-page/navbar/skill-editor-navbar.component.spec.ts index 9f0d0072a875..84de0e16cd1d 100644 --- a/core/templates/pages/skill-editor-page/navbar/skill-editor-navbar.component.spec.ts +++ b/core/templates/pages/skill-editor-page/navbar/skill-editor-navbar.component.spec.ts @@ -16,21 +16,26 @@ * @fileoverview Unit tests for the Skill Editor Navbar Component. */ -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { SkillEditorStateService } from '../services/skill-editor-state.service'; -import { Skill } from 'domain/skill/SkillObjectFactory'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { UndoRedoService } from 'domain/editor/undo_redo/undo-redo.service'; -import { UrlService } from 'services/contextual/url.service'; -import { ConceptCard } from 'domain/skill/concept-card.model'; -import { AppConstants } from 'app.constants'; -import { RecordedVoiceovers } from 'domain/exploration/recorded-voiceovers.model'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { NgbModal, NgbModalRef, NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { SkillUpdateService } from 'domain/skill/skill-update.service'; -import { SkillEditorNavabarComponent } from './skill-editor-navbar.component'; -import { SkillEditorRoutingService } from '../services/skill-editor-routing.service'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {SkillEditorStateService} from '../services/skill-editor-state.service'; +import {Skill} from 'domain/skill/SkillObjectFactory'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import {UndoRedoService} from 'domain/editor/undo_redo/undo-redo.service'; +import {UrlService} from 'services/contextual/url.service'; +import {ConceptCard} from 'domain/skill/concept-card.model'; +import {AppConstants} from 'app.constants'; +import {RecordedVoiceovers} from 'domain/exploration/recorded-voiceovers.model'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {NgbModal, NgbModalRef, NgbModule} from '@ng-bootstrap/ng-bootstrap'; +import {SkillUpdateService} from 'domain/skill/skill-update.service'; +import {SkillEditorNavabarComponent} from './skill-editor-navbar.component'; +import {SkillEditorRoutingService} from '../services/skill-editor-routing.service'; class MockNgbModalRef { componentInstance!: { @@ -62,12 +67,10 @@ describe('Skill Editor Navbar Component', () => { SkillEditorStateService, SkillUpdateService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); - - beforeEach(() => { fixture = TestBed.createComponent(SkillEditorNavabarComponent); component = fixture.componentInstance; @@ -80,23 +83,38 @@ describe('Skill Editor Navbar Component', () => { const conceptCard = new ConceptCard( SubtitledHtml.createDefault( - 'review material', AppConstants.COMPONENT_NAME_EXPLANATION), + 'review material', + AppConstants.COMPONENT_NAME_EXPLANATION + ), [], RecordedVoiceovers.createFromBackendDict({ voiceovers_mapping: { - COMPONENT_NAME_EXPLANATION: {} - } + COMPONENT_NAME_EXPLANATION: {}, + }, }) ); sampleSkill = new Skill( - 'id1', 'description', [], [], conceptCard, 'en', 1, 0, 'id1', false, [] + 'id1', + 'description', + [], + [], + conceptCard, + 'en', + 1, + 0, + 'id1', + false, + [] ); spyOn(skillEditorStateService, 'getSkill').and.returnValue(sampleSkill); - spyOnProperty(skillEditorStateService, 'onSkillChange') - .and.returnValue(mockEventEmitter); - spyOnProperty(skillUpdateService, 'onPrerequisiteSkillChange').and. - returnValue(mockPrerequisiteSkillChangeEventEmitter); + spyOnProperty(skillEditorStateService, 'onSkillChange').and.returnValue( + mockEventEmitter + ); + spyOnProperty( + skillUpdateService, + 'onPrerequisiteSkillChange' + ).and.returnValue(mockPrerequisiteSkillChangeEventEmitter); }); it('should set properties when initialized', () => { @@ -110,54 +128,61 @@ describe('Skill Editor Navbar Component', () => { expect(component.activeTab).toBe('Editor'); }); - it('should get current active tab name when ' + - 'calling \'getActiveTabName\'', () => { - spyOn(skillEditorRoutingService, 'getActiveTabName') - .and.returnValue('activeTab'); + it( + 'should get current active tab name when ' + "calling 'getActiveTabName'", + () => { + spyOn(skillEditorRoutingService, 'getActiveTabName').and.returnValue( + 'activeTab' + ); - let result = component.getActiveTabName(); + let result = component.getActiveTabName(); - expect(result).toBe('activeTab'); - }); + expect(result).toBe('activeTab'); + } + ); - it('should check whether the skill is still loading when ' + - 'calling \'isLoadingSkill\'', () => { - spyOn(skillEditorStateService, 'isLoadingSkill') - .and.returnValue(false); + it( + 'should check whether the skill is still loading when ' + + "calling 'isLoadingSkill'", + () => { + spyOn(skillEditorStateService, 'isLoadingSkill').and.returnValue(false); - let result = component.isLoadingSkill(); + let result = component.isLoadingSkill(); - expect(result).toBe(false); - }); + expect(result).toBe(false); + } + ); - it('should check whether the skill is being saved when ' + - 'calling \'isSaveInProgress \'', () => { - spyOn(skillEditorStateService, 'isSavingSkill') - .and.returnValue(false); + it( + 'should check whether the skill is being saved when ' + + "calling 'isSaveInProgress '", + () => { + spyOn(skillEditorStateService, 'isSavingSkill').and.returnValue(false); - let result = component.isSaveInProgress(); + let result = component.isSaveInProgress(); - expect(result).toBe(false); - }); + expect(result).toBe(false); + } + ); - it('should get change list count when calling ' + - '\'getChangeListCount\'', () => { - spyOn(undoRedoService, 'getChangeCount') - .and.returnValue(2); + it( + 'should get change list count when calling ' + "'getChangeListCount'", + () => { + spyOn(undoRedoService, 'getChangeCount').and.returnValue(2); - let result = component.getChangeListCount(); + let result = component.getChangeListCount(); - expect(result).toBe(2); - }); + expect(result).toBe(2); + } + ); - it('should discard changes when calling ' + - '\'discardChanges\'', () => { - let discardSpy = spyOn(undoRedoService, 'clearChanges') - .and.returnValue(); - let loadSkillSpy = spyOn(skillEditorStateService, 'loadSkill') - .and.returnValue(); - let urlSpy = spyOn(urlService, 'getSkillIdFromUrl') - .and.returnValue(''); + it('should discard changes when calling ' + "'discardChanges'", () => { + let discardSpy = spyOn(undoRedoService, 'clearChanges').and.returnValue(); + let loadSkillSpy = spyOn( + skillEditorStateService, + 'loadSkill' + ).and.returnValue(); + let urlSpy = spyOn(urlService, 'getSkillIdFromUrl').and.returnValue(''); component.ngOnInit(); component.discardChanges(); @@ -167,169 +192,198 @@ describe('Skill Editor Navbar Component', () => { expect(urlSpy).toHaveBeenCalled(); }); - it('should get change list count when calling ' + - '\'getChangeListCount\'', () => { - spyOn(undoRedoService, 'getChangeCount').and.returnValue(3); - - expect(component.getChangeListCount()).toBe(3); - }); - - it('should get number of warnings when calling ' + - '\'getWarningsCount\'', () => { - spyOn(skillEditorStateService, 'getSkillValidationIssues') - .and.returnValue(['issue 1', 'issue 2', 'issue 3']); - - expect(component.getWarningsCount()).toBe(3); - }); - - it('should check whether the skill is saveable when ' + - 'calling \'isSkillSaveable\'', () => { - spyOn(skillEditorStateService, 'isSavingSkill') - .and.returnValue(false); - - let result = component.isSkillSaveable(); - - expect(result).toBe(false); - }); - - it('should toggle navigation options when calling ' + - '\'toggleNavigationOptions\'', () => { - component.showNavigationOptions = true; - - component.toggleNavigationOptions(); - expect(component.showNavigationOptions).toBe(false); - - component.toggleNavigationOptions(); - expect(component.showNavigationOptions).toBe(true); - }); - - it('should navigate to main tab when ' + - 'calling \'selectMainTab\'', () => { + it( + 'should get change list count when calling ' + "'getChangeListCount'", + () => { + spyOn(undoRedoService, 'getChangeCount').and.returnValue(3); + + expect(component.getChangeListCount()).toBe(3); + } + ); + + it( + 'should get number of warnings when calling ' + "'getWarningsCount'", + () => { + spyOn( + skillEditorStateService, + 'getSkillValidationIssues' + ).and.returnValue(['issue 1', 'issue 2', 'issue 3']); + + expect(component.getWarningsCount()).toBe(3); + } + ); + + it( + 'should check whether the skill is saveable when ' + + "calling 'isSkillSaveable'", + () => { + spyOn(skillEditorStateService, 'isSavingSkill').and.returnValue(false); + + let result = component.isSkillSaveable(); + + expect(result).toBe(false); + } + ); + + it( + 'should toggle navigation options when calling ' + + "'toggleNavigationOptions'", + () => { + component.showNavigationOptions = true; + + component.toggleNavigationOptions(); + expect(component.showNavigationOptions).toBe(false); + + component.toggleNavigationOptions(); + expect(component.showNavigationOptions).toBe(true); + } + ); + + it('should navigate to main tab when ' + "calling 'selectMainTab'", () => { let navigateToMainTabSpy = spyOn( - skillEditorRoutingService, 'navigateToMainTab') - .and.returnValue(); + skillEditorRoutingService, + 'navigateToMainTab' + ).and.returnValue(); component.selectMainTab(); expect(navigateToMainTabSpy).toHaveBeenCalled(); }); - it('should navigate to main tab when ' + - 'calling \'selectPreviewTab\'', () => { + it('should navigate to main tab when ' + "calling 'selectPreviewTab'", () => { let navigateToPreviewTabSpy = spyOn( - skillEditorRoutingService, 'navigateToPreviewTab') - .and.returnValue(); + skillEditorRoutingService, + 'navigateToPreviewTab' + ).and.returnValue(); component.selectPreviewTab(); expect(navigateToPreviewTabSpy).toHaveBeenCalled(); }); - it('should toggle skill edit options when calling ' + - '\'toggleSkillEditOptions\'', () => { - component.showSkillEditOptions = true; - - component.toggleSkillEditOptions(); - expect(component.showSkillEditOptions).toBe(false); - - component.toggleSkillEditOptions(); - expect(component.showSkillEditOptions).toBe(true); - }); - - it('should save changes if save changes modal is opened and confirm ' + - 'button is clicked', fakeAsync(() => { - const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ - result: Promise.resolve('success') - } as NgbModalRef); - }); + it( + 'should toggle skill edit options when calling ' + + "'toggleSkillEditOptions'", + () => { + component.showSkillEditOptions = true; + + component.toggleSkillEditOptions(); + expect(component.showSkillEditOptions).toBe(false); + + component.toggleSkillEditOptions(); + expect(component.showSkillEditOptions).toBe(true); + } + ); + + it( + 'should save changes if save changes modal is opened and confirm ' + + 'button is clicked', + fakeAsync(() => { + const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + result: Promise.resolve('success'), + } as NgbModalRef; + }); - let saveSkillSpy = spyOn(skillEditorStateService, 'saveSkill') - .and.callFake((message, cb) => { + let saveSkillSpy = spyOn( + skillEditorStateService, + 'saveSkill' + ).and.callFake((message, cb) => { cb(); return true; }); - component.saveChanges(); - tick(); - - expect(modalSpy).toHaveBeenCalled(); - expect(saveSkillSpy).toHaveBeenCalled(); - })); + component.saveChanges(); + tick(); - it('should not save changes if save changes modal is opened and cancel ' + - 'button is clicked', fakeAsync(() => { - let ngbModalSpy = spyOn(ngbModal, 'open').and.callFake( - (modal, modalOptions) => { - return ({ - result: Promise.reject() - } as NgbModalRef); - }); - spyOn(skillEditorStateService, 'saveSkill'); + expect(modalSpy).toHaveBeenCalled(); + expect(saveSkillSpy).toHaveBeenCalled(); + }) + ); + + it( + 'should not save changes if save changes modal is opened and cancel ' + + 'button is clicked', + fakeAsync(() => { + let ngbModalSpy = spyOn(ngbModal, 'open').and.callFake( + (modal, modalOptions) => { + return { + result: Promise.reject(), + } as NgbModalRef; + } + ); + spyOn(skillEditorStateService, 'saveSkill'); - component.saveChanges(); - tick(); + component.saveChanges(); + tick(); - expect(ngbModalSpy).toHaveBeenCalled(); - })); + expect(ngbModalSpy).toHaveBeenCalled(); + }) + ); describe('on navigating to questions tab ', () => { - it('should open undo changes modal if there are unsaved ' + - 'changes', fakeAsync(() => { - // Setting unsaved changes to be two. - spyOn(undoRedoService, 'getChangeCount') - .and.returnValue(2); - const ngbModalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ - componentInstance: MockNgbModalRef, - result: Promise.resolve() - }) as NgbModalRef; - }); - let navigateToQuestionsTabSpy = spyOn( - skillEditorRoutingService, 'navigateToQuestionsTab') - .and.returnValue(); + it( + 'should open undo changes modal if there are unsaved ' + 'changes', + fakeAsync(() => { + // Setting unsaved changes to be two. + spyOn(undoRedoService, 'getChangeCount').and.returnValue(2); + const ngbModalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + componentInstance: MockNgbModalRef, + result: Promise.resolve(), + } as NgbModalRef; + }); + let navigateToQuestionsTabSpy = spyOn( + skillEditorRoutingService, + 'navigateToQuestionsTab' + ).and.returnValue(); + + component.selectQuestionsTab(); + tick(); + + expect(ngbModalSpy).toHaveBeenCalled(); + expect(navigateToQuestionsTabSpy).not.toHaveBeenCalled(); + }) + ); - component.selectQuestionsTab(); - tick(); + it( + 'should close undo changes modal if somewhere outside is' + ' clicked', + fakeAsync(() => { + // Setting unsaved changes to be two. + spyOn(undoRedoService, 'getChangeCount').and.returnValue(2); + const ngbModalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + componentInstance: MockNgbModalRef, + result: Promise.reject(), + } as NgbModalRef; + }); + let navigateToQuestionsTabSpy = spyOn( + skillEditorRoutingService, + 'navigateToQuestionsTab' + ).and.returnValue(); + + component.selectQuestionsTab(); + tick(); + + expect(ngbModalSpy).toHaveBeenCalled(); + expect(navigateToQuestionsTabSpy).not.toHaveBeenCalled(); + }) + ); - expect(ngbModalSpy).toHaveBeenCalled(); - expect(navigateToQuestionsTabSpy).not.toHaveBeenCalled(); - })); - - it('should close undo changes modal if somewhere outside is' + - ' clicked', fakeAsync(() => { - // Setting unsaved changes to be two. - spyOn(undoRedoService, 'getChangeCount') - .and.returnValue(2); - const ngbModalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ - componentInstance: MockNgbModalRef, - result: Promise.reject() - }) as NgbModalRef; - }); - let navigateToQuestionsTabSpy = spyOn( - skillEditorRoutingService, 'navigateToQuestionsTab') - .and.returnValue(); + it( + 'should navigate to questions tab if there are no unsaved ' + 'changes', + () => { + // Setting unsaved changes to be zero. + spyOn(undoRedoService, 'getChangeCount').and.returnValue(0); + let navigateToQuestionsTabSpy = spyOn( + skillEditorRoutingService, + 'navigateToQuestionsTab' + ).and.returnValue(); - component.selectQuestionsTab(); - tick(); + component.selectQuestionsTab(); - expect(ngbModalSpy).toHaveBeenCalled(); - expect(navigateToQuestionsTabSpy).not.toHaveBeenCalled(); - })); - - it('should navigate to questions tab if there are no unsaved ' + - 'changes', () => { - // Setting unsaved changes to be zero. - spyOn(undoRedoService, 'getChangeCount') - .and.returnValue(0); - let navigateToQuestionsTabSpy = spyOn( - skillEditorRoutingService, 'navigateToQuestionsTab') - .and.returnValue(); - - component.selectQuestionsTab(); - - expect(navigateToQuestionsTabSpy).toHaveBeenCalled(); - }); + expect(navigateToQuestionsTabSpy).toHaveBeenCalled(); + } + ); }); }); diff --git a/core/templates/pages/skill-editor-page/navbar/skill-editor-navbar.component.ts b/core/templates/pages/skill-editor-page/navbar/skill-editor-navbar.component.ts index 6a8caf86903b..d101531c1bd2 100644 --- a/core/templates/pages/skill-editor-page/navbar/skill-editor-navbar.component.ts +++ b/core/templates/pages/skill-editor-page/navbar/skill-editor-navbar.component.ts @@ -16,19 +16,19 @@ * @fileoverview Component for the navbar of the skill editor. */ -import { Subscription } from 'rxjs'; -import { SkillEditorSaveModalComponent } from '../modal-templates/skill-editor-save-modal.component'; -import { SavePendingChangesModalComponent } from 'components/save-pending-changes/save-pending-changes-modal.component'; -import { Component, OnInit } from '@angular/core'; -import { SkillEditorRoutingService } from '../services/skill-editor-routing.service'; -import { SkillEditorStateService } from '../services/skill-editor-state.service'; -import { UndoRedoService } from 'domain/editor/undo_redo/undo-redo.service'; -import { UrlService } from 'services/contextual/url.service'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { AlertsService } from 'services/alerts.service'; -import { Skill } from 'domain/skill/SkillObjectFactory'; -import { SkillUpdateService } from 'domain/skill/skill-update.service'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Subscription} from 'rxjs'; +import {SkillEditorSaveModalComponent} from '../modal-templates/skill-editor-save-modal.component'; +import {SavePendingChangesModalComponent} from 'components/save-pending-changes/save-pending-changes-modal.component'; +import {Component, OnInit} from '@angular/core'; +import {SkillEditorRoutingService} from '../services/skill-editor-routing.service'; +import {SkillEditorStateService} from '../services/skill-editor-state.service'; +import {UndoRedoService} from 'domain/editor/undo_redo/undo-redo.service'; +import {UrlService} from 'services/contextual/url.service'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {AlertsService} from 'services/alerts.service'; +import {Skill} from 'domain/skill/SkillObjectFactory'; +import {SkillUpdateService} from 'domain/skill/skill-update.service'; +import {downgradeComponent} from '@angular/upgrade/static'; @Component({ selector: 'oppia-skill-editor-navbar', @@ -58,7 +58,6 @@ export class SkillEditorNavabarComponent implements OnInit { ACTIVE_TAB_QUESTIONS = 'Questions'; ACTIVE_TAB_PREVIEW = 'Preview'; - getActiveTabName(): string { return this.skillEditorRoutingService.getActiveTabName(); } @@ -85,24 +84,26 @@ export class SkillEditorNavabarComponent implements OnInit { } isSkillSaveable(): boolean { - return ( - this.getChangeListCount() > 0 && - this.getWarningsCount() === 0 - ); + return this.getChangeListCount() > 0 && this.getWarningsCount() === 0; } saveChanges(): void { - this.ngbModal.open(SkillEditorSaveModalComponent, { - backdrop: 'static', - }).result.then((commitMessage) => { - this.skillEditorStateService.saveSkill(commitMessage, () => { - this.alertsService.addSuccessMessage('Changes Saved.'); - }); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + this.ngbModal + .open(SkillEditorSaveModalComponent, { + backdrop: 'static', + }) + .result.then( + commitMessage => { + this.skillEditorStateService.saveSkill(commitMessage, () => { + this.alertsService.addSuccessMessage('Changes Saved.'); + }); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } toggleNavigationOptions(): void { @@ -130,14 +131,13 @@ export class SkillEditorNavabarComponent implements OnInit { // discarded, the misconceptions won't be saved, but there will be // some questions with these now non-existent misconceptions. if (this.undoRedoService.getChangeCount() > 0) { - const modalRef = this.ngbModal.open( - SavePendingChangesModalComponent, { - backdrop: true - }); + const modalRef = this.ngbModal.open(SavePendingChangesModalComponent, { + backdrop: true, + }); - modalRef.componentInstance.body = ( + modalRef.componentInstance.body = 'Please save all pending ' + - 'changes before viewing the questions list.'); + 'changes before viewing the questions list.'; modalRef.result.then(null, () => { // Note to developers: @@ -153,22 +153,22 @@ export class SkillEditorNavabarComponent implements OnInit { ngOnInit(): void { this.activeTab = this.ACTIVE_TAB_EDITOR; this.directiveSubscriptions.add( - this.skillEditorStateService.onSkillChange.subscribe( - () => { - this.skill = this.skillEditorStateService.getSkill(); - }), + this.skillEditorStateService.onSkillChange.subscribe(() => { + this.skill = this.skillEditorStateService.getSkill(); + }) ); this.directiveSubscriptions.add( - this.skillUpdateService.onPrerequisiteSkillChange.subscribe( - () => {} - )); + this.skillUpdateService.onPrerequisiteSkillChange.subscribe(() => {}) + ); this.directiveSubscriptions.add( this.undoRedoService._undoRedoChangeEventEmitter.subscribe(() => {}) ); } } -angular.module('oppia').directive('oppiaSkillEditorNavbar', +angular.module('oppia').directive( + 'oppiaSkillEditorNavbar', downgradeComponent({ - component: SkillEditorNavabarComponent - }) as angular.IDirectiveFactory); + component: SkillEditorNavabarComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/skill-editor-page/questions-tab/skill-questions-tab.component.spec.ts b/core/templates/pages/skill-editor-page/questions-tab/skill-questions-tab.component.spec.ts index a7a013524216..2c2d0e223987 100644 --- a/core/templates/pages/skill-editor-page/questions-tab/skill-questions-tab.component.spec.ts +++ b/core/templates/pages/skill-editor-page/questions-tab/skill-questions-tab.component.spec.ts @@ -16,12 +16,12 @@ * @fileoverview Unit tests for the Skill question tab Component. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { SkillEditorStateService } from '../services/skill-editor-state.service'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { Skill, SkillObjectFactory } from 'domain/skill/SkillObjectFactory'; -import { SkillQuestionsTabComponent } from './skill-questions-tab.component'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {SkillEditorStateService} from '../services/skill-editor-state.service'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import {Skill, SkillObjectFactory} from 'domain/skill/SkillObjectFactory'; +import {SkillQuestionsTabComponent} from './skill-questions-tab.component'; describe('Skill question tab component', () => { let component: SkillQuestionsTabComponent; @@ -35,10 +35,8 @@ describe('Skill question tab component', () => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], declarations: [SkillQuestionsTabComponent], - providers: [ - SkillEditorStateService - ], - schemas: [NO_ERRORS_SCHEMA] + providers: [SkillEditorStateService], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); @@ -53,12 +51,12 @@ describe('Skill question tab component', () => { name: 'test name', notes: 'test notes', feedback: 'test feedback', - must_be_addressed: true + must_be_addressed: true, }; const rubricDict = { difficulty: 'medium', - explanations: ['explanation'] + explanations: ['explanation'], }; const skillContentsDict = { @@ -68,8 +66,8 @@ describe('Skill question tab component', () => { }, worked_examples: [], recorded_voiceovers: { - voiceovers_mapping: {} - } + voiceovers_mapping: {}, + }, }; sampleSkill = skillObjectFactory.createFromBackendDict({ @@ -83,11 +81,12 @@ describe('Skill question tab component', () => { next_misconception_id: 3, prerequisite_skill_ids: ['skill_1'], superseding_skill_id: 'skill0', - all_questions_merged: true + all_questions_merged: true, }); - spyOnProperty(skillEditorStateService, 'onSkillChange') - .and.returnValue(initEventEmitter); + spyOnProperty(skillEditorStateService, 'onSkillChange').and.returnValue( + initEventEmitter + ); }); afterEach(() => { @@ -96,37 +95,39 @@ describe('Skill question tab component', () => { it('should fetch skill when initialized', () => { const fetchSkillSpy = spyOn( - skillEditorStateService, 'getSkill').and.returnValue(sampleSkill); + skillEditorStateService, + 'getSkill' + ).and.returnValue(sampleSkill); spyOn(skillEditorStateService, 'getGroupedSkillSummaries'); component.ngOnInit(); initEventEmitter.emit(); expect(fetchSkillSpy).toHaveBeenCalled(); - expect( - skillEditorStateService.getGroupedSkillSummaries).toHaveBeenCalled(); + expect(skillEditorStateService.getGroupedSkillSummaries).toHaveBeenCalled(); }); it('should not initialize when skill is not available', () => { - const fetchSkillSpy = spyOn( - skillEditorStateService, 'getSkill'); + const fetchSkillSpy = spyOn(skillEditorStateService, 'getSkill'); spyOn(skillEditorStateService, 'getGroupedSkillSummaries'); component.ngOnInit(); expect(fetchSkillSpy).toHaveBeenCalled(); expect( - skillEditorStateService.getGroupedSkillSummaries).not.toHaveBeenCalled(); + skillEditorStateService.getGroupedSkillSummaries + ).not.toHaveBeenCalled(); }); it('should initialize when skill is available', () => { const fetchSkillSpy = spyOn( - skillEditorStateService, 'getSkill').and.returnValue(sampleSkill); + skillEditorStateService, + 'getSkill' + ).and.returnValue(sampleSkill); spyOn(skillEditorStateService, 'getGroupedSkillSummaries'); component.ngOnInit(); expect(fetchSkillSpy).toHaveBeenCalled(); - expect( - skillEditorStateService.getGroupedSkillSummaries).toHaveBeenCalled(); + expect(skillEditorStateService.getGroupedSkillSummaries).toHaveBeenCalled(); }); }); diff --git a/core/templates/pages/skill-editor-page/questions-tab/skill-questions-tab.component.ts b/core/templates/pages/skill-editor-page/questions-tab/skill-questions-tab.component.ts index 716794316185..6c877118826c 100644 --- a/core/templates/pages/skill-editor-page/questions-tab/skill-questions-tab.component.ts +++ b/core/templates/pages/skill-editor-page/questions-tab/skill-questions-tab.component.ts @@ -16,16 +16,19 @@ * @fileoverview Component for the questions tab. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { Rubric } from 'domain/skill/rubric.model'; -import { Skill } from 'domain/skill/SkillObjectFactory'; -import { Subscription } from 'rxjs'; -import { GroupedSkillSummaries, SkillEditorStateService } from '../services/skill-editor-state.service'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {Rubric} from 'domain/skill/rubric.model'; +import {Skill} from 'domain/skill/SkillObjectFactory'; +import {Subscription} from 'rxjs'; +import { + GroupedSkillSummaries, + SkillEditorStateService, +} from '../services/skill-editor-state.service'; @Component({ selector: 'oppia-questions-tab', - templateUrl: './skill-questions-tab.component.html' + templateUrl: './skill-questions-tab.component.html', }) export class SkillQuestionsTabComponent implements OnInit, OnDestroy { // These properties below are initialized using Angular lifecycle hooks @@ -35,18 +38,15 @@ export class SkillQuestionsTabComponent implements OnInit, OnDestroy { groupedSkillSummaries!: GroupedSkillSummaries; skillIdToRubricsObject: Record = {}; - constructor( - private skillEditorStateService: SkillEditorStateService - ) {} + constructor(private skillEditorStateService: SkillEditorStateService) {} directiveSubscriptions = new Subscription(); _init(): void { this.skill = this.skillEditorStateService.getSkill(); - this.groupedSkillSummaries = ( - this.skillEditorStateService.getGroupedSkillSummaries()); + this.groupedSkillSummaries = + this.skillEditorStateService.getGroupedSkillSummaries(); this.skillIdToRubricsObject = {}; - this.skillIdToRubricsObject[this.skill.getId()] = - this.skill.getRubrics(); + this.skillIdToRubricsObject[this.skill.getId()] = this.skill.getRubrics(); } ngOnInit(): void { @@ -54,8 +54,7 @@ export class SkillQuestionsTabComponent implements OnInit, OnDestroy { this._init(); } this.directiveSubscriptions.add( - this.skillEditorStateService.onSkillChange.subscribe( - () => this._init()) + this.skillEditorStateService.onSkillChange.subscribe(() => this._init()) ); } @@ -64,7 +63,9 @@ export class SkillQuestionsTabComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive('oppiaQuestionsTab', +angular.module('oppia').directive( + 'oppiaQuestionsTab', downgradeComponent({ - component: SkillQuestionsTabComponent - }) as angular.IDirectiveFactory); + component: SkillQuestionsTabComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/skill-editor-page/services/skill-editor-routing.service.spec.ts b/core/templates/pages/skill-editor-page/services/skill-editor-routing.service.spec.ts index d20eef9e0c52..22d88258ede4 100644 --- a/core/templates/pages/skill-editor-page/services/skill-editor-routing.service.spec.ts +++ b/core/templates/pages/skill-editor-page/services/skill-editor-routing.service.spec.ts @@ -16,17 +16,15 @@ * @fileoverview Unit tests for SkillEditorRoutingService. */ -import { fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { SkillEditorRoutingService } from 'pages/skill-editor-page/services/skill-editor-routing.service'; +import {fakeAsync, TestBed, tick} from '@angular/core/testing'; +import {SkillEditorRoutingService} from 'pages/skill-editor-page/services/skill-editor-routing.service'; describe('Skill Editor Routing Service', () => { let sers: SkillEditorRoutingService; beforeEach(() => { TestBed.configureTestingModule({ - providers: [ - SkillEditorRoutingService - ] + providers: [SkillEditorRoutingService], }); sers = TestBed.inject(SkillEditorRoutingService); @@ -50,26 +48,25 @@ describe('Skill Editor Routing Service', () => { expect(sers.getTabStatuses()).toBe('main'); })); - it('should toggle between main tab, questions and preview tab', - fakeAsync(() => { - sers.navigateToQuestionsTab(); - tick(); + it('should toggle between main tab, questions and preview tab', fakeAsync(() => { + sers.navigateToQuestionsTab(); + tick(); - expect(sers.getActiveTabName()).toBe('questions'); - expect(sers.getTabStatuses()).toBe('questions'); + expect(sers.getActiveTabName()).toBe('questions'); + expect(sers.getTabStatuses()).toBe('questions'); - sers.navigateToPreviewTab(); - tick(); + sers.navigateToPreviewTab(); + tick(); - expect(sers.getActiveTabName()).toBe('preview'); - expect(sers.getTabStatuses()).toBe('preview'); + expect(sers.getActiveTabName()).toBe('preview'); + expect(sers.getTabStatuses()).toBe('preview'); - sers.navigateToMainTab(); - tick(); + sers.navigateToMainTab(); + tick(); - expect(sers.getActiveTabName()).toBe('main'); - expect(sers.getTabStatuses()).toBe('main'); - })); + expect(sers.getActiveTabName()).toBe('main'); + expect(sers.getTabStatuses()).toBe('main'); + })); it('should open the question-editor directly', fakeAsync(() => { sers.creatingNewQuestion(true); diff --git a/core/templates/pages/skill-editor-page/services/skill-editor-routing.service.ts b/core/templates/pages/skill-editor-page/services/skill-editor-routing.service.ts index be7ac39781a2..a7da5ea8c0f9 100644 --- a/core/templates/pages/skill-editor-page/services/skill-editor-routing.service.ts +++ b/core/templates/pages/skill-editor-page/services/skill-editor-routing.service.ts @@ -16,12 +16,12 @@ * @fileoverview Service that handles routing for the skill editor page. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {WindowRef} from 'services/contextual/window-ref.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class SkillEditorRoutingService { MAIN_TAB = 'main'; @@ -30,9 +30,7 @@ export class SkillEditorRoutingService { activeTab = this.MAIN_TAB; questionIsBeingCreated: boolean = false; - constructor( - private windowRef: WindowRef, - ) { + constructor(private windowRef: WindowRef) { let currentHash: string = this.windowRef.nativeWindow.location.hash; this._changeTab(currentHash.substring(1, currentHash.length)); } @@ -89,5 +87,9 @@ export class SkillEditorRoutingService { } } -angular.module('oppia').factory('SkillEditorRoutingService', - downgradeInjectable(SkillEditorRoutingService)); +angular + .module('oppia') + .factory( + 'SkillEditorRoutingService', + downgradeInjectable(SkillEditorRoutingService) + ); diff --git a/core/templates/pages/skill-editor-page/services/skill-editor-staleness-detection.service.spec.ts b/core/templates/pages/skill-editor-page/services/skill-editor-staleness-detection.service.spec.ts index 84f0bee39aaf..9224428d41ab 100644 --- a/core/templates/pages/skill-editor-page/services/skill-editor-staleness-detection.service.spec.ts +++ b/core/templates/pages/skill-editor-page/services/skill-editor-staleness-detection.service.spec.ts @@ -15,24 +15,27 @@ * @fileoverview Unit tests for skill editor staleness detection service. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TestBed } from '@angular/core/testing'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { UndoRedoService } from 'domain/editor/undo_redo/undo-redo.service'; -import { EntityEditorBrowserTabsInfo } from 'domain/entity_editor_browser_tabs_info/entity-editor-browser-tabs-info.model'; -import { SkillBackendDict, SkillObjectFactory } from 'domain/skill/SkillObjectFactory'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { FaviconService } from 'services/favicon.service'; -import { LocalStorageService } from 'services/local-storage.service'; -import { StalenessDetectionService } from 'services/staleness-detection.service'; -import { SkillEditorStalenessDetectionService } from './skill-editor-staleness-detection.service'; -import { SkillEditorStateService } from './skill-editor-state.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TestBed} from '@angular/core/testing'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {UndoRedoService} from 'domain/editor/undo_redo/undo-redo.service'; +import {EntityEditorBrowserTabsInfo} from 'domain/entity_editor_browser_tabs_info/entity-editor-browser-tabs-info.model'; +import { + SkillBackendDict, + SkillObjectFactory, +} from 'domain/skill/SkillObjectFactory'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {FaviconService} from 'services/favicon.service'; +import {LocalStorageService} from 'services/local-storage.service'; +import {StalenessDetectionService} from 'services/staleness-detection.service'; +import {SkillEditorStalenessDetectionService} from './skill-editor-staleness-detection.service'; +import {SkillEditorStateService} from './skill-editor-state.service'; class MockWindowRef { nativeWindow = { location: { - reload: () => {} - } + reload: () => {}, + }, }; } @@ -56,23 +59,29 @@ const skillContentsDict = { const skillDict: SkillBackendDict = { id: 'skill_id_1', description: 'Description', - misconceptions: [{ - id: 2, - name: 'test name', - notes: 'test notes', - feedback: 'test feedback', - must_be_addressed: true, - }], - rubrics: [{ - difficulty: 'Easy', - explanations: ['explanation'], - }, { - difficulty: 'Medium', - explanations: ['explanation'], - }, { - difficulty: 'Hard', - explanations: ['explanation'], - }], + misconceptions: [ + { + id: 2, + name: 'test name', + notes: 'test notes', + feedback: 'test feedback', + must_be_addressed: true, + }, + ], + rubrics: [ + { + difficulty: 'Easy', + explanations: ['explanation'], + }, + { + difficulty: 'Medium', + explanations: ['explanation'], + }, + { + difficulty: 'Hard', + explanations: ['explanation'], + }, + ], skill_contents: skillContentsDict, language_code: 'en', version: 3, @@ -83,8 +92,7 @@ const skillDict: SkillBackendDict = { }; describe('Skill editor staleness detection service', () => { - let skillEditorStalenessDetectionService: - SkillEditorStalenessDetectionService; + let skillEditorStalenessDetectionService: SkillEditorStalenessDetectionService; let skillObjectFactory: SkillObjectFactory; let skillEditorStateService: SkillEditorStateService; let localStorageService: LocalStorageService; @@ -106,13 +114,14 @@ describe('Skill editor staleness detection service', () => { UndoRedoService, { provide: WindowRef, - useValue: mockWindowRef - } - ] + useValue: mockWindowRef, + }, + ], }).compileComponents(); - skillEditorStalenessDetectionService = - TestBed.inject(SkillEditorStalenessDetectionService); + skillEditorStalenessDetectionService = TestBed.inject( + SkillEditorStalenessDetectionService + ); skillObjectFactory = TestBed.inject(SkillObjectFactory); skillEditorStateService = TestBed.inject(SkillEditorStateService); localStorageService = TestBed.inject(LocalStorageService); @@ -126,14 +135,21 @@ describe('Skill editor staleness detection service', () => { let skill = skillObjectFactory.createFromBackendDict(skillDict); spyOn(skillEditorStateService, 'getSkill').and.returnValue(skill); let skillEditorBrowserTabsInfo = EntityEditorBrowserTabsInfo.create( - 'skill', 'skill_id', 2, 1, false); + 'skill', + 'skill_id', + 2, + 1, + false + ); spyOn( - localStorageService, 'getEntityEditorBrowserTabsInfo' + localStorageService, + 'getEntityEditorBrowserTabsInfo' ).and.returnValue(skillEditorBrowserTabsInfo); spyOn(mockWindowRef.nativeWindow.location, 'reload'); spyOn(faviconService, 'setFavicon').and.callFake(() => {}); spyOn( - skillEditorStalenessDetectionService, 'showStaleTabInfoModal' + skillEditorStalenessDetectionService, + 'showStaleTabInfoModal' ).and.callThrough(); class MockNgbModalRef { result = Promise.resolve(); @@ -149,68 +165,79 @@ describe('Skill editor staleness detection service', () => { skillEditorStalenessDetectionService.showStaleTabInfoModal ).toHaveBeenCalled(); expect(faviconService.setFavicon).toHaveBeenCalledWith( - '/assets/images/favicon_alert/favicon_alert.ico'); + '/assets/images/favicon_alert/favicon_alert.ico' + ); expect(ngbModal.open).toHaveBeenCalled(); }); - it('should open or close presence of unsaved changes info modal ' + - 'depending on the presence of unsaved changes on some other tab', () => { - let skill = skillObjectFactory.createFromBackendDict(skillDict); - spyOn(skillEditorStateService, 'getSkill').and.returnValue(skill); - let skillEditorBrowserTabsInfo = EntityEditorBrowserTabsInfo.create( - 'skill', 'skill_id', 2, 2, true); - spyOn( - localStorageService, 'getEntityEditorBrowserTabsInfo' - ).and.returnValue(skillEditorBrowserTabsInfo); - spyOn(mockWindowRef.nativeWindow.location, 'reload'); - spyOn( - skillEditorStalenessDetectionService, 'showPresenceOfUnsavedChangesModal' - ).and.callThrough(); - class MockNgbModalRef { - result = Promise.resolve(); - componentInstance = {}; - dismiss() {} + it( + 'should open or close presence of unsaved changes info modal ' + + 'depending on the presence of unsaved changes on some other tab', + () => { + let skill = skillObjectFactory.createFromBackendDict(skillDict); + spyOn(skillEditorStateService, 'getSkill').and.returnValue(skill); + let skillEditorBrowserTabsInfo = EntityEditorBrowserTabsInfo.create( + 'skill', + 'skill_id', + 2, + 2, + true + ); + spyOn( + localStorageService, + 'getEntityEditorBrowserTabsInfo' + ).and.returnValue(skillEditorBrowserTabsInfo); + spyOn(mockWindowRef.nativeWindow.location, 'reload'); + spyOn( + skillEditorStalenessDetectionService, + 'showPresenceOfUnsavedChangesModal' + ).and.callThrough(); + class MockNgbModalRef { + result = Promise.resolve(); + componentInstance = {}; + dismiss() {} + } + const ngbModalRef = new MockNgbModalRef() as NgbModalRef; + spyOn(ngbModalRef, 'dismiss'); + spyOn(ngbModal, 'open').and.returnValue(ngbModalRef); + spyOn(undoRedoService, 'getChangeCount').and.returnValue(0); + spyOn( + stalenessDetectionService, + 'doesSomeOtherEntityEditorPageHaveUnsavedChanges' + ).and.returnValues(true, false); + + skillEditorStalenessDetectionService.init(); + skillEditorStalenessDetectionService.presenceOfUnsavedChangesEventEmitter.emit(); + + expect( + skillEditorStalenessDetectionService.showPresenceOfUnsavedChangesModal + ).toHaveBeenCalled(); + expect(ngbModal.open).toHaveBeenCalled(); + + skillEditorStalenessDetectionService.presenceOfUnsavedChangesEventEmitter.emit(); + + expect(ngbModalRef.dismiss).toHaveBeenCalled(); } - const ngbModalRef = new MockNgbModalRef() as NgbModalRef; - spyOn(ngbModalRef, 'dismiss'); - spyOn(ngbModal, 'open').and.returnValue(ngbModalRef); - spyOn(undoRedoService, 'getChangeCount').and.returnValue(0); - spyOn( - stalenessDetectionService, - 'doesSomeOtherEntityEditorPageHaveUnsavedChanges' - ).and.returnValues(true, false); - - skillEditorStalenessDetectionService.init(); - skillEditorStalenessDetectionService - .presenceOfUnsavedChangesEventEmitter.emit(); - - expect( - skillEditorStalenessDetectionService.showPresenceOfUnsavedChangesModal - ).toHaveBeenCalled(); - expect(ngbModal.open).toHaveBeenCalled(); - - skillEditorStalenessDetectionService - .presenceOfUnsavedChangesEventEmitter.emit(); - - expect(ngbModalRef.dismiss).toHaveBeenCalled(); - }); - - it('should not show the presence of unsaved changes modal on the page' + - 'which itself contains those unsaved changes', () => { - class MockNgbModalRef { - result = Promise.resolve(); - componentInstance = {}; - dismiss() {} + ); + + it( + 'should not show the presence of unsaved changes modal on the page' + + 'which itself contains those unsaved changes', + () => { + class MockNgbModalRef { + result = Promise.resolve(); + componentInstance = {}; + dismiss() {} + } + const ngbModalRef = new MockNgbModalRef() as NgbModalRef; + spyOn(ngbModalRef, 'dismiss'); + spyOn(ngbModal, 'open').and.returnValue(ngbModalRef); + spyOn(undoRedoService, 'getChangeCount').and.returnValue(1); + + skillEditorStalenessDetectionService.init(); + skillEditorStalenessDetectionService.presenceOfUnsavedChangesEventEmitter.emit(); + + expect(ngbModal.open).not.toHaveBeenCalled(); } - const ngbModalRef = new MockNgbModalRef() as NgbModalRef; - spyOn(ngbModalRef, 'dismiss'); - spyOn(ngbModal, 'open').and.returnValue(ngbModalRef); - spyOn(undoRedoService, 'getChangeCount').and.returnValue(1); - - skillEditorStalenessDetectionService.init(); - skillEditorStalenessDetectionService - .presenceOfUnsavedChangesEventEmitter.emit(); - - expect(ngbModal.open).not.toHaveBeenCalled(); - }); + ); }); diff --git a/core/templates/pages/skill-editor-page/services/skill-editor-staleness-detection.service.ts b/core/templates/pages/skill-editor-page/services/skill-editor-staleness-detection.service.ts index 0332241a68f4..63b186fa4e65 100644 --- a/core/templates/pages/skill-editor-page/services/skill-editor-staleness-detection.service.ts +++ b/core/templates/pages/skill-editor-page/services/skill-editor-staleness-detection.service.ts @@ -16,23 +16,23 @@ * @fileoverview Service for emitting events when a skill editor tab is stale. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { EventEmitter, Injectable } from '@angular/core'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { StalenessDetectionService } from 'services/staleness-detection.service'; -import { EntityEditorBrowserTabsInfoDomainConstants } from 'domain/entity_editor_browser_tabs_info/entity-editor-browser-tabs-info-domain.constants'; -import { SkillEditorStateService } from './skill-editor-state.service'; -import { EntityEditorBrowserTabsInfo } from 'domain/entity_editor_browser_tabs_info/entity-editor-browser-tabs-info.model'; -import { FaviconService } from 'services/favicon.service'; -import { LocalStorageService } from 'services/local-storage.service'; -import { UndoRedoService } from 'domain/editor/undo_redo/undo-redo.service'; -import { StaleTabInfoModalComponent } from 'components/stale-tab-info/stale-tab-info-modal.component'; -import { UnsavedChangesStatusInfoModalComponent } from 'components/unsaved-changes-status-info/unsaved-changes-status-info-modal.component'; -import { AppConstants } from 'app.constants'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {EventEmitter, Injectable} from '@angular/core'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {StalenessDetectionService} from 'services/staleness-detection.service'; +import {EntityEditorBrowserTabsInfoDomainConstants} from 'domain/entity_editor_browser_tabs_info/entity-editor-browser-tabs-info-domain.constants'; +import {SkillEditorStateService} from './skill-editor-state.service'; +import {EntityEditorBrowserTabsInfo} from 'domain/entity_editor_browser_tabs_info/entity-editor-browser-tabs-info.model'; +import {FaviconService} from 'services/favicon.service'; +import {LocalStorageService} from 'services/local-storage.service'; +import {UndoRedoService} from 'domain/editor/undo_redo/undo-redo.service'; +import {StaleTabInfoModalComponent} from 'components/stale-tab-info/stale-tab-info-modal.component'; +import {UnsavedChangesStatusInfoModalComponent} from 'components/unsaved-changes-status-info/unsaved-changes-status-info-modal.component'; +import {AppConstants} from 'app.constants'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class SkillEditorStalenessDetectionService { _staleTabEventEmitter = new EventEmitter(); @@ -64,10 +64,11 @@ export class SkillEditorStalenessDetectionService { showStaleTabInfoModal(): void { const skill = this.skillEditorStateService.getSkill(); // Return null if skill id is not present in the local storage. - const skillEditorBrowserTabsInfo: EntityEditorBrowserTabsInfo | null = ( + const skillEditorBrowserTabsInfo: EntityEditorBrowserTabsInfo | null = this.localStorageService.getEntityEditorBrowserTabsInfo( - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_SKILL_EDITOR_BROWSER_TABS, skill.getId())); + EntityEditorBrowserTabsInfoDomainConstants.OPENED_SKILL_EDITOR_BROWSER_TABS, + skill.getId() + ); if ( skillEditorBrowserTabsInfo && @@ -75,14 +76,16 @@ export class SkillEditorStalenessDetectionService { ) { this.faviconService.setFavicon(AppConstants.FAVICON_ALERT_PATH); this.ngbModal.dismissAll(); - const modalRef = this.ngbModal.open( - StaleTabInfoModalComponent, { - backdrop: 'static', - }); + const modalRef = this.ngbModal.open(StaleTabInfoModalComponent, { + backdrop: 'static', + }); modalRef.componentInstance.entity = 'skill'; - modalRef.result.then(() => { - this.windowRef.nativeWindow.location.reload(); - }, () => {}); + modalRef.result.then( + () => { + this.windowRef.nativeWindow.location.reload(); + }, + () => {} + ); } } @@ -91,19 +94,23 @@ export class SkillEditorStalenessDetectionService { return; } if ( - this.stalenessDetectionService - .doesSomeOtherEntityEditorPageHaveUnsavedChanges( - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_SKILL_EDITOR_BROWSER_TABS, - this.skillEditorStateService.getSkill().getId()) + this.stalenessDetectionService.doesSomeOtherEntityEditorPageHaveUnsavedChanges( + EntityEditorBrowserTabsInfoDomainConstants.OPENED_SKILL_EDITOR_BROWSER_TABS, + this.skillEditorStateService.getSkill().getId() + ) ) { this.ngbModal.dismissAll(); this.unsavedChangesWarningModalRef = this.ngbModal.open( - UnsavedChangesStatusInfoModalComponent, { + UnsavedChangesStatusInfoModalComponent, + { backdrop: 'static', - }); + } + ); this.unsavedChangesWarningModalRef.componentInstance.entity = 'skill'; - this.unsavedChangesWarningModalRef.result.then(() => {}, () => {}); + this.unsavedChangesWarningModalRef.result.then( + () => {}, + () => {} + ); } else if (this.unsavedChangesWarningModalRef) { this.unsavedChangesWarningModalRef.dismiss(); } @@ -118,6 +125,9 @@ export class SkillEditorStalenessDetectionService { } } -angular.module('oppia').factory( - 'SkillEditorStalenessDetectionService', - downgradeInjectable(SkillEditorStalenessDetectionService)); +angular + .module('oppia') + .factory( + 'SkillEditorStalenessDetectionService', + downgradeInjectable(SkillEditorStalenessDetectionService) + ); diff --git a/core/templates/pages/skill-editor-page/services/skill-editor-state.service.spec.ts b/core/templates/pages/skill-editor-page/services/skill-editor-state.service.spec.ts index 190b9fdc5169..c4cc062a33c0 100644 --- a/core/templates/pages/skill-editor-page/services/skill-editor-state.service.spec.ts +++ b/core/templates/pages/skill-editor-page/services/skill-editor-state.service.spec.ts @@ -16,19 +16,22 @@ * @fileoverview Unit tests for SkillEditorStateService.js */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { fakeAsync, TestBed, tick } from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {fakeAsync, TestBed, tick} from '@angular/core/testing'; -import { SkillBackendApiService } from 'domain/skill/skill-backend-api.service'; -import { SkillRights, SkillRightsBackendDict } from 'domain/skill/skill-rights.model'; -import { SkillRightsBackendApiService } from 'domain/skill/skill-rights-backend-api.service'; -import { SkillUpdateService } from 'domain/skill/skill-update.service'; +import {SkillBackendApiService} from 'domain/skill/skill-backend-api.service'; +import { + SkillRights, + SkillRightsBackendDict, +} from 'domain/skill/skill-rights.model'; +import {SkillRightsBackendApiService} from 'domain/skill/skill-rights-backend-api.service'; +import {SkillUpdateService} from 'domain/skill/skill-update.service'; import { Skill, SkillBackendDict, SkillObjectFactory, } from 'domain/skill/SkillObjectFactory'; -import { SkillEditorStateService } from 'pages/skill-editor-page/services/skill-editor-state.service'; +import {SkillEditorStateService} from 'pages/skill-editor-page/services/skill-editor-state.service'; const skillContentsDict = { explanation: { @@ -50,23 +53,29 @@ const skillContentsDict = { const skillDict: SkillBackendDict = { id: 'skill_id_1', description: 'Description', - misconceptions: [{ - id: 2, - name: 'test name', - notes: 'test notes', - feedback: 'test feedback', - must_be_addressed: true, - }], - rubrics: [{ - difficulty: 'Easy', - explanations: ['explanation'], - }, { - difficulty: 'Medium', - explanations: ['explanation'], - }, { - difficulty: 'Hard', - explanations: ['explanation'], - }], + misconceptions: [ + { + id: 2, + name: 'test name', + notes: 'test notes', + feedback: 'test feedback', + must_be_addressed: true, + }, + ], + rubrics: [ + { + difficulty: 'Easy', + explanations: ['explanation'], + }, + { + difficulty: 'Medium', + explanations: ['explanation'], + }, + { + difficulty: 'Hard', + explanations: ['explanation'], + }, + ], skill_contents: skillContentsDict, language_code: 'en', version: 3, @@ -79,17 +88,21 @@ const skillDict: SkillBackendDict = { const skillDict2: SkillBackendDict = { id: 'skill_id_2', description: 'Description 2', - misconceptions: [{ - id: 2, - name: 'test name', - notes: 'test notes', - feedback: 'test feedback', - must_be_addressed: true, - }], - rubrics: [{ - difficulty: 'Easy', - explanations: ['explanation'], - }], + misconceptions: [ + { + id: 2, + name: 'test name', + notes: 'test notes', + feedback: 'test feedback', + must_be_addressed: true, + }, + ], + rubrics: [ + { + difficulty: 'Easy', + explanations: ['explanation'], + }, + ], skill_contents: skillContentsDict, language_code: 'en', version: 3, @@ -237,13 +250,11 @@ describe('Skill editor state service', () => { can_edit_skill_description: true, }; fakeSkillRightsBackendApiService.backendSkillRightsObject = - skillRightsObject; + skillRightsObject; fakeSkillBackendApiService.newBackendSkillObject = skillDict; fakeSkillBackendApiService.skillObject = - skillObjectFactory.createFromBackendDict( - skillDict - ); + skillObjectFactory.createFromBackendDict(skillDict); }); it('should test getters', () => { @@ -264,63 +275,59 @@ describe('Skill editor state service', () => { expect(skillEditorStateService.isLoadingSkill()).toBe(false); })); - it('should indicate a collection is no longer loading after an error', - fakeAsync(() => { - expect(skillEditorStateService.isLoadingSkill()).toBe(false); - fakeSkillBackendApiService.failure = 'Internal 500 error'; - skillEditorStateService.loadSkill('skill_id_1'); - expect(skillEditorStateService.isLoadingSkill()).toBe(true); - tick(1000); - expect(skillEditorStateService.isLoadingSkill()).toBe(false); - })); - - it('should report that a skill has loaded through loadSkill()', - fakeAsync(() => { - spyOn( - fakeSkillRightsBackendApiService, - 'fetchSkillRightsAsync' - ).and.callThrough(); - expect(skillEditorStateService.hasLoadedSkill()).toBe(false); - skillEditorStateService.loadSkill('skill_id_1'); - expect(skillEditorStateService.hasLoadedSkill()).toBe(false); - tick(1000); - expect(skillEditorStateService.hasLoadedSkill()).toBe(true); - const groupedSkillSummaries = - skillEditorStateService.getGroupedSkillSummaries(); - expect(groupedSkillSummaries.current.length).toEqual(2); - expect(groupedSkillSummaries.others.length).toEqual(2); - - expect(groupedSkillSummaries.current[0].id).toEqual('skill_id_1'); - expect(groupedSkillSummaries.current[1].id).toEqual('skill_id_2'); - - expect( - skillEditorStateService.getAssignedSkillTopicData() - ).toEqual({ - topicName: ['tester'] - }); - })); - - it('should return the last skill loaded as the same object', - fakeAsync(() => { - skillEditorStateService.setSkillRights( - SkillRights.createFromBackendDict(skillRightsObject)); - skillEditorStateService.loadSkill('skill_id_1'); - tick(1000); - const previousSkill = skillEditorStateService.getSkill(); - - fakeSkillBackendApiService.newBackendSkillObject = skillDict2; - fakeSkillBackendApiService.skillObject = - skillObjectFactory.createFromBackendDict(skillDict2); - - const expectedSkill = fakeSkillBackendApiService.skillObject; - expect(previousSkill).not.toEqual(expectedSkill); - skillEditorStateService.loadSkill('skill_id_2'); - tick(1000); - const actualSkill = skillEditorStateService.getSkill(); - expect(actualSkill).toEqual(expectedSkill); - expect(actualSkill).toBe(previousSkill); - expect(actualSkill).not.toBe(expectedSkill); - })); + it('should indicate a collection is no longer loading after an error', fakeAsync(() => { + expect(skillEditorStateService.isLoadingSkill()).toBe(false); + fakeSkillBackendApiService.failure = 'Internal 500 error'; + skillEditorStateService.loadSkill('skill_id_1'); + expect(skillEditorStateService.isLoadingSkill()).toBe(true); + tick(1000); + expect(skillEditorStateService.isLoadingSkill()).toBe(false); + })); + + it('should report that a skill has loaded through loadSkill()', fakeAsync(() => { + spyOn( + fakeSkillRightsBackendApiService, + 'fetchSkillRightsAsync' + ).and.callThrough(); + expect(skillEditorStateService.hasLoadedSkill()).toBe(false); + skillEditorStateService.loadSkill('skill_id_1'); + expect(skillEditorStateService.hasLoadedSkill()).toBe(false); + tick(1000); + expect(skillEditorStateService.hasLoadedSkill()).toBe(true); + const groupedSkillSummaries = + skillEditorStateService.getGroupedSkillSummaries(); + expect(groupedSkillSummaries.current.length).toEqual(2); + expect(groupedSkillSummaries.others.length).toEqual(2); + + expect(groupedSkillSummaries.current[0].id).toEqual('skill_id_1'); + expect(groupedSkillSummaries.current[1].id).toEqual('skill_id_2'); + + expect(skillEditorStateService.getAssignedSkillTopicData()).toEqual({ + topicName: ['tester'], + }); + })); + + it('should return the last skill loaded as the same object', fakeAsync(() => { + skillEditorStateService.setSkillRights( + SkillRights.createFromBackendDict(skillRightsObject) + ); + skillEditorStateService.loadSkill('skill_id_1'); + tick(1000); + const previousSkill = skillEditorStateService.getSkill(); + + fakeSkillBackendApiService.newBackendSkillObject = skillDict2; + fakeSkillBackendApiService.skillObject = + skillObjectFactory.createFromBackendDict(skillDict2); + + const expectedSkill = fakeSkillBackendApiService.skillObject; + expect(previousSkill).not.toEqual(expectedSkill); + skillEditorStateService.loadSkill('skill_id_2'); + tick(1000); + const actualSkill = skillEditorStateService.getSkill(); + expect(actualSkill).toEqual(expectedSkill); + expect(actualSkill).toBe(previousSkill); + expect(actualSkill).not.toBe(expectedSkill); + })); it('should fail to load a skill without first loading one', () => { expect(() => { @@ -328,51 +335,50 @@ describe('Skill editor state service', () => { }).toThrowError('Cannot save a skill before one is loaded.'); }); - it('should not save the skill if there are no pending changes', - fakeAsync(() => { - skillEditorStateService.loadSkill('skill_id_1'); - tick(1000); - expect(skillEditorStateService.saveSkill( + it('should not save the skill if there are no pending changes', fakeAsync(() => { + skillEditorStateService.loadSkill('skill_id_1'); + tick(1000); + expect( + skillEditorStateService.saveSkill( 'commit message', - () => 'Cannot save a skill before one is loaded.')).toBe( - false - ); - })); - - it('should be able to save the collection and pending changes', fakeAsync( - () => { - spyOn(fakeSkillBackendApiService, 'updateSkillAsync').and.callThrough(); - - skillEditorStateService.loadSkill('skill_id_1'); - tick(1000); - expect(skillEditorStateService.hasLoadedSkill()).toBeTrue(); - skillUpdateService.setSkillDescription( - skillEditorStateService.getSkill(), - 'new description' - ); - tick(1000); - - expect(skillEditorStateService.saveSkill('commit message', () => {})); - tick(1000); - - const expectedId = 'skill_id_1'; - const expectedVersion = 3; - const expectedCommitMessage = 'commit message'; - const updateSkillSpy = fakeSkillBackendApiService.updateSkillAsync; - expect(updateSkillSpy).toHaveBeenCalledWith( - expectedId, - expectedVersion, - expectedCommitMessage, - [ - { - property_name: 'description', - new_value: 'new description', - old_value: 'Description', - cmd: 'update_skill_property', - }, - ] - ); - })); + () => 'Cannot save a skill before one is loaded.' + ) + ).toBe(false); + })); + + it('should be able to save the collection and pending changes', fakeAsync(() => { + spyOn(fakeSkillBackendApiService, 'updateSkillAsync').and.callThrough(); + + skillEditorStateService.loadSkill('skill_id_1'); + tick(1000); + expect(skillEditorStateService.hasLoadedSkill()).toBeTrue(); + skillUpdateService.setSkillDescription( + skillEditorStateService.getSkill(), + 'new description' + ); + tick(1000); + + expect(skillEditorStateService.saveSkill('commit message', () => {})); + tick(1000); + + const expectedId = 'skill_id_1'; + const expectedVersion = 3; + const expectedCommitMessage = 'commit message'; + const updateSkillSpy = fakeSkillBackendApiService.updateSkillAsync; + expect(updateSkillSpy).toHaveBeenCalledWith( + expectedId, + expectedVersion, + expectedCommitMessage, + [ + { + property_name: 'description', + new_value: 'new description', + old_value: 'Description', + cmd: 'update_skill_property', + }, + ] + ); + })); it('should track whether it is currently saving the skill', fakeAsync(() => { skillEditorStateService.loadSkill('skill_id_1'); @@ -391,27 +397,26 @@ describe('Skill editor state service', () => { expect(skillEditorStateService.isSavingSkill()).toBe(false); })); - it('should indicate a skill is no longer saving after an error', - fakeAsync(() => { - skillEditorStateService.loadSkill('skill_id_1'); - tick(1000); - expect(skillEditorStateService.hasLoadedSkill()).toBeTrue(); - skillUpdateService.setSkillDescription( - skillEditorStateService.getSkill(), - 'new description' - ); - tick(1000); + it('should indicate a skill is no longer saving after an error', fakeAsync(() => { + skillEditorStateService.loadSkill('skill_id_1'); + tick(1000); + expect(skillEditorStateService.hasLoadedSkill()).toBeTrue(); + skillUpdateService.setSkillDescription( + skillEditorStateService.getSkill(), + 'new description' + ); + tick(1000); - expect(skillEditorStateService.isSavingSkill()).toBe(false); - fakeSkillBackendApiService.failure = 'Internal 500 error'; + expect(skillEditorStateService.isSavingSkill()).toBe(false); + fakeSkillBackendApiService.failure = 'Internal 500 error'; - skillEditorStateService.saveSkill('commit message', () => {}); + skillEditorStateService.saveSkill('commit message', () => {}); - expect(skillEditorStateService.isSavingSkill()).toBe(true); - tick(1000); + expect(skillEditorStateService.isSavingSkill()).toBe(true); + tick(1000); - expect(skillEditorStateService.isSavingSkill()).toBe(false); - })); + expect(skillEditorStateService.isSavingSkill()).toBe(false); + })); it('should request to load the skill rights from the backend', () => { spyOn( @@ -426,14 +431,15 @@ describe('Skill editor state service', () => { }); it('should be able to set a new skill rights with an in-place copy', () => { - skillEditorStateService.setSkillRights(SkillRights.createFromBackendDict({ - skill_id: 'skill_id', - can_edit_skill_description: true, - })); - const previousSkillRights = skillEditorStateService.getSkillRights(); - const expectedSkillRights = SkillRights.createFromBackendDict( - skillRightsObject + skillEditorStateService.setSkillRights( + SkillRights.createFromBackendDict({ + skill_id: 'skill_id', + can_edit_skill_description: true, + }) ); + const previousSkillRights = skillEditorStateService.getSkillRights(); + const expectedSkillRights = + SkillRights.createFromBackendDict(skillRightsObject); expect(previousSkillRights).not.toEqual(expectedSkillRights); skillEditorStateService.setSkillRights(expectedSkillRights); @@ -453,27 +459,38 @@ describe('Skill editor state service', () => { expect(skillEditorStateService.getSkillValidationIssues()).toEqual([]); })); - it('should update the skill description when calling ' + - '\'updateExistenceOfSkillDescription\'', fakeAsync(() => { - spyOn(fakeSkillBackendApiService, 'doesSkillWithDescriptionExistAsync') - .and.callThrough(); - let successCb = jasmine.createSpy('success'); - skillEditorStateService.updateExistenceOfSkillDescription( - 'description', successCb); - tick(); + it( + 'should update the skill description when calling ' + + "'updateExistenceOfSkillDescription'", + fakeAsync(() => { + spyOn( + fakeSkillBackendApiService, + 'doesSkillWithDescriptionExistAsync' + ).and.callThrough(); + let successCb = jasmine.createSpy('success'); + skillEditorStateService.updateExistenceOfSkillDescription( + 'description', + successCb + ); + tick(); - expect(successCb).toHaveBeenCalledWith(true); - })); + expect(successCb).toHaveBeenCalledWith(true); + }) + ); - it('should fail to update the skill description when ' + - 'description is empty', fakeAsync(() => { - spyOn(fakeSkillBackendApiService, 'doesSkillWithDescriptionExistAsync') - .and.callThrough(); - let successCb = jasmine.createSpy('success'); - skillEditorStateService.updateExistenceOfSkillDescription( - '', successCb); - tick(); + it( + 'should fail to update the skill description when ' + + 'description is empty', + fakeAsync(() => { + spyOn( + fakeSkillBackendApiService, + 'doesSkillWithDescriptionExistAsync' + ).and.callThrough(); + let successCb = jasmine.createSpy('success'); + skillEditorStateService.updateExistenceOfSkillDescription('', successCb); + tick(); - expect(successCb).not.toHaveBeenCalled(); - })); + expect(successCb).not.toHaveBeenCalled(); + }) + ); }); diff --git a/core/templates/pages/skill-editor-page/services/skill-editor-state.service.ts b/core/templates/pages/skill-editor-page/services/skill-editor-state.service.ts index dcdf97b0e825..83434807ef7d 100644 --- a/core/templates/pages/skill-editor-page/services/skill-editor-state.service.ts +++ b/core/templates/pages/skill-editor-page/services/skill-editor-state.service.ts @@ -19,18 +19,18 @@ import cloneDeep from 'lodash/cloneDeep'; -import { EventEmitter, Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {EventEmitter, Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; -import { UndoRedoService } from 'domain/editor/undo_redo/undo-redo.service'; -import { SkillBackendApiService } from 'domain/skill/skill-backend-api.service'; -import { SkillRights } from 'domain/skill/skill-rights.model'; -import { SkillRightsBackendApiService } from 'domain/skill/skill-rights-backend-api.service'; -import { SkillSummaryBackendDict } from 'domain/skill/skill-summary.model'; -import { Skill } from 'domain/skill/SkillObjectFactory'; -import { AlertsService } from 'services/alerts.service'; -import { QuestionsListService } from 'services/questions-list.service'; -import { LoaderService } from 'services/loader.service'; +import {UndoRedoService} from 'domain/editor/undo_redo/undo-redo.service'; +import {SkillBackendApiService} from 'domain/skill/skill-backend-api.service'; +import {SkillRights} from 'domain/skill/skill-rights.model'; +import {SkillRightsBackendApiService} from 'domain/skill/skill-rights-backend-api.service'; +import {SkillSummaryBackendDict} from 'domain/skill/skill-summary.model'; +import {Skill} from 'domain/skill/SkillObjectFactory'; +import {AlertsService} from 'services/alerts.service'; +import {QuestionsListService} from 'services/questions-list.service'; +import {LoaderService} from 'services/loader.service'; export interface AssignedSkillTopicData { [topicName: string]: string; @@ -47,7 +47,7 @@ export interface GroupedSkillSummaries { others: SkillSummaryBackendDict[]; } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class SkillEditorStateService { constructor( @@ -56,7 +56,7 @@ export class SkillEditorStateService { private skillBackendApiService: SkillBackendApiService, private skillRightsBackendApiService: SkillRightsBackendApiService, private loaderService: LoaderService, - private undoRedoService: UndoRedoService, + private undoRedoService: UndoRedoService ) {} // These properties are initialized using Angular lifecycle hooks @@ -70,7 +70,7 @@ export class SkillEditorStateService { private _skillIsBeingSaved: boolean = false; private _groupedSkillSummaries: GroupedSkillSummaries = { current: [], - others: [] + others: [], }; private _skillChangedEventEmitter = new EventEmitter(); @@ -94,7 +94,7 @@ export class SkillEditorStateService { }; private _updateGroupedSkillSummaries = ( - groupedSkillSummaries: GroupedSkillSummaryDictionaries + groupedSkillSummaries: GroupedSkillSummaryDictionaries ) => { let topicName = null; this._groupedSkillSummaries.current = []; @@ -115,7 +115,8 @@ export class SkillEditorStateService { if (topicName !== null) { for (let idx in groupedSkillSummaries[topicName]) { this._groupedSkillSummaries.current.push( - groupedSkillSummaries[topicName][idx]); + groupedSkillSummaries[topicName][idx] + ); } } for (let name in groupedSkillSummaries) { @@ -154,25 +155,30 @@ export class SkillEditorStateService { this._skillIsBeingLoaded = true; this.loaderService.showLoadingScreen('Loading Skill Editor'); let skillDataPromise = this.skillBackendApiService.fetchSkillAsync(skillId); - let skillRightsPromise = ( - this.skillRightsBackendApiService.fetchSkillRightsAsync(skillId)); + let skillRightsPromise = + this.skillRightsBackendApiService.fetchSkillRightsAsync(skillId); Promise.all([skillDataPromise, skillRightsPromise]).then( ([newBackendSkillObject, newSkillRightsObject]) => { this._updateSkillRights(newSkillRightsObject); - this._assignedSkillTopicData = ( - newBackendSkillObject.assignedSkillTopicData); + this._assignedSkillTopicData = + newBackendSkillObject.assignedSkillTopicData; this._updateSkill(newBackendSkillObject.skill); this._updateGroupedSkillSummaries( - newBackendSkillObject.groupedSkillSummaries); + newBackendSkillObject.groupedSkillSummaries + ); this.questionsListService.getQuestionSummariesAsync( - skillId, true, false + skillId, + true, + false ); this._skillIsBeingLoaded = false; this.loaderService.hideLoadingScreen(); - }, (error) => { + }, + error => { this.alertsService.addWarning(error); this._skillIsBeingLoaded = false; - }); + } + ); } /** @@ -194,9 +200,9 @@ export class SkillEditorStateService { } /** - * Returns whether a skill has yet been loaded using either - * loadSkill(). - */ + * Returns whether a skill has yet been loaded using either + * loadSkill(). + */ hasLoadedSkill(): boolean { return this._skillIsInitialized; } @@ -222,11 +228,13 @@ export class SkillEditorStateService { * shares behavior with setSkill(), when it succeeds. */ saveSkill( - commitMessage: string, - successCallback: (value?: Object) => void): boolean { + commitMessage: string, + successCallback: (value?: Object) => void + ): boolean { if (!this._skillIsInitialized) { this.alertsService.fatalWarning( - 'Cannot save a skill before one is loaded.'); + 'Cannot save a skill before one is loaded.' + ); } // Don't attempt to save the skill if there are no changes pending. if (!this.undoRedoService.hasChanges()) { @@ -234,21 +242,29 @@ export class SkillEditorStateService { } this._skillIsBeingSaved = true; - this.skillBackendApiService.updateSkillAsync( - this._skill.getId(), this._skill.getVersion(), commitMessage, - this.undoRedoService.getCommittableChangeList()).then( - (skill) => { - this._updateSkill(skill); - this.undoRedoService.clearChanges(); - this._skillIsBeingSaved = false; - if (successCallback) { - successCallback(); + this.skillBackendApiService + .updateSkillAsync( + this._skill.getId(), + this._skill.getVersion(), + commitMessage, + this.undoRedoService.getCommittableChangeList() + ) + .then( + skill => { + this._updateSkill(skill); + this.undoRedoService.clearChanges(); + this._skillIsBeingSaved = false; + if (successCallback) { + successCallback(); + } + }, + error => { + this.alertsService.addWarning( + error || 'There was an error when saving the skill' + ); + this._skillIsBeingSaved = false; } - }, (error) => { - this.alertsService.addWarning( - error || 'There was an error when saving the skill'); - this._skillIsBeingSaved = false; - }); + ); return true; } @@ -266,18 +282,23 @@ export class SkillEditorStateService { * for that variable. */ updateExistenceOfSkillDescription( - description: string, - successCallback: (skillDescriptionExists: boolean) => void): void { - this.skillBackendApiService.doesSkillWithDescriptionExistAsync( - description).then( - (skillDescriptionExists) => { - successCallback(skillDescriptionExists); - }, (error) => { - this.alertsService.addWarning( - error || - 'There was an error when checking if the skill description ' + - 'exists for another skill.'); - }); + description: string, + successCallback: (skillDescriptionExists: boolean) => void + ): void { + this.skillBackendApiService + .doesSkillWithDescriptionExistAsync(description) + .then( + skillDescriptionExists => { + successCallback(skillDescriptionExists); + }, + error => { + this.alertsService.addWarning( + error || + 'There was an error when checking if the skill description ' + + 'exists for another skill.' + ); + } + ); } get onSkillChange(): EventEmitter { @@ -297,5 +318,9 @@ export class SkillEditorStateService { } } -angular.module('oppia').factory('SkillEditorStateService', - downgradeInjectable(SkillEditorStateService)); +angular + .module('oppia') + .factory( + 'SkillEditorStateService', + downgradeInjectable(SkillEditorStateService) + ); diff --git a/core/templates/pages/skill-editor-page/skill-editor-page.component.spec.ts b/core/templates/pages/skill-editor-page/skill-editor-page.component.spec.ts index 08de3fccffc6..620eb89dbe47 100644 --- a/core/templates/pages/skill-editor-page/skill-editor-page.component.spec.ts +++ b/core/templates/pages/skill-editor-page/skill-editor-page.component.spec.ts @@ -16,28 +16,37 @@ * @fileoverview Unit tests for skill editor page component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/compiler'; -import { EventEmitter } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { NgbModal, NgbModalRef, NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { AppConstants } from 'app.constants'; -import { UndoRedoService } from 'domain/editor/undo_redo/undo-redo.service'; -import { EntityEditorBrowserTabsInfoDomainConstants } from 'domain/entity_editor_browser_tabs_info/entity-editor-browser-tabs-info-domain.constants'; -import { EntityEditorBrowserTabsInfo } from 'domain/entity_editor_browser_tabs_info/entity-editor-browser-tabs-info.model'; -import { RecordedVoiceovers } from 'domain/exploration/recorded-voiceovers.model'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { ConceptCard } from 'domain/skill/concept-card.model'; -import { SkillUpdateService } from 'domain/skill/skill-update.service'; -import { Skill, SkillBackendDict, SkillObjectFactory } from 'domain/skill/SkillObjectFactory'; -import { UrlService } from 'services/contextual/url.service'; -import { LocalStorageService } from 'services/local-storage.service'; -import { PreventPageUnloadEventService } from 'services/prevent-page-unload-event.service'; -import { SkillEditorRoutingService } from './services/skill-editor-routing.service'; -import { SkillEditorStalenessDetectionService } from './services/skill-editor-staleness-detection.service'; -import { SkillEditorStateService } from './services/skill-editor-state.service'; -import { SkillEditorPageComponent } from './skill-editor-page.component'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/compiler'; +import {EventEmitter} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {NgbModal, NgbModalRef, NgbModule} from '@ng-bootstrap/ng-bootstrap'; +import {AppConstants} from 'app.constants'; +import {UndoRedoService} from 'domain/editor/undo_redo/undo-redo.service'; +import {EntityEditorBrowserTabsInfoDomainConstants} from 'domain/entity_editor_browser_tabs_info/entity-editor-browser-tabs-info-domain.constants'; +import {EntityEditorBrowserTabsInfo} from 'domain/entity_editor_browser_tabs_info/entity-editor-browser-tabs-info.model'; +import {RecordedVoiceovers} from 'domain/exploration/recorded-voiceovers.model'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {ConceptCard} from 'domain/skill/concept-card.model'; +import {SkillUpdateService} from 'domain/skill/skill-update.service'; +import { + Skill, + SkillBackendDict, + SkillObjectFactory, +} from 'domain/skill/SkillObjectFactory'; +import {UrlService} from 'services/contextual/url.service'; +import {LocalStorageService} from 'services/local-storage.service'; +import {PreventPageUnloadEventService} from 'services/prevent-page-unload-event.service'; +import {SkillEditorRoutingService} from './services/skill-editor-routing.service'; +import {SkillEditorStalenessDetectionService} from './services/skill-editor-staleness-detection.service'; +import {SkillEditorStateService} from './services/skill-editor-state.service'; +import {SkillEditorPageComponent} from './skill-editor-page.component'; +import {WindowRef} from 'services/contextual/window-ref.service'; class MockNgbModalRef { componentInstance!: { @@ -50,17 +59,17 @@ class MockWindowRef { location: { pathname: '/path/name', reload: () => {}, - hash: '123' - }, - onresize: () => { + hash: '123', }, + onresize: () => {}, dispatchEvent: (ev: Event) => true, addEventListener( - event: string, - callback: (arg0: { returnValue: null }) => void) { + event: string, + callback: (arg0: {returnValue: null}) => void + ) { callback({returnValue: null}); }, - scrollTo: () => {} + scrollTo: () => {}, }; } @@ -70,8 +79,7 @@ describe('Skill editor page', () => { let localStorageService: LocalStorageService; let preventPageUnloadEventService: PreventPageUnloadEventService; let skillEditorRoutingService: SkillEditorRoutingService; - let skillEditorStalenessDetectionService: - SkillEditorStalenessDetectionService; + let skillEditorStalenessDetectionService: SkillEditorStalenessDetectionService; let skillEditorStateService: SkillEditorStateService; let undoRedoService: UndoRedoService; let ngbModal: NgbModal; @@ -93,10 +101,10 @@ describe('Skill editor page', () => { SkillEditorStalenessDetectionService, { provide: WindowRef, - useClass: MockWindowRef + useClass: MockWindowRef, }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); @@ -105,11 +113,13 @@ describe('Skill editor page', () => { component = fixture.componentInstance; localStorageService = TestBed.inject(LocalStorageService); preventPageUnloadEventService = TestBed.inject( - PreventPageUnloadEventService); + PreventPageUnloadEventService + ); skillEditorRoutingService = TestBed.inject(SkillEditorRoutingService); ngbModal = TestBed.inject(NgbModal); - skillEditorStalenessDetectionService = ( - TestBed.inject(SkillEditorStalenessDetectionService)); + skillEditorStalenessDetectionService = TestBed.inject( + SkillEditorStalenessDetectionService + ); skillEditorStateService = TestBed.inject(SkillEditorStateService); undoRedoService = TestBed.inject(UndoRedoService); urlService = TestBed.inject(UrlService); @@ -137,23 +147,29 @@ describe('Skill editor page', () => { let skillDict: SkillBackendDict = { id: 'skill_1', description: 'Description', - misconceptions: [{ - id: 2, - name: 'test name', - notes: 'test notes', - feedback: 'test feedback', - must_be_addressed: true, - }], - rubrics: [{ - difficulty: 'Easy', - explanations: ['explanation'], - }, { - difficulty: 'Medium', - explanations: ['explanation'], - }, { - difficulty: 'Hard', - explanations: ['explanation'], - }], + misconceptions: [ + { + id: 2, + name: 'test name', + notes: 'test notes', + feedback: 'test feedback', + must_be_addressed: true, + }, + ], + rubrics: [ + { + difficulty: 'Easy', + explanations: ['explanation'], + }, + { + difficulty: 'Medium', + explanations: ['explanation'], + }, + { + difficulty: 'Hard', + explanations: ['explanation'], + }, + ], skill_contents: skillContentsDict, language_code: 'en', version: 3, @@ -165,329 +181,441 @@ describe('Skill editor page', () => { skill = skillObjectFactory.createFromBackendDict(skillDict); spyOn(skillEditorStateService, 'getSkill').and.returnValue(skill); localStorageService.removeOpenedEntityEditorBrowserTabsInfo( - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_SKILL_EDITOR_BROWSER_TABS); + EntityEditorBrowserTabsInfoDomainConstants.OPENED_SKILL_EDITOR_BROWSER_TABS + ); }); - it('should load skill based on its id in url when component is initialized', - fakeAsync(() => { - let BrowserTabsInfo = EntityEditorBrowserTabsInfo.create( - 'skill', 'skill_id', 2, 1, false); - spyOn( - localStorageService, 'getEntityEditorBrowserTabsInfo' - ).and.returnValue(BrowserTabsInfo); - spyOn(skillEditorStateService, 'loadSkill').and.stub(); - spyOn(urlService, 'getSkillIdFromUrl').and.returnValue('skill_1'); - - tick(); - - component.ngOnInit(); - expect(skillEditorStateService.loadSkill).toHaveBeenCalledWith('skill_1'); - })); - - it('should addListener by passing getChangeCount to ' + - 'PreventPageUnloadEventService', () => { + it('should load skill based on its id in url when component is initialized', fakeAsync(() => { let BrowserTabsInfo = EntityEditorBrowserTabsInfo.create( - 'skill', 'skill_id', 2, 1, false); + 'skill', + 'skill_id', + 2, + 1, + false + ); spyOn( - localStorageService, 'getEntityEditorBrowserTabsInfo' + localStorageService, + 'getEntityEditorBrowserTabsInfo' ).and.returnValue(BrowserTabsInfo); spyOn(skillEditorStateService, 'loadSkill').and.stub(); spyOn(urlService, 'getSkillIdFromUrl').and.returnValue('skill_1'); - spyOn(undoRedoService, 'getChangeCount').and.returnValue(10); - spyOn(preventPageUnloadEventService, 'addListener').and - .callFake((callback: () => false) => callback()); - component.ngOnInit(); + tick(); - expect(preventPageUnloadEventService.addListener) - .toHaveBeenCalledWith(jasmine.any(Function)); - }); + component.ngOnInit(); + expect(skillEditorStateService.loadSkill).toHaveBeenCalledWith('skill_1'); + })); - it('should get active tab name from skill editor routing service', + it( + 'should addListener by passing getChangeCount to ' + + 'PreventPageUnloadEventService', () => { let BrowserTabsInfo = EntityEditorBrowserTabsInfo.create( - 'skill', 'skill_id', 2, 1, false); + 'skill', + 'skill_id', + 2, + 1, + false + ); spyOn( - localStorageService, 'getEntityEditorBrowserTabsInfo' + localStorageService, + 'getEntityEditorBrowserTabsInfo' ).and.returnValue(BrowserTabsInfo); - spyOn(skillEditorRoutingService, 'getActiveTabName').and.returnValue( - 'questions'); - expect(component.getActiveTabName()).toBe('questions'); - }); + spyOn(skillEditorStateService, 'loadSkill').and.stub(); + spyOn(urlService, 'getSkillIdFromUrl').and.returnValue('skill_1'); + spyOn(undoRedoService, 'getChangeCount').and.returnValue(10); + spyOn(preventPageUnloadEventService, 'addListener').and.callFake( + (callback: () => false) => callback() + ); - it('should go to main tab when selecting main tab', () => { + component.ngOnInit(); + + expect(preventPageUnloadEventService.addListener).toHaveBeenCalledWith( + jasmine.any(Function) + ); + } + ); + + it('should get active tab name from skill editor routing service', () => { let BrowserTabsInfo = EntityEditorBrowserTabsInfo.create( - 'skill', 'skill_id', 2, 1, false); + 'skill', + 'skill_id', + 2, + 1, + false + ); spyOn( - localStorageService, 'getEntityEditorBrowserTabsInfo' + localStorageService, + 'getEntityEditorBrowserTabsInfo' ).and.returnValue(BrowserTabsInfo); - let routingSpy = spyOn( - skillEditorRoutingService, 'navigateToMainTab'); - component.selectMainTab(); - expect(routingSpy).toHaveBeenCalled(); + spyOn(skillEditorRoutingService, 'getActiveTabName').and.returnValue( + 'questions' + ); + expect(component.getActiveTabName()).toBe('questions'); }); - it('should go to preview tab when selecting preview tab', () => { + it('should go to main tab when selecting main tab', () => { let BrowserTabsInfo = EntityEditorBrowserTabsInfo.create( - 'skill', 'skill_id', 2, 1, false); + 'skill', + 'skill_id', + 2, + 1, + false + ); spyOn( - localStorageService, 'getEntityEditorBrowserTabsInfo' + localStorageService, + 'getEntityEditorBrowserTabsInfo' ).and.returnValue(BrowserTabsInfo); - let routingSpy = spyOn( - skillEditorRoutingService, 'navigateToPreviewTab'); - component.selectPreviewTab(); + let routingSpy = spyOn(skillEditorRoutingService, 'navigateToMainTab'); + component.selectMainTab(); expect(routingSpy).toHaveBeenCalled(); }); - it('should open save changes modal with ngbModal when unsaved changes are' + - ' present', () => { + it('should go to preview tab when selecting preview tab', () => { let BrowserTabsInfo = EntityEditorBrowserTabsInfo.create( - 'skill', 'skill_id', 2, 1, false); + 'skill', + 'skill_id', + 2, + 1, + false + ); spyOn( - localStorageService, 'getEntityEditorBrowserTabsInfo' + localStorageService, + 'getEntityEditorBrowserTabsInfo' ).and.returnValue(BrowserTabsInfo); - spyOn(undoRedoService, 'getChangeCount').and.returnValue(1); - const modalSpy = spyOn(ngbModal, 'open').and.callFake( - () => { - return ({ - componentInstance: MockNgbModalRef, - result: Promise.resolve() - }) as NgbModalRef; - }); - - component.selectQuestionsTab(); - expect(modalSpy).toHaveBeenCalled(); + let routingSpy = spyOn(skillEditorRoutingService, 'navigateToPreviewTab'); + component.selectPreviewTab(); + expect(routingSpy).toHaveBeenCalled(); }); - it('should close save changes modal when somewhere outside is clicked', + it( + 'should open save changes modal with ngbModal when unsaved changes are' + + ' present', () => { let BrowserTabsInfo = EntityEditorBrowserTabsInfo.create( - 'skill', 'skill_id', 2, 1, false); + 'skill', + 'skill_id', + 2, + 1, + false + ); spyOn( - localStorageService, 'getEntityEditorBrowserTabsInfo' + localStorageService, + 'getEntityEditorBrowserTabsInfo' ).and.returnValue(BrowserTabsInfo); spyOn(undoRedoService, 'getChangeCount').and.returnValue(1); - const modalSpy = spyOn(ngbModal, 'open').and.callFake( - () => { - return ({ - componentInstance: MockNgbModalRef, - result: Promise.reject() - }) as NgbModalRef; - }); + const modalSpy = spyOn(ngbModal, 'open').and.callFake(() => { + return { + componentInstance: MockNgbModalRef, + result: Promise.resolve(), + } as NgbModalRef; + }); component.selectQuestionsTab(); expect(modalSpy).toHaveBeenCalled(); - }); + } + ); - it('should navigate to questions tab when unsaved changes are not present', - () => { - let BrowserTabsInfo = EntityEditorBrowserTabsInfo.create( - 'skill', 'skill_id', 2, 1, false); - spyOn( - localStorageService, 'getEntityEditorBrowserTabsInfo' - ).and.returnValue(BrowserTabsInfo); - spyOn(undoRedoService, 'getChangeCount').and.returnValue(0); - let routingSpy = spyOn( - skillEditorRoutingService, 'navigateToQuestionsTab').and.callThrough(); - component.selectQuestionsTab(); - expect(routingSpy).toHaveBeenCalled(); + it('should close save changes modal when somewhere outside is clicked', () => { + let BrowserTabsInfo = EntityEditorBrowserTabsInfo.create( + 'skill', + 'skill_id', + 2, + 1, + false + ); + spyOn( + localStorageService, + 'getEntityEditorBrowserTabsInfo' + ).and.returnValue(BrowserTabsInfo); + spyOn(undoRedoService, 'getChangeCount').and.returnValue(1); + const modalSpy = spyOn(ngbModal, 'open').and.callFake(() => { + return { + componentInstance: MockNgbModalRef, + result: Promise.reject(), + } as NgbModalRef; }); + component.selectQuestionsTab(); + expect(modalSpy).toHaveBeenCalled(); + }); + + it('should navigate to questions tab when unsaved changes are not present', () => { + let BrowserTabsInfo = EntityEditorBrowserTabsInfo.create( + 'skill', + 'skill_id', + 2, + 1, + false + ); + spyOn( + localStorageService, + 'getEntityEditorBrowserTabsInfo' + ).and.returnValue(BrowserTabsInfo); + spyOn(undoRedoService, 'getChangeCount').and.returnValue(0); + let routingSpy = spyOn( + skillEditorRoutingService, + 'navigateToQuestionsTab' + ).and.callThrough(); + component.selectQuestionsTab(); + expect(routingSpy).toHaveBeenCalled(); + }); + it('should return warnings count for the skill', () => { let BrowserTabsInfo = EntityEditorBrowserTabsInfo.create( - 'skill', 'skill_id', 2, 1, false); + 'skill', + 'skill_id', + 2, + 1, + false + ); spyOn( - localStorageService, 'getEntityEditorBrowserTabsInfo' + localStorageService, + 'getEntityEditorBrowserTabsInfo' ).and.returnValue(BrowserTabsInfo); const conceptCard = new ConceptCard( SubtitledHtml.createDefault( - 'review material', AppConstants.COMPONENT_NAME_EXPLANATION), - [], RecordedVoiceovers.createFromBackendDict({ + 'review material', + AppConstants.COMPONENT_NAME_EXPLANATION + ), + [], + RecordedVoiceovers.createFromBackendDict({ voiceovers_mapping: { - COMPONENT_NAME_EXPLANATION: {} - } + COMPONENT_NAME_EXPLANATION: {}, + }, }) ); component.skill = new Skill( - 'id1', 'description', [], [], conceptCard, 'en', 1, 0, 'id1', false, [] + 'id1', + 'description', + [], + [], + conceptCard, + 'en', + 1, + 0, + 'id1', + false, + [] ); expect(component.getWarningsCount()).toEqual(1); }); - it('should create or update skill editor browser tabs info on ' + - 'local storage when a new tab opens', () => { - let BrowserTabsInfo = EntityEditorBrowserTabsInfo.create( - 'skill', 'skill_id', 2, 1, false); - spyOn( - localStorageService, 'getEntityEditorBrowserTabsInfo' - ).and.returnValue(BrowserTabsInfo); - spyOn(skillEditorStateService, 'loadSkill').and.stub(); - spyOn(urlService, 'getSkillIdFromUrl').and.returnValue('skill_1'); - component.ngOnInit(); - - let skillEditorBrowserTabsInfo: EntityEditorBrowserTabsInfo = ( - localStorageService.getEntityEditorBrowserTabsInfo( - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_SKILL_EDITOR_BROWSER_TABS, skill.getId() - ) as EntityEditorBrowserTabsInfo); - - expect(skillEditorBrowserTabsInfo).toBe(skillEditorBrowserTabsInfo); + it( + 'should create or update skill editor browser tabs info on ' + + 'local storage when a new tab opens', + () => { + let BrowserTabsInfo = EntityEditorBrowserTabsInfo.create( + 'skill', + 'skill_id', + 2, + 1, + false + ); + spyOn( + localStorageService, + 'getEntityEditorBrowserTabsInfo' + ).and.returnValue(BrowserTabsInfo); + spyOn(skillEditorStateService, 'loadSkill').and.stub(); + spyOn(urlService, 'getSkillIdFromUrl').and.returnValue('skill_1'); + component.ngOnInit(); - // Opening the first tab. - skillEditorStateService.onSkillChange.emit(); - skillEditorBrowserTabsInfo = ( - localStorageService.getEntityEditorBrowserTabsInfo( - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_SKILL_EDITOR_BROWSER_TABS, skill.getId() - ) as EntityEditorBrowserTabsInfo); + let skillEditorBrowserTabsInfo: EntityEditorBrowserTabsInfo = + localStorageService.getEntityEditorBrowserTabsInfo( + EntityEditorBrowserTabsInfoDomainConstants.OPENED_SKILL_EDITOR_BROWSER_TABS, + skill.getId() + ) as EntityEditorBrowserTabsInfo; - expect(skillEditorBrowserTabsInfo).toBeDefined(); - expect(skillEditorBrowserTabsInfo.getNumberOfOpenedTabs()).toEqual(1); + expect(skillEditorBrowserTabsInfo).toBe(skillEditorBrowserTabsInfo); - // Opening the second tab. - component.ngOnInit(); - skillEditorStateService.onSkillChange.emit(); - skillEditorBrowserTabsInfo = ( - localStorageService.getEntityEditorBrowserTabsInfo( - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_SKILL_EDITOR_BROWSER_TABS, skill.getId() - ) as EntityEditorBrowserTabsInfo); - - expect(skillEditorBrowserTabsInfo.getNumberOfOpenedTabs()).toEqual(1); - expect(skillEditorBrowserTabsInfo.getLatestVersion()).toEqual(3); - - // Save some changes on the skill which will increment its version. - spyOn(skill, 'getVersion').and.returnValue(4); - skillEditorStateService.onSkillChange.emit(); - skillEditorBrowserTabsInfo = ( - localStorageService.getEntityEditorBrowserTabsInfo( - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_SKILL_EDITOR_BROWSER_TABS, skill.getId() - ) as EntityEditorBrowserTabsInfo); - - expect(skillEditorBrowserTabsInfo.getLatestVersion()).toEqual(4); - }); + // Opening the first tab. + skillEditorStateService.onSkillChange.emit(); + skillEditorBrowserTabsInfo = + localStorageService.getEntityEditorBrowserTabsInfo( + EntityEditorBrowserTabsInfoDomainConstants.OPENED_SKILL_EDITOR_BROWSER_TABS, + skill.getId() + ) as EntityEditorBrowserTabsInfo; - it('should create or update skill editor browser tabs info if browser' + - 'tabs info is null', () => { - spyOn(localStorageService, 'getEntityEditorBrowserTabsInfo') - .and.returnValue(null); + expect(skillEditorBrowserTabsInfo).toBeDefined(); + expect(skillEditorBrowserTabsInfo.getNumberOfOpenedTabs()).toEqual(1); - component.skillIsInitialized = false; - component.createOrUpdateSkillEditorBrowserTabsInfo(); + // Opening the second tab. + component.ngOnInit(); + skillEditorStateService.onSkillChange.emit(); + skillEditorBrowserTabsInfo = + localStorageService.getEntityEditorBrowserTabsInfo( + EntityEditorBrowserTabsInfoDomainConstants.OPENED_SKILL_EDITOR_BROWSER_TABS, + skill.getId() + ) as EntityEditorBrowserTabsInfo; + + expect(skillEditorBrowserTabsInfo.getNumberOfOpenedTabs()).toEqual(1); + expect(skillEditorBrowserTabsInfo.getLatestVersion()).toEqual(3); + + // Save some changes on the skill which will increment its version. + spyOn(skill, 'getVersion').and.returnValue(4); + skillEditorStateService.onSkillChange.emit(); + skillEditorBrowserTabsInfo = + localStorageService.getEntityEditorBrowserTabsInfo( + EntityEditorBrowserTabsInfoDomainConstants.OPENED_SKILL_EDITOR_BROWSER_TABS, + skill.getId() + ) as EntityEditorBrowserTabsInfo; + + expect(skillEditorBrowserTabsInfo.getLatestVersion()).toEqual(4); + } + ); + + it( + 'should create or update skill editor browser tabs info if browser' + + 'tabs info is null', + () => { + spyOn( + localStorageService, + 'getEntityEditorBrowserTabsInfo' + ).and.returnValue(null); - expect(component.skillIsInitialized).toBeTrue(); - }); + component.skillIsInitialized = false; + component.createOrUpdateSkillEditorBrowserTabsInfo(); - it('should decrement number of opened skill editor tabs when ' + - 'a tab is closed', () => { - let BrowserTabsInfo = EntityEditorBrowserTabsInfo.create( - 'skill', 'skill_id', 2, 1, false); - spyOn( - localStorageService, 'getEntityEditorBrowserTabsInfo' - ).and.returnValue(BrowserTabsInfo); - spyOn(preventPageUnloadEventService, 'addListener'); - spyOn(undoRedoService, 'getChangeCount').and.returnValue(1); - spyOn(skillEditorStateService, 'loadSkill').and.stub(); - spyOn(urlService, 'getSkillIdFromUrl').and.returnValue('skill_1'); + expect(component.skillIsInitialized).toBeTrue(); + } + ); - // Opening of the first tab. - component.ngOnInit(); - skillEditorStateService.onSkillChange.emit(); - // Opening of the second tab. - component.ngOnInit(); - skillEditorStateService.onSkillChange.emit(); - - let skillEditorBrowserTabsInfo: EntityEditorBrowserTabsInfo = ( - localStorageService.getEntityEditorBrowserTabsInfo( - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_SKILL_EDITOR_BROWSER_TABS, skill.getId() - ) as EntityEditorBrowserTabsInfo); - - // Making some unsaved changes on the editor page. - skillEditorBrowserTabsInfo.setSomeTabHasUnsavedChanges(true); - localStorageService.updateEntityEditorBrowserTabsInfo( - skillEditorBrowserTabsInfo, EntityEditorBrowserTabsInfoDomainConstants - .OPENED_SKILL_EDITOR_BROWSER_TABS); - - expect( - skillEditorBrowserTabsInfo.doesSomeTabHaveUnsavedChanges() - ).toBeTrue(); - expect(skillEditorBrowserTabsInfo.getNumberOfOpenedTabs()).toEqual(1); - - component.onClosingSkillEditorBrowserTab(); - skillEditorBrowserTabsInfo = ( - localStorageService.getEntityEditorBrowserTabsInfo( - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_SKILL_EDITOR_BROWSER_TABS, skill.getId() - ) as EntityEditorBrowserTabsInfo); - - expect(skillEditorBrowserTabsInfo.getNumberOfOpenedTabs()).toEqual(0); - - // Since the tab containing unsaved changes is closed, the value of - // unsaved changes status will become false. - expect( - skillEditorBrowserTabsInfo.doesSomeTabHaveUnsavedChanges() - ).toBeFalse(); - }); + it( + 'should decrement number of opened skill editor tabs when ' + + 'a tab is closed', + () => { + let BrowserTabsInfo = EntityEditorBrowserTabsInfo.create( + 'skill', + 'skill_id', + 2, + 1, + false + ); + spyOn( + localStorageService, + 'getEntityEditorBrowserTabsInfo' + ).and.returnValue(BrowserTabsInfo); + spyOn(preventPageUnloadEventService, 'addListener'); + spyOn(undoRedoService, 'getChangeCount').and.returnValue(1); + spyOn(skillEditorStateService, 'loadSkill').and.stub(); + spyOn(urlService, 'getSkillIdFromUrl').and.returnValue('skill_1'); - it('should emit the stale tab and presence of unsaved changes events ' + - 'when the \'storage\' event is triggered', () => { - let BrowserTabsInfo = EntityEditorBrowserTabsInfo.create( - 'skill', 'skill_id', 2, 1, false); - spyOn( - localStorageService, 'getEntityEditorBrowserTabsInfo' - ).and.returnValue(BrowserTabsInfo); - let staleTabEventEmitter = new EventEmitter(); - let presenceOfUnsavedChangesEventEmitter = new EventEmitter(); - let storageEvent = new StorageEvent('storage', { - key: EntityEditorBrowserTabsInfoDomainConstants - .OPENED_SKILL_EDITOR_BROWSER_TABS - }); + // Opening of the first tab. + component.ngOnInit(); + skillEditorStateService.onSkillChange.emit(); + // Opening of the second tab. + component.ngOnInit(); + skillEditorStateService.onSkillChange.emit(); + + let skillEditorBrowserTabsInfo: EntityEditorBrowserTabsInfo = + localStorageService.getEntityEditorBrowserTabsInfo( + EntityEditorBrowserTabsInfoDomainConstants.OPENED_SKILL_EDITOR_BROWSER_TABS, + skill.getId() + ) as EntityEditorBrowserTabsInfo; + + // Making some unsaved changes on the editor page. + skillEditorBrowserTabsInfo.setSomeTabHasUnsavedChanges(true); + localStorageService.updateEntityEditorBrowserTabsInfo( + skillEditorBrowserTabsInfo, + EntityEditorBrowserTabsInfoDomainConstants.OPENED_SKILL_EDITOR_BROWSER_TABS + ); + + expect( + skillEditorBrowserTabsInfo.doesSomeTabHaveUnsavedChanges() + ).toBeTrue(); + expect(skillEditorBrowserTabsInfo.getNumberOfOpenedTabs()).toEqual(1); + + component.onClosingSkillEditorBrowserTab(); + skillEditorBrowserTabsInfo = + localStorageService.getEntityEditorBrowserTabsInfo( + EntityEditorBrowserTabsInfoDomainConstants.OPENED_SKILL_EDITOR_BROWSER_TABS, + skill.getId() + ) as EntityEditorBrowserTabsInfo; + + expect(skillEditorBrowserTabsInfo.getNumberOfOpenedTabs()).toEqual(0); + + // Since the tab containing unsaved changes is closed, the value of + // unsaved changes status will become false. + expect( + skillEditorBrowserTabsInfo.doesSomeTabHaveUnsavedChanges() + ).toBeFalse(); + } + ); + + it( + 'should emit the stale tab and presence of unsaved changes events ' + + "when the 'storage' event is triggered", + () => { + let BrowserTabsInfo = EntityEditorBrowserTabsInfo.create( + 'skill', + 'skill_id', + 2, + 1, + false + ); + spyOn( + localStorageService, + 'getEntityEditorBrowserTabsInfo' + ).and.returnValue(BrowserTabsInfo); + let staleTabEventEmitter = new EventEmitter(); + let presenceOfUnsavedChangesEventEmitter = new EventEmitter(); + let storageEvent = new StorageEvent('storage', { + key: EntityEditorBrowserTabsInfoDomainConstants.OPENED_SKILL_EDITOR_BROWSER_TABS, + }); - spyOn( - skillEditorStalenessDetectionService, 'staleTabEventEmitter' - ).and.returnValue(staleTabEventEmitter); - spyOn( - skillEditorStalenessDetectionService, - 'presenceOfUnsavedChangesEventEmitter' - ).and.returnValue(presenceOfUnsavedChangesEventEmitter); - spyOn(skillEditorStateService, 'loadSkill').and.stub(); - spyOn(urlService, 'getSkillIdFromUrl').and.returnValue('skill_1'); + spyOn( + skillEditorStalenessDetectionService, + 'staleTabEventEmitter' + ).and.returnValue(staleTabEventEmitter); + spyOn( + skillEditorStalenessDetectionService, + 'presenceOfUnsavedChangesEventEmitter' + ).and.returnValue(presenceOfUnsavedChangesEventEmitter); + spyOn(skillEditorStateService, 'loadSkill').and.stub(); + spyOn(urlService, 'getSkillIdFromUrl').and.returnValue('skill_1'); - component.ngOnInit(); + component.ngOnInit(); - staleTabEventEmitter.emit(); - presenceOfUnsavedChangesEventEmitter.emit(); - windowRef.nativeWindow.dispatchEvent(storageEvent); + staleTabEventEmitter.emit(); + presenceOfUnsavedChangesEventEmitter.emit(); + windowRef.nativeWindow.dispatchEvent(storageEvent); - expect(component.skillIsInitialized).toBeFalse(); - }); + expect(component.skillIsInitialized).toBeFalse(); + } + ); - it('should emit events if the duplicate tab opened is stale or' + - 'there are some unsaved changes present', () => { - let BrowserTabsInfo = EntityEditorBrowserTabsInfo.create( - 'skill', 'skill_id', 2, 1, false); - spyOn( - localStorageService, 'getEntityEditorBrowserTabsInfo' - ).and.returnValue(BrowserTabsInfo); - spyOn(skillEditorStalenessDetectionService.staleTabEventEmitter, 'emit'); - spyOn( - skillEditorStalenessDetectionService - .presenceOfUnsavedChangesEventEmitter, 'emit'); - let storageEvent = new StorageEvent('storage', { - key: EntityEditorBrowserTabsInfoDomainConstants - .OPENED_SKILL_EDITOR_BROWSER_TABS - }); - component.onCreateOrUpdateSkillEditorBrowserTabsInfo(storageEvent); - - expect( - skillEditorStalenessDetectionService.staleTabEventEmitter.emit - ).toHaveBeenCalled(); - expect( - skillEditorStalenessDetectionService - .presenceOfUnsavedChangesEventEmitter.emit - ).toHaveBeenCalled(); - }); + it( + 'should emit events if the duplicate tab opened is stale or' + + 'there are some unsaved changes present', + () => { + let BrowserTabsInfo = EntityEditorBrowserTabsInfo.create( + 'skill', + 'skill_id', + 2, + 1, + false + ); + spyOn( + localStorageService, + 'getEntityEditorBrowserTabsInfo' + ).and.returnValue(BrowserTabsInfo); + spyOn(skillEditorStalenessDetectionService.staleTabEventEmitter, 'emit'); + spyOn( + skillEditorStalenessDetectionService.presenceOfUnsavedChangesEventEmitter, + 'emit' + ); + let storageEvent = new StorageEvent('storage', { + key: EntityEditorBrowserTabsInfoDomainConstants.OPENED_SKILL_EDITOR_BROWSER_TABS, + }); + component.onCreateOrUpdateSkillEditorBrowserTabsInfo(storageEvent); + + expect( + skillEditorStalenessDetectionService.staleTabEventEmitter.emit + ).toHaveBeenCalled(); + expect( + skillEditorStalenessDetectionService + .presenceOfUnsavedChangesEventEmitter.emit + ).toHaveBeenCalled(); + } + ); }); diff --git a/core/templates/pages/skill-editor-page/skill-editor-page.component.ts b/core/templates/pages/skill-editor-page/skill-editor-page.component.ts index 321f30322323..0027ced7a464 100644 --- a/core/templates/pages/skill-editor-page/skill-editor-page.component.ts +++ b/core/templates/pages/skill-editor-page/skill-editor-page.component.ts @@ -16,43 +16,40 @@ * @fileoverview Component for the skill editor page. */ -import { Component, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { SavePendingChangesModalComponent } from 'components/save-pending-changes/save-pending-changes-modal.component'; -import { UndoRedoService } from 'domain/editor/undo_redo/undo-redo.service'; -import { EntityEditorBrowserTabsInfoDomainConstants } from 'domain/entity_editor_browser_tabs_info/entity-editor-browser-tabs-info-domain.constants'; -import { EntityEditorBrowserTabsInfo } from 'domain/entity_editor_browser_tabs_info/entity-editor-browser-tabs-info.model'; -import { Skill } from 'domain/skill/SkillObjectFactory'; -import { Subscription } from 'rxjs'; -import { BottomNavbarStatusService } from 'services/bottom-navbar-status.service'; -import { UrlService } from 'services/contextual/url.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { LocalStorageService } from 'services/local-storage.service'; -import { PreventPageUnloadEventService } from 'services/prevent-page-unload-event.service'; -import { SkillEditorRoutingService } from './services/skill-editor-routing.service'; -import { SkillEditorStalenessDetectionService } from './services/skill-editor-staleness-detection.service'; -import { SkillEditorStateService } from './services/skill-editor-state.service'; +import {Component, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {SavePendingChangesModalComponent} from 'components/save-pending-changes/save-pending-changes-modal.component'; +import {UndoRedoService} from 'domain/editor/undo_redo/undo-redo.service'; +import {EntityEditorBrowserTabsInfoDomainConstants} from 'domain/entity_editor_browser_tabs_info/entity-editor-browser-tabs-info-domain.constants'; +import {EntityEditorBrowserTabsInfo} from 'domain/entity_editor_browser_tabs_info/entity-editor-browser-tabs-info.model'; +import {Skill} from 'domain/skill/SkillObjectFactory'; +import {Subscription} from 'rxjs'; +import {BottomNavbarStatusService} from 'services/bottom-navbar-status.service'; +import {UrlService} from 'services/contextual/url.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {LocalStorageService} from 'services/local-storage.service'; +import {PreventPageUnloadEventService} from 'services/prevent-page-unload-event.service'; +import {SkillEditorRoutingService} from './services/skill-editor-routing.service'; +import {SkillEditorStalenessDetectionService} from './services/skill-editor-staleness-detection.service'; +import {SkillEditorStateService} from './services/skill-editor-state.service'; @Component({ selector: 'oppia-skill-editor-page', - templateUrl: './skill-editor-page.component.html' + templateUrl: './skill-editor-page.component.html', }) export class SkillEditorPageComponent implements OnInit { constructor( - private bottomNavbarStatusService: - BottomNavbarStatusService, + private bottomNavbarStatusService: BottomNavbarStatusService, private localStorageService: LocalStorageService, private ngbModal: NgbModal, - private preventPageUnloadEventService: - PreventPageUnloadEventService, + private preventPageUnloadEventService: PreventPageUnloadEventService, private skillEditorRoutingService: SkillEditorRoutingService, private skillEditorStateService: SkillEditorStateService, - private skillEditorStalenessDetectionService: - SkillEditorStalenessDetectionService, + private skillEditorStalenessDetectionService: SkillEditorStalenessDetectionService, private undoRedoService: UndoRedoService, private urlService: UrlService, - private windowRef: WindowRef, + private windowRef: WindowRef ) {} skill: Skill; @@ -80,18 +77,21 @@ export class SkillEditorPageComponent implements OnInit { // some questions with these now non-existent misconceptions. if (this.undoRedoService.getChangeCount() > 0) { const modalRef = this.ngbModal.open(SavePendingChangesModalComponent, { - backdrop: true + backdrop: true, }); - modalRef.componentInstance.body = ( + modalRef.componentInstance.body = 'Please save all pending changes ' + - 'before viewing the questions list.'); - - modalRef.result.then(() => {}, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + 'before viewing the questions list.'; + + modalRef.result.then( + () => {}, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } else { this.skillEditorRoutingService.navigateToQuestionsTab(); } @@ -104,30 +104,34 @@ export class SkillEditorPageComponent implements OnInit { onClosingSkillEditorBrowserTab(): void { const skill = this.skillEditorStateService.getSkill(); - const skillEditorBrowserTabsInfo: EntityEditorBrowserTabsInfo = ( + const skillEditorBrowserTabsInfo: EntityEditorBrowserTabsInfo = this.localStorageService.getEntityEditorBrowserTabsInfo( - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_SKILL_EDITOR_BROWSER_TABS, skill.getId())); + EntityEditorBrowserTabsInfoDomainConstants.OPENED_SKILL_EDITOR_BROWSER_TABS, + skill.getId() + ); - if (skillEditorBrowserTabsInfo.doesSomeTabHaveUnsavedChanges() && - this.undoRedoService.getChangeCount() > 0) { + if ( + skillEditorBrowserTabsInfo.doesSomeTabHaveUnsavedChanges() && + this.undoRedoService.getChangeCount() > 0 + ) { skillEditorBrowserTabsInfo.setSomeTabHasUnsavedChanges(false); } skillEditorBrowserTabsInfo.decrementNumberOfOpenedTabs(); this.localStorageService.updateEntityEditorBrowserTabsInfo( skillEditorBrowserTabsInfo, - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_SKILL_EDITOR_BROWSER_TABS); + EntityEditorBrowserTabsInfoDomainConstants.OPENED_SKILL_EDITOR_BROWSER_TABS + ); } createOrUpdateSkillEditorBrowserTabsInfo(): void { const skill = this.skillEditorStateService.getSkill(); - let skillEditorBrowserTabsInfo: EntityEditorBrowserTabsInfo = ( + let skillEditorBrowserTabsInfo: EntityEditorBrowserTabsInfo = this.localStorageService.getEntityEditorBrowserTabsInfo( - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_SKILL_EDITOR_BROWSER_TABS, skill.getId())); + EntityEditorBrowserTabsInfoDomainConstants.OPENED_SKILL_EDITOR_BROWSER_TABS, + skill.getId() + ); if (this.skillIsInitialized) { skillEditorBrowserTabsInfo.setLatestVersion(skill.getVersion()); @@ -138,33 +142,37 @@ export class SkillEditorPageComponent implements OnInit { skillEditorBrowserTabsInfo.incrementNumberOfOpenedTabs(); } else { skillEditorBrowserTabsInfo = EntityEditorBrowserTabsInfo.create( - 'skill', skill.getId(), skill.getVersion(), 1, false); + 'skill', + skill.getId(), + skill.getVersion(), + 1, + false + ); } this.skillIsInitialized = true; } this.localStorageService.updateEntityEditorBrowserTabsInfo( skillEditorBrowserTabsInfo, - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_SKILL_EDITOR_BROWSER_TABS); + EntityEditorBrowserTabsInfoDomainConstants.OPENED_SKILL_EDITOR_BROWSER_TABS + ); } onCreateOrUpdateSkillEditorBrowserTabsInfo(event: StorageEvent): void { - if (event.key === ( - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_SKILL_EDITOR_BROWSER_TABS) + if ( + event.key === + EntityEditorBrowserTabsInfoDomainConstants.OPENED_SKILL_EDITOR_BROWSER_TABS ) { - this.skillEditorStalenessDetectionService - .staleTabEventEmitter.emit(); - this.skillEditorStalenessDetectionService - .presenceOfUnsavedChangesEventEmitter.emit(); + this.skillEditorStalenessDetectionService.staleTabEventEmitter.emit(); + this.skillEditorStalenessDetectionService.presenceOfUnsavedChangesEventEmitter.emit(); } } ngOnInit(): void { this.bottomNavbarStatusService.markBottomNavbarStatus(true); this.preventPageUnloadEventService.addListener( - this.undoRedoService.getChangeCount.bind(this.undoRedoService)); + this.undoRedoService.getChangeCount.bind(this.undoRedoService) + ); this.skillEditorStateService.loadSkill(this.urlService.getSkillIdFromUrl()); this.skill = this.skillEditorStateService.getSkill(); this.directiveSubscriptions.add( @@ -174,18 +182,18 @@ export class SkillEditorPageComponent implements OnInit { ); this.skillIsInitialized = false; this.skillEditorStalenessDetectionService.init(); - this.windowRef.nativeWindow.addEventListener( - 'beforeunload', (event) => { - this.onClosingSkillEditorBrowserTab(); - }); - this.windowRef.nativeWindow.addEventListener( - 'storage', (event) => { - this.onCreateOrUpdateSkillEditorBrowserTabsInfo(event); - }); + this.windowRef.nativeWindow.addEventListener('beforeunload', event => { + this.onClosingSkillEditorBrowserTab(); + }); + this.windowRef.nativeWindow.addEventListener('storage', event => { + this.onCreateOrUpdateSkillEditorBrowserTabsInfo(event); + }); } } -angular.module('oppia').directive('oppiaSkillEditorPage', +angular.module('oppia').directive( + 'oppiaSkillEditorPage', downgradeComponent({ - component: SkillEditorPageComponent - }) as angular.IDirectiveFactory); + component: SkillEditorPageComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/skill-editor-page/skill-editor-page.constants.ajs.ts b/core/templates/pages/skill-editor-page/skill-editor-page.constants.ajs.ts index 79c4b4af2349..cdb03a40d6ee 100644 --- a/core/templates/pages/skill-editor-page/skill-editor-page.constants.ajs.ts +++ b/core/templates/pages/skill-editor-page/skill-editor-page.constants.ajs.ts @@ -18,9 +18,11 @@ // TODO(#7092): Delete this file once migration is complete and these AngularJS // equivalents of the Angular constants are no longer needed. -import { SkillEditorPageConstants } from - 'pages/skill-editor-page/skill-editor-page.constants'; +import {SkillEditorPageConstants} from 'pages/skill-editor-page/skill-editor-page.constants'; -angular.module('oppia').constant( - 'SKILL_RIGHTS_URL_TEMPLATE', - SkillEditorPageConstants.SKILL_RIGHTS_URL_TEMPLATE); +angular + .module('oppia') + .constant( + 'SKILL_RIGHTS_URL_TEMPLATE', + SkillEditorPageConstants.SKILL_RIGHTS_URL_TEMPLATE + ); diff --git a/core/templates/pages/skill-editor-page/skill-editor-page.constants.ts b/core/templates/pages/skill-editor-page/skill-editor-page.constants.ts index 92caf7d29ead..e1296ad517cd 100644 --- a/core/templates/pages/skill-editor-page/skill-editor-page.constants.ts +++ b/core/templates/pages/skill-editor-page/skill-editor-page.constants.ts @@ -17,6 +17,5 @@ */ export const SkillEditorPageConstants = { - SKILL_RIGHTS_URL_TEMPLATE: - '/skill_editor_handler/rights/' + SKILL_RIGHTS_URL_TEMPLATE: '/skill_editor_handler/rights/', } as const; diff --git a/core/templates/pages/skill-editor-page/skill-editor-page.import.ts b/core/templates/pages/skill-editor-page/skill-editor-page.import.ts index 16be3d870f8a..bc58750357ba 100644 --- a/core/templates/pages/skill-editor-page/skill-editor-page.import.ts +++ b/core/templates/pages/skill-editor-page/skill-editor-page.import.ts @@ -24,9 +24,15 @@ import 'third-party-imports/ui-codemirror.import'; import 'third-party-imports/ui-tree.import'; angular.module('oppia', [ - require('angular-cookies'), 'ngAnimate', - 'ngMaterial', 'ngSanitize', 'ngTouch', 'pascalprecht.translate', - 'ui.bootstrap', 'ui.tree', uiValidate + require('angular-cookies'), + 'ngAnimate', + 'ngMaterial', + 'ngSanitize', + 'ngTouch', + 'pascalprecht.translate', + 'ui.bootstrap', + 'ui.tree', + uiValidate, ]); require('Polyfills.ts'); diff --git a/core/templates/pages/skill-editor-page/skill-editor-page.module.ts b/core/templates/pages/skill-editor-page/skill-editor-page.module.ts index ea8b15943017..cbf370c9158a 100644 --- a/core/templates/pages/skill-editor-page/skill-editor-page.module.ts +++ b/core/templates/pages/skill-editor-page/skill-editor-page.module.ts @@ -16,44 +16,45 @@ * @fileoverview Module for the skill editor page. */ -import { APP_INITIALIZER, NgModule, StaticProvider } from '@angular/core'; -import { BrowserModule, HAMMER_GESTURE_CONFIG } from '@angular/platform-browser'; -import { DragDropModule } from '@angular/cdk/drag-drop'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { HttpClientModule } from '@angular/common/http'; -import { HTTP_INTERCEPTORS } from '@angular/common/http'; -import { RouterModule } from '@angular/router'; -import { APP_BASE_HREF } from '@angular/common'; +import {APP_INITIALIZER, NgModule, StaticProvider} from '@angular/core'; +import {BrowserModule, HAMMER_GESTURE_CONFIG} from '@angular/platform-browser'; +import {DragDropModule} from '@angular/cdk/drag-drop'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {HttpClientModule} from '@angular/common/http'; +import {HTTP_INTERCEPTORS} from '@angular/common/http'; +import {RouterModule} from '@angular/router'; +import {APP_BASE_HREF} from '@angular/common'; -import { RequestInterceptor } from 'services/request-interceptor.service'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { OppiaAngularRootComponent } from - 'components/oppia-angular-root.component'; -import { InteractionExtensionsModule } from 'interactions/interactions.module'; -import { SkillEditorNavbarBreadcrumbComponent } from 'pages/skill-editor-page/navbar/skill-editor-navbar-breadcrumb.component'; -import { platformFeatureInitFactory, PlatformFeatureService } from - 'services/platform-feature.service'; -import { DeleteMisconceptionModalComponent } from './modal-templates/delete-misconception-modal.component'; -import { SkillDescriptionEditorComponent } from './editor-tab/skill-description-editor/skill-description-editor.component'; -import { MyHammerConfig, toastrConfig } from 'pages/oppia-root/app.module'; -import { SkillPrerequisiteSkillsEditorComponent } from './editor-tab/skill-prerequisite-skills-editor/skill-prerequisite-skills-editor.component'; -import { WorkedExampleEditorComponent } from './editor-tab/skill-concept-card-editor/worked-example-editor.component'; -import { MisconceptionEditorComponent } from './editor-tab/skill-misconceptions-editor/misconception-editor.component'; -import { DeleteWorkedExampleComponent } from './modal-templates/delete-worked-example-modal.component'; -import { AddWorkedExampleModalComponent } from './modal-templates/add-worked-example.component'; -import { SkillRubricsEditorComponent } from './editor-tab/skill-rubrics-editor/skill-rubrics-editor.component'; -import { AddMisconceptionModalComponent } from './modal-templates/add-misconception-modal.component'; -import { SkillEditorSaveModalComponent } from './modal-templates/skill-editor-save-modal.component'; -import { SkillMisconceptionsEditorComponent } from './editor-tab/skill-misconceptions-editor/skill-misconceptions-editor.component'; -import { SkillPreviewModalComponent } from './editor-tab/skill-preview-modal.component'; -import { SkillConceptCardEditorComponent } from './editor-tab/skill-concept-card-editor/skill-concept-card-editor.component'; -import { SmartRouterModule } from 'hybrid-router-module-provider'; -import { AppErrorHandlerProvider } from 'pages/oppia-root/app-error-handler'; -import { SkillEditorNavabarComponent } from './navbar/skill-editor-navbar.component'; -import { SkillQuestionsTabComponent } from './questions-tab/skill-questions-tab.component'; -import { SkillPreviewTabComponent } from './skill-preview-tab/skill-preview-tab.component'; -import { SkillEditorMainTabComponent } from './editor-tab/skill-editor-main-tab.component'; -import { SkillEditorPageComponent } from './skill-editor-page.component'; +import {RequestInterceptor} from 'services/request-interceptor.service'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {OppiaAngularRootComponent} from 'components/oppia-angular-root.component'; +import {InteractionExtensionsModule} from 'interactions/interactions.module'; +import {SkillEditorNavbarBreadcrumbComponent} from 'pages/skill-editor-page/navbar/skill-editor-navbar-breadcrumb.component'; +import { + platformFeatureInitFactory, + PlatformFeatureService, +} from 'services/platform-feature.service'; +import {DeleteMisconceptionModalComponent} from './modal-templates/delete-misconception-modal.component'; +import {SkillDescriptionEditorComponent} from './editor-tab/skill-description-editor/skill-description-editor.component'; +import {MyHammerConfig, toastrConfig} from 'pages/oppia-root/app.module'; +import {SkillPrerequisiteSkillsEditorComponent} from './editor-tab/skill-prerequisite-skills-editor/skill-prerequisite-skills-editor.component'; +import {WorkedExampleEditorComponent} from './editor-tab/skill-concept-card-editor/worked-example-editor.component'; +import {MisconceptionEditorComponent} from './editor-tab/skill-misconceptions-editor/misconception-editor.component'; +import {DeleteWorkedExampleComponent} from './modal-templates/delete-worked-example-modal.component'; +import {AddWorkedExampleModalComponent} from './modal-templates/add-worked-example.component'; +import {SkillRubricsEditorComponent} from './editor-tab/skill-rubrics-editor/skill-rubrics-editor.component'; +import {AddMisconceptionModalComponent} from './modal-templates/add-misconception-modal.component'; +import {SkillEditorSaveModalComponent} from './modal-templates/skill-editor-save-modal.component'; +import {SkillMisconceptionsEditorComponent} from './editor-tab/skill-misconceptions-editor/skill-misconceptions-editor.component'; +import {SkillPreviewModalComponent} from './editor-tab/skill-preview-modal.component'; +import {SkillConceptCardEditorComponent} from './editor-tab/skill-concept-card-editor/skill-concept-card-editor.component'; +import {SmartRouterModule} from 'hybrid-router-module-provider'; +import {AppErrorHandlerProvider} from 'pages/oppia-root/app-error-handler'; +import {SkillEditorNavabarComponent} from './navbar/skill-editor-navbar.component'; +import {SkillQuestionsTabComponent} from './questions-tab/skill-questions-tab.component'; +import {SkillPreviewTabComponent} from './skill-preview-tab/skill-preview-tab.component'; +import {SkillEditorMainTabComponent} from './editor-tab/skill-editor-main-tab.component'; +import {SkillEditorPageComponent} from './skill-editor-page.component'; @NgModule({ imports: [ @@ -67,7 +68,7 @@ import { SkillEditorPageComponent } from './skill-editor-page.component'; SmartRouterModule, RouterModule.forRoot([]), SharedComponentsModule, - ToastrModule.forRoot(toastrConfig) + ToastrModule.forRoot(toastrConfig), ], declarations: [ DeleteMisconceptionModalComponent, @@ -88,7 +89,7 @@ import { SkillEditorPageComponent } from './skill-editor-page.component'; SkillQuestionsTabComponent, SkillPreviewTabComponent, SkillEditorMainTabComponent, - SkillEditorPageComponent + SkillEditorPageComponent, ], entryComponents: [ DeleteMisconceptionModalComponent, @@ -109,42 +110,42 @@ import { SkillEditorPageComponent } from './skill-editor-page.component'; SkillQuestionsTabComponent, SkillPreviewTabComponent, SkillEditorMainTabComponent, - SkillEditorPageComponent + SkillEditorPageComponent, ], providers: [ { provide: HTTP_INTERCEPTORS, useClass: RequestInterceptor, - multi: true + multi: true, }, { provide: APP_INITIALIZER, useFactory: platformFeatureInitFactory, deps: [PlatformFeatureService], - multi: true + multi: true, }, { provide: HAMMER_GESTURE_CONFIG, - useClass: MyHammerConfig + useClass: MyHammerConfig, }, AppErrorHandlerProvider, { provide: APP_BASE_HREF, - useValue: '/' - } - ] + useValue: '/', + }, + ], }) class SkillEditorPageModule { // Empty placeholder method to satisfy the `Compiler`. ngDoBootstrap() {} } -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { downgradeModule } from '@angular/upgrade/static'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { ToastrModule } from 'ngx-toastr'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {downgradeModule} from '@angular/upgrade/static'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {ToastrModule} from 'ngx-toastr'; -const bootstrapFnAsync = async(extraProviders: StaticProvider[]) => { +const bootstrapFnAsync = async (extraProviders: StaticProvider[]) => { const platformRef = platformBrowserDynamic(extraProviders); return platformRef.bootstrapModule(SkillEditorPageModule); }; @@ -159,5 +160,6 @@ angular.module('oppia').directive( // bootstrap the Angular 8. 'oppiaAngularRoot', downgradeComponent({ - component: OppiaAngularRootComponent - }) as angular.IDirectiveFactory); + component: OppiaAngularRootComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/skill-editor-page/skill-preview-tab/skill-preview-tab.component.spec.ts b/core/templates/pages/skill-editor-page/skill-preview-tab/skill-preview-tab.component.spec.ts index ec442db04687..c8d52dc6a212 100644 --- a/core/templates/pages/skill-editor-page/skill-preview-tab/skill-preview-tab.component.spec.ts +++ b/core/templates/pages/skill-editor-page/skill-preview-tab/skill-preview-tab.component.spec.ts @@ -16,102 +16,110 @@ * @fileoverview Unit tests for skill preview tab component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; -import { MockTranslateService } from 'components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec'; -import { QuestionBackendApiService } from 'domain/question/question-backend-api.service'; -import { QuestionBackendDict } from 'domain/question/QuestionObjectFactory'; -import { InteractionRulesService } from 'pages/exploration-player-page/services/answer-classification.service'; -import { Interaction } from 'domain/exploration/InteractionObjectFactory'; -import { CurrentInteractionService } from 'pages/exploration-player-page/services/current-interaction.service'; -import { ExplorationPlayerStateService } from 'pages/exploration-player-page/services/exploration-player-state.service'; -import { ExplorationPlayerConstants } from 'pages/exploration-player-page/exploration-player-page.constants.ts'; -import { UrlService } from 'services/contextual/url.service'; -import { SkillEditorStateService } from '../services/skill-editor-state.service'; -import { SkillPreviewTabComponent } from './skill-preview-tab.component'; -import { QuestionPlayerEngineService } from 'pages/exploration-player-page/services/question-player-engine.service'; -import { StateCard } from 'domain/state_card/state-card.model'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { InteractionCustomizationArgs } from 'interactions/customization-args-defs'; -import { RecordedVoiceovers } from 'domain/exploration/recorded-voiceovers.model'; -import { AudioTranslationLanguageService } from - 'pages/exploration-player-page/services/audio-translation-language.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; +import {MockTranslateService} from 'components/forms/schema-based-editors/integration-tests/schema-based-editors.integration.spec'; +import {QuestionBackendApiService} from 'domain/question/question-backend-api.service'; +import {QuestionBackendDict} from 'domain/question/QuestionObjectFactory'; +import {InteractionRulesService} from 'pages/exploration-player-page/services/answer-classification.service'; +import {Interaction} from 'domain/exploration/InteractionObjectFactory'; +import {CurrentInteractionService} from 'pages/exploration-player-page/services/current-interaction.service'; +import {ExplorationPlayerStateService} from 'pages/exploration-player-page/services/exploration-player-state.service'; +import {ExplorationPlayerConstants} from 'pages/exploration-player-page/exploration-player-page.constants.ts'; +import {UrlService} from 'services/contextual/url.service'; +import {SkillEditorStateService} from '../services/skill-editor-state.service'; +import {SkillPreviewTabComponent} from './skill-preview-tab.component'; +import {QuestionPlayerEngineService} from 'pages/exploration-player-page/services/question-player-engine.service'; +import {StateCard} from 'domain/state_card/state-card.model'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {InteractionCustomizationArgs} from 'interactions/customization-args-defs'; +import {RecordedVoiceovers} from 'domain/exploration/recorded-voiceovers.model'; +import {AudioTranslationLanguageService} from 'pages/exploration-player-page/services/audio-translation-language.service'; const questionDict = { id: 'question_id', question_state_data: { content: { html: 'Question 1', - content_id: 'content_1' + content_id: 'content_1', }, interaction: { - answer_groups: [{ - outcome: { - dest: 'outcome 1', - dest_if_really_stuck: null, - feedback: { - content_id: 'content_5', - html: '' + answer_groups: [ + { + outcome: { + dest: 'outcome 1', + dest_if_really_stuck: null, + feedback: { + content_id: 'content_5', + html: '', + }, + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, }, - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null + rule_specs: [ + { + rule_type: 'Equals', + inputs: {x: 10}, + }, + ], }, - rule_specs: [{ - rule_type: 'Equals', - inputs: {x: 10} - }], - }], + ], confirmed_unclassified_answers: [], customization_args: { placeholder: { - value: 'abc' + value: 'abc', }, rows: { - value: 1 - } + value: 1, + }, }, default_outcome: { dest: null, dest_if_really_stuck: null, feedback: { html: 'Correct Answer', - content_id: 'content_2' + content_id: 'content_2', }, param_changes: [], - labelled_as_correct: false + labelled_as_correct: false, }, hints: [ { hint_content: { html: 'Hint 1', - content_id: 'content_3' - } - } + content_id: 'content_3', + }, + }, ], solution: { correct_answer: 'This is the correct answer', answer_is_exclusive: false, explanation: { html: 'Solution explanation', - content_id: 'content_4' - } + content_id: 'content_4', + }, }, - id: 'TextInput' + id: 'TextInput', }, param_changes: [], recorded_voiceovers: { voiceovers_mapping: { - content_1: {} - } + content_1: {}, + }, }, written_translations: { translations_mapping: { - content_1: {} - } + content_1: {}, + }, }, - solicit_answer_details: false + solicit_answer_details: false, }, language_code: 'en', }; @@ -134,13 +142,24 @@ describe('Skill Preview Tab Component', () => { let questionPlayerEngineService: QuestionPlayerEngineService; let windowDimensionsService: WindowDimensionsService; - let displayedCard = new StateCard( - '', '', '', new Interaction( - [], [], null as unknown as InteractionCustomizationArgs, null, - [], null, null), - [], null as unknown as RecordedVoiceovers, - '', null as unknown as AudioTranslationLanguageService); + '', + '', + '', + new Interaction( + [], + [], + null as unknown as InteractionCustomizationArgs, + null, + [], + null, + null + ), + [], + null as unknown as RecordedVoiceovers, + '', + null as unknown as AudioTranslationLanguageService + ); beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -154,52 +173,56 @@ describe('Skill Preview Tab Component', () => { QuestionPlayerEngineService, { provide: QuestionBackendApiService, - useClass: MockQuestionBackendApiService + useClass: MockQuestionBackendApiService, }, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); let questionDict1 = { question_state_data: { content: { - html: 'question1' - }, interaction: { - id: 'TextInput' - } - } + html: 'question1', + }, + interaction: { + id: 'TextInput', + }, + }, } as QuestionBackendDict; let questionDict2 = { question_state_data: { content: { - html: 'question2' - }, interaction: { - id: 'ItemSelectionInput' - } - } + html: 'question2', + }, + interaction: { + id: 'ItemSelectionInput', + }, + }, } as QuestionBackendDict; let questionDict3 = { question_state_data: { content: { - html: 'question3' - }, interaction: { - id: 'NumericInput' - } - } + html: 'question3', + }, + interaction: { + id: 'NumericInput', + }, + }, } as QuestionBackendDict; let questionDict4 = { question_state_data: { content: { - html: 'question4' - }, interaction: { - id: 'MultipleChoiceInput' - } - } + html: 'question4', + }, + interaction: { + id: 'MultipleChoiceInput', + }, + }, } as QuestionBackendDict; beforeEach(() => { @@ -209,15 +232,16 @@ describe('Skill Preview Tab Component', () => { skillEditorStateService = TestBed.inject(SkillEditorStateService); currentInteractionService = TestBed.inject(CurrentInteractionService); explorationPlayerStateService = TestBed.inject( - ExplorationPlayerStateService); + ExplorationPlayerStateService + ); questionPlayerEngineService = TestBed.inject(QuestionPlayerEngineService); windowDimensionsService = TestBed.inject(WindowDimensionsService); - questionPlayerEngineService = (questionPlayerEngineService as unknown) as - jasmine.SpyObj; + questionPlayerEngineService = + questionPlayerEngineService as unknown as jasmine.SpyObj; let skillId = 'df432fe'; - spyOn(questionPlayerEngineService, 'init').and.callFake(( - questionObject, successCallback, errorCallback - ) => {}); + spyOn(questionPlayerEngineService, 'init').and.callFake( + (questionObject, successCallback, errorCallback) => {} + ); spyOn(urlService, 'getSkillIdFromUrl').and.returnValue(skillId); component.ngOnInit(); }); @@ -227,13 +251,18 @@ describe('Skill Preview Tab Component', () => { expect(component.displayCardIsInitialized).toEqual(false); expect(component.questionsFetched).toEqual(false); expect(component.ALLOWED_QUESTION_INTERACTIONS).toEqual([ - 'All', 'Text Input', 'Multiple Choice', 'Numeric Input', - 'Item Selection']); + 'All', + 'Text Input', + 'Multiple Choice', + 'Numeric Input', + 'Item Selection', + ]); }); it('should trigger a digest loop when onSkillChange is emitted', () => { spyOnProperty(skillEditorStateService, 'onSkillChange').and.returnValue( - mockOnSkillChangeEmitter); + mockOnSkillChangeEmitter + ); spyOn(skillEditorStateService, 'loadSkill').and.stub(); component.ngOnInit(); @@ -251,18 +280,31 @@ describe('Skill Preview Tab Component', () => { expect(component.isCurrentSupplementalCardNonEmpty()).toBeFalse(); component.displayedCard = new StateCard( - '', '', '', new Interaction( - [], [], null as unknown as InteractionCustomizationArgs, null, - [], 'ImageClickInput', null), - [], null as unknown as RecordedVoiceovers, - '', null as unknown as AudioTranslationLanguageService); + '', + '', + '', + new Interaction( + [], + [], + null as unknown as InteractionCustomizationArgs, + null, + [], + 'ImageClickInput', + null + ), + [], + null as unknown as RecordedVoiceovers, + '', + null as unknown as AudioTranslationLanguageService + ); expect(component.isCurrentSupplementalCardNonEmpty()).toBeTrue(); }); it('should tell if window can show two cards', () => { spyOn(windowDimensionsService, 'getWidth').and.returnValue( - ExplorationPlayerConstants.TWO_CARD_THRESHOLD_PX + 1); + ExplorationPlayerConstants.TWO_CARD_THRESHOLD_PX + 1 + ); expect(component.canWindowShowTwoCards()).toBeTrue(); }); @@ -272,19 +314,34 @@ describe('Skill Preview Tab Component', () => { expect(component.displayedCard.isInteractionInline()).toBeTrue(); component.displayedCard = new StateCard( - '', '', '', new Interaction( - [], [], null as unknown as InteractionCustomizationArgs, null, - [], 'ImageClickInput', null), - [], null as unknown as RecordedVoiceovers, - '', null as unknown as AudioTranslationLanguageService); + '', + '', + '', + new Interaction( + [], + [], + null as unknown as InteractionCustomizationArgs, + null, + [], + 'ImageClickInput', + null + ), + [], + null as unknown as RecordedVoiceovers, + '', + null as unknown as AudioTranslationLanguageService + ); expect(component.displayedCard.isInteractionInline()).toBeFalse(); }); - it('should filter the questions', () => { - component.questionDicts = [questionDict1, questionDict2, - questionDict3, questionDict4]; + component.questionDicts = [ + questionDict1, + questionDict2, + questionDict3, + questionDict4, + ]; component.questionTextFilter = 'question1'; component.applyFilters(); diff --git a/core/templates/pages/skill-editor-page/skill-preview-tab/skill-preview-tab.component.ts b/core/templates/pages/skill-editor-page/skill-preview-tab/skill-preview-tab.component.ts index 1bdadbff77ad..6d815ec669b0 100644 --- a/core/templates/pages/skill-editor-page/skill-preview-tab/skill-preview-tab.component.ts +++ b/core/templates/pages/skill-editor-page/skill-preview-tab/skill-preview-tab.component.ts @@ -16,26 +16,28 @@ * @fileoverview Component for the skill preview tab. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { QuestionBackendApiService } from 'domain/question/question-backend-api.service'; -import { QuestionBackendDict, QuestionObjectFactory } from 'domain/question/QuestionObjectFactory'; -import { Skill } from 'domain/skill/SkillObjectFactory'; -import { StateCard } from 'domain/state_card/state-card.model'; -import { ExplorationPlayerConstants } from 'pages/exploration-player-page/exploration-player-page.constants'; -import { CurrentInteractionService } from 'pages/exploration-player-page/services/current-interaction.service'; -import { ExplorationPlayerStateService } from 'pages/exploration-player-page/services/exploration-player-state.service'; -import { QuestionPlayerEngineService } from 'pages/exploration-player-page/services/question-player-engine.service'; -import { Subscription } from 'rxjs'; -import { ContextService } from 'services/context.service'; -import { UrlService } from 'services/contextual/url.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { SkillEditorStateService } from '../services/skill-editor-state.service'; - +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {QuestionBackendApiService} from 'domain/question/question-backend-api.service'; +import { + QuestionBackendDict, + QuestionObjectFactory, +} from 'domain/question/QuestionObjectFactory'; +import {Skill} from 'domain/skill/SkillObjectFactory'; +import {StateCard} from 'domain/state_card/state-card.model'; +import {ExplorationPlayerConstants} from 'pages/exploration-player-page/exploration-player-page.constants'; +import {CurrentInteractionService} from 'pages/exploration-player-page/services/current-interaction.service'; +import {ExplorationPlayerStateService} from 'pages/exploration-player-page/services/exploration-player-state.service'; +import {QuestionPlayerEngineService} from 'pages/exploration-player-page/services/question-player-engine.service'; +import {Subscription} from 'rxjs'; +import {ContextService} from 'services/context.service'; +import {UrlService} from 'services/contextual/url.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {SkillEditorStateService} from '../services/skill-editor-state.service'; @Component({ selector: 'oppia-skill-preview-tab', - templateUrl: './skill-preview-tab.component.html' + templateUrl: './skill-preview-tab.component.html', }) export class SkillPreviewTabComponent implements OnInit, OnDestroy { constructor( @@ -70,7 +72,7 @@ export class SkillPreviewTabComponent implements OnInit, OnDestroy { TEXT_INPUT: 'Text Input', MULTIPLE_CHOICE: 'Multiple Choice', NUMERIC_INPUT: 'Numeric Input', - ITEM_SELECTION: 'Item Selection' + ITEM_SELECTION: 'Item Selection', }; directiveSubscriptions = new Subscription(); @@ -85,27 +87,28 @@ export class SkillPreviewTabComponent implements OnInit, OnDestroy { for (let interaction in this.INTERACTION_TYPES) { this.ALLOWED_QUESTION_INTERACTIONS.push( this.INTERACTION_TYPES[ - interaction as keyof typeof that.INTERACTION_TYPES]); + interaction as keyof typeof that.INTERACTION_TYPES + ] + ); } this.skill = this.skillEditorStateService.getSkill(); - this.htmlData = ( - this.skill ? - this.skill.getConceptCard().getExplanation().html : - 'loading review material' - ); + this.htmlData = this.skill + ? this.skill.getConceptCard().getExplanation().html + : 'loading review material'; - this.questionBackendApiService.fetchQuestionsAsync( - [this.skillId], this.QUESTION_COUNT, false).then((response) => { - this.questionsFetched = true; - this.questionDicts = response; - this.displayedQuestions = response; - if (this.questionDicts.length) { - this.selectQuestionToPreview(0); - } - }); + this.questionBackendApiService + .fetchQuestionsAsync([this.skillId], this.QUESTION_COUNT, false) + .then(response => { + this.questionsFetched = true; + this.questionDicts = response; + this.displayedQuestions = response; + if (this.questionDicts.length) { + this.selectQuestionToPreview(0); + } + }); this.directiveSubscriptions.add( - this.skillEditorStateService.onSkillChange.subscribe( - () => {})); + this.skillEditorStateService.onSkillChange.subscribe(() => {}) + ); this.currentInteractionService.setOnSubmitFn(() => { this.explorationPlayerStateService.onOppiaFeedbackAvailable.emit(); }); @@ -117,40 +120,46 @@ export class SkillPreviewTabComponent implements OnInit, OnDestroy { } applyFilters(): void { - this.displayedQuestions = this.questionDicts.filter( - questionDict => { - var contentData = questionDict.question_state_data.content.html; - var interactionType = ( - questionDict.question_state_data.interaction.id); - var htmlContentIsMatching = Boolean( - contentData.toLowerCase().includes( - this.questionTextFilter.toLowerCase())); - if (this.interactionFilter === this.INTERACTION_TYPES.ALL) { - return htmlContentIsMatching; - } else if ( - this.interactionFilter === this.INTERACTION_TYPES.TEXT_INPUT && - interactionType !== 'TextInput') { - return false; - } else if ( - this.interactionFilter === this.INTERACTION_TYPES.MULTIPLE_CHOICE && - interactionType !== 'MultipleChoiceInput') { - return false; - } else if ( - this.interactionFilter === this.INTERACTION_TYPES.ITEM_SELECTION && - interactionType !== 'ItemSelectionInput') { - return false; - } else if ( - this.interactionFilter === this.INTERACTION_TYPES.NUMERIC_INPUT && - interactionType !== 'NumericInput') { - return false; - } + this.displayedQuestions = this.questionDicts.filter(questionDict => { + var contentData = questionDict.question_state_data.content.html; + var interactionType = questionDict.question_state_data.interaction.id; + var htmlContentIsMatching = Boolean( + contentData + .toLowerCase() + .includes(this.questionTextFilter.toLowerCase()) + ); + if (this.interactionFilter === this.INTERACTION_TYPES.ALL) { return htmlContentIsMatching; - }); + } else if ( + this.interactionFilter === this.INTERACTION_TYPES.TEXT_INPUT && + interactionType !== 'TextInput' + ) { + return false; + } else if ( + this.interactionFilter === this.INTERACTION_TYPES.MULTIPLE_CHOICE && + interactionType !== 'MultipleChoiceInput' + ) { + return false; + } else if ( + this.interactionFilter === this.INTERACTION_TYPES.ITEM_SELECTION && + interactionType !== 'ItemSelectionInput' + ) { + return false; + } else if ( + this.interactionFilter === this.INTERACTION_TYPES.NUMERIC_INPUT && + interactionType !== 'NumericInput' + ) { + return false; + } + return htmlContentIsMatching; + }); } canWindowShowTwoCards(): boolean { - return this.windowDimensionsService.getWidth() > - ExplorationPlayerConstants.TWO_CARD_THRESHOLD_PX; + return ( + this.windowDimensionsService.getWidth() > + ExplorationPlayerConstants.TWO_CARD_THRESHOLD_PX + ); } isCurrentSupplementalCardNonEmpty(): boolean { @@ -164,9 +173,10 @@ export class SkillPreviewTabComponent implements OnInit, OnDestroy { [ this.questionObjectFactory.createFromBackendDict( this.displayedQuestions[index] - ) + ), ], - this.initializeQuestionCard.bind(this), () => {} + this.initializeQuestionCard.bind(this), + () => {} ); } @@ -175,7 +185,9 @@ export class SkillPreviewTabComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive('oppiaSkillPreviewTab', +angular.module('oppia').directive( + 'oppiaSkillPreviewTab', downgradeComponent({ - component: SkillPreviewTabComponent - }) as angular.IDirectiveFactory); + component: SkillPreviewTabComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/splash-page/splash-page-root.component.spec.ts b/core/templates/pages/splash-page/splash-page-root.component.spec.ts index 64cb2d634e1d..a7cc4ad8e361 100644 --- a/core/templates/pages/splash-page/splash-page-root.component.spec.ts +++ b/core/templates/pages/splash-page/splash-page-root.component.spec.ts @@ -16,15 +16,15 @@ * @fileoverview Unit tests for the splash page root component. */ -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { SplashPageRootComponent } from './splash-page-root.component'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {SplashPageRootComponent} from './splash-page-root.component'; class MockTranslateService { onLangChange: EventEmitter = new EventEmitter(); @@ -41,18 +41,15 @@ describe('Splash Page Root', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - SplashPageRootComponent, - MockTranslatePipe - ], + declarations: [SplashPageRootComponent, MockTranslatePipe], providers: [ PageHeadService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -63,10 +60,9 @@ describe('Splash Page Root', () => { translateService = TestBed.inject(TranslateService); }); - it('should successfully instantiate the component', - () => { - expect(component).toBeDefined(); - }); + it('should successfully instantiate the component', () => { + expect(component).toBeDefined(); + }); it('should initialize and subscribe to onLangChange', () => { spyOn(translateService.onLangChange, 'subscribe'); @@ -92,10 +88,12 @@ describe('Splash Page Root', () => { component.setPageTitleAndMetaTags(); expect(translateService.instant).toHaveBeenCalledWith( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.SPLASH.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.SPLASH.TITLE + ); expect(pageHeadService.updateTitleAndMetaTags).toHaveBeenCalledWith( AppConstants.PAGES_REGISTERED_WITH_FRONTEND.SPLASH.TITLE, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.SPLASH.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.SPLASH.META + ); }); it('should unsubscribe on component destruction', () => { diff --git a/core/templates/pages/splash-page/splash-page-root.component.ts b/core/templates/pages/splash-page/splash-page-root.component.ts index 0a504f18e708..c893a27aac10 100644 --- a/core/templates/pages/splash-page/splash-page-root.component.ts +++ b/core/templates/pages/splash-page/splash-page-root.component.ts @@ -16,16 +16,16 @@ * @fileoverview Root component for Splash Page. */ -import { Component, OnDestroy } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; +import {Component, OnDestroy} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; @Component({ selector: 'oppia-splash-page-root', - templateUrl: './splash-page-root.component.html' + templateUrl: './splash-page-root.component.html', }) export class SplashPageRootComponent implements OnDestroy { directiveSubscriptions = new Subscription(); @@ -36,10 +36,12 @@ export class SplashPageRootComponent implements OnDestroy { setPageTitleAndMetaTags(): void { let translatedTitle = this.translateService.instant( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.SPLASH.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.SPLASH.TITLE + ); this.pageHeadService.updateTitleAndMetaTags( translatedTitle, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.SPLASH.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.SPLASH.META + ); } ngOnInit(): void { diff --git a/core/templates/pages/splash-page/splash-page-routing.module.ts b/core/templates/pages/splash-page/splash-page-routing.module.ts index d03caa765a0f..c492b7982aac 100644 --- a/core/templates/pages/splash-page/splash-page-routing.module.ts +++ b/core/templates/pages/splash-page/splash-page-routing.module.ts @@ -16,24 +16,19 @@ * @fileoverview Routing module for splash page. */ -import { NgModule } from '@angular/core'; -import { Route, RouterModule } from '@angular/router'; -import { SplashPageRootComponent } from './splash-page-root.component'; +import {NgModule} from '@angular/core'; +import {Route, RouterModule} from '@angular/router'; +import {SplashPageRootComponent} from './splash-page-root.component'; const routes: Route[] = [ { path: '', - component: SplashPageRootComponent - } + component: SplashPageRootComponent, + }, ]; @NgModule({ - imports: [ - RouterModule.forChild(routes) - ], - exports: [ - RouterModule - ] + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], }) - export class SplashPageRoutingModule {} diff --git a/core/templates/pages/splash-page/splash-page.component.spec.ts b/core/templates/pages/splash-page/splash-page.component.spec.ts index c1ae130c03e6..ad01dd669d36 100644 --- a/core/templates/pages/splash-page/splash-page.component.spec.ts +++ b/core/templates/pages/splash-page/splash-page.component.spec.ts @@ -16,22 +16,20 @@ * @fileoverview Unit tests for the splash page. */ -import { EventEmitter } from '@angular/core'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { LoaderService } from 'services/loader.service'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; -import { WindowDimensionsService } from - 'services/contextual/window-dimensions.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { UserInfo } from 'domain/user/user-info.model'; -import { UserService } from 'services/user.service'; -import { SplashPageComponent } from './splash-page.component'; -import { of } from 'rxjs'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; +import {EventEmitter} from '@angular/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {LoaderService} from 'services/loader.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {UserInfo} from 'domain/user/user-info.model'; +import {UserService} from 'services/user.service'; +import {SplashPageComponent} from './splash-page.component'; +import {of} from 'rxjs'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; class MockWindowRef { _window = { @@ -43,13 +41,13 @@ class MockWindowRef { set href(val) { this._href = val; }, - replace: (val: string) => {} + replace: (val: string) => {}, }, sessionStorage: { last_uploaded_audio_lang: 'en', - removeItem: (name: string) => {} + removeItem: (name: string) => {}, }, - gtag: () => {} + gtag: () => {}, }; get nativeWindow() { @@ -80,34 +78,34 @@ describe('Splash Page', () => { let resizeEvent = new Event('resize'); let mockWindowRef = new MockWindowRef(); - beforeEach(async() => { + beforeEach(async () => { TestBed.configureTestingModule({ declarations: [SplashPageComponent, MockTranslatePipe], providers: [ { provide: I18nLanguageCodeService, - useClass: MockI18nLanguageCodeService + useClass: MockI18nLanguageCodeService, }, { provide: WindowDimensionsService, useValue: { isWindowNarrow: () => true, - getResizeEvent: () => of(resizeEvent) - } + getResizeEvent: () => of(resizeEvent), + }, }, SiteAnalyticsService, UrlInterpolationService, { provide: WindowRef, - useValue: mockWindowRef - } - ] + useValue: mockWindowRef, + }, + ], }).compileComponents(); }); beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] + imports: [HttpClientTestingModule], }); loaderService = TestBed.get(LoaderService); userService = TestBed.get(UserService); @@ -121,32 +119,37 @@ describe('Splash Page', () => { component = splashPageComponent.componentInstance; }); - it('should get static image url', function() { + it('should get static image url', function () { expect(component.getStaticImageUrl('/path/to/image')).toBe( - '/assets/images/path/to/image'); + '/assets/images/path/to/image' + ); }); - it('should record analytics when start learning is clicked', function() { + it('should record analytics when start learning is clicked', function () { spyOn( - siteAnalyticsService, 'registerClickHomePageStartLearningButtonEvent') - .and.callThrough(); + siteAnalyticsService, + 'registerClickHomePageStartLearningButtonEvent' + ).and.callThrough(); component.onClickStartLearningButton(); - expect(siteAnalyticsService.registerClickHomePageStartLearningButtonEvent) - .toHaveBeenCalled(); + expect( + siteAnalyticsService.registerClickHomePageStartLearningButtonEvent + ).toHaveBeenCalled(); }); - it('should record analytics when Browse Lessons is clicked', function() { + it('should record analytics when Browse Lessons is clicked', function () { spyOn( - siteAnalyticsService, 'registerClickBrowseLessonsButtonEvent') - .and.callThrough(); + siteAnalyticsService, + 'registerClickBrowseLessonsButtonEvent' + ).and.callThrough(); component.onClickBrowseLessonsButton(); - expect(siteAnalyticsService.registerClickBrowseLessonsButtonEvent) - .toHaveBeenCalled(); + expect( + siteAnalyticsService.registerClickBrowseLessonsButtonEvent + ).toHaveBeenCalled(); }); - it('should direct users to the android page on click', function() { + it('should direct users to the android page on click', function () { expect(mockWindowRef.nativeWindow.location.href).not.toEqual('/android'); component.onClickAccessAndroidButton(); @@ -154,25 +157,29 @@ describe('Splash Page', () => { expect(mockWindowRef.nativeWindow.location.href).toEqual('/android'); }); - it('should record analytics when Start Contributing is clicked', function() { + it('should record analytics when Start Contributing is clicked', function () { spyOn( - siteAnalyticsService, 'registerClickStartContributingButtonEvent') - .and.callThrough(); + siteAnalyticsService, + 'registerClickStartContributingButtonEvent' + ).and.callThrough(); component.onClickStartContributingButton(); - expect(siteAnalyticsService.registerClickStartContributingButtonEvent) - .toHaveBeenCalled(); + expect( + siteAnalyticsService.registerClickStartContributingButtonEvent + ).toHaveBeenCalled(); }); - it('should record analytics when Start Teaching is clicked', function() { + it('should record analytics when Start Teaching is clicked', function () { spyOn( - siteAnalyticsService, 'registerClickStartTeachingButtonEvent' + siteAnalyticsService, + 'registerClickStartTeachingButtonEvent' ).and.callThrough(); component.onClickStartTeachingButton(); - expect(siteAnalyticsService.registerClickStartTeachingButtonEvent) - .toHaveBeenCalled(); + expect( + siteAnalyticsService.registerClickStartTeachingButtonEvent + ).toHaveBeenCalled(); }); - it('should increment and decrement testimonial IDs correctly', function() { + it('should increment and decrement testimonial IDs correctly', function () { component.ngOnInit(); expect(component.displayedTestimonialId).toBe(0); component.incrementDisplayedTestimonialId(); @@ -188,7 +195,7 @@ describe('Splash Page', () => { expect(component.displayedTestimonialId).toBe(2); }); - it('should get testimonials correctly', function() { + it('should get testimonials correctly', function () { component.ngOnInit(); expect(component.getTestimonials().length).toBe(component.testimonialCount); }); @@ -204,10 +211,10 @@ describe('Splash Page', () => { preferred_site_language_code: null, username: 'tester', email: 'test@test.com', - user_is_logged_in: true + user_is_logged_in: true, }; - spyOn(userService, 'getUserInfoAsync').and.returnValue(Promise.resolve( - UserInfo.createFromBackendDict(UserInfoObject)) + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve(UserInfo.createFromBackendDict(UserInfoObject)) ); component.ngOnInit(); flushMicrotasks(); @@ -225,10 +232,10 @@ describe('Splash Page', () => { preferred_site_language_code: null, username: 'tester', email: 'test@test.com', - user_is_logged_in: false + user_is_logged_in: false, }; - spyOn(userService, 'getUserInfoAsync').and.returnValue(Promise.resolve( - UserInfo.createFromBackendDict(UserInfoObject)) + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve(UserInfo.createFromBackendDict(UserInfoObject)) ); component.ngOnInit(); flushMicrotasks(); @@ -238,8 +245,7 @@ describe('Splash Page', () => { it('should check if loader screen is working', fakeAsync(() => { spyOn(loaderService, 'showLoadingScreen').and.callThrough(); component.ngOnInit(); - expect(loaderService.showLoadingScreen) - .toHaveBeenCalledWith('Loading'); + expect(loaderService.showLoadingScreen).toHaveBeenCalledWith('Loading'); })); it('should set component properties when ngOnInit() is called', () => { diff --git a/core/templates/pages/splash-page/splash-page.component.ts b/core/templates/pages/splash-page/splash-page.component.ts index b93df107f1aa..d18f2cb3425a 100644 --- a/core/templates/pages/splash-page/splash-page.component.ts +++ b/core/templates/pages/splash-page/splash-page.component.ts @@ -15,16 +15,16 @@ /** * @fileoverview Component for the Oppia splash page. */ -import { Component, OnInit } from '@angular/core'; - -import { AppConstants } from 'app.constants'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { LoaderService } from 'services/loader.service'; -import { UserService } from 'services/user.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; +import {Component, OnInit} from '@angular/core'; + +import {AppConstants} from 'app.constants'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {LoaderService} from 'services/loader.service'; +import {UserService} from 'services/user.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; import './splash-page.component.css'; export interface Testimonial { @@ -38,7 +38,7 @@ export interface Testimonial { @Component({ selector: 'oppia-splash-page', templateUrl: './splash-page.component.html', - styleUrls: ['./splash-page.component.css'] + styleUrls: ['./splash-page.component.css'], }) export class SplashPageComponent implements OnInit { // These properties are initialized using Angular lifecycle hooks @@ -59,7 +59,7 @@ export class SplashPageComponent implements OnInit { private windowDimensionService: WindowDimensionsService, private windowRef: WindowRef, private userService: UserService, - private loaderService: LoaderService, + private loaderService: LoaderService ) {} getStaticImageUrl(imagePath: string): string { @@ -68,9 +68,12 @@ export class SplashPageComponent implements OnInit { getImageSet(imageName: string, imageExt: string): string { return ( - this.getStaticImageUrl(imageName + '1x.' + imageExt) + ' 1x, ' + - this.getStaticImageUrl(imageName + '15x.' + imageExt) + ' 1.5x, ' + - this.getStaticImageUrl(imageName + '2x.' + imageExt) + ' 2x' + this.getStaticImageUrl(imageName + '1x.' + imageExt) + + ' 1x, ' + + this.getStaticImageUrl(imageName + '15x.' + imageExt) + + ' 1.5x, ' + + this.getStaticImageUrl(imageName + '2x.' + imageExt) + + ' 2x' ); } @@ -99,7 +102,7 @@ export class SplashPageComponent implements OnInit { onClickStartTeachingButton(): void { this.siteAnalyticsService.registerClickStartTeachingButtonEvent(); - this.windowRef.nativeWindow.location.href = ('/creator-guidelines'); + this.windowRef.nativeWindow.location.href = '/creator-guidelines'; } // TODO(#11657): Extract the testimonials code into a separate component. @@ -109,46 +112,50 @@ export class SplashPageComponent implements OnInit { // This makes sure that incrementing from (testimonialCount - 1) // returns 0 instead of testimonialCount,since we want the testimonials // to cycle through. - this.displayedTestimonialId = ( - this.displayedTestimonialId + 1) % this.testimonialCount; + this.displayedTestimonialId = + (this.displayedTestimonialId + 1) % this.testimonialCount; } decrementDisplayedTestimonialId(): void { // This makes sure that decrementing from 0, returns // (testimonialCount - 1) instead of -1, since we want the testimonials // to cycle through. - this.displayedTestimonialId = ( - this.displayedTestimonialId + this.testimonialCount - 1) % + this.displayedTestimonialId = + (this.displayedTestimonialId + this.testimonialCount - 1) % this.testimonialCount; } getTestimonials(): [Testimonial, Testimonial, Testimonial, Testimonial] { - return [{ - quote: 'I18N_SPLASH_TESTIMONIAL_1', - studentDetails: 'I18N_SPLASH_STUDENT_DETAILS_1', - imageUrl: this.getImageSet('/splash/mira', 'png'), - imageUrlWebp: this.getImageSet('/splash/mira', 'webp'), - borderPresent: false - }, - { - quote: 'I18N_SPLASH_TESTIMONIAL_2', - studentDetails: 'I18N_SPLASH_STUDENT_DETAILS_2', - imageUrl: this.getImageSet('/splash/Dheeraj', 'png'), - imageUrlWebp: this.getImageSet('/splash/Dheeraj', 'webp'), - borderPresent: true - }, { - quote: 'I18N_SPLASH_TESTIMONIAL_3', - studentDetails: 'I18N_SPLASH_STUDENT_DETAILS_3', - imageUrl: this.getImageSet('/splash/sama', 'png'), - imageUrlWebp: this.getImageSet('/splash/sama', 'webp'), - borderPresent: false - }, { - quote: 'I18N_SPLASH_TESTIMONIAL_4', - studentDetails: 'I18N_SPLASH_STUDENT_DETAILS_4', - imageUrl: this.getImageSet('/splash/Gaurav', 'png'), - imageUrlWebp: this.getImageSet('/splash/Gaurav', 'webp'), - borderPresent: true - }]; + return [ + { + quote: 'I18N_SPLASH_TESTIMONIAL_1', + studentDetails: 'I18N_SPLASH_STUDENT_DETAILS_1', + imageUrl: this.getImageSet('/splash/mira', 'png'), + imageUrlWebp: this.getImageSet('/splash/mira', 'webp'), + borderPresent: false, + }, + { + quote: 'I18N_SPLASH_TESTIMONIAL_2', + studentDetails: 'I18N_SPLASH_STUDENT_DETAILS_2', + imageUrl: this.getImageSet('/splash/Dheeraj', 'png'), + imageUrlWebp: this.getImageSet('/splash/Dheeraj', 'webp'), + borderPresent: true, + }, + { + quote: 'I18N_SPLASH_TESTIMONIAL_3', + studentDetails: 'I18N_SPLASH_STUDENT_DETAILS_3', + imageUrl: this.getImageSet('/splash/sama', 'png'), + imageUrlWebp: this.getImageSet('/splash/sama', 'webp'), + borderPresent: false, + }, + { + quote: 'I18N_SPLASH_TESTIMONIAL_4', + studentDetails: 'I18N_SPLASH_STUDENT_DETAILS_4', + imageUrl: this.getImageSet('/splash/Gaurav', 'png'), + imageUrlWebp: this.getImageSet('/splash/Gaurav', 'webp'), + borderPresent: true, + }, + ]; } ngOnInit(): void { @@ -156,11 +163,13 @@ export class SplashPageComponent implements OnInit { this.testimonialCount = 4; this.testimonials = this.getTestimonials(); this.classroomUrl = this.urlInterpolationService.interpolateUrl( - '/learn/', { - classroomUrlFragment: AppConstants.DEFAULT_CLASSROOM_URL_FRAGMENT - }); + '/learn/', + { + classroomUrlFragment: AppConstants.DEFAULT_CLASSROOM_URL_FRAGMENT, + } + ); this.loaderService.showLoadingScreen('Loading'); - this.userService.getUserInfoAsync().then((userInfo) => { + this.userService.getUserInfoAsync().then(userInfo => { this.userIsLoggedIn = userInfo.isLoggedIn(); this.loaderService.hideLoadingScreen(); }); diff --git a/core/templates/pages/splash-page/splash-page.module.ts b/core/templates/pages/splash-page/splash-page.module.ts index 7a834a1bac15..5c887f831110 100644 --- a/core/templates/pages/splash-page/splash-page.module.ts +++ b/core/templates/pages/splash-page/splash-page.module.ts @@ -16,15 +16,15 @@ * @fileoverview Module for the splash page. */ -import { CommonModule } from '@angular/common'; -import { HttpClientModule } from '@angular/common/http'; -import { NgModule } from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {HttpClientModule} from '@angular/common/http'; +import {NgModule} from '@angular/core'; -import { BaseModule } from 'base-components/base.module'; -import { RichTextComponentsModule } from 'rich_text_components/rich-text-components.module'; -import { SplashPageComponent } from './splash-page.component'; -import { SplashPageRootComponent } from './splash-page-root.component'; -import { SplashPageRoutingModule } from './splash-page-routing.module'; +import {BaseModule} from 'base-components/base.module'; +import {RichTextComponentsModule} from 'rich_text_components/rich-text-components.module'; +import {SplashPageComponent} from './splash-page.component'; +import {SplashPageRootComponent} from './splash-page-root.component'; +import {SplashPageRoutingModule} from './splash-page-routing.module'; @NgModule({ imports: [ @@ -34,13 +34,7 @@ import { SplashPageRoutingModule } from './splash-page-routing.module'; RichTextComponentsModule, SplashPageRoutingModule, ], - declarations: [ - SplashPageComponent, - SplashPageRootComponent, - ], - entryComponents: [ - SplashPageComponent, - SplashPageRootComponent, - ] + declarations: [SplashPageComponent, SplashPageRootComponent], + entryComponents: [SplashPageComponent, SplashPageRootComponent], }) export class SplashPageModule {} diff --git a/core/templates/pages/story-editor-page/chapter-editor/chapter-editor-tab.component.spec.ts b/core/templates/pages/story-editor-page/chapter-editor/chapter-editor-tab.component.spec.ts index dd6fc572f854..053e04953f4a 100644 --- a/core/templates/pages/story-editor-page/chapter-editor/chapter-editor-tab.component.spec.ts +++ b/core/templates/pages/story-editor-page/chapter-editor/chapter-editor-tab.component.spec.ts @@ -16,14 +16,14 @@ * @fileoverview Unit tests for chapter editor tab component. */ -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { StoryEditorStateService } from '../services/story-editor-state.service'; -import { StoryBackendDict, Story } from 'domain/story/story.model'; -import { StoryEditorNavigationService } from '../services/story-editor-navigation.service'; -import { EditableStoryBackendApiService } from '../../../domain/story/editable-story-backend-api.service'; -import { ChapterEditorTabComponent } from './chapter-editor-tab.component'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {StoryEditorStateService} from '../services/story-editor-state.service'; +import {StoryBackendDict, Story} from 'domain/story/story.model'; +import {StoryEditorNavigationService} from '../services/story-editor-navigation.service'; +import {EditableStoryBackendApiService} from '../../../domain/story/editable-story-backend-api.service'; +import {ChapterEditorTabComponent} from './chapter-editor-tab.component'; describe('Chapter Editor Tab Component', () => { let component: ChapterEditorTabComponent; @@ -54,10 +54,10 @@ describe('Chapter Editor Tab Component', () => { EditableStoryBackendApiService, { provide: StoryEditorNavigationService, - useClass: MockStoryEditorNavigationService - } + useClass: MockStoryEditorNavigationService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); @@ -76,20 +76,22 @@ describe('Chapter Editor Tab Component', () => { story_contents: { initial_node_id: 'node_1', next_node_id: 'node_2', - nodes: [{ - id: 'node_1', - prerequisite_skill_ids: [], - acquired_skill_ids: [], - destination_node_ids: [], - outline: 'Outline', - exploration_id: null, - outline_is_finalized: false - }], + nodes: [ + { + id: 'node_1', + prerequisite_skill_ids: [], + acquired_skill_ids: [], + destination_node_ids: [], + outline: 'Outline', + exploration_id: null, + outline_is_finalized: false, + }, + ], }, language_code: 'en', story_contents_schema_version: 1, version: 1, - corresponding_topic_id: 'topic_id' + corresponding_topic_id: 'topic_id', } as unknown as StoryBackendDict); storyInitializedEventEmitter = new EventEmitter(); @@ -98,11 +100,13 @@ describe('Chapter Editor Tab Component', () => { spyOnProperty(storyEditorStateService, 'onStoryInitialized').and.callFake( () => { return storyInitializedEventEmitter; - }); + } + ); spyOnProperty(storyEditorStateService, 'onStoryReinitialized').and.callFake( () => { return storyReinitializedEventEmitter; - }); + } + ); storyEditorStateService.setStory(newStory); }); @@ -117,18 +121,16 @@ describe('Chapter Editor Tab Component', () => { expect(component.chapterIndex).toEqual(0); }); - it('should call StoryEditorNavigationService to navigate to story editor', - () => { - component.ngOnInit(); - component.navigateToStoryEditor(); - }); + it('should call StoryEditorNavigationService to navigate to story editor', () => { + component.ngOnInit(); + component.navigateToStoryEditor(); + }); - it('should called initEditor on calls from story being initialized', - () => { - spyOn(component, 'initEditor').and.callThrough(); - component.ngOnInit(); - storyInitializedEventEmitter.emit(); - storyReinitializedEventEmitter.emit(); - expect(component.initEditor).toHaveBeenCalledTimes(3); - }); + it('should called initEditor on calls from story being initialized', () => { + spyOn(component, 'initEditor').and.callThrough(); + component.ngOnInit(); + storyInitializedEventEmitter.emit(); + storyReinitializedEventEmitter.emit(); + expect(component.initEditor).toHaveBeenCalledTimes(3); + }); }); diff --git a/core/templates/pages/story-editor-page/chapter-editor/chapter-editor-tab.component.ts b/core/templates/pages/story-editor-page/chapter-editor/chapter-editor-tab.component.ts index fc0ee34845d1..b121332b9302 100644 --- a/core/templates/pages/story-editor-page/chapter-editor/chapter-editor-tab.component.ts +++ b/core/templates/pages/story-editor-page/chapter-editor/chapter-editor-tab.component.ts @@ -16,19 +16,19 @@ * @fileoverview Component for the chapter editor tab. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { Subscription } from 'rxjs'; -import { StoryEditorStateService } from '../services/story-editor-state.service'; -import { StoryEditorNavigationService } from '../services/story-editor-navigation.service'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { StoryUpdateService } from 'domain/story/story-update.service'; -import { Story } from 'domain/story/story.model'; -import { StoryContents } from 'domain/story/story-contents-object.model'; -import { StoryNode } from 'domain/story/story-node.model'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {Subscription} from 'rxjs'; +import {StoryEditorStateService} from '../services/story-editor-state.service'; +import {StoryEditorNavigationService} from '../services/story-editor-navigation.service'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {StoryUpdateService} from 'domain/story/story-update.service'; +import {Story} from 'domain/story/story.model'; +import {StoryContents} from 'domain/story/story-contents-object.model'; +import {StoryNode} from 'domain/story/story-node.model'; @Component({ selector: 'oppia-chapter-editor-tab', - templateUrl: './chapter-editor-tab.component.html' + templateUrl: './chapter-editor-tab.component.html', }) export class ChapterEditorTabComponent implements OnInit, OnDestroy { story: Story; @@ -42,7 +42,6 @@ export class ChapterEditorTabComponent implements OnInit, OnDestroy { private storyUpdateService: StoryUpdateService, private storyEditorStateService: StoryEditorStateService, private storyEditorNavigationService: StoryEditorNavigationService - ) {} directiveSubscriptions = new Subscription(); @@ -52,8 +51,7 @@ export class ChapterEditorTabComponent implements OnInit, OnDestroy { this.storyContents = this.story.getStoryContents(); this.chapterIndex = this.storyEditorNavigationService.getChapterIndex(); this.chapterId = this.storyEditorNavigationService.getChapterId(); - if (this.storyContents && - this.storyContents.getNodes().length > 0) { + if (this.storyContents && this.storyContents.getNodes().length > 0) { this.nodes = this.storyContents.getNodes(); if (!this.chapterIndex) { this.storyContents.getNodes().map((node, index) => { @@ -73,19 +71,17 @@ export class ChapterEditorTabComponent implements OnInit, OnDestroy { ngOnInit(): void { this.directiveSubscriptions.add( - this.storyEditorStateService.onStoryInitialized.subscribe( - () => this.initEditor() + this.storyEditorStateService.onStoryInitialized.subscribe(() => + this.initEditor() ) ); this.directiveSubscriptions.add( - this.storyEditorStateService.onStoryReinitialized.subscribe( - () => this.initEditor() + this.storyEditorStateService.onStoryReinitialized.subscribe(() => + this.initEditor() ) ); this.directiveSubscriptions.add( - this.storyUpdateService.storyChapterUpdateEventEmitter.subscribe( - () => {} - ) + this.storyUpdateService.storyChapterUpdateEventEmitter.subscribe(() => {}) ); this.initEditor(); } @@ -95,6 +91,9 @@ export class ChapterEditorTabComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive('oppiaChapterEditorTab', downgradeComponent({ - component: ChapterEditorTabComponent -})); +angular.module('oppia').directive( + 'oppiaChapterEditorTab', + downgradeComponent({ + component: ChapterEditorTabComponent, + }) +); diff --git a/core/templates/pages/story-editor-page/editor-tab/story-editor.component.spec.ts b/core/templates/pages/story-editor-page/editor-tab/story-editor.component.spec.ts index f630054c867b..3b0e8d5dede7 100644 --- a/core/templates/pages/story-editor-page/editor-tab/story-editor.component.spec.ts +++ b/core/templates/pages/story-editor-page/editor-tab/story-editor.component.spec.ts @@ -16,23 +16,29 @@ * @fileoverview Unit tests for the story editor component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, fakeAsync, tick, waitForAsync } from '@angular/core/testing'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { UndoRedoService } from 'domain/editor/undo_redo/undo-redo.service'; -import { StoryUpdateService } from 'domain/story/story-update.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { StoryEditorNavigationService } from '../services/story-editor-navigation.service'; -import { StoryEditorComponent } from './story-editor.component'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { StoryEditorStateService } from '../services/story-editor-state.service'; -import { Story } from 'domain/story/story.model'; -import { NewChapterTitleModalComponent } from '../modal-templates/new-chapter-title-modal.component'; -import { DeleteChapterModalComponent } from '../modal-templates/delete-chapter-modal.component'; -import { CdkDragDrop } from '@angular/cdk/drag-drop'; -import { StoryNode } from 'domain/story/story-node.model'; -import { PlatformFeatureService } from '../../../services/platform-feature.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + TestBed, + fakeAsync, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {UndoRedoService} from 'domain/editor/undo_redo/undo-redo.service'; +import {StoryUpdateService} from 'domain/story/story-update.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {StoryEditorNavigationService} from '../services/story-editor-navigation.service'; +import {StoryEditorComponent} from './story-editor.component'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {StoryEditorStateService} from '../services/story-editor-state.service'; +import {Story} from 'domain/story/story.model'; +import {NewChapterTitleModalComponent} from '../modal-templates/new-chapter-title-modal.component'; +import {DeleteChapterModalComponent} from '../modal-templates/delete-chapter-modal.component'; +import {CdkDragDrop} from '@angular/cdk/drag-drop'; +import {StoryNode} from 'domain/story/story-node.model'; +import {PlatformFeatureService} from '../../../services/platform-feature.service'; class MockNgbModalRef { componentInstance: { @@ -43,7 +49,7 @@ class MockNgbModalRef { class MockNgbModal { open() { return { - result: Promise.resolve() + result: Promise.resolve(), }; } } @@ -51,8 +57,8 @@ class MockNgbModal { class MockPlatformFeatureService { status = { SerialChapterLaunchCurriculumAdminView: { - isEnabled: false - } + isEnabled: false, + }, }; } @@ -76,7 +82,7 @@ describe('Story Editor Component having three story nodes', () => { declarations: [ StoryEditorComponent, NewChapterTitleModalComponent, - DeleteChapterModalComponent + DeleteChapterModalComponent, ], providers: [ WindowDimensionsService, @@ -86,31 +92,28 @@ describe('Story Editor Component having three story nodes', () => { StoryEditorStateService, { provide: PlatformFeatureService, - useValue: mockPlatformFeatureService + useValue: mockPlatformFeatureService, }, { provide: NgbModal, - useClass: MockNgbModal - } + useClass: MockNgbModal, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }); })); beforeEach(() => { - fixture = TestBed.createComponent( - StoryEditorComponent); + fixture = TestBed.createComponent(StoryEditorComponent); component = fixture.componentInstance; ngbModal = TestBed.inject(NgbModal); windowDimensionsService = TestBed.inject(WindowDimensionsService); - storyEditorNavigationService = TestBed.inject( - StoryEditorNavigationService); + storyEditorNavigationService = TestBed.inject(StoryEditorNavigationService); undoRedoService = TestBed.inject(UndoRedoService); windowRef = TestBed.inject(WindowRef); storyUpdateService = TestBed.inject(StoryUpdateService); storyEditorStateService = TestBed.inject(StoryEditorStateService); - let sampleStoryBackendObject = { id: 'sample_story_id', title: 'Story title', @@ -136,8 +139,9 @@ describe('Story Editor Component having three story nodes', () => { planned_publication_date_msecs: 30, last_modified_msecs: 20, first_publication_date_msecs: 10, - unpublishing_reason: 'Bad Content' - }, { + unpublishing_reason: 'Bad Content', + }, + { id: 'node_2', title: 'Title 2', description: 'Description 2', @@ -151,8 +155,9 @@ describe('Story Editor Component having three story nodes', () => { planned_publication_date_msecs: 30, last_modified_msecs: 20, first_publication_date_msecs: 10, - unpublishing_reason: null - }, { + unpublishing_reason: null, + }, + { id: 'node_3', title: 'Title 3', description: 'Description 3', @@ -166,21 +171,25 @@ describe('Story Editor Component having three story nodes', () => { planned_publication_date_msecs: 30, last_modified_msecs: 20, first_publication_date_msecs: 10, - unpublishing_reason: null - }], - next_node_id: 'node_3' + unpublishing_reason: null, + }, + ], + next_node_id: 'node_3', }, - language_code: 'en' + language_code: 'en', }; story = Story.createFromBackendDict(sampleStoryBackendObject); spyOn(windowDimensionsService, 'isWindowNarrow').and.returnValue(true); - fetchSpy = spyOn(storyEditorStateService, 'getStory') - .and.returnValue(story); + fetchSpy = spyOn(storyEditorStateService, 'getStory').and.returnValue( + story + ); spyOn(storyEditorStateService, 'getClassroomUrlFragment').and.returnValue( - 'math'); + 'math' + ); spyOn(storyEditorStateService, 'getTopicUrlFragment').and.returnValue( - 'fractions'); + 'fractions' + ); spyOn(storyEditorStateService, 'getTopicName').and.returnValue('addition'); component.ngOnInit(); }); @@ -192,8 +201,8 @@ describe('Story Editor Component having three story nodes', () => { it('should get status of Serial Chapter Launch Feature flag', () => { expect(component.isSerialChapterFeatureFlagEnabled()).toEqual(false); - mockPlatformFeatureService. - status.SerialChapterLaunchCurriculumAdminView.isEnabled = true; + mockPlatformFeatureService.status.SerialChapterLaunchCurriculumAdminView.isEnabled = + true; expect(component.isSerialChapterFeatureFlagEnabled()).toEqual(true); }); @@ -205,10 +214,11 @@ describe('Story Editor Component having three story nodes', () => { it('should get medium dateStyle locale date string', () => { const options = { - dateStyle: 'medium' + dateStyle: 'medium', } as Intl.DateTimeFormatOptions; expect(component.getMediumStyleLocaleDateString(1692144000000)).toEqual( - (new Date(1692144000000)).toLocaleDateString(undefined, options)); + new Date(1692144000000).toLocaleDateString(undefined, options) + ); }); it('should disable drag and drop', () => { @@ -228,7 +238,7 @@ describe('Story Editor Component having three story nodes', () => { planned_publication_date_msecs: 100, last_modified_msecs: 100, first_publication_date_msecs: 200, - unpublishing_reason: null + unpublishing_reason: null, }); expect(component.isDragAndDropDisabled(node)).toBeTrue(); @@ -239,60 +249,62 @@ describe('Story Editor Component having three story nodes', () => { it('should change list order', fakeAsync(() => { spyOn(storyUpdateService, 'rearrangeNodeInStory').and.stub(); - component.linearNodesList = [StoryNode.createFromBackendDict({ - id: 'node_1', - thumbnail_filename: 'image.png', - title: 'Title 1', - description: 'Description 1', - prerequisite_skill_ids: ['skill_1'], - acquired_skill_ids: ['skill_2'], - destination_node_ids: ['node_2'], - outline: 'Outline', - exploration_id: null, - outline_is_finalized: false, - thumbnail_bg_color: '#a33f40', - status: 'Published', - planned_publication_date_msecs: 100, - last_modified_msecs: 100, - first_publication_date_msecs: 200, - unpublishing_reason: null - }), - StoryNode.createFromBackendDict({ - id: 'node_2', - thumbnail_filename: 'image.png', - title: 'Title 2', - description: 'Description 2', - prerequisite_skill_ids: ['skill_1'], - acquired_skill_ids: ['skill_2'], - destination_node_ids: ['node_2'], - outline: 'Outline', - exploration_id: null, - outline_is_finalized: false, - thumbnail_bg_color: '#a33f40', - status: 'Ready To Publish', - planned_publication_date_msecs: 100, - last_modified_msecs: 100, - first_publication_date_msecs: 200, - unpublishing_reason: null - }), - StoryNode.createFromBackendDict({ - id: 'node_3', - thumbnail_filename: 'image.png', - title: 'Title 3', - description: 'Description 3', - prerequisite_skill_ids: ['skill_1'], - acquired_skill_ids: ['skill_2'], - destination_node_ids: ['node_2'], - outline: 'Outline', - exploration_id: null, - outline_is_finalized: false, - thumbnail_bg_color: '#a33f40', - status: 'Draft', - planned_publication_date_msecs: 100, - last_modified_msecs: 100, - first_publication_date_msecs: 200, - unpublishing_reason: null - })]; + component.linearNodesList = [ + StoryNode.createFromBackendDict({ + id: 'node_1', + thumbnail_filename: 'image.png', + title: 'Title 1', + description: 'Description 1', + prerequisite_skill_ids: ['skill_1'], + acquired_skill_ids: ['skill_2'], + destination_node_ids: ['node_2'], + outline: 'Outline', + exploration_id: null, + outline_is_finalized: false, + thumbnail_bg_color: '#a33f40', + status: 'Published', + planned_publication_date_msecs: 100, + last_modified_msecs: 100, + first_publication_date_msecs: 200, + unpublishing_reason: null, + }), + StoryNode.createFromBackendDict({ + id: 'node_2', + thumbnail_filename: 'image.png', + title: 'Title 2', + description: 'Description 2', + prerequisite_skill_ids: ['skill_1'], + acquired_skill_ids: ['skill_2'], + destination_node_ids: ['node_2'], + outline: 'Outline', + exploration_id: null, + outline_is_finalized: false, + thumbnail_bg_color: '#a33f40', + status: 'Ready To Publish', + planned_publication_date_msecs: 100, + last_modified_msecs: 100, + first_publication_date_msecs: 200, + unpublishing_reason: null, + }), + StoryNode.createFromBackendDict({ + id: 'node_3', + thumbnail_filename: 'image.png', + title: 'Title 3', + description: 'Description 3', + prerequisite_skill_ids: ['skill_1'], + acquired_skill_ids: ['skill_2'], + destination_node_ids: ['node_2'], + outline: 'Outline', + exploration_id: null, + outline_is_finalized: false, + thumbnail_bg_color: '#a33f40', + status: 'Draft', + planned_publication_date_msecs: 100, + last_modified_msecs: 100, + first_publication_date_msecs: 200, + unpublishing_reason: null, + }), + ]; const event1 = { previousIndex: 1, @@ -437,124 +449,124 @@ describe('Story Editor Component having three story nodes', () => { expect(component.getTopicUrlFragment()).toEqual('fractions'); }); - it('should not open confirm or cancel modal if the initial node is' + + it( + 'should not open confirm or cancel modal if the initial node is' + ' being deleted', - () => { - let modalSpy = spyOn(ngbModal, 'open'); + () => { + let modalSpy = spyOn(ngbModal, 'open'); - component.deleteNode('node_2'); + component.deleteNode('node_2'); - expect(modalSpy).not.toHaveBeenCalled(); - }); + expect(modalSpy).not.toHaveBeenCalled(); + } + ); - it('should open confirm or cancel modal when a node is being deleted', - fakeAsync(() => { - let modalSpy = spyOn(ngbModal, 'open').and.returnValue({ - result: Promise.resolve() - } as NgbModalRef); - let storyUpdateSpy = spyOn( - storyUpdateService, 'deleteStoryNode').and.stub(); + it('should open confirm or cancel modal when a node is being deleted', fakeAsync(() => { + let modalSpy = spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.resolve(), + } as NgbModalRef); + let storyUpdateSpy = spyOn( + storyUpdateService, + 'deleteStoryNode' + ).and.stub(); - component.deleteNode('node_1'); - tick(); + component.deleteNode('node_1'); + tick(); + + expect(storyUpdateSpy).toHaveBeenCalled(); + expect(modalSpy).toHaveBeenCalled(); + })); - expect(storyUpdateSpy).toHaveBeenCalled(); - expect(modalSpy).toHaveBeenCalled(); - })); + it('should call storyUpdateService to add destination node id', () => { + class MockComponentInstance { + compoenentInstance!: { + nodeTitles: null; + }; + } - it('should call storyUpdateService to add destination node id', - () => { - class MockComponentInstance { - compoenentInstance!: { - nodeTitles: null; - }; - } - - let modalSpy = spyOn(ngbModal, 'open').and.callFake(() => { - return ({ - componentInstance: MockComponentInstance, - result: Promise.resolve() - }) as NgbModalRef; - }); - - component.createNode(); - - expect(modalSpy).toHaveBeenCalled(); + let modalSpy = spyOn(ngbModal, 'open').and.callFake(() => { + return { + componentInstance: MockComponentInstance, + result: Promise.resolve(), + } as NgbModalRef; }); - it('should call storyUpdateService to add destination node id', - fakeAsync(() => { - class MockComponentInstance { - compoenentInstance!: { - nodeTitles: null; - }; - } - let sampleStoryBackendObject = { - id: 'sample_story_id', - title: 'Story title', - description: 'Story description', - notes: 'Story notes', - version: 1, - corresponding_topic_id: 'topic_id', - story_contents: { - initial_node_id: 'node_1', - nodes: [ - { - id: 'node_1', - title: 'Title 1', - description: 'Description 1', - prerequisite_skill_ids: ['skill_1'], - acquired_skill_ids: ['skill_2'], - destination_node_ids: [], - outline: 'Outline', - exploration_id: 'exp_id', - outline_is_finalized: false, - thumbnail_filename: 'fileName', - thumbnail_bg_color: 'blue', - }], - next_node_id: 'node_1' - }, - language_code: 'en', - thumbnail_filename: 'fileName', - thumbnail_bg_color: 'blue', - url_fragment: 'url', - meta_tag_content: 'meta' + component.createNode(); + + expect(modalSpy).toHaveBeenCalled(); + }); + + it('should call storyUpdateService to add destination node id', fakeAsync(() => { + class MockComponentInstance { + compoenentInstance!: { + nodeTitles: null; }; - spyOn(component, '_initEditor').and.stub(); - component.story = Story.createFromBackendDict( - sampleStoryBackendObject); - let modalSpy = spyOn(ngbModal, 'open').and.callFake(() => { - return ({ - componentInstance: MockComponentInstance, - result: Promise.resolve() - }) as NgbModalRef; - }); - - component.createNode(); - tick(); - - expect(modalSpy).toHaveBeenCalled(); - })); - - it('should call storyUpdateService to add destination node id', - fakeAsync(() => { - class MockComponentInstance { - compoenentInstance!: { - nodeTitles: null; - }; - } - let storySpy = spyOn(storyUpdateService, 'addDestinationNodeIdToNode'); - let modalSpy = spyOn(ngbModal, 'open').and.returnValue({ + } + let sampleStoryBackendObject = { + id: 'sample_story_id', + title: 'Story title', + description: 'Story description', + notes: 'Story notes', + version: 1, + corresponding_topic_id: 'topic_id', + story_contents: { + initial_node_id: 'node_1', + nodes: [ + { + id: 'node_1', + title: 'Title 1', + description: 'Description 1', + prerequisite_skill_ids: ['skill_1'], + acquired_skill_ids: ['skill_2'], + destination_node_ids: [], + outline: 'Outline', + exploration_id: 'exp_id', + outline_is_finalized: false, + thumbnail_filename: 'fileName', + thumbnail_bg_color: 'blue', + }, + ], + next_node_id: 'node_1', + }, + language_code: 'en', + thumbnail_filename: 'fileName', + thumbnail_bg_color: 'blue', + url_fragment: 'url', + meta_tag_content: 'meta', + }; + spyOn(component, '_initEditor').and.stub(); + component.story = Story.createFromBackendDict(sampleStoryBackendObject); + let modalSpy = spyOn(ngbModal, 'open').and.callFake(() => { + return { componentInstance: MockComponentInstance, - result: Promise.resolve() - } as NgbModalRef); + result: Promise.resolve(), + } as NgbModalRef; + }); + + component.createNode(); + tick(); - component.createNode(); - tick(); + expect(modalSpy).toHaveBeenCalled(); + })); + + it('should call storyUpdateService to add destination node id', fakeAsync(() => { + class MockComponentInstance { + compoenentInstance!: { + nodeTitles: null; + }; + } + let storySpy = spyOn(storyUpdateService, 'addDestinationNodeIdToNode'); + let modalSpy = spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: MockComponentInstance, + result: Promise.resolve(), + } as NgbModalRef); + + component.createNode(); + tick(); - expect(modalSpy).toHaveBeenCalled(); - expect(storySpy).toHaveBeenCalled(); - })); + expect(modalSpy).toHaveBeenCalled(); + expect(storySpy).toHaveBeenCalled(); + })); it('should call storyUpdateService to update story notes', () => { let storyUpdateSpy = spyOn(storyUpdateService, 'setStoryNotes'); @@ -583,8 +595,8 @@ describe('Story Editor Component having three story nodes', () => { it('should update the existence of story url fragment', () => { let storyUpdateSpy = spyOn( storyEditorStateService, - 'updateExistenceOfStoryUrlFragment').and.callFake( - (urlFragment, callback) => callback()); + 'updateExistenceOfStoryUrlFragment' + ).and.callFake((urlFragment, callback) => callback()); component.updateStoryUrlFragment('story_second'); @@ -592,23 +604,23 @@ describe('Story Editor Component having three story nodes', () => { }); it('should set story url fragment', () => { - let storyUpdateSpy = spyOn( - storyUpdateService, 'setStoryUrlFragment'); + let storyUpdateSpy = spyOn(storyUpdateService, 'setStoryUrlFragment'); component.updateStoryUrlFragment(''); expect(storyUpdateSpy).toHaveBeenCalled(); }); - it('should call storyEditorNavigationService to navigate to chapters', - () => { - let navigationSpy = spyOn( - storyEditorNavigationService, 'navigateToChapterEditorWithId'); + it('should call storyEditorNavigationService to navigate to chapters', () => { + let navigationSpy = spyOn( + storyEditorNavigationService, + 'navigateToChapterEditorWithId' + ); - component.navigateToChapterWithId('chapter_1', 0); + component.navigateToChapterWithId('chapter_1', 0); - expect(navigationSpy).toHaveBeenCalled(); - }); + expect(navigationSpy).toHaveBeenCalled(); + }); it('should make story description status', () => { component.editableDescriptionIsEmpty = true; @@ -619,8 +631,7 @@ describe('Story Editor Component having three story nodes', () => { }); it('should update the story description', () => { - let storyUpdateSpy = spyOn( - storyUpdateService, 'setStoryDescription'); + let storyUpdateSpy = spyOn(storyUpdateService, 'setStoryDescription'); component.updateStoryDescription('New skill description'); @@ -630,10 +641,10 @@ describe('Story Editor Component having three story nodes', () => { it('should show modal if there are unsaved changes on leaving', () => { spyOn(undoRedoService, 'getChangeCount').and.returnValue(10); const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ + return { componentInstance: MockNgbModalRef, - result: Promise.resolve() - }) as NgbModalRef; + result: Promise.resolve(), + } as NgbModalRef; }); component.returnToTopicEditorPage(); @@ -641,24 +652,23 @@ describe('Story Editor Component having three story nodes', () => { expect(modalSpy).toHaveBeenCalled(); }); - it('should show modal if there are unsaved changes and click reject', - () => { - spyOn(undoRedoService, 'getChangeCount').and.returnValue(10); - const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ - componentInstance: MockNgbModalRef, - result: Promise.reject() - }) as NgbModalRef; - }); - - component.returnToTopicEditorPage(); - expect(modalSpy).toHaveBeenCalled(); + it('should show modal if there are unsaved changes and click reject', () => { + spyOn(undoRedoService, 'getChangeCount').and.returnValue(10); + const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + componentInstance: MockNgbModalRef, + result: Promise.reject(), + } as NgbModalRef; }); + component.returnToTopicEditorPage(); + expect(modalSpy).toHaveBeenCalled(); + }); + it('should call windowref to open a tab', () => { spyOn(undoRedoService, 'getChangeCount').and.returnValue(0); spyOnProperty(windowRef, 'nativeWindow').and.returnValue({ - open: jasmine.createSpy('open', () => {}) + open: jasmine.createSpy('open', () => {}), }); component.returnToTopicEditorPage(); @@ -668,10 +678,14 @@ describe('Story Editor Component having three story nodes', () => { it('should fetch story when story is initialized', () => { let mockEventEmitter = new EventEmitter(); - spyOnProperty(storyEditorStateService, 'onStoryInitialized') - .and.returnValue(mockEventEmitter); + spyOnProperty( + storyEditorStateService, + 'onStoryInitialized' + ).and.returnValue(mockEventEmitter); let updatePublishUptoChapterSelectionSpy = spyOn( - component, 'updatePublishUptoChapterSelection'); + component, + 'updatePublishUptoChapterSelection' + ); component.ngOnInit(); mockEventEmitter.emit(); @@ -682,8 +696,10 @@ describe('Story Editor Component having three story nodes', () => { it('should fetch story when story is reinitialized', () => { let mockEventEmitter = new EventEmitter(); - spyOnProperty(storyEditorStateService, 'onStoryReinitialized') - .and.returnValue(mockEventEmitter); + spyOnProperty( + storyEditorStateService, + 'onStoryReinitialized' + ).and.returnValue(mockEventEmitter); component.ngOnInit(); mockEventEmitter.emit(); @@ -693,8 +709,10 @@ describe('Story Editor Component having three story nodes', () => { it('should fetch story node when story editor is opened', () => { let mockEventEmitter = new EventEmitter(); - spyOnProperty(storyEditorStateService, 'onViewStoryNodeEditor') - .and.returnValue(mockEventEmitter); + spyOnProperty( + storyEditorStateService, + 'onViewStoryNodeEditor' + ).and.returnValue(mockEventEmitter); component.ngOnInit(); mockEventEmitter.emit(); @@ -704,11 +722,17 @@ describe('Story Editor Component having three story nodes', () => { it('should update publish upto dropdown chapter selection', () => { let selectChapterSpy = spyOn( - storyEditorStateService, 'setSelectedChapterIndexInPublishUptoDropdown'); + storyEditorStateService, + 'setSelectedChapterIndexInPublishUptoDropdown' + ); let chaptersAreBeingPublishedSpy = spyOn( - storyEditorStateService, 'setChaptersAreBeingPublished'); + storyEditorStateService, + 'setChaptersAreBeingPublished' + ); let newChapterPublicationIsDisabledSpy = spyOn( - storyEditorStateService, 'setNewChapterPublicationIsDisabled'); + storyEditorStateService, + 'setNewChapterPublicationIsDisabled' + ); component.updatePublishUptoChapterSelection(1); expect(selectChapterSpy).toHaveBeenCalledWith(1); diff --git a/core/templates/pages/story-editor-page/editor-tab/story-editor.component.ts b/core/templates/pages/story-editor-page/editor-tab/story-editor.component.ts index c8096260d4d1..72cc8e7c2c43 100644 --- a/core/templates/pages/story-editor-page/editor-tab/story-editor.component.ts +++ b/core/templates/pages/story-editor-page/editor-tab/story-editor.component.ts @@ -16,34 +16,34 @@ * @fileoverview Controller for the main story editor. */ -import { Subscription } from 'rxjs'; -import { SavePendingChangesModalComponent } from 'components/save-pending-changes/save-pending-changes-modal.component'; -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { StoryEditorStateService } from '../services/story-editor-state.service'; -import { StoryUpdateService } from 'domain/story/story-update.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { AlertsService } from 'services/alerts.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { StoryNode } from 'domain/story/story-node.model'; -import { StoryEditorNavigationService } from '../services/story-editor-navigation.service'; -import { UndoRedoService } from 'domain/editor/undo_redo/undo-redo.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { NewChapterTitleModalComponent } from '../modal-templates/new-chapter-title-modal.component'; -import { DeleteChapterModalComponent } from '../modal-templates/delete-chapter-modal.component'; -import { Story } from 'domain/story/story.model'; -import { StoryContents } from 'domain/story/story-contents-object.model'; -import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; -import { PlatformFeatureService } from 'services/platform-feature.service'; -import { DateTimeFormatService } from 'services/date-time-format.service'; +import {Subscription} from 'rxjs'; +import {SavePendingChangesModalComponent} from 'components/save-pending-changes/save-pending-changes-modal.component'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {AppConstants} from 'app.constants'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {StoryEditorStateService} from '../services/story-editor-state.service'; +import {StoryUpdateService} from 'domain/story/story-update.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {AlertsService} from 'services/alerts.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {StoryNode} from 'domain/story/story-node.model'; +import {StoryEditorNavigationService} from '../services/story-editor-navigation.service'; +import {UndoRedoService} from 'domain/editor/undo_redo/undo-redo.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {NewChapterTitleModalComponent} from '../modal-templates/new-chapter-title-modal.component'; +import {DeleteChapterModalComponent} from '../modal-templates/delete-chapter-modal.component'; +import {Story} from 'domain/story/story.model'; +import {StoryContents} from 'domain/story/story-contents-object.model'; +import {CdkDragDrop, moveItemInArray} from '@angular/cdk/drag-drop'; +import {PlatformFeatureService} from 'services/platform-feature.service'; +import {DateTimeFormatService} from 'services/date-time-format.service'; import constants from 'assets/constants'; @Component({ selector: 'oppia-story-editor', - templateUrl: './story-editor.component.html' + templateUrl: './story-editor.component.html', }) export class StoryEditorComponent implements OnInit, OnDestroy { story: Story; @@ -51,8 +51,7 @@ export class StoryEditorComponent implements OnInit, OnDestroy { disconnectedNodes: string[]; linearNodesList: StoryNode[]; nodes: StoryNode[]; - allowedBgColors = ( - AppConstants.ALLOWED_THUMBNAIL_BG_COLORS.story); + allowedBgColors = AppConstants.ALLOWED_THUMBNAIL_BG_COLORS.story; initialNodeId: string; notesEditorIsShown: boolean; @@ -79,8 +78,8 @@ export class StoryEditorComponent implements OnInit, OnDestroy { NOTES_SCHEMA = { type: 'html', ui_config: { - startupFocusEnabled: false - } + startupFocusEnabled: false, + }, }; constructor( @@ -95,17 +94,16 @@ export class StoryEditorComponent implements OnInit, OnDestroy { private undoRedoService: UndoRedoService, private urlInterpolationService: UrlInterpolationService, private platformFeatureService: PlatformFeatureService, - private dateTimeFormatService: DateTimeFormatService, + private dateTimeFormatService: DateTimeFormatService ) {} directiveSubscriptions = new Subscription(); - MAX_CHARS_IN_STORY_DESCRIPTION = ( - AppConstants.MAX_CHARS_IN_STORY_DESCRIPTION); + MAX_CHARS_IN_STORY_DESCRIPTION = AppConstants.MAX_CHARS_IN_STORY_DESCRIPTION; MAX_CHARS_IN_STORY_TITLE = AppConstants.MAX_CHARS_IN_STORY_TITLE; - MAX_CHARS_IN_STORY_URL_FRAGMENT = ( - AppConstants.MAX_CHARS_IN_STORY_URL_FRAGMENT); + MAX_CHARS_IN_STORY_URL_FRAGMENT = + AppConstants.MAX_CHARS_IN_STORY_URL_FRAGMENT; MAX_CHARS_IN_META_TAG_CONTENT = AppConstants.MAX_CHARS_IN_META_TAG_CONTENT; @@ -124,27 +122,30 @@ export class StoryEditorComponent implements OnInit, OnDestroy { } rearrangeNodeInList(fromIndex: number, toIndex: number): void { - moveItemInArray( - this.linearNodesList, - fromIndex, toIndex); + moveItemInArray(this.linearNodesList, fromIndex, toIndex); if (fromIndex === 0) { this.storyUpdateService.setInitialNodeId( - this.story, this.story.getStoryContents().getNodes()[ - toIndex].getId()); + this.story, + this.story.getStoryContents().getNodes()[toIndex].getId() + ); } if (fromIndex === 0) { this.storyUpdateService.setInitialNodeId( - this.story, this.story.getStoryContents().getNodes()[ - toIndex].getId()); + this.story, + this.story.getStoryContents().getNodes()[toIndex].getId() + ); } this.storyUpdateService.rearrangeNodeInStory( - this.story, fromIndex, toIndex); + this.story, + fromIndex, + toIndex + ); this._initEditor(); } getMediumStyleLocaleDateString(millisSinceEpoch: number): string { const options = { - dateStyle: 'medium' + dateStyle: 'medium', } as Intl.DateTimeFormatOptions; let date = new Date(millisSinceEpoch); return date.toLocaleDateString(undefined, options); @@ -153,7 +154,8 @@ export class StoryEditorComponent implements OnInit, OnDestroy { isDragAndDropDisabled(node: StoryNode): boolean { return ( node.getStatus() === constants.STORY_NODE_STATUS_PUBLISHED || - window.innerWidth <= 425); + window.innerWidth <= 425 + ); } drop(event: CdkDragDrop): void { @@ -178,9 +180,8 @@ export class StoryEditorComponent implements OnInit, OnDestroy { } isSerialChapterFeatureFlagEnabled(): boolean { - return ( - this.platformFeatureService. - status.SerialChapterLaunchCurriculumAdminView.isEnabled); + return this.platformFeatureService.status + .SerialChapterLaunchCurriculumAdminView.isEnabled; } _initEditor(): void { @@ -190,27 +191,28 @@ export class StoryEditorComponent implements OnInit, OnDestroy { this.disconnectedNodes = []; this.linearNodesList = []; this.nodes = []; - if (this.storyContents && - this.storyContents.getNodes().length > 0) { + if (this.storyContents && this.storyContents.getNodes().length > 0) { this.nodes = this.storyContents.getNodes(); this.initialNodeId = this.storyContents.getInitialNodeId(); - this.linearNodesList = - this.storyContents.getLinearNodesList(); + this.linearNodesList = this.storyContents.getLinearNodesList(); this.chapterIsPublishable = []; this.linearNodesList.forEach((node, index) => { if (node.getStatus() === 'Published') { this.chapterIsPublishable.push(true); this.selectedChapterIndexInPublishUptoDropdown = index; - } else if (node.getStatus() === 'Ready To Publish' && + } else if ( + node.getStatus() === 'Ready To Publish' && ((index !== 0 && this.chapterIsPublishable[index - 1]) || - index === 0)) { + index === 0) + ) { this.chapterIsPublishable.push(true); } else { this.chapterIsPublishable.push(false); } }); this.updatePublishUptoChapterSelection( - this.selectedChapterIndexInPublishUptoDropdown); + this.selectedChapterIndexInPublishUptoDropdown + ); } this.notesEditorIsShown = false; this.storyTitleEditorIsShown = false; @@ -220,8 +222,7 @@ export class StoryEditorComponent implements OnInit, OnDestroy { this.initialStoryUrlFragment = this.story.getUrlFragment(); this.editableNotes = this.story.getNotes(); this.editableDescription = this.story.getDescription(); - this.editableDescriptionIsEmpty = ( - this.editableDescription === ''); + this.editableDescriptionIsEmpty = this.editableDescription === ''; this.storyDescriptionChanged = false; this.storyUrlFragmentExists = false; } @@ -240,31 +241,37 @@ export class StoryEditorComponent implements OnInit, OnDestroy { } isInitialNode(nodeId: string): boolean { - return ( - this.story.getStoryContents().getInitialNodeId() === nodeId); + return this.story.getStoryContents().getInitialNodeId() === nodeId; } deleteNode(nodeId: string): void { if (this.isInitialNode(nodeId)) { this.alertsService.addInfoMessage( - 'Cannot delete the first chapter of a story.', 3000); + 'Cannot delete the first chapter of a story.', + 3000 + ); return; } - this.ngbModal.open(DeleteChapterModalComponent, { - backdrop: true, - }).result.then(() => { - this.storyUpdateService.deleteStoryNode(this.story, nodeId); - this._initEditor(); - this.storyEditorStateService.onRecalculateAvailableNodes.emit(); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + this.ngbModal + .open(DeleteChapterModalComponent, { + backdrop: true, + }) + .result.then( + () => { + this.storyUpdateService.deleteStoryNode(this.story, nodeId); + this._initEditor(); + this.storyEditorStateService.onRecalculateAvailableNodes.emit(); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } createNode(): void { - let nodeTitles = this.linearNodesList.map((node) => { + let nodeTitles = this.linearNodesList.map(node => { return node.getTitle(); }); const modalRef = this.ngbModal.open(NewChapterTitleModalComponent, { @@ -272,26 +279,31 @@ export class StoryEditorComponent implements OnInit, OnDestroy { windowClass: 'create-new-chapter', }); modalRef.componentInstance.nodeTitles = nodeTitles; - modalRef.result.then(() => { - this._initEditor(); - // If the first node is added, open it just after creation. - if (this.story.getStoryContents().getNodes().length === 1) { - this.setNodeToEdit( - this.story.getStoryContents().getInitialNodeId()); - } else { - let nodesArray = this.story.getStoryContents().getNodes(); - let nodesLength = nodesArray.length; - let secondLastNodeId = nodesArray[nodesLength - 2].getId(); - let lastNodeId = nodesArray[nodesLength - 1].getId(); - this.storyUpdateService.addDestinationNodeIdToNode( - this.story, secondLastNodeId, lastNodeId); + modalRef.result.then( + () => { + this._initEditor(); + // If the first node is added, open it just after creation. + if (this.story.getStoryContents().getNodes().length === 1) { + this.setNodeToEdit(this.story.getStoryContents().getInitialNodeId()); + } else { + let nodesArray = this.story.getStoryContents().getNodes(); + let nodesLength = nodesArray.length; + let secondLastNodeId = nodesArray[nodesLength - 2].getId(); + let lastNodeId = nodesArray[nodesLength - 1].getId(); + this.storyUpdateService.addDestinationNodeIdToNode( + this.story, + secondLastNodeId, + lastNodeId + ); + } + this.storyEditorStateService.onRecalculateAvailableNodes.emit(); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. } - this.storyEditorStateService.onRecalculateAvailableNodes.emit(); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + ); } updateNotes(newNotes: string): void { @@ -302,47 +314,53 @@ export class StoryEditorComponent implements OnInit, OnDestroy { } navigateToChapterWithId(id: string, index: number): void { - this.storyEditorNavigationService.navigateToChapterEditorWithId( - id, index); + this.storyEditorNavigationService.navigateToChapterEditorWithId(id, index); } updateStoryDescriptionStatus(description: string): void { - this.editableDescriptionIsEmpty = (description === ''); + this.editableDescriptionIsEmpty = description === ''; this.storyDescriptionChanged = true; } updateStoryMetaTagContent(newMetaTagContent: string): void { if (newMetaTagContent !== this.story.getMetaTagContent()) { this.storyUpdateService.setStoryMetaTagContent( - this.story, newMetaTagContent); + this.story, + newMetaTagContent + ); } } returnToTopicEditorPage(): void { if (this.undoRedoService.getChangeCount() > 0) { - const modalRef = this.ngbModal.open( - SavePendingChangesModalComponent, { - backdrop: true - }); + const modalRef = this.ngbModal.open(SavePendingChangesModalComponent, { + backdrop: true, + }); - modalRef.componentInstance.body = ( - 'Please save all pending changes ' + - 'before returning to the topic.'); + modalRef.componentInstance.body = + 'Please save all pending changes ' + 'before returning to the topic.'; - modalRef.result.then(() => {}, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + modalRef.result.then( + () => {}, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } else { - const topicId = ( - this.storyEditorStateService.getStory().getCorrespondingTopicId()); + const topicId = this.storyEditorStateService + .getStory() + .getCorrespondingTopicId(); this.windowRef.nativeWindow.open( this.urlInterpolationService.interpolateUrl( - this.TOPIC_EDITOR_URL_TEMPLATE, { - topic_id: topicId + this.TOPIC_EDITOR_URL_TEMPLATE, + { + topic_id: topicId, } - ), '_self'); + ), + '_self' + ); } } @@ -371,38 +389,42 @@ export class StoryEditorComponent implements OnInit, OnDestroy { } if (newUrlFragment) { this.storyEditorStateService.updateExistenceOfStoryUrlFragment( - newUrlFragment, () => { - this.storyUrlFragmentExists = ( - this.storyEditorStateService.getStoryWithUrlFragmentExists()); + newUrlFragment, + () => { + this.storyUrlFragmentExists = + this.storyEditorStateService.getStoryWithUrlFragmentExists(); this.storyUpdateService.setStoryUrlFragment( - this.story, newUrlFragment); - }); + this.story, + newUrlFragment + ); + } + ); } else { - this.storyUpdateService.setStoryUrlFragment( - this.story, newUrlFragment); + this.storyUpdateService.setStoryUrlFragment(this.story, newUrlFragment); } } - updateStoryThumbnailFilename( - newThumbnailFilename: string): void { + updateStoryThumbnailFilename(newThumbnailFilename: string): void { if (newThumbnailFilename !== this.story.getThumbnailFilename()) { this.storyUpdateService.setThumbnailFilename( - this.story, newThumbnailFilename); + this.story, + newThumbnailFilename + ); } } - updateStoryThumbnailBgColor( - newThumbnailBgColor: string): void { + updateStoryThumbnailBgColor(newThumbnailBgColor: string): void { if (newThumbnailBgColor !== this.story.getThumbnailBgColor()) { this.storyUpdateService.setThumbnailBgColor( - this.story, newThumbnailBgColor); + this.story, + newThumbnailBgColor + ); } } updateStoryDescription(newDescription: string): void { if (newDescription !== this.story.getDescription()) { - this.storyUpdateService.setStoryDescription( - this.story, newDescription); + this.storyUpdateService.setStoryDescription(this.story, newDescription); } } @@ -411,8 +433,10 @@ export class StoryEditorComponent implements OnInit, OnDestroy { chapterIndex ); if (Number(chapterIndex) === -1) { - if (this.linearNodesList.length && - this.linearNodesList[0].getStatus() === 'Published') { + if ( + this.linearNodesList.length && + this.linearNodesList[0].getStatus() === 'Published' + ) { this.storyEditorStateService.setChaptersAreBeingPublished(false); this.storyEditorStateService.setNewChapterPublicationIsDisabled(false); } else { @@ -424,19 +448,23 @@ export class StoryEditorComponent implements OnInit, OnDestroy { let nextChapterIndex = Number(chapterIndex) + 1; - if (this.linearNodesList[chapterIndex].getStatus() === 'Published' && - nextChapterIndex < this.linearNodesList.length && - this.linearNodesList[nextChapterIndex].getStatus() === 'Published') { + if ( + this.linearNodesList[chapterIndex].getStatus() === 'Published' && + nextChapterIndex < this.linearNodesList.length && + this.linearNodesList[nextChapterIndex].getStatus() === 'Published' + ) { this.storyEditorStateService.setChaptersAreBeingPublished(false); } else { this.storyEditorStateService.setChaptersAreBeingPublished(true); } - if (this.linearNodesList.length === 0 || ( - chapterIndex === 0 && !this.chapterIsPublishable[0]) || ( - this.linearNodesList[chapterIndex].getStatus() === 'Published' && ( - nextChapterIndex === this.linearNodesList.length || - this.linearNodesList[nextChapterIndex].getStatus() !== 'Published'))) { + if ( + this.linearNodesList.length === 0 || + (chapterIndex === 0 && !this.chapterIsPublishable[0]) || + (this.linearNodesList[chapterIndex].getStatus() === 'Published' && + (nextChapterIndex === this.linearNodesList.length || + this.linearNodesList[nextChapterIndex].getStatus() !== 'Published')) + ) { this.storyEditorStateService.setNewChapterPublicationIsDisabled(true); } else { this.storyEditorStateService.setNewChapterPublicationIsDisabled(false); @@ -444,12 +472,12 @@ export class StoryEditorComponent implements OnInit, OnDestroy { } togglePreview(): void { - this.storyPreviewCardIsShown = !(this.storyPreviewCardIsShown); + this.storyPreviewCardIsShown = !this.storyPreviewCardIsShown; } toggleChapterEditOptions(chapterIndex: number): void { - this.selectedChapterIndex = ( - this.selectedChapterIndex === chapterIndex) ? -1 : chapterIndex; + this.selectedChapterIndex = + this.selectedChapterIndex === chapterIndex ? -1 : chapterIndex; } toggleChapterLists(): void { @@ -468,8 +496,7 @@ export class StoryEditorComponent implements OnInit, OnDestroy { this.storyPreviewCardIsShown = false; this.mainStoryCardIsShown = true; this.selectedChapterIndexInPublishUptoDropdown = -1; - this.chaptersListIsShown = ( - !this.windowDimensionsService.isWindowNarrow()); + this.chaptersListIsShown = !this.windowDimensionsService.isWindowNarrow(); this.directiveSubscriptions.add( this.storyEditorStateService.onViewStoryNodeEditor.subscribe( (nodeId: string) => this.setNodeToEdit(nodeId) @@ -477,16 +504,16 @@ export class StoryEditorComponent implements OnInit, OnDestroy { ); this.directiveSubscriptions.add( - this.storyEditorStateService.onStoryInitialized.subscribe( - () =>{ - this._init(); - this.focusManagerService.setFocus('metaTagInputField'); - } - )); + this.storyEditorStateService.onStoryInitialized.subscribe(() => { + this._init(); + this.focusManagerService.setFocus('metaTagInputField'); + }) + ); this.directiveSubscriptions.add( - this.storyEditorStateService.onStoryReinitialized.subscribe( - () => this._initEditor() - )); + this.storyEditorStateService.onStoryReinitialized.subscribe(() => + this._initEditor() + ) + ); this._init(); this._initEditor(); @@ -497,6 +524,9 @@ export class StoryEditorComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive('oppiaStoryEditor', downgradeComponent({ - component: StoryEditorComponent -})); +angular.module('oppia').directive( + 'oppiaStoryEditor', + downgradeComponent({ + component: StoryEditorComponent, + }) +); diff --git a/core/templates/pages/story-editor-page/editor-tab/story-node-editor.component.spec.ts b/core/templates/pages/story-editor-page/editor-tab/story-node-editor.component.spec.ts index 937e9bb1d7c4..fa44961529b8 100644 --- a/core/templates/pages/story-editor-page/editor-tab/story-node-editor.component.spec.ts +++ b/core/templates/pages/story-editor-page/editor-tab/story-node-editor.component.spec.ts @@ -15,26 +15,33 @@ /** * @fileoverview Unit tests for the story node editor directive. */ -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, flush, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { RecordedVoiceovers } from 'domain/exploration/recorded-voiceovers.model'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { ConceptCard } from 'domain/skill/concept-card.model'; -import { Skill } from 'domain/skill/SkillObjectFactory'; -import { StoryUpdateService } from 'domain/story/story-update.service'; -import { Story } from 'domain/story/story.model'; -import { AlertsService } from 'services/alerts.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { StoryEditorStateService } from '../services/story-editor-state.service'; -import { CuratedExplorationValidationService } from '../../../domain/exploration/curated-exploration-validation.service'; -import { WindowDimensionsService } from '../../../services/contextual/window-dimensions.service'; -import { StoryNodeEditorComponent } from './story-node-editor.component'; -import { EditableStoryBackendApiService } from '../../../domain/story/editable-story-backend-api.service'; -import { SkillBackendApiService } from '../../../domain/skill/skill-backend-api.service'; -import { TopicsAndSkillsDashboardBackendApiService } from '../../../domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { PlatformFeatureService } from '../../../services/platform-feature.service'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + flush, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {RecordedVoiceovers} from 'domain/exploration/recorded-voiceovers.model'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {ConceptCard} from 'domain/skill/concept-card.model'; +import {Skill} from 'domain/skill/SkillObjectFactory'; +import {StoryUpdateService} from 'domain/story/story-update.service'; +import {Story} from 'domain/story/story.model'; +import {AlertsService} from 'services/alerts.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {StoryEditorStateService} from '../services/story-editor-state.service'; +import {CuratedExplorationValidationService} from '../../../domain/exploration/curated-exploration-validation.service'; +import {WindowDimensionsService} from '../../../services/contextual/window-dimensions.service'; +import {StoryNodeEditorComponent} from './story-node-editor.component'; +import {EditableStoryBackendApiService} from '../../../domain/story/editable-story-backend-api.service'; +import {SkillBackendApiService} from '../../../domain/skill/skill-backend-api.service'; +import {TopicsAndSkillsDashboardBackendApiService} from '../../../domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {PlatformFeatureService} from '../../../services/platform-feature.service'; class MockNgbModalRef { componentInstance: { @@ -50,17 +57,17 @@ class MockTopicsAndSkillsDashboardBackendApiService { fetchDashboardDataAsync = () => { return Promise.resolve({ categorizedSkillsDict: { - addition: {} + addition: {}, }, untriagedSkillSummaries: { - addition: {} - } + addition: {}, + }, }); }; } class MockSkillBackendApiService { - fetchMultiSkillsAsync = (skillIds) => { + fetchMultiSkillsAsync = skillIds => { // The skillId ='2' case is used to test the case when the // SkillBackendApiService rejects the request. if (skillIds[0] === '2') { @@ -68,23 +75,56 @@ class MockSkillBackendApiService { } else { return Promise.resolve([ new Skill( - '1', 'test', [], [], + '1', + 'test', + [], + [], new ConceptCard( - new SubtitledHtml( - '', '1'), [], RecordedVoiceovers.createEmpty()), - 'en', 1, 1, '0', true, []), + new SubtitledHtml('', '1'), + [], + RecordedVoiceovers.createEmpty() + ), + 'en', + 1, + 1, + '0', + true, + [] + ), new Skill( - '2', 'test2', [], [], + '2', + 'test2', + [], + [], new ConceptCard( - new SubtitledHtml( - '', '1'), [], RecordedVoiceovers.createEmpty()), - 'en', 1, 1, '0', true, []), + new SubtitledHtml('', '1'), + [], + RecordedVoiceovers.createEmpty() + ), + 'en', + 1, + 1, + '0', + true, + [] + ), new Skill( - '3', 'test3', [], [], + '3', + 'test3', + [], + [], new ConceptCard( - new SubtitledHtml( - '', '1'), [], RecordedVoiceovers.createEmpty()), - 'en', 1, 1, '0', true, []) + new SubtitledHtml('', '1'), + [], + RecordedVoiceovers.createEmpty() + ), + 'en', + 1, + 1, + '0', + true, + [] + ), ]); } }; @@ -93,8 +133,8 @@ class MockSkillBackendApiService { class MockPlatformFeatureService { status = { SerialChapterLaunchCurriculumAdminView: { - isEnabled: false - } + isEnabled: false, + }, }; } @@ -105,8 +145,7 @@ describe('Story node editor component', () => { let story: Story; let windowDimensionsService: WindowDimensionsService; let storyUpdateService: StoryUpdateService; - let curatedExplorationValidationService: - CuratedExplorationValidationService; + let curatedExplorationValidationService: CuratedExplorationValidationService; let alertsService: AlertsService; let storyEditorStateService: StoryEditorStateService; let focusManagerService: FocusManagerService; @@ -116,9 +155,7 @@ describe('Story node editor component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - StoryNodeEditorComponent - ], + declarations: [StoryNodeEditorComponent], providers: [ WindowDimensionsService, StoryUpdateService, @@ -129,18 +166,18 @@ describe('Story node editor component', () => { EditableStoryBackendApiService, { provide: PlatformFeatureService, - useValue: mockPlatformFeatureService + useValue: mockPlatformFeatureService, }, { provide: SkillBackendApiService, - useClass: MockSkillBackendApiService + useClass: MockSkillBackendApiService, }, { provide: TopicsAndSkillsDashboardBackendApiService, - useClass: MockTopicsAndSkillsDashboardBackendApiService - } + useClass: MockTopicsAndSkillsDashboardBackendApiService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -149,8 +186,9 @@ describe('Story node editor component', () => { component = fixture.componentInstance; ngbModal = TestBed.inject(NgbModal); windowDimensionsService = TestBed.inject(WindowDimensionsService); - curatedExplorationValidationService = ( - TestBed.inject(CuratedExplorationValidationService)); + curatedExplorationValidationService = TestBed.inject( + CuratedExplorationValidationService + ); alertsService = TestBed.inject(AlertsService); storyUpdateService = TestBed.inject(StoryUpdateService); storyEditorStateService = TestBed.inject(StoryEditorStateService); @@ -176,7 +214,7 @@ describe('Story node editor component', () => { destination_node_ids: [], outline: 'Outline', exploration_id: null, - outline_is_finalized: false + outline_is_finalized: false, }, { id: 'node_1', @@ -188,8 +226,9 @@ describe('Story node editor component', () => { outline: 'Outline', exploration_id: null, outline_is_finalized: false, - planned_publication_date_msecs: 168960000000 - }, { + planned_publication_date_msecs: 168960000000, + }, + { id: 'node_2', title: 'Title 2', description: 'Description 2', @@ -198,8 +237,9 @@ describe('Story node editor component', () => { destination_node_ids: ['node_1'], outline: 'Outline 2', exploration_id: 'exp_1', - outline_is_finalized: true - }, { + outline_is_finalized: true, + }, + { id: 'break', title: 'Title 2', description: 'Description 2', @@ -208,25 +248,31 @@ describe('Story node editor component', () => { destination_node_ids: ['node_1'], outline: 'Outline 2', exploration_id: 'exp_1', - outline_is_finalized: true - }], - next_node_id: 'node_3' + outline_is_finalized: true, + }, + ], + next_node_id: 'node_3', }, - language_code: 'en' + language_code: 'en', }; story = Story.createFromBackendDict(sampleStoryBackendObject); spyOn(windowDimensionsService, 'isWindowNarrow').and.returnValue(true); - spyOn(storyEditorStateService, 'getSkillSummaries').and.returnValue( - [{ id: '1', description: 'Skill description' }]); + spyOn(storyEditorStateService, 'getSkillSummaries').and.returnValue([ + {id: '1', description: 'Skill description'}, + ]); spyOn(storyEditorStateService, 'getStory').and.returnValue(story); spyOn(storyEditorStateService, 'getClassroomUrlFragment').and.returnValue( - 'math'); + 'math' + ); spyOn(storyEditorStateService, 'getTopicUrlFragment').and.returnValue( - 'fractions'); + 'fractions' + ); spyOn(storyEditorStateService, 'getTopicName').and.returnValue('addition'); - spyOnProperty(storyEditorStateService, 'onRecalculateAvailableNodes') - .and.returnValue(mockEventEmitterLast); + spyOnProperty( + storyEditorStateService, + 'onRecalculateAvailableNodes' + ).and.returnValue(mockEventEmitterLast); component.nodeId = 'node1'; component.storyNodeIds = ['node1', 'node_2', 'node_3', 'wroking']; @@ -249,14 +295,15 @@ describe('Story node editor component', () => { it('should get status of Serial Chapter Launch Feature flag', () => { expect(component.isSerialChapterFeatureFlagEnabled()).toEqual(false); - mockPlatformFeatureService. - status.SerialChapterLaunchCurriculumAdminView.isEnabled = true; + mockPlatformFeatureService.status.SerialChapterLaunchCurriculumAdminView.isEnabled = + true; expect(component.isSerialChapterFeatureFlagEnabled()).toEqual(true); }); it('should return skill editor URL', () => { expect(component.getSkillEditorUrl('skill_1')).toEqual( - '/skill_editor/skill_1'); + '/skill_editor/skill_1' + ); }); it('should fetch the descriptions for prerequisite skills', fakeAsync(() => { @@ -265,21 +312,22 @@ describe('Story node editor component', () => { component.getPrerequisiteSkillsDescription(); tick(); - expect(component.skillIdToSummaryMap).toEqual( - { 1: 'test', 2: 'test2', 3: 'test3' } - ); + expect(component.skillIdToSummaryMap).toEqual({ + 1: 'test', + 2: 'test2', + 3: 'test3', + }); })); - it('should call Alerts Service if getting skill desc. fails', fakeAsync( - () => { - component.prerequisiteSkillIds = ['2']; - let alertsSpy = spyOn(alertsService, 'addWarning').and.callThrough(); + it('should call Alerts Service if getting skill desc. fails', fakeAsync(() => { + component.prerequisiteSkillIds = ['2']; + let alertsSpy = spyOn(alertsService, 'addWarning').and.callThrough(); - component.getPrerequisiteSkillsDescription(); - tick(); + component.getPrerequisiteSkillsDescription(); + tick(); - expect(alertsSpy).toHaveBeenCalled(); - })); + expect(alertsSpy).toHaveBeenCalled(); + })); it('should check if exploration can be saved', () => { component.checkCanSaveExpId(); @@ -287,15 +335,16 @@ describe('Story node editor component', () => { expect(component.expIdCanBeSaved).toEqual(true); }); - it('should call StoryUpdate service remove prerequisite skill id', - () => { - let skillSpy = spyOn( - storyUpdateService, 'removePrerequisiteSkillIdFromNode'); + it('should call StoryUpdate service remove prerequisite skill id', () => { + let skillSpy = spyOn( + storyUpdateService, + 'removePrerequisiteSkillIdFromNode' + ); - component.removePrerequisiteSkillId('skill_3'); + component.removePrerequisiteSkillId('skill_3'); - expect(skillSpy).toHaveBeenCalled(); - }); + expect(skillSpy).toHaveBeenCalled(); + }); it('should call StoryUpdate service remove acquired skill id', () => { let skillSpy = spyOn(storyUpdateService, 'removeAcquiredSkillIdFromNode'); @@ -329,53 +378,57 @@ describe('Story node editor component', () => { expect(component.prerequisiteSkillIsShown).toBeFalse(); }); - it('should call StoryUpdate service to set story thumbnail filename', - () => { - let storySpy = spyOn(storyUpdateService, 'setStoryNodeThumbnailFilename'); - let currentNodeIsPublishableSpy = spyOn( - component, 'updateCurrentNodeIsPublishable'); + it('should call StoryUpdate service to set story thumbnail filename', () => { + let storySpy = spyOn(storyUpdateService, 'setStoryNodeThumbnailFilename'); + let currentNodeIsPublishableSpy = spyOn( + component, + 'updateCurrentNodeIsPublishable' + ); - component.updateThumbnailFilename('new_file.png'); + component.updateThumbnailFilename('new_file.png'); - expect(storySpy).toHaveBeenCalled(); - expect(currentNodeIsPublishableSpy).toHaveBeenCalled(); - }); + expect(storySpy).toHaveBeenCalled(); + expect(currentNodeIsPublishableSpy).toHaveBeenCalled(); + }); - it('should call StoryUpdate service to set story thumbnail filename', - () => { - let storySpy = spyOn(storyUpdateService, 'setStoryNodeThumbnailBgColor'); - let currentNodeIsPublishableSpy = spyOn( - component, 'updateCurrentNodeIsPublishable'); + it('should call StoryUpdate service to set story thumbnail filename', () => { + let storySpy = spyOn(storyUpdateService, 'setStoryNodeThumbnailBgColor'); + let currentNodeIsPublishableSpy = spyOn( + component, + 'updateCurrentNodeIsPublishable' + ); - component.updateThumbnailBgColor('#333'); + component.updateThumbnailBgColor('#333'); - expect(storySpy).toHaveBeenCalled(); - expect(currentNodeIsPublishableSpy).toHaveBeenCalled(); - }); + expect(storySpy).toHaveBeenCalled(); + expect(currentNodeIsPublishableSpy).toHaveBeenCalled(); + }); - it('should call StoryUpdate service to finalize story node outline', - () => { - let storySpy = spyOn(storyUpdateService, 'unfinalizeStoryNodeOutline'); - let currentNodeIsPublishableSpy = spyOn( - component, 'updateCurrentNodeIsPublishable'); + it('should call StoryUpdate service to finalize story node outline', () => { + let storySpy = spyOn(storyUpdateService, 'unfinalizeStoryNodeOutline'); + let currentNodeIsPublishableSpy = spyOn( + component, + 'updateCurrentNodeIsPublishable' + ); - component.unfinalizeOutline(); + component.unfinalizeOutline(); - expect(storySpy).toHaveBeenCalled(); - expect(currentNodeIsPublishableSpy).toHaveBeenCalled(); - }); + expect(storySpy).toHaveBeenCalled(); + expect(currentNodeIsPublishableSpy).toHaveBeenCalled(); + }); - it('should call StoryUpdate service to finalize story node outline', - () => { - let storySpy = spyOn(storyUpdateService, 'finalizeStoryNodeOutline'); - let currentNodeIsPublishableSpy = spyOn( - component, 'updateCurrentNodeIsPublishable'); + it('should call StoryUpdate service to finalize story node outline', () => { + let storySpy = spyOn(storyUpdateService, 'finalizeStoryNodeOutline'); + let currentNodeIsPublishableSpy = spyOn( + component, + 'updateCurrentNodeIsPublishable' + ); - component.finalizeOutline(); + component.finalizeOutline(); - expect(storySpy).toHaveBeenCalled(); - expect(currentNodeIsPublishableSpy).toHaveBeenCalled(); - }); + expect(storySpy).toHaveBeenCalled(); + expect(currentNodeIsPublishableSpy).toHaveBeenCalled(); + }); it('should call StoryUpdate service to update outline', () => { let storySpy = spyOn(storyUpdateService, 'setStoryNodeOutline'); @@ -388,7 +441,9 @@ describe('Story node editor component', () => { it('should call StoryUpdate service to update description', () => { let storySpy = spyOn(storyUpdateService, 'setStoryNodeDescription'); let currentNodeIsPublishableSpy = spyOn( - component, 'updateCurrentNodeIsPublishable'); + component, + 'updateCurrentNodeIsPublishable' + ); component.updateDescription('New description'); @@ -396,65 +451,78 @@ describe('Story node editor component', () => { expect(currentNodeIsPublishableSpy).toHaveBeenCalled(); }); - it('should call StoryUpdate service to update planned publication date', - () => { - let storySpy = spyOn( - storyUpdateService, 'setStoryNodePlannedPublicationDateMsecs'); - let currentNodeIsPublishableSpy = spyOn( - component, 'updateCurrentNodeIsPublishable'); - - component.plannedPublicationDate = null; - component.updatePlannedPublicationDate(null); - expect(currentNodeIsPublishableSpy).toHaveBeenCalled(); - expect(storyUpdateService.setStoryNodePlannedPublicationDateMsecs). - toHaveBeenCalledTimes(0); - - let oldDateString = '2000-09-09'; - component.updatePlannedPublicationDate(oldDateString); - expect(currentNodeIsPublishableSpy).toHaveBeenCalled(); - expect(storyUpdateService.setStoryNodePlannedPublicationDateMsecs). - toHaveBeenCalledTimes(0); - expect(component.plannedPublicationDate).toBe(null); - expect(component.plannedPublicationDateIsInPast).toBeTrue(); - - let futureDateString = '2037-04-20'; - component.updatePlannedPublicationDate(futureDateString); - let futureDate = new Date(futureDateString); - expect(storySpy).toHaveBeenCalledWith( - component.story, component.nodeId, futureDate.getTime()); - expect(currentNodeIsPublishableSpy).toHaveBeenCalled(); - expect(component.plannedPublicationDateIsInPast).toBeFalse(); - - component.updatePlannedPublicationDate(''); - expect(storySpy).toHaveBeenCalled(); - expect(currentNodeIsPublishableSpy).toHaveBeenCalled(); - expect(component.plannedPublicationDate).toBe(null); - expect(component.plannedPublicationDateIsInPast).toBeFalse(); + it('should call StoryUpdate service to update planned publication date', () => { + let storySpy = spyOn( + storyUpdateService, + 'setStoryNodePlannedPublicationDateMsecs' + ); + let currentNodeIsPublishableSpy = spyOn( + component, + 'updateCurrentNodeIsPublishable' + ); - component.plannedPublicationDate = new Date(); - component.updatePlannedPublicationDate(oldDateString); - expect(storySpy).toHaveBeenCalled(); - }); + component.plannedPublicationDate = null; + component.updatePlannedPublicationDate(null); + expect(currentNodeIsPublishableSpy).toHaveBeenCalled(); + expect( + storyUpdateService.setStoryNodePlannedPublicationDateMsecs + ).toHaveBeenCalledTimes(0); - it('should update check if current node can be changed to' + - 'Ready To Publish', () => { - let currentNodeIsPublishableSpy = spyOn( - storyEditorStateService, 'setCurrentNodeAsPublishable'); - component.updateCurrentNodeIsPublishable(); + let oldDateString = '2000-09-09'; + component.updatePlannedPublicationDate(oldDateString); + expect(currentNodeIsPublishableSpy).toHaveBeenCalled(); + expect( + storyUpdateService.setStoryNodePlannedPublicationDateMsecs + ).toHaveBeenCalledTimes(0); + expect(component.plannedPublicationDate).toBe(null); + expect(component.plannedPublicationDateIsInPast).toBeTrue(); + + let futureDateString = '2037-04-20'; + component.updatePlannedPublicationDate(futureDateString); + let futureDate = new Date(futureDateString); + expect(storySpy).toHaveBeenCalledWith( + component.story, + component.nodeId, + futureDate.getTime() + ); + expect(currentNodeIsPublishableSpy).toHaveBeenCalled(); + expect(component.plannedPublicationDateIsInPast).toBeFalse(); - expect(currentNodeIsPublishableSpy).toHaveBeenCalledWith(false); + component.updatePlannedPublicationDate(''); + expect(storySpy).toHaveBeenCalled(); + expect(currentNodeIsPublishableSpy).toHaveBeenCalled(); + expect(component.plannedPublicationDate).toBe(null); + expect(component.plannedPublicationDateIsInPast).toBeFalse(); - component.outlineIsFinalized = true; - component.editableThumbnailBgColor = '#fff'; - component.explorationId = 'exp_1'; - component.currentTitle = 'title'; - component.currentDescription = 'desc'; component.plannedPublicationDate = new Date(); - component.updateCurrentNodeIsPublishable(); - - expect(currentNodeIsPublishableSpy).toHaveBeenCalledWith(true); + component.updatePlannedPublicationDate(oldDateString); + expect(storySpy).toHaveBeenCalled(); }); + it( + 'should update check if current node can be changed to' + + 'Ready To Publish', + () => { + let currentNodeIsPublishableSpy = spyOn( + storyEditorStateService, + 'setCurrentNodeAsPublishable' + ); + component.updateCurrentNodeIsPublishable(); + + expect(currentNodeIsPublishableSpy).toHaveBeenCalledWith(false); + + component.outlineIsFinalized = true; + component.editableThumbnailBgColor = '#fff'; + component.explorationId = 'exp_1'; + component.currentTitle = 'title'; + component.currentDescription = 'desc'; + component.plannedPublicationDate = new Date(); + component.updateCurrentNodeIsPublishable(); + + expect(currentNodeIsPublishableSpy).toHaveBeenCalledWith(true); + } + ); + it('should open and close node title editor', () => { component.openNodeTitleEditor(); @@ -467,11 +535,10 @@ describe('Story node editor component', () => { it('should open add skill modal for adding prerequisite skill', () => { const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ( - { - componentInstance: MockNgbModalRef, - result: Promise.resolve('success') - }) as NgbModalRef; + return { + componentInstance: MockNgbModalRef, + result: Promise.resolve('success'), + } as NgbModalRef; }); component.addPrerequisiteSkillId(); @@ -479,75 +546,83 @@ describe('Story node editor component', () => { expect(modalSpy).toHaveBeenCalled(); }); - it('should show alert message when we try to ' + - 'add a prerequisite skill id which already exists', fakeAsync(() => { - spyOn(storyUpdateService, 'addPrerequisiteSkillIdToNode') - .and.callFake(() => { - throw new Error('Given skill is already a prerequisite skill'); + it( + 'should show alert message when we try to ' + + 'add a prerequisite skill id which already exists', + fakeAsync(() => { + spyOn(storyUpdateService, 'addPrerequisiteSkillIdToNode').and.callFake( + () => { + throw new Error('Given skill is already a prerequisite skill'); + } + ); + let alertsSpy = spyOn(alertsService, 'addInfoMessage').and.returnValue( + null + ); + spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + componentInstance: MockNgbModalRef, + result: Promise.resolve({ + summary: { + id: 'id', + description: 'description', + }, + }), + } as NgbModalRef; }); - let alertsSpy = spyOn(alertsService, 'addInfoMessage') - .and.returnValue(null); - spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ( - { - componentInstance: MockNgbModalRef, - result: Promise.resolve({ - summary: { - id: 'id', - description: 'description' - } - }) - } - ) as NgbModalRef; + + component.addPrerequisiteSkillId(); + tick(); + + expect(alertsSpy).toHaveBeenCalledWith( + 'Given skill is already a prerequisite skill', + 5000 + ); + }) + ); + + it('should open add skill modal for adding acquired skill', fakeAsync(() => { + spyOn(storyUpdateService, 'addAcquiredSkillIdToNode').and.callFake( + () => {} + ); + const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + componentInstance: MockNgbModalRef, + result: Promise.resolve('success'), + } as NgbModalRef; }); - component.addPrerequisiteSkillId(); + component.addAcquiredSkillId(); tick(); - expect(alertsSpy).toHaveBeenCalledWith( - 'Given skill is already a prerequisite skill', 5000); + expect(modalSpy).toHaveBeenCalled(); })); - it('should open add skill modal for adding acquired skill', fakeAsync( - () => { - spyOn(storyUpdateService, 'addAcquiredSkillIdToNode').and.callFake( - () => { }); - const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ( - { - componentInstance: MockNgbModalRef, - result: Promise.resolve('success') - }) as NgbModalRef; + it( + 'should show alert message when we try to ' + + 'add a acquired skill id which already exists', + fakeAsync(() => { + spyOn(storyUpdateService, 'addAcquiredSkillIdToNode').and.callFake(() => { + throw new Error('skill id already exist.'); + }); + let alertsSpy = spyOn(alertsService, 'addInfoMessage').and.returnValue( + null + ); + spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + componentInstance: MockNgbModalRef, + result: Promise.resolve('success'), + } as NgbModalRef; }); component.addAcquiredSkillId(); tick(); - expect(modalSpy).toHaveBeenCalled(); - })); - - it('should show alert message when we try to ' + - 'add a acquired skill id which already exists', fakeAsync(() => { - spyOn(storyUpdateService, 'addAcquiredSkillIdToNode') - .and.callFake(() => { - throw new Error('skill id already exist.'); - }); - let alertsSpy = spyOn(alertsService, 'addInfoMessage') - .and.returnValue(null); - spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ( - { - componentInstance: MockNgbModalRef, - result: Promise.resolve('success') - }) as NgbModalRef; - }); - - component.addAcquiredSkillId(); - tick(); - - expect(alertsSpy).toHaveBeenCalledWith( - 'Given skill is already an acquired skill', 5000); - })); + expect(alertsSpy).toHaveBeenCalledWith( + 'Given skill is already an acquired skill', + 5000 + ); + }) + ); it('should toggle chapter outline', fakeAsync(() => { component.chapterOutlineIsShown = false; @@ -638,108 +713,135 @@ describe('Story node editor component', () => { expect(component.editableOutline).toBe('value'); }); - it('should call StoryUpdateService and curatedExplorationValidationService' + - ' to set node exploration id if story is published', - fakeAsync(() => { - spyOn(storyEditorStateService, 'isStoryPublished').and.returnValue(true); - let value = Promise.resolve(true); - let expSpy = spyOn( - curatedExplorationValidationService, 'isExpPublishedAsync' - ).and.returnValue(value); - let storyUpdateSpy = spyOn(storyUpdateService, 'setStoryNodeExplorationId'); - - component.updateExplorationId('exp10'); - tick(); + it( + 'should call StoryUpdateService and curatedExplorationValidationService' + + ' to set node exploration id if story is published', + fakeAsync(() => { + spyOn(storyEditorStateService, 'isStoryPublished').and.returnValue(true); + let value = Promise.resolve(true); + let expSpy = spyOn( + curatedExplorationValidationService, + 'isExpPublishedAsync' + ).and.returnValue(value); + let storyUpdateSpy = spyOn( + storyUpdateService, + 'setStoryNodeExplorationId' + ); + + component.updateExplorationId('exp10'); + tick(); - expect(expSpy).toHaveBeenCalled(); - expect(storyUpdateSpy).toHaveBeenCalled(); - })); + expect(expSpy).toHaveBeenCalled(); + expect(storyUpdateSpy).toHaveBeenCalled(); + }) + ); - it('should call StoryUpdateService to set node exploration id and set ' + - 'invalid exp error if story is published and exp id is invalid', - fakeAsync(() => { - component.invalidExpErrorIsShown = false; - spyOn(storyEditorStateService, 'isStoryPublished').and.returnValue(true); - let value = Promise.resolve(false); - let expSpy = spyOn( - curatedExplorationValidationService, 'isExpPublishedAsync' - ).and.returnValue(value); - let storyUpdateSpy = spyOn( - storyUpdateService, 'setStoryNodeExplorationId'); - - component.updateExplorationId('exp10'); - tick(); + it( + 'should call StoryUpdateService to set node exploration id and set ' + + 'invalid exp error if story is published and exp id is invalid', + fakeAsync(() => { + component.invalidExpErrorIsShown = false; + spyOn(storyEditorStateService, 'isStoryPublished').and.returnValue(true); + let value = Promise.resolve(false); + let expSpy = spyOn( + curatedExplorationValidationService, + 'isExpPublishedAsync' + ).and.returnValue(value); + let storyUpdateSpy = spyOn( + storyUpdateService, + 'setStoryNodeExplorationId' + ); + + component.updateExplorationId('exp10'); + tick(); - expect(expSpy).toHaveBeenCalled(); - expect(storyUpdateSpy).not.toHaveBeenCalled(); - expect(component.invalidExpErrorIsShown).toEqual(true); - })); + expect(expSpy).toHaveBeenCalled(); + expect(storyUpdateSpy).not.toHaveBeenCalled(); + expect(component.invalidExpErrorIsShown).toEqual(true); + }) + ); it('should show error if story is published and exp id is null', () => { spyOn(storyEditorStateService, 'isStoryPublished').and.returnValue(true); let alertsSpy = spyOn(alertsService, 'addInfoMessage'); - let storyUpdateSpy = spyOn( - storyUpdateService, 'setStoryNodeExplorationId'); + let storyUpdateSpy = spyOn(storyUpdateService, 'setStoryNodeExplorationId'); component.updateExplorationId(null as unknown as string); expect(storyUpdateSpy).not.toHaveBeenCalled(); expect(alertsSpy).toHaveBeenCalled(); }); - it('should call StoryUpdateService to set node exploration id and set ' + - 'invalid exp error if story is published and exp id is invalid', - () => { - spyOn(storyEditorStateService, 'isStoryPublished').and.returnValue(false); - Promise.resolve(false); + it( + 'should call StoryUpdateService to set node exploration id and set ' + + 'invalid exp error if story is published and exp id is invalid', + () => { + spyOn(storyEditorStateService, 'isStoryPublished').and.returnValue(false); + Promise.resolve(false); - let storyUpdateSpy = spyOn( - storyUpdateService, 'setStoryNodeExplorationId'); + let storyUpdateSpy = spyOn( + storyUpdateService, + 'setStoryNodeExplorationId' + ); - component.updateExplorationId(null as unknown as string); - expect(storyUpdateSpy).toHaveBeenCalled(); - }); + component.updateExplorationId(null as unknown as string); + expect(storyUpdateSpy).toHaveBeenCalled(); + } + ); - it('should show alert message if we try to update ' + - 'exploration id with empty value', () => { - spyOn(storyEditorStateService, 'isStoryPublished').and.returnValue(false); - Promise.resolve(false); - spyOn(storyUpdateService, 'setStoryNodeExplorationId'); - let alertsSpy = spyOn(alertsService, 'addInfoMessage') - .and.returnValue(null); - - component.updateExplorationId(''); - expect(alertsSpy).toHaveBeenCalledWith( - 'Please click the delete icon to remove an exploration ' + - 'from the story.', 5000); - }); + it( + 'should show alert message if we try to update ' + + 'exploration id with empty value', + () => { + spyOn(storyEditorStateService, 'isStoryPublished').and.returnValue(false); + Promise.resolve(false); + spyOn(storyUpdateService, 'setStoryNodeExplorationId'); + let alertsSpy = spyOn(alertsService, 'addInfoMessage').and.returnValue( + null + ); + + component.updateExplorationId(''); + expect(alertsSpy).toHaveBeenCalledWith( + 'Please click the delete icon to remove an exploration ' + + 'from the story.', + 5000 + ); + } + ); it('should call StoryUpdate service to set story node title', () => { - let storyUpdateSpy = spyOn( - storyUpdateService, 'setStoryNodeTitle'); + let storyUpdateSpy = spyOn(storyUpdateService, 'setStoryNodeTitle'); let currentNodeIsPublishableSpy = spyOn( - component, 'updateCurrentNodeIsPublishable'); + component, + 'updateCurrentNodeIsPublishable' + ); component.updateTitle('Title 10'); expect(storyUpdateSpy).toHaveBeenCalled(); expect(currentNodeIsPublishableSpy).toHaveBeenCalled(); }); - it('should not call StoryUpdate service to set story node title and ' + - 'call alertsService if the name is a duplicate', () => { - let storyUpdateSpy = spyOn( - storyUpdateService, 'setStoryNodeTitle'); - let alertsSpy = spyOn(alertsService, 'addInfoMessage'); - component.updateTitle('Title 2'); - expect(storyUpdateSpy).not.toHaveBeenCalled(); - expect(alertsSpy).toHaveBeenCalled(); - }); + it( + 'should not call StoryUpdate service to set story node title and ' + + 'call alertsService if the name is a duplicate', + () => { + let storyUpdateSpy = spyOn(storyUpdateService, 'setStoryNodeTitle'); + let alertsSpy = spyOn(alertsService, 'addInfoMessage'); + component.updateTitle('Title 2'); + expect(storyUpdateSpy).not.toHaveBeenCalled(); + expect(alertsSpy).toHaveBeenCalled(); + } + ); it('should focus on story node when story is initialized', fakeAsync(() => { let mockEventEmitter = new EventEmitter(); - spyOnProperty(storyEditorStateService, 'onStoryInitialized') - .and.returnValue(mockEventEmitter); - let focusSpy = spyOn(focusManagerService, 'setFocusWithoutScroll') - .and.returnValue(null); + spyOnProperty( + storyEditorStateService, + 'onStoryInitialized' + ).and.returnValue(mockEventEmitter); + let focusSpy = spyOn( + focusManagerService, + 'setFocusWithoutScroll' + ).and.returnValue(null); component.ngOnInit(); flush(); @@ -748,52 +850,56 @@ describe('Story node editor component', () => { expect(focusSpy).toHaveBeenCalled(); })); - it('should focus on story node when story is reinitialized', fakeAsync( - () => { - let mockEventEmitter = new EventEmitter(); - spyOnProperty(storyEditorStateService, 'onStoryReinitialized') - .and.returnValue(mockEventEmitter); - let focusSpy = spyOn(focusManagerService, 'setFocusWithoutScroll') - .and.returnValue(null); + it('should focus on story node when story is reinitialized', fakeAsync(() => { + let mockEventEmitter = new EventEmitter(); + spyOnProperty( + storyEditorStateService, + 'onStoryReinitialized' + ).and.returnValue(mockEventEmitter); + let focusSpy = spyOn( + focusManagerService, + 'setFocusWithoutScroll' + ).and.returnValue(null); - component.ngOnInit(); - flush(); + component.ngOnInit(); + flush(); - mockEventEmitter.emit(); - tick(); + mockEventEmitter.emit(); + tick(); - expect(focusSpy).toHaveBeenCalled(); - })); + expect(focusSpy).toHaveBeenCalled(); + })); - it('should focus on story node after recalculation of available node', - fakeAsync(() => { - spyOn(component, '_recalculateAvailableNodes').and.callThrough(); - - component.nodeId = 'node1'; - component.storyNodeIds = ['node1', 'node_2', 'working', 'duty']; - component.destinationNodeIds = ['node_2']; - component.story = { - getStoryContents: () => { - return { - getLinearNodesList: () => { - return [{ + it('should focus on story node after recalculation of available node', fakeAsync(() => { + spyOn(component, '_recalculateAvailableNodes').and.callThrough(); + + component.nodeId = 'node1'; + component.storyNodeIds = ['node1', 'node_2', 'working', 'duty']; + component.destinationNodeIds = ['node_2']; + component.story = { + getStoryContents: () => { + return { + getLinearNodesList: () => { + return [ + { getId: () => { return 'NodeID_1'; - } + }, }, { getId: () => { return 'NodeID_2'; - } - }]; - } - }; - } - }; + }, + }, + ]; + }, + }; + }, + }; - mockEventEmitterLast.emit(); - tick(); + mockEventEmitterLast.emit(); + tick(); - expect(component._recalculateAvailableNodes).toHaveBeenCalled(); - })); + expect(component._recalculateAvailableNodes).toHaveBeenCalled(); + })); }); diff --git a/core/templates/pages/story-editor-page/editor-tab/story-node-editor.component.ts b/core/templates/pages/story-editor-page/editor-tab/story-node-editor.component.ts index fc766fe91dd2..4135f3257467 100644 --- a/core/templates/pages/story-editor-page/editor-tab/story-node-editor.component.ts +++ b/core/templates/pages/story-editor-page/editor-tab/story-node-editor.component.ts @@ -16,27 +16,33 @@ * @fileoverview Component for the story node editor. */ -import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { AppConstants } from 'app.constants'; -import { SelectSkillModalComponent } from 'components/skill-selector/select-skill-modal.component'; -import { CuratedExplorationValidationService } from 'domain/exploration/curated-exploration-validation.service'; -import { SkillBackendApiService } from 'domain/skill/skill-backend-api.service'; -import { StoryUpdateService } from 'domain/story/story-update.service'; -import { Story } from 'domain/story/story.model'; -import { TopicsAndSkillsDashboardBackendApiService } from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; -import { Subscription } from 'rxjs'; -import { AlertsService } from 'services/alerts.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { PageTitleService } from 'services/page-title.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { StoryEditorStateService } from '../services/story-editor-state.service'; -import { PlatformFeatureService } from 'services/platform-feature.service'; +import { + ChangeDetectorRef, + Component, + Input, + OnDestroy, + OnInit, +} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {AppConstants} from 'app.constants'; +import {SelectSkillModalComponent} from 'components/skill-selector/select-skill-modal.component'; +import {CuratedExplorationValidationService} from 'domain/exploration/curated-exploration-validation.service'; +import {SkillBackendApiService} from 'domain/skill/skill-backend-api.service'; +import {StoryUpdateService} from 'domain/story/story-update.service'; +import {Story} from 'domain/story/story.model'; +import {TopicsAndSkillsDashboardBackendApiService} from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; +import {Subscription} from 'rxjs'; +import {AlertsService} from 'services/alerts.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {PageTitleService} from 'services/page-title.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {StoryEditorStateService} from '../services/story-editor-state.service'; +import {PlatformFeatureService} from 'services/platform-feature.service'; @Component({ selector: 'oppia-story-node-editor', - templateUrl: './story-node-editor.component.html' + templateUrl: './story-node-editor.component.html', }) export class StoryNodeEditorComponent implements OnInit, OnDestroy { @Input() nodeId: string; @@ -89,8 +95,8 @@ export class StoryNodeEditorComponent implements OnInit, OnDestroy { type: 'html', ui_config: { startupFocusEnabled: false, - rows: 100 - } + rows: 100, + }, }; private _categorizedSkills = {}; @@ -102,8 +108,7 @@ export class StoryNodeEditorComponent implements OnInit, OnDestroy { constructor( private alertsService: AlertsService, - private curatedExplorationValidationService: - CuratedExplorationValidationService, + private curatedExplorationValidationService: CuratedExplorationValidationService, private changeDetectorRef: ChangeDetectorRef, private focusManagerService: FocusManagerService, private ngbModal: NgbModal, @@ -111,31 +116,31 @@ export class StoryNodeEditorComponent implements OnInit, OnDestroy { private skillBackendApiService: SkillBackendApiService, private storyEditorStateService: StoryEditorStateService, private storyUpdateService: StoryUpdateService, - private topicsAndSkillsDashboardBackendApiService: - TopicsAndSkillsDashboardBackendApiService, + private topicsAndSkillsDashboardBackendApiService: TopicsAndSkillsDashboardBackendApiService, private windowDimensionsService: WindowDimensionsService, private platformFeatureService: PlatformFeatureService ) {} isSerialChapterFeatureFlagEnabled(): boolean { - return ( - this.platformFeatureService. - status.SerialChapterLaunchCurriculumAdminView.isEnabled); + return this.platformFeatureService.status + .SerialChapterLaunchCurriculumAdminView.isEnabled; } private _init(): void { this.story = this.storyEditorStateService.getStory(); this.storyNodeIds = this.story.getStoryContents().getNodeIds(); - this.nodeIdToTitleMap = this.story.getStoryContents().getNodeIdsToTitleMap( - this.storyNodeIds); + this.nodeIdToTitleMap = this.story + .getStoryContents() + .getNodeIdsToTitleMap(this.storyNodeIds); this.skillInfoHasLoaded = false; this._recalculateAvailableNodes(); let skillSummaries = this.storyEditorStateService.getSkillSummaries(); - this.topicsAndSkillsDashboardBackendApiService.fetchDashboardDataAsync() - .then((response) => { + this.topicsAndSkillsDashboardBackendApiService + .fetchDashboardDataAsync() + .then(response => { this._categorizedSkills = response.categorizedSkillsDict; this._untriagedSkillSummaries = response.untriagedSkillSummaries; this.skillInfoHasLoaded = true; @@ -143,7 +148,7 @@ export class StoryNodeEditorComponent implements OnInit, OnDestroy { for (let idx in skillSummaries) { this.skillIdToSummaryMap[skillSummaries[idx].id] = - skillSummaries[idx].description; + skillSummaries[idx].description; } this.isStoryPublished = this.storyEditorStateService.isStoryPublished; @@ -160,8 +165,9 @@ export class StoryNodeEditorComponent implements OnInit, OnDestroy { this.expIdIsValid = true; this.invalidExpErrorIsShown = false; this.nodeTitleEditorIsShown = false; - this.plannedPublicationDate = this.plannedPublicationDateMsecs ? new Date( - this.plannedPublicationDateMsecs) : null; + this.plannedPublicationDate = this.plannedPublicationDateMsecs + ? new Date(this.plannedPublicationDateMsecs) + : null; this.editablePlannedPublicationDate = this.plannedPublicationDate; this.updateCurrentNodeIsPublishable(); @@ -176,20 +182,22 @@ export class StoryNodeEditorComponent implements OnInit, OnDestroy { if (skills && skills.length > 0) { this.skillBackendApiService.fetchMultiSkillsAsync(skills).then( - (response) => { + response => { for (let idx in response) { - this.skillIdToSummaryMap[response[idx].getId()] = ( - response[idx].getDescription()); + this.skillIdToSummaryMap[response[idx].getId()] = + response[idx].getDescription(); } - }, (error) => { + }, + error => { this.alertsService.addWarning(''); - }); + } + ); } } checkCanSaveExpId(): void { - this.expIdCanBeSaved = this.explorationIdPattern.test( - this.explorationId) || !this.explorationId; + this.expIdCanBeSaved = + this.explorationIdPattern.test(this.explorationId) || !this.explorationId; this.invalidExpErrorIsShown = false; } @@ -202,14 +210,18 @@ export class StoryNodeEditorComponent implements OnInit, OnDestroy { if (node.getTitle() === newTitle) { titleIsValid = false; this.alertsService.addInfoMessage( - 'A chapter already exists with given title.', 5000 + 'A chapter already exists with given title.', + 5000 ); } } if (titleIsValid) { this.storyUpdateService.setStoryNodeTitle( - this.story, this.nodeId, newTitle); + this.story, + this.nodeId, + newTitle + ); this.currentTitle = newTitle; this.updateCurrentNodeIsPublishable(); } @@ -219,7 +231,10 @@ export class StoryNodeEditorComponent implements OnInit, OnDestroy { updateDescription(newDescription: string): void { if (newDescription !== this.currentDescription) { this.storyUpdateService.setStoryNodeDescription( - this.story, this.nodeId, newDescription); + this.story, + this.nodeId, + newDescription + ); this.currentDescription = newDescription; this.updateCurrentNodeIsPublishable(); } @@ -235,22 +250,33 @@ export class StoryNodeEditorComponent implements OnInit, OnDestroy { this.plannedPublicationDateIsInPast = true; if (this.plannedPublicationDate) { this.storyUpdateService.setStoryNodePlannedPublicationDateMsecs( - this.story, this.nodeId, null); + this.story, + this.nodeId, + null + ); } this.plannedPublicationDate = null; this.editablePlannedPublicationDate = null; return; - } else if (this.plannedPublicationDate === null || + } else if ( + this.plannedPublicationDate === null || this.plannedPublicationDate.getTime() !== - newPlannedPublicationDate.getTime()) { + newPlannedPublicationDate.getTime() + ) { this.storyUpdateService.setStoryNodePlannedPublicationDateMsecs( - this.story, this.nodeId, newPlannedPublicationDate.getTime()); + this.story, + this.nodeId, + newPlannedPublicationDate.getTime() + ); this.plannedPublicationDate = newPlannedPublicationDate; this.plannedPublicationDateIsInPast = false; } } else { this.storyUpdateService.setStoryNodePlannedPublicationDateMsecs( - this.story, this.nodeId, null); + this.story, + this.nodeId, + null + ); this.plannedPublicationDate = null; this.plannedPublicationDateIsInPast = false; } @@ -262,7 +288,10 @@ export class StoryNodeEditorComponent implements OnInit, OnDestroy { updateThumbnailFilename(newThumbnailFilename: string): void { if (newThumbnailFilename !== this.editableThumbnailFilename) { this.storyUpdateService.setStoryNodeThumbnailFilename( - this.story, this.nodeId, newThumbnailFilename); + this.story, + this.nodeId, + newThumbnailFilename + ); this.updateCurrentNodeIsPublishable(); } } @@ -270,16 +299,24 @@ export class StoryNodeEditorComponent implements OnInit, OnDestroy { updateThumbnailBgColor(newThumbnailBgColor: string): void { if (newThumbnailBgColor !== this.editableThumbnailBgColor) { this.storyUpdateService.setStoryNodeThumbnailBgColor( - this.story, this.nodeId, newThumbnailBgColor); + this.story, + this.nodeId, + newThumbnailBgColor + ); this.editableThumbnailBgColor = newThumbnailBgColor; this.updateCurrentNodeIsPublishable(); } } updateCurrentNodeIsPublishable(): void { - if (this.currentTitle && this.currentDescription && this.explorationId && + if ( + this.currentTitle && + this.currentDescription && + this.explorationId && (this.editableThumbnailBgColor || this.editableThumbnailFilename) && - this.outlineIsFinalized && this.plannedPublicationDate) { + this.outlineIsFinalized && + this.plannedPublicationDate + ) { this.storyEditorStateService.setCurrentNodeAsPublishable(true); } else { this.storyEditorStateService.setCurrentNodeAsPublishable(false); @@ -302,18 +339,23 @@ export class StoryNodeEditorComponent implements OnInit, OnDestroy { if (this.storyEditorStateService.isStoryPublished()) { if (explorationId === '' || explorationId === null) { this.alertsService.addInfoMessage( - 'You cannot remove an exploration from a published story.', 5000); + 'You cannot remove an exploration from a published story.', + 5000 + ); return; } - this.curatedExplorationValidationService.isExpPublishedAsync( - explorationId) - .then((expIdIsValid) => { + this.curatedExplorationValidationService + .isExpPublishedAsync(explorationId) + .then(expIdIsValid => { this.expIdIsValid = expIdIsValid; if (this.expIdIsValid) { this.storyUpdateService.setStoryNodeExplorationId( - this.story, this.nodeId, explorationId); + this.story, + this.nodeId, + explorationId + ); this.currentExplorationId = explorationId; } else { this.invalidExpErrorIsShown = true; @@ -323,12 +365,17 @@ export class StoryNodeEditorComponent implements OnInit, OnDestroy { if (explorationId === '') { this.alertsService.addInfoMessage( 'Please click the delete icon to remove an exploration ' + - 'from the story.', 5000); + 'from the story.', + 5000 + ); return; } this.storyUpdateService.setStoryNodeExplorationId( - this.story, this.nodeId, explorationId); + this.story, + this.nodeId, + explorationId + ); this.currentExplorationId = explorationId; if (explorationId === null) { this.explorationId = null; @@ -338,94 +385,107 @@ export class StoryNodeEditorComponent implements OnInit, OnDestroy { removePrerequisiteSkillId(skillId: string): void { this.storyUpdateService.removePrerequisiteSkillIdFromNode( - this.story, this.nodeId, skillId); + this.story, + this.nodeId, + skillId + ); } addPrerequisiteSkillId(): void { let sortedSkillSummaries = this.storyEditorStateService.getSkillSummaries(); let allowSkillsFromOtherTopics = true; let skillsInSameTopicCount = 0; - let modalRef: NgbModalRef = this.ngbModal.open( - SelectSkillModalComponent, { - backdrop: 'static', - windowClass: 'skill-select-modal', - size: 'xl' - } - ); + let modalRef: NgbModalRef = this.ngbModal.open(SelectSkillModalComponent, { + backdrop: 'static', + windowClass: 'skill-select-modal', + size: 'xl', + }); modalRef.componentInstance.skillSummaries = sortedSkillSummaries; - modalRef.componentInstance.skillsInSameTopicCount = ( - skillsInSameTopicCount); + modalRef.componentInstance.skillsInSameTopicCount = skillsInSameTopicCount; modalRef.componentInstance.categorizedSkills = this._categorizedSkills; - modalRef.componentInstance.allowSkillsFromOtherTopics = ( - allowSkillsFromOtherTopics); - modalRef.componentInstance.untriagedSkillSummaries = ( - this._untriagedSkillSummaries); - - modalRef.result.then((summary) => { - try { - this.skillIdToSummaryMap[summary.id] = summary.description; - this.storyUpdateService.addPrerequisiteSkillIdToNode( - this.story, this.nodeId, summary.id); - // The catch parameter type can only be any or unknown. The type - // 'unknown' is safer than type 'any' because it reminds us - // that we need to performsome sorts of type-checks before - // operating on our values. - } catch (err: unknown) { - if (err instanceof Error) { - this.alertsService.addInfoMessage( - err.message, 5000); + modalRef.componentInstance.allowSkillsFromOtherTopics = + allowSkillsFromOtherTopics; + modalRef.componentInstance.untriagedSkillSummaries = + this._untriagedSkillSummaries; + + modalRef.result.then( + summary => { + try { + this.skillIdToSummaryMap[summary.id] = summary.description; + this.storyUpdateService.addPrerequisiteSkillIdToNode( + this.story, + this.nodeId, + summary.id + ); + // The catch parameter type can only be any or unknown. The type + // 'unknown' is safer than type 'any' because it reminds us + // that we need to performsome sorts of type-checks before + // operating on our values. + } catch (err: unknown) { + if (err instanceof Error) { + this.alertsService.addInfoMessage(err.message, 5000); + } } + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. } - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + ); } addAcquiredSkillId(): void { - let sortedSkillSummaries = ( - this.storyEditorStateService.getSkillSummaries()); + let sortedSkillSummaries = this.storyEditorStateService.getSkillSummaries(); let allowSkillsFromOtherTopics = false; let skillsInSameTopicCount = 0; let topicName = this.storyEditorStateService.getTopicName(); let categorizedSkillsInTopic = {}; categorizedSkillsInTopic[topicName] = this._categorizedSkills[topicName]; - let modalRef: NgbModalRef = this.ngbModal.open( - SelectSkillModalComponent, { - backdrop: 'static', - windowClass: 'skill-select-modal', - size: 'xl' - }); + let modalRef: NgbModalRef = this.ngbModal.open(SelectSkillModalComponent, { + backdrop: 'static', + windowClass: 'skill-select-modal', + size: 'xl', + }); modalRef.componentInstance.skillSummaries = sortedSkillSummaries; - modalRef.componentInstance.skillsInSameTopicCount = ( - skillsInSameTopicCount); + modalRef.componentInstance.skillsInSameTopicCount = skillsInSameTopicCount; modalRef.componentInstance.categorizedSkills = this._categorizedSkills; - modalRef.componentInstance.allowSkillsFromOtherTopics = ( - allowSkillsFromOtherTopics); - modalRef.componentInstance.untriagedSkillSummaries = ( - this._untriagedSkillSummaries); - - modalRef.result.then((summary) => { - try { - this.storyUpdateService.addAcquiredSkillIdToNode( - this.story, this.nodeId, summary.id); - } catch (err) { - this.alertsService.addInfoMessage( - 'Given skill is already an acquired skill', 5000); + modalRef.componentInstance.allowSkillsFromOtherTopics = + allowSkillsFromOtherTopics; + modalRef.componentInstance.untriagedSkillSummaries = + this._untriagedSkillSummaries; + + modalRef.result.then( + summary => { + try { + this.storyUpdateService.addAcquiredSkillIdToNode( + this.story, + this.nodeId, + summary.id + ); + } catch (err) { + this.alertsService.addInfoMessage( + 'Given skill is already an acquired skill', + 5000 + ); + } + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. } - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + ); } removeAcquiredSkillId(skillId: string): void { this.storyUpdateService.removeAcquiredSkillIdFromNode( - this.story, this.nodeId, skillId); + this.story, + this.nodeId, + skillId + ); } unfinalizeOutline(): void { @@ -449,7 +509,10 @@ export class StoryNodeEditorComponent implements OnInit, OnDestroy { updateOutline(newOutline: string): void { if (this.isOutlineModified(newOutline)) { this.storyUpdateService.setStoryNodeOutline( - this.story, this.nodeId, newOutline); + this.story, + this.nodeId, + newOutline + ); this.oldOutline = newOutline; } } @@ -489,8 +552,8 @@ export class StoryNodeEditorComponent implements OnInit, OnDestroy { } toggleExplorationInputButtons(): void { - this.explorationInputButtonsAreShown = ( - !this.explorationInputButtonsAreShown); + this.explorationInputButtonsAreShown = + !this.explorationInputButtonsAreShown; } updateLocalEditableOutline($event: string): void { @@ -512,7 +575,7 @@ export class StoryNodeEditorComponent implements OnInit, OnDestroy { this.availableNodes = []; let linearNodesList = this.story.getStoryContents().getLinearNodesList(); - let linearNodeIds = linearNodesList.map((node) => node.getId()); + let linearNodeIds = linearNodesList.map(node => node.getId()); for (let i = 0; i < this.storyNodeIds.length; i++) { if (this.storyNodeIds[i] === this.nodeId) { @@ -526,7 +589,7 @@ export class StoryNodeEditorComponent implements OnInit, OnDestroy { if (linearNodeIds.indexOf(this.storyNodeIds[i]) === -1) { this.availableNodes.push({ id: this.storyNodeIds[i], - text: this.nodeIdToTitleMap[this.storyNodeIds[i]] + text: this.nodeIdToTitleMap[this.storyNodeIds[i]], }); } } @@ -535,26 +598,27 @@ export class StoryNodeEditorComponent implements OnInit, OnDestroy { ngOnInit(): void { this.pageTitleService.setNavbarTitleForMobileView('Chapter Editor'); this.chapterOutlineIsShown = !this.windowDimensionsService.isWindowNarrow(); - this.chapterTodoCardIsShown = ( - !this.windowDimensionsService.isWindowNarrow()); - this.prerequisiteSkillIsShown = ( - !this.windowDimensionsService.isWindowNarrow()); - this.acquiredSkillIsShown = ( - !this.windowDimensionsService.isWindowNarrow()); + this.chapterTodoCardIsShown = + !this.windowDimensionsService.isWindowNarrow(); + this.prerequisiteSkillIsShown = + !this.windowDimensionsService.isWindowNarrow(); + this.acquiredSkillIsShown = !this.windowDimensionsService.isWindowNarrow(); this.subscriptions.add( - this.storyEditorStateService.onStoryInitialized.subscribe( - () => this._init()) + this.storyEditorStateService.onStoryInitialized.subscribe(() => + this._init() + ) ); this.subscriptions.add( - this.storyEditorStateService.onStoryReinitialized.subscribe( - () => this._init()) + this.storyEditorStateService.onStoryReinitialized.subscribe(() => + this._init() + ) ); this.subscriptions.add( - this.storyEditorStateService.onRecalculateAvailableNodes.subscribe( - () => this._recalculateAvailableNodes() + this.storyEditorStateService.onRecalculateAvailableNodes.subscribe(() => + this._recalculateAvailableNodes() ) ); @@ -573,6 +637,9 @@ export class StoryNodeEditorComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive('oppiaStoryNodeEditor', downgradeComponent({ - component: StoryNodeEditorComponent -})); +angular.module('oppia').directive( + 'oppiaStoryNodeEditor', + downgradeComponent({ + component: StoryNodeEditorComponent, + }) +); diff --git a/core/templates/pages/story-editor-page/modal-templates/delete-chapter-modal.component.spec.ts b/core/templates/pages/story-editor-page/modal-templates/delete-chapter-modal.component.spec.ts index 547cb497a135..7d208472cdc8 100644 --- a/core/templates/pages/story-editor-page/modal-templates/delete-chapter-modal.component.spec.ts +++ b/core/templates/pages/story-editor-page/modal-templates/delete-chapter-modal.component.spec.ts @@ -16,9 +16,9 @@ * @fileoverview Unit tests for Delete Chapter Modal. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { DeleteChapterModalComponent } from './delete-chapter-modal.component'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {DeleteChapterModalComponent} from './delete-chapter-modal.component'; describe('Delete Topic Modal Component', () => { let fixture: ComponentFixture; @@ -26,12 +26,8 @@ describe('Delete Topic Modal Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - DeleteChapterModalComponent - ], - providers: [ - NgbActiveModal - ] + declarations: [DeleteChapterModalComponent], + providers: [NgbActiveModal], }).compileComponents(); })); diff --git a/core/templates/pages/story-editor-page/modal-templates/delete-chapter-modal.component.ts b/core/templates/pages/story-editor-page/modal-templates/delete-chapter-modal.component.ts index 267451f84b22..4586b165e77b 100644 --- a/core/templates/pages/story-editor-page/modal-templates/delete-chapter-modal.component.ts +++ b/core/templates/pages/story-editor-page/modal-templates/delete-chapter-modal.component.ts @@ -16,24 +16,24 @@ * @fileoverview Component for DeleteChapterModal modal. */ -import { Component } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-delete-chapter-modal', - templateUrl: './delete-chapter-modal.component.html' + templateUrl: './delete-chapter-modal.component.html', }) export class DeleteChapterModalComponent extends ConfirmOrCancelModal { - constructor( - private ngbActiveModal: NgbActiveModal - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } } -angular.module('oppia').directive('oppiaDeleteChapterModal', +angular.module('oppia').directive( + 'oppiaDeleteChapterModal', downgradeComponent({ - component: DeleteChapterModalComponent - }) as angular.IDirectiveFactory); + component: DeleteChapterModalComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/story-editor-page/modal-templates/draft-chapter-confirmation-modal.component.spec.ts b/core/templates/pages/story-editor-page/modal-templates/draft-chapter-confirmation-modal.component.spec.ts index c6ede7298cfc..4025462ad3e1 100644 --- a/core/templates/pages/story-editor-page/modal-templates/draft-chapter-confirmation-modal.component.spec.ts +++ b/core/templates/pages/story-editor-page/modal-templates/draft-chapter-confirmation-modal.component.spec.ts @@ -16,10 +16,9 @@ * @fileoverview Unit tests for draft chapter confirmation modal. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { DraftChapterConfirmationModalComponent } from './draft-chapter-confirmation-modal.component'; - +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {DraftChapterConfirmationModalComponent} from './draft-chapter-confirmation-modal.component'; class MockActiveModal { dismiss(): void { @@ -42,15 +41,14 @@ describe('Draft Chapter Confirmation Modal Component', () => { providers: [ { provide: NgbActiveModal, - useClass: MockActiveModal - } - ] + useClass: MockActiveModal, + }, + ], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent( - DraftChapterConfirmationModalComponent); + fixture = TestBed.createComponent(DraftChapterConfirmationModalComponent); component = fixture.componentInstance; ngbActiveModal = TestBed.inject(NgbActiveModal); }); diff --git a/core/templates/pages/story-editor-page/modal-templates/draft-chapter-confirmation-modal.component.ts b/core/templates/pages/story-editor-page/modal-templates/draft-chapter-confirmation-modal.component.ts index 1f40e3b75f50..e74725d0ead0 100644 --- a/core/templates/pages/story-editor-page/modal-templates/draft-chapter-confirmation-modal.component.ts +++ b/core/templates/pages/story-editor-page/modal-templates/draft-chapter-confirmation-modal.component.ts @@ -16,17 +16,15 @@ * @fileoverview Component for the Draft Chapter Confirmation Modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; @Component({ selector: 'oppia-story-editor-draft-confirmation-modal', - templateUrl: './draft-chapter-confirmation-modal.component.html' + templateUrl: './draft-chapter-confirmation-modal.component.html', }) export class DraftChapterConfirmationModalComponent { - constructor( - private activeModal: NgbActiveModal - ) {} + constructor(private activeModal: NgbActiveModal) {} cancel(): void { this.activeModal.dismiss(); diff --git a/core/templates/pages/story-editor-page/modal-templates/new-chapter-title-modal.component.ts b/core/templates/pages/story-editor-page/modal-templates/new-chapter-title-modal.component.ts index 1099f3611756..8fa70c873fd2 100644 --- a/core/templates/pages/story-editor-page/modal-templates/new-chapter-title-modal.component.ts +++ b/core/templates/pages/story-editor-page/modal-templates/new-chapter-title-modal.component.ts @@ -16,22 +16,22 @@ * @fileoverview Component for new chapter title modal. */ -import { Component, Input, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { AppConstants } from 'app.constants'; +import {Component, Input, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {AppConstants} from 'app.constants'; import newChapterConstants from 'assets/constants'; -import { CuratedExplorationValidationService } from 'domain/exploration/curated-exploration-validation.service'; -import { EditableStoryBackendApiService } from 'domain/story/editable-story-backend-api.service'; -import { StoryUpdateService } from 'domain/story/story-update.service'; -import { Story } from 'domain/story/story.model'; -import { ValidatorsService } from 'services/validators.service'; -import { StoryEditorStateService } from '../services/story-editor-state.service'; -import { PlatformFeatureService } from 'services/platform-feature.service'; +import {CuratedExplorationValidationService} from 'domain/exploration/curated-exploration-validation.service'; +import {EditableStoryBackendApiService} from 'domain/story/editable-story-backend-api.service'; +import {StoryUpdateService} from 'domain/story/story-update.service'; +import {Story} from 'domain/story/story.model'; +import {ValidatorsService} from 'services/validators.service'; +import {StoryEditorStateService} from '../services/story-editor-state.service'; +import {PlatformFeatureService} from 'services/platform-feature.service'; @Component({ selector: 'oppia-new-chapter-title-modal', - templateUrl: './new-chapter-title-modal.component.html' + templateUrl: './new-chapter-title-modal.component.html', }) export class NewChapterTitleModalComponent implements OnInit { @Input() nodeTitles!: string | string[]; @@ -48,12 +48,10 @@ export class NewChapterTitleModalComponent implements OnInit { categoryIsDefault!: boolean; statesWithRestrictedInteractions!: string | string[]; statesWithTooFewMultipleChoiceOptions!: string | string[]; - allowedBgColors = ( - newChapterConstants.ALLOWED_THUMBNAIL_BG_COLORS.chapter); + allowedBgColors = newChapterConstants.ALLOWED_THUMBNAIL_BG_COLORS.chapter; constructor( - private curatedExplorationValidationService: - CuratedExplorationValidationService, + private curatedExplorationValidationService: CuratedExplorationValidationService, private editableStoryBackendApiService: EditableStoryBackendApiService, private ngbActiveModal: NgbActiveModal, private storyEditorStateService: StoryEditorStateService, @@ -69,15 +67,29 @@ export class NewChapterTitleModalComponent implements OnInit { addStoryNodeWithData(): void { this.storyUpdateService.addStoryNode(this.story, this.title); this.storyUpdateService.setStoryNodeTitle( - this.story, this.nodeId, this.title); + this.story, + this.nodeId, + this.title + ); this.storyUpdateService.setStoryNodeThumbnailFilename( - this.story, this.nodeId, this.editableThumbnailFilename); + this.story, + this.nodeId, + this.editableThumbnailFilename + ); this.storyUpdateService.setStoryNodeThumbnailBgColor( - this.story, this.nodeId, this.editableThumbnailBgColor); - if (this.platformFeatureService.status. - SerialChapterLaunchCurriculumAdminView.isEnabled) { + this.story, + this.nodeId, + this.editableThumbnailBgColor + ); + if ( + this.platformFeatureService.status.SerialChapterLaunchCurriculumAdminView + .isEnabled + ) { this.storyUpdateService.setStoryNodeStatus( - this.story, this.nodeId, 'Draft'); + this.story, + this.nodeId, + 'Draft' + ); } } @@ -113,13 +125,17 @@ export class NewChapterTitleModalComponent implements OnInit { for (var i = 0; i < nodes.length; i++) { if (nodes[i].getExplorationId() === this.explorationId) { this.invalidExpErrorStrings = [ - 'The given exploration already exists in the story.']; + 'The given exploration already exists in the story.', + ]; this.invalidExpId = true; return; } } this.storyUpdateService.setStoryNodeExplorationId( - this.story, this.nodeId, this.explorationId); + this.story, + this.nodeId, + this.explorationId + ); this.ngbActiveModal.close(); } @@ -133,14 +149,20 @@ export class NewChapterTitleModalComponent implements OnInit { validateExplorationId(): boolean { return this.validatorsService.isValidExplorationId( - this.explorationId, false); + this.explorationId, + false + ); } isValid(): boolean { return Boolean( this.title && - this.validatorsService.isValidExplorationId(this.explorationId, false) && - this.editableThumbnailFilename); + this.validatorsService.isValidExplorationId( + this.explorationId, + false + ) && + this.editableThumbnailFilename + ); } async saveAsync(): Promise { @@ -149,12 +171,13 @@ export class NewChapterTitleModalComponent implements OnInit { return; } - const expIsPublished = ( + const expIsPublished = await this.curatedExplorationValidationService.isExpPublishedAsync( - this.explorationId)); + this.explorationId + ); if (!expIsPublished) { this.invalidExpErrorStrings = [ - 'This exploration does not exist or is not published yet.' + 'This exploration does not exist or is not published yet.', ]; this.invalidExpId = true; return; @@ -162,33 +185,37 @@ export class NewChapterTitleModalComponent implements OnInit { this.invalidExpId = false; - const categoryIsDefault = ( + const categoryIsDefault = await this.curatedExplorationValidationService.isDefaultCategoryAsync( - this.explorationId)); + this.explorationId + ); if (!categoryIsDefault) { this.categoryIsDefault = false; return; } this.categoryIsDefault = true; - this.statesWithRestrictedInteractions = ( - await this.curatedExplorationValidationService - .getStatesWithRestrictedInteractions(this.explorationId)); + this.statesWithRestrictedInteractions = + await this.curatedExplorationValidationService.getStatesWithRestrictedInteractions( + this.explorationId + ); if (this.statesWithRestrictedInteractions.length > 0) { return; } - this.statesWithTooFewMultipleChoiceOptions = ( - await this.curatedExplorationValidationService - .getStatesWithInvalidMultipleChoices(this.explorationId)); + this.statesWithTooFewMultipleChoiceOptions = + await this.curatedExplorationValidationService.getStatesWithInvalidMultipleChoices( + this.explorationId + ); if (this.statesWithTooFewMultipleChoiceOptions.length > 0) { return; } - const validationErrorMessages = ( + const validationErrorMessages = await this.editableStoryBackendApiService.validateExplorationsAsync( - this.story.getId(), [this.explorationId] - )); + this.story.getId(), + [this.explorationId] + ); if (validationErrorMessages.length > 0) { this.invalidExpId = true; this.invalidExpErrorStrings = validationErrorMessages; @@ -201,7 +228,9 @@ export class NewChapterTitleModalComponent implements OnInit { } } -angular.module('oppia').directive('oppiaNewChapterTitleModal', +angular.module('oppia').directive( + 'oppiaNewChapterTitleModal', downgradeComponent({ - component: NewChapterTitleModalComponent - }) as angular.IDirectiveFactory); + component: NewChapterTitleModalComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/story-editor-page/modal-templates/new-chapter-title-modal.controller.spec.ts b/core/templates/pages/story-editor-page/modal-templates/new-chapter-title-modal.controller.spec.ts index 3071ad439344..d1b9b35e8fd8 100644 --- a/core/templates/pages/story-editor-page/modal-templates/new-chapter-title-modal.controller.spec.ts +++ b/core/templates/pages/story-editor-page/modal-templates/new-chapter-title-modal.controller.spec.ts @@ -16,17 +16,22 @@ * @fileoverview Unit tests for CreateNewChapterModalController. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, fakeAsync, flushMicrotasks, TestBed } from '@angular/core/testing'; -import { Story } from 'domain/story/story.model'; -import { CuratedExplorationValidationService } from 'domain/exploration/curated-exploration-validation.service'; -import { NewChapterTitleModalComponent } from './new-chapter-title-modal.component'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { EditableStoryBackendApiService } from '../../../domain/story/editable-story-backend-api.service'; -import { StoryEditorStateService } from '../services/story-editor-state.service'; -import { StoryUpdateService } from '../../../domain/story/story-update.service'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { PlatformFeatureService } from '../../../services/platform-feature.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import { + ComponentFixture, + fakeAsync, + flushMicrotasks, + TestBed, +} from '@angular/core/testing'; +import {Story} from 'domain/story/story.model'; +import {CuratedExplorationValidationService} from 'domain/exploration/curated-exploration-validation.service'; +import {NewChapterTitleModalComponent} from './new-chapter-title-modal.component'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {EditableStoryBackendApiService} from '../../../domain/story/editable-story-backend-api.service'; +import {StoryEditorStateService} from '../services/story-editor-state.service'; +import {StoryUpdateService} from '../../../domain/story/story-update.service'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {PlatformFeatureService} from '../../../services/platform-feature.service'; class MockActiveModal { close(): void { @@ -41,8 +46,8 @@ class MockActiveModal { class MockPlatformFeatureService { status = { SerialChapterLaunchCurriculumAdminView: { - isEnabled: false - } + isEnabled: false, + }, }; } @@ -54,15 +59,12 @@ describe('Create New Chapter Modal Component', () => { let curatedExplorationValidationService; let nodeTitles = ['title 1', 'title 2', 'title 3']; let mockPlatformFeatureService = new MockPlatformFeatureService(); - let editableStoryBackendApiService: - EditableStoryBackendApiService; + let editableStoryBackendApiService: EditableStoryBackendApiService; beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - NewChapterTitleModalComponent - ], + declarations: [NewChapterTitleModalComponent], providers: [ StoryEditorStateService, StoryUpdateService, @@ -70,28 +72,31 @@ describe('Create New Chapter Modal Component', () => { EditableStoryBackendApiService, { provide: PlatformFeatureService, - useValue: mockPlatformFeatureService + useValue: mockPlatformFeatureService, }, { provide: NgbActiveModal, - useClass: MockActiveModal - } + useClass: MockActiveModal, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }); }); beforeEach(() => { fixture = TestBed.createComponent(NewChapterTitleModalComponent); component = fixture.componentInstance; - curatedExplorationValidationService = ( - TestBed.inject(CuratedExplorationValidationService)); - editableStoryBackendApiService = ( - TestBed.inject(EditableStoryBackendApiService)); + curatedExplorationValidationService = TestBed.inject( + CuratedExplorationValidationService + ); + editableStoryBackendApiService = TestBed.inject( + EditableStoryBackendApiService + ); storyUpdateService = TestBed.inject(StoryUpdateService); storyEditorStateService = TestBed.inject(StoryEditorStateService); curatedExplorationValidationService = TestBed.inject( - CuratedExplorationValidationService); + CuratedExplorationValidationService + ); component.nodeTitles = nodeTitles; let sampleStoryBackendObject = { @@ -113,8 +118,9 @@ describe('Create New Chapter Modal Component', () => { destination_node_ids: [], outline: 'Outline', exploration_id: null, - outline_is_finalized: false - }, { + outline_is_finalized: false, + }, + { id: 'node_2', title: 'Title 2', description: 'Description 2', @@ -123,14 +129,14 @@ describe('Create New Chapter Modal Component', () => { destination_node_ids: ['node_1'], outline: 'Outline 2', exploration_id: 'exp_1', - outline_is_finalized: true - }], - next_node_id: 'node_3' + outline_is_finalized: true, + }, + ], + next_node_id: 'node_3', }, - language_code: 'en' + language_code: 'en', }; - let story = Story.createFromBackendDict( - sampleStoryBackendObject); + let story = Story.createFromBackendDict(sampleStoryBackendObject); spyOn(storyEditorStateService, 'getStory').and.returnValue(story); component.ngOnInit(); @@ -138,13 +144,16 @@ describe('Create New Chapter Modal Component', () => { it('should add story node with data', () => { let storyUpdateSpyThumbnailBgColor = spyOn( - storyUpdateService, 'setStoryNodeThumbnailBgColor'); + storyUpdateService, + 'setStoryNodeThumbnailBgColor' + ); let storyUpdateSpyThumbnailFilename = spyOn( - storyUpdateService, 'setStoryNodeThumbnailFilename'); - let storyUpdateSpyStatus = spyOn( - storyUpdateService, 'setStoryNodeStatus'); - mockPlatformFeatureService. - status.SerialChapterLaunchCurriculumAdminView.isEnabled = true; + storyUpdateService, + 'setStoryNodeThumbnailFilename' + ); + let storyUpdateSpyStatus = spyOn(storyUpdateService, 'setStoryNodeStatus'); + mockPlatformFeatureService.status.SerialChapterLaunchCurriculumAdminView.isEnabled = + true; component.addStoryNodeWithData(); @@ -153,221 +162,275 @@ describe('Create New Chapter Modal Component', () => { expect(storyUpdateSpyStatus).toHaveBeenCalled(); }); - it('should initialize component properties after controller is initialized', - () => { - expect(component.nodeTitles).toEqual(nodeTitles); - expect(component.errorMsg).toBe(null); - expect(component.categoryIsDefault).toBe(true); - }); + it('should initialize component properties after controller is initialized', () => { + expect(component.nodeTitles).toEqual(nodeTitles); + expect(component.errorMsg).toBe(null); + expect(component.categoryIsDefault).toBe(true); + }); - it('should validate explorationId correctly', - () => { - component.explorationId = 'validId'; - expect(component.validateExplorationId()).toBeTrue(); - component.explorationId = 'oppia.org/validId'; - expect(component.validateExplorationId()).toBeFalse(); - }); + it('should validate explorationId correctly', () => { + component.explorationId = 'validId'; + expect(component.validateExplorationId()).toBeTrue(); + component.explorationId = 'oppia.org/validId'; + expect(component.validateExplorationId()).toBeFalse(); + }); - it('should update thumbnail filename when changing thumbnail file', - () => { - component.updateThumbnailFilename('abc'); - expect(component.editableThumbnailFilename).toEqual('abc'); - }); + it('should update thumbnail filename when changing thumbnail file', () => { + component.updateThumbnailFilename('abc'); + expect(component.editableThumbnailFilename).toEqual('abc'); + }); + + it('should update thumbnail bg color when changing thumbnail color', () => { + component.updateThumbnailBgColor('abc'); + component.cancel(); + expect(component.editableThumbnailBgColor).toEqual('abc'); + }); - it('should update thumbnail bg color when changing thumbnail color', + it( + 'should check if chapter is valid when it has title, exploration id and' + + ' thumbnail file', () => { - component.updateThumbnailBgColor('abc'); - component.cancel(); - expect(component.editableThumbnailBgColor).toEqual('abc'); - }); + expect(component.isValid()).toEqual(false); + component.title = 'title'; + component.explorationId = '1'; + expect(component.isValid()).toEqual(false); + component.editableThumbnailFilename = '1'; + expect(component.isValid()).toEqual(true); + component.explorationId = ''; + expect(component.isValid()).toEqual(false); + } + ); - it('should check if chapter is valid when it has title, exploration id and' + - ' thumbnail file', () => { - expect(component.isValid()).toEqual(false); - component.title = 'title'; - component.explorationId = '1'; - expect(component.isValid()).toEqual(false); - component.editableThumbnailFilename = '1'; - expect(component.isValid()).toEqual(true); - component.explorationId = ''; - expect(component.isValid()).toEqual(false); - }); + it('should show warning message when exploration cannot be curated', fakeAsync(() => { + spyOn(storyEditorStateService, 'isStoryPublished').and.returnValue(true); + spyOn( + curatedExplorationValidationService, + 'isExpPublishedAsync' + ).and.resolveTo(true); + spyOn( + curatedExplorationValidationService, + 'isDefaultCategoryAsync' + ).and.resolveTo(true); + spyOn( + curatedExplorationValidationService, + 'getStatesWithRestrictedInteractions' + ).and.resolveTo([]); + spyOn( + curatedExplorationValidationService, + 'getStatesWithInvalidMultipleChoices' + ).and.resolveTo([]); + spyOn( + editableStoryBackendApiService, + 'validateExplorationsAsync' + ).and.resolveTo([ + 'Explorations in a story are not expected to contain ' + + 'training data for any answer group. State Introduction of ' + + 'exploration with ID 1 contains training data in one of ' + + 'its answer groups.', + ]); + component.saveAsync(); + flushMicrotasks(); - it('should show warning message when exploration cannot be curated', + expect(component.invalidExpId).toEqual(true); + expect(component.invalidExpErrorStrings).toEqual([ + 'Explorations in a story are not expected to contain ' + + 'training data for any answer group. State Introduction of ' + + 'exploration with ID 1 contains training data in one of ' + + 'its answer groups.', + ]); + })); + + it( + 'should warn that the exploration is not published when trying to save' + + ' a chapter with an invalid exploration id', fakeAsync(() => { spyOn(storyEditorStateService, 'isStoryPublished').and.returnValue(true); - spyOn(curatedExplorationValidationService, 'isExpPublishedAsync') - .and.resolveTo(true); - spyOn(curatedExplorationValidationService, 'isDefaultCategoryAsync') - .and.resolveTo(true); - spyOn( - curatedExplorationValidationService, - 'getStatesWithRestrictedInteractions').and.resolveTo([]); spyOn( curatedExplorationValidationService, - 'getStatesWithInvalidMultipleChoices').and.resolveTo([]); + 'isExpPublishedAsync' + ).and.resolveTo(false); spyOn( - editableStoryBackendApiService, 'validateExplorationsAsync' - ).and.resolveTo([ - 'Explorations in a story are not expected to contain ' + - 'training data for any answer group. State Introduction of ' + - 'exploration with ID 1 contains training data in one of ' + - 'its answer groups.' - ]); + editableStoryBackendApiService, + 'validateExplorationsAsync' + ).and.resolveTo([]); component.saveAsync(); flushMicrotasks(); expect(component.invalidExpId).toEqual(true); + }) + ); + + it( + 'should warn that the exploration already exists in the story when' + + ' trying to save a chapter with an already used exploration id', + () => { + component.explorationId = 'exp_1'; + component.updateExplorationId(); expect(component.invalidExpErrorStrings).toEqual([ - 'Explorations in a story are not expected to contain ' + - 'training data for any answer group. State Introduction of ' + - 'exploration with ID 1 contains training data in one of ' + - 'its answer groups.' + 'The given exploration already exists in the story.', ]); - })); - - it('should warn that the exploration is not published when trying to save' + - ' a chapter with an invalid exploration id', fakeAsync(() => { - spyOn(storyEditorStateService, 'isStoryPublished').and.returnValue(true); - spyOn(curatedExplorationValidationService, 'isExpPublishedAsync') - .and.resolveTo(false); - spyOn( - editableStoryBackendApiService, 'validateExplorationsAsync' - ).and.resolveTo([]); - component.saveAsync(); - flushMicrotasks(); - - expect(component.invalidExpId).toEqual(true); - })); + expect(component.invalidExpId).toEqual(true); + } + ); - it('should warn that the exploration already exists in the story when' + - ' trying to save a chapter with an already used exploration id', - () => { - component.explorationId = 'exp_1'; + it('should set story node exploration id when updating exploration id', () => { + let storyUpdateSpy = spyOn(storyUpdateService, 'setStoryNodeExplorationId'); component.updateExplorationId(); - expect(component.invalidExpErrorStrings).toEqual([ - 'The given exploration already exists in the story.' - ]); - expect(component.invalidExpId).toEqual(true); + expect(storyUpdateSpy).toHaveBeenCalled(); }); - it('should set story node exploration id when updating exploration id', - () => { - let storyUpdateSpy = spyOn( - storyUpdateService, 'setStoryNodeExplorationId'); - component.updateExplorationId(); - expect(storyUpdateSpy).toHaveBeenCalled(); - }); - it('should not save when the chapter title is already used', () => { component.title = nodeTitles[0]; component.saveAsync(); expect(component.errorMsg).toBe('A chapter with this title already exists'); }); - it('should prevent exploration from being added if it doesn\'t exist ' + - 'or isn\'t published yet', fakeAsync(() => { - component.title = 'dummy_title'; - spyOn( - editableStoryBackendApiService, 'validateExplorationsAsync' - ).and.resolveTo([]); - spyOn(curatedExplorationValidationService, 'isExpPublishedAsync') - .and.returnValue(false); - const categorySpy = spyOn( - curatedExplorationValidationService, 'isDefaultCategoryAsync'); - component.saveAsync(); - flushMicrotasks(); - expect(component.invalidExpId).toEqual(true); - expect(categorySpy).not.toHaveBeenCalled(); - })); - - it('should prevent exploration from being added if its category ' + - 'is not default', fakeAsync(() => { - component.title = 'dummy_title'; - - spyOn( - editableStoryBackendApiService, 'validateExplorationsAsync' - ).and.resolveTo([]); - spyOn(curatedExplorationValidationService, 'isExpPublishedAsync') - .and.resolveTo(true); - spyOn(curatedExplorationValidationService, 'isDefaultCategoryAsync') - .and.resolveTo(false); - - component.saveAsync(); - flushMicrotasks(); - - expect(component.categoryIsDefault).toBe(false); - })); + it( + "should prevent exploration from being added if it doesn't exist " + + "or isn't published yet", + fakeAsync(() => { + component.title = 'dummy_title'; + spyOn( + editableStoryBackendApiService, + 'validateExplorationsAsync' + ).and.resolveTo([]); + spyOn( + curatedExplorationValidationService, + 'isExpPublishedAsync' + ).and.returnValue(false); + const categorySpy = spyOn( + curatedExplorationValidationService, + 'isDefaultCategoryAsync' + ); + component.saveAsync(); + flushMicrotasks(); + expect(component.invalidExpId).toEqual(true); + expect(categorySpy).not.toHaveBeenCalled(); + }) + ); - it('should prevent exploration from being added if it contains restricted ' + - 'interaction types', fakeAsync(() => { - component.title = 'dummy_title'; - const invalidStates = ['some_invalid_state']; + it( + 'should prevent exploration from being added if its category ' + + 'is not default', + fakeAsync(() => { + component.title = 'dummy_title'; - spyOn( - editableStoryBackendApiService, 'validateExplorationsAsync' - ).and.resolveTo([]); - spyOn(curatedExplorationValidationService, 'isExpPublishedAsync') - .and.resolveTo(true); - spyOn(curatedExplorationValidationService, 'isDefaultCategoryAsync') - .and.resolveTo(true); - spyOn( - curatedExplorationValidationService, - 'getStatesWithRestrictedInteractions').and.resolveTo(invalidStates); + spyOn( + editableStoryBackendApiService, + 'validateExplorationsAsync' + ).and.resolveTo([]); + spyOn( + curatedExplorationValidationService, + 'isExpPublishedAsync' + ).and.resolveTo(true); + spyOn( + curatedExplorationValidationService, + 'isDefaultCategoryAsync' + ).and.resolveTo(false); - component.saveAsync(); - flushMicrotasks(); + component.saveAsync(); + flushMicrotasks(); - expect(component.statesWithRestrictedInteractions).toBe(invalidStates); - })); + expect(component.categoryIsDefault).toBe(false); + }) + ); - it('should prevent exploration from being added if it contains an invalid ' + - 'multiple choice input', fakeAsync(() => { - component.title = 'dummy_title'; - const invalidStates = ['some_invalid_state']; + it( + 'should prevent exploration from being added if it contains restricted ' + + 'interaction types', + fakeAsync(() => { + component.title = 'dummy_title'; + const invalidStates = ['some_invalid_state']; - spyOn( - editableStoryBackendApiService, 'validateExplorationsAsync' - ).and.resolveTo([]); - spyOn(curatedExplorationValidationService, 'isExpPublishedAsync') - .and.resolveTo(true); - spyOn(curatedExplorationValidationService, 'isDefaultCategoryAsync') - .and.resolveTo(true); - spyOn( - curatedExplorationValidationService, - 'getStatesWithRestrictedInteractions').and.resolveTo([]); - spyOn( - curatedExplorationValidationService, - 'getStatesWithInvalidMultipleChoices').and.resolveTo(invalidStates); + spyOn( + editableStoryBackendApiService, + 'validateExplorationsAsync' + ).and.resolveTo([]); + spyOn( + curatedExplorationValidationService, + 'isExpPublishedAsync' + ).and.resolveTo(true); + spyOn( + curatedExplorationValidationService, + 'isDefaultCategoryAsync' + ).and.resolveTo(true); + spyOn( + curatedExplorationValidationService, + 'getStatesWithRestrictedInteractions' + ).and.resolveTo(invalidStates); - component.saveAsync(); - flushMicrotasks(); + component.saveAsync(); + flushMicrotasks(); - expect(component.statesWithTooFewMultipleChoiceOptions).toBe(invalidStates); - })); + expect(component.statesWithRestrictedInteractions).toBe(invalidStates); + }) + ); - it('should attempt to save exploration when all validation checks pass', + it( + 'should prevent exploration from being added if it contains an invalid ' + + 'multiple choice input', fakeAsync(() => { component.title = 'dummy_title'; + const invalidStates = ['some_invalid_state']; + spyOn( - editableStoryBackendApiService, 'validateExplorationsAsync' + editableStoryBackendApiService, + 'validateExplorationsAsync' ).and.resolveTo([]); - spyOn(curatedExplorationValidationService, 'isExpPublishedAsync') - .and.resolveTo(true); - spyOn(curatedExplorationValidationService, 'isDefaultCategoryAsync') - .and.resolveTo(true); spyOn( curatedExplorationValidationService, - 'getStatesWithRestrictedInteractions').and.resolveTo([]); + 'isExpPublishedAsync' + ).and.resolveTo(true); + spyOn( + curatedExplorationValidationService, + 'isDefaultCategoryAsync' + ).and.resolveTo(true); spyOn( curatedExplorationValidationService, - 'getStatesWithInvalidMultipleChoices').and.resolveTo([]); - const updateExplorationIdSpy = spyOn(component, 'updateExplorationId'); + 'getStatesWithRestrictedInteractions' + ).and.resolveTo([]); + spyOn( + curatedExplorationValidationService, + 'getStatesWithInvalidMultipleChoices' + ).and.resolveTo(invalidStates); + component.saveAsync(); flushMicrotasks(); - expect(updateExplorationIdSpy).toHaveBeenCalled(); - })); + expect(component.statesWithTooFewMultipleChoiceOptions).toBe( + invalidStates + ); + }) + ); + + it('should attempt to save exploration when all validation checks pass', fakeAsync(() => { + component.title = 'dummy_title'; + spyOn( + editableStoryBackendApiService, + 'validateExplorationsAsync' + ).and.resolveTo([]); + spyOn( + curatedExplorationValidationService, + 'isExpPublishedAsync' + ).and.resolveTo(true); + spyOn( + curatedExplorationValidationService, + 'isDefaultCategoryAsync' + ).and.resolveTo(true); + spyOn( + curatedExplorationValidationService, + 'getStatesWithRestrictedInteractions' + ).and.resolveTo([]); + spyOn( + curatedExplorationValidationService, + 'getStatesWithInvalidMultipleChoices' + ).and.resolveTo([]); + const updateExplorationIdSpy = spyOn(component, 'updateExplorationId'); + component.saveAsync(); + flushMicrotasks(); + + expect(updateExplorationIdSpy).toHaveBeenCalled(); + })); it('should clear error message when changing exploration id', () => { component.title = nodeTitles[0]; @@ -378,7 +441,7 @@ describe('Create New Chapter Modal Component', () => { expect(component.errorMsg).toBe(null); expect(component.invalidExpId).toBe(false); expect(component.invalidExpErrorStrings).toEqual([ - 'Please enter a valid exploration id.' + 'Please enter a valid exploration id.', ]); }); }); diff --git a/core/templates/pages/story-editor-page/modal-templates/story-editor-save-modal.component.spec.ts b/core/templates/pages/story-editor-page/modal-templates/story-editor-save-modal.component.spec.ts index 8fa58f1fd132..4bcb43748ba7 100644 --- a/core/templates/pages/story-editor-page/modal-templates/story-editor-save-modal.component.spec.ts +++ b/core/templates/pages/story-editor-page/modal-templates/story-editor-save-modal.component.spec.ts @@ -16,10 +16,10 @@ * @fileoverview Unit tests for story editor save modal. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { StoryEditorSaveModalComponent } from './story-editor-save-modal.component'; -import { FormsModule } from '@angular/forms'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {StoryEditorSaveModalComponent} from './story-editor-save-modal.component'; +import {FormsModule} from '@angular/forms'; class MockActiveModal { dismiss(): void { @@ -42,9 +42,9 @@ describe('Story Editor Save Modal Component', () => { providers: [ { provide: NgbActiveModal, - useClass: MockActiveModal - } - ] + useClass: MockActiveModal, + }, + ], }).compileComponents(); })); diff --git a/core/templates/pages/story-editor-page/modal-templates/story-editor-save-modal.component.ts b/core/templates/pages/story-editor-page/modal-templates/story-editor-save-modal.component.ts index 2c5634c27c0f..1731596ce9f8 100644 --- a/core/templates/pages/story-editor-page/modal-templates/story-editor-save-modal.component.ts +++ b/core/templates/pages/story-editor-page/modal-templates/story-editor-save-modal.component.ts @@ -16,21 +16,19 @@ * @fileoverview Component for the Story Editor Save Modal Component. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; @Component({ selector: 'oppia-story-editor-save-modal', - templateUrl: './story-editor-save-modal.component.html' + templateUrl: './story-editor-save-modal.component.html', }) export class StoryEditorSaveModalComponent { // This property is initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 bindedMessage!: string; - constructor( - private activeModal: NgbActiveModal - ) {} + constructor(private activeModal: NgbActiveModal) {} cancel(): void { this.activeModal.dismiss(); diff --git a/core/templates/pages/story-editor-page/modal-templates/story-editor-unpublish-modal.component.spec.ts b/core/templates/pages/story-editor-page/modal-templates/story-editor-unpublish-modal.component.spec.ts index b3ca681812db..50ae73e23ecb 100644 --- a/core/templates/pages/story-editor-page/modal-templates/story-editor-unpublish-modal.component.spec.ts +++ b/core/templates/pages/story-editor-page/modal-templates/story-editor-unpublish-modal.component.spec.ts @@ -16,11 +16,10 @@ * @fileoverview Unit tests for story editor unpublish modal. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { StoryEditorUnpublishModalComponent } from './story-editor-unpublish-modal.component'; -import { PlatformFeatureService } from '../../../services/platform-feature.service'; - +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {StoryEditorUnpublishModalComponent} from './story-editor-unpublish-modal.component'; +import {PlatformFeatureService} from '../../../services/platform-feature.service'; class MockActiveModal { dismiss(): void { @@ -35,8 +34,8 @@ class MockActiveModal { class MockPlatformFeatureService { status = { SerialChapterLaunchCurriculumAdminView: { - isEnabled: false - } + isEnabled: false, + }, }; } @@ -52,13 +51,13 @@ describe('Story Editor Unpublish Modal Component', () => { providers: [ { provide: PlatformFeatureService, - useValue: mockPlatformFeatureService + useValue: mockPlatformFeatureService, }, { provide: NgbActiveModal, - useClass: MockActiveModal - } - ] + useClass: MockActiveModal, + }, + ], }).compileComponents(); })); @@ -75,33 +74,32 @@ describe('Story Editor Unpublish Modal Component', () => { }); it('should close by proceeding with unpublishing', () => { - mockPlatformFeatureService. - status.SerialChapterLaunchCurriculumAdminView.isEnabled = false; + mockPlatformFeatureService.status.SerialChapterLaunchCurriculumAdminView.isEnabled = + false; const confirmSpy = spyOn(ngbActiveModal, 'close').and.callThrough(); component.confirm(); expect(confirmSpy).toHaveBeenCalled(); - mockPlatformFeatureService. - status.SerialChapterLaunchCurriculumAdminView.isEnabled = true; + mockPlatformFeatureService.status.SerialChapterLaunchCurriculumAdminView.isEnabled = + true; component.confirm(); expect(confirmSpy).toHaveBeenCalledWith(component.unpublishingReason); }); it('should get status of Serial Chapter Launch Feature flag', () => { - mockPlatformFeatureService. - status.SerialChapterLaunchCurriculumAdminView.isEnabled = false; + mockPlatformFeatureService.status.SerialChapterLaunchCurriculumAdminView.isEnabled = + false; expect(component.isSerialChapterFeatureFlagEnabled()).toEqual(false); - mockPlatformFeatureService. - status.SerialChapterLaunchCurriculumAdminView.isEnabled = true; + mockPlatformFeatureService.status.SerialChapterLaunchCurriculumAdminView.isEnabled = + true; expect(component.isSerialChapterFeatureFlagEnabled()).toEqual(true); }); - it('should check if the default reason for unpublishing is BAD_CONTENT', - () => { - expect(component.unpublishingReason).toBe('BAD_CONTENT'); - expect(component.selectedReasonText).toBe(component.badContentReasonText); - }); + it('should check if the default reason for unpublishing is BAD_CONTENT', () => { + expect(component.unpublishingReason).toBe('BAD_CONTENT'); + expect(component.selectedReasonText).toBe(component.badContentReasonText); + }); it('should set unpublishing reason', () => { expect(component.unpublishingReason).toBe('BAD_CONTENT'); @@ -110,13 +108,11 @@ describe('Story Editor Unpublish Modal Component', () => { component.setReason('CHAPTER_NEEDS_SPLITTING'); expect(component.unpublishingReason).toBe('CHAPTER_NEEDS_SPLITTING'); - expect(component.selectedReasonText).toBe( - component.splitChapterReasonText); + expect(component.selectedReasonText).toBe(component.splitChapterReasonText); component.setReason('BAD_CONTENT'); expect(component.unpublishingReason).toBe('BAD_CONTENT'); - expect(component.selectedReasonText).toBe( - component.badContentReasonText); + expect(component.selectedReasonText).toBe(component.badContentReasonText); }); }); diff --git a/core/templates/pages/story-editor-page/modal-templates/story-editor-unpublish-modal.component.ts b/core/templates/pages/story-editor-page/modal-templates/story-editor-unpublish-modal.component.ts index a1215f194dfe..e492b71e448d 100644 --- a/core/templates/pages/story-editor-page/modal-templates/story-editor-unpublish-modal.component.ts +++ b/core/templates/pages/story-editor-page/modal-templates/story-editor-unpublish-modal.component.ts @@ -16,27 +16,27 @@ * @fileoverview Component for the Story Editor Unpublish Modal Component. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { PlatformFeatureService } from 'services/platform-feature.service'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {PlatformFeatureService} from 'services/platform-feature.service'; @Component({ selector: 'oppia-story-editor-unpublish-modal', - templateUrl: './story-editor-unpublish-modal.component.html' + templateUrl: './story-editor-unpublish-modal.component.html', }) export class StoryEditorUnpublishModalComponent { constructor( - private platformFeatureService: PlatformFeatureService, - private activeModal: NgbActiveModal + private platformFeatureService: PlatformFeatureService, + private activeModal: NgbActiveModal ) {} unpublishedChapters: number[] = []; - badContentReasonText: string = 'Bad content (no new explorations ' + - 'will be added)'; + badContentReasonText: string = + 'Bad content (no new explorations ' + 'will be added)'; - splitChapterReasonText: string = 'Split Chapters' + - ' (requires new explorations to be added)'; + splitChapterReasonText: string = + 'Split Chapters' + ' (requires new explorations to be added)'; selectedReasonText: string = this.badContentReasonText; unpublishingReason: string = 'BAD_CONTENT'; @@ -54,9 +54,8 @@ export class StoryEditorUnpublishModalComponent { } isSerialChapterFeatureFlagEnabled(): boolean { - return ( - this.platformFeatureService. - status.SerialChapterLaunchCurriculumAdminView.isEnabled); + return this.platformFeatureService.status + .SerialChapterLaunchCurriculumAdminView.isEnabled; } setReason(reason: string): void { diff --git a/core/templates/pages/story-editor-page/navbar/story-editor-navbar-breadcrumb.component.spec.ts b/core/templates/pages/story-editor-page/navbar/story-editor-navbar-breadcrumb.component.spec.ts index d44a7714790f..f40c9245c26a 100644 --- a/core/templates/pages/story-editor-page/navbar/story-editor-navbar-breadcrumb.component.spec.ts +++ b/core/templates/pages/story-editor-page/navbar/story-editor-navbar-breadcrumb.component.spec.ts @@ -16,16 +16,16 @@ * @fileoverview Unit tests for the navbar breadcrumb of the story editor. */ -import { EventEmitter } from '@angular/core'; -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { Story } from 'domain/story/story.model'; -import { StoryEditorStateService } from '../services/story-editor-state.service'; -import { StoryEditorNavbarBreadcrumbComponent } from './story-editor-navbar-breadcrumb.component'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { SavePendingChangesModalComponent } from 'components/save-pending-changes/save-pending-changes-modal.component'; -import { UndoRedoService } from 'domain/editor/undo_redo/undo-redo.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; +import {EventEmitter} from '@angular/core'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {Story} from 'domain/story/story.model'; +import {StoryEditorStateService} from '../services/story-editor-state.service'; +import {StoryEditorNavbarBreadcrumbComponent} from './story-editor-navbar-breadcrumb.component'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {SavePendingChangesModalComponent} from 'components/save-pending-changes/save-pending-changes-modal.component'; +import {UndoRedoService} from 'domain/editor/undo_redo/undo-redo.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; class MockNgbModalRef { componentInstance!: { @@ -46,7 +46,7 @@ describe('StoryEditorNavbarBreadcrumbComponent', () => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], declarations: [StoryEditorNavbarBreadcrumbComponent], - providers: [StoryEditorStateService, WindowRef] + providers: [StoryEditorStateService, WindowRef], }).compileComponents(); })); @@ -66,41 +66,44 @@ describe('StoryEditorNavbarBreadcrumbComponent', () => { story_contents: { initial_node_id: 'node_1', next_node_id: 'node_3', - nodes: [{ - title: 'title_1', - description: 'description_1', - id: 'node_1', - prerequisite_skill_ids: [], - acquired_skill_ids: [], - destination_node_ids: [], - outline: 'Outline', - exploration_id: 'exp_1', - outline_is_finalized: false, - thumbnail_filename: 'img.png', - thumbnail_bg_color: '#a33f40', - status: 'Published', - planned_publication_date_msecs: 100, - last_modified_msecs: 100, - first_publication_date_msecs: 200, - unpublishing_reason: null - }, { - title: 'title_2', - description: 'description_2', - id: 'node_2', - prerequisite_skill_ids: [], - acquired_skill_ids: [], - destination_node_ids: [], - outline: 'Outline', - exploration_id: 'exp_2', - outline_is_finalized: false, - thumbnail_filename: 'img2.png', - thumbnail_bg_color: '#a33f40', - status: 'Published', - planned_publication_date_msecs: 100, - last_modified_msecs: 100, - first_publication_date_msecs: 200, - unpublishing_reason: null - }], + nodes: [ + { + title: 'title_1', + description: 'description_1', + id: 'node_1', + prerequisite_skill_ids: [], + acquired_skill_ids: [], + destination_node_ids: [], + outline: 'Outline', + exploration_id: 'exp_1', + outline_is_finalized: false, + thumbnail_filename: 'img.png', + thumbnail_bg_color: '#a33f40', + status: 'Published', + planned_publication_date_msecs: 100, + last_modified_msecs: 100, + first_publication_date_msecs: 200, + unpublishing_reason: null, + }, + { + title: 'title_2', + description: 'description_2', + id: 'node_2', + prerequisite_skill_ids: [], + acquired_skill_ids: [], + destination_node_ids: [], + outline: 'Outline', + exploration_id: 'exp_2', + outline_is_finalized: false, + thumbnail_filename: 'img2.png', + thumbnail_bg_color: '#a33f40', + status: 'Published', + planned_publication_date_msecs: 100, + last_modified_msecs: 100, + first_publication_date_msecs: 200, + unpublishing_reason: null, + }, + ], }, language_code: 'en', meta_tag_content: 'meta', @@ -108,7 +111,7 @@ describe('StoryEditorNavbarBreadcrumbComponent', () => { version: 1, corresponding_topic_id: 'topic_id', thumbnail_bg_color: 'red', - thumbnail_filename: 'image' + thumbnail_filename: 'image', }); }); @@ -118,8 +121,10 @@ describe('StoryEditorNavbarBreadcrumbComponent', () => { it('should initialise component when user open story editor', () => { let mockStoryInitializedEventEmitter = new EventEmitter(); - spyOnProperty(storyEditorStateService, 'onStoryInitialized') - .and.returnValue(mockStoryInitializedEventEmitter); + spyOnProperty( + storyEditorStateService, + 'onStoryInitialized' + ).and.returnValue(mockStoryInitializedEventEmitter); spyOn(storyEditorStateService, 'getTopicName').and.returnValue('Topic 1'); spyOn(storyEditorStateService, 'getStory').and.returnValue(story); component.ngOnInit(); @@ -138,37 +143,42 @@ describe('StoryEditorNavbarBreadcrumbComponent', () => { component.returnToTopicEditorPage(); expect(windowRef.nativeWindow.open).toHaveBeenCalledWith( - '/topic_editor/topic_id', '_self'); + '/topic_editor/topic_id', + '_self' + ); }); - it('should open modal to save changes when user clicks topic name' + - ' with unsaved changes', () => { - spyOn(ngbModal, 'open').and.returnValue( - { + it( + 'should open modal to save changes when user clicks topic name' + + ' with unsaved changes', + () => { + spyOn(ngbModal, 'open').and.returnValue({ componentInstance: MockNgbModalRef, - result: Promise.resolve() - } as NgbModalRef - ); - spyOn(undoRedoService, 'getChangeCount').and.returnValue(1); + result: Promise.resolve(), + } as NgbModalRef); + spyOn(undoRedoService, 'getChangeCount').and.returnValue(1); - component.returnToTopicEditorPage(); + component.returnToTopicEditorPage(); - expect(ngbModal.open).toHaveBeenCalledWith( - SavePendingChangesModalComponent, { backdrop: true }); - }); + expect(ngbModal.open).toHaveBeenCalledWith( + SavePendingChangesModalComponent, + {backdrop: true} + ); + } + ); it('should close save pending changes modal when user clicks cancel', () => { - spyOn(ngbModal, 'open').and.returnValue( - { - componentInstance: MockNgbModalRef, - result: Promise.reject() - } as NgbModalRef - ); + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: MockNgbModalRef, + result: Promise.reject(), + } as NgbModalRef); spyOn(undoRedoService, 'getChangeCount').and.returnValue(1); component.returnToTopicEditorPage(); expect(ngbModal.open).toHaveBeenCalledWith( - SavePendingChangesModalComponent, { backdrop: true }); + SavePendingChangesModalComponent, + {backdrop: true} + ); }); }); diff --git a/core/templates/pages/story-editor-page/navbar/story-editor-navbar-breadcrumb.component.ts b/core/templates/pages/story-editor-page/navbar/story-editor-navbar-breadcrumb.component.ts index 49271b467d23..c4ff695fd7d8 100644 --- a/core/templates/pages/story-editor-page/navbar/story-editor-navbar-breadcrumb.component.ts +++ b/core/templates/pages/story-editor-page/navbar/story-editor-navbar-breadcrumb.component.ts @@ -16,28 +16,28 @@ * @fileoverview Component for the navbar breadcrumb of the story editor. */ -import { Component } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { UndoRedoService } from 'domain/editor/undo_redo/undo-redo.service'; -import { Story } from 'domain/story/story.model'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { Subscription } from 'rxjs'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { SavePendingChangesModalComponent } from 'components/save-pending-changes/save-pending-changes-modal.component'; -import { StoryEditorStateService } from '../services/story-editor-state.service'; +import {Component} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {UndoRedoService} from 'domain/editor/undo_redo/undo-redo.service'; +import {Story} from 'domain/story/story.model'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {Subscription} from 'rxjs'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {SavePendingChangesModalComponent} from 'components/save-pending-changes/save-pending-changes-modal.component'; +import {StoryEditorStateService} from '../services/story-editor-state.service'; - @Component({ - selector: 'oppia-story-editor-navbar-breadcrumb', - templateUrl: './story-editor-navbar-breadcrumb.component.html' - }) +@Component({ + selector: 'oppia-story-editor-navbar-breadcrumb', + templateUrl: './story-editor-navbar-breadcrumb.component.html', +}) export class StoryEditorNavbarBreadcrumbComponent { constructor( - private undoRedoService: UndoRedoService, - private ngbModal: NgbModal, - private storyEditorStateService: StoryEditorStateService, - private windowRef: WindowRef, - private urlInterpolationService: UrlInterpolationService + private undoRedoService: UndoRedoService, + private ngbModal: NgbModal, + private storyEditorStateService: StoryEditorStateService, + private windowRef: WindowRef, + private urlInterpolationService: UrlInterpolationService ) {} // These properties are initialized using Angular lifecycle hooks @@ -50,24 +50,27 @@ export class StoryEditorNavbarBreadcrumbComponent { returnToTopicEditorPage(): void { if (this.undoRedoService.getChangeCount() > 0) { - const modalRef = this.ngbModal.open( - SavePendingChangesModalComponent, - {backdrop: true} - ); + const modalRef = this.ngbModal.open(SavePendingChangesModalComponent, { + backdrop: true, + }); - modalRef.componentInstance.body = ( - 'Please save all pending changes before returning to the topic.'); + modalRef.componentInstance.body = + 'Please save all pending changes before returning to the topic.'; - modalRef.result.then(() => {}, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + modalRef.result.then( + () => {}, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } else { this.windowRef.nativeWindow.open( this.urlInterpolationService.interpolateUrl( - this.TOPIC_EDITOR_URL_TEMPLATE, { - topicId: this.story.getCorrespondingTopicId() + this.TOPIC_EDITOR_URL_TEMPLATE, + { + topicId: this.story.getCorrespondingTopicId(), } ), '_self' @@ -77,12 +80,10 @@ export class StoryEditorNavbarBreadcrumbComponent { ngOnInit(): void { this.directiveSubscriptions.add( - this.storyEditorStateService.onStoryInitialized.subscribe( - () => { - this.topicName = this.storyEditorStateService.getTopicName(); - this.story = this.storyEditorStateService.getStory(); - } - ) + this.storyEditorStateService.onStoryInitialized.subscribe(() => { + this.topicName = this.storyEditorStateService.getTopicName(); + this.story = this.storyEditorStateService.getStory(); + }) ); } @@ -91,5 +92,9 @@ export class StoryEditorNavbarBreadcrumbComponent { } } -angular.module('oppia').directive('oppiaStoryEditorNavbarBreadcrumb', - downgradeComponent({component: StoryEditorNavbarBreadcrumbComponent})); +angular + .module('oppia') + .directive( + 'oppiaStoryEditorNavbarBreadcrumb', + downgradeComponent({component: StoryEditorNavbarBreadcrumbComponent}) + ); diff --git a/core/templates/pages/story-editor-page/navbar/story-editor-navbar.component.spec.ts b/core/templates/pages/story-editor-page/navbar/story-editor-navbar.component.spec.ts index 5756140a5c6d..419ad5313501 100644 --- a/core/templates/pages/story-editor-page/navbar/story-editor-navbar.component.spec.ts +++ b/core/templates/pages/story-editor-page/navbar/story-editor-navbar.component.spec.ts @@ -16,22 +16,27 @@ * @fileoverview Unit tests for the story editor navbar component. */ -import { Story, StoryBackendDict } from 'domain/story/story.model'; -import { StoryNode } from 'domain/story/story-node.model'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { UndoRedoService } from 'domain/editor/undo_redo/undo-redo.service'; -import { StoryEditorStateService } from '../services/story-editor-state.service'; -import { StoryEditorNavbarComponent } from './story-editor-navbar.component'; -import { StoryEditorUnpublishModalComponent } from '../modal-templates/story-editor-unpublish-modal.component'; -import { EditableStoryBackendApiService } from 'domain/story/editable-story-backend-api.service'; -import { AlertsService } from 'services/alerts.service'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { StoryEditorNavigationService } from '../services/story-editor-navigation.service'; -import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; -import { NgbModal, NgbModalRef, NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { PlatformFeatureService } from '../../../services/platform-feature.service'; -import { StoryUpdateService } from 'domain/story/story-update.service'; +import {Story, StoryBackendDict} from 'domain/story/story.model'; +import {StoryNode} from 'domain/story/story-node.model'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {UndoRedoService} from 'domain/editor/undo_redo/undo-redo.service'; +import {StoryEditorStateService} from '../services/story-editor-state.service'; +import {StoryEditorNavbarComponent} from './story-editor-navbar.component'; +import {StoryEditorUnpublishModalComponent} from '../modal-templates/story-editor-unpublish-modal.component'; +import {EditableStoryBackendApiService} from 'domain/story/editable-story-backend-api.service'; +import {AlertsService} from 'services/alerts.service'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import {StoryEditorNavigationService} from '../services/story-editor-navigation.service'; +import {BrowserDynamicTestingModule} from '@angular/platform-browser-dynamic/testing'; +import {NgbModal, NgbModalRef, NgbModule} from '@ng-bootstrap/ng-bootstrap'; +import {PlatformFeatureService} from '../../../services/platform-feature.service'; +import {StoryUpdateService} from 'domain/story/story-update.service'; class MockNgbModalRef { componentInstance!: { @@ -42,8 +47,8 @@ class MockNgbModalRef { class MockPlatformFeatureService { status = { SerialChapterLaunchCurriculumAdminView: { - isEnabled: false - } + isEnabled: false, + }, }; } @@ -66,7 +71,7 @@ describe('Story editor navbar component', () => { imports: [HttpClientTestingModule, NgbModule], declarations: [ StoryEditorNavbarComponent, - StoryEditorUnpublishModalComponent + StoryEditorUnpublishModalComponent, ], providers: [ StoryEditorStateService, @@ -77,15 +82,17 @@ describe('Story editor navbar component', () => { AlertsService, { provide: PlatformFeatureService, - useValue: mockPlatformFeatureService + useValue: mockPlatformFeatureService, }, ], - schemas: [NO_ERRORS_SCHEMA] - }).overrideModule(BrowserDynamicTestingModule, { - set: { - entryComponents: [StoryEditorUnpublishModalComponent] - } - }).compileComponents(); + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideModule(BrowserDynamicTestingModule, { + set: { + entryComponents: [StoryEditorUnpublishModalComponent], + }, + }) + .compileComponents(); }); beforeEach(() => { @@ -94,13 +101,13 @@ describe('Story editor navbar component', () => { alertsService = TestBed.inject(AlertsService); undoRedoService = TestBed.inject(UndoRedoService); storyEditorStateService = TestBed.inject(StoryEditorStateService); - storyEditorNavigationService = TestBed.inject( - StoryEditorNavigationService); + storyEditorNavigationService = TestBed.inject(StoryEditorNavigationService); storyUpdateService = TestBed.inject(StoryUpdateService); undoRedoService = TestBed.inject(UndoRedoService); ngbModal = TestBed.inject(NgbModal); editableStoryBackendApiService = TestBed.inject( - EditableStoryBackendApiService); + EditableStoryBackendApiService + ); storyBackendDict = { id: 'storyId_0', title: 'Story title', @@ -109,41 +116,44 @@ describe('Story editor navbar component', () => { story_contents: { initial_node_id: 'node_1', next_node_id: 'node_3', - nodes: [{ - title: 'title_1', - description: 'description_1', - id: 'node_1', - prerequisite_skill_ids: [], - acquired_skill_ids: [], - destination_node_ids: [], - outline: 'Outline', - exploration_id: 'exp_1', - outline_is_finalized: false, - thumbnail_filename: 'img.png', - thumbnail_bg_color: '#a33f40', - status: 'Published', - planned_publication_date_msecs: 100, - last_modified_msecs: 100, - first_publication_date_msecs: 100, - unpublishing_reason: null - }, { - title: 'title_2', - description: 'description_2', - id: 'node_2', - prerequisite_skill_ids: [], - acquired_skill_ids: [], - destination_node_ids: [], - outline: 'Outline', - exploration_id: 'exp_2', - outline_is_finalized: false, - thumbnail_filename: 'img2.png', - thumbnail_bg_color: '#a33f40', - status: 'Ready To Publish', - planned_publication_date_msecs: 100, - last_modified_msecs: 100, - first_publication_date_msecs: 100, - unpublishing_reason: null - }], + nodes: [ + { + title: 'title_1', + description: 'description_1', + id: 'node_1', + prerequisite_skill_ids: [], + acquired_skill_ids: [], + destination_node_ids: [], + outline: 'Outline', + exploration_id: 'exp_1', + outline_is_finalized: false, + thumbnail_filename: 'img.png', + thumbnail_bg_color: '#a33f40', + status: 'Published', + planned_publication_date_msecs: 100, + last_modified_msecs: 100, + first_publication_date_msecs: 100, + unpublishing_reason: null, + }, + { + title: 'title_2', + description: 'description_2', + id: 'node_2', + prerequisite_skill_ids: [], + acquired_skill_ids: [], + destination_node_ids: [], + outline: 'Outline', + exploration_id: 'exp_2', + outline_is_finalized: false, + thumbnail_filename: 'img2.png', + thumbnail_bg_color: '#a33f40', + status: 'Ready To Publish', + planned_publication_date_msecs: 100, + last_modified_msecs: 100, + first_publication_date_msecs: 100, + unpublishing_reason: null, + }, + ], }, language_code: 'en', meta_tag_content: 'meta', @@ -151,11 +161,11 @@ describe('Story editor navbar component', () => { version: 1, corresponding_topic_id: 'topic_id', thumbnail_bg_color: 'red', - thumbnail_filename: 'image' + thumbnail_filename: 'image', }; - spyOn(storyEditorStateService, 'getSkillSummaries').and.returnValue( - [{ + spyOn(storyEditorStateService, 'getSkillSummaries').and.returnValue([ + { id: 'abc', description: 'description', language_code: 'en', @@ -164,17 +174,24 @@ describe('Story editor navbar component', () => { worked_examples_count: 1, skill_model_created_on: 1, skill_model_last_updated: 1, - }]); + }, + ]); spyOn(storyEditorStateService, 'getClassroomUrlFragment').and.returnValue( - 'math'); + 'math' + ); spyOn(storyEditorStateService, 'getTopicUrlFragment').and.returnValue( - 'fractions'); + 'fractions' + ); spyOn(storyEditorStateService, 'getTopicName').and.returnValue('addition'); - spyOn(editableStoryBackendApiService, 'changeStoryPublicationStatusAsync') - .and.returnValue(Promise.resolve()); - spyOn(editableStoryBackendApiService, 'validateExplorationsAsync') - .and.returnValue(Promise.resolve([])); + spyOn( + editableStoryBackendApiService, + 'changeStoryPublicationStatusAsync' + ).and.returnValue(Promise.resolve()); + spyOn( + editableStoryBackendApiService, + 'validateExplorationsAsync' + ).and.returnValue(Promise.resolve([])); }); afterEach(() => { @@ -184,90 +201,100 @@ describe('Story editor navbar component', () => { it('should get status of Serial Chapter Launch Feature flag', () => { expect(component.isSerialChapterFeatureFlagEnabled()).toBeFalse(); - mockPlatformFeatureService. - status.SerialChapterLaunchCurriculumAdminView.isEnabled = true; + mockPlatformFeatureService.status.SerialChapterLaunchCurriculumAdminView.isEnabled = + true; expect(component.isSerialChapterFeatureFlagEnabled()).toBeTrue(); }); it('should get if chapter is publishable', () => { - spyOn( - storyEditorStateService, 'isCurrentNodePublishable'). - and.returnValue(true); + spyOn(storyEditorStateService, 'isCurrentNodePublishable').and.returnValue( + true + ); expect(component.isChapterPublishable()).toBeTrue(); - storyEditorStateService.isCurrentNodePublishable = jasmine. - createSpy().and.returnValue(false); + storyEditorStateService.isCurrentNodePublishable = jasmine + .createSpy() + .and.returnValue(false); expect(component.isChapterPublishable()).toBeFalse(); }); it('should get if publish button is disabled', () => { spyOn( - storyEditorStateService, 'getNewChapterPublicationIsDisabled'). - and.returnValue(true); + storyEditorStateService, + 'getNewChapterPublicationIsDisabled' + ).and.returnValue(true); expect(component.isPublishButtonDisabled()).toBeTrue(); - storyEditorStateService.getNewChapterPublicationIsDisabled = jasmine. - createSpy().and.returnValue(false); + storyEditorStateService.getNewChapterPublicationIsDisabled = jasmine + .createSpy() + .and.returnValue(false); expect(component.isPublishButtonDisabled()).toBeFalse(); }); it('should get if chapters are being published', () => { - spyOn( - storyEditorStateService, 'areChaptersBeingPublished'). - and.returnValue(true); + spyOn(storyEditorStateService, 'areChaptersBeingPublished').and.returnValue( + true + ); expect(component.areChaptersBeingPublished()).toBeTrue(); - storyEditorStateService.areChaptersBeingPublished = jasmine. - createSpy().and.returnValue(false); + storyEditorStateService.areChaptersBeingPublished = jasmine + .createSpy() + .and.returnValue(false); expect(component.areChaptersBeingPublished()).toBeFalse(); }); it('should get if chapter status is being changed', () => { - spyOn( - storyEditorStateService, 'isChangingChapterStatus'). - and.returnValue(true); + spyOn(storyEditorStateService, 'isChangingChapterStatus').and.returnValue( + true + ); expect(component.isChapterStatusBeingChanged()).toBeTrue(); - storyEditorStateService.isChangingChapterStatus = jasmine. - createSpy().and.returnValue(false); + storyEditorStateService.isChangingChapterStatus = jasmine + .createSpy() + .and.returnValue(false); expect(component.isChapterStatusBeingChanged()).toBeFalse(); }); describe('on initialization ', () => { - it('should show validation error when story ' + - 'title name is empty', () => { - // Setting story title to be empty. - storyBackendDict.title = ''; - story = Story.createFromBackendDict(storyBackendDict); - let mockStoryInitializedEventEmitter = new EventEmitter(); - - spyOnProperty(storyEditorStateService, 'onStoryInitialized') - .and.returnValue(mockStoryInitializedEventEmitter); - spyOn(storyEditorStateService, 'getStory').and.returnValue(story); - - component.ngOnInit(); - fixture.detectChanges(); - - expect(component.validationIssues.length).toBe(0); - - mockStoryInitializedEventEmitter.emit(); - fixture.detectChanges(); - - expect(component.validationIssues).toContain( - 'Story title should not be empty'); - }); - + it( + 'should show validation error when story ' + 'title name is empty', + () => { + // Setting story title to be empty. + storyBackendDict.title = ''; + story = Story.createFromBackendDict(storyBackendDict); + let mockStoryInitializedEventEmitter = new EventEmitter(); + + spyOnProperty( + storyEditorStateService, + 'onStoryInitialized' + ).and.returnValue(mockStoryInitializedEventEmitter); + spyOn(storyEditorStateService, 'getStory').and.returnValue(story); + + component.ngOnInit(); + fixture.detectChanges(); + + expect(component.validationIssues.length).toBe(0); + + mockStoryInitializedEventEmitter.emit(); + fixture.detectChanges(); + + expect(component.validationIssues).toContain( + 'Story title should not be empty' + ); + } + ); - it('should show validation error when url ' + - 'fragment is empty', () => { + it('should show validation error when url ' + 'fragment is empty', () => { // Setting url fragment to be empty. storyBackendDict.url_fragment = ''; story = Story.createFromBackendDict(storyBackendDict); let mockStoryInitializedEventEmitter = new EventEmitter(); spyOn(storyEditorStateService, 'getStory').and.returnValue(story); - spyOnProperty(storyEditorStateService, 'onStoryInitialized') - .and.returnValue(mockStoryInitializedEventEmitter); + spyOnProperty( + storyEditorStateService, + 'onStoryInitialized' + ).and.returnValue(mockStoryInitializedEventEmitter); component.ngOnInit(); fixture.detectChanges(); @@ -278,124 +305,157 @@ describe('Story editor navbar component', () => { fixture.detectChanges(); expect(component.validationIssues).toContain( - 'Url Fragment should not be empty.'); - }); - - it('should show validation error when we ' + - 'try to add url fragment if it already exists', () => { - story = Story.createFromBackendDict(storyBackendDict); - let mockStoryReinitializedEventEmitter = new EventEmitter(); - - spyOn(storyEditorStateService, 'getStory').and.returnValue(story); - spyOnProperty(storyEditorStateService, 'onStoryInitialized') - .and.returnValue(mockStoryReinitializedEventEmitter); - spyOn(storyEditorStateService, 'getStoryWithUrlFragmentExists') - .and.returnValue(true); - - component.ngOnInit(); - fixture.detectChanges(); - - expect(component.validationIssues.length).toBe(0); - - mockStoryReinitializedEventEmitter.emit(); - fixture.detectChanges(); - - expect(component.validationIssues).toContain( - 'Story URL fragment already exists.'); + 'Url Fragment should not be empty.' + ); }); - it('should show validation error when chapters in story ' + - 'does not have any linked exploration', fakeAsync(() => { - // Setting exploration ID to be empty. - storyBackendDict.story_contents.nodes[0].exploration_id = null; - story = Story.createFromBackendDict(storyBackendDict); - let mockStoryReinitializedEventEmitter = new EventEmitter(); - - spyOn(storyEditorStateService, 'getStory').and.returnValue(story); - spyOnProperty(storyEditorStateService, 'onStoryInitialized') - .and.returnValue(mockStoryReinitializedEventEmitter); - spyOn(storyEditorStateService, 'getStoryWithUrlFragmentExists') - .and.returnValue(true); - - component.ngOnInit(); - fixture.detectChanges(); - - expect(component.explorationValidationIssues.length).toBe(0); - - mockStoryReinitializedEventEmitter.emit(); - tick(); - fixture.detectChanges(); + it( + 'should show validation error when we ' + + 'try to add url fragment if it already exists', + () => { + story = Story.createFromBackendDict(storyBackendDict); + let mockStoryReinitializedEventEmitter = new EventEmitter(); + + spyOn(storyEditorStateService, 'getStory').and.returnValue(story); + spyOnProperty( + storyEditorStateService, + 'onStoryInitialized' + ).and.returnValue(mockStoryReinitializedEventEmitter); + spyOn( + storyEditorStateService, + 'getStoryWithUrlFragmentExists' + ).and.returnValue(true); + + component.ngOnInit(); + fixture.detectChanges(); + + expect(component.validationIssues.length).toBe(0); + + mockStoryReinitializedEventEmitter.emit(); + fixture.detectChanges(); + + expect(component.validationIssues).toContain( + 'Story URL fragment already exists.' + ); + } + ); - expect(component.explorationValidationIssues).toContain( - 'Some chapters don\'t have exploration IDs provided.'); - })); + it( + 'should show validation error when chapters in story ' + + 'does not have any linked exploration', + fakeAsync(() => { + // Setting exploration ID to be empty. + storyBackendDict.story_contents.nodes[0].exploration_id = null; + story = Story.createFromBackendDict(storyBackendDict); + let mockStoryReinitializedEventEmitter = new EventEmitter(); + + spyOn(storyEditorStateService, 'getStory').and.returnValue(story); + spyOnProperty( + storyEditorStateService, + 'onStoryInitialized' + ).and.returnValue(mockStoryReinitializedEventEmitter); + spyOn( + storyEditorStateService, + 'getStoryWithUrlFragmentExists' + ).and.returnValue(true); + + component.ngOnInit(); + fixture.detectChanges(); + + expect(component.explorationValidationIssues.length).toBe(0); + + mockStoryReinitializedEventEmitter.emit(); + tick(); + fixture.detectChanges(); + + expect(component.explorationValidationIssues).toContain( + "Some chapters don't have exploration IDs provided." + ); + }) + ); - it('should validate story without any validation errors ' + - 'on initialization', () => { - story = Story.createFromBackendDict(storyBackendDict); - let mockStoryInitializedEventEmitter = new EventEmitter(); + it( + 'should validate story without any validation errors ' + + 'on initialization', + () => { + story = Story.createFromBackendDict(storyBackendDict); + let mockStoryInitializedEventEmitter = new EventEmitter(); - spyOn(storyEditorStateService, 'getStory').and.returnValue(story); - spyOnProperty(storyEditorStateService, 'onStoryInitialized') - .and.returnValue(mockStoryInitializedEventEmitter); + spyOn(storyEditorStateService, 'getStory').and.returnValue(story); + spyOnProperty( + storyEditorStateService, + 'onStoryInitialized' + ).and.returnValue(mockStoryInitializedEventEmitter); - component.ngOnInit(); - fixture.detectChanges(); + component.ngOnInit(); + fixture.detectChanges(); - expect(component.validationIssues.length).toBe(0); + expect(component.validationIssues.length).toBe(0); - mockStoryInitializedEventEmitter.emit(); - fixture.detectChanges(); + mockStoryInitializedEventEmitter.emit(); + fixture.detectChanges(); - expect(component.validationIssues.length).toBe(0); - }); + expect(component.validationIssues.length).toBe(0); + } + ); - it('should validate story without any validation errors ' + - 'on reinitalization', () => { - story = Story.createFromBackendDict(storyBackendDict); - let mockStoryReinitializedEventEmitter = new EventEmitter(); + it( + 'should validate story without any validation errors ' + + 'on reinitalization', + () => { + story = Story.createFromBackendDict(storyBackendDict); + let mockStoryReinitializedEventEmitter = new EventEmitter(); - spyOnProperty(storyEditorStateService, 'onStoryReinitialized') - .and.returnValue(mockStoryReinitializedEventEmitter); - spyOn(storyEditorStateService, 'getStory').and.returnValue(story); + spyOnProperty( + storyEditorStateService, + 'onStoryReinitialized' + ).and.returnValue(mockStoryReinitializedEventEmitter); + spyOn(storyEditorStateService, 'getStory').and.returnValue(story); - component.ngOnInit(); - fixture.detectChanges(); + component.ngOnInit(); + fixture.detectChanges(); - expect(component.validationIssues.length).toBe(0); + expect(component.validationIssues.length).toBe(0); - mockStoryReinitializedEventEmitter.emit(); - fixture.detectChanges(); + mockStoryReinitializedEventEmitter.emit(); + fixture.detectChanges(); - expect(component.validationIssues.length).toBe(0); - }); + expect(component.validationIssues.length).toBe(0); + } + ); - it('should validate story without any validation errors ' + - 'when undo operation is performed', () => { - story = Story.createFromBackendDict(storyBackendDict); - let mockUndoRedoChangeEventEmitter = new EventEmitter(); + it( + 'should validate story without any validation errors ' + + 'when undo operation is performed', + () => { + story = Story.createFromBackendDict(storyBackendDict); + let mockUndoRedoChangeEventEmitter = new EventEmitter(); - spyOn(storyEditorStateService, 'getStory').and.returnValue(story); - spyOn(undoRedoService, 'getUndoRedoChangeEventEmitter').and.returnValue( - mockUndoRedoChangeEventEmitter); + spyOn(storyEditorStateService, 'getStory').and.returnValue(story); + spyOn(undoRedoService, 'getUndoRedoChangeEventEmitter').and.returnValue( + mockUndoRedoChangeEventEmitter + ); - component.ngOnInit(); - fixture.detectChanges(); + component.ngOnInit(); + fixture.detectChanges(); - expect(component.validationIssues.length).toBe(0); + expect(component.validationIssues.length).toBe(0); - mockUndoRedoChangeEventEmitter.emit(); - fixture.detectChanges(); + mockUndoRedoChangeEventEmitter.emit(); + fixture.detectChanges(); - expect(component.validationIssues.length).toBe(0); - }); + expect(component.validationIssues.length).toBe(0); + } + ); it('should get story node data when the tab is chapter editor', () => { story = Story.createFromBackendDict(storyBackendDict); let getStoryNodeSpy = spyOn(component, 'getStoryNodeData'); let mockStoryInitializedEventEmitter = new EventEmitter(); - spyOnProperty(storyEditorStateService, 'onStoryInitialized'). - and.returnValue(mockStoryInitializedEventEmitter); + spyOnProperty( + storyEditorStateService, + 'onStoryInitialized' + ).and.returnValue(mockStoryInitializedEventEmitter); spyOn(storyEditorStateService, 'getStory').and.returnValue(story); component.ngOnInit(); @@ -414,12 +474,14 @@ describe('Story editor navbar component', () => { let mockStoryInitializedEventEmitter = new EventEmitter(); spyOn(storyEditorStateService, 'getStory').and.returnValue(story); - spyOnProperty(storyEditorStateService, 'onStoryInitialized') - .and.returnValue(mockStoryInitializedEventEmitter); + spyOnProperty( + storyEditorStateService, + 'onStoryInitialized' + ).and.returnValue(mockStoryInitializedEventEmitter); const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ - result: Promise.resolve('success') - } as NgbModalRef); + return { + result: Promise.resolve('success'), + } as NgbModalRef; }); component.ngOnInit(); @@ -508,7 +570,9 @@ describe('Story editor navbar component', () => { component.story = story; let clearChangesSpy = spyOn( - undoRedoService, 'clearChanges').and.callThrough(); + undoRedoService, + 'clearChanges' + ).and.callThrough(); component.discardChanges(); @@ -517,8 +581,10 @@ describe('Story editor navbar component', () => { it('should set switch current tab', () => { let mockStoryInitializedEventEmitter = new EventEmitter(); - spyOnProperty(storyEditorNavigationService, 'onChangeActiveTab') - .and.returnValue(mockStoryInitializedEventEmitter); + spyOnProperty( + storyEditorNavigationService, + 'onChangeActiveTab' + ).and.returnValue(mockStoryInitializedEventEmitter); let getStoryNodeSpy = spyOn(component, 'getStoryNodeData'); component.ngOnInit(); @@ -539,26 +605,30 @@ describe('Story editor navbar component', () => { story = Story.createFromBackendDict(storyBackendDict); component.story = story; spyOn(storyEditorNavigationService, 'getChapterId').and.returnValue( - 'node_1'); + 'node_1' + ); component.getStoryNodeData(); - expect(component.storyNode).toEqual(StoryNode.createFromBackendDict( - storyBackendDict.story_contents.nodes[0])); + expect(component.storyNode).toEqual( + StoryNode.createFromBackendDict(storyBackendDict.story_contents.nodes[0]) + ); }); describe('open a confirmation modal for saving changes ', () => { - it('should save story successfully on' + - 'clicking save draft button', fakeAsync(() => { - const commitMessage = 'commitMessage'; - - story = Story.createFromBackendDict(storyBackendDict); - let mockStoryInitializedEventEmitter = new EventEmitter(); - - spyOn(storyEditorStateService, 'getStory').and.returnValue(story); - const saveChangesSpy = spyOn( - storyEditorStateService, 'saveStory') - .and.callFake((commitMessage, successCallback, errorCallback) => { + it( + 'should save story successfully on' + 'clicking save draft button', + fakeAsync(() => { + const commitMessage = 'commitMessage'; + + story = Story.createFromBackendDict(storyBackendDict); + let mockStoryInitializedEventEmitter = new EventEmitter(); + + spyOn(storyEditorStateService, 'getStory').and.returnValue(story); + const saveChangesSpy = spyOn( + storyEditorStateService, + 'saveStory' + ).and.callFake((commitMessage, successCallback, errorCallback) => { if (commitMessage !== null) { successCallback(); } else { @@ -566,43 +636,51 @@ describe('Story editor navbar component', () => { } return true; }); - const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ( - { componentInstance: MockNgbModalRef, - result: Promise.resolve(commitMessage) - } as NgbModalRef); - }); - - component.ngOnInit(); - mockStoryInitializedEventEmitter.emit(); - storyEditorStateService.setStory(story); - fixture.detectChanges(); + const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + componentInstance: MockNgbModalRef, + result: Promise.resolve(commitMessage), + } as NgbModalRef; + }); - expect(modalSpy).not.toHaveBeenCalled(); - expect(saveChangesSpy).not.toHaveBeenCalled(); + component.ngOnInit(); + mockStoryInitializedEventEmitter.emit(); + storyEditorStateService.setStory(story); + fixture.detectChanges(); - component.saveChanges(); - tick(); - fixture.detectChanges(); + expect(modalSpy).not.toHaveBeenCalled(); + expect(saveChangesSpy).not.toHaveBeenCalled(); - expect(modalSpy).toHaveBeenCalled(); - expect(saveChangesSpy).toHaveBeenCalled(); - })); + component.saveChanges(); + tick(); + fixture.detectChanges(); - it('should show error message if the story was not saved' + - 'on clicking save draft button', fakeAsync(() => { - story = Story.createFromBackendDict(storyBackendDict); - const commitMessage = null; - let mockStoryInitializedEventEmitter = new EventEmitter(); + expect(modalSpy).toHaveBeenCalled(); + expect(saveChangesSpy).toHaveBeenCalled(); + }) + ); - spyOn(storyEditorStateService, 'getStory').and.returnValue(story); - spyOnProperty(storyEditorStateService, 'onStoryInitialized') - .and.returnValue(mockStoryInitializedEventEmitter); - const alertsSpy = spyOn( - alertsService, 'addInfoMessage').and.callThrough(); - const saveChangesSpy = spyOn( - storyEditorStateService, 'saveStory') - .and.callFake((commitMessage, successCallback, errorCallback) => { + it( + 'should show error message if the story was not saved' + + 'on clicking save draft button', + fakeAsync(() => { + story = Story.createFromBackendDict(storyBackendDict); + const commitMessage = null; + let mockStoryInitializedEventEmitter = new EventEmitter(); + + spyOn(storyEditorStateService, 'getStory').and.returnValue(story); + spyOnProperty( + storyEditorStateService, + 'onStoryInitialized' + ).and.returnValue(mockStoryInitializedEventEmitter); + const alertsSpy = spyOn( + alertsService, + 'addInfoMessage' + ).and.callThrough(); + const saveChangesSpy = spyOn( + storyEditorStateService, + 'saveStory' + ).and.callFake((commitMessage, successCallback, errorCallback) => { if (commitMessage !== null) { successCallback(); } else { @@ -610,54 +688,60 @@ describe('Story editor navbar component', () => { } return true; }); - const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ( - { componentInstance: MockNgbModalRef, - result: Promise.resolve(commitMessage) - } as NgbModalRef); - }); - - component.ngOnInit(); - mockStoryInitializedEventEmitter.emit(); - storyEditorStateService.setStory(story); - fixture.detectChanges(); - - expect(modalSpy).not.toHaveBeenCalled(); - expect(saveChangesSpy).not.toHaveBeenCalled(); - - component.commitMessage = ''; - component.saveChanges(); - tick(); - fixture.detectChanges(); + const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + componentInstance: MockNgbModalRef, + result: Promise.resolve(commitMessage), + } as NgbModalRef; + }); - expect(modalSpy).toHaveBeenCalled(); - expect(saveChangesSpy).toHaveBeenCalled(); - expect(alertsSpy).toHaveBeenCalledWith( - 'Expected a commit message but received none.', 5000); - })); + component.ngOnInit(); + mockStoryInitializedEventEmitter.emit(); + storyEditorStateService.setStory(story); + fixture.detectChanges(); + + expect(modalSpy).not.toHaveBeenCalled(); + expect(saveChangesSpy).not.toHaveBeenCalled(); + + component.commitMessage = ''; + component.saveChanges(); + tick(); + fixture.detectChanges(); + + expect(modalSpy).toHaveBeenCalled(); + expect(saveChangesSpy).toHaveBeenCalled(); + expect(alertsSpy).toHaveBeenCalledWith( + 'Expected a commit message but received none.', + 5000 + ); + }) + ); it('should not save story on clicking cancel button', fakeAsync(() => { story = Story.createFromBackendDict(storyBackendDict); let mockStoryInitializedEventEmitter = new EventEmitter(); spyOn(storyEditorStateService, 'getStory').and.returnValue(story); - spyOnProperty(storyEditorStateService, 'onStoryInitialized') - .and.returnValue(mockStoryInitializedEventEmitter); + spyOnProperty( + storyEditorStateService, + 'onStoryInitialized' + ).and.returnValue(mockStoryInitializedEventEmitter); const saveChangesSpy = spyOn( - storyEditorStateService, 'saveStory') - .and.callFake((commitMessage, successCallback, errorCallback) => { - if (commitMessage !== null) { - successCallback(); - } else { - errorCallback('Expected a commit message but received none.'); - } - return true; - }); + storyEditorStateService, + 'saveStory' + ).and.callFake((commitMessage, successCallback, errorCallback) => { + if (commitMessage !== null) { + successCallback(); + } else { + errorCallback('Expected a commit message but received none.'); + } + return true; + }); const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ( - { componentInstance: MockNgbModalRef, - result: Promise.reject() - } as NgbModalRef); + return { + componentInstance: MockNgbModalRef, + result: Promise.reject(), + } as NgbModalRef; }); component.ngOnInit(); @@ -678,23 +762,28 @@ describe('Story editor navbar component', () => { }); it('should change chapter status to Draft or Ready To Publish', () => { - component.storyNode = StoryNode. - createFromBackendDict(storyBackendDict.story_contents.nodes[1]); + component.storyNode = StoryNode.createFromBackendDict( + storyBackendDict.story_contents.nodes[1] + ); component.story = Story.createFromBackendDict(storyBackendDict); - spyOn(storyEditorStateService, 'getStory').and.returnValue( - component.story); + spyOn(storyEditorStateService, 'getStory').and.returnValue(component.story); let saveChapterSpy = spyOn( - storyEditorStateService, 'saveChapter').and.callThrough(); + storyEditorStateService, + 'saveChapter' + ).and.callThrough(); let storyNodeStatusSpy = spyOn(storyUpdateService, 'setStoryNodeStatus'); let chapterStatusChangingSpy = spyOn( - storyEditorStateService, 'setChapterStatusIsChanging'); + storyEditorStateService, + 'setChapterStatusIsChanging' + ); const saveChangesSpy = spyOn( - storyEditorStateService, 'saveStory') - .and.callFake((commitMessage, successCallback, errorCallback) => { - storyEditorStateService.setChapterStatusIsChanging(false); - errorCallback(commitMessage); - return true; - }); + storyEditorStateService, + 'saveStory' + ).and.callFake((commitMessage, successCallback, errorCallback) => { + storyEditorStateService.setChapterStatusIsChanging(false); + errorCallback(commitMessage); + return true; + }); component.changeChapterStatus('Draft'); expect(saveChapterSpy).toHaveBeenCalled(); @@ -705,23 +794,26 @@ describe('Story editor navbar component', () => { it('should save changes in Ready To Publish chapter', fakeAsync(() => { component.storyNode = StoryNode.createFromBackendDict( - storyBackendDict.story_contents.nodes[1]); - storyEditorStateService.isCurrentNodePublishable = jasmine. - createSpy().and.returnValue(true); + storyBackendDict.story_contents.nodes[1] + ); + storyEditorStateService.isCurrentNodePublishable = jasmine + .createSpy() + .and.returnValue(true); let saveChangesSpy = spyOn(component, 'saveChanges'); let storyNodeStatusSpy = spyOn(storyUpdateService, 'setStoryNodeStatus'); const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ( - { componentInstance: MockNgbModalRef, - result: Promise.resolve('BAD_CONTENT') - } as NgbModalRef); + return { + componentInstance: MockNgbModalRef, + result: Promise.resolve('BAD_CONTENT'), + } as NgbModalRef; }); component.saveChangesInReadyToPublishChapter(); expect(saveChangesSpy).toHaveBeenCalled(); - storyEditorStateService.isCurrentNodePublishable = jasmine. - createSpy().and.returnValue(false); + storyEditorStateService.isCurrentNodePublishable = jasmine + .createSpy() + .and.returnValue(false); component.saveChangesInReadyToPublishChapter(); tick(); expect(modalSpy).toHaveBeenCalled(); @@ -735,22 +827,33 @@ describe('Story editor navbar component', () => { component.story = story; spyOn( storyEditorStateService, - 'getSelectedChapterIndexInPublishUptoDropdown').and.returnValue(1); + 'getSelectedChapterIndexInPublishUptoDropdown' + ).and.returnValue(1); let storyNodeFirstPublicationDateSpy = spyOn( - storyUpdateService, 'setStoryNodeFirstPublicationDateMsecs'); + storyUpdateService, + 'setStoryNodeFirstPublicationDateMsecs' + ); let chapterStatusChangingSpy = spyOn( - storyEditorStateService, 'setChapterStatusIsChanging'); + storyEditorStateService, + 'setChapterStatusIsChanging' + ); let storyNodeStatusSpy = spyOn(storyUpdateService, 'setStoryNodeStatus'); let storyNodeUnpublishingReasonSpy = spyOn( - storyUpdateService, 'setStoryNodeUnpublishingReason'); + storyUpdateService, + 'setStoryNodeUnpublishingReason' + ); let publishStorySpy = spyOn(component, 'publishStory'); let loadStorySpy = spyOn( - storyEditorStateService, 'loadStory').and.returnValue(); + storyEditorStateService, + 'loadStory' + ).and.returnValue(); let alertServiceSpy = spyOn( - alertsService, 'addInfoMessage').and.callThrough(); + alertsService, + 'addInfoMessage' + ).and.callThrough(); let successCallbackFunctionIsCalled: boolean = false; - let saveStorySpy = spyOn(storyEditorStateService, 'saveStory') - .and.callFake((commitMessage, successCallback, errorCallback) => { + let saveStorySpy = spyOn(storyEditorStateService, 'saveStory').and.callFake( + (commitMessage, successCallback, errorCallback) => { if (successCallbackFunctionIsCalled) { successCallback(); } else { @@ -758,7 +861,8 @@ describe('Story editor navbar component', () => { } storyEditorStateService.setChapterStatusIsChanging(false); return true; - }); + } + ); component.changeChapterStatus('Published'); expect(storyNodeStatusSpy).toHaveBeenCalledTimes(1); @@ -767,11 +871,14 @@ describe('Story editor navbar component', () => { expect(alertServiceSpy).toHaveBeenCalled(); expect(saveStorySpy).toHaveBeenCalled(); - - component.story.getStoryContents().getNodes()[0]. - setStatus('Ready To Publish'); - component.story.getStoryContents().getNodes()[0]. - setFirstPublicationDateMsecs(null); + component.story + .getStoryContents() + .getNodes()[0] + .setStatus('Ready To Publish'); + component.story + .getStoryContents() + .getNodes()[0] + .setFirstPublicationDateMsecs(null); successCallbackFunctionIsCalled = true; component.changeChapterStatus('Published'); @@ -790,36 +897,48 @@ describe('Story editor navbar component', () => { component.story = story; spyOn( storyEditorStateService, - 'getSelectedChapterIndexInPublishUptoDropdown').and.returnValue(0); + 'getSelectedChapterIndexInPublishUptoDropdown' + ).and.returnValue(0); let storyNodePlannedPublicationDateSpy = spyOn( - storyUpdateService, 'setStoryNodePlannedPublicationDateMsecs'); + storyUpdateService, + 'setStoryNodePlannedPublicationDateMsecs' + ); let chapterStatusChangingSpy = spyOn( - storyEditorStateService, 'setChapterStatusIsChanging'); + storyEditorStateService, + 'setChapterStatusIsChanging' + ); let storyNodeStatusSpy = spyOn(storyUpdateService, 'setStoryNodeStatus'); let storyNodeUnpublishingReasonSpy = spyOn( - storyUpdateService, 'setStoryNodeUnpublishingReason'); + storyUpdateService, + 'setStoryNodeUnpublishingReason' + ); let unpublishStorySpy = spyOn(component, 'unpublishStory'); let loadStorySpy = spyOn( - storyEditorStateService, 'loadStory').and.returnValue(); + storyEditorStateService, + 'loadStory' + ).and.returnValue(); let alertServiceSpy = spyOn( - alertsService, 'addInfoMessage').and.callThrough(); + alertsService, + 'addInfoMessage' + ).and.callThrough(); let successCallbackFunctionIsCalled: boolean = false; const saveStorySpy = spyOn( - storyEditorStateService, 'saveStory') - .and.callFake((commitMessage, successCallback, errorCallback) => { - if (successCallbackFunctionIsCalled) { - successCallback(); - } else { - errorCallback('Error'); - } - storyEditorStateService.setChapterStatusIsChanging(false); - return true; - }); + storyEditorStateService, + 'saveStory' + ).and.callFake((commitMessage, successCallback, errorCallback) => { + if (successCallbackFunctionIsCalled) { + successCallback(); + } else { + errorCallback('Error'); + } + storyEditorStateService.setChapterStatusIsChanging(false); + return true; + }); const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ( - { componentInstance: MockNgbModalRef, - result: Promise.resolve('BAD_CONTENT') - } as NgbModalRef); + return { + componentInstance: MockNgbModalRef, + result: Promise.resolve('BAD_CONTENT'), + } as NgbModalRef; }); component.story.getStoryContents().getNodes()[1].setStatus('Published'); @@ -836,8 +955,10 @@ describe('Story editor navbar component', () => { expect(alertServiceSpy).toHaveBeenCalled(); component.story.getStoryContents().getNodes()[1].setStatus('Published'); - component.story.getStoryContents().getNodes()[1]. - setPlannedPublicationDateMsecs(null); + component.story + .getStoryContents() + .getNodes()[1] + .setPlannedPublicationDateMsecs(null); successCallbackFunctionIsCalled = true; component.changeChapterStatus('Published'); @@ -851,9 +972,8 @@ describe('Story editor navbar component', () => { expect(saveStorySpy).toHaveBeenCalled(); expect(loadStorySpy).toHaveBeenCalled(); - storyEditorStateService.getSelectedChapterIndexInPublishUptoDropdown = ( - jasmine.createSpy().and.returnValue(-1) - ); + storyEditorStateService.getSelectedChapterIndexInPublishUptoDropdown = + jasmine.createSpy().and.returnValue(-1); component.story.getStoryContents().getNodes()[1].setStatus('Published'); component.changeChapterStatus('Published'); diff --git a/core/templates/pages/story-editor-page/navbar/story-editor-navbar.component.ts b/core/templates/pages/story-editor-page/navbar/story-editor-navbar.component.ts index 6bd63db149d9..29b0d3c3daea 100644 --- a/core/templates/pages/story-editor-page/navbar/story-editor-navbar.component.ts +++ b/core/templates/pages/story-editor-page/navbar/story-editor-navbar.component.ts @@ -16,27 +16,27 @@ * @fileoverview Component for the navbar of the story editor. */ -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { UndoRedoService } from 'domain/editor/undo_redo/undo-redo.service'; -import { EditableStoryBackendApiService } from 'domain/story/editable-story-backend-api.service'; -import { StoryValidationService } from 'domain/story/story-validation.service'; -import { Story } from 'domain/story/story.model'; -import { StoryNode } from 'domain/story/story-node.model'; -import { Subscription } from 'rxjs'; -import { AlertsService } from 'services/alerts.service'; -import { StoryEditorStateService } from '../services/story-editor-state.service'; -import { StoryUpdateService } from 'domain/story/story-update.service'; -import { StoryEditorSaveModalComponent } from '../modal-templates/story-editor-save-modal.component'; -import { StoryEditorUnpublishModalComponent } from '../modal-templates/story-editor-unpublish-modal.component'; -import { Component, Input, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { StoryEditorNavigationService } from '../services/story-editor-navigation.service'; -import { PlatformFeatureService } from 'services/platform-feature.service'; -import { DraftChapterConfirmationModalComponent } from '../modal-templates/draft-chapter-confirmation-modal.component'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {UndoRedoService} from 'domain/editor/undo_redo/undo-redo.service'; +import {EditableStoryBackendApiService} from 'domain/story/editable-story-backend-api.service'; +import {StoryValidationService} from 'domain/story/story-validation.service'; +import {Story} from 'domain/story/story.model'; +import {StoryNode} from 'domain/story/story-node.model'; +import {Subscription} from 'rxjs'; +import {AlertsService} from 'services/alerts.service'; +import {StoryEditorStateService} from '../services/story-editor-state.service'; +import {StoryUpdateService} from 'domain/story/story-update.service'; +import {StoryEditorSaveModalComponent} from '../modal-templates/story-editor-save-modal.component'; +import {StoryEditorUnpublishModalComponent} from '../modal-templates/story-editor-unpublish-modal.component'; +import {Component, Input, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {StoryEditorNavigationService} from '../services/story-editor-navigation.service'; +import {PlatformFeatureService} from 'services/platform-feature.service'; +import {DraftChapterConfirmationModalComponent} from '../modal-templates/draft-chapter-confirmation-modal.component'; @Component({ selector: 'oppia-story-editor-navbar', - templateUrl: './story-editor-navbar.component.html' + templateUrl: './story-editor-navbar.component.html', }) export class StoryEditorNavbarComponent implements OnInit { // These properties are initialized using Angular lifecycle hooks @@ -73,9 +73,8 @@ export class StoryEditorNavbarComponent implements OnInit { explorationValidationIssues: string[] = []; isSerialChapterFeatureFlagEnabled(): boolean { - return ( - this.platformFeatureService. - status.SerialChapterLaunchCurriculumAdminView.isEnabled); + return this.platformFeatureService.status + .SerialChapterLaunchCurriculumAdminView.isEnabled; } isStoryPublished(): boolean { @@ -102,18 +101,17 @@ export class StoryEditorNavbarComponent implements OnInit { return ( this.validationIssues.length + this.explorationValidationIssues.length + - this.prepublishValidationIssues.length); + this.prepublishValidationIssues.length + ); } isStorySaveable(): boolean { if (this.storyEditorStateService.isStoryPublished()) { return ( - this.getChangeListLength() > 0 && - this.getTotalWarningsCount() === 0); + this.getChangeListLength() > 0 && this.getTotalWarningsCount() === 0 + ); } - return ( - this.getChangeListLength() > 0 && - this.getWarningsCount() === 0); + return this.getChangeListLength() > 0 && this.getWarningsCount() === 0; } isChapterPublishable(): boolean { @@ -133,9 +131,10 @@ export class StoryEditorNavbarComponent implements OnInit { } getAllStoryWarnings(): string { - return this.validationIssues.concat( - this.explorationValidationIssues - ).concat(this.prepublishValidationIssues).join('\n'); + return this.validationIssues + .concat(this.explorationValidationIssues) + .concat(this.prepublishValidationIssues) + .join('\n'); } discardChanges(): void { @@ -152,32 +151,32 @@ export class StoryEditorNavbarComponent implements OnInit { if (this.currentTab === 'chapter_editor') { this.getStoryNodeData(); } - let skillIdsInTopic = ( - this.storyEditorStateService.getSkillSummaries().map( - skill => skill.id)); + let skillIdsInTopic = this.storyEditorStateService + .getSkillSummaries() + .map(skill => skill.id); if (this.validationIssues.length === 0 && nodes.length > 0) { - let prerequisiteSkillValidationIssues = ( - this.storyValidationService - .validatePrerequisiteSkillsInStoryContents( - skillIdsInTopic, this.story.getStoryContents())); - this.validationIssues = ( - this.validationIssues.concat( - prerequisiteSkillValidationIssues)); + let prerequisiteSkillValidationIssues = + this.storyValidationService.validatePrerequisiteSkillsInStoryContents( + skillIdsInTopic, + this.story.getStoryContents() + ); + this.validationIssues = this.validationIssues.concat( + prerequisiteSkillValidationIssues + ); } if (this.storyEditorStateService.getStoryWithUrlFragmentExists()) { - this.validationIssues.push( - 'Story URL fragment already exists.'); + this.validationIssues.push('Story URL fragment already exists.'); } this.forceValidateExplorations = true; this._validateExplorations(); - let storyPrepublishValidationIssues = ( - this.story.prepublishValidate()); - let nodePrepublishValidationIssues = ( - Array.prototype.concat.apply([], nodes.map( - (node) => node.prepublishValidate()))); - this.prepublishValidationIssues = ( - storyPrepublishValidationIssues.concat( - nodePrepublishValidationIssues)); + let storyPrepublishValidationIssues = this.story.prepublishValidate(); + let nodePrepublishValidationIssues = Array.prototype.concat.apply( + [], + nodes.map(node => node.prepublishValidate()) + ); + this.prepublishValidationIssues = storyPrepublishValidationIssues.concat( + nodePrepublishValidationIssues + ); } private _validateExplorations(): void { @@ -186,7 +185,8 @@ export class StoryEditorNavbarComponent implements OnInit { if ( this.storyEditorStateService.areAnyExpIdsChanged() || - this.forceValidateExplorations) { + this.forceValidateExplorations + ) { this.explorationValidationIssues = []; for (let i = 0; i < nodes.length; i++) { let explorationId = nodes[i].getExplorationId(); @@ -194,92 +194,106 @@ export class StoryEditorNavbarComponent implements OnInit { explorationIds.push(explorationId); } else { this.explorationValidationIssues.push( - 'Some chapters don\'t have exploration IDs provided.'); + "Some chapters don't have exploration IDs provided." + ); } } this.forceValidateExplorations = false; if (explorationIds.length > 0) { - this.editableStoryBackendApiService.validateExplorationsAsync( - this.story.getId(), explorationIds - ).then((validationIssues) => { - this.explorationValidationIssues = - this.explorationValidationIssues.concat(validationIssues); - }); + this.editableStoryBackendApiService + .validateExplorationsAsync(this.story.getId(), explorationIds) + .then(validationIssues => { + this.explorationValidationIssues = + this.explorationValidationIssues.concat(validationIssues); + }); } } this.storyEditorStateService.resetExpIdsChanged(); } saveChanges(): void { - const modalRef = this.ngbModal.open( - StoryEditorSaveModalComponent, - { backdrop: 'static' }); - modalRef.componentInstance.bindedMessage = this.commitMessage; - modalRef.result.then((commitMessage) => { - this.storyEditorStateService.saveStory( - commitMessage, () => { - }, (errorMessage: string) => { - this.alertsService.addInfoMessage(errorMessage, 5000); - } - ); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. + const modalRef = this.ngbModal.open(StoryEditorSaveModalComponent, { + backdrop: 'static', }); + modalRef.componentInstance.bindedMessage = this.commitMessage; + modalRef.result.then( + commitMessage => { + this.storyEditorStateService.saveStory( + commitMessage, + () => {}, + (errorMessage: string) => { + this.alertsService.addInfoMessage(errorMessage, 5000); + } + ); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } saveChangesInReadyToPublishChapter(): void { if (!this.isChapterPublishable()) { const modalRef = this.ngbModal.open( DraftChapterConfirmationModalComponent, - { backdrop: 'static' }); - modalRef.result.then(() => { - this.storyUpdateService.setStoryNodeStatus( - this.story, this.storyNode.getId(), 'Draft'); - this.saveChanges(); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + {backdrop: 'static'} + ); + modalRef.result.then( + () => { + this.storyUpdateService.setStoryNodeStatus( + this.story, + this.storyNode.getId(), + 'Draft' + ); + this.saveChanges(); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } else { this.saveChanges(); } } publishStory(): void { - this.storyEditorStateService.changeStoryPublicationStatus( - true, () => { - this.storyIsPublished = - this.storyEditorStateService.isStoryPublished(); - }); + this.storyEditorStateService.changeStoryPublicationStatus(true, () => { + this.storyIsPublished = this.storyEditorStateService.isStoryPublished(); + }); } unpublishStory(): void { - this.ngbModal.open( - StoryEditorUnpublishModalComponent, - { backdrop: 'static' } - ).result.then(() => { - this.storyEditorStateService.changeStoryPublicationStatus( - false, () => { - this.storyIsPublished = - this.storyEditorStateService.isStoryPublished(); - this.forceValidateExplorations = true; - this._validateStory(); - }); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + this.ngbModal + .open(StoryEditorUnpublishModalComponent, {backdrop: 'static'}) + .result.then( + () => { + this.storyEditorStateService.changeStoryPublicationStatus( + false, + () => { + this.storyIsPublished = + this.storyEditorStateService.isStoryPublished(); + this.forceValidateExplorations = true; + this._validateStory(); + } + ); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } changeChapterStatus(newStatus: string): void { this.storyEditorStateService.setChapterStatusIsChanging(true); if (newStatus === 'Published') { - let selectedChapterIndexInPublishUptoDropdown = this. - storyEditorStateService.getSelectedChapterIndexInPublishUptoDropdown(); + let selectedChapterIndexInPublishUptoDropdown = + this.storyEditorStateService.getSelectedChapterIndexInPublishUptoDropdown(); let nodes = this.story.getStoryContents().getLinearNodesList(); let lastPublishedChapterIndex = -1; for (let i = 0; i < nodes.length; i++) { @@ -288,67 +302,102 @@ export class StoryEditorNavbarComponent implements OnInit { } } - if (selectedChapterIndexInPublishUptoDropdown < - lastPublishedChapterIndex) { + if ( + selectedChapterIndexInPublishUptoDropdown < lastPublishedChapterIndex + ) { const modalRef = this.ngbModal.open( StoryEditorUnpublishModalComponent, - { backdrop: 'static' } + {backdrop: 'static'} ); let unpublishedChapters = []; - for (let i = Number(selectedChapterIndexInPublishUptoDropdown) + 1; - i <= lastPublishedChapterIndex; i++) { + for ( + let i = Number(selectedChapterIndexInPublishUptoDropdown) + 1; + i <= lastPublishedChapterIndex; + i++ + ) { unpublishedChapters.push(Number(i) + 1); } modalRef.componentInstance.unpublishedChapters = unpublishedChapters; - modalRef.result.then((unpublishingReason) => { - for (let i = Number(selectedChapterIndexInPublishUptoDropdown) + 1; - i <= lastPublishedChapterIndex; i++) { - this.storyUpdateService.setStoryNodeStatus( - this.story, nodes[i].getId(), 'Draft'); - this.storyUpdateService.setStoryNodeUnpublishingReason( - this.story, nodes[i].getId(), unpublishingReason); - if (nodes[i].getPlannedPublicationDateMsecs()) { - this.storyUpdateService.setStoryNodePlannedPublicationDateMsecs( - this.story, nodes[i].getId(), null); + modalRef.result.then( + unpublishingReason => { + for ( + let i = Number(selectedChapterIndexInPublishUptoDropdown) + 1; + i <= lastPublishedChapterIndex; + i++ + ) { + this.storyUpdateService.setStoryNodeStatus( + this.story, + nodes[i].getId(), + 'Draft' + ); + this.storyUpdateService.setStoryNodeUnpublishingReason( + this.story, + nodes[i].getId(), + unpublishingReason + ); + if (nodes[i].getPlannedPublicationDateMsecs()) { + this.storyUpdateService.setStoryNodePlannedPublicationDateMsecs( + this.story, + nodes[i].getId(), + null + ); + } } - } - if (Number(selectedChapterIndexInPublishUptoDropdown) === -1) { - this.unpublishStory(); - } - this.storyEditorStateService.saveStory( - 'Unpublished chapters', () => { - this.storyEditorStateService.loadStory(this.story.getId()); - this._validateStory(); - }, (errorMessage: string) => { - this.alertsService.addInfoMessage(errorMessage, 5000); + if (Number(selectedChapterIndexInPublishUptoDropdown) === -1) { + this.unpublishStory(); } - ); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + this.storyEditorStateService.saveStory( + 'Unpublished chapters', + () => { + this.storyEditorStateService.loadStory(this.story.getId()); + this._validateStory(); + }, + (errorMessage: string) => { + this.alertsService.addInfoMessage(errorMessage, 5000); + } + ); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } else { - for (let i = Number(lastPublishedChapterIndex) + 1; - i <= selectedChapterIndexInPublishUptoDropdown; i++) { + for ( + let i = Number(lastPublishedChapterIndex) + 1; + i <= selectedChapterIndexInPublishUptoDropdown; + i++ + ) { this.storyUpdateService.setStoryNodeStatus( - this.story, nodes[i].getId(), 'Published'); + this.story, + nodes[i].getId(), + 'Published' + ); this.storyUpdateService.setStoryNodeUnpublishingReason( - this.story, nodes[i].getId(), null); + this.story, + nodes[i].getId(), + null + ); if (nodes[i].getFirstPublicationDateMsecs() === null) { let currentDate = new Date(); this.storyUpdateService.setStoryNodeFirstPublicationDateMsecs( - this.story, nodes[i].getId(), currentDate.getTime()); + this.story, + nodes[i].getId(), + currentDate.getTime() + ); } } if (lastPublishedChapterIndex === -1) { this.publishStory(); } this.storyEditorStateService.saveStory( - 'Published chapters', () => { + 'Published chapters', + () => { this.storyEditorStateService.loadStory(this.story.getId()); this._validateStory(); - }, (errorMessage: string) => { + }, + (errorMessage: string) => { this.alertsService.addInfoMessage(errorMessage, 5000); } ); @@ -358,12 +407,18 @@ export class StoryEditorNavbarComponent implements OnInit { let oldStatus = this.storyNode.getStatus(); this.storyUpdateService.setStoryNodeStatus( - this.story, this.storyNode.getId(), newStatus); + this.story, + this.storyNode.getId(), + newStatus + ); this.storyEditorStateService.saveChapter( + () => {}, () => { - }, () => { this.storyUpdateService.setStoryNodeStatus( - this.story, this.storyNode.getId(), oldStatus); + this.story, + this.storyNode.getId(), + oldStatus + ); } ); } @@ -395,28 +450,30 @@ export class StoryEditorNavbarComponent implements OnInit { getStoryNodeData(): void { let nodeId = this.storyEditorNavigationService.getChapterId(); let nodeIndex = this.story.getStoryContents().getNodeIndex(nodeId); - this.storyNode = this.story.getStoryContents(). - getLinearNodesList()[nodeIndex]; + this.storyNode = this.story.getStoryContents().getLinearNodesList()[ + nodeIndex + ]; } ngOnInit(): void { this.directiveSubscriptions.add( - this.storyEditorStateService.onStoryInitialized.subscribe( - () => this._validateStory() - )); + this.storyEditorStateService.onStoryInitialized.subscribe(() => + this._validateStory() + ) + ); this.directiveSubscriptions.add( - this.storyEditorStateService.onStoryReinitialized.subscribe( - () => this._validateStory() - )); + this.storyEditorStateService.onStoryReinitialized.subscribe(() => + this._validateStory() + ) + ); this.directiveSubscriptions.add( - this.storyEditorNavigationService.onChangeActiveTab.subscribe( - (tab) => { - this.currentTab = tab; - if (tab === 'chapter_editor') { - this.getStoryNodeData(); - } + this.storyEditorNavigationService.onChangeActiveTab.subscribe(tab => { + this.currentTab = tab; + if (tab === 'chapter_editor') { + this.getStoryNodeData(); } - )); + }) + ); this.forceValidateExplorations = true; this.warningsAreShown = false; this.activeTab = this.EDITOR; @@ -427,9 +484,9 @@ export class StoryEditorNavbarComponent implements OnInit { this.validationIssues = []; this.prepublishValidationIssues = []; this.directiveSubscriptions.add( - this.undoRedoService.getUndoRedoChangeEventEmitter().subscribe( - () => this._validateStory() - ) + this.undoRedoService + .getUndoRedoChangeEventEmitter() + .subscribe(() => this._validateStory()) ); } @@ -438,5 +495,9 @@ export class StoryEditorNavbarComponent implements OnInit { } } -angular.module('oppia').directive('oppiaStoryEditorNavbar', - downgradeComponent({component: StoryEditorNavbarComponent})); +angular + .module('oppia') + .directive( + 'oppiaStoryEditorNavbar', + downgradeComponent({component: StoryEditorNavbarComponent}) + ); diff --git a/core/templates/pages/story-editor-page/services/story-editor-navigation.service.spec.ts b/core/templates/pages/story-editor-page/services/story-editor-navigation.service.spec.ts index 69dbc6102b76..32bb60e86690 100644 --- a/core/templates/pages/story-editor-page/services/story-editor-navigation.service.spec.ts +++ b/core/templates/pages/story-editor-page/services/story-editor-navigation.service.spec.ts @@ -16,19 +16,20 @@ * @fileoverview Unit tests for StoryEditorNavigationService. */ -import { TestBed } from '@angular/core/testing'; +import {TestBed} from '@angular/core/testing'; -import { StoryEditorNavigationService } from - 'pages/story-editor-page/services/story-editor-navigation.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import {StoryEditorNavigationService} from 'pages/story-editor-page/services/story-editor-navigation.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; describe('Story editor navigation service', () => { let windowRef: WindowRef; let sens: StoryEditorNavigationService; let sampleHash = '/chapter_editor/node_1'; let pathname = '/chapter_editor'; - let mockLocation: - Pick; + let mockLocation: Pick< + Location, + 'hash' | 'href' | 'origin' | 'pathname' | 'search' + >; let origin = 'http://sample.com'; beforeEach(() => { @@ -37,37 +38,40 @@ describe('Story editor navigation service', () => { origin: origin, pathname: pathname, hash: sampleHash, - search: '' + search: '', }; windowRef = TestBed.get(WindowRef); sens = TestBed.get(StoryEditorNavigationService); }); - it('should return the active tab', function() { + it('should return the active tab', function () { expect(sens.getActiveTab()).toEqual('story_editor'); }); - it('should set the chapter id', function() { + it('should set the chapter id', function () { sens.setChapterId('node_id_1'); expect(sens.getChapterId()).toEqual('node_id_1'); }); - it('should navigate to chapter editor with the given id', function() { + it('should navigate to chapter editor with the given id', function () { sens.navigateToChapterEditorWithId('node_id_1', 1); expect(sens.getChapterId()).toEqual('node_id_1'); expect(sens.getChapterIndex()).toEqual(1); }); - it('should navigate to story editor', function() { + it('should navigate to story editor', function () { sens.navigateToStoryEditor(); expect(sens.getActiveTab()).toEqual('story_editor'); }); - it('should return true if current tab is chapter editor tab', function() { - spyOnProperty(windowRef, 'nativeWindow').and.callFake(() => ({ - location: mockLocation as Location - } as Window)); + it('should return true if current tab is chapter editor tab', function () { + spyOnProperty(windowRef, 'nativeWindow').and.callFake( + () => + ({ + location: mockLocation as Location, + }) as Window + ); expect(sens.checkIfPresentInChapterEditor()).toEqual(true); mockLocation.hash = 'story/'; @@ -75,35 +79,40 @@ describe('Story editor navigation service', () => { mockLocation.hash = '/chapter_editor/node_1'; }); - it('should return false if the active tab is not chapter editor tab', - function() { - spyOnProperty(windowRef, 'nativeWindow').and.callFake(() => ({ - location: mockLocation as Location - } as Window)); - expect(sens.checkIfPresentInChapterEditor()).toEqual(true); + it('should return false if the active tab is not chapter editor tab', function () { + spyOnProperty(windowRef, 'nativeWindow').and.callFake( + () => + ({ + location: mockLocation as Location, + }) as Window + ); + expect(sens.checkIfPresentInChapterEditor()).toEqual(true); - mockLocation.hash = 'story/'; - expect(sens.checkIfPresentInChapterEditor()).toEqual(false); - mockLocation.hash = '/chapter_editor/node_1'; - }); + mockLocation.hash = 'story/'; + expect(sens.checkIfPresentInChapterEditor()).toEqual(false); + mockLocation.hash = '/chapter_editor/node_1'; + }); - it('should return true if url is in story preview', function() { + it('should return true if url is in story preview', function () { mockLocation.hash = '/story_preview/'; - spyOnProperty(windowRef, 'nativeWindow').and.callFake(() => ({ - location: mockLocation as Location - } as Window)); + spyOnProperty(windowRef, 'nativeWindow').and.callFake( + () => + ({ + location: mockLocation as Location, + }) as Window + ); expect(sens.checkIfPresentInStoryPreviewTab()).toEqual(true); mockLocation.hash = '/chapter_editor/'; expect(sens.checkIfPresentInStoryPreviewTab()).toEqual(false); }); - it('should navigate to story preview tab', function() { + it('should navigate to story preview tab', function () { sens.navigateToStoryPreviewTab(); expect(sens.getActiveTab()).toEqual('story_preview'); }); - it('should navigate to chapter editor', function() { + it('should navigate to chapter editor', function () { sens.navigateToChapterEditor(); expect(sens.getActiveTab()).toEqual('chapter_editor'); }); diff --git a/core/templates/pages/story-editor-page/services/story-editor-navigation.service.ts b/core/templates/pages/story-editor-page/services/story-editor-navigation.service.ts index b9f43d2ed0cb..7ca0bbdf6942 100644 --- a/core/templates/pages/story-editor-page/services/story-editor-navigation.service.ts +++ b/core/templates/pages/story-editor-page/services/story-editor-navigation.service.ts @@ -16,17 +16,17 @@ * @fileoverview Service to handle navigation in story editor page. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable, EventEmitter } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable, EventEmitter} from '@angular/core'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; const STORY_EDITOR = 'story_editor'; const CHAPTER_EDITOR = 'chapter_editor'; const STORY_PREVIEW = 'story_preview'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class StoryEditorNavigationService { activeTab: string = 'story_editor'; @@ -34,8 +34,8 @@ export class StoryEditorNavigationService { // 'chapterIndex' is null when we are navigating to a chapter with its ID. chapterIndex: number | null = null; - private _activeTabIsSwitchedEventEmitter: EventEmitter = ( - new EventEmitter()); + private _activeTabIsSwitchedEventEmitter: EventEmitter = + new EventEmitter(); constructor(private windowRef: WindowRef) {} @@ -75,7 +75,8 @@ export class StoryEditorNavigationService { checkIfPresentInStoryPreviewTab(): boolean { return ( this.windowRef.nativeWindow.location.hash.split('/')[1] === - 'story_preview'); + 'story_preview' + ); } navigateToChapterEditor(): void { @@ -98,6 +99,9 @@ export class StoryEditorNavigationService { } } -angular.module('oppia').factory( - 'StoryEditorNavigationService', - downgradeInjectable(StoryEditorNavigationService)); +angular + .module('oppia') + .factory( + 'StoryEditorNavigationService', + downgradeInjectable(StoryEditorNavigationService) + ); diff --git a/core/templates/pages/story-editor-page/services/story-editor-staleness-detection.service.spec.ts b/core/templates/pages/story-editor-page/services/story-editor-staleness-detection.service.spec.ts index 90f5adbe6c91..3b91cd30c50b 100644 --- a/core/templates/pages/story-editor-page/services/story-editor-staleness-detection.service.spec.ts +++ b/core/templates/pages/story-editor-page/services/story-editor-staleness-detection.service.spec.ts @@ -15,30 +15,29 @@ * @fileoverview Unit tests for story editor staleness detection service. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TestBed } from '@angular/core/testing'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { UndoRedoService } from 'domain/editor/undo_redo/undo-redo.service'; -import { EntityEditorBrowserTabsInfo } from 'domain/entity_editor_browser_tabs_info/entity-editor-browser-tabs-info.model'; -import { Story } from 'domain/story/story.model'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { FaviconService } from 'services/favicon.service'; -import { LocalStorageService } from 'services/local-storage.service'; -import { StalenessDetectionService } from 'services/staleness-detection.service'; -import { StoryEditorStalenessDetectionService } from './story-editor-staleness-detection.service'; -import { StoryEditorStateService } from './story-editor-state.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TestBed} from '@angular/core/testing'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {UndoRedoService} from 'domain/editor/undo_redo/undo-redo.service'; +import {EntityEditorBrowserTabsInfo} from 'domain/entity_editor_browser_tabs_info/entity-editor-browser-tabs-info.model'; +import {Story} from 'domain/story/story.model'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {FaviconService} from 'services/favicon.service'; +import {LocalStorageService} from 'services/local-storage.service'; +import {StalenessDetectionService} from 'services/staleness-detection.service'; +import {StoryEditorStalenessDetectionService} from './story-editor-staleness-detection.service'; +import {StoryEditorStateService} from './story-editor-state.service'; class MockWindowRef { nativeWindow = { location: { - reload: () => {} - } + reload: () => {}, + }, }; } describe('Story editor staleness detection service', () => { - let storyEditorStalenessDetectionService: - StoryEditorStalenessDetectionService; + let storyEditorStalenessDetectionService: StoryEditorStalenessDetectionService; let storyEditorStateService: StoryEditorStateService; let localStorageService: LocalStorageService; let ngbModal: NgbModal; @@ -60,13 +59,14 @@ describe('Story editor staleness detection service', () => { UndoRedoService, { provide: WindowRef, - useValue: mockWindowRef - } - ] + useValue: mockWindowRef, + }, + ], }).compileComponents(); - storyEditorStalenessDetectionService = - TestBed.inject(StoryEditorStalenessDetectionService); + storyEditorStalenessDetectionService = TestBed.inject( + StoryEditorStalenessDetectionService + ); storyEditorStateService = TestBed.inject(StoryEditorStateService); localStorageService = TestBed.inject(LocalStorageService); ngbModal = TestBed.inject(NgbModal); @@ -100,8 +100,9 @@ describe('Story editor staleness detection service', () => { planned_publication_date_msecs: 100, last_modified_msecs: 100, first_publication_date_msecs: 200, - unpublishing_reason: null - }, { + unpublishing_reason: null, + }, + { id: 'node_2', title: 'Title 2', description: 'Description 2', @@ -117,32 +118,39 @@ describe('Story editor staleness detection service', () => { planned_publication_date_msecs: 100, last_modified_msecs: 100, first_publication_date_msecs: 200, - unpublishing_reason: null - }], - next_node_id: 'node_3' + unpublishing_reason: null, + }, + ], + next_node_id: 'node_3', }, language_code: 'en', thumbnail_filename: 'fileName', thumbnail_bg_color: 'blue', url_fragment: 'url', - meta_tag_content: 'meta' + meta_tag_content: 'meta', }; - sampleStory = Story.createFromBackendDict( - sampleStoryBackendObject); + sampleStory = Story.createFromBackendDict(sampleStoryBackendObject); }); it('should show stale tab info modal and change the favicon', () => { spyOn(storyEditorStateService, 'getStory').and.returnValue(sampleStory); let storyEditorBrowserTabsInfo = EntityEditorBrowserTabsInfo.create( - 'story', 'story_id', 2, 1, false); + 'story', + 'story_id', + 2, + 1, + false + ); spyOn( - localStorageService, 'getEntityEditorBrowserTabsInfo' + localStorageService, + 'getEntityEditorBrowserTabsInfo' ).and.returnValue(storyEditorBrowserTabsInfo); spyOn(mockWindowRef.nativeWindow.location, 'reload'); spyOn(faviconService, 'setFavicon').and.callFake(() => {}); spyOn( - storyEditorStalenessDetectionService, 'showStaleTabInfoModal' + storyEditorStalenessDetectionService, + 'showStaleTabInfoModal' ).and.callThrough(); class MockNgbModalRef { result = Promise.resolve(); @@ -158,7 +166,8 @@ describe('Story editor staleness detection service', () => { storyEditorStalenessDetectionService.showStaleTabInfoModal ).toHaveBeenCalled(); expect(faviconService.setFavicon).toHaveBeenCalledWith( - '/assets/images/favicon_alert/favicon_alert.ico'); + '/assets/images/favicon_alert/favicon_alert.ico' + ); expect(ngbModal.open).toHaveBeenCalled(); storyEditorStateService.onStoryInitialized.emit(); @@ -168,63 +177,73 @@ describe('Story editor staleness detection service', () => { ).toHaveBeenCalled(); }); - it('should open or close presence of unsaved changes info modal ' + - 'depending on the presence of unsaved changes on some other tab', () => { - spyOn(storyEditorStateService, 'getStory').and.returnValue(sampleStory); - let storyEditorBrowserTabsInfo = EntityEditorBrowserTabsInfo.create( - 'story', 'story_id', 2, 2, true); - spyOn( - localStorageService, 'getEntityEditorBrowserTabsInfo' - ).and.returnValue(storyEditorBrowserTabsInfo); - spyOn(mockWindowRef.nativeWindow.location, 'reload'); - spyOn( - storyEditorStalenessDetectionService, 'showPresenceOfUnsavedChangesModal' - ).and.callThrough(); - class MockNgbModalRef { - result = Promise.resolve(); - componentInstance = {}; - dismiss() {} + it( + 'should open or close presence of unsaved changes info modal ' + + 'depending on the presence of unsaved changes on some other tab', + () => { + spyOn(storyEditorStateService, 'getStory').and.returnValue(sampleStory); + let storyEditorBrowserTabsInfo = EntityEditorBrowserTabsInfo.create( + 'story', + 'story_id', + 2, + 2, + true + ); + spyOn( + localStorageService, + 'getEntityEditorBrowserTabsInfo' + ).and.returnValue(storyEditorBrowserTabsInfo); + spyOn(mockWindowRef.nativeWindow.location, 'reload'); + spyOn( + storyEditorStalenessDetectionService, + 'showPresenceOfUnsavedChangesModal' + ).and.callThrough(); + class MockNgbModalRef { + result = Promise.resolve(); + componentInstance = {}; + dismiss() {} + } + const ngbModalRef = new MockNgbModalRef() as NgbModalRef; + spyOn(ngbModalRef, 'dismiss'); + spyOn(ngbModal, 'open').and.returnValue(ngbModalRef); + spyOn(undoRedoService, 'getChangeCount').and.returnValue(0); + spyOn( + stalenessDetectionService, + 'doesSomeOtherEntityEditorPageHaveUnsavedChanges' + ).and.returnValues(true, false); + + storyEditorStalenessDetectionService.init(); + storyEditorStalenessDetectionService.presenceOfUnsavedChangesEventEmitter.emit(); + + expect( + storyEditorStalenessDetectionService.showPresenceOfUnsavedChangesModal + ).toHaveBeenCalled(); + expect(ngbModal.open).toHaveBeenCalled(); + + storyEditorStalenessDetectionService.presenceOfUnsavedChangesEventEmitter.emit(); + + expect(ngbModalRef.dismiss).toHaveBeenCalled(); } - const ngbModalRef = new MockNgbModalRef() as NgbModalRef; - spyOn(ngbModalRef, 'dismiss'); - spyOn(ngbModal, 'open').and.returnValue(ngbModalRef); - spyOn(undoRedoService, 'getChangeCount').and.returnValue(0); - spyOn( - stalenessDetectionService, - 'doesSomeOtherEntityEditorPageHaveUnsavedChanges' - ).and.returnValues(true, false); - - storyEditorStalenessDetectionService.init(); - storyEditorStalenessDetectionService - .presenceOfUnsavedChangesEventEmitter.emit(); - - expect( - storyEditorStalenessDetectionService.showPresenceOfUnsavedChangesModal - ).toHaveBeenCalled(); - expect(ngbModal.open).toHaveBeenCalled(); - - storyEditorStalenessDetectionService - .presenceOfUnsavedChangesEventEmitter.emit(); - - expect(ngbModalRef.dismiss).toHaveBeenCalled(); - }); - - it('should not show the presence of unsaved changes modal on the page' + - 'which itself contains those unsaved changes', () => { - class MockNgbModalRef { - result = Promise.resolve(); - componentInstance = {}; - dismiss() {} + ); + + it( + 'should not show the presence of unsaved changes modal on the page' + + 'which itself contains those unsaved changes', + () => { + class MockNgbModalRef { + result = Promise.resolve(); + componentInstance = {}; + dismiss() {} + } + const ngbModalRef = new MockNgbModalRef() as NgbModalRef; + spyOn(ngbModalRef, 'dismiss'); + spyOn(ngbModal, 'open').and.returnValue(ngbModalRef); + spyOn(undoRedoService, 'getChangeCount').and.returnValue(1); + + storyEditorStalenessDetectionService.init(); + storyEditorStalenessDetectionService.presenceOfUnsavedChangesEventEmitter.emit(); + + expect(ngbModal.open).not.toHaveBeenCalled(); } - const ngbModalRef = new MockNgbModalRef() as NgbModalRef; - spyOn(ngbModalRef, 'dismiss'); - spyOn(ngbModal, 'open').and.returnValue(ngbModalRef); - spyOn(undoRedoService, 'getChangeCount').and.returnValue(1); - - storyEditorStalenessDetectionService.init(); - storyEditorStalenessDetectionService - .presenceOfUnsavedChangesEventEmitter.emit(); - - expect(ngbModal.open).not.toHaveBeenCalled(); - }); + ); }); diff --git a/core/templates/pages/story-editor-page/services/story-editor-staleness-detection.service.ts b/core/templates/pages/story-editor-page/services/story-editor-staleness-detection.service.ts index 86b291537bc7..33b79282f8f8 100644 --- a/core/templates/pages/story-editor-page/services/story-editor-staleness-detection.service.ts +++ b/core/templates/pages/story-editor-page/services/story-editor-staleness-detection.service.ts @@ -16,22 +16,22 @@ * @fileoverview Service for emitting events when a story editor tab is stale. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { EventEmitter, Injectable } from '@angular/core'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { StalenessDetectionService } from 'services/staleness-detection.service'; -import { EntityEditorBrowserTabsInfoDomainConstants } from 'domain/entity_editor_browser_tabs_info/entity-editor-browser-tabs-info-domain.constants'; -import { StoryEditorStateService } from './story-editor-state.service'; -import { EntityEditorBrowserTabsInfo } from 'domain/entity_editor_browser_tabs_info/entity-editor-browser-tabs-info.model'; -import { FaviconService } from 'services/favicon.service'; -import { LocalStorageService } from 'services/local-storage.service'; -import { UndoRedoService } from 'domain/editor/undo_redo/undo-redo.service'; -import { StaleTabInfoModalComponent } from 'components/stale-tab-info/stale-tab-info-modal.component'; -import { UnsavedChangesStatusInfoModalComponent } from 'components/unsaved-changes-status-info/unsaved-changes-status-info-modal.component'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {EventEmitter, Injectable} from '@angular/core'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {StalenessDetectionService} from 'services/staleness-detection.service'; +import {EntityEditorBrowserTabsInfoDomainConstants} from 'domain/entity_editor_browser_tabs_info/entity-editor-browser-tabs-info-domain.constants'; +import {StoryEditorStateService} from './story-editor-state.service'; +import {EntityEditorBrowserTabsInfo} from 'domain/entity_editor_browser_tabs_info/entity-editor-browser-tabs-info.model'; +import {FaviconService} from 'services/favicon.service'; +import {LocalStorageService} from 'services/local-storage.service'; +import {UndoRedoService} from 'domain/editor/undo_redo/undo-redo.service'; +import {StaleTabInfoModalComponent} from 'components/stale-tab-info/stale-tab-info-modal.component'; +import {UnsavedChangesStatusInfoModalComponent} from 'components/unsaved-changes-status-info/unsaved-changes-status-info-modal.component'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class StoryEditorStalenessDetectionService { _staleTabEventEmitter = new EventEmitter(); @@ -67,25 +67,29 @@ export class StoryEditorStalenessDetectionService { showStaleTabInfoModal(): void { const story = this.storyEditorStateService.getStory(); if (story) { - const storyEditorBrowserTabsInfo: EntityEditorBrowserTabsInfo | null = ( + const storyEditorBrowserTabsInfo: EntityEditorBrowserTabsInfo | null = this.localStorageService.getEntityEditorBrowserTabsInfo( - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_STORY_EDITOR_BROWSER_TABS, story.getId())); + EntityEditorBrowserTabsInfoDomainConstants.OPENED_STORY_EDITOR_BROWSER_TABS, + story.getId() + ); if ( storyEditorBrowserTabsInfo && storyEditorBrowserTabsInfo.getLatestVersion() !== story.getVersion() ) { this.faviconService.setFavicon( - '/assets/images/favicon_alert/favicon_alert.ico'); + '/assets/images/favicon_alert/favicon_alert.ico' + ); this.ngbModal.dismissAll(); - const modalRef = this.ngbModal.open( - StaleTabInfoModalComponent, { - backdrop: 'static', - }); + const modalRef = this.ngbModal.open(StaleTabInfoModalComponent, { + backdrop: 'static', + }); modalRef.componentInstance.entity = 'story'; - modalRef.result.then(() => { - this.windowRef.nativeWindow.location.reload(); - }, () => {}); + modalRef.result.then( + () => { + this.windowRef.nativeWindow.location.reload(); + }, + () => {} + ); } } } @@ -96,18 +100,23 @@ export class StoryEditorStalenessDetectionService { return; } if ( - this.stalenessDetectionService - .doesSomeOtherEntityEditorPageHaveUnsavedChanges( - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_STORY_EDITOR_BROWSER_TABS, story.getId()) + this.stalenessDetectionService.doesSomeOtherEntityEditorPageHaveUnsavedChanges( + EntityEditorBrowserTabsInfoDomainConstants.OPENED_STORY_EDITOR_BROWSER_TABS, + story.getId() + ) ) { this.ngbModal.dismissAll(); this.unsavedChangesWarningModalRef = this.ngbModal.open( - UnsavedChangesStatusInfoModalComponent, { + UnsavedChangesStatusInfoModalComponent, + { backdrop: 'static', - }); + } + ); this.unsavedChangesWarningModalRef.componentInstance.entity = 'story'; - this.unsavedChangesWarningModalRef.result.then(() => {}, () => {}); + this.unsavedChangesWarningModalRef.result.then( + () => {}, + () => {} + ); } else if (this.unsavedChangesWarningModalRef) { this.unsavedChangesWarningModalRef.dismiss(); } @@ -122,6 +131,9 @@ export class StoryEditorStalenessDetectionService { } } -angular.module('oppia').factory( - 'StoryEditorStalenessDetectionService', - downgradeInjectable(StoryEditorStalenessDetectionService)); +angular + .module('oppia') + .factory( + 'StoryEditorStalenessDetectionService', + downgradeInjectable(StoryEditorStalenessDetectionService) + ); diff --git a/core/templates/pages/story-editor-page/services/story-editor-state.service.spec.ts b/core/templates/pages/story-editor-page/services/story-editor-state.service.spec.ts index ff5acdbccd6d..21b031cb7ac8 100644 --- a/core/templates/pages/story-editor-page/services/story-editor-state.service.spec.ts +++ b/core/templates/pages/story-editor-page/services/story-editor-state.service.spec.ts @@ -16,16 +16,16 @@ * @fileoverview Unit tests for StoryEditorStateService. */ -import { fakeAsync, TestBed, tick } from '@angular/core/testing'; +import {fakeAsync, TestBed, tick} from '@angular/core/testing'; -import { Subscription } from 'rxjs'; -import { Story } from 'domain/story/story.model'; -import { StoryBackendDict } from 'domain/story/story.model'; -import { EditableStoryBackendApiService } from 'domain/story/editable-story-backend-api.service'; -import { StoryEditorStateService } from 'pages/story-editor-page/services/story-editor-state.service'; -import { importAllAngularServices } from 'tests/unit-test-utils.ajs'; -import { AlertsService } from 'services/alerts.service'; -import { StoryUpdateService } from 'domain/story/story-update.service'; +import {Subscription} from 'rxjs'; +import {Story} from 'domain/story/story.model'; +import {StoryBackendDict} from 'domain/story/story.model'; +import {EditableStoryBackendApiService} from 'domain/story/editable-story-backend-api.service'; +import {StoryEditorStateService} from 'pages/story-editor-page/services/story-editor-state.service'; +import {importAllAngularServices} from 'tests/unit-test-utils.ajs'; +import {AlertsService} from 'services/alerts.service'; +import {StoryUpdateService} from 'domain/story/story-update.service'; class MockEditableStoryBackendApiService { newBackendStoryObject!: StoryBackendDict; @@ -38,18 +38,20 @@ class MockEditableStoryBackendApiService { story: this.newBackendStoryObject, topicName: 'Topic Name', storyIsPublished: false, - skillSummaries: [{ - id: 'Skill 1', - description: 'Skill Description', - language_code: 'en', - version: 1, - misconception_count: 0, - worked_examples_count: 0, - skill_model_created_on: 0, - skill_model_last_updated: 0, - }], + skillSummaries: [ + { + id: 'Skill 1', + description: 'Skill Description', + language_code: 'en', + version: 1, + misconception_count: 0, + worked_examples_count: 0, + skill_model_created_on: 0, + skill_model_last_updated: 0, + }, + ], classroomUrlFragment: 'classroomUrlFragment', - topicUrlFragment: 'topicUrlFragment' + topicUrlFragment: 'topicUrlFragment', }); } else { reject(); @@ -103,8 +105,8 @@ describe('Story editor state service', () => { const storyReinitializedSpy = jasmine.createSpy('storyReinitialized'); beforeEach(() => { - fakeEditableStoryBackendApiService = ( - new MockEditableStoryBackendApiService()); + fakeEditableStoryBackendApiService = + new MockEditableStoryBackendApiService(); fakeEditableStoryBackendApiService.newBackendStoryObject = { id: 'storyId_0', @@ -114,7 +116,7 @@ describe('Story editor state service', () => { story_contents: { initial_node_id: 'node_1', next_node_id: 'node_2', - nodes: [] + nodes: [], }, language_code: 'en', version: 1, @@ -122,7 +124,7 @@ describe('Story editor state service', () => { thumbnail_filename: 'img.svg', thumbnail_bg_color: '', url_fragment: 'url_fragment1', - meta_tag_content: 'meta_content1' + meta_tag_content: 'meta_content1', }; secondBackendStoryObject = { @@ -133,7 +135,7 @@ describe('Story editor state service', () => { story_contents: { initial_node_id: 'node_2', next_node_id: 'node_1', - nodes: [] + nodes: [], }, language_code: 'en', version: 1, @@ -141,17 +143,17 @@ describe('Story editor state service', () => { thumbnail_filename: 'img.svg', thumbnail_bg_color: '', url_fragment: 'url_fragment2', - meta_tag_content: 'meta_content2' + meta_tag_content: 'meta_content2', }; TestBed.configureTestingModule({ providers: [ { provide: EditableStoryBackendApiService, - useValue: fakeEditableStoryBackendApiService + useValue: fakeEditableStoryBackendApiService, }, - StoryUpdateService - ] + StoryUpdateService, + ], }).compileComponents(); alertsService = TestBed.inject(AlertsService); @@ -161,11 +163,14 @@ describe('Story editor state service', () => { beforeEach(() => { testSubscriptions = new Subscription(); - testSubscriptions.add(storyEditorStateService.onStoryInitialized.subscribe( - storyInitializedSpy)); + testSubscriptions.add( + storyEditorStateService.onStoryInitialized.subscribe(storyInitializedSpy) + ); testSubscriptions.add( storyEditorStateService.onStoryReinitialized.subscribe( - storyReinitializedSpy)); + storyReinitializedSpy + ) + ); }); afterEach(() => { @@ -174,21 +179,26 @@ describe('Story editor state service', () => { it('should request to load the story from the backend', () => { spyOn( - fakeEditableStoryBackendApiService, 'fetchStoryAsync').and.callThrough(); + fakeEditableStoryBackendApiService, + 'fetchStoryAsync' + ).and.callThrough(); storyEditorStateService.loadStory('storyId_0'); expect( - fakeEditableStoryBackendApiService.fetchStoryAsync).toHaveBeenCalled(); + fakeEditableStoryBackendApiService.fetchStoryAsync + ).toHaveBeenCalled(); }); it( 'should fire an init event and set the topic name after loading the ' + - 'first story', fakeAsync(() => { + 'first story', + fakeAsync(() => { storyEditorStateService.loadStory('storyId_0'); tick(1000); expect(storyEditorStateService.getTopicName()).toEqual('Topic Name'); expect(storyInitializedSpy).toHaveBeenCalled(); - })); + }) + ); it('should fire an update event after loading more stories', fakeAsync(() => { // Load initial story. @@ -211,56 +221,51 @@ describe('Story editor state service', () => { expect(storyEditorStateService.isLoadingStory()).toBe(false); })); - it('should indicate a story is no longer loading after an error', - fakeAsync(() => { - expect(storyEditorStateService.isLoadingStory()).toBe(false); - fakeEditableStoryBackendApiService.failure = 'Internal 500 error'; + it('should indicate a story is no longer loading after an error', fakeAsync(() => { + expect(storyEditorStateService.isLoadingStory()).toBe(false); + fakeEditableStoryBackendApiService.failure = 'Internal 500 error'; - storyEditorStateService.loadStory('storyId_0'); - expect(storyEditorStateService.isLoadingStory()).toBe(true); + storyEditorStateService.loadStory('storyId_0'); + expect(storyEditorStateService.isLoadingStory()).toBe(true); - tick(1000); - expect(storyEditorStateService.isLoadingStory()).toBe(false); - })); + tick(1000); + expect(storyEditorStateService.isLoadingStory()).toBe(false); + })); - it('should report that a story has loaded through loadStory()', - fakeAsync(() => { - expect(storyEditorStateService.hasLoadedStory()).toBe(false); + it('should report that a story has loaded through loadStory()', fakeAsync(() => { + expect(storyEditorStateService.hasLoadedStory()).toBe(false); - storyEditorStateService.loadStory('storyId_0'); - expect(storyEditorStateService.hasLoadedStory()).toBe(false); + storyEditorStateService.loadStory('storyId_0'); + expect(storyEditorStateService.hasLoadedStory()).toBe(false); - tick(1000); - expect(storyEditorStateService.hasLoadedStory()).toBe(true); - })); + tick(1000); + expect(storyEditorStateService.hasLoadedStory()).toBe(true); + })); it('should report that a story has loaded through setStory()', () => { expect(storyEditorStateService.hasLoadedStory()).toBe(false); - var newStory = Story.createFromBackendDict( - secondBackendStoryObject); + var newStory = Story.createFromBackendDict(secondBackendStoryObject); storyEditorStateService.setStory(newStory); expect(storyEditorStateService.hasLoadedStory()).toBe(true); }); - it('should be able to set a new story with an in-place copy', - fakeAsync(() => { - storyEditorStateService.loadStory('storyId_0'); - tick(1000); + it('should be able to set a new story with an in-place copy', fakeAsync(() => { + storyEditorStateService.loadStory('storyId_0'); + tick(1000); - var previousStory = storyEditorStateService.getStory(); - var expectedStory = Story.createFromBackendDict( - secondBackendStoryObject); - expect(previousStory).not.toEqual(expectedStory); + var previousStory = storyEditorStateService.getStory(); + var expectedStory = Story.createFromBackendDict(secondBackendStoryObject); + expect(previousStory).not.toEqual(expectedStory); - storyEditorStateService.setStory(expectedStory); + storyEditorStateService.setStory(expectedStory); - var actualStory = storyEditorStateService.getStory(); - expect(actualStory).toEqual(expectedStory); + var actualStory = storyEditorStateService.getStory(); + expect(actualStory).toEqual(expectedStory); - expect(actualStory).toBe(previousStory); - expect(actualStory).not.toBe(expectedStory); - })); + expect(actualStory).toBe(previousStory); + expect(actualStory).not.toBe(expectedStory); + })); it('should fail to save the story without first loading one', () => { expect(() => { @@ -268,37 +273,52 @@ describe('Story editor state service', () => { const errorCallback = jasmine.createSpy('errorCallback'); storyEditorStateService.saveStory( - 'Commit message', successCallback, errorCallback); + 'Commit message', + successCallback, + errorCallback + ); }).toThrowError(); }); - it('should not save the story if there are no pending changes', - fakeAsync(() => { - const successCallback = jasmine.createSpy('successCallback'); - const errorCallback = jasmine.createSpy('errorCallback'); + it('should not save the story if there are no pending changes', fakeAsync(() => { + const successCallback = jasmine.createSpy('successCallback'); + const errorCallback = jasmine.createSpy('errorCallback'); - storyEditorStateService.loadStory('storyId_0'); - tick(1000); + storyEditorStateService.loadStory('storyId_0'); + tick(1000); - expect(storyEditorStateService.saveStory( - 'Commit message', successCallback, errorCallback)).toBe(false); - })); + expect( + storyEditorStateService.saveStory( + 'Commit message', + successCallback, + errorCallback + ) + ).toBe(false); + })); it('should be able to save the story and pending changes', fakeAsync(() => { spyOn( fakeEditableStoryBackendApiService, - 'updateStoryAsync').and.callThrough(); + 'updateStoryAsync' + ).and.callThrough(); var successCallback = jasmine.createSpy('successCallback'); var errorCallback = jasmine.createSpy('errorCallback'); storyEditorStateService.loadStory('storyId_0'); tick(1000); storyUpdateService.setStoryTitle( - storyEditorStateService.getStory(), 'New title'); + storyEditorStateService.getStory(), + 'New title' + ); tick(1000); - expect(storyEditorStateService.saveStory( - 'Commit message', successCallback, errorCallback)).toBe(true); + expect( + storyEditorStateService.saveStory( + 'Commit message', + successCallback, + errorCallback + ) + ).toBe(true); tick(1000); var expectedId = 'storyId_0'; @@ -308,13 +328,15 @@ describe('Story editor state service', () => { property_name: 'title', new_value: 'New title', old_value: 'Story title', - cmd: 'update_story_property' + cmd: 'update_story_property', }; - var updateStorySpy = ( - fakeEditableStoryBackendApiService.updateStoryAsync); + var updateStorySpy = fakeEditableStoryBackendApiService.updateStoryAsync; expect(updateStorySpy).toHaveBeenCalledWith( - expectedId, expectedVersion, - expectedCommitMessage, [expectedObject]); + expectedId, + expectedVersion, + expectedCommitMessage, + [expectedObject] + ); expect(successCallback).toHaveBeenCalled(); })); @@ -325,13 +347,17 @@ describe('Story editor state service', () => { storyEditorStateService.saveChapter(successCallback, errorCallback); expect(saveStorySpy).toHaveBeenCalledWith( - 'Changed Chapter Status', successCallback, errorCallback); + 'Changed Chapter Status', + successCallback, + errorCallback + ); })); it('should be able to publish the story', fakeAsync(() => { spyOn( fakeEditableStoryBackendApiService, - 'changeStoryPublicationStatusAsync').and.callThrough(); + 'changeStoryPublicationStatusAsync' + ).and.callThrough(); var successCallback = jasmine.createSpy('successCallback'); storyEditorStateService.loadStory('storyId_0'); @@ -340,14 +366,16 @@ describe('Story editor state service', () => { expect(storyEditorStateService.isStoryPublished()).toBe(false); expect( storyEditorStateService.changeStoryPublicationStatus( - true, successCallback)).toBe(true); + true, + successCallback + ) + ).toBe(true); tick(1000); var expectedId = 'storyId_0'; - var publishStorySpy = ( - fakeEditableStoryBackendApiService.changeStoryPublicationStatusAsync); - expect(publishStorySpy).toHaveBeenCalledWith( - expectedId, true); + var publishStorySpy = + fakeEditableStoryBackendApiService.changeStoryPublicationStatusAsync; + expect(publishStorySpy).toHaveBeenCalledWith(expectedId, true); expect(storyEditorStateService.isStoryPublished()).toBe(true); expect(successCallback).toHaveBeenCalled(); })); @@ -356,7 +384,8 @@ describe('Story editor state service', () => { const successCallback = jasmine.createSpy('successCallback'); spyOn( fakeEditableStoryBackendApiService, - 'changeStoryPublicationStatusAsync').and.callThrough(); + 'changeStoryPublicationStatusAsync' + ).and.callThrough(); spyOn(alertsService, 'addWarning'); storyEditorStateService.loadStory('storyId_0'); @@ -366,35 +395,36 @@ describe('Story editor state service', () => { expect(storyEditorStateService.isStoryPublished()).toBe(false); expect( storyEditorStateService.changeStoryPublicationStatus( - true, successCallback)).toBe(true); + true, + successCallback + ) + ).toBe(true); tick(1000); var expectedId = 'storyId_0'; - var publishStorySpy = ( - fakeEditableStoryBackendApiService.changeStoryPublicationStatusAsync); - expect(publishStorySpy).toHaveBeenCalledWith( - expectedId, true); + var publishStorySpy = + fakeEditableStoryBackendApiService.changeStoryPublicationStatusAsync; + expect(publishStorySpy).toHaveBeenCalledWith(expectedId, true); expect(storyEditorStateService.isStoryPublished()).toBe(false); expect(alertsService.addWarning).toHaveBeenCalledWith( 'There was an error when publishing/unpublishing the story.' ); })); - it('should warn user when user attepts to publish story before it loads', - fakeAsync(() => { - storyEditorStateService.loadStory('storyId_0'); - tick(1000); + it('should warn user when user attepts to publish story before it loads', fakeAsync(() => { + storyEditorStateService.loadStory('storyId_0'); + tick(1000); - const successCallback = jasmine.createSpy('successCallback'); - spyOn(alertsService, 'fatalWarning'); - storyEditorStateService._storyIsInitialized = false; + const successCallback = jasmine.createSpy('successCallback'); + spyOn(alertsService, 'fatalWarning'); + storyEditorStateService._storyIsInitialized = false; - storyEditorStateService.changeStoryPublicationStatus( - true, successCallback); + storyEditorStateService.changeStoryPublicationStatus(true, successCallback); - expect(alertsService.fatalWarning) - .toHaveBeenCalledWith('Cannot publish a story before one is loaded.'); - })); + expect(alertsService.fatalWarning).toHaveBeenCalledWith( + 'Cannot publish a story before one is loaded.' + ); + })); it('should fire an update event after saving the story', fakeAsync(() => { const successCallback = jasmine.createSpy('successCallback'); @@ -402,11 +432,16 @@ describe('Story editor state service', () => { storyEditorStateService.loadStory('storyId_0'); tick(1000); storyUpdateService.setStoryTitle( - storyEditorStateService.getStory(), 'New title'); + storyEditorStateService.getStory(), + 'New title' + ); tick(1000); storyEditorStateService.saveStory( - 'Commit message', successCallback, errorCallback); + 'Commit message', + successCallback, + errorCallback + ); tick(1000); expect(storyReinitializedSpy).toHaveBeenCalled(); })); @@ -417,12 +452,17 @@ describe('Story editor state service', () => { storyEditorStateService.loadStory('storyId_0'); tick(1000); storyUpdateService.setStoryTitle( - storyEditorStateService.getStory(), 'New title'); + storyEditorStateService.getStory(), + 'New title' + ); tick(1000); expect(storyEditorStateService.isSavingStory()).toBe(false); storyEditorStateService.saveStory( - 'Commit message', successCallback, errorCallback); + 'Commit message', + successCallback, + errorCallback + ); expect(storyEditorStateService.isSavingStory()).toBe(true); tick(1000); @@ -437,124 +477,149 @@ describe('Story editor state service', () => { storyEditorStateService.loadStory('storyId_0'); tick(1000); storyUpdateService.setStoryTitle( - storyEditorStateService.getStory(), 'New title'); + storyEditorStateService.getStory(), + 'New title' + ); tick(1000); fakeEditableStoryBackendApiService.failure = 'Internal 500 error'; storyEditorStateService.saveStory( - 'Commit message', successCallback, errorCallback); + 'Commit message', + successCallback, + errorCallback + ); tick(1000); - expect(alertsService.addWarning) - .toHaveBeenCalledWith('There was an error when saving the story.'); - expect(errorCallback) - .toHaveBeenCalledWith('There was an error when saving the story.'); + expect(alertsService.addWarning).toHaveBeenCalledWith( + 'There was an error when saving the story.' + ); + expect(errorCallback).toHaveBeenCalledWith( + 'There was an error when saving the story.' + ); })); - it('should indicate a story is no longer saving after an error', - fakeAsync(() => { - const successCallback = jasmine.createSpy('successCallback'); - const errorCallback = jasmine.createSpy('errorCallback'); - storyEditorStateService.loadStory('storyId_0'); - tick(1000); - storyUpdateService.setStoryTitle( - storyEditorStateService.getStory(), 'New title'); - tick(1000); - - expect(storyEditorStateService.isSavingStory()).toBe(false); - fakeEditableStoryBackendApiService.failure = 'Internal 500 error'; - - storyEditorStateService.saveStory( - 'Commit message', successCallback, errorCallback); - expect(storyEditorStateService.isSavingStory()).toBe(true); - - tick(1000); - expect(storyEditorStateService.isSavingStory()).toBe(false); - expect(storyEditorStateService.isChangingChapterStatus()).toBeFalse(); - })); - - it('should update stories URL when user updates the storie\'s URL', - fakeAsync(() => { - var newStory = Story.createFromBackendDict( - secondBackendStoryObject); - storyEditorStateService.setStory(newStory); + it('should indicate a story is no longer saving after an error', fakeAsync(() => { + const successCallback = jasmine.createSpy('successCallback'); + const errorCallback = jasmine.createSpy('errorCallback'); + storyEditorStateService.loadStory('storyId_0'); + tick(1000); + storyUpdateService.setStoryTitle( + storyEditorStateService.getStory(), + 'New title' + ); + tick(1000); - fakeEditableStoryBackendApiService.failure = ''; - storyEditorStateService._storyWithUrlFragmentExists = true; + expect(storyEditorStateService.isSavingStory()).toBe(false); + fakeEditableStoryBackendApiService.failure = 'Internal 500 error'; - storyEditorStateService.updateExistenceOfStoryUrlFragment( - 'test_url', () =>{}); - tick(1000); + storyEditorStateService.saveStory( + 'Commit message', + successCallback, + errorCallback + ); + expect(storyEditorStateService.isSavingStory()).toBe(true); - expect(storyEditorStateService.getStoryWithUrlFragmentExists()) - .toBe(false); - })); + tick(1000); + expect(storyEditorStateService.isSavingStory()).toBe(false); + expect(storyEditorStateService.isChangingChapterStatus()).toBeFalse(); + })); - it('should warn user when user updates the storie\'s URL to an URL' + - ' that already exits', fakeAsync(() => { - spyOn(alertsService, 'addWarning'); - var newStory = Story.createFromBackendDict( - secondBackendStoryObject); + it("should update stories URL when user updates the storie's URL", fakeAsync(() => { + var newStory = Story.createFromBackendDict(secondBackendStoryObject); storyEditorStateService.setStory(newStory); - fakeEditableStoryBackendApiService.failure = 'Story URL exists'; - storyEditorStateService._storyWithUrlFragmentExists = false; + fakeEditableStoryBackendApiService.failure = ''; + storyEditorStateService._storyWithUrlFragmentExists = true; storyEditorStateService.updateExistenceOfStoryUrlFragment( - 'test_url', () =>{}); + 'test_url', + () => {} + ); tick(1000); - expect(alertsService.addWarning).toHaveBeenCalledWith( - 'There was an error when checking if the story url fragment ' + - 'exists for another story.'); + expect(storyEditorStateService.getStoryWithUrlFragmentExists()).toBe(false); })); + it( + "should warn user when user updates the storie's URL to an URL" + + ' that already exits', + fakeAsync(() => { + spyOn(alertsService, 'addWarning'); + var newStory = Story.createFromBackendDict(secondBackendStoryObject); + storyEditorStateService.setStory(newStory); + + fakeEditableStoryBackendApiService.failure = 'Story URL exists'; + storyEditorStateService._storyWithUrlFragmentExists = false; + + storyEditorStateService.updateExistenceOfStoryUrlFragment( + 'test_url', + () => {} + ); + tick(1000); + + expect(alertsService.addWarning).toHaveBeenCalledWith( + 'There was an error when checking if the story url fragment ' + + 'exists for another story.' + ); + }) + ); + it('should return classroom url fragment when called', fakeAsync(() => { storyEditorStateService.loadStory('storyId_0'); tick(1000); - expect(storyEditorStateService.getClassroomUrlFragment()) - .toBe('classroomUrlFragment'); + expect(storyEditorStateService.getClassroomUrlFragment()).toBe( + 'classroomUrlFragment' + ); })); it('should return topic url fragment when called', fakeAsync(() => { storyEditorStateService.loadStory('storyId_0'); tick(1000); - expect(storyEditorStateService.getTopicUrlFragment()) - .toBe('topicUrlFragment'); + expect(storyEditorStateService.getTopicUrlFragment()).toBe( + 'topicUrlFragment' + ); })); it('should return event emitters when called', () => { expect(storyEditorStateService.onStoryInitialized).toBe( - storyEditorStateService._storyInitializedEventEmitter); + storyEditorStateService._storyInitializedEventEmitter + ); expect(storyEditorStateService.onStoryReinitialized).toBe( - storyEditorStateService._storyReinitializedEventEmitter); + storyEditorStateService._storyReinitializedEventEmitter + ); expect(storyEditorStateService.onViewStoryNodeEditor).toBe( - storyEditorStateService._viewStoryNodeEditorEventEmitter); + storyEditorStateService._viewStoryNodeEditorEventEmitter + ); expect(storyEditorStateService.onRecalculateAvailableNodes).toBe( - storyEditorStateService._recalculateAvailableNodesEventEmitter); + storyEditorStateService._recalculateAvailableNodesEventEmitter + ); }); - it('should set _expIdsChanged to true when setExpIdsChanged is ' + - 'called', () => { - expect(storyEditorStateService.areAnyExpIdsChanged()).toBeFalse(); + it( + 'should set _expIdsChanged to true when setExpIdsChanged is ' + 'called', + () => { + expect(storyEditorStateService.areAnyExpIdsChanged()).toBeFalse(); - storyEditorStateService.setExpIdsChanged(); + storyEditorStateService.setExpIdsChanged(); - expect(storyEditorStateService.areAnyExpIdsChanged()).toBeTrue(); - }); + expect(storyEditorStateService.areAnyExpIdsChanged()).toBeTrue(); + } + ); - it('should set _expIdsChanged to false when resetExpIdsChanged is ' + - 'called', () => { - storyEditorStateService.setExpIdsChanged(); + it( + 'should set _expIdsChanged to false when resetExpIdsChanged is ' + 'called', + () => { + storyEditorStateService.setExpIdsChanged(); - expect(storyEditorStateService.areAnyExpIdsChanged()).toBeTrue(); + expect(storyEditorStateService.areAnyExpIdsChanged()).toBeTrue(); - storyEditorStateService.resetExpIdsChanged(); + storyEditorStateService.resetExpIdsChanged(); - expect(storyEditorStateService.areAnyExpIdsChanged()).toBeFalse(); - }); + expect(storyEditorStateService.areAnyExpIdsChanged()).toBeFalse(); + } + ); it('should set current node as publishable', () => { storyEditorStateService._currentNodeIsPublishable = false; @@ -569,79 +634,77 @@ describe('Story editor state service', () => { expect(storyEditorStateService.isCurrentNodePublishable()).toBe(true); }); - it('should set the selected chapter index in the publish upto chapter ' + - 'dropdown', () => { - storyEditorStateService.setSelectedChapterIndexInPublishUptoDropdown(1); - expect( - storyEditorStateService._selectedChapterIndexInPublishUptoDropdown). - toBe(1); - }); + it( + 'should set the selected chapter index in the publish upto chapter ' + + 'dropdown', + () => { + storyEditorStateService.setSelectedChapterIndexInPublishUptoDropdown(1); + expect( + storyEditorStateService._selectedChapterIndexInPublishUptoDropdown + ).toBe(1); + } + ); - it('should get the selected chapter index in the publish upto chapter ' + - 'dropdown', () => { - storyEditorStateService._selectedChapterIndexInPublishUptoDropdown = 2; - expect( - storyEditorStateService. - getSelectedChapterIndexInPublishUptoDropdown()).toBe(2); - }); + it( + 'should get the selected chapter index in the publish upto chapter ' + + 'dropdown', + () => { + storyEditorStateService._selectedChapterIndexInPublishUptoDropdown = 2; + expect( + storyEditorStateService.getSelectedChapterIndexInPublishUptoDropdown() + ).toBe(2); + } + ); it('should set if the chapters are being published', () => { storyEditorStateService.setChaptersAreBeingPublished(true); - expect( - storyEditorStateService._chaptersAreBeingPublished). - toBe(true); + expect(storyEditorStateService._chaptersAreBeingPublished).toBe(true); storyEditorStateService.setChaptersAreBeingPublished(false); - expect( - storyEditorStateService._chaptersAreBeingPublished). - toBe(false); + expect(storyEditorStateService._chaptersAreBeingPublished).toBe(false); }); it('should get if the chapters are being published', () => { storyEditorStateService._chaptersAreBeingPublished = true; - expect( - storyEditorStateService.areChaptersBeingPublished()). - toBe(true); + expect(storyEditorStateService.areChaptersBeingPublished()).toBe(true); storyEditorStateService._chaptersAreBeingPublished = false; - expect( - storyEditorStateService.areChaptersBeingPublished()). - toBe(false); + expect(storyEditorStateService.areChaptersBeingPublished()).toBe(false); }); it('should set if new chapter publication is disabled', () => { storyEditorStateService.setNewChapterPublicationIsDisabled(true); - expect( - storyEditorStateService._newChapterPublicationIsDisabled). - toBe(true); + expect(storyEditorStateService._newChapterPublicationIsDisabled).toBe(true); storyEditorStateService.setNewChapterPublicationIsDisabled(false); - expect( - storyEditorStateService._newChapterPublicationIsDisabled). - toBe(false); + expect(storyEditorStateService._newChapterPublicationIsDisabled).toBe( + false + ); }); it('should get if new chapter publication is disabled', () => { storyEditorStateService._newChapterPublicationIsDisabled = true; - expect( - storyEditorStateService.getNewChapterPublicationIsDisabled()). - toBe(true); + expect(storyEditorStateService.getNewChapterPublicationIsDisabled()).toBe( + true + ); storyEditorStateService._newChapterPublicationIsDisabled = false; - expect( - storyEditorStateService.getNewChapterPublicationIsDisabled()). - toBe(false); + expect(storyEditorStateService.getNewChapterPublicationIsDisabled()).toBe( + false + ); }); it('should return skill summaries when called', fakeAsync(() => { storyEditorStateService.loadStory('storyId_0'); tick(1000); - expect(storyEditorStateService.getSkillSummaries()).toEqual([{ - id: 'Skill 1', - description: 'Skill Description', - language_code: 'en', - version: 1, - misconception_count: 0, - worked_examples_count: 0, - skill_model_created_on: 0, - skill_model_last_updated: 0, - }]); + expect(storyEditorStateService.getSkillSummaries()).toEqual([ + { + id: 'Skill 1', + description: 'Skill Description', + language_code: 'en', + version: 1, + misconception_count: 0, + worked_examples_count: 0, + skill_model_created_on: 0, + skill_model_last_updated: 0, + }, + ]); })); }); diff --git a/core/templates/pages/story-editor-page/services/story-editor-state.service.ts b/core/templates/pages/story-editor-page/services/story-editor-state.service.ts index 2d0c6bf62448..a9d7ac05be19 100644 --- a/core/templates/pages/story-editor-page/services/story-editor-state.service.ts +++ b/core/templates/pages/story-editor-page/services/story-editor-state.service.ts @@ -18,19 +18,19 @@ * retrieving the story, saving it, and listening for changes. */ -import { EventEmitter, Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {EventEmitter, Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; -import { StoryChange } from 'domain/editor/undo_redo/change.model'; -import { UndoRedoService } from 'domain/editor/undo_redo/undo-redo.service'; -import { SkillSummaryBackendDict } from 'domain/skill/skill-summary.model'; -import { Story, StoryBackendDict } from 'domain/story/story.model'; -import { EditableStoryBackendApiService } from 'domain/story/editable-story-backend-api.service'; -import { AlertsService } from 'services/alerts.service'; -import { LoaderService } from 'services/loader.service'; +import {StoryChange} from 'domain/editor/undo_redo/change.model'; +import {UndoRedoService} from 'domain/editor/undo_redo/undo-redo.service'; +import {SkillSummaryBackendDict} from 'domain/skill/skill-summary.model'; +import {Story, StoryBackendDict} from 'domain/story/story.model'; +import {EditableStoryBackendApiService} from 'domain/story/editable-story-backend-api.service'; +import {AlertsService} from 'services/alerts.service'; +import {LoaderService} from 'services/loader.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class StoryEditorStateService { // These properties are initialized using Angular lifecycle hooks @@ -62,7 +62,8 @@ export class StoryEditorStateService { private alertsService: AlertsService, private editableStoryBackendApiService: EditableStoryBackendApiService, private loaderService: LoaderService, - private undoRedoService: UndoRedoService) {} + private undoRedoService: UndoRedoService + ) {} private _setStory(story: Story): void { if (!this._story) { @@ -95,12 +96,12 @@ export class StoryEditorStateService { } private _updateStory(newBackendStoryObject: StoryBackendDict): void { - this._setStory( - Story.createFromBackendDict(newBackendStoryObject)); + this._setStory(Story.createFromBackendDict(newBackendStoryObject)); } private _setStoryWithUrlFragmentExists( - storyWithUrlFragmentExists: boolean): void { + storyWithUrlFragmentExists: boolean + ): void { this._storyWithUrlFragmentExists = storyWithUrlFragmentExists; } @@ -121,22 +122,25 @@ export class StoryEditorStateService { this._storyIsLoading = true; this.loaderService.showLoadingScreen('Loading Story Editor'); this.editableStoryBackendApiService.fetchStoryAsync(storyId).then( - (newBackendStoryObject) => { + newBackendStoryObject => { this._setTopicName(newBackendStoryObject.topicName); - this._setStoryPublicationStatus( - newBackendStoryObject.storyIsPublished); + this._setStoryPublicationStatus(newBackendStoryObject.storyIsPublished); this._setSkillSummaries(newBackendStoryObject.skillSummaries); this._updateStory(newBackendStoryObject.story); this._storyIsLoading = false; this._setClassroomUrlFragment( - newBackendStoryObject.classroomUrlFragment); + newBackendStoryObject.classroomUrlFragment + ); this._setTopicUrlFragment(newBackendStoryObject.topicUrlFragment); this.loaderService.hideLoadingScreen(); - }, error => { + }, + error => { this.alertsService.addWarning( - error || 'There was an error when loading the story.'); + error || 'There was an error when loading the story.' + ); this._storyIsLoading = false; - }); + } + ); } /** @@ -212,12 +216,14 @@ export class StoryEditorStateService { * shares behavior with setStory(), when it succeeds. */ saveStory( - commitMessage: string, - successCallback: (value?: Object) => void, - errorCallback: (value: string) => void): boolean { + commitMessage: string, + successCallback: (value?: Object) => void, + errorCallback: (value: string) => void + ): boolean { if (!this._storyIsInitialized) { this.alertsService.fatalWarning( - 'Cannot save a story before one is loaded.'); + 'Cannot save a story before one is loaded.' + ); } // Don't attempt to save the story if there are no changes pending. @@ -225,34 +231,39 @@ export class StoryEditorStateService { return false; } this._storyIsBeingSaved = true; - this.editableStoryBackendApiService.updateStoryAsync( - this._story.getId(), this._story.getVersion(), commitMessage, - this.undoRedoService.getCommittableChangeList() as StoryChange[] - ).then( - (storyBackendObject) => { - this._updateStory(storyBackendObject); - this.undoRedoService.clearChanges(); - this._storyIsBeingSaved = false; - this.setChapterStatusIsChanging(false); - if (successCallback) { - successCallback(); + this.editableStoryBackendApiService + .updateStoryAsync( + this._story.getId(), + this._story.getVersion(), + commitMessage, + this.undoRedoService.getCommittableChangeList() as StoryChange[] + ) + .then( + storyBackendObject => { + this._updateStory(storyBackendObject); + this.undoRedoService.clearChanges(); + this._storyIsBeingSaved = false; + this.setChapterStatusIsChanging(false); + if (successCallback) { + successCallback(); + } + }, + error => { + let errorMessage = + error || 'There was an error when saving the story.'; + this.alertsService.addWarning(errorMessage); + this._storyIsBeingSaved = false; + this.setChapterStatusIsChanging(false); + if (errorCallback) { + errorCallback(errorMessage); + } } - }, error => { - let errorMessage = error || 'There was an error when saving the story.'; - this.alertsService.addWarning(errorMessage); - this._storyIsBeingSaved = false; - this.setChapterStatusIsChanging(false); - if (errorCallback) { - errorCallback(errorMessage); - } - }); + ); return true; } - saveChapter( - successCallback: () => void, errorCallback: () => void): void { - this.saveStory( - 'Changed Chapter Status', successCallback, errorCallback); + saveChapter(successCallback: () => void, errorCallback: () => void): void { + this.saveStory('Changed Chapter Status', successCallback, errorCallback); } getTopicUrlFragment(): string { @@ -264,26 +275,32 @@ export class StoryEditorStateService { } changeStoryPublicationStatus( - newStoryStatusIsPublic: boolean, - successCallback: (value?: Object) => void): boolean { + newStoryStatusIsPublic: boolean, + successCallback: (value?: Object) => void + ): boolean { const storyId = this._story.getId(); if (!storyId || !this._storyIsInitialized) { this.alertsService.fatalWarning( - 'Cannot publish a story before one is loaded.'); + 'Cannot publish a story before one is loaded.' + ); return false; } - this.editableStoryBackendApiService.changeStoryPublicationStatusAsync( - storyId, newStoryStatusIsPublic).then( - (storyBackendObject) => { - this._setStoryPublicationStatus(newStoryStatusIsPublic); - if (successCallback) { - successCallback(); + this.editableStoryBackendApiService + .changeStoryPublicationStatusAsync(storyId, newStoryStatusIsPublic) + .then( + storyBackendObject => { + this._setStoryPublicationStatus(newStoryStatusIsPublic); + if (successCallback) { + successCallback(); + } + }, + error => { + this.alertsService.addWarning( + error || + 'There was an error when publishing/unpublishing the story.' + ); } - }, error => { - this.alertsService.addWarning( - error || - 'There was an error when publishing/unpublishing the story.'); - }); + ); return true; } @@ -328,7 +345,8 @@ export class StoryEditorStateService { } setNewChapterPublicationIsDisabled( - chapterPublicationIsDisabled: boolean): void { + chapterPublicationIsDisabled: boolean + ): void { this._newChapterPublicationIsDisabled = chapterPublicationIsDisabled; } @@ -368,23 +386,32 @@ export class StoryEditorStateService { * has been successfully updated. */ updateExistenceOfStoryUrlFragment( - storyUrlFragment: string, - successCallback: (value?: Object) => void): void { - this.editableStoryBackendApiService.doesStoryWithUrlFragmentExistAsync( - storyUrlFragment).then( - (storyUrlFragmentExists) => { - this._setStoryWithUrlFragmentExists(storyUrlFragmentExists); - if (successCallback) { - successCallback(); + storyUrlFragment: string, + successCallback: (value?: Object) => void + ): void { + this.editableStoryBackendApiService + .doesStoryWithUrlFragmentExistAsync(storyUrlFragment) + .then( + storyUrlFragmentExists => { + this._setStoryWithUrlFragmentExists(storyUrlFragmentExists); + if (successCallback) { + successCallback(); + } + }, + error => { + this.alertsService.addWarning( + error || + 'There was an error when checking if the story url fragment ' + + 'exists for another story.' + ); } - }, error => { - this.alertsService.addWarning( - error || - 'There was an error when checking if the story url fragment ' + - 'exists for another story.'); - }); + ); } } -angular.module('oppia').factory( - 'StoryEditorStateService', downgradeInjectable(StoryEditorStateService)); +angular + .module('oppia') + .factory( + 'StoryEditorStateService', + downgradeInjectable(StoryEditorStateService) + ); diff --git a/core/templates/pages/story-editor-page/story-editor-page.component.spec.ts b/core/templates/pages/story-editor-page/story-editor-page.component.spec.ts index 5c8bd1d46060..0717aad3c98d 100644 --- a/core/templates/pages/story-editor-page/story-editor-page.component.spec.ts +++ b/core/templates/pages/story-editor-page/story-editor-page.component.spec.ts @@ -16,26 +16,32 @@ * @fileoverview Unit tests for story editor page component. */ -import { EventEmitter } from '@angular/core'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { EntityEditorBrowserTabsInfo } from 'domain/entity_editor_browser_tabs_info/entity-editor-browser-tabs-info.model'; -import { EntityEditorBrowserTabsInfoDomainConstants } from 'domain/entity_editor_browser_tabs_info/entity-editor-browser-tabs-info-domain.constants'; -import { StoryEditorPageComponent } from './story-editor-page.component'; -import { PageTitleService } from '../../services/page-title.service'; -import { ComponentFixture, TestBed, fakeAsync, flush, tick } from '@angular/core/testing'; -import { PreventPageUnloadEventService } from 'services/prevent-page-unload-event.service'; -import { StoryEditorStateService } from './services/story-editor-state.service'; -import { UndoRedoService } from 'domain/editor/undo_redo/undo-redo.service'; -import { UrlService } from 'services/contextual/url.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { LocalStorageService } from 'services/local-storage.service'; -import { StoryEditorStalenessDetectionService } from './services/story-editor-staleness-detection.service'; -import { Story, StoryBackendDict } from 'domain/story/story.model'; -import { EditableStoryBackendApiService } from '../../domain/story/editable-story-backend-api.service'; -import { StoryEditorNavigationService } from './services/story-editor-navigation.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import {EventEmitter} from '@angular/core'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {EntityEditorBrowserTabsInfo} from 'domain/entity_editor_browser_tabs_info/entity-editor-browser-tabs-info.model'; +import {EntityEditorBrowserTabsInfoDomainConstants} from 'domain/entity_editor_browser_tabs_info/entity-editor-browser-tabs-info-domain.constants'; +import {StoryEditorPageComponent} from './story-editor-page.component'; +import {PageTitleService} from '../../services/page-title.service'; +import { + ComponentFixture, + TestBed, + fakeAsync, + flush, + tick, +} from '@angular/core/testing'; +import {PreventPageUnloadEventService} from 'services/prevent-page-unload-event.service'; +import {StoryEditorStateService} from './services/story-editor-state.service'; +import {UndoRedoService} from 'domain/editor/undo_redo/undo-redo.service'; +import {UrlService} from 'services/contextual/url.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {LocalStorageService} from 'services/local-storage.service'; +import {StoryEditorStalenessDetectionService} from './services/story-editor-staleness-detection.service'; +import {Story, StoryBackendDict} from 'domain/story/story.model'; +import {EditableStoryBackendApiService} from '../../domain/story/editable-story-backend-api.service'; +import {StoryEditorNavigationService} from './services/story-editor-navigation.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; class MockNgbModalRef { componentInstance: { @@ -61,7 +67,6 @@ class MockStoryEditorNavigationService { }; } - class MockEditableStoryBackendApiService { newBackendStoryObject!: StoryBackendDict; failure: string | null = null; @@ -77,18 +82,20 @@ class MockEditableStoryBackendApiService { story: this.newBackendStoryObject, topicName: 'Topic Name', storyIsPublished: false, - skillSummaries: [{ - id: 'Skill 1', - description: 'Skill Description', - language_code: 'en', - version: 1, - misconception_count: 0, - worked_examples_count: 0, - skill_model_created_on: 0, - skill_model_last_updated: 0, - }], + skillSummaries: [ + { + id: 'Skill 1', + description: 'Skill Description', + language_code: 'en', + version: 1, + misconception_count: 0, + worked_examples_count: 0, + skill_model_created_on: 0, + skill_model_last_updated: 0, + }, + ], classroomUrlFragment: 'classroomUrlFragment', - topicUrlFragment: 'topicUrlFragment' + topicUrlFragment: 'topicUrlFragment', }); } else { reject(); @@ -138,8 +145,7 @@ describe('Story Editor Page Component', () => { let undoRedoService: UndoRedoService; let urlInterpolationService: UrlInterpolationService; let localStorageService: LocalStorageService; - let storyEditorStalenessDetectionService: - StoryEditorStalenessDetectionService; + let storyEditorStalenessDetectionService: StoryEditorStalenessDetectionService; let urlService: UrlService; let storyEditorNavigationService: StoryEditorNavigationService; let story: Story; @@ -148,7 +154,7 @@ describe('Story Editor Page Component', () => { class MockWindowRef { nativeWindow = { open: (url: string) => {}, - addEventListener: (value1, value2) => {} + addEventListener: (value1, value2) => {}, }; } @@ -167,18 +173,18 @@ describe('Story Editor Page Component', () => { UrlInterpolationService, { provide: EditableStoryBackendApiService, - useClass: MockEditableStoryBackendApiService + useClass: MockEditableStoryBackendApiService, }, { provide: StoryEditorNavigationService, - useClass: MockStoryEditorNavigationService + useClass: MockStoryEditorNavigationService, }, { provide: WindowRef, - useClass: MockWindowRef - } + useClass: MockWindowRef, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); @@ -190,14 +196,16 @@ describe('Story Editor Page Component', () => { ngbModal = TestBed.inject(NgbModal); pageTitleService = TestBed.inject(PageTitleService); preventPageUnloadEventService = TestBed.inject( - PreventPageUnloadEventService); + PreventPageUnloadEventService + ); storyEditorStateService = TestBed.inject(StoryEditorStateService); undoRedoService = TestBed.inject(UndoRedoService); urlService = TestBed.inject(UrlService); urlInterpolationService = TestBed.inject(UrlInterpolationService); localStorageService = TestBed.inject(LocalStorageService); storyEditorStalenessDetectionService = TestBed.inject( - StoryEditorStalenessDetectionService); + StoryEditorStalenessDetectionService + ); windowRef = TestBed.inject(WindowRef); story = Story.createFromBackendDict({ @@ -207,133 +215,142 @@ describe('Story Editor Page Component', () => { notes: 'Story notes', story_contents: { initial_node_id: 'node_2', - nodes: [{ - id: 'node_2', - title: 'Title 2', - prerequisite_skill_ids: [], - acquired_skill_ids: [], - destination_node_ids: [], - outline: 'Outline', - exploration_id: 'asd4242', - outline_is_finalized: false, - description: 'Description', - thumbnail_filename: 'img.png', - thumbnail_bg_color: '#a33f40' - }, { - id: 'node_3', - title: 'Title 3', - prerequisite_skill_ids: [], - acquired_skill_ids: [], - destination_node_ids: [], - outline: 'Outline', - exploration_id: null, - outline_is_finalized: false, - description: 'Description', - thumbnail_filename: 'img.png', - thumbnail_bg_color: '#a33f40' - }], - next_node_id: 'node_4' + nodes: [ + { + id: 'node_2', + title: 'Title 2', + prerequisite_skill_ids: [], + acquired_skill_ids: [], + destination_node_ids: [], + outline: 'Outline', + exploration_id: 'asd4242', + outline_is_finalized: false, + description: 'Description', + thumbnail_filename: 'img.png', + thumbnail_bg_color: '#a33f40', + }, + { + id: 'node_3', + title: 'Title 3', + prerequisite_skill_ids: [], + acquired_skill_ids: [], + destination_node_ids: [], + outline: 'Outline', + exploration_id: null, + outline_is_finalized: false, + description: 'Description', + thumbnail_filename: 'img.png', + thumbnail_bg_color: '#a33f40', + }, + ], + next_node_id: 'node_4', }, language_code: 'en', version: 1, corresponding_topic_id: '2', thumbnail_bg_color: null, thumbnail_filename: null, - url_fragment: 'story-url-fragment' + url_fragment: 'story-url-fragment', } as StoryBackendDict); spyOn(storyEditorStateService, 'getStory').and.returnValue(story); localStorageService.removeOpenedEntityEditorBrowserTabsInfo( - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_STORY_EDITOR_BROWSER_TABS); + EntityEditorBrowserTabsInfoDomainConstants.OPENED_STORY_EDITOR_BROWSER_TABS + ); }); afterEach(() => { component.ngOnDestroy(); }); - it('should load story based on its id on url when component is initialized' + - ' and set page title', () => { - let storyInitializedEventEmitter = new EventEmitter(); - let storyReinitializedEventEmitter = new EventEmitter(); - spyOn(storyEditorStateService, 'loadStory').and.callFake(() => { - storyInitializedEventEmitter.emit(); - storyReinitializedEventEmitter.emit(); + it( + 'should load story based on its id on url when component is initialized' + + ' and set page title', + () => { + let storyInitializedEventEmitter = new EventEmitter(); + let storyReinitializedEventEmitter = new EventEmitter(); + spyOn(storyEditorStateService, 'loadStory').and.callFake(() => { + storyInitializedEventEmitter.emit(); + storyReinitializedEventEmitter.emit(); + }); + spyOnProperty( + storyEditorStateService, + 'onStoryInitialized' + ).and.returnValue(storyInitializedEventEmitter); + spyOnProperty( + storyEditorStateService, + 'onStoryReinitialized' + ).and.returnValue(storyReinitializedEventEmitter); + spyOn(urlService, 'getStoryIdFromUrl').and.returnValue('story_1'); + spyOn(pageTitleService, 'setDocumentTitle').and.callThrough(); + storyEditorNavigationService.checkIfPresentInChapterEditor = () => true; + + component.ngOnInit(); + + expect(storyEditorStateService.loadStory).toHaveBeenCalledWith('story_1'); + expect(pageTitleService.setDocumentTitle).toHaveBeenCalledTimes(2); + } + ); + + it( + 'should addListener by passing getChangeCount to ' + + 'PreventPageUnloadEventService', + () => { + spyOn(urlService, 'getStoryIdFromUrl').and.returnValue('story_1'); + spyOn(pageTitleService, 'setDocumentTitle'); + spyOn(undoRedoService, 'getChangeCount').and.returnValue(10); + spyOn(preventPageUnloadEventService, 'addListener').and.callFake( + callback => callback() + ); + + component.ngOnInit(); + + expect(preventPageUnloadEventService.addListener).toHaveBeenCalledWith( + jasmine.any(Function) + ); + } + ); + + it('should return to topic editor page when closing confirmation modal', () => { + spyOn(undoRedoService, 'getChangeCount').and.returnValue(1); + const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + componentInstance: MockNgbModalRef, + result: Promise.resolve(), + } as NgbModalRef; }); - spyOnProperty( - storyEditorStateService, 'onStoryInitialized').and.returnValue( - storyInitializedEventEmitter); - spyOnProperty( - storyEditorStateService, 'onStoryReinitialized').and.returnValue( - storyReinitializedEventEmitter); - spyOn(urlService, 'getStoryIdFromUrl').and.returnValue('story_1'); - spyOn(pageTitleService, 'setDocumentTitle').and.callThrough(); - storyEditorNavigationService - .checkIfPresentInChapterEditor = () => true; - component.ngOnInit(); + component.returnToTopicEditorPage(); - expect(storyEditorStateService.loadStory).toHaveBeenCalledWith('story_1'); - expect(pageTitleService.setDocumentTitle).toHaveBeenCalledTimes(2); + expect(modalSpy).toHaveBeenCalled(); }); - it('should addListener by passing getChangeCount to ' + - 'PreventPageUnloadEventService', () => { - spyOn(urlService, 'getStoryIdFromUrl').and.returnValue('story_1'); - spyOn(pageTitleService, 'setDocumentTitle'); - spyOn(undoRedoService, 'getChangeCount').and.returnValue(10); - spyOn(preventPageUnloadEventService, 'addListener').and - .callFake((callback) => callback()); - - component.ngOnInit(); - - expect(preventPageUnloadEventService.addListener) - .toHaveBeenCalledWith(jasmine.any(Function)); - }); - - it('should return to topic editor page when closing confirmation modal', - () => { - spyOn(undoRedoService, 'getChangeCount').and.returnValue(1); - const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ - componentInstance: MockNgbModalRef, - result: Promise.resolve() - }) as NgbModalRef; - }); - - component.returnToTopicEditorPage(); - - expect(modalSpy).toHaveBeenCalled(); + it('should return to topic editor page when dismissing confirmation modal', () => { + spyOn(undoRedoService, 'getChangeCount').and.returnValue(1); + const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + componentInstance: MockNgbModalRef, + result: Promise.reject(), + } as NgbModalRef; }); - it('should return to topic editor page when dismissing confirmation modal', - () => { - spyOn(undoRedoService, 'getChangeCount').and.returnValue(1); - const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ - componentInstance: MockNgbModalRef, - result: Promise.reject() - }) as NgbModalRef; - }); - - component.returnToTopicEditorPage(); + component.returnToTopicEditorPage(); - expect(modalSpy).toHaveBeenCalled(); - }); + expect(modalSpy).toHaveBeenCalled(); + }); - it('should open topic editor page when there is no change', - fakeAsync(() => { - spyOn(undoRedoService, 'getChangeCount').and.returnValue(0); - spyOn(urlInterpolationService, 'interpolateUrl').and.returnValue('/url'); - spyOn(windowRef.nativeWindow, 'open'); + it('should open topic editor page when there is no change', fakeAsync(() => { + spyOn(undoRedoService, 'getChangeCount').and.returnValue(0); + spyOn(urlInterpolationService, 'interpolateUrl').and.returnValue('/url'); + spyOn(windowRef.nativeWindow, 'open'); - component.returnToTopicEditorPage(); + component.returnToTopicEditorPage(); - flush(); - tick(); + flush(); + tick(); - expect(windowRef.nativeWindow.open).toHaveBeenCalledWith('/url', '_self'); - })); + expect(windowRef.nativeWindow.open).toHaveBeenCalledWith('/url', '_self'); + })); it('should return the active tab', () => { storyEditorNavigationService.activeTab = 'story_editor'; @@ -363,25 +380,31 @@ describe('Story Editor Page Component', () => { storyReinitializedEventEmitter.emit(); }); spyOnProperty( - storyEditorStateService, 'onStoryInitialized').and.returnValue( - storyInitializedEventEmitter); + storyEditorStateService, + 'onStoryInitialized' + ).and.returnValue(storyInitializedEventEmitter); spyOnProperty( - storyEditorStateService, 'onStoryReinitialized').and.returnValue( - storyReinitializedEventEmitter); + storyEditorStateService, + 'onStoryReinitialized' + ).and.returnValue(storyReinitializedEventEmitter); spyOn(urlService, 'getStoryIdFromUrl').and.returnValue('story_1'); spyOn(pageTitleService, 'setDocumentTitle').and.callThrough(); spyOn( storyEditorStateService, - 'getStoryWithUrlFragmentExists').and.returnValue(true); - spyOn(storyEditorStateService, 'getSkillSummaries').and.returnValue([{ - id: 'skill_id' - }]); + 'getStoryWithUrlFragmentExists' + ).and.returnValue(true); + spyOn(storyEditorStateService, 'getSkillSummaries').and.returnValue([ + { + id: 'skill_id', + }, + ]); storyEditorNavigationService.checkIfPresentInChapterEditor = () => true; component.ngOnInit(); - expect(component.validationIssues).toEqual( - ['Story URL fragment already exists.']); + expect(component.validationIssues).toEqual([ + 'Story URL fragment already exists.', + ]); }); it('should toggle the display of warnings', () => { @@ -413,12 +436,9 @@ describe('Story Editor Page Component', () => { spyOn(urlService, 'getStoryIdFromUrl').and.returnValue('story_1'); spyOn(pageTitleService, 'setDocumentTitle').and.callThrough(); storyEditorNavigationService.activeTab = 'story_preview'; - storyEditorNavigationService.checkIfPresentInChapterEditor = ( - () => false); - storyEditorNavigationService.checkIfPresentInStoryPreviewTab = ( - () => true); - storyEditorNavigationService.getActiveTab = ( - () => 'story_preview'); + storyEditorNavigationService.checkIfPresentInChapterEditor = () => false; + storyEditorNavigationService.checkIfPresentInStoryPreviewTab = () => true; + storyEditorNavigationService.getActiveTab = () => 'story_preview'; component.ngOnInit(); tick(); @@ -463,9 +483,9 @@ describe('Story Editor Page Component', () => { it('should init page on undo redo change applied', () => { let mockUndoRedoChangeEventEmitter = new EventEmitter(); - spyOn(undoRedoService, 'getUndoRedoChangeEventEmitter') - .and.returnValue( - mockUndoRedoChangeEventEmitter); + spyOn(undoRedoService, 'getUndoRedoChangeEventEmitter').and.returnValue( + mockUndoRedoChangeEventEmitter + ); spyOn(urlService, 'getStoryIdFromUrl').and.returnValue('story_1'); spyOn(pageTitleService, 'setDocumentTitle'); @@ -475,134 +495,157 @@ describe('Story Editor Page Component', () => { expect(pageTitleService.setDocumentTitle).toHaveBeenCalled(); }); - it('should create story editor browser tabs info on ' + - 'local storage when a new tab opens', () => { - spyOn(urlService, 'getStoryIdFromUrl').and.returnValue('story_1'); - spyOn(pageTitleService, 'setDocumentTitle'); - component.ngOnInit(); - - let storyEditorBrowserTabsInfo: EntityEditorBrowserTabsInfo = ( - localStorageService.getEntityEditorBrowserTabsInfo( - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_STORY_EDITOR_BROWSER_TABS, story.getId())); - - expect(storyEditorBrowserTabsInfo).toBeNull(); - - // Opening the first tab. - storyEditorStateService.onStoryInitialized.emit(); - storyEditorBrowserTabsInfo = ( - localStorageService.getEntityEditorBrowserTabsInfo( - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_STORY_EDITOR_BROWSER_TABS, story.getId())); - - expect(storyEditorBrowserTabsInfo).toBeDefined(); - expect(storyEditorBrowserTabsInfo.getNumberOfOpenedTabs()).toEqual(1); - - // Opening the second tab. - storyEditorStateService.onStoryInitialized.emit(); - storyEditorBrowserTabsInfo = ( - localStorageService.getEntityEditorBrowserTabsInfo( - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_STORY_EDITOR_BROWSER_TABS, story.getId())); - - expect(storyEditorBrowserTabsInfo.getNumberOfOpenedTabs()).toEqual(2); - }); - - it('should update story editor browser tabs info on local storage when ' + - 'some new changes are saved', () => { - spyOn(urlService, 'getStoryIdFromUrl').and.returnValue('story_1'); - spyOn(pageTitleService, 'setDocumentTitle'); - component.ngOnInit(); - - let storyEditorBrowserTabsInfo: EntityEditorBrowserTabsInfo = ( - localStorageService.getEntityEditorBrowserTabsInfo( - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_STORY_EDITOR_BROWSER_TABS, story.getId())); - - expect(storyEditorBrowserTabsInfo).toBeNull(); - - // First time opening of the tab. - storyEditorStateService.onStoryInitialized.emit(); - storyEditorBrowserTabsInfo = ( - localStorageService.getEntityEditorBrowserTabsInfo( - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_STORY_EDITOR_BROWSER_TABS, story.getId())); - - expect(storyEditorBrowserTabsInfo.getLatestVersion()).toEqual(1); - - // Save some changes on the story and increasing its version. - story._version = 2; - storyEditorStateService.onStoryReinitialized.emit(); - storyEditorBrowserTabsInfo = ( - localStorageService.getEntityEditorBrowserTabsInfo( - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_STORY_EDITOR_BROWSER_TABS, story.getId())); - - expect(storyEditorBrowserTabsInfo.getLatestVersion()).toEqual(2); - }); - - it('should decrement number of opened story editor tabs when ' + - 'a tab is closed', () => { - spyOn(undoRedoService, 'getChangeCount').and.returnValue(1); - spyOn(urlService, 'getStoryIdFromUrl').and.returnValue('story_1'); - spyOn(pageTitleService, 'setDocumentTitle'); - component.ngOnInit(); - - // Opening of the first tab. - storyEditorStateService.onStoryInitialized.emit(); - // Opening of the second tab. - storyEditorStateService.onStoryInitialized.emit(); - - let storyEditorBrowserTabsInfo: EntityEditorBrowserTabsInfo = ( - localStorageService.getEntityEditorBrowserTabsInfo( - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_STORY_EDITOR_BROWSER_TABS, story.getId())); - - // Making some unsaved changes on the editor page. - storyEditorBrowserTabsInfo.setSomeTabHasUnsavedChanges(true); - localStorageService.updateEntityEditorBrowserTabsInfo( - storyEditorBrowserTabsInfo, EntityEditorBrowserTabsInfoDomainConstants - .OPENED_STORY_EDITOR_BROWSER_TABS); - - expect( - storyEditorBrowserTabsInfo.doesSomeTabHaveUnsavedChanges() - ).toBeTrue(); - expect(storyEditorBrowserTabsInfo.getNumberOfOpenedTabs()).toEqual(2); - - component.onClosingStoryEditorBrowserTab(); - storyEditorBrowserTabsInfo = ( - localStorageService.getEntityEditorBrowserTabsInfo( - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_STORY_EDITOR_BROWSER_TABS, story.getId())); - - expect(storyEditorBrowserTabsInfo.getNumberOfOpenedTabs()).toEqual(1); - - // Since the tab containing unsaved changes is closed, the value of - // unsaved changes status will become false. - expect( - storyEditorBrowserTabsInfo.doesSomeTabHaveUnsavedChanges() - ).toBeFalse(); - }); + it( + 'should create story editor browser tabs info on ' + + 'local storage when a new tab opens', + () => { + spyOn(urlService, 'getStoryIdFromUrl').and.returnValue('story_1'); + spyOn(pageTitleService, 'setDocumentTitle'); + component.ngOnInit(); + + let storyEditorBrowserTabsInfo: EntityEditorBrowserTabsInfo = + localStorageService.getEntityEditorBrowserTabsInfo( + EntityEditorBrowserTabsInfoDomainConstants.OPENED_STORY_EDITOR_BROWSER_TABS, + story.getId() + ); + + expect(storyEditorBrowserTabsInfo).toBeNull(); + + // Opening the first tab. + storyEditorStateService.onStoryInitialized.emit(); + storyEditorBrowserTabsInfo = + localStorageService.getEntityEditorBrowserTabsInfo( + EntityEditorBrowserTabsInfoDomainConstants.OPENED_STORY_EDITOR_BROWSER_TABS, + story.getId() + ); + + expect(storyEditorBrowserTabsInfo).toBeDefined(); + expect(storyEditorBrowserTabsInfo.getNumberOfOpenedTabs()).toEqual(1); + + // Opening the second tab. + storyEditorStateService.onStoryInitialized.emit(); + storyEditorBrowserTabsInfo = + localStorageService.getEntityEditorBrowserTabsInfo( + EntityEditorBrowserTabsInfoDomainConstants.OPENED_STORY_EDITOR_BROWSER_TABS, + story.getId() + ); + + expect(storyEditorBrowserTabsInfo.getNumberOfOpenedTabs()).toEqual(2); + } + ); + + it( + 'should update story editor browser tabs info on local storage when ' + + 'some new changes are saved', + () => { + spyOn(urlService, 'getStoryIdFromUrl').and.returnValue('story_1'); + spyOn(pageTitleService, 'setDocumentTitle'); + component.ngOnInit(); + + let storyEditorBrowserTabsInfo: EntityEditorBrowserTabsInfo = + localStorageService.getEntityEditorBrowserTabsInfo( + EntityEditorBrowserTabsInfoDomainConstants.OPENED_STORY_EDITOR_BROWSER_TABS, + story.getId() + ); + + expect(storyEditorBrowserTabsInfo).toBeNull(); + + // First time opening of the tab. + storyEditorStateService.onStoryInitialized.emit(); + storyEditorBrowserTabsInfo = + localStorageService.getEntityEditorBrowserTabsInfo( + EntityEditorBrowserTabsInfoDomainConstants.OPENED_STORY_EDITOR_BROWSER_TABS, + story.getId() + ); + + expect(storyEditorBrowserTabsInfo.getLatestVersion()).toEqual(1); + + // Save some changes on the story and increasing its version. + story._version = 2; + storyEditorStateService.onStoryReinitialized.emit(); + storyEditorBrowserTabsInfo = + localStorageService.getEntityEditorBrowserTabsInfo( + EntityEditorBrowserTabsInfoDomainConstants.OPENED_STORY_EDITOR_BROWSER_TABS, + story.getId() + ); + + expect(storyEditorBrowserTabsInfo.getLatestVersion()).toEqual(2); + } + ); + + it( + 'should decrement number of opened story editor tabs when ' + + 'a tab is closed', + () => { + spyOn(undoRedoService, 'getChangeCount').and.returnValue(1); + spyOn(urlService, 'getStoryIdFromUrl').and.returnValue('story_1'); + spyOn(pageTitleService, 'setDocumentTitle'); + component.ngOnInit(); + + // Opening of the first tab. + storyEditorStateService.onStoryInitialized.emit(); + // Opening of the second tab. + storyEditorStateService.onStoryInitialized.emit(); + + let storyEditorBrowserTabsInfo: EntityEditorBrowserTabsInfo = + localStorageService.getEntityEditorBrowserTabsInfo( + EntityEditorBrowserTabsInfoDomainConstants.OPENED_STORY_EDITOR_BROWSER_TABS, + story.getId() + ); + + // Making some unsaved changes on the editor page. + storyEditorBrowserTabsInfo.setSomeTabHasUnsavedChanges(true); + localStorageService.updateEntityEditorBrowserTabsInfo( + storyEditorBrowserTabsInfo, + EntityEditorBrowserTabsInfoDomainConstants.OPENED_STORY_EDITOR_BROWSER_TABS + ); + + expect( + storyEditorBrowserTabsInfo.doesSomeTabHaveUnsavedChanges() + ).toBeTrue(); + expect(storyEditorBrowserTabsInfo.getNumberOfOpenedTabs()).toEqual(2); + + component.onClosingStoryEditorBrowserTab(); + storyEditorBrowserTabsInfo = + localStorageService.getEntityEditorBrowserTabsInfo( + EntityEditorBrowserTabsInfoDomainConstants.OPENED_STORY_EDITOR_BROWSER_TABS, + story.getId() + ); + + expect(storyEditorBrowserTabsInfo.getNumberOfOpenedTabs()).toEqual(1); + + // Since the tab containing unsaved changes is closed, the value of + // unsaved changes status will become false. + expect( + storyEditorBrowserTabsInfo.doesSomeTabHaveUnsavedChanges() + ).toBeFalse(); + } + ); + + it( + 'should emit the stale tab and presence of unsaved changes events ' + + "when the 'storage' event is triggered", + () => { + spyOn( + storyEditorStalenessDetectionService.staleTabEventEmitter, + 'emit' + ).and.callThrough(); + spyOn( + storyEditorStalenessDetectionService.presenceOfUnsavedChangesEventEmitter, + 'emit' + ).and.callThrough(); + + component.onCreateOrUpdateStoryEditorBrowserTabsInfo({ + key: 'opened_story_editor_browser_tabs', + }); - it('should emit the stale tab and presence of unsaved changes events ' + - 'when the \'storage\' event is triggered', () => { - spyOn( - storyEditorStalenessDetectionService.staleTabEventEmitter, 'emit' - ).and.callThrough(); - spyOn( - storyEditorStalenessDetectionService - .presenceOfUnsavedChangesEventEmitter, 'emit' - ).and.callThrough(); - - component.onCreateOrUpdateStoryEditorBrowserTabsInfo( - {key: 'opened_story_editor_browser_tabs'}); - - expect( - storyEditorStalenessDetectionService.staleTabEventEmitter.emit - ).toHaveBeenCalled(); - expect( - storyEditorStalenessDetectionService - .presenceOfUnsavedChangesEventEmitter.emit - ).toHaveBeenCalled(); - }); + expect( + storyEditorStalenessDetectionService.staleTabEventEmitter.emit + ).toHaveBeenCalled(); + expect( + storyEditorStalenessDetectionService + .presenceOfUnsavedChangesEventEmitter.emit + ).toHaveBeenCalled(); + } + ); }); diff --git a/core/templates/pages/story-editor-page/story-editor-page.component.ts b/core/templates/pages/story-editor-page/story-editor-page.component.ts index f36fb4e17a8e..bd207977ed39 100644 --- a/core/templates/pages/story-editor-page/story-editor-page.component.ts +++ b/core/templates/pages/story-editor-page/story-editor-page.component.ts @@ -16,33 +16,33 @@ * @fileoverview Component for the story editor page. */ -import { Subscription } from 'rxjs'; -import { SavePendingChangesModalComponent } from 'components/save-pending-changes/save-pending-changes-modal.component'; -import { EntityEditorBrowserTabsInfo } from 'domain/entity_editor_browser_tabs_info/entity-editor-browser-tabs-info.model'; -import { EntityEditorBrowserTabsInfoDomainConstants } from 'domain/entity_editor_browser_tabs_info/entity-editor-browser-tabs-info-domain.constants'; -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { UndoRedoService } from 'domain/editor/undo_redo/undo-redo.service'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { StoryEditorStateService } from './services/story-editor-state.service'; -import { PageTitleService } from 'services/page-title.service'; -import { StoryEditorNavigationService } from './services/story-editor-navigation.service'; -import { LocalStorageService } from 'services/local-storage.service'; -import { LoaderService } from 'services/loader.service'; -import { StoryValidationService } from 'domain/story/story-validation.service'; -import { EditableStoryBackendApiService } from 'domain/story/editable-story-backend-api.service'; -import { StoryEditorStalenessDetectionService } from './services/story-editor-staleness-detection.service'; -import { BottomNavbarStatusService } from 'services/bottom-navbar-status.service'; -import { PreventPageUnloadEventService } from 'services/prevent-page-unload-event.service'; -import { UrlService } from 'services/contextual/url.service'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { Story } from 'domain/story/story.model'; +import {Subscription} from 'rxjs'; +import {SavePendingChangesModalComponent} from 'components/save-pending-changes/save-pending-changes-modal.component'; +import {EntityEditorBrowserTabsInfo} from 'domain/entity_editor_browser_tabs_info/entity-editor-browser-tabs-info.model'; +import {EntityEditorBrowserTabsInfoDomainConstants} from 'domain/entity_editor_browser_tabs_info/entity-editor-browser-tabs-info-domain.constants'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {AppConstants} from 'app.constants'; +import {UndoRedoService} from 'domain/editor/undo_redo/undo-redo.service'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {StoryEditorStateService} from './services/story-editor-state.service'; +import {PageTitleService} from 'services/page-title.service'; +import {StoryEditorNavigationService} from './services/story-editor-navigation.service'; +import {LocalStorageService} from 'services/local-storage.service'; +import {LoaderService} from 'services/loader.service'; +import {StoryValidationService} from 'domain/story/story-validation.service'; +import {EditableStoryBackendApiService} from 'domain/story/editable-story-backend-api.service'; +import {StoryEditorStalenessDetectionService} from './services/story-editor-staleness-detection.service'; +import {BottomNavbarStatusService} from 'services/bottom-navbar-status.service'; +import {PreventPageUnloadEventService} from 'services/prevent-page-unload-event.service'; +import {UrlService} from 'services/contextual/url.service'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {Story} from 'domain/story/story.model'; @Component({ selector: 'oppia-story-editor-page', - templateUrl: './story-editor-page.component.html' + templateUrl: './story-editor-page.component.html', }) export class StoryEditorPageComponent implements OnInit, OnDestroy { warningsAreShown: boolean; @@ -64,8 +64,7 @@ export class StoryEditorPageComponent implements OnInit, OnDestroy { private loaderService: LoaderService, private storyValidationService: StoryValidationService, private editableStoryBackendApiService: EditableStoryBackendApiService, - private storyEditorStalenessDetectionService: - StoryEditorStalenessDetectionService, + private storyEditorStalenessDetectionService: StoryEditorStalenessDetectionService, private bottomNavbarStatusService: BottomNavbarStatusService, private preventPageUnloadEventService: PreventPageUnloadEventService, private urlService: UrlService @@ -79,36 +78,43 @@ export class StoryEditorPageComponent implements OnInit, OnDestroy { returnToTopicEditorPage(): void { if (this.undoRedoService.getChangeCount() > 0) { - const modalRef = this.ngbModal.open( - SavePendingChangesModalComponent, { - backdrop: true - }); - - modalRef.componentInstance.body = ( - 'Please save all pending changes before returning to the topic.'); - - modalRef.result.then(() => {}, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. + const modalRef = this.ngbModal.open(SavePendingChangesModalComponent, { + backdrop: true, }); + + modalRef.componentInstance.body = + 'Please save all pending changes before returning to the topic.'; + + modalRef.result.then( + () => {}, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } else { this.windowRef.nativeWindow.open( this.urlInterpolationService.interpolateUrl( - this.TOPIC_EDITOR_URL_TEMPLATE, { - topicId: - this.storyEditorStateService. - getStory().getCorrespondingTopicId() + this.TOPIC_EDITOR_URL_TEMPLATE, + { + topicId: this.storyEditorStateService + .getStory() + .getCorrespondingTopicId(), } - ), '_self'); + ), + '_self' + ); } } setDocumentTitle(): void { this.pageTitleService.setDocumentTitle( - this.storyEditorStateService.getStory().getTitle() + ' - Oppia'); + this.storyEditorStateService.getStory().getTitle() + ' - Oppia' + ); this.pageTitleService.setNavbarSubtitleForMobileView( - this.storyEditorStateService.getStory().getTitle()); + this.storyEditorStateService.getStory().getTitle() + ); } getActiveTab(): string { @@ -138,28 +144,31 @@ export class StoryEditorPageComponent implements OnInit, OnDestroy { _validateStory(): void { this.validationIssues = this.story.validate(); let nodes = this.story.getStoryContents().getNodes(); - let skillIdsInTopic = this.storyEditorStateService.getSkillSummaries().map( - skill => skill.id); + let skillIdsInTopic = this.storyEditorStateService + .getSkillSummaries() + .map(skill => skill.id); if (this.validationIssues.length === 0 && nodes.length > 0) { - let prerequisiteSkillValidationIssues = ( + let prerequisiteSkillValidationIssues = this.storyValidationService.validatePrerequisiteSkillsInStoryContents( - skillIdsInTopic, this.story.getStoryContents())); - this.validationIssues = ( - this.validationIssues.concat(prerequisiteSkillValidationIssues)); + skillIdsInTopic, + this.story.getStoryContents() + ); + this.validationIssues = this.validationIssues.concat( + prerequisiteSkillValidationIssues + ); } if (this.storyEditorStateService.getStoryWithUrlFragmentExists()) { - this.validationIssues.push( - 'Story URL fragment already exists.'); + this.validationIssues.push('Story URL fragment already exists.'); } this._validateExplorations(); - let storyPrepublishValidationIssues = ( - this.story.prepublishValidate()); - let nodePrepublishValidationIssues = ( - [].concat.apply([], nodes.map( - (node) => node.prepublishValidate()))); - this.prepublishValidationIssues = ( - storyPrepublishValidationIssues.concat( - nodePrepublishValidationIssues)); + let storyPrepublishValidationIssues = this.story.prepublishValidate(); + let nodePrepublishValidationIssues = [].concat.apply( + [], + nodes.map(node => node.prepublishValidate()) + ); + this.prepublishValidationIssues = storyPrepublishValidationIssues.concat( + nodePrepublishValidationIssues + ); } _validateExplorations(): void { @@ -168,24 +177,26 @@ export class StoryEditorPageComponent implements OnInit, OnDestroy { if ( this.storyEditorStateService.areAnyExpIdsChanged() || - this.forceValidateExplorations) { + this.forceValidateExplorations + ) { this.explorationValidationIssues = []; for (let i = 0; i < nodes.length; i++) { if (nodes[i].getExplorationId() !== null) { explorationIds.push(nodes[i].getExplorationId()); } else { this.explorationValidationIssues.push( - 'Some chapters don\'t have exploration IDs provided.'); + "Some chapters don't have exploration IDs provided." + ); } } this.forceValidateExplorations = false; if (explorationIds.length > 0) { - this.editableStoryBackendApiService.validateExplorationsAsync( - this.story.getId(), explorationIds - ).then((validationIssues) => { - this.explorationValidationIssues = + this.editableStoryBackendApiService + .validateExplorationsAsync(this.story.getId(), explorationIds) + .then(validationIssues => { + this.explorationValidationIssues = this.explorationValidationIssues.concat(validationIssues); - }); + }); } } this.storyEditorStateService.resetExpIdsChanged(); @@ -194,8 +205,9 @@ export class StoryEditorPageComponent implements OnInit, OnDestroy { getTotalWarningsCount(): number { return ( this.validationIssues.length + - this.explorationValidationIssues.length + - this.prepublishValidationIssues.length); + this.explorationValidationIssues.length + + this.prepublishValidationIssues.length + ); } _initPage(): void { @@ -215,91 +227,97 @@ export class StoryEditorPageComponent implements OnInit, OnDestroy { onClosingStoryEditorBrowserTab(): void { const story = this.storyEditorStateService.getStory(); - const storyEditorBrowserTabsInfo: EntityEditorBrowserTabsInfo = ( + const storyEditorBrowserTabsInfo: EntityEditorBrowserTabsInfo = this.localStorageService.getEntityEditorBrowserTabsInfo( - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_STORY_EDITOR_BROWSER_TABS, story.getId())); + EntityEditorBrowserTabsInfoDomainConstants.OPENED_STORY_EDITOR_BROWSER_TABS, + story.getId() + ); - if (storyEditorBrowserTabsInfo.doesSomeTabHaveUnsavedChanges() && - this.undoRedoService.getChangeCount() > 0) { + if ( + storyEditorBrowserTabsInfo.doesSomeTabHaveUnsavedChanges() && + this.undoRedoService.getChangeCount() > 0 + ) { storyEditorBrowserTabsInfo.setSomeTabHasUnsavedChanges(false); } storyEditorBrowserTabsInfo.decrementNumberOfOpenedTabs(); this.localStorageService.updateEntityEditorBrowserTabsInfo( storyEditorBrowserTabsInfo, - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_STORY_EDITOR_BROWSER_TABS); + EntityEditorBrowserTabsInfoDomainConstants.OPENED_STORY_EDITOR_BROWSER_TABS + ); } createStoryEditorBrowserTabsInfo(): void { const story = this.storyEditorStateService.getStory(); - let storyEditorBrowserTabsInfo: EntityEditorBrowserTabsInfo = ( + let storyEditorBrowserTabsInfo: EntityEditorBrowserTabsInfo = this.localStorageService.getEntityEditorBrowserTabsInfo( - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_STORY_EDITOR_BROWSER_TABS, story.getId())); + EntityEditorBrowserTabsInfoDomainConstants.OPENED_STORY_EDITOR_BROWSER_TABS, + story.getId() + ); if (storyEditorBrowserTabsInfo) { storyEditorBrowserTabsInfo.setLatestVersion(story.getVersion()); storyEditorBrowserTabsInfo.incrementNumberOfOpenedTabs(); } else { storyEditorBrowserTabsInfo = EntityEditorBrowserTabsInfo.create( - 'story', story.getId(), story.getVersion(), 1, false); + 'story', + story.getId(), + story.getVersion(), + 1, + false + ); } this.localStorageService.updateEntityEditorBrowserTabsInfo( storyEditorBrowserTabsInfo, - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_STORY_EDITOR_BROWSER_TABS); + EntityEditorBrowserTabsInfoDomainConstants.OPENED_STORY_EDITOR_BROWSER_TABS + ); } updateStoryEditorBrowserTabsInfo(): void { const story = this.storyEditorStateService.getStory(); - const storyEditorBrowserTabsInfo: EntityEditorBrowserTabsInfo = ( + const storyEditorBrowserTabsInfo: EntityEditorBrowserTabsInfo = this.localStorageService.getEntityEditorBrowserTabsInfo( - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_STORY_EDITOR_BROWSER_TABS, story.getId())); + EntityEditorBrowserTabsInfoDomainConstants.OPENED_STORY_EDITOR_BROWSER_TABS, + story.getId() + ); storyEditorBrowserTabsInfo.setLatestVersion(story.getVersion()); storyEditorBrowserTabsInfo.setSomeTabHasUnsavedChanges(false); this.localStorageService.updateEntityEditorBrowserTabsInfo( storyEditorBrowserTabsInfo, - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_STORY_EDITOR_BROWSER_TABS); + EntityEditorBrowserTabsInfoDomainConstants.OPENED_STORY_EDITOR_BROWSER_TABS + ); } - onCreateOrUpdateStoryEditorBrowserTabsInfo(event: { key: string }): void { - if (event.key === ( - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_STORY_EDITOR_BROWSER_TABS) + onCreateOrUpdateStoryEditorBrowserTabsInfo(event: {key: string}): void { + if ( + event.key === + EntityEditorBrowserTabsInfoDomainConstants.OPENED_STORY_EDITOR_BROWSER_TABS ) { - this.storyEditorStalenessDetectionService - .staleTabEventEmitter.emit(); - this.storyEditorStalenessDetectionService - .presenceOfUnsavedChangesEventEmitter.emit(); + this.storyEditorStalenessDetectionService.staleTabEventEmitter.emit(); + this.storyEditorStalenessDetectionService.presenceOfUnsavedChangesEventEmitter.emit(); } } ngOnInit(): void { this.loaderService.showLoadingScreen('Loading Story'); this.directiveSubscriptions.add( - this.storyEditorStateService.onStoryInitialized.subscribe( - () => { - this._initPage(); - this.createStoryEditorBrowserTabsInfo(); - this.loaderService.hideLoadingScreen(); - } - )); + this.storyEditorStateService.onStoryInitialized.subscribe(() => { + this._initPage(); + this.createStoryEditorBrowserTabsInfo(); + this.loaderService.hideLoadingScreen(); + }) + ); this.directiveSubscriptions.add( - this.storyEditorStateService.onStoryReinitialized.subscribe( - () => { - this._initPage(); - this.updateStoryEditorBrowserTabsInfo(); - } - )); + this.storyEditorStateService.onStoryReinitialized.subscribe(() => { + this._initPage(); + this.updateStoryEditorBrowserTabsInfo(); + }) + ); this.validationIssues = []; this.prepublishValidationIssues = []; this.explorationValidationIssues = []; @@ -307,7 +325,8 @@ export class StoryEditorPageComponent implements OnInit, OnDestroy { this.warningsAreShown = false; this.bottomNavbarStatusService.markBottomNavbarStatus(true); this.preventPageUnloadEventService.addListener( - this.undoRedoService.getChangeCount.bind(this.undoRedoService)); + this.undoRedoService.getChangeCount.bind(this.undoRedoService) + ); this.storyEditorStateService.loadStory(this.urlService.getStoryIdFromUrl()); this.story = this.storyEditorStateService.getStory(); @@ -316,20 +335,24 @@ export class StoryEditorPageComponent implements OnInit, OnDestroy { if (this.storyEditorNavigationService.checkIfPresentInChapterEditor()) { this.storyEditorNavigationService.navigateToChapterEditor(); } else if ( - this.storyEditorNavigationService.checkIfPresentInStoryPreviewTab()) { + this.storyEditorNavigationService.checkIfPresentInStoryPreviewTab() + ) { this.storyEditorNavigationService.navigateToStoryPreviewTab(); } this.directiveSubscriptions.add( - this.undoRedoService.getUndoRedoChangeEventEmitter().subscribe( - () => this._initPage() - ) + this.undoRedoService + .getUndoRedoChangeEventEmitter() + .subscribe(() => this._initPage()) ); this.storyEditorStalenessDetectionService.init(); this.windowRef.nativeWindow.addEventListener( - 'beforeunload', this.onClosingStoryEditorBrowserTab); + 'beforeunload', + this.onClosingStoryEditorBrowserTab + ); this.localStorageService.registerNewStorageEventListener( - this.onCreateOrUpdateStoryEditorBrowserTabsInfo); + this.onCreateOrUpdateStoryEditorBrowserTabsInfo + ); } ngOnDestroy(): void { @@ -337,6 +360,9 @@ export class StoryEditorPageComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive('oppiaStoryEditorPage', downgradeComponent({ - component: StoryEditorPageComponent -})); +angular.module('oppia').directive( + 'oppiaStoryEditorPage', + downgradeComponent({ + component: StoryEditorPageComponent, + }) +); diff --git a/core/templates/pages/story-editor-page/story-editor-page.constants.ajs.ts b/core/templates/pages/story-editor-page/story-editor-page.constants.ajs.ts index cd39dd79d30e..d1b3ac962392 100644 --- a/core/templates/pages/story-editor-page/story-editor-page.constants.ajs.ts +++ b/core/templates/pages/story-editor-page/story-editor-page.constants.ajs.ts @@ -18,8 +18,8 @@ // TODO(#7092): Delete this file once migration is complete and these AngularJS // equivalents of the Angular constants are no longer needed. -import { StoryEditorPageConstants } from - 'pages/story-editor-page/story-editor-page.constants'; +import {StoryEditorPageConstants} from 'pages/story-editor-page/story-editor-page.constants'; -angular.module('oppia').constant( - 'NODE_ID_PREFIX', StoryEditorPageConstants.NODE_ID_PREFIX); +angular + .module('oppia') + .constant('NODE_ID_PREFIX', StoryEditorPageConstants.NODE_ID_PREFIX); diff --git a/core/templates/pages/story-editor-page/story-editor-page.constants.ts b/core/templates/pages/story-editor-page/story-editor-page.constants.ts index 5f14efeee172..7d19b1d2071f 100644 --- a/core/templates/pages/story-editor-page/story-editor-page.constants.ts +++ b/core/templates/pages/story-editor-page/story-editor-page.constants.ts @@ -17,5 +17,5 @@ */ export const StoryEditorPageConstants = { - NODE_ID_PREFIX: 'node_' + NODE_ID_PREFIX: 'node_', } as const; diff --git a/core/templates/pages/story-editor-page/story-editor-page.import.ts b/core/templates/pages/story-editor-page/story-editor-page.import.ts index 750715d7b1dc..302d72ff6f43 100644 --- a/core/templates/pages/story-editor-page/story-editor-page.import.ts +++ b/core/templates/pages/story-editor-page/story-editor-page.import.ts @@ -23,10 +23,15 @@ import uiValidate from 'angular-ui-validate'; import 'third-party-imports/dnd-lists.import'; angular.module('oppia', [ - require('angular-cookies'), 'dndLists', 'ngAnimate', - 'ngMaterial', 'ngSanitize', 'ngTouch', 'pascalprecht.translate', + require('angular-cookies'), + 'dndLists', + 'ngAnimate', + 'ngMaterial', + 'ngSanitize', + 'ngTouch', + 'pascalprecht.translate', 'ui.bootstrap', - uiValidate + uiValidate, ]); require('Polyfills.ts'); @@ -38,11 +43,8 @@ require('App.ts'); require('base-components/oppia-root.directive.ts'); require('base-components/base-content.component.ts'); -require( - 'pages/story-editor-page/chapter-editor/chapter-editor-tab.component.ts'); -require( - 'pages/story-editor-page/story-preview-tab/story-preview-tab.component.ts'); -require( - 'pages/story-editor-page/navbar/story-editor-navbar-breadcrumb.component.ts'); +require('pages/story-editor-page/chapter-editor/chapter-editor-tab.component.ts'); +require('pages/story-editor-page/story-preview-tab/story-preview-tab.component.ts'); +require('pages/story-editor-page/navbar/story-editor-navbar-breadcrumb.component.ts'); require('pages/story-editor-page/navbar/story-editor-navbar.component.ts'); require('pages/story-editor-page/story-editor-page.component.ts'); diff --git a/core/templates/pages/story-editor-page/story-editor-page.module.ts b/core/templates/pages/story-editor-page/story-editor-page.module.ts index 6c246de69825..90c015c0148b 100644 --- a/core/templates/pages/story-editor-page/story-editor-page.module.ts +++ b/core/templates/pages/story-editor-page/story-editor-page.module.ts @@ -16,36 +16,37 @@ * @fileoverview Module for the story editor page. */ -import { APP_INITIALIZER, NgModule, StaticProvider } from '@angular/core'; -import { BrowserModule, HAMMER_GESTURE_CONFIG } from '@angular/platform-browser'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { HttpClientModule } from '@angular/common/http'; -import { HTTP_INTERCEPTORS } from '@angular/common/http'; -import { RouterModule } from '@angular/router'; -import { APP_BASE_HREF } from '@angular/common'; +import {APP_INITIALIZER, NgModule, StaticProvider} from '@angular/core'; +import {BrowserModule, HAMMER_GESTURE_CONFIG} from '@angular/platform-browser'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {HttpClientModule} from '@angular/common/http'; +import {HTTP_INTERCEPTORS} from '@angular/common/http'; +import {RouterModule} from '@angular/router'; +import {APP_BASE_HREF} from '@angular/common'; -import { RequestInterceptor } from 'services/request-interceptor.service'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { OppiaAngularRootComponent } from - 'components/oppia-angular-root.component'; -import { platformFeatureInitFactory, PlatformFeatureService } from - 'services/platform-feature.service'; -import { StoryEditorNavbarComponent } from './navbar/story-editor-navbar.component'; -import { StoryEditorNavbarBreadcrumbComponent } from './navbar/story-editor-navbar-breadcrumb.component'; -import { StoryEditorSaveModalComponent } from './modal-templates/story-editor-save-modal.component'; -import { StoryEditorUnpublishModalComponent } from './modal-templates/story-editor-unpublish-modal.component'; -import { DraftChapterConfirmationModalComponent } from './modal-templates/draft-chapter-confirmation-modal.component'; -import { StoryPreviewTabComponent } from './story-preview-tab/story-preview-tab.component'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { MyHammerConfig, toastrConfig } from 'pages/oppia-root/app.module'; -import { SmartRouterModule } from 'hybrid-router-module-provider'; -import { AppErrorHandlerProvider } from 'pages/oppia-root/app-error-handler'; -import { StoryNodeEditorComponent } from './editor-tab/story-node-editor.component'; -import { ChapterEditorTabComponent } from './chapter-editor/chapter-editor-tab.component'; -import { StoryEditorComponent } from './editor-tab/story-editor.component'; -import { StoryEditorPageComponent } from './story-editor-page.component'; -import { DeleteChapterModalComponent } from './modal-templates/delete-chapter-modal.component'; -import { NewChapterTitleModalComponent } from './modal-templates/new-chapter-title-modal.component'; +import {RequestInterceptor} from 'services/request-interceptor.service'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {OppiaAngularRootComponent} from 'components/oppia-angular-root.component'; +import { + platformFeatureInitFactory, + PlatformFeatureService, +} from 'services/platform-feature.service'; +import {StoryEditorNavbarComponent} from './navbar/story-editor-navbar.component'; +import {StoryEditorNavbarBreadcrumbComponent} from './navbar/story-editor-navbar-breadcrumb.component'; +import {StoryEditorSaveModalComponent} from './modal-templates/story-editor-save-modal.component'; +import {StoryEditorUnpublishModalComponent} from './modal-templates/story-editor-unpublish-modal.component'; +import {DraftChapterConfirmationModalComponent} from './modal-templates/draft-chapter-confirmation-modal.component'; +import {StoryPreviewTabComponent} from './story-preview-tab/story-preview-tab.component'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {MyHammerConfig, toastrConfig} from 'pages/oppia-root/app.module'; +import {SmartRouterModule} from 'hybrid-router-module-provider'; +import {AppErrorHandlerProvider} from 'pages/oppia-root/app-error-handler'; +import {StoryNodeEditorComponent} from './editor-tab/story-node-editor.component'; +import {ChapterEditorTabComponent} from './chapter-editor/chapter-editor-tab.component'; +import {StoryEditorComponent} from './editor-tab/story-editor.component'; +import {StoryEditorPageComponent} from './story-editor-page.component'; +import {DeleteChapterModalComponent} from './modal-templates/delete-chapter-modal.component'; +import {NewChapterTitleModalComponent} from './modal-templates/new-chapter-title-modal.component'; @NgModule({ imports: [ @@ -57,7 +58,7 @@ import { NewChapterTitleModalComponent } from './modal-templates/new-chapter-tit SmartRouterModule, RouterModule.forRoot([]), SharedComponentsModule, - ToastrModule.forRoot(toastrConfig) + ToastrModule.forRoot(toastrConfig), ], declarations: [ StoryEditorNavbarBreadcrumbComponent, @@ -71,7 +72,7 @@ import { NewChapterTitleModalComponent } from './modal-templates/new-chapter-tit StoryEditorComponent, NewChapterTitleModalComponent, StoryEditorPageComponent, - DeleteChapterModalComponent + DeleteChapterModalComponent, ], entryComponents: [ StoryEditorNavbarBreadcrumbComponent, @@ -85,41 +86,41 @@ import { NewChapterTitleModalComponent } from './modal-templates/new-chapter-tit StoryEditorComponent, NewChapterTitleModalComponent, StoryEditorPageComponent, - DeleteChapterModalComponent + DeleteChapterModalComponent, ], providers: [ { provide: HTTP_INTERCEPTORS, useClass: RequestInterceptor, - multi: true + multi: true, }, { provide: APP_INITIALIZER, useFactory: platformFeatureInitFactory, deps: [PlatformFeatureService], - multi: true + multi: true, }, { provide: HAMMER_GESTURE_CONFIG, - useClass: MyHammerConfig + useClass: MyHammerConfig, }, AppErrorHandlerProvider, { provide: APP_BASE_HREF, - useValue: '/' - } - ] + useValue: '/', + }, + ], }) class StoryEditorPageModule { // Empty placeholder method to satisfy the `Compiler`. ngDoBootstrap() {} } -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { downgradeModule } from '@angular/upgrade/static'; -import { ToastrModule } from 'ngx-toastr'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {downgradeModule} from '@angular/upgrade/static'; +import {ToastrModule} from 'ngx-toastr'; -const bootstrapFnAsync = async(extraProviders: StaticProvider[]) => { +const bootstrapFnAsync = async (extraProviders: StaticProvider[]) => { const platformRef = platformBrowserDynamic(extraProviders); return platformRef.bootstrapModule(StoryEditorPageModule); }; @@ -134,5 +135,6 @@ angular.module('oppia').directive( // bootstrap the Angular 8. 'oppiaAngularRoot', downgradeComponent({ - component: OppiaAngularRootComponent - }) as angular.IDirectiveFactory); + component: OppiaAngularRootComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/story-editor-page/story-preview-tab/story-preview-tab.component.spec.ts b/core/templates/pages/story-editor-page/story-preview-tab/story-preview-tab.component.spec.ts index 60e32014b3a2..f7af30a93ed0 100644 --- a/core/templates/pages/story-editor-page/story-preview-tab/story-preview-tab.component.spec.ts +++ b/core/templates/pages/story-editor-page/story-preview-tab/story-preview-tab.component.spec.ts @@ -16,15 +16,14 @@ * @fileoverview Unit tests for story preview tab component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { EventEmitter } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { StoryEditorNavigationService } from - 'pages/story-editor-page/services/story-editor-navigation.service'; -import { Story } from 'domain/story/story.model'; -import { StoryPreviewTabComponent } from './story-preview-tab.component'; -import { StoryEditorStateService } from '../services/story-editor-state.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {EventEmitter} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {StoryEditorNavigationService} from 'pages/story-editor-page/services/story-editor-navigation.service'; +import {Story} from 'domain/story/story.model'; +import {StoryPreviewTabComponent} from './story-preview-tab.component'; +import {StoryEditorStateService} from '../services/story-editor-state.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; class MockStoryEditorNavigationService { activeTab!: 'story_preview'; @@ -44,15 +43,17 @@ describe('Story Preview tab', () => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], declarations: [StoryPreviewTabComponent, MockTranslatePipe], - providers: [{ - StoryEditorNavigationService, - provide: [ - { - provide: StoryEditorNavigationService, - UseClass: MockStoryEditorNavigationService - } - ] - }] + providers: [ + { + StoryEditorNavigationService, + provide: [ + { + provide: StoryEditorNavigationService, + UseClass: MockStoryEditorNavigationService, + }, + ], + }, + ], }).compileComponents(); }); beforeEach(() => { @@ -85,8 +86,9 @@ describe('Story Preview tab', () => { planned_publication_date_msecs: 10, last_modified_msecs: 20, first_publication_date_msecs: 10, - unpublishing_reason: null - }, { + unpublishing_reason: null, + }, + { id: 'node_2', title: 'Title 2', description: 'Description 2', @@ -102,15 +104,16 @@ describe('Story Preview tab', () => { planned_publication_date_msecs: 10, last_modified_msecs: 20, first_publication_date_msecs: 10, - unpublishing_reason: null - }], - next_node_id: 'node_3' + unpublishing_reason: null, + }, + ], + next_node_id: 'node_3', }, language_code: 'en', thumbnail_filename: 'fileName', thumbnail_bg_color: 'blue', url_fragment: 'url', - meta_tag_content: 'meta' + meta_tag_content: 'meta', }); storyInitializedEventEmitter = new EventEmitter(); @@ -120,11 +123,13 @@ describe('Story Preview tab', () => { spyOnProperty(storyEditorStateService, 'onStoryInitialized').and.callFake( () => { return storyInitializedEventEmitter; - }); + } + ); spyOnProperty(storyEditorStateService, 'onStoryReinitialized').and.callFake( () => { return storyReinitializedEventEmitter; - }); + } + ); }); afterEach(() => { @@ -141,18 +146,19 @@ describe('Story Preview tab', () => { component.ngOnInit(); let node = story.getStoryContents().getNodes()[0]; expect(component.getExplorationUrl(node)).toEqual( - '/explore/exp_1?story_id=storyId_0&node_id=node_1'); + '/explore/exp_1?story_id=storyId_0&node_id=node_1' + ); node = story.getStoryContents().getNodes()[1]; expect(component.getExplorationUrl(node)).toEqual( - '/explore/exp_2?story_id=storyId_0&node_id=node_2'); + '/explore/exp_2?story_id=storyId_0&node_id=node_2' + ); }); - it('should called initEditor on calls from story being initialized', - () => { - spyOn(component, 'initEditor').and.callThrough(); - component.ngOnInit(); - storyInitializedEventEmitter.emit(); - storyReinitializedEventEmitter.emit(); - expect(component.initEditor).toHaveBeenCalledTimes(3); - }); + it('should called initEditor on calls from story being initialized', () => { + spyOn(component, 'initEditor').and.callThrough(); + component.ngOnInit(); + storyInitializedEventEmitter.emit(); + storyReinitializedEventEmitter.emit(); + expect(component.initEditor).toHaveBeenCalledTimes(3); + }); }); diff --git a/core/templates/pages/story-editor-page/story-preview-tab/story-preview-tab.component.ts b/core/templates/pages/story-editor-page/story-preview-tab/story-preview-tab.component.ts index 12f7416f8cdd..de614d5d0ea1 100644 --- a/core/templates/pages/story-editor-page/story-preview-tab/story-preview-tab.component.ts +++ b/core/templates/pages/story-editor-page/story-preview-tab/story-preview-tab.component.ts @@ -16,27 +16,27 @@ * @fileoverview Component for the story preview tab. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { StoryNode } from 'domain/story/story-node.model'; -import { StoryContents } from 'domain/story/story-contents-object.model'; -import { Story } from 'domain/story/story.model'; -import { Subscription } from 'rxjs'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; -import { UrlService } from 'services/contextual/url.service'; -import { StoryEditorStateService } from '../services/story-editor-state.service'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {StoryNode} from 'domain/story/story-node.model'; +import {StoryContents} from 'domain/story/story-contents-object.model'; +import {Story} from 'domain/story/story.model'; +import {Subscription} from 'rxjs'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; +import {UrlService} from 'services/contextual/url.service'; +import {StoryEditorStateService} from '../services/story-editor-state.service'; interface IconsArray { - // Thumbnails for the story nodes are null if they are not yet uploaded. - // Means when we are making a new story with no nodes, the thumbnails are - // null. - 'thumbnailIconUrl': string | null; - 'thumbnailBgColor': string | null; + // Thumbnails for the story nodes are null if they are not yet uploaded. + // Means when we are making a new story with no nodes, the thumbnails are + // null. + thumbnailIconUrl: string | null; + thumbnailBgColor: string | null; } @Component({ selector: 'oppia-story-preview-tab', - templateUrl: './story-preview-tab.component.html' + templateUrl: './story-preview-tab.component.html', }) export class StoryPreviewTabComponent implements OnInit, OnDestroy { // These properties are initialized using Angular lifecycle hooks @@ -50,7 +50,7 @@ export class StoryPreviewTabComponent implements OnInit, OnDestroy { constructor( private storyEditorStateService: StoryEditorStateService, private assetsBackendApiService: AssetsBackendApiService, - private urlService: UrlService, + private urlService: UrlService ) {} directiveSubscriptions = new Subscription(); @@ -58,8 +58,7 @@ export class StoryPreviewTabComponent implements OnInit, OnDestroy { this.story = this.storyEditorStateService.getStory(); this.storyId = this.story.getId(); this.storyContents = this.story.getStoryContents(); - if (this.storyContents && - this.storyContents.getNodes().length > 0) { + if (this.storyContents && this.storyContents.getNodes().length > 0) { this.nodes = this.storyContents.getNodes(); this.pathIconParameters = this.generatePathIconParameters(); } @@ -69,23 +68,30 @@ export class StoryPreviewTabComponent implements OnInit, OnDestroy { var storyNodes = this.nodes; var iconParametersArray = []; let thumbnailFilename = storyNodes[0].getThumbnailFilename(); - let thumbnailIconUrl = thumbnailFilename ? ( - this.assetsBackendApiService.getThumbnailUrlForPreview( - 'story', this.storyId, thumbnailFilename)) : null; + let thumbnailIconUrl = thumbnailFilename + ? this.assetsBackendApiService.getThumbnailUrlForPreview( + 'story', + this.storyId, + thumbnailFilename + ) + : null; iconParametersArray.push({ thumbnailIconUrl: thumbnailIconUrl, - thumbnailBgColor: storyNodes[0].getThumbnailBgColor() + thumbnailBgColor: storyNodes[0].getThumbnailBgColor(), }); - for ( - var i = 1; i < this.nodes.length; i++) { + for (var i = 1; i < this.nodes.length; i++) { let thumbnailFilename = storyNodes[i].getThumbnailFilename(); - thumbnailIconUrl = thumbnailFilename ? ( - this.assetsBackendApiService.getThumbnailUrlForPreview( - 'story', this.storyId, thumbnailFilename)) : null; + thumbnailIconUrl = thumbnailFilename + ? this.assetsBackendApiService.getThumbnailUrlForPreview( + 'story', + this.storyId, + thumbnailFilename + ) + : null; iconParametersArray.push({ thumbnailIconUrl: thumbnailIconUrl, - thumbnailBgColor: storyNodes[i].getThumbnailBgColor() + thumbnailBgColor: storyNodes[i].getThumbnailBgColor(), }); } return iconParametersArray; @@ -93,22 +99,20 @@ export class StoryPreviewTabComponent implements OnInit, OnDestroy { getExplorationUrl(node: StoryNode): string { var result = '/explore/' + node.getExplorationId(); - result = this.urlService.addField( - result, 'story_id', this.storyId); - result = this.urlService.addField( - result, 'node_id', node.getId()); + result = this.urlService.addField(result, 'story_id', this.storyId); + result = this.urlService.addField(result, 'node_id', node.getId()); return result; } ngOnInit(): void { this.directiveSubscriptions.add( - this.storyEditorStateService.onStoryInitialized.subscribe( - () => this.initEditor() + this.storyEditorStateService.onStoryInitialized.subscribe(() => + this.initEditor() ) ); this.directiveSubscriptions.add( - this.storyEditorStateService.onStoryReinitialized.subscribe( - () => this.initEditor() + this.storyEditorStateService.onStoryReinitialized.subscribe(() => + this.initEditor() ) ); this.initEditor(); @@ -118,6 +122,9 @@ export class StoryPreviewTabComponent implements OnInit, OnDestroy { this.directiveSubscriptions.unsubscribe(); } } -angular.module('oppia').directive( - 'oppiaStoryPreviewTab', downgradeComponent( - {component: StoryPreviewTabComponent})); +angular + .module('oppia') + .directive( + 'oppiaStoryPreviewTab', + downgradeComponent({component: StoryPreviewTabComponent}) + ); diff --git a/core/templates/pages/story-viewer-page/navbar-breadcrumb/story-viewer-navbar-breadcrumb.component.spec.ts b/core/templates/pages/story-viewer-page/navbar-breadcrumb/story-viewer-navbar-breadcrumb.component.spec.ts index f261052b9f0d..095996a204bd 100644 --- a/core/templates/pages/story-viewer-page/navbar-breadcrumb/story-viewer-navbar-breadcrumb.component.spec.ts +++ b/core/templates/pages/story-viewer-page/navbar-breadcrumb/story-viewer-navbar-breadcrumb.component.spec.ts @@ -16,17 +16,20 @@ * @fileoverview Unit tests for storyViewerNavbarBreadcrumb. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { StoryViewerNavbarBreadcrumbComponent } from './story-viewer-navbar-breadcrumb.component'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { StoryViewerBackendApiService } from 'domain/story_viewer/story-viewer-backend-api.service'; -import { UrlService } from 'services/contextual/url.service'; -import { StoryPlaythrough } from 'domain/story_viewer/story-playthrough.model'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { TopicViewerBackendApiService } from 'domain/topic_viewer/topic-viewer-backend-api.service'; -import { ReadOnlyTopicBackendDict, ReadOnlyTopicObjectFactory } from 'domain/topic_viewer/read-only-topic-object.factory'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {StoryViewerNavbarBreadcrumbComponent} from './story-viewer-navbar-breadcrumb.component'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {StoryViewerBackendApiService} from 'domain/story_viewer/story-viewer-backend-api.service'; +import {UrlService} from 'services/contextual/url.service'; +import {StoryPlaythrough} from 'domain/story_viewer/story-playthrough.model'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {TopicViewerBackendApiService} from 'domain/topic_viewer/topic-viewer-backend-api.service'; +import { + ReadOnlyTopicBackendDict, + ReadOnlyTopicObjectFactory, +} from 'domain/topic_viewer/read-only-topic-object.factory'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; class MockUrlService { getTopicUrlFragmentFromLearnerUrl() { @@ -52,17 +55,14 @@ let urlService: UrlService; describe('Subtopic viewer navbar breadcrumb component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - StoryViewerNavbarBreadcrumbComponent, - MockTranslatePipe - ], + declarations: [StoryViewerNavbarBreadcrumbComponent, MockTranslatePipe], imports: [HttpClientTestingModule], providers: [ { provide: StoryViewerBackendApiService, useValue: { - fetchStoryDataAsync: async() => ( - new Promise((resolve) => { + fetchStoryDataAsync: async () => + new Promise(resolve => { resolve( StoryPlaythrough.createFromBackendDict({ story_id: 'id', @@ -70,13 +70,13 @@ describe('Subtopic viewer navbar breadcrumb component', () => { story_title: 'title', story_description: 'description', topic_name: 'topic_1', - meta_tag_content: 'this is a meta tag content' - })); - }) - ) - } + meta_tag_content: 'this is a meta tag content', + }) + ); + }), + }, }, - { provide: UrlService, useClass: MockUrlService }, + {provide: UrlService, useClass: MockUrlService}, UrlInterpolationService, ], }).compileComponents(); @@ -113,56 +113,60 @@ describe('Subtopic viewer navbar breadcrumb component', () => { component.ngOnDestroy(); }); - it('should set story title when component is initialized', - waitForAsync(() => { - component.ngOnInit(); - fixture.whenStable().then(() => { - fixture.detectChanges(); - expect(component.topicName).toBe('topic_1'); - expect(component.storyTitle).toBe('title'); - }); - }) - ); + it('should set story title when component is initialized', waitForAsync(() => { + component.ngOnInit(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(component.topicName).toBe('topic_1'); + expect(component.storyTitle).toBe('title'); + }); + })); it('should get topic url after component is initialized', () => { component.ngOnInit(); - expect(component.getTopicUrl()).toBe( - '/learn/classroom_1/topic_1/story'); + expect(component.getTopicUrl()).toBe('/learn/classroom_1/topic_1/story'); }); it('should throw error if story url fragment is not present', () => { - spyOn( - urlService, 'getStoryUrlFragmentFromLearnerUrl').and.returnValue(null); + spyOn(urlService, 'getStoryUrlFragmentFromLearnerUrl').and.returnValue( + null + ); expect(() => { component.ngOnInit(); }).toThrowError('Story url fragment is null'); }); - it('should set topic name and story title translation key and ' + - 'check whether hacky translations are displayed or not correctly', - waitForAsync(() => { - component.ngOnInit(); - fixture.whenStable().then(() => { - fixture.detectChanges(); - - expect(component.topicNameTranslationKey) - .toBe('I18N_TOPIC_topic1_TITLE'); - expect(component.storyTitleTranslationKey) - .toBe('I18N_STORY_id_TITLE'); - - spyOn(i18nLanguageCodeService, 'isHackyTranslationAvailable') - .and.returnValues(true, false); - spyOn(i18nLanguageCodeService, 'isCurrentLanguageEnglish') - .and.returnValues(false, false); - - let hackyTopicNameTranslationIsDisplayed = - component.isHackyTopicNameTranslationDisplayed(); - expect(hackyTopicNameTranslationIsDisplayed).toBe(true); + it( + 'should set topic name and story title translation key and ' + + 'check whether hacky translations are displayed or not correctly', + waitForAsync(() => { + component.ngOnInit(); + fixture.whenStable().then(() => { + fixture.detectChanges(); - let hackyStoryTitleTranslationIsDisplayed = - component.isHackyStoryTitleTranslationDisplayed(); - expect(hackyStoryTitleTranslationIsDisplayed).toBe(false); - }); - })); + expect(component.topicNameTranslationKey).toBe( + 'I18N_TOPIC_topic1_TITLE' + ); + expect(component.storyTitleTranslationKey).toBe('I18N_STORY_id_TITLE'); + + spyOn( + i18nLanguageCodeService, + 'isHackyTranslationAvailable' + ).and.returnValues(true, false); + spyOn( + i18nLanguageCodeService, + 'isCurrentLanguageEnglish' + ).and.returnValues(false, false); + + let hackyTopicNameTranslationIsDisplayed = + component.isHackyTopicNameTranslationDisplayed(); + expect(hackyTopicNameTranslationIsDisplayed).toBe(true); + + let hackyStoryTitleTranslationIsDisplayed = + component.isHackyStoryTitleTranslationDisplayed(); + expect(hackyStoryTitleTranslationIsDisplayed).toBe(false); + }); + }) + ); }); diff --git a/core/templates/pages/story-viewer-page/navbar-breadcrumb/story-viewer-navbar-breadcrumb.component.ts b/core/templates/pages/story-viewer-page/navbar-breadcrumb/story-viewer-navbar-breadcrumb.component.ts index c760878550db..9cf2d4cb1c69 100644 --- a/core/templates/pages/story-viewer-page/navbar-breadcrumb/story-viewer-navbar-breadcrumb.component.ts +++ b/core/templates/pages/story-viewer-page/navbar-breadcrumb/story-viewer-navbar-breadcrumb.component.ts @@ -16,21 +16,24 @@ * @fileoverview Component for the navbar breadcrumb of the story viewer. */ -import { ClassroomDomainConstants } from 'domain/classroom/classroom-domain.constants'; -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { StoryViewerBackendApiService } from 'domain/story_viewer/story-viewer-backend-api.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { Subscription } from 'rxjs'; -import { UrlService } from 'services/contextual/url.service'; -import { I18nLanguageCodeService, TranslationKeyType } from 'services/i18n-language-code.service'; -import { TopicViewerBackendApiService } from 'domain/topic_viewer/topic-viewer-backend-api.service'; -import { ReadOnlyTopic } from 'domain/topic_viewer/read-only-topic-object.factory'; +import {ClassroomDomainConstants} from 'domain/classroom/classroom-domain.constants'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {StoryViewerBackendApiService} from 'domain/story_viewer/story-viewer-backend-api.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {Subscription} from 'rxjs'; +import {UrlService} from 'services/contextual/url.service'; +import { + I18nLanguageCodeService, + TranslationKeyType, +} from 'services/i18n-language-code.service'; +import {TopicViewerBackendApiService} from 'domain/topic_viewer/topic-viewer-backend-api.service'; +import {ReadOnlyTopic} from 'domain/topic_viewer/read-only-topic-object.factory'; @Component({ selector: 'oppia-story-viewer-navbar-breadcrumb', templateUrl: './story-viewer-navbar-breadcrumb.component.html', - styleUrls: [] + styleUrls: [], }) export class StoryViewerNavbarBreadcrumbComponent implements OnInit, OnDestroy { // These properties are initialized using Angular lifecycle hooks @@ -54,51 +57,49 @@ export class StoryViewerNavbarBreadcrumbComponent implements OnInit, OnDestroy { directiveSubscriptions = new Subscription(); getTopicUrl(): string { return this.urlInterpolationService.interpolateUrl( - ClassroomDomainConstants.TOPIC_VIEWER_STORY_URL_TEMPLATE, { + ClassroomDomainConstants.TOPIC_VIEWER_STORY_URL_TEMPLATE, + { topic_url_fragment: this.topicUrlFragment, classroom_url_fragment: this.classroomUrlFragment, - story_url_fragment: this.storyUrlFragment - }); + story_url_fragment: this.storyUrlFragment, + } + ); } ngOnInit(): void { - this.topicUrlFragment = ( - this.urlService.getTopicUrlFragmentFromLearnerUrl()); + this.topicUrlFragment = this.urlService.getTopicUrlFragmentFromLearnerUrl(); this.classroomUrlFragment = - (this.urlService.getClassroomUrlFragmentFromLearnerUrl()); - let storyUrlFragment = ( - this.urlService.getStoryUrlFragmentFromLearnerUrl()); + this.urlService.getClassroomUrlFragmentFromLearnerUrl(); + let storyUrlFragment = this.urlService.getStoryUrlFragmentFromLearnerUrl(); if (storyUrlFragment === null) { throw new Error('Story url fragment is null'); } this.storyUrlFragment = storyUrlFragment; - this.storyViewerBackendApiService.fetchStoryDataAsync( - this.topicUrlFragment, - this.classroomUrlFragment, - this.storyUrlFragment).then( - (storyDataObject) => { + this.storyViewerBackendApiService + .fetchStoryDataAsync( + this.topicUrlFragment, + this.classroomUrlFragment, + this.storyUrlFragment + ) + .then(storyDataObject => { this.topicName = storyDataObject.topicName; - this.storyTitleTranslationKey = ( + this.storyTitleTranslationKey = this.i18nLanguageCodeService.getStoryTranslationKey( storyDataObject.id, TranslationKeyType.TITLE - ) - ); + ); this.storyTitle = storyDataObject.title; - } - ); + }); - this.topicViewerBackendApiService.fetchTopicDataAsync( - this.topicUrlFragment, this.classroomUrlFragment).then( - (readOnlyTopic: ReadOnlyTopic) => { - this.topicNameTranslationKey = ( + this.topicViewerBackendApiService + .fetchTopicDataAsync(this.topicUrlFragment, this.classroomUrlFragment) + .then((readOnlyTopic: ReadOnlyTopic) => { + this.topicNameTranslationKey = this.i18nLanguageCodeService.getTopicTranslationKey( readOnlyTopic.getTopicId(), TranslationKeyType.TITLE - ) - ); - } - ); + ); + }); } ngOnDestroy(): void { @@ -126,5 +127,9 @@ export class StoryViewerNavbarBreadcrumbComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive('oppiaStoryViewerNavbarBreadcrumb', - downgradeComponent({component: StoryViewerNavbarBreadcrumbComponent})); +angular + .module('oppia') + .directive( + 'oppiaStoryViewerNavbarBreadcrumb', + downgradeComponent({component: StoryViewerNavbarBreadcrumbComponent}) + ); diff --git a/core/templates/pages/story-viewer-page/navbar-pre-logo-action/story-viewer-navbar-pre-logo-action.component.spec.ts b/core/templates/pages/story-viewer-page/navbar-pre-logo-action/story-viewer-navbar-pre-logo-action.component.spec.ts index 2e1bd3bab4a6..5afa262e7f33 100644 --- a/core/templates/pages/story-viewer-page/navbar-pre-logo-action/story-viewer-navbar-pre-logo-action.component.spec.ts +++ b/core/templates/pages/story-viewer-page/navbar-pre-logo-action/story-viewer-navbar-pre-logo-action.component.spec.ts @@ -16,13 +16,13 @@ * @fileoverview Unit tests for the story viewer pre logo action */ -import { ComponentFixture, TestBed, fakeAsync } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { StoryViewerNavbarPreLogoActionComponent } from './story-viewer-navbar-pre-logo-action.component'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { StoryViewerBackendApiService } from 'domain/story_viewer/story-viewer-backend-api.service'; -import { UrlService } from 'services/contextual/url.service'; -import { StoryPlaythrough } from 'domain/story_viewer/story-playthrough.model'; +import {ComponentFixture, TestBed, fakeAsync} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {StoryViewerNavbarPreLogoActionComponent} from './story-viewer-navbar-pre-logo-action.component'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {StoryViewerBackendApiService} from 'domain/story_viewer/story-viewer-backend-api.service'; +import {UrlService} from 'services/contextual/url.service'; +import {StoryPlaythrough} from 'domain/story_viewer/story-playthrough.model'; class MockUrlService { getTopicUrlFragmentFromLearnerUrl() { @@ -51,8 +51,8 @@ describe('Subtopic viewer navbar breadcrumb component', () => { { provide: StoryViewerBackendApiService, useValue: { - fetchStoryDataAsync: async() => ( - new Promise((resolve) => { + fetchStoryDataAsync: async () => + new Promise(resolve => { resolve( StoryPlaythrough.createFromBackendDict({ story_id: 'id', @@ -60,13 +60,13 @@ describe('Subtopic viewer navbar breadcrumb component', () => { story_title: 'title', story_description: 'description', topic_name: 'topic_1', - meta_tag_content: 'this is a meta tag content' - })); - }) - ) - } + meta_tag_content: 'this is a meta tag content', + }) + ); + }), + }, }, - { provide: UrlService, useClass: MockUrlService }, + {provide: UrlService, useClass: MockUrlService}, UrlInterpolationService, ], }).compileComponents(); @@ -92,8 +92,9 @@ describe('Subtopic viewer navbar breadcrumb component', () => { })); it('should throw error if story url fragment is not present', () => { - spyOn( - urlService, 'getStoryUrlFragmentFromLearnerUrl').and.returnValue(null); + spyOn(urlService, 'getStoryUrlFragmentFromLearnerUrl').and.returnValue( + null + ); expect(() => { component.ngOnInit(); @@ -102,7 +103,6 @@ describe('Subtopic viewer navbar breadcrumb component', () => { it('should get topic url after component is initialized', () => { component.ngOnInit(); - expect(component.getTopicUrl()).toBe( - '/learn/classroom_1/topic_1/story'); + expect(component.getTopicUrl()).toBe('/learn/classroom_1/topic_1/story'); }); }); diff --git a/core/templates/pages/story-viewer-page/navbar-pre-logo-action/story-viewer-navbar-pre-logo-action.component.ts b/core/templates/pages/story-viewer-page/navbar-pre-logo-action/story-viewer-navbar-pre-logo-action.component.ts index dd310eb0d529..f692f0d1c3e7 100644 --- a/core/templates/pages/story-viewer-page/navbar-pre-logo-action/story-viewer-navbar-pre-logo-action.component.ts +++ b/core/templates/pages/story-viewer-page/navbar-pre-logo-action/story-viewer-navbar-pre-logo-action.component.ts @@ -17,20 +17,21 @@ * of the story viewer. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { StoryViewerBackendApiService } from 'domain/story_viewer/story-viewer-backend-api.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { Subscription } from 'rxjs'; -import { UrlService } from 'services/contextual/url.service'; -import { ClassroomDomainConstants } from 'domain/classroom/classroom-domain.constants'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {StoryViewerBackendApiService} from 'domain/story_viewer/story-viewer-backend-api.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {Subscription} from 'rxjs'; +import {UrlService} from 'services/contextual/url.service'; +import {ClassroomDomainConstants} from 'domain/classroom/classroom-domain.constants'; +import {downgradeComponent} from '@angular/upgrade/static'; @Component({ selector: 'oppia-story-viewer-navbar-pre-logo-action', - templateUrl: './story-viewer-navbar-pre-logo-action.component.html' + templateUrl: './story-viewer-navbar-pre-logo-action.component.html', }) export class StoryViewerNavbarPreLogoActionComponent -implements OnInit, OnDestroy { + implements OnInit, OnDestroy +{ // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -47,28 +48,31 @@ implements OnInit, OnDestroy { directiveSubscriptions = new Subscription(); getTopicUrl(): string { return this.urlInterpolationService.interpolateUrl( - ClassroomDomainConstants.TOPIC_VIEWER_STORY_URL_TEMPLATE, { + ClassroomDomainConstants.TOPIC_VIEWER_STORY_URL_TEMPLATE, + { topic_url_fragment: this.topicUrlFragment, classroom_url_fragment: this.classroomUrlFragment, - story_url_fragment: this.storyUrlFragment - }); + story_url_fragment: this.storyUrlFragment, + } + ); } ngOnInit(): void { this.topicUrlFragment = this.urlService.getTopicUrlFragmentFromLearnerUrl(); this.classroomUrlFragment = - this.urlService.getClassroomUrlFragmentFromLearnerUrl(); - let storyUrlFragment = ( - this.urlService.getStoryUrlFragmentFromLearnerUrl()); + this.urlService.getClassroomUrlFragmentFromLearnerUrl(); + let storyUrlFragment = this.urlService.getStoryUrlFragmentFromLearnerUrl(); if (storyUrlFragment === null) { throw new Error('Story url fragment is null'); } this.storyUrlFragment = storyUrlFragment; - this.storyViewerBackendApiService.fetchStoryDataAsync( - this.topicUrlFragment, - this.classroomUrlFragment, - this.storyUrlFragment).then( - (storyDataObject) => { + this.storyViewerBackendApiService + .fetchStoryDataAsync( + this.topicUrlFragment, + this.classroomUrlFragment, + this.storyUrlFragment + ) + .then(storyDataObject => { this.topicName = storyDataObject.topicName; }); } @@ -77,5 +81,9 @@ implements OnInit, OnDestroy { return this.directiveSubscriptions.unsubscribe(); } } -angular.module('oppia').directive('oppiaStoryViewerNavbarPreLogoAction', - downgradeComponent({component: StoryViewerNavbarPreLogoActionComponent})); +angular + .module('oppia') + .directive( + 'oppiaStoryViewerNavbarPreLogoAction', + downgradeComponent({component: StoryViewerNavbarPreLogoActionComponent}) + ); diff --git a/core/templates/pages/story-viewer-page/story-viewer-page-root.component.spec.ts b/core/templates/pages/story-viewer-page/story-viewer-page-root.component.spec.ts index b1129db48802..f6795cb63217 100644 --- a/core/templates/pages/story-viewer-page/story-viewer-page-root.component.spec.ts +++ b/core/templates/pages/story-viewer-page/story-viewer-page-root.component.spec.ts @@ -16,11 +16,11 @@ * @fileoverview Unit tests for storyViewerPageRoot. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { StoryViewerPageRootComponent } from './story-viewer-page-root.component'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {StoryViewerPageRootComponent} from './story-viewer-page-root.component'; describe('Story Viewer Page Root component', () => { let i18nLanguageCodeService: I18nLanguageCodeService; @@ -30,7 +30,7 @@ describe('Story Viewer Page Root component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [StoryViewerPageRootComponent], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(StoryViewerPageRootComponent); @@ -40,7 +40,8 @@ describe('Story Viewer Page Root component', () => { beforeEach(() => { i18nLanguageCodeService = TestBed.inject(I18nLanguageCodeService); spyOn(i18nLanguageCodeService, 'isCurrentLanguageRTL').and.returnValue( - true); + true + ); }); it('should be defined', () => { diff --git a/core/templates/pages/story-viewer-page/story-viewer-page-root.component.ts b/core/templates/pages/story-viewer-page/story-viewer-page-root.component.ts index d02e1c359fa9..fc1d7a01eadd 100644 --- a/core/templates/pages/story-viewer-page/story-viewer-page-root.component.ts +++ b/core/templates/pages/story-viewer-page/story-viewer-page-root.component.ts @@ -16,15 +16,14 @@ * @fileoverview Root component for Story Viewer Page. */ -import { Component, ViewEncapsulation } from '@angular/core'; +import {Component, ViewEncapsulation} from '@angular/core'; import './story-viewer-page-root.component.css'; - @Component({ selector: 'oppia-story-viewer-page-root', templateUrl: './story-viewer-page-root.component.html', styleUrls: ['./story-viewer-page-root.component.css'], - encapsulation: ViewEncapsulation.None + encapsulation: ViewEncapsulation.None, }) export class StoryViewerPageRootComponent {} diff --git a/core/templates/pages/story-viewer-page/story-viewer-page-routing.module.ts b/core/templates/pages/story-viewer-page/story-viewer-page-routing.module.ts index 4366ccbfe69b..7b942fad3261 100644 --- a/core/templates/pages/story-viewer-page/story-viewer-page-routing.module.ts +++ b/core/templates/pages/story-viewer-page/story-viewer-page-routing.module.ts @@ -16,24 +16,19 @@ * @fileoverview Routing module for story viewer page. */ -import { NgModule } from '@angular/core'; -import { Route, RouterModule } from '@angular/router'; -import { StoryViewerPageRootComponent } from './story-viewer-page-root.component'; +import {NgModule} from '@angular/core'; +import {Route, RouterModule} from '@angular/router'; +import {StoryViewerPageRootComponent} from './story-viewer-page-root.component'; const routes: Route[] = [ { path: '', - component: StoryViewerPageRootComponent - } + component: StoryViewerPageRootComponent, + }, ]; @NgModule({ - imports: [ - RouterModule.forChild(routes) - ], - exports: [ - RouterModule - ] + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], }) - export class StoryViewerPageRoutingModule {} diff --git a/core/templates/pages/story-viewer-page/story-viewer-page.component.spec.ts b/core/templates/pages/story-viewer-page/story-viewer-page.component.spec.ts index 93304559cc5a..389ab5c22f13 100644 --- a/core/templates/pages/story-viewer-page/story-viewer-page.component.spec.ts +++ b/core/templates/pages/story-viewer-page/story-viewer-page.component.spec.ts @@ -16,25 +16,28 @@ * @fileoverview Unit tests for storyViewerPage. */ -import { TestBed, fakeAsync, flushMicrotasks, tick } from '@angular/core/testing'; -import { ElementRef, NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { TranslateService } from '@ngx-translate/core'; - -import { StoryNode } from 'domain/story/story-node.model'; -import { StoryPlaythrough } from 'domain/story_viewer/story-playthrough.model'; -import { StoryViewerPageComponent } from './story-viewer-page.component'; -import { UserService } from 'services/user.service'; -import { StoryViewerBackendApiService } from 'domain/story_viewer/story-viewer-backend-api.service'; -import { AlertsService } from 'services/alerts.service'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; -import { UrlService } from 'services/contextual/url.service'; -import { PageTitleService } from 'services/page-title.service'; -import { UserInfo } from 'domain/user/user-info.model'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { ReadOnlyStoryNode } from 'domain/story_viewer/read-only-story-node.model'; +import {TestBed, fakeAsync, flushMicrotasks, tick} from '@angular/core/testing'; +import {ElementRef, NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {TranslateService} from '@ngx-translate/core'; + +import {StoryNode} from 'domain/story/story-node.model'; +import {StoryPlaythrough} from 'domain/story_viewer/story-playthrough.model'; +import {StoryViewerPageComponent} from './story-viewer-page.component'; +import {UserService} from 'services/user.service'; +import {StoryViewerBackendApiService} from 'domain/story_viewer/story-viewer-backend-api.service'; +import {AlertsService} from 'services/alerts.service'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; +import {UrlService} from 'services/contextual/url.service'; +import {PageTitleService} from 'services/page-title.service'; +import {UserInfo} from 'domain/user/user-info.model'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {ReadOnlyStoryNode} from 'domain/story_viewer/read-only-story-node.model'; class MockAssetsBackendApiService { getThumbnailUrlForPreview() { @@ -72,7 +75,7 @@ describe('Story Viewer Page component', () => { preferred_site_language_code: null, username: 'tester', email: 'test@test.com', - user_is_logged_in: false + user_is_logged_in: false, }; beforeEach(fakeAsync(() => { @@ -82,14 +85,14 @@ describe('Story Viewer Page component', () => { providers: [ { provide: assetsBackendApiService, - useClass: MockAssetsBackendApiService + useClass: MockAssetsBackendApiService, }, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); httpTestingController = TestBed.get(HttpTestingController); pageTitleService = TestBed.get(PageTitleService); @@ -105,20 +108,22 @@ describe('Story Viewer Page component', () => { let fixture = TestBed.createComponent(StoryViewerPageComponent); component = fixture.componentInstance; spyOn(i18nLanguageCodeService, 'isCurrentLanguageRTL').and.returnValue( - true); + true + ); spyOnProperty(windowRef, 'nativeWindow').and.returnValue({ location: { - reload: ()=>{}, - href: '/home' - } + reload: () => {}, + href: '/home', + }, } as Window); })); beforeEach(() => { - spyOn(assetsBackendApiService, 'getThumbnailUrlForPreview').and - .returnValue('thumbnail-url'); - spyOn(userService, 'getUserInfoAsync').and.returnValue(Promise.resolve( - UserInfo.createFromBackendDict(UserInfoObject)) + spyOn(assetsBackendApiService, 'getThumbnailUrlForPreview').and.returnValue( + 'thumbnail-url' + ); + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve(UserInfo.createFromBackendDict(UserInfoObject)) ); }); @@ -151,15 +156,15 @@ describe('Story Viewer Page component', () => { 2: 0, 3: 0, 4: 0, - 5: 0 + 5: 0, }, tags: [], activity_type: 'exploration', - category: 'Algebra' + category: 'Algebra', }, completed: true, thumbnail_bg_color: '#927117', - thumbnail_filename: 'filename' + thumbnail_filename: 'filename', }; var secondSampleReadOnlyStoryNodeBackendDict = { id: 'node_2', @@ -189,11 +194,11 @@ describe('Story Viewer Page component', () => { 2: 0, 3: 0, 4: 0, - 5: 0 + 5: 0, }, tags: [], activity_type: 'exploration', - category: 'Algebra' + category: 'Algebra', }, completed: false, thumbnail_bg_color: '#927117', @@ -203,87 +208,101 @@ describe('Story Viewer Page component', () => { story_id: 'qwerty', story_nodes: [ firstSampleReadOnlyStoryNodeBackendDict, - secondSampleReadOnlyStoryNodeBackendDict], + secondSampleReadOnlyStoryNodeBackendDict, + ], story_title: 'Story', story_description: 'Description', topic_name: 'Topic 1', - meta_tag_content: 'Story meta tag content' + meta_tag_content: 'Story meta tag content', }; - _samplePlaythroughObject = - StoryPlaythrough.createFromBackendDict( - storyPlaythroughBackendObject); + _samplePlaythroughObject = StoryPlaythrough.createFromBackendDict( + storyPlaythroughBackendObject + ); }); afterEach(() => { httpTestingController.verify(); }); - it('should get complete exploration url when clicking on svg element', - () => { - spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue( - 'topic'); - spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl') - .and.returnValue('math'); - spyOn( - urlService, 'getStoryUrlFragmentFromLearnerUrl').and.returnValue( - 'story'); - let node = StoryNode.createFromIdAndTitle( - '1', 'Story node title'); - expect(component.getExplorationUrl(node)).toBe( - '/explore/null?topic_url_fragment=topic&' + + it('should get complete exploration url when clicking on svg element', () => { + spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue( + 'topic' + ); + spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl').and.returnValue( + 'math' + ); + spyOn(urlService, 'getStoryUrlFragmentFromLearnerUrl').and.returnValue( + 'story' + ); + let node = StoryNode.createFromIdAndTitle('1', 'Story node title'); + expect(component.getExplorationUrl(node)).toBe( + '/explore/null?topic_url_fragment=topic&' + 'classroom_url_fragment=math&story_url_fragment=story&' + - 'node_id=1'); - }); - - it('should get complete image path corresponding to a given' + - ' relative path', () => { - let imagePath = '/path/to/image.png'; - expect(component.getStaticImageUrl(imagePath)).toBe( - '/assets/images/path/to/image.png'); + 'node_id=1' + ); }); - it('should not show story\'s chapters when story has no chapters', + it( + 'should get complete image path corresponding to a given' + + ' relative path', () => { - spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue( - 'topic'); - spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl') - .and.returnValue('math'); - spyOn( - urlService, 'getStoryUrlFragmentFromLearnerUrl').and.returnValue( - 'story'); - spyOn( - storyViewerBackendApiService, 'fetchStoryDataAsync').and.returnValue( - Promise.resolve(StoryPlaythrough.createFromBackendDict({ + let imagePath = '/path/to/image.png'; + expect(component.getStaticImageUrl(imagePath)).toBe( + '/assets/images/path/to/image.png' + ); + } + ); + + it("should not show story's chapters when story has no chapters", () => { + spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue( + 'topic' + ); + spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl').and.returnValue( + 'math' + ); + spyOn(urlService, 'getStoryUrlFragmentFromLearnerUrl').and.returnValue( + 'story' + ); + spyOn(storyViewerBackendApiService, 'fetchStoryDataAsync').and.returnValue( + Promise.resolve( + StoryPlaythrough.createFromBackendDict({ story_nodes: [], story_title: 'Story Title 1', story_description: 'Story Description 1', topic_name: 'topic_1', meta_tag_content: 'this is a meta tag content', - story_id: 'id' - }))); + story_id: 'id', + }) + ) + ); - component.ngOnInit(); + component.ngOnInit(); - expect(component.showChapters()).toBeFalse(); - }); + expect(component.showChapters()).toBeFalse(); + }); it('should throw error if story url fragment is not present', () => { spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue( - 'topic'); - spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl') - .and.returnValue('math'); - spyOn( - urlService, 'getStoryUrlFragmentFromLearnerUrl').and.returnValue(null); - spyOn( - storyViewerBackendApiService, 'fetchStoryDataAsync').and.returnValue( - Promise.resolve(StoryPlaythrough.createFromBackendDict({ - story_nodes: [], - story_title: 'Story Title 1', - story_description: 'Story Description 1', - topic_name: 'topic_1', - meta_tag_content: 'this is a meta tag content', - story_id: 'id' - }))); + 'topic' + ); + spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl').and.returnValue( + 'math' + ); + spyOn(urlService, 'getStoryUrlFragmentFromLearnerUrl').and.returnValue( + null + ); + spyOn(storyViewerBackendApiService, 'fetchStoryDataAsync').and.returnValue( + Promise.resolve( + StoryPlaythrough.createFromBackendDict({ + story_nodes: [], + story_title: 'Story Title 1', + story_description: 'Story Description 1', + topic_name: 'topic_1', + meta_tag_content: 'this is a meta tag content', + story_id: 'id', + }) + ) + ); let node = StoryNode.createFromIdAndTitle('1', 'Story node title'); expect(() => { @@ -295,69 +314,71 @@ describe('Story Viewer Page component', () => { }).toThrowError('Story url fragment is null'); }); - it('should show story\'s chapters when story has chapters', - () => { - let sampleDataResults = { - story_id: 'qwerty', - story_title: 'Story title', - story_description: 'Story description', - story_nodes: [], - topic_name: 'Topic name', - meta_tag_content: 'Story meta tag content' - }; - let samplePlaythroughObject = - StoryPlaythrough.createFromBackendDict( - sampleDataResults); - component.storyPlaythroughObject = { - id: '1', - nodes: [], - title: 'title', - description: 'description', - topicName: 'topic_name', - metaTagContent: 'this is meta tag content', - getInitialNode(): ReadOnlyStoryNode { - return samplePlaythroughObject.getInitialNode(); - }, - getStoryNodeCount(): number { - return 2; - }, - getStoryNodes(): ReadOnlyStoryNode[] { - return []; - }, - hasFinishedStory(): boolean { - return false; - }, - getNextPendingNodeId(): string { - return 'id'; - }, - hasStartedStory(): boolean { - return false; - }, - getStoryId(): string { - return this.id; - }, - getMetaTagContent(): string { - return this.metaTagContent; - } - }; - spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue( - 'topic'); - spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl') - .and.returnValue('math'); - spyOn( - urlService, 'getStoryUrlFragmentFromLearnerUrl').and.returnValue( - 'story'); - spyOn( - storyViewerBackendApiService, 'fetchStoryDataAsync').and.returnValue( - Promise.resolve(_samplePlaythroughObject)); - - expect( - _samplePlaythroughObject.getStoryNodes()[0].getId()).toEqual('node_1'); - expect( - _samplePlaythroughObject.getStoryNodes()[1].getId()).toEqual('node_2'); - - expect(component.showChapters()).toBeTrue(); - }); + it("should show story's chapters when story has chapters", () => { + let sampleDataResults = { + story_id: 'qwerty', + story_title: 'Story title', + story_description: 'Story description', + story_nodes: [], + topic_name: 'Topic name', + meta_tag_content: 'Story meta tag content', + }; + let samplePlaythroughObject = + StoryPlaythrough.createFromBackendDict(sampleDataResults); + component.storyPlaythroughObject = { + id: '1', + nodes: [], + title: 'title', + description: 'description', + topicName: 'topic_name', + metaTagContent: 'this is meta tag content', + getInitialNode(): ReadOnlyStoryNode { + return samplePlaythroughObject.getInitialNode(); + }, + getStoryNodeCount(): number { + return 2; + }, + getStoryNodes(): ReadOnlyStoryNode[] { + return []; + }, + hasFinishedStory(): boolean { + return false; + }, + getNextPendingNodeId(): string { + return 'id'; + }, + hasStartedStory(): boolean { + return false; + }, + getStoryId(): string { + return this.id; + }, + getMetaTagContent(): string { + return this.metaTagContent; + }, + }; + spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue( + 'topic' + ); + spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl').and.returnValue( + 'math' + ); + spyOn(urlService, 'getStoryUrlFragmentFromLearnerUrl').and.returnValue( + 'story' + ); + spyOn(storyViewerBackendApiService, 'fetchStoryDataAsync').and.returnValue( + Promise.resolve(_samplePlaythroughObject) + ); + + expect(_samplePlaythroughObject.getStoryNodes()[0].getId()).toEqual( + 'node_1' + ); + expect(_samplePlaythroughObject.getStoryNodes()[1].getId()).toEqual( + 'node_2' + ); + + expect(component.showChapters()).toBeTrue(); + }); it('should sign in correctly', fakeAsync(() => { spyOn(userService, 'getLoginUrlAsync').and.resolveTo('/home'); @@ -366,77 +387,88 @@ describe('Story Viewer Page component', () => { expect(windowRef.nativeWindow.location.href).toBe('/home'); })); - it('should refresh page if login url is not provided when login button is' + - ' clicked', fakeAsync(() => { - const reloadSpy = spyOn(windowRef.nativeWindow.location, 'reload'); - spyOn(userService, 'getLoginUrlAsync') - .and.resolveTo(undefined); - component.signIn(); - flushMicrotasks(); - - expect(reloadSpy).toHaveBeenCalled(); - })); - - it('should show warnings when fetching story data fails', + it( + 'should refresh page if login url is not provided when login button is' + + ' clicked', fakeAsync(() => { - spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl') - .and.returnValue('math'); - spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue( - 'topic'); - spyOn( - urlService, 'getStoryUrlFragmentFromLearnerUrl').and.returnValue( - 'story'); - spyOn( - storyViewerBackendApiService, 'fetchStoryDataAsync').and.returnValue( - Promise.reject( - { - status: 404 - })); - spyOn(alertsService, 'addWarning').and.callThrough(); - component.ngOnInit(); + const reloadSpy = spyOn(windowRef.nativeWindow.location, 'reload'); + spyOn(userService, 'getLoginUrlAsync').and.resolveTo(undefined); + component.signIn(); flushMicrotasks(); - expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Failed to get dashboard data'); - })); - it('should get path icon parameters after story data is loaded', - fakeAsync(() => { - spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue( - 'topic'); - spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl') - .and.returnValue('math'); - spyOn( - urlService, 'getStoryUrlFragmentFromLearnerUrl').and.returnValue( - 'story'); - spyOn( - storyViewerBackendApiService, 'fetchStoryDataAsync').and.returnValue( - Promise.resolve(_samplePlaythroughObject)); - spyOn(component, 'subscribeToOnLangChange'); - component.ngOnInit(); + expect(reloadSpy).toHaveBeenCalled(); + }) + ); - flushMicrotasks(); + it('should show warnings when fetching story data fails', fakeAsync(() => { + spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl').and.returnValue( + 'math' + ); + spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue( + 'topic' + ); + spyOn(urlService, 'getStoryUrlFragmentFromLearnerUrl').and.returnValue( + 'story' + ); + spyOn(storyViewerBackendApiService, 'fetchStoryDataAsync').and.returnValue( + Promise.reject({ + status: 404, + }) + ); + spyOn(alertsService, 'addWarning').and.callThrough(); + component.ngOnInit(); + flushMicrotasks(); + expect(alertsService.addWarning).toHaveBeenCalledWith( + 'Failed to get dashboard data' + ); + })); - expect(component.subscribeToOnLangChange).toHaveBeenCalled(); - expect(component.pathIconParameters).toEqual([{ + it('should get path icon parameters after story data is loaded', fakeAsync(() => { + spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue( + 'topic' + ); + spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl').and.returnValue( + 'math' + ); + spyOn(urlService, 'getStoryUrlFragmentFromLearnerUrl').and.returnValue( + 'story' + ); + spyOn(storyViewerBackendApiService, 'fetchStoryDataAsync').and.returnValue( + Promise.resolve(_samplePlaythroughObject) + ); + spyOn(component, 'subscribeToOnLangChange'); + component.ngOnInit(); + + flushMicrotasks(); + + expect(component.subscribeToOnLangChange).toHaveBeenCalled(); + expect(component.pathIconParameters).toEqual([ + { thumbnailIconUrl: 'thumbnail-url', left: '225px', top: '35px', - thumbnailBgColor: '#927117' - }, { + thumbnailBgColor: '#927117', + }, + { thumbnailIconUrl: 'thumbnail-url', left: '225px', top: '35px', - thumbnailBgColor: '#927117' }]); - })); + thumbnailBgColor: '#927117', + }, + ]); + })); - it('should obtain translated title and set it whenever the ' + - 'selected language changes', () => { - component.subscribeToOnLangChange(); - spyOn(component, 'setPageTitle'); - translateService.onLangChange.emit(); + it( + 'should obtain translated title and set it whenever the ' + + 'selected language changes', + () => { + component.subscribeToOnLangChange(); + spyOn(component, 'setPageTitle'); + translateService.onLangChange.emit(); - expect(component.setPageTitle).toHaveBeenCalled(); - }); + expect(component.setPageTitle).toHaveBeenCalled(); + } + ); it('should set page title', () => { spyOn(translateService, 'instant').and.callThrough(); @@ -446,12 +478,15 @@ describe('Story Viewer Page component', () => { component.setPageTitle(); expect(translateService.instant).toHaveBeenCalledWith( - 'I18N_STORY_VIEWER_PAGE_TITLE', { + 'I18N_STORY_VIEWER_PAGE_TITLE', + { topicName: 'dummy_topic_name', - storyTitle: 'dummy_story_title' - }); + storyTitle: 'dummy_story_title', + } + ); expect(pageTitleService.setDocumentTitle).toHaveBeenCalledWith( - 'I18N_STORY_VIEWER_PAGE_TITLE'); + 'I18N_STORY_VIEWER_PAGE_TITLE' + ); }); it('should unsubscribe upon component destruction', () => { @@ -462,116 +497,117 @@ describe('Story Viewer Page component', () => { expect(component.directiveSubscriptions.closed).toBe(true); }); - it('should place empty values if Filename and BgColor are null', - fakeAsync(() => { - var firstSampleReadOnlyStoryNodeBackendDict = { - id: 'node_1', - description: 'description', - title: 'Title 1', - prerequisite_skill_ids: [], - acquired_skill_ids: [], - destination_node_ids: ['node_2'], - outline: 'Outline', - exploration_id: 'exp_id', - outline_is_finalized: false, - exp_summary_dict: { - title: 'Title', - status: 'private', - last_updated_msec: 1591296737470.528, - community_owned: false, - objective: 'Test Objective', - id: '44LKoKLlIbGe', - num_views: 0, - thumbnail_icon_url: '/subjects/Algebra.svg', - human_readable_contributors_summary: {}, - language_code: 'en', - thumbnail_bg_color: '#cc4b00', - created_on_msec: 1591296635736.666, - ratings: { - 1: 0, - 2: 0, - 3: 0, - 4: 0, - 5: 0 - }, - tags: [], - activity_type: 'exploration', - category: 'Algebra' + it('should place empty values if Filename and BgColor are null', fakeAsync(() => { + var firstSampleReadOnlyStoryNodeBackendDict = { + id: 'node_1', + description: 'description', + title: 'Title 1', + prerequisite_skill_ids: [], + acquired_skill_ids: [], + destination_node_ids: ['node_2'], + outline: 'Outline', + exploration_id: 'exp_id', + outline_is_finalized: false, + exp_summary_dict: { + title: 'Title', + status: 'private', + last_updated_msec: 1591296737470.528, + community_owned: false, + objective: 'Test Objective', + id: '44LKoKLlIbGe', + num_views: 0, + thumbnail_icon_url: '/subjects/Algebra.svg', + human_readable_contributors_summary: {}, + language_code: 'en', + thumbnail_bg_color: '#cc4b00', + created_on_msec: 1591296635736.666, + ratings: { + 1: 0, + 2: 0, + 3: 0, + 4: 0, + 5: 0, }, - completed: true, - thumbnail_bg_color: '#927117', - thumbnail_filename: '' - }; - var secondSampleReadOnlyStoryNodeBackendDict = { - id: 'node_2', - description: 'description', - title: 'Title 2', - prerequisite_skill_ids: [], - acquired_skill_ids: [], - destination_node_ids: ['node_3'], - outline: 'Outline', - exploration_id: 'exp_id', - outline_is_finalized: false, - exp_summary_dict: { - title: 'Title', - status: 'private', - last_updated_msec: 1591296737470.528, - community_owned: false, - objective: 'Test Objective', - id: '44LKoKLlIbGe', - num_views: 0, - thumbnail_icon_url: '/subjects/Algebra.svg', - human_readable_contributors_summary: {}, - language_code: 'en', - thumbnail_bg_color: '#cc4b00', - created_on_msec: 1591296635736.666, - ratings: { - 1: 0, - 2: 0, - 3: 0, - 4: 0, - 5: 0 - }, - tags: [], - activity_type: 'exploration', - category: 'Algebra' + tags: [], + activity_type: 'exploration', + category: 'Algebra', + }, + completed: true, + thumbnail_bg_color: '#927117', + thumbnail_filename: '', + }; + var secondSampleReadOnlyStoryNodeBackendDict = { + id: 'node_2', + description: 'description', + title: 'Title 2', + prerequisite_skill_ids: [], + acquired_skill_ids: [], + destination_node_ids: ['node_3'], + outline: 'Outline', + exploration_id: 'exp_id', + outline_is_finalized: false, + exp_summary_dict: { + title: 'Title', + status: 'private', + last_updated_msec: 1591296737470.528, + community_owned: false, + objective: 'Test Objective', + id: '44LKoKLlIbGe', + num_views: 0, + thumbnail_icon_url: '/subjects/Algebra.svg', + human_readable_contributors_summary: {}, + language_code: 'en', + thumbnail_bg_color: '#cc4b00', + created_on_msec: 1591296635736.666, + ratings: { + 1: 0, + 2: 0, + 3: 0, + 4: 0, + 5: 0, }, - completed: false, - thumbnail_bg_color: '#927117', - thumbnail_filename: '', - }; - - var storyPlaythroughBackendObject = { - story_id: 'qwerty', - story_nodes: [ - firstSampleReadOnlyStoryNodeBackendDict, - secondSampleReadOnlyStoryNodeBackendDict - ], - story_title: 'Story', - story_description: 'Description', - topic_name: 'Topic 1', - meta_tag_content: 'Story meta tag content' - }; - _samplePlaythroughObject = - StoryPlaythrough.createFromBackendDict( - storyPlaythroughBackendObject); - spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl') - .and.returnValue('math'); - spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue( - 'topic'); - spyOn( - urlService, 'getStoryUrlFragmentFromLearnerUrl').and.returnValue( - 'story'); - spyOn( - storyViewerBackendApiService, 'fetchStoryDataAsync').and.returnValue( - Promise.resolve(_samplePlaythroughObject)); - component.ngOnInit(); - flushMicrotasks(); - expect(component.thumbnailFilename === ''); - expect(component.iconUrl === ''); - })); + tags: [], + activity_type: 'exploration', + category: 'Algebra', + }, + completed: false, + thumbnail_bg_color: '#927117', + thumbnail_filename: '', + }; - it('should close the login overlay', fakeAsync(()=>{ + var storyPlaythroughBackendObject = { + story_id: 'qwerty', + story_nodes: [ + firstSampleReadOnlyStoryNodeBackendDict, + secondSampleReadOnlyStoryNodeBackendDict, + ], + story_title: 'Story', + story_description: 'Description', + topic_name: 'Topic 1', + meta_tag_content: 'Story meta tag content', + }; + _samplePlaythroughObject = StoryPlaythrough.createFromBackendDict( + storyPlaythroughBackendObject + ); + spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl').and.returnValue( + 'math' + ); + spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue( + 'topic' + ); + spyOn(urlService, 'getStoryUrlFragmentFromLearnerUrl').and.returnValue( + 'story' + ); + spyOn(storyViewerBackendApiService, 'fetchStoryDataAsync').and.returnValue( + Promise.resolve(_samplePlaythroughObject) + ); + component.ngOnInit(); + flushMicrotasks(); + expect(component.thumbnailFilename === ''); + expect(component.iconUrl === ''); + })); + + it('should close the login overlay', fakeAsync(() => { spyOn(component, 'hideLoginOverlay').and.callThrough(); expect(component.showLoginOverlay).toEqual(true); @@ -582,7 +618,7 @@ describe('Story Viewer Page component', () => { expect(component.showLoginOverlay).toEqual(false); })); - it('should set focus on skip button', fakeAsync(()=>{ + it('should set focus on skip button', fakeAsync(() => { let target = document.createElement('div'); target.classList.add('target'); @@ -611,10 +647,16 @@ describe('Story Viewer Page component', () => { expect(component.skipButton.nativeElement.focus).toHaveBeenCalled(); })); it('should check if hacky translation is displayed correctly', () => { - spyOn(i18nLanguageCodeService, 'isHackyTranslationAvailable') - .and.returnValues(false, true, false, true); - spyOn(i18nLanguageCodeService, 'isCurrentLanguageEnglish') - .and.returnValues(false, false, true, false); + spyOn( + i18nLanguageCodeService, + 'isHackyTranslationAvailable' + ).and.returnValues(false, true, false, true); + spyOn(i18nLanguageCodeService, 'isCurrentLanguageEnglish').and.returnValues( + false, + false, + true, + false + ); let hackyStoryTitleTranslationIsDisplayed = component.isHackyStoryTitleTranslationDisplayed(); diff --git a/core/templates/pages/story-viewer-page/story-viewer-page.component.ts b/core/templates/pages/story-viewer-page/story-viewer-page.component.ts index 4b8195faf5a8..079d0d97a99c 100644 --- a/core/templates/pages/story-viewer-page/story-viewer-page.component.ts +++ b/core/templates/pages/story-viewer-page/story-viewer-page.component.ts @@ -16,28 +16,36 @@ * @fileoverview Component for the main page of the story viewer. */ -import { Component, ElementRef, OnInit, ViewChild, OnDestroy } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; +import { + Component, + ElementRef, + OnInit, + ViewChild, + OnDestroy, +} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; -import { StoryViewerBackendApiService } from 'domain/story_viewer/story-viewer-backend-api.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { UrlService } from 'services/contextual/url.service'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; -import { UserService } from 'services/user.service'; -import { AppConstants } from 'app.constants'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { LoaderService } from 'services/loader.service'; -import { PageTitleService } from 'services/page-title.service'; -import { AlertsService } from 'services/alerts.service'; -import { StoryPlaythrough } from 'domain/story_viewer/story-playthrough.model'; -import { ReadOnlyStoryNode } from 'domain/story_viewer/read-only-story-node.model'; -import { I18nLanguageCodeService, TranslationKeyType } from 'services/i18n-language-code.service'; +import {StoryViewerBackendApiService} from 'domain/story_viewer/story-viewer-backend-api.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {UrlService} from 'services/contextual/url.service'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; +import {UserService} from 'services/user.service'; +import {AppConstants} from 'app.constants'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {LoaderService} from 'services/loader.service'; +import {PageTitleService} from 'services/page-title.service'; +import {AlertsService} from 'services/alerts.service'; +import {StoryPlaythrough} from 'domain/story_viewer/story-playthrough.model'; +import {ReadOnlyStoryNode} from 'domain/story_viewer/read-only-story-node.model'; +import { + I18nLanguageCodeService, + TranslationKeyType, +} from 'services/i18n-language-code.service'; import './story-viewer-page.component.css'; -import { StoryNode } from 'domain/story/story-node.model'; - +import {StoryNode} from 'domain/story/story-node.model'; interface IconParametersArray { thumbnailIconUrl: string; @@ -49,7 +57,7 @@ interface IconParametersArray { @Component({ selector: 'oppia-story-viewer-page', templateUrl: './story-viewer-page.component.html', - styleUrls: ['./story-viewer-page.component.css'] + styleUrls: ['./story-viewer-page.component.css'], }) export class StoryViewerPageComponent implements OnInit, OnDestroy { // These properties are initialized using Angular lifecycle hooks @@ -97,8 +105,10 @@ export class StoryViewerPageComponent implements OnInit, OnDestroy { return; } const target = eventTarget; - if (target.closest('.story-viewer-login-container') !== - this.overlay.nativeElement) { + if ( + target.closest('.story-viewer-login-container') !== + this.overlay.nativeElement + ) { this.skipButton.nativeElement.focus(); } } @@ -116,14 +126,14 @@ export class StoryViewerPageComponent implements OnInit, OnDestroy { generatePathIconParameters(): IconParametersArray[] { let iconParametersArray: IconParametersArray[] = []; - for ( - let i = 0; i < this.storyPlaythroughObject.getStoryNodeCount(); - i++) { + for (let i = 0; i < this.storyPlaythroughObject.getStoryNodeCount(); i++) { this.thumbnailFilename = this.storyNodes[i].getThumbnailFilename(); this.thumbnailBgColor = this.storyNodes[i].getThumbnailBgColor(); this.iconUrl = this.assetsBackendApiService.getThumbnailUrlForPreview( - AppConstants.ENTITY_TYPE.STORY, this.storyId, - this.thumbnailFilename); + AppConstants.ENTITY_TYPE.STORY, + this.storyId, + this.thumbnailFilename + ); iconParametersArray.push({ thumbnailIconUrl: this.iconUrl, left: '225px', @@ -139,30 +149,35 @@ export class StoryViewerPageComponent implements OnInit, OnDestroy { } signIn(): void { - this.userService.getLoginUrlAsync().then( - (loginUrl) => { - loginUrl ? this.windowRef.nativeWindow.location.href = loginUrl : ( - this.windowRef.nativeWindow.location.reload()); - }); + this.userService.getLoginUrlAsync().then(loginUrl => { + loginUrl + ? (this.windowRef.nativeWindow.location.href = loginUrl) + : this.windowRef.nativeWindow.location.reload(); + }); } getExplorationUrl(node: StoryNode): string { let result = '/explore/' + node.getExplorationId(); result = this.urlService.addField( - result, 'topic_url_fragment', - this.urlService.getTopicUrlFragmentFromLearnerUrl()); + result, + 'topic_url_fragment', + this.urlService.getTopicUrlFragmentFromLearnerUrl() + ); result = this.urlService.addField( - result, 'classroom_url_fragment', - this.urlService.getClassroomUrlFragmentFromLearnerUrl()); - let storyUrlFragment = ( - this.urlService.getStoryUrlFragmentFromLearnerUrl()); + result, + 'classroom_url_fragment', + this.urlService.getClassroomUrlFragmentFromLearnerUrl() + ); + let storyUrlFragment = this.urlService.getStoryUrlFragmentFromLearnerUrl(); if (storyUrlFragment === null) { throw new Error('Story url fragment is null'); } result = this.urlService.addField( - result, 'story_url_fragment', storyUrlFragment); - result = this.urlService.addField( - result, 'node_id', node.getId()); + result, + 'story_url_fragment', + storyUrlFragment + ); + result = this.urlService.addField(result, 'node_id', node.getId()); return result; } @@ -176,88 +191,94 @@ export class StoryViewerPageComponent implements OnInit, OnDestroy { setPageTitle(): void { let translatedTitle = this.translateService.instant( - 'I18N_STORY_VIEWER_PAGE_TITLE', { + 'I18N_STORY_VIEWER_PAGE_TITLE', + { topicName: this.topicName, - storyTitle: this.storyTitle - }); + storyTitle: this.storyTitle, + } + ); this.pageTitleService.setDocumentTitle(translatedTitle); } ngOnInit(): void { this.storyIsLoaded = false; this.isLoggedIn = false; - this.userService.getUserInfoAsync().then((userInfo) => { + this.userService.getUserInfoAsync().then(userInfo => { this.isLoggedIn = userInfo.isLoggedIn(); }); - this.topicUrlFragment = ( - this.urlService.getTopicUrlFragmentFromLearnerUrl()); - this.classroomUrlFragment = ( - this.urlService.getClassroomUrlFragmentFromLearnerUrl()); - let storyUrlFragment = ( - this.urlService.getStoryUrlFragmentFromLearnerUrl()); + this.topicUrlFragment = this.urlService.getTopicUrlFragmentFromLearnerUrl(); + this.classroomUrlFragment = + this.urlService.getClassroomUrlFragmentFromLearnerUrl(); + let storyUrlFragment = this.urlService.getStoryUrlFragmentFromLearnerUrl(); if (storyUrlFragment === null) { throw new Error('Story url fragment is null'); } this.storyUrlFragment = storyUrlFragment; this.loaderService.showLoadingScreen('Loading'); - this.storyViewerBackendApiService.fetchStoryDataAsync( - this.topicUrlFragment, - this.classroomUrlFragment, - this.storyUrlFragment).then( - (storyDataDict) => { - this.storyIsLoaded = true; - this.storyPlaythroughObject = storyDataDict; - this.storyNodes = this.storyPlaythroughObject.getStoryNodes(); - this.storyId = this.storyPlaythroughObject.getStoryId(); - this.topicName = this.storyPlaythroughObject.topicName; - this.pageTitleService.updateMetaTag( - storyDataDict.getMetaTagContent()); - this.storyTitle = storyDataDict.title; - - // The onLangChange event is initially fired before the story is - // loaded. Hence the first setpageTitle() call needs to made - // manually, and the onLangChange subscription is added after - // the story is loaded. - this.setPageTitle(); - this.subscribeToOnLangChange(); - this.storyTitleTranslationKey = ( - this.i18nLanguageCodeService - .getStoryTranslationKey( - this.storyId, TranslationKeyType.TITLE) - ); - this.storyDescription = storyDataDict.description; - this.storyDescTranslationKey = ( - this.i18nLanguageCodeService - .getStoryTranslationKey( - this.storyId, TranslationKeyType.DESCRIPTION) - ); - this.loaderService.hideLoadingScreen(); - this.pathIconParameters = this.generatePathIconParameters(); - for (let idx in this.storyNodes) { - let storyNode: ReadOnlyStoryNode = this.storyNodes[idx]; - let storyNodeTitleTranslationKey = ( - this.i18nLanguageCodeService. - getExplorationTranslationKey( - storyNode.getExplorationId(), TranslationKeyType.TITLE) - ); - let storyNodeDescTranslationKey = ( - this.i18nLanguageCodeService. - getExplorationTranslationKey( - storyNode.getExplorationId(), TranslationKeyType.DESCRIPTION) + this.storyViewerBackendApiService + .fetchStoryDataAsync( + this.topicUrlFragment, + this.classroomUrlFragment, + this.storyUrlFragment + ) + .then( + storyDataDict => { + this.storyIsLoaded = true; + this.storyPlaythroughObject = storyDataDict; + this.storyNodes = this.storyPlaythroughObject.getStoryNodes(); + this.storyId = this.storyPlaythroughObject.getStoryId(); + this.topicName = this.storyPlaythroughObject.topicName; + this.pageTitleService.updateMetaTag( + storyDataDict.getMetaTagContent() ); - this.storyNodesTitleTranslationKeys.push( - storyNodeTitleTranslationKey); - this.storyNodesDescTranslationKeys.push( - storyNodeDescTranslationKey); - } - }, - (errorResponse) => { - let errorCodes = AppConstants.FATAL_ERROR_CODES; - if (errorCodes.indexOf(errorResponse.status) !== -1) { - this.alertsService.addWarning('Failed to get dashboard data'); + this.storyTitle = storyDataDict.title; + + // The onLangChange event is initially fired before the story is + // loaded. Hence the first setpageTitle() call needs to made + // manually, and the onLangChange subscription is added after + // the story is loaded. + this.setPageTitle(); + this.subscribeToOnLangChange(); + this.storyTitleTranslationKey = + this.i18nLanguageCodeService.getStoryTranslationKey( + this.storyId, + TranslationKeyType.TITLE + ); + this.storyDescription = storyDataDict.description; + this.storyDescTranslationKey = + this.i18nLanguageCodeService.getStoryTranslationKey( + this.storyId, + TranslationKeyType.DESCRIPTION + ); + this.loaderService.hideLoadingScreen(); + this.pathIconParameters = this.generatePathIconParameters(); + for (let idx in this.storyNodes) { + let storyNode: ReadOnlyStoryNode = this.storyNodes[idx]; + let storyNodeTitleTranslationKey = + this.i18nLanguageCodeService.getExplorationTranslationKey( + storyNode.getExplorationId(), + TranslationKeyType.TITLE + ); + let storyNodeDescTranslationKey = + this.i18nLanguageCodeService.getExplorationTranslationKey( + storyNode.getExplorationId(), + TranslationKeyType.DESCRIPTION + ); + this.storyNodesTitleTranslationKeys.push( + storyNodeTitleTranslationKey + ); + this.storyNodesDescTranslationKeys.push( + storyNodeDescTranslationKey + ); + } + }, + errorResponse => { + let errorCodes = AppConstants.FATAL_ERROR_CODES; + if (errorCodes.indexOf(errorResponse.status) !== -1) { + this.alertsService.addWarning('Failed to get dashboard data'); + } } - } - ); + ); // The pathIconParameters is an array containing the co-ordinates, // background color and icon url for the icons generated on the @@ -302,5 +323,9 @@ export class StoryViewerPageComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive('oppiaStoryViewerPage', - downgradeComponent({component: StoryViewerPageComponent})); +angular + .module('oppia') + .directive( + 'oppiaStoryViewerPage', + downgradeComponent({component: StoryViewerPageComponent}) + ); diff --git a/core/templates/pages/story-viewer-page/story-viewer-page.module.ts b/core/templates/pages/story-viewer-page/story-viewer-page.module.ts index 599886bfc9a6..db7a18162c81 100644 --- a/core/templates/pages/story-viewer-page/story-viewer-page.module.ts +++ b/core/templates/pages/story-viewer-page/story-viewer-page.module.ts @@ -16,32 +16,28 @@ * @fileoverview Module for the story viewer page. */ -import { NgModule } from '@angular/core'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { StoryViewerNavbarBreadcrumbComponent } from './navbar-breadcrumb/story-viewer-navbar-breadcrumb.component'; -import { StoryViewerNavbarPreLogoActionComponent } from './navbar-pre-logo-action/story-viewer-navbar-pre-logo-action.component'; -import { StoryViewerPageComponent } from './story-viewer-page.component'; -import { StoryViewerPageRootComponent } from './story-viewer-page-root.component'; -import { CommonModule } from '@angular/common'; -import { StoryViewerPageRoutingModule } from './story-viewer-page-routing.module'; +import {NgModule} from '@angular/core'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {StoryViewerNavbarBreadcrumbComponent} from './navbar-breadcrumb/story-viewer-navbar-breadcrumb.component'; +import {StoryViewerNavbarPreLogoActionComponent} from './navbar-pre-logo-action/story-viewer-navbar-pre-logo-action.component'; +import {StoryViewerPageComponent} from './story-viewer-page.component'; +import {StoryViewerPageRootComponent} from './story-viewer-page-root.component'; +import {CommonModule} from '@angular/common'; +import {StoryViewerPageRoutingModule} from './story-viewer-page-routing.module'; @NgModule({ - imports: [ - CommonModule, - SharedComponentsModule, - StoryViewerPageRoutingModule - ], + imports: [CommonModule, SharedComponentsModule, StoryViewerPageRoutingModule], declarations: [ StoryViewerNavbarBreadcrumbComponent, StoryViewerNavbarPreLogoActionComponent, StoryViewerPageComponent, - StoryViewerPageRootComponent + StoryViewerPageRootComponent, ], entryComponents: [ StoryViewerNavbarBreadcrumbComponent, StoryViewerNavbarPreLogoActionComponent, StoryViewerPageComponent, - StoryViewerPageRootComponent - ] + StoryViewerPageRootComponent, + ], }) export class StoryViewerPageModule {} diff --git a/core/templates/pages/subtopic-viewer-page/navbar-breadcrumb/subtopic-viewer-navbar-breadcrumb.component.spec.ts b/core/templates/pages/subtopic-viewer-page/navbar-breadcrumb/subtopic-viewer-navbar-breadcrumb.component.spec.ts index 5a3aa96f7f52..89a4b9ec8b9e 100644 --- a/core/templates/pages/subtopic-viewer-page/navbar-breadcrumb/subtopic-viewer-navbar-breadcrumb.component.spec.ts +++ b/core/templates/pages/subtopic-viewer-page/navbar-breadcrumb/subtopic-viewer-navbar-breadcrumb.component.spec.ts @@ -16,19 +16,15 @@ * @fileoverview Unit tests for the subtopic viewer navbar breadcrumb. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { SubtopicViewerNavbarBreadcrumbComponent } from - './subtopic-viewer-navbar-breadcrumb.component'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; -import { SubtopicViewerBackendApiService } from - 'domain/subtopic_viewer/subtopic-viewer-backend-api.service'; -import { UrlService } from 'services/contextual/url.service'; -import { ReadOnlySubtopicPageData } from - 'domain/subtopic_viewer/read-only-subtopic-page-data.model'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {SubtopicViewerNavbarBreadcrumbComponent} from './subtopic-viewer-navbar-breadcrumb.component'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {SubtopicViewerBackendApiService} from 'domain/subtopic_viewer/subtopic-viewer-backend-api.service'; +import {UrlService} from 'services/contextual/url.service'; +import {ReadOnlySubtopicPageData} from 'domain/subtopic_viewer/read-only-subtopic-page-data.model'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; class MockUrlService { getTopicUrlFragmentFromLearnerUrl() { @@ -48,44 +44,42 @@ let component: SubtopicViewerNavbarBreadcrumbComponent; let fixture: ComponentFixture; let i18nLanguageCodeService: I18nLanguageCodeService; -describe('Subtopic viewer navbar breadcrumb component', function() { +describe('Subtopic viewer navbar breadcrumb component', function () { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ SubtopicViewerNavbarBreadcrumbComponent, - MockTranslatePipe - ], - imports: [ - HttpClientTestingModule + MockTranslatePipe, ], + imports: [HttpClientTestingModule], providers: [ { provide: SubtopicViewerBackendApiService, useValue: { - fetchSubtopicDataAsync: async() => ( - new Promise((resolve) => { + fetchSubtopicDataAsync: async () => + new Promise(resolve => { resolve( ReadOnlySubtopicPageData.createFromBackendDict({ subtopic_title: 'Subtopic Title', page_contents: { subtitled_html: { content_id: 'content_1', - html: 'This is a html' + html: 'This is a html', }, recorded_voiceovers: { - voiceovers_mapping: {} - } + voiceovers_mapping: {}, + }, }, next_subtopic_dict: null, prev_subtopic_dict: null, topic_id: 'topic_1', - topic_name: 'topic_1' - })); - }) - ) - } + topic_name: 'topic_1', + }) + ); + }), + }, }, - { provide: UrlService, useClass: MockUrlService }, + {provide: UrlService, useClass: MockUrlService}, UrlInterpolationService, ], }).compileComponents(); @@ -98,45 +92,51 @@ describe('Subtopic viewer navbar breadcrumb component', function() { fixture.detectChanges(); }); - it('should set subtopic title when component is initialized', - waitForAsync(() => { - component.ngOnInit(); - fixture.whenStable().then(() => { - fixture.detectChanges(); - expect(component.topicName).toBe('topic_1'); - expect(component.subtopicTitle).toBe('Subtopic Title'); - }); - }) - ); + it('should set subtopic title when component is initialized', waitForAsync(() => { + component.ngOnInit(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(component.topicName).toBe('topic_1'); + expect(component.subtopicTitle).toBe('Subtopic Title'); + }); + })); it('should get topic url after component is initialized', () => { component.ngOnInit(); expect(component.getTopicUrl()).toBe('/learn/classroom_1/topic_1/revision'); }); - it('should set topic name and subtopic title translation key and ' + - 'check whether hacky translations are displayed or not correctly', - waitForAsync(() => { - component.ngOnInit(); - fixture.whenStable().then(() => { - fixture.detectChanges(); - expect(component.topicNameTranslationKey) - .toBe('I18N_TOPIC_topic_1_TITLE'); - expect(component.subtopicTitleTranslationKey) - .toBe('I18N_SUBTOPIC_topic_1_subtopic_1_TITLE'); + it( + 'should set topic name and subtopic title translation key and ' + + 'check whether hacky translations are displayed or not correctly', + waitForAsync(() => { + component.ngOnInit(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(component.topicNameTranslationKey).toBe( + 'I18N_TOPIC_topic_1_TITLE' + ); + expect(component.subtopicTitleTranslationKey).toBe( + 'I18N_SUBTOPIC_topic_1_subtopic_1_TITLE' + ); - spyOn(i18nLanguageCodeService, 'isHackyTranslationAvailable') - .and.returnValues(true, false); - spyOn(i18nLanguageCodeService, 'isCurrentLanguageEnglish') - .and.returnValues(false, false); + spyOn( + i18nLanguageCodeService, + 'isHackyTranslationAvailable' + ).and.returnValues(true, false); + spyOn( + i18nLanguageCodeService, + 'isCurrentLanguageEnglish' + ).and.returnValues(false, false); - let hackyTopicNameTranslationIsDisplayed = - component.isHackyTopicNameTranslationDisplayed(); - expect(hackyTopicNameTranslationIsDisplayed).toBe(true); + let hackyTopicNameTranslationIsDisplayed = + component.isHackyTopicNameTranslationDisplayed(); + expect(hackyTopicNameTranslationIsDisplayed).toBe(true); - let hackySubtopicTitleTranslationIsDisplayed = - component.isHackySubtopicTitleTranslationDisplayed(); - expect(hackySubtopicTitleTranslationIsDisplayed).toBe(false); - }); - })); + let hackySubtopicTitleTranslationIsDisplayed = + component.isHackySubtopicTitleTranslationDisplayed(); + expect(hackySubtopicTitleTranslationIsDisplayed).toBe(false); + }); + }) + ); }); diff --git a/core/templates/pages/subtopic-viewer-page/navbar-breadcrumb/subtopic-viewer-navbar-breadcrumb.component.ts b/core/templates/pages/subtopic-viewer-page/navbar-breadcrumb/subtopic-viewer-navbar-breadcrumb.component.ts index c0c370974108..7b1152588ee6 100644 --- a/core/templates/pages/subtopic-viewer-page/navbar-breadcrumb/subtopic-viewer-navbar-breadcrumb.component.ts +++ b/core/templates/pages/subtopic-viewer-page/navbar-breadcrumb/subtopic-viewer-navbar-breadcrumb.component.ts @@ -16,23 +16,23 @@ * @fileoverview Component for the navbar breadcrumb of the subtopic viewer. */ -import { Component, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; -import { ClassroomDomainConstants } from 'domain/classroom/classroom-domain.constants'; -import { ReadOnlySubtopicPageData } from - 'domain/subtopic_viewer/read-only-subtopic-page-data.model'; -import { SubtopicViewerBackendApiService } from - 'domain/subtopic_viewer/subtopic-viewer-backend-api.service'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; -import { UrlService } from 'services/contextual/url.service'; -import { I18nLanguageCodeService, TranslationKeyType } from 'services/i18n-language-code.service'; +import {ClassroomDomainConstants} from 'domain/classroom/classroom-domain.constants'; +import {ReadOnlySubtopicPageData} from 'domain/subtopic_viewer/read-only-subtopic-page-data.model'; +import {SubtopicViewerBackendApiService} from 'domain/subtopic_viewer/subtopic-viewer-backend-api.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {UrlService} from 'services/contextual/url.service'; +import { + I18nLanguageCodeService, + TranslationKeyType, +} from 'services/i18n-language-code.service'; @Component({ selector: 'subtopic-viewer-navbar-breadcrumb', templateUrl: './subtopic-viewer-navbar-breadcrumb.component.html', - styleUrls: [] + styleUrls: [], }) export class SubtopicViewerNavbarBreadcrumbComponent implements OnInit { // These properties are initialized using Angular lifecycle hooks @@ -53,42 +53,42 @@ export class SubtopicViewerNavbarBreadcrumbComponent implements OnInit { ) {} ngOnInit(): void { - this.topicUrlFragment = ( - this.urlService.getTopicUrlFragmentFromLearnerUrl()); - this.classroomUrlFragment = ( - this.urlService.getClassroomUrlFragmentFromLearnerUrl()); - this.subtopicUrlFragment = ( - this.urlService.getSubtopicUrlFragmentFromLearnerUrl()); - this.subtopicViewerBackendApiService.fetchSubtopicDataAsync( - this.topicUrlFragment, - this.classroomUrlFragment, - this.subtopicUrlFragment).then( - (subtopicDataObject: ReadOnlySubtopicPageData) => { + this.topicUrlFragment = this.urlService.getTopicUrlFragmentFromLearnerUrl(); + this.classroomUrlFragment = + this.urlService.getClassroomUrlFragmentFromLearnerUrl(); + this.subtopicUrlFragment = + this.urlService.getSubtopicUrlFragmentFromLearnerUrl(); + this.subtopicViewerBackendApiService + .fetchSubtopicDataAsync( + this.topicUrlFragment, + this.classroomUrlFragment, + this.subtopicUrlFragment + ) + .then((subtopicDataObject: ReadOnlySubtopicPageData) => { this.subtopicTitle = subtopicDataObject.getSubtopicTitle(); - this.subtopicTitleTranslationKey = ( + this.subtopicTitleTranslationKey = this.i18nLanguageCodeService.getSubtopicTranslationKey( subtopicDataObject.getParentTopicId(), this.subtopicUrlFragment, TranslationKeyType.TITLE - ) - ); + ); this.topicName = subtopicDataObject.getParentTopicName(); - this.topicNameTranslationKey = ( + this.topicNameTranslationKey = this.i18nLanguageCodeService.getTopicTranslationKey( subtopicDataObject.getParentTopicId(), TranslationKeyType.TITLE - ) - ); - } - ); + ); + }); } getTopicUrl(): string { return this.urlInterpolationService.interpolateUrl( - ClassroomDomainConstants.TOPIC_VIEWER_REVISION_URL_TEMPLATE, { + ClassroomDomainConstants.TOPIC_VIEWER_REVISION_URL_TEMPLATE, + { topic_url_fragment: this.topicUrlFragment, - classroom_url_fragment: this.classroomUrlFragment - }); + classroom_url_fragment: this.classroomUrlFragment, + } + ); } isHackyTopicNameTranslationDisplayed(): boolean { @@ -108,6 +108,9 @@ export class SubtopicViewerNavbarBreadcrumbComponent implements OnInit { } } -angular.module('oppia').directive( - 'subtopicViewerNavbarBreadcrumb', downgradeComponent( - {component: SubtopicViewerNavbarBreadcrumbComponent})); +angular + .module('oppia') + .directive( + 'subtopicViewerNavbarBreadcrumb', + downgradeComponent({component: SubtopicViewerNavbarBreadcrumbComponent}) + ); diff --git a/core/templates/pages/subtopic-viewer-page/navbar-pre-logo-action/subtopic-viewer-navbar-pre-logo-action.component.spec.ts b/core/templates/pages/subtopic-viewer-page/navbar-pre-logo-action/subtopic-viewer-navbar-pre-logo-action.component.spec.ts index bc041d59b594..445432e20a5a 100644 --- a/core/templates/pages/subtopic-viewer-page/navbar-pre-logo-action/subtopic-viewer-navbar-pre-logo-action.component.spec.ts +++ b/core/templates/pages/subtopic-viewer-page/navbar-pre-logo-action/subtopic-viewer-navbar-pre-logo-action.component.spec.ts @@ -16,9 +16,9 @@ * @fileoverview Unit tests for the subtopic viewer pre logo action */ -import { ComponentFixture, TestBed, async } from '@angular/core/testing'; -import { SubtopicViewerNavbarPreLogoActionComponent } from './subtopic-viewer-navbar-pre-logo-action.component'; -import { UrlService } from 'services/contextual/url.service'; +import {ComponentFixture, TestBed, async} from '@angular/core/testing'; +import {SubtopicViewerNavbarPreLogoActionComponent} from './subtopic-viewer-navbar-pre-logo-action.component'; +import {UrlService} from 'services/contextual/url.service'; describe('subtopic viewer pre logo action component', () => { let component: SubtopicViewerNavbarPreLogoActionComponent; @@ -32,15 +32,18 @@ describe('subtopic viewer pre logo action component', () => { urlService = TestBed.get(UrlService); - spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl') - .and.returnValue('url-fragment'); - spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl') - .and.returnValue('math'); + spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue( + 'url-fragment' + ); + spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl').and.returnValue( + 'math' + ); })); beforeEach(() => { fixture = TestBed.createComponent( - SubtopicViewerNavbarPreLogoActionComponent); + SubtopicViewerNavbarPreLogoActionComponent + ); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/core/templates/pages/subtopic-viewer-page/navbar-pre-logo-action/subtopic-viewer-navbar-pre-logo-action.component.ts b/core/templates/pages/subtopic-viewer-page/navbar-pre-logo-action/subtopic-viewer-navbar-pre-logo-action.component.ts index 2d3ff5ef2d02..fcc604b460ed 100644 --- a/core/templates/pages/subtopic-viewer-page/navbar-pre-logo-action/subtopic-viewer-navbar-pre-logo-action.component.ts +++ b/core/templates/pages/subtopic-viewer-page/navbar-pre-logo-action/subtopic-viewer-navbar-pre-logo-action.component.ts @@ -17,17 +17,17 @@ * of the subtopic viewer. */ -import { Component, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; -import { ClassroomDomainConstants } from 'domain/classroom/classroom-domain.constants'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { UrlService } from 'services/contextual/url.service'; +import {ClassroomDomainConstants} from 'domain/classroom/classroom-domain.constants'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {UrlService} from 'services/contextual/url.service'; @Component({ selector: 'subtopic-viewer-navbar-pre-logo-action', templateUrl: './subtopic-viewer-navbar-pre-logo-action.component.html', - styleUrls: [] + styleUrls: [], }) export class SubtopicViewerNavbarPreLogoActionComponent implements OnInit { // These properties are initialized using Angular lifecycle hooks @@ -41,17 +41,21 @@ export class SubtopicViewerNavbarPreLogoActionComponent implements OnInit { ) {} ngOnInit(): void { - this.topicUrlFragment = ( - this.urlService.getTopicUrlFragmentFromLearnerUrl()); + this.topicUrlFragment = this.urlService.getTopicUrlFragmentFromLearnerUrl(); this.topicUrl = this.urlInterpolationService.interpolateUrl( - ClassroomDomainConstants.TOPIC_VIEWER_REVISION_URL_TEMPLATE, { + ClassroomDomainConstants.TOPIC_VIEWER_REVISION_URL_TEMPLATE, + { topic_url_fragment: this.topicUrlFragment, - classroom_url_fragment: ( - this.urlService.getClassroomUrlFragmentFromLearnerUrl()) - }); + classroom_url_fragment: + this.urlService.getClassroomUrlFragmentFromLearnerUrl(), + } + ); } } -angular.module('oppia').component( - 'subtopicViewerNavbarPreLogoActionComponent', downgradeComponent( - { component: SubtopicViewerNavbarPreLogoActionComponent })); +angular + .module('oppia') + .component( + 'subtopicViewerNavbarPreLogoActionComponent', + downgradeComponent({component: SubtopicViewerNavbarPreLogoActionComponent}) + ); diff --git a/core/templates/pages/subtopic-viewer-page/subtopic-viewer-page.component.spec.ts b/core/templates/pages/subtopic-viewer-page/subtopic-viewer-page.component.spec.ts index 46bb83a99235..1ac9ce69d145 100644 --- a/core/templates/pages/subtopic-viewer-page/subtopic-viewer-page.component.spec.ts +++ b/core/templates/pages/subtopic-viewer-page/subtopic-viewer-page.component.spec.ts @@ -16,24 +16,30 @@ * @fileoverview Unit tests for subtopic viewer page component. */ -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TranslateService } from '@ngx-translate/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TranslateService} from '@ngx-translate/core'; -import { PageTitleService } from 'services/page-title.service'; -import { ReadOnlySubtopicPageData } from 'domain/subtopic_viewer/read-only-subtopic-page-data.model'; -import { SubtopicViewerPageComponent } from './subtopic-viewer-page.component'; -import { AlertsService } from 'services/alerts.service'; -import { ContextService } from 'services/context.service'; -import { LoaderService } from 'services/loader.service'; -import { SubtopicViewerBackendApiService } from 'domain/subtopic_viewer/subtopic-viewer-backend-api.service'; -import { TopicViewerBackendApiService } from 'domain/topic_viewer/topic-viewer-backend-api.service'; -import { UrlService } from 'services/contextual/url.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { ReadOnlyTopic } from 'domain/topic_viewer/read-only-topic-object.factory'; +import {PageTitleService} from 'services/page-title.service'; +import {ReadOnlySubtopicPageData} from 'domain/subtopic_viewer/read-only-subtopic-page-data.model'; +import {SubtopicViewerPageComponent} from './subtopic-viewer-page.component'; +import {AlertsService} from 'services/alerts.service'; +import {ContextService} from 'services/context.service'; +import {LoaderService} from 'services/loader.service'; +import {SubtopicViewerBackendApiService} from 'domain/subtopic_viewer/subtopic-viewer-backend-api.service'; +import {TopicViewerBackendApiService} from 'domain/topic_viewer/topic-viewer-backend-api.service'; +import {UrlService} from 'services/contextual/url.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {ReadOnlyTopic} from 'domain/topic_viewer/read-only-topic-object.factory'; class MockTranslateService { onLangChange: EventEmitter = new EventEmitter(); @@ -42,7 +48,7 @@ class MockTranslateService { } } -describe('Subtopic viewer page', function() { +describe('Subtopic viewer page', function () { let component: SubtopicViewerPageComponent; let fixture: ComponentFixture; let pageTitleService: PageTitleService; @@ -75,7 +81,7 @@ describe('Subtopic viewer page', function() { let subtopicTitle = 'Subtopic Title'; let subtopicUrlFragment = 'subtopic-title'; - let subtopicDataObject: ReadOnlySubtopicPageData = ( + let subtopicDataObject: ReadOnlySubtopicPageData = ReadOnlySubtopicPageData.createFromBackendDict({ topic_id: topicId, topic_name: topicName, @@ -83,11 +89,11 @@ describe('Subtopic viewer page', function() { page_contents: { subtitled_html: { content_id: '', - html: 'This is a html' + html: 'This is a html', }, recorded_voiceovers: { - voiceovers_mapping: {} - } + voiceovers_mapping: {}, + }, }, next_subtopic_dict: { id: 2, @@ -95,12 +101,12 @@ describe('Subtopic viewer page', function() { skill_ids: [], thumbnail_filename: '', thumbnail_bg_color: '', - url_fragment: subtopicUrlFragment + url_fragment: subtopicUrlFragment, }, - prev_subtopic_dict: null - })); + prev_subtopic_dict: null, + }); - let subtopicDataObjectWithPrevSubtopic: ReadOnlySubtopicPageData = ( + let subtopicDataObjectWithPrevSubtopic: ReadOnlySubtopicPageData = ReadOnlySubtopicPageData.createFromBackendDict({ topic_id: topicId, topic_name: topicName, @@ -108,11 +114,11 @@ describe('Subtopic viewer page', function() { page_contents: { subtitled_html: { content_id: '', - html: 'This is a html' + html: 'This is a html', }, recorded_voiceovers: { - voiceovers_mapping: {} - } + voiceovers_mapping: {}, + }, }, next_subtopic_dict: null, prev_subtopic_dict: { @@ -121,19 +127,14 @@ describe('Subtopic viewer page', function() { skill_ids: [], thumbnail_filename: '', thumbnail_bg_color: '', - url_fragment: subtopicUrlFragment - } - })); + url_fragment: subtopicUrlFragment, + }, + }); beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - SubtopicViewerPageComponent, - MockTranslatePipe - ], + imports: [HttpClientTestingModule], + declarations: [SubtopicViewerPageComponent, MockTranslatePipe], providers: [ AlertsService, ContextService, @@ -144,10 +145,10 @@ describe('Subtopic viewer page', function() { WindowDimensionsService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -160,85 +161,114 @@ describe('Subtopic viewer page', function() { i18nLanguageCodeService = TestBed.inject(I18nLanguageCodeService); alertsService = TestBed.inject(AlertsService); subtopicViewerBackendApiService = TestBed.inject( - SubtopicViewerBackendApiService); - topicViewerBackendApiService = TestBed.inject( - TopicViewerBackendApiService); + SubtopicViewerBackendApiService + ); + topicViewerBackendApiService = TestBed.inject(TopicViewerBackendApiService); urlService = TestBed.inject(UrlService); loaderService = TestBed.inject(LoaderService); translateService = TestBed.inject(TranslateService); spyOn(i18nLanguageCodeService, 'isCurrentLanguageRTL').and.returnValue( - true); + true + ); }); - it('should successfully get topic/subtopic data and set context with ' + - 'next subtopic card', fakeAsync(() => { - spyOn(component, 'subscribeToOnLangChange'); - spyOn(contextService, 'setCustomEntityContext'); - spyOn(contextService, 'removeCustomEntityContext'); - spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue( - 'topic-url'); - spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl').and.returnValue( - 'classroom-url'); - spyOn(urlService, 'getSubtopicUrlFragmentFromLearnerUrl').and.returnValue( - 'subtopic-url'); - spyOn(loaderService, 'showLoadingScreen'); + it( + 'should successfully get topic/subtopic data and set context with ' + + 'next subtopic card', + fakeAsync(() => { + spyOn(component, 'subscribeToOnLangChange'); + spyOn(contextService, 'setCustomEntityContext'); + spyOn(contextService, 'removeCustomEntityContext'); + spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue( + 'topic-url' + ); + spyOn( + urlService, + 'getClassroomUrlFragmentFromLearnerUrl' + ).and.returnValue('classroom-url'); + spyOn(urlService, 'getSubtopicUrlFragmentFromLearnerUrl').and.returnValue( + 'subtopic-url' + ); + spyOn(loaderService, 'showLoadingScreen'); - expect(component.subtopicSummaryIsShown).toBe(false); - spyOn(subtopicViewerBackendApiService, 'fetchSubtopicDataAsync') - .and.returnValue(Promise.resolve(subtopicDataObject)); - spyOn(topicViewerBackendApiService, 'fetchTopicDataAsync') - .and.returnValue(Promise.resolve(topicDataObject)); - spyOn(i18nLanguageCodeService, 'getSubtopicTranslationKey') - .and.returnValue('I18N_SUBTOPIC_123abcd_test_TITLE'); - spyOn(i18nLanguageCodeService, 'getTopicTranslationKey') - .and.returnValue('I18N_SUBTOPIC_123abcd_test_TITLE'); - spyOn(i18nLanguageCodeService, 'isCurrentLanguageEnglish') - .and.returnValue(false); - spyOn(i18nLanguageCodeService, 'isHackyTranslationAvailable') - .and.returnValue(true); + expect(component.subtopicSummaryIsShown).toBe(false); + spyOn( + subtopicViewerBackendApiService, + 'fetchSubtopicDataAsync' + ).and.returnValue(Promise.resolve(subtopicDataObject)); + spyOn( + topicViewerBackendApiService, + 'fetchTopicDataAsync' + ).and.returnValue(Promise.resolve(topicDataObject)); + spyOn( + i18nLanguageCodeService, + 'getSubtopicTranslationKey' + ).and.returnValue('I18N_SUBTOPIC_123abcd_test_TITLE'); + spyOn(i18nLanguageCodeService, 'getTopicTranslationKey').and.returnValue( + 'I18N_SUBTOPIC_123abcd_test_TITLE' + ); + spyOn( + i18nLanguageCodeService, + 'isCurrentLanguageEnglish' + ).and.returnValue(false); + spyOn( + i18nLanguageCodeService, + 'isHackyTranslationAvailable' + ).and.returnValue(true); - component.ngOnInit(); - tick(); + component.ngOnInit(); + tick(); - expect(component.pageContents).toEqual( - subtopicDataObject.getPageContents()); - expect(component.subtopicTitle).toEqual( - subtopicDataObject.getSubtopicTitle()); - expect(component.parentTopicId).toEqual( - subtopicDataObject.getParentTopicId()); - expect(component.parentTopicTitle).toEqual( - subtopicDataObject.getParentTopicName()); - expect(component.nextSubtopic).toEqual( - subtopicDataObject.getNextSubtopic()); - expect(component.prevSubtopic).toBeUndefined(); - expect(component.subtopicSummaryIsShown).toBeTrue(); + expect(component.pageContents).toEqual( + subtopicDataObject.getPageContents() + ); + expect(component.subtopicTitle).toEqual( + subtopicDataObject.getSubtopicTitle() + ); + expect(component.parentTopicId).toEqual( + subtopicDataObject.getParentTopicId() + ); + expect(component.parentTopicTitle).toEqual( + subtopicDataObject.getParentTopicName() + ); + expect(component.nextSubtopic).toEqual( + subtopicDataObject.getNextSubtopic() + ); + expect(component.prevSubtopic).toBeUndefined(); + expect(component.subtopicSummaryIsShown).toBeTrue(); - expect(component.subtopicTitleTranslationKey).toEqual( - 'I18N_SUBTOPIC_123abcd_test_TITLE'); - expect(component.parentTopicTitleTranslationKey).toEqual( - 'I18N_SUBTOPIC_123abcd_test_TITLE'); - let hackySubtopicTitleTranslationIsDisplayed = - component.isHackySubtopicTitleTranslationDisplayed(); - let hackyTopicTitleTranslationIsDisplayed = - component.isHackyTopicTitleTranslationDisplayed(); - expect(hackySubtopicTitleTranslationIsDisplayed).toBe(true); - expect(hackyTopicTitleTranslationIsDisplayed).toBe(true); - expect(contextService.setCustomEntityContext).toHaveBeenCalled(); - expect(component.subscribeToOnLangChange).toHaveBeenCalled(); + expect(component.subtopicTitleTranslationKey).toEqual( + 'I18N_SUBTOPIC_123abcd_test_TITLE' + ); + expect(component.parentTopicTitleTranslationKey).toEqual( + 'I18N_SUBTOPIC_123abcd_test_TITLE' + ); + let hackySubtopicTitleTranslationIsDisplayed = + component.isHackySubtopicTitleTranslationDisplayed(); + let hackyTopicTitleTranslationIsDisplayed = + component.isHackyTopicTitleTranslationDisplayed(); + expect(hackySubtopicTitleTranslationIsDisplayed).toBe(true); + expect(hackyTopicTitleTranslationIsDisplayed).toBe(true); + expect(contextService.setCustomEntityContext).toHaveBeenCalled(); + expect(component.subscribeToOnLangChange).toHaveBeenCalled(); - component.ngOnDestroy(); - expect(contextService.removeCustomEntityContext).toHaveBeenCalled(); - })); + component.ngOnDestroy(); + expect(contextService.removeCustomEntityContext).toHaveBeenCalled(); + }) + ); - it('should obtain translated title and set it whenever the ' + - 'selected language changes', () => { - component.subscribeToOnLangChange(); - spyOn(component, 'setPageTitle'); - translateService.onLangChange.emit(); + it( + 'should obtain translated title and set it whenever the ' + + 'selected language changes', + () => { + component.subscribeToOnLangChange(); + spyOn(component, 'setPageTitle'); + translateService.onLangChange.emit(); - expect(component.setPageTitle).toHaveBeenCalled(); - }); + expect(component.setPageTitle).toHaveBeenCalled(); + } + ); it('should set page title', () => { spyOn(translateService, 'instant').and.callThrough(); @@ -247,11 +277,14 @@ describe('Subtopic viewer page', function() { component.setPageTitle(); expect(translateService.instant).toHaveBeenCalledWith( - 'I18N_SUBTOPIC_VIEWER_PAGE_TITLE', { - subtopicTitle: 'Subtopic Title' - }); + 'I18N_SUBTOPIC_VIEWER_PAGE_TITLE', + { + subtopicTitle: 'Subtopic Title', + } + ); expect(pageTitleService.setDocumentTitle).toHaveBeenCalledWith( - 'I18N_SUBTOPIC_VIEWER_PAGE_TITLE'); + 'I18N_SUBTOPIC_VIEWER_PAGE_TITLE' + ); }); it('should unsubscribe upon component destruction', () => { @@ -262,59 +295,73 @@ describe('Subtopic viewer page', function() { expect(component.directiveSubscriptions.closed).toBe(true); }); - it('should successfully get subtopic data with prev subtopic card', - fakeAsync(() => { - spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue( - 'topic-url'); - spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl') - .and.returnValue('classroom-url'); - spyOn(urlService, 'getSubtopicUrlFragmentFromLearnerUrl').and.returnValue( - 'subtopic-url'); - spyOn(loaderService, 'showLoadingScreen'); + it('should successfully get subtopic data with prev subtopic card', fakeAsync(() => { + spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue( + 'topic-url' + ); + spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl').and.returnValue( + 'classroom-url' + ); + spyOn(urlService, 'getSubtopicUrlFragmentFromLearnerUrl').and.returnValue( + 'subtopic-url' + ); + spyOn(loaderService, 'showLoadingScreen'); - expect(component.subtopicSummaryIsShown).toBe(false); - spyOn(subtopicViewerBackendApiService, 'fetchSubtopicDataAsync') - .and.returnValue(Promise.resolve(subtopicDataObjectWithPrevSubtopic)); + expect(component.subtopicSummaryIsShown).toBe(false); + spyOn( + subtopicViewerBackendApiService, + 'fetchSubtopicDataAsync' + ).and.returnValue(Promise.resolve(subtopicDataObjectWithPrevSubtopic)); - component.ngOnInit(); - tick(); + component.ngOnInit(); + tick(); - expect(component.pageContents).toEqual( - subtopicDataObjectWithPrevSubtopic.getPageContents()); - expect(component.subtopicTitle).toEqual( - subtopicDataObjectWithPrevSubtopic.getSubtopicTitle()); - expect(component.parentTopicId).toEqual( - subtopicDataObjectWithPrevSubtopic.getParentTopicId()); - expect(component.prevSubtopic).toEqual( - subtopicDataObjectWithPrevSubtopic.getPrevSubtopic()); - expect(component.nextSubtopic).toBeUndefined(); - expect(component.subtopicSummaryIsShown).toBeTrue(); + expect(component.pageContents).toEqual( + subtopicDataObjectWithPrevSubtopic.getPageContents() + ); + expect(component.subtopicTitle).toEqual( + subtopicDataObjectWithPrevSubtopic.getSubtopicTitle() + ); + expect(component.parentTopicId).toEqual( + subtopicDataObjectWithPrevSubtopic.getParentTopicId() + ); + expect(component.prevSubtopic).toEqual( + subtopicDataObjectWithPrevSubtopic.getPrevSubtopic() + ); + expect(component.nextSubtopic).toBeUndefined(); + expect(component.subtopicSummaryIsShown).toBeTrue(); - component.ngOnDestroy(); - })); + component.ngOnDestroy(); + })); - it( - 'should use reject handler when fetching subtopic data fails', - fakeAsync(() => { - spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue( - 'topic-url'); - spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl') - .and.returnValue('classroom-url'); - spyOn(urlService, 'getSubtopicUrlFragmentFromLearnerUrl').and.returnValue( - 'subtopic-url'); - spyOn(loaderService, 'showLoadingScreen'); - spyOn(subtopicViewerBackendApiService, 'fetchSubtopicDataAsync').and - .returnValue(Promise.reject({ - status: 404 - })); - spyOn(alertsService, 'addWarning'); - component.ngOnInit(); - tick(); - expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Failed to get subtopic data'); - })); + it('should use reject handler when fetching subtopic data fails', fakeAsync(() => { + spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue( + 'topic-url' + ); + spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl').and.returnValue( + 'classroom-url' + ); + spyOn(urlService, 'getSubtopicUrlFragmentFromLearnerUrl').and.returnValue( + 'subtopic-url' + ); + spyOn(loaderService, 'showLoadingScreen'); + spyOn( + subtopicViewerBackendApiService, + 'fetchSubtopicDataAsync' + ).and.returnValue( + Promise.reject({ + status: 404, + }) + ); + spyOn(alertsService, 'addWarning'); + component.ngOnInit(); + tick(); + expect(alertsService.addWarning).toHaveBeenCalledWith( + 'Failed to get subtopic data' + ); + })); - it('should check if the view is mobile or not', function() { + it('should check if the view is mobile or not', function () { let widthSpy = spyOn(windowDimensionsService, 'getWidth'); widthSpy.and.returnValue(400); expect(component.checkMobileView()).toBe(true); diff --git a/core/templates/pages/subtopic-viewer-page/subtopic-viewer-page.component.ts b/core/templates/pages/subtopic-viewer-page/subtopic-viewer-page.component.ts index 9c2a3e895fb4..b13bed3aa291 100644 --- a/core/templates/pages/subtopic-viewer-page/subtopic-viewer-page.component.ts +++ b/core/templates/pages/subtopic-viewer-page/subtopic-viewer-page.component.ts @@ -16,31 +16,33 @@ * @fileoverview Component for the subtopic viewer. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; - -import { AppConstants } from 'app.constants'; -import { SubtopicViewerBackendApiService } from 'domain/subtopic_viewer/subtopic-viewer-backend-api.service'; -import { SubtopicPageContents } from 'domain/topic/subtopic-page-contents.model'; -import { Subtopic } from 'domain/topic/subtopic.model'; -import { TopicViewerBackendApiService } from 'domain/topic_viewer/topic-viewer-backend-api.service'; -import { AlertsService } from 'services/alerts.service'; -import { ContextService } from 'services/context.service'; -import { UrlService } from 'services/contextual/url.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { I18nLanguageCodeService, TranslationKeyType } from 'services/i18n-language-code.service'; -import { LoaderService } from 'services/loader.service'; -import { PageTitleService } from 'services/page-title.service'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; + +import {AppConstants} from 'app.constants'; +import {SubtopicViewerBackendApiService} from 'domain/subtopic_viewer/subtopic-viewer-backend-api.service'; +import {SubtopicPageContents} from 'domain/topic/subtopic-page-contents.model'; +import {Subtopic} from 'domain/topic/subtopic.model'; +import {TopicViewerBackendApiService} from 'domain/topic_viewer/topic-viewer-backend-api.service'; +import {AlertsService} from 'services/alerts.service'; +import {ContextService} from 'services/context.service'; +import {UrlService} from 'services/contextual/url.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import { + I18nLanguageCodeService, + TranslationKeyType, +} from 'services/i18n-language-code.service'; +import {LoaderService} from 'services/loader.service'; +import {PageTitleService} from 'services/page-title.service'; import './subtopic-viewer-page.component.css'; - @Component({ selector: 'oppia-subtopic-viewer-page', templateUrl: './subtopic-viewer-page.component.html', - styleUrls: ['./subtopic-viewer-page.component.css'] + styleUrls: ['./subtopic-viewer-page.component.css'], }) export class SubtopicViewerPageComponent implements OnInit, OnDestroy { // These properties are initialized using Angular lifecycle hooks @@ -74,7 +76,7 @@ export class SubtopicViewerPageComponent implements OnInit, OnDestroy { ) {} checkMobileView(): boolean { - return (this.windowDimensionsService.getWidth() < 500); + return this.windowDimensionsService.getWidth() < 500; } subscribeToOnLangChange(): void { @@ -87,80 +89,90 @@ export class SubtopicViewerPageComponent implements OnInit, OnDestroy { setPageTitle(): void { let translatedTitle = this.translateService.instant( - 'I18N_SUBTOPIC_VIEWER_PAGE_TITLE', { - subtopicTitle: this.subtopicTitle - }); + 'I18N_SUBTOPIC_VIEWER_PAGE_TITLE', + { + subtopicTitle: this.subtopicTitle, + } + ); this.pageTitleService.setDocumentTitle(translatedTitle); } ngOnInit(): void { - this.topicUrlFragment = ( - this.urlService.getTopicUrlFragmentFromLearnerUrl()); - this.classroomUrlFragment = ( - this.urlService.getClassroomUrlFragmentFromLearnerUrl()); - this.subtopicUrlFragment = ( - this.urlService.getSubtopicUrlFragmentFromLearnerUrl()); + this.topicUrlFragment = this.urlService.getTopicUrlFragmentFromLearnerUrl(); + this.classroomUrlFragment = + this.urlService.getClassroomUrlFragmentFromLearnerUrl(); + this.subtopicUrlFragment = + this.urlService.getSubtopicUrlFragmentFromLearnerUrl(); this.loaderService.showLoadingScreen('Loading'); - this.subtopicViewerBackendApiService.fetchSubtopicDataAsync( - this.topicUrlFragment, - this.classroomUrlFragment, - this.subtopicUrlFragment).then((subtopicDataObject) => { - this.pageContents = subtopicDataObject.getPageContents(); - this.subtopicTitle = subtopicDataObject.getSubtopicTitle(); - this.parentTopicId = subtopicDataObject.getParentTopicId(); - this.contextService.setCustomEntityContext( - AppConstants.ENTITY_TYPE.TOPIC, this.parentTopicId); - - // The onLangChange event is initially fired before the subtopic is - // loaded. Hence the first setpageTitle() call needs to made - // manually, and the onLangChange subscription is added after - // the subtopic is loaded. - this.setPageTitle(); - this.subscribeToOnLangChange(); - this.pageTitleService.updateMetaTag( - `Review the skill of ${this.subtopicTitle.toLowerCase()}.`); - - let nextSubtopic = subtopicDataObject.getNextSubtopic(); - let prevSubtopic = subtopicDataObject.getPrevSubtopic(); - if (nextSubtopic) { - this.nextSubtopic = nextSubtopic; - this.subtopicSummaryIsShown = true; - } - if (prevSubtopic) { - this.prevSubtopic = prevSubtopic; - this.subtopicSummaryIsShown = true; - } - - this.subtopicTitleTranslationKey = ( - this.i18nLanguageCodeService. - getSubtopicTranslationKey( - this.parentTopicId, this.subtopicUrlFragment, - TranslationKeyType.TITLE) - ); - - this.topicViewerBackendApiService.fetchTopicDataAsync( + this.subtopicViewerBackendApiService + .fetchSubtopicDataAsync( this.topicUrlFragment, - this.classroomUrlFragment - ).then(topicDataObject => { - this.parentTopicTitle = topicDataObject.getTopicName(); - this.parentTopicTitleTranslationKey = ( - this.i18nLanguageCodeService - .getTopicTranslationKey( - topicDataObject.getTopicId(), + this.classroomUrlFragment, + this.subtopicUrlFragment + ) + .then( + subtopicDataObject => { + this.pageContents = subtopicDataObject.getPageContents(); + this.subtopicTitle = subtopicDataObject.getSubtopicTitle(); + this.parentTopicId = subtopicDataObject.getParentTopicId(); + this.contextService.setCustomEntityContext( + AppConstants.ENTITY_TYPE.TOPIC, + this.parentTopicId + ); + + // The onLangChange event is initially fired before the subtopic is + // loaded. Hence the first setpageTitle() call needs to made + // manually, and the onLangChange subscription is added after + // the subtopic is loaded. + this.setPageTitle(); + this.subscribeToOnLangChange(); + this.pageTitleService.updateMetaTag( + `Review the skill of ${this.subtopicTitle.toLowerCase()}.` + ); + + let nextSubtopic = subtopicDataObject.getNextSubtopic(); + let prevSubtopic = subtopicDataObject.getPrevSubtopic(); + if (nextSubtopic) { + this.nextSubtopic = nextSubtopic; + this.subtopicSummaryIsShown = true; + } + if (prevSubtopic) { + this.prevSubtopic = prevSubtopic; + this.subtopicSummaryIsShown = true; + } + + this.subtopicTitleTranslationKey = + this.i18nLanguageCodeService.getSubtopicTranslationKey( + this.parentTopicId, + this.subtopicUrlFragment, TranslationKeyType.TITLE + ); + + this.topicViewerBackendApiService + .fetchTopicDataAsync( + this.topicUrlFragment, + this.classroomUrlFragment ) - ); - }); - - this.loaderService.hideLoadingScreen(); - }, - (errorResponse) => { - if ( - AppConstants.FATAL_ERROR_CODES.indexOf(errorResponse.status) !== -1) { - this.alertsService.addWarning('Failed to get subtopic data'); - } - }); + .then(topicDataObject => { + this.parentTopicTitle = topicDataObject.getTopicName(); + this.parentTopicTitleTranslationKey = + this.i18nLanguageCodeService.getTopicTranslationKey( + topicDataObject.getTopicId(), + TranslationKeyType.TITLE + ); + }); + + this.loaderService.hideLoadingScreen(); + }, + errorResponse => { + if ( + AppConstants.FATAL_ERROR_CODES.indexOf(errorResponse.status) !== -1 + ) { + this.alertsService.addWarning('Failed to get subtopic data'); + } + } + ); } ngOnDestroy(): void { @@ -185,8 +197,9 @@ export class SubtopicViewerPageComponent implements OnInit, OnDestroy { } } - - -angular.module('oppia').directive( - 'oppiaSubtopicViewerPage', - downgradeComponent({component: SubtopicViewerPageComponent})); +angular + .module('oppia') + .directive( + 'oppiaSubtopicViewerPage', + downgradeComponent({component: SubtopicViewerPageComponent}) + ); diff --git a/core/templates/pages/subtopic-viewer-page/subtopic-viewer-page.import.ts b/core/templates/pages/subtopic-viewer-page/subtopic-viewer-page.import.ts index b915ff6c9a62..6048ad63bba0 100644 --- a/core/templates/pages/subtopic-viewer-page/subtopic-viewer-page.import.ts +++ b/core/templates/pages/subtopic-viewer-page/subtopic-viewer-page.import.ts @@ -22,9 +22,14 @@ import 'zone.js'; import uiValidate from 'angular-ui-validate'; angular.module('oppia', [ - require('angular-cookies'), 'ngAnimate', - 'ngMaterial', 'ngSanitize', 'ngTouch', 'pascalprecht.translate', - 'ui.bootstrap', uiValidate + require('angular-cookies'), + 'ngAnimate', + 'ngMaterial', + 'ngSanitize', + 'ngTouch', + 'pascalprecht.translate', + 'ui.bootstrap', + uiValidate, ]); require('Polyfills.ts'); diff --git a/core/templates/pages/subtopic-viewer-page/subtopic-viewer-page.module.ts b/core/templates/pages/subtopic-viewer-page/subtopic-viewer-page.module.ts index 5878870ea21f..d1c94f72b9a6 100644 --- a/core/templates/pages/subtopic-viewer-page/subtopic-viewer-page.module.ts +++ b/core/templates/pages/subtopic-viewer-page/subtopic-viewer-page.module.ts @@ -16,26 +16,28 @@ * @fileoverview Module for the story viewer page. */ -import { APP_INITIALIZER, NgModule, StaticProvider } from '@angular/core'; -import { BrowserModule, HAMMER_GESTURE_CONFIG } from '@angular/platform-browser'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { HttpClientModule } from '@angular/common/http'; -import { HTTP_INTERCEPTORS } from '@angular/common/http'; -import { RouterModule } from '@angular/router'; -import { APP_BASE_HREF } from '@angular/common'; +import {APP_INITIALIZER, NgModule, StaticProvider} from '@angular/core'; +import {BrowserModule, HAMMER_GESTURE_CONFIG} from '@angular/platform-browser'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {HttpClientModule} from '@angular/common/http'; +import {HTTP_INTERCEPTORS} from '@angular/common/http'; +import {RouterModule} from '@angular/router'; +import {APP_BASE_HREF} from '@angular/common'; -import { RequestInterceptor } from 'services/request-interceptor.service'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { OppiaAngularRootComponent } from 'components/oppia-angular-root.component'; -import { SubtopicViewerNavbarBreadcrumbComponent } from './navbar-breadcrumb/subtopic-viewer-navbar-breadcrumb.component'; -import { SubtopicViewerPageComponent } from './subtopic-viewer-page.component'; -import { SubtopicViewerNavbarPreLogoActionComponent } from './navbar-pre-logo-action/subtopic-viewer-navbar-pre-logo-action.component'; -import { platformFeatureInitFactory, PlatformFeatureService } from - 'services/platform-feature.service'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { MyHammerConfig, toastrConfig } from 'pages/oppia-root/app.module'; -import { SmartRouterModule } from 'hybrid-router-module-provider'; -import { AppErrorHandlerProvider } from 'pages/oppia-root/app-error-handler'; +import {RequestInterceptor} from 'services/request-interceptor.service'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {OppiaAngularRootComponent} from 'components/oppia-angular-root.component'; +import {SubtopicViewerNavbarBreadcrumbComponent} from './navbar-breadcrumb/subtopic-viewer-navbar-breadcrumb.component'; +import {SubtopicViewerPageComponent} from './subtopic-viewer-page.component'; +import {SubtopicViewerNavbarPreLogoActionComponent} from './navbar-pre-logo-action/subtopic-viewer-navbar-pre-logo-action.component'; +import { + platformFeatureInitFactory, + PlatformFeatureService, +} from 'services/platform-feature.service'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {MyHammerConfig, toastrConfig} from 'pages/oppia-root/app.module'; +import {SmartRouterModule} from 'hybrid-router-module-provider'; +import {AppErrorHandlerProvider} from 'pages/oppia-root/app-error-handler'; @NgModule({ imports: [ @@ -47,52 +49,52 @@ import { AppErrorHandlerProvider } from 'pages/oppia-root/app-error-handler'; SmartRouterModule, RouterModule.forRoot([]), SharedComponentsModule, - ToastrModule.forRoot(toastrConfig) + ToastrModule.forRoot(toastrConfig), ], declarations: [ SubtopicViewerNavbarBreadcrumbComponent, SubtopicViewerPageComponent, - SubtopicViewerNavbarPreLogoActionComponent + SubtopicViewerNavbarPreLogoActionComponent, ], entryComponents: [ SubtopicViewerNavbarBreadcrumbComponent, SubtopicViewerPageComponent, SubtopicViewerNavbarPreLogoActionComponent, - SubtopicViewerNavbarBreadcrumbComponent + SubtopicViewerNavbarBreadcrumbComponent, ], providers: [ { provide: HTTP_INTERCEPTORS, useClass: RequestInterceptor, - multi: true + multi: true, }, { provide: APP_INITIALIZER, useFactory: platformFeatureInitFactory, deps: [PlatformFeatureService], - multi: true + multi: true, }, { provide: HAMMER_GESTURE_CONFIG, - useClass: MyHammerConfig + useClass: MyHammerConfig, }, AppErrorHandlerProvider, { provide: APP_BASE_HREF, - useValue: '/' - } - ] + useValue: '/', + }, + ], }) class SubtopicViewerPageModule { // Empty placeholder method to satisfy the `Compiler`. ngDoBootstrap() {} } -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { downgradeModule } from '@angular/upgrade/static'; -import { ToastrModule } from 'ngx-toastr'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {downgradeModule} from '@angular/upgrade/static'; +import {ToastrModule} from 'ngx-toastr'; -const bootstrapFnAsync = async(extraProviders: StaticProvider[]) => { +const bootstrapFnAsync = async (extraProviders: StaticProvider[]) => { const platformRef = platformBrowserDynamic(extraProviders); return platformRef.bootstrapModule(SubtopicViewerPageModule); }; @@ -107,5 +109,6 @@ angular.module('oppia').directive( // bootstrap the Angular 8. 'oppiaAngularRoot', downgradeComponent({ - component: OppiaAngularRootComponent - }) as angular.IDirectiveFactory); + component: OppiaAngularRootComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/teach-page/teach-page-root.component.spec.ts b/core/templates/pages/teach-page/teach-page-root.component.spec.ts index 7973a2dbb522..c65d4d7e3625 100644 --- a/core/templates/pages/teach-page/teach-page-root.component.spec.ts +++ b/core/templates/pages/teach-page/teach-page-root.component.spec.ts @@ -16,14 +16,14 @@ * @fileoverview Unit tests for the teach page root component. */ -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { TeachPageRootComponent } from './teach-page-root.component'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {TeachPageRootComponent} from './teach-page-root.component'; class MockTranslateService { onLangChange: EventEmitter = new EventEmitter(); @@ -38,21 +38,17 @@ describe('Teach Page Root', () => { let pageHeadService: PageHeadService; let translateService: TranslateService; - beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - TeachPageRootComponent, - MockTranslatePipe - ], + declarations: [TeachPageRootComponent, MockTranslatePipe], providers: [ PageHeadService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -63,10 +59,9 @@ describe('Teach Page Root', () => { translateService = TestBed.inject(TranslateService); }); - it('should successfully instantiate the component', - () => { - expect(component).toBeDefined(); - }); + it('should successfully instantiate the component', () => { + expect(component).toBeDefined(); + }); it('should initialize and subscribe to onLangChange', () => { spyOn(translateService.onLangChange, 'subscribe'); @@ -92,10 +87,12 @@ describe('Teach Page Root', () => { component.setPageTitleAndMetaTags(); expect(translateService.instant).toHaveBeenCalledWith( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.TEACH.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.TEACH.TITLE + ); expect(pageHeadService.updateTitleAndMetaTags).toHaveBeenCalledWith( AppConstants.PAGES_REGISTERED_WITH_FRONTEND.TEACH.TITLE, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.TEACH.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.TEACH.META + ); }); it('should unsubscribe on component destruction', () => { diff --git a/core/templates/pages/teach-page/teach-page-root.component.ts b/core/templates/pages/teach-page/teach-page-root.component.ts index 9f3439f70239..cbdaa8c4040e 100644 --- a/core/templates/pages/teach-page/teach-page-root.component.ts +++ b/core/templates/pages/teach-page/teach-page-root.component.ts @@ -16,16 +16,16 @@ * @fileoverview Root component for Teach Page. */ -import { Component, OnDestroy } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; +import {Component, OnDestroy} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; @Component({ selector: 'oppia-teach-page-root', - templateUrl: './teach-page-root.component.html' + templateUrl: './teach-page-root.component.html', }) export class TeachPageRootComponent implements OnDestroy { directiveSubscriptions = new Subscription(); @@ -36,10 +36,12 @@ export class TeachPageRootComponent implements OnDestroy { setPageTitleAndMetaTags(): void { let translatedTitle = this.translateService.instant( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.TEACH.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.TEACH.TITLE + ); this.pageHeadService.updateTitleAndMetaTags( translatedTitle, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.TEACH.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.TEACH.META + ); } ngOnInit(): void { diff --git a/core/templates/pages/teach-page/teach-page-routing.module.ts b/core/templates/pages/teach-page/teach-page-routing.module.ts index 364e48973455..b8bc068ed85f 100644 --- a/core/templates/pages/teach-page/teach-page-routing.module.ts +++ b/core/templates/pages/teach-page/teach-page-routing.module.ts @@ -16,24 +16,19 @@ * @fileoverview Routing module for teach page. */ -import { NgModule } from '@angular/core'; -import { Route, RouterModule } from '@angular/router'; -import { TeachPageRootComponent } from './teach-page-root.component'; +import {NgModule} from '@angular/core'; +import {Route, RouterModule} from '@angular/router'; +import {TeachPageRootComponent} from './teach-page-root.component'; const routes: Route[] = [ { path: '', - component: TeachPageRootComponent - } + component: TeachPageRootComponent, + }, ]; @NgModule({ - imports: [ - RouterModule.forChild(routes) - ], - exports: [ - RouterModule - ] + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], }) - export class TeachPageRoutingModule {} diff --git a/core/templates/pages/teach-page/teach-page.component.spec.ts b/core/templates/pages/teach-page/teach-page.component.spec.ts index 5a2d404d0dcc..fc641b0963ed 100644 --- a/core/templates/pages/teach-page/teach-page.component.spec.ts +++ b/core/templates/pages/teach-page/teach-page.component.spec.ts @@ -15,22 +15,20 @@ /** * @fileoverview Unit tests for the teach page. */ -import { EventEmitter } from '@angular/core'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { TeachPageComponent } from './teach-page.component'; -import { LoaderService } from 'services/loader.service'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; -import { WindowDimensionsService } from - 'services/contextual/window-dimensions.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { UserInfo } from 'domain/user/user-info.model'; -import { UserService } from 'services/user.service'; -import { of } from 'rxjs'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; +import {EventEmitter} from '@angular/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {TeachPageComponent} from './teach-page.component'; +import {LoaderService} from 'services/loader.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {UserInfo} from 'domain/user/user-info.model'; +import {UserService} from 'services/user.service'; +import {of} from 'rxjs'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; class MockWindowRef { _window = { location: { @@ -41,13 +39,13 @@ class MockWindowRef { set href(val) { this._href = val; }, - replace: (val: string) => {} + replace: (val: string) => {}, }, sessionStorage: { last_uploaded_audio_lang: 'en', - removeItem: (name: string) => {} + removeItem: (name: string) => {}, }, - gtag: () => {} + gtag: () => {}, }; get nativeWindow() { @@ -73,36 +71,36 @@ describe('Teach Page', () => { let windowDimensionsService: WindowDimensionsService; let windowRef: MockWindowRef; let resizeEvent = new Event('resize'); - beforeEach(async() => { + beforeEach(async () => { windowRef = new MockWindowRef(); TestBed.configureTestingModule({ declarations: [TeachPageComponent, MockTranslatePipe], providers: [ { provide: I18nLanguageCodeService, - useClass: MockI18nLanguageCodeService + useClass: MockI18nLanguageCodeService, }, { provide: WindowRef, - useValue: windowRef + useValue: windowRef, }, { provide: WindowDimensionsService, useValue: { isWindowNarrow: () => true, - getResizeEvent: () => of(resizeEvent) - } + getResizeEvent: () => of(resizeEvent), + }, }, SiteAnalyticsService, UrlInterpolationService, - ] + ], }).compileComponents(); }); beforeEach(angular.mock.module('oppia')); beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] + imports: [HttpClientTestingModule], }); loaderService = TestBed.get(LoaderService); userService = TestBed.get(UserService); @@ -116,13 +114,13 @@ describe('Teach Page', () => { component = teachPageComponent.componentInstance; }); - it('should successfully instantiate the component from beforeEach block', - () => { - expect(component).toBeDefined(); - }); + it('should successfully instantiate the component from beforeEach block', () => { + expect(component).toBeDefined(); + }); it('should get static image url', () => { expect(component.getStaticImageUrl('/path/to/image')).toBe( - '/assets/images/path/to/image'); + '/assets/images/path/to/image' + ); }); it('should set component properties when ngOnInit() is called', () => { @@ -139,8 +137,7 @@ describe('Teach Page', () => { fakeAsync(() => { component.ngOnInit(); spyOn(loaderService, 'showLoadingScreen').and.callThrough(); - expect(loaderService.showLoadingScreen) - .toHaveBeenCalledWith('Loading'); + expect(loaderService.showLoadingScreen).toHaveBeenCalledWith('Loading'); })); it('should check if user is logged in or not', fakeAsync(() => { @@ -154,10 +151,10 @@ describe('Teach Page', () => { preferred_site_language_code: null, username: 'tester', email: 'test@test.com', - user_is_logged_in: true + user_is_logged_in: true, }; - spyOn(userService, 'getUserInfoAsync').and.returnValue(Promise.resolve( - UserInfo.createFromBackendDict(UserInfoObject)) + spyOn(userService, 'getUserInfoAsync').and.returnValue( + Promise.resolve(UserInfo.createFromBackendDict(UserInfoObject)) ); component.ngOnInit(); flushMicrotasks(); @@ -166,77 +163,83 @@ describe('Teach Page', () => { it('should record analytics when Start Learning is clicked', () => { spyOn( - siteAnalyticsService, 'registerClickStartLearningButtonEvent') - .and.callThrough(); + siteAnalyticsService, + 'registerClickStartLearningButtonEvent' + ).and.callThrough(); component.onClickStartLearningButton(); - expect(siteAnalyticsService.registerClickStartLearningButtonEvent) - .toHaveBeenCalled(); + expect( + siteAnalyticsService.registerClickStartLearningButtonEvent + ).toHaveBeenCalled(); }); it('should record analytics when Visit Classroom is clicked', () => { spyOn( - siteAnalyticsService, 'registerClickVisitClassroomButtonEvent') - .and.callThrough(); + siteAnalyticsService, + 'registerClickVisitClassroomButtonEvent' + ).and.callThrough(); component.onClickVisitClassroomButton(); - expect(siteAnalyticsService.registerClickVisitClassroomButtonEvent) - .toHaveBeenCalled(); + expect( + siteAnalyticsService.registerClickVisitClassroomButtonEvent + ).toHaveBeenCalled(); }); - it('should redirect to library page when Browse Library is clicked', - () => { - component.onClickBrowseLibraryButton(); - expect(windowRef.nativeWindow.location.href).toBe('/community-library'); - } - ); + it('should redirect to library page when Browse Library is clicked', () => { + component.onClickBrowseLibraryButton(); + expect(windowRef.nativeWindow.location.href).toBe('/community-library'); + }); it('should record analytics when Browse Library is clicked', () => { spyOn( - siteAnalyticsService, 'registerClickBrowseLibraryButtonEvent') - .and.callThrough(); + siteAnalyticsService, + 'registerClickBrowseLibraryButtonEvent' + ).and.callThrough(); component.onClickBrowseLibraryButton(); - expect(siteAnalyticsService.registerClickBrowseLibraryButtonEvent) - .toHaveBeenCalled(); + expect( + siteAnalyticsService.registerClickBrowseLibraryButtonEvent + ).toHaveBeenCalled(); }); - it('should redirect to teach page when Guide For Parents is clicked', - () => { - component.onClickGuideParentsButton(); - expect(windowRef.nativeWindow.location.href).toBe('/teach'); - } - ); + it('should redirect to teach page when Guide For Parents is clicked', () => { + component.onClickGuideParentsButton(); + expect(windowRef.nativeWindow.location.href).toBe('/teach'); + }); it('should record analytics when Guide For Parents is clicked', () => { spyOn( - siteAnalyticsService, 'registerClickGuideParentsButtonEvent') - .and.callThrough(); + siteAnalyticsService, + 'registerClickGuideParentsButtonEvent' + ).and.callThrough(); component.onClickGuideParentsButton(); - expect(siteAnalyticsService.registerClickGuideParentsButtonEvent) - .toHaveBeenCalled(); + expect( + siteAnalyticsService.registerClickGuideParentsButtonEvent + ).toHaveBeenCalled(); }); - it('should redirect to teach page when Tips For Parents is clicked', - () => { - component.onClickTipforParentsButton(); - expect(windowRef.nativeWindow.location.href).toBe('/teach'); - } - ); + it('should redirect to teach page when Tips For Parents is clicked', () => { + component.onClickTipforParentsButton(); + expect(windowRef.nativeWindow.location.href).toBe('/teach'); + }); it('should record analytics when Tips For Parents is clicked', () => { spyOn( - siteAnalyticsService, 'registerClickTipforParentsButtonEvent') - .and.callThrough(); + siteAnalyticsService, + 'registerClickTipforParentsButtonEvent' + ).and.callThrough(); component.onClickTipforParentsButton(); - expect(siteAnalyticsService.registerClickTipforParentsButtonEvent) - .toHaveBeenCalled(); + expect( + siteAnalyticsService.registerClickTipforParentsButtonEvent + ).toHaveBeenCalled(); }); it('should record analytics when Explore Lessons is clicked', () => { spyOn( - siteAnalyticsService, 'registerClickExploreLessonsButtonEvent') - .and.callThrough(); + siteAnalyticsService, + 'registerClickExploreLessonsButtonEvent' + ).and.callThrough(); component.onClickExploreLessonsButton(); - expect(siteAnalyticsService.registerClickExploreLessonsButtonEvent) - .toHaveBeenCalled(); + expect( + siteAnalyticsService.registerClickExploreLessonsButtonEvent + ).toHaveBeenCalled(); }); it('should increment and decrement testimonial IDs correctly', () => { @@ -261,7 +264,7 @@ describe('Teach Page', () => { expect(component.getTestimonials().length).toBe(component.testimonialCount); }); - it('should direct users to the android page on click', function() { + it('should direct users to the android page on click', function () { expect(windowRef.nativeWindow.location.href).not.toEqual('/android'); component.onClickAccessAndroidButton(); diff --git a/core/templates/pages/teach-page/teach-page.component.ts b/core/templates/pages/teach-page/teach-page.component.ts index b77179395d24..847e16a09d2d 100644 --- a/core/templates/pages/teach-page/teach-page.component.ts +++ b/core/templates/pages/teach-page/teach-page.component.ts @@ -15,18 +15,17 @@ /** * @fileoverview Component for the teach page. */ -import { Component, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; - -import { AppConstants } from 'app.constants'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { LoaderService } from 'services/loader.service'; -import { UserService } from 'services/user.service'; -import { Subscription } from 'rxjs'; +import {Component, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; + +import {AppConstants} from 'app.constants'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {LoaderService} from 'services/loader.service'; +import {UserService} from 'services/user.service'; +import {Subscription} from 'rxjs'; export interface Testimonial { quote: string; @@ -40,9 +39,8 @@ export interface Testimonial { @Component({ selector: 'teach-page', templateUrl: './teach-page.component.html', - styleUrls: [] + styleUrls: [], }) - export class TeachPageComponent implements OnInit { // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see @@ -63,7 +61,7 @@ export class TeachPageComponent implements OnInit { private windowDimensionService: WindowDimensionsService, private windowRef: WindowRef, private userService: UserService, - private loaderService: LoaderService, + private loaderService: LoaderService ) {} ngOnInit(): void { @@ -72,12 +70,14 @@ export class TeachPageComponent implements OnInit { this.testimonialCount = 3; this.testimonials = this.getTestimonials(); this.classroomUrl = this.urlInterpolationService.interpolateUrl( - '/learn/', { - classroomUrlFragment: AppConstants.DEFAULT_CLASSROOM_URL_FRAGMENT - }); + '/learn/', + { + classroomUrlFragment: AppConstants.DEFAULT_CLASSROOM_URL_FRAGMENT, + } + ); this.libraryUrl = '/community-library'; this.loaderService.showLoadingScreen('Loading'); - this.userService.getUserInfoAsync().then((userInfo) => { + this.userService.getUserInfoAsync().then(userInfo => { this.userIsLoggedIn = userInfo.isLoggedIn(); this.loaderService.hideLoadingScreen(); }); @@ -85,52 +85,57 @@ export class TeachPageComponent implements OnInit { this.directiveSubscriptions.add( this.windowDimensionService.getResizeEvent().subscribe(() => { this.isWindowNarrow = this.windowDimensionService.isWindowNarrow(); - })); + }) + ); } // TODO(#11657): Extract the testimonials code into a separate component. // The 2 functions below are to cycle between values: // 0 to (testimonialCount - 1) for displayedTestimonialId. incrementDisplayedTestimonialId(): void { - // This makes sure that incrementing from (testimonialCount - 1) - // returns 0 instead of testimonialCount,since we want the testimonials - // to cycle through. - this.displayedTestimonialId = ( - this.displayedTestimonialId + 1) % this.testimonialCount; + // This makes sure that incrementing from (testimonialCount - 1) + // returns 0 instead of testimonialCount,since we want the testimonials + // to cycle through. + this.displayedTestimonialId = + (this.displayedTestimonialId + 1) % this.testimonialCount; } decrementDisplayedTestimonialId(): void { - // This makes sure that decrementing from 0, returns - // (testimonialCount - 1) instead of -1, since we want the testimonials - // to cycle through. - this.displayedTestimonialId = ( - this.displayedTestimonialId + this.testimonialCount - 1) % + // This makes sure that decrementing from 0, returns + // (testimonialCount - 1) instead of -1, since we want the testimonials + // to cycle through. + this.displayedTestimonialId = + (this.displayedTestimonialId + this.testimonialCount - 1) % this.testimonialCount; } getTestimonials(): [Testimonial, Testimonial, Testimonial] { - return [{ - quote: 'I18N_TEACH_TESTIMONIAL_1', - studentDetails: 'I18N_TEACH_STUDENT_DETAILS_1', - imageUrl: '/teach/riya.jpg', - imageUrlWebp: '/teach/riya.webp', - borderPresent: true, - altText: 'Photo of Riya' - }, { - quote: 'I18N_TEACH_TESTIMONIAL_2', - studentDetails: 'I18N_TEACH_STUDENT_DETAILS_2', - imageUrl: '/teach/awad.jpg', - imageUrlWebp: '/teach/awad.webp', - borderPresent: true, - altText: 'Photo of Awad' - }, { - quote: 'I18N_TEACH_TESTIMONIAL_3', - studentDetails: 'I18N_TEACH_STUDENT_DETAILS_3', - imageUrl: '/teach/himanshu.jpg', - imageUrlWebp: '/teach/himanshu.webp', - borderPresent: true, - altText: 'Photo of Himanshu' - }]; + return [ + { + quote: 'I18N_TEACH_TESTIMONIAL_1', + studentDetails: 'I18N_TEACH_STUDENT_DETAILS_1', + imageUrl: '/teach/riya.jpg', + imageUrlWebp: '/teach/riya.webp', + borderPresent: true, + altText: 'Photo of Riya', + }, + { + quote: 'I18N_TEACH_TESTIMONIAL_2', + studentDetails: 'I18N_TEACH_STUDENT_DETAILS_2', + imageUrl: '/teach/awad.jpg', + imageUrlWebp: '/teach/awad.webp', + borderPresent: true, + altText: 'Photo of Awad', + }, + { + quote: 'I18N_TEACH_TESTIMONIAL_3', + studentDetails: 'I18N_TEACH_STUDENT_DETAILS_3', + imageUrl: '/teach/himanshu.jpg', + imageUrlWebp: '/teach/himanshu.webp', + borderPresent: true, + altText: 'Photo of Himanshu', + }, + ]; } onClickAccessAndroidButton(): void { @@ -178,5 +183,6 @@ export class TeachPageComponent implements OnInit { } } -angular.module('oppia').directive('teachPage', - downgradeComponent({component: TeachPageComponent})); +angular + .module('oppia') + .directive('teachPage', downgradeComponent({component: TeachPageComponent})); diff --git a/core/templates/pages/teach-page/teach-page.module.ts b/core/templates/pages/teach-page/teach-page.module.ts index 09414dd7eeaf..1aa839b5f4c3 100644 --- a/core/templates/pages/teach-page/teach-page.module.ts +++ b/core/templates/pages/teach-page/teach-page.module.ts @@ -16,26 +16,16 @@ * @fileoverview Module for the teach page. */ -import { NgModule } from '@angular/core'; -import { TeachPageComponent } from './teach-page.component'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { TeachPageRootComponent } from './teach-page-root.component'; -import { CommonModule } from '@angular/common'; -import { TeachPageRoutingModule } from './teach-page-routing.module'; +import {NgModule} from '@angular/core'; +import {TeachPageComponent} from './teach-page.component'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {TeachPageRootComponent} from './teach-page-root.component'; +import {CommonModule} from '@angular/common'; +import {TeachPageRoutingModule} from './teach-page-routing.module'; @NgModule({ - imports: [ - CommonModule, - SharedComponentsModule, - TeachPageRoutingModule - ], - declarations: [ - TeachPageComponent, - TeachPageRootComponent, - ], - entryComponents: [ - TeachPageComponent, - TeachPageRootComponent, - ] + imports: [CommonModule, SharedComponentsModule, TeachPageRoutingModule], + declarations: [TeachPageComponent, TeachPageRootComponent], + entryComponents: [TeachPageComponent, TeachPageRootComponent], }) export class TeachPageModule {} diff --git a/core/templates/pages/terms-page/terms-page-root.component.spec.ts b/core/templates/pages/terms-page/terms-page-root.component.spec.ts index ddf8f6597ecd..1a03f1695b8b 100644 --- a/core/templates/pages/terms-page/terms-page-root.component.spec.ts +++ b/core/templates/pages/terms-page/terms-page-root.component.spec.ts @@ -16,15 +16,15 @@ * @fileoverview Unit tests for the terms page root component. */ -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { TermsPageRootComponent } from './terms-page-root.component'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {TermsPageRootComponent} from './terms-page-root.component'; class MockTranslateService { onLangChange: EventEmitter = new EventEmitter(); @@ -41,18 +41,15 @@ describe('Terms Page Root', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - TermsPageRootComponent, - MockTranslatePipe - ], + declarations: [TermsPageRootComponent, MockTranslatePipe], providers: [ PageHeadService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -63,10 +60,9 @@ describe('Terms Page Root', () => { translateService = TestBed.inject(TranslateService); }); - it('should successfully instantiate the component', - () => { - expect(component).toBeDefined(); - }); + it('should successfully instantiate the component', () => { + expect(component).toBeDefined(); + }); it('should initialize and subscribe to onLangChange', () => { spyOn(translateService.onLangChange, 'subscribe'); @@ -92,10 +88,12 @@ describe('Terms Page Root', () => { component.setPageTitleAndMetaTags(); expect(translateService.instant).toHaveBeenCalledWith( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.TERMS.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.TERMS.TITLE + ); expect(pageHeadService.updateTitleAndMetaTags).toHaveBeenCalledWith( AppConstants.PAGES_REGISTERED_WITH_FRONTEND.TERMS.TITLE, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.TERMS.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.TERMS.META + ); }); it('should unsubscribe on component destruction', () => { diff --git a/core/templates/pages/terms-page/terms-page-root.component.ts b/core/templates/pages/terms-page/terms-page-root.component.ts index 7610ecd942fb..49621fa709fe 100644 --- a/core/templates/pages/terms-page/terms-page-root.component.ts +++ b/core/templates/pages/terms-page/terms-page-root.component.ts @@ -16,16 +16,16 @@ * @fileoverview Root component for Terms Page. */ -import { Component, OnDestroy } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; +import {Component, OnDestroy} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; @Component({ selector: 'oppia-terms-page-root', - templateUrl: './terms-page-root.component.html' + templateUrl: './terms-page-root.component.html', }) export class TermsPageRootComponent implements OnDestroy { directiveSubscriptions = new Subscription(); @@ -36,10 +36,12 @@ export class TermsPageRootComponent implements OnDestroy { setPageTitleAndMetaTags(): void { let translatedTitle = this.translateService.instant( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.TERMS.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.TERMS.TITLE + ); this.pageHeadService.updateTitleAndMetaTags( translatedTitle, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.TERMS.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.TERMS.META + ); } ngOnInit(): void { diff --git a/core/templates/pages/terms-page/terms-page-routing.module.ts b/core/templates/pages/terms-page/terms-page-routing.module.ts index d8306a6625f7..670a6553525d 100644 --- a/core/templates/pages/terms-page/terms-page-routing.module.ts +++ b/core/templates/pages/terms-page/terms-page-routing.module.ts @@ -16,24 +16,19 @@ * @fileoverview Routing module for terms page. */ -import { NgModule } from '@angular/core'; -import { Route, RouterModule } from '@angular/router'; -import { TermsPageRootComponent } from './terms-page-root.component'; +import {NgModule} from '@angular/core'; +import {Route, RouterModule} from '@angular/router'; +import {TermsPageRootComponent} from './terms-page-root.component'; const routes: Route[] = [ { path: '', - component: TermsPageRootComponent - } + component: TermsPageRootComponent, + }, ]; @NgModule({ - imports: [ - RouterModule.forChild(routes) - ], - exports: [ - RouterModule - ] + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], }) - export class TermsPageRoutingModule {} diff --git a/core/templates/pages/terms-page/terms-page.component.spec.ts b/core/templates/pages/terms-page/terms-page.component.spec.ts index 7885929c9a24..cbf397674d59 100644 --- a/core/templates/pages/terms-page/terms-page.component.spec.ts +++ b/core/templates/pages/terms-page/terms-page.component.spec.ts @@ -16,19 +16,19 @@ * @fileoverview Unit tests for terms page component. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; -import { TermsPageComponent } from './terms-page.component'; +import {TermsPageComponent} from './terms-page.component'; -describe('Terms page', function() { +describe('Terms page', function () { let component: TermsPageComponent; let fixture: ComponentFixture; beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [TermsPageComponent], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); diff --git a/core/templates/pages/terms-page/terms-page.component.ts b/core/templates/pages/terms-page/terms-page.component.ts index 904fa35d2ac1..2285655010b5 100644 --- a/core/templates/pages/terms-page/terms-page.component.ts +++ b/core/templates/pages/terms-page/terms-page.component.ts @@ -16,23 +16,23 @@ * @fileoverview Component for the 'terms' page. */ -import { Component } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { AppConstants } from 'app.constants'; +import {Component} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {AppConstants} from 'app.constants'; @Component({ selector: 'terms-page', templateUrl: './terms-page.component.html', - styleUrls: [] + styleUrls: [], }) export class TermsPageComponent { - PAGES_REGISTERED_WITH_FRONTEND = ( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND); + PAGES_REGISTERED_WITH_FRONTEND = AppConstants.PAGES_REGISTERED_WITH_FRONTEND; scrollTo(el: HTMLElement): void { el.scrollIntoView({behavior: 'smooth'}); } } -angular.module('oppia').directive( - 'termsPage', downgradeComponent({component: TermsPageComponent})); +angular + .module('oppia') + .directive('termsPage', downgradeComponent({component: TermsPageComponent})); diff --git a/core/templates/pages/terms-page/terms-page.module.ts b/core/templates/pages/terms-page/terms-page.module.ts index 2345a78f2270..6635f0f9379c 100644 --- a/core/templates/pages/terms-page/terms-page.module.ts +++ b/core/templates/pages/terms-page/terms-page.module.ts @@ -16,14 +16,13 @@ * @fileoverview Module for the terms page. */ -import { NgModule } from '@angular/core'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { TermsPageComponent } from - 'pages/terms-page/terms-page.component'; -import { TermsPageRootComponent } from './terms-page-root.component'; -import { CommonModule } from '@angular/common'; -import { TermsPageRoutingModule } from './terms-page-routing.module'; -import { SmartRouterModule } from 'hybrid-router-module-provider'; +import {NgModule} from '@angular/core'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {TermsPageComponent} from 'pages/terms-page/terms-page.component'; +import {TermsPageRootComponent} from './terms-page-root.component'; +import {CommonModule} from '@angular/common'; +import {TermsPageRoutingModule} from './terms-page-routing.module'; +import {SmartRouterModule} from 'hybrid-router-module-provider'; @NgModule({ imports: [ @@ -32,15 +31,9 @@ import { SmartRouterModule } from 'hybrid-router-module-provider'; // TODO(#13443): Remove smart router module provider once all pages are // migrated to angular router. SmartRouterModule, - TermsPageRoutingModule + TermsPageRoutingModule, ], - declarations: [ - TermsPageComponent, - TermsPageRootComponent - ], - entryComponents: [ - TermsPageComponent, - TermsPageRootComponent - ] + declarations: [TermsPageComponent, TermsPageRootComponent], + entryComponents: [TermsPageComponent, TermsPageRootComponent], }) export class TermsPageModule {} diff --git a/core/templates/pages/thanks-page/thanks-page-root.component.spec.ts b/core/templates/pages/thanks-page/thanks-page-root.component.spec.ts index 97988d7fe429..4b4bf3465919 100644 --- a/core/templates/pages/thanks-page/thanks-page-root.component.spec.ts +++ b/core/templates/pages/thanks-page/thanks-page-root.component.spec.ts @@ -16,15 +16,15 @@ * @fileoverview Unit tests for the thanks page root component. */ -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { ThanksPageRootComponent } from './thanks-page-root.component'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {ThanksPageRootComponent} from './thanks-page-root.component'; class MockTranslateService { onLangChange: EventEmitter = new EventEmitter(); @@ -41,18 +41,15 @@ describe('Thanks Page Root', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - ThanksPageRootComponent, - MockTranslatePipe - ], + declarations: [ThanksPageRootComponent, MockTranslatePipe], providers: [ PageHeadService, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -63,10 +60,9 @@ describe('Thanks Page Root', () => { translateService = TestBed.inject(TranslateService); }); - it('should successfully instantiate the component', - () => { - expect(component).toBeDefined(); - }); + it('should successfully instantiate the component', () => { + expect(component).toBeDefined(); + }); it('should initialize and subscribe to onLangChange', () => { spyOn(translateService.onLangChange, 'subscribe'); @@ -92,10 +88,12 @@ describe('Thanks Page Root', () => { component.setPageTitleAndMetaTags(); expect(translateService.instant).toHaveBeenCalledWith( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.THANKS.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.THANKS.TITLE + ); expect(pageHeadService.updateTitleAndMetaTags).toHaveBeenCalledWith( AppConstants.PAGES_REGISTERED_WITH_FRONTEND.THANKS.TITLE, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.THANKS.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.THANKS.META + ); }); it('should unsubscribe on component destruction', () => { diff --git a/core/templates/pages/thanks-page/thanks-page-root.component.ts b/core/templates/pages/thanks-page/thanks-page-root.component.ts index 9de651bc7733..9168c46fb104 100644 --- a/core/templates/pages/thanks-page/thanks-page-root.component.ts +++ b/core/templates/pages/thanks-page/thanks-page-root.component.ts @@ -16,16 +16,16 @@ * @fileoverview Root component for Thanks Page. */ -import { Component, OnDestroy } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; +import {Component, OnDestroy} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; @Component({ selector: 'oppia-thanks-page-root', - templateUrl: './thanks-page-root.component.html' + templateUrl: './thanks-page-root.component.html', }) export class ThanksPageRootComponent implements OnDestroy { directiveSubscriptions = new Subscription(); @@ -36,10 +36,12 @@ export class ThanksPageRootComponent implements OnDestroy { setPageTitleAndMetaTags(): void { let translatedTitle = this.translateService.instant( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.THANKS.TITLE); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.THANKS.TITLE + ); this.pageHeadService.updateTitleAndMetaTags( translatedTitle, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.THANKS.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.THANKS.META + ); } ngOnInit(): void { diff --git a/core/templates/pages/thanks-page/thanks-page-routing.module.ts b/core/templates/pages/thanks-page/thanks-page-routing.module.ts index ab64199079dc..11e0a50f6158 100644 --- a/core/templates/pages/thanks-page/thanks-page-routing.module.ts +++ b/core/templates/pages/thanks-page/thanks-page-routing.module.ts @@ -16,24 +16,19 @@ * @fileoverview Routing module for thanks page. */ -import { NgModule } from '@angular/core'; -import { Route, RouterModule } from '@angular/router'; -import { ThanksPageRootComponent } from './thanks-page-root.component'; +import {NgModule} from '@angular/core'; +import {Route, RouterModule} from '@angular/router'; +import {ThanksPageRootComponent} from './thanks-page-root.component'; const routes: Route[] = [ { path: '', - component: ThanksPageRootComponent - } + component: ThanksPageRootComponent, + }, ]; @NgModule({ - imports: [ - RouterModule.forChild(routes) - ], - exports: [ - RouterModule - ] + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], }) - export class ThanksPageRoutingModule {} diff --git a/core/templates/pages/thanks-page/thanks-page.component.spec.ts b/core/templates/pages/thanks-page/thanks-page.component.spec.ts index 34e1e7c77501..bd0af3d2bc22 100644 --- a/core/templates/pages/thanks-page/thanks-page.component.spec.ts +++ b/core/templates/pages/thanks-page/thanks-page.component.spec.ts @@ -16,19 +16,19 @@ * @fileoverview Unit tests for thanks page component. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; -import { ThanksPageComponent } from './thanks-page.component'; +import {ThanksPageComponent} from './thanks-page.component'; -describe('Thanks page', function() { +describe('Thanks page', function () { let component: ThanksPageComponent; let fixture: ComponentFixture; beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ThanksPageComponent], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -39,7 +39,8 @@ describe('Thanks page', function() { }); it('should set thanks image url when getStaticImageUrl is called', () => { - expect(component.getStaticImageUrl('/general/donate.png')) - .toBe('/assets/images/general/donate.png'); + expect(component.getStaticImageUrl('/general/donate.png')).toBe( + '/assets/images/general/donate.png' + ); }); }); diff --git a/core/templates/pages/thanks-page/thanks-page.component.ts b/core/templates/pages/thanks-page/thanks-page.component.ts index 39c01f73f343..5f15fcff0a3d 100644 --- a/core/templates/pages/thanks-page/thanks-page.component.ts +++ b/core/templates/pages/thanks-page/thanks-page.component.ts @@ -16,16 +16,15 @@ * @fileoverview Component for the 'thanks' page. */ -import { Component } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; @Component({ selector: 'thanks-page', templateUrl: './thanks-page.component.html', - styleUrls: [] + styleUrls: [], }) export class ThanksPageComponent { constructor(private urlInterpolationService: UrlInterpolationService) {} @@ -34,5 +33,9 @@ export class ThanksPageComponent { return this.urlInterpolationService.getStaticImageUrl(imagePath); } } -angular.module('oppia').directive( - 'thanksPage', downgradeComponent({component: ThanksPageComponent})); +angular + .module('oppia') + .directive( + 'thanksPage', + downgradeComponent({component: ThanksPageComponent}) + ); diff --git a/core/templates/pages/thanks-page/thanks-page.module.ts b/core/templates/pages/thanks-page/thanks-page.module.ts index 7cac1d08861a..158cf11779ae 100644 --- a/core/templates/pages/thanks-page/thanks-page.module.ts +++ b/core/templates/pages/thanks-page/thanks-page.module.ts @@ -16,27 +16,17 @@ * @fileoverview Module for the thanks page. */ -import { NgModule } from '@angular/core'; +import {NgModule} from '@angular/core'; -import { ThanksPageComponent } from './thanks-page.component'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { ThanksPageRootComponent } from './thanks-page-root.component'; -import { CommonModule } from '@angular/common'; -import { ThanksPageRoutingModule } from './thanks-page-routing.module'; +import {ThanksPageComponent} from './thanks-page.component'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {ThanksPageRootComponent} from './thanks-page-root.component'; +import {CommonModule} from '@angular/common'; +import {ThanksPageRoutingModule} from './thanks-page-routing.module'; @NgModule({ - imports: [ - CommonModule, - SharedComponentsModule, - ThanksPageRoutingModule - ], - declarations: [ - ThanksPageComponent, - ThanksPageRootComponent - ], - entryComponents: [ - ThanksPageComponent, - ThanksPageRootComponent - ] + imports: [CommonModule, SharedComponentsModule, ThanksPageRoutingModule], + declarations: [ThanksPageComponent, ThanksPageRootComponent], + entryComponents: [ThanksPageComponent, ThanksPageRootComponent], }) export class ThanksPageModule {} diff --git a/core/templates/pages/topic-editor-page/editor-tab/topic-editor-stories-list.component.spec.ts b/core/templates/pages/topic-editor-page/editor-tab/topic-editor-stories-list.component.spec.ts index 112edb3b3726..2bc2ec5f3716 100644 --- a/core/templates/pages/topic-editor-page/editor-tab/topic-editor-stories-list.component.spec.ts +++ b/core/templates/pages/topic-editor-page/editor-tab/topic-editor-stories-list.component.spec.ts @@ -16,16 +16,22 @@ * @fileoverview Unit tests for the stories list viewer. */ -import { CdkDragDrop } from '@angular/cdk/drag-drop'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { UndoRedoService } from 'domain/editor/undo_redo/undo-redo.service'; -import { StorySummary } from 'domain/story/story-summary.model'; -import { TopicUpdateService } from 'domain/topic/topic-update.service'; -import { TopicEditorStoriesListComponent } from './topic-editor-stories-list.component'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { PlatformFeatureService } from '../../../services/platform-feature.service'; +import {CdkDragDrop} from '@angular/cdk/drag-drop'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import { + async, + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {UndoRedoService} from 'domain/editor/undo_redo/undo-redo.service'; +import {StorySummary} from 'domain/story/story-summary.model'; +import {TopicUpdateService} from 'domain/topic/topic-update.service'; +import {TopicEditorStoriesListComponent} from './topic-editor-stories-list.component'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {PlatformFeatureService} from '../../../services/platform-feature.service'; class MockNgbModalRef { componentInstance: { @@ -36,8 +42,8 @@ class MockNgbModalRef { class MockPlatformFeatureService { status = { SerialChapterLaunchCurriculumAdminView: { - isEnabled: false - } + isEnabled: false, + }, }; } @@ -53,18 +59,14 @@ describe('topicEditorStoriesList', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ - TopicEditorStoriesListComponent - ], - imports: [ - HttpClientTestingModule, - ], + declarations: [TopicEditorStoriesListComponent], + imports: [HttpClientTestingModule], providers: [ { provide: PlatformFeatureService, - useValue: mockPlatformFeatureService + useValue: mockPlatformFeatureService, }, - ] + ], }); })); @@ -76,46 +78,49 @@ describe('topicEditorStoriesList', () => { undoRedoService = TestBed.inject(UndoRedoService); ngbModal = TestBed.inject(NgbModal); - storySummaries = [StorySummary.createFromBackendDict({ - id: 'storyId', - title: 'Story Title', - node_titles: ['node1', 'node2', 'node3'], - thumbnail_filename: 'thumbnail.jpg', - thumbnail_bg_color: '#FF9933', - description: 'This is the story description', - story_is_published: true, - completed_node_titles: ['node1'], - url_fragment: 'story1', - all_node_dicts: [], - total_chapters_count: 3, - published_chapters_count: 2, - overdue_chapters_count: 0, - upcoming_chapters_count: 0 - }), StorySummary.createFromBackendDict({ - id: 'storyId2', - title: 'Story Title2', - node_titles: ['node1', 'node2', 'node3'], - thumbnail_filename: 'thumbnail.jpg', - thumbnail_bg_color: '#FF9933', - description: 'This is the story description', - story_is_published: true, - completed_node_titles: ['node1'], - url_fragment: 'story1', - all_node_dicts: [], - total_chapters_count: 3, - published_chapters_count: 3, - overdue_chapters_count: 3, - upcoming_chapters_count: 0 - })]; + storySummaries = [ + StorySummary.createFromBackendDict({ + id: 'storyId', + title: 'Story Title', + node_titles: ['node1', 'node2', 'node3'], + thumbnail_filename: 'thumbnail.jpg', + thumbnail_bg_color: '#FF9933', + description: 'This is the story description', + story_is_published: true, + completed_node_titles: ['node1'], + url_fragment: 'story1', + all_node_dicts: [], + total_chapters_count: 3, + published_chapters_count: 2, + overdue_chapters_count: 0, + upcoming_chapters_count: 0, + }), + StorySummary.createFromBackendDict({ + id: 'storyId2', + title: 'Story Title2', + node_titles: ['node1', 'node2', 'node3'], + thumbnail_filename: 'thumbnail.jpg', + thumbnail_bg_color: '#FF9933', + description: 'This is the story description', + story_is_published: true, + completed_node_titles: ['node1'], + url_fragment: 'story1', + all_node_dicts: [], + total_chapters_count: 3, + published_chapters_count: 3, + overdue_chapters_count: 3, + upcoming_chapters_count: 0, + }), + ]; }); it('should get status of Serial Chapter Launch Feature flag', () => { - mockPlatformFeatureService. - status.SerialChapterLaunchCurriculumAdminView.isEnabled = false; + mockPlatformFeatureService.status.SerialChapterLaunchCurriculumAdminView.isEnabled = + false; expect(component.isSerialChapterLaunchFeatureEnabled()).toEqual(false); - mockPlatformFeatureService. - status.SerialChapterLaunchCurriculumAdminView.isEnabled = true; + mockPlatformFeatureService.status.SerialChapterLaunchCurriculumAdminView.isEnabled = + true; expect(component.isSerialChapterLaunchFeatureEnabled()).toEqual(true); }); @@ -126,32 +131,37 @@ describe('topicEditorStoriesList', () => { component.topic = null; component.drop({ previousIndex: 1, - currentIndex: 2 + currentIndex: 2, } as CdkDragDrop); expect(topicUpdateService.rearrangeCanonicalStory).toHaveBeenCalled(); }); it('should initialise component when list of stories is displayed', () => { - mockPlatformFeatureService. - status.SerialChapterLaunchCurriculumAdminView.isEnabled = false; + mockPlatformFeatureService.status.SerialChapterLaunchCurriculumAdminView.isEnabled = + false; component.ngOnInit(); expect(component.STORY_TABLE_COLUMN_HEADINGS).toEqual([ - 'title', 'node_count', 'publication_status']); + 'title', + 'node_count', + 'publication_status', + ]); - mockPlatformFeatureService. - status.SerialChapterLaunchCurriculumAdminView.isEnabled = true; + mockPlatformFeatureService.status.SerialChapterLaunchCurriculumAdminView.isEnabled = + true; component.ngOnInit(); expect(component.STORY_TABLE_COLUMN_HEADINGS).toEqual([ - 'title', 'publication_status', 'node_count', 'notifications']); + 'title', + 'publication_status', + 'node_count', + 'notifications', + ]); }); it('should delete story when user deletes story', fakeAsync(() => { - spyOn(ngbModal, 'open').and.returnValue( - { - result: Promise.resolve() - } as NgbModalRef - ); + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.resolve(), + } as NgbModalRef); spyOn(topicUpdateService, 'removeCanonicalStory'); component.storySummaries = storySummaries; @@ -167,11 +177,9 @@ describe('topicEditorStoriesList', () => { })); it('should close modal when user click cancel button', () => { - spyOn(ngbModal, 'open').and.returnValue( - { - result: Promise.reject() - } as NgbModalRef - ); + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.reject(), + } as NgbModalRef); component.deleteCanonicalStory('storyId'); expect(ngbModal.open).toHaveBeenCalled(); @@ -186,47 +194,54 @@ describe('topicEditorStoriesList', () => { expect(windowRef.nativeWindow.open).toHaveBeenCalled(); }); - it('should open save changes modal when user tries to open story editor' + - ' without saving changes', () => { - const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ - componentInstance: MockNgbModalRef, - result: Promise.resolve() - }) as NgbModalRef; - }); - spyOn(undoRedoService, 'getChangeCount').and.returnValue(1); - - component.openStoryEditor('storyId'); - - expect(modalSpy).toHaveBeenCalled(); - }); - - it('should close save changes modal when closes the saves changes' + - ' modal', () => { - const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ - componentInstance: MockNgbModalRef, - result: Promise.reject() - }) as NgbModalRef; - }); - spyOn(undoRedoService, 'getChangeCount').and.returnValue(1); - - component.openStoryEditor('storyId'); - - expect(modalSpy).toHaveBeenCalled(); - }); + it( + 'should open save changes modal when user tries to open story editor' + + ' without saving changes', + () => { + const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + componentInstance: MockNgbModalRef, + result: Promise.resolve(), + } as NgbModalRef; + }); + spyOn(undoRedoService, 'getChangeCount').and.returnValue(1); + + component.openStoryEditor('storyId'); + + expect(modalSpy).toHaveBeenCalled(); + } + ); + + it( + 'should close save changes modal when closes the saves changes' + ' modal', + () => { + const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + componentInstance: MockNgbModalRef, + result: Promise.reject(), + } as NgbModalRef; + }); + spyOn(undoRedoService, 'getChangeCount').and.returnValue(1); + + component.openStoryEditor('storyId'); + + expect(modalSpy).toHaveBeenCalled(); + } + ); it('should return if some chapters are not published', () => { expect(component.areChaptersAwaitingPublication(storySummaries[0])).toBe( - true); + true + ); expect(component.areChaptersAwaitingPublication(storySummaries[1])).toBe( - false); + false + ); }); it('should return if chapter notifications are empty', () => { - expect(component.isChapterNotificationsEmpty(storySummaries[0])).toBe( - true); + expect(component.isChapterNotificationsEmpty(storySummaries[0])).toBe(true); expect(component.isChapterNotificationsEmpty(storySummaries[1])).toBe( - false); + false + ); }); }); diff --git a/core/templates/pages/topic-editor-page/editor-tab/topic-editor-stories-list.component.ts b/core/templates/pages/topic-editor-page/editor-tab/topic-editor-stories-list.component.ts index d5fa18212154..b005e4fa6455 100644 --- a/core/templates/pages/topic-editor-page/editor-tab/topic-editor-stories-list.component.ts +++ b/core/templates/pages/topic-editor-page/editor-tab/topic-editor-stories-list.component.ts @@ -16,25 +16,25 @@ * @fileoverview Component for the stories list viewer. */ -import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; -import { Component, Input, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { SavePendingChangesModalComponent } from 'components/save-pending-changes/save-pending-changes-modal.component'; -import { UndoRedoService } from 'domain/editor/undo_redo/undo-redo.service'; -import { StorySummary } from 'domain/story/story-summary.model'; -import { TopicUpdateService } from 'domain/topic/topic-update.service'; -import { Topic } from 'domain/topic/topic-object.model'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { DeleteStoryModalComponent } from '../modal-templates/delete-story-modal.component'; -import { TopicRights } from 'domain/topic/topic-rights.model'; -import { TopicEditorStateService } from '../services/topic-editor-state.service'; -import { PlatformFeatureService } from 'services/platform-feature.service'; +import {CdkDragDrop, moveItemInArray} from '@angular/cdk/drag-drop'; +import {Component, Input, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {SavePendingChangesModalComponent} from 'components/save-pending-changes/save-pending-changes-modal.component'; +import {UndoRedoService} from 'domain/editor/undo_redo/undo-redo.service'; +import {StorySummary} from 'domain/story/story-summary.model'; +import {TopicUpdateService} from 'domain/topic/topic-update.service'; +import {Topic} from 'domain/topic/topic-object.model'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {DeleteStoryModalComponent} from '../modal-templates/delete-story-modal.component'; +import {TopicRights} from 'domain/topic/topic-rights.model'; +import {TopicEditorStateService} from '../services/topic-editor-state.service'; +import {PlatformFeatureService} from 'services/platform-feature.service'; @Component({ selector: 'oppia-topic-editor-stories-list', - templateUrl: './topic-editor-stories-list.component.html' + templateUrl: './topic-editor-stories-list.component.html', }) export class TopicEditorStoriesListComponent implements OnInit { @Input() storySummaries: StorySummary[]; @@ -58,84 +58,109 @@ export class TopicEditorStoriesListComponent implements OnInit { drop(event: CdkDragDrop): void { moveItemInArray( this.storySummaries, - event.previousIndex, event.currentIndex); + event.previousIndex, + event.currentIndex + ); this.topicUpdateService.rearrangeCanonicalStory( - this.topic, event.previousIndex, event.currentIndex); + this.topic, + event.previousIndex, + event.currentIndex + ); } openStoryEditor(storyId: string): void { if (this.undoRedoService.getChangeCount() > 0) { - const modalRef = this.ngbModal.open( - SavePendingChangesModalComponent, { - backdrop: true - }); - - modalRef.componentInstance.body = ( - 'Please save all pending changes ' + - 'before exiting the topic editor.'); - - modalRef.result.then(() => {}, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. + const modalRef = this.ngbModal.open(SavePendingChangesModalComponent, { + backdrop: true, }); + + modalRef.componentInstance.body = + 'Please save all pending changes ' + 'before exiting the topic editor.'; + + modalRef.result.then( + () => {}, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } else { this.windowRef.nativeWindow.open( this.urlInterpolationService.interpolateUrl( - this.STORY_EDITOR_URL_TEMPLATE, { - story_id: storyId - }), '_self'); + this.STORY_EDITOR_URL_TEMPLATE, + { + story_id: storyId, + } + ), + '_self' + ); } } deleteCanonicalStory(storyId: string): void { - this.ngbModal.open(DeleteStoryModalComponent, { - backdrop: true - }).result.then(() => { - this.topicUpdateService.removeCanonicalStory( - this.topic, storyId); - for (let i = 0; i < this.storySummaries.length; i++) { - if (this.storySummaries[i].getId() === storyId) { - this.storySummaries.splice(i, 1); + this.ngbModal + .open(DeleteStoryModalComponent, { + backdrop: true, + }) + .result.then( + () => { + this.topicUpdateService.removeCanonicalStory(this.topic, storyId); + for (let i = 0; i < this.storySummaries.length; i++) { + if (this.storySummaries[i].getId() === storyId) { + this.storySummaries.splice(i, 1); + } + } + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. } - } - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + ); } isSerialChapterLaunchFeatureEnabled(): boolean { - return this.platformFeatureService.status. - SerialChapterLaunchCurriculumAdminView.isEnabled; + return this.platformFeatureService.status + .SerialChapterLaunchCurriculumAdminView.isEnabled; } areChaptersAwaitingPublication(summary: StorySummary): boolean { return ( - summary.getTotalChaptersCount() !== summary.getPublishedChaptersCount()); + summary.getTotalChaptersCount() !== summary.getPublishedChaptersCount() + ); } isChapterNotificationsEmpty(summary: StorySummary): boolean { return ( summary.getUpcomingChaptersCount() === 0 && - summary.getOverdueChaptersCount() === 0); + summary.getOverdueChaptersCount() === 0 + ); } ngOnInit(): void { if (this.isSerialChapterLaunchFeatureEnabled()) { this.STORY_TABLE_COLUMN_HEADINGS = [ - 'title', 'publication_status', 'node_count', 'notifications']; + 'title', + 'publication_status', + 'node_count', + 'notifications', + ]; } else { this.STORY_TABLE_COLUMN_HEADINGS = [ - 'title', 'node_count', 'publication_status']; + 'title', + 'node_count', + 'publication_status', + ]; } this.topicRights = this.topicEditorStateService.getTopicRights(); } } -angular.module('oppia').directive('oppiaTopicEditorStoriesList', +angular.module('oppia').directive( + 'oppiaTopicEditorStoriesList', downgradeComponent({ - component: TopicEditorStoriesListComponent - }) as angular.IDirectiveFactory); + component: TopicEditorStoriesListComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/topic-editor-page/editor-tab/topic-editor-tab.directive.spec.ts b/core/templates/pages/topic-editor-page/editor-tab/topic-editor-tab.directive.spec.ts index 9fb09d1994d4..3c946ec025f6 100644 --- a/core/templates/pages/topic-editor-page/editor-tab/topic-editor-tab.directive.spec.ts +++ b/core/templates/pages/topic-editor-page/editor-tab/topic-editor-tab.directive.spec.ts @@ -12,42 +12,47 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Unit tests for the topic editor tab directive. */ -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { ShortSkillSummary } from 'domain/skill/short-skill-summary.model'; -import { Subtopic } from 'domain/topic/subtopic.model'; -import { StoryReference } from 'domain/topic/story-reference-object.model'; -import { Topic } from 'domain/topic/topic-object.model'; -import { ImageUploadHelperService } from 'services/image-upload-helper.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { TopicEditorStateService } from 'pages/topic-editor-page/services/topic-editor-state.service'; -import { SkillCreationService } from 'components/entity-creation-services/skill-creation.service'; -import { TopicUpdateService } from 'domain/topic/topic-update.service'; -import { StoryCreationBackendApiService } from 'components/entity-creation-services/story-creation-backend-api.service'; -import { UndoRedoService } from 'domain/editor/undo_redo/undo-redo.service'; -import { EntityCreationService } from 'pages/topic-editor-page/services/entity-creation.service'; -import { TopicEditorRoutingService } from '../services/topic-editor-routing.service'; -import { QuestionBackendApiService } from 'domain/question/question-backend-api.service'; -import { TopicEditorTabComponent } from './topic-editor-tab.directive'; -import { ContextService } from 'services/context.service'; -import { RearrangeSkillsInSubtopicsModalComponent } from '../modal-templates/rearrange-skills-in-subtopics-modal.component'; -import { ChangeSubtopicAssignmentModalComponent } from '../modal-templates/change-subtopic-assignment-modal.component'; -import { SavePendingChangesModalComponent } from 'components/save-pending-changes/save-pending-changes-modal.component'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { TopicsAndSkillsDashboardBackendApiService } from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; -import { CdkDragDrop } from '@angular/cdk/drag-drop'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {ShortSkillSummary} from 'domain/skill/short-skill-summary.model'; +import {Subtopic} from 'domain/topic/subtopic.model'; +import {StoryReference} from 'domain/topic/story-reference-object.model'; +import {Topic} from 'domain/topic/topic-object.model'; +import {ImageUploadHelperService} from 'services/image-upload-helper.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {TopicEditorStateService} from 'pages/topic-editor-page/services/topic-editor-state.service'; +import {SkillCreationService} from 'components/entity-creation-services/skill-creation.service'; +import {TopicUpdateService} from 'domain/topic/topic-update.service'; +import {StoryCreationBackendApiService} from 'components/entity-creation-services/story-creation-backend-api.service'; +import {UndoRedoService} from 'domain/editor/undo_redo/undo-redo.service'; +import {EntityCreationService} from 'pages/topic-editor-page/services/entity-creation.service'; +import {TopicEditorRoutingService} from '../services/topic-editor-routing.service'; +import {QuestionBackendApiService} from 'domain/question/question-backend-api.service'; +import {TopicEditorTabComponent} from './topic-editor-tab.directive'; +import {ContextService} from 'services/context.service'; +import {RearrangeSkillsInSubtopicsModalComponent} from '../modal-templates/rearrange-skills-in-subtopics-modal.component'; +import {ChangeSubtopicAssignmentModalComponent} from '../modal-templates/change-subtopic-assignment-modal.component'; +import {SavePendingChangesModalComponent} from 'components/save-pending-changes/save-pending-changes-modal.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {TopicsAndSkillsDashboardBackendApiService} from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; +import {CdkDragDrop} from '@angular/cdk/drag-drop'; class MockNgbModal { open() { return { - result: Promise.resolve('1') + result: Promise.resolve('1'), }; } } @@ -67,10 +72,8 @@ class MockContextService { } class MockImageUploadHelperService { - getTrustedResourceUrlForThumbnailFilename( - filename, entityType, entityId) { - return ( - entityType + '/' + entityId + '/' + filename); + getTrustedResourceUrlForThumbnailFilename(filename, entityType, entityId) { + return entityType + '/' + entityId + '/' + filename; } } @@ -95,24 +98,22 @@ describe('Topic editor tab directive', () => { let topicInitializedEventEmitter = new EventEmitter(); let topicReinitializedEventEmitter = new EventEmitter(); let MockWindowDimensionsService = { - isWindowNarrow: () => false + isWindowNarrow: () => false, }; let MockTopicsAndSkillsDashboardBackendApiService = { get onTopicsAndSkillsDashboardReinitialized() { return mockTasdReinitializedEventEmitter; - } + }, }; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - ], + imports: [HttpClientTestingModule], declarations: [ TopicEditorTabComponent, RearrangeSkillsInSubtopicsModalComponent, ChangeSubtopicAssignmentModalComponent, - SavePendingChangesModalComponent + SavePendingChangesModalComponent, ], providers: [ UrlInterpolationService, @@ -134,18 +135,18 @@ describe('Topic editor tab directive', () => { }, { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, { provide: ContextService, - useClass: MockContextService + useClass: MockContextService, }, { provide: ImageUploadHelperService, - useClass: MockImageUploadHelperService + useClass: MockImageUploadHelperService, }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -166,22 +167,37 @@ describe('Topic editor tab directive', () => { spyOnProperty(topicEditorStateService, 'onTopicInitialized').and.callFake( () => { return topicInitializedEventEmitter; - }); - spyOnProperty( - topicEditorStateService, 'onTopicReinitialized').and.callFake( + } + ); + spyOnProperty(topicEditorStateService, 'onTopicReinitialized').and.callFake( () => { return topicReinitializedEventEmitter; - }); - + } + ); let subtopic = Subtopic.createFromTitle(1, 'subtopic1'); topic = new Topic( - 'id', 'Topic name loading', 'Abbrev. name loading', - 'Url Fragment loading', 'Topic description loading', 'en', - [], [], [], 1, 1, [], 'str', '', {}, false, '', '', [] + 'id', + 'Topic name loading', + 'Abbrev. name loading', + 'Url Fragment loading', + 'Topic description loading', + 'en', + [], + [], + [], + 1, + 1, + [], + 'str', + '', + {}, + false, + '', + '', + [] ); - skillSummary = ShortSkillSummary.create( - 'skill_1', 'Description 1'); + skillSummary = ShortSkillSummary.create('skill_1', 'Description 1'); subtopic._skillSummaries = [skillSummary]; topic._uncategorizedSkillSummaries = [skillSummary]; topic._subtopics = [subtopic]; @@ -194,12 +210,12 @@ describe('Topic editor tab directive', () => { spyOn(topicEditorStateService, 'getTopic').and.returnValue(topic); spyOnProperty( - topicEditorStateService, 'onStorySummariesInitialized').and.returnValue( - mockStorySummariesInitializedEventEmitter); - spyOn(urlInterpolationService, 'getStaticImageUrl') - .and.callFake((value) => { - return ('/assets/images' + value); - }); + topicEditorStateService, + 'onStorySummariesInitialized' + ).and.returnValue(mockStorySummariesInitializedEventEmitter); + spyOn(urlInterpolationService, 'getStaticImageUrl').and.callFake(value => { + return '/assets/images' + value; + }); component.ngOnInit(); fixture.detectChanges(); @@ -217,7 +233,7 @@ describe('Topic editor tab directive', () => { component.topic = null; component.drop({ previousIndex: 1, - currentIndex: 2 + currentIndex: 2, } as CdkDragDrop); expect(topicUpdateService.rearrangeSubtopic).toHaveBeenCalled(); @@ -258,33 +274,27 @@ describe('Topic editor tab directive', () => { subtopics: null; }; } - let uibModalSpy = spyOn(ngbModal, 'open').and.returnValue( - { - componentInstance: MockNgbModalRef, - result: Promise.resolve(1) - } as NgbModalRef - ); + let uibModalSpy = spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: MockNgbModalRef, + result: Promise.resolve(1), + } as NgbModalRef); component.reassignSkillsInSubtopics(); tick(); expect(uibModalSpy).toHaveBeenCalled(); })); - it('should call the TopicUpdateService if skill is removed from subtopic', - () => { - let removeSkillSpy = ( - spyOn(topicUpdateService, 'removeSkillFromSubtopic')); - component.removeSkillFromSubtopic(0, null); - expect(removeSkillSpy).toHaveBeenCalled(); - }); + it('should call the TopicUpdateService if skill is removed from subtopic', () => { + let removeSkillSpy = spyOn(topicUpdateService, 'removeSkillFromSubtopic'); + component.removeSkillFromSubtopic(0, null); + expect(removeSkillSpy).toHaveBeenCalled(); + }); - it('should call the TopicUpdateService if skill is removed from topic', - () => { - let removeSkillSpy = ( - spyOn(topicUpdateService, 'removeSkillFromSubtopic')); - component.removeSkillFromTopic(0, skillSummary); - expect(removeSkillSpy).toHaveBeenCalled(); - }); + it('should call the TopicUpdateService if skill is removed from topic', () => { + let removeSkillSpy = spyOn(topicUpdateService, 'removeSkillFromSubtopic'); + component.removeSkillFromTopic(0, skillSummary); + expect(removeSkillSpy).toHaveBeenCalled(); + }); it('should show subtopic edit options', () => { component.showSubtopicEditOptions(1); @@ -307,41 +317,41 @@ describe('Topic editor tab directive', () => { it('should get the classroom URL fragment', () => { expect(component.getClassroomUrlFragment()).toEqual('staging'); - spyOn( - topicEditorStateService, - 'getClassroomUrlFragment').and.returnValue('classroom-frag'); + spyOn(topicEditorStateService, 'getClassroomUrlFragment').and.returnValue( + 'classroom-frag' + ); expect(component.getClassroomUrlFragment()).toEqual('classroom-frag'); }); - it('should open save changes warning modal before creating skill', - () => { - class MockNgbModalRef { - componentInstance: { - body: 'xyz'; - }; - } - spyOn(undoRedoService, 'getChangeCount').and.returnValue(1); - const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ - componentInstance: MockNgbModalRef, - result: Promise.resolve() - }) as NgbModalRef; - }); - component.createSkill(); - expect(modalSpy).toHaveBeenCalled(); + it('should open save changes warning modal before creating skill', () => { + class MockNgbModalRef { + componentInstance: { + body: 'xyz'; + }; + } + spyOn(undoRedoService, 'getChangeCount').and.returnValue(1); + const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + componentInstance: MockNgbModalRef, + result: Promise.resolve(), + } as NgbModalRef; }); + component.createSkill(); + expect(modalSpy).toHaveBeenCalled(); + }); - it('should call TopicEditorStateService to load topic when ' + + it( + 'should call TopicEditorStateService to load topic when ' + 'topics and skills dashboard is reinitialized', - fakeAsync(() => { - let refreshTopicSpy = spyOn(topicEditorStateService, 'loadTopic'); + fakeAsync(() => { + let refreshTopicSpy = spyOn(topicEditorStateService, 'loadTopic'); - MockTopicsAndSkillsDashboardBackendApiService. - onTopicsAndSkillsDashboardReinitialized.emit(); - tick(); + MockTopicsAndSkillsDashboardBackendApiService.onTopicsAndSkillsDashboardReinitialized.emit(); + tick(); - expect(refreshTopicSpy).toHaveBeenCalled(); - })); + expect(refreshTopicSpy).toHaveBeenCalled(); + }) + ); it('should call EntityCreationService to create subtopic', () => { let skillSpy = spyOn(entityCreationService, 'createSubtopic'); @@ -358,21 +368,21 @@ describe('Topic editor tab directive', () => { it('should call the TopicUpdateService if name is updated', () => { let topicNameSpy = spyOn(topicUpdateService, 'setTopicName'); spyOn(topicEditorStateService, 'updateExistenceOfTopicName').and.callFake( - (newName, successCallback) => successCallback()); + (newName, successCallback) => successCallback() + ); component.updateTopicName('Different Name'); expect(topicNameSpy).toHaveBeenCalled(); }); - it('should not call updateExistenceOfTopicName if name is empty', - () => { - let topicNameSpy = spyOn(topicUpdateService, 'setTopicName'); - spyOn(topicEditorStateService, 'updateExistenceOfTopicName'); - component.updateTopicName(''); - expect(topicNameSpy).toHaveBeenCalled(); - expect( - topicEditorStateService.updateExistenceOfTopicName - ).not.toHaveBeenCalled(); - }); + it('should not call updateExistenceOfTopicName if name is empty', () => { + let topicNameSpy = spyOn(topicUpdateService, 'setTopicName'); + spyOn(topicEditorStateService, 'updateExistenceOfTopicName'); + component.updateTopicName(''); + expect(topicNameSpy).toHaveBeenCalled(); + expect( + topicEditorStateService.updateExistenceOfTopicName + ).not.toHaveBeenCalled(); + }); it('should not call the TopicUpdateService if name is same', () => { let topicNameSpy = spyOn(topicUpdateService, 'setTopicName'); @@ -380,150 +390,159 @@ describe('Topic editor tab directive', () => { expect(topicNameSpy).not.toHaveBeenCalled(); }); - it('should not call the TopicUpdateService if url fragment is same', - () => { - let topicUrlFragmentSpy = spyOn( - topicUpdateService, 'setTopicUrlFragment'); - component.updateTopicUrlFragment('topic-url-fragment'); - expect(topicUrlFragmentSpy).not.toHaveBeenCalled(); - }); - - it('should not call the getTopicWithUrlFragmentExists if url fragment' + - 'is not correct', () => { - let topicUrlFragmentSpy = spyOn( - topicUpdateService, 'setTopicUrlFragment'); - let topicUrlFragmentExists = spyOn( - topicEditorStateService, 'getTopicWithUrlFragmentExists'); - spyOn( - topicEditorStateService, - 'updateExistenceOfTopicUrlFragment').and.callFake( - (newUrlFragment, successCallback, errorCallback) => errorCallback()); - component.updateTopicUrlFragment('topic-url fragment'); - expect(topicUrlFragmentSpy).toHaveBeenCalled(); - expect(topicUrlFragmentExists).not.toHaveBeenCalled(); + it('should not call the TopicUpdateService if url fragment is same', () => { + let topicUrlFragmentSpy = spyOn(topicUpdateService, 'setTopicUrlFragment'); + component.updateTopicUrlFragment('topic-url-fragment'); + expect(topicUrlFragmentSpy).not.toHaveBeenCalled(); }); - it('should call the TopicUpdateService if url fragment is updated', + it( + 'should not call the getTopicWithUrlFragmentExists if url fragment' + + 'is not correct', () => { let topicUrlFragmentSpy = spyOn( - topicUpdateService, 'setTopicUrlFragment'); + topicUpdateService, + 'setTopicUrlFragment' + ); + let topicUrlFragmentExists = spyOn( + topicEditorStateService, + 'getTopicWithUrlFragmentExists' + ); spyOn( topicEditorStateService, - 'updateExistenceOfTopicUrlFragment').and.callFake( - (newUrlFragment, successCallback, errorCallback) => successCallback()); - component.updateTopicUrlFragment('topic'); + 'updateExistenceOfTopicUrlFragment' + ).and.callFake((newUrlFragment, successCallback, errorCallback) => + errorCallback() + ); + component.updateTopicUrlFragment('topic-url fragment'); expect(topicUrlFragmentSpy).toHaveBeenCalled(); - }); + expect(topicUrlFragmentExists).not.toHaveBeenCalled(); + } + ); - it('should not update topic url fragment existence for empty url fragment', - () => { - let topicUrlFragmentSpy = spyOn( - topicUpdateService, 'setTopicUrlFragment'); - spyOn(topicEditorStateService, 'updateExistenceOfTopicUrlFragment'); - component.updateTopicUrlFragment(''); - expect(topicUrlFragmentSpy).toHaveBeenCalled(); - expect( - topicEditorStateService.updateExistenceOfTopicUrlFragment - ).not.toHaveBeenCalled(); - }); + it('should call the TopicUpdateService if url fragment is updated', () => { + let topicUrlFragmentSpy = spyOn(topicUpdateService, 'setTopicUrlFragment'); + spyOn( + topicEditorStateService, + 'updateExistenceOfTopicUrlFragment' + ).and.callFake((newUrlFragment, successCallback, errorCallback) => + successCallback() + ); + component.updateTopicUrlFragment('topic'); + expect(topicUrlFragmentSpy).toHaveBeenCalled(); + }); + + it('should not update topic url fragment existence for empty url fragment', () => { + let topicUrlFragmentSpy = spyOn(topicUpdateService, 'setTopicUrlFragment'); + spyOn(topicEditorStateService, 'updateExistenceOfTopicUrlFragment'); + component.updateTopicUrlFragment(''); + expect(topicUrlFragmentSpy).toHaveBeenCalled(); + expect( + topicEditorStateService.updateExistenceOfTopicUrlFragment + ).not.toHaveBeenCalled(); + }); it('should call the TopicUpdateService if thumbnail is updated', () => { - let topicThumbnailSpy = ( - spyOn(topicUpdateService, 'setTopicThumbnailFilename')); + let topicThumbnailSpy = spyOn( + topicUpdateService, + 'setTopicThumbnailFilename' + ); component.updateTopicThumbnailFilename('img2.svg'); expect(topicThumbnailSpy).toHaveBeenCalled(); }); it('should call the TopicUpdateService if thumbnail is updated', () => { component.updateTopicThumbnailFilename('img2.svg'); - let topicThumbnailSpy = ( - spyOn(topicUpdateService, 'setTopicThumbnailFilename')); + let topicThumbnailSpy = spyOn( + topicUpdateService, + 'setTopicThumbnailFilename' + ); component.updateTopicThumbnailFilename('img2.svg'); expect(topicThumbnailSpy).not.toHaveBeenCalled(); }); - it('should call the TopicUpdateService if topic description is updated', - () => { - let topicDescriptionSpy = ( - spyOn(topicUpdateService, 'setTopicDescription')); - component.updateTopicDescription('New description'); - expect(topicDescriptionSpy).toHaveBeenCalled(); - }); - - it('should not call the TopicUpdateService if topic description is same', - () => { - component.updateTopicDescription('New description'); - let topicDescriptionSpy = ( - spyOn(topicUpdateService, 'setTopicDescription')); - component.updateTopicDescription('New description'); - expect(topicDescriptionSpy).not.toHaveBeenCalled(); - }); + it('should call the TopicUpdateService if topic description is updated', () => { + let topicDescriptionSpy = spyOn(topicUpdateService, 'setTopicDescription'); + component.updateTopicDescription('New description'); + expect(topicDescriptionSpy).toHaveBeenCalled(); + }); - it('should call the TopicUpdateService if topic meta tag content is updated', - () => { - let topicMetaTagContentSpy = ( - spyOn(topicUpdateService, 'setMetaTagContent')); - component.updateTopicMetaTagContent('new meta tag content'); - expect(topicMetaTagContentSpy).toHaveBeenCalled(); - }); + it('should not call the TopicUpdateService if topic description is same', () => { + component.updateTopicDescription('New description'); + let topicDescriptionSpy = spyOn(topicUpdateService, 'setTopicDescription'); + component.updateTopicDescription('New description'); + expect(topicDescriptionSpy).not.toHaveBeenCalled(); + }); - it('should not call the TopicUpdateService if topic description is same', - () => { - component.updateTopicMetaTagContent('New meta tag content'); - let topicMetaTagContentSpy = ( - spyOn(topicUpdateService, 'setMetaTagContent')); - component.updateTopicMetaTagContent('New meta tag content'); - expect(topicMetaTagContentSpy).not.toHaveBeenCalled(); - }); + it('should call the TopicUpdateService if topic meta tag content is updated', () => { + let topicMetaTagContentSpy = spyOn(topicUpdateService, 'setMetaTagContent'); + component.updateTopicMetaTagContent('new meta tag content'); + expect(topicMetaTagContentSpy).toHaveBeenCalled(); + }); - it('should call the TopicUpdateService if topic page title is updated', - () => { - let topicPageTitleFragmentForWebSpy = spyOn( - topicUpdateService, 'setPageTitleFragmentForWeb'); - component.updateTopicPageTitleFragmentForWeb('new page title'); - expect(topicPageTitleFragmentForWebSpy).toHaveBeenCalled(); - }); + it('should not call the TopicUpdateService if topic description is same', () => { + component.updateTopicMetaTagContent('New meta tag content'); + let topicMetaTagContentSpy = spyOn(topicUpdateService, 'setMetaTagContent'); + component.updateTopicMetaTagContent('New meta tag content'); + expect(topicMetaTagContentSpy).not.toHaveBeenCalled(); + }); - it('should not call the TopicUpdateService if topic page title is same', - () => { - component.updateTopicPageTitleFragmentForWeb('New page title'); - let topicPageTitleFragmentForWebSpy = spyOn( - topicUpdateService, 'setPageTitleFragmentForWeb'); - component.updateTopicPageTitleFragmentForWeb('New page title'); - expect(topicPageTitleFragmentForWebSpy).not.toHaveBeenCalled(); - }); + it('should call the TopicUpdateService if topic page title is updated', () => { + let topicPageTitleFragmentForWebSpy = spyOn( + topicUpdateService, + 'setPageTitleFragmentForWeb' + ); + component.updateTopicPageTitleFragmentForWeb('new page title'); + expect(topicPageTitleFragmentForWebSpy).toHaveBeenCalled(); + }); - it('should set the practice tab as displayed if there are the defined ' + - 'minimum number of practice questions in the topic', () => { - let topicPracticeTabSpy = ( - spyOn(topicUpdateService, 'setPracticeTabIsDisplayed')); - component.skillQuestionCountDict = {skill1: 3, skill2: 6}; - component.updatePracticeTabIsDisplayed(true); - expect(topicPracticeTabSpy).not.toHaveBeenCalled(); - component.skillQuestionCountDict = {skill1: 3, skill2: 7}; - component.updatePracticeTabIsDisplayed(true); - expect(topicPracticeTabSpy).toHaveBeenCalled(); + it('should not call the TopicUpdateService if topic page title is same', () => { + component.updateTopicPageTitleFragmentForWeb('New page title'); + let topicPageTitleFragmentForWebSpy = spyOn( + topicUpdateService, + 'setPageTitleFragmentForWeb' + ); + component.updateTopicPageTitleFragmentForWeb('New page title'); + expect(topicPageTitleFragmentForWebSpy).not.toHaveBeenCalled(); }); - it('should call the TopicUpdateService if skill is deleted from topic', + it( + 'should set the practice tab as displayed if there are the defined ' + + 'minimum number of practice questions in the topic', () => { - let topicDeleteSpy = ( - spyOn(topicUpdateService, 'removeUncategorizedSkill')); - component.deleteUncategorizedSkillFromTopic(null); - expect(topicDeleteSpy).toHaveBeenCalled(); - }); + let topicPracticeTabSpy = spyOn( + topicUpdateService, + 'setPracticeTabIsDisplayed' + ); + component.skillQuestionCountDict = {skill1: 3, skill2: 6}; + component.updatePracticeTabIsDisplayed(true); + expect(topicPracticeTabSpy).not.toHaveBeenCalled(); + component.skillQuestionCountDict = {skill1: 3, skill2: 7}; + component.updatePracticeTabIsDisplayed(true); + expect(topicPracticeTabSpy).toHaveBeenCalled(); + } + ); - it('should call the TopicUpdateService if thumbnail bg color is updated', - () => { - let topicThumbnailBGSpy = ( - spyOn(topicUpdateService, 'setTopicThumbnailBgColor')); - component.updateTopicThumbnailBgColor('#FFFFFF'); - expect(topicThumbnailBGSpy).toHaveBeenCalled(); - }); + it('should call the TopicUpdateService if skill is deleted from topic', () => { + let topicDeleteSpy = spyOn(topicUpdateService, 'removeUncategorizedSkill'); + component.deleteUncategorizedSkillFromTopic(null); + expect(topicDeleteSpy).toHaveBeenCalled(); + }); + + it('should call the TopicUpdateService if thumbnail bg color is updated', () => { + let topicThumbnailBGSpy = spyOn( + topicUpdateService, + 'setTopicThumbnailBgColor' + ); + component.updateTopicThumbnailBgColor('#FFFFFF'); + expect(topicThumbnailBGSpy).toHaveBeenCalled(); + }); it('should call TopicEditorRoutingService to navigate to skill', () => { - let topicThumbnailBGSpy = ( - spyOn(topicEditorRoutingService, 'navigateToSkillEditorWithId')); + let topicThumbnailBGSpy = spyOn( + topicEditorRoutingService, + 'navigateToSkillEditorWithId' + ); component.navigateToSkill('id1'); expect(topicThumbnailBGSpy).toHaveBeenCalledWith('id1'); }); @@ -531,17 +550,19 @@ describe('Topic editor tab directive', () => { it('should return skill editor URL', () => { let skillId = 'asd4242a'; expect(component.getSkillEditorUrl(skillId)).toEqual( - '/skill_editor/' + skillId); + '/skill_editor/' + skillId + ); }); - it('should not call the TopicUpdateService if thumbnail bg color is same', - () => { - component.updateTopicThumbnailBgColor('#FFFFFF'); - let topicThumbnailBGSpy = ( - spyOn(topicUpdateService, 'setTopicThumbnailBgColor')); - component.updateTopicThumbnailBgColor('#FFFFFF'); - expect(topicThumbnailBGSpy).not.toHaveBeenCalled(); - }); + it('should not call the TopicUpdateService if thumbnail bg color is same', () => { + component.updateTopicThumbnailBgColor('#FFFFFF'); + let topicThumbnailBGSpy = spyOn( + topicUpdateService, + 'setTopicThumbnailBgColor' + ); + component.updateTopicThumbnailBgColor('#FFFFFF'); + expect(topicThumbnailBGSpy).not.toHaveBeenCalled(); + }); it('should toggle topic preview', () => { expect(component.topicPreviewCardIsShown).toEqual(false); @@ -568,31 +589,35 @@ describe('Topic editor tab directive', () => { } spyOn(undoRedoService, 'getChangeCount').and.returnValue(1); const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ + return { componentInstance: MockNgbModalRef, - result: Promise.resolve() - }) as NgbModalRef; + result: Promise.resolve(), + } as NgbModalRef; }); component.createCanonicalStory(); expect(modalSpy).toHaveBeenCalled(); }); it('should call TopicRoutingService to navigate to subtopic', () => { - let topicRoutingSpy = ( - spyOn(topicEditorRoutingService, 'navigateToSubtopicEditorWithId')); + let topicRoutingSpy = spyOn( + topicEditorRoutingService, + 'navigateToSubtopicEditorWithId' + ); component.navigateToSubtopic(2, ''); expect(topicRoutingSpy).toHaveBeenCalledWith(2); }); - it('should call TopicEditorService and TopicUpdateService ' + - 'on to delete subtopic', () => { - let topicEditorSpy = spyOn(topicEditorStateService, 'deleteSubtopicPage'); - let topicUpdateSpy = ( - spyOn(topicUpdateService, 'deleteSubtopic')); - component.deleteSubtopic(2); - expect(topicEditorSpy).toHaveBeenCalled(); - expect(topicUpdateSpy).toHaveBeenCalled(); - }); + it( + 'should call TopicEditorService and TopicUpdateService ' + + 'on to delete subtopic', + () => { + let topicEditorSpy = spyOn(topicEditorStateService, 'deleteSubtopicPage'); + let topicUpdateSpy = spyOn(topicUpdateService, 'deleteSubtopic'); + component.deleteSubtopic(2); + expect(topicEditorSpy).toHaveBeenCalled(); + expect(topicUpdateSpy).toHaveBeenCalled(); + } + ); it('should return preview footer text for topic preview', () => { expect(component.getPreviewFooter()).toEqual('2 Stories'); @@ -602,36 +627,37 @@ describe('Topic editor tab directive', () => { expect(component.getPreviewFooter()).toEqual('1 Story'); }); - it('should only toggle preview of entity lists in mobile view', - fakeAsync(() => { - let MockWindowDimensionsServiceSpy = spyOn( - windowDimensionsService, 'isWindowNarrow'); + it('should only toggle preview of entity lists in mobile view', fakeAsync(() => { + let MockWindowDimensionsServiceSpy = spyOn( + windowDimensionsService, + 'isWindowNarrow' + ); - expect(component.mainTopicCardIsShown).toEqual(true); - component.togglePreviewListCards('topic'); - expect(component.mainTopicCardIsShown).toEqual(true); - tick(); + expect(component.mainTopicCardIsShown).toEqual(true); + component.togglePreviewListCards('topic'); + expect(component.mainTopicCardIsShown).toEqual(true); + tick(); - MockWindowDimensionsServiceSpy.and.returnValue(true); - expect(component.subtopicsListIsShown).toEqual(true); - expect(component.storiesListIsShown).toEqual(true); - tick(); + MockWindowDimensionsServiceSpy.and.returnValue(true); + expect(component.subtopicsListIsShown).toEqual(true); + expect(component.storiesListIsShown).toEqual(true); + tick(); - component.togglePreviewListCards('subtopic'); - expect(component.subtopicsListIsShown).toEqual(false); - expect(component.storiesListIsShown).toEqual(true); - tick(); + component.togglePreviewListCards('subtopic'); + expect(component.subtopicsListIsShown).toEqual(false); + expect(component.storiesListIsShown).toEqual(true); + tick(); - component.togglePreviewListCards('story'); - expect(component.subtopicsListIsShown).toEqual(false); - expect(component.storiesListIsShown).toEqual(false); - tick(); + component.togglePreviewListCards('story'); + expect(component.subtopicsListIsShown).toEqual(false); + expect(component.storiesListIsShown).toEqual(false); + tick(); - expect(component.mainTopicCardIsShown).toEqual(true); - component.togglePreviewListCards('topic'); - expect(component.mainTopicCardIsShown).toEqual(false); - tick(); - })); + expect(component.mainTopicCardIsShown).toEqual(true); + component.togglePreviewListCards('topic'); + expect(component.mainTopicCardIsShown).toEqual(false); + tick(); + })); it('should toggle uncategorized skill options', () => { component.toggleUncategorizedSkillOptions(10); @@ -640,73 +666,68 @@ describe('Topic editor tab directive', () => { expect(component.uncategorizedEditOptionsIndex).toEqual(20); }); - it('should open ChangeSubtopicAssignment modal when change ' + - 'subtopic assignment is called', () => { - class MockNgbModalRef { - componentInstance: { - subtopics: null; - }; - } - - const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ( - { - componentInstance: MockNgbModalRef, - result: Promise.resolve(1) - } as NgbModalRef); - }); - component.changeSubtopicAssignment(1, skillSummary); - expect(modalSpy).toHaveBeenCalled(); - }); - - it('should open ChangeSubtopicAssignment modal and call TopicUpdateService', - fakeAsync(() => { - let moveSkillUpdateSpy = spyOn( - topicUpdateService, 'moveSkillToSubtopic'); + it( + 'should open ChangeSubtopicAssignment modal when change ' + + 'subtopic assignment is called', + () => { class MockNgbModalRef { componentInstance: { subtopics: null; }; } - spyOn(ngbModal, 'open').and.returnValue( - { - componentInstance: MockNgbModalRef, - result: Promise.resolve(2) - } as NgbModalRef - ); + const modalSpy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + componentInstance: MockNgbModalRef, + result: Promise.resolve(1), + } as NgbModalRef; + }); component.changeSubtopicAssignment(1, skillSummary); - tick(); + expect(modalSpy).toHaveBeenCalled(); + } + ); - expect(moveSkillUpdateSpy).toHaveBeenCalled(); - })); + it('should open ChangeSubtopicAssignment modal and call TopicUpdateService', fakeAsync(() => { + let moveSkillUpdateSpy = spyOn(topicUpdateService, 'moveSkillToSubtopic'); + class MockNgbModalRef { + componentInstance: { + subtopics: null; + }; + } + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: MockNgbModalRef, + result: Promise.resolve(2), + } as NgbModalRef); - it('should not call the TopicUpdateService if subtopicIds are same', - fakeAsync(() => { - class MockNgbModalRef { - componentInstance: { - subtopics: null; - }; - } + component.changeSubtopicAssignment(1, skillSummary); + tick(); - spyOn(ngbModal, 'open').and.returnValue( - { - componentInstance: MockNgbModalRef, - result: Promise.resolve(1) - } as NgbModalRef - ); - let moveSkillSpy = ( - spyOn(topicUpdateService, 'moveSkillToSubtopic')); - component.changeSubtopicAssignment(1, skillSummary); - tick(); - expect(moveSkillSpy).not.toHaveBeenCalled(); - })); + expect(moveSkillUpdateSpy).toHaveBeenCalled(); + })); + + it('should not call the TopicUpdateService if subtopicIds are same', fakeAsync(() => { + class MockNgbModalRef { + componentInstance: { + subtopics: null; + }; + } + + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: MockNgbModalRef, + result: Promise.resolve(1), + } as NgbModalRef); + let moveSkillSpy = spyOn(topicUpdateService, 'moveSkillToSubtopic'); + component.changeSubtopicAssignment(1, skillSummary); + tick(); + expect(moveSkillSpy).not.toHaveBeenCalled(); + })); it('should react to event when story summaries are initialized', () => { spyOn(topicEditorStateService, 'getCanonicalStorySummaries'); mockStorySummariesInitializedEventEmitter.emit(); expect( - topicEditorStateService.getCanonicalStorySummaries).toHaveBeenCalled(); + topicEditorStateService.getCanonicalStorySummaries + ).toHaveBeenCalled(); }); it('should call initEditor on initialization of topic', () => { @@ -716,51 +737,67 @@ describe('Topic editor tab directive', () => { expect(component.initEditor).toHaveBeenCalled(); }); - it('should call the TopicUpdateService if skillId is added in the ' + - 'diagnostic test', fakeAsync(() => { - let updateSkillIdForDiagnosticTestSpy = spyOn( - topicUpdateService, 'updateDiagnosticTestSkills'); - component.selectedSkillForDiagnosticTest = skillSummary; - component.availableSkillSummariesForDiagnosticTest = [skillSummary]; - component.addSkillForDiagnosticTest(); - tick(); - tick(); - expect(updateSkillIdForDiagnosticTestSpy).toHaveBeenCalledWith( - component.topic, component.selectedSkillSummariesForDiagnosticTest); - })); + it( + 'should call the TopicUpdateService if skillId is added in the ' + + 'diagnostic test', + fakeAsync(() => { + let updateSkillIdForDiagnosticTestSpy = spyOn( + topicUpdateService, + 'updateDiagnosticTestSkills' + ); + component.selectedSkillForDiagnosticTest = skillSummary; + component.availableSkillSummariesForDiagnosticTest = [skillSummary]; + component.addSkillForDiagnosticTest(); + tick(); + tick(); + expect(updateSkillIdForDiagnosticTestSpy).toHaveBeenCalledWith( + component.topic, + component.selectedSkillSummariesForDiagnosticTest + ); + }) + ); - it('should call the TopicUpdateService if any skillId is removed from the ' + - 'diagnostic test', () => { - let updateSkillIdForDiagnosticTestSpy = spyOn( - topicUpdateService, 'updateDiagnosticTestSkills'); - component.selectedSkillSummariesForDiagnosticTest = [skillSummary]; + it( + 'should call the TopicUpdateService if any skillId is removed from the ' + + 'diagnostic test', + () => { + let updateSkillIdForDiagnosticTestSpy = spyOn( + topicUpdateService, + 'updateDiagnosticTestSkills' + ); + component.selectedSkillSummariesForDiagnosticTest = [skillSummary]; - component.removeSkillFromDiagnosticTest(skillSummary); - expect(updateSkillIdForDiagnosticTestSpy).toHaveBeenCalledWith( - component.topic, component.selectedSkillSummariesForDiagnosticTest); - }); + component.removeSkillFromDiagnosticTest(skillSummary); + expect(updateSkillIdForDiagnosticTestSpy).toHaveBeenCalledWith( + component.topic, + component.selectedSkillSummariesForDiagnosticTest + ); + } + ); it('should get eligible skill for diagnostic test selection', () => { component.skillQuestionCountDict = { - skill_1: 3 + skill_1: 3, }; topic._uncategorizedSkillSummaries = []; topic._subtopics = []; expect(component.getEligibleSkillSummariesForDiagnosticTest()).toEqual([]); - spyOn(component.topic, 'getAvailableSkillSummariesForDiagnosticTest') - .and.returnValue([skillSummary]); - expect(component.getEligibleSkillSummariesForDiagnosticTest()).toEqual( - [skillSummary]); + spyOn( + component.topic, + 'getAvailableSkillSummariesForDiagnosticTest' + ).and.returnValue([skillSummary]); + expect(component.getEligibleSkillSummariesForDiagnosticTest()).toEqual([ + skillSummary, + ]); }); - it('should be able to present diagnostic test dropdown selector correctly', - () => { - expect(component.diagnosticTestSkillsDropdownIsShown).toBeFalse(); - component.presentDiagnosticTestSkillDropdown(); - expect(component.diagnosticTestSkillsDropdownIsShown).toBeTrue(); + it('should be able to present diagnostic test dropdown selector correctly', () => { + expect(component.diagnosticTestSkillsDropdownIsShown).toBeFalse(); + component.presentDiagnosticTestSkillDropdown(); + expect(component.diagnosticTestSkillsDropdownIsShown).toBeTrue(); - component.removeDiagnosticTestSkillDropdown(); - expect(component.diagnosticTestSkillsDropdownIsShown).toBeFalse(); - }); + component.removeDiagnosticTestSkillDropdown(); + expect(component.diagnosticTestSkillsDropdownIsShown).toBeFalse(); + }); }); diff --git a/core/templates/pages/topic-editor-page/editor-tab/topic-editor-tab.directive.ts b/core/templates/pages/topic-editor-page/editor-tab/topic-editor-tab.directive.ts index 6321d7b7c0d3..ef6a103bc086 100644 --- a/core/templates/pages/topic-editor-page/editor-tab/topic-editor-tab.directive.ts +++ b/core/templates/pages/topic-editor-page/editor-tab/topic-editor-tab.directive.ts @@ -16,39 +16,39 @@ * @fileoverview Component for the main topic editor. */ -import { ChangeSubtopicAssignmentModalComponent } from '../modal-templates/change-subtopic-assignment-modal.component'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { SavePendingChangesModalComponent } from 'components/save-pending-changes/save-pending-changes-modal.component'; -import { Subscription } from 'rxjs'; -import { AppConstants } from 'app.constants'; +import {ChangeSubtopicAssignmentModalComponent} from '../modal-templates/change-subtopic-assignment-modal.component'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {SavePendingChangesModalComponent} from 'components/save-pending-changes/save-pending-changes-modal.component'; +import {Subscription} from 'rxjs'; +import {AppConstants} from 'app.constants'; import cloneDeep from 'lodash/cloneDeep'; -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { TopicEditorStateService } from '../services/topic-editor-state.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { TopicsAndSkillsDashboardBackendApiService } from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { ImageUploadHelperService } from 'services/image-upload-helper.service'; -import { ContextService } from 'services/context.service'; -import { TopicUpdateService } from 'domain/topic/topic-update.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { StorySummary } from 'domain/story/story-summary.model'; -import { EntityCreationService } from '../services/entity-creation.service'; -import { UndoRedoService } from 'domain/editor/undo_redo/undo-redo.service'; -import { ShortSkillSummary } from 'domain/skill/short-skill-summary.model'; -import { StoryCreationBackendApiService } from 'components/entity-creation-services/story-creation-backend-api.service'; -import { TopicEditorRoutingService } from '../services/topic-editor-routing.service'; -import { PageTitleService } from 'services/page-title.service'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { Subtopic } from 'domain/topic/subtopic.model'; -import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; -import { Topic } from 'domain/topic/topic-object.model'; -import { TopicRights } from 'domain/topic/topic-rights.model'; -import { RearrangeSkillsInSubtopicsModalComponent } from '../modal-templates/rearrange-skills-in-subtopics-modal.component'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {TopicEditorStateService} from '../services/topic-editor-state.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {TopicsAndSkillsDashboardBackendApiService} from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {ImageUploadHelperService} from 'services/image-upload-helper.service'; +import {ContextService} from 'services/context.service'; +import {TopicUpdateService} from 'domain/topic/topic-update.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {StorySummary} from 'domain/story/story-summary.model'; +import {EntityCreationService} from '../services/entity-creation.service'; +import {UndoRedoService} from 'domain/editor/undo_redo/undo-redo.service'; +import {ShortSkillSummary} from 'domain/skill/short-skill-summary.model'; +import {StoryCreationBackendApiService} from 'components/entity-creation-services/story-creation-backend-api.service'; +import {TopicEditorRoutingService} from '../services/topic-editor-routing.service'; +import {PageTitleService} from 'services/page-title.service'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {Subtopic} from 'domain/topic/subtopic.model'; +import {CdkDragDrop, moveItemInArray} from '@angular/cdk/drag-drop'; +import {Topic} from 'domain/topic/topic-object.model'; +import {TopicRights} from 'domain/topic/topic-rights.model'; +import {RearrangeSkillsInSubtopicsModalComponent} from '../modal-templates/rearrange-skills-in-subtopics-modal.component'; @Component({ selector: 'oppia-topic-editor-tab', - templateUrl: './topic-editor-tab.directive.html' + templateUrl: './topic-editor-tab.directive.html', }) export class TopicEditorTabComponent implements OnInit, OnDestroy { skillCreationIsAllowed: boolean; @@ -101,8 +101,7 @@ export class TopicEditorTabComponent implements OnInit, OnDestroy { private ngbModal: NgbModal, private pageTitleService: PageTitleService, private storyCreationBackendApiService: StoryCreationBackendApiService, - private topicsAndSkillsDashboardBackendApiService: - TopicsAndSkillsDashboardBackendApiService, + private topicsAndSkillsDashboardBackendApiService: TopicsAndSkillsDashboardBackendApiService, private topicEditorStateService: TopicEditorStateService, private topicEditorRoutingService: TopicEditorRoutingService, private topicUpdateService: TopicUpdateService, @@ -115,21 +114,22 @@ export class TopicEditorTabComponent implements OnInit, OnDestroy { directiveSubscriptions = new Subscription(); drop(event: CdkDragDrop): void { - moveItemInArray( - this.subtopics, - event.previousIndex, event.currentIndex); + moveItemInArray(this.subtopics, event.previousIndex, event.currentIndex); this.topicUpdateService.rearrangeSubtopic( - this.topic, event.previousIndex, event.currentIndex); + this.topic, + event.previousIndex, + event.currentIndex + ); this.initEditor(); } initEditor(): void { - this.skillCreationIsAllowed = ( - this.topicEditorStateService.isSkillCreationAllowed()); + this.skillCreationIsAllowed = + this.topicEditorStateService.isSkillCreationAllowed(); this.topic = this.topicEditorStateService.getTopic(); - this.skillQuestionCountDict = ( - this.topicEditorStateService.getSkillQuestionCountDict()); + this.skillQuestionCountDict = + this.topicEditorStateService.getSkillQuestionCountDict(); this.topicRights = this.topicEditorStateService.getTopicRights(); this.topicNameEditorIsShown = false; if (this.topicEditorStateService.hasLoadedTopic()) { @@ -138,61 +138,56 @@ export class TopicEditorTabComponent implements OnInit, OnDestroy { } this.editableName = this.topic.getName(); this.editableMetaTagContent = this.topic.getMetaTagContent(); - this.editablePageTitleFragmentForWeb = ( - this.topic.getPageTitleFragmentForWeb()); - this.editablePracticeIsDisplayed = ( - this.topic.getPracticeTabIsDisplayed()); + this.editablePageTitleFragmentForWeb = + this.topic.getPageTitleFragmentForWeb(); + this.editablePracticeIsDisplayed = this.topic.getPracticeTabIsDisplayed(); this.initialTopicName = this.topic.getName(); this.initialTopicUrlFragment = this.topic.getUrlFragment(); this.editableTopicUrlFragment = this.topic.getUrlFragment(); this.editableDescription = this.topic.getDescription(); - this.allowedBgColors = ( - AppConstants.ALLOWED_THUMBNAIL_BG_COLORS.topic); + this.allowedBgColors = AppConstants.ALLOWED_THUMBNAIL_BG_COLORS.topic; this.topicNameExists = false; this.topicUrlFragmentExists = false; this.hostname = this.windowRef.nativeWindow.location.hostname; - this.availableSkillSummariesForDiagnosticTest = ( - this.getEligibleSkillSummariesForDiagnosticTest()); - this.selectedSkillSummariesForDiagnosticTest = ( - this.topic.getSkillSummariesForDiagnosticTest()); + this.availableSkillSummariesForDiagnosticTest = + this.getEligibleSkillSummariesForDiagnosticTest(); + this.selectedSkillSummariesForDiagnosticTest = + this.topic.getSkillSummariesForDiagnosticTest(); this.diagnosticTestSkillsDropdownIsShown = false; this.selectedSkillForDiagnosticTest = null; - - this.editableDescriptionIsEmpty = ( - this.editableDescription === ''); + this.editableDescriptionIsEmpty = this.editableDescription === ''; this.topicDescriptionChanged = false; this.subtopics = this.topic.getSubtopics(); this.subtopicQuestionCountDict = {}; - this.subtopics.map((subtopic) => { + this.subtopics.map(subtopic => { const subtopicId = subtopic.getId(); this.subtopicQuestionCountDict[subtopicId] = 0; - subtopic.getSkillSummaries().map((skill) => { - this.subtopicQuestionCountDict[subtopicId] += ( - this.skillQuestionCountDict[skill.id]); + subtopic.getSkillSummaries().map(skill => { + this.subtopicQuestionCountDict[subtopicId] += + this.skillQuestionCountDict[skill.id]; }); }); - this.uncategorizedSkillSummaries = ( - this.topic.getUncategorizedSkillSummaries()); - this.editableThumbnailDataUrl = ( - this.imageUploadHelperService - .getTrustedResourceUrlForThumbnailFilename( - this.topic.getThumbnailFilename(), - this.contextService.getEntityType(), - this.contextService.getEntityId())); + this.uncategorizedSkillSummaries = + this.topic.getUncategorizedSkillSummaries(); + this.editableThumbnailDataUrl = + this.imageUploadHelperService.getTrustedResourceUrlForThumbnailFilename( + this.topic.getThumbnailFilename(), + this.contextService.getEntityType(), + this.contextService.getEntityId() + ); } getEligibleSkillSummariesForDiagnosticTest(): ShortSkillSummary[] { - let availableSkillSummaries = ( - this.topic.getAvailableSkillSummariesForDiagnosticTest()); - - let eligibleSkillSummaries = ( - availableSkillSummaries.filter( - skillSummary => - this.skillQuestionCountDict[skillSummary.getId()] >= - AppConstants.MIN_QUESTION_COUNT_FOR_A_DIAGNOSTIC_TEST_SKILL - )); + let availableSkillSummaries = + this.topic.getAvailableSkillSummariesForDiagnosticTest(); + + let eligibleSkillSummaries = availableSkillSummaries.filter( + skillSummary => + this.skillQuestionCountDict[skillSummary.getId()] >= + AppConstants.MIN_QUESTION_COUNT_FOR_A_DIAGNOSTIC_TEST_SKILL + ); return eligibleSkillSummaries; } @@ -201,41 +196,38 @@ export class TopicEditorTabComponent implements OnInit, OnDestroy { this.selectedSkillForDiagnosticTest = null; this.selectedSkillSummariesForDiagnosticTest.push(skillToAdd); - let skillSummary = ( - this.availableSkillSummariesForDiagnosticTest.find( - skill => skill.getId() === skillToAdd.getId()) + let skillSummary = this.availableSkillSummariesForDiagnosticTest.find( + skill => skill.getId() === skillToAdd.getId() ); if (skillSummary) { - let index = this.availableSkillSummariesForDiagnosticTest - .indexOf(skillSummary); - this.availableSkillSummariesForDiagnosticTest.splice( - index, 1); + let index = + this.availableSkillSummariesForDiagnosticTest.indexOf(skillSummary); + this.availableSkillSummariesForDiagnosticTest.splice(index, 1); } this.topicUpdateService.updateDiagnosticTestSkills( this.topic, - cloneDeep(this.selectedSkillSummariesForDiagnosticTest)); + cloneDeep(this.selectedSkillSummariesForDiagnosticTest) + ); this.diagnosticTestSkillsDropdownIsShown = false; } removeSkillFromDiagnosticTest(skillToRemove: ShortSkillSummary): void { - let skillSummary = ( - this.selectedSkillSummariesForDiagnosticTest.find( - skill => skill.getId() === skillToRemove.getId()) + let skillSummary = this.selectedSkillSummariesForDiagnosticTest.find( + skill => skill.getId() === skillToRemove.getId() ); if (skillSummary) { - let index = this.selectedSkillSummariesForDiagnosticTest - .indexOf(skillSummary); - this.selectedSkillSummariesForDiagnosticTest.splice( - index, 1); + let index = + this.selectedSkillSummariesForDiagnosticTest.indexOf(skillSummary); + this.selectedSkillSummariesForDiagnosticTest.splice(index, 1); } - this.availableSkillSummariesForDiagnosticTest.push( - skillToRemove); + this.availableSkillSummariesForDiagnosticTest.push(skillToRemove); this.topicUpdateService.updateDiagnosticTestSkills( this.topic, - cloneDeep(this.selectedSkillSummariesForDiagnosticTest)); + cloneDeep(this.selectedSkillSummariesForDiagnosticTest) + ); } getClassroomUrlFragment(): string { @@ -244,7 +236,7 @@ export class TopicEditorTabComponent implements OnInit, OnDestroy { _initStorySummaries(): void { this.canonicalStorySummaries = - this.topicEditorStateService.getCanonicalStorySummaries(); + this.topicEditorStateService.getCanonicalStorySummaries(); } // This is added because when we create a skill from the topic @@ -268,35 +260,41 @@ export class TopicEditorTabComponent implements OnInit, OnDestroy { } reassignSkillsInSubtopics(): void { - this.ngbModal.open(RearrangeSkillsInSubtopicsModalComponent, { - backdrop: 'static', - windowClass: 'rearrange-skills-modal', - size: 'xl' - }).result.then(() => { - this.initEditor(); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + this.ngbModal + .open(RearrangeSkillsInSubtopicsModalComponent, { + backdrop: 'static', + windowClass: 'rearrange-skills-modal', + size: 'xl', + }) + .result.then( + () => { + this.initEditor(); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } createCanonicalStory(): void { if (this.undoRedoService.getChangeCount() > 0) { - const modalRef = this.ngbModal.open( - SavePendingChangesModalComponent, { - backdrop: true - }); - - modalRef.componentInstance.body = ( - 'Please save all pending changes ' + - 'before exiting the topic editor.'); - - modalRef.result.then(() => {}, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. + const modalRef = this.ngbModal.open(SavePendingChangesModalComponent, { + backdrop: true, }); + + modalRef.componentInstance.body = + 'Please save all pending changes ' + 'before exiting the topic editor.'; + + modalRef.result.then( + () => {}, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } else { this.storyCreationBackendApiService.createNewCanonicalStory(); } @@ -304,20 +302,21 @@ export class TopicEditorTabComponent implements OnInit, OnDestroy { createSkill(): void { if (this.undoRedoService.getChangeCount() > 0) { - const modalRef = this.ngbModal.open( - SavePendingChangesModalComponent, { - backdrop: true - }); - - modalRef.componentInstance.body = ( - 'Please save all pending changes ' + - 'before exiting the topic editor.'); - - modalRef.result.then(() => {}, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. + const modalRef = this.ngbModal.open(SavePendingChangesModalComponent, { + backdrop: true, }); + + modalRef.componentInstance.body = + 'Please save all pending changes ' + 'before exiting the topic editor.'; + + modalRef.result.then( + () => {}, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } else { this.entityCreationService.createSkill(); } @@ -328,7 +327,7 @@ export class TopicEditorTabComponent implements OnInit, OnDestroy { } updateTopicDescriptionStatus(description: string): void { - this.editableDescriptionIsEmpty = (description === ''); + this.editableDescriptionIsEmpty = description === ''; this.topicDescriptionChanged = true; } @@ -338,13 +337,12 @@ export class TopicEditorTabComponent implements OnInit, OnDestroy { return; } if (newName) { - this.topicEditorStateService.updateExistenceOfTopicName( - newName, () => { - this.topicNameExists = ( - this.topicEditorStateService.getTopicWithNameExists()); - this.topicUpdateService.setTopicName(this.topic, newName); - this.topicNameEditorIsShown = false; - }); + this.topicEditorStateService.updateExistenceOfTopicName(newName, () => { + this.topicNameExists = + this.topicEditorStateService.getTopicWithNameExists(); + this.topicUpdateService.setTopicName(this.topic, newName); + this.topicNameEditorIsShown = false; + }); } else { this.topicUpdateService.setTopicName(this.topic, newName); this.topicNameEditorIsShown = false; @@ -358,18 +356,27 @@ export class TopicEditorTabComponent implements OnInit, OnDestroy { } if (newTopicUrlFragment) { this.topicEditorStateService.updateExistenceOfTopicUrlFragment( - newTopicUrlFragment, () => { - this.topicUrlFragmentExists = ( - this.topicEditorStateService.getTopicWithUrlFragmentExists()); + newTopicUrlFragment, + () => { + this.topicUrlFragmentExists = + this.topicEditorStateService.getTopicWithUrlFragmentExists(); this.topicUpdateService.setTopicUrlFragment( - this.topic, newTopicUrlFragment); - }, () => { + this.topic, + newTopicUrlFragment + ); + }, + () => { this.topicUpdateService.setTopicUrlFragment( - this.topic, newTopicUrlFragment); - }); + this.topic, + newTopicUrlFragment + ); + } + ); } else { this.topicUpdateService.setTopicUrlFragment( - this.topic, newTopicUrlFragment); + this.topic, + newTopicUrlFragment + ); } } @@ -378,7 +385,9 @@ export class TopicEditorTabComponent implements OnInit, OnDestroy { return; } this.topicUpdateService.setTopicThumbnailFilename( - this.topic, newThumbnailFilename); + this.topic, + newThumbnailFilename + ); } updateTopicThumbnailBgColor(newThumbnailBgColor: string): void { @@ -386,89 +395,106 @@ export class TopicEditorTabComponent implements OnInit, OnDestroy { return; } this.topicUpdateService.setTopicThumbnailBgColor( - this.topic, newThumbnailBgColor); + this.topic, + newThumbnailBgColor + ); } updateTopicDescription(newDescription: string): void { if (newDescription !== this.topic.getDescription()) { - this.topicUpdateService.setTopicDescription( - this.topic, newDescription); + this.topicUpdateService.setTopicDescription(this.topic, newDescription); } } updateTopicMetaTagContent(newMetaTagContent: string): void { if (newMetaTagContent !== this.topic.getMetaTagContent()) { - this.topicUpdateService.setMetaTagContent( - this.topic, newMetaTagContent); + this.topicUpdateService.setMetaTagContent(this.topic, newMetaTagContent); } } updateTopicPageTitleFragmentForWeb( - newTopicPageTitleFragmentForWeb: string): void { + newTopicPageTitleFragmentForWeb: string + ): void { let currentValue = this.topic.getPageTitleFragmentForWeb(); if (newTopicPageTitleFragmentForWeb !== currentValue) { this.topicUpdateService.setPageTitleFragmentForWeb( - this.topic, newTopicPageTitleFragmentForWeb); + this.topic, + newTopicPageTitleFragmentForWeb + ); } } // Only update the topic if 1) the creator is turning off the // practice tab or 2) the creator is turning on the practice tab // and it has enough practice questions. - updatePracticeTabIsDisplayed( - newPracticeTabIsDisplayed: boolean): void { - if (!newPracticeTabIsDisplayed || - this.doesTopicHaveMinimumPracticeQuestions() + updatePracticeTabIsDisplayed(newPracticeTabIsDisplayed: boolean): void { + if ( + !newPracticeTabIsDisplayed || + this.doesTopicHaveMinimumPracticeQuestions() ) { this.topicUpdateService.setPracticeTabIsDisplayed( - this.topic, newPracticeTabIsDisplayed); + this.topic, + newPracticeTabIsDisplayed + ); this.editablePracticeIsDisplayed = newPracticeTabIsDisplayed; } } doesTopicHaveMinimumPracticeQuestions(): boolean { - const skillQuestionCounts = ( - Object.values(this.skillQuestionCountDict)); - const numberOfPracticeQuestions = ( - skillQuestionCounts.reduce((a: number, b: number) => a + b, 0)); + const skillQuestionCounts = Object.values(this.skillQuestionCountDict); + const numberOfPracticeQuestions = skillQuestionCounts.reduce( + (a: number, b: number) => a + b, + 0 + ); return ( numberOfPracticeQuestions >= - AppConstants.TOPIC_MINIMUM_QUESTIONS_TO_PRACTICE + AppConstants.TOPIC_MINIMUM_QUESTIONS_TO_PRACTICE ); } deleteUncategorizedSkillFromTopic(skillSummary: ShortSkillSummary): void { - this.topicUpdateService.removeUncategorizedSkill( - this.topic, skillSummary); + this.topicUpdateService.removeUncategorizedSkill(this.topic, skillSummary); this.removeSkillFromDiagnosticTest(skillSummary); this.initEditor(); } removeSkillFromSubtopic( - subtopicId: number, skillSummary: ShortSkillSummary): void { + subtopicId: number, + skillSummary: ShortSkillSummary + ): void { this.skillOptionDialogueBox = true; this.selectedSkillEditOptionsIndex = {}; this.topicUpdateService.removeSkillFromSubtopic( - this.topic, subtopicId, skillSummary); + this.topic, + subtopicId, + skillSummary + ); this.initEditor(); } removeSkillFromTopic( - subtopicId: number, skillSummary: ShortSkillSummary): void { + subtopicId: number, + skillSummary: ShortSkillSummary + ): void { this.skillOptionDialogueBox = true; this.selectedSkillEditOptionsIndex = {}; this.topicUpdateService.removeSkillFromSubtopic( - this.topic, subtopicId, skillSummary); + this.topic, + subtopicId, + skillSummary + ); this.deleteUncategorizedSkillFromTopic(skillSummary); } togglePreview(): void { - this.topicPreviewCardIsShown = !(this.topicPreviewCardIsShown); + this.topicPreviewCardIsShown = !this.topicPreviewCardIsShown; } deleteSubtopic(subtopicId: number): void { this.topicEditorStateService.deleteSubtopicPage( - this.topic.getId(), subtopicId); + this.topic.getId(), + subtopicId + ); this.topicUpdateService.deleteSubtopic(this.topic, subtopicId); this.initEditor(); } @@ -476,15 +502,15 @@ export class TopicEditorTabComponent implements OnInit, OnDestroy { navigateToSubtopic(subtopicId: number, subtopicName: string): void { this.pageTitleService.setNavbarTitleForMobileView('Subtopic Editor'); this.pageTitleService.setNavbarSubtitleForMobileView(subtopicName); - this.topicEditorRoutingService.navigateToSubtopicEditorWithId( - subtopicId); + this.topicEditorRoutingService.navigateToSubtopicEditorWithId(subtopicId); } getSkillEditorUrl(skillId: string): string { var SKILL_EDITOR_URL_TEMPLATE = '/skill_editor/'; return this.urlInterpolationService.interpolateUrl( - SKILL_EDITOR_URL_TEMPLATE, { - skillId: skillId + SKILL_EDITOR_URL_TEMPLATE, + { + skillId: skillId, } ); } @@ -494,8 +520,7 @@ export class TopicEditorTabComponent implements OnInit, OnDestroy { } getPreviewFooter(): string { - var canonicalStoriesLength = ( - this.topic.getCanonicalStoryIds().length); + var canonicalStoriesLength = this.topic.getCanonicalStoryIds().length; if (canonicalStoriesLength === 0 || canonicalStoriesLength > 1) { return canonicalStoriesLength + ' Stories'; } @@ -516,47 +541,55 @@ export class TopicEditorTabComponent implements OnInit, OnDestroy { } showSubtopicEditOptions(index: number): void { - this.subtopicEditOptionsAreShown = ( - (this.subtopicEditOptionsAreShown === index) ? null : index); + this.subtopicEditOptionsAreShown = + this.subtopicEditOptionsAreShown === index ? null : index; } toggleUncategorizedSkillOptions(index: number): void { - this.uncategorizedEditOptionsIndex = ( - (this.uncategorizedEditOptionsIndex === index) ? - null : index); + this.uncategorizedEditOptionsIndex = + this.uncategorizedEditOptionsIndex === index ? null : index; } changeSubtopicAssignment( - oldSubtopicId: number, - skillSummary: ShortSkillSummary): void { + oldSubtopicId: number, + skillSummary: ShortSkillSummary + ): void { this.skillOptionDialogueBox = true; const modalRef: NgbModalRef = this.ngbModal.open( - ChangeSubtopicAssignmentModalComponent, { + ChangeSubtopicAssignmentModalComponent, + { backdrop: 'static', windowClass: 'oppia-change-subtopic-assignment-modal', - size: 'xl' - }); + size: 'xl', + } + ); modalRef.componentInstance.subtopics = this.subtopics; - modalRef.result.then((newSubtopicId) => { - if (oldSubtopicId === newSubtopicId) { - return; + modalRef.result.then( + newSubtopicId => { + if (oldSubtopicId === newSubtopicId) { + return; + } + this.topicUpdateService.moveSkillToSubtopic( + this.topic, + oldSubtopicId, + newSubtopicId, + skillSummary + ); + this.initEditor(); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. } - this.topicUpdateService.moveSkillToSubtopic( - this.topic, oldSubtopicId, newSubtopicId, - skillSummary); - this.initEditor(); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + ); } showSkillEditOptions( - subtopicIndex: string | number = null, - skillIndex: number = null): void { - if (subtopicIndex === null && - skillIndex === null) { + subtopicIndex: string | number = null, + skillIndex: number = null + ): void { + if (subtopicIndex === null && skillIndex === null) { this.skillOptionDialogueBox = true; return; } else { @@ -569,7 +602,7 @@ export class TopicEditorTabComponent implements OnInit, OnDestroy { } this.selectedSkillEditOptionsIndex[subtopicIndex] = {}; this.selectedSkillEditOptionsIndex[subtopicIndex] = { - [skillIndex]: true + [skillIndex]: true, }; } @@ -590,32 +623,31 @@ export class TopicEditorTabComponent implements OnInit, OnDestroy { this.topicDataHasLoaded = false; this.subtopicCardSelectedIndexes = {}; this.selectedSkillEditOptionsIndex = {}; - this.subtopicsListIsShown = ( - !this.windowDimensionsService.isWindowNarrow()); - this.storiesListIsShown = ( - !this.windowDimensionsService.isWindowNarrow()); + this.subtopicsListIsShown = !this.windowDimensionsService.isWindowNarrow(); + this.storiesListIsShown = !this.windowDimensionsService.isWindowNarrow(); this.directiveSubscriptions.add( - this.topicEditorStateService.onTopicInitialized.subscribe( - () => this.initEditor() - )); + this.topicEditorStateService.onTopicInitialized.subscribe(() => + this.initEditor() + ) + ); this.directiveSubscriptions.add( - this.topicEditorStateService.onTopicReinitialized.subscribe( - () => this.initEditor() - )); + this.topicEditorStateService.onTopicReinitialized.subscribe(() => + this.initEditor() + ) + ); this.mainTopicCardIsShown = true; this.directiveSubscriptions.add( - this.topicEditorStateService.onStorySummariesInitialized.subscribe( - () => this._initStorySummaries() + this.topicEditorStateService.onStorySummariesInitialized.subscribe(() => + this._initStorySummaries() ) ); this.directiveSubscriptions.add( - this.topicsAndSkillsDashboardBackendApiService. - onTopicsAndSkillsDashboardReinitialized.subscribe( - () => { - this.refreshTopic(); - } - ) + this.topicsAndSkillsDashboardBackendApiService.onTopicsAndSkillsDashboardReinitialized.subscribe( + () => { + this.refreshTopic(); + } + ) ); this.initEditor(); this._initStorySummaries(); @@ -626,7 +658,9 @@ export class TopicEditorTabComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive('oppiaTopicEditorTab', +angular.module('oppia').directive( + 'oppiaTopicEditorTab', downgradeComponent({ - component: TopicEditorTabComponent - }) as angular.IDirectiveFactory); + component: TopicEditorTabComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/topic-editor-page/modal-templates/change-subtopic-assignment-modal.component.spec.ts b/core/templates/pages/topic-editor-page/modal-templates/change-subtopic-assignment-modal.component.spec.ts index abc793d96545..9e7904f690a4 100644 --- a/core/templates/pages/topic-editor-page/modal-templates/change-subtopic-assignment-modal.component.spec.ts +++ b/core/templates/pages/topic-editor-page/modal-templates/change-subtopic-assignment-modal.component.spec.ts @@ -16,11 +16,11 @@ * @fileoverview Unit tests for the change subtopic assignment modal. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { ChangeSubtopicAssignmentModalComponent } from './change-subtopic-assignment-modal.component'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { Subtopic } from 'domain/topic/subtopic.model'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {ChangeSubtopicAssignmentModalComponent} from './change-subtopic-assignment-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {Subtopic} from 'domain/topic/subtopic.model'; describe('Change subtopic assignment modal', () => { let component: ChangeSubtopicAssignmentModalComponent; @@ -31,9 +31,7 @@ describe('Change subtopic assignment modal', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ChangeSubtopicAssignmentModalComponent], - providers: [ - NgbActiveModal, - ], + providers: [NgbActiveModal], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -44,15 +42,14 @@ describe('Change subtopic assignment modal', () => { component.subtopics = subtopics; }); - it('should initialize component properties after component is initialized', - function() { - component.ngOnInit(); + it('should initialize component properties after component is initialized', function () { + component.ngOnInit(); - expect(component.subtopics).toEqual(subtopics); - expect(component.selectedSubtopicId).toEqual(null); - }); + expect(component.subtopics).toEqual(subtopics); + expect(component.selectedSubtopicId).toEqual(null); + }); - it('should change the selected subtopic index', function() { + it('should change the selected subtopic index', function () { // Setup. component.changeSelectedSubtopic(10); // Pre-check. diff --git a/core/templates/pages/topic-editor-page/modal-templates/change-subtopic-assignment-modal.component.ts b/core/templates/pages/topic-editor-page/modal-templates/change-subtopic-assignment-modal.component.ts index b1f62b799bec..ad4cbad3292d 100644 --- a/core/templates/pages/topic-editor-page/modal-templates/change-subtopic-assignment-modal.component.ts +++ b/core/templates/pages/topic-editor-page/modal-templates/change-subtopic-assignment-modal.component.ts @@ -16,27 +16,26 @@ * @fileoverview Component for change subtopic assignment modal. */ -import { Component, OnInit } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { Subtopic } from 'domain/topic/subtopic.model'; +import {Component, OnInit} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Subtopic} from 'domain/topic/subtopic.model'; @Component({ selector: 'oppia-change-subtopic-assignment-modal', - templateUrl: './change-subtopic-assignment-modal.component.html' + templateUrl: './change-subtopic-assignment-modal.component.html', }) - export class ChangeSubtopicAssignmentModalComponent - extends ConfirmOrCancelModal implements OnInit { + extends ConfirmOrCancelModal + implements OnInit +{ // These properties below are initialized using Angular lifecycle hooks // where we need to do non-null assertion. For more information see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 subtopics!: Subtopic[]; // Selected subtopic id is null when the user not selects any subtopic. selectedSubtopicId!: number | null; - constructor( - private ngbActiveModal: NgbActiveModal, - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } diff --git a/core/templates/pages/topic-editor-page/modal-templates/create-new-story-modal.component.spec.ts b/core/templates/pages/topic-editor-page/modal-templates/create-new-story-modal.component.spec.ts index 94c69e0521a8..551efb8fa3e8 100644 --- a/core/templates/pages/topic-editor-page/modal-templates/create-new-story-modal.component.spec.ts +++ b/core/templates/pages/topic-editor-page/modal-templates/create-new-story-modal.component.spec.ts @@ -16,15 +16,18 @@ * @fileoverview Unit tests for Create New Story Modal Component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { AppConstants } from 'app.constants'; -import { EditableStoryBackendApiService } from 'domain/story/editable-story-backend-api.service'; -import { StoryEditorStateService } from 'pages/story-editor-page/services/story-editor-state.service'; -import { ImageLocalStorageService, ImagesData } from 'services/image-local-storage.service'; -import { CreateNewStoryModalComponent } from './create-new-story-modal.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {AppConstants} from 'app.constants'; +import {EditableStoryBackendApiService} from 'domain/story/editable-story-backend-api.service'; +import {StoryEditorStateService} from 'pages/story-editor-page/services/story-editor-state.service'; +import { + ImageLocalStorageService, + ImagesData, +} from 'services/image-local-storage.service'; +import {CreateNewStoryModalComponent} from './create-new-story-modal.component'; class MockActiveModal { close(): void { @@ -42,7 +45,6 @@ describe('Create New Story Modal Component', () => { let imageLocalStorageService: ImageLocalStorageService; let storyEditorStateService: StoryEditorStateService; - beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], @@ -51,10 +53,10 @@ describe('Create New Story Modal Component', () => { EditableStoryBackendApiService, { provide: NgbActiveModal, - useClass: MockActiveModal + useClass: MockActiveModal, }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); @@ -64,42 +66,44 @@ describe('Create New Story Modal Component', () => { imageLocalStorageService = TestBed.inject(ImageLocalStorageService); storyEditorStateService = TestBed.inject(StoryEditorStateService); - spyOn(imageLocalStorageService, 'getStoredImagesData').and.returnValue( - [{ filename: 'a.png', image: 'faf' } as unknown as ImagesData]); + spyOn(imageLocalStorageService, 'getStoredImagesData').and.returnValue([ + {filename: 'a.png', image: 'faf'} as unknown as ImagesData, + ]); }); it('should check if properties was initialized correctly', () => { expect(component.story.title).toBe(''); expect(component.story.description).toBe(''); expect(component.MAX_CHARS_IN_STORY_TITLE).toBe( - AppConstants.MAX_CHARS_IN_STORY_TITLE); + AppConstants.MAX_CHARS_IN_STORY_TITLE + ); }); it('should check if url fragment already exists', () => { spyOn( storyEditorStateService, - 'updateExistenceOfStoryUrlFragment').and.callFake( - (urlFragment, callback) => callback()); + 'updateExistenceOfStoryUrlFragment' + ).and.callFake((urlFragment, callback) => callback()); spyOn( storyEditorStateService, - 'getStoryWithUrlFragmentExists').and.returnValue(true); + 'getStoryWithUrlFragmentExists' + ).and.returnValue(true); expect(component.storyUrlFragmentExists).toBeFalse(); component.story.urlFragment = 'test-url'; component.onStoryUrlFragmentChange(); expect(component.storyUrlFragmentExists).toBeTrue(); }); - it('should not update story url fragment existence for empty url fragment', - () => { - spyOn(storyEditorStateService, 'updateExistenceOfStoryUrlFragment'); - component.story.urlFragment = ''; - component.onStoryUrlFragmentChange(); - component.save(); - component.cancel(); - expect( - storyEditorStateService.updateExistenceOfStoryUrlFragment - ).not.toHaveBeenCalled(); - }); + it('should not update story url fragment existence for empty url fragment', () => { + spyOn(storyEditorStateService, 'updateExistenceOfStoryUrlFragment'); + component.story.urlFragment = ''; + component.onStoryUrlFragmentChange(); + component.save(); + component.cancel(); + expect( + storyEditorStateService.updateExistenceOfStoryUrlFragment + ).not.toHaveBeenCalled(); + }); it('should check if the story is valid', () => { expect(component.isValid()).toBe(false); diff --git a/core/templates/pages/topic-editor-page/modal-templates/create-new-story-modal.component.ts b/core/templates/pages/topic-editor-page/modal-templates/create-new-story-modal.component.ts index 0e3f02f95019..b82e4812c5ac 100644 --- a/core/templates/pages/topic-editor-page/modal-templates/create-new-story-modal.component.ts +++ b/core/templates/pages/topic-editor-page/modal-templates/create-new-story-modal.component.ts @@ -16,19 +16,19 @@ * @fileoverview Component for create new story modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { AppConstants } from 'app.constants'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { NewlyCreatedStory } from 'domain/topic/newly-created-story.model'; -import { StoryEditorStateService } from 'pages/story-editor-page/services/story-editor-state.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { ImageLocalStorageService } from 'services/image-local-storage.service'; -import { TopicEditorStateService } from '../services/topic-editor-state.service'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {AppConstants} from 'app.constants'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {NewlyCreatedStory} from 'domain/topic/newly-created-story.model'; +import {StoryEditorStateService} from 'pages/story-editor-page/services/story-editor-state.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {ImageLocalStorageService} from 'services/image-local-storage.service'; +import {TopicEditorStateService} from '../services/topic-editor-state.service'; @Component({ selector: 'oppia-create-new-story-modal', - templateUrl: './create-new-story-modal.component.html' + templateUrl: './create-new-story-modal.component.html', }) export class CreateNewStoryModalComponent extends ConfirmOrCancelModal { constructor( @@ -41,38 +41,35 @@ export class CreateNewStoryModalComponent extends ConfirmOrCancelModal { super(ngbActiveModal); } - validUrlFragmentRegex = new RegExp( - AppConstants.VALID_URL_FRAGMENT_REGEX); + validUrlFragmentRegex = new RegExp(AppConstants.VALID_URL_FRAGMENT_REGEX); story = NewlyCreatedStory.createDefault(); MAX_CHARS_IN_STORY_TITLE = AppConstants.MAX_CHARS_IN_STORY_TITLE; - MAX_CHARS_IN_STORY_URL_FRAGMENT = ( - AppConstants.MAX_CHARS_IN_STORY_URL_FRAGMENT); + MAX_CHARS_IN_STORY_URL_FRAGMENT = + AppConstants.MAX_CHARS_IN_STORY_URL_FRAGMENT; - MAX_CHARS_IN_STORY_DESCRIPTION = ( - AppConstants.MAX_CHARS_IN_STORY_DESCRIPTION); + MAX_CHARS_IN_STORY_DESCRIPTION = AppConstants.MAX_CHARS_IN_STORY_DESCRIPTION; - allowedBgColors = ( - AppConstants.ALLOWED_THUMBNAIL_BG_COLORS.story); + allowedBgColors = AppConstants.ALLOWED_THUMBNAIL_BG_COLORS.story; storyUrlFragmentExists = false; hostname = this.windowRef.nativeWindow.location.hostname; - classroomUrlFragment = ( - this.topicEditorStateService.getClassroomUrlFragment()); + classroomUrlFragment = this.topicEditorStateService.getClassroomUrlFragment(); - topicUrlFragment = ( - this.topicEditorStateService.getTopic()?.getUrlFragment()); + topicUrlFragment = this.topicEditorStateService.getTopic()?.getUrlFragment(); onStoryUrlFragmentChange(): void { if (!this.story.urlFragment) { return; } this.storyEditorStateService.updateExistenceOfStoryUrlFragment( - this.story.urlFragment, () => { - this.storyUrlFragmentExists = ( - this.storyEditorStateService.getStoryWithUrlFragmentExists()); - }); + this.story.urlFragment, + () => { + this.storyUrlFragmentExists = + this.storyEditorStateService.getStoryWithUrlFragmentExists(); + } + ); } save(): void { @@ -86,7 +83,8 @@ export class CreateNewStoryModalComponent extends ConfirmOrCancelModal { isValid(): boolean { return Boolean( this.story.isValid() && - this.imageLocalStorageService.getStoredImagesData().length > 0 && - !this.storyUrlFragmentExists); + this.imageLocalStorageService.getStoredImagesData().length > 0 && + !this.storyUrlFragmentExists + ); } } diff --git a/core/templates/pages/topic-editor-page/modal-templates/create-new-subtopic-modal.component.spec.ts b/core/templates/pages/topic-editor-page/modal-templates/create-new-subtopic-modal.component.spec.ts index dc4ba304dc3c..dda90b5c26e3 100644 --- a/core/templates/pages/topic-editor-page/modal-templates/create-new-subtopic-modal.component.spec.ts +++ b/core/templates/pages/topic-editor-page/modal-templates/create-new-subtopic-modal.component.spec.ts @@ -16,24 +16,29 @@ * @fileoverview Unit tests for the create new subtopic modal component. */ -import { Topic } from 'domain/topic/topic-object.model'; -import { ComponentFixture, waitForAsync, TestBed, fakeAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { TopicUpdateService } from 'domain/topic/topic-update.service'; -import { TopicEditorStateService } from 'pages/topic-editor-page/services/topic-editor-state.service'; -import { SubtopicValidationService } from 'pages/topic-editor-page/services/subtopic-validation.service'; -import { AppConstants } from 'app.constants'; -import { CreateNewSubtopicModalComponent } from './create-new-subtopic-modal.component'; -import { Subtopic } from 'domain/topic/subtopic.model'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { SubtopicPage } from 'domain/topic/subtopic-page.model'; +import {Topic} from 'domain/topic/topic-object.model'; +import { + ComponentFixture, + waitForAsync, + TestBed, + fakeAsync, +} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {TopicUpdateService} from 'domain/topic/topic-update.service'; +import {TopicEditorStateService} from 'pages/topic-editor-page/services/topic-editor-state.service'; +import {SubtopicValidationService} from 'pages/topic-editor-page/services/subtopic-validation.service'; +import {AppConstants} from 'app.constants'; +import {CreateNewSubtopicModalComponent} from './create-new-subtopic-modal.component'; +import {Subtopic} from 'domain/topic/subtopic.model'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import {SubtopicPage} from 'domain/topic/subtopic-page.model'; class MockWindowRef { nativeWindow = { location: { - hostname: 'local' - } + hostname: 'local', + }, }; } class MockActiveModal { @@ -48,9 +53,25 @@ class MockActiveModal { class MockTopicEditorStateService { getTopic() { return new Topic( - '', 'Topic name loading', 'Abbrev. name loading', - 'Url Fragment loading', 'Topic description loading', 'en', - [], [], [], 1, 1, [], '', '', {}, false, '', '', [] + '', + 'Topic name loading', + 'Abbrev. name loading', + 'Url Fragment loading', + 'Topic description loading', + 'en', + [], + [], + [], + 1, + 1, + [], + '', + '', + {}, + false, + '', + '', + [] ); } @@ -58,20 +79,17 @@ class MockTopicEditorStateService { return 'non'; } - deleteSubtopicPage() { - } + deleteSubtopicPage() {} get onTopicReinitialized(): EventEmitter { - let topicReinitializedEventEmitter: EventEmitter = ( - new EventEmitter()); + let topicReinitializedEventEmitter: EventEmitter = new EventEmitter(); return topicReinitializedEventEmitter; } - setSubtopicPage() { - } + setSubtopicPage() {} } -describe('create new subtopic modal', function() { +describe('create new subtopic modal', function () { let component: CreateNewSubtopicModalComponent; let fixture: ComponentFixture; let ngbActiveModal: NgbActiveModal; @@ -82,33 +100,31 @@ describe('create new subtopic modal', function() { let DefaultSubtopicPageSchema = { type: 'html', ui_config: { - rows: 100 - } + rows: 100, + }, }; beforeEach(waitForAsync(() => { topicEditorStateService = new MockTopicEditorStateService(); TestBed.configureTestingModule({ - declarations: [ - CreateNewSubtopicModalComponent - ], + declarations: [CreateNewSubtopicModalComponent], providers: [ { provide: WindowRef, - useClass: MockWindowRef + useClass: MockWindowRef, }, { provide: NgbActiveModal, - useClass: MockActiveModal + useClass: MockActiveModal, }, { provide: TopicEditorStateService, - useValue: topicEditorStateService + useValue: topicEditorStateService, }, TopicUpdateService, - SubtopicValidationService + SubtopicValidationService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); beforeEach(() => { @@ -121,18 +137,34 @@ describe('create new subtopic modal', function() { subtopicValidationService = TestBed.inject(SubtopicValidationService); topic = new Topic( - '', 'Topic name loading', 'Abbrev. name loading', - 'Url Fragment loading', 'Topic description loading', 'en', - [], [], [], 1, 1, [], '', '', {}, false, '', '', [] + '', + 'Topic name loading', + 'Abbrev. name loading', + 'Url Fragment loading', + 'Topic description loading', + 'en', + [], + [], + [], + 1, + 1, + [], + '', + '', + {}, + false, + '', + '', + [] ); let subtopic1 = Subtopic.createFromTitle(1, 'Subtopic1'); - topic.getSubtopics = function() { + topic.getSubtopics = function () { return [subtopic1]; }; - topic.getId = function() { + topic.getId = function () { return '1'; }; - topic.getNextSubtopicId = function() { + topic.getNextSubtopicId = function () { return 1; }; spyOn(topicEditorStateService, 'getTopic').and.returnValue(topic); @@ -149,112 +181,143 @@ describe('create new subtopic modal', function() { expect(component.schemaEditorIsShown).toBe(false); expect(component.subtopicUrlFragmentExists).toBe(false); expect(component.errorMsg).toBe(null); - expect(component.MAX_CHARS_IN_SUBTOPIC_TITLE) - .toBe(AppConstants.MAX_CHARS_IN_SUBTOPIC_TITLE); - expect(component.MAX_CHARS_IN_SUBTOPIC_URL_FRAGMENT) - .toBe(AppConstants.MAX_CHARS_IN_SUBTOPIC_URL_FRAGMENT); + expect(component.MAX_CHARS_IN_SUBTOPIC_TITLE).toBe( + AppConstants.MAX_CHARS_IN_SUBTOPIC_TITLE + ); + expect(component.MAX_CHARS_IN_SUBTOPIC_URL_FRAGMENT).toBe( + AppConstants.MAX_CHARS_IN_SUBTOPIC_URL_FRAGMENT + ); component.localValueChange('working fine'); expect(component.htmlData).toBe('working fine'); }); - it('should show Schema editor when user clicks' + - 'on \"Give a description or explanation of the subtopic.\" button', () => { - let SUBTOPIC_PAGE_SCHEMA = component.getSchema(); - expect(SUBTOPIC_PAGE_SCHEMA).toEqual(DefaultSubtopicPageSchema); - - component.showSchemaEditor(); - expect(component.schemaEditorIsShown).toBe(true); - }); - - it('should update editableThumbnailFilename when ' + - 'filename updated in \"Thubmnail Image\" modal', () => { - let newFileName = 'shivamOppiaFile'; - component.updateSubtopicThumbnailFilename(newFileName); - - expect(component.editableThumbnailFilename).toBe(newFileName); - }); - - it('should update ThumbnailBgColor when ' + - 'user select new color in \"Thubmnail Image\" modal', () => { - let newThumbnailBgColor = 'red'; - component.updateSubtopicThumbnailBgColor(newThumbnailBgColor); - - expect(component.editableThumbnailBgColor).toBe(newThumbnailBgColor); - }); - - it('should reset errorMsg when user' + - ' enter data in \"Title\" input area', () => { - component.resetErrorMsg(); + it( + 'should show Schema editor when user clicks' + + 'on "Give a description or explanation of the subtopic." button', + () => { + let SUBTOPIC_PAGE_SCHEMA = component.getSchema(); + expect(SUBTOPIC_PAGE_SCHEMA).toEqual(DefaultSubtopicPageSchema); - expect(component.errorMsg).toBe(null); - }); - - it('should check whether subtopic is valid when' + - ' \"Create Subtopic\" button clicked', () => { - component.editableThumbnailFilename = 'examplefilename'; - component.subtopicTitle = 'title'; - component.htmlData = 'data'; - component.editableUrlFragment = 'url'; + component.showSchemaEditor(); + expect(component.schemaEditorIsShown).toBe(true); + } + ); - let isSubtopicValid = component.isSubtopicValid(); + it( + 'should update editableThumbnailFilename when ' + + 'filename updated in "Thubmnail Image" modal', + () => { + let newFileName = 'shivamOppiaFile'; + component.updateSubtopicThumbnailFilename(newFileName); - spyOn(subtopicValidationService, 'isUrlFragmentValid') - .and.returnValue(true); - expect(isSubtopicValid).toBe(true); - }); + expect(component.editableThumbnailFilename).toBe(newFileName); + } + ); - it('should not create subtopic when \"Cancel\" button clicked', - fakeAsync(() => { - spyOn(topicEditorStateService, 'deleteSubtopicPage'); + it( + 'should update ThumbnailBgColor when ' + + 'user select new color in "Thubmnail Image" modal', + () => { + let newThumbnailBgColor = 'red'; + component.updateSubtopicThumbnailBgColor(newThumbnailBgColor); - component.cancel(); + expect(component.editableThumbnailBgColor).toBe(newThumbnailBgColor); + } + ); - expect(topicEditorStateService.deleteSubtopicPage).toHaveBeenCalled(); - })); + it( + 'should reset errorMsg when user' + ' enter data in "Title" input area', + () => { + component.resetErrorMsg(); - it('should check whether subtopicUrlFragmentExists when user enter data' + - ' in \"Enter the url fragment for the subtopic\" input area', () => { - spyOn(subtopicValidationService, 'doesSubtopicWithUrlFragmentExist') - .and.returnValue(true); - component.checkSubtopicExistence(); + expect(component.errorMsg).toBe(null); + } + ); + + it( + 'should check whether subtopic is valid when' + + ' "Create Subtopic" button clicked', + () => { + component.editableThumbnailFilename = 'examplefilename'; + component.subtopicTitle = 'title'; + component.htmlData = 'data'; + component.editableUrlFragment = 'url'; + + let isSubtopicValid = component.isSubtopicValid(); + + spyOn(subtopicValidationService, 'isUrlFragmentValid').and.returnValue( + true + ); + expect(isSubtopicValid).toBe(true); + } + ); - expect(component.subtopicUrlFragmentExists).toBe(true); - }); + it('should not create subtopic when "Cancel" button clicked', fakeAsync(() => { + spyOn(topicEditorStateService, 'deleteSubtopicPage'); - it('should save create new subtoic when' + - ' \"Create Subtopic\" button clicked', () => { - component.subtopicId = 123; - spyOn(subtopicValidationService, 'checkValidSubtopicName') - .and.returnValue(true); - spyOn(topicUpdateService, 'setSubtopicTitle'); - spyOn(topicUpdateService, 'addSubtopic'); - spyOn(topicUpdateService, 'setSubtopicThumbnailFilename').and.stub(); - spyOn(topicUpdateService, 'setSubtopicThumbnailBgColor').and.stub(); - spyOn(topicUpdateService, 'setSubtopicUrlFragment'); - spyOn(SubtopicPage, 'createDefault').and.callThrough(); - spyOn(topicEditorStateService, 'setSubtopicPage').and.callThrough(); - spyOn(ngbActiveModal, 'close'); - - component.save(); - - expect(topicUpdateService.addSubtopic).toHaveBeenCalled(); - expect(topicUpdateService.setSubtopicTitle).toHaveBeenCalled(); - expect(topicUpdateService.setSubtopicUrlFragment).toHaveBeenCalled(); - expect(SubtopicPage.createDefault).toHaveBeenCalled(); - expect(topicEditorStateService.setSubtopicPage).toHaveBeenCalled(); - expect(ngbActiveModal.close).toHaveBeenCalled(); - }); + component.cancel(); - it('should not close modal if subtopic name is not valid' + - ' when \"Create Subtopic\" button clicked', () => { - spyOn(ngbActiveModal, 'close'); + expect(topicEditorStateService.deleteSubtopicPage).toHaveBeenCalled(); + })); - spyOn(subtopicValidationService, 'checkValidSubtopicName') - .and.returnValue(false); - component.save(); - expect(component.errorMsg) - .toBe('A subtopic with this title already exists'); - expect(ngbActiveModal.close).not.toHaveBeenCalled(); - }); + it( + 'should check whether subtopicUrlFragmentExists when user enter data' + + ' in "Enter the url fragment for the subtopic" input area', + () => { + spyOn( + subtopicValidationService, + 'doesSubtopicWithUrlFragmentExist' + ).and.returnValue(true); + component.checkSubtopicExistence(); + + expect(component.subtopicUrlFragmentExists).toBe(true); + } + ); + + it( + 'should save create new subtoic when' + ' "Create Subtopic" button clicked', + () => { + component.subtopicId = 123; + spyOn( + subtopicValidationService, + 'checkValidSubtopicName' + ).and.returnValue(true); + spyOn(topicUpdateService, 'setSubtopicTitle'); + spyOn(topicUpdateService, 'addSubtopic'); + spyOn(topicUpdateService, 'setSubtopicThumbnailFilename').and.stub(); + spyOn(topicUpdateService, 'setSubtopicThumbnailBgColor').and.stub(); + spyOn(topicUpdateService, 'setSubtopicUrlFragment'); + spyOn(SubtopicPage, 'createDefault').and.callThrough(); + spyOn(topicEditorStateService, 'setSubtopicPage').and.callThrough(); + spyOn(ngbActiveModal, 'close'); + + component.save(); + + expect(topicUpdateService.addSubtopic).toHaveBeenCalled(); + expect(topicUpdateService.setSubtopicTitle).toHaveBeenCalled(); + expect(topicUpdateService.setSubtopicUrlFragment).toHaveBeenCalled(); + expect(SubtopicPage.createDefault).toHaveBeenCalled(); + expect(topicEditorStateService.setSubtopicPage).toHaveBeenCalled(); + expect(ngbActiveModal.close).toHaveBeenCalled(); + } + ); + + it( + 'should not close modal if subtopic name is not valid' + + ' when "Create Subtopic" button clicked', + () => { + spyOn(ngbActiveModal, 'close'); + + spyOn( + subtopicValidationService, + 'checkValidSubtopicName' + ).and.returnValue(false); + component.save(); + expect(component.errorMsg).toBe( + 'A subtopic with this title already exists' + ); + expect(ngbActiveModal.close).not.toHaveBeenCalled(); + } + ); }); diff --git a/core/templates/pages/topic-editor-page/modal-templates/create-new-subtopic-modal.component.ts b/core/templates/pages/topic-editor-page/modal-templates/create-new-subtopic-modal.component.ts index 0473a9572d16..74f161299bcb 100644 --- a/core/templates/pages/topic-editor-page/modal-templates/create-new-subtopic-modal.component.ts +++ b/core/templates/pages/topic-editor-page/modal-templates/create-new-subtopic-modal.component.ts @@ -16,25 +16,26 @@ * @fileoverview Component for create new subtopic modal controller. */ -import { Component, OnInit } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { AppConstants } from 'app.constants'; +import {Component, OnInit} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {AppConstants} from 'app.constants'; import cloneDeep from 'lodash/cloneDeep'; -import { Topic } from 'domain/topic/topic-object.model'; -import { SubtopicPage } from 'domain/topic/subtopic-page.model'; -import { TopicUpdateService } from 'domain/topic/topic-update.service'; -import { TopicEditorStateService } from 'pages/topic-editor-page/services/topic-editor-state.service'; -import { SubtopicValidationService } from 'pages/topic-editor-page/services/subtopic-validation.service'; +import {Topic} from 'domain/topic/topic-object.model'; +import {SubtopicPage} from 'domain/topic/subtopic-page.model'; +import {TopicUpdateService} from 'domain/topic/topic-update.service'; +import {TopicEditorStateService} from 'pages/topic-editor-page/services/topic-editor-state.service'; +import {SubtopicValidationService} from 'pages/topic-editor-page/services/subtopic-validation.service'; @Component({ selector: 'oppia-create-new-subtopic-modal', - templateUrl: './create-new-subtopic-modal.component.html' + templateUrl: './create-new-subtopic-modal.component.html', }) - export class CreateNewSubtopicModalComponent - extends ConfirmOrCancelModal implements OnInit { + extends ConfirmOrCancelModal + implements OnInit +{ // These properties below are initialized using Angular lifecycle hooks // where we need to do non-null assertion. For more information see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -70,25 +71,24 @@ export class CreateNewSubtopicModalComponent ngOnInit(): void { this.topic = this.topicEditorStateService.getTopic(); this.hostname = this.windowRef.nativeWindow.location.hostname; - this.classroomUrlFragment = ( - this.topicEditorStateService.getClassroomUrlFragment()); + this.classroomUrlFragment = + this.topicEditorStateService.getClassroomUrlFragment(); this.SUBTOPIC_PAGE_SCHEMA = { type: 'html', ui_config: { - rows: 100 - } + rows: 100, + }, }; this.htmlData = ''; this.schemaEditorIsShown = false; this.editableThumbnailFilename = ''; this.editableThumbnailBgColor = ''; this.editableUrlFragment = ''; - this.allowedBgColors = ( - AppConstants.ALLOWED_THUMBNAIL_BG_COLORS.subtopic); + this.allowedBgColors = AppConstants.ALLOWED_THUMBNAIL_BG_COLORS.subtopic; this.subtopicId = this.topic.getNextSubtopicId(); this.MAX_CHARS_IN_SUBTOPIC_TITLE = AppConstants.MAX_CHARS_IN_SUBTOPIC_TITLE; - this.MAX_CHARS_IN_SUBTOPIC_URL_FRAGMENT = ( - AppConstants.MAX_CHARS_IN_SUBTOPIC_URL_FRAGMENT); + this.MAX_CHARS_IN_SUBTOPIC_URL_FRAGMENT = + AppConstants.MAX_CHARS_IN_SUBTOPIC_URL_FRAGMENT; this.subtopicTitle = ''; this.errorMsg = null; this.subtopicUrlFragmentExists = false; @@ -103,14 +103,23 @@ export class CreateNewSubtopicModalComponent } addSubtopic(): void { - this.topicUpdateService - .addSubtopic(this.topic, this.subtopicTitle, this.editableUrlFragment); + this.topicUpdateService.addSubtopic( + this.topic, + this.subtopicTitle, + this.editableUrlFragment + ); this.topicUpdateService.setSubtopicThumbnailFilename( - this.topic, this.subtopicId, this.editableThumbnailFilename); + this.topic, + this.subtopicId, + this.editableThumbnailFilename + ); this.topicUpdateService.setSubtopicThumbnailBgColor( - this.topic, this.subtopicId, this.editableThumbnailBgColor); + this.topic, + this.subtopicId, + this.editableThumbnailBgColor + ); } updateSubtopicThumbnailFilename(newThumbnailFilename: string): void { @@ -128,33 +137,39 @@ export class CreateNewSubtopicModalComponent isSubtopicValid(): boolean { return Boolean( this.editableThumbnailFilename && - this.subtopicTitle && - this.htmlData && - this.editableUrlFragment && - this.isUrlFragmentValid()); + this.subtopicTitle && + this.htmlData && + this.editableUrlFragment && + this.isUrlFragmentValid() + ); } cancel(): void { this.topicEditorStateService.deleteSubtopicPage( - this.topic.getId(), this.subtopicId); + this.topic.getId(), + this.subtopicId + ); this.topicEditorStateService.onTopicReinitialized.emit(); this.ngbActiveModal.dismiss('cancel'); } isUrlFragmentValid(): boolean { return this.subtopicValidationService.isUrlFragmentValid( - this.editableUrlFragment); + this.editableUrlFragment + ); } checkSubtopicExistence(): void { - this.subtopicUrlFragmentExists = ( + this.subtopicUrlFragmentExists = this.subtopicValidationService.doesSubtopicWithUrlFragmentExist( - this.editableUrlFragment)); + this.editableUrlFragment + ); } save(): void { - if (!this.subtopicValidationService.checkValidSubtopicName( - this.subtopicTitle)) { + if ( + !this.subtopicValidationService.checkValidSubtopicName(this.subtopicTitle) + ) { this.errorMsg = 'A subtopic with this title already exists'; return; } @@ -162,22 +177,37 @@ export class CreateNewSubtopicModalComponent this.addSubtopic(); this.topicUpdateService.setSubtopicTitle( - this.topic, this.subtopicId, this.subtopicTitle); + this.topic, + this.subtopicId, + this.subtopicTitle + ); this.topicUpdateService.setSubtopicUrlFragment( - this.topic, this.subtopicId, this.editableUrlFragment); + this.topic, + this.subtopicId, + this.editableUrlFragment + ); this.subtopicPage = SubtopicPage.createDefault( - this.topic.getId(), this.subtopicId); + this.topic.getId(), + this.subtopicId + ); let subtitledHtml = cloneDeep( - this.subtopicPage.getPageContents().getSubtitledHtml()); + this.subtopicPage.getPageContents().getSubtitledHtml() + ); subtitledHtml.html = this.htmlData; this.topicUpdateService.setSubtopicPageContentsHtml( - this.subtopicPage, this.subtopicId, subtitledHtml); + this.subtopicPage, + this.subtopicId, + subtitledHtml + ); this.subtopicPage.getPageContents().setHtml(this.htmlData); this.topicEditorStateService.setSubtopicPage(this.subtopicPage); this.topicUpdateService.setSubtopicTitle( - this.topic, this.subtopicId, this.subtopicTitle); + this.topic, + this.subtopicId, + this.subtopicTitle + ); this.ngbActiveModal.close(this.subtopicId); } diff --git a/core/templates/pages/topic-editor-page/modal-templates/delete-story-modal.component.spec.ts b/core/templates/pages/topic-editor-page/modal-templates/delete-story-modal.component.spec.ts index 12458c3864f6..29834e564cb5 100644 --- a/core/templates/pages/topic-editor-page/modal-templates/delete-story-modal.component.spec.ts +++ b/core/templates/pages/topic-editor-page/modal-templates/delete-story-modal.component.spec.ts @@ -16,25 +16,25 @@ * @fileoverview Unit tests for the DeleteStoryModalComponent. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; -import { DeleteStoryModalComponent } from './delete-story-modal.component'; +import {DeleteStoryModalComponent} from './delete-story-modal.component'; -describe('Delete Story Modal Component', function() { +describe('Delete Story Modal Component', function () { let component: DeleteStoryModalComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - DeleteStoryModalComponent + declarations: [DeleteStoryModalComponent], + providers: [ + { + provide: NgbActiveModal, + }, ], - providers: [{ - provide: NgbActiveModal - }], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); diff --git a/core/templates/pages/topic-editor-page/modal-templates/delete-story-modal.component.ts b/core/templates/pages/topic-editor-page/modal-templates/delete-story-modal.component.ts index 836628b07df3..9fb8393352bb 100644 --- a/core/templates/pages/topic-editor-page/modal-templates/delete-story-modal.component.ts +++ b/core/templates/pages/topic-editor-page/modal-templates/delete-story-modal.component.ts @@ -15,19 +15,17 @@ /** * @fileoverview Component for delete story modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-delete-story-modal', - templateUrl: './delete-story-modal.component.html' + templateUrl: './delete-story-modal.component.html', }) export class DeleteStoryModalComponent extends ConfirmOrCancelModal { - constructor( - private ngbActiveModal: NgbActiveModal - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } } diff --git a/core/templates/pages/topic-editor-page/modal-templates/preview-thumbnail.component.spec.ts b/core/templates/pages/topic-editor-page/modal-templates/preview-thumbnail.component.spec.ts index efd0342a3256..2c13b0008d0c 100644 --- a/core/templates/pages/topic-editor-page/modal-templates/preview-thumbnail.component.spec.ts +++ b/core/templates/pages/topic-editor-page/modal-templates/preview-thumbnail.component.spec.ts @@ -16,14 +16,14 @@ * @fileoverview Unit tests for the preview thumbnail component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { ThumbnailDisplayComponent } from 'components/forms/custom-forms-directives/thumbnail-display.component'; -import { ImageUploadHelperService } from 'services/image-upload-helper.service'; -import { PreviewThumbnailComponent } from './preview-thumbnail.component'; -import { ContextService } from 'services/context.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {ThumbnailDisplayComponent} from 'components/forms/custom-forms-directives/thumbnail-display.component'; +import {ImageUploadHelperService} from 'services/image-upload-helper.service'; +import {PreviewThumbnailComponent} from './preview-thumbnail.component'; +import {ContextService} from 'services/context.service'; -describe('Preview Thumbnail Component', function() { +describe('Preview Thumbnail Component', function () { let componentInstance: PreviewThumbnailComponent; let fixture: ComponentFixture; let imageUploadHelperService: ImageUploadHelperService; @@ -33,27 +33,22 @@ describe('Preview Thumbnail Component', function() { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - PreviewThumbnailComponent, - ThumbnailDisplayComponent - ], - providers: [ - ImageUploadHelperService, - ContextService - ] + declarations: [PreviewThumbnailComponent, ThumbnailDisplayComponent], + providers: [ImageUploadHelperService, ContextService], }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(PreviewThumbnailComponent); componentInstance = fixture.componentInstance; - imageUploadHelperService = ( - TestBed.inject(ImageUploadHelperService)) as - jasmine.SpyObj; + imageUploadHelperService = TestBed.inject( + ImageUploadHelperService + ) as jasmine.SpyObj; contextService = TestBed.inject(ContextService); spyOn( - imageUploadHelperService, 'getTrustedResourceUrlForThumbnailFilename') - .and.returnValue(testUrl); + imageUploadHelperService, + 'getTrustedResourceUrlForThumbnailFilename' + ).and.returnValue(testUrl); }); it('should create', () => { diff --git a/core/templates/pages/topic-editor-page/modal-templates/preview-thumbnail.component.ts b/core/templates/pages/topic-editor-page/modal-templates/preview-thumbnail.component.ts index bc7c4f6cb609..7affa3cd4fd3 100644 --- a/core/templates/pages/topic-editor-page/modal-templates/preview-thumbnail.component.ts +++ b/core/templates/pages/topic-editor-page/modal-templates/preview-thumbnail.component.ts @@ -16,14 +16,14 @@ * @fileoverview Component for previewing thumbnails. */ -import { Component, Input } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { ContextService } from 'services/context.service'; -import { ImageUploadHelperService } from 'services/image-upload-helper.service'; +import {Component, Input} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {ContextService} from 'services/context.service'; +import {ImageUploadHelperService} from 'services/image-upload-helper.service'; @Component({ selector: 'oppia-preview-thumbnail', - templateUrl: './preview-thumbnail.component.html' + templateUrl: './preview-thumbnail.component.html', }) export class PreviewThumbnailComponent { // These properties are initialized using Angular lifecycle hooks @@ -49,17 +49,18 @@ export class PreviewThumbnailComponent { if (entityType === undefined) { throw new Error('No image present for preview'); } - this.editableThumbnailDataUrl = ( + this.editableThumbnailDataUrl = this.imageUploadHelperService.getTrustedResourceUrlForThumbnailFilename( this.filename, entityType, this.contextService.getEntityId() - ) - ); + ); } } -angular.module('oppia').directive('oppiaPreviewThumbnail', +angular.module('oppia').directive( + 'oppiaPreviewThumbnail', downgradeComponent({ - component: PreviewThumbnailComponent - }) as angular.IDirectiveFactory); + component: PreviewThumbnailComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/topic-editor-page/modal-templates/questions-list-select-skill-and-difficulty-modal.component.spec.ts b/core/templates/pages/topic-editor-page/modal-templates/questions-list-select-skill-and-difficulty-modal.component.spec.ts index 40dc4fda5e81..b0e60a087871 100644 --- a/core/templates/pages/topic-editor-page/modal-templates/questions-list-select-skill-and-difficulty-modal.component.spec.ts +++ b/core/templates/pages/topic-editor-page/modal-templates/questions-list-select-skill-and-difficulty-modal.component.spec.ts @@ -17,13 +17,13 @@ * skill and difficulty modal component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ShortSkillSummary } from 'domain/skill/short-skill-summary.model'; -import { SkillDifficulty } from 'domain/skill/skill-difficulty.model'; -import { QuestionsListSelectSkillAndDifficultyModalComponent } from './questions-list-select-skill-and-difficulty-modal.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ShortSkillSummary} from 'domain/skill/short-skill-summary.model'; +import {SkillDifficulty} from 'domain/skill/skill-difficulty.model'; +import {QuestionsListSelectSkillAndDifficultyModalComponent} from './questions-list-select-skill-and-difficulty-modal.component'; class MockActiveModal { close(): void { @@ -37,38 +37,41 @@ class MockActiveModal { describe('Questions List Select Skill And Difficulty Modal Component', () => { let component: QuestionsListSelectSkillAndDifficultyModalComponent; - let fixture: - ComponentFixture; + let fixture: ComponentFixture; let ngbActiveModal: NgbActiveModal; - let allSkillSummaries = [{ - id: '1', - description: 'Skill 1 description', - language_code: 'en', - version: 1, - misconception_count: 2, - worked_examples_count: 2, - skill_model_created_on: 2, - skill_model_last_updated: 2, - }, { - id: '2', - description: 'Skill 2 description', - language_code: 'en', - version: 1, - misconception_count: 2, - worked_examples_count: 2, - skill_model_created_on: 2, - skill_model_last_updated: 2, - }, { - id: '3', - description: 'Skill 3 description', - language_code: 'en', - version: 1, - misconception_count: 2, - worked_examples_count: 2, - skill_model_created_on: 2, - skill_model_last_updated: 2, - }]; + let allSkillSummaries = [ + { + id: '1', + description: 'Skill 1 description', + language_code: 'en', + version: 1, + misconception_count: 2, + worked_examples_count: 2, + skill_model_created_on: 2, + skill_model_last_updated: 2, + }, + { + id: '2', + description: 'Skill 2 description', + language_code: 'en', + version: 1, + misconception_count: 2, + worked_examples_count: 2, + skill_model_created_on: 2, + skill_model_last_updated: 2, + }, + { + id: '3', + description: 'Skill 3 description', + language_code: 'en', + version: 1, + misconception_count: 2, + worked_examples_count: 2, + skill_model_created_on: 2, + skill_model_last_updated: 2, + }, + ]; let countOfSkillsToPrioritize = 2; let currentMode: string; let linkedSkillsWithDifficulty: SkillDifficulty[] = []; @@ -77,27 +80,27 @@ describe('Questions List Select Skill And Difficulty Modal Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - QuestionsListSelectSkillAndDifficultyModalComponent - ], + declarations: [QuestionsListSelectSkillAndDifficultyModalComponent], providers: [ { provide: NgbActiveModal, - useClass: MockActiveModal - } + useClass: MockActiveModal, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent( - QuestionsListSelectSkillAndDifficultyModalComponent); + QuestionsListSelectSkillAndDifficultyModalComponent + ); component = fixture.componentInstance; ngbActiveModal = TestBed.inject(NgbActiveModal); - allSkillSummaries.map(summary => ( - ShortSkillSummary.create(summary.id, summary.description))); + allSkillSummaries.map(summary => + ShortSkillSummary.create(summary.id, summary.description) + ); component.allSkillSummaries = allSkillSummaries; component.countOfSkillsToPrioritize = countOfSkillsToPrioritize; @@ -108,20 +111,26 @@ describe('Questions List Select Skill And Difficulty Modal Component', () => { fixture.detectChanges(); }); - it('should initialize component properties after component' + - ' is initialized', () => { - expect(component.countOfSkillsToPrioritize).toBe( - countOfSkillsToPrioritize); - expect(component.instructionMessage).toBe( - 'Select the skill(s) to link the question to:'); - expect(component.currentMode).toBe(currentMode); - expect(component.linkedSkillsWithDifficulty).toEqual( - linkedSkillsWithDifficulty); - expect(component.skillSummaries).toBe(allSkillSummaries); - expect(component.skillSummariesInitial.length).toBe(2); - expect(component.skillSummariesFinal.length).toBe(1); - expect(component.skillIdToRubricsObject).toEqual(skillIdToRubricsObject); - }); + it( + 'should initialize component properties after component' + + ' is initialized', + () => { + expect(component.countOfSkillsToPrioritize).toBe( + countOfSkillsToPrioritize + ); + expect(component.instructionMessage).toBe( + 'Select the skill(s) to link the question to:' + ); + expect(component.currentMode).toBe(currentMode); + expect(component.linkedSkillsWithDifficulty).toEqual( + linkedSkillsWithDifficulty + ); + expect(component.skillSummaries).toBe(allSkillSummaries); + expect(component.skillSummariesInitial.length).toBe(2); + expect(component.skillSummariesFinal.length).toBe(1); + expect(component.skillIdToRubricsObject).toEqual(skillIdToRubricsObject); + } + ); it('should toggle skill selection when clicking on it', () => { expect(component.linkedSkillsWithDifficulty.length).toBe(0); @@ -138,40 +147,40 @@ describe('Questions List Select Skill And Difficulty Modal Component', () => { expect(component.linkedSkillsWithDifficulty.length).toBe(0); }); - it('should change view mode to select skill when changing view', - () => { - expect(component.currentMode).toBe(currentMode); + it('should change view mode to select skill when changing view', () => { + expect(component.currentMode).toBe(currentMode); - component.goToSelectSkillView(); + component.goToSelectSkillView(); - expect(component.currentMode).toBe('MODE_SELECT_SKILL'); - }); + expect(component.currentMode).toBe('MODE_SELECT_SKILL'); + }); - it('should change view mode to select difficulty after selecting a skill', - () => { - expect(component.currentMode).toBe(currentMode); + it('should change view mode to select difficulty after selecting a skill', () => { + expect(component.currentMode).toBe(currentMode); - component.goToNextStep(); + component.goToNextStep(); - expect(component.currentMode).toBe('MODE_SELECT_DIFFICULTY'); - }); + expect(component.currentMode).toBe('MODE_SELECT_DIFFICULTY'); + }); - it('should select skill and its difficulty properly when closing the modal', - () => { - spyOn(ngbActiveModal, 'close'); - let summary = allSkillSummaries[1]; - component.selectOrDeselectSkill(summary); + it('should select skill and its difficulty properly when closing the modal', () => { + spyOn(ngbActiveModal, 'close'); + let summary = allSkillSummaries[1]; + component.selectOrDeselectSkill(summary); - component.startQuestionCreation(); + component.startQuestionCreation(); - expect(ngbActiveModal.close).toHaveBeenCalledWith([ - SkillDifficulty.create( - allSkillSummaries[1].id, allSkillSummaries[1].description, 0.6) - ]); + expect(ngbActiveModal.close).toHaveBeenCalledWith([ + SkillDifficulty.create( + allSkillSummaries[1].id, + allSkillSummaries[1].description, + 0.6 + ), + ]); - // Remove summary to not affect other specs. - component.selectOrDeselectSkill(summary); - }); + // Remove summary to not affect other specs. + component.selectOrDeselectSkill(summary); + }); it('should filter the skills', () => { component.filterSkills('Skill 1 description'); diff --git a/core/templates/pages/topic-editor-page/modal-templates/questions-list-select-skill-and-difficulty-modal.component.ts b/core/templates/pages/topic-editor-page/modal-templates/questions-list-select-skill-and-difficulty-modal.component.ts index 4ef0d284f0c0..052d32c97b4d 100644 --- a/core/templates/pages/topic-editor-page/modal-templates/questions-list-select-skill-and-difficulty-modal.component.ts +++ b/core/templates/pages/topic-editor-page/modal-templates/questions-list-select-skill-and-difficulty-modal.component.ts @@ -17,13 +17,13 @@ * difficulty modal. */ -import { Component, Input, OnInit } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { AppConstants } from 'app.constants'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { QuestionsListConstants } from 'components/question-directives/questions-list/questions-list.constants'; -import { SkillDifficulty } from 'domain/skill/skill-difficulty.model'; -import { SkillSummaryBackendDict } from 'domain/skill/skill-summary.model'; +import {Component, Input, OnInit} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {AppConstants} from 'app.constants'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {QuestionsListConstants} from 'components/question-directives/questions-list/questions-list.constants'; +import {SkillDifficulty} from 'domain/skill/skill-difficulty.model'; +import {SkillSummaryBackendDict} from 'domain/skill/skill-summary.model'; interface Summary { id: string; @@ -33,10 +33,12 @@ interface Summary { @Component({ selector: 'oppia-questions-list-select-skill-and-difficulty-modal', templateUrl: - './questions-list-select-skill-and-difficulty-modal.component.html' + './questions-list-select-skill-and-difficulty-modal.component.html', }) export class QuestionsListSelectSkillAndDifficultyModalComponent - extends ConfirmOrCancelModal implements OnInit { + extends ConfirmOrCancelModal + implements OnInit +{ // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -55,15 +57,12 @@ export class QuestionsListSelectSkillAndDifficultyModalComponent MODE_SELECT_SKILL!: string; skillsToShow: SkillSummaryBackendDict[] = []; - constructor( - private ngbActiveModal: NgbActiveModal - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } ngOnInit(): void { - this.instructionMessage = ( - 'Select the skill(s) to link the question to:'); + this.instructionMessage = 'Select the skill(s) to link the question to:'; this.skillSummaries = this.allSkillSummaries; this.skillSummariesInitial = []; this.skillSummariesFinal = []; @@ -75,11 +74,9 @@ export class QuestionsListSelectSkillAndDifficultyModalComponent for (let idx = 0; idx < this.allSkillSummaries.length; idx++) { if (idx < this.countOfSkillsToPrioritize) { - this.skillSummariesInitial.push( - this.allSkillSummaries[idx]); + this.skillSummariesInitial.push(this.allSkillSummaries[idx]); } else { - this.skillSummariesFinal.push( - this.allSkillSummaries[idx]); + this.skillSummariesFinal.push(this.allSkillSummaries[idx]); } } } @@ -92,7 +89,7 @@ export class QuestionsListSelectSkillAndDifficultyModalComponent skillSelector = skillSelector.toLowerCase(); this.skillsToShow = this.skillSummariesInitial.filter( - option => (option.description.toLowerCase().indexOf(skillSelector) >= 0) + option => option.description.toLowerCase().indexOf(skillSelector) >= 0 ); } @@ -104,14 +101,18 @@ export class QuestionsListSelectSkillAndDifficultyModalComponent if (!this.isSkillSelected(summary.id)) { this.linkedSkillsWithDifficulty.push( SkillDifficulty.create( - summary.id, summary.description, - this.DEFAULT_SKILL_DIFFICULTY)); + summary.id, + summary.description, + this.DEFAULT_SKILL_DIFFICULTY + ) + ); this.selectedSkills.push(summary.id); } else { - let idIndex = this.linkedSkillsWithDifficulty.map( - (linkedSkillWithDifficulty) => { + let idIndex = this.linkedSkillsWithDifficulty + .map(linkedSkillWithDifficulty => { return linkedSkillWithDifficulty.getId(); - }).indexOf(summary.id); + }) + .indexOf(summary.id); this.linkedSkillsWithDifficulty.splice(idIndex, 1); let index = this.selectedSkills.indexOf(summary.id); this.selectedSkills.splice(index, 1); @@ -119,7 +120,8 @@ export class QuestionsListSelectSkillAndDifficultyModalComponent } changeSkillWithDifficulty( - newSkillWithDifficulty: SkillDifficulty, index: number + newSkillWithDifficulty: SkillDifficulty, + index: number ): void { this.linkedSkillsWithDifficulty[index] = newSkillWithDifficulty; } diff --git a/core/templates/pages/topic-editor-page/modal-templates/questions-opportunities-select-difficulty-modal.component.spec.ts b/core/templates/pages/topic-editor-page/modal-templates/questions-opportunities-select-difficulty-modal.component.spec.ts index f4ce72f3c967..8b338ab98ae6 100644 --- a/core/templates/pages/topic-editor-page/modal-templates/questions-opportunities-select-difficulty-modal.component.spec.ts +++ b/core/templates/pages/topic-editor-page/modal-templates/questions-opportunities-select-difficulty-modal.component.spec.ts @@ -17,21 +17,27 @@ * select difficulty modal component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConceptCardBackendDict } from 'domain/skill/concept-card.model'; -import { MisconceptionBackendDict } from 'domain/skill/MisconceptionObjectFactory'; -import { RubricBackendDict } from 'domain/skill/rubric.model'; -import { SkillBackendApiService } from 'domain/skill/skill-backend-api.service'; -import { SkillDifficultyBackendDict } from 'domain/skill/skill-difficulty.model'; -import { Skill, SkillObjectFactory } from 'domain/skill/SkillObjectFactory'; -import { ImageFile } from 'domain/utilities/image-file.model'; -import { ExtractImageFilenamesFromModelService } from 'pages/exploration-player-page/services/extract-image-filenames-from-model.service'; -import { AlertsService } from 'services/alerts.service'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; -import { QuestionsOpportunitiesSelectDifficultyModalComponent } from './questions-opportunities-select-difficulty-modal.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConceptCardBackendDict} from 'domain/skill/concept-card.model'; +import {MisconceptionBackendDict} from 'domain/skill/MisconceptionObjectFactory'; +import {RubricBackendDict} from 'domain/skill/rubric.model'; +import {SkillBackendApiService} from 'domain/skill/skill-backend-api.service'; +import {SkillDifficultyBackendDict} from 'domain/skill/skill-difficulty.model'; +import {Skill, SkillObjectFactory} from 'domain/skill/SkillObjectFactory'; +import {ImageFile} from 'domain/utilities/image-file.model'; +import {ExtractImageFilenamesFromModelService} from 'pages/exploration-player-page/services/extract-image-filenames-from-model.service'; +import {AlertsService} from 'services/alerts.service'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; +import {QuestionsOpportunitiesSelectDifficultyModalComponent} from './questions-opportunities-select-difficulty-modal.component'; class MockActiveModal { close(): void { @@ -58,169 +64,174 @@ class MockReaderObject { } } -describe( - 'Questions Opportunities Select Difficulty Modal Component', () => { - let component: - QuestionsOpportunitiesSelectDifficultyModalComponent; - let fixture: - ComponentFixture< - QuestionsOpportunitiesSelectDifficultyModalComponent>; - let alertsService: AlertsService; - let assetsBackendApiService: AssetsBackendApiService; - let ngbActiveModal: NgbActiveModal; - let skillBackendApiService: SkillBackendApiService; - let skillObjectFactory: SkillObjectFactory; - let extractImageFilenamesFromModelService: - ExtractImageFilenamesFromModelService; - let mockImageFile: ImageFile; - let mockBlob: Blob; +describe('Questions Opportunities Select Difficulty Modal Component', () => { + let component: QuestionsOpportunitiesSelectDifficultyModalComponent; + let fixture: ComponentFixture; + let alertsService: AlertsService; + let assetsBackendApiService: AssetsBackendApiService; + let ngbActiveModal: NgbActiveModal; + let skillBackendApiService: SkillBackendApiService; + let skillObjectFactory: SkillObjectFactory; + let extractImageFilenamesFromModelService: ExtractImageFilenamesFromModelService; + let mockImageFile: ImageFile; + let mockBlob: Blob; - let misconceptionDict1: MisconceptionBackendDict; - let rubricDict: RubricBackendDict; - let skill: Skill; - let skillContentsDict: ConceptCardBackendDict; - let skillDifficulties: string[] = ['easy', 'medium']; - let skillId: string = 'skill_1'; + let misconceptionDict1: MisconceptionBackendDict; + let rubricDict: RubricBackendDict; + let skill: Skill; + let skillContentsDict: ConceptCardBackendDict; + let skillDifficulties: string[] = ['easy', 'medium']; + let skillId: string = 'skill_1'; - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - imports: [HttpClientTestingModule], - declarations: [ - QuestionsOpportunitiesSelectDifficultyModalComponent - ], - providers: [ - AlertsService, - AssetsBackendApiService, - ExtractImageFilenamesFromModelService, - { - provide: NgbActiveModal, - useClass: MockActiveModal - }, - SkillBackendApiService - ], - schemas: [NO_ERRORS_SCHEMA] - }).compileComponents(); - })); + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + declarations: [QuestionsOpportunitiesSelectDifficultyModalComponent], + providers: [ + AlertsService, + AssetsBackendApiService, + ExtractImageFilenamesFromModelService, + { + provide: NgbActiveModal, + useClass: MockActiveModal, + }, + SkillBackendApiService, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + })); - describe('when fetching skill successfully', () => { - beforeEach(() => { - fixture = TestBed.createComponent( - QuestionsOpportunitiesSelectDifficultyModalComponent); - component = fixture.componentInstance; - alertsService = TestBed.inject(AlertsService); - assetsBackendApiService = TestBed.inject(AssetsBackendApiService); - ngbActiveModal = TestBed.inject(NgbActiveModal); - skillBackendApiService = TestBed.inject(SkillBackendApiService); - skillObjectFactory = TestBed.inject(SkillObjectFactory); - extractImageFilenamesFromModelService = TestBed.inject( - ExtractImageFilenamesFromModelService); - - misconceptionDict1 = { - id: 2, - name: 'test name', - notes: 'test notes', - feedback: 'test feedback', - must_be_addressed: true - }; - rubricDict = { - difficulty: skillDifficulties[0], - explanations: ['explanation'] - }; - skillContentsDict = { - explanation: { - html: 'test explanation', - content_id: 'explanation', - }, - worked_examples: [], - recorded_voiceovers: { - voiceovers_mapping: {} - } - }; - skill = skillObjectFactory.createFromBackendDict({ - id: skillId, - description: 'Skill 1 description', - misconceptions: [misconceptionDict1], - rubrics: [rubricDict], - skill_contents: skillContentsDict, - language_code: 'en', - version: 1, - next_misconception_id: 3, - prerequisite_skill_ids: [], - all_questions_merged: true, - superseding_skill_id: 'skill', - }); + describe('when fetching skill successfully', () => { + beforeEach(() => { + fixture = TestBed.createComponent( + QuestionsOpportunitiesSelectDifficultyModalComponent + ); + component = fixture.componentInstance; + alertsService = TestBed.inject(AlertsService); + assetsBackendApiService = TestBed.inject(AssetsBackendApiService); + ngbActiveModal = TestBed.inject(NgbActiveModal); + skillBackendApiService = TestBed.inject(SkillBackendApiService); + skillObjectFactory = TestBed.inject(SkillObjectFactory); + extractImageFilenamesFromModelService = TestBed.inject( + ExtractImageFilenamesFromModelService + ); - mockImageFile = new ImageFile('dummyImg.png', mockBlob); - spyOn( - extractImageFilenamesFromModelService, - 'getImageFilenamesInSkill').and.returnValue(['dummyImg.png']); - spyOn(assetsBackendApiService, 'loadImage').and.returnValue( - Promise.resolve(mockImageFile)); - // This throws "Argument of type 'MockReaderObject' is not assignable - // to parameter of type 'FileReader'.". We need to suppress this error - // because 'FileReader' has around 15 more properties. We have only - // defined the properties we need in 'MockReaderObject'. - // @ts-expect-error - spyOn(window, 'FileReader').and.returnValue(new MockReaderObject()); + misconceptionDict1 = { + id: 2, + name: 'test name', + notes: 'test notes', + feedback: 'test feedback', + must_be_addressed: true, + }; + rubricDict = { + difficulty: skillDifficulties[0], + explanations: ['explanation'], + }; + skillContentsDict = { + explanation: { + html: 'test explanation', + content_id: 'explanation', + }, + worked_examples: [], + recorded_voiceovers: { + voiceovers_mapping: {}, + }, + }; + skill = skillObjectFactory.createFromBackendDict({ + id: skillId, + description: 'Skill 1 description', + misconceptions: [misconceptionDict1], + rubrics: [rubricDict], + skill_contents: skillContentsDict, + language_code: 'en', + version: 1, + next_misconception_id: 3, + prerequisite_skill_ids: [], + all_questions_merged: true, + superseding_skill_id: 'skill', }); - it('should initialize properties after component is' + - ' initialized', fakeAsync(() => { + mockImageFile = new ImageFile('dummyImg.png', mockBlob); + spyOn( + extractImageFilenamesFromModelService, + 'getImageFilenamesInSkill' + ).and.returnValue(['dummyImg.png']); + spyOn(assetsBackendApiService, 'loadImage').and.returnValue( + Promise.resolve(mockImageFile) + ); + // This throws "Argument of type 'MockReaderObject' is not assignable + // to parameter of type 'FileReader'.". We need to suppress this error + // because 'FileReader' has around 15 more properties. We have only + // defined the properties we need in 'MockReaderObject'. + // @ts-expect-error + spyOn(window, 'FileReader').and.returnValue(new MockReaderObject()); + }); + + it( + 'should initialize properties after component is' + ' initialized', + fakeAsync(() => { spyOn(skillBackendApiService, 'fetchSkillAsync').and.returnValue( Promise.resolve({ skill: skill, assignedSkillTopicData: {}, groupedSkillSummaries: {}, - })); + }) + ); component.ngOnInit(); tick(); expect(component.skill).toEqual(skill); - })); + }) + ); - it('should create a question and select its difficulty when closing' + - ' the modal', fakeAsync(() => { + it( + 'should create a question and select its difficulty when closing' + + ' the modal', + fakeAsync(() => { spyOn(ngbActiveModal, 'close'); spyOn(skillBackendApiService, 'fetchSkillAsync').and.returnValue( Promise.resolve({ skill: skill, assignedSkillTopicData: {}, groupedSkillSummaries: {}, - })); - component.linkedSkillsWithDifficulty = [{ - _id: '1', - _description: 'desc', - _difficulty: 0.6, + }) + ); + component.linkedSkillsWithDifficulty = [ + { + _id: '1', + _description: 'desc', + _difficulty: 0.6, - toBackendDict(): SkillDifficultyBackendDict { - return { - id: '1', - description: 'desc', - difficulty: 0.6, - }; - }, + toBackendDict(): SkillDifficultyBackendDict { + return { + id: '1', + description: 'desc', + difficulty: 0.6, + }; + }, - getDescription(): string { - return 'desc'; - }, + getDescription(): string { + return 'desc'; + }, - getDifficulty(): number { - return 0.6; - }, + getDifficulty(): number { + return 0.6; + }, - setDifficulty(): void { - return; - }, + setDifficulty(): void { + return; + }, - setDescription(): void { - return; - }, + setDescription(): void { + return; + }, - getId(): string { - return '1'; - } - }]; + getId(): string { + return '1'; + }, + }, + ]; component.ngOnInit(); tick(); @@ -228,31 +239,35 @@ describe( expect(ngbActiveModal.close).toHaveBeenCalledWith({ skill: skill, - skillDifficulty: 0.6 + skillDifficulty: 0.6, }); - })); - }); + }) + ); + }); - describe('when fetching skill fails', () => { - beforeEach(() => { - fixture = TestBed.createComponent( - QuestionsOpportunitiesSelectDifficultyModalComponent); - component = fixture.componentInstance; - alertsService = TestBed.inject(AlertsService); - ngbActiveModal = TestBed.inject(NgbActiveModal); - skillBackendApiService = TestBed.inject(SkillBackendApiService); - }); + describe('when fetching skill fails', () => { + beforeEach(() => { + fixture = TestBed.createComponent( + QuestionsOpportunitiesSelectDifficultyModalComponent + ); + component = fixture.componentInstance; + alertsService = TestBed.inject(AlertsService); + ngbActiveModal = TestBed.inject(NgbActiveModal); + skillBackendApiService = TestBed.inject(SkillBackendApiService); + }); - it('should shows a warning error', fakeAsync(() => { - let addWarningSpy = spyOn(alertsService, 'addWarning'); - spyOn(skillBackendApiService, 'fetchSkillAsync').and.returnValue( - Promise.reject('It was not possible to fetch the skill')); + it('should shows a warning error', fakeAsync(() => { + let addWarningSpy = spyOn(alertsService, 'addWarning'); + spyOn(skillBackendApiService, 'fetchSkillAsync').and.returnValue( + Promise.reject('It was not possible to fetch the skill') + ); - component.ngOnInit(); - tick(); + component.ngOnInit(); + tick(); - expect(addWarningSpy.calls.allArgs()[0]).toEqual( - ['Error populating skill: It was not possible to fetch the skill.']); - })); - }); + expect(addWarningSpy.calls.allArgs()[0]).toEqual([ + 'Error populating skill: It was not possible to fetch the skill.', + ]); + })); }); +}); diff --git a/core/templates/pages/topic-editor-page/modal-templates/questions-opportunities-select-difficulty-modal.component.ts b/core/templates/pages/topic-editor-page/modal-templates/questions-opportunities-select-difficulty-modal.component.ts index 93f413cc4fee..3bba8e6a92cf 100644 --- a/core/templates/pages/topic-editor-page/modal-templates/questions-opportunities-select-difficulty-modal.component.ts +++ b/core/templates/pages/topic-editor-page/modal-templates/questions-opportunities-select-difficulty-modal.component.ts @@ -16,27 +16,29 @@ * @fileoverview Component for questions opportunities select difficulty modal. */ -import { Component, Input, OnInit } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { AppConstants } from 'app.constants'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { Rubric } from 'domain/skill/rubric.model'; -import { SkillBackendApiService } from 'domain/skill/skill-backend-api.service'; -import { SkillDifficulty } from 'domain/skill/skill-difficulty.model'; -import { Skill } from 'domain/skill/SkillObjectFactory'; -import { ImageFile } from 'domain/utilities/image-file.model'; -import { ExtractImageFilenamesFromModelService } from 'pages/exploration-player-page/services/extract-image-filenames-from-model.service'; -import { AlertsService } from 'services/alerts.service'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; -import { ImageLocalStorageService } from 'services/image-local-storage.service'; +import {Component, Input, OnInit} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {AppConstants} from 'app.constants'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Rubric} from 'domain/skill/rubric.model'; +import {SkillBackendApiService} from 'domain/skill/skill-backend-api.service'; +import {SkillDifficulty} from 'domain/skill/skill-difficulty.model'; +import {Skill} from 'domain/skill/SkillObjectFactory'; +import {ImageFile} from 'domain/utilities/image-file.model'; +import {ExtractImageFilenamesFromModelService} from 'pages/exploration-player-page/services/extract-image-filenames-from-model.service'; +import {AlertsService} from 'services/alerts.service'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; +import {ImageLocalStorageService} from 'services/image-local-storage.service'; @Component({ selector: 'oppia-questions-opportunities-select-difficulty-modal', templateUrl: - './questions-opportunities-select-difficulty-modal.component.html' + './questions-opportunities-select-difficulty-modal.component.html', }) export class QuestionsOpportunitiesSelectDifficultyModalComponent - extends ConfirmOrCancelModal implements OnInit { + extends ConfirmOrCancelModal + implements OnInit +{ // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -49,8 +51,7 @@ export class QuestionsOpportunitiesSelectDifficultyModalComponent constructor( private alertsService: AlertsService, private assetsBackendApiService: AssetsBackendApiService, - private extractImageFilenamesFromModelService: - ExtractImageFilenamesFromModelService, + private extractImageFilenamesFromModelService: ExtractImageFilenamesFromModelService, private imageLocalStorageService: ImageLocalStorageService, private ngbActiveModal: NgbActiveModal, private skillBackendApiService: SkillBackendApiService @@ -59,10 +60,9 @@ export class QuestionsOpportunitiesSelectDifficultyModalComponent } ngOnInit(): void { - this.instructionMessage = ( - 'Select the skill(s) to link the question to:'); - this.skillBackendApiService.fetchSkillAsync(this.skillId) - .then((backendSkillObject) => { + this.instructionMessage = 'Select the skill(s) to link the question to:'; + this.skillBackendApiService.fetchSkillAsync(this.skillId).then( + backendSkillObject => { this.skill = backendSkillObject.skill; // Skills have SubtitledHtml fields that can contain images. In // order to render them in the contributor dashboard, we parse the @@ -71,13 +71,18 @@ export class QuestionsOpportunitiesSelectDifficultyModalComponent // storage. The image components will use the data from the local // storage to render the image. let imageFileFetchPromises: Promise[] = []; - let imageFilenames = ( + let imageFilenames = this.extractImageFilenamesFromModelService.getImageFilenamesInSkill( - this.skill)); + this.skill + ); imageFilenames.forEach(imageFilename => { - imageFileFetchPromises.push(this.assetsBackendApiService.loadImage( - AppConstants.ENTITY_TYPE.SKILL, this.skillId, - imageFilename)); + imageFileFetchPromises.push( + this.assetsBackendApiService.loadImage( + AppConstants.ENTITY_TYPE.SKILL, + this.skillId, + imageFilename + ) + ); }); Promise.all(imageFileFetchPromises).then(files => { files.forEach(file => { @@ -90,21 +95,24 @@ export class QuestionsOpportunitiesSelectDifficultyModalComponent }); this.linkedSkillsWithDifficulty = [ SkillDifficulty.create( - this.skillId, this.skill.getDescription(), - AppConstants.DEFAULT_SKILL_DIFFICULTY) + this.skillId, + this.skill.getDescription(), + AppConstants.DEFAULT_SKILL_DIFFICULTY + ), ]; this.skillIdToRubricsObject = {}; - this.skillIdToRubricsObject[this.skillId] = ( - this.skill.getRubrics()); + this.skillIdToRubricsObject[this.skillId] = this.skill.getRubrics(); }); - }, (error) => { - this.alertsService.addWarning( - `Error populating skill: ${error}.`); - }); + }, + error => { + this.alertsService.addWarning(`Error populating skill: ${error}.`); + } + ); } changeSkillWithDifficulty( - newSkillWithDifficulty: SkillDifficulty, idx: number + newSkillWithDifficulty: SkillDifficulty, + idx: number ): void { this.linkedSkillsWithDifficulty[idx] = newSkillWithDifficulty; } @@ -112,8 +120,7 @@ export class QuestionsOpportunitiesSelectDifficultyModalComponent startQuestionCreation(): void { const result = { skill: this.skill, - skillDifficulty: - this.linkedSkillsWithDifficulty[0].getDifficulty() + skillDifficulty: this.linkedSkillsWithDifficulty[0].getDifficulty(), }; this.ngbActiveModal.close(result); } diff --git a/core/templates/pages/topic-editor-page/modal-templates/rearrange-skills-in-subtopics-modal.component.spec.ts b/core/templates/pages/topic-editor-page/modal-templates/rearrange-skills-in-subtopics-modal.component.spec.ts index 0255d75359a8..cd882d422fb4 100644 --- a/core/templates/pages/topic-editor-page/modal-templates/rearrange-skills-in-subtopics-modal.component.spec.ts +++ b/core/templates/pages/topic-editor-page/modal-templates/rearrange-skills-in-subtopics-modal.component.spec.ts @@ -16,18 +16,18 @@ * @fileoverview Unit tests for Rearrange Skills In Subtopics Modal. */ -import { CdkDrag, CdkDragDrop, CdkDropList } from '@angular/cdk/drag-drop'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/compiler'; -import { EventEmitter } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ShortSkillSummary } from 'domain/skill/short-skill-summary.model'; -import { Subtopic } from 'domain/topic/subtopic.model'; -import { TopicUpdateService } from 'domain/topic/topic-update.service'; -import { Topic, TopicBackendDict } from 'domain/topic/topic-object.model'; -import { TopicEditorStateService } from '../services/topic-editor-state.service'; -import { RearrangeSkillsInSubtopicsModalComponent } from './rearrange-skills-in-subtopics-modal.component'; +import {CdkDrag, CdkDragDrop, CdkDropList} from '@angular/cdk/drag-drop'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/compiler'; +import {EventEmitter} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ShortSkillSummary} from 'domain/skill/short-skill-summary.model'; +import {Subtopic} from 'domain/topic/subtopic.model'; +import {TopicUpdateService} from 'domain/topic/topic-update.service'; +import {Topic, TopicBackendDict} from 'domain/topic/topic-object.model'; +import {TopicEditorStateService} from '../services/topic-editor-state.service'; +import {RearrangeSkillsInSubtopicsModalComponent} from './rearrange-skills-in-subtopics-modal.component'; class MockActiveModal { close(): void { @@ -47,41 +47,44 @@ interface ContainerModel { class DragAndDropEventClass { createInContainerEvent( - containerId: string, data: T[], fromIndex: number, toIndex: number + containerId: string, + data: T[], + fromIndex: number, + toIndex: number ): CdkDragDrop { const event = this.createEvent(fromIndex, toIndex); - const container = { id: containerId, data: data }; + const container = {id: containerId, data: data}; event.container = container as CdkDropList; event.previousContainer = event.container; - event.item = { data: data[fromIndex] } as CdkDrag; + event.item = {data: data[fromIndex]} as CdkDrag; return event; } createCrossContainerEvent( - from: ContainerModel, to: ContainerModel + from: ContainerModel, + to: ContainerModel ): CdkDragDrop { const event = this.createEvent(from.index, to.index); event.container = this.createContainer(to); event.previousContainer = this.createContainer(from); - event.item = { data: from.data[from.index] } as CdkDrag; + event.item = {data: from.data[from.index]} as CdkDrag; return event; } private createEvent( - previousIndex: number, currentIndex: number + previousIndex: number, + currentIndex: number ): CdkDragDrop { return { previousIndex: previousIndex, currentIndex: currentIndex, isPointerOverContainer: true, - distance: { x: 0, y: 0 } + distance: {x: 0, y: 0}, } as CdkDragDrop; } - private createContainer( - model: ContainerModel - ): CdkDropList { - const container = { id: model.id, data: model.data }; + private createContainer(model: ContainerModel): CdkDropList { + const container = {id: model.id, data: model.data}; return container as CdkDropList; } } @@ -98,18 +101,16 @@ describe('Rearrange Skills In Subtopic Modal Component', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - RearrangeSkillsInSubtopicsModalComponent - ], + declarations: [RearrangeSkillsInSubtopicsModalComponent], providers: [ TopicEditorStateService, TopicUpdateService, { provide: NgbActiveModal, - useClass: MockActiveModal - } + useClass: MockActiveModal, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); @@ -121,45 +122,52 @@ describe('Rearrange Skills In Subtopic Modal Component', () => { description: 'Topic description', version: 1, uncategorized_skill_ids: ['skill_1'], - canonical_story_references: [{ - story_id: 'story_1', - story_is_published: true - }, { - story_id: 'story_2', - story_is_published: true - }, { - story_id: 'story_3', - story_is_published: true - }], - additional_story_references: [{ - story_id: 'story_2', - story_is_published: true - }], - subtopics: [{ - id: 1, - title: 'Title', - skill_ids: ['skill_2'] - }], + canonical_story_references: [ + { + story_id: 'story_1', + story_is_published: true, + }, + { + story_id: 'story_2', + story_is_published: true, + }, + { + story_id: 'story_3', + story_is_published: true, + }, + ], + additional_story_references: [ + { + story_id: 'story_2', + story_is_published: true, + }, + ], + subtopics: [ + { + id: 1, + title: 'Title', + skill_ids: ['skill_2'], + }, + ], next_subtopic_id: 2, language_code: 'en', - skill_ids_for_diagnostic_test: [] + skill_ids_for_diagnostic_test: [], }, skillIdToDescriptionDict: { skill_1: 'Description 1', - skill_2: 'Description 2' - } + skill_2: 'Description 2', + }, }; - fixture = TestBed.createComponent( - RearrangeSkillsInSubtopicsModalComponent - ); + fixture = TestBed.createComponent(RearrangeSkillsInSubtopicsModalComponent); component = fixture.componentInstance; topicEditorStateService = TestBed.inject(TopicEditorStateService); topicUpdateService = TestBed.inject(TopicUpdateService); let subtopic = Subtopic.createFromTitle(1, 'subtopic1'); topic = Topic.create( sampleTopicBackendObject.topicDict as TopicBackendDict, - sampleTopicBackendObject.skillIdToDescriptionDict); + sampleTopicBackendObject.skillIdToDescriptionDict + ); topic._subtopics = [subtopic]; spyOn(topicEditorStateService, 'getTopic').and.returnValue(topic); }); @@ -178,90 +186,87 @@ describe('Rearrange Skills In Subtopic Modal Component', () => { }); it('should record skill summary to move and subtopic Id', () => { - let skillSummary = ShortSkillSummary.create( - '1', 'Skill description'); + let skillSummary = ShortSkillSummary.create('1', 'Skill description'); component.onMoveSkillStart(1, skillSummary); expect(component.skillSummaryToMove).toEqual(skillSummary); expect(component.oldSubtopicId).toEqual(1); }); - it('should not call TopicUpdateService when skill is moved to same subtopic', - () => { - const dragAndDropEventClass = ( - new DragAndDropEventClass()); - const containerData = [ - {} as ShortSkillSummary - ]; - const dragDropEvent = dragAndDropEventClass.createInContainerEvent( - 'selectedItems', containerData, 1, 0); - let removeSkillSpy = spyOn(topicUpdateService, 'removeSkillFromSubtopic'); - component.onMoveSkillEnd(dragDropEvent, null); - expect(removeSkillSpy).not.toHaveBeenCalled(); - }); + it('should not call TopicUpdateService when skill is moved to same subtopic', () => { + const dragAndDropEventClass = + new DragAndDropEventClass(); + const containerData = [{} as ShortSkillSummary]; + const dragDropEvent = dragAndDropEventClass.createInContainerEvent( + 'selectedItems', + containerData, + 1, + 0 + ); + let removeSkillSpy = spyOn(topicUpdateService, 'removeSkillFromSubtopic'); + component.onMoveSkillEnd(dragDropEvent, null); + expect(removeSkillSpy).not.toHaveBeenCalled(); + }); it('should call TopicUpdateService when skill is moved', () => { const event = { previousIndex: 1, currentIndex: 2, previousContainer: { - data: ['1', '2'] + data: ['1', '2'], }, container: { - data: ['3'] + data: ['3'], }, - item: {} + item: {}, } as unknown as CdkDragDrop; let moveSkillSpy = spyOn(topicUpdateService, 'moveSkillToSubtopic'); component.onMoveSkillEnd(event, 1); expect(moveSkillSpy).toHaveBeenCalled(); }); - it('should call TopicUpdateService when skill is removed from subtopic', - () => { - const event = { - previousIndex: 1, - currentIndex: 1, - previousContainer: { - data: ['1', '2'] - }, - container: { - data: ['1'] - }, - item: {} - } as unknown as CdkDragDrop; - let removeSkillSpy = spyOn(topicUpdateService, 'removeSkillFromSubtopic'); - component.oldSubtopicId = 1; - component.onMoveSkillEnd(event, null); - expect(removeSkillSpy).toHaveBeenCalled(); - }); + it('should call TopicUpdateService when skill is removed from subtopic', () => { + const event = { + previousIndex: 1, + currentIndex: 1, + previousContainer: { + data: ['1', '2'], + }, + container: { + data: ['1'], + }, + item: {}, + } as unknown as CdkDragDrop; + let removeSkillSpy = spyOn(topicUpdateService, 'removeSkillFromSubtopic'); + component.oldSubtopicId = 1; + component.onMoveSkillEnd(event, null); + expect(removeSkillSpy).toHaveBeenCalled(); + }); - it('should call TopicUpdateService when new Subtopic Id is null', - () => { - const event = { - previousIndex: 1, - currentIndex: 1, - previousContainer: { - data: ['1'] - }, - container: { - data: ['1'] - }, - item: {} - } as unknown as CdkDragDrop; + it('should call TopicUpdateService when new Subtopic Id is null', () => { + const event = { + previousIndex: 1, + currentIndex: 1, + previousContainer: { + data: ['1'], + }, + container: { + data: ['1'], + }, + item: {}, + } as unknown as CdkDragDrop; - let removeSkillSpy = spyOn(topicUpdateService, 'removeSkillFromSubtopic'); - component.oldSubtopicId = null; - component.onMoveSkillEnd(event, null); - expect(removeSkillSpy).not.toHaveBeenCalled(); - }); + let removeSkillSpy = spyOn(topicUpdateService, 'removeSkillFromSubtopic'); + component.oldSubtopicId = null; + component.onMoveSkillEnd(event, null); + expect(removeSkillSpy).not.toHaveBeenCalled(); + }); - it('should not call TopicUpdateService if subtopic name validation fails', - () => { - component.editableName = 'subtopic1'; - let subtopicTitleSpy = spyOn(topicUpdateService, 'setSubtopicTitle'); - component.updateSubtopicTitle(1); - expect(subtopicTitleSpy).not.toHaveBeenCalled(); - }); + it('should not call TopicUpdateService if subtopic name validation fails', () => { + component.editableName = 'subtopic1'; + let subtopicTitleSpy = spyOn(topicUpdateService, 'setSubtopicTitle'); + component.updateSubtopicTitle(1); + expect(subtopicTitleSpy).not.toHaveBeenCalled(); + }); it('should call TopicUpdateService to update subtopic title', () => { let subtopicTitleSpy = spyOn(topicUpdateService, 'setSubtopicTitle'); @@ -279,32 +284,32 @@ describe('Rearrange Skills In Subtopic Modal Component', () => { expect(component.selectedSubtopicId).toEqual(0); }); - it('should call initEditor on calls from topic being initialized', - () => { - topicInitializedEventEmitter = new EventEmitter(); - topicReinitializedEventEmitter = new EventEmitter(); + it('should call initEditor on calls from topic being initialized', () => { + topicInitializedEventEmitter = new EventEmitter(); + topicReinitializedEventEmitter = new EventEmitter(); - spyOnProperty(topicEditorStateService, 'onTopicInitialized').and.callFake( - () => { - return topicInitializedEventEmitter; - }); - spyOnProperty( - topicEditorStateService, 'onTopicReinitialized').and.callFake( - () => { - return topicReinitializedEventEmitter; - }); - spyOn(component, 'initEditor').and.callThrough(); - component.ngOnInit(); - expect(component.initEditor).toHaveBeenCalledTimes(1); - topicInitializedEventEmitter.emit(); - expect(component.initEditor).toHaveBeenCalledTimes(2); - topicReinitializedEventEmitter.emit(); - expect(component.initEditor).toHaveBeenCalledTimes(3); - let skillSummary = { - getDescription: () => { - return null; - } - }; - component.isSkillDeleted(skillSummary as ShortSkillSummary); - }); + spyOnProperty(topicEditorStateService, 'onTopicInitialized').and.callFake( + () => { + return topicInitializedEventEmitter; + } + ); + spyOnProperty(topicEditorStateService, 'onTopicReinitialized').and.callFake( + () => { + return topicReinitializedEventEmitter; + } + ); + spyOn(component, 'initEditor').and.callThrough(); + component.ngOnInit(); + expect(component.initEditor).toHaveBeenCalledTimes(1); + topicInitializedEventEmitter.emit(); + expect(component.initEditor).toHaveBeenCalledTimes(2); + topicReinitializedEventEmitter.emit(); + expect(component.initEditor).toHaveBeenCalledTimes(3); + let skillSummary = { + getDescription: () => { + return null; + }, + }; + component.isSkillDeleted(skillSummary as ShortSkillSummary); + }); }); diff --git a/core/templates/pages/topic-editor-page/modal-templates/rearrange-skills-in-subtopics-modal.component.ts b/core/templates/pages/topic-editor-page/modal-templates/rearrange-skills-in-subtopics-modal.component.ts index 57b79063a11c..6d5eaca73483 100644 --- a/core/templates/pages/topic-editor-page/modal-templates/rearrange-skills-in-subtopics-modal.component.ts +++ b/core/templates/pages/topic-editor-page/modal-templates/rearrange-skills-in-subtopics-modal.component.ts @@ -16,25 +16,31 @@ * @fileoverview Component for RearrangeSkillsInSubtopicsModal. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { ShortSkillSummary } from 'domain/skill/short-skill-summary.model'; -import { TopicUpdateService } from 'domain/topic/topic-update.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { Subscription } from 'rxjs'; -import { SubtopicValidationService } from '../services/subtopic-validation.service'; -import { TopicEditorStateService } from '../services/topic-editor-state.service'; -import { Topic } from 'domain/topic/topic-object.model'; -import { Subtopic } from 'domain/topic/subtopic.model'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { CdkDragDrop, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {ShortSkillSummary} from 'domain/skill/short-skill-summary.model'; +import {TopicUpdateService} from 'domain/topic/topic-update.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {Subscription} from 'rxjs'; +import {SubtopicValidationService} from '../services/subtopic-validation.service'; +import {TopicEditorStateService} from '../services/topic-editor-state.service'; +import {Topic} from 'domain/topic/topic-object.model'; +import {Subtopic} from 'domain/topic/subtopic.model'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import { + CdkDragDrop, + moveItemInArray, + transferArrayItem, +} from '@angular/cdk/drag-drop'; @Component({ selector: 'oppia-rearrange-skills-in-subtopics-modal', - templateUrl: './rearrange-skills-in-subtopics-modal.component.html' + templateUrl: './rearrange-skills-in-subtopics-modal.component.html', }) export class RearrangeSkillsInSubtopicsModalComponent - extends ConfirmOrCancelModal implements OnInit, OnDestroy { + extends ConfirmOrCancelModal + implements OnInit, OnDestroy +{ topic: Topic; subtopics: Subtopic[]; uncategorizedSkillSummaries: ShortSkillSummary[]; @@ -52,7 +58,7 @@ export class RearrangeSkillsInSubtopicsModalComponent private topicEditorStateService: TopicEditorStateService, private topicUpdateService: TopicUpdateService, private urlInterpolationService: UrlInterpolationService, - private ngbActiveModal: NgbActiveModal, + private ngbActiveModal: NgbActiveModal ) { super(ngbActiveModal); } @@ -60,14 +66,15 @@ export class RearrangeSkillsInSubtopicsModalComponent initEditor(): void { this.topic = this.topicEditorStateService.getTopic(); this.subtopics = this.topic.getSubtopics(); - this.uncategorizedSkillSummaries = ( - this.topic.getUncategorizedSkillSummaries()); + this.uncategorizedSkillSummaries = + this.topic.getUncategorizedSkillSummaries(); } getSkillEditorUrl(skillId: string): string { return this.urlInterpolationService.interpolateUrl( - this.SKILL_EDITOR_URL_TEMPLATE, { - skillId: skillId + this.SKILL_EDITOR_URL_TEMPLATE, + { + skillId: skillId, } ); } @@ -80,7 +87,9 @@ export class RearrangeSkillsInSubtopicsModalComponent * is to be moved. */ onMoveSkillStart( - oldSubtopicId: number | null, skillSummary: ShortSkillSummary): void { + oldSubtopicId: number | null, + skillSummary: ShortSkillSummary + ): void { this.skillSummaryToMove = skillSummary; this.oldSubtopicId = oldSubtopicId ? oldSubtopicId : null; } @@ -91,17 +100,21 @@ export class RearrangeSkillsInSubtopicsModalComponent * uncategorized section. */ onMoveSkillEnd( - event: CdkDragDrop, - newSubtopicId: number | null): void { + event: CdkDragDrop, + newSubtopicId: number | null + ): void { if (event.previousContainer === event.container) { moveItemInArray( - event.container.data, event.previousIndex, event.currentIndex); + event.container.data, + event.previousIndex, + event.currentIndex + ); } else { transferArrayItem( event.previousContainer.data, event.container.data, event.previousIndex, - event.currentIndex, + event.currentIndex ); if (newSubtopicId === this.oldSubtopicId) { return; @@ -109,25 +122,35 @@ export class RearrangeSkillsInSubtopicsModalComponent if (newSubtopicId === null) { this.topicUpdateService.removeSkillFromSubtopic( - this.topic, this.oldSubtopicId, this.skillSummaryToMove); + this.topic, + this.oldSubtopicId, + this.skillSummaryToMove + ); } else { this.topicUpdateService.moveSkillToSubtopic( - this.topic, this.oldSubtopicId, newSubtopicId, - this.skillSummaryToMove); + this.topic, + this.oldSubtopicId, + newSubtopicId, + this.skillSummaryToMove + ); } } this.initEditor(); } updateSubtopicTitle(subtopicId: number): void { - if (!this.subtopicValidationService.checkValidSubtopicName( - this.editableName)) { + if ( + !this.subtopicValidationService.checkValidSubtopicName(this.editableName) + ) { this.errorMsg = 'A subtopic with this title already exists'; return; } this.topicUpdateService.setSubtopicTitle( - this.topic, subtopicId, this.editableName); + this.topic, + subtopicId, + this.editableName + ); this.editNameOfSubtopicWithId(null); } @@ -145,13 +168,15 @@ export class RearrangeSkillsInSubtopicsModalComponent ngOnInit(): void { this.editableName = ''; this.directiveSubscriptions.add( - this.topicEditorStateService.onTopicInitialized.subscribe( - () => this.initEditor() - )); + this.topicEditorStateService.onTopicInitialized.subscribe(() => + this.initEditor() + ) + ); this.directiveSubscriptions.add( - this.topicEditorStateService.onTopicReinitialized.subscribe( - () => this.initEditor() - )); + this.topicEditorStateService.onTopicReinitialized.subscribe(() => + this.initEditor() + ) + ); this.initEditor(); } diff --git a/core/templates/pages/topic-editor-page/modal-templates/topic-editor-save-modal.component.spec.ts b/core/templates/pages/topic-editor-page/modal-templates/topic-editor-save-modal.component.spec.ts index 85ef03707ca9..9fc8f534918c 100644 --- a/core/templates/pages/topic-editor-page/modal-templates/topic-editor-save-modal.component.spec.ts +++ b/core/templates/pages/topic-editor-page/modal-templates/topic-editor-save-modal.component.spec.ts @@ -16,10 +16,10 @@ * @fileoverview Unit tests for TopicEditorSaveModalComponent. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { TopicEditorSaveModalComponent } from './topic-editor-save-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {TopicEditorSaveModalComponent} from './topic-editor-save-modal.component'; class MockActiveModal { close(): void { @@ -31,21 +31,21 @@ class MockActiveModal { } } -describe('Topic Editor Save Modal Controller', function() { +describe('Topic Editor Save Modal Controller', function () { let component: TopicEditorSaveModalComponent; let fixture: ComponentFixture; let topicIsPublished = true; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - TopicEditorSaveModalComponent + declarations: [TopicEditorSaveModalComponent], + providers: [ + { + provide: NgbActiveModal, + useClass: MockActiveModal, + }, ], - providers: [{ - provide: NgbActiveModal, - useClass: MockActiveModal - }], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -57,9 +57,8 @@ describe('Topic Editor Save Modal Controller', function() { fixture.detectChanges(); }); - it('should initialize component properties after component is initialized', - () => { - component.isTopicPublished = topicIsPublished; - expect(component.isTopicPublished).toBe(true); - }); + it('should initialize component properties after component is initialized', () => { + component.isTopicPublished = topicIsPublished; + expect(component.isTopicPublished).toBe(true); + }); }); diff --git a/core/templates/pages/topic-editor-page/modal-templates/topic-editor-save-modal.component.ts b/core/templates/pages/topic-editor-page/modal-templates/topic-editor-save-modal.component.ts index ec741736f89a..c9d8c3c21913 100644 --- a/core/templates/pages/topic-editor-page/modal-templates/topic-editor-save-modal.component.ts +++ b/core/templates/pages/topic-editor-page/modal-templates/topic-editor-save-modal.component.ts @@ -16,17 +16,19 @@ * @fileoverview Component for topic editor save modal. */ -import { Component, Input, OnInit } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { AppConstants } from 'app.constants'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component, Input, OnInit} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {AppConstants} from 'app.constants'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-topic-editor-save-modal', - templateUrl: './topic-editor-save-modal.component.html' + templateUrl: './topic-editor-save-modal.component.html', }) -export class TopicEditorSaveModalComponent extends ConfirmOrCancelModal - implements OnInit { +export class TopicEditorSaveModalComponent + extends ConfirmOrCancelModal + implements OnInit +{ // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -35,9 +37,7 @@ export class TopicEditorSaveModalComponent extends ConfirmOrCancelModal commitMessage: string = ''; isTopicPublished: boolean = false; - constructor( - private ngbActiveModal: NgbActiveModal, - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } diff --git a/core/templates/pages/topic-editor-page/modal-templates/topic-editor-send-mail-modal.component.spec.ts b/core/templates/pages/topic-editor-page/modal-templates/topic-editor-send-mail-modal.component.spec.ts index f57b58e7710d..e969cc62833e 100644 --- a/core/templates/pages/topic-editor-page/modal-templates/topic-editor-send-mail-modal.component.spec.ts +++ b/core/templates/pages/topic-editor-page/modal-templates/topic-editor-send-mail-modal.component.spec.ts @@ -16,25 +16,25 @@ * @fileoverview Unit tests for the TopicEditorSendMailComponent. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; -import { TopicEditorSendMailComponent } from './topic-editor-send-mail-modal.component'; +import {TopicEditorSendMailComponent} from './topic-editor-send-mail-modal.component'; -describe('Topic Editor Send Mail Modal Component', function() { +describe('Topic Editor Send Mail Modal Component', function () { let component: TopicEditorSendMailComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - TopicEditorSendMailComponent + declarations: [TopicEditorSendMailComponent], + providers: [ + { + provide: NgbActiveModal, + }, ], - providers: [{ - provide: NgbActiveModal - }], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); diff --git a/core/templates/pages/topic-editor-page/modal-templates/topic-editor-send-mail-modal.component.ts b/core/templates/pages/topic-editor-page/modal-templates/topic-editor-send-mail-modal.component.ts index 181aa1341f06..28797628eee5 100644 --- a/core/templates/pages/topic-editor-page/modal-templates/topic-editor-send-mail-modal.component.ts +++ b/core/templates/pages/topic-editor-page/modal-templates/topic-editor-send-mail-modal.component.ts @@ -16,19 +16,17 @@ * @fileoverview Component for topic editor send mail modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-topic-editor-send-mail-modal', - templateUrl: './topic-editor-send-mail-modal.component.html' + templateUrl: './topic-editor-send-mail-modal.component.html', }) export class TopicEditorSendMailComponent extends ConfirmOrCancelModal { - constructor( - private ngbActiveModal: NgbActiveModal - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } } diff --git a/core/templates/pages/topic-editor-page/navbar/topic-editor-navbar-breadcrumb.component.spec.ts b/core/templates/pages/topic-editor-page/navbar/topic-editor-navbar-breadcrumb.component.spec.ts index 968e020a2436..452bfb572a0f 100644 --- a/core/templates/pages/topic-editor-page/navbar/topic-editor-navbar-breadcrumb.component.spec.ts +++ b/core/templates/pages/topic-editor-page/navbar/topic-editor-navbar-breadcrumb.component.spec.ts @@ -16,14 +16,14 @@ * @fileoverview Unit tests for the navbar breadcrumb of the topic editor. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { EventEmitter } from '@angular/core'; -import { Subtopic } from 'domain/topic/subtopic.model'; -import { Topic } from 'domain/topic/topic-object.model'; -import { TopicEditorRoutingService } from '../services/topic-editor-routing.service'; -import { TopicEditorStateService } from '../services/topic-editor-state.service'; -import { TopicEditorNavbarBreadcrumbComponent } from './topic-editor-navbar-breadcrumb.component'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {EventEmitter} from '@angular/core'; +import {Subtopic} from 'domain/topic/subtopic.model'; +import {Topic} from 'domain/topic/topic-object.model'; +import {TopicEditorRoutingService} from '../services/topic-editor-routing.service'; +import {TopicEditorStateService} from '../services/topic-editor-state.service'; +import {TopicEditorNavbarBreadcrumbComponent} from './topic-editor-navbar-breadcrumb.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; describe('TopicEditorNavbarBreadcrumbComponent', () => { let component: TopicEditorNavbarBreadcrumbComponent; @@ -37,7 +37,7 @@ describe('TopicEditorNavbarBreadcrumbComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [TopicEditorNavbarBreadcrumbComponent] + declarations: [TopicEditorNavbarBreadcrumbComponent], }).compileComponents(); })); @@ -47,9 +47,25 @@ describe('TopicEditorNavbarBreadcrumbComponent', () => { fixture = TestBed.createComponent(TopicEditorNavbarBreadcrumbComponent); component = fixture.componentInstance; topic = new Topic( - 'id', 'Topic name loading', 'Abbrev. name loading', - 'Url Fragment loading', 'Topic description loading', 'en', - [], [], [], 1, 1, [], 'str', '', {}, false, '', '', [] + 'id', + 'Topic name loading', + 'Abbrev. name loading', + 'Url Fragment loading', + 'Topic description loading', + 'en', + [], + [], + [], + 1, + 1, + [], + 'str', + '', + {}, + false, + '', + '', + [] ); }); @@ -72,8 +88,10 @@ describe('TopicEditorNavbarBreadcrumbComponent', () => { it('should validate topic when topic is initialised', () => { spyOn(topicEditorStateService, 'getTopic').and.returnValue(topic); - spyOnProperty(topicEditorStateService, 'onTopicInitialized').and - .returnValue(topicInitializedEventEmitter); + spyOnProperty( + topicEditorStateService, + 'onTopicInitialized' + ).and.returnValue(topicInitializedEventEmitter); component.ngOnInit(); topicInitializedEventEmitter.emit(); @@ -83,8 +101,10 @@ describe('TopicEditorNavbarBreadcrumbComponent', () => { it('should validate topic when topic is reinitialised', () => { spyOn(topicEditorStateService, 'getTopic').and.returnValue(topic); - spyOnProperty(topicEditorStateService, 'onTopicReinitialized').and - .returnValue(topicReinitializedEventEmitter); + spyOnProperty( + topicEditorStateService, + 'onTopicReinitialized' + ).and.returnValue(topicReinitializedEventEmitter); component.ngOnInit(); topicReinitializedEventEmitter.emit(); @@ -92,7 +112,7 @@ describe('TopicEditorNavbarBreadcrumbComponent', () => { expect(component.topic).toEqual(topic); }); - it('should navigate to main tab when user clicks \'Back to Topic\'', () => { + it("should navigate to main tab when user clicks 'Back to Topic'", () => { spyOn(topicEditorRoutingService, 'navigateToMainTab'); component.navigateToMainTab(); @@ -102,29 +122,37 @@ describe('TopicEditorNavbarBreadcrumbComponent', () => { it('should return true when the current tab is in a subtopic', () => { spyOn(topicEditorRoutingService, 'getActiveTabName').and.returnValue( - 'subtopic_editor'); + 'subtopic_editor' + ); spyOn(topicEditorRoutingService, 'getLastTabVisited').and.returnValue( - 'topic_preview'); + 'topic_preview' + ); expect(component.canNavigateToTopicEditorPage()).toBeTrue(); }); it('should return true when the last visited tab is subtopic', () => { spyOn(topicEditorRoutingService, 'getActiveTabName').and.returnValue( - 'topic_editor'); + 'topic_editor' + ); spyOn(topicEditorRoutingService, 'getLastTabVisited').and.returnValue( - 'subtopic'); + 'subtopic' + ); expect(component.canNavigateToTopicEditorPage()).toBeTrue(); }); - it('should return false when user cannot navigate to topic editor ' + - 'page', () => { - spyOn(topicEditorRoutingService, 'getActiveTabName').and.returnValue( - 'topic_preview'); - spyOn(topicEditorRoutingService, 'getLastTabVisited').and.returnValue( - 'main'); - - expect(component.canNavigateToTopicEditorPage()).toBeFalse(); - }); + it( + 'should return false when user cannot navigate to topic editor ' + 'page', + () => { + spyOn(topicEditorRoutingService, 'getActiveTabName').and.returnValue( + 'topic_preview' + ); + spyOn(topicEditorRoutingService, 'getLastTabVisited').and.returnValue( + 'main' + ); + + expect(component.canNavigateToTopicEditorPage()).toBeFalse(); + } + ); }); diff --git a/core/templates/pages/topic-editor-page/navbar/topic-editor-navbar-breadcrumb.component.ts b/core/templates/pages/topic-editor-page/navbar/topic-editor-navbar-breadcrumb.component.ts index 668b2a26fbaa..4e9617851890 100644 --- a/core/templates/pages/topic-editor-page/navbar/topic-editor-navbar-breadcrumb.component.ts +++ b/core/templates/pages/topic-editor-page/navbar/topic-editor-navbar-breadcrumb.component.ts @@ -16,17 +16,17 @@ * @fileoverview Component for the navbar breadcrumb of the topic editor. */ -import { Component } from '@angular/core'; -import { Subscription } from 'rxjs'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { Topic } from 'domain/topic/topic-object.model'; -import { TopicEditorRoutingService } from '../services/topic-editor-routing.service'; -import { TopicEditorStateService } from '../services/topic-editor-state.service'; -import { TopicRights } from 'domain/topic/topic-rights.model'; +import {Component} from '@angular/core'; +import {Subscription} from 'rxjs'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {Topic} from 'domain/topic/topic-object.model'; +import {TopicEditorRoutingService} from '../services/topic-editor-routing.service'; +import {TopicEditorStateService} from '../services/topic-editor-state.service'; +import {TopicRights} from 'domain/topic/topic-rights.model'; @Component({ selector: 'oppia-topic-editor-navbar-breadcrumb', - templateUrl: './topic-editor-navbar-breadcrumb.component.html' + templateUrl: './topic-editor-navbar-breadcrumb.component.html', }) export class TopicEditorNavbarBreadcrumbComponent { // This property is initialized using Angular lifecycle hooks @@ -46,7 +46,8 @@ export class TopicEditorNavbarBreadcrumbComponent { const activeTab = this.topicEditorRoutingService.getActiveTabName(); return ( activeTab.startsWith('subtopic') || - this.topicEditorRoutingService.getLastTabVisited() === 'subtopic'); + this.topicEditorRoutingService.getLastTabVisited() === 'subtopic' + ); } navigateToMainTab(): void { @@ -55,23 +56,23 @@ export class TopicEditorNavbarBreadcrumbComponent { ngOnInit(): void { this.directiveSubscriptions.add( - this.topicEditorStateService.onTopicInitialized.subscribe( - () => { - this.topic = this.topicEditorStateService.getTopic(); - } - )); + this.topicEditorStateService.onTopicInitialized.subscribe(() => { + this.topic = this.topicEditorStateService.getTopic(); + }) + ); this.directiveSubscriptions.add( - this.topicEditorStateService.onTopicReinitialized.subscribe( - () => { - this.topic = this.topicEditorStateService.getTopic(); - } - )); + this.topicEditorStateService.onTopicReinitialized.subscribe(() => { + this.topic = this.topicEditorStateService.getTopic(); + }) + ); this.topic = this.topicEditorStateService.getTopic(); this.topicRights = this.topicEditorStateService.getTopicRights(); } } -angular.module('oppia').directive('oppiaTopicEditorNavbarBreadcrumb', +angular.module('oppia').directive( + 'oppiaTopicEditorNavbarBreadcrumb', downgradeComponent({ - component: TopicEditorNavbarBreadcrumbComponent - }) as angular.IDirectiveFactory); + component: TopicEditorNavbarBreadcrumbComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/topic-editor-page/navbar/topic-editor-navbar.component.spec.ts b/core/templates/pages/topic-editor-page/navbar/topic-editor-navbar.component.spec.ts index 9ada38826aae..15bfbe9f0946 100644 --- a/core/templates/pages/topic-editor-page/navbar/topic-editor-navbar.component.spec.ts +++ b/core/templates/pages/topic-editor-page/navbar/topic-editor-navbar.component.spec.ts @@ -16,34 +16,43 @@ * @fileoverview Unit tests for the navbar of the topic editor. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { UndoRedoService } from 'domain/editor/undo_redo/undo-redo.service'; -import { ShortSkillSummary } from 'domain/skill/short-skill-summary.model'; -import { Subtopic } from 'domain/topic/subtopic.model'; -import { TopicRightsBackendApiService, TopicRightsBackendResponse } from 'domain/topic/topic-rights-backend-api.service'; -import { TopicRights } from 'domain/topic/topic-rights.model'; -import { Topic } from 'domain/topic/topic-object.model'; -import { AlertsService } from 'services/alerts.service'; -import { UrlService } from 'services/contextual/url.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { TopicEditorSaveModalComponent } from '../modal-templates/topic-editor-save-modal.component'; -import { TopicEditorSendMailComponent } from '../modal-templates/topic-editor-send-mail-modal.component'; -import { TopicEditorRoutingService } from '../services/topic-editor-routing.service'; -import { TopicEditorStateService } from '../services/topic-editor-state.service'; -import { TopicEditorNavbarComponent } from './topic-editor-navbar.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {UndoRedoService} from 'domain/editor/undo_redo/undo-redo.service'; +import {ShortSkillSummary} from 'domain/skill/short-skill-summary.model'; +import {Subtopic} from 'domain/topic/subtopic.model'; +import { + TopicRightsBackendApiService, + TopicRightsBackendResponse, +} from 'domain/topic/topic-rights-backend-api.service'; +import {TopicRights} from 'domain/topic/topic-rights.model'; +import {Topic} from 'domain/topic/topic-object.model'; +import {AlertsService} from 'services/alerts.service'; +import {UrlService} from 'services/contextual/url.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {TopicEditorSaveModalComponent} from '../modal-templates/topic-editor-save-modal.component'; +import {TopicEditorSendMailComponent} from '../modal-templates/topic-editor-send-mail-modal.component'; +import {TopicEditorRoutingService} from '../services/topic-editor-routing.service'; +import {TopicEditorStateService} from '../services/topic-editor-state.service'; +import {TopicEditorNavbarComponent} from './topic-editor-navbar.component'; class MockWindowRef { _window = { location: { hash: '123', href: '', - replace: (val: string) => {} + replace: (val: string) => {}, }, open: (url: string) => {}, - gtag: () => {} + gtag: () => {}, }; get nativeWindow() { @@ -74,7 +83,7 @@ describe('Topic Editor Navbar', () => { declarations: [ TopicEditorNavbarComponent, TopicEditorSaveModalComponent, - TopicEditorSendMailComponent + TopicEditorSendMailComponent, ], providers: [ TopicEditorStateService, @@ -83,10 +92,10 @@ describe('Topic Editor Navbar', () => { TopicRightsBackendApiService, { provide: WindowRef, - useValue: windowRef + useValue: windowRef, }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -99,17 +108,31 @@ describe('Topic Editor Navbar', () => { urlService = TestBed.inject(UrlService); undoRedoService = TestBed.inject(UndoRedoService); alertsService = TestBed.inject(AlertsService); - topicRightsBackendApiService = - TestBed.inject(TopicRightsBackendApiService); + topicRightsBackendApiService = TestBed.inject(TopicRightsBackendApiService); let subtopic = Subtopic.createFromTitle(1, 'subtopic1'); subtopic.setUrlFragment('dummy-url'); - let skillSummary = ShortSkillSummary.create( - 'skill_1', 'Description 1'); + let skillSummary = ShortSkillSummary.create('skill_1', 'Description 1'); topic = new Topic( - '', 'Topic name loading', 'Abbrev. name loading', - 'Url Fragment loading', 'Topic description loading', 'en', - [], [], [], 1, 1, [], '', '', {}, false, '', '', [] + '', + 'Topic name loading', + 'Abbrev. name loading', + 'Url Fragment loading', + 'Topic description loading', + 'en', + [], + [], + [], + 1, + 1, + [], + '', + '', + {}, + false, + '', + '', + [] ); topic._uncategorizedSkillSummaries = [skillSummary]; topic._subtopics = [subtopic]; @@ -128,8 +151,11 @@ describe('Topic Editor Navbar', () => { componentInstance.ngOnInit(); expect(componentInstance.topicId).toBe('topic_1'); - expect(componentInstance.navigationChoices).toEqual( - ['Topic', 'Questions', 'Preview']); + expect(componentInstance.navigationChoices).toEqual([ + 'Topic', + 'Questions', + 'Preview', + ]); expect(componentInstance.activeTab).toEqual('Editor'); expect(componentInstance.showNavigationOptions).toBeFalse(); expect(componentInstance.warningsAreShown).toBeFalse(); @@ -145,12 +171,17 @@ describe('Topic Editor Navbar', () => { it('should validate topic when topic is initialised', () => { spyOn(urlService, 'getTopicIdFromUrl').and.returnValue('topic_1'); spyOn(topicEditorStateService, 'getTopic').and.returnValue(topic); - spyOnProperty(topicEditorStateService, 'onTopicInitialized').and - .returnValue(topicInitializedEventEmitter); + spyOnProperty( + topicEditorStateService, + 'onTopicInitialized' + ).and.returnValue(topicInitializedEventEmitter); spyOn(topicEditorStateService, 'getTopicWithNameExists').and.returnValue( - false); - spyOn(topicEditorStateService, 'getTopicWithUrlFragmentExists').and - .returnValue(false); + false + ); + spyOn( + topicEditorStateService, + 'getTopicWithUrlFragmentExists' + ).and.returnValue(false); componentInstance.ngOnInit(); expect(componentInstance.validationIssues).toEqual([]); @@ -159,26 +190,31 @@ describe('Topic Editor Navbar', () => { topicInitializedEventEmitter.emit(); expect(componentInstance.validationIssues).toEqual([ - 'Topic url fragment is not valid.' + 'Topic url fragment is not valid.', ]); expect(componentInstance.prepublishValidationIssues).toEqual([ 'Topic should have a thumbnail.', 'Subtopic with title subtopic1 does not have any skill IDs linked.', 'Topic should have page title fragment.', 'Topic should have meta tag content.', - 'Subtopic subtopic1 should have a thumbnail.' + 'Subtopic subtopic1 should have a thumbnail.', ]); }); it('should validate topic when topic is reinitialised', () => { spyOn(urlService, 'getTopicIdFromUrl').and.returnValue('topic_1'); spyOn(topicEditorStateService, 'getTopic').and.returnValue(topic); - spyOnProperty(topicEditorStateService, 'onTopicReinitialized').and - .returnValue(topicReinitializedEventEmitter); + spyOnProperty( + topicEditorStateService, + 'onTopicReinitialized' + ).and.returnValue(topicReinitializedEventEmitter); spyOn(topicEditorStateService, 'getTopicWithNameExists').and.returnValue( - true); - spyOn(topicEditorStateService, 'getTopicWithUrlFragmentExists').and - .returnValue(true); + true + ); + spyOn( + topicEditorStateService, + 'getTopicWithUrlFragmentExists' + ).and.returnValue(true); componentInstance.ngOnInit(); expect(componentInstance.validationIssues).toEqual([]); @@ -189,14 +225,14 @@ describe('Topic Editor Navbar', () => { expect(componentInstance.validationIssues).toEqual([ 'Topic url fragment is not valid.', 'A topic with this name already exists.', - 'Topic URL fragment already exists.' + 'Topic URL fragment already exists.', ]); expect(componentInstance.prepublishValidationIssues).toEqual([ 'Topic should have a thumbnail.', 'Subtopic with title subtopic1 does not have any skill IDs linked.', 'Topic should have page title fragment.', 'Topic should have meta tag content.', - 'Subtopic subtopic1 should have a thumbnail.' + 'Subtopic subtopic1 should have a thumbnail.', ]); }); @@ -215,12 +251,16 @@ describe('Topic Editor Navbar', () => { it('should validate topic when user undo or redo changes', () => { spyOn(urlService, 'getTopicIdFromUrl').and.returnValue('topic_1'); spyOn(topicEditorStateService, 'getTopic').and.returnValue(topic); - spyOn(undoRedoService, 'getUndoRedoChangeEventEmitter').and - .returnValue(undoRedoChangeAppliedEventEmitter); + spyOn(undoRedoService, 'getUndoRedoChangeEventEmitter').and.returnValue( + undoRedoChangeAppliedEventEmitter + ); spyOn(topicEditorStateService, 'getTopicWithNameExists').and.returnValue( - false); - spyOn(topicEditorStateService, 'getTopicWithUrlFragmentExists').and - .returnValue(false); + false + ); + spyOn( + topicEditorStateService, + 'getTopicWithUrlFragmentExists' + ).and.returnValue(false); componentInstance.ngOnInit(); expect(componentInstance.validationIssues).toEqual([]); @@ -229,14 +269,14 @@ describe('Topic Editor Navbar', () => { undoRedoChangeAppliedEventEmitter.emit(); expect(componentInstance.validationIssues).toEqual([ - 'Topic url fragment is not valid.' + 'Topic url fragment is not valid.', ]); expect(componentInstance.prepublishValidationIssues).toEqual([ 'Topic should have a thumbnail.', 'Subtopic with title subtopic1 does not have any skill IDs linked.', 'Topic should have page title fragment.', 'Topic should have meta tag content.', - 'Subtopic subtopic1 should have a thumbnail.' + 'Subtopic subtopic1 should have a thumbnail.', ]); }); @@ -253,16 +293,16 @@ describe('Topic Editor Navbar', () => { }); it('should return active tab name when called', () => { - spyOn(topicEditorRoutingService, 'getActiveTabName').and - .returnValue('topic_editor'); + spyOn(topicEditorRoutingService, 'getActiveTabName').and.returnValue( + 'topic_editor' + ); expect(componentInstance.getActiveTabName()).toBe('topic_editor'); }); it('should return the navbar text on mobile', () => { componentInstance.selectQuestionsTab(); - let routingSpy = spyOn( - topicEditorRoutingService, 'getActiveTabName'); + let routingSpy = spyOn(topicEditorRoutingService, 'getActiveTabName'); routingSpy.and.returnValue('questions'); expect(componentInstance.getMobileNavigatorText()).toBe('Questions'); routingSpy.and.returnValue('subtopic_editor'); @@ -275,129 +315,162 @@ describe('Topic Editor Navbar', () => { expect(componentInstance.getMobileNavigatorText()).toEqual('Editor'); }); - it('should navigate to main tab when user clicks the \'Editor\' ' + - 'option', () => { - spyOn(topicEditorRoutingService, 'navigateToMainTab'); - componentInstance.activeTab = 'Question'; - componentInstance.showNavigationOptions = true; + it( + "should navigate to main tab when user clicks the 'Editor' " + 'option', + () => { + spyOn(topicEditorRoutingService, 'navigateToMainTab'); + componentInstance.activeTab = 'Question'; + componentInstance.showNavigationOptions = true; - componentInstance.selectMainTab(); + componentInstance.selectMainTab(); - expect(componentInstance.activeTab).toBe('Editor'); - expect(componentInstance.showNavigationOptions).toBe(false); - expect(topicEditorRoutingService.navigateToMainTab).toHaveBeenCalled(); - }); + expect(componentInstance.activeTab).toBe('Editor'); + expect(componentInstance.showNavigationOptions).toBe(false); + expect(topicEditorRoutingService.navigateToMainTab).toHaveBeenCalled(); + } + ); - it('should navigate to Questions tab when user clicks the \'Questions\' ' + - 'option', () => { - spyOn(topicEditorRoutingService, 'navigateToQuestionsTab'); - componentInstance.activeTab = 'Editor'; - componentInstance.showNavigationOptions = true; + it( + "should navigate to Questions tab when user clicks the 'Questions' " + + 'option', + () => { + spyOn(topicEditorRoutingService, 'navigateToQuestionsTab'); + componentInstance.activeTab = 'Editor'; + componentInstance.showNavigationOptions = true; - componentInstance.selectQuestionsTab(); + componentInstance.selectQuestionsTab(); - expect(componentInstance.activeTab).toBe('Questions'); - expect(componentInstance.showNavigationOptions).toBe(false); - expect(topicEditorRoutingService.navigateToQuestionsTab).toHaveBeenCalled(); - }); + expect(componentInstance.activeTab).toBe('Questions'); + expect(componentInstance.showNavigationOptions).toBe(false); + expect( + topicEditorRoutingService.navigateToQuestionsTab + ).toHaveBeenCalled(); + } + ); - it('should open topic viewer when user clicks the \'preview\' button', () => { + it("should open topic viewer when user clicks the 'preview' button", () => { spyOn(undoRedoService, 'getChangeCount').and.returnValue(0); - spyOn(topicEditorRoutingService, 'getActiveTabName').and - .returnValue('topic_editor'); - spyOn(topicEditorStateService, 'getClassroomUrlFragment').and - .returnValue('classroom_url'); + spyOn(topicEditorRoutingService, 'getActiveTabName').and.returnValue( + 'topic_editor' + ); + spyOn(topicEditorStateService, 'getClassroomUrlFragment').and.returnValue( + 'classroom_url' + ); spyOn(windowRef.nativeWindow, 'open'); componentInstance.topic = topic; componentInstance.openTopicViewer(); expect(windowRef.nativeWindow.open).toHaveBeenCalledWith( - '/learn/classroom_url/Url%20Fragment%20loading', 'blank'); - }); - - it('should alert user to save changes when user clicks the ' + - '\'preview\' button with unsaved changes', () => { - spyOn(undoRedoService, 'getChangeCount').and.returnValue(1); - spyOn(topicEditorRoutingService, 'getActiveTabName').and - .returnValue('topic_editor'); - spyOn(alertsService, 'addInfoMessage'); - componentInstance.topic = topic; - - componentInstance.openTopicViewer(); - - expect(alertsService.addInfoMessage).toHaveBeenCalledWith( - 'Please save all pending changes to preview the topic ' + - 'with the changes', 2000); + '/learn/classroom_url/Url%20Fragment%20loading', + 'blank' + ); }); - it('should open subtopic editor when user clicks the \'editor\' ' + - 'button in the subtopic preview page', () => { - spyOn(topicEditorRoutingService, 'getActiveTabName').and - .returnValue('subtopic_preview'); - spyOn(topicEditorRoutingService, 'getSubtopicIdFromUrl').and - .returnValue(1); - spyOn(topicEditorRoutingService, 'navigateToSubtopicEditorWithId'); - componentInstance.topic = topic; + it( + 'should alert user to save changes when user clicks the ' + + "'preview' button with unsaved changes", + () => { + spyOn(undoRedoService, 'getChangeCount').and.returnValue(1); + spyOn(topicEditorRoutingService, 'getActiveTabName').and.returnValue( + 'topic_editor' + ); + spyOn(alertsService, 'addInfoMessage'); + componentInstance.topic = topic; - componentInstance.selectMainTab(); + componentInstance.openTopicViewer(); - expect(componentInstance.activeTab).toBe('Editor'); - expect(topicEditorRoutingService.navigateToSubtopicEditorWithId) - .toHaveBeenCalledWith(1); - }); + expect(alertsService.addInfoMessage).toHaveBeenCalledWith( + 'Please save all pending changes to preview the topic ' + + 'with the changes', + 2000 + ); + } + ); + + it( + "should open subtopic editor when user clicks the 'editor' " + + 'button in the subtopic preview page', + () => { + spyOn(topicEditorRoutingService, 'getActiveTabName').and.returnValue( + 'subtopic_preview' + ); + spyOn(topicEditorRoutingService, 'getSubtopicIdFromUrl').and.returnValue( + 1 + ); + spyOn(topicEditorRoutingService, 'navigateToSubtopicEditorWithId'); + componentInstance.topic = topic; - it('should open subtopic preview when user clicks the \'preview\' ' + - 'button in the subtopic editor page', () => { - spyOn(topicEditorRoutingService, 'getActiveTabName').and - .returnValue('subtopic_editor'); - spyOn(topicEditorRoutingService, 'getSubtopicIdFromUrl').and - .returnValue(1); - spyOn(topicEditorRoutingService, 'navigateToSubtopicPreviewTab'); - componentInstance.topic = topic; + componentInstance.selectMainTab(); - componentInstance.openTopicViewer(); + expect(componentInstance.activeTab).toBe('Editor'); + expect( + topicEditorRoutingService.navigateToSubtopicEditorWithId + ).toHaveBeenCalledWith(1); + } + ); + + it( + "should open subtopic preview when user clicks the 'preview' " + + 'button in the subtopic editor page', + () => { + spyOn(topicEditorRoutingService, 'getActiveTabName').and.returnValue( + 'subtopic_editor' + ); + spyOn(topicEditorRoutingService, 'getSubtopicIdFromUrl').and.returnValue( + 1 + ); + spyOn(topicEditorRoutingService, 'navigateToSubtopicPreviewTab'); + componentInstance.topic = topic; - expect(componentInstance.activeTab).toBe('Preview'); - expect(topicEditorRoutingService.navigateToSubtopicPreviewTab) - .toHaveBeenCalledWith(1); - }); + componentInstance.openTopicViewer(); - it('should not send email when user who doesn\'t have publishing rights' + - ' clicks the \'publish\' button and then cancels', fakeAsync(() => { - spyOn(topicRightsBackendApiService, 'sendMailAsync').and.returnValue( - Promise.resolve()); - spyOn(ngbModal, 'open').and.returnValue( - { - result: Promise.reject() - } as NgbModalRef - ); - spyOn(alertsService, 'addSuccessMessage'); - componentInstance.topicRights = TopicRights.createFromBackendDict({ - published: false, - can_publish_topic: false, - can_edit_topic: true - }); + expect(componentInstance.activeTab).toBe('Preview'); + expect( + topicEditorRoutingService.navigateToSubtopicPreviewTab + ).toHaveBeenCalledWith(1); + } + ); + + it( + "should not send email when user who doesn't have publishing rights" + + " clicks the 'publish' button and then cancels", + fakeAsync(() => { + spyOn(topicRightsBackendApiService, 'sendMailAsync').and.returnValue( + Promise.resolve() + ); + spyOn(ngbModal, 'open').and.returnValue({ + result: Promise.reject(), + } as NgbModalRef); + spyOn(alertsService, 'addSuccessMessage'); + componentInstance.topicRights = TopicRights.createFromBackendDict({ + published: false, + can_publish_topic: false, + can_edit_topic: true, + }); - componentInstance.publishTopic(); - tick(); + componentInstance.publishTopic(); + tick(); - expect(alertsService.addSuccessMessage).not.toHaveBeenCalled(); - })); + expect(alertsService.addSuccessMessage).not.toHaveBeenCalled(); + }) + ); - it('should discard changes when user clicks \'Discard Changes\'' + - ' button', () => { - spyOn(topicEditorStateService, 'loadTopic'); - spyOn(undoRedoService, 'clearChanges'); - componentInstance.discardChangesButtonIsShown = true; - componentInstance.topicId = 'topicId'; + it( + "should discard changes when user clicks 'Discard Changes'" + ' button', + () => { + spyOn(topicEditorStateService, 'loadTopic'); + spyOn(undoRedoService, 'clearChanges'); + componentInstance.discardChangesButtonIsShown = true; + componentInstance.topicId = 'topicId'; - componentInstance.discardChanges(); + componentInstance.discardChanges(); - expect(undoRedoService.clearChanges).toHaveBeenCalled(); - expect(componentInstance.discardChangesButtonIsShown).toBe(false); - expect(topicEditorStateService.loadTopic).toHaveBeenCalledWith('topicId'); - }); + expect(undoRedoService.clearChanges).toHaveBeenCalled(); + expect(componentInstance.discardChangesButtonIsShown).toBe(false); + expect(topicEditorStateService.loadTopic).toHaveBeenCalledWith('topicId'); + } + ); it('should return the number of changes when called', () => { spyOn(undoRedoService, 'getChangeCount').and.returnValue(1); @@ -410,7 +483,7 @@ describe('Topic Editor Navbar', () => { componentInstance.topicRights = TopicRights.createFromBackendDict({ published: false, can_publish_topic: true, - can_edit_topic: true + can_edit_topic: true, }); componentInstance.validationIssues = []; componentInstance.prepublishValidationIssues = []; @@ -423,7 +496,7 @@ describe('Topic Editor Navbar', () => { componentInstance.topicRights = TopicRights.createFromBackendDict({ published: false, can_publish_topic: true, - can_edit_topic: true + can_edit_topic: true, }); componentInstance.validationIssues = []; componentInstance.prepublishValidationIssues = []; @@ -436,7 +509,7 @@ describe('Topic Editor Navbar', () => { componentInstance.topicRights = TopicRights.createFromBackendDict({ published: true, can_publish_topic: true, - can_edit_topic: true + can_edit_topic: true, }); componentInstance.validationIssues = ['warn1']; componentInstance.prepublishValidationIssues = []; @@ -449,7 +522,7 @@ describe('Topic Editor Navbar', () => { componentInstance.topicRights = TopicRights.createFromBackendDict({ published: true, can_publish_topic: true, - can_edit_topic: true + can_edit_topic: true, }); componentInstance.validationIssues = []; componentInstance.prepublishValidationIssues = ['warn1']; @@ -479,18 +552,18 @@ describe('Topic Editor Navbar', () => { it('should save topic when user saves topic changes', fakeAsync(() => { const modalspy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ + return { componentInstance: { - topicIsPublished: true + topicIsPublished: true, }, - result: Promise.resolve('commitMessage') - } as NgbModalRef); + result: Promise.resolve('commitMessage'), + } as NgbModalRef; }); topicEditorStateService.setTopic(topic); componentInstance.topicRights = TopicRights.createFromBackendDict({ published: true, can_publish_topic: true, - can_edit_topic: true + can_edit_topic: true, }); spyOn(alertsService, 'addSuccessMessage'); spyOn(topicEditorStateService, 'saveTopic').and.callFake( @@ -502,29 +575,31 @@ describe('Topic Editor Navbar', () => { (commitMessage: string, successCallback: () => void) => { successCallback(); expect(commitMessage).toBe('commitMessage'); - }); + } + ); componentInstance.saveChanges(); tick(); expect(modalspy).toHaveBeenCalled(); - expect(alertsService.addSuccessMessage) - .toHaveBeenCalledWith('Changes Saved.'); + expect(alertsService.addSuccessMessage).toHaveBeenCalledWith( + 'Changes Saved.' + ); })); it('should close save topic modal when user clicks cancel', fakeAsync(() => { const modalspy = spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ + return { componentInstance: { - topicIsPublished: true + topicIsPublished: true, }, - result: Promise.reject() - } as NgbModalRef); + result: Promise.reject(), + } as NgbModalRef; }); topicEditorStateService.setTopic(topic); componentInstance.topicRights = TopicRights.createFromBackendDict({ published: true, can_publish_topic: true, - can_edit_topic: true + can_edit_topic: true, }); spyOn(alertsService, 'addSuccessMessage'); componentInstance.saveChanges(); @@ -534,44 +609,52 @@ describe('Topic Editor Navbar', () => { expect(alertsService.addSuccessMessage).not.toHaveBeenCalled(); })); - it('should toggle navigation option when user clicks the drop down' + - ' button next to navigation options in mobile', () => { - componentInstance.showNavigationOptions = true; + it( + 'should toggle navigation option when user clicks the drop down' + + ' button next to navigation options in mobile', + () => { + componentInstance.showNavigationOptions = true; - componentInstance.toggleNavigationOptions(); + componentInstance.toggleNavigationOptions(); - expect(componentInstance.showNavigationOptions).toBeFalse(); + expect(componentInstance.showNavigationOptions).toBeFalse(); - componentInstance.toggleNavigationOptions(); + componentInstance.toggleNavigationOptions(); - expect(componentInstance.showNavigationOptions).toBeTrue(); - }); + expect(componentInstance.showNavigationOptions).toBeTrue(); + } + ); - it('should toggle topic edit option when user clicks the drop down' + - ' button next to topic edit options in mobile', () => { - componentInstance.showTopicEditOptions = true; + it( + 'should toggle topic edit option when user clicks the drop down' + + ' button next to topic edit options in mobile', + () => { + componentInstance.showTopicEditOptions = true; - componentInstance.toggleTopicEditOptions(); + componentInstance.toggleTopicEditOptions(); - expect(componentInstance.showTopicEditOptions).toBeFalse(); + expect(componentInstance.showTopicEditOptions).toBeFalse(); - componentInstance.toggleTopicEditOptions(); + componentInstance.toggleTopicEditOptions(); - expect(componentInstance.showTopicEditOptions).toBeTrue(); - }); + expect(componentInstance.showTopicEditOptions).toBeTrue(); + } + ); - it('should toggle warnings when user clicks the warning symbol in' + - ' mobile', () => { - componentInstance.warningsAreShown = true; + it( + 'should toggle warnings when user clicks the warning symbol in' + ' mobile', + () => { + componentInstance.warningsAreShown = true; - componentInstance.toggleWarningText(); + componentInstance.toggleWarningText(); - expect(componentInstance.warningsAreShown).toBeFalse(); + expect(componentInstance.warningsAreShown).toBeFalse(); - componentInstance.toggleWarningText(); + componentInstance.toggleWarningText(); - expect(componentInstance.warningsAreShown).toBeTrue(); - }); + expect(componentInstance.warningsAreShown).toBeTrue(); + } + ); it('should validate topic when called', () => { componentInstance.topic = topic; @@ -579,14 +662,14 @@ describe('Topic Editor Navbar', () => { componentInstance._validateTopic(); expect(componentInstance.validationIssues).toEqual([ - 'Topic url fragment is not valid.' + 'Topic url fragment is not valid.', ]); expect(componentInstance.prepublishValidationIssues).toEqual([ 'Topic should have a thumbnail.', 'Subtopic with title subtopic1 does not have any skill IDs linked.', 'Topic should have page title fragment.', 'Topic should have meta tag content.', - 'Subtopic subtopic1 should have a thumbnail.' + 'Subtopic subtopic1 should have a thumbnail.', ]); }); @@ -594,119 +677,127 @@ describe('Topic Editor Navbar', () => { componentInstance.topic = topic; componentInstance._validateTopic(); expect(componentInstance.validationIssues).toEqual([ - 'Topic url fragment is not valid.' + 'Topic url fragment is not valid.', ]); expect(componentInstance.prepublishValidationIssues).toEqual([ 'Topic should have a thumbnail.', 'Subtopic with title subtopic1 does not have any skill IDs linked.', 'Topic should have page title fragment.', 'Topic should have meta tag content.', - 'Subtopic subtopic1 should have a thumbnail.' + 'Subtopic subtopic1 should have a thumbnail.', ]); expect(componentInstance.getTotalWarningsCount()).toBe(6); }); - it('should unpublish topic when user clicks the \'Unpublish\' button', - fakeAsync(() => { - componentInstance.topicRights = TopicRights.createFromBackendDict({ - published: true, - can_publish_topic: true, - can_edit_topic: true - }); - componentInstance.showTopicEditOptions = true; - spyOn(topicRightsBackendApiService, 'unpublishTopicAsync').and - .returnValue( - Promise.resolve() as unknown as Promise); - spyOn(topicEditorStateService, 'setTopicRights'); - - componentInstance.unpublishTopic(); - tick(); + it("should unpublish topic when user clicks the 'Unpublish' button", fakeAsync(() => { + componentInstance.topicRights = TopicRights.createFromBackendDict({ + published: true, + can_publish_topic: true, + can_edit_topic: true, + }); + componentInstance.showTopicEditOptions = true; + spyOn(topicRightsBackendApiService, 'unpublishTopicAsync').and.returnValue( + Promise.resolve() as unknown as Promise + ); + spyOn(topicEditorStateService, 'setTopicRights'); - expect(componentInstance.showTopicEditOptions).toBeFalse(); - expect(componentInstance.topicRights.isPublished()).toBe(false); - expect(topicEditorStateService.setTopicRights).toHaveBeenCalledWith( - TopicRights.createFromBackendDict({ - published: false, - can_publish_topic: true, - can_edit_topic: true - }) - ); - })); + componentInstance.unpublishTopic(); + tick(); - it('should not unpublish topic if topic has not been published', - fakeAsync(() => { - componentInstance.topicRights = TopicRights.createFromBackendDict({ + expect(componentInstance.showTopicEditOptions).toBeFalse(); + expect(componentInstance.topicRights.isPublished()).toBe(false); + expect(topicEditorStateService.setTopicRights).toHaveBeenCalledWith( + TopicRights.createFromBackendDict({ published: false, - can_publish_topic: false, - can_edit_topic: true - }); - spyOn(topicRightsBackendApiService, 'unpublishTopicAsync').and - .returnValue( - Promise.resolve() as unknown as Promise); - - componentInstance.unpublishTopic(); - tick(); + can_publish_topic: true, + can_edit_topic: true, + }) + ); + })); - expect(componentInstance.topicRights.isPublished()).toBe(false); - })); + it('should not unpublish topic if topic has not been published', fakeAsync(() => { + componentInstance.topicRights = TopicRights.createFromBackendDict({ + published: false, + can_publish_topic: false, + can_edit_topic: true, + }); + spyOn(topicRightsBackendApiService, 'unpublishTopicAsync').and.returnValue( + Promise.resolve() as unknown as Promise + ); - it('should publish topic when user clicks the \'publish\' button', - fakeAsync(() => { - spyOn(topicRightsBackendApiService, 'publishTopicAsync').and.returnValue( - Promise.resolve() as unknown as Promise); - spyOn(alertsService, 'addSuccessMessage'); - componentInstance.topicRights = TopicRights.createFromBackendDict({ - published: false, - can_publish_topic: true, - can_edit_topic: true - }); + componentInstance.unpublishTopic(); + tick(); - componentInstance.publishTopic(); - tick(100); + expect(componentInstance.topicRights.isPublished()).toBe(false); + })); - expect(alertsService.addSuccessMessage) - .toHaveBeenCalledWith('Topic published.', 1000); - expect(componentInstance.topicRights.isPublished()).toBeTrue(); - expect( - windowRef.nativeWindow.location.href).toBe( - '/topics-and-skills-dashboard'); - })); - - it('should send email when user who doesn\'t have publishing rights' + - ' clicks the \'publish\' button', fakeAsync(() => { - spyOn(topicRightsBackendApiService, 'sendMailAsync').and.returnValue( - Promise.resolve()); - spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ - result: Promise.resolve('success') - } as NgbModalRef); - }); + it("should publish topic when user clicks the 'publish' button", fakeAsync(() => { + spyOn(topicRightsBackendApiService, 'publishTopicAsync').and.returnValue( + Promise.resolve() as unknown as Promise + ); spyOn(alertsService, 'addSuccessMessage'); componentInstance.topicRights = TopicRights.createFromBackendDict({ published: false, - can_publish_topic: false, - can_edit_topic: true + can_publish_topic: true, + can_edit_topic: true, }); componentInstance.publishTopic(); tick(100); - expect(alertsService.addSuccessMessage) - .toHaveBeenCalledWith('Mail Sent.', 1000); + expect(alertsService.addSuccessMessage).toHaveBeenCalledWith( + 'Topic published.', + 1000 + ); + expect(componentInstance.topicRights.isPublished()).toBeTrue(); + expect(windowRef.nativeWindow.location.href).toBe( + '/topics-and-skills-dashboard' + ); })); + it( + "should send email when user who doesn't have publishing rights" + + " clicks the 'publish' button", + fakeAsync(() => { + spyOn(topicRightsBackendApiService, 'sendMailAsync').and.returnValue( + Promise.resolve() + ); + spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + result: Promise.resolve('success'), + } as NgbModalRef; + }); + spyOn(alertsService, 'addSuccessMessage'); + componentInstance.topicRights = TopicRights.createFromBackendDict({ + published: false, + can_publish_topic: false, + can_edit_topic: true, + }); + + componentInstance.publishTopic(); + tick(100); + + expect(alertsService.addSuccessMessage).toHaveBeenCalledWith( + 'Mail Sent.', + 1000 + ); + }) + ); + it('should return all the warnings when called', () => { componentInstance.topic = topic; componentInstance._validateTopic(); - expect(componentInstance.getAllTopicWarnings()).toEqual([ - 'Topic url fragment is not valid.', - 'Topic should have a thumbnail.', - 'Subtopic with title subtopic1 does not have any skill IDs linked.', - 'Topic should have page title fragment.', - 'Topic should have meta tag content.', - 'Subtopic subtopic1 should have a thumbnail.' - ].join('\n')); + expect(componentInstance.getAllTopicWarnings()).toEqual( + [ + 'Topic url fragment is not valid.', + 'Topic should have a thumbnail.', + 'Subtopic with title subtopic1 does not have any skill IDs linked.', + 'Topic should have page title fragment.', + 'Topic should have meta tag content.', + 'Subtopic subtopic1 should have a thumbnail.', + ].join('\n') + ); }); it('should return true or false when called', () => { diff --git a/core/templates/pages/topic-editor-page/navbar/topic-editor-navbar.component.ts b/core/templates/pages/topic-editor-page/navbar/topic-editor-navbar.component.ts index 8857b4c09e4b..07aa92c292b2 100644 --- a/core/templates/pages/topic-editor-page/navbar/topic-editor-navbar.component.ts +++ b/core/templates/pages/topic-editor-page/navbar/topic-editor-navbar.component.ts @@ -16,30 +16,31 @@ * @fileoverview Component for the navbar of the topic editor. */ -import { Subscription } from 'rxjs'; -import { TopicEditorSendMailComponent } from '../modal-templates/topic-editor-send-mail-modal.component'; -import { TopicEditorSaveModalComponent } from '../modal-templates/topic-editor-save-modal.component'; -import { AfterContentChecked, Component, OnDestroy, OnInit } from '@angular/core'; -import { TopicEditorStateService } from '../services/topic-editor-state.service'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { TopicRightsBackendApiService } from 'domain/topic/topic-rights-backend-api.service'; -import { AlertsService } from 'services/alerts.service'; -import { UndoRedoService } from 'domain/editor/undo_redo/undo-redo.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { UrlService } from 'services/contextual/url.service'; -import { TopicEditorRoutingService } from '../services/topic-editor-routing.service'; -import { ClassroomDomainConstants } from 'domain/classroom/classroom-domain.constants'; -import { Topic } from 'domain/topic/topic-object.model'; -import { TopicRights } from 'domain/topic/topic-rights.model'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Subscription} from 'rxjs'; +import {TopicEditorSendMailComponent} from '../modal-templates/topic-editor-send-mail-modal.component'; +import {TopicEditorSaveModalComponent} from '../modal-templates/topic-editor-save-modal.component'; +import {AfterContentChecked, Component, OnDestroy, OnInit} from '@angular/core'; +import {TopicEditorStateService} from '../services/topic-editor-state.service'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {TopicRightsBackendApiService} from 'domain/topic/topic-rights-backend-api.service'; +import {AlertsService} from 'services/alerts.service'; +import {UndoRedoService} from 'domain/editor/undo_redo/undo-redo.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {UrlService} from 'services/contextual/url.service'; +import {TopicEditorRoutingService} from '../services/topic-editor-routing.service'; +import {ClassroomDomainConstants} from 'domain/classroom/classroom-domain.constants'; +import {Topic} from 'domain/topic/topic-object.model'; +import {TopicRights} from 'domain/topic/topic-rights.model'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {downgradeComponent} from '@angular/upgrade/static'; @Component({ selector: 'oppia-topic-editor-navbar', - templateUrl: './topic-editor-navbar.component.html' + templateUrl: './topic-editor-navbar.component.html', }) -export class TopicEditorNavbarComponent implements OnInit, - OnDestroy, AfterContentChecked { +export class TopicEditorNavbarComponent + implements OnInit, OnDestroy, AfterContentChecked +{ validationIssues: string[]; topic: Topic; prepublishValidationIssues: string[]; @@ -78,59 +79,62 @@ export class TopicEditorNavbarComponent implements OnInit, _validateTopic(): void { this.validationIssues = this.topic.validate(); if (this.topicEditorStateService.getTopicWithNameExists()) { - this.validationIssues.push( - 'A topic with this name already exists.'); + this.validationIssues.push('A topic with this name already exists.'); } if (this.topicEditorStateService.getTopicWithUrlFragmentExists()) { - this.validationIssues.push( - 'Topic URL fragment already exists.'); + this.validationIssues.push('Topic URL fragment already exists.'); } - let prepublishTopicValidationIssues = ( - this.topic.prepublishValidate()); - let subtopicPrepublishValidationIssues = ( - [].concat.apply([], this.topic.getSubtopics().map( - (subtopic) => subtopic.prepublishValidate()))); - this.prepublishValidationIssues = ( - prepublishTopicValidationIssues.concat( - subtopicPrepublishValidationIssues)); + let prepublishTopicValidationIssues = this.topic.prepublishValidate(); + let subtopicPrepublishValidationIssues = [].concat.apply( + [], + this.topic.getSubtopics().map(subtopic => subtopic.prepublishValidate()) + ); + this.prepublishValidationIssues = prepublishTopicValidationIssues.concat( + subtopicPrepublishValidationIssues + ); } publishTopic(): void { if (!this.topicRights.canPublishTopic()) { - this.ngbModal.open(TopicEditorSendMailComponent, { - backdrop: true - }).result.then(() => { - this.topicRightsBackendApiService.sendMailAsync( - this.topicId, this.topicName).then(() => { - let successToast = 'Mail Sent.'; - this.alertsService.addSuccessMessage( - successToast, 1000); - }); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + this.ngbModal + .open(TopicEditorSendMailComponent, { + backdrop: true, + }) + .result.then( + () => { + this.topicRightsBackendApiService + .sendMailAsync(this.topicId, this.topicName) + .then(() => { + let successToast = 'Mail Sent.'; + this.alertsService.addSuccessMessage(successToast, 1000); + }); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); return; } let redirectToDashboard = false; - this.topicRightsBackendApiService.publishTopicAsync(this.topicId).then( - () => { + this.topicRightsBackendApiService + .publishTopicAsync(this.topicId) + .then(() => { if (!this.topicRights.isPublished()) { redirectToDashboard = true; } this.topicRights.markTopicAsPublished(); this.topicEditorStateService.setTopicRights(this.topicRights); - } - ).then(() => { - let successToast = 'Topic published.'; - if (redirectToDashboard) { - this.windowRef.nativeWindow.location.href = ( - '/topics-and-skills-dashboard'); - } - this.alertsService.addSuccessMessage( - successToast, 1000); - }); + }) + .then(() => { + let successToast = 'Topic published.'; + if (redirectToDashboard) { + this.windowRef.nativeWindow.location.href = + '/topics-and-skills-dashboard'; + } + this.alertsService.addSuccessMessage(successToast, 1000); + }); } discardChanges(): void { @@ -146,10 +150,9 @@ export class TopicEditorNavbarComponent implements OnInit, isTopicSaveable(): boolean { return ( this.getChangeListLength() > 0 && - this.getWarningsCount() === 0 && ( - !this.topicRights.isPublished() || - this.prepublishValidationIssues.length === 0 - ) + this.getWarningsCount() === 0 && + (!this.topicRights.isPublished() || + this.prepublishValidationIssues.length === 0) ); } @@ -158,14 +161,15 @@ export class TopicEditorNavbarComponent implements OnInit, } getAllTopicWarnings(): string { - return this.validationIssues.concat( - ).concat(this.prepublishValidationIssues).join('\n'); + return this.validationIssues + .concat() + .concat(this.prepublishValidationIssues) + .join('\n'); } toggleDiscardChangeButton(): void { this.showTopicEditOptions = false; - this.discardChangesButtonIsShown = ( - !this.discardChangesButtonIsShown); + this.discardChangesButtonIsShown = !this.discardChangesButtonIsShown; } saveChanges(): void { @@ -174,15 +178,18 @@ export class TopicEditorNavbarComponent implements OnInit, backdrop: 'static', }); modelRef.componentInstance.topicIsPublished = isTopicPublished; - modelRef.result.then((commitMessage: string) => { - this.topicEditorStateService.saveTopic(commitMessage, () => { - this.alertsService.addSuccessMessage('Changes Saved.'); - }); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + modelRef.result.then( + (commitMessage: string) => { + this.topicEditorStateService.saveTopic(commitMessage, () => { + this.alertsService.addSuccessMessage('Changes Saved.'); + }); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } unpublishTopic(): boolean { @@ -190,11 +197,12 @@ export class TopicEditorNavbarComponent implements OnInit, if (!this.topicRights.canPublishTopic()) { return false; } - this.topicRightsBackendApiService.unpublishTopicAsync( - this.topicId).then(() => { - this.topicRights.markTopicAsUnpublished(); - this.topicEditorStateService.setTopicRights(this.topicRights); - }); + this.topicRightsBackendApiService + .unpublishTopicAsync(this.topicId) + .then(() => { + this.topicRights.markTopicAsUnpublished(); + this.topicEditorStateService.setTopicRights(this.topicRights); + }); } toggleNavigationOptions(): void { @@ -215,8 +223,8 @@ export class TopicEditorNavbarComponent implements OnInit, getTotalWarningsCount(): number { let validationIssuesCount = this.validationIssues.length; - let prepublishValidationIssuesCount = ( - this.prepublishValidationIssues.length); + let prepublishValidationIssuesCount = + this.prepublishValidationIssues.length; return validationIssuesCount + prepublishValidationIssuesCount; } @@ -242,23 +250,27 @@ export class TopicEditorNavbarComponent implements OnInit, if (this.getChangeListLength() > 0) { this.alertsService.addInfoMessage( 'Please save all pending changes to preview the topic ' + - 'with the changes', 2000); + 'with the changes', + 2000 + ); return; } let topicUrlFragment = this.topic.getUrlFragment(); - let classroomUrlFragment = ( - this.topicEditorStateService.getClassroomUrlFragment()); + let classroomUrlFragment = + this.topicEditorStateService.getClassroomUrlFragment(); this.windowRef.nativeWindow.open( this.urlInterpolationService.interpolateUrl( - ClassroomDomainConstants.TOPIC_VIEWER_URL_TEMPLATE, { + ClassroomDomainConstants.TOPIC_VIEWER_URL_TEMPLATE, + { topic_url_fragment: topicUrlFragment, - classroom_url_fragment: classroomUrlFragment + classroom_url_fragment: classroomUrlFragment, } - ), 'blank'); + ), + 'blank' + ); } else { let subtopicId = this.topicEditorRoutingService.getSubtopicIdFromUrl(); - this.topicEditorRoutingService.navigateToSubtopicPreviewTab( - subtopicId); + this.topicEditorRoutingService.navigateToSubtopicPreviewTab(subtopicId); this.activeTab = 'Preview'; } } @@ -271,8 +283,7 @@ export class TopicEditorNavbarComponent implements OnInit, this.activeTab = 'Editor'; } else { let subtopicId = this.topicEditorRoutingService.getSubtopicIdFromUrl(); - this.topicEditorRoutingService.navigateToSubtopicEditorWithId( - subtopicId); + this.topicEditorRoutingService.navigateToSubtopicEditorWithId(subtopicId); this.activeTab = 'Editor'; } } @@ -289,19 +300,17 @@ export class TopicEditorNavbarComponent implements OnInit, ngOnInit(): void { this.directiveSubscriptions.add( - this.topicEditorStateService.onTopicInitialized.subscribe( - () => { - this.topic = this.topicEditorStateService.getTopic(); - this._validateTopic(); - } - )); + this.topicEditorStateService.onTopicInitialized.subscribe(() => { + this.topic = this.topicEditorStateService.getTopic(); + this._validateTopic(); + }) + ); this.directiveSubscriptions.add( - this.topicEditorStateService.onTopicReinitialized.subscribe( - () => { - this.topic = this.topicEditorStateService.getTopic(); - this._validateTopic(); - } - )); + this.topicEditorStateService.onTopicReinitialized.subscribe(() => { + this.topic = this.topicEditorStateService.getTopic(); + this._validateTopic(); + }) + ); this.topicId = this.urlService.getTopicIdFromUrl(); this.navigationChoices = ['Topic', 'Questions', 'Preview']; this.activeTab = this.getMobileNavigatorText(); @@ -314,12 +323,10 @@ export class TopicEditorNavbarComponent implements OnInit, this.prepublishValidationIssues = []; this.topicRights = this.topicEditorStateService.getTopicRights(); this.directiveSubscriptions.add( - this.undoRedoService.getUndoRedoChangeEventEmitter().subscribe( - () => { - this.topic = this.topicEditorStateService.getTopic(); - this._validateTopic(); - } - ) + this.undoRedoService.getUndoRedoChangeEventEmitter().subscribe(() => { + this.topic = this.topicEditorStateService.getTopic(); + this._validateTopic(); + }) ); } @@ -334,7 +341,9 @@ export class TopicEditorNavbarComponent implements OnInit, } } -angular.module('oppia').directive('oppiaTopicEditorNavbar', +angular.module('oppia').directive( + 'oppiaTopicEditorNavbar', downgradeComponent({ - component: TopicEditorNavbarComponent - }) as angular.IDirectiveFactory); + component: TopicEditorNavbarComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/topic-editor-page/preview-tab/topic-preview-tab.component.spec.ts b/core/templates/pages/topic-editor-page/preview-tab/topic-preview-tab.component.spec.ts index 4c7eb2369df7..732c94509cfc 100644 --- a/core/templates/pages/topic-editor-page/preview-tab/topic-preview-tab.component.spec.ts +++ b/core/templates/pages/topic-editor-page/preview-tab/topic-preview-tab.component.spec.ts @@ -16,23 +16,43 @@ * @fileoverview Unit tests for topic preview tab. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { MaterialModule } from 'modules/material.module'; -import { StorySummary } from 'domain/story/story-summary.model'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { TopicEditorStateService } from '../services/topic-editor-state.service'; -import { TopicPreviewTabComponent } from './topic-preview-tab.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {MaterialModule} from 'modules/material.module'; +import {StorySummary} from 'domain/story/story-summary.model'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {TopicEditorStateService} from '../services/topic-editor-state.service'; +import {TopicPreviewTabComponent} from './topic-preview-tab.component'; describe('Topic Preview Tab Component', () => { let fixture: ComponentFixture; let componentInstance: TopicPreviewTabComponent; let testName = 'test_name'; let mockUrl = 'mock_url'; - let storySummaries = [new StorySummary( - 'id', 'title', [], 'thumbnailFilename', 'thumbnailBgColor', - 'description', false, [], 'url', [], '', '', '', 0, 0, 0, [], 0, [])]; + let storySummaries = [ + new StorySummary( + 'id', + 'title', + [], + 'thumbnailFilename', + 'thumbnailBgColor', + 'description', + false, + [], + 'url', + [], + '', + '', + '', + 0, + 0, + 0, + [], + 0, + [] + ), + ]; class MockTopicEditorStateService { getTopic() { @@ -59,24 +79,19 @@ describe('Topic Preview Tab Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - BrowserAnimationsModule, - MaterialModule - ], - declarations: [ - TopicPreviewTabComponent, - ], + imports: [BrowserAnimationsModule, MaterialModule], + declarations: [TopicPreviewTabComponent], providers: [ { provide: TopicEditorStateService, - useClass: MockTopicEditorStateService + useClass: MockTopicEditorStateService, }, { provide: UrlInterpolationService, - useClass: MockUrlInterpolationService - } + useClass: MockUrlInterpolationService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); diff --git a/core/templates/pages/topic-editor-page/preview-tab/topic-preview-tab.component.ts b/core/templates/pages/topic-editor-page/preview-tab/topic-preview-tab.component.ts index a578a1c84b52..1774b03f14a9 100644 --- a/core/templates/pages/topic-editor-page/preview-tab/topic-preview-tab.component.ts +++ b/core/templates/pages/topic-editor-page/preview-tab/topic-preview-tab.component.ts @@ -16,17 +16,17 @@ * @fileoverview Component for the topic preview tab. */ -import { Component } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { StorySummary } from 'domain/story/story-summary.model'; -import { Subtopic } from 'domain/topic/subtopic.model'; -import { Topic } from 'domain/topic/topic-object.model'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { TopicEditorStateService } from '../services/topic-editor-state.service'; +import {Component} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {StorySummary} from 'domain/story/story-summary.model'; +import {Subtopic} from 'domain/topic/subtopic.model'; +import {Topic} from 'domain/topic/topic-object.model'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {TopicEditorStateService} from '../services/topic-editor-state.service'; @Component({ selector: 'oppia-topic-preview-tab', - templateUrl: './topic-preview-tab.component.html' + templateUrl: './topic-preview-tab.component.html', }) export class TopicPreviewTabComponent { private _TAB_STORY: string = 'story'; @@ -51,11 +51,11 @@ export class TopicPreviewTabComponent { this.topic = this.topicEditorStateService.getTopic(); this.topicName = this.topic.getName(); this.subtopics = this.topic.getSubtopics(); - this.cannonicalStorySummaries = ( - this.topicEditorStateService.getCanonicalStorySummaries()); + this.cannonicalStorySummaries = + this.topicEditorStateService.getCanonicalStorySummaries(); for (let idx in this.cannonicalStorySummaries) { - this.chapterCount += ( - this.cannonicalStorySummaries[idx].getNodeTitles().length); + this.chapterCount += + this.cannonicalStorySummaries[idx].getNodeTitles().length; } } @@ -78,7 +78,9 @@ export class TopicPreviewTabComponent { } } -angular.module('oppia').directive('oppiaTopicPreviewTab', +angular.module('oppia').directive( + 'oppiaTopicPreviewTab', downgradeComponent({ - component: TopicPreviewTabComponent - }) as angular.IDirectiveFactory); + component: TopicPreviewTabComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/topic-editor-page/questions-tab/topic-questions-tab.component.spec.ts b/core/templates/pages/topic-editor-page/questions-tab/topic-questions-tab.component.spec.ts index 58769f549018..093b1e89fad3 100644 --- a/core/templates/pages/topic-editor-page/questions-tab/topic-questions-tab.component.spec.ts +++ b/core/templates/pages/topic-editor-page/questions-tab/topic-questions-tab.component.spec.ts @@ -16,18 +16,24 @@ * @fileoverview Unit tests for topic questions tab. */ -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { QuestionsListService } from 'services/questions-list.service'; -import { SkillSummary, SkillSummaryBackendDict } from 'domain/skill/skill-summary.model'; -import { Subtopic } from 'domain/topic/subtopic.model'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { TopicRights } from 'domain/topic/topic-rights.model'; -import { TopicsAndSkillsDashboardBackendApiService, TopicsAndSkillDashboardData } from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TopicQuestionsTabComponent } from './topic-questions-tab.component'; -import { TopicEditorStateService } from '../services/topic-editor-state.service'; -import { Topic } from 'domain/topic/topic-object.model'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {QuestionsListService} from 'services/questions-list.service'; +import { + SkillSummary, + SkillSummaryBackendDict, +} from 'domain/skill/skill-summary.model'; +import {Subtopic} from 'domain/topic/subtopic.model'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {TopicRights} from 'domain/topic/topic-rights.model'; +import { + TopicsAndSkillsDashboardBackendApiService, + TopicsAndSkillDashboardData, +} from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TopicQuestionsTabComponent} from './topic-questions-tab.component'; +import {TopicEditorStateService} from '../services/topic-editor-state.service'; +import {Topic} from 'domain/topic/topic-object.model'; let skillSummaryBackendDict: SkillSummaryBackendDict = { id: 'test_id', @@ -37,7 +43,7 @@ let skillSummaryBackendDict: SkillSummaryBackendDict = { misconception_count: 0, worked_examples_count: 1, skill_model_created_on: 2, - skill_model_last_updated: 3 + skill_model_last_updated: 3, }; let categorizedSkillsDictData: { @@ -49,13 +55,12 @@ let categorizedSkillsDictData: { let skillIdToRubricsObject = {}; -let untriagedSkillSummariesData: SkillSummary[] = ( - [SkillSummary.createFromBackendDict(skillSummaryBackendDict)]); +let untriagedSkillSummariesData: SkillSummary[] = [ + SkillSummary.createFromBackendDict(skillSummaryBackendDict), +]; const topicsAndSkillsDashboardData: TopicsAndSkillDashboardData = { - allClassroomNames: [ - 'math' - ], + allClassroomNames: ['math'], canDeleteTopic: true, canCreateTopic: true, canDeleteSkill: true, @@ -70,8 +75,8 @@ const topicsAndSkillsDashboardData: TopicsAndSkillDashboardData = { misconceptionCount: 0, workedExamplesCount: 0, skillModelCreatedOn: 1622827020924.104, - skillModelLastUpdated: 1622827020924.109 - } + skillModelLastUpdated: 1622827020924.109, + }, ], totalSkillCount: 1, topicSummaries: [], @@ -89,7 +94,7 @@ class MockTopicsAndSkillsDashboardBackendApiService { return { then: (callback: (resp: TopicsAndSkillDashboardData) => void) => { callback(topicsAndSkillsDashboardData); - } + }, }; } } @@ -113,10 +118,10 @@ describe('Topic questions tab', () => { TopicsAndSkillsDashboardBackendApiService, { provide: TopicsAndSkillsDashboardBackendApiService, - useClass: MockTopicsAndSkillsDashboardBackendApiService - } + useClass: MockTopicsAndSkillsDashboardBackendApiService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); @@ -130,9 +135,25 @@ describe('Topic questions tab', () => { topicReinitializedEventEmitter = new EventEmitter(); topic = new Topic( - '', 'Topic name loading', 'Abbrev. name loading', - 'Url Fragment loading', 'Topic description loading', 'en', - [], [], [], 1, 1, [], '', '', {}, false, '', '', [] + '', + 'Topic name loading', + 'Abbrev. name loading', + 'Url Fragment loading', + 'Topic description loading', + 'en', + [], + [], + [], + 1, + 1, + [], + '', + '', + {}, + false, + '', + '', + [] ); subtopic1 = Subtopic.createFromTitle(1, 'Subtopic1'); subtopic1.addSkill('skill1', 'subtopic1 skill'); @@ -145,12 +166,13 @@ describe('Topic questions tab', () => { spyOnProperty(topicEditorStateService, 'onTopicInitialized').and.callFake( () => { return topicInitializedEventEmitter; - }); - spyOnProperty( - topicEditorStateService, 'onTopicReinitialized').and.callFake( + } + ); + spyOnProperty(topicEditorStateService, 'onTopicReinitialized').and.callFake( () => { return topicReinitializedEventEmitter; - }); + } + ); component.ngOnInit(); }); @@ -161,20 +183,24 @@ describe('Topic questions tab', () => { it('should initialize the variables when topic is initialized', () => { const topicRights = new TopicRights(false, false, false); const allSkillSummaries = subtopic1.getSkillSummaries(); - spyOn(topicEditorStateService, 'getSkillIdToRubricsObject').and - .returnValue(skillIdToRubricsObject); + spyOn(topicEditorStateService, 'getSkillIdToRubricsObject').and.returnValue( + skillIdToRubricsObject + ); expect(focusManagerService.setFocus).toHaveBeenCalledWith( - 'selectSkillField'); + 'selectSkillField' + ); expect(component.selectedSkillId).toBeUndefined(); expect(component.topic).toBe(topic); expect(component.topicRights).toEqual(topicRights); expect(component.skillIdToRubricsObject).toEqual(skillIdToRubricsObject); expect(component.allSkillSummaries).toEqual(allSkillSummaries); - expect( - component.getSkillsCategorizedByTopics).toBe(categorizedSkillsDictData); - expect(component.getUntriagedSkillSummaries) - .toBe(untriagedSkillSummariesData); + expect(component.getSkillsCategorizedByTopics).toBe( + categorizedSkillsDictData + ); + expect(component.getUntriagedSkillSummaries).toBe( + untriagedSkillSummariesData + ); expect(component.canEditQuestion).toBe(false); }); @@ -182,7 +208,8 @@ describe('Topic questions tab', () => { component.ngAfterViewInit(); expect(focusManagerService.setFocus).toHaveBeenCalledWith( - 'selectSkillField'); + 'selectSkillField' + ); }); it('should unsubscribe when component is destroyed', () => { diff --git a/core/templates/pages/topic-editor-page/questions-tab/topic-questions-tab.component.ts b/core/templates/pages/topic-editor-page/questions-tab/topic-questions-tab.component.ts index 45966c8f05b6..5b34d49d7a74 100644 --- a/core/templates/pages/topic-editor-page/questions-tab/topic-questions-tab.component.ts +++ b/core/templates/pages/topic-editor-page/questions-tab/topic-questions-tab.component.ts @@ -16,24 +16,28 @@ * @fileoverview Component for the questions tab. */ -import { AfterViewInit, Component, OnDestroy, OnInit } from '@angular/core'; -import { Topic } from 'domain/topic/topic-object.model'; -import { TopicRights } from 'domain/topic/topic-rights.model'; -import { CategorizedSkills, TopicsAndSkillsDashboardBackendApiService } from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; -import { Subscription } from 'rxjs'; -import { QuestionsListService } from 'services/questions-list.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { TopicEditorStateService } from '../services/topic-editor-state.service'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { SkillSummary } from 'domain/skill/skill-summary.model'; -import { ShortSkillSummary } from 'domain/skill/short-skill-summary.model'; +import {AfterViewInit, Component, OnDestroy, OnInit} from '@angular/core'; +import {Topic} from 'domain/topic/topic-object.model'; +import {TopicRights} from 'domain/topic/topic-rights.model'; +import { + CategorizedSkills, + TopicsAndSkillsDashboardBackendApiService, +} from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; +import {Subscription} from 'rxjs'; +import {QuestionsListService} from 'services/questions-list.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {TopicEditorStateService} from '../services/topic-editor-state.service'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {SkillSummary} from 'domain/skill/skill-summary.model'; +import {ShortSkillSummary} from 'domain/skill/short-skill-summary.model'; @Component({ selector: 'oppia-topic-questions-tab', - templateUrl: './topic-questions-tab.component.html' + templateUrl: './topic-questions-tab.component.html', }) -export class TopicQuestionsTabComponent implements OnInit, - AfterViewInit, OnDestroy { +export class TopicQuestionsTabComponent + implements OnInit, AfterViewInit, OnDestroy +{ topic!: Topic; topicRights!: TopicRights; groupedSkillSummaries!: object; @@ -47,9 +51,8 @@ export class TopicQuestionsTabComponent implements OnInit, constructor( private focusManagerService: FocusManagerService, private questionsListService: QuestionsListService, - private topicsAndSkillsDashboardBackendApiService: - TopicsAndSkillsDashboardBackendApiService, - private topicEditorStateService: TopicEditorStateService, + private topicsAndSkillsDashboardBackendApiService: TopicsAndSkillsDashboardBackendApiService, + private topicEditorStateService: TopicEditorStateService ) {} directiveSubscriptions = new Subscription(); @@ -57,24 +60,25 @@ export class TopicQuestionsTabComponent implements OnInit, _initTab(): void { this.topic = this.topicEditorStateService.getTopic(); this.topicRights = this.topicEditorStateService.getTopicRights(); - this.groupedSkillSummaries = ( - this.topicEditorStateService.getGroupedSkillSummaries()); - this.skillIdToRubricsObject = ( - this.topicEditorStateService.getSkillIdToRubricsObject()); + this.groupedSkillSummaries = + this.topicEditorStateService.getGroupedSkillSummaries(); + this.skillIdToRubricsObject = + this.topicEditorStateService.getSkillIdToRubricsObject(); this.allSkillSummaries = []; this.allSkillSummaries = this.allSkillSummaries.concat( - this.topic.getUncategorizedSkillSummaries()); + this.topic.getUncategorizedSkillSummaries() + ); for (let i = 0; i < this.topic.getSubtopics().length; i++) { let subtopic = this.topic.getSubtopics()[i]; this.allSkillSummaries = this.allSkillSummaries.concat( - subtopic.getSkillSummaries()); + subtopic.getSkillSummaries() + ); } - this.topicsAndSkillsDashboardBackendApiService.fetchDashboardDataAsync() - .then((response) => { - this.getSkillsCategorizedByTopics = ( - response.categorizedSkillsDict); - this.getUntriagedSkillSummaries = ( - response.untriagedSkillSummaries); + this.topicsAndSkillsDashboardBackendApiService + .fetchDashboardDataAsync() + .then(response => { + this.getSkillsCategorizedByTopics = response.categorizedSkillsDict; + this.getUntriagedSkillSummaries = response.untriagedSkillSummaries; }); this.canEditQuestion = this.topicRights.canEditTopic(); } @@ -82,9 +86,7 @@ export class TopicQuestionsTabComponent implements OnInit, reinitializeQuestionsList(skillId: string): void { this.selectedSkillId = skillId; this.questionsListService.resetPageNumber(); - this.questionsListService.getQuestionSummariesAsync( - skillId, true, true - ); + this.questionsListService.getQuestionSummariesAsync(skillId, true, true); } ngAfterViewInit(): void { @@ -97,13 +99,15 @@ export class TopicQuestionsTabComponent implements OnInit, // question-editor-tab. this.focusManagerService.setFocus('selectSkillField'); this.directiveSubscriptions.add( - this.topicEditorStateService.onTopicInitialized.subscribe( - () => this._initTab() - )); + this.topicEditorStateService.onTopicInitialized.subscribe(() => + this._initTab() + ) + ); this.directiveSubscriptions.add( - this.topicEditorStateService.onTopicReinitialized.subscribe( - () => this._initTab() - )); + this.topicEditorStateService.onTopicReinitialized.subscribe(() => + this._initTab() + ) + ); this._initTab(); } @@ -112,7 +116,9 @@ export class TopicQuestionsTabComponent implements OnInit, } } -angular.module('oppia').directive('oppiaTopicQuestionsTab', +angular.module('oppia').directive( + 'oppiaTopicQuestionsTab', downgradeComponent({ - component: TopicQuestionsTabComponent - }) as angular.IDirectiveFactory); + component: TopicQuestionsTabComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/topic-editor-page/services/create-new-skill-modal.service.spec.ts b/core/templates/pages/topic-editor-page/services/create-new-skill-modal.service.spec.ts index 4231215237f2..f2bace2df390 100644 --- a/core/templates/pages/topic-editor-page/services/create-new-skill-modal.service.spec.ts +++ b/core/templates/pages/topic-editor-page/services/create-new-skill-modal.service.spec.ts @@ -16,24 +16,23 @@ * @fileoverview Unit tests for CreateNewSkillModalService. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { Rubric } from 'domain/skill/rubric.model'; -import { SkillCreationBackendApiService } from 'domain/skill/skill-creation-backend-api.service'; -import { TopicsAndSkillsDashboardBackendApiService } from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { AlertsService } from 'services/alerts.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { ImageLocalStorageService } from 'services/image-local-storage.service'; -import { CreateNewSkillModalService } from './create-new-skill-modal.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {fakeAsync, TestBed, tick, waitForAsync} from '@angular/core/testing'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {Rubric} from 'domain/skill/rubric.model'; +import {SkillCreationBackendApiService} from 'domain/skill/skill-creation-backend-api.service'; +import {TopicsAndSkillsDashboardBackendApiService} from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {AlertsService} from 'services/alerts.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {ImageLocalStorageService} from 'services/image-local-storage.service'; +import {CreateNewSkillModalService} from './create-new-skill-modal.service'; describe('Create New Skill Modal Service', () => { let createNewSkillModalService: CreateNewSkillModalService; let alertsService: AlertsService; let imageLocalStorageService: ImageLocalStorageService; - let topicsAndSkillsDashboardBackendApiService: - TopicsAndSkillsDashboardBackendApiService; + let topicsAndSkillsDashboardBackendApiService: TopicsAndSkillsDashboardBackendApiService; let mockSkillCreationBackendApiService: MockSkillCreationBackendApiService; class MockNgbModal { @@ -44,13 +43,13 @@ describe('Create New Skill Modal Service', () => { let result = { rubrics: { rubric1: { - toBackendDict: () => {} - } - } + toBackendDict: () => {}, + }, + }, }; callb(result); - } - } + }, + }, }; } } @@ -59,25 +58,25 @@ describe('Create New Skill Modal Service', () => { throwError: boolean = false; message!: string; createSkillAsync( - description: string, - rubrics: Rubric[], - explanation: string, - topicsIds: string[], - imagesData: ImageData[] + description: string, + rubrics: Rubric[], + explanation: string, + topicsIds: string[], + imagesData: ImageData[] ): object { return { then: ( - successCallback: (response: { skillId: string }) => void, - errorCallback: (errorMessage: string) => void + successCallback: (response: {skillId: string}) => void, + errorCallback: (errorMessage: string) => void ) => { if (this.throwError) { errorCallback(this.message); } else { successCallback({ - skillId: 'test_id' + skillId: 'test_id', }); } - } + }, }; } } @@ -87,10 +86,10 @@ describe('Create New Skill Modal Service', () => { open: () => { return { location: { - href: '' - } + href: '', + }, }; - } + }, }; } @@ -100,11 +99,11 @@ describe('Create New Skill Modal Service', () => { providers: [ { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, { provide: SkillCreationBackendApiService, - useClass: MockSkillCreationBackendApiService + useClass: MockSkillCreationBackendApiService, }, CreateNewSkillModalService, AlertsService, @@ -113,9 +112,9 @@ describe('Create New Skill Modal Service', () => { ImageLocalStorageService, { provide: WindowRef, - useClass: MockWindowRef - } - ] + useClass: MockWindowRef, + }, + ], }).compileComponents(); })); @@ -124,23 +123,22 @@ describe('Create New Skill Modal Service', () => { alertsService = TestBed.inject(AlertsService); alertsService = alertsService as jasmine.SpyObj; imageLocalStorageService = TestBed.inject(ImageLocalStorageService); - imageLocalStorageService = imageLocalStorageService as - jasmine.SpyObj; - topicsAndSkillsDashboardBackendApiService = - TestBed.inject(TopicsAndSkillsDashboardBackendApiService); - topicsAndSkillsDashboardBackendApiService = ( - topicsAndSkillsDashboardBackendApiService as - jasmine.SpyObj + imageLocalStorageService = + imageLocalStorageService as jasmine.SpyObj; + topicsAndSkillsDashboardBackendApiService = TestBed.inject( + TopicsAndSkillsDashboardBackendApiService ); - mockSkillCreationBackendApiService = ( + topicsAndSkillsDashboardBackendApiService = + topicsAndSkillsDashboardBackendApiService as jasmine.SpyObj; + mockSkillCreationBackendApiService = // This throws "Type 'MockSkillCreationBackendApiService' is not // assignable to type desire". We need to suppress this error because of // the need to test validations. This happens because the // MockSkillCreationBackendApiService is a class and not an interface. // @ts-ignore - TestBed.inject(SkillCreationBackendApiService) as - MockSkillCreationBackendApiService - ); + TestBed.inject( + SkillCreationBackendApiService + ) as MockSkillCreationBackendApiService; }); it('should not create new skill when skill creation is in progress', () => { @@ -152,20 +150,20 @@ describe('Create New Skill Modal Service', () => { it('should create new skill', fakeAsync(() => { spyOn(alertsService, 'clearWarnings'); - spyOn(imageLocalStorageService, 'getStoredImagesData').and - .returnValue([]); + spyOn(imageLocalStorageService, 'getStoredImagesData').and.returnValue([]); spyOn(imageLocalStorageService, 'flushStoredImagesData'); spyOn( - topicsAndSkillsDashboardBackendApiService. - onTopicsAndSkillsDashboardReinitialized, 'emit'); + topicsAndSkillsDashboardBackendApiService.onTopicsAndSkillsDashboardReinitialized, + 'emit' + ); createNewSkillModalService.createNewSkill(['klsadlfj223']); tick(500); expect(createNewSkillModalService.skillCreationInProgress).toBeFalse(); expect(alertsService.clearWarnings).toHaveBeenCalled(); expect( topicsAndSkillsDashboardBackendApiService - .onTopicsAndSkillsDashboardReinitialized.emit) - .toHaveBeenCalledWith(true); + .onTopicsAndSkillsDashboardReinitialized.emit + ).toHaveBeenCalledWith(true); expect(createNewSkillModalService.skillCreationInProgress).toBeFalse(); expect(imageLocalStorageService.flushStoredImagesData).toHaveBeenCalled(); })); @@ -173,8 +171,7 @@ describe('Create New Skill Modal Service', () => { it('should handle error when creating skill', () => { spyOn(alertsService, 'clearWarnings'); spyOn(alertsService, 'addWarning'); - spyOn(imageLocalStorageService, 'getStoredImagesData').and - .returnValue([]); + spyOn(imageLocalStorageService, 'getStoredImagesData').and.returnValue([]); mockSkillCreationBackendApiService.throwError = true; let message = 'error_message'; mockSkillCreationBackendApiService.message = message; diff --git a/core/templates/pages/topic-editor-page/services/create-new-skill-modal.service.ts b/core/templates/pages/topic-editor-page/services/create-new-skill-modal.service.ts index 311afcd41e17..0490add7d971 100644 --- a/core/templates/pages/topic-editor-page/services/create-new-skill-modal.service.ts +++ b/core/templates/pages/topic-editor-page/services/create-new-skill-modal.service.ts @@ -16,19 +16,19 @@ * @fileoverview Functionality for showing skill modal. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { SkillCreationBackendApiService } from 'domain/skill/skill-creation-backend-api.service'; -import { TopicsAndSkillsDashboardBackendApiService } from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { CreateNewSkillModalComponent } from 'pages/topics-and-skills-dashboard-page/modals/create-new-skill-modal.component'; -import { AlertsService } from 'services/alerts.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { ImageLocalStorageService } from 'services/image-local-storage.service'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {SkillCreationBackendApiService} from 'domain/skill/skill-creation-backend-api.service'; +import {TopicsAndSkillsDashboardBackendApiService} from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {CreateNewSkillModalComponent} from 'pages/topics-and-skills-dashboard-page/modals/create-new-skill-modal.component'; +import {AlertsService} from 'services/alerts.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {ImageLocalStorageService} from 'services/image-local-storage.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class CreateNewSkillModalService { skillCreationInProgress: boolean = false; @@ -40,62 +40,78 @@ export class CreateNewSkillModalService { private windowRef: WindowRef, private imageLocalStorageService: ImageLocalStorageService, private skillCreationBackendApiService: SkillCreationBackendApiService, - private topicsAndSkillsDashboardBackendApiService: - TopicsAndSkillsDashboardBackendApiService, + private topicsAndSkillsDashboardBackendApiService: TopicsAndSkillsDashboardBackendApiService, private urlInterpolationService: UrlInterpolationService ) {} createNewSkill(topicsIds: string[] = []): void { - const modalRef = this.ngbModal.open( - CreateNewSkillModalComponent, { - windowClass: 'create-new-skill-modal', - backdrop: 'static' - }); - modalRef.result.then((result) => { - if (this.skillCreationInProgress) { - return; - } + const modalRef = this.ngbModal.open(CreateNewSkillModalComponent, { + windowClass: 'create-new-skill-modal', + backdrop: 'static', + }); + modalRef.result.then( + result => { + if (this.skillCreationInProgress) { + return; + } - let rubrics = result.rubrics; - for (let idx in rubrics) { - rubrics[idx] = rubrics[idx].toBackendDict(); - } - this.skillCreationInProgress = true; - this.alertsService.clearWarnings(); - // The window.open has to be initialized separately since if the 'open - // new tab' action does not directly result from a user input - // (which is not the case, if we wait for result from the backend - // before opening a new tab), some browsers block it as a popup. - // Here, the new tab is created as soon as the user clicks the - // 'Create' button and filled with URL once the details are - // fetched from the backend. - let newTab = this.windowRef.nativeWindow.open() as WindowProxy; - let imagesData = this.imageLocalStorageService.getStoredImagesData(); - this.skillCreationBackendApiService.createSkillAsync( - result.description, rubrics, result.explanation, - topicsIds, imagesData).then((response) => { - setTimeout(() => { - this.topicsAndSkillsDashboardBackendApiService. - onTopicsAndSkillsDashboardReinitialized.emit(true); - this.skillCreationInProgress = false; - this.imageLocalStorageService.flushStoredImagesData(); - newTab.location.href = - this.urlInterpolationService.interpolateUrl( - this.CREATE_NEW_SKILL_URL_TEMPLATE, { - skill_id: response.skillId + let rubrics = result.rubrics; + for (let idx in rubrics) { + rubrics[idx] = rubrics[idx].toBackendDict(); + } + this.skillCreationInProgress = true; + this.alertsService.clearWarnings(); + // The window.open has to be initialized separately since if the 'open + // new tab' action does not directly result from a user input + // (which is not the case, if we wait for result from the backend + // before opening a new tab), some browsers block it as a popup. + // Here, the new tab is created as soon as the user clicks the + // 'Create' button and filled with URL once the details are + // fetched from the backend. + let newTab = this.windowRef.nativeWindow.open() as WindowProxy; + let imagesData = this.imageLocalStorageService.getStoredImagesData(); + this.skillCreationBackendApiService + .createSkillAsync( + result.description, + rubrics, + result.explanation, + topicsIds, + imagesData + ) + .then( + response => { + setTimeout(() => { + this.topicsAndSkillsDashboardBackendApiService.onTopicsAndSkillsDashboardReinitialized.emit( + true + ); + this.skillCreationInProgress = false; + this.imageLocalStorageService.flushStoredImagesData(); + newTab.location.href = + this.urlInterpolationService.interpolateUrl( + this.CREATE_NEW_SKILL_URL_TEMPLATE, + { + skill_id: response.skillId, + } + ); + }, 150); + }, + errorMessage => { + this.alertsService.addWarning(errorMessage); } ); - }, 150); - }, (errorMessage) => { - this.alertsService.addWarning(errorMessage); - }); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is - // clicked. No further action is needed. - }); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is + // clicked. No further action is needed. + } + ); } } -angular.module('oppia').service('CreateNewSkillModalService', - downgradeInjectable(CreateNewSkillModalService)); +angular + .module('oppia') + .service( + 'CreateNewSkillModalService', + downgradeInjectable(CreateNewSkillModalService) + ); diff --git a/core/templates/pages/topic-editor-page/services/entity-creation.service.spec.ts b/core/templates/pages/topic-editor-page/services/entity-creation.service.spec.ts index 2cf01cf85c81..434c0b4f5d57 100644 --- a/core/templates/pages/topic-editor-page/services/entity-creation.service.spec.ts +++ b/core/templates/pages/topic-editor-page/services/entity-creation.service.spec.ts @@ -16,23 +16,23 @@ * @fileoverview Unit tests for EntityCreationService. */ -import { Subtopic } from 'domain/topic/subtopic.model'; -import { NgbModal, NgbModalRef, NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { Topic } from 'domain/topic/topic-object.model'; -import { TopicEditorStateService } from './topic-editor-state.service'; -import { TopicEditorRoutingService } from './topic-editor-routing.service'; -import { EntityCreationService } from './entity-creation.service'; -import { CreateNewSkillModalService } from './create-new-skill-modal.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; -import { CreateNewSubtopicModalComponent } from '../modal-templates/create-new-subtopic-modal.component'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import {Subtopic} from 'domain/topic/subtopic.model'; +import {NgbModal, NgbModalRef, NgbModule} from '@ng-bootstrap/ng-bootstrap'; +import {fakeAsync, TestBed, tick, waitForAsync} from '@angular/core/testing'; +import {Topic} from 'domain/topic/topic-object.model'; +import {TopicEditorStateService} from './topic-editor-state.service'; +import {TopicEditorRoutingService} from './topic-editor-routing.service'; +import {EntityCreationService} from './entity-creation.service'; +import {CreateNewSkillModalService} from './create-new-skill-modal.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {BrowserDynamicTestingModule} from '@angular/platform-browser-dynamic/testing'; +import {CreateNewSubtopicModalComponent} from '../modal-templates/create-new-subtopic-modal.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; class MockNgbModal { - open(): { result: Promise } { + open(): {result: Promise} { return { - result: Promise.resolve('1') + result: Promise.resolve('1'), }; } } @@ -47,28 +47,25 @@ describe('Entity creation service', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - NgbModule - ], - declarations: [ - CreateNewSubtopicModalComponent - ], + imports: [HttpClientTestingModule, NgbModule], + declarations: [CreateNewSubtopicModalComponent], providers: [ TopicEditorStateService, TopicEditorRoutingService, CreateNewSkillModalService, { provide: NgbModal, - useClass: MockNgbModal - } + useClass: MockNgbModal, + }, ], - schemas: [NO_ERRORS_SCHEMA] - }).overrideModule(BrowserDynamicTestingModule, { - set: { - entryComponents: [CreateNewSubtopicModalComponent], - } - }).compileComponents(); + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideModule(BrowserDynamicTestingModule, { + set: { + entryComponents: [CreateNewSubtopicModalComponent], + }, + }) + .compileComponents(); })); beforeEach(() => { @@ -78,9 +75,25 @@ describe('Entity creation service', () => { entityCreationService = TestBed.inject(EntityCreationService); createNewSkillModalService = TestBed.inject(CreateNewSkillModalService); topic = new Topic( - 'id', 'Topic name loading', 'Abbrev. name loading', - 'Url Fragment loading', 'Topic description loading', 'en', - [], [], [], 1, 1, [], 'str', '', {}, false, '', '', [] + 'id', + 'Topic name loading', + 'Abbrev. name loading', + 'Url Fragment loading', + 'Topic description loading', + 'en', + [], + [], + [], + 1, + 1, + [], + 'str', + '', + {}, + false, + '', + '', + [] ); let subtopic1 = Subtopic.createFromTitle(1, 'Subtopic1'); let subtopic2 = Subtopic.createFromTitle(2, 'Subtopic2'); @@ -94,14 +107,14 @@ describe('Entity creation service', () => { spyOn(topicEditorStateService, 'getTopic').and.returnValue(topic); }); - it('should call TopicEditorRoutingService to navigate to subtopic editor', - fakeAsync(() => { - spyOn(topicEditorRoutingService, 'navigateToSubtopicEditorWithId'); - entityCreationService.createSubtopic(); - tick(); - expect(topicEditorRoutingService.navigateToSubtopicEditorWithId) - .toHaveBeenCalled(); - })); + it('should call TopicEditorRoutingService to navigate to subtopic editor', fakeAsync(() => { + spyOn(topicEditorRoutingService, 'navigateToSubtopicEditorWithId'); + entityCreationService.createSubtopic(); + tick(); + expect( + topicEditorRoutingService.navigateToSubtopicEditorWithId + ).toHaveBeenCalled(); + })); it('should open create subtopic modal', () => { let spy = spyOn(ngbModal, 'open').and.callThrough(); @@ -110,28 +123,29 @@ describe('Entity creation service', () => { expect(spy).toHaveBeenCalled(); }); - it('should close create subtopic modal when cancel button is clicked', - () => { - spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ - result: Promise.reject() - } as NgbModalRef); - }); - let routingSpy = ( - spyOn(topicEditorRoutingService, 'navigateToSubtopicEditorWithId')); + it('should close create subtopic modal when cancel button is clicked', () => { + spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { + return { + result: Promise.reject(), + } as NgbModalRef; + }); + let routingSpy = spyOn( + topicEditorRoutingService, + 'navigateToSubtopicEditorWithId' + ); - entityCreationService.createSubtopic(); + entityCreationService.createSubtopic(); - expect(routingSpy).not.toHaveBeenCalledWith('1'); - }); + expect(routingSpy).not.toHaveBeenCalledWith('1'); + }); - it('should call CreateNewSkillModalService to navigate to skill editor', - () => { - spyOn(createNewSkillModalService, 'createNewSkill'); + it('should call CreateNewSkillModalService to navigate to skill editor', () => { + spyOn(createNewSkillModalService, 'createNewSkill'); - entityCreationService.createSkill(); + entityCreationService.createSkill(); - expect(createNewSkillModalService.createNewSkill) - .toHaveBeenCalledWith(['1']); - }); + expect(createNewSkillModalService.createNewSkill).toHaveBeenCalledWith([ + '1', + ]); + }); }); diff --git a/core/templates/pages/topic-editor-page/services/entity-creation.service.ts b/core/templates/pages/topic-editor-page/services/entity-creation.service.ts index c2233b404037..2de6794f8ba8 100644 --- a/core/templates/pages/topic-editor-page/services/entity-creation.service.ts +++ b/core/templates/pages/topic-editor-page/services/entity-creation.service.ts @@ -18,16 +18,16 @@ * an entity. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { CreateNewSubtopicModalComponent } from 'pages/topic-editor-page/modal-templates/create-new-subtopic-modal.component'; -import { CreateNewSkillModalService } from './create-new-skill-modal.service'; -import { TopicEditorRoutingService } from './topic-editor-routing.service'; -import { TopicEditorStateService } from './topic-editor-state.service'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {CreateNewSubtopicModalComponent} from 'pages/topic-editor-page/modal-templates/create-new-subtopic-modal.component'; +import {CreateNewSkillModalService} from './create-new-skill-modal.service'; +import {TopicEditorRoutingService} from './topic-editor-routing.service'; +import {TopicEditorStateService} from './topic-editor-state.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class EntityCreationService { constructor( @@ -38,16 +38,23 @@ export class EntityCreationService { ) {} createSubtopic(): void { - this.ngbModal.open(CreateNewSubtopicModalComponent, { - backdrop: 'static', - windowClass: 'create-new-subtopic' - }).result.then((subtopicId) => { - this.topicEditorRoutingService.navigateToSubtopicEditorWithId(subtopicId); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + this.ngbModal + .open(CreateNewSubtopicModalComponent, { + backdrop: 'static', + windowClass: 'create-new-subtopic', + }) + .result.then( + subtopicId => { + this.topicEditorRoutingService.navigateToSubtopicEditorWithId( + subtopicId + ); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } createSkill(): void { @@ -56,5 +63,6 @@ export class EntityCreationService { } } -angular.module('oppia').factory('EntityCreationService', - downgradeInjectable(EntityCreationService)); +angular + .module('oppia') + .factory('EntityCreationService', downgradeInjectable(EntityCreationService)); diff --git a/core/templates/pages/topic-editor-page/services/subtopic-validation.service.spec.ts b/core/templates/pages/topic-editor-page/services/subtopic-validation.service.spec.ts index 86da013e96d7..b62f423b53fa 100644 --- a/core/templates/pages/topic-editor-page/services/subtopic-validation.service.spec.ts +++ b/core/templates/pages/topic-editor-page/services/subtopic-validation.service.spec.ts @@ -16,12 +16,12 @@ * @fileoverview Unit tests for SubtopicValidationService. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TestBed, waitForAsync } from '@angular/core/testing'; -import { Subtopic } from 'domain/topic/subtopic.model'; -import { Topic } from 'domain/topic/topic-object.model'; -import { SubtopicValidationService } from './subtopic-validation.service'; -import { TopicEditorStateService } from './topic-editor-state.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TestBed, waitForAsync} from '@angular/core/testing'; +import {Subtopic} from 'domain/topic/subtopic.model'; +import {Topic} from 'domain/topic/topic-object.model'; +import {SubtopicValidationService} from './subtopic-validation.service'; +import {TopicEditorStateService} from './topic-editor-state.service'; describe('Subtopic validation service', () => { let subtopicValidationService: SubtopicValidationService; @@ -29,12 +29,8 @@ describe('Subtopic validation service', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - providers: [ - TopicEditorStateService, - ] + imports: [HttpClientTestingModule], + providers: [TopicEditorStateService], }).compileComponents(); })); @@ -43,9 +39,25 @@ describe('Subtopic validation service', () => { topicEditorStateService = TestBed.inject(TopicEditorStateService); let topic = new Topic( - 'id', 'Topic name loading', 'Abbrev. name loading', - 'Url Fragment loading', 'Topic description loading', 'en', - [], [], [], 1, 1, [], 'str', '', {}, false, '', '', [] + 'id', + 'Topic name loading', + 'Abbrev. name loading', + 'Url Fragment loading', + 'Topic description loading', + 'en', + [], + [], + [], + 1, + 1, + [], + 'str', + '', + {}, + false, + '', + '', + [] ); let subtopic1 = Subtopic.createFromTitle(1, 'Subtopic1'); subtopic1.setUrlFragment('subtopic-one'); @@ -60,35 +72,51 @@ describe('Subtopic validation service', () => { }); it('should validate subtopic name correctly', () => { - expect(subtopicValidationService.checkValidSubtopicName( - 'Random name')).toEqual(true); - expect(subtopicValidationService.checkValidSubtopicName( - 'Subtopic1')).toEqual(false); - expect(subtopicValidationService.checkValidSubtopicName( - 'Subtopic2')).toEqual(false); - expect(subtopicValidationService.checkValidSubtopicName( - 'Subtopic3')).toEqual(false); - expect(subtopicValidationService.checkValidSubtopicName( - 'Subtopic4')).toEqual(true); + expect( + subtopicValidationService.checkValidSubtopicName('Random name') + ).toEqual(true); + expect( + subtopicValidationService.checkValidSubtopicName('Subtopic1') + ).toEqual(false); + expect( + subtopicValidationService.checkValidSubtopicName('Subtopic2') + ).toEqual(false); + expect( + subtopicValidationService.checkValidSubtopicName('Subtopic3') + ).toEqual(false); + expect( + subtopicValidationService.checkValidSubtopicName('Subtopic4') + ).toEqual(true); }); it('should validate if subtopic with url fragment exists', () => { - expect(subtopicValidationService.doesSubtopicWithUrlFragmentExist( - 'random-name')).toEqual(false); - expect(subtopicValidationService.doesSubtopicWithUrlFragmentExist( - 'subtopic-one')).toEqual(true); - expect(subtopicValidationService.doesSubtopicWithUrlFragmentExist( - 'subtopic-two')).toEqual(true); - expect(subtopicValidationService.doesSubtopicWithUrlFragmentExist( - 'subtopic-three')).toEqual(true); - expect(subtopicValidationService.doesSubtopicWithUrlFragmentExist( - 'subtopic-four')).toEqual(false); + expect( + subtopicValidationService.doesSubtopicWithUrlFragmentExist('random-name') + ).toEqual(false); + expect( + subtopicValidationService.doesSubtopicWithUrlFragmentExist('subtopic-one') + ).toEqual(true); + expect( + subtopicValidationService.doesSubtopicWithUrlFragmentExist('subtopic-two') + ).toEqual(true); + expect( + subtopicValidationService.doesSubtopicWithUrlFragmentExist( + 'subtopic-three' + ) + ).toEqual(true); + expect( + subtopicValidationService.doesSubtopicWithUrlFragmentExist( + 'subtopic-four' + ) + ).toEqual(false); }); it('should validate url fragement', () => { - expect(subtopicValidationService.isUrlFragmentValid('CAPITAL_INVALID')) - .toBeFalse(); - expect(subtopicValidationService.isUrlFragmentValid('valid-fragement')) - .toBeTrue(); + expect( + subtopicValidationService.isUrlFragmentValid('CAPITAL_INVALID') + ).toBeFalse(); + expect( + subtopicValidationService.isUrlFragmentValid('valid-fragement') + ).toBeTrue(); }); }); diff --git a/core/templates/pages/topic-editor-page/services/subtopic-validation.service.ts b/core/templates/pages/topic-editor-page/services/subtopic-validation.service.ts index b182bf403e7d..0fdc59015f73 100644 --- a/core/templates/pages/topic-editor-page/services/subtopic-validation.service.ts +++ b/core/templates/pages/topic-editor-page/services/subtopic-validation.service.ts @@ -16,26 +16,25 @@ * @fileoverview Service to validate subtopic name. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { TopicEditorStateService } from './topic-editor-state.service'; -import { AppConstants } from 'app.constants'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {TopicEditorStateService} from './topic-editor-state.service'; +import {AppConstants} from 'app.constants'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class SubtopicValidationService { private _VALID_URL_FRAGMENT_REGEX = new RegExp( - AppConstants.VALID_URL_FRAGMENT_REGEX); + AppConstants.VALID_URL_FRAGMENT_REGEX + ); - constructor( - private topicEditorStateService: TopicEditorStateService - ) {} + constructor(private topicEditorStateService: TopicEditorStateService) {} checkValidSubtopicName(title: string): boolean { let subtopicTitles: string[] = []; let topic = this.topicEditorStateService.getTopic(); - topic.getSubtopics().forEach((subtopic) => { + topic.getSubtopics().forEach(subtopic => { subtopicTitles.push(subtopic.getTitle()); }); return subtopicTitles.indexOf(title) === -1; @@ -43,8 +42,9 @@ export class SubtopicValidationService { doesSubtopicWithUrlFragmentExist(urlFragment: string): boolean { let topic = this.topicEditorStateService.getTopic(); - return topic.getSubtopics().some( - subtopic => subtopic.getUrlFragment() === urlFragment); + return topic + .getSubtopics() + .some(subtopic => subtopic.getUrlFragment() === urlFragment); } isUrlFragmentValid(urlFragment: string): boolean { @@ -52,5 +52,9 @@ export class SubtopicValidationService { } } -angular.module('oppia').factory('SubtopicValidationService', - downgradeInjectable(SubtopicValidationService)); +angular + .module('oppia') + .factory( + 'SubtopicValidationService', + downgradeInjectable(SubtopicValidationService) + ); diff --git a/core/templates/pages/topic-editor-page/services/topic-editor-routing.service.spec.ts b/core/templates/pages/topic-editor-page/services/topic-editor-routing.service.spec.ts index e44d97441344..3f7efc46ec08 100644 --- a/core/templates/pages/topic-editor-page/services/topic-editor-routing.service.spec.ts +++ b/core/templates/pages/topic-editor-page/services/topic-editor-routing.service.spec.ts @@ -16,11 +16,11 @@ * @fileoverview Unit tests for TopicEditorRoutingService. */ -import { TestBed, waitForAsync } from '@angular/core/testing'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { PageTitleService } from 'services/page-title.service'; -import { TopicEditorRoutingService } from './topic-editor-routing.service'; +import {TestBed, waitForAsync} from '@angular/core/testing'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {PageTitleService} from 'services/page-title.service'; +import {TopicEditorRoutingService} from './topic-editor-routing.service'; describe('Topic Editor Routing Service', () => { let ters: TopicEditorRoutingService; @@ -30,9 +30,9 @@ describe('Topic Editor Routing Service', () => { nativeWindow = { location: { href: '', - hash: '/' + hash: '/', }, - open: (url: string) => {} + open: (url: string) => {}, }; } @@ -41,11 +41,11 @@ describe('Topic Editor Routing Service', () => { providers: [ { provide: WindowRef, - useClass: MockWindowRef + useClass: MockWindowRef, }, PageTitleService, - UrlInterpolationService - ] + UrlInterpolationService, + ], }).compileComponents(); })); @@ -58,7 +58,7 @@ describe('Topic Editor Routing Service', () => { expect(ters.getActiveTabName()).toEqual('main'); }); - it('should navigate to different tabs', function() { + it('should navigate to different tabs', function () { expect(ters.getActiveTabName()).toEqual('main'); ters.navigateToSubtopicEditorWithId(1); expect(ters.getActiveTabName()).toEqual('subtopic_editor'); @@ -79,11 +79,12 @@ describe('Topic Editor Routing Service', () => { expect(ters.getActiveTabName()).toEqual('main'); }); - it('should navigate to skill editor', function() { + it('should navigate to skill editor', function () { spyOn(mockWindowRef.nativeWindow, 'open'); ters.navigateToSkillEditorWithId('10'); - expect(mockWindowRef.nativeWindow.open) - .toHaveBeenCalledWith('/skill_editor/10'); + expect(mockWindowRef.nativeWindow.open).toHaveBeenCalledWith( + '/skill_editor/10' + ); }); it('should return last tab visited', () => { diff --git a/core/templates/pages/topic-editor-page/services/topic-editor-routing.service.ts b/core/templates/pages/topic-editor-page/services/topic-editor-routing.service.ts index 2bff9140ca23..02adebb4d9c6 100644 --- a/core/templates/pages/topic-editor-page/services/topic-editor-routing.service.ts +++ b/core/templates/pages/topic-editor-page/services/topic-editor-routing.service.ts @@ -16,14 +16,14 @@ * @fileoverview Service that handles routing for the topic editor page. */ -import { Injectable, EventEmitter } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { PageTitleService } from 'services/page-title.service'; +import {Injectable, EventEmitter} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {PageTitleService} from 'services/page-title.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class TopicEditorRoutingService { private _MAIN_TAB = 'main'; @@ -110,17 +110,18 @@ export class TopicEditorRoutingService { } getSubtopicIdFromUrl(): number { - return parseInt( - this.windowRef.nativeWindow.location.hash.split('/')[2]); + return parseInt(this.windowRef.nativeWindow.location.hash.split('/')[2]); } navigateToSkillEditorWithId(skillId: string): void { let SKILL_EDITOR_URL_TEMPLATE = '/skill_editor/'; let skillEditorUrl = this.urlInterpolationService.interpolateUrl( - SKILL_EDITOR_URL_TEMPLATE, { - skill_id: skillId - }); + SKILL_EDITOR_URL_TEMPLATE, + { + skill_id: skillId, + } + ); this.windowRef.nativeWindow.open(skillEditorUrl); } @@ -129,5 +130,9 @@ export class TopicEditorRoutingService { } } -angular.module('oppia').factory('TopicEditorRoutingService', - downgradeInjectable(TopicEditorRoutingService)); +angular + .module('oppia') + .factory( + 'TopicEditorRoutingService', + downgradeInjectable(TopicEditorRoutingService) + ); diff --git a/core/templates/pages/topic-editor-page/services/topic-editor-state.service.spec.ts b/core/templates/pages/topic-editor-page/services/topic-editor-state.service.spec.ts index 768e11351a7a..95bbfc770621 100644 --- a/core/templates/pages/topic-editor-page/services/topic-editor-state.service.spec.ts +++ b/core/templates/pages/topic-editor-page/services/topic-editor-state.service.spec.ts @@ -16,18 +16,28 @@ * @fileoverview Unit tests for TopicEditorStateService. */ -import { fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { BackendChangeObject } from 'domain/editor/undo_redo/change.model'; -import { UndoRedoService } from 'domain/editor/undo_redo/undo-redo.service'; -import { EditableStoryBackendApiService } from 'domain/story/editable-story-backend-api.service'; -import { StorySummaryBackendDict } from 'domain/story/story-summary.model'; -import { EditableTopicBackendApiService, FetchTopicResponse, UpdateTopicResponse } from 'domain/topic/editable-topic-backend-api.service'; -import { SubtopicPage, SubtopicPageBackendDict } from 'domain/topic/subtopic-page.model'; -import { TopicRightsBackendApiService } from 'domain/topic/topic-rights-backend-api.service'; -import { TopicRights, TopicRightsBackendDict } from 'domain/topic/topic-rights.model'; -import { TopicBackendDict, Topic } from 'domain/topic/topic-object.model'; -import { AlertsService } from 'services/alerts.service'; -import { TopicEditorStateService } from './topic-editor-state.service'; +import {fakeAsync, TestBed, tick, waitForAsync} from '@angular/core/testing'; +import {BackendChangeObject} from 'domain/editor/undo_redo/change.model'; +import {UndoRedoService} from 'domain/editor/undo_redo/undo-redo.service'; +import {EditableStoryBackendApiService} from 'domain/story/editable-story-backend-api.service'; +import {StorySummaryBackendDict} from 'domain/story/story-summary.model'; +import { + EditableTopicBackendApiService, + FetchTopicResponse, + UpdateTopicResponse, +} from 'domain/topic/editable-topic-backend-api.service'; +import { + SubtopicPage, + SubtopicPageBackendDict, +} from 'domain/topic/subtopic-page.model'; +import {TopicRightsBackendApiService} from 'domain/topic/topic-rights-backend-api.service'; +import { + TopicRights, + TopicRightsBackendDict, +} from 'domain/topic/topic-rights.model'; +import {TopicBackendDict, Topic} from 'domain/topic/topic-object.model'; +import {AlertsService} from 'services/alerts.service'; +import {TopicEditorStateService} from './topic-editor-state.service'; describe('Topic editor state service', () => { let topicEditorStateService: TopicEditorStateService; @@ -39,26 +49,30 @@ describe('Topic editor state service', () => { let skillCreationIsAllowed: boolean = true; let skillQuestionCountDict = {}; let groupedSkillSummaries = { - topic_name: [{ - id: 'skillId1', - description: 'description1', - language_code: 'en', - version: 1, - misconception_count: 3, - worked_examples_count: 3, - skill_model_created_on: 1593138898626.193, - skill_model_last_updated: 1593138898626.193 - }], - others: [{ - id: 'skillId2', - description: 'description2', - language_code: 'en', - version: 1, - misconception_count: 3, - worked_examples_count: 3, - skill_model_created_on: 1593138898626.193, - skill_model_last_updated: 1593138898626.193 - }] + topic_name: [ + { + id: 'skillId1', + description: 'description1', + language_code: 'en', + version: 1, + misconception_count: 3, + worked_examples_count: 3, + skill_model_created_on: 1593138898626.193, + skill_model_last_updated: 1593138898626.193, + }, + ], + others: [ + { + id: 'skillId2', + description: 'description2', + language_code: 'en', + version: 1, + misconception_count: 3, + worked_examples_count: 3, + skill_model_created_on: 1593138898626.193, + skill_model_last_updated: 1593138898626.193, + }, + ], }; let topicDict: TopicBackendDict; let storySummaryBackendDict: StorySummaryBackendDict; @@ -73,7 +87,7 @@ describe('Topic editor state service', () => { topicDict: topicDict, skillIdToDescriptionDict: {}, skillIdToRubricsDict: {}, - classroomUrlFragment: 'url_fragment' + classroomUrlFragment: 'url_fragment', } as unknown as FetchTopicResponse); } @@ -82,45 +96,41 @@ describe('Topic editor state service', () => { } fetchSubtopicPageAsync( - topicId: number, subtopicId: number): Promise { + topicId: number, + subtopicId: number + ): Promise { return Promise.resolve(subtopicPage); } updateTopicAsync( - topicId: string, - version: number, - commitMessage: string, - changeList: BackendChangeObject[]): - Promise { + topicId: string, + version: number, + commitMessage: string, + changeList: BackendChangeObject[] + ): Promise { return Promise.resolve({ topicDict: topicDict, skillIdToRubricsDict: { skill_id_1: [ { difficulty: 'Easy', - explanations: [ - 'Explanation 1' - ] + explanations: ['Explanation 1'], }, { difficulty: 'Medium', - explanations: [ - 'Explanation 2' - ] + explanations: ['Explanation 2'], }, { difficulty: 'Hard', - explanations: [ - 'Explanation 3' - ] - } + explanations: ['Explanation 3'], + }, ], - skill_id_2: [] + skill_id_2: [], }, skillIdToDescriptionDict: { skill_id_1: 'Description 1', - skill_id_2: '' - } + skill_id_2: '', + }, }); } @@ -138,7 +148,7 @@ describe('Topic editor state service', () => { return Promise.resolve({ published: true, can_publish_topic: true, - can_edit_topic: true + can_edit_topic: true, }); } } @@ -155,18 +165,18 @@ describe('Topic editor state service', () => { AlertsService, { provide: EditableStoryBackendApiService, - useClass: MockEditableStoryBackendApiService + useClass: MockEditableStoryBackendApiService, }, { provide: EditableTopicBackendApiService, - useClass: MockEditableTopicBackendApiService + useClass: MockEditableTopicBackendApiService, }, { provide: TopicRightsBackendApiService, - useClass: MockTopicRightsBackendApiService + useClass: MockTopicRightsBackendApiService, }, - UndoRedoService - ] + UndoRedoService, + ], }).compileComponents(); })); @@ -178,13 +188,15 @@ describe('Topic editor state service', () => { // the need to test validations. This is because the backend api service // returns an unknown type. // @ts-ignore - mockEditableTopicBackendApiService = (TestBed.inject( - EditableTopicBackendApiService)) as - jasmine.SpyObj; - editableStoryBackendApiService = - TestBed.inject(EditableStoryBackendApiService); - alertsService = (TestBed.inject(AlertsService) as unknown) as - jasmine.SpyObj; + mockEditableTopicBackendApiService = TestBed.inject( + EditableTopicBackendApiService + ) as jasmine.SpyObj; + editableStoryBackendApiService = TestBed.inject( + EditableStoryBackendApiService + ); + alertsService = TestBed.inject( + AlertsService + ) as unknown as jasmine.SpyObj; topicDict = { id: 'topic_id', @@ -204,7 +216,7 @@ describe('Topic editor state service', () => { practice_tab_is_displayed: true, meta_tag_content: 'content', page_title_fragment_for_web: 'title_fragment', - skill_ids_for_diagnostic_test: [] + skill_ids_for_diagnostic_test: [], }; storySummaryBackendDict = { id: 'id', @@ -216,7 +228,7 @@ describe('Topic editor state service', () => { story_is_published: true, completed_node_titles: [], url_fragment: 'story_fragment', - all_node_dicts: [] + all_node_dicts: [], }; subtopicPage = { id: 'subtopic_id', @@ -224,13 +236,13 @@ describe('Topic editor state service', () => { page_contents: { subtitled_html: { content_id: 'content_id', - html: 'html' + html: 'html', }, recorded_voiceovers: { - voiceovers_mapping: {} - } + voiceovers_mapping: {}, + }, }, - language_code: 'en' + language_code: 'en', }; }); @@ -241,8 +253,9 @@ describe('Topic editor state service', () => { it('should load topic', fakeAsync(() => { topicEditorStateService.loadTopic('test_id'); tick(); - expect(topicEditorStateService.isSkillCreationAllowed()) - .toEqual(skillCreationIsAllowed); + expect(topicEditorStateService.isSkillCreationAllowed()).toEqual( + skillCreationIsAllowed + ); expect(topicEditorStateService.isLoadingTopic()).toEqual(false); expect(topicEditorStateService.hasLoadedTopic()).toBeTrue(); expect(topicEditorStateService.getGroupedSkillSummaries()).toBeDefined(); @@ -252,8 +265,10 @@ describe('Topic editor state service', () => { it('should display error message when topic fails to load', fakeAsync(() => { let errorMsg: string = 'Error Message'; - spyOn(mockEditableTopicBackendApiService, 'fetchTopicAsync').and - .returnValue(Promise.reject(errorMsg)); + spyOn( + mockEditableTopicBackendApiService, + 'fetchTopicAsync' + ).and.returnValue(Promise.reject(errorMsg)); spyOn(alertsService, 'addWarning'); topicEditorStateService.loadTopic('test_id'); @@ -261,32 +276,38 @@ describe('Topic editor state service', () => { expect(alertsService.addWarning).toHaveBeenCalledWith(errorMsg); })); - it('should display default error message when topic fails to load', - fakeAsync(() => { - spyOn(mockEditableTopicBackendApiService, 'fetchTopicAsync').and - .returnValue(Promise.reject()); - spyOn(alertsService, 'addWarning'); + it('should display default error message when topic fails to load', fakeAsync(() => { + spyOn( + mockEditableTopicBackendApiService, + 'fetchTopicAsync' + ).and.returnValue(Promise.reject()); + spyOn(alertsService, 'addWarning'); - topicEditorStateService.loadTopic('test_id'); - tick(); - expect(alertsService.addWarning).toHaveBeenCalledWith( - 'There was an error when loading the topic editor.'); - })); + topicEditorStateService.loadTopic('test_id'); + tick(); + expect(alertsService.addWarning).toHaveBeenCalledWith( + 'There was an error when loading the topic editor.' + ); + })); it('should load subtopic page', fakeAsync(() => { topicEditorStateService.loadSubtopicPage('1', 2); tick(); expect(topicEditorStateService.getSubtopicPage()).toEqual( - SubtopicPage.createFromBackendDict(subtopicPage)); + SubtopicPage.createFromBackendDict(subtopicPage) + ); expect(topicEditorStateService.getCachedSubtopicPages()).toHaveSize(1); topicEditorStateService.loadSubtopicPage('1', 2); expect(topicEditorStateService.getSubtopicPage()).toEqual( - SubtopicPage.createFromBackendDict(subtopicPage)); + SubtopicPage.createFromBackendDict(subtopicPage) + ); })); it('should show error when loading subtopic page fails', fakeAsync(() => { - spyOn(mockEditableTopicBackendApiService, 'fetchSubtopicPageAsync') - .and.returnValue(Promise.reject()); + spyOn( + mockEditableTopicBackendApiService, + 'fetchSubtopicPageAsync' + ).and.returnValue(Promise.reject()); spyOn(alertsService, 'addWarning'); // This throws "Argument of type 'null' is not assignable to parameter of // type 'string'" We need to suppress this error because of the need to test @@ -296,42 +317,53 @@ describe('Topic editor state service', () => { topicEditorStateService.loadSubtopicPage(null, null); tick(); expect(alertsService.addWarning).toHaveBeenCalledWith( - 'There was an error when loading the topic.'); + 'There was an error when loading the topic.' + ); })); it('should show error when loading subtopic page fails', fakeAsync(() => { - spyOn(mockEditableTopicBackendApiService, 'fetchSubtopicPageAsync') - .and.returnValue(Promise.reject()); + spyOn( + mockEditableTopicBackendApiService, + 'fetchSubtopicPageAsync' + ).and.returnValue(Promise.reject()); spyOn(alertsService, 'addWarning'); topicEditorStateService.loadSubtopicPage('1', 2); tick(); expect(alertsService.addWarning).toHaveBeenCalledWith( - 'There was an error when loading the topic.'); + 'There was an error when loading the topic.' + ); })); it('should show error when invalid topic id is provided', fakeAsync(() => { - spyOn(mockEditableTopicBackendApiService, 'fetchSubtopicPageAsync') - .and.returnValue(Promise.reject()); + spyOn( + mockEditableTopicBackendApiService, + 'fetchSubtopicPageAsync' + ).and.returnValue(Promise.reject()); spyOn(alertsService, 'addWarning'); topicEditorStateService.loadSubtopicPage('', 2); tick(); expect(alertsService.addWarning).toHaveBeenCalledWith( - 'There was an error when loading the topic.'); + 'There was an error when loading the topic.' + ); })); it('should set subtopic page', fakeAsync(() => { subtopicPage.id = 'topic_id1234-0'; topicEditorStateService.setSubtopicPage( - SubtopicPage.createFromBackendDict(subtopicPage)); + SubtopicPage.createFromBackendDict(subtopicPage) + ); expect(topicEditorStateService.getSubtopicPage()).toEqual( - SubtopicPage.createFromBackendDict(subtopicPage)); + SubtopicPage.createFromBackendDict(subtopicPage) + ); topicEditorStateService.loadSubtopicPage('topic_id1234', 0); tick(); topicEditorStateService.setSubtopicPage( - SubtopicPage.createFromBackendDict(subtopicPage)); + SubtopicPage.createFromBackendDict(subtopicPage) + ); expect(topicEditorStateService.getSubtopicPage()).toEqual( - SubtopicPage.createFromBackendDict(subtopicPage)); + SubtopicPage.createFromBackendDict(subtopicPage) + ); })); it('should set topic', () => { @@ -346,15 +378,18 @@ describe('Topic editor state service', () => { topicEditorStateService.setTopic(topic); subtopicPage.id = 'topic_id1234-0'; topicEditorStateService.setSubtopicPage( - SubtopicPage.createFromBackendDict(subtopicPage)); + SubtopicPage.createFromBackendDict(subtopicPage) + ); tick(); subtopicPage.id = 'topic_id1234-1'; topicEditorStateService.setSubtopicPage( - SubtopicPage.createFromBackendDict(subtopicPage)); + SubtopicPage.createFromBackendDict(subtopicPage) + ); tick(); subtopicPage.id = 'topic_id1234-2'; topicEditorStateService.setSubtopicPage( - SubtopicPage.createFromBackendDict(subtopicPage)); + SubtopicPage.createFromBackendDict(subtopicPage) + ); tick(); topicEditorStateService.deleteSubtopicPage('topic_id1234', 1); @@ -364,21 +399,25 @@ describe('Topic editor state service', () => { subtopicPage.id = 'topic_id1234-1'; let subtopic1 = SubtopicPage.createFromBackendDict(subtopicPage); expect(topicEditorStateService.getCachedSubtopicPages()).toEqual([ - subtopic0, subtopic1 + subtopic0, + subtopic1, ]); })); it('should save topic when user saves a topic', fakeAsync(() => { spyOn(undoRedoService, 'hasChanges').and.returnValue(true); spyOn(undoRedoService, 'clearChanges'); - spyOn(undoRedoService, 'getCommittableChangeList').and.returnValue([{ - cmd: 'delete_canonical_story', - story_id: 'story_id' - }]); - spyOn(mockEditableTopicBackendApiService, 'updateTopicAsync').and - .callThrough(); - spyOn(editableStoryBackendApiService, 'deleteStoryAsync').and - .callThrough(); + spyOn(undoRedoService, 'getCommittableChangeList').and.returnValue([ + { + cmd: 'delete_canonical_story', + story_id: 'story_id', + }, + ]); + spyOn( + mockEditableTopicBackendApiService, + 'updateTopicAsync' + ).and.callThrough(); + spyOn(editableStoryBackendApiService, 'deleteStoryAsync').and.callThrough(); var successCallback = jasmine.createSpy('successCallback'); let topic = Topic.create(topicDict, {}); topicEditorStateService.setTopic(topic); @@ -386,52 +425,53 @@ describe('Topic editor state service', () => { topicEditorStateService.saveTopic('Commit Message', successCallback); tick(); - expect(mockEditableTopicBackendApiService.updateTopicAsync) - .toHaveBeenCalledWith( - 'topic_id', 1, 'Commit Message', [ - { - cmd: 'delete_canonical_story', - story_id: 'story_id' - } - ] - ); + expect( + mockEditableTopicBackendApiService.updateTopicAsync + ).toHaveBeenCalledWith('topic_id', 1, 'Commit Message', [ + { + cmd: 'delete_canonical_story', + story_id: 'story_id', + }, + ]); expect(successCallback).toHaveBeenCalled(); expect(undoRedoService.getCommittableChangeList).toHaveBeenCalled(); - expect(editableStoryBackendApiService.deleteStoryAsync) - .toHaveBeenCalled(); + expect(editableStoryBackendApiService.deleteStoryAsync).toHaveBeenCalled(); expect(undoRedoService.clearChanges).toHaveBeenCalled(); })); - it('should warn user when there is an error saving the topic', - fakeAsync(() => { - spyOn(undoRedoService, 'hasChanges').and.returnValue(true); - spyOn(undoRedoService, 'getCommittableChangeList').and.returnValue([{ + it('should warn user when there is an error saving the topic', fakeAsync(() => { + spyOn(undoRedoService, 'hasChanges').and.returnValue(true); + spyOn(undoRedoService, 'getCommittableChangeList').and.returnValue([ + { cmd: 'delete_canonical_story', - story_id: 'story_id' - }]); - spyOn(mockEditableTopicBackendApiService, 'updateTopicAsync').and - .returnValue(Promise.reject()); - spyOn(alertsService, 'addWarning'); - var successCallback = jasmine.createSpy('successCallback'); - let topic = Topic.create(topicDict, {}); - topicEditorStateService.setTopic(topic); + story_id: 'story_id', + }, + ]); + spyOn( + mockEditableTopicBackendApiService, + 'updateTopicAsync' + ).and.returnValue(Promise.reject()); + spyOn(alertsService, 'addWarning'); + var successCallback = jasmine.createSpy('successCallback'); + let topic = Topic.create(topicDict, {}); + topicEditorStateService.setTopic(topic); - topicEditorStateService.saveTopic('Commit Message', successCallback); - tick(); + topicEditorStateService.saveTopic('Commit Message', successCallback); + tick(); - expect(mockEditableTopicBackendApiService.updateTopicAsync) - .toHaveBeenCalledWith( - 'topic_id', 1, 'Commit Message', [ - { - cmd: 'delete_canonical_story', - story_id: 'story_id' - } - ] - ); - expect(successCallback).not.toHaveBeenCalled(); - expect(alertsService.addWarning) - .toHaveBeenCalledWith('There was an error when saving the topic.'); - })); + expect( + mockEditableTopicBackendApiService.updateTopicAsync + ).toHaveBeenCalledWith('topic_id', 1, 'Commit Message', [ + { + cmd: 'delete_canonical_story', + story_id: 'story_id', + }, + ]); + expect(successCallback).not.toHaveBeenCalled(); + expect(alertsService.addWarning).toHaveBeenCalledWith( + 'There was an error when saving the topic.' + ); + })); it('should not save topic when there are no changes', fakeAsync(() => { spyOn(undoRedoService, 'hasChanges').and.returnValue(false); @@ -443,8 +483,9 @@ describe('Topic editor state service', () => { topicEditorStateService.saveTopic('Commit Message', () => {}); tick(); - expect(mockEditableTopicBackendApiService.updateTopicAsync).not - .toHaveBeenCalled(); + expect( + mockEditableTopicBackendApiService.updateTopicAsync + ).not.toHaveBeenCalled(); })); it('should not save topic when topicis not initialised', fakeAsync(() => { @@ -457,8 +498,9 @@ describe('Topic editor state service', () => { expect(alertsService.fatalWarning).toHaveBeenCalledWith( 'Cannot save a topic before one is loaded.' ); - expect(mockEditableTopicBackendApiService.updateTopicAsync).not - .toHaveBeenCalled(); + expect( + mockEditableTopicBackendApiService.updateTopicAsync + ).not.toHaveBeenCalled(); })); it('should set topic rights when called', () => { @@ -468,21 +510,25 @@ describe('Topic editor state service', () => { TopicRights.createFromBackendDict({ published: false, can_publish_topic: false, - can_edit_topic: false - })); + can_edit_topic: false, + }) + ); - topicEditorStateService.setTopicRights(TopicRights.createFromBackendDict({ - published: true, - can_publish_topic: true, - can_edit_topic: true - })); + topicEditorStateService.setTopicRights( + TopicRights.createFromBackendDict({ + published: true, + can_publish_topic: true, + can_edit_topic: true, + }) + ); expect(topicEditorStateService.getTopicRights()).toEqual( TopicRights.createFromBackendDict({ published: true, can_publish_topic: true, - can_edit_topic: true - })); + can_edit_topic: true, + }) + ); }); it('should delete subtopic page', fakeAsync(() => { @@ -510,8 +556,10 @@ describe('Topic editor state service', () => { it('should show error when updation of topic name', fakeAsync(() => { spyOn(alertsService, 'addWarning'); - spyOn(mockEditableTopicBackendApiService, 'doesTopicWithNameExistAsync') - .and.returnValue(Promise.reject()); + spyOn( + mockEditableTopicBackendApiService, + 'doesTopicWithNameExistAsync' + ).and.returnValue(Promise.reject()); topicEditorStateService.updateExistenceOfTopicName('test_topic', () => {}); tick(); @@ -523,70 +571,89 @@ describe('Topic editor state service', () => { it('should update existence of topic url fragment', fakeAsync(() => { topicEditorStateService.updateExistenceOfTopicUrlFragment( - 'test_topic', () => {}, () => {}); + 'test_topic', + () => {}, + () => {} + ); tick(); expect(topicEditorStateService.getTopicWithUrlFragmentExists()).toBeTrue(); })); - it('should not show error when updation of topic url fragment failed' + - 'with 400 status code', fakeAsync(() => { - let errorResponse = { - headers: { - normalizedNames: {}, - lazyUpdate: null - }, - status: 400, - statusText: 'Bad Request', - url: '', - ok: false, - name: 'HttpErrorResponse', - message: 'Http failure response for test url: 400 Bad Request', - error: { - error: 'Error: Bad request to server', - status_code: 400 - } - }; - - spyOn(alertsService, 'addWarning'); - spyOn( - mockEditableTopicBackendApiService, 'doesTopicWithUrlFragmentExistAsync') - .and.returnValue(Promise.reject(errorResponse)); - - topicEditorStateService.updateExistenceOfTopicUrlFragment( - 'test_topic', () => {}, () => {}); - tick(); - expect(alertsService.addWarning).not.toHaveBeenCalled(); - })); + it( + 'should not show error when updation of topic url fragment failed' + + 'with 400 status code', + fakeAsync(() => { + let errorResponse = { + headers: { + normalizedNames: {}, + lazyUpdate: null, + }, + status: 400, + statusText: 'Bad Request', + url: '', + ok: false, + name: 'HttpErrorResponse', + message: 'Http failure response for test url: 400 Bad Request', + error: { + error: 'Error: Bad request to server', + status_code: 400, + }, + }; - it('should not show error when updation of topic url fragment failed' + - 'with 400 status code', fakeAsync(() => { - let errorResponse = { - headers: { - normalizedNames: {}, - lazyUpdate: null - }, - status: 500, - statusText: 'Error: Failed to check topic url fragment.', - url: '', - ok: false, - name: 'HttpErrorResponse', - message: 'Http failure response for test url: 500' + - 'Error: Failed to check topic url fragment.', - error: { - error: 'Error: Failed to check topic url fragment.', - status_code: 500 - } - }; + spyOn(alertsService, 'addWarning'); + spyOn( + mockEditableTopicBackendApiService, + 'doesTopicWithUrlFragmentExistAsync' + ).and.returnValue(Promise.reject(errorResponse)); + + topicEditorStateService.updateExistenceOfTopicUrlFragment( + 'test_topic', + () => {}, + () => {} + ); + tick(); + expect(alertsService.addWarning).not.toHaveBeenCalled(); + }) + ); - spyOn(alertsService, 'addWarning'); - spyOn( - mockEditableTopicBackendApiService, 'doesTopicWithUrlFragmentExistAsync') - .and.returnValue(Promise.reject(errorResponse)); + it( + 'should not show error when updation of topic url fragment failed' + + 'with 400 status code', + fakeAsync(() => { + let errorResponse = { + headers: { + normalizedNames: {}, + lazyUpdate: null, + }, + status: 500, + statusText: 'Error: Failed to check topic url fragment.', + url: '', + ok: false, + name: 'HttpErrorResponse', + message: + 'Http failure response for test url: 500' + + 'Error: Failed to check topic url fragment.', + error: { + error: 'Error: Failed to check topic url fragment.', + status_code: 500, + }, + }; - topicEditorStateService.updateExistenceOfTopicUrlFragment( - 'test_topic', () => {}, () => {}); - tick(); - expect(alertsService.addWarning).toHaveBeenCalledWith( - errorResponse.message); - })); + spyOn(alertsService, 'addWarning'); + spyOn( + mockEditableTopicBackendApiService, + 'doesTopicWithUrlFragmentExistAsync' + ).and.returnValue(Promise.reject(errorResponse)); + + topicEditorStateService.updateExistenceOfTopicUrlFragment( + 'test_topic', + () => {}, + () => {} + ); + tick(); + expect(alertsService.addWarning).toHaveBeenCalledWith( + errorResponse.message + ); + }) + ); }); diff --git a/core/templates/pages/topic-editor-page/services/topic-editor-state.service.ts b/core/templates/pages/topic-editor-page/services/topic-editor-state.service.ts index e81258bf149d..3e74376e68ac 100644 --- a/core/templates/pages/topic-editor-page/services/topic-editor-state.service.ts +++ b/core/templates/pages/topic-editor-page/services/topic-editor-state.service.ts @@ -18,25 +18,36 @@ * retrieving the topic, saving it, and listening for changes. */ -import { EventEmitter, Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { UndoRedoService } from 'domain/editor/undo_redo/undo-redo.service'; -import { Rubric, RubricBackendDict } from 'domain/skill/rubric.model'; -import { SkillSummaryBackendDict } from 'domain/skill/skill-summary.model'; -import { EditableStoryBackendApiService } from 'domain/story/editable-story-backend-api.service'; -import { StorySummary, StorySummaryBackendDict } from 'domain/story/story-summary.model'; -import { EditableTopicBackendApiService } from 'domain/topic/editable-topic-backend-api.service'; -import { SubtopicPage, SubtopicPageBackendDict } from 'domain/topic/subtopic-page.model'; -import { SkillIdToDescriptionMap } from 'domain/topic/subtopic.model'; -import { TopicRightsBackendApiService } from 'domain/topic/topic-rights-backend-api.service'; -import { TopicRights, TopicRightsBackendDict } from 'domain/topic/topic-rights.model'; -import { Topic, TopicBackendDict } from 'domain/topic/topic-object.model'; +import {EventEmitter, Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {UndoRedoService} from 'domain/editor/undo_redo/undo-redo.service'; +import {Rubric, RubricBackendDict} from 'domain/skill/rubric.model'; +import {SkillSummaryBackendDict} from 'domain/skill/skill-summary.model'; +import {EditableStoryBackendApiService} from 'domain/story/editable-story-backend-api.service'; +import { + StorySummary, + StorySummaryBackendDict, +} from 'domain/story/story-summary.model'; +import {EditableTopicBackendApiService} from 'domain/topic/editable-topic-backend-api.service'; +import { + SubtopicPage, + SubtopicPageBackendDict, +} from 'domain/topic/subtopic-page.model'; +import {SkillIdToDescriptionMap} from 'domain/topic/subtopic.model'; +import {TopicRightsBackendApiService} from 'domain/topic/topic-rights-backend-api.service'; +import { + TopicRights, + TopicRightsBackendDict, +} from 'domain/topic/topic-rights.model'; +import {Topic, TopicBackendDict} from 'domain/topic/topic-object.model'; import cloneDeep from 'lodash/cloneDeep'; -import { AlertsService } from 'services/alerts.service'; -import { TopicDeleteCanonicalStoryChange, TopicDeleteAdditionalStoryChange } - from 'domain/editor/undo_redo/change.model'; -import { LoaderService } from 'services/loader.service'; -import { SubtopicPageContents } from 'domain/topic/subtopic-page-contents.model'; +import {AlertsService} from 'services/alerts.service'; +import { + TopicDeleteCanonicalStoryChange, + TopicDeleteAdditionalStoryChange, +} from 'domain/editor/undo_redo/change.model'; +import {LoaderService} from 'services/loader.service'; +import {SubtopicPageContents} from 'domain/topic/subtopic-page-contents.model'; interface GroupedSkillSummaryDict { current: SkillSummaryBackendDict[]; @@ -44,7 +55,7 @@ interface GroupedSkillSummaryDict { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class TopicEditorStateService { // These properties below are initialized using Angular lifecycle hooks @@ -69,22 +80,22 @@ export class TopicEditorStateService { private _skillQuestionCountDict: Record = {}; private _groupedSkillSummaries: GroupedSkillSummaryDict = { current: [], - others: [] + others: [], }; private _skillCreationIsAllowed: boolean = false; private _classroomUrlFragment: string = 'staging'; - private _storySummariesInitializedEventEmitter: EventEmitter = ( - new EventEmitter()); + private _storySummariesInitializedEventEmitter: EventEmitter = + new EventEmitter(); - private _subtopicPageLoadedEventEmitter: EventEmitter = ( - new EventEmitter()); + private _subtopicPageLoadedEventEmitter: EventEmitter = + new EventEmitter(); - private _topicInitializedEventEmitter: EventEmitter = ( - new EventEmitter()); + private _topicInitializedEventEmitter: EventEmitter = + new EventEmitter(); - private _topicReinitializedEventEmitter: EventEmitter = ( - new EventEmitter()); + private _topicReinitializedEventEmitter: EventEmitter = + new EventEmitter(); constructor( private alertsService: AlertsService, @@ -96,7 +107,11 @@ export class TopicEditorStateService { ) { this._topicRights = new TopicRights(false, false, false); this._subtopicPage = new SubtopicPage( - 'id', 'topic_id', SubtopicPageContents.createDefault(), 'en'); + 'id', + 'topic_id', + SubtopicPageContents.createDefault(), + 'en' + ); } private _getSubtopicPageId(topicId: string, subtopicId: number): string { @@ -106,17 +121,16 @@ export class TopicEditorStateService { return ''; } - private _updateGroupedSkillSummaries( - groupedSkillSummaries: { - [topicName: string]: SkillSummaryBackendDict[]; - } - ): void { + private _updateGroupedSkillSummaries(groupedSkillSummaries: { + [topicName: string]: SkillSummaryBackendDict[]; + }): void { this._groupedSkillSummaries.current = []; this._groupedSkillSummaries.others = []; for (let idx in groupedSkillSummaries[this._topic.getName()]) { this._groupedSkillSummaries.current.push( - groupedSkillSummaries[this._topic.getName()][idx]); + groupedSkillSummaries[this._topic.getName()][idx] + ); } for (let name in groupedSkillSummaries) { if (name === this._topic.getName()) { @@ -162,22 +176,23 @@ export class TopicEditorStateService { } private _updateTopic( - newBackendTopicDict: TopicBackendDict, - skillIdToDescriptionDict: SkillIdToDescriptionMap): void { - this._setTopic( - Topic.create( - newBackendTopicDict, skillIdToDescriptionDict)); + newBackendTopicDict: TopicBackendDict, + skillIdToDescriptionDict: SkillIdToDescriptionMap + ): void { + this._setTopic(Topic.create(newBackendTopicDict, skillIdToDescriptionDict)); } private _updateSkillIdToRubricsObject( - skillIdToRubricsObject: Record): void { + skillIdToRubricsObject: Record + ): void { for (let skillId in skillIdToRubricsObject) { // Skips deleted skills. if (skillIdToRubricsObject[skillId]) { let rubrics = skillIdToRubricsObject[skillId].map( (rubric: RubricBackendDict) => { return Rubric.createFromBackendDict(rubric); - }); + } + ); this._skillIdToRubricsObject[skillId] = rubrics; } } @@ -190,9 +205,11 @@ export class TopicEditorStateService { } private _updateSubtopicPage( - newBackendSubtopicPageObject: SubtopicPageBackendDict): void { - this._setSubtopicPage(SubtopicPage.createFromBackendDict( - newBackendSubtopicPageObject)); + newBackendSubtopicPageObject: SubtopicPageBackendDict + ): void { + this._setSubtopicPage( + SubtopicPage.createFromBackendDict(newBackendSubtopicPageObject) + ); } private _setTopicRights(topicRights: TopicRights): void { @@ -200,18 +217,21 @@ export class TopicEditorStateService { } private _updateTopicRights( - newBackendTopicRightsObject: TopicRightsBackendDict): void { - this._setTopicRights(TopicRights.createFromBackendDict( - newBackendTopicRightsObject)); + newBackendTopicRightsObject: TopicRightsBackendDict + ): void { + this._setTopicRights( + TopicRights.createFromBackendDict(newBackendTopicRightsObject) + ); } private _setCanonicalStorySummaries( - canonicalStorySummaries: StorySummaryBackendDict[]): void { + canonicalStorySummaries: StorySummaryBackendDict[] + ): void { this._canonicalStorySummaries = canonicalStorySummaries.map( - (storySummaryDict) => { - return StorySummary.createFromBackendDict( - storySummaryDict); - }); + storySummaryDict => { + return StorySummary.createFromBackendDict(storySummaryDict); + } + ); this._storySummariesInitializedEventEmitter.emit(); } @@ -220,7 +240,8 @@ export class TopicEditorStateService { } private _setTopicWithUrlFragmentExists( - topicWithUrlFragmentExists: boolean): void { + topicWithUrlFragmentExists: boolean + ): void { this._topicWithUrlFragmentExists = topicWithUrlFragmentExists; } @@ -232,46 +253,50 @@ export class TopicEditorStateService { loadTopic(topicId: string): void { this._topicIsLoading = true; this.loaderService.showLoadingScreen('Loading Topic Editor'); - let topicDataPromise = this.editableTopicBackendApiService - .fetchTopicAsync(topicId); - let storyDataPromise = this.editableTopicBackendApiService - .fetchStoriesAsync(topicId); - let topicRightsPromise = this.topicRightsBackendApiService - .fetchTopicRightsAsync(topicId); - Promise.all([ - topicDataPromise, - storyDataPromise, - topicRightsPromise - ]).then(([ - newBackendTopicObject, - canonicalStorySummaries, - newBackendTopicRightsObject - ]) => { - this._updateTopic( - newBackendTopicObject.topicDict, - newBackendTopicObject.skillIdToDescriptionDict - ); - this._skillCreationIsAllowed = ( - newBackendTopicObject.skillCreationIsAllowed); - this._skillQuestionCountDict = ( - newBackendTopicObject.skillQuestionCountDict); - this._updateGroupedSkillSummaries( - newBackendTopicObject.groupedSkillSummaries); - this._updateGroupedSkillSummaries( - newBackendTopicObject.groupedSkillSummaries); - this._updateSkillIdToRubricsObject( - newBackendTopicObject.skillIdToRubricsDict); - this._updateClassroomUrlFragment( - newBackendTopicObject.classroomUrlFragment); - this._updateTopicRights(newBackendTopicRightsObject); - this._setCanonicalStorySummaries(canonicalStorySummaries); - this._topicIsLoading = false; - this.loaderService.hideLoadingScreen(); - }, (error) => { - this.alertsService.addWarning( - error || 'There was an error when loading the topic editor.'); - this._topicIsLoading = false; - }); + let topicDataPromise = + this.editableTopicBackendApiService.fetchTopicAsync(topicId); + let storyDataPromise = + this.editableTopicBackendApiService.fetchStoriesAsync(topicId); + let topicRightsPromise = + this.topicRightsBackendApiService.fetchTopicRightsAsync(topicId); + Promise.all([topicDataPromise, storyDataPromise, topicRightsPromise]).then( + ([ + newBackendTopicObject, + canonicalStorySummaries, + newBackendTopicRightsObject, + ]) => { + this._updateTopic( + newBackendTopicObject.topicDict, + newBackendTopicObject.skillIdToDescriptionDict + ); + this._skillCreationIsAllowed = + newBackendTopicObject.skillCreationIsAllowed; + this._skillQuestionCountDict = + newBackendTopicObject.skillQuestionCountDict; + this._updateGroupedSkillSummaries( + newBackendTopicObject.groupedSkillSummaries + ); + this._updateGroupedSkillSummaries( + newBackendTopicObject.groupedSkillSummaries + ); + this._updateSkillIdToRubricsObject( + newBackendTopicObject.skillIdToRubricsDict + ); + this._updateClassroomUrlFragment( + newBackendTopicObject.classroomUrlFragment + ); + this._updateTopicRights(newBackendTopicRightsObject); + this._setCanonicalStorySummaries(canonicalStorySummaries); + this._topicIsLoading = false; + this.loaderService.hideLoadingScreen(); + }, + error => { + this.alertsService.addWarning( + error || 'There was an error when loading the topic editor.' + ); + this._topicIsLoading = false; + } + ); } getGroupedSkillSummaries(): object { @@ -304,22 +329,24 @@ export class TopicEditorStateService { let subtopicPageId = this._getSubtopicPageId(topicId, subtopicId); let pageIndex = this._getSubtopicPageIndex(subtopicPageId); if (pageIndex !== null) { - this._subtopicPage = cloneDeep( - this._cachedSubtopicPages[pageIndex]); + this._subtopicPage = cloneDeep(this._cachedSubtopicPages[pageIndex]); this._subtopicPageLoadedEventEmitter.emit(); return; } this.loaderService.showLoadingScreen('Loading Subtopic Editor'); - this.editableTopicBackendApiService.fetchSubtopicPageAsync( - topicId, subtopicId).then( - (newBackendSubtopicPageObject) => { - this._updateSubtopicPage(newBackendSubtopicPageObject); - this.loaderService.hideLoadingScreen(); - }, - (error) => { - this.alertsService.addWarning( - error || 'There was an error when loading the topic.'); - }); + this.editableTopicBackendApiService + .fetchSubtopicPageAsync(topicId, subtopicId) + .then( + newBackendSubtopicPageObject => { + this._updateSubtopicPage(newBackendSubtopicPageObject); + this.loaderService.hideLoadingScreen(); + }, + error => { + this.alertsService.addWarning( + error || 'There was an error when loading the topic.' + ); + } + ); } /** @@ -444,20 +471,25 @@ export class TopicEditorStateService { this._newSubtopicPageIds.splice(newIndex, 1); for (let i = 0; i < this._cachedSubtopicPages.length; i++) { let newSubtopicId = this._getSubtopicIdFromSubtopicPageId( - this._cachedSubtopicPages[i].getId()); + this._cachedSubtopicPages[i].getId() + ); if (newSubtopicId > subtopicId) { newSubtopicId--; this._cachedSubtopicPages[i].setId( - this._getSubtopicPageId(topicId, newSubtopicId)); + this._getSubtopicPageId(topicId, newSubtopicId) + ); } } for (let i = 0; i < this._newSubtopicPageIds.length; i++) { let newSubtopicId = this._getSubtopicIdFromSubtopicPageId( - this._newSubtopicPageIds[i]); + this._newSubtopicPageIds[i] + ); if (newSubtopicId > subtopicId) { newSubtopicId--; this._newSubtopicPageIds[i] = this._getSubtopicPageId( - topicId, newSubtopicId); + topicId, + newSubtopicId + ); } } } @@ -483,7 +515,8 @@ export class TopicEditorStateService { saveTopic(commitMessage: string, successCallback: () => void): boolean { if (!this._topicIsInitialized) { this.alertsService.fatalWarning( - 'Cannot save a topic before one is loaded.'); + 'Cannot save a topic before one is loaded.' + ); } // Don't attempt to save the topic if there are no changes pending. @@ -491,35 +524,50 @@ export class TopicEditorStateService { return false; } this._topicIsBeingSaved = true; - this.editableTopicBackendApiService.updateTopicAsync( - this._topic.getId(), this._topic.getVersion(), - commitMessage, this.undoRedoService.getCommittableChangeList()).then( - (topicBackendObject) => { - this._updateTopic( - topicBackendObject.topicDict, - topicBackendObject.skillIdToDescriptionDict - ); - this._updateSkillIdToRubricsObject( - topicBackendObject.skillIdToRubricsDict); - let changeList = this.undoRedoService.getCommittableChangeList(); - for (let i = 0; i < changeList.length; i++) { - if (changeList[i].cmd === 'delete_canonical_story' || - changeList[i].cmd === 'delete_additional_story') { - this.editableStoryBackendApiService.deleteStoryAsync(( - changeList[i] as TopicDeleteAdditionalStoryChange | - TopicDeleteCanonicalStoryChange).story_id); + this.editableTopicBackendApiService + .updateTopicAsync( + this._topic.getId(), + this._topic.getVersion(), + commitMessage, + this.undoRedoService.getCommittableChangeList() + ) + .then( + topicBackendObject => { + this._updateTopic( + topicBackendObject.topicDict, + topicBackendObject.skillIdToDescriptionDict + ); + this._updateSkillIdToRubricsObject( + topicBackendObject.skillIdToRubricsDict + ); + let changeList = this.undoRedoService.getCommittableChangeList(); + for (let i = 0; i < changeList.length; i++) { + if ( + changeList[i].cmd === 'delete_canonical_story' || + changeList[i].cmd === 'delete_additional_story' + ) { + this.editableStoryBackendApiService.deleteStoryAsync( + ( + changeList[i] as + | TopicDeleteAdditionalStoryChange + | TopicDeleteCanonicalStoryChange + ).story_id + ); + } } + this.undoRedoService.clearChanges(); + this._topicIsBeingSaved = false; + if (successCallback) { + successCallback(); + } + }, + error => { + this.alertsService.addWarning( + error || 'There was an error when saving the topic.' + ); + this._topicIsBeingSaved = false; } - this.undoRedoService.clearChanges(); - this._topicIsBeingSaved = false; - if (successCallback) { - successCallback(); - } - }, (error) => { - this.alertsService.addWarning( - error || 'There was an error when saving the topic.'); - this._topicIsBeingSaved = false; - }); + ); return true; } @@ -555,19 +603,26 @@ export class TopicEditorStateService { * has been successfully updated. */ updateExistenceOfTopicName( - topicName: string, successCallback: () => void): void { - this.editableTopicBackendApiService.doesTopicWithNameExistAsync( - topicName).then((topicNameExists) => { - this._setTopicWithNameExists(topicNameExists); - if (successCallback) { - successCallback(); - } - }, (error) => { - this.alertsService.addWarning( - error || - 'There was an error when checking if the topic name ' + - 'exists for another topic.'); - }); + topicName: string, + successCallback: () => void + ): void { + this.editableTopicBackendApiService + .doesTopicWithNameExistAsync(topicName) + .then( + topicNameExists => { + this._setTopicWithNameExists(topicNameExists); + if (successCallback) { + successCallback(); + } + }, + error => { + this.alertsService.addWarning( + error || + 'There was an error when checking if the topic name ' + + 'exists for another topic.' + ); + } + ); } /** @@ -579,34 +634,39 @@ export class TopicEditorStateService { * has been successfully updated. */ updateExistenceOfTopicUrlFragment( - topicUrlFragment: string, - successCallback: () => void, - errorCallback: () => void + topicUrlFragment: string, + successCallback: () => void, + errorCallback: () => void ): void { - this.editableTopicBackendApiService.doesTopicWithUrlFragmentExistAsync( - topicUrlFragment).then((topicUrlFragmentExists) => { - this._setTopicWithUrlFragmentExists(topicUrlFragmentExists); - if (successCallback) { - successCallback(); - } - }, (errorResponse) => { - if (errorCallback) { - errorCallback(); - } - /** - * This backend api service uses a HTTP link which is generated with - * the help of inputted url fragment. So, whenever a url fragment is - * entered against the specified reg-ex(or rules) wrong HTTP link is - * generated and causes server to respond with 400 error. Because - * server also checks for reg-ex match. - */ - if (errorResponse.status !== 400) { - this.alertsService.addWarning( - errorResponse.message || - 'There was an error when checking if the topic url fragment ' + - 'exists for another topic.'); - } - }); + this.editableTopicBackendApiService + .doesTopicWithUrlFragmentExistAsync(topicUrlFragment) + .then( + topicUrlFragmentExists => { + this._setTopicWithUrlFragmentExists(topicUrlFragmentExists); + if (successCallback) { + successCallback(); + } + }, + errorResponse => { + if (errorCallback) { + errorCallback(); + } + /** + * This backend api service uses a HTTP link which is generated with + * the help of inputted url fragment. So, whenever a url fragment is + * entered against the specified reg-ex(or rules) wrong HTTP link is + * generated and causes server to respond with 400 error. Because + * server also checks for reg-ex match. + */ + if (errorResponse.status !== 400) { + this.alertsService.addWarning( + errorResponse.message || + 'There was an error when checking if the topic url fragment ' + + 'exists for another topic.' + ); + } + } + ); } get onStorySummariesInitialized(): EventEmitter { @@ -618,5 +678,9 @@ export class TopicEditorStateService { } } -angular.module('oppia').factory('TopicEditorStateService', - downgradeInjectable(TopicEditorStateService)); +angular + .module('oppia') + .factory( + 'TopicEditorStateService', + downgradeInjectable(TopicEditorStateService) + ); diff --git a/core/templates/pages/topic-editor-page/subtopic-editor/subtopic-editor-tab.component.spec.ts b/core/templates/pages/topic-editor-page/subtopic-editor/subtopic-editor-tab.component.spec.ts index 7038bb342dae..fdeb358aa54c 100644 --- a/core/templates/pages/topic-editor-page/subtopic-editor/subtopic-editor-tab.component.spec.ts +++ b/core/templates/pages/topic-editor-page/subtopic-editor/subtopic-editor-tab.component.spec.ts @@ -12,27 +12,26 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Unit tests for the subtopic editor tab component. */ -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ShortSkillSummary } from 'domain/skill/short-skill-summary.model'; -import { Subtopic } from 'domain/topic/subtopic.model'; -import { SubtopicPage } from 'domain/topic/subtopic-page.model'; -import { SubtopicEditorTabComponent } from './subtopic-editor-tab.component'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TopicEditorStateService } from '../services/topic-editor-state.service'; -import { TopicUpdateService } from 'domain/topic/topic-update.service'; -import { SubtopicValidationService } from '../services/subtopic-validation.service'; -import { TopicEditorRoutingService } from '../services/topic-editor-routing.service'; -import { Topic } from 'domain/topic/topic-object.model'; -import { QuestionBackendApiService } from 'domain/question/question-backend-api.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { CdkDragDrop } from '@angular/cdk/drag-drop'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import {ShortSkillSummary} from 'domain/skill/short-skill-summary.model'; +import {Subtopic} from 'domain/topic/subtopic.model'; +import {SubtopicPage} from 'domain/topic/subtopic-page.model'; +import {SubtopicEditorTabComponent} from './subtopic-editor-tab.component'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TopicEditorStateService} from '../services/topic-editor-state.service'; +import {TopicUpdateService} from 'domain/topic/topic-update.service'; +import {SubtopicValidationService} from '../services/subtopic-validation.service'; +import {TopicEditorRoutingService} from '../services/topic-editor-routing.service'; +import {Topic} from 'domain/topic/topic-object.model'; +import {QuestionBackendApiService} from 'domain/question/question-backend-api.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {CdkDragDrop} from '@angular/cdk/drag-drop'; class MockQuestionBackendApiService { async fetchTotalQuestionCountForSkillIdsAsync() { @@ -56,15 +55,14 @@ class MockWindowRef { href: 'href', pathname: 'pathname', search: 'search', - hash: 'hash' + hash: 'hash', }, open() { return; - } + }, }; } - describe('Subtopic editor tab', () => { let component: SubtopicEditorTabComponent; let fixture: ComponentFixture; @@ -87,18 +85,18 @@ describe('Subtopic editor tab', () => { TopicEditorRoutingService, { provide: QuestionBackendApiService, - useClass: MockQuestionBackendApiService + useClass: MockQuestionBackendApiService, }, { provide: WindowDimensionsService, - useClass: MockWindowDimensionsService + useClass: MockWindowDimensionsService, }, { provide: WindowRef, - useClass: MockWindowRef - } + useClass: MockWindowRef, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); @@ -112,15 +110,30 @@ describe('Subtopic editor tab', () => { wds = TestBed.inject(WindowDimensionsService); let topic = new Topic( - 'id', 'Topic name loading', 'Abbrev. name loading', - 'Url Fragment loading', 'Topic description loading', 'en', - [], [], [], 1, 1, [], 'str', '', {}, false, '', '', [] + 'id', + 'Topic name loading', + 'Abbrev. name loading', + 'Url Fragment loading', + 'Topic description loading', + 'en', + [], + [], + [], + 1, + 1, + [], + 'str', + '', + {}, + false, + '', + '', + [] ); let subtopic = Subtopic.createFromTitle(1, 'Subtopic1'); subtopic._skillIds = ['skill_1']; subtopic.setUrlFragment('dummy-url'); - skillSummary = ShortSkillSummary.create( - 'skill_1', 'Description 1'); + skillSummary = ShortSkillSummary.create('skill_1', 'Description 1'); topic._uncategorizedSkillSummaries = [skillSummary]; let subtopicPage = SubtopicPage.createDefault('asd2r42', 1); topic._id = 'sndsjfn42'; @@ -128,21 +141,22 @@ describe('Subtopic editor tab', () => { spyOnProperty(topicEditorStateService, 'onTopicInitialized').and.callFake( () => { return topicInitializedEventEmitter; - }); - spyOnProperty( - topicEditorStateService, 'onTopicReinitialized').and.callFake( + } + ); + spyOnProperty(topicEditorStateService, 'onTopicReinitialized').and.callFake( () => { return topicReinitializedEventEmitter; - }); + } + ); - topic.getSubtopicById = (id) => { + topic.getSubtopicById = id => { return id === 99 ? null : subtopic; }; spyOn(topicEditorStateService, 'getTopic').and.returnValue(topic); spyOn(topicEditorStateService, 'hasLoadedTopic').and.returnValue(true); - spyOn( - topicEditorStateService, - 'getSubtopicPage').and.returnValue(subtopicPage); + spyOn(topicEditorStateService, 'getSubtopicPage').and.returnValue( + subtopicPage + ); component.ngOnInit(); component.initEditor(); }); @@ -161,81 +175,83 @@ describe('Subtopic editor tab', () => { expect(titleSpy).toHaveBeenCalled(); }); - it('should call topicUpdateService if subtopic title is not updated', - () => { - component.updateSubtopicTitle('New title'); - let titleSpy = spyOn(topicUpdateService, 'setSubtopicTitle'); - component.updateSubtopicTitle('New title'); - expect(titleSpy).not.toHaveBeenCalled(); - }); + it('should call topicUpdateService if subtopic title is not updated', () => { + component.updateSubtopicTitle('New title'); + let titleSpy = spyOn(topicUpdateService, 'setSubtopicTitle'); + component.updateSubtopicTitle('New title'); + expect(titleSpy).not.toHaveBeenCalled(); + }); - it('should call topicUpdateService if subtopic url fragment is updated', - () => { - let urlFragmentSpy = spyOn(topicUpdateService, 'setSubtopicUrlFragment'); - component.updateSubtopicUrlFragment('new-url'); - expect(urlFragmentSpy).toHaveBeenCalled(); - }); + it('should call topicUpdateService if subtopic url fragment is updated', () => { + let urlFragmentSpy = spyOn(topicUpdateService, 'setSubtopicUrlFragment'); + component.updateSubtopicUrlFragment('new-url'); + expect(urlFragmentSpy).toHaveBeenCalled(); + }); - it('should not call topicUpdateService when url fragment has not changed', - () => { - component.updateSubtopicUrlFragment('subtopic-url'); - component.initialSubtopicUrlFragment = 'subtopic-url'; - let urlFragmentSpy = spyOn(topicUpdateService, 'setSubtopicUrlFragment'); - component.updateSubtopicUrlFragment('subtopic-url'); - expect(urlFragmentSpy).not.toHaveBeenCalled(); - }); - - it('should not call topicUpdateService if subtopic url fragment is invalid', - () => { - let urlFragmentSpy = spyOn(topicUpdateService, 'setSubtopicUrlFragment'); - component.updateSubtopicUrlFragment('new url'); - expect(urlFragmentSpy).not.toHaveBeenCalled(); - component.updateSubtopicUrlFragment('New-Url'); - expect(urlFragmentSpy).not.toHaveBeenCalled(); - component.updateSubtopicUrlFragment('new-url-'); - expect(urlFragmentSpy).not.toHaveBeenCalled(); - component.updateSubtopicUrlFragment('new123url'); - expect(urlFragmentSpy).not.toHaveBeenCalled(); - }); - - it('should call topicUpdateService if subtopic thumbnail updates', - () => { - let thubmnailSpy = ( - spyOn(topicUpdateService, 'setSubtopicThumbnailFilename')); - component.updateSubtopicThumbnailFilename('img.svg'); - expect(thubmnailSpy).toHaveBeenCalled(); - }); + it('should not call topicUpdateService when url fragment has not changed', () => { + component.updateSubtopicUrlFragment('subtopic-url'); + component.initialSubtopicUrlFragment = 'subtopic-url'; + let urlFragmentSpy = spyOn(topicUpdateService, 'setSubtopicUrlFragment'); + component.updateSubtopicUrlFragment('subtopic-url'); + expect(urlFragmentSpy).not.toHaveBeenCalled(); + }); - it('should call topicUpdateService if subtopic thumbnail is not updated', - () => { - component.updateSubtopicThumbnailFilename('img.svg'); - let thubmnailSpy = spyOn(topicUpdateService, 'setSubtopicTitle'); - component.updateSubtopicThumbnailFilename('img.svg'); - expect(thubmnailSpy).not.toHaveBeenCalled(); - }); + it('should not call topicUpdateService if subtopic url fragment is invalid', () => { + let urlFragmentSpy = spyOn(topicUpdateService, 'setSubtopicUrlFragment'); + component.updateSubtopicUrlFragment('new url'); + expect(urlFragmentSpy).not.toHaveBeenCalled(); + component.updateSubtopicUrlFragment('New-Url'); + expect(urlFragmentSpy).not.toHaveBeenCalled(); + component.updateSubtopicUrlFragment('new-url-'); + expect(urlFragmentSpy).not.toHaveBeenCalled(); + component.updateSubtopicUrlFragment('new123url'); + expect(urlFragmentSpy).not.toHaveBeenCalled(); + }); - it('should call topicUpdateService if subtopic thumbnail bg color updates', - () => { - let thubmnailBgSpy = ( - spyOn(topicUpdateService, 'setSubtopicThumbnailBgColor')); - component.updateSubtopicThumbnailBgColor('#FFFFFF'); - expect(thubmnailBgSpy).toHaveBeenCalled(); - }); + it('should call topicUpdateService if subtopic thumbnail updates', () => { + let thubmnailSpy = spyOn( + topicUpdateService, + 'setSubtopicThumbnailFilename' + ); + component.updateSubtopicThumbnailFilename('img.svg'); + expect(thubmnailSpy).toHaveBeenCalled(); + }); - it('should not call topicUpdateService if subtopic ' + - 'thumbnail bg color is not updated', - () => { - component.updateSubtopicThumbnailBgColor('#FFFFFF'); + it('should call topicUpdateService if subtopic thumbnail is not updated', () => { + component.updateSubtopicThumbnailFilename('img.svg'); + let thubmnailSpy = spyOn(topicUpdateService, 'setSubtopicTitle'); + component.updateSubtopicThumbnailFilename('img.svg'); + expect(thubmnailSpy).not.toHaveBeenCalled(); + }); + + it('should call topicUpdateService if subtopic thumbnail bg color updates', () => { let thubmnailBgSpy = spyOn( - topicUpdateService, 'setSubtopicThumbnailBgColor'); + topicUpdateService, + 'setSubtopicThumbnailBgColor' + ); component.updateSubtopicThumbnailBgColor('#FFFFFF'); - expect(thubmnailBgSpy).not.toHaveBeenCalled(); + expect(thubmnailBgSpy).toHaveBeenCalled(); }); + it( + 'should not call topicUpdateService if subtopic ' + + 'thumbnail bg color is not updated', + () => { + component.updateSubtopicThumbnailBgColor('#FFFFFF'); + let thubmnailBgSpy = spyOn( + topicUpdateService, + 'setSubtopicThumbnailBgColor' + ); + component.updateSubtopicThumbnailBgColor('#FFFFFF'); + expect(thubmnailBgSpy).not.toHaveBeenCalled(); + } + ); + it('should return skill editor URL', () => { let skillId = 'asd4242a'; expect(component.getSkillEditorUrl(skillId)).toEqual( - '/skill_editor/' + skillId); + '/skill_editor/' + skillId + ); }); it('should show schema editor', () => { @@ -245,78 +261,76 @@ describe('Subtopic editor tab', () => { }); it('should return if skill is deleted', () => { - let skillSummary = ShortSkillSummary.create( - '1', 'Skill description'); + let skillSummary = ShortSkillSummary.create('1', 'Skill description'); expect(component.isSkillDeleted(skillSummary)).toEqual(false); }); - it('should call topicUpdateService when skill is rearranged', - () => { - let removeSkillSpy = spyOn( - topicUpdateService, 'rearrangeSkillInSubtopic'); - let skillSummaries = [ - ShortSkillSummary.createFromBackendDict({ - skill_id: '1', - skill_description: 'Skill Description' - }), - ShortSkillSummary.createFromBackendDict({ - skill_id: '2', - skill_description: 'Skill Description' - }) - ]; - subtopic = Subtopic.createFromTitle(1, 'subtopic1'); - subtopic._skillSummaries = skillSummaries; - const event = { - previousIndex: 1, - currentIndex: 2, - } as CdkDragDrop; - component.drop(event); - expect(removeSkillSpy).toHaveBeenCalled(); - }); + it('should call topicUpdateService when skill is rearranged', () => { + let removeSkillSpy = spyOn(topicUpdateService, 'rearrangeSkillInSubtopic'); + let skillSummaries = [ + ShortSkillSummary.createFromBackendDict({ + skill_id: '1', + skill_description: 'Skill Description', + }), + ShortSkillSummary.createFromBackendDict({ + skill_id: '2', + skill_description: 'Skill Description', + }), + ]; + subtopic = Subtopic.createFromTitle(1, 'subtopic1'); + subtopic._skillSummaries = skillSummaries; + const event = { + previousIndex: 1, + currentIndex: 2, + } as CdkDragDrop; + component.drop(event); + expect(removeSkillSpy).toHaveBeenCalled(); + }); it('should set the error message if subtopic title is invalid', () => { expect(component.errorMsg).toEqual(null); - spyOn(subtopicValidationService, 'checkValidSubtopicName') - .and.callFake(() => false); + spyOn(subtopicValidationService, 'checkValidSubtopicName').and.callFake( + () => false + ); component.updateSubtopicTitle('New Subtopic1'); expect(component.errorMsg).toEqual( - 'A subtopic with this title already exists'); + 'A subtopic with this title already exists' + ); }); it('should reset the error message', () => { - spyOn(subtopicValidationService, 'checkValidSubtopicName') - .and.callFake(() => false); + spyOn(subtopicValidationService, 'checkValidSubtopicName').and.callFake( + () => false + ); component.updateSubtopicTitle('New Subtopic1'); expect(component.errorMsg).toEqual( - 'A subtopic with this title already exists'); + 'A subtopic with this title already exists' + ); component.resetErrorMsg(); expect(component.errorMsg).toEqual(null); }); - it('should call topicUpdateService to update the SubtopicPageContent', - () => { - let updateSubtopicSpy = ( - spyOn(topicUpdateService, 'setSubtopicPageContentsHtml')); - component.htmlData = 'new html data'; - component.updateHtmlData(); - expect(updateSubtopicSpy).toHaveBeenCalled(); - }); - - it('should call the topicUpdateService if skill is removed from subtopic', - () => { - let removeSkillSpy = ( - spyOn(topicUpdateService, 'removeSkillFromSubtopic')); - component.removeSkillFromSubtopic({} as ShortSkillSummary); - expect(removeSkillSpy).toHaveBeenCalled(); - }); + it('should call topicUpdateService to update the SubtopicPageContent', () => { + let updateSubtopicSpy = spyOn( + topicUpdateService, + 'setSubtopicPageContentsHtml' + ); + component.htmlData = 'new html data'; + component.updateHtmlData(); + expect(updateSubtopicSpy).toHaveBeenCalled(); + }); - it('should call the topicUpdateService if skill is removed from topic', - () => { - let removeSkillSpy = ( - spyOn(topicUpdateService, 'removeSkillFromSubtopic')); - component.removeSkillFromTopic(skillSummary); - expect(removeSkillSpy).toHaveBeenCalled(); - }); + it('should call the topicUpdateService if skill is removed from subtopic', () => { + let removeSkillSpy = spyOn(topicUpdateService, 'removeSkillFromSubtopic'); + component.removeSkillFromSubtopic({} as ShortSkillSummary); + expect(removeSkillSpy).toHaveBeenCalled(); + }); + + it('should call the topicUpdateService if skill is removed from topic', () => { + let removeSkillSpy = spyOn(topicUpdateService, 'removeSkillFromSubtopic'); + component.removeSkillFromTopic(skillSummary); + expect(removeSkillSpy).toHaveBeenCalled(); + }); it('should set skill edit options index', () => { component.showSkillEditOptions(10); @@ -325,43 +339,55 @@ describe('Subtopic editor tab', () => { expect(component.selectedSkillEditOptionsIndex).toEqual(20); }); - it('should toggle skills list preview only in mobile view' + - 'when window is narrow', () => { - spyOn(wds, 'isWindowNarrow').and.returnValue(true); - expect(component.skillsListIsShown).toEqual(true); - component.togglePreviewSkillCard(); - expect(component.skillsListIsShown).toEqual(false); - component.togglePreviewSkillCard(); - expect(component.skillsListIsShown).toEqual(true); - component.togglePreviewSkillCard(); - }); + it( + 'should toggle skills list preview only in mobile view' + + 'when window is narrow', + () => { + spyOn(wds, 'isWindowNarrow').and.returnValue(true); + expect(component.skillsListIsShown).toEqual(true); + component.togglePreviewSkillCard(); + expect(component.skillsListIsShown).toEqual(false); + component.togglePreviewSkillCard(); + expect(component.skillsListIsShown).toEqual(true); + component.togglePreviewSkillCard(); + } + ); - it('should toggle skills list preview only in mobile view' + - 'when window is not narrow', () => { - spyOn(wds, 'isWindowNarrow').and.returnValue(false); - component.skillsListIsShown = true; - component.togglePreviewSkillCard(); - expect(component.skillsListIsShown).toEqual(true); - }); + it( + 'should toggle skills list preview only in mobile view' + + 'when window is not narrow', + () => { + spyOn(wds, 'isWindowNarrow').and.returnValue(false); + component.skillsListIsShown = true; + component.togglePreviewSkillCard(); + expect(component.skillsListIsShown).toEqual(true); + } + ); - it('should toggle subtopic editor card only in mobile view' + - 'when window is narrow', () => { - spyOn(wds, 'isWindowNarrow').and.returnValue(true); - expect(component.subtopicEditorCardIsShown).toEqual(true); - component.toggleSubtopicEditorCard(); - expect(component.subtopicEditorCardIsShown).toEqual(false); - component.toggleSubtopicEditorCard(); - expect(component.subtopicEditorCardIsShown).toEqual(true); - component.toggleSubtopicEditorCard(); - }); + it( + 'should toggle subtopic editor card only in mobile view' + + 'when window is narrow', + () => { + spyOn(wds, 'isWindowNarrow').and.returnValue(true); + expect(component.subtopicEditorCardIsShown).toEqual(true); + component.toggleSubtopicEditorCard(); + expect(component.subtopicEditorCardIsShown).toEqual(false); + component.toggleSubtopicEditorCard(); + expect(component.subtopicEditorCardIsShown).toEqual(true); + component.toggleSubtopicEditorCard(); + } + ); - it('should toggle subtopic editor card only in mobile view' + - 'when window is not narrow', () => { - spyOn(wds, 'isWindowNarrow').and.returnValue(false); - component.subtopicEditorCardIsShown = true; - component.toggleSubtopicEditorCard(); - expect(component.subtopicEditorCardIsShown).toEqual(true); - }); + it( + 'should toggle subtopic editor card only in mobile view' + + 'when window is not narrow', + () => { + spyOn(wds, 'isWindowNarrow').and.returnValue(false); + component.subtopicEditorCardIsShown = true; + component.toggleSubtopicEditorCard(); + expect(component.subtopicEditorCardIsShown).toEqual(true); + } + ); it('should toggle subtopic preview', () => { expect(component.subtopicPreviewCardIsShown).toEqual(false); @@ -372,12 +398,11 @@ describe('Subtopic editor tab', () => { component.toggleSubtopicPreview(); }); - it('should call topicEditorRoutingService to navigate To Topic Editor', - () => { - let navigateSpy = spyOn(topicEditorRoutingService, 'navigateToMainTab'); - component.navigateToTopicEditor(); - expect(navigateSpy).toHaveBeenCalled(); - }); + it('should call topicEditorRoutingService to navigate To Topic Editor', () => { + let navigateSpy = spyOn(topicEditorRoutingService, 'navigateToMainTab'); + component.navigateToTopicEditor(); + expect(navigateSpy).toHaveBeenCalled(); + }); it('should call initEditor when topic is initialized', () => { spyOn(component, 'initEditor').and.callThrough(); @@ -394,8 +419,9 @@ describe('Subtopic editor tab', () => { }); it('should redirect to topic editor if subtopic id is invalid', () => { - spyOn(topicEditorRoutingService, 'getSubtopicIdFromUrl').and - .returnValue(99); + spyOn(topicEditorRoutingService, 'getSubtopicIdFromUrl').and.returnValue( + 99 + ); let navigateSpy = spyOn(topicEditorRoutingService, 'navigateToMainTab'); component.initEditor(); expect(navigateSpy).toHaveBeenCalled(); diff --git a/core/templates/pages/topic-editor-page/subtopic-editor/subtopic-editor-tab.component.ts b/core/templates/pages/topic-editor-page/subtopic-editor/subtopic-editor-tab.component.ts index 16c2b41071aa..6275ded4d23a 100644 --- a/core/templates/pages/topic-editor-page/subtopic-editor/subtopic-editor-tab.component.ts +++ b/core/templates/pages/topic-editor-page/subtopic-editor/subtopic-editor-tab.component.ts @@ -16,27 +16,27 @@ * @fileoverview Component for the Subtopic Editor Tab. */ -import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { AppConstants } from 'app.constants'; -import { QuestionBackendApiService } from 'domain/question/question-backend-api.service'; -import { ShortSkillSummary } from 'domain/skill/short-skill-summary.model'; -import { SubtopicPage } from 'domain/topic/subtopic-page.model'; -import { Subtopic } from 'domain/topic/subtopic.model'; -import { TopicUpdateService } from 'domain/topic/topic-update.service'; -import { Topic } from 'domain/topic/topic-object.model'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { Subscription } from 'rxjs'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { SubtopicValidationService } from '../services/subtopic-validation.service'; -import { TopicEditorRoutingService } from '../services/topic-editor-routing.service'; -import { TopicEditorStateService } from '../services/topic-editor-state.service'; +import {CdkDragDrop, moveItemInArray} from '@angular/cdk/drag-drop'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {AppConstants} from 'app.constants'; +import {QuestionBackendApiService} from 'domain/question/question-backend-api.service'; +import {ShortSkillSummary} from 'domain/skill/short-skill-summary.model'; +import {SubtopicPage} from 'domain/topic/subtopic-page.model'; +import {Subtopic} from 'domain/topic/subtopic.model'; +import {TopicUpdateService} from 'domain/topic/topic-update.service'; +import {Topic} from 'domain/topic/topic-object.model'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {Subscription} from 'rxjs'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {SubtopicValidationService} from '../services/subtopic-validation.service'; +import {TopicEditorRoutingService} from '../services/topic-editor-routing.service'; +import {TopicEditorStateService} from '../services/topic-editor-state.service'; @Component({ selector: 'oppia-subtopic-editor-tab', - templateUrl: './subtopic-editor-tab.component.html' + templateUrl: './subtopic-editor-tab.component.html', }) export class SubtopicEditorTabComponent implements OnInit, OnDestroy { hostname: string; @@ -67,7 +67,7 @@ export class SubtopicEditorTabComponent implements OnInit, OnDestroy { skillsListIsShown: boolean; subtopicEditorCardIsShown: boolean; selectedSkillEditOptionsIndex: number; - SUBTOPIC_PAGE_SCHEMA: { type: string; ui_config: { rows: number }}; + SUBTOPIC_PAGE_SCHEMA: {type: string; ui_config: {rows: number}}; constructor( private questionBackendApiService: QuestionBackendApiService, @@ -77,7 +77,7 @@ export class SubtopicEditorTabComponent implements OnInit, OnDestroy { private topicUpdateService: TopicUpdateService, private urlInterpolationService: UrlInterpolationService, private windowDimensionsService: WindowDimensionsService, - private windowRef: WindowRef, + private windowRef: WindowRef ) {} directiveSubscriptions = new Subscription(); @@ -86,8 +86,8 @@ export class SubtopicEditorTabComponent implements OnInit, OnDestroy { initEditor(): void { this.hostname = this.windowRef.nativeWindow.location.hostname; this.topic = this.topicEditorStateService.getTopic(); - this.classroomUrlFragment = ( - this.topicEditorStateService.getClassroomUrlFragment()); + this.classroomUrlFragment = + this.topicEditorStateService.getClassroomUrlFragment(); this.subtopicId = this.topicEditorRoutingService.getSubtopicIdFromUrl(); this.subtopic = this.topic.getSubtopicById(this.subtopicId); if (!this.subtopic) { @@ -98,37 +98,37 @@ export class SubtopicEditorTabComponent implements OnInit, OnDestroy { this.subtopicUrlFragmentIsValid = false; if (this.topic.getId() && this.subtopic) { this.topicEditorStateService.loadSubtopicPage( - this.topic.getId(), this.subtopicId); + this.topic.getId(), + this.subtopicId + ); this.skillIds = this.subtopic.getSkillIds(); this.questionCount = 0; if (this.skillIds.length) { - this.questionBackendApiService.fetchTotalQuestionCountForSkillIdsAsync( - this.skillIds).then((questionCount) => { - this.questionCount = questionCount; - }); + this.questionBackendApiService + .fetchTotalQuestionCountForSkillIdsAsync(this.skillIds) + .then(questionCount => { + this.questionCount = questionCount; + }); } - this.skillQuestionCountDict = ( - this.topicEditorStateService.getSkillQuestionCountDict()); + this.skillQuestionCountDict = + this.topicEditorStateService.getSkillQuestionCountDict(); this.editableTitle = this.subtopic.getTitle(); - this.editableThumbnailFilename = ( - this.subtopic.getThumbnailFilename()); - this.editableThumbnailBgColor = ( - this.subtopic.getThumbnailBgColor()); + this.editableThumbnailFilename = this.subtopic.getThumbnailFilename(); + this.editableThumbnailBgColor = this.subtopic.getThumbnailBgColor(); this.editableUrlFragment = this.subtopic.getUrlFragment(); this.initialSubtopicUrlFragment = this.subtopic.getUrlFragment(); - this.subtopicPage = ( - this.topicEditorStateService.getSubtopicPage()); - this.allowedBgColors = ( - AppConstants.ALLOWED_THUMBNAIL_BG_COLORS.subtopic); + this.subtopicPage = this.topicEditorStateService.getSubtopicPage(); + this.allowedBgColors = AppConstants.ALLOWED_THUMBNAIL_BG_COLORS.subtopic; var pageContents = this.subtopicPage.getPageContents(); if (pageContents) { this.htmlData = pageContents.getHtml(); } - this.uncategorizedSkillSummaries = ( - this.topic.getUncategorizedSkillSummaries()); - this.subtopicUrlFragmentIsValid = ( + this.uncategorizedSkillSummaries = + this.topic.getUncategorizedSkillSummaries(); + this.subtopicUrlFragmentIsValid = this.subtopicValidationService.isUrlFragmentValid( - this.editableUrlFragment)); + this.editableUrlFragment + ); } } @@ -143,38 +143,48 @@ export class SubtopicEditorTabComponent implements OnInit, OnDestroy { } this.topicUpdateService.setSubtopicTitle( - this.topic, this.subtopic.getId(), title); + this.topic, + this.subtopic.getId(), + title + ); this.editableTitle = title; } drop(event: CdkDragDrop): void { moveItemInArray( this.subtopic.getSkillSummaries(), - event.previousIndex, event.currentIndex); + event.previousIndex, + event.currentIndex + ); this.topicUpdateService.rearrangeSkillInSubtopic( - this.topic, this.subtopic.getId(), - event.previousIndex, event.currentIndex); + this.topic, + this.subtopic.getId(), + event.previousIndex, + event.currentIndex + ); } updateSubtopicUrlFragment(urlFragment: string): void { - this.subtopicUrlFragmentIsValid = ( - this.subtopicValidationService.isUrlFragmentValid(urlFragment)); + this.subtopicUrlFragmentIsValid = + this.subtopicValidationService.isUrlFragmentValid(urlFragment); if (urlFragment === this.initialSubtopicUrlFragment) { this.subtopicUrlFragmentExists = false; return; } - this.subtopicUrlFragmentExists = ( + this.subtopicUrlFragmentExists = this.subtopicValidationService.doesSubtopicWithUrlFragmentExist( - urlFragment)); - if ( - !this.subtopicUrlFragmentIsValid || - this.subtopicUrlFragmentExists) { + urlFragment + ); + if (!this.subtopicUrlFragmentIsValid || this.subtopicUrlFragmentExists) { return; } this.topicUpdateService.setSubtopicUrlFragment( - this.topic, this.subtopic.getId(), urlFragment); + this.topic, + this.subtopic.getId(), + urlFragment + ); this.editableUrlFragment = urlFragment; } @@ -184,7 +194,10 @@ export class SubtopicEditorTabComponent implements OnInit, OnDestroy { return; } this.topicUpdateService.setSubtopicThumbnailFilename( - this.topic, this.subtopic.getId(), newThumbnailFilename); + this.topic, + this.subtopic.getId(), + newThumbnailFilename + ); this.editableThumbnailFilename = newThumbnailFilename; } @@ -194,7 +207,10 @@ export class SubtopicEditorTabComponent implements OnInit, OnDestroy { return; } this.topicUpdateService.setSubtopicThumbnailBgColor( - this.topic, this.subtopic.getId(), newThumbnailBgColor); + this.topic, + this.subtopic.getId(), + newThumbnailBgColor + ); this.editableThumbnailBgColor = newThumbnailBgColor; } @@ -208,20 +224,24 @@ export class SubtopicEditorTabComponent implements OnInit, OnDestroy { getSkillEditorUrl(skillId: string): string { return this.urlInterpolationService.interpolateUrl( - this.SKILL_EDITOR_URL_TEMPLATE, { - skillId: skillId + this.SKILL_EDITOR_URL_TEMPLATE, + { + skillId: skillId, } ); } updateHtmlData(): void { - if (this.htmlData !== - this.subtopicPage.getPageContents().getHtml()) { + if (this.htmlData !== this.subtopicPage.getPageContents().getHtml()) { var subtitledHtml = angular.copy( - this.subtopicPage.getPageContents().getSubtitledHtml()); + this.subtopicPage.getPageContents().getSubtitledHtml() + ); subtitledHtml.html = this.htmlData; this.topicUpdateService.setSubtopicPageContentsHtml( - this.subtopicPage, this.subtopic.getId(), subtitledHtml); + this.subtopicPage, + this.subtopic.getId(), + subtitledHtml + ); this.topicEditorStateService.setSubtopicPage(this.subtopicPage); this.schemaEditorIsShown = false; } @@ -257,23 +277,28 @@ export class SubtopicEditorTabComponent implements OnInit, OnDestroy { } showSkillEditOptions(index: number): void { - this.selectedSkillEditOptionsIndex = ( - (this.selectedSkillEditOptionsIndex === index) ? -1 : index); + this.selectedSkillEditOptionsIndex = + this.selectedSkillEditOptionsIndex === index ? -1 : index; } removeSkillFromSubtopic(skillSummary: ShortSkillSummary): void { this.selectedSkillEditOptionsIndex = -1; this.topicUpdateService.removeSkillFromSubtopic( - this.topic, this.subtopicId, skillSummary); + this.topic, + this.subtopicId, + skillSummary + ); this.initEditor(); } removeSkillFromTopic(skillSummary: ShortSkillSummary): void { this.selectedSkillEditOptionsIndex = -1; this.topicUpdateService.removeSkillFromSubtopic( - this.topic, this.subtopicId, skillSummary); - this.topicUpdateService.removeUncategorizedSkill( - this.topic, skillSummary); + this.topic, + this.subtopicId, + skillSummary + ); + this.topicUpdateService.removeUncategorizedSkill(this.topic, skillSummary); this.initEditor(); } @@ -285,37 +310,31 @@ export class SubtopicEditorTabComponent implements OnInit, OnDestroy { this.SUBTOPIC_PAGE_SCHEMA = { type: 'html', ui_config: { - rows: 100 - } + rows: 100, + }, }; this.htmlData = ''; - this.skillsListIsShown = ( - !this.windowDimensionsService.isWindowNarrow()); + this.skillsListIsShown = !this.windowDimensionsService.isWindowNarrow(); this.subtopicPreviewCardIsShown = false; this.subtopicEditorCardIsShown = true; this.schemaEditorIsShown = false; this.directiveSubscriptions.add( - this.topicEditorStateService.onSubtopicPageLoaded.subscribe( - () => { - this.subtopicPage = ( - this.topicEditorStateService.getSubtopicPage()); - var pageContents = this.subtopicPage.getPageContents(); - this.htmlData = pageContents.getHtml(); - } - ) + this.topicEditorStateService.onSubtopicPageLoaded.subscribe(() => { + this.subtopicPage = this.topicEditorStateService.getSubtopicPage(); + var pageContents = this.subtopicPage.getPageContents(); + this.htmlData = pageContents.getHtml(); + }) ); this.directiveSubscriptions.add( - this.topicEditorStateService.onTopicInitialized.subscribe( - () => { - this.initEditor(); - } - )); + this.topicEditorStateService.onTopicInitialized.subscribe(() => { + this.initEditor(); + }) + ); this.directiveSubscriptions.add( - this.topicEditorStateService.onTopicReinitialized.subscribe( - () => { - this.initEditor(); - } - )); + this.topicEditorStateService.onTopicReinitialized.subscribe(() => { + this.initEditor(); + }) + ); if (this.topicEditorStateService.hasLoadedTopic()) { this.initEditor(); } @@ -326,7 +345,9 @@ export class SubtopicEditorTabComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive('oppiaSubtopicEditorTab', +angular.module('oppia').directive( + 'oppiaSubtopicEditorTab', downgradeComponent({ - component: SubtopicEditorTabComponent - }) as angular.IDirectiveFactory); + component: SubtopicEditorTabComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/topic-editor-page/subtopic-editor/subtopic-preview-tab.component.spec.ts b/core/templates/pages/topic-editor-page/subtopic-editor/subtopic-preview-tab.component.spec.ts index 4b06ffb0d460..58d9ca6233b5 100644 --- a/core/templates/pages/topic-editor-page/subtopic-editor/subtopic-preview-tab.component.spec.ts +++ b/core/templates/pages/topic-editor-page/subtopic-editor/subtopic-preview-tab.component.spec.ts @@ -16,17 +16,17 @@ * @fileoverview Unit tests for the subtopic preview tab directive. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { SubtopicPreviewTab } from './subtopic-preview-tab.component'; -import { Topic } from 'domain/topic/topic-object.model'; -import { Subtopic } from 'domain/topic/subtopic.model'; -import { TopicEditorRoutingService } from '../services/topic-editor-routing.service'; -import { TopicEditorStateService } from '../services/topic-editor-state.service'; -import { SubtopicPage } from 'domain/topic/subtopic-page.model'; -import { SubtopicPageContents } from 'domain/topic/subtopic-page-contents.model'; -import { EventEmitter } from '@angular/core'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {SubtopicPreviewTab} from './subtopic-preview-tab.component'; +import {Topic} from 'domain/topic/topic-object.model'; +import {Subtopic} from 'domain/topic/subtopic.model'; +import {TopicEditorRoutingService} from '../services/topic-editor-routing.service'; +import {TopicEditorStateService} from '../services/topic-editor-state.service'; +import {SubtopicPage} from 'domain/topic/subtopic-page.model'; +import {SubtopicPageContents} from 'domain/topic/subtopic-page-contents.model'; +import {EventEmitter} from '@angular/core'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; describe('SubtopicPreviewTab', () => { let component: SubtopicPreviewTab; @@ -39,7 +39,7 @@ describe('SubtopicPreviewTab', () => { let subtopicPageContentsDict = SubtopicPageContents.createFromBackendDict({ subtitled_html: { html: 'test content', - content_id: 'content' + content_id: 'content', }, recorded_voiceovers: { voiceovers_mapping: { @@ -48,11 +48,11 @@ describe('SubtopicPreviewTab', () => { filename: 'test.mp3', file_size_bytes: 100, needs_update: false, - duration_secs: 10 - } - } - } - } + duration_secs: 10, + }, + }, + }, + }, }); let topicInitializedEventEmitter = new EventEmitter(); let topicReinitializedEventEmitter = new EventEmitter(); @@ -63,7 +63,7 @@ describe('SubtopicPreviewTab', () => { imports: [HttpClientTestingModule], declarations: [SubtopicPreviewTab], providers: [TopicEditorStateService, TopicEditorRoutingService], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -79,20 +79,36 @@ describe('SubtopicPreviewTab', () => { component = fixture.componentInstance; topic = new Topic( - 'id', 'Topic name loading', 'Abbrev. name loading', - 'Url Fragment loading', 'Topic description loading', 'en', - [], [], [], 1, 1, [], 'str', '', {}, false, '', '', [] + 'id', + 'Topic name loading', + 'Abbrev. name loading', + 'Url Fragment loading', + 'Topic description loading', + 'en', + [], + [], + [], + 1, + 1, + [], + 'str', + '', + {}, + false, + '', + '', + [] ); subtopic = Subtopic.createFromTitle(1, 'Subtopic1'); subtopic.setThumbnailFilename('thumbnailFilename.svg'); subtopic.setThumbnailBgColor('#FFFFFF'); - topic.getSubtopics = function() { + topic.getSubtopics = function () { return [subtopic]; }; topic.getId = () => { return '1'; }; - topic.getSubtopicById = function(id) { + topic.getSubtopicById = function (id) { return id === 99 ? null : subtopic; }; @@ -100,10 +116,10 @@ describe('SubtopicPreviewTab', () => { subtopicPage.setPageContents(subtopicPageContentsDict); spyOn(topicEditorStateService, 'getTopic').and.returnValue(topic); - spyOn(topicEditorStateService, 'getSubtopicPage').and - .returnValue(subtopicPage); - spyOn(topicEditorRoutingService, 'getSubtopicIdFromUrl').and - .returnValue(1); + spyOn(topicEditorStateService, 'getSubtopicPage').and.returnValue( + subtopicPage + ); + spyOn(topicEditorRoutingService, 'getSubtopicIdFromUrl').and.returnValue(1); }); it('should initialize when subtopic preview tab is opened', () => { @@ -113,8 +129,10 @@ describe('SubtopicPreviewTab', () => { expect(component.topic).toEqual(topic); expect(component.subtopicId).toBe(1); expect(component.subtopic).toEqual(subtopic); - expect(topicEditorStateService.loadSubtopicPage) - .toHaveBeenCalledWith('1', 1); + expect(topicEditorStateService.loadSubtopicPage).toHaveBeenCalledWith( + '1', + 1 + ); expect(component.editableTitle).toBe('Subtopic1'); expect(component.editableThumbnailFilename).toBe('thumbnailFilename.svg'); expect(component.editableThumbnailBgColor).toBe('#FFFFFF'); @@ -123,24 +141,30 @@ describe('SubtopicPreviewTab', () => { expect(component.htmlData).toEqual('test content'); }); - it('should get subtopic contents when subtopic preview page is' + - ' loaded', () => { - spyOnProperty(topicEditorStateService, 'onSubtopicPageLoaded').and - .returnValue(subtopicPageLoadedEventEmitter); - // The ngOnInit is called to add the directiveSubscriptions. - component.ngOnInit(); - component.htmlData = ''; - - subtopicPageLoadedEventEmitter.emit(); - - expect(component.subtopicPage).toEqual(subtopicPage); - expect(component.pageContents).toEqual(subtopicPageContentsDict); - expect(component.htmlData).toEqual('test content'); - }); + it( + 'should get subtopic contents when subtopic preview page is' + ' loaded', + () => { + spyOnProperty( + topicEditorStateService, + 'onSubtopicPageLoaded' + ).and.returnValue(subtopicPageLoadedEventEmitter); + // The ngOnInit is called to add the directiveSubscriptions. + component.ngOnInit(); + component.htmlData = ''; + + subtopicPageLoadedEventEmitter.emit(); + + expect(component.subtopicPage).toEqual(subtopicPage); + expect(component.pageContents).toEqual(subtopicPageContentsDict); + expect(component.htmlData).toEqual('test content'); + } + ); it('should call initEditor when topic is initialized', () => { - spyOnProperty(topicEditorStateService, 'onTopicInitialized').and - .returnValue(topicInitializedEventEmitter); + spyOnProperty( + topicEditorStateService, + 'onTopicInitialized' + ).and.returnValue(topicInitializedEventEmitter); // The ngOnInit is called to add the directiveSubscriptions. component.ngOnInit(); component.subtopicId = 2; @@ -155,10 +179,14 @@ describe('SubtopicPreviewTab', () => { }); it('should call initEditor when topic is reinitialized', () => { - spyOnProperty(topicEditorStateService, 'onTopicInitialized').and - .returnValue(topicInitializedEventEmitter); - spyOnProperty(topicEditorStateService, 'onTopicReinitialized').and - .returnValue(topicReinitializedEventEmitter); + spyOnProperty( + topicEditorStateService, + 'onTopicInitialized' + ).and.returnValue(topicInitializedEventEmitter); + spyOnProperty( + topicEditorStateService, + 'onTopicReinitialized' + ).and.returnValue(topicReinitializedEventEmitter); // The ngOnInit is called to add the directiveSubscriptions. component.ngOnInit(); @@ -184,7 +212,8 @@ describe('SubtopicPreviewTab', () => { component.navigateToSubtopic(); expect(component.subtopicId).toBe(1); - expect(topicEditorRoutingService.navigateToSubtopicEditorWithId) - .toHaveBeenCalledWith(1); + expect( + topicEditorRoutingService.navigateToSubtopicEditorWithId + ).toHaveBeenCalledWith(1); }); }); diff --git a/core/templates/pages/topic-editor-page/subtopic-editor/subtopic-preview-tab.component.ts b/core/templates/pages/topic-editor-page/subtopic-editor/subtopic-preview-tab.component.ts index 39b60b240c5b..e66bd06f1220 100644 --- a/core/templates/pages/topic-editor-page/subtopic-editor/subtopic-preview-tab.component.ts +++ b/core/templates/pages/topic-editor-page/subtopic-editor/subtopic-preview-tab.component.ts @@ -16,20 +16,20 @@ * @fileoverview Component for the subtopic preview tab directive. */ -import { Component } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { SubtopicPageContents } from 'domain/topic/subtopic-page-contents.model'; -import { SubtopicPage } from 'domain/topic/subtopic-page.model'; -import { Subtopic } from 'domain/topic/subtopic.model'; -import { Topic } from 'domain/topic/topic-object.model'; -import { Subscription } from 'rxjs'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { TopicEditorRoutingService } from '../services/topic-editor-routing.service'; -import { TopicEditorStateService } from '../services/topic-editor-state.service'; +import {Component} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {SubtopicPageContents} from 'domain/topic/subtopic-page-contents.model'; +import {SubtopicPage} from 'domain/topic/subtopic-page.model'; +import {Subtopic} from 'domain/topic/subtopic.model'; +import {Topic} from 'domain/topic/topic-object.model'; +import {Subscription} from 'rxjs'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {TopicEditorRoutingService} from '../services/topic-editor-routing.service'; +import {TopicEditorStateService} from '../services/topic-editor-state.service'; @Component({ selector: 'oppia-subtopic-preview-tab', - templateUrl: './subtopic-preview-tab.component.html' + templateUrl: './subtopic-preview-tab.component.html', }) export class SubtopicPreviewTab { directiveSubscriptions = new Subscription(); @@ -60,21 +60,18 @@ export class SubtopicPreviewTab { private _initEditor(): void { this.topic = this.topicEditorStateService.getTopic(); - this.subtopicId = ( - this.topicEditorRoutingService.getSubtopicIdFromUrl()); - this.subtopic = ( - this.topic.getSubtopicById(this.subtopicId)); + this.subtopicId = this.topicEditorRoutingService.getSubtopicIdFromUrl(); + this.subtopic = this.topic.getSubtopicById(this.subtopicId); if (this.topic.getId() && this.subtopic) { this.topicEditorStateService.loadSubtopicPage( - this.topic.getId(), this.subtopicId); + this.topic.getId(), + this.subtopicId + ); this.editableTitle = this.subtopic.getTitle(); - this.editableThumbnailFilename = ( - this.subtopic.getThumbnailFilename()); - this.editableThumbnailBgColor = ( - this.subtopic.getThumbnailBgColor()); - this.subtopicPage = ( - this.topicEditorStateService.getSubtopicPage()); + this.editableThumbnailFilename = this.subtopic.getThumbnailFilename(); + this.editableThumbnailBgColor = this.subtopic.getThumbnailBgColor(); + this.subtopicPage = this.topicEditorStateService.getSubtopicPage(); this.pageContents = this.subtopicPage.getPageContents(); if (this.pageContents) { this.htmlData = this.pageContents.getHtml(); @@ -84,7 +81,8 @@ export class SubtopicPreviewTab { navigateToSubtopic(): void { this.topicEditorRoutingService.navigateToSubtopicEditorWithId( - this.subtopicId); + this.subtopicId + ); } ngOnInit(): void { @@ -97,14 +95,16 @@ export class SubtopicPreviewTab { ); this.directiveSubscriptions.add( - this.topicEditorStateService.onTopicInitialized.subscribe( - () => this._initEditor() - )); + this.topicEditorStateService.onTopicInitialized.subscribe(() => + this._initEditor() + ) + ); this.directiveSubscriptions.add( - this.topicEditorStateService.onTopicReinitialized.subscribe( - () => this._initEditor() - )); + this.topicEditorStateService.onTopicReinitialized.subscribe(() => + this._initEditor() + ) + ); this.thumbnailIsShown = !this.windowDimensionsService.isWindowNarrow(); this._initEditor(); @@ -115,7 +115,9 @@ export class SubtopicPreviewTab { } } -angular.module('oppia').directive('oppiaSubtopicPreviewTab', +angular.module('oppia').directive( + 'oppiaSubtopicPreviewTab', downgradeComponent({ - component: SubtopicPreviewTab - }) as angular.IDirectiveFactory); + component: SubtopicPreviewTab, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/topic-editor-page/topic-editor-page.component.spec.ts b/core/templates/pages/topic-editor-page/topic-editor-page.component.spec.ts index 94275c43138c..189718259846 100644 --- a/core/templates/pages/topic-editor-page/topic-editor-page.component.spec.ts +++ b/core/templates/pages/topic-editor-page/topic-editor-page.component.spec.ts @@ -16,21 +16,21 @@ * @fileoverview Unit tests for topic editor page component. */ -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { Subtopic } from 'domain/topic/subtopic.model'; -import { ShortSkillSummary } from 'domain/skill/short-skill-summary.model'; -import { StoryReference } from 'domain/topic/story-reference-object.model'; -import { Topic } from 'domain/topic/topic-object.model'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { TopicEditorRoutingService } from './services/topic-editor-routing.service'; -import { TopicEditorStateService } from './services/topic-editor-state.service'; -import { TopicEditorPageComponent } from './topic-editor-page.component'; -import { UndoRedoService } from 'domain/editor/undo_redo/undo-redo.service'; -import { ContextService } from 'services/context.service'; -import { UrlService } from 'services/contextual/url.service'; -import { PageTitleService } from 'services/page-title.service'; -import { PreventPageUnloadEventService } from 'services/prevent-page-unload-event.service'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import {Subtopic} from 'domain/topic/subtopic.model'; +import {ShortSkillSummary} from 'domain/skill/short-skill-summary.model'; +import {StoryReference} from 'domain/topic/story-reference-object.model'; +import {Topic} from 'domain/topic/topic-object.model'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {TopicEditorRoutingService} from './services/topic-editor-routing.service'; +import {TopicEditorStateService} from './services/topic-editor-state.service'; +import {TopicEditorPageComponent} from './topic-editor-page.component'; +import {UndoRedoService} from 'domain/editor/undo_redo/undo-redo.service'; +import {ContextService} from 'services/context.service'; +import {UrlService} from 'services/contextual/url.service'; +import {PageTitleService} from 'services/page-title.service'; +import {PreventPageUnloadEventService} from 'services/prevent-page-unload-event.service'; class MockContextService { getExplorationId() { @@ -59,12 +59,8 @@ describe('Topic editor page', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - ], - declarations: [ - TopicEditorPageComponent - ], + imports: [HttpClientTestingModule], + declarations: [TopicEditorPageComponent], providers: [ PageTitleService, PreventPageUnloadEventService, @@ -74,10 +70,10 @@ describe('Topic editor page', () => { UrlService, { provide: ContextService, - useClass: MockContextService - } + useClass: MockContextService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -88,20 +84,36 @@ describe('Topic editor page', () => { undoRedoService = TestBed.inject(UndoRedoService); pageTitleService = TestBed.inject(PageTitleService); preventPageUnloadEventService = TestBed.inject( - PreventPageUnloadEventService); + PreventPageUnloadEventService + ); topicEditorRoutingService = TestBed.inject(TopicEditorRoutingService); topicEditorStateService = TestBed.inject(TopicEditorStateService); urlService = TestBed.inject(UrlService); let subtopic = Subtopic.createFromTitle(1, 'subtopic1'); subtopic._thumbnailFilename = 'b.svg'; - let skillSummary = ShortSkillSummary.create( - 'skill1', 'Addition'); + let skillSummary = ShortSkillSummary.create('skill1', 'Addition'); subtopic._skillSummaries = [skillSummary]; topic = new Topic( - 'id', 'Topic name loading', 'Abbrev. name loading', - 'Url Fragment loading', 'Topic description loading', 'en', - [], [], [], 1, 1, [], 'str', '', {}, false, '', '', [] + 'id', + 'Topic name loading', + 'Abbrev. name loading', + 'Url Fragment loading', + 'Topic description loading', + 'en', + [], + [], + [], + 1, + 1, + [], + 'str', + '', + {}, + false, + '', + '', + [] ); topic._subtopics = [subtopic]; topic._thumbnailFilename = 'a.svg'; @@ -118,42 +130,50 @@ describe('Topic editor page', () => { spyOn(topicEditorStateService, 'getTopic').and.returnValue(topic); }); - it('should load topic based on its id on url when component is initialized' + - ' and set page title', () => { - let topicInitializedEventEmitter = new EventEmitter(); - let topicReinitializedEventEmitter = new EventEmitter(); - let undoRedoChangeEventEmitter = new EventEmitter(); - let topicUpdateViewEmitter = new EventEmitter(); - - spyOn(topicEditorStateService, 'loadTopic').and.callFake(() => { - topicInitializedEventEmitter.emit(); - topicReinitializedEventEmitter.emit(); - undoRedoChangeEventEmitter.emit(); - topicUpdateViewEmitter.emit(); - }); - spyOnProperty( - topicEditorStateService, 'onTopicInitialized').and.returnValue( - topicInitializedEventEmitter); - spyOnProperty( - topicEditorStateService, 'onTopicReinitialized').and.returnValue( - topicReinitializedEventEmitter); - spyOnProperty(topicEditorRoutingService, 'updateViewEventEmitter') - .and.returnValue(topicUpdateViewEmitter); - spyOn(urlService, 'getTopicIdFromUrl').and.returnValue('topic_1'); - spyOn(pageTitleService, 'setDocumentTitle').and.callThrough(); - - component.ngOnInit(); - - expect(topicEditorStateService.loadTopic).toHaveBeenCalledWith('topic_1'); - expect(pageTitleService.setDocumentTitle).toHaveBeenCalledTimes(2); - - component.ngOnDestroy(); - }); + it( + 'should load topic based on its id on url when component is initialized' + + ' and set page title', + () => { + let topicInitializedEventEmitter = new EventEmitter(); + let topicReinitializedEventEmitter = new EventEmitter(); + let undoRedoChangeEventEmitter = new EventEmitter(); + let topicUpdateViewEmitter = new EventEmitter(); + + spyOn(topicEditorStateService, 'loadTopic').and.callFake(() => { + topicInitializedEventEmitter.emit(); + topicReinitializedEventEmitter.emit(); + undoRedoChangeEventEmitter.emit(); + topicUpdateViewEmitter.emit(); + }); + spyOnProperty( + topicEditorStateService, + 'onTopicInitialized' + ).and.returnValue(topicInitializedEventEmitter); + spyOnProperty( + topicEditorStateService, + 'onTopicReinitialized' + ).and.returnValue(topicReinitializedEventEmitter); + spyOnProperty( + topicEditorRoutingService, + 'updateViewEventEmitter' + ).and.returnValue(topicUpdateViewEmitter); + spyOn(urlService, 'getTopicIdFromUrl').and.returnValue('topic_1'); + spyOn(pageTitleService, 'setDocumentTitle').and.callThrough(); + + component.ngOnInit(); + + expect(topicEditorStateService.loadTopic).toHaveBeenCalledWith('topic_1'); + expect(pageTitleService.setDocumentTitle).toHaveBeenCalledTimes(2); + + component.ngOnDestroy(); + } + ); it('should get active tab name', () => { component.selectQuestionsTab(); spyOn(topicEditorRoutingService, 'getActiveTabName').and.returnValue( - 'questions'); + 'questions' + ); expect(component.getActiveTabName()).toBe('questions'); expect(component.isInTopicEditorTabs()).toBe(true); expect(component.isInPreviewTab()).toBe(false); @@ -164,19 +184,24 @@ describe('Topic editor page', () => { expect(component.warningsAreShown).toBe(false); }); - it('should addListener by passing getChangeCount to ' + - 'PreventPageUnloadEventService', () => { - spyOn(urlService, 'getTopicIdFromUrl').and.returnValue('topic_1'); - spyOn(pageTitleService, 'setDocumentTitle').and.callThrough(); - spyOn(undoRedoService, 'getChangeCount').and.returnValue(10); - spyOn(preventPageUnloadEventService, 'addListener').and - .callFake((callback) => callback()); + it( + 'should addListener by passing getChangeCount to ' + + 'PreventPageUnloadEventService', + () => { + spyOn(urlService, 'getTopicIdFromUrl').and.returnValue('topic_1'); + spyOn(pageTitleService, 'setDocumentTitle').and.callThrough(); + spyOn(undoRedoService, 'getChangeCount').and.returnValue(10); + spyOn(preventPageUnloadEventService, 'addListener').and.callFake( + callback => callback() + ); - component.ngOnInit(); + component.ngOnInit(); - expect(preventPageUnloadEventService.addListener) - .toHaveBeenCalledWith(jasmine.any(Function)); - }); + expect(preventPageUnloadEventService.addListener).toHaveBeenCalledWith( + jasmine.any(Function) + ); + } + ); it('should return the change count', () => { spyOn(undoRedoService, 'getChangeCount').and.returnValue(10); @@ -187,67 +212,83 @@ describe('Topic editor page', () => { expect(component.getEntityType()).toBe('exploration'); }); - it('should open subtopic preview tab if active tab is subtopic editor', - () => { - spyOn(topicEditorRoutingService, 'getActiveTabName').and.returnValue( - 'subtopic_editor'); - const topicPreviewSpy = spyOn( - topicEditorRoutingService, 'navigateToSubtopicPreviewTab'); - component.openTopicViewer(); - expect(topicPreviewSpy).toHaveBeenCalled(); - }); + it('should open subtopic preview tab if active tab is subtopic editor', () => { + spyOn(topicEditorRoutingService, 'getActiveTabName').and.returnValue( + 'subtopic_editor' + ); + const topicPreviewSpy = spyOn( + topicEditorRoutingService, + 'navigateToSubtopicPreviewTab' + ); + component.openTopicViewer(); + expect(topicPreviewSpy).toHaveBeenCalled(); + }); it('should open topic preview if active tab is topic editor', () => { spyOn(topicEditorRoutingService, 'getActiveTabName').and.returnValue( - 'topic_editor'); + 'topic_editor' + ); const topicPreviewSpy = spyOn( - topicEditorRoutingService, 'navigateToTopicPreviewTab'); + topicEditorRoutingService, + 'navigateToTopicPreviewTab' + ); component.openTopicViewer(); expect(topicPreviewSpy).toHaveBeenCalled(); }); - it('should open subtopic preview tab if active tab is subtopic editor', - () => { - spyOn(topicEditorRoutingService, 'getActiveTabName').and.returnValue( - 'subtopic_editor'); - const topicPreviewSpy = spyOn( - topicEditorRoutingService, 'navigateToSubtopicPreviewTab'); - component.openTopicViewer(); - expect(topicPreviewSpy).toHaveBeenCalled(); - }); + it('should open subtopic preview tab if active tab is subtopic editor', () => { + spyOn(topicEditorRoutingService, 'getActiveTabName').and.returnValue( + 'subtopic_editor' + ); + const topicPreviewSpy = spyOn( + topicEditorRoutingService, + 'navigateToSubtopicPreviewTab' + ); + component.openTopicViewer(); + expect(topicPreviewSpy).toHaveBeenCalled(); + }); it('should navigate to topic editor tab in topic editor', () => { spyOn(topicEditorRoutingService, 'getActiveTabName').and.returnValue( - 'topic_preview'); + 'topic_preview' + ); const topicPreviewSpy = spyOn( - topicEditorRoutingService, 'navigateToMainTab'); + topicEditorRoutingService, + 'navigateToMainTab' + ); component.selectMainTab(); expect(topicPreviewSpy).toHaveBeenCalled(); }); - it('should select navigate to the subtopic editor tab in subtopic editor', - () => { - spyOn(topicEditorRoutingService, 'getActiveTabName').and.returnValue( - 'subtopic_preview'); - const topicPreviewSpy = spyOn( - topicEditorRoutingService, 'navigateToSubtopicEditorWithId'); - component.selectMainTab(); - expect(topicPreviewSpy).toHaveBeenCalled(); - }); + it('should select navigate to the subtopic editor tab in subtopic editor', () => { + spyOn(topicEditorRoutingService, 'getActiveTabName').and.returnValue( + 'subtopic_preview' + ); + const topicPreviewSpy = spyOn( + topicEditorRoutingService, + 'navigateToSubtopicEditorWithId' + ); + component.selectMainTab(); + expect(topicPreviewSpy).toHaveBeenCalled(); + }); it('should validate the topic and return validation issues', () => { component.topic = topic; + spyOn(topicEditorStateService, 'getTopicWithNameExists').and.returnValue( + true + ); spyOn( - topicEditorStateService, 'getTopicWithNameExists').and.returnValue(true); - spyOn( - topicEditorStateService, 'getTopicWithUrlFragmentExists').and.returnValue( - true); + topicEditorStateService, + 'getTopicWithUrlFragmentExists' + ).and.returnValue(true); component._validateTopic(); expect(component.validationIssues.length).toEqual(2); expect(component.validationIssues[0]).toEqual( - 'A topic with this name already exists.'); + 'A topic with this name already exists.' + ); expect(component.validationIssues[1]).toEqual( - 'Topic URL fragment already exists.'); + 'Topic URL fragment already exists.' + ); expect(component.getWarningsCount()).toEqual(2); expect(component.getTotalWarningsCount()).toEqual(2); }); @@ -255,8 +296,9 @@ describe('Topic editor page', () => { it('should return the navbar text', () => { component.selectQuestionsTab(); let routingSpy = spyOn( - topicEditorRoutingService, 'getActiveTabName').and.returnValue( - 'questions'); + topicEditorRoutingService, + 'getActiveTabName' + ).and.returnValue('questions'); expect(component.getNavbarText()).toBe('Question Editor'); routingSpy.and.returnValue('subtopic_editor'); expect(component.getNavbarText()).toEqual('Subtopic Editor'); @@ -268,20 +310,25 @@ describe('Topic editor page', () => { expect(component.getNavbarText()).toEqual('Topic Editor'); }); - it('should load topic based on its id on url when undo or redo action' + - ' is performed', () => { - let mockUndoRedoChangeEventEmitter = new EventEmitter(); - spyOn(undoRedoService, 'getUndoRedoChangeEventEmitter') - .and.returnValue(mockUndoRedoChangeEventEmitter); - spyOn(pageTitleService, 'setDocumentTitle').and.callThrough(); - spyOn(urlService, 'getTopicIdFromUrl').and.returnValue('topic_1'); - component.ngOnInit(); - mockUndoRedoChangeEventEmitter.emit(); - - expect(pageTitleService.setDocumentTitle) - .toHaveBeenCalledWith('New Name - Oppia'); - expect(component.topic).toEqual(topic); - - component.ngOnDestroy(); - }); + it( + 'should load topic based on its id on url when undo or redo action' + + ' is performed', + () => { + let mockUndoRedoChangeEventEmitter = new EventEmitter(); + spyOn(undoRedoService, 'getUndoRedoChangeEventEmitter').and.returnValue( + mockUndoRedoChangeEventEmitter + ); + spyOn(pageTitleService, 'setDocumentTitle').and.callThrough(); + spyOn(urlService, 'getTopicIdFromUrl').and.returnValue('topic_1'); + component.ngOnInit(); + mockUndoRedoChangeEventEmitter.emit(); + + expect(pageTitleService.setDocumentTitle).toHaveBeenCalledWith( + 'New Name - Oppia' + ); + expect(component.topic).toEqual(topic); + + component.ngOnDestroy(); + } + ); }); diff --git a/core/templates/pages/topic-editor-page/topic-editor-page.component.ts b/core/templates/pages/topic-editor-page/topic-editor-page.component.ts index ee5a635080dc..000254fa1a22 100644 --- a/core/templates/pages/topic-editor-page/topic-editor-page.component.ts +++ b/core/templates/pages/topic-editor-page/topic-editor-page.component.ts @@ -16,24 +16,24 @@ * @fileoverview Component for the topic editor page. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { UndoRedoService } from 'domain/editor/undo_redo/undo-redo.service'; -import { Topic } from 'domain/topic/topic-object.model'; -import { TopicRights } from 'domain/topic/topic-rights.model'; -import { Subscription } from 'rxjs'; -import { BottomNavbarStatusService } from 'services/bottom-navbar-status.service'; -import { ContextService } from 'services/context.service'; -import { UrlService } from 'services/contextual/url.service'; -import { LoaderService } from 'services/loader.service'; -import { PageTitleService } from 'services/page-title.service'; -import { PreventPageUnloadEventService } from 'services/prevent-page-unload-event.service'; -import { TopicEditorRoutingService } from './services/topic-editor-routing.service'; -import { TopicEditorStateService } from './services/topic-editor-state.service'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {UndoRedoService} from 'domain/editor/undo_redo/undo-redo.service'; +import {Topic} from 'domain/topic/topic-object.model'; +import {TopicRights} from 'domain/topic/topic-rights.model'; +import {Subscription} from 'rxjs'; +import {BottomNavbarStatusService} from 'services/bottom-navbar-status.service'; +import {ContextService} from 'services/context.service'; +import {UrlService} from 'services/contextual/url.service'; +import {LoaderService} from 'services/loader.service'; +import {PageTitleService} from 'services/page-title.service'; +import {PreventPageUnloadEventService} from 'services/prevent-page-unload-event.service'; +import {TopicEditorRoutingService} from './services/topic-editor-routing.service'; +import {TopicEditorStateService} from './services/topic-editor-state.service'; @Component({ selector: './oppia-topic-editor-page', - templateUrl: './topic-editor-page.component.html' + templateUrl: './topic-editor-page.component.html', }) export class TopicEditorPageComponent implements OnInit, OnDestroy { topic: Topic; @@ -66,8 +66,7 @@ export class TopicEditorPageComponent implements OnInit, OnDestroy { setDocumentTitle(): void { let topicName = this.topicEditorStateService.getTopic().getName(); - this.pageTitleService.setDocumentTitle( - topicName + ' - Oppia'); + this.pageTitleService.setDocumentTitle(topicName + ' - Oppia'); this.pageTitleService.setNavbarSubtitleForMobileView(topicName); this.topic = this.topicEditorStateService.getTopic(); this._validateTopic(); @@ -84,35 +83,29 @@ export class TopicEditorPageComponent implements OnInit, OnDestroy { openTopicViewer(): void { let activeTab = this.topicEditorRoutingService.getActiveTabName(); - let lastSubtopicIdVisited = ( - this.topicEditorRoutingService.getLastSubtopicIdVisited()); + let lastSubtopicIdVisited = + this.topicEditorRoutingService.getLastSubtopicIdVisited(); if (!activeTab.startsWith('subtopic') && !lastSubtopicIdVisited) { this.topicEditorRoutingService.navigateToTopicPreviewTab(); } else { let subtopicId = this.topicEditorRoutingService.getSubtopicIdFromUrl(); - this.topicEditorRoutingService.navigateToSubtopicPreviewTab( - subtopicId); + this.topicEditorRoutingService.navigateToSubtopicPreviewTab(subtopicId); } } isInPreviewTab(): boolean { let activeTab = this.topicEditorRoutingService.getActiveTabName(); - return ( - activeTab === 'subtopic_preview' || - activeTab === 'topic_preview'); + return activeTab === 'subtopic_preview' || activeTab === 'topic_preview'; } selectMainTab(): void { const activeTab = this.getActiveTabName(); - const subtopicId = ( + const subtopicId = this.topicEditorRoutingService.getSubtopicIdFromUrl() || - this.topicEditorRoutingService.getLastSubtopicIdVisited()); - const lastTabVisited = ( - this.topicEditorRoutingService.getLastTabVisited()); - if (activeTab.startsWith('subtopic') || - lastTabVisited === 'subtopic') { - this.topicEditorRoutingService.navigateToSubtopicEditorWithId( - subtopicId); + this.topicEditorRoutingService.getLastSubtopicIdVisited(); + const lastTabVisited = this.topicEditorRoutingService.getLastTabVisited(); + if (activeTab.startsWith('subtopic') || lastTabVisited === 'subtopic') { + this.topicEditorRoutingService.navigateToSubtopicEditorWithId(subtopicId); return; } this.topicEditorRoutingService.navigateToMainTab(); @@ -151,21 +144,19 @@ export class TopicEditorPageComponent implements OnInit, OnDestroy { _validateTopic(): void { this.validationIssues = this.topic.validate(); if (this.topicEditorStateService.getTopicWithNameExists()) { - this.validationIssues.push( - 'A topic with this name already exists.'); + this.validationIssues.push('A topic with this name already exists.'); } if (this.topicEditorStateService.getTopicWithUrlFragmentExists()) { - this.validationIssues.push( - 'Topic URL fragment already exists.'); + this.validationIssues.push('Topic URL fragment already exists.'); } - let prepublishTopicValidationIssues = ( - this.topic.prepublishValidate()); - let subtopicPrepublishValidationIssues = ( - [].concat.apply([], this.topic.getSubtopics().map( - (subtopic) => subtopic.prepublishValidate()))); - this.prepublishValidationIssues = ( - prepublishTopicValidationIssues.concat( - subtopicPrepublishValidationIssues)); + let prepublishTopicValidationIssues = this.topic.prepublishValidate(); + let subtopicPrepublishValidationIssues = [].concat.apply( + [], + this.topic.getSubtopics().map(subtopic => subtopic.prepublishValidate()) + ); + this.prepublishValidationIssues = prepublishTopicValidationIssues.concat( + subtopicPrepublishValidationIssues + ); } getWarningsCount(): number { @@ -174,39 +165,38 @@ export class TopicEditorPageComponent implements OnInit, OnDestroy { getTotalWarningsCount(): number { let validationIssuesCount = this.validationIssues.length; - let prepublishValidationIssuesCount = ( - this.prepublishValidationIssues.length); + let prepublishValidationIssuesCount = + this.prepublishValidationIssues.length; return validationIssuesCount + prepublishValidationIssuesCount; } ngOnInit(): void { this.loaderService.showLoadingScreen('Loading Topic'); this.directiveSubscriptions.add( - this.topicEditorStateService.onTopicInitialized.subscribe( - () => { - this.loaderService.hideLoadingScreen(); - this.setDocumentTitle(); - } - )); + this.topicEditorStateService.onTopicInitialized.subscribe(() => { + this.loaderService.hideLoadingScreen(); + this.setDocumentTitle(); + }) + ); this.directiveSubscriptions.add( - this.topicEditorStateService.onTopicReinitialized.subscribe( - () => { - this.setDocumentTitle(); - } - )); + this.topicEditorStateService.onTopicReinitialized.subscribe(() => { + this.setDocumentTitle(); + }) + ); this.topicEditorStateService.loadTopic(this.urlService.getTopicIdFromUrl()); this.pageTitleService.setNavbarTitleForMobileView('Topic Editor'); this.preventPageUnloadEventService.addListener( - this.undoRedoService.getChangeCount.bind(this.undoRedoService)); + this.undoRedoService.getChangeCount.bind(this.undoRedoService) + ); this.validationIssues = []; this.prepublishValidationIssues = []; this.warningsAreShown = false; this.bottomNavbarStatusService.markBottomNavbarStatus(true); this.topicRights = this.topicEditorStateService.getTopicRights(); this.directiveSubscriptions.add( - this.undoRedoService.getUndoRedoChangeEventEmitter().subscribe( - () => this.setDocumentTitle() - ) + this.undoRedoService + .getUndoRedoChangeEventEmitter() + .subscribe(() => this.setDocumentTitle()) ); } @@ -215,7 +205,9 @@ export class TopicEditorPageComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive('oppiaTopicEditorPage', +angular.module('oppia').directive( + 'oppiaTopicEditorPage', downgradeComponent({ - component: TopicEditorPageComponent - }) as angular.IDirectiveFactory); + component: TopicEditorPageComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/topic-editor-page/topic-editor-page.constants.ajs.ts b/core/templates/pages/topic-editor-page/topic-editor-page.constants.ajs.ts index 5695dee5c83f..3f2e748b9a38 100644 --- a/core/templates/pages/topic-editor-page/topic-editor-page.constants.ajs.ts +++ b/core/templates/pages/topic-editor-page/topic-editor-page.constants.ajs.ts @@ -18,9 +18,11 @@ // TODO(#7092): Delete this file once migration is complete and these AngularJS // equivalents of the Angular constants are no longer needed. -import { TopicEditorPageConstants } from - 'pages/topic-editor-page/topic-editor-page.constants'; +import {TopicEditorPageConstants} from 'pages/topic-editor-page/topic-editor-page.constants'; -angular.module('oppia').constant( - 'TOPIC_NAME_INPUT_FOCUS_LABEL', - TopicEditorPageConstants.TOPIC_NAME_INPUT_FOCUS_LABEL); +angular + .module('oppia') + .constant( + 'TOPIC_NAME_INPUT_FOCUS_LABEL', + TopicEditorPageConstants.TOPIC_NAME_INPUT_FOCUS_LABEL + ); diff --git a/core/templates/pages/topic-editor-page/topic-editor-page.constants.ts b/core/templates/pages/topic-editor-page/topic-editor-page.constants.ts index 3547e0a70789..76716b994a9a 100644 --- a/core/templates/pages/topic-editor-page/topic-editor-page.constants.ts +++ b/core/templates/pages/topic-editor-page/topic-editor-page.constants.ts @@ -17,5 +17,5 @@ */ export const TopicEditorPageConstants = { - TOPIC_NAME_INPUT_FOCUS_LABEL: 'topicNameInputFocusLabel' + TOPIC_NAME_INPUT_FOCUS_LABEL: 'topicNameInputFocusLabel', } as const; diff --git a/core/templates/pages/topic-editor-page/topic-editor-page.import.ts b/core/templates/pages/topic-editor-page/topic-editor-page.import.ts index 2b331ba4e44e..0b231904dccc 100644 --- a/core/templates/pages/topic-editor-page/topic-editor-page.import.ts +++ b/core/templates/pages/topic-editor-page/topic-editor-page.import.ts @@ -25,10 +25,16 @@ import 'third-party-imports/ui-codemirror.import'; import 'third-party-imports/ui-tree.import'; angular.module('oppia', [ - require('angular-cookies'), 'dndLists', 'ngAnimate', - 'ngMaterial', 'ngSanitize', 'ngTouch', 'pascalprecht.translate', - 'ui.bootstrap', 'ui.tree', - uiValidate + require('angular-cookies'), + 'dndLists', + 'ngAnimate', + 'ngMaterial', + 'ngSanitize', + 'ngTouch', + 'pascalprecht.translate', + 'ui.bootstrap', + 'ui.tree', + uiValidate, ]); require('Polyfills.ts'); @@ -39,7 +45,6 @@ require('pages/topic-editor-page/topic-editor-page.module.ts'); require('App.ts'); require('base-components/oppia-root.directive.ts'); -require( - 'pages/topic-editor-page/navbar/topic-editor-navbar-breadcrumb.component.ts'); +require('pages/topic-editor-page/navbar/topic-editor-navbar-breadcrumb.component.ts'); require('pages/topic-editor-page/navbar/topic-editor-navbar.component.ts'); require('pages/topic-editor-page/topic-editor-page.component.ts'); diff --git a/core/templates/pages/topic-editor-page/topic-editor-page.module.ts b/core/templates/pages/topic-editor-page/topic-editor-page.module.ts index d4baa4ed7bb2..2f96d13e52c4 100644 --- a/core/templates/pages/topic-editor-page/topic-editor-page.module.ts +++ b/core/templates/pages/topic-editor-page/topic-editor-page.module.ts @@ -16,40 +16,41 @@ * @fileoverview Module for the story viewer page. */ -import { APP_INITIALIZER, NgModule, StaticProvider } from '@angular/core'; -import { BrowserModule, HAMMER_GESTURE_CONFIG } from '@angular/platform-browser'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { HttpClientModule } from '@angular/common/http'; -import { HTTP_INTERCEPTORS } from '@angular/common/http'; -import { InteractionExtensionsModule } from 'interactions/interactions.module'; -import { RequestInterceptor } from 'services/request-interceptor.service'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { OppiaAngularRootComponent } from - 'components/oppia-angular-root.component'; -import { platformFeatureInitFactory, PlatformFeatureService } from - 'services/platform-feature.service'; -import { RouterModule } from '@angular/router'; -import { APP_BASE_HREF } from '@angular/common'; -import { SubtopicPreviewTab } from './subtopic-editor/subtopic-preview-tab.component'; -import { ChangeSubtopicAssignmentModalComponent } from './modal-templates/change-subtopic-assignment-modal.component'; -import { TopicPreviewTabComponent } from './preview-tab/topic-preview-tab.component'; -import { TopicEditorNavbarBreadcrumbComponent } from './navbar/topic-editor-navbar-breadcrumb.component'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { MyHammerConfig, toastrConfig } from 'pages/oppia-root/app.module'; -import { CreateNewSubtopicModalComponent } from 'pages/topic-editor-page/modal-templates/create-new-subtopic-modal.component'; -import { DeleteStoryModalComponent } from './modal-templates/delete-story-modal.component'; -import { TopicEditorSendMailComponent } from './modal-templates/topic-editor-send-mail-modal.component'; -import { TopicEditorSaveModalComponent } from './modal-templates/topic-editor-save-modal.component'; -import { SmartRouterModule } from 'hybrid-router-module-provider'; -import { AppErrorHandlerProvider } from 'pages/oppia-root/app-error-handler'; -import { TopicEditorNavbarComponent } from './navbar/topic-editor-navbar.component'; -import { TopicQuestionsTabComponent } from './questions-tab/topic-questions-tab.component'; -import { RearrangeSkillsInSubtopicsModalComponent } from './modal-templates/rearrange-skills-in-subtopics-modal.component'; -import { CreateNewStoryModalComponent } from './modal-templates/create-new-story-modal.component'; -import { TopicEditorStoriesListComponent } from './editor-tab/topic-editor-stories-list.component'; -import { TopicEditorTabComponent } from './editor-tab/topic-editor-tab.directive'; -import { TopicEditorPageComponent } from './topic-editor-page.component'; -import { SubtopicEditorTabComponent } from './subtopic-editor/subtopic-editor-tab.component'; +import {APP_INITIALIZER, NgModule, StaticProvider} from '@angular/core'; +import {BrowserModule, HAMMER_GESTURE_CONFIG} from '@angular/platform-browser'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {HttpClientModule} from '@angular/common/http'; +import {HTTP_INTERCEPTORS} from '@angular/common/http'; +import {InteractionExtensionsModule} from 'interactions/interactions.module'; +import {RequestInterceptor} from 'services/request-interceptor.service'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {OppiaAngularRootComponent} from 'components/oppia-angular-root.component'; +import { + platformFeatureInitFactory, + PlatformFeatureService, +} from 'services/platform-feature.service'; +import {RouterModule} from '@angular/router'; +import {APP_BASE_HREF} from '@angular/common'; +import {SubtopicPreviewTab} from './subtopic-editor/subtopic-preview-tab.component'; +import {ChangeSubtopicAssignmentModalComponent} from './modal-templates/change-subtopic-assignment-modal.component'; +import {TopicPreviewTabComponent} from './preview-tab/topic-preview-tab.component'; +import {TopicEditorNavbarBreadcrumbComponent} from './navbar/topic-editor-navbar-breadcrumb.component'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {MyHammerConfig, toastrConfig} from 'pages/oppia-root/app.module'; +import {CreateNewSubtopicModalComponent} from 'pages/topic-editor-page/modal-templates/create-new-subtopic-modal.component'; +import {DeleteStoryModalComponent} from './modal-templates/delete-story-modal.component'; +import {TopicEditorSendMailComponent} from './modal-templates/topic-editor-send-mail-modal.component'; +import {TopicEditorSaveModalComponent} from './modal-templates/topic-editor-save-modal.component'; +import {SmartRouterModule} from 'hybrid-router-module-provider'; +import {AppErrorHandlerProvider} from 'pages/oppia-root/app-error-handler'; +import {TopicEditorNavbarComponent} from './navbar/topic-editor-navbar.component'; +import {TopicQuestionsTabComponent} from './questions-tab/topic-questions-tab.component'; +import {RearrangeSkillsInSubtopicsModalComponent} from './modal-templates/rearrange-skills-in-subtopics-modal.component'; +import {CreateNewStoryModalComponent} from './modal-templates/create-new-story-modal.component'; +import {TopicEditorStoriesListComponent} from './editor-tab/topic-editor-stories-list.component'; +import {TopicEditorTabComponent} from './editor-tab/topic-editor-tab.directive'; +import {TopicEditorPageComponent} from './topic-editor-page.component'; +import {SubtopicEditorTabComponent} from './subtopic-editor/subtopic-editor-tab.component'; @NgModule({ imports: [ @@ -63,7 +64,7 @@ import { SubtopicEditorTabComponent } from './subtopic-editor/subtopic-editor-ta InteractionExtensionsModule, SharedComponentsModule, TopicPlayerViewerCommonModule, - ToastrModule.forRoot(toastrConfig) + ToastrModule.forRoot(toastrConfig), ], declarations: [ ChangeSubtopicAssignmentModalComponent, @@ -81,7 +82,7 @@ import { SubtopicEditorTabComponent } from './subtopic-editor/subtopic-editor-ta TopicEditorStoriesListComponent, TopicEditorTabComponent, TopicEditorPageComponent, - SubtopicEditorTabComponent + SubtopicEditorTabComponent, ], entryComponents: [ ChangeSubtopicAssignmentModalComponent, @@ -99,42 +100,42 @@ import { SubtopicEditorTabComponent } from './subtopic-editor/subtopic-editor-ta TopicEditorStoriesListComponent, TopicEditorTabComponent, TopicEditorPageComponent, - SubtopicEditorTabComponent + SubtopicEditorTabComponent, ], providers: [ { provide: HTTP_INTERCEPTORS, useClass: RequestInterceptor, - multi: true + multi: true, }, { provide: APP_INITIALIZER, useFactory: platformFeatureInitFactory, deps: [PlatformFeatureService], - multi: true + multi: true, }, { provide: HAMMER_GESTURE_CONFIG, - useClass: MyHammerConfig + useClass: MyHammerConfig, }, AppErrorHandlerProvider, { provide: APP_BASE_HREF, - useValue: '/' - } - ] + useValue: '/', + }, + ], }) class TopicEditorPageModule { // Empty placeholder method to satisfy the `Compiler`. ngDoBootstrap() {} } -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { downgradeModule } from '@angular/upgrade/static'; -import { ToastrModule } from 'ngx-toastr'; -import { TopicPlayerViewerCommonModule } from 'pages/topic-viewer-page/topic-viewer-player-common.module'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {downgradeModule} from '@angular/upgrade/static'; +import {ToastrModule} from 'ngx-toastr'; +import {TopicPlayerViewerCommonModule} from 'pages/topic-viewer-page/topic-viewer-player-common.module'; -const bootstrapFnAsync = async(extraProviders: StaticProvider[]) => { +const bootstrapFnAsync = async (extraProviders: StaticProvider[]) => { const platformRef = platformBrowserDynamic(extraProviders); return platformRef.bootstrapModule(TopicEditorPageModule); }; @@ -149,5 +150,6 @@ angular.module('oppia').directive( // bootstrap the Angular 8. 'oppiaAngularRoot', downgradeComponent({ - component: OppiaAngularRootComponent - }) as angular.IDirectiveFactory); + component: OppiaAngularRootComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/topic-viewer-page/modals/practice-session-confirmation-modal.component.spec.ts b/core/templates/pages/topic-viewer-page/modals/practice-session-confirmation-modal.component.spec.ts index 722c380e69b2..849cc6b969a5 100644 --- a/core/templates/pages/topic-viewer-page/modals/practice-session-confirmation-modal.component.spec.ts +++ b/core/templates/pages/topic-viewer-page/modals/practice-session-confirmation-modal.component.spec.ts @@ -16,19 +16,19 @@ * @fileoverview Unit tests for practice session confirmation modal component. */ -import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; -import { PracticeSessionConfirmationModal } from './practice-session-confirmation-modal.component'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; +import {ComponentFixture, waitForAsync, TestBed} from '@angular/core/testing'; +import {PracticeSessionConfirmationModal} from './practice-session-confirmation-modal.component'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; -describe('Practice session confirmation modal component', function() { +describe('Practice session confirmation modal component', function () { let component: PracticeSessionConfirmationModal; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [PracticeSessionConfirmationModal, MockTranslatePipe], - providers: [NgbActiveModal] + providers: [NgbActiveModal], }).compileComponents(); })); diff --git a/core/templates/pages/topic-viewer-page/modals/practice-session-confirmation-modal.component.ts b/core/templates/pages/topic-viewer-page/modals/practice-session-confirmation-modal.component.ts index d5a290fccc8a..489e0fabec48 100644 --- a/core/templates/pages/topic-viewer-page/modals/practice-session-confirmation-modal.component.ts +++ b/core/templates/pages/topic-viewer-page/modals/practice-session-confirmation-modal.component.ts @@ -16,19 +16,16 @@ * @fileoverview Component for practice session confirmation modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-practice-session-confirmation-modal', - templateUrl: './practice-session-confirmation-modal.component.html' + templateUrl: './practice-session-confirmation-modal.component.html', }) - export class PracticeSessionConfirmationModal extends ConfirmOrCancelModal { - constructor( - private ngbActiveModal: NgbActiveModal - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } } diff --git a/core/templates/pages/topic-viewer-page/navbar-breadcrumb/topic-viewer-navbar-breadcrumb.component.spec.ts b/core/templates/pages/topic-viewer-page/navbar-breadcrumb/topic-viewer-navbar-breadcrumb.component.spec.ts index 7cad45bce8d0..6bc2aef0e61c 100644 --- a/core/templates/pages/topic-viewer-page/navbar-breadcrumb/topic-viewer-navbar-breadcrumb.component.spec.ts +++ b/core/templates/pages/topic-viewer-page/navbar-breadcrumb/topic-viewer-navbar-breadcrumb.component.spec.ts @@ -16,16 +16,17 @@ * @fileoverview Unit tests for classroom page component. */ -import { TestBed, ComponentFixture, waitForAsync } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { UrlService } from 'services/contextual/url.service'; -import { TopicViewerBackendApiService } from - 'domain/topic_viewer/topic-viewer-backend-api.service'; -import { ReadOnlyTopicBackendDict, ReadOnlyTopicObjectFactory } from - 'domain/topic_viewer/read-only-topic-object.factory'; -import { TopicViewerNavbarBreadcrumbComponent } from 'pages/topic-viewer-page/navbar-breadcrumb/topic-viewer-navbar-breadcrumb.component'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; +import {TestBed, ComponentFixture, waitForAsync} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {UrlService} from 'services/contextual/url.service'; +import {TopicViewerBackendApiService} from 'domain/topic_viewer/topic-viewer-backend-api.service'; +import { + ReadOnlyTopicBackendDict, + ReadOnlyTopicObjectFactory, +} from 'domain/topic_viewer/read-only-topic-object.factory'; +import {TopicViewerNavbarBreadcrumbComponent} from 'pages/topic-viewer-page/navbar-breadcrumb/topic-viewer-navbar-breadcrumb.component'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; describe('Topic viewer navbar breadcrumb component', () => { let component: TopicViewerNavbarBreadcrumbComponent; @@ -38,10 +39,7 @@ describe('Topic viewer navbar breadcrumb component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [ - MockTranslatePipe, - TopicViewerNavbarBreadcrumbComponent - ], + declarations: [MockTranslatePipe, TopicViewerNavbarBreadcrumbComponent], }).compileComponents(); readOnlyTopicObjectFactory = TestBed.inject(ReadOnlyTopicObjectFactory); @@ -50,9 +48,11 @@ describe('Topic viewer navbar breadcrumb component', () => { urlService = TestBed.inject(UrlService); spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue( - 'topic1'); + 'topic1' + ); spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl').and.returnValue( - 'classroom1'); + 'classroom1' + ); spyOn(topicViewerBackendApiService, 'fetchTopicDataAsync').and.resolveTo( readOnlyTopicObjectFactory.createFromBackendDict({ @@ -68,7 +68,8 @@ describe('Topic viewer navbar breadcrumb component', () => { practice_tab_is_displayed: false, meta_tag_content: 'content', page_title_fragment_for_web: 'title', - } as ReadOnlyTopicBackendDict)); + } as ReadOnlyTopicBackendDict) + ); })); beforeEach(() => { @@ -77,31 +78,38 @@ describe('Topic viewer navbar breadcrumb component', () => { fixture.detectChanges(); }); - it('should set topic name using the data retrieved from the backend', + it('should set topic name using the data retrieved from the backend', waitForAsync(() => { + component.ngOnInit(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(component.topicName).toBe('Topic Name 1'); + }); + })); + + it( + 'should set topic name translation key and check whether hacky ' + + 'translations are displayed or not correctly', waitForAsync(() => { component.ngOnInit(); fixture.whenStable().then(() => { fixture.detectChanges(); - expect(component.topicName).toBe('Topic Name 1'); - }); - })); + expect(component.topicNameTranslationKey).toBe( + 'I18N_TOPIC_topic1_TITLE' + ); - it('should set topic name translation key and check whether hacky ' + - 'translations are displayed or not correctly', waitForAsync(() => { - component.ngOnInit(); - fixture.whenStable().then(() => { - fixture.detectChanges(); - expect(component.topicNameTranslationKey) - .toBe('I18N_TOPIC_topic1_TITLE'); - - spyOn(i18nLanguageCodeService, 'isHackyTranslationAvailable') - .and.returnValue(true); - spyOn(i18nLanguageCodeService, 'isCurrentLanguageEnglish') - .and.returnValue(false); + spyOn( + i18nLanguageCodeService, + 'isHackyTranslationAvailable' + ).and.returnValue(true); + spyOn( + i18nLanguageCodeService, + 'isCurrentLanguageEnglish' + ).and.returnValue(false); - let hackyTopicNameTranslationIsDisplayed = - component.isHackyTopicNameTranslationDisplayed(); - expect(hackyTopicNameTranslationIsDisplayed).toBe(true); - }); - })); + let hackyTopicNameTranslationIsDisplayed = + component.isHackyTopicNameTranslationDisplayed(); + expect(hackyTopicNameTranslationIsDisplayed).toBe(true); + }); + }) + ); }); diff --git a/core/templates/pages/topic-viewer-page/navbar-breadcrumb/topic-viewer-navbar-breadcrumb.component.ts b/core/templates/pages/topic-viewer-page/navbar-breadcrumb/topic-viewer-navbar-breadcrumb.component.ts index 802a474635e9..ec0db46d3ff7 100644 --- a/core/templates/pages/topic-viewer-page/navbar-breadcrumb/topic-viewer-navbar-breadcrumb.component.ts +++ b/core/templates/pages/topic-viewer-page/navbar-breadcrumb/topic-viewer-navbar-breadcrumb.component.ts @@ -16,20 +16,21 @@ * @fileoverview Component for the navbar breadcrumb of the topic viewer. */ -import { Component, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; -import { ReadOnlyTopic } from - 'domain/topic_viewer/read-only-topic-object.factory'; -import { TopicViewerBackendApiService } from - 'domain/topic_viewer/topic-viewer-backend-api.service'; -import { UrlService } from 'services/contextual/url.service'; -import { I18nLanguageCodeService, TranslationKeyType } from 'services/i18n-language-code.service'; +import {ReadOnlyTopic} from 'domain/topic_viewer/read-only-topic-object.factory'; +import {TopicViewerBackendApiService} from 'domain/topic_viewer/topic-viewer-backend-api.service'; +import {UrlService} from 'services/contextual/url.service'; +import { + I18nLanguageCodeService, + TranslationKeyType, +} from 'services/i18n-language-code.service'; @Component({ selector: 'topic-viewer-navbar-breadcrumb', templateUrl: './topic-viewer-navbar-breadcrumb.component.html', - styleUrls: [] + styleUrls: [], }) export class TopicViewerNavbarBreadcrumbComponent implements OnInit { topicName: string = ''; @@ -41,17 +42,18 @@ export class TopicViewerNavbarBreadcrumbComponent implements OnInit { ) {} ngOnInit(): void { - this.topicViewerBackendApiService.fetchTopicDataAsync( - this.urlService.getTopicUrlFragmentFromLearnerUrl(), - this.urlService.getClassroomUrlFragmentFromLearnerUrl()).then( - (readOnlyTopic: ReadOnlyTopic) => { + this.topicViewerBackendApiService + .fetchTopicDataAsync( + this.urlService.getTopicUrlFragmentFromLearnerUrl(), + this.urlService.getClassroomUrlFragmentFromLearnerUrl() + ) + .then((readOnlyTopic: ReadOnlyTopic) => { this.topicName = readOnlyTopic.getTopicName(); - this.topicNameTranslationKey = ( + this.topicNameTranslationKey = this.i18nLanguageCodeService.getTopicTranslationKey( readOnlyTopic.getTopicId(), TranslationKeyType.TITLE - ) - ); + ); }); } @@ -67,6 +69,9 @@ export class TopicViewerNavbarBreadcrumbComponent implements OnInit { return this.i18nLanguageCodeService.isCurrentLanguageRTL(); } } -angular.module('oppia').directive( - 'topicViewerNavbarBreadcrumb', downgradeComponent( - {component: TopicViewerNavbarBreadcrumbComponent})); +angular + .module('oppia') + .directive( + 'topicViewerNavbarBreadcrumb', + downgradeComponent({component: TopicViewerNavbarBreadcrumbComponent}) + ); diff --git a/core/templates/pages/topic-viewer-page/practice-tab/practice-tab.component.spec.ts b/core/templates/pages/topic-viewer-page/practice-tab/practice-tab.component.spec.ts index 7a5d30e32f58..4d8b8da196c5 100644 --- a/core/templates/pages/topic-viewer-page/practice-tab/practice-tab.component.spec.ts +++ b/core/templates/pages/topic-viewer-page/practice-tab/practice-tab.component.spec.ts @@ -16,24 +16,28 @@ * @fileoverview Unit tests for practiceTab. */ -import { TestBed, async, ComponentFixture, fakeAsync, flushMicrotasks, tick } from - '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { TranslateService } from '@ngx-translate/core'; - -import { Subtopic } from 'domain/topic/subtopic.model'; -import { PracticeTabComponent } from './practice-tab.component'; -import { QuestionBackendApiService } from - 'domain/question/question-backend-api.service'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; -import { UrlService } from 'services/contextual/url.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { LoaderService } from 'services/loader.service'; +import { + TestBed, + async, + ComponentFixture, + fakeAsync, + flushMicrotasks, + tick, +} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {TranslateService} from '@ngx-translate/core'; + +import {Subtopic} from 'domain/topic/subtopic.model'; +import {PracticeTabComponent} from './practice-tab.component'; +import {QuestionBackendApiService} from 'domain/question/question-backend-api.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {UrlService} from 'services/contextual/url.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {LoaderService} from 'services/loader.service'; class MockUrlService { getTopicUrlFragmentFromLearnerUrl() { @@ -49,9 +53,9 @@ class MockWindowRef { _window = { location: { href: '', - reload: (val: boolean) => val + reload: (val: boolean) => val, }, - gtag: () => {} + gtag: () => {}, }; get nativeWindow() { @@ -72,8 +76,7 @@ class MockTranslateService { } } - -describe('Practice tab component', function() { +describe('Practice tab component', function () { let component: PracticeTabComponent; let fixture: ComponentFixture; let windowRef: MockWindowRef; @@ -94,18 +97,18 @@ describe('Practice tab component', function() { I18nLanguageCodeService, LoaderService, UrlInterpolationService, - { provide: UrlService, useClass: MockUrlService }, - { provide: WindowRef, useValue: windowRef }, + {provide: UrlService, useClass: MockUrlService}, + {provide: WindowRef, useValue: windowRef}, { provide: QuestionBackendApiService, - useValue: questionBackendApiService + useValue: questionBackendApiService, }, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -114,33 +117,39 @@ describe('Practice tab component', function() { component = fixture.componentInstance; component.topicName = 'Topic Name'; component.subtopicsList = [ - Subtopic.create({ - id: 1, - title: 'Subtopic 1', - skill_ids: ['1', '2'], - thumbnail_filename: '', - thumbnail_bg_color: '', - url_fragment: '' - }, { - 1: 'First skill', - 2: 'Second skill' - }), - Subtopic.create({ - id: 2, - title: 'Subtopic 2', - skill_ids: [], - thumbnail_filename: '', - thumbnail_bg_color: '', - url_fragment: '' - }, { - 1: 'First skill', - 2: 'Second skill' - }) + Subtopic.create( + { + id: 1, + title: 'Subtopic 1', + skill_ids: ['1', '2'], + thumbnail_filename: '', + thumbnail_bg_color: '', + url_fragment: '', + }, + { + 1: 'First skill', + 2: 'Second skill', + } + ), + Subtopic.create( + { + id: 2, + title: 'Subtopic 2', + skill_ids: [], + thumbnail_filename: '', + thumbnail_bg_color: '', + url_fragment: '', + }, + { + 1: 'First skill', + 2: 'Second skill', + } + ), ]; component.subtopicIds = [1, 2, 3]; component.subtopicMastery = { 1: 0, - 2: 1 + 2: 1, }; fixture.detectChanges(); ngbModal = TestBed.inject(NgbModal); @@ -149,219 +158,260 @@ describe('Practice tab component', function() { translateService = TestBed.inject(TranslateService); }); - it('should initialize controller properties after its initilization', - function() { + it('should initialize controller properties after its initilization', function () { + component.ngOnInit(); + + expect(component.selectedSubtopics).toEqual([]); + expect(component.availableSubtopics.length).toBe(1); + expect(component.selectedSubtopicIndices).toEqual([false]); + }); + + it( + 'should obtain topic translation key upon initialization, and subscribe ' + + 'to the language change event emitter', + () => { + component.topicId = 'topic_id_1'; + spyOn(i18nLanguageCodeService, 'getTopicTranslationKey').and.returnValue( + 'dummy_topic_translation_key' + ); + spyOn(component, 'subscribeToOnLangChange'); + spyOn(component, 'getTranslatedTopicName'); + component.ngOnInit(); - expect(component.selectedSubtopics).toEqual([]); - expect(component.availableSubtopics.length).toBe(1); - expect(component.selectedSubtopicIndices).toEqual([false]); - }); - - it('should obtain topic translation key upon initialization, and subscribe ' + - 'to the language change event emitter', () => { - component.topicId = 'topic_id_1'; - spyOn(i18nLanguageCodeService, 'getTopicTranslationKey').and.returnValue( - 'dummy_topic_translation_key'); - spyOn(component, 'subscribeToOnLangChange'); + expect( + i18nLanguageCodeService.getTopicTranslationKey + ).toHaveBeenCalledWith('topic_id_1', 'TITLE'); + expect(component.topicNameTranslationKey).toEqual( + 'dummy_topic_translation_key' + ); + expect(component.subscribeToOnLangChange).toHaveBeenCalled(); + expect(component.getTranslatedTopicName).toHaveBeenCalled(); + } + ); + + it('should obtain translated topic name whenever selected language changes', () => { + component.subscribeToOnLangChange(); spyOn(component, 'getTranslatedTopicName'); - component.ngOnInit(); + translateService.onLangChange.emit(); - expect(i18nLanguageCodeService.getTopicTranslationKey) - .toHaveBeenCalledWith('topic_id_1', 'TITLE'); - expect(component.topicNameTranslationKey).toEqual( - 'dummy_topic_translation_key'); - expect(component.subscribeToOnLangChange).toHaveBeenCalled(); expect(component.getTranslatedTopicName).toHaveBeenCalled(); }); - it('should obtain translated topic name whenever selected language changes', + it( + 'should obtain translated topic name and set it when hacky ' + + 'translations are available', () => { - component.subscribeToOnLangChange(); - spyOn(component, 'getTranslatedTopicName'); - - translateService.onLangChange.emit(); + spyOn( + i18nLanguageCodeService, + 'isHackyTranslationAvailable' + ).and.returnValue(true); + spyOn( + i18nLanguageCodeService, + 'isCurrentLanguageEnglish' + ).and.returnValue(false); + spyOn(translateService, 'instant').and.callThrough(); + component.topicNameTranslationKey = 'dummy_translation_key'; + + component.getTranslatedTopicName(); + + expect(translateService.instant).toHaveBeenCalledWith( + 'dummy_translation_key' + ); + expect(component.translatedTopicName).toEqual('dummy_translation_key'); + } + ); + + it( + 'should not obtain translated topic name when hacky translations are ' + + 'unavailable, and use the default english topic name instead', + () => { + spyOn( + i18nLanguageCodeService, + 'isHackyTranslationAvailable' + ).and.returnValue(false); + spyOn( + i18nLanguageCodeService, + 'isCurrentLanguageEnglish' + ).and.returnValue(false); + spyOn(translateService, 'instant'); + component.topicName = 'default_topic_name'; + + component.getTranslatedTopicName(); + + expect(translateService.instant).not.toHaveBeenCalled(); + expect(component.translatedTopicName).toEqual('default_topic_name'); + } + ); + + it('should have start button enabled when a subtopic is selected', function () { + component.selectedSubtopicIndices[0] = true; + component.questionsAreAvailable = true; - expect(component.getTranslatedTopicName).toHaveBeenCalled(); - }); - - it('should obtain translated topic name and set it when hacky ' + - 'translations are available', () => { - spyOn(i18nLanguageCodeService, 'isHackyTranslationAvailable') - .and.returnValue(true); - spyOn(i18nLanguageCodeService, 'isCurrentLanguageEnglish') - .and.returnValue(false); - spyOn(translateService, 'instant').and.callThrough(); - component.topicNameTranslationKey = 'dummy_translation_key'; - - component.getTranslatedTopicName(); - - expect(translateService.instant) - .toHaveBeenCalledWith('dummy_translation_key'); - expect(component.translatedTopicName).toEqual('dummy_translation_key'); + expect(component.isStartButtonDisabled()).toBe(false); }); - it('should not obtain translated topic name when hacky translations are ' + - 'unavailable, and use the default english topic name instead', () => { - spyOn(i18nLanguageCodeService, 'isHackyTranslationAvailable') - .and.returnValue(false); - spyOn(i18nLanguageCodeService, 'isCurrentLanguageEnglish') - .and.returnValue(false); - spyOn(translateService, 'instant'); - component.topicName = 'default_topic_name'; + it('should have start button disabled when there is no subtopic selected', function () { + component.selectedSubtopicIndices[0] = false; + + expect(component.isStartButtonDisabled()).toBe(true); + }); - component.getTranslatedTopicName(); + it('should have start button disabled when the disable boolean is set', function () { + component.previewMode = true; - expect(translateService.instant).not.toHaveBeenCalled(); - expect(component.translatedTopicName).toEqual('default_topic_name'); + expect(component.isStartButtonDisabled()).toBe(true); }); - it('should have start button enabled when a subtopic is selected', - function() { + it( + 'should open a new practice session containing the selected subtopic' + + ' when start button is clicked for topicViewer display area', + function () { + spyOn(loaderService, 'showLoadingScreen'); component.selectedSubtopicIndices[0] = true; - component.questionsAreAvailable = true; - expect(component.isStartButtonDisabled()).toBe(false); - }); + component.openNewPracticeSession(); - it('should have start button disabled when there is no subtopic selected', - function() { - component.selectedSubtopicIndices[0] = false; + expect(windowRef.nativeWindow.location.href).toBe( + '/learn/classroom_1/topic_1/practice/session?' + + 'selected_subtopic_ids=%5B1%5D' + ); + expect(loaderService.showLoadingScreen).toHaveBeenCalledWith('Loading'); + } + ); - expect(component.isStartButtonDisabled()).toBe(true); - }); + it('should check if questions exist for the selected subtopics', fakeAsync(() => { + component.checkIfQuestionsExist([true]); + flushMicrotasks(); - it('should have start button disabled when the disable boolean is set', - function() { - component.previewMode = true; + expect(component.questionsAreAvailable).toBeTrue(); - expect(component.isStartButtonDisabled()).toBe(true); - }); + component.checkIfQuestionsExist([false]); + flushMicrotasks(); - it('should open a new practice session containing the selected subtopic' + - ' when start button is clicked for topicViewer display area', function() { - spyOn(loaderService, 'showLoadingScreen'); - component.selectedSubtopicIndices[0] = true; + expect(component.questionsAreAvailable).toBeFalse(); + })); - component.openNewPracticeSession(); + it( + 'should not ask learner for confirmation before starting a new practice ' + + 'session if site language is set to English', + fakeAsync(() => { + let isLanguageEnglishSpy = spyOn( + i18nLanguageCodeService, + 'isCurrentLanguageEnglish' + ).and.returnValue(true); + let ngbModalSpy = spyOn(ngbModal, 'open').and.callFake( + (modal, modalOptions) => { + return { + result: Promise.resolve(), + } as NgbModalRef; + } + ); - expect(windowRef.nativeWindow.location.href).toBe( - '/learn/classroom_1/topic_1/practice/session?' + - 'selected_subtopic_ids=%5B1%5D' - ); - expect(loaderService.showLoadingScreen).toHaveBeenCalledWith('Loading'); - }); + component.checkSiteLanguageBeforeBeginningPracticeSession(); + tick(); - it('should check if questions exist for the selected subtopics', + expect(isLanguageEnglishSpy).toHaveBeenCalled(); + expect(ngbModalSpy).not.toHaveBeenCalled(); + }) + ); + + it( + 'should ask learner for confirmation before starting a new practice ' + + 'session if site language is not set to English', fakeAsync(() => { - component.checkIfQuestionsExist([true]); - flushMicrotasks(); - - expect(component.questionsAreAvailable).toBeTrue(); - - component.checkIfQuestionsExist([false]); - flushMicrotasks(); - - expect(component.questionsAreAvailable).toBeFalse(); - })); - - it('should not ask learner for confirmation before starting a new practice ' + - 'session if site language is set to English', fakeAsync(() => { - let isLanguageEnglishSpy = spyOn( - i18nLanguageCodeService, 'isCurrentLanguageEnglish') - .and.returnValue(true); - let ngbModalSpy = spyOn(ngbModal, 'open').and.callFake( - (modal, modalOptions) => { - return ({ - result: Promise.resolve() - } as NgbModalRef); - }); - - component.checkSiteLanguageBeforeBeginningPracticeSession(); - tick(); - - expect(isLanguageEnglishSpy).toHaveBeenCalled(); - expect(ngbModalSpy).not.toHaveBeenCalled(); - })); + let isLanguageEnglishSpy = spyOn( + i18nLanguageCodeService, + 'isCurrentLanguageEnglish' + ).and.returnValue(false); + let ngbModalSpy = spyOn(ngbModal, 'open').and.callFake( + (modal, modalOptions) => { + return { + result: Promise.resolve(), + } as NgbModalRef; + } + ); - it('should ask learner for confirmation before starting a new practice ' + - 'session if site language is not set to English', fakeAsync(() => { - let isLanguageEnglishSpy = spyOn( - i18nLanguageCodeService, 'isCurrentLanguageEnglish') - .and.returnValue(false); - let ngbModalSpy = spyOn(ngbModal, 'open').and.callFake( - (modal, modalOptions) => { - return ({ - result: Promise.resolve() - } as NgbModalRef); - }); - - component.checkSiteLanguageBeforeBeginningPracticeSession(); - tick(); - - expect(isLanguageEnglishSpy).toHaveBeenCalled(); - expect(ngbModalSpy).toHaveBeenCalled(); - })); + component.checkSiteLanguageBeforeBeginningPracticeSession(); + tick(); - it('should start a new practice session if the learner agrees to ' + - 'continue when site language is not set to English', fakeAsync(() => { - let isLanguageEnglishSpy = spyOn( - i18nLanguageCodeService, 'isCurrentLanguageEnglish') - .and.returnValue(false); - let newPracticeSessionSpy = spyOn(component, 'openNewPracticeSession'); - let ngbModalSpy = spyOn(ngbModal, 'open').and.callFake( - (modal, modalOptions) => { - return ({ - result: Promise.resolve() - } as NgbModalRef); - }); - - component.checkSiteLanguageBeforeBeginningPracticeSession(); - tick(); - - expect(isLanguageEnglishSpy).toHaveBeenCalled(); - expect(ngbModalSpy).toHaveBeenCalled(); - expect(newPracticeSessionSpy).toHaveBeenCalled(); - })); + expect(isLanguageEnglishSpy).toHaveBeenCalled(); + expect(ngbModalSpy).toHaveBeenCalled(); + }) + ); - it('should not start a new practice session if the learner refuses to ' + - 'continue when site language is not set to English', fakeAsync(() => { - let isLanguageEnglishSpy = spyOn( - i18nLanguageCodeService, 'isCurrentLanguageEnglish') - .and.returnValue(false); - let newPracticeSessionSpy = spyOn(component, 'openNewPracticeSession'); - let ngbModalSpy = spyOn(ngbModal, 'open').and.callFake( - (modal, modalOptions) => { - return ({ - result: Promise.reject() - } as NgbModalRef); - }); - - component.checkSiteLanguageBeforeBeginningPracticeSession(); - tick(); - - expect(isLanguageEnglishSpy).toHaveBeenCalled(); - expect(ngbModalSpy).toHaveBeenCalled(); - expect(newPracticeSessionSpy).not.toHaveBeenCalled(); - })); + it( + 'should start a new practice session if the learner agrees to ' + + 'continue when site language is not set to English', + fakeAsync(() => { + let isLanguageEnglishSpy = spyOn( + i18nLanguageCodeService, + 'isCurrentLanguageEnglish' + ).and.returnValue(false); + let newPracticeSessionSpy = spyOn(component, 'openNewPracticeSession'); + let ngbModalSpy = spyOn(ngbModal, 'open').and.callFake( + (modal, modalOptions) => { + return { + result: Promise.resolve(), + } as NgbModalRef; + } + ); - it('should open a new practice session containing the selected subtopic' + - ' when start button is clicked and learner agrees to continue', function() { - spyOn(loaderService, 'showLoadingScreen'); - component.displayArea = 'progressTab'; - component.topicUrlFragment = 'topic_1'; - component.classroomUrlFragment = 'classroom_1'; - component.selectedSubtopicIndices[0] = true; + component.checkSiteLanguageBeforeBeginningPracticeSession(); + tick(); - component.openNewPracticeSession(); + expect(isLanguageEnglishSpy).toHaveBeenCalled(); + expect(ngbModalSpy).toHaveBeenCalled(); + expect(newPracticeSessionSpy).toHaveBeenCalled(); + }) + ); - expect(windowRef.nativeWindow.location.href).toBe( - '/learn/classroom_1/topic_1/practice/session?' + - 'selected_subtopic_ids=%5B1%5D' - ); - expect(loaderService.showLoadingScreen).toHaveBeenCalledWith('Loading'); - }); + it( + 'should not start a new practice session if the learner refuses to ' + + 'continue when site language is not set to English', + fakeAsync(() => { + let isLanguageEnglishSpy = spyOn( + i18nLanguageCodeService, + 'isCurrentLanguageEnglish' + ).and.returnValue(false); + let newPracticeSessionSpy = spyOn(component, 'openNewPracticeSession'); + let ngbModalSpy = spyOn(ngbModal, 'open').and.callFake( + (modal, modalOptions) => { + return { + result: Promise.reject(), + } as NgbModalRef; + } + ); + + component.checkSiteLanguageBeforeBeginningPracticeSession(); + tick(); + + expect(isLanguageEnglishSpy).toHaveBeenCalled(); + expect(ngbModalSpy).toHaveBeenCalled(); + expect(newPracticeSessionSpy).not.toHaveBeenCalled(); + }) + ); + + it( + 'should open a new practice session containing the selected subtopic' + + ' when start button is clicked and learner agrees to continue', + function () { + spyOn(loaderService, 'showLoadingScreen'); + component.displayArea = 'progressTab'; + component.topicUrlFragment = 'topic_1'; + component.classroomUrlFragment = 'classroom_1'; + component.selectedSubtopicIndices[0] = true; + + component.openNewPracticeSession(); + + expect(windowRef.nativeWindow.location.href).toBe( + '/learn/classroom_1/topic_1/practice/session?' + + 'selected_subtopic_ids=%5B1%5D' + ); + expect(loaderService.showLoadingScreen).toHaveBeenCalledWith('Loading'); + } + ); it('should return background for progress of a subtopic', () => { component.subtopicMasteryArray = [10, 20]; diff --git a/core/templates/pages/topic-viewer-page/practice-tab/practice-tab.component.ts b/core/templates/pages/topic-viewer-page/practice-tab/practice-tab.component.ts index a1bed6613001..5b89825c4394 100644 --- a/core/templates/pages/topic-viewer-page/practice-tab/practice-tab.component.ts +++ b/core/templates/pages/topic-viewer-page/practice-tab/practice-tab.component.ts @@ -16,33 +16,32 @@ * @fileoverview Component for the topic viewer practice tab. */ -import { Component, Input, OnInit, OnDestroy } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; - -import { Subtopic } from 'domain/topic/subtopic.model'; -import { QuestionBackendApiService } from - 'domain/question/question-backend-api.service'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; -import { PracticeSessionPageConstants } from - 'pages/practice-session-page/practice-session-page.constants'; -import { UrlService } from 'services/contextual/url.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { I18nLanguageCodeService, TranslationKeyType } from 'services/i18n-language-code.service'; -import { PracticeSessionConfirmationModal } from 'pages/topic-viewer-page/modals/practice-session-confirmation-modal.component'; -import { LoaderService } from 'services/loader.service'; +import {Component, Input, OnInit, OnDestroy} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; -import './practice-tab.component.css'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; +import {Subtopic} from 'domain/topic/subtopic.model'; +import {QuestionBackendApiService} from 'domain/question/question-backend-api.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {PracticeSessionPageConstants} from 'pages/practice-session-page/practice-session-page.constants'; +import {UrlService} from 'services/contextual/url.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import { + I18nLanguageCodeService, + TranslationKeyType, +} from 'services/i18n-language-code.service'; +import {PracticeSessionConfirmationModal} from 'pages/topic-viewer-page/modals/practice-session-confirmation-modal.component'; +import {LoaderService} from 'services/loader.service'; +import './practice-tab.component.css'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; @Component({ selector: 'practice-tab', templateUrl: './practice-tab.component.html', - styleUrls: ['./practice-tab.component.css'] + styleUrls: ['./practice-tab.component.css'], }) export class PracticeTabComponent implements OnInit, OnDestroy { directiveSubscriptions = new Subscription(); @@ -92,24 +91,27 @@ export class PracticeTabComponent implements OnInit, OnDestroy { } for (let item of this.subtopicIds) { if (this.subtopicMastery[item] !== undefined) { - this.subtopicMasteryArray.push(Math.floor( - this.subtopicMastery[item] * 100)); + this.subtopicMasteryArray.push( + Math.floor(this.subtopicMastery[item] * 100) + ); } else { this.subtopicMasteryArray.push(0); } } - this.selectedSubtopicIndices = Array( - this.availableSubtopics.length).fill(false); + this.selectedSubtopicIndices = Array(this.availableSubtopics.length).fill( + false + ); this.clientWidth = window.innerWidth; if (this.displayArea === 'topicViewer' && !this.previewMode) { - this.topicUrlFragment = ( - this.urlService.getTopicUrlFragmentFromLearnerUrl()); - this.classroomUrlFragment = ( - this.urlService.getClassroomUrlFragmentFromLearnerUrl()); + this.topicUrlFragment = + this.urlService.getTopicUrlFragmentFromLearnerUrl(); + this.classroomUrlFragment = + this.urlService.getClassroomUrlFragmentFromLearnerUrl(); } this.topicNameTranslationKey = this.i18nLanguageCodeService.getTopicTranslationKey( - this.topicId, TranslationKeyType.TITLE + this.topicId, + TranslationKeyType.TITLE ); this.getTranslatedTopicName(); this.subscribeToOnLangChange(); @@ -130,7 +132,8 @@ export class PracticeTabComponent implements OnInit, OnDestroy { getTranslatedTopicName(): void { if (this.isTopicNameTranslationAvailable()) { this.translatedTopicName = this.translateService.instant( - this.topicNameTranslationKey); + this.topicNameTranslationKey + ); } else { this.translatedTopicName = this.topicName; } @@ -165,11 +168,12 @@ export class PracticeTabComponent implements OnInit, OnDestroy { } } if (skillIds.length > 0) { - this.questionBackendApiService.fetchTotalQuestionCountForSkillIdsAsync( - skillIds).then(questionCount => { - this.questionsAreAvailable = questionCount > 0; - this.questionsStatusCallIsComplete = true; - }); + this.questionBackendApiService + .fetchTotalQuestionCountForSkillIdsAsync(skillIds) + .then(questionCount => { + this.questionsAreAvailable = questionCount > 0; + this.questionsStatusCallIsComplete = true; + }); } else { this.questionsAreAvailable = false; this.questionsStatusCallIsComplete = true; @@ -181,28 +185,34 @@ export class PracticeTabComponent implements OnInit, OnDestroy { this.openNewPracticeSession(); return; } - this.ngbModal.open(PracticeSessionConfirmationModal, { - centered: true, - backdrop: 'static' - }).result.then(() => { - this.openNewPracticeSession(); - }, () => { }); + this.ngbModal + .open(PracticeSessionConfirmationModal, { + centered: true, + backdrop: 'static', + }) + .result.then( + () => { + this.openNewPracticeSession(); + }, + () => {} + ); } openNewPracticeSession(): void { const selectedSubtopicIds = []; for (let idx in this.selectedSubtopicIndices) { if (this.selectedSubtopicIndices[idx]) { - selectedSubtopicIds.push( - this.availableSubtopics[idx].getId()); + selectedSubtopicIds.push(this.availableSubtopics[idx].getId()); } } let practiceSessionsUrl = this.urlInterpolationService.interpolateUrl( - PracticeSessionPageConstants.PRACTICE_SESSIONS_URL, { + PracticeSessionPageConstants.PRACTICE_SESSIONS_URL, + { topic_url_fragment: this.topicUrlFragment, classroom_url_fragment: this.classroomUrlFragment, - stringified_subtopic_ids: JSON.stringify(selectedSubtopicIds) - }); + stringified_subtopic_ids: JSON.stringify(selectedSubtopicIds), + } + ); this.siteAnalyticsService.registerPracticeSessionStartEvent( this.classroomUrlFragment, this.topicName, @@ -227,12 +237,12 @@ export class PracticeTabComponent implements OnInit, OnDestroy { if (this.subtopicMasteryArray[i] <= 89) { return 225 - this.subtopicMasteryArray[i] * 2.5; } - return 225 - (this.subtopicMasteryArray[i] * 2) - 15; + return 225 - this.subtopicMasteryArray[i] * 2 - 15; } else { if (this.subtopicMasteryArray[i] <= 89) { - return 215 - (this.subtopicMasteryArray[i] * 2) - 25; + return 215 - this.subtopicMasteryArray[i] * 2 - 25; } - return 215 - (this.subtopicMasteryArray[i] * 2) - 10; + return 215 - this.subtopicMasteryArray[i] * 2 - 10; } } @@ -244,6 +254,9 @@ export class PracticeTabComponent implements OnInit, OnDestroy { } } -angular.module('oppia').directive( - 'practiceTab', downgradeComponent( - {component: PracticeTabComponent})); +angular + .module('oppia') + .directive( + 'practiceTab', + downgradeComponent({component: PracticeTabComponent}) + ); diff --git a/core/templates/pages/topic-viewer-page/stories-list/topic-viewer-stories-list.component.spec.ts b/core/templates/pages/topic-viewer-page/stories-list/topic-viewer-stories-list.component.spec.ts index 06d9e0cc26c9..eec940265329 100644 --- a/core/templates/pages/topic-viewer-page/stories-list/topic-viewer-stories-list.component.spec.ts +++ b/core/templates/pages/topic-viewer-page/stories-list/topic-viewer-stories-list.component.spec.ts @@ -16,12 +16,12 @@ * @fileoverview Unit tests for storiesList. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { StoriesListComponent } from './topic-viewer-stories-list.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {StoriesListComponent} from './topic-viewer-stories-list.component'; describe('Topic Viewer Stories List Component', () => { let component: StoriesListComponent; @@ -31,11 +31,8 @@ describe('Topic Viewer Stories List Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - StoriesListComponent, - MockTranslatePipe - ], - schemas: [NO_ERRORS_SCHEMA] + declarations: [StoriesListComponent, MockTranslatePipe], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -53,39 +50,44 @@ describe('Topic Viewer Stories List Component', () => { i18nLanguageCodeService = TestBed.inject(I18nLanguageCodeService); spyOn(i18nLanguageCodeService, 'isCurrentLanguageRTL').and.returnValue( - true); + true + ); }); it('should initialize properties after successfully', () => { - spyOn(i18nLanguageCodeService, 'getTopicTranslationKey') - .and.returnValues( - 'I18N_TOPIC_123abcd_TITLE', 'I18N_TOPIC_123abcd_DESCRIPTION'); + spyOn(i18nLanguageCodeService, 'getTopicTranslationKey').and.returnValues( + 'I18N_TOPIC_123abcd_TITLE', + 'I18N_TOPIC_123abcd_DESCRIPTION' + ); expect(component).toBeDefined(); component.ngOnInit(); - expect(component.topicNameTranslationKey).toBe( - 'I18N_TOPIC_123abcd_TITLE'); + expect(component.topicNameTranslationKey).toBe('I18N_TOPIC_123abcd_TITLE'); expect(component.topicDescTranslationKey).toBe( - 'I18N_TOPIC_123abcd_DESCRIPTION'); + 'I18N_TOPIC_123abcd_DESCRIPTION' + ); }); - it('should check if topic name, desc translation is displayed correctly', - () => { - spyOn(i18nLanguageCodeService, 'getTopicTranslationKey') - .and.returnValues( - 'I18N_TOPIC_123abcd_TITLE', 'I18N_TOPIC_123abcd_DESCRIPTION'); - spyOn(i18nLanguageCodeService, 'isHackyTranslationAvailable') - .and.returnValues(true, true); - spyOn(i18nLanguageCodeService, 'isCurrentLanguageEnglish') - .and.returnValues(false, false); + it('should check if topic name, desc translation is displayed correctly', () => { + spyOn(i18nLanguageCodeService, 'getTopicTranslationKey').and.returnValues( + 'I18N_TOPIC_123abcd_TITLE', + 'I18N_TOPIC_123abcd_DESCRIPTION' + ); + spyOn( + i18nLanguageCodeService, + 'isHackyTranslationAvailable' + ).and.returnValues(true, true); + spyOn(i18nLanguageCodeService, 'isCurrentLanguageEnglish').and.returnValues( + false, + false + ); - component.ngOnInit(); + component.ngOnInit(); - expect(component.isHackyTopicNameTranslationDisplayed()).toBe(true); - expect(component.isHackyTopicDescTranslationDisplayed()).toBe(true); - } - ); + expect(component.isHackyTopicNameTranslationDisplayed()).toBe(true); + expect(component.isHackyTopicDescTranslationDisplayed()).toBe(true); + }); it('should check if the view is tablet or not', () => { var widthSpy = spyOn(windowDimensionsService, 'getWidth'); diff --git a/core/templates/pages/topic-viewer-page/stories-list/topic-viewer-stories-list.component.ts b/core/templates/pages/topic-viewer-page/stories-list/topic-viewer-stories-list.component.ts index 28168d1a646d..a1d6e93792a4 100644 --- a/core/templates/pages/topic-viewer-page/stories-list/topic-viewer-stories-list.component.ts +++ b/core/templates/pages/topic-viewer-page/stories-list/topic-viewer-stories-list.component.ts @@ -16,21 +16,22 @@ * @fileoverview Component for the topic viewer stories list. */ -import { Component, Input, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component, Input, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; -import { StorySummary } from 'domain/story/story-summary.model'; -import { I18nLanguageCodeService, TranslationKeyType } from 'services/i18n-language-code.service'; -import { WindowDimensionsService } from - 'services/contextual/window-dimensions.service'; +import {StorySummary} from 'domain/story/story-summary.model'; +import { + I18nLanguageCodeService, + TranslationKeyType, +} from 'services/i18n-language-code.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; import './topic-viewer-stories-list.component.css'; - @Component({ selector: 'stories-list', templateUrl: './topic-viewer-stories-list.component.html', - styleUrls: ['./topic-viewer-stories-list.component.css'] + styleUrls: ['./topic-viewer-stories-list.component.css'], }) export class StoriesListComponent implements OnInit { // These properties are initialized using Angular lifecycle hooks @@ -51,10 +52,16 @@ export class StoriesListComponent implements OnInit { ) {} ngOnInit(): void { - this.topicNameTranslationKey = this.i18nLanguageCodeService - .getTopicTranslationKey(this.topicId, TranslationKeyType.TITLE); - this.topicDescTranslationKey = this.i18nLanguageCodeService - .getTopicTranslationKey(this.topicId, TranslationKeyType.DESCRIPTION); + this.topicNameTranslationKey = + this.i18nLanguageCodeService.getTopicTranslationKey( + this.topicId, + TranslationKeyType.TITLE + ); + this.topicDescTranslationKey = + this.i18nLanguageCodeService.getTopicTranslationKey( + this.topicId, + TranslationKeyType.DESCRIPTION + ); } isHackyTopicNameTranslationDisplayed(): boolean { @@ -77,6 +84,9 @@ export class StoriesListComponent implements OnInit { return this.windowDimensionsService.getWidth() < 768; } } -angular.module('oppia').directive( - 'storiesList', downgradeComponent( - {component: StoriesListComponent})); +angular + .module('oppia') + .directive( + 'storiesList', + downgradeComponent({component: StoriesListComponent}) + ); diff --git a/core/templates/pages/topic-viewer-page/subtopics-list/subtopics-list.component.spec.ts b/core/templates/pages/topic-viewer-page/subtopics-list/subtopics-list.component.spec.ts index 24ad04704ec1..e45fc1dbea4c 100644 --- a/core/templates/pages/topic-viewer-page/subtopics-list/subtopics-list.component.spec.ts +++ b/core/templates/pages/topic-viewer-page/subtopics-list/subtopics-list.component.spec.ts @@ -16,12 +16,12 @@ * @fileoverview Unit tests for subtopicsList. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { SubtopicSummaryTileComponent } from 'components/summary-tile/subtopic-summary-tile.component'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { SubtopicsListComponent } from './subtopics-list.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {SubtopicSummaryTileComponent} from 'components/summary-tile/subtopic-summary-tile.component'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {SubtopicsListComponent} from './subtopics-list.component'; describe('Subtopics List Component', () => { let component: SubtopicsListComponent; @@ -33,9 +33,9 @@ describe('Subtopics List Component', () => { declarations: [ MockTranslatePipe, SubtopicsListComponent, - SubtopicSummaryTileComponent + SubtopicSummaryTileComponent, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -49,7 +49,8 @@ describe('Subtopics List Component', () => { component.topicName = 'Topic Name'; component.topicId = 'topicId'; spyOn(i18nLanguageCodeService, 'isCurrentLanguageRTL').and.returnValue( - true); + true + ); }); it('should create', () => { @@ -57,27 +58,29 @@ describe('Subtopics List Component', () => { }); it('should initialize properties after successfully', () => { - spyOn(i18nLanguageCodeService, 'getTopicTranslationKey') - .and.returnValue('I18N_TOPIC_123abcd_TITLE'); + spyOn(i18nLanguageCodeService, 'getTopicTranslationKey').and.returnValue( + 'I18N_TOPIC_123abcd_TITLE' + ); component.ngOnInit(); - expect(component.topicNameTranslationKey).toBe( - 'I18N_TOPIC_123abcd_TITLE'); + expect(component.topicNameTranslationKey).toBe('I18N_TOPIC_123abcd_TITLE'); }); - it('should check if topic name, desc translation is displayed correctly', - () => { - spyOn(i18nLanguageCodeService, 'getTopicTranslationKey') - .and.returnValue('I18N_TOPIC_123abcd_TITLE'); - spyOn(i18nLanguageCodeService, 'isHackyTranslationAvailable') - .and.returnValue(true); - spyOn(i18nLanguageCodeService, 'isCurrentLanguageEnglish') - .and.returnValue(false); + it('should check if topic name, desc translation is displayed correctly', () => { + spyOn(i18nLanguageCodeService, 'getTopicTranslationKey').and.returnValue( + 'I18N_TOPIC_123abcd_TITLE' + ); + spyOn( + i18nLanguageCodeService, + 'isHackyTranslationAvailable' + ).and.returnValue(true); + spyOn(i18nLanguageCodeService, 'isCurrentLanguageEnglish').and.returnValue( + false + ); - component.ngOnInit(); + component.ngOnInit(); - expect(component.isHackyTopicNameTranslationDisplayed()).toBe(true); - } - ); + expect(component.isHackyTopicNameTranslationDisplayed()).toBe(true); + }); }); diff --git a/core/templates/pages/topic-viewer-page/subtopics-list/subtopics-list.component.ts b/core/templates/pages/topic-viewer-page/subtopics-list/subtopics-list.component.ts index 06e69fb723e2..86dc70858f2d 100644 --- a/core/templates/pages/topic-viewer-page/subtopics-list/subtopics-list.component.ts +++ b/core/templates/pages/topic-viewer-page/subtopics-list/subtopics-list.component.ts @@ -16,19 +16,21 @@ * @fileoverview Component for topic-viewer subtopics list. */ -import { Component, Input, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component, Input, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; -import { Subtopic } from 'domain/topic/subtopic.model'; -import { I18nLanguageCodeService, TranslationKeyType } from 'services/i18n-language-code.service'; +import {Subtopic} from 'domain/topic/subtopic.model'; +import { + I18nLanguageCodeService, + TranslationKeyType, +} from 'services/i18n-language-code.service'; import './subtopics-list.component.css'; - @Component({ selector: 'subtopics-list', templateUrl: './subtopics-list.component.html', - styleUrls: ['./subtopics-list.component.css'] + styleUrls: ['./subtopics-list.component.css'], }) export class SubtopicsListComponent implements OnInit { // These properties are initialized using Angular lifecycle hooks @@ -41,13 +43,14 @@ export class SubtopicsListComponent implements OnInit { @Input() topicName!: string; topicNameTranslationKey!: string; - constructor( - private i18nLanguageCodeService: I18nLanguageCodeService - ) {} + constructor(private i18nLanguageCodeService: I18nLanguageCodeService) {} ngOnInit(): void { - this.topicNameTranslationKey = this.i18nLanguageCodeService - .getTopicTranslationKey(this.topicId, TranslationKeyType.TITLE); + this.topicNameTranslationKey = + this.i18nLanguageCodeService.getTopicTranslationKey( + this.topicId, + TranslationKeyType.TITLE + ); } isHackyTopicNameTranslationDisplayed(): boolean { @@ -59,6 +62,9 @@ export class SubtopicsListComponent implements OnInit { } } -angular.module('oppia').directive( - 'subtopicsList', downgradeComponent( - {component: SubtopicsListComponent})); +angular + .module('oppia') + .directive( + 'subtopicsList', + downgradeComponent({component: SubtopicsListComponent}) + ); diff --git a/core/templates/pages/topic-viewer-page/topic-viewer-page.component.spec.ts b/core/templates/pages/topic-viewer-page/topic-viewer-page.component.spec.ts index 72ba6bd4bf60..8bffb1626c4c 100644 --- a/core/templates/pages/topic-viewer-page/topic-viewer-page.component.spec.ts +++ b/core/templates/pages/topic-viewer-page/topic-viewer-page.component.spec.ts @@ -16,22 +16,22 @@ * @fileoverview Unit tests for topic viewer page component. */ -import { HttpClientTestingModule, HttpTestingController } from - '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; - -import { TopicViewerPageComponent } from - 'pages/topic-viewer-page/topic-viewer-page.component'; -import { AlertsService } from 'services/alerts.service'; -import { UrlService } from 'services/contextual/url.service'; -import { WindowDimensionsService } from - 'services/contextual/window-dimensions.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { PageTitleService } from 'services/page-title.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; + +import {TopicViewerPageComponent} from 'pages/topic-viewer-page/topic-viewer-page.component'; +import {AlertsService} from 'services/alerts.service'; +import {UrlService} from 'services/contextual/url.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {PageTitleService} from 'services/page-title.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; class MockWindowRef { _window = { @@ -40,11 +40,11 @@ class MockWindowRef { _hash: '', toString() { return 'http://localhost/test_path'; - } + }, }, history: { - pushState(title: string, url: string | null) {} - } + pushState(title: string, url: string | null) {}, + }, }; get nativeWindow() { @@ -76,16 +76,18 @@ describe('Topic viewer page', () => { topic_id: '1', topic_name: 'Topic Name', topic_description: 'Topic Description', - canonical_story_dicts: [{ - id: '2', - title: 'Story Title', - node_titles: ['Node title 1', 'Node title 2'], - thumbnail_filename: '', - thumbnail_bg_color: '', - description: 'Story Description', - story_is_published: true, - all_node_dicts: [] - }], + canonical_story_dicts: [ + { + id: '2', + title: 'Story Title', + node_titles: ['Node title 1', 'Node title 2'], + thumbnail_filename: '', + thumbnail_bg_color: '', + description: 'Story Description', + story_is_published: true, + all_node_dicts: [], + }, + ], additional_story_dicts: [], uncategorized_skill_ids: [], subtopics: [], @@ -93,30 +95,25 @@ describe('Topic viewer page', () => { skill_descriptions: {}, practice_tab_is_displayed: true, meta_tag_content: 'Topic Meta Tag', - page_title_fragment_for_web: 'Topic page title' + page_title_fragment_for_web: 'Topic page title', }; beforeEach(() => { windowRef = new MockWindowRef(); TestBed.configureTestingModule({ - declarations: [ - TopicViewerPageComponent, - MockTranslatePipe - ], - imports: [ - HttpClientTestingModule - ], + declarations: [TopicViewerPageComponent, MockTranslatePipe], + imports: [HttpClientTestingModule], providers: [ { provide: WindowRef, - useValue: windowRef + useValue: windowRef, }, { provide: TranslateService, - useClass: MockTranslateService - } + useClass: MockTranslateService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); httpTestingController = TestBed.inject(HttpTestingController); alertsService = TestBed.inject(AlertsService); @@ -129,7 +126,8 @@ describe('Topic viewer page', () => { topicViewerPageComponent = fixture.componentInstance; spyOn(i18nLanguageCodeService, 'isCurrentLanguageRTL').and.returnValue( - true); + true + ); }); afterEach(() => { @@ -138,9 +136,11 @@ describe('Topic viewer page', () => { it('should successfully get topic data', fakeAsync(() => { spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue( - topicUrlFragment); + topicUrlFragment + ); spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl').and.returnValue( - 'math'); + 'math' + ); spyOn(topicViewerPageComponent, 'subscribeToOnLangChange'); spyOn(windowRef.nativeWindow.history, 'pushState'); @@ -148,17 +148,20 @@ describe('Topic viewer page', () => { expect(topicViewerPageComponent.canonicalStorySummaries).toEqual([]); expect(topicViewerPageComponent.activeTab).toBe('story'); expect(windowRef.nativeWindow.history.pushState).toHaveBeenCalledWith( - {}, '', 'http://localhost/test_path/story'); + {}, + '', + 'http://localhost/test_path/story' + ); var req = httpTestingController.expectOne( - `/topic_data_handler/math/${topicUrlFragment}`); + `/topic_data_handler/math/${topicUrlFragment}` + ); req.flush(topicDict); flushMicrotasks(); expect(topicViewerPageComponent.topicId).toBe('1'); expect(topicViewerPageComponent.topicName).toBe('Topic Name'); expect(topicViewerPageComponent.subscribeToOnLangChange); - expect(topicViewerPageComponent.topicDescription).toBe( - 'Topic Description'); + expect(topicViewerPageComponent.topicDescription).toBe('Topic Description'); expect(topicViewerPageComponent.canonicalStorySummaries.length).toBe(1); expect(topicViewerPageComponent.chapterCount).toBe(2); expect(topicViewerPageComponent.degreesOfMastery).toEqual({}); @@ -168,14 +171,17 @@ describe('Topic viewer page', () => { expect(topicViewerPageComponent.practiceTabIsDisplayed).toBe(true); })); - it('should obtain translated title and set it whenever the ' + - 'selected language changes', () => { - topicViewerPageComponent.subscribeToOnLangChange(); - spyOn(topicViewerPageComponent, 'setPageTitle'); - translateService.onLangChange.emit(); + it( + 'should obtain translated title and set it whenever the ' + + 'selected language changes', + () => { + topicViewerPageComponent.subscribeToOnLangChange(); + spyOn(topicViewerPageComponent, 'setPageTitle'); + translateService.onLangChange.emit(); - expect(topicViewerPageComponent.setPageTitle).toHaveBeenCalled(); - }); + expect(topicViewerPageComponent.setPageTitle).toHaveBeenCalled(); + } + ); it('should set page title', () => { spyOn(translateService, 'instant').and.callThrough(); @@ -186,12 +192,15 @@ describe('Topic viewer page', () => { topicViewerPageComponent.setPageTitle(); expect(translateService.instant).toHaveBeenCalledWith( - 'I18N_TOPIC_VIEWER_PAGE_TITLE', { + 'I18N_TOPIC_VIEWER_PAGE_TITLE', + { topicName: 'Topic Name', - pageTitleFragment: 'Topic page title' - }); + pageTitleFragment: 'Topic page title', + } + ); expect(pageTitleService.setDocumentTitle).toHaveBeenCalledWith( - 'I18N_TOPIC_VIEWER_PAGE_TITLE'); + 'I18N_TOPIC_VIEWER_PAGE_TITLE' + ); }); it('should unsubscribe upon component destruction', () => { @@ -204,14 +213,18 @@ describe('Topic viewer page', () => { it('should set story tab correctly', fakeAsync(() => { spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue( - topicUrlFragment); + topicUrlFragment + ); spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl').and.returnValue( - 'math'); + 'math' + ); spyOn(urlService, 'getPathname').and.returnValue( - `/learn/math/${topicUrlFragment}/story`); + `/learn/math/${topicUrlFragment}/story` + ); topicViewerPageComponent.ngOnInit(); var req = httpTestingController.expectOne( - `/topic_data_handler/math/${topicUrlFragment}`); + `/topic_data_handler/math/${topicUrlFragment}` + ); req.flush(topicDict); flushMicrotasks(); expect(topicViewerPageComponent.activeTab).toBe('story'); @@ -219,51 +232,61 @@ describe('Topic viewer page', () => { it('should set revision tab correctly', fakeAsync(() => { spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue( - topicUrlFragment); + topicUrlFragment + ); spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl').and.returnValue( - 'math'); + 'math' + ); spyOn(urlService, 'getPathname').and.returnValue( - `/learn/math/${topicUrlFragment}/revision`); + `/learn/math/${topicUrlFragment}/revision` + ); topicViewerPageComponent.ngOnInit(); var req = httpTestingController.expectOne( - `/topic_data_handler/math/${topicUrlFragment}`); + `/topic_data_handler/math/${topicUrlFragment}` + ); req.flush(topicDict); expect(topicViewerPageComponent.activeTab).toBe('subtopics'); })); it('should set practice tab correctly', fakeAsync(() => { spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue( - topicUrlFragment); + topicUrlFragment + ); spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl').and.returnValue( - 'math'); + 'math' + ); spyOn(urlService, 'getPathname').and.returnValue( - `/learn/math/${topicUrlFragment}/practice`); + `/learn/math/${topicUrlFragment}/practice` + ); topicViewerPageComponent.ngOnInit(); var req = httpTestingController.expectOne( - `/topic_data_handler/math/${topicUrlFragment}`); + `/topic_data_handler/math/${topicUrlFragment}` + ); req.flush(topicDict); expect(topicViewerPageComponent.activeTab).toBe('practice'); })); - it('should use reject handler when fetching subtopic data fails', - fakeAsync(() => { - spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue( - topicUrlFragment); - spyOn( - urlService, 'getClassroomUrlFragmentFromLearnerUrl').and.returnValue( - 'math'); - spyOn(alertsService, 'addWarning').and.callThrough(); - - topicViewerPageComponent.ngOnInit(); - let req = httpTestingController.expectOne( - `/topic_data_handler/math/${topicUrlFragment}`); - let errorObject = { status: 404, statusText: 'Not Found' }; - req.flush({ error: errorObject }, errorObject); - flushMicrotasks(); - - expect(alertsService.addWarning).toHaveBeenCalledWith( - 'Failed to get dashboard data'); - })); + it('should use reject handler when fetching subtopic data fails', fakeAsync(() => { + spyOn(urlService, 'getTopicUrlFragmentFromLearnerUrl').and.returnValue( + topicUrlFragment + ); + spyOn(urlService, 'getClassroomUrlFragmentFromLearnerUrl').and.returnValue( + 'math' + ); + spyOn(alertsService, 'addWarning').and.callThrough(); + + topicViewerPageComponent.ngOnInit(); + let req = httpTestingController.expectOne( + `/topic_data_handler/math/${topicUrlFragment}` + ); + let errorObject = {status: 404, statusText: 'Not Found'}; + req.flush({error: errorObject}, errorObject); + flushMicrotasks(); + + expect(alertsService.addWarning).toHaveBeenCalledWith( + 'Failed to get dashboard data' + ); + })); it('should get static image url', () => { var imagePath = '/path/to/image.png'; @@ -290,45 +313,65 @@ describe('Topic viewer page', () => { expect(topicViewerPageComponent.checkTabletView()).toBe(false); }); - it('should set url accordingly when user changes active tab to' + - ' story tab', () => { - spyOn(windowRef.nativeWindow.history, 'pushState'); - topicViewerPageComponent.activeTab = 'subtopics'; - spyOn(windowRef.nativeWindow.location, 'toString').and.returnValue( - 'http://localhost/test_path/revision'); - - topicViewerPageComponent.setActiveTab('story'); - - expect(windowRef.nativeWindow.history.pushState).toHaveBeenCalledWith( - {}, '', 'http://localhost/test_path/story'); - expect(topicViewerPageComponent.activeTab).toBe('story'); - }); - - it('should set url hash accordingly when user changes active tab to' + - ' practice tab', () => { - spyOn(windowRef.nativeWindow.history, 'pushState'); - topicViewerPageComponent.activeTab = 'subtopics'; - spyOn(windowRef.nativeWindow.location, 'toString').and.returnValue( - 'http://localhost/test_path/revision'); - - topicViewerPageComponent.setActiveTab('practice'); - - expect(windowRef.nativeWindow.history.pushState).toHaveBeenCalledWith( - {}, '', 'http://localhost/test_path/practice'); - expect(topicViewerPageComponent.activeTab).toBe('practice'); - }); - - it('should set url hash accordingly when user changes active tab to' + - ' subtopics tab', () => { - spyOn(windowRef.nativeWindow.history, 'pushState'); - topicViewerPageComponent.activeTab = 'story'; - spyOn(windowRef.nativeWindow.location, 'toString').and.returnValue( - 'http://localhost/test_path/story'); - - topicViewerPageComponent.setActiveTab('subtopics'); - - expect(windowRef.nativeWindow.history.pushState).toHaveBeenCalledWith( - {}, '', 'http://localhost/test_path/revision'); - expect(topicViewerPageComponent.activeTab).toBe('subtopics'); - }); + it( + 'should set url accordingly when user changes active tab to' + ' story tab', + () => { + spyOn(windowRef.nativeWindow.history, 'pushState'); + topicViewerPageComponent.activeTab = 'subtopics'; + spyOn(windowRef.nativeWindow.location, 'toString').and.returnValue( + 'http://localhost/test_path/revision' + ); + + topicViewerPageComponent.setActiveTab('story'); + + expect(windowRef.nativeWindow.history.pushState).toHaveBeenCalledWith( + {}, + '', + 'http://localhost/test_path/story' + ); + expect(topicViewerPageComponent.activeTab).toBe('story'); + } + ); + + it( + 'should set url hash accordingly when user changes active tab to' + + ' practice tab', + () => { + spyOn(windowRef.nativeWindow.history, 'pushState'); + topicViewerPageComponent.activeTab = 'subtopics'; + spyOn(windowRef.nativeWindow.location, 'toString').and.returnValue( + 'http://localhost/test_path/revision' + ); + + topicViewerPageComponent.setActiveTab('practice'); + + expect(windowRef.nativeWindow.history.pushState).toHaveBeenCalledWith( + {}, + '', + 'http://localhost/test_path/practice' + ); + expect(topicViewerPageComponent.activeTab).toBe('practice'); + } + ); + + it( + 'should set url hash accordingly when user changes active tab to' + + ' subtopics tab', + () => { + spyOn(windowRef.nativeWindow.history, 'pushState'); + topicViewerPageComponent.activeTab = 'story'; + spyOn(windowRef.nativeWindow.location, 'toString').and.returnValue( + 'http://localhost/test_path/story' + ); + + topicViewerPageComponent.setActiveTab('subtopics'); + + expect(windowRef.nativeWindow.history.pushState).toHaveBeenCalledWith( + {}, + '', + 'http://localhost/test_path/revision' + ); + expect(topicViewerPageComponent.activeTab).toBe('subtopics'); + } + ); }); diff --git a/core/templates/pages/topic-viewer-page/topic-viewer-page.component.ts b/core/templates/pages/topic-viewer-page/topic-viewer-page.component.ts index b95f7cec7319..79c4990a428b 100644 --- a/core/templates/pages/topic-viewer-page/topic-viewer-page.component.ts +++ b/core/templates/pages/topic-viewer-page/topic-viewer-page.component.ts @@ -16,39 +16,32 @@ * @fileoverview Component for the topic viewer. */ -import { Component, OnInit, OnDestroy } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; - -import { AppConstants } from 'app.constants'; -import { ReadOnlyTopic } from - 'domain/topic_viewer/read-only-topic-object.factory'; -import { StorySummary } from 'domain/story/story-summary.model'; -import { Subtopic, SkillIdToDescriptionMap } from - 'domain/topic/subtopic.model'; -import { DegreesOfMastery } from - 'domain/topic_viewer/read-only-topic-object.factory'; -import { TopicViewerBackendApiService } from - 'domain/topic_viewer/topic-viewer-backend-api.service'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; -import { AlertsService } from 'services/alerts.service'; -import { UrlService } from 'services/contextual/url.service'; -import { WindowDimensionsService } from - 'services/contextual/window-dimensions.service'; -import { LoaderService } from 'services/loader.service'; -import { PageTitleService } from 'services/page-title.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; +import {Component, OnInit, OnDestroy} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {TranslateService} from '@ngx-translate/core'; +import {Subscription} from 'rxjs'; + +import {AppConstants} from 'app.constants'; +import {ReadOnlyTopic} from 'domain/topic_viewer/read-only-topic-object.factory'; +import {StorySummary} from 'domain/story/story-summary.model'; +import {Subtopic, SkillIdToDescriptionMap} from 'domain/topic/subtopic.model'; +import {DegreesOfMastery} from 'domain/topic_viewer/read-only-topic-object.factory'; +import {TopicViewerBackendApiService} from 'domain/topic_viewer/topic-viewer-backend-api.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {AlertsService} from 'services/alerts.service'; +import {UrlService} from 'services/contextual/url.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {LoaderService} from 'services/loader.service'; +import {PageTitleService} from 'services/page-title.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; import './topic-viewer-page.component.css'; - @Component({ selector: 'topic-viewer-page', templateUrl: './topic-viewer-page.component.html', - styleUrls: ['./topic-viewer-page.component.css'] + styleUrls: ['./topic-viewer-page.component.css'], }) export class TopicViewerPageComponent implements OnInit, OnDestroy { directiveSubscriptions = new Subscription(); @@ -91,49 +84,54 @@ export class TopicViewerPageComponent implements OnInit, OnDestroy { } this.activeTab = 'story'; } - this.topicUrlFragment = ( - this.urlService.getTopicUrlFragmentFromLearnerUrl()); - this.classroomUrlFragment = ( - this.urlService.getClassroomUrlFragmentFromLearnerUrl()); + this.topicUrlFragment = this.urlService.getTopicUrlFragmentFromLearnerUrl(); + this.classroomUrlFragment = + this.urlService.getClassroomUrlFragmentFromLearnerUrl(); this.loaderService.showLoadingScreen('Loading'); - this.topicViewerBackendApiService.fetchTopicDataAsync( - this.topicUrlFragment, this.classroomUrlFragment).then( - (readOnlyTopic: ReadOnlyTopic) => { - this.topicId = readOnlyTopic.getTopicId(); - this.topicName = readOnlyTopic.getTopicName(); - this.topicDescription = readOnlyTopic.getTopicDescription(); - this.pageTitleFragment = readOnlyTopic.getPageTitleFragmentForWeb(); - - // The onLangChange event is initially fired before the topic is - // loaded. Hence the first setpageTitle() call needs to made - // manually, and the onLangChange subscription is added after - // the topic is loaded. - this.setPageTitle(); - this.subscribeToOnLangChange(); - this.pageTitleService.updateMetaTag(readOnlyTopic.getMetaTagContent()); - this.canonicalStorySummaries = ( - readOnlyTopic.getCanonicalStorySummaries()); - this.chapterCount = 0; - for (let idx in this.canonicalStorySummaries) { - this.chapterCount += ( - this.canonicalStorySummaries[idx].getNodeTitles().length); - } - this.degreesOfMastery = readOnlyTopic.getDegreesOfMastery(); - this.subtopics = readOnlyTopic.getSubtopics(); - this.skillDescriptions = readOnlyTopic.getSkillDescriptions(); - this.topicIsLoading = false; - this.loaderService.hideLoadingScreen(); - this.practiceTabIsDisplayed = ( - readOnlyTopic.getPracticeTabIsDisplayed()); - }, - errorResponse => { - let errorCodes = AppConstants.FATAL_ERROR_CODES; - if (errorResponse && errorCodes.indexOf(errorResponse.status) !== -1) { - this.alertsService.addWarning('Failed to get dashboard data'); + this.topicViewerBackendApiService + .fetchTopicDataAsync(this.topicUrlFragment, this.classroomUrlFragment) + .then( + (readOnlyTopic: ReadOnlyTopic) => { + this.topicId = readOnlyTopic.getTopicId(); + this.topicName = readOnlyTopic.getTopicName(); + this.topicDescription = readOnlyTopic.getTopicDescription(); + this.pageTitleFragment = readOnlyTopic.getPageTitleFragmentForWeb(); + + // The onLangChange event is initially fired before the topic is + // loaded. Hence the first setpageTitle() call needs to made + // manually, and the onLangChange subscription is added after + // the topic is loaded. + this.setPageTitle(); + this.subscribeToOnLangChange(); + this.pageTitleService.updateMetaTag( + readOnlyTopic.getMetaTagContent() + ); + this.canonicalStorySummaries = + readOnlyTopic.getCanonicalStorySummaries(); + this.chapterCount = 0; + for (let idx in this.canonicalStorySummaries) { + this.chapterCount += + this.canonicalStorySummaries[idx].getNodeTitles().length; + } + this.degreesOfMastery = readOnlyTopic.getDegreesOfMastery(); + this.subtopics = readOnlyTopic.getSubtopics(); + this.skillDescriptions = readOnlyTopic.getSkillDescriptions(); + this.topicIsLoading = false; + this.loaderService.hideLoadingScreen(); + this.practiceTabIsDisplayed = + readOnlyTopic.getPracticeTabIsDisplayed(); + }, + errorResponse => { + let errorCodes = AppConstants.FATAL_ERROR_CODES; + if ( + errorResponse && + errorCodes.indexOf(errorResponse.status) !== -1 + ) { + this.alertsService.addWarning('Failed to get dashboard data'); + } } - } - ); + ); } ngOnDestroy(): void { @@ -150,9 +148,10 @@ export class TopicViewerPageComponent implements OnInit, OnDestroy { setPageTitle(): void { let translatedTitle = this.translateService.instant( - 'I18N_TOPIC_VIEWER_PAGE_TITLE', { + 'I18N_TOPIC_VIEWER_PAGE_TITLE', + { topicName: this.topicName, - pageTitleFragment: this.pageTitleFragment + pageTitleFragment: this.pageTitleFragment, } ); this.pageTitleService.setDocumentTitle(translatedTitle); @@ -185,17 +184,29 @@ export class TopicViewerPageComponent implements OnInit, OnDestroy { let getCurrentLocation = this.windowRef.nativeWindow.location.toString(); if (this.activeTab === '') { this.windowRef.nativeWindow.history.pushState( - {}, '', getCurrentLocation + '/' + newTabName); + {}, + '', + getCurrentLocation + '/' + newTabName + ); } else if (this.activeTab === 'subtopics') { this.windowRef.nativeWindow.history.pushState( - {}, '', getCurrentLocation.replace('revision', newTabName)); + {}, + '', + getCurrentLocation.replace('revision', newTabName) + ); } else { this.windowRef.nativeWindow.history.pushState( - {}, '', getCurrentLocation.replace(this.activeTab, newTabName)); + {}, + '', + getCurrentLocation.replace(this.activeTab, newTabName) + ); } } } -angular.module('oppia').directive( - 'topicViewerPage', downgradeComponent( - {component: TopicViewerPageComponent})); +angular + .module('oppia') + .directive( + 'topicViewerPage', + downgradeComponent({component: TopicViewerPageComponent}) + ); diff --git a/core/templates/pages/topic-viewer-page/topic-viewer-page.import.ts b/core/templates/pages/topic-viewer-page/topic-viewer-page.import.ts index 05b7b36c3723..179b4338e620 100644 --- a/core/templates/pages/topic-viewer-page/topic-viewer-page.import.ts +++ b/core/templates/pages/topic-viewer-page/topic-viewer-page.import.ts @@ -22,9 +22,14 @@ import 'zone.js'; import uiValidate from 'angular-ui-validate'; angular.module('oppia', [ - require('angular-cookies'), 'ngAnimate', - 'ngMaterial', 'ngSanitize', 'ngTouch', 'pascalprecht.translate', - 'ui.bootstrap', uiValidate + require('angular-cookies'), + 'ngAnimate', + 'ngMaterial', + 'ngSanitize', + 'ngTouch', + 'pascalprecht.translate', + 'ui.bootstrap', + uiValidate, ]); require('Polyfills.ts'); @@ -38,5 +43,6 @@ require('base-components/oppia-root.directive.ts'); require( 'pages/topic-viewer-page/navbar-breadcrumb/' + - 'topic-viewer-navbar-breadcrumb.component.ts'); + 'topic-viewer-navbar-breadcrumb.component.ts' +); require('pages/topic-viewer-page/topic-viewer-page.component.ts'); diff --git a/core/templates/pages/topic-viewer-page/topic-viewer-page.module.ts b/core/templates/pages/topic-viewer-page/topic-viewer-page.module.ts index 8c547a5d07e3..9b5333f8f6ea 100644 --- a/core/templates/pages/topic-viewer-page/topic-viewer-page.module.ts +++ b/core/templates/pages/topic-viewer-page/topic-viewer-page.module.ts @@ -16,30 +16,31 @@ * @fileoverview Module for the topic viewer page. */ -import { APP_INITIALIZER, NgModule, StaticProvider } from '@angular/core'; -import { BrowserModule, HAMMER_GESTURE_CONFIG } from '@angular/platform-browser'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { HttpClientModule } from '@angular/common/http'; -import { HTTP_INTERCEPTORS } from '@angular/common/http'; -import { platformFeatureInitFactory, PlatformFeatureService } from - 'services/platform-feature.service'; -import { RouterModule } from '@angular/router'; -import { APP_BASE_HREF } from '@angular/common'; +import {APP_INITIALIZER, NgModule, StaticProvider} from '@angular/core'; +import {BrowserModule, HAMMER_GESTURE_CONFIG} from '@angular/platform-browser'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {HttpClientModule} from '@angular/common/http'; +import {HTTP_INTERCEPTORS} from '@angular/common/http'; +import { + platformFeatureInitFactory, + PlatformFeatureService, +} from 'services/platform-feature.service'; +import {RouterModule} from '@angular/router'; +import {APP_BASE_HREF} from '@angular/common'; -import { OppiaAngularRootComponent } from - 'components/oppia-angular-root.component'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { TopicViewerNavbarBreadcrumbComponent } from +import {OppiaAngularRootComponent} from 'components/oppia-angular-root.component'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import { + TopicViewerNavbarBreadcrumbComponent, // eslint-disable-next-line max-len - 'pages/topic-viewer-page/navbar-breadcrumb/topic-viewer-navbar-breadcrumb.component'; -import { RequestInterceptor } from 'services/request-interceptor.service'; -import { TopicViewerPageComponent } from - 'pages/topic-viewer-page/topic-viewer-page.component'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { MyHammerConfig, toastrConfig } from 'pages/oppia-root/app.module'; -import { PracticeSessionConfirmationModal } from './modals/practice-session-confirmation-modal.component'; +} from 'pages/topic-viewer-page/navbar-breadcrumb/topic-viewer-navbar-breadcrumb.component'; +import {RequestInterceptor} from 'services/request-interceptor.service'; +import {TopicViewerPageComponent} from 'pages/topic-viewer-page/topic-viewer-page.component'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {MyHammerConfig, toastrConfig} from 'pages/oppia-root/app.module'; +import {PracticeSessionConfirmationModal} from './modals/practice-session-confirmation-modal.component'; import {SmartRouterModule} from 'hybrid-router-module-provider'; -import { AppErrorHandlerProvider } from 'pages/oppia-root/app-error-handler'; +import {AppErrorHandlerProvider} from 'pages/oppia-root/app-error-handler'; @NgModule({ imports: [ @@ -52,52 +53,52 @@ import { AppErrorHandlerProvider } from 'pages/oppia-root/app-error-handler'; RouterModule.forRoot([]), SharedComponentsModule, TopicPlayerViewerCommonModule, - ToastrModule.forRoot(toastrConfig) + ToastrModule.forRoot(toastrConfig), ], declarations: [ TopicViewerNavbarBreadcrumbComponent, TopicViewerPageComponent, - PracticeSessionConfirmationModal + PracticeSessionConfirmationModal, ], entryComponents: [ TopicViewerNavbarBreadcrumbComponent, TopicViewerPageComponent, - PracticeSessionConfirmationModal + PracticeSessionConfirmationModal, ], providers: [ { provide: HTTP_INTERCEPTORS, useClass: RequestInterceptor, - multi: true + multi: true, }, { provide: APP_INITIALIZER, useFactory: platformFeatureInitFactory, deps: [PlatformFeatureService], - multi: true + multi: true, }, { provide: HAMMER_GESTURE_CONFIG, - useClass: MyHammerConfig + useClass: MyHammerConfig, }, AppErrorHandlerProvider, { provide: APP_BASE_HREF, - useValue: '/' - } - ] + useValue: '/', + }, + ], }) class TopicViewerPageModule { // Empty placeholder method to satisfy the `Compiler`. ngDoBootstrap() {} } -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { downgradeModule } from '@angular/upgrade/static'; -import { ToastrModule } from 'ngx-toastr'; -import { TopicPlayerViewerCommonModule } from './topic-viewer-player-common.module'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {downgradeModule} from '@angular/upgrade/static'; +import {ToastrModule} from 'ngx-toastr'; +import {TopicPlayerViewerCommonModule} from './topic-viewer-player-common.module'; -const bootstrapFnAsync = async(extraProviders: StaticProvider[]) => { +const bootstrapFnAsync = async (extraProviders: StaticProvider[]) => { const platformRef = platformBrowserDynamic(extraProviders); return platformRef.bootstrapModule(TopicViewerPageModule); }; @@ -112,5 +113,6 @@ angular.module('oppia').directive( // bootstrap the Angular 8. 'oppiaAngularRoot', downgradeComponent({ - component: OppiaAngularRootComponent - }) as angular.IDirectiveFactory); + component: OppiaAngularRootComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/topic-viewer-page/topic-viewer-player-common.module.ts b/core/templates/pages/topic-viewer-page/topic-viewer-player-common.module.ts index 1af4c9ed5505..79058a18f3bb 100644 --- a/core/templates/pages/topic-viewer-page/topic-viewer-player-common.module.ts +++ b/core/templates/pages/topic-viewer-page/topic-viewer-player-common.module.ts @@ -19,31 +19,17 @@ import 'core-js/es7/reflect'; import 'zone.js'; -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { SubtopicsListComponent } from './subtopics-list/subtopics-list.component'; -import { StoriesListComponent } from './stories-list/topic-viewer-stories-list.component'; -import { MatCardModule } from '@angular/material/card'; -import { SharedComponentsModule } from 'components/shared-component.module'; +import {CommonModule} from '@angular/common'; +import {NgModule} from '@angular/core'; +import {SubtopicsListComponent} from './subtopics-list/subtopics-list.component'; +import {StoriesListComponent} from './stories-list/topic-viewer-stories-list.component'; +import {MatCardModule} from '@angular/material/card'; +import {SharedComponentsModule} from 'components/shared-component.module'; @NgModule({ - imports: [ - CommonModule, - MatCardModule, - SharedComponentsModule - ], - declarations: [ - StoriesListComponent, - SubtopicsListComponent - ], - entryComponents: [ - StoriesListComponent, - SubtopicsListComponent - ], - exports: [ - StoriesListComponent, - SubtopicsListComponent - ], + imports: [CommonModule, MatCardModule, SharedComponentsModule], + declarations: [StoriesListComponent, SubtopicsListComponent], + entryComponents: [StoriesListComponent, SubtopicsListComponent], + exports: [StoriesListComponent, SubtopicsListComponent], }) - -export class TopicPlayerViewerCommonModule { } +export class TopicPlayerViewerCommonModule {} diff --git a/core/templates/pages/topics-and-skills-dashboard-page/modals/assign-skill-to-topic-modal.component.spec.ts b/core/templates/pages/topics-and-skills-dashboard-page/modals/assign-skill-to-topic-modal.component.spec.ts index 2421f654a856..64e5610c087b 100644 --- a/core/templates/pages/topics-and-skills-dashboard-page/modals/assign-skill-to-topic-modal.component.spec.ts +++ b/core/templates/pages/topics-and-skills-dashboard-page/modals/assign-skill-to-topic-modal.component.spec.ts @@ -16,11 +16,11 @@ * @fileoverview Unit tests for Assign Skill To Topic Modal. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { FormsModule } from '@angular/forms'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { SelectTopicsComponent } from '../topic-selector/select-topics.component'; -import { AssignSkillToTopicModalComponent } from './assign-skill-to-topic-modal.component'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {FormsModule} from '@angular/forms'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {SelectTopicsComponent} from '../topic-selector/select-topics.component'; +import {AssignSkillToTopicModalComponent} from './assign-skill-to-topic-modal.component'; describe('Assign Skill to Topic Modal Component', () => { let fixture: ComponentFixture; @@ -28,16 +28,9 @@ describe('Assign Skill to Topic Modal Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - FormsModule - ], - declarations: [ - AssignSkillToTopicModalComponent, - SelectTopicsComponent - ], - providers: [ - NgbActiveModal - ] + imports: [FormsModule], + declarations: [AssignSkillToTopicModalComponent, SelectTopicsComponent], + providers: [NgbActiveModal], }).compileComponents(); })); diff --git a/core/templates/pages/topics-and-skills-dashboard-page/modals/assign-skill-to-topic-modal.component.ts b/core/templates/pages/topics-and-skills-dashboard-page/modals/assign-skill-to-topic-modal.component.ts index bdbe4c477a34..1159a4cb0a6c 100644 --- a/core/templates/pages/topics-and-skills-dashboard-page/modals/assign-skill-to-topic-modal.component.ts +++ b/core/templates/pages/topics-and-skills-dashboard-page/modals/assign-skill-to-topic-modal.component.ts @@ -16,14 +16,14 @@ * @fileoverview Component for Assign Skill To Topic Modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { CreatorTopicSummary } from 'domain/topic/creator-topic-summary.model'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {CreatorTopicSummary} from 'domain/topic/creator-topic-summary.model'; @Component({ selector: 'oppia-assign-skill-to-topic-modal', - templateUrl: './assign-skill-to-topic-modal.component.html' + templateUrl: './assign-skill-to-topic-modal.component.html', }) export class AssignSkillToTopicModalComponent extends ConfirmOrCancelModal { // This property is initialized using component interactions @@ -32,9 +32,7 @@ export class AssignSkillToTopicModalComponent extends ConfirmOrCancelModal { topicSummaries!: CreatorTopicSummary[]; selectedTopicIds: string[] = []; - constructor( - private ngbActiveModal: NgbActiveModal - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } } diff --git a/core/templates/pages/topics-and-skills-dashboard-page/modals/create-new-skill-modal.component.spec.ts b/core/templates/pages/topics-and-skills-dashboard-page/modals/create-new-skill-modal.component.spec.ts index 6af5eb7ac40a..13bf7bb77d86 100644 --- a/core/templates/pages/topics-and-skills-dashboard-page/modals/create-new-skill-modal.component.spec.ts +++ b/core/templates/pages/topics-and-skills-dashboard-page/modals/create-new-skill-modal.component.spec.ts @@ -16,43 +16,37 @@ * @fileoverview Unit Test for create new skill modal. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ChangeDetectorRef, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { FormsModule } from '@angular/forms'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { SkillCreationService } from 'components/entity-creation-services/skill-creation.service'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { SkillObjectFactory } from 'domain/skill/SkillObjectFactory'; -import { SkillEditorStateService } from 'pages/skill-editor-page/services/skill-editor-state.service'; -import { ContextService } from 'services/context.service'; -import { CreateNewSkillModalComponent } from './create-new-skill-modal.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ChangeDetectorRef, NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {FormsModule} from '@angular/forms'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {SkillCreationService} from 'components/entity-creation-services/skill-creation.service'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {SkillObjectFactory} from 'domain/skill/SkillObjectFactory'; +import {SkillEditorStateService} from 'pages/skill-editor-page/services/skill-editor-state.service'; +import {ContextService} from 'services/context.service'; +import {CreateNewSkillModalComponent} from './create-new-skill-modal.component'; describe('Create new skill modal', () => { let fixture: ComponentFixture; let componentInstance: CreateNewSkillModalComponent; let contextService: ContextService; let skillObjectFactory: SkillObjectFactory; - let testObj: SubtitledHtml = SubtitledHtml - .createDefault('test_html', 'test_id'); + let testObj: SubtitledHtml = SubtitledHtml.createDefault( + 'test_html', + 'test_id' + ); let ngbActiveModal: NgbActiveModal; let skillEditorStateService: SkillEditorStateService; let skillCreationService: SkillCreationService; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - FormsModule - ], - declarations: [ - CreateNewSkillModalComponent - ], - providers: [ - NgbActiveModal, - ChangeDetectorRef, - ], - schemas: [NO_ERRORS_SCHEMA] + imports: [HttpClientTestingModule, FormsModule], + declarations: [CreateNewSkillModalComponent], + providers: [NgbActiveModal, ChangeDetectorRef], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -73,15 +67,17 @@ describe('Create new skill modal', () => { it('should initialize', () => { spyOn(contextService, 'setImageSaveDestinationToLocalStorage'); componentInstance.ngOnInit(); - expect(contextService.setImageSaveDestinationToLocalStorage). - toHaveBeenCalled(); + expect( + contextService.setImageSaveDestinationToLocalStorage + ).toHaveBeenCalled(); }); it('should update explanation', () => { componentInstance.bindableDict.displayedConceptCardExplanation = 'text1'; componentInstance.updateExplanation('text2'); - expect(componentInstance.bindableDict.displayedConceptCardExplanation). - toEqual('text2'); + expect( + componentInstance.bindableDict.displayedConceptCardExplanation + ).toEqual('text2'); }); it('should open concept card explanation editor', () => { @@ -90,7 +86,7 @@ describe('Create new skill modal', () => { }); it('should get html schema', () => { - let schema: { type: string } = { type: 'html' }; + let schema: {type: string} = {type: 'html'}; componentInstance.HTML_SCHEMA = schema; expect(componentInstance.getHtmlSchema()).toEqual(schema); }); @@ -101,27 +97,31 @@ describe('Create new skill modal', () => { componentInstance.setErrorMessageIfNeeded(); expect(componentInstance.errorMsg).toEqual( 'Please use a non-empty description consisting of ' + - 'alphanumeric characters, spaces and/or hyphens.'); + 'alphanumeric characters, spaces and/or hyphens.' + ); componentInstance.skillDescriptionExists = true; componentInstance.setErrorMessageIfNeeded(); expect(componentInstance.errorMsg).toEqual( 'This description already exists. Please choose a ' + - 'new name or modify the existing skill.'); + 'new name or modify the existing skill.' + ); }); it('should update SkillDescription and check if exists', () => { spyOn(skillEditorStateService, 'updateExistenceOfSkillDescription'); - spyOn(skillCreationService, 'getSkillDescriptionStatus').and - .returnValue('not_disabled'); + spyOn(skillCreationService, 'getSkillDescriptionStatus').and.returnValue( + 'not_disabled' + ); spyOn(skillCreationService, 'markChangeInSkillDescription'); componentInstance.newSkillDescription = 'not_empty'; componentInstance.updateSkillDescriptionAndCheckIfExists(); componentInstance._skillDescriptionExistsCallback(false); expect(componentInstance.rubrics[1].getExplanations()).toEqual([ - componentInstance.newSkillDescription + componentInstance.newSkillDescription, ]); - expect(skillCreationService.markChangeInSkillDescription) - .toHaveBeenCalled(); + expect( + skillCreationService.markChangeInSkillDescription + ).toHaveBeenCalled(); expect(componentInstance.skillDescriptionExists).toBeFalse(); }); @@ -134,11 +134,12 @@ describe('Create new skill modal', () => { it('should save concept card explanation', () => { spyOn(SubtitledHtml, 'createDefault').and.returnValue(testObj); componentInstance.saveConceptCardExplanation(); - expect(componentInstance.bindableDict.displayedConceptCardExplanation) - .toEqual('test_html'); + expect( + componentInstance.bindableDict.displayedConceptCardExplanation + ).toEqual('test_html'); expect(componentInstance.newExplanationObject).toEqual({ html: 'test_html', - content_id: 'test_id' + content_id: 'test_id', }); }); @@ -150,7 +151,7 @@ describe('Create new skill modal', () => { expect(ngbActiveModal.close).toHaveBeenCalledWith({ description: componentInstance.newSkillDescription, rubrics: componentInstance.rubrics, - explanation: componentInstance.newExplanationObject + explanation: componentInstance.newExplanationObject, }); }); diff --git a/core/templates/pages/topics-and-skills-dashboard-page/modals/create-new-skill-modal.component.ts b/core/templates/pages/topics-and-skills-dashboard-page/modals/create-new-skill-modal.component.ts index 3c0d7c8ad625..94af8ce7e455 100644 --- a/core/templates/pages/topics-and-skills-dashboard-page/modals/create-new-skill-modal.component.ts +++ b/core/templates/pages/topics-and-skills-dashboard-page/modals/create-new-skill-modal.component.ts @@ -16,36 +16,39 @@ * @fileoverview Modal for the creating new skill. */ -import { ChangeDetectorRef, Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { AppConstants } from 'app.constants'; -import { SkillCreationService } from 'components/entity-creation-services/skill-creation.service'; -import { SubtitledHtml, SubtitledHtmlBackendDict } from 'domain/exploration/subtitled-html.model'; -import { Rubric } from 'domain/skill/rubric.model'; -import { SkillObjectFactory } from 'domain/skill/SkillObjectFactory'; -import { SkillEditorStateService } from 'pages/skill-editor-page/services/skill-editor-state.service'; -import { ContextService } from 'services/context.service'; -import { ImageLocalStorageService } from 'services/image-local-storage.service'; -import { TopicsAndSkillsDashboardPageConstants } from '../topics-and-skills-dashboard-page.constants'; +import {ChangeDetectorRef, Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {AppConstants} from 'app.constants'; +import {SkillCreationService} from 'components/entity-creation-services/skill-creation.service'; +import { + SubtitledHtml, + SubtitledHtmlBackendDict, +} from 'domain/exploration/subtitled-html.model'; +import {Rubric} from 'domain/skill/rubric.model'; +import {SkillObjectFactory} from 'domain/skill/SkillObjectFactory'; +import {SkillEditorStateService} from 'pages/skill-editor-page/services/skill-editor-state.service'; +import {ContextService} from 'services/context.service'; +import {ImageLocalStorageService} from 'services/image-local-storage.service'; +import {TopicsAndSkillsDashboardPageConstants} from '../topics-and-skills-dashboard-page.constants'; @Component({ selector: 'oppia-create-new-skill-modal', - templateUrl: './create-new-skill-modal.component.html' + templateUrl: './create-new-skill-modal.component.html', }) export class CreateNewSkillModalComponent { rubrics = [ Rubric.create(AppConstants.SKILL_DIFFICULTIES[0], []), Rubric.create(AppConstants.SKILL_DIFFICULTIES[1], ['']), - Rubric.create(AppConstants.SKILL_DIFFICULTIES[2], [])]; + Rubric.create(AppConstants.SKILL_DIFFICULTIES[2], []), + ]; newSkillDescription: string = ''; errorMsg: string = ''; skillDescriptionExists: boolean = true; conceptCardExplanationEditorIsShown: boolean = false; bindableDict = {displayedConceptCardExplanation: ''}; - HTML_SCHEMA: {type: string} = { type: 'html' }; - MAX_CHARS_IN_SKILL_DESCRIPTION = ( - AppConstants.MAX_CHARS_IN_SKILL_DESCRIPTION); + HTML_SCHEMA: {type: string} = {type: 'html'}; + MAX_CHARS_IN_SKILL_DESCRIPTION = AppConstants.MAX_CHARS_IN_SKILL_DESCRIPTION; // This property is initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see @@ -77,22 +80,22 @@ export class CreateNewSkillModalComponent { this.conceptCardExplanationEditorIsShown = true; } - getHtmlSchema(): { type: string } { + getHtmlSchema(): {type: string} { return this.HTML_SCHEMA; } setErrorMessageIfNeeded(): void { if ( - !this.skillObjectFactory.hasValidDescription( - this.newSkillDescription)) { - this.errorMsg = ( + !this.skillObjectFactory.hasValidDescription(this.newSkillDescription) + ) { + this.errorMsg = 'Please use a non-empty description consisting of ' + - 'alphanumeric characters, spaces and/or hyphens.'); + 'alphanumeric characters, spaces and/or hyphens.'; } if (this.skillDescriptionExists) { - this.errorMsg = ( + this.errorMsg = 'This description already exists. Please choose a ' + - 'new name or modify the existing skill.'); + 'new name or modify the existing skill.'; } } @@ -112,8 +115,9 @@ export class CreateNewSkillModalComponent { } if ( this.skillCreationService.getSkillDescriptionStatus() !== - TopicsAndSkillsDashboardPageConstants.SKILL_DESCRIPTION_STATUS_VALUES - .STATUS_DISABLED) { + TopicsAndSkillsDashboardPageConstants.SKILL_DESCRIPTION_STATUS_VALUES + .STATUS_DISABLED + ) { this.rubrics[1].setExplanations([this.newSkillDescription]); this.skillCreationService.markChangeInSkillDescription(); } @@ -124,13 +128,12 @@ export class CreateNewSkillModalComponent { } saveConceptCardExplanation(): void { - const explanationObject: SubtitledHtml = - SubtitledHtml.createDefault( + const explanationObject: SubtitledHtml = SubtitledHtml.createDefault( this.bindableDict.displayedConceptCardExplanation, - AppConstants.COMPONENT_NAME_EXPLANATION); + AppConstants.COMPONENT_NAME_EXPLANATION + ); this.newExplanationObject = explanationObject.toBackendDict(); - this.bindableDict.displayedConceptCardExplanation = ( - explanationObject.html); + this.bindableDict.displayedConceptCardExplanation = explanationObject.html; } createNewSkill(): void { @@ -142,7 +145,7 @@ export class CreateNewSkillModalComponent { this.ngbActiveModal.close({ description: this.newSkillDescription, rubrics: this.rubrics, - explanation: this.newExplanationObject + explanation: this.newExplanationObject, }); } diff --git a/core/templates/pages/topics-and-skills-dashboard-page/modals/create-new-topic-modal.component.spec.ts b/core/templates/pages/topics-and-skills-dashboard-page/modals/create-new-topic-modal.component.spec.ts index dbd6683ad5f4..ce7caf5be597 100644 --- a/core/templates/pages/topics-and-skills-dashboard-page/modals/create-new-topic-modal.component.spec.ts +++ b/core/templates/pages/topics-and-skills-dashboard-page/modals/create-new-topic-modal.component.spec.ts @@ -16,16 +16,16 @@ * @fileoverview Unit Test for create new topic modal. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { FormsModule } from '@angular/forms'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { TopicEditorStateService } from 'pages/topic-editor-page/services/topic-editor-state.service'; -import { ContextService } from 'services/context.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { ImageLocalStorageService } from 'services/image-local-storage.service'; -import { CreateNewTopicModalComponent } from './create-new-topic-modal.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {FormsModule} from '@angular/forms'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {TopicEditorStateService} from 'pages/topic-editor-page/services/topic-editor-state.service'; +import {ContextService} from 'services/context.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {ImageLocalStorageService} from 'services/image-local-storage.service'; +import {CreateNewTopicModalComponent} from './create-new-topic-modal.component'; describe('Create new topic modal', () => { let fixture: ComponentFixture; @@ -38,30 +38,25 @@ describe('Create new topic modal', () => { class MockWindowRef { nativeWindow = { location: { - hostname: '' - } + hostname: '', + }, }; } beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - FormsModule - ], - declarations: [ - CreateNewTopicModalComponent, - ], + imports: [HttpClientTestingModule, FormsModule], + declarations: [CreateNewTopicModalComponent], providers: [ NgbActiveModal, ImageLocalStorageService, TopicEditorStateService, { provide: WindowRef, - useClass: MockWindowRef - } + useClass: MockWindowRef, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -81,8 +76,9 @@ describe('Create new topic modal', () => { it('should intialize', () => { spyOn(contextService, 'setImageSaveDestinationToLocalStorage'); componentInstance.ngOnInit(); - expect(contextService.setImageSaveDestinationToLocalStorage) - .toHaveBeenCalled(); + expect( + contextService.setImageSaveDestinationToLocalStorage + ).toHaveBeenCalled(); }); it('should save new topic', () => { @@ -99,78 +95,92 @@ describe('Create new topic modal', () => { it('should validate newly created topic', () => { spyOn(componentInstance.newlyCreatedTopic, 'isValid').and.returnValue(true); - spyOn(imageLocalStorageService, 'getStoredImagesData').and.returnValue([{ - filename: '', - imageBlob: null - }]); + spyOn(imageLocalStorageService, 'getStoredImagesData').and.returnValue([ + { + filename: '', + imageBlob: null, + }, + ]); expect(componentInstance.isValid()).toBeTrue(); }); it('should update topic url framgent', () => { componentInstance.newlyCreatedTopic.urlFragment = 'not-empty'; - spyOn(topicEditorStateService, 'updateExistenceOfTopicUrlFragment') - .and.callFake((urlFragment: string, callb: () => void) => { - callb(); - }); - spyOn(topicEditorStateService, 'getTopicWithUrlFragmentExists') - .and.returnValue(true); + spyOn( + topicEditorStateService, + 'updateExistenceOfTopicUrlFragment' + ).and.callFake((urlFragment: string, callb: () => void) => { + callb(); + }); + spyOn( + topicEditorStateService, + 'getTopicWithUrlFragmentExists' + ).and.returnValue(true); componentInstance.onTopicUrlFragmentChange(); - expect(topicEditorStateService.updateExistenceOfTopicUrlFragment) - .toHaveBeenCalled(); - expect(topicEditorStateService.getTopicWithUrlFragmentExists) - .toHaveBeenCalled(); + expect( + topicEditorStateService.updateExistenceOfTopicUrlFragment + ).toHaveBeenCalled(); + expect( + topicEditorStateService.getTopicWithUrlFragmentExists + ).toHaveBeenCalled(); }); it('should not update topic url with wrong framgent', () => { componentInstance.newlyCreatedTopic.urlFragment = 'not empty'; - spyOn(topicEditorStateService, 'updateExistenceOfTopicUrlFragment') - .and.callFake((urlFragment, successCallback, errorCallback) => { - errorCallback(); - }); + spyOn( + topicEditorStateService, + 'updateExistenceOfTopicUrlFragment' + ).and.callFake((urlFragment, successCallback, errorCallback) => { + errorCallback(); + }); spyOn(topicEditorStateService, 'getTopicWithUrlFragmentExists'); componentInstance.onTopicUrlFragmentChange(); - expect(topicEditorStateService.updateExistenceOfTopicUrlFragment) - .toHaveBeenCalled(); - expect(topicEditorStateService.getTopicWithUrlFragmentExists) - .not.toHaveBeenCalled(); + expect( + topicEditorStateService.updateExistenceOfTopicUrlFragment + ).toHaveBeenCalled(); + expect( + topicEditorStateService.getTopicWithUrlFragmentExists + ).not.toHaveBeenCalled(); }); it('should update topic name', () => { componentInstance.newlyCreatedTopic.name = 'not-empty'; - spyOn(topicEditorStateService, 'updateExistenceOfTopicName') - .and.callFake((topicName: string, callb: () => void) => { + spyOn(topicEditorStateService, 'updateExistenceOfTopicName').and.callFake( + (topicName: string, callb: () => void) => { callb(); - }); - spyOn(topicEditorStateService, 'getTopicWithNameExists') - .and.returnValue(true); + } + ); + spyOn(topicEditorStateService, 'getTopicWithNameExists').and.returnValue( + true + ); componentInstance.onTopicNameChange(); - expect(topicEditorStateService.updateExistenceOfTopicName) - .toHaveBeenCalled(); + expect( + topicEditorStateService.updateExistenceOfTopicName + ).toHaveBeenCalled(); expect(topicEditorStateService.getTopicWithNameExists).toHaveBeenCalled(); }); - it('should not update existence of topic name if not provided by user', - () => { - componentInstance.newlyCreatedTopic.name = ''; - spyOn(topicEditorStateService, 'updateExistenceOfTopicName'); - componentInstance.onTopicNameChange(); - expect(topicEditorStateService.updateExistenceOfTopicName) - .not.toHaveBeenCalled(); - }); + it('should not update existence of topic name if not provided by user', () => { + componentInstance.newlyCreatedTopic.name = ''; + spyOn(topicEditorStateService, 'updateExistenceOfTopicName'); + componentInstance.onTopicNameChange(); + expect( + topicEditorStateService.updateExistenceOfTopicName + ).not.toHaveBeenCalled(); + }); - it('should remove unnecessary spaces from topic name', - () => { - componentInstance.newlyCreatedTopic.name = ' extra spaces '; - componentInstance.onTopicNameChange(); - expect(componentInstance.newlyCreatedTopic.name).toBe('extra spaces'); - }); + it('should remove unnecessary spaces from topic name', () => { + componentInstance.newlyCreatedTopic.name = ' extra spaces '; + componentInstance.onTopicNameChange(); + expect(componentInstance.newlyCreatedTopic.name).toBe('extra spaces'); + }); - it('should not update topic url fragment if not provided by user', - () => { - componentInstance.newlyCreatedTopic.urlFragment = ''; - spyOn(topicEditorStateService, 'updateExistenceOfTopicUrlFragment'); - componentInstance.onTopicUrlFragmentChange(); - expect(topicEditorStateService.updateExistenceOfTopicUrlFragment) - .not.toHaveBeenCalled(); - }); + it('should not update topic url fragment if not provided by user', () => { + componentInstance.newlyCreatedTopic.urlFragment = ''; + spyOn(topicEditorStateService, 'updateExistenceOfTopicUrlFragment'); + componentInstance.onTopicUrlFragmentChange(); + expect( + topicEditorStateService.updateExistenceOfTopicUrlFragment + ).not.toHaveBeenCalled(); + }); }); diff --git a/core/templates/pages/topics-and-skills-dashboard-page/modals/create-new-topic-modal.component.ts b/core/templates/pages/topics-and-skills-dashboard-page/modals/create-new-topic-modal.component.ts index 227ffcf1e1bd..738e9a8b32b9 100644 --- a/core/templates/pages/topics-and-skills-dashboard-page/modals/create-new-topic-modal.component.ts +++ b/core/templates/pages/topics-and-skills-dashboard-page/modals/create-new-topic-modal.component.ts @@ -16,19 +16,19 @@ * @fileoverview Modal for the creating new topic. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { AppConstants } from 'app.constants'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { NewlyCreatedTopic } from 'domain/topics_and_skills_dashboard/newly-created-topic.model'; -import { TopicEditorStateService } from 'pages/topic-editor-page/services/topic-editor-state.service'; -import { ContextService } from 'services/context.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { ImageLocalStorageService } from 'services/image-local-storage.service'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {AppConstants} from 'app.constants'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {NewlyCreatedTopic} from 'domain/topics_and_skills_dashboard/newly-created-topic.model'; +import {TopicEditorStateService} from 'pages/topic-editor-page/services/topic-editor-state.service'; +import {ContextService} from 'services/context.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {ImageLocalStorageService} from 'services/image-local-storage.service'; @Component({ selector: 'oppia-create-new-topic-modal', - templateUrl: './create-new-topic-modal.component.html' + templateUrl: './create-new-topic-modal.component.html', }) export class CreateNewTopicModalComponent extends ConfirmOrCancelModal { allowedBgColors: object = AppConstants.ALLOWED_THUMBNAIL_BG_COLORS.topic; @@ -36,11 +36,11 @@ export class CreateNewTopicModalComponent extends ConfirmOrCancelModal { newlyCreatedTopic: NewlyCreatedTopic = NewlyCreatedTopic.createDefault(); hostname: string = this.windowRef.nativeWindow.location.hostname; MAX_CHARS_IN_TOPIC_NAME: number = AppConstants.MAX_CHARS_IN_TOPIC_NAME; - MAX_CHARS_IN_TOPIC_DESCRIPTION: number = ( - AppConstants.MAX_CHARS_IN_TOPIC_DESCRIPTION); + MAX_CHARS_IN_TOPIC_DESCRIPTION: number = + AppConstants.MAX_CHARS_IN_TOPIC_DESCRIPTION; - MAX_CHARS_IN_TOPIC_URL_FRAGMENT = ( - AppConstants.MAX_CHARS_IN_TOPIC_URL_FRAGMENT); + MAX_CHARS_IN_TOPIC_URL_FRAGMENT = + AppConstants.MAX_CHARS_IN_TOPIC_URL_FRAGMENT; topicUrlFragmentExists: boolean = false; topicNameExists: boolean = false; @@ -72,7 +72,7 @@ export class CreateNewTopicModalComponent extends ConfirmOrCancelModal { isValid(): boolean { return Boolean( this.newlyCreatedTopic.isValid() && - this.imageLocalStorageService.getStoredImagesData().length > 0 + this.imageLocalStorageService.getStoredImagesData().length > 0 ); } @@ -82,12 +82,15 @@ export class CreateNewTopicModalComponent extends ConfirmOrCancelModal { } this.topicEditorStateService.updateExistenceOfTopicUrlFragment( - this.newlyCreatedTopic.urlFragment, () => { - this.topicUrlFragmentExists = ( - this.topicEditorStateService.getTopicWithUrlFragmentExists()); - }, () => { + this.newlyCreatedTopic.urlFragment, + () => { + this.topicUrlFragmentExists = + this.topicEditorStateService.getTopicWithUrlFragmentExists(); + }, + () => { return; - }); + } + ); } onTopicNameChange(): void { @@ -96,11 +99,13 @@ export class CreateNewTopicModalComponent extends ConfirmOrCancelModal { } this.newlyCreatedTopic.name = this.newlyCreatedTopic.name - .replace(/\s+/g, ' ').trim(); + .replace(/\s+/g, ' ') + .trim(); this.topicEditorStateService.updateExistenceOfTopicName( - this.newlyCreatedTopic.name, () => { - this.topicNameExists = ( - this.topicEditorStateService.getTopicWithNameExists()); + this.newlyCreatedTopic.name, + () => { + this.topicNameExists = + this.topicEditorStateService.getTopicWithNameExists(); } ); } diff --git a/core/templates/pages/topics-and-skills-dashboard-page/modals/delete-skill-modal.component.spec.ts b/core/templates/pages/topics-and-skills-dashboard-page/modals/delete-skill-modal.component.spec.ts index fe6a1d053e40..d628f5372bf2 100644 --- a/core/templates/pages/topics-and-skills-dashboard-page/modals/delete-skill-modal.component.spec.ts +++ b/core/templates/pages/topics-and-skills-dashboard-page/modals/delete-skill-modal.component.spec.ts @@ -16,13 +16,28 @@ * @fileoverview Unit tests for Delete Skill Modal. */ -import { ComponentFixture, fakeAsync, TestBed, waitForAsync, tick } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { AssignedSkill, AssignedSkillBackendDict } from 'domain/skill/assigned-skill.model'; -import { TopicsAndSkillsDashboardBackendApiService, TopicIdToDiagnosticTestSkillIdsResponse } from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { DeleteSkillModalComponent, TopicAssignmentsSummary } from './delete-skill-modal.component'; -import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { + ComponentFixture, + fakeAsync, + TestBed, + waitForAsync, + tick, +} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import { + AssignedSkill, + AssignedSkillBackendDict, +} from 'domain/skill/assigned-skill.model'; +import { + TopicsAndSkillsDashboardBackendApiService, + TopicIdToDiagnosticTestSkillIdsResponse, +} from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import { + DeleteSkillModalComponent, + TopicAssignmentsSummary, +} from './delete-skill-modal.component'; +import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; describe('Delete Skill Modal Component', () => { let fixture: ComponentFixture; @@ -32,18 +47,18 @@ describe('Delete Skill Modal Component', () => { topic_id: 'topicId1', topic_name: 'topicName', topic_version: 1, - subtopic_id: 2 + subtopic_id: 2, }; const testSkills: AssignedSkill[] = [ - AssignedSkill.createFromBackendDict(skillBackendDict) + AssignedSkill.createFromBackendDict(skillBackendDict), ]; - const testTopicIdToDiagnosticTestSkillIds: - TopicIdToDiagnosticTestSkillIdsResponse = { + const testTopicIdToDiagnosticTestSkillIds: TopicIdToDiagnosticTestSkillIdsResponse = + { topicIdToDiagnosticTestSkillIds: { - topicId1: [] - } + topicId1: [], + }, }; class MockTopicsAndSkillsDashboardBackendApiService { @@ -51,36 +66,33 @@ describe('Delete Skill Modal Component', () => { return { then: (callback: (resp: AssignedSkill[]) => void) => { callback(testSkills); - } + }, }; } fetchTopicIdToDiagnosticTestSkillIdsAsync(topicIds: string[]) { return { - then: (callback: ( - resp: TopicIdToDiagnosticTestSkillIdsResponse) => void) => { + then: ( + callback: (resp: TopicIdToDiagnosticTestSkillIdsResponse) => void + ) => { callback(testTopicIdToDiagnosticTestSkillIds); - } + }, }; } } beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - MatProgressSpinnerModule - ], - declarations: [ - DeleteSkillModalComponent - ], + imports: [MatProgressSpinnerModule], + declarations: [DeleteSkillModalComponent], providers: [ NgbActiveModal, { provide: TopicsAndSkillsDashboardBackendApiService, - useClass: MockTopicsAndSkillsDashboardBackendApiService + useClass: MockTopicsAndSkillsDashboardBackendApiService, }, - UrlInterpolationService - ] + UrlInterpolationService, + ], }).compileComponents(); })); @@ -122,15 +134,18 @@ describe('Delete Skill Modal Component', () => { it('should allow skill deletion', fakeAsync(() => { let topicsAndSkillsDashboardBackendApiService = TestBed.inject( - TopicsAndSkillsDashboardBackendApiService); + TopicsAndSkillsDashboardBackendApiService + ); componentInstance.skillId = 'skill_id'; componentInstance.topicsAssignmentsAreFetched = false; spyOn( topicsAndSkillsDashboardBackendApiService, 'fetchTopicIdToDiagnosticTestSkillIdsAsync' - ).and.returnValue(Promise.resolve({ - topicIdToDiagnosticTestSkillIds: {topicId1: []} - })); + ).and.returnValue( + Promise.resolve({ + topicIdToDiagnosticTestSkillIds: {topicId1: []}, + }) + ); componentInstance.fetchTopicAssignmentsForSkill(); tick(50); expect(componentInstance.skillCanBeDeleted).toBeTrue(); @@ -138,10 +153,11 @@ describe('Delete Skill Modal Component', () => { it( 'should not be able to delete the skill when the skill is linked to the ' + - 'diagnostic test of any topic', + 'diagnostic test of any topic', fakeAsync(() => { let topicsAndSkillsDashboardBackendApiService = TestBed.inject( - TopicsAndSkillsDashboardBackendApiService); + TopicsAndSkillsDashboardBackendApiService + ); componentInstance.skillId = 'skill_id'; componentInstance.topicsAssignmentsAreFetched = false; @@ -150,28 +166,32 @@ describe('Delete Skill Modal Component', () => { spyOn( topicsAndSkillsDashboardBackendApiService, 'fetchTopicIdToDiagnosticTestSkillIdsAsync' - ).and.returnValue(Promise.resolve({ - topicIdToDiagnosticTestSkillIds: { - topicId1: ['skill_id'], - topicId2: [] - } - })); + ).and.returnValue( + Promise.resolve({ + topicIdToDiagnosticTestSkillIds: { + topicId1: ['skill_id'], + topicId2: [], + }, + }) + ); componentInstance.fetchTopicAssignmentsForSkill(); tick(); expect(componentInstance.skillCanBeDeleted).toBeFalse(); - })); + }) + ); it('should get topic editor url', () => { - spyOn(urlInterpolationService, 'interpolateUrl').and - .returnValue('test_url'); + spyOn(urlInterpolationService, 'interpolateUrl').and.returnValue( + 'test_url' + ); let topicsAssignment: TopicAssignmentsSummary = { subtopicId: 1, topicVersion: 1, - topicId: 'topicID' + topicId: 'topicID', }; - expect( - componentInstance.getTopicEditorUrl(topicsAssignment)).toEqual( - 'test_url'); + expect(componentInstance.getTopicEditorUrl(topicsAssignment)).toEqual( + 'test_url' + ); }); }); diff --git a/core/templates/pages/topics-and-skills-dashboard-page/modals/delete-skill-modal.component.ts b/core/templates/pages/topics-and-skills-dashboard-page/modals/delete-skill-modal.component.ts index ebbfde230801..2123bfa27eed 100644 --- a/core/templates/pages/topics-and-skills-dashboard-page/modals/delete-skill-modal.component.ts +++ b/core/templates/pages/topics-and-skills-dashboard-page/modals/delete-skill-modal.component.ts @@ -16,12 +16,15 @@ * @fileoverview Component for Delete Skill Modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { AssignedSkill } from 'domain/skill/assigned-skill.model'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { TopicsAndSkillsDashboardBackendApiService, TopicIdToDiagnosticTestSkillIdsResponse } from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {AssignedSkill} from 'domain/skill/assigned-skill.model'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import { + TopicsAndSkillsDashboardBackendApiService, + TopicIdToDiagnosticTestSkillIdsResponse, +} from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; export interface TopicAssignmentsSummary { subtopicId: number; @@ -35,7 +38,7 @@ export interface TopicNameToTopicAssignments { @Component({ selector: 'oppia-delete-skill-modal', - templateUrl: './delete-skill-modal.component.html' + templateUrl: './delete-skill-modal.component.html', }) export class DeleteSkillModalComponent extends ConfirmOrCancelModal { // These properties are initialized using Angular lifecycle hooks @@ -50,50 +53,50 @@ export class DeleteSkillModalComponent extends ConfirmOrCancelModal { constructor( private ngbActiveModal: NgbActiveModal, - private topicsAndSkillsDashboardBackendApiService: - TopicsAndSkillsDashboardBackendApiService, + private topicsAndSkillsDashboardBackendApiService: TopicsAndSkillsDashboardBackendApiService, private urlInterpolationService: UrlInterpolationService ) { super(ngbActiveModal); } fetchTopicIdToDiagnosticTestSkillIds( - topicAssignments: AssignedSkill[]): void { + topicAssignments: AssignedSkill[] + ): void { let allTopicIds = []; for (let topic of topicAssignments) { allTopicIds.push(topic.topicId); } this.ineligibleTopicNameToTopicAssignments = {}; this.topicsAndSkillsDashboardBackendApiService - .fetchTopicIdToDiagnosticTestSkillIdsAsync(allTopicIds).then( - (responseDict: TopicIdToDiagnosticTestSkillIdsResponse) => { - for (let topic of topicAssignments) { - let diagnosticTestSkillIds = ( - responseDict.topicIdToDiagnosticTestSkillIds[topic.topicId]); + .fetchTopicIdToDiagnosticTestSkillIdsAsync(allTopicIds) + .then((responseDict: TopicIdToDiagnosticTestSkillIdsResponse) => { + for (let topic of topicAssignments) { + let diagnosticTestSkillIds = + responseDict.topicIdToDiagnosticTestSkillIds[topic.topicId]; - if ( - diagnosticTestSkillIds.length === 1 && - diagnosticTestSkillIds.indexOf(this.skillId) !== -1 - ) { - this.ineligibleTopicNameToTopicAssignments[topic.topicName] = { - topicId: topic.topicId, - subtopicId: topic.subtopicId, - topicVersion: topic.topicVersion - }; - this.skillCanBeDeleted = false; - } + if ( + diagnosticTestSkillIds.length === 1 && + diagnosticTestSkillIds.indexOf(this.skillId) !== -1 + ) { + this.ineligibleTopicNameToTopicAssignments[topic.topicName] = { + topicId: topic.topicId, + subtopicId: topic.subtopicId, + topicVersion: topic.topicVersion, + }; + this.skillCanBeDeleted = false; } - this.ineligibleTopicsCount = Object.keys( - this.ineligibleTopicNameToTopicAssignments).length; - this.topicsAssignments = topicAssignments; - }); + } + this.ineligibleTopicsCount = Object.keys( + this.ineligibleTopicNameToTopicAssignments + ).length; + this.topicsAssignments = topicAssignments; + }); } fetchTopicAssignmentsForSkill(): void { this.topicsAndSkillsDashboardBackendApiService - .fetchTopicAssignmentsForSkillAsync( - this.skillId - ).then((response: AssignedSkill[]) => { + .fetchTopicAssignmentsForSkillAsync(this.skillId) + .then((response: AssignedSkill[]) => { this.fetchTopicIdToDiagnosticTestSkillIds(response); this.topicsAssignmentsAreFetched = true; }); @@ -103,9 +106,11 @@ export class DeleteSkillModalComponent extends ConfirmOrCancelModal { let topicId = topicAssignment.topicId; const TOPIC_EDITOR_URL_TEMPLATE = '/topic_editor/#/'; return this.urlInterpolationService.interpolateUrl( - TOPIC_EDITOR_URL_TEMPLATE, { - topic_id: topicId - }); + TOPIC_EDITOR_URL_TEMPLATE, + { + topic_id: topicId, + } + ); } ngOnInit(): void { @@ -114,7 +119,7 @@ export class DeleteSkillModalComponent extends ConfirmOrCancelModal { showTopicsAssignments(): boolean { return Boolean( - this.topicsAssignmentsAreFetched && - this.topicsAssignments.length); + this.topicsAssignmentsAreFetched && this.topicsAssignments.length + ); } } diff --git a/core/templates/pages/topics-and-skills-dashboard-page/modals/delete-topic-modal.component.spec.ts b/core/templates/pages/topics-and-skills-dashboard-page/modals/delete-topic-modal.component.spec.ts index 709cbd245a66..5a70fb1b8850 100644 --- a/core/templates/pages/topics-and-skills-dashboard-page/modals/delete-topic-modal.component.spec.ts +++ b/core/templates/pages/topics-and-skills-dashboard-page/modals/delete-topic-modal.component.spec.ts @@ -16,9 +16,9 @@ * @fileoverview Unit tests for Delete Topic Modal. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { DeleteTopicModalComponent } from './delete-topic-modal.component'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {DeleteTopicModalComponent} from './delete-topic-modal.component'; describe('Delete Topic Modal Component', () => { let fixture: ComponentFixture; @@ -26,12 +26,8 @@ describe('Delete Topic Modal Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - DeleteTopicModalComponent - ], - providers: [ - NgbActiveModal - ] + declarations: [DeleteTopicModalComponent], + providers: [NgbActiveModal], }).compileComponents(); })); diff --git a/core/templates/pages/topics-and-skills-dashboard-page/modals/delete-topic-modal.component.ts b/core/templates/pages/topics-and-skills-dashboard-page/modals/delete-topic-modal.component.ts index ca22f2a78639..6faba69f9b8b 100644 --- a/core/templates/pages/topics-and-skills-dashboard-page/modals/delete-topic-modal.component.ts +++ b/core/templates/pages/topics-and-skills-dashboard-page/modals/delete-topic-modal.component.ts @@ -16,13 +16,13 @@ * @fileoverview Component for Delete Topic Modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-delete-topic-modal', - templateUrl: './delete-topic-modal.component.html' + templateUrl: './delete-topic-modal.component.html', }) export class DeleteTopicModalComponent extends ConfirmOrCancelModal { // This property is initialized using Angular lifecycle hooks @@ -30,9 +30,7 @@ export class DeleteTopicModalComponent extends ConfirmOrCancelModal { // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 topicName!: string; - constructor( - private ngbActiveModal: NgbActiveModal - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } } diff --git a/core/templates/pages/topics-and-skills-dashboard-page/modals/unassign-skill-from-topics-modal.component.spec.ts b/core/templates/pages/topics-and-skills-dashboard-page/modals/unassign-skill-from-topics-modal.component.spec.ts index 2703945492da..5ed4a86473eb 100644 --- a/core/templates/pages/topics-and-skills-dashboard-page/modals/unassign-skill-from-topics-modal.component.spec.ts +++ b/core/templates/pages/topics-and-skills-dashboard-page/modals/unassign-skill-from-topics-modal.component.spec.ts @@ -16,13 +16,23 @@ * @fileoverview Unit tests for Unassign Skill Modal. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { AssignedSkillBackendDict, AssignedSkill } from 'domain/skill/assigned-skill.model'; -import { TopicsAndSkillsDashboardBackendApiService, TopicIdToDiagnosticTestSkillIdsResponse } from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; -import { TopicNameToTopicAssignments, UnassignSkillFromTopicsModalComponent, TopicAssignmentsSummary } from './unassign-skill-from-topics-modal.component'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import { + AssignedSkillBackendDict, + AssignedSkill, +} from 'domain/skill/assigned-skill.model'; +import { + TopicsAndSkillsDashboardBackendApiService, + TopicIdToDiagnosticTestSkillIdsResponse, +} from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; +import { + TopicNameToTopicAssignments, + UnassignSkillFromTopicsModalComponent, + TopicAssignmentsSummary, +} from './unassign-skill-from-topics-modal.component'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; describe('Skill unassignment modal', () => { let fixture: ComponentFixture; @@ -33,62 +43,59 @@ describe('Skill unassignment modal', () => { topic_id: 'test_id_1', topic_name: 'Addition', topic_version: 1, - subtopic_id: 2 + subtopic_id: 2, }; let skillBackendDictForFractions: AssignedSkillBackendDict = { topic_id: 'test_id_2', topic_name: 'Fractions', topic_version: 1, - subtopic_id: 2 + subtopic_id: 2, }; const testSkills: AssignedSkill[] = [ AssignedSkill.createFromBackendDict(skillBackendDictForAddition), - AssignedSkill.createFromBackendDict(skillBackendDictForFractions) + AssignedSkill.createFromBackendDict(skillBackendDictForFractions), ]; - const testTopicIdToDiagnosticTestSkillIds: - TopicIdToDiagnosticTestSkillIdsResponse = { - topicIdToDiagnosticTestSkillIds: { - test_id_1: [], - test_id_2: ['skill_id'] - } - }; + const testTopicIdToDiagnosticTestSkillIds: TopicIdToDiagnosticTestSkillIdsResponse = + { + topicIdToDiagnosticTestSkillIds: { + test_id_1: [], + test_id_2: ['skill_id'], + }, + }; class MockTopicsAndSkillsDashboardBackendApiService { fetchTopicAssignmentsForSkillAsync(skillId: string) { return { then: (callback: (resp: AssignedSkill[]) => void) => { callback(testSkills); - } + }, }; } fetchTopicIdToDiagnosticTestSkillIdsAsync(topicIds: string[]) { return { - then: (callback: ( - resp: TopicIdToDiagnosticTestSkillIdsResponse) => void) => { + then: ( + callback: (resp: TopicIdToDiagnosticTestSkillIdsResponse) => void + ) => { callback(testTopicIdToDiagnosticTestSkillIds); - } + }, }; } } beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - MatProgressSpinnerModule - ], - declarations: [ - UnassignSkillFromTopicsModalComponent - ], + imports: [MatProgressSpinnerModule], + declarations: [UnassignSkillFromTopicsModalComponent], providers: [ NgbActiveModal, { provide: TopicsAndSkillsDashboardBackendApiService, - useClass: MockTopicsAndSkillsDashboardBackendApiService + useClass: MockTopicsAndSkillsDashboardBackendApiService, }, - UrlInterpolationService - ] + UrlInterpolationService, + ], }).compileComponents(); })); @@ -118,17 +125,19 @@ describe('Skill unassignment modal', () => { subtopicId: 0, topicVersion: 0, topicId: '', - } + }, }; componentInstance.close(); expect(ngbActiveModal.close).toHaveBeenCalledWith( - componentInstance.selectedTopics); + componentInstance.selectedTopics + ); }); it('should select topic to unassign', () => { componentInstance.selectedTopicToUnassign('abc'); - expect(componentInstance.selectedTopicNames.indexOf('abc')) - .toBeGreaterThan(-1); + expect(componentInstance.selectedTopicNames.indexOf('abc')).toBeGreaterThan( + -1 + ); componentInstance.selectedTopicToUnassign('abc'); expect(componentInstance.selectedTopicNames.indexOf('abc')).toEqual(-1); }); @@ -140,23 +149,25 @@ describe('Skill unassignment modal', () => { assignments[skillBackendDictForAddition.topic_name] = { subtopicId: skillBackendDictForAddition.subtopic_id, topicVersion: skillBackendDictForAddition.topic_version, - topicId: skillBackendDictForAddition.topic_id + topicId: skillBackendDictForAddition.topic_id, }; expect(componentInstance.eligibleTopicNameToTopicAssignments).toEqual( - assignments); + assignments + ); expect(componentInstance.topicsAssignmentsAreFetched).toBeTrue(); }); it('should get topic editor url', () => { - spyOn(urlInterpolationService, 'interpolateUrl').and - .returnValue('test_url'); + spyOn(urlInterpolationService, 'interpolateUrl').and.returnValue( + 'test_url' + ); let topicsAssignment: TopicAssignmentsSummary = { subtopicId: 1, topicVersion: 1, - topicId: 'topicID' + topicId: 'topicID', }; - expect( - componentInstance.getTopicEditorUrl(topicsAssignment)).toEqual( - 'test_url'); + expect(componentInstance.getTopicEditorUrl(topicsAssignment)).toEqual( + 'test_url' + ); }); }); diff --git a/core/templates/pages/topics-and-skills-dashboard-page/modals/unassign-skill-from-topics-modal.component.ts b/core/templates/pages/topics-and-skills-dashboard-page/modals/unassign-skill-from-topics-modal.component.ts index 145096ac67b9..ad97379cc557 100644 --- a/core/templates/pages/topics-and-skills-dashboard-page/modals/unassign-skill-from-topics-modal.component.ts +++ b/core/templates/pages/topics-and-skills-dashboard-page/modals/unassign-skill-from-topics-modal.component.ts @@ -16,12 +16,15 @@ * @fileoverview Component for Unassign Skill Modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; -import { AssignedSkill } from 'domain/skill/assigned-skill.model'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { TopicsAndSkillsDashboardBackendApiService, TopicIdToDiagnosticTestSkillIdsResponse } from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; +import {AssignedSkill} from 'domain/skill/assigned-skill.model'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import { + TopicsAndSkillsDashboardBackendApiService, + TopicIdToDiagnosticTestSkillIdsResponse, +} from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; export interface TopicAssignmentsSummary { subtopicId: number; @@ -35,10 +38,9 @@ export interface TopicNameToTopicAssignments { @Component({ selector: 'oppia-unassign-skill-from-topics-modal', - templateUrl: './unassign-skill-from-topics-modal.component.html' + templateUrl: './unassign-skill-from-topics-modal.component.html', }) -export class UnassignSkillFromTopicsModalComponent - extends ConfirmOrCancelModal { +export class UnassignSkillFromTopicsModalComponent extends ConfirmOrCancelModal { // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 @@ -55,15 +57,15 @@ export class UnassignSkillFromTopicsModalComponent constructor( private ngbActiveModal: NgbActiveModal, - private topicsAndSkillsDashboardBackendApiService: - TopicsAndSkillsDashboardBackendApiService, + private topicsAndSkillsDashboardBackendApiService: TopicsAndSkillsDashboardBackendApiService, private urlInterpolationService: UrlInterpolationService ) { super(ngbActiveModal); } fetchTopicIdToDiagnosticTestSkillIds( - topicAssignments: AssignedSkill[]): void { + topicAssignments: AssignedSkill[] + ): void { let allTopicIds = []; for (let topic of topicAssignments) { allTopicIds.push(topic.topicId); @@ -71,42 +73,43 @@ export class UnassignSkillFromTopicsModalComponent this.eligibleTopicNameToTopicAssignments = {}; this.ineligibleTopicNameToTopicAssignments = {}; this.topicsAndSkillsDashboardBackendApiService - .fetchTopicIdToDiagnosticTestSkillIdsAsync(allTopicIds).then( - (responseDict: TopicIdToDiagnosticTestSkillIdsResponse) => { - for (let topic of topicAssignments) { - let diagnosticTestSkillIds = ( - responseDict.topicIdToDiagnosticTestSkillIds[topic.topicId]); + .fetchTopicIdToDiagnosticTestSkillIdsAsync(allTopicIds) + .then((responseDict: TopicIdToDiagnosticTestSkillIdsResponse) => { + for (let topic of topicAssignments) { + let diagnosticTestSkillIds = + responseDict.topicIdToDiagnosticTestSkillIds[topic.topicId]; - if ( - diagnosticTestSkillIds.length === 1 && - diagnosticTestSkillIds.indexOf(this.skillId) !== -1 - ) { - this.ineligibleTopicNameToTopicAssignments[topic.topicName] = { - topicId: topic.topicId, - subtopicId: topic.subtopicId, - topicVersion: topic.topicVersion - }; - } else { - this.eligibleTopicNameToTopicAssignments[topic.topicName] = { - topicId: topic.topicId, - subtopicId: topic.subtopicId, - topicVersion: topic.topicVersion - }; - } + if ( + diagnosticTestSkillIds.length === 1 && + diagnosticTestSkillIds.indexOf(this.skillId) !== -1 + ) { + this.ineligibleTopicNameToTopicAssignments[topic.topicName] = { + topicId: topic.topicId, + subtopicId: topic.subtopicId, + topicVersion: topic.topicVersion, + }; + } else { + this.eligibleTopicNameToTopicAssignments[topic.topicName] = { + topicId: topic.topicId, + subtopicId: topic.subtopicId, + topicVersion: topic.topicVersion, + }; } + } - this.eligibleTopicsCount = Object.keys( - this.eligibleTopicNameToTopicAssignments).length; - this.ineligibleTopicsCount = Object.keys( - this.ineligibleTopicNameToTopicAssignments).length; - }); + this.eligibleTopicsCount = Object.keys( + this.eligibleTopicNameToTopicAssignments + ).length; + this.ineligibleTopicsCount = Object.keys( + this.ineligibleTopicNameToTopicAssignments + ).length; + }); } fetchTopicAssignmentsForSkill(): void { this.topicsAndSkillsDashboardBackendApiService - .fetchTopicAssignmentsForSkillAsync( - this.skillId - ).then((response: AssignedSkill[]) => { + .fetchTopicAssignmentsForSkillAsync(this.skillId) + .then((response: AssignedSkill[]) => { this.fetchTopicIdToDiagnosticTestSkillIds(response); this.topicsAssignmentsAreFetched = true; }); @@ -116,9 +119,11 @@ export class UnassignSkillFromTopicsModalComponent let topicId = topicAssignment.topicId; const TOPIC_EDITOR_URL_TEMPLATE = '/topic_editor/#/'; return this.urlInterpolationService.interpolateUrl( - TOPIC_EDITOR_URL_TEMPLATE, { - topic_id: topicId - }); + TOPIC_EDITOR_URL_TEMPLATE, + { + topic_id: topicId, + } + ); } ngOnInit(): void { @@ -137,8 +142,8 @@ export class UnassignSkillFromTopicsModalComponent close(): void { for (let index in this.selectedTopicNames) { this.selectedTopics.push( - this.eligibleTopicNameToTopicAssignments[ - this.selectedTopicNames[index]]); + this.eligibleTopicNameToTopicAssignments[this.selectedTopicNames[index]] + ); } this.ngbActiveModal.close(this.selectedTopics); } diff --git a/core/templates/pages/topics-and-skills-dashboard-page/navbar/topics-and-skills-dashboard-navbar-breadcrumb.component.ts b/core/templates/pages/topics-and-skills-dashboard-page/navbar/topics-and-skills-dashboard-navbar-breadcrumb.component.ts index fe84df08d725..56efb1e5c94e 100644 --- a/core/templates/pages/topics-and-skills-dashboard-page/navbar/topics-and-skills-dashboard-navbar-breadcrumb.component.ts +++ b/core/templates/pages/topics-and-skills-dashboard-page/navbar/topics-and-skills-dashboard-navbar-breadcrumb.component.ts @@ -16,18 +16,18 @@ * @fileoverview Component for the navbar breadcrumb of the collection editor. */ -import { Component } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; @Component({ selector: 'oppia-topics-and-skills-dashboard-navbar-breadcrumb', - templateUrl: './topics-and-skills-dashboard-navbar-breadcrumb.component.html' + templateUrl: './topics-and-skills-dashboard-navbar-breadcrumb.component.html', }) -export class TopicsAndSkillsDashboardNavbarBreadcrumbComponent { } +export class TopicsAndSkillsDashboardNavbarBreadcrumbComponent {} -angular.module('oppia') - .directive( - 'oppiaTopicsAndSkillsDashboardNavbarBreadcrumb', - downgradeComponent({ - component: TopicsAndSkillsDashboardNavbarBreadcrumbComponent - })); +angular.module('oppia').directive( + 'oppiaTopicsAndSkillsDashboardNavbarBreadcrumb', + downgradeComponent({ + component: TopicsAndSkillsDashboardNavbarBreadcrumbComponent, + }) +); diff --git a/core/templates/pages/topics-and-skills-dashboard-page/skills-list/skills-list.component.spec.ts b/core/templates/pages/topics-and-skills-dashboard-page/skills-list/skills-list.component.spec.ts index 9e50a39e34f2..069ed0ee4499 100644 --- a/core/templates/pages/topics-and-skills-dashboard-page/skills-list/skills-list.component.spec.ts +++ b/core/templates/pages/topics-and-skills-dashboard-page/skills-list/skills-list.component.spec.ts @@ -16,34 +16,45 @@ * @fileoverview Unit tests for the skills list component. */ -import { EventEmitter } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { FormsModule } from '@angular/forms'; -import { MatCardModule } from '@angular/material/card'; -import { MatCheckboxModule } from '@angular/material/checkbox'; -import { MatRadioModule } from '@angular/material/radio'; -import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; -import { NgbModal, NgbModalOptions } from '@ng-bootstrap/ng-bootstrap'; -import { MergeSkillModalComponent } from 'components/skill-selector/merge-skill-modal.component'; -import { SkillSelectorComponent } from 'components/skill-selector/skill-selector.component'; -import { AugmentedSkillSummary, AugmentedSkillSummaryBackendDict } from 'domain/skill/augmented-skill-summary.model'; -import { SkillBackendApiService } from 'domain/skill/skill-backend-api.service'; -import { EditableTopicBackendApiService } from 'domain/topic/editable-topic-backend-api.service'; -import { CreatorTopicSummary } from 'domain/topic/creator-topic-summary.model'; -import { TopicsAndSkillsDashboardBackendApiService } from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { AlertsService } from 'services/alerts.service'; -import { AssignSkillToTopicModalComponent } from '../modals/assign-skill-to-topic-modal.component'; -import { DeleteSkillModalComponent } from '../modals/delete-skill-modal.component'; -import { TopicNameToTopicAssignments, UnassignSkillFromTopicsModalComponent } from '../modals/unassign-skill-from-topics-modal.component'; -import { SelectTopicsComponent } from '../topic-selector/select-topics.component'; -import { SkillsListComponent } from './skills-list.component'; +import {EventEmitter} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {FormsModule} from '@angular/forms'; +import {MatCardModule} from '@angular/material/card'; +import {MatCheckboxModule} from '@angular/material/checkbox'; +import {MatRadioModule} from '@angular/material/radio'; +import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; +import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap'; +import {MergeSkillModalComponent} from 'components/skill-selector/merge-skill-modal.component'; +import {SkillSelectorComponent} from 'components/skill-selector/skill-selector.component'; +import { + AugmentedSkillSummary, + AugmentedSkillSummaryBackendDict, +} from 'domain/skill/augmented-skill-summary.model'; +import {SkillBackendApiService} from 'domain/skill/skill-backend-api.service'; +import {EditableTopicBackendApiService} from 'domain/topic/editable-topic-backend-api.service'; +import {CreatorTopicSummary} from 'domain/topic/creator-topic-summary.model'; +import {TopicsAndSkillsDashboardBackendApiService} from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {AlertsService} from 'services/alerts.service'; +import {AssignSkillToTopicModalComponent} from '../modals/assign-skill-to-topic-modal.component'; +import {DeleteSkillModalComponent} from '../modals/delete-skill-modal.component'; +import { + TopicNameToTopicAssignments, + UnassignSkillFromTopicsModalComponent, +} from '../modals/unassign-skill-from-topics-modal.component'; +import {SelectTopicsComponent} from '../topic-selector/select-topics.component'; +import {SkillsListComponent} from './skills-list.component'; describe('Skills List Component', () => { let fixture: ComponentFixture; let componentInstance: SkillsListComponent; - let topicsAndSkillsDashboardBackendApiService: - MockTopicsAndSkillsDashboardBackendApiService; + let topicsAndSkillsDashboardBackendApiService: MockTopicsAndSkillsDashboardBackendApiService; let alertsService: AlertsService; let mockNgbModal: MockNgbModal; let mockSkillBackendApiService: MockSkillBackendApiService; @@ -57,17 +68,16 @@ describe('Skills List Component', () => { skill_model_created_on: 2, skill_model_last_updated: 3, topic_names: ['a'], - classroom_names: ['a'] + classroom_names: ['a'], }; let topicAssignmentsSummary: TopicNameToTopicAssignments = { topic1: { subtopicId: 12, topicVersion: 7, - topicId: 'topic_id' - } + topicId: 'topic_id', + }, }; - class MockNgbModal { // Avoiding non-null assertion for modal below by suffixing '!' symbol. // It was done as the value of modal is not being initialized right away. @@ -75,24 +85,20 @@ describe('Skills List Component', () => { modal!: string; success: boolean = true; open( - content: ( - AssignSkillToTopicModalComponent | - DeleteSkillModalComponent | - MergeSkillModalComponent | - UnassignSkillFromTopicsModalComponent - ), - options: NgbModalOptions + content: + | AssignSkillToTopicModalComponent + | DeleteSkillModalComponent + | MergeSkillModalComponent + | UnassignSkillFromTopicsModalComponent, + options: NgbModalOptions ) { if (this.modal === 'delete_skill') { return { componentInstance: { - skillId: '' + skillId: '', }, result: { - then: ( - successCallback: () => void, - errorCallback: () => void - ) => { + then: (successCallback: () => void, errorCallback: () => void) => { if (this.success) { successCallback(); } else { @@ -101,10 +107,10 @@ describe('Skills List Component', () => { return { then: (callback: () => void) => { callback(); - } + }, }; - } - } + }, + }, }; } else if (this.modal === 'merge_skill') { return { @@ -113,28 +119,26 @@ describe('Skills List Component', () => { skill: null, categorizedSkills: null, allowSkillsFromOtherTopics: null, - untriagedSkillSummaries: null + untriagedSkillSummaries: null, }, result: { then: ( - successCallback: ( - result: { - skill: {}; - supersedingSkillId: string; - } - ) => void, - cancelCallback: () => void + successCallback: (result: { + skill: {}; + supersedingSkillId: string; + }) => void, + cancelCallback: () => void ) => { if (this.success) { successCallback({ skill: {}, - supersedingSkillId: 'test_id' + supersedingSkillId: 'test_id', }); } else { cancelCallback(); } - } - } + }, + }, }; } else if (this.modal === 'unassign_skill') { return { @@ -143,35 +147,36 @@ describe('Skills List Component', () => { }, result: { then: ( - successCallback: ( - topicsToUnassign: TopicNameToTopicAssignments) => void, - cancelCallback: () => void + successCallback: ( + topicsToUnassign: TopicNameToTopicAssignments + ) => void, + cancelCallback: () => void ) => { if (this.success) { successCallback(topicAssignmentsSummary); } else { cancelCallback(); } - } - } + }, + }, }; } else if (this.modal === 'assign_skill_to_topic') { return { componentInstance: { - topicSummaries: null + topicSummaries: null, }, result: { then: ( - successCallback: (skillsToAssign: string[]) => void, - cancelCallback: () => void + successCallback: (skillsToAssign: string[]) => void, + cancelCallback: () => void ) => { if (this.success) { successCallback(['test_id', 'b', 'c']); } else { cancelCallback(); } - } - } + }, + }, }; } } @@ -184,62 +189,60 @@ describe('Skills List Component', () => { then: (callb: () => void) => { callb(); return { - 'catch': (callback: (errorMessage: string) => void) => { + catch: (callback: (errorMessage: string) => void) => { if (this.doesNotHaveSkillLinked) { callback('does not have any skills linked'); } else { callback(''); } - } + }, }; - } + }, }; } } class MockTopicsAndSkillsDashboardBackendApiService { error: boolean = false; - onTopicsAndSkillsDashboardReinitialized: EventEmitter = ( - new EventEmitter()); + onTopicsAndSkillsDashboardReinitialized: EventEmitter = + new EventEmitter(); mergeSkillsAsync(skillId: string, supersedingSkillId: string) { return { then: ( - successCallback: () => void, - errorCallback: (errorMessage: { error: { error: string } }) => void + successCallback: () => void, + errorCallback: (errorMessage: {error: {error: string}}) => void ) => { if (this.error) { errorCallback({ error: { - error: 'error' - } + error: 'error', + }, }); } else { successCallback(); } - } + }, }; } } class MockEditableBackendApiService { updateTopicAsync( - topicId: string, - topicVersion: number, - msg: string, - changeList: [] + topicId: string, + topicVersion: number, + msg: string, + changeList: [] ) { return { - then: ( - successCallback: () => void - ) => { + then: (successCallback: () => void) => { successCallback(); return { then: (successCallback: () => void) => { successCallback(); - } + }, }; - } + }, }; } } @@ -251,7 +254,7 @@ describe('Skills List Component', () => { MatCheckboxModule, MatRadioModule, MatProgressSpinnerModule, - FormsModule + FormsModule, ], declarations: [ SkillsListComponent, @@ -260,28 +263,28 @@ describe('Skills List Component', () => { AssignSkillToTopicModalComponent, MergeSkillModalComponent, SelectTopicsComponent, - SkillSelectorComponent + SkillSelectorComponent, ], providers: [ AlertsService, UrlInterpolationService, { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, { provide: EditableTopicBackendApiService, - useClass: MockEditableBackendApiService + useClass: MockEditableBackendApiService, }, { provide: SkillBackendApiService, - useClass: MockSkillBackendApiService + useClass: MockSkillBackendApiService, }, { provide: TopicsAndSkillsDashboardBackendApiService, - useClass: MockTopicsAndSkillsDashboardBackendApiService - } - ] + useClass: MockTopicsAndSkillsDashboardBackendApiService, + }, + ], }).compileComponents(); })); @@ -295,16 +298,17 @@ describe('Skills List Component', () => { componentInstance.mergeableSkillSummaries = []; componentInstance.untriagedSkillSummaries = []; - topicsAndSkillsDashboardBackendApiService = ( - TestBed.inject(TopicsAndSkillsDashboardBackendApiService) as unknown - ) as jasmine.SpyObj; + topicsAndSkillsDashboardBackendApiService = TestBed.inject( + TopicsAndSkillsDashboardBackendApiService + ) as unknown as jasmine.SpyObj; alertsService = TestBed.inject(AlertsService); - alertsService = (alertsService as unknown) as jasmine.SpyObj; - mockNgbModal = (TestBed.inject(NgbModal) as unknown) as - jasmine.SpyObj; - mockSkillBackendApiService = ( - TestBed.inject(SkillBackendApiService) as unknown - ) as jasmine.SpyObj; + alertsService = alertsService as unknown as jasmine.SpyObj; + mockNgbModal = TestBed.inject( + NgbModal + ) as unknown as jasmine.SpyObj; + mockSkillBackendApiService = TestBed.inject( + SkillBackendApiService + ) as unknown as jasmine.SpyObj; }); it('should create', () => { @@ -314,8 +318,9 @@ describe('Skills List Component', () => { it('should destroy', () => { spyOn(componentInstance.directiveSubscriptions, 'unsubscribe'); componentInstance.ngOnDestroy(); - expect(componentInstance.directiveSubscriptions.unsubscribe) - .toHaveBeenCalled(); + expect( + componentInstance.directiveSubscriptions.unsubscribe + ).toHaveBeenCalled(); }); it('should show edit options', () => { @@ -339,21 +344,24 @@ describe('Skills List Component', () => { }); it('should get skill editor url', () => { - expect(componentInstance.getSkillEditorUrl('test_id')) - .toEqual('/skill_editor/test_id#/'); + expect(componentInstance.getSkillEditorUrl('test_id')).toEqual( + '/skill_editor/test_id#/' + ); }); it('should delete skill', fakeAsync(() => { mockNgbModal.modal = 'delete_skill'; spyOn( - topicsAndSkillsDashboardBackendApiService - .onTopicsAndSkillsDashboardReinitialized, 'emit'); + topicsAndSkillsDashboardBackendApiService.onTopicsAndSkillsDashboardReinitialized, + 'emit' + ); spyOn(alertsService, 'addSuccessMessage'); componentInstance.deleteSkill('skill_id'); tick(500); expect( topicsAndSkillsDashboardBackendApiService - .onTopicsAndSkillsDashboardReinitialized.emit).toHaveBeenCalled(); + .onTopicsAndSkillsDashboardReinitialized.emit + ).toHaveBeenCalled(); expect(alertsService.addSuccessMessage).toHaveBeenCalledWith( 'The skill has been deleted.', 1000 @@ -372,23 +380,27 @@ describe('Skills List Component', () => { it('should merge skill', fakeAsync(() => { mockNgbModal.modal = 'merge_skill'; spyOn( - topicsAndSkillsDashboardBackendApiService - .onTopicsAndSkillsDashboardReinitialized, + topicsAndSkillsDashboardBackendApiService.onTopicsAndSkillsDashboardReinitialized, 'emit' ); spyOn(alertsService, 'addSuccessMessage'); componentInstance.mergeSkill( - AugmentedSkillSummary - .createFromBackendDict(augmentedSkillSummaryBackendDict)); + AugmentedSkillSummary.createFromBackendDict( + augmentedSkillSummaryBackendDict + ) + ); tick(300); expect( topicsAndSkillsDashboardBackendApiService - .onTopicsAndSkillsDashboardReinitialized.emit).toHaveBeenCalled(); + .onTopicsAndSkillsDashboardReinitialized.emit + ).toHaveBeenCalled(); expect(alertsService.addSuccessMessage).toHaveBeenCalled(); topicsAndSkillsDashboardBackendApiService.error = true; componentInstance.mergeSkill( - AugmentedSkillSummary - .createFromBackendDict(augmentedSkillSummaryBackendDict)); + AugmentedSkillSummary.createFromBackendDict( + augmentedSkillSummaryBackendDict + ) + ); })); it('should handle cancel on merge skill modal', fakeAsync(() => { @@ -396,69 +408,77 @@ describe('Skills List Component', () => { mockNgbModal.success = false; componentInstance.mergeSkill( AugmentedSkillSummary.createFromBackendDict( - augmentedSkillSummaryBackendDict)); + augmentedSkillSummaryBackendDict + ) + ); })); it('should unassign skill', fakeAsync(() => { mockNgbModal.modal = 'unassign_skill'; let skillId: string = 'test_skill'; spyOn( - topicsAndSkillsDashboardBackendApiService - .onTopicsAndSkillsDashboardReinitialized, 'emit'); + topicsAndSkillsDashboardBackendApiService.onTopicsAndSkillsDashboardReinitialized, + 'emit' + ); spyOn(alertsService, 'addSuccessMessage'); componentInstance.unassignSkill(skillId); tick(500); expect( topicsAndSkillsDashboardBackendApiService - .onTopicsAndSkillsDashboardReinitialized.emit) - .toHaveBeenCalledWith(true); + .onTopicsAndSkillsDashboardReinitialized.emit + ).toHaveBeenCalledWith(true); expect(alertsService.addSuccessMessage).toHaveBeenCalledWith( - 'The skill has been unassigned to the topic.' - , 1000); + 'The skill has been unassigned to the topic.', + 1000 + ); mockNgbModal.success = false; componentInstance.unassignSkill('skill_id'); })); it('should assign skill to topic', fakeAsync(() => { mockNgbModal.modal = 'assign_skill_to_topic'; - componentInstance.editableTopicSummaries = [new CreatorTopicSummary( - 'test_id', - 'asd', - 1, - 2, - 3, - 4, - 23, - 'sadf', - 'asdf', - 12, - 21, - 123, - 23, - true, - false, - 'sad', - 'asdf', - 'asdf', - 'sdf', - 1, - 1, - [5, 4], - [3, 4] - )]; + componentInstance.editableTopicSummaries = [ + new CreatorTopicSummary( + 'test_id', + 'asd', + 1, + 2, + 3, + 4, + 23, + 'sadf', + 'asdf', + 12, + 21, + 123, + 23, + true, + false, + 'sad', + 'asdf', + 'asdf', + 'sdf', + 1, + 1, + [5, 4], + [3, 4] + ), + ]; spyOn( - topicsAndSkillsDashboardBackendApiService - .onTopicsAndSkillsDashboardReinitialized, 'emit'); + topicsAndSkillsDashboardBackendApiService.onTopicsAndSkillsDashboardReinitialized, + 'emit' + ); spyOn(alertsService, 'addSuccessMessage'); componentInstance.assignSkillToTopic( AugmentedSkillSummary.createFromBackendDict( - augmentedSkillSummaryBackendDict) + augmentedSkillSummaryBackendDict + ) ); tick(500); expect( topicsAndSkillsDashboardBackendApiService - .onTopicsAndSkillsDashboardReinitialized.emit) - .toHaveBeenCalledWith(true); + .onTopicsAndSkillsDashboardReinitialized.emit + ).toHaveBeenCalledWith(true); expect(alertsService.addSuccessMessage).toHaveBeenCalledWith( 'The skill has been assigned to the topic.', 1000 @@ -466,6 +486,8 @@ describe('Skills List Component', () => { mockNgbModal.success = false; componentInstance.assignSkillToTopic( AugmentedSkillSummary.createFromBackendDict( - augmentedSkillSummaryBackendDict)); + augmentedSkillSummaryBackendDict + ) + ); })); }); diff --git a/core/templates/pages/topics-and-skills-dashboard-page/skills-list/skills-list.component.ts b/core/templates/pages/topics-and-skills-dashboard-page/skills-list/skills-list.component.ts index 51ae0403a57c..bf1e80f38845 100644 --- a/core/templates/pages/topics-and-skills-dashboard-page/skills-list/skills-list.component.ts +++ b/core/templates/pages/topics-and-skills-dashboard-page/skills-list/skills-list.component.ts @@ -16,24 +16,27 @@ * @fileoverview Component for the skills list viewer. */ -import { Component, Input } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { MergeSkillModalComponent } from 'components/skill-selector/merge-skill-modal.component'; -import { BackendChangeObject } from 'domain/editor/undo_redo/change.model'; -import { AugmentedSkillSummary } from 'domain/skill/augmented-skill-summary.model'; -import { ShortSkillSummary } from 'domain/skill/short-skill-summary.model'; -import { SkillBackendApiService } from 'domain/skill/skill-backend-api.service'; -import { SkillSummary } from 'domain/skill/skill-summary.model'; -import { EditableTopicBackendApiService } from 'domain/topic/editable-topic-backend-api.service'; -import { CreatorTopicSummary } from 'domain/topic/creator-topic-summary.model'; -import { TopicsAndSkillsDashboardBackendApiService } from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { Subscription } from 'rxjs'; -import { AlertsService } from 'services/alerts.service'; -import { AssignSkillToTopicModalComponent } from '../modals/assign-skill-to-topic-modal.component'; -import { DeleteSkillModalComponent } from '../modals/delete-skill-modal.component'; -import { TopicAssignmentsSummary, UnassignSkillFromTopicsModalComponent } from '../modals/unassign-skill-from-topics-modal.component'; +import {Component, Input} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {MergeSkillModalComponent} from 'components/skill-selector/merge-skill-modal.component'; +import {BackendChangeObject} from 'domain/editor/undo_redo/change.model'; +import {AugmentedSkillSummary} from 'domain/skill/augmented-skill-summary.model'; +import {ShortSkillSummary} from 'domain/skill/short-skill-summary.model'; +import {SkillBackendApiService} from 'domain/skill/skill-backend-api.service'; +import {SkillSummary} from 'domain/skill/skill-summary.model'; +import {EditableTopicBackendApiService} from 'domain/topic/editable-topic-backend-api.service'; +import {CreatorTopicSummary} from 'domain/topic/creator-topic-summary.model'; +import {TopicsAndSkillsDashboardBackendApiService} from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {Subscription} from 'rxjs'; +import {AlertsService} from 'services/alerts.service'; +import {AssignSkillToTopicModalComponent} from '../modals/assign-skill-to-topic-modal.component'; +import {DeleteSkillModalComponent} from '../modals/delete-skill-modal.component'; +import { + TopicAssignmentsSummary, + UnassignSkillFromTopicsModalComponent, +} from '../modals/unassign-skill-from-topics-modal.component'; export interface SkillsCategorizedByTopics { [topicName: string]: { @@ -48,7 +51,7 @@ interface MergeModalResult { @Component({ selector: 'oppia-skills-list', - templateUrl: './skills-list.component.html' + templateUrl: './skills-list.component.html', }) export class SkillsListComponent { // These properties below are initialized using Angular lifecycle hooks @@ -67,8 +70,12 @@ export class SkillsListComponent { selectedIndex!: string; directiveSubscriptions: Subscription = new Subscription(); SKILL_HEADINGS: string[] = [ - 'index', 'description', 'worked_examples_count', - 'misconception_count', 'status']; + 'index', + 'description', + 'worked_examples_count', + 'misconception_count', + 'status', + ]; constructor( private alertsService: AlertsService, @@ -76,69 +83,74 @@ export class SkillsListComponent { private ngbModal: NgbModal, private editableTopicBackendApiService: EditableTopicBackendApiService, private skillBackendApiService: SkillBackendApiService, - private topicsAndSkillsDashboardBackendApiService: - TopicsAndSkillsDashboardBackendApiService + private topicsAndSkillsDashboardBackendApiService: TopicsAndSkillsDashboardBackendApiService ) {} getSkillEditorUrl(skillId: string): string { let SKILL_EDITOR_URL_TEMPLATE: string = '/skill_editor/#/'; return this.urlInterpolationService.interpolateUrl( - SKILL_EDITOR_URL_TEMPLATE, { - skill_id: skillId + SKILL_EDITOR_URL_TEMPLATE, + { + skill_id: skillId, } ); } deleteSkill(skillId: string): void { - let modalRef: NgbModalRef = - this.ngbModal.open(DeleteSkillModalComponent, { + let modalRef: NgbModalRef = this.ngbModal.open(DeleteSkillModalComponent, { backdrop: true, - windowClass: 'delete-skill-modal' + windowClass: 'delete-skill-modal', }); modalRef.componentInstance.skillId = skillId; - modalRef.result.then(() => { - this.skillBackendApiService.deleteSkillAsync(skillId).then( + modalRef.result + .then( () => { - setTimeout(() => { - this.topicsAndSkillsDashboardBackendApiService. - onTopicsAndSkillsDashboardReinitialized.emit(); - let successToast = 'The skill has been deleted.'; - this.alertsService.addSuccessMessage(successToast, 1000); - }, 100); - } - ).catch((errorMessage: string) => { - let errorToast: string; - // This error is thrown as part of a final validation check in - // the backend, hence the message does not include instructions - // for the user to follow. - if (errorMessage.includes('does not have any skills linked')) { - errorToast = ( - 'The skill is assigned to a subtopic in a published ' + - 'topic. Please unpublish the topic before deleting ' + - 'this skill.'); - } else { - errorToast = errorMessage; + this.skillBackendApiService + .deleteSkillAsync(skillId) + .then(() => { + setTimeout(() => { + this.topicsAndSkillsDashboardBackendApiService.onTopicsAndSkillsDashboardReinitialized.emit(); + let successToast = 'The skill has been deleted.'; + this.alertsService.addSuccessMessage(successToast, 1000); + }, 100); + }) + .catch((errorMessage: string) => { + let errorToast: string; + // This error is thrown as part of a final validation check in + // the backend, hence the message does not include instructions + // for the user to follow. + if (errorMessage.includes('does not have any skills linked')) { + errorToast = + 'The skill is assigned to a subtopic in a published ' + + 'topic. Please unpublish the topic before deleting ' + + 'this skill.'; + } else { + errorToast = errorMessage; + } + setTimeout(() => { + this.topicsAndSkillsDashboardBackendApiService.onTopicsAndSkillsDashboardReinitialized.emit(); + }, 100); + this.alertsService.addInfoMessage(errorToast, 5000); + }); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. } - setTimeout(() => { - this.topicsAndSkillsDashboardBackendApiService. - onTopicsAndSkillsDashboardReinitialized.emit(); - }, 100); - this.alertsService.addInfoMessage(errorToast, 5000); - }); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }).then(() => {}); + ) + .then(() => {}); } unassignSkill(skillId: string): void { - let modalRef: NgbModalRef = this.ngbModal. - open(UnassignSkillFromTopicsModalComponent, { - backdrop: 'static' - }); + let modalRef: NgbModalRef = this.ngbModal.open( + UnassignSkillFromTopicsModalComponent, + { + backdrop: 'static', + } + ); modalRef.componentInstance.skillId = skillId; modalRef.result.then( (topicsToUnassign: {[key: string]: TopicAssignmentsSummary}) => { @@ -148,130 +160,157 @@ export class SkillsListComponent { changeList.push({ cmd: 'remove_skill_id_from_subtopic', subtopic_id: topicsToUnassign[topic].subtopicId, - skill_id: skillId + skill_id: skillId, }); } changeList.push({ cmd: 'remove_uncategorized_skill_id', - uncategorized_skill_id: skillId + uncategorized_skill_id: skillId, }); - this.editableTopicBackendApiService.updateTopicAsync( - topicsToUnassign[topic].topicId, - topicsToUnassign[topic].topicVersion, - `Unassigned skill with id ${skillId} from the topic.`, - changeList - ).then(() => { - setTimeout(() => { - this.topicsAndSkillsDashboardBackendApiService. - onTopicsAndSkillsDashboardReinitialized.emit(true); - }, 100); - }).then(() => { - let successToast: string = ( - 'The skill has been unassigned to the topic.'); - this.alertsService.addSuccessMessage(successToast, 1000); - }); + this.editableTopicBackendApiService + .updateTopicAsync( + topicsToUnassign[topic].topicId, + topicsToUnassign[topic].topicVersion, + `Unassigned skill with id ${skillId} from the topic.`, + changeList + ) + .then(() => { + setTimeout(() => { + this.topicsAndSkillsDashboardBackendApiService.onTopicsAndSkillsDashboardReinitialized.emit( + true + ); + }, 100); + }) + .then(() => { + let successToast: string = + 'The skill has been unassigned to the topic.'; + this.alertsService.addSuccessMessage(successToast, 1000); + }); } - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } assignSkillToTopic(skill: AugmentedSkillSummary): void { let skillId: string = skill.id; - let topicSummaries: CreatorTopicSummary[] = ( + let topicSummaries: CreatorTopicSummary[] = this.editableTopicSummaries.filter( - topicSummary => !skill.topicNames.includes(topicSummary.name))); - let modalRef: NgbModalRef = this.ngbModal - .open(AssignSkillToTopicModalComponent, { + topicSummary => !skill.topicNames.includes(topicSummary.name) + ); + let modalRef: NgbModalRef = this.ngbModal.open( + AssignSkillToTopicModalComponent, + { backdrop: 'static', - windowClass: 'assign-skill-to-topic-modal' - }); + windowClass: 'assign-skill-to-topic-modal', + } + ); modalRef.componentInstance.topicSummaries = topicSummaries; - modalRef.result.then((topicIds: string[]) => { - let changeList: BackendChangeObject[] = [{ - cmd: 'add_uncategorized_skill_id', - new_uncategorized_skill_id: skillId - }]; - let topicSummaries: CreatorTopicSummary[] = this.editableTopicSummaries; - for (let i = 0; i < topicIds.length; i++) { - for (let j = 0; j < topicSummaries.length; j++) { - if (topicSummaries[j].id === topicIds[i]) { - this.editableTopicBackendApiService.updateTopicAsync( - topicIds[i], topicSummaries[j].version, - 'Added skill with id ' + skillId + ' to topic.', - changeList - ).then(() => { - setTimeout(() => { - this.topicsAndSkillsDashboardBackendApiService. - onTopicsAndSkillsDashboardReinitialized.emit(true); - }, 100); - }).then(() => { - let successToast = ( - 'The skill has been assigned to the topic.'); - this.alertsService.addSuccessMessage(successToast, 1000); - }); + modalRef.result.then( + (topicIds: string[]) => { + let changeList: BackendChangeObject[] = [ + { + cmd: 'add_uncategorized_skill_id', + new_uncategorized_skill_id: skillId, + }, + ]; + let topicSummaries: CreatorTopicSummary[] = this.editableTopicSummaries; + for (let i = 0; i < topicIds.length; i++) { + for (let j = 0; j < topicSummaries.length; j++) { + if (topicSummaries[j].id === topicIds[i]) { + this.editableTopicBackendApiService + .updateTopicAsync( + topicIds[i], + topicSummaries[j].version, + 'Added skill with id ' + skillId + ' to topic.', + changeList + ) + .then(() => { + setTimeout(() => { + this.topicsAndSkillsDashboardBackendApiService.onTopicsAndSkillsDashboardReinitialized.emit( + true + ); + }, 100); + }) + .then(() => { + let successToast = + 'The skill has been assigned to the topic.'; + this.alertsService.addSuccessMessage(successToast, 1000); + }); + } } } + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. } - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + ); } mergeSkill(skill: AugmentedSkillSummary): void { let skillSummaries: SkillSummary[] = this.mergeableSkillSummaries; let categorizedSkills: SkillsCategorizedByTopics = - this.skillsCategorizedByTopics; + this.skillsCategorizedByTopics; let untriagedSkillSummaries: SkillSummary[] = this.untriagedSkillSummaries; let allowSkillsFromOtherTopics: boolean = true; let modalRef: NgbModalRef = this.ngbModal.open(MergeSkillModalComponent, { backdrop: 'static', windowClass: 'skill-select-modal', - size: 'xl' + size: 'xl', }); modalRef.componentInstance.skillSummaries = skillSummaries; modalRef.componentInstance.skill = skill; modalRef.componentInstance.categorizedSkills = categorizedSkills; modalRef.componentInstance.allowSkillsFromOtherTopics = - allowSkillsFromOtherTopics; + allowSkillsFromOtherTopics; modalRef.componentInstance.untriagedSkillSummaries = - untriagedSkillSummaries; + untriagedSkillSummaries; - modalRef.result.then((result: MergeModalResult) => { - let skill: AugmentedSkillSummary = result.skill; - let supersedingSkillId: string = result.supersedingSkillId; - // Transfer questions from the old skill to the new skill. - this.topicsAndSkillsDashboardBackendApiService.mergeSkillsAsync( - skill.id, supersedingSkillId).then(() => { - // Broadcast will update the skills list in the dashboard so - // that the merged skills are not shown anymore. - setTimeout(() => { - this.topicsAndSkillsDashboardBackendApiService. - onTopicsAndSkillsDashboardReinitialized.emit(true); - let successToast: string = 'Merged Skills.'; - this.alertsService.addSuccessMessage(successToast, 1000); - }, 100); - }, (errorResponse) => { - this.alertsService.addWarning(errorResponse); - }); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is clicked. - // No further action is needed. - }); + modalRef.result.then( + (result: MergeModalResult) => { + let skill: AugmentedSkillSummary = result.skill; + let supersedingSkillId: string = result.supersedingSkillId; + // Transfer questions from the old skill to the new skill. + this.topicsAndSkillsDashboardBackendApiService + .mergeSkillsAsync(skill.id, supersedingSkillId) + .then( + () => { + // Broadcast will update the skills list in the dashboard so + // that the merged skills are not shown anymore. + setTimeout(() => { + this.topicsAndSkillsDashboardBackendApiService.onTopicsAndSkillsDashboardReinitialized.emit( + true + ); + let successToast: string = 'Merged Skills.'; + this.alertsService.addSuccessMessage(successToast, 1000); + }, 100); + }, + errorResponse => { + this.alertsService.addWarning(errorResponse); + } + ); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is clicked. + // No further action is needed. + } + ); } getSerialNumberForSkill(skillIndex: number): number { - const skillSerialNumber: number = ( - skillIndex + (this.pageNumber * this.itemsPerPage)); - return (skillSerialNumber + 1); + const skillSerialNumber: number = + skillIndex + this.pageNumber * this.itemsPerPage; + return skillSerialNumber + 1; } changeEditOptions(skillId: string): void { @@ -287,5 +326,9 @@ export class SkillsListComponent { } } -angular.module('oppia').directive('oppiaSkillsList', - downgradeComponent({ component: SkillsListComponent })); +angular + .module('oppia') + .directive( + 'oppiaSkillsList', + downgradeComponent({component: SkillsListComponent}) + ); diff --git a/core/templates/pages/topics-and-skills-dashboard-page/topic-selector/select-topics.component.spec.ts b/core/templates/pages/topics-and-skills-dashboard-page/topic-selector/select-topics.component.spec.ts index 6f3ca0f718bf..26c2a33c79d7 100644 --- a/core/templates/pages/topics-and-skills-dashboard-page/topic-selector/select-topics.component.spec.ts +++ b/core/templates/pages/topics-and-skills-dashboard-page/topic-selector/select-topics.component.spec.ts @@ -16,62 +16,61 @@ * @fileoverview Unit tests for the select topics component. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { SelectTopicsComponent } from './select-topics.component'; -import { FormsModule } from '@angular/forms'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {SelectTopicsComponent} from './select-topics.component'; +import {FormsModule} from '@angular/forms'; describe('Topic Selector Component', () => { let fixture: ComponentFixture; let componentInstance: SelectTopicsComponent; - let topicSummaries = [{ - additionalStoryCount: 0, - canEditTopic: true, - canonicalStoryCount: 0, - classroom: null, - description: 'dasd', - id: 'grVKDzKnenYL', - isPublished: false, - languageCode: 'en', - name: 'asd', - subtopicCount: 0, - thumbnailBgColor: '#C6DCDA', - thumbnailFilename: 'a.svg', - topicModelCreatedOn: 1598310242241.483, - topicModelLastUpdated: 1598310242544.855, - totalSkillCount: 0, - uncategorizedSkillCount: 0, - urlFragment: 'd', - isSelected: false, - version: 2}, - { - additionalStoryCount: 0, - canEditTopic: true, - canonicalStoryCount: 0, - classroom: null, - description: 'dasd', - id: 'topic2', - isPublished: false, - languageCode: 'en', - name: 'asd', - subtopicCount: 0, - thumbnailBgColor: '#C6DCDA', - thumbnailFilename: 'a.svg', - topicModelCreatedOn: 1598310242241.483, - topicModelLastUpdated: 1598310242544.855, - totalSkillCount: 0, - uncategorizedSkillCount: 0, - isSelected: false, - urlFragment: 'd2', - }]; + let topicSummaries = [ + { + additionalStoryCount: 0, + canEditTopic: true, + canonicalStoryCount: 0, + classroom: null, + description: 'dasd', + id: 'grVKDzKnenYL', + isPublished: false, + languageCode: 'en', + name: 'asd', + subtopicCount: 0, + thumbnailBgColor: '#C6DCDA', + thumbnailFilename: 'a.svg', + topicModelCreatedOn: 1598310242241.483, + topicModelLastUpdated: 1598310242544.855, + totalSkillCount: 0, + uncategorizedSkillCount: 0, + urlFragment: 'd', + isSelected: false, + version: 2, + }, + { + additionalStoryCount: 0, + canEditTopic: true, + canonicalStoryCount: 0, + classroom: null, + description: 'dasd', + id: 'topic2', + isPublished: false, + languageCode: 'en', + name: 'asd', + subtopicCount: 0, + thumbnailBgColor: '#C6DCDA', + thumbnailFilename: 'a.svg', + topicModelCreatedOn: 1598310242241.483, + topicModelLastUpdated: 1598310242544.855, + totalSkillCount: 0, + uncategorizedSkillCount: 0, + isSelected: false, + urlFragment: 'd2', + }, + ]; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - FormsModule - ], - declarations: [ - SelectTopicsComponent - ] + imports: [FormsModule], + declarations: [SelectTopicsComponent], }).compileComponents(); })); @@ -93,8 +92,10 @@ describe('Topic Selector Component', () => { expect(componentInstance.selectedTopicIds).toEqual([topicSummaries[0].id]); expect(topicSummaries[0].isSelected).toEqual(true); componentInstance.selectOrDeselectTopic(topicSummaries[1].id); - expect(componentInstance.selectedTopicIds).toEqual( - [topicSummaries[0].id, topicSummaries[1].id]); + expect(componentInstance.selectedTopicIds).toEqual([ + topicSummaries[0].id, + topicSummaries[1].id, + ]); expect(topicSummaries[1].isSelected).toEqual(true); componentInstance.selectOrDeselectTopic(topicSummaries[0].id); expect(componentInstance.selectedTopicIds).toEqual([topicSummaries[1].id]); @@ -104,16 +105,20 @@ describe('Topic Selector Component', () => { it('should allow filter the topics', () => { componentInstance.searchInTopics(topicSummaries[0].name); expect(componentInstance.filteredTopics[0].name).toEqual( - topicSummaries[0].name); + topicSummaries[0].name + ); componentInstance.searchInTopics(topicSummaries[1].name); expect(componentInstance.filteredTopics[1].name).toEqual( - topicSummaries[1].name); + topicSummaries[1].name + ); }); - it('should throw error if topicId is wrong and no' + - ' topicSummaries exist', () => { - expect(() => { - componentInstance.selectOrDeselectTopic(''); - }).toThrowError('No Topic with given topicId exists!'); - }); + it( + 'should throw error if topicId is wrong and no' + ' topicSummaries exist', + () => { + expect(() => { + componentInstance.selectOrDeselectTopic(''); + }).toThrowError('No Topic with given topicId exists!'); + } + ); }); diff --git a/core/templates/pages/topics-and-skills-dashboard-page/topic-selector/select-topics.component.ts b/core/templates/pages/topics-and-skills-dashboard-page/topic-selector/select-topics.component.ts index 7d779fb93029..051995607d0c 100644 --- a/core/templates/pages/topics-and-skills-dashboard-page/topic-selector/select-topics.component.ts +++ b/core/templates/pages/topics-and-skills-dashboard-page/topic-selector/select-topics.component.ts @@ -16,31 +16,28 @@ * @fileoverview Component for the select topics viewer. */ -import { Component, Input, Output, EventEmitter } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; +import {Component, Input, Output, EventEmitter} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; @Component({ selector: 'oppia-select-topics', - templateUrl: './select-topics.component.html' + templateUrl: './select-topics.component.html', }) export class SelectTopicsComponent { // These properties are initialized using Angular lifecycle hooks // and we need to do non-null assertion. For more information, see // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 - @Input() topicSummaries!: - { id: string; name: string; isSelected: boolean }[]; + @Input() topicSummaries!: {id: string; name: string; isSelected: boolean}[]; @Input() selectedTopicIds!: string[]; - @Output() selectedTopicIdsChange: EventEmitter = ( - new EventEmitter()); + @Output() selectedTopicIdsChange: EventEmitter = new EventEmitter(); topicsSelected: string[] = []; topicFilterText: string = ''; - filteredTopics: { id: string; name: string; isSelected: boolean }[] = []; + filteredTopics: {id: string; name: string; isSelected: boolean}[] = []; selectOrDeselectTopic(topicId: string): void { - let topic = this.topicSummaries.find( - topic => topic.id === topicId); + let topic = this.topicSummaries.find(topic => topic.id === topicId); if (topic === undefined) { throw new Error('No Topic with given topicId exists!'); } @@ -52,7 +49,8 @@ export class SelectTopicsComponent { } else { let idIndex: number = this.selectedTopicIds.indexOf(topicId); let nameIndex = this.topicsSelected.indexOf( - this.topicSummaries[index].name); + this.topicSummaries[index].name + ); this.selectedTopicIds.splice(idIndex, 1); this.topicSummaries[index].isSelected = false; this.topicsSelected.splice(nameIndex, 1); @@ -60,14 +58,19 @@ export class SelectTopicsComponent { this.selectedTopicIdsChange.emit(this.selectedTopicIds); } - searchInTopics(searchText: string): - { id: string; name: string; isSelected: boolean }[] { + searchInTopics( + searchText: string + ): {id: string; name: string; isSelected: boolean}[] { this.filteredTopics = this.topicSummaries.filter( - topic => topic.name.toLowerCase().indexOf( - searchText.toLowerCase()) !== -1); + topic => topic.name.toLowerCase().indexOf(searchText.toLowerCase()) !== -1 + ); return this.filteredTopics; } } -angular.module('oppia').directive('oppiaSelectTopics', - downgradeComponent({ component: SelectTopicsComponent })); +angular + .module('oppia') + .directive( + 'oppiaSelectTopics', + downgradeComponent({component: SelectTopicsComponent}) + ); diff --git a/core/templates/pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.component.spec.ts b/core/templates/pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.component.spec.ts index 07eff69047d0..b6337937058d 100644 --- a/core/templates/pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.component.spec.ts +++ b/core/templates/pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.component.spec.ts @@ -16,22 +16,32 @@ * @fileoverview Unit tests for the topics and skills dashboard component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { TopicCreationService } from 'components/entity-creation-services/topic-creation.service'; -import { SkillSummary } from 'domain/skill/skill-summary.model'; -import { CreatorTopicSummary } from 'domain/topic/creator-topic-summary.model'; -import { TopicsAndSkillsDashboardBackendApiService } from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; -import { TopicsAndSkillsDashboardFilter } from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-filter.model'; -import { CreateNewSkillModalService } from 'pages/topic-editor-page/services/create-new-skill-modal.service'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { TopicsAndSkillsDashboardPageComponent } from './topics-and-skills-dashboard-page.component'; -import { TopicsAndSkillsDashboardPageService } from './topics-and-skills-dashboard-page.service'; -import { PlatformFeatureService } from '../../services/platform-feature.service'; -import { ETopicPublishedOptions, ETopicStatusOptions, TopicsAndSkillsDashboardPageConstants } from './topics-and-skills-dashboard-page.constants'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {TopicCreationService} from 'components/entity-creation-services/topic-creation.service'; +import {SkillSummary} from 'domain/skill/skill-summary.model'; +import {CreatorTopicSummary} from 'domain/topic/creator-topic-summary.model'; +import {TopicsAndSkillsDashboardBackendApiService} from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; +import {TopicsAndSkillsDashboardFilter} from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-filter.model'; +import {CreateNewSkillModalService} from 'pages/topic-editor-page/services/create-new-skill-modal.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {TopicsAndSkillsDashboardPageComponent} from './topics-and-skills-dashboard-page.component'; +import {TopicsAndSkillsDashboardPageService} from './topics-and-skills-dashboard-page.service'; +import {PlatformFeatureService} from '../../services/platform-feature.service'; +import { + ETopicPublishedOptions, + ETopicStatusOptions, + TopicsAndSkillsDashboardPageConstants, +} from './topics-and-skills-dashboard-page.constants'; /** * @fileoverview Unit tests for the topics and skills dashboard component. @@ -40,8 +50,8 @@ import { ETopicPublishedOptions, ETopicStatusOptions, TopicsAndSkillsDashboardPa class MockPlatformFeatureService { status = { SerialChapterLaunchCurriculumAdminView: { - isEnabled: false - } + isEnabled: false, + }, }; } @@ -49,8 +59,7 @@ describe('Topics and skills dashboard page component', () => { let fixture: ComponentFixture; let componentInstance: TopicsAndSkillsDashboardPageComponent; let windowDimensionsService: WindowDimensionsService; - let topicsAndSkillsDashboardBackendApiService: - TopicsAndSkillsDashboardBackendApiService; + let topicsAndSkillsDashboardBackendApiService: TopicsAndSkillsDashboardBackendApiService; let focusManagerService: FocusManagerService; let topicCreationService: TopicCreationService; let createNewSkillModalService: CreateNewSkillModalService; @@ -59,13 +68,8 @@ describe('Topics and skills dashboard page component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - declarations: [ - TopicsAndSkillsDashboardPageComponent, - MockTranslatePipe - ], + imports: [HttpClientTestingModule], + declarations: [TopicsAndSkillsDashboardPageComponent, MockTranslatePipe], providers: [ FocusManagerService, CreateNewSkillModalService, @@ -75,10 +79,10 @@ describe('Topics and skills dashboard page component', () => { WindowDimensionsService, { provide: PlatformFeatureService, - useValue: mockPlatformFeatureService - } + useValue: mockPlatformFeatureService, + }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -87,12 +91,14 @@ describe('Topics and skills dashboard page component', () => { componentInstance = fixture.componentInstance; windowDimensionsService = TestBed.inject(WindowDimensionsService); topicsAndSkillsDashboardBackendApiService = TestBed.inject( - TopicsAndSkillsDashboardBackendApiService); + TopicsAndSkillsDashboardBackendApiService + ); focusManagerService = TestBed.inject(FocusManagerService); topicCreationService = TestBed.inject(TopicCreationService); createNewSkillModalService = TestBed.inject(CreateNewSkillModalService); topicsAndSkillsDashboardPageService = TestBed.inject( - TopicsAndSkillsDashboardPageService); + TopicsAndSkillsDashboardPageService + ); }); it('should create', () => { @@ -104,62 +110,66 @@ describe('Topics and skills dashboard page component', () => { spyOn(TopicsAndSkillsDashboardFilter, 'createDefault').and.callThrough(); spyOn(componentInstance, '_initDashboard'); componentInstance.ngOnInit(); - topicsAndSkillsDashboardBackendApiService - .onTopicsAndSkillsDashboardReinitialized.emit(true); + topicsAndSkillsDashboardBackendApiService.onTopicsAndSkillsDashboardReinitialized.emit( + true + ); tick(); expect(componentInstance._initDashboard).toHaveBeenCalled(); })); it('should correctly initialize sort and status options list', () => { - type TopicPublishedOptionsKeys = ( - keyof typeof TopicsAndSkillsDashboardPageConstants. - TOPIC_PUBLISHED_OPTIONS); - type TopicSortOptionsKeys = ( - keyof typeof TopicsAndSkillsDashboardPageConstants.TOPIC_SORT_OPTIONS); - type TopicSortingOptionsKeys = ( - keyof typeof TopicsAndSkillsDashboardPageConstants.TOPIC_SORTING_OPTIONS); - type TopicStatusOptionsKeys = ( - keyof typeof TopicsAndSkillsDashboardPageConstants - .TOPIC_STATUS_OPTIONS); - - mockPlatformFeatureService. - status.SerialChapterLaunchCurriculumAdminView.isEnabled = false; + type TopicPublishedOptionsKeys = + keyof typeof TopicsAndSkillsDashboardPageConstants.TOPIC_PUBLISHED_OPTIONS; + type TopicSortOptionsKeys = + keyof typeof TopicsAndSkillsDashboardPageConstants.TOPIC_SORT_OPTIONS; + type TopicSortingOptionsKeys = + keyof typeof TopicsAndSkillsDashboardPageConstants.TOPIC_SORTING_OPTIONS; + type TopicStatusOptionsKeys = + keyof typeof TopicsAndSkillsDashboardPageConstants.TOPIC_STATUS_OPTIONS; + + mockPlatformFeatureService.status.SerialChapterLaunchCurriculumAdminView.isEnabled = + false; let topicSortOptions: string[] = []; for (let key in TopicsAndSkillsDashboardPageConstants.TOPIC_SORT_OPTIONS) { topicSortOptions.push( TopicsAndSkillsDashboardPageConstants.TOPIC_SORT_OPTIONS[ - key as TopicSortOptionsKeys]); + key as TopicSortOptionsKeys + ] + ); } - let topicStatusOptions: ( - ETopicPublishedOptions | ETopicStatusOptions)[] = []; - for (let key in TopicsAndSkillsDashboardPageConstants - .TOPIC_PUBLISHED_OPTIONS) { + let topicStatusOptions: (ETopicPublishedOptions | ETopicStatusOptions)[] = + []; + for (let key in TopicsAndSkillsDashboardPageConstants.TOPIC_PUBLISHED_OPTIONS) { topicStatusOptions.push( TopicsAndSkillsDashboardPageConstants.TOPIC_PUBLISHED_OPTIONS[ - key as TopicPublishedOptionsKeys]); + key as TopicPublishedOptionsKeys + ] + ); } componentInstance.ngOnInit(); expect(componentInstance.sortOptions).toEqual(topicSortOptions); expect(componentInstance.statusOptions).toEqual(topicStatusOptions); - mockPlatformFeatureService. - status.SerialChapterLaunchCurriculumAdminView.isEnabled = true; + mockPlatformFeatureService.status.SerialChapterLaunchCurriculumAdminView.isEnabled = + true; topicSortOptions = []; - for (let key in TopicsAndSkillsDashboardPageConstants - .TOPIC_SORTING_OPTIONS) { + for (let key in TopicsAndSkillsDashboardPageConstants.TOPIC_SORTING_OPTIONS) { topicSortOptions.push( TopicsAndSkillsDashboardPageConstants.TOPIC_SORTING_OPTIONS[ - key as TopicSortingOptionsKeys]); + key as TopicSortingOptionsKeys + ] + ); } topicStatusOptions = []; - for (let key in TopicsAndSkillsDashboardPageConstants - .TOPIC_STATUS_OPTIONS) { + for (let key in TopicsAndSkillsDashboardPageConstants.TOPIC_STATUS_OPTIONS) { topicStatusOptions.push( TopicsAndSkillsDashboardPageConstants.TOPIC_STATUS_OPTIONS[ - key as TopicStatusOptionsKeys]); + key as TopicStatusOptionsKeys + ] + ); } componentInstance.ngOnInit(); @@ -170,8 +180,9 @@ describe('Topics and skills dashboard page component', () => { it('should destroy', () => { spyOn(componentInstance.directiveSubscriptions, 'unsubscribe'); componentInstance.ngOnDestroy(); - expect(componentInstance.directiveSubscriptions.unsubscribe) - .toHaveBeenCalled(); + expect( + componentInstance.directiveSubscriptions.unsubscribe + ).toHaveBeenCalled(); }); it('should generate numbers till range', () => { @@ -180,8 +191,9 @@ describe('Topics and skills dashboard page component', () => { it('should check whether next skill page is present', () => { for (let i = 0; i < 10; i++) { - componentInstance.skillSummaries.push(new SkillSummary( - '', '', '', 1, 2, 3, 4, 5)); + componentInstance.skillSummaries.push( + new SkillSummary('', '', '', 1, 2, 3, 4, 5) + ); } componentInstance.skillPageNumber = 0; componentInstance.itemsPerPage = 4; @@ -192,7 +204,7 @@ describe('Topics and skills dashboard page component', () => { it('should set topic tab as active tab', () => { componentInstance.filterObject = { - reset: () => {} + reset: () => {}, } as TopicsAndSkillsDashboardFilter; spyOn(componentInstance.filterObject, 'reset'); spyOn(componentInstance, 'goToPageNumber'); @@ -205,7 +217,7 @@ describe('Topics and skills dashboard page component', () => { it('should set skills tab as active tab', () => { componentInstance.filterObject = { - reset: () => {} + reset: () => {}, } as TopicsAndSkillsDashboardFilter; spyOn(componentInstance.filterObject, 'reset'); spyOn(componentInstance, 'initSkillDashboard'); @@ -241,8 +253,31 @@ describe('Topics and skills dashboard page component', () => { for (let i = 0; i < 20; i++) { componentInstance.topicSummaries.push( new CreatorTopicSummary( - '', '', 2, 2, 2, 2, 2, '', '', 2, 2, 2, 2, true, true, '', '', - '', '', 1, 1, [5, 4], [3, 4])); + '', + '', + 2, + 2, + 2, + 2, + 2, + '', + '', + 2, + 2, + 2, + 2, + true, + true, + '', + '', + '', + '', + 1, + 1, + [5, 4], + [3, 4] + ) + ); } componentInstance.pageNumber = 1; componentInstance.itemsPerPage = 4; @@ -251,8 +286,9 @@ describe('Topics and skills dashboard page component', () => { componentInstance.activeTab = componentInstance.TAB_NAME_SKILLS; for (let i = 0; i < 10; i++) { - componentInstance.skillSummaries.push(new SkillSummary( - '', '', '', 1, 2, 3, 4, 5)); + componentInstance.skillSummaries.push( + new SkillSummary('', '', '', 1, 2, 3, 4, 5) + ); } componentInstance.pageNumber = 1; componentInstance.itemsPerPage = 2; @@ -267,11 +303,12 @@ describe('Topics and skills dashboard page component', () => { const resp = { skillSummaries: [], nextCursor: '', - more: true + more: true, }; spyOn( topicsAndSkillsDashboardBackendApiService, - 'fetchSkillsDashboardDataAsync').and.returnValue(Promise.resolve(resp)); + 'fetchSkillsDashboardDataAsync' + ).and.returnValue(Promise.resolve(resp)); spyOn(componentInstance, 'goToPageNumber'); componentInstance.fetchSkills(); @@ -279,23 +316,27 @@ describe('Topics and skills dashboard page component', () => { expect(componentInstance.moreSkillsPresent).toEqual(resp.more); expect(componentInstance.nextCursor).toEqual(resp.nextCursor); expect(componentInstance.currentCount).toEqual( - componentInstance.skillSummaries.length); + componentInstance.skillSummaries.length + ); expect(componentInstance.goToPageNumber).toHaveBeenCalledWith(0); expect(componentInstance.firstTimeFetchingSkills).toBeFalse(); componentInstance.fetchSkills(); tick(); expect(componentInstance.goToPageNumber).toHaveBeenCalledWith( - componentInstance.pageNumber + 1); + componentInstance.pageNumber + 1 + ); componentInstance.skillPageNumber = 1; componentInstance.itemsPerPage = 1; componentInstance.moreSkillsPresent = false; for (let i = 0; i < 5; i++) { - componentInstance.skillSummaries.push(new SkillSummary( - '', '', '', 1, 2, 3, 4, 5)); + componentInstance.skillSummaries.push( + new SkillSummary('', '', '', 1, 2, 3, 4, 5) + ); } componentInstance.fetchSkills(); expect(componentInstance.goToPageNumber).toHaveBeenCalledWith( - componentInstance.pageNumber + 1); + componentInstance.pageNumber + 1 + ); })); it('should apply filters', () => { @@ -307,8 +348,10 @@ describe('Topics and skills dashboard page component', () => { // Topics tab. componentInstance.activeTab = componentInstance.TAB_NAME_TOPICS; - spyOn(topicsAndSkillsDashboardPageService, 'getFilteredTopics') - .and.returnValue([]); + spyOn( + topicsAndSkillsDashboardPageService, + 'getFilteredTopics' + ).and.returnValue([]); spyOn(componentInstance, 'goToPageNumber'); componentInstance.applyFilters(); expect(componentInstance.goToPageNumber).toHaveBeenCalledWith(0); @@ -316,14 +359,15 @@ describe('Topics and skills dashboard page component', () => { it('should reset filters', () => { componentInstance.filterObject = { - reset: () => {} + reset: () => {}, } as TopicsAndSkillsDashboardFilter; spyOn(componentInstance, 'getUpperLimitValueForPagination'); spyOn(componentInstance.filterObject, 'reset'); spyOn(componentInstance, 'applyFilters'); componentInstance.resetFilters(); - expect(componentInstance.getUpperLimitValueForPagination) - .toHaveBeenCalled(); + expect( + componentInstance.getUpperLimitValueForPagination + ).toHaveBeenCalled(); expect(componentInstance.filterObject.reset).toHaveBeenCalled(); expect(componentInstance.applyFilters).toHaveBeenCalled(); }); @@ -367,7 +411,8 @@ describe('Topics and skills dashboard page component', () => { it('should get total count value for skills', () => { componentInstance.skillSummaries = [ - new SkillSummary('', '', '', 2, 3, 4, 6, 7)]; + new SkillSummary('', '', '', 2, 3, 4, 6, 7), + ]; componentInstance.itemsPerPage = 0; expect(componentInstance.getTotalCountValueForSkills()).toEqual('many'); componentInstance.itemsPerPage = 2; @@ -382,8 +427,10 @@ describe('Topics and skills dashboard page component', () => { it('should initialize topics and skills dashboard', fakeAsync(() => { spyOn( - topicsAndSkillsDashboardBackendApiService, 'fetchDashboardDataAsync') - .and.returnValues(Promise.resolve({ + topicsAndSkillsDashboardBackendApiService, + 'fetchDashboardDataAsync' + ).and.returnValues( + Promise.resolve({ allClassroomNames: ['math'], canDeleteSkill: true, canDeleteTopic: true, @@ -392,10 +439,34 @@ describe('Topics and skills dashboard page component', () => { untriagedSkillSummaries: [], mergeableSkillSummaries: [], totalSkillCount: 5, - topicSummaries: [new CreatorTopicSummary( - '', '', 2, 2, 2, 2, 2, '', '', 1, 1, 2, 3, true, true, '', '', '', - '', 1, 1, [5, 4], [3, 4])], - categorizedSkillsDict: {} + topicSummaries: [ + new CreatorTopicSummary( + '', + '', + 2, + 2, + 2, + 2, + 2, + '', + '', + 1, + 1, + 2, + 3, + true, + true, + '', + '', + '', + '', + 1, + 1, + [5, 4], + [3, 4] + ), + ], + categorizedSkillsDict: {}, }), Promise.resolve({ allClassroomNames: ['math'], @@ -403,14 +474,13 @@ describe('Topics and skills dashboard page component', () => { canDeleteTopic: true, canCreateTopic: true, canCreateSkill: true, - untriagedSkillSummaries: [ - new SkillSummary('', '', '', 2, 3, 4, 5, 6) - ], + untriagedSkillSummaries: [new SkillSummary('', '', '', 2, 3, 4, 5, 6)], mergeableSkillSummaries: [], totalSkillCount: 5, topicSummaries: [], - categorizedSkillsDict: {} - })); + categorizedSkillsDict: {}, + }) + ); spyOn(componentInstance, 'applyFilters'); spyOn(focusManagerService, 'setFocus'); spyOn(componentInstance, 'initSkillDashboard'); @@ -423,12 +493,15 @@ describe('Topics and skills dashboard page component', () => { it('should navigate to skill page', () => { componentInstance.fetchSkillsDebounced = () => {}; spyOn(componentInstance, 'isNextSkillPagePresent').and.returnValues( - true, false); + true, + false + ); spyOn(componentInstance, 'goToPageNumber'); spyOn(componentInstance, 'fetchSkillsDebounced'); componentInstance.navigateSkillPage(componentInstance.MOVE_TO_NEXT_PAGE); expect(componentInstance.goToPageNumber).toHaveBeenCalledWith( - componentInstance.pageNumber + 1); + componentInstance.pageNumber + 1 + ); componentInstance.navigateSkillPage(componentInstance.MOVE_TO_NEXT_PAGE); expect(componentInstance.fetchSkillsDebounced).toHaveBeenCalled(); componentInstance.pageNumber = 5; @@ -443,10 +516,12 @@ describe('Topics and skills dashboard page component', () => { spyOn(componentInstance, 'goToPageNumber'); componentInstance.changePageByOne(componentInstance.MOVE_TO_PREV_PAGE); expect(componentInstance.goToPageNumber).toHaveBeenCalledWith( - componentInstance.pageNumber - 1); + componentInstance.pageNumber - 1 + ); componentInstance.pageNumber = 1; componentInstance.changePageByOne(componentInstance.MOVE_TO_NEXT_PAGE); expect(componentInstance.goToPageNumber).toHaveBeenCalledWith( - componentInstance.pageNumber + 1); + componentInstance.pageNumber + 1 + ); }); }); diff --git a/core/templates/pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.component.ts b/core/templates/pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.component.ts index 39097ce6c238..9ddeb5cf0f35 100644 --- a/core/templates/pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.component.ts +++ b/core/templates/pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.component.ts @@ -16,36 +16,43 @@ * @fileoverview Component for the topics and skills dashboard. */ -import { Component, HostListener } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { TopicCreationService } from 'components/entity-creation-services/topic-creation.service'; -import { SkillSummary } from 'domain/skill/skill-summary.model'; -import { CreatorTopicSummary } from 'domain/topic/creator-topic-summary.model'; -import { CategorizedSkills, TopicsAndSkillsDashboardBackendApiService } from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; -import { TopicsAndSkillsDashboardFilter } from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-filter.model'; +import {Component, HostListener} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {TopicCreationService} from 'components/entity-creation-services/topic-creation.service'; +import {SkillSummary} from 'domain/skill/skill-summary.model'; +import {CreatorTopicSummary} from 'domain/topic/creator-topic-summary.model'; +import { + CategorizedSkills, + TopicsAndSkillsDashboardBackendApiService, +} from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; +import {TopicsAndSkillsDashboardFilter} from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-filter.model'; import debounce from 'lodash/debounce'; -import { CreateNewSkillModalService } from 'pages/topic-editor-page/services/create-new-skill-modal.service'; -import { Subscription } from 'rxjs'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { ETopicPublishedOptions, ETopicStatusOptions, TopicsAndSkillsDashboardPageConstants } from './topics-and-skills-dashboard-page.constants'; -import { TopicsAndSkillsDashboardPageService } from './topics-and-skills-dashboard-page.service'; -import { PlatformFeatureService } from 'services/platform-feature.service'; - -type TopicPublishedOptionsKeys = ( - keyof typeof TopicsAndSkillsDashboardPageConstants.TOPIC_PUBLISHED_OPTIONS); -type TopicStatusOptionsKeys = ( - keyof typeof TopicsAndSkillsDashboardPageConstants.TOPIC_STATUS_OPTIONS); -type TopicSortOptionsKeys = ( - keyof typeof TopicsAndSkillsDashboardPageConstants.TOPIC_SORT_OPTIONS); -type TopicSortingOptionsKeys = ( - keyof typeof TopicsAndSkillsDashboardPageConstants.TOPIC_SORTING_OPTIONS); -type SkillStatusOptionsKeys = ( - keyof typeof TopicsAndSkillsDashboardPageConstants.SKILL_STATUS_OPTIONS); +import {CreateNewSkillModalService} from 'pages/topic-editor-page/services/create-new-skill-modal.service'; +import {Subscription} from 'rxjs'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import { + ETopicPublishedOptions, + ETopicStatusOptions, + TopicsAndSkillsDashboardPageConstants, +} from './topics-and-skills-dashboard-page.constants'; +import {TopicsAndSkillsDashboardPageService} from './topics-and-skills-dashboard-page.service'; +import {PlatformFeatureService} from 'services/platform-feature.service'; + +type TopicPublishedOptionsKeys = + keyof typeof TopicsAndSkillsDashboardPageConstants.TOPIC_PUBLISHED_OPTIONS; +type TopicStatusOptionsKeys = + keyof typeof TopicsAndSkillsDashboardPageConstants.TOPIC_STATUS_OPTIONS; +type TopicSortOptionsKeys = + keyof typeof TopicsAndSkillsDashboardPageConstants.TOPIC_SORT_OPTIONS; +type TopicSortingOptionsKeys = + keyof typeof TopicsAndSkillsDashboardPageConstants.TOPIC_SORTING_OPTIONS; +type SkillStatusOptionsKeys = + keyof typeof TopicsAndSkillsDashboardPageConstants.SKILL_STATUS_OPTIONS; @Component({ selector: 'oppia-topics-and-skills-dashboard-page', - templateUrl: './topics-and-skills-dashboard-page.component.html' + templateUrl: './topics-and-skills-dashboard-page.component.html', }) export class TopicsAndSkillsDashboardPageComponent { directiveSubscriptions: Subscription = new Subscription(); @@ -97,10 +104,8 @@ export class TopicsAndSkillsDashboardPageComponent { private focusManagerService: FocusManagerService, private createNewSkillModalService: CreateNewSkillModalService, private topicCreationService: TopicCreationService, - private topicsAndSkillsDashboardBackendApiService: - TopicsAndSkillsDashboardBackendApiService, - private topicsAndSkillsDashboardPageService: - TopicsAndSkillsDashboardPageService, + private topicsAndSkillsDashboardBackendApiService: TopicsAndSkillsDashboardBackendApiService, + private topicsAndSkillsDashboardPageService: TopicsAndSkillsDashboardPageService, private windowDimensionsService: WindowDimensionsService, private platformFeatureService: PlatformFeatureService ) {} @@ -113,47 +118,55 @@ export class TopicsAndSkillsDashboardPageComponent { for (let key in TopicsAndSkillsDashboardPageConstants.TOPIC_SORT_OPTIONS) { this.sortOptions.push( TopicsAndSkillsDashboardPageConstants.TOPIC_SORT_OPTIONS[ - key as TopicSortOptionsKeys]); + key as TopicSortOptionsKeys + ] + ); } - if (this.platformFeatureService.status. - SerialChapterLaunchCurriculumAdminView.isEnabled) { + if ( + this.platformFeatureService.status.SerialChapterLaunchCurriculumAdminView + .isEnabled + ) { this.sortOptions = []; - for (let key in TopicsAndSkillsDashboardPageConstants - .TOPIC_SORTING_OPTIONS) { + for (let key in TopicsAndSkillsDashboardPageConstants.TOPIC_SORTING_OPTIONS) { this.sortOptions.push( TopicsAndSkillsDashboardPageConstants.TOPIC_SORTING_OPTIONS[ - key as TopicSortingOptionsKeys]); + key as TopicSortingOptionsKeys + ] + ); } } - for (let key in TopicsAndSkillsDashboardPageConstants - .TOPIC_PUBLISHED_OPTIONS) { + for (let key in TopicsAndSkillsDashboardPageConstants.TOPIC_PUBLISHED_OPTIONS) { this.statusOptions.push( TopicsAndSkillsDashboardPageConstants.TOPIC_PUBLISHED_OPTIONS[ - key as TopicPublishedOptionsKeys]); + key as TopicPublishedOptionsKeys + ] + ); } - if (this.platformFeatureService.status. - SerialChapterLaunchCurriculumAdminView.isEnabled) { + if ( + this.platformFeatureService.status.SerialChapterLaunchCurriculumAdminView + .isEnabled + ) { this.statusOptions = []; - for (let key in TopicsAndSkillsDashboardPageConstants - .TOPIC_STATUS_OPTIONS) { + for (let key in TopicsAndSkillsDashboardPageConstants.TOPIC_STATUS_OPTIONS) { this.statusOptions.push( TopicsAndSkillsDashboardPageConstants.TOPIC_STATUS_OPTIONS[ - key as TopicStatusOptionsKeys]); + key as TopicStatusOptionsKeys + ] + ); } } this.fetchSkillsDebounced = debounce(this.fetchSkills, 300); this.directiveSubscriptions.add( - this.topicsAndSkillsDashboardBackendApiService. - onTopicsAndSkillsDashboardReinitialized.subscribe( - (stayInSameTab: boolean) => { - this._initDashboard(stayInSameTab); - } - ) + this.topicsAndSkillsDashboardBackendApiService.onTopicsAndSkillsDashboardReinitialized.subscribe( + (stayInSameTab: boolean) => { + this._initDashboard(stayInSameTab); + } + ) ); this._initDashboard(false); } @@ -182,8 +195,8 @@ export class TopicsAndSkillsDashboardPageComponent { let totalSkillsPresent: number = this.skillSummaries.length; // Here +1 is used since we are checking the next page and // another +1 because page numbers start from 0. - let numberOfSkillsRequired: number = ( - (this.skillPageNumber + 2) * this.itemsPerPage); + let numberOfSkillsRequired: number = + (this.skillPageNumber + 2) * this.itemsPerPage; return totalSkillsPresent >= numberOfSkillsRequired; } @@ -208,11 +221,12 @@ export class TopicsAndSkillsDashboardPageComponent { this.skillStatusOptions = []; this.moreSkillsPresent = true; this.firstTimeFetchingSkills = true; - for (let key in TopicsAndSkillsDashboardPageConstants - .SKILL_STATUS_OPTIONS) { + for (let key in TopicsAndSkillsDashboardPageConstants.SKILL_STATUS_OPTIONS) { this.skillStatusOptions.push( TopicsAndSkillsDashboardPageConstants.SKILL_STATUS_OPTIONS[ - key as SkillStatusOptionsKeys]); + key as SkillStatusOptionsKeys + ] + ); } this.applyFilters(); } @@ -233,16 +247,17 @@ export class TopicsAndSkillsDashboardPageComponent { this.topicPageNumber = pageNumber; this.pageNumber = this.topicPageNumber; this.currentCount = this.topicSummaries.length; - this.displayedTopicSummaries = - this.topicSummaries.slice( - pageNumber * this.itemsPerPage, - (pageNumber + 1) * this.itemsPerPage); + this.displayedTopicSummaries = this.topicSummaries.slice( + pageNumber * this.itemsPerPage, + (pageNumber + 1) * this.itemsPerPage + ); } else if (this.activeTab === this.TAB_NAME_SKILLS) { this.skillPageNumber = pageNumber; this.pageNumber = this.skillPageNumber; this.displayedSkillSummaries = this.skillSummaries.slice( pageNumber * this.itemsPerPage, - (pageNumber + 1) * this.itemsPerPage); + (pageNumber + 1) * this.itemsPerPage + ); } } @@ -250,21 +265,26 @@ export class TopicsAndSkillsDashboardPageComponent { if (this.moreSkillsPresent) { this.topicsAndSkillsDashboardBackendApiService .fetchSkillsDashboardDataAsync( - this.filterObject, this.itemsPerPage, this.nextCursor).then( - (response) => { - this.moreSkillsPresent = response.more; - this.nextCursor = response.nextCursor; - this.skillSummaries.push(...response.skillSummaries); - this.currentCount = this.skillSummaries.length; - if (this.firstTimeFetchingSkills) { - this.goToPageNumber(0); - this.firstTimeFetchingSkills = false; - } else { - this.goToPageNumber(this.pageNumber + 1); - } - }); - } else if (this.skillSummaries.length > - ((this.skillPageNumber + 1) * this.itemsPerPage)) { + this.filterObject, + this.itemsPerPage, + this.nextCursor + ) + .then(response => { + this.moreSkillsPresent = response.more; + this.nextCursor = response.nextCursor; + this.skillSummaries.push(...response.skillSummaries); + this.currentCount = this.skillSummaries.length; + if (this.firstTimeFetchingSkills) { + this.goToPageNumber(0); + this.firstTimeFetchingSkills = false; + } else { + this.goToPageNumber(this.pageNumber + 1); + } + }); + } else if ( + this.skillSummaries.length > + (this.skillPageNumber + 1) * this.itemsPerPage + ) { this.goToPageNumber(this.pageNumber + 1); } } @@ -286,12 +306,13 @@ export class TopicsAndSkillsDashboardPageComponent { * page to left or right by 1. */ changePageByOne(direction: string): void { - this.lastPage = parseInt( - String(this.currentCount / this.itemsPerPage)); + this.lastPage = parseInt(String(this.currentCount / this.itemsPerPage)); if (direction === this.MOVE_TO_PREV_PAGE && this.pageNumber >= 1) { this.goToPageNumber(this.pageNumber - 1); - } else if (direction === this.MOVE_TO_NEXT_PAGE && - this.pageNumber < this.lastPage - 1) { + } else if ( + direction === this.MOVE_TO_NEXT_PAGE && + this.pageNumber < this.lastPage - 1 + ) { this.goToPageNumber(this.pageNumber + 1); } } @@ -305,12 +326,16 @@ export class TopicsAndSkillsDashboardPageComponent { this.fetchSkills(); return; } - this.topicSummaries = ( + this.topicSummaries = this.topicsAndSkillsDashboardPageService.getFilteredTopics( - this.totalTopicSummaries, this.filterObject)); + this.totalTopicSummaries, + this.filterObject + ); - this.displayedTopicSummaries = - this.topicSummaries.slice(0, this.itemsPerPage); + this.displayedTopicSummaries = this.topicSummaries.slice( + 0, + this.itemsPerPage + ); this.currentCount = this.topicSummaries.length; this.goToPageNumber(0); } @@ -336,10 +361,10 @@ export class TopicsAndSkillsDashboardPageComponent { } getUpperLimitValueForPagination(): number { - return ( - Math.min(( - (this.pageNumber * this.itemsPerPage) + - this.itemsPerPage), this.currentCount)); + return Math.min( + this.pageNumber * this.itemsPerPage + this.itemsPerPage, + this.currentCount + ); } getTotalCountValueForSkills(): number | string { @@ -358,17 +383,19 @@ export class TopicsAndSkillsDashboardPageComponent { * Calls the TopicsAndSkillsDashboardBackendApiService and fetches * the topics and skills dashboard data. * @param {Boolean} stayInSameTab - To stay in the same tab or not. - */ + */ _initDashboard(stayInSameTab: boolean): void { - this.topicsAndSkillsDashboardBackendApiService.fetchDashboardDataAsync() - .then((response) => { + this.topicsAndSkillsDashboardBackendApiService + .fetchDashboardDataAsync() + .then(response => { this.totalTopicSummaries = response.topicSummaries; this.topicSummaries = this.totalTopicSummaries; this.totalEntityCountToDisplay = this.topicSummaries.length; this.currentCount = this.totalEntityCountToDisplay; this.applyFilters(); - this.editableTopicSummaries = this.topicSummaries - .filter((summary) => summary.canEditTopic === true); + this.editableTopicSummaries = this.topicSummaries.filter( + summary => summary.canEditTopic === true + ); this.focusManagerService.setFocus('createTopicBtn'); this.totalSkillCount = response.totalSkillCount; this.skillsCategorizedByTopics = response.categorizedSkillsDict; @@ -384,8 +411,10 @@ export class TopicsAndSkillsDashboardPageComponent { this.userCanDeleteSkill = response.canDeleteSkill; this.userCanDeleteTopic = response.canDeleteTopic; - if (this.topicSummaries.length === 0 && - this.untriagedSkillSummaries.length !== 0) { + if ( + this.topicSummaries.length === 0 && + this.untriagedSkillSummaries.length !== 0 + ) { this.activeTab = this.TAB_NAME_SKILLS; this.initSkillDashboard(); this.focusManagerService.setFocus('createSkillBtn'); @@ -395,5 +424,9 @@ export class TopicsAndSkillsDashboardPageComponent { } } -angular.module('oppia').directive('oppiaTopicsAndSkillsDashboardPage', - downgradeComponent({ component: TopicsAndSkillsDashboardPageComponent })); +angular + .module('oppia') + .directive( + 'oppiaTopicsAndSkillsDashboardPage', + downgradeComponent({component: TopicsAndSkillsDashboardPageComponent}) + ); diff --git a/core/templates/pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.constants.ajs.ts b/core/templates/pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.constants.ajs.ts index 8da49f28273e..294728e51950 100644 --- a/core/templates/pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.constants.ajs.ts +++ b/core/templates/pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.constants.ajs.ts @@ -18,21 +18,36 @@ // TODO(#7092): Delete this file once migration is complete and these AngularJS // equivalents of the Angular constants are no longer needed. -import { TopicsAndSkillsDashboardPageConstants } from 'pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.constants'; +import {TopicsAndSkillsDashboardPageConstants} from 'pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.constants'; -angular.module('oppia').constant( - 'SKILL_DESCRIPTION_STATUS_VALUES', - TopicsAndSkillsDashboardPageConstants.SKILL_DESCRIPTION_STATUS_VALUES); +angular + .module('oppia') + .constant( + 'SKILL_DESCRIPTION_STATUS_VALUES', + TopicsAndSkillsDashboardPageConstants.SKILL_DESCRIPTION_STATUS_VALUES + ); -angular.module('oppia').constant( - 'TOPIC_SORT_OPTIONS', - TopicsAndSkillsDashboardPageConstants.TOPIC_SORT_OPTIONS); -angular.module('oppia').constant( - 'TOPIC_PUBLISHED_OPTIONS', - TopicsAndSkillsDashboardPageConstants.TOPIC_PUBLISHED_OPTIONS); -angular.module('oppia').constant( - 'TOPIC_FILTER_CLASSROOM_ALL', - TopicsAndSkillsDashboardPageConstants.TOPIC_FILTER_CLASSROOM_ALL); -angular.module('oppia').constant( - 'SKILL_STATUS_OPTIONS', - TopicsAndSkillsDashboardPageConstants.SKILL_STATUS_OPTIONS); +angular + .module('oppia') + .constant( + 'TOPIC_SORT_OPTIONS', + TopicsAndSkillsDashboardPageConstants.TOPIC_SORT_OPTIONS + ); +angular + .module('oppia') + .constant( + 'TOPIC_PUBLISHED_OPTIONS', + TopicsAndSkillsDashboardPageConstants.TOPIC_PUBLISHED_OPTIONS + ); +angular + .module('oppia') + .constant( + 'TOPIC_FILTER_CLASSROOM_ALL', + TopicsAndSkillsDashboardPageConstants.TOPIC_FILTER_CLASSROOM_ALL + ); +angular + .module('oppia') + .constant( + 'SKILL_STATUS_OPTIONS', + TopicsAndSkillsDashboardPageConstants.SKILL_STATUS_OPTIONS + ); diff --git a/core/templates/pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.constants.ts b/core/templates/pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.constants.ts index 755f3399f8a7..cb04ff75e702 100644 --- a/core/templates/pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.constants.ts +++ b/core/templates/pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.constants.ts @@ -15,7 +15,7 @@ /** * @fileoverview Constants for the topics and skills dashboard. */ -import { AppConstants } from 'app.constants'; +import {AppConstants} from 'app.constants'; export enum ETopicSortOptions { IncreasingCreatedOn = 'Newly Created', @@ -30,13 +30,13 @@ export enum ETopicNewSortingOptions { IncreasingUpdatedOn = 'Most Recently Updated', DecreasingUpdatedOn = 'Least Recently Updated', DecreasingUpcomingLaunches = 'Most Upcoming Launches', - DecreasingOverdueLaunches = 'Most Launches Behind Schedule' + DecreasingOverdueLaunches = 'Most Launches Behind Schedule', } export enum ETopicPublishedOptions { All = 'All', Published = 'Published', - NotPublished = 'Not Published' + NotPublished = 'Not Published', } export enum ETopicStatusOptions { @@ -50,14 +50,12 @@ export const TopicsAndSkillsDashboardPageConstants = { SKILL_DESCRIPTION_STATUS_VALUES: { STATUS_UNCHANGED: 'unchanged', STATUS_CHANGED: 'changed', - STATUS_DISABLED: 'disabled' + STATUS_DISABLED: 'disabled', }, - TOPIC_SORT_OPTIONS: ( - AppConstants.TOPIC_SKILL_DASHBOARD_SORT_OPTIONS), - TOPIC_SORTING_OPTIONS: ( - AppConstants.TOPIC_SKILL_DASHBOARD_SORTING_OPTIONS), + TOPIC_SORT_OPTIONS: AppConstants.TOPIC_SKILL_DASHBOARD_SORT_OPTIONS, + TOPIC_SORTING_OPTIONS: AppConstants.TOPIC_SKILL_DASHBOARD_SORTING_OPTIONS, TOPIC_PUBLISHED_OPTIONS: ETopicPublishedOptions, TOPIC_STATUS_OPTIONS: ETopicStatusOptions, TOPIC_FILTER_CLASSROOM_ALL: 'All', - SKILL_STATUS_OPTIONS: AppConstants.SKILL_STATUS_OPTIONS + SKILL_STATUS_OPTIONS: AppConstants.SKILL_STATUS_OPTIONS, } as const; diff --git a/core/templates/pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.import.ts b/core/templates/pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.import.ts index 1c63c3daf1e5..1c3252ff61ab 100644 --- a/core/templates/pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.import.ts +++ b/core/templates/pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.import.ts @@ -22,9 +22,14 @@ import 'zone.js'; import uiValidate from 'angular-ui-validate'; angular.module('oppia', [ - require('angular-cookies'), 'ngAnimate', - 'ngMaterial', 'ngSanitize', 'ngTouch', 'pascalprecht.translate', - 'ui.bootstrap', uiValidate + require('angular-cookies'), + 'ngAnimate', + 'ngMaterial', + 'ngSanitize', + 'ngTouch', + 'pascalprecht.translate', + 'ui.bootstrap', + uiValidate, ]); require('Polyfills.ts'); @@ -33,14 +38,16 @@ require('Polyfills.ts'); // main module the elements are attached to. require( 'pages/topics-and-skills-dashboard-page/' + - 'topics-and-skills-dashboard-page.module.ts'); + 'topics-and-skills-dashboard-page.module.ts' +); require('App.ts'); require('base-components/oppia-root.directive.ts'); require( 'pages/topics-and-skills-dashboard-page/navbar/' + - 'topics-and-skills-dashboard-navbar-breadcrumb.component.ts' + 'topics-and-skills-dashboard-navbar-breadcrumb.component.ts' ); require( 'pages/topics-and-skills-dashboard-page/' + - 'topics-and-skills-dashboard-page.component.ts'); + 'topics-and-skills-dashboard-page.component.ts' +); diff --git a/core/templates/pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.module.ts b/core/templates/pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.module.ts index 653697b1d0ad..8cf4e25c44c0 100644 --- a/core/templates/pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.module.ts +++ b/core/templates/pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.module.ts @@ -16,39 +16,40 @@ * @fileoverview Module for the story viewer page. */ -import { APP_INITIALIZER, NgModule, StaticProvider } from '@angular/core'; -import { BrowserModule, HAMMER_GESTURE_CONFIG } from '@angular/platform-browser'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { OppiaAngularRootComponent } from - 'components/oppia-angular-root.component'; -import { platformFeatureInitFactory, PlatformFeatureService } from - 'services/platform-feature.service'; -import { RouterModule } from '@angular/router'; -import { APP_BASE_HREF } from '@angular/common'; +import {APP_INITIALIZER, NgModule, StaticProvider} from '@angular/core'; +import {BrowserModule, HAMMER_GESTURE_CONFIG} from '@angular/platform-browser'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {HttpClientModule, HTTP_INTERCEPTORS} from '@angular/common/http'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {OppiaAngularRootComponent} from 'components/oppia-angular-root.component'; +import { + platformFeatureInitFactory, + PlatformFeatureService, +} from 'services/platform-feature.service'; +import {RouterModule} from '@angular/router'; +import {APP_BASE_HREF} from '@angular/common'; -import { RequestInterceptor } from 'services/request-interceptor.service'; -import { SkillCreationService } from 'components/entity-creation-services/skill-creation.service'; -import { SelectTopicsComponent } from './topic-selector/select-topics.component'; -import { SkillsListComponent } from './skills-list/skills-list.component'; -import { DeleteSkillModalComponent } from './modals/delete-skill-modal.component'; -import { UnassignSkillFromTopicsModalComponent } from './modals/unassign-skill-from-topics-modal.component'; -import { TopicsListComponent } from './topics-list/topics-list.component'; -import { DeleteTopicModalComponent } from './modals/delete-topic-modal.component'; -import { AssignSkillToTopicModalComponent } from './modals/assign-skill-to-topic-modal.component'; -import { MergeSkillModalComponent } from 'components/skill-selector/merge-skill-modal.component'; -import { DynamicContentModule } from 'components/interaction-display/dynamic-content.module'; -import { TopicsAndSkillsDashboardPageComponent } from './topics-and-skills-dashboard-page.component'; -import { FormsModule } from '@angular/forms'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { SmartRouterModule } from 'hybrid-router-module-provider'; -import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import {RequestInterceptor} from 'services/request-interceptor.service'; +import {SkillCreationService} from 'components/entity-creation-services/skill-creation.service'; +import {SelectTopicsComponent} from './topic-selector/select-topics.component'; +import {SkillsListComponent} from './skills-list/skills-list.component'; +import {DeleteSkillModalComponent} from './modals/delete-skill-modal.component'; +import {UnassignSkillFromTopicsModalComponent} from './modals/unassign-skill-from-topics-modal.component'; +import {TopicsListComponent} from './topics-list/topics-list.component'; +import {DeleteTopicModalComponent} from './modals/delete-topic-modal.component'; +import {AssignSkillToTopicModalComponent} from './modals/assign-skill-to-topic-modal.component'; +import {MergeSkillModalComponent} from 'components/skill-selector/merge-skill-modal.component'; +import {DynamicContentModule} from 'components/interaction-display/dynamic-content.module'; +import {TopicsAndSkillsDashboardPageComponent} from './topics-and-skills-dashboard-page.component'; +import {FormsModule} from '@angular/forms'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {SmartRouterModule} from 'hybrid-router-module-provider'; +import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; -import { CreateNewTopicModalComponent } from './modals/create-new-topic-modal.component'; -import { ToastrModule } from 'ngx-toastr'; -import { MyHammerConfig, toastrConfig } from 'pages/oppia-root/app.module'; -import { AppErrorHandlerProvider } from 'pages/oppia-root/app-error-handler'; +import {CreateNewTopicModalComponent} from './modals/create-new-topic-modal.component'; +import {ToastrModule} from 'ngx-toastr'; +import {MyHammerConfig, toastrConfig} from 'pages/oppia-root/app.module'; +import {AppErrorHandlerProvider} from 'pages/oppia-root/app-error-handler'; @NgModule({ imports: [ @@ -63,7 +64,7 @@ import { AppErrorHandlerProvider } from 'pages/oppia-root/app-error-handler'; DynamicContentModule, FormsModule, MatProgressSpinnerModule, - ToastrModule.forRoot(toastrConfig) + ToastrModule.forRoot(toastrConfig), ], declarations: [ SkillsListComponent, @@ -77,7 +78,7 @@ import { AppErrorHandlerProvider } from 'pages/oppia-root/app-error-handler'; SelectTopicsComponent, TopicsAndSkillsDashboardPageComponent, CreateNewTopicModalComponent, - DeleteTopicModalComponent + DeleteTopicModalComponent, ], entryComponents: [ SkillsListComponent, @@ -90,41 +91,41 @@ import { AppErrorHandlerProvider } from 'pages/oppia-root/app-error-handler'; DeleteTopicModalComponent, SelectTopicsComponent, TopicsAndSkillsDashboardPageComponent, - CreateNewTopicModalComponent + CreateNewTopicModalComponent, ], providers: [ SkillCreationService, { provide: HTTP_INTERCEPTORS, useClass: RequestInterceptor, - multi: true + multi: true, }, { provide: APP_INITIALIZER, useFactory: platformFeatureInitFactory, deps: [PlatformFeatureService], - multi: true + multi: true, }, { provide: HAMMER_GESTURE_CONFIG, - useClass: MyHammerConfig + useClass: MyHammerConfig, }, AppErrorHandlerProvider, { provide: APP_BASE_HREF, - useValue: '/' - } - ] + useValue: '/', + }, + ], }) class TopicsAndSkillsDashboardPageModule { // Empty placeholder method to satisfy the `Compiler`. ngDoBootstrap() {} } -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { downgradeModule } from '@angular/upgrade/static'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {downgradeModule} from '@angular/upgrade/static'; -const bootstrapFnAsync = async(extraProviders: StaticProvider[]) => { +const bootstrapFnAsync = async (extraProviders: StaticProvider[]) => { const platformRef = platformBrowserDynamic(extraProviders); return platformRef.bootstrapModule(TopicsAndSkillsDashboardPageModule); }; @@ -139,5 +140,6 @@ angular.module('oppia').directive( // bootstrap the Angular 8. 'oppiaAngularRoot', downgradeComponent({ - component: OppiaAngularRootComponent - }) as angular.IDirectiveFactory); + component: OppiaAngularRootComponent, + }) as angular.IDirectiveFactory +); diff --git a/core/templates/pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.service.spec.ts b/core/templates/pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.service.spec.ts index 8364c653e4a2..54e20e3f2fd5 100644 --- a/core/templates/pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.service.spec.ts +++ b/core/templates/pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.service.spec.ts @@ -16,27 +16,31 @@ * @fileoverview Unit tests for TopicsAndSkillsDashboardPageService. */ -import { ETopicPublishedOptions, ETopicSortOptions, - ETopicNewSortingOptions, ETopicStatusOptions } from +import { + ETopicPublishedOptions, + ETopicSortOptions, + ETopicNewSortingOptions, + ETopicStatusOptions, // eslint-disable-next-line max-len - 'pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.constants'; -import { TopicsAndSkillsDashboardFilter } from +} from 'pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.constants'; +import { + TopicsAndSkillsDashboardFilter, // eslint-disable-next-line max-len - 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-filter.model'; -import { TopicsAndSkillsDashboardPageService } from +} from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-filter.model'; +import { + TopicsAndSkillsDashboardPageService, // eslint-disable-next-line max-len - 'pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.service'; -import { CreatorTopicSummary } from - 'domain/topic/creator-topic-summary.model'; -import { PlatformFeatureService } from '../../services/platform-feature.service'; -import { TestBed } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; +} from 'pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.service'; +import {CreatorTopicSummary} from 'domain/topic/creator-topic-summary.model'; +import {PlatformFeatureService} from '../../services/platform-feature.service'; +import {TestBed} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; class MockPlatformFeatureService { status = { SerialChapterLaunchCurriculumAdminView: { - isEnabled: false - } + isEnabled: false, + }, }; } @@ -47,10 +51,11 @@ describe('Topic and Skill dashboard page service', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [{ - provide: PlatformFeatureService, - useValue: mockPlatformFeatureService - } + providers: [ + { + provide: PlatformFeatureService, + useValue: mockPlatformFeatureService, + }, ], }); tsds = TestBed.inject(TopicsAndSkillsDashboardPageService); @@ -80,7 +85,7 @@ describe('Topic and Skill dashboard page service', () => { total_upcoming_chapters_count: 2, total_overdue_chapters_count: 5, total_chapter_counts_for_each_story: [5, 4], - published_chapter_counts_for_each_story: [5, 4] + published_chapter_counts_for_each_story: [5, 4], }); const topic2 = CreatorTopicSummary.createFromBackendDict({ topic_model_created_on: 1681839432987.596, @@ -105,7 +110,7 @@ describe('Topic and Skill dashboard page service', () => { total_upcoming_chapters_count: 3, total_overdue_chapters_count: 1, total_chapter_counts_for_each_story: [5, 4], - published_chapter_counts_for_each_story: [3, 4] + published_chapter_counts_for_each_story: [3, 4], }); const topic3 = CreatorTopicSummary.createFromBackendDict({ topic_model_created_on: 1781839432987.596, @@ -130,10 +135,10 @@ describe('Topic and Skill dashboard page service', () => { total_upcoming_chapters_count: 1, total_overdue_chapters_count: 0, total_chapter_counts_for_each_story: [5, 4], - published_chapter_counts_for_each_story: [3, 4] + published_chapter_counts_for_each_story: [3, 4], }); - mockPlatformFeatureService. - status.SerialChapterLaunchCurriculumAdminView.isEnabled = false; + mockPlatformFeatureService.status.SerialChapterLaunchCurriculumAdminView.isEnabled = + false; let topicsArray = [topic1, topic2, topic3]; let filterOptions = TopicsAndSkillsDashboardFilter.createDefault(); let filteredArray = tsds.getFilteredTopics(topicsArray, filterOptions); @@ -152,8 +157,8 @@ describe('Topic and Skill dashboard page service', () => { filteredArray = tsds.getFilteredTopics(topicsArray, filterOptions); expect(filteredArray).toEqual([topic2]); - mockPlatformFeatureService. - status.SerialChapterLaunchCurriculumAdminView.isEnabled = true; + mockPlatformFeatureService.status.SerialChapterLaunchCurriculumAdminView.isEnabled = + true; filterOptions.status = ETopicStatusOptions.FullyPublished; filteredArray = tsds.getFilteredTopics(topicsArray, filterOptions); diff --git a/core/templates/pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.service.ts b/core/templates/pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.service.ts index 5d501fe79a2c..9baf0119ed5b 100644 --- a/core/templates/pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.service.ts +++ b/core/templates/pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.service.ts @@ -16,27 +16,26 @@ * @fileoverview Service for topics and skills dashboard page. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { CreatorTopicSummary } from - 'domain/topic/creator-topic-summary.model'; +import {CreatorTopicSummary} from 'domain/topic/creator-topic-summary.model'; // eslint-disable-next-line max-len -import { TopicsAndSkillsDashboardFilter } from +import { + TopicsAndSkillsDashboardFilter, // eslint-disable-next-line max-len - 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-filter.model'; -import { TopicsAndSkillsDashboardPageConstants } from +} from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-filter.model'; +import { + TopicsAndSkillsDashboardPageConstants, // eslint-disable-next-line max-len - 'pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.constants'; -import { PlatformFeatureService } from 'services/platform-feature.service'; +} from 'pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.constants'; +import {PlatformFeatureService} from 'services/platform-feature.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class TopicsAndSkillsDashboardPageService { - constructor( - private platformFeatureService: PlatformFeatureService - ) {} + constructor(private platformFeatureService: PlatformFeatureService) {} /** * @param {Array} topicsArray - The original topics array @@ -45,21 +44,24 @@ export class TopicsAndSkillsDashboardPageService { * @returns {Array} filteredTopics - The filtered Topics array */ getFilteredTopics( - topicsArray: CreatorTopicSummary[], - filterObject: TopicsAndSkillsDashboardFilter): CreatorTopicSummary[] { - let ESortOptions = ( - TopicsAndSkillsDashboardPageConstants.TOPIC_SORTING_OPTIONS); - let EPublishedOptions = ( - TopicsAndSkillsDashboardPageConstants.TOPIC_PUBLISHED_OPTIONS); - let EStatusOptions = ( - TopicsAndSkillsDashboardPageConstants.TOPIC_STATUS_OPTIONS); + topicsArray: CreatorTopicSummary[], + filterObject: TopicsAndSkillsDashboardFilter + ): CreatorTopicSummary[] { + let ESortOptions = + TopicsAndSkillsDashboardPageConstants.TOPIC_SORTING_OPTIONS; + let EPublishedOptions = + TopicsAndSkillsDashboardPageConstants.TOPIC_PUBLISHED_OPTIONS; + let EStatusOptions = + TopicsAndSkillsDashboardPageConstants.TOPIC_STATUS_OPTIONS; let filteredTopics = topicsArray; if (filterObject.keywords.length) { - filteredTopics = topicsArray.filter((topic) => { + filteredTopics = topicsArray.filter(topic => { for (let keyword of filterObject.keywords) { - if (topic.name.toLowerCase().includes(keyword.toLowerCase()) || - topic.description.toLowerCase().includes(keyword.toLowerCase())) { + if ( + topic.name.toLowerCase().includes(keyword.toLowerCase()) || + topic.description.toLowerCase().includes(keyword.toLowerCase()) + ) { return true; } } @@ -67,57 +69,70 @@ export class TopicsAndSkillsDashboardPageService { }); } - if (filterObject.classroom !== - TopicsAndSkillsDashboardPageConstants.TOPIC_FILTER_CLASSROOM_ALL) { - filteredTopics = filteredTopics.filter((topic) => { + if ( + filterObject.classroom !== + TopicsAndSkillsDashboardPageConstants.TOPIC_FILTER_CLASSROOM_ALL + ) { + filteredTopics = filteredTopics.filter(topic => { if (filterObject.classroom === 'Unassigned' && !topic.classroom) { return true; } return ( - topic.classroom && filterObject.classroom.toLowerCase() === - topic.classroom.toLowerCase()); + topic.classroom && + filterObject.classroom.toLowerCase() === topic.classroom.toLowerCase() + ); }); } if (filterObject.status !== EPublishedOptions.All) { - if (this.platformFeatureService.status. - SerialChapterLaunchCurriculumAdminView.isEnabled) { - filteredTopics = filteredTopics.filter((topic) => { + if ( + this.platformFeatureService.status + .SerialChapterLaunchCurriculumAdminView.isEnabled + ) { + filteredTopics = filteredTopics.filter(topic => { let fullyPublishedStoriesCount = 0; let totalStories = topic.getTotalChaptersCounts().length; for (let i = 0; i < totalStories; i++) { - if (topic.getTotalChaptersCounts()[i] === - topic.getPublishedChaptersCounts()[i]) { + if ( + topic.getTotalChaptersCounts()[i] === + topic.getPublishedChaptersCounts()[i] + ) { fullyPublishedStoriesCount++; } } - if (filterObject.status === EStatusOptions.FullyPublished && - totalStories && - totalStories === fullyPublishedStoriesCount && - topic.isPublished) { + if ( + filterObject.status === EStatusOptions.FullyPublished && + totalStories && + totalStories === fullyPublishedStoriesCount && + topic.isPublished + ) { return true; } else if ( - filterObject.status === EStatusOptions.PartiallyPublished && ( - !totalStories || - totalStories !== fullyPublishedStoriesCount) && - topic.isPublished) { + filterObject.status === EStatusOptions.PartiallyPublished && + (!totalStories || totalStories !== fullyPublishedStoriesCount) && + topic.isPublished + ) { return true; } else if ( filterObject.status === EStatusOptions.NotPublished && - !topic.isPublished) { + !topic.isPublished + ) { return true; } return false; }); } else { - filteredTopics = filteredTopics.filter((topic) => { - if (filterObject.status === EPublishedOptions.Published && - topic.isPublished) { + filteredTopics = filteredTopics.filter(topic => { + if ( + filterObject.status === EPublishedOptions.Published && + topic.isPublished + ) { return true; } else if ( filterObject.status === EPublishedOptions.NotPublished && - !topic.isPublished) { + !topic.isPublished + ) { return true; } return false; @@ -127,32 +142,35 @@ export class TopicsAndSkillsDashboardPageService { switch (filterObject.sort) { case ESortOptions.IncreasingUpdatedOn: - filteredTopics.sort((a, b) => ( - b.topicModelCreatedOn - a.topicModelCreatedOn)); + filteredTopics.sort( + (a, b) => b.topicModelCreatedOn - a.topicModelCreatedOn + ); break; case ESortOptions.DecreasingUpdatedOn: filteredTopics.sort( - (a, b) => -(b.topicModelCreatedOn - a.topicModelCreatedOn)); + (a, b) => -(b.topicModelCreatedOn - a.topicModelCreatedOn) + ); break; case ESortOptions.IncreasingCreatedOn: filteredTopics.sort( - (a, b) => (b.topicModelLastUpdated - a.topicModelLastUpdated)); + (a, b) => b.topicModelLastUpdated - a.topicModelLastUpdated + ); break; case ESortOptions.DecreasingCreatedOn: filteredTopics.sort( - (a, b) => -(b.topicModelLastUpdated - a.topicModelLastUpdated)); + (a, b) => -(b.topicModelLastUpdated - a.topicModelLastUpdated) + ); break; case ESortOptions.DecreasingUpcomingLaunches: filteredTopics.sort( - (a, b) => -( - a.totalUpcomingChaptersCount - - b.totalUpcomingChaptersCount)); + (a, b) => + -(a.totalUpcomingChaptersCount - b.totalUpcomingChaptersCount) + ); break; case ESortOptions.DecreasingOverdueLaunches: filteredTopics.sort( - (a, b) => -( - a.totalOverdueChaptersCount - - b.totalOverdueChaptersCount)); + (a, b) => -(a.totalOverdueChaptersCount - b.totalOverdueChaptersCount) + ); break; default: throw new Error('Invalid filter by sort value provided.'); @@ -162,6 +180,9 @@ export class TopicsAndSkillsDashboardPageService { } } -angular.module('oppia').factory( - 'TopicsAndSkillsDashboardPageService', - downgradeInjectable(TopicsAndSkillsDashboardPageService)); +angular + .module('oppia') + .factory( + 'TopicsAndSkillsDashboardPageService', + downgradeInjectable(TopicsAndSkillsDashboardPageService) + ); diff --git a/core/templates/pages/topics-and-skills-dashboard-page/topics-list/topics-list.component.spec.ts b/core/templates/pages/topics-and-skills-dashboard-page/topics-list/topics-list.component.spec.ts index 0711c44c65a6..0ec91cfea836 100644 --- a/core/templates/pages/topics-and-skills-dashboard-page/topics-list/topics-list.component.spec.ts +++ b/core/templates/pages/topics-and-skills-dashboard-page/topics-list/topics-list.component.spec.ts @@ -16,28 +16,30 @@ * @fileoverview Unit tests for the Topic List Component. */ -import { CommonModule } from '@angular/common'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { MatCardModule } from '@angular/material/card'; -import { NgbModal, NgbModalModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; -import { EditableTopicBackendApiService } from 'domain/topic/editable-topic-backend-api.service'; -import { TopicsAndSkillsDashboardBackendApiService } from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { AlertsService } from 'services/alerts.service'; -import { DeleteTopicModalComponent } from '../modals/delete-topic-modal.component'; -import { TopicsListComponent } from './topics-list.component'; -import { PlatformFeatureService } from - '../../../services/platform-feature.service'; -import { CreatorTopicSummary } from - 'domain/topic/creator-topic-summary.model'; +import {CommonModule} from '@angular/common'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {MatCardModule} from '@angular/material/card'; +import { + NgbModal, + NgbModalModule, + NgbTooltipModule, +} from '@ng-bootstrap/ng-bootstrap'; +import {EditableTopicBackendApiService} from 'domain/topic/editable-topic-backend-api.service'; +import {TopicsAndSkillsDashboardBackendApiService} from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {AlertsService} from 'services/alerts.service'; +import {DeleteTopicModalComponent} from '../modals/delete-topic-modal.component'; +import {TopicsListComponent} from './topics-list.component'; +import {PlatformFeatureService} from '../../../services/platform-feature.service'; +import {CreatorTopicSummary} from 'domain/topic/creator-topic-summary.model'; import constants from 'assets/constants'; class MockPlatformFeatureService { status = { SerialChapterLaunchCurriculumAdminView: { - isEnabled: false - } + isEnabled: false, + }, }; } @@ -47,8 +49,7 @@ describe('Topics List Component', () => { let urlInterpolationService: UrlInterpolationService; let alertsService: AlertsService; let editableTopicBackendApiService: MockEditableBackendApiService; - let topicsAndSkillsDashboardBackendApiService: - TopicsAndSkillsDashboardBackendApiService; + let topicsAndSkillsDashboardBackendApiService: TopicsAndSkillsDashboardBackendApiService; let mockPlatformFeatureService = new MockPlatformFeatureService(); let mockNgbModal: MockNgbModal; const topicId: string = 'topicId'; @@ -57,20 +58,17 @@ describe('Topics List Component', () => { class MockNgbRef { success: boolean = true; componentInstance = { - topiceName: '' + topiceName: '', }; result = { - then: ( - successCallback: () => void, - cancelCallback: () => void - ) => { + then: (successCallback: () => void, cancelCallback: () => void) => { if (this.success) { successCallback(); } else { cancelCallback(); } - } + }, }; } @@ -87,15 +85,15 @@ describe('Topics List Component', () => { deleteTopicAsync(topicId: string): object { return { then: ( - successCallback: (status: number) => void, - errorCallback: (error: string) => void + successCallback: (status: number) => void, + errorCallback: (error: string) => void ) => { if (this.success) { successCallback(123); } else { errorCallback(this.message); } - } + }, }; } } @@ -107,29 +105,26 @@ describe('Topics List Component', () => { NgbTooltipModule, HttpClientTestingModule, CommonModule, - MatCardModule - ], - declarations: [ - TopicsListComponent, - DeleteTopicModalComponent + MatCardModule, ], + declarations: [TopicsListComponent, DeleteTopicModalComponent], providers: [ AlertsService, { provide: EditableTopicBackendApiService, - useClass: MockEditableBackendApiService + useClass: MockEditableBackendApiService, }, TopicsAndSkillsDashboardBackendApiService, UrlInterpolationService, { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, { provide: PlatformFeatureService, - useValue: mockPlatformFeatureService - } - ] + useValue: mockPlatformFeatureService, + }, + ], }).compileComponents(); })); @@ -138,54 +133,64 @@ describe('Topics List Component', () => { componentInstance = fixture.componentInstance; fixture.detectChanges(); urlInterpolationService = TestBed.inject(UrlInterpolationService); - urlInterpolationService = (urlInterpolationService as unknown) as - jasmine.SpyObj; + urlInterpolationService = + urlInterpolationService as unknown as jasmine.SpyObj; alertsService = TestBed.inject(AlertsService); - alertsService = (alertsService as unknown) as - jasmine.SpyObj; - editableTopicBackendApiService = ( - TestBed.inject(EditableTopicBackendApiService) as unknown) as - MockEditableBackendApiService; - mockNgbModal = (TestBed.inject(NgbModal) as unknown) as MockNgbModal; + alertsService = alertsService as unknown as jasmine.SpyObj; + editableTopicBackendApiService = TestBed.inject( + EditableTopicBackendApiService + ) as unknown as MockEditableBackendApiService; + mockNgbModal = TestBed.inject(NgbModal) as unknown as MockNgbModal; topicsAndSkillsDashboardBackendApiService = TestBed.inject( - TopicsAndSkillsDashboardBackendApiService); - topicsAndSkillsDashboardBackendApiService = ( - topicsAndSkillsDashboardBackendApiService as unknown) as - jasmine.SpyObj; + TopicsAndSkillsDashboardBackendApiService + ); + topicsAndSkillsDashboardBackendApiService = + topicsAndSkillsDashboardBackendApiService as unknown as jasmine.SpyObj; }); - it('should get status of Serial Chapter Launch Feature flag', () => { - mockPlatformFeatureService. - status.SerialChapterLaunchCurriculumAdminView.isEnabled = false; - expect(componentInstance.isSerialChapterLaunchFeatureEnabled()). - toEqual(false); - - mockPlatformFeatureService. - status.SerialChapterLaunchCurriculumAdminView.isEnabled = true; - expect(componentInstance.isSerialChapterLaunchFeatureEnabled()). - toEqual(true); + mockPlatformFeatureService.status.SerialChapterLaunchCurriculumAdminView.isEnabled = + false; + expect(componentInstance.isSerialChapterLaunchFeatureEnabled()).toEqual( + false + ); + + mockPlatformFeatureService.status.SerialChapterLaunchCurriculumAdminView.isEnabled = + true; + expect(componentInstance.isSerialChapterLaunchFeatureEnabled()).toEqual( + true + ); }); it('should get correct headings list based on feature flag', () => { - mockPlatformFeatureService. - status.SerialChapterLaunchCurriculumAdminView.isEnabled = true; + mockPlatformFeatureService.status.SerialChapterLaunchCurriculumAdminView.isEnabled = + true; componentInstance.ngOnInit(); expect(componentInstance.TOPIC_HEADINGS.length).toBe(8); expect(componentInstance.TOPIC_HEADINGS).toEqual([ - 'index', 'name', 'added_stories_count', 'published_stories_count', - 'notifications', 'subtopic_count', 'skill_count', 'topic_status' + 'index', + 'name', + 'added_stories_count', + 'published_stories_count', + 'notifications', + 'subtopic_count', + 'skill_count', + 'topic_status', ]); - mockPlatformFeatureService. - status.SerialChapterLaunchCurriculumAdminView.isEnabled = false; + mockPlatformFeatureService.status.SerialChapterLaunchCurriculumAdminView.isEnabled = + false; componentInstance.ngOnInit(); expect(componentInstance.TOPIC_HEADINGS.length).toBe(6); expect(componentInstance.TOPIC_HEADINGS).toEqual([ - 'index', 'name', 'canonical_story_count', 'subtopic_count', - 'skill_count', 'topic_status' + 'index', + 'name', + 'canonical_story_count', + 'subtopic_count', + 'skill_count', + 'topic_status', ]); }); @@ -196,13 +201,15 @@ describe('Topics List Component', () => { it('should destory correctly', () => { spyOn(componentInstance.directiveSubscriptions, 'unsubscribe'); componentInstance.ngOnDestroy(); - expect(componentInstance.directiveSubscriptions.unsubscribe) - .toHaveBeenCalled(); + expect( + componentInstance.directiveSubscriptions.unsubscribe + ).toHaveBeenCalled(); }); it('should get topic editor url', () => { - spyOn(urlInterpolationService, 'interpolateUrl').and - .returnValue('test_url'); + spyOn(urlInterpolationService, 'interpolateUrl').and.returnValue( + 'test_url' + ); expect(componentInstance.getTopicEditorUrl('')).toEqual('test_url'); }); @@ -228,20 +235,23 @@ describe('Topics List Component', () => { componentInstance.pageNumber = pageNumber; componentInstance.itemsPerPage = itemsPerPage; let topicIndex: number = 3; - let expectedSerialNumber: number = topicIndex + 1 + - (pageNumber * itemsPerPage); - expect(componentInstance.getSerialNumberForTopic(topicIndex)) - .toEqual(expectedSerialNumber); + let expectedSerialNumber: number = + topicIndex + 1 + pageNumber * itemsPerPage; + expect(componentInstance.getSerialNumberForTopic(topicIndex)).toEqual( + expectedSerialNumber + ); }); it('should delete topic', () => { spyOn( - topicsAndSkillsDashboardBackendApiService - .onTopicsAndSkillsDashboardReinitialized, 'emit'); + topicsAndSkillsDashboardBackendApiService.onTopicsAndSkillsDashboardReinitialized, + 'emit' + ); componentInstance.deleteTopic(topicId, topicName); expect( topicsAndSkillsDashboardBackendApiService - .onTopicsAndSkillsDashboardReinitialized.emit).toHaveBeenCalled(); + .onTopicsAndSkillsDashboardReinitialized.emit + ).toHaveBeenCalled(); }); it('should handle modal cancel', () => { @@ -251,11 +261,11 @@ describe('Topics List Component', () => { it('should handle error when deleting topic', () => { editableTopicBackendApiService.success = false; - spyOn( - alertsService, 'addWarning'); + spyOn(alertsService, 'addWarning'); componentInstance.deleteTopic(topicId, topicName); expect(alertsService.addWarning).toHaveBeenCalledWith( - 'There was an error when deleting the topic.'); + 'There was an error when deleting the topic.' + ); }); it('should handle error when deleting topic and show error message', () => { @@ -266,56 +276,57 @@ describe('Topics List Component', () => { expect(alertsService.addWarning).toHaveBeenCalledWith('error_message'); }); - it('should update the chapter counts upon changing the input topic ' + - 'summaries', () => { - let topic = CreatorTopicSummary.createFromBackendDict({ - topic_model_created_on: 1581839432987.596, - uncategorized_skill_count: 0, - canonical_story_count: 1, - id: 'wbL5aAyTWfOH1', - is_published: true, - total_skill_count: 10, - total_published_node_count: 6, - can_edit_topic: true, - topic_model_last_updated: 1581839492500.852, - additional_story_count: 0, - name: 'Alpha', - classroom: 'Math', - version: 1, - description: 'Alpha description', - subtopic_count: 0, - language_code: 'en', - url_fragment: 'alpha', - thumbnail_filename: 'image.svg', - thumbnail_bg_color: '#C6DCDA', - total_upcoming_chapters_count: 1, - total_overdue_chapters_count: 1, - total_chapter_counts_for_each_story: [5, 4], - published_chapter_counts_for_each_story: [3, 4] - }); - componentInstance.topicSummaries = [topic]; - - componentInstance.ngOnChanges(); - - expect( - componentInstance.fullyPublishedStoriesCounts.length).toBe(1); - expect(componentInstance.fullyPublishedStoriesCounts[0]).toBe(1); - expect( - componentInstance.partiallyPublishedStoriesCounts.length).toBe(1); - expect( - componentInstance.partiallyPublishedStoriesCounts[0]).toBe(1); - expect( - componentInstance. - totalChaptersInPartiallyPublishedStories.length).toBe(1); - expect( - componentInstance.totalChaptersInPartiallyPublishedStories[0]).toBe(5); - expect( - componentInstance. - publishedChaptersInPartiallyPublishedStories.length).toBe(1); - expect( - componentInstance. - publishedChaptersInPartiallyPublishedStories[0]).toBe(3); - }); + it( + 'should update the chapter counts upon changing the input topic ' + + 'summaries', + () => { + let topic = CreatorTopicSummary.createFromBackendDict({ + topic_model_created_on: 1581839432987.596, + uncategorized_skill_count: 0, + canonical_story_count: 1, + id: 'wbL5aAyTWfOH1', + is_published: true, + total_skill_count: 10, + total_published_node_count: 6, + can_edit_topic: true, + topic_model_last_updated: 1581839492500.852, + additional_story_count: 0, + name: 'Alpha', + classroom: 'Math', + version: 1, + description: 'Alpha description', + subtopic_count: 0, + language_code: 'en', + url_fragment: 'alpha', + thumbnail_filename: 'image.svg', + thumbnail_bg_color: '#C6DCDA', + total_upcoming_chapters_count: 1, + total_overdue_chapters_count: 1, + total_chapter_counts_for_each_story: [5, 4], + published_chapter_counts_for_each_story: [3, 4], + }); + componentInstance.topicSummaries = [topic]; + + componentInstance.ngOnChanges(); + + expect(componentInstance.fullyPublishedStoriesCounts.length).toBe(1); + expect(componentInstance.fullyPublishedStoriesCounts[0]).toBe(1); + expect(componentInstance.partiallyPublishedStoriesCounts.length).toBe(1); + expect(componentInstance.partiallyPublishedStoriesCounts[0]).toBe(1); + expect( + componentInstance.totalChaptersInPartiallyPublishedStories.length + ).toBe(1); + expect( + componentInstance.totalChaptersInPartiallyPublishedStories[0] + ).toBe(5); + expect( + componentInstance.publishedChaptersInPartiallyPublishedStories.length + ).toBe(1); + expect( + componentInstance.publishedChaptersInPartiallyPublishedStories[0] + ).toBe(3); + } + ); it('should get text for upcoming chapter notifications', () => { let topic = CreatorTopicSummary.createFromBackendDict({ @@ -341,16 +352,20 @@ describe('Topics List Component', () => { total_upcoming_chapters_count: 1, total_overdue_chapters_count: 1, total_chapter_counts_for_each_story: [5, 4], - published_chapter_counts_for_each_story: [3, 4] + published_chapter_counts_for_each_story: [3, 4], }); expect(componentInstance.getUpcomingChapterNotificationsText(topic)).toBe( '1 upcoming launch in the next ' + - constants.CHAPTER_PUBLICATION_NOTICE_PERIOD_IN_DAYS + ' days'); + constants.CHAPTER_PUBLICATION_NOTICE_PERIOD_IN_DAYS + + ' days' + ); topic.totalUpcomingChaptersCount = 2; expect(componentInstance.getUpcomingChapterNotificationsText(topic)).toBe( - '2 upcoming launches in the next ' + constants. - CHAPTER_PUBLICATION_NOTICE_PERIOD_IN_DAYS + ' days'); + '2 upcoming launches in the next ' + + constants.CHAPTER_PUBLICATION_NOTICE_PERIOD_IN_DAYS + + ' days' + ); }); it('should get text for upcoming chapter notifications', () => { @@ -377,14 +392,16 @@ describe('Topics List Component', () => { total_upcoming_chapters_count: 1, total_overdue_chapters_count: 1, total_chapter_counts_for_each_story: [5, 4], - published_chapter_counts_for_each_story: [3, 4] + published_chapter_counts_for_each_story: [3, 4], }); expect(componentInstance.getOverdueChapterNotificationsText(topic)).toBe( - '1 launch behind schedule'); + '1 launch behind schedule' + ); topic.totalOverdueChaptersCount = 2; expect(componentInstance.getOverdueChapterNotificationsText(topic)).toBe( - '2 launches behind schedule'); + '2 launches behind schedule' + ); }); it('should return if all topic chapters are published', () => { @@ -411,13 +428,14 @@ describe('Topics List Component', () => { total_upcoming_chapters_count: 1, total_overdue_chapters_count: 1, total_chapter_counts_for_each_story: [5, 4], - published_chapter_counts_for_each_story: [5, 4] + published_chapter_counts_for_each_story: [5, 4], }); componentInstance.topicSummaries = [topic]; componentInstance.ngOnChanges(); - expect(componentInstance.areTopicChaptersFullyPublished( - topic, 0)).toBeTrue(); + expect( + componentInstance.areTopicChaptersFullyPublished(topic, 0) + ).toBeTrue(); topic.totalChaptersCounts = []; topic.publishedChaptersCounts = []; @@ -425,7 +443,8 @@ describe('Topics List Component', () => { componentInstance.topicSummaries = [topic]; componentInstance.ngOnChanges(); - expect(componentInstance.areTopicChaptersFullyPublished( - topic, 0)).toBeFalse(); + expect( + componentInstance.areTopicChaptersFullyPublished(topic, 0) + ).toBeFalse(); }); }); diff --git a/core/templates/pages/topics-and-skills-dashboard-page/topics-list/topics-list.component.ts b/core/templates/pages/topics-and-skills-dashboard-page/topics-list/topics-list.component.ts index da58997c98ba..32ecd9a21f3b 100644 --- a/core/templates/pages/topics-and-skills-dashboard-page/topics-list/topics-list.component.ts +++ b/core/templates/pages/topics-and-skills-dashboard-page/topics-list/topics-list.component.ts @@ -16,22 +16,22 @@ * @fileoverview Component for the topics list viewer. */ -import { Component, Input, EventEmitter, Output } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { EditableTopicBackendApiService } from 'domain/topic/editable-topic-backend-api.service'; -import { CreatorTopicSummary } from 'domain/topic/creator-topic-summary.model'; -import { TopicsAndSkillsDashboardBackendApiService } from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { Subscription } from 'rxjs'; -import { AlertsService } from 'services/alerts.service'; -import { DeleteTopicModalComponent } from '../modals/delete-topic-modal.component'; -import { PlatformFeatureService } from 'services/platform-feature.service'; +import {Component, Input, EventEmitter, Output} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {EditableTopicBackendApiService} from 'domain/topic/editable-topic-backend-api.service'; +import {CreatorTopicSummary} from 'domain/topic/creator-topic-summary.model'; +import {TopicsAndSkillsDashboardBackendApiService} from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {Subscription} from 'rxjs'; +import {AlertsService} from 'services/alerts.service'; +import {DeleteTopicModalComponent} from '../modals/delete-topic-modal.component'; +import {PlatformFeatureService} from 'services/platform-feature.service'; import constants from 'assets/constants'; @Component({ selector: 'oppia-topics-list', - templateUrl: './topics-list.component.html' + templateUrl: './topics-list.component.html', }) export class TopicsListComponent { // These properties are initialized using Angular lifecycle hooks @@ -42,8 +42,7 @@ export class TopicsListComponent { @Input() itemsPerPage!: number; @Input() userCanDeleteTopic!: boolean; @Input() selectedTopicIds!: string; - @Output() selectedTopicIdsChange: EventEmitter = ( - new EventEmitter()); + @Output() selectedTopicIdsChange: EventEmitter = new EventEmitter(); directiveSubscriptions: Subscription = new Subscription(); // Selected topic index is set to null when the delete topic modal is closed. @@ -55,16 +54,19 @@ export class TopicsListComponent { publishedChaptersInPartiallyPublishedStories: number[] = []; totalChaptersInPartiallyPublishedStories: number[] = []; TOPIC_HEADINGS: string[] = [ - 'index', 'name', 'canonical_story_count', 'subtopic_count', - 'skill_count', 'topic_status' + 'index', + 'name', + 'canonical_story_count', + 'subtopic_count', + 'skill_count', + 'topic_status', ]; constructor( private ngbModal: NgbModal, private alertsService: AlertsService, private editableTopicBackendApiService: EditableTopicBackendApiService, - private topicsAndSkillsDashboardBackendApiService: - TopicsAndSkillsDashboardBackendApiService, + private topicsAndSkillsDashboardBackendApiService: TopicsAndSkillsDashboardBackendApiService, private urlInterpolationService: UrlInterpolationService, private platformFeatureService: PlatformFeatureService ) {} @@ -77,9 +79,11 @@ export class TopicsListComponent { getTopicEditorUrl(topicId: string): string { const TOPIC_EDITOR_URL_TEMPLATE = '/topic_editor/#/'; return this.urlInterpolationService.interpolateUrl( - TOPIC_EDITOR_URL_TEMPLATE, { - topic_id: topicId - }); + TOPIC_EDITOR_URL_TEMPLATE, + { + topic_id: topicId, + } + ); } /** @@ -100,15 +104,15 @@ export class TopicsListComponent { /** ** @param {Number} topicIndex - Index of the topic in - * the topicSummaries. - * @returns {Number} The calculated serial number - * of the topic taking into consideration the current page - * number and the items being displayed per page. - */ + * the topicSummaries. + * @returns {Number} The calculated serial number + * of the topic taking into consideration the current page + * number and the items being displayed per page. + */ getSerialNumberForTopic(topicIndex: number): number { - let topicSerialNumber: number = ( - topicIndex + (this.pageNumber * this.itemsPerPage)); - return (topicSerialNumber + 1); + let topicSerialNumber: number = + topicIndex + this.pageNumber * this.itemsPerPage; + return topicSerialNumber + 1; } /** @@ -119,21 +123,24 @@ export class TopicsListComponent { this.selectedIndex = null; let modalRef: NgbModalRef = this.ngbModal.open(DeleteTopicModalComponent, { backdrop: true, - windowClass: 'delete-topic-modal' + windowClass: 'delete-topic-modal', }); modalRef.componentInstance.topicName = topicName; - modalRef.result.then(() => { - this.editableTopicBackendApiService.deleteTopicAsync(topicId).then( - (status: number) => { - this.topicsAndSkillsDashboardBackendApiService. - onTopicsAndSkillsDashboardReinitialized.emit(); - }, - (error: string) => { - this.alertsService.addWarning( - error || 'There was an error when deleting the topic.'); - } - ); - }, () => {}); + modalRef.result.then( + () => { + this.editableTopicBackendApiService.deleteTopicAsync(topicId).then( + (status: number) => { + this.topicsAndSkillsDashboardBackendApiService.onTopicsAndSkillsDashboardReinitialized.emit(); + }, + (error: string) => { + this.alertsService.addWarning( + error || 'There was an error when deleting the topic.' + ); + } + ); + }, + () => {} + ); } /** @@ -141,8 +148,8 @@ export class TopicsListComponent { * flag is enabled. */ isSerialChapterLaunchFeatureEnabled(): boolean { - return this.platformFeatureService.status. - SerialChapterLaunchCurriculumAdminView.isEnabled; + return this.platformFeatureService.status + .SerialChapterLaunchCurriculumAdminView.isEnabled; } /** @@ -152,14 +159,15 @@ export class TopicsListComponent { * HTML template. */ getUpcomingChapterNotificationsText(topic: CreatorTopicSummary): string { - let upcomingChapterNotificationsText = ( - topic.getTotalUpcomingChaptersCount() + ' upcoming launch'); + let upcomingChapterNotificationsText = + topic.getTotalUpcomingChaptersCount() + ' upcoming launch'; if (topic.getTotalUpcomingChaptersCount() > 1) { upcomingChapterNotificationsText += 'es'; } - upcomingChapterNotificationsText += ( + upcomingChapterNotificationsText += ' in the next ' + - constants.CHAPTER_PUBLICATION_NOTICE_PERIOD_IN_DAYS + ' days'); + constants.CHAPTER_PUBLICATION_NOTICE_PERIOD_IN_DAYS + + ' days'; return upcomingChapterNotificationsText; } @@ -170,8 +178,8 @@ export class TopicsListComponent { * HTML template. */ getOverdueChapterNotificationsText(topic: CreatorTopicSummary): string { - let overdueChapterNotificationsText = ( - topic.getTotalOverdueChaptersCount() + ' launch'); + let overdueChapterNotificationsText = + topic.getTotalOverdueChaptersCount() + ' launch'; if (topic.getTotalOverdueChaptersCount() > 1) { overdueChapterNotificationsText += 'es'; } @@ -187,11 +195,14 @@ export class TopicsListComponent { * published. */ areTopicChaptersFullyPublished( - topic: CreatorTopicSummary, idx: number): boolean { + topic: CreatorTopicSummary, + idx: number + ): boolean { if ( topic.getTotalChaptersCounts().length && topic.getTotalChaptersCounts().length === - this.fullyPublishedStoriesCounts[idx]) { + this.fullyPublishedStoriesCounts[idx] + ) { return true; } else { return false; @@ -201,12 +212,23 @@ export class TopicsListComponent { ngOnInit(): void { if (this.isSerialChapterLaunchFeatureEnabled()) { this.TOPIC_HEADINGS = [ - 'index', 'name', 'added_stories_count', 'published_stories_count', - 'notifications', 'subtopic_count', 'skill_count', 'topic_status']; + 'index', + 'name', + 'added_stories_count', + 'published_stories_count', + 'notifications', + 'subtopic_count', + 'skill_count', + 'topic_status', + ]; } else { this.TOPIC_HEADINGS = [ - 'index', 'name', 'canonical_story_count', 'subtopic_count', - 'skill_count', 'topic_status' + 'index', + 'name', + 'canonical_story_count', + 'subtopic_count', + 'skill_count', + 'topic_status', ]; } } @@ -226,15 +248,17 @@ export class TopicsListComponent { let totalStories = this.topicSummaries[i].getTotalChaptersCounts().length; for (let j = 0; this.topicSummaries[i] && j < totalStories; j++) { - if (this.topicSummaries[i].getTotalChaptersCounts()[j] === - this.topicSummaries[i].getPublishedChaptersCounts()[j]) { + if ( + this.topicSummaries[i].getTotalChaptersCounts()[j] === + this.topicSummaries[i].getPublishedChaptersCounts()[j] + ) { this.fullyPublishedStoriesCounts[i]++; } else { this.partiallyPublishedStoriesCounts[i]++; this.totalChaptersInPartiallyPublishedStories[i] += - this.topicSummaries[i].getTotalChaptersCounts()[j]; + this.topicSummaries[i].getTotalChaptersCounts()[j]; this.publishedChaptersInPartiallyPublishedStories[i] += - this.topicSummaries[i].getPublishedChaptersCounts()[j]; + this.topicSummaries[i].getPublishedChaptersCounts()[j]; } } } @@ -245,5 +269,9 @@ export class TopicsListComponent { } } -angular.module('oppia').directive('oppiaTopicsList', - downgradeComponent({ component: TopicsListComponent })); +angular + .module('oppia') + .directive( + 'oppiaTopicsList', + downgradeComponent({component: TopicsListComponent}) + ); diff --git a/core/templates/pages/voiceover-admin-page/modals/language-accent-removal-confirm-modal.component.spec.ts b/core/templates/pages/voiceover-admin-page/modals/language-accent-removal-confirm-modal.component.spec.ts index a150c26a130e..ee50652d8cf6 100644 --- a/core/templates/pages/voiceover-admin-page/modals/language-accent-removal-confirm-modal.component.spec.ts +++ b/core/templates/pages/voiceover-admin-page/modals/language-accent-removal-confirm-modal.component.spec.ts @@ -16,10 +16,9 @@ * @fileoverview Tests for language accent removal confirmation modal. */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { VoiceoverRemovalConfirmModalComponent } from './language-accent-removal-confirm-modal.component'; - +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {VoiceoverRemovalConfirmModalComponent} from './language-accent-removal-confirm-modal.component'; describe('Language Accent Removal Confirmation Modal', () => { let fixture: ComponentFixture; @@ -29,12 +28,8 @@ describe('Language Accent Removal Confirmation Modal', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - VoiceoverRemovalConfirmModalComponent - ], - providers: [ - NgbActiveModal, - ] + declarations: [VoiceoverRemovalConfirmModalComponent], + providers: [NgbActiveModal], }).compileComponents(); })); diff --git a/core/templates/pages/voiceover-admin-page/modals/language-accent-removal-confirm-modal.component.ts b/core/templates/pages/voiceover-admin-page/modals/language-accent-removal-confirm-modal.component.ts index 25341febdc1c..e4705c394bfc 100644 --- a/core/templates/pages/voiceover-admin-page/modals/language-accent-removal-confirm-modal.component.ts +++ b/core/templates/pages/voiceover-admin-page/modals/language-accent-removal-confirm-modal.component.ts @@ -16,21 +16,17 @@ * @fileoverview Close language accent removal confirmation modal. */ -import { Component } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmOrCancelModal } from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; - +import {Component} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ConfirmOrCancelModal} from 'components/common-layout-directives/common-elements/confirm-or-cancel-modal.component'; @Component({ selector: 'oppia-language-accent-removal-confirm-modal', - templateUrl: './language-accent-removal-confirm-modal.component.html' + templateUrl: './language-accent-removal-confirm-modal.component.html', }) -export class VoiceoverRemovalConfirmModalComponent - extends ConfirmOrCancelModal { +export class VoiceoverRemovalConfirmModalComponent extends ConfirmOrCancelModal { languageAccentDescription: string = ''; - constructor( - private ngbActiveModal: NgbActiveModal - ) { + constructor(private ngbActiveModal: NgbActiveModal) { super(ngbActiveModal); } diff --git a/core/templates/pages/voiceover-admin-page/navbar/voiceover-admin-navbar.component.spec.ts b/core/templates/pages/voiceover-admin-page/navbar/voiceover-admin-navbar.component.spec.ts index d1b70ce154f8..a87889322b7c 100644 --- a/core/templates/pages/voiceover-admin-page/navbar/voiceover-admin-navbar.component.spec.ts +++ b/core/templates/pages/voiceover-admin-page/navbar/voiceover-admin-navbar.component.spec.ts @@ -16,23 +16,27 @@ * @fileoverview Unit tests for voiceover admin navbar component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { RouterModule } from '@angular/router'; -import { APP_BASE_HREF } from '@angular/common'; - -import { UserService } from 'services/user.service'; -import { VoiceoverAdminNavbarComponent } from './voiceover-admin-navbar.component'; -import { SmartRouterModule } from 'hybrid-router-module-provider'; -import { UserInfo } from 'domain/user/user-info.model'; - +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {RouterModule} from '@angular/router'; +import {APP_BASE_HREF} from '@angular/common'; + +import {UserService} from 'services/user.service'; +import {VoiceoverAdminNavbarComponent} from './voiceover-admin-navbar.component'; +import {SmartRouterModule} from 'hybrid-router-module-provider'; +import {UserInfo} from 'domain/user/user-info.model'; describe('Vocieover Admin navbar component', () => { let component: VoiceoverAdminNavbarComponent; let userService: UserService; let userInfo = { getUsername: () => 'username1', - isSuperAdmin: () => true + isSuperAdmin: () => true, }; let profileUrl = '/profile/username1'; let fixture: ComponentFixture; @@ -43,33 +47,37 @@ describe('Vocieover Admin navbar component', () => { // TODO(#13443): Remove hybrid router module provider once all pages are // migrated to angular router. SmartRouterModule, - RouterModule.forRoot([]) + RouterModule.forRoot([]), ], declarations: [VoiceoverAdminNavbarComponent], - providers: [{ - provide: APP_BASE_HREF, - useValue: '/' - }] + providers: [ + { + provide: APP_BASE_HREF, + useValue: '/', + }, + ], }).compileComponents(); fixture = TestBed.createComponent(VoiceoverAdminNavbarComponent); component = fixture.componentInstance; userService = TestBed.inject(UserService); fixture.detectChanges(); - spyOn(userService, 'getProfileImageDataUrl').and.returnValue( - ['default-image-url-png', 'default-image-url-webp']); + spyOn(userService, 'getProfileImageDataUrl').and.returnValue([ + 'default-image-url-png', + 'default-image-url-webp', + ]); })); it('should initialize component properties correctly', fakeAsync(() => { - spyOn(userService, 'getUserInfoAsync') - .and.resolveTo(userInfo as UserInfo); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo as UserInfo); component.ngOnInit(); tick(); expect(component.profilePicturePngDataUrl).toEqual('default-image-url-png'); expect(component.profilePictureWebpDataUrl).toEqual( - 'default-image-url-webp'); + 'default-image-url-webp' + ); expect(component.profileUrl).toBe(profileUrl); expect(component.profileDropdownIsActive).toBe(false); })); @@ -77,10 +85,9 @@ describe('Vocieover Admin navbar component', () => { it('should throw error if username is invalid', fakeAsync(() => { let userInfo = { getUsername: () => null, - isSuperAdmin: () => true + isSuperAdmin: () => true, }; - spyOn(userService, 'getUserInfoAsync') - .and.resolveTo(userInfo as UserInfo); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo as UserInfo); expect(() => { component.ngOnInit(); @@ -89,8 +96,7 @@ describe('Vocieover Admin navbar component', () => { })); it('should set profileDropdownIsActive to true', fakeAsync(() => { - spyOn(userService, 'getUserInfoAsync') - .and.resolveTo(userInfo as UserInfo); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo as UserInfo); component.ngOnInit(); tick(); @@ -103,8 +109,7 @@ describe('Vocieover Admin navbar component', () => { })); it('should set profileDropdownIsActive to false', fakeAsync(() => { - spyOn(userService, 'getUserInfoAsync') - .and.resolveTo(userInfo as UserInfo); + spyOn(userService, 'getUserInfoAsync').and.resolveTo(userInfo as UserInfo); component.ngOnInit(); tick(); diff --git a/core/templates/pages/voiceover-admin-page/navbar/voiceover-admin-navbar.component.ts b/core/templates/pages/voiceover-admin-page/navbar/voiceover-admin-navbar.component.ts index 644f6ab00f41..0c3dbdcfc119 100644 --- a/core/templates/pages/voiceover-admin-page/navbar/voiceover-admin-navbar.component.ts +++ b/core/templates/pages/voiceover-admin-page/navbar/voiceover-admin-navbar.component.ts @@ -17,13 +17,12 @@ * panel. */ -import { Component, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; - -import { AppConstants } from 'app.constants'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { UserService } from 'services/user.service'; +import {Component, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {AppConstants} from 'app.constants'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {UserService} from 'services/user.service'; @Component({ selector: 'oppia-voiceover-admin-navbar', @@ -39,22 +38,21 @@ export class VoiceoverAdminNavbarComponent implements OnInit { username!: string | null; logoWebpImageSrc!: string; logoPngImageSrc!: string; - PAGES_REGISTERED_WITH_FRONTEND = ( - AppConstants.PAGES_REGISTERED_WITH_FRONTEND); + PAGES_REGISTERED_WITH_FRONTEND = AppConstants.PAGES_REGISTERED_WITH_FRONTEND; profileDropdownIsActive: boolean = false; constructor( private urlInterpolationService: UrlInterpolationService, - private userService: UserService, + private userService: UserService ) {} activateProfileDropdown(): boolean { - return this.profileDropdownIsActive = true; + return (this.profileDropdownIsActive = true); } deactivateProfileDropdown(): boolean { - return this.profileDropdownIsActive = false; + return (this.profileDropdownIsActive = false); } async getUserInfoAsync(): Promise { @@ -64,25 +62,31 @@ export class VoiceoverAdminNavbarComponent implements OnInit { if (this.username === null) { throw new Error('Cannot fetch username.'); } - this.profileUrl = ( - this.urlInterpolationService.interpolateUrl( - '/profile/', { - username: this.username - })); - [this.profilePicturePngDataUrl, this.profilePictureWebpDataUrl] = ( - this.userService.getProfileImageDataUrl(this.username)); + this.profileUrl = this.urlInterpolationService.interpolateUrl( + '/profile/', + { + username: this.username, + } + ); + [this.profilePicturePngDataUrl, this.profilePictureWebpDataUrl] = + this.userService.getProfileImageDataUrl(this.username); } ngOnInit(): void { this.getUserInfoAsync(); this.logoPngImageSrc = this.urlInterpolationService.getStaticImageUrl( - '/logo/288x128_logo_white.png'); + '/logo/288x128_logo_white.png' + ); this.logoWebpImageSrc = this.urlInterpolationService.getStaticImageUrl( - '/logo/288x128_logo_white.webp'); + '/logo/288x128_logo_white.webp' + ); } } -angular.module('oppia').directive( - 'oppiaVoiceoverAdminNavbar', downgradeComponent( - {component: VoiceoverAdminNavbarComponent})); +angular + .module('oppia') + .directive( + 'oppiaVoiceoverAdminNavbar', + downgradeComponent({component: VoiceoverAdminNavbarComponent}) + ); diff --git a/core/templates/pages/voiceover-admin-page/voiceover-admin-page.component.spec.ts b/core/templates/pages/voiceover-admin-page/voiceover-admin-page.component.spec.ts index 542565f765f1..8b96b397b22e 100644 --- a/core/templates/pages/voiceover-admin-page/voiceover-admin-page.component.spec.ts +++ b/core/templates/pages/voiceover-admin-page/voiceover-admin-page.component.spec.ts @@ -16,28 +16,31 @@ * @fileoverview Tests for the voiceover admin component. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { MatAutocompleteModule } from '@angular/material/autocomplete'; -import { VoiceoverAdminPageComponent } from './voiceover-admin-page.component'; -import { VoiceoverBackendApiService} from '../../domain/voiceover/voiceover-backend-api.service'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { MaterialModule } from 'modules/material.module'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; - +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {MatAutocompleteModule} from '@angular/material/autocomplete'; +import {VoiceoverAdminPageComponent} from './voiceover-admin-page.component'; +import {VoiceoverBackendApiService} from '../../domain/voiceover/voiceover-backend-api.service'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {MaterialModule} from 'modules/material.module'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; class MockNgbModal { open() { return { - result: Promise.resolve() + result: Promise.resolve(), }; } } - describe('Voiceover Admin Page component ', () => { let component: VoiceoverAdminPageComponent; let fixture: ComponentFixture; @@ -54,18 +57,15 @@ describe('Voiceover Admin Page component ', () => { MatAutocompleteModule, ReactiveFormsModule, ], - declarations: [ - VoiceoverAdminPageComponent, - MockTranslatePipe - ], + declarations: [VoiceoverAdminPageComponent, MockTranslatePipe], providers: [ VoiceoverBackendApiService, { provide: NgbModal, - useClass: MockNgbModal + useClass: MockNgbModal, }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(VoiceoverAdminPageComponent); component = fixture.componentInstance; @@ -79,18 +79,18 @@ describe('Voiceover Admin Page component ', () => { 'en-US': 'English (United State)', }, hi: { - 'hi-IN': 'Hindi (India)' - } + 'hi-IN': 'Hindi (India)', + }, }; let languageCodesMapping = { en: { - 'en-US': true - } + 'en-US': true, + }, }; component.availableLanguageAccentCodesToDescriptions = {}; let voiceoverAdminDataResponse = { languageAccentMasterList: languageAccentMasterList, - languageCodesMapping: languageCodesMapping + languageCodesMapping: languageCodesMapping, }; spyOn( voiceoverBackendApiService, @@ -108,23 +108,24 @@ describe('Voiceover Admin Page component ', () => { expect( voiceoverBackendApiService.fetchVoiceoverAdminDataAsync ).not.toHaveBeenCalledWith(voiceoverAdminDataResponse); - expect(component.availableLanguageAccentCodesToDescriptions).toEqual( - {'hi-IN': 'Hindi (India)'}); + expect(component.availableLanguageAccentCodesToDescriptions).toEqual({ + 'hi-IN': 'Hindi (India)', + }); expect(component.pageIsInitialized).toBeTrue(); })); it('should be able to add language accent pair', fakeAsync(() => { component.availableLanguageAccentCodesToDescriptions = { 'en-US': 'English (United States)', - 'hi-IN': 'Hindi (India)' + 'hi-IN': 'Hindi (India)', }; component.languageAccentCodesToDescriptionsMasterList = { 'en-US': 'English (United States)', - 'hi-IN': 'Hindi (India)' + 'hi-IN': 'Hindi (India)', }; component.languageAccentCodeToLanguageCode = { 'en-US': 'en', - 'hi-IN': 'hi' + 'hi-IN': 'hi', }; component.languageCodesMapping = {}; component.supportedLanguageAccentCodesToDescriptions = {}; @@ -135,38 +136,38 @@ describe('Voiceover Admin Page component ', () => { component.addLanguageAccentCodeSupport('en-US'); - expect(component.supportedLanguageAccentCodesToDescriptions).toEqual( - {'en-US': 'English (United States)'}); - expect(component.availableLanguageAccentCodesToDescriptions).toEqual( - {'hi-IN': 'Hindi (India)'}); + expect(component.supportedLanguageAccentCodesToDescriptions).toEqual({ + 'en-US': 'English (United States)', + }); + expect(component.availableLanguageAccentCodesToDescriptions).toEqual({ + 'hi-IN': 'Hindi (India)', + }); })); it('should be able to remove language accent pair', fakeAsync(() => { component.availableLanguageAccentCodesToDescriptions = { - 'hi-IN': 'Hindi (India)' + 'hi-IN': 'Hindi (India)', }; component.languageAccentCodesToDescriptionsMasterList = { 'en-US': 'English (United States)', - 'hi-IN': 'Hindi (India)' + 'hi-IN': 'Hindi (India)', }; component.languageAccentCodeToLanguageCode = { 'en-US': 'en', - 'hi-IN': 'hi' + 'hi-IN': 'hi', }; component.languageCodesMapping = { en: { - 'en-US': false - } + 'en-US': false, + }, }; component.supportedLanguageAccentCodesToDescriptions = { - 'en-US': 'English (United States)' - }; - spyOn(ngbModal, 'open').and.returnValue( - { - componentInstance: {}, - result: Promise.resolve() - } as NgbModalRef - ); + 'en-US': 'English (United States)', + }; + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: {}, + result: Promise.resolve(), + } as NgbModalRef); spyOn( voiceoverBackendApiService, 'updateVoiceoverLanguageCodesMappingAsync' @@ -177,49 +178,48 @@ describe('Voiceover Admin Page component ', () => { expect(ngbModal.open).toHaveBeenCalled(); expect(component.supportedLanguageAccentCodesToDescriptions).toEqual({}); - expect(component.availableLanguageAccentCodesToDescriptions).toEqual( - {'hi-IN': 'Hindi (India)', 'en-US': 'English (United States)'}); + expect(component.availableLanguageAccentCodesToDescriptions).toEqual({ + 'hi-IN': 'Hindi (India)', + 'en-US': 'English (United States)', + }); })); - it( - 'should not remove language accent pair when confirm modal is cancelled', - fakeAsync(() => { - component.availableLanguageAccentCodesToDescriptions = { - 'hi-IN': 'Hindi (India)' - }; - component.languageAccentCodesToDescriptionsMasterList = { - 'en-US': 'English (United States)', - 'hi-IN': 'Hindi (India)' - }; - component.languageAccentCodeToLanguageCode = { - 'en-US': 'en', - 'hi-IN': 'hi' - }; - component.languageCodesMapping = { - en: { - 'en-US': false - } - }; - component.supportedLanguageAccentCodesToDescriptions = { - 'en-US': 'English (United States)' - }; - spyOn(ngbModal, 'open').and.returnValue( - { - componentInstance: {}, - result: Promise.reject() - } as NgbModalRef - ); - - component.removeLanguageAccentCodeSupport('en-US'); - tick(); - - expect(ngbModal.open).toHaveBeenCalled(); - expect(component.supportedLanguageAccentCodesToDescriptions).toEqual({ - 'en-US': 'English (United States)' - }); - expect(component.availableLanguageAccentCodesToDescriptions).toEqual( - {'hi-IN': 'Hindi (India)'}); - })); + it('should not remove language accent pair when confirm modal is cancelled', fakeAsync(() => { + component.availableLanguageAccentCodesToDescriptions = { + 'hi-IN': 'Hindi (India)', + }; + component.languageAccentCodesToDescriptionsMasterList = { + 'en-US': 'English (United States)', + 'hi-IN': 'Hindi (India)', + }; + component.languageAccentCodeToLanguageCode = { + 'en-US': 'en', + 'hi-IN': 'hi', + }; + component.languageCodesMapping = { + en: { + 'en-US': false, + }, + }; + component.supportedLanguageAccentCodesToDescriptions = { + 'en-US': 'English (United States)', + }; + spyOn(ngbModal, 'open').and.returnValue({ + componentInstance: {}, + result: Promise.reject(), + } as NgbModalRef); + + component.removeLanguageAccentCodeSupport('en-US'); + tick(); + + expect(ngbModal.open).toHaveBeenCalled(); + expect(component.supportedLanguageAccentCodesToDescriptions).toEqual({ + 'en-US': 'English (United States)', + }); + expect(component.availableLanguageAccentCodesToDescriptions).toEqual({ + 'hi-IN': 'Hindi (India)', + }); + })); it('should be able to show language accent dropdown', () => { component.languageAccentDropdownIsShown = false; diff --git a/core/templates/pages/voiceover-admin-page/voiceover-admin-page.component.ts b/core/templates/pages/voiceover-admin-page/voiceover-admin-page.component.ts index 7001737b4579..97d3c0df32c8 100644 --- a/core/templates/pages/voiceover-admin-page/voiceover-admin-page.component.ts +++ b/core/templates/pages/voiceover-admin-page/voiceover-admin-page.component.ts @@ -16,22 +16,21 @@ * @fileoverview Voiceover admin component. */ -import { Component, OnInit } from '@angular/core'; -import { downgradeComponent } from '@angular/upgrade/static'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { VoiceoverRemovalConfirmModalComponent } from - './modals/language-accent-removal-confirm-modal.component'; +import {Component, OnInit} from '@angular/core'; +import {downgradeComponent} from '@angular/upgrade/static'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {VoiceoverRemovalConfirmModalComponent} from './modals/language-accent-removal-confirm-modal.component'; import { - VoiceoverBackendApiService, LanguageAccentToDescription, - LanguageCodesMapping, LanguageAccentMasterList + VoiceoverBackendApiService, + LanguageAccentToDescription, + LanguageCodesMapping, + LanguageAccentMasterList, } from 'domain/voiceover/voiceover-backend-api.service'; - interface LanguageAccentCodeToLanguageCode { [languageAccentCode: string]: string; } - @Component({ selector: 'oppia-voiceover-admin-page', templateUrl: './voiceover-admin-page.component.html', @@ -42,7 +41,7 @@ export class VoiceoverAdminPageComponent implements OnInit { // https://github.com/oppia/oppia/wiki/Guide-on-defining-types#ts-7-1 constructor( private ngbModal: NgbModal, - private voiceoverBackendApiService: VoiceoverBackendApiService, + private voiceoverBackendApiService: VoiceoverBackendApiService ) {} languageAccentCodeToLanguageCode!: LanguageAccentCodeToLanguageCode; @@ -55,133 +54,147 @@ export class VoiceoverAdminPageComponent implements OnInit { languageAccentCodeIsPresent: boolean = false; ngOnInit(): void { - this.voiceoverBackendApiService.fetchVoiceoverAdminDataAsync().then( - response => { + this.voiceoverBackendApiService + .fetchVoiceoverAdminDataAsync() + .then(response => { this.languageCodesMapping = response.languageCodesMapping; this.languageAccentCodeToLanguageCode = {}; this.supportedLanguageAccentCodesToDescriptions = {}; this.availableLanguageAccentCodesToDescriptions = {}; this.languageAccentCodesToDescriptionsMasterList = {}; this.initializeLanguageAccentCodesFields( - response.languageAccentMasterList); + response.languageAccentMasterList + ); this.pageIsInitialized = true; - } - ); + }); } initializeLanguageAccentCodesFields( - languageAccentMasterList: LanguageAccentMasterList): void { + languageAccentMasterList: LanguageAccentMasterList + ): void { for (let languageCode in languageAccentMasterList) { - const languageAccentCodesToDescriptions = ( - languageAccentMasterList[languageCode]); + const languageAccentCodesToDescriptions = + languageAccentMasterList[languageCode]; for (let languageAccentCode in languageAccentCodesToDescriptions) { - const languageAccentDescription = ( - languageAccentCodesToDescriptions[languageAccentCode]); + const languageAccentDescription = + languageAccentCodesToDescriptions[languageAccentCode]; - this.languageAccentCodeToLanguageCode[ - languageAccentCode] = languageCode; + this.languageAccentCodeToLanguageCode[languageAccentCode] = + languageCode; - this.languageAccentCodesToDescriptionsMasterList[ - languageAccentCode] = languageAccentDescription; + this.languageAccentCodesToDescriptionsMasterList[languageAccentCode] = + languageAccentDescription; } } for (let languageCode in this.languageCodesMapping) { - const languageAccentToSupportsAutogeneration = ( - this.languageCodesMapping[languageCode]); + const languageAccentToSupportsAutogeneration = + this.languageCodesMapping[languageCode]; for (let languageAccentCode in languageAccentToSupportsAutogeneration) { - const languageAccentDescription = ( - this.languageAccentCodesToDescriptionsMasterList[languageAccentCode]); + const languageAccentDescription = + this.languageAccentCodesToDescriptionsMasterList[languageAccentCode]; - this.supportedLanguageAccentCodesToDescriptions[ - languageAccentCode] = languageAccentDescription; + this.supportedLanguageAccentCodesToDescriptions[languageAccentCode] = + languageAccentDescription; } } - for (let languageAccentCode in - this.languageAccentCodesToDescriptionsMasterList) { - const languageAccentDescription = ( - this.languageAccentCodesToDescriptionsMasterList[languageAccentCode]); + for (let languageAccentCode in this + .languageAccentCodesToDescriptionsMasterList) { + const languageAccentDescription = + this.languageAccentCodesToDescriptionsMasterList[languageAccentCode]; - if (!(languageAccentCode in - this.supportedLanguageAccentCodesToDescriptions)) { - this.availableLanguageAccentCodesToDescriptions[ - languageAccentCode] = languageAccentDescription; + if ( + !(languageAccentCode in this.supportedLanguageAccentCodesToDescriptions) + ) { + this.availableLanguageAccentCodesToDescriptions[languageAccentCode] = + languageAccentDescription; } } - this.languageAccentCodeIsPresent = (Object.keys( - this.supportedLanguageAccentCodesToDescriptions).length !== 0); + this.languageAccentCodeIsPresent = + Object.keys(this.supportedLanguageAccentCodesToDescriptions).length !== 0; } addLanguageAccentCodeSupport(languageAccentCodeToAdd: string): void { - const languageCode = ( - this.languageAccentCodeToLanguageCode[languageAccentCodeToAdd]); - const languageAccentDescription = ( - this.languageAccentCodesToDescriptionsMasterList[ - languageAccentCodeToAdd]); + const languageCode = + this.languageAccentCodeToLanguageCode[languageAccentCodeToAdd]; + const languageAccentDescription = + this.languageAccentCodesToDescriptionsMasterList[languageAccentCodeToAdd]; - this.supportedLanguageAccentCodesToDescriptions[ - languageAccentCodeToAdd] = languageAccentDescription; + this.supportedLanguageAccentCodesToDescriptions[languageAccentCodeToAdd] = + languageAccentDescription; delete this.availableLanguageAccentCodesToDescriptions[ - languageAccentCodeToAdd]; + languageAccentCodeToAdd + ]; if (!(languageCode in this.languageCodesMapping)) { this.languageCodesMapping[languageCode] = {}; } this.languageCodesMapping[languageCode][languageAccentCodeToAdd] = false; - this.languageAccentCodeIsPresent = (Object.keys( - this.supportedLanguageAccentCodesToDescriptions).length !== 0); + this.languageAccentCodeIsPresent = + Object.keys(this.supportedLanguageAccentCodesToDescriptions).length !== 0; this.removeLanguageAccentDropdown(); this.saveUpdatedLanguageAccentSupport(); } removeLanguageAccentCodeSupport(languageAccentCodeToRemove: string): void { - const languageCode = ( - this.languageAccentCodeToLanguageCode[languageAccentCodeToRemove]); - const languageAccentDescription = ( + const languageCode = + this.languageAccentCodeToLanguageCode[languageAccentCodeToRemove]; + const languageAccentDescription = this.languageAccentCodesToDescriptionsMasterList[ - languageAccentCodeToRemove]); - - let modalRef: NgbModalRef = this.ngbModal. - open(VoiceoverRemovalConfirmModalComponent, { - backdrop: 'static' - }); - - modalRef.componentInstance.languageAccentDescription = ( - languageAccentDescription); + languageAccentCodeToRemove + ]; - modalRef.result.then(() => { - delete this.supportedLanguageAccentCodesToDescriptions[ - languageAccentCodeToRemove]; - this.availableLanguageAccentCodesToDescriptions[ - languageAccentCodeToRemove] = languageAccentDescription; + let modalRef: NgbModalRef = this.ngbModal.open( + VoiceoverRemovalConfirmModalComponent, + { + backdrop: 'static', + } + ); - delete this.languageCodesMapping[ - languageCode][languageAccentCodeToRemove]; + modalRef.componentInstance.languageAccentDescription = + languageAccentDescription; - if (Object.keys(this.languageCodesMapping[languageCode]).length === 0) { - delete this.languageCodesMapping[languageCode]; + modalRef.result.then( + () => { + delete this.supportedLanguageAccentCodesToDescriptions[ + languageAccentCodeToRemove + ]; + this.availableLanguageAccentCodesToDescriptions[ + languageAccentCodeToRemove + ] = languageAccentDescription; + + delete this.languageCodesMapping[languageCode][ + languageAccentCodeToRemove + ]; + + if (Object.keys(this.languageCodesMapping[languageCode]).length === 0) { + delete this.languageCodesMapping[languageCode]; + } + + this.languageAccentCodeIsPresent = + Object.keys(this.supportedLanguageAccentCodesToDescriptions) + .length !== 0; + this.saveUpdatedLanguageAccentSupport(); + }, + () => { + // Note to developers: + // This callback is triggered when the Cancel button is + // clicked. No further action is needed. } - - this.languageAccentCodeIsPresent = (Object.keys( - this.supportedLanguageAccentCodesToDescriptions).length !== 0); - this.saveUpdatedLanguageAccentSupport(); - }, () => { - // Note to developers: - // This callback is triggered when the Cancel button is - // clicked. No further action is needed. - }); + ); } saveUpdatedLanguageAccentSupport(): void { - this.voiceoverBackendApiService.updateVoiceoverLanguageCodesMappingAsync( - this.languageCodesMapping).then(() => { - this.removeLanguageAccentDropdown(); - }); + this.voiceoverBackendApiService + .updateVoiceoverLanguageCodesMappingAsync(this.languageCodesMapping) + .then(() => { + this.removeLanguageAccentDropdown(); + }); } showLanguageAccentDropdown(): void { @@ -193,6 +206,9 @@ export class VoiceoverAdminPageComponent implements OnInit { } } -angular.module('oppia').directive( - 'oppiaVoiceoverAdminPage', downgradeComponent( - {component: VoiceoverAdminPageComponent})); +angular + .module('oppia') + .directive( + 'oppiaVoiceoverAdminPage', + downgradeComponent({component: VoiceoverAdminPageComponent}) + ); diff --git a/core/templates/pages/voiceover-admin-page/voiceover-admin-page.import.ts b/core/templates/pages/voiceover-admin-page/voiceover-admin-page.import.ts index 2635570c6169..519509c6db86 100644 --- a/core/templates/pages/voiceover-admin-page/voiceover-admin-page.import.ts +++ b/core/templates/pages/voiceover-admin-page/voiceover-admin-page.import.ts @@ -22,9 +22,14 @@ import 'zone.js'; import 'angular-ui-validate'; angular.module('oppia', [ - require('angular-cookies'), 'ngAnimate', - 'ngMaterial', 'ngSanitize', 'ngTouch', 'pascalprecht.translate', - 'ui.bootstrap', 'ui.validate' + require('angular-cookies'), + 'ngAnimate', + 'ngMaterial', + 'ngSanitize', + 'ngTouch', + 'pascalprecht.translate', + 'ui.bootstrap', + 'ui.validate', ]); require('Polyfills.ts'); diff --git a/core/templates/pages/voiceover-admin-page/voiceover-admin-page.module.ts b/core/templates/pages/voiceover-admin-page/voiceover-admin-page.module.ts index b9a920c8ed4b..78d7cfc15567 100644 --- a/core/templates/pages/voiceover-admin-page/voiceover-admin-page.module.ts +++ b/core/templates/pages/voiceover-admin-page/voiceover-admin-page.module.ts @@ -16,32 +16,32 @@ * @fileoverview Module for the voicover-admin page. */ +import {HttpClientModule, HTTP_INTERCEPTORS} from '@angular/common/http'; +import {APP_INITIALIZER, DoBootstrap, NgModule} from '@angular/core'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {MatCardModule} from '@angular/material/card'; +import {MatTooltipModule} from '@angular/material/tooltip'; +import {BrowserModule, HAMMER_GESTURE_CONFIG} from '@angular/platform-browser'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {downgradeComponent, downgradeModule} from '@angular/upgrade/static'; +import {RouterModule} from '@angular/router'; +import {APP_BASE_HREF} from '@angular/common'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; -import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; -import { APP_INITIALIZER, DoBootstrap, NgModule } from '@angular/core'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { MatCardModule } from '@angular/material/card'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { BrowserModule, HAMMER_GESTURE_CONFIG } from '@angular/platform-browser'; -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { downgradeComponent, downgradeModule } from '@angular/upgrade/static'; -import { RouterModule } from '@angular/router'; -import { APP_BASE_HREF } from '@angular/common'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; - - -import { OppiaAngularRootComponent } from 'components/oppia-angular-root.component'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { platformFeatureInitFactory, PlatformFeatureService } from 'services/platform-feature.service'; -import { RequestInterceptor } from 'services/request-interceptor.service'; -import { ToastrModule } from 'ngx-toastr'; -import { MyHammerConfig, toastrConfig } from 'pages/oppia-root/app.module'; -import { SmartRouterModule } from 'hybrid-router-module-provider'; -import { AppErrorHandlerProvider } from 'pages/oppia-root/app-error-handler'; -import { VoiceoverAdminPageComponent } from './voiceover-admin-page.component'; -import { VoiceoverAdminNavbarComponent } from './navbar/voiceover-admin-navbar.component'; -import { VoiceoverRemovalConfirmModalComponent } from './modals/language-accent-removal-confirm-modal.component'; - +import {OppiaAngularRootComponent} from 'components/oppia-angular-root.component'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import { + platformFeatureInitFactory, + PlatformFeatureService, +} from 'services/platform-feature.service'; +import {RequestInterceptor} from 'services/request-interceptor.service'; +import {ToastrModule} from 'ngx-toastr'; +import {MyHammerConfig, toastrConfig} from 'pages/oppia-root/app.module'; +import {SmartRouterModule} from 'hybrid-router-module-provider'; +import {AppErrorHandlerProvider} from 'pages/oppia-root/app-error-handler'; +import {VoiceoverAdminPageComponent} from './voiceover-admin-page.component'; +import {VoiceoverAdminNavbarComponent} from './navbar/voiceover-admin-navbar.component'; +import {VoiceoverRemovalConfirmModalComponent} from './modals/language-accent-removal-confirm-modal.component'; declare var angular: ng.IAngularStatic; @@ -59,17 +59,17 @@ declare var angular: ng.IAngularStatic; MatTooltipModule, ReactiveFormsModule, SharedComponentsModule, - ToastrModule.forRoot(toastrConfig) + ToastrModule.forRoot(toastrConfig), ], declarations: [ VoiceoverAdminPageComponent, VoiceoverAdminNavbarComponent, - VoiceoverRemovalConfirmModalComponent + VoiceoverRemovalConfirmModalComponent, ], entryComponents: [ VoiceoverAdminPageComponent, VoiceoverAdminNavbarComponent, - VoiceoverRemovalConfirmModalComponent + VoiceoverRemovalConfirmModalComponent, ], providers: [ { @@ -85,24 +85,29 @@ declare var angular: ng.IAngularStatic; }, { provide: HAMMER_GESTURE_CONFIG, - useClass: MyHammerConfig + useClass: MyHammerConfig, }, AppErrorHandlerProvider, { provide: APP_BASE_HREF, - useValue: '/' - } + useValue: '/', + }, ], }) class VoiceoverAdminPageModule implements DoBootstrap { ngDoBootstrap() {} } -angular.module('oppia').requires.push(downgradeModule(extraProviders => { - const platformRef = platformBrowserDynamic(extraProviders); - return platformRef.bootstrapModule(VoiceoverAdminPageModule); -})); +angular.module('oppia').requires.push( + downgradeModule(extraProviders => { + const platformRef = platformBrowserDynamic(extraProviders); + return platformRef.bootstrapModule(VoiceoverAdminPageModule); + }) +); -angular.module('oppia').directive('oppiaAngularRoot', downgradeComponent({ - component: OppiaAngularRootComponent, -})); +angular.module('oppia').directive( + 'oppiaAngularRoot', + downgradeComponent({ + component: OppiaAngularRootComponent, + }) +); diff --git a/core/templates/pages/volunteer-page/volunteer-page-root.component.spec.ts b/core/templates/pages/volunteer-page/volunteer-page-root.component.spec.ts index 086984da41b5..8bd3440235f3 100644 --- a/core/templates/pages/volunteer-page/volunteer-page-root.component.spec.ts +++ b/core/templates/pages/volunteer-page/volunteer-page-root.component.spec.ts @@ -16,14 +16,14 @@ * @fileoverview Unit tests for the volunteer page root component. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; -import { VolunteerPageRootComponent } from './volunteer-page-root.component'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; +import {VolunteerPageRootComponent} from './volunteer-page-root.component'; describe('Volunteer Page Root', () => { let fixture: ComponentFixture; @@ -32,14 +32,9 @@ describe('Volunteer Page Root', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - VolunteerPageRootComponent, - MockTranslatePipe - ], - providers: [ - PageHeadService - ], - schemas: [NO_ERRORS_SCHEMA] + declarations: [VolunteerPageRootComponent, MockTranslatePipe], + providers: [PageHeadService], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -49,10 +44,9 @@ describe('Volunteer Page Root', () => { pageHeadService = TestBed.inject(PageHeadService); }); - it('should successfully instantiate the component', - () => { - expect(component).toBeDefined(); - }); + it('should successfully instantiate the component', () => { + expect(component).toBeDefined(); + }); it('should initialize', () => { spyOn(pageHeadService, 'updateTitleAndMetaTags'); @@ -61,6 +55,7 @@ describe('Volunteer Page Root', () => { expect(pageHeadService.updateTitleAndMetaTags).toHaveBeenCalledWith( AppConstants.PAGES_REGISTERED_WITH_FRONTEND.VOLUNTEER.TITLE, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.VOLUNTEER.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.VOLUNTEER.META + ); }); }); diff --git a/core/templates/pages/volunteer-page/volunteer-page-root.component.ts b/core/templates/pages/volunteer-page/volunteer-page-root.component.ts index e9e21b7f7a30..48c88744f2af 100644 --- a/core/templates/pages/volunteer-page/volunteer-page-root.component.ts +++ b/core/templates/pages/volunteer-page/volunteer-page-root.component.ts @@ -16,23 +16,22 @@ * @fileoverview Root Component for volunteer page. */ -import { Component } from '@angular/core'; +import {Component} from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { PageHeadService } from 'services/page-head.service'; +import {AppConstants} from 'app.constants'; +import {PageHeadService} from 'services/page-head.service'; @Component({ selector: 'oppia-volunteer-page-root', - templateUrl: './volunteer-page-root.component.html' + templateUrl: './volunteer-page-root.component.html', }) export class VolunteerPageRootComponent { - constructor( - private pageHeadService: PageHeadService - ) {} + constructor(private pageHeadService: PageHeadService) {} ngOnInit(): void { this.pageHeadService.updateTitleAndMetaTags( AppConstants.PAGES_REGISTERED_WITH_FRONTEND.VOLUNTEER.TITLE, - AppConstants.PAGES_REGISTERED_WITH_FRONTEND.VOLUNTEER.META); + AppConstants.PAGES_REGISTERED_WITH_FRONTEND.VOLUNTEER.META + ); } } diff --git a/core/templates/pages/volunteer-page/volunteer-page-routing.module.ts b/core/templates/pages/volunteer-page/volunteer-page-routing.module.ts index 5f7de489fbfb..c26c4882f77e 100644 --- a/core/templates/pages/volunteer-page/volunteer-page-routing.module.ts +++ b/core/templates/pages/volunteer-page/volunteer-page-routing.module.ts @@ -16,24 +16,19 @@ * @fileoverview Routing module for volunteer page. */ -import { NgModule } from '@angular/core'; -import { Route, RouterModule } from '@angular/router'; -import { VolunteerPageRootComponent } from './volunteer-page-root.component'; +import {NgModule} from '@angular/core'; +import {Route, RouterModule} from '@angular/router'; +import {VolunteerPageRootComponent} from './volunteer-page-root.component'; const routes: Route[] = [ { path: '', - component: VolunteerPageRootComponent - } + component: VolunteerPageRootComponent, + }, ]; @NgModule({ - imports: [ - RouterModule.forChild(routes) - ], - exports: [ - RouterModule - ] + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], }) - export class VolunteerPageRoutingModule {} diff --git a/core/templates/pages/volunteer-page/volunteer-page.component.spec.ts b/core/templates/pages/volunteer-page/volunteer-page.component.spec.ts index 2a547c0a37b2..16ee58d9726b 100644 --- a/core/templates/pages/volunteer-page/volunteer-page.component.spec.ts +++ b/core/templates/pages/volunteer-page/volunteer-page.component.spec.ts @@ -16,16 +16,15 @@ * @fileoverview Unit tests for volunteer page. */ -import { NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; -import { TranslateService } from '@ngx-translate/core'; -import { NgbCarouselConfig } from '@ng-bootstrap/ng-bootstrap'; +import {NO_ERRORS_SCHEMA, EventEmitter} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; +import {TranslateService} from '@ngx-translate/core'; +import {NgbCarouselConfig} from '@ng-bootstrap/ng-bootstrap'; -import { VolunteerPageComponent } from './volunteer-page.component'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; -import { PageTitleService } from 'services/page-title.service'; -import { MockTranslatePipe } from 'tests/unit-test-utils'; +import {VolunteerPageComponent} from './volunteer-page.component'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {PageTitleService} from 'services/page-title.service'; +import {MockTranslatePipe} from 'tests/unit-test-utils'; class MockTranslateService { onLangChange: EventEmitter = new EventEmitter(); @@ -37,22 +36,19 @@ class MockTranslateService { describe('Volunteer page', () => { let translateService: TranslateService; let pageTitleService: PageTitleService; - beforeEach(async() => { + beforeEach(async () => { TestBed.configureTestingModule({ - declarations: [ - VolunteerPageComponent, - MockTranslatePipe - ], + declarations: [VolunteerPageComponent, MockTranslatePipe], providers: [ UrlInterpolationService, NgbCarouselConfig, { provide: TranslateService, - useClass: MockTranslateService + useClass: MockTranslateService, }, - PageTitleService + PageTitleService, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); @@ -60,23 +56,22 @@ describe('Volunteer page', () => { beforeEach(() => { const volunteerPageComponent = TestBed.createComponent( - VolunteerPageComponent); + VolunteerPageComponent + ); component = volunteerPageComponent.componentInstance; translateService = TestBed.inject(TranslateService); pageTitleService = TestBed.inject(PageTitleService); }); - it('should successfully instantiate the component from beforeEach block', - () => { - expect(component).toBeDefined(); - }); + it('should successfully instantiate the component from beforeEach block', () => { + expect(component).toBeDefined(); + }); it('should set component properties when ngOnInit() is called', () => { spyOn(translateService.onLangChange, 'subscribe'); component.ngOnInit(); - expect(component.bannerImgPath).toBe( - '/volunteer/banner.webp'); + expect(component.bannerImgPath).toBe('/volunteer/banner.webp'); expect(translateService.onLangChange.subscribe).toHaveBeenCalled(); }); @@ -84,14 +79,17 @@ describe('Volunteer page', () => { expect(component.getStaticImageUrl('/test')).toEqual('/assets/images/test'); }); - it('should obtain translated page title whenever the selected' + - 'language changes', () => { - component.ngOnInit(); - spyOn(component, 'setPageTitle'); - translateService.onLangChange.emit(); + it( + 'should obtain translated page title whenever the selected' + + 'language changes', + () => { + component.ngOnInit(); + spyOn(component, 'setPageTitle'); + translateService.onLangChange.emit(); - expect(component.setPageTitle).toHaveBeenCalled(); - }); + expect(component.setPageTitle).toHaveBeenCalled(); + } + ); it('should set new page title', () => { spyOn(translateService, 'instant').and.callThrough(); @@ -99,9 +97,11 @@ describe('Volunteer page', () => { component.setPageTitle(); expect(translateService.instant).toHaveBeenCalledWith( - 'I18N_VOLUNTEER_PAGE_TITLE'); + 'I18N_VOLUNTEER_PAGE_TITLE' + ); expect(pageTitleService.setDocumentTitle).toHaveBeenCalledWith( - 'I18N_VOLUNTEER_PAGE_TITLE'); + 'I18N_VOLUNTEER_PAGE_TITLE' + ); }); it('should get webp extended file name', () => { diff --git a/core/templates/pages/volunteer-page/volunteer-page.component.ts b/core/templates/pages/volunteer-page/volunteer-page.component.ts index 83baf0f557fd..d070b66ebd1c 100644 --- a/core/templates/pages/volunteer-page/volunteer-page.component.ts +++ b/core/templates/pages/volunteer-page/volunteer-page.component.ts @@ -16,23 +16,21 @@ * @fileoverview Component for the volunteer page. */ -import { Component, OnInit, OnDestroy } from '@angular/core'; -import { ViewEncapsulation } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; -import { NgbCarouselConfig } from '@ng-bootstrap/ng-bootstrap'; -import { Subscription } from 'rxjs'; - -import { PageTitleService } from 'services/page-title.service'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; +import {Component, OnInit, OnDestroy} from '@angular/core'; +import {ViewEncapsulation} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; +import {NgbCarouselConfig} from '@ng-bootstrap/ng-bootstrap'; +import {Subscription} from 'rxjs'; +import {PageTitleService} from 'services/page-title.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; @Component({ selector: 'volunteer-page', templateUrl: './volunteer-page.component.html', styleUrls: [], encapsulation: ViewEncapsulation.None, - providers: [NgbCarouselConfig] + providers: [NgbCarouselConfig], }) export class VolunteerPageComponent implements OnInit, OnDestroy { directiveSubscriptions = new Subscription(); @@ -59,17 +57,17 @@ export class VolunteerPageComponent implements OnInit, OnDestroy { growth!: { images: string[]; - caption: { content: string; name: string; type: string}[]; + caption: {content: string; name: string; type: string}[]; }; lessonCreation!: { images: string[]; - caption: { content: string; name: string; type: string}[]; + caption: {content: string; name: string; type: string}[]; }; translation!: { images: string[]; - caption: { content: string; name: string; type: string}[]; + caption: {content: string; name: string; type: string}[]; }; constructor( @@ -89,7 +87,8 @@ export class VolunteerPageComponent implements OnInit, OnDestroy { setPageTitle(): void { let translatedTitle = this.translateService.instant( - 'I18N_VOLUNTEER_PAGE_TITLE'); + 'I18N_VOLUNTEER_PAGE_TITLE' + ); this.pageTitleService.setDocumentTitle(translatedTitle); } @@ -108,44 +107,43 @@ export class VolunteerPageComponent implements OnInit, OnDestroy { '/volunteer/profile_images/mark.jpg', '/volunteer/profile_images/liwei.jpg', '/volunteer/profile_images/pearl.jpg', - '/volunteer/profile_images/molly.jpg' + '/volunteer/profile_images/molly.jpg', ], caption: [ { - content: ( + content: 'I contribute to Oppia because of its remarkable goal:' + ' to make a quality education available to those who may not' + - ' have easy access to it.'), + ' have easy access to it.', name: 'Mark Halpin', - type: 'Artist' + type: 'Artist', }, { - content: ( + content: 'I joined Oppia driven by a deep passion for education. ' + 'With over five years of experience in edtech, I am committed to ' + 'leveraging technology to enhance and enrich the learning ' + - 'experience, making education accessible and engaging for all.' - ), + 'experience, making education accessible and engaging for all.', name: 'Liwei Zhang', - type: 'Design team' + type: 'Design team', }, { - content: ( + content: 'Oppia is able to give education to those who need it, ' + 'nd knowing that the art we create helps learners ' + - 'means the world to me.'), + 'means the world to me.', name: 'Pearl Nunag', - type: 'Graphics team' + type: 'Graphics team', }, { - content: ( - 'At Oppia, not only do I work with great people, but I\'m able ' + + content: + "At Oppia, not only do I work with great people, but I'm able " + 'to contribute to their mission of bringing free, ' + - 'quality education to everyone.'), + 'quality education to everyone.', name: 'Molly Rhodes', - type: 'Graphics team' - } - ] + type: 'Graphics team', + }, + ], }; this.development = { @@ -156,31 +154,31 @@ export class VolunteerPageComponent implements OnInit, OnDestroy { ], caption: [ { - content: ( + content: 'As I got involved with Oppia, I became more and more ' + 'invested in the ideals of it, which is to provide an ' + 'easy to use learning platform in which anyone can share ' + - 'their knowledge about a subject to the world.'), + 'their knowledge about a subject to the world.', name: 'Akshay Anand', - type: 'Full-Stack Developer' + type: 'Full-Stack Developer', }, { - content: ( + content: 'Making quality education accessible and fun to ' + 'experience is something that is important to me. ' + - 'I enjoy contributing to Oppia because it does exactly this.'), + 'I enjoy contributing to Oppia because it does exactly this.', name: 'Kevin Thomas', - type: 'Full-Stack Developer' + type: 'Full-Stack Developer', }, { - content: ( + content: 'I believe education is the route to social upliftment ' + 'and progress. At Oppia, I get to be a part of this' + - 'movement to provide free and accessible education for all.'), + 'movement to provide free and accessible education for all.', name: 'Jay Vivarekar', - type: 'Web Developer' - } - ] + type: 'Web Developer', + }, + ], }; this.growth = { @@ -188,46 +186,46 @@ export class VolunteerPageComponent implements OnInit, OnDestroy { '/volunteer/profile_images/yiga.jpg', '/volunteer/profile_images/jennifer.jpg', '/volunteer/profile_images/erio.jpg', - '/volunteer/profile_images/diana.jpg' + '/volunteer/profile_images/diana.jpg', ], caption: [ { - content: ( + content: 'I contribute to Oppia because I believe in its cause. ' + 'Every child can do well at math if they’re taught in the right ' + 'way and it is broken down in a language they understand. Also, ' + 'no matter how good a product is, people will not know it ' + - 'without effective marketing.'), + 'without effective marketing.', name: 'Yiga Ikpae', - type: 'Marketing team' + type: 'Marketing team', }, { - content: ( - 'Oppia\'s mission and values inspire me daily. I volunteer ' + + content: + "Oppia's mission and values inspire me daily. I volunteer " + 'because everyone deserves an education and helping to market ' + - 'this opportunity is a privilege'), + 'this opportunity is a privilege', name: 'Jennifer Nunez', - type: 'Marketing team' + type: 'Marketing team', }, { - content: ( + content: 'I love to tell stories through content creation. This not only' + - 'amplifies Oppia\'s mission of providing free accessible ' + + "amplifies Oppia's mission of providing free accessible " + 'education but also fosters a sense of community and inspiration,' + - 'encouraging more individuals to engage with Oppia\'s ' + - 'educational resources.'), + "encouraging more individuals to engage with Oppia's " + + 'educational resources.', name: 'Erio Crucecia', - type: 'Video creation team' + type: 'Video creation team', }, { - content: ( + content: 'I contribute to Oppia because seeing the community ' + 'and the impact it creates makes me hopeful for the future of ' + - 'education, and I want to be a part of that change.'), + 'education, and I want to be a part of that change.', name: 'Diana Chen', - type: 'Product Manager' - } - ] + type: 'Product Manager', + }, + ], }; this.lessonCreation = { @@ -235,44 +233,44 @@ export class VolunteerPageComponent implements OnInit, OnDestroy { '/volunteer/profile_images/aanuoluwapo.jpg', '/volunteer/profile_images/viksar.jpg', '/volunteer/profile_images/jackson.jpg', - '/volunteer/profile_images/abha.jpg' + '/volunteer/profile_images/abha.jpg', ], caption: [ { - content: ( + content: 'At Oppia, my love for writing stories as a ' + 'creative intertwines with my passion for ' + - 'education.'), + 'education.', name: 'Aanuoluwapo Adeoti', - type: 'Lessons team' + type: 'Lessons team', }, { - content: ( + content: 'Contributing to Oppia has been incredibly rewarding. ' + - 'I\'ve always loved helping others, so being able to use ' + + "I've always loved helping others, so being able to use " + 'such a great platform to provide quality education to ' + - 'those in need is truly fulfilling.'), + 'those in need is truly fulfilling.', name: 'Viksar Dubey', - type: 'Practice questions team' + type: 'Practice questions team', }, { - content: ( + content: 'I feel so grateful to help children gain access to ' + 'educational resources. I feel proud when I contribute, ' + - 'and excited when my team makes progress.'), + 'and excited when my team makes progress.', name: 'Christopher Jackson Felton', - type: 'Lessons team' + type: 'Lessons team', }, { - content: ( + content: 'Oppia has provided a platform, through which I can share my ' + 'love of Maths with dedicated learners, around the world. I ' + 'contribute to Oppia because it helps students learn important ' + - 'concepts in a fun and interactive way.'), + 'concepts in a fun and interactive way.', name: 'Abha Barge', - type: 'Lessons team' - } - ] + type: 'Lessons team', + }, + ], }; this.translation = { @@ -285,53 +283,53 @@ export class VolunteerPageComponent implements OnInit, OnDestroy { ], caption: [ { - content: ( + content: 'I was always told that quality education is only for those who ' + 'are privileged. But by volunteering at Oppia, I prove every day ' + 'that a decent education is for everyone and I feel grateful to ' + - 'be able to help thousands of people worldwide on this journey.'), + 'be able to help thousands of people worldwide on this journey.', name: 'Giovana Alonso', - type: 'Brazilian Portuguese Translator' + type: 'Brazilian Portuguese Translator', }, { - content: ( + content: 'Being a part of this magical world of online learning and ' + 'volunteering at Oppia makes me believe that when you give the ' + 'gift of education, you are giving happiness and smiles on ' + 'faces, and that is the biggest gift…and YES, as a volunteer I ' + - 'am playing my part and helping kids all over the world.'), + 'am playing my part and helping kids all over the world.', name: 'Kanupriya Gupta', - type: 'Hindi Translator' + type: 'Hindi Translator', }, { - content: ( + content: 'Every small voluntary effort, when done with love and ' + - 'dedication, can achieve incredible results, and that\'s what ' + - 'I\'m doing at Oppia, hoping to achieve more results and help ' + + "dedication, can achieve incredible results, and that's what " + + "I'm doing at Oppia, hoping to achieve more results and help " + 'accomplish what many say is impossible. Alone we are nothing, ' + - 'but together we change the world.'), + 'but together we change the world.', name: 'Vanessa Gelinski', - type: 'Brazilian Portuguese Translator' + type: 'Brazilian Portuguese Translator', }, { - content: ( + content: 'Empowering learners with knowledge is not just a task; for me, ' + - 'it\'s a passion. Contributing to Oppia affords to extend this ' + + "it's a passion. Contributing to Oppia affords to extend this " + 'impact to the lives of many more children, and I am' + - 'truly grateful for the privilege.'), + 'truly grateful for the privilege.', name: 'Pretty Agu', - type: 'Translations coordinator' + type: 'Translations coordinator', }, { - content: ( + content: 'Volunteering with Oppia gives me immense ' + 'satisfaction because it aligns with a tenet ' + 'I firmly believe in: everyone should have access to basic ' + - 'education. I\'m glad to be a part of the organization!'), + "education. I'm glad to be a part of the organization!", name: 'Anubhuti Varshney', - type: 'Translations/Voiceovers Coordinator' - } - ] + type: 'Translations/Voiceovers Coordinator', + }, + ], }; this.ngbCarouselConfig.interval = 10000; diff --git a/core/templates/pages/volunteer-page/volunteer-page.module.ts b/core/templates/pages/volunteer-page/volunteer-page.module.ts index 1a76e019a1ed..008fd077b7ca 100644 --- a/core/templates/pages/volunteer-page/volunteer-page.module.ts +++ b/core/templates/pages/volunteer-page/volunteer-page.module.ts @@ -16,29 +16,22 @@ * @fileoverview Module for the volunteer page. */ -import { NgModule } from '@angular/core'; -import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { VolunteerPageComponent } from './volunteer-page.component'; -import { SharedComponentsModule } from 'components/shared-component.module'; -import { VolunteerPageRootComponent } from - './volunteer-page-root.component'; -import { CommonModule } from '@angular/common'; -import { VolunteerPageRoutingModule } from './volunteer-page-routing.module'; +import {NgModule} from '@angular/core'; +import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; +import {VolunteerPageComponent} from './volunteer-page.component'; +import {SharedComponentsModule} from 'components/shared-component.module'; +import {VolunteerPageRootComponent} from './volunteer-page-root.component'; +import {CommonModule} from '@angular/common'; +import {VolunteerPageRoutingModule} from './volunteer-page-routing.module'; @NgModule({ imports: [ CommonModule, SharedComponentsModule, VolunteerPageRoutingModule, - NgbModule + NgbModule, ], - declarations: [ - VolunteerPageComponent, - VolunteerPageRootComponent - ], - entryComponents: [ - VolunteerPageComponent, - VolunteerPageRootComponent - ] + declarations: [VolunteerPageComponent, VolunteerPageRootComponent], + entryComponents: [VolunteerPageComponent, VolunteerPageRootComponent], }) export class VolunteerPageModule {} diff --git a/core/templates/services/UpgradedServices.ts b/core/templates/services/UpgradedServices.ts index ab335247bc87..c02cbafaf1b2 100644 --- a/core/templates/services/UpgradedServices.ts +++ b/core/templates/services/UpgradedServices.ts @@ -16,465 +16,364 @@ * @fileoverview Service for storing all upgraded services */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; -import { Meta, Title } from '@angular/platform-browser'; -// eslint-disable-next-line oppia/disallow-httpclient -import { HttpClient, HttpXhrBackend, +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {Meta, Title} from '@angular/platform-browser'; +import { + // eslint-disable-next-line oppia/disallow-httpclient + HttpClient, + HttpXhrBackend, // eslint-disable-next-line camelcase, oppia/disallow-flags - ɵangular_packages_common_http_http_d + ɵangular_packages_common_http_http_d, } from '@angular/common/http'; - -import { AdminBackendApiService } from - 'domain/admin/admin-backend-api.service'; -import { AdminDataService } from - 'pages/admin-page/services/admin-data.service'; -import { AdminRouterService } from - 'pages/admin-page/services/admin-router.service'; -import { AdminTaskManagerService } from - 'pages/admin-page/services/admin-task-manager.service'; -import { AlertsService } from 'services/alerts.service'; -import { AlgebraicExpressionInputRulesService } from +import {AdminBackendApiService} from 'domain/admin/admin-backend-api.service'; +import {AdminDataService} from 'pages/admin-page/services/admin-data.service'; +import {AdminRouterService} from 'pages/admin-page/services/admin-router.service'; +import {AdminTaskManagerService} from 'pages/admin-page/services/admin-task-manager.service'; +import {AlertsService} from 'services/alerts.service'; +import { + AlgebraicExpressionInputRulesService, // eslint-disable-next-line max-len - 'interactions/AlgebraicExpressionInput/directives/algebraic-expression-input-rules.service'; -import { AlgebraicExpressionInputValidationService } from +} from 'interactions/AlgebraicExpressionInput/directives/algebraic-expression-input-rules.service'; +import { + AlgebraicExpressionInputValidationService, // eslint-disable-next-line max-len - 'interactions/AlgebraicExpressionInput/directives/algebraic-expression-input-validation.service'; -import { AngularNameService } from - 'pages/exploration-editor-page/services/angular-name.service'; -import { AnswerClassificationService } from - 'pages/exploration-player-page/services/answer-classification.service'; -import { AnswerGroupObjectFactory } from - 'domain/exploration/AnswerGroupObjectFactory'; -import { AppService } from 'services/app.service'; -import { AssetsBackendApiService } from - 'services/assets-backend-api.service'; -import { AudioBarStatusService } from 'services/audio-bar-status.service'; -import { AudioPreloaderService } from - 'pages/exploration-player-page/services/audio-preloader.service'; -import { AudioTranslationLanguageService } from - 'pages/exploration-player-page/services/audio-translation-language.service'; -import { AudioTranslationManagerService } from - 'pages/exploration-player-page/services/audio-translation-manager.service'; -import { AutogeneratedAudioPlayerService } from - 'services/autogenerated-audio-player.service'; -import { AutoplayedVideosService } from 'services/autoplayed-videos.service'; -import { BackgroundMaskService } from - 'services/stateful/background-mask.service'; -import { baseInteractionValidationService } from - 'interactions/base-interaction-validation.service'; -import { BottomNavbarStatusService } from - 'services/bottom-navbar-status.service'; -import { PreventPageUnloadEventService } from - 'services/prevent-page-unload-event.service'; -import { BrowserCheckerService } from - 'domain/utilities/browser-checker.service'; -import { CamelCaseToHyphensPipe } from - 'filters/string-utility-filters/camel-case-to-hyphens.pipe'; -import { CkEditorCopyContentService } from - 'components/ck-editor-helpers/ck-editor-copy-content.service'; -import { ClassifierDataBackendApiService } from - 'services/classifier-data-backend-api.service'; -import { ClassroomBackendApiService } from - 'domain/classroom/classroom-backend-api.service'; -import { CodeNormalizerService } from 'services/code-normalizer.service'; -import { CodeReplRulesService } from - 'interactions/CodeRepl/directives/code-repl-rules.service'; -import { CodeReplValidationService } from - 'interactions/CodeRepl/directives/code-repl-validation.service'; -import { CollectionCreationBackendService } from - 'components/entity-creation-services/collection-creation-backend-api.service'; -import { CollectionCreationService } from - 'components/entity-creation-services/collection-creation.service'; -import { CollectionRightsBackendApiService } from - 'domain/collection/collection-rights-backend-api.service'; -import { CollectionValidationService } from - 'domain/collection/collection-validation.service'; -import { ComputeGraphService } from 'services/compute-graph.service'; -import { ConceptCardBackendApiService } from - 'domain/skill/concept-card-backend-api.service'; -import { ContextService } from 'services/context.service'; -import { ContinueRulesService } from - 'interactions/Continue/directives/continue-rules.service'; -import { ContinueValidationService } from - 'interactions/Continue/directives/continue-validation.service'; -import { ContributionOpportunitiesBackendApiService } from +} from 'interactions/AlgebraicExpressionInput/directives/algebraic-expression-input-validation.service'; +import {AngularNameService} from 'pages/exploration-editor-page/services/angular-name.service'; +import {AnswerClassificationService} from 'pages/exploration-player-page/services/answer-classification.service'; +import {AnswerGroupObjectFactory} from 'domain/exploration/AnswerGroupObjectFactory'; +import {AppService} from 'services/app.service'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; +import {AudioBarStatusService} from 'services/audio-bar-status.service'; +import {AudioPreloaderService} from 'pages/exploration-player-page/services/audio-preloader.service'; +import {AudioTranslationLanguageService} from 'pages/exploration-player-page/services/audio-translation-language.service'; +import {AudioTranslationManagerService} from 'pages/exploration-player-page/services/audio-translation-manager.service'; +import {AutogeneratedAudioPlayerService} from 'services/autogenerated-audio-player.service'; +import {AutoplayedVideosService} from 'services/autoplayed-videos.service'; +import {BackgroundMaskService} from 'services/stateful/background-mask.service'; +import {baseInteractionValidationService} from 'interactions/base-interaction-validation.service'; +import {BottomNavbarStatusService} from 'services/bottom-navbar-status.service'; +import {PreventPageUnloadEventService} from 'services/prevent-page-unload-event.service'; +import {BrowserCheckerService} from 'domain/utilities/browser-checker.service'; +import {CamelCaseToHyphensPipe} from 'filters/string-utility-filters/camel-case-to-hyphens.pipe'; +import {CkEditorCopyContentService} from 'components/ck-editor-helpers/ck-editor-copy-content.service'; +import {ClassifierDataBackendApiService} from 'services/classifier-data-backend-api.service'; +import {ClassroomBackendApiService} from 'domain/classroom/classroom-backend-api.service'; +import {CodeNormalizerService} from 'services/code-normalizer.service'; +import {CodeReplRulesService} from 'interactions/CodeRepl/directives/code-repl-rules.service'; +import {CodeReplValidationService} from 'interactions/CodeRepl/directives/code-repl-validation.service'; +import {CollectionCreationBackendService} from 'components/entity-creation-services/collection-creation-backend-api.service'; +import {CollectionCreationService} from 'components/entity-creation-services/collection-creation.service'; +import {CollectionRightsBackendApiService} from 'domain/collection/collection-rights-backend-api.service'; +import {CollectionValidationService} from 'domain/collection/collection-validation.service'; +import {ComputeGraphService} from 'services/compute-graph.service'; +import {ConceptCardBackendApiService} from 'domain/skill/concept-card-backend-api.service'; +import {ContextService} from 'services/context.service'; +import {ContinueRulesService} from 'interactions/Continue/directives/continue-rules.service'; +import {ContinueValidationService} from 'interactions/Continue/directives/continue-validation.service'; +import { + ContributionOpportunitiesBackendApiService, // eslint-disable-next-line max-len - 'pages/contributor-dashboard-page/services/contribution-opportunities-backend-api.service'; -import { ConstructTranslationIdsService } from - 'services/construct-translation-ids.service'; -import { CountVectorizerService } from 'classifiers/count-vectorizer.service'; -import { CreatorDashboardBackendApiService } from - 'domain/creator_dashboard/creator-dashboard-backend-api.service'; -import { CsrfTokenService } from 'services/csrf-token.service'; -import { CurrentInteractionService } from - 'pages/exploration-player-page/services/current-interaction.service'; -import { DateTimeFormatService } from 'services/date-time-format.service'; -import { DeviceInfoService } from 'services/contextual/device-info.service'; -import { DocumentAttributeCustomizationService } from - 'services/contextual/document-attribute-customization.service'; -import { DragAndDropSortInputRulesService } from +} from 'pages/contributor-dashboard-page/services/contribution-opportunities-backend-api.service'; +import {ConstructTranslationIdsService} from 'services/construct-translation-ids.service'; +import {CountVectorizerService} from 'classifiers/count-vectorizer.service'; +import {CreatorDashboardBackendApiService} from 'domain/creator_dashboard/creator-dashboard-backend-api.service'; +import {CsrfTokenService} from 'services/csrf-token.service'; +import {CurrentInteractionService} from 'pages/exploration-player-page/services/current-interaction.service'; +import {DateTimeFormatService} from 'services/date-time-format.service'; +import {DeviceInfoService} from 'services/contextual/device-info.service'; +import {DocumentAttributeCustomizationService} from 'services/contextual/document-attribute-customization.service'; +import { + DragAndDropSortInputRulesService, // eslint-disable-next-line max-len - 'interactions/DragAndDropSortInput/directives/drag-and-drop-sort-input-rules.service'; -import { DragAndDropSortInputValidationService } from +} from 'interactions/DragAndDropSortInput/directives/drag-and-drop-sort-input-rules.service'; +import { + DragAndDropSortInputValidationService, // eslint-disable-next-line max-len - 'interactions/DragAndDropSortInput/directives/drag-and-drop-sort-input-validation.service'; -import { EditableCollectionBackendApiService } from - 'domain/collection/editable-collection-backend-api.service'; -import { EditabilityService } from 'services/editability.service'; -import { EditorFirstTimeEventsService } from - 'pages/exploration-editor-page/services/editor-first-time-events.service'; -import { EmailDashboardBackendApiService } from - 'domain/email-dashboard/email-dashboard-backend-api.service'; -import { EndExplorationRulesService } from - 'interactions/EndExploration/directives/end-exploration-rules.service'; -import { EndExplorationValidationService } from - 'interactions/EndExploration/directives/end-exploration-validation.service'; -import { ExplorationDiffService } from - 'pages/exploration-editor-page/services/exploration-diff.service'; -import { ExplorationFeaturesBackendApiService } from - 'services/exploration-features-backend-api.service'; -import { ExplorationFeaturesService } from - 'services/exploration-features.service'; -import { ExplorationHtmlFormatterService } from - 'services/exploration-html-formatter.service'; -import { ExplorationImprovementsBackendApiService } from - 'services/exploration-improvements-backend-api.service'; -import { ExplorationImprovementsTaskRegistryService } from - 'services/exploration-improvements-task-registry.service'; -import { ExplorationObjectFactory } from - 'domain/exploration/ExplorationObjectFactory'; -import { ExplorationPermissionsBackendApiService } from - 'domain/exploration/exploration-permissions-backend-api.service'; -import { ExplorationRecommendationsBackendApiService } from - 'domain/recommendations/exploration-recommendations-backend-api.service'; -import { ExplorationStatsBackendApiService } from - 'services/exploration-stats-backend-api.service'; -import { ExplorationStatsService } from 'services/exploration-stats.service'; -import { ExpressionEvaluatorService } from - 'expressions/expression-evaluator.service'; -import { ExpressionParserService } from 'expressions/expression-parser.service'; -import { ExplorationRecommendationsService } from +} from 'interactions/DragAndDropSortInput/directives/drag-and-drop-sort-input-validation.service'; +import {EditableCollectionBackendApiService} from 'domain/collection/editable-collection-backend-api.service'; +import {EditabilityService} from 'services/editability.service'; +import {EditorFirstTimeEventsService} from 'pages/exploration-editor-page/services/editor-first-time-events.service'; +import {EmailDashboardBackendApiService} from 'domain/email-dashboard/email-dashboard-backend-api.service'; +import {EndExplorationRulesService} from 'interactions/EndExploration/directives/end-exploration-rules.service'; +import {EndExplorationValidationService} from 'interactions/EndExploration/directives/end-exploration-validation.service'; +import {ExplorationDiffService} from 'pages/exploration-editor-page/services/exploration-diff.service'; +import {ExplorationFeaturesBackendApiService} from 'services/exploration-features-backend-api.service'; +import {ExplorationFeaturesService} from 'services/exploration-features.service'; +import {ExplorationHtmlFormatterService} from 'services/exploration-html-formatter.service'; +import {ExplorationImprovementsBackendApiService} from 'services/exploration-improvements-backend-api.service'; +import {ExplorationImprovementsTaskRegistryService} from 'services/exploration-improvements-task-registry.service'; +import {ExplorationObjectFactory} from 'domain/exploration/ExplorationObjectFactory'; +import {ExplorationPermissionsBackendApiService} from 'domain/exploration/exploration-permissions-backend-api.service'; +import {ExplorationRecommendationsBackendApiService} from 'domain/recommendations/exploration-recommendations-backend-api.service'; +import {ExplorationStatsBackendApiService} from 'services/exploration-stats-backend-api.service'; +import {ExplorationStatsService} from 'services/exploration-stats.service'; +import {ExpressionEvaluatorService} from 'expressions/expression-evaluator.service'; +import {ExpressionParserService} from 'expressions/expression-parser.service'; +import { + ExplorationRecommendationsService, // eslint-disable-next-line max-len - 'pages/exploration-player-page/services/exploration-recommendations.service'; -import { ExpressionSyntaxTreeService } from - 'expressions/expression-syntax-tree.service'; -import { ExtensionTagAssemblerService } from - 'services/extension-tag-assembler.service'; -import { ExternalSaveService } from 'services/external-save.service'; -import { FeedbackThreadObjectFactory } from - 'domain/feedback_thread/FeedbackThreadObjectFactory'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { FractionInputRulesService } from - 'interactions/FractionInput/directives/fraction-input-rules.service'; -import { FractionInputValidationService } from - 'interactions/FractionInput/directives/fraction-input-validation.service'; -import { GraphDetailService } from - 'interactions/GraphInput/directives/graph-detail.service'; -import { GraphInputRulesService } from - 'interactions/GraphInput/directives/graph-input-rules.service'; -import { GraphInputValidationService } from - 'interactions/GraphInput/directives/graph-input-validation.service'; -import { GraphUtilsService } from - 'interactions/GraphInput/directives/graph-utils.service'; -import { GuestCollectionProgressService } from - 'domain/collection/guest-collection-progress.service'; -import { HtmlEscaperService } from 'services/html-escaper.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { IdGenerationService } from 'services/id-generation.service'; -import { ImageClickInputRulesService } from - 'interactions/ImageClickInput/directives/image-click-input-rules.service'; -import { ImageClickInputValidationService } from +} from 'pages/exploration-player-page/services/exploration-recommendations.service'; +import {ExpressionSyntaxTreeService} from 'expressions/expression-syntax-tree.service'; +import {ExtensionTagAssemblerService} from 'services/extension-tag-assembler.service'; +import {ExternalSaveService} from 'services/external-save.service'; +import {FeedbackThreadObjectFactory} from 'domain/feedback_thread/FeedbackThreadObjectFactory'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {FractionInputRulesService} from 'interactions/FractionInput/directives/fraction-input-rules.service'; +import {FractionInputValidationService} from 'interactions/FractionInput/directives/fraction-input-validation.service'; +import {GraphDetailService} from 'interactions/GraphInput/directives/graph-detail.service'; +import {GraphInputRulesService} from 'interactions/GraphInput/directives/graph-input-rules.service'; +import {GraphInputValidationService} from 'interactions/GraphInput/directives/graph-input-validation.service'; +import {GraphUtilsService} from 'interactions/GraphInput/directives/graph-utils.service'; +import {GuestCollectionProgressService} from 'domain/collection/guest-collection-progress.service'; +import {HtmlEscaperService} from 'services/html-escaper.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {IdGenerationService} from 'services/id-generation.service'; +import {ImageClickInputRulesService} from 'interactions/ImageClickInput/directives/image-click-input-rules.service'; +import { + ImageClickInputValidationService, // eslint-disable-next-line max-len - 'interactions/ImageClickInput/directives/image-click-input-validation.service'; -import { ImprovementsService } from 'services/improvements.service'; -import { InteractionAttributesExtractorService } from - 'interactions/interaction-attributes-extractor.service'; -import { InteractionDetailsCacheService } from +} from 'interactions/ImageClickInput/directives/image-click-input-validation.service'; +import {ImprovementsService} from 'services/improvements.service'; +import {InteractionAttributesExtractorService} from 'interactions/interaction-attributes-extractor.service'; +import { + InteractionDetailsCacheService, // eslint-disable-next-line max-len - 'pages/exploration-editor-page/editor-tab/services/interaction-details-cache.service'; -import { InteractionObjectFactory } from - 'domain/exploration/InteractionObjectFactory'; -import { InteractionRulesRegistryService } from - 'services/interaction-rules-registry.service'; -import { InteractionSpecsService } from 'services/interaction-specs.service'; -import { InteractiveMapRulesService } from - 'interactions/InteractiveMap/directives/interactive-map-rules.service'; -import { InteractiveMapValidationService } from - 'interactions/InteractiveMap/directives/interactive-map-validation.service'; -import { ItemSelectionInputRulesService } from +} from 'pages/exploration-editor-page/editor-tab/services/interaction-details-cache.service'; +import {InteractionObjectFactory} from 'domain/exploration/InteractionObjectFactory'; +import {InteractionRulesRegistryService} from 'services/interaction-rules-registry.service'; +import {InteractionSpecsService} from 'services/interaction-specs.service'; +import {InteractiveMapRulesService} from 'interactions/InteractiveMap/directives/interactive-map-rules.service'; +import {InteractiveMapValidationService} from 'interactions/InteractiveMap/directives/interactive-map-validation.service'; +import { + ItemSelectionInputRulesService, // eslint-disable-next-line max-len - 'interactions/ItemSelectionInput/directives/item-selection-input-rules.service'; -import { ItemSelectionInputValidationService } from +} from 'interactions/ItemSelectionInput/directives/item-selection-input-rules.service'; +import { + ItemSelectionInputValidationService, // eslint-disable-next-line max-len - 'interactions/ItemSelectionInput/directives/item-selection-input-validation.service'; -import { LanguageUtilService } from 'domain/utilities/language-util.service'; -import { LearnerAnswerDetailsBackendApiService } from - 'domain/statistics/learner-answer-details-backend-api.service'; -import { LearnerDashboardBackendApiService } from - 'domain/learner_dashboard/learner-dashboard-backend-api.service'; -import { LearnerDashboardIdsBackendApiService } from - 'domain/learner_dashboard/learner-dashboard-ids-backend-api.service'; -import { LearnerParamsService } from - 'pages/exploration-player-page/services/learner-params.service'; -import { LocalStorageService } from 'services/local-storage.service'; -import { LoaderService } from 'services/loader.service'; -import { LoggerService } from 'services/contextual/logger.service'; -import { LostChangeObjectFactory } from - 'domain/exploration/LostChangeObjectFactory'; -import { MathEquationInputRulesService } from +} from 'interactions/ItemSelectionInput/directives/item-selection-input-validation.service'; +import {LanguageUtilService} from 'domain/utilities/language-util.service'; +import {LearnerAnswerDetailsBackendApiService} from 'domain/statistics/learner-answer-details-backend-api.service'; +import {LearnerDashboardBackendApiService} from 'domain/learner_dashboard/learner-dashboard-backend-api.service'; +import {LearnerDashboardIdsBackendApiService} from 'domain/learner_dashboard/learner-dashboard-ids-backend-api.service'; +import {LearnerParamsService} from 'pages/exploration-player-page/services/learner-params.service'; +import {LocalStorageService} from 'services/local-storage.service'; +import {LoaderService} from 'services/loader.service'; +import {LoggerService} from 'services/contextual/logger.service'; +import {LostChangeObjectFactory} from 'domain/exploration/LostChangeObjectFactory'; +import { + MathEquationInputRulesService, // eslint-disable-next-line max-len - 'interactions/MathEquationInput/directives/math-equation-input-rules.service'; -import { MathEquationInputValidationService } from +} from 'interactions/MathEquationInput/directives/math-equation-input-rules.service'; +import { + MathEquationInputValidationService, // eslint-disable-next-line max-len - 'interactions/MathEquationInput/directives/math-equation-input-validation.service'; -import { MessengerService } from 'services/messenger.service'; -import { MetaTagCustomizationService } from - 'services/contextual/meta-tag-customization.service'; -import { MisconceptionObjectFactory } from - 'domain/skill/MisconceptionObjectFactory'; -import { MultipleChoiceInputRulesService } from +} from 'interactions/MathEquationInput/directives/math-equation-input-validation.service'; +import {MessengerService} from 'services/messenger.service'; +import {MetaTagCustomizationService} from 'services/contextual/meta-tag-customization.service'; +import {MisconceptionObjectFactory} from 'domain/skill/MisconceptionObjectFactory'; +import { + MultipleChoiceInputRulesService, // eslint-disable-next-line max-len - 'interactions/MultipleChoiceInput/directives/multiple-choice-input-rules.service'; -import { MultipleChoiceInputValidationService } from +} from 'interactions/MultipleChoiceInput/directives/multiple-choice-input-rules.service'; +import { + MultipleChoiceInputValidationService, // eslint-disable-next-line max-len - 'interactions/MultipleChoiceInput/directives/multiple-choice-input-validation.service'; -import { MusicNotesInputRulesService } from - 'interactions/MusicNotesInput/directives/music-notes-input-rules.service'; -import { MusicNotesInputValidationService } from +} from 'interactions/MultipleChoiceInput/directives/multiple-choice-input-validation.service'; +import {MusicNotesInputRulesService} from 'interactions/MusicNotesInput/directives/music-notes-input-rules.service'; +import { + MusicNotesInputValidationService, // eslint-disable-next-line max-len - 'interactions/MusicNotesInput/directives/music-notes-input-validation.service'; -import { MusicPhrasePlayerService } from +} from 'interactions/MusicNotesInput/directives/music-notes-input-validation.service'; +import { + MusicPhrasePlayerService, // eslint-disable-next-line max-len - 'interactions/MusicNotesInput/directives/music-phrase-player.service'; -import { NormalizeWhitespacePipe } from - 'filters/string-utility-filters/normalize-whitespace.pipe'; -import { NormalizeWhitespacePunctuationAndCasePipe } from +} from 'interactions/MusicNotesInput/directives/music-phrase-player.service'; +import {NormalizeWhitespacePipe} from 'filters/string-utility-filters/normalize-whitespace.pipe'; +import { + NormalizeWhitespacePunctuationAndCasePipe, // eslint-disable-next-line max-len - 'filters/string-utility-filters/normalize-whitespace-punctuation-and-case.pipe'; -import { NumberAttemptsService } from - 'pages/exploration-player-page/services/number-attempts.service'; -import { NumericInputRulesService } from - 'interactions/NumericInput/directives/numeric-input-rules.service'; -import { NumericInputValidationService } from - 'interactions/NumericInput/directives/numeric-input-validation.service'; -import { NumberWithUnitsObjectFactory } from - 'domain/objects/NumberWithUnitsObjectFactory'; -import { NumberWithUnitsRulesService } from - 'interactions/NumberWithUnits/directives/number-with-units-rules.service'; -import { NumberWithUnitsValidationService } from +} from 'filters/string-utility-filters/normalize-whitespace-punctuation-and-case.pipe'; +import {NumberAttemptsService} from 'pages/exploration-player-page/services/number-attempts.service'; +import {NumericInputRulesService} from 'interactions/NumericInput/directives/numeric-input-rules.service'; +import {NumericInputValidationService} from 'interactions/NumericInput/directives/numeric-input-validation.service'; +import {NumberWithUnitsObjectFactory} from 'domain/objects/NumberWithUnitsObjectFactory'; +import {NumberWithUnitsRulesService} from 'interactions/NumberWithUnits/directives/number-with-units-rules.service'; +import { + NumberWithUnitsValidationService, // eslint-disable-next-line max-len - 'interactions/NumberWithUnits/directives/number-with-units-validation.service'; -import { NumericExpressionInputRulesService } from +} from 'interactions/NumberWithUnits/directives/number-with-units-validation.service'; +import { + NumericExpressionInputRulesService, // eslint-disable-next-line max-len - 'interactions/NumericExpressionInput/directives/numeric-expression-input-rules.service'; -import { NumericExpressionInputValidationService } from +} from 'interactions/NumericExpressionInput/directives/numeric-expression-input-rules.service'; +import { + NumericExpressionInputValidationService, // eslint-disable-next-line max-len - 'interactions/NumericExpressionInput/directives/numeric-expression-input-validation.service'; -import { OutcomeObjectFactory } from 'domain/exploration/OutcomeObjectFactory'; -import { PageTitleService } from 'services/page-title.service'; -import { ParamChangeObjectFactory } from - 'domain/exploration/ParamChangeObjectFactory'; -import { ParamChangesObjectFactory } from - 'domain/exploration/ParamChangesObjectFactory'; -import { ParamSpecObjectFactory } from - 'domain/exploration/ParamSpecObjectFactory'; -import { ParamSpecsObjectFactory } from - 'domain/exploration/ParamSpecsObjectFactory'; -import { ParamTypeObjectFactory } from - 'domain/exploration/ParamTypeObjectFactory'; -import { PencilCodeEditorRulesService } from - 'interactions/PencilCodeEditor/directives/pencil-code-editor-rules.service'; -import { PencilCodeEditorValidationService } from +} from 'interactions/NumericExpressionInput/directives/numeric-expression-input-validation.service'; +import {OutcomeObjectFactory} from 'domain/exploration/OutcomeObjectFactory'; +import {PageTitleService} from 'services/page-title.service'; +import {ParamChangeObjectFactory} from 'domain/exploration/ParamChangeObjectFactory'; +import {ParamChangesObjectFactory} from 'domain/exploration/ParamChangesObjectFactory'; +import {ParamSpecObjectFactory} from 'domain/exploration/ParamSpecObjectFactory'; +import {ParamSpecsObjectFactory} from 'domain/exploration/ParamSpecsObjectFactory'; +import {ParamTypeObjectFactory} from 'domain/exploration/ParamTypeObjectFactory'; +import {PencilCodeEditorRulesService} from 'interactions/PencilCodeEditor/directives/pencil-code-editor-rules.service'; +import { + PencilCodeEditorValidationService, // eslint-disable-next-line max-len - 'interactions/PencilCodeEditor/directives/pencil-code-editor-validation.service'; -import { FeatureFlagDummyBackendApiService } from - 'domain/feature-flag/feature-flag-dummy-backend-api.service'; -import { PlatformFeatureService } from 'services/platform-feature.service'; -import { PlatformParameterAdminBackendApiService } from - 'domain/platform-parameter/platform-parameter-admin-backend-api.service'; -import { FeatureFlagBackendApiService } from - 'domain/feature-flag/feature-flag-backend-api.service'; -import { PlayerPositionService } from - 'pages/exploration-player-page/services/player-position.service'; -import { PlayerTranscriptService } from - 'pages/exploration-player-page/services/player-transcript.service'; -import { PlaythroughBackendApiService } from - 'domain/statistics/playthrough-backend-api.service'; -import { PlaythroughIssuesBackendApiService } from - 'services/playthrough-issues-backend-api.service'; -import { PopulateRuleContentIdsService } from - 'pages/exploration-editor-page/services/populate-rule-content-ids.service'; -import { PredictionAlgorithmRegistryService } from +} from 'interactions/PencilCodeEditor/directives/pencil-code-editor-validation.service'; +import {FeatureFlagDummyBackendApiService} from 'domain/feature-flag/feature-flag-dummy-backend-api.service'; +import {PlatformFeatureService} from 'services/platform-feature.service'; +import {PlatformParameterAdminBackendApiService} from 'domain/platform-parameter/platform-parameter-admin-backend-api.service'; +import {FeatureFlagBackendApiService} from 'domain/feature-flag/feature-flag-backend-api.service'; +import {PlayerPositionService} from 'pages/exploration-player-page/services/player-position.service'; +import {PlayerTranscriptService} from 'pages/exploration-player-page/services/player-transcript.service'; +import {PlaythroughBackendApiService} from 'domain/statistics/playthrough-backend-api.service'; +import {PlaythroughIssuesBackendApiService} from 'services/playthrough-issues-backend-api.service'; +import {PopulateRuleContentIdsService} from 'pages/exploration-editor-page/services/populate-rule-content-ids.service'; +import { + PredictionAlgorithmRegistryService, // eslint-disable-next-line max-len - 'pages/exploration-player-page/services/prediction-algorithm-registry.service'; -import { PretestQuestionBackendApiService } from - 'domain/question/pretest-question-backend-api.service'; -import { ProfilePageBackendApiService } from - 'pages/profile-page/profile-page-backend-api.service'; -import { PythonProgramTokenizer } from 'classifiers/python-program.tokenizer'; -import { QuestionBackendApiService } from - 'domain/question/question-backend-api.service'; -import { QuestionObjectFactory } from 'domain/question/QuestionObjectFactory'; -import { RatingComputationService } from - 'components/ratings/rating-computation/rating-computation.service'; -import { RatioExpressionInputRulesService } from +} from 'pages/exploration-player-page/services/prediction-algorithm-registry.service'; +import {PretestQuestionBackendApiService} from 'domain/question/pretest-question-backend-api.service'; +import {ProfilePageBackendApiService} from 'pages/profile-page/profile-page-backend-api.service'; +import {PythonProgramTokenizer} from 'classifiers/python-program.tokenizer'; +import {QuestionBackendApiService} from 'domain/question/question-backend-api.service'; +import {QuestionObjectFactory} from 'domain/question/QuestionObjectFactory'; +import {RatingComputationService} from 'components/ratings/rating-computation/rating-computation.service'; +import { + RatioExpressionInputRulesService, // eslint-disable-next-line max-len - 'interactions/RatioExpressionInput/directives/ratio-expression-input-rules.service'; -import { RatioExpressionInputValidationService } from +} from 'interactions/RatioExpressionInput/directives/ratio-expression-input-rules.service'; +import { + RatioExpressionInputValidationService, // eslint-disable-next-line max-len - 'interactions/RatioExpressionInput/directives/ratio-expression-input-validation.service'; -import { ReadOnlyCollectionBackendApiService } from - 'domain/collection/read-only-collection-backend-api.service'; -import { ReadOnlyTopicObjectFactory } from - 'domain/topic_viewer/read-only-topic-object.factory'; -import { ReviewTestBackendApiService } from - 'domain/review_test/review-test-backend-api.service'; -import { ReviewTestEngineService } from - 'pages/review-test-page/review-test-engine.service'; -import { SchemaDefaultValueService } from - 'services/schema-default-value.service'; -import { SchemaFormSubmittedService } from - 'services/schema-form-submitted.service'; -import { SchemaUndefinedLastElementService } from - 'services/schema-undefined-last-element.service'; -import { SearchExplorationsBackendApiService } from - 'domain/collection/search-explorations-backend-api.service'; -import { SetInputRulesService } from - 'interactions/SetInput/directives/set-input-rules.service'; -import { SetInputValidationService } from - 'interactions/SetInput/directives/set-input-validation.service'; -import { SVMPredictionService } from 'classifiers/svm-prediction.service'; -import { SidebarStatusService } from 'services/sidebar-status.service'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { SkillCreationBackendApiService } from - 'domain/skill/skill-creation-backend-api.service'; -import { SkillMasteryBackendApiService } from - 'domain/skill/skill-mastery-backend-api.service'; -import { SkillObjectFactory } from 'domain/skill/SkillObjectFactory'; -import { SkillRightsBackendApiService} from - 'domain/skill/skill-rights-backend-api.service'; -import { SolutionObjectFactory } from - 'domain/exploration/SolutionObjectFactory'; -import { SolutionValidityService } from - 'pages/exploration-editor-page/editor-tab/services/solution-validity.service'; -import { SpeechSynthesisChunkerService } from - 'services/speech-synthesis-chunker.service'; -import { StateClassifierMappingService } from - 'pages/exploration-player-page/services/state-classifier-mapping.service'; -import { StateContentService } from +} from 'interactions/RatioExpressionInput/directives/ratio-expression-input-validation.service'; +import {ReadOnlyCollectionBackendApiService} from 'domain/collection/read-only-collection-backend-api.service'; +import {ReadOnlyTopicObjectFactory} from 'domain/topic_viewer/read-only-topic-object.factory'; +import {ReviewTestBackendApiService} from 'domain/review_test/review-test-backend-api.service'; +import {ReviewTestEngineService} from 'pages/review-test-page/review-test-engine.service'; +import {SchemaDefaultValueService} from 'services/schema-default-value.service'; +import {SchemaFormSubmittedService} from 'services/schema-form-submitted.service'; +import {SchemaUndefinedLastElementService} from 'services/schema-undefined-last-element.service'; +import {SearchExplorationsBackendApiService} from 'domain/collection/search-explorations-backend-api.service'; +import {SetInputRulesService} from 'interactions/SetInput/directives/set-input-rules.service'; +import {SetInputValidationService} from 'interactions/SetInput/directives/set-input-validation.service'; +import {SVMPredictionService} from 'classifiers/svm-prediction.service'; +import {SidebarStatusService} from 'services/sidebar-status.service'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {SkillCreationBackendApiService} from 'domain/skill/skill-creation-backend-api.service'; +import {SkillMasteryBackendApiService} from 'domain/skill/skill-mastery-backend-api.service'; +import {SkillObjectFactory} from 'domain/skill/SkillObjectFactory'; +import {SkillRightsBackendApiService} from 'domain/skill/skill-rights-backend-api.service'; +import {SolutionObjectFactory} from 'domain/exploration/SolutionObjectFactory'; +import {SolutionValidityService} from 'pages/exploration-editor-page/editor-tab/services/solution-validity.service'; +import {SpeechSynthesisChunkerService} from 'services/speech-synthesis-chunker.service'; +import {StateClassifierMappingService} from 'pages/exploration-player-page/services/state-classifier-mapping.service'; +import { + StateContentService, // eslint-disable-next-line max-len - 'components/state-editor/state-editor-properties-services/state-content.service'; -import { StateCustomizationArgsService } from +} from 'components/state-editor/state-editor-properties-services/state-content.service'; +import { + StateCustomizationArgsService, // eslint-disable-next-line max-len - 'components/state-editor/state-editor-properties-services/state-customization-args.service'; -import { StateEditorRefreshService } from - 'pages/exploration-editor-page/services/state-editor-refresh.service'; -import { StateEditorService } from +} from 'components/state-editor/state-editor-properties-services/state-customization-args.service'; +import {StateEditorRefreshService} from 'pages/exploration-editor-page/services/state-editor-refresh.service'; +import { + StateEditorService, // eslint-disable-next-line max-len - 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { StateGraphLayoutService } from - 'components/graph-services/graph-layout.service'; -import { StateHintsService } from +} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {StateGraphLayoutService} from 'components/graph-services/graph-layout.service'; +import { + StateHintsService, // eslint-disable-next-line max-len - 'components/state-editor/state-editor-properties-services/state-hints.service'; -import { StateInteractionIdService } from +} from 'components/state-editor/state-editor-properties-services/state-hints.service'; +import { + StateInteractionIdService, // eslint-disable-next-line max-len - 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; -import { StateInteractionStatsBackendApiService } from - 'domain/exploration/state-interaction-stats-backend-api.service'; -import { StateInteractionStatsService } from - 'services/state-interaction-stats.service'; -import { StateNameService } from - 'components/state-editor/state-editor-properties-services/state-name.service'; -import { StateObjectFactory } from 'domain/state/StateObjectFactory'; -import { StateParamChangesService } from +} from 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; +import {StateInteractionStatsBackendApiService} from 'domain/exploration/state-interaction-stats-backend-api.service'; +import {StateInteractionStatsService} from 'services/state-interaction-stats.service'; +import {StateNameService} from 'components/state-editor/state-editor-properties-services/state-name.service'; +import {StateObjectFactory} from 'domain/state/StateObjectFactory'; +import { + StateParamChangesService, // eslint-disable-next-line max-len - 'components/state-editor/state-editor-properties-services/state-param-changes.service'; -import { StatePropertyService } from +} from 'components/state-editor/state-editor-properties-services/state-param-changes.service'; +import { + StatePropertyService, // eslint-disable-next-line max-len - 'components/state-editor/state-editor-properties-services/state-property.service'; -import { StateRecordedVoiceoversService } from +} from 'components/state-editor/state-editor-properties-services/state-property.service'; +import { + StateRecordedVoiceoversService, // eslint-disable-next-line max-len - 'components/state-editor/state-editor-properties-services/state-recorded-voiceovers.service'; -import { StateSolicitAnswerDetailsService } from +} from 'components/state-editor/state-editor-properties-services/state-recorded-voiceovers.service'; +import { + StateSolicitAnswerDetailsService, // eslint-disable-next-line max-len - 'components/state-editor/state-editor-properties-services/state-solicit-answer-details.service'; -import { StateSolutionService } from +} from 'components/state-editor/state-editor-properties-services/state-solicit-answer-details.service'; +import { + StateSolutionService, // eslint-disable-next-line max-len - 'components/state-editor/state-editor-properties-services/state-solution.service'; -import { StateTopAnswersStatsBackendApiService } from - 'services/state-top-answers-stats-backend-api.service'; -import { StateTopAnswersStatsObjectFactory } from - 'domain/statistics/state-top-answers-stats-object.factory'; -import { StateTopAnswersStatsService } from - 'services/state-top-answers-stats.service'; -import { StateWrittenTranslationsService } from +} from 'components/state-editor/state-editor-properties-services/state-solution.service'; +import {StateTopAnswersStatsBackendApiService} from 'services/state-top-answers-stats-backend-api.service'; +import {StateTopAnswersStatsObjectFactory} from 'domain/statistics/state-top-answers-stats-object.factory'; +import {StateTopAnswersStatsService} from 'services/state-top-answers-stats.service'; +import { + StateWrittenTranslationsService, // eslint-disable-next-line max-len - 'components/state-editor/state-editor-properties-services/state-written-translations.service'; -import { StatsReportingBackendApiService } from - 'domain/exploration/stats-reporting-backend-api.service'; -import { StatesObjectFactory } from 'domain/exploration/StatesObjectFactory'; -import { StoryEditorNavigationService } from - 'pages/story-editor-page/services/story-editor-navigation.service'; -import { StoryViewerBackendApiService } from - 'domain/story_viewer/story-viewer-backend-api.service'; -import { SubtitledUnicodeObjectFactory } from - 'domain/exploration/SubtitledUnicodeObjectFactory'; -import { SubtopicViewerBackendApiService } from - 'domain/subtopic_viewer/subtopic-viewer-backend-api.service'; -import { SuggestionModalService } from 'services/suggestion-modal.service'; -import { SuggestionsService } from 'services/suggestions.service'; -import { TextInputPredictionService } from - 'interactions/TextInput/text-input-prediction.service'; -import { TextInputRulesService } from - 'interactions/TextInput/directives/text-input-rules.service'; -import { TextInputTokenizer } from 'classifiers/text-input.tokenizer'; -import { TextInputValidationService } from - 'interactions/TextInput/directives/text-input-validation.service'; -import { ThreadStatusDisplayService } from +} from 'components/state-editor/state-editor-properties-services/state-written-translations.service'; +import {StatsReportingBackendApiService} from 'domain/exploration/stats-reporting-backend-api.service'; +import {StatesObjectFactory} from 'domain/exploration/StatesObjectFactory'; +import {StoryEditorNavigationService} from 'pages/story-editor-page/services/story-editor-navigation.service'; +import {StoryViewerBackendApiService} from 'domain/story_viewer/story-viewer-backend-api.service'; +import {SubtitledUnicodeObjectFactory} from 'domain/exploration/SubtitledUnicodeObjectFactory'; +import {SubtopicViewerBackendApiService} from 'domain/subtopic_viewer/subtopic-viewer-backend-api.service'; +import {SuggestionModalService} from 'services/suggestion-modal.service'; +import {SuggestionsService} from 'services/suggestions.service'; +import {TextInputPredictionService} from 'interactions/TextInput/text-input-prediction.service'; +import {TextInputRulesService} from 'interactions/TextInput/directives/text-input-rules.service'; +import {TextInputTokenizer} from 'classifiers/text-input.tokenizer'; +import {TextInputValidationService} from 'interactions/TextInput/directives/text-input-validation.service'; +import { + ThreadStatusDisplayService, // eslint-disable-next-line max-len - 'pages/exploration-editor-page/feedback-tab/services/thread-status-display.service'; -import { TopicCreationBackendApiService } from - 'domain/topic/topic-creation-backend-api.service'; -import { TopicsAndSkillsDashboardBackendApiService } from +} from 'pages/exploration-editor-page/feedback-tab/services/thread-status-display.service'; +import {TopicCreationBackendApiService} from 'domain/topic/topic-creation-backend-api.service'; +import { + TopicsAndSkillsDashboardBackendApiService, // eslint-disable-next-line max-len - 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; -import { TopicsAndSkillsDashboardPageService } from +} from 'domain/topics_and_skills_dashboard/topics-and-skills-dashboard-backend-api.service'; +import { + TopicsAndSkillsDashboardPageService, // eslint-disable-next-line max-len - 'pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.service'; -import { TopicViewerBackendApiService } from - 'domain/topic_viewer/topic-viewer-backend-api.service'; -import { UnitsObjectFactory } from 'domain/objects/UnitsObjectFactory'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; -import { UrlService } from 'services/contextual/url.service'; -import { UserBackendApiService } from 'services/user-backend-api.service'; -import { UserService } from 'services/user.service'; -import { UserExplorationPermissionsService } from - 'pages/exploration-editor-page/services/user-exploration-permissions.service'; -import { UtilsService } from 'services/utils.service'; -import { ValidatorsService } from 'services/validators.service'; -import { VersionTreeService } from - 'pages/exploration-editor-page/history-tab/services/version-tree.service'; -import { VoiceoverBackendApiService } from 'domain/voiceover/voiceover-backend-api.service'; -import { WindowDimensionsService } from - 'services/contextual/window-dimensions.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { WinnowingPreprocessingService } from - 'classifiers/winnowing-preprocessing.service'; -import { WrittenTranslationObjectFactory } from - 'domain/exploration/WrittenTranslationObjectFactory'; -import { WrittenTranslationsObjectFactory } from - 'domain/exploration/WrittenTranslationsObjectFactory'; -import { SolutionVerificationService } from +} from 'pages/topics-and-skills-dashboard-page/topics-and-skills-dashboard-page.service'; +import {TopicViewerBackendApiService} from 'domain/topic_viewer/topic-viewer-backend-api.service'; +import {UnitsObjectFactory} from 'domain/objects/UnitsObjectFactory'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {UrlService} from 'services/contextual/url.service'; +import {UserBackendApiService} from 'services/user-backend-api.service'; +import {UserService} from 'services/user.service'; +import {UserExplorationPermissionsService} from 'pages/exploration-editor-page/services/user-exploration-permissions.service'; +import {UtilsService} from 'services/utils.service'; +import {ValidatorsService} from 'services/validators.service'; +import {VersionTreeService} from 'pages/exploration-editor-page/history-tab/services/version-tree.service'; +import {VoiceoverBackendApiService} from 'domain/voiceover/voiceover-backend-api.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {WinnowingPreprocessingService} from 'classifiers/winnowing-preprocessing.service'; +import {WrittenTranslationObjectFactory} from 'domain/exploration/WrittenTranslationObjectFactory'; +import {WrittenTranslationsObjectFactory} from 'domain/exploration/WrittenTranslationsObjectFactory'; +import { + SolutionVerificationService, // eslint-disable-next-line max-len - 'pages/exploration-editor-page/editor-tab/services/solution-verification.service'; -import { ResponsesService } from - 'pages/exploration-editor-page/editor-tab/services/responses.service'; -import { QuestionValidationService } from './question-validation.service'; -import { MathInteractionsService } from './math-interactions.service'; +} from 'pages/exploration-editor-page/editor-tab/services/solution-verification.service'; +import {ResponsesService} from 'pages/exploration-editor-page/editor-tab/services/responses.service'; +import {QuestionValidationService} from './question-validation.service'; +import {MathInteractionsService} from './math-interactions.service'; interface UpgradedServicesDict { // Type 'unknown' is used here because we don't know the exact type of @@ -483,7 +382,7 @@ interface UpgradedServicesDict { [service: string]: unknown; } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class UpgradedServices { getUpgradedServices(): UpgradedServicesDict { @@ -502,7 +401,8 @@ export class UpgradedServices { upgradedServices['AlgebraicExpressionInputRulesService'] = new AlgebraicExpressionInputRulesService( new MathInteractionsService(), - new NumericExpressionInputRulesService()); + new NumericExpressionInputRulesService() + ); upgradedServices['AngularNameService'] = new AngularNameService(); upgradedServices['AppService'] = new AppService(); upgradedServices['AudioBarStatusService'] = new AudioBarStatusService(); @@ -512,8 +412,9 @@ export class UpgradedServices { upgradedServices['BackgroundMaskService'] = new BackgroundMaskService(); upgradedServices['baseInteractionValidationService'] = new baseInteractionValidationService(); - upgradedServices['BrowserCheckerService'] = - new BrowserCheckerService(new WindowRef()); + upgradedServices['BrowserCheckerService'] = new BrowserCheckerService( + new WindowRef() + ); upgradedServices['CamelCaseToHyphensPipe'] = new CamelCaseToHyphensPipe(); upgradedServices['CodeNormalizerService'] = new CodeNormalizerService(); upgradedServices['CollectionValidationService'] = @@ -552,7 +453,8 @@ export class UpgradedServices { upgradedServices['LoaderService'] = new LoaderService(); upgradedServices['LoggerService'] = new LoggerService(); upgradedServices['LostChangeObjectFactory'] = new LostChangeObjectFactory( - new UtilsService); + new UtilsService() + ); upgradedServices['MathEquationInputRulesService'] = new MathEquationInputRulesService( upgradedServices['AlgebraicExpressionInputRulesService'] @@ -598,8 +500,9 @@ export class UpgradedServices { new ThreadStatusDisplayService(); upgradedServices['Title'] = new Title({}); upgradedServices['TopicsAndSkillsDashboardPageService'] = - new TopicsAndSkillsDashboardPageService( - upgradedServices['PlatformFeatureService']); + new TopicsAndSkillsDashboardPageService( + upgradedServices['PlatformFeatureService'] + ); upgradedServices['UnitsObjectFactory'] = new UnitsObjectFactory(); upgradedServices['UtilsService'] = new UtilsService(); upgradedServices['VersionTreeService'] = new VersionTreeService(); @@ -616,279 +519,352 @@ export class UpgradedServices { // Topological level: 1. upgradedServices['AlgebraicExpressionInputValidationService'] = new AlgebraicExpressionInputValidationService( - upgradedServices['baseInteractionValidationService']); + upgradedServices['baseInteractionValidationService'] + ); upgradedServices['AlertsService'] = new AlertsService( - upgradedServices['LoggerService']); + upgradedServices['LoggerService'] + ); upgradedServices['BrowserCheckerService'] = new BrowserCheckerService( - upgradedServices['WindowRef']); + upgradedServices['WindowRef'] + ); upgradedServices['CodeReplValidationService'] = new CodeReplValidationService( - upgradedServices['baseInteractionValidationService']); + upgradedServices['baseInteractionValidationService'] + ); upgradedServices['ContinueValidationService'] = new ContinueValidationService( - upgradedServices['baseInteractionValidationService']); + upgradedServices['baseInteractionValidationService'] + ); upgradedServices['DeviceInfoService'] = new DeviceInfoService( - upgradedServices['WindowRef']); + upgradedServices['WindowRef'] + ); upgradedServices['DocumentAttributeCustomizationService'] = new DocumentAttributeCustomizationService(upgradedServices['WindowRef']); upgradedServices['DragAndDropSortInputValidationService'] = new DragAndDropSortInputValidationService( - upgradedServices['baseInteractionValidationService']); + upgradedServices['baseInteractionValidationService'] + ); upgradedServices['EndExplorationValidationService'] = new EndExplorationValidationService( - upgradedServices['baseInteractionValidationService']); + upgradedServices['baseInteractionValidationService'] + ); upgradedServices['ExpressionSyntaxTreeService'] = new ExpressionSyntaxTreeService( - upgradedServices['ExpressionParserService']); + upgradedServices['ExpressionParserService'] + ); upgradedServices['FeedbackThreadObjectFactory'] = new FeedbackThreadObjectFactory(); upgradedServices['FractionInputRulesService'] = - new FractionInputRulesService( - upgradedServices['UtilsService']); + new FractionInputRulesService(upgradedServices['UtilsService']); upgradedServices['FractionInputValidationService'] = new FractionInputValidationService( - upgradedServices['baseInteractionValidationService']); - upgradedServices['GraphInputRulesService'] = - new GraphInputRulesService( - upgradedServices['GraphUtilsService'], - upgradedServices['UtilsService']); + upgradedServices['baseInteractionValidationService'] + ); + upgradedServices['GraphInputRulesService'] = new GraphInputRulesService( + upgradedServices['GraphUtilsService'], + upgradedServices['UtilsService'] + ); upgradedServices['GraphInputValidationService'] = new GraphInputValidationService( - upgradedServices['baseInteractionValidationService']); + upgradedServices['baseInteractionValidationService'] + ); upgradedServices['GuestCollectionProgressService'] = - new GuestCollectionProgressService( - upgradedServices['WindowRef']); + new GuestCollectionProgressService(upgradedServices['WindowRef']); upgradedServices['HtmlEscaperService'] = new HtmlEscaperService( - upgradedServices['LoggerService']); + upgradedServices['LoggerService'] + ); upgradedServices['HttpXhrBackend'] = new HttpXhrBackend( - upgradedServices['ɵangular_packages_common_http_http_d']); + upgradedServices['ɵangular_packages_common_http_http_d'] + ); upgradedServices['ImageClickInputValidationService'] = new ImageClickInputValidationService( - upgradedServices['baseInteractionValidationService']); + upgradedServices['baseInteractionValidationService'] + ); upgradedServices['InteractiveMapValidationService'] = new InteractiveMapValidationService( - upgradedServices['baseInteractionValidationService']); + upgradedServices['baseInteractionValidationService'] + ); upgradedServices['ItemSelectionInputValidationService'] = new ItemSelectionInputValidationService( - upgradedServices['baseInteractionValidationService']); + upgradedServices['baseInteractionValidationService'] + ); upgradedServices['LocalStorageService'] = new LocalStorageService( - upgradedServices['WindowRef']); + upgradedServices['WindowRef'] + ); upgradedServices['MathEquationInputValidationService'] = new MathEquationInputValidationService( - upgradedServices['baseInteractionValidationService']); + upgradedServices['baseInteractionValidationService'] + ); upgradedServices['MessengerService'] = new MessengerService( upgradedServices['LoggerService'], - upgradedServices['WindowRef']); + upgradedServices['WindowRef'] + ); upgradedServices['MetaTagCustomizationService'] = new MetaTagCustomizationService(upgradedServices['WindowRef']); upgradedServices['MultipleChoiceInputValidationService'] = new MultipleChoiceInputValidationService( - upgradedServices['baseInteractionValidationService']); + upgradedServices['baseInteractionValidationService'] + ); upgradedServices['MusicNotesInputRulesService'] = - new MusicNotesInputRulesService( - upgradedServices['UtilsService']); + new MusicNotesInputRulesService(upgradedServices['UtilsService']); upgradedServices['MusicNotesInputValidationService'] = new MusicNotesInputValidationService( - upgradedServices['baseInteractionValidationService']); + upgradedServices['baseInteractionValidationService'] + ); upgradedServices['NormalizeWhitespacePipe'] = new NormalizeWhitespacePipe( - upgradedServices['UtilsService']); + upgradedServices['UtilsService'] + ); upgradedServices['NumericInputValidationService'] = new NumericInputValidationService( - upgradedServices['baseInteractionValidationService']); + upgradedServices['baseInteractionValidationService'] + ); upgradedServices['NumberWithUnitsObjectFactory'] = - new NumberWithUnitsObjectFactory( - upgradedServices['UnitsObjectFactory']); + new NumberWithUnitsObjectFactory(upgradedServices['UnitsObjectFactory']); upgradedServices['NumericExpressionInputValidationService'] = new NumericExpressionInputValidationService( - upgradedServices['baseInteractionValidationService']); + upgradedServices['baseInteractionValidationService'] + ); upgradedServices['NumberWithUnitsRulesService'] = new NumberWithUnitsRulesService( upgradedServices['UnitsObjectFactory'], - upgradedServices['UtilsService']); - upgradedServices['OutcomeObjectFactory'] = - new OutcomeObjectFactory(); + upgradedServices['UtilsService'] + ); + upgradedServices['OutcomeObjectFactory'] = new OutcomeObjectFactory(); upgradedServices['PageTitleService'] = new PageTitleService( - upgradedServices['Meta'], upgradedServices['Title']); + upgradedServices['Meta'], + upgradedServices['Title'] + ); upgradedServices['ParamChangesObjectFactory'] = new ParamChangesObjectFactory( - upgradedServices['ParamChangeObjectFactory']); + upgradedServices['ParamChangeObjectFactory'] + ); upgradedServices['ParamSpecObjectFactory'] = new ParamSpecObjectFactory( - upgradedServices['ParamTypeObjectFactory']); + upgradedServices['ParamTypeObjectFactory'] + ); upgradedServices['PencilCodeEditorValidationService'] = new PencilCodeEditorValidationService( - upgradedServices['baseInteractionValidationService']); + upgradedServices['baseInteractionValidationService'] + ); upgradedServices['PlayerTranscriptService'] = new PlayerTranscriptService( - upgradedServices['LoggerService']); + upgradedServices['LoggerService'] + ); upgradedServices['PythonProgramTokenizer'] = new PythonProgramTokenizer( - upgradedServices['LoggerService']); - upgradedServices['ResponsesService'] = - new ResponsesService( - upgradedServices['AlertsService'], - upgradedServices['LoggerService'], - upgradedServices['OutcomeObjectFactory'], - upgradedServices['SolutionValidityService'], - upgradedServices['SolutionVerificationService'], - upgradedServices['StateCustomizationArgsService'], - upgradedServices['StateEditorService'], - upgradedServices['StateInteractionIdService'], - upgradedServices['StateSolutionService'] - ); + upgradedServices['LoggerService'] + ); + upgradedServices['ResponsesService'] = new ResponsesService( + upgradedServices['AlertsService'], + upgradedServices['LoggerService'], + upgradedServices['OutcomeObjectFactory'], + upgradedServices['SolutionValidityService'], + upgradedServices['SolutionVerificationService'], + upgradedServices['StateCustomizationArgsService'], + upgradedServices['StateEditorService'], + upgradedServices['StateInteractionIdService'], + upgradedServices['StateSolutionService'] + ); upgradedServices['QuestionValidationService'] = new QuestionValidationService( upgradedServices['ResponsesService'], upgradedServices['StateEditorService'] ); upgradedServices['RatioExpressionInputValidationService'] = - new RatioExpressionInputValidationService( - upgradedServices['baseInteractionValidationService']); + new RatioExpressionInputValidationService( + upgradedServices['baseInteractionValidationService'] + ); upgradedServices['SetInputValidationService'] = new SetInputValidationService( - upgradedServices['baseInteractionValidationService']); + upgradedServices['baseInteractionValidationService'] + ); upgradedServices['SkillCreationBackendApiService'] = - new SkillCreationBackendApiService( - upgradedServices['HttpClient'], - upgradedServices['ImageLocalStorageService']); + new SkillCreationBackendApiService( + upgradedServices['HttpClient'], + upgradedServices['ImageLocalStorageService'] + ); upgradedServices['StateTopAnswersStatsObjectFactory'] = - new StateTopAnswersStatsObjectFactory(); + new StateTopAnswersStatsObjectFactory(); upgradedServices['SpeechSynthesisChunkerService'] = - new SpeechSynthesisChunkerService( - upgradedServices['HtmlEscaperService']); + new SpeechSynthesisChunkerService(upgradedServices['HtmlEscaperService']); upgradedServices['SVMPredictionService'] = new SVMPredictionService(); upgradedServices['SchemaDefaultValueService'] = new SchemaDefaultValueService( upgradedServices['LoggerService'], - upgradedServices['SubtitledUnicodeObjectFactory']); + upgradedServices['SubtitledUnicodeObjectFactory'] + ); upgradedServices['SiteAnalyticsService'] = new SiteAnalyticsService( - upgradedServices['WindowRef']); + upgradedServices['WindowRef'] + ); upgradedServices['StateEditorService'] = new StateEditorService( - upgradedServices['SolutionValidityService']); + upgradedServices['SolutionValidityService'] + ); upgradedServices['TextInputValidationService'] = new TextInputValidationService( - upgradedServices['baseInteractionValidationService']); + upgradedServices['baseInteractionValidationService'] + ); upgradedServices['TopicCreationBackendApiService'] = - new TopicCreationBackendApiService( - upgradedServices['HttpClient']); + new TopicCreationBackendApiService(upgradedServices['HttpClient']); upgradedServices['UrlService'] = new UrlService( - upgradedServices['WindowRef']); + upgradedServices['WindowRef'] + ); upgradedServices['WindowDimensionsService'] = new WindowDimensionsService( - upgradedServices['WindowRef']); + upgradedServices['WindowRef'] + ); upgradedServices['WrittenTranslationsObjectFactory'] = new WrittenTranslationsObjectFactory( - upgradedServices['WrittenTranslationObjectFactory']); + upgradedServices['WrittenTranslationObjectFactory'] + ); // Topological level: 2. upgradedServices['CsrfTokenService'] = new CsrfTokenService( - upgradedServices['HttpXhrBackend']); + upgradedServices['HttpXhrBackend'] + ); upgradedServices['AnswerGroupObjectFactory'] = new AnswerGroupObjectFactory( - upgradedServices['OutcomeObjectFactory']); + upgradedServices['OutcomeObjectFactory'] + ); upgradedServices['CkEditorCopyContentService'] = - new CkEditorCopyContentService( - upgradedServices['HtmlEscaperService']); + new CkEditorCopyContentService(upgradedServices['HtmlEscaperService']); upgradedServices['AutogeneratedAudioPlayerService'] = new AutogeneratedAudioPlayerService( - upgradedServices['SpeechSynthesisChunkerService']); - upgradedServices['BottomNavbarStatusService'] = ( + upgradedServices['SpeechSynthesisChunkerService'] + ); + upgradedServices['BottomNavbarStatusService'] = new BottomNavbarStatusService( - upgradedServices['WindowDimensionsService'])); + upgradedServices['WindowDimensionsService'] + ); upgradedServices['PreventPageUnloadEventService'] = - new PreventPageUnloadEventService(upgradedServices['WindowRef']); + new PreventPageUnloadEventService(upgradedServices['WindowRef']); upgradedServices['CodeReplRulesService'] = new CodeReplRulesService( upgradedServices['NormalizeWhitespacePipe'], - upgradedServices['CodeNormalizerService']); + upgradedServices['CodeNormalizerService'] + ); upgradedServices['ContextService'] = new ContextService( upgradedServices['UrlService'], - upgradedServices['BlogPostPageService']); + upgradedServices['BlogPostPageService'] + ); upgradedServices['EditorFirstTimeEventsService'] = new EditorFirstTimeEventsService( - upgradedServices['SiteAnalyticsService']); + upgradedServices['SiteAnalyticsService'] + ); upgradedServices['ExplorationImprovementsTaskRegistryService'] = new ExplorationImprovementsTaskRegistryService(); upgradedServices['ExpressionEvaluatorService'] = new ExpressionEvaluatorService( upgradedServices['ExpressionParserService'], - upgradedServices['ExpressionSyntaxTreeService']); + upgradedServices['ExpressionSyntaxTreeService'] + ); upgradedServices['ExtensionTagAssemblerService'] = new ExtensionTagAssemblerService( upgradedServices['HtmlEscaperService'], - upgradedServices['CamelCaseToHyphensPipe']); + upgradedServices['CamelCaseToHyphensPipe'] + ); upgradedServices['FocusManagerService'] = new FocusManagerService( upgradedServices['DeviceInfoService'], upgradedServices['IdGenerationService'], - upgradedServices['WindowRef'], + upgradedServices['WindowRef'] ); upgradedServices['HttpClient'] = new HttpClient( - upgradedServices['HttpXhrBackend']); + upgradedServices['HttpXhrBackend'] + ); upgradedServices['LanguageUtilService'] = new LanguageUtilService( - upgradedServices['BrowserCheckerService']); + upgradedServices['BrowserCheckerService'] + ); upgradedServices['NumberWithUnitsValidationService'] = new NumberWithUnitsValidationService( upgradedServices['NumberWithUnitsObjectFactory'], - upgradedServices['baseInteractionValidationService']); + upgradedServices['baseInteractionValidationService'] + ); upgradedServices['ParamSpecsObjectFactory'] = new ParamSpecsObjectFactory( - upgradedServices['ParamSpecObjectFactory']); + upgradedServices['ParamSpecObjectFactory'] + ); upgradedServices['PencilCodeEditorRulesService'] = new PencilCodeEditorRulesService( upgradedServices['NormalizeWhitespacePipe'], upgradedServices['NormalizeWhitespacePunctuationAndCasePipe'], - upgradedServices['CodeNormalizerService']); + upgradedServices['CodeNormalizerService'] + ); upgradedServices['SidebarStatusService'] = new SidebarStatusService( - upgradedServices['WindowDimensionsService']); + upgradedServices['WindowDimensionsService'] + ); upgradedServices['StateContentService'] = new StateContentService( - upgradedServices['AlertsService'], upgradedServices['UtilsService']); + upgradedServices['AlertsService'], + upgradedServices['UtilsService'] + ); upgradedServices['StateCustomizationArgsService'] = new StateCustomizationArgsService( - upgradedServices['AlertsService'], upgradedServices['UtilsService']); + upgradedServices['AlertsService'], + upgradedServices['UtilsService'] + ); upgradedServices['StateHintsService'] = new StateHintsService( - upgradedServices['AlertsService'], upgradedServices['UtilsService']); + upgradedServices['AlertsService'], + upgradedServices['UtilsService'] + ); upgradedServices['StateInteractionIdService'] = new StateInteractionIdService( - upgradedServices['AlertsService'], upgradedServices['UtilsService']); + upgradedServices['AlertsService'], + upgradedServices['UtilsService'] + ); upgradedServices['StatePropertyService'] = new StatePropertyService( - upgradedServices['AlertsService'], upgradedServices['UtilsService']); + upgradedServices['AlertsService'], + upgradedServices['UtilsService'] + ); upgradedServices['StateRecordedVoiceoversService'] = new StateRecordedVoiceoversService( - upgradedServices['AlertsService'], upgradedServices['UtilsService']); + upgradedServices['AlertsService'], + upgradedServices['UtilsService'] + ); upgradedServices['StateSolicitAnswerDetailsService'] = new StateSolicitAnswerDetailsService( - upgradedServices['AlertsService'], upgradedServices['UtilsService']); + upgradedServices['AlertsService'], + upgradedServices['UtilsService'] + ); upgradedServices['StateSolutionService'] = new StateSolutionService( - upgradedServices['AlertsService'], upgradedServices['UtilsService']); + upgradedServices['AlertsService'], + upgradedServices['UtilsService'] + ); upgradedServices['StateWrittenTranslationsService'] = new StateWrittenTranslationsService( - upgradedServices['AlertsService'], upgradedServices['UtilsService']); + upgradedServices['AlertsService'], + upgradedServices['UtilsService'] + ); upgradedServices['StoryEditorNavigationService'] = - new StoryEditorNavigationService(upgradedServices['WindowRef']); + new StoryEditorNavigationService(upgradedServices['WindowRef']); upgradedServices['TextInputRulesService'] = new TextInputRulesService( - upgradedServices['NormalizeWhitespacePipe']); + upgradedServices['NormalizeWhitespacePipe'] + ); upgradedServices['UrlInterpolationService'] = new UrlInterpolationService( - upgradedServices['AlertsService'], upgradedServices['UrlService'], - upgradedServices['UtilsService']); + upgradedServices['AlertsService'], + upgradedServices['UrlService'], + upgradedServices['UtilsService'] + ); upgradedServices['ValidatorsService'] = new ValidatorsService( upgradedServices['AlertsService'], - upgradedServices['NormalizeWhitespacePipe']); + upgradedServices['NormalizeWhitespacePipe'] + ); // Topological level: 3. upgradedServices['AdminBackendApiService'] = new AdminBackendApiService( upgradedServices['HttpClient'], - upgradedServices['UrlInterpolationService']); + upgradedServices['UrlInterpolationService'] + ); upgradedServices['AdminDataService'] = new AdminDataService( - upgradedServices['HttpClient']); - upgradedServices['AssetsBackendApiService'] = - new AssetsBackendApiService( - upgradedServices['CsrfTokenService'], - upgradedServices['HttpClient'], - upgradedServices['UrlInterpolationService']); + upgradedServices['HttpClient'] + ); + upgradedServices['AssetsBackendApiService'] = new AssetsBackendApiService( + upgradedServices['CsrfTokenService'], + upgradedServices['HttpClient'], + upgradedServices['UrlInterpolationService'] + ); upgradedServices['EmailDashboardBackendApiService'] = - new EmailDashboardBackendApiService( - upgradedServices['HttpClient']); + new EmailDashboardBackendApiService(upgradedServices['HttpClient']); upgradedServices['ExplorationPermissionsBackendApiService'] = new ExplorationPermissionsBackendApiService( upgradedServices['ContextService'], upgradedServices['HttpClient'], - upgradedServices['UrlInterpolationService']); + upgradedServices['UrlInterpolationService'] + ); upgradedServices['ExplorationRecommendationsBackendApiService'] = new ExplorationRecommendationsBackendApiService( - upgradedServices['HttpClient']); + upgradedServices['HttpClient'] + ); upgradedServices['InteractionRulesRegistryService'] = new InteractionRulesRegistryService( upgradedServices['AlgebraicExpressionInputRulesService'], @@ -910,184 +886,211 @@ export class UpgradedServices { upgradedServices['PencilCodeEditorRulesService'], upgradedServices['RatioExpressionInputRulesService'], upgradedServices['SetInputRulesService'], - upgradedServices['TextInputRulesService']); + upgradedServices['TextInputRulesService'] + ); upgradedServices['AudioTranslationLanguageService'] = new AudioTranslationLanguageService( upgradedServices['BrowserCheckerService'], - upgradedServices['LanguageUtilService']); + upgradedServices['LanguageUtilService'] + ); upgradedServices['ConceptCardBackendApiService'] = new ConceptCardBackendApiService( upgradedServices['HttpClient'], - upgradedServices['UrlInterpolationService']); + upgradedServices['UrlInterpolationService'] + ); upgradedServices['ClassifierDataBackendApiService'] = new ClassifierDataBackendApiService( upgradedServices['HttpClient'], - upgradedServices['UrlInterpolationService']); + upgradedServices['UrlInterpolationService'] + ); upgradedServices['ClassroomBackendApiService'] = new ClassroomBackendApiService( upgradedServices['UrlInterpolationService'], - upgradedServices['HttpClient']); - upgradedServices['CollectionCreationBackendService'] = ( - new CollectionCreationBackendService( - upgradedServices['HttpClient'])); + upgradedServices['HttpClient'] + ); + upgradedServices['CollectionCreationBackendService'] = + new CollectionCreationBackendService(upgradedServices['HttpClient']); upgradedServices['CollectionRightsBackendApiService'] = new CollectionRightsBackendApiService( upgradedServices['HttpClient'], - upgradedServices['UrlInterpolationService']); + upgradedServices['UrlInterpolationService'] + ); upgradedServices['ContributionOpportunitiesBackendApiService'] = new ContributionOpportunitiesBackendApiService( upgradedServices['UrlInterpolationService'], upgradedServices['HttpClient'], - upgradedServices['UserService']); + upgradedServices['UserService'] + ); upgradedServices['CreatorDashboardBackendApiService'] = new CreatorDashboardBackendApiService( upgradedServices['HttpClient'], upgradedServices['FeedbackThreadObjectFactory'], upgradedServices['SuggestionsService'], - upgradedServices['LoggerService']); + upgradedServices['LoggerService'] + ); upgradedServices['CurrentInteractionService'] = new CurrentInteractionService( upgradedServices['ContextService'], upgradedServices['PlayerPositionService'], - upgradedServices['PlayerTranscriptService']); + upgradedServices['PlayerTranscriptService'] + ); upgradedServices['ExplorationFeaturesBackendApiService'] = new ExplorationFeaturesBackendApiService( upgradedServices['HttpClient'], - upgradedServices['UrlInterpolationService']); + upgradedServices['UrlInterpolationService'] + ); upgradedServices['ExplorationHtmlFormatterService'] = new ExplorationHtmlFormatterService( upgradedServices['CamelCaseToHyphensPipe'], upgradedServices['ExtensionTagAssemblerService'], - upgradedServices['HtmlEscaperService']); + upgradedServices['HtmlEscaperService'] + ); upgradedServices['ExplorationImprovementsBackendApiService'] = new ExplorationImprovementsBackendApiService( upgradedServices['HttpClient'], - upgradedServices['UrlInterpolationService']); + upgradedServices['UrlInterpolationService'] + ); upgradedServices['ExplorationStatsBackendApiService'] = new ExplorationStatsBackendApiService( upgradedServices['HttpClient'], - upgradedServices['UrlInterpolationService']); + upgradedServices['UrlInterpolationService'] + ); upgradedServices['LearnerAnswerDetailsBackendApiService'] = - new LearnerAnswerDetailsBackendApiService( - upgradedServices['HttpClient'], - upgradedServices['UrlInterpolationService']); + new LearnerAnswerDetailsBackendApiService( + upgradedServices['HttpClient'], + upgradedServices['UrlInterpolationService'] + ); upgradedServices['LearnerDashboardBackendApiService'] = - new LearnerDashboardBackendApiService( - upgradedServices['HttpClient']); + new LearnerDashboardBackendApiService(upgradedServices['HttpClient']); upgradedServices['LearnerDashboardIdsBackendApiService'] = - new LearnerDashboardIdsBackendApiService( - upgradedServices['HttpClient']); + new LearnerDashboardIdsBackendApiService(upgradedServices['HttpClient']); upgradedServices['FeatureFlagBackendApiService'] = - new FeatureFlagBackendApiService( - upgradedServices['HttpClient'] - ); + new FeatureFlagBackendApiService(upgradedServices['HttpClient']); upgradedServices['PlatformParameterAdminBackendApiService'] = new PlatformParameterAdminBackendApiService( - upgradedServices['HttpClient']); + upgradedServices['HttpClient'] + ); upgradedServices['FeatureFlagDummyBackendApiService'] = - new FeatureFlagDummyBackendApiService( - upgradedServices['HttpClient']); + new FeatureFlagDummyBackendApiService(upgradedServices['HttpClient']); upgradedServices['PlayerPositionService'] = new PlayerPositionService( - upgradedServices['PlayerTranscriptService']); + upgradedServices['PlayerTranscriptService'] + ); upgradedServices['PlaythroughBackendApiService'] = new PlaythroughBackendApiService( upgradedServices['HttpClient'], - upgradedServices['UrlInterpolationService']); + upgradedServices['UrlInterpolationService'] + ); upgradedServices['PlaythroughIssuesBackendApiService'] = new PlaythroughIssuesBackendApiService( upgradedServices['HttpClient'], - upgradedServices['UrlInterpolationService']); + upgradedServices['UrlInterpolationService'] + ); upgradedServices['ProfilePageBackendApiService'] = new ProfilePageBackendApiService( upgradedServices['UrlInterpolationService'], upgradedServices['HttpClient'], upgradedServices['UrlService'], - upgradedServices['UserService']); + upgradedServices['UserService'] + ); upgradedServices['QuestionBackendApiService'] = new QuestionBackendApiService( upgradedServices['HttpClient'], upgradedServices['UrlInterpolationService'], - upgradedServices['QuestionObjectFactory']); + upgradedServices['QuestionObjectFactory'] + ); upgradedServices['ReadOnlyCollectionBackendApiService'] = new ReadOnlyCollectionBackendApiService( upgradedServices['HttpClient'], - upgradedServices['UrlInterpolationService']); + upgradedServices['UrlInterpolationService'] + ); upgradedServices['ReadOnlyTopicObjectFactory'] = new ReadOnlyTopicObjectFactory(); upgradedServices['ReviewTestBackendApiService'] = new ReviewTestBackendApiService( upgradedServices['HttpClient'], upgradedServices['UrlInterpolationService'], - upgradedServices['UrlService']); + upgradedServices['UrlService'] + ); upgradedServices['SearchExplorationsBackendApiService'] = new SearchExplorationsBackendApiService( upgradedServices['HttpClient'], - upgradedServices['UrlInterpolationService']); + upgradedServices['UrlInterpolationService'] + ); upgradedServices['SolutionVerificationService'] = new SolutionVerificationService( upgradedServices['InteractionRulesRegistryService'], upgradedServices['AnswerClassificationService'], - upgradedServices['StateEditorService']); + upgradedServices['StateEditorService'] + ); upgradedServices['SkillCreationBackendApiService'] = new SkillCreationBackendApiService( upgradedServices['HttpClient'], - upgradedServices['ImageLocalStorageService']); + upgradedServices['ImageLocalStorageService'] + ); upgradedServices['SkillMasteryBackendApiService'] = - new SkillMasteryBackendApiService( - upgradedServices['HttpClient']); - upgradedServices['SkillObjectFactory'] = - new SkillObjectFactory( - upgradedServices['MisconceptionObjectFactory'], - upgradedServices['ValidatorsService']); + new SkillMasteryBackendApiService(upgradedServices['HttpClient']); + upgradedServices['SkillObjectFactory'] = new SkillObjectFactory( + upgradedServices['MisconceptionObjectFactory'], + upgradedServices['ValidatorsService'] + ); upgradedServices['SkillRightsBackendApiService'] = new SkillRightsBackendApiService( upgradedServices['HttpClient'], - upgradedServices['UrlInterpolationService']); + upgradedServices['UrlInterpolationService'] + ); upgradedServices['StateClassifierMappingService'] = new StateClassifierMappingService( upgradedServices['AppService'], upgradedServices['ClassifierDataBackendApiService'], - upgradedServices['LoggerService']); + upgradedServices['LoggerService'] + ); upgradedServices['StateInteractionStatsBackendApiService'] = new StateInteractionStatsBackendApiService( upgradedServices['HttpClient'], - upgradedServices['UrlInterpolationService']); - upgradedServices['StateParamChangesService'] = - new StateParamChangesService( - upgradedServices['AlertsService'], - upgradedServices['UrlInterpolationService']); + upgradedServices['UrlInterpolationService'] + ); + upgradedServices['StateParamChangesService'] = new StateParamChangesService( + upgradedServices['AlertsService'], + upgradedServices['UrlInterpolationService'] + ); upgradedServices['StateTopAnswersStatsBackendApiService'] = new StateTopAnswersStatsBackendApiService( upgradedServices['HttpClient'], upgradedServices['StateTopAnswersStatsObjectFactory'], - upgradedServices['UrlInterpolationService']); + upgradedServices['UrlInterpolationService'] + ); upgradedServices['StatsReportingBackendApiService'] = new StatsReportingBackendApiService( upgradedServices['ContextService'], upgradedServices['HttpClient'], - upgradedServices['UrlInterpolationService']); + upgradedServices['UrlInterpolationService'] + ); upgradedServices['StoryViewerBackendApiService'] = - new StoryViewerBackendApiService( - upgradedServices['HttpClient'], - upgradedServices['UrlInterpolationService']); + new StoryViewerBackendApiService( + upgradedServices['HttpClient'], + upgradedServices['UrlInterpolationService'] + ); upgradedServices['TextInputPredictionService'] = new TextInputPredictionService( upgradedServices['CountVectorizerService'], upgradedServices['SVMPredictionService'], - upgradedServices['TextInputTokenizer']); + upgradedServices['TextInputTokenizer'] + ); upgradedServices['TopicCreationBackendApiService'] = new TopicCreationBackendApiService(upgradedServices['HttpClient']); upgradedServices['TopicsAndSkillsDashboardBackendApiService'] = new TopicsAndSkillsDashboardBackendApiService( upgradedServices['HttpClient'], - upgradedServices['UrlInterpolationService']); + upgradedServices['UrlInterpolationService'] + ); upgradedServices['TopicViewerBackendApiService'] = new TopicViewerBackendApiService( upgradedServices['HttpClient'], - upgradedServices['UrlInterpolationService']); - upgradedServices['UserBackendApiService'] = - new UserBackendApiService( - upgradedServices['HttpClient']); + upgradedServices['UrlInterpolationService'] + ); + upgradedServices['UserBackendApiService'] = new UserBackendApiService( + upgradedServices['HttpClient'] + ); upgradedServices['UserService'] = new UserService( upgradedServices['AssetsBackendApiService'], upgradedServices['ImageLocalStorageService'], @@ -1099,7 +1102,8 @@ export class UpgradedServices { upgradedServices['VoiceoverBackendApiService'] = new VoiceoverBackendApiService( upgradedServices['UrlInterpolationService'], - upgradedServices['HttpClient']); + upgradedServices['HttpClient'] + ); // Topological level: 4. upgradedServices['CollectionCreationService'] = @@ -1109,41 +1113,51 @@ export class UpgradedServices { upgradedServices['SiteAnalyticsService'], upgradedServices['UrlInterpolationService'], upgradedServices['LoaderService'], - upgradedServices['WindowRef']); + upgradedServices['WindowRef'] + ); upgradedServices['EditableCollectionBackendApiService'] = new EditableCollectionBackendApiService( upgradedServices['HttpClient'], upgradedServices['ReadOnlyCollectionBackendApiService'], - upgradedServices['UrlInterpolationService']); + upgradedServices['UrlInterpolationService'] + ); upgradedServices['ExplorationRecommendationsService'] = new ExplorationRecommendationsService( upgradedServices['ContextService'], upgradedServices['UrlService'], - upgradedServices['ExplorationRecommendationsBackendApiService']); + upgradedServices['ExplorationRecommendationsBackendApiService'] + ); upgradedServices['ExplorationStatsService'] = new ExplorationStatsService( - upgradedServices['ExplorationStatsBackendApiService']); + upgradedServices['ExplorationStatsBackendApiService'] + ); upgradedServices['ExtensionTagAssemblerService'] = new ExtensionTagAssemblerService( upgradedServices['HtmlEscaperService'], - upgradedServices['CamelCaseToHyphensPipe']); + upgradedServices['CamelCaseToHyphensPipe'] + ); upgradedServices['PlatformFeatureService'] = new PlatformFeatureService( upgradedServices['FeatureFlagBackendApiService'], upgradedServices['WindowRef'], upgradedServices['LoggerService'], - upgradedServices['UrlService']); + upgradedServices['UrlService'] + ); upgradedServices['PopulateRuleContentIdsService'] = new PopulateRuleContentIdsService( - upgradedServices['GenerateContentIdService']); + upgradedServices['GenerateContentIdService'] + ); upgradedServices['PredictionAlgorithmRegistryService'] = new PredictionAlgorithmRegistryService( - upgradedServices['TextInputPredictionService']); - upgradedServices['UserExplorationPermissionsService'] = ( + upgradedServices['TextInputPredictionService'] + ); + upgradedServices['UserExplorationPermissionsService'] = new UserExplorationPermissionsService( - upgradedServices['ExplorationPermissionsBackendApiService'])); + upgradedServices['ExplorationPermissionsBackendApiService'] + ); upgradedServices['SubtopicViewerBackendApiService'] = new SubtopicViewerBackendApiService( upgradedServices['HttpClient'], - upgradedServices['UrlInterpolationService']); + upgradedServices['UrlInterpolationService'] + ); // Topological level: 5. upgradedServices['AnswerClassificationService'] = @@ -1152,57 +1166,69 @@ export class UpgradedServices { upgradedServices['AppService'], upgradedServices['InteractionSpecsService'], upgradedServices['PredictionAlgorithmRegistryService'], - upgradedServices['StateClassifierMappingService']); + upgradedServices['StateClassifierMappingService'] + ); upgradedServices['AudioPreloaderService'] = new AudioPreloaderService( upgradedServices['AssetsBackendApiService'], upgradedServices['AudioTranslationLanguageService'], upgradedServices['ComputeGraphService'], - upgradedServices['ContextService']); + upgradedServices['ContextService'] + ); upgradedServices['ExplorationHtmlFormatterService'] = new ExplorationHtmlFormatterService( upgradedServices['CamelCaseToHyphensPipe'], upgradedServices['ExtensionTagAssemblerService'], - upgradedServices['HtmlEscaperService']); + upgradedServices['HtmlEscaperService'] + ); upgradedServices['SubtopicViewerBackendApiService'] = new SubtopicViewerBackendApiService( upgradedServices['HttpClient'], - upgradedServices['UrlInterpolationService']); + upgradedServices['UrlInterpolationService'] + ); // Topological level: 6. upgradedServices['SolutionObjectFactory'] = new SolutionObjectFactory( - upgradedServices['ExplorationHtmlFormatterService']); + upgradedServices['ExplorationHtmlFormatterService'] + ); upgradedServices['StateInteractionStatsService'] = new StateInteractionStatsService( upgradedServices['AnswerClassificationService'], upgradedServices['InteractionRulesRegistryService'], - upgradedServices['StateInteractionStatsBackendApiService']); + upgradedServices['StateInteractionStatsBackendApiService'] + ); upgradedServices['StateTopAnswersStatsService'] = new StateTopAnswersStatsService( upgradedServices['AnswerClassificationService'], upgradedServices['InteractionRulesRegistryService'], - upgradedServices['StateTopAnswersStatsBackendApiService']); + upgradedServices['StateTopAnswersStatsBackendApiService'] + ); // Topological level: 7. upgradedServices['InteractionObjectFactory'] = new InteractionObjectFactory( upgradedServices['AnswerGroupObjectFactory'], upgradedServices['SolutionObjectFactory'], upgradedServices['OutcomeObjectFactory'], - upgradedServices['SubtitledUnicodeObjectFactory']); + upgradedServices['SubtitledUnicodeObjectFactory'] + ); // Topological level: 8. upgradedServices['InteractionAttributesExtractorService'] = new InteractionAttributesExtractorService( upgradedServices['HtmlEscaperService'], - upgradedServices['InteractionObjectFactory']); + upgradedServices['InteractionObjectFactory'] + ); upgradedServices['StateObjectFactory'] = new StateObjectFactory( upgradedServices['InteractionObjectFactory'], - upgradedServices['ParamChangesObjectFactory']); + upgradedServices['ParamChangesObjectFactory'] + ); // Topological level: 9. upgradedServices['StatesObjectFactory'] = new StatesObjectFactory( - upgradedServices['StateObjectFactory']); + upgradedServices['StateObjectFactory'] + ); upgradedServices['QuestionObjectFactory'] = new QuestionObjectFactory( - upgradedServices['StateObjectFactory']); + upgradedServices['StateObjectFactory'] + ); // Topological level: 10. upgradedServices['ExplorationObjectFactory'] = new ExplorationObjectFactory( @@ -1210,18 +1236,20 @@ export class UpgradedServices { upgradedServices['ParamChangesObjectFactory'], upgradedServices['ParamSpecsObjectFactory'], upgradedServices['StatesObjectFactory'], - upgradedServices['UrlInterpolationService']); + upgradedServices['UrlInterpolationService'] + ); upgradedServices['PretestQuestionBackendApiService'] = new PretestQuestionBackendApiService( upgradedServices['UrlInterpolationService'], upgradedServices['HttpClient'], - upgradedServices['QuestionObjectFactory']); + upgradedServices['QuestionObjectFactory'] + ); /* eslint-enable dot-notation */ return upgradedServices; } } -angular.module('oppia').factory( - 'UpgradedServices', - downgradeInjectable(UpgradedServices)); +angular + .module('oppia') + .factory('UpgradedServices', downgradeInjectable(UpgradedServices)); diff --git a/core/templates/services/alerts.service.spec.ts b/core/templates/services/alerts.service.spec.ts index 11b3e3906896..6dd25975c522 100644 --- a/core/templates/services/alerts.service.spec.ts +++ b/core/templates/services/alerts.service.spec.ts @@ -15,16 +15,16 @@ /** * @fileoverview Unit tests for the Alerts Service. */ -import { TestBed } from '@angular/core/testing'; +import {TestBed} from '@angular/core/testing'; -import { AlertsService } from 'services/alerts.service'; +import {AlertsService} from 'services/alerts.service'; -describe('Alerts Service', function() { +describe('Alerts Service', function () { let alertsService: AlertsService; beforeEach(() => { TestBed.configureTestingModule({ - providers: [AlertsService] + providers: [AlertsService], }); alertsService = TestBed.inject(AlertsService); }); @@ -55,7 +55,7 @@ describe('Alerts Service', function() { expect(alertsService.warnings.length).toBe(3); alertsService.deleteWarning({ type: 'warning', - content: warning + content: warning, }); expect(alertsService.warnings.length).toBe(2); @@ -83,7 +83,7 @@ describe('Alerts Service', function() { expect(alertsService.warnings.length).toBe(4); alertsService.deleteWarning({ type: 'warning', - content: warning + content: warning, }); expect(alertsService.warnings.length).toBe(2); @@ -148,15 +148,17 @@ describe('Alerts Service', function() { alertsService.deleteMessage({ type: 'info', content: message, - timeout: 1 + timeout: 1, }); expect(alertsService.messages.length).toBe(2); // Search for the message. let found = false; for (let i = 0; i < alertsService.messages.length; i++) { - if (alertsService.messages[i].content === message && - alertsService.messages[i].type === 'info') { + if ( + alertsService.messages[i].content === message && + alertsService.messages[i].type === 'info' + ) { found = true; } } @@ -178,15 +180,17 @@ describe('Alerts Service', function() { alertsService.deleteMessage({ type: 'info', content: message, - timeout: 1 + timeout: 1, }); expect(alertsService.messages.length).toBe(2); // Search for the message. let found = false; for (let i = 0; i < alertsService.messages.length; i++) { - if (alertsService.messages[i].content === message && - alertsService.messages[i].type === 'info') { + if ( + alertsService.messages[i].content === message && + alertsService.messages[i].type === 'info' + ) { found = true; } } diff --git a/core/templates/services/alerts.service.ts b/core/templates/services/alerts.service.ts index 9328187935de..102716d5bd39 100644 --- a/core/templates/services/alerts.service.ts +++ b/core/templates/services/alerts.service.ts @@ -16,10 +16,10 @@ * @fileoverview Factory for handling warnings and info messages. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; -import { LoggerService } from 'services/contextual/logger.service'; +import {LoggerService} from 'services/contextual/logger.service'; export interface Warning { type: string; @@ -27,13 +27,13 @@ export interface Warning { } export interface Message { - type: string; - content: string; - timeout: number; + type: string; + content: string; + timeout: number; } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class AlertsService { /** @@ -79,7 +79,7 @@ export class AlertsService { } this.warnings.push({ type: 'warning', - content: warning + content: warning, }); } @@ -98,8 +98,9 @@ export class AlertsService { * @param {Object} warningToDelete - The warning message to be deleted. */ deleteWarning(warningToDelete: Warning): void { - const filteredWarnings = ( - this.warnings.filter(w => w.content !== warningToDelete.content)); + const filteredWarnings = this.warnings.filter( + w => w.content !== warningToDelete.content + ); this.warnings.splice(0, this.warnings.length, ...filteredWarnings); } @@ -123,7 +124,7 @@ export class AlertsService { this.messages.push({ type: type, content: message, - timeout: timeoutMilliseconds + timeout: timeoutMilliseconds, }); } @@ -132,8 +133,8 @@ export class AlertsService { * @param {Object} messageToDelete - Message to be deleted. */ deleteMessage(messageToDelete: Message): void { - const isMessageToKeep = (m: Message) => ( - m.type !== messageToDelete.type || m.content !== messageToDelete.content); + const isMessageToKeep = (m: Message) => + m.type !== messageToDelete.type || m.content !== messageToDelete.content; const filteredMessages = this.messages.filter(isMessageToKeep); this.messages.splice(0, this.messages.length, ...filteredMessages); } @@ -170,5 +171,6 @@ export class AlertsService { } } -angular.module('oppia').factory( - 'AlertsService', downgradeInjectable(AlertsService)); +angular + .module('oppia') + .factory('AlertsService', downgradeInjectable(AlertsService)); diff --git a/core/templates/services/app.service.spec.ts b/core/templates/services/app.service.spec.ts index 5b21d5ccff5a..4b52108244dd 100644 --- a/core/templates/services/app.service.spec.ts +++ b/core/templates/services/app.service.spec.ts @@ -16,10 +16,10 @@ * @fileoverview Unit tests for the app service. */ -import { TestBed } from '@angular/core/testing'; +import {TestBed} from '@angular/core/testing'; -import { AppConstants } from 'app.constants'; -import { AppService } from 'services/app.service'; +import {AppConstants} from 'app.constants'; +import {AppService} from 'services/app.service'; describe('App Service', () => { let appService: AppService; @@ -40,26 +40,24 @@ describe('App Service', () => { AppConstants.ENABLE_ML_CLASSIFIERS = initialValue; }); - it('should return true if AppConstants.ENABLE_ML_CLASSIFIERS is true', - () => { - // This throws "Cannot assign to 'ENABLE_ML_CLASSIFIERS' because it - // is a read-only property.". We need to suppress this error because - // we need to change the value of 'ENABLE_ML_CLASSIFIERS' for testing - // purposes. - // @ts-expect-error - AppConstants.ENABLE_ML_CLASSIFIERS = true; - expect(appService.isMachineLearningClassificationEnabled()).toBeTrue(); - }); + it('should return true if AppConstants.ENABLE_ML_CLASSIFIERS is true', () => { + // This throws "Cannot assign to 'ENABLE_ML_CLASSIFIERS' because it + // is a read-only property.". We need to suppress this error because + // we need to change the value of 'ENABLE_ML_CLASSIFIERS' for testing + // purposes. + // @ts-expect-error + AppConstants.ENABLE_ML_CLASSIFIERS = true; + expect(appService.isMachineLearningClassificationEnabled()).toBeTrue(); + }); - it('should return false if AppConstants.ENABLE_ML_CLASSIFIERS is false', - () => { - // This throws "Cannot assign to 'ENABLE_ML_CLASSIFIERS' because it - // is a read-only property.". We need to suppress this error because - // we need to change the value of 'ENABLE_ML_CLASSIFIERS' for testing - // purposes. - // @ts-expect-error - AppConstants.ENABLE_ML_CLASSIFIERS = false; - expect(appService.isMachineLearningClassificationEnabled()).toBeFalse(); - }); + it('should return false if AppConstants.ENABLE_ML_CLASSIFIERS is false', () => { + // This throws "Cannot assign to 'ENABLE_ML_CLASSIFIERS' because it + // is a read-only property.". We need to suppress this error because + // we need to change the value of 'ENABLE_ML_CLASSIFIERS' for testing + // purposes. + // @ts-expect-error + AppConstants.ENABLE_ML_CLASSIFIERS = false; + expect(appService.isMachineLearningClassificationEnabled()).toBeFalse(); + }); }); }); diff --git a/core/templates/services/app.service.ts b/core/templates/services/app.service.ts index 1af608b721ae..8136a0ab356c 100644 --- a/core/templates/services/app.service.ts +++ b/core/templates/services/app.service.ts @@ -16,10 +16,10 @@ * @fileoverview Service for querying the shared constants of the Oppia module. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { AppConstants } from 'app.constants'; +import {AppConstants} from 'app.constants'; @Injectable({providedIn: 'root'}) export class AppService { diff --git a/core/templates/services/assets-backend-api.service.spec.ts b/core/templates/services/assets-backend-api.service.spec.ts index 8be0e9398085..e7398defbd2d 100644 --- a/core/templates/services/assets-backend-api.service.spec.ts +++ b/core/templates/services/assets-backend-api.service.spec.ts @@ -16,14 +16,17 @@ * @fileoverview Unit tests for AssetsBackendApiService */ -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; - -import { AppConstants } from 'app.constants'; -import { AudioFile } from 'domain/utilities/audio-file.model'; -import { ImageFile } from 'domain/utilities/image-file.model'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; -import { CsrfTokenService } from 'services/csrf-token.service'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; + +import {AppConstants} from 'app.constants'; +import {AudioFile} from 'domain/utilities/audio-file.model'; +import {ImageFile} from 'domain/utilities/image-file.model'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; +import {CsrfTokenService} from 'services/csrf-token.service'; describe('Assets Backend API Service', () => { describe('on dev mode', () => { @@ -31,24 +34,25 @@ describe('Assets Backend API Service', () => { let csrfTokenService: CsrfTokenService; let httpTestingController: HttpTestingController; - const audioRequestUrl = ( - '/assetsdevhandler/exploration/0/assets/audio/myfile.mp3'); - const imageRequestUrl = ( - '/assetsdevhandler/exploration/0/assets/image/myfile.png'); + const audioRequestUrl = + '/assetsdevhandler/exploration/0/assets/audio/myfile.mp3'; + const imageRequestUrl = + '/assetsdevhandler/exploration/0/assets/image/myfile.png'; const audioBlob = new Blob(['audio data'], {type: 'audiotype'}); const imageBlob = new Blob(['image data'], {type: 'imagetype'}); beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] + imports: [HttpClientTestingModule], }); assetsBackendApiService = TestBed.inject(AssetsBackendApiService); csrfTokenService = TestBed.inject(CsrfTokenService); httpTestingController = TestBed.inject(HttpTestingController); - spyOn(csrfTokenService, 'getTokenAsync') - .and.returnValue(Promise.resolve('token')); + spyOn(csrfTokenService, 'getTokenAsync').and.returnValue( + Promise.resolve('token') + ); }); afterEach(() => { @@ -58,24 +62,34 @@ describe('Assets Backend API Service', () => { it('should correctly formulate the download URL for audio', () => { expect( assetsBackendApiService.getAudioDownloadUrl( - AppConstants.ENTITY_TYPE.EXPLORATION, 'expid12345', 'a.mp3') + AppConstants.ENTITY_TYPE.EXPLORATION, + 'expid12345', + 'a.mp3' + ) ).toEqual('/assetsdevhandler/exploration/expid12345/assets/audio/a.mp3'); }); it('should correctly formulate the preview URL for images', () => { expect( assetsBackendApiService.getImageUrlForPreview( - AppConstants.ENTITY_TYPE.EXPLORATION, 'expid12345', 'a.png') + AppConstants.ENTITY_TYPE.EXPLORATION, + 'expid12345', + 'a.png' + ) ).toEqual('/assetsdevhandler/exploration/expid12345/assets/image/a.png'); }); it('should correctly formulate the thumbnail url for preview', () => { expect( assetsBackendApiService.getThumbnailUrlForPreview( - AppConstants.ENTITY_TYPE.EXPLORATION, 'expid12345', 'thumbnail.png') + AppConstants.ENTITY_TYPE.EXPLORATION, + 'expid12345', + 'thumbnail.png' + ) ).toEqual( '/assetsdevhandler/exploration/expid12345/assets/' + - 'thumbnail/thumbnail.png'); + 'thumbnail/thumbnail.png' + ); }); it('should successfully fetch and cache audio', fakeAsync(() => { @@ -84,19 +98,21 @@ describe('Assets Backend API Service', () => { expect(assetsBackendApiService.isCached('myfile.mp3')).toBeFalse(); - - assetsBackendApiService.loadAudio('0', 'myfile.mp3').then( - successHandler, failHandler); + assetsBackendApiService + .loadAudio('0', 'myfile.mp3') + .then(successHandler, failHandler); const req = httpTestingController.expectOne(audioRequestUrl); expect(req.request.method).toEqual('GET'); expect( - assetsBackendApiService.getAssetsFilesCurrentlyBeingRequested() - .audio.length).toEqual(1); + assetsBackendApiService.getAssetsFilesCurrentlyBeingRequested().audio + .length + ).toEqual(1); req.flush(audioBlob); flushMicrotasks(); expect( - assetsBackendApiService.getAssetsFilesCurrentlyBeingRequested() - .audio.length).toEqual(0); + assetsBackendApiService.getAssetsFilesCurrentlyBeingRequested().audio + .length + ).toEqual(0); expect(assetsBackendApiService.isCached('myfile.mp3')).toBeTrue(); expect(successHandler).toHaveBeenCalled(); expect(failHandler).not.toHaveBeenCalled(); @@ -107,8 +123,9 @@ describe('Assets Backend API Service', () => { const failHandler = jasmine.createSpy('fail'); expect(assetsBackendApiService.isCached('myfile.mp3')).toBeFalse(); - assetsBackendApiService.loadAudio('0', 'myfile.mp3').then( - successHandler, failHandler); + assetsBackendApiService + .loadAudio('0', 'myfile.mp3') + .then(successHandler, failHandler); const req = httpTestingController.expectOne(audioRequestUrl); expect(req.request.method).toEqual('GET'); req.flush(audioBlob); @@ -118,8 +135,9 @@ describe('Assets Backend API Service', () => { expect(successHandler).toHaveBeenCalled(); expect(failHandler).not.toHaveBeenCalled(); - assetsBackendApiService.loadAudio('0', 'myfile.mp3').then( - (cachedFile: AudioFile) => { + assetsBackendApiService + .loadAudio('0', 'myfile.mp3') + .then((cachedFile: AudioFile) => { expect(cachedFile).toEqual(new AudioFile('myfile.mp3', audioBlob)); }); })); @@ -130,8 +148,9 @@ describe('Assets Backend API Service', () => { expect(assetsBackendApiService.isCached('myfile.mp3')).toBeFalse(); - assetsBackendApiService.loadAudio('0', 'myfile.mp3').then( - successHandler, failHandler); + assetsBackendApiService + .loadAudio('0', 'myfile.mp3') + .then(successHandler, failHandler); const req = httpTestingController.expectOne(audioRequestUrl); expect(req.request.method).toEqual('GET'); req.flush(audioBlob, {status: 400, statusText: 'Failed'}); @@ -146,11 +165,13 @@ describe('Assets Backend API Service', () => { const onSuccess = jasmine.createSpy('onSuccess'); const onFailure = jasmine.createSpy('onFailure'); - assetsBackendApiService.saveAudio('0', 'a.mp3', new File([], 'a.mp3')) + assetsBackendApiService + .saveAudio('0', 'a.mp3', new File([], 'a.mp3')) .then(onSuccess, onFailure); flushMicrotasks(); - httpTestingController.expectOne('/createhandler/audioupload/0') + httpTestingController + .expectOne('/createhandler/audioupload/0') .flush(successMessage); flushMicrotasks(); @@ -163,14 +184,19 @@ describe('Assets Backend API Service', () => { const onSuccess = jasmine.createSpy('onSuccess'); const onFailure = jasmine.createSpy('onFailure'); - assetsBackendApiService.saveMathExpressionImage( - imageBlob, 'newMathExpression.svg', 'exploration', 'expid12345') + assetsBackendApiService + .saveMathExpressionImage( + imageBlob, + 'newMathExpression.svg', + 'exploration', + 'expid12345' + ) .then(onSuccess, onFailure); flushMicrotasks(); - httpTestingController.expectOne( - '/createhandler/imageupload/exploration/expid12345' - ).flush(successMessage); + httpTestingController + .expectOne('/createhandler/imageupload/exploration/expid12345') + .flush(successMessage); flushMicrotasks(); expect(onSuccess).toHaveBeenCalledWith(successMessage); @@ -182,17 +208,19 @@ describe('Assets Backend API Service', () => { const onSuccess = jasmine.createSpy('onSuccess'); const onFailure = jasmine.createSpy('onFailure'); - assetsBackendApiService.postThumbnailFile( - new Blob(['abc']), - 'filename.svg', - 'entity_type', - 'entity_id' - ).subscribe(onSuccess, onFailure); + assetsBackendApiService + .postThumbnailFile( + new Blob(['abc']), + 'filename.svg', + 'entity_type', + 'entity_id' + ) + .subscribe(onSuccess, onFailure); flushMicrotasks(); - httpTestingController.expectOne( - '/createhandler/imageupload/entity_type/entity_id' - ).flush(successMessage); + httpTestingController + .expectOne('/createhandler/imageupload/entity_type/entity_id') + .flush(successMessage); expect(onSuccess).toHaveBeenCalledWith(successMessage); expect(onFailure).not.toHaveBeenCalled(); @@ -202,86 +230,95 @@ describe('Assets Backend API Service', () => { const onSuccess = jasmine.createSpy('onSuccess'); const onFailure = jasmine.createSpy('onFailure'); - assetsBackendApiService.saveMathExpressionImage( - imageBlob, 'new.svg', 'exploration', 'expid12345') + assetsBackendApiService + .saveMathExpressionImage( + imageBlob, + 'new.svg', + 'exploration', + 'expid12345' + ) .then(onSuccess, onFailure); flushMicrotasks(); - httpTestingController.expectOne( - '/createhandler/imageupload/exploration/expid12345' - ).flush(null, {status: 400, statusText: 'Failure'}); + httpTestingController + .expectOne('/createhandler/imageupload/exploration/expid12345') + .flush(null, {status: 400, statusText: 'Failure'}); flushMicrotasks(); expect(onSuccess).not.toHaveBeenCalled(); expect(onFailure).toHaveBeenCalled(); })); - it('should handle rejection when saving a math SVG fails with non HTTP err', - fakeAsync(() => { - const onSuccess = jasmine.createSpy('onSuccess'); - const onFailure = jasmine.createSpy('onFailure'); - - spyOn( - // This throws "Argument of type 'getImageUploadUrl' is not assignable - // to parameter of type 'keyof AssetsBackendApiService'. We need to - // suppress this error because of strict type checking. This is - // because the type of getImageUploadUrl is string and not a - // function. - // @ts-ignore - assetsBackendApiService, 'getImageUploadUrl' - ).and.throwError(Error('token')); - - assetsBackendApiService.saveMathExpressionImage( - imageBlob, 'new.svg', 'exploration', 'expid12345' - ).then(onSuccess, onFailure); - flushMicrotasks(); - - expect(onSuccess).not.toHaveBeenCalled(); - expect(onFailure).toHaveBeenCalled(); - }) - ); + it('should handle rejection when saving a math SVG fails with non HTTP err', fakeAsync(() => { + const onSuccess = jasmine.createSpy('onSuccess'); + const onFailure = jasmine.createSpy('onFailure'); + + spyOn( + assetsBackendApiService, + // This throws "Argument of type 'getImageUploadUrl' is not assignable + // to parameter of type 'keyof AssetsBackendApiService'. We need to + // suppress this error because of strict type checking. This is + // because the type of getImageUploadUrl is string and not a + // function. + // @ts-ignore + 'getImageUploadUrl' + ).and.throwError(Error('token')); + + assetsBackendApiService + .saveMathExpressionImage( + imageBlob, + 'new.svg', + 'exploration', + 'expid12345' + ) + .then(onSuccess, onFailure); + flushMicrotasks(); + + expect(onSuccess).not.toHaveBeenCalled(); + expect(onFailure).toHaveBeenCalled(); + })); it('should handle rejection when saving a file fails', fakeAsync(() => { const onSuccess = jasmine.createSpy('onSuccess'); const onFailure = jasmine.createSpy('onFailure'); - assetsBackendApiService.saveAudio('0', 'a.mp3', audioBlob) + assetsBackendApiService + .saveAudio('0', 'a.mp3', audioBlob) .then(onSuccess, onFailure); flushMicrotasks(); - httpTestingController.expectOne( - '/createhandler/audioupload/0' - ).flush(null, {status: 400, statusText: 'Failure'}); + httpTestingController + .expectOne('/createhandler/audioupload/0') + .flush(null, {status: 400, statusText: 'Failure'}); flushMicrotasks(); expect(onSuccess).not.toHaveBeenCalled(); expect(onFailure).toHaveBeenCalled(); })); - it('should handle rejection when saving a file fails with non HTTP error', - fakeAsync(() => { - const onSuccess = jasmine.createSpy('onSuccess'); - const onFailure = jasmine.createSpy('onFailure'); - - spyOn( - // This throws "Argument of type 'getImageUploadUrl' is not assignable - // to parameter of type 'keyof AssetsBackendApiService'. We need to - // suppress this error because of strict type checking. This is - // because the type of getImageUploadUrl is string and not a - // function. - // @ts-ignore - assetsBackendApiService, 'getAudioUploadUrl' - ).and.throwError(Error('token')); - - assetsBackendApiService.saveAudio( - '0', 'a.mp3', audioBlob - ).then(onSuccess, onFailure); - flushMicrotasks(); - - expect(onSuccess).not.toHaveBeenCalled(); - expect(onFailure).toHaveBeenCalled(); - }) - ); + it('should handle rejection when saving a file fails with non HTTP error', fakeAsync(() => { + const onSuccess = jasmine.createSpy('onSuccess'); + const onFailure = jasmine.createSpy('onFailure'); + + spyOn( + assetsBackendApiService, + // This throws "Argument of type 'getImageUploadUrl' is not assignable + // to parameter of type 'keyof AssetsBackendApiService'. We need to + // suppress this error because of strict type checking. This is + // because the type of getImageUploadUrl is string and not a + // function. + // @ts-ignore + 'getAudioUploadUrl' + ).and.throwError(Error('token')); + + assetsBackendApiService + .saveAudio('0', 'a.mp3', audioBlob) + .then(onSuccess, onFailure); + flushMicrotasks(); + + expect(onSuccess).not.toHaveBeenCalled(); + expect(onFailure).toHaveBeenCalled(); + })); it('should successfully fetch and cache image', fakeAsync(() => { const successHandler = jasmine.createSpy('success'); @@ -289,20 +326,22 @@ describe('Assets Backend API Service', () => { expect(assetsBackendApiService.isCached('myfile.png')).toBeFalse(); - assetsBackendApiService.loadImage( - AppConstants.ENTITY_TYPE.EXPLORATION, '0', 'myfile.png').then( - successHandler, failHandler); + assetsBackendApiService + .loadImage(AppConstants.ENTITY_TYPE.EXPLORATION, '0', 'myfile.png') + .then(successHandler, failHandler); const req = httpTestingController.expectOne(imageRequestUrl); expect(req.request.method).toEqual('GET'); expect( - assetsBackendApiService.getAssetsFilesCurrentlyBeingRequested() - .image.length).toEqual(1); + assetsBackendApiService.getAssetsFilesCurrentlyBeingRequested().image + .length + ).toEqual(1); req.flush(imageBlob); flushMicrotasks(); expect( - assetsBackendApiService.getAssetsFilesCurrentlyBeingRequested() - .image.length).toEqual(0); + assetsBackendApiService.getAssetsFilesCurrentlyBeingRequested().image + .length + ).toEqual(0); expect(assetsBackendApiService.isCached('myfile.png')).toBeTrue(); expect(successHandler).toHaveBeenCalled(); expect(failHandler).not.toHaveBeenCalled(); @@ -314,9 +353,9 @@ describe('Assets Backend API Service', () => { expect(assetsBackendApiService.isCached('myfile.png')).toBeFalse(); - assetsBackendApiService.loadImage( - AppConstants.ENTITY_TYPE.EXPLORATION, '0', 'myfile.png').then( - successHandler, failHandler); + assetsBackendApiService + .loadImage(AppConstants.ENTITY_TYPE.EXPLORATION, '0', 'myfile.png') + .then(successHandler, failHandler); let req = httpTestingController.expectOne(imageRequestUrl); expect(req.request.method).toEqual('GET'); @@ -326,111 +365,117 @@ describe('Assets Backend API Service', () => { expect(successHandler).toHaveBeenCalled(); expect(failHandler).not.toHaveBeenCalled(); - assetsBackendApiService.loadImage( - AppConstants.ENTITY_TYPE.EXPLORATION, '0', 'myfile.png').then( - (cachedFile: ImageFile) => { + assetsBackendApiService + .loadImage(AppConstants.ENTITY_TYPE.EXPLORATION, '0', 'myfile.png') + .then((cachedFile: ImageFile) => { expect(cachedFile).toEqual(new ImageFile('myfile.png', new Blob())); }); })); - it('should call the provided failure handler on HTTP failure for an audio', - fakeAsync(() => { - let successHandler = jasmine.createSpy('success'); - let failHandler = jasmine.createSpy('fail'); - - assetsBackendApiService.loadAudio('0', 'myfile.mp3').then( - successHandler, failHandler); - - let req = httpTestingController.expectOne(audioRequestUrl); - expect(req.request.method).toEqual('GET'); - req.flush(audioBlob, {status: 400, statusText: 'Failure'}); - - flushMicrotasks(); - expect(successHandler).not.toHaveBeenCalled(); - expect(failHandler).toHaveBeenCalled(); - })); - - it('should call the provided failure handler on HTTP failure for an image', - fakeAsync(() => { - const successHandler = jasmine.createSpy('success'); - const failHandler = jasmine.createSpy('fail'); - - assetsBackendApiService.loadImage( - AppConstants.ENTITY_TYPE.EXPLORATION, '0', 'myfile.png').then( - successHandler, failHandler); - - let req = httpTestingController.expectOne(imageRequestUrl); - expect(req.request.method).toEqual('GET'); - req.flush(audioBlob, {status: 400, statusText: 'Failure'}); - - flushMicrotasks(); - expect(successHandler).not.toHaveBeenCalled(); - expect(failHandler).toHaveBeenCalled(); - })); - - it('should successfully abort the download of all the audio files', - fakeAsync(() => { - const successHandler = jasmine.createSpy('success'); - const failHandler = jasmine.createSpy('fail'); - - assetsBackendApiService.loadAudio('0', 'myfile.mp3').then( - successHandler, failHandler); - let req = httpTestingController.expectOne(audioRequestUrl); - expect(req.request.method).toEqual('GET'); - expect( - assetsBackendApiService.getAssetsFilesCurrentlyBeingRequested() - .audio.length).toEqual(1); - - assetsBackendApiService.abortAllCurrentAudioDownloads(); - expect( - assetsBackendApiService.getAssetsFilesCurrentlyBeingRequested() - .audio.length).toEqual(0); - expect(assetsBackendApiService.isCached('myfile.mp3')).toBeFalse(); - })); - - it('should successfully abort the download of the all the image files', - fakeAsync(() => { - const successHandler = jasmine.createSpy('success'); - const failHandler = jasmine.createSpy('fail'); - - assetsBackendApiService.loadImage( - AppConstants.ENTITY_TYPE.EXPLORATION, '0', 'myfile.png').then( - successHandler, failHandler); - let req = httpTestingController.expectOne(imageRequestUrl); - expect(req.request.method).toEqual('GET'); - expect( - assetsBackendApiService.getAssetsFilesCurrentlyBeingRequested() - .image.length).toEqual(1); - - assetsBackendApiService.abortAllCurrentImageDownloads(); - expect( - assetsBackendApiService.getAssetsFilesCurrentlyBeingRequested() - .image.length).toEqual(0); - expect(assetsBackendApiService.isCached('myfile.png')).toBeFalse(); - })); + it('should call the provided failure handler on HTTP failure for an audio', fakeAsync(() => { + let successHandler = jasmine.createSpy('success'); + let failHandler = jasmine.createSpy('fail'); + + assetsBackendApiService + .loadAudio('0', 'myfile.mp3') + .then(successHandler, failHandler); + + let req = httpTestingController.expectOne(audioRequestUrl); + expect(req.request.method).toEqual('GET'); + req.flush(audioBlob, {status: 400, statusText: 'Failure'}); + + flushMicrotasks(); + expect(successHandler).not.toHaveBeenCalled(); + expect(failHandler).toHaveBeenCalled(); + })); + + it('should call the provided failure handler on HTTP failure for an image', fakeAsync(() => { + const successHandler = jasmine.createSpy('success'); + const failHandler = jasmine.createSpy('fail'); + + assetsBackendApiService + .loadImage(AppConstants.ENTITY_TYPE.EXPLORATION, '0', 'myfile.png') + .then(successHandler, failHandler); + + let req = httpTestingController.expectOne(imageRequestUrl); + expect(req.request.method).toEqual('GET'); + req.flush(audioBlob, {status: 400, statusText: 'Failure'}); + + flushMicrotasks(); + expect(successHandler).not.toHaveBeenCalled(); + expect(failHandler).toHaveBeenCalled(); + })); + + it('should successfully abort the download of all the audio files', fakeAsync(() => { + const successHandler = jasmine.createSpy('success'); + const failHandler = jasmine.createSpy('fail'); + + assetsBackendApiService + .loadAudio('0', 'myfile.mp3') + .then(successHandler, failHandler); + let req = httpTestingController.expectOne(audioRequestUrl); + expect(req.request.method).toEqual('GET'); + expect( + assetsBackendApiService.getAssetsFilesCurrentlyBeingRequested().audio + .length + ).toEqual(1); + + assetsBackendApiService.abortAllCurrentAudioDownloads(); + expect( + assetsBackendApiService.getAssetsFilesCurrentlyBeingRequested().audio + .length + ).toEqual(0); + expect(assetsBackendApiService.isCached('myfile.mp3')).toBeFalse(); + })); + + it('should successfully abort the download of the all the image files', fakeAsync(() => { + const successHandler = jasmine.createSpy('success'); + const failHandler = jasmine.createSpy('fail'); + + assetsBackendApiService + .loadImage(AppConstants.ENTITY_TYPE.EXPLORATION, '0', 'myfile.png') + .then(successHandler, failHandler); + let req = httpTestingController.expectOne(imageRequestUrl); + expect(req.request.method).toEqual('GET'); + expect( + assetsBackendApiService.getAssetsFilesCurrentlyBeingRequested().image + .length + ).toEqual(1); + + assetsBackendApiService.abortAllCurrentImageDownloads(); + expect( + assetsBackendApiService.getAssetsFilesCurrentlyBeingRequested().image + .length + ).toEqual(0); + expect(assetsBackendApiService.isCached('myfile.png')).toBeFalse(); + })); it('should use the correct blob type for audio assets', fakeAsync(() => { const successHandler = jasmine.createSpy('success'); const failHandler = jasmine.createSpy('fail'); - assetsBackendApiService.loadAudio('0', 'myfile.mp3').then( - successHandler, failHandler); + assetsBackendApiService + .loadAudio('0', 'myfile.mp3') + .then(successHandler, failHandler); let req = httpTestingController.expectOne(audioRequestUrl); expect(req.request.method).toEqual('GET'); expect( - assetsBackendApiService.getAssetsFilesCurrentlyBeingRequested() - .audio.length).toEqual(1); + assetsBackendApiService.getAssetsFilesCurrentlyBeingRequested().audio + .length + ).toEqual(1); req.flush(audioBlob); flushMicrotasks(); expect( - assetsBackendApiService.getAssetsFilesCurrentlyBeingRequested() - .audio.length).toEqual(0); + assetsBackendApiService.getAssetsFilesCurrentlyBeingRequested().audio + .length + ).toEqual(0); expect(successHandler).toHaveBeenCalled(); expect(failHandler).not.toHaveBeenCalled(); - expect(successHandler.calls.first().args[0].data.type) - .toEqual('audiotype'); + expect(successHandler.calls.first().args[0].data.type).toEqual( + 'audiotype' + ); })); }); @@ -441,11 +486,14 @@ describe('Assets Backend API Service', () => { 'https://storage.googleapis.com/app_default_bucket'; beforeEach(() => { - spyOnProperty(AssetsBackendApiService, 'EMULATOR_MODE', 'get') - .and.returnValue(false); + spyOnProperty( + AssetsBackendApiService, + 'EMULATOR_MODE', + 'get' + ).and.returnValue(false); TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [AssetsBackendApiService] + providers: [AssetsBackendApiService], }); httpTestingController = TestBed.inject(HttpTestingController); assetsBackendApiService = TestBed.inject(AssetsBackendApiService); @@ -454,24 +502,33 @@ describe('Assets Backend API Service', () => { it('should correctly formulate the download URL for audios', () => { expect( assetsBackendApiService.getAudioDownloadUrl( - AppConstants.ENTITY_TYPE.EXPLORATION, 'expid12345', 'a.mp3') - ).toEqual( - gcsPrefix + '/exploration/expid12345/assets/audio/a.mp3'); + AppConstants.ENTITY_TYPE.EXPLORATION, + 'expid12345', + 'a.mp3' + ) + ).toEqual(gcsPrefix + '/exploration/expid12345/assets/audio/a.mp3'); }); it('should correctly formulate the preview URL for images', () => { expect( assetsBackendApiService.getImageUrlForPreview( - AppConstants.ENTITY_TYPE.EXPLORATION, 'expid12345', 'a.png') + AppConstants.ENTITY_TYPE.EXPLORATION, + 'expid12345', + 'a.png' + ) ).toEqual(gcsPrefix + '/exploration/expid12345/assets/image/a.png'); }); it('should correctly formulate the thumbnail url for preview', () => { expect( assetsBackendApiService.getThumbnailUrlForPreview( - AppConstants.ENTITY_TYPE.EXPLORATION, 'expid12345', 'thumbnail.png') + AppConstants.ENTITY_TYPE.EXPLORATION, + 'expid12345', + 'thumbnail.png' + ) ).toEqual( - gcsPrefix + '/exploration/expid12345/assets/thumbnail/thumbnail.png'); + gcsPrefix + '/exploration/expid12345/assets/thumbnail/thumbnail.png' + ); }); afterEach(() => { diff --git a/core/templates/services/assets-backend-api.service.ts b/core/templates/services/assets-backend-api.service.ts index 59ea385631ab..83c09342c219 100644 --- a/core/templates/services/assets-backend-api.service.ts +++ b/core/templates/services/assets-backend-api.service.ts @@ -17,29 +17,29 @@ * assets from Google Cloud Storage. */ -import { HttpClient, HttpErrorResponse } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; - -import { AppConstants } from 'app.constants'; -import { AudioFile } from 'domain/utilities/audio-file.model'; -import { FileDownloadRequest } from 'domain/utilities/file-download-request.model'; -import { ImageFile } from 'domain/utilities/image-file.model'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; -import { Observable } from 'rxjs'; -import { CsrfTokenService } from 'services/csrf-token.service'; +import {HttpClient, HttpErrorResponse} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; + +import {AppConstants} from 'app.constants'; +import {AudioFile} from 'domain/utilities/audio-file.model'; +import {FileDownloadRequest} from 'domain/utilities/file-download-request.model'; +import {ImageFile} from 'domain/utilities/image-file.model'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; +import {Observable} from 'rxjs'; +import {CsrfTokenService} from 'services/csrf-token.service'; interface SaveAudioResponse { - 'filename': string; - 'duration_secs': number; + filename: string; + duration_secs: number; } interface SaveImageResponse { - 'filename': string; + filename: string; } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class AssetsBackendApiService { public readonly profileImagePngUrlTemplate: string; @@ -54,22 +54,22 @@ export class AssetsBackendApiService { private assetsCache: Map = new Map(); constructor( - private csrfTokenService: CsrfTokenService, - private http: HttpClient, - private urlInterpolationService: UrlInterpolationService) { + private csrfTokenService: CsrfTokenService, + private http: HttpClient, + private urlInterpolationService: UrlInterpolationService + ) { let urlPrefix = '/assetsdevhandler'; if (!AssetsBackendApiService.EMULATOR_MODE) { - urlPrefix = ( + urlPrefix = 'https://storage.googleapis.com/' + - AssetsBackendApiService.GCS_RESOURCE_BUCKET_NAME - ); + AssetsBackendApiService.GCS_RESOURCE_BUCKET_NAME; } - this.downloadUrlTemplate = ( - urlPrefix + '///assets//'); - this.profileImagePngUrlTemplate = ( - urlPrefix + '/user//assets/profile_picture.png'); - this.profileImageWebpUrlTemplate = ( - urlPrefix + '/user//assets/profile_picture.webp'); + this.downloadUrlTemplate = + urlPrefix + '///assets//'; + this.profileImagePngUrlTemplate = + urlPrefix + '/user//assets/profile_picture.png'; + this.profileImageWebpUrlTemplate = + urlPrefix + '/user//assets/profile_picture.webp'; } static get EMULATOR_MODE(): boolean { @@ -86,35 +86,47 @@ export class AssetsBackendApiService { return new AudioFile(filename, data); } return this.fetchFile( - AppConstants.ENTITY_TYPE.EXPLORATION, explorationId, filename, - AppConstants.ASSET_TYPE_AUDIO); + AppConstants.ENTITY_TYPE.EXPLORATION, + explorationId, + filename, + AppConstants.ASSET_TYPE_AUDIO + ); } async loadImage( - entityType: string, entityId: string, - filename: string): Promise { + entityType: string, + entityId: string, + filename: string + ): Promise { let data = this.assetsCache.get(filename); if (this.isCached(filename) && data !== undefined) { return new ImageFile(filename, data); } return this.fetchFile( - entityType, entityId, filename, AppConstants.ASSET_TYPE_IMAGE); + entityType, + entityId, + filename, + AppConstants.ASSET_TYPE_IMAGE + ); } async saveAudio( - explorationId: string, filename: string, - rawAssetData: Blob): Promise { + explorationId: string, + filename: string, + rawAssetData: Blob + ): Promise { const form = new FormData(); form.append('raw_audio_file', rawAssetData); form.append('payload', JSON.stringify({filename})); form.append('csrf_token', await this.csrfTokenService.getTokenAsync()); try { - return await this.http.post( - this.getAudioUploadUrl(explorationId), form).toPromise(); - // We use unknown type because we are unsure of the type of error - // that was thrown. Since the catch block cannot identify the - // specific type of error, we are unable to further optimise the - // code by introducing more types of errors. + return await this.http + .post(this.getAudioUploadUrl(explorationId), form) + .toPromise(); + // We use unknown type because we are unsure of the type of error + // that was thrown. Since the catch block cannot identify the + // specific type of error, we are unable to further optimise the + // code by introducing more types of errors. } catch (error: unknown) { if (error instanceof HttpErrorResponse) { return Promise.reject(error.error); @@ -124,20 +136,29 @@ export class AssetsBackendApiService { } async saveMathExpressionImage( - resampledFile: Blob, filename: string, entityType: string, - entityId: string): Promise { + resampledFile: Blob, + filename: string, + entityType: string, + entityId: string + ): Promise { const form = new FormData(); form.append('image', resampledFile); form.append( - 'payload', JSON.stringify({filename, filename_prefix: 'image'})); + 'payload', + JSON.stringify({filename, filename_prefix: 'image'}) + ); form.append('csrf_token', await this.csrfTokenService.getTokenAsync()); try { - return await this.http.post( - this.getImageUploadUrl(entityType, entityId), form).toPromise(); - // We use unknown type because we are unsure of the type of error - // that was thrown. Since the catch block cannot identify the - // specific type of error, we are unable to further optimise the - // code by introducing more types of errors. + return await this.http + .post( + this.getImageUploadUrl(entityType, entityId), + form + ) + .toPromise(); + // We use unknown type because we are unsure of the type of error + // that was thrown. Since the catch block cannot identify the + // specific type of error, we are unable to further optimise the + // code by introducing more types of errors. } catch (error: unknown) { if (error instanceof HttpErrorResponse) { return Promise.reject(error.error); @@ -147,21 +168,29 @@ export class AssetsBackendApiService { } postThumbnailFile( - resampledFile: Blob, filename: string, - entityType: string, entityId: string): Observable<{filename: string}> { + resampledFile: Blob, + filename: string, + entityType: string, + entityId: string + ): Observable<{filename: string}> { let form = new FormData(); form.append('image', resampledFile); - form.append('payload', JSON.stringify({ - filename: filename, - filename_prefix: 'thumbnail' - })); - let imageUploadUrlTemplate = '/createhandler/imageupload/' + - '/'; + form.append( + 'payload', + JSON.stringify({ + filename: filename, + filename_prefix: 'thumbnail', + }) + ); + let imageUploadUrlTemplate = + '/createhandler/imageupload/' + '/'; let thumbnailFileUrl = this.urlInterpolationService.interpolateUrl( - imageUploadUrlTemplate, { + imageUploadUrlTemplate, + { entity_type: entityType, - entity_id: entityId - }); + entity_id: entityId, + } + ); return this.http.post<{filename: string}>(thumbnailFileUrl, form); } @@ -177,8 +206,9 @@ export class AssetsBackendApiService { this.abortAllCurrentDownloads(AppConstants.ASSET_TYPE_IMAGE); } - getAssetsFilesCurrentlyBeingRequested(): ( - {[assetType: string]: readonly FileDownloadRequest[]}) { + getAssetsFilesCurrentlyBeingRequested(): { + [assetType: string]: readonly FileDownloadRequest[]; + } { return { [AppConstants.ASSET_TYPE_AUDIO]: this.audioFileDownloadRequests, [AppConstants.ASSET_TYPE_IMAGE]: this.imageFileDownloadRequests, @@ -186,38 +216,65 @@ export class AssetsBackendApiService { } getAudioDownloadUrl( - entityType: string, entityId: string, filename: string): string { + entityType: string, + entityId: string, + filename: string + ): string { return this.getDownloadUrl( - entityType, entityId, filename, AppConstants.ASSET_TYPE_AUDIO); + entityType, + entityId, + filename, + AppConstants.ASSET_TYPE_AUDIO + ); } getImageUrlForPreview( - entityType: string, entityId: string, filename: string): string { + entityType: string, + entityId: string, + filename: string + ): string { return this.getDownloadUrl( - entityType, entityId, filename, AppConstants.ASSET_TYPE_IMAGE); + entityType, + entityId, + filename, + AppConstants.ASSET_TYPE_IMAGE + ); } getThumbnailUrlForPreview( - entityType: string, entityId: string, filename: string): string { + entityType: string, + entityId: string, + filename: string + ): string { return this.getDownloadUrl( - entityType, entityId, filename, AppConstants.ASSET_TYPE_THUMBNAIL); + entityType, + entityId, + filename, + AppConstants.ASSET_TYPE_THUMBNAIL + ); } private getDownloadUrl( - entityType: string, entityId: string, filename: string, - assetType: string): string { + entityType: string, + entityId: string, + filename: string, + assetType: string + ): string { let downloadUrl = this.urlInterpolationService.interpolateUrl( - this.downloadUrlTemplate, { + this.downloadUrlTemplate, + { entity_type: entityType, entity_id: entityId, asset_type: assetType, filename: filename, - }); + } + ); return downloadUrl; } private getFileDownloadRequestsByAssetType( - assetType: string): FileDownloadRequest[] { + assetType: string + ): FileDownloadRequest[] { if (assetType === AppConstants.ASSET_TYPE_AUDIO) { return this.audioFileDownloadRequests; } else { @@ -226,8 +283,11 @@ export class AssetsBackendApiService { } private async fetchFile( - entityType: string, entityId: string, filename: string, - assetType: string): Promise { + entityType: string, + entityId: string, + filename: string, + assetType: string + ): Promise { let onResolve!: (_: Blob) => void; let onReject!: () => void; const blobPromise = new Promise((resolve, reject) => { @@ -235,13 +295,14 @@ export class AssetsBackendApiService { onReject = reject; }); - const subscription = this.http.get( - this.getDownloadUrl(entityType, entityId, filename, assetType), { - responseType: 'blob' - }).subscribe(onResolve, onReject); + const subscription = this.http + .get(this.getDownloadUrl(entityType, entityId, filename, assetType), { + responseType: 'blob', + }) + .subscribe(onResolve, onReject); - const fileDownloadRequests = ( - this.getFileDownloadRequestsByAssetType(assetType)); + const fileDownloadRequests = + this.getFileDownloadRequestsByAssetType(assetType); fileDownloadRequests.push(new FileDownloadRequest(filename, subscription)); try { @@ -263,28 +324,34 @@ export class AssetsBackendApiService { } private abortAllCurrentDownloads(assetType: string): void { - const fileDownloadRequests = ( - this.getFileDownloadRequestsByAssetType(assetType)); + const fileDownloadRequests = + this.getFileDownloadRequestsByAssetType(assetType); fileDownloadRequests.forEach(r => r.subscription.unsubscribe()); fileDownloadRequests.length = 0; } private getAudioUploadUrl(explorationId: string): string { let audioUploadUrl = this.urlInterpolationService.interpolateUrl( - AppConstants.AUDIO_UPLOAD_URL_TEMPLATE, { - exploration_id: explorationId - }); + AppConstants.AUDIO_UPLOAD_URL_TEMPLATE, + { + exploration_id: explorationId, + } + ); return audioUploadUrl; } - private getImageUploadUrl( - entityType: string, entityId: string): string { + private getImageUploadUrl(entityType: string, entityId: string): string { let imageUploadUrl = this.urlInterpolationService.interpolateUrl( AppConstants.IMAGE_UPLOAD_URL_TEMPLATE, - { entity_type: entityType, entity_id: entityId }); + {entity_type: entityType, entity_id: entityId} + ); return imageUploadUrl; } } -angular.module('oppia').factory( - 'AssetsBackendApiService', downgradeInjectable(AssetsBackendApiService)); +angular + .module('oppia') + .factory( + 'AssetsBackendApiService', + downgradeInjectable(AssetsBackendApiService) + ); diff --git a/core/templates/services/attribution.service.spec.ts b/core/templates/services/attribution.service.spec.ts index 23f481a7c6ce..21be19e98f4e 100644 --- a/core/templates/services/attribution.service.spec.ts +++ b/core/templates/services/attribution.service.spec.ts @@ -16,13 +16,15 @@ * @fileoverview Tests for AudioBarStatusService. */ -import { HttpClientTestingModule, HttpTestingController } from - '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; -import { AttributionService } from 'services/attribution.service'; -import { ContextService } from 'services/context.service'; -import { CsrfTokenService } from 'services/csrf-token.service'; +import {AttributionService} from 'services/attribution.service'; +import {ContextService} from 'services/context.service'; +import {CsrfTokenService} from 'services/csrf-token.service'; describe('AttributionService', () => { let attributionService: AttributionService; @@ -32,14 +34,14 @@ describe('AttributionService', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] + imports: [HttpClientTestingModule], }); attributionService = TestBed.get(AttributionService); contextService = TestBed.get(ContextService); csrfService = TestBed.get(CsrfTokenService); httpTestingController = TestBed.get(HttpTestingController); - spyOn(csrfService, 'getTokenAsync').and.callFake(async() => { + spyOn(csrfService, 'getTokenAsync').and.callFake(async () => { return Promise.resolve('simple-csrf-token'); }); }); @@ -48,105 +50,118 @@ describe('AttributionService', () => { httpTestingController.verify(); }); - it('should set authors and exploration title correctly', - fakeAsync(() => { - spyOn(contextService, 'getExplorationId').and.returnValue('0'); - const explorationIds = ['0']; - const sampleResults = { - summaries: [{ + it('should set authors and exploration title correctly', fakeAsync(() => { + spyOn(contextService, 'getExplorationId').and.returnValue('0'); + const explorationIds = ['0']; + const sampleResults = { + summaries: [ + { title: 'Title 1', category: 'Category 1', status: 'public', language_code: 'en', human_readable_contributors_summary: { - a: { num_commits: 2 }, - b: { num_commits: 5 } - } - }] - }; - - const requestUrl = '/explorationsummarieshandler/data?' + - 'stringified_exp_ids=' + encodeURI(JSON.stringify(explorationIds)) + - '&' + 'include_private_explorations=true'; - - attributionService.init(); - attributionService.showAttributionModal(); - - const req = httpTestingController.expectOne(requestUrl); - expect(req.request.method).toEqual('GET'); - req.flush(sampleResults); - - flushMicrotasks(); - expect(attributionService.getAuthors()).toEqual(['b', 'a']); - expect(attributionService.getExplorationTitle()).toEqual('Title 1'); - expect(attributionService.isAttributionModalShown()).toBeTrue(); - })); - - it('should show and hide modal correctly', - fakeAsync(() => { - spyOn(contextService, 'getExplorationId').and.returnValue('0'); - const explorationIds = ['0']; - const sampleResults = { - summaries: [{ + a: {num_commits: 2}, + b: {num_commits: 5}, + }, + }, + ], + }; + + const requestUrl = + '/explorationsummarieshandler/data?' + + 'stringified_exp_ids=' + + encodeURI(JSON.stringify(explorationIds)) + + '&' + + 'include_private_explorations=true'; + + attributionService.init(); + attributionService.showAttributionModal(); + + const req = httpTestingController.expectOne(requestUrl); + expect(req.request.method).toEqual('GET'); + req.flush(sampleResults); + + flushMicrotasks(); + expect(attributionService.getAuthors()).toEqual(['b', 'a']); + expect(attributionService.getExplorationTitle()).toEqual('Title 1'); + expect(attributionService.isAttributionModalShown()).toBeTrue(); + })); + + it('should show and hide modal correctly', fakeAsync(() => { + spyOn(contextService, 'getExplorationId').and.returnValue('0'); + const explorationIds = ['0']; + const sampleResults = { + summaries: [ + { title: 'Title 1', category: 'Category 1', status: 'public', language_code: 'en', human_readable_contributors_summary: { - a: { num_commits: 2 }, - b: { num_commits: 5 } - } - }] - }; - - const requestUrl = '/explorationsummarieshandler/data?' + - 'stringified_exp_ids=' + encodeURI(JSON.stringify(explorationIds)) + - '&' + 'include_private_explorations=true'; - - attributionService.init(); - attributionService.showAttributionModal(); - - const req = httpTestingController.expectOne(requestUrl); - expect(req.request.method).toEqual('GET'); - req.flush(sampleResults); - - flushMicrotasks(); - expect(attributionService.getAuthors()).toEqual(['b', 'a']); - expect(attributionService.getExplorationTitle()).toEqual('Title 1'); - expect(attributionService.isAttributionModalShown()).toBeTrue(); - - attributionService.hideAttributionModal(); - expect(attributionService.isAttributionModalShown()).toBeFalse(); - - attributionService.showAttributionModal(); - expect(attributionService.isAttributionModalShown()).toBeTrue(); - })); - - it('should not initialise fields if backend call fails', - fakeAsync(() => { - spyOn(contextService, 'getExplorationId').and.returnValue('0'); - const explorationIds = ['0']; - - const requestUrl = '/explorationsummarieshandler/data?' + - 'stringified_exp_ids=' + encodeURI(JSON.stringify(explorationIds)) + - '&' + 'include_private_explorations=true'; - - attributionService.init(); - - const req = httpTestingController.expectOne(requestUrl); - expect(req.request.method).toEqual('GET'); - req.flush({ - error: 'Error fetching data.' - }, { + a: {num_commits: 2}, + b: {num_commits: 5}, + }, + }, + ], + }; + + const requestUrl = + '/explorationsummarieshandler/data?' + + 'stringified_exp_ids=' + + encodeURI(JSON.stringify(explorationIds)) + + '&' + + 'include_private_explorations=true'; + + attributionService.init(); + attributionService.showAttributionModal(); + + const req = httpTestingController.expectOne(requestUrl); + expect(req.request.method).toEqual('GET'); + req.flush(sampleResults); + + flushMicrotasks(); + expect(attributionService.getAuthors()).toEqual(['b', 'a']); + expect(attributionService.getExplorationTitle()).toEqual('Title 1'); + expect(attributionService.isAttributionModalShown()).toBeTrue(); + + attributionService.hideAttributionModal(); + expect(attributionService.isAttributionModalShown()).toBeFalse(); + + attributionService.showAttributionModal(); + expect(attributionService.isAttributionModalShown()).toBeTrue(); + })); + + it('should not initialise fields if backend call fails', fakeAsync(() => { + spyOn(contextService, 'getExplorationId').and.returnValue('0'); + const explorationIds = ['0']; + + const requestUrl = + '/explorationsummarieshandler/data?' + + 'stringified_exp_ids=' + + encodeURI(JSON.stringify(explorationIds)) + + '&' + + 'include_private_explorations=true'; + + attributionService.init(); + + const req = httpTestingController.expectOne(requestUrl); + expect(req.request.method).toEqual('GET'); + req.flush( + { + error: 'Error fetching data.', + }, + { status: 500, - statusText: 'Error fetching data.' - }); - - flushMicrotasks(); - expect(attributionService.isAttributionModalShown()).toBeFalse(); - expect(attributionService.getAuthors()).toEqual([]); - expect(attributionService.getExplorationTitle()).toEqual(''); - })); + statusText: 'Error fetching data.', + } + ); + + flushMicrotasks(); + expect(attributionService.isAttributionModalShown()).toBeFalse(); + expect(attributionService.getAuthors()).toEqual([]); + expect(attributionService.getExplorationTitle()).toEqual(''); + })); it('should allow attribution generation in exp player page', () => { spyOn(contextService, 'isInExplorationPlayerPage').and.returnValue(true); diff --git a/core/templates/services/attribution.service.ts b/core/templates/services/attribution.service.ts index 705aa9544bbd..322744b958db 100644 --- a/core/templates/services/attribution.service.ts +++ b/core/templates/services/attribution.service.ts @@ -16,15 +16,15 @@ * @fileoverview Service to handle the attribution experience. */ -import { ApplicationRef, Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {ApplicationRef, Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; -import { ExplorationSummaryBackendApiService } from 'domain/summary/exploration-summary-backend-api.service'; -import { HumanReadableContributorsSummary } from 'domain/summary/creator-exploration-summary.model'; -import { ContextService } from 'services/context.service'; +import {ExplorationSummaryBackendApiService} from 'domain/summary/exploration-summary-backend-api.service'; +import {HumanReadableContributorsSummary} from 'domain/summary/creator-exploration-summary.model'; +import {ContextService} from 'services/context.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class AttributionService { attributionModalIsShown: boolean = false; @@ -33,33 +33,34 @@ export class AttributionService { constructor( private applicationRef: ApplicationRef, private contextService: ContextService, - private explorationSummaryBackendApiService: ( - ExplorationSummaryBackendApiService) + private explorationSummaryBackendApiService: ExplorationSummaryBackendApiService ) {} init(): void { this.explorationSummaryBackendApiService - .loadPublicAndPrivateExplorationSummariesAsync( - [this.contextService.getExplorationId()]).then(responseObject => { - let summaries = responseObject.summaries; - let contributorSummary = ( - summaries.length ? - summaries[0].human_readable_contributors_summary : - {} as HumanReadableContributorsSummary - ); - this.authors = ( - Object.keys(contributorSummary).sort( + .loadPublicAndPrivateExplorationSummariesAsync([ + this.contextService.getExplorationId(), + ]) + .then( + responseObject => { + let summaries = responseObject.summaries; + let contributorSummary = summaries.length + ? summaries[0].human_readable_contributors_summary + : ({} as HumanReadableContributorsSummary); + this.authors = Object.keys(contributorSummary).sort( (contributorUsername1, contributorUsername2) => { - let { num_commits: commitsOfContributor1 } = contributorSummary[ - contributorUsername1]; - let { num_commits: commitsOfContributor2 } = contributorSummary[ - contributorUsername2]; + let {num_commits: commitsOfContributor1} = + contributorSummary[contributorUsername1]; + let {num_commits: commitsOfContributor2} = + contributorSummary[contributorUsername2]; return commitsOfContributor2 - commitsOfContributor1; - }) - ); - this.explorationTitle = summaries.length ? summaries[0].title : ''; - this.applicationRef.tick(); - }, () => {}); + } + ); + this.explorationTitle = summaries.length ? summaries[0].title : ''; + this.applicationRef.tick(); + }, + () => {} + ); } isGenerateAttributionAllowed(): boolean { @@ -87,5 +88,6 @@ export class AttributionService { } } -angular.module('oppia').factory( - 'AttributionService', downgradeInjectable(AttributionService)); +angular + .module('oppia') + .factory('AttributionService', downgradeInjectable(AttributionService)); diff --git a/core/templates/services/audio-bar-status.service.spec.ts b/core/templates/services/audio-bar-status.service.spec.ts index 65afd8602017..b48aa156096f 100644 --- a/core/templates/services/audio-bar-status.service.spec.ts +++ b/core/templates/services/audio-bar-status.service.spec.ts @@ -16,9 +16,9 @@ * @fileoverview Tests for AudioBarStatusService. */ -import { TestBed } from '@angular/core/testing'; +import {TestBed} from '@angular/core/testing'; -import { AudioBarStatusService } from 'services/audio-bar-status.service'; +import {AudioBarStatusService} from 'services/audio-bar-status.service'; describe('AudioBarStatusService', () => { let audioBarStatusService: AudioBarStatusService; diff --git a/core/templates/services/audio-bar-status.service.ts b/core/templates/services/audio-bar-status.service.ts index e138960e31f4..d440f6ca66c3 100644 --- a/core/templates/services/audio-bar-status.service.ts +++ b/core/templates/services/audio-bar-status.service.ts @@ -18,11 +18,11 @@ * or collapsed. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class AudioBarStatusService { audioBarIsExpanded: boolean = false; @@ -40,5 +40,6 @@ export class AudioBarStatusService { } } -angular.module('oppia').factory('AudioBarStatusService', - downgradeInjectable(AudioBarStatusService)); +angular + .module('oppia') + .factory('AudioBarStatusService', downgradeInjectable(AudioBarStatusService)); diff --git a/core/templates/services/audio-player.service.spec.ts b/core/templates/services/audio-player.service.spec.ts index 21230f452687..ac64b05e2aad 100644 --- a/core/templates/services/audio-player.service.spec.ts +++ b/core/templates/services/audio-player.service.spec.ts @@ -16,16 +16,23 @@ * @fileoverview Unit tests to operate the playback of audio. */ -import { discardPeriodicTasks, fakeAsync, flushMicrotasks, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { AudioPlayerService } from './audio-player.service'; -import { AssetsBackendApiService } from './assets-backend-api.service'; -import { ContextService } from './context.service'; -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { + discardPeriodicTasks, + fakeAsync, + flushMicrotasks, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {AudioPlayerService} from './audio-player.service'; +import {AssetsBackendApiService} from './assets-backend-api.service'; +import {ContextService} from './context.service'; +import {EventEmitter, NO_ERRORS_SCHEMA} from '@angular/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; import * as howler from 'howler'; -import { AudioTranslationManagerService } from 'pages/exploration-player-page/services/audio-translation-manager.service'; -import { Subject } from 'rxjs'; -import { Howl } from 'howler'; +import {AudioTranslationManagerService} from 'pages/exploration-player-page/services/audio-translation-manager.service'; +import {Subject} from 'rxjs'; +import {Howl} from 'howler'; describe('AudioPlayerService', () => { let audioPlayerService: AudioPlayerService; @@ -35,22 +42,18 @@ describe('AudioPlayerService', () => { let successHandler: jasmine.Spy; let failHandler: jasmine.Spy; - beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [ - AudioPlayerService, - ContextService, - AssetsBackendApiService, - ], - schemas: [NO_ERRORS_SCHEMA] + providers: [AudioPlayerService, ContextService, AssetsBackendApiService], + schemas: [NO_ERRORS_SCHEMA], }); })); beforeEach(() => { - audioTranslationManagerService = - TestBed.inject(AudioTranslationManagerService); + audioTranslationManagerService = TestBed.inject( + AudioTranslationManagerService + ); audioPlayerService = TestBed.inject(AudioPlayerService); contextService = TestBed.inject(ContextService); assetsBackendApiService = TestBed.inject(AssetsBackendApiService); @@ -89,33 +92,34 @@ describe('AudioPlayerService', () => { }, duration: () => { return 30; - } + }, } as Howl); spyOn(assetsBackendApiService, 'loadAudio').and.returnValue( Promise.resolve({ data: new Blob(), - filename: 'test.mp3' - })); + filename: 'test.mp3', + }) + ); }); - it('should load track when user plays audio', fakeAsync(async() => { - audioPlayerService.loadAsync('test.mp3').then( - successHandler, failHandler); + it('should load track when user plays audio', fakeAsync(async () => { + audioPlayerService + .loadAsync('test.mp3') + .then(successHandler, failHandler); flushMicrotasks(); expect(successHandler).toHaveBeenCalled(); expect(failHandler).not.toHaveBeenCalled(); })); - it('should not load track when a track has already been loaded', - fakeAsync(() => { - audioPlayerService.loadAsync('test.mp3'); - flushMicrotasks(); - audioPlayerService.loadAsync('test.mp3'); - flushMicrotasks(); + it('should not load track when a track has already been loaded', fakeAsync(() => { + audioPlayerService.loadAsync('test.mp3'); + flushMicrotasks(); + audioPlayerService.loadAsync('test.mp3'); + flushMicrotasks(); - expect(assetsBackendApiService.loadAudio).toHaveBeenCalledTimes(1); - })); + expect(assetsBackendApiService.loadAudio).toHaveBeenCalledTimes(1); + })); it('should play audio when user clicks the play button', fakeAsync(() => { spyOn(console, 'error'); @@ -134,100 +138,101 @@ describe('AudioPlayerService', () => { expect(console.error).toHaveBeenCalledWith('Howl.play'); })); - it('should not play audio again when audio is already being played', - fakeAsync(() => { - spyOn(console, 'error'); - spyOn(audioPlayerService, 'isPlaying').and.returnValue(true); - - audioPlayerService.loadAsync('test.mp3'); - flushMicrotasks(); - - audioPlayerService.play(); - discardPeriodicTasks(); - - // The play function of the Howl calss is called when the user clicks - // the play button. Since the Howl class is mocked, a cosole.error stmt - // is placed inside the play function and is tested if the console.error - // stmt inside it triggered. - expect(console.error).not.toHaveBeenCalledWith('Howl.play'); - })); + it('should not play audio again when audio is already being played', fakeAsync(() => { + spyOn(console, 'error'); + spyOn(audioPlayerService, 'isPlaying').and.returnValue(true); - it('should not pause track when audio is not being played', - fakeAsync(() => { - spyOn(audioPlayerService, 'isPlaying').and.returnValue(false); - spyOn(audioPlayerService, 'getCurrentTime'); - let subjectNext = spyOn(Subject.prototype, 'next'); - audioPlayerService.loadAsync('test.mp3'); - flushMicrotasks(); + audioPlayerService.loadAsync('test.mp3'); + flushMicrotasks(); - audioPlayerService.pause(); + audioPlayerService.play(); + discardPeriodicTasks(); - expect(audioPlayerService.getCurrentTime).not.toHaveBeenCalled(); - expect(subjectNext).toHaveBeenCalledTimes(1); - })); + // The play function of the Howl calss is called when the user clicks + // the play button. Since the Howl class is mocked, a cosole.error stmt + // is placed inside the play function and is tested if the console.error + // stmt inside it triggered. + expect(console.error).not.toHaveBeenCalledWith('Howl.play'); + })); - it('should pause track when user clicks the \'Pause\' ' + - 'button', fakeAsync(() => { - spyOn(audioPlayerService, 'isPlaying').and.returnValue(true); - let subjectNext = spyOn(Subject.prototype, 'next'); + it('should not pause track when audio is not being played', fakeAsync(() => { + spyOn(audioPlayerService, 'isPlaying').and.returnValue(false); spyOn(audioPlayerService, 'getCurrentTime'); + let subjectNext = spyOn(Subject.prototype, 'next'); audioPlayerService.loadAsync('test.mp3'); flushMicrotasks(); audioPlayerService.pause(); - expect(audioPlayerService.getCurrentTime).toHaveBeenCalled(); - expect(subjectNext).toHaveBeenCalled(); + expect(audioPlayerService.getCurrentTime).not.toHaveBeenCalled(); + expect(subjectNext).toHaveBeenCalledTimes(1); })); - it('should stop playing track when called', + it( + "should pause track when user clicks the 'Pause' " + 'button', fakeAsync(() => { - spyOn(audioPlayerService, 'setCurrentTime'); - spyOn(console, 'error'); - spyOn( - audioTranslationManagerService, - 'clearSecondaryAudioTranslations'); + spyOn(audioPlayerService, 'isPlaying').and.returnValue(true); let subjectNext = spyOn(Subject.prototype, 'next'); + spyOn(audioPlayerService, 'getCurrentTime'); audioPlayerService.loadAsync('test.mp3'); flushMicrotasks(); - audioPlayerService.stop(); + audioPlayerService.pause(); - expect(console.error).toHaveBeenCalledWith('Howl.stop'); - expect(subjectNext).toHaveBeenCalledTimes(2); - expect(audioTranslationManagerService.clearSecondaryAudioTranslations) - .toHaveBeenCalled(); - })); + expect(audioPlayerService.getCurrentTime).toHaveBeenCalled(); + expect(subjectNext).toHaveBeenCalled(); + }) + ); - it('should rewind the track when user clicks the \'Rewind\' ' + - 'button', fakeAsync(() => { + it('should stop playing track when called', fakeAsync(() => { + spyOn(audioPlayerService, 'setCurrentTime'); spyOn(console, 'error'); + spyOn(audioTranslationManagerService, 'clearSecondaryAudioTranslations'); + let subjectNext = spyOn(Subject.prototype, 'next'); audioPlayerService.loadAsync('test.mp3'); flushMicrotasks(); - audioPlayerService.rewind(5); + audioPlayerService.stop(); - expect(console.error).toHaveBeenCalledWith('Howl.seek ', 5); + expect(console.error).toHaveBeenCalledWith('Howl.stop'); + expect(subjectNext).toHaveBeenCalledTimes(2); + expect( + audioTranslationManagerService.clearSecondaryAudioTranslations + ).toHaveBeenCalled(); })); - it('should forward the track when user clicks the \'forward\'' + - ' button', fakeAsync(() => { - spyOn(console, 'error'); - audioPlayerService.loadAsync('test.mp3'); - flushMicrotasks(); + it( + "should rewind the track when user clicks the 'Rewind' " + 'button', + fakeAsync(() => { + spyOn(console, 'error'); + audioPlayerService.loadAsync('test.mp3'); + flushMicrotasks(); - audioPlayerService.forward(5); + audioPlayerService.rewind(5); - expect(console.error).toHaveBeenCalledWith('Howl.seek ', 15); - })); + expect(console.error).toHaveBeenCalledWith('Howl.seek ', 5); + }) + ); - it('should get the current time of the track when it is being played', + it( + "should forward the track when user clicks the 'forward'" + ' button', fakeAsync(() => { + spyOn(console, 'error'); audioPlayerService.loadAsync('test.mp3'); flushMicrotasks(); - expect(audioPlayerService.getCurrentTime()).toBe(10); - })); + audioPlayerService.forward(5); + + expect(console.error).toHaveBeenCalledWith('Howl.seek ', 15); + }) + ); + + it('should get the current time of the track when it is being played', fakeAsync(() => { + audioPlayerService.loadAsync('test.mp3'); + flushMicrotasks(); + + expect(audioPlayerService.getCurrentTime()).toBe(10); + })); it('should set the time when user clicks on the track', fakeAsync(() => { spyOn(console, 'error'); @@ -239,27 +244,33 @@ describe('AudioPlayerService', () => { expect(console.error).toHaveBeenCalledWith('Howl.seek ', 15); })); - it('should set the time as track duration when user clicks on end ' + - 'of the track', fakeAsync(() => { - spyOn(console, 'error'); - audioPlayerService.loadAsync('test.mp3'); - flushMicrotasks(); + it( + 'should set the time as track duration when user clicks on end ' + + 'of the track', + fakeAsync(() => { + spyOn(console, 'error'); + audioPlayerService.loadAsync('test.mp3'); + flushMicrotasks(); - audioPlayerService.setCurrentTime(31); + audioPlayerService.setCurrentTime(31); - expect(console.error).toHaveBeenCalledWith('Howl.seek ', 30); - })); + expect(console.error).toHaveBeenCalledWith('Howl.seek ', 30); + }) + ); - it('should set the time as 0 when user clicks on beginning ' + - 'of the track', fakeAsync(() => { - spyOn(console, 'error'); - audioPlayerService.loadAsync('test.mp3'); - flushMicrotasks(); + it( + 'should set the time as 0 when user clicks on beginning ' + + 'of the track', + fakeAsync(() => { + spyOn(console, 'error'); + audioPlayerService.loadAsync('test.mp3'); + flushMicrotasks(); - audioPlayerService.setCurrentTime(-1); + audioPlayerService.setCurrentTime(-1); - expect(console.error).toHaveBeenCalledWith('Howl.seek ', 0); - })); + expect(console.error).toHaveBeenCalledWith('Howl.seek ', 0); + }) + ); it('should return duration of the track when called', fakeAsync(() => { audioPlayerService.loadAsync('test.mp3'); @@ -338,13 +349,14 @@ describe('AudioPlayerService', () => { }, playing: () => { return true; - } + }, } as Howl); spyOn(assetsBackendApiService, 'loadAudio').and.returnValue( Promise.resolve({ data: new Blob(), - filename: 'test.mp3' - })); + filename: 'test.mp3', + }) + ); spyOn(console, 'error'); }); @@ -364,16 +376,15 @@ describe('AudioPlayerService', () => { expect(console.error).not.toHaveBeenCalled(); })); - it('should not rewind track when seek does not return an number', - fakeAsync(() => { - audioPlayerService.loadAsync('test.mp3'); - flushMicrotasks(); + it('should not rewind track when seek does not return an number', fakeAsync(() => { + audioPlayerService.loadAsync('test.mp3'); + flushMicrotasks(); - audioPlayerService.rewind(5); + audioPlayerService.rewind(5); - expect(console.error).toHaveBeenCalledTimes(1); - expect(console.error).toHaveBeenCalledWith('Howl.seek'); - })); + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith('Howl.seek'); + })); it('should not forward track when no audio is loaded', fakeAsync(() => { audioPlayerService.forward(5); @@ -381,29 +392,33 @@ describe('AudioPlayerService', () => { expect(console.error).not.toHaveBeenCalled(); })); - it('should not foward track when seek does not return an number', - fakeAsync(() => { - audioPlayerService.loadAsync('test.mp3'); - flushMicrotasks(); + it('should not foward track when seek does not return an number', fakeAsync(() => { + audioPlayerService.loadAsync('test.mp3'); + flushMicrotasks(); - audioPlayerService.forward(5); + audioPlayerService.forward(5); - expect(console.error).toHaveBeenCalledTimes(1); - expect(console.error).toHaveBeenCalledWith('Howl.seek'); - })); + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith('Howl.seek'); + })); - it('should not get the current time of track when no audio is' + - ' loaded', () => { - expect(audioPlayerService.getCurrentTime()).toBe(0); - }); + it( + 'should not get the current time of track when no audio is' + ' loaded', + () => { + expect(audioPlayerService.getCurrentTime()).toBe(0); + } + ); - it('should not get the current time of track when seek does not ' + - 'return an number', fakeAsync(() => { - audioPlayerService.loadAsync('test.mp3'); - flushMicrotasks(); + it( + 'should not get the current time of track when seek does not ' + + 'return an number', + fakeAsync(() => { + audioPlayerService.loadAsync('test.mp3'); + flushMicrotasks(); - expect(audioPlayerService.getCurrentTime()).toBe(0); - })); + expect(audioPlayerService.getCurrentTime()).toBe(0); + }) + ); it('should not forward track when no audio is loaded', fakeAsync(() => { audioPlayerService.forward(5); @@ -429,36 +444,40 @@ describe('AudioPlayerService', () => { })); }); - it('should clear secondary audio translations when audio ' + - 'ends', fakeAsync(async() => { - spyOn(howler, 'Howl').and.returnValue({ - on: (evt: string, func: () => void) => { - if (evt === 'end') { - func(); - } - } - } as Howl); - spyOn(assetsBackendApiService, 'loadAudio').and.returnValue( - Promise.resolve({ - data: new Blob(), - filename: 'test' - })); - spyOn(audioTranslationManagerService, 'clearSecondaryAudioTranslations'); + it( + 'should clear secondary audio translations when audio ' + 'ends', + fakeAsync(async () => { + spyOn(howler, 'Howl').and.returnValue({ + on: (evt: string, func: () => void) => { + if (evt === 'end') { + func(); + } + }, + } as Howl); + spyOn(assetsBackendApiService, 'loadAudio').and.returnValue( + Promise.resolve({ + data: new Blob(), + filename: 'test', + }) + ); + spyOn(audioTranslationManagerService, 'clearSecondaryAudioTranslations'); - audioPlayerService.loadAsync('test.mp3'); - flushMicrotasks(); + audioPlayerService.loadAsync('test.mp3'); + flushMicrotasks(); - expect(audioTranslationManagerService.clearSecondaryAudioTranslations) - .toHaveBeenCalled(); - })); + expect( + audioTranslationManagerService.clearSecondaryAudioTranslations + ).toHaveBeenCalled(); + }) + ); it('should display error when audio fails to load', fakeAsync(() => { spyOn(assetsBackendApiService, 'loadAudio').and.returnValue( - Promise.reject('Error')); + Promise.reject('Error') + ); spyOn(audioTranslationManagerService, 'clearSecondaryAudioTranslations'); - audioPlayerService.loadAsync('test.mp3').then( - successHandler, failHandler); + audioPlayerService.loadAsync('test.mp3').then(successHandler, failHandler); flushMicrotasks(); expect(successHandler).not.toHaveBeenCalled(); @@ -467,19 +486,16 @@ describe('AudioPlayerService', () => { it('should fetch event emitter for update in view', () => { let mockEventEmitter = new EventEmitter(); - expect(audioPlayerService.viewUpdate).toEqual( - mockEventEmitter); + expect(audioPlayerService.viewUpdate).toEqual(mockEventEmitter); }); it('should fetch event emitter for auto play audio', () => { let mockEventEmitter = new EventEmitter(); - expect(audioPlayerService.onAutoplayAudio).toEqual( - mockEventEmitter); + expect(audioPlayerService.onAutoplayAudio).toEqual(mockEventEmitter); }); it('should return subject when audio stops playing', () => { let mockSubject = new Subject(); - expect(audioPlayerService.onAudioStop).toEqual( - mockSubject); + expect(audioPlayerService.onAudioStop).toEqual(mockSubject); }); }); diff --git a/core/templates/services/audio-player.service.ts b/core/templates/services/audio-player.service.ts index d2d16d98c511..56f1b584a93b 100644 --- a/core/templates/services/audio-player.service.ts +++ b/core/templates/services/audio-player.service.ts @@ -16,15 +16,18 @@ * @fileoverview Service to operate the playback of audio. */ -import { EventEmitter, Injectable, NgZone } from '@angular/core'; -import { AudioFile } from 'domain/utilities/audio-file.model'; -import { AudioTranslationManagerService, AudioTranslations } from 'pages/exploration-player-page/services/audio-translation-manager.service'; -import { AssetsBackendApiService } from './assets-backend-api.service'; -import { ContextService } from './context.service'; -import { Howl } from 'howler'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { interval, Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; +import {EventEmitter, Injectable, NgZone} from '@angular/core'; +import {AudioFile} from 'domain/utilities/audio-file.model'; +import { + AudioTranslationManagerService, + AudioTranslations, +} from 'pages/exploration-player-page/services/audio-translation-manager.service'; +import {AssetsBackendApiService} from './assets-backend-api.service'; +import {ContextService} from './context.service'; +import {Howl} from 'howler'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {interval, Subject} from 'rxjs'; +import {takeUntil} from 'rxjs/operators'; export interface AutoPlayAudioEvent { audioTranslations: AudioTranslations; @@ -33,7 +36,7 @@ export interface AutoPlayAudioEvent { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class AudioPlayerService { // 'currentTrackFilename','currentTrack' and 'lastPauseOrSeekPos' @@ -42,8 +45,8 @@ export class AudioPlayerService { private _currentTrack: Howl | null = null; private _lastPauseOrSeekPos: number | null = null; private _updateViewEventEmitter = new EventEmitter(); - private _autoplayAudioEventEmitter = ( - new EventEmitter()); + private _autoplayAudioEventEmitter = + new EventEmitter(); private _stopIntervalSubject = new Subject(); constructor( @@ -54,36 +57,37 @@ export class AudioPlayerService { ) {} private async _loadAsync( - filename: string, - successCallback: () => void, - errorCallback: (reason?: string[]) => void + filename: string, + successCallback: () => void, + errorCallback: (reason?: string[]) => void ) { if (this._currentTrackFilename === filename) { return; } - this.assetsBackendApiService.loadAudio( - this.contextService.getExplorationId(), - filename).then( - (loadedAudioFile: AudioFile) => { - this._currentTrack = new Howl({ - src: [URL.createObjectURL(loadedAudioFile.data)], - format: ['mp3'] - }); - this._currentTrack.on('load', () => { - this._stopIntervalSubject.next(); - this._currentTrackFilename = loadedAudioFile.filename; - this._lastPauseOrSeekPos = 0; - successCallback(); - }); - this._currentTrack.on('end', () => { - this._stopIntervalSubject.next(); - this._currentTrack = null; - this._currentTrackFilename = null; - this._lastPauseOrSeekPos = null; - this.audioTranslationManagerService.clearSecondaryAudioTranslations(); - }); - }, (e) => errorCallback(e) - ); + this.assetsBackendApiService + .loadAudio(this.contextService.getExplorationId(), filename) + .then( + (loadedAudioFile: AudioFile) => { + this._currentTrack = new Howl({ + src: [URL.createObjectURL(loadedAudioFile.data)], + format: ['mp3'], + }); + this._currentTrack.on('load', () => { + this._stopIntervalSubject.next(); + this._currentTrackFilename = loadedAudioFile.filename; + this._lastPauseOrSeekPos = 0; + successCallback(); + }); + this._currentTrack.on('end', () => { + this._stopIntervalSubject.next(); + this._currentTrack = null; + this._currentTrackFilename = null; + this._lastPauseOrSeekPos = null; + this.audioTranslationManagerService.clearSecondaryAudioTranslations(); + }); + }, + e => errorCallback(e) + ); } async loadAsync(filename: string): Promise { @@ -103,12 +107,13 @@ export class AudioPlayerService { // We can safely typecast it to 'number'. this._currentTrack.seek(this._lastPauseOrSeekPos as number); } - interval(500).pipe(takeUntil( - this._stopIntervalSubject)).subscribe(() => { - this.ngZone.run(() => { - this._updateViewEventEmitter.emit(); + interval(500) + .pipe(takeUntil(this._stopIntervalSubject)) + .subscribe(() => { + this.ngZone.run(() => { + this._updateViewEventEmitter.emit(); + }); }); - }); // 'currentTrack' is not null since the audio event has been emitted // and that is why we use '?'. this._currentTrack?.play(); @@ -142,8 +147,7 @@ export class AudioPlayerService { if (!this._currentTrack) { return; } - const currentSeconds = ( - this._currentTrack.seek()); + const currentSeconds = this._currentTrack.seek(); if (typeof currentSeconds !== 'number') { return; } @@ -228,6 +232,6 @@ export class AudioPlayerService { } } -angular.module('oppia').factory('AudioPlayerService', downgradeInjectable( - AudioPlayerService -)); +angular + .module('oppia') + .factory('AudioPlayerService', downgradeInjectable(AudioPlayerService)); diff --git a/core/templates/services/auth-backend-api.service.spec.ts b/core/templates/services/auth-backend-api.service.spec.ts index 42e16fca5321..af7a5e743628 100644 --- a/core/templates/services/auth-backend-api.service.spec.ts +++ b/core/templates/services/auth-backend-api.service.spec.ts @@ -16,10 +16,13 @@ * @fileoverview Tests that the user service is working as expected. */ -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { fakeAsync, flushMicrotasks, TestBed } from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {fakeAsync, flushMicrotasks, TestBed} from '@angular/core/testing'; -import { AuthBackendApiService } from 'services/auth-backend-api.service'; +import {AuthBackendApiService} from 'services/auth-backend-api.service'; describe('Auth Backend Api Service', () => { let authBackendApiService: AuthBackendApiService; @@ -33,7 +36,7 @@ describe('Auth Backend Api Service', () => { afterEach(() => httpTestingController.verify()); - it('should call /session_begin', fakeAsync(async() => { + it('should call /session_begin', fakeAsync(async () => { const response = authBackendApiService.beginSessionAsync('TKN'); flushMicrotasks(); @@ -46,7 +49,7 @@ describe('Auth Backend Api Service', () => { await expectAsync(response).toBeResolved(); })); - it('should call /session_end', fakeAsync(async() => { + it('should call /session_end', fakeAsync(async () => { const response = authBackendApiService.endSessionAsync(); flushMicrotasks(); diff --git a/core/templates/services/auth-backend-api.service.ts b/core/templates/services/auth-backend-api.service.ts index de56b9de0292..ffdf65ed5839 100644 --- a/core/templates/services/auth-backend-api.service.ts +++ b/core/templates/services/auth-backend-api.service.ts @@ -16,20 +16,22 @@ * @fileoverview Service for user data. */ -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class AuthBackendApiService { constructor(private httpClient: HttpClient) {} async beginSessionAsync(idToken: string): Promise { - await this.httpClient.get('/session_begin', { - headers: {Authorization: `Bearer ${idToken}`} - }).toPromise(); + await this.httpClient + .get('/session_begin', { + headers: {Authorization: `Bearer ${idToken}`}, + }) + .toPromise(); } async endSessionAsync(): Promise { @@ -37,5 +39,6 @@ export class AuthBackendApiService { } } -angular.module('oppia').factory( - 'AuthBackendApiService', downgradeInjectable(AuthBackendApiService)); +angular + .module('oppia') + .factory('AuthBackendApiService', downgradeInjectable(AuthBackendApiService)); diff --git a/core/templates/services/auth.service.spec.ts b/core/templates/services/auth.service.spec.ts index f5816f119a04..5cc6261725c4 100644 --- a/core/templates/services/auth.service.spec.ts +++ b/core/templates/services/auth.service.spec.ts @@ -16,15 +16,15 @@ * @fileoverview Unit tests for AuthService. */ -import { TestBed } from '@angular/core/testing'; -import { AngularFireAuth } from '@angular/fire/auth'; -import { md5 } from 'hash-wasm'; +import {TestBed} from '@angular/core/testing'; +import {AngularFireAuth} from '@angular/fire/auth'; +import {md5} from 'hash-wasm'; -import { AuthService } from 'services/auth.service'; -import { AuthBackendApiService } from 'services/auth-backend-api.service'; +import {AuthService} from 'services/auth.service'; +import {AuthBackendApiService} from 'services/auth-backend-api.service'; import firebase from 'firebase'; -describe('Auth service', function() { +describe('Auth service', function () { let authService: AuthService; let email: string; let password: string; @@ -33,7 +33,7 @@ describe('Auth service', function() { let authBackendApiService: jasmine.SpyObj; let angularFireAuth: jasmine.SpyObj; - beforeEach(async() => { + beforeEach(async () => { angularFireAuth = jasmine.createSpyObj([ 'createUserWithEmailAndPassword', 'getRedirectResult', @@ -50,7 +50,7 @@ describe('Auth service', function() { providers: [ {provide: AngularFireAuth, useValue: angularFireAuth}, {provide: AuthBackendApiService, useValue: authBackendApiService}, - ] + ], }); authService = TestBed.inject(AuthService); @@ -65,99 +65,114 @@ describe('Auth service', function() { }; }); - it('should return emulator config when emulator is enabled under ' + - 'docker environment', () => { - spyOnProperty(AuthService, 'firebaseEmulatorIsEnabled', 'get') - .and.returnValue(true); - - // TODO(#18260): Change this when we permanently move to the Docker Setup. - process.env.OPPIA_IS_DOCKERIZED = 'true'; - expect(AuthService.firebaseEmulatorConfig).toEqual(['0.0.0.0', 9099]); - }); + it( + 'should return emulator config when emulator is enabled under ' + + 'docker environment', + () => { + spyOnProperty( + AuthService, + 'firebaseEmulatorIsEnabled', + 'get' + ).and.returnValue(true); + + // TODO(#18260): Change this when we permanently move to the Docker Setup. + process.env.OPPIA_IS_DOCKERIZED = 'true'; + expect(AuthService.firebaseEmulatorConfig).toEqual(['0.0.0.0', 9099]); + } + ); it('should return undefined when emulator is disabled', () => { - spyOnProperty(AuthService, 'firebaseEmulatorIsEnabled', 'get') - .and.returnValue(false); + spyOnProperty( + AuthService, + 'firebaseEmulatorIsEnabled', + 'get' + ).and.returnValue(false); expect(AuthService.firebaseEmulatorConfig).toBeUndefined(); }); - it('should resolve when sign out succeeds', async() => { + it('should resolve when sign out succeeds', async () => { angularFireAuth.signOut.and.resolveTo(); await expectAsync(authService.signOutAsync()).toBeResolvedTo(); }); - it('should reject when sign out fails', async() => { + it('should reject when sign out fails', async () => { angularFireAuth.signOut.and.rejectWith(new Error('fail')); await expectAsync(authService.signOutAsync()).toBeRejectedWithError('fail'); }); - it('should throw if signOutAsync is called without angular fire', async() => { + it('should throw if signOutAsync is called without angular fire', async () => { await expectAsync( new AuthService(null, authBackendApiService).signOutAsync() ).toBeRejectedWithError('AngularFireAuth is not available'); }); - it('should throw if signInWithRedirectAsync is called without angular fire', - async() => { - await expectAsync( - new AuthService( - null, authBackendApiService).signInWithRedirectAsync() - ).toBeRejectedWithError('AngularFireAuth is not available'); - }); + it('should throw if signInWithRedirectAsync is called without angular fire', async () => { + await expectAsync( + new AuthService(null, authBackendApiService).signInWithRedirectAsync() + ).toBeRejectedWithError('AngularFireAuth is not available'); + }); - it('should throw if handleRedirectResultAsync is called without angular fire', - async() => { - await expectAsync( - new AuthService( - null, authBackendApiService).handleRedirectResultAsync() - ).toBeRejectedWithError('AngularFireAuth is not available'); - }); + it('should throw if handleRedirectResultAsync is called without angular fire', async () => { + await expectAsync( + new AuthService(null, authBackendApiService).handleRedirectResultAsync() + ).toBeRejectedWithError('AngularFireAuth is not available'); + }); - it('should delegate to signInWithEmailAndPassword', async() => { - angularFireAuth.signInWithEmailAndPassword - .and.rejectWith({code: 'auth/user-not-found'}); + it('should delegate to signInWithEmailAndPassword', async () => { + angularFireAuth.signInWithEmailAndPassword.and.rejectWith({ + code: 'auth/user-not-found', + }); angularFireAuth.createUserWithEmailAndPassword.and.resolveTo(creds); - await expectAsync(authService.signInWithEmail(email)) - .toBeResolvedTo(); - - expect(angularFireAuth.signInWithEmailAndPassword) - .toHaveBeenCalledWith(email, password); - expect(angularFireAuth.createUserWithEmailAndPassword) - .toHaveBeenCalledWith(email, password); - expect(authBackendApiService.beginSessionAsync) - .toHaveBeenCalledWith(idToken); + await expectAsync(authService.signInWithEmail(email)).toBeResolvedTo(); + + expect(angularFireAuth.signInWithEmailAndPassword).toHaveBeenCalledWith( + email, + password + ); + expect(angularFireAuth.createUserWithEmailAndPassword).toHaveBeenCalledWith( + email, + password + ); + expect(authBackendApiService.beginSessionAsync).toHaveBeenCalledWith( + idToken + ); }); - it('should propogate signInWithEmailAndPassword errors', async() => { + it('should propogate signInWithEmailAndPassword errors', async () => { const unknownError = {code: 'auth/unknown-error'}; spyOn(window, 'prompt').and.returnValue(email); angularFireAuth.signInWithEmailAndPassword.and.rejectWith(unknownError); - await expectAsync(authService.signInWithEmail(email)) - .toBeRejectedWith(unknownError); + await expectAsync(authService.signInWithEmail(email)).toBeRejectedWith( + unknownError + ); }); - it('should propogate createUserWithEmailAndPassword errors', async() => { + it('should propogate createUserWithEmailAndPassword errors', async () => { const unknownError = {code: 'auth/unknown-error'}; spyOn(window, 'prompt').and.returnValue(email); - angularFireAuth.signInWithEmailAndPassword - .and.rejectWith({code: 'auth/user-not-found'}); - angularFireAuth.createUserWithEmailAndPassword - .and.rejectWith(unknownError); + angularFireAuth.signInWithEmailAndPassword.and.rejectWith({ + code: 'auth/user-not-found', + }); + angularFireAuth.createUserWithEmailAndPassword.and.rejectWith(unknownError); - await expectAsync(authService.signInWithEmail(email)) - .toBeRejectedWith(unknownError); + await expectAsync(authService.signInWithEmail(email)).toBeRejectedWith( + unknownError + ); expect(authBackendApiService.beginSessionAsync).not.toHaveBeenCalled(); }); describe('Production mode', () => { - beforeEach(async() => { - spyOnProperty(AuthService, 'firebaseEmulatorIsEnabled', 'get') - .and.returnValue(false); + beforeEach(async () => { + spyOnProperty( + AuthService, + 'firebaseEmulatorIsEnabled', + 'get' + ).and.returnValue(false); idToken = 'TKN'; creds = { @@ -169,17 +184,20 @@ describe('Auth service', function() { authService = new AuthService(angularFireAuth, authBackendApiService); }); - it('should fail to call signInWithEmail', async() => { - await expectAsync(authService.signInWithEmail(email)) - .toBeRejectedWithError( - 'signInWithEmail can only be called in emulator mode'); + it('should fail to call signInWithEmail', async () => { + await expectAsync( + authService.signInWithEmail(email) + ).toBeRejectedWithError( + 'signInWithEmail can only be called in emulator mode' + ); expect(angularFireAuth.signInWithEmailAndPassword).not.toHaveBeenCalled(); - expect(angularFireAuth.createUserWithEmailAndPassword) - .not.toHaveBeenCalled(); + expect( + angularFireAuth.createUserWithEmailAndPassword + ).not.toHaveBeenCalled(); }); - it('should delegate to AngularFireAuth.signInWithRedirect', async() => { + it('should delegate to AngularFireAuth.signInWithRedirect', async () => { angularFireAuth.signInWithRedirect.and.resolveTo(); await expectAsync(authService.signInWithRedirectAsync()).toBeResolvedTo(); @@ -187,18 +205,20 @@ describe('Auth service', function() { expect(angularFireAuth.signInWithRedirect).toHaveBeenCalled(); }); - it('should delegate to AngularFireAuth.getRedirectResult', async() => { + it('should delegate to AngularFireAuth.getRedirectResult', async () => { angularFireAuth.getRedirectResult.and.resolveTo(creds); - await expectAsync(authService.handleRedirectResultAsync()) - .toBeResolvedTo(true); + await expectAsync(authService.handleRedirectResultAsync()).toBeResolvedTo( + true + ); expect(angularFireAuth.getRedirectResult).toHaveBeenCalled(); - expect(authBackendApiService.beginSessionAsync) - .toHaveBeenCalledWith(idToken); + expect(authBackendApiService.beginSessionAsync).toHaveBeenCalledWith( + idToken + ); }); - it('should delegate to AngularFireAuth.signOut', async() => { + it('should delegate to AngularFireAuth.signOut', async () => { angularFireAuth.signOut.and.resolveTo(); await expectAsync(authService.signOutAsync()).toBeResolvedTo(); @@ -207,38 +227,42 @@ describe('Auth service', function() { expect(authBackendApiService.endSessionAsync).toHaveBeenCalled(); }); - it('should resolve to false if user is missing', async() => { + it('should resolve to false if user is missing', async () => { creds.user = null; angularFireAuth.getRedirectResult.and.resolveTo(creds); - await expectAsync(authService.handleRedirectResultAsync()) - .toBeResolvedTo(false); + await expectAsync(authService.handleRedirectResultAsync()).toBeResolvedTo( + false + ); }); }); describe('Emulator mode', () => { - beforeEach(async() => { - spyOnProperty(AuthService, 'firebaseEmulatorIsEnabled', 'get') - .and.returnValue(true); + beforeEach(async () => { + spyOnProperty( + AuthService, + 'firebaseEmulatorIsEnabled', + 'get' + ).and.returnValue(true); authService = new AuthService(angularFireAuth, authBackendApiService); }); - it('should not delegate to signInWithRedirectAsync', async() => { - await expectAsync(authService.signInWithRedirectAsync()) - .toBeResolvedTo(); + it('should not delegate to signInWithRedirectAsync', async () => { + await expectAsync(authService.signInWithRedirectAsync()).toBeResolvedTo(); expect(angularFireAuth.signInWithRedirect).not.toHaveBeenCalled(); }); - it('should not delegate to handleRedirectResultAsync', async() => { - await expectAsync(authService.handleRedirectResultAsync()) - .toBeResolvedTo(false); + it('should not delegate to handleRedirectResultAsync', async () => { + await expectAsync(authService.handleRedirectResultAsync()).toBeResolvedTo( + false + ); expect(angularFireAuth.getRedirectResult).not.toHaveBeenCalled(); }); - it('should sign out and end session', async() => { + it('should sign out and end session', async () => { await expectAsync(authService.signOutAsync()).toBeResolvedTo(); expect(angularFireAuth.signOut).toHaveBeenCalled(); expect(authBackendApiService.endSessionAsync).toHaveBeenCalled(); diff --git a/core/templates/services/auth.service.ts b/core/templates/services/auth.service.ts index 42e91cbae414..550b182bd554 100644 --- a/core/templates/services/auth.service.ts +++ b/core/templates/services/auth.service.ts @@ -16,21 +16,19 @@ * @fileoverview Service for managing the authorizations of logged-in users. */ -import { Injectable, Optional } from '@angular/core'; -import { FirebaseOptions } from '@angular/fire'; -import { AngularFireAuth } from '@angular/fire/auth'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {Injectable, Optional} from '@angular/core'; +import {FirebaseOptions} from '@angular/fire'; +import {AngularFireAuth} from '@angular/fire/auth'; +import {downgradeInjectable} from '@angular/upgrade/static'; import firebase from 'firebase/app'; import 'firebase/auth'; -import { md5 } from 'hash-wasm'; +import {md5} from 'hash-wasm'; -import { AppConstants } from 'app.constants'; -import { AuthBackendApiService } from 'services/auth-backend-api.service'; +import {AppConstants} from 'app.constants'; +import {AuthBackendApiService} from 'services/auth-backend-api.service'; abstract class AuthServiceImpl { - abstract getRedirectResultAsync(): Promise< - firebase.auth.UserCredential | null - >; + abstract getRedirectResultAsync(): Promise; abstract signInWithRedirectAsync(): Promise; abstract signOutAsync(): Promise; } @@ -56,8 +54,7 @@ class DevAuthServiceImpl extends AuthServiceImpl { super(); } - async signInWithRedirectAsync(): Promise { - } + async signInWithRedirectAsync(): Promise {} async getRedirectResultAsync(): Promise { return null; @@ -95,15 +92,16 @@ class ProdAuthServiceImpl extends AuthServiceImpl { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class AuthService { private authServiceImpl: AuthServiceImpl; creds!: firebase.auth.UserCredential; constructor( - @Optional() private angularFireAuth: AngularFireAuth | null, - private authBackendApiService: AuthBackendApiService) { + @Optional() private angularFireAuth: AngularFireAuth | null, + private authBackendApiService: AuthBackendApiService + ) { if (!this.angularFireAuth) { this.authServiceImpl = new NullAuthServiceImpl(); } else if (AuthService.firebaseEmulatorIsEnabled) { @@ -129,11 +127,13 @@ export class AuthService { } static get firebaseEmulatorConfig(): readonly [string, number] | undefined { - let firebaseHost = ( - process.env.OPPIA_IS_DOCKERIZED ? '0.0.0.0' : 'localhost'); + let firebaseHost = process.env.OPPIA_IS_DOCKERIZED + ? '0.0.0.0' + : 'localhost'; // TODO(#18260): Change this when we permanently move to the Docker Setup. - return AuthService.firebaseEmulatorIsEnabled ? - [firebaseHost, 9099] : undefined; + return AuthService.firebaseEmulatorIsEnabled + ? [firebaseHost, 9099] + : undefined; } async handleRedirectResultAsync(): Promise { @@ -165,18 +165,22 @@ export class AuthService { try { if (this.angularFireAuth !== null) { this.creds = await this.angularFireAuth.signInWithEmailAndPassword( - email, password); + email, + password + ); } - // We use unknown type because we are unsure of the type of error - // that was thrown. Since the catch block cannot identify the - // specific type of error, we are unable to further optimise the - // code by introducing more types of errors. + // We use unknown type because we are unsure of the type of error + // that was thrown. Since the catch block cannot identify the + // specific type of error, we are unable to further optimise the + // code by introducing more types of errors. } catch (err: unknown) { if ((err as firebase.auth.Error).code === 'auth/user-not-found') { if (this.angularFireAuth !== null) { this.creds = - await this.angularFireAuth.createUserWithEmailAndPassword( - email, password); + await this.angularFireAuth.createUserWithEmailAndPassword( + email, + password + ); } } else { throw err; @@ -198,5 +202,6 @@ export class AuthService { } } -angular.module('oppia').factory( - 'AuthService', downgradeInjectable(AuthService)); +angular + .module('oppia') + .factory('AuthService', downgradeInjectable(AuthService)); diff --git a/core/templates/services/autogenerated-audio-player.service.spec.ts b/core/templates/services/autogenerated-audio-player.service.spec.ts index c55a854b3bb4..45eaf8526043 100644 --- a/core/templates/services/autogenerated-audio-player.service.spec.ts +++ b/core/templates/services/autogenerated-audio-player.service.spec.ts @@ -17,10 +17,10 @@ * using the SpeechSynthesis API. */ -import { async, TestBed } from '@angular/core/testing'; -import { AutogeneratedAudioPlayerService } from './autogenerated-audio-player.service'; -import { ContextService } from './context.service'; -import { SpeechSynthesisChunkerService } from './speech-synthesis-chunker.service'; +import {async, TestBed} from '@angular/core/testing'; +import {AutogeneratedAudioPlayerService} from './autogenerated-audio-player.service'; +import {ContextService} from './context.service'; +import {SpeechSynthesisChunkerService} from './speech-synthesis-chunker.service'; describe('BannerComponent', () => { let autogeneratedAudioPlayerService: AutogeneratedAudioPlayerService; @@ -32,67 +32,81 @@ describe('BannerComponent', () => { providers: [ AutogeneratedAudioPlayerService, SpeechSynthesisChunkerService, - ContextService - ] + ContextService, + ], }); - speechSynthesisChunkerService = - TestBed.inject(SpeechSynthesisChunkerService); + speechSynthesisChunkerService = TestBed.inject( + SpeechSynthesisChunkerService + ); contextService = TestBed.inject(ContextService); })); describe('if the audio is not playing', () => { beforeEach(() => { - spyOnProperty(window, 'speechSynthesis').and - .returnValue({speaking: false} as SpeechSynthesis); - autogeneratedAudioPlayerService = - TestBed.inject(AutogeneratedAudioPlayerService); + spyOnProperty(window, 'speechSynthesis').and.returnValue({ + speaking: false, + } as SpeechSynthesis); + autogeneratedAudioPlayerService = TestBed.inject( + AutogeneratedAudioPlayerService + ); spyOn(contextService, 'getExplorationId').and.returnValue('exp1'); }); it('should return void when utterance is null', () => { spyOn(autogeneratedAudioPlayerService, '_play').and.callThrough(); - spyOn(speechSynthesisChunkerService, 'speak').and.callFake(( - utterance, audioFinishedCallback: Function) => { - audioFinishedCallback(); - }); + spyOn(speechSynthesisChunkerService, 'speak').and.callFake( + (utterance, audioFinishedCallback: Function) => { + audioFinishedCallback(); + } + ); spyOn(speechSynthesisChunkerService, 'cancel'); spyOn(speechSynthesisChunkerService, 'convertToSpeakableText'); autogeneratedAudioPlayerService.utterance = null; autogeneratedAudioPlayerService.play( - '

test text

', 'en-US', () => {} + '

test text

', + 'en-US', + () => {} ); expect(autogeneratedAudioPlayerService._play).toHaveBeenCalled(); expect(speechSynthesisChunkerService.cancel).toHaveBeenCalled(); - expect(speechSynthesisChunkerService.convertToSpeakableText) - .not.toHaveBeenCalled(); + expect( + speechSynthesisChunkerService.convertToSpeakableText + ).not.toHaveBeenCalled(); }); it('should start playing audio when play button is clicked', () => { spyOn(autogeneratedAudioPlayerService, '_play').and.callThrough(); - spyOn(speechSynthesisChunkerService, 'speak').and.callFake(( - utterance, audioFinishedCallback: Function) => { - audioFinishedCallback(); - }); + spyOn(speechSynthesisChunkerService, 'speak').and.callFake( + (utterance, audioFinishedCallback: Function) => { + audioFinishedCallback(); + } + ); spyOn(speechSynthesisChunkerService, 'cancel'); autogeneratedAudioPlayerService.play( - '

test text

', 'en-US', () => {} + '

test text

', + 'en-US', + () => {} ); expect(autogeneratedAudioPlayerService._play).toHaveBeenCalledWith( - '

test text

', 'en-US', jasmine.any(Function) + '

test text

', + 'en-US', + jasmine.any(Function) ); expect(speechSynthesisChunkerService.cancel).toHaveBeenCalled(); - expect(autogeneratedAudioPlayerService.utterance) - .toBeInstanceOf(SpeechSynthesisUtterance); + expect(autogeneratedAudioPlayerService.utterance).toBeInstanceOf( + SpeechSynthesisUtterance + ); // Value of utteance can be null or object of SpeechSynthesisUtterance, // so to prevent typescript strict check error // "object is possibly null" we imposed an if statement here. if (autogeneratedAudioPlayerService.utterance !== null) { - expect(autogeneratedAudioPlayerService.utterance.text) - .toBe('test text'); + expect(autogeneratedAudioPlayerService.utterance.text).toBe( + 'test text' + ); expect(autogeneratedAudioPlayerService.utterance.lang).toBe('en-US'); expect(autogeneratedAudioPlayerService.utterance.rate).toBeCloseTo( autogeneratedAudioPlayerService.DEFAULT_PLAYBACK_RATE @@ -101,10 +115,10 @@ describe('BannerComponent', () => { autogeneratedAudioPlayerService.DEFAULT_PLAYBACK_VOLUME ); } - expect(speechSynthesisChunkerService.speak) - .toHaveBeenCalledWith( - autogeneratedAudioPlayerService.utterance, jasmine.any(Function) - ); + expect(speechSynthesisChunkerService.speak).toHaveBeenCalledWith( + autogeneratedAudioPlayerService.utterance, + jasmine.any(Function) + ); }); it('should stop playing audio when pause button is clicked', () => { @@ -122,8 +136,9 @@ describe('BannerComponent', () => { // window.speechSynthesis, so to prevent typescript strict check error // "object is possibly null" we imposed an if statement here. if (autogeneratedAudioPlayerService._speechSynthesis !== null) { - expect(autogeneratedAudioPlayerService._speechSynthesis.speaking) - .toBeFalse(); + expect( + autogeneratedAudioPlayerService._speechSynthesis.speaking + ).toBeFalse(); } expect(autogeneratedAudioPlayerService.isPlaying()).toBeFalse(); }); @@ -131,10 +146,12 @@ describe('BannerComponent', () => { describe('if the audio is playing', () => { beforeEach(() => { - spyOnProperty(window, 'speechSynthesis').and - .returnValue({speaking: true} as SpeechSynthesis); - autogeneratedAudioPlayerService = - TestBed.inject(AutogeneratedAudioPlayerService); + spyOnProperty(window, 'speechSynthesis').and.returnValue({ + speaking: true, + } as SpeechSynthesis); + autogeneratedAudioPlayerService = TestBed.inject( + AutogeneratedAudioPlayerService + ); }); it('should return true if auto generated audio is playing', () => { @@ -144,8 +161,9 @@ describe('BannerComponent', () => { // window.speechSynthesis, so to prevent typescript strict check error // "object is possibly null" we imposed an if statement here. if (autogeneratedAudioPlayerService._speechSynthesis !== null) { - expect(autogeneratedAudioPlayerService._speechSynthesis.speaking) - .toBeTrue(); + expect( + autogeneratedAudioPlayerService._speechSynthesis.speaking + ).toBeTrue(); } expect(autogeneratedAudioPlayerService.isPlaying()).toBeTrue(); }); diff --git a/core/templates/services/autogenerated-audio-player.service.ts b/core/templates/services/autogenerated-audio-player.service.ts index 074886f9aa3c..809b55bf5b0c 100644 --- a/core/templates/services/autogenerated-audio-player.service.ts +++ b/core/templates/services/autogenerated-audio-player.service.ts @@ -17,33 +17,41 @@ * using the SpeechSynthesis API. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; -import { SpeechSynthesisChunkerService } from 'services/speech-synthesis-chunker.service'; +import {SpeechSynthesisChunkerService} from 'services/speech-synthesis-chunker.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class AutogeneratedAudioPlayerService { constructor( - private speechSynthesisChunkerService: SpeechSynthesisChunkerService) {} + private speechSynthesisChunkerService: SpeechSynthesisChunkerService + ) {} DEFAULT_PLAYBACK_RATE: number = 0.92; DEFAULT_PLAYBACK_VOLUME: number = 1; // Not all browsers support SpeechSynthesisUtterance, so we need the // check to prevent a "SpeechSynthesisUtterance is not defined" error. - _speechSynthesis: SpeechSynthesis | null = - (window.hasOwnProperty('speechSynthesis')) ? window.speechSynthesis : null; + _speechSynthesis: SpeechSynthesis | null = window.hasOwnProperty( + 'speechSynthesis' + ) + ? window.speechSynthesis + : null; - utterance: SpeechSynthesisUtterance | null = - (window.hasOwnProperty( - 'speechSynthesis')) ? new SpeechSynthesisUtterance() : null; + utterance: SpeechSynthesisUtterance | null = window.hasOwnProperty( + 'speechSynthesis' + ) + ? new SpeechSynthesisUtterance() + : null; _play( - html: string, language: string, - audioFinishedCallback: () => void): void { + html: string, + language: string, + audioFinishedCallback: () => void + ): void { this.speechSynthesisChunkerService.cancel(); if (this.utterance === null) { return; @@ -54,17 +62,16 @@ export class AutogeneratedAudioPlayerService { this.utterance.lang = language; this.utterance.rate = this.DEFAULT_PLAYBACK_RATE; this.utterance.volume = this.DEFAULT_PLAYBACK_VOLUME; - this.speechSynthesisChunkerService.speak( - this.utterance, - () => { - audioFinishedCallback(); - } - ); + this.speechSynthesisChunkerService.speak(this.utterance, () => { + audioFinishedCallback(); + }); } play( - html: string, language: string, - audioFinishedCallback: () => void): void { + html: string, + language: string, + audioFinishedCallback: () => void + ): void { return this._play(html, language, audioFinishedCallback); } @@ -77,6 +84,9 @@ export class AutogeneratedAudioPlayerService { } } -angular.module('oppia').factory( - 'AutogeneratedAudioPlayerService', - downgradeInjectable(AutogeneratedAudioPlayerService)); +angular + .module('oppia') + .factory( + 'AutogeneratedAudioPlayerService', + downgradeInjectable(AutogeneratedAudioPlayerService) + ); diff --git a/core/templates/services/autoplayed-videos.service.spec.ts b/core/templates/services/autoplayed-videos.service.spec.ts index 423df0abc43a..648aeed303c3 100644 --- a/core/templates/services/autoplayed-videos.service.spec.ts +++ b/core/templates/services/autoplayed-videos.service.spec.ts @@ -15,7 +15,7 @@ * @fileoverview Unit tests for AutoplayedVideosService. */ -import { AutoplayedVideosService } from 'services/autoplayed-videos.service'; +import {AutoplayedVideosService} from 'services/autoplayed-videos.service'; describe('AutoplayedVideosService', () => { let autoplayedVideosService: AutoplayedVideosService; @@ -26,12 +26,14 @@ describe('AutoplayedVideosService', () => { it('should add video to a list of autoplayed videos', () => { autoplayedVideosService.addAutoplayedVideo('Ntcw0H0hwPU'); - expect(autoplayedVideosService.hasVideoBeenAutoplayed('Ntcw0H0hwPU')). - toBe(true); + expect(autoplayedVideosService.hasVideoBeenAutoplayed('Ntcw0H0hwPU')).toBe( + true + ); }); it('should test video not yet played', () => { - expect(autoplayedVideosService.hasVideoBeenAutoplayed('Ntcw0H0hwPU')). - toBe(false); + expect(autoplayedVideosService.hasVideoBeenAutoplayed('Ntcw0H0hwPU')).toBe( + false + ); }); }); diff --git a/core/templates/services/autoplayed-videos.service.ts b/core/templates/services/autoplayed-videos.service.ts index 7043ffd47f79..f2d2fd90128e 100644 --- a/core/templates/services/autoplayed-videos.service.ts +++ b/core/templates/services/autoplayed-videos.service.ts @@ -29,14 +29,14 @@ // component and use that id instead to determine whether to suppress // autoplaying. -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class AutoplayedVideosService { - autoplayedVideosDict: { [key: string]: boolean } = {}; + autoplayedVideosDict: {[key: string]: boolean} = {}; /** * Adds video to the autoplayed videos dictionary. @@ -58,5 +58,9 @@ export class AutoplayedVideosService { } } -angular.module('oppia').factory( - 'AutoplayedVideosService', downgradeInjectable(AutoplayedVideosService)); +angular + .module('oppia') + .factory( + 'AutoplayedVideosService', + downgradeInjectable(AutoplayedVideosService) + ); diff --git a/core/templates/services/blog-search.service.spec.ts b/core/templates/services/blog-search.service.spec.ts index eb46b99a5884..4158e553d223 100644 --- a/core/templates/services/blog-search.service.spec.ts +++ b/core/templates/services/blog-search.service.spec.ts @@ -16,14 +16,19 @@ * @fileoverview Tests that blog post search service gets correct results. */ -import { EventEmitter } from '@angular/core'; -import { HttpClientTestingModule } from - '@angular/common/http/testing'; -import { TestBed, fakeAsync, tick } from '@angular/core/testing'; - -import { BlogPostSearchService, UrlSearchQuery } from 'services/blog-search.service'; -import { BlogHomePageBackendApiService, SearchResponseData } from 'domain/blog/blog-homepage-backend-api.service'; -import { Subscription } from 'rxjs'; +import {EventEmitter} from '@angular/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, tick} from '@angular/core/testing'; + +import { + BlogPostSearchService, + UrlSearchQuery, +} from 'services/blog-search.service'; +import { + BlogHomePageBackendApiService, + SearchResponseData, +} from 'domain/blog/blog-homepage-backend-api.service'; +import {Subscription} from 'rxjs'; describe('Blog Post Search Service', () => { let searchService: BlogPostSearchService; @@ -32,14 +37,12 @@ describe('Blog Post Search Service', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [ - BlogPostSearchService, - BlogHomePageBackendApiService, - ] + providers: [BlogPostSearchService, BlogHomePageBackendApiService], }); searchService = TestBed.inject(BlogPostSearchService); blogHomePageBackendApiService = TestBed.inject( - BlogHomePageBackendApiService); + BlogHomePageBackendApiService + ); }); describe('updateSearchFieldsBasedOnUrlQuery', () => { @@ -48,76 +51,63 @@ describe('Blog Post Search Service', () => { // eslint-disable-next-line max-len it('should identify two tags given in url search query', () => { urlSearchQuery = '?q=testBlogSearch&tags=("News"%20OR%20"Mathematics")'; - response = searchService.updateSearchFieldsBasedOnUrlQuery( - urlSearchQuery); + response = + searchService.updateSearchFieldsBasedOnUrlQuery(urlSearchQuery); expect(response.selectedTags.sort()).toEqual(['Mathematics', 'News']); expect(response.searchQuery).toEqual('testBlogSearch'); }); - it('should find one tag if given in url search without query', - () => { - urlSearchQuery = '?q=&tags=("News"%20OR%20"Mathematics")'; - response = searchService.updateSearchFieldsBasedOnUrlQuery( - urlSearchQuery); - expect(response.selectedTags.sort()).toEqual(['Mathematics', 'News']); - expect(response.searchQuery).toBe(''); - } - ); - it('should find no tags if not given in url search', - () => { - urlSearchQuery = '?q=test'; - response = searchService.updateSearchFieldsBasedOnUrlQuery( - urlSearchQuery); - expect(response.selectedTags.length).toBe(0); - expect(response.searchQuery).toEqual('test'); - } - ); + it('should find one tag if given in url search without query', () => { + urlSearchQuery = '?q=&tags=("News"%20OR%20"Mathematics")'; + response = + searchService.updateSearchFieldsBasedOnUrlQuery(urlSearchQuery); + expect(response.selectedTags.sort()).toEqual(['Mathematics', 'News']); + expect(response.searchQuery).toBe(''); + }); + it('should find no tags if not given in url search', () => { + urlSearchQuery = '?q=test'; + response = + searchService.updateSearchFieldsBasedOnUrlQuery(urlSearchQuery); + expect(response.selectedTags.length).toBe(0); + expect(response.searchQuery).toEqual('test'); + }); - it('should find as many keywords as provided in search query', - () => { - urlSearchQuery = '?q=protractor%20blog%20test&tags=("News"%20OR%20' + - '"Mathematics")'; - response = searchService.updateSearchFieldsBasedOnUrlQuery( - urlSearchQuery); - expect(response.selectedTags.sort()).toEqual(['Mathematics', 'News']); - expect(response.searchQuery).toEqual('protractor blog test'); - } - ); + it('should find as many keywords as provided in search query', () => { + urlSearchQuery = + '?q=protractor%20blog%20test&tags=("News"%20OR%20' + '"Mathematics")'; + response = + searchService.updateSearchFieldsBasedOnUrlQuery(urlSearchQuery); + expect(response.selectedTags.sort()).toEqual(['Mathematics', 'News']); + expect(response.searchQuery).toEqual('protractor blog test'); + }); - it('should not find tags when ampersand is escaped', - () => { - urlSearchQuery = '?q=protractor%20test%26tags=("Mathematics")'; - response = searchService.updateSearchFieldsBasedOnUrlQuery( - urlSearchQuery); - expect(response.selectedTags.length).toBe(0); - expect(response.searchQuery).toEqual( - 'protractor test&tags=("Mathematics")'); - } - ); + it('should not find tags when ampersand is escaped', () => { + urlSearchQuery = '?q=protractor%20test%26tags=("Mathematics")'; + response = + searchService.updateSearchFieldsBasedOnUrlQuery(urlSearchQuery); + expect(response.selectedTags.length).toBe(0); + expect(response.searchQuery).toEqual( + 'protractor test&tags=("Mathematics")' + ); + }); - it('should only use correct fields when ampersand is not escaped anywhere', - () => { - urlSearchQuery = '?q=protractor&test&tags=("Mathematics")'; - response = searchService.updateSearchFieldsBasedOnUrlQuery( - urlSearchQuery); - expect(response.selectedTags).toEqual(['Mathematics']); - expect(response.searchQuery).toEqual( - 'protractor'); - } - ); + it('should only use correct fields when ampersand is not escaped anywhere', () => { + urlSearchQuery = '?q=protractor&test&tags=("Mathematics")'; + response = + searchService.updateSearchFieldsBasedOnUrlQuery(urlSearchQuery); + expect(response.selectedTags).toEqual(['Mathematics']); + expect(response.searchQuery).toEqual('protractor'); + }); - it('should omit url component if it is malformed', - () => { - // In the search query below tags param are not wrapped in parentheses. - // updateSearchFieldsBasedOnUrlQuery is expected to clean tags from url. - urlSearchQuery = ( - '?q=protractor%20test&tags="Mathematics"'); - response = searchService.updateSearchFieldsBasedOnUrlQuery( - urlSearchQuery); - expect(response.selectedTags.length).toBe(0); - expect(response.searchQuery).toEqual('protractor test'); - } - ); + it('should omit url component if it is malformed', () => { + // In the search query below tags param are not wrapped in parentheses. + // updateSearchFieldsBasedOnUrlQuery is expected to clean tags from url. + urlSearchQuery = '?q=protractor%20test&tags="Mathematics"'; + response = + searchService.updateSearchFieldsBasedOnUrlQuery(urlSearchQuery); + expect(response.selectedTags.length).toBe(0); + expect(response.searchQuery).toEqual('protractor test'); + }); }); describe('getSearchUrlQueryString', () => { @@ -126,26 +116,35 @@ describe('Blog Post Search Service', () => { const tags = ['tag1', 'tag2']; expect(searchService.getSearchUrlQueryString(searchQuery, tags)).toBe( - 'blog%20search&tags=("tag1" OR "tag2")'); + 'blog%20search&tags=("tag1" OR "tag2")' + ); }); - it('should successfully get search url query string when there is no' + - 'tags query params', () => { - const searchQuery: string = 'blog search'; - const tags: string[] = []; + it( + 'should successfully get search url query string when there is no' + + 'tags query params', + () => { + const searchQuery: string = 'blog search'; + const tags: string[] = []; - expect(searchService.getSearchUrlQueryString(searchQuery, tags)).toBe( - 'blog%20search'); - }); + expect(searchService.getSearchUrlQueryString(searchQuery, tags)).toBe( + 'blog%20search' + ); + } + ); - it('should successfully get search url query string when there is no' + - ' query params', () => { - const searchQuery = ''; - const tags = ['tag1', 'tag2']; + it( + 'should successfully get search url query string when there is no' + + ' query params', + () => { + const searchQuery = ''; + const tags = ['tag1', 'tag2']; - expect(searchService.getSearchUrlQueryString(searchQuery, tags)).toBe( - '&tags=("tag1" OR "tag2")'); - }); + expect(searchService.getSearchUrlQueryString(searchQuery, tags)).toBe( + '&tags=("tag1" OR "tag2")' + ); + } + ); }); describe('executeSearchQuery', () => { @@ -160,7 +159,8 @@ describe('Blog Post Search Service', () => { searchQuery = 'example'; tags = ['tag1', 'tag2']; initialSearchResultsLoadedSpy = jasmine.createSpy( - 'initialSearchResultsLoadedSpy'); + 'initialSearchResultsLoadedSpy' + ); successHandler = jasmine.createSpy('success'); errorHandler = jasmine.createSpy('error'); sampleResponse = { @@ -172,44 +172,54 @@ describe('Blog Post Search Service', () => { testSubscriptions.add( searchService.onInitialSearchResultsLoaded.subscribe( initialSearchResultsLoadedSpy - )); + ) + ); }); it('should successfully execute search query', fakeAsync(() => { - spyOn(blogHomePageBackendApiService, 'fetchBlogPostSearchResultAsync') - .and.returnValue(Promise.resolve(sampleResponse)); + spyOn( + blogHomePageBackendApiService, + 'fetchBlogPostSearchResultAsync' + ).and.returnValue(Promise.resolve(sampleResponse)); - searchService.executeSearchQuery( - searchQuery, tags, () => {}); + searchService.executeSearchQuery(searchQuery, tags, () => {}); expect(searchService.isSearchInProgress()).toBe(true); tick(); - expect(blogHomePageBackendApiService.fetchBlogPostSearchResultAsync) - .toHaveBeenCalled(); + expect( + blogHomePageBackendApiService.fetchBlogPostSearchResultAsync + ).toHaveBeenCalled(); expect(searchService.isSearchInProgress()).toBe(false); expect(initialSearchResultsLoadedSpy).toHaveBeenCalled(); })); - it('should use reject handler if fetching data fails while search exec', - fakeAsync(() => { - spyOn(blogHomePageBackendApiService, 'fetchBlogPostSearchResultAsync') - .and.returnValue(Promise.reject({ - error: {error: 'Some error in the backend.'} - })); + it('should use reject handler if fetching data fails while search exec', fakeAsync(() => { + spyOn( + blogHomePageBackendApiService, + 'fetchBlogPostSearchResultAsync' + ).and.returnValue( + Promise.reject({ + error: {error: 'Some error in the backend.'}, + }) + ); - searchService.executeSearchQuery( - searchQuery, tags, () => {}, errorHandler); - expect(searchService.isSearchInProgress()).toBe(true); + searchService.executeSearchQuery( + searchQuery, + tags, + () => {}, + errorHandler + ); + expect(searchService.isSearchInProgress()).toBe(true); - tick(); + tick(); - expect(searchService.isSearchInProgress()).toBe(false); - expect(blogHomePageBackendApiService.fetchBlogPostSearchResultAsync) - .toHaveBeenCalled(); - expect(errorHandler).toHaveBeenCalledWith( - 'Some error in the backend.'); - })); + expect(searchService.isSearchInProgress()).toBe(false); + expect( + blogHomePageBackendApiService.fetchBlogPostSearchResultAsync + ).toHaveBeenCalled(); + expect(errorHandler).toHaveBeenCalledWith('Some error in the backend.'); + })); describe('loadMoreData', () => { let moreSampleResponse: SearchResponseData = { @@ -219,11 +229,13 @@ describe('Blog Post Search Service', () => { }; it('should successfully load more data', fakeAsync(() => { - spyOn(blogHomePageBackendApiService, 'fetchBlogPostSearchResultAsync') - .and.returnValues( - Promise.resolve(sampleResponse), - Promise.resolve(moreSampleResponse) - ); + spyOn( + blogHomePageBackendApiService, + 'fetchBlogPostSearchResultAsync' + ).and.returnValues( + Promise.resolve(sampleResponse), + Promise.resolve(moreSampleResponse) + ); searchService.executeSearchQuery(searchQuery, tags, () => {}); tick(); @@ -236,50 +248,52 @@ describe('Blog Post Search Service', () => { expect(errorHandler).not.toHaveBeenCalled(); })); - it('should not load more data when a new query is still being sent', - fakeAsync(() => { - spyOn(blogHomePageBackendApiService, 'fetchBlogPostSearchResultAsync') - .and.returnValues( - Promise.resolve(sampleResponse), - Promise.resolve(moreSampleResponse) - ); - - searchService.executeSearchQuery(searchQuery, tags, () => {}); - tick(); - expect(initialSearchResultsLoadedSpy).toHaveBeenCalled(); - - searchService.loadMoreData(successHandler); - searchService.loadMoreData(successHandler, errorHandler); - tick(); - expect(errorHandler).toHaveBeenCalledWith(false); - })); - - it('should not load more data when the end of page has been reached', - fakeAsync(() => { - let lastSampleResponse: SearchResponseData = { - searchOffset: null, - blogPostSummariesList: [], - listOfDefaultTags: [], - }; - spyOn(blogHomePageBackendApiService, 'fetchBlogPostSearchResultAsync') - .and.returnValues( - Promise.resolve(sampleResponse), - Promise.resolve(lastSampleResponse), - ); - - searchService.executeSearchQuery(searchQuery, tags, () => {}); - tick(); - expect(initialSearchResultsLoadedSpy).toHaveBeenCalled(); - - searchService.loadMoreData(successHandler); - tick(); - expect(successHandler).toHaveBeenCalledWith(lastSampleResponse); - expect(errorHandler).not.toHaveBeenCalled(); - - searchService.loadMoreData(successHandler, errorHandler); - tick(); - expect(errorHandler).toHaveBeenCalledWith(true); - })); + it('should not load more data when a new query is still being sent', fakeAsync(() => { + spyOn( + blogHomePageBackendApiService, + 'fetchBlogPostSearchResultAsync' + ).and.returnValues( + Promise.resolve(sampleResponse), + Promise.resolve(moreSampleResponse) + ); + + searchService.executeSearchQuery(searchQuery, tags, () => {}); + tick(); + expect(initialSearchResultsLoadedSpy).toHaveBeenCalled(); + + searchService.loadMoreData(successHandler); + searchService.loadMoreData(successHandler, errorHandler); + tick(); + expect(errorHandler).toHaveBeenCalledWith(false); + })); + + it('should not load more data when the end of page has been reached', fakeAsync(() => { + let lastSampleResponse: SearchResponseData = { + searchOffset: null, + blogPostSummariesList: [], + listOfDefaultTags: [], + }; + spyOn( + blogHomePageBackendApiService, + 'fetchBlogPostSearchResultAsync' + ).and.returnValues( + Promise.resolve(sampleResponse), + Promise.resolve(lastSampleResponse) + ); + + searchService.executeSearchQuery(searchQuery, tags, () => {}); + tick(); + expect(initialSearchResultsLoadedSpy).toHaveBeenCalled(); + + searchService.loadMoreData(successHandler); + tick(); + expect(successHandler).toHaveBeenCalledWith(lastSampleResponse); + expect(errorHandler).not.toHaveBeenCalled(); + + searchService.loadMoreData(successHandler, errorHandler); + tick(); + expect(errorHandler).toHaveBeenCalledWith(true); + })); }); }); diff --git a/core/templates/services/blog-search.service.ts b/core/templates/services/blog-search.service.ts index d49be52a066b..859e9281496b 100644 --- a/core/templates/services/blog-search.service.ts +++ b/core/templates/services/blog-search.service.ts @@ -16,10 +16,13 @@ * @fileoverview Search service for Blog Posts. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable, EventEmitter } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable, EventEmitter} from '@angular/core'; -import { BlogHomePageBackendApiService, SearchResponseData } from 'domain/blog/blog-homepage-backend-api.service'; +import { + BlogHomePageBackendApiService, + SearchResponseData, +} from 'domain/blog/blog-homepage-backend-api.service'; import cloneDeep from 'lodash/cloneDeep'; export interface UrlSearchQuery { @@ -28,7 +31,7 @@ export interface UrlSearchQuery { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class BlogPostSearchService { // These properties are initialized using functions @@ -45,7 +48,7 @@ export class BlogPostSearchService { public numSearchesInProgress = 0; constructor( - private _blogHomePageBackendApiService: BlogHomePageBackendApiService, + private _blogHomePageBackendApiService: BlogHomePageBackendApiService ) {} private _getSuffixForQuery(selectedTags: string[]): string { @@ -76,64 +79,73 @@ export class BlogPostSearchService { const EXPECTED_PREFIX = '=("'; const EXPECTED_SUFFIX = '")'; - if (!itemCodes || - itemCodes.indexOf(EXPECTED_PREFIX) !== 0 || - itemCodes.lastIndexOf(EXPECTED_SUFFIX) !== - itemCodes.length - EXPECTED_SUFFIX.length || - itemCodes.lastIndexOf(EXPECTED_SUFFIX) === -1) { + if ( + !itemCodes || + itemCodes.indexOf(EXPECTED_PREFIX) !== 0 || + itemCodes.lastIndexOf(EXPECTED_SUFFIX) !== + itemCodes.length - EXPECTED_SUFFIX.length || + itemCodes.lastIndexOf(EXPECTED_SUFFIX) === -1 + ) { throw new Error( 'Invalid search query url fragment for ' + - itemsType + ': ' + urlComponent); + itemsType + + ': ' + + urlComponent + ); } - const items = itemCodes.substring( - EXPECTED_PREFIX.length, itemCodes.length - EXPECTED_SUFFIX.length - ).split('" OR "'); + const items = itemCodes + .substring( + EXPECTED_PREFIX.length, + itemCodes.length - EXPECTED_SUFFIX.length + ) + .split('" OR "'); return items; } - getQueryUrl(searchUrlQueryString: string): string { return '?q=' + searchUrlQueryString; } - getSearchUrlQueryString( - searchQuery: string, - selectedTags: string[] - ): string { - return encodeURIComponent(searchQuery) + - this._getSuffixForQuery(selectedTags); + getSearchUrlQueryString(searchQuery: string, selectedTags: string[]): string { + return ( + encodeURIComponent(searchQuery) + this._getSuffixForQuery(selectedTags) + ); } - // Note that an empty query results in all blog posts being shown. executeSearchQuery( - searchQuery: string, - selectedTags: string[], - successCallback: () => void, - errorCallback?: (reason: string) => void): void { + searchQuery: string, + selectedTags: string[], + successCallback: () => void, + errorCallback?: (reason: string) => void + ): void { const queryUrl = this.getQueryUrl( - this.getSearchUrlQueryString(searchQuery, selectedTags)); + this.getSearchUrlQueryString(searchQuery, selectedTags) + ); this._isCurrentlyFetchingResults = true; this.numSearchesInProgress++; - this._blogHomePageBackendApiService.fetchBlogPostSearchResultAsync( - queryUrl - ).then((response: SearchResponseData) => { - this._lastQuery = searchQuery; - this._lastSelectedTags = cloneDeep(selectedTags); - this._searchOffset = response.searchOffset; - this.numSearchesInProgress--; - - this._initialSearchResultsLoadedEventEmitter.emit(response); - - this._isCurrentlyFetchingResults = false; - }, (error) => { - this.numSearchesInProgress--; - if (errorCallback) { - errorCallback(error.error.error); - } - }); + this._blogHomePageBackendApiService + .fetchBlogPostSearchResultAsync(queryUrl) + .then( + (response: SearchResponseData) => { + this._lastQuery = searchQuery; + this._lastSelectedTags = cloneDeep(selectedTags); + this._searchOffset = response.searchOffset; + this.numSearchesInProgress--; + + this._initialSearchResultsLoadedEventEmitter.emit(response); + + this._isCurrentlyFetchingResults = false; + }, + error => { + this.numSearchesInProgress--; + if (errorCallback) { + errorCallback(error.error.error); + } + } + ); if (successCallback) { successCallback(); @@ -144,8 +156,7 @@ export class BlogPostSearchService { return this.numSearchesInProgress > 0; } - updateSearchFieldsBasedOnUrlQuery( - urlComponent: string): UrlSearchQuery { + updateSearchFieldsBasedOnUrlQuery(urlComponent: string): UrlSearchQuery { let newSearchQuery: UrlSearchQuery = { searchQuery: '', selectedTags: [], @@ -178,17 +189,15 @@ export class BlogPostSearchService { getCurrentUrlQueryString(): string { return this.getSearchUrlQueryString( this._lastQuery, - this._lastSelectedTags, + this._lastSelectedTags ); } // Here failure callback is optional so that it gets invoked // only when the end of page has reached and return void otherwise. loadMoreData( - successCallback: ( - SearchResponseData: SearchResponseData - ) => void, - failureCallback?: (arg0: boolean) => void + successCallback: (SearchResponseData: SearchResponseData) => void, + failureCallback?: (arg0: boolean) => void ): void { // If a new query is still being sent, or the last page has been // reached, do not fetch more results. @@ -206,24 +215,23 @@ export class BlogPostSearchService { } this._isCurrentlyFetchingResults = true; - this._blogHomePageBackendApiService.fetchBlogPostSearchResultAsync( - queryUrl - ).then((data: SearchResponseData) => { - this._searchOffset = data.searchOffset; - this._isCurrentlyFetchingResults = false; - - if (successCallback) { - successCallback(data); - } - }); + this._blogHomePageBackendApiService + .fetchBlogPostSearchResultAsync(queryUrl) + .then((data: SearchResponseData) => { + this._searchOffset = data.searchOffset; + this._isCurrentlyFetchingResults = false; + + if (successCallback) { + successCallback(data); + } + }); } get onSearchBarLoaded(): EventEmitter { return this._searchBarLoadedEventEmitter; } - get onInitialSearchResultsLoaded(): - EventEmitter { + get onInitialSearchResultsLoaded(): EventEmitter { return this._initialSearchResultsLoadedEventEmitter; } @@ -232,7 +240,6 @@ export class BlogPostSearchService { } } -angular.module('oppia').factory( - 'BlogPostSearchService', - downgradeInjectable(BlogPostSearchService) -); +angular + .module('oppia') + .factory('BlogPostSearchService', downgradeInjectable(BlogPostSearchService)); diff --git a/core/templates/services/bottom-navbar-status.service.spec.ts b/core/templates/services/bottom-navbar-status.service.spec.ts index 473e0cdb428f..233e64cb0430 100644 --- a/core/templates/services/bottom-navbar-status.service.spec.ts +++ b/core/templates/services/bottom-navbar-status.service.spec.ts @@ -16,25 +16,25 @@ * @fileoverview Tests for BottomNavbarStatusService. */ -import { TestBed } from '@angular/core/testing'; +import {TestBed} from '@angular/core/testing'; -import { BottomNavbarStatusService } from - 'services/bottom-navbar-status.service'; -import { WindowDimensionsService } from - 'services/contextual/window-dimensions.service'; +import {BottomNavbarStatusService} from 'services/bottom-navbar-status.service'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; describe('BottomNavbarStatusService', () => { let bss: BottomNavbarStatusService; beforeEach(() => { TestBed.configureTestingModule({ - providers: [{ - provide: WindowDimensionsService, - useValue: { - isWindowNarrow: () => true, - getWidth: () => 800 - } - }] + providers: [ + { + provide: WindowDimensionsService, + useValue: { + isWindowNarrow: () => true, + getWidth: () => 800, + }, + }, + ], }); bss = TestBed.get(BottomNavbarStatusService); diff --git a/core/templates/services/bottom-navbar-status.service.ts b/core/templates/services/bottom-navbar-status.service.ts index 66c824f5b55d..55ae9201e079 100644 --- a/core/templates/services/bottom-navbar-status.service.ts +++ b/core/templates/services/bottom-navbar-status.service.ts @@ -17,15 +17,13 @@ * bottom navigation bar. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; - -import { WindowDimensionsService } from - 'services/contextual/window-dimensions.service'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class BottomNavbarStatusService { bottomNavbarIsEnabled: boolean = false; @@ -38,9 +36,14 @@ export class BottomNavbarStatusService { isBottomNavbarEnabled(): boolean { return ( this.bottomNavbarIsEnabled && - this.windowDimensionsService.getWidth() < 1000); + this.windowDimensionsService.getWidth() < 1000 + ); } } -angular.module('oppia').factory( - 'BottomNavbarStatusService', downgradeInjectable(BottomNavbarStatusService)); +angular + .module('oppia') + .factory( + 'BottomNavbarStatusService', + downgradeInjectable(BottomNavbarStatusService) + ); diff --git a/core/templates/services/classifier-data-backend-api.service.spec.ts b/core/templates/services/classifier-data-backend-api.service.spec.ts index 2159f47cdbff..e8e36227b5fd 100644 --- a/core/templates/services/classifier-data-backend-api.service.spec.ts +++ b/core/templates/services/classifier-data-backend-api.service.spec.ts @@ -16,13 +16,15 @@ * @fileoverview Unit tests for ClassifierDataBackendApiService */ -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; -import { deflateSync } from 'zlib'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; +import {deflateSync} from 'zlib'; -import { AppConstants } from 'app.constants'; -import { ClassifierDataBackendApiService } from - 'services/classifier-data-backend-api.service'; +import {AppConstants} from 'app.constants'; +import {ClassifierDataBackendApiService} from 'services/classifier-data-backend-api.service'; describe('Classifier Data Backend API Service', () => { describe('on dev mode', () => { @@ -30,18 +32,20 @@ describe('Classifier Data Backend API Service', () => { let httpTestingController: HttpTestingController; const classifierMetaDataRequestUrl = '/ml/trainedclassifierhandler'; - const classifierDataRequestUrl = ( - '/_ah/gcs/' + AppConstants.GCS_RESOURCE_BUCKET_NAME + - '/exploration/0/assets/classifier.pb.xz'); + const classifierDataRequestUrl = + '/_ah/gcs/' + + AppConstants.GCS_RESOURCE_BUCKET_NAME + + '/exploration/0/assets/classifier.pb.xz'; const classifierBuffer = deflateSync(Buffer.alloc(10)); beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [ClassifierDataBackendApiService] + providers: [ClassifierDataBackendApiService], }); classifierDataBackendApiService = TestBed.inject( - ClassifierDataBackendApiService); + ClassifierDataBackendApiService + ); httpTestingController = TestBed.inject(HttpTestingController); }); @@ -53,104 +57,120 @@ describe('Classifier Data Backend API Service', () => { const successHandler = jasmine.createSpy('success'); const failHandler = jasmine.createSpy('fail'); - classifierDataBackendApiService.getClassifierDataAsync( - '0', 1, 'state').then( - successHandler, failHandler); + classifierDataBackendApiService + .getClassifierDataAsync('0', 1, 'state') + .then(successHandler, failHandler); const metaDataReq = httpTestingController.expectOne( - req => req.url === classifierMetaDataRequestUrl); + req => req.url === classifierMetaDataRequestUrl + ); expect(metaDataReq.request.method).toEqual('GET'); expect(metaDataReq.request.params.get('exploration_id')).toEqual('0'); - expect(metaDataReq.request.params.get( - 'exploration_version')).toEqual('1'); + expect(metaDataReq.request.params.get('exploration_version')).toEqual( + '1' + ); expect(metaDataReq.request.params.get('state_name')).toEqual('state'); metaDataReq.flush({ algorithm_id: 'TextClassifier', algorithm_version: 0, - gcs_filename: 'classifier.pb.xz' + gcs_filename: 'classifier.pb.xz', }); flushMicrotasks(); const classifierDataReq = httpTestingController.expectOne( - classifierDataRequestUrl); + classifierDataRequestUrl + ); expect(classifierDataReq.request.method).toEqual('GET'); classifierDataReq.flush( classifierBuffer.buffer.slice( classifierBuffer.byteOffset, - classifierBuffer.byteOffset + classifierBuffer.byteLength)); + classifierBuffer.byteOffset + classifierBuffer.byteLength + ) + ); flushMicrotasks(); expect(successHandler).toHaveBeenCalled(); expect(failHandler).not.toHaveBeenCalled(); })); - it('should handle rejection when fetching meta data fails', - fakeAsync(() => { - const successHandler = jasmine.createSpy('success'); - const failHandler = jasmine.createSpy('fail'); - - classifierDataBackendApiService.getClassifierDataAsync( - '0', 1, 'state').then( - successHandler, failHandler); - const req = httpTestingController.expectOne( - req => req.url === classifierMetaDataRequestUrl); - expect(req.request.method).toEqual('GET'); - req.flush('', {status: 400, statusText: 'Failed'}); - flushMicrotasks(); - expect(successHandler).not.toHaveBeenCalled(); - expect(failHandler).toHaveBeenCalled(); - })); - - it('should handle rejection when fetching classifier data fails', - fakeAsync(() => { - const successHandler = jasmine.createSpy('success'); - const failHandler = jasmine.createSpy('fail'); - - classifierDataBackendApiService.getClassifierDataAsync( - '0', 1, 'state').then( - successHandler, failHandler); - - const metaDataReq = httpTestingController.expectOne( - req => req.url === classifierMetaDataRequestUrl); - metaDataReq.flush({ - algorithm_id: 'TextClassifier', - algorithm_version: 0, - gcs_filename: 'classifier.pb.xz' - }); - flushMicrotasks(); - - const classifierDataReq = httpTestingController.expectOne( - classifierDataRequestUrl); - expect(classifierDataReq.request.method).toEqual('GET'); - classifierDataReq.flush( - classifierBuffer.buffer.slice( - classifierBuffer.byteOffset, - classifierBuffer.byteOffset + classifierBuffer.byteLength), - {status: 400, statusText: 'Failed'}); - flushMicrotasks(); - expect(successHandler).not.toHaveBeenCalled(); - expect(failHandler).toHaveBeenCalled(); - })); + it('should handle rejection when fetching meta data fails', fakeAsync(() => { + const successHandler = jasmine.createSpy('success'); + const failHandler = jasmine.createSpy('fail'); + + classifierDataBackendApiService + .getClassifierDataAsync('0', 1, 'state') + .then(successHandler, failHandler); + const req = httpTestingController.expectOne( + req => req.url === classifierMetaDataRequestUrl + ); + expect(req.request.method).toEqual('GET'); + req.flush('', {status: 400, statusText: 'Failed'}); + flushMicrotasks(); + expect(successHandler).not.toHaveBeenCalled(); + expect(failHandler).toHaveBeenCalled(); + })); + + it('should handle rejection when fetching classifier data fails', fakeAsync(() => { + const successHandler = jasmine.createSpy('success'); + const failHandler = jasmine.createSpy('fail'); + + classifierDataBackendApiService + .getClassifierDataAsync('0', 1, 'state') + .then(successHandler, failHandler); + + const metaDataReq = httpTestingController.expectOne( + req => req.url === classifierMetaDataRequestUrl + ); + metaDataReq.flush({ + algorithm_id: 'TextClassifier', + algorithm_version: 0, + gcs_filename: 'classifier.pb.xz', + }); + flushMicrotasks(); + + const classifierDataReq = httpTestingController.expectOne( + classifierDataRequestUrl + ); + expect(classifierDataReq.request.method).toEqual('GET'); + classifierDataReq.flush( + classifierBuffer.buffer.slice( + classifierBuffer.byteOffset, + classifierBuffer.byteOffset + classifierBuffer.byteLength + ), + {status: 400, statusText: 'Failed'} + ); + flushMicrotasks(); + expect(successHandler).not.toHaveBeenCalled(); + expect(failHandler).toHaveBeenCalled(); + })); }); describe('without dev mode settings', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [ClassifierDataBackendApiService] + providers: [ClassifierDataBackendApiService], }); - spyOnProperty(ClassifierDataBackendApiService, 'DEV_MODE', 'get') - .and.returnValue(false); spyOnProperty( - ClassifierDataBackendApiService, 'GCS_RESOURCE_BUCKET_NAME', 'get') - .and.returnValue(''); + ClassifierDataBackendApiService, + 'DEV_MODE', + 'get' + ).and.returnValue(false); + spyOnProperty( + ClassifierDataBackendApiService, + 'GCS_RESOURCE_BUCKET_NAME', + 'get' + ).and.returnValue(''); }); - it('should throw an error when is not on dev mode and Google Cloud' + - ' Service bucket name is not set', fakeAsync(() => { - expect(() => { - TestBed.inject(ClassifierDataBackendApiService); - }).toThrowError('GCS_RESOURCE_BUCKET_NAME is not set in prod.'); - })); + it( + 'should throw an error when is not on dev mode and Google Cloud' + + ' Service bucket name is not set', + fakeAsync(() => { + expect(() => { + TestBed.inject(ClassifierDataBackendApiService); + }).toThrowError('GCS_RESOURCE_BUCKET_NAME is not set in prod.'); + }) + ); }); }); diff --git a/core/templates/services/classifier-data-backend-api.service.ts b/core/templates/services/classifier-data-backend-api.service.ts index dc6bbd430183..065e63b7bef7 100644 --- a/core/templates/services/classifier-data-backend-api.service.ts +++ b/core/templates/services/classifier-data-backend-api.service.ts @@ -17,21 +17,21 @@ * data file name from backend. */ -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; -import { Buffer } from 'buffer'; -import { unzipSync } from 'zlib'; +import {Buffer} from 'buffer'; +import {unzipSync} from 'zlib'; -import { AppConstants } from 'app.constants'; -import { Classifier } from 'domain/classifier/classifier.model'; -import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; +import {AppConstants} from 'app.constants'; +import {Classifier} from 'domain/classifier/classifier.model'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; interface ClassifierMetaDataBackendDict { - 'algorithm_id': string; - 'algorithm_version': number; - 'gcs_filename': string; + algorithm_id: string; + algorithm_version: number; + gcs_filename: string; } export interface ClassifierMetaData { @@ -41,25 +41,29 @@ export interface ClassifierMetaData { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ClassifierDataBackendApiService { private readonly classifierDataDownloadUrlTemplate: string; constructor( - private http: HttpClient, - private urlInterpolationService: UrlInterpolationService) { + private http: HttpClient, + private urlInterpolationService: UrlInterpolationService + ) { if ( !ClassifierDataBackendApiService.DEV_MODE && - !ClassifierDataBackendApiService.GCS_RESOURCE_BUCKET_NAME) { + !ClassifierDataBackendApiService.GCS_RESOURCE_BUCKET_NAME + ) { throw new Error('GCS_RESOURCE_BUCKET_NAME is not set in prod.'); } - const urlPrefix = ClassifierDataBackendApiService.DEV_MODE ? - '/_ah/gcs/' : 'https://storage.googleapis.com/'; - this.classifierDataDownloadUrlTemplate = ( - urlPrefix + ClassifierDataBackendApiService.GCS_RESOURCE_BUCKET_NAME + - '///assets/'); + const urlPrefix = ClassifierDataBackendApiService.DEV_MODE + ? '/_ah/gcs/' + : 'https://storage.googleapis.com/'; + this.classifierDataDownloadUrlTemplate = + urlPrefix + + ClassifierDataBackendApiService.GCS_RESOURCE_BUCKET_NAME + + '///assets/'; } static get DEV_MODE(): boolean { @@ -71,68 +75,102 @@ export class ClassifierDataBackendApiService { } private _getDownloadUrl( - entityType: string, entityId: string, filename: string): string { + entityType: string, + entityId: string, + filename: string + ): string { return this.urlInterpolationService.interpolateUrl( - this.classifierDataDownloadUrlTemplate, { + this.classifierDataDownloadUrlTemplate, + { entity_type: entityType, entity_id: entityId, filename: filename, - }); + } + ); } private async getClassifierMetadataAsync( - explorationId: string, explorationVersion: number, - stateName: string): Promise { + explorationId: string, + explorationVersion: number, + stateName: string + ): Promise { return new Promise((resolve, reject) => { - this.http.get( - '/ml/trainedclassifierhandler', { + this.http + .get('/ml/trainedclassifierhandler', { params: { exploration_id: explorationId, exploration_version: explorationVersion.toString(), - state_name: stateName + state_name: stateName, }, - responseType: 'json' - }).toPromise().then(response => { - resolve({ - algorithmId: response.algorithm_id, - algorithmVersion: response.algorithm_version, - filename: response.gcs_filename - }); - }, errorResponse => { - reject(errorResponse); - }); + responseType: 'json', + }) + .toPromise() + .then( + response => { + resolve({ + algorithmId: response.algorithm_id, + algorithmVersion: response.algorithm_version, + filename: response.gcs_filename, + }); + }, + errorResponse => { + reject(errorResponse); + } + ); }); } async getClassifierDataAsync( - explorationId: string, explorationVersion: number, - stateName: string): Promise { + explorationId: string, + explorationVersion: number, + stateName: string + ): Promise { return new Promise((resolve, reject) => { this.getClassifierMetadataAsync( - explorationId, explorationVersion, stateName).then( + explorationId, + explorationVersion, + stateName + ).then( response => { let classifierMetaData = response; - this.http.get( - this._getDownloadUrl( - AppConstants.ENTITY_TYPE.EXPLORATION, explorationId, - response.filename), { - responseType: 'arraybuffer' - }).toPromise().then(response => { - resolve(new Classifier( - classifierMetaData.algorithmId, - unzipSync(Buffer.from(response)), - classifierMetaData.algorithmVersion - )); - }, classifierErrorResponse => { - reject(classifierErrorResponse); - }); - }, classifierMetadataErrorResponse => { + this.http + .get( + this._getDownloadUrl( + AppConstants.ENTITY_TYPE.EXPLORATION, + explorationId, + response.filename + ), + { + responseType: 'arraybuffer', + } + ) + .toPromise() + .then( + response => { + resolve( + new Classifier( + classifierMetaData.algorithmId, + unzipSync(Buffer.from(response)), + classifierMetaData.algorithmVersion + ) + ); + }, + classifierErrorResponse => { + reject(classifierErrorResponse); + } + ); + }, + classifierMetadataErrorResponse => { reject(classifierMetadataErrorResponse); - }); + } + ); }); } } -angular.module('oppia').factory( - 'ClassifierDataBackendApiService', - downgradeInjectable(ClassifierDataBackendApiService)); +angular + .module('oppia') + .factory( + 'ClassifierDataBackendApiService', + downgradeInjectable(ClassifierDataBackendApiService) + ); diff --git a/core/templates/services/code-normalizer.service.spec.ts b/core/templates/services/code-normalizer.service.spec.ts index 345a46994164..93b0ea97e072 100644 --- a/core/templates/services/code-normalizer.service.spec.ts +++ b/core/templates/services/code-normalizer.service.spec.ts @@ -16,7 +16,7 @@ * @fileoverview Unit tests for the Code Normalizer Service. */ -import { CodeNormalizerService } from 'services/code-normalizer.service'; +import {CodeNormalizerService} from 'services/code-normalizer.service'; describe('Code Normalization', () => { let cns: CodeNormalizerService; @@ -25,99 +25,102 @@ describe('Code Normalization', () => { }); it('should not modify contents of code', () => { - expect(cns.getNormalizedCode( - 'def x():\n' + - ' y = 345' - )).toBe( - 'def x():\n' + - ' y = 345' + expect(cns.getNormalizedCode('def x():\n' + ' y = 345')).toBe( + 'def x():\n' + ' y = 345' ); }); - it('should convert indentation to 4 spaces, remove trailing whitespace ' + - 'and empty lines', () => { - expect(cns.getNormalizedCode( - 'def x(): \n' + - ' \n' + - ' y = 345\n' + - ' \n' + - ' ' - )).toBe( - 'def x():\n' + - ' y = 345' - ); - }); + it( + 'should convert indentation to 4 spaces, remove trailing whitespace ' + + 'and empty lines', + () => { + expect( + cns.getNormalizedCode( + 'def x(): \n' + + ' \n' + + ' y = 345\n' + + ' \n' + + ' ' + ) + ).toBe('def x():\n' + ' y = 345'); + } + ); - it('should remove full-line comments, but not comments in the middle ' + - 'of a line', () => { - expect(cns.getNormalizedCode( - '# This is a comment.\n' + - ' # This is a comment with some spaces before it.\n' + - 'def x(): # And a comment with some code before it.\n' + - ' y = \'#string with hashes#\'' - )).toBe( - 'def x(): # And a comment with some code before it.\n' + - ' y = \'#string with hashes#\'' - ); - }); + it( + 'should remove full-line comments, but not comments in the middle ' + + 'of a line', + () => { + expect( + cns.getNormalizedCode( + '# This is a comment.\n' + + ' # This is a comment with some spaces before it.\n' + + 'def x(): # And a comment with some code before it.\n' + + " y = '#string with hashes#'" + ) + ).toBe( + 'def x(): # And a comment with some code before it.\n' + + " y = '#string with hashes#'" + ); + } + ); it('should handle complex indentation', () => { - expect(cns.getNormalizedCode( - 'abcdefg\n' + - ' hij\n' + - ' ppppp\n' + - 'x\n' + - ' abc\n' + - ' abc\n' + - ' bcd\n' + - ' cde\n' + - ' xxxxx\n' + - ' y\n' + - ' z' - )).toBe( + expect( + cns.getNormalizedCode( + 'abcdefg\n' + + ' hij\n' + + ' ppppp\n' + + 'x\n' + + ' abc\n' + + ' abc\n' + + ' bcd\n' + + ' cde\n' + + ' xxxxx\n' + + ' y\n' + + ' z' + ) + ).toBe( 'abcdefg\n' + - ' hij\n' + - ' ppppp\n' + - 'x\n' + - ' abc\n' + - ' abc\n' + - ' bcd\n' + - ' cde\n' + - ' xxxxx\n' + - ' y\n' + - 'z' + ' hij\n' + + ' ppppp\n' + + 'x\n' + + ' abc\n' + + ' abc\n' + + ' bcd\n' + + ' cde\n' + + ' xxxxx\n' + + ' y\n' + + 'z' ); }); it('should handle shortfall lines', () => { - expect(cns.getNormalizedCode( + expect( + cns.getNormalizedCode( + 'abcdefg\n' + + ' hij\n' + + ' ppppp\n' + + ' x\n' + + ' abc\n' + + ' bcd\n' + + ' cde' + ) + ).toBe( 'abcdefg\n' + - ' hij\n' + - ' ppppp\n' + - ' x\n' + - ' abc\n' + - ' bcd\n' + - ' cde' - )).toBe( - 'abcdefg\n' + - ' hij\n' + - ' ppppp\n' + - ' x\n' + - 'abc\n' + - ' bcd\n' + - 'cde' + ' hij\n' + + ' ppppp\n' + + ' x\n' + + 'abc\n' + + ' bcd\n' + + 'cde' ); }); it('should normalize multiple spaces within a line', () => { - expect(cns.getNormalizedCode( - 'abcdefg\n' + - ' hij klm\n' + - ' ab "cde fgh"\n' - )).toBe( - 'abcdefg\n' + - ' hij klm\n' + - ' ab "cde fgh"' - ); + expect( + cns.getNormalizedCode( + 'abcdefg\n' + ' hij klm\n' + ' ab "cde fgh"\n' + ) + ).toBe('abcdefg\n' + ' hij klm\n' + ' ab "cde fgh"'); }); }); diff --git a/core/templates/services/code-normalizer.service.ts b/core/templates/services/code-normalizer.service.ts index 0b0d2d31ecf4..b3b0b4821385 100644 --- a/core/templates/services/code-normalizer.service.ts +++ b/core/templates/services/code-normalizer.service.ts @@ -17,11 +17,11 @@ * and pencil code interactions. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class CodeNormalizerService { removeLeadingWhitespace(str: string): string { @@ -55,8 +55,8 @@ export class CodeNormalizerService { var FOUR_SPACES = ' '; // Maps the number of spaces at the beginning of a line to an int // specifying the desired indentation level. - var numSpacesToDesiredIndentLevel: { [key: number]: number } = { - 0: 0 + var numSpacesToDesiredIndentLevel: {[key: number]: number} = { + 0: 0, }; var codeLines = this.removeTrailingWhitespace(codeString).split('\n'); @@ -72,9 +72,9 @@ export class CodeNormalizerService { var numSpaces = line.length - this.removeLeadingWhitespace(line).length; - var existingNumSpaces = Object.keys( - numSpacesToDesiredIndentLevel - ).map(Number); + var existingNumSpaces = Object.keys(numSpacesToDesiredIndentLevel).map( + Number + ); var maxNumSpaces = Math.max.apply(null, existingNumSpaces); if (numSpaces > maxNumSpaces) { // Add a new indentation level. @@ -99,9 +99,9 @@ export class CodeNormalizerService { } if (isShortfallLine) { - existingNumSpaces = Object.keys( - numSpacesToDesiredIndentLevel - ).map(Number); + existingNumSpaces = Object.keys(numSpacesToDesiredIndentLevel).map( + Number + ); numSpaces = Math.max.apply(null, existingNumSpaces); } @@ -110,12 +110,14 @@ export class CodeNormalizerService { normalizedLine += FOUR_SPACES; } normalizedLine += this.removeIntermediateWhitespace( - this.removeLeadingWhitespace(line)); + this.removeLeadingWhitespace(line) + ); normalizedCodeLines.push(normalizedLine); }); return normalizedCodeLines.join('\n'); } } -angular.module('oppia').factory( - 'CodeNormalizerService', downgradeInjectable(CodeNormalizerService)); +angular + .module('oppia') + .factory('CodeNormalizerService', downgradeInjectable(CodeNormalizerService)); diff --git a/core/templates/services/compute-graph.service.ts b/core/templates/services/compute-graph.service.ts index 9c0fa1e2a0b8..e372762cb6a6 100644 --- a/core/templates/services/compute-graph.service.ts +++ b/core/templates/services/compute-graph.service.ts @@ -17,10 +17,10 @@ * exploration. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { States } from 'domain/exploration/StatesObjectFactory'; +import {States} from 'domain/exploration/StatesObjectFactory'; export interface GraphLink { source: string; @@ -41,7 +41,7 @@ export interface GraphData { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ComputeGraphService { _computeGraphData(initStateId: string, states: States): GraphData { @@ -49,7 +49,7 @@ export class ComputeGraphService { let links: GraphLink[] = []; let finalStateIds = states.getFinalStateNames(); - states.getStateNames().forEach(function(stateName) { + states.getStateNames().forEach(function (stateName) { let interaction = states.getState(stateName).interaction; nodes[stateName] = stateName; if (interaction.id) { @@ -59,7 +59,7 @@ export class ComputeGraphService { source: stateName, target: groups[h].outcome.dest, linkProperty: null, - connectsDestIfStuck: false + connectsDestIfStuck: false, }); if (groups[h].outcome.destIfReallyStuck) { links.push({ @@ -72,7 +72,7 @@ export class ComputeGraphService { // @ts-ignore target: groups[h].outcome.destIfReallyStuck, linkProperty: null, - connectsDestIfStuck: true + connectsDestIfStuck: true, }); } } @@ -82,14 +82,14 @@ export class ComputeGraphService { source: stateName, target: interaction.defaultOutcome.dest, linkProperty: null, - connectsDestIfStuck: false + connectsDestIfStuck: false, }); if (interaction.defaultOutcome.destIfReallyStuck) { links.push({ source: stateName, target: interaction.defaultOutcome.destIfReallyStuck, linkProperty: null, - connectsDestIfStuck: true + connectsDestIfStuck: true, }); } } @@ -99,12 +99,15 @@ export class ComputeGraphService { finalStateIds: finalStateIds, initStateId: initStateId, links: links, - nodes: nodes + nodes: nodes, }; } _computeBfsTraversalOfStates( - initStateId: string, states: States, sourceStateName: string): string[] { + initStateId: string, + states: States, + sourceStateName: string + ): string[] { let stateGraph = this._computeGraphData(initStateId, states); let stateNamesInBfsOrder: string[] = []; let queue: string[] = []; @@ -134,11 +137,18 @@ export class ComputeGraphService { } computeBfsTraversalOfStates( - initStateId: string, states: States, sourceStateName: string): string[] { + initStateId: string, + states: States, + sourceStateName: string + ): string[] { return this._computeBfsTraversalOfStates( - initStateId, states, sourceStateName); + initStateId, + states, + sourceStateName + ); } } -angular.module('oppia').factory( - 'ComputeGraphService', downgradeInjectable(ComputeGraphService)); +angular + .module('oppia') + .factory('ComputeGraphService', downgradeInjectable(ComputeGraphService)); diff --git a/core/templates/services/construct-translation-ids.service.spec.ts b/core/templates/services/construct-translation-ids.service.spec.ts index fb2aa5aee71d..bf903675fbe2 100644 --- a/core/templates/services/construct-translation-ids.service.spec.ts +++ b/core/templates/services/construct-translation-ids.service.spec.ts @@ -16,9 +16,8 @@ * @fileoverview Unit tests for ConstructTranslationIdsService. */ -import { TestBed } from '@angular/core/testing'; -import { ConstructTranslationIdsService } from - 'services/construct-translation-ids.service'; +import {TestBed} from '@angular/core/testing'; +import {ConstructTranslationIdsService} from 'services/construct-translation-ids.service'; describe('Construct Translation Ids Service', () => { let ctis: ConstructTranslationIdsService; @@ -28,22 +27,23 @@ describe('Construct Translation Ids Service', () => { }); it('should get library id', () => { - expect(ctis.getLibraryId('categories', 'Algorithms')) - .toBe('I18N_LIBRARY_CATEGORIES_ALGORITHMS'); + expect(ctis.getLibraryId('categories', 'Algorithms')).toBe( + 'I18N_LIBRARY_CATEGORIES_ALGORITHMS' + ); expect(ctis.getLibraryId('', '')).toBe('I18N_LIBRARY__'); }); it('should get classroom title id', () => { - expect(ctis.getClassroomTitleId('math')) - .toBe('I18N_CLASSROOM_MATH_TITLE'); + expect(ctis.getClassroomTitleId('math')).toBe('I18N_CLASSROOM_MATH_TITLE'); expect(ctis.getClassroomTitleId('')).toBe('I18N_CLASSROOM__TITLE'); }); it('should get syllabus type title id', () => { - expect(ctis.getSyllabusTypeTitleId('skill')) - .toBe('I18N_SYLLABUS_SKILL_TITLE'); + expect(ctis.getSyllabusTypeTitleId('skill')).toBe( + 'I18N_SYLLABUS_SKILL_TITLE' + ); expect(ctis.getSyllabusTypeTitleId('')).toBe('I18N_SYLLABUS__TITLE'); }); diff --git a/core/templates/services/construct-translation-ids.service.ts b/core/templates/services/construct-translation-ids.service.ts index db50d395de75..228f3a25d29e 100755 --- a/core/templates/services/construct-translation-ids.service.ts +++ b/core/templates/services/construct-translation-ids.service.ts @@ -16,33 +16,37 @@ * @fileoverview Service to dynamically construct translation ids for i18n. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ConstructTranslationIdsService { // Construct a translation id for library from name and a prefix. // Ex: 'categories', 'art' -> 'I18N_LIBRARY_CATEGORIES_ART'. getLibraryId(prefix: string, name: string): string { return ( - 'I18N_LIBRARY_' + prefix.toUpperCase() + '_' + - name.toUpperCase().split(' ').join('_')); + 'I18N_LIBRARY_' + + prefix.toUpperCase() + + '_' + + name.toUpperCase().split(' ').join('_') + ); } // Construct a translation id for a classroom title from name. getClassroomTitleId(name: string): string { - return ( - 'I18N_CLASSROOM_' + name.toUpperCase() + '_TITLE'); + return 'I18N_CLASSROOM_' + name.toUpperCase() + '_TITLE'; } getSyllabusTypeTitleId(name: string): string { - return ( - 'I18N_SYLLABUS_' + name.toUpperCase() + '_TITLE'); + return 'I18N_SYLLABUS_' + name.toUpperCase() + '_TITLE'; } } -angular.module('oppia').factory( - 'ConstructTranslationIdsService', - downgradeInjectable(ConstructTranslationIdsService)); +angular + .module('oppia') + .factory( + 'ConstructTranslationIdsService', + downgradeInjectable(ConstructTranslationIdsService) + ); diff --git a/core/templates/services/context.service.spec.ts b/core/templates/services/context.service.spec.ts index ef9ebfe1648d..5757a90ab9fe 100644 --- a/core/templates/services/context.service.spec.ts +++ b/core/templates/services/context.service.spec.ts @@ -17,15 +17,14 @@ * editor page. */ -import { TestBed } from '@angular/core/testing'; +import {TestBed} from '@angular/core/testing'; -import { ContextService } from 'services/context.service'; -import { UrlService } from 'services/contextual/url.service'; -import { BlogPostPageService } from 'pages/blog-post-page/services/blog-post-page.service'; +import {ContextService} from 'services/context.service'; +import {UrlService} from 'services/contextual/url.service'; +import {BlogPostPageService} from 'pages/blog-post-page/services/blog-post-page.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; class MockWindowRef { _window = { @@ -43,8 +42,8 @@ class MockWindowRef { }, set href(val) { this._href = val; - } - } + }, + }, }; get nativeWindow() { @@ -110,7 +109,8 @@ describe('Context service', () => { ecs = TestBed.get(ContextService); urlService = TestBed.get(UrlService); spyOn(urlService, 'getPathname').and.returnValue( - '/embed/exploration/123'); + '/embed/exploration/123' + ); spyOn(urlService, 'getHash').and.returnValue(''); ecs.removeCustomEntityContext(); }); @@ -172,10 +172,9 @@ describe('Context service', () => { expect(ecs.getEntityType()).toBe('exploration'); }); - it('should correctly check that page allows editing of RTE components', - () => { - expect(ecs.canAddOrEditComponents()).toBe(true); - }); + it('should correctly check that page allows editing of RTE components', () => { + expect(ecs.canAddOrEditComponents()).toBe(true); + }); }); describe('behavior in the topic editor view', () => { @@ -212,19 +211,21 @@ describe('Context service', () => { expect(ecs.getPageContext()).toBe('topic_editor'); }); - it('should correctly check that page allows editing of RTE components', - () => { - expect(ecs.canAddOrEditComponents()).toBe(false); - spyOn(urlService, 'getPathname').and.returnValue('/topic_editor/123'); - expect(ecs.canAddOrEditComponents()).toBe(true); - }); - - it('should not report exploration context when the context' + - ' is not related to editor or player', ()=> { - expect(ecs.getPageContext()).toBe('other'); + it('should correctly check that page allows editing of RTE components', () => { + expect(ecs.canAddOrEditComponents()).toBe(false); spyOn(urlService, 'getPathname').and.returnValue('/topic_editor/123'); - expect(ecs.isInExplorationContext()).toBe(false); + expect(ecs.canAddOrEditComponents()).toBe(true); }); + + it( + 'should not report exploration context when the context' + + ' is not related to editor or player', + () => { + expect(ecs.getPageContext()).toBe('other'); + spyOn(urlService, 'getPathname').and.returnValue('/topic_editor/123'); + expect(ecs.isInExplorationContext()).toBe(false); + } + ); }); describe('behavior in question editor modal', () => { @@ -250,19 +251,17 @@ describe('Context service', () => { expect(ecs.getEntityId()).toBe('questionId'); }); - it('should affirm the exploration context for exploration player', - ()=> { - expect(ecs.isInExplorationContext()).toBe(false); - spyOn(urlService, 'getPathname').and.returnValue('/explore/123'); - expect(ecs.isInExplorationContext()).toBe(true); - }); + it('should affirm the exploration context for exploration player', () => { + expect(ecs.isInExplorationContext()).toBe(false); + spyOn(urlService, 'getPathname').and.returnValue('/explore/123'); + expect(ecs.isInExplorationContext()).toBe(true); + }); - it('should affirm the exploration context for exploration editor', - ()=> { - expect(ecs.isInExplorationContext()).toBe(false); - spyOn(urlService, 'getPathname').and.returnValue('/create/123'); - expect(ecs.isInExplorationContext()).toBe(true); - }); + it('should affirm the exploration context for exploration editor', () => { + expect(ecs.isInExplorationContext()).toBe(false); + spyOn(urlService, 'getPathname').and.returnValue('/create/123'); + expect(ecs.isInExplorationContext()).toBe(true); + }); }); describe('behavior in the story editor view', () => { @@ -298,12 +297,11 @@ describe('Context service', () => { expect(ecs.getPageContext()).toBe('story_editor'); }); - it('should correctly check that page allows editing of RTE components', - () => { - expect(ecs.canAddOrEditComponents()).toBe(false); - spyOn(urlService, 'getPathname').and.returnValue('/story_editor/123'); - expect(ecs.canAddOrEditComponents()).toBe(true); - }); + it('should correctly check that page allows editing of RTE components', () => { + expect(ecs.canAddOrEditComponents()).toBe(false); + spyOn(urlService, 'getPathname').and.returnValue('/story_editor/123'); + expect(ecs.canAddOrEditComponents()).toBe(true); + }); }); describe('behavior in the skill editor view', () => { @@ -340,12 +338,11 @@ describe('Context service', () => { expect(ecs.getPageContext()).toBe('skill_editor'); }); - it('should correctly check that page allows editing of RTE components', - () => { - expect(ecs.canAddOrEditComponents()).toBe(false); - spyOn(urlService, 'getPathname').and.returnValue('/skill_editor/123'); - expect(ecs.canAddOrEditComponents()).toBe(true); - }); + it('should correctly check that page allows editing of RTE components', () => { + expect(ecs.canAddOrEditComponents()).toBe(false); + spyOn(urlService, 'getPathname').and.returnValue('/skill_editor/123'); + expect(ecs.canAddOrEditComponents()).toBe(true); + }); }); describe('behavior in the blog dashboard page', () => { @@ -358,11 +355,9 @@ describe('Context service', () => { it('should correctly retrieve the blog post id', () => { expect(ecs.getEntityId()).toBe('undefined'); - spyOn(urlService, 'getPathname').and.returnValue( - '/blog-dashboard'); + spyOn(urlService, 'getPathname').and.returnValue('/blog-dashboard'); spyOn(urlService, 'getHash').and.returnValue(''); - spyOn(urlService, 'getBlogPostIdFromUrl').and.returnValue( - 'sample123456'); + spyOn(urlService, 'getBlogPostIdFromUrl').and.returnValue('sample123456'); expect(ecs.getEntityId()).toBe('sample123456'); }); @@ -370,8 +365,7 @@ describe('Context service', () => { it('should correctly retrieve the entity type', () => { expect(ecs.getEntityType()).toBeUndefined(); - spyOn(urlService, 'getPathname').and.returnValue( - '/blog-dashboard'); + spyOn(urlService, 'getPathname').and.returnValue('/blog-dashboard'); expect(ecs.getEntityType()).toBe('blog_post'); }); @@ -379,27 +373,23 @@ describe('Context service', () => { it('should correctly retrieve the page context', () => { expect(ecs.getPageContext()).toBe('other'); - spyOn(urlService, 'getPathname').and.returnValue( - '/blog-dashboard'); + spyOn(urlService, 'getPathname').and.returnValue('/blog-dashboard'); expect(ecs.getPageContext()).toBe('blog_dashboard'); }); - it('should correctly check that page allows editing of RTE components', - () => { - expect(ecs.canAddOrEditComponents()).toBe(false); + it('should correctly check that page allows editing of RTE components', () => { + expect(ecs.canAddOrEditComponents()).toBe(false); - spyOn(urlService, 'getPathname').and.returnValue( - '/blog-dashboard'); + spyOn(urlService, 'getPathname').and.returnValue('/blog-dashboard'); - expect(ecs.canAddOrEditComponents()).toBe(true); - }); + expect(ecs.canAddOrEditComponents()).toBe(true); + }); it('should check if rte is in blog post editor', () => { expect(ecs.isInBlogPostEditorPage()).toBe(false); - spyOn(urlService, 'getPathname').and.returnValue( - '/blog-dashboard'); + spyOn(urlService, 'getPathname').and.returnValue('/blog-dashboard'); expect(ecs.isInBlogPostEditorPage()).toBe(true); }); @@ -416,8 +406,7 @@ describe('Context service', () => { it('should correctly retrieve the entity type', () => { expect(ecs.getEntityType()).toBeUndefined(); - spyOn(urlService, 'getPathname').and.returnValue( - '/blog'); + spyOn(urlService, 'getPathname').and.returnValue('/blog'); expect(ecs.getEntityType()).toBe('blog_post'); }); @@ -425,8 +414,7 @@ describe('Context service', () => { it('should correctly retrieve the blog post id', () => { expect(ecs.getEntityId()).toBe('undefined'); - spyOn(urlService, 'getPathname').and.returnValue( - '/blog'); + spyOn(urlService, 'getPathname').and.returnValue('/blog'); spyOn(urlService, 'getHash').and.returnValue(''); blogPostPageService.blogPostId = 'sample123456'; @@ -440,7 +428,7 @@ describe('Context service', () => { TestBed.configureTestingModule({ providers: [ UrlInterpolationService, - { provide: WindowRef, useValue: windowRef }, + {provide: WindowRef, useValue: windowRef}, ], }); ecs = TestBed.get(ContextService); @@ -450,13 +438,15 @@ describe('Context service', () => { it('should correctly retrieve the learner group id', () => { spyOn(urlService, 'getPathname').and.returnValue( - '/edit-learner-group/groupId'); + '/edit-learner-group/groupId' + ); expect(ecs.getLearnerGroupId()).toBe('groupId'); }); it('should correctly retrieve the page context', () => { spyOn(urlService, 'getPathname').and.returnValue( - '/edit-learner-group/groupId'); + '/edit-learner-group/groupId' + ); expect(ecs.getPageContext()).toBe('learner_group_editor'); }); @@ -473,7 +463,8 @@ describe('Context service', () => { ecs = TestBed.get(ContextService); urlService = TestBed.get(UrlService); spyOn(urlService, 'getPathname').and.returnValue( - '/learner-group/groupId'); + '/learner-group/groupId' + ); ecs.removeCustomEntityContext(); }); @@ -501,29 +492,33 @@ describe('Context service', () => { expect(ecs.getPageContext()).toBe('question_player'); }); - it('should correctly retrieve the page context as collection editor', - () => { - expect(ecs.getPageContext()).toBe('other'); - spyOn(urlService, 'getPathname').and.returnValue( - '/collection_editor/123'); - expect(ecs.getPageContext()).toBe('collection_editor'); - }); - - it('should correctly retrieve the page context as ' + - 'topics and skills dashboard', () => { + it('should correctly retrieve the page context as collection editor', () => { expect(ecs.getPageContext()).toBe('other'); spyOn(urlService, 'getPathname').and.returnValue( - '/topics-and-skills-dashboard/123'); - expect(ecs.getPageContext()).toBe('topics_and_skills_dashboard'); + '/collection_editor/123' + ); + expect(ecs.getPageContext()).toBe('collection_editor'); }); - it('should correctly retrieve the page context as contributor dashboard', + it( + 'should correctly retrieve the page context as ' + + 'topics and skills dashboard', () => { expect(ecs.getPageContext()).toBe('other'); spyOn(urlService, 'getPathname').and.returnValue( - '/contributor-dashboard/123'); - expect(ecs.getPageContext()).toBe('contributor_dashboard'); - }); + '/topics-and-skills-dashboard/123' + ); + expect(ecs.getPageContext()).toBe('topics_and_skills_dashboard'); + } + ); + + it('should correctly retrieve the page context as contributor dashboard', () => { + expect(ecs.getPageContext()).toBe('other'); + spyOn(urlService, 'getPathname').and.returnValue( + '/contributor-dashboard/123' + ); + expect(ecs.getPageContext()).toBe('contributor_dashboard'); + }); }); describe('behavior in other pages', () => { @@ -534,26 +529,23 @@ describe('Context service', () => { ecs.removeCustomEntityContext(); }); - it('should throw an error when trying to retrieve the exploration id', - () => { - expect(() => ecs.getExplorationId()).toThrowError( - 'ContextService should not be used outside the ' + - 'context of an exploration or a question.'); - } - ); + it('should throw an error when trying to retrieve the exploration id', () => { + expect(() => ecs.getExplorationId()).toThrowError( + 'ContextService should not be used outside the ' + + 'context of an exploration or a question.' + ); + }); - it('should throw an error when trying to retrieve the learner group id', - () => { - expect(() => ecs.getLearnerGroupId()).toThrowError( - 'ContextService should not be used outside the ' + - 'context of a learner group.'); - } - ); + it('should throw an error when trying to retrieve the learner group id', () => { + expect(() => ecs.getLearnerGroupId()).toThrowError( + 'ContextService should not be used outside the ' + + 'context of a learner group.' + ); + }); it('should retrieve other as page context', () => { expect(ecs.getPageContext()).toBe('other'); - } - ); + }); it('should detect editor tab context is preview', () => { let urlServiceGetHash = spyOn(urlService, 'getHash'); @@ -587,7 +579,7 @@ describe('Context service', () => { TestBed.configureTestingModule({ providers: [ UrlInterpolationService, - { provide: WindowRef, useValue: windowRef }, + {provide: WindowRef, useValue: windowRef}, ], }); ecs = TestBed.get(ContextService); diff --git a/core/templates/services/context.service.ts b/core/templates/services/context.service.ts index 94987634cd95..455e24f31a46 100644 --- a/core/templates/services/context.service.ts +++ b/core/templates/services/context.service.ts @@ -17,17 +17,17 @@ * context. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { EntityContext } from 'domain/utilities/entity-context.model'; -import { ServicesConstants } from 'services/services.constants'; -import { UrlService } from 'services/contextual/url.service'; -import { BlogPostPageService } from 'pages/blog-post-page/services/blog-post-page.service'; +import {AppConstants} from 'app.constants'; +import {EntityContext} from 'domain/utilities/entity-context.model'; +import {ServicesConstants} from 'services/services.constants'; +import {UrlService} from 'services/contextual/url.service'; +import {BlogPostPageService} from 'pages/blog-post-page/services/blog-post-page.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ContextService { constructor( @@ -44,8 +44,8 @@ export class ContextService { // it (using the appropriate reset fn) initially. Since these are static, // depending on the order of tests, values may be retained across tests. static customEntityContext: EntityContext | null = null; - static imageSaveDestination: string = ( - AppConstants.IMAGE_SAVE_DESTINATION_SERVER); + static imageSaveDestination: string = + AppConstants.IMAGE_SAVE_DESTINATION_SERVER; // Page Context is null initially when no shared service exist. pageContext: string | null = null; @@ -98,10 +98,12 @@ export class ContextService { } else { let pathnameArray = this.urlService.getPathname().split('/'); for (let i = 0; i < pathnameArray.length; i++) { - if (pathnameArray[i] === 'explore' || - pathnameArray[i] === 'lesson' || - (pathnameArray[i] === 'embed' && - pathnameArray[i + 1] === 'exploration')) { + if ( + pathnameArray[i] === 'explore' || + pathnameArray[i] === 'lesson' || + (pathnameArray[i] === 'embed' && + pathnameArray[i + 1] === 'exploration') + ) { this.pageContext = ServicesConstants.PAGE_CONTEXT.EXPLORATION_PLAYER; return ServicesConstants.PAGE_CONTEXT.EXPLORATION_PLAYER; } else if (pathnameArray[i] === 'create') { @@ -121,27 +123,27 @@ export class ContextService { return ServicesConstants.PAGE_CONTEXT.SKILL_EDITOR; } else if ( pathnameArray[i] === 'session' || - pathnameArray[i] === 'review-test') { + pathnameArray[i] === 'review-test' + ) { this.pageContext = ServicesConstants.PAGE_CONTEXT.QUESTION_PLAYER; return ServicesConstants.PAGE_CONTEXT.QUESTION_PLAYER; } else if (pathnameArray[i] === 'collection_editor') { this.pageContext = ServicesConstants.PAGE_CONTEXT.COLLECTION_EDITOR; return ServicesConstants.PAGE_CONTEXT.COLLECTION_EDITOR; } else if (pathnameArray[i] === 'topics-and-skills-dashboard') { - this.pageContext = ( - ServicesConstants.PAGE_CONTEXT.TOPICS_AND_SKILLS_DASHBOARD); + this.pageContext = + ServicesConstants.PAGE_CONTEXT.TOPICS_AND_SKILLS_DASHBOARD; return ServicesConstants.PAGE_CONTEXT.TOPICS_AND_SKILLS_DASHBOARD; } else if (pathnameArray[i] === 'contributor-dashboard') { - this.pageContext = ( - ServicesConstants.PAGE_CONTEXT.CONTRIBUTOR_DASHBOARD); + this.pageContext = + ServicesConstants.PAGE_CONTEXT.CONTRIBUTOR_DASHBOARD; return ServicesConstants.PAGE_CONTEXT.CONTRIBUTOR_DASHBOARD; } else if (pathnameArray[i] === 'blog-dashboard') { - this.pageContext = ( - ServicesConstants.PAGE_CONTEXT.BLOG_DASHBOARD); + this.pageContext = ServicesConstants.PAGE_CONTEXT.BLOG_DASHBOARD; return ServicesConstants.PAGE_CONTEXT.BLOG_DASHBOARD; } else if (pathnameArray[i] === 'edit-learner-group') { - this.pageContext = ( - ServicesConstants.PAGE_CONTEXT.LEARNER_GROUP_EDITOR); + this.pageContext = + ServicesConstants.PAGE_CONTEXT.LEARNER_GROUP_EDITOR; return ServicesConstants.PAGE_CONTEXT.LEARNER_GROUP_EDITOR; } } @@ -175,9 +177,10 @@ export class ContextService { isInExplorationContext(): boolean { return ( this.getPageContext() === - ServicesConstants.PAGE_CONTEXT.EXPLORATION_EDITOR || + ServicesConstants.PAGE_CONTEXT.EXPLORATION_EDITOR || this.getPageContext() === - ServicesConstants.PAGE_CONTEXT.EXPLORATION_PLAYER); + ServicesConstants.PAGE_CONTEXT.EXPLORATION_PLAYER + ); } // This function is used in cases where the URL does not specify the @@ -185,7 +188,9 @@ export class ContextService { // any page via the RTE. setCustomEntityContext(entityType: string, entityId: string): void { ContextService.customEntityContext = new EntityContext( - entityId, entityType); + entityId, + entityType + ); } removeCustomEntityContext(): void { @@ -223,11 +228,12 @@ export class ContextService { let pathnameArray = this.urlService.getPathname().split('/'); let hashValues = this.urlService.getHash().split('#'); for (let i = 0; i < pathnameArray.length; i++) { - if (pathnameArray[i] === 'create' || - pathnameArray[i] === 'explore' || - pathnameArray[i] === 'lesson' || - (pathnameArray[i] === 'embed' && - pathnameArray[i + 1] === 'exploration')) { + if ( + pathnameArray[i] === 'create' || + pathnameArray[i] === 'explore' || + pathnameArray[i] === 'lesson' || + (pathnameArray[i] === 'embed' && pathnameArray[i + 1] === 'exploration') + ) { return AppConstants.ENTITY_TYPE.EXPLORATION; } if (pathnameArray[i] === 'topic_editor') { @@ -267,10 +273,12 @@ export class ContextService { // /create/{exploration_id} or /embed/exploration/{exploration_id}. let pathnameArray = this.urlService.getPathname().split('/'); for (let i = 0; i < pathnameArray.length; i++) { - if (pathnameArray[i] === 'explore' || - pathnameArray[i] === 'create' || - pathnameArray[i] === 'skill_editor' || - pathnameArray[i] === 'lesson') { + if ( + pathnameArray[i] === 'explore' || + pathnameArray[i] === 'create' || + pathnameArray[i] === 'skill_editor' || + pathnameArray[i] === 'lesson' + ) { this.explorationId = pathnameArray[i + 1]; return pathnameArray[i + 1]; } @@ -282,7 +290,7 @@ export class ContextService { } throw new Error( 'ContextService should not be used outside the ' + - 'context of an exploration or a question.' + 'context of an exploration or a question.' ); } @@ -296,15 +304,17 @@ export class ContextService { // /learner-group/{group_id}. let pathnameArray = this.urlService.getPathname().split('/'); for (let i = 0; i < pathnameArray.length; i++) { - if (pathnameArray[i] === 'edit-learner-group' || - pathnameArray[i] === 'learner-group') { + if ( + pathnameArray[i] === 'edit-learner-group' || + pathnameArray[i] === 'learner-group' + ) { this.learnerGroupId = pathnameArray[i + 1]; return pathnameArray[i + 1]; } } throw new Error( 'ContextService should not be used outside the ' + - 'context of a learner group.' + 'context of a learner group.' ); } @@ -313,34 +323,37 @@ export class ContextService { isInExplorationEditorMode(): boolean { return ( this.getPageContext() === - ServicesConstants.PAGE_CONTEXT.EXPLORATION_EDITOR && - this.getEditorTabContext() === ( - ServicesConstants.EXPLORATION_EDITOR_TAB_CONTEXT.EDITOR)); + ServicesConstants.PAGE_CONTEXT.EXPLORATION_EDITOR && + this.getEditorTabContext() === + ServicesConstants.EXPLORATION_EDITOR_TAB_CONTEXT.EDITOR + ); } isInQuestionPlayerMode(): boolean { return ( this.getPageContext() === ServicesConstants.PAGE_CONTEXT.QUESTION_PLAYER || - this.questionPlayerIsManuallySet); + this.questionPlayerIsManuallySet + ); } isInExplorationPlayerPage(): boolean { return ( this.getPageContext() === - ServicesConstants.PAGE_CONTEXT.EXPLORATION_PLAYER); + ServicesConstants.PAGE_CONTEXT.EXPLORATION_PLAYER + ); } isInExplorationEditorPage(): boolean { return ( this.getPageContext() === - ServicesConstants.PAGE_CONTEXT.EXPLORATION_EDITOR); + ServicesConstants.PAGE_CONTEXT.EXPLORATION_EDITOR + ); } isInBlogPostEditorPage(): boolean { return ( - this.getPageContext() === - ServicesConstants.PAGE_CONTEXT.BLOG_DASHBOARD + this.getPageContext() === ServicesConstants.PAGE_CONTEXT.BLOG_DASHBOARD ); } @@ -357,18 +370,18 @@ export class ContextService { ServicesConstants.PAGE_CONTEXT.CONTRIBUTOR_DASHBOARD, ServicesConstants.PAGE_CONTEXT.BLOG_DASHBOARD, ]; - return (allowedPageContext.includes(currentPageContext)); + return allowedPageContext.includes(currentPageContext); } // Sets the current context to save images to the server. resetImageSaveDestination(): void { - ContextService.imageSaveDestination = ( - AppConstants.IMAGE_SAVE_DESTINATION_SERVER); + ContextService.imageSaveDestination = + AppConstants.IMAGE_SAVE_DESTINATION_SERVER; } setImageSaveDestinationToLocalStorage(): void { - ContextService.imageSaveDestination = ( - AppConstants.IMAGE_SAVE_DESTINATION_LOCAL_STORAGE); + ContextService.imageSaveDestination = + AppConstants.IMAGE_SAVE_DESTINATION_LOCAL_STORAGE; } getImageSaveDestination(): string { @@ -376,5 +389,6 @@ export class ContextService { } } -angular.module('oppia').factory( - 'ContextService', downgradeInjectable(ContextService)); +angular + .module('oppia') + .factory('ContextService', downgradeInjectable(ContextService)); diff --git a/core/templates/services/contextual/device-info.service.spec.ts b/core/templates/services/contextual/device-info.service.spec.ts index 36491da5c8cd..89e405b65971 100644 --- a/core/templates/services/contextual/device-info.service.spec.ts +++ b/core/templates/services/contextual/device-info.service.spec.ts @@ -16,19 +16,20 @@ * @fileoverview Unit tests for DeviceInfoService. */ -import { TestBed } from '@angular/core/testing'; +import {TestBed} from '@angular/core/testing'; -import { DeviceInfoService } from - 'services/contextual/device-info.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import {DeviceInfoService} from 'services/contextual/device-info.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; describe('Device Info Service', () => { let dis: DeviceInfoService; let wrs: WindowRef; - const mobileUserAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like ' + + const mobileUserAgent = + 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like ' + 'Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 ' + 'Mobile/15A372 Safari/604.1'; - const desktopUserAgent = 'Mozilla/5.0 (Windows NT 6.1; WOW64) ' + + const desktopUserAgent = + 'Mozilla/5.0 (Windows NT 6.1; WOW64) ' + 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 ' + 'Safari/537.36'; @@ -38,18 +39,24 @@ describe('Device Info Service', () => { }); it('should evaluate when a device is a mobile device', () => { - spyOnProperty(wrs.nativeWindow, 'navigator').and.callFake(() => ({ - userAgent: mobileUserAgent - } as Navigator)); + spyOnProperty(wrs.nativeWindow, 'navigator').and.callFake( + () => + ({ + userAgent: mobileUserAgent, + }) as Navigator + ); expect(dis.isMobileDevice()).toBe(true); expect(dis.isMobileUserAgent()).toBe(true); }); it('should evaluate when a device is not a mobile device', () => { - spyOnProperty(wrs.nativeWindow, 'navigator').and.callFake(() => ({ - userAgent: desktopUserAgent - } as Navigator)); + spyOnProperty(wrs.nativeWindow, 'navigator').and.callFake( + () => + ({ + userAgent: desktopUserAgent, + }) as Navigator + ); expect(dis.isMobileDevice()).toBe(false); expect(dis.isMobileUserAgent()).toBe(false); diff --git a/core/templates/services/contextual/device-info.service.ts b/core/templates/services/contextual/device-info.service.ts index c6438a7b1b4b..9da8ff12ae8d 100644 --- a/core/templates/services/contextual/device-info.service.ts +++ b/core/templates/services/contextual/device-info.service.ts @@ -15,13 +15,13 @@ /** * @fileoverview Service to check if user is on a mobile device. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) // See: https://stackoverflow.com/a/11381730 export class DeviceInfoService { @@ -30,12 +30,13 @@ export class DeviceInfoService { isMobileDevice(): boolean { return Boolean( navigator.userAgent.match(/Android/i) || - navigator.userAgent.match(/webOS/i) || - navigator.userAgent.match(/iPhone/i) || - navigator.userAgent.match(/iPad/i) || - navigator.userAgent.match(/iPod/i) || - navigator.userAgent.match(/BlackBerry/i) || - navigator.userAgent.match(/Windows Phone/i)); + navigator.userAgent.match(/webOS/i) || + navigator.userAgent.match(/iPhone/i) || + navigator.userAgent.match(/iPad/i) || + navigator.userAgent.match(/iPod/i) || + navigator.userAgent.match(/BlackBerry/i) || + navigator.userAgent.match(/Windows Phone/i) + ); } isMobileUserAgent(): boolean { @@ -47,6 +48,6 @@ export class DeviceInfoService { } } -angular.module('oppia').factory( - 'DeviceInfoService', - downgradeInjectable(DeviceInfoService)); +angular + .module('oppia') + .factory('DeviceInfoService', downgradeInjectable(DeviceInfoService)); diff --git a/core/templates/services/contextual/document-attribute-customization.service.spec.ts b/core/templates/services/contextual/document-attribute-customization.service.spec.ts index d3d1272fef15..79199d392f23 100644 --- a/core/templates/services/contextual/document-attribute-customization.service.spec.ts +++ b/core/templates/services/contextual/document-attribute-customization.service.spec.ts @@ -15,11 +15,10 @@ /** * @fileoverview Unit tests for DocumentAttributeCustomizationService. */ -import { TestBed } from '@angular/core/testing'; +import {TestBed} from '@angular/core/testing'; -import { DocumentAttributeCustomizationService } from - 'services/contextual/document-attribute-customization.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import {DocumentAttributeCustomizationService} from 'services/contextual/document-attribute-customization.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; describe('Document Attribute Customization Service', () => { let dacs: DocumentAttributeCustomizationService; @@ -32,7 +31,9 @@ describe('Document Attribute Customization Service', () => { it('should add a atribute', () => { const setAttributeSpy = spyOn( - wrs.nativeWindow.document.documentElement, 'setAttribute').and.stub(); + wrs.nativeWindow.document.documentElement, + 'setAttribute' + ).and.stub(); dacs.addAttribute('class', 'oppia-base-container'); expect(setAttributeSpy).toHaveBeenCalled(); diff --git a/core/templates/services/contextual/document-attribute-customization.service.ts b/core/templates/services/contextual/document-attribute-customization.service.ts index 2017215149cf..0e01ecc880a2 100644 --- a/core/templates/services/contextual/document-attribute-customization.service.ts +++ b/core/templates/services/contextual/document-attribute-customization.service.ts @@ -16,23 +16,28 @@ * @fileoverview Service to add custom attributes to the element. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class DocumentAttributeCustomizationService { constructor(private windowRef: WindowRef) {} addAttribute(attribute: string, value: string): void { this.windowRef.nativeWindow.document.documentElement.setAttribute( - attribute, value); + attribute, + value + ); } } -angular.module('oppia').factory( - 'DocumentAttributeCustomizationService', - downgradeInjectable(DocumentAttributeCustomizationService)); +angular + .module('oppia') + .factory( + 'DocumentAttributeCustomizationService', + downgradeInjectable(DocumentAttributeCustomizationService) + ); diff --git a/core/templates/services/contextual/logger.service.spec.ts b/core/templates/services/contextual/logger.service.spec.ts index f9fa5827218a..10b458ae2f26 100644 --- a/core/templates/services/contextual/logger.service.spec.ts +++ b/core/templates/services/contextual/logger.service.spec.ts @@ -15,8 +15,8 @@ /** * @fileoverview Unit tests for LoggerService. */ -import { TestBed } from '@angular/core/testing'; -import { LoggerService } from 'services/contextual/logger.service'; +import {TestBed} from '@angular/core/testing'; +import {LoggerService} from 'services/contextual/logger.service'; describe('Logger Service', () => { let ls: LoggerService; diff --git a/core/templates/services/contextual/logger.service.ts b/core/templates/services/contextual/logger.service.ts index f3df96065f3b..0c02a0032718 100644 --- a/core/templates/services/contextual/logger.service.ts +++ b/core/templates/services/contextual/logger.service.ts @@ -16,10 +16,10 @@ * @fileoverview Service for logging. */ -import { Injectable } from '@angular/core'; +import {Injectable} from '@angular/core'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class LoggerService { constructor() {} diff --git a/core/templates/services/contextual/meta-tag-customization.service.spec.ts b/core/templates/services/contextual/meta-tag-customization.service.spec.ts index 8a11b743ea3a..8a3f425403dd 100644 --- a/core/templates/services/contextual/meta-tag-customization.service.spec.ts +++ b/core/templates/services/contextual/meta-tag-customization.service.spec.ts @@ -16,10 +16,9 @@ * @fileoverview Unit tests for MetaTagCustomizationService. */ -import { TestBed } from '@angular/core/testing'; -import { MetaTagCustomizationService } from - 'services/contextual/meta-tag-customization.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import {TestBed} from '@angular/core/testing'; +import {MetaTagCustomizationService} from 'services/contextual/meta-tag-customization.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; describe('Meta Tag Customization Service', () => { let mtcs: MetaTagCustomizationService; @@ -35,8 +34,8 @@ describe('Meta Tag Customization Service', () => { { propertyType: 'name', propertyValue: 'application-name', - content: 'Oppia.org' - } + content: 'Oppia.org', + }, ]; let removeSpy = jasmine.createSpy(); @@ -44,11 +43,13 @@ describe('Meta Tag Customization Service', () => { spyOn(wrs.nativeWindow.document, 'querySelector').and.returnValue({ remove: () => { removeSpy(); - } + }, } as Element); const appendChildSpy = spyOn( - wrs.nativeWindow.document.head, 'appendChild').and.callThrough(); + wrs.nativeWindow.document.head, + 'appendChild' + ).and.callThrough(); mtcs.addOrReplaceMetaTags(metaTags); const meta = wrs.nativeWindow.document.createElement('meta'); diff --git a/core/templates/services/contextual/meta-tag-customization.service.ts b/core/templates/services/contextual/meta-tag-customization.service.ts index 47f1903d87f2..befaf4bdeb9b 100644 --- a/core/templates/services/contextual/meta-tag-customization.service.ts +++ b/core/templates/services/contextual/meta-tag-customization.service.ts @@ -16,10 +16,10 @@ * @fileoverview Service to add custom meta tags. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { WindowRef } from './window-ref.service'; +import {WindowRef} from './window-ref.service'; export interface MetaAttribute { propertyType: string; @@ -28,7 +28,7 @@ export interface MetaAttribute { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class MetaTagCustomizationService { constructor(private windowRef: WindowRef) {} @@ -36,9 +36,9 @@ export class MetaTagCustomizationService { addOrReplaceMetaTags(attrArray: MetaAttribute[]): void { attrArray.forEach(attr => { // Find and remove exisiting meta tag. - let existingMetaTag = ( - this.windowRef.nativeWindow.document.querySelector( - 'meta[' + attr.propertyType + '="' + attr.propertyValue + '"]')); + let existingMetaTag = this.windowRef.nativeWindow.document.querySelector( + 'meta[' + attr.propertyType + '="' + attr.propertyValue + '"]' + ); if (existingMetaTag) { existingMetaTag.remove(); } @@ -51,6 +51,9 @@ export class MetaTagCustomizationService { } } -angular.module('oppia').factory( - 'MetaTagCustomizationService', - downgradeInjectable(MetaTagCustomizationService)); +angular + .module('oppia') + .factory( + 'MetaTagCustomizationService', + downgradeInjectable(MetaTagCustomizationService) + ); diff --git a/core/templates/services/contextual/url.service.spec.ts b/core/templates/services/contextual/url.service.spec.ts index eca7302c5511..e3c87698a924 100644 --- a/core/templates/services/contextual/url.service.spec.ts +++ b/core/templates/services/contextual/url.service.spec.ts @@ -16,10 +16,10 @@ * @fileoverview Unit tests for UrlService. */ -import { TestBed } from '@angular/core/testing'; +import {TestBed} from '@angular/core/testing'; -import { UrlService } from 'services/contextual/url.service'; -import { WindowRef } from './window-ref.service'; +import {UrlService} from 'services/contextual/url.service'; +import {WindowRef} from './window-ref.service'; describe('Url Service', () => { let urlService: UrlService; @@ -27,8 +27,10 @@ describe('Url Service', () => { let sampleHash = 'sampleHash'; let pathname = '/embed'; // Check https://www.typescriptlang.org/docs/handbook/utility-types.html#picktype-keys - let mockLocation: - Pick; + let mockLocation: Pick< + Location, + 'href' | 'origin' | 'pathname' | 'hash' | 'search' + >; let origin = 'http://sample.com'; beforeEach(() => { @@ -37,56 +39,60 @@ describe('Url Service', () => { origin: origin, pathname: pathname, hash: sampleHash, - search: '' + search: '', }; urlService = TestBed.get(UrlService); windowRef = TestBed.get(WindowRef); - spyOnProperty(windowRef, 'nativeWindow').and.callFake(() => ({ - location: mockLocation as Location - } as Window)); + spyOnProperty(windowRef, 'nativeWindow').and.callFake( + () => + ({ + location: mockLocation as Location, + }) as Window + ); }); it('should return correct query value list for each query field', () => { expect(urlService.getQueryFieldValuesAsList('field1')).toEqual([]); - mockLocation.search = '?field1=value1&' + + mockLocation.search = + '?field1=value1&' + 'field2=value2&field1=value3&field1=value4&field2=value5&' + 'field1=value6&field1=value%3F%3D%20%266'; let expectedList1 = ['value1', 'value3', 'value4', 'value6', 'value?= &6']; let expectedList2 = ['value2', 'value5']; - expect( - urlService.getQueryFieldValuesAsList('field1')).toEqual(expectedList1); - expect( - urlService.getQueryFieldValuesAsList('field2')).toEqual(expectedList2); + expect(urlService.getQueryFieldValuesAsList('field1')).toEqual( + expectedList1 + ); + expect(urlService.getQueryFieldValuesAsList('field2')).toEqual( + expectedList2 + ); }); - it('should correctly decode special characters in query value in url', - () => { - let expectedObject = { - field1: '?value=1', - field2: '?value&1' - }; - mockLocation.search = '?field1=%3Fvalue%3D1&field2=%3Fvalue%261'; - expect(urlService.getUrlParams()).toEqual(expectedObject); - }); - - it('should correctly encode and add query field and value to url', - () => { - let queryValue = '&value=1?'; - let queryField = 'field 1'; - let baseUrl = '/sample'; - let expectedUrl1 = baseUrl + '?field%201=%26value%3D1%3F'; - expect( - urlService.addField(baseUrl, queryField, queryValue)).toBe( - expectedUrl1); - - baseUrl = '/sample?field=value'; - let expectedUrl2 = baseUrl + '&field%201=%26value%3D1%3F'; - expect( - urlService.addField(baseUrl, queryField, queryValue)).toBe( - expectedUrl2); - }); + it('should correctly decode special characters in query value in url', () => { + let expectedObject = { + field1: '?value=1', + field2: '?value&1', + }; + mockLocation.search = '?field1=%3Fvalue%3D1&field2=%3Fvalue%261'; + expect(urlService.getUrlParams()).toEqual(expectedObject); + }); + + it('should correctly encode and add query field and value to url', () => { + let queryValue = '&value=1?'; + let queryField = 'field 1'; + let baseUrl = '/sample'; + let expectedUrl1 = baseUrl + '?field%201=%26value%3D1%3F'; + expect(urlService.addField(baseUrl, queryField, queryValue)).toBe( + expectedUrl1 + ); + + baseUrl = '/sample?field=value'; + let expectedUrl2 = baseUrl + '&field%201=%26value%3D1%3F'; + expect(urlService.addField(baseUrl, queryField, queryValue)).toBe( + expectedUrl2 + ); + }); it('should correctly return true if embed present in pathname', () => { expect(urlService.isIframed()).toBe(true); @@ -107,21 +113,19 @@ describe('Url Service', () => { it('should correctly retrieve topic id from url', () => { mockLocation.pathname = '/topic_editor/abcdefgijklm'; - expect( - urlService.getTopicIdFromUrl() - ).toBe('abcdefgijklm'); + expect(urlService.getTopicIdFromUrl()).toBe('abcdefgijklm'); mockLocation.pathname = '/topic_editor/abcdefgij'; - expect(function() { + expect(function () { urlService.getTopicIdFromUrl(); }).toThrowError('Invalid topic id url'); mockLocation.pathname = '/topiceditor/abcdefgijklm'; - expect(function() { + expect(function () { urlService.getTopicIdFromUrl(); }).toThrowError('Invalid topic id url'); mockLocation.pathname = '/topic_editor'; - expect(function() { + expect(function () { urlService.getTopicIdFromUrl(); }).toThrowError('Invalid topic id url'); }); @@ -129,13 +133,11 @@ describe('Url Service', () => { it('should correctly retrieve blog post id from url', () => { mockLocation.pathname = '/blog-dashboard'; mockLocation.hash = '/blog_post_editor/abcdefgijklm'; - expect( - urlService.getBlogPostIdFromUrl() - ).toBe('abcdefgijklm'); + expect(urlService.getBlogPostIdFromUrl()).toBe('abcdefgijklm'); mockLocation.pathname = '/blog-dashboard'; mockLocation.hash = '/blog_post_editor/abcdefgij'; - expect(function() { + expect(function () { urlService.getBlogPostIdFromUrl(); }).toThrowError('Invalid Blog Post Id.'); }); @@ -145,12 +147,12 @@ describe('Url Service', () => { expect(urlService.getBlogPostUrlFromUrl()).toBe('sample-blog-post-123'); mockLocation.pathname = '/blog/invalid/blog-post-1234'; - expect(function() { + expect(function () { urlService.getBlogPostUrlFromUrl(); }).toThrowError('Invalid Blog Post Url.'); mockLocation.pathname = '/invalid/blog-post-1234'; - expect(function() { + expect(function () { urlService.getBlogPostUrlFromUrl(); }).toThrowError('Invalid Blog Post Url.'); }); @@ -163,98 +165,72 @@ describe('Url Service', () => { // Checking with invalid blog author profile page url. The url has extra an // url segment. mockLocation.pathname = '/blog/author/invalid/username'; - expect(function() { + expect(function () { urlService.getBlogAuthorUsernameFromUrl(); }).toThrowError('Invalid Blog Author Profile Page Url.'); // Checking with invalid blog author profile page url. The url does not // start with 'blog/author'. mockLocation.pathname = 'blog/invalid/username'; - expect(function() { + expect(function () { urlService.getBlogAuthorUsernameFromUrl(); }).toThrowError('Invalid Blog Author Profile Page Url.'); }); it('should correctly retrieve story url fragment from url', () => { mockLocation.pathname = '/learn/math/abcdefgijklm/story/bakery'; - expect( - urlService.getStoryUrlFragmentFromLearnerUrl() - ).toBe('bakery'); + expect(urlService.getStoryUrlFragmentFromLearnerUrl()).toBe('bakery'); mockLocation.pathname = '/learn/math/topic-name/review-test/bakery'; - expect( - urlService.getStoryUrlFragmentFromLearnerUrl() - ).toBe('bakery'); + expect(urlService.getStoryUrlFragmentFromLearnerUrl()).toBe('bakery'); mockLocation.pathname = '/topc/abcdefgijklm'; - expect( - urlService.getStoryUrlFragmentFromLearnerUrl() - ).toBe(null); + expect(urlService.getStoryUrlFragmentFromLearnerUrl()).toBe(null); mockLocation.pathname = '/explore/16'; - mockLocation.search = ( - '?topic_url_fragment=topic&story_url_fragment=story-one'); - expect( - urlService.getStoryUrlFragmentFromLearnerUrl() - ).toBe('story-one'); - mockLocation.search = ( - '?topic_url_fragment=topic&story_url_fragment=story_one'); - expect( - urlService.getStoryUrlFragmentFromLearnerUrl() - ).toBe(null); + mockLocation.search = + '?topic_url_fragment=topic&story_url_fragment=story-one'; + expect(urlService.getStoryUrlFragmentFromLearnerUrl()).toBe('story-one'); + mockLocation.search = + '?topic_url_fragment=topic&story_url_fragment=story_one'; + expect(urlService.getStoryUrlFragmentFromLearnerUrl()).toBe(null); }); it('should correctly retrieve subtopic url fragment from url', () => { mockLocation.pathname = '/learn/math/fractions/revision/xyz'; - expect( - urlService.getSubtopicUrlFragmentFromLearnerUrl() - ).toBe('xyz'); + expect(urlService.getSubtopicUrlFragmentFromLearnerUrl()).toBe('xyz'); mockLocation.pathname = '/learn/math/topic-name/revision/negative-numbers'; - expect( - urlService.getSubtopicUrlFragmentFromLearnerUrl() - ).toBe('negative-numbers'); + expect(urlService.getSubtopicUrlFragmentFromLearnerUrl()).toBe( + 'negative-numbers' + ); mockLocation.pathname = '/sub/abcdefgijklm'; - expect(function() { + expect(function () { urlService.getSubtopicUrlFragmentFromLearnerUrl(); }).toThrowError('Invalid URL for subtopic'); }); it('should correctly retrieve topic url fragment from url', () => { mockLocation.pathname = '/learn/math/abcdefgijklm'; - expect( - urlService.getTopicUrlFragmentFromLearnerUrl() - ).toBe('abcdefgijklm'); + expect(urlService.getTopicUrlFragmentFromLearnerUrl()).toBe('abcdefgijklm'); mockLocation.pathname = '/learn/math/topic-name'; - expect( - urlService.getTopicUrlFragmentFromLearnerUrl() - ).toBe('topic-name'); + expect(urlService.getTopicUrlFragmentFromLearnerUrl()).toBe('topic-name'); mockLocation.pathname = '/learn/math/topic-name/practice'; - expect( - urlService.getTopicUrlFragmentFromLearnerUrl() - ).toBe('topic-name'); + expect(urlService.getTopicUrlFragmentFromLearnerUrl()).toBe('topic-name'); mockLocation.pathname = '/explore/16'; - mockLocation.search = ( - '?topic_url_fragment=topic'); - expect( - urlService.getTopicUrlFragmentFromLearnerUrl() - ).toBe('topic'); + mockLocation.search = '?topic_url_fragment=topic'; + expect(urlService.getTopicUrlFragmentFromLearnerUrl()).toBe('topic'); mockLocation.pathname = '/topc/abcdefgijklm'; - expect(function() { + expect(function () { urlService.getTopicUrlFragmentFromLearnerUrl(); }).toThrowError('Invalid URL for topic'); }); it('should correctly retrieve classroom name from url', () => { mockLocation.pathname = '/learn/math/abcdefgijklm'; - expect( - urlService.getClassroomUrlFragmentFromLearnerUrl() - ).toBe('math'); + expect(urlService.getClassroomUrlFragmentFromLearnerUrl()).toBe('math'); mockLocation.pathname = '/explore/16'; - mockLocation.search = ( - '&classroom_url_fragment=math'); - expect( - urlService.getClassroomUrlFragmentFromLearnerUrl() - ).toBe('math'); + mockLocation.search = '&classroom_url_fragment=math'; + expect(urlService.getClassroomUrlFragmentFromLearnerUrl()).toBe('math'); mockLocation.pathname = '/english/topic-name'; - expect(function() { + expect(function () { urlService.getClassroomUrlFragmentFromLearnerUrl(); }).toThrowError('Invalid URL for classroom'); }); @@ -262,121 +238,97 @@ describe('Url Service', () => { it('should correctly retrieve selected subtopics from url', () => { mockLocation.pathname = '/practice_session/topicName'; mockLocation.search = '?selected_subtopic_ids=abcdefgijklm'; - expect( - urlService.getSelectedSubtopicsFromUrl() - ).toBe('abcdefgijklm'); + expect(urlService.getSelectedSubtopicsFromUrl()).toBe('abcdefgijklm'); mockLocation.pathname = '/topic/abcdefgijklm'; - expect(function() { + expect(function () { urlService.getSelectedSubtopicsFromUrl(); }).toThrowError('Invalid URL for practice session'); mockLocation.pathname = '/practice_session/topicName'; mockLocation.search = '?selected_subtopic_idsabcdefgijklm'; - expect(function() { + expect(function () { urlService.getSelectedSubtopicsFromUrl(); }).toThrowError('Invalid URL for practice session'); }); it('should correctly retrieve classroom url fragment from url', () => { mockLocation.pathname = '/learn/abcdefgijklm'; - expect( - urlService.getClassroomUrlFragmentFromUrl() - ).toBe('abcdefgijklm'); + expect(urlService.getClassroomUrlFragmentFromUrl()).toBe('abcdefgijklm'); mockLocation.pathname = '/learn/class%20name'; - expect( - urlService.getClassroomUrlFragmentFromUrl() - ).toBe('class name'); + expect(urlService.getClassroomUrlFragmentFromUrl()).toBe('class name'); mockLocation.pathname = '/invalid/abcdefgijklm'; - expect(function() { + expect(function () { urlService.getClassroomUrlFragmentFromUrl(); }).toThrowError('Invalid URL for classroom'); }); it('should correctly retrieve subtopic id from url', () => { mockLocation.pathname = '/learn/math/abcdefgijklm/revision/1'; - expect( - urlService.getSubtopicIdFromUrl() - ).toBe('1'); + expect(urlService.getSubtopicIdFromUrl()).toBe('1'); mockLocation.pathname = '/learn/math/topic%20name/revision/20'; - expect( - urlService.getSubtopicIdFromUrl() - ).toBe('20'); + expect(urlService.getSubtopicIdFromUrl()).toBe('20'); mockLocation.pathname = '/subtopic/abcdefgijklm'; - expect(function() { + expect(function () { urlService.getSubtopicIdFromUrl(); }).toThrowError('Invalid URL for subtopic'); mockLocation.pathname = '/topic/abcdefgijklm/1'; - expect(function() { + expect(function () { urlService.getSubtopicIdFromUrl(); }).toThrowError('Invalid URL for subtopic'); }); it('should correctly retrieve story id from url', () => { mockLocation.pathname = '/story_editor/abcdefgijklm'; - expect( - urlService.getStoryIdFromUrl() - ).toBe('abcdefgijklm'); + expect(urlService.getStoryIdFromUrl()).toBe('abcdefgijklm'); mockLocation.pathname = '/story_editor/abcdefgij'; - expect(function() { + expect(function () { urlService.getStoryIdFromUrl(); }).toThrowError('Invalid story id url'); mockLocation.pathname = '/storyeditor/abcdefgijklm'; - expect(function() { + expect(function () { urlService.getStoryIdFromUrl(); }).toThrowError('Invalid story id url'); mockLocation.pathname = '/story_editor'; - expect(function() { + expect(function () { urlService.getStoryIdFromUrl(); }).toThrowError('Invalid story id url'); }); it('should correctly retrieve story id from story viewer url', () => { mockLocation.pathname = '/story_viewer/abcdefgijklm'; - expect(function() { + expect(function () { urlService.getStoryIdFromViewerUrl(); }).toThrowError('Invalid story id url'); mockLocation.pathname = '/learn/math/abcdefgijklm/story/abcdefg'; - expect(function() { + expect(function () { urlService.getStoryIdFromViewerUrl(); }).toThrowError('Invalid story id url'); mockLocation.pathname = '/learn/math/abcdefgijklm/story/abcdefgijklm'; - expect( - urlService.getStoryIdFromViewerUrl() - ).toEqual('abcdefgijklm'); + expect(urlService.getStoryIdFromViewerUrl()).toEqual('abcdefgijklm'); }); it('should correctly retrieve skill id from url', () => { mockLocation.pathname = '/skill_editor/abcdefghijkl'; - expect( - urlService.getSkillIdFromUrl() - ).toBe('abcdefghijkl'); + expect(urlService.getSkillIdFromUrl()).toBe('abcdefghijkl'); mockLocation.pathname = '/skill_editor/abcdefghijk'; - expect(function() { + expect(function () { urlService.getSkillIdFromUrl(); }).toThrowError('Invalid Skill Id'); }); - it('should correctly retrieve collection id from url in exploration player', - function() { - mockLocation.search = '?collection_id=abcdefghijkl'; - expect( - urlService.getCollectionIdFromExplorationUrl() - ).toBe('abcdefghijkl'); - - mockLocation.search = '?collection=abcdefghijkl'; - expect( - urlService.getCollectionIdFromExplorationUrl() - ).toBe(null); - - mockLocation.search = '?collection_id=abcdefghijkl&parent=mnopqrst'; - expect( - urlService.getCollectionIdFromExplorationUrl() - ).toBe(null); - } - ); + it('should correctly retrieve collection id from url in exploration player', function () { + mockLocation.search = '?collection_id=abcdefghijkl'; + expect(urlService.getCollectionIdFromExplorationUrl()).toBe('abcdefghijkl'); + + mockLocation.search = '?collection=abcdefghijkl'; + expect(urlService.getCollectionIdFromExplorationUrl()).toBe(null); + + mockLocation.search = '?collection_id=abcdefghijkl&parent=mnopqrst'; + expect(urlService.getCollectionIdFromExplorationUrl()).toBe(null); + }); it('should correctly retrieve exploration version from the url', () => { mockLocation.search = '?v=1'; @@ -408,7 +360,7 @@ describe('Url Service', () => { expect(urlService.getUsernameFromProfileUrl()).toBe('abcdefgijklm'); mockLocation.pathname = '/wrong_url/abcdefgijklm'; - expect(function() { + expect(function () { urlService.getUsernameFromProfileUrl(); }).toThrowError('Invalid profile URL'); }); @@ -418,7 +370,7 @@ describe('Url Service', () => { expect(urlService.getCollectionIdFromUrl()).toBe('abcdefgijklm'); mockLocation.pathname = '/wrong_url/abcdefgijklm'; - expect(function() { + expect(function () { urlService.getCollectionIdFromUrl(); }).toThrowError('Invalid collection URL'); }); @@ -428,12 +380,12 @@ describe('Url Service', () => { expect(urlService.getCollectionIdFromEditorUrl()).toBe('abcdefgijklm'); mockLocation.pathname = '/collection_editor/abcdefgijklm'; - expect(function() { + expect(function () { urlService.getCollectionIdFromEditorUrl(); }).toThrowError('Invalid collection editor URL'); mockLocation.pathname = '/collection_editor/wrong/abcdefgijklm'; - expect(function() { + expect(function () { urlService.getCollectionIdFromEditorUrl(); }).toThrowError('Invalid collection editor URL'); }); diff --git a/core/templates/services/contextual/url.service.ts b/core/templates/services/contextual/url.service.ts index a55c21e08afc..6ec039d8ecbf 100644 --- a/core/templates/services/contextual/url.service.ts +++ b/core/templates/services/contextual/url.service.ts @@ -17,12 +17,12 @@ * functions on $window to be mocked in unit tests. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { AppConstants } from 'app.constants'; +import {AppConstants} from 'app.constants'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; // This makes the UrlParamsType like a dict whose keys and values both are // string. @@ -31,7 +31,7 @@ export interface UrlParamsType { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class UrlService { constructor(private windowRef: WindowRef) {} @@ -63,8 +63,9 @@ export class UrlService { getUrlParams(): UrlParamsType { let params: UrlParamsType = {}; this.getCurrentQueryString().replace( - /[?&]+([^=&]+)=([^&]*)/gi, function(m, key, value) { - return params[decodeURIComponent(key)] = decodeURIComponent(value); + /[?&]+([^=&]+)=([^&]*)/gi, + function (m, key, value) { + return (params[decodeURIComponent(key)] = decodeURIComponent(value)); } ); return params; @@ -117,7 +118,9 @@ export class UrlService { if ( this.getUrlParams().hasOwnProperty('topic_url_fragment') && this.getUrlParams().topic_url_fragment.match( - AppConstants.VALID_URL_FRAGMENT_REGEX)) { + AppConstants.VALID_URL_FRAGMENT_REGEX + ) + ) { return this.getUrlParams().topic_url_fragment; } } @@ -130,7 +133,8 @@ export class UrlService { // pages. if ( pathname.startsWith('/learn') && - pathname.match(/\/story\/|\/review-test\//g)) { + pathname.match(/\/story\/|\/review-test\//g) + ) { return decodeURIComponent(pathname.split('/')[5]); } // The following section is for getting the URL fragment from the @@ -139,7 +143,9 @@ export class UrlService { if ( this.getUrlParams().hasOwnProperty('story_url_fragment') && this.getUrlParams().story_url_fragment.match( - AppConstants.VALID_URL_FRAGMENT_REGEX)) { + AppConstants.VALID_URL_FRAGMENT_REGEX + ) + ) { return this.getUrlParams().story_url_fragment; } } @@ -166,7 +172,9 @@ export class UrlService { if ( this.getUrlParams().hasOwnProperty('classroom_url_fragment') && this.getUrlParams().classroom_url_fragment.match( - AppConstants.VALID_URL_FRAGMENT_REGEX)) { + AppConstants.VALID_URL_FRAGMENT_REGEX + ) + ) { return this.getUrlParams().classroom_url_fragment; } } @@ -187,7 +195,6 @@ export class UrlService { throw new Error('Invalid URL for practice session'); } - /** * This function is used to find the classroom URL fragment from the learner's * URL. @@ -225,7 +232,8 @@ export class UrlService { getStoryIdFromUrl(): string { let pathname = this.getPathname(); var matchedPath = pathname.match( - /\/(story_editor|review-test)\/(\w|-){12}/g); + /\/(story_editor|review-test)\/(\w|-){12}/g + ); if (matchedPath) { return matchedPath[0].split('/')[2]; } @@ -274,10 +282,10 @@ export class UrlService { } /** - * This function is used to find the blog post url fragment from the url. - * @return {string} the blog post url fragment. - * @throws Will throw an error if the blog post url is invalid. - */ + * This function is used to find the blog post url fragment from the url. + * @return {string} the blog post url fragment. + * @throws Will throw an error if the blog post url is invalid. + */ getBlogPostUrlFromUrl(): string { let pathname = this.getPathname(); let argumentsArray = pathname.split('/'); @@ -289,10 +297,10 @@ export class UrlService { } /** - * This function is used to find the blog author username from the url. - * @return {string} the blog author username fragment. - * @throws Will throw an error if the url is invalid. - */ + * This function is used to find the blog author username from the url. + * @return {string} the blog author username fragment. + * @throws Will throw an error if the url is invalid. + */ getBlogAuthorUsernameFromUrl(): string { let pathname = this.getPathname(); let argumentsArray = pathname.split('/'); @@ -312,13 +320,12 @@ export class UrlService { let fieldValues = []; if (this.getCurrentQueryString().indexOf('?') > -1) { // Each queryItem return one field-value pair in the url. - let queryItems = this.getCurrentQueryString().slice( - this.getCurrentQueryString().indexOf('?') + 1).split('&'); + let queryItems = this.getCurrentQueryString() + .slice(this.getCurrentQueryString().indexOf('?') + 1) + .split('&'); for (let i = 0; i < queryItems.length; i++) { - let currentFieldName = decodeURIComponent( - queryItems[i].split('=')[0]); - let currentFieldValue = decodeURIComponent( - queryItems[i].split('=')[1]); + let currentFieldName = decodeURIComponent(queryItems[i].split('=')[0]); + let currentFieldValue = decodeURIComponent(queryItems[i].split('=')[1]); if (currentFieldName === fieldName) { fieldValues.push(currentFieldValue); } @@ -338,8 +345,13 @@ export class UrlService { addField(url: string, fieldName: string, fieldValue: string): string { let encodedFieldValue = encodeURIComponent(fieldValue); let encodedFieldName = encodeURIComponent(fieldName); - return url + (url.indexOf('?') !== -1 ? '&' : '?') + encodedFieldName + - '=' + encodedFieldValue; + return ( + url + + (url.indexOf('?') !== -1 ? '&' : '?') + + encodedFieldName + + '=' + + encodedFieldValue + ); } /** @@ -445,5 +457,4 @@ export class UrlService { } } -angular.module('oppia').factory( - 'UrlService', downgradeInjectable(UrlService)); +angular.module('oppia').factory('UrlService', downgradeInjectable(UrlService)); diff --git a/core/templates/services/contextual/window-dimensions.service.spec.ts b/core/templates/services/contextual/window-dimensions.service.spec.ts index 2ced13316045..b4a57d1e99c2 100644 --- a/core/templates/services/contextual/window-dimensions.service.spec.ts +++ b/core/templates/services/contextual/window-dimensions.service.spec.ts @@ -15,9 +15,9 @@ /** * @fileoverview Unit tests for WindowDimensionsService. */ -import { TestBed } from '@angular/core/testing'; -import { WindowDimensionsService } from 'services/contextual/window-dimensions.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import {TestBed} from '@angular/core/testing'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; describe('Window Dimensions Service', () => { let wds: WindowDimensionsService; @@ -35,10 +35,10 @@ describe('Window Dimensions Service', () => { // ref: https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty // ref: https://github.com/jasmine/jasmine/issues/1415 Object.defineProperty(wr.nativeWindow, 'innerWidth', { - get: () => undefined + get: () => undefined, }); Object.defineProperty(wr.nativeWindow, 'innerHeight', { - get: () => undefined + get: () => undefined, }); }); @@ -50,17 +50,23 @@ describe('Window Dimensions Service', () => { it('should get window width by clientWidth', () => { spyOnProperty(wr.nativeWindow, 'innerWidth').and.returnValue(0); - spyOnProperty(wr.nativeWindow.document.documentElement, 'clientWidth') - .and.returnValue(1000); + spyOnProperty( + wr.nativeWindow.document.documentElement, + 'clientWidth' + ).and.returnValue(1000); expect(wds.getWidth()).toEqual(1000); }); it('should get window width by document clientWidth', () => { spyOnProperty(wr.nativeWindow, 'innerWidth').and.returnValue(0); - spyOnProperty(wr.nativeWindow.document.documentElement, 'clientWidth') - .and.returnValue(0); - spyOnProperty(wr.nativeWindow.document.body, 'clientWidth') - .and.returnValue(1000); + spyOnProperty( + wr.nativeWindow.document.documentElement, + 'clientWidth' + ).and.returnValue(0); + spyOnProperty( + wr.nativeWindow.document.body, + 'clientWidth' + ).and.returnValue(1000); expect(wds.getWidth()).toEqual(1000); }); }); @@ -73,17 +79,23 @@ describe('Window Dimensions Service', () => { it('should get window Height by clientHeight', () => { spyOnProperty(wr.nativeWindow, 'innerHeight').and.returnValue(0); - spyOnProperty(wr.nativeWindow.document.documentElement, 'clientHeight') - .and.returnValue(1000); + spyOnProperty( + wr.nativeWindow.document.documentElement, + 'clientHeight' + ).and.returnValue(1000); expect(wds.getHeight()).toEqual(1000); }); it('should get window Height by document clientHeight', () => { spyOnProperty(wr.nativeWindow, 'innerHeight').and.returnValue(0); - spyOnProperty(wr.nativeWindow.document.documentElement, 'clientHeight') - .and.returnValue(0); - spyOnProperty(wr.nativeWindow.document.body, 'clientHeight') - .and.returnValue(1000); + spyOnProperty( + wr.nativeWindow.document.documentElement, + 'clientHeight' + ).and.returnValue(0); + spyOnProperty( + wr.nativeWindow.document.body, + 'clientHeight' + ).and.returnValue(1000); expect(wds.getHeight()).toEqual(1000); }); }); diff --git a/core/templates/services/contextual/window-dimensions.service.ts b/core/templates/services/contextual/window-dimensions.service.ts index af69ff3d21e8..3832f15af625 100644 --- a/core/templates/services/contextual/window-dimensions.service.ts +++ b/core/templates/services/contextual/window-dimensions.service.ts @@ -16,13 +16,13 @@ * @fileoverview Service for computing the window dimensions. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { fromEvent, Observable } from 'rxjs'; -import { Injectable } from '@angular/core'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {fromEvent, Observable} from 'rxjs'; +import {Injectable} from '@angular/core'; +import {WindowRef} from 'services/contextual/window-ref.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class WindowDimensionsService { constructor(private windowRef: WindowRef) {} @@ -35,14 +35,16 @@ export class WindowDimensionsService { return ( this.windowRef.nativeWindow.innerWidth || this.windowRef.nativeWindow.document.documentElement.clientWidth || - this.windowRef.nativeWindow.document.body.clientWidth); + this.windowRef.nativeWindow.document.body.clientWidth + ); } getHeight(): number { return ( this.windowRef.nativeWindow.innerHeight || this.windowRef.nativeWindow.document.documentElement.clientHeight || - this.windowRef.nativeWindow.document.body.clientHeight); + this.windowRef.nativeWindow.document.body.clientHeight + ); } isWindowNarrow(): boolean { @@ -51,6 +53,9 @@ export class WindowDimensionsService { } } -angular.module('oppia').factory( - 'WindowDimensionsService', - downgradeInjectable(WindowDimensionsService)); +angular + .module('oppia') + .factory( + 'WindowDimensionsService', + downgradeInjectable(WindowDimensionsService) + ); diff --git a/core/templates/services/contextual/window-ref.service.spec.ts b/core/templates/services/contextual/window-ref.service.spec.ts index 9de713d79e11..85a6134bd607 100644 --- a/core/templates/services/contextual/window-ref.service.spec.ts +++ b/core/templates/services/contextual/window-ref.service.spec.ts @@ -16,8 +16,8 @@ * @fileoverview Unit tests for WindowRef. */ -import { TestBed } from '@angular/core/testing'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import {TestBed} from '@angular/core/testing'; +import {WindowRef} from 'services/contextual/window-ref.service'; describe('Window Ref', () => { let wrs: WindowRef; diff --git a/core/templates/services/contextual/window-ref.service.ts b/core/templates/services/contextual/window-ref.service.ts index 9d2cb9ee507d..e0b0039cf92c 100644 --- a/core/templates/services/contextual/window-ref.service.ts +++ b/core/templates/services/contextual/window-ref.service.ts @@ -16,22 +16,22 @@ * @fileoverview Service to wrap the window object. */ -import { Injectable } from '@angular/core'; +import {Injectable} from '@angular/core'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class WindowRef { /** - * @returns The global native browser window object. - */ + * @returns The global native browser window object. + */ _window(): Window { return window; } /** - * @returns The global native browser window object. - */ + * @returns The global native browser window object. + */ get nativeWindow(): Window { return this._window(); } diff --git a/core/templates/services/csrf-token.service.spec.ts b/core/templates/services/csrf-token.service.spec.ts index efd2a12989d7..b840822260cf 100644 --- a/core/templates/services/csrf-token.service.spec.ts +++ b/core/templates/services/csrf-token.service.spec.ts @@ -15,28 +15,34 @@ /** * @fileoverview Unit tests for the csrf service */ -import { CsrfTokenService } from 'services/csrf-token.service'; -import { TestBed } from '@angular/core/testing'; -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; - -describe('Csrf Token Service', function() { +import {CsrfTokenService} from 'services/csrf-token.service'; +import {TestBed} from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; + +describe('Csrf Token Service', function () { let csrfTokenService: CsrfTokenService; let httpTestingController: HttpTestingController; beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] + imports: [HttpClientTestingModule], }); httpTestingController = TestBed.inject(HttpTestingController); csrfTokenService = TestBed.inject(CsrfTokenService); }); - it('should correctly set the csrf token', (done) => { + it('should correctly set the csrf token', done => { csrfTokenService.initializeToken(); - csrfTokenService.getTokenAsync().then(function(token) { - expect(token).toEqual('sample-csrf-token'); - }).then(done, done.fail); + csrfTokenService + .getTokenAsync() + .then(function (token) { + expect(token).toEqual('sample-csrf-token'); + }) + .then(done, done.fail); let req = httpTestingController.expectOne('/csrfhandler'); expect(req.request.method).toEqual('GET'); @@ -45,40 +51,42 @@ describe('Csrf Token Service', function() { httpTestingController.verify(); }); - it('should throw error when the request failed', (done) => { + it('should throw error when the request failed', done => { csrfTokenService.initializeToken(); csrfTokenService.getTokenAsync().then(done.fail, done); let req = httpTestingController.expectOne('/csrfhandler'); expect(req.request.method).toEqual('GET'); - req.error( - new ErrorEvent('network error'), {status: 500, statusText: 'error'} - ); + req.error(new ErrorEvent('network error'), { + status: 500, + statusText: 'error', + }); httpTestingController.verify(); }); - it('should throw error when the request failed', (done) => { + it('should throw error when the request failed', done => { csrfTokenService.initializeToken(); csrfTokenService.getTokenAsync().then(done.fail, done); let req = httpTestingController.expectOne('/csrfhandler'); expect(req.request.method).toEqual('GET'); - req.error( - new ErrorEvent('network error'), {status: 500, statusText: 'error'} - ); + req.error(new ErrorEvent('network error'), { + status: 500, + statusText: 'error', + }); httpTestingController.verify(); }); - it('should error if initialize is called more than once', () => { csrfTokenService.initializeToken(); - expect(() => csrfTokenService.initializeToken()) - .toThrowError('Token request has already been made'); + expect(() => csrfTokenService.initializeToken()).toThrowError( + 'Token request has already been made' + ); }); it('should error if getTokenAsync is called before initialize', () => { diff --git a/core/templates/services/csrf-token.service.ts b/core/templates/services/csrf-token.service.ts index 566d60b0bf9e..0a4ab0c5ea8e 100644 --- a/core/templates/services/csrf-token.service.ts +++ b/core/templates/services/csrf-token.service.ts @@ -20,13 +20,13 @@ // because Angular doesn't support global definitions and every library used // needs to be imported explicitly. -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; // eslint-disable-next-line oppia/disallow-httpclient -import { HttpClient, HttpBackend } from '@angular/common/http'; +import {HttpClient, HttpBackend} from '@angular/common/http'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class CsrfTokenService { // 'tokenPromise' will be null when token is not initialized. @@ -43,14 +43,18 @@ export class CsrfTokenService { if (this.tokenPromise !== null) { throw new Error('Token request has already been made'); } - this.tokenPromise = this.http.get( - '/csrfhandler', { responseType: 'text' } - ).toPromise().then((responseText: string) => { - // Remove the protective XSSI (cross-site scripting inclusion) prefix. - return JSON.parse(responseText.substring(5)).token; - }, (err) => { - throw err; - }); + this.tokenPromise = this.http + .get('/csrfhandler', {responseType: 'text'}) + .toPromise() + .then( + (responseText: string) => { + // Remove the protective XSSI (cross-site scripting inclusion) prefix. + return JSON.parse(responseText.substring(5)).token; + }, + err => { + throw err; + } + ); } getTokenAsync(): PromiseLike { @@ -61,5 +65,6 @@ export class CsrfTokenService { } } -angular.module('oppia').factory( - 'CsrfTokenService', downgradeInjectable(CsrfTokenService)); +angular + .module('oppia') + .factory('CsrfTokenService', downgradeInjectable(CsrfTokenService)); diff --git a/core/templates/services/date-time-format.service.spec.ts b/core/templates/services/date-time-format.service.spec.ts index 1475c1a0c2f1..a65995812111 100644 --- a/core/templates/services/date-time-format.service.spec.ts +++ b/core/templates/services/date-time-format.service.spec.ts @@ -16,7 +16,7 @@ * @fileoverview Unit test for DateTimeFormatService. */ -import { DateTimeFormatService } from 'services/date-time-format.service'; +import {DateTimeFormatService} from 'services/date-time-format.service'; import dayjs from 'dayjs'; describe('datetimeformatter', () => { @@ -28,7 +28,7 @@ describe('datetimeformatter', () => { beforeEach(() => { df = new DateTimeFormatService(); - let MockDateContructor = function(millisSinceEpoch = 0) { + let MockDateContructor = function (millisSinceEpoch = 0) { if (millisSinceEpoch === 0) { return new OldDate(NOW_MILLIS); } else { @@ -61,16 +61,19 @@ describe('datetimeformatter', () => { let expectedDatetime = new Date(NOW_MILLIS - 1).toLocaleTimeString([], { hour: 'numeric', minute: 'numeric', - hour12: true + hour12: true, }); expect(df.getLocaleAbbreviatedDatetimeString(NOW_MILLIS - 1)).toBe( - expectedDatetime); + expectedDatetime + ); expect( - df.getLocaleAbbreviatedDatetimeString( - NOW_MILLIS + 48 * 60 * 60 * 1000)).toBe('Nov 23'); + df.getLocaleAbbreviatedDatetimeString(NOW_MILLIS + 48 * 60 * 60 * 1000) + ).toBe('Nov 23'); expect( df.getLocaleAbbreviatedDatetimeString( - NOW_MILLIS - 365 * 24 * 60 * 60 * 1000)).toBe('11/21/13'); + NOW_MILLIS - 365 * 24 * 60 * 60 * 1000 + ) + ).toBe('11/21/13'); }); it('should provide date time hour in MMM D, h:mm A format', () => { @@ -96,21 +99,22 @@ describe('datetimeformatter', () => { // month and year of df.getLocaleDateString, which is why // toLocaleDateString() needs to be computed in the expected // value of the test as well. - expect((new Date(NOW_MILLIS)).toLocaleDateString()).toBe( - df.getLocaleDateString(NOW_MILLIS)); - expect((new Date(NaN).toLocaleDateString())).toBe( - df.getLocaleDateString(NaN)); + expect(new Date(NOW_MILLIS).toLocaleDateString()).toBe( + df.getLocaleDateString(NOW_MILLIS) + ); + expect(new Date(NaN).toLocaleDateString()).toBe( + df.getLocaleDateString(NaN) + ); }); it('should provide relative time from a given timestamp', () => { let timeAFewSecondsAgo = NOW_MILLIS - 5 * 1000; let timeAnHourAgo = NOW_MILLIS - 60 * 60 * 1000; let timeADayAgo = NOW_MILLIS - 24 * 60 * 60 * 1000; - expect(df.getRelativeTimeFromNow(timeAFewSecondsAgo)) - .toBe('a few seconds ago'); - expect(df.getRelativeTimeFromNow(timeAnHourAgo)) - .toBe('an hour ago'); - expect(df.getRelativeTimeFromNow(timeADayAgo)) - .toBe('a day ago'); + expect(df.getRelativeTimeFromNow(timeAFewSecondsAgo)).toBe( + 'a few seconds ago' + ); + expect(df.getRelativeTimeFromNow(timeAnHourAgo)).toBe('an hour ago'); + expect(df.getRelativeTimeFromNow(timeADayAgo)).toBe('a day ago'); }); }); diff --git a/core/templates/services/date-time-format.service.ts b/core/templates/services/date-time-format.service.ts index 34b859afab3e..d51d718bd7e1 100644 --- a/core/templates/services/date-time-format.service.ts +++ b/core/templates/services/date-time-format.service.ts @@ -17,31 +17,31 @@ * since the Epoch to human-readable dates. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; import dayjs from 'dayjs'; import relativeTime from 'dayjs/plugin/relativeTime'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class DateTimeFormatService { -/** - * This function returns the time (using locale conventions) if the local - * datetime representation has the same date as the current date. Else if the - * local datetime representation has the same year as the current date, it - * returns the date in the format 'MMM D'. Else, it returns the full date in - * the format 'MM/DD/YY'. - * @param {number} millisSinceEpoch - milliseconds since Epoch - * @returns {string} - a date - */ + /** + * This function returns the time (using locale conventions) if the local + * datetime representation has the same date as the current date. Else if the + * local datetime representation has the same year as the current date, it + * returns the date in the format 'MMM D'. Else, it returns the full date in + * the format 'MM/DD/YY'. + * @param {number} millisSinceEpoch - milliseconds since Epoch + * @returns {string} - a date + */ getLocaleAbbreviatedDatetimeString(millisSinceEpoch: number): string { let date = new Date(millisSinceEpoch); if (date.toLocaleDateString() === new Date().toLocaleDateString()) { return date.toLocaleTimeString([], { hour: 'numeric', minute: 'numeric', - hour12: true + hour12: true, }); } else if (date.getFullYear() === new Date().getFullYear()) { // Moment will return Oct 10. @@ -115,5 +115,6 @@ export class DateTimeFormatService { } } -angular.module('oppia').factory( - 'DateTimeFormatService', downgradeInjectable(DateTimeFormatService)); +angular + .module('oppia') + .factory('DateTimeFormatService', downgradeInjectable(DateTimeFormatService)); diff --git a/core/templates/services/editability.service.spec.ts b/core/templates/services/editability.service.spec.ts index 25ab70298df2..25c29634dc59 100644 --- a/core/templates/services/editability.service.spec.ts +++ b/core/templates/services/editability.service.spec.ts @@ -16,7 +16,7 @@ * @fileoverview Unit tests for EditabilityService. */ -import { EditabilityService } from 'services/editability.service'; +import {EditabilityService} from 'services/editability.service'; describe('EditabilityService', () => { let editabilityService: EditabilityService; @@ -45,24 +45,21 @@ describe('EditabilityService', () => { expect(editabilityService.isEditable()).toBe(true); }); - it('should allow to translate an exploration after the tutorial ends', - () => { - editabilityService.onEndTutorial(); - editabilityService.markTranslatable(); - expect(editabilityService.isTranslatable()).toBe(true); - }); + it('should allow to translate an exploration after the tutorial ends', () => { + editabilityService.onEndTutorial(); + editabilityService.markTranslatable(); + expect(editabilityService.isTranslatable()).toBe(true); + }); - it('should allow to edit an exploration outside the tutorial mode', - () => { - editabilityService.markEditable(); - expect(editabilityService.isEditableOutsideTutorialMode()).toBe(true); - }); + it('should allow to edit an exploration outside the tutorial mode', () => { + editabilityService.markEditable(); + expect(editabilityService.isEditableOutsideTutorialMode()).toBe(true); + }); - it('should not allow to edit an exploration during tutorial mode', - () => { - editabilityService.onStartTutorial(); - expect(editabilityService.isEditable()).toBe(false); - }); + it('should not allow to edit an exploration during tutorial mode', () => { + editabilityService.onStartTutorial(); + expect(editabilityService.isEditable()).toBe(false); + }); it('should not allow to edit an uneditable exploration', () => { editabilityService.markNotEditable(); diff --git a/core/templates/services/editability.service.ts b/core/templates/services/editability.service.ts index ede2b493268d..9caf10f84090 100644 --- a/core/templates/services/editability.service.ts +++ b/core/templates/services/editability.service.ts @@ -16,11 +16,11 @@ * @fileoverview Service for checking the ability to edit an exploration. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class EditabilityService { static isEditable: boolean = false; @@ -35,7 +35,8 @@ export class EditabilityService { return ( EditabilityService.isEditable && !EditabilityService.inTutorialMode && - !EditabilityService.isLockedByAdmin); + !EditabilityService.isLockedByAdmin + ); } /** @@ -43,7 +44,8 @@ export class EditabilityService { */ isTranslatable(): boolean { return ( - EditabilityService.isTranslatable && !EditabilityService.inTutorialMode); + EditabilityService.isTranslatable && !EditabilityService.inTutorialMode + ); } /** @@ -104,5 +106,6 @@ export class EditabilityService { } } -angular.module('oppia').factory( - 'EditabilityService', downgradeInjectable(EditabilityService)); +angular + .module('oppia') + .factory('EditabilityService', downgradeInjectable(EditabilityService)); diff --git a/core/templates/services/entity-translations.services.spec.ts b/core/templates/services/entity-translations.services.spec.ts index ddf50dcb2b85..180ca7b0ecfb 100644 --- a/core/templates/services/entity-translations.services.spec.ts +++ b/core/templates/services/entity-translations.services.spec.ts @@ -16,11 +16,11 @@ * @fileoverview Tests for EntityTranslationsService. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks, tick } from '@angular/core/testing'; -import { EntityTranslation } from 'domain/translation/EntityTranslationObjectFactory'; -import { EntityTranslationBackendApiService } from 'pages/exploration-editor-page/services/entity-translation-backend-api.service'; -import { EntityTranslationsService } from './entity-translations.services'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks, tick} from '@angular/core/testing'; +import {EntityTranslation} from 'domain/translation/EntityTranslationObjectFactory'; +import {EntityTranslationBackendApiService} from 'pages/exploration-editor-page/services/entity-translation-backend-api.service'; +import {EntityTranslationsService} from './entity-translations.services'; describe('Entity translations service', () => { let entityTranslationsService: EntityTranslationsService; @@ -30,7 +30,7 @@ describe('Entity translations service', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [EntityTranslationsService] + providers: [EntityTranslationsService], }); entityTranslationsService = TestBed.inject(EntityTranslationsService); etbs = TestBed.inject(EntityTranslationBackendApiService); @@ -44,61 +44,60 @@ describe('Entity translations service', () => { content: { content_format: 'html', content_value: '

fr content

', - needs_update: false + needs_update: false, }, hint_0: { content_format: 'html', content_value: '

fr hint

', - needs_update: false + needs_update: false, }, solution: { content_format: 'html', content_value: '

fr solution

', - needs_update: false + needs_update: false, }, ca_placeholder_0: { content_format: 'unicode', content_value: 'fr placeholder', - needs_update: false + needs_update: false, }, outcome_1: { content_format: 'html', content_value: '

fr feedback

', - needs_update: false + needs_update: false, }, default_outcome: { content_format: 'html', content_value: '

fr default outcome

', - needs_update: false + needs_update: false, }, rule_input_3: { content_format: 'set_of_normalized_string', content_value: ['fr rule input 1', 'fr rule input 2'], - needs_update: false - } - } + needs_update: false, + }, + }, }); spyOn(etbs, 'fetchEntityTranslationAsync').and.returnValue( Promise.resolve(entityTranslation) ); }); - it('should successfully fetch data from backend api service', - fakeAsync(() => { - var successHandler = jasmine.createSpy('success'); - var failHandler = jasmine.createSpy('fail'); + it('should successfully fetch data from backend api service', fakeAsync(() => { + var successHandler = jasmine.createSpy('success'); + var failHandler = jasmine.createSpy('fail'); - entityTranslationsService.init('entity1', 'exploration', 5); + entityTranslationsService.init('entity1', 'exploration', 5); - entityTranslationsService.getEntityTranslationsAsync('hi') - .then(successHandler, failHandler); - tick(); - flushMicrotasks(); + entityTranslationsService + .getEntityTranslationsAsync('hi') + .then(successHandler, failHandler); + tick(); + flushMicrotasks(); - expect(successHandler).toHaveBeenCalled(); - expect(failHandler).not.toHaveBeenCalled(); - }) - ); + expect(successHandler).toHaveBeenCalled(); + expect(failHandler).not.toHaveBeenCalled(); + })); it('should remove fetched translations when reset', fakeAsync(() => { var successHandler = jasmine.createSpy('success'); @@ -106,7 +105,8 @@ describe('Entity translations service', () => { entityTranslationsService.init('entity1', 'exploration', 5); - entityTranslationsService.getEntityTranslationsAsync('hi') + entityTranslationsService + .getEntityTranslationsAsync('hi') .then(successHandler, failHandler); tick(); flushMicrotasks(); @@ -115,56 +115,59 @@ describe('Entity translations service', () => { expect(failHandler).not.toHaveBeenCalled(); expect( - entityTranslationsService - .languageCodeToEntityTranslations.hasOwnProperty('hi') + entityTranslationsService.languageCodeToEntityTranslations.hasOwnProperty( + 'hi' + ) ).toBeTrue(); entityTranslationsService.reset(); expect( - entityTranslationsService - .languageCodeToEntityTranslations.hasOwnProperty('hi') + entityTranslationsService.languageCodeToEntityTranslations.hasOwnProperty( + 'hi' + ) ).not.toBeTrue(); })); - it('should store fetched data and return without calling api service', - fakeAsync(() => { - var successHandler = jasmine.createSpy('success'); - var failHandler = jasmine.createSpy('fail'); + it('should store fetched data and return without calling api service', fakeAsync(() => { + var successHandler = jasmine.createSpy('success'); + var failHandler = jasmine.createSpy('fail'); - entityTranslationsService.init('entity1', 'exploration', 5); - entityTranslationsService.languageCodeToEntityTranslations.hi = ( - entityTranslation - ); + entityTranslationsService.init('entity1', 'exploration', 5); + entityTranslationsService.languageCodeToEntityTranslations.hi = + entityTranslation; - entityTranslationsService.getEntityTranslationsAsync('hi') - .then(successHandler, failHandler); - tick(); - flushMicrotasks(); + entityTranslationsService + .getEntityTranslationsAsync('hi') + .then(successHandler, failHandler); + tick(); + flushMicrotasks(); - expect(etbs.fetchEntityTranslationAsync).not.toHaveBeenCalled(); - }) - ); + expect(etbs.fetchEntityTranslationAsync).not.toHaveBeenCalled(); + })); it('should return correct html for given contentIds', () => { - entityTranslationsService.languageCodeToEntityTranslations.hi = ( - entityTranslation - ); + entityTranslationsService.languageCodeToEntityTranslations.hi = + entityTranslation; - const htmlData = entityTranslationsService.getHtmlTranslations( - 'hi', ['content', 'invalid_content', 'rule_input_3']); + const htmlData = entityTranslationsService.getHtmlTranslations('hi', [ + 'content', + 'invalid_content', + 'rule_input_3', + ]); expect(htmlData).toEqual(['

fr content

']); }); - it('should return empty list for translation not available in language', - () => { - entityTranslationsService.languageCodeToEntityTranslations.hi = ( - entityTranslation - ); + it('should return empty list for translation not available in language', () => { + entityTranslationsService.languageCodeToEntityTranslations.hi = + entityTranslation; - const htmlData = entityTranslationsService.getHtmlTranslations( - 'ar', ['content', 'invalid_content', 'rule_input_3']); + const htmlData = entityTranslationsService.getHtmlTranslations('ar', [ + 'content', + 'invalid_content', + 'rule_input_3', + ]); - expect(htmlData).toEqual([]); - }); + expect(htmlData).toEqual([]); + }); }); diff --git a/core/templates/services/entity-translations.services.ts b/core/templates/services/entity-translations.services.ts index ffa28a9eb06d..08d45c2cd38c 100644 --- a/core/templates/services/entity-translations.services.ts +++ b/core/templates/services/entity-translations.services.ts @@ -17,82 +17,81 @@ * entity in a given langauge. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { TranslatedContent } from 'domain/exploration/TranslatedContentObjectFactory'; -import { EntityTranslation } from 'domain/translation/EntityTranslationObjectFactory'; -import { EntityTranslationBackendApiService } from 'pages/exploration-editor-page/services/entity-translation-backend-api.service'; -import { AlertsService } from './alerts.service'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {TranslatedContent} from 'domain/exploration/TranslatedContentObjectFactory'; +import {EntityTranslation} from 'domain/translation/EntityTranslationObjectFactory'; +import {EntityTranslationBackendApiService} from 'pages/exploration-editor-page/services/entity-translation-backend-api.service'; +import {AlertsService} from './alerts.service'; export interface LanguageCodeToEntityTranslations { [languageCode: string]: EntityTranslation; } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class EntityTranslationsService { private entityId!: string; private entityType!: string; private entityVersion!: number; - public languageCodeToEntityTranslations: LanguageCodeToEntityTranslations = ( - {} - ); + public languageCodeToEntityTranslations: LanguageCodeToEntityTranslations = + {}; constructor( private alertsService: AlertsService, - private entityTranslationBackendApiService: ( - EntityTranslationBackendApiService) + private entityTranslationBackendApiService: EntityTranslationBackendApiService ) {} - init( - entityId: string, - entityType: string, - entityVersion: number - ): void { + init(entityId: string, entityType: string, entityVersion: number): void { this.entityType = entityType; this.entityVersion = entityVersion; this.entityId = entityId; } - async getEntityTranslationsAsync(languageCode: string): - Promise { + async getEntityTranslationsAsync( + languageCode: string + ): Promise { return new Promise((resolve, reject) => { if (languageCode in this.languageCodeToEntityTranslations) { resolve(this.languageCodeToEntityTranslations[languageCode]); return; } this.alertsService.addInfoMessage('Fetching translation.'); - this.entityTranslationBackendApiService.fetchEntityTranslationAsync( - this.entityId, - this.entityType, - this.entityVersion, - languageCode - ).then((entityTranslation) => { - this.languageCodeToEntityTranslations[languageCode] = entityTranslation; - this.alertsService.clearMessages(); - this.alertsService.addSuccessMessage('Translations fetched.'); - resolve(entityTranslation); - }); + this.entityTranslationBackendApiService + .fetchEntityTranslationAsync( + this.entityId, + this.entityType, + this.entityVersion, + languageCode + ) + .then(entityTranslation => { + this.languageCodeToEntityTranslations[languageCode] = + entityTranslation; + this.alertsService.clearMessages(); + this.alertsService.addSuccessMessage('Translations fetched.'); + resolve(entityTranslation); + }); }); } getHtmlTranslations(languageCode: string, contentIds: string[]): string[] { - if ( - !this.languageCodeToEntityTranslations.hasOwnProperty(languageCode)) { + if (!this.languageCodeToEntityTranslations.hasOwnProperty(languageCode)) { return []; } let entityTranslation = this.languageCodeToEntityTranslations[ - languageCode] as EntityTranslation; + languageCode + ] as EntityTranslation; let htmlStrings: string[] = []; - contentIds.forEach((contentId) => { + contentIds.forEach(contentId => { if (!entityTranslation.hasWrittenTranslation(contentId)) { return; } let writtenTranslation = entityTranslation.getWrittenTranslation( - contentId) as TranslatedContent; + contentId + ) as TranslatedContent; if (writtenTranslation.dataFormat === 'html') { htmlStrings.push(writtenTranslation.translation as string); } @@ -105,6 +104,9 @@ export class EntityTranslationsService { } } -angular.module('oppia').factory( - 'EntityTranslationsService', - downgradeInjectable(EntityTranslationsService)); +angular + .module('oppia') + .factory( + 'EntityTranslationsService', + downgradeInjectable(EntityTranslationsService) + ); diff --git a/core/templates/services/exploration-features-backend-api.service.spec.ts b/core/templates/services/exploration-features-backend-api.service.spec.ts index 3933217e9bfb..ffa99278194a 100644 --- a/core/templates/services/exploration-features-backend-api.service.spec.ts +++ b/core/templates/services/exploration-features-backend-api.service.spec.ts @@ -16,32 +16,33 @@ * @fileoverview Tests for ExplorationFeaturesBackendApiService. */ -import { HttpClientTestingModule, HttpTestingController } from - '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; -import { ExplorationFeaturesBackendApiService } from - 'services/exploration-features-backend-api.service'; +import {ExplorationFeaturesBackendApiService} from 'services/exploration-features-backend-api.service'; describe('exploration features backend api service', () => { - let explorationFeaturesBackendApiService: - ExplorationFeaturesBackendApiService; + let explorationFeaturesBackendApiService: ExplorationFeaturesBackendApiService; let httpTestingController: HttpTestingController; var ERROR_STATUS_CODE = 500; var sampleDataResults = { explorationIsCurated: true, - alwaysAskLearnersForAnswerDetails: false + alwaysAskLearnersForAnswerDetails: false, }; beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [ExplorationFeaturesBackendApiService] + providers: [ExplorationFeaturesBackendApiService], }); explorationFeaturesBackendApiService = TestBed.get( - ExplorationFeaturesBackendApiService); + ExplorationFeaturesBackendApiService + ); httpTestingController = TestBed.get(HttpTestingController); }); @@ -49,43 +50,42 @@ describe('exploration features backend api service', () => { httpTestingController.verify(); }); - it('should successfully fetch data from the backend', - fakeAsync(() => { - var successHandler = jasmine.createSpy('success'); - var failHandler = jasmine.createSpy('fail'); + it('should successfully fetch data from the backend', fakeAsync(() => { + var successHandler = jasmine.createSpy('success'); + var failHandler = jasmine.createSpy('fail'); - explorationFeaturesBackendApiService.fetchExplorationFeaturesAsync('0') - .then(successHandler, failHandler); + explorationFeaturesBackendApiService + .fetchExplorationFeaturesAsync('0') + .then(successHandler, failHandler); - var req = httpTestingController.expectOne('/explorehandler/features/0'); - expect(req.request.method).toEqual('GET'); - req.flush(sampleDataResults); + var req = httpTestingController.expectOne('/explorehandler/features/0'); + expect(req.request.method).toEqual('GET'); + req.flush(sampleDataResults); - flushMicrotasks(); + flushMicrotasks(); - expect(successHandler).toHaveBeenCalled(); - expect(failHandler).not.toHaveBeenCalled(); - }) - ); + expect(successHandler).toHaveBeenCalled(); + expect(failHandler).not.toHaveBeenCalled(); + })); - it('should use rejection handler if data backend request failed', - fakeAsync(() => { - var successHandler = jasmine.createSpy('success'); - var failHandler = jasmine.createSpy('fail'); + it('should use rejection handler if data backend request failed', fakeAsync(() => { + var successHandler = jasmine.createSpy('success'); + var failHandler = jasmine.createSpy('fail'); - explorationFeaturesBackendApiService.fetchExplorationFeaturesAsync('0') - .then(successHandler, failHandler); + explorationFeaturesBackendApiService + .fetchExplorationFeaturesAsync('0') + .then(successHandler, failHandler); - var req = httpTestingController.expectOne('/explorehandler/features/0'); - expect(req.request.method).toEqual('GET'); - req.flush('Error loading data.', { - status: ERROR_STATUS_CODE, statusText: 'Invalid Request' - }); + var req = httpTestingController.expectOne('/explorehandler/features/0'); + expect(req.request.method).toEqual('GET'); + req.flush('Error loading data.', { + status: ERROR_STATUS_CODE, + statusText: 'Invalid Request', + }); - flushMicrotasks(); + flushMicrotasks(); - expect(successHandler).not.toHaveBeenCalled(); - expect(failHandler).toHaveBeenCalled(); - }) - ); + expect(successHandler).not.toHaveBeenCalled(); + expect(failHandler).toHaveBeenCalled(); + })); }); diff --git a/core/templates/services/exploration-features-backend-api.service.ts b/core/templates/services/exploration-features-backend-api.service.ts index 41124ac3e366..db9b65323d34 100644 --- a/core/templates/services/exploration-features-backend-api.service.ts +++ b/core/templates/services/exploration-features-backend-api.service.ts @@ -17,17 +17,16 @@ * configured to support. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; -import { ServicesConstants } from 'services/services.constants'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; +import {ServicesConstants} from 'services/services.constants'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; interface ExplorationFeaturesBackendDict { - 'exploration_is_curated': boolean; - 'always_ask_learners_for_answer_details': boolean; + exploration_is_curated: boolean; + always_ask_learners_for_answer_details: boolean; } export interface ExplorationFeatures { @@ -36,30 +35,41 @@ export interface ExplorationFeatures { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ExplorationFeaturesBackendApiService { constructor( - private http: HttpClient, - private urlInterpolationService: UrlInterpolationService) {} + private http: HttpClient, + private urlInterpolationService: UrlInterpolationService + ) {} async fetchExplorationFeaturesAsync( - explorationId: string): Promise { - return this.http.get( - this.urlInterpolationService.interpolateUrl( - ServicesConstants.EXPLORATION_FEATURES_URL, - {exploration_id: explorationId} - ) as string - ).toPromise().then(response => ({ - explorationIsCurated: response.exploration_is_curated, - alwaysAskLearnersForAnswerDetails: ( - response.always_ask_learners_for_answer_details), - }), errorResponse => { - throw new Error(errorResponse.error.error); - }); + explorationId: string + ): Promise { + return this.http + .get( + this.urlInterpolationService.interpolateUrl( + ServicesConstants.EXPLORATION_FEATURES_URL, + {exploration_id: explorationId} + ) as string + ) + .toPromise() + .then( + response => ({ + explorationIsCurated: response.exploration_is_curated, + alwaysAskLearnersForAnswerDetails: + response.always_ask_learners_for_answer_details, + }), + errorResponse => { + throw new Error(errorResponse.error.error); + } + ); } } -angular.module('oppia').factory( - 'ExplorationFeaturesBackendApiService', - downgradeInjectable(ExplorationFeaturesBackendApiService)); +angular + .module('oppia') + .factory( + 'ExplorationFeaturesBackendApiService', + downgradeInjectable(ExplorationFeaturesBackendApiService) + ); diff --git a/core/templates/services/exploration-features.service.spec.ts b/core/templates/services/exploration-features.service.spec.ts index e17c255ef9df..62e8197528f0 100644 --- a/core/templates/services/exploration-features.service.spec.ts +++ b/core/templates/services/exploration-features.service.spec.ts @@ -1,4 +1,3 @@ - // Copyright 2020 The Oppia Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,12 +16,14 @@ * @fileoverview Unit test for ExplorationFeaturesService */ -import { TestBed } from '@angular/core/testing'; -import { ParamChangeBackendDict } from 'domain/exploration/ParamChangeObjectFactory'; +import {TestBed} from '@angular/core/testing'; +import {ParamChangeBackendDict} from 'domain/exploration/ParamChangeObjectFactory'; -import { ExplorationFeaturesService, ExplorationDataDict} from - 'services/exploration-features.service'; -import { ExplorationFeatures } from './exploration-features-backend-api.service'; +import { + ExplorationFeaturesService, + ExplorationDataDict, +} from 'services/exploration-features.service'; +import {ExplorationFeatures} from './exploration-features-backend-api.service'; describe('ExplorationFeatureService', () => { let explorationFeatureService: ExplorationFeaturesService; @@ -41,11 +42,11 @@ describe('ExplorationFeatureService', () => { // for complete the ExplorationFeatures interface. featureData = { explorationIsCurated: true, - alwaysAskLearnersForAnswerDetails: false + alwaysAskLearnersForAnswerDetails: false, }; explorationData = { param_changes: [testParamChange], - states: {} + states: {}, }; explorationData2 = { param_changes: [], @@ -55,51 +56,51 @@ describe('ExplorationFeatureService', () => { classifier_model_id: '', content: { content_id: 'content', - html: '' + html: '', }, recorded_voiceovers: { voiceovers_mapping: { content: {}, - default_outcome: {} - } + default_outcome: {}, + }, }, interaction: { answer_groups: [], confirmed_unclassified_answers: [], customization_args: { buttonText: { - value: 'Continue' - } + value: 'Continue', + }, }, default_outcome: { dest: 'End State', dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: '' + html: '', }, param_changes: [], labelled_as_correct: true, refresher_exploration_id: null, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, hints: [], solution: null, - id: 'Continue' + id: 'Continue', }, linked_skill_id: null, solicit_answer_details: false, - card_is_checkpoint: false - } - } + card_is_checkpoint: false, + }, + }, }; testParamChange = { name: 'param_1', generator_id: 'test_id', customization_args: { parse_with_jinja: true, - value: '1' - } + value: '1', + }, }; }); @@ -107,25 +108,27 @@ describe('ExplorationFeatureService', () => { explorationFeatureService.init(explorationData, featureData); expect(explorationFeatureService.isInitialized()).toEqual(true); expect(explorationFeatureService.areParametersEnabled()).toEqual(true); - expect(explorationFeatureService.isPlaythroughRecordingEnabled()) - .toEqual(true); + expect(explorationFeatureService.isPlaythroughRecordingEnabled()).toEqual( + true + ); }); it('should init the exploration features from states', () => { explorationFeatureService.init(explorationData2, featureData); expect(explorationFeatureService.isInitialized()).toEqual(true); expect(explorationFeatureService.areParametersEnabled()).toEqual(true); - expect(explorationFeatureService.isPlaythroughRecordingEnabled()) - .toEqual(true); + expect(explorationFeatureService.isPlaythroughRecordingEnabled()).toEqual( + true + ); }); - it('should not init the exploration feature if service is initialized', - () => { - ExplorationFeaturesService.serviceIsInitialized = true; - explorationFeatureService.init(explorationData, featureData); - expect(explorationFeatureService.isInitialized()).toEqual(true); - expect(explorationFeatureService.areParametersEnabled()).toEqual(false); - expect(explorationFeatureService.isPlaythroughRecordingEnabled()) - .toEqual(false); - }); + it('should not init the exploration feature if service is initialized', () => { + ExplorationFeaturesService.serviceIsInitialized = true; + explorationFeatureService.init(explorationData, featureData); + expect(explorationFeatureService.isInitialized()).toEqual(true); + expect(explorationFeatureService.areParametersEnabled()).toEqual(false); + expect(explorationFeatureService.isPlaythroughRecordingEnabled()).toEqual( + false + ); + }); }); diff --git a/core/templates/services/exploration-features.service.ts b/core/templates/services/exploration-features.service.ts index 0df8390838fe..0fcc2df667de 100644 --- a/core/templates/services/exploration-features.service.ts +++ b/core/templates/services/exploration-features.service.ts @@ -17,21 +17,20 @@ * the exploration editor. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { ExplorationFeatures } from - 'services/exploration-features-backend-api.service'; -import { ParamChangeBackendDict } from 'domain/exploration/ParamChangeObjectFactory'; -import { StateObjectsBackendDict } from 'domain/exploration/StatesObjectFactory'; +import {ExplorationFeatures} from 'services/exploration-features-backend-api.service'; +import {ParamChangeBackendDict} from 'domain/exploration/ParamChangeObjectFactory'; +import {StateObjectsBackendDict} from 'domain/exploration/StatesObjectFactory'; export interface ExplorationDataDict { - 'param_changes': ParamChangeBackendDict[] | []; + param_changes: ParamChangeBackendDict[] | []; states: StateObjectsBackendDict; } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ExplorationFeaturesService { /** @@ -40,7 +39,7 @@ export class ExplorationFeaturesService { static serviceIsInitialized = false; static settings = { areParametersEnabled: false, - isPlaythroughRecordingEnabled: false + isPlaythroughRecordingEnabled: false, }; /** @@ -50,15 +49,18 @@ export class ExplorationFeaturesService { * @param featuresData - An ExplorationFeatures object. */ init( - explorationData: ExplorationDataDict, - featuresData: ExplorationFeatures): void { + explorationData: ExplorationDataDict, + featuresData: ExplorationFeatures + ): void { if (ExplorationFeaturesService.serviceIsInitialized) { return; } ExplorationFeaturesService.settings.isPlaythroughRecordingEnabled = featuresData.explorationIsCurated; - if (explorationData.param_changes && - explorationData.param_changes.length > 0) { + if ( + explorationData.param_changes && + explorationData.param_changes.length > 0 + ) { this.enableParameters(); } else { for (var state in explorationData.states) { @@ -100,6 +102,9 @@ export class ExplorationFeaturesService { } } -angular.module('oppia').factory( - 'ExplorationFeaturesService', - downgradeInjectable(ExplorationFeaturesService)); +angular + .module('oppia') + .factory( + 'ExplorationFeaturesService', + downgradeInjectable(ExplorationFeaturesService) + ); diff --git a/core/templates/services/exploration-html-formatter.service.spec.ts b/core/templates/services/exploration-html-formatter.service.spec.ts index 8e3d624eb4ab..fddd239a07ea 100644 --- a/core/templates/services/exploration-html-formatter.service.spec.ts +++ b/core/templates/services/exploration-html-formatter.service.spec.ts @@ -17,42 +17,43 @@ * by both the learner and editor views */ -import { TestBed } from '@angular/core/testing'; +import {TestBed} from '@angular/core/testing'; -import { CamelCaseToHyphensPipe } from - 'filters/string-utility-filters/camel-case-to-hyphens.pipe'; -import { ExplorationHtmlFormatterService } from - 'services/exploration-html-formatter.service'; -import { SubtitledHtml } from - 'domain/exploration/subtitled-html.model'; -import { SubtitledUnicode } from - 'domain/exploration/SubtitledUnicodeObjectFactory'; +import {CamelCaseToHyphensPipe} from 'filters/string-utility-filters/camel-case-to-hyphens.pipe'; +import {ExplorationHtmlFormatterService} from 'services/exploration-html-formatter.service'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {SubtitledUnicode} from 'domain/exploration/SubtitledUnicodeObjectFactory'; describe('Exploration Html Formatter Service', () => { let ehfs: ExplorationHtmlFormatterService; beforeEach(() => { TestBed.configureTestingModule({ - providers: [CamelCaseToHyphensPipe] + providers: [CamelCaseToHyphensPipe], }); ehfs = TestBed.inject(ExplorationHtmlFormatterService); }); - it('should correctly set interaction HTML for a non migrated interaction ' + - 'when it is in editor mode', () => { - var interactionId = 'EndExploration'; - let custArgs = { - placeholder: {value: new SubtitledUnicode('enter here', '')}, - rows: {value: 1} - }; - var expectedHtmlTag = '' + - ''; - expect(ehfs.getInteractionHtml(interactionId, custArgs, true, null, null)) - .toBe(expectedHtmlTag); - }); + it( + 'should correctly set interaction HTML for a non migrated interaction ' + + 'when it is in editor mode', + () => { + var interactionId = 'EndExploration'; + let custArgs = { + placeholder: {value: new SubtitledUnicode('enter here', '')}, + rows: {value: 1}, + }; + var expectedHtmlTag = + '' + + ''; + expect( + ehfs.getInteractionHtml(interactionId, custArgs, true, null, null) + ).toBe(expectedHtmlTag); + } + ); it('should fail for unknown interaction', () => { expect(() => { @@ -73,94 +74,126 @@ describe('Exploration Html Formatter Service', () => { it('should fail for non-alphabetic label for focus target', () => { expect(() => { ehfs.getInteractionHtml( - 'GraphInput', {}, true, '', 'savedSolution'); + 'GraphInput', + {}, + true, + '', + 'savedSolution' + ); }).toThrowError('Unexpected label for focus target: .'); }); - it('should correctly set [last-answer] for MigratedInteractions when it' + - ' is in editor mode', () => { - var interactionId = 'GraphInput'; - let custArgs = { - placeholder: {value: new SubtitledUnicode('enter here', '')}, - rows: {value: 1} - }; - var expectedHtmlTag = '' + - ''; - expect( - ehfs.getInteractionHtml(interactionId, custArgs, true, null, null) - ).toBe(expectedHtmlTag); - }); - - it('should correctly set [last-answer] for MigratedInteractions when it' + - ' is in editor mode', () => { - var interactionId = 'GraphInput'; - let custArgs = { - placeholder: {value: new SubtitledUnicode('enter here', '')}, - rows: {value: 1} - }; - var expectedHtmlTag = '' + - ''; - expect(ehfs.getInteractionHtml( - interactionId, custArgs, true, null, null) - ).toBe(expectedHtmlTag); - }); - - it('should correctly set interaction HTML when it is in player mode', + it( + 'should correctly set [last-answer] for MigratedInteractions when it' + + ' is in editor mode', () => { - var interactionId = 'EndExploration'; - var focusLabel = 'sampleLabel'; - var expectedHtmlTag = '' + - ''; + var interactionId = 'GraphInput'; + let custArgs = { + placeholder: {value: new SubtitledUnicode('enter here', '')}, + rows: {value: 1}, + }; + var expectedHtmlTag = + '' + + ''; expect( - ehfs.getInteractionHtml(interactionId, {}, false, focusLabel, null) + ehfs.getInteractionHtml(interactionId, custArgs, true, null, null) ).toBe(expectedHtmlTag); - }); + } + ); - it('should correctly set interaction HTML when solution has been provided', + it( + 'should correctly set [last-answer] for MigratedInteractions when it' + + ' is in editor mode', () => { - var interactionId = 'EndExploration'; - var focusLabel = 'sampleLabel'; - var expectedHtmlTag = '' + - ''; - expect( - ehfs.getInteractionHtml( - interactionId, {}, false, focusLabel, 'savedSolution') - ).toBe(expectedHtmlTag); - interactionId = 'GraphInput'; - focusLabel = 'sampleLabel'; - expectedHtmlTag = '' + + var interactionId = 'GraphInput'; + let custArgs = { + placeholder: {value: new SubtitledUnicode('enter here', '')}, + rows: {value: 1}, + }; + var expectedHtmlTag = + '' + ''; expect( - ehfs.getInteractionHtml( - interactionId, {}, false, focusLabel, 'savedSolution') + ehfs.getInteractionHtml(interactionId, custArgs, true, null, null) ).toBe(expectedHtmlTag); - }); + } + ); + + it('should correctly set interaction HTML when it is in player mode', () => { + var interactionId = 'EndExploration'; + var focusLabel = 'sampleLabel'; + var expectedHtmlTag = + '' + + ''; + expect( + ehfs.getInteractionHtml(interactionId, {}, false, focusLabel, null) + ).toBe(expectedHtmlTag); + }); + + it('should correctly set interaction HTML when solution has been provided', () => { + var interactionId = 'EndExploration'; + var focusLabel = 'sampleLabel'; + var expectedHtmlTag = + '' + + ''; + expect( + ehfs.getInteractionHtml( + interactionId, + {}, + false, + focusLabel, + 'savedSolution' + ) + ).toBe(expectedHtmlTag); + interactionId = 'GraphInput'; + focusLabel = 'sampleLabel'; + expectedHtmlTag = + '' + + ''; + expect( + ehfs.getInteractionHtml( + interactionId, + {}, + false, + focusLabel, + 'savedSolution' + ) + ).toBe(expectedHtmlTag); + }); it('should set answer HTML correctly', () => { var interactionId = 'sampleId'; var answer = 'sampleAnswer'; var interactionCustomizationArgs = { choices: { - value: [new SubtitledHtml('sampleChoice', '')] - } + value: [new SubtitledHtml('sampleChoice', '')], + }, }; - var expectedHtmlTag = ''; - expect(ehfs.getAnswerHtml( - answer, interactionId, interactionCustomizationArgs) + expect( + ehfs.getAnswerHtml(answer, interactionId, interactionCustomizationArgs) ).toBe(expectedHtmlTag); }); @@ -175,16 +208,23 @@ describe('Exploration Html Formatter Service', () => { var answer = 'sampleAnswer'; var interactionCustomizationArgs = { choices: { - value: [new SubtitledHtml('sampleChoice', '')] - } + value: [new SubtitledHtml('sampleChoice', '')], + }, }; - var expectedHtmlTag = ''; - expect(ehfs.getShortAnswerHtml( - answer, interactionId, interactionCustomizationArgs) + expect( + ehfs.getShortAnswerHtml( + answer, + interactionId, + interactionCustomizationArgs + ) ).toBe(expectedHtmlTag); }); }); diff --git a/core/templates/services/exploration-html-formatter.service.ts b/core/templates/services/exploration-html-formatter.service.ts index 78d5544aa196..dd6dd303d9f1 100644 --- a/core/templates/services/exploration-html-formatter.service.ts +++ b/core/templates/services/exploration-html-formatter.service.ts @@ -16,20 +16,20 @@ * @fileoverview Utility services for explorations which may be shared by both * the learner and editor views. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { CamelCaseToHyphensPipe } from 'filters/string-utility-filters/camel-case-to-hyphens.pipe'; -import { ExtensionTagAssemblerService } from 'services/extension-tag-assembler.service'; -import { HtmlEscaperService } from 'services/html-escaper.service'; -import { InteractionAnswer } from 'interactions/answer-defs'; -import { InteractionCustomizationArgs } from 'interactions/customization-args-defs'; +import {AppConstants} from 'app.constants'; +import {CamelCaseToHyphensPipe} from 'filters/string-utility-filters/camel-case-to-hyphens.pipe'; +import {ExtensionTagAssemblerService} from 'services/extension-tag-assembler.service'; +import {HtmlEscaperService} from 'services/html-escaper.service'; +import {InteractionAnswer} from 'interactions/answer-defs'; +import {InteractionCustomizationArgs} from 'interactions/customization-args-defs'; // A service that provides a number of utility functions useful to both the // editor and player. @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ExplorationHtmlFormatterService { private readonly migratedInteractions: string[] = [ @@ -56,9 +56,9 @@ export class ExplorationHtmlFormatterService { ]; constructor( - private camelCaseToHyphens: CamelCaseToHyphensPipe, - private extensionTagAssembler: ExtensionTagAssemblerService, - private htmlEscaper: HtmlEscaperService + private camelCaseToHyphens: CamelCaseToHyphensPipe, + private extensionTagAssembler: ExtensionTagAssemblerService, + private htmlEscaper: HtmlEscaperService ) {} /** @@ -79,11 +79,11 @@ export class ExplorationHtmlFormatterService { * scope where the return value of this function is compiled. */ getInteractionHtml( - interactionId: string, - interactionCustomizationArgs: InteractionCustomizationArgs, - parentHasLastAnswerProperty: boolean, - labelForFocusTarget: string | null, - savedSolution: 'savedSolution' | null + interactionId: string, + interactionCustomizationArgs: InteractionCustomizationArgs, + parentHasLastAnswerProperty: boolean, + labelForFocusTarget: string | null, + savedSolution: 'savedSolution' | null ): string { let availableInteractionIds = Array.prototype.concat.apply( [], @@ -98,10 +98,11 @@ export class ExplorationHtmlFormatterService { // The createElement is safe because we verify that the interactionId // belongs to a list of interaction IDs. let element = document.createElement( - `oppia-interactive-${htmlInteractionId}`); - element = ( - this.extensionTagAssembler.formatCustomizationArgAttrs( - element, interactionCustomizationArgs) + `oppia-interactive-${htmlInteractionId}` + ); + element = this.extensionTagAssembler.formatCustomizationArgAttrs( + element, + interactionCustomizationArgs ); // The setAttribute is safe because we verify that the savedSolution // is 'savedMemento()'. @@ -127,12 +128,13 @@ export class ExplorationHtmlFormatterService { element.setAttribute('label-for-focus-target', labelForFocusTarget); } else if (labelForFocusTarget) { throw new Error( - `Unexpected label for focus target: ${labelForFocusTarget}.`); + `Unexpected label for focus target: ${labelForFocusTarget}.` + ); } - let lastAnswerPropValue = ( - parentHasLastAnswerProperty ? 'lastAnswer' : 'null' - ); + let lastAnswerPropValue = parentHasLastAnswerProperty + ? 'lastAnswer' + : 'null'; // The setAttribute is safe because the only possible value is 'lastAnswer' // as per the line above. element.setAttribute('last-answer', lastAnswerPropValue); @@ -160,9 +162,9 @@ export class ExplorationHtmlFormatterService { } getAnswerHtml( - answer: InteractionAnswer, - interactionId: string | null, - interactionCustomizationArgs: InteractionCustomizationArgs + answer: InteractionAnswer, + interactionId: string | null, + interactionCustomizationArgs: InteractionCustomizationArgs ): string { // TODO(#14464): Remove this check once interaction ID is // not allowed to be null. @@ -170,21 +172,24 @@ export class ExplorationHtmlFormatterService { throw new Error('InteractionId cannot be null'); } var element = document.createElement( - `oppia-response-${this.camelCaseToHyphens.transform(interactionId)}`); + `oppia-response-${this.camelCaseToHyphens.transform(interactionId)}` + ); element.setAttribute('answer', this.htmlEscaper.objToEscapedJson(answer)); // TODO(sll): Get rid of this special case for multiple choice. if ('choices' in interactionCustomizationArgs) { let interactionChoices = interactionCustomizationArgs.choices.value; element.setAttribute( - 'choices', this.htmlEscaper.objToEscapedJson(interactionChoices)); + 'choices', + this.htmlEscaper.objToEscapedJson(interactionChoices) + ); } return element.outerHTML; } getShortAnswerHtml( - answer: InteractionAnswer, - interactionId: string, - interactionCustomizationArgs: InteractionCustomizationArgs + answer: InteractionAnswer, + interactionId: string, + interactionCustomizationArgs: InteractionCustomizationArgs ): string { let element = document.createElement( `oppia-short-response-${this.camelCaseToHyphens.transform(interactionId)}` @@ -194,12 +199,17 @@ export class ExplorationHtmlFormatterService { if ('choices' in interactionCustomizationArgs) { let interactionChoices = interactionCustomizationArgs.choices.value; element.setAttribute( - 'choices', this.htmlEscaper.objToEscapedJson(interactionChoices)); + 'choices', + this.htmlEscaper.objToEscapedJson(interactionChoices) + ); } return element.outerHTML; } } -angular.module('oppia').factory( - 'ExplorationHtmlFormatterService', - downgradeInjectable(ExplorationHtmlFormatterService)); +angular + .module('oppia') + .factory( + 'ExplorationHtmlFormatterService', + downgradeInjectable(ExplorationHtmlFormatterService) + ); diff --git a/core/templates/services/exploration-improvements-backend-api.service.spec.ts b/core/templates/services/exploration-improvements-backend-api.service.spec.ts index d44756ca353f..fe1aa5f54bd8 100644 --- a/core/templates/services/exploration-improvements-backend-api.service.spec.ts +++ b/core/templates/services/exploration-improvements-backend-api.service.spec.ts @@ -16,9 +16,11 @@ * @fileoverview Unit tests for the ExplorationImprovementsBackendApiService. */ -import { HttpClientTestingModule, HttpTestingController } from - '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; import { ExplorationImprovementsConfig, @@ -33,18 +35,18 @@ import { ExplorationImprovementsHistoryResponse, ExplorationImprovementsHistoryResponseBackendDict, ExplorationImprovementsResponse, - ExplorationImprovementsResponseBackendDict + ExplorationImprovementsResponseBackendDict, } from 'services/exploration-improvements-backend-api.service'; describe('Exploration stats back-end API service', () => { let httpTestingController: HttpTestingController; - let explorationImprovementsBackendApiService: - ExplorationImprovementsBackendApiService; + let explorationImprovementsBackendApiService: ExplorationImprovementsBackendApiService; beforeEach(() => { TestBed.configureTestingModule({imports: [HttpClientTestingModule]}); - explorationImprovementsBackendApiService = ( - TestBed.get(ExplorationImprovementsBackendApiService)); + explorationImprovementsBackendApiService = TestBed.get( + ExplorationImprovementsBackendApiService + ); httpTestingController = TestBed.get(HttpTestingController); }); @@ -52,7 +54,7 @@ describe('Exploration stats back-end API service', () => { httpTestingController.verify(); }); - it('should return an ExplorationImprovementsResponse', fakeAsync(async() => { + it('should return an ExplorationImprovementsResponse', fakeAsync(async () => { const taskDict: ExplorationTaskBackendDict = { entity_type: 'exploration', entity_id: 'eid', @@ -66,11 +68,12 @@ describe('Exploration stats back-end API service', () => { resolved_on_msecs: 123456789, }; - const response = ( - explorationImprovementsBackendApiService.getTasksAsync('eid')); + const response = + explorationImprovementsBackendApiService.getTasksAsync('eid'); - const req = ( - httpTestingController.expectOne('/improvements/exploration/eid')); + const req = httpTestingController.expectOne( + '/improvements/exploration/eid' + ); expect(req.request.method).toEqual('GET'); req.flush({ open_tasks: [taskDict], @@ -81,46 +84,12 @@ describe('Exploration stats back-end API service', () => { expect(await response).toEqual( new ExplorationImprovementsResponse( [ExplorationTaskModel.createFromBackendDict(taskDict)], - new Map([['Introduction', ['high_bounce_rate']]]))); + new Map([['Introduction', ['high_bounce_rate']]]) + ) + ); })); - it('should return an ExplorationImprovementsHistoryResponse', - fakeAsync(async() => { - const taskDict: ExplorationTaskBackendDict = { - entity_type: 'exploration', - entity_id: 'eid', - entity_version: 1, - task_type: 'high_bounce_rate', - target_type: 'state', - target_id: 'Introduction', - issue_description: '20% of learners dropped at this state', - status: 'resolved', - resolver_username: 'test_user', - resolved_on_msecs: 123456789, - }; - - const response = ( - explorationImprovementsBackendApiService.getHistoryPageAsync('eid')); - - const req = httpTestingController.expectOne( - '/improvements/history/exploration/eid'); - expect(req.request.method).toEqual('GET'); - req.flush({ - results: [taskDict], - cursor: 'cursor123', - more: true, - } as ExplorationImprovementsHistoryResponseBackendDict); - flushMicrotasks(); - - expect(await response).toEqual( - new ExplorationImprovementsHistoryResponse( - [ExplorationTaskModel.createFromBackendDict(taskDict)], - 'cursor123', - true)); - })); - - it('should return an ExplorationImprovementsHistoryResponse when given a ' + - 'cursor', fakeAsync(async() => { + it('should return an ExplorationImprovementsHistoryResponse', fakeAsync(async () => { const taskDict: ExplorationTaskBackendDict = { entity_type: 'exploration', entity_id: 'eid', @@ -134,28 +103,74 @@ describe('Exploration stats back-end API service', () => { resolved_on_msecs: 123456789, }; - const response = ( - explorationImprovementsBackendApiService.getHistoryPageAsync( - 'eid', 'cursor123')); + const response = + explorationImprovementsBackendApiService.getHistoryPageAsync('eid'); const req = httpTestingController.expectOne( - '/improvements/history/exploration/eid?cursor=cursor123'); + '/improvements/history/exploration/eid' + ); expect(req.request.method).toEqual('GET'); req.flush({ results: [taskDict], - cursor: 'cursor456', - more: false, + cursor: 'cursor123', + more: true, } as ExplorationImprovementsHistoryResponseBackendDict); flushMicrotasks(); expect(await response).toEqual( new ExplorationImprovementsHistoryResponse( [ExplorationTaskModel.createFromBackendDict(taskDict)], - 'cursor456', - false)); + 'cursor123', + true + ) + ); })); - it('should try to post a task dict to the back-end', fakeAsync(async() => { + it( + 'should return an ExplorationImprovementsHistoryResponse when given a ' + + 'cursor', + fakeAsync(async () => { + const taskDict: ExplorationTaskBackendDict = { + entity_type: 'exploration', + entity_id: 'eid', + entity_version: 1, + task_type: 'high_bounce_rate', + target_type: 'state', + target_id: 'Introduction', + issue_description: '20% of learners dropped at this state', + status: 'resolved', + resolver_username: 'test_user', + resolved_on_msecs: 123456789, + }; + + const response = + explorationImprovementsBackendApiService.getHistoryPageAsync( + 'eid', + 'cursor123' + ); + + const req = httpTestingController.expectOne( + '/improvements/history/exploration/eid?cursor=cursor123' + ); + expect(req.request.method).toEqual('GET'); + req.flush({ + results: [taskDict], + cursor: 'cursor456', + more: false, + } as ExplorationImprovementsHistoryResponseBackendDict); + flushMicrotasks(); + + expect(await response).toEqual( + new ExplorationImprovementsHistoryResponse( + [ExplorationTaskModel.createFromBackendDict(taskDict)], + 'cursor456', + false + ) + ); + }) + ); + + it('should try to post a task dict to the back-end', fakeAsync(async () => { const task = ExplorationTaskModel.createFromBackendDict({ entity_type: 'exploration', entity_id: 'eid', @@ -171,14 +186,16 @@ describe('Exploration stats back-end API service', () => { const onSuccess = jasmine.createSpy('onSuccess'); const onFailure = jasmine.createSpy('onFailure'); - explorationImprovementsBackendApiService.postTasksAsync('eid', [task]) + explorationImprovementsBackendApiService + .postTasksAsync('eid', [task]) .then(onSuccess, onFailure); - const req = ( - httpTestingController.expectOne('/improvements/exploration/eid')); + const req = httpTestingController.expectOne( + '/improvements/exploration/eid' + ); expect(req.request.method).toEqual('POST'); expect(req.request.body).toEqual({ - task_entries: [task.toPayloadDict()] + task_entries: [task.toPayloadDict()], }); req.flush({}); flushMicrotasks(); @@ -187,10 +204,11 @@ describe('Exploration stats back-end API service', () => { expect(onFailure).not.toHaveBeenCalled(); })); - it('should not make HTTP call when posting empty list', fakeAsync(async() => { + it('should not make HTTP call when posting empty list', fakeAsync(async () => { const onSuccess = jasmine.createSpy('onSuccess'); const onFailure = jasmine.createSpy('onFailure'); - explorationImprovementsBackendApiService.postTasksAsync('eid', []) + explorationImprovementsBackendApiService + .postTasksAsync('eid', []) .then(onSuccess, onFailure); httpTestingController.expectNone('/improvements/exploration/eid'); @@ -200,24 +218,26 @@ describe('Exploration stats back-end API service', () => { expect(onFailure).not.toHaveBeenCalled(); })); - it('should return an ExplorationImprovementsConfig', fakeAsync(async() => { - const response = ( - explorationImprovementsBackendApiService.getConfigAsync('eid')); + it('should return an ExplorationImprovementsConfig', fakeAsync(async () => { + const response = + explorationImprovementsBackendApiService.getConfigAsync('eid'); - const req = ( - httpTestingController.expectOne('/improvements/config/exploration/eid')); + const req = httpTestingController.expectOne( + '/improvements/config/exploration/eid' + ); expect(req.request.method).toEqual('GET'); req.flush({ exploration_id: 'eid', exploration_version: 1, is_improvements_tab_enabled: true, high_bounce_rate_task_state_bounce_rate_creation_threshold: 0.25, - high_bounce_rate_task_state_bounce_rate_obsoletion_threshold: 0.20, + high_bounce_rate_task_state_bounce_rate_obsoletion_threshold: 0.2, high_bounce_rate_task_minimum_exploration_starts: 100, } as ExplorationImprovementsConfigBackendDict); flushMicrotasks(); expect(await response).toEqual( - new ExplorationImprovementsConfig('eid', 1, true, 0.25, 0.20, 100)); + new ExplorationImprovementsConfig('eid', 1, true, 0.25, 0.2, 100) + ); })); }); diff --git a/core/templates/services/exploration-improvements-backend-api.service.ts b/core/templates/services/exploration-improvements-backend-api.service.ts index f0dead5e9236..3bf80f6ebd7e 100644 --- a/core/templates/services/exploration-improvements-backend-api.service.ts +++ b/core/templates/services/exploration-improvements-backend-api.service.ts @@ -16,9 +16,9 @@ * @fileoverview Service for fetching improvement tasks from the backend. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { HttpClient, HttpParams } from '@angular/common/http'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {HttpClient, HttpParams} from '@angular/common/http'; +import {Injectable} from '@angular/core'; import { ExplorationImprovementsConfig, @@ -27,113 +27,145 @@ import { import { ExplorationTask, ExplorationTaskBackendDict, - ExplorationTaskModel + ExplorationTaskModel, } from 'domain/improvements/exploration-task.model'; -import { ImprovementsConstants } from - 'domain/improvements/improvements.constants'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; +import {ImprovementsConstants} from 'domain/improvements/improvements.constants'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; export interface ExplorationImprovementsResponseBackendDict { - 'open_tasks': ExplorationTaskBackendDict[]; - 'resolved_task_types_by_state_name': { + open_tasks: ExplorationTaskBackendDict[]; + resolved_task_types_by_state_name: { [stateName: string]: string[]; }; } export interface ExplorationImprovementsHistoryResponseBackendDict { - 'results': ExplorationTaskBackendDict[]; - 'cursor': string; - 'more': boolean; + results: ExplorationTaskBackendDict[]; + cursor: string; + more: boolean; } export class ExplorationImprovementsResponse { constructor( - public readonly openTasks: ExplorationTask[], - public readonly resolvedTaskTypesByStateName: Map) {} + public readonly openTasks: ExplorationTask[], + public readonly resolvedTaskTypesByStateName: Map + ) {} } export class ExplorationImprovementsHistoryResponse { constructor( - public readonly results: ExplorationTask[], - public readonly cursor: string, - public readonly more: boolean) {} + public readonly results: ExplorationTask[], + public readonly cursor: string, + public readonly more: boolean + ) {} } @Injectable({providedIn: 'root'}) export class ExplorationImprovementsBackendApiService { constructor( - private http: HttpClient, - private urlInterpolationService: UrlInterpolationService) {} + private http: HttpClient, + private urlInterpolationService: UrlInterpolationService + ) {} async getTasksAsync(expId: string): Promise { - const explorationImprovementsUrl = ( + const explorationImprovementsUrl = this.urlInterpolationService.interpolateUrl( - ImprovementsConstants.EXPLORATION_IMPROVEMENTS_URL, { - exploration_id: expId - })); - return this.http.get( - explorationImprovementsUrl - ).toPromise().then( - backendDict => new ExplorationImprovementsResponse( - backendDict.open_tasks.map( - d => ExplorationTaskModel.createFromBackendDict(d)), - new Map(Object.entries(backendDict.resolved_task_types_by_state_name))) - ); + ImprovementsConstants.EXPLORATION_IMPROVEMENTS_URL, + { + exploration_id: expId, + } + ); + return this.http + .get( + explorationImprovementsUrl + ) + .toPromise() + .then( + backendDict => + new ExplorationImprovementsResponse( + backendDict.open_tasks.map(d => + ExplorationTaskModel.createFromBackendDict(d) + ), + new Map( + Object.entries(backendDict.resolved_task_types_by_state_name) + ) + ) + ); } async postTasksAsync(expId: string, tasks: ExplorationTask[]): Promise { if (tasks.length === 0) { return; } - const explorationImprovementsUrl = ( + const explorationImprovementsUrl = this.urlInterpolationService.interpolateUrl( - ImprovementsConstants.EXPLORATION_IMPROVEMENTS_URL, { - exploration_id: expId - })); - return this.http.post(explorationImprovementsUrl, { - task_entries: tasks.map(t => t.toPayloadDict()) - }).toPromise(); + ImprovementsConstants.EXPLORATION_IMPROVEMENTS_URL, + { + exploration_id: expId, + } + ); + return this.http + .post(explorationImprovementsUrl, { + task_entries: tasks.map(t => t.toPayloadDict()), + }) + .toPromise(); } async getHistoryPageAsync( - expId: string, - cursor?: string): Promise { - const explorationImprovementsHistoryUrl = ( + expId: string, + cursor?: string + ): Promise { + const explorationImprovementsHistoryUrl = this.urlInterpolationService.interpolateUrl( - ImprovementsConstants.EXPLORATION_IMPROVEMENTS_HISTORY_URL, { - exploration_id: expId - })); + ImprovementsConstants.EXPLORATION_IMPROVEMENTS_HISTORY_URL, + { + exploration_id: expId, + } + ); let params = new HttpParams(); if (cursor) { params = params.append('cursor', cursor); } - return this.http.get( - explorationImprovementsHistoryUrl, {params} - ).toPromise().then( - backendDict => new ExplorationImprovementsHistoryResponse( - backendDict.results.map( - d => ExplorationTaskModel.createFromBackendDict(d)), - backendDict.cursor, - backendDict.more)); + return this.http + .get( + explorationImprovementsHistoryUrl, + {params} + ) + .toPromise() + .then( + backendDict => + new ExplorationImprovementsHistoryResponse( + backendDict.results.map(d => + ExplorationTaskModel.createFromBackendDict(d) + ), + backendDict.cursor, + backendDict.more + ) + ); } - async getConfigAsync( - expId: string): Promise { - const explorationImprovementsConfigUrl = ( + async getConfigAsync(expId: string): Promise { + const explorationImprovementsConfigUrl = this.urlInterpolationService.interpolateUrl( - ImprovementsConstants.EXPLORATION_IMPROVEMENTS_CONFIG_URL, { - exploration_id: expId - })); - return this.http.get( - explorationImprovementsConfigUrl - ).toPromise().then( - backendDict => - ExplorationImprovementsConfig.createFromBackendDict( - backendDict)); + ImprovementsConstants.EXPLORATION_IMPROVEMENTS_CONFIG_URL, + { + exploration_id: expId, + } + ); + return this.http + .get( + explorationImprovementsConfigUrl + ) + .toPromise() + .then(backendDict => + ExplorationImprovementsConfig.createFromBackendDict(backendDict) + ); } } -angular.module('oppia').factory( - 'ExplorationImprovementsBackendApiService', - downgradeInjectable(ExplorationImprovementsBackendApiService)); +angular + .module('oppia') + .factory( + 'ExplorationImprovementsBackendApiService', + downgradeInjectable(ExplorationImprovementsBackendApiService) + ); diff --git a/core/templates/services/exploration-improvements-task-registry.service.spec.ts b/core/templates/services/exploration-improvements-task-registry.service.spec.ts index 4f7a482e288a..54a29f08b556 100644 --- a/core/templates/services/exploration-improvements-task-registry.service.spec.ts +++ b/core/templates/services/exploration-improvements-task-registry.service.spec.ts @@ -16,31 +16,29 @@ * @fileoverview Unit tests for the ExplorationImprovementsTaskRegistryService. */ -import { TestBed } from '@angular/core/testing'; +import {TestBed} from '@angular/core/testing'; -import { AnswerStats, AnswerStatsBackendDict } from - 'domain/exploration/answer-stats.model'; +import { + AnswerStats, + AnswerStatsBackendDict, +} from 'domain/exploration/answer-stats.model'; import { ExplorationTask, ExplorationTaskType, ExplorationTaskBackendDict, - ExplorationTaskModel + ExplorationTaskModel, } from 'domain/improvements/exploration-task.model'; -import { HighBounceRateTask } from - 'domain/improvements/high-bounce-rate-task.model'; -import { StateStatsBackendDict } from - 'domain/statistics/state-stats-model'; -import { IneffectiveFeedbackLoopTask } from - 'domain/improvements/ineffective-feedback-loop-task.model'; -import { NeedsGuidingResponsesTask } from - 'domain/improvements/needs-guiding-response-task.model'; -import { SuccessiveIncorrectAnswersTask } from - 'domain/improvements/successive-incorrect-answers-task.model'; -import { ExplorationImprovementsConfig } from - 'domain/improvements/exploration-improvements-config.model'; -import { StateBackendDict } from 'domain/state/StateObjectFactory'; -import { ExplorationStats, ExplorationStatsBackendDict } from - 'domain/statistics/exploration-stats.model'; +import {HighBounceRateTask} from 'domain/improvements/high-bounce-rate-task.model'; +import {StateStatsBackendDict} from 'domain/statistics/state-stats-model'; +import {IneffectiveFeedbackLoopTask} from 'domain/improvements/ineffective-feedback-loop-task.model'; +import {NeedsGuidingResponsesTask} from 'domain/improvements/needs-guiding-response-task.model'; +import {SuccessiveIncorrectAnswersTask} from 'domain/improvements/successive-incorrect-answers-task.model'; +import {ExplorationImprovementsConfig} from 'domain/improvements/exploration-improvements-config.model'; +import {StateBackendDict} from 'domain/state/StateObjectFactory'; +import { + ExplorationStats, + ExplorationStatsBackendDict, +} from 'domain/statistics/exploration-stats.model'; import { PlaythroughIssue, PlaythroughIssueType, @@ -49,10 +47,8 @@ import { CyclicStateTransitionsCustomizationArgs, MultipleIncorrectSubmissionsCustomizationArgs, } from 'domain/statistics/playthrough-issue.model'; -import { StatesObjectFactory } from 'domain/exploration/StatesObjectFactory'; -import { ExplorationImprovementsTaskRegistryService } from - 'services/exploration-improvements-task-registry.service'; - +import {StatesObjectFactory} from 'domain/exploration/StatesObjectFactory'; +import {ExplorationImprovementsTaskRegistryService} from 'services/exploration-improvements-task-registry.service'; describe('Exploration improvements task registrar service', () => { let taskRegistryService: ExplorationImprovementsTaskRegistryService; @@ -60,13 +56,10 @@ describe('Exploration improvements task registrar service', () => { let statesObjectFactory: StatesObjectFactory; let answerStatsBackendDict: AnswerStatsBackendDict; - let cstPlaythroughIssueBackendDict: - PlaythroughIssueBackendDict; - let eqPlaythroughIssueBackendDict: - PlaythroughIssueBackendDict; + let cstPlaythroughIssueBackendDict: PlaythroughIssueBackendDict; + let eqPlaythroughIssueBackendDict: PlaythroughIssueBackendDict; let expStatsBackendDict: ExplorationStatsBackendDict; - let misPlaythroughIssueBackendDict: - PlaythroughIssueBackendDict; + let misPlaythroughIssueBackendDict: PlaythroughIssueBackendDict; let stateBackendDict: StateBackendDict; let stateStatsBackendDict: StateStatsBackendDict; let statesBackendDict: {[stateName: string]: StateBackendDict}; @@ -77,25 +70,32 @@ describe('Exploration improvements task registrar service', () => { const expVersion = 1; beforeEach(() => { - taskRegistryService = ( - TestBed.get(ExplorationImprovementsTaskRegistryService)); + taskRegistryService = TestBed.get( + ExplorationImprovementsTaskRegistryService + ); statesObjectFactory = TestBed.get(StatesObjectFactory); config = new ExplorationImprovementsConfig( - expId, expVersion, true, 0.25, 0.20, 100); + expId, + expVersion, + true, + 0.25, + 0.2, + 100 + ); stateBackendDict = { classifier_model_id: null, content: { content_id: 'content', - html: '' + html: '', }, recorded_voiceovers: { voiceovers_mapping: { content: {}, - default_outcome: {} - } + default_outcome: {}, + }, }, interaction: { answer_groups: [], @@ -104,20 +104,20 @@ describe('Exploration improvements task registrar service', () => { placeholder: { value: { content_id: 'ca_placeholder_0', - unicode_str: '' - } + unicode_str: '', + }, }, - rows: { value: 1 }, + rows: {value: 1}, catchMisspellings: { - value: false - } + value: false, + }, }, default_outcome: { dest: 'new state', dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: '' + html: '', }, labelled_as_correct: false, param_changes: [], @@ -130,15 +130,15 @@ describe('Exploration improvements task registrar service', () => { correct_answer: 'answer', explanation: { content_id: 'solution', - html: '

This is an explanation.

' - } + html: '

This is an explanation.

', + }, }, - id: 'TextInput' + id: 'TextInput', }, linked_skill_id: null, param_changes: [], solicit_answer_details: false, - card_is_checkpoint: false + card_is_checkpoint: false, }; stateStatsBackendDict = { @@ -198,7 +198,7 @@ describe('Exploration improvements task registrar service', () => { misPlaythroughIssueBackendDict = { issue_type: PlaythroughIssueType.MultipleIncorrectSubmissions, issue_customization_args: { - state_name: { value: 'Introduction' }, + state_name: {value: 'Introduction'}, num_times_answered_incorrectly: { value: 3, }, @@ -228,10 +228,11 @@ describe('Exploration improvements task registrar service', () => { }; }); - const makeTask = ( - (dict = taskBackendDict) => { - return ExplorationTaskModel.createFromBackendDict(dict) as T; - }); + const makeTask = ( + dict = taskBackendDict + ) => { + return ExplorationTaskModel.createFromBackendDict(dict) as T; + }; const makeStates = (map = statesBackendDict) => { return statesObjectFactory.createFromBackendDict(map); }; @@ -242,32 +243,41 @@ describe('Exploration improvements task registrar service', () => { return AnswerStats.createFromBackendDict(dict); }; const makeCstPlaythroughIssue = (dict = cstPlaythroughIssueBackendDict) => { - return ( - PlaythroughIssue.createFromBackendDict(dict) - ); + return PlaythroughIssue.createFromBackendDict(dict); }; const makeEqPlaythroughIssue = (dict = eqPlaythroughIssueBackendDict) => { - return ( - PlaythroughIssue.createFromBackendDict(dict) - ); + return PlaythroughIssue.createFromBackendDict(dict); }; const makeMisPlaythroughIssue = (dict = misPlaythroughIssueBackendDict) => { - return ( - PlaythroughIssue.createFromBackendDict(dict)); + return PlaythroughIssue.createFromBackendDict(dict); }; it('should initialize successfully using default test values', () => { - expect( - () => taskRegistryService.initialize( - config, makeStates(), makeExpStats(), [], new Map(), new Map(), [])) - .not.toThrowError(); + expect(() => + taskRegistryService.initialize( + config, + makeStates(), + makeExpStats(), + [], + new Map(), + new Map(), + [] + ) + ).not.toThrowError(); }); it('should return exp stats passed to initialization', () => { const expStats = makeExpStats(); taskRegistryService.initialize( - config, makeStates(), expStats, [], new Map(), new Map(), []); + config, + makeStates(), + expStats, + [], + new Map(), + new Map(), + [] + ); expect(taskRegistryService.getExplorationStats()).toBe(expStats); }); @@ -275,190 +285,307 @@ describe('Exploration improvements task registrar service', () => { describe('Validating initialize arguments', () => { it('should throw if stats is for wrong exploration', () => { expStatsBackendDict.exp_id = 'wrong_exp_id'; - expect( - () => taskRegistryService.initialize( - config, makeStates(), makeExpStats(), [], new Map(), new Map(), [])) - .toThrowError( - 'Expected stats for exploration "eid", but got stats for ' + - 'exploration "wrong_exp_id"'); + expect(() => + taskRegistryService.initialize( + config, + makeStates(), + makeExpStats(), + [], + new Map(), + new Map(), + [] + ) + ).toThrowError( + 'Expected stats for exploration "eid", but got stats for ' + + 'exploration "wrong_exp_id"' + ); }); it('should throw if stats is for wrong exploration version', () => { expStatsBackendDict.exp_version = 2; - expect( - () => taskRegistryService.initialize( - config, makeStates(), makeExpStats(), [], new Map(), new Map(), [])) - .toThrowError( - 'Expected stats for exploration version 1, but got stats for ' + - 'exploration version 2'); + expect(() => + taskRegistryService.initialize( + config, + makeStates(), + makeExpStats(), + [], + new Map(), + new Map(), + [] + ) + ).toThrowError( + 'Expected stats for exploration version 1, but got stats for ' + + 'exploration version 2' + ); }); it('should throw if a task targets an unknown state', () => { delete statesBackendDict.End; taskBackendDict.target_id = 'End'; - expect( - () => taskRegistryService.initialize( - config, makeStates(), makeExpStats(), [makeTask()], new Map(), - new Map(), [])) - .toThrowError( - 'Unexpected reference to state "End", which does not exist'); + expect(() => + taskRegistryService.initialize( + config, + makeStates(), + makeExpStats(), + [makeTask()], + new Map(), + new Map(), + [] + ) + ).toThrowError( + 'Unexpected reference to state "End", which does not exist' + ); }); it('should throw if a resolved task type targets an unknown state', () => { delete statesBackendDict.End; const resolvedTaskTypesByStateName = new Map([ - ['End', ['high_bounce_rate'] as ExplorationTaskType[]] + ['End', ['high_bounce_rate'] as ExplorationTaskType[]], ]); - expect( - () => taskRegistryService.initialize( - config, makeStates(), makeExpStats(), [], - resolvedTaskTypesByStateName, new Map(), [])) - .toThrowError( - 'Unexpected reference to state "End", which does not exist'); + expect(() => + taskRegistryService.initialize( + config, + makeStates(), + makeExpStats(), + [], + resolvedTaskTypesByStateName, + new Map(), + [] + ) + ).toThrowError( + 'Unexpected reference to state "End", which does not exist' + ); }); it('should throw if answer stats maps to an unknown state', () => { delete statesBackendDict.End; const answerStats = new Map([['End', [makeAnswerStats()]]]); - expect( - () => taskRegistryService.initialize( - config, makeStates(), makeExpStats(), [], new Map(), answerStats, [])) - .toThrowError( - 'Unexpected reference to state "End", which does not exist'); + expect(() => + taskRegistryService.initialize( + config, + makeStates(), + makeExpStats(), + [], + new Map(), + answerStats, + [] + ) + ).toThrowError( + 'Unexpected reference to state "End", which does not exist' + ); }); it('should throw if CST playthrough issue maps to an unknown state', () => { delete statesBackendDict.End; ( - cstPlaythroughIssueBackendDict.issue_customization_args as - CyclicStateTransitionsCustomizationArgs + cstPlaythroughIssueBackendDict.issue_customization_args as CyclicStateTransitionsCustomizationArgs ).state_names.value = ['Introduction', 'End']; - expect( - () => taskRegistryService.initialize( - config, makeStates(), makeExpStats(), [], new Map(), new Map(), - [makeCstPlaythroughIssue()])) - .toThrowError( - 'Unexpected reference to state "End", which does not exist'); + expect(() => + taskRegistryService.initialize( + config, + makeStates(), + makeExpStats(), + [], + new Map(), + new Map(), + [makeCstPlaythroughIssue()] + ) + ).toThrowError( + 'Unexpected reference to state "End", which does not exist' + ); }); it('should throw if EQ playthrough issue maps to an unknown state', () => { delete statesBackendDict.End; ( - eqPlaythroughIssueBackendDict.issue_customization_args as - EarlyQuitCustomizationArgs + eqPlaythroughIssueBackendDict.issue_customization_args as EarlyQuitCustomizationArgs ).state_name.value = 'End'; - expect( - () => taskRegistryService.initialize( - config, makeStates(), makeExpStats(), [], new Map(), new Map(), - [makeEqPlaythroughIssue()])) - .toThrowError( - 'Unexpected reference to state "End", which does not exist'); + expect(() => + taskRegistryService.initialize( + config, + makeStates(), + makeExpStats(), + [], + new Map(), + new Map(), + [makeEqPlaythroughIssue()] + ) + ).toThrowError( + 'Unexpected reference to state "End", which does not exist' + ); }); it('should throw if MIS playthrough issue maps to an unknown state', () => { delete statesBackendDict.End; ( - misPlaythroughIssueBackendDict.issue_customization_args as - MultipleIncorrectSubmissionsCustomizationArgs + misPlaythroughIssueBackendDict.issue_customization_args as MultipleIncorrectSubmissionsCustomizationArgs ).state_name.value = 'End'; - expect( - () => taskRegistryService.initialize( - config, makeStates(), makeExpStats(), [], new Map(), new Map(), - [makeMisPlaythroughIssue()])) - .toThrowError( - 'Unexpected reference to state "End", which does not exist'); + expect(() => + taskRegistryService.initialize( + config, + makeStates(), + makeExpStats(), + [], + new Map(), + new Map(), + [makeMisPlaythroughIssue()] + ) + ).toThrowError( + 'Unexpected reference to state "End", which does not exist' + ); }); it('should throw if task targets wrong exploration', () => { taskBackendDict.entity_id = 'wrong_exp_id'; - expect( - () => taskRegistryService.initialize( - config, makeStates(), makeExpStats(), [makeTask()], new Map(), - new Map(), [])) - .toThrowError( - 'Expected task for exploration "eid", but got task for exploration ' + - '"wrong_exp_id"'); + expect(() => + taskRegistryService.initialize( + config, + makeStates(), + makeExpStats(), + [makeTask()], + new Map(), + new Map(), + [] + ) + ).toThrowError( + 'Expected task for exploration "eid", but got task for exploration ' + + '"wrong_exp_id"' + ); }); it('should throw if task targets wrong exploration version', () => { taskBackendDict.entity_version = 2; - expect( - () => taskRegistryService.initialize( - config, makeStates(), makeExpStats(), [makeTask()], new Map(), - new Map(), [])) - .toThrowError( - 'Expected task for exploration version 1, but got task for ' + - 'exploration version 2'); + expect(() => + taskRegistryService.initialize( + config, + makeStates(), + makeExpStats(), + [makeTask()], + new Map(), + new Map(), + [] + ) + ).toThrowError( + 'Expected task for exploration version 1, but got task for ' + + 'exploration version 2' + ); }); - it('should throw if open tasks with the same type are targeting the same ' + - 'state', () => { - taskBackendDict.target_id = 'Introduction'; - taskBackendDict.task_type = 'high_bounce_rate'; - const tasks = [makeTask(), makeTask()]; - expect( - () => taskRegistryService.initialize( - config, makeStates(), makeExpStats(), tasks, new Map(), new Map(), - [])) - .toThrowError( + it( + 'should throw if open tasks with the same type are targeting the same ' + + 'state', + () => { + taskBackendDict.target_id = 'Introduction'; + taskBackendDict.task_type = 'high_bounce_rate'; + const tasks = [makeTask(), makeTask()]; + expect(() => + taskRegistryService.initialize( + config, + makeStates(), + makeExpStats(), + tasks, + new Map(), + new Map(), + [] + ) + ).toThrowError( 'Found duplicate task of type "high_bounce_rate" targeting state ' + - '"Introduction"'); - }); + '"Introduction"' + ); + } + ); - it('should throw if open and resolved tasks with the same type are ' + - 'targeting the same state', () => { - taskBackendDict.target_id = 'Introduction'; - taskBackendDict.task_type = 'high_bounce_rate'; - const resolvedTaskTypesByStateName = new Map([ - ['Introduction', ['high_bounce_rate'] as ExplorationTaskType[]] - ]); - expect( - () => taskRegistryService.initialize( - config, makeStates(), makeExpStats(), [makeTask()], - resolvedTaskTypesByStateName, new Map(), [])) - .toThrowError( + it( + 'should throw if open and resolved tasks with the same type are ' + + 'targeting the same state', + () => { + taskBackendDict.target_id = 'Introduction'; + taskBackendDict.task_type = 'high_bounce_rate'; + const resolvedTaskTypesByStateName = new Map([ + ['Introduction', ['high_bounce_rate'] as ExplorationTaskType[]], + ]); + expect(() => + taskRegistryService.initialize( + config, + makeStates(), + makeExpStats(), + [makeTask()], + resolvedTaskTypesByStateName, + new Map(), + [] + ) + ).toThrowError( 'Found duplicate task of type "high_bounce_rate" targeting state ' + - '"Introduction"'); - }); + '"Introduction"' + ); + } + ); - it('should throw if resolved tasks with the same type are targeting the ' + - 'same state', () => { - const resolvedTaskTypesByStateName = new Map([ - ['Introduction', - ['high_bounce_rate', 'high_bounce_rate'] as ExplorationTaskType[] - ], - ]); - expect( - () => taskRegistryService.initialize( - config, makeStates(), makeExpStats(), [], - resolvedTaskTypesByStateName, new Map(), [])) - .toThrowError( + it( + 'should throw if resolved tasks with the same type are targeting the ' + + 'same state', + () => { + const resolvedTaskTypesByStateName = new Map([ + [ + 'Introduction', + ['high_bounce_rate', 'high_bounce_rate'] as ExplorationTaskType[], + ], + ]); + expect(() => + taskRegistryService.initialize( + config, + makeStates(), + makeExpStats(), + [], + resolvedTaskTypesByStateName, + new Map(), + [] + ) + ).toThrowError( 'Found duplicate task of type "high_bounce_rate" targeting state ' + - '"Introduction"'); - }); + '"Introduction"' + ); + } + ); }); describe('Post-initialization', () => { it('should not return a resolved task from the open tasks API', () => { statesBackendDict = {Introduction: stateBackendDict}; - const resolvedTaskTypesByStateName = ( - new Map([ - ['Introduction', [ - 'high_bounce_rate', 'ineffective_feedback_loop', - 'needs_guiding_responses', 'successive_incorrect_answers', - ]], - ])); + const resolvedTaskTypesByStateName = new Map< + string, + ExplorationTaskType[] + >([ + [ + 'Introduction', + [ + 'high_bounce_rate', + 'ineffective_feedback_loop', + 'needs_guiding_responses', + 'successive_incorrect_answers', + ], + ], + ]); taskRegistryService.initialize( - config, makeStates(), makeExpStats(), [], resolvedTaskTypesByStateName, - new Map(), []); - - expect(taskRegistryService.getOpenHighBounceRateTasks().length) - .toEqual(0); - expect(taskRegistryService.getOpenIneffectiveFeedbackLoopTasks().length) - .toEqual(0); - expect(taskRegistryService.getOpenNeedsGuidingResponsesTasks().length) - .toEqual(0); + config, + makeStates(), + makeExpStats(), + [], + resolvedTaskTypesByStateName, + new Map(), + [] + ); + + expect(taskRegistryService.getOpenHighBounceRateTasks().length).toEqual( + 0 + ); + expect( + taskRegistryService.getOpenIneffectiveFeedbackLoopTasks().length + ).toEqual(0); + expect( + taskRegistryService.getOpenNeedsGuidingResponsesTasks().length + ).toEqual(0); expect( taskRegistryService.getOpenSuccessiveIncorrectAnswersTasks().length ).toEqual(0); @@ -472,10 +599,18 @@ describe('Exploration improvements task registrar service', () => { }; taskRegistryService.initialize( - config, makeStates(), makeExpStats(), [], new Map(), new Map(), []); + config, + makeStates(), + makeExpStats(), + [], + new Map(), + new Map(), + [] + ); - expect(taskRegistryService.getAllStateTasks().map(st => st.stateName)) - .toEqual(jasmine.arrayContaining(['Introduction', 'Middle', 'End'])); + expect( + taskRegistryService.getAllStateTasks().map(st => st.stateName) + ).toEqual(jasmine.arrayContaining(['Introduction', 'Middle', 'End'])); }); it('should throw an error when state does not exist', () => { @@ -486,10 +621,18 @@ describe('Exploration improvements task registrar service', () => { }; taskRegistryService.initialize( - config, makeStates(), makeExpStats(), [], new Map(), new Map(), []); - - expect(() => taskRegistryService.getStateTasks('Epilogue')) - .toThrowError('Unknown state with name: Epilogue'); + config, + makeStates(), + makeExpStats(), + [], + new Map(), + new Map(), + [] + ); + + expect(() => taskRegistryService.getStateTasks('Epilogue')).toThrowError( + 'Unknown state with name: Epilogue' + ); }); it('should return tasks for a specific state', () => { @@ -500,10 +643,18 @@ describe('Exploration improvements task registrar service', () => { }; taskRegistryService.initialize( - config, makeStates(), makeExpStats(), [], new Map(), new Map(), []); + config, + makeStates(), + makeExpStats(), + [], + new Map(), + new Map(), + [] + ); - expect(taskRegistryService.getStateTasks('Introduction').stateName) - .toEqual('Introduction'); + expect( + taskRegistryService.getStateTasks('Introduction').stateName + ).toEqual('Introduction'); }); }); @@ -511,14 +662,24 @@ describe('Exploration improvements task registrar service', () => { it('should not generate open tasks when they do not exist', () => { statesBackendDict = {Introduction: stateBackendDict}; taskRegistryService.initialize( - config, makeStates(), makeExpStats(), [], new Map(), new Map(), []); - - expect(taskRegistryService.getOpenHighBounceRateTasks().length) - .toEqual(0); - expect(taskRegistryService.getOpenIneffectiveFeedbackLoopTasks().length) - .toEqual(0); - expect(taskRegistryService.getOpenNeedsGuidingResponsesTasks().length) - .toEqual(0); + config, + makeStates(), + makeExpStats(), + [], + new Map(), + new Map(), + [] + ); + + expect(taskRegistryService.getOpenHighBounceRateTasks().length).toEqual( + 0 + ); + expect( + taskRegistryService.getOpenIneffectiveFeedbackLoopTasks().length + ).toEqual(0); + expect( + taskRegistryService.getOpenNeedsGuidingResponsesTasks().length + ).toEqual(0); expect( taskRegistryService.getOpenSuccessiveIncorrectAnswersTasks().length ).toEqual(0); @@ -537,8 +698,14 @@ describe('Exploration improvements task registrar service', () => { }; taskRegistryService.initialize( - config, makeStates(), makeExpStats(), [], new Map(), new Map(), - [makeEqPlaythroughIssue()]); + config, + makeStates(), + makeExpStats(), + [], + new Map(), + new Map(), + [makeEqPlaythroughIssue()] + ); const [hbrTask] = taskRegistryService.getOpenHighBounceRateTasks(); expect(hbrTask.isOpen()).toBeTrue(); @@ -546,11 +713,17 @@ describe('Exploration improvements task registrar service', () => { it('should generate a new ineffective feedback loop task', () => { taskRegistryService.initialize( - config, makeStates(), makeExpStats(), [], new Map(), new Map(), - [makeCstPlaythroughIssue()]); - - const [iflTask] = ( - taskRegistryService.getOpenIneffectiveFeedbackLoopTasks()); + config, + makeStates(), + makeExpStats(), + [], + new Map(), + new Map(), + [makeCstPlaythroughIssue()] + ); + + const [iflTask] = + taskRegistryService.getOpenIneffectiveFeedbackLoopTasks(); expect(iflTask.isOpen()).toBeTrue(); }); @@ -560,8 +733,14 @@ describe('Exploration improvements task registrar service', () => { const stateAnswerStats = new Map([['Introduction', [answerStats]]]); taskRegistryService.initialize( - config, makeStates(), makeExpStats(), [], new Map(), stateAnswerStats, - []); + config, + makeStates(), + makeExpStats(), + [], + new Map(), + stateAnswerStats, + [] + ); const [ngrTask] = taskRegistryService.getOpenNeedsGuidingResponsesTasks(); expect(ngrTask.isOpen()).toBeTrue(); @@ -569,19 +748,27 @@ describe('Exploration improvements task registrar service', () => { it('should generate a new successive incorrect answers task', () => { taskRegistryService.initialize( - config, makeStates(), makeExpStats(), [], new Map(), new Map(), - [makeMisPlaythroughIssue()]); - - const [siaTask] = ( - taskRegistryService.getOpenSuccessiveIncorrectAnswersTasks()); + config, + makeStates(), + makeExpStats(), + [], + new Map(), + new Map(), + [makeMisPlaythroughIssue()] + ); + + const [siaTask] = + taskRegistryService.getOpenSuccessiveIncorrectAnswersTasks(); expect(siaTask.isOpen()).toBeTrue(); }); }); describe('Discarding open tasks', () => { it('should discard an HBR task when bounce rate is too low', () => { - const task = makeTask( - {...taskBackendDict, ...{status: 'open'}}); + const task = makeTask({ + ...taskBackendDict, + ...{status: 'open'}, + }); statesBackendDict = { Introduction: stateBackendDict, }; @@ -594,10 +781,18 @@ describe('Exploration improvements task registrar service', () => { }; taskRegistryService.initialize( - config, makeStates(), makeExpStats(), [task], new Map(), new Map(), []); - - expect(taskRegistryService.getOpenHighBounceRateTasks().length) - .toEqual(0); + config, + makeStates(), + makeExpStats(), + [task], + new Map(), + new Map(), + [] + ); + + expect(taskRegistryService.getOpenHighBounceRateTasks().length).toEqual( + 0 + ); }); it('should discard an NGR task when all answers are addressed', () => { @@ -607,7 +802,14 @@ describe('Exploration improvements task registrar service', () => { const stateAnswerStats = new Map([['Introduction', [answerStats]]]); taskRegistryService.initialize( - config, states, makeExpStats(), [], new Map(), stateAnswerStats, []); + config, + states, + makeExpStats(), + [], + new Map(), + stateAnswerStats, + [] + ); const [ngrTask] = taskRegistryService.getOpenNeedsGuidingResponsesTasks(); expect(ngrTask.isOpen()).toBeTrue(); @@ -615,7 +817,8 @@ describe('Exploration improvements task registrar service', () => { answerStats.isAddressed = true; taskRegistryService.onStateInteractionSaved( - states.getState('Introduction')); + states.getState('Introduction') + ); expect(ngrTask.isOpen()).toBeFalse(); }); }); @@ -624,101 +827,130 @@ describe('Exploration improvements task registrar service', () => { it('should create new obsolete tasks for newly created state', () => { statesBackendDict = {Introduction: stateBackendDict}; taskRegistryService.initialize( - config, makeStates(), makeExpStats(), [], new Map(), new Map(), []); - - expect(taskRegistryService.getOpenHighBounceRateTasks().length) - .toEqual(0); - expect(taskRegistryService.getOpenIneffectiveFeedbackLoopTasks().length) - .toEqual(0); - expect(taskRegistryService.getOpenNeedsGuidingResponsesTasks().length) - .toEqual(0); + config, + makeStates(), + makeExpStats(), + [], + new Map(), + new Map(), + [] + ); + + expect(taskRegistryService.getOpenHighBounceRateTasks().length).toEqual( + 0 + ); + expect( + taskRegistryService.getOpenIneffectiveFeedbackLoopTasks().length + ).toEqual(0); + expect( + taskRegistryService.getOpenNeedsGuidingResponsesTasks().length + ).toEqual(0); expect( taskRegistryService.getOpenSuccessiveIncorrectAnswersTasks().length ).toEqual(0); taskRegistryService.onStateAdded('Middle'); - expect(taskRegistryService.getOpenHighBounceRateTasks().length) - .toEqual(0); - expect(taskRegistryService.getOpenIneffectiveFeedbackLoopTasks().length) - .toEqual(0); - expect(taskRegistryService.getOpenNeedsGuidingResponsesTasks().length) - .toEqual(0); + expect(taskRegistryService.getOpenHighBounceRateTasks().length).toEqual( + 0 + ); + expect( + taskRegistryService.getOpenIneffectiveFeedbackLoopTasks().length + ).toEqual(0); + expect( + taskRegistryService.getOpenNeedsGuidingResponsesTasks().length + ).toEqual(0); expect( taskRegistryService.getOpenSuccessiveIncorrectAnswersTasks().length ).toEqual(0); }); - it('should have an obsolete and re-targeted task for states that are ' + - 'renamed', () => { - statesBackendDict = { - Introduction: stateBackendDict, - }; - expStatsBackendDict.num_starts = 500; - expStatsBackendDict.state_stats_mapping = { - Introduction: { - ...stateStatsBackendDict, - ...{total_hit_count: 500, num_completions: 350}, - }, - }; - const answerStats = makeAnswerStats(); - answerStats.isAddressed = false; - const stateAnswerStats = new Map([['Introduction', [answerStats]]]); - - taskBackendDict.status = 'open'; - let [hbrTask, iflTask, ngrTask, siaTask] = [ - makeTask( - {...taskBackendDict, ...{task_type: 'high_bounce_rate'}}), - makeTask( - {...taskBackendDict, ...{task_type: 'ineffective_feedback_loop'}}), - makeTask( - {...taskBackendDict, ...{task_type: 'needs_guiding_responses'}}), - makeTask( - {...taskBackendDict, ...{task_type: 'successive_incorrect_answers'}}), - ]; - statesBackendDict = {Introduction: stateBackendDict}; - - taskRegistryService.initialize( - config, makeStates(), makeExpStats(), - [hbrTask, iflTask, ngrTask, siaTask], new Map(), stateAnswerStats, - [makeEqPlaythroughIssue()]); - - expect(hbrTask.targetId).toEqual('Introduction'); - expect(hbrTask.isOpen()).toBeTrue(); - expect(iflTask.targetId).toEqual('Introduction'); - expect(iflTask.isOpen()).toBeTrue(); - expect(ngrTask.targetId).toEqual('Introduction'); - expect(ngrTask.isOpen()).toBeTrue(); - expect(siaTask.targetId).toEqual('Introduction'); - expect(siaTask.isOpen()).toBeTrue(); - - taskRegistryService.onStateRenamed('Introduction', 'Prologue'); - - let [[newHbrTask], [newIflTask], [newNgrTask], [newSiaTask]] = [ - taskRegistryService.getOpenHighBounceRateTasks(), - taskRegistryService.getOpenIneffectiveFeedbackLoopTasks(), - taskRegistryService.getOpenNeedsGuidingResponsesTasks(), - taskRegistryService.getOpenSuccessiveIncorrectAnswersTasks(), - ]; - - expect(hbrTask.isObsolete()).toBeTrue(); - expect(hbrTask.targetId).toEqual('Introduction'); - expect(iflTask.isObsolete()).toBeTrue(); - expect(iflTask.targetId).toEqual('Introduction'); - expect(ngrTask.isObsolete()).toBeTrue(); - expect(ngrTask.targetId).toEqual('Introduction'); - expect(siaTask.isObsolete()).toBeTrue(); - expect(siaTask.targetId).toEqual('Introduction'); - - expect(newHbrTask.isOpen()).toBeTrue(); - expect(newHbrTask.targetId).toEqual('Prologue'); - expect(newIflTask.isOpen()).toBeTrue(); - expect(newIflTask.targetId).toEqual('Prologue'); - expect(newNgrTask.isOpen()).toBeTrue(); - expect(newNgrTask.targetId).toEqual('Prologue'); - expect(newSiaTask.isOpen()).toBeTrue(); - expect(newSiaTask.targetId).toEqual('Prologue'); - }); + it( + 'should have an obsolete and re-targeted task for states that are ' + + 'renamed', + () => { + statesBackendDict = { + Introduction: stateBackendDict, + }; + expStatsBackendDict.num_starts = 500; + expStatsBackendDict.state_stats_mapping = { + Introduction: { + ...stateStatsBackendDict, + ...{total_hit_count: 500, num_completions: 350}, + }, + }; + const answerStats = makeAnswerStats(); + answerStats.isAddressed = false; + const stateAnswerStats = new Map([['Introduction', [answerStats]]]); + + taskBackendDict.status = 'open'; + let [hbrTask, iflTask, ngrTask, siaTask] = [ + makeTask({ + ...taskBackendDict, + ...{task_type: 'high_bounce_rate'}, + }), + makeTask({ + ...taskBackendDict, + ...{task_type: 'ineffective_feedback_loop'}, + }), + makeTask({ + ...taskBackendDict, + ...{task_type: 'needs_guiding_responses'}, + }), + makeTask({ + ...taskBackendDict, + ...{task_type: 'successive_incorrect_answers'}, + }), + ]; + statesBackendDict = {Introduction: stateBackendDict}; + + taskRegistryService.initialize( + config, + makeStates(), + makeExpStats(), + [hbrTask, iflTask, ngrTask, siaTask], + new Map(), + stateAnswerStats, + [makeEqPlaythroughIssue()] + ); + + expect(hbrTask.targetId).toEqual('Introduction'); + expect(hbrTask.isOpen()).toBeTrue(); + expect(iflTask.targetId).toEqual('Introduction'); + expect(iflTask.isOpen()).toBeTrue(); + expect(ngrTask.targetId).toEqual('Introduction'); + expect(ngrTask.isOpen()).toBeTrue(); + expect(siaTask.targetId).toEqual('Introduction'); + expect(siaTask.isOpen()).toBeTrue(); + + taskRegistryService.onStateRenamed('Introduction', 'Prologue'); + + let [[newHbrTask], [newIflTask], [newNgrTask], [newSiaTask]] = [ + taskRegistryService.getOpenHighBounceRateTasks(), + taskRegistryService.getOpenIneffectiveFeedbackLoopTasks(), + taskRegistryService.getOpenNeedsGuidingResponsesTasks(), + taskRegistryService.getOpenSuccessiveIncorrectAnswersTasks(), + ]; + + expect(hbrTask.isObsolete()).toBeTrue(); + expect(hbrTask.targetId).toEqual('Introduction'); + expect(iflTask.isObsolete()).toBeTrue(); + expect(iflTask.targetId).toEqual('Introduction'); + expect(ngrTask.isObsolete()).toBeTrue(); + expect(ngrTask.targetId).toEqual('Introduction'); + expect(siaTask.isObsolete()).toBeTrue(); + expect(siaTask.targetId).toEqual('Introduction'); + + expect(newHbrTask.isOpen()).toBeTrue(); + expect(newHbrTask.targetId).toEqual('Prologue'); + expect(newIflTask.isOpen()).toBeTrue(); + expect(newIflTask.targetId).toEqual('Prologue'); + expect(newNgrTask.isOpen()).toBeTrue(); + expect(newNgrTask.targetId).toEqual('Prologue'); + expect(newSiaTask.isOpen()).toBeTrue(); + expect(newSiaTask.targetId).toEqual('Prologue'); + } + ); it('should discard tasks targeting a state that is newly deleted', () => { statesBackendDict = { @@ -737,21 +969,34 @@ describe('Exploration improvements task registrar service', () => { taskBackendDict.status = 'open'; let [hbrTask, iflTask, ngrTask, siaTask] = [ - makeTask( - {...taskBackendDict, ...{task_type: 'high_bounce_rate'}}), - makeTask( - {...taskBackendDict, ...{task_type: 'ineffective_feedback_loop'}}), - makeTask( - {...taskBackendDict, ...{task_type: 'needs_guiding_responses'}}), - makeTask( - {...taskBackendDict, ...{task_type: 'successive_incorrect_answers'}}), + makeTask({ + ...taskBackendDict, + ...{task_type: 'high_bounce_rate'}, + }), + makeTask({ + ...taskBackendDict, + ...{task_type: 'ineffective_feedback_loop'}, + }), + makeTask({ + ...taskBackendDict, + ...{task_type: 'needs_guiding_responses'}, + }), + makeTask({ + ...taskBackendDict, + ...{task_type: 'successive_incorrect_answers'}, + }), ]; statesBackendDict = {Introduction: stateBackendDict}; taskRegistryService.initialize( - config, makeStates(), makeExpStats(), - [hbrTask, iflTask, ngrTask, siaTask], new Map(), stateAnswerStats, - [makeEqPlaythroughIssue()]); + config, + makeStates(), + makeExpStats(), + [hbrTask, iflTask, ngrTask, siaTask], + new Map(), + stateAnswerStats, + [makeEqPlaythroughIssue()] + ); expect(hbrTask.targetId).toEqual('Introduction'); expect(hbrTask.isOpen()).toBeTrue(); @@ -770,25 +1015,38 @@ describe('Exploration improvements task registrar service', () => { expect(siaTask.isOpen()).toBeFalse(); }); - it('should open a new NGR task after initialization if an answer becomes ' + - 'unaddressed', () => { - const states = makeStates(); - const answerStats = makeAnswerStats(); - answerStats.isAddressed = true; - const stateAnswerStats = new Map([['Introduction', [answerStats]]]); - - taskRegistryService.initialize( - config, states, makeExpStats(), [], new Map(), stateAnswerStats, []); - - expect(taskRegistryService.getOpenNeedsGuidingResponsesTasks().length) - .toEqual(0); - - answerStats.isAddressed = false; - - taskRegistryService.onStateInteractionSaved( - states.getState('Introduction')); - expect(taskRegistryService.getOpenNeedsGuidingResponsesTasks().length) - .toEqual(1); - }); + it( + 'should open a new NGR task after initialization if an answer becomes ' + + 'unaddressed', + () => { + const states = makeStates(); + const answerStats = makeAnswerStats(); + answerStats.isAddressed = true; + const stateAnswerStats = new Map([['Introduction', [answerStats]]]); + + taskRegistryService.initialize( + config, + states, + makeExpStats(), + [], + new Map(), + stateAnswerStats, + [] + ); + + expect( + taskRegistryService.getOpenNeedsGuidingResponsesTasks().length + ).toEqual(0); + + answerStats.isAddressed = false; + + taskRegistryService.onStateInteractionSaved( + states.getState('Introduction') + ); + expect( + taskRegistryService.getOpenNeedsGuidingResponsesTasks().length + ).toEqual(1); + } + ); }); }); diff --git a/core/templates/services/exploration-improvements-task-registry.service.ts b/core/templates/services/exploration-improvements-task-registry.service.ts index c22ecd6416d2..0c9353db330b 100644 --- a/core/templates/services/exploration-improvements-task-registry.service.ts +++ b/core/templates/services/exploration-improvements-task-registry.service.ts @@ -17,37 +17,30 @@ * improvements tasks for an exploration. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; -import { group } from 'd3-array'; - -import { AnswerStats } from 'domain/exploration/answer-stats.model'; -import { States } from 'domain/exploration/StatesObjectFactory'; -import { ExplorationImprovementsConfig } from - 'domain/improvements/exploration-improvements-config.model'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {group} from 'd3-array'; + +import {AnswerStats} from 'domain/exploration/answer-stats.model'; +import {States} from 'domain/exploration/StatesObjectFactory'; +import {ExplorationImprovementsConfig} from 'domain/improvements/exploration-improvements-config.model'; import { ExplorationTask, ExplorationTaskModel, ExplorationTaskType, } from 'domain/improvements/exploration-task.model'; -import { HighBounceRateTask } from - 'domain/improvements/high-bounce-rate-task.model'; -import { ImprovementsConstants } from - 'domain/improvements/improvements.constants'; -import { IneffectiveFeedbackLoopTask } from - 'domain/improvements/ineffective-feedback-loop-task.model'; -import { NeedsGuidingResponsesTask } from - 'domain/improvements/needs-guiding-response-task.model'; -import { SuccessiveIncorrectAnswersTask } from - 'domain/improvements/successive-incorrect-answers-task.model'; -import { State } from 'domain/state/StateObjectFactory'; +import {HighBounceRateTask} from 'domain/improvements/high-bounce-rate-task.model'; +import {ImprovementsConstants} from 'domain/improvements/improvements.constants'; +import {IneffectiveFeedbackLoopTask} from 'domain/improvements/ineffective-feedback-loop-task.model'; +import {NeedsGuidingResponsesTask} from 'domain/improvements/needs-guiding-response-task.model'; +import {SuccessiveIncorrectAnswersTask} from 'domain/improvements/successive-incorrect-answers-task.model'; +import {State} from 'domain/state/StateObjectFactory'; import { PlaythroughIssue, PlaythroughIssueType, } from 'domain/statistics/playthrough-issue.model'; -import { ExplorationStats } from - 'domain/statistics/exploration-stats.model'; -import { StateStats } from 'domain/statistics/state-stats-model'; +import {ExplorationStats} from 'domain/statistics/exploration-stats.model'; +import {StateStats} from 'domain/statistics/state-stats-model'; type HbrTask = HighBounceRateTask; type IflTask = IneffectiveFeedbackLoopTask; @@ -80,11 +73,12 @@ export class SupportingStateStats { public readonly misPlaythroughIssues: readonly MisPlaythroughIssue[]; constructor( - stateStats: StateStats, - answerStats: readonly AnswerStats[] = [], - cstPlaythroughIssues: readonly CstPlaythroughIssue[] = [], - eqPlaythroughIssues: readonly EqPlaythroughIssue[] = [], - misPlaythroughIssues: readonly MisPlaythroughIssue[] = []) { + stateStats: StateStats, + answerStats: readonly AnswerStats[] = [], + cstPlaythroughIssues: readonly CstPlaythroughIssue[] = [], + eqPlaythroughIssues: readonly EqPlaythroughIssue[] = [], + misPlaythroughIssues: readonly MisPlaythroughIssue[] = [] + ) { this.stateStats = stateStats; this.answerStats = [...answerStats]; this.cstPlaythroughIssues = [...cstPlaythroughIssues]; @@ -111,31 +105,42 @@ export class StateTasks implements Iterable { public readonly supportingStats: SupportingStateStats; constructor( - stateName: string, - tasksByType: ReadonlyMap, - supportingStats: SupportingStateStats) { + stateName: string, + tasksByType: ReadonlyMap, + supportingStats: SupportingStateStats + ) { this.stateName = stateName; this.hbrTask = tasksByType.get( - ImprovementsConstants.TASK_TYPE_HIGH_BOUNCE_RATE) as HbrTask; + ImprovementsConstants.TASK_TYPE_HIGH_BOUNCE_RATE + ) as HbrTask; this.iflTask = tasksByType.get( - ImprovementsConstants.TASK_TYPE_INEFFECTIVE_FEEDBACK_LOOP) as IflTask; + ImprovementsConstants.TASK_TYPE_INEFFECTIVE_FEEDBACK_LOOP + ) as IflTask; this.ngrTask = tasksByType.get( - ImprovementsConstants.TASK_TYPE_NEEDS_GUIDING_RESPONSES) as NgrTask; + ImprovementsConstants.TASK_TYPE_NEEDS_GUIDING_RESPONSES + ) as NgrTask; this.siaTask = tasksByType.get( - ImprovementsConstants.TASK_TYPE_SUCCESSIVE_INCORRECT_ANSWERS) as SiaTask; + ImprovementsConstants.TASK_TYPE_SUCCESSIVE_INCORRECT_ANSWERS + ) as SiaTask; this.supportingStats = supportingStats; } public refresh( - expStats: ExplorationStats, config: ExplorationImprovementsConfig): void { + expStats: ExplorationStats, + config: ExplorationImprovementsConfig + ): void { this.hbrTask.refreshStatus( - expStats, this.supportingStats.eqPlaythroughIssues.length, config); + expStats, + this.supportingStats.eqPlaythroughIssues.length, + config + ); this.iflTask.refreshStatus( - this.supportingStats.cstPlaythroughIssues.length); - this.ngrTask.refreshStatus( - this.supportingStats.answerStats); + this.supportingStats.cstPlaythroughIssues.length + ); + this.ngrTask.refreshStatus(this.supportingStats.answerStats); this.siaTask.refreshStatus( - this.supportingStats.misPlaythroughIssues.length); + this.supportingStats.misPlaythroughIssues.length + ); } *[Symbol.iterator](): Iterator { @@ -169,34 +174,46 @@ export class ExplorationImprovementsTaskRegistryService { private openTasksByType!: ReadonlyMap; initialize( - config: ExplorationImprovementsConfig, - states: States, - expStats: ExplorationStats, - openTasks: readonly ExplorationTask[], - resolvedTaskTypesByStateName: - ReadonlyMap, - topAnswersByStateName: ReadonlyMap, - playthroughIssues: readonly PlaythroughIssue[]): void { + config: ExplorationImprovementsConfig, + states: States, + expStats: ExplorationStats, + openTasks: readonly ExplorationTask[], + resolvedTaskTypesByStateName: ReadonlyMap< + string, + readonly ExplorationTaskType[] + >, + topAnswersByStateName: ReadonlyMap, + playthroughIssues: readonly PlaythroughIssue[] + ): void { this.validateInitializationArgs( - config, states, expStats, openTasks, - resolvedTaskTypesByStateName, topAnswersByStateName, - playthroughIssues); + config, + states, + expStats, + openTasks, + resolvedTaskTypesByStateName, + topAnswersByStateName, + playthroughIssues + ); this.config = config; this.expStats = expStats; this.tasksByState = new Map(); this.openTasksByType = new Map( - ImprovementsConstants.TASK_TYPES.map(taskType => [taskType, []])); + ImprovementsConstants.TASK_TYPES.map(taskType => [taskType, []]) + ); const openTasksByStateName = group(openTasks, t => t.targetId); - const playthroughIssuesByStateName = ( - group(playthroughIssues, p => p.getStateNameWithIssue())); + const playthroughIssuesByStateName = group(playthroughIssues, p => + p.getStateNameWithIssue() + ); for (const stateName of states.getStateNames()) { const playthroughIssuesByType = group( - playthroughIssuesByStateName.get(stateName) || [], p => p.issueType); - const cstPlaythroughIssues = ( - playthroughIssuesByType.get(PlaythroughIssueType.CyclicStateTransitions) + playthroughIssuesByStateName.get(stateName) || [], + p => p.issueType + ); + const cstPlaythroughIssues = playthroughIssuesByType.get( + PlaythroughIssueType.CyclicStateTransitions ) as CstPlaythroughIssue[]; const eqPlaythroughIssues = playthroughIssuesByType.get( PlaythroughIssueType.EarlyQuit @@ -212,7 +229,8 @@ export class ExplorationImprovementsTaskRegistryService { topAnswersByStateName.get(stateName) || [], cstPlaythroughIssues || [], eqPlaythroughIssues || [], - misPlaythroughIssues || []); + misPlaythroughIssues || [] + ); this.refreshStateTasks(stateName); } } @@ -225,12 +243,19 @@ export class ExplorationImprovementsTaskRegistryService { this.expStats = this.expStats.createNewWithStateAdded(newStateName); const newStateTasks = new StateTasks( newStateName, - new Map(ImprovementsConstants.TASK_TYPES.map(taskType => [ - taskType, ExplorationTaskModel.createNewObsoleteTask( - this.config.explorationId, this.config.explorationVersion, taskType, - newStateName), - ])), - new SupportingStateStats(this.expStats.getStateStats(newStateName))); + new Map( + ImprovementsConstants.TASK_TYPES.map(taskType => [ + taskType, + ExplorationTaskModel.createNewObsoleteTask( + this.config.explorationId, + this.config.explorationVersion, + taskType, + newStateName + ), + ]) + ), + new SupportingStateStats(this.expStats.getStateStats(newStateName)) + ); this.tasksByState.set(newStateName, newStateTasks); } @@ -239,7 +264,7 @@ export class ExplorationImprovementsTaskRegistryService { this.expStats = this.expStats.createNewWithStateDeleted(oldStateName); const oldStateTasks = this.tasksByState.get(oldStateName); - for (const oldTask of (oldStateTasks as StateTasks)) { + for (const oldTask of oldStateTasks as StateTasks) { if (oldTask.isOpen()) { this.popOpenTask(oldTask); } @@ -251,30 +276,36 @@ export class ExplorationImprovementsTaskRegistryService { onStateRenamed(oldStateName: string, newStateName: string): void { this.expStats = this.expStats.createNewWithStateRenamed( - oldStateName, newStateName); + oldStateName, + newStateName + ); const oldStateTasks = this.tasksByState.get(oldStateName); const newStateTasks = new StateTasks( newStateName, - new Map((oldStateTasks as StateTasks).map(oldTask => [ - oldTask.taskType, - ExplorationTaskModel.createFromBackendDict({ - ...oldTask.toBackendDict(), - ...{target_id: newStateName}, - }) - ])), + new Map( + (oldStateTasks as StateTasks).map(oldTask => [ + oldTask.taskType, + ExplorationTaskModel.createFromBackendDict({ + ...oldTask.toBackendDict(), + ...{target_id: newStateName}, + }), + ]) + ), new SupportingStateStats( this.expStats.getStateStats(newStateName), (oldStateTasks as StateTasks).supportingStats.answerStats, (oldStateTasks as StateTasks).supportingStats.cstPlaythroughIssues, (oldStateTasks as StateTasks).supportingStats.eqPlaythroughIssues, - (oldStateTasks as StateTasks).supportingStats.misPlaythroughIssues)); + (oldStateTasks as StateTasks).supportingStats.misPlaythroughIssues + ) + ); for (const newTask of newStateTasks) { if (newTask.isOpen()) { this.pushOpenTask(newTask); } } - for (const oldTask of (oldStateTasks as StateTasks)) { + for (const oldTask of oldStateTasks as StateTasks) { if (oldTask.isOpen()) { this.popOpenTask(oldTask); } @@ -291,17 +322,20 @@ export class ExplorationImprovementsTaskRegistryService { getOpenHighBounceRateTasks(): HbrTask[] { return this.openTasksByType.get( - ImprovementsConstants.TASK_TYPE_HIGH_BOUNCE_RATE) as HbrTask[]; + ImprovementsConstants.TASK_TYPE_HIGH_BOUNCE_RATE + ) as HbrTask[]; } getOpenIneffectiveFeedbackLoopTasks(): IflTask[] { return this.openTasksByType.get( - ImprovementsConstants.TASK_TYPE_INEFFECTIVE_FEEDBACK_LOOP) as IflTask[]; + ImprovementsConstants.TASK_TYPE_INEFFECTIVE_FEEDBACK_LOOP + ) as IflTask[]; } getOpenNeedsGuidingResponsesTasks(): NgrTask[] { return this.openTasksByType.get( - ImprovementsConstants.TASK_TYPE_NEEDS_GUIDING_RESPONSES) as NgrTask[]; + ImprovementsConstants.TASK_TYPE_NEEDS_GUIDING_RESPONSES + ) as NgrTask[]; } getOpenSuccessiveIncorrectAnswersTasks(): SiaTask[] { @@ -322,24 +356,35 @@ export class ExplorationImprovementsTaskRegistryService { } private validateInitializationArgs( - config: ExplorationImprovementsConfig, - states: States, - expStats: ExplorationStats, - openTasks: readonly ExplorationTask[], - resolvedTaskTypesByStateName: ReadonlyMap< - string, readonly ExplorationTaskType[]>, - topAnswersByStateName: ReadonlyMap, - playthroughIssues: readonly PlaythroughIssue[]): void { + config: ExplorationImprovementsConfig, + states: States, + expStats: ExplorationStats, + openTasks: readonly ExplorationTask[], + resolvedTaskTypesByStateName: ReadonlyMap< + string, + readonly ExplorationTaskType[] + >, + topAnswersByStateName: ReadonlyMap, + playthroughIssues: readonly PlaythroughIssue[] + ): void { // Validate that the exploration stats correspond with provided exploration. if (expStats.expId !== config.explorationId) { throw new Error( - 'Expected stats for exploration "' + config.explorationId + '", but ' + - 'got stats for exploration "' + expStats.expId + '"'); + 'Expected stats for exploration "' + + config.explorationId + + '", but ' + + 'got stats for exploration "' + + expStats.expId + + '"' + ); } if (expStats.expVersion !== config.explorationVersion) { throw new Error( - 'Expected stats for exploration version ' + config.explorationVersion + - ', but got stats for exploration version ' + expStats.expVersion); + 'Expected stats for exploration version ' + + config.explorationVersion + + ', but got stats for exploration version ' + + expStats.expVersion + ); } // Validate that all state names referenced by the provided arguments exist @@ -349,13 +394,16 @@ export class ExplorationImprovementsTaskRegistryService { ...openTasks.map(t => t.targetId), ...resolvedTaskTypesByStateName.keys(), ...topAnswersByStateName.keys(), - ...playthroughIssues.map(p => p.getStateNameWithIssue()) + ...playthroughIssues.map(p => p.getStateNameWithIssue()), ]); for (const stateName of allStateNameReferences) { if (!actualStateNames.has(stateName)) { throw new Error( - 'Unexpected reference to state "' + stateName + '", which does not ' + - 'exist'); + 'Unexpected reference to state "' + + stateName + + '", which does not ' + + 'exist' + ); } } @@ -363,28 +411,43 @@ export class ExplorationImprovementsTaskRegistryService { for (const task of openTasks) { if (task.entityId !== config.explorationId) { throw new Error( - 'Expected task for exploration "' + config.explorationId + '", but ' + - 'got task for exploration "' + task.entityId + '"'); + 'Expected task for exploration "' + + config.explorationId + + '", but ' + + 'got task for exploration "' + + task.entityId + + '"' + ); } if (task.entityVersion !== config.explorationVersion) { throw new Error( 'Expected task for exploration version ' + - config.explorationVersion + ', but got task for exploration ' + - 'version ' + task.entityVersion); + config.explorationVersion + + ', but got task for exploration ' + + 'version ' + + task.entityVersion + ); } } // Validate that there are no tasks with the same type targeting the same // state. const stateNameReferencesByTaskType = new Map( - ImprovementsConstants.TASK_TYPES.map(taskType => [taskType, new Set()])); + ImprovementsConstants.TASK_TYPES.map(taskType => [taskType, new Set()]) + ); for (const task of openTasks) { - const stateNameReferences = ( - stateNameReferencesByTaskType.get(task.taskType)); + const stateNameReferences = stateNameReferencesByTaskType.get( + task.taskType + ); if ((stateNameReferences as Set).has(task.targetId)) { throw new Error( - 'Found duplicate task of type "' + task.taskType + '" targeting ' + - 'state "' + task.targetId + '"'); + 'Found duplicate task of type "' + + task.taskType + + '" targeting ' + + 'state "' + + task.targetId + + '"' + ); } else { (stateNameReferences as Set).add(task.targetId); } @@ -394,8 +457,13 @@ export class ExplorationImprovementsTaskRegistryService { const stateNameReferences = stateNameReferencesByTaskType.get(taskType); if ((stateNameReferences as Set).has(stateName)) { throw new Error( - 'Found duplicate task of type "' + taskType + '" targeting state ' + - '"' + stateName + '"'); + 'Found duplicate task of type "' + + taskType + + '" targeting state ' + + '"' + + stateName + + '"' + ); } else { (stateNameReferences as Set).add(stateName); } @@ -404,13 +472,14 @@ export class ExplorationImprovementsTaskRegistryService { } private registerNewStateTasks( - stateName: string, - openTasks: readonly ExplorationTask[], - resolvedTaskTypes: readonly ExplorationTaskType[], - answerStats: readonly AnswerStats[], - cstPlaythroughIssues: readonly CstPlaythroughIssue[], - eqPlaythroughIssues: readonly EqPlaythroughIssue[], - misPlaythroughIssues: readonly MisPlaythroughIssue[]): StateTasks { + stateName: string, + openTasks: readonly ExplorationTask[], + resolvedTaskTypes: readonly ExplorationTaskType[], + answerStats: readonly AnswerStats[], + cstPlaythroughIssues: readonly CstPlaythroughIssue[], + eqPlaythroughIssues: readonly EqPlaythroughIssue[], + misPlaythroughIssues: readonly MisPlaythroughIssue[] + ): StateTasks { if (!this.expStats.hasStateStates(stateName)) { // Not an error to be missing stats. this.expStats = this.expStats.createNewWithStateAdded(stateName); @@ -420,24 +489,37 @@ export class ExplorationImprovementsTaskRegistryService { // let map = new Map([['a', 1], ['b', 3], ['a', 9]]); // map.get('a'); // Returns 9. ...ImprovementsConstants.TASK_TYPES.map(taskType => [ - taskType, ExplorationTaskModel.createNewObsoleteTask( - this.config.explorationId, this.config.explorationVersion, taskType, - stateName) - ]), - ...openTasks.map(task => [ - task.taskType, task + taskType, + ExplorationTaskModel.createNewObsoleteTask( + this.config.explorationId, + this.config.explorationVersion, + taskType, + stateName + ), ]), + ...openTasks.map(task => [task.taskType, task]), ...resolvedTaskTypes.map(taskType => [ - taskType, ExplorationTaskModel.createNewResolvedTask( - this.config.explorationId, this.config.explorationVersion, taskType, - stateName) + taskType, + ExplorationTaskModel.createNewResolvedTask( + this.config.explorationId, + this.config.explorationVersion, + taskType, + stateName + ), ]), ] as [ExplorationTaskType, ExplorationTask][]); const supportingStats = new SupportingStateStats( - this.expStats.getStateStats(stateName), answerStats, cstPlaythroughIssues, - eqPlaythroughIssues, misPlaythroughIssues); - const newStateTasks = ( - new StateTasks(stateName, tasksByType, supportingStats)); + this.expStats.getStateStats(stateName), + answerStats, + cstPlaythroughIssues, + eqPlaythroughIssues, + misPlaythroughIssues + ); + const newStateTasks = new StateTasks( + stateName, + tasksByType, + supportingStats + ); for (const task of newStateTasks) { if (task.isOpen()) { @@ -452,7 +534,8 @@ export class ExplorationImprovementsTaskRegistryService { private refreshStateTasks(stateName: string): void { const stateTasks = this.tasksByState.get(stateName); const tasksWithOldStatus: [ExplorationTask, string][] = ( - (stateTasks as StateTasks).map(task => [task, task.getStatus()])); + stateTasks as StateTasks + ).map(task => [task, task.getStatus()]); (stateTasks as StateTasks).refresh(this.expStats, this.config); @@ -476,10 +559,15 @@ export class ExplorationImprovementsTaskRegistryService { private popOpenTask(task: ExplorationTask): void { const arrayWithTask = this.openTasksByType.get(task.taskType); (arrayWithTask as ExplorationTask[]).splice( - (arrayWithTask as ExplorationTask[]).indexOf(task), 1); + (arrayWithTask as ExplorationTask[]).indexOf(task), + 1 + ); } } -angular.module('oppia').factory( - 'ExplorationImprovementsTaskRegistryService', - downgradeInjectable(ExplorationImprovementsTaskRegistryService)); +angular + .module('oppia') + .factory( + 'ExplorationImprovementsTaskRegistryService', + downgradeInjectable(ExplorationImprovementsTaskRegistryService) + ); diff --git a/core/templates/services/exploration-improvements.service.spec.ts b/core/templates/services/exploration-improvements.service.spec.ts index a7839781b8db..ac5bd1db70da 100644 --- a/core/templates/services/exploration-improvements.service.spec.ts +++ b/core/templates/services/exploration-improvements.service.spec.ts @@ -12,43 +12,45 @@ // See the License for the specific language governing permissions and // limitations under the License. - /** * @fileoverview Tests for ExplorationImprovementsService. */ -import { AnswerStats } from 'domain/exploration/answer-stats.model'; -import { ChangeListService } from 'pages/exploration-editor-page/services/change-list.service'; -import { ConfirmDeleteStateModalComponent } from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/confirm-delete-state-modal.component'; -import { ContextService } from 'services/context.service'; -import { ExplorationImprovementsBackendApiService, ExplorationImprovementsResponse } from 'services/exploration-improvements-backend-api.service'; -import { ExplorationImprovementsConfig } from 'domain/improvements/exploration-improvements-config.model'; -import { ExplorationImprovementsService } from './exploration-improvements.service'; -import { ExplorationImprovementsTaskRegistryService } from 'services/exploration-improvements-task-registry.service'; -import { ExplorationPermissions } from 'domain/exploration/exploration-permissions.model'; -import { ExplorationRightsService } from 'pages/exploration-editor-page/services/exploration-rights.service'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; -import { ExplorationStats } from 'domain/statistics/exploration-stats.model'; -import { ExplorationStatsService } from 'services/exploration-stats.service'; -import { fakeAsync, flushMicrotasks, TestBed } from '@angular/core/testing'; -import { HighBounceRateTask } from 'domain/improvements/high-bounce-rate-task.model'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { PlaythroughIssuesBackendApiService } from 'services/playthrough-issues-backend-api.service'; -import { Playthrough } from 'domain/statistics/playthrough.model'; -import { StateBackendDict } from 'domain/state/StateObjectFactory'; -import { StateObjectsBackendDict } from 'domain/exploration/StatesObjectFactory'; -import { StateStats } from 'domain/statistics/state-stats-model'; -import { StateTopAnswersStatsService } from 'services/state-top-answers-stats.service'; -import { UserExplorationPermissionsService } from 'pages/exploration-editor-page/services/user-exploration-permissions.service'; -import { GenerateContentIdService } from './generate-content-id.service'; -import { ExplorationTask } from 'domain/improvements/exploration-task.model'; +import {AnswerStats} from 'domain/exploration/answer-stats.model'; +import {ChangeListService} from 'pages/exploration-editor-page/services/change-list.service'; +import {ConfirmDeleteStateModalComponent} from 'pages/exploration-editor-page/editor-tab/templates/modal-templates/confirm-delete-state-modal.component'; +import {ContextService} from 'services/context.service'; +import { + ExplorationImprovementsBackendApiService, + ExplorationImprovementsResponse, +} from 'services/exploration-improvements-backend-api.service'; +import {ExplorationImprovementsConfig} from 'domain/improvements/exploration-improvements-config.model'; +import {ExplorationImprovementsService} from './exploration-improvements.service'; +import {ExplorationImprovementsTaskRegistryService} from 'services/exploration-improvements-task-registry.service'; +import {ExplorationPermissions} from 'domain/exploration/exploration-permissions.model'; +import {ExplorationRightsService} from 'pages/exploration-editor-page/services/exploration-rights.service'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; +import {ExplorationStats} from 'domain/statistics/exploration-stats.model'; +import {ExplorationStatsService} from 'services/exploration-stats.service'; +import {fakeAsync, flushMicrotasks, TestBed} from '@angular/core/testing'; +import {HighBounceRateTask} from 'domain/improvements/high-bounce-rate-task.model'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {PlaythroughIssuesBackendApiService} from 'services/playthrough-issues-backend-api.service'; +import {Playthrough} from 'domain/statistics/playthrough.model'; +import {StateBackendDict} from 'domain/state/StateObjectFactory'; +import {StateObjectsBackendDict} from 'domain/exploration/StatesObjectFactory'; +import {StateStats} from 'domain/statistics/state-stats-model'; +import {StateTopAnswersStatsService} from 'services/state-top-answers-stats.service'; +import {UserExplorationPermissionsService} from 'pages/exploration-editor-page/services/user-exploration-permissions.service'; +import {GenerateContentIdService} from './generate-content-id.service'; +import {ExplorationTask} from 'domain/improvements/exploration-task.model'; class MockNgbModal { open() { return { componentInstance: {}, - result: Promise.resolve() + result: Promise.resolve(), }; } } @@ -58,11 +60,9 @@ describe('Exploration Improvements Service', () => { let contextService: ContextService; let eibasGetTasksAsyncSpy: jasmine.Spy; let essGetExplorationStatsSpy: jasmine.Spy; - let explorationImprovementsBackendApiService: - ExplorationImprovementsBackendApiService; + let explorationImprovementsBackendApiService: ExplorationImprovementsBackendApiService; let explorationImprovementsService: ExplorationImprovementsService; - let explorationImprovementsTaskRegistryService: - ExplorationImprovementsTaskRegistryService; + let explorationImprovementsTaskRegistryService: ExplorationImprovementsTaskRegistryService; let explorationRightsService: ExplorationRightsService; let explorationStatesService: ExplorationStatesService; let explorationStatsService: ExplorationStatsService; @@ -99,10 +99,10 @@ describe('Exploration Improvements Service', () => { unicode_str: '', }, }, - rows: { value: 1 }, + rows: {value: 1}, catchMisspellings: { - value: false - } + value: false, + }, }, default_outcome: { dest: 'new state', @@ -130,7 +130,7 @@ describe('Exploration Improvements Service', () => { linked_skill_id: null, param_changes: [], solicit_answer_details: false, - card_is_checkpoint: false + card_is_checkpoint: false, }; const statesBackendDict: StateObjectsBackendDict = { [stateName]: stateBackendDict, @@ -138,12 +138,25 @@ describe('Exploration Improvements Service', () => { }; const newExpImprovementsConfig = (improvementsTabIsEnabled: boolean) => { return new ExplorationImprovementsConfig( - expId, expVersion, improvementsTabIsEnabled, 0.25, 0.20, 100); + expId, + expVersion, + improvementsTabIsEnabled, + 0.25, + 0.2, + 100 + ); }; const newExpPermissions = (canEdit: boolean) => { - return ( - new ExplorationPermissions( - false, false, false, false, false, false, canEdit, false)); + return new ExplorationPermissions( + false, + false, + false, + false, + false, + false, + canEdit, + false + ); }; beforeEach(() => { @@ -154,149 +167,185 @@ describe('Exploration Improvements Service', () => { ChangeListService, { provide: NgbModal, - useClass: MockNgbModal - } + useClass: MockNgbModal, + }, ], - declarations: [ - ConfirmDeleteStateModalComponent - ] + declarations: [ConfirmDeleteStateModalComponent], }); }); beforeEach(() => { changeListService = TestBed.inject(ChangeListService); contextService = TestBed.inject(ContextService); - explorationImprovementsBackendApiService = ( - TestBed.inject(ExplorationImprovementsBackendApiService)); - explorationImprovementsService = ( - TestBed.inject(ExplorationImprovementsService)); - explorationImprovementsTaskRegistryService = ( - TestBed.inject(ExplorationImprovementsTaskRegistryService)); + explorationImprovementsBackendApiService = TestBed.inject( + ExplorationImprovementsBackendApiService + ); + explorationImprovementsService = TestBed.inject( + ExplorationImprovementsService + ); + explorationImprovementsTaskRegistryService = TestBed.inject( + ExplorationImprovementsTaskRegistryService + ); explorationRightsService = TestBed.inject(ExplorationRightsService); explorationStatesService = TestBed.inject(ExplorationStatesService); explorationStatsService = TestBed.inject(ExplorationStatsService); ngbModal = TestBed.inject(NgbModal); - playthroughIssuesBackendApiService = ( - TestBed.inject(PlaythroughIssuesBackendApiService)); + playthroughIssuesBackendApiService = TestBed.inject( + PlaythroughIssuesBackendApiService + ); stateTopAnswersStatsService = TestBed.inject(StateTopAnswersStatsService); - userExplorationPermissionsService = ( - TestBed.inject(UserExplorationPermissionsService)); + userExplorationPermissionsService = TestBed.inject( + UserExplorationPermissionsService + ); generateContentIdService = TestBed.inject(GenerateContentIdService); - generateContentIdService.init(() => 0, () => { }); + generateContentIdService.init( + () => 0, + () => {} + ); spyOn(contextService, 'getExplorationId').and.returnValue(expId); - eibasGetTasksAsyncSpy = ( - spyOn(explorationImprovementsBackendApiService, 'getTasksAsync')); - essGetExplorationStatsSpy = ( - spyOn(explorationStatsService, 'getExplorationStatsAsync')); - pibasFetchIssuesSpy = ( - spyOn(playthroughIssuesBackendApiService, 'fetchIssuesAsync')); - stassGetTopAnswersByStateNameAsyncSpy = ( - spyOn(stateTopAnswersStatsService, 'getTopAnswersByStateNameAsync')); - - eibasGetTasksAsyncSpy.and.returnValue(Promise.resolve( - new ExplorationImprovementsResponse([], new Map()))); - essGetExplorationStatsSpy.and.returnValue(Promise.resolve( - new ExplorationStats(expId, expVersion, 0, 0, 0, new Map()))); - pibasFetchIssuesSpy.and.returnValue(Promise.resolve( - [])); - stassGetTopAnswersByStateNameAsyncSpy.and.returnValue(Promise.resolve( - new Map())); + eibasGetTasksAsyncSpy = spyOn( + explorationImprovementsBackendApiService, + 'getTasksAsync' + ); + essGetExplorationStatsSpy = spyOn( + explorationStatsService, + 'getExplorationStatsAsync' + ); + pibasFetchIssuesSpy = spyOn( + playthroughIssuesBackendApiService, + 'fetchIssuesAsync' + ); + stassGetTopAnswersByStateNameAsyncSpy = spyOn( + stateTopAnswersStatsService, + 'getTopAnswersByStateNameAsync' + ); + + eibasGetTasksAsyncSpy.and.returnValue( + Promise.resolve(new ExplorationImprovementsResponse([], new Map())) + ); + essGetExplorationStatsSpy.and.returnValue( + Promise.resolve( + new ExplorationStats(expId, expVersion, 0, 0, 0, new Map()) + ) + ); + pibasFetchIssuesSpy.and.returnValue(Promise.resolve([])); + stassGetTopAnswersByStateNameAsyncSpy.and.returnValue( + Promise.resolve(new Map()) + ); explorationStatesService.init(statesBackendDict, false); }); - it('should enable improvements tab based on back-end response', - fakeAsync(async() => { - spyOn(explorationRightsService, 'isPublic').and.returnValue(true); - spyOn(userExplorationPermissionsService, 'getPermissionsAsync') - .and.returnValue(Promise.resolve(newExpPermissions(true))); - spyOn(explorationImprovementsBackendApiService, 'getConfigAsync') - .and.returnValue(Promise.resolve(newExpImprovementsConfig(true))); - - explorationImprovementsService.initAsync(); - flushMicrotasks(); - - expect( - await explorationImprovementsService.isImprovementsTabEnabledAsync() - ).toBeTrue(); - })); - - it('should disable improvements tab based on back-end response', - fakeAsync(async() => { - spyOn(explorationRightsService, 'isPublic').and.returnValue(true); - spyOn(userExplorationPermissionsService, 'getPermissionsAsync') - .and.returnValue(Promise.resolve(newExpPermissions(true))); - spyOn(explorationImprovementsBackendApiService, 'getConfigAsync') - .and.returnValue(Promise.resolve(newExpImprovementsConfig(false))); - - explorationImprovementsService.initAsync(); - flushMicrotasks(); - - expect( - await explorationImprovementsService.isImprovementsTabEnabledAsync() - ).toBeFalse(); - })); + it('should enable improvements tab based on back-end response', fakeAsync(async () => { + spyOn(explorationRightsService, 'isPublic').and.returnValue(true); + spyOn( + userExplorationPermissionsService, + 'getPermissionsAsync' + ).and.returnValue(Promise.resolve(newExpPermissions(true))); + spyOn( + explorationImprovementsBackendApiService, + 'getConfigAsync' + ).and.returnValue(Promise.resolve(newExpImprovementsConfig(true))); + + explorationImprovementsService.initAsync(); + flushMicrotasks(); - it('should disable improvements tab for private explorations', - fakeAsync(async() => { - spyOn(explorationRightsService, 'isPublic').and.returnValue(false); - spyOn(userExplorationPermissionsService, 'getPermissionsAsync') - .and.returnValue(Promise.resolve(newExpPermissions(true))); - spyOn(explorationImprovementsBackendApiService, 'getConfigAsync') - .and.returnValue(Promise.resolve(newExpImprovementsConfig(true))); + expect( + await explorationImprovementsService.isImprovementsTabEnabledAsync() + ).toBeTrue(); + })); - explorationImprovementsService.initAsync(); - flushMicrotasks(); + it('should disable improvements tab based on back-end response', fakeAsync(async () => { + spyOn(explorationRightsService, 'isPublic').and.returnValue(true); + spyOn( + userExplorationPermissionsService, + 'getPermissionsAsync' + ).and.returnValue(Promise.resolve(newExpPermissions(true))); + spyOn( + explorationImprovementsBackendApiService, + 'getConfigAsync' + ).and.returnValue(Promise.resolve(newExpImprovementsConfig(false))); + + explorationImprovementsService.initAsync(); + flushMicrotasks(); - expect( - await explorationImprovementsService.isImprovementsTabEnabledAsync() - ).toBeFalse(); - })); + expect( + await explorationImprovementsService.isImprovementsTabEnabledAsync() + ).toBeFalse(); + })); - it('should disable improvements tab for non-editors when config gives false', - fakeAsync(async() => { - spyOn(explorationRightsService, 'isPublic').and.returnValue(true); - spyOn(userExplorationPermissionsService, 'getPermissionsAsync') - .and.returnValue(Promise.resolve(newExpPermissions(false))); - spyOn(explorationImprovementsBackendApiService, 'getConfigAsync') - .and.returnValue(Promise.resolve(newExpImprovementsConfig(false))); + it('should disable improvements tab for private explorations', fakeAsync(async () => { + spyOn(explorationRightsService, 'isPublic').and.returnValue(false); + spyOn( + userExplorationPermissionsService, + 'getPermissionsAsync' + ).and.returnValue(Promise.resolve(newExpPermissions(true))); + spyOn( + explorationImprovementsBackendApiService, + 'getConfigAsync' + ).and.returnValue(Promise.resolve(newExpImprovementsConfig(true))); + + explorationImprovementsService.initAsync(); + flushMicrotasks(); - explorationImprovementsService.initAsync(); - flushMicrotasks(); + expect( + await explorationImprovementsService.isImprovementsTabEnabledAsync() + ).toBeFalse(); + })); - expect( - await explorationImprovementsService.isImprovementsTabEnabledAsync() - ).toBeFalse(); - })); + it('should disable improvements tab for non-editors when config gives false', fakeAsync(async () => { + spyOn(explorationRightsService, 'isPublic').and.returnValue(true); + spyOn( + userExplorationPermissionsService, + 'getPermissionsAsync' + ).and.returnValue(Promise.resolve(newExpPermissions(false))); + spyOn( + explorationImprovementsBackendApiService, + 'getConfigAsync' + ).and.returnValue(Promise.resolve(newExpImprovementsConfig(false))); + + explorationImprovementsService.initAsync(); + flushMicrotasks(); - it('should disable improvements tab for non-editors when config gives true', - fakeAsync(async() => { - spyOn(explorationRightsService, 'isPublic').and.returnValue(true); - spyOn(userExplorationPermissionsService, 'getPermissionsAsync') - .and.returnValue(Promise.resolve(newExpPermissions(false))); - spyOn(explorationImprovementsBackendApiService, 'getConfigAsync') - .and.returnValue(Promise.resolve(newExpImprovementsConfig(true))); + expect( + await explorationImprovementsService.isImprovementsTabEnabledAsync() + ).toBeFalse(); + })); - explorationImprovementsService.initAsync(); - flushMicrotasks(); + it('should disable improvements tab for non-editors when config gives true', fakeAsync(async () => { + spyOn(explorationRightsService, 'isPublic').and.returnValue(true); + spyOn( + userExplorationPermissionsService, + 'getPermissionsAsync' + ).and.returnValue(Promise.resolve(newExpPermissions(false))); + spyOn( + explorationImprovementsBackendApiService, + 'getConfigAsync' + ).and.returnValue(Promise.resolve(newExpImprovementsConfig(true))); + + explorationImprovementsService.initAsync(); + flushMicrotasks(); - expect( - await explorationImprovementsService.isImprovementsTabEnabledAsync() - ).toBeFalse(); - })); + expect( + await explorationImprovementsService.isImprovementsTabEnabledAsync() + ).toBeFalse(); + })); - it('should propagate errors from the back-end', fakeAsync(async() => { + it('should propagate errors from the back-end', fakeAsync(async () => { const error = new Error('Whoops!'); - spyOn(userExplorationPermissionsService, 'getPermissionsAsync') - .and.throwError(error); + spyOn( + userExplorationPermissionsService, + 'getPermissionsAsync' + ).and.throwError(error); const onSuccess = jasmine.createSpy('onSuccess'); const onFailure = jasmine.createSpy('onFailure', reason => { expect(reason).toBe(error); }); - const promise = explorationImprovementsService.initAsync() + const promise = explorationImprovementsService + .initAsync() .then(onSuccess, onFailure); flushMicrotasks(); await promise; @@ -309,60 +358,80 @@ describe('Exploration Improvements Service', () => { let eibasGetConfigAsyncSpy: jasmine.Spy; beforeEach(() => { - eibasPostTasksAsyncSpy = ( - spyOn(explorationImprovementsBackendApiService, 'postTasksAsync')); - eibasGetConfigAsyncSpy = ( - spyOn(explorationImprovementsBackendApiService, 'getConfigAsync')); + eibasPostTasksAsyncSpy = spyOn( + explorationImprovementsBackendApiService, + 'postTasksAsync' + ); + eibasGetConfigAsyncSpy = spyOn( + explorationImprovementsBackendApiService, + 'getConfigAsync' + ); spyOn(explorationRightsService, 'isPublic').and.returnValue(true); - spyOn(userExplorationPermissionsService, 'getPermissionsAsync') - .and.returnValue(Promise.resolve(newExpPermissions(true))); - eibasGetConfigAsyncSpy.and.returnValue(Promise.resolve( - newExpImprovementsConfig(true))); + spyOn( + userExplorationPermissionsService, + 'getPermissionsAsync' + ).and.returnValue(Promise.resolve(newExpPermissions(true))); + eibasGetConfigAsyncSpy.and.returnValue( + Promise.resolve(newExpImprovementsConfig(true)) + ); }); - it('should do nothing when flush is attempted while the improvements ' + - 'tab is disabled', fakeAsync(() => { - eibasGetConfigAsyncSpy.and.returnValue(Promise.resolve( - newExpImprovementsConfig(false))); + it( + 'should do nothing when flush is attempted while the improvements ' + + 'tab is disabled', + fakeAsync(() => { + eibasGetConfigAsyncSpy.and.returnValue( + Promise.resolve(newExpImprovementsConfig(false)) + ); - explorationImprovementsService.initAsync(); - expect( - async() => ( - await explorationImprovementsService.flushUpdatedTasksToBackend())) - .not.toThrowError(); + explorationImprovementsService.initAsync(); + expect( + async () => + await explorationImprovementsService.flushUpdatedTasksToBackend() + ).not.toThrowError(); - flushMicrotasks(); - })); + flushMicrotasks(); + }) + ); - it('should post new high bounce rate tasks', fakeAsync(async() => { + it('should post new high bounce rate tasks', fakeAsync(async () => { // Set-up the conditions to generate an HBR task: // - A state demonstrating a high bounce-rate (determined by config). const numStarts = 100; const numCompletions = 60; // Bounce-rate is 40% because only 40% of starts led to a completion. const expStats = new ExplorationStats( - expId, expVersion, numStarts, numStarts, numCompletions, new Map([ + expId, + expVersion, + numStarts, + numStarts, + numCompletions, + new Map([ [stateName, new StateStats(0, 0, numStarts, 0, 0, numCompletions)], ['End', new StateStats(0, 0, numStarts, 0, 0, numCompletions)], - ])); + ]) + ); essGetExplorationStatsSpy.and.returnValue(Promise.resolve(expStats)); // - A state with an early-quit playthrough associated to it. - const eqPlaythrough = ( - Playthrough.createNewEarlyQuitPlaythrough( - expId, expVersion, { - state_name: {value: stateName}, - time_spent_in_exp_in_msecs: {value: 1000}, - }, [])); - pibasFetchIssuesSpy.and.returnValue(Promise.resolve( - [eqPlaythrough])); + const eqPlaythrough = Playthrough.createNewEarlyQuitPlaythrough( + expId, + expVersion, + { + state_name: {value: stateName}, + time_spent_in_exp_in_msecs: {value: 1000}, + }, + [] + ); + pibasFetchIssuesSpy.and.returnValue(Promise.resolve([eqPlaythrough])); // The newly open HBR tasks should be flushed to the back-end. eibasPostTasksAsyncSpy.and.callFake( - async(_: number, tasks: ExplorationTask[]) => { + async (_: number, tasks: ExplorationTask[]) => { expect(tasks.length).toEqual(1); expect(tasks[0].taskType).toEqual('high_bounce_rate'); - }); + } + ); explorationImprovementsService.initAsync(); let p = explorationImprovementsService.flushUpdatedTasksToBackend(); @@ -374,9 +443,10 @@ describe('Exploration Improvements Service', () => { // Each newly opened HBR task is flushed once and only once. eibasPostTasksAsyncSpy.calls.reset(); eibasPostTasksAsyncSpy.and.callFake( - async(_: number, tasks: ExplorationTask[]) => { + async (_: number, tasks: ExplorationTask[]) => { expect(tasks.length).toEqual(0); - }); + } + ); p = explorationImprovementsService.flushUpdatedTasksToBackend(); flushMicrotasks(); @@ -385,17 +455,23 @@ describe('Exploration Improvements Service', () => { expect(eibasPostTasksAsyncSpy).toHaveBeenCalled(); })); - it('should post obsolete high bounce rate tasks', fakeAsync(async() => { + it('should post obsolete high bounce rate tasks', fakeAsync(async () => { // Set-up the conditions to obsolete an HBR task: // - A state demonstrating a low bounce-rate. const numStarts = 100; const numCompletions = 100; // Bounce-rate is 0% because every start led to a completion. const expStats = new ExplorationStats( - expId, expVersion, numStarts, numStarts, numCompletions, new Map([ + expId, + expVersion, + numStarts, + numStarts, + numCompletions, + new Map([ [stateName, new StateStats(0, 0, numStarts, 0, 0, numCompletions)], ['End', new StateStats(0, 0, numStarts, 0, 0, numCompletions)], - ])); + ]) + ); essGetExplorationStatsSpy.and.returnValue(Promise.resolve(expStats)); // Mock a preexisting open HBR task provided by the back-end. @@ -411,8 +487,11 @@ describe('Exploration Improvements Service', () => { resolved_on_msecs: null, resolver_username: null, }); - eibasGetTasksAsyncSpy.and.returnValue(Promise.resolve( - new ExplorationImprovementsResponse([hbrTask], new Map()))); + eibasGetTasksAsyncSpy.and.returnValue( + Promise.resolve( + new ExplorationImprovementsResponse([hbrTask], new Map()) + ) + ); // The HBR task should no longer be open. let p = explorationImprovementsService.initAsync(); @@ -422,14 +501,15 @@ describe('Exploration Improvements Service', () => { expect(hbrTask.isOpen()).toBeFalse(); expect( explorationImprovementsTaskRegistryService.getOpenHighBounceRateTasks() - .length) - .toEqual(0); + .length + ).toEqual(0); // The HBR task should be flushed. eibasPostTasksAsyncSpy.and.callFake( - async(_: number, tasks: ExplorationTask[]) => { + async (_: number, tasks: ExplorationTask[]) => { expect(tasks).toEqual([hbrTask]); - }); + } + ); p = explorationImprovementsService.flushUpdatedTasksToBackend(); flushMicrotasks(); @@ -440,9 +520,10 @@ describe('Exploration Improvements Service', () => { // The HBR task should not be flushed again. eibasPostTasksAsyncSpy.calls.reset(); eibasPostTasksAsyncSpy.and.callFake( - async(_: number, tasks: ExplorationTask[]) => { + async (_: number, tasks: ExplorationTask[]) => { expect(tasks).toEqual([]); - }); + } + ); p = explorationImprovementsService.flushUpdatedTasksToBackend(); flushMicrotasks(); @@ -451,80 +532,87 @@ describe('Exploration Improvements Service', () => { expect(eibasPostTasksAsyncSpy).toHaveBeenCalled(); })); - it('should post new NGR tasks after they are resolved', fakeAsync( - async() => { + it('should post new NGR tasks after they are resolved', fakeAsync(async () => { // Set-up the conditions to generate an NGR task: // - A high-frequency unaddressed answer. - const answerStats = new AnswerStats('foo', 'foo', 100, false); - stassGetTopAnswersByStateNameAsyncSpy.and.returnValue( - Promise.resolve(new Map([[stateName, [answerStats]]]))); - const expStats = new ExplorationStats( - expId, expVersion, 0, 0, 0, - new Map([[stateName, new StateStats(0, 0, 0, 0, 0, 0)]])); - essGetExplorationStatsSpy.and.returnValue( - Promise.resolve(expStats)); - - // Initialize the service, this should generate a new NGR task. - let p = explorationImprovementsService.initAsync(); - flushMicrotasks(); - await p; - - const [ngrTask] = ( - explorationImprovementsTaskRegistryService - .getOpenNeedsGuidingResponsesTasks()); - expect(ngrTask).toBeDefined(); - expect(ngrTask.targetId).toEqual(stateName); - - // There should be no tasks to flush, because the NGR task is still - // open. - eibasPostTasksAsyncSpy.and.callFake( - async(_: number, tasks: ExplorationTask[]) => { - expect(tasks).toEqual([]); - }); - - p = explorationImprovementsService.flushUpdatedTasksToBackend(); - flushMicrotasks(); - await p; - expect(eibasPostTasksAsyncSpy).toHaveBeenCalled(); - - // Once the NGR task is resolved, however, it should get flushed. - answerStats.isAddressed = true; - explorationImprovementsTaskRegistryService - .onStateInteractionSaved( - explorationStatesService.getState(stateName)); - expect(ngrTask.isResolved()).toBeTrue(); - - eibasPostTasksAsyncSpy.calls.reset(); - eibasPostTasksAsyncSpy.and.callFake( - async(_: number, tasks: ExplorationTask[]) => { - expect(tasks).toEqual([ngrTask]); - }); - - p = explorationImprovementsService.flushUpdatedTasksToBackend(); - flushMicrotasks(); - await p; + const answerStats = new AnswerStats('foo', 'foo', 100, false); + stassGetTopAnswersByStateNameAsyncSpy.and.returnValue( + Promise.resolve(new Map([[stateName, [answerStats]]])) + ); + const expStats = new ExplorationStats( + expId, + expVersion, + 0, + 0, + 0, + new Map([[stateName, new StateStats(0, 0, 0, 0, 0, 0)]]) + ); + essGetExplorationStatsSpy.and.returnValue(Promise.resolve(expStats)); - expect(eibasPostTasksAsyncSpy).toHaveBeenCalled(); + // Initialize the service, this should generate a new NGR task. + let p = explorationImprovementsService.initAsync(); + flushMicrotasks(); + await p; - // The NGR task should be flushed once and only once. - eibasPostTasksAsyncSpy.calls.reset(); - eibasPostTasksAsyncSpy.and.callFake( - async(_: number, tasks: ExplorationTask[]) => { - expect(tasks).toEqual([]); - }); + const [ngrTask] = + explorationImprovementsTaskRegistryService.getOpenNeedsGuidingResponsesTasks(); + expect(ngrTask).toBeDefined(); + expect(ngrTask.targetId).toEqual(stateName); - p = explorationImprovementsService.flushUpdatedTasksToBackend(); - flushMicrotasks(); - await p; + // There should be no tasks to flush, because the NGR task is still + // open. + eibasPostTasksAsyncSpy.and.callFake( + async (_: number, tasks: ExplorationTask[]) => { + expect(tasks).toEqual([]); + } + ); - expect(eibasPostTasksAsyncSpy).toHaveBeenCalled(); - })); + p = explorationImprovementsService.flushUpdatedTasksToBackend(); + flushMicrotasks(); + await p; + expect(eibasPostTasksAsyncSpy).toHaveBeenCalled(); - it('should not store post-init NGR tasks', fakeAsync(async() => { + // Once the NGR task is resolved, however, it should get flushed. + answerStats.isAddressed = true; + explorationImprovementsTaskRegistryService.onStateInteractionSaved( + explorationStatesService.getState(stateName) + ); + expect(ngrTask.isResolved()).toBeTrue(); + + eibasPostTasksAsyncSpy.calls.reset(); + eibasPostTasksAsyncSpy.and.callFake( + async (_: number, tasks: ExplorationTask[]) => { + expect(tasks).toEqual([ngrTask]); + } + ); + + p = explorationImprovementsService.flushUpdatedTasksToBackend(); + flushMicrotasks(); + await p; + + expect(eibasPostTasksAsyncSpy).toHaveBeenCalled(); + + // The NGR task should be flushed once and only once. + eibasPostTasksAsyncSpy.calls.reset(); + eibasPostTasksAsyncSpy.and.callFake( + async (_: number, tasks: ExplorationTask[]) => { + expect(tasks).toEqual([]); + } + ); + + p = explorationImprovementsService.flushUpdatedTasksToBackend(); + flushMicrotasks(); + await p; + + expect(eibasPostTasksAsyncSpy).toHaveBeenCalled(); + })); + + it('should not store post-init NGR tasks', fakeAsync(async () => { // An NGR task will not be generated because all answers are addressed. const answerStats = new AnswerStats('foo', 'foo', 100, true); stassGetTopAnswersByStateNameAsyncSpy.and.returnValue( - Promise.resolve(new Map([[stateName, [answerStats]]]))); + Promise.resolve(new Map([[stateName, [answerStats]]])) + ); // Initialize the service. This should not generate a new NGR task. let p = explorationImprovementsService.initAsync(); @@ -532,30 +620,32 @@ describe('Exploration Improvements Service', () => { await p; expect( - explorationImprovementsTaskRegistryService - .getOpenNeedsGuidingResponsesTasks().length) - .toEqual(0); + explorationImprovementsTaskRegistryService.getOpenNeedsGuidingResponsesTasks() + .length + ).toEqual(0); // After making answer unaddressed, a new NGR task should be generated. answerStats.isAddressed = false; - explorationImprovementsTaskRegistryService - .onStateInteractionSaved(explorationStatesService.getState(stateName)); - const [ngrTask] = ( - explorationImprovementsTaskRegistryService - .getOpenNeedsGuidingResponsesTasks()); + explorationImprovementsTaskRegistryService.onStateInteractionSaved( + explorationStatesService.getState(stateName) + ); + const [ngrTask] = + explorationImprovementsTaskRegistryService.getOpenNeedsGuidingResponsesTasks(); expect(ngrTask).toBeDefined(); // Even after resolving the new task. answerStats.isAddressed = true; - explorationImprovementsTaskRegistryService - .onStateInteractionSaved(explorationStatesService.getState(stateName)); + explorationImprovementsTaskRegistryService.onStateInteractionSaved( + explorationStatesService.getState(stateName) + ); expect(ngrTask.isResolved()).toBeTrue(); // It should not be flushed because it wasn't created by initAsync(). eibasPostTasksAsyncSpy.and.callFake( - async(_: number, tasks: ExplorationTask[]) => { + async (_: number, tasks: ExplorationTask[]) => { expect(tasks).toEqual([]); - }); + } + ); p = explorationImprovementsService.flushUpdatedTasksToBackend(); flushMicrotasks(); @@ -571,19 +661,25 @@ describe('Exploration Improvements Service', () => { spyOn(changeListService, 'deleteState').and.stub(); spyOn(changeListService, 'editStateProperty').and.stub(); spyOn(changeListService, 'renameState').and.stub(); - spyOn(explorationImprovementsBackendApiService, 'getConfigAsync') - .and.returnValue(Promise.resolve(newExpImprovementsConfig(true))); + spyOn( + explorationImprovementsBackendApiService, + 'getConfigAsync' + ).and.returnValue(Promise.resolve(newExpImprovementsConfig(true))); spyOn(explorationRightsService, 'isPublic').and.returnValue(true); - spyOn(userExplorationPermissionsService, 'getPermissionsAsync') - .and.returnValue(Promise.resolve(newExpPermissions(true))); + spyOn( + userExplorationPermissionsService, + 'getPermissionsAsync' + ).and.returnValue(Promise.resolve(newExpPermissions(true))); explorationImprovementsService.initAsync(); flushMicrotasks(); })); it('should respond to state additions', fakeAsync(() => { - let onStateAddedSpy = ( - spyOn(explorationImprovementsTaskRegistryService, 'onStateAdded')); + let onStateAddedSpy = spyOn( + explorationImprovementsTaskRegistryService, + 'onStateAdded' + ); explorationStatesService.addState('Prologue', () => {}); flushMicrotasks(); @@ -593,13 +689,15 @@ describe('Exploration Improvements Service', () => { it('should respond to state deletions', fakeAsync(() => { spyOn(ngbModal, 'open').and.callFake((dlg, opt) => { - return ({ + return { componentInstance: NgbModalRef, - result: Promise.resolve() - } as NgbModalRef); + result: Promise.resolve(), + } as NgbModalRef; }); - let onStateDeletedSpy = ( - spyOn(explorationImprovementsTaskRegistryService, 'onStateDeleted')); + let onStateDeletedSpy = spyOn( + explorationImprovementsTaskRegistryService, + 'onStateDeleted' + ); explorationStatesService.deleteState('End'); flushMicrotasks(); @@ -608,8 +706,10 @@ describe('Exploration Improvements Service', () => { })); it('should respond to state renames', fakeAsync(() => { - let onStateRenamedSpy = ( - spyOn(explorationImprovementsTaskRegistryService, 'onStateRenamed')); + let onStateRenamedSpy = spyOn( + explorationImprovementsTaskRegistryService, + 'onStateRenamed' + ); explorationStatesService.renameState('Introduction', 'Start'); flushMicrotasks(); @@ -618,13 +718,16 @@ describe('Exploration Improvements Service', () => { it('should respond to state interaction changes', fakeAsync(() => { let onStateInteractionSavedSpy = spyOn( - explorationImprovementsTaskRegistryService, 'onStateInteractionSaved'); + explorationImprovementsTaskRegistryService, + 'onStateInteractionSaved' + ); explorationStatesService.saveInteractionAnswerGroups('Introduction', []); flushMicrotasks(); expect(onStateInteractionSavedSpy).toHaveBeenCalledWith( - explorationStatesService.getState('Introduction')); + explorationStatesService.getState('Introduction') + ); })); }); }); diff --git a/core/templates/services/exploration-improvements.service.ts b/core/templates/services/exploration-improvements.service.ts index 21009abc8c2b..947dc0950d4b 100644 --- a/core/templates/services/exploration-improvements.service.ts +++ b/core/templates/services/exploration-improvements.service.ts @@ -17,26 +17,26 @@ * of data related to exploration improvement tasks. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { merge } from 'd3-array'; -import { ExplorationImprovementsConfig } from 'domain/improvements/exploration-improvements-config.model'; -import { HighBounceRateTask } from 'domain/improvements/high-bounce-rate-task.model'; -import { NeedsGuidingResponsesTask } from 'domain/improvements/needs-guiding-response-task.model'; -import { State } from 'domain/state/StateObjectFactory'; -import { ExplorationRightsService } from 'pages/exploration-editor-page/services/exploration-rights.service'; -import { ExplorationStatesService } from 'pages/exploration-editor-page/services/exploration-states.service'; -import { UserExplorationPermissionsService } from 'pages/exploration-editor-page/services/user-exploration-permissions.service'; -import { ContextService } from 'services/context.service'; -import { ExplorationImprovementsBackendApiService } from 'services/exploration-improvements-backend-api.service'; -import { ExplorationImprovementsTaskRegistryService } from 'services/exploration-improvements-task-registry.service'; -import { ExplorationStatsService } from 'services/exploration-stats.service'; -import { StateTopAnswersStatsService } from 'services/state-top-answers-stats.service'; -import { PlaythroughIssuesService } from 'services/playthrough-issues.service'; -import { ExplorationTaskType } from 'domain/improvements/exploration-task.model'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {merge} from 'd3-array'; +import {ExplorationImprovementsConfig} from 'domain/improvements/exploration-improvements-config.model'; +import {HighBounceRateTask} from 'domain/improvements/high-bounce-rate-task.model'; +import {NeedsGuidingResponsesTask} from 'domain/improvements/needs-guiding-response-task.model'; +import {State} from 'domain/state/StateObjectFactory'; +import {ExplorationRightsService} from 'pages/exploration-editor-page/services/exploration-rights.service'; +import {ExplorationStatesService} from 'pages/exploration-editor-page/services/exploration-states.service'; +import {UserExplorationPermissionsService} from 'pages/exploration-editor-page/services/user-exploration-permissions.service'; +import {ContextService} from 'services/context.service'; +import {ExplorationImprovementsBackendApiService} from 'services/exploration-improvements-backend-api.service'; +import {ExplorationImprovementsTaskRegistryService} from 'services/exploration-improvements-task-registry.service'; +import {ExplorationStatsService} from 'services/exploration-stats.service'; +import {StateTopAnswersStatsService} from 'services/state-top-answers-stats.service'; +import {PlaythroughIssuesService} from 'services/playthrough-issues.service'; +import {ExplorationTaskType} from 'domain/improvements/exploration-task.model'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ExplorationImprovementsService { // These properties are initialized using int method and we need to do @@ -54,16 +54,13 @@ export class ExplorationImprovementsService { constructor( private explorationRightsService: ExplorationRightsService, private explorationStatesService: ExplorationStatesService, - private userExplorationPermissionsService: - UserExplorationPermissionsService, + private userExplorationPermissionsService: UserExplorationPermissionsService, private contextService: ContextService, - private explorationImprovementsBackendApiService: - ExplorationImprovementsBackendApiService, - private explorationImprovementsTaskRegistryService: - ExplorationImprovementsTaskRegistryService, + private explorationImprovementsBackendApiService: ExplorationImprovementsBackendApiService, + private explorationImprovementsTaskRegistryService: ExplorationImprovementsTaskRegistryService, private explorationStatsService: ExplorationStatsService, private stateTopAnswersStatsService: StateTopAnswersStatsService, - private playthroughIssuesService: PlaythroughIssuesService, + private playthroughIssuesService: PlaythroughIssuesService ) { this.openHbrTasks = []; this.ngrTasksOpenSinceInit = []; @@ -83,13 +80,12 @@ export class ExplorationImprovementsService { } async flushUpdatedTasksToBackend(): Promise { - if (!await this.isImprovementsTabEnabledAsync()) { + if (!(await this.isImprovementsTabEnabledAsync())) { return; } - const hbrTasksStillOpen = ( - this.explorationImprovementsTaskRegistryService - .getOpenHighBounceRateTasks()); + const hbrTasksStillOpen = + this.explorationImprovementsTaskRegistryService.getOpenHighBounceRateTasks(); await this.explorationImprovementsBackendApiService.postTasksAsync( this.config.explorationId, @@ -97,92 +93,112 @@ export class ExplorationImprovementsService { this.openHbrTasks.filter(t => t.isObsolete()), hbrTasksStillOpen.filter(t => !this.openHbrTasks.includes(t)), this.ngrTasksOpenSinceInit.filter(t => t.isResolved()), - ])); + ]) + ); this.openHbrTasks = hbrTasksStillOpen; - this.ngrTasksOpenSinceInit = - this.ngrTasksOpenSinceInit.filter(t => t.isOpen()); + this.ngrTasksOpenSinceInit = this.ngrTasksOpenSinceInit.filter(t => + t.isOpen() + ); } async isImprovementsTabEnabledAsync(): Promise { await this.initAsync(); return ( - this.improvementsTabIsAccessible && - this.config.improvementsTabIsEnabled); + this.improvementsTabIsAccessible && this.config.improvementsTabIsEnabled + ); } async doInitAsync(): Promise { - const userPermissions = ( - await this.userExplorationPermissionsService.getPermissionsAsync()); + const userPermissions = + await this.userExplorationPermissionsService.getPermissionsAsync(); - this.improvementsTabIsAccessible = ( - this.explorationRightsService.isPublic() && userPermissions.canEdit); + this.improvementsTabIsAccessible = + this.explorationRightsService.isPublic() && userPermissions.canEdit; if (!this.improvementsTabIsAccessible) { return; } const expId = this.contextService.getExplorationId(); - this.config = ( - await this.explorationImprovementsBackendApiService - .getConfigAsync(expId)); + this.config = + await this.explorationImprovementsBackendApiService.getConfigAsync(expId); if (!this.config.improvementsTabIsEnabled) { return; } - this.playthroughIssuesService - .initSession(expId, this.config.explorationVersion); + this.playthroughIssuesService.initSession( + expId, + this.config.explorationVersion + ); const states = this.explorationStatesService.getStates(); - const expStats = ( - await this.explorationStatsService.getExplorationStatsAsync(expId)); - const {openTasks, resolvedTaskTypesByStateName} = ( - await this.explorationImprovementsBackendApiService.getTasksAsync(expId)); - const topAnswersByStateName = ( + const expStats = + await this.explorationStatsService.getExplorationStatsAsync(expId); + const {openTasks, resolvedTaskTypesByStateName} = + await this.explorationImprovementsBackendApiService.getTasksAsync(expId); + const topAnswersByStateName = await this.stateTopAnswersStatsService.getTopAnswersByStateNameAsync( - expId, states)); + expId, + states + ); const playthroughIssues = await this.playthroughIssuesService.getIssues(); - this.openHbrTasks = ( - openTasks.filter(t => t.taskType === 'high_bounce_rate') as - HighBounceRateTask[]); + this.openHbrTasks = openTasks.filter( + t => t.taskType === 'high_bounce_rate' + ) as HighBounceRateTask[]; this.explorationImprovementsTaskRegistryService.initialize( - this.config, states, expStats, openTasks, - resolvedTaskTypesByStateName as - Map, - topAnswersByStateName, playthroughIssues); + this.config, + states, + expStats, + openTasks, + resolvedTaskTypesByStateName as Map, + topAnswersByStateName, + playthroughIssues + ); - this.ngrTasksOpenSinceInit = ( - this.explorationImprovementsTaskRegistryService - .getOpenNeedsGuidingResponsesTasks()); + this.ngrTasksOpenSinceInit = + this.explorationImprovementsTaskRegistryService.getOpenNeedsGuidingResponsesTasks(); this.explorationStatesService.registerOnStateAddedCallback( (stateName: string) => { this.explorationImprovementsTaskRegistryService.onStateAdded(stateName); - }); + } + ); this.explorationStatesService.registerOnStateDeletedCallback( (stateName: string) => { - this.explorationImprovementsTaskRegistryService - .onStateDeleted(stateName); - }); + this.explorationImprovementsTaskRegistryService.onStateDeleted( + stateName + ); + } + ); this.explorationStatesService.registerOnStateRenamedCallback( (oldName: string, newName: string) => { this.explorationImprovementsTaskRegistryService.onStateRenamed( - oldName, newName); - }); + oldName, + newName + ); + } + ); this.explorationStatesService.registerOnStateInteractionSavedCallback( (state: State) => { this.explorationImprovementsTaskRegistryService.onStateInteractionSaved( - state); - }); + state + ); + } + ); } } -angular.module('oppia').factory('ExplorationImprovementsService', - downgradeInjectable(ExplorationImprovementsService)); +angular + .module('oppia') + .factory( + 'ExplorationImprovementsService', + downgradeInjectable(ExplorationImprovementsService) + ); diff --git a/core/templates/services/exploration-stats-backend-api.service.spec.ts b/core/templates/services/exploration-stats-backend-api.service.spec.ts index c92f5d7039b7..1566d6e82b4c 100644 --- a/core/templates/services/exploration-stats-backend-api.service.spec.ts +++ b/core/templates/services/exploration-stats-backend-api.service.spec.ts @@ -16,18 +16,18 @@ * @fileoverview Unit tests for the ExplorationStatsBackendApiService. */ -import { HttpClientTestingModule, HttpTestingController } from - '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; import { ExplorationStats, - ExplorationStatsBackendDict + ExplorationStatsBackendDict, } from 'domain/statistics/exploration-stats.model'; -import { ExplorationStatsBackendApiService } from - 'services/exploration-stats-backend-api.service'; -import { StateStatsBackendDict } from - 'domain/statistics/state-stats-model'; +import {ExplorationStatsBackendApiService} from 'services/exploration-stats-backend-api.service'; +import {StateStatsBackendDict} from 'domain/statistics/state-stats-model'; describe('Exploration stats backend api service', () => { let explorationStatsBackendApiService: ExplorationStatsBackendApiService; @@ -36,8 +36,9 @@ describe('Exploration stats backend api service', () => { beforeEach(() => { TestBed.configureTestingModule({imports: [HttpClientTestingModule]}); - explorationStatsBackendApiService = ( - TestBed.get(ExplorationStatsBackendApiService)); + explorationStatsBackendApiService = TestBed.get( + ExplorationStatsBackendApiService + ); httpTestingController = TestBed.get(HttpTestingController); }); @@ -49,26 +50,25 @@ describe('Exploration stats backend api service', () => { num_actual_starts: 0, num_completions: 0, state_stats_mapping: { - Introduction: ( - { - total_answers_count: 0, - useful_feedback_count: 0, - total_hit_count: 0, - first_hit_count: 0, - num_completions: 0, - } as StateStatsBackendDict), + Introduction: { + total_answers_count: 0, + useful_feedback_count: 0, + total_hit_count: 0, + first_hit_count: 0, + num_completions: 0, + } as StateStatsBackendDict, }, }; - let explorationStats: ExplorationStats = ( - ExplorationStats.createFromBackendDict( - explorationStatsBackendDict)); + let explorationStats: ExplorationStats = + ExplorationStats.createFromBackendDict(explorationStatsBackendDict); let onSuccess = jasmine.createSpy('onSuccess', stats => { expect(stats).toEqual(explorationStats); }); let onFailure = jasmine.createSpy('onFailure'); - explorationStatsBackendApiService.fetchExplorationStatsAsync('eid') + explorationStatsBackendApiService + .fetchExplorationStatsAsync('eid') .then(onSuccess, onFailure); let req = httpTestingController.expectOne('/createhandler/statistics/eid'); diff --git a/core/templates/services/exploration-stats-backend-api.service.ts b/core/templates/services/exploration-stats-backend-api.service.ts index be74fd093b70..5b5d28ec65e5 100644 --- a/core/templates/services/exploration-stats-backend-api.service.ts +++ b/core/templates/services/exploration-stats-backend-api.service.ts @@ -17,35 +17,43 @@ * backend. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; import { ExplorationStats, - ExplorationStatsBackendDict + ExplorationStatsBackendDict, } from 'domain/statistics/exploration-stats.model'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; @Injectable({providedIn: 'root'}) export class ExplorationStatsBackendApiService { constructor( - private http: HttpClient, - private urlInterpolationService: UrlInterpolationService) {} + private http: HttpClient, + private urlInterpolationService: UrlInterpolationService + ) {} async fetchExplorationStatsAsync(expId: string): Promise { - return this.http.get( - this.urlInterpolationService.interpolateUrl( - '/createhandler/statistics/', { - exploration_id: expId - })).toPromise() + return this.http + .get( + this.urlInterpolationService.interpolateUrl( + '/createhandler/statistics/', + { + exploration_id: expId, + } + ) + ) + .toPromise() .then((dict: ExplorationStatsBackendDict) => { return ExplorationStats.createFromBackendDict(dict); }); } } -angular.module('oppia').factory( - 'ExplorationStatsBackendApiService', - downgradeInjectable(ExplorationStatsBackendApiService)); +angular + .module('oppia') + .factory( + 'ExplorationStatsBackendApiService', + downgradeInjectable(ExplorationStatsBackendApiService) + ); diff --git a/core/templates/services/exploration-stats.service.spec.ts b/core/templates/services/exploration-stats.service.spec.ts index fc3d89a859b4..8a23bf69c2e0 100644 --- a/core/templates/services/exploration-stats.service.spec.ts +++ b/core/templates/services/exploration-stats.service.spec.ts @@ -16,16 +16,14 @@ * @fileoverview Unit tests for the ExplorationStatsService. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; -import { ExplorationStatsBackendApiService } from - 'services/exploration-stats-backend-api.service'; -import { ExplorationStats } from - 'domain/statistics/exploration-stats.model'; -import { ExplorationStatsService } from 'services/exploration-stats.service'; +import {ExplorationStatsBackendApiService} from 'services/exploration-stats-backend-api.service'; +import {ExplorationStats} from 'domain/statistics/exploration-stats.model'; +import {ExplorationStatsService} from 'services/exploration-stats.service'; -describe('Exploration stats service', function() { +describe('Exploration stats service', function () { let explorationStatsBackendApiService: ExplorationStatsBackendApiService; let explorationStatsService: ExplorationStatsService; let explorationStats: ExplorationStats; @@ -33,31 +31,34 @@ describe('Exploration stats service', function() { beforeEach(() => { TestBed.configureTestingModule({imports: [HttpClientTestingModule]}); - explorationStatsBackendApiService = ( - TestBed.inject(ExplorationStatsBackendApiService)); + explorationStatsBackendApiService = TestBed.inject( + ExplorationStatsBackendApiService + ); explorationStatsService = TestBed.inject(ExplorationStatsService); }); beforeEach(() => { - explorationStats = ( - ExplorationStats.createFromBackendDict({ - exp_id: 'eid', - exp_version: 1, - num_starts: 2, - num_actual_starts: 20, - num_completions: 200, - state_stats_mapping: {}, - })); + explorationStats = ExplorationStats.createFromBackendDict({ + exp_id: 'eid', + exp_version: 1, + num_starts: 2, + num_actual_starts: 20, + num_completions: 200, + state_stats_mapping: {}, + }); }); it('should callout to backend api service for stats', fakeAsync(() => { - spyOn(explorationStatsBackendApiService, 'fetchExplorationStatsAsync') - .and.returnValue(Promise.resolve(explorationStats)); + spyOn( + explorationStatsBackendApiService, + 'fetchExplorationStatsAsync' + ).and.returnValue(Promise.resolve(explorationStats)); const onSuccess = jasmine.createSpy('onSuccess'); const onFailure = jasmine.createSpy('onFailure'); - explorationStatsService.getExplorationStatsAsync('eid') + explorationStatsService + .getExplorationStatsAsync('eid') .then(onSuccess, onFailure); flushMicrotasks(); @@ -66,18 +67,22 @@ describe('Exploration stats service', function() { })); it('should cache results after the first call', fakeAsync(() => { - const backendApiSpy = ( - spyOn(explorationStatsBackendApiService, 'fetchExplorationStatsAsync') - .and.returnValue(Promise.resolve(explorationStats))); + const backendApiSpy = spyOn( + explorationStatsBackendApiService, + 'fetchExplorationStatsAsync' + ).and.returnValue(Promise.resolve(explorationStats)); const onSuccess = jasmine.createSpy('onSuccess'); const onFailure = jasmine.createSpy('onFailure'); - explorationStatsService.getExplorationStatsAsync('eid') + explorationStatsService + .getExplorationStatsAsync('eid') .then(onSuccess, onFailure); - explorationStatsService.getExplorationStatsAsync('eid') + explorationStatsService + .getExplorationStatsAsync('eid') .then(onSuccess, onFailure); - explorationStatsService.getExplorationStatsAsync('eid') + explorationStatsService + .getExplorationStatsAsync('eid') .then(onSuccess, onFailure); flushMicrotasks(); diff --git a/core/templates/services/exploration-stats.service.ts b/core/templates/services/exploration-stats.service.ts index 12bb193726ea..ed6423b8df03 100644 --- a/core/templates/services/exploration-stats.service.ts +++ b/core/templates/services/exploration-stats.service.ts @@ -16,16 +16,14 @@ * @fileoverview Service for managing exploration-level statistics. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { ExplorationStats } from - 'domain/statistics/exploration-stats.model'; -import { ExplorationStatsBackendApiService } from - 'services/exploration-stats-backend-api.service'; +import {ExplorationStats} from 'domain/statistics/exploration-stats.model'; +import {ExplorationStatsBackendApiService} from 'services/exploration-stats-backend-api.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ExplorationStatsService { // 'statsCache' will be null until the exploration stats are fetched from @@ -33,18 +31,23 @@ export class ExplorationStatsService { private statsCache: Promise | null = null; constructor( - private explorationStatsBackendApiService: - ExplorationStatsBackendApiService) {} + private explorationStatsBackendApiService: ExplorationStatsBackendApiService + ) {} async getExplorationStatsAsync(expId: string): Promise { if (this.statsCache === null) { - this.statsCache = ( + this.statsCache = this.explorationStatsBackendApiService.fetchExplorationStatsAsync( - expId)); + expId + ); } return this.statsCache; } } -angular.module('oppia').factory( - 'ExplorationStatsService', downgradeInjectable(ExplorationStatsService)); +angular + .module('oppia') + .factory( + 'ExplorationStatsService', + downgradeInjectable(ExplorationStatsService) + ); diff --git a/core/templates/services/extension-tag-assembler.service.spec.ts b/core/templates/services/extension-tag-assembler.service.spec.ts index ab71c9c2189d..39b9b5a914af 100644 --- a/core/templates/services/extension-tag-assembler.service.spec.ts +++ b/core/templates/services/extension-tag-assembler.service.spec.ts @@ -16,21 +16,17 @@ * @fileoverview Unit tests for ExtensionTagAssemblerService. */ -import { TestBed } from '@angular/core/testing'; -import { ExtensionTagAssemblerService } from - './extension-tag-assembler.service'; -import { CamelCaseToHyphensPipe } from - 'filters/string-utility-filters/camel-case-to-hyphens.pipe'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; +import {TestBed} from '@angular/core/testing'; +import {ExtensionTagAssemblerService} from './extension-tag-assembler.service'; +import {CamelCaseToHyphensPipe} from 'filters/string-utility-filters/camel-case-to-hyphens.pipe'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; describe('Extension Tag Assembler Service', () => { let etas: ExtensionTagAssemblerService; beforeEach(() => { TestBed.configureTestingModule({ - providers: [ - CamelCaseToHyphensPipe - ] + providers: [CamelCaseToHyphensPipe], }); etas = TestBed.inject(ExtensionTagAssemblerService); }); @@ -41,25 +37,22 @@ describe('Extension Tag Assembler Service', () => { const expectedElement = '

'; expect( - etas.formatCustomizationArgAttrs( - element, interactionCustomizationArgs - ).outerHTML + etas.formatCustomizationArgAttrs(element, interactionCustomizationArgs) + .outerHTML ).toEqual(expectedElement); }); it('should format element with customization', () => { const element = document.createElement('p'); const interactionCustomizationArgs = { - choices: {value: 'sampleChoice'} + choices: {value: 'sampleChoice'}, }; - const expectedElement = '

'; + const expectedElement = + '

'; expect( - etas.formatCustomizationArgAttrs( - element, interactionCustomizationArgs - ).outerHTML + etas.formatCustomizationArgAttrs(element, interactionCustomizationArgs) + .outerHTML ).toEqual(expectedElement); }); @@ -68,18 +61,18 @@ describe('Extension Tag Assembler Service', () => { const interactionCustomizationArgs = { test: { value: { - attr: [new SubtitledHtml('html', 'ca_id')] - } - } + attr: [new SubtitledHtml('html', 'ca_id')], + }, + }, }; - const expectedElement = '

'; expect( - etas.formatCustomizationArgAttrs( - element, interactionCustomizationArgs - ).outerHTML + etas.formatCustomizationArgAttrs(element, interactionCustomizationArgs) + .outerHTML ).toEqual(expectedElement); }); @@ -87,17 +80,17 @@ describe('Extension Tag Assembler Service', () => { const element = document.createElement('p'); const interactionCustomizationArgs = { choices: {value: 'sampleChoice'}, - test: {value: 'sampleValue'} + test: {value: 'sampleValue'}, }; - const expectedElement = '

'; expect( - etas.formatCustomizationArgAttrs( - element, interactionCustomizationArgs - ).outerHTML + etas.formatCustomizationArgAttrs(element, interactionCustomizationArgs) + .outerHTML ).toEqual(expectedElement); }); }); diff --git a/core/templates/services/extension-tag-assembler.service.ts b/core/templates/services/extension-tag-assembler.service.ts index 54d0498520c4..941b52ddfa61 100644 --- a/core/templates/services/extension-tag-assembler.service.ts +++ b/core/templates/services/extension-tag-assembler.service.ts @@ -17,32 +17,30 @@ * the learner and editor views. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { CamelCaseToHyphensPipe } from - 'filters/string-utility-filters/camel-case-to-hyphens.pipe'; -import { HtmlEscaperService } from 'services/html-escaper.service'; +import {CamelCaseToHyphensPipe} from 'filters/string-utility-filters/camel-case-to-hyphens.pipe'; +import {HtmlEscaperService} from 'services/html-escaper.service'; import { InteractionCustomizationArgs, - InteractionCustomizationArgsBackendDict -} from - 'interactions/customization-args-defs'; -import { SubtitledUnicode } from - 'domain/exploration/SubtitledUnicodeObjectFactory'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; + InteractionCustomizationArgsBackendDict, +} from 'interactions/customization-args-defs'; +import {SubtitledUnicode} from 'domain/exploration/SubtitledUnicodeObjectFactory'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; // Service for assembling extension tags (for interactions). @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ExtensionTagAssemblerService { constructor( private htmlEscaperService: HtmlEscaperService, - private camelCaseToHyphens: CamelCaseToHyphensPipe) {} + private camelCaseToHyphens: CamelCaseToHyphensPipe + ) {} _convertCustomizationArgsToBackendDict( - customizationArgs: InteractionCustomizationArgs + customizationArgs: InteractionCustomizationArgs ): InteractionCustomizationArgsBackendDict { // Because of issues with circular dependencies, we cannot import // Interaction from InteractionObjectFactory in this file. @@ -50,13 +48,14 @@ export class ExtensionTagAssemblerService { // here to avoid the circular dependency. const traverseSchemaAndConvertSubtitledToDicts = ( - value: Object[] | Object + value: Object[] | Object ): Object[] | Object => { if (value instanceof SubtitledUnicode || value instanceof SubtitledHtml) { return value.toBackendDict(); } else if (value instanceof Array) { - return value.map( - element => traverseSchemaAndConvertSubtitledToDicts(element)); + return value.map(element => + traverseSchemaAndConvertSubtitledToDicts(element) + ); } else if (value instanceof Object) { type KeyOfValue = keyof typeof value; let _result: Record = {}; @@ -73,8 +72,7 @@ export class ExtensionTagAssemblerService { const customizationArgsBackendDict: Record = {}; Object.entries(customizationArgs).forEach(([caName, caValue]) => { customizationArgsBackendDict[caName] = { - value: traverseSchemaAndConvertSubtitledToDicts( - caValue.value) + value: traverseSchemaAndConvertSubtitledToDicts(caValue.value), }; }); @@ -82,21 +80,26 @@ export class ExtensionTagAssemblerService { } formatCustomizationArgAttrs( - element: HTMLElement, customizationArgs: InteractionCustomizationArgs + element: HTMLElement, + customizationArgs: InteractionCustomizationArgs ): HTMLElement { - const caBackendDict = ( - this._convertCustomizationArgsToBackendDict(customizationArgs) + const caBackendDict = this._convertCustomizationArgsToBackendDict( + customizationArgs ) as Record>; for (const caName in customizationArgs) { const caBackendDictValue = caBackendDict[caName].value; element.setAttribute( this.camelCaseToHyphens.transform(caName) + '-with-value', - this.htmlEscaperService.objToEscapedJson(caBackendDictValue)); + this.htmlEscaperService.objToEscapedJson(caBackendDictValue) + ); } return element; } } -angular.module('oppia').factory( - 'ExtensionTagAssemblerService', - downgradeInjectable(ExtensionTagAssemblerService)); +angular + .module('oppia') + .factory( + 'ExtensionTagAssemblerService', + downgradeInjectable(ExtensionTagAssemblerService) + ); diff --git a/core/templates/services/external-rte-save.service.spec.ts b/core/templates/services/external-rte-save.service.spec.ts index 6058149753a6..9c14164211dd 100644 --- a/core/templates/services/external-rte-save.service.spec.ts +++ b/core/templates/services/external-rte-save.service.spec.ts @@ -14,13 +14,12 @@ /** * @fileoverview Unit tests for ExternalRteSaveService -*/ + */ -import { EventEmitter } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; +import {EventEmitter} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; -import { ExternalRteSaveService } from - 'services/external-rte-save.service'; +import {ExternalRteSaveService} from 'services/external-rte-save.service'; describe('External Rte Save Service', () => { let externalRteSaveService: ExternalRteSaveService; @@ -32,6 +31,7 @@ describe('External Rte Save Service', () => { it('should fetch externalRteSave event emitter', () => { let sampleExternalRteSaveEventEmitter = new EventEmitter(); expect(externalRteSaveService.onExternalRteSave).toEqual( - sampleExternalRteSaveEventEmitter); + sampleExternalRteSaveEventEmitter + ); }); }); diff --git a/core/templates/services/external-rte-save.service.ts b/core/templates/services/external-rte-save.service.ts index 6d9fe75be2b1..07f85ca627dd 100644 --- a/core/templates/services/external-rte-save.service.ts +++ b/core/templates/services/external-rte-save.service.ts @@ -16,11 +16,11 @@ * @fileoverview Service provides emitter for external rte save */ -import { Injectable, EventEmitter } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {Injectable, EventEmitter} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ExternalRteSaveService { private _externalRteSaveEventEmitter = new EventEmitter(); @@ -30,5 +30,9 @@ export class ExternalRteSaveService { } } -angular.module('oppia').factory('ExternalRteSaveService', - downgradeInjectable(ExternalRteSaveService)); +angular + .module('oppia') + .factory( + 'ExternalRteSaveService', + downgradeInjectable(ExternalRteSaveService) + ); diff --git a/core/templates/services/external-save.service.spec.ts b/core/templates/services/external-save.service.spec.ts index 7ab4321a8cb7..899e37e2455b 100644 --- a/core/templates/services/external-save.service.spec.ts +++ b/core/templates/services/external-save.service.spec.ts @@ -14,13 +14,12 @@ /** * @fileoverview Unit tests for ExternalSaveService -*/ + */ -import { EventEmitter } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; +import {EventEmitter} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; -import { ExternalSaveService } from - 'services/external-save.service'; +import {ExternalSaveService} from 'services/external-save.service'; describe('External Save Service', () => { let externalSaveService: ExternalSaveService; @@ -32,6 +31,7 @@ describe('External Save Service', () => { it('should fetch externalSave event emitter', () => { let sampleExternalSaveEventEmitter = new EventEmitter(); expect(externalSaveService.onExternalSave).toEqual( - sampleExternalSaveEventEmitter); + sampleExternalSaveEventEmitter + ); }); }); diff --git a/core/templates/services/external-save.service.ts b/core/templates/services/external-save.service.ts index 57e27f71aa6e..7276a25f1d2f 100644 --- a/core/templates/services/external-save.service.ts +++ b/core/templates/services/external-save.service.ts @@ -16,11 +16,11 @@ * @fileoverview Service provides emitter for external save */ -import { Injectable, EventEmitter } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {Injectable, EventEmitter} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ExternalSaveService { private _externalSaveEventEmitter = new EventEmitter(); @@ -30,5 +30,6 @@ export class ExternalSaveService { } } -angular.module('oppia').factory('ExternalSaveService', - downgradeInjectable(ExternalSaveService)); +angular + .module('oppia') + .factory('ExternalSaveService', downgradeInjectable(ExternalSaveService)); diff --git a/core/templates/services/favicon.service.spec.ts b/core/templates/services/favicon.service.spec.ts index c01831a01948..d6da73379723 100644 --- a/core/templates/services/favicon.service.spec.ts +++ b/core/templates/services/favicon.service.spec.ts @@ -16,9 +16,9 @@ * @fileoverview Unit tests for favicon service. */ -import { TestBed } from '@angular/core/testing'; -import { WindowRef } from './contextual/window-ref.service'; -import { FaviconService } from './favicon.service'; +import {TestBed} from '@angular/core/testing'; +import {WindowRef} from './contextual/window-ref.service'; +import {FaviconService} from './favicon.service'; describe('Favicon service', () => { let faviconService: FaviconService; @@ -32,9 +32,9 @@ describe('Favicon service', () => { it('should set the favicon to the given url', () => { const faviconUrl = '/assets/images/logo/favicon.png'; const linkElement: HTMLLinkElement = document.createElement('link'); - spyOn( - windowRef.nativeWindow.document, 'querySelector' - ).and.returnValue(linkElement); + spyOn(windowRef.nativeWindow.document, 'querySelector').and.returnValue( + linkElement + ); faviconService.setFavicon(faviconUrl); diff --git a/core/templates/services/favicon.service.ts b/core/templates/services/favicon.service.ts index 23e7bb169b9e..4247f7da9610 100644 --- a/core/templates/services/favicon.service.ts +++ b/core/templates/services/favicon.service.ts @@ -16,30 +16,30 @@ * @fileoverview Service for changing favicon dynamically. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { WindowRef } from './contextual/window-ref.service'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {WindowRef} from './contextual/window-ref.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class FaviconService { - constructor( - private windowRef: WindowRef - ) {} + constructor(private windowRef: WindowRef) {} setFavicon(faviconUrl: string): void { - const link: HTMLLinkElement = ( + const link: HTMLLinkElement = this.windowRef.nativeWindow.document.querySelector('link[rel*="icon"]') || - this.windowRef.nativeWindow.document.createElement('link')); + this.windowRef.nativeWindow.document.createElement('link'); link.href = faviconUrl; link.type = 'image/x-icon'; link.rel = 'icon'; link.remove(); this.windowRef.nativeWindow.document - .getElementsByTagName('head')[0].appendChild(link); + .getElementsByTagName('head')[0] + .appendChild(link); } } -angular.module('oppia').factory('FaviconService', - downgradeInjectable(FaviconService)); +angular + .module('oppia') + .factory('FaviconService', downgradeInjectable(FaviconService)); diff --git a/core/templates/services/generate-content-id.service.spec.ts b/core/templates/services/generate-content-id.service.spec.ts index 80547848ce79..c5a9b3505c02 100644 --- a/core/templates/services/generate-content-id.service.spec.ts +++ b/core/templates/services/generate-content-id.service.spec.ts @@ -16,7 +16,7 @@ * @fileoverview Unit tests for GenerateContentIdService. */ -import { GenerateContentIdService } from 'services/generate-content-id.service'; +import {GenerateContentIdService} from 'services/generate-content-id.service'; describe('GenerateContentIdService', () => { let gcis: GenerateContentIdService; @@ -24,14 +24,20 @@ describe('GenerateContentIdService', () => { beforeEach(() => { gcis = new GenerateContentIdService(); let currentIndex = 0; - gcis.init(() => currentIndex++, () => {}); + gcis.init( + () => currentIndex++, + () => {} + ); }); - it('should generate content id for new feedbacks using next content' + - 'id index', () => { - expect(gcis.getNextStateId('feedback')).toEqual('feedback_0'); - expect(gcis.getNextStateId('feedback')).toEqual('feedback_1'); - }); + it( + 'should generate content id for new feedbacks using next content' + + 'id index', + () => { + expect(gcis.getNextStateId('feedback')).toEqual('feedback_0'); + expect(gcis.getNextStateId('feedback')).toEqual('feedback_1'); + } + ); it('should generate content id for new worked example', () => { expect( @@ -39,12 +45,14 @@ describe('GenerateContentIdService', () => { ).toEqual('worked_example_question_2'); expect( gcis.getNextId( - ['worked_example_explanation_1'], 'worked_example_explanation') + ['worked_example_explanation_1'], + 'worked_example_explanation' + ) ).toEqual('worked_example_explanation_2'); }); it('should throw error for unknown content id', () => { - expect(function() { + expect(function () { gcis.getNextId(['xyz'], 'random_component_name'); }).toThrowError('Unknown component name provided.'); }); diff --git a/core/templates/services/generate-content-id.service.ts b/core/templates/services/generate-content-id.service.ts index 25d528ccbbe1..c2a373cb5a4f 100644 --- a/core/templates/services/generate-content-id.service.ts +++ b/core/templates/services/generate-content-id.service.ts @@ -17,14 +17,13 @@ * SubtitledHtml domain objects. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; - -import { AppConstants } from 'app.constants'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {AppConstants} from 'app.constants'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class GenerateContentIdService { getNextIndex!: () => number; @@ -36,32 +35,31 @@ export class GenerateContentIdService { } generateIdForComponent( - existingComponentIds: string[], - componentName: string): string { + existingComponentIds: string[], + componentName: string + ): string { let contentIdList = JSON.parse(JSON.stringify(existingComponentIds)); let searchKey = componentName + '_'; let count = 0; for (let contentId in contentIdList) { if (contentIdList[contentId].indexOf(searchKey) === 0) { let splitContentId = contentIdList[contentId].split('_'); - let tempCount = - parseInt(splitContentId[splitContentId.length - 1]); + let tempCount = parseInt(splitContentId[splitContentId.length - 1]); if (tempCount > count) { count = tempCount; } } } - return (searchKey + String(count + 1)); + return searchKey + String(count + 1); } - _getNextId( - existingComponentIds: string[], - componentName: string): string { + _getNextId(existingComponentIds: string[], componentName: string): string { // Worked example questions and explanations do not live in the State domain // so they do not use next content id index. - if (componentName === AppConstants.COMPONENT_NAME_WORKED_EXAMPLE.QUESTION || - componentName === - AppConstants.COMPONENT_NAME_WORKED_EXAMPLE.EXPLANATION) { + if ( + componentName === AppConstants.COMPONENT_NAME_WORKED_EXAMPLE.QUESTION || + componentName === AppConstants.COMPONENT_NAME_WORKED_EXAMPLE.EXPLANATION + ) { return this.generateIdForComponent(existingComponentIds, componentName); } else { throw new Error('Unknown component name provided.'); @@ -73,9 +71,7 @@ export class GenerateContentIdService { return `${prefix}_${contentIdIndex}`; } - getNextId( - existingComponentIds: string[], - componentName: string): string { + getNextId(existingComponentIds: string[], componentName: string): string { return this._getNextId(existingComponentIds, componentName); } @@ -88,5 +84,9 @@ export class GenerateContentIdService { } } -angular.module('oppia').factory( - 'GenerateContentIdService', downgradeInjectable(GenerateContentIdService)); +angular + .module('oppia') + .factory( + 'GenerateContentIdService', + downgradeInjectable(GenerateContentIdService) + ); diff --git a/core/templates/services/guppy-configuration.service.spec.ts b/core/templates/services/guppy-configuration.service.spec.ts index 320527f12571..9deb5e93a0f8 100644 --- a/core/templates/services/guppy-configuration.service.spec.ts +++ b/core/templates/services/guppy-configuration.service.spec.ts @@ -16,12 +16,11 @@ * @fileoverview Unit test for GuppyConfigurationService */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { Component, OnInit } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {Component, OnInit} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; -import { GuppyConfigurationService } from - 'services/guppy-configuration.service'; +import {GuppyConfigurationService} from 'services/guppy-configuration.service'; declare global { interface Window { @@ -42,8 +41,8 @@ class MockGuppy { } static configure(name: string, val: Object): void {} - static 'remove_global_symbol'(symbol: string): void {} - static 'add_global_symbol'(name: string, symbol: Object): void {} + static remove_global_symbol(symbol: string): void {} + static add_global_symbol(name: string, symbol: Object): void {} } class MockComponent { @@ -59,7 +58,7 @@ class MockComponent { @Component({ template: '', - selector: 'mock-component-b' + selector: 'mock-component-b', }) class MockComponentB implements OnInit { constructor(private guppyConfigService: GuppyConfigurationService) {} @@ -100,22 +99,18 @@ describe('GuppyConfigurationService', () => { let fixture: ComponentFixture; beforeEach(waitForAsync(() => { - TestBed.configureTestingModule( - { - imports: [HttpClientTestingModule], - declarations: [MockComponentB], - } - ).compileComponents(); + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + declarations: [MockComponentB], + }).compileComponents(); guppyConfigurationService = TestBed.get(GuppyConfigurationService); window.Guppy = MockGuppy as unknown as Guppy; })); beforeEach(() => { - fixture = TestBed.createComponent( - MockComponentB); + fixture = TestBed.createComponent(MockComponentB); component = fixture.componentInstance; }); - it('should configure guppy on the first initialization', () => { GuppyConfigurationService.serviceIsInitialized = false; spyOn(Guppy, 'remove_global_symbol'); diff --git a/core/templates/services/guppy-configuration.service.ts b/core/templates/services/guppy-configuration.service.ts index 2b12a94ca5eb..f66601922b1f 100644 --- a/core/templates/services/guppy-configuration.service.ts +++ b/core/templates/services/guppy-configuration.service.ts @@ -16,27 +16,47 @@ * @fileoverview Service for initializing Guppy instances. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; const SYMBOLS_TO_REMOVE = [ - 'norm', 'utf8', 'text', 'sym_name', 'eval', 'floor', 'factorial', 'sub', - 'int', 'defi', 'deriv', 'sum', 'prod', 'root', 'vec', 'point', - 'infinity', 'leq', 'less', 'geq', 'greater', 'neq']; + 'norm', + 'utf8', + 'text', + 'sym_name', + 'eval', + 'floor', + 'factorial', + 'sub', + 'int', + 'defi', + 'deriv', + 'sum', + 'prod', + 'root', + 'vec', + 'point', + 'infinity', + 'leq', + 'less', + 'geq', + 'greater', + 'neq', +]; const MULTIPLICATION_SYMBOL_DICT = { output: { latex: '\\times', - asciimath: '*' + asciimath: '*', }, keys: ['*'], attrs: { group: 'operations', - type: '*' + type: '*', }, ast: { - type: 'operator' - } + type: 'operator', + }, }; const FRACTION_SYMBOL_DICT = { @@ -44,46 +64,49 @@ const FRACTION_SYMBOL_DICT = { latex: '\\frac{{$1}}{{$2}}', small_latex: '\\frac{{$1}}{{$2}}', asciimath: '/', - text: '({$1})/({$2})' + text: '({$1})/({$2})', }, input: 1, keys: ['/'], attrs: { type: 'fraction', - group: 'functions' + group: 'functions', }, - args: [{ - up: '1', - down: '2', - name: 'numerator', - small: 'yes' - }, { - up: '1', - down: '2', - 'delete': '1', - name: 'denominator', - small: 'yes' - }] + args: [ + { + up: '1', + down: '2', + name: 'numerator', + small: 'yes', + }, + { + up: '1', + down: '2', + delete: '1', + name: 'denominator', + small: 'yes', + }, + ], }; const DIVISION_SYMBOL_DICT = { output: { latex: '\\div', asciimath: '/', - text: '/' + text: '/', }, keys: ['/'], attrs: { group: 'operations', - type: '/' + type: '/', }, ast: { - type: 'operator' - } + type: 'operator', + }, }; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class GuppyConfigurationService { static serviceIsInitialized = false; @@ -105,17 +128,22 @@ export class GuppyConfigurationService { Guppy.configure('buttons', ['controls']); Guppy.configure( 'empty_content', - '\\color{grey}{\\text{\\small{Type a formula here.}}}'); + '\\color{grey}{\\text{\\small{Type a formula here.}}}' + ); GuppyConfigurationService.serviceIsInitialized = true; } - changeDivSymbol(useFraction: (boolean | { value: boolean }) = false): void { + changeDivSymbol(useFraction: boolean | {value: boolean} = false): void { Guppy.add_global_symbol( - '/', useFraction ? FRACTION_SYMBOL_DICT : DIVISION_SYMBOL_DICT + '/', + useFraction ? FRACTION_SYMBOL_DICT : DIVISION_SYMBOL_DICT ); } } -angular.module('oppia').factory( - 'GuppyConfigurationService', - downgradeInjectable(GuppyConfigurationService)); +angular + .module('oppia') + .factory( + 'GuppyConfigurationService', + downgradeInjectable(GuppyConfigurationService) + ); diff --git a/core/templates/services/guppy-initialization.service.spec.ts b/core/templates/services/guppy-initialization.service.spec.ts index 4836266cb521..09cb6764f161 100644 --- a/core/templates/services/guppy-initialization.service.spec.ts +++ b/core/templates/services/guppy-initialization.service.spec.ts @@ -16,10 +16,9 @@ * @fileoverview Unit test for GuppyInitializationService */ -import { TestBed } from '@angular/core/testing'; +import {TestBed} from '@angular/core/testing'; -import { GuppyInitializationService } from - 'services/guppy-initialization.service'; +import {GuppyInitializationService} from 'services/guppy-initialization.service'; declare global { interface Window { @@ -31,11 +30,11 @@ class MockGuppy { constructor(id: string, config: Object) {} engine = { - end: () => {} + end: () => {}, }; render(): void {} - 'import_text'(): void {} + import_text(): void {} asciimath(): string { return 'Dummy value'; } @@ -46,8 +45,8 @@ class MockGuppy { } static configure(name: string, val: Object): void {} - static 'remove_global_symbol'(symbol: string): void {} - static 'add_global_symbol'(name: string, symbol: Object): void {} + static remove_global_symbol(symbol: string): void {} + static add_global_symbol(name: string, symbol: Object): void {} } describe('GuppyInitializationService', () => { @@ -58,7 +57,7 @@ describe('GuppyInitializationService', () => { window.Guppy = MockGuppy as unknown as Guppy; }); - it('should assign a random id to the guppy divs', function() { + it('should assign a random id to the guppy divs', function () { let mockDocument = document.createElement('div'); mockDocument.classList.add('guppy-div-creator', 'guppy_active'); angular.element(document).find('body').append(mockDocument.outerHTML); @@ -71,7 +70,7 @@ describe('GuppyInitializationService', () => { } }); - it('should find active guppy div', function() { + it('should find active guppy div', function () { let mockDocument = document.createElement('div'); mockDocument.classList.add('guppy-div-creator', 'guppy_active'); angular.element(document).find('body').append(mockDocument.outerHTML); @@ -79,10 +78,11 @@ describe('GuppyInitializationService', () => { guppyInitializationService.init('guppy-div-creator', 'placeholder', 'x'); expect(guppyInitializationService.findActiveGuppyObject()).not.toBe( - undefined); + undefined + ); }); - it('should correctly change and get the value of showOSK var', function() { + it('should correctly change and get the value of showOSK var', function () { guppyInitializationService.setShowOSK(true); expect(guppyInitializationService.getShowOSK()).toBeTrue(); guppyInitializationService.setShowOSK(false); diff --git a/core/templates/services/guppy-initialization.service.ts b/core/templates/services/guppy-initialization.service.ts index 590361afed7f..706d40c8b38a 100644 --- a/core/templates/services/guppy-initialization.service.ts +++ b/core/templates/services/guppy-initialization.service.ts @@ -16,10 +16,10 @@ * @fileoverview Service for initializing guppy instances. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; -import { MathInteractionsService } from 'services/math-interactions.service'; +import {MathInteractionsService} from 'services/math-interactions.service'; export class GuppyObject { // These properties are initialized using constructor function @@ -34,7 +34,7 @@ export class GuppyObject { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class GuppyInitializationService { private guppyInstances: GuppyObject[] = []; @@ -42,8 +42,11 @@ export class GuppyInitializationService { static interactionType: string; private static allowedVariables: string[] = []; - init(guppyDivClassName: string, placeholderText: string, initialValue = ''): - void { + init( + guppyDivClassName: string, + placeholderText: string, + initialValue = '' + ): void { this.onScreenKeyboardShown = false; let guppyDivs = document.querySelectorAll('.' + guppyDivClassName); let divId, guppyInstance; @@ -57,20 +60,23 @@ export class GuppyInitializationService { guppyInstance.configure( 'empty_content', - '\\color{grey}{\\text{\\small{' + placeholderText + '}}}'); + '\\color{grey}{\\text{\\small{' + placeholderText + '}}}' + ); // Initialize it with a value for the creator's view. if (initialValue.length !== 0) { if (initialValue.indexOf('=') !== -1) { let splitByEquals = initialValue.split('='); splitByEquals[0] = mathInteractionsService.insertMultiplicationSigns( - splitByEquals[0]); + splitByEquals[0] + ); splitByEquals[1] = mathInteractionsService.insertMultiplicationSigns( - splitByEquals[1]); + splitByEquals[1] + ); initialValue = splitByEquals.join('='); } else { - initialValue = mathInteractionsService.insertMultiplicationSigns( - initialValue); + initialValue = + mathInteractionsService.insertMultiplicationSigns(initialValue); } initialValue = initialValue.replace(/abs\(/g, 'absolutevalue('); initialValue = initialValue.replace(/sqrt\(/g, 'squareroot('); @@ -108,6 +114,9 @@ export class GuppyInitializationService { } } -angular.module('oppia').factory( - 'GuppyInitializationService', - downgradeInjectable(GuppyInitializationService)); +angular + .module('oppia') + .factory( + 'GuppyInitializationService', + downgradeInjectable(GuppyInitializationService) + ); diff --git a/core/templates/services/html-escaper.service.spec.ts b/core/templates/services/html-escaper.service.spec.ts index 25a62f84a766..8c83155ea89f 100644 --- a/core/templates/services/html-escaper.service.spec.ts +++ b/core/templates/services/html-escaper.service.spec.ts @@ -16,10 +16,10 @@ * @fileoverview Unit tests for HTML serialization and escaping services. */ -import { TestBed } from '@angular/core/testing'; +import {TestBed} from '@angular/core/testing'; -import { HtmlEscaperService } from 'services/html-escaper.service'; -import { LoggerService } from './contextual/logger.service'; +import {HtmlEscaperService} from 'services/html-escaper.service'; +import {LoggerService} from './contextual/logger.service'; describe('HTML escaper service', () => { let ohe: HtmlEscaperService; @@ -27,40 +27,49 @@ describe('HTML escaper service', () => { beforeEach(() => { TestBed.configureTestingModule({ - providers: [HtmlEscaperService] + providers: [HtmlEscaperService], }); ohe = TestBed.inject(HtmlEscaperService); loggerService = TestBed.inject(LoggerService); }); - it('should correctly translate between escaped and unescaped strings', - () => { - let strs = ['abc', 'a&b', '&&&&&']; - for (let i = 0; i < strs.length; i++) { - expect(ohe.escapedStrToUnescapedStr( - ohe.unescapedStrToEscapedStr(strs[i]))).toEqual(strs[i]); - } + it('should correctly translate between escaped and unescaped strings', () => { + let strs = ['abc', 'a&b', '&&&&&']; + for (let i = 0; i < strs.length; i++) { + expect( + ohe.escapedStrToUnescapedStr(ohe.unescapedStrToEscapedStr(strs[i])) + ).toEqual(strs[i]); } - ); + }); it('should correctly escape and unescape JSON', () => { - let objs = [{ - a: 'b' - }, ['a', 'b'], 2, true, 'abc']; + let objs = [ + { + a: 'b', + }, + ['a', 'b'], + 2, + true, + 'abc', + ]; for (let i = 0; i < objs.length; i++) { - expect(ohe.escapedJsonToObj( - ohe.objToEscapedJson(objs[i]))).toEqual(objs[i]); + expect(ohe.escapedJsonToObj(ohe.objToEscapedJson(objs[i]))).toEqual( + objs[i] + ); } }); - it('should log an error if an empty string was passed to' + - ' JSON decoder', () => { - spyOn(loggerService, 'error'); + it( + 'should log an error if an empty string was passed to' + ' JSON decoder', + () => { + spyOn(loggerService, 'error'); - let escapedJsonToObjResponse = ohe.escapedJsonToObj(''); + let escapedJsonToObjResponse = ohe.escapedJsonToObj(''); - expect(escapedJsonToObjResponse).toBe(''); - expect(loggerService.error).toHaveBeenCalledWith( - 'Empty string was passed to JSON decoder.'); - }); + expect(escapedJsonToObjResponse).toBe(''); + expect(loggerService.error).toHaveBeenCalledWith( + 'Empty string was passed to JSON decoder.' + ); + } + ); }); diff --git a/core/templates/services/html-escaper.service.ts b/core/templates/services/html-escaper.service.ts index 0bed20fbcf64..d79f9f382cc2 100644 --- a/core/templates/services/html-escaper.service.ts +++ b/core/templates/services/html-escaper.service.ts @@ -16,13 +16,13 @@ * @fileoverview Service for HTML serialization and escaping. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { LoggerService } from 'services/contextual/logger.service'; +import {LoggerService} from 'services/contextual/logger.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class HtmlEscaperService { constructor(private loggerService: LoggerService) {} @@ -36,7 +36,6 @@ export class HtmlEscaperService { return this.unescapedStrToEscapedStr(JSON.stringify(obj)); } - /** * This function is used to convert * a escaped JSON string to its object counterpart. @@ -76,12 +75,13 @@ export class HtmlEscaperService { escapedStrToUnescapedStr(value: string): string { return String(value) .replace(/"/g, '"') - .replace(/'/g, '\'') + .replace(/'/g, "'") .replace(/</g, '<') .replace(/>/g, '>') .replace(/&/g, '&'); } } -angular.module('oppia').factory( - 'HtmlEscaperService', downgradeInjectable(HtmlEscaperService)); +angular + .module('oppia') + .factory('HtmlEscaperService', downgradeInjectable(HtmlEscaperService)); diff --git a/core/templates/services/html-length.service.spec.ts b/core/templates/services/html-length.service.spec.ts index ea7ee659f3e8..e31ef03051c5 100644 --- a/core/templates/services/html-length.service.spec.ts +++ b/core/templates/services/html-length.service.spec.ts @@ -16,11 +16,11 @@ * @fileoverview Unit tests for HtmlLengthService. */ -import { TestBed } from '@angular/core/testing'; -import { DomSanitizer} from '@angular/platform-browser'; -import { SecurityContext } from '@angular/core'; -import { HtmlLengthService } from 'services/html-length.service'; -import { LoggerService } from './contextual/logger.service'; +import {TestBed} from '@angular/core/testing'; +import {DomSanitizer} from '@angular/platform-browser'; +import {SecurityContext} from '@angular/core'; +import {HtmlLengthService} from 'services/html-length.service'; +import {LoggerService} from './contextual/logger.service'; class MockLoggerService { error(message: string) {} @@ -41,13 +41,13 @@ describe('Html Length Service', () => { HtmlLengthService, { provide: LoggerService, - useClass: MockLoggerService + useClass: MockLoggerService, }, { provide: DomSanitizer, - useClass: MockDomSanitizer - } - ] + useClass: MockDomSanitizer, + }, + ], }); htmlLengthService = TestBed.inject(HtmlLengthService); }); @@ -65,169 +65,187 @@ describe('Html Length Service', () => { }); it('should compute word count for strings with only paragraph tag', () => { - const htmlString = ( - '

Earth Our home planet is the third planet' + - ' from the sun.

'); + const htmlString = + '

Earth Our home planet is the third planet' + ' from the sun.

'; const result = htmlLengthService.computeHtmlLength(htmlString, 'word'); expect(result).toBe(11); }); - it('should compute word count for strings with paragraph tag and ' + - 'descendants text nodes', () => { - const testCases = [ - { - input: '

This is a brief exploration about conjugations' + - ' in Spanish.

', - expected: 9 - }, - { - input: '

This is a test.

', - expected: 4 - }, - { - input: '

This text is bolded. This is italic

', - expected: 7 - }, - { - input: '

Check out below

"Text is bolded"

', - expected: 6 - }, - { - input: '

🙂 Hello, how are you?

', - expected: 5 - }, - { - input: '

مر حبا كيف حالك؟

', - expected: 4 - }, - ]; - - for (const testCase of testCases) { - const result = htmlLengthService - .computeHtmlLength(testCase.input, 'word'); - expect(result).toBe(testCase.expected); + it( + 'should compute word count for strings with paragraph tag and ' + + 'descendants text nodes', + () => { + const testCases = [ + { + input: + '

This is a brief exploration about conjugations' + + ' in Spanish.

', + expected: 9, + }, + { + input: '

This is a test.

', + expected: 4, + }, + { + input: '

This text is bolded. This is italic

', + expected: 7, + }, + { + input: '

Check out below

"Text is bolded"

', + expected: 6, + }, + { + input: '

🙂 Hello, how are you?

', + expected: 5, + }, + { + input: '

مر حبا كيف حالك؟

', + expected: 4, + }, + ]; + + for (const testCase of testCases) { + const result = htmlLengthService.computeHtmlLength( + testCase.input, + 'word' + ); + expect(result).toBe(testCase.expected); + } } - }); + ); - it('should compute word count of content with text and non-text ' + - '(1 math tag)', () => { - const htmlString = '

Hi this seems too good to be true but what' + - ' to do man

'; + it( + 'should compute word count of content with text and non-text ' + + '(1 math tag)', + () => { + const htmlString = + '

Hi this seems too good to be true but what' + + ' to do man

'; - const result = htmlLengthService.computeHtmlLength(htmlString, 'word'); + const result = htmlLengthService.computeHtmlLength(htmlString, 'word'); - /* + /* The paragraph "Hi this seems too good to be true but what to do man" contains 13 words. The 'math' tag is also considered as a single word. Therefore, the total word count is 14 (13 words from the paragraph + 1 'math' tag). */ - expect(result).toBe(14); - }); + expect(result).toBe(14); + } + ); - it('should compute word count of content with both text and non-text' + - '(1 image tag)', () => { - const htmlString = '

naghiue abghy gjuh  

' + - ''; + it( + 'should compute word count of content with both text and non-text' + + '(1 image tag)', + () => { + const htmlString = + '

naghiue abghy gjuh  

' + + ''; - const result = htmlLengthService.computeHtmlLength(htmlString, 'word'); + const result = htmlLengthService.computeHtmlLength(htmlString, 'word'); - /* + /* "naghiue abghy gjuh  " is a paragraph with 3 words. "Svg file for demo" is the alt text for an image, contributing 4 words, and the image itself counts as 10 words. Therefore, the total word count is 3 (paragraph) + 4 (alt text) + 10 (image count) = 17 words. */ - expect(result).toBe(17); - }); - - it('should compute word count of content with text and non-text ' + - '(1 math tag and 1 image tag)', () => { - const htmlString = '

Hi this seems too good to be true but what' + - ' to do man

' + - ''; - - const result = htmlLengthService.computeHtmlLength(htmlString, 'word'); + expect(result).toBe(17); + } + ); - /* + it( + 'should compute word count of content with text and non-text ' + + '(1 math tag and 1 image tag)', + () => { + const htmlString = + '

Hi this seems too good to be true but what' + + ' to do man

' + + ''; + + const result = htmlLengthService.computeHtmlLength(htmlString, 'word'); + + /* "Hi this seems too good to be true but what to do man" is a paragraph with 13 words. The 'math' tag is counted as 1 word. "Svg file for demo" is the alt text for an image, contributing 4 words, and the image itself counts as 10 words. Therefore, the total word count is 13 (paragraph) + 1 ('math' tag) + 4 (alt text) + 10 (image count) = 28 words. */ - expect(result).toBe(28); - }); - - - it('should compute word count of content with text and all non-text' + - '(1 Collapsible, 1 Tabs, 1 Image, 1 Link, 1 Math, 1 SkillReview,' + - ' 1 Video)', () => { - const htmlString = ( - '' + - '

Demo hint just to check

' + - '' + - '' + - '

' + - '

' + - '

' + - '

' + - '' + - '

 

' + - '

done!

'); + expect(result).toBe(28); + } + ); - const result = htmlLengthService.computeHtmlLength(htmlString, 'word'); - /* + it( + 'should compute word count of content with text and all non-text' + + '(1 Collapsible, 1 Tabs, 1 Image, 1 Link, 1 Math, 1 SkillReview,' + + ' 1 Video)', + () => { + const htmlString = + '' + + '

Demo hint just to check

' + + '' + + '' + + '

' + + '

' + + '

' + + '

' + + '' + + '

 

' + + '

done!

'; + + const result = htmlLengthService.computeHtmlLength(htmlString, 'word'); + /* The 'collapsible' and 'tab' tags each add a count of 1000 words, totaling 2000 words. The phrases "Demo hint just to check" and "done!" contribute 5 and 1 words respectively, adding up to 6 words. The 'math' @@ -239,66 +257,76 @@ describe('Html Length Service', () => { 4 (alt text) + 10 (image count) + 2 ('skillreview' tag) + 2 ('link' tag) = 2025 words. */ - expect(result).toBe(2025); - }); + expect(result).toBe(2025); + } + ); - it('should compute word count of content with text and non-text ' + - '(1 Collapsible tag and 1 tab tag)', () => { - const htmlString = '' + - '

Demo hint just to check

{ + const htmlString = + '' + + '

Demo hint just to check

'; - const result = htmlLengthService.computeHtmlLength(htmlString, 'word'); - /* + const result = htmlLengthService.computeHtmlLength(htmlString, 'word'); + /* The 'collapsible' and 'tab' tags each add a count of 1000 words, totaling 2000 words. "Demo hint just to check" is a paragraph with 5 words. Therefore, the total word count is 5 (paragraph) + 2000 (tag counts) = 2005 words. */ - expect(result).toBe(2005); - }); - - it('should compute word count of content with text and non-text ' + - '(1 link tag and 1 concept card tag)', () => { - const htmlString = '

' + - '

' + - '

Demo hint just to check

' + - '

'; - - const result = htmlLengthService.computeHtmlLength(htmlString, 'word'); + expect(result).toBe(2005); + } + ); - /* + it( + 'should compute word count of content with text and non-text ' + + '(1 link tag and 1 concept card tag)', + () => { + const htmlString = + '

' + + '

' + + '

Demo hint just to check

' + + '

'; + + const result = htmlLengthService.computeHtmlLength(htmlString, 'word'); + + /* The 'skillreview' tag contains "Concept card" and the 'link' tag contains "oppia link", each contributing 2 words. "Demo hint just to check" is a paragraph with 5 words. Therefore, the total word count is 5 (paragraph) + 2 ('skillreview' tag) + 2 ('link' tag) = 9 words. */ - expect(result).toBe(9); - }); + expect(result).toBe(9); + } + ); it('should compute word count of content of ordered lists', () => { - const htmlString = '
    ' + - '
  1. This is the first item
  2. ' + - '
  3. This is second item
  4. ' + - '
  5. This is the third item
  6. ' + - '
'; + const htmlString = + '
    ' + + '
  1. This is the first item
  2. ' + + '
  3. This is second item
  4. ' + + '
  5. This is the third item
  6. ' + + '
'; const result = htmlLengthService.computeHtmlLength(htmlString, 'word'); @@ -306,11 +334,12 @@ describe('Html Length Service', () => { }); it('should compute word count of content of unordered lists', () => { - const htmlString = '
    ' + - '
  • This is the first item
  • ' + - '
  • This is second item
  • ' + - '
  • This is the third item
  • ' + - '
'; + const htmlString = + '
    ' + + '
  • This is the first item
  • ' + + '
  • This is second item
  • ' + + '
  • This is the third item
  • ' + + '
'; const result = htmlLengthService.computeHtmlLength(htmlString, 'word'); @@ -325,112 +354,135 @@ describe('Html Length Service', () => { expect(result).toBe(0); }); - it('should compute character count for strings with only paragraph tag', - () => { - const htmlString = ( - '

Earth Our home planet is the third planet' + - ' from the sun.

'); - - const result = htmlLengthService - .computeHtmlLength(htmlString, 'character'); + it('should compute character count for strings with only paragraph tag', () => { + const htmlString = + '

Earth Our home planet is the third planet' + ' from the sun.

'; - expect(result).toBe(55); - }); + const result = htmlLengthService.computeHtmlLength(htmlString, 'character'); - it('should compute character count for strings with paragraph tag and' + - ' descendants text nodes', () => { - const testCases = [ - { - input: '

This is a brief exploration about conjugations' + - 'in Spanish.

', - expected: 57 - }, - { - input: '

This is a test.

', - expected: 15 - }, - { - input: '

This text is bolded. This is italic

', - expected: 35 - }, - { - input: '

Check out below

"Text is bolded"

', - expected: 32 - }, - { - input: '

🙂 Hello, how are you?

', - expected: 22 - }, - { - input: '

مر حبا كيف حالك؟

', - expected: 16 - }, - ]; - - for (const testCase of testCases) { - const result = htmlLengthService - .computeHtmlLength(testCase.input, 'character'); - expect(result).toBe(testCase.expected); - } + expect(result).toBe(55); }); - it('should compute character count of content with text and non-text ' + - '(1 math tag)', () => { - const htmlString = '

Hi this seems too good to be true but what' + - ' to do man

'; - - const result = htmlLengthService.computeHtmlLength(htmlString, 'character'); + it( + 'should compute character count for strings with paragraph tag and' + + ' descendants text nodes', + () => { + const testCases = [ + { + input: + '

This is a brief exploration about conjugations' + + 'in Spanish.

', + expected: 57, + }, + { + input: '

This is a test.

', + expected: 15, + }, + { + input: '

This text is bolded. This is italic

', + expected: 35, + }, + { + input: '

Check out below

"Text is bolded"

', + expected: 32, + }, + { + input: '

🙂 Hello, how are you?

', + expected: 22, + }, + { + input: '

مر حبا كيف حالك؟

', + expected: 16, + }, + ]; + + for (const testCase of testCases) { + const result = htmlLengthService.computeHtmlLength( + testCase.input, + 'character' + ); + expect(result).toBe(testCase.expected); + } + } + ); - /* + it( + 'should compute character count of content with text and non-text ' + + '(1 math tag)', + () => { + const htmlString = + '

Hi this seems too good to be true but what' + + ' to do man

'; + + const result = htmlLengthService.computeHtmlLength( + htmlString, + 'character' + ); + + /* The paragraph "Hi this seems too good to be true but what to do man" contains 52 characters. The 'math' tag is counted as 1 character. Therefore, the total character count is 52 (paragraph) + 1 ('math' tag) = 53 characters. */ - expect(result).toBe(53); - }); - - it('should compute character count of content with both text and non-text' + - '(1 image tag)', () => { - const htmlString = '

naghiue abghy gjuh  

' + - ''; - - const result = htmlLengthService.computeHtmlLength(htmlString, 'character'); + expect(result).toBe(53); + } + ); - /* + it( + 'should compute character count of content with both text and non-text' + + '(1 image tag)', + () => { + const htmlString = + '

naghiue abghy gjuh  

' + + ''; + + const result = htmlLengthService.computeHtmlLength( + htmlString, + 'character' + ); + + /* "naghiue abghy gjuh  " is a paragraph with 18 characters. "Svg file for demo" is the alt text for an image, contributing 17 characters, and the image itself counts as 10 characters. Therefore, the total character count is 18 (paragraph) + 17 (alt text) + 10 (image count) = 45 characters. */ - expect(result).toBe(45); - }); - - it('should compute character count of content with text and non-text ' + - '(1 math tag and 1 image tag)', () => { - const htmlString = '

Hi this seems too good to be true but what' + - ' to do man

' + - ''; + expect(result).toBe(45); + } + ); - const result = htmlLengthService.computeHtmlLength(htmlString, 'character'); - /* + it( + 'should compute character count of content with text and non-text ' + + '(1 math tag and 1 image tag)', + () => { + const htmlString = + '

Hi this seems too good to be true but what' + + ' to do man

' + + ''; + + const result = htmlLengthService.computeHtmlLength( + htmlString, + 'character' + ); + /* "Hi this seems too good to be true but what to do man" is a paragraph with 52 characters. The 'math' tag is counted as 1 character. "Svg file for demo" is the alt text for an image, contributing 17 characters, and @@ -438,113 +490,130 @@ describe('Html Length Service', () => { count is 52 (paragraph) + 1 ('math' tag) + 17 (alt text) + 10 (image count) = 80 characters. */ - expect(result).toBe(80); - }); - + expect(result).toBe(80); + } + ); - it('should compute character count of content with text and non-text ' + - '(1 Collapsible tag and 1 tab tag)', () => { - const htmlString = '' + - '

Demo hint just to check

'; + it( + 'should compute character count of content with text and non-text ' + + '(1 Collapsible tag and 1 tab tag)', + () => { + const htmlString = + '' + + '

Demo hint just to check

'; - const result = htmlLengthService.computeHtmlLength(htmlString, 'character'); + const result = htmlLengthService.computeHtmlLength( + htmlString, + 'character' + ); - /* + /* The 'collapsible' and 'tab' tags each add a count of 1000 characters, totaling 2000 characters. "Demo hint just to check" is a paragraph with 23 characters. Therefore, the total character count is 23 (paragraph) + 2000 (tag counts) = 2023 characters. */ - expect(result).toBe(2023); - }); - - - it('should compute character count of content with text and non-text ' + - '(1 link tag and 1 concept card tag)', () => { - const htmlString = '

' + - '

' + - '

Demo hint just to check

' + - '

'; - - const result = htmlLengthService.computeHtmlLength(htmlString, 'character'); + expect(result).toBe(2023); + } + ); - /* + it( + 'should compute character count of content with text and non-text ' + + '(1 link tag and 1 concept card tag)', + () => { + const htmlString = + '

' + + '

' + + '

Demo hint just to check

' + + '

'; + + const result = htmlLengthService.computeHtmlLength( + htmlString, + 'character' + ); + + /* The 'skillreview' tag contains "Concept card" with 12 characters and the 'link' tag contains "oppia link" with 10 characters. "Demo hint just to check" is a paragraph with 23 characters. Therefore, the total character count is 12 ('skillreview' tag) + 10 ('link' tag) + 23 (paragraph) = 45 characters. */ - expect(result).toBe(45); - }); - - - it('should compute character count of content with text and all non-text' + - '(1 Collapsible, 1 Tabs, 1 Image, 1 Link, 1 Math, 1 SkillReview,' + - ' 1 Video)', () => { - const htmlString = ( - '' + - '

Demo hint just to check

' + - '' + - '' + - '

' + - '

' + - '

' + - '

' + - '' + - '

 

' + - '

done!

'); - - const result = htmlLengthService.computeHtmlLength(htmlString, 'character'); + expect(result).toBe(45); + } + ); - /* + it( + 'should compute character count of content with text and all non-text' + + '(1 Collapsible, 1 Tabs, 1 Image, 1 Link, 1 Math, 1 SkillReview,' + + ' 1 Video)', + () => { + const htmlString = + '' + + '

Demo hint just to check

' + + '' + + '' + + '

' + + '

' + + '

' + + '

' + + '' + + '

 

' + + '

done!

'; + + const result = htmlLengthService.computeHtmlLength( + htmlString, + 'character' + ); + + /* The 'collapsible' and 'tab' tags each add a count of 1000 characters, totaling 2000 characters. "Demo hint just to check" and "done!" are paragraphs with 23 and 5 characters respectively, adding up @@ -558,15 +627,17 @@ describe('Html Length Service', () => { + 22 (alt text) + 10 (image count) + 12 ('skillreview' tag) + 10 ('link' tag) = 2083 characters. */ - expect(result).toBe(2083); - }); + expect(result).toBe(2083); + } + ); it('should compute character count of content of ordered lists', () => { - const htmlString = '
    ' + - '
  1. This is the first item
  2. ' + - '
  3. This is second item
  4. ' + - '
  5. This is the third item
  6. ' + - '
'; + const htmlString = + '
    ' + + '
  1. This is the first item
  2. ' + + '
  3. This is second item
  4. ' + + '
  5. This is the third item
  6. ' + + '
'; const result = htmlLengthService.computeHtmlLength(htmlString, 'character'); @@ -574,11 +645,12 @@ describe('Html Length Service', () => { }); it('should compute character count of content of unordered lists', () => { - const htmlString = '
    ' + - '
  • This is the first item
  • ' + - '
  • This is second item
  • ' + - '
  • This is the third item
  • ' + - '
'; + const htmlString = + '
    ' + + '
  • This is the first item
  • ' + + '
  • This is second item
  • ' + + '
  • This is the third item
  • ' + + '
'; const result = htmlLengthService.computeHtmlLength(htmlString, 'character'); @@ -593,7 +665,8 @@ describe('Html Length Service', () => { }); it('should compute character count of text and normal string', () => { - const htmlString = '

naghiue abghy gjuh  

' + + const htmlString = + '

naghiue abghy gjuh  

' + 'Hello, how are you?' + '

naghiue abghy gjuh  

'; const result = htmlLengthService.computeHtmlLength(htmlString, 'character'); @@ -621,16 +694,21 @@ describe('Html Length Service', () => { }); describe('getLengthForNonTextNodes', () => { - it('should throw an error when unable to determine ' + - 'length for non-text node', () => { - const nonTextNode = ( - 'This is not a ' + - 'text node'); - const calculationType = 'character'; - expect(() => { - htmlLengthService.getLengthForNonTextNodes( - nonTextNode, calculationType); - }).toThrowError('Invalid non-text node: oppia-noninteractive-xyz'); - }); + it( + 'should throw an error when unable to determine ' + + 'length for non-text node', + () => { + const nonTextNode = + 'This is not a ' + + 'text node'; + const calculationType = 'character'; + expect(() => { + htmlLengthService.getLengthForNonTextNodes( + nonTextNode, + calculationType + ); + }).toThrowError('Invalid non-text node: oppia-noninteractive-xyz'); + } + ); }); }); diff --git a/core/templates/services/html-length.service.ts b/core/templates/services/html-length.service.ts index 16843e70d685..262581297642 100644 --- a/core/templates/services/html-length.service.ts +++ b/core/templates/services/html-length.service.ts @@ -20,18 +20,20 @@ export const CALCULATION_TYPE_WORD = 'word'; export const CALCULATION_TYPE_CHARACTER = 'character'; // eslint-disable-next-line max-len -const CUSTOM_TAG_REGEX = /]*>/g; +const CUSTOM_TAG_REGEX = + /]*>/g; type CalculationType = - typeof CALCULATION_TYPE_WORD | typeof CALCULATION_TYPE_CHARACTER; + | typeof CALCULATION_TYPE_WORD + | typeof CALCULATION_TYPE_CHARACTER; -import { Injectable, SecurityContext } from '@angular/core'; -import { LoggerService } from './contextual/logger.service'; -import { DomSanitizer } from '@angular/platform-browser'; -import { HtmlEscaperService } from './html-escaper.service'; +import {Injectable, SecurityContext} from '@angular/core'; +import {LoggerService} from './contextual/logger.service'; +import {DomSanitizer} from '@angular/platform-browser'; +import {HtmlEscaperService} from './html-escaper.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class HtmlLengthService { constructor( @@ -41,8 +43,8 @@ export class HtmlLengthService { ) {} /** - * The below tags constitutes for non text nodes. - */ + * The below tags constitutes for non text nodes. + */ nonTextTags = [ 'oppia-noninteractive-math', 'oppia-noninteractive-image', @@ -50,69 +52,79 @@ export class HtmlLengthService { 'oppia-noninteractive-collapsible', 'oppia-noninteractive-video', 'oppia-noninteractive-tabs', - 'oppia-noninteractive-skillreview']; + 'oppia-noninteractive-skillreview', + ]; /** - * This function calculates the length of a given HTML - * string. The length can be calculated in two ways, - * depending on the 'calculationType' parameter: - * 1. If 'calculationType' is 'word', the function will - * count the number of words in the HTML string. A - * word is typically defined as a sequence of - * characters separated by spaces. - * 2. If 'calculationType' is 'character', the function - * will count the number of characters in the HTML - * string. This includes all visible characters, - * punctuation, and whitespace. - * @param {string} htmlString - The HTML string for - * which the length is to be calculated. - * @param {CalculationType} calculationType - The type - * of calculation to be performed. It can be either - * 'word' or 'character'. - * @returns {number} The calculated length of the HTML - * string according to the specified 'calculationType'. - */ + * This function calculates the length of a given HTML + * string. The length can be calculated in two ways, + * depending on the 'calculationType' parameter: + * 1. If 'calculationType' is 'word', the function will + * count the number of words in the HTML string. A + * word is typically defined as a sequence of + * characters separated by spaces. + * 2. If 'calculationType' is 'character', the function + * will count the number of characters in the HTML + * string. This includes all visible characters, + * punctuation, and whitespace. + * @param {string} htmlString - The HTML string for + * which the length is to be calculated. + * @param {CalculationType} calculationType - The type + * of calculation to be performed. It can be either + * 'word' or 'character'. + * @returns {number} The calculated length of the HTML + * string according to the specified 'calculationType'. + */ computeHtmlLength( - htmlString: string, - calculationType: CalculationType): number { + htmlString: string, + calculationType: CalculationType + ): number { const sanitizedHtml = this.sanitizer.sanitize( - SecurityContext.HTML, htmlString) as string; + SecurityContext.HTML, + htmlString + ) as string; // Identify custom tags using regex on the original HTML string. const customTags = htmlString.match(CUSTOM_TAG_REGEX); let totalLength = this.calculateBaselineLength( - sanitizedHtml, calculationType); + sanitizedHtml, + calculationType + ); if (customTags) { for (const customTag of customTags) { totalLength += this.getLengthForNonTextNodes( - customTag, calculationType); + customTag, + calculationType + ); } } return totalLength; } /** - * This function calculates the baseline length of a - * sanitized HTML string. The length can be calculated - * in two ways, depending on the 'calculationType' - * parameter: - * 1. If 'calculationType' is 'word' - * 2. If 'calculationType' is 'character' - * - * @param {string} sanitizedHtml - The sanitized HTML - * string for which the length is to be calculated. - * It can also process normal strings. - * @param {CalculationType} calculationType - The type - * of calculation to be performed. It can be either - * 'word' or 'character'. - * @returns {number} The calculated length of the HTML - * string according to the specified 'calculationType'. - */ + * This function calculates the baseline length of a + * sanitized HTML string. The length can be calculated + * in two ways, depending on the 'calculationType' + * parameter: + * 1. If 'calculationType' is 'word' + * 2. If 'calculationType' is 'character' + * + * @param {string} sanitizedHtml - The sanitized HTML + * string for which the length is to be calculated. + * It can also process normal strings. + * @param {CalculationType} calculationType - The type + * of calculation to be performed. It can be either + * 'word' or 'character'. + * @returns {number} The calculated length of the HTML + * string according to the specified 'calculationType'. + */ calculateBaselineLength( - sanitizedHtml: string, calculationType: CalculationType): number { + sanitizedHtml: string, + calculationType: CalculationType + ): number { let domparser = new DOMParser(); let dom: Document; dom = domparser.parseFromString(sanitizedHtml, 'text/html'); @@ -122,43 +134,46 @@ export class HtmlLengthService { Array.from(dom.body.childNodes).forEach(node => { if (node.nodeType === Node.TEXT_NODE && node.nodeValue !== null) { totalLength += this.calculateTextLength( - node.nodeValue, calculationType); + node.nodeValue, + calculationType + ); } }); for (let tag of Array.from(dom.body.children)) { - /** - * Guarding against tag.textContent === null, which can - * arise in special cases as explained in the following reference: - * (https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent) - */ + /** + * Guarding against tag.textContent === null, which can + * arise in special cases as explained in the following reference: + * (https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent) + */ const textContent = tag.textContent || ''; - totalLength += this.calculateTextLength( - textContent, calculationType); + totalLength += this.calculateTextLength(textContent, calculationType); } return totalLength; } /** - * This function calculates the length of a given text content - * based on the specified calculation type. - * - * @param {string} textContent - The text content for which the - * length is to be calculated. This is typically a string of - * text that may include words, spaces, punctuation, and other - * characters. - * @param {CalculationType} calculationType - The method used to - * calculate the length. It can be either 'word' or 'character'. - * If 'word', the function counts the number of words in the - * text content. If 'character', it counts the number of - * characters. - * @returns {number} The length of the text content. This is - * calculated as the number of words or characters in the text - * content, depending on the calculation type. - */ + * This function calculates the length of a given text content + * based on the specified calculation type. + * + * @param {string} textContent - The text content for which the + * length is to be calculated. This is typically a string of + * text that may include words, spaces, punctuation, and other + * characters. + * @param {CalculationType} calculationType - The method used to + * calculate the length. It can be either 'word' or 'character'. + * If 'word', the function counts the number of words in the + * text content. If 'character', it counts the number of + * characters. + * @returns {number} The length of the text content. This is + * calculated as the number of words or characters in the text + * content, depending on the calculation type. + */ private calculateTextLength( - textContent: string, calculationType: CalculationType): number { + textContent: string, + calculationType: CalculationType + ): number { let trimmedTextContent = textContent.trim(); let totalLength = 0; if (calculationType === CALCULATION_TYPE_WORD && trimmedTextContent) { @@ -171,27 +186,29 @@ export class HtmlLengthService { } /** - * This function calculates the length of a given non-text - * node based on the specified calculation type. - * - * @param {string} nonTextNode - The non-text node for which - * the length is to be calculated. This is typically an HTML - * string that represents a non-text element in the DOM, - * such as an image or a video element. - * @param {CalculationType} calculationType - The method used - * to calculate the length. It can be either 'word' or - * 'character'. If 'word', the function counts the number - * of words in the non-text node. If 'character', it counts - * the number of characters. - * @returns {number} The length of the non-text node. This is - * calculated as the number of words or characters in the - * non-text node, depending on the calculation type. - */ + * This function calculates the length of a given non-text + * node based on the specified calculation type. + * + * @param {string} nonTextNode - The non-text node for which + * the length is to be calculated. This is typically an HTML + * string that represents a non-text element in the DOM, + * such as an image or a video element. + * @param {CalculationType} calculationType - The method used + * to calculate the length. It can be either 'word' or + * 'character'. If 'word', the function counts the number + * of words in the non-text node. If 'character', it counts + * the number of characters. + * @returns {number} The length of the non-text node. This is + * calculated as the number of words or characters in the + * non-text node, depending on the calculation type. + */ // TODO(#19729): Create RTE-component-specific logic // for calculating the lengths of RTE-components. getLengthForNonTextNodes( - nonTextNode: string, calculationType: CalculationType): number { + nonTextNode: string, + calculationType: CalculationType + ): number { let domparser = new DOMParser(); let dom: Document; dom = domparser.parseFromString(nonTextNode, 'text/html'); @@ -209,14 +226,16 @@ export class HtmlLengthService { case 'oppia-noninteractive-skillreview': { const textValueAttr = domTag.getAttribute('text-with-value') || ''; const textValue = this.htmlEscaperService.escapedJsonToObj( - textValueAttr) as string; + textValueAttr + ) as string; const length = this.calculateTextLength(textValue, calculationType); return length; } case 'oppia-noninteractive-image': { const altTextAttr = domTag.getAttribute('alt-with-value') || ''; const altTextValue = this.htmlEscaperService.escapedJsonToObj( - altTextAttr) as string; + altTextAttr + ) as string; const length = this.calculateTextLength(altTextValue, calculationType); return length + 10; } diff --git a/core/templates/services/i18n-language-code.service.spec.ts b/core/templates/services/i18n-language-code.service.spec.ts index 985c30f6e273..c4076f72c7d3 100644 --- a/core/templates/services/i18n-language-code.service.spec.ts +++ b/core/templates/services/i18n-language-code.service.spec.ts @@ -16,10 +16,13 @@ * @fileoverview Unit tests for I18nLanguageCodeService. */ -import { EventEmitter } from '@angular/core'; +import {EventEmitter} from '@angular/core'; -import { I18nLanguageCodeService, TranslationKeyType } from 'services/i18n-language-code.service'; -import { Subscription } from 'rxjs'; +import { + I18nLanguageCodeService, + TranslationKeyType, +} from 'services/i18n-language-code.service'; +import {Subscription} from 'rxjs'; describe('I18nLanguageCodeService', () => { const i18nLanguageCodeService = new I18nLanguageCodeService(); @@ -30,8 +33,10 @@ describe('I18nLanguageCodeService', () => { beforeEach(() => { testSubscriptions = new Subscription(); testSubscriptions.add( - i18nLanguageCodeService.onI18nLanguageCodeChange - .subscribe((code: string) => languageCode = code)); + i18nLanguageCodeService.onI18nLanguageCodeChange.subscribe( + (code: string) => (languageCode = code) + ) + ); }); afterEach(() => { @@ -72,69 +77,92 @@ describe('I18nLanguageCodeService', () => { it('should check whether hacky translation is available correctly', () => { // I18N_CLASSROOM_MATH_TITLE is present in constants file and hence // it is valid. - hackyTranslationIsAvailable = i18nLanguageCodeService - .isHackyTranslationAvailable('I18N_CLASSROOM_MATH_TITLE'); + hackyTranslationIsAvailable = + i18nLanguageCodeService.isHackyTranslationAvailable( + 'I18N_CLASSROOM_MATH_TITLE' + ); expect(hackyTranslationIsAvailable).toBe(true); // I18N_TOPIC_12345axa_TITLE is not present in constants file and hence // it is invalid. - hackyTranslationIsAvailable = i18nLanguageCodeService - .isHackyTranslationAvailable('I18N_TOPIC_12345axa_TITLE'); + hackyTranslationIsAvailable = + i18nLanguageCodeService.isHackyTranslationAvailable( + 'I18N_TOPIC_12345axa_TITLE' + ); expect(hackyTranslationIsAvailable).toBe(false); }); it('should get classroom translation key correctly', () => { - translationKey = i18nLanguageCodeService.getClassroomTranslationKey( - 'Math'); + translationKey = i18nLanguageCodeService.getClassroomTranslationKey('Math'); expect(translationKey).toBe('I18N_CLASSROOM_MATH_TITLE'); - translationKey = i18nLanguageCodeService.getClassroomTranslationKey( - 'Science'); + translationKey = + i18nLanguageCodeService.getClassroomTranslationKey('Science'); expect(translationKey).toBe('I18N_CLASSROOM_SCIENCE_TITLE'); }); it('should get topic and subtopic translation key correctly', () => { translationKey = i18nLanguageCodeService.getTopicTranslationKey( - 'abc1234', TranslationKeyType.TITLE); + 'abc1234', + TranslationKeyType.TITLE + ); expect(translationKey).toBe('I18N_TOPIC_abc1234_TITLE'); translationKey = i18nLanguageCodeService.getSubtopicTranslationKey( - 'abc1234', 'test-subtopic', TranslationKeyType.TITLE); + 'abc1234', + 'test-subtopic', + TranslationKeyType.TITLE + ); expect(translationKey).toBe('I18N_SUBTOPIC_abc1234_test-subtopic_TITLE'); translationKey = i18nLanguageCodeService.getTopicTranslationKey( - 'abc1234', TranslationKeyType.DESCRIPTION); + 'abc1234', + TranslationKeyType.DESCRIPTION + ); expect(translationKey).toBe('I18N_TOPIC_abc1234_DESCRIPTION'); translationKey = i18nLanguageCodeService.getSubtopicTranslationKey( - 'abc1234', 'test-subtopic', TranslationKeyType.DESCRIPTION); + 'abc1234', + 'test-subtopic', + TranslationKeyType.DESCRIPTION + ); expect(translationKey).toBe( - 'I18N_SUBTOPIC_abc1234_test-subtopic_DESCRIPTION'); + 'I18N_SUBTOPIC_abc1234_test-subtopic_DESCRIPTION' + ); }); it('should get story translation keys correctly', () => { translationKey = i18nLanguageCodeService.getStoryTranslationKey( - 'abc1234', TranslationKeyType.TITLE); + 'abc1234', + TranslationKeyType.TITLE + ); expect(translationKey).toBe('I18N_STORY_abc1234_TITLE'); translationKey = i18nLanguageCodeService.getStoryTranslationKey( - 'abc1234', TranslationKeyType.DESCRIPTION); + 'abc1234', + TranslationKeyType.DESCRIPTION + ); expect(translationKey).toBe('I18N_STORY_abc1234_DESCRIPTION'); }); it('should get exploration translation key correctly', () => { translationKey = i18nLanguageCodeService.getExplorationTranslationKey( - 'abc1234', TranslationKeyType.TITLE); + 'abc1234', + TranslationKeyType.TITLE + ); expect(translationKey).toBe('I18N_EXPLORATION_abc1234_TITLE'); translationKey = i18nLanguageCodeService.getExplorationTranslationKey( - 'abc1234', TranslationKeyType.DESCRIPTION); + 'abc1234', + TranslationKeyType.DESCRIPTION + ); expect(translationKey).toBe('I18N_EXPLORATION_abc1234_DESCRIPTION'); }); it('should get event emitter for loading of preferred language codes', () => { let mockPreferredLanguageCodesLoadedEventEmitter = new EventEmitter(); expect(i18nLanguageCodeService.onPreferredLanguageCodesLoaded).toEqual( - mockPreferredLanguageCodesLoadedEventEmitter); + mockPreferredLanguageCodesLoadedEventEmitter + ); }); }); diff --git a/core/templates/services/i18n-language-code.service.ts b/core/templates/services/i18n-language-code.service.ts index 38cef1bb8a21..702995f7274c 100644 --- a/core/templates/services/i18n-language-code.service.ts +++ b/core/templates/services/i18n-language-code.service.ts @@ -16,9 +16,9 @@ * @fileoverview Service for informing of the i18n language code changes. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable, EventEmitter } from '@angular/core'; -import { AppConstants } from 'app.constants'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable, EventEmitter} from '@angular/core'; +import {AppConstants} from 'app.constants'; /** * Used to define if the translation key is type title or desciption. @@ -36,7 +36,7 @@ export interface LanguageInfo { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class I18nLanguageCodeService { // TODO(#9154): Remove static when migration is complete. @@ -49,7 +49,7 @@ export class I18nLanguageCodeService { * complete. */ static prevLangCode: string = 'en'; - static languageCodeChangeEventEmitter = new EventEmitter (); + static languageCodeChangeEventEmitter = new EventEmitter(); static languageCode: string = AppConstants.DEFAULT_LANGUAGE_CODE; // TODO(#9154): Remove this variable when translation service is extended. /** @@ -60,15 +60,16 @@ export class I18nLanguageCodeService { private _HACKY_TRANSLATION_KEYS: readonly string[] = AppConstants.HACKY_TRANSLATION_KEYS; - private _preferredLanguageCodesLoadedEventEmitter = - new EventEmitter(); + private _preferredLanguageCodesLoadedEventEmitter = new EventEmitter< + string[] + >(); private supportedSiteLanguageCodes = Object.assign( {}, ...AppConstants.SUPPORTED_SITE_LANGUAGES.map( - (languageInfo: LanguageInfo) => ( - {[languageInfo.id]: languageInfo.direction} - ) + (languageInfo: LanguageInfo) => ({ + [languageInfo.id]: languageInfo.direction, + }) ) ); @@ -116,8 +117,7 @@ export class I18nLanguageCodeService { * @param {TranslationKeyType} keyType - either Title or Description. * @returns {string} - translation key for the topic name/description. */ - getTopicTranslationKey( - topicId: string, keyType: TranslationKeyType): string { + getTopicTranslationKey(topicId: string, keyType: TranslationKeyType): string { return `I18N_TOPIC_${topicId}_${keyType}`; } @@ -133,8 +133,10 @@ export class I18nLanguageCodeService { * @returns {string} - translation key for the subtopic name/description. */ getSubtopicTranslationKey( - topicId: string, subtopicUrlFragment: string, - keyType: TranslationKeyType): string { + topicId: string, + subtopicUrlFragment: string, + keyType: TranslationKeyType + ): string { return `I18N_SUBTOPIC_${topicId}_${subtopicUrlFragment}_${keyType}`; } @@ -147,8 +149,7 @@ export class I18nLanguageCodeService { * @param {TranslationKeyType} keyType - either Title or Description. * @returns {string} - translation key for the story name/description. */ - getStoryTranslationKey( - storyId: string, keyType: TranslationKeyType): string { + getStoryTranslationKey(storyId: string, keyType: TranslationKeyType): string { return `I18N_STORY_${storyId}_${keyType}`; } @@ -162,7 +163,9 @@ export class I18nLanguageCodeService { * @returns {string} - translation key for the exploration name/description. */ getExplorationTranslationKey( - explorationId: string, keyType: TranslationKeyType): string { + explorationId: string, + keyType: TranslationKeyType + ): string { return `I18N_EXPLORATION_${explorationId}_${keyType}`; } @@ -177,8 +180,7 @@ export class I18nLanguageCodeService { * in the language JSON files. */ isHackyTranslationAvailable(translationKey: string): boolean { - return ( - this._HACKY_TRANSLATION_KEYS.indexOf(translationKey) !== -1); + return this._HACKY_TRANSLATION_KEYS.indexOf(translationKey) !== -1; } get onI18nLanguageCodeChange(): EventEmitter { @@ -198,6 +200,9 @@ export class I18nLanguageCodeService { } } -angular.module('oppia').factory( - 'I18nLanguageCodeService', - downgradeInjectable(I18nLanguageCodeService)); +angular + .module('oppia') + .factory( + 'I18nLanguageCodeService', + downgradeInjectable(I18nLanguageCodeService) + ); diff --git a/core/templates/services/id-generation.service.spec.ts b/core/templates/services/id-generation.service.spec.ts index c62af7bafe01..064c05b19a57 100644 --- a/core/templates/services/id-generation.service.spec.ts +++ b/core/templates/services/id-generation.service.spec.ts @@ -16,7 +16,7 @@ * @fileoverview Unit tests for IdGenerationService. */ -import { IdGenerationService } from 'services/id-generation.service'; +import {IdGenerationService} from 'services/id-generation.service'; describe('IdGenerationService', () => { let idGenerationService: IdGenerationService; @@ -35,21 +35,27 @@ describe('IdGenerationService', () => { expect(id1).not.toEqual(id2); }); - it('should generate id with 10 digits when random string has length' + - ' greater than or equal to 10', function() { - // It returns a number that represents 10 digits string. - spyOn(Math, 'random').and.returnValue(0.5023019837490587); - var generatedId = idGenerationService.generateNewId(); - expect(generatedId.length).toBe(10); - }); - - it('should generate id with 10 digits when random string has length' + - ' less than 10', function() { - // It returns a number that represents 9 digits string. - spyOn(Math, 'random').and.returnValue(0.25275092369714336); - var generatedId = idGenerationService.generateNewId(); - expect(generatedId.length).toBe(10); - // 0 is inserted for generated id to be of length 10. - expect(generatedId.slice(-1)).toBe('0'); - }); + it( + 'should generate id with 10 digits when random string has length' + + ' greater than or equal to 10', + function () { + // It returns a number that represents 10 digits string. + spyOn(Math, 'random').and.returnValue(0.5023019837490587); + var generatedId = idGenerationService.generateNewId(); + expect(generatedId.length).toBe(10); + } + ); + + it( + 'should generate id with 10 digits when random string has length' + + ' less than 10', + function () { + // It returns a number that represents 9 digits string. + spyOn(Math, 'random').and.returnValue(0.25275092369714336); + var generatedId = idGenerationService.generateNewId(); + expect(generatedId.length).toBe(10); + // 0 is inserted for generated id to be of length 10. + expect(generatedId.slice(-1)).toBe('0'); + } + ); }); diff --git a/core/templates/services/id-generation.service.ts b/core/templates/services/id-generation.service.ts index fe47e394ff9e..921dbe846938 100644 --- a/core/templates/services/id-generation.service.ts +++ b/core/templates/services/id-generation.service.ts @@ -16,11 +16,11 @@ * @fileoverview Service for generating random IDs. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class IdGenerationService { generateNewId(): string { @@ -34,6 +34,6 @@ export class IdGenerationService { } } -angular.module('oppia').factory( - 'IdGenerationService', - downgradeInjectable(IdGenerationService)); +angular + .module('oppia') + .factory('IdGenerationService', downgradeInjectable(IdGenerationService)); diff --git a/core/templates/services/image-local-storage.service.spec.ts b/core/templates/services/image-local-storage.service.spec.ts index 419dfc68b457..5561c957cde8 100644 --- a/core/templates/services/image-local-storage.service.spec.ts +++ b/core/templates/services/image-local-storage.service.spec.ts @@ -16,11 +16,10 @@ * @fileoverview Unit test for ImageLocalStorageService. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TestBed } from '@angular/core/testing'; -import { AlertsService } from './alerts.service'; -import { ImageLocalStorageService } from './image-local-storage.service'; - +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TestBed} from '@angular/core/testing'; +import {AlertsService} from './alerts.service'; +import {ImageLocalStorageService} from './image-local-storage.service'; describe('ImageLocalStorageService', () => { let alertsService: AlertsService; @@ -30,7 +29,7 @@ describe('ImageLocalStorageService', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] + imports: [HttpClientTestingModule], }); }); @@ -44,21 +43,19 @@ describe('ImageLocalStorageService', () => { imageLocalStorageService.saveImage('filename 2', sampleImageData); imageLocalStorageService.saveImage('filename 3', sampleImageData); imageLocalStorageService.deleteImage('filename 2'); - expect( - imageLocalStorageService.getStoredImagesData().length).toEqual(2); + expect(imageLocalStorageService.getStoredImagesData().length).toEqual(2); imageLocalStorageService.flushStoredImagesData(); - expect( - imageLocalStorageService.getStoredImagesData().length).toEqual(0); + expect(imageLocalStorageService.getStoredImagesData().length).toEqual(0); }); it('should get raw image data correctly', () => { imageLocalStorageService.saveImage(imageFilename, sampleImageData); - expect( - imageLocalStorageService.getRawImageData(imageFilename)).toEqual( - sampleImageData); - expect( - imageLocalStorageService.getRawImageData('invalidFilename')).toEqual( - null); + expect(imageLocalStorageService.getRawImageData(imageFilename)).toEqual( + sampleImageData + ); + expect(imageLocalStorageService.getRawImageData('invalidFilename')).toEqual( + null + ); }); it('should return correctly check whether file exist in storage', () => { @@ -68,8 +65,8 @@ describe('ImageLocalStorageService', () => { }); it( - 'should show error message if number of stored images crosses ' + - 'limit', () => { + 'should show error message if number of stored images crosses ' + 'limit', + () => { for (let i = 0; i <= 50; i++) { imageLocalStorageService.saveImage('filename' + i, sampleImageData); } @@ -86,46 +83,56 @@ describe('ImageLocalStorageService', () => { expect(imageLocalStorageService.getThumbnailBgColor()).toEqual(bgColor); }); - it('should map image filenames to base64 string', async() => { - const sampleImageData = [{ - filename: 'image1.png', - imageBlob: new Blob(['image1'], {type: 'image/png'}) - }, { - filename: 'image2.png', - imageBlob: new Blob(['image2'], {type: 'image/png'}) - }]; - const imageFilenameTob64Mapping = ( + it('should map image filenames to base64 string', async () => { + const sampleImageData = [ + { + filename: 'image1.png', + imageBlob: new Blob(['image1'], {type: 'image/png'}), + }, + { + filename: 'image2.png', + imageBlob: new Blob(['image2'], {type: 'image/png'}), + }, + ]; + const imageFilenameTob64Mapping = await imageLocalStorageService.getFilenameToBase64MappingAsync( - sampleImageData)); - expect(Object.keys(imageFilenameTob64Mapping).sort()).toEqual( - ['image1.png', 'image2.png']); + sampleImageData + ); + expect(Object.keys(imageFilenameTob64Mapping).sort()).toEqual([ + 'image1.png', + 'image2.png', + ]); expect(imageFilenameTob64Mapping['image1.png']).toEqual('aW1hZ2Ux'); expect(imageFilenameTob64Mapping['image2.png']).toEqual('aW1hZ2Uy'); }); - it('should handle mapping scenario when image data is empty', async() => { - const imageFilenameTob64Mapping = ( - await imageLocalStorageService.getFilenameToBase64MappingAsync([])); + it('should handle mapping scenario when image data is empty', async () => { + const imageFilenameTob64Mapping = + await imageLocalStorageService.getFilenameToBase64MappingAsync([]); expect(imageFilenameTob64Mapping).toEqual({}); }); - it('should throw error if image blob is null', async() => { - const sampleImageData = [{ - filename: 'imageFilename1', - imageBlob: null - }]; - await expectAsync(( - imageLocalStorageService.getFilenameToBase64MappingAsync( - sampleImageData))).toBeRejectedWithError('No image data found'); + it('should throw error if image blob is null', async () => { + const sampleImageData = [ + { + filename: 'imageFilename1', + imageBlob: null, + }, + ]; + await expectAsync( + imageLocalStorageService.getFilenameToBase64MappingAsync(sampleImageData) + ).toBeRejectedWithError('No image data found'); }); - it('should throw error if prefix is invalid', async() => { - const sampleImageData = [{ - filename: 'imageFilename1', - imageBlob: new Blob(['data:random/xyz;base64,Blob1'], {type: 'image'}) - }]; - await expectAsync(( + it('should throw error if prefix is invalid', async () => { + const sampleImageData = [ + { + filename: 'imageFilename1', + imageBlob: new Blob(['data:random/xyz;base64,Blob1'], {type: 'image'}), + }, + ]; + await expectAsync( imageLocalStorageService.getFilenameToBase64MappingAsync(sampleImageData) - )).toBeRejectedWithError('No valid prefix found in data url'); + ).toBeRejectedWithError('No valid prefix found in data url'); }); }); diff --git a/core/templates/services/image-local-storage.service.ts b/core/templates/services/image-local-storage.service.ts index 0931deefe459..14379af71f6b 100644 --- a/core/templates/services/image-local-storage.service.ts +++ b/core/templates/services/image-local-storage.service.ts @@ -16,12 +16,12 @@ * @fileoverview Service for managing images in localStorage. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; -import { AlertsService } from 'services/alerts.service'; -import { ImageUploadHelperService } from 'services/image-upload-helper.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import {AlertsService} from 'services/alerts.service'; +import {ImageUploadHelperService} from 'services/image-upload-helper.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; export interface ImagesData { filename: string; @@ -31,7 +31,7 @@ export interface ImagesData { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ImageLocalStorageService { storedImageFilenames: string[] = []; @@ -39,7 +39,7 @@ export class ImageLocalStorageService { // minimum limit, for all browsers, per hostname, that can be stored in // sessionStorage and 100kB is the max size limit for uploaded images, hence // the limit below. - MAX_IMAGES_STORABLE: number = 5 * 1024 / 100; + MAX_IMAGES_STORABLE: number = (5 * 1024) / 100; // 'null' value here represents that either image is not present in local // storage or ImageData has been flushed. thumbnailBgColor: string | null = null; @@ -47,7 +47,8 @@ export class ImageLocalStorageService { constructor( private alertsService: AlertsService, private imageUploadHelperService: ImageUploadHelperService, - private windowRef: WindowRef) {} + private windowRef: WindowRef + ) {} // Function returns null if filename doesn't exist in local storage. getRawImageData(filename: string): string | null { @@ -66,7 +67,8 @@ export class ImageLocalStorageService { // local storage would no longer be used. this.alertsService.addInfoMessage( 'Image storage limit reached. More images can be added after ' + - 'creation.'); + 'creation.' + ); return; } this.windowRef.nativeWindow.sessionStorage.setItem(filename, rawImage); @@ -86,7 +88,9 @@ export class ImageLocalStorageService { filename: this.storedImageFilenames[idx], imageBlob: this.imageUploadHelperService.convertImageDataToImageFile( this.windowRef.nativeWindow.sessionStorage.getItem( - this.storedImageFilenames[idx])) + this.storedImageFilenames[idx] + ) + ), }); } return returnData; @@ -112,9 +116,9 @@ export class ImageLocalStorageService { } async getFilenameToBase64MappingAsync( - imagesData: ImagesData[] + imagesData: ImagesData[] ): Promise> { - let filesToBase64Mapping: { [key: string]: string } = {}; + let filesToBase64Mapping: {[key: string]: string} = {}; if (imagesData.length > 0) { for await (const obj of imagesData) { if (obj.imageBlob === null) { @@ -128,7 +132,7 @@ export class ImageLocalStorageService { } private async _blobtoBase64(blob: Blob): Promise { - return new Promise ((resolve, reject)=> { + return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { // Read the base64 data from result. @@ -149,5 +153,9 @@ export class ImageLocalStorageService { } } -angular.module('oppia').factory( - 'ImageLocalStorageService', downgradeInjectable(ImageLocalStorageService)); +angular + .module('oppia') + .factory( + 'ImageLocalStorageService', + downgradeInjectable(ImageLocalStorageService) + ); diff --git a/core/templates/services/image-upload-helper.service.spec.ts b/core/templates/services/image-upload-helper.service.spec.ts index 3198b43193c7..ac64e180dda5 100644 --- a/core/templates/services/image-upload-helper.service.spec.ts +++ b/core/templates/services/image-upload-helper.service.spec.ts @@ -16,73 +16,83 @@ * @fileoverview Unit test for imageUploadHelperService. */ -import { HttpClientTestingModule } from - '@angular/common/http/testing'; -import { TestBed } from '@angular/core/testing'; -import { ImageUploadHelperService } from './image-upload-helper.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TestBed} from '@angular/core/testing'; +import {ImageUploadHelperService} from './image-upload-helper.service'; describe('imageUploadHelperService', () => { let imageUploadHelperService: ImageUploadHelperService; beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] + imports: [HttpClientTestingModule], }); - imageUploadHelperService = - TestBed.inject(ImageUploadHelperService); + imageUploadHelperService = TestBed.inject(ImageUploadHelperService); }); it('should convert image data to image file', () => { - const imageFile = ( - imageUploadHelperService.convertImageDataToImageFile( - 'data:image/png;base64,JUMzJTg3JTJD')); + const imageFile = imageUploadHelperService.convertImageDataToImageFile( + 'data:image/png;base64,JUMzJTg3JTJD' + ); expect(imageFile instanceof Blob).toBe(true); }); - it('should return null for non-image data', function() { - const imageFile = ( - imageUploadHelperService.convertImageDataToImageFile( - 'data:text/plain;base64,JUMzJTg3JTJD')); + it('should return null for non-image data', function () { + const imageFile = imageUploadHelperService.convertImageDataToImageFile( + 'data:text/plain;base64,JUMzJTg3JTJD' + ); expect(imageFile).toEqual(null); }); - it('should generate a filename for a math SVG', function() { + it('should generate a filename for a math SVG', function () { const height = '1d345'; const width = '2d455'; const verticalPadding = '0d123'; - const generatedFilename = ( + const generatedFilename = imageUploadHelperService.generateMathExpressionImageFilename( - height, width, verticalPadding)); - expect(generatedFilename.endsWith( - '_height_1d345_width_2d455_vertical_0d123.svg')).toBe(true); + height, + width, + verticalPadding + ); + expect( + generatedFilename.endsWith('_height_1d345_width_2d455_vertical_0d123.svg') + ).toBe(true); }); - it('should throw error for an invalid filename', function() { + it('should throw error for an invalid filename', function () { const height = 'height'; const width = '2d455'; const verticalPadding = '0d123'; - expect(() => imageUploadHelperService.generateMathExpressionImageFilename( - height, width, verticalPadding)) - .toThrowError('The Math SVG filename format is invalid.'); + expect(() => + imageUploadHelperService.generateMathExpressionImageFilename( + height, + width, + verticalPadding + ) + ).toThrowError('The Math SVG filename format is invalid.'); }); - it('should generate a filename for a normal image', function() { + it('should generate a filename for a normal image', function () { const height = 720; const width = 180; const format = 'png'; - const generatedFilename = ( - imageUploadHelperService.generateImageFilename(height, width, format)); + const generatedFilename = imageUploadHelperService.generateImageFilename( + height, + width, + format + ); expect(generatedFilename.endsWith('_height_720_width_180.png')).toBe(true); }); - it('should get trusted resource Url for thumbnail filename', function() { + it('should get trusted resource Url for thumbnail filename', function () { const imageFileName = 'image.svg'; const entityType = 'logo'; const entityId = 'id'; - const trustedResourceUrl = ( + const trustedResourceUrl = imageUploadHelperService.getTrustedResourceUrlForThumbnailFilename( - imageFileName, entityType, entityId - ) - ); + imageFileName, + entityType, + entityId + ); expect(String(trustedResourceUrl)).toEqual( '/assetsdevhandler/logo/id/assets/thumbnail/image.svg' ); diff --git a/core/templates/services/image-upload-helper.service.ts b/core/templates/services/image-upload-helper.service.ts index e80e1f82f0cd..74b3b84a0993 100644 --- a/core/templates/services/image-upload-helper.service.ts +++ b/core/templates/services/image-upload-helper.service.ts @@ -16,16 +16,16 @@ * @fileoverview Image upload helper service. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; -import { SvgSanitizerService } from './svg-sanitizer.service'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; +import {SvgSanitizerService} from './svg-sanitizer.service'; -import { AppConstants } from 'app.constants'; +import {AppConstants} from 'app.constants'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ImageUploadHelperService { constructor( @@ -35,13 +35,17 @@ export class ImageUploadHelperService { private _generateDateTimeStringForFilename(): string { let date = new Date(); - return date.getFullYear() + + return ( + date.getFullYear() + ('0' + (date.getMonth() + 1)).slice(-2) + - ('0' + date.getDate()).slice(-2) + '_' + + ('0' + date.getDate()).slice(-2) + + '_' + ('0' + date.getHours()).slice(-2) + ('0' + date.getMinutes()).slice(-2) + - ('0' + date.getSeconds()).slice(-2) + '_' + - Math.random().toString(36).substr(2, 10); + ('0' + date.getSeconds()).slice(-2) + + '_' + + Math.random().toString(36).substr(2, 10) + ); } // Image file returned will be null when blob is not of type @@ -63,9 +67,8 @@ export class ImageUploadHelperService { ia[i] = byteString.charCodeAt(i); } - let blob = new Blob([ia], { type: mime }); - if (blob.type.match('image') && - blob.size > 0) { + let blob = new Blob([ia], {type: mime}); + if (blob.type.match('image') && blob.size > 0) { return blob; } } @@ -74,42 +77,64 @@ export class ImageUploadHelperService { } getTrustedResourceUrlForThumbnailFilename( - imageFileName: string, entityType: string, entityId: string): string { + imageFileName: string, + entityType: string, + entityId: string + ): string { let encodedFilepath = window.encodeURIComponent(imageFileName); return this.assetsBackendApiService.getThumbnailUrlForPreview( - entityType, entityId, encodedFilepath); + entityType, + entityId, + encodedFilepath + ); } generateImageFilename( - height: number, width: number, extension: string): string { - return 'img_' + + height: number, + width: number, + extension: string + ): string { + return ( + 'img_' + this._generateDateTimeStringForFilename() + - '_height_' + height + - '_width_' + width + - '.' + extension; + '_height_' + + height + + '_width_' + + width + + '.' + + extension + ); } generateMathExpressionImageFilename( - height: string, width: string, verticalPadding: string): string { - let filename = ( + height: string, + width: string, + verticalPadding: string + ): string { + let filename = 'mathImg_' + - this._generateDateTimeStringForFilename() + - '_height_' + height + - '_width_' + width + - '_vertical_' + verticalPadding + - '.' + 'svg' - ); + this._generateDateTimeStringForFilename() + + '_height_' + + height + + '_width_' + + width + + '_vertical_' + + verticalPadding + + '.' + + 'svg'; let filenameRegexString = AppConstants.MATH_SVG_FILENAME_REGEX; let filenameRegex = RegExp(filenameRegexString, 'g'); if (filenameRegex.exec(filename)) { return filename; } else { - throw new Error( - 'The Math SVG filename format is invalid.'); + throw new Error('The Math SVG filename format is invalid.'); } } } -angular.module('oppia').factory( - 'ImageUploadHelperService', - downgradeInjectable(ImageUploadHelperService)); +angular + .module('oppia') + .factory( + 'ImageUploadHelperService', + downgradeInjectable(ImageUploadHelperService) + ); diff --git a/core/templates/services/improvements.service.spec.ts b/core/templates/services/improvements.service.spec.ts index 164b46737184..75999f7e9bd3 100644 --- a/core/templates/services/improvements.service.spec.ts +++ b/core/templates/services/improvements.service.spec.ts @@ -16,13 +16,11 @@ * @fileoverview Unit tests for improvements service. */ -import { TestBed } from '@angular/core/testing'; - -import { CamelCaseToHyphensPipe } from - 'filters/string-utility-filters/camel-case-to-hyphens.pipe'; -import { ImprovementsService } from 'services/improvements.service'; -import { StateObjectFactory } from 'domain/state/StateObjectFactory'; +import {TestBed} from '@angular/core/testing'; +import {CamelCaseToHyphensPipe} from 'filters/string-utility-filters/camel-case-to-hyphens.pipe'; +import {ImprovementsService} from 'services/improvements.service'; +import {StateObjectFactory} from 'domain/state/StateObjectFactory'; describe('ImprovementsService', () => { let improvementsService: ImprovementsService; @@ -30,7 +28,7 @@ describe('ImprovementsService', () => { beforeEach(() => { TestBed.configureTestingModule({ - providers: [CamelCaseToHyphensPipe] + providers: [CamelCaseToHyphensPipe], }); improvementsService = new ImprovementsService(); @@ -43,23 +41,23 @@ describe('ImprovementsService', () => { classifier_model_id: null, content: { html: '', - content_id: 'content' + content_id: 'content', }, interaction: { id: 'TextInput', customization_args: { rows: { - value: 1 + value: 1, }, placeholder: { value: { unicode_str: 'Type your answer here.', - content_id: '' - } + content_id: '', + }, }, catchMisspellings: { - value: false - } + value: false, + }, }, answer_groups: [], default_outcome: { @@ -67,35 +65,38 @@ describe('ImprovementsService', () => { dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: '' + html: '', }, labelled_as_correct: false, param_changes: [], refresher_exploration_id: null, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, confirmed_unclassified_answers: [], hints: [], - solution: null + solution: null, }, linked_skill_id: null, param_changes: [], recorded_voiceovers: { voiceovers_mapping: { content: {}, - default_outcome: {} - } + default_outcome: {}, + }, }, solicit_answer_details: false, - card_is_checkpoint: false + card_is_checkpoint: false, }; let mockState = stateObjectFactory.createFromBackendDict( - 'stateName', mockStateBackendDict); + 'stateName', + mockStateBackendDict + ); expect( - improvementsService - .isStateForcedToResolveOutstandingUnaddressedAnswers(mockState) + improvementsService.isStateForcedToResolveOutstandingUnaddressedAnswers( + mockState + ) ).toBe(true); }); @@ -104,18 +105,20 @@ describe('ImprovementsService', () => { classifier_model_id: null, content: { html: '', - content_id: 'content' + content_id: 'content', }, interaction: { id: 'FractionInput', customization_args: { - requireSimplestForm: { value: false }, - allowImproperFraction: { value: true }, - allowNonzeroIntegerPart: { value: true }, - customPlaceholder: { value: { - content_id: '', - unicode_str: '' - } }, + requireSimplestForm: {value: false}, + allowImproperFraction: {value: true}, + allowNonzeroIntegerPart: {value: true}, + customPlaceholder: { + value: { + content_id: '', + unicode_str: '', + }, + }, }, answer_groups: [], default_outcome: { @@ -123,35 +126,38 @@ describe('ImprovementsService', () => { dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: '' + html: '', }, labelled_as_correct: false, param_changes: [], refresher_exploration_id: null, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, confirmed_unclassified_answers: [], hints: [], - solution: null + solution: null, }, linked_skill_id: null, param_changes: [], recorded_voiceovers: { voiceovers_mapping: { content: {}, - default_outcome: {} - } + default_outcome: {}, + }, }, solicit_answer_details: false, - card_is_checkpoint: false + card_is_checkpoint: false, }; let mockState = stateObjectFactory.createFromBackendDict( - 'stateName', mockStateBackendDict); + 'stateName', + mockStateBackendDict + ); expect( - improvementsService - .isStateForcedToResolveOutstandingUnaddressedAnswers(mockState) + improvementsService.isStateForcedToResolveOutstandingUnaddressedAnswers( + mockState + ) ).toBe(false); }); @@ -160,18 +166,20 @@ describe('ImprovementsService', () => { classifier_model_id: null, content: { html: '', - content_id: 'content' + content_id: 'content', }, interaction: { id: null, customization_args: { - requireSimplestForm: { value: false }, - allowImproperFraction: { value: true }, - allowNonzeroIntegerPart: { value: true }, - customPlaceholder: { value: { - content_id: '', - unicode_str: '' - } }, + requireSimplestForm: {value: false}, + allowImproperFraction: {value: true}, + allowNonzeroIntegerPart: {value: true}, + customPlaceholder: { + value: { + content_id: '', + unicode_str: '', + }, + }, }, answer_groups: [], default_outcome: { @@ -179,34 +187,37 @@ describe('ImprovementsService', () => { dest_if_really_stuck: null, feedback: { content_id: 'default_outcome', - html: '' + html: '', }, labelled_as_correct: false, param_changes: [], refresher_exploration_id: null, - missing_prerequisite_skill_id: null + missing_prerequisite_skill_id: null, }, confirmed_unclassified_answers: [], hints: [], - solution: null + solution: null, }, linked_skill_id: null, param_changes: [], recorded_voiceovers: { voiceovers_mapping: { content: {}, - default_outcome: {} - } + default_outcome: {}, + }, }, solicit_answer_details: false, - card_is_checkpoint: false + card_is_checkpoint: false, }; let mockState = stateObjectFactory.createFromBackendDict( - 'stateName', mockStateBackendDict); + 'stateName', + mockStateBackendDict + ); expect( - improvementsService - .isStateForcedToResolveOutstandingUnaddressedAnswers(mockState) + improvementsService.isStateForcedToResolveOutstandingUnaddressedAnswers( + mockState + ) ).toBeFalse(); }); }); diff --git a/core/templates/services/improvements.service.ts b/core/templates/services/improvements.service.ts index 6e8218243962..2a81fc5351b4 100644 --- a/core/templates/services/improvements.service.ts +++ b/core/templates/services/improvements.service.ts @@ -17,25 +17,29 @@ * states based on statistics. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; -import { State } from 'domain/state/StateObjectFactory'; +import {State} from 'domain/state/StateObjectFactory'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ImprovementsService { INTERACTION_IDS_REQUIRED_TO_BE_RESOLVED = ['TextInput']; isStateForcedToResolveOutstandingUnaddressedAnswers(state: State): boolean { - if (!state || (state.interaction.id === null)) { + if (!state || state.interaction.id === null) { return false; } - return this.INTERACTION_IDS_REQUIRED_TO_BE_RESOLVED.indexOf( - state.interaction.id) !== -1; + return ( + this.INTERACTION_IDS_REQUIRED_TO_BE_RESOLVED.indexOf( + state.interaction.id + ) !== -1 + ); } } -angular.module('oppia').factory( - 'ImprovementsService', downgradeInjectable(ImprovementsService)); +angular + .module('oppia') + .factory('ImprovementsService', downgradeInjectable(ImprovementsService)); diff --git a/core/templates/services/insert-script.service.spec.ts b/core/templates/services/insert-script.service.spec.ts index 728ccf8cd3ce..7643cc3a9aa2 100644 --- a/core/templates/services/insert-script.service.spec.ts +++ b/core/templates/services/insert-script.service.spec.ts @@ -15,9 +15,12 @@ /** * @fileoverview test for insert script service */ -import { Renderer2 } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; -import { InsertScriptService, KNOWN_SCRIPTS } from 'services/insert-script.service'; +import {Renderer2} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; +import { + InsertScriptService, + KNOWN_SCRIPTS, +} from 'services/insert-script.service'; class MockRenderer { appendChild() { @@ -29,10 +32,13 @@ describe('InsertScriptService', () => { let insertScriptService: InsertScriptService; beforeEach(() => { TestBed.configureTestingModule({ - providers: [{ - provide: Renderer2, - useValue: MockRenderer, - }]}); + providers: [ + { + provide: Renderer2, + useValue: MockRenderer, + }, + ], + }); insertScriptService = TestBed.get(InsertScriptService); }); diff --git a/core/templates/services/insert-script.service.ts b/core/templates/services/insert-script.service.ts index 0f72b001bc0f..0bf0e1089dea 100644 --- a/core/templates/services/insert-script.service.ts +++ b/core/templates/services/insert-script.service.ts @@ -16,8 +16,8 @@ * @fileoverview Service to help inserting script element into html page. */ -import { Injectable, Renderer2, RendererFactory2 } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {Injectable, Renderer2, RendererFactory2} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; export enum KNOWN_SCRIPTS { DONORBOX = 'DONORBOX', @@ -25,7 +25,7 @@ export enum KNOWN_SCRIPTS { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class InsertScriptService { private loadedScripts: Set = new Set(); @@ -60,5 +60,6 @@ export class InsertScriptService { } } -angular.module('oppia').factory( - 'InsertScriptService', downgradeInjectable(InsertScriptService)); +angular + .module('oppia') + .factory('InsertScriptService', downgradeInjectable(InsertScriptService)); diff --git a/core/templates/services/interaction-rules-registry.service.spec.ts b/core/templates/services/interaction-rules-registry.service.spec.ts index 6ab2d87df491..7c6f489cea8b 100644 --- a/core/templates/services/interaction-rules-registry.service.spec.ts +++ b/core/templates/services/interaction-rules-registry.service.spec.ts @@ -16,69 +16,60 @@ * @fileoverview Unit tests for interaction rules registry service. */ -import { TestBed } from '@angular/core/testing'; +import {TestBed} from '@angular/core/testing'; -import { AlgebraicExpressionInputRulesService } from +import { + AlgebraicExpressionInputRulesService, // eslint-disable-next-line max-len - 'interactions/AlgebraicExpressionInput/directives/algebraic-expression-input-rules.service'; -import { CodeReplRulesService } from - 'interactions/CodeRepl/directives/code-repl-rules.service'; -import { ContinueRulesService } from - 'interactions/Continue/directives/continue-rules.service'; -import { DragAndDropSortInputRulesService } from +} from 'interactions/AlgebraicExpressionInput/directives/algebraic-expression-input-rules.service'; +import {CodeReplRulesService} from 'interactions/CodeRepl/directives/code-repl-rules.service'; +import {ContinueRulesService} from 'interactions/Continue/directives/continue-rules.service'; +import { + DragAndDropSortInputRulesService, // eslint-disable-next-line max-len - 'interactions/DragAndDropSortInput/directives/drag-and-drop-sort-input-rules.service'; -import { EndExplorationRulesService } from - 'interactions/EndExploration/directives/end-exploration-rules.service'; -import { FractionInputRulesService } from - 'interactions/FractionInput/directives/fraction-input-rules.service'; -import { GraphInputRulesService } from - 'interactions/GraphInput/directives/graph-input-rules.service'; -import { ImageClickInputRulesService } from - 'interactions/ImageClickInput/directives/image-click-input-rules.service'; -import { InteractionRulesRegistryService } from - 'services/interaction-rules-registry.service'; -import { InteractionSpecsConstants } from - 'pages/interaction-specs.constants'; -import { InteractiveMapRulesService } from - 'interactions/InteractiveMap/directives/interactive-map-rules.service'; -import { ItemSelectionInputRulesService } from +} from 'interactions/DragAndDropSortInput/directives/drag-and-drop-sort-input-rules.service'; +import {EndExplorationRulesService} from 'interactions/EndExploration/directives/end-exploration-rules.service'; +import {FractionInputRulesService} from 'interactions/FractionInput/directives/fraction-input-rules.service'; +import {GraphInputRulesService} from 'interactions/GraphInput/directives/graph-input-rules.service'; +import {ImageClickInputRulesService} from 'interactions/ImageClickInput/directives/image-click-input-rules.service'; +import {InteractionRulesRegistryService} from 'services/interaction-rules-registry.service'; +import {InteractionSpecsConstants} from 'pages/interaction-specs.constants'; +import {InteractiveMapRulesService} from 'interactions/InteractiveMap/directives/interactive-map-rules.service'; +import { + ItemSelectionInputRulesService, // eslint-disable-next-line max-len - 'interactions/ItemSelectionInput/directives/item-selection-input-rules.service'; -import { MathEquationInputRulesService } from +} from 'interactions/ItemSelectionInput/directives/item-selection-input-rules.service'; +import { + MathEquationInputRulesService, // eslint-disable-next-line max-len - 'interactions/MathEquationInput/directives/math-equation-input-rules.service'; -import { MultipleChoiceInputRulesService } from +} from 'interactions/MathEquationInput/directives/math-equation-input-rules.service'; +import { + MultipleChoiceInputRulesService, // eslint-disable-next-line max-len - 'interactions/MultipleChoiceInput/directives/multiple-choice-input-rules.service'; -import { MusicNotesInputRulesService } from - 'interactions/MusicNotesInput/directives/music-notes-input-rules.service'; -import { NormalizeWhitespacePipe } from - 'filters/string-utility-filters/normalize-whitespace.pipe'; -import { NormalizeWhitespacePunctuationAndCasePipe } from +} from 'interactions/MultipleChoiceInput/directives/multiple-choice-input-rules.service'; +import {MusicNotesInputRulesService} from 'interactions/MusicNotesInput/directives/music-notes-input-rules.service'; +import {NormalizeWhitespacePipe} from 'filters/string-utility-filters/normalize-whitespace.pipe'; +import { + NormalizeWhitespacePunctuationAndCasePipe, // eslint-disable-next-line max-len - 'filters/string-utility-filters/normalize-whitespace-punctuation-and-case.pipe'; -import { NumberWithUnitsRulesService } from - 'interactions/NumberWithUnits/directives/number-with-units-rules.service'; -import { NumericExpressionInputRulesService } from +} from 'filters/string-utility-filters/normalize-whitespace-punctuation-and-case.pipe'; +import {NumberWithUnitsRulesService} from 'interactions/NumberWithUnits/directives/number-with-units-rules.service'; +import { + NumericExpressionInputRulesService, // eslint-disable-next-line max-len - 'interactions/NumericExpressionInput/directives/numeric-expression-input-rules.service'; -import { NumericInputRulesService } from - 'interactions/NumericInput/directives/numeric-input-rules.service'; -import { PencilCodeEditorRulesService } from - 'interactions/PencilCodeEditor/directives/pencil-code-editor-rules.service'; -import { RatioExpressionInputRulesService } from +} from 'interactions/NumericExpressionInput/directives/numeric-expression-input-rules.service'; +import {NumericInputRulesService} from 'interactions/NumericInput/directives/numeric-input-rules.service'; +import {PencilCodeEditorRulesService} from 'interactions/PencilCodeEditor/directives/pencil-code-editor-rules.service'; +import { + RatioExpressionInputRulesService, // eslint-disable-next-line max-len - 'interactions/RatioExpressionInput/directives/ratio-expression-input-rules.service'; -import { SetInputRulesService } from - 'interactions/SetInput/directives/set-input-rules.service'; -import { TextInputRulesService } from - 'interactions/TextInput/directives/text-input-rules.service'; +} from 'interactions/RatioExpressionInput/directives/ratio-expression-input-rules.service'; +import {SetInputRulesService} from 'interactions/SetInput/directives/set-input-rules.service'; +import {TextInputRulesService} from 'interactions/TextInput/directives/text-input-rules.service'; describe('Interaction Rules Registry Service', () => { let interactionRulesRegistryService: InteractionRulesRegistryService; - let algebraicExpressionInputRulesService: - AlgebraicExpressionInputRulesService; + let algebraicExpressionInputRulesService: AlgebraicExpressionInputRulesService; let codeReplRulesService: CodeReplRulesService; let continueRulesService: ContinueRulesService; let dragAndDropSortInputRulesService: DragAndDropSortInputRulesService; @@ -107,191 +98,207 @@ describe('Interaction Rules Registry Service', () => { ], }); - interactionRulesRegistryService = ( - TestBed.get(InteractionRulesRegistryService)); + interactionRulesRegistryService = TestBed.get( + InteractionRulesRegistryService + ); - algebraicExpressionInputRulesService = ( - TestBed.get(AlgebraicExpressionInputRulesService)); + algebraicExpressionInputRulesService = TestBed.get( + AlgebraicExpressionInputRulesService + ); codeReplRulesService = TestBed.get(CodeReplRulesService); continueRulesService = TestBed.get(ContinueRulesService); - dragAndDropSortInputRulesService = ( - TestBed.get(DragAndDropSortInputRulesService)); + dragAndDropSortInputRulesService = TestBed.get( + DragAndDropSortInputRulesService + ); endExplorationRulesService = TestBed.get(EndExplorationRulesService); fractionInputRulesService = TestBed.get(FractionInputRulesService); graphInputRulesService = TestBed.get(GraphInputRulesService); imageClickInputRulesService = TestBed.get(ImageClickInputRulesService); interactiveMapRulesService = TestBed.get(InteractiveMapRulesService); - itemSelectionInputRulesService = ( - TestBed.get(ItemSelectionInputRulesService)); - mathEquationInputRulesService = ( - TestBed.get(MathEquationInputRulesService)); - multipleChoiceInputRulesService = ( - TestBed.get(MultipleChoiceInputRulesService)); + itemSelectionInputRulesService = TestBed.get( + ItemSelectionInputRulesService + ); + mathEquationInputRulesService = TestBed.get(MathEquationInputRulesService); + multipleChoiceInputRulesService = TestBed.get( + MultipleChoiceInputRulesService + ); musicNotesInputRulesService = TestBed.get(MusicNotesInputRulesService); numberWithUnitsRulesService = TestBed.get(NumberWithUnitsRulesService); - numericExpressionInputRulesService = ( - TestBed.get(NumericExpressionInputRulesService)); + numericExpressionInputRulesService = TestBed.get( + NumericExpressionInputRulesService + ); numericInputRulesService = TestBed.get(NumericInputRulesService); - pencilCodeEditorRulesService = ( - TestBed.get(PencilCodeEditorRulesService)); - ratioExpressionInputRulesService = ( - TestBed.get(RatioExpressionInputRulesService)); + pencilCodeEditorRulesService = TestBed.get(PencilCodeEditorRulesService); + ratioExpressionInputRulesService = TestBed.get( + RatioExpressionInputRulesService + ); setInputRulesService = TestBed.get(SetInputRulesService); textInputRulesService = TestBed.get(TextInputRulesService); }); it('should throw an error for falsey interaction ids', () => { - expect( - () => interactionRulesRegistryService.getRulesServiceByInteractionId('') + expect(() => + interactionRulesRegistryService.getRulesServiceByInteractionId('') ).toThrowError('Interaction ID must not be empty'); }); it('should throw an error for an interaction id that does not exist', () => { - expect( - () => interactionRulesRegistryService.getRulesServiceByInteractionId( - 'FakeInput') + expect(() => + interactionRulesRegistryService.getRulesServiceByInteractionId( + 'FakeInput' + ) ).toThrowError('Unknown interaction ID: FakeInput'); }); it('should return a non-null service for each interaction spec', () => { for (const interactionId in InteractionSpecsConstants.INTERACTION_SPECS) { - expect( - () => interactionRulesRegistryService.getRulesServiceByInteractionId( - interactionId) + expect(() => + interactionRulesRegistryService.getRulesServiceByInteractionId( + interactionId + ) ).not.toThrowError(); } }); - it('should return the correct rules service for AlgebraicExpressionInput', - () => { - expect(interactionRulesRegistryService.getRulesServiceByInteractionId( - 'AlgebraicExpressionInput')).toBe( - algebraicExpressionInputRulesService); - } - ); + it('should return the correct rules service for AlgebraicExpressionInput', () => { + expect( + interactionRulesRegistryService.getRulesServiceByInteractionId( + 'AlgebraicExpressionInput' + ) + ).toBe(algebraicExpressionInputRulesService); + }); it('should return the correct rules service for CodeRepl', () => { expect( - interactionRulesRegistryService - .getRulesServiceByInteractionId('CodeRepl') + interactionRulesRegistryService.getRulesServiceByInteractionId('CodeRepl') ).toBe(codeReplRulesService); }); it('should return the correct rules service for Continue', () => { expect( - interactionRulesRegistryService - .getRulesServiceByInteractionId('Continue') + interactionRulesRegistryService.getRulesServiceByInteractionId('Continue') ).toBe(continueRulesService); }); it('should return the correct rules service for DragAndDropSortInput', () => { expect( - interactionRulesRegistryService - .getRulesServiceByInteractionId('DragAndDropSortInput') + interactionRulesRegistryService.getRulesServiceByInteractionId( + 'DragAndDropSortInput' + ) ).toBe(dragAndDropSortInputRulesService); }); it('should return the correct rules service for EndExploration', () => { expect( - interactionRulesRegistryService - .getRulesServiceByInteractionId('EndExploration') + interactionRulesRegistryService.getRulesServiceByInteractionId( + 'EndExploration' + ) ).toBe(endExplorationRulesService); }); it('should return the correct rules service for FractionInput', () => { expect( - interactionRulesRegistryService - .getRulesServiceByInteractionId('FractionInput') + interactionRulesRegistryService.getRulesServiceByInteractionId( + 'FractionInput' + ) ).toBe(fractionInputRulesService); }); it('should return the correct rules service for GraphInput', () => { expect( - interactionRulesRegistryService - .getRulesServiceByInteractionId('GraphInput') + interactionRulesRegistryService.getRulesServiceByInteractionId( + 'GraphInput' + ) ).toBe(graphInputRulesService); }); it('should return the correct rules service for ImageClickInput', () => { expect( - interactionRulesRegistryService - .getRulesServiceByInteractionId('ImageClickInput') + interactionRulesRegistryService.getRulesServiceByInteractionId( + 'ImageClickInput' + ) ).toBe(imageClickInputRulesService); }); it('should return the correct rules service for InteractiveMap', () => { expect( - interactionRulesRegistryService - .getRulesServiceByInteractionId('InteractiveMap') + interactionRulesRegistryService.getRulesServiceByInteractionId( + 'InteractiveMap' + ) ).toBe(interactiveMapRulesService); }); it('should return the correct rules service for ItemSelectionInput', () => { expect( - interactionRulesRegistryService - .getRulesServiceByInteractionId('ItemSelectionInput') + interactionRulesRegistryService.getRulesServiceByInteractionId( + 'ItemSelectionInput' + ) ).toBe(itemSelectionInputRulesService); }); it('should return the correct rules service for MathEquationInput', () => { expect( - interactionRulesRegistryService - .getRulesServiceByInteractionId('MathEquationInput') + interactionRulesRegistryService.getRulesServiceByInteractionId( + 'MathEquationInput' + ) ).toBe(mathEquationInputRulesService); }); it('should return the correct rules service for MultipleChoiceInput', () => { expect( - interactionRulesRegistryService - .getRulesServiceByInteractionId('MultipleChoiceInput') + interactionRulesRegistryService.getRulesServiceByInteractionId( + 'MultipleChoiceInput' + ) ).toBe(multipleChoiceInputRulesService); }); it('should return the correct rules service for MusicNotesInput', () => { expect( - interactionRulesRegistryService - .getRulesServiceByInteractionId('MusicNotesInput') + interactionRulesRegistryService.getRulesServiceByInteractionId( + 'MusicNotesInput' + ) ).toBe(musicNotesInputRulesService); }); it('should return the correct rules service for NumberWithUnits', () => { expect( - interactionRulesRegistryService - .getRulesServiceByInteractionId('NumberWithUnits') + interactionRulesRegistryService.getRulesServiceByInteractionId( + 'NumberWithUnits' + ) ).toBe(numberWithUnitsRulesService); }); - it('should return the correct rules service for NumericExpressionInput', - () => { - expect( - interactionRulesRegistryService - .getRulesServiceByInteractionId('NumericExpressionInput') - ).toBe(numericExpressionInputRulesService); - } - ); + it('should return the correct rules service for NumericExpressionInput', () => { + expect( + interactionRulesRegistryService.getRulesServiceByInteractionId( + 'NumericExpressionInput' + ) + ).toBe(numericExpressionInputRulesService); + }); it('should return the correct rules service for NumericInput', () => { expect( - interactionRulesRegistryService - .getRulesServiceByInteractionId('NumericInput') + interactionRulesRegistryService.getRulesServiceByInteractionId( + 'NumericInput' + ) ).toBe(numericInputRulesService); }); it('should return the correct rules service for PencilCodeEditor', () => { expect( - interactionRulesRegistryService - .getRulesServiceByInteractionId('PencilCodeEditor') + interactionRulesRegistryService.getRulesServiceByInteractionId( + 'PencilCodeEditor' + ) ).toBe(pencilCodeEditorRulesService); }); - it('should return the correct rules service for RatioExpressionInput', - () => { - expect( - interactionRulesRegistryService - .getRulesServiceByInteractionId('RatioExpressionInput') - ).toBe(ratioExpressionInputRulesService); - } - ); + it('should return the correct rules service for RatioExpressionInput', () => { + expect( + interactionRulesRegistryService.getRulesServiceByInteractionId( + 'RatioExpressionInput' + ) + ).toBe(ratioExpressionInputRulesService); + }); it('should return the correct rules service for SetInput', () => { expect( @@ -301,8 +308,9 @@ describe('Interaction Rules Registry Service', () => { it('should return the correct rules service for TextInput', () => { expect( - interactionRulesRegistryService - .getRulesServiceByInteractionId('TextInput') + interactionRulesRegistryService.getRulesServiceByInteractionId( + 'TextInput' + ) ).toBe(textInputRulesService); }); }); diff --git a/core/templates/services/interaction-rules-registry.service.ts b/core/templates/services/interaction-rules-registry.service.ts index d00ff7edd0ea..80b14c5e370d 100644 --- a/core/templates/services/interaction-rules-registry.service.ts +++ b/core/templates/services/interaction-rules-registry.service.ts @@ -16,62 +16,58 @@ * @fileoverview Service for getting the rules services of interactions. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { AlgebraicExpressionInputRulesService } from +import { + AlgebraicExpressionInputRulesService, // eslint-disable-next-line max-len - 'interactions/AlgebraicExpressionInput/directives/algebraic-expression-input-rules.service'; -import { CodeReplRulesService } from - 'interactions/CodeRepl/directives/code-repl-rules.service'; -import { ContinueRulesService } from - 'interactions/Continue/directives/continue-rules.service'; -import { DragAndDropSortInputRulesService } from +} from 'interactions/AlgebraicExpressionInput/directives/algebraic-expression-input-rules.service'; +import {CodeReplRulesService} from 'interactions/CodeRepl/directives/code-repl-rules.service'; +import {ContinueRulesService} from 'interactions/Continue/directives/continue-rules.service'; +import { + DragAndDropSortInputRulesService, // eslint-disable-next-line max-len - 'interactions/DragAndDropSortInput/directives/drag-and-drop-sort-input-rules.service'; -import { EndExplorationRulesService } from - 'interactions/EndExploration/directives/end-exploration-rules.service'; -import { FractionInputRulesService } from - 'interactions/FractionInput/directives/fraction-input-rules.service'; -import { GraphInputRulesService } from - 'interactions/GraphInput/directives/graph-input-rules.service'; -import { ImageClickInputRulesService } from - 'interactions/ImageClickInput/directives/image-click-input-rules.service'; -import { InteractiveMapRulesService } from - 'interactions/InteractiveMap/directives/interactive-map-rules.service'; -import { ItemSelectionInputRulesService } from +} from 'interactions/DragAndDropSortInput/directives/drag-and-drop-sort-input-rules.service'; +import {EndExplorationRulesService} from 'interactions/EndExploration/directives/end-exploration-rules.service'; +import {FractionInputRulesService} from 'interactions/FractionInput/directives/fraction-input-rules.service'; +import {GraphInputRulesService} from 'interactions/GraphInput/directives/graph-input-rules.service'; +import {ImageClickInputRulesService} from 'interactions/ImageClickInput/directives/image-click-input-rules.service'; +import {InteractiveMapRulesService} from 'interactions/InteractiveMap/directives/interactive-map-rules.service'; +import { + ItemSelectionInputRulesService, // eslint-disable-next-line max-len - 'interactions/ItemSelectionInput/directives/item-selection-input-rules.service'; -import { MathEquationInputRulesService } from +} from 'interactions/ItemSelectionInput/directives/item-selection-input-rules.service'; +import { + MathEquationInputRulesService, // eslint-disable-next-line max-len - 'interactions/MathEquationInput/directives/math-equation-input-rules.service'; -import { MultipleChoiceInputRulesService } from +} from 'interactions/MathEquationInput/directives/math-equation-input-rules.service'; +import { + MultipleChoiceInputRulesService, // eslint-disable-next-line max-len - 'interactions/MultipleChoiceInput/directives/multiple-choice-input-rules.service'; -import { MusicNotesInputRulesService } from - 'interactions/MusicNotesInput/directives/music-notes-input-rules.service'; -import { NumberWithUnitsRulesService } from - 'interactions/NumberWithUnits/directives/number-with-units-rules.service'; -import { NumericExpressionInputRulesService } from +} from 'interactions/MultipleChoiceInput/directives/multiple-choice-input-rules.service'; +import {MusicNotesInputRulesService} from 'interactions/MusicNotesInput/directives/music-notes-input-rules.service'; +import {NumberWithUnitsRulesService} from 'interactions/NumberWithUnits/directives/number-with-units-rules.service'; +import { + NumericExpressionInputRulesService, // eslint-disable-next-line max-len - 'interactions/NumericExpressionInput/directives/numeric-expression-input-rules.service'; -import { NumericInputRulesService } from - 'interactions/NumericInput/directives/numeric-input-rules.service'; -import { PencilCodeEditorRulesService } from - 'interactions/PencilCodeEditor/directives/pencil-code-editor-rules.service'; -import { RatioExpressionInputRulesService } from +} from 'interactions/NumericExpressionInput/directives/numeric-expression-input-rules.service'; +import {NumericInputRulesService} from 'interactions/NumericInput/directives/numeric-input-rules.service'; +import {PencilCodeEditorRulesService} from 'interactions/PencilCodeEditor/directives/pencil-code-editor-rules.service'; +import { + RatioExpressionInputRulesService, // eslint-disable-next-line max-len - 'interactions/RatioExpressionInput/directives/ratio-expression-input-rules.service'; -import { SetInputRulesService } from - 'interactions/SetInput/directives/set-input-rules.service'; -import { TextInputRulesService } from - 'interactions/TextInput/directives/text-input-rules.service'; -import { InteractionAnswer } from 'interactions/answer-defs'; -import { InteractionRuleInputs } from 'interactions/rule-input-defs'; +} from 'interactions/RatioExpressionInput/directives/ratio-expression-input-rules.service'; +import {SetInputRulesService} from 'interactions/SetInput/directives/set-input-rules.service'; +import {TextInputRulesService} from 'interactions/TextInput/directives/text-input-rules.service'; +import {InteractionAnswer} from 'interactions/answer-defs'; +import {InteractionRuleInputs} from 'interactions/rule-input-defs'; interface InteractionRulesService { [ruleName: string]: ( - answer: InteractionAnswer, ruleInputs: InteractionRuleInputs) => boolean; + answer: InteractionAnswer, + ruleInputs: InteractionRuleInputs + ) => boolean; } @Injectable({providedIn: 'root'}) @@ -79,58 +75,58 @@ export class InteractionRulesRegistryService { private rulesServiceRegistry: Map; constructor( - private algebraicExpressionInputRulesService: - AlgebraicExpressionInputRulesService, - private codeReplRulesService: CodeReplRulesService, - private continueRulesService: ContinueRulesService, - private dragAndDropSortInputRulesService: - DragAndDropSortInputRulesService, - private endExplorationRulesService: EndExplorationRulesService, - private fractionInputRulesService: FractionInputRulesService, - private graphInputRulesService: GraphInputRulesService, - private imageClickInputRulesService: ImageClickInputRulesService, - private interactiveMapRulesService: InteractiveMapRulesService, - private itemSelectionInputRulesService: ItemSelectionInputRulesService, - private mathEquationInputRulesService: MathEquationInputRulesService, - private multipleChoiceInputRulesService: MultipleChoiceInputRulesService, - private musicNotesInputRulesService: MusicNotesInputRulesService, - private numberWithUnitsRulesService: NumberWithUnitsRulesService, - private numericExpressionInputRulesService: - NumericExpressionInputRulesService, - private numericInputRulesService: NumericInputRulesService, - private pencilCodeEditorRulesService: PencilCodeEditorRulesService, - private ratioExpressionInputRulesService: - RatioExpressionInputRulesService, - private setInputRulesService: SetInputRulesService, - private textInputRulesService: TextInputRulesService) { - this.rulesServiceRegistry = new Map(Object.entries({ - AlgebraicExpressionInputRulesService: - this.algebraicExpressionInputRulesService, - CodeReplRulesService: this.codeReplRulesService, - ContinueRulesService: this.continueRulesService, - DragAndDropSortInputRulesService: this.dragAndDropSortInputRulesService, - EndExplorationRulesService: this.endExplorationRulesService, - FractionInputRulesService: this.fractionInputRulesService, - GraphInputRulesService: this.graphInputRulesService, - ImageClickInputRulesService: this.imageClickInputRulesService, - InteractiveMapRulesService: this.interactiveMapRulesService, - ItemSelectionInputRulesService: this.itemSelectionInputRulesService, - MathEquationInputRulesService: this.mathEquationInputRulesService, - MultipleChoiceInputRulesService: this.multipleChoiceInputRulesService, - MusicNotesInputRulesService: this.musicNotesInputRulesService, - NumberWithUnitsRulesService: this.numberWithUnitsRulesService, - NumericExpressionInputRulesService: - this.numericExpressionInputRulesService, - NumericInputRulesService: this.numericInputRulesService, - PencilCodeEditorRulesService: this.pencilCodeEditorRulesService, - RatioExpressionInputRulesService: this.ratioExpressionInputRulesService, - SetInputRulesService: this.setInputRulesService, - TextInputRulesService: this.textInputRulesService, - })); + private algebraicExpressionInputRulesService: AlgebraicExpressionInputRulesService, + private codeReplRulesService: CodeReplRulesService, + private continueRulesService: ContinueRulesService, + private dragAndDropSortInputRulesService: DragAndDropSortInputRulesService, + private endExplorationRulesService: EndExplorationRulesService, + private fractionInputRulesService: FractionInputRulesService, + private graphInputRulesService: GraphInputRulesService, + private imageClickInputRulesService: ImageClickInputRulesService, + private interactiveMapRulesService: InteractiveMapRulesService, + private itemSelectionInputRulesService: ItemSelectionInputRulesService, + private mathEquationInputRulesService: MathEquationInputRulesService, + private multipleChoiceInputRulesService: MultipleChoiceInputRulesService, + private musicNotesInputRulesService: MusicNotesInputRulesService, + private numberWithUnitsRulesService: NumberWithUnitsRulesService, + private numericExpressionInputRulesService: NumericExpressionInputRulesService, + private numericInputRulesService: NumericInputRulesService, + private pencilCodeEditorRulesService: PencilCodeEditorRulesService, + private ratioExpressionInputRulesService: RatioExpressionInputRulesService, + private setInputRulesService: SetInputRulesService, + private textInputRulesService: TextInputRulesService + ) { + this.rulesServiceRegistry = new Map( + Object.entries({ + AlgebraicExpressionInputRulesService: + this.algebraicExpressionInputRulesService, + CodeReplRulesService: this.codeReplRulesService, + ContinueRulesService: this.continueRulesService, + DragAndDropSortInputRulesService: this.dragAndDropSortInputRulesService, + EndExplorationRulesService: this.endExplorationRulesService, + FractionInputRulesService: this.fractionInputRulesService, + GraphInputRulesService: this.graphInputRulesService, + ImageClickInputRulesService: this.imageClickInputRulesService, + InteractiveMapRulesService: this.interactiveMapRulesService, + ItemSelectionInputRulesService: this.itemSelectionInputRulesService, + MathEquationInputRulesService: this.mathEquationInputRulesService, + MultipleChoiceInputRulesService: this.multipleChoiceInputRulesService, + MusicNotesInputRulesService: this.musicNotesInputRulesService, + NumberWithUnitsRulesService: this.numberWithUnitsRulesService, + NumericExpressionInputRulesService: + this.numericExpressionInputRulesService, + NumericInputRulesService: this.numericInputRulesService, + PencilCodeEditorRulesService: this.pencilCodeEditorRulesService, + RatioExpressionInputRulesService: this.ratioExpressionInputRulesService, + SetInputRulesService: this.setInputRulesService, + TextInputRulesService: this.textInputRulesService, + }) + ); } getRulesServiceByInteractionId( - interactionId: string): InteractionRulesService { + interactionId: string + ): InteractionRulesService { if (!interactionId) { throw new Error('Interaction ID must not be empty'); } @@ -138,14 +134,15 @@ export class InteractionRulesRegistryService { if (!this.rulesServiceRegistry.has(rulesServiceName)) { throw new Error('Unknown interaction ID: ' + interactionId); } - return ( - this.rulesServiceRegistry.get( - rulesServiceName - ) as InteractionRulesService - ); + return this.rulesServiceRegistry.get( + rulesServiceName + ) as InteractionRulesService; } } -angular.module('oppia').factory( - 'InteractionRulesRegistryService', - downgradeInjectable(InteractionRulesRegistryService)); +angular + .module('oppia') + .factory( + 'InteractionRulesRegistryService', + downgradeInjectable(InteractionRulesRegistryService) + ); diff --git a/core/templates/services/interaction-specs.service.spec.ts b/core/templates/services/interaction-specs.service.spec.ts index 40222cfd9f05..552b1bd1ee9d 100644 --- a/core/templates/services/interaction-specs.service.spec.ts +++ b/core/templates/services/interaction-specs.service.spec.ts @@ -16,9 +16,9 @@ * @fileoverview Unit tests for the interaction specs service. */ -import { TestBed } from '@angular/core/testing'; +import {TestBed} from '@angular/core/testing'; -import { InteractionSpecsService } from 'services/interaction-specs.service'; +import {InteractionSpecsService} from 'services/interaction-specs.service'; describe('InteractionSpecsService', () => { let interactionSpecsService: InteractionSpecsService; @@ -29,8 +29,9 @@ describe('InteractionSpecsService', () => { describe('checking whether an interaction can be trained with ML', () => { it('should throw an error when interaction does not exist.', () => { - expect(() => interactionSpecsService.isInteractionTrainable('Fake')) - .toThrowError('Fake is not a valid interaction id'); + expect(() => + interactionSpecsService.isInteractionTrainable('Fake') + ).toThrowError('Fake is not a valid interaction id'); }); it('should return false for ImageClickInput', () => { @@ -53,26 +54,26 @@ describe('InteractionSpecsService', () => { it('should return false for DragAndDropSortInput', () => { expect( - interactionSpecsService.isInteractionTrainable( - 'DragAndDropSortInput') + interactionSpecsService.isInteractionTrainable('DragAndDropSortInput') ).toBeFalse(); }); it('should return false for ItemSelectionInput', () => { expect( - interactionSpecsService.isInteractionTrainable( - 'ItemSelectionInput') + interactionSpecsService.isInteractionTrainable('ItemSelectionInput') ).toBeFalse(); }); it('should return false for Continue', () => { - expect(interactionSpecsService.isInteractionTrainable('Continue')) - .toBeFalse(); + expect( + interactionSpecsService.isInteractionTrainable('Continue') + ).toBeFalse(); }); it('should return false for GraphInput', () => { - expect(interactionSpecsService.isInteractionTrainable('GraphInput')) - .toBeFalse(); + expect( + interactionSpecsService.isInteractionTrainable('GraphInput') + ).toBeFalse(); }); it('should return false for EndExploration', () => { @@ -82,19 +83,20 @@ describe('InteractionSpecsService', () => { }); it('should return false for SetInput', () => { - expect(interactionSpecsService.isInteractionTrainable('SetInput')) - .toBeFalse(); + expect( + interactionSpecsService.isInteractionTrainable('SetInput') + ).toBeFalse(); }); it('should return true for CodeRepl', () => { - expect(interactionSpecsService.isInteractionTrainable('CodeRepl')) - .toBeTrue(); + expect( + interactionSpecsService.isInteractionTrainable('CodeRepl') + ).toBeTrue(); }); it('should return false for MultipleChoiceInput', () => { expect( - interactionSpecsService.isInteractionTrainable( - 'MultipleChoiceInput') + interactionSpecsService.isInteractionTrainable('MultipleChoiceInput') ).toBeFalse(); }); @@ -105,8 +107,9 @@ describe('InteractionSpecsService', () => { }); it('should return true for TextInput', () => { - expect(interactionSpecsService.isInteractionTrainable('TextInput')) - .toBeTrue(); + expect( + interactionSpecsService.isInteractionTrainable('TextInput') + ).toBeTrue(); }); it('should return false for InteractiveMap', () => { diff --git a/core/templates/services/interaction-specs.service.ts b/core/templates/services/interaction-specs.service.ts index 42b29d70b72b..d71d415eb58c 100644 --- a/core/templates/services/interaction-specs.service.ts +++ b/core/templates/services/interaction-specs.service.ts @@ -16,17 +16,16 @@ * @fileoverview Service for querying the INTERACTION_SPECS constants. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { InteractionSpecsConstants } from 'pages/interaction-specs.constants'; +import {InteractionSpecsConstants} from 'pages/interaction-specs.constants'; @Injectable({providedIn: 'root'}) export class InteractionSpecsService { isInteractionTrainable(interactionId: string): boolean { - const _interactionId = ( - interactionId as keyof typeof InteractionSpecsConstants.INTERACTION_SPECS - ); + const _interactionId = + interactionId as keyof typeof InteractionSpecsConstants.INTERACTION_SPECS; const interactionSpecs = InteractionSpecsConstants.INTERACTION_SPECS[_interactionId]; if (!interactionSpecs) { @@ -36,6 +35,9 @@ export class InteractionSpecsService { } } -angular.module('oppia').factory( - 'InteractionSpecsService', - downgradeInjectable(InteractionSpecsService)); +angular + .module('oppia') + .factory( + 'InteractionSpecsService', + downgradeInjectable(InteractionSpecsService) + ); diff --git a/core/templates/services/internet-connectivity.service.spec.ts b/core/templates/services/internet-connectivity.service.spec.ts index 2a035c21d67c..2b0c46466653 100644 --- a/core/templates/services/internet-connectivity.service.spec.ts +++ b/core/templates/services/internet-connectivity.service.spec.ts @@ -16,12 +16,20 @@ * @fileoverview Unit tests for the Connection Service. */ -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { TestBed } from '@angular/core/testing'; -import { InternetConnectivityService } from 'services/internet-connectivity.service'; -import { Subscription } from 'rxjs'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { discardPeriodicTasks, fakeAsync, flushMicrotasks, tick } from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {TestBed} from '@angular/core/testing'; +import {InternetConnectivityService} from 'services/internet-connectivity.service'; +import {Subscription} from 'rxjs'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import { + discardPeriodicTasks, + fakeAsync, + flushMicrotasks, + tick, +} from '@angular/core/testing'; class MockWindowRef { nativeWindow = { @@ -32,8 +40,8 @@ class MockWindowRef { return; }, navigator: { - onLine: true - } + onLine: true, + }, }; } @@ -46,15 +54,14 @@ describe('Connection Service', () => { beforeEach(() => { mockWindowRef = new MockWindowRef(); TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - ], + imports: [HttpClientTestingModule], providers: [ InternetConnectivityService, { provide: WindowRef, - useValue: mockWindowRef - }] + useValue: mockWindowRef, + }, + ], }); internetConnectivityService = TestBed.get(InternetConnectivityService); httpTestingController = TestBed.get(HttpTestingController); @@ -65,7 +72,8 @@ describe('Connection Service', () => { subscriptions.add( internetConnectivityService.onInternetStateChange.subscribe( connectionStateSpy - )); + ) + ); }); afterEach(() => { @@ -78,55 +86,51 @@ describe('Connection Service', () => { expect(internetConnectivityService).toBeTruthy(); }); - it('should report network status false when disconnected from network', - () => { - internetConnectivityService.startCheckingConnection(); - spyOn(internetConnectivityService, 'startCheckingConnection'); - mockWindowRef.nativeWindow.onoffline(); - expect(connectionStateSpy).toHaveBeenCalledWith(false); - }); + it('should report network status false when disconnected from network', () => { + internetConnectivityService.startCheckingConnection(); + spyOn(internetConnectivityService, 'startCheckingConnection'); + mockWindowRef.nativeWindow.onoffline(); + expect(connectionStateSpy).toHaveBeenCalledWith(false); + }); - it('should report internet status as online when internet is available', - fakeAsync(() => { - internetConnectivityService.startCheckingConnection(); - tick(100); - discardPeriodicTasks(); - let req = httpTestingController.expectOne('/internetconnectivityhandler'); - expect(req.request.method).toEqual('GET'); - req.flush({ - isInternetConnected: true, - }); - flushMicrotasks(); - var internetAccessible = internetConnectivityService.isOnline(); - expect(internetAccessible).toEqual(true); - })); + it('should report internet status as online when internet is available', fakeAsync(() => { + internetConnectivityService.startCheckingConnection(); + tick(100); + discardPeriodicTasks(); + let req = httpTestingController.expectOne('/internetconnectivityhandler'); + expect(req.request.method).toEqual('GET'); + req.flush({ + isInternetConnected: true, + }); + flushMicrotasks(); + var internetAccessible = internetConnectivityService.isOnline(); + expect(internetAccessible).toEqual(true); + })); - it('should report internet status as online when reconnected after offline', - fakeAsync(() => { - internetConnectivityService.startCheckingConnection(); - tick(100); - let req = httpTestingController.expectOne('/internetconnectivityhandler'); - expect(req.request.method).toEqual('GET'); - req.flush({ - isInternetConnected: true, - }); - flushMicrotasks(); - // Disconnecting window from the network. - // eslint-disable-next-line dot-notation - internetConnectivityService['_connectedToNetwork'] = false; - // Delay timer to test for the network connection again after offline. - tick(8000); - // Connecting window to the network. - mockWindowRef.nativeWindow.ononline(); - tick(3000); - discardPeriodicTasks(); - let req2 = httpTestingController.expectOne( - '/internetconnectivityhandler'); - expect(req2.request.method).toEqual('GET'); - req2.flush({ - isInternetConnected: true, - }); - flushMicrotasks(); - expect(connectionStateSpy).toHaveBeenCalledWith(true); - })); + it('should report internet status as online when reconnected after offline', fakeAsync(() => { + internetConnectivityService.startCheckingConnection(); + tick(100); + let req = httpTestingController.expectOne('/internetconnectivityhandler'); + expect(req.request.method).toEqual('GET'); + req.flush({ + isInternetConnected: true, + }); + flushMicrotasks(); + // Disconnecting window from the network. + // eslint-disable-next-line dot-notation + internetConnectivityService['_connectedToNetwork'] = false; + // Delay timer to test for the network connection again after offline. + tick(8000); + // Connecting window to the network. + mockWindowRef.nativeWindow.ononline(); + tick(3000); + discardPeriodicTasks(); + let req2 = httpTestingController.expectOne('/internetconnectivityhandler'); + expect(req2.request.method).toEqual('GET'); + req2.flush({ + isInternetConnected: true, + }); + flushMicrotasks(); + expect(connectionStateSpy).toHaveBeenCalledWith(true); + })); }); diff --git a/core/templates/services/internet-connectivity.service.ts b/core/templates/services/internet-connectivity.service.ts index 23d5925a6176..ae392455c7ad 100644 --- a/core/templates/services/internet-connectivity.service.ts +++ b/core/templates/services/internet-connectivity.service.ts @@ -16,15 +16,15 @@ * @fileoverview Service to check for the network & Internet connection. */ -import { EventEmitter, Injectable, NgZone } from '@angular/core'; -import { Subscription, timer, throwError } from 'rxjs'; -import { delay, retryWhen, switchMap, tap } from 'rxjs/operators'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { ServerConnectionBackendApiService } from './server-connection-backend-api.service'; +import {EventEmitter, Injectable, NgZone} from '@angular/core'; +import {Subscription, timer, throwError} from 'rxjs'; +import {delay, retryWhen, switchMap, tap} from 'rxjs/operators'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {ServerConnectionBackendApiService} from './server-connection-backend-api.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class InternetConnectivityService { private INTERNET_CONNECTIVITY_CHECK_INTERVAL_MILLISECS: number = 3500; @@ -32,17 +32,15 @@ export class InternetConnectivityService { private _internetAccessible: boolean = true; private _connectedToNetwork: boolean; - private _connectionStateChangeEventEmitter = ( - new EventEmitter()); + private _connectionStateChangeEventEmitter = new EventEmitter(); private httpSubscription: Subscription; - constructor( - private windowRef: WindowRef, - private _serverConnectionBackendApiService: ( - ServerConnectionBackendApiService), - private ngZone: NgZone) { + private windowRef: WindowRef, + private _serverConnectionBackendApiService: ServerConnectionBackendApiService, + private ngZone: NgZone + ) { this.httpSubscription = new Subscription(); this._connectedToNetwork = this.windowRef.nativeWindow.navigator.onLine; } @@ -53,40 +51,43 @@ export class InternetConnectivityService { // browser has Internet access and emits the current state of the // connection. this.ngZone.runOutsideAngular(() => { - this.httpSubscription.add(timer( - 0, this.INTERNET_CONNECTIVITY_CHECK_INTERVAL_MILLISECS) - .pipe( - switchMap(() => { - if (this._connectedToNetwork) { - return ( - this._serverConnectionBackendApiService - .fetchConnectionCheckResultAsync()); - } else { - return throwError('No Internet'); - } - }), - retryWhen(errors => errors.pipe( - tap(val => { - if (this._internetAccessible) { - this._internetAccessible = false; - this.ngZone.run(() => { - this._connectionStateChangeEventEmitter.emit( - this._internetAccessible); - }); + this.httpSubscription.add( + timer(0, this.INTERNET_CONNECTIVITY_CHECK_INTERVAL_MILLISECS) + .pipe( + switchMap(() => { + if (this._connectedToNetwork) { + return this._serverConnectionBackendApiService.fetchConnectionCheckResultAsync(); + } else { + return throwError('No Internet'); } }), - delay(this.MAX_MILLISECS_TO_WAIT_UNTIL_NEXT_CONNECTIVITY_CHECK) + retryWhen(errors => + errors.pipe( + tap(val => { + if (this._internetAccessible) { + this._internetAccessible = false; + this.ngZone.run(() => { + this._connectionStateChangeEventEmitter.emit( + this._internetAccessible + ); + }); + } + }), + delay(this.MAX_MILLISECS_TO_WAIT_UNTIL_NEXT_CONNECTIVITY_CHECK) + ) + ) ) - ) - ).subscribe(result => { - if (!this._internetAccessible) { - this._internetAccessible = true; - this.ngZone.run(() => { - this._connectionStateChangeEventEmitter.emit( - this._internetAccessible); - }); - } - })); + .subscribe(result => { + if (!this._internetAccessible) { + this._internetAccessible = true; + this.ngZone.run(() => { + this._connectionStateChangeEventEmitter.emit( + this._internetAccessible + ); + }); + } + }) + ); }); } @@ -101,8 +102,7 @@ export class InternetConnectivityService { this.windowRef.nativeWindow.onoffline = () => { this._connectedToNetwork = false; this._internetAccessible = false; - this._connectionStateChangeEventEmitter.emit( - this._internetAccessible); + this._connectionStateChangeEventEmitter.emit(this._internetAccessible); }; } @@ -124,6 +124,9 @@ export class InternetConnectivityService { } } -angular.module('oppia').factory( - 'InternetConnectivityService', - downgradeInjectable(InternetConnectivityService)); +angular + .module('oppia') + .factory( + 'InternetConnectivityService', + downgradeInjectable(InternetConnectivityService) + ); diff --git a/core/templates/services/keyboard-shortcut.service.spec.ts b/core/templates/services/keyboard-shortcut.service.spec.ts index bcaaea7338bf..291b0d30602d 100644 --- a/core/templates/services/keyboard-shortcut.service.spec.ts +++ b/core/templates/services/keyboard-shortcut.service.spec.ts @@ -17,14 +17,15 @@ */ import Mousetrap from 'mousetrap'; -import { ApplicationRef } from '@angular/core'; -import { async, TestBed } from '@angular/core/testing'; -import { KeyboardShortcutService } from 'services/keyboard-shortcut.service'; -import { KeyboardShortcutHelpModalComponent } from +import {ApplicationRef} from '@angular/core'; +import {async, TestBed} from '@angular/core/testing'; +import {KeyboardShortcutService} from 'services/keyboard-shortcut.service'; +import { + KeyboardShortcutHelpModalComponent, // eslint-disable-next-line max-len - 'components/keyboard-shortcut-help/keyboard-shortcut-help-modal.component'; -import { NgbModal, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { WindowRef } from 'services/contextual/window-ref.service'; +} from 'components/keyboard-shortcut-help/keyboard-shortcut-help-modal.component'; +import {NgbModal, NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {WindowRef} from 'services/contextual/window-ref.service'; class MockActiveModal { dismiss(): void { @@ -43,8 +44,8 @@ describe('Keyboard Shortcuts', () => { let mockWindow = { location: { - href: '' - } + href: '', + }, } as Window; let windowRef: WindowRef; @@ -52,20 +53,19 @@ describe('Keyboard Shortcuts', () => { let keyboardShortcutService: KeyboardShortcutService; let ngbModal: NgbModal; - beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [KeyboardShortcutHelpModalComponent], providers: [ { provide: NgbActiveModal, - useClass: MockActiveModal - } - ] + useClass: MockActiveModal, + }, + ], }).compileComponents(); })); - beforeEach(async() => { + beforeEach(async () => { ngbModal = TestBed.get(NgbModal); windowRef = new WindowRef(); appRef = TestBed.get(ApplicationRef); @@ -90,90 +90,103 @@ describe('Keyboard Shortcuts', () => { document.body.append(categoryBar); }); + it( + 'should navigate to the corresponding page' + + ' when the navigation key is pressed', + () => { + spyOnProperty(windowRef, 'nativeWindow').and.returnValue(mockWindow); + keyboardShortcutService.bindNavigationShortcuts(); + + mockWindow.location.href = ''; + expect(windowRef.nativeWindow.location.href).toBe(''); + + Mousetrap.trigger('ctrl+6'); + expect(windowRef.nativeWindow.location.href).toEqual('/get-started'); + mockWindow.location.href = ''; + expect(windowRef.nativeWindow.location.href).toBe(''); + + Mousetrap.trigger('ctrl+1'); + expect(windowRef.nativeWindow.location.href).toEqual( + '/community-library' + ); + mockWindow.location.href = ''; + expect(windowRef.nativeWindow.location.href).toBe(''); + + Mousetrap.trigger('ctrl+2'); + expect(windowRef.nativeWindow.location.href).toEqual( + '/learner-dashboard' + ); + mockWindow.location.href = ''; + expect(windowRef.nativeWindow.location.href).toBe(''); + + Mousetrap.trigger('ctrl+3'); + expect(windowRef.nativeWindow.location.href).toEqual( + '/creator-dashboard' + ); + mockWindow.location.href = ''; + expect(windowRef.nativeWindow.location.href).toBe(''); + + Mousetrap.trigger('ctrl+4'); + expect(windowRef.nativeWindow.location.href).toEqual('/about'); + mockWindow.location.href = ''; + expect(windowRef.nativeWindow.location.href).toBe(''); + + Mousetrap.trigger('ctrl+5'); + expect(windowRef.nativeWindow.location.href).toEqual('/preferences'); + mockWindow.location.href = ''; + expect(windowRef.nativeWindow.location.href).toBe(''); + } + ); - it('should navigate to the corresponding page' + - ' when the navigation key is pressed', () => { - spyOnProperty(windowRef, 'nativeWindow').and.returnValue(mockWindow); - keyboardShortcutService.bindNavigationShortcuts(); - - mockWindow.location.href = ''; - expect(windowRef.nativeWindow.location.href).toBe(''); - - Mousetrap.trigger('ctrl+6'); - expect(windowRef.nativeWindow.location.href).toEqual('/get-started'); - mockWindow.location.href = ''; - expect(windowRef.nativeWindow.location.href).toBe(''); - - Mousetrap.trigger('ctrl+1'); - expect(windowRef.nativeWindow.location.href).toEqual('/community-library'); - mockWindow.location.href = ''; - expect(windowRef.nativeWindow.location.href).toBe(''); - - Mousetrap.trigger('ctrl+2'); - expect(windowRef.nativeWindow.location.href).toEqual('/learner-dashboard'); - mockWindow.location.href = ''; - expect(windowRef.nativeWindow.location.href).toBe(''); - - Mousetrap.trigger('ctrl+3'); - expect(windowRef.nativeWindow.location.href).toEqual('/creator-dashboard'); - mockWindow.location.href = ''; - expect(windowRef.nativeWindow.location.href).toBe(''); - - Mousetrap.trigger('ctrl+4'); - expect(windowRef.nativeWindow.location.href).toEqual('/about'); - mockWindow.location.href = ''; - expect(windowRef.nativeWindow.location.href).toBe(''); - - Mousetrap.trigger('ctrl+5'); - expect(windowRef.nativeWindow.location.href).toEqual('/preferences'); - mockWindow.location.href = ''; - expect(windowRef.nativeWindow.location.href).toBe(''); - }); - - it('should move the focus to the corresponding element' + - ' when the action key is pressed', () => { - openQuickReferenceSpy = spyOn( - keyboardShortcutService, 'openQuickReference').and.callThrough(); - spyOn(ngbModal, 'open'); - spyOn(ngbModal, 'dismissAll'); - spyOn(appRef, 'tick'); + it( + 'should move the focus to the corresponding element' + + ' when the action key is pressed', + () => { + openQuickReferenceSpy = spyOn( + keyboardShortcutService, + 'openQuickReference' + ).and.callThrough(); + spyOn(ngbModal, 'open'); + spyOn(ngbModal, 'dismissAll'); + spyOn(appRef, 'tick'); - keyboardShortcutService.bindLibraryPageShortcuts(); + keyboardShortcutService.bindLibraryPageShortcuts(); - Mousetrap.trigger('s'); - expect(skipButton.isEqualNode(document.activeElement)); + Mousetrap.trigger('s'); + expect(skipButton.isEqualNode(document.activeElement)); - Mousetrap.trigger('/'); - expect(searchBar.isEqualNode(document.activeElement)); + Mousetrap.trigger('/'); + expect(searchBar.isEqualNode(document.activeElement)); - Mousetrap.trigger('c'); - expect(categoryBar.isEqualNode(document.activeElement)); + Mousetrap.trigger('c'); + expect(categoryBar.isEqualNode(document.activeElement)); - Mousetrap.trigger('?'); - expect(openQuickReferenceSpy).toHaveBeenCalled(); + Mousetrap.trigger('?'); + expect(openQuickReferenceSpy).toHaveBeenCalled(); - keyboardShortcutService.bindExplorationPlayerShortcuts(); + keyboardShortcutService.bindExplorationPlayerShortcuts(); - Mousetrap.trigger('s'); - expect(skipButton.isEqualNode(document.activeElement)); + Mousetrap.trigger('s'); + expect(skipButton.isEqualNode(document.activeElement)); - Mousetrap.trigger('k'); - expect(backButton.isEqualNode(document.activeElement)); + Mousetrap.trigger('k'); + expect(backButton.isEqualNode(document.activeElement)); - Mousetrap.trigger('j'); - expect(continueButton.isEqualNode(document.activeElement)); + Mousetrap.trigger('j'); + expect(continueButton.isEqualNode(document.activeElement)); - document.body.append(nextButton); - Mousetrap.trigger('j'); - expect(nextButton.isEqualNode(document.activeElement)); + document.body.append(nextButton); + Mousetrap.trigger('j'); + expect(nextButton.isEqualNode(document.activeElement)); - Mousetrap.trigger('?'); - expect(openQuickReferenceSpy).toHaveBeenCalled(); + Mousetrap.trigger('?'); + expect(openQuickReferenceSpy).toHaveBeenCalled(); - Mousetrap.trigger('left'); - expect(backButton.isEqualNode(document.activeElement)); + Mousetrap.trigger('left'); + expect(backButton.isEqualNode(document.activeElement)); - Mousetrap.trigger('right'); - expect(nextButton.isEqualNode(document.activeElement)); - }); + Mousetrap.trigger('right'); + expect(nextButton.isEqualNode(document.activeElement)); + } + ); }); diff --git a/core/templates/services/keyboard-shortcut.service.ts b/core/templates/services/keyboard-shortcut.service.ts index 6e6c7636110c..b22fe7072da2 100644 --- a/core/templates/services/keyboard-shortcut.service.ts +++ b/core/templates/services/keyboard-shortcut.service.ts @@ -17,33 +17,33 @@ */ import Mousetrap from 'mousetrap'; -import { Injectable, ApplicationRef } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { KeyboardShortcutHelpModalComponent } from 'components/keyboard-shortcut-help/keyboard-shortcut-help-modal.component'; -import { WindowRef } from 'services/contextual/window-ref.service'; - +import {Injectable, ApplicationRef} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {KeyboardShortcutHelpModalComponent} from 'components/keyboard-shortcut-help/keyboard-shortcut-help-modal.component'; +import {WindowRef} from 'services/contextual/window-ref.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class KeyboardShortcutService { constructor( private windowRef: WindowRef, private ngbModal: NgbModal, - private appRef: ApplicationRef) {} + private appRef: ApplicationRef + ) {} openQuickReference(): void { this.ngbModal.dismissAll(); - this.ngbModal.open( - KeyboardShortcutHelpModalComponent, {backdrop: true}); + this.ngbModal.open(KeyboardShortcutHelpModalComponent, {backdrop: true}); this.appRef.tick(); } bindExplorationPlayerShortcuts(): void { Mousetrap.bind('s', () => { var skipButton = document.querySelector( - '.oppia-skip-to-content') as HTMLElement; + '.oppia-skip-to-content' + ) as HTMLElement; if (skipButton !== null) { skipButton.focus(); } @@ -51,7 +51,8 @@ export class KeyboardShortcutService { Mousetrap.bind('k', () => { var previousButton = document.querySelector( - '.oppia-back-button') as HTMLElement; + '.oppia-back-button' + ) as HTMLElement; if (previousButton !== null) { previousButton.focus(); } @@ -59,9 +60,11 @@ export class KeyboardShortcutService { Mousetrap.bind('j', () => { var nextButton = document.querySelector( - '.oppia-next-button') as HTMLElement; + '.oppia-next-button' + ) as HTMLElement; var continueButton = document.querySelector( - '.oppia-learner-confirm-button') as HTMLElement; + '.oppia-learner-confirm-button' + ) as HTMLElement; if (nextButton !== null) { nextButton.focus(); } @@ -76,7 +79,8 @@ export class KeyboardShortcutService { Mousetrap.bind('left', () => { let previousButton = document.querySelector( - '.oppia-back-button') as HTMLElement; + '.oppia-back-button' + ) as HTMLElement; if (previousButton !== null) { previousButton.click(); } @@ -84,7 +88,8 @@ export class KeyboardShortcutService { Mousetrap.bind('right', () => { let nextButton = document.querySelector( - '.oppia-next-button') as HTMLElement; + '.oppia-next-button' + ) as HTMLElement; if (nextButton !== null) { nextButton.click(); } @@ -94,20 +99,23 @@ export class KeyboardShortcutService { bindLibraryPageShortcuts(): void { Mousetrap.bind('/', () => { var searchBar = document.querySelector( - '.oppia-search-bar-text-input') as HTMLElement; + '.oppia-search-bar-text-input' + ) as HTMLElement; searchBar.focus(); return false; }); Mousetrap.bind('c', () => { var categoryBar = document.querySelector( - '.oppia-search-bar-dropdown-toggle') as HTMLElement; + '.oppia-search-bar-dropdown-toggle' + ) as HTMLElement; categoryBar.focus(); }); Mousetrap.bind('s', () => { var skipButton = document.querySelector( - '.oppia-skip-to-content') as HTMLElement; + '.oppia-skip-to-content' + ) as HTMLElement; if (skipButton !== null) { skipButton.focus(); } @@ -118,7 +126,6 @@ export class KeyboardShortcutService { }); } - bindNavigationShortcuts(): void { Mousetrap.bind('ctrl+6', () => { this.windowRef.nativeWindow.location.href = '/get-started'; @@ -146,5 +153,9 @@ export class KeyboardShortcutService { } } -angular.module('oppia').factory( - 'KeyboardShortcutService', downgradeInjectable(KeyboardShortcutService)); +angular + .module('oppia') + .factory( + 'KeyboardShortcutService', + downgradeInjectable(KeyboardShortcutService) + ); diff --git a/core/templates/services/loader.service.spec.ts b/core/templates/services/loader.service.spec.ts index ac901153bf5b..119231e42d6e 100644 --- a/core/templates/services/loader.service.spec.ts +++ b/core/templates/services/loader.service.spec.ts @@ -16,8 +16,8 @@ * @fileoverview Unit tests for loader service. */ -import { LoaderService } from 'services/loader.service'; -import { Subscription } from 'rxjs'; +import {LoaderService} from 'services/loader.service'; +import {Subscription} from 'rxjs'; describe('Loader Service', () => { const loaderService = new LoaderService(); @@ -25,9 +25,11 @@ describe('Loader Service', () => { let subscriptions: Subscription; beforeEach(() => { subscriptions = new Subscription(); - subscriptions.add(loaderService.onLoadingMessageChange.subscribe( - (message: string) => loadingMessage = message - )); + subscriptions.add( + loaderService.onLoadingMessageChange.subscribe( + (message: string) => (loadingMessage = message) + ) + ); }); afterEach(() => { diff --git a/core/templates/services/loader.service.ts b/core/templates/services/loader.service.ts index 7163991a9edd..51b3ac336a71 100644 --- a/core/templates/services/loader.service.ts +++ b/core/templates/services/loader.service.ts @@ -16,11 +16,11 @@ * @fileoverview A service to show loading screen. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { EventEmitter, Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {EventEmitter, Injectable} from '@angular/core'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class LoaderService { // TODO(#8472): Remove static when migration is complete. @@ -46,6 +46,6 @@ export class LoaderService { } } -angular.module('oppia').factory( - 'LoaderService', - downgradeInjectable(LoaderService)); +angular + .module('oppia') + .factory('LoaderService', downgradeInjectable(LoaderService)); diff --git a/core/templates/services/local-storage.service.spec.ts b/core/templates/services/local-storage.service.spec.ts index fee454dc8426..7acbecfd73af 100644 --- a/core/templates/services/local-storage.service.spec.ts +++ b/core/templates/services/local-storage.service.spec.ts @@ -16,12 +16,19 @@ * @fileoverview unit tests for the local save services. */ -import { TestBed } from '@angular/core/testing'; -import { EntityEditorBrowserTabsInfo, EntityEditorBrowserTabsInfoLocalStorageDict } from 'domain/entity_editor_browser_tabs_info/entity-editor-browser-tabs-info.model'; -import { EntityEditorBrowserTabsInfoDomainConstants } from 'domain/entity_editor_browser_tabs_info/entity-editor-browser-tabs-info-domain.constants'; -import { ExplorationChange, ExplorationDraft, ExplorationDraftDict } from 'domain/exploration/exploration-draft.model'; -import { LocalStorageService } from 'services/local-storage.service'; -import { WindowRef } from './contextual/window-ref.service'; +import {TestBed} from '@angular/core/testing'; +import { + EntityEditorBrowserTabsInfo, + EntityEditorBrowserTabsInfoLocalStorageDict, +} from 'domain/entity_editor_browser_tabs_info/entity-editor-browser-tabs-info.model'; +import {EntityEditorBrowserTabsInfoDomainConstants} from 'domain/entity_editor_browser_tabs_info/entity-editor-browser-tabs-info-domain.constants'; +import { + ExplorationChange, + ExplorationDraft, + ExplorationDraftDict, +} from 'domain/exploration/exploration-draft.model'; +import {LocalStorageService} from 'services/local-storage.service'; +import {WindowRef} from './contextual/window-ref.service'; describe('LocalStorageService', () => { describe('behavior in editor', () => { @@ -33,11 +40,11 @@ describe('LocalStorageService', () => { const draftChangeListIdTwo = 1; const draftDictOne: ExplorationDraftDict = { draftChanges: changeList, - draftChangeListId: draftChangeListIdOne + draftChangeListId: draftChangeListIdOne, }; const draftDictTwo: ExplorationDraftDict = { draftChanges: changeList, - draftChangeListId: draftChangeListIdTwo + draftChangeListId: draftChangeListIdTwo, }; let draftOne: ExplorationDraft; let draftTwo: ExplorationDraft; @@ -57,55 +64,76 @@ describe('LocalStorageService', () => { it('should correctly save the draft', () => { localStorageService.saveExplorationDraft( - explorationIdOne, changeList, draftChangeListIdOne); + explorationIdOne, + changeList, + draftChangeListIdOne + ); localStorageService.saveExplorationDraft( - explorationIdTwo, changeList, draftChangeListIdTwo); + explorationIdTwo, + changeList, + draftChangeListIdTwo + ); - expect(localStorageService.getExplorationDraft( - explorationIdOne)).toEqual(draftOne); - expect(localStorageService.getExplorationDraft( - explorationIdTwo)).toEqual(draftTwo); + expect(localStorageService.getExplorationDraft(explorationIdOne)).toEqual( + draftOne + ); + expect(localStorageService.getExplorationDraft(explorationIdTwo)).toEqual( + draftTwo + ); }); it('should correctly change and save a draft', () => { localStorageService.saveExplorationDraft( - explorationIdOne, changeList, draftChangeListIdOne); + explorationIdOne, + changeList, + draftChangeListIdOne + ); - expect(localStorageService.getExplorationDraft( - explorationIdOne)).toEqual(draftOne); + expect(localStorageService.getExplorationDraft(explorationIdOne)).toEqual( + draftOne + ); const draftChangeListIdOneChanged = 3; - const draftOneChanged = ExplorationDraft - .createFromLocalStorageDict({ - draftChanges: changeList, - draftChangeListId: draftChangeListIdOneChanged - }); + const draftOneChanged = ExplorationDraft.createFromLocalStorageDict({ + draftChanges: changeList, + draftChangeListId: draftChangeListIdOneChanged, + }); localStorageService.saveExplorationDraft( - explorationIdOne, changeList, draftChangeListIdOneChanged); + explorationIdOne, + changeList, + draftChangeListIdOneChanged + ); - expect(localStorageService.getExplorationDraft( - explorationIdOne)).toEqual(draftOneChanged); + expect(localStorageService.getExplorationDraft(explorationIdOne)).toEqual( + draftOneChanged + ); }); it('should correctly remove the draft', () => { localStorageService.saveExplorationDraft( - explorationIdTwo, changeList, draftChangeListIdTwo); + explorationIdTwo, + changeList, + draftChangeListIdTwo + ); localStorageService.removeExplorationDraft(explorationIdTwo); - expect(localStorageService.getExplorationDraft( - explorationIdTwo)).toBeNull(); + expect( + localStorageService.getExplorationDraft(explorationIdTwo) + ).toBeNull(); }); it('should correctly save a language code', () => { localStorageService.updateLastSelectedTranslationLanguageCode('en'); - expect(localStorageService.getLastSelectedTranslationLanguageCode()) - .toBe('en'); + expect(localStorageService.getLastSelectedTranslationLanguageCode()).toBe( + 'en' + ); localStorageService.updateLastSelectedTranslationLanguageCode('hi'); - expect(localStorageService.getLastSelectedTranslationLanguageCode()) - .toBe('hi'); + expect(localStorageService.getLastSelectedTranslationLanguageCode()).toBe( + 'hi' + ); }); it('should not save a language code when storage is not available', () => { @@ -113,18 +141,21 @@ describe('LocalStorageService', () => { localStorageService.updateLastSelectedTranslationLanguageCode('en'); - expect(localStorageService.getLastSelectedTranslationLanguageCode()) - .toBeNull(); + expect( + localStorageService.getLastSelectedTranslationLanguageCode() + ).toBeNull(); }); it('should correctly save a topic name', () => { localStorageService.updateLastSelectedTranslationTopicName('Topic 1'); - expect(localStorageService.getLastSelectedTranslationTopicName()) - .toBe('Topic 1'); + expect(localStorageService.getLastSelectedTranslationTopicName()).toBe( + 'Topic 1' + ); localStorageService.updateLastSelectedTranslationTopicName('Topic 1'); - expect(localStorageService.getLastSelectedTranslationTopicName()) - .toBe('Topic 1'); + expect(localStorageService.getLastSelectedTranslationTopicName()).toBe( + 'Topic 1' + ); }); it('should not save a topic name when storage is not available', () => { @@ -132,104 +163,118 @@ describe('LocalStorageService', () => { localStorageService.updateLastSelectedTranslationTopicName('Topic 1'); - expect(localStorageService.getLastSelectedTranslationTopicName()) - .toBeNull(); + expect( + localStorageService.getLastSelectedTranslationTopicName() + ).toBeNull(); }); it('should correctly save and retrieve sign up section preference', () => { - expect(localStorageService.getEndChapterSignUpSectionHiddenPreference()) - .toBeNull(); - - localStorageService.updateEndChapterSignUpSectionHiddenPreference('true'); - - expect(localStorageService.getEndChapterSignUpSectionHiddenPreference()) - .toBe('true'); - }); - - it('should not save sign up section preference when local storage isn\'t ' + - 'available', () => { - spyOn(localStorageService, 'isStorageAvailable').and.returnValue(false); + expect( + localStorageService.getEndChapterSignUpSectionHiddenPreference() + ).toBeNull(); localStorageService.updateEndChapterSignUpSectionHiddenPreference('true'); - expect(localStorageService.getEndChapterSignUpSectionHiddenPreference()) - .toBeNull(); + expect( + localStorageService.getEndChapterSignUpSectionHiddenPreference() + ).toBe('true'); }); - it('should not get entity editor browser tabs info from local ' + - 'storage when storage is not available', () => { - spyOn(localStorageService, 'isStorageAvailable').and.returnValue(false); - - expect(localStorageService.getEntityEditorBrowserTabsInfo( - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_TOPIC_EDITOR_BROWSER_TABS, 'topic_1')).toBeNull(); - }); + it( + "should not save sign up section preference when local storage isn't " + + 'available', + () => { + spyOn(localStorageService, 'isStorageAvailable').and.returnValue(false); + + localStorageService.updateEndChapterSignUpSectionHiddenPreference( + 'true' + ); + + expect( + localStorageService.getEndChapterSignUpSectionHiddenPreference() + ).toBeNull(); + } + ); + + it( + 'should not get entity editor browser tabs info from local ' + + 'storage when storage is not available', + () => { + spyOn(localStorageService, 'isStorageAvailable').and.returnValue(false); + + expect( + localStorageService.getEntityEditorBrowserTabsInfo( + EntityEditorBrowserTabsInfoDomainConstants.OPENED_TOPIC_EDITOR_BROWSER_TABS, + 'topic_1' + ) + ).toBeNull(); + } + ); it('should add entity editor browser tabs info', () => { - const entityEditorBrowserTabsInfoLocalStorageDict: - EntityEditorBrowserTabsInfoLocalStorageDict = { + const entityEditorBrowserTabsInfoLocalStorageDict: EntityEditorBrowserTabsInfoLocalStorageDict = + { entityType: 'topic', latestVersion: 1, numberOfOpenedTabs: 1, - someTabHasUnsavedChanges: false + someTabHasUnsavedChanges: false, }; localStorageService.updateEntityEditorBrowserTabsInfo( EntityEditorBrowserTabsInfo.fromLocalStorageDict( - entityEditorBrowserTabsInfoLocalStorageDict, 'topic_1'), - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_TOPIC_EDITOR_BROWSER_TABS + entityEditorBrowserTabsInfoLocalStorageDict, + 'topic_1' + ), + EntityEditorBrowserTabsInfoDomainConstants.OPENED_TOPIC_EDITOR_BROWSER_TABS ); - const topicEditorBrowserTabsInfo = ( + const topicEditorBrowserTabsInfo = localStorageService.getEntityEditorBrowserTabsInfo( - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_TOPIC_EDITOR_BROWSER_TABS, - 'topic_1') - ); + EntityEditorBrowserTabsInfoDomainConstants.OPENED_TOPIC_EDITOR_BROWSER_TABS, + 'topic_1' + ); expect(topicEditorBrowserTabsInfo).not.toBeNull(); // The "?" in the below assertion is to avoid typescript errors because // localStorageService.getEntityEditorBrowserTabsInfo can either return // null or an instance of EntityEditorBrowserTabsInfo. - expect( - topicEditorBrowserTabsInfo?.toLocalStorageDict() - ).toEqual(entityEditorBrowserTabsInfoLocalStorageDict); + expect(topicEditorBrowserTabsInfo?.toLocalStorageDict()).toEqual( + entityEditorBrowserTabsInfoLocalStorageDict + ); }); it('should update entity editor browser tabs info', () => { - const entityEditorBrowserTabsInfoLocalStorageDict: - EntityEditorBrowserTabsInfoLocalStorageDict = { + const entityEditorBrowserTabsInfoLocalStorageDict: EntityEditorBrowserTabsInfoLocalStorageDict = + { entityType: 'skill', latestVersion: 1, numberOfOpenedTabs: 1, - someTabHasUnsavedChanges: false + someTabHasUnsavedChanges: false, }; - const entityEditorBrowserTabsInfo = ( + const entityEditorBrowserTabsInfo = EntityEditorBrowserTabsInfo.fromLocalStorageDict( - entityEditorBrowserTabsInfoLocalStorageDict, 'skill_1')); + entityEditorBrowserTabsInfoLocalStorageDict, + 'skill_1' + ); localStorageService.updateEntityEditorBrowserTabsInfo( entityEditorBrowserTabsInfo, - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_SKILL_EDITOR_BROWSER_TABS + EntityEditorBrowserTabsInfoDomainConstants.OPENED_SKILL_EDITOR_BROWSER_TABS ); entityEditorBrowserTabsInfo.setLatestVersion(2); localStorageService.updateEntityEditorBrowserTabsInfo( entityEditorBrowserTabsInfo, - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_SKILL_EDITOR_BROWSER_TABS + EntityEditorBrowserTabsInfoDomainConstants.OPENED_SKILL_EDITOR_BROWSER_TABS ); entityEditorBrowserTabsInfo.decrementNumberOfOpenedTabs(); localStorageService.updateEntityEditorBrowserTabsInfo( entityEditorBrowserTabsInfo, - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_SKILL_EDITOR_BROWSER_TABS + EntityEditorBrowserTabsInfoDomainConstants.OPENED_SKILL_EDITOR_BROWSER_TABS ); }); it('should register new callback for storage event listener', () => { - const callbackFnSpy = jasmine.createSpy('callbackFn', (event) => {}); + const callbackFnSpy = jasmine.createSpy('callbackFn', event => {}); localStorageService.registerNewStorageEventListener(callbackFnSpy); windowRef.nativeWindow.dispatchEvent(new StorageEvent('storage')); @@ -238,36 +283,42 @@ describe('LocalStorageService', () => { it('should correctly save unique progress URL ID', () => { expect( - localStorageService.getUniqueProgressIdOfLoggedOutLearner()).toBeNull(); + localStorageService.getUniqueProgressIdOfLoggedOutLearner() + ).toBeNull(); localStorageService.updateUniqueProgressIdOfLoggedOutLearner('abcdef'); expect( - localStorageService.getUniqueProgressIdOfLoggedOutLearner()) - .toEqual('abcdef'); + localStorageService.getUniqueProgressIdOfLoggedOutLearner() + ).toEqual('abcdef'); }); - it('should not save unique progress URL ID when storage is not ' + - 'available', () => { - spyOn(localStorageService, 'isStorageAvailable').and.returnValue(false); + it( + 'should not save unique progress URL ID when storage is not ' + + 'available', + () => { + spyOn(localStorageService, 'isStorageAvailable').and.returnValue(false); - localStorageService.updateUniqueProgressIdOfLoggedOutLearner('abcdef'); + localStorageService.updateUniqueProgressIdOfLoggedOutLearner('abcdef'); - expect( - localStorageService.getUniqueProgressIdOfLoggedOutLearner()).toBeNull(); - }); + expect( + localStorageService.getUniqueProgressIdOfLoggedOutLearner() + ).toBeNull(); + } + ); it('should correctly remove unique progress URL ID', () => { localStorageService.updateUniqueProgressIdOfLoggedOutLearner('abcdef'); expect( - localStorageService.getUniqueProgressIdOfLoggedOutLearner()) - .toEqual('abcdef'); + localStorageService.getUniqueProgressIdOfLoggedOutLearner() + ).toEqual('abcdef'); localStorageService.removeUniqueProgressIdOfLoggedOutLearner(); expect( - localStorageService.getUniqueProgressIdOfLoggedOutLearner()).toBeNull(); + localStorageService.getUniqueProgressIdOfLoggedOutLearner() + ).toBeNull(); }); }); }); diff --git a/core/templates/services/local-storage.service.ts b/core/templates/services/local-storage.service.ts index 5bda60d704c5..9b876019d589 100644 --- a/core/templates/services/local-storage.service.ts +++ b/core/templates/services/local-storage.service.ts @@ -21,28 +21,29 @@ // Note that the draft is only saved if localStorage exists and works // (i.e. has storage capacity). -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; import { ExplorationChange, - ExplorationDraft + ExplorationDraft, } from 'domain/exploration/exploration-draft.model'; -import { EntityEditorBrowserTabsInfo, EntityEditorBrowserTabsInfoDict } from 'domain/entity_editor_browser_tabs_info/entity-editor-browser-tabs-info.model'; -import { WindowRef } from './contextual/window-ref.service'; +import { + EntityEditorBrowserTabsInfo, + EntityEditorBrowserTabsInfoDict, +} from 'domain/entity_editor_browser_tabs_info/entity-editor-browser-tabs-info.model'; +import {WindowRef} from './contextual/window-ref.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class LocalStorageService { - constructor( - private windowRef: WindowRef - ) {} + constructor(private windowRef: WindowRef) {} // Check that local storage exists and works as expected. // If it does storage stores the localStorage object, // else storage is undefined or false. - storage = (function() { + storage = (function () { let test = 'test'; let result; try { @@ -51,13 +52,13 @@ export class LocalStorageService { localStorage.removeItem(test); return result && localStorage; } catch (exception) {} - }()); + })(); - LAST_SELECTED_TRANSLATION_LANGUAGE_KEY = ('last_selected_translation_lang'); + LAST_SELECTED_TRANSLATION_LANGUAGE_KEY = 'last_selected_translation_lang'; - LAST_SELECTED_TRANSLATION_TOPIC_NAME = ('last_selected_translation_topic'); + LAST_SELECTED_TRANSLATION_TOPIC_NAME = 'last_selected_translation_topic'; - HIDE_SIGN_UP_SECTION_PREFERENCE = ('hide_sign_up_section'); + HIDE_SIGN_UP_SECTION_PREFERENCE = 'hide_sign_up_section'; /** * Create the key to access the changeList in localStorage @@ -85,18 +86,23 @@ export class LocalStorageService { * @param {Integer} draftChangeListId - The id of the draft to be saved. */ saveExplorationDraft( - explorationId: string, changeList: ExplorationChange[], - draftChangeListId: number): void { + explorationId: string, + changeList: ExplorationChange[], + draftChangeListId: number + ): void { let localSaveKey = this._createExplorationDraftKey(explorationId); if (this.isStorageAvailable()) { let draftDict = ExplorationDraft.toLocalStorageDict( - changeList, draftChangeListId); + changeList, + draftChangeListId + ); // It is possible that storage does not exist or the user does not have // permission to access it but this condition is already being checked by // calling 'isStorageAvailable()' so the typecast is safe. (this.storage as Storage).setItem( localSaveKey, - JSON.stringify(draftDict)); + JSON.stringify(draftDict) + ); } } @@ -114,10 +120,11 @@ export class LocalStorageService { // permission to access it but this condition is already being checked by // calling 'isStorageAvailable()' so the typecast is safe. let draftDict = (this.storage as Storage).getItem( - this._createExplorationDraftKey(explorationId)); + this._createExplorationDraftKey(explorationId) + ); if (draftDict) { - return ( - ExplorationDraft.createFromLocalStorageDict(JSON.parse(draftDict)) + return ExplorationDraft.createFromLocalStorageDict( + JSON.parse(draftDict) ); } } @@ -136,7 +143,8 @@ export class LocalStorageService { // permission to access it but this condition is already being checked by // calling 'isStorageAvailable()' so the typecast is safe. (this.storage as Storage).removeItem( - this._createExplorationDraftKey(explorationId)); + this._createExplorationDraftKey(explorationId) + ); } } @@ -150,7 +158,9 @@ export class LocalStorageService { // permission to access it but this condition is already being checked by // calling 'isStorageAvailable()' so the typecast is safe. (this.storage as Storage).setItem( - this.LAST_SELECTED_TRANSLATION_LANGUAGE_KEY, languageCode); + this.LAST_SELECTED_TRANSLATION_LANGUAGE_KEY, + languageCode + ); } } @@ -166,7 +176,9 @@ export class LocalStorageService { // permission to access it but this condition is already being checked // by calling 'isStorageAvailable()' so the typecast is safe. (this.storage as Storage).getItem( - this.LAST_SELECTED_TRANSLATION_LANGUAGE_KEY)); + this.LAST_SELECTED_TRANSLATION_LANGUAGE_KEY + ) + ); } return null; } @@ -181,7 +193,9 @@ export class LocalStorageService { // permission to access it but this condition is already being checked by // calling 'isStorageAvailable()' so the typecast is safe. (this.storage as Storage).setItem( - this.LAST_SELECTED_TRANSLATION_TOPIC_NAME, topicName); + this.LAST_SELECTED_TRANSLATION_TOPIC_NAME, + topicName + ); } } @@ -197,7 +211,9 @@ export class LocalStorageService { // permission to access it but this condition is already being checked // by calling 'isStorageAvailable()' so the typecast is safe. (this.storage as Storage).getItem( - this.LAST_SELECTED_TRANSLATION_TOPIC_NAME)); + this.LAST_SELECTED_TRANSLATION_TOPIC_NAME + ) + ); } return null; } @@ -205,7 +221,9 @@ export class LocalStorageService { updateUniqueProgressIdOfLoggedOutLearner(uniqueProgressUrlId: string): void { if (this.isStorageAvailable()) { (this.storage as Storage).setItem( - 'unique_progress_id', uniqueProgressUrlId); + 'unique_progress_id', + uniqueProgressUrlId + ); } } @@ -232,7 +250,9 @@ export class LocalStorageService { // permission to access it but this condition is already being checked by // calling 'isStorageAvailable()' so the typecast is safe. (this.storage as Storage).setItem( - this.HIDE_SIGN_UP_SECTION_PREFERENCE, isSectionHidden); + this.HIDE_SIGN_UP_SECTION_PREFERENCE, + isSectionHidden + ); } } @@ -247,8 +267,8 @@ export class LocalStorageService { // It is possible that storage does not exist or the user does not have // permission to access it but this condition is already being checked // by calling 'isStorageAvailable()' so the typecast is safe. - (this.storage as Storage).getItem( - this.HIDE_SIGN_UP_SECTION_PREFERENCE)); + (this.storage as Storage).getItem(this.HIDE_SIGN_UP_SECTION_PREFERENCE) + ); } return null; } @@ -265,25 +285,30 @@ export class LocalStorageService { * storage. Otherwise, returns null. */ getEntityEditorBrowserTabsInfo( - entityEditorBrowserTabsInfoConstant: string, entityId: string + entityEditorBrowserTabsInfoConstant: string, + entityId: string ): EntityEditorBrowserTabsInfo | null { if (this.isStorageAvailable()) { - let allEntityEditorBrowserTabsInfoDicts: - EntityEditorBrowserTabsInfoDict = {}; + let allEntityEditorBrowserTabsInfoDicts: EntityEditorBrowserTabsInfoDict = + {}; - const stringifiedEntityEditorBrowserTabsInfo = (this.storage as Storage) - .getItem(entityEditorBrowserTabsInfoConstant); + const stringifiedEntityEditorBrowserTabsInfo = ( + this.storage as Storage + ).getItem(entityEditorBrowserTabsInfoConstant); if (stringifiedEntityEditorBrowserTabsInfo) { allEntityEditorBrowserTabsInfoDicts = JSON.parse( - stringifiedEntityEditorBrowserTabsInfo); + stringifiedEntityEditorBrowserTabsInfo + ); } - const requiredEntityEditorBrowserTabsInfoDict = ( - allEntityEditorBrowserTabsInfoDicts[entityId]); + const requiredEntityEditorBrowserTabsInfoDict = + allEntityEditorBrowserTabsInfoDicts[entityId]; if (requiredEntityEditorBrowserTabsInfoDict) { return EntityEditorBrowserTabsInfo.fromLocalStorageDict( - requiredEntityEditorBrowserTabsInfoDict, entityId); + requiredEntityEditorBrowserTabsInfoDict, + entityId + ); } } return null; @@ -297,23 +322,24 @@ export class LocalStorageService { * the local storage. */ updateEntityEditorBrowserTabsInfo( - entityEditorBrowserTabsInfo: EntityEditorBrowserTabsInfo, - entityEditorBrowserTabsInfoConstant: string + entityEditorBrowserTabsInfo: EntityEditorBrowserTabsInfo, + entityEditorBrowserTabsInfoConstant: string ): void { if (this.isStorageAvailable()) { - const updatedEntityEditorBrowserTabsInfoDict = ( - entityEditorBrowserTabsInfo.toLocalStorageDict() - ); + const updatedEntityEditorBrowserTabsInfoDict = + entityEditorBrowserTabsInfo.toLocalStorageDict(); const entityId = entityEditorBrowserTabsInfo.getId(); - let allEntityEditorBrowserTabsInfoDicts: - EntityEditorBrowserTabsInfoDict = {}; + let allEntityEditorBrowserTabsInfoDicts: EntityEditorBrowserTabsInfoDict = + {}; - const stringifiedEntityEditorBrowserTabsInfo = (this.storage as Storage) - .getItem(entityEditorBrowserTabsInfoConstant); + const stringifiedEntityEditorBrowserTabsInfo = ( + this.storage as Storage + ).getItem(entityEditorBrowserTabsInfoConstant); if (stringifiedEntityEditorBrowserTabsInfo) { allEntityEditorBrowserTabsInfoDicts = JSON.parse( - stringifiedEntityEditorBrowserTabsInfo); + stringifiedEntityEditorBrowserTabsInfo + ); } if (allEntityEditorBrowserTabsInfoDicts[entityId]) { @@ -326,22 +352,24 @@ export class LocalStorageService { // has become zero. if (Object.keys(allEntityEditorBrowserTabsInfoDicts).length === 0) { this.removeOpenedEntityEditorBrowserTabsInfo( - entityEditorBrowserTabsInfoConstant); + entityEditorBrowserTabsInfoConstant + ); } } else { // If none of the above mentioned edge cases are present, then just // update the local storage with the updated info. - allEntityEditorBrowserTabsInfoDicts[entityId] = ( - updatedEntityEditorBrowserTabsInfoDict); + allEntityEditorBrowserTabsInfoDicts[entityId] = + updatedEntityEditorBrowserTabsInfoDict; } } else { - allEntityEditorBrowserTabsInfoDicts[entityId] = ( - updatedEntityEditorBrowserTabsInfoDict); + allEntityEditorBrowserTabsInfoDicts[entityId] = + updatedEntityEditorBrowserTabsInfoDict; } (this.storage as Storage).setItem( entityEditorBrowserTabsInfoConstant, - JSON.stringify(allEntityEditorBrowserTabsInfoDicts)); + JSON.stringify(allEntityEditorBrowserTabsInfoDicts) + ); } } @@ -352,7 +380,7 @@ export class LocalStorageService { * the local storage. */ removeOpenedEntityEditorBrowserTabsInfo( - entityEditorBrowserTabsInfoConstant: string + entityEditorBrowserTabsInfoConstant: string ): void { if (this.isStorageAvailable()) { (this.storage as Storage).removeItem(entityEditorBrowserTabsInfoConstant); @@ -365,12 +393,12 @@ export class LocalStorageService { * event is triggered. */ registerNewStorageEventListener(callbackFn: Function): void { - this.windowRef.nativeWindow.addEventListener('storage', (event) => { + this.windowRef.nativeWindow.addEventListener('storage', event => { callbackFn(event); }); } } -angular.module('oppia').factory( - 'LocalStorageService', - downgradeInjectable(LocalStorageService)); +angular + .module('oppia') + .factory('LocalStorageService', downgradeInjectable(LocalStorageService)); diff --git a/core/templates/services/math-interactions.service.spec.ts b/core/templates/services/math-interactions.service.spec.ts index 636fa38f0cd2..51d0b1b02067 100644 --- a/core/templates/services/math-interactions.service.spec.ts +++ b/core/templates/services/math-interactions.service.spec.ts @@ -16,9 +16,9 @@ * @fileoverview Unit test for MathInteractionsService */ -import { TestBed } from '@angular/core/testing'; +import {TestBed} from '@angular/core/testing'; -import { MathInteractionsService } from 'services/math-interactions.service'; +import {MathInteractionsService} from 'services/math-interactions.service'; describe('MathInteractionsService', () => { let mathInteractionsService: MathInteractionsService; @@ -27,639 +27,1112 @@ describe('MathInteractionsService', () => { mathInteractionsService = TestBed.get(MathInteractionsService); }); - it('should validate expressions correctly', function() { + it('should validate expressions correctly', function () { // Success cases. // Algebraic Expressions. - expect(mathInteractionsService.validateAlgebraicExpression( - 'a/2', ['a'])).toBeTrue(); + expect( + mathInteractionsService.validateAlgebraicExpression('a/2', ['a']) + ).toBeTrue(); expect(mathInteractionsService.getWarningText()).toBe(''); - expect(mathInteractionsService.validateAlgebraicExpression( - 'sqrt(alpha)', ['α'])).toBeTrue(); + expect( + mathInteractionsService.validateAlgebraicExpression('sqrt(alpha)', ['α']) + ).toBeTrue(); expect(mathInteractionsService.getWarningText()).toBe(''); - expect(mathInteractionsService.validateAlgebraicExpression( - 'a^2 + 2*a*b + b^2', ['a', 'b'])).toBeTrue(); + expect( + mathInteractionsService.validateAlgebraicExpression('a^2 + 2*a*b + b^2', [ + 'a', + 'b', + ]) + ).toBeTrue(); expect(mathInteractionsService.getWarningText()).toBe(''); - expect(mathInteractionsService.validateAlgebraicExpression( - '(a+b+c)^(-3.5)', ['a', 'b', 'c'])).toBeTrue(); + expect( + mathInteractionsService.validateAlgebraicExpression('(a+b+c)^(-3.5)', [ + 'a', + 'b', + 'c', + ]) + ).toBeTrue(); expect(mathInteractionsService.getWarningText()).toBe(''); - expect(mathInteractionsService.validateAlgebraicExpression( - '(alpha - beta)^pi', ['α', 'β', 'π'])).toBeTrue(); + expect( + mathInteractionsService.validateAlgebraicExpression('(alpha - beta)^pi', [ + 'α', + 'β', + 'π', + ]) + ).toBeTrue(); expect(mathInteractionsService.getWarningText()).toBe(''); - expect(mathInteractionsService.validateAlgebraicExpression( - '((-3.4)^(gamma/(y^2)))/2', ['y', 'γ'])).toBeTrue(); + expect( + mathInteractionsService.validateAlgebraicExpression( + '((-3.4)^(gamma/(y^2)))/2', + ['y', 'γ'] + ) + ).toBeTrue(); expect(mathInteractionsService.getWarningText()).toBe(''); - expect(mathInteractionsService.validateAlgebraicExpression( - 'a/b/c/d/e/f/g', ['a', 'b', 'c', 'd', 'e', 'f', 'g'])).toBeTrue(); + expect( + mathInteractionsService.validateAlgebraicExpression('a/b/c/d/e/f/g', [ + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', + 'g', + ]) + ).toBeTrue(); expect(mathInteractionsService.getWarningText()).toBe(''); - expect(mathInteractionsService.validateAlgebraicExpression( - 'a^(b-c)', ['a', 'b', 'c'])).toBeTrue(); + expect( + mathInteractionsService.validateAlgebraicExpression('a^(b-c)', [ + 'a', + 'b', + 'c', + ]) + ).toBeTrue(); // We need to ignore redundant parens here since guppy auto adds extra // parens while using exponents. - expect(mathInteractionsService.validateAlgebraicExpression( - '((a+b))^(2)', ['a', 'b'])).toBeTrue(); - expect(mathInteractionsService.validateAlgebraicExpression( - '((x)^(2))', ['x'])).toBeTrue(); - - expect(mathInteractionsService.validateAlgebraicExpression( - 'a+(-b+c)', ['a', 'b', 'c'])).toBeTrue(); - - expect(mathInteractionsService.validateAlgebraicExpression( - 'a-(b+c)', ['a', 'b', 'c'])).toBeTrue(); - - expect(mathInteractionsService.validateAlgebraicExpression( - 'a*(b-c)', ['a', 'b', 'c'])).toBeTrue(); - - expect(mathInteractionsService.validateAlgebraicExpression( - 'a+(b-c)/d', ['a', 'b', 'c', 'd', 'e'])).toBeTrue(); + expect( + mathInteractionsService.validateAlgebraicExpression('((a+b))^(2)', [ + 'a', + 'b', + ]) + ).toBeTrue(); + expect( + mathInteractionsService.validateAlgebraicExpression('((x)^(2))', ['x']) + ).toBeTrue(); + + expect( + mathInteractionsService.validateAlgebraicExpression('a+(-b+c)', [ + 'a', + 'b', + 'c', + ]) + ).toBeTrue(); + + expect( + mathInteractionsService.validateAlgebraicExpression('a-(b+c)', [ + 'a', + 'b', + 'c', + ]) + ).toBeTrue(); + + expect( + mathInteractionsService.validateAlgebraicExpression('a*(b-c)', [ + 'a', + 'b', + 'c', + ]) + ).toBeTrue(); + + expect( + mathInteractionsService.validateAlgebraicExpression('a+(b-c)/d', [ + 'a', + 'b', + 'c', + 'd', + 'e', + ]) + ).toBeTrue(); // Numeric Expressions. - expect(mathInteractionsService.validateNumericExpression( - '1/2')).toBeTrue(); + expect(mathInteractionsService.validateNumericExpression('1/2')).toBeTrue(); expect(mathInteractionsService.getWarningText()).toBe(''); - expect(mathInteractionsService.validateNumericExpression( - 'sqrt(49)')).toBeTrue(); + expect( + mathInteractionsService.validateNumericExpression('sqrt(49)') + ).toBeTrue(); expect(mathInteractionsService.getWarningText()).toBe(''); - expect(mathInteractionsService.validateNumericExpression( - '4^2 + 2*3*4 + 2^2')).toBeTrue(); + expect( + mathInteractionsService.validateNumericExpression('4^2 + 2*3*4 + 2^2') + ).toBeTrue(); expect(mathInteractionsService.getWarningText()).toBe(''); - expect(mathInteractionsService.validateNumericExpression( - '(1+2+3)^(-3.5)')).toBeTrue(); + expect( + mathInteractionsService.validateNumericExpression('(1+2+3)^(-3.5)') + ).toBeTrue(); expect(mathInteractionsService.getWarningText()).toBe(''); - expect(mathInteractionsService.validateNumericExpression( - '((-3.4)^(35/(2^2)))/2')).toBeTrue(); + expect( + mathInteractionsService.validateNumericExpression('((-3.4)^(35/(2^2)))/2') + ).toBeTrue(); expect(mathInteractionsService.getWarningText()).toBe(''); - expect(mathInteractionsService.validateNumericExpression( - '1/2/3/4/5/6/7')).toBeTrue(); + expect( + mathInteractionsService.validateNumericExpression('1/2/3/4/5/6/7') + ).toBeTrue(); expect(mathInteractionsService.getWarningText()).toBe(''); // Failure cases. - expect(mathInteractionsService.validateAlgebraicExpression( - '', [])).toBeFalse(); + expect( + mathInteractionsService.validateAlgebraicExpression('', []) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( - 'Please enter an answer before submitting.'); + 'Please enter an answer before submitting.' + ); - expect(mathInteractionsService.validateAlgebraicExpression( - '+', [])).toBeFalse(); + expect( + mathInteractionsService.validateAlgebraicExpression('+', []) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( - 'Your answer seems to be missing a variable/number after the "+".'); + 'Your answer seems to be missing a variable/number after the "+".' + ); - expect(mathInteractionsService.validateAlgebraicExpression( - '(+)', [])).toBeFalse(); + expect( + mathInteractionsService.validateAlgebraicExpression('(+)', []) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( - 'Your answer seems to be missing a variable/number after the "+".'); + 'Your answer seems to be missing a variable/number after the "+".' + ); - expect(mathInteractionsService.validateAlgebraicExpression( - 'a/', ['a'])).toBeFalse(); + expect( + mathInteractionsService.validateAlgebraicExpression('a/', ['a']) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( - 'Your answer seems to be missing a variable/number after the "/".'); + 'Your answer seems to be missing a variable/number after the "/".' + ); - expect(mathInteractionsService.validateAlgebraicExpression( - '(x-)3', ['x'])).toBeFalse(); + expect( + mathInteractionsService.validateAlgebraicExpression('(x-)3', ['x']) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( - 'Your answer seems to be missing a variable/number after the "-".'); + 'Your answer seems to be missing a variable/number after the "-".' + ); - expect(mathInteractionsService.validateAlgebraicExpression( - 'xy+c/2', ['x', 'y', 'z'])).toBeFalse(); + expect( + mathInteractionsService.validateAlgebraicExpression('xy+c/2', [ + 'x', + 'y', + 'z', + ]) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( 'You have entered an invalid variable: c. Please use only the ' + - 'variables x,y,z in your answer.'); + 'variables x,y,z in your answer.' + ); - expect(mathInteractionsService.validateAlgebraicExpression( - 'ae^2 + 4b', ['a', 'b'])).toBeFalse(); + expect( + mathInteractionsService.validateAlgebraicExpression('ae^2 + 4b', [ + 'a', + 'b', + ]) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( 'You have entered an invalid variable: e. Please use only the ' + - 'variables a,b in your answer.'); + 'variables a,b in your answer.' + ); - expect(mathInteractionsService.validateAlgebraicExpression( - 'xyz + pi', ['x', 'y', 'z'])).toBeFalse(); + expect( + mathInteractionsService.validateAlgebraicExpression('xyz + pi', [ + 'x', + 'y', + 'z', + ]) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( 'You have entered an invalid variable: π. Please use only the ' + - 'variables x,y,z in your answer.'); + 'variables x,y,z in your answer.' + ); - expect(mathInteractionsService.validateAlgebraicExpression( - 'aalpha/2beta', ['α', 'β', 'γ'])).toBeFalse(); + expect( + mathInteractionsService.validateAlgebraicExpression('aalpha/2beta', [ + 'α', + 'β', + 'γ', + ]) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( 'You have entered an invalid variable: a. Please use only the ' + - 'variables α,β,γ in your answer.'); + 'variables α,β,γ in your answer.' + ); - expect(mathInteractionsService.validateAlgebraicExpression( - '(x^3.5)^/2', ['x'])).toBeFalse(); + expect( + mathInteractionsService.validateAlgebraicExpression('(x^3.5)^/2', ['x']) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( - 'Your answer has two symbols next to each other: "^" and "/".'); + 'Your answer has two symbols next to each other: "^" and "/".' + ); - expect(mathInteractionsService.validateAlgebraicExpression( - 'sqrt() + x', ['x'])).toBeFalse(); + expect( + mathInteractionsService.validateAlgebraicExpression('sqrt() + x', ['x']) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( 'The sqrt function(s) cannot be empty. ' + - 'Please enter a variable/number in it.'); + 'Please enter a variable/number in it.' + ); - expect(mathInteractionsService.validateAlgebraicExpression( - 'sin()/x', ['x'])).toBeFalse(); + expect( + mathInteractionsService.validateAlgebraicExpression('sin()/x', ['x']) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( 'The sin function(s) cannot be empty. ' + - 'Please enter a variable/number in it.'); + 'Please enter a variable/number in it.' + ); - expect(mathInteractionsService.validateAlgebraicExpression( - 'tan()sin()', ['x'])).toBeFalse(); + expect( + mathInteractionsService.validateAlgebraicExpression('tan()sin()', ['x']) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( 'The sin, tan function(s) cannot be empty. ' + - 'Please enter a variable/number in it.'); + 'Please enter a variable/number in it.' + ); - expect(mathInteractionsService.validateAlgebraicExpression( - 'x-y=0', ['x', 'y'])).toBeFalse(); + expect( + mathInteractionsService.validateAlgebraicExpression('x-y=0', ['x', 'y']) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( - 'Please remove the equal sign to make ' + - 'your answer an expression.'); + 'Please remove the equal sign to make ' + 'your answer an expression.' + ); - expect(mathInteractionsService.validateAlgebraicExpression( - 'x^2 < 2.5', ['x'])).toBeFalse(); + expect( + mathInteractionsService.validateAlgebraicExpression('x^2 < 2.5', ['x']) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( - 'Please remove the equal sign to make ' + - 'your answer an expression.'); + 'Please remove the equal sign to make ' + 'your answer an expression.' + ); - expect(mathInteractionsService.validateAlgebraicExpression( - '5 >= 2*alpha', ['α'])).toBeFalse(); + expect( + mathInteractionsService.validateAlgebraicExpression('5 >= 2*alpha', ['α']) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( - 'Please remove the equal sign to make ' + - 'your answer an expression.'); + 'Please remove the equal sign to make ' + 'your answer an expression.' + ); - expect(mathInteractionsService.validateAlgebraicExpression( - '(x+y)/0', ['x', 'y'])).toBeFalse(); + expect( + mathInteractionsService.validateAlgebraicExpression('(x+y)/0', ['x', 'y']) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( - 'Your answer includes a division by zero, which is not valid.'); + 'Your answer includes a division by zero, which is not valid.' + ); - expect(mathInteractionsService.validateAlgebraicExpression( - '(x+y)/(y-y)', ['x', 'y'])).toBeFalse(); + expect( + mathInteractionsService.validateAlgebraicExpression('(x+y)/(y-y)', [ + 'x', + 'y', + ]) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( - 'Your answer includes a division by zero, which is not valid.'); + 'Your answer includes a division by zero, which is not valid.' + ); - expect(mathInteractionsService.validateAlgebraicExpression( - 'a)(b', ['a', 'b'])).toBeFalse(); + expect( + mathInteractionsService.validateAlgebraicExpression('a)(b', ['a', 'b']) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( - 'It looks like your answer has an invalid bracket pairing.'); + 'It looks like your answer has an invalid bracket pairing.' + ); - expect(mathInteractionsService.validateAlgebraicExpression( - 'a_2 + 3', ['a'])).toBeFalse(); + expect( + mathInteractionsService.validateAlgebraicExpression('a_2 + 3', ['a']) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( - 'Your answer contains an invalid character: "_".'); + 'Your answer contains an invalid character: "_".' + ); - expect(mathInteractionsService.validateAlgebraicExpression( - '3.4.5 + 45/a', ['a'])).toBeFalse(); + expect( + mathInteractionsService.validateAlgebraicExpression('3.4.5 + 45/a', ['a']) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( - 'Your answer contains an invalid term: 3.4.5'); + 'Your answer contains an invalid term: 3.4.5' + ); - expect(mathInteractionsService.validateAlgebraicExpression( - 'a4', ['a'])).toBeFalse(); + expect( + mathInteractionsService.validateAlgebraicExpression('a4', ['a']) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( 'When multiplying, the variable should come after the number: 4a. ' + - 'Please update your answer and try again.'); + 'Please update your answer and try again.' + ); - expect(mathInteractionsService.validateAlgebraicExpression( - 'a45', ['a'])).toBeFalse(); + expect( + mathInteractionsService.validateAlgebraicExpression('a45', ['a']) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( 'When multiplying, the variable should come after the number: 45a. ' + - 'Please update your answer and try again.'); + 'Please update your answer and try again.' + ); - expect(mathInteractionsService.validateAlgebraicExpression( - 'a * omega34', ['a', 'omega'])).toBeFalse(); + expect( + mathInteractionsService.validateAlgebraicExpression('a * omega34', [ + 'a', + 'omega', + ]) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( 'When multiplying, the variable should come after the number: 34omega. ' + - 'Please update your answer and try again.'); + 'Please update your answer and try again.' + ); - expect(mathInteractionsService.validateAlgebraicExpression( - 'x^y^z', ['x', 'y', 'z'])).toBeFalse(); + expect( + mathInteractionsService.validateAlgebraicExpression('x^y^z', [ + 'x', + 'y', + 'z', + ]) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( 'Your expression contains an exponent in an exponent which is not ' + - 'supported.'); + 'supported.' + ); - expect(mathInteractionsService.validateAlgebraicExpression( - '3^4^5', ['a', 'x', 'y', 'z'])).toBeFalse(); + expect( + mathInteractionsService.validateAlgebraicExpression('3^4^5', [ + 'a', + 'x', + 'y', + 'z', + ]) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( 'Your expression contains an exponent in an exponent which is not ' + - 'supported.'); + 'supported.' + ); - expect(mathInteractionsService.validateAlgebraicExpression( - 'x^(y^z)', ['x', 'y', 'z'])).toBeFalse(); + expect( + mathInteractionsService.validateAlgebraicExpression('x^(y^z)', [ + 'x', + 'y', + 'z', + ]) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( 'Your expression contains an exponent in an exponent which is not ' + - 'supported.'); + 'supported.' + ); - expect(mathInteractionsService.validateAlgebraicExpression( - 'x^(y*a^z)', ['a', 'x', 'y', 'z'])).toBeFalse(); + expect( + mathInteractionsService.validateAlgebraicExpression('x^(y*a^z)', [ + 'a', + 'x', + 'y', + 'z', + ]) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( 'Your expression contains an exponent in an exponent which is not ' + - 'supported.'); + 'supported.' + ); - expect(mathInteractionsService.validateAlgebraicExpression( - 'a^6', ['a'])).toBeFalse(); + expect( + mathInteractionsService.validateAlgebraicExpression('a^6', ['a']) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( 'Your expression contains an exponent with value greater than 5 ' + - 'which is not supported.'); + 'which is not supported.' + ); - expect(mathInteractionsService.validateAlgebraicExpression( - 'a^(3+4)', ['a'])).toBeFalse(); + expect( + mathInteractionsService.validateAlgebraicExpression('a^(3+4)', ['a']) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( 'Your expression contains an exponent with value greater than 5 ' + - 'which is not supported.'); + 'which is not supported.' + ); - expect(mathInteractionsService.validateNumericExpression( - '5^100')).toBeFalse(); + expect( + mathInteractionsService.validateNumericExpression('5^100') + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( 'Your expression contains an exponent with value greater than 5 ' + - 'which is not supported.'); + 'which is not supported.' + ); - expect(mathInteractionsService.validateNumericExpression( - '((x)^(6))')).toBeFalse(); + expect( + mathInteractionsService.validateNumericExpression('((x)^(6))') + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( 'Your expression contains an exponent with value greater than 5 ' + - 'which is not supported.'); + 'which is not supported.' + ); - expect(mathInteractionsService.validateAlgebraicExpression( - 'a+((b-c))', ['a', 'b', 'c'])).toBeFalse(); + expect( + mathInteractionsService.validateAlgebraicExpression('a+((b-c))', [ + 'a', + 'b', + 'c', + ]) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( 'Your expression contains redundant parentheses: ((b-c)).' ); - expect(mathInteractionsService.validateAlgebraicExpression( - '(((a + b))) + c', ['a', 'b', 'c'])).toBeFalse(); + expect( + mathInteractionsService.validateAlgebraicExpression('(((a + b))) + c', [ + 'a', + 'b', + 'c', + ]) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( 'Your expression contains redundant parentheses: ((a+b)).' ); - expect(mathInteractionsService.validateNumericExpression( - 'a/2')).toBeFalse(); + expect( + mathInteractionsService.validateNumericExpression('a/2') + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( 'It looks like you have entered some variables. ' + - 'Please enter numbers only.'); + 'Please enter numbers only.' + ); - expect(mathInteractionsService.validateNumericExpression( - 'sqrt(alpha/beta)')).toBeFalse(); + expect( + mathInteractionsService.validateNumericExpression('sqrt(alpha/beta)') + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( 'It looks like you have entered some variables. ' + - 'Please enter numbers only.'); + 'Please enter numbers only.' + ); }); - it('should validate equations correctly', function() { + it('should validate equations correctly', function () { // Success cases. - expect(mathInteractionsService.validateEquation( - 'x=y', ['x', 'y'])).toBeTrue(); + expect( + mathInteractionsService.validateEquation('x=y', ['x', 'y']) + ).toBeTrue(); expect(mathInteractionsService.getWarningText()).toBe(''); - expect(mathInteractionsService.validateEquation( - 'sqrt(alpha) = -1', ['α'])).toBeTrue(); + expect( + mathInteractionsService.validateEquation('sqrt(alpha) = -1', ['α']) + ).toBeTrue(); expect(mathInteractionsService.getWarningText()).toBe(''); - expect(mathInteractionsService.validateEquation( - 'x + y - 12^3 = 0', ['x', 'y'])).toBeTrue(); + expect( + mathInteractionsService.validateEquation('x + y - 12^3 = 0', ['x', 'y']) + ).toBeTrue(); expect(mathInteractionsService.getWarningText()).toBe(''); - expect(mathInteractionsService.validateEquation( - '(a+b+c)^(-3.5) = (-3.5)^(a+b+c)', ['a', 'b', 'c'])).toBeTrue(); + expect( + mathInteractionsService.validateEquation( + '(a+b+c)^(-3.5) = (-3.5)^(a+b+c)', + ['a', 'b', 'c'] + ) + ).toBeTrue(); expect(mathInteractionsService.getWarningText()).toBe(''); - expect(mathInteractionsService.validateEquation( - 'y = m*x + c', ['y', 'm', 'x', 'c'])).toBeTrue(); + expect( + mathInteractionsService.validateEquation('y = m*x + c', [ + 'y', + 'm', + 'x', + 'c', + ]) + ).toBeTrue(); expect(mathInteractionsService.getWarningText()).toBe(''); - expect(mathInteractionsService.validateEquation( - 'T = t*(1/sqrt(1-(v^2)/(c^2)))', ['T', 't', 'v', 'c'])).toBeTrue(); + expect( + mathInteractionsService.validateEquation( + 'T = t*(1/sqrt(1-(v^2)/(c^2)))', + ['T', 't', 'v', 'c'] + ) + ).toBeTrue(); expect(mathInteractionsService.getWarningText()).toBe(''); // Failure cases. expect(mathInteractionsService.validateEquation('', [])).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( - 'Please enter an answer before submitting.'); + 'Please enter an answer before submitting.' + ); - expect(mathInteractionsService.validateEquation( - 'a+b = ', ['a', 'b'])).toBeFalse(); + expect( + mathInteractionsService.validateEquation('a+b = ', ['a', 'b']) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( - 'The RHS of your equation is empty.'); + 'The RHS of your equation is empty.' + ); - expect(mathInteractionsService.validateEquation( - ' =(x-y)/2', ['x', 'y'])).toBeFalse(); + expect( + mathInteractionsService.validateEquation(' =(x-y)/2', ['x', 'y']) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( - 'The LHS of your equation is empty.'); + 'The LHS of your equation is empty.' + ); - expect(mathInteractionsService.validateEquation( - 'a=b=c', ['a', 'b', 'c'])).toBeFalse(); + expect( + mathInteractionsService.validateEquation('a=b=c', ['a', 'b', 'c']) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( - 'Your equation contains multiple = signs.'); + 'Your equation contains multiple = signs.' + ); - expect(mathInteractionsService.validateEquation( - 'a==b', ['a', 'b'])).toBeFalse(); + expect( + mathInteractionsService.validateEquation('a==b', ['a', 'b']) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( - 'Your equation contains multiple = signs.'); + 'Your equation contains multiple = signs.' + ); - expect(mathInteractionsService.validateEquation( - 'a+b=0=0', ['a', 'b'])).toBeFalse(); + expect( + mathInteractionsService.validateEquation('a+b=0=0', ['a', 'b']) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( - 'Your equation contains multiple = signs.'); + 'Your equation contains multiple = signs.' + ); - expect(mathInteractionsService.validateEquation( - 'a/ = (-5)', ['a'])).toBeFalse(); + expect( + mathInteractionsService.validateEquation('a/ = (-5)', ['a']) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( - 'Your answer seems to be missing a variable/number after the "/".'); + 'Your answer seems to be missing a variable/number after the "/".' + ); - expect(mathInteractionsService.validateEquation( - '(x-)3 = 2.5', ['x'])).toBeFalse(); + expect( + mathInteractionsService.validateEquation('(x-)3 = 2.5', ['x']) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( - 'Your answer seems to be missing a variable/number after the "-".'); + 'Your answer seems to be missing a variable/number after the "-".' + ); - expect(mathInteractionsService.validateEquation( - '(x^3.5)^/2 = 0', ['x'])).toBeFalse(); + expect( + mathInteractionsService.validateEquation('(x^3.5)^/2 = 0', ['x']) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( - 'Your answer has two symbols next to each other: "^" and "/".'); + 'Your answer has two symbols next to each other: "^" and "/".' + ); - expect(mathInteractionsService.validateEquation( - '12 = sqrt(144)', [])).toBeFalse(); + expect( + mathInteractionsService.validateEquation('12 = sqrt(144)', []) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( - 'The equation must contain at least one variable.'); + 'The equation must contain at least one variable.' + ); - expect(mathInteractionsService.validateEquation( - 'x^2 < 2.5', ['x'])).toBeFalse(); + expect( + mathInteractionsService.validateEquation('x^2 < 2.5', ['x']) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( 'It looks like you have entered an inequality. ' + - 'Please enter an equation instead.'); + 'Please enter an equation instead.' + ); - expect(mathInteractionsService.validateEquation( - '5 >= 2*alpha', ['α'])).toBeFalse(); + expect( + mathInteractionsService.validateEquation('5 >= 2*alpha', ['α']) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( 'It looks like you have entered an inequality. ' + - 'Please enter an equation instead.'); + 'Please enter an equation instead.' + ); - expect(mathInteractionsService.validateEquation( - '2*x^2 + 3', ['x'])).toBeFalse(); + expect( + mathInteractionsService.validateEquation('2*x^2 + 3', ['x']) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( 'It looks like you have entered an expression. ' + - 'Please enter an equation instead.'); + 'Please enter an equation instead.' + ); - expect(mathInteractionsService.validateEquation( - '(x+y)/0 = 5', ['x', 'y'])).toBeFalse(); + expect( + mathInteractionsService.validateEquation('(x+y)/0 = 5', ['x', 'y']) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( - 'Your answer includes a division by zero, which is not valid.'); + 'Your answer includes a division by zero, which is not valid.' + ); - expect(mathInteractionsService.validateEquation( - '(x+y)/(y-y) = 3*x^2', ['x', 'y'])).toBeFalse(); + expect( + mathInteractionsService.validateEquation('(x+y)/(y-y) = 3*x^2', [ + 'x', + 'y', + ]) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( - 'Your answer includes a division by zero, which is not valid.'); + 'Your answer includes a division by zero, which is not valid.' + ); - expect(mathInteractionsService.validateEquation( - 'a)(b = x', ['a', 'b', 'x'])).toBeFalse(); + expect( + mathInteractionsService.validateEquation('a)(b = x', ['a', 'b', 'x']) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( - 'It looks like your answer has an invalid bracket pairing.'); + 'It looks like your answer has an invalid bracket pairing.' + ); - expect(mathInteractionsService.validateEquation( - '3.4.5 = 45/a', ['a'])).toBeFalse(); + expect( + mathInteractionsService.validateEquation('3.4.5 = 45/a', ['a']) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( - 'Your answer contains an invalid term: 3.4.5'); + 'Your answer contains an invalid term: 3.4.5' + ); - expect(mathInteractionsService.validateEquation( - 'y=mx+b', ['x', 'y', 'm', 'c'])).toBeFalse(); + expect( + mathInteractionsService.validateEquation('y=mx+b', ['x', 'y', 'm', 'c']) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( 'You have entered an invalid variable: b. Please use only the ' + - 'variables x,y,m,c in your answer.'); + 'variables x,y,m,c in your answer.' + ); - expect(mathInteractionsService.validateEquation( - 'alpha(x^2)=beta/2', ['α', 'β', 'γ'])).toBeFalse(); + expect( + mathInteractionsService.validateEquation('alpha(x^2)=beta/2', [ + 'α', + 'β', + 'γ', + ]) + ).toBeFalse(); expect(mathInteractionsService.getWarningText()).toBe( 'You have entered an invalid variable: x. Please use only the ' + - 'variables α,β,γ in your answer.'); + 'variables α,β,γ in your answer.' + ); }); - it('should insert missing multiplication signs', function() { - expect(mathInteractionsService.insertMultiplicationSigns( - 'ab/2')).toBe('a*b/2'); - expect(mathInteractionsService.insertMultiplicationSigns( - '5ab/2')).toBe('5*a*b/2'); - expect(mathInteractionsService.insertMultiplicationSigns( - '3alpha+ax^2')).toBe('3*alpha+a*x^2'); - expect(mathInteractionsService.insertMultiplicationSigns( - 'sqrt(xyz)')).toBe('sqrt(x*y*z)'); - expect(mathInteractionsService.insertMultiplicationSigns( - 'ax^2+2*ab+b^2')).toBe('a*x^2+2*a*b+b^2'); - expect(mathInteractionsService.insertMultiplicationSigns( - 'cos(theta/ab)+sin(xy)')).toBe('cos(theta/a*b)+sin(x*y)'); - expect(mathInteractionsService.insertMultiplicationSigns( - 'log(alpha/pi)')).toBe('log(alpha/pi)'); - expect(mathInteractionsService.insertMultiplicationSigns( - 'Al^2')).toBe('A*l^2'); - expect(mathInteractionsService.insertMultiplicationSigns( - 'a(b)/2')).toBe('a*(b)/2'); - expect(mathInteractionsService.insertMultiplicationSigns( - '(a)b/2')).toBe('(a)*b/2'); - expect(mathInteractionsService.insertMultiplicationSigns( - '(a)(b)/2')).toBe('(a)*(b)/2'); - expect(mathInteractionsService.insertMultiplicationSigns( - '5sqrt(4)')).toBe('5*sqrt(4)'); - expect(mathInteractionsService.insertMultiplicationSigns( - 'cos(theta)sin(theta)')).toBe('cos(theta)*sin(theta)'); - expect(mathInteractionsService.insertMultiplicationSigns( - 'sqrt(4)abs(5)')).toBe('sqrt(4)*abs(5)'); - expect(mathInteractionsService.insertMultiplicationSigns( - '(3+alpha)(3-alpha)4')).toBe('(3+alpha)*(3-alpha)*4'); - expect(mathInteractionsService.insertMultiplicationSigns( - 'alphabeta gamma')).toBe('alpha*beta*gamma'); - expect(mathInteractionsService.insertMultiplicationSigns( - 'xalphayzgamma')).toBe('x*alpha*y*z*gamma'); + it('should insert missing multiplication signs', function () { + expect(mathInteractionsService.insertMultiplicationSigns('ab/2')).toBe( + 'a*b/2' + ); + expect(mathInteractionsService.insertMultiplicationSigns('5ab/2')).toBe( + '5*a*b/2' + ); + expect( + mathInteractionsService.insertMultiplicationSigns('3alpha+ax^2') + ).toBe('3*alpha+a*x^2'); + expect(mathInteractionsService.insertMultiplicationSigns('sqrt(xyz)')).toBe( + 'sqrt(x*y*z)' + ); + expect( + mathInteractionsService.insertMultiplicationSigns('ax^2+2*ab+b^2') + ).toBe('a*x^2+2*a*b+b^2'); + expect( + mathInteractionsService.insertMultiplicationSigns('cos(theta/ab)+sin(xy)') + ).toBe('cos(theta/a*b)+sin(x*y)'); + expect( + mathInteractionsService.insertMultiplicationSigns('log(alpha/pi)') + ).toBe('log(alpha/pi)'); + expect(mathInteractionsService.insertMultiplicationSigns('Al^2')).toBe( + 'A*l^2' + ); + expect(mathInteractionsService.insertMultiplicationSigns('a(b)/2')).toBe( + 'a*(b)/2' + ); + expect(mathInteractionsService.insertMultiplicationSigns('(a)b/2')).toBe( + '(a)*b/2' + ); + expect(mathInteractionsService.insertMultiplicationSigns('(a)(b)/2')).toBe( + '(a)*(b)/2' + ); + expect(mathInteractionsService.insertMultiplicationSigns('5sqrt(4)')).toBe( + '5*sqrt(4)' + ); + expect( + mathInteractionsService.insertMultiplicationSigns('cos(theta)sin(theta)') + ).toBe('cos(theta)*sin(theta)'); + expect( + mathInteractionsService.insertMultiplicationSigns('sqrt(4)abs(5)') + ).toBe('sqrt(4)*abs(5)'); + expect( + mathInteractionsService.insertMultiplicationSigns('(3+alpha)(3-alpha)4') + ).toBe('(3+alpha)*(3-alpha)*4'); + expect( + mathInteractionsService.insertMultiplicationSigns('alphabeta gamma') + ).toBe('alpha*beta*gamma'); + expect( + mathInteractionsService.insertMultiplicationSigns('xalphayzgamma') + ).toBe('x*alpha*y*z*gamma'); }); - it('should replace abs symbol with text', function() { - expect(mathInteractionsService.replaceAbsSymbolWithText( - '|x|')).toBe('abs(x)'); - expect(mathInteractionsService.replaceAbsSymbolWithText( - '40alpha/|beta|')).toBe('40alpha/abs(beta)'); - expect(mathInteractionsService.replaceAbsSymbolWithText( - 'abs(xyz)')).toBe('abs(xyz)'); - expect(mathInteractionsService.replaceAbsSymbolWithText( - '|sqrt(a+b^2)|')).toBe('abs(sqrt(a+b^2))'); - expect(mathInteractionsService.replaceAbsSymbolWithText( - '||')).toBe('abs()'); + it('should replace abs symbol with text', function () { + expect(mathInteractionsService.replaceAbsSymbolWithText('|x|')).toBe( + 'abs(x)' + ); + expect( + mathInteractionsService.replaceAbsSymbolWithText('40alpha/|beta|') + ).toBe('40alpha/abs(beta)'); + expect(mathInteractionsService.replaceAbsSymbolWithText('abs(xyz)')).toBe( + 'abs(xyz)' + ); + expect( + mathInteractionsService.replaceAbsSymbolWithText('|sqrt(a+b^2)|') + ).toBe('abs(sqrt(a+b^2))'); + expect(mathInteractionsService.replaceAbsSymbolWithText('||')).toBe( + 'abs()' + ); }); - it('should get terms from given expression', function() { + it('should get terms from given expression', function () { // Split by addition. - expect(mathInteractionsService.getTerms('3+4*a')).toEqual( - ['3', '4*a']); - expect(mathInteractionsService.getTerms('4-(-beta)')).toEqual( - ['4', '-((-beta))']); - expect(mathInteractionsService.getTerms('3*10^(-1)')).toEqual( - ['3*10^(-1)']); - expect(mathInteractionsService.getTerms('a-x+4.5')).toEqual( - ['a', '-(x)', '4.5']); - expect(mathInteractionsService.getTerms('4----5')).toEqual( - ['4', '-(---5)']); - expect(mathInteractionsService.getTerms('100 + 20 + 3')).toEqual( - ['100', '20', '3']); - expect(mathInteractionsService.getTerms('4-sqrt(x + alpha)')).toEqual( - ['4', '-(sqrt(x+alpha))']); - expect(mathInteractionsService.getTerms('a^2+b^2+2*a*b')).toEqual( - ['a^2', 'b^2', '2*a*b']); - expect(mathInteractionsService.getTerms('pi/(4+3)')).toEqual( - ['pi/(4+3)']); - expect(mathInteractionsService.getTerms('tan(30)-(-cos(60))')).toEqual( - ['tan(30)', '-((-cos(60)))']); + expect(mathInteractionsService.getTerms('3+4*a')).toEqual(['3', '4*a']); + expect(mathInteractionsService.getTerms('4-(-beta)')).toEqual([ + '4', + '-((-beta))', + ]); + expect(mathInteractionsService.getTerms('3*10^(-1)')).toEqual([ + '3*10^(-1)', + ]); + expect(mathInteractionsService.getTerms('a-x+4.5')).toEqual([ + 'a', + '-(x)', + '4.5', + ]); + expect(mathInteractionsService.getTerms('4----5')).toEqual([ + '4', + '-(---5)', + ]); + expect(mathInteractionsService.getTerms('100 + 20 + 3')).toEqual([ + '100', + '20', + '3', + ]); + expect(mathInteractionsService.getTerms('4-sqrt(x + alpha)')).toEqual([ + '4', + '-(sqrt(x+alpha))', + ]); + expect(mathInteractionsService.getTerms('a^2+b^2+2*a*b')).toEqual([ + 'a^2', + 'b^2', + '2*a*b', + ]); + expect(mathInteractionsService.getTerms('pi/(4+3)')).toEqual(['pi/(4+3)']); + expect(mathInteractionsService.getTerms('tan(30)-(-cos(60))')).toEqual([ + 'tan(30)', + '-((-cos(60)))', + ]); // Split by multiplication. - expect(mathInteractionsService.getTerms('4*a', false)).toEqual( - ['4', 'a']); - expect(mathInteractionsService.getTerms('4/beta', false)).toEqual( - ['4', '(beta)^(-1)']); - expect(mathInteractionsService.getTerms('3*10^(-1)', false)).toEqual( - ['3', '10^(-1)']); - expect(mathInteractionsService.getTerms('(a)/((x)/(3))', false)).toEqual( - ['(a)', '(((x)/(3)))^(-1)']); - expect(mathInteractionsService.getTerms('2*2*3*4', false)).toEqual( - ['2', '2', '3', '4']); - expect(mathInteractionsService.getTerms('100 + 20 + 3', false)).toEqual( - ['100+20+3']); - expect(mathInteractionsService.getTerms('4/sqrt(x+alpha)', false)).toEqual( - ['4', '(sqrt(x+alpha))^(-1)']); - expect(mathInteractionsService.getTerms('(x+y)*(x-y)', false)).toEqual( - ['(x+y)', '(x-y)']); - expect(mathInteractionsService.getTerms('pi/(4+3)', false)).toEqual( - ['pi', '((4+3))^(-1)']); + expect(mathInteractionsService.getTerms('4*a', false)).toEqual(['4', 'a']); + expect(mathInteractionsService.getTerms('4/beta', false)).toEqual([ + '4', + '(beta)^(-1)', + ]); + expect(mathInteractionsService.getTerms('3*10^(-1)', false)).toEqual([ + '3', + '10^(-1)', + ]); + expect(mathInteractionsService.getTerms('(a)/((x)/(3))', false)).toEqual([ + '(a)', + '(((x)/(3)))^(-1)', + ]); + expect(mathInteractionsService.getTerms('2*2*3*4', false)).toEqual([ + '2', + '2', + '3', + '4', + ]); + expect(mathInteractionsService.getTerms('100 + 20 + 3', false)).toEqual([ + '100+20+3', + ]); + expect(mathInteractionsService.getTerms('4/sqrt(x+alpha)', false)).toEqual([ + '4', + '(sqrt(x+alpha))^(-1)', + ]); + expect(mathInteractionsService.getTerms('(x+y)*(x-y)', false)).toEqual([ + '(x+y)', + '(x-y)', + ]); + expect(mathInteractionsService.getTerms('pi/(4+3)', false)).toEqual([ + 'pi', + '((4+3))^(-1)', + ]); }); - it('should correctly match terms', function() { + it('should correctly match terms', function () { expect(mathInteractionsService.doTermsMatch('4*5', '5*4')).toBeTrue(); expect(mathInteractionsService.doTermsMatch('2*pi*r', 'r*pi*2')).toBeTrue(); - expect(mathInteractionsService.doTermsMatch( - 'x*(y+z)', '(y+z)*x')).toBeTrue(); - expect(mathInteractionsService.doTermsMatch( - 'x*(y+z)*(3-alpha)/2', '(3-alpha)/2*(z+y)*x')).toBeTrue(); + expect( + mathInteractionsService.doTermsMatch('x*(y+z)', '(y+z)*x') + ).toBeTrue(); + expect( + mathInteractionsService.doTermsMatch( + 'x*(y+z)*(3-alpha)/2', + '(3-alpha)/2*(z+y)*x' + ) + ).toBeTrue(); expect(mathInteractionsService.doTermsMatch('4*5', '20')).toBeFalse(); expect(mathInteractionsService.doTermsMatch('3*10^2', '300')).toBeFalse(); expect(mathInteractionsService.doTermsMatch('1/3', '3^(-1)')).toBeFalse(); - expect(mathInteractionsService.doTermsMatch( - 'pi*r^2', '(pi*r^3)/r')).toBeFalse(); + expect( + mathInteractionsService.doTermsMatch('pi*r^2', '(pi*r^3)/r') + ).toBeFalse(); expect(mathInteractionsService.doTermsMatch('1/3', '0.333')).toBeFalse(); expect(mathInteractionsService.doTermsMatch('4*(5+3)', '32')).toBeFalse(); expect(mathInteractionsService.doTermsMatch('sqrt(x^2)', 'x')).toBeFalse(); - expect(mathInteractionsService.doTermsMatch( - '3*10^2', '3/10^(-2)')).toBeFalse(); + expect( + mathInteractionsService.doTermsMatch('3*10^2', '3/10^(-2)') + ).toBeFalse(); expect(mathInteractionsService.doTermsMatch('sqrt(4)', '2')).toBeFalse(); expect(mathInteractionsService.doTermsMatch('abs(-4)', '4')).toBeFalse(); - expect(mathInteractionsService.doTermsMatch( - 'sqrt(x^2)', 'abs(x)')).toBeFalse(); - expect(mathInteractionsService.doTermsMatch( - '(x^2)/2', '(x*x)/2')).toBeFalse(); - expect(mathInteractionsService.doTermsMatch( - '2*4.5', '(9/2)*2')).toBeFalse(); + expect( + mathInteractionsService.doTermsMatch('sqrt(x^2)', 'abs(x)') + ).toBeFalse(); + expect( + mathInteractionsService.doTermsMatch('(x^2)/2', '(x*x)/2') + ).toBeFalse(); + expect( + mathInteractionsService.doTermsMatch('2*4.5', '(9/2)*2') + ).toBeFalse(); }); - it('should correctly match terms with placeholders', function() { + it('should correctly match terms with placeholders', function () { let expressionWithPlaceholders = 'a*x + b'; - expect(mathInteractionsService.expressionMatchWithPlaceholders( - expressionWithPlaceholders, '2x + 3', ['a', 'b'])).toBeTrue(); - expect(mathInteractionsService.expressionMatchWithPlaceholders( - expressionWithPlaceholders, '3 + 4x', ['a', 'b'])).toBeTrue(); - expect(mathInteractionsService.expressionMatchWithPlaceholders( - expressionWithPlaceholders, '-3 + 4x', ['a', 'b'])).toBeTrue(); - expect(mathInteractionsService.expressionMatchWithPlaceholders( - expressionWithPlaceholders, '3 - 4.5x', ['a', 'b'])).toBeTrue(); - expect(mathInteractionsService.expressionMatchWithPlaceholders( - expressionWithPlaceholders, '3 + x*5/2', ['a', 'b'])).toBeTrue(); - expect(mathInteractionsService.expressionMatchWithPlaceholders( - expressionWithPlaceholders, '3^5 + 4x', ['a', 'b'])).toBeTrue(); - expect(mathInteractionsService.expressionMatchWithPlaceholders( - expressionWithPlaceholders, 'x + 5/2', ['a', 'b'])).toBeTrue(); - expect(mathInteractionsService.expressionMatchWithPlaceholders( - expressionWithPlaceholders, '0 + x', ['a', 'b'])).toBeTrue(); - - expect(mathInteractionsService.expressionMatchWithPlaceholders( - expressionWithPlaceholders, '4x', ['a', 'b'])).toBeFalse(); - expect(mathInteractionsService.expressionMatchWithPlaceholders( - expressionWithPlaceholders, '4x^2', ['a', 'b'])).toBeFalse(); - expect(mathInteractionsService.expressionMatchWithPlaceholders( - expressionWithPlaceholders, '4a + 3', ['a', 'b'])).toBeFalse(); - expect(mathInteractionsService.expressionMatchWithPlaceholders( - expressionWithPlaceholders, 'ax + b', ['a', 'b'])).toBeFalse(); - expect(mathInteractionsService.expressionMatchWithPlaceholders( - expressionWithPlaceholders, '3x^2 + 2', ['a', 'b'])).toBeFalse(); - expect(mathInteractionsService.expressionMatchWithPlaceholders( - expressionWithPlaceholders, '5x + 4 + 5', ['a', 'b'])).toBeFalse(); - expect(mathInteractionsService.expressionMatchWithPlaceholders( - expressionWithPlaceholders, '3x + 2y + 4', ['a', 'b'])).toBeFalse(); - expect(mathInteractionsService.expressionMatchWithPlaceholders( - expressionWithPlaceholders, 'ax + 3', ['a', 'b'])).toBeFalse(); - expect(mathInteractionsService.expressionMatchWithPlaceholders( - expressionWithPlaceholders, '3x + b', ['a', 'b'])).toBeFalse(); - expect(mathInteractionsService.expressionMatchWithPlaceholders( - expressionWithPlaceholders, '4x + 5 + b', ['a', 'b'])).toBeFalse(); - + expect( + mathInteractionsService.expressionMatchWithPlaceholders( + expressionWithPlaceholders, + '2x + 3', + ['a', 'b'] + ) + ).toBeTrue(); + expect( + mathInteractionsService.expressionMatchWithPlaceholders( + expressionWithPlaceholders, + '3 + 4x', + ['a', 'b'] + ) + ).toBeTrue(); + expect( + mathInteractionsService.expressionMatchWithPlaceholders( + expressionWithPlaceholders, + '-3 + 4x', + ['a', 'b'] + ) + ).toBeTrue(); + expect( + mathInteractionsService.expressionMatchWithPlaceholders( + expressionWithPlaceholders, + '3 - 4.5x', + ['a', 'b'] + ) + ).toBeTrue(); + expect( + mathInteractionsService.expressionMatchWithPlaceholders( + expressionWithPlaceholders, + '3 + x*5/2', + ['a', 'b'] + ) + ).toBeTrue(); + expect( + mathInteractionsService.expressionMatchWithPlaceholders( + expressionWithPlaceholders, + '3^5 + 4x', + ['a', 'b'] + ) + ).toBeTrue(); + expect( + mathInteractionsService.expressionMatchWithPlaceholders( + expressionWithPlaceholders, + 'x + 5/2', + ['a', 'b'] + ) + ).toBeTrue(); + expect( + mathInteractionsService.expressionMatchWithPlaceholders( + expressionWithPlaceholders, + '0 + x', + ['a', 'b'] + ) + ).toBeTrue(); + + expect( + mathInteractionsService.expressionMatchWithPlaceholders( + expressionWithPlaceholders, + '4x', + ['a', 'b'] + ) + ).toBeFalse(); + expect( + mathInteractionsService.expressionMatchWithPlaceholders( + expressionWithPlaceholders, + '4x^2', + ['a', 'b'] + ) + ).toBeFalse(); + expect( + mathInteractionsService.expressionMatchWithPlaceholders( + expressionWithPlaceholders, + '4a + 3', + ['a', 'b'] + ) + ).toBeFalse(); + expect( + mathInteractionsService.expressionMatchWithPlaceholders( + expressionWithPlaceholders, + 'ax + b', + ['a', 'b'] + ) + ).toBeFalse(); + expect( + mathInteractionsService.expressionMatchWithPlaceholders( + expressionWithPlaceholders, + '3x^2 + 2', + ['a', 'b'] + ) + ).toBeFalse(); + expect( + mathInteractionsService.expressionMatchWithPlaceholders( + expressionWithPlaceholders, + '5x + 4 + 5', + ['a', 'b'] + ) + ).toBeFalse(); + expect( + mathInteractionsService.expressionMatchWithPlaceholders( + expressionWithPlaceholders, + '3x + 2y + 4', + ['a', 'b'] + ) + ).toBeFalse(); + expect( + mathInteractionsService.expressionMatchWithPlaceholders( + expressionWithPlaceholders, + 'ax + 3', + ['a', 'b'] + ) + ).toBeFalse(); + expect( + mathInteractionsService.expressionMatchWithPlaceholders( + expressionWithPlaceholders, + '3x + b', + ['a', 'b'] + ) + ).toBeFalse(); + expect( + mathInteractionsService.expressionMatchWithPlaceholders( + expressionWithPlaceholders, + '4x + 5 + b', + ['a', 'b'] + ) + ).toBeFalse(); expressionWithPlaceholders = 'x/alpha + y/beta'; - expect(mathInteractionsService.expressionMatchWithPlaceholders( - expressionWithPlaceholders, 'x/2 + y/3', ['alpha', 'beta'])).toBeTrue(); - expect(mathInteractionsService.expressionMatchWithPlaceholders( - expressionWithPlaceholders, 'y/2 + x/3', ['alpha', 'beta'])).toBeTrue(); - expect(mathInteractionsService.expressionMatchWithPlaceholders( - expressionWithPlaceholders, '4x/2.5 + y', ['alpha', 'beta'])).toBeTrue(); - expect(mathInteractionsService.expressionMatchWithPlaceholders( - expressionWithPlaceholders, 'x + y', ['alpha', 'beta'])).toBeTrue(); - expect(mathInteractionsService.expressionMatchWithPlaceholders( - expressionWithPlaceholders, 'x/5 - y/2', ['alpha', 'beta'])).toBeTrue(); - expect(mathInteractionsService.expressionMatchWithPlaceholders( - expressionWithPlaceholders, '-x/2 + 3y', ['alpha', 'beta'])).toBeTrue(); - expect(mathInteractionsService.expressionMatchWithPlaceholders( - expressionWithPlaceholders, 'y - 8x', ['alpha', 'beta'])).toBeTrue(); - expect(mathInteractionsService.expressionMatchWithPlaceholders( - expressionWithPlaceholders, - 'x/3 + y/(8/22)', ['alpha', 'beta'])).toBeTrue(); - - expect(mathInteractionsService.expressionMatchWithPlaceholders( - expressionWithPlaceholders, - '(x^2)/4 + y/2', ['alpha', 'beta'])).toBeFalse(); - expect(mathInteractionsService.expressionMatchWithPlaceholders( - expressionWithPlaceholders, 'x/5', ['alpha', 'beta'])).toBeFalse(); - expect(mathInteractionsService.expressionMatchWithPlaceholders( - expressionWithPlaceholders, - 'x/2 + y/3 - 5', ['alpha', 'beta'])).toBeFalse(); - expect(mathInteractionsService.expressionMatchWithPlaceholders( - expressionWithPlaceholders, 'x', ['alpha', 'beta'])).toBeFalse(); - expect(mathInteractionsService.expressionMatchWithPlaceholders( - expressionWithPlaceholders, - 'x/alpha + y/2', ['alpha', 'beta'])).toBeFalse(); - expect(mathInteractionsService.expressionMatchWithPlaceholders( - expressionWithPlaceholders, - 'x/2 + y/5 + 2x/2', ['alpha', 'beta'])).toBeFalse(); - expect(mathInteractionsService.expressionMatchWithPlaceholders( - expressionWithPlaceholders, 'z/2 + y/3', ['alpha', 'beta'])).toBeFalse(); - expect(mathInteractionsService.expressionMatchWithPlaceholders( - expressionWithPlaceholders, - 'x/(x+1) + y/8', ['alpha', 'beta'])).toBeFalse(); + expect( + mathInteractionsService.expressionMatchWithPlaceholders( + expressionWithPlaceholders, + 'x/2 + y/3', + ['alpha', 'beta'] + ) + ).toBeTrue(); + expect( + mathInteractionsService.expressionMatchWithPlaceholders( + expressionWithPlaceholders, + 'y/2 + x/3', + ['alpha', 'beta'] + ) + ).toBeTrue(); + expect( + mathInteractionsService.expressionMatchWithPlaceholders( + expressionWithPlaceholders, + '4x/2.5 + y', + ['alpha', 'beta'] + ) + ).toBeTrue(); + expect( + mathInteractionsService.expressionMatchWithPlaceholders( + expressionWithPlaceholders, + 'x + y', + ['alpha', 'beta'] + ) + ).toBeTrue(); + expect( + mathInteractionsService.expressionMatchWithPlaceholders( + expressionWithPlaceholders, + 'x/5 - y/2', + ['alpha', 'beta'] + ) + ).toBeTrue(); + expect( + mathInteractionsService.expressionMatchWithPlaceholders( + expressionWithPlaceholders, + '-x/2 + 3y', + ['alpha', 'beta'] + ) + ).toBeTrue(); + expect( + mathInteractionsService.expressionMatchWithPlaceholders( + expressionWithPlaceholders, + 'y - 8x', + ['alpha', 'beta'] + ) + ).toBeTrue(); + expect( + mathInteractionsService.expressionMatchWithPlaceholders( + expressionWithPlaceholders, + 'x/3 + y/(8/22)', + ['alpha', 'beta'] + ) + ).toBeTrue(); + + expect( + mathInteractionsService.expressionMatchWithPlaceholders( + expressionWithPlaceholders, + '(x^2)/4 + y/2', + ['alpha', 'beta'] + ) + ).toBeFalse(); + expect( + mathInteractionsService.expressionMatchWithPlaceholders( + expressionWithPlaceholders, + 'x/5', + ['alpha', 'beta'] + ) + ).toBeFalse(); + expect( + mathInteractionsService.expressionMatchWithPlaceholders( + expressionWithPlaceholders, + 'x/2 + y/3 - 5', + ['alpha', 'beta'] + ) + ).toBeFalse(); + expect( + mathInteractionsService.expressionMatchWithPlaceholders( + expressionWithPlaceholders, + 'x', + ['alpha', 'beta'] + ) + ).toBeFalse(); + expect( + mathInteractionsService.expressionMatchWithPlaceholders( + expressionWithPlaceholders, + 'x/alpha + y/2', + ['alpha', 'beta'] + ) + ).toBeFalse(); + expect( + mathInteractionsService.expressionMatchWithPlaceholders( + expressionWithPlaceholders, + 'x/2 + y/5 + 2x/2', + ['alpha', 'beta'] + ) + ).toBeFalse(); + expect( + mathInteractionsService.expressionMatchWithPlaceholders( + expressionWithPlaceholders, + 'z/2 + y/3', + ['alpha', 'beta'] + ) + ).toBeFalse(); + expect( + mathInteractionsService.expressionMatchWithPlaceholders( + expressionWithPlaceholders, + 'x/(x+1) + y/8', + ['alpha', 'beta'] + ) + ).toBeFalse(); }); - it('should correctly check for unsupported functions', function() { + it('should correctly check for unsupported functions', function () { // Currently, the supported functions are 'sqrt' and 'abs', so any // other function usages should raise a validation error which // should be caught using the checkUnsupportedFunctions function. - expect(mathInteractionsService.checkUnsupportedFunctions( - 'a + sqrt(b) + abs(3)')).toEqual([]); - expect(mathInteractionsService.checkUnsupportedFunctions( - 'a + b')).toEqual([]); - expect(mathInteractionsService.checkUnsupportedFunctions( - 'a + x * (y)')).toEqual([]); - expect(mathInteractionsService.checkUnsupportedFunctions( - 'a*b*(c)')).toEqual([]); - - expect(mathInteractionsService.checkUnsupportedFunctions( - 'a + log(b) + abs(3)')).toEqual(['log']); - expect(mathInteractionsService.checkUnsupportedFunctions( - 'a - tan(b)*cos(c)')).toEqual(['tan', 'cos']); + expect( + mathInteractionsService.checkUnsupportedFunctions('a + sqrt(b) + abs(3)') + ).toEqual([]); + expect(mathInteractionsService.checkUnsupportedFunctions('a + b')).toEqual( + [] + ); + expect( + mathInteractionsService.checkUnsupportedFunctions('a + x * (y)') + ).toEqual([]); + expect( + mathInteractionsService.checkUnsupportedFunctions('a*b*(c)') + ).toEqual([]); + + expect( + mathInteractionsService.checkUnsupportedFunctions('a + log(b) + abs(3)') + ).toEqual(['log']); + expect( + mathInteractionsService.checkUnsupportedFunctions('a - tan(b)*cos(c)') + ).toEqual(['tan', 'cos']); }); }); diff --git a/core/templates/services/math-interactions.service.ts b/core/templates/services/math-interactions.service.ts index dbf64d249744..169ca6ea514e 100644 --- a/core/templates/services/math-interactions.service.ts +++ b/core/templates/services/math-interactions.service.ts @@ -16,15 +16,15 @@ * @fileoverview Service for providing helper functions for math interactions. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; import nerdamer from 'nerdamer'; -import { AppConstants } from 'app.constants'; +import {AppConstants} from 'app.constants'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class MathInteractionsService { private warningText = ''; @@ -32,7 +32,9 @@ export class MathInteractionsService { private supportedFunctionNames = AppConstants.SUPPORTED_FUNCTION_NAMES; private cleanErrorMessage( - errorMessage: string, expressionString: string): string { + errorMessage: string, + expressionString: string + ): string { // The error thrown by nerdamer includes the index of the violation which // starts with a colon. That part needs to be removed before displaying // the error to the end user. Same rationale applies for stripping the @@ -57,13 +59,14 @@ export class MathInteractionsService { errorMessage += '.'; } if (errorMessage === 'Division by zero not allowed.') { - errorMessage = 'Your answer includes a division by zero, which is ' + - 'not valid.'; + errorMessage = + 'Your answer includes a division by zero, which is ' + 'not valid.'; } if (errorMessage.indexOf('is not a valid postfix operator.') !== -1) { - errorMessage = ( + errorMessage = 'Your answer seems to be missing a variable/number after the "' + - errorMessage[0] + '".'); + errorMessage[0] + + '".'; } if (errorMessage === 'A prefix operator was expected.') { let symbol1, symbol2; @@ -75,14 +78,17 @@ export class MathInteractionsService { } } } - errorMessage = ( - 'Your answer has two symbols next to each other: "' + symbol1 + - '" and "' + symbol2 + '".'); + errorMessage = + 'Your answer has two symbols next to each other: "' + + symbol1 + + '" and "' + + symbol2 + + '".'; } if ( - errorMessage === 'Cannot read property \'column\' of undefined.' || - errorMessage === 'Cannot read properties of undefined ' + - '(reading \'column\').' + errorMessage === "Cannot read property 'column' of undefined." || + errorMessage === + 'Cannot read properties of undefined ' + "(reading 'column')." ) { let emptyFunctionNames = []; for (let functionName of this.mathFunctionNames) { @@ -90,17 +96,18 @@ export class MathInteractionsService { emptyFunctionNames.push(functionName); } } - errorMessage = ( - 'The ' + emptyFunctionNames.join(', ') + - ' function(s) cannot be empty. Please enter a variable/number in it.'); + errorMessage = + 'The ' + + emptyFunctionNames.join(', ') + + ' function(s) cannot be empty. Please enter a variable/number in it.'; } return errorMessage; } isParenRedundant( - expressionString: string, - openingInd: number, - closingInd: number + expressionString: string, + openingInd: number, + closingInd: number ): boolean { /* Assumes that expressionString is syntactically valid. @@ -108,31 +115,32 @@ export class MathInteractionsService { Multiple consecutive parens are considered redundant. eg: for ((a - b)) the outer pair of parens are considered as redundant. */ - if ((closingInd + 2 < expressionString.length && + if ( + (closingInd + 2 < expressionString.length && expressionString[closingInd + 2] === '^') || - (openingInd - 2 >= 0 && - expressionString[openingInd - 2] === '^')) { + (openingInd - 2 >= 0 && expressionString[openingInd - 2] === '^') + ) { // Guppy adds redundant parens while using exponents, so we need to ignore // them. return false; } - let leftParenIsRedundant = ( - openingInd - 1 >= 0 && expressionString[openingInd - 1] === '('); - let rightParenIsRedundant = ( + let leftParenIsRedundant = + openingInd - 1 >= 0 && expressionString[openingInd - 1] === '('; + let rightParenIsRedundant = closingInd + 1 < expressionString.length && - expressionString[closingInd + 1] === ')'); + expressionString[closingInd + 1] === ')'; return leftParenIsRedundant && rightParenIsRedundant; } /** - * This function checks if an expression contains redundant params. It assumes - * that the expression will be syntactically valid. - * @param expressionString The math expression to be validated. - * - * @returns [boolean, string]. The boolean represents if the given expression - * contains any redundant params, and the string is the substring of the - * expression that contains redundant params. - */ + * This function checks if an expression contains redundant params. It assumes + * that the expression will be syntactically valid. + * @param expressionString The math expression to be validated. + * + * @returns [boolean, string]. The boolean represents if the given expression + * contains any redundant params, and the string is the substring of the + * expression that contains redundant params. + */ containsRedundantParens(expressionString: string): [boolean, string] { let stack: number[] = []; @@ -148,8 +156,10 @@ export class MathInteractionsService { } } else if (char === ')') { let openingInd = stack.pop() || 0; - if (openingInd !== -1 && this.isParenRedundant( - expressionString, openingInd, i)) { + if ( + openingInd !== -1 && + this.isParenRedundant(expressionString, openingInd, i) + ) { return [true, expressionString.slice(openingInd - 1, i + 2)]; } } @@ -162,59 +172,65 @@ export class MathInteractionsService { if (expressionString.length === 0) { this.warningText = 'Please enter an answer before submitting.'; return false; - } else if (expressionString.indexOf('=') !== -1 || expressionString.indexOf( - '<') !== -1 || expressionString.indexOf('>') !== -1) { - this.warningText = 'Please remove the equal sign to make ' + - 'your answer an expression.'; + } else if ( + expressionString.indexOf('=') !== -1 || + expressionString.indexOf('<') !== -1 || + expressionString.indexOf('>') !== -1 + ) { + this.warningText = + 'Please remove the equal sign to make ' + 'your answer an expression.'; return false; } else if (expressionString.indexOf('_') !== -1) { this.warningText = 'Your answer contains an invalid character: "_".'; return false; } if (expressionString.match(/(\+$)|(\+\))/g)) { - this.warningText = ( - 'Your answer seems to be missing a variable/number after the "+".'); + this.warningText = + 'Your answer seems to be missing a variable/number after the "+".'; return false; } let invalidIntegers = expressionString.match( - /(\d*\.\d*\.\d*)|(\d+\.\D)|(\D\.\d+)|(\d+\.$)/g); + /(\d*\.\d*\.\d*)|(\d+\.\D)|(\D\.\d+)|(\d+\.$)/g + ); if (invalidIntegers !== null) { - this.warningText = ( - 'Your answer contains an invalid term: ' + invalidIntegers[0]); + this.warningText = + 'Your answer contains an invalid term: ' + invalidIntegers[0]; return false; } let invalidMultiTerms = expressionString.match(/([a-zA-Z]+\d+)/g); if (invalidMultiTerms !== null) { let firstNumberIndex = invalidMultiTerms[0].search(/\d/); - let correctString = ( + let correctString = invalidMultiTerms[0].slice(firstNumberIndex) + - invalidMultiTerms[0].slice(0, firstNumberIndex)); - this.warningText = ( + invalidMultiTerms[0].slice(0, firstNumberIndex); + this.warningText = 'When multiplying, the variable should come after the number: ' + - correctString + '. Please update your answer and try again.' - ); + correctString + + '. Please update your answer and try again.'; return false; } try { expressionString = this.insertMultiplicationSigns(expressionString); nerdamer(expressionString); - // We use unknown type because we are unsure of the type of error - // that was thrown. Since the catch block cannot identify the - // specific type of error, we are unable to further optimise the - // code by introducing more types of errors. + // We use unknown type because we are unsure of the type of error + // that was thrown. Since the catch block cannot identify the + // specific type of error, we are unable to further optimise the + // code by introducing more types of errors. } catch (err: unknown) { if (err instanceof Error) { - this.warningText = ( - this.cleanErrorMessage(err.message, expressionString)); + this.warningText = this.cleanErrorMessage( + err.message, + expressionString + ); } return false; } if (expressionString.match(/\w\^((\w+\^)|(\(.*\^.*\)))/g)) { - this.warningText = ( + this.warningText = 'Your expression contains an exponent in an exponent ' + - 'which is not supported.'); + 'which is not supported.'; return false; } @@ -223,19 +239,18 @@ export class MathInteractionsService { for (let exponent of exponents) { exponent = exponent.replace(/^\^/g, ''); if (nerdamer(exponent).gt('5')) { - this.warningText = ( + this.warningText = 'Your expression contains an exponent with value greater than 5 ' + - 'which is not supported.'); + 'which is not supported.'; return false; } } } - let [expressionContainsRedundantParens, errorString] = ( - this.containsRedundantParens(expressionString)); + let [expressionContainsRedundantParens, errorString] = + this.containsRedundantParens(expressionString); if (expressionContainsRedundantParens) { - this.warningText = ( - `Your expression contains redundant parentheses: ${errorString}.`); + this.warningText = `Your expression contains redundant parentheses: ${errorString}.`; return false; } @@ -244,7 +259,9 @@ export class MathInteractionsService { } validateAlgebraicExpression( - expressionString: string, validVariablesList: string[]): boolean { + expressionString: string, + validVariablesList: string[] + ): boolean { if (!this._validateExpression(expressionString)) { return false; } @@ -258,8 +275,8 @@ export class MathInteractionsService { if (expressionString.match(/(^|[^a-zA-Z])pi($|[^a-zA-Z])/g)) { variablesList.push('pi'); } - const greekNameToSymbolMap: { [greekName: string]: string } = ( - AppConstants.GREEK_LETTER_NAMES_TO_SYMBOLS); + const greekNameToSymbolMap: {[greekName: string]: string} = + AppConstants.GREEK_LETTER_NAMES_TO_SYMBOLS; // Replacing greek names with symbols. for (let i = 0; i < variablesList.length; i++) { @@ -271,10 +288,12 @@ export class MathInteractionsService { if (validVariablesList.length !== 0) { for (let variable of variablesList) { if (validVariablesList.indexOf(variable) === -1) { - this.warningText = ( - 'You have entered an invalid variable: ' + variable + - '. Please use only the variables ' + validVariablesList.join() + - ' in your answer.'); + this.warningText = + 'You have entered an invalid variable: ' + + variable + + '. Please use only the variables ' + + validVariablesList.join() + + ' in your answer.'; return false; } } @@ -288,10 +307,13 @@ export class MathInteractionsService { } for (let functionName of this.mathFunctionNames) { expressionString = expressionString.replace( - new RegExp(functionName, 'g'), ''); + new RegExp(functionName, 'g'), + '' + ); } if (/[a-zA-Z]/.test(expressionString)) { - this.warningText = 'It looks like you have entered some variables. ' + + this.warningText = + 'It looks like you have entered some variables. ' + 'Please enter numbers only.'; return false; } @@ -299,18 +321,24 @@ export class MathInteractionsService { } validateEquation( - equationString: string, validVariablesList: string[]): boolean { + equationString: string, + validVariablesList: string[] + ): boolean { equationString = equationString.replace(/\s/g, ''); if (equationString.length === 0) { this.warningText = 'Please enter an answer before submitting.'; return false; - } else if (equationString.indexOf( - '<') !== -1 || equationString.indexOf('>') !== -1) { - this.warningText = 'It looks like you have entered an ' + + } else if ( + equationString.indexOf('<') !== -1 || + equationString.indexOf('>') !== -1 + ) { + this.warningText = + 'It looks like you have entered an ' + 'inequality. Please enter an equation instead.'; return false; } else if (equationString.indexOf('=') === -1) { - this.warningText = 'It looks like you have entered an ' + + this.warningText = + 'It looks like you have entered an ' + 'expression. Please enter an equation instead.'; return false; } else if (equationString.indexOf('=') === 0) { @@ -325,11 +353,16 @@ export class MathInteractionsService { this.warningText = 'Your equation contains multiple = signs.'; return false; } - let lhsString = splitString[0], rhsString = splitString[1]; + let lhsString = splitString[0], + rhsString = splitString[1]; let lhsIsAlgebraicallyValid = this.validateAlgebraicExpression( - lhsString, validVariablesList); + lhsString, + validVariablesList + ); let rhsIsAlgebraicallyValid = this.validateAlgebraicExpression( - rhsString, validVariablesList); + rhsString, + validVariablesList + ); let lhsIsNumericallyValid = this.validateNumericExpression(lhsString); let rhsIsNumericallyValid = this.validateNumericExpression(rhsString); @@ -339,9 +372,11 @@ export class MathInteractionsService { this.warningText = 'The equation must contain at least one variable.'; return false; } - if (lhsIsAlgebraicallyValid && rhsIsAlgebraicallyValid || - lhsIsAlgebraicallyValid && rhsIsNumericallyValid || - lhsIsNumericallyValid && rhsIsAlgebraicallyValid) { + if ( + (lhsIsAlgebraicallyValid && rhsIsAlgebraicallyValid) || + (lhsIsAlgebraicallyValid && rhsIsNumericallyValid) || + (lhsIsNumericallyValid && rhsIsAlgebraicallyValid) + ) { this.warningText = ''; return true; } @@ -359,10 +394,10 @@ export class MathInteractionsService { } insertMultiplicationSigns(expressionString: string): string { - let greekLetters = Object.keys( - AppConstants.GREEK_LETTER_NAMES_TO_SYMBOLS); + let greekLetters = Object.keys(AppConstants.GREEK_LETTER_NAMES_TO_SYMBOLS); let greekSymbols = Object.values( - AppConstants.GREEK_LETTER_NAMES_TO_SYMBOLS); + AppConstants.GREEK_LETTER_NAMES_TO_SYMBOLS + ); let greekLettersAndSymbols = []; for (let i = 0; i < greekLetters.length; i++) { greekLettersAndSymbols.push([greekLetters[i], greekSymbols[i]]); @@ -372,8 +407,8 @@ export class MathInteractionsService { // ['alpha', 'beta'] and not ['alpha', 'b', 'eta']. greekLettersAndSymbols.sort((a, b) => b[0].length - a[0].length); - let greekLetterToSymbol: { [letter: string]: string } = {}; - let greekSymbolToLetter: { [symbol: string]: string } = {}; + let greekLetterToSymbol: {[letter: string]: string} = {}; + let greekSymbolToLetter: {[symbol: string]: string} = {}; for (let letterAndSymbol of greekLettersAndSymbols) { greekLetterToSymbol[letterAndSymbol[0]] = letterAndSymbol[1]; greekSymbolToLetter[letterAndSymbol[1]] = letterAndSymbol[0]; @@ -382,7 +417,9 @@ export class MathInteractionsService { // Temporarily replacing letters with symbols. for (let letter in greekLetterToSymbol) { expressionString = expressionString.replace( - new RegExp(letter, 'g'), greekLetterToSymbol[letter]); + new RegExp(letter, 'g'), + greekLetterToSymbol[letter] + ); } expressionString = expressionString.replace(/\s/g, ''); @@ -396,25 +433,33 @@ export class MathInteractionsService { for (let variable of variables) { let separatedVariables = variable.split('').join('*'); expressionString = expressionString.replace( - new RegExp(variable, 'g'), separatedVariables); + new RegExp(variable, 'g'), + separatedVariables + ); } // Reverting the temporary replacement of letters. for (let symbol in greekSymbolToLetter) { expressionString = expressionString.replace( - new RegExp(symbol, 'g'), greekSymbolToLetter[symbol]); + new RegExp(symbol, 'g'), + greekSymbolToLetter[symbol] + ); } // Inserting multiplication signs before functions. For eg. 5sqrt(x) should // be treated as 5*sqrt(x). for (let functionName of this.mathFunctionNames) { - expressionString = expressionString.replace(new RegExp( - '([a-zA-Z0-9\\)])' + functionName, 'g'), '$1*' + functionName); + expressionString = expressionString.replace( + new RegExp('([a-zA-Z0-9\\)])' + functionName, 'g'), + '$1*' + functionName + ); } // Inserting multiplication signs between digit and variable. // For eg. 5w - z => 5*w - z. - expressionString = expressionString.replace(new RegExp( - '([0-9])([a-zA-Z])', 'g'), '$1*$2'); + expressionString = expressionString.replace( + new RegExp('([0-9])([a-zA-Z])', 'g'), + '$1*$2' + ); // Inserting multiplication signs after closing parens. expressionString = expressionString.replace(/\)([^\*\+\/\-\^\)])/g, ')*$1'); @@ -423,10 +468,14 @@ export class MathInteractionsService { // functions, for eg., we want to convert a(b) to a*(b) but not sqrt(4) to // sqrt*(4). expressionString = expressionString.replace( - /([^\*|\+|\/|\-|\^|\(])\(/g, '$1*('); + /([^\*|\+|\/|\-|\^|\(])\(/g, + '$1*(' + ); // Removing the '*' added before math function parens. - expressionString = expressionString.replace(new RegExp( - '(' + this.mathFunctionNames.join('|') + ')\\*\\(', 'g'), '$1('); + expressionString = expressionString.replace( + new RegExp('(' + this.mathFunctionNames.join('|') + ')\\*\\(', 'g'), + '$1(' + ); return expressionString; } @@ -458,7 +507,7 @@ export class MathInteractionsService { let currentTerm: string = ''; let bracketBalance: number = 0; let shouldModifyNextTerm: boolean = false; - let modifyTerm = function(termString: string): string { + let modifyTerm = function (termString: string): string { // If the shouldModifyNextTerm flag is set to true, we add the '-' sign, // or raise the term to a power of -1. This ensures that when the final // list is joined by the '+'/'*' sign, it matches with the original @@ -491,13 +540,15 @@ export class MathInteractionsService { for (let i = 0; i < expressionString.length; i++) { let currentVal = expressionString[i]; if (currentVal === '(' || currentVal === ')') { - bracketBalance += (currentVal === '(') ? 1 : -1; + bracketBalance += currentVal === '(' ? 1 : -1; } // Split term only if we are not inside a set of parens and the current // value is a delimiter. - if (bracketBalance === 0 && ( - currentVal === primaryDelimiter || currentVal === secondaryDelimiter)) { + if ( + bracketBalance === 0 && + (currentVal === primaryDelimiter || currentVal === secondaryDelimiter) + ) { if (currentTerm.length !== 0) { if (shouldModifyNextTerm) { currentTerm = modifyTerm(currentTerm); @@ -527,7 +578,9 @@ export class MathInteractionsService { } replaceConstantsWithVariables( - expressionString: string, replaceZero = true): string { + expressionString: string, + replaceZero = true + ): string { // Multiple instances of the same constant will be replaced by the same // variable. @@ -537,7 +590,9 @@ export class MathInteractionsService { // We need to do this differently since const3.4 would be // an invalid variable. expressionString = expressionString.replace( - /([0-9]+)\.([0-9]+)/g, 'const$1point$2'); + /([0-9]+)\.([0-9]+)/g, + 'const$1point$2' + ); // Replacing integers with variables. // Eg: 2 + x * 4 + 2 => const2 + x * const4 + const2 @@ -545,7 +600,9 @@ export class MathInteractionsService { expressionString = expressionString.replace(/([0-9]+)/g, 'const$1'); } else { expressionString = expressionString.replace( - /([1-9]+)\.([1-9]+)/g, 'const$1point$2'); + /([1-9]+)\.([1-9]+)/g, + 'const$1point$2' + ); expressionString = expressionString.replace(/([1-9]+)/g, 'const$1'); } return expressionString; @@ -586,8 +643,10 @@ export class MathInteractionsService { } expressionMatchWithPlaceholders( - expressionWithPlaceholders: string, inputExpression: string, - placeholders: string[]): boolean { + expressionWithPlaceholders: string, + inputExpression: string, + placeholders: string[] + ): boolean { // Check if inputExpression contains any placeholders. for (let variable of nerdamer(inputExpression).variables()) { if (placeholders.includes(variable)) { @@ -612,18 +671,22 @@ export class MathInteractionsService { let divisionCondition; // Try catch block is meant to catch division by zero errors. try { - let variablesAfterDivision = nerdamer(termWithPlaceholders).divide( - termWithoutPlaceholders).variables(); - divisionCondition = variablesAfterDivision.every( - variable => placeholders.includes(variable)); + let variablesAfterDivision = nerdamer(termWithPlaceholders) + .divide(termWithoutPlaceholders) + .variables(); + divisionCondition = variablesAfterDivision.every(variable => + placeholders.includes(variable) + ); } catch (e) { divisionCondition = true; } - let variablesAfterSubtraction = nerdamer(termWithPlaceholders).subtract( - termWithoutPlaceholders).variables(); - let subtractionCondition = variablesAfterSubtraction.every( - variable => placeholders.includes(variable)); + let variablesAfterSubtraction = nerdamer(termWithPlaceholders) + .subtract(termWithoutPlaceholders) + .variables(); + let subtractionCondition = variablesAfterSubtraction.every(variable => + placeholders.includes(variable) + ); // If only placeholders are left in the term after dividing/subtracting // them, then the terms are said to match. @@ -664,6 +727,9 @@ export class MathInteractionsService { } } -angular.module('oppia').factory( - 'MathInteractionsService', - downgradeInjectable(MathInteractionsService)); +angular + .module('oppia') + .factory( + 'MathInteractionsService', + downgradeInjectable(MathInteractionsService) + ); diff --git a/core/templates/services/messenger.service.spec.ts b/core/templates/services/messenger.service.spec.ts index 52cac9bec6f1..8f7a207b3f66 100644 --- a/core/templates/services/messenger.service.spec.ts +++ b/core/templates/services/messenger.service.spec.ts @@ -19,11 +19,11 @@ * be attempted due to cross-domain security issues.) */ -import { TestBed } from '@angular/core/testing'; -import { MessengerService } from './messenger.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { LoggerService } from './contextual/logger.service'; -import { ServicesConstants } from './services.constants'; +import {TestBed} from '@angular/core/testing'; +import {MessengerService} from './messenger.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {LoggerService} from './contextual/logger.service'; +import {ServicesConstants} from './services.constants'; describe('BannerComponent', () => { let messengerService: MessengerService; @@ -46,11 +46,11 @@ describe('BannerComponent', () => { }, set hash(val) { this._hash = val; - } + }, }, parent: { - postMessage: () => {} - } + postMessage: () => {}, + }, }; get nativeWindow() { @@ -63,9 +63,9 @@ describe('BannerComponent', () => { providers: [ { provide: WindowRef, - useClass: MockWindowRef + useClass: MockWindowRef, }, - ] + ], }); mockWindowRef = TestBed.inject(WindowRef) as unknown as MockWindowRef; }); @@ -75,158 +75,205 @@ describe('BannerComponent', () => { loggerService = TestBed.inject(LoggerService); }); - it('should post height change when the user changes the height of the' + - 'exploration player', () => { - spyOn(mockWindowRef._window.parent, 'postMessage'); - mockWindowRef.nativeWindow.location.hash = - '/version=0.0.2&secret=secret1&tagid=1'; - - messengerService.sendMessage( - ServicesConstants.MESSENGER_PAYLOAD.HEIGHT_CHANGE, { - height: 100, - scroll: true - }); - - expect(mockWindowRef._window.parent.postMessage).toHaveBeenCalledWith( - '{"title":"heightChange",' + - '"payload":{"height":100,"scroll":true},' + - '"sourceTagId":null,"secret":null}', '*'); - }); - - - it('should post exploration loaded when the exploration completes loading' + - ' in the exploration player', () => { - spyOn(mockWindowRef._window.parent, 'postMessage'); - mockWindowRef.nativeWindow.location.hash = - '/version=0.0.2&secret=secret1&tagid=1'; + it( + 'should post height change when the user changes the height of the' + + 'exploration player', + () => { + spyOn(mockWindowRef._window.parent, 'postMessage'); + mockWindowRef.nativeWindow.location.hash = + '/version=0.0.2&secret=secret1&tagid=1'; - messengerService.sendMessage( - ServicesConstants.MESSENGER_PAYLOAD.EXPLORATION_LOADED, { - explorationVersion: 1, - explorationTitle: 'exploration title' - }); + messengerService.sendMessage( + ServicesConstants.MESSENGER_PAYLOAD.HEIGHT_CHANGE, + { + height: 100, + scroll: true, + } + ); - expect(mockWindowRef._window.parent.postMessage).toHaveBeenCalledWith( - '{"title":"explorationLoaded",' + - '"payload":{"explorationVersion":1,' + - '"explorationTitle":"exploration title"},' + - '"sourceTagId":null,"secret":null}', '*'); - }); + expect(mockWindowRef._window.parent.postMessage).toHaveBeenCalledWith( + '{"title":"heightChange",' + + '"payload":{"height":100,"scroll":true},' + + '"sourceTagId":null,"secret":null}', + '*' + ); + } + ); - it('should post state transition when the exploration state changes' + - ' in the exploration player', () => { - spyOn(mockWindowRef._window.parent, 'postMessage'); - mockWindowRef.nativeWindow.location.hash = - '/version=0.0.2&secret=secret1&tagid=1'; + it( + 'should post exploration loaded when the exploration completes loading' + + ' in the exploration player', + () => { + spyOn(mockWindowRef._window.parent, 'postMessage'); + mockWindowRef.nativeWindow.location.hash = + '/version=0.0.2&secret=secret1&tagid=1'; - messengerService.sendMessage( - ServicesConstants.MESSENGER_PAYLOAD.STATE_TRANSITION, { - explorationVersion: 1, - oldStateName: 'old state', - jsonAnswer: '{answer: 0}', - newStateName: 'new state' - }); + messengerService.sendMessage( + ServicesConstants.MESSENGER_PAYLOAD.EXPLORATION_LOADED, + { + explorationVersion: 1, + explorationTitle: 'exploration title', + } + ); - expect(mockWindowRef._window.parent.postMessage).toHaveBeenCalledWith( - '{"title":"stateTransition","payload":{"explorationVersion":1,' + - '"oldStateName":"old state","jsonAnswer":"{answer: 0}",' + - '"newStateName":"new state"},"sourceTagId":null,"secret":null}', '*'); - }); + expect(mockWindowRef._window.parent.postMessage).toHaveBeenCalledWith( + '{"title":"explorationLoaded",' + + '"payload":{"explorationVersion":1,' + + '"explorationTitle":"exploration title"},' + + '"sourceTagId":null,"secret":null}', + '*' + ); + } + ); - it('should post exploration completed when the exploration is completed' + - ' in the exploration player', () => { - spyOn(mockWindowRef._window.parent, 'postMessage'); - mockWindowRef.nativeWindow.location.hash = - '/version=0.0.2&secret=secret1&tagid=1'; + it( + 'should post state transition when the exploration state changes' + + ' in the exploration player', + () => { + spyOn(mockWindowRef._window.parent, 'postMessage'); + mockWindowRef.nativeWindow.location.hash = + '/version=0.0.2&secret=secret1&tagid=1'; - messengerService.sendMessage( - ServicesConstants.MESSENGER_PAYLOAD.EXPLORATION_COMPLETED, { - explorationVersion: 1 - }); + messengerService.sendMessage( + ServicesConstants.MESSENGER_PAYLOAD.STATE_TRANSITION, + { + explorationVersion: 1, + oldStateName: 'old state', + jsonAnswer: '{answer: 0}', + newStateName: 'new state', + } + ); - expect(mockWindowRef._window.parent.postMessage).toHaveBeenCalledWith( - '{"title":"explorationCompleted","payload":{"explorationVersion":1},' + - '"sourceTagId":null,"secret":null}', '*'); - }); + expect(mockWindowRef._window.parent.postMessage).toHaveBeenCalledWith( + '{"title":"stateTransition","payload":{"explorationVersion":1,' + + '"oldStateName":"old state","jsonAnswer":"{answer: 0}",' + + '"newStateName":"new state"},"sourceTagId":null,"secret":null}', + '*' + ); + } + ); - it('should post exploration reset when the exploration is reset' + - ' in the exploration player', () => { - spyOn(mockWindowRef._window.parent, 'postMessage'); - mockWindowRef.nativeWindow.location.hash = - '/version=0.0.2&secret=secret1&tagid=1'; + it( + 'should post exploration completed when the exploration is completed' + + ' in the exploration player', + () => { + spyOn(mockWindowRef._window.parent, 'postMessage'); + mockWindowRef.nativeWindow.location.hash = + '/version=0.0.2&secret=secret1&tagid=1'; - messengerService.sendMessage( - ServicesConstants.MESSENGER_PAYLOAD.EXPLORATION_RESET, { - stateName: 'state name' - }); + messengerService.sendMessage( + ServicesConstants.MESSENGER_PAYLOAD.EXPLORATION_COMPLETED, + { + explorationVersion: 1, + } + ); - expect(mockWindowRef._window.parent.postMessage).toHaveBeenCalledWith( - '{"title":"explorationReset","payload":{"stateName":' + - '{"stateName":"state name"}},"sourceTagId":null,"secret":null}', '*'); - }); + expect(mockWindowRef._window.parent.postMessage).toHaveBeenCalledWith( + '{"title":"explorationCompleted","payload":{"explorationVersion":1},' + + '"sourceTagId":null,"secret":null}', + '*' + ); + } + ); - it('should post \'Secret\' and \'Tag Id\' if the version is \'0.0.0\'', + it( + 'should post exploration reset when the exploration is reset' + + ' in the exploration player', () => { spyOn(mockWindowRef._window.parent, 'postMessage'); mockWindowRef.nativeWindow.location.hash = - '/version=0.0.0&secret=secret1&tagid=1'; + '/version=0.0.2&secret=secret1&tagid=1'; messengerService.sendMessage( - ServicesConstants.MESSENGER_PAYLOAD.HEIGHT_CHANGE, { - height: 100, - scroll: true - }); + ServicesConstants.MESSENGER_PAYLOAD.EXPLORATION_RESET, + { + stateName: 'state name', + } + ); expect(mockWindowRef._window.parent.postMessage).toHaveBeenCalledWith( - '{"title":"heightChange",' + - '"payload":{"height":100,"scroll":true},' + - '"sourceTagId":"1","secret":"secret1"}', '*'); - }); + '{"title":"explorationReset","payload":{"stateName":' + + '{"stateName":"state name"}},"sourceTagId":null,"secret":null}', + '*' + ); + } + ); - it('should post message when height of exploration window changes', () => { - spyOn(loggerService, 'error').and.stub(); + it("should post 'Secret' and 'Tag Id' if the version is '0.0.0'", () => { + spyOn(mockWindowRef._window.parent, 'postMessage'); mockWindowRef.nativeWindow.location.hash = - '/version0.0.0'; + '/version=0.0.0&secret=secret1&tagid=1'; messengerService.sendMessage( - ServicesConstants.MESSENGER_PAYLOAD.HEIGHT_CHANGE, { + ServicesConstants.MESSENGER_PAYLOAD.HEIGHT_CHANGE, + { height: 100, - scroll: true - }); + scroll: true, + } + ); - expect(loggerService.error) - .toHaveBeenCalledWith('Invalid hash for embedding: version0.0.0'); + expect(mockWindowRef._window.parent.postMessage).toHaveBeenCalledWith( + '{"title":"heightChange",' + + '"payload":{"height":100,"scroll":true},' + + '"sourceTagId":"1","secret":"secret1"}', + '*' + ); }); - it('should throw error when an invalid version or secret is' + - ' presenti in the url', () => { + it('should post message when height of exploration window changes', () => { spyOn(loggerService, 'error').and.stub(); - mockWindowRef.nativeWindow.location.hash = - '/version=0.0.0'; + mockWindowRef.nativeWindow.location.hash = '/version0.0.0'; messengerService.sendMessage( - ServicesConstants.MESSENGER_PAYLOAD.HEIGHT_CHANGE, { + ServicesConstants.MESSENGER_PAYLOAD.HEIGHT_CHANGE, + { height: 100, - scroll: true - }); + scroll: true, + } + ); - expect(loggerService.error) - .toHaveBeenCalledWith('Invalid hash for embedding: version=0.0.0'); + expect(loggerService.error).toHaveBeenCalledWith( + 'Invalid hash for embedding: version0.0.0' + ); }); + it( + 'should throw error when an invalid version or secret is' + + ' presenti in the url', + () => { + spyOn(loggerService, 'error').and.stub(); + mockWindowRef.nativeWindow.location.hash = '/version=0.0.0'; + + messengerService.sendMessage( + ServicesConstants.MESSENGER_PAYLOAD.HEIGHT_CHANGE, + { + height: 100, + scroll: true, + } + ); + + expect(loggerService.error).toHaveBeenCalledWith( + 'Invalid hash for embedding: version=0.0.0' + ); + } + ); + it('should thow error when hasdict version is not supported', () => { spyOn(loggerService, 'error').and.stub(); mockWindowRef.nativeWindow.location.hash = '/version=0.0.0&secret=secret1&tagid=1'; messengerService.sendMessage( - ServicesConstants.MESSENGER_PAYLOAD.HEIGHT_CHANGE, { + ServicesConstants.MESSENGER_PAYLOAD.HEIGHT_CHANGE, + { height: 100, - scroll: 100 - }); + scroll: 100, + } + ); - expect(loggerService.error) - .toHaveBeenCalledWith('Error validating payload: [object Object]'); + expect(loggerService.error).toHaveBeenCalledWith( + 'Error validating payload: [object Object]' + ); }); it('should throw error when version of embedding is unknown', () => { @@ -235,12 +282,15 @@ describe('BannerComponent', () => { '/version=0.0.10&secret=secret1&tagid=1'; messengerService.sendMessage( - ServicesConstants.MESSENGER_PAYLOAD.HEIGHT_CHANGE, { + ServicesConstants.MESSENGER_PAYLOAD.HEIGHT_CHANGE, + { height: 100, - scroll: true - }); + scroll: true, + } + ); - expect(loggerService.error) - .toHaveBeenCalledWith('Unknown version for embedding: 0.0.10'); + expect(loggerService.error).toHaveBeenCalledWith( + 'Unknown version for embedding: 0.0.10' + ); }); }); diff --git a/core/templates/services/messenger.service.ts b/core/templates/services/messenger.service.ts index 982b8c496c63..3e4b0d52b90d 100644 --- a/core/templates/services/messenger.service.ts +++ b/core/templates/services/messenger.service.ts @@ -19,12 +19,12 @@ * be attempted due to cross-domain security issues.) */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { LoggerService } from 'services/contextual/logger.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { ServicesConstants } from 'services/services.constants'; +import {LoggerService} from 'services/contextual/logger.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {ServicesConstants} from 'services/services.constants'; interface HeightChangeData { height: number; @@ -64,20 +64,19 @@ interface GetPayloadType { explorationLoaded: (data: ExplorationLoadedData) => ExplorationLoadedData; stateTransition: (data: StateTransitionData) => StateTransitionData; explorationCompleted: ( - data: ExplorationCompletedData) => ExplorationCompletedData; + data: ExplorationCompletedData + ) => ExplorationCompletedData; explorationReset: (data: string) => ExplorationResetData; } -type MessageTitles = typeof ServicesConstants.MESSENGER_PAYLOAD[ - keyof typeof ServicesConstants.MESSENGER_PAYLOAD -]; +type MessageTitles = + (typeof ServicesConstants.MESSENGER_PAYLOAD)[keyof typeof ServicesConstants.MESSENGER_PAYLOAD]; -type PayloadType = ( - HeightChangeData | - ExplorationCompletedData | - StateTransitionData | - ExplorationResetData -); +type PayloadType = + | HeightChangeData + | ExplorationCompletedData + | StateTransitionData + | ExplorationResetData; // The 'secret' and 'tagId' sent to the parent will be 'null' if the supported // hash version is not '0.0.0'. They are used to ensure backwards-compatibility. @@ -88,20 +87,27 @@ interface HashDict { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class MessengerService { constructor( - private loggerService: LoggerService, private windowRef: WindowRef) {} + private loggerService: LoggerService, + private windowRef: WindowRef + ) {} - SUPPORTED_HASHDICT_VERSIONS: Set = ( - new Set(['0.0.0', '0.0.1', '0.0.2', '0.0.3'])); + SUPPORTED_HASHDICT_VERSIONS: Set = new Set([ + '0.0.0', + '0.0.1', + '0.0.2', + '0.0.3', + ]); MESSAGE_VALIDATORS: MessageValidatorsType = { heightChange(payload: HeightChangeData): boolean { const {height, scroll} = payload; return ( - Number.isInteger(height) && height > 0 && typeof scroll === 'boolean'); + Number.isInteger(height) && height > 0 && typeof scroll === 'boolean' + ); }, explorationLoaded(): boolean { return true; @@ -114,20 +120,20 @@ export class MessengerService { }, explorationCompleted(): boolean { return true; - } + }, }; getPayload: GetPayloadType = { heightChange(data: HeightChangeData): HeightChangeData { return { height: data.height, - scroll: data.scroll + scroll: data.scroll, }; }, explorationLoaded(data: ExplorationLoadedData): ExplorationLoadedData { return { explorationVersion: data.explorationVersion, - explorationTitle: data.explorationTitle + explorationTitle: data.explorationTitle, }; }, stateTransition(data: StateTransitionData): StateTransitionData { @@ -135,21 +141,22 @@ export class MessengerService { explorationVersion: data.explorationVersion, oldStateName: data.oldStateName, jsonAnswer: data.jsonAnswer, - newStateName: data.newStateName + newStateName: data.newStateName, }; }, explorationCompleted( - data: ExplorationCompletedData): ExplorationCompletedData { + data: ExplorationCompletedData + ): ExplorationCompletedData { return { - explorationVersion: data.explorationVersion + explorationVersion: data.explorationVersion, }; }, // ---- DEPRECATED ---- explorationReset(data: string): ExplorationResetData { return { - stateName: data + stateName: data, }; - } + }, }; /** @@ -169,16 +176,18 @@ export class MessengerService { // a hash is passed in. let window = this.windowRef.nativeWindow; let rawHash = window.location.hash.substring(1); - if (window.parent !== window && rawHash && - this.MESSAGE_VALIDATORS.hasOwnProperty(messageTitle)) { + if ( + window.parent !== window && + rawHash && + this.MESSAGE_VALIDATORS.hasOwnProperty(messageTitle) + ) { // Protractor tests may prepend a / to this hash, which we remove. - let hash = - (rawHash.charAt(0) === '/') ? rawHash.substring(1) : rawHash; + let hash = rawHash.charAt(0) === '/' ? rawHash.substring(1) : rawHash; let hashParts = hash.split('&'); let hashDict: HashDict = { version: '', secret: null, - tagid: null + tagid: null, }; for (let i = 0; i < hashParts.length; i++) { if (hashParts[i].indexOf('=') === -1) { @@ -187,10 +196,11 @@ export class MessengerService { } let separatorLocation = hashParts[i].indexOf('='); - const _hashProp = ( - hashParts[i].substring(0, separatorLocation) as keyof HashDict); - hashDict[_hashProp] = ( - hashParts[i].substring(separatorLocation + 1)); + const _hashProp = hashParts[i].substring( + 0, + separatorLocation + ) as keyof HashDict; + hashDict[_hashProp] = hashParts[i].substring(separatorLocation + 1); } if (!hashDict.version || !hashDict.secret) { @@ -199,41 +209,39 @@ export class MessengerService { } if (this.SUPPORTED_HASHDICT_VERSIONS.has(hashDict.version)) { - this.loggerService.info( - 'Posting message to parent: ' + messageTitle); + this.loggerService.info('Posting message to parent: ' + messageTitle); let payload: PayloadType; let isValidMessage: boolean; switch (messageTitle) { case ServicesConstants.MESSENGER_PAYLOAD.EXPLORATION_COMPLETED: payload = this.getPayload.explorationCompleted( - messageData as ExplorationCompletedData); - isValidMessage = ( - this.MESSAGE_VALIDATORS.explorationCompleted()); + messageData as ExplorationCompletedData + ); + isValidMessage = this.MESSAGE_VALIDATORS.explorationCompleted(); break; case ServicesConstants.MESSENGER_PAYLOAD.EXPLORATION_LOADED: payload = this.getPayload.explorationLoaded( - messageData as ExplorationLoadedData); - isValidMessage = ( - this.MESSAGE_VALIDATORS.explorationLoaded()); + messageData as ExplorationLoadedData + ); + isValidMessage = this.MESSAGE_VALIDATORS.explorationLoaded(); break; case ServicesConstants.MESSENGER_PAYLOAD.EXPLORATION_RESET: - payload = this.getPayload.explorationReset( - messageData as string); - isValidMessage = ( - this.MESSAGE_VALIDATORS.explorationReset(payload)); + payload = this.getPayload.explorationReset(messageData as string); + isValidMessage = this.MESSAGE_VALIDATORS.explorationReset(payload); break; case ServicesConstants.MESSENGER_PAYLOAD.HEIGHT_CHANGE: payload = this.getPayload.heightChange( - messageData as HeightChangeData); - isValidMessage = ( - this.MESSAGE_VALIDATORS.heightChange(payload)); + messageData as HeightChangeData + ); + isValidMessage = this.MESSAGE_VALIDATORS.heightChange(payload); break; case ServicesConstants.MESSENGER_PAYLOAD.STATE_TRANSITION: payload = this.getPayload.stateTransition( - messageData as StateTransitionData); - isValidMessage = ( - this.MESSAGE_VALIDATORS.stateTransition( - payload as StateTransitionData)); + messageData as StateTransitionData + ); + isValidMessage = this.MESSAGE_VALIDATORS.stateTransition( + payload as StateTransitionData + ); break; } @@ -253,7 +261,7 @@ export class MessengerService { title: messageTitle, payload: payload, sourceTagId: null, - secret: null + secret: null, }; if (hashDict.version === '0.0.0') { // Ensure backwards-compatibility. @@ -266,11 +274,13 @@ export class MessengerService { window.parent.postMessage(JSON.stringify(objToSendToParent), '*'); } else { this.loggerService.error( - 'Unknown version for embedding: ' + hashDict.version); + 'Unknown version for embedding: ' + hashDict.version + ); return; } } } } -angular.module('oppia').factory('MessengerService', - downgradeInjectable(MessengerService)); +angular + .module('oppia') + .factory('MessengerService', downgradeInjectable(MessengerService)); diff --git a/core/templates/services/navigation.service.spec.ts b/core/templates/services/navigation.service.spec.ts index e1d092c92d71..56c63fa942b1 100644 --- a/core/templates/services/navigation.service.spec.ts +++ b/core/templates/services/navigation.service.spec.ts @@ -16,20 +16,20 @@ * @fileoverview Unit tests for NavigationService */ -import { TestBed } from '@angular/core/testing'; -import { EventToCodes, NavigationService } from './navigation.service'; +import {TestBed} from '@angular/core/testing'; +import {EventToCodes, NavigationService} from './navigation.service'; describe('Navigation Service', () => { let navigationService: NavigationService; let element = { focus: () => {}, - closest: () => {} + closest: () => {}, }; let closestReturn = { - find: () => {} + find: () => {}, }; let findReturn = { - blur: () => {} + blur: () => {}, }; beforeEach(() => { @@ -37,16 +37,16 @@ describe('Navigation Service', () => { navigationService.KEYBOARD_EVENT_TO_KEY_CODES = { enter: { shiftKeyIsPressed: false, - keyCode: 13 + keyCode: 13, }, tab: { shiftKeyIsPressed: false, - keyCode: 9 + keyCode: 9, }, shiftTab: { shiftKeyIsPressed: true, - keyCode: 9 - } + keyCode: 9, + }, }; navigationService.ACTION_OPEN = 'open'; navigationService.ACTION_CLOSE = 'close'; @@ -61,29 +61,32 @@ describe('Navigation Service', () => { it('should open submenu when event has open action type', () => { let mockEvent = { keyCode: 13, - shiftKey: false + shiftKey: false, } as KeyboardEvent; let eventsTobeHandled = { - enter: 'open' + enter: 'open', } as EventToCodes; - let openSubmenuSpy = spyOn(navigationService, 'openSubmenu') - .and.callThrough(); - navigationService.onMenuKeypress( - mockEvent, 'New menu', eventsTobeHandled); + let openSubmenuSpy = spyOn( + navigationService, + 'openSubmenu' + ).and.callThrough(); + navigationService.onMenuKeypress(mockEvent, 'New menu', eventsTobeHandled); expect(openSubmenuSpy).toHaveBeenCalled(); }); it('should close submenu when event has close action type', () => { let mockEvent = { keyCode: 9, - shiftKey: true + shiftKey: true, } as KeyboardEvent; let eventsTobeHandled = { shiftTab: 'close', } as EventToCodes; - let closeSubmenuSpy = spyOn(navigationService, 'closeSubmenu').and - .callThrough(); + let closeSubmenuSpy = spyOn( + navigationService, + 'closeSubmenu' + ).and.callThrough(); navigationService.onMenuKeypress(mockEvent, 'New menu', eventsTobeHandled); expect(closeSubmenuSpy).toHaveBeenCalled(); }); @@ -91,15 +94,18 @@ describe('Navigation Service', () => { it('should throw an error when event has invalid action type', () => { let mockEvent = { keyCode: 9, - shiftKey: true + shiftKey: true, } as KeyboardEvent; let eventsTobeHandled = { - shiftTab: 'invalid' + shiftTab: 'invalid', } as EventToCodes; expect(() => { navigationService.onMenuKeypress( - mockEvent, 'New menu', eventsTobeHandled); + mockEvent, + 'New menu', + eventsTobeHandled + ); }).toThrowError('Invalid action type.'); }); }); diff --git a/core/templates/services/navigation.service.ts b/core/templates/services/navigation.service.ts index d4267db6ba1a..b281f8eb17bc 100644 --- a/core/templates/services/navigation.service.ts +++ b/core/templates/services/navigation.service.ts @@ -17,8 +17,8 @@ * tab and shift-tab. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; interface KeyFunc { shiftKeyIsPressed: boolean; @@ -49,7 +49,7 @@ export interface EventToCodes { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class NavigationService { // This property is initialized using Angular lifecycle hooks @@ -61,26 +61,26 @@ export class NavigationService { KEYBOARD_EVENT_TO_KEY_CODES: KeyboardEventToCodes = { enter: { shiftKeyIsPressed: false, - keyCode: 13 + keyCode: 13, }, tab: { shiftKeyIsPressed: false, - keyCode: 9 + keyCode: 9, }, shiftTab: { shiftKeyIsPressed: true, - keyCode: 9 - } + keyCode: 9, + }, }; constructor() {} /** - * Opens the submenu. - * @param {object} evt - * @param {String} menuName - name of menu, on which - * open/close action to be performed (category,language). - */ + * Opens the submenu. + * @param {object} evt + * @param {String} menuName - name of menu, on which + * open/close action to be performed (category,language). + */ openSubmenu(evt: KeyboardEvent, menuName: string): void { // Focus on the current target before opening its submenu. this.activeMenuName = menuName; @@ -102,19 +102,20 @@ export class NavigationService { * onMenuKeypress($event, 'category', {enter: 'open'}) */ onMenuKeypress( - evt: KeyboardEvent, - menuName: string, - eventsTobeHandled: EventToCodes): void { + evt: KeyboardEvent, + menuName: string, + eventsTobeHandled: EventToCodes + ): void { let targetEvents = Object.keys(eventsTobeHandled); for (let i = 0; i < targetEvents.length; i++) { - let keyCodeSpec = - this.KEYBOARD_EVENT_TO_KEY_CODES[targetEvents[i]]; - if (keyCodeSpec.keyCode === evt.keyCode && - evt.shiftKey === keyCodeSpec.shiftKeyIsPressed) { + let keyCodeSpec = this.KEYBOARD_EVENT_TO_KEY_CODES[targetEvents[i]]; + if ( + keyCodeSpec.keyCode === evt.keyCode && + evt.shiftKey === keyCodeSpec.shiftKeyIsPressed + ) { if (eventsTobeHandled[targetEvents[i]] === this.ACTION_OPEN) { this.openSubmenu(evt, menuName); - } else if (eventsTobeHandled[targetEvents[i]] === - this.ACTION_CLOSE) { + } else if (eventsTobeHandled[targetEvents[i]] === this.ACTION_CLOSE) { this.closeSubmenu(evt); } else { throw new Error('Invalid action type.'); @@ -124,5 +125,6 @@ export class NavigationService { } } -angular.module('oppia').factory('NavigationService', - downgradeInjectable(NavigationService)); +angular + .module('oppia') + .factory('NavigationService', downgradeInjectable(NavigationService)); diff --git a/core/templates/services/nested-directives-recursion-timeout-prevention.service.spec.ts b/core/templates/services/nested-directives-recursion-timeout-prevention.service.spec.ts index 8a22c9cc21f3..f0a0477b91af 100644 --- a/core/templates/services/nested-directives-recursion-timeout-prevention.service.spec.ts +++ b/core/templates/services/nested-directives-recursion-timeout-prevention.service.spec.ts @@ -17,43 +17,47 @@ * NestedDirectivesRecursionTimeoutPreventionService. */ -import { importAllAngularServices } from 'tests/unit-test-utils.ajs'; +import {importAllAngularServices} from 'tests/unit-test-utils.ajs'; require('services/nested-directives-recursion-timeout-prevention.service'); require('services/contextual/logger.service'); -describe('Nested Directives Recursion Timeout Prevention Service', - function() { - var ndrtps, ls; - var $scope; - var element = { - append: function() {}, - contents: function() { - return { - remove: function() {} - }; - } - }; - var functions; +describe('Nested Directives Recursion Timeout Prevention Service', function () { + var ndrtps, ls; + var $scope; + var element = { + append: function () {}, + contents: function () { + return { + remove: function () {}, + }; + }, + }; + var functions; - beforeEach(angular.mock.module('oppia')); - importAllAngularServices(); - beforeEach(angular.mock.inject(function($injector, $rootScope) { + beforeEach(angular.mock.module('oppia')); + importAllAngularServices(); + beforeEach( + angular.mock.inject(function ($injector, $rootScope) { ndrtps = $injector.get( - 'NestedDirectivesRecursionTimeoutPreventionService'); + 'NestedDirectivesRecursionTimeoutPreventionService' + ); ls = $injector.get('LoggerService'); $scope = $rootScope.$new(); functions = { - pre: function() { + pre: function () { ls.log('Calling pre function'); }, - post: function() {} + post: function () {}, }; - })); + }) + ); - it('should return linking functions when object is passed as' + - ' arguments on compile function', function() { + it( + 'should return linking functions when object is passed as' + + ' arguments on compile function', + function () { var logSpy = spyOn(ls, 'log').and.callThrough(); var postFunctionSpy = spyOn(functions, 'post').and.callThrough(); var appendElementSpy = spyOn(element, 'append').and.callThrough(); @@ -66,10 +70,13 @@ describe('Nested Directives Recursion Timeout Prevention Service', linkingFunctions.post($scope, element); expect(appendElementSpy).toHaveBeenCalled(); expect(postFunctionSpy).toHaveBeenCalledWith($scope, element); - }); + } + ); - it('should return post linking function when a function is passed' + - ' as argument on compile function', function() { + it( + 'should return post linking function when a function is passed' + + ' as argument on compile function', + function () { var postFunctionSpy = spyOn(functions, 'post').and.callThrough(); var appendElementSpy = spyOn(element, 'append').and.callThrough(); @@ -79,5 +86,6 @@ describe('Nested Directives Recursion Timeout Prevention Service', linkingFunctions.post($scope, element); expect(appendElementSpy).toHaveBeenCalled(); expect(postFunctionSpy).toHaveBeenCalledWith($scope, element); - }); - }); + } + ); +}); diff --git a/core/templates/services/nested-directives-recursion-timeout-prevention.service.ts b/core/templates/services/nested-directives-recursion-timeout-prevention.service.ts index 6d9b20870bc4..a2ca5676574d 100644 --- a/core/templates/services/nested-directives-recursion-timeout-prevention.service.ts +++ b/core/templates/services/nested-directives-recursion-timeout-prevention.service.ts @@ -17,9 +17,11 @@ * in nested directives. See: http://stackoverflow.com/q/14430655 */ -angular.module('oppia').factory( - 'NestedDirectivesRecursionTimeoutPreventionService', [ - '$compile', function($compile) { +angular + .module('oppia') + .factory('NestedDirectivesRecursionTimeoutPreventionService', [ + '$compile', + function ($compile) { return { /** * Manually compiles the element, fixing the recursion loop. @@ -28,11 +30,11 @@ angular.module('oppia').factory( * with function(s) registered via pre and post properties. * @return {object} An object containing the linking functions. */ - compile: function(element, link) { + compile: function (element, link) { // Normalize the link parameter. if (angular.isFunction(link)) { link = { - post: link + post: link, }; } @@ -40,14 +42,14 @@ angular.module('oppia').factory( var contents = element.contents().remove(); var compiledContents; return { - pre: (link && link.pre) ? link.pre : null, - post: function(scope, element) { + pre: link && link.pre ? link.pre : null, + post: function (scope, element) { // Compile the contents. if (!compiledContents) { compiledContents = $compile(contents); } // Re-add the compiled contents to the element. - compiledContents(scope, function(clone) { + compiledContents(scope, function (clone) { element.append(clone); }); @@ -55,8 +57,9 @@ angular.module('oppia').factory( if (link && link.post) { link.post.apply(null, arguments); } - } + }, }; - } + }, }; - }]); + }, + ]); diff --git a/core/templates/services/ngb-modal.service.ts b/core/templates/services/ngb-modal.service.ts index da7a31582af1..186c23dd0574 100644 --- a/core/templates/services/ngb-modal.service.ts +++ b/core/templates/services/ngb-modal.service.ts @@ -16,8 +16,7 @@ * @fileoverview Downgraded NgbModal service for use in AngularJS files. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; -angular.module('oppia').factory( - 'NgbModal', downgradeInjectable(NgbModal)); +angular.module('oppia').factory('NgbModal', downgradeInjectable(NgbModal)); diff --git a/core/templates/services/number-conversion.service.spec.ts b/core/templates/services/number-conversion.service.spec.ts index a9a5904193d0..b393b059f852 100644 --- a/core/templates/services/number-conversion.service.spec.ts +++ b/core/templates/services/number-conversion.service.spec.ts @@ -16,9 +16,9 @@ * @fileoverview Unit tests for NumberConversionService. */ -import { NumberConversionService } from './number-conversion.service'; -import { I18nLanguageCodeService } from 'services/i18n-language-code.service'; -import { TestBed } from '@angular/core/testing'; +import {NumberConversionService} from './number-conversion.service'; +import {I18nLanguageCodeService} from 'services/i18n-language-code.service'; +import {TestBed} from '@angular/core/testing'; describe('NumberConversionService', () => { let i18nLanguageCodeService: I18nLanguageCodeService; @@ -26,14 +26,13 @@ describe('NumberConversionService', () => { beforeEach(() => { TestBed.configureTestingModule({ - providers: [NumberConversionService, - I18nLanguageCodeService] + providers: [NumberConversionService, I18nLanguageCodeService], }); numberConversionService = TestBed.inject(NumberConversionService); i18nLanguageCodeService = TestBed.inject(I18nLanguageCodeService); }); - it('should get the decimal separator depending on the page context', ()=>{ + it('should get the decimal separator depending on the page context', () => { i18nLanguageCodeService.setI18nLanguageCode('en'); expect(numberConversionService.currentDecimalSeparator()).toEqual('.'); @@ -44,39 +43,55 @@ describe('NumberConversionService', () => { expect(numberConversionService.currentDecimalSeparator()).toEqual(','); }); - it('should convert a number string to the English decimal number', ()=>{ + it('should convert a number string to the English decimal number', () => { let number1 = '-1.22'; let number2 = '1,5'; let number3 = '1,31e1'; let number4 = 'abc'; let number5 = 'e'; let number6 = ''; - spyOn(numberConversionService, 'currentDecimalSeparator') - .and.returnValues('.', ',', ',', '.', ',', '.'); + spyOn(numberConversionService, 'currentDecimalSeparator').and.returnValues( + '.', + ',', + ',', + '.', + ',', + '.' + ); - expect(numberConversionService.convertToEnglishDecimal(number1)) - .toEqual(-1.22); - expect(numberConversionService.convertToEnglishDecimal(number2)) - .toEqual(1.5); - expect(numberConversionService.convertToEnglishDecimal(number3)) - .toEqual(13.1); - expect(numberConversionService.convertToEnglishDecimal(number4)) - .toEqual(null); - expect(numberConversionService.convertToEnglishDecimal(number5)) - .toEqual(null); - expect(numberConversionService.convertToEnglishDecimal(number6)) - .toEqual(null); + expect(numberConversionService.convertToEnglishDecimal(number1)).toEqual( + -1.22 + ); + expect(numberConversionService.convertToEnglishDecimal(number2)).toEqual( + 1.5 + ); + expect(numberConversionService.convertToEnglishDecimal(number3)).toEqual( + 13.1 + ); + expect(numberConversionService.convertToEnglishDecimal(number4)).toEqual( + null + ); + expect(numberConversionService.convertToEnglishDecimal(number5)).toEqual( + null + ); + expect(numberConversionService.convertToEnglishDecimal(number6)).toEqual( + null + ); }); - it('should convert a number to the local format', ()=>{ + it('should convert a number to the local format', () => { let number = -198.234; - spyOn(numberConversionService, 'currentDecimalSeparator') - .and.returnValues('.', ','); + spyOn(numberConversionService, 'currentDecimalSeparator').and.returnValues( + '.', + ',' + ); - expect(numberConversionService.convertToLocalizedNumber(number)) - .toEqual('-198.234'); - expect(numberConversionService.convertToLocalizedNumber(number)) - .toEqual('-198,234'); + expect(numberConversionService.convertToLocalizedNumber(number)).toEqual( + '-198.234' + ); + expect(numberConversionService.convertToLocalizedNumber(number)).toEqual( + '-198,234' + ); }); }); diff --git a/core/templates/services/number-conversion.service.ts b/core/templates/services/number-conversion.service.ts index 0bcdd7274110..498d3c1f4eee 100644 --- a/core/templates/services/number-conversion.service.ts +++ b/core/templates/services/number-conversion.service.ts @@ -16,22 +16,20 @@ * @fileoverview Service for providing conversion services to the numeric input. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { I18nLanguageCodeService } from './i18n-language-code.service'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {AppConstants} from 'app.constants'; +import {I18nLanguageCodeService} from './i18n-language-code.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class NumberConversionService { - constructor( - private i18nLanguageCodeService: I18nLanguageCodeService, - ) {} + constructor(private i18nLanguageCodeService: I18nLanguageCodeService) {} currentDecimalSeparator(): string { - const currentLanguage = this.i18nLanguageCodeService - .getCurrentI18nLanguageCode(); + const currentLanguage = + this.i18nLanguageCodeService.getCurrentI18nLanguageCode(); const supportedLanguages = AppConstants.SUPPORTED_SITE_LANGUAGES; let decimalSeparator: string = '.'; @@ -44,8 +42,7 @@ export class NumberConversionService { return decimalSeparator; } - - convertToEnglishDecimal(number: string): (null | number) { + convertToEnglishDecimal(number: string): null | number { const decimalSeparator = this.currentDecimalSeparator(); let numString = number.replace(`${decimalSeparator}`, '.'); @@ -58,7 +55,7 @@ export class NumberConversionService { return engNum; } - convertToLocalizedNumber(number: number|string): string { + convertToLocalizedNumber(number: number | string): string { let decimalSeparator = this.currentDecimalSeparator(); let stringNumber = number.toString(); let convertedNumber: string = stringNumber; @@ -69,6 +66,9 @@ export class NumberConversionService { } } -angular.module('oppia').factory( - 'NumberConversionService', - downgradeInjectable(NumberConversionService)); +angular + .module('oppia') + .factory( + 'NumberConversionService', + downgradeInjectable(NumberConversionService) + ); diff --git a/core/templates/services/oppia-rte-parser.service.spec.ts b/core/templates/services/oppia-rte-parser.service.spec.ts index b76f2ef50e86..dcf4b916b09f 100644 --- a/core/templates/services/oppia-rte-parser.service.spec.ts +++ b/core/templates/services/oppia-rte-parser.service.spec.ts @@ -16,9 +16,13 @@ * @fileoverview Spec for service that parses rich text string. */ -import { DOCUMENT } from '@angular/common'; -import { TestBed } from '@angular/core/testing'; -import { OppiaRteNode, OppiaRteParserService, TextNode } from './oppia-rte-parser.service'; +import {DOCUMENT} from '@angular/common'; +import {TestBed} from '@angular/core/testing'; +import { + OppiaRteNode, + OppiaRteParserService, + TextNode, +} from './oppia-rte-parser.service'; describe('RTE parser service', () => { let rteParserService: OppiaRteParserService; @@ -75,43 +79,47 @@ describe('RTE parser service', () => { }); it('should generate a custom representation of the rte string', () => { - let testCases = [{ - rteString: '

HiHelloHello

' + - '' + - '', - representation: { - tag: 'body', - attrs: {}, - children: [ - { - tag: 'p', - attrs: {}, - children: [ - new TextNode('Hi'), - { - tag: 'em', - attrs: {}, - children: [new TextNode('Hello')] + let testCases = [ + { + rteString: + '

HiHelloHello

' + + '' + + '', + representation: { + tag: 'body', + attrs: {}, + children: [ + { + tag: 'p', + attrs: {}, + children: [ + new TextNode('Hi'), + { + tag: 'em', + attrs: {}, + children: [new TextNode('Hello')], + }, + new TextNode('Hello'), + ], + }, + { + tag: 'oppia-noninteractive-link', + attrs: { + urlWithValue: 'url', + linkWithValue: 'link', }, - new TextNode('Hello') - ] - }, - { - tag: 'oppia-noninteractive-link', - attrs: { - urlWithValue: 'url', - linkWithValue: 'link' - } - } - ] - } - }]; + }, + ], + }, + }, + ]; testCases.forEach(testCase => { let node = rteParserService.constructFromRteString(testCase.rteString); - expect( - compareRteNodeToObject(node, testCase.representation) - ).toBe(true, testCase.rteString); + expect(compareRteNodeToObject(node, testCase.representation)).toBe( + true, + testCase.rteString + ); }); }); @@ -131,8 +139,9 @@ describe('RTE parser service', () => { rteParserService.constructFromDomParser(new DummyHtmlElement()); }).toThrowError( 'tagName is undefined.\n' + - 'body: \n ' + - 'node: '); + 'body: \n ' + + 'node: ' + ); }); it('should parse a simple element', () => { @@ -148,18 +157,22 @@ describe('RTE parser service', () => { // by the prefix. In that case, the code will throw an error. it( 'should throw an error when a noninteractive component is not valid rte ' + - 'component', + 'component', () => { - let testCases = [{ - rteString: '' + - '', - errorString: 'Unexpected tag encountered: oppia-noninteractive-lin' - }]; + let testCases = [ + { + rteString: + '' + + '', + errorString: 'Unexpected tag encountered: oppia-noninteractive-lin', + }, + ]; testCases.forEach(testCase => { - expect( - () => rteParserService.constructFromRteString(testCase.rteString) + expect(() => + rteParserService.constructFromRteString(testCase.rteString) ).toThrowError(testCase.errorString); }); - }); + } + ); }); diff --git a/core/templates/services/oppia-rte-parser.service.ts b/core/templates/services/oppia-rte-parser.service.ts index f7cacdd212e1..2fcd6d1f12f9 100644 --- a/core/templates/services/oppia-rte-parser.service.ts +++ b/core/templates/services/oppia-rte-parser.service.ts @@ -16,15 +16,15 @@ * @fileoverview Service for parsing rich text string. */ -import { TemplatePortal } from '@angular/cdk/portal'; -import { Injectable } from '@angular/core'; -import { NoninteractiveCollapsible } from 'rich_text_components/Collapsible/directives/oppia-noninteractive-collapsible.component'; -import { NoninteractiveImage } from 'rich_text_components/Image/directives/oppia-noninteractive-image.component'; -import { NoninteractiveLink } from 'rich_text_components/Link/directives/oppia-noninteractive-link.component'; -import { NoninteractiveMath } from 'rich_text_components/Math/directives/oppia-noninteractive-math.component'; -import { NoninteractiveSkillreview } from 'rich_text_components/Skillreview/directives/oppia-noninteractive-skillreview.component'; -import { NoninteractiveTabs } from 'rich_text_components/Tabs/directives/oppia-noninteractive-tabs.component'; -import { NoninteractiveVideo } from 'rich_text_components/Video/directives/oppia-noninteractive-video.component'; +import {TemplatePortal} from '@angular/cdk/portal'; +import {Injectable} from '@angular/core'; +import {NoninteractiveCollapsible} from 'rich_text_components/Collapsible/directives/oppia-noninteractive-collapsible.component'; +import {NoninteractiveImage} from 'rich_text_components/Image/directives/oppia-noninteractive-image.component'; +import {NoninteractiveLink} from 'rich_text_components/Link/directives/oppia-noninteractive-link.component'; +import {NoninteractiveMath} from 'rich_text_components/Math/directives/oppia-noninteractive-math.component'; +import {NoninteractiveSkillreview} from 'rich_text_components/Skillreview/directives/oppia-noninteractive-skillreview.component'; +import {NoninteractiveTabs} from 'rich_text_components/Tabs/directives/oppia-noninteractive-tabs.component'; +import {NoninteractiveVideo} from 'rich_text_components/Video/directives/oppia-noninteractive-video.component'; const selectorToComponentClassMap = { 'oppia-noninteractive-collapsible': NoninteractiveCollapsible, @@ -33,7 +33,7 @@ const selectorToComponentClassMap = { 'oppia-noninteractive-math': NoninteractiveMath, 'oppia-noninteractive-skillreview': NoninteractiveSkillreview, 'oppia-noninteractive-tabs': NoninteractiveTabs, - 'oppia-noninteractive-video': NoninteractiveVideo + 'oppia-noninteractive-video': NoninteractiveVideo, }; export class TextNode { @@ -41,7 +41,6 @@ export class TextNode { constructor(public value: string) {} } - export class OppiaRteNode { children: (OppiaRteNode | TextNode)[] = []; parent: OppiaRteNode | null = null; @@ -63,7 +62,7 @@ export class OppiaRteNode { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class OppiaRteParserService { NON_INTERACTIVE_PREFIX = 'oppia-noninteractive-'; @@ -72,9 +71,10 @@ export class OppiaRteParserService { let arr = key.replace(/_/g, '-').split('-'); let capital = arr.map((item, index) => { // eslint-disable-next-line max-len - return index ? item.charAt(0).toUpperCase() + item.slice(1).toLowerCase() : item.toLowerCase(); - } - ); + return index + ? item.charAt(0).toUpperCase() + item.slice(1).toLowerCase() + : item.toLowerCase(); + }); return capital.join(''); } @@ -83,7 +83,8 @@ export class OppiaRteParserService { if (!node.tagName) { throw new Error( 'tagName is undefined.\n' + - `body: ${ body.outerHTML }\n node: ${ node.outerHTML }`); + `body: ${body.outerHTML}\n node: ${node.outerHTML}` + ); } const tagName = node.tagName.toLowerCase(); const attrs: Record = {}; @@ -110,8 +111,7 @@ export class OppiaRteParserService { const childNode = new OppiaRteNode(tagName, attrs); for (let child = 0; child < max; child++) { if (node.childNodes[child].nodeType === 3) { - const text = node.childNodes[child].nodeValue.replace( - /[\t\n]/g, ''); + const text = node.childNodes[child].nodeValue.replace(/[\t\n]/g, ''); childNode.children.push(new TextNode(text)); continue; } diff --git a/core/templates/services/page-head.service.spec.ts b/core/templates/services/page-head.service.spec.ts index d34076212cbf..ba43ff7914cd 100644 --- a/core/templates/services/page-head.service.spec.ts +++ b/core/templates/services/page-head.service.spec.ts @@ -16,11 +16,11 @@ * @fileoverview Unit tests for PageHeadService. */ -import { TestBed } from '@angular/core/testing'; +import {TestBed} from '@angular/core/testing'; -import { PageTitleService } from 'services/page-title.service'; -import { MetaTagCustomizationService } from './contextual/meta-tag-customization.service'; -import { PageHeadService } from './page-head.service'; +import {PageTitleService} from 'services/page-title.service'; +import {MetaTagCustomizationService} from './contextual/meta-tag-customization.service'; +import {PageHeadService} from './page-head.service'; describe('Page head service', () => { let pageHeadService: PageHeadService; @@ -38,7 +38,7 @@ describe('Page head service', () => { let meta = { PROPERTY_TYPE: '', PROPERTY_VALUE: '', - CONTENT: '' + CONTENT: '', }; spyOn(pageTitleService, 'setDocumentTitle'); spyOn(metaTagCustomizationService, 'addOrReplaceMetaTags'); @@ -46,13 +46,14 @@ describe('Page head service', () => { pageHeadService.updateTitleAndMetaTags(title, [meta]); expect(pageTitleService.setDocumentTitle).toHaveBeenCalledWith(title); - expect(metaTagCustomizationService.addOrReplaceMetaTags) - .toHaveBeenCalledWith([ - { - propertyType: meta.PROPERTY_TYPE, - propertyValue: meta.PROPERTY_VALUE, - content: meta.CONTENT - } - ]); + expect( + metaTagCustomizationService.addOrReplaceMetaTags + ).toHaveBeenCalledWith([ + { + propertyType: meta.PROPERTY_TYPE, + propertyValue: meta.PROPERTY_VALUE, + content: meta.CONTENT, + }, + ]); }); }); diff --git a/core/templates/services/page-head.service.ts b/core/templates/services/page-head.service.ts index ad6e64adc0d1..454aa66a57d4 100644 --- a/core/templates/services/page-head.service.ts +++ b/core/templates/services/page-head.service.ts @@ -16,10 +16,13 @@ * @fileoverview Service to update page's title and meta tags. */ -import { Injectable } from '@angular/core'; +import {Injectable} from '@angular/core'; -import { PageTitleService } from 'services/page-title.service'; -import { MetaAttribute, MetaTagCustomizationService } from './contextual/meta-tag-customization.service'; +import {PageTitleService} from 'services/page-title.service'; +import { + MetaAttribute, + MetaTagCustomizationService, +} from './contextual/meta-tag-customization.service'; interface MetaTagData { readonly PROPERTY_TYPE: string; @@ -28,7 +31,7 @@ interface MetaTagData { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class PageHeadService { constructor( @@ -37,7 +40,9 @@ export class PageHeadService { ) {} updateTitleAndMetaTags( - pageTitle: string, pageMetaAttributes: readonly MetaTagData[]): void { + pageTitle: string, + pageMetaAttributes: readonly MetaTagData[] + ): void { // Update default title. this.pageTitleService.setDocumentTitle(pageTitle); @@ -46,7 +51,7 @@ export class PageHeadService { metaAttributes.push({ propertyType: pageMetaAttributes[i].PROPERTY_TYPE, propertyValue: pageMetaAttributes[i].PROPERTY_VALUE, - content: pageMetaAttributes[i].CONTENT + content: pageMetaAttributes[i].CONTENT, }); } // Update meta tags. diff --git a/core/templates/services/page-title.service.spec.ts b/core/templates/services/page-title.service.spec.ts index dd3a7b72c5d1..1a844489e709 100644 --- a/core/templates/services/page-title.service.spec.ts +++ b/core/templates/services/page-title.service.spec.ts @@ -16,9 +16,9 @@ * @fileoverview Unit test for the page title service. */ -import { PageTitleService } from 'services/page-title.service'; -import { TestBed } from '@angular/core/testing'; -import { Title, Meta } from '@angular/platform-browser'; +import {PageTitleService} from 'services/page-title.service'; +import {TestBed} from '@angular/core/testing'; +import {Title, Meta} from '@angular/platform-browser'; describe('Page title service', () => { let pts: PageTitleService; @@ -27,7 +27,7 @@ describe('Page title service', () => { beforeEach(() => { TestBed.configureTestingModule({ - providers: [PageTitleService, Title, Meta] + providers: [PageTitleService, Title, Meta], }); titleService = TestBed.inject(Title); metaTagService = TestBed.inject(Meta); @@ -73,15 +73,15 @@ describe('Page title service', () => { expect(updateTagSpy).toHaveBeenCalledTimes(3); expect(updateTagSpy).toHaveBeenCalledWith({ name: 'description', - content: 'description_text' + content: 'description_text', }); expect(updateTagSpy).toHaveBeenCalledWith({ itemprop: 'description', - content: 'description_text' + content: 'description_text', }); expect(updateTagSpy).toHaveBeenCalledWith({ property: 'og:description', - content: 'description_text' + content: 'description_text', }); }); diff --git a/core/templates/services/page-title.service.ts b/core/templates/services/page-title.service.ts index efc81740c1b7..3b84abda5639 100644 --- a/core/templates/services/page-title.service.ts +++ b/core/templates/services/page-title.service.ts @@ -16,12 +16,12 @@ * @fileoverview Service to set and get the title of the page. */ -import { Injectable } from '@angular/core'; -import { Meta, Title } from '@angular/platform-browser'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {Meta, Title} from '@angular/platform-browser'; +import {downgradeInjectable} from '@angular/upgrade/static'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class PageTitleService { // These properties are initialized using Angular lifecycle hooks @@ -50,15 +50,15 @@ export class PageTitleService { updateMetaTag(content: string): void { this.metaTagService.updateTag({ name: 'description', - content: content + content: content, }); this.metaTagService.updateTag({ itemprop: 'description', - content: content + content: content, }); this.metaTagService.updateTag({ property: 'og:description', - content: content + content: content, }); } @@ -83,5 +83,6 @@ export class PageTitleService { } } -angular.module('oppia').factory( - 'PageTitleService', downgradeInjectable(PageTitleService)); +angular + .module('oppia') + .factory('PageTitleService', downgradeInjectable(PageTitleService)); diff --git a/core/templates/services/platform-feature.service.spec.ts b/core/templates/services/platform-feature.service.spec.ts index 6e7f65b0453e..1e9a0b63d03b 100644 --- a/core/templates/services/platform-feature.service.spec.ts +++ b/core/templates/services/platform-feature.service.spec.ts @@ -16,19 +16,20 @@ * @fileoverview Unit tests for PlatformFeatureService. */ -import { TestBed, fakeAsync, flushMicrotasks, tick } from - '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; - -import { WindowRef } from 'services/contextual/window-ref.service'; -import { PlatformFeatureService, platformFeatureInitFactory } from - 'services/platform-feature.service'; -import { FeatureFlagBackendApiService } from - 'domain/feature-flag/feature-flag-backend-api.service'; -import { FeatureNames, FeatureStatusSummary } from - 'domain/feature-flag/feature-status-summary.model'; -import { UrlService } from 'services/contextual/url.service'; - +import {TestBed, fakeAsync, flushMicrotasks, tick} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; + +import {WindowRef} from 'services/contextual/window-ref.service'; +import { + PlatformFeatureService, + platformFeatureInitFactory, +} from 'services/platform-feature.service'; +import {FeatureFlagBackendApiService} from 'domain/feature-flag/feature-flag-backend-api.service'; +import { + FeatureNames, + FeatureStatusSummary, +} from 'domain/feature-flag/feature-status-summary.model'; +import {UrlService} from 'services/contextual/url.service'; describe('PlatformFeatureService', () => { let windowRef: WindowRef; @@ -78,28 +79,28 @@ describe('PlatformFeatureService', () => { spyOnProperty(windowRef, 'nativeWindow').and.returnValue({ sessionStorage: { getItem: (key: string) => store[key] || null, - setItem: (key: string, value: string) => store[key] = value, - removeItem: (key: string) => delete store[key] + setItem: (key: string, value: string) => (store[key] = value), + removeItem: (key: string) => delete store[key], }, document: { get cookie() { return cookie; - } + }, }, navigator: { get userAgent() { return userAgent; - } - } + }, + }, } as unknown as Window); mockSessionStore = (obj: object) => { Object.assign(store, obj); }; - mockCookie = (cookieStr: string) => cookie = cookieStr; + mockCookie = (cookieStr: string) => (cookie = cookieStr); let pathName = '/'; spyOn(urlService, 'getPathname').and.callFake(() => pathName); - mockPathName = path => pathName = path; + mockPathName = path => (pathName = path); apiSpy = spyOn(apiService, 'fetchFeatureFlags').and.resolveTo( FeatureStatusSummary.createFromBackendDict({ @@ -113,8 +114,7 @@ describe('PlatformFeatureService', () => { const successHandler = jasmine.createSpy('success'); const failHandler = jasmine.createSpy('fail'); platformFeatureService = TestBed.inject(PlatformFeatureService); - platformFeatureService.initialize() - .then(successHandler, failHandler); + platformFeatureService.initialize().then(successHandler, failHandler); flushMicrotasks(); @@ -124,7 +124,33 @@ describe('PlatformFeatureService', () => { expect(platformFeatureService.isInitializedWithError).toBeFalse(); })); - it('should load from server if saved results have expired.', + it('should load from server if saved results have expired.', fakeAsync(() => { + const sessionId = 'session_id'; + mockCookie(`session=${sessionId}`); + mockSessionStore({ + SAVED_FEATURE_FLAGS: JSON.stringify({ + sessionId: sessionId, + timestamp: Date.now(), + featureStatusSummary: { + [FeatureNames.DummyFeatureFlagForE2ETests]: true, + }, + }), + }); + + // Ticks 13 hrs, as stored results are valid for 12 hrs, ths results + // should have expired. + tick(13 * 3600 * 1000); + platformFeatureService = TestBed.inject(PlatformFeatureService); + + flushMicrotasks(); + + expect(apiService.fetchFeatureFlags).toHaveBeenCalled(); + expect(platformFeatureService.isInitializedWithError).toBeFalse(); + })); + + it( + "should load from server if the stored features don't match with" + + ' feature list', fakeAsync(() => { const sessionId = 'session_id'; mockCookie(`session=${sessionId}`); @@ -132,15 +158,10 @@ describe('PlatformFeatureService', () => { SAVED_FEATURE_FLAGS: JSON.stringify({ sessionId: sessionId, timestamp: Date.now(), - featureStatusSummary: { - [FeatureNames.DummyFeatureFlagForE2ETests]: true, - } - }) + featureStatusSummary: {}, + }), }); - // Ticks 13 hrs, as stored results are valid for 12 hrs, ths results - // should have expired. - tick(13 * 3600 * 1000); platformFeatureService = TestBed.inject(PlatformFeatureService); flushMicrotasks(); @@ -150,38 +171,21 @@ describe('PlatformFeatureService', () => { }) ); - it('should load from server if the stored features don\'t match with' + - ' feature list', fakeAsync(() => { - const sessionId = 'session_id'; - mockCookie(`session=${sessionId}`); - mockSessionStore({ - SAVED_FEATURE_FLAGS: JSON.stringify({ - sessionId: sessionId, - timestamp: Date.now(), - featureStatusSummary: {} - }) - }); - - platformFeatureService = TestBed.inject(PlatformFeatureService); - - flushMicrotasks(); - - expect(apiService.fetchFeatureFlags).toHaveBeenCalled(); - expect(platformFeatureService.isInitializedWithError).toBeFalse(); - })); - - it('should request only once if there are more than one call to ' + - '.initialize.', fakeAsync(() => { - platformFeatureService = TestBed.inject(PlatformFeatureService); + it( + 'should request only once if there are more than one call to ' + + '.initialize.', + fakeAsync(() => { + platformFeatureService = TestBed.inject(PlatformFeatureService); - platformFeatureService.initialize(); - platformFeatureService.initialize(); + platformFeatureService.initialize(); + platformFeatureService.initialize(); - flushMicrotasks(); + flushMicrotasks(); - expect(apiService.fetchFeatureFlags).toHaveBeenCalledTimes(1); - expect(platformFeatureService.isInitializedWithError).toBeFalse(); - })); + expect(apiService.fetchFeatureFlags).toHaveBeenCalledTimes(1); + expect(platformFeatureService.isInitializedWithError).toBeFalse(); + }) + ); it('should disable all features when loading fails.', fakeAsync(() => { apiSpy.and.throwError('mock error'); @@ -220,21 +224,18 @@ describe('PlatformFeatureService', () => { expect(platformFeatureService.isInitializedWithError).toBeFalse(); })); - it('should throw error when accessed before initialization.', fakeAsync( - () => { - platformFeatureService = TestBed.inject(PlatformFeatureService); - expect( - () => ( - platformFeatureService.status.DummyFeatureFlagForE2ETests.isEnabled) - ).toThrowError( - 'The platform feature service has not been initialized.'); - }) - ); + it('should throw error when accessed before initialization.', fakeAsync(() => { + platformFeatureService = TestBed.inject(PlatformFeatureService); + expect( + () => + platformFeatureService.status.DummyFeatureFlagForE2ETests.isEnabled + ).toThrowError('The platform feature service has not been initialized.'); + })); }); describe('platformFeatureInitFactory', () => { let factoryFn = (service: PlatformFeatureService) => { - return async(): Promise => service.initialize(); + return async (): Promise => service.initialize(); }; beforeEach(() => { @@ -242,10 +243,11 @@ describe('PlatformFeatureService', () => { platformFeatureService = TestBed.inject(PlatformFeatureService); }); - it('should return a function that calls initialize', async() => { + it('should return a function that calls initialize', async () => { const mockPromise = Promise.resolve(); - const spy = spyOn(platformFeatureService, 'initialize') - .and.returnValue(mockPromise); + const spy = spyOn(platformFeatureService, 'initialize').and.returnValue( + mockPromise + ); const returnedFn = factoryFn(platformFeatureService); const returnedPromise = returnedFn(); diff --git a/core/templates/services/platform-feature.service.ts b/core/templates/services/platform-feature.service.ts index 3f7e7901fe5c..9270c5d49d44 100644 --- a/core/templates/services/platform-feature.service.ts +++ b/core/templates/services/platform-feature.service.ts @@ -34,17 +34,20 @@ * values in a new tab. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; - -import { FeatureStatusChecker, FeatureStatusSummary } from 'domain/feature-flag/feature-status-summary.model'; -import { FeatureFlagBackendApiService } from 'domain/feature-flag/feature-flag-backend-api.service'; -import { LoggerService } from 'services/contextual/logger.service'; -import { UrlService } from 'services/contextual/url.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; + +import { + FeatureStatusChecker, + FeatureStatusSummary, +} from 'domain/feature-flag/feature-status-summary.model'; +import {FeatureFlagBackendApiService} from 'domain/feature-flag/feature-flag-backend-api.service'; +import {LoggerService} from 'services/contextual/logger.service'; +import {UrlService} from 'services/contextual/url.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class PlatformFeatureService { private static SESSION_STORAGE_KEY = 'SAVED_FEATURE_FLAGS'; @@ -57,11 +60,11 @@ export class PlatformFeatureService { static _isSkipped = false; constructor( - private featureFlagBackendApiService: - FeatureFlagBackendApiService, - private windowRef: WindowRef, - private loggerService: LoggerService, - private urlService: UrlService) { + private featureFlagBackendApiService: FeatureFlagBackendApiService, + private windowRef: WindowRef, + private loggerService: LoggerService, + private urlService: UrlService + ) { this.initialize(); } @@ -132,22 +135,23 @@ export class PlatformFeatureService { // erased, leading to the 'Registration session expired' error. if (this.urlService.getPathname() === '/signup') { PlatformFeatureService._isSkipped = true; - PlatformFeatureService.featureStatusSummary = ( - FeatureStatusSummary.createDefault()); + PlatformFeatureService.featureStatusSummary = + FeatureStatusSummary.createDefault(); return; } - PlatformFeatureService.featureStatusSummary = ( - await this.loadFeatureFlagsFromServer()); + PlatformFeatureService.featureStatusSummary = + await this.loadFeatureFlagsFromServer(); } catch (err: unknown) { if (err instanceof Error) { this.loggerService.error( 'Error during initialization of PlatformFeatureService: ' + - `${err.message ? err.message : err}`); + `${err.message ? err.message : err}` + ); } // If any error, just disable all features. - PlatformFeatureService.featureStatusSummary = ( - FeatureStatusSummary.createDefault()); + PlatformFeatureService.featureStatusSummary = + FeatureStatusSummary.createDefault(); PlatformFeatureService._isInitializedWithError = true; this.clearSavedResults(); } @@ -162,13 +166,18 @@ export class PlatformFeatureService { */ private clearSavedResults(): void { this.windowRef.nativeWindow.sessionStorage.removeItem( - PlatformFeatureService.SESSION_STORAGE_KEY); + PlatformFeatureService.SESSION_STORAGE_KEY + ); } } export const platformFeatureInitFactory = (service: PlatformFeatureService) => { - return async(): Promise => service.initialize(); + return async (): Promise => service.initialize(); }; -angular.module('oppia').factory( - 'PlatformFeatureService', downgradeInjectable(PlatformFeatureService)); +angular + .module('oppia') + .factory( + 'PlatformFeatureService', + downgradeInjectable(PlatformFeatureService) + ); diff --git a/core/templates/services/playthrough-issues-backend-api.service.spec.ts b/core/templates/services/playthrough-issues-backend-api.service.spec.ts index 7f27fa57528a..c0824cd9ea64 100644 --- a/core/templates/services/playthrough-issues-backend-api.service.spec.ts +++ b/core/templates/services/playthrough-issues-backend-api.service.spec.ts @@ -16,12 +16,13 @@ * @fileoverview Unit tests for the issues backend api service. */ -import { HttpClientTestingModule, HttpTestingController } from - '@angular/common/http/testing'; -import { fakeAsync, flushMicrotasks, TestBed } from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {fakeAsync, flushMicrotasks, TestBed} from '@angular/core/testing'; -import { PlaythroughIssuesBackendApiService } from - 'services/playthrough-issues-backend-api.service'; +import {PlaythroughIssuesBackendApiService} from 'services/playthrough-issues-backend-api.service'; import { PlaythroughIssue, PlaythroughIssueType, @@ -32,22 +33,25 @@ describe('PlaythroughIssuesBackendApiService', () => { let httpTestingController: HttpTestingController; let playthroughIssuesBackendApiService: PlaythroughIssuesBackendApiService; - let backendIssues: PlaythroughIssueBackendDict[] = [{ - issue_type: PlaythroughIssueType.MultipleIncorrectSubmissions, - issue_customization_args: { - state_name: { value: 'state_name1' }, - num_times_answered_incorrectly: { value: 7 } + let backendIssues: PlaythroughIssueBackendDict[] = [ + { + issue_type: PlaythroughIssueType.MultipleIncorrectSubmissions, + issue_customization_args: { + state_name: {value: 'state_name1'}, + num_times_answered_incorrectly: {value: 7}, + }, + playthrough_ids: ['playthrough_id2'], + schema_version: 1, + is_valid: true, }, - playthrough_ids: ['playthrough_id2'], - schema_version: 1, - is_valid: true - }]; + ]; beforeEach(() => { - TestBed.configureTestingModule({ imports: [HttpClientTestingModule] }); + TestBed.configureTestingModule({imports: [HttpClientTestingModule]}); httpTestingController = TestBed.inject(HttpTestingController); playthroughIssuesBackendApiService = TestBed.inject( - PlaythroughIssuesBackendApiService); + PlaythroughIssuesBackendApiService + ); }); afterEach(() => { @@ -55,155 +59,171 @@ describe('PlaythroughIssuesBackendApiService', () => { }); describe('.fetch', () => { - it('should return the issues data provided by the backend', fakeAsync( - () => { - let successHandler = jasmine.createSpy('success'); - let failureHandler = jasmine.createSpy('failure'); - - playthroughIssuesBackendApiService.fetchIssuesAsync('7', 1).then( - successHandler, failureHandler); - - let req = httpTestingController.expectOne( - '/issuesdatahandler/7?exp_version=1'); - expect(req.request.method).toEqual('GET'); - req.flush({unresolved_issues: backendIssues}); - flushMicrotasks(); - - expect(successHandler).toHaveBeenCalledWith( - backendIssues.map( - PlaythroughIssue.createFromBackendDict)); - expect(failureHandler).not.toHaveBeenCalled(); - })); - - it('should use the rejection handler if the backend request failed.', - fakeAsync(() => { - var successHandler = jasmine.createSpy('success'); - var failHandler = jasmine.createSpy('fail'); - - playthroughIssuesBackendApiService.fetchIssuesAsync('7', 1).then( - successHandler, failHandler); - - var req = httpTestingController.expectOne( - '/issuesdatahandler/7?exp_version=1'); - expect(req.request.method).toEqual('GET'); - req.flush({ - error: 'Some error in the backend.' - }, { - status: 500, statusText: 'Internal Server Error' - }); - - flushMicrotasks(); - - expect(successHandler).not.toHaveBeenCalled(); - expect(failHandler).toHaveBeenCalledWith('Some error in the backend.'); - }) - ); + it('should return the issues data provided by the backend', fakeAsync(() => { + let successHandler = jasmine.createSpy('success'); + let failureHandler = jasmine.createSpy('failure'); - it('should not fetch an issue when another issue was already fetched', - fakeAsync(() => { - let successHandler = jasmine.createSpy('success'); - let failureHandler = jasmine.createSpy('failure'); - - playthroughIssuesBackendApiService.fetchIssuesAsync('7', 1).then( - successHandler, failureHandler); - - let req = httpTestingController.expectOne( - '/issuesdatahandler/7?exp_version=1'); - expect(req.request.method).toEqual('GET'); - req.flush({unresolved_issues: backendIssues}); - flushMicrotasks(); - - expect(successHandler).toHaveBeenCalledWith( - backendIssues.map( - PlaythroughIssue.createFromBackendDict)); - expect(failureHandler).not.toHaveBeenCalled(); - - // Try to fetch another issue. - playthroughIssuesBackendApiService.fetchIssuesAsync('8', 1).then( - successHandler, failureHandler); - - flushMicrotasks(); - - expect(successHandler).toHaveBeenCalledWith(backendIssues.map( - PlaythroughIssue.createFromBackendDict)); - expect(failureHandler).not.toHaveBeenCalled(); - })); - - it('should return the playthrough data provided by the backend', fakeAsync( - () => { - let backendPlaythrough: PlaythroughIssueBackendDict = { - issue_type: PlaythroughIssueType.EarlyQuit, - issue_customization_args: { - state_name: { value: 'state_name1' }, - time_spent_in_exp_in_msecs: { value: 200 } - }, - playthrough_ids: ['pID'], - schema_version: 2, - is_valid: true - }; - - let successHandler = jasmine.createSpy('success'); - let failureHandler = jasmine.createSpy('failure'); - - playthroughIssuesBackendApiService.fetchPlaythroughAsync('7', '1').then( - successHandler, failureHandler); - let req = httpTestingController.expectOne( - '/playthroughdatahandler/7/1'); - expect(req.request.method).toEqual('GET'); - req.flush(backendPlaythrough); - flushMicrotasks(); - - expect(successHandler).toHaveBeenCalledWith( - PlaythroughIssue.createFromBackendDict( - backendPlaythrough)); - expect(failureHandler).not.toHaveBeenCalled(); - })); - }); + playthroughIssuesBackendApiService + .fetchIssuesAsync('7', 1) + .then(successHandler, failureHandler); + + let req = httpTestingController.expectOne( + '/issuesdatahandler/7?exp_version=1' + ); + expect(req.request.method).toEqual('GET'); + req.flush({unresolved_issues: backendIssues}); + flushMicrotasks(); + + expect(successHandler).toHaveBeenCalledWith( + backendIssues.map(PlaythroughIssue.createFromBackendDict) + ); + expect(failureHandler).not.toHaveBeenCalled(); + })); - it('should use the rejection handler if the backend request failed.', - fakeAsync(() => { + it('should use the rejection handler if the backend request failed.', fakeAsync(() => { var successHandler = jasmine.createSpy('success'); var failHandler = jasmine.createSpy('fail'); - playthroughIssuesBackendApiService.fetchPlaythroughAsync('7', '1').then( - successHandler, failHandler); + playthroughIssuesBackendApiService + .fetchIssuesAsync('7', 1) + .then(successHandler, failHandler); var req = httpTestingController.expectOne( - '/playthroughdatahandler/7/1'); + '/issuesdatahandler/7?exp_version=1' + ); expect(req.request.method).toEqual('GET'); - req.flush({ - error: 'Some error in the backend.' - }, { - status: 500, statusText: 'Internal Server Error' - }); + req.flush( + { + error: 'Some error in the backend.', + }, + { + status: 500, + statusText: 'Internal Server Error', + } + ); flushMicrotasks(); expect(successHandler).not.toHaveBeenCalled(); expect(failHandler).toHaveBeenCalledWith('Some error in the backend.'); - }) - ); + })); + + it('should not fetch an issue when another issue was already fetched', fakeAsync(() => { + let successHandler = jasmine.createSpy('success'); + let failureHandler = jasmine.createSpy('failure'); + + playthroughIssuesBackendApiService + .fetchIssuesAsync('7', 1) + .then(successHandler, failureHandler); + + let req = httpTestingController.expectOne( + '/issuesdatahandler/7?exp_version=1' + ); + expect(req.request.method).toEqual('GET'); + req.flush({unresolved_issues: backendIssues}); + flushMicrotasks(); + + expect(successHandler).toHaveBeenCalledWith( + backendIssues.map(PlaythroughIssue.createFromBackendDict) + ); + expect(failureHandler).not.toHaveBeenCalled(); + + // Try to fetch another issue. + playthroughIssuesBackendApiService + .fetchIssuesAsync('8', 1) + .then(successHandler, failureHandler); + + flushMicrotasks(); + + expect(successHandler).toHaveBeenCalledWith( + backendIssues.map(PlaythroughIssue.createFromBackendDict) + ); + expect(failureHandler).not.toHaveBeenCalled(); + })); + + it('should return the playthrough data provided by the backend', fakeAsync(() => { + let backendPlaythrough: PlaythroughIssueBackendDict = { + issue_type: PlaythroughIssueType.EarlyQuit, + issue_customization_args: { + state_name: {value: 'state_name1'}, + time_spent_in_exp_in_msecs: {value: 200}, + }, + playthrough_ids: ['pID'], + schema_version: 2, + is_valid: true, + }; + + let successHandler = jasmine.createSpy('success'); + let failureHandler = jasmine.createSpy('failure'); + + playthroughIssuesBackendApiService + .fetchPlaythroughAsync('7', '1') + .then(successHandler, failureHandler); + let req = httpTestingController.expectOne('/playthroughdatahandler/7/1'); + expect(req.request.method).toEqual('GET'); + req.flush(backendPlaythrough); + flushMicrotasks(); + + expect(successHandler).toHaveBeenCalledWith( + PlaythroughIssue.createFromBackendDict(backendPlaythrough) + ); + expect(failureHandler).not.toHaveBeenCalled(); + })); + }); + + it('should use the rejection handler if the backend request failed.', fakeAsync(() => { + var successHandler = jasmine.createSpy('success'); + var failHandler = jasmine.createSpy('fail'); + + playthroughIssuesBackendApiService + .fetchPlaythroughAsync('7', '1') + .then(successHandler, failHandler); + + var req = httpTestingController.expectOne('/playthroughdatahandler/7/1'); + expect(req.request.method).toEqual('GET'); + req.flush( + { + error: 'Some error in the backend.', + }, + { + status: 500, + statusText: 'Internal Server Error', + } + ); + + flushMicrotasks(); + + expect(successHandler).not.toHaveBeenCalled(); + expect(failHandler).toHaveBeenCalledWith('Some error in the backend.'); + })); describe('.resolve', () => { it('should resolve an issue', fakeAsync(() => { let successHandler = jasmine.createSpy('success'); let failureHandler = jasmine.createSpy('failure'); let explorationId = '7'; - let playthroughIssue = PlaythroughIssue - .createFromBackendDict(backendIssues[0]); - - playthroughIssuesBackendApiService.fetchIssuesAsync('7', 1) - .then(async() => playthroughIssuesBackendApiService.resolveIssueAsync( - playthroughIssue, explorationId, 1)) + let playthroughIssue = PlaythroughIssue.createFromBackendDict( + backendIssues[0] + ); + + playthroughIssuesBackendApiService + .fetchIssuesAsync('7', 1) + .then(async () => + playthroughIssuesBackendApiService.resolveIssueAsync( + playthroughIssue, + explorationId, + 1 + ) + ) .then(successHandler, failureHandler); let req = httpTestingController.expectOne( - '/issuesdatahandler/7?exp_version=1'); + '/issuesdatahandler/7?exp_version=1' + ); expect(req.request.method).toEqual('GET'); req.flush({unresolved_issues: backendIssues}); flushMicrotasks(); - req = httpTestingController.expectOne( - '/resolveissuehandler/7'); + req = httpTestingController.expectOne('/resolveissuehandler/7'); req.flush(backendIssues); flushMicrotasks(); @@ -212,60 +232,72 @@ describe('PlaythroughIssuesBackendApiService', () => { expect(failureHandler).not.toHaveBeenCalled(); })); - it('should use the rejection handler if the backend request failed.', - fakeAsync(() => { - var successHandler = jasmine.createSpy('success'); - var failHandler = jasmine.createSpy('fail'); - let explorationId = '7'; - let playthroughIssue = PlaythroughIssue - .createFromBackendDict(backendIssues[0]); - - playthroughIssuesBackendApiService.fetchIssuesAsync('7', 1) - .then(async() => playthroughIssuesBackendApiService.resolveIssueAsync( - playthroughIssue, explorationId, 1)) - .then(successHandler, failHandler); - - let req = httpTestingController.expectOne( - '/issuesdatahandler/7?exp_version=1'); - expect(req.request.method).toEqual('GET'); - req.flush({unresolved_issues: backendIssues}); - flushMicrotasks(); - - req = httpTestingController.expectOne( - '/resolveissuehandler/7'); - expect(req.request.method).toEqual('POST'); - req.flush({ - error: 'Some error in the backend.' - }, { - status: 500, statusText: 'Internal Server Error' - }); - - flushMicrotasks(); - - expect(successHandler).not.toHaveBeenCalled(); - expect(failHandler).toHaveBeenCalledWith('Some error in the backend.'); - }) - ); + it('should use the rejection handler if the backend request failed.', fakeAsync(() => { + var successHandler = jasmine.createSpy('success'); + var failHandler = jasmine.createSpy('fail'); + let explorationId = '7'; + let playthroughIssue = PlaythroughIssue.createFromBackendDict( + backendIssues[0] + ); + + playthroughIssuesBackendApiService + .fetchIssuesAsync('7', 1) + .then(async () => + playthroughIssuesBackendApiService.resolveIssueAsync( + playthroughIssue, + explorationId, + 1 + ) + ) + .then(successHandler, failHandler); + + let req = httpTestingController.expectOne( + '/issuesdatahandler/7?exp_version=1' + ); + expect(req.request.method).toEqual('GET'); + req.flush({unresolved_issues: backendIssues}); + flushMicrotasks(); + + req = httpTestingController.expectOne('/resolveissuehandler/7'); + expect(req.request.method).toEqual('POST'); + req.flush( + { + error: 'Some error in the backend.', + }, + { + status: 500, + statusText: 'Internal Server Error', + } + ); + + flushMicrotasks(); + + expect(successHandler).not.toHaveBeenCalled(); + expect(failHandler).toHaveBeenCalledWith('Some error in the backend.'); + })); - it('should use the rejection handler when try to get non fetched issue', - fakeAsync(() => { - let successHandler = jasmine.createSpy('success'); - let failHandler = jasmine.createSpy('fail'); - let explorationId = '7'; - let playthroughIssue = PlaythroughIssue - .createFromBackendDict(backendIssues[0]); - - playthroughIssuesBackendApiService.resolveIssueAsync( - playthroughIssue, explorationId, 1).then(successHandler, failHandler); - let req = httpTestingController.expectOne( - '/resolveissuehandler/' + explorationId); - expect(req.request.method).toEqual('POST'); - req.flush(backendIssues); - flushMicrotasks(); - - expect(successHandler).not.toHaveBeenCalled(); - expect(failHandler).toHaveBeenCalledWith( - 'An issue which was not fetched from the backend has been resolved'); - })); + it('should use the rejection handler when try to get non fetched issue', fakeAsync(() => { + let successHandler = jasmine.createSpy('success'); + let failHandler = jasmine.createSpy('fail'); + let explorationId = '7'; + let playthroughIssue = PlaythroughIssue.createFromBackendDict( + backendIssues[0] + ); + + playthroughIssuesBackendApiService + .resolveIssueAsync(playthroughIssue, explorationId, 1) + .then(successHandler, failHandler); + let req = httpTestingController.expectOne( + '/resolveissuehandler/' + explorationId + ); + expect(req.request.method).toEqual('POST'); + req.flush(backendIssues); + flushMicrotasks(); + + expect(successHandler).not.toHaveBeenCalled(); + expect(failHandler).toHaveBeenCalledWith( + 'An issue which was not fetched from the backend has been resolved' + ); + })); }); }); diff --git a/core/templates/services/playthrough-issues-backend-api.service.ts b/core/templates/services/playthrough-issues-backend-api.service.ts index 939b7b1d4cec..7e3bb60042aa 100644 --- a/core/templates/services/playthrough-issues-backend-api.service.ts +++ b/core/templates/services/playthrough-issues-backend-api.service.ts @@ -16,115 +16,153 @@ * @fileoverview Service for fetching issues and playthroughs from the backend. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; export interface FetchIssuesResponseBackendDict { - 'unresolved_issues': PlaythroughIssueBackendDict[]; + unresolved_issues: PlaythroughIssueBackendDict[]; } import { PlaythroughIssue, - PlaythroughIssueBackendDict + PlaythroughIssueBackendDict, } from 'domain/statistics/playthrough-issue.model'; -import { ServicesConstants } from 'services/services.constants'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; +import {ServicesConstants} from 'services/services.constants'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; -@Injectable({ providedIn: 'root' }) +@Injectable({providedIn: 'root'}) export class PlaythroughIssuesBackendApiService { private cachedIssues: PlaythroughIssue[] = []; constructor( - private httpClient: HttpClient, - private urlInterpolationService: UrlInterpolationService) {} + private httpClient: HttpClient, + private urlInterpolationService: UrlInterpolationService + ) {} async fetchIssuesAsync( - explorationId: string, - explorationVersion: number): Promise { + explorationId: string, + explorationVersion: number + ): Promise { if (this.cachedIssues.length !== 0) { return Promise.resolve(this.cachedIssues); } return new Promise((resolve, reject) => { - this.httpClient.get( - this.getFetchIssuesUrl(explorationId), { - params: { exp_version: explorationVersion.toString() }}).toPromise() - .then(response => { - resolve(this.cachedIssues = response.unresolved_issues.map( - PlaythroughIssue.createFromBackendDict)); - }, errorResponse => { - reject(errorResponse.error.error); - }); + this.httpClient + .get( + this.getFetchIssuesUrl(explorationId), + { + params: {exp_version: explorationVersion.toString()}, + } + ) + .toPromise() + .then( + response => { + resolve( + (this.cachedIssues = response.unresolved_issues.map( + PlaythroughIssue.createFromBackendDict + )) + ); + }, + errorResponse => { + reject(errorResponse.error.error); + } + ); }); } async fetchPlaythroughAsync( - explorationId: string, - playthroughId: string): Promise { + explorationId: string, + playthroughId: string + ): Promise { return new Promise((resolve, reject) => { - this.httpClient.get( - this.getFetchPlaythroughUrl(explorationId, playthroughId)).toPromise() - .then(response => { - resolve(PlaythroughIssue.createFromBackendDict( - response)); - }, errorResponse => { - reject(errorResponse.error.error); - }); + this.httpClient + .get( + this.getFetchPlaythroughUrl(explorationId, playthroughId) + ) + .toPromise() + .then( + response => { + resolve(PlaythroughIssue.createFromBackendDict(response)); + }, + errorResponse => { + reject(errorResponse.error.error); + } + ); }); } async resolveIssueAsync( - issueToResolve: PlaythroughIssue, - explorationId: string, explorationVersion: number): Promise { + issueToResolve: PlaythroughIssue, + explorationId: string, + explorationVersion: number + ): Promise { return new Promise((resolve, reject) => { - this.httpClient.post(this.getResolveIssueUrl(explorationId), { - exp_issue_dict: issueToResolve.toBackendDict(), - exp_version: explorationVersion - }).toPromise() - .then(() => { - if (this.cachedIssues.length !== 0) { - const issueIndex = this.cachedIssues.findIndex( - issue => angular.equals(issue, issueToResolve)); - if (issueIndex !== -1) { - this.cachedIssues.splice(issueIndex, 1); - resolve(); + this.httpClient + .post(this.getResolveIssueUrl(explorationId), { + exp_issue_dict: issueToResolve.toBackendDict(), + exp_version: explorationVersion, + }) + .toPromise() + .then( + () => { + if (this.cachedIssues.length !== 0) { + const issueIndex = this.cachedIssues.findIndex(issue => + angular.equals(issue, issueToResolve) + ); + if (issueIndex !== -1) { + this.cachedIssues.splice(issueIndex, 1); + resolve(); + } } + reject( + 'An issue which was not fetched from the backend ' + + 'has been resolved' + ); + }, + errorResponse => { + reject(errorResponse.error.error); } - reject( - 'An issue which was not fetched from the backend ' + - 'has been resolved'); - }, errorResponse => { - reject(errorResponse.error.error); - }); + ); }); } private getFetchIssuesUrl(explorationId: string): string { return this.urlInterpolationService.interpolateUrl( - ServicesConstants.FETCH_ISSUES_URL, { - exploration_id: explorationId - }); + ServicesConstants.FETCH_ISSUES_URL, + { + exploration_id: explorationId, + } + ); } private getFetchPlaythroughUrl( - explorationId: string, playthroughId: string): string { + explorationId: string, + playthroughId: string + ): string { return this.urlInterpolationService.interpolateUrl( - ServicesConstants.FETCH_PLAYTHROUGH_URL, { + ServicesConstants.FETCH_PLAYTHROUGH_URL, + { exploration_id: explorationId, - playthrough_id: playthroughId - }); + playthrough_id: playthroughId, + } + ); } private getResolveIssueUrl(explorationId: string): string { return this.urlInterpolationService.interpolateUrl( - ServicesConstants.RESOLVE_ISSUE_URL, { - exploration_id: explorationId - }); + ServicesConstants.RESOLVE_ISSUE_URL, + { + exploration_id: explorationId, + } + ); } } -angular.module('oppia').factory( - 'PlaythroughIssuesBackendApiService', - downgradeInjectable(PlaythroughIssuesBackendApiService)); +angular + .module('oppia') + .factory( + 'PlaythroughIssuesBackendApiService', + downgradeInjectable(PlaythroughIssuesBackendApiService) + ); diff --git a/core/templates/services/playthrough-issues.service.spec.ts b/core/templates/services/playthrough-issues.service.spec.ts index 8d60417b263a..cae0cae63137 100644 --- a/core/templates/services/playthrough-issues.service.spec.ts +++ b/core/templates/services/playthrough-issues.service.spec.ts @@ -16,10 +16,10 @@ * @fileoverview Unit tests for PlaythroughIssuesService. */ -import { TestBed } from '@angular/core/testing'; -import { PlaythroughIssuesService } from './playthrough-issues.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { PlaythroughIssuesBackendApiService } from 'services/playthrough-issues-backend-api.service'; +import {TestBed} from '@angular/core/testing'; +import {PlaythroughIssuesService} from './playthrough-issues.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {PlaythroughIssuesBackendApiService} from 'services/playthrough-issues-backend-api.service'; describe('Playthrough Issues Service', () => { let playthroughIssuesService: PlaythroughIssuesService; @@ -28,18 +28,15 @@ describe('Playthrough Issues Service', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [ - PlaythroughIssuesService, - PlaythroughIssuesBackendApiService - ] + providers: [PlaythroughIssuesService, PlaythroughIssuesBackendApiService], }); playthroughIssuesService = TestBed.inject(PlaythroughIssuesService); - playthroughIssuesBackendApiService = ( - TestBed.inject(PlaythroughIssuesBackendApiService)); + playthroughIssuesBackendApiService = TestBed.inject( + PlaythroughIssuesBackendApiService + ); - spyOn(playthroughIssuesBackendApiService, 'fetchIssuesAsync') - .and.stub(); + spyOn(playthroughIssuesBackendApiService, 'fetchIssuesAsync').and.stub(); }); it('should be defined', () => { @@ -50,7 +47,8 @@ describe('Playthrough Issues Service', () => { playthroughIssuesService.getIssues(); - expect(playthroughIssuesBackendApiService.fetchIssuesAsync) - .toHaveBeenCalled(); + expect( + playthroughIssuesBackendApiService.fetchIssuesAsync + ).toHaveBeenCalled(); }); }); diff --git a/core/templates/services/playthrough-issues.service.ts b/core/templates/services/playthrough-issues.service.ts index 3aa246cfb200..ee8b28272276 100644 --- a/core/templates/services/playthrough-issues.service.ts +++ b/core/templates/services/playthrough-issues.service.ts @@ -16,13 +16,13 @@ * @fileoverview Service for retrieving issues and playthroughs. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { PlaythroughIssuesBackendApiService } from 'services/playthrough-issues-backend-api.service'; -import { PlaythroughIssue } from 'domain/statistics/playthrough-issue.model'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {PlaythroughIssuesBackendApiService} from 'services/playthrough-issues-backend-api.service'; +import {PlaythroughIssue} from 'domain/statistics/playthrough-issue.model'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class PlaythroughIssuesService { // These properties are initialized using init method and we need to do @@ -32,29 +32,33 @@ export class PlaythroughIssuesService { explorationVersion!: number; constructor( - private playthroughIssuesBackendApiService: - PlaythroughIssuesBackendApiService - ) { } + private playthroughIssuesBackendApiService: PlaythroughIssuesBackendApiService + ) {} /** Prepares the PlaythroughIssuesService for subsequent calls to other - * functions. - * - * @param {string} newExplorationId - the exploration id the service will - * be targeting. - * @param {number} newExplorationVersion - the version of the exploration - * the service will be targeting. - */ - initSession( - newExplorationId: string, newExplorationVersion: number): void { + * functions. + * + * @param {string} newExplorationId - the exploration id the service will + * be targeting. + * @param {number} newExplorationVersion - the version of the exploration + * the service will be targeting. + */ + initSession(newExplorationId: string, newExplorationVersion: number): void { this.explorationId = newExplorationId; this.explorationVersion = newExplorationVersion; } getIssues(): Promise { return this.playthroughIssuesBackendApiService.fetchIssuesAsync( - this.explorationId, this.explorationVersion); + this.explorationId, + this.explorationVersion + ); } } -angular.module('oppia').factory('PlaythroughIssuesService', - downgradeInjectable(PlaythroughIssuesService)); +angular + .module('oppia') + .factory( + 'PlaythroughIssuesService', + downgradeInjectable(PlaythroughIssuesService) + ); diff --git a/core/templates/services/playthrough.service.spec.ts b/core/templates/services/playthrough.service.spec.ts index fd700f89e238..c7bf0675aba3 100644 --- a/core/templates/services/playthrough.service.spec.ts +++ b/core/templates/services/playthrough.service.spec.ts @@ -16,18 +16,15 @@ * @fileoverview Unit tests for the playthrough service. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TestBed } from '@angular/core/testing'; - -import { ExplorationFeaturesService } from - 'services/exploration-features.service'; -import { LearnerAction } from - 'domain/statistics/learner-action.model'; -import { Playthrough } from 'domain/statistics/playthrough.model'; -import { PlaythroughService } from 'services/playthrough.service'; -import { PlaythroughBackendApiService } from - 'domain/statistics/playthrough-backend-api.service'; -import { Stopwatch } from 'domain/utilities/stopwatch.model'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TestBed} from '@angular/core/testing'; + +import {ExplorationFeaturesService} from 'services/exploration-features.service'; +import {LearnerAction} from 'domain/statistics/learner-action.model'; +import {Playthrough} from 'domain/statistics/playthrough.model'; +import {PlaythroughService} from 'services/playthrough.service'; +import {PlaythroughBackendApiService} from 'domain/statistics/playthrough-backend-api.service'; +import {Stopwatch} from 'domain/utilities/stopwatch.model'; describe('PlaythroughService', () => { let explorationFeaturesService: ExplorationFeaturesService; @@ -51,15 +48,26 @@ describe('PlaythroughService', () => { const recordStateTransitions = (stateNames: string[]) => { for (let i = 0; i < stateNames.length - 1; ++i) { playthroughService.recordAnswerSubmitAction( - stateNames[i], stateNames[i + 1], - 'TextInput', 'Hello', 'Correct', 30); + stateNames[i], + stateNames[i + 1], + 'TextInput', + 'Hello', + 'Correct', + 30 + ); } }; const recordIncorrectAnswers = (stateName: string, times: number) => { for (let i = 0; i < times; ++i) { playthroughService.recordAnswerSubmitAction( - stateName, stateName, 'TextInput', 'Hello', 'Wrong', 30); + stateName, + stateName, + 'TextInput', + 'Hello', + 'Wrong', + 30 + ); } }; @@ -69,7 +77,13 @@ describe('PlaythroughService', () => { const fromState = stateNames[i % stateNames.length]; const destState = stateNames[(i + 1) % stateNames.length]; playthroughService.recordAnswerSubmitAction( - fromState, destState, 'TextInput', 'Hello', 'Correct', 30); + fromState, + destState, + 'TextInput', + 'Hello', + 'Correct', + 30 + ); } }; @@ -82,14 +96,18 @@ describe('PlaythroughService', () => { }; const spyOnStorePlaythrough = ( - callback: ((p: Playthrough) => void) | null = null + callback: ((p: Playthrough) => void) | null = null ) => { if (callback) { - return spyOn(playthroughBackendApiService, 'storePlaythroughAsync') - .and.callFake(async(p: Playthrough, _: number) => callback(p)); + return spyOn( + playthroughBackendApiService, + 'storePlaythroughAsync' + ).and.callFake(async (p: Playthrough, _: number) => callback(p)); } else { return spyOn( - playthroughBackendApiService, 'storePlaythroughAsync').and.stub(); + playthroughBackendApiService, + 'storePlaythroughAsync' + ).and.stub(); } }; @@ -104,8 +122,10 @@ describe('PlaythroughService', () => { describe('Recording playthroughs', () => { beforeEach(() => { playthroughService.initSession('expId', 1, 1.0); - spyOn(explorationFeaturesService, 'isPlaythroughRecordingEnabled') - .and.returnValue(true); + spyOn( + explorationFeaturesService, + 'isPlaythroughRecordingEnabled' + ).and.returnValue(true); }); describe('Managing playthroughs', () => { @@ -133,7 +153,13 @@ describe('PlaythroughService', () => { mockTimedExplorationDurationInSecs(70); playthroughService.recordExplorationStartAction('A'); playthroughService.recordAnswerSubmitAction( - 'A', 'B', 'TextInput', 'Hello', 'Wrong!', 30); + 'A', + 'B', + 'TextInput', + 'Hello', + 'Wrong!', + 30 + ); playthroughService.recordExplorationQuitAction('B', 40); playthroughService.storePlaythrough(); @@ -166,16 +192,34 @@ describe('PlaythroughService', () => { // Actions which should be recorded (everything before quit). playthroughService.recordExplorationStartAction('A'); playthroughService.recordAnswerSubmitAction( - 'A', 'B', 'TextInput', 'Hello', 'Wrong!', 30); + 'A', + 'B', + 'TextInput', + 'Hello', + 'Wrong!', + 30 + ); playthroughService.recordExplorationQuitAction('B', 40); // Extraneous actions which should be ignored. playthroughService.recordExplorationStartAction('A'); playthroughService.recordExplorationStartAction('B'); playthroughService.recordAnswerSubmitAction( - 'A', 'B', 'TextInput', 'Hello', 'Try again', 30); + 'A', + 'B', + 'TextInput', + 'Hello', + 'Try again', + 30 + ); playthroughService.recordAnswerSubmitAction( - 'A', 'B', 'TextInput', 'Hello', 'Try again', 30); + 'A', + 'B', + 'TextInput', + 'Hello', + 'Try again', + 30 + ); playthroughService.recordExplorationQuitAction('B', 13); playthroughService.recordExplorationQuitAction('C', 13); playthroughService.storePlaythrough(); @@ -193,7 +237,13 @@ describe('PlaythroughService', () => { expect(storePlaythroughSpy).not.toHaveBeenCalled(); playthroughService.recordAnswerSubmitAction( - 'A', 'A', 'TextInput', 'Hello', 'Try again', 30); + 'A', + 'A', + 'TextInput', + 'Hello', + 'Try again', + 30 + ); playthroughService.storePlaythrough(); expect(storePlaythroughSpy).not.toHaveBeenCalled(); @@ -230,18 +280,21 @@ describe('PlaythroughService', () => { expect(storePlaythroughSpy).toHaveBeenCalled(); }); - it('should return null if state with multiple incorrect submissions is ' + - 'eventually completed', () => { - const storePlaythroughSpy = spyOnStorePlaythrough(); - mockTimedExplorationDurationInSecs(400); - playthroughService.recordExplorationStartAction('A'); - recordIncorrectAnswers('A', 5); - recordStateTransitions(['A', 'B', 'C']); - playthroughService.recordExplorationQuitAction('C', 60); - playthroughService.storePlaythrough(); + it( + 'should return null if state with multiple incorrect submissions is ' + + 'eventually completed', + () => { + const storePlaythroughSpy = spyOnStorePlaythrough(); + mockTimedExplorationDurationInSecs(400); + playthroughService.recordExplorationStartAction('A'); + recordIncorrectAnswers('A', 5); + recordStateTransitions(['A', 'B', 'C']); + playthroughService.recordExplorationQuitAction('C', 60); + playthroughService.storePlaythrough(); - expect(storePlaythroughSpy).not.toHaveBeenCalled(); - }); + expect(storePlaythroughSpy).not.toHaveBeenCalled(); + } + ); it('should identify cyclic state transitions', () => { const storePlaythroughSpy = spyOnStorePlaythrough(playthrough => { @@ -284,54 +337,60 @@ describe('PlaythroughService', () => { mockTimedExplorationDurationInSecs(400); }); - it('should identify p-shaped cyclic state transitions with cyclic ' + - 'portion at the tail', () => { - // P-shaped cycles look like: - // A - B - C - D - // | | - // F - E - // - // For this test, we check when the cyclic portion appears at the end - // (tail) of the playthrough. - const storePlaythroughSpy = spyOnStorePlaythrough(playthrough => { - expect(playthrough.issueType).toEqual('CyclicStateTransitions'); - expect(playthrough.issueCustomizationArgs).toEqual({ - state_names: {value: ['C', 'D', 'E', 'C']}, + it( + 'should identify p-shaped cyclic state transitions with cyclic ' + + 'portion at the tail', + () => { + // P-shaped cycles look like: + // A - B - C - D + // | | + // F - E + // + // For this test, we check when the cyclic portion appears at the end + // (tail) of the playthrough. + const storePlaythroughSpy = spyOnStorePlaythrough(playthrough => { + expect(playthrough.issueType).toEqual('CyclicStateTransitions'); + expect(playthrough.issueCustomizationArgs).toEqual({ + state_names: {value: ['C', 'D', 'E', 'C']}, + }); }); - }); - playthroughService.recordExplorationStartAction('A'); - recordStateTransitions(['A', 'B', 'C']); - recordCycle(['C', 'D', 'E'], 3); - playthroughService.recordExplorationQuitAction('C', 60); - playthroughService.storePlaythrough(); + playthroughService.recordExplorationStartAction('A'); + recordStateTransitions(['A', 'B', 'C']); + recordCycle(['C', 'D', 'E'], 3); + playthroughService.recordExplorationQuitAction('C', 60); + playthroughService.storePlaythrough(); - expect(storePlaythroughSpy).toHaveBeenCalled(); - }); + expect(storePlaythroughSpy).toHaveBeenCalled(); + } + ); - it('should identify p-shaped cyclic state transitions with cyclic ' + - 'portion at the head', () => { - // P-shaped cycles look like: - // D - A - E - F - // | | - // C - B - // - // For this test, we check when the cyclic portion appears at the start - // (head) of the playthrough. - const storePlaythroughSpy = spyOnStorePlaythrough(playthrough => { - expect(playthrough.issueType).toEqual('CyclicStateTransitions'); - expect(playthrough.issueCustomizationArgs).toEqual({ - state_names: {value: ['A', 'B', 'C', 'A']}, + it( + 'should identify p-shaped cyclic state transitions with cyclic ' + + 'portion at the head', + () => { + // P-shaped cycles look like: + // D - A - E - F + // | | + // C - B + // + // For this test, we check when the cyclic portion appears at the start + // (head) of the playthrough. + const storePlaythroughSpy = spyOnStorePlaythrough(playthrough => { + expect(playthrough.issueType).toEqual('CyclicStateTransitions'); + expect(playthrough.issueCustomizationArgs).toEqual({ + state_names: {value: ['A', 'B', 'C', 'A']}, + }); }); - }); - playthroughService.recordExplorationStartAction('A'); - recordCycle(['A', 'B', 'C'], 3); - recordStateTransitions(['A', 'D', 'E']); - playthroughService.recordExplorationQuitAction('F', 60); - playthroughService.storePlaythrough(); + playthroughService.recordExplorationStartAction('A'); + recordCycle(['A', 'B', 'C'], 3); + recordStateTransitions(['A', 'D', 'E']); + playthroughService.recordExplorationQuitAction('F', 60); + playthroughService.storePlaythrough(); - expect(storePlaythroughSpy).toHaveBeenCalled(); - }); + expect(storePlaythroughSpy).toHaveBeenCalled(); + } + ); it('should identify cycle within an otherwise linear path', () => { const storePlaythroughSpy = spyOnStorePlaythrough(playthrough => { @@ -406,95 +465,104 @@ describe('PlaythroughService', () => { expect(storePlaythroughSpy).not.toHaveBeenCalled(); }); - it('should return most recent cycle when there are many with same ' + - 'number of occurrences', () => { - const storePlaythroughSpy = spyOnStorePlaythrough(playthrough => { - expect(playthrough).not.toBeNull(); - expect(playthrough.issueType).toEqual('CyclicStateTransitions'); - expect(playthrough.issueCustomizationArgs).toEqual({ - state_names: {value: ['S', 'T', 'S']}, + it( + 'should return most recent cycle when there are many with same ' + + 'number of occurrences', + () => { + const storePlaythroughSpy = spyOnStorePlaythrough(playthrough => { + expect(playthrough).not.toBeNull(); + expect(playthrough.issueType).toEqual('CyclicStateTransitions'); + expect(playthrough.issueCustomizationArgs).toEqual({ + state_names: {value: ['S', 'T', 'S']}, + }); }); - }); - playthroughService.recordExplorationStartAction('A'); - recordCycle(['A', 'B'], 3); - recordStateTransitions(['A', 'C']); - recordCycle(['C', 'D'], 3); - recordStateTransitions(['C', 'E']); - recordCycle(['E', 'F'], 3); - recordStateTransitions(['E', 'G']); - recordCycle(['G', 'H'], 3); - recordStateTransitions(['G', 'I']); - recordCycle(['I', 'J'], 3); - recordStateTransitions(['I', 'K']); - recordCycle(['K', 'L'], 3); - recordStateTransitions(['K', 'M']); - recordCycle(['M', 'N'], 3); - recordStateTransitions(['M', 'O']); - recordCycle(['O', 'P'], 3); - recordStateTransitions(['O', 'Q']); - recordCycle(['Q', 'R'], 3); - recordStateTransitions(['Q', 'S']); - recordCycle(['S', 'T'], 3); - recordStateTransitions(['S', 'U']); - playthroughService.recordExplorationQuitAction('U', 30); - playthroughService.storePlaythrough(); + playthroughService.recordExplorationStartAction('A'); + recordCycle(['A', 'B'], 3); + recordStateTransitions(['A', 'C']); + recordCycle(['C', 'D'], 3); + recordStateTransitions(['C', 'E']); + recordCycle(['E', 'F'], 3); + recordStateTransitions(['E', 'G']); + recordCycle(['G', 'H'], 3); + recordStateTransitions(['G', 'I']); + recordCycle(['I', 'J'], 3); + recordStateTransitions(['I', 'K']); + recordCycle(['K', 'L'], 3); + recordStateTransitions(['K', 'M']); + recordCycle(['M', 'N'], 3); + recordStateTransitions(['M', 'O']); + recordCycle(['O', 'P'], 3); + recordStateTransitions(['O', 'Q']); + recordCycle(['Q', 'R'], 3); + recordStateTransitions(['Q', 'S']); + recordCycle(['S', 'T'], 3); + recordStateTransitions(['S', 'U']); + playthroughService.recordExplorationQuitAction('U', 30); + playthroughService.storePlaythrough(); - expect(storePlaythroughSpy).toHaveBeenCalled(); - }); + expect(storePlaythroughSpy).toHaveBeenCalled(); + } + ); - it('should not report issue if state is not visited from the same card ' + - 'enough times', () => { - const storePlaythroughSpy = spyOnStorePlaythrough(playthrough => { - expect(playthrough).not.toBeNull(); - expect(playthrough.issueType).toBeNull(); - expect(playthrough.issueCustomizationArgs).toBeNull(); - }); + it( + 'should not report issue if state is not visited from the same card ' + + 'enough times', + () => { + const storePlaythroughSpy = spyOnStorePlaythrough(playthrough => { + expect(playthrough).not.toBeNull(); + expect(playthrough.issueType).toBeNull(); + expect(playthrough.issueCustomizationArgs).toBeNull(); + }); - playthroughService.recordExplorationStartAction('A'); - recordCycle(['A', 'B'], 1); - recordCycle(['A', 'C'], 1); - recordCycle(['A', 'D'], 1); - playthroughService.recordExplorationQuitAction('A', 60); - playthroughService.storePlaythrough(); + playthroughService.recordExplorationStartAction('A'); + recordCycle(['A', 'B'], 1); + recordCycle(['A', 'C'], 1); + recordCycle(['A', 'D'], 1); + playthroughService.recordExplorationQuitAction('A', 60); + playthroughService.storePlaythrough(); - expect(storePlaythroughSpy).not.toHaveBeenCalled(); - }); + expect(storePlaythroughSpy).not.toHaveBeenCalled(); + } + ); }); describe('Issue prioritization', () => { - it('should prioritize multiple incorrect submissions over cyclic state ' + - 'transitions and early quit', () => { - const storePlaythroughSpy = spyOnStorePlaythrough(playthrough => { - expect(playthrough.issueType).toEqual('MultipleIncorrectSubmissions'); - }); - - mockTimedExplorationDurationInSecs(50); - playthroughService.recordExplorationStartAction('A'); - recordCycle(['A', 'B'], 3); - recordIncorrectAnswers('A', 5); - playthroughService.recordExplorationQuitAction('A', 10); - playthroughService.storePlaythrough(); - - expect(storePlaythroughSpy).toHaveBeenCalled(); - }); - - it('should prioritize multiple incorrect submissions over early quit', + it( + 'should prioritize multiple incorrect submissions over cyclic state ' + + 'transitions and early quit', () => { const storePlaythroughSpy = spyOnStorePlaythrough(playthrough => { - expect(playthrough.issueType) - .toEqual('MultipleIncorrectSubmissions'); + expect(playthrough.issueType).toEqual( + 'MultipleIncorrectSubmissions' + ); }); mockTimedExplorationDurationInSecs(50); playthroughService.recordExplorationStartAction('A'); + recordCycle(['A', 'B'], 3); recordIncorrectAnswers('A', 5); playthroughService.recordExplorationQuitAction('A', 10); playthroughService.storePlaythrough(); expect(storePlaythroughSpy).toHaveBeenCalled(); + } + ); + + it('should prioritize multiple incorrect submissions over early quit', () => { + const storePlaythroughSpy = spyOnStorePlaythrough(playthrough => { + expect(playthrough.issueType).toEqual('MultipleIncorrectSubmissions'); }); + mockTimedExplorationDurationInSecs(50); + playthroughService.recordExplorationStartAction('A'); + recordIncorrectAnswers('A', 5); + playthroughService.recordExplorationQuitAction('A', 10); + playthroughService.storePlaythrough(); + + expect(storePlaythroughSpy).toHaveBeenCalled(); + }); + it('should prioritize cyclic state transitions over early quit', () => { const storePlaythroughSpy = spyOnStorePlaythrough(playthrough => { expect(playthrough.issueType).toEqual('CyclicStateTransitions'); @@ -523,25 +591,26 @@ describe('PlaythroughService', () => { expect(storePlaythroughSpy).not.toHaveBeenCalled(); }); - it('should not store playthrough if learner did not submit any answers', - () => { - const storePlaythroughSpy = spyOnStorePlaythrough(); + it('should not store playthrough if learner did not submit any answers', () => { + const storePlaythroughSpy = spyOnStorePlaythrough(); - mockTimedExplorationDurationInSecs(60); - playthroughService.recordExplorationStartAction('A'); - playthroughService.recordExplorationQuitAction('A', 60); - playthroughService.storePlaythrough(); + mockTimedExplorationDurationInSecs(60); + playthroughService.recordExplorationStartAction('A'); + playthroughService.recordExplorationQuitAction('A', 60); + playthroughService.storePlaythrough(); - expect(storePlaythroughSpy).not.toHaveBeenCalled(); - }); + expect(storePlaythroughSpy).not.toHaveBeenCalled(); + }); }); }); describe('Disabling playthrough recordings', () => { it('should not record learner actions when recording is disabled', () => { const storePlaythroughSpy = spyOnStorePlaythrough(); - spyOn(explorationFeaturesService, 'isPlaythroughRecordingEnabled') - .and.returnValue(false); + spyOn( + explorationFeaturesService, + 'isPlaythroughRecordingEnabled' + ).and.returnValue(false); playthroughService.initSession('expId', 1, 1.0); @@ -555,8 +624,10 @@ describe('PlaythroughService', () => { }); it('should not record learner that is not in sample population', () => { - spyOn(explorationFeaturesService, 'isPlaythroughRecordingEnabled') - .and.returnValue(false); + spyOn( + explorationFeaturesService, + 'isPlaythroughRecordingEnabled' + ).and.returnValue(false); const storePlaythroughSpy = spyOnStorePlaythrough(); const sampleSizePopulationProportion = 0.6; @@ -564,7 +635,10 @@ describe('PlaythroughService', () => { mockTimedExplorationDurationInSecs(400); playthroughService.initSession( - 'expId', 1, sampleSizePopulationProportion); + 'expId', + 1, + sampleSizePopulationProportion + ); playthroughService.recordExplorationStartAction('A'); recordIncorrectAnswers('A', 5); playthroughService.recordExplorationQuitAction('A', 10); diff --git a/core/templates/services/playthrough.service.ts b/core/templates/services/playthrough.service.ts index 893358713d91..8b40f6a86529 100644 --- a/core/templates/services/playthrough.service.ts +++ b/core/templates/services/playthrough.service.ts @@ -16,25 +16,21 @@ * @fileoverview Service for recording and scrutinizing playthroughs. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { AppConstants } from 'app.constants'; -import { ExplorationFeaturesService } from - 'services/exploration-features.service'; +import {AppConstants} from 'app.constants'; +import {ExplorationFeaturesService} from 'services/exploration-features.service'; import { CyclicStateTransitionsCustomizationArgs, EarlyQuitCustomizationArgs, - MultipleIncorrectSubmissionsCustomizationArgs + MultipleIncorrectSubmissionsCustomizationArgs, } from 'domain/statistics/playthrough-issue.model'; -import { LearnerAction } from - 'domain/statistics/learner-action.model'; -import { Playthrough } from - 'domain/statistics/playthrough.model'; -import { PlaythroughBackendApiService } from - 'domain/statistics/playthrough-backend-api.service'; -import { ServicesConstants } from 'services/services.constants'; -import { Stopwatch } from 'domain/utilities/stopwatch.model'; +import {LearnerAction} from 'domain/statistics/learner-action.model'; +import {Playthrough} from 'domain/statistics/playthrough.model'; +import {PlaythroughBackendApiService} from 'domain/statistics/playthrough-backend-api.service'; +import {ServicesConstants} from 'services/services.constants'; +import {Stopwatch} from 'domain/utilities/stopwatch.model'; class CyclicStateTransitionsTracker { /** A path of visited states without any repeats. */ @@ -85,8 +81,9 @@ class CyclicStateTransitionsTracker { return; } if (this.pathOfVisitedStates.includes(destStateName)) { - const cycleOfVisitedStates = ( - this.makeCycle(this.pathOfVisitedStates.indexOf(destStateName))); + const cycleOfVisitedStates = this.makeCycle( + this.pathOfVisitedStates.indexOf(destStateName) + ); if (angular.equals(this.cycleOfVisitedStates, cycleOfVisitedStates)) { this.numLoops += 1; } else { @@ -100,7 +97,7 @@ class CyclicStateTransitionsTracker { generateIssueCustomizationArgs(): CyclicStateTransitionsCustomizationArgs { return { - state_names: {value: this.cycleOfVisitedStates} + state_names: {value: this.cycleOfVisitedStates}, }; } @@ -124,7 +121,8 @@ class EarlyQuitTracker { foundAnIssue(): boolean { return ( - this.expDurationInSecs < ServicesConstants.EARLY_QUIT_THRESHOLD_IN_SECS); + this.expDurationInSecs < ServicesConstants.EARLY_QUIT_THRESHOLD_IN_SECS + ); } recordExplorationQuit(stateName: string, expDurationInSecs: number): void { @@ -162,8 +160,7 @@ class MultipleIncorrectAnswersTracker { } } - generateIssueCustomizationArgs( - ): MultipleIncorrectSubmissionsCustomizationArgs { + generateIssueCustomizationArgs(): MultipleIncorrectSubmissionsCustomizationArgs { return { state_name: {value: this.currStateName}, num_times_answered_incorrectly: {value: this.numTries}, @@ -172,7 +169,7 @@ class MultipleIncorrectAnswersTracker { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class PlaythroughService { // These properties are initialized using initSession method @@ -189,17 +186,19 @@ export class PlaythroughService { private learnerIsInSamplePopulation: boolean = false; constructor( - private explorationFeaturesService: ExplorationFeaturesService, - private playthroughBackendApiService: PlaythroughBackendApiService, + private explorationFeaturesService: ExplorationFeaturesService, + private playthroughBackendApiService: PlaythroughBackendApiService ) {} initSession( - explorationId: string, explorationVersion: number, - sampleSizePopulationProportion: number): void { + explorationId: string, + explorationVersion: number, + sampleSizePopulationProportion: number + ): void { this.explorationId = explorationId; this.explorationVersion = explorationVersion; - this.learnerIsInSamplePopulation = ( - Math.random() < sampleSizePopulationProportion); + this.learnerIsInSamplePopulation = + Math.random() < sampleSizePopulationProportion; } recordExplorationStartAction(initStateName: string): void { @@ -210,7 +209,7 @@ export class PlaythroughService { this.recordedLearnerActions = [ LearnerAction.createNewExplorationStartAction({ state_name: {value: initStateName}, - }) + }), ]; this.eqTracker = new EarlyQuitTracker(); @@ -223,8 +222,13 @@ export class PlaythroughService { } recordAnswerSubmitAction( - stateName: string, destStateName: string, interactionId: string, - answer: string, feedback: string, timeSpentInStateSecs: number): void { + stateName: string, + destStateName: string, + interactionId: string, + answer: string, + feedback: string, + timeSpentInStateSecs: number + ): void { if (!this.hasRecordingBegun() || this.hasRecordingFinished()) { return; } @@ -236,15 +240,18 @@ export class PlaythroughService { interaction_id: {value: interactionId}, submitted_answer: {value: answer}, feedback: {value: feedback}, - time_spent_state_in_msecs: {value: 1000 * timeSpentInStateSecs} - })); + time_spent_state_in_msecs: {value: 1000 * timeSpentInStateSecs}, + }) + ); this.misTracker.recordStateTransition(destStateName); this.cstTracker.recordStateTransition(destStateName); } recordExplorationQuitAction( - stateName: string, timeSpentInStateSecs: number): void { + stateName: string, + timeSpentInStateSecs: number + ): void { if (!this.hasRecordingBegun() || this.hasRecordingFinished()) { return; } @@ -252,13 +259,15 @@ export class PlaythroughService { this.recordedLearnerActions.push( LearnerAction.createNewExplorationQuitAction({ state_name: {value: stateName}, - time_spent_in_state_in_msecs: {value: 1000 * timeSpentInStateSecs} - })); + time_spent_in_state_in_msecs: {value: 1000 * timeSpentInStateSecs}, + }) + ); - this.playthroughDurationInSecs = ( - this.playthroughStopwatch.getTimeInSecs()); + this.playthroughDurationInSecs = this.playthroughStopwatch.getTimeInSecs(); this.eqTracker.recordExplorationQuit( - stateName, this.playthroughDurationInSecs); + stateName, + this.playthroughDurationInSecs + ); } storePlaythrough(): void { @@ -281,23 +290,26 @@ export class PlaythroughService { */ private createNewPlaythrough(): Playthrough | null { if (this.misTracker && this.misTracker.foundAnIssue()) { - return Playthrough - .createNewMultipleIncorrectSubmissionsPlaythrough( - this.explorationId, this.explorationVersion, - this.misTracker.generateIssueCustomizationArgs(), - this.recordedLearnerActions); + return Playthrough.createNewMultipleIncorrectSubmissionsPlaythrough( + this.explorationId, + this.explorationVersion, + this.misTracker.generateIssueCustomizationArgs(), + this.recordedLearnerActions + ); } else if (this.cstTracker && this.cstTracker.foundAnIssue()) { - return Playthrough - .createNewCyclicStateTransitionsPlaythrough( - this.explorationId, this.explorationVersion, - this.cstTracker.generateIssueCustomizationArgs(), - this.recordedLearnerActions); + return Playthrough.createNewCyclicStateTransitionsPlaythrough( + this.explorationId, + this.explorationVersion, + this.cstTracker.generateIssueCustomizationArgs(), + this.recordedLearnerActions + ); } else if (this.eqTracker && this.eqTracker.foundAnIssue()) { - return Playthrough - .createNewEarlyQuitPlaythrough( - this.explorationId, this.explorationVersion, - this.eqTracker.generateIssueCustomizationArgs(), - this.recordedLearnerActions); + return Playthrough.createNewEarlyQuitPlaythrough( + this.explorationId, + this.explorationVersion, + this.eqTracker.generateIssueCustomizationArgs(), + this.recordedLearnerActions + ); } return null; } @@ -305,15 +317,15 @@ export class PlaythroughService { private isPlaythroughRecordingEnabled(): boolean { return ( this.explorationFeaturesService.isPlaythroughRecordingEnabled() && - this.learnerIsInSamplePopulation === true); + this.learnerIsInSamplePopulation === true + ); } private hasRecordingBegun(): boolean { return ( // Check this.recordedLearnerActions because // it could be null before recording begun. - this.recordedLearnerActions && - this.isPlaythroughRecordingEnabled() + this.recordedLearnerActions && this.isPlaythroughRecordingEnabled() ); } @@ -322,7 +334,8 @@ export class PlaythroughService { this.hasRecordingBegun() && this.recordedLearnerActions.length > 1 && this.recordedLearnerActions[this.recordedLearnerActions.length - 1] - .actionType === AppConstants.ACTION_TYPE_EXPLORATION_QUIT); + .actionType === AppConstants.ACTION_TYPE_EXPLORATION_QUIT + ); } private isRecordedPlaythroughHelpful(): boolean { @@ -331,13 +344,15 @@ export class PlaythroughService { this.hasRecordingFinished() && // Playthroughs are only helpful if learners have attempted an answer. this.recordedLearnerActions.some( - a => a.actionType === AppConstants.ACTION_TYPE_ANSWER_SUBMIT) && + a => a.actionType === AppConstants.ACTION_TYPE_ANSWER_SUBMIT + ) && // Playthroughs are only helpful if learners have invested enough time. this.playthroughDurationInSecs >= - ServicesConstants.MIN_PLAYTHROUGH_DURATION_IN_SECS); + ServicesConstants.MIN_PLAYTHROUGH_DURATION_IN_SECS + ); } } -angular.module('oppia').factory( - 'PlaythroughService', - downgradeInjectable(PlaythroughService)); +angular + .module('oppia') + .factory('PlaythroughService', downgradeInjectable(PlaythroughService)); diff --git a/core/templates/services/prevent-page-unload-event.service.spec.ts b/core/templates/services/prevent-page-unload-event.service.spec.ts index 45e6c67ea4ac..693918696bc8 100644 --- a/core/templates/services/prevent-page-unload-event.service.spec.ts +++ b/core/templates/services/prevent-page-unload-event.service.spec.ts @@ -16,12 +16,11 @@ * @fileoverview Unit tests for the preventPageUnloadEventService. */ -import { TestBed } from '@angular/core/testing'; -import { PreventPageUnloadEventService } - from 'services/prevent-page-unload-event.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import {TestBed} from '@angular/core/testing'; +import {PreventPageUnloadEventService} from 'services/prevent-page-unload-event.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; -describe ('Prevent page unload event service', function() { +describe('Prevent page unload event service', function () { let preventPageUnloadEventService: PreventPageUnloadEventService; let windowRef: WindowRef; @@ -32,18 +31,18 @@ describe ('Prevent page unload event service', function() { beforeEach(() => { TestBed.configureTestingModule({ - providers: [PreventPageUnloadEventService] + providers: [PreventPageUnloadEventService], }); preventPageUnloadEventService = TestBed.inject( - PreventPageUnloadEventService); + PreventPageUnloadEventService + ); windowRef = TestBed.inject(WindowRef); }); - // Mocking window object here because beforeunload requres the // full page to reload. Page reloads raise an error in karma. var mockWindow = { - addEventListener: function(eventname: string, callback: () => {}) { + addEventListener: function (eventname: string, callback: () => {}) { document.addEventListener('mock' + eventname, callback); }, location: { @@ -51,8 +50,8 @@ describe ('Prevent page unload event service', function() { if (val) { document.dispatchEvent(reloadEvt); } - } - } + }, + }, } as Window; it('should adding listener', () => { diff --git a/core/templates/services/prevent-page-unload-event.service.ts b/core/templates/services/prevent-page-unload-event.service.ts index e7037c5c6ea9..c4be09d05c20 100644 --- a/core/templates/services/prevent-page-unload-event.service.ts +++ b/core/templates/services/prevent-page-unload-event.service.ts @@ -16,18 +16,20 @@ * @fileoverview Service to handle prevent reload events. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {WindowRef} from 'services/contextual/window-ref.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class PreventPageUnloadEventService { private listenerActive: boolean; validationCallback: undefined | (() => boolean); _preventPageUnloadEventHandlerBind?: ( - this: Window, ev: BeforeUnloadEvent) => void; + this: Window, + ev: BeforeUnloadEvent + ) => void; constructor(private windowRef: WindowRef) { this.listenerActive = false; @@ -44,7 +46,10 @@ export class PreventPageUnloadEventService { this._preventPageUnloadEventHandlerBind = this._preventPageUnloadEventHandler.bind(null, this.validationCallback); this.windowRef.nativeWindow.addEventListener( - 'beforeunload', this._preventPageUnloadEventHandlerBind, true); + 'beforeunload', + this._preventPageUnloadEventHandlerBind, + true + ); this.listenerActive = true; } @@ -53,12 +58,17 @@ export class PreventPageUnloadEventService { return; } this.windowRef.nativeWindow.removeEventListener( - 'beforeunload', this._preventPageUnloadEventHandlerBind, true); + 'beforeunload', + this._preventPageUnloadEventHandlerBind, + true + ); this.listenerActive = false; } private _preventPageUnloadEventHandler( - validationCallback: () => boolean, e: BeforeUnloadEvent): void { + validationCallback: () => boolean, + e: BeforeUnloadEvent + ): void { if (validationCallback()) { // The preventDefault call is used to trigger a confirmation // before leaving. @@ -81,6 +91,9 @@ export class PreventPageUnloadEventService { } } -angular.module('oppia').factory( - 'PreventPageUnloadEventService', - downgradeInjectable(PreventPageUnloadEventService)); +angular + .module('oppia') + .factory( + 'PreventPageUnloadEventService', + downgradeInjectable(PreventPageUnloadEventService) + ); diff --git a/core/templates/services/promo-bar-backend-api.service.spec.ts b/core/templates/services/promo-bar-backend-api.service.spec.ts index 231253d76553..07be01105ca9 100644 --- a/core/templates/services/promo-bar-backend-api.service.spec.ts +++ b/core/templates/services/promo-bar-backend-api.service.spec.ts @@ -16,29 +16,30 @@ * @fileoverview Unit tests for PromoBarBackendApiService. */ -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; -import { PromoBar } from 'domain/promo_bar/promo-bar.model'; -import { PromoBarBackendApiService } from 'services/promo-bar-backend-api.service'; -import { ServicesConstants } from './services.constants'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; +import {PromoBar} from 'domain/promo_bar/promo-bar.model'; +import {PromoBarBackendApiService} from 'services/promo-bar-backend-api.service'; +import {ServicesConstants} from './services.constants'; describe('Promo bar backend api service', () => { const initialValue = ServicesConstants.ENABLE_PROMO_BAR; - let promoBarBackendApiService: - PromoBarBackendApiService; + let promoBarBackendApiService: PromoBarBackendApiService; let httpTestingController: HttpTestingController; let promoBar = { promoBarEnabled: true, - promoBarMessage: 'test message' + promoBarMessage: 'test message', }; beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [PromoBarBackendApiService] + providers: [PromoBarBackendApiService], }); - promoBarBackendApiService = TestBed.get( - PromoBarBackendApiService); + promoBarBackendApiService = TestBed.get(PromoBarBackendApiService); httpTestingController = TestBed.get(HttpTestingController); }); @@ -46,124 +47,127 @@ describe('Promo bar backend api service', () => { httpTestingController.verify(); }); - it('should successfully fetch data from the backend', - fakeAsync(() => { - let successHandler = jasmine.createSpy('success'); - let failHandler = jasmine.createSpy('fail'); + it('should successfully fetch data from the backend', fakeAsync(() => { + let successHandler = jasmine.createSpy('success'); + let failHandler = jasmine.createSpy('fail'); - promoBarBackendApiService.getPromoBarDataAsync() - .then(successHandler, failHandler); + promoBarBackendApiService + .getPromoBarDataAsync() + .then(successHandler, failHandler); - let req = httpTestingController.expectOne('/promo_bar_handler'); - expect(req.request.method).toEqual('GET'); - req.flush(promoBar); + let req = httpTestingController.expectOne('/promo_bar_handler'); + expect(req.request.method).toEqual('GET'); + req.flush(promoBar); - flushMicrotasks(); + flushMicrotasks(); - expect(successHandler).toHaveBeenCalled(); - expect(failHandler).not.toHaveBeenCalled(); - }) - ); + expect(successHandler).toHaveBeenCalled(); + expect(failHandler).not.toHaveBeenCalled(); + })); - it('should use rejection handler if data backend request failed', - fakeAsync(() => { - let successHandler = jasmine.createSpy('success'); - let failHandler = jasmine.createSpy('fail'); + it('should use rejection handler if data backend request failed', fakeAsync(() => { + let successHandler = jasmine.createSpy('success'); + let failHandler = jasmine.createSpy('fail'); - promoBarBackendApiService.getPromoBarDataAsync() - .then(successHandler, failHandler); + promoBarBackendApiService + .getPromoBarDataAsync() + .then(successHandler, failHandler); - let req = httpTestingController.expectOne('/promo_bar_handler'); - expect(req.request.method).toEqual('GET'); - req.flush({ + let req = httpTestingController.expectOne('/promo_bar_handler'); + expect(req.request.method).toEqual('GET'); + req.flush( + { error: 'Error loading data.', - }, { - status: 500, statusText: 'Invalid Request' - }); - - flushMicrotasks(); - - expect(successHandler).not.toHaveBeenCalled(); - expect(failHandler).toHaveBeenCalled(); - }) - ); - - it('should make request to update promo bar platform param data', - fakeAsync(() => { - let successHandler = jasmine.createSpy('success'); - let failHandler = jasmine.createSpy('fail'); - - promoBarBackendApiService.updatePromoBarDataAsync(true, 'New message') - .then(successHandler, failHandler); - - let req = httpTestingController.expectOne('/promo_bar_handler'); - expect(req.request.method).toEqual('PUT'); - expect(req.request.body).toEqual({ - promo_bar_enabled: true, - promo_bar_message: 'New message' - }); - req.flush({ status: 200, statusText: 'Success.'}); - flushMicrotasks(); - - expect(successHandler).toHaveBeenCalled(); - expect(failHandler).not.toHaveBeenCalled(); - }) - ); - - it('should make request to update promo bar platform param data', - fakeAsync(() => { - let successHandler = jasmine.createSpy('success'); - let failHandler = jasmine.createSpy('fail'); - - promoBarBackendApiService.updatePromoBarDataAsync(true, 'New message') - .then(successHandler, failHandler); - - let req = httpTestingController.expectOne('/promo_bar_handler'); - expect(req.request.method).toEqual('PUT'); - expect(req.request.body).toEqual({ - promo_bar_enabled: true, - promo_bar_message: 'New message' - }); - req.flush({ - error: 'You don\'t have rights to updated promo bar config data.', - }, { - status: 401, statusText: 'Invalid Request' - }); - - flushMicrotasks(); - - expect(successHandler).not.toHaveBeenCalled(); - expect(failHandler).toHaveBeenCalled(); - }) - ); - - it('should not fetch data from the backend when promo bar is not enabled', - fakeAsync(() => { - // This throws "Cannot assign to 'ENABLE_PROMO_BAR' because it - // is a read-only property.". We need to suppress this error because - // we need to change the value of 'ENABLE_PROMO_BAR' for testing - // purposes. - // @ts-expect-error - ServicesConstants.ENABLE_PROMO_BAR = false; - - let successHandler = jasmine.createSpy('success').and.callFake((data) => { - expect(data).toEqual(PromoBar.createEmpty()); - }); - let failHandler = jasmine.createSpy('fail'); - - promoBarBackendApiService.getPromoBarDataAsync() - .then(successHandler, failHandler); - flushMicrotasks(); - - expect(successHandler).toHaveBeenCalled(); - expect(failHandler).not.toHaveBeenCalled(); - - // This throws "Cannot assign to 'ENABLE_PROMO_BAR' because it - // is a read-only property.". We need to suppress this error because - // we need to change the value of 'ENABLE_PROMO_BAR' for testing - // purposes. - // @ts-expect-error - ServicesConstants.ENABLE_PROMO_BAR = initialValue; - }) - ); + }, + { + status: 500, + statusText: 'Invalid Request', + } + ); + + flushMicrotasks(); + + expect(successHandler).not.toHaveBeenCalled(); + expect(failHandler).toHaveBeenCalled(); + })); + + it('should make request to update promo bar platform param data', fakeAsync(() => { + let successHandler = jasmine.createSpy('success'); + let failHandler = jasmine.createSpy('fail'); + + promoBarBackendApiService + .updatePromoBarDataAsync(true, 'New message') + .then(successHandler, failHandler); + + let req = httpTestingController.expectOne('/promo_bar_handler'); + expect(req.request.method).toEqual('PUT'); + expect(req.request.body).toEqual({ + promo_bar_enabled: true, + promo_bar_message: 'New message', + }); + req.flush({status: 200, statusText: 'Success.'}); + flushMicrotasks(); + + expect(successHandler).toHaveBeenCalled(); + expect(failHandler).not.toHaveBeenCalled(); + })); + + it('should make request to update promo bar platform param data', fakeAsync(() => { + let successHandler = jasmine.createSpy('success'); + let failHandler = jasmine.createSpy('fail'); + + promoBarBackendApiService + .updatePromoBarDataAsync(true, 'New message') + .then(successHandler, failHandler); + + let req = httpTestingController.expectOne('/promo_bar_handler'); + expect(req.request.method).toEqual('PUT'); + expect(req.request.body).toEqual({ + promo_bar_enabled: true, + promo_bar_message: 'New message', + }); + req.flush( + { + error: "You don't have rights to updated promo bar config data.", + }, + { + status: 401, + statusText: 'Invalid Request', + } + ); + + flushMicrotasks(); + + expect(successHandler).not.toHaveBeenCalled(); + expect(failHandler).toHaveBeenCalled(); + })); + + it('should not fetch data from the backend when promo bar is not enabled', fakeAsync(() => { + // This throws "Cannot assign to 'ENABLE_PROMO_BAR' because it + // is a read-only property.". We need to suppress this error because + // we need to change the value of 'ENABLE_PROMO_BAR' for testing + // purposes. + // @ts-expect-error + ServicesConstants.ENABLE_PROMO_BAR = false; + + let successHandler = jasmine.createSpy('success').and.callFake(data => { + expect(data).toEqual(PromoBar.createEmpty()); + }); + let failHandler = jasmine.createSpy('fail'); + + promoBarBackendApiService + .getPromoBarDataAsync() + .then(successHandler, failHandler); + flushMicrotasks(); + + expect(successHandler).toHaveBeenCalled(); + expect(failHandler).not.toHaveBeenCalled(); + + // This throws "Cannot assign to 'ENABLE_PROMO_BAR' because it + // is a read-only property.". We need to suppress this error because + // we need to change the value of 'ENABLE_PROMO_BAR' for testing + // purposes. + // @ts-expect-error + ServicesConstants.ENABLE_PROMO_BAR = initialValue; + })); }); diff --git a/core/templates/services/promo-bar-backend-api.service.ts b/core/templates/services/promo-bar-backend-api.service.ts index cc9674ab350a..7ae1346eb932 100644 --- a/core/templates/services/promo-bar-backend-api.service.ts +++ b/core/templates/services/promo-bar-backend-api.service.ts @@ -16,15 +16,15 @@ * @fileoverview The backend API service to fetch promo bar data. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { PromoBar, PromoBarBackendDict } from 'domain/promo_bar/promo-bar.model'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {PromoBar, PromoBarBackendDict} from 'domain/promo_bar/promo-bar.model'; -import { ServicesConstants } from 'services/services.constants'; +import {ServicesConstants} from 'services/services.constants'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class PromoBarBackendApiService { constructor(private http: HttpClient) {} @@ -37,34 +37,46 @@ export class PromoBarBackendApiService { } return new Promise((resolve, reject) => { - this.http.get( - ServicesConstants.PROMO_BAR_URL, {} - ).toPromise().then((response: PromoBarBackendDict) => { - resolve(PromoBar.createFromBackendDict(response)); - }, errorResponse => { - reject(errorResponse.error.error); - }); + this.http + .get(ServicesConstants.PROMO_BAR_URL, {}) + .toPromise() + .then( + (response: PromoBarBackendDict) => { + resolve(PromoBar.createFromBackendDict(response)); + }, + errorResponse => { + reject(errorResponse.error.error); + } + ); }); } async updatePromoBarDataAsync( - promoBarEnabled: boolean, promoBarMessage: string): Promise { + promoBarEnabled: boolean, + promoBarMessage: string + ): Promise { return new Promise((resolve, reject) => { - this.http.put( - ServicesConstants.PROMO_BAR_URL, { + this.http + .put(ServicesConstants.PROMO_BAR_URL, { promo_bar_enabled: promoBarEnabled, - promo_bar_message: promoBarMessage - } - ).toPromise().then(() => { - resolve(); - }, errorResponse => { - reject(errorResponse.error.error); - }); + promo_bar_message: promoBarMessage, + }) + .toPromise() + .then( + () => { + resolve(); + }, + errorResponse => { + reject(errorResponse.error.error); + } + ); }); } } -angular.module('oppia').factory( - 'PromoBarBackendApiService', - downgradeInjectable(PromoBarBackendApiService) -); +angular + .module('oppia') + .factory( + 'PromoBarBackendApiService', + downgradeInjectable(PromoBarBackendApiService) + ); diff --git a/core/templates/services/question-validation.service.spec.ts b/core/templates/services/question-validation.service.spec.ts index 4e1388d79073..17f804e58659 100644 --- a/core/templates/services/question-validation.service.spec.ts +++ b/core/templates/services/question-validation.service.spec.ts @@ -16,14 +16,20 @@ * @fileoverview Unit tests for QuestionValidationService. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TestBed, waitForAsync } from '@angular/core/testing'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { QuestionBackendDict, QuestionObjectFactory } from 'domain/question/QuestionObjectFactory'; -import { MisconceptionObjectFactory, MisconceptionSkillMap } from 'domain/skill/MisconceptionObjectFactory'; -import { ResponsesService } from 'pages/exploration-editor-page/editor-tab/services/responses.service'; -import { QuestionValidationService } from './question-validation.service'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TestBed, waitForAsync} from '@angular/core/testing'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import { + QuestionBackendDict, + QuestionObjectFactory, +} from 'domain/question/QuestionObjectFactory'; +import { + MisconceptionObjectFactory, + MisconceptionSkillMap, +} from 'domain/skill/MisconceptionObjectFactory'; +import {ResponsesService} from 'pages/exploration-editor-page/editor-tab/services/responses.service'; +import {QuestionValidationService} from './question-validation.service'; describe('Question Validation Service', () => { let misconceptionObjectFactory: MisconceptionObjectFactory; @@ -38,11 +44,8 @@ describe('Question Validation Service', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [ - StateEditorService, - ResponsesService, - ], - schemas: [NO_ERRORS_SCHEMA] + providers: [StateEditorService, ResponsesService], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -63,81 +66,88 @@ describe('Question Validation Service', () => { question_state_data: { content: { html: 'Question 1', - content_id: 'content_1' + content_id: 'content_1', }, interaction: { - answer_groups: [{ - outcome: { - dest: 'outcome 1', - dest_if_really_stuck: null, - feedback: { - content_id: 'content_5', - html: '' + answer_groups: [ + { + outcome: { + dest: 'outcome 1', + dest_if_really_stuck: null, + feedback: { + content_id: 'content_5', + html: '', + }, + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, }, - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null + rule_specs: [ + { + rule_type: 'Equals', + inputs: {x: 10}, + }, + ], + tagged_skill_misconception_id: null, }, - rule_specs: [{ - rule_type: 'Equals', - inputs: {x: 10} - }], - tagged_skill_misconception_id: null - }, { - outcome: { - dest: 'outcome 1', - dest_if_really_stuck: null, - feedback: { - content_id: 'content_5', - html: '' + { + outcome: { + dest: 'outcome 1', + dest_if_really_stuck: null, + feedback: { + content_id: 'content_5', + html: '', + }, + labelled_as_correct: false, + param_changes: [], + refresher_exploration_id: null, }, - labelled_as_correct: false, - param_changes: [], - refresher_exploration_id: null + rule_specs: [ + { + rule_type: 'Equals', + inputs: {x: 10}, + }, + ], + tagged_skill_misconception_id: 'abc-1', }, - rule_specs: [{ - rule_type: 'Equals', - inputs: {x: 10} - }], - tagged_skill_misconception_id: 'abc-1' - }], + ], confirmed_unclassified_answers: [], customization_args: { placeholder: { value: { content_id: 'ca_placeholder_0', - unicode_str: '' - } + unicode_str: '', + }, }, - rows: { value: 1 } + rows: {value: 1}, }, default_outcome: { dest: null, dest_if_really_stuck: null, feedback: { html: 'Correct Answer', - content_id: 'content_2' + content_id: 'content_2', }, param_changes: [], - labelled_as_correct: false + labelled_as_correct: false, }, hints: [ { hint_content: { html: 'Hint 1', - content_id: 'content_3' - } - } + content_id: 'content_3', + }, + }, ], solution: { correct_answer: 'This is the correct answer', answer_is_exclusive: false, explanation: { html: 'Solution explanation', - content_id: 'content_4' - } + content_id: 'content_4', + }, }, - id: 'TextInput' + id: 'TextInput', }, param_changes: [], recorded_voiceovers: { @@ -146,23 +156,33 @@ describe('Question Validation Service', () => { content_2: {}, content_3: {}, content_4: {}, - content_5: {} - } + content_5: {}, + }, }, - solicit_answer_details: false + solicit_answer_details: false, }, language_code: 'en', version: 1, linked_skill_ids: ['abc'], - inapplicable_skill_misconception_ids: ['abc-2'] + inapplicable_skill_misconception_ids: ['abc-2'], } as unknown as QuestionBackendDict; mockMisconceptionObject = { abc: [ misconceptionObjectFactory.create( - 1, 'misc1', 'notes1', 'feedback1', true), + 1, + 'misc1', + 'notes1', + 'feedback1', + true + ), misconceptionObjectFactory.create( - 2, 'misc2', 'notes2', 'feedback1', false) - ] + 2, + 'misc2', + 'notes2', + 'feedback1', + false + ), + ], }; }); @@ -172,7 +192,9 @@ describe('Question Validation Service', () => { expect( qvs.isQuestionValid( questionObjectFactory.createFromBackendDict(mockQuestionDict), - mockMisconceptionObject)).toBeFalse(); + mockMisconceptionObject + ) + ).toBeFalse(); }); it('should return false if misconceptions are not addressed', () => { @@ -181,7 +203,9 @@ describe('Question Validation Service', () => { expect( qvs.isQuestionValid( questionObjectFactory.createFromBackendDict(mockQuestionDict), - mockMisconceptionObject)).toBeFalse(); + mockMisconceptionObject + ) + ).toBeFalse(); }); it('should return false if solution is invalid', () => { @@ -189,12 +213,14 @@ describe('Question Validation Service', () => { expect( qvs.isQuestionValid( questionObjectFactory.createFromBackendDict(mockQuestionDict), - mockMisconceptionObject)).toBeFalse(); + mockMisconceptionObject + ) + ).toBeFalse(); }); it('should return true if validation is successful', () => { - let question = questionObjectFactory.createFromBackendDict( - mockQuestionDict); + let question = + questionObjectFactory.createFromBackendDict(mockQuestionDict); expect(qvs.isQuestionValid(question, mockMisconceptionObject)).toBeTrue(); }); @@ -204,8 +230,8 @@ describe('Question Validation Service', () => { describe('getValidationErrorMessage()', () => { it('should return null when there are no errors', () => { - const question = questionObjectFactory.createFromBackendDict( - mockQuestionDict); + const question = + questionObjectFactory.createFromBackendDict(mockQuestionDict); expect(qvs.getValidationErrorMessage(question)).toBeNull(); }); @@ -215,75 +241,84 @@ describe('Question Validation Service', () => { // error because the object is initialized in the beforeEach(). // @ts-ignore interaction.default_outcome.feedback.html = ''; - const question = questionObjectFactory.createFromBackendDict( - mockQuestionDict); + const question = + questionObjectFactory.createFromBackendDict(mockQuestionDict); expect(qvs.getValidationErrorMessage(question)).toEqual( - 'Please enter a feedback for the default outcome.'); + 'Please enter a feedback for the default outcome.' + ); }); - it('should return null if no feedback for default outcome but default ' + - 'answer group is hidden', () => { - shouldHideDefaultAnswerGroupSpy.and.returnValue(true); - const interaction = mockQuestionDict.question_state_data.interaction; - // This throws "Object is possibly 'null'.". We need to suppress this - // error because the object is initialized in the beforeEach(). - // @ts-ignore - interaction.default_outcome.feedback.html = ''; - const question = questionObjectFactory.createFromBackendDict( - mockQuestionDict); + it( + 'should return null if no feedback for default outcome but default ' + + 'answer group is hidden', + () => { + shouldHideDefaultAnswerGroupSpy.and.returnValue(true); + const interaction = mockQuestionDict.question_state_data.interaction; + // This throws "Object is possibly 'null'.". We need to suppress this + // error because the object is initialized in the beforeEach(). + // @ts-ignore + interaction.default_outcome.feedback.html = ''; + const question = + questionObjectFactory.createFromBackendDict(mockQuestionDict); - expect(qvs.getValidationErrorMessage(question)).toBeNull(); - }); + expect(qvs.getValidationErrorMessage(question)).toBeNull(); + } + ); it('should return error message if no answer is marked correct', () => { const interaction = mockQuestionDict.question_state_data.interaction; interaction.answer_groups[0].outcome.labelled_as_correct = false; - const question = questionObjectFactory.createFromBackendDict( - mockQuestionDict); + const question = + questionObjectFactory.createFromBackendDict(mockQuestionDict); expect(qvs.getValidationErrorMessage(question)).toEqual( - 'At least one answer should be marked correct'); + 'At least one answer should be marked correct' + ); }); it('should return error message if no solution', () => { const interaction = mockQuestionDict.question_state_data.interaction; interaction.solution = null; - const question = questionObjectFactory.createFromBackendDict( - mockQuestionDict); + const question = + questionObjectFactory.createFromBackendDict(mockQuestionDict); expect(qvs.getValidationErrorMessage(question)).toEqual( - 'A solution must be specified'); + 'A solution must be specified' + ); }); it('should return error message if no hint', () => { const interaction = mockQuestionDict.question_state_data.interaction; interaction.hints = []; - const question = questionObjectFactory.createFromBackendDict( - mockQuestionDict); + const question = + questionObjectFactory.createFromBackendDict(mockQuestionDict); expect(qvs.getValidationErrorMessage(question)).toEqual( - 'At least 1 hint should be specified'); + 'At least 1 hint should be specified' + ); }); it('should return error message if no interaction', () => { const interaction = mockQuestionDict.question_state_data.interaction; interaction.id = null; - const question = questionObjectFactory.createFromBackendDict( - mockQuestionDict); + const question = + questionObjectFactory.createFromBackendDict(mockQuestionDict); expect(qvs.getValidationErrorMessage(question)).toEqual( - 'An interaction must be specified'); + 'An interaction must be specified' + ); }); it('should return error message if no question content', () => { const questionContent = mockQuestionDict.question_state_data.content; questionContent.html = ''; - const question = questionObjectFactory.createFromBackendDict( - mockQuestionDict); + const question = + questionObjectFactory.createFromBackendDict(mockQuestionDict); expect(qvs.getValidationErrorMessage(question)).toEqual( - 'Please enter a question.'); + 'Please enter a question.' + ); }); }); }); diff --git a/core/templates/services/question-validation.service.ts b/core/templates/services/question-validation.service.ts index 7bcdba7b1913..1276cf656adc 100644 --- a/core/templates/services/question-validation.service.ts +++ b/core/templates/services/question-validation.service.ts @@ -17,36 +17,40 @@ * */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { StateEditorService } from 'components/state-editor/state-editor-properties-services/state-editor.service'; -import { Question } from 'domain/question/QuestionObjectFactory'; -import { MisconceptionSkillMap } from 'domain/skill/MisconceptionObjectFactory'; -import { ResponsesService } from 'pages/exploration-editor-page/editor-tab/services/responses.service'; -import { InteractionSpecsConstants, InteractionSpecsKey } from 'pages/interaction-specs.constants'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {StateEditorService} from 'components/state-editor/state-editor-properties-services/state-editor.service'; +import {Question} from 'domain/question/QuestionObjectFactory'; +import {MisconceptionSkillMap} from 'domain/skill/MisconceptionObjectFactory'; +import {ResponsesService} from 'pages/exploration-editor-page/editor-tab/services/responses.service'; +import { + InteractionSpecsConstants, + InteractionSpecsKey, +} from 'pages/interaction-specs.constants'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class QuestionValidationService { constructor( private responsesService: ResponsesService, private stateEditorService: StateEditorService - ) { } + ) {} isQuestionValid( - question: Question | null | undefined, - misconceptionsBySkill: MisconceptionSkillMap): boolean { + question: Question | null | undefined, + misconceptionsBySkill: MisconceptionSkillMap + ): boolean { if (question === undefined || question === null) { return false; } return !( this.getValidationErrorMessage(question) || - question.getUnaddressedMisconceptionNames( - misconceptionsBySkill - ).length > 0 || - !this.stateEditorService.isCurrentSolutionValid()); + question.getUnaddressedMisconceptionNames(misconceptionsBySkill).length > + 0 || + !this.stateEditorService.isCurrentSolutionValid() + ); } // Returns 'null' when the message is valid. @@ -74,9 +78,8 @@ export class QuestionValidationService { } if ( !interaction.solution && - InteractionSpecsConstants.INTERACTION_SPECS[ - interactionId - ].can_have_solution + InteractionSpecsConstants.INTERACTION_SPECS[interactionId] + .can_have_solution ) { return 'A solution must be specified'; } @@ -95,5 +98,9 @@ export class QuestionValidationService { } } -angular.module('oppia').factory( - 'QuestionValidationService', downgradeInjectable(QuestionValidationService)); +angular + .module('oppia') + .factory( + 'QuestionValidationService', + downgradeInjectable(QuestionValidationService) + ); diff --git a/core/templates/services/questions-list.service.spec.ts b/core/templates/services/questions-list.service.spec.ts index a8fa5ee69b11..00cb4ed0a50e 100644 --- a/core/templates/services/questions-list.service.spec.ts +++ b/core/templates/services/questions-list.service.spec.ts @@ -16,12 +16,14 @@ * @fileoverview Unit tests for QuestionsListService. */ -import { HttpClientTestingModule, HttpTestingController } from - '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; -import { Subscription } from 'rxjs'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; +import {Subscription} from 'rxjs'; -import { QuestionsListService } from 'services/questions-list.service'; +import {QuestionsListService} from 'services/questions-list.service'; describe('Questions List Service', () => { let qls: QuestionsListService; @@ -29,22 +31,24 @@ describe('Questions List Service', () => { let quesionSummariesInitializedSpy: jasmine.Spy; let testSubscriptions: Subscription; let sampleResponse = { - question_summary_dicts: [{ - skill_descriptions: [], - summary: { - creator_id: '1', - created_on_msec: 0, - last_updated_msec: 0, - id: '0', - question_content: '' - } - }], - more: false + question_summary_dicts: [ + { + skill_descriptions: [], + summary: { + creator_id: '1', + created_on_msec: 0, + last_updated_msec: 0, + id: '0', + question_content: '', + }, + }, + ], + more: false, }; beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] + imports: [HttpClientTestingModule], }); qls = TestBed.get(QuestionsListService); httpTestingController = TestBed.get(HttpTestingController); @@ -52,11 +56,14 @@ describe('Questions List Service', () => { beforeEach(() => { quesionSummariesInitializedSpy = jasmine.createSpy( - 'questionSummariesInitialized'); + 'questionSummariesInitialized' + ); testSubscriptions = new Subscription(); - testSubscriptions.add(qls.onQuestionSummariesInitialized.subscribe( - quesionSummariesInitializedSpy - )); + testSubscriptions.add( + qls.onQuestionSummariesInitialized.subscribe( + quesionSummariesInitializedSpy + ) + ); }); afterEach(() => { @@ -76,19 +83,41 @@ describe('Questions List Service', () => { expect(qls.getCurrentPageNumber()).toBe(0); })); - it('should not get question summaries when no skill id is provided', - fakeAsync(() => { - httpTestingController.expectNone('/questions_list_handler/?offset='); - qls.getQuestionSummariesAsync('', false, false); - flushMicrotasks(); - }) - ); + it('should not get question summaries when no skill id is provided', fakeAsync(() => { + httpTestingController.expectNone('/questions_list_handler/?offset='); + qls.getQuestionSummariesAsync('', false, false); + flushMicrotasks(); + })); - it('should get question summaries twice with history reset', + it('should get question summaries twice with history reset', fakeAsync(() => { + qls.getQuestionSummariesAsync('1', true, true); + let req = httpTestingController.expectOne( + '/questions_list_handler/1?offset=0' + ); + expect(req.request.method).toEqual('GET'); + req.flush(sampleResponse); + flushMicrotasks(); + + expect(qls.getCurrentPageNumber()).toBe(0); + expect(qls.isLastQuestionBatch()).toBe(true); + + qls.getQuestionSummariesAsync('1', true, true); + req = httpTestingController.expectOne('/questions_list_handler/1?offset=0'); + expect(req.request.method).toEqual('GET'); + req.flush(sampleResponse); + flushMicrotasks(); + + expect(quesionSummariesInitializedSpy).toHaveBeenCalledTimes(2); + })); + + it( + "should not get question summaries twice when page number doesn't" + + ' increase', fakeAsync(() => { - qls.getQuestionSummariesAsync('1', true, true); + qls.getQuestionSummariesAsync('1', true, false); let req = httpTestingController.expectOne( - '/questions_list_handler/1?offset=0'); + '/questions_list_handler/1?offset=0' + ); expect(req.request.method).toEqual('GET'); req.flush(sampleResponse); flushMicrotasks(); @@ -96,9 +125,23 @@ describe('Questions List Service', () => { expect(qls.getCurrentPageNumber()).toBe(0); expect(qls.isLastQuestionBatch()).toBe(true); + // Try to get questions again before incresing pagenumber. qls.getQuestionSummariesAsync('1', true, true); req = httpTestingController.expectOne( - '/questions_list_handler/1?offset=0'); + '/questions_list_handler/1?offset=0' + ); + flushMicrotasks(); + httpTestingController.verify(); + + // Increase page number. + qls.incrementPageNumber(); + expect(qls.getCurrentPageNumber()).toBe(1); + expect(qls.isLastQuestionBatch()).toBe(false); + + qls.getQuestionSummariesAsync('1', true, false); + req = httpTestingController.expectOne( + '/questions_list_handler/1?offset=0' + ); expect(req.request.method).toEqual('GET'); req.flush(sampleResponse); flushMicrotasks(); @@ -107,44 +150,11 @@ describe('Questions List Service', () => { }) ); - it('should not get question summaries twice when page number doesn\'t' + - ' increase', fakeAsync(() => { - qls.getQuestionSummariesAsync('1', true, false); - let req = httpTestingController.expectOne( - '/questions_list_handler/1?offset=0'); - expect(req.request.method).toEqual('GET'); - req.flush(sampleResponse); - flushMicrotasks(); - - expect(qls.getCurrentPageNumber()).toBe(0); - expect(qls.isLastQuestionBatch()).toBe(true); - - // Try to get questions again before incresing pagenumber. - qls.getQuestionSummariesAsync('1', true, true); - req = httpTestingController.expectOne( - '/questions_list_handler/1?offset=0'); - flushMicrotasks(); - httpTestingController.verify(); - - // Increase page number. - qls.incrementPageNumber(); - expect(qls.getCurrentPageNumber()).toBe(1); - expect(qls.isLastQuestionBatch()).toBe(false); - - qls.getQuestionSummariesAsync('1', true, false); - req = httpTestingController.expectOne( - '/questions_list_handler/1?offset=0'); - expect(req.request.method).toEqual('GET'); - req.flush(sampleResponse); - flushMicrotasks(); - - expect(quesionSummariesInitializedSpy).toHaveBeenCalledTimes(2); - })); - it('should get cached question summaries', fakeAsync(() => { qls.getQuestionSummariesAsync('1', true, true); const req = httpTestingController.expectOne( - '/questions_list_handler/1?offset=0'); + '/questions_list_handler/1?offset=0' + ); expect(req.request.method).toEqual('GET'); req.flush(sampleResponse); flushMicrotasks(); diff --git a/core/templates/services/questions-list.service.ts b/core/templates/services/questions-list.service.ts index 846be916f7d5..64300c0fd121 100644 --- a/core/templates/services/questions-list.service.ts +++ b/core/templates/services/questions-list.service.ts @@ -17,20 +17,18 @@ * questions list in editors. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; - -import { AppConstants } from 'app.constants'; -import { EventEmitter } from '@angular/core'; -import { FormatRtePreviewPipe } from 'filters/format-rte-preview.pipe'; -import { QuestionBackendApiService } from - 'domain/question/question-backend-api.service'; -import { QuestionSummaryForOneSkill } from - 'domain/question/question-summary-for-one-skill-object.model'; -import { TruncatePipe } from 'filters/string-utility-filters/truncate.pipe'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; + +import {AppConstants} from 'app.constants'; +import {EventEmitter} from '@angular/core'; +import {FormatRtePreviewPipe} from 'filters/format-rte-preview.pipe'; +import {QuestionBackendApiService} from 'domain/question/question-backend-api.service'; +import {QuestionSummaryForOneSkill} from 'domain/question/question-summary-for-one-skill-object.model'; +import {TruncatePipe} from 'filters/string-utility-filters/truncate.pipe'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class QuestionsListService { private _questionSummariesForOneSkill: QuestionSummaryForOneSkill[] = []; @@ -38,23 +36,25 @@ export class QuestionsListService { // Whether there are more questions available to fetch. private _moreQuestionsAvailable: boolean = true; private _currentPage: number = 0; - private _questionSummartiesInitializedEventEmitter: EventEmitter = ( - new EventEmitter()); + private _questionSummartiesInitializedEventEmitter: EventEmitter = + new EventEmitter(); constructor( private formatRtePreviewPipe: FormatRtePreviewPipe, private questionBackendApiService: QuestionBackendApiService, - private truncatePipe: TruncatePipe) {} + private truncatePipe: TruncatePipe + ) {} private _setQuestionSummariesForOneSkill( - newQuestionSummaries: QuestionSummaryForOneSkill[], - resetHistory: boolean): void { + newQuestionSummaries: QuestionSummaryForOneSkill[], + resetHistory: boolean + ): void { if (resetHistory) { this._questionSummariesForOneSkill = []; } - this._questionSummariesForOneSkill = ( - this._questionSummariesForOneSkill.concat(newQuestionSummaries)); + this._questionSummariesForOneSkill = + this._questionSummariesForOneSkill.concat(newQuestionSummaries); this._questionSummartiesInitializedEventEmitter.emit(); } @@ -74,11 +74,15 @@ export class QuestionsListService { return ( this._moreQuestionsAvailable === false && (this._currentPage + 1) * AppConstants.NUM_QUESTIONS_PER_PAGE >= - this._questionSummariesForOneSkill.length); + this._questionSummariesForOneSkill.length + ); } getQuestionSummariesAsync( - skillId: string, fetchMore: boolean, resetHistory: boolean): void { + skillId: string, + fetchMore: boolean, + resetHistory: boolean + ): void { if (resetHistory) { this._questionSummariesForOneSkill = []; this._nextOffsetForQuestions = 0; @@ -93,37 +97,43 @@ export class QuestionsListService { if ( (this._currentPage + 1) * num > - this._questionSummariesForOneSkill.length && - this._moreQuestionsAvailable === true && fetchMore) { - this.questionBackendApiService.fetchQuestionSummariesAsync( - skillId, this._nextOffsetForQuestions).then(response => { - let questionSummaries = response.questionSummaries.map(summary => { - return ( - QuestionSummaryForOneSkill. - createFromBackendDict(summary)); + this._questionSummariesForOneSkill.length && + this._moreQuestionsAvailable === true && + fetchMore + ) { + this.questionBackendApiService + .fetchQuestionSummariesAsync(skillId, this._nextOffsetForQuestions) + .then(response => { + let questionSummaries = response.questionSummaries.map(summary => { + return QuestionSummaryForOneSkill.createFromBackendDict(summary); + }); + + this._changeNextQuestionsOffset(resetHistory); + this._setMoreQuestionsAvailable(response.more); + this._setQuestionSummariesForOneSkill( + questionSummaries, + resetHistory + ); }); - - this._changeNextQuestionsOffset(resetHistory); - this._setMoreQuestionsAvailable(response.more); - this._setQuestionSummariesForOneSkill( - questionSummaries, resetHistory); - }); } } getCachedQuestionSummaries(): QuestionSummaryForOneSkill[] { const num = AppConstants.NUM_QUESTIONS_PER_PAGE; - return this._questionSummariesForOneSkill.slice( - this._currentPage * num, (this._currentPage + 1) * num).map(question => { - const summary = this.formatRtePreviewPipe.transform( - question.getQuestionSummary().getQuestionContent()); + return this._questionSummariesForOneSkill + .slice(this._currentPage * num, (this._currentPage + 1) * num) + .map(question => { + const summary = this.formatRtePreviewPipe.transform( + question.getQuestionSummary().getQuestionContent() + ); - question.getQuestionSummary().setQuestionContent( - this.truncatePipe.transform(summary, 100)); + question + .getQuestionSummary() + .setQuestionContent(this.truncatePipe.transform(summary, 100)); - return question; - }); + return question; + }); } incrementPageNumber(): void { @@ -147,6 +157,6 @@ export class QuestionsListService { } } -angular.module('oppia').factory( - 'QuestionsListService', - downgradeInjectable(QuestionsListService)); +angular + .module('oppia') + .factory('QuestionsListService', downgradeInjectable(QuestionsListService)); diff --git a/core/templates/services/request-interceptor.service.spec.ts b/core/templates/services/request-interceptor.service.spec.ts index b8d1b94c7240..67e1a4707fe1 100644 --- a/core/templates/services/request-interceptor.service.spec.ts +++ b/core/templates/services/request-interceptor.service.spec.ts @@ -15,17 +15,22 @@ /** * @fileoverview Unit tests for RequestInterceptorService. */ -import { TestBed } from '@angular/core/testing'; +import {TestBed} from '@angular/core/testing'; import { HttpClientTestingModule, - HttpTestingController + HttpTestingController, } from '@angular/common/http/testing'; -// eslint-disable-next-line oppia/disallow-httpclient -import { HTTP_INTERCEPTORS, HttpClient, HttpHandler, HttpRequest, HttpParams } from '@angular/common/http'; - -import { RequestInterceptor } from 'services/request-interceptor.service'; -import { CsrfTokenService } from './csrf-token.service'; +import { + HTTP_INTERCEPTORS, + // eslint-disable-next-line oppia/disallow-httpclient + HttpClient, + HttpHandler, + HttpRequest, + HttpParams, +} from '@angular/common/http'; +import {RequestInterceptor} from 'services/request-interceptor.service'; +import {CsrfTokenService} from './csrf-token.service'; describe('Request Interceptor Service', () => { let csrfTokenService: CsrfTokenService; @@ -36,11 +41,13 @@ describe('Request Interceptor Service', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [{ - provide: HTTP_INTERCEPTORS, - useClass: RequestInterceptor, - multi: true - }] + providers: [ + { + provide: HTTP_INTERCEPTORS, + useClass: RequestInterceptor, + multi: true, + }, + ], }); requestInterceptor = TestBed.get(RequestInterceptor); @@ -62,8 +69,9 @@ describe('Request Interceptor Service', () => { ['sample-csrf-token'] ); - httpClient.post('/api', {data: 'test'}).subscribe( - async(response) => expect(response).toBeTruthy()); + httpClient + .post('/api', {data: 'test'}) + .subscribe(async response => expect(response).toBeTruthy()); let req = httpTestingController.expectOne('/api'); let reqCSFR = httpTestingController.expectOne('/csrfhandler'); @@ -83,8 +91,9 @@ describe('Request Interceptor Service', () => { ['sample-csrf-token'] ); - httpClient.patch('/api', {data: 'test'}).subscribe( - async(response) => expect(response).toBeTruthy()); + httpClient + .patch('/api', {data: 'test'}) + .subscribe(async response => expect(response).toBeTruthy()); let req = httpTestingController.expectOne('/api'); let reqCSRF = httpTestingController.expectOne('/csrfhandler'); @@ -96,7 +105,7 @@ describe('Request Interceptor Service', () => { expect(req.request.body).toEqual({ csrf_token: 'sample-csrf-token', source: document.URL, - payload: JSON.stringify({data: 'test'}) + payload: JSON.stringify({data: 'test'}), }); }); @@ -109,10 +118,12 @@ describe('Request Interceptor Service', () => { ['sample-csrf-token'] ); - httpClient.get('/api').subscribe( - async(response) => expect(response).toBeTruthy()); - httpClient.patch('/api2', {data: 'test'}).subscribe( - async(response) => expect(response).toBeTruthy()); + httpClient + .get('/api') + .subscribe(async response => expect(response).toBeTruthy()); + httpClient + .patch('/api2', {data: 'test'}) + .subscribe(async response => expect(response).toBeTruthy()); let req = httpTestingController.expectOne('/api'); let req2 = httpTestingController.expectOne('/api2'); @@ -129,7 +140,7 @@ describe('Request Interceptor Service', () => { expect(req2.request.body).toEqual({ csrf_token: 'sample-csrf-token', source: document.URL, - payload: JSON.stringify({data: 'test'}) + payload: JSON.stringify({data: 'test'}), }); }); @@ -140,16 +151,21 @@ describe('Request Interceptor Service', () => { expect(() => { requestInterceptor.intercept( - new HttpRequest('GET', 'url'), {} as HttpHandler); + new HttpRequest('GET', 'url'), + {} as HttpHandler + ); }).toThrowError('Error'); }); - it('should correctly set the csrf token', (done) => { + it('should correctly set the csrf token', done => { csrfTokenService.initializeToken(); - csrfTokenService.getTokenAsync().then(function(token) { - expect(token).toEqual('sample-csrf-token'); - }).then(done, done.fail); + csrfTokenService + .getTokenAsync() + .then(function (token) { + expect(token).toEqual('sample-csrf-token'); + }) + .then(done, done.fail); let reqCSFR = httpTestingController.expectOne('/csrfhandler'); reqCSFR.flush('12345{"token": "sample-csrf-token"}'); @@ -161,8 +177,9 @@ describe('Request Interceptor Service', () => { let reqCSFR = httpTestingController.expectOne('/csrfhandler'); reqCSFR.flush('12345{"token": "sample-csrf-token"}'); - expect(() => csrfTokenService.initializeToken()) - .toThrowError('Token request has already been made'); + expect(() => csrfTokenService.initializeToken()).toThrowError( + 'Token request has already been made' + ); }); it('should error if getTokenAsync is called before initialize', () => { @@ -176,32 +193,31 @@ describe('Request Interceptor Service', () => { it('should not throw error if params are valid', () => { expect(() => { requestInterceptor.intercept( - new HttpRequest( - 'GET', - 'url', - {params: new HttpParams({fromString: 'key=valid'})}), - {} as HttpHandler); + new HttpRequest('GET', 'url', { + params: new HttpParams({fromString: 'key=valid'}), + }), + {} as HttpHandler + ); }).toBeTruthy(); }); it('should not throw error if no params are specified', () => { expect(() => { requestInterceptor.intercept( - new HttpRequest( - 'GET', - 'url'), - {} as HttpHandler); + new HttpRequest('GET', 'url'), + {} as HttpHandler + ); }).toBeTruthy(); }); it('should throw error if param with null value is supplied', () => { expect(() => { requestInterceptor.intercept( - new HttpRequest( - 'GET', - 'url', - {params: new HttpParams({fromString: 'key=null'})}), - {} as HttpHandler); + new HttpRequest('GET', 'url', { + params: new HttpParams({fromString: 'key=null'}), + }), + {} as HttpHandler + ); }).toThrowError('Cannot supply params with value "None" or "null".'); let reqCSFR = httpTestingController.expectOne('/csrfhandler'); @@ -211,11 +227,11 @@ describe('Request Interceptor Service', () => { it('should throw error if param with None value is supplied', () => { expect(() => { requestInterceptor.intercept( - new HttpRequest( - 'GET', - 'url', - {params: new HttpParams({fromString: 'key=None'})}), - {} as HttpHandler); + new HttpRequest('GET', 'url', { + params: new HttpParams({fromString: 'key=None'}), + }), + {} as HttpHandler + ); }).toThrowError('Cannot supply params with value "None" or "null".'); let reqCSFR = httpTestingController.expectOne('/csrfhandler'); @@ -225,11 +241,11 @@ describe('Request Interceptor Service', () => { it('should throw error if null param in DELETE request', () => { expect(() => { requestInterceptor.intercept( - new HttpRequest( - 'DELETE', - 'url', - {params: new HttpParams({fromString: 'key=null'})}), - {} as HttpHandler); + new HttpRequest('DELETE', 'url', { + params: new HttpParams({fromString: 'key=null'}), + }), + {} as HttpHandler + ); }).toThrowError('Cannot supply params with value "None" or "null".'); let reqCSFR = httpTestingController.expectOne('/csrfhandler'); @@ -245,11 +261,13 @@ describe('Request Interceptor Service', () => { ['sample-csrf-token'] ); - httpClient.post( - '/api', - {data: 'test'}, - {params: new HttpParams({fromString: 'key=null'})}).subscribe( - async(response) => expect(response).toBeTruthy()); + httpClient + .post( + '/api', + {data: 'test'}, + {params: new HttpParams({fromString: 'key=null'})} + ) + .subscribe(async response => expect(response).toBeTruthy()); const req = httpTestingController.expectOne('/api?key=null'); req.flush({data: 'test'}); @@ -266,11 +284,13 @@ describe('Request Interceptor Service', () => { ['sample-csrf-token'] ); - httpClient.put( - '/api', - {data: 'test'}, - {params: new HttpParams({fromString: 'key=null'})}).subscribe( - async(response) => expect(response).toBeTruthy()); + httpClient + .put( + '/api', + {data: 'test'}, + {params: new HttpParams({fromString: 'key=null'})} + ) + .subscribe(async response => expect(response).toBeTruthy()); const req = httpTestingController.expectOne('/api?key=null'); req.flush({data: 'test'}); @@ -287,11 +307,13 @@ describe('Request Interceptor Service', () => { ['sample-csrf-token'] ); - httpClient.patch( - '/api', - {data: 'test'}, - {params: new HttpParams({fromString: 'key=null'})}).subscribe( - async(response) => expect(response).toBeTruthy()); + httpClient + .patch( + '/api', + {data: 'test'}, + {params: new HttpParams({fromString: 'key=null'})} + ) + .subscribe(async response => expect(response).toBeTruthy()); const req = httpTestingController.expectOne('/api?key=null'); req.flush({data: 'test'}); diff --git a/core/templates/services/request-interceptor.service.ts b/core/templates/services/request-interceptor.service.ts index 18e766de6d8f..241af3667d34 100644 --- a/core/templates/services/request-interceptor.service.ts +++ b/core/templates/services/request-interceptor.service.ts @@ -16,29 +16,34 @@ * @fileoverview Http Interceptor. */ -import { from, Observable } from 'rxjs'; +import {from, Observable} from 'rxjs'; // eslint-disable-next-line oppia/disallow-httpclient -import { HttpRequest, HttpInterceptor, HttpEvent, HttpHandler } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { switchMap } from 'rxjs/operators'; -import { CsrfTokenService } from './csrf-token.service'; - +import { + HttpRequest, + HttpInterceptor, + HttpEvent, + HttpHandler, +} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {switchMap} from 'rxjs/operators'; +import {CsrfTokenService} from './csrf-token.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class RequestInterceptor implements HttpInterceptor { constructor(private csrf: CsrfTokenService) {} intercept( - request: HttpRequest, next: HttpHandler + request: HttpRequest, + next: HttpHandler ): Observable> { var csrf = this.csrf; try { csrf.initializeToken(); - // We use unknown type because we are unsure of the type of error - // that was thrown. Since the catch block cannot identify the - // specific type of error, we are unable to further optimise the - // code by introducing more types of errors. + // We use unknown type because we are unsure of the type of error + // that was thrown. Since the catch block cannot identify the + // specific type of error, we are unable to further optimise the + // code by introducing more types of errors. } catch (e: unknown) { if ( e instanceof Error && @@ -51,39 +56,38 @@ export class RequestInterceptor implements HttpInterceptor { RequestInterceptor.checkForNullParams(request); if (request.body) { - return from(this.csrf.getTokenAsync()) - .pipe( - switchMap((token: string) => { - if (request.method === 'POST' || request.method === 'PUT') { - // If the body of the http request created is already in FormData - // form, no need to create the FormData object here. - if (!(request.body instanceof FormData)) { - var body = new FormData(); - body.append('payload', JSON.stringify(request.body)); - // This throws "Cannot assign to 'body' because it is a - // read-only property". We need to suppress this error because - // this is a request interceptor and we need to modify the - // contents of the request. - // @ts-ignore - request.body = body; - } - request.body.append('csrf_token', token); - request.body.append('source', document.URL); - } else { + return from(this.csrf.getTokenAsync()).pipe( + switchMap((token: string) => { + if (request.method === 'POST' || request.method === 'PUT') { + // If the body of the http request created is already in FormData + // form, no need to create the FormData object here. + if (!(request.body instanceof FormData)) { + var body = new FormData(); + body.append('payload', JSON.stringify(request.body)); // This throws "Cannot assign to 'body' because it is a // read-only property". We need to suppress this error because // this is a request interceptor and we need to modify the // contents of the request. // @ts-ignore - request.body = { - csrf_token: token, - source: document.URL, - payload: JSON.stringify(request.body) - }; + request.body = body; } - return next.handle(request); - }) - ); + request.body.append('csrf_token', token); + request.body.append('source', document.URL); + } else { + // This throws "Cannot assign to 'body' because it is a + // read-only property". We need to suppress this error because + // this is a request interceptor and we need to modify the + // contents of the request. + // @ts-ignore + request.body = { + csrf_token: token, + source: document.URL, + payload: JSON.stringify(request.body), + }; + } + return next.handle(request); + }) + ); } else { return next.handle(request); } diff --git a/core/templates/services/rte-helper-modal.controller.spec.ts b/core/templates/services/rte-helper-modal.controller.spec.ts index dae83db5641c..6c3d08ebd678 100644 --- a/core/templates/services/rte-helper-modal.controller.spec.ts +++ b/core/templates/services/rte-helper-modal.controller.spec.ts @@ -16,21 +16,32 @@ * @fileoverview Unit tests for RteHelperModalController. */ -import { TestBed, ComponentFixture, waitForAsync, fakeAsync, flush } from '@angular/core/testing'; -import { RteHelperModalComponent } from './rte-helper-modal.controller'; -import { ExternalRteSaveService } from './external-rte-save.service'; -import { AlertsService } from './alerts.service'; -import { ContextService } from './context.service'; -import { ImageLocalStorageService } from './image-local-storage.service'; -import { AssetsBackendApiService } from './assets-backend-api.service'; -import { ImageUploadHelperService } from './image-upload-helper.service'; -import { SharedFormsModule } from 'components/forms/shared-forms.module'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { DirectivesModule } from 'directives/directives.module'; -import { NgbActiveModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { EventEmitter } from '@angular/core'; -import { TranslateFakeLoader, TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; +import { + TestBed, + ComponentFixture, + waitForAsync, + fakeAsync, + flush, +} from '@angular/core/testing'; +import {RteHelperModalComponent} from './rte-helper-modal.controller'; +import {ExternalRteSaveService} from './external-rte-save.service'; +import {AlertsService} from './alerts.service'; +import {ContextService} from './context.service'; +import {ImageLocalStorageService} from './image-local-storage.service'; +import {AssetsBackendApiService} from './assets-backend-api.service'; +import {ImageUploadHelperService} from './image-upload-helper.service'; +import {SharedFormsModule} from 'components/forms/shared-forms.module'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {DirectivesModule} from 'directives/directives.module'; +import {NgbActiveModal, NgbModalModule} from '@ng-bootstrap/ng-bootstrap'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {EventEmitter} from '@angular/core'; +import { + TranslateFakeLoader, + TranslateLoader, + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; describe('RteHelperModalComponent', () => { let component: RteHelperModalComponent; @@ -51,9 +62,9 @@ describe('RteHelperModalComponent', () => { TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: TranslateFakeLoader - } - }) + useClass: TranslateFakeLoader, + }, + }), ], declarations: [RteHelperModalComponent], providers: [ @@ -64,13 +75,13 @@ describe('RteHelperModalComponent', () => { ImageUploadHelperService, { provide: NgbActiveModal, - useValue: jasmine.createSpyObj('activeModal', ['close', 'dismiss']) + useValue: jasmine.createSpyObj('activeModal', ['close', 'dismiss']), }, { provide: ExternalRteSaveService, - useValue: { onExternalRteSave: mockExternalRteSaveEventEmitter } + useValue: {onExternalRteSave: mockExternalRteSaveEventEmitter}, }, - TranslateService + TranslateService, ], }).compileComponents(); })); @@ -82,21 +93,23 @@ describe('RteHelperModalComponent', () => { activeModal = TestBed.inject(NgbActiveModal); }); - describe('when customization args has a valid youtube video', function() { - var customizationArgSpecs = [{ - name: 'heading', - default_value: 'default value' - }, { - name: 'video_id', - default_value: 'https://www.youtube.com/watch?v=Ntcw0H0hwPU' - }]; - + describe('when customization args has a valid youtube video', function () { + var customizationArgSpecs = [ + { + name: 'heading', + default_value: 'default value', + }, + { + name: 'video_id', + default_value: 'https://www.youtube.com/watch?v=Ntcw0H0hwPU', + }, + ]; beforeEach(() => { fixture = TestBed.createComponent(RteHelperModalComponent); component = fixture.componentInstance; component.attrsCustomizationArgsDict = { - heading: 'This value is not default.' + heading: 'This value is not default.', }; component.customizationArgSpecs = customizationArgSpecs; }); @@ -125,28 +138,32 @@ describe('RteHelperModalComponent', () => { expect(mockExternalRteSaveEventEmitter.emit).toHaveBeenCalled(); expect(activeModal.close).toHaveBeenCalledWith({ heading: 'This value is not default.', - video_id: 'Ntcw0H0hwPU' + video_id: 'Ntcw0H0hwPU', }); })); }); - describe('when there are validation errors in any form control', function() { - var customizationArgSpecs = [{ - name: 'alt', - default_value: 'def', - schema: { - type: 'unicode', - validators: [{ - id: 'has_length_at_least', - min_value: 5 - }] - } - }]; + describe('when there are validation errors in any form control', function () { + var customizationArgSpecs = [ + { + name: 'alt', + default_value: 'def', + schema: { + type: 'unicode', + validators: [ + { + id: 'has_length_at_least', + min_value: 5, + }, + ], + }, + }, + ]; beforeEach(() => { fixture = TestBed.createComponent(RteHelperModalComponent); component = fixture.componentInstance; component.attrsCustomizationArgsDict = { - heading: 'This value is not default.' + heading: 'This value is not default.', }; component.customizationArgSpecs = customizationArgSpecs; }); diff --git a/core/templates/services/rte-helper-modal.controller.ts b/core/templates/services/rte-helper-modal.controller.ts index 529ffa980aef..1a4623d5bcdd 100644 --- a/core/templates/services/rte-helper-modal.controller.ts +++ b/core/templates/services/rte-helper-modal.controller.ts @@ -16,20 +16,20 @@ * @fileoverview Component for RteHelperModal. */ -import { Component, Input, ViewChild } from '@angular/core'; -import { NgForm } from '@angular/forms'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { AppConstants } from 'app.constants'; +import {Component, Input, ViewChild} from '@angular/core'; +import {NgForm} from '@angular/forms'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {AppConstants} from 'app.constants'; import cloneDeep from 'lodash/cloneDeep'; -import { AlertsService } from 'services/alerts.service'; -import { AssetsBackendApiService } from 'services/assets-backend-api.service'; -import { ContextService } from 'services/context.service'; -import { ExternalRteSaveService } from 'services/external-rte-save.service'; -import { ImageLocalStorageService } from 'services/image-local-storage.service'; -import { ImageUploadHelperService } from 'services/image-upload-helper.service'; -import { ServicesConstants } from 'services/services.constants'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { FormBuilder, FormGroup } from '@angular/forms'; +import {AlertsService} from 'services/alerts.service'; +import {AssetsBackendApiService} from 'services/assets-backend-api.service'; +import {ContextService} from 'services/context.service'; +import {ExternalRteSaveService} from 'services/external-rte-save.service'; +import {ImageLocalStorageService} from 'services/image-local-storage.service'; +import {ImageUploadHelperService} from 'services/image-upload-helper.service'; +import {ServicesConstants} from 'services/services.constants'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {FormBuilder, FormGroup} from '@angular/forms'; const typedCloneDeep = (obj: T): T => cloneDeep(obj); @@ -42,10 +42,10 @@ type ComponentSpecsType = typeof ServicesConstants.RTE_COMPONENT_SPECS; type ConvertStringLiteralsToString = T extends string ? string // If T is a string, return the string type. : T extends object - ? // // If T is an object, map each key K of T to a new object with the same - // key but a value of ConvertStringLiteralsToString. - { [K in keyof T]: ConvertStringLiteralsToString } - : T; // If T is not a string or an object, return T unchanged. + ? // // If T is an object, map each key K of T to a new object with the same + // key but a value of ConvertStringLiteralsToString. + {[K in keyof T]: ConvertStringLiteralsToString} + : T; // If T is not a string or an object, return T unchanged. // CustomizationArgsSpecsType extracts the customization_arg_specs array from // each component in ComponentSpecsType. @@ -53,8 +53,7 @@ type ConvertStringLiteralsToString = T extends string // then indexes into the object using [number] to represent any index in the // array. export type CustomizationArgsSpecsType = { - [K in keyof ComponentSpecsType]: - ComponentSpecsType[K]['customization_arg_specs'][number][]; + [K in keyof ComponentSpecsType]: ComponentSpecsType[K]['customization_arg_specs'][number][]; // Finally, use [keyof ComponentSpecsType] to create a union of all the array // types. }[keyof ComponentSpecsType]; @@ -62,12 +61,11 @@ export type CustomizationArgsSpecsType = { // object with keys as 'name' and values as the 'default_value'. // It uses mapped types and Extract to achieve this. export type CustomizationArgsForRteType = { - [K in CustomizationArgsSpecsType[number]['name']]: - ConvertStringLiteralsToString< + [K in CustomizationArgsSpecsType[number]['name']]: ConvertStringLiteralsToString< // Extract is used to find the correct customization_arg_specs object that // has the 'name' property equal to K. - Extract['default_value'] - > + Extract['default_value'] + >; }; // CustomizationArgsNameAndValueArray creates an array of objects with 'name' @@ -78,23 +76,20 @@ type CustomizationArgsNameAndValueArray = { [K in keyof ComponentSpecsType]: { // Extract the 'name' property from the customization_arg_specs array. name: ComponentSpecsType[K]['customization_arg_specs'][number]['name']; - value: ( - // Check if the 'name' property is equal to 'math_content' using a - // conditional type. - ComponentSpecsType[K][ - 'customization_arg_specs'][number]['name'] extends 'math_content' ? - ConvertStringLiteralsToString< - ComponentSpecsType[K][ - 'customization_arg_specs'][number]['default_value'] - > & { - svgFile: string | null; - mathExpressionSvgIsBeingProcessed: boolean; - } : - // If the 'name' property is not equal to 'math_content', create a type with - // the 'default_value' converted to a string. - ConvertStringLiteralsToString< - ComponentSpecsType[K]['customization_arg_specs'][number]['default_value'] - >); + value: // Check if the 'name' property is equal to 'math_content' using a + // conditional type. + ComponentSpecsType[K]['customization_arg_specs'][number]['name'] extends 'math_content' + ? ConvertStringLiteralsToString< + ComponentSpecsType[K]['customization_arg_specs'][number]['default_value'] + > & { + svgFile: string | null; + mathExpressionSvgIsBeingProcessed: boolean; + } + : // If the 'name' property is not equal to 'math_content', create a type with + // the 'default_value' converted to a string. + ConvertStringLiteralsToString< + ComponentSpecsType[K]['customization_arg_specs'][number]['default_value'] + >; }[]; // Finally, use [keyof ComponentSpecsType] to create a union. }[keyof ComponentSpecsType]; @@ -142,12 +137,12 @@ export class RteHelperModalComponent { // the correct type. const mathValueDict = { name: caName, - value: this.attrsCustomizationArgsDict.hasOwnProperty(caName) ? - typedCloneDeep(this.attrsCustomizationArgsDict[caName]) : - this.customizationArgSpecs[i].default_value, + value: this.attrsCustomizationArgsDict.hasOwnProperty(caName) + ? typedCloneDeep(this.attrsCustomizationArgsDict[caName]) + : this.customizationArgSpecs[i].default_value, } as Extract< CustomizationArgsNameAndValueArray[number], - { name: typeof caName } + {name: typeof caName} >; // If the component being created or edited is math rich text component, // we need to pass this extra attribute svgFile to the math RTE editor. @@ -159,7 +154,7 @@ export class RteHelperModalComponent { ( this.tmpCustomizationArgs as Extract< CustomizationArgsNameAndValueArray[number], - { name: typeof caName } + {name: typeof caName} >[] ).push(mathValueDict); } else { @@ -169,17 +164,17 @@ export class RteHelperModalComponent { // the correct type. const tmpCustomizationArg = { name: caName, - value: this.attrsCustomizationArgsDict.hasOwnProperty(caName) ? - angular.copy(this.attrsCustomizationArgsDict[caName]) : - this.customizationArgSpecs[i].default_value, + value: this.attrsCustomizationArgsDict.hasOwnProperty(caName) + ? angular.copy(this.attrsCustomizationArgsDict[caName]) + : this.customizationArgSpecs[i].default_value, } as Extract< CustomizationArgsNameAndValueArray[number], - { name: typeof caName } + {name: typeof caName} >; ( this.tmpCustomizationArgs as Extract< CustomizationArgsNameAndValueArray[number], - { name: typeof caName } + {name: typeof caName} >[] ).push(tmpCustomizationArg); } @@ -189,8 +184,8 @@ export class RteHelperModalComponent { // TODO(#18219): Remove the typecast once Typescript is able to infer // the correct type.. const customizationArgNames = ( - this.customizationArgSpecs as { name: string }[] - ).map((x) => x.name); + this.customizationArgSpecs as {name: string}[] + ).map(x => x.name); if ( customizationArgNames.includes('url') && customizationArgNames.includes('text') @@ -232,9 +227,9 @@ export class RteHelperModalComponent { } else { // We know that this is a math rich text component. Hence we can make the // the type more specific. - const { value } = this.tmpCustomizationArgs[0] as Extract< + const {value} = this.tmpCustomizationArgs[0] as Extract< CustomizationArgsNameAndValueArray[number], - { name: 'math_content' } + {name: 'math_content'} >; return value.mathExpressionSvgIsBeingProcessed || value.raw_latex === ''; } @@ -252,7 +247,7 @@ export class RteHelperModalComponent { // the type more specific. const tmpCustomizationArgs = this.tmpCustomizationArgs as Extract< CustomizationArgsNameAndValueArray[number], - { name: 'url' | 'text' } + {name: 'url' | 'text'} >[]; let url: string = tmpCustomizationArgs[0].value; let text: string = tmpCustomizationArgs[1].value; @@ -286,14 +281,13 @@ export class RteHelperModalComponent { save(): void { for (let index in this.customizationArgsForm.value) { - this.tmpCustomizationArgs[index].value = ( - this.customizationArgsForm.value[index]); + this.tmpCustomizationArgs[index].value = + this.customizationArgsForm.value[index]; } this.externalRteSaveService.onExternalRteSave.emit(); const customizationArgsDict: { - [Prop in keyof CustomizationArgsForRteType]?: - CustomizationArgsForRteType[Prop] + [Prop in keyof CustomizationArgsForRteType]?: CustomizationArgsForRteType[Prop]; } = {}; // For the case of the math rich text components, we need to handle the // saving of the generated SVG file here because the process of saving @@ -309,7 +303,7 @@ export class RteHelperModalComponent { // the type more specific. const tmpCustomizationArgs = this.tmpCustomizationArgs as Extract< CustomizationArgsNameAndValueArray[number], - { name: 'math_content' } + {name: 'math_content'} >[]; const svgFile = tmpCustomizationArgs[0].value.svgFile; const svgFileName = tmpCustomizationArgs[0].value.svg_filename; @@ -369,7 +363,7 @@ export class RteHelperModalComponent { this.contextService.getEntityId() ) .then( - (response) => { + response => { const mathContentDict = { raw_latex: tmpCustomizationArgs[0].value.raw_latex, svg_filename: response.filename, @@ -378,7 +372,7 @@ export class RteHelperModalComponent { customizationArgsDict[caName] = mathContentDict; this.ngbActiveModal.close(customizationArgsDict); }, - (errorResponse) => { + errorResponse => { this.alertsService.addWarning( errorResponse.error || 'Error communicating with server.' ); @@ -397,8 +391,7 @@ export class RteHelperModalComponent { // Set the link `text` to the link `url` if the `text` is empty. ( customizationArgsDict as { - [Prop in CustomizationArgsNameAndValueArray[number]['name']]: - CustomizationArgsNameAndValueArray[number]['value']; + [Prop in CustomizationArgsNameAndValueArray[number]['name']]: CustomizationArgsNameAndValueArray[number]['value']; } )[caName] = this.tmpCustomizationArgs[i].value || @@ -406,8 +399,7 @@ export class RteHelperModalComponent { } else { ( customizationArgsDict as { - [Prop in CustomizationArgsNameAndValueArray[number]['name']]: - CustomizationArgsNameAndValueArray[number]['value']; + [Prop in CustomizationArgsNameAndValueArray[number]['name']]: CustomizationArgsNameAndValueArray[number]['value']; } )[caName] = this.tmpCustomizationArgs[i].value; } @@ -418,8 +410,8 @@ export class RteHelperModalComponent { extractVideoIdFromVideoUrl(url: string): string { const videoUrl = url.split(/(vi\/|v=|\/v\/|youtu\.be\/|\/embed\/)/); - return videoUrl[2] !== undefined ? - videoUrl[2].split(/[^0-9a-z_\-]/i)[0] : - videoUrl[0]; + return videoUrl[2] !== undefined + ? videoUrl[2].split(/[^0-9a-z_\-]/i)[0] + : videoUrl[0]; } } diff --git a/core/templates/services/rte-helper.service.spec.ts b/core/templates/services/rte-helper.service.spec.ts index 402e0d1e9b2f..68d86ae320bd 100644 --- a/core/templates/services/rte-helper.service.spec.ts +++ b/core/templates/services/rte-helper.service.spec.ts @@ -16,9 +16,9 @@ * @fileoverview Unit tests for RteHelperService. */ -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { RteHelperService } from './rte-helper.service'; -import { TestBed, fakeAsync, tick } from '@angular/core/testing'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {RteHelperService} from './rte-helper.service'; +import {TestBed, fakeAsync, tick} from '@angular/core/testing'; describe('Rte Helper Service', () => { let rteHelperService: RteHelperService; @@ -30,246 +30,296 @@ describe('Rte Helper Service', () => { }); it('should get rich text components', () => { - expect(rteHelperService.getRichTextComponents()).toEqual([{ - backendId: 'Collapsible', - customizationArgSpecs: [{ - name: 'heading', - description: 'The heading for the collapsible block', - schema: { - type: 'unicode' - }, - default_value_obtainable_from_highlight: false, - default_value: 'Sample Header' - }, { - name: 'content', - description: 'The content of the collapsible block', - schema: { - type: 'html', - ui_config: { - hide_complex_extensions: true - } - }, - default_value_obtainable_from_highlight: false, - default_value: 'You have opened the collapsible block.' - }], - id: 'collapsible', - iconDataUrl: '/rich_text_components/Collapsible/Collapsible.png', - isComplex: true, - isBlockElement: true, - requiresFs: false, - tooltip: 'Insert collapsible block', - requiresInternet: false - }, { - backendId: 'Image', - customizationArgSpecs: [{ - name: 'filepath', - description: 'The image (Allowed extensions: gif, jpeg, jpg, png, svg)', - schema: { - type: 'custom', - obj_type: 'Filepath' - }, - default_value_obtainable_from_highlight: false, - default_value: '' - }, { - name: 'caption', - description: 'Caption for image (optional)', - schema: { - type: 'unicode', - validators: [{ - id: 'has_length_at_most', - max_value: 500 - }] - }, - default_value_obtainable_from_highlight: false, - default_value: '' - }, { - name: 'alt', - description: 'Briefly explain this image to a visually impaired' + - ' learner', - schema: { - type: 'unicode', - validators: [{ - id: 'has_length_at_least', - min_value: 5 - }], - ui_config: { - placeholder: 'Description of Image (Example : George Handel,' + - ' 18th century baroque composer)', - rows: 3 - } - }, - default_value_obtainable_from_highlight: false, - default_value: '' - }], - id: 'image', - iconDataUrl: '/rich_text_components/Image/Image.png', - isComplex: false, - isBlockElement: true, - requiresFs: true, - tooltip: 'Insert image', - requiresInternet: true - }, { - backendId: 'Link', - customizationArgSpecs: [{ - name: 'url', - description: 'The link URL. If no protocol is specified, HTTPS will' + - ' be used.', - schema: { - type: 'custom', - obj_type: 'SanitizedUrl' - }, - default_value_obtainable_from_highlight: false, - default_value: '' - }, { - name: 'text', - description: 'The link text. If left blank, the link URL will be used.', - schema: { - type: 'unicode' - }, - default_value_obtainable_from_highlight: false, - default_value: '' - }], - id: 'link', - iconDataUrl: '/rich_text_components/Link/Link.png', - isComplex: false, - isBlockElement: false, - requiresFs: false, - tooltip: 'Insert link', - requiresInternet: false - }, { - backendId: 'Math', - customizationArgSpecs: [{ - name: 'math_content', - description: 'The Math Expression to be displayed.', - schema: { - type: 'custom', - obj_type: 'MathExpressionContent' - }, - default_value_obtainable_from_highlight: false, - default_value: { - raw_latex: '', - svg_filename: '' - } - }], - id: 'math', - iconDataUrl: '/rich_text_components/Math/Math.png', - isComplex: false, - isBlockElement: false, - requiresFs: false, - tooltip: 'Insert mathematical formula', - requiresInternet: true - }, { - backendId: 'skillreview', - customizationArgSpecs: [{ - name: 'text', - description: 'The text to be displayed', - schema: { - type: 'unicode', - validators: [{ - id: 'is_nonempty' - }] - }, - default_value_obtainable_from_highlight: true, - default_value: 'concept card' - }, { - name: 'skill_id', - description: 'The skill that this link refers to', - schema: { - type: 'custom', - obj_type: 'SkillSelector' - }, - default_value_obtainable_from_highlight: false, - default_value: '' - }], - id: 'skillreview', - iconDataUrl: '/rich_text_components/Skillreview/Skillreview.png', - isComplex: false, - isBlockElement: false, - requiresFs: false, - tooltip: 'Insert Concept Card Link', - requiresInternet: true - }, { - backendId: 'Tabs', - customizationArgSpecs: [{ - name: 'tab_contents', - description: 'The tab titles and contents.', - schema: { - type: 'custom', - obj_type: 'ListOfTabs' - }, - default_value_obtainable_from_highlight: false, - default_value: [{ - title: 'Hint introduction', - content: 'This set of tabs shows some hints. Click on the other' + - ' tabs to display the relevant hints.' - }, - { - title: 'Hint 1', - content: 'This is a first hint.' - }] - }], - id: 'tabs', - iconDataUrl: '/rich_text_components/Tabs/Tabs.png', - isComplex: true, - isBlockElement: true, - requiresFs: false, - tooltip: 'Insert tabs (e.g. for hints)', - requiresInternet: false - }, { - backendId: 'Video', - customizationArgSpecs: [{ - name: 'video_id', - description: 'The Youtube URL or the YouTube id for this video.' + - ' (The Youtube id is the 11-character string after \"v=\" in' + - ' the video URL.)', - schema: { - type: 'unicode' - }, - default_value_obtainable_from_highlight: false, - default_value: '' - }, { - name: 'start', - description: 'Video start time in seconds: (leave at 0 to start' + - ' at the beginning.)', - schema: { - type: 'int', - validators: [{ - id: 'is_at_least', - min_value: 0 - }] - }, - default_value_obtainable_from_highlight: false, - default_value: 0 - }, { - name: 'end', - description: 'Video end time in seconds: (leave at 0 to play until' + - ' the end.)', - schema: { - type: 'int', - validators: [{ - id: 'is_at_least', - min_value: 0 - }] - }, - default_value_obtainable_from_highlight: false, - default_value: 0 - }, { - name: 'autoplay', - description: 'Autoplay this video once the question has loaded?', - schema: { - type: 'bool' - }, - default_value_obtainable_from_highlight: false, - default_value: false - }], - id: 'video', - iconDataUrl: '/rich_text_components/Video/Video.png', - isComplex: false, - isBlockElement: true, - requiresFs: false, - tooltip: 'Insert video', - requiresInternet: true - }]); + expect(rteHelperService.getRichTextComponents()).toEqual([ + { + backendId: 'Collapsible', + customizationArgSpecs: [ + { + name: 'heading', + description: 'The heading for the collapsible block', + schema: { + type: 'unicode', + }, + default_value_obtainable_from_highlight: false, + default_value: 'Sample Header', + }, + { + name: 'content', + description: 'The content of the collapsible block', + schema: { + type: 'html', + ui_config: { + hide_complex_extensions: true, + }, + }, + default_value_obtainable_from_highlight: false, + default_value: 'You have opened the collapsible block.', + }, + ], + id: 'collapsible', + iconDataUrl: '/rich_text_components/Collapsible/Collapsible.png', + isComplex: true, + isBlockElement: true, + requiresFs: false, + tooltip: 'Insert collapsible block', + requiresInternet: false, + }, + { + backendId: 'Image', + customizationArgSpecs: [ + { + name: 'filepath', + description: + 'The image (Allowed extensions: gif, jpeg, jpg, png, svg)', + schema: { + type: 'custom', + obj_type: 'Filepath', + }, + default_value_obtainable_from_highlight: false, + default_value: '', + }, + { + name: 'caption', + description: 'Caption for image (optional)', + schema: { + type: 'unicode', + validators: [ + { + id: 'has_length_at_most', + max_value: 500, + }, + ], + }, + default_value_obtainable_from_highlight: false, + default_value: '', + }, + { + name: 'alt', + description: + 'Briefly explain this image to a visually impaired' + ' learner', + schema: { + type: 'unicode', + validators: [ + { + id: 'has_length_at_least', + min_value: 5, + }, + ], + ui_config: { + placeholder: + 'Description of Image (Example : George Handel,' + + ' 18th century baroque composer)', + rows: 3, + }, + }, + default_value_obtainable_from_highlight: false, + default_value: '', + }, + ], + id: 'image', + iconDataUrl: '/rich_text_components/Image/Image.png', + isComplex: false, + isBlockElement: true, + requiresFs: true, + tooltip: 'Insert image', + requiresInternet: true, + }, + { + backendId: 'Link', + customizationArgSpecs: [ + { + name: 'url', + description: + 'The link URL. If no protocol is specified, HTTPS will' + + ' be used.', + schema: { + type: 'custom', + obj_type: 'SanitizedUrl', + }, + default_value_obtainable_from_highlight: false, + default_value: '', + }, + { + name: 'text', + description: + 'The link text. If left blank, the link URL will be used.', + schema: { + type: 'unicode', + }, + default_value_obtainable_from_highlight: false, + default_value: '', + }, + ], + id: 'link', + iconDataUrl: '/rich_text_components/Link/Link.png', + isComplex: false, + isBlockElement: false, + requiresFs: false, + tooltip: 'Insert link', + requiresInternet: false, + }, + { + backendId: 'Math', + customizationArgSpecs: [ + { + name: 'math_content', + description: 'The Math Expression to be displayed.', + schema: { + type: 'custom', + obj_type: 'MathExpressionContent', + }, + default_value_obtainable_from_highlight: false, + default_value: { + raw_latex: '', + svg_filename: '', + }, + }, + ], + id: 'math', + iconDataUrl: '/rich_text_components/Math/Math.png', + isComplex: false, + isBlockElement: false, + requiresFs: false, + tooltip: 'Insert mathematical formula', + requiresInternet: true, + }, + { + backendId: 'skillreview', + customizationArgSpecs: [ + { + name: 'text', + description: 'The text to be displayed', + schema: { + type: 'unicode', + validators: [ + { + id: 'is_nonempty', + }, + ], + }, + default_value_obtainable_from_highlight: true, + default_value: 'concept card', + }, + { + name: 'skill_id', + description: 'The skill that this link refers to', + schema: { + type: 'custom', + obj_type: 'SkillSelector', + }, + default_value_obtainable_from_highlight: false, + default_value: '', + }, + ], + id: 'skillreview', + iconDataUrl: '/rich_text_components/Skillreview/Skillreview.png', + isComplex: false, + isBlockElement: false, + requiresFs: false, + tooltip: 'Insert Concept Card Link', + requiresInternet: true, + }, + { + backendId: 'Tabs', + customizationArgSpecs: [ + { + name: 'tab_contents', + description: 'The tab titles and contents.', + schema: { + type: 'custom', + obj_type: 'ListOfTabs', + }, + default_value_obtainable_from_highlight: false, + default_value: [ + { + title: 'Hint introduction', + content: + 'This set of tabs shows some hints. Click on the other' + + ' tabs to display the relevant hints.', + }, + { + title: 'Hint 1', + content: 'This is a first hint.', + }, + ], + }, + ], + id: 'tabs', + iconDataUrl: '/rich_text_components/Tabs/Tabs.png', + isComplex: true, + isBlockElement: true, + requiresFs: false, + tooltip: 'Insert tabs (e.g. for hints)', + requiresInternet: false, + }, + { + backendId: 'Video', + customizationArgSpecs: [ + { + name: 'video_id', + description: + 'The Youtube URL or the YouTube id for this video.' + + ' (The Youtube id is the 11-character string after "v=" in' + + ' the video URL.)', + schema: { + type: 'unicode', + }, + default_value_obtainable_from_highlight: false, + default_value: '', + }, + { + name: 'start', + description: + 'Video start time in seconds: (leave at 0 to start' + + ' at the beginning.)', + schema: { + type: 'int', + validators: [ + { + id: 'is_at_least', + min_value: 0, + }, + ], + }, + default_value_obtainable_from_highlight: false, + default_value: 0, + }, + { + name: 'end', + description: + 'Video end time in seconds: (leave at 0 to play until' + + ' the end.)', + schema: { + type: 'int', + validators: [ + { + id: 'is_at_least', + min_value: 0, + }, + ], + }, + default_value_obtainable_from_highlight: false, + default_value: 0, + }, + { + name: 'autoplay', + description: 'Autoplay this video once the question has loaded?', + schema: { + type: 'bool', + }, + default_value_obtainable_from_highlight: false, + default_value: false, + }, + ], + id: 'video', + iconDataUrl: '/rich_text_components/Video/Video.png', + isComplex: false, + isBlockElement: true, + requiresFs: false, + tooltip: 'Insert video', + requiresInternet: true, + }, + ]); }); it('should evalute when rich text component is inline', () => { @@ -285,41 +335,65 @@ describe('Rte Helper Service', () => { }); it('should open customization modal', () => { - var ngbModalSpy = spyOn(ngbModal, 'open').and.callFake(() => ({ - componentInstance: {}, - result: Promise.resolve() - } as unknown as NgbModalRef)); + var ngbModalSpy = spyOn(ngbModal, 'open').and.callFake( + () => + ({ + componentInstance: {}, + result: Promise.resolve(), + }) as unknown as NgbModalRef + ); var submitCallBackSpy = jasmine.createSpy('submit'); var dismissCallBackSpy = jasmine.createSpy('dismiss'); rteHelperService.openCustomizationModal( - false, [], {}, submitCallBackSpy, dismissCallBackSpy); + false, + [], + {}, + submitCallBackSpy, + dismissCallBackSpy + ); expect(ngbModalSpy).toHaveBeenCalled(); }); it('should open customization modal', fakeAsync(() => { - spyOn(ngbModal, 'open').and.callFake(() => ({ - componentInstance: {}, - result: Promise.resolve() - } as unknown as NgbModalRef)); + spyOn(ngbModal, 'open').and.callFake( + () => + ({ + componentInstance: {}, + result: Promise.resolve(), + }) as unknown as NgbModalRef + ); var submitCallBackSpy = jasmine.createSpy('submit'); var dismissCallBackSpy = jasmine.createSpy('dismiss'); rteHelperService.openCustomizationModal( - false, [], {}, submitCallBackSpy, dismissCallBackSpy); + false, + [], + {}, + submitCallBackSpy, + dismissCallBackSpy + ); tick(); expect(submitCallBackSpy).toHaveBeenCalled(); })); it('should open customization modal', fakeAsync(() => { - spyOn(ngbModal, 'open').and.callFake(() => ({ - componentInstance: {}, - result: Promise.reject() - } as unknown as NgbModalRef)); + spyOn(ngbModal, 'open').and.callFake( + () => + ({ + componentInstance: {}, + result: Promise.reject(), + }) as unknown as NgbModalRef + ); var submitCallBackSpy = jasmine.createSpy('submit'); var dismissCallBackSpy = jasmine.createSpy('dismiss'); rteHelperService.openCustomizationModal( - false, [], {}, submitCallBackSpy, dismissCallBackSpy); + false, + [], + {}, + submitCallBackSpy, + dismissCallBackSpy + ); tick(); expect(dismissCallBackSpy).toHaveBeenCalled(); diff --git a/core/templates/services/rte-helper.service.ts b/core/templates/services/rte-helper.service.ts index 5ddb9e9bfd5b..96576b36d389 100644 --- a/core/templates/services/rte-helper.service.ts +++ b/core/templates/services/rte-helper.service.ts @@ -16,41 +16,42 @@ * @fileoverview A helper service for the Rich text editor(RTE). */ -import { Injectable } from '@angular/core'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { AppConstants } from 'app.constants'; -import { ServicesConstants } from 'services/services.constants'; -import { CustomizationArgsForRteType, CustomizationArgsSpecsType, RteHelperModalComponent } from './rte-helper-modal.controller'; +import {Injectable} from '@angular/core'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {AppConstants} from 'app.constants'; +import {ServicesConstants} from 'services/services.constants'; +import { + CustomizationArgsForRteType, + CustomizationArgsSpecsType, + RteHelperModalComponent, +} from './rte-helper-modal.controller'; import cloneDeep from 'lodash/cloneDeep'; const RTE_COMPONENT_SPECS = ServicesConstants.RTE_COMPONENT_SPECS; const _RICH_TEXT_COMPONENTS = Object.values(RTE_COMPONENT_SPECS).map(spec => ({ backendId: spec.backend_id, - customizationArgSpecs: cloneDeep( - spec.customization_arg_specs), + customizationArgSpecs: cloneDeep(spec.customization_arg_specs), id: spec.frontend_id, iconDataUrl: spec.icon_data_url, isComplex: spec.is_complex, isBlockElement: spec.is_block_element, requiresFs: spec.requires_fs, tooltip: spec.tooltip, - requiresInternet: spec.requires_internet + requiresInternet: spec.requires_internet, })); @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class RteHelperService { - constructor( - private modalService: NgbModal, - ) {} + constructor(private modalService: NgbModal) {} getRichTextComponents(): typeof _RICH_TEXT_COMPONENTS { return cloneDeep(_RICH_TEXT_COMPONENTS); } isInlineComponent( - richTextComponent: typeof AppConstants.INLINE_RTE_COMPONENTS[number] + richTextComponent: (typeof AppConstants.INLINE_RTE_COMPONENTS)[number] ): boolean { return AppConstants.INLINE_RTE_COMPONENTS.indexOf(richTextComponent) !== -1; } @@ -59,28 +60,28 @@ export class RteHelperService { // after exiting the modal, and moves the cursor back to where it was // before the modal was opened. openCustomizationModal( - componentIsNewlyCreated: boolean, - customizationArgSpecs: CustomizationArgsSpecsType, - attrsCustomizationArgsDict: CustomizationArgsForRteType, - onSubmitCallback?: (arg0: unknown) => void, - onDismissCallback?: ( - reason: boolean | 'cancel') => void): void { + componentIsNewlyCreated: boolean, + customizationArgSpecs: CustomizationArgsSpecsType, + attrsCustomizationArgsDict: CustomizationArgsForRteType, + onSubmitCallback?: (arg0: unknown) => void, + onDismissCallback?: (reason: boolean | 'cancel') => void + ): void { document.execCommand('enableObjectResizing', false); const modalRef = this.modalService.open(RteHelperModalComponent, { - backdrop: 'static' + backdrop: 'static', }); - modalRef.componentInstance.componentIsNewlyCreated = ( - componentIsNewlyCreated); + modalRef.componentInstance.componentIsNewlyCreated = + componentIsNewlyCreated; modalRef.componentInstance.customizationArgSpecs = customizationArgSpecs; - modalRef.componentInstance.attrsCustomizationArgsDict = ( - attrsCustomizationArgsDict); + modalRef.componentInstance.attrsCustomizationArgsDict = + attrsCustomizationArgsDict; modalRef.result.then( - (result) => { + result => { if (onSubmitCallback) { onSubmitCallback(result); } }, - (reason) => { + reason => { if (onDismissCallback) { onDismissCallback(reason); } diff --git a/core/templates/services/schema-default-value.service.spec.ts b/core/templates/services/schema-default-value.service.spec.ts index 04a76d272f24..b312e40cb7a4 100644 --- a/core/templates/services/schema-default-value.service.spec.ts +++ b/core/templates/services/schema-default-value.service.spec.ts @@ -16,12 +16,15 @@ * @fileoverview Unit tests for SchemaDefaultValueService. */ -import { TestBed } from '@angular/core/testing'; +import {TestBed} from '@angular/core/testing'; -import { LoggerService } from 'services/contextual/logger.service'; -import { Schema, SchemaDefaultValueService } from 'services/schema-default-value.service'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { SubtitledUnicode } from 'domain/exploration/SubtitledUnicodeObjectFactory'; +import {LoggerService} from 'services/contextual/logger.service'; +import { + Schema, + SchemaDefaultValueService, +} from 'services/schema-default-value.service'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import {SubtitledUnicode} from 'domain/exploration/SubtitledUnicodeObjectFactory'; describe('Schema Default Value Service', () => { let sdvs: SchemaDefaultValueService; @@ -34,26 +37,26 @@ describe('Schema Default Value Service', () => { it('should get default value if schema has choices', () => { const schema = { - choices: ['Choice 1'] + choices: ['Choice 1'], } as Schema; expect(sdvs.getDefaultValue(schema)).toBe('Choice 1'); }); it('should get default value if schema type is bool', () => { const schema = { - type: 'bool' + type: 'bool', } as Schema; expect(sdvs.getDefaultValue(schema)).toBeFalse(); }); it('should get default value if schema type is unicode or html', () => { let schema = { - type: 'unicode' + type: 'unicode', } as Schema; expect(sdvs.getDefaultValue(schema)).toBe(''); schema = { - type: 'html' + type: 'html', } as Schema; expect(sdvs.getDefaultValue(schema)).toBe(''); }); @@ -61,17 +64,20 @@ describe('Schema Default Value Service', () => { it('should get default value if schema type is list', () => { let schema = { type: 'list', - items: [{ - type: 'bool' - }, { - type: 'int' - }] + items: [ + { + type: 'bool', + }, + { + type: 'int', + }, + ], } as Schema; expect(sdvs.getDefaultValue(schema)).toEqual([false, 0]); let schema2 = { type: 'list', - items: '' + items: '', } as Schema; expect(sdvs.getDefaultValue(schema2)).toEqual([]); }); @@ -79,38 +85,42 @@ describe('Schema Default Value Service', () => { it('should get default value if schema type is dict', () => { const schema = { type: 'dict', - properties: [{ - name: 'property_1', - schema: { - type: 'bool' - } - }, { - name: 'property_2', - schema: { - type: 'unicode' - } - }, { - name: 'property_3', - schema: { - type: 'int' - } - }] + properties: [ + { + name: 'property_1', + schema: { + type: 'bool', + }, + }, + { + name: 'property_2', + schema: { + type: 'unicode', + }, + }, + { + name: 'property_3', + schema: { + type: 'int', + }, + }, + ], } as Schema; expect(sdvs.getDefaultValue(schema)).toEqual({ property_1: false, property_2: '', - property_3: 0 + property_3: 0, }); }); it('should get default value if schema type is int or float', () => { let schema = { - type: 'int' + type: 'int', } as Schema; expect(sdvs.getDefaultValue(schema)).toBe(0); schema = { - type: 'float' + type: 'float', } as Schema; expect(sdvs.getDefaultValue(schema)).toBe(0); }); @@ -118,21 +128,19 @@ describe('Schema Default Value Service', () => { it('should get default value if schema type SubtitledHtml', () => { let schema = { type: 'custom', - obj_type: 'SubtitledHtml' + obj_type: 'SubtitledHtml', } as Schema; - expect( - sdvs.getDefaultValue(schema) - ).toEqual(new SubtitledHtml('', null)); + expect(sdvs.getDefaultValue(schema)).toEqual(new SubtitledHtml('', null)); }); it('should get default value if schema type is SubtitledUnicode', () => { let schema = { type: 'custom', - obj_type: 'SubtitledUnicode' + obj_type: 'SubtitledUnicode', } as Schema; - expect( - sdvs.getDefaultValue(schema) - ).toEqual(new SubtitledUnicode('', null)); + expect(sdvs.getDefaultValue(schema)).toEqual( + new SubtitledUnicode('', null) + ); }); it('should not get default value if schema type is invalid', () => { @@ -143,11 +151,12 @@ describe('Schema Default Value Service', () => { // value in order to test validations. // @ts-expect-error const schema = { - type: 'invalid' + type: 'invalid', } as Schema; expect(() => sdvs.getDefaultValue(schema)).toThrowError('Invalid Schema'); expect(loggerErrorSpy).toHaveBeenCalledWith( - 'Invalid schema: ' + JSON.stringify(schema)); + 'Invalid schema: ' + JSON.stringify(schema) + ); }); }); diff --git a/core/templates/services/schema-default-value.service.ts b/core/templates/services/schema-default-value.service.ts index ae4a5aad81e1..8f6a4d0b43e9 100644 --- a/core/templates/services/schema-default-value.service.ts +++ b/core/templates/services/schema-default-value.service.ts @@ -17,13 +17,15 @@ * SchemaBasedList item. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { LoggerService } from 'services/contextual/logger.service'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { SubtitledUnicodeObjectFactory, SubtitledUnicode } from 'domain/exploration/SubtitledUnicodeObjectFactory'; -import { SchemaConstants } from 'components/forms/schema-based-editors/schema.constants'; - +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {LoggerService} from 'services/contextual/logger.service'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import { + SubtitledUnicodeObjectFactory, + SubtitledUnicode, +} from 'domain/exploration/SubtitledUnicodeObjectFactory'; +import {SchemaConstants} from 'components/forms/schema-based-editors/schema.constants'; interface BoolSchema { type: 'bool'; @@ -67,20 +69,19 @@ export interface DictSchema { } export interface CustomSchema { - 'type': 'custom'; - 'obj_type': string; + type: 'custom'; + obj_type: string; } -export type Schema = ( - BoolSchema | - UnicodeSchema | - HtmlSchema | - IntSchema | - FloatSchema | - ListSchema | - DictSchema | - CustomSchema -); +export type Schema = + | BoolSchema + | UnicodeSchema + | HtmlSchema + | IntSchema + | FloatSchema + | ListSchema + | DictSchema + | CustomSchema; interface DictSchemaDefaultValue { [property: string]: SchemaDefaultValue; @@ -89,68 +90,74 @@ interface DictSchemaDefaultValue { // SchemaDefaultValue is a value that is used to represent the default value // of a property in a schema. It may be null as well when input is empty or not // provided. -export type SchemaDefaultValue = ( - null | - string | - number | - boolean | - SubtitledUnicode | - SubtitledHtml | - SchemaDefaultValue[] | - DictSchemaDefaultValue); +export type SchemaDefaultValue = + | null + | string + | number + | boolean + | SubtitledUnicode + | SubtitledHtml + | SchemaDefaultValue[] + | DictSchemaDefaultValue; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class SchemaDefaultValueService { constructor( - private logger: LoggerService, - private subtitledUnicodeObjectFactory: SubtitledUnicodeObjectFactory, + private logger: LoggerService, + private subtitledUnicodeObjectFactory: SubtitledUnicodeObjectFactory ) {} // TODO(sll): Rewrite this to take validators into account, so that // we always start with a valid value. getDefaultValue(schema: Schema): SchemaDefaultValue { - const schemaIsSubtitledHtml = ( + const schemaIsSubtitledHtml = schema.type === SchemaConstants.SCHEMA_TYPE_CUSTOM && - schema.obj_type === SchemaConstants.SCHEMA_OBJ_TYPE_SUBTITLED_HTML); - const schemaIsSubtitledUnicode = ( + schema.obj_type === SchemaConstants.SCHEMA_OBJ_TYPE_SUBTITLED_HTML; + const schemaIsSubtitledUnicode = schema.type === SchemaConstants.SCHEMA_TYPE_CUSTOM && - schema.obj_type === SchemaConstants.SCHEMA_OBJ_TYPE_SUBTITLED_UNICODE - ); + schema.obj_type === SchemaConstants.SCHEMA_OBJ_TYPE_SUBTITLED_UNICODE; if ('choices' in schema && schema.choices !== undefined) { return schema.choices[0]; } else if (schemaIsSubtitledHtml) { return SubtitledHtml.createFromBackendDict({ - html: '', content_id: null + html: '', + content_id: null, }); } else if (schemaIsSubtitledUnicode) { return this.subtitledUnicodeObjectFactory.createFromBackendDict({ - unicode_str: '', content_id: null + unicode_str: '', + content_id: null, }); } else if (schema.type === SchemaConstants.SCHEMA_TYPE_BOOL) { return false; - } else if (schema.type === SchemaConstants.SCHEMA_TYPE_UNICODE || - schema.type === SchemaConstants.SCHEMA_TYPE_HTML) { + } else if ( + schema.type === SchemaConstants.SCHEMA_TYPE_UNICODE || + schema.type === SchemaConstants.SCHEMA_TYPE_HTML + ) { return ''; } else if (schema.type === SchemaConstants.SCHEMA_KEY_LIST) { var that = this; if (!Array.isArray(schema.items)) { return []; } - return schema.items.map((item) => { + return schema.items.map(item => { return that.getDefaultValue(item); }); } else if (schema.type === SchemaConstants.SCHEMA_TYPE_DICT) { var result: SchemaDefaultValue = {}; for (var i = 0; i < schema.properties.length; i++) { result[schema.properties[i].name] = this.getDefaultValue( - schema.properties[i].schema); + schema.properties[i].schema + ); } return result; - } else if (schema.type === SchemaConstants.SCHEMA_TYPE_INT || - schema.type === SchemaConstants.SCHEMA_TYPE_FLOAT) { + } else if ( + schema.type === SchemaConstants.SCHEMA_TYPE_INT || + schema.type === SchemaConstants.SCHEMA_TYPE_FLOAT + ) { return 0; } else { this.logger.error('Invalid schema: ' + JSON.stringify(schema)); @@ -159,5 +166,9 @@ export class SchemaDefaultValueService { } } -angular.module('oppia').factory( - 'SchemaDefaultValueService', downgradeInjectable(SchemaDefaultValueService)); +angular + .module('oppia') + .factory( + 'SchemaDefaultValueService', + downgradeInjectable(SchemaDefaultValueService) + ); diff --git a/core/templates/services/schema-form-submitted.service.spec.ts b/core/templates/services/schema-form-submitted.service.spec.ts index a504d24ce3ef..107d7b3151ed 100644 --- a/core/templates/services/schema-form-submitted.service.spec.ts +++ b/core/templates/services/schema-form-submitted.service.spec.ts @@ -14,13 +14,12 @@ /** * @fileoverview Unit tests for SchemaFormSubmittedService -*/ + */ -import { EventEmitter } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; +import {EventEmitter} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; -import { SchemaFormSubmittedService } from - 'services/schema-form-submitted.service'; +import {SchemaFormSubmittedService} from 'services/schema-form-submitted.service'; describe('Schema Submitted Service', () => { let schemaFormSubmittedService: SchemaFormSubmittedService; @@ -32,6 +31,7 @@ describe('Schema Submitted Service', () => { it('should fetch submittedSchemaBasedForm event emitter', () => { let sampleSubmittedSchemaBasedFormEventEmitter = new EventEmitter(); expect(schemaFormSubmittedService.onSubmittedSchemaBasedForm).toEqual( - sampleSubmittedSchemaBasedFormEventEmitter); + sampleSubmittedSchemaBasedFormEventEmitter + ); }); }); diff --git a/core/templates/services/schema-form-submitted.service.ts b/core/templates/services/schema-form-submitted.service.ts index cefd13d01315..32955276645c 100644 --- a/core/templates/services/schema-form-submitted.service.ts +++ b/core/templates/services/schema-form-submitted.service.ts @@ -17,11 +17,11 @@ * submission of different schema forms */ -import { Injectable, EventEmitter } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {Injectable, EventEmitter} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class SchemaFormSubmittedService { private _submittedSchemaBasedFormEventEmitter = new EventEmitter(); @@ -31,5 +31,9 @@ export class SchemaFormSubmittedService { } } -angular.module('oppia').factory('SchemaFormSubmittedService', - downgradeInjectable(SchemaFormSubmittedService)); +angular + .module('oppia') + .factory( + 'SchemaFormSubmittedService', + downgradeInjectable(SchemaFormSubmittedService) + ); diff --git a/core/templates/services/schema-undefined-last-element.service.spec.ts b/core/templates/services/schema-undefined-last-element.service.spec.ts index 77339ff594a9..b3c468ff5f0b 100644 --- a/core/templates/services/schema-undefined-last-element.service.spec.ts +++ b/core/templates/services/schema-undefined-last-element.service.spec.ts @@ -14,11 +14,10 @@ /** * @fileoverview Unit tests for SchemaUndefinedLastElementService. -*/ + */ -import { TestBed } from '@angular/core/testing'; -import { SchemaUndefinedLastElementService } from - 'services/schema-undefined-last-element.service'; +import {TestBed} from '@angular/core/testing'; +import {SchemaUndefinedLastElementService} from 'services/schema-undefined-last-element.service'; describe('Schema Undefined Last Element Service', () => { let sules: SchemaUndefinedLastElementService; diff --git a/core/templates/services/schema-undefined-last-element.service.ts b/core/templates/services/schema-undefined-last-element.service.ts index 92745ec388e4..42e3d54e8b20 100644 --- a/core/templates/services/schema-undefined-last-element.service.ts +++ b/core/templates/services/schema-undefined-last-element.service.ts @@ -17,12 +17,12 @@ * is undefined. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Schema } from 'services/schema-default-value.service'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Schema} from 'services/schema-default-value.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class SchemaUndefinedLastElementService { // Returns true if the input value, taken as the last element in a list, @@ -36,6 +36,9 @@ export class SchemaUndefinedLastElementService { } } -angular.module('oppia').factory( - 'SchemaUndefinedLastElementService', - downgradeInjectable(SchemaUndefinedLastElementService)); +angular + .module('oppia') + .factory( + 'SchemaUndefinedLastElementService', + downgradeInjectable(SchemaUndefinedLastElementService) + ); diff --git a/core/templates/services/search-backend-api.service.spec.ts b/core/templates/services/search-backend-api.service.spec.ts index b1ced636e121..d9a7fa935fc7 100644 --- a/core/templates/services/search-backend-api.service.spec.ts +++ b/core/templates/services/search-backend-api.service.spec.ts @@ -16,11 +16,16 @@ * @fileoverview Tests that search service is working as expected. */ -import { HttpClientTestingModule, HttpTestingController } from - '@angular/common/http/testing'; -import { fakeAsync, flushMicrotasks, TestBed } from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {fakeAsync, flushMicrotasks, TestBed} from '@angular/core/testing'; -import { SearchBackendApiService, SearchResponseBackendDict } from 'services/search-backend-api.service'; +import { + SearchBackendApiService, + SearchResponseBackendDict, +} from 'services/search-backend-api.service'; describe('Search Backend Api Service', () => { let searchBackendApiService: SearchBackendApiService; @@ -38,12 +43,13 @@ describe('Search Backend Api Service', () => { describe('fetchExplorationSearchResultAsync', () => { const sampleSearchResponse = { search_cursor: 'notempty', - activity_list: [] + activity_list: [], }; it('should return exploration search results', fakeAsync(() => { - searchBackendApiService.fetchExplorationSearchResultAsync('').then( - (response: SearchResponseBackendDict) => { + searchBackendApiService + .fetchExplorationSearchResultAsync('') + .then((response: SearchResponseBackendDict) => { expect(response.activity_list).toEqual([]); expect(response.search_cursor).toBe('notempty'); }); diff --git a/core/templates/services/search-backend-api.service.ts b/core/templates/services/search-backend-api.service.ts index e3799e4cbd14..165a74fffefd 100644 --- a/core/templates/services/search-backend-api.service.ts +++ b/core/templates/services/search-backend-api.service.ts @@ -16,31 +16,37 @@ * @fileoverview Service for executing searches. */ -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { ExplorationSummaryDict } from 'domain/summary/exploration-summary-backend-api.service'; -import { ServicesConstants } from './services.constants'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {ExplorationSummaryDict} from 'domain/summary/exploration-summary-backend-api.service'; +import {ServicesConstants} from './services.constants'; export class SearchResponseBackendDict { 'search_cursor': number | null; 'activity_list': ExplorationSummaryDict[]; } - @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class SearchBackendApiService { constructor(private http: HttpClient) {} async fetchExplorationSearchResultAsync( - searchQuery: string): Promise { - return this.http.get( - ServicesConstants.SEARCH_DATA_URL + searchQuery).toPromise(); + searchQuery: string + ): Promise { + return this.http + .get( + ServicesConstants.SEARCH_DATA_URL + searchQuery + ) + .toPromise(); } } -angular.module('oppia').factory( - 'SearchBackendApiService', - downgradeInjectable(SearchBackendApiService)); +angular + .module('oppia') + .factory( + 'SearchBackendApiService', + downgradeInjectable(SearchBackendApiService) + ); diff --git a/core/templates/services/search.service.spec.ts b/core/templates/services/search.service.spec.ts index 1dc0ef4a126b..c1217c060a15 100644 --- a/core/templates/services/search.service.spec.ts +++ b/core/templates/services/search.service.spec.ts @@ -16,13 +16,19 @@ * @fileoverview Tests that search service gets correct collections. */ -import { EventEmitter } from '@angular/core'; -import { HttpClientTestingModule, HttpTestingController } from - '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; - -import { SearchService, SelectionDetails, SelectionList } from 'services/search.service'; -import { Subscription } from 'rxjs'; +import {EventEmitter} from '@angular/core'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; + +import { + SearchService, + SelectionDetails, + SelectionList, +} from 'services/search.service'; +import {Subscription} from 'rxjs'; describe('Search Service', () => { let searchService: SearchService; @@ -32,7 +38,7 @@ describe('Search Service', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [SearchService] + providers: [SearchService], }); searchService = TestBed.get(SearchService); }); @@ -47,7 +53,7 @@ describe('Search Service', () => { masterList: [], numSelections: 0, selections: {}, - summary: '' + summary: '', }, languageCodes: { description: '', @@ -55,141 +61,137 @@ describe('Search Service', () => { masterList: [], numSelections: 0, selections: {}, - summary: '' - } + summary: '', + }, }; }); // eslint-disable-next-line max-len it('should identify two categories and two languages given in url search query', () => { - urlComponent = '?q=test&category=("Architecture"%20OR%20' + + urlComponent = + '?q=test&category=("Architecture"%20OR%20' + '"Mathematics")&language_code=("en"%20OR%20"ar")'; - expect(searchService.updateSearchFieldsBasedOnUrlQuery( - urlComponent, results)).toBe('test'); + expect( + searchService.updateSearchFieldsBasedOnUrlQuery(urlComponent, results) + ).toBe('test'); expect(results.languageCodes.selections).toEqual({ ar: true, - en: true + en: true, }); expect(results.categories.selections).toEqual({ Architecture: true, - Mathematics: true + Mathematics: true, }); }); - it('should find one category and two languages if given in url search', - () => { - urlComponent = '?q=test&category=("Mathematics")&' + - 'language_code=("en"%20OR%20"ar")'; - expect(searchService.updateSearchFieldsBasedOnUrlQuery( - urlComponent, results)).toBe('test'); - expect(results.languageCodes.selections).toEqual({ - ar: true, - en: true - }); - expect(results.categories.selections).toEqual({ - Mathematics: true - }); - } - ); - it('should find one category and one language if given in url search', - () => { - urlComponent = - '?q=test&category=("Mathematics")&language_code=("en")'; - expect(searchService.updateSearchFieldsBasedOnUrlQuery( - urlComponent, results)).toBe('test'); - expect(results.languageCodes.selections).toEqual({ - en: true - }); - expect(results.categories.selections).toEqual({ - Mathematics: true - }); - } - ); - it('should find no categories and one language if given in url search', - () => { - urlComponent = '?q=test&language_code=("en")'; - expect(searchService.updateSearchFieldsBasedOnUrlQuery( - urlComponent, results)).toBe('test'); - expect(results.languageCodes.selections).toEqual({ - en: true - }); - expect(results.categories.selections).toEqual({}); - } - ); + it('should find one category and two languages if given in url search', () => { + urlComponent = + '?q=test&category=("Mathematics")&' + + 'language_code=("en"%20OR%20"ar")'; + expect( + searchService.updateSearchFieldsBasedOnUrlQuery(urlComponent, results) + ).toBe('test'); + expect(results.languageCodes.selections).toEqual({ + ar: true, + en: true, + }); + expect(results.categories.selections).toEqual({ + Mathematics: true, + }); + }); + it('should find one category and one language if given in url search', () => { + urlComponent = '?q=test&category=("Mathematics")&language_code=("en")'; + expect( + searchService.updateSearchFieldsBasedOnUrlQuery(urlComponent, results) + ).toBe('test'); + expect(results.languageCodes.selections).toEqual({ + en: true, + }); + expect(results.categories.selections).toEqual({ + Mathematics: true, + }); + }); + it('should find no categories and one language if given in url search', () => { + urlComponent = '?q=test&language_code=("en")'; + expect( + searchService.updateSearchFieldsBasedOnUrlQuery(urlComponent, results) + ).toBe('test'); + expect(results.languageCodes.selections).toEqual({ + en: true, + }); + expect(results.categories.selections).toEqual({}); + }); - it('should find as many keywords as provided in search query', - () => { - urlComponent = '?q=protractor%20test&language_code=("en")'; - expect(searchService.updateSearchFieldsBasedOnUrlQuery( - urlComponent, results)).toBe('protractor test'); - expect(results.languageCodes.selections).toEqual({ - en: true - }); - expect(results.categories.selections).toEqual({}); - } - ); + it('should find as many keywords as provided in search query', () => { + urlComponent = '?q=protractor%20test&language_code=("en")'; + expect( + searchService.updateSearchFieldsBasedOnUrlQuery(urlComponent, results) + ).toBe('protractor test'); + expect(results.languageCodes.selections).toEqual({ + en: true, + }); + expect(results.categories.selections).toEqual({}); + }); - it('should not find languages nor categories when ampersand is escaped', - () => { - urlComponent = '?q=protractor%20test%26category=("Mathematics")' + - '%26language_code=("en"%20OR%20"ar")'; - expect(searchService.updateSearchFieldsBasedOnUrlQuery( - urlComponent, results)).toBe( - 'protractor test&category=("Mathematics")' + - '&language_code=("en" OR "ar")'); - expect(results.languageCodes.selections).toEqual({}); - expect(results.categories.selections).toEqual({}); - } - ); + it('should not find languages nor categories when ampersand is escaped', () => { + urlComponent = + '?q=protractor%20test%26category=("Mathematics")' + + '%26language_code=("en"%20OR%20"ar")'; + expect( + searchService.updateSearchFieldsBasedOnUrlQuery(urlComponent, results) + ).toBe( + 'protractor test&category=("Mathematics")' + + '&language_code=("en" OR "ar")' + ); + expect(results.languageCodes.selections).toEqual({}); + expect(results.categories.selections).toEqual({}); + }); - it('should only use correct fields when ampersand is not escaped anywhere', - () => { - urlComponent = '?q=protractor&test&category=("Mathematics")' + - '&language_code=("en"%20OR%20"ar")'; - expect( - searchService.updateSearchFieldsBasedOnUrlQuery(urlComponent, results) - ).toBe('protractor'); - expect(results.languageCodes.selections).toEqual({ - en: true, - ar: true - }); - expect(results.categories.selections).toEqual({ Mathematics: true }); - } - ); + it('should only use correct fields when ampersand is not escaped anywhere', () => { + urlComponent = + '?q=protractor&test&category=("Mathematics")' + + '&language_code=("en"%20OR%20"ar")'; + expect( + searchService.updateSearchFieldsBasedOnUrlQuery(urlComponent, results) + ).toBe('protractor'); + expect(results.languageCodes.selections).toEqual({ + en: true, + ar: true, + }); + expect(results.categories.selections).toEqual({Mathematics: true}); + }); - it('should omit url component if it is malformed', - () => { - // In the two cases below, language_code param is not wrapped in - // parentheses. However, the category param is defined correctly. - // updateSearchFieldsBasedOnUrlQuery is expected to clean language_code. - urlComponent = ( - '?q=protractor%20test&category=("Mathematics")&' + - 'language_code="en" OR "ar")'); - searchService.updateSearchFieldsBasedOnUrlQuery(urlComponent, results); - expect(results.languageCodes.selections).toEqual({}); - expect(results.categories.selections).toEqual({ - Mathematics: true - }); - - urlComponent = ( - '?q=protractor%20test&category=("Mathematics")&' + - 'language_code="en" OR "ar"'); - searchService.updateSearchFieldsBasedOnUrlQuery(urlComponent, results); - expect(results.languageCodes.selections).toEqual({}); - expect(results.categories.selections).toEqual({ - Mathematics: true - }); - - // In this case, neither of the params are wrapped in parentheses. - // updateSearchFieldsBasedOnUrlQuery is expected to clean category and - // language_code. - urlComponent = ( - '?q=protractor%20test&category="Mathematics"&' + - 'language_code="en" OR "ar"'); - searchService.updateSearchFieldsBasedOnUrlQuery(urlComponent, results); - expect(results.languageCodes.selections).toEqual({}); - expect(results.categories.selections).toEqual({}); - } - ); + it('should omit url component if it is malformed', () => { + // In the two cases below, language_code param is not wrapped in + // parentheses. However, the category param is defined correctly. + // updateSearchFieldsBasedOnUrlQuery is expected to clean language_code. + urlComponent = + '?q=protractor%20test&category=("Mathematics")&' + + 'language_code="en" OR "ar")'; + searchService.updateSearchFieldsBasedOnUrlQuery(urlComponent, results); + expect(results.languageCodes.selections).toEqual({}); + expect(results.categories.selections).toEqual({ + Mathematics: true, + }); + + urlComponent = + '?q=protractor%20test&category=("Mathematics")&' + + 'language_code="en" OR "ar"'; + searchService.updateSearchFieldsBasedOnUrlQuery(urlComponent, results); + expect(results.languageCodes.selections).toEqual({}); + expect(results.categories.selections).toEqual({ + Mathematics: true, + }); + + // In this case, neither of the params are wrapped in parentheses. + // updateSearchFieldsBasedOnUrlQuery is expected to clean category and + // language_code. + urlComponent = + '?q=protractor%20test&category="Mathematics"&' + + 'language_code="en" OR "ar"'; + searchService.updateSearchFieldsBasedOnUrlQuery(urlComponent, results); + expect(results.languageCodes.selections).toEqual({}); + expect(results.categories.selections).toEqual({}); + }); }); describe('getSearchUrlQueryString', () => { @@ -197,29 +199,42 @@ describe('Search Service', () => { const searchQuery = '["1", "2"]'; const categories: SelectionList = { exploration: true, - feedback: true + feedback: true, }; const languageCodes: SelectionList = { en: true, - hi: true + hi: true, }; - expect(searchService.getSearchUrlQueryString( - searchQuery, categories, languageCodes)).toBe( + expect( + searchService.getSearchUrlQueryString( + searchQuery, + categories, + languageCodes + ) + ).toBe( '%5B%221%22%2C%20%222%22%5D&category=("exploration" OR "feedback")' + - '&language_code=("en" OR "hi")'); + '&language_code=("en" OR "hi")' + ); }); - it('should successfully get search url query string when there is no' + - ' category or language_code query params', () => { - const searchQuery = '["1", "2"]'; - const categories = {}; - const languageCodes = {}; + it( + 'should successfully get search url query string when there is no' + + ' category or language_code query params', + () => { + const searchQuery = '["1", "2"]'; + const categories = {}; + const languageCodes = {}; - expect(searchService.getSearchUrlQueryString( - searchQuery, categories, languageCodes)).toBe( - '%5B%221%22%2C%20%222%22%5D'); - }); + expect( + searchService.getSearchUrlQueryString( + searchQuery, + categories, + languageCodes + ) + ).toBe('%5B%221%22%2C%20%222%22%5D'); + } + ); }); describe('executeSearchQuery', () => { @@ -232,39 +247,45 @@ describe('Search Service', () => { let testSubscriptions: Subscription; const SAMPLE_RESULTS = { search_cursor: 'notempty', - activity_list: [] + activity_list: [], }; - const SAMPLE_QUERY = '/searchhandler/data?q=example&category=' + - '("exploration")&language_code=("en" OR "hi")'; + const SAMPLE_QUERY = + '/searchhandler/data?q=example&category=' + + '("exploration")&language_code=("en" OR "hi")'; beforeEach(() => { successHandler = jasmine.createSpy('success'); errorHandler = jasmine.createSpy('error'); searchQuery = 'example'; categories = { - exploration: true + exploration: true, }; languageCodes = { en: true, - hi: true + hi: true, }; initialSearchResultsLoadedSpy = jasmine.createSpy( - 'initialSearchResultsLoadedSpy'); + 'initialSearchResultsLoadedSpy' + ); testSubscriptions = new Subscription(); testSubscriptions.add( searchService.onInitialSearchResultsLoaded.subscribe( initialSearchResultsLoadedSpy - )); + ) + ); httpTestingController = TestBed.get(HttpTestingController); }); - afterEach(() => { httpTestingController.verify(); }); it('should successfully execute search query', fakeAsync(() => { searchService.executeSearchQuery( - searchQuery, categories, languageCodes, successHandler); + searchQuery, + categories, + languageCodes, + successHandler + ); expect(searchService.isSearchInProgress()).toBe(true); const req = httpTestingController.expectOne(SAMPLE_QUERY); expect(req.request.method).toEqual('GET'); @@ -276,30 +297,40 @@ describe('Search Service', () => { expect(successHandler).toHaveBeenCalled(); })); - it('should use reject handler when fetching query url fails', - fakeAsync(() => { - searchService.executeSearchQuery( - searchQuery, categories, languageCodes, successHandler, errorHandler); - expect(searchService.isSearchInProgress()).toBe(true); - const req = httpTestingController.expectOne(SAMPLE_QUERY); - expect(req.request.method).toEqual('GET'); - req.error(new ErrorEvent('network error')); - flushMicrotasks(); + it('should use reject handler when fetching query url fails', fakeAsync(() => { + searchService.executeSearchQuery( + searchQuery, + categories, + languageCodes, + successHandler, + errorHandler + ); + expect(searchService.isSearchInProgress()).toBe(true); + const req = httpTestingController.expectOne(SAMPLE_QUERY); + expect(req.request.method).toEqual('GET'); + req.error(new ErrorEvent('network error')); + flushMicrotasks(); - expect(searchService.isSearchInProgress()).toBe(false); - expect(errorHandler).toHaveBeenCalled(); - })); + expect(searchService.isSearchInProgress()).toBe(false); + expect(errorHandler).toHaveBeenCalled(); + })); describe('loadMoreData', () => { - const MORE_DATA_REQUEST = '/searchhandler/data?q=example&category=' + - '("exploration")&language_code=("en" OR "hi")&offset=notempty'; + const MORE_DATA_REQUEST = + '/searchhandler/data?q=example&category=' + + '("exploration")&language_code=("en" OR "hi")&offset=notempty'; const MORE_DATA_RESPONSE = { - search_cursor: 'newcursor' + search_cursor: 'newcursor', }; it('should successfully load more data', fakeAsync(() => { searchService.executeSearchQuery( - searchQuery, categories, languageCodes, successHandler, errorHandler); + searchQuery, + categories, + languageCodes, + successHandler, + errorHandler + ); const req = httpTestingController.expectOne(SAMPLE_QUERY); expect(req.request.method).toEqual('GET'); req.flush(SAMPLE_RESULTS); @@ -314,42 +345,54 @@ describe('Search Service', () => { expect(errorHandler).not.toHaveBeenCalled(); })); - it('should not load more data when a new query is still being sent', - fakeAsync(() => { - searchService.executeSearchQuery( - searchQuery, categories, languageCodes, successHandler); - const req = httpTestingController.expectOne(SAMPLE_QUERY); - expect(req.request.method).toEqual('GET'); - req.flush(SAMPLE_RESULTS); - flushMicrotasks(); - - searchService.loadMoreData(() => { }, () => { }); - searchService.loadMoreData(successHandler, errorHandler); - const moreDataReq = httpTestingController.expectOne( - MORE_DATA_REQUEST); - moreDataReq.flush(MORE_DATA_RESPONSE); - flushMicrotasks(); - expect(errorHandler).toHaveBeenCalledWith(false); - })); - - it('should not load more data when the end of page has been reached', - fakeAsync(() => { - searchService.executeSearchQuery( - searchQuery, categories, languageCodes, successHandler); - const req = httpTestingController.expectOne(SAMPLE_QUERY); - expect(req.request.method).toEqual('GET'); - req.flush(SAMPLE_RESULTS); - flushMicrotasks(); - - searchService.loadMoreData(() => { }, () => { }); - const moreDataReq = httpTestingController.expectOne( - SAMPLE_QUERY + '&offset=notempty'); - moreDataReq.flush({search_cursor: null}); - flushMicrotasks(); - searchService.loadMoreData(successHandler, errorHandler); - - expect(errorHandler).toHaveBeenCalledWith(true); - })); + it('should not load more data when a new query is still being sent', fakeAsync(() => { + searchService.executeSearchQuery( + searchQuery, + categories, + languageCodes, + successHandler + ); + const req = httpTestingController.expectOne(SAMPLE_QUERY); + expect(req.request.method).toEqual('GET'); + req.flush(SAMPLE_RESULTS); + flushMicrotasks(); + + searchService.loadMoreData( + () => {}, + () => {} + ); + searchService.loadMoreData(successHandler, errorHandler); + const moreDataReq = httpTestingController.expectOne(MORE_DATA_REQUEST); + moreDataReq.flush(MORE_DATA_RESPONSE); + flushMicrotasks(); + expect(errorHandler).toHaveBeenCalledWith(false); + })); + + it('should not load more data when the end of page has been reached', fakeAsync(() => { + searchService.executeSearchQuery( + searchQuery, + categories, + languageCodes, + successHandler + ); + const req = httpTestingController.expectOne(SAMPLE_QUERY); + expect(req.request.method).toEqual('GET'); + req.flush(SAMPLE_RESULTS); + flushMicrotasks(); + + searchService.loadMoreData( + () => {}, + () => {} + ); + const moreDataReq = httpTestingController.expectOne( + SAMPLE_QUERY + '&offset=notempty' + ); + moreDataReq.flush({search_cursor: null}); + flushMicrotasks(); + searchService.loadMoreData(successHandler, errorHandler); + + expect(errorHandler).toHaveBeenCalledWith(true); + })); }); }); diff --git a/core/templates/services/search.service.ts b/core/templates/services/search.service.ts index 6a65a46b8b97..9ef76ccfe469 100644 --- a/core/templates/services/search.service.ts +++ b/core/templates/services/search.service.ts @@ -16,11 +16,14 @@ * @fileoverview Search service for activityTilesInfinityGrid. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable, EventEmitter } from '@angular/core'; - -import { SearchBackendApiService, SearchResponseBackendDict } from './search-backend-api.service'; -import { ExplorationSummaryDict } from 'domain/summary/exploration-summary-backend-api.service'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable, EventEmitter} from '@angular/core'; + +import { + SearchBackendApiService, + SearchResponseBackendDict, +} from './search-backend-api.service'; +import {ExplorationSummaryDict} from 'domain/summary/exploration-summary-backend-api.service'; import cloneDeep from 'lodash/cloneDeep'; export interface SelectionList { @@ -46,7 +49,7 @@ export interface SelectionDetails { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class SearchService { // These properties are initialized using functions @@ -58,18 +61,18 @@ export class SearchService { private _lastSelectedLanguageCodes: SelectionList = {}; private _isCurrentlyFetchingResults = false; private _searchBarLoadedEventEmitter = new EventEmitter(); - private _initialSearchResultsLoadedEventEmitter = - new EventEmitter(); + private _initialSearchResultsLoadedEventEmitter = new EventEmitter< + ExplorationSummaryDict[] + >(); public numSearchesInProgress = 0; - constructor( - private _searchBackendApiService: SearchBackendApiService) { - } + constructor(private _searchBackendApiService: SearchBackendApiService) {} private _getSuffixForQuery( - selectedCategories: SelectionList, - selectedLanguageCodes: SelectionList): string { + selectedCategories: SelectionList, + selectedLanguageCodes: SelectionList + ): string { let querySuffix = ''; let _categories = ''; @@ -106,8 +109,9 @@ export class SearchService { } updateSearchFields( - itemsType: string, urlComponent: string, - selectionDetails: SelectionDetails + itemsType: string, + urlComponent: string, + selectionDetails: SelectionDetails ): void { const itemCodeGroup = urlComponent.match(/=\("[A-Za-z%20" ]+"\)/); const itemCodes = itemCodeGroup ? itemCodeGroup[0] : null; @@ -115,19 +119,27 @@ export class SearchService { const EXPECTED_PREFIX = '=("'; const EXPECTED_SUFFIX = '")'; - if (!itemCodes || - itemCodes.indexOf(EXPECTED_PREFIX) !== 0 || - itemCodes.lastIndexOf(EXPECTED_SUFFIX) !== - itemCodes.length - EXPECTED_SUFFIX.length || - itemCodes.lastIndexOf(EXPECTED_SUFFIX) === -1) { + if ( + !itemCodes || + itemCodes.indexOf(EXPECTED_PREFIX) !== 0 || + itemCodes.lastIndexOf(EXPECTED_SUFFIX) !== + itemCodes.length - EXPECTED_SUFFIX.length || + itemCodes.lastIndexOf(EXPECTED_SUFFIX) === -1 + ) { throw new Error( 'Invalid search query url fragment for ' + - itemsType + ': ' + urlComponent); + itemsType + + ': ' + + urlComponent + ); } - const items = itemCodes.substring( - EXPECTED_PREFIX.length, itemCodes.length - EXPECTED_SUFFIX.length - ).split('" OR "'); + const items = itemCodes + .substring( + EXPECTED_PREFIX.length, + itemCodes.length - EXPECTED_SUFFIX.length + ) + .split('" OR "'); const selections = selectionDetails[itemsType].selections; for (let i = 0; i < items.length; i++) { @@ -140,45 +152,57 @@ export class SearchService { } getSearchUrlQueryString( - searchQuery: string, - selectedCategories: SelectionList, - selectedLanguageCodes: SelectionList): string { - return encodeURIComponent(searchQuery) + - this._getSuffixForQuery(selectedCategories, selectedLanguageCodes); + searchQuery: string, + selectedCategories: SelectionList, + selectedLanguageCodes: SelectionList + ): string { + return ( + encodeURIComponent(searchQuery) + + this._getSuffixForQuery(selectedCategories, selectedLanguageCodes) + ); } - // Note that an empty query results in all activities being shown. executeSearchQuery( - searchQuery: string, - selectedCategories: SelectionList, - selectedLanguageCodes: SelectionList, - successCallback: () => void, - errorCallback?: (reason: string) => void): void { + searchQuery: string, + selectedCategories: SelectionList, + selectedLanguageCodes: SelectionList, + successCallback: () => void, + errorCallback?: (reason: string) => void + ): void { const queryUrl = this.getQueryUrl( this.getSearchUrlQueryString( - searchQuery, selectedCategories, selectedLanguageCodes)); + searchQuery, + selectedCategories, + selectedLanguageCodes + ) + ); this._isCurrentlyFetchingResults = true; this.numSearchesInProgress++; - this._searchBackendApiService.fetchExplorationSearchResultAsync(queryUrl) - .then((response) => { - this._lastQuery = searchQuery; - this._lastSelectedCategories = cloneDeep(selectedCategories); - this._lastSelectedLanguageCodes = cloneDeep(selectedLanguageCodes); - this._searchOffset = response.search_cursor; - this.numSearchesInProgress--; - - this._initialSearchResultsLoadedEventEmitter.emit( - response.activity_list); - - this._isCurrentlyFetchingResults = false; - }, (errorResponse) => { - this.numSearchesInProgress--; - if (errorCallback) { - errorCallback(errorResponse.error.error); + this._searchBackendApiService + .fetchExplorationSearchResultAsync(queryUrl) + .then( + response => { + this._lastQuery = searchQuery; + this._lastSelectedCategories = cloneDeep(selectedCategories); + this._lastSelectedLanguageCodes = cloneDeep(selectedLanguageCodes); + this._searchOffset = response.search_cursor; + this.numSearchesInProgress--; + + this._initialSearchResultsLoadedEventEmitter.emit( + response.activity_list + ); + + this._isCurrentlyFetchingResults = false; + }, + errorResponse => { + this.numSearchesInProgress--; + if (errorCallback) { + errorCallback(errorResponse.error.error); + } } - }); + ); if (successCallback) { successCallback(); @@ -194,9 +218,11 @@ export class SearchService { * selectionDetails. It will update selectionDetails with the relevant * fields that were extracted from the url. * @returns the unencoded search query string. - */ + */ updateSearchFieldsBasedOnUrlQuery( - urlComponent: string, selectionDetails: SelectionDetails): string { + urlComponent: string, + selectionDetails: SelectionDetails + ): string { const urlQuery = urlComponent.substring('?q='.length); // The following will split the urlQuery into 3 components: // 1. query @@ -237,11 +263,11 @@ export class SearchService { // Here failure callback is optional so that it gets invoked // only when the end of page has reached and return void otherwise. loadMoreData( - successCallback: ( - SearchResponseData: SearchResponseBackendDict, - boolean: boolean - ) => void, - failureCallback?: (arg0: boolean) => void + successCallback: ( + SearchResponseData: SearchResponseBackendDict, + boolean: boolean + ) => void, + failureCallback?: (arg0: boolean) => void ): void { // If a new query is still being sent, or the end of the page has been // reached, do not fetch more results. @@ -259,8 +285,9 @@ export class SearchService { } this._isCurrentlyFetchingResults = true; - this._searchBackendApiService.fetchExplorationSearchResultAsync(queryUrl) - .then((response) => { + this._searchBackendApiService + .fetchExplorationSearchResultAsync(queryUrl) + .then(response => { this._searchOffset = response.search_cursor; this._isCurrentlyFetchingResults = false; @@ -274,13 +301,11 @@ export class SearchService { return this._searchBarLoadedEventEmitter; } - get onInitialSearchResultsLoaded(): - EventEmitter { + get onInitialSearchResultsLoaded(): EventEmitter { return this._initialSearchResultsLoadedEventEmitter; } } -angular.module('oppia').factory( - 'SearchService', - downgradeInjectable(SearchService) -); +angular + .module('oppia') + .factory('SearchService', downgradeInjectable(SearchService)); diff --git a/core/templates/services/server-connection-backend-api.service.spec.ts b/core/templates/services/server-connection-backend-api.service.spec.ts index 1dd0d5948169..69ffb8c73089 100644 --- a/core/templates/services/server-connection-backend-api.service.spec.ts +++ b/core/templates/services/server-connection-backend-api.service.spec.ts @@ -16,10 +16,12 @@ * @fileoverview Tests that server connection service is working as expected. */ -import { HttpClientTestingModule, HttpTestingController } from - '@angular/common/http/testing'; -import { fakeAsync, flushMicrotasks, TestBed } from '@angular/core/testing'; -import { ServerConnectionBackendApiService } from 'services/server-connection-backend-api.service'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {fakeAsync, flushMicrotasks, TestBed} from '@angular/core/testing'; +import {ServerConnectionBackendApiService} from 'services/server-connection-backend-api.service'; describe('Server Connection Backend Api Service', () => { let serverConnectionBackendApiService: ServerConnectionBackendApiService; @@ -32,7 +34,8 @@ describe('Server Connection Backend Api Service', () => { httpTestingController = TestBed.inject(HttpTestingController); serverConnectionBackendApiService = TestBed.inject( - ServerConnectionBackendApiService); + ServerConnectionBackendApiService + ); }); afterEach(() => { diff --git a/core/templates/services/server-connection-backend-api.service.ts b/core/templates/services/server-connection-backend-api.service.ts index e922e5cd40c6..30782b0a1db9 100644 --- a/core/templates/services/server-connection-backend-api.service.ts +++ b/core/templates/services/server-connection-backend-api.service.ts @@ -17,27 +17,31 @@ * for connectivity. */ -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; export interface ConnectionCheckResponse { - isInternetConnected: boolean; - } + isInternetConnected: boolean; +} @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ServerConnectionBackendApiService { private checkConnectionUrl: string = '/internetconnectivityhandler'; constructor(private http: HttpClient) {} async fetchConnectionCheckResultAsync(): Promise { - return this.http.get( - this.checkConnectionUrl).toPromise(); + return this.http + .get(this.checkConnectionUrl) + .toPromise(); } } -angular.module('oppia').factory( - 'ServerConnectionBackendApiService', - downgradeInjectable(ServerConnectionBackendApiService)); +angular + .module('oppia') + .factory( + 'ServerConnectionBackendApiService', + downgradeInjectable(ServerConnectionBackendApiService) + ); diff --git a/core/templates/services/services.constants.ajs.ts b/core/templates/services/services.constants.ajs.ts index 491396a5ee40..aee7b36870d3 100644 --- a/core/templates/services/services.constants.ajs.ts +++ b/core/templates/services/services.constants.ajs.ts @@ -18,62 +18,90 @@ // TODO(#7092): Delete this file once migration is complete and these AngularJS // equivalents of the Angular constants are no longer needed. -import { ServicesConstants } from 'services/services.constants'; +import {ServicesConstants} from 'services/services.constants'; -angular.module('oppia').constant( - 'PAGE_CONTEXT', ServicesConstants.PAGE_CONTEXT); +angular + .module('oppia') + .constant('PAGE_CONTEXT', ServicesConstants.PAGE_CONTEXT); -angular.module('oppia').constant( - 'EXPLORATION_EDITOR_TAB_CONTEXT', - ServicesConstants.EXPLORATION_EDITOR_TAB_CONTEXT); +angular + .module('oppia') + .constant( + 'EXPLORATION_EDITOR_TAB_CONTEXT', + ServicesConstants.EXPLORATION_EDITOR_TAB_CONTEXT + ); -angular.module('oppia').constant( - 'EXPLORATION_FEATURES_URL', ServicesConstants.EXPLORATION_FEATURES_URL); +angular + .module('oppia') + .constant( + 'EXPLORATION_FEATURES_URL', + ServicesConstants.EXPLORATION_FEATURES_URL + ); -angular.module('oppia').constant( - 'FETCH_ISSUES_URL', ServicesConstants.FETCH_ISSUES_URL); +angular + .module('oppia') + .constant('FETCH_ISSUES_URL', ServicesConstants.FETCH_ISSUES_URL); -angular.module('oppia').constant( - 'FETCH_PLAYTHROUGH_URL', - ServicesConstants.FETCH_PLAYTHROUGH_URL); +angular + .module('oppia') + .constant('FETCH_PLAYTHROUGH_URL', ServicesConstants.FETCH_PLAYTHROUGH_URL); -angular.module('oppia').constant( - 'RESOLVE_ISSUE_URL', ServicesConstants.RESOLVE_ISSUE_URL); +angular + .module('oppia') + .constant('RESOLVE_ISSUE_URL', ServicesConstants.RESOLVE_ISSUE_URL); -angular.module('oppia').constant( - 'STORE_PLAYTHROUGH_URL', - ServicesConstants.STORE_PLAYTHROUGH_URL); +angular + .module('oppia') + .constant('STORE_PLAYTHROUGH_URL', ServicesConstants.STORE_PLAYTHROUGH_URL); // Enables recording playthroughs from learner sessions. -angular.module('oppia').constant( - 'EARLY_QUIT_THRESHOLD_IN_SECS', - ServicesConstants.EARLY_QUIT_THRESHOLD_IN_SECS); -angular.module('oppia').constant( - 'NUM_INCORRECT_ANSWERS_THRESHOLD', - ServicesConstants.NUM_INCORRECT_ANSWERS_THRESHOLD); -angular.module('oppia').constant( - 'NUM_REPEATED_CYCLES_THRESHOLD', - ServicesConstants.NUM_REPEATED_CYCLES_THRESHOLD); -angular.module('oppia').constant( - 'CURRENT_ACTION_SCHEMA_VERSION', - ServicesConstants.CURRENT_ACTION_SCHEMA_VERSION); -angular.module('oppia').constant( - 'CURRENT_ISSUE_SCHEMA_VERSION', - ServicesConstants.CURRENT_ISSUE_SCHEMA_VERSION); +angular + .module('oppia') + .constant( + 'EARLY_QUIT_THRESHOLD_IN_SECS', + ServicesConstants.EARLY_QUIT_THRESHOLD_IN_SECS + ); +angular + .module('oppia') + .constant( + 'NUM_INCORRECT_ANSWERS_THRESHOLD', + ServicesConstants.NUM_INCORRECT_ANSWERS_THRESHOLD + ); +angular + .module('oppia') + .constant( + 'NUM_REPEATED_CYCLES_THRESHOLD', + ServicesConstants.NUM_REPEATED_CYCLES_THRESHOLD + ); +angular + .module('oppia') + .constant( + 'CURRENT_ACTION_SCHEMA_VERSION', + ServicesConstants.CURRENT_ACTION_SCHEMA_VERSION + ); +angular + .module('oppia') + .constant( + 'CURRENT_ISSUE_SCHEMA_VERSION', + ServicesConstants.CURRENT_ISSUE_SCHEMA_VERSION + ); // Whether to enable the promo bar functionality. This does not actually turn on // the promo bar, as that is gated by a config value (see config_domain). This // merely avoids checking for whether the promo bar is enabled for every Oppia // page visited. -angular.module('oppia').constant( - 'ENABLE_PROMO_BAR', ServicesConstants.ENABLE_PROMO_BAR); +angular + .module('oppia') + .constant('ENABLE_PROMO_BAR', ServicesConstants.ENABLE_PROMO_BAR); -angular.module('oppia').constant( - 'RTE_COMPONENT_SPECS', ServicesConstants.RTE_COMPONENT_SPECS); +angular + .module('oppia') + .constant('RTE_COMPONENT_SPECS', ServicesConstants.RTE_COMPONENT_SPECS); -angular.module('oppia').constant( - 'SEARCH_DATA_URL', ServicesConstants.SEARCH_DATA_URL); +angular + .module('oppia') + .constant('SEARCH_DATA_URL', ServicesConstants.SEARCH_DATA_URL); -angular.module('oppia').constant( - 'STATE_ANSWER_STATS_URL', - ServicesConstants.STATE_ANSWER_STATS_URL); +angular + .module('oppia') + .constant('STATE_ANSWER_STATS_URL', ServicesConstants.STATE_ANSWER_STATS_URL); diff --git a/core/templates/services/services.constants.ts b/core/templates/services/services.constants.ts index 4ac32e502981..1a9810f247a7 100644 --- a/core/templates/services/services.constants.ts +++ b/core/templates/services/services.constants.ts @@ -32,16 +32,15 @@ export const ServicesConstants = { TOPICS_AND_SKILLS_DASHBOARD: 'topics_and_skills_dashboard', CONTRIBUTOR_DASHBOARD: 'contributor_dashboard', BLOG_DASHBOARD: 'blog_dashboard', - OTHER: 'other' + OTHER: 'other', }, EXPLORATION_EDITOR_TAB_CONTEXT: { EDITOR: 'editor', - PREVIEW: 'preview' + PREVIEW: 'preview', }, - EXPLORATION_FEATURES_URL: - '/explorehandler/features/', + EXPLORATION_FEATURES_URL: '/explorehandler/features/', FETCH_ISSUES_URL: '/issuesdatahandler/', @@ -52,8 +51,7 @@ export const ServicesConstants = { PROMO_BAR_URL: '/promo_bar_handler', - STORE_PLAYTHROUGH_URL: - '/explorehandler/store_playthrough/', + STORE_PLAYTHROUGH_URL: '/explorehandler/store_playthrough/', // Enables recording playthroughs from learner sessions. MIN_PLAYTHROUGH_DURATION_IN_SECS: 45, @@ -71,8 +69,7 @@ export const ServicesConstants = { SEARCH_DATA_URL: '/searchhandler/data', - STATE_ANSWER_STATS_URL: - '/createhandler/state_answer_stats/', + STATE_ANSWER_STATS_URL: '/createhandler/state_answer_stats/', RTE_COMPONENT_SPECS: RTE_COMPONENT_SPECS, @@ -81,7 +78,6 @@ export const ServicesConstants = { EXPLORATION_LOADED: 'explorationLoaded', STATE_TRANSITION: 'stateTransition', EXPLORATION_RESET: 'explorationReset', - EXPLORATION_COMPLETED: 'explorationCompleted' + EXPLORATION_COMPLETED: 'explorationCompleted', }, - } as const; diff --git a/core/templates/services/sidebar-status.service.spec.ts b/core/templates/services/sidebar-status.service.spec.ts index 15ba0be8bb66..fa692486d90c 100644 --- a/core/templates/services/sidebar-status.service.spec.ts +++ b/core/templates/services/sidebar-status.service.spec.ts @@ -16,10 +16,10 @@ * @fileoverview Tests for SidebarStatusService. */ -import { TestBed } from '@angular/core/testing'; +import {TestBed} from '@angular/core/testing'; -import { SidebarStatusService } from 'services/sidebar-status.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import {SidebarStatusService} from 'services/sidebar-status.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; describe('SidebarStatusService', () => { let sss: SidebarStatusService; @@ -37,7 +37,7 @@ describe('SidebarStatusService', () => { // ref: https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty // ref: https://github.com/jasmine/jasmine/issues/1415 Object.defineProperty($window.nativeWindow, 'innerWidth', { - get: () => undefined + get: () => undefined, }); spyOnProperty($window.nativeWindow, 'innerWidth').and.returnValue(600); }); diff --git a/core/templates/services/sidebar-status.service.ts b/core/templates/services/sidebar-status.service.ts index 14c52ab1446a..4a00fef6bd54 100644 --- a/core/templates/services/sidebar-status.service.ts +++ b/core/templates/services/sidebar-status.service.ts @@ -17,13 +17,12 @@ * hamburger-menu sidebar. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { WindowDimensionsService } from - 'services/contextual/window-dimensions.service'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {WindowDimensionsService} from 'services/contextual/window-dimensions.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class SidebarStatusService { constructor(private wds: WindowDimensionsService) {} @@ -84,5 +83,6 @@ export class SidebarStatusService { } } -angular.module('oppia').factory( - 'SidebarStatusService', downgradeInjectable(SidebarStatusService)); +angular + .module('oppia') + .factory('SidebarStatusService', downgradeInjectable(SidebarStatusService)); diff --git a/core/templates/services/site-analytics.service.spec.ts b/core/templates/services/site-analytics.service.spec.ts index c1290ac2213a..5e7e86292d85 100644 --- a/core/templates/services/site-analytics.service.spec.ts +++ b/core/templates/services/site-analytics.service.spec.ts @@ -16,9 +16,9 @@ * @fileoverview Unit tests for SiteAnalyticsService. */ -import { TestBed } from '@angular/core/testing'; -import { SiteAnalyticsService } from 'services/site-analytics.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import {TestBed} from '@angular/core/testing'; +import {SiteAnalyticsService} from 'services/site-analytics.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; describe('Site Analytics Service', () => { let sas: SiteAnalyticsService; @@ -31,8 +31,8 @@ describe('Site Analytics Service', () => { nativeWindow = { gtag: () => {}, location: { - pathname - } + pathname, + }, }; } @@ -41,9 +41,9 @@ describe('Site Analytics Service', () => { providers: [ { provide: WindowRef, - useClass: MockWindowRef - } - ] + useClass: MockWindowRef, + }, + ], }).compileComponents(); sas = TestBed.inject(SiteAnalyticsService); @@ -65,7 +65,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'click', { event_category: 'LoginButton', - event_label: pathname + ' LoginEventButton' + event_label: pathname + ' LoginEventButton', }); }); @@ -73,7 +73,7 @@ describe('Site Analytics Service', () => { sas.registerNewSignupEvent('srcElement'); expect(gtagSpy).toHaveBeenCalledWith('event', 'sign_up', { - source_element: 'srcElement' + source_element: 'srcElement', }); }); @@ -82,7 +82,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'click', { event_category: 'BrowseLessonsButton', - event_label: pathname + event_label: pathname, }); }); @@ -91,7 +91,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'click', { event_category: 'StartLearningButton', - event_label: pathname + event_label: pathname, }); }); @@ -100,7 +100,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'click', { event_category: 'StartContributingButton', - event_label: pathname + event_label: pathname, }); }); @@ -110,7 +110,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'click', { event_category: 'GoToDonationSite', - event_label: donationSite + event_label: donationSite, }); }); @@ -119,7 +119,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'click', { event_category: 'ApplyToTeachWithOppia', - event_label: '' + event_label: '', }); }); @@ -128,7 +128,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'click', { event_category: 'CreateExplorationButton', - event_label: pathname + event_label: pathname, }); }); @@ -137,18 +137,17 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'create', { event_category: 'NewExploration', - event_label: explorationId + event_label: explorationId, }); }); it('should register create new exploration in collection event', () => { sas.registerCreateNewExplorationInCollectionEvent(explorationId); - expect(gtagSpy).toHaveBeenCalledWith( - 'event', 'create', { - event_category: 'NewExplorationFromCollection', - event_label: explorationId - }); + expect(gtagSpy).toHaveBeenCalledWith('event', 'create', { + event_category: 'NewExplorationFromCollection', + event_label: explorationId, + }); }); it('should register new collection event', () => { @@ -157,18 +156,17 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'create', { event_category: 'NewCollection', - event_label: collectionId + event_label: collectionId, }); }); it('should register commit changes to private exploration event', () => { sas.registerCommitChangesToPrivateExplorationEvent(explorationId); - expect(gtagSpy).toHaveBeenCalledWith( - 'event', 'click', { - event_category: 'CommitToPrivateExploration', - event_label: explorationId - }); + expect(gtagSpy).toHaveBeenCalledWith('event', 'click', { + event_category: 'CommitToPrivateExploration', + event_label: explorationId, + }); }); it('should register share exploration event', () => { @@ -177,7 +175,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'share', { event_category: network, - event_label: pathname + event_label: pathname, }); }); @@ -187,7 +185,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'share', { event_category: network, - event_label: pathname + event_label: pathname, }); }); @@ -197,7 +195,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'share', { event_category: network, - event_label: pathname + event_label: pathname, }); }); @@ -206,7 +204,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'open', { event_category: 'EmbedInfoModal', - event_label: explorationId + event_label: explorationId, }); }); @@ -215,7 +213,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'click', { event_category: 'CommitToPublicExploration', - event_label: explorationId + event_label: explorationId, }); }); @@ -224,7 +222,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'open', { event_category: 'TutorialModalOpen', - event_label: explorationId + event_label: explorationId, }); }); @@ -233,7 +231,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'click', { event_category: 'DeclineTutorialModal', - event_label: explorationId + event_label: explorationId, }); }); @@ -242,7 +240,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'click', { event_category: 'AcceptTutorialModal', - event_label: explorationId + event_label: explorationId, }); }); @@ -251,7 +249,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'click', { event_category: 'ClickHelpButton', - event_label: explorationId + event_label: explorationId, }); }); @@ -260,7 +258,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'click', { event_category: 'VisitHelpCenter', - event_label: explorationId + event_label: explorationId, }); }); @@ -269,7 +267,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'click', { event_category: 'OpenTutorialFromHelpCenter', - event_label: explorationId + event_label: explorationId, }); }); @@ -278,7 +276,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'click', { event_category: 'SkipTutorial', - event_label: explorationId + event_label: explorationId, }); }); @@ -287,7 +285,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'click', { event_category: 'FinishTutorial', - event_label: explorationId + event_label: explorationId, }); }); @@ -296,7 +294,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'open', { event_category: 'FirstEnterEditor', - event_label: explorationId + event_label: explorationId, }); }); @@ -305,7 +303,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'open', { event_category: 'FirstOpenContentBox', - event_label: explorationId + event_label: explorationId, }); }); @@ -314,7 +312,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'click', { event_category: 'FirstSaveContent', - event_label: explorationId + event_label: explorationId, }); }); @@ -323,7 +321,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'click', { event_category: 'FirstClickAddInteraction', - event_label: explorationId + event_label: explorationId, }); }); @@ -332,7 +330,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'click', { event_category: 'FirstSelectInteractionType', - event_label: explorationId + event_label: explorationId, }); }); @@ -341,7 +339,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'click', { event_category: 'FirstSaveInteraction', - event_label: explorationId + event_label: explorationId, }); }); @@ -350,7 +348,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'click', { event_category: 'FirstSaveRule', - event_label: explorationId + event_label: explorationId, }); }); @@ -359,7 +357,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'create', { event_category: 'FirstCreateSecondState', - event_label: explorationId + event_label: explorationId, }); }); @@ -368,7 +366,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'save', { event_category: 'SavePlayableExploration', - event_label: explorationId + event_label: explorationId, }); }); @@ -377,7 +375,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'open', { event_category: 'PublishExplorationModal', - event_label: explorationId + event_label: explorationId, }); }); @@ -386,7 +384,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'click', { event_category: 'PublishExploration', - event_label: explorationId + event_label: explorationId, }); }); @@ -395,7 +393,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'click', { event_category: 'VisitOppiaFromIframe', - event_label: explorationId + event_label: explorationId, }); }); @@ -405,20 +403,23 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'click', { event_category: 'PlayerNewCard', - event_label: String(cardNumber) + event_label: String(cardNumber), }); }); - it('should register new card when card number is greather than 10 and' + - ' it\'s a multiple of 10', () => { - const cardNumber = 20; - sas.registerNewCard(cardNumber, 'abc1'); + it( + 'should register new card when card number is greather than 10 and' + + " it's a multiple of 10", + () => { + const cardNumber = 20; + sas.registerNewCard(cardNumber, 'abc1'); - expect(gtagSpy).toHaveBeenCalledWith('event', 'click', { - event_category: 'PlayerNewCard', - event_label: String(cardNumber) - }); - }); + expect(gtagSpy).toHaveBeenCalledWith('event', 'click', { + event_category: 'PlayerNewCard', + event_label: String(cardNumber), + }); + } + ); it('should not register new card', () => { const cardNumber = 35; @@ -432,7 +433,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'engage', { event_category: 'PlayerFinishExploration', - event_label: '123' + event_label: '123', }); }); @@ -444,7 +445,7 @@ describe('Site Analytics Service', () => { 'classroom_lesson_started', { topic_name: 'Fractions', - exploration_id: '123' + exploration_id: '123', } ); }); @@ -470,7 +471,7 @@ describe('Site Analytics Service', () => { exploration_id: '123', chapter_number: '2', chapter_card_count: '3', - exploration_language: 'en' + exploration_language: 'en', } ); }); @@ -481,7 +482,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'click', { event_category: 'OpenFractionsFromLandingPage', - event_label: collectionId + event_label: collectionId, }); }); @@ -490,7 +491,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'click', { event_category: 'SaveRecordedAudio', - event_label: explorationId + event_label: explorationId, }); }); @@ -499,7 +500,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'click', { event_category: 'StartAudioRecording', - event_label: explorationId + event_label: explorationId, }); }); @@ -508,7 +509,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'click', { event_category: 'UploadRecordedAudio', - event_label: explorationId + event_label: explorationId, }); }); @@ -518,7 +519,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'click', { event_category: 'ContributorDashboardSuggest', - event_label: contributionType + event_label: contributionType, }); }); @@ -528,21 +529,19 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'click', { event_category: 'ContributorDashboardSubmitSuggestion', - event_label: contributionType + event_label: contributionType, }); }); - it('should register Contributor Dashboard view suggestion for review event', - () => { - const contributionType = 'Translation'; - sas.registerContributorDashboardViewSuggestionForReview( - contributionType); + it('should register Contributor Dashboard view suggestion for review event', () => { + const contributionType = 'Translation'; + sas.registerContributorDashboardViewSuggestionForReview(contributionType); - expect(gtagSpy).toHaveBeenCalledWith('event', 'click', { - event_category: 'ContributorDashboardViewSuggestionForReview', - event_label: contributionType - }); + expect(gtagSpy).toHaveBeenCalledWith('event', 'click', { + event_category: 'ContributorDashboardViewSuggestionForReview', + event_label: contributionType, }); + }); it('should register Contributor Dashboard accept suggestion event', () => { const contributionType = 'Translation'; @@ -550,7 +549,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'click', { event_category: 'ContributorDashboardAcceptSuggestion', - event_label: contributionType + event_label: contributionType, }); }); @@ -560,7 +559,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'click', { event_category: 'ContributorDashboardRejectSuggestion', - event_label: contributionType + event_label: contributionType, }); }); @@ -569,7 +568,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'engage', { event_category: 'ActiveUserStartAndSawCards', - event_label: '' + event_label: '', }); }); @@ -578,7 +577,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'engage', { event_category: 'PlayerStartExploration', - event_label: explorationId + event_label: explorationId, }); }); @@ -587,13 +586,23 @@ describe('Site Analytics Service', () => { sas.registerClassroomPageViewed(); expect(sas._sendEventToLegacyGoogleAnalytics).toHaveBeenCalledWith( - 'ClassroomEngagement', 'impression', 'ViewClassroom'); + 'ClassroomEngagement', + 'impression', + 'ViewClassroom' + ); }); it('should register active classroom lesson usage', () => { let explorationId = '123'; sas.registerClassroomLessonEngagedWithEvent( - 'math', 'Fractions', 'ch1', explorationId, '2', '3', 'en'); + 'math', + 'Fractions', + 'ch1', + explorationId, + '2', + '3', + 'en' + ); expect(gtagSpy).toHaveBeenCalledWith( 'event', @@ -605,7 +614,7 @@ describe('Site Analytics Service', () => { exploration_id: '123', chapter_number: '2', chapter_card_count: '3', - exploration_language: 'en' + exploration_language: 'en', } ); }); @@ -615,7 +624,7 @@ describe('Site Analytics Service', () => { expect(gtagSpy).toHaveBeenCalledWith('event', 'click', { event_category: 'ClassroomEngagement', - event_label: 'ClickOnClassroom' + event_label: 'ClickOnClassroom', }); }); @@ -623,9 +632,10 @@ describe('Site Analytics Service', () => { sas.registerCommunityLessonCompleted('exp_id'); expect(gtagSpy).toHaveBeenCalledWith( - 'event', 'community_lesson_completed', + 'event', + 'community_lesson_completed', { - exploration_id: 'exp_id' + exploration_id: 'exp_id', } ); }); @@ -634,9 +644,10 @@ describe('Site Analytics Service', () => { sas.registerCommunityLessonStarted('exp_id'); expect(gtagSpy).toHaveBeenCalledWith( - 'event', 'community_lesson_started', + 'event', + 'community_lesson_started', { - exploration_id: 'exp_id' + exploration_id: 'exp_id', } ); }); @@ -644,40 +655,34 @@ describe('Site Analytics Service', () => { it('should register audio play event', () => { sas.registerStartAudioPlayedEvent('exp_id', 0); - expect(gtagSpy).toHaveBeenCalledWith( - 'event', 'audio_played', - { - exploration_id: 'exp_id', - card_number: 0 - } - ); + expect(gtagSpy).toHaveBeenCalledWith('event', 'audio_played', { + exploration_id: 'exp_id', + card_number: 0, + }); }); it('should register practice session start event', () => { sas.registerPracticeSessionStartEvent('math', 'topic', '1,2,3'); - expect(gtagSpy).toHaveBeenCalledWith( - 'event', 'practice_session_start', - { - classroom_name: 'math', - topic_name: 'topic', - practice_session_id: '1,2,3' - } - ); + expect(gtagSpy).toHaveBeenCalledWith('event', 'practice_session_start', { + classroom_name: 'math', + topic_name: 'topic', + practice_session_id: '1,2,3', + }); }); it('should register practice session end event', () => { - sas.registerPracticeSessionEndEvent( - 'math', 'topic', '1,2,3', 10, 10); + sas.registerPracticeSessionEndEvent('math', 'topic', '1,2,3', 10, 10); expect(gtagSpy).toHaveBeenCalledWith( - 'event', 'practice_session_complete', + 'event', + 'practice_session_complete', { classroom_name: 'math', topic_name: 'topic', practice_session_id: '1,2,3', questions_answered: 10, - total_score: 10 + total_score: 10, } ); }); @@ -685,16 +690,16 @@ describe('Site Analytics Service', () => { it('should register search results viewed event', () => { sas.registerSearchResultsViewedEvent(); - expect(gtagSpy).toHaveBeenCalledWith( - 'event', 'view_search_results', {} - ); + expect(gtagSpy).toHaveBeenCalledWith('event', 'view_search_results', {}); }); it('should register homepage start learning button click event', () => { sas.registerClickHomePageStartLearningButtonEvent(); expect(gtagSpy).toHaveBeenCalledWith( - 'event', 'discovery_start_learning', {} + 'event', + 'discovery_start_learning', + {} ); }); @@ -702,12 +707,10 @@ describe('Site Analytics Service', () => { const answerIsCorrect = true; sas.registerAnswerSubmitted(explorationId, answerIsCorrect); - expect(gtagSpy).toHaveBeenCalledWith( - 'event', 'answer_submitted', { - exploration_id: explorationId, - answer_is_correct: answerIsCorrect, - } - ); + expect(gtagSpy).toHaveBeenCalledWith('event', 'answer_submitted', { + exploration_id: explorationId, + answer_is_correct: answerIsCorrect, + }); }); }); }); diff --git a/core/templates/services/site-analytics.service.ts b/core/templates/services/site-analytics.service.ts index 33dbfe6da5f3..141888a2c985 100644 --- a/core/templates/services/site-analytics.service.ts +++ b/core/templates/services/site-analytics.service.ts @@ -17,11 +17,11 @@ * the learner and editor views. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { WindowRef } from 'services/contextual/window-ref.service'; -import { initializeGoogleAnalytics } from 'google-analytics.initializer'; +import {WindowRef} from 'services/contextual/window-ref.service'; +import {initializeGoogleAnalytics} from 'google-analytics.initializer'; // Service for sending events to Google Analytics. // @@ -30,7 +30,7 @@ import { initializeGoogleAnalytics } from 'google-analytics.initializer'; // owner in feconf.py. @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class SiteAnalyticsService { static googleAnalyticsIsInitialized: boolean = false; @@ -47,50 +47,53 @@ export class SiteAnalyticsService { // For definitions of the various arguments, please see: // https://developers.google.com/analytics/devguides/collection/analyticsjs/events _sendEventToLegacyGoogleAnalytics( - eventCategory: string, eventAction: string, eventLabel: string): void { + eventCategory: string, + eventAction: string, + eventLabel: string + ): void { this.windowRef.nativeWindow.gtag('event', eventAction, { event_category: eventCategory, - event_label: eventLabel + event_label: eventLabel, }); } _sendEventToGoogleAnalytics( - eventName: string, - eventParameters: Object = {} + eventName: string, + eventParameters: Object = {} ): void { - this.windowRef.nativeWindow.gtag( - 'event', - eventName, - eventParameters - ); + this.windowRef.nativeWindow.gtag('event', eventName, eventParameters); } // The srcElement refers to the element on the page that is clicked. registerStartLoginEvent(srcElement: string): void { this._sendEventToLegacyGoogleAnalytics( - 'LoginButton', 'click', - this.windowRef.nativeWindow.location.pathname + ' ' + srcElement); + 'LoginButton', + 'click', + this.windowRef.nativeWindow.location.pathname + ' ' + srcElement + ); this._sendEventToGoogleAnalytics('login', { - source_element: srcElement + source_element: srcElement, }); } registerNewSignupEvent(srcElement: string): void { this._sendEventToGoogleAnalytics('sign_up', { - source_element: srcElement + source_element: srcElement, }); } registerSiteLanguageChangeEvent(siteLanguageCode: string): void { this._sendEventToGoogleAnalytics('page_load', { - site_language: siteLanguageCode + site_language: siteLanguageCode, }); } registerClickBrowseLessonsButtonEvent(): void { this._sendEventToLegacyGoogleAnalytics( - 'BrowseLessonsButton', 'click', - this.windowRef.nativeWindow.location.pathname); + 'BrowseLessonsButton', + 'click', + this.windowRef.nativeWindow.location.pathname + ); this._sendEventToGoogleAnalytics('discovery_browse_lessons'); } @@ -104,316 +107,443 @@ export class SiteAnalyticsService { registerClickGuideParentsButtonEvent(): void { this._sendEventToLegacyGoogleAnalytics( - 'GuideParentsButton', 'click', - this.windowRef.nativeWindow.location.pathname); + 'GuideParentsButton', + 'click', + this.windowRef.nativeWindow.location.pathname + ); } registerClickTipforParentsButtonEvent(): void { this._sendEventToLegacyGoogleAnalytics( - 'TipforParentsButton', 'click', - this.windowRef.nativeWindow.location.pathname); + 'TipforParentsButton', + 'click', + this.windowRef.nativeWindow.location.pathname + ); } registerClickExploreLessonsButtonEvent(): void { this._sendEventToLegacyGoogleAnalytics( - 'ExploreLessonsButton', 'click', - this.windowRef.nativeWindow.location.pathname); + 'ExploreLessonsButton', + 'click', + this.windowRef.nativeWindow.location.pathname + ); } registerClickStartLearningButtonEvent(): void { this._sendEventToLegacyGoogleAnalytics( - 'StartLearningButton', 'click', - this.windowRef.nativeWindow.location.pathname); + 'StartLearningButton', + 'click', + this.windowRef.nativeWindow.location.pathname + ); } registerClickStartContributingButtonEvent(): void { this._sendEventToLegacyGoogleAnalytics( - 'StartContributingButton', 'click', - this.windowRef.nativeWindow.location.pathname); + 'StartContributingButton', + 'click', + this.windowRef.nativeWindow.location.pathname + ); } registerClickStartTeachingButtonEvent(): void { this._sendEventToLegacyGoogleAnalytics( - 'StartTeachingButton', 'click', - this.windowRef.nativeWindow.location.pathname); + 'StartTeachingButton', + 'click', + this.windowRef.nativeWindow.location.pathname + ); } registerClickVisitClassroomButtonEvent(): void { this._sendEventToLegacyGoogleAnalytics( - 'ClassroomButton', 'click', - this.windowRef.nativeWindow.location.pathname); + 'ClassroomButton', + 'click', + this.windowRef.nativeWindow.location.pathname + ); } registerClickBrowseLibraryButtonEvent(): void { this._sendEventToLegacyGoogleAnalytics( - 'BrowseLibraryButton', 'click', - this.windowRef.nativeWindow.location.pathname); + 'BrowseLibraryButton', + 'click', + this.windowRef.nativeWindow.location.pathname + ); } registerGoToDonationSiteEvent(donationSiteName: string): void { this._sendEventToLegacyGoogleAnalytics( - 'GoToDonationSite', 'click', donationSiteName); + 'GoToDonationSite', + 'click', + donationSiteName + ); } registerCreateLessonButtonEvent(): void { this._sendEventToLegacyGoogleAnalytics( - 'CreateLessonButton', 'click', - this.windowRef.nativeWindow.location.pathname); + 'CreateLessonButton', + 'click', + this.windowRef.nativeWindow.location.pathname + ); } registerApplyToTeachWithOppiaEvent(): void { this._sendEventToLegacyGoogleAnalytics( - 'ApplyToTeachWithOppia', 'click', ''); + 'ApplyToTeachWithOppia', + 'click', + '' + ); } registerClickCreateExplorationButtonEvent(): void { this._sendEventToLegacyGoogleAnalytics( - 'CreateExplorationButton', 'click', - this.windowRef.nativeWindow.location.pathname); + 'CreateExplorationButton', + 'click', + this.windowRef.nativeWindow.location.pathname + ); } registerCreateNewExplorationEvent(explorationId: string): void { this._sendEventToLegacyGoogleAnalytics( - 'NewExploration', 'create', explorationId); + 'NewExploration', + 'create', + explorationId + ); } registerCreateNewExplorationInCollectionEvent(explorationId: string): void { this._sendEventToLegacyGoogleAnalytics( - 'NewExplorationFromCollection', 'create', explorationId); + 'NewExplorationFromCollection', + 'create', + explorationId + ); } registerCreateNewCollectionEvent(collectionId: string): void { this._sendEventToLegacyGoogleAnalytics( - 'NewCollection', 'create', collectionId); + 'NewCollection', + 'create', + collectionId + ); } registerCommitChangesToPrivateExplorationEvent(explorationId: string): void { this._sendEventToLegacyGoogleAnalytics( - 'CommitToPrivateExploration', 'click', explorationId); + 'CommitToPrivateExploration', + 'click', + explorationId + ); } registerShareExplorationEvent(network: string): void { this._sendEventToLegacyGoogleAnalytics( - network, 'share', this.windowRef.nativeWindow.location.pathname); + network, + 'share', + this.windowRef.nativeWindow.location.pathname + ); } registerShareCollectionEvent(network: string): void { this._sendEventToLegacyGoogleAnalytics( - network, 'share', this.windowRef.nativeWindow.location.pathname); + network, + 'share', + this.windowRef.nativeWindow.location.pathname + ); } registerShareBlogPostEvent(network: string): void { this._sendEventToLegacyGoogleAnalytics( - network, 'share', this.windowRef.nativeWindow.location.pathname); + network, + 'share', + this.windowRef.nativeWindow.location.pathname + ); } registerOpenEmbedInfoEvent(explorationId: string): void { this._sendEventToLegacyGoogleAnalytics( - 'EmbedInfoModal', 'open', explorationId); + 'EmbedInfoModal', + 'open', + explorationId + ); } registerCommitChangesToPublicExplorationEvent(explorationId: string): void { this._sendEventToLegacyGoogleAnalytics( - 'CommitToPublicExploration', 'click', explorationId); + 'CommitToPublicExploration', + 'click', + explorationId + ); } // Metrics for tutorial on first creating exploration. registerTutorialModalOpenEvent(explorationId: string): void { this._sendEventToLegacyGoogleAnalytics( - 'TutorialModalOpen', 'open', explorationId); + 'TutorialModalOpen', + 'open', + explorationId + ); } registerDeclineTutorialModalEvent(explorationId: string): void { this._sendEventToLegacyGoogleAnalytics( - 'DeclineTutorialModal', 'click', explorationId); + 'DeclineTutorialModal', + 'click', + explorationId + ); } registerAcceptTutorialModalEvent(explorationId: string): void { this._sendEventToLegacyGoogleAnalytics( - 'AcceptTutorialModal', 'click', explorationId); + 'AcceptTutorialModal', + 'click', + explorationId + ); } // Metrics for visiting the help center. registerClickHelpButtonEvent(explorationId: string): void { this._sendEventToLegacyGoogleAnalytics( - 'ClickHelpButton', 'click', explorationId); + 'ClickHelpButton', + 'click', + explorationId + ); } registerVisitHelpCenterEvent(explorationId: string): void { this._sendEventToLegacyGoogleAnalytics( - 'VisitHelpCenter', 'click', explorationId); + 'VisitHelpCenter', + 'click', + explorationId + ); } registerOpenTutorialFromHelpCenterEvent(explorationId: string): void { this._sendEventToLegacyGoogleAnalytics( - 'OpenTutorialFromHelpCenter', 'click', explorationId); + 'OpenTutorialFromHelpCenter', + 'click', + explorationId + ); } // Metrics for exiting the tutorial. registerSkipTutorialEvent(explorationId: string): void { this._sendEventToLegacyGoogleAnalytics( - 'SkipTutorial', 'click', explorationId); + 'SkipTutorial', + 'click', + explorationId + ); } registerFinishTutorialEvent(explorationId: string): void { this._sendEventToLegacyGoogleAnalytics( - 'FinishTutorial', 'click', explorationId); + 'FinishTutorial', + 'click', + explorationId + ); } // Metrics for first time editor use. registerEditorFirstEntryEvent(explorationId: string): void { this._sendEventToLegacyGoogleAnalytics( - 'FirstEnterEditor', 'open', explorationId); + 'FirstEnterEditor', + 'open', + explorationId + ); } registerFirstOpenContentBoxEvent(explorationId: string): void { this._sendEventToLegacyGoogleAnalytics( - 'FirstOpenContentBox', 'open', explorationId); + 'FirstOpenContentBox', + 'open', + explorationId + ); } registerFirstSaveContentEvent(explorationId: string): void { this._sendEventToLegacyGoogleAnalytics( - 'FirstSaveContent', 'click', explorationId); + 'FirstSaveContent', + 'click', + explorationId + ); } registerFirstClickAddInteractionEvent(explorationId: string): void { this._sendEventToLegacyGoogleAnalytics( - 'FirstClickAddInteraction', 'click', explorationId); + 'FirstClickAddInteraction', + 'click', + explorationId + ); } registerFirstSelectInteractionTypeEvent(explorationId: string): void { this._sendEventToLegacyGoogleAnalytics( - 'FirstSelectInteractionType', 'click', explorationId); + 'FirstSelectInteractionType', + 'click', + explorationId + ); } registerFirstSaveInteractionEvent(explorationId: string): void { this._sendEventToLegacyGoogleAnalytics( - 'FirstSaveInteraction', 'click', explorationId); + 'FirstSaveInteraction', + 'click', + explorationId + ); } registerFirstSaveRuleEvent(explorationId: string): void { this._sendEventToLegacyGoogleAnalytics( - 'FirstSaveRule', 'click', explorationId); + 'FirstSaveRule', + 'click', + explorationId + ); } registerFirstCreateSecondStateEvent(explorationId: string): void { this._sendEventToLegacyGoogleAnalytics( - 'FirstCreateSecondState', 'create', explorationId); + 'FirstCreateSecondState', + 'create', + explorationId + ); } // Metrics for publishing explorations. registerSavePlayableExplorationEvent(explorationId: string): void { this._sendEventToLegacyGoogleAnalytics( - 'SavePlayableExploration', 'save', explorationId); + 'SavePlayableExploration', + 'save', + explorationId + ); } registerOpenPublishExplorationModalEvent(explorationId: string): void { this._sendEventToLegacyGoogleAnalytics( - 'PublishExplorationModal', 'open', explorationId); + 'PublishExplorationModal', + 'open', + explorationId + ); } registerPublishExplorationEvent(explorationId: string): void { this._sendEventToLegacyGoogleAnalytics( - 'PublishExploration', 'click', explorationId); + 'PublishExploration', + 'click', + explorationId + ); } registerVisitOppiaFromIframeEvent(explorationId: string): void { this._sendEventToLegacyGoogleAnalytics( - 'VisitOppiaFromIframe', 'click', explorationId); + 'VisitOppiaFromIframe', + 'click', + explorationId + ); } registerNewCard(cardNum: number, explorationId: string): void { if (cardNum <= 10 || cardNum % 10 === 0) { this._sendEventToLegacyGoogleAnalytics( - 'PlayerNewCard', 'click', cardNum.toString()); - this._sendEventToGoogleAnalytics( - 'new_card_load', - { - exploration_id: explorationId, - card_number: cardNum - } + 'PlayerNewCard', + 'click', + cardNum.toString() ); + this._sendEventToGoogleAnalytics('new_card_load', { + exploration_id: explorationId, + card_number: cardNum, + }); } } registerStartAudioPlayedEvent( - explorationId: string, - cardIndex: number + explorationId: string, + cardIndex: number ): void { - this._sendEventToGoogleAnalytics( - 'audio_played', { - exploration_id: explorationId, - card_number: cardIndex - } - ); + this._sendEventToGoogleAnalytics('audio_played', { + exploration_id: explorationId, + card_number: cardIndex, + }); } registerPracticeSessionStartEvent( - classroomName: string, - topicName: string, - stringifiedSubtopicIds: string + classroomName: string, + topicName: string, + stringifiedSubtopicIds: string ): void { - this._sendEventToGoogleAnalytics( - 'practice_session_start', { - classroom_name: classroomName, - topic_name: topicName, - practice_session_id: stringifiedSubtopicIds - } - ); + this._sendEventToGoogleAnalytics('practice_session_start', { + classroom_name: classroomName, + topic_name: topicName, + practice_session_id: stringifiedSubtopicIds, + }); } registerPracticeSessionEndEvent( - classroomName: string, - topicName: string, - stringifiedSubtopicIds: string, - questionsAnswered: number, - totalScore: number + classroomName: string, + topicName: string, + stringifiedSubtopicIds: string, + questionsAnswered: number, + totalScore: number ): void { - this._sendEventToGoogleAnalytics( - 'practice_session_complete', { - classroom_name: classroomName, - topic_name: topicName, - practice_session_id: stringifiedSubtopicIds, - questions_answered: questionsAnswered, - total_score: totalScore - } - ); + this._sendEventToGoogleAnalytics('practice_session_complete', { + classroom_name: classroomName, + topic_name: topicName, + practice_session_id: stringifiedSubtopicIds, + questions_answered: questionsAnswered, + total_score: totalScore, + }); } registerOpenCollectionFromLandingPageEvent(collectionId: string): void { this._sendEventToLegacyGoogleAnalytics( - 'OpenFractionsFromLandingPage', 'click', collectionId); + 'OpenFractionsFromLandingPage', + 'click', + collectionId + ); } registerSaveRecordedAudioEvent(explorationId: string): void { this._sendEventToLegacyGoogleAnalytics( - 'SaveRecordedAudio', 'click', explorationId); + 'SaveRecordedAudio', + 'click', + explorationId + ); } registerStartAudioRecordingEvent(explorationId: string): void { this._sendEventToLegacyGoogleAnalytics( - 'StartAudioRecording', 'click', explorationId); + 'StartAudioRecording', + 'click', + explorationId + ); } registerUploadAudioEvent(explorationId: string): void { this._sendEventToLegacyGoogleAnalytics( - 'UploadRecordedAudio', 'click', explorationId); + 'UploadRecordedAudio', + 'click', + explorationId + ); } // Contributor Dashboard Events. registerContributorDashboardSuggestEvent(contributionType: string): void { this._sendEventToLegacyGoogleAnalytics( - 'ContributorDashboardSuggest', 'click', contributionType); + 'ContributorDashboardSuggest', + 'click', + contributionType + ); } registerContributorDashboardSubmitSuggestionEvent( - contributionType: string): void { + contributionType: string + ): void { this._sendEventToLegacyGoogleAnalytics( - 'ContributorDashboardSubmitSuggestion', 'click', contributionType); + 'ContributorDashboardSubmitSuggestion', + 'click', + contributionType + ); } registerContributorDashboardViewSuggestionForReview( - contributionType: string): void { + contributionType: string + ): void { this._sendEventToLegacyGoogleAnalytics( 'ContributorDashboardViewSuggestionForReview', 'click', @@ -421,165 +551,166 @@ export class SiteAnalyticsService { ); } - registerContributorDashboardAcceptSuggestion( - contributionType: string - ): void { + registerContributorDashboardAcceptSuggestion(contributionType: string): void { this._sendEventToLegacyGoogleAnalytics( - 'ContributorDashboardAcceptSuggestion', 'click', contributionType); + 'ContributorDashboardAcceptSuggestion', + 'click', + contributionType + ); } - registerContributorDashboardRejectSuggestion( - contributionType: string - ): void { + registerContributorDashboardRejectSuggestion(contributionType: string): void { this._sendEventToLegacyGoogleAnalytics( - 'ContributorDashboardRejectSuggestion', 'click', contributionType); + 'ContributorDashboardRejectSuggestion', + 'click', + contributionType + ); } registerLessonActiveUse(): void { this._sendEventToLegacyGoogleAnalytics( - 'ActiveUserStartAndSawCards', 'engage', ''); + 'ActiveUserStartAndSawCards', + 'engage', + '' + ); } registerStartExploration(explorationId: string): void { this._sendEventToLegacyGoogleAnalytics( - 'PlayerStartExploration', 'engage', explorationId); - this._sendEventToGoogleAnalytics( - 'lesson_started', { - exploration_id: explorationId - } + 'PlayerStartExploration', + 'engage', + explorationId ); + this._sendEventToGoogleAnalytics('lesson_started', { + exploration_id: explorationId, + }); } registerFinishExploration(explorationId: string): void { this._sendEventToLegacyGoogleAnalytics( - 'PlayerFinishExploration', 'engage', explorationId); - this._sendEventToGoogleAnalytics( - 'lesson_completed', { - exploration_id: explorationId - } + 'PlayerFinishExploration', + 'engage', + explorationId ); + this._sendEventToGoogleAnalytics('lesson_completed', { + exploration_id: explorationId, + }); } - registerCuratedLessonStarted( - topicName: string, explorationId: string): void { - this._sendEventToGoogleAnalytics( - 'classroom_lesson_started', { - topic_name: topicName, - exploration_id: explorationId - } - ); + registerCuratedLessonStarted(topicName: string, explorationId: string): void { + this._sendEventToGoogleAnalytics('classroom_lesson_started', { + topic_name: topicName, + exploration_id: explorationId, + }); } registerCuratedLessonCompleted( - classroomName: string, - topicName: string, - chapterName: string, - explorationId: string, - chapterNumber: string, - chapterCardCount: string, - explorationLanguage: string + classroomName: string, + topicName: string, + chapterName: string, + explorationId: string, + chapterNumber: string, + chapterCardCount: string, + explorationLanguage: string ): void { - this._sendEventToGoogleAnalytics( - 'classroom_lesson_completed', { - classroom_name: classroomName, - topic_name: topicName, - chapter_name: chapterName, - exploration_id: explorationId, - chapter_number: chapterNumber, - chapter_card_count: chapterCardCount, - exploration_language: explorationLanguage - } - ); + this._sendEventToGoogleAnalytics('classroom_lesson_completed', { + classroom_name: classroomName, + topic_name: topicName, + chapter_name: chapterName, + exploration_id: explorationId, + chapter_number: chapterNumber, + chapter_card_count: chapterCardCount, + exploration_language: explorationLanguage, + }); } registerCommunityLessonStarted(explorationId: string): void { - this._sendEventToGoogleAnalytics( - 'community_lesson_started', { - exploration_id: explorationId - } - ); + this._sendEventToGoogleAnalytics('community_lesson_started', { + exploration_id: explorationId, + }); } registerCommunityLessonCompleted(explorationId: string): void { - this._sendEventToGoogleAnalytics( - 'community_lesson_completed', { - exploration_id: explorationId - } - ); + this._sendEventToGoogleAnalytics('community_lesson_completed', { + exploration_id: explorationId, + }); } registerClassroomLessonEngagedWithEvent( - classroomName: string, - topicName: string, - chapterName: string, - explorationId: string, - chapterNumber: string, - chapterCardCount: string, - explorationLanguage: string + classroomName: string, + topicName: string, + chapterName: string, + explorationId: string, + chapterNumber: string, + chapterCardCount: string, + explorationLanguage: string ): void { - this._sendEventToGoogleAnalytics( - 'classroom_lesson_engaged_with', { - classroom_name: classroomName, - topic_name: topicName, - chapter_name: chapterName, - exploration_id: explorationId, - chapter_number: chapterNumber, - chapter_card_count: chapterCardCount, - exploration_language: explorationLanguage - } - ); + this._sendEventToGoogleAnalytics('classroom_lesson_engaged_with', { + classroom_name: classroomName, + topic_name: topicName, + chapter_name: chapterName, + exploration_id: explorationId, + chapter_number: chapterNumber, + chapter_card_count: chapterCardCount, + exploration_language: explorationLanguage, + }); } registerCommunityLessonEngagedWithEvent( - explorationId: string, - explorationLanguage: string + explorationId: string, + explorationLanguage: string ): void { - this._sendEventToGoogleAnalytics( - 'community_lesson_engaged_with', { - exploration_id: explorationId, - exploration_language: explorationLanguage - } - ); + this._sendEventToGoogleAnalytics('community_lesson_engaged_with', { + exploration_id: explorationId, + exploration_language: explorationLanguage, + }); } registerLessonEngagedWithEvent( - explorationId: string, - explorationLanguage: string + explorationId: string, + explorationLanguage: string ): void { - this._sendEventToGoogleAnalytics( - 'lesson_engaged_with', { - exploration_id: explorationId, - exploration_language: explorationLanguage - } - ); + this._sendEventToGoogleAnalytics('lesson_engaged_with', { + exploration_id: explorationId, + exploration_language: explorationLanguage, + }); } registerClassroomHeaderClickEvent(): void { this._sendEventToLegacyGoogleAnalytics( - 'ClassroomEngagement', 'click', 'ClickOnClassroom'); + 'ClassroomEngagement', + 'click', + 'ClickOnClassroom' + ); } registerClassroomPageViewed(): void { this._sendEventToLegacyGoogleAnalytics( - 'ClassroomEngagement', 'impression', 'ViewClassroom'); + 'ClassroomEngagement', + 'impression', + 'ViewClassroom' + ); } registerAccountDeletion(): void { this._sendEventToLegacyGoogleAnalytics( - 'OnboardingEngagement', 'delete', 'AccountDeletion'); + 'OnboardingEngagement', + 'delete', + 'AccountDeletion' + ); } registerAnswerSubmitted( - explorationId: string, answerIsCorrect: boolean): void { - this._sendEventToGoogleAnalytics( - 'answer_submitted', { - exploration_id: explorationId, - answer_is_correct: answerIsCorrect, - } - ); + explorationId: string, + answerIsCorrect: boolean + ): void { + this._sendEventToGoogleAnalytics('answer_submitted', { + exploration_id: explorationId, + answer_is_correct: answerIsCorrect, + }); } } -angular.module('oppia').factory( - 'SiteAnalyticsService', - downgradeInjectable(SiteAnalyticsService)); +angular + .module('oppia') + .factory('SiteAnalyticsService', downgradeInjectable(SiteAnalyticsService)); diff --git a/core/templates/services/speech-synthesis-chunker.service.spec.ts b/core/templates/services/speech-synthesis-chunker.service.spec.ts index bf93e7bfd68c..a50e59a9abf5 100644 --- a/core/templates/services/speech-synthesis-chunker.service.spec.ts +++ b/core/templates/services/speech-synthesis-chunker.service.spec.ts @@ -16,147 +16,147 @@ * @fileoverview Unit tests for SpeechSynthesisChunkerService. */ -import { TestBed, fakeAsync, flush } from '@angular/core/testing'; +import {TestBed, fakeAsync, flush} from '@angular/core/testing'; -import { SpeechSynthesisChunkerService } from - 'services/speech-synthesis-chunker.service'; +import {SpeechSynthesisChunkerService} from 'services/speech-synthesis-chunker.service'; describe('Speech Synthesis Chunker Service', () => { let speechSynthesisChunkerService: SpeechSynthesisChunkerService; beforeEach(() => { TestBed.configureTestingModule({ - providers: [SpeechSynthesisChunkerService] + providers: [SpeechSynthesisChunkerService], }); speechSynthesisChunkerService = TestBed.inject( - SpeechSynthesisChunkerService); + SpeechSynthesisChunkerService + ); }); describe('formatLatexToSpeakableText', () => { - it('should properly convert subtraction in LaTeX to speakable text', - () => { - var latex1 = '5 - 3'; - var latex2 = 'i - j'; - var speakableLatex1 = - speechSynthesisChunkerService.formatLatexToSpeakableText(latex1); - var speakableLatex2 = - speechSynthesisChunkerService.formatLatexToSpeakableText(latex2); - expect(speakableLatex1).toEqual('5 minus 3'); - expect(speakableLatex2).toEqual('i minus j'); - } - ); + it('should properly convert subtraction in LaTeX to speakable text', () => { + var latex1 = '5 - 3'; + var latex2 = 'i - j'; + var speakableLatex1 = + speechSynthesisChunkerService.formatLatexToSpeakableText(latex1); + var speakableLatex2 = + speechSynthesisChunkerService.formatLatexToSpeakableText(latex2); + expect(speakableLatex1).toEqual('5 minus 3'); + expect(speakableLatex2).toEqual('i minus j'); + }); - it('should properly convert fractions in LaTeX to speakable text', - () => { - var latex1 = '\\\\frac{2}{3}'; - var latex2 = '\\\\frac{abc}{xyz}'; - var latex3 = '\\\\frac{3n}{5}'; - var latex4 = '\\\\frac{ijk}{5xy}'; - var speakableLatex1 = - speechSynthesisChunkerService.formatLatexToSpeakableText(latex1); - var speakableLatex2 = - speechSynthesisChunkerService.formatLatexToSpeakableText(latex2); - var speakableLatex3 = - speechSynthesisChunkerService.formatLatexToSpeakableText(latex3); - var speakableLatex4 = - speechSynthesisChunkerService.formatLatexToSpeakableText(latex4); - expect(speakableLatex1).toEqual('2/3'); - expect(speakableLatex2).toEqual('a b c over x y z'); - expect(speakableLatex3).toEqual('3n over 5'); - expect(speakableLatex4).toEqual('i j k over 5x y'); - } - ); + it('should properly convert fractions in LaTeX to speakable text', () => { + var latex1 = '\\\\frac{2}{3}'; + var latex2 = '\\\\frac{abc}{xyz}'; + var latex3 = '\\\\frac{3n}{5}'; + var latex4 = '\\\\frac{ijk}{5xy}'; + var speakableLatex1 = + speechSynthesisChunkerService.formatLatexToSpeakableText(latex1); + var speakableLatex2 = + speechSynthesisChunkerService.formatLatexToSpeakableText(latex2); + var speakableLatex3 = + speechSynthesisChunkerService.formatLatexToSpeakableText(latex3); + var speakableLatex4 = + speechSynthesisChunkerService.formatLatexToSpeakableText(latex4); + expect(speakableLatex1).toEqual('2/3'); + expect(speakableLatex2).toEqual('a b c over x y z'); + expect(speakableLatex3).toEqual('3n over 5'); + expect(speakableLatex4).toEqual('i j k over 5x y'); + }); - it('should properly convert square roots in LaTeX to speakable text', - () => { - var latex1 = '\\\\sqrt{3}'; - var latex2 = '\\\\sqrt{xy}'; - var speakableLatex1 = - speechSynthesisChunkerService.formatLatexToSpeakableText(latex1); - var speakableLatex2 = - speechSynthesisChunkerService.formatLatexToSpeakableText(latex2); - expect(speakableLatex1).toEqual('the square root of 3'); - expect(speakableLatex2).toEqual('the square root of x y'); - } - ); + it('should properly convert square roots in LaTeX to speakable text', () => { + var latex1 = '\\\\sqrt{3}'; + var latex2 = '\\\\sqrt{xy}'; + var speakableLatex1 = + speechSynthesisChunkerService.formatLatexToSpeakableText(latex1); + var speakableLatex2 = + speechSynthesisChunkerService.formatLatexToSpeakableText(latex2); + expect(speakableLatex1).toEqual('the square root of 3'); + expect(speakableLatex2).toEqual('the square root of x y'); + }); + + it('should properly convert exponents in LaTeX to speakable text', () => { + var latex1 = 'x ^ 2'; + var latex2 = '42 ^ 4'; + var latex3 = 'x ^ 62'; + var latex4 = '3n ^ 4x'; + var speakableLatex1 = + speechSynthesisChunkerService.formatLatexToSpeakableText(latex1); + var speakableLatex2 = + speechSynthesisChunkerService.formatLatexToSpeakableText(latex2); + var speakableLatex3 = + speechSynthesisChunkerService.formatLatexToSpeakableText(latex3); + var speakableLatex4 = + speechSynthesisChunkerService.formatLatexToSpeakableText(latex4); + expect(speakableLatex1).toEqual('x^2'); + expect(speakableLatex2).toEqual('42 to the power of 4'); + expect(speakableLatex3).toEqual('x to the power of 62'); + expect(speakableLatex4).toEqual('3n to the power of 4x'); + }); - it('should properly convert exponents in LaTeX to speakable text', + it( + 'should properly convert trigonometric functions in LaTeX to ' + + 'speakable text', () => { - var latex1 = 'x ^ 2'; - var latex2 = '42 ^ 4'; - var latex3 = 'x ^ 62'; - var latex4 = '3n ^ 4x'; + var latex1 = '\\\\sin{90}'; + var latex2 = '\\\\cos{0}'; + var latex3 = '\\\\tan{uv}'; var speakableLatex1 = speechSynthesisChunkerService.formatLatexToSpeakableText(latex1); var speakableLatex2 = speechSynthesisChunkerService.formatLatexToSpeakableText(latex2); var speakableLatex3 = speechSynthesisChunkerService.formatLatexToSpeakableText(latex3); - var speakableLatex4 = - speechSynthesisChunkerService.formatLatexToSpeakableText(latex4); - expect(speakableLatex1).toEqual('x^2'); - expect(speakableLatex2).toEqual('42 to the power of 4'); - expect(speakableLatex3).toEqual('x to the power of 62'); - expect(speakableLatex4).toEqual('3n to the power of 4x'); + expect(speakableLatex1).toEqual('the sine of 90'); + expect(speakableLatex2).toEqual('the cosine of 0'); + expect(speakableLatex3).toEqual('the tangent of u v'); } ); - - it('should properly convert trigonometric functions in LaTeX to ' + - 'speakable text', () => { - var latex1 = '\\\\sin{90}'; - var latex2 = '\\\\cos{0}'; - var latex3 = '\\\\tan{uv}'; - var speakableLatex1 = - speechSynthesisChunkerService.formatLatexToSpeakableText(latex1); - var speakableLatex2 = - speechSynthesisChunkerService.formatLatexToSpeakableText(latex2); - var speakableLatex3 = - speechSynthesisChunkerService.formatLatexToSpeakableText(latex3); - expect(speakableLatex1).toEqual('the sine of 90'); - expect(speakableLatex2).toEqual('the cosine of 0'); - expect(speakableLatex3).toEqual('the tangent of u v'); - }); }); describe('convertToSpeakableText', () => { - it('should properly convert the raw_latex-with-value attribute to' + - ' speakable text', () => { - const html = ( - '' + - '
  • Speech
  • ' + - '
  • Text
  • ' - ); + it( + 'should properly convert the raw_latex-with-value attribute to' + + ' speakable text', + () => { + const html = + '' + + '
  • Speech
  • ' + + '
  • Text
  • '; - expect(speechSynthesisChunkerService.convertToSpeakableText(html)) - .toBe('5 minus 1 Speech. Text. '); - }); + expect(speechSynthesisChunkerService.convertToSpeakableText(html)).toBe( + '5 minus 1 Speech. Text. ' + ); + } + ); - it('should properly convert the text-with-value attribute to' + - ' speakable text', () => { - const html = ( - '' + - '' + - '
  • "Speech"
  • ' + - '
  • Text
  • ' - ); + it( + 'should properly convert the text-with-value attribute to' + + ' speakable text', + () => { + const html = + '' + + '' + + '
  • "Speech"
  • ' + + '
  • Text
  • '; - expect(speechSynthesisChunkerService.convertToSpeakableText(html)) - .toBe('Testing Speech. Text. '); - }); + expect(speechSynthesisChunkerService.convertToSpeakableText(html)).toBe( + 'Testing Speech. Text. ' + ); + } + ); }); - describe('speak', function() { - const MockSpeechSynthesisUtteranceConstructor = ( - SpeechSynthesisUtterance); + describe('speak', function () { + const MockSpeechSynthesisUtteranceConstructor = SpeechSynthesisUtterance; const mockSpeechSynthesisUtteran = { speak: () => {}, onend: () => {}, - addEventListener: function(_: string, cb: () => void) { + addEventListener: function (_: string, cb: () => void) { this.onend = cb; - } + }, }; beforeEach(() => { @@ -168,70 +168,81 @@ describe('Speech Synthesis Chunker Service', () => { // only defined the properties we need in 'mockSpeechSynthesisUtteran'. // @ts-expect-error Object.assign({}, mockSpeechSynthesisUtteran), - Object.assign({}, mockSpeechSynthesisUtteran)); + Object.assign({}, mockSpeechSynthesisUtteran) + ); }); it('should not speak when chunk is too short', () => { - const speakSpy = spyOn(window.speechSynthesis, 'speak').and - .callFake(function(utterance) { - // This throws "Argument of type '{ speak: () => void; onend: - // () => void; ...}' is not assignable to parameter of type - // 'SpeechSynthesisUtterance'.". We need to suppress this error because - // 'SpeechSynthesisUtterance' has around 10 more properties. We have - // only defined the properties we need in 'mockSpeechSynthesisUtteran'. - // @ts-expect-error + const speakSpy = spyOn(window.speechSynthesis, 'speak').and.callFake( + function (utterance) { + // This throws "Argument of type '{ speak: () => void; onend: + // () => void; ...}' is not assignable to parameter of type + // 'SpeechSynthesisUtterance'.". We need to suppress this error because + // 'SpeechSynthesisUtterance' has around 10 more properties. We have + // only defined the properties we need in 'mockSpeechSynthesisUtteran'. + // @ts-expect-error utterance.onend(); - }); - const speechSynthesisUtterance = ( - new MockSpeechSynthesisUtteranceConstructor('a')); + } + ); + const speechSynthesisUtterance = + new MockSpeechSynthesisUtteranceConstructor('a'); const callbackSpy = jasmine.createSpy('callback'); speechSynthesisChunkerService.speak( - speechSynthesisUtterance, callbackSpy); + speechSynthesisUtterance, + callbackSpy + ); expect(callbackSpy).toHaveBeenCalled(); expect(speakSpy).not.toHaveBeenCalled(); }); it('should not speak when chunk is a falsy value', () => { - const speakSpy = spyOn(window.speechSynthesis, 'speak').and - .callFake(function(utterance: SpeechSynthesisUtterance) { - // This throws "Argument of type '{ speak: () => void; onend: - // () => void; ...}' is not assignable to parameter of type - // 'SpeechSynthesisUtterance'.". We need to suppress this error because - // 'SpeechSynthesisUtterance' has around 10 more properties. We have - // only defined the properties we need in 'mockSpeechSynthesisUtteran'. - // @ts-expect-error + const speakSpy = spyOn(window.speechSynthesis, 'speak').and.callFake( + function (utterance: SpeechSynthesisUtterance) { + // This throws "Argument of type '{ speak: () => void; onend: + // () => void; ...}' is not assignable to parameter of type + // 'SpeechSynthesisUtterance'.". We need to suppress this error because + // 'SpeechSynthesisUtterance' has around 10 more properties. We have + // only defined the properties we need in 'mockSpeechSynthesisUtteran'. + // @ts-expect-error utterance.onend(); - }); - const speechSynthesisUtterance = ( - new MockSpeechSynthesisUtteranceConstructor('')); + } + ); + const speechSynthesisUtterance = + new MockSpeechSynthesisUtteranceConstructor(''); const callbackSpy = jasmine.createSpy('callback'); speechSynthesisChunkerService.speak( - speechSynthesisUtterance, callbackSpy); + speechSynthesisUtterance, + callbackSpy + ); expect(callbackSpy).toHaveBeenCalled(); expect(speakSpy).not.toHaveBeenCalled(); }); it('should speak two phrases at a time', fakeAsync(() => { - const speakSpy = spyOn(window.speechSynthesis, 'speak').and - .callFake(function(utterance: SpeechSynthesisUtterance) { - // This throws "Argument of type '{ speak: () => void; onend: - // () => void; ...}' is not assignable to parameter of type - // 'SpeechSynthesisUtterance'.". We need to suppress this error because - // 'SpeechSynthesisUtterance' has around 10 more properties. We have - // only defined the properties we need in 'mockSpeechSynthesisUtteran'. - // @ts-expect-error + const speakSpy = spyOn(window.speechSynthesis, 'speak').and.callFake( + function (utterance: SpeechSynthesisUtterance) { + // This throws "Argument of type '{ speak: () => void; onend: + // () => void; ...}' is not assignable to parameter of type + // 'SpeechSynthesisUtterance'.". We need to suppress this error because + // 'SpeechSynthesisUtterance' has around 10 more properties. We have + // only defined the properties we need in 'mockSpeechSynthesisUtteran'. + // @ts-expect-error utterance.onend(); - }); + } + ); - const speechSynthesisUtterance = ( + const speechSynthesisUtterance = new MockSpeechSynthesisUtteranceConstructor( 'Value inside utterance for testing purposes.' + - ' This is the next chunk')); + ' This is the next chunk' + ); const callbackSpy = jasmine.createSpy('callback'); speechSynthesisChunkerService.speak( - speechSynthesisUtterance, callbackSpy); + speechSynthesisUtterance, + callbackSpy + ); // Wait for 2 setTimeout calls to finished because there are // two chuncks in speechSynthesisUtterance. @@ -241,10 +252,9 @@ describe('Speech Synthesis Chunker Service', () => { expect(speakSpy).toHaveBeenCalledTimes(2); })); - it('should speak only one phrase when cancel is requested', - fakeAsync(() => { - const speakSpy = spyOn(window.speechSynthesis, 'speak').and - .callFake(function(utterance: SpeechSynthesisUtterance) { + it('should speak only one phrase when cancel is requested', fakeAsync(() => { + const speakSpy = spyOn(window.speechSynthesis, 'speak').and.callFake( + function (utterance: SpeechSynthesisUtterance) { // This throws "Argument of type '{ speak: () => void; onend: // () => void; ...}' is not assignable to parameter of type // 'SpeechSynthesisUtterance'.". We need to suppress this error @@ -252,23 +262,26 @@ describe('Speech Synthesis Chunker Service', () => { // We have only defined the properties we need in // 'mockSpeechSynthesisUtteran'. // @ts-expect-error - utterance.onend(); - }); - const speechSynthesisUtterance = ( - new MockSpeechSynthesisUtteranceConstructor( - - 'Value inside utterance for testing purposes.' + - ' This is the next chunk')); - const callbackSpy = jasmine.createSpy('callback'); - speechSynthesisChunkerService.speak( - speechSynthesisUtterance, callbackSpy); - speechSynthesisChunkerService.cancel(); + utterance.onend(); + } + ); + const speechSynthesisUtterance = + new MockSpeechSynthesisUtteranceConstructor( + 'Value inside utterance for testing purposes.' + + ' This is the next chunk' + ); + const callbackSpy = jasmine.createSpy('callback'); + speechSynthesisChunkerService.speak( + speechSynthesisUtterance, + callbackSpy + ); + speechSynthesisChunkerService.cancel(); - // Wait for 1 setTimeout call to finished. - flush(1); + // Wait for 1 setTimeout call to finished. + flush(1); - expect(callbackSpy).not.toHaveBeenCalled(); - expect(speakSpy).toHaveBeenCalledTimes(1); - })); + expect(callbackSpy).not.toHaveBeenCalled(); + expect(speakSpy).toHaveBeenCalledTimes(1); + })); }); }); diff --git a/core/templates/services/speech-synthesis-chunker.service.ts b/core/templates/services/speech-synthesis-chunker.service.ts index e0a468a69c97..5b96a427605b 100644 --- a/core/templates/services/speech-synthesis-chunker.service.ts +++ b/core/templates/services/speech-synthesis-chunker.service.ts @@ -23,16 +23,16 @@ * Credits to Peter Woolley and Brett Zamir. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; -import { HtmlEscaperService } from 'services/html-escaper.service'; -import { ServicesConstants } from 'services/services.constants'; +import {HtmlEscaperService} from 'services/html-escaper.service'; +import {ServicesConstants} from 'services/services.constants'; type RTEComponentSpecsKey = keyof typeof ServicesConstants.RTE_COMPONENT_SPECS; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class SpeechSynthesisChunkerService { constructor(private htmlEscaper: HtmlEscaperService) {} @@ -73,25 +73,37 @@ export class SpeechSynthesisChunkerService { * chunked utterance finishes playing. */ _speechUtteranceChunker( - utterance: SpeechSynthesisUtterance, offset: number, - callback: () => void): void { + utterance: SpeechSynthesisUtterance, + offset: number, + callback: () => void + ): void { var newUtterance: SpeechSynthesisUtterance; - var text = ( - offset !== undefined ? utterance.text.substring(offset) : utterance.text); + var text = + offset !== undefined ? utterance.text.substring(offset) : utterance.text; // This regex pattern finds the next string at most 160 // characters in length that ends on a punctuation mark in // PUNCTUATION_MARKS_TO_END_CHUNKS. var delimitChunkRegex = new RegExp( - '^[\\s\\S]{' + Math.floor(this.CHUNK_LENGTH / 4) + ',' + - this.CHUNK_LENGTH + '}[' + this.PUNCTUATION_MARKS_TO_END_CHUNKS + - ']{1}|^[\\s\\S]{1,' + this.CHUNK_LENGTH + '}$|^[\\s\\S]{1,' + - this.CHUNK_LENGTH + '} '); + '^[\\s\\S]{' + + Math.floor(this.CHUNK_LENGTH / 4) + + ',' + + this.CHUNK_LENGTH + + '}[' + + this.PUNCTUATION_MARKS_TO_END_CHUNKS + + ']{1}|^[\\s\\S]{1,' + + this.CHUNK_LENGTH + + '}$|^[\\s\\S]{1,' + + this.CHUNK_LENGTH + + '} ' + ); var chunkArray = text.match(delimitChunkRegex); - if (chunkArray === null || - chunkArray[0] === undefined || - chunkArray[0].length <= 2) { + if ( + chunkArray === null || + chunkArray[0] === undefined || + chunkArray[0].length <= 2 + ) { // Call once all text has been spoken. if (callback !== undefined) { callback(); @@ -106,11 +118,9 @@ export class SpeechSynthesisChunkerService { for (var property in utterance) { const _property = property as keyof SpeechSynthesisUtterance; if (_property !== 'text' && _property !== 'addEventListener') { - Object.defineProperty( - newUtterance, _property, { - value: utterance[_property] - } - ); + Object.defineProperty(newUtterance, _property, { + value: utterance[_property], + }); } } newUtterance.addEventListener('end', () => { @@ -122,7 +132,6 @@ export class SpeechSynthesisChunkerService { this._speechUtteranceChunker(utterance, offset, callback); }); - // IMPORTANT!! Do not remove: Logging the object out fixes some onend // firing issues. Placing the speak invocation inside a callback // fixes ordering and onend issues. @@ -134,91 +143,96 @@ export class SpeechSynthesisChunkerService { } _formatLatexToSpeakableText(latex: string): string { - return latex - .replace(/"/g, '') - .replace(/\\/g, '') - .replace(/\s+/, ' ') - // Separate consecutive characters with spaces so that 'ab' - // is pronounced 'a' followed by 'b'. - .split('').join(' ') - .replace(/\s*(\d+)\s*/g, '$1') - // Replace dashes with 'minus'. - .replace(/-/g, ' minus ') - // Ensure that 'x^2' is pronounced 'x squared' rather than - // 'x caret 2'. - .replace(/\s*\^\s*/g, '^') - // Speak 'x^y' as 'x to the power of y' unless the exponent is two or - // three, in which case Web Speech will read 'squared' and 'cubed' - // respectively. - .replace(/(.*)\^(\{*[0-9].+|[0-14-9]\}*)/g, '$1 to the power of $2') - // Handle simple fractions. - .replace( - /f\sr\sa\sc\s\{\s*(.+)\s*\}\s\{\s*(.+)\s*\}/g, '$1/$2') - // If a fraction contains a variable, then say (numerator) 'over' - // (denominator). - .replace(/(\d*\D+)\/(\d*\D*)|(\d*\D*)\/(\d*\D+)/g, '$1 over $2') - // Handle basic trigonometric functions. - .replace(/t\sa\sn/g, 'the tangent of') - .replace(/s\si\sn/g, 'the sine of') - .replace(/c\so\ss/g, 'the cosine of') - // Handle square roots. - .replace(/s\sq\sr\st\s\{\s*(.+)\s*\}/g, 'the square root of $1') - // Remove brackets. - .replace(/[\}\{]/g, '') - // Replace multiple spaces with single space. - .replace(/\s\s+/g, ' ') - .trim(); + return ( + latex + .replace(/"/g, '') + .replace(/\\/g, '') + .replace(/\s+/, ' ') + // Separate consecutive characters with spaces so that 'ab' + // is pronounced 'a' followed by 'b'. + .split('') + .join(' ') + .replace(/\s*(\d+)\s*/g, '$1') + // Replace dashes with 'minus'. + .replace(/-/g, ' minus ') + // Ensure that 'x^2' is pronounced 'x squared' rather than + // 'x caret 2'. + .replace(/\s*\^\s*/g, '^') + // Speak 'x^y' as 'x to the power of y' unless the exponent is two or + // three, in which case Web Speech will read 'squared' and 'cubed' + // respectively. + .replace(/(.*)\^(\{*[0-9].+|[0-14-9]\}*)/g, '$1 to the power of $2') + // Handle simple fractions. + .replace(/f\sr\sa\sc\s\{\s*(.+)\s*\}\s\{\s*(.+)\s*\}/g, '$1/$2') + // If a fraction contains a variable, then say (numerator) 'over' + // (denominator). + .replace(/(\d*\D+)\/(\d*\D*)|(\d*\D*)\/(\d*\D+)/g, '$1 over $2') + // Handle basic trigonometric functions. + .replace(/t\sa\sn/g, 'the tangent of') + .replace(/s\si\sn/g, 'the sine of') + .replace(/c\so\ss/g, 'the cosine of') + // Handle square roots. + .replace(/s\sq\sr\st\s\{\s*(.+)\s*\}/g, 'the square root of $1') + // Remove brackets. + .replace(/[\}\{]/g, '') + // Replace multiple spaces with single space. + .replace(/\s\s+/g, ' ') + .trim() + ); } _convertToSpeakableText(html: string): string { const rteCompSpecsKeys = Object.keys( - ServicesConstants.RTE_COMPONENT_SPECS) as RTEComponentSpecsKey[]; - rteCompSpecsKeys.forEach( - (componentSpec) => { - this.RTE_COMPONENT_NAMES[componentSpec] = + ServicesConstants.RTE_COMPONENT_SPECS + ) as RTEComponentSpecsKey[]; + rteCompSpecsKeys.forEach(componentSpec => { + this.RTE_COMPONENT_NAMES[componentSpec] = ServicesConstants.RTE_COMPONENT_SPECS[componentSpec].frontend_id; - }); + }); interface MathExpressionContent { - 'raw_latex': string; - 'svg_filename': string; + raw_latex: string; + svg_filename: string; } var elt = $('
    ' + html + '
    '); // Convert links into speakable text by extracting the readable value. - elt.find('oppia-noninteractive-' + this.RTE_COMPONENT_NAMES.Link) - .replaceWith(function() { + elt + .find('oppia-noninteractive-' + this.RTE_COMPONENT_NAMES.Link) + .replaceWith(function () { // TODO(#13015): Remove use of unknown as a type. // Unknown has been used here becuase of use of jQuery. var element = this as unknown as HTMLElement; const _newTextAttr = element.attributes[ - 'text-with-value' as keyof NamedNodeMap] as Attr; + 'text-with-value' as keyof NamedNodeMap + ] as Attr; // 'Node.textContent' only returns 'null' if the Node is a // 'document' or a 'DocType'. '_newTextAttr' is neither. - const newTextContent = _newTextAttr.textContent?.replace( - /"/g, ''); - // Variable newTextContent ends with a " character, so this is being - // ignored in the condition below. - return ( - newTextContent && newTextContent !== '"' ? - newTextContent + ' ' : '' - ); + const newTextContent = _newTextAttr.textContent?.replace(/"/g, ''); + // Variable newTextContent ends with a " character, so this is being + // ignored in the condition below. + return newTextContent && newTextContent !== '"' + ? newTextContent + ' ' + : ''; }); var _this = this; // Convert LaTeX to speakable text. - elt.find('oppia-noninteractive-' + this.RTE_COMPONENT_NAMES.Math) - .replaceWith(function() { + elt + .find('oppia-noninteractive-' + this.RTE_COMPONENT_NAMES.Math) + .replaceWith(function () { // TODO(#13015): Remove use of unknown as a type. // Unknown has been used here becuase of use of jQuery. var element = this as unknown as HTMLElement; const _mathContentAttr = element.attributes[ - 'math_content-with-value' as keyof NamedNodeMap] as Attr; - var mathContent = ( - (_this.htmlEscaper.escapedJsonToObj( - // 'Node.textContent' only returns 'null' if the Node is a - // 'document' or a 'DocType'. '_mathContentAttr' is neither. - _mathContentAttr.textContent as string) as MathExpressionContent)); + 'math_content-with-value' as keyof NamedNodeMap + ] as Attr; + var mathContent = _this.htmlEscaper.escapedJsonToObj( + // 'Node.textContent' only returns 'null' if the Node is a + // 'document' or a 'DocType'. '_mathContentAttr' is neither. + _mathContentAttr.textContent as string + ) as MathExpressionContent; const latexSpeakableText = _this._formatLatexToSpeakableText( - mathContent.raw_latex); + mathContent.raw_latex + ); return latexSpeakableText.length > 0 ? latexSpeakableText + ' ' : ''; }); @@ -238,13 +252,19 @@ export class SpeechSynthesisChunkerService { // pause more naturally. Remove any punctuation marks that have no // effect on speaking. for (var i = 0; i < textToSpeakWithoutPauses.length; i++) { - if (this.PUNCTUATION_MARKS_TO_IGNORE.indexOf( - textToSpeakWithoutPauses.charAt(i)) > -1) { + if ( + this.PUNCTUATION_MARKS_TO_IGNORE.indexOf( + textToSpeakWithoutPauses.charAt(i) + ) > -1 + ) { continue; } textToSpeak += textToSpeakWithoutPauses.charAt(i); - if (this.PUNCTUATION_MARKS_TO_END_CHUNKS.indexOf( - textToSpeakWithoutPauses.charAt(i)) > -1) { + if ( + this.PUNCTUATION_MARKS_TO_END_CHUNKS.indexOf( + textToSpeakWithoutPauses.charAt(i) + ) > -1 + ) { textToSpeak += ' '; } } @@ -272,6 +292,9 @@ export class SpeechSynthesisChunkerService { } } -angular.module('oppia').factory( - 'SpeechSynthesisChunkerService', - downgradeInjectable(SpeechSynthesisChunkerService)); +angular + .module('oppia') + .factory( + 'SpeechSynthesisChunkerService', + downgradeInjectable(SpeechSynthesisChunkerService) + ); diff --git a/core/templates/services/staleness-detection.service.spec.ts b/core/templates/services/staleness-detection.service.spec.ts index 5d1baff2c579..edbe9a5d5b9f 100644 --- a/core/templates/services/staleness-detection.service.spec.ts +++ b/core/templates/services/staleness-detection.service.spec.ts @@ -16,11 +16,11 @@ * @fileoverview Unit tests for staleness detection service. */ -import { TestBed } from '@angular/core/testing'; -import { StalenessDetectionService } from './staleness-detection.service'; -import { EntityEditorBrowserTabsInfoDomainConstants } from 'domain/entity_editor_browser_tabs_info/entity-editor-browser-tabs-info-domain.constants'; -import { LocalStorageService } from './local-storage.service'; -import { EntityEditorBrowserTabsInfo } from 'domain/entity_editor_browser_tabs_info/entity-editor-browser-tabs-info.model'; +import {TestBed} from '@angular/core/testing'; +import {StalenessDetectionService} from './staleness-detection.service'; +import {EntityEditorBrowserTabsInfoDomainConstants} from 'domain/entity_editor_browser_tabs_info/entity-editor-browser-tabs-info-domain.constants'; +import {LocalStorageService} from './local-storage.service'; +import {EntityEditorBrowserTabsInfo} from 'domain/entity_editor_browser_tabs_info/entity-editor-browser-tabs-info.model'; describe('Staleness Detection Service', () => { let stalenessDetectionService: StalenessDetectionService; @@ -33,49 +33,54 @@ describe('Staleness Detection Service', () => { it('should find whether an entity editor tab is stale', () => { localStorageService.updateEntityEditorBrowserTabsInfo( - EntityEditorBrowserTabsInfo.create( - 'topic', 'topic_1', 2, 1, false), - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_TOPIC_EDITOR_BROWSER_TABS - ); - - expect(stalenessDetectionService.isEntityEditorTabStale( - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_TOPIC_EDITOR_BROWSER_TABS, - 'topic_1', 1 - )).toBeTrue(); - expect(stalenessDetectionService.isEntityEditorTabStale( - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_TOPIC_EDITOR_BROWSER_TABS, - 'topic_1', 2 - )).toBeFalse(); - expect(stalenessDetectionService.isEntityEditorTabStale( - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_SKILL_EDITOR_BROWSER_TABS, - 'skill_2', 1 - )).toBeFalse(); - }); - - it('should find whether some other editor tab of the same url has ' + - 'unsaved changes', () => { - localStorageService.updateEntityEditorBrowserTabsInfo( - EntityEditorBrowserTabsInfo.create( - 'skill', 'skill_1', 2, 2, true), - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_SKILL_EDITOR_BROWSER_TABS + EntityEditorBrowserTabsInfo.create('topic', 'topic_1', 2, 1, false), + EntityEditorBrowserTabsInfoDomainConstants.OPENED_TOPIC_EDITOR_BROWSER_TABS ); expect( - stalenessDetectionService - .doesSomeOtherEntityEditorPageHaveUnsavedChanges( - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_SKILL_EDITOR_BROWSER_TABS, 'skill_1' - )).toBeTrue(); + stalenessDetectionService.isEntityEditorTabStale( + EntityEditorBrowserTabsInfoDomainConstants.OPENED_TOPIC_EDITOR_BROWSER_TABS, + 'topic_1', + 1 + ) + ).toBeTrue(); + expect( + stalenessDetectionService.isEntityEditorTabStale( + EntityEditorBrowserTabsInfoDomainConstants.OPENED_TOPIC_EDITOR_BROWSER_TABS, + 'topic_1', + 2 + ) + ).toBeFalse(); expect( - stalenessDetectionService - .doesSomeOtherEntityEditorPageHaveUnsavedChanges( - EntityEditorBrowserTabsInfoDomainConstants - .OPENED_SKILL_EDITOR_BROWSER_TABS, 'skill_2' - )).toBeFalse(); + stalenessDetectionService.isEntityEditorTabStale( + EntityEditorBrowserTabsInfoDomainConstants.OPENED_SKILL_EDITOR_BROWSER_TABS, + 'skill_2', + 1 + ) + ).toBeFalse(); }); + + it( + 'should find whether some other editor tab of the same url has ' + + 'unsaved changes', + () => { + localStorageService.updateEntityEditorBrowserTabsInfo( + EntityEditorBrowserTabsInfo.create('skill', 'skill_1', 2, 2, true), + EntityEditorBrowserTabsInfoDomainConstants.OPENED_SKILL_EDITOR_BROWSER_TABS + ); + + expect( + stalenessDetectionService.doesSomeOtherEntityEditorPageHaveUnsavedChanges( + EntityEditorBrowserTabsInfoDomainConstants.OPENED_SKILL_EDITOR_BROWSER_TABS, + 'skill_1' + ) + ).toBeTrue(); + expect( + stalenessDetectionService.doesSomeOtherEntityEditorPageHaveUnsavedChanges( + EntityEditorBrowserTabsInfoDomainConstants.OPENED_SKILL_EDITOR_BROWSER_TABS, + 'skill_2' + ) + ).toBeFalse(); + } + ); }); diff --git a/core/templates/services/staleness-detection.service.ts b/core/templates/services/staleness-detection.service.ts index 85d91576f930..3669a4937ba5 100644 --- a/core/templates/services/staleness-detection.service.ts +++ b/core/templates/services/staleness-detection.service.ts @@ -22,17 +22,15 @@ * before working further and avoid unnecessary data loss. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; -import { LocalStorageService } from './local-storage.service'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; +import {LocalStorageService} from './local-storage.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class StalenessDetectionService { - constructor( - private localStorageService: LocalStorageService - ) {} + constructor(private localStorageService: LocalStorageService) {} /** * Checks if an entity editor tab with a particular url is stale. @@ -44,12 +42,15 @@ export class StalenessDetectionService { * @returns {boolean} Whether the particular tab is stale. */ isEntityEditorTabStale( - entityEditorBrowserTabsInfoConstant: string, - entityId: string, currentVersion: number + entityEditorBrowserTabsInfoConstant: string, + entityId: string, + currentVersion: number ): boolean { - const entityEditorBrowserTabsInfo = ( + const entityEditorBrowserTabsInfo = this.localStorageService.getEntityEditorBrowserTabsInfo( - entityEditorBrowserTabsInfoConstant, entityId)); + entityEditorBrowserTabsInfoConstant, + entityId + ); if (entityEditorBrowserTabsInfo) { return entityEditorBrowserTabsInfo.getLatestVersion() !== currentVersion; @@ -67,12 +68,14 @@ export class StalenessDetectionService { * has some unsaved changes on them. */ doesSomeOtherEntityEditorPageHaveUnsavedChanges( - entityEditorBrowserTabsInfoConstant: string, - entityId: string + entityEditorBrowserTabsInfoConstant: string, + entityId: string ): boolean { - const entityEditorBrowserTabsInfo = ( + const entityEditorBrowserTabsInfo = this.localStorageService.getEntityEditorBrowserTabsInfo( - entityEditorBrowserTabsInfoConstant, entityId)); + entityEditorBrowserTabsInfoConstant, + entityId + ); if (entityEditorBrowserTabsInfo) { return entityEditorBrowserTabsInfo.doesSomeTabHaveUnsavedChanges(); @@ -81,6 +84,9 @@ export class StalenessDetectionService { } } -angular.module('oppia').factory( - 'StalenessDetectionService', - downgradeInjectable(StalenessDetectionService)); +angular + .module('oppia') + .factory( + 'StalenessDetectionService', + downgradeInjectable(StalenessDetectionService) + ); diff --git a/core/templates/services/state-interaction-stats.service.spec.ts b/core/templates/services/state-interaction-stats.service.spec.ts index b1aa69a2b2e5..8125f2b8f48a 100644 --- a/core/templates/services/state-interaction-stats.service.spec.ts +++ b/core/templates/services/state-interaction-stats.service.spec.ts @@ -16,20 +16,27 @@ * @fileoverview Unit tests for state interaction stats service. */ -import { TestBed, flushMicrotasks, fakeAsync, tick } from '@angular/core/testing'; -import { HttpClientTestingModule, HttpTestingController } from - '@angular/common/http/testing'; - -import { NormalizeWhitespacePipe } from - 'filters/string-utility-filters/normalize-whitespace.pipe'; -import { NormalizeWhitespacePunctuationAndCasePipe } from +import {TestBed, flushMicrotasks, fakeAsync, tick} from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; + +import {NormalizeWhitespacePipe} from 'filters/string-utility-filters/normalize-whitespace.pipe'; +import { + NormalizeWhitespacePunctuationAndCasePipe, // eslint-disable-next-line max-len - 'filters/string-utility-filters/normalize-whitespace-punctuation-and-case.pipe'; -import { StateInteractionStats, StateInteractionStatsService } from - 'services/state-interaction-stats.service'; -import { SubtitledHtml } from 'domain/exploration/subtitled-html.model'; -import { State, StateBackendDict, StateObjectFactory } from - 'domain/state/StateObjectFactory'; +} from 'filters/string-utility-filters/normalize-whitespace-punctuation-and-case.pipe'; +import { + StateInteractionStats, + StateInteractionStatsService, +} from 'services/state-interaction-stats.service'; +import {SubtitledHtml} from 'domain/exploration/subtitled-html.model'; +import { + State, + StateBackendDict, + StateObjectFactory, +} from 'domain/state/StateObjectFactory'; const joC = jasmine.objectContaining; @@ -40,19 +47,16 @@ describe('State Interaction Stats Service', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - ], + imports: [HttpClientTestingModule], providers: [ NormalizeWhitespacePipe, - NormalizeWhitespacePunctuationAndCasePipe + NormalizeWhitespacePunctuationAndCasePipe, ], }); stateObjectFactory = TestBed.get(StateObjectFactory); httpTestingController = TestBed.get(HttpTestingController); - stateInteractionStatsService = ( - TestBed.get(StateInteractionStatsService)); + stateInteractionStatsService = TestBed.get(StateInteractionStatsService); }); afterEach(() => httpTestingController.verify()); @@ -65,21 +69,25 @@ describe('State Interaction Stats Service', () => { classifier_model_id: 'model_id', content: { content_id: 'content', - html: 'content' + html: 'content', }, recorded_voiceovers: { - voiceovers_mapping: {} + voiceovers_mapping: {}, }, interaction: { answer_groups: [ { - rule_specs: [{ - rule_type: 'Equals', - inputs: {x: { - contentId: 'rule_input', - normalizedStrSet: ['hola!'] - }} - }], + rule_specs: [ + { + rule_type: 'Equals', + inputs: { + x: { + contentId: 'rule_input', + normalizedStrSet: ['hola!'], + }, + }, + }, + ], outcome: { dest: 'Me Llamo', dest_if_really_stuck: null, @@ -93,13 +101,17 @@ describe('State Interaction Stats Service', () => { tagged_skill_misconception_id: null, }, { - rule_specs: [{ - rule_type: 'Contains', - inputs: {x: { - contentId: 'rule_input', - normalizedStrSet: ['hola'] - }} - }], + rule_specs: [ + { + rule_type: 'Contains', + inputs: { + x: { + contentId: 'rule_input', + normalizedStrSet: ['hola'], + }, + }, + }, + ], outcome: { dest: 'Me Llamo', dest_if_really_stuck: null, @@ -113,13 +125,17 @@ describe('State Interaction Stats Service', () => { tagged_skill_misconception_id: null, }, { - rule_specs: [{ - rule_type: 'FuzzyEquals', - inputs: {x: { - contentId: 'rule_input', - normalizedStrSet: ['hola'] - }} - }], + rule_specs: [ + { + rule_type: 'FuzzyEquals', + inputs: { + x: { + contentId: 'rule_input', + normalizedStrSet: ['hola'], + }, + }, + }, + ], outcome: { dest: 'Me Llamo', dest_if_really_stuck: null, @@ -131,20 +147,20 @@ describe('State Interaction Stats Service', () => { }, training_data: [], tagged_skill_misconception_id: null, - } + }, ], confirmed_unclassified_answers: [], customization_args: { placeholder: { value: { content_id: 'ca_placeholder_0', - unicode_str: '' - } + unicode_str: '', + }, }, - rows: { value: 1 }, + rows: {value: 1}, catchMisspellings: { - value: false - } + value: false, + }, }, default_outcome: { dest: 'Hola', @@ -162,9 +178,9 @@ describe('State Interaction Stats Service', () => { correct_answer: '', explanation: { content_id: '', - html: '' - } - } + html: '', + }, + }, }, param_changes: [], solicit_answer_details: false, @@ -181,25 +197,23 @@ describe('State Interaction Stats Service', () => { ).toBeTrue(); }); - it('should throw error if state name does not exist', - fakeAsync(async() => { - mockState.name = null; + it('should throw error if state name does not exist', fakeAsync(async () => { + mockState.name = null; - expect(() => { - stateInteractionStatsService.computeStatsAsync(expId, mockState); - tick(); - }).toThrowError(); - })); + expect(() => { + stateInteractionStatsService.computeStatsAsync(expId, mockState); + tick(); + }).toThrowError(); + })); - it('should throw error if interaction id does not exist', - fakeAsync(async() => { - mockState.interaction.id = null; + it('should throw error if interaction id does not exist', fakeAsync(async () => { + mockState.interaction.id = null; - expect(() => { - stateInteractionStatsService.computeStatsAsync(expId, mockState); - tick(); - }).toThrowError(); - })); + expect(() => { + stateInteractionStatsService.computeStatsAsync(expId, mockState); + tick(); + }).toThrowError(); + })); describe('when gathering stats from the backend', () => { it('should provide cached results after first call', fakeAsync(() => { @@ -209,26 +223,32 @@ describe('State Interaction Stats Service', () => { statsCaptured.push(stats); }; - stateInteractionStatsService.computeStatsAsync(expId, mockState) + stateInteractionStatsService + .computeStatsAsync(expId, mockState) .then(captureStats); const req = httpTestingController.expectOne( - '/createhandler/state_interaction_stats/expid/Hola'); + '/createhandler/state_interaction_stats/expid/Hola' + ); expect(req.request.method).toEqual('GET'); req.flush({ - visualizations_info: [{ - data: [ - {answer: 'Ni Hao', frequency: 5}, - {answer: 'Aloha', frequency: 3}, - {answer: 'Hola', frequency: 1} - ] - }] + visualizations_info: [ + { + data: [ + {answer: 'Ni Hao', frequency: 5}, + {answer: 'Aloha', frequency: 3}, + {answer: 'Hola', frequency: 1}, + ], + }, + ], }); flushMicrotasks(); - stateInteractionStatsService.computeStatsAsync(expId, mockState) + stateInteractionStatsService + .computeStatsAsync(expId, mockState) .then(captureStats); httpTestingController.expectNone( - '/createhandler/state_interaction_stats/expid/Hola'); + '/createhandler/state_interaction_stats/expid/Hola' + ); flushMicrotasks(); expect(statsCaptured.length).toEqual(2); @@ -243,36 +263,44 @@ describe('State Interaction Stats Service', () => { statsCaptured.push(stats); }; - stateInteractionStatsService.computeStatsAsync(expId, mockState) + stateInteractionStatsService + .computeStatsAsync(expId, mockState) .then(captureStats); const holaReq = httpTestingController.expectOne( - '/createhandler/state_interaction_stats/expid/Hola'); + '/createhandler/state_interaction_stats/expid/Hola' + ); expect(holaReq.request.method).toEqual('GET'); holaReq.flush({ - visualizations_info: [{ - data: [ - {answer: 'Ni Hao', frequency: 5}, - {answer: 'Aloha', frequency: 3}, - {answer: 'Hola', frequency: 1} - ] - }] + visualizations_info: [ + { + data: [ + {answer: 'Ni Hao', frequency: 5}, + {answer: 'Aloha', frequency: 3}, + {answer: 'Hola', frequency: 1}, + ], + }, + ], }); flushMicrotasks(); mockState.name = 'Adios'; - stateInteractionStatsService.computeStatsAsync(expId, mockState) + stateInteractionStatsService + .computeStatsAsync(expId, mockState) .then(captureStats); const adiosReq = httpTestingController.expectOne( - '/createhandler/state_interaction_stats/expid/Adios'); + '/createhandler/state_interaction_stats/expid/Adios' + ); expect(adiosReq.request.method).toEqual('GET'); adiosReq.flush({ - visualizations_info: [{ - data: [ - {answer: 'Zai Jian', frequency: 5}, - {answer: 'Aloha', frequency: 3}, - {answer: 'Adios', frequency: 1} - ] - }] + visualizations_info: [ + { + data: [ + {answer: 'Zai Jian', frequency: 5}, + {answer: 'Aloha', frequency: 3}, + {answer: 'Adios', frequency: 1}, + ], + }, + ], }); flushMicrotasks(); @@ -285,66 +313,80 @@ describe('State Interaction Stats Service', () => { const onSuccess = jasmine.createSpy('success'); const onFailure = jasmine.createSpy('failure'); - stateInteractionStatsService.computeStatsAsync(expId, mockState) + stateInteractionStatsService + .computeStatsAsync(expId, mockState) .then(onSuccess, onFailure); const req = httpTestingController.expectOne( - '/createhandler/state_interaction_stats/expid/Hola'); + '/createhandler/state_interaction_stats/expid/Hola' + ); expect(req.request.method).toEqual('GET'); req.flush({ - visualizations_info: [{ - data: [ - {answer: 'Ni Hao', frequency: 5}, - {answer: 'Aloha', frequency: 3}, - {answer: 'Hola', frequency: 1} - ] - }] + visualizations_info: [ + { + data: [ + {answer: 'Ni Hao', frequency: 5}, + {answer: 'Aloha', frequency: 3}, + {answer: 'Hola', frequency: 1}, + ], + }, + ], }); flushMicrotasks(); - expect(onSuccess).toHaveBeenCalledWith(joC({ - visualizationsInfo: [joC({ - data: [ - joC({answer: 'Ni Hao', frequency: 5}), - joC({answer: 'Aloha', frequency: 3}), - joC({answer: 'Hola', frequency: 1}) - ] - })] - })); + expect(onSuccess).toHaveBeenCalledWith( + joC({ + visualizationsInfo: [ + joC({ + data: [ + joC({answer: 'Ni Hao', frequency: 5}), + joC({answer: 'Aloha', frequency: 3}), + joC({answer: 'Hola', frequency: 1}), + ], + }), + ], + }) + ); expect(onFailure).not.toHaveBeenCalled(); })); - it( - 'should determine whether TextInput answers are addressed explicitly', - fakeAsync(() => { - const onSuccess = jasmine.createSpy('success'); - const onFailure = jasmine.createSpy('failure'); + it('should determine whether TextInput answers are addressed explicitly', fakeAsync(() => { + const onSuccess = jasmine.createSpy('success'); + const onFailure = jasmine.createSpy('failure'); - stateInteractionStatsService.computeStatsAsync(expId, mockState).then( - onSuccess, onFailure); + stateInteractionStatsService + .computeStatsAsync(expId, mockState) + .then(onSuccess, onFailure); - const req = httpTestingController.expectOne( - '/createhandler/state_interaction_stats/expid/Hola'); - expect(req.request.method).toEqual('GET'); - req.flush({ - visualizations_info: [{ + const req = httpTestingController.expectOne( + '/createhandler/state_interaction_stats/expid/Hola' + ); + expect(req.request.method).toEqual('GET'); + req.flush({ + visualizations_info: [ + { data: [{answer: 'Ni Hao'}, {answer: 'Aloha'}, {answer: 'Hola'}], - addressed_info_is_supported: true - }] - }); - flushMicrotasks(); + addressed_info_is_supported: true, + }, + ], + }); + flushMicrotasks(); - expect(onSuccess).toHaveBeenCalledWith(joC({ - visualizationsInfo: [joC({ - data: [ - joC({answer: 'Ni Hao', isAddressed: false}), - joC({answer: 'Aloha', isAddressed: false}), - joC({answer: 'Hola', isAddressed: true}) - ] - })] - })); - expect(onFailure).not.toHaveBeenCalled(); - })); + expect(onSuccess).toHaveBeenCalledWith( + joC({ + visualizationsInfo: [ + joC({ + data: [ + joC({answer: 'Ni Hao', isAddressed: false}), + joC({answer: 'Aloha', isAddressed: false}), + joC({answer: 'Hola', isAddressed: true}), + ], + }), + ], + }) + ); + expect(onFailure).not.toHaveBeenCalled(); + })); it('should return content of MultipleChoiceInput answers', fakeAsync(() => { const onSuccess = jasmine.createSpy('success'); @@ -356,92 +398,101 @@ describe('State Interaction Stats Service', () => { choices: { value: [ new SubtitledHtml('

    foo

    ', ''), - new SubtitledHtml('

    bar

    ', '') - ] - } + new SubtitledHtml('

    bar

    ', ''), + ], + }, }; - stateInteractionStatsService.computeStatsAsync(expId, mockState).then( - onSuccess, onFailure); + stateInteractionStatsService + .computeStatsAsync(expId, mockState) + .then(onSuccess, onFailure); const req = httpTestingController.expectOne( - '/createhandler/state_interaction_stats/expid/Fraction'); + '/createhandler/state_interaction_stats/expid/Fraction' + ); expect(req.request.method).toEqual('GET'); req.flush({ - visualizations_info: [{ - data: [{answer: 0, frequency: 3}, {answer: 1, frequency: 5}], - }] + visualizations_info: [ + { + data: [ + {answer: 0, frequency: 3}, + {answer: 1, frequency: 5}, + ], + }, + ], }); flushMicrotasks(); - expect(onSuccess).toHaveBeenCalledWith(joC({ - visualizationsInfo: [joC({ - data: [ - joC({answer: '

    foo

    '}), - joC({answer: '

    bar

    '}), - ] - })] - })); + expect(onSuccess).toHaveBeenCalledWith( + joC({ + visualizationsInfo: [ + joC({ + data: [joC({answer: '

    foo

    '}), joC({answer: '

    bar

    '})], + }), + ], + }) + ); })); - it( - 'should return FractionInput answers as readable strings', - fakeAsync(() => { - const onSuccess = jasmine.createSpy('success'); - const onFailure = jasmine.createSpy('failure'); - - mockState.name = 'Fraction'; - mockState.interaction.id = 'FractionInput'; - mockState.interaction.customizationArgs = { - choices: { - value: [ - new SubtitledHtml('

    foo

    ', ''), - new SubtitledHtml('

    bar

    ', '') - ] - } - }; - - stateInteractionStatsService.computeStatsAsync(expId, mockState).then( - onSuccess, onFailure); - - const req = httpTestingController.expectOne( - '/createhandler/state_interaction_stats/expid/Fraction'); - expect(req.request.method).toEqual('GET'); - req.flush({ - visualizations_info: [ - { - data: [ - { - answer: { - isNegative: false, - wholeNumber: 0, - numerator: 1, - denominator: 2 - }, - frequency: 3 - }, - { - answer: { - isNegative: false, - wholeNumber: 0, - numerator: 0, - denominator: 1 - }, - frequency: 5 - } - ] - } - ] - }); - flushMicrotasks(); - - expect(onSuccess).toHaveBeenCalledWith(joC({ - visualizationsInfo: [joC({ + it('should return FractionInput answers as readable strings', fakeAsync(() => { + const onSuccess = jasmine.createSpy('success'); + const onFailure = jasmine.createSpy('failure'); + + mockState.name = 'Fraction'; + mockState.interaction.id = 'FractionInput'; + mockState.interaction.customizationArgs = { + choices: { + value: [ + new SubtitledHtml('

    foo

    ', ''), + new SubtitledHtml('

    bar

    ', ''), + ], + }, + }; + + stateInteractionStatsService + .computeStatsAsync(expId, mockState) + .then(onSuccess, onFailure); + + const req = httpTestingController.expectOne( + '/createhandler/state_interaction_stats/expid/Fraction' + ); + expect(req.request.method).toEqual('GET'); + req.flush({ + visualizations_info: [ + { data: [ - joC({ answer: '1/2' }), - joC({ answer: '0' }) - ] - })] - })); - })); + { + answer: { + isNegative: false, + wholeNumber: 0, + numerator: 1, + denominator: 2, + }, + frequency: 3, + }, + { + answer: { + isNegative: false, + wholeNumber: 0, + numerator: 0, + denominator: 1, + }, + frequency: 5, + }, + ], + }, + ], + }); + flushMicrotasks(); + + expect(onSuccess).toHaveBeenCalledWith( + joC({ + visualizationsInfo: [ + joC({ + data: [joC({answer: '1/2'}), joC({answer: '0'})], + }), + ], + }) + ); + })); }); }); diff --git a/core/templates/services/state-interaction-stats.service.ts b/core/templates/services/state-interaction-stats.service.ts index dddca5fe6e56..3832fed934eb 100644 --- a/core/templates/services/state-interaction-stats.service.ts +++ b/core/templates/services/state-interaction-stats.service.ts @@ -16,21 +16,20 @@ * @fileoverview Factory for calculating the statistics of a particular state. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { AnswerClassificationService } from - 'pages/exploration-player-page/services/answer-classification.service'; -import { Fraction } from 'domain/objects/fraction.model'; -import { InteractionAnswer, FractionAnswer, MultipleChoiceAnswer } from - 'interactions/answer-defs'; -import { MultipleChoiceInputCustomizationArgs } from - 'extensions/interactions/customization-args-defs'; -import { InteractionRulesRegistryService } from - 'services/interaction-rules-registry.service'; -import { State } from 'domain/state/StateObjectFactory'; -import { StateInteractionStatsBackendApiService } from - 'domain/exploration/state-interaction-stats-backend-api.service'; +import {AnswerClassificationService} from 'pages/exploration-player-page/services/answer-classification.service'; +import {Fraction} from 'domain/objects/fraction.model'; +import { + InteractionAnswer, + FractionAnswer, + MultipleChoiceAnswer, +} from 'interactions/answer-defs'; +import {MultipleChoiceInputCustomizationArgs} from 'extensions/interactions/customization-args-defs'; +import {InteractionRulesRegistryService} from 'services/interaction-rules-registry.service'; +import {State} from 'domain/state/StateObjectFactory'; +import {StateInteractionStatsBackendApiService} from 'domain/exploration/state-interaction-stats-backend-api.service'; type Option = string | string[]; @@ -61,10 +60,10 @@ export class StateInteractionStatsService { statsCache: Map> = new Map(); constructor( - private answerClassificationService: AnswerClassificationService, - private interactionRulesRegistryService: InteractionRulesRegistryService, - private stateInteractionStatsBackendApiService: - StateInteractionStatsBackendApiService) {} + private answerClassificationService: AnswerClassificationService, + private interactionRulesRegistryService: InteractionRulesRegistryService, + private stateInteractionStatsBackendApiService: StateInteractionStatsBackendApiService + ) {} /** * Returns whether given state has an implementation for displaying the @@ -76,15 +75,16 @@ export class StateInteractionStatsService { // Converts answer to a more-readable representation based on its type. private getReadableAnswerString( - state: State, answer: InteractionAnswer): InteractionAnswer { + state: State, + answer: InteractionAnswer + ): InteractionAnswer { if (state.interaction.id === 'FractionInput') { return Fraction.fromDict(answer as FractionAnswer).toString(); } else if (state.interaction.id === 'MultipleChoiceInput') { - const customizationArgs = ( - state.interaction.customizationArgs - ) as MultipleChoiceInputCustomizationArgs; - return customizationArgs.choices.value[ - answer as MultipleChoiceAnswer].html; + const customizationArgs = state.interaction + .customizationArgs as MultipleChoiceInputCustomizationArgs; + return customizationArgs.choices.value[answer as MultipleChoiceAnswer] + .html; } return answer; } @@ -94,7 +94,9 @@ export class StateInteractionStatsService { * answer-statistics. */ async computeStatsAsync( - expId: string, state: State): Promise { + expId: string, + state: State + ): Promise { const stateName = state.name; if (stateName === null) { throw new Error('State name cannot be null.'); @@ -107,36 +109,52 @@ export class StateInteractionStatsService { if (!interactionId) { throw new Error('Cannot compute stats for a state with no interaction.'); } - const interactionRulesService = ( + const interactionRulesService = this.interactionRulesRegistryService.getRulesServiceByInteractionId( - interactionId)); - const statsPromise = ( - this.stateInteractionStatsBackendApiService.getStatsAsync( - expId, - stateName - )).then(vizInfo => ({ - explorationId: expId, - stateName: stateName, - visualizationsInfo: vizInfo.map(info => ({ - addressedInfoIsSupported: info.addressedInfoIsSupported, - data: info.data.map(datum => ({ - answer: this.getReadableAnswerString(state, datum.answer), - frequency: datum.frequency, - isAddressed: ( - info.addressedInfoIsSupported ? - this.answerClassificationService - .isClassifiedExplicitlyOrGoesToNewState( - stateName, state, datum.answer, interactionRulesService) : - undefined) - }) as AnswerData), - id: info.id, - options: info.options - }) as VisualizationInfo), - }) as StateInteractionStats); + interactionId + ); + const statsPromise = this.stateInteractionStatsBackendApiService + .getStatsAsync(expId, stateName) + .then( + vizInfo => + ({ + explorationId: expId, + stateName: stateName, + visualizationsInfo: vizInfo.map( + info => + ({ + addressedInfoIsSupported: info.addressedInfoIsSupported, + data: info.data.map( + datum => + ({ + answer: this.getReadableAnswerString( + state, + datum.answer + ), + frequency: datum.frequency, + isAddressed: info.addressedInfoIsSupported + ? this.answerClassificationService.isClassifiedExplicitlyOrGoesToNewState( + stateName, + state, + datum.answer, + interactionRulesService + ) + : undefined, + }) as AnswerData + ), + id: info.id, + options: info.options, + }) as VisualizationInfo + ), + }) as StateInteractionStats + ); this.statsCache.set(stateName, statsPromise); return statsPromise; } } -angular.module('oppia').factory( - 'StateInteractionStatsService', - downgradeInjectable(StateInteractionStatsService)); +angular + .module('oppia') + .factory( + 'StateInteractionStatsService', + downgradeInjectable(StateInteractionStatsService) + ); diff --git a/core/templates/services/state-top-answers-stats-backend-api.service.spec.ts b/core/templates/services/state-top-answers-stats-backend-api.service.spec.ts index 309a5791983f..48f37dc15691 100644 --- a/core/templates/services/state-top-answers-stats-backend-api.service.spec.ts +++ b/core/templates/services/state-top-answers-stats-backend-api.service.spec.ts @@ -16,18 +16,17 @@ * @fileoverview Unit tests for StateTopAnswersStatsBackendApiService. */ +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; -import { HttpClientTestingModule, HttpTestingController } from - '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; - -import { StateTopAnswersStatsBackendApiService } from - 'services/state-top-answers-stats-backend-api.service'; +import {StateTopAnswersStatsBackendApiService} from 'services/state-top-answers-stats-backend-api.service'; describe('StateTopAnswersStatsBackendApiService', () => { let httpTestingController: HttpTestingController; - let stateTopAnswersStatsBackendApiService: - StateTopAnswersStatsBackendApiService; + let stateTopAnswersStatsBackendApiService: StateTopAnswersStatsBackendApiService; var ERROR_STATUS_CODE = 500; @@ -37,7 +36,7 @@ describe('StateTopAnswersStatsBackendApiService', () => { {answer: 'hola', frequency: 7}, {answer: 'adios', frequency: 5}, {answer: 'que?', frequency: 2}, - ] + ], }, interaction_ids: {Hola: 'TextInput'}, }; @@ -45,10 +44,11 @@ describe('StateTopAnswersStatsBackendApiService', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [StateTopAnswersStatsBackendApiService] + providers: [StateTopAnswersStatsBackendApiService], }); stateTopAnswersStatsBackendApiService = TestBed.get( - StateTopAnswersStatsBackendApiService); + StateTopAnswersStatsBackendApiService + ); httpTestingController = TestBed.get(HttpTestingController); }); @@ -56,45 +56,46 @@ describe('StateTopAnswersStatsBackendApiService', () => { httpTestingController.verify(); }); - it('should successfully fetch data from the backend', - fakeAsync(() => { - var successHandler = jasmine.createSpy('success'); - var failHandler = jasmine.createSpy('fail'); - - stateTopAnswersStatsBackendApiService.fetchStatsAsync('7') - .then(successHandler, failHandler); - - var req = httpTestingController.expectOne( - '/createhandler/state_answer_stats/7'); - expect(req.request.method).toEqual('GET'); - req.flush(sampleDataResults); - - flushMicrotasks(); - - expect(successHandler).toHaveBeenCalled(); - expect(failHandler).not.toHaveBeenCalled(); - }) - ); - - it('should use rejection handler if data backend request failed', - fakeAsync(() => { - var successHandler = jasmine.createSpy('success'); - var failHandler = jasmine.createSpy('fail'); - - stateTopAnswersStatsBackendApiService.fetchStatsAsync('7') - .then(successHandler, failHandler); - - var req = httpTestingController.expectOne( - '/createhandler/state_answer_stats/7'); - expect(req.request.method).toEqual('GET'); - req.flush('Error loading data.', { - status: ERROR_STATUS_CODE, statusText: 'Invalid Request' - }); + it('should successfully fetch data from the backend', fakeAsync(() => { + var successHandler = jasmine.createSpy('success'); + var failHandler = jasmine.createSpy('fail'); + + stateTopAnswersStatsBackendApiService + .fetchStatsAsync('7') + .then(successHandler, failHandler); + + var req = httpTestingController.expectOne( + '/createhandler/state_answer_stats/7' + ); + expect(req.request.method).toEqual('GET'); + req.flush(sampleDataResults); + + flushMicrotasks(); + + expect(successHandler).toHaveBeenCalled(); + expect(failHandler).not.toHaveBeenCalled(); + })); + + it('should use rejection handler if data backend request failed', fakeAsync(() => { + var successHandler = jasmine.createSpy('success'); + var failHandler = jasmine.createSpy('fail'); + + stateTopAnswersStatsBackendApiService + .fetchStatsAsync('7') + .then(successHandler, failHandler); + + var req = httpTestingController.expectOne( + '/createhandler/state_answer_stats/7' + ); + expect(req.request.method).toEqual('GET'); + req.flush('Error loading data.', { + status: ERROR_STATUS_CODE, + statusText: 'Invalid Request', + }); - flushMicrotasks(); + flushMicrotasks(); - expect(successHandler).not.toHaveBeenCalled(); - expect(failHandler).toHaveBeenCalled(); - }) - ); + expect(successHandler).not.toHaveBeenCalled(); + expect(failHandler).toHaveBeenCalled(); + })); }); diff --git a/core/templates/services/state-top-answers-stats-backend-api.service.ts b/core/templates/services/state-top-answers-stats-backend-api.service.ts index 2cc275a58189..c4e0b514eb29 100644 --- a/core/templates/services/state-top-answers-stats-backend-api.service.ts +++ b/core/templates/services/state-top-answers-stats-backend-api.service.ts @@ -16,39 +16,47 @@ * @fileoverview Service to fetch statistics about an exploration's states. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; -import { ServicesConstants } from 'services/services.constants'; +import {ServicesConstants} from 'services/services.constants'; import { StateTopAnswersStats, StateTopAnswersStatsBackendDict, - StateTopAnswersStatsObjectFactory + StateTopAnswersStatsObjectFactory, } from 'domain/statistics/state-top-answers-stats-object.factory'; -import { UrlInterpolationService } from - 'domain/utilities/url-interpolation.service'; +import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; @Injectable({providedIn: 'root'}) export class StateTopAnswersStatsBackendApiService { constructor( - private http: HttpClient, - private stateTopAnswersStatsObjectFactory: - StateTopAnswersStatsObjectFactory, - private urlInterpolationService: UrlInterpolationService) {} + private http: HttpClient, + private stateTopAnswersStatsObjectFactory: StateTopAnswersStatsObjectFactory, + private urlInterpolationService: UrlInterpolationService + ) {} async fetchStatsAsync(expId: string): Promise { - return this.http.get( - this.urlInterpolationService.interpolateUrl( - ServicesConstants.STATE_ANSWER_STATS_URL, {exploration_id: expId})) - .toPromise().then( + return this.http + .get( + this.urlInterpolationService.interpolateUrl( + ServicesConstants.STATE_ANSWER_STATS_URL, + {exploration_id: expId} + ) + ) + .toPromise() + .then( d => this.stateTopAnswersStatsObjectFactory.createFromBackendDict(d), errorResponse => { throw new Error(errorResponse.error.error); - }); + } + ); } } -angular.module('oppia').factory( - 'StateTopAnswersStatsBackendApiService', - downgradeInjectable(StateTopAnswersStatsBackendApiService)); +angular + .module('oppia') + .factory( + 'StateTopAnswersStatsBackendApiService', + downgradeInjectable(StateTopAnswersStatsBackendApiService) + ); diff --git a/core/templates/services/state-top-answers-stats.service.spec.ts b/core/templates/services/state-top-answers-stats.service.spec.ts index 1d678c84f57f..1f55eda2969c 100644 --- a/core/templates/services/state-top-answers-stats.service.spec.ts +++ b/core/templates/services/state-top-answers-stats.service.spec.ts @@ -17,37 +17,34 @@ * statistics for a particular state. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; - -import { AnswerStats } from - 'domain/exploration/answer-stats.model'; -import { AnswerStatsBackendDict } from - 'domain/exploration/visualization-info.model'; -import { StateBackendDict } from 'domain/state/StateObjectFactory'; -import { Rule } from 'domain/exploration/rule.model'; -import { StateTopAnswersStats } from - 'domain/statistics/state-top-answers-stats-object.factory'; -import { StateTopAnswersStatsService } from - 'services/state-top-answers-stats.service'; -import { StateTopAnswersStatsBackendApiService } from - 'services/state-top-answers-stats-backend-api.service'; -import { States, StatesObjectFactory } from - 'domain/exploration/StatesObjectFactory'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; + +import {AnswerStats} from 'domain/exploration/answer-stats.model'; +import {AnswerStatsBackendDict} from 'domain/exploration/visualization-info.model'; +import {StateBackendDict} from 'domain/state/StateObjectFactory'; +import {Rule} from 'domain/exploration/rule.model'; +import {StateTopAnswersStats} from 'domain/statistics/state-top-answers-stats-object.factory'; +import {StateTopAnswersStatsService} from 'services/state-top-answers-stats.service'; +import {StateTopAnswersStatsBackendApiService} from 'services/state-top-answers-stats-backend-api.service'; +import { + States, + StatesObjectFactory, +} from 'domain/exploration/StatesObjectFactory'; const joC = jasmine.objectContaining; describe('StateTopAnswersStatsService', () => { - let stateTopAnswersStatsBackendApiService: - StateTopAnswersStatsBackendApiService; + let stateTopAnswersStatsBackendApiService: StateTopAnswersStatsBackendApiService; let stateTopAnswersStatsService: StateTopAnswersStatsService; let statesObjectFactory: StatesObjectFactory; beforeEach(() => { TestBed.configureTestingModule({imports: [HttpClientTestingModule]}); - stateTopAnswersStatsBackendApiService = ( - TestBed.get(StateTopAnswersStatsBackendApiService)); + stateTopAnswersStatsBackendApiService = TestBed.get( + StateTopAnswersStatsBackendApiService + ); stateTopAnswersStatsService = TestBed.get(StateTopAnswersStatsService); statesObjectFactory = TestBed.get(StatesObjectFactory); }); @@ -59,26 +56,32 @@ describe('StateTopAnswersStatsService', () => { linked_skill_id: null, param_changes: [], interaction: { - answer_groups: [{ - rule_specs: [{ - rule_type: 'Contains', - inputs: {x: { - contentId: 'rule_input', - normalizedStrSet: ['hola'] - }} - }], - outcome: { - dest: 'Me Llamo', - dest_if_really_stuck: null, - feedback: {content_id: 'feedback_1', html: '¡Buen trabajo!'}, - labelled_as_correct: true, - param_changes: [], - refresher_exploration_id: null, - missing_prerequisite_skill_id: null, + answer_groups: [ + { + rule_specs: [ + { + rule_type: 'Contains', + inputs: { + x: { + contentId: 'rule_input', + normalizedStrSet: ['hola'], + }, + }, + }, + ], + outcome: { + dest: 'Me Llamo', + dest_if_really_stuck: null, + feedback: {content_id: 'feedback_1', html: '¡Buen trabajo!'}, + labelled_as_correct: true, + param_changes: [], + refresher_exploration_id: null, + missing_prerequisite_skill_id: null, + }, + training_data: [], + tagged_skill_misconception_id: null, }, - training_data: [], - tagged_skill_misconception_id: null, - }], + ], default_outcome: { dest: 'Hola', dest_if_really_stuck: null, @@ -95,13 +98,13 @@ describe('StateTopAnswersStatsService', () => { placeholder: { value: { content_id: 'ca_placeholder_0', - unicode_str: '' - } + unicode_str: '', + }, }, - rows: { value: 1 }, + rows: {value: 1}, catchMisspellings: { - value: false - } + value: false, + }, }, solution: null, }, @@ -122,20 +125,30 @@ describe('StateTopAnswersStatsService', () => { }; const spyOnBackendApiFetchStatsAsync = ( - stateName: string, - answersStatsBackendDicts: AnswerStatsBackendDict[]): jasmine.Spy => { - const answersStats = answersStatsBackendDicts.map( - a => AnswerStats.createFromBackendDict(a)); - return spyOn(stateTopAnswersStatsBackendApiService, 'fetchStatsAsync') - .and.returnValue(Promise.resolve(new StateTopAnswersStats( - {[stateName]: answersStats}, {[stateName]: 'TextInput'}))); + stateName: string, + answersStatsBackendDicts: AnswerStatsBackendDict[] + ): jasmine.Spy => { + const answersStats = answersStatsBackendDicts.map(a => + AnswerStats.createFromBackendDict(a) + ); + return spyOn( + stateTopAnswersStatsBackendApiService, + 'fetchStatsAsync' + ).and.returnValue( + Promise.resolve( + new StateTopAnswersStats( + {[stateName]: answersStats}, + {[stateName]: 'TextInput'} + ) + ) + ); }; it('should not contain any stats before init', () => { expect(stateTopAnswersStatsService.hasStateStats('Hola')).toBeFalse(); }); - it('should identify unaddressed issues', fakeAsync(async() => { + it('should identify unaddressed issues', fakeAsync(async () => { const states = makeStates(); spyOnBackendApiFetchStatsAsync('Hola', [ {answer: 'hola', frequency: 5, is_addressed: false}, @@ -152,23 +165,26 @@ describe('StateTopAnswersStatsService', () => { expect(stateStats).toContain(joC({answer: 'ciao', isAddressed: false})); })); - it('should reject with error', fakeAsync(async() => { + it('should reject with error', fakeAsync(async () => { const states = makeStates(); let successHandler = jasmine.createSpy('success'); let failHandler = jasmine.createSpy('fail'); - spyOn(stateTopAnswersStatsBackendApiService, 'fetchStatsAsync') - .and.callFake(() => { - throw new Error('Random Error'); - }); - - stateTopAnswersStatsService.initAsync(expId, states).then( - successHandler, failHandler); + spyOn( + stateTopAnswersStatsBackendApiService, + 'fetchStatsAsync' + ).and.callFake(() => { + throw new Error('Random Error'); + }); + + stateTopAnswersStatsService + .initAsync(expId, states) + .then(successHandler, failHandler); flushMicrotasks(); expect(failHandler).toHaveBeenCalledWith(new Error('Random Error')); })); - it('should order results by frequency', fakeAsync(async() => { + it('should order results by frequency', fakeAsync(async () => { const states = makeStates(); spyOnBackendApiFetchStatsAsync('Hola', [ {answer: 'hola', frequency: 7, is_addressed: false}, @@ -186,7 +202,7 @@ describe('StateTopAnswersStatsService', () => { ]); })); - it('should throw when stats for state do not exist', fakeAsync(async() => { + it('should throw when stats for state do not exist', fakeAsync(async () => { const states = makeStates(); spyOnBackendApiFetchStatsAsync('Hola', [ {answer: 'hola', frequency: 7, is_addressed: false}, @@ -197,14 +213,16 @@ describe('StateTopAnswersStatsService', () => { flushMicrotasks(); await stateTopAnswersStatsService.getInitPromiseAsync(); - expect(() => stateTopAnswersStatsService.getStateStats('Me Llamo')) - .toThrowError('Me Llamo does not exist.'); + expect(() => + stateTopAnswersStatsService.getStateStats('Me Llamo') + ).toThrowError('Me Llamo does not exist.'); })); - it('should have stats for state provided by backend', fakeAsync(async() => { + it('should have stats for state provided by backend', fakeAsync(async () => { const states = makeStates(); - spyOnBackendApiFetchStatsAsync( - 'Hola', [{answer: 'hola', frequency: 3, is_addressed: false}]); + spyOnBackendApiFetchStatsAsync('Hola', [ + {answer: 'hola', frequency: 3, is_addressed: false}, + ]); stateTopAnswersStatsService.initAsync(expId, states); flushMicrotasks(); await stateTopAnswersStatsService.getInitPromiseAsync(); @@ -212,7 +230,7 @@ describe('StateTopAnswersStatsService', () => { expect(stateTopAnswersStatsService.hasStateStats('Hola')).toBeTrue(); })); - it('should have stats for state without any answers', fakeAsync(async() => { + it('should have stats for state without any answers', fakeAsync(async () => { const states = makeStates(); spyOnBackendApiFetchStatsAsync('Hola', []); stateTopAnswersStatsService.initAsync(expId, states); @@ -222,45 +240,45 @@ describe('StateTopAnswersStatsService', () => { expect(stateTopAnswersStatsService.hasStateStats('Hola')).toBeTrue(); })); - it('should not have stats for state not provided by backend', - fakeAsync(async() => { - const states = makeStates(); - spyOnBackendApiFetchStatsAsync('Hola', []); - stateTopAnswersStatsService.initAsync(expId, states); - flushMicrotasks(); - await stateTopAnswersStatsService.getInitPromiseAsync(); + it('should not have stats for state not provided by backend', fakeAsync(async () => { + const states = makeStates(); + spyOnBackendApiFetchStatsAsync('Hola', []); + stateTopAnswersStatsService.initAsync(expId, states); + flushMicrotasks(); + await stateTopAnswersStatsService.getInitPromiseAsync(); - expect(stateTopAnswersStatsService.hasStateStats('Me Llamo')).toBeFalse(); - })); + expect(stateTopAnswersStatsService.hasStateStats('Me Llamo')).toBeFalse(); + })); - it('should only returns state names with stats', fakeAsync(async() => { + it('should only returns state names with stats', fakeAsync(async () => { const states = makeStates(); spyOnBackendApiFetchStatsAsync('Hola', []); stateTopAnswersStatsService.initAsync(expId, states); flushMicrotasks(); await stateTopAnswersStatsService.getInitPromiseAsync(); - expect(stateTopAnswersStatsService.getStateNamesWithStats()) - .toEqual(['Hola']); + expect(stateTopAnswersStatsService.getStateNamesWithStats()).toEqual([ + 'Hola', + ]); })); - it('should return empty stats for a newly added state', fakeAsync(async() => { + it('should return empty stats for a newly added state', fakeAsync(async () => { const states = makeStates(); spyOnBackendApiFetchStatsAsync('Hola', []); stateTopAnswersStatsService.initAsync(expId, states); flushMicrotasks(); await stateTopAnswersStatsService.getInitPromiseAsync(); - expect(() => stateTopAnswersStatsService.getStateStats('Me Llamo')) - .toThrowError('Me Llamo does not exist.'); + expect(() => + stateTopAnswersStatsService.getStateStats('Me Llamo') + ).toThrowError('Me Llamo does not exist.'); stateTopAnswersStatsService.onStateAdded('Me Llamo'); - expect(stateTopAnswersStatsService.getStateStats('Me Llamo')) - .toEqual([]); + expect(stateTopAnswersStatsService.getStateStats('Me Llamo')).toEqual([]); })); - it('should throw when accessing a deleted state', fakeAsync(async() => { + it('should throw when accessing a deleted state', fakeAsync(async () => { const states = makeStates(); spyOnBackendApiFetchStatsAsync('Hola', []); stateTopAnswersStatsService.initAsync(expId, states); @@ -270,11 +288,12 @@ describe('StateTopAnswersStatsService', () => { stateTopAnswersStatsService.onStateDeleted('Hola'); flushMicrotasks(); - expect(() => stateTopAnswersStatsService.getStateStats('Hola')) - .toThrowError('Hola does not exist.'); + expect(() => + stateTopAnswersStatsService.getStateStats('Hola') + ).toThrowError('Hola does not exist.'); })); - it('should respond to changes in state names', fakeAsync(async() => { + it('should respond to changes in state names', fakeAsync(async () => { const states = makeStates(); spyOnBackendApiFetchStatsAsync('Hola', []); stateTopAnswersStatsService.initAsync(expId, states); @@ -285,26 +304,31 @@ describe('StateTopAnswersStatsService', () => { stateTopAnswersStatsService.onStateRenamed('Hola', 'Bonjour'); - expect(stateTopAnswersStatsService.getStateStats('Bonjour')) - .toEqual(oldStats); + expect(stateTopAnswersStatsService.getStateStats('Bonjour')).toEqual( + oldStats + ); - expect(() => stateTopAnswersStatsService.getStateStats('Hola')) - .toThrowError('Hola does not exist.'); - expect(() => stateTopAnswersStatsService.onStateRenamed('Hola', 'Bonjour')) - .toThrowError('Hola does not exist.'); + expect(() => + stateTopAnswersStatsService.getStateStats('Hola') + ).toThrowError('Hola does not exist.'); + expect(() => + stateTopAnswersStatsService.onStateRenamed('Hola', 'Bonjour') + ).toThrowError('Hola does not exist.'); })); - it('should recognize newly resolved answers', fakeAsync(async() => { + it('should recognize newly resolved answers', fakeAsync(async () => { const states = makeStates(); - spyOnBackendApiFetchStatsAsync( - 'Hola', [{answer: 'adios', frequency: 3, is_addressed: false}]); + spyOnBackendApiFetchStatsAsync('Hola', [ + {answer: 'adios', frequency: 3, is_addressed: false}, + ]); stateTopAnswersStatsService.initAsync(expId, states); flushMicrotasks(); await stateTopAnswersStatsService.getInitPromiseAsync(); - expect(stateTopAnswersStatsService.getUnresolvedStateStats('Hola')) - .toContain(joC({answer: 'adios'})); + expect( + stateTopAnswersStatsService.getUnresolvedStateStats('Hola') + ).toContain(joC({answer: 'adios'})); const updatedState = states.getState('Hola'); updatedState.interaction.answerGroups[0].rules.push( @@ -314,65 +338,71 @@ describe('StateTopAnswersStatsService', () => { inputs: { x: { contentId: 'rule_input', - normalizedStrSet: ['adios'] - } - } + normalizedStrSet: ['adios'], + }, + }, }, 'TextInput' - )); + ) + ); stateTopAnswersStatsService.onStateInteractionSaved(updatedState); - expect(stateTopAnswersStatsService.getUnresolvedStateStats('Hola')) - .not.toContain(joC({answer: 'adios'})); + expect( + stateTopAnswersStatsService.getUnresolvedStateStats('Hola') + ).not.toContain(joC({answer: 'adios'})); })); - it('should add new answer when Interaction Id\'s are not equal', - fakeAsync(async() => { - const states = makeStates(); - spyOnBackendApiFetchStatsAsync( - 'Hola', [{answer: 'adios', frequency: 3, is_addressed: false}]); - stateTopAnswersStatsService.initAsync(expId, states); - flushMicrotasks(); - await stateTopAnswersStatsService.getInitPromiseAsync(); - - const updatedState = states.getState('Hola'); - updatedState.interaction.answerGroups[0].rules.push( - Rule.createFromBackendDict( - { - rule_type: 'Equals', - inputs: { - x: { - contentId: 'rule_input', - normalizedStrSet: ['adios'] - } - } + it("should add new answer when Interaction Id's are not equal", fakeAsync(async () => { + const states = makeStates(); + spyOnBackendApiFetchStatsAsync('Hola', [ + {answer: 'adios', frequency: 3, is_addressed: false}, + ]); + stateTopAnswersStatsService.initAsync(expId, states); + flushMicrotasks(); + await stateTopAnswersStatsService.getInitPromiseAsync(); + + const updatedState = states.getState('Hola'); + updatedState.interaction.answerGroups[0].rules.push( + Rule.createFromBackendDict( + { + rule_type: 'Equals', + inputs: { + x: { + contentId: 'rule_input', + normalizedStrSet: ['adios'], + }, }, - 'MultipleChoiceInput' - )); - updatedState.interaction.id = 'MultipleChoiceInput'; + }, + 'MultipleChoiceInput' + ) + ); + updatedState.interaction.id = 'MultipleChoiceInput'; - // Pre-checks. - expect(stateTopAnswersStatsService.getStateStats('Hola')) - .toEqual([new AnswerStats('adios', 'adios', 3, false)]); + // Pre-checks. + expect(stateTopAnswersStatsService.getStateStats('Hola')).toEqual([ + new AnswerStats('adios', 'adios', 3, false), + ]); - // Action. - stateTopAnswersStatsService.onStateInteractionSaved(updatedState); + // Action. + stateTopAnswersStatsService.onStateInteractionSaved(updatedState); - // Post-Check. - expect(stateTopAnswersStatsService.getStateStats('Hola')).toEqual([]); - })); + // Post-Check. + expect(stateTopAnswersStatsService.getStateStats('Hola')).toEqual([]); + })); - it('should recognize newly unresolved answers', fakeAsync(async() => { + it('should recognize newly unresolved answers', fakeAsync(async () => { const states = makeStates(); - spyOnBackendApiFetchStatsAsync( - 'Hola', [{answer: 'hola', frequency: 3, is_addressed: false}]); + spyOnBackendApiFetchStatsAsync('Hola', [ + {answer: 'hola', frequency: 3, is_addressed: false}, + ]); stateTopAnswersStatsService.initAsync(expId, states); flushMicrotasks(); await stateTopAnswersStatsService.getInitPromiseAsync(); - expect(stateTopAnswersStatsService.getUnresolvedStateStats('Hola')) - .not.toContain(joC({answer: 'hola'})); + expect( + stateTopAnswersStatsService.getUnresolvedStateStats('Hola') + ).not.toContain(joC({answer: 'hola'})); const updatedState = states.getState('Hola'); updatedState.interaction.answerGroups[0].rules = [ @@ -382,20 +412,21 @@ describe('StateTopAnswersStatsService', () => { inputs: { x: { contentId: 'rule_input', - normalizedStrSet: ['bonjour'] - } - } + normalizedStrSet: ['bonjour'], + }, + }, }, 'TextInput' - ) + ), ]; stateTopAnswersStatsService.onStateInteractionSaved(updatedState); - expect(stateTopAnswersStatsService.getUnresolvedStateStats('Hola')) - .toContain(joC({answer: 'hola'})); + expect( + stateTopAnswersStatsService.getUnresolvedStateStats('Hola') + ).toContain(joC({answer: 'hola'})); })); - it('should throw error if state does not exist', fakeAsync(async() => { + it('should throw error if state does not exist', fakeAsync(async () => { const states = makeStates(); const updatedState = states.getState('Hola'); @@ -406,34 +437,35 @@ describe('StateTopAnswersStatsService', () => { inputs: { x: { contentId: 'rule_input', - normalizedStrSet: ['adios'] - } - } + normalizedStrSet: ['adios'], + }, + }, }, 'TextInput' - )); + ) + ); expect(() => { stateTopAnswersStatsService.onStateInteractionSaved(updatedState); }).toThrowError('Hola does not exist.'); })); - it('should throw error if Interaction id does not exist', - fakeAsync(async() => { - const states = makeStates(); - spyOnBackendApiFetchStatsAsync( - 'Hola', [{answer: 'adios', frequency: 3, is_addressed: false}]); - stateTopAnswersStatsService.initAsync(expId, states); - flushMicrotasks(); - await stateTopAnswersStatsService.getInitPromiseAsync(); + it('should throw error if Interaction id does not exist', fakeAsync(async () => { + const states = makeStates(); + spyOnBackendApiFetchStatsAsync('Hola', [ + {answer: 'adios', frequency: 3, is_addressed: false}, + ]); + stateTopAnswersStatsService.initAsync(expId, states); + flushMicrotasks(); + await stateTopAnswersStatsService.getInitPromiseAsync(); - const updatedState = states.getState('Hola'); - updatedState.interaction.id = null; + const updatedState = states.getState('Hola'); + updatedState.interaction.id = null; - expect(() => { - stateTopAnswersStatsService.onStateInteractionSaved(updatedState); - }).toThrowError('Interaction ID cannot be null.'); - })); + expect(() => { + stateTopAnswersStatsService.onStateInteractionSaved(updatedState); + }).toThrowError('Interaction ID cannot be null.'); + })); it('should getTopAnswersByStateNameAsync', fakeAsync(() => { const states = makeStates(); @@ -446,13 +478,14 @@ describe('StateTopAnswersStatsService', () => { flushMicrotasks(); - stateTopAnswersStatsService.getTopAnswersByStateNameAsync( - expId, states).then( - (data) => { + stateTopAnswersStatsService + .getTopAnswersByStateNameAsync(expId, states) + .then(data => { expect(data.get('Hola')).toEqual([ new AnswerStats('hola', 'hola', 7, true), new AnswerStats('adios', 'adios', 4, false), - new AnswerStats('ciao', 'ciao', 2, false)]); + new AnswerStats('ciao', 'ciao', 2, false), + ]); }); })); }); diff --git a/core/templates/services/state-top-answers-stats.service.ts b/core/templates/services/state-top-answers-stats.service.ts index 627ab89b8366..92d27ee34875 100644 --- a/core/templates/services/state-top-answers-stats.service.ts +++ b/core/templates/services/state-top-answers-stats.service.ts @@ -17,28 +17,25 @@ * each state of an exploration. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; - -import { AnswerStats } from - 'domain/exploration/answer-stats.model'; -import { States } from 'domain/exploration/StatesObjectFactory'; -import { AnswerClassificationService } from - 'pages/exploration-player-page/services/answer-classification.service'; -import { InteractionRulesRegistryService } from - 'services/interaction-rules-registry.service'; -import { StateTopAnswersStatsBackendApiService } from - 'services/state-top-answers-stats-backend-api.service'; -import { State } from 'domain/state/StateObjectFactory'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; + +import {AnswerStats} from 'domain/exploration/answer-stats.model'; +import {States} from 'domain/exploration/StatesObjectFactory'; +import {AnswerClassificationService} from 'pages/exploration-player-page/services/answer-classification.service'; +import {InteractionRulesRegistryService} from 'services/interaction-rules-registry.service'; +import {StateTopAnswersStatsBackendApiService} from 'services/state-top-answers-stats-backend-api.service'; +import {State} from 'domain/state/StateObjectFactory'; export class AnswerStatsEntry { constructor( - public readonly answers: readonly AnswerStats[], - public readonly interactionId: string) {} + public readonly answers: readonly AnswerStats[], + public readonly interactionId: string + ) {} } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class StateTopAnswersStatsService { private initializationHasStarted: boolean; @@ -52,10 +49,10 @@ export class StateTopAnswersStatsService { private initPromise: Promise; constructor( - private answerClassificationService: AnswerClassificationService, - private interactionRulesRegistryService: InteractionRulesRegistryService, - private stateTopAnswersStatsBackendApiService: - StateTopAnswersStatsBackendApiService) { + private answerClassificationService: AnswerClassificationService, + private interactionRulesRegistryService: InteractionRulesRegistryService, + private stateTopAnswersStatsBackendApiService: StateTopAnswersStatsBackendApiService + ) { this.initializationHasStarted = false; this.topAnswersStatsByStateName = new Map(); this.initPromise = new Promise((resolve, reject) => { @@ -72,13 +69,15 @@ export class StateTopAnswersStatsService { if (!this.initializationHasStarted) { this.initializationHasStarted = true; try { - const {answers, interactionIds} = ( + const {answers, interactionIds} = await this.stateTopAnswersStatsBackendApiService.fetchStatsAsync( - explorationId)); + explorationId + ); for (const stateName of Object.keys(answers)) { this.topAnswersStatsByStateName.set( - stateName, new AnswerStatsEntry( - answers[stateName], interactionIds[stateName])); + stateName, + new AnswerStatsEntry(answers[stateName], interactionIds[stateName]) + ); this.refreshAddressedInfo(states.getState(stateName)); } this.resolveInitPromise(); @@ -113,16 +112,24 @@ export class StateTopAnswersStatsService { return this.getStateStats(stateName).filter(a => !a.isAddressed); } - async getTopAnswersByStateNameAsync(expId: string, states: States): Promise< - Map> { + async getTopAnswersByStateNameAsync( + expId: string, + states: States + ): Promise> { await this.initAsync(expId, states); - return new Map([...this.topAnswersStatsByStateName].map( - ([stateName, cachedStats]) => [stateName, cachedStats.answers])); + return new Map( + [...this.topAnswersStatsByStateName].map(([stateName, cachedStats]) => [ + stateName, + cachedStats.answers, + ]) + ); } onStateAdded(stateName: string): void { this.topAnswersStatsByStateName.set( - stateName, new AnswerStatsEntry([], '')); + stateName, + new AnswerStatsEntry([], '') + ); } onStateDeleted(stateName: string): void { @@ -150,7 +157,8 @@ export class StateTopAnswersStatsService { } const stateStats = this.topAnswersStatsByStateName.get( - stateName) as AnswerStatsEntry; + stateName + ) as AnswerStatsEntry; let interactionId = updatedState.interaction.id; if (interactionId === null) { @@ -158,17 +166,29 @@ export class StateTopAnswersStatsService { } if (stateStats.interactionId !== interactionId) { this.topAnswersStatsByStateName.set( - stateName, new AnswerStatsEntry([], interactionId)); + stateName, + new AnswerStatsEntry([], interactionId) + ); } else { - stateStats.answers.forEach(a => a.isAddressed = ( - this.answerClassificationService.isClassifiedExplicitlyOrGoesToNewState( - stateName, updatedState, a.answer, - this.interactionRulesRegistryService.getRulesServiceByInteractionId( - stateStats.interactionId)))); + stateStats.answers.forEach( + a => + (a.isAddressed = + this.answerClassificationService.isClassifiedExplicitlyOrGoesToNewState( + stateName, + updatedState, + a.answer, + this.interactionRulesRegistryService.getRulesServiceByInteractionId( + stateStats.interactionId + ) + )) + ); } } } -angular.module('oppia').factory( - 'StateTopAnswersStatsService', - downgradeInjectable(StateTopAnswersStatsService)); +angular + .module('oppia') + .factory( + 'StateTopAnswersStatsService', + downgradeInjectable(StateTopAnswersStatsService) + ); diff --git a/core/templates/services/stateful/background-mask.service.spec.ts b/core/templates/services/stateful/background-mask.service.spec.ts index 9a39654d3d18..0f83508b62a2 100644 --- a/core/templates/services/stateful/background-mask.service.spec.ts +++ b/core/templates/services/stateful/background-mask.service.spec.ts @@ -16,8 +16,7 @@ * @fileoverview Unit tests for the BackgroundMaskService. */ -import { BackgroundMaskService } from - 'services/stateful/background-mask.service'; +import {BackgroundMaskService} from 'services/stateful/background-mask.service'; describe('Background Mask Service', () => { let backgroundMaskService: BackgroundMaskService; diff --git a/core/templates/services/stateful/background-mask.service.ts b/core/templates/services/stateful/background-mask.service.ts index 183a7c62eafd..ec1433112c39 100644 --- a/core/templates/services/stateful/background-mask.service.ts +++ b/core/templates/services/stateful/background-mask.service.ts @@ -17,12 +17,11 @@ * visible. */ - -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class BackgroundMaskService { maskIsActive: boolean = false; @@ -40,6 +39,6 @@ export class BackgroundMaskService { } } -angular.module('oppia').factory( - 'BackgroundMaskService', - downgradeInjectable(BackgroundMaskService)); +angular + .module('oppia') + .factory('BackgroundMaskService', downgradeInjectable(BackgroundMaskService)); diff --git a/core/templates/services/stateful/focus-manager.service.spec.ts b/core/templates/services/stateful/focus-manager.service.spec.ts index 0fd52053237f..22d22ca9883a 100644 --- a/core/templates/services/stateful/focus-manager.service.spec.ts +++ b/core/templates/services/stateful/focus-manager.service.spec.ts @@ -16,21 +16,21 @@ * @fileoverview Unit tests for the FocusManagerService. */ -import { fakeAsync, flush, TestBed } from '@angular/core/testing'; +import {fakeAsync, flush, TestBed} from '@angular/core/testing'; -import { Subscription } from 'rxjs'; +import {Subscription} from 'rxjs'; -import { AppConstants } from 'app.constants'; -import { IdGenerationService } from 'services/id-generation.service'; -import { DeviceInfoService } from 'services/contextual/device-info.service'; -import { FocusManagerService } from 'services/stateful/focus-manager.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import {AppConstants} from 'app.constants'; +import {IdGenerationService} from 'services/id-generation.service'; +import {DeviceInfoService} from 'services/contextual/device-info.service'; +import {FocusManagerService} from 'services/stateful/focus-manager.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; describe('Focus Manager Service', () => { let focusManagerService: FocusManagerService; let deviceInfoService: DeviceInfoService; let idGenerationService: IdGenerationService; - let windowRef: WindowRef = new WindowRef; + let windowRef: WindowRef = new WindowRef(); const clearLabel = AppConstants.LABEL_FOR_CLEARING_FOCUS; const focusLabel = 'FocusLabel'; @@ -83,9 +83,10 @@ describe('Focus Manager Service', () => { } })); - it('should set focus without scrolling when schema based list editor is not' + - 'active', fakeAsync( - () => { + it( + 'should set focus without scrolling when schema based list editor is not' + + 'active', + fakeAsync(() => { spyOn(focusManagerService, 'setFocus'); spyOn(windowRef.nativeWindow, 'scrollTo'); focusManagerService.schemaBasedListEditorIsActive = false; @@ -97,9 +98,10 @@ describe('Focus Manager Service', () => { }) ); - it('should set focus without scrolling to top when schema based list editor' + - 'is active', fakeAsync( - () => { + it( + 'should set focus without scrolling to top when schema based list editor' + + 'is active', + fakeAsync(() => { spyOn(focusManagerService, 'setFocus'); spyOn(windowRef.nativeWindow, 'scrollTo'); focusManagerService.schemaBasedListEditorIsActive = true; diff --git a/core/templates/services/stateful/focus-manager.service.ts b/core/templates/services/stateful/focus-manager.service.ts index 59035fafa662..9e71885c1843 100644 --- a/core/templates/services/stateful/focus-manager.service.ts +++ b/core/templates/services/stateful/focus-manager.service.ts @@ -19,15 +19,15 @@ * somewhere in the HTML page. */ -import { EventEmitter, Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import {EventEmitter, Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; -import { AppConstants } from 'app.constants'; -import { IdGenerationService } from 'services/id-generation.service'; -import { DeviceInfoService } from 'services/contextual/device-info.service'; -import { WindowRef } from 'services/contextual/window-ref.service'; +import {AppConstants} from 'app.constants'; +import {IdGenerationService} from 'services/id-generation.service'; +import {DeviceInfoService} from 'services/contextual/device-info.service'; +import {WindowRef} from 'services/contextual/window-ref.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class FocusManagerService { // This property can be undefined but not null because we need to emit it. @@ -36,9 +36,9 @@ export class FocusManagerService { private _schemaBasedListEditorIsActive: boolean = false; constructor( - private deviceInfoService: DeviceInfoService, - private idGenerationService: IdGenerationService, - private windowRef: WindowRef = new WindowRef(), + private deviceInfoService: DeviceInfoService, + private idGenerationService: IdGenerationService, + private windowRef: WindowRef = new WindowRef() ) {} clearFocus(): void { @@ -85,5 +85,6 @@ export class FocusManagerService { } } -angular.module('oppia').factory( - 'FocusManagerService', downgradeInjectable(FocusManagerService)); +angular + .module('oppia') + .factory('FocusManagerService', downgradeInjectable(FocusManagerService)); diff --git a/core/templates/services/suggestion-modal.service.spec.ts b/core/templates/services/suggestion-modal.service.spec.ts index 6697d48ecfdf..88bf244e09c5 100644 --- a/core/templates/services/suggestion-modal.service.spec.ts +++ b/core/templates/services/suggestion-modal.service.spec.ts @@ -15,10 +15,10 @@ /** * @fileoverview Unit tests for SuggestionModalService. */ -import { TestBed } from '@angular/core/testing'; -import { ui } from 'angular'; -import { AppConstants } from 'app.constants'; -import { ParamDict, SuggestionModalService } from './suggestion-modal.service'; +import {TestBed} from '@angular/core/testing'; +import {ui} from 'angular'; +import {AppConstants} from 'app.constants'; +import {ParamDict, SuggestionModalService} from './suggestion-modal.service'; describe('Suggestion Modal Service', () => { let sms: SuggestionModalService; @@ -42,7 +42,7 @@ describe('Suggestion Modal Service', () => { action: AppConstants.ACTION_ACCEPT_SUGGESTION, commitMessage: '', reviewMessage: '', - audioUpdateRequired: false + audioUpdateRequired: false, }; sms.acceptSuggestion(uibModalInstanceMock, paramDict); @@ -55,7 +55,7 @@ describe('Suggestion Modal Service', () => { action: AppConstants.ACTION_REJECT_SUGGESTION, commitMessage: '', reviewMessage: '', - audioUpdateRequired: false + audioUpdateRequired: false, }; sms.rejectSuggestion(uibModalInstanceMock, paramDict); diff --git a/core/templates/services/suggestion-modal.service.ts b/core/templates/services/suggestion-modal.service.ts index 97edbe68b71c..cdcb5aa79120 100644 --- a/core/templates/services/suggestion-modal.service.ts +++ b/core/templates/services/suggestion-modal.service.ts @@ -16,9 +16,9 @@ * @fileoverview Service to handle common code for suggestion modal display. */ -import { Injectable } from '@angular/core'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import {Injectable} from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; export interface ParamDict { action: string; @@ -29,22 +29,22 @@ export interface ParamDict { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class SuggestionModalService { - SUGGESTION_ACCEPTED_MSG: string = ( - 'This suggestion has already been accepted.'); + SUGGESTION_ACCEPTED_MSG: string = + 'This suggestion has already been accepted.'; - SUGGESTION_REJECTED_MSG: string = ( - 'This suggestion has already been rejected.'); + SUGGESTION_REJECTED_MSG: string = + 'This suggestion has already been rejected.'; - SUGGESTION_INVALID_MSG: string = ( + SUGGESTION_INVALID_MSG: string = 'This suggestion was made for a state that no longer exists.' + - ' It cannot be accepted.'); + ' It cannot be accepted.'; - UNSAVED_CHANGES_MSG: string = ( + UNSAVED_CHANGES_MSG: string = 'You have unsaved changes to this exploration. Please save/discard your ' + - 'unsaved changes if you wish to accept.'); + 'unsaved changes if you wish to accept.'; ACTION_RESUBMIT_SUGGESTION: string = 'resubmit'; SUGGESTION_ACCEPTED: string = 'accepted'; @@ -70,9 +70,7 @@ export class SuggestionModalService { * - commitMessage: commit message for the suggestion. * - reviewMessage: review message for the suggestion. */ - acceptSuggestion( - ngbActiveModal: NgbActiveModal, - paramDict: ParamDict): void { + acceptSuggestion(ngbActiveModal: NgbActiveModal, paramDict: ParamDict): void { ngbActiveModal.close(paramDict); } @@ -96,9 +94,7 @@ export class SuggestionModalService { * - commitMessage: commit message for the suggestion. * - reviewMessage: review message for the suggestion. */ - rejectSuggestion( - ngbActiveModal: NgbActiveModal, - paramDict: ParamDict): void { + rejectSuggestion(ngbActiveModal: NgbActiveModal, paramDict: ParamDict): void { ngbActiveModal.close(paramDict); } @@ -115,11 +111,14 @@ export class SuggestionModalService { * - closed: a promise that is resolved when a modal is closed and the * animation completes. */ - cancelSuggestion( - ngbActiveModal: NgbActiveModal): void { + cancelSuggestion(ngbActiveModal: NgbActiveModal): void { ngbActiveModal.dismiss('cancel'); } } -angular.module('oppia').factory( - 'SuggestionModalService', downgradeInjectable(SuggestionModalService)); +angular + .module('oppia') + .factory( + 'SuggestionModalService', + downgradeInjectable(SuggestionModalService) + ); diff --git a/core/templates/services/suggestions.service.spec.ts b/core/templates/services/suggestions.service.spec.ts index 200242429092..4dedf3135687 100644 --- a/core/templates/services/suggestions.service.spec.ts +++ b/core/templates/services/suggestions.service.spec.ts @@ -16,9 +16,9 @@ * @fileoverview Unit tests for SuggestionsService. */ -import { TestBed } from '@angular/core/testing'; +import {TestBed} from '@angular/core/testing'; -import { SuggestionsService } from 'services/suggestions.service'; +import {SuggestionsService} from 'services/suggestions.service'; describe('SuggestionsService', () => { let suggestionService: SuggestionsService; @@ -29,26 +29,28 @@ describe('SuggestionsService', () => { describe('getThreadIdFromSuggestionBackendDict', () => { it('should return the suggestion id of the backend dict', () => { - expect(suggestionService.getThreadIdFromSuggestionBackendDict({ - suggestion_id: 'exploration.exp1.abc1', - suggestion_type: 'exploration', - target_type: 'state', - target_id: '1', - status: 'pending', - author_name: 'someone', - change_cmd: { - skill_id: 'skill_id', - state_name: 'State 1', - new_value: { - html: 'new value' + expect( + suggestionService.getThreadIdFromSuggestionBackendDict({ + suggestion_id: 'exploration.exp1.abc1', + suggestion_type: 'exploration', + target_type: 'state', + target_id: '1', + status: 'pending', + author_name: 'someone', + change_cmd: { + skill_id: 'skill_id', + state_name: 'State 1', + new_value: { + html: 'new value', + }, + old_value: { + html: 'old value', + }, + content_id: 'content', }, - old_value: { - html: 'old value' - }, - content_id: 'content' - }, - last_updated_msecs: 10000000 - })).toEqual('exploration.exp1.abc1'); + last_updated_msecs: 10000000, + }) + ).toEqual('exploration.exp1.abc1'); }); }); }); diff --git a/core/templates/services/suggestions.service.ts b/core/templates/services/suggestions.service.ts index a2632952beb7..1c98a8e1e785 100644 --- a/core/templates/services/suggestions.service.ts +++ b/core/templates/services/suggestions.service.ts @@ -16,19 +16,20 @@ * @fileoverview Service for inspecting and managing suggestion objects. */ -import { downgradeInjectable } from '@angular/upgrade/static'; -import { Injectable } from '@angular/core'; +import {downgradeInjectable} from '@angular/upgrade/static'; +import {Injectable} from '@angular/core'; -import { SuggestionBackendDict } from 'domain/suggestion/suggestion.model'; +import {SuggestionBackendDict} from 'domain/suggestion/suggestion.model'; @Injectable({providedIn: 'root'}) export class SuggestionsService { getThreadIdFromSuggestionBackendDict( - suggestionBackendDict: SuggestionBackendDict): string { + suggestionBackendDict: SuggestionBackendDict + ): string { return suggestionBackendDict.suggestion_id; } } -angular.module('oppia').factory( - 'SuggestionsService', - downgradeInjectable(SuggestionsService)); +angular + .module('oppia') + .factory('SuggestionsService', downgradeInjectable(SuggestionsService)); diff --git a/core/templates/services/svg-sanitizer.service.spec.ts b/core/templates/services/svg-sanitizer.service.spec.ts index cb46921ff0f9..9405841a2939 100644 --- a/core/templates/services/svg-sanitizer.service.spec.ts +++ b/core/templates/services/svg-sanitizer.service.spec.ts @@ -16,10 +16,10 @@ * @fileoverview Unit test for SVGSanitizationService. */ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TestBed } from '@angular/core/testing'; -import { DomSanitizer } from '@angular/platform-browser'; -import { SvgSanitizerService } from './svg-sanitizer.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TestBed} from '@angular/core/testing'; +import {DomSanitizer} from '@angular/platform-browser'; +import {SvgSanitizerService} from './svg-sanitizer.service'; describe('SvgSanitizerService', () => { let svgSanitizerService: SvgSanitizerService; @@ -39,9 +39,9 @@ describe('SvgSanitizerService', () => { providers: [ { provide: DomSanitizer, - useClass: MockDomSanitizer - } - ] + useClass: MockDomSanitizer, + }, + ], }); svgSanitizerService = TestBed.inject(SvgSanitizerService); }); @@ -50,143 +50,145 @@ describe('SvgSanitizerService', () => { expect(svgSanitizerService.isBase64Svg(invalidBase64data)).toBe(false); }); - it('should return null when a invalid base64 SVG is requested as' + - 'SafeResourceUrl', - () => { - expect(svgSanitizerService.getTrustedSvgResourceUrl( - invalidBase64data)).toBeNull(); - }); - it( - 'should return safeResourceUrl after removing invalid tags and attributes', + 'should return null when a invalid base64 SVG is requested as' + + 'SafeResourceUrl', () => { - const testCases = [ - { - // Test when SVG has an invalid tag ('circel'). - svgString: ( - ''), - expectedSvgString: ( - '') - }, - { - // Test when SVG has an invalid attribute ('data-name'). - svgString: ( - ''), - expectedSvgString: ( - '') - }, - { - // Test when SVG has an invalid self closing tag ('paht'). - svgString: ( - ''), - expectedSvgString: ( - '') - }, - { - // Test when SVG has more than one invalid tags ('pth', 'circel'). - svgString: ( - ''), - expectedSvgString: ( - '') - }, - { - // Test when SVG has more than one invalid - // attributes ('styyle', 'strokke'). - svgString: ( - ''), - expectedSvgString: ( - '') - }, - { - // Test when SVG has a hidden script. - svgString: ( - ''), - expectedSvgString: ( - '') - } - ]; - testCases.forEach(testCase => { - let dataURI = ( - 'data:image/svg+xml;base64,' + - btoa(unescape(encodeURIComponent(testCase.svgString)))); - let safeResourceUrl = svgSanitizerService.getTrustedSvgResourceUrl( - dataURI); - expect(safeResourceUrl).toEqual( - 'data:image/svg+xml;base64,' + - btoa(unescape(encodeURIComponent(testCase.expectedSvgString)))); - }); + expect( + svgSanitizerService.getTrustedSvgResourceUrl(invalidBase64data) + ).toBeNull(); + } + ); + + it('should return safeResourceUrl after removing invalid tags and attributes', () => { + const testCases = [ + { + // Test when SVG has an invalid tag ('circel'). + svgString: + '', + expectedSvgString: + '', + }, + { + // Test when SVG has an invalid attribute ('data-name'). + svgString: + '', + expectedSvgString: + '', + }, + { + // Test when SVG has an invalid self closing tag ('paht'). + svgString: + '', + expectedSvgString: + '', + }, + { + // Test when SVG has more than one invalid tags ('pth', 'circel'). + svgString: + '', + expectedSvgString: + '', + }, + { + // Test when SVG has more than one invalid + // attributes ('styyle', 'strokke'). + svgString: + '', + expectedSvgString: + '', + }, + { + // Test when SVG has a hidden script. + svgString: + '', + expectedSvgString: + '', + }, + ]; + testCases.forEach(testCase => { + let dataURI = + 'data:image/svg+xml;base64,' + + btoa(unescape(encodeURIComponent(testCase.svgString))); + let safeResourceUrl = + svgSanitizerService.getTrustedSvgResourceUrl(dataURI); + expect(safeResourceUrl).toEqual( + 'data:image/svg+xml;base64,' + + btoa(unescape(encodeURIComponent(testCase.expectedSvgString))) + ); }); + }); it('should remove the role attribute from the Math SVG string', () => { - let svgString = ( + let svgString = '' - ); - let cleanedSvgString = ( - svgSanitizerService.cleanMathExpressionSvgString(svgString)); - let expectedCleanSvgString = ( + '"/>'; + let cleanedSvgString = + svgSanitizerService.cleanMathExpressionSvgString(svgString); + let expectedCleanSvgString = '' - ); + '0Q466 150 469 151T485 153H489Q504 153 504 145284 52 289Z"/>'; expect(cleanedSvgString).toEqual(expectedCleanSvgString); }); it('should remove custom data attribute from the SVG string', () => { - var svgString = ( + var svgString = '' - ); - var cleanedSvgString = ( - svgSanitizerService.cleanMathExpressionSvgString(svgString)); - var expectedCleanSvgString = ( + '" data-custom="datacustom"/>'; + var cleanedSvgString = + svgSanitizerService.cleanMathExpressionSvgString(svgString); + var expectedCleanSvgString = '' - ); + '0Q466 150 469 151T485 153H489Q504 153 504 145284 52 289Z"/>'; expect(cleanedSvgString).toEqual(expectedCleanSvgString); }); it('should replace xmlns:xlink with xmlns in a Math SVG string', () => { - let svgString = ( + let svgString = ' { '="1" d="M52 289Q59 331 106 386T222 442Q257 442 2864Q412 404 406 402Q3' + '68 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 ' + '140Q466 150 469 151T485 153H489Q504 153 504 145284 52 289Z">' - ); - let cleanedSvgString = ( - svgSanitizerService.cleanMathExpressionSvgString(svgString)); - let expectedCleanSvgString = ( + 'g>'; + let cleanedSvgString = + svgSanitizerService.cleanMathExpressionSvgString(svgString); + let expectedCleanSvgString = '' - ); + '0Q466 150 469 151T485 153H489Q504 153 504 145284 52 289Z"/>'; expect(cleanedSvgString).toEqual(expectedCleanSvgString); }); it('should extract dimensions from an math SVG string', () => { - let svgString = ( + let svgString = '' - ); - let dimensions = ( + '0Q466 150 469 151T485 153H489Q504 153 504 145284 52 289Z"/>'; + let dimensions = svgSanitizerService.extractDimensionsFromMathExpressionSvgString( - svgString)); + svgString + ); let expectedDimension = { height: '1d429', width: '1d33', - verticalPadding: '0d241' + verticalPadding: '0d241', }; expect(dimensions).toEqual(expectedDimension); }); it('should extract dimensions from SVG string without style', () => { - var svgString = ( + var svgString = '' - ); - var dimensions = ( + 'H489Q504 153 504 145284 52 289Z"/>'; + var dimensions = svgSanitizerService.extractDimensionsFromMathExpressionSvgString( - svgString)); + svgString + ); var expectedDimension = { height: '1d429', width: '1d33', - verticalPadding: '' + verticalPadding: '', }; expect(dimensions).toEqual(expectedDimension); }); it('should throw error if height attribute is missing from SVG', () => { - var svgString = ( + var svgString = '' - ); + 'H489Q504 153 504 145284 52 289Z"/>'; expect(() => { svgSanitizerService.extractDimensionsFromMathExpressionSvgString( - svgString); + svgString + ); }).toThrowError('SVG height attribute is missing.'); }); it('should throw error if width attribute is missing from SVG', () => { - var svgString = ( + var svgString = '' - ); + 'H489Q504 153 504 145284 52 289Z"/>'; expect(() => { svgSanitizerService.extractDimensionsFromMathExpressionSvgString( - svgString); + svgString + ); }).toThrowError('SVG width attribute is missing.'); }); - it('should expect dimensions.verticalPadding to be zero if attribute style' + - 'is invalid', () => { - var svgString = ( - '' - ); - var dimensions = ( - svgSanitizerService.extractDimensionsFromMathExpressionSvgString( - svgString)); - var expectedDimension = { - height: '1d429', - width: '1d33', - verticalPadding: '0' - }; - expect(dimensions).toEqual(expectedDimension); - }); + it( + 'should expect dimensions.verticalPadding to be zero if attribute style' + + 'is invalid', + () => { + var svgString = + ''; + var dimensions = + svgSanitizerService.extractDimensionsFromMathExpressionSvgString( + svgString + ); + var expectedDimension = { + height: '1d429', + width: '1d33', + verticalPadding: '0', + }; + expect(dimensions).toEqual(expectedDimension); + } + ); it('should correctly get SVG from DataUri', () => { - const svgString = ( + const svgString = '' - ); - const dataURI = ( + '0Q466 150 469 151T485 153H489Q504 153 504 145284 52 289Z"/>'; + const dataURI = 'data:image/svg+xml;base64,' + - btoa(unescape(encodeURIComponent(svgString)))); + btoa(unescape(encodeURIComponent(svgString))); const parsedSvg = svgSanitizerService.getSvgFromDataUri(dataURI); expect(parsedSvg).toEqual( - domParser.parseFromString(svgString, 'image/svg+xml')); + domParser.parseFromString(svgString, 'image/svg+xml') + ); }); it('should get invalid svg tags and attributes', () => { - var dataURI = ( + var dataURI = 'data:image/svg+xml;base64,' + - btoa(unescape(encodeURIComponent( - '' - )))); - var invalidSvgTagsAndAttrs = ( - svgSanitizerService.getInvalidSvgTagsAndAttrsFromDataUri(dataURI)); + btoa( + unescape( + encodeURIComponent( + '' + ) + ) + ); + var invalidSvgTagsAndAttrs = + svgSanitizerService.getInvalidSvgTagsAndAttrsFromDataUri(dataURI); var expectedInvalidSvgTagsAndAttrs = { tags: ['circel'], - attrs: ['svg:widdth'] + attrs: ['svg:widdth'], }; expect(invalidSvgTagsAndAttrs).toEqual(expectedInvalidSvgTagsAndAttrs); }); it('should catch malicious SVGs', () => { const testCases: { - title: string; payload: string; expected: [number, number]; }[] = [{ + title: string; + payload: string; + expected: [number, number]; + }[] = [ + { title: 'DOM clobbering attack using name=body', - payload: '@mmrupp', - expected: [0, 6] + payload: + '@mmrupp', + expected: [0, 6], }, { title: 'DOM clobbering attack using activeElement', payload: '', - expected: [0, 6] + expected: [0, 6], }, { - title: 'DOM clobbering attack using name=body and injecting SVG + ke' + - 'ygen', - payload: ',', - expected: [0, 6] + title: + 'DOM clobbering attack using name=body and injecting SVG + ke' + + 'ygen', + payload: + ',', + expected: [0, 6], }, { title: 'XSS attack using onerror', - payload: '
    //["\'`-->]]>]
    ', - expected: [0, 5] + payload: + '
    //["\'`-->]]>]
    ', + expected: [0, 5], }, { title: 'Inline SVG (data-uri)', - payload: '
    ")\'>\n\n//["\'`-->]]>]
    ', - expected: [0, 5] + payload: + '
    ")\'>\n\n//[\"'`-->]]>]
    ", + expected: [0, 5], }, { title: 'from="javascript:Malicious code"', - payload: '
    \n\n\n\n//["\'`-->]]>]
    ', - expected: [2, 5] + payload: + '
    \n\n\n\n//["\'`-->]]>]
    ', + expected: [2, 5], }, { title: 'mXSS behavior with SVG in Chrome 77 and alike 1/2', - payload: '

    ', - expected: [0, 4] + payload: + '

    ', + expected: [0, 4], }, { title: 'mXSS behavior with SVG in Chrome 77 and alike 2/2', - payload: '

    ', - expected: [0, 4] + payload: + '

    ', + expected: [0, 4], }, { title: 'mXSS behavior with SVG Templates in Chrome 77 and alike', - payload: '